Findings
Things noticed while doing something else. A finding is the observation; a work item is the decision. Recording one must cost almost nothing, or it will not happen — so a finding is a row, not a file. Items are files; findings are rows. The asymmetry is deliberate.
Open
| Id | Sev | Pri | What | Evidence | When to act | How to fix |
|---|---|---|---|---|---|---|
| F-056 | medium | next | A session can name a completed work item as active while all session gates pass | At inspected commit 69b6059, git show 69b6059:.ai/session.md names WI-016 in progress; its archived item is done; session gate checks section presence only; WI-084 recorded 30 passing aggregate checks | Before relying on a generated current-work view, or when extending the session module | Add optional explicit active-item references and reconcile their status against the configured local backlog; do not infer staleness from age or score narrative quality; the live handoff is refreshed separately at this session’s close |
Closed — 2026-09-06 by the findings follow-up items
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-058 | An interrupted add or init could leave incomplete files without rollback, and a retry preserved those files while losing their managed ownership: written before the record, they read as “already present”, were classified kept, and never hashed (recorded by the user, 2026-09-06, from source inspection of the write loop, the phase ordering and the doctor ownership checks) | fixed | WI-098: a journal under .ai/ is written before the first file, every module file is written atomically by temporary name and rename, and the journal is cleared only after the record and renderings are saved; a retry of the same modules rewrites and hashes the files the interrupted run created, a different module set is refused until then, and doctor reports the in-flight install. Rollback of shared-file merges stays out of scope by decision |
| F-062 | The design-sync module shipped a gate that could not function: design-mirror-not-edited declared a detection no engine implements and a rungs design pull command that does not exist, so it passed on every repo by examining nothing | fixed | WI-097: the gate, its table entry and its two fixtures are retired (design-sync 1.1.0); the rule stays in the /design-pull skill’s text; module-commands-exist now reads generated_by values as command claims; the self-test inventory’s unsupported allowlist is empty and asserted so |
| F-060 | The repository’s private PreToolUse hook blocked a legitimate multi-line shell command whenever -c or -e appeared anywhere after node on a line, or inside a heredoc body being written to a file | fixed | WI-096: the inline-flag match is bounded to the interpreter’s own command segment and heredoc bodies that feed no interpreter are stripped first; a core test drives four refusals and five allowed forms through the hook. It blocked three honest commands in this session before the fix, so the severity was understated |
| F-059 | A failing equality assertion on two large values makes the test process build a character-level diff that can run for minutes and exhaust memory, and nothing mechanical prevented a test from doing it | fixed | WI-095: every test file imports test/assert.js, whose equal/deepEqual forms refuse at once, naming the digest form, to diff two values over 32 KB that differ; the tests-guard-large-equality gate holds every test file to that import and the test script to --max-old-space-size, and npm test runs files serially under the cap, which a worker was measured to inherit (2,096 MB) |
| F-063 | check full meant the full tier through the ejected runner and a repository named full through the CLI, so the launcher command the ejected README documents failed before ejection with “no gates registered” | fixed | WI-094: both surfaces parse check [path] [tier], a lone positional that is not an existing directory is the tier, the ejected runner refuses a path that is not its own repository, and the core test and packed journey drive check full, check . full, --full and an unknown tier through both |
| F-061 | rungs add <module> on an already-initialised repo rewrites .ai/rungs.toml with only that run’s dependency closure, dropping every other installed module’s entry and every recorded hash | fixed | WI-093: the record is extended, never re-derived — every existing line kept byte for byte, only unrecorded modules appended; a core test drives init → add → doctor → upgrade → add through the CLI and the packed journey runs add on a copy of the isolated consumer |
Closed — 2026-09-06 by WI-085
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-054 | The instructions module declares shell-safety hook protection but a fresh tracked consumer receives neither a Claude hook configuration nor a hook-degradation report | promoted | WI-086 implements the shell-safety engine, a rungs hook dispatch command reached through the pinned launcher, the .claude/settings.json merge for the claude harness, the render-report degradation row for harnesses without hooks, and the ejected runner’s retained hook surface |
| F-055 | The gates module promises comparison of fast_budget_ms against observed durations, but the runner does not consume the setting | promoted | WI-088 brings the ledger to ADR-0005’s schema (tier and a run id per row) and reports observed fast-tier wall-clock against the budget from doctor, as a measurement with no verdict; timeouts and cancellation stay out of scope |
| F-057 | Worktree inspection reports dirty = false when reading Git status fails, conflating unknown state with confirmed clean | promoted | WI-089 replaces the boolean with clean/dirty/unknown carrying Git’s reason, keeps prunable off unknown rows, and reproduces the failing read with a worktree whose directory was removed |
Closed — 2026-09-05 by WI-084
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-029 | The concurrency module described inherited/introduced failure attribution before that behavior was implemented | fixed | Already implemented in concurrency.ts, with independent exact-integration control hardened by completed WI-081. Reconciled during WI-084 merge preparation after its initial assessment repeated this stale row. On 2026-09-05, node --test --test-name-pattern 'land distinguishes an inherited failure' test/core.test.js passed the selected inherited/introduced/unattributable regression; the full source also handles mixed and unverified findings. This closure repairs bookkeeping, not attribution code. |
Closed — 2026-09-05 by WI-083
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-053 | The site lock resolved fast-uri@3.1.5 under four high-severity host-confusion and SSRF advisories | fixed | WI-083 updates only the transitive version, tarball URL and integrity to fast-uri@3.1.7; fresh root and site installs audit at zero, the dependency path and registry integrity are verified, and both audits now fail fast in the concrete release gate before v0.4.0 work proceeds |
Closed — 2026-09-05 by WI-082
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-034 | CRLF consumer checkouts changed gate truth and same-version upgrades dirtied the install record | fixed | WI-082 normalizes decoded text only at semantic parsing boundaries, preserves raw bytes for ownership and divergence, preserves the record’s newline and terminal-newline convention, suppresses byte-identical writes, and proves a fresh core.autocrlf=true packed consumer remains clean on origin-only and detached refs across the full OS/Node matrix |
Closed — 2026-09-05 by WI-072
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-025 | release-fragment-current could not catch a retained fragment equal to the version just published | promoted | WI-072 adds the consumer-owned CONSUMED_THROUGH boundary, reconciles it with package versions, rejects fragments at or below a concrete boundary, and proves the exact retained-0.2.0 false green fails through production runGates |
Closed — 2026-09-05 by WI-079
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-050 | rungs land can advance a checked-out green or parked branch behind that worktree’s index, and can delete a checked-out parked recovery branch | promoted | WI-079 applies canonical direct-ref and holder checks to every branch land mutates, advances integration plus green atomically, allocates recovery refs without overwriting prior work and removes automatic parked-branch deletion |
Closed — 2026-09-05 by WI-081
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-052 | rungs land could call an environment-only detached-scratch command failure inherited and advance an unverified merge | promoted | WI-081 requires a clean detached exact-integration control to reproduce each inherited finding, refuses and parks unavailable or mismatched verification, preserves complete normalized command diagnostics, and removes Rungs’ own ignored-dependency requirement from both command-claim gates |
Closed — 2026-09-05 by WI-080
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-051 | Emitted-path comparison missed APFS aliases such as ß/ss and fi/fi, allowing two targets to address one inode | promoted | WI-080 centralizes the NFKD/full-case storage key, compares native paths per segment so compatibility characters cannot manufacture structure, and proves both-order exact/structural add and stored-upgrade refusal before mutation across the full OS/Node matrix |
Closed — 2026-09-05 by WI-078
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-043 | change-requires-file lets an unchanged historical marker exempt unrelated work on every later branch touching that file | promoted | WI-078 scopes the existing substantive same-line escape hatch to marker/reason lines added or modified in the complete current branch/worktree delta, rejects inherited/renamed/copied evidence and preserves deterministic ref and NUL-safe path handling |
Closed — 2026-09-05 by WI-077
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-045 | Ejection leaves the normal aggregate launcher dependent on the pinned npm package | promoted | WI-077 switches the same committed launcher used by local instructions and generated CI to the materialized runner, preserving aggregate tier, result, exit and ledger semantics without npm or package access |
| F-042 | The ejected runner copies an incomplete TypeScript/runtime dependency closure and cannot load | promoted | WI-077 ships one dependency-bundled runner, materializes the raw metadata and substituted tables its gates consume, and proves direct plus aggregate execution in an isolated packed consumer after producer/package access is removed |
Closed — 2026-09-05 by WI-076
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-049 | rungs backlog archive can move a finished item outside the repository through an outward archive-directory junction | promoted | WI-076 takes the configured root, every rewrite leaf and every move source/destination as one untrusted mutation set; it reuses WI-073’s portable/canonical/writable-leaf boundary, validates again at apply time and proves refusal leaves in-repo links/items and outside bytes unchanged |
Closed — 2026-09-05 by WI-075
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-048 | rungs land advances a checked-out integration branch without updating that worktree’s index or files | promoted | WI-075 enforces ADR-0009’s existing no-integration-checkout precondition at the command’s mutation boundary, names every holding worktree, refuses clean and dirty cases before gates or refs move, and preserves the current compare-and-swap path only for an unheld integration branch |
Closed — 2026-09-05 by WI-074
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-047 | release-version-consistent advertises pyproject.toml as a version source but silently ignores it, so a Python version can disagree with every other release surface while the gate reports green | promoted | WI-074 takes format-aware JSON/TOML/XML extraction, explicit malformed/missing-source failures, no-source refusal, preserved exclusions, module fixtures and an exact production-path mismatch regression before any multi-ecosystem or 0.4.0 claim |
Closed — 2026-09-05 by WI-073
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-046 | A parameter substituted into an emitted filename can escape the consumer repository, allowing init or upgrade --apply to write outside the target root | promoted | WI-073 takes the full shared boundary: portable lexical rejection, canonical symlink/junction containment, complete-operation preflight, direct install and stored-record upgrade paths, and exact matrix evidence. It blocks WI-072 integration and the 0.4.0 cut rather than being narrowed to the release marker that exposed it |
Closed — 2026-09-05 by WI-070
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-041 | release-fragment-current was registered but production and ejected dispatch omitted its table key, so it examined zero fragments and passed vacuously | fixed | One dependency-free engine-table selector now serves production checks, explain, module self-tests and the generated runner; unknown engines and missing sections fail closed with no whole-document fallback. A production-path regression feeds a stale 0.1.0 fragment to a 0.2.0 package and requires failure with one examined fragment. The generated runner imports the same selector; its unrelated inability to load the complete engine dependency closure remains explicit as F-042 rather than being claimed fixed here |
| F-039 | release-changelog-fragment declared branch-change keys that its file-population engine never read, leaving five fixtures unrun and the promised obligation inert | fixed | The new change-requires-file engine observes committed, staged, unstaged and non-ignored untracked paths against a deterministic integration ref; only a fragment changed in the same work or a substantive same-line exemption satisfies shipping work. Missing configuration, refs and table sections fail closed. All five existing fixtures execute with their declared outcomes, focused regressions cover inherited/deleted/ignored fragments and ambiguous refs, and Rungs’ registered gate passes after examining 19 paths. An independent acceptance audit found no remaining blocker and exact-tip run 33954333619 passes all six OS/Node cells plus the site |
Closed — 2026-09-05 by WI-071
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-044 | The packed existing-repository journey compared a canonical installed package path with a lexical temporary prefix, so a correct isolated install failed on macOS where /var resolves through /private/var | fixed | Existing isolation and cleanup paths are now strictly realpathed into one namespace; tolerant PATH probing models POSIX symlink-before-.. and Windows lexical junction semantics separately, uses the consumer cwd, and drops ambiguous paths. Regressions cover inward/outward aliases, existing/missing descendants, relative/root/drive paths and Windows quotes. Two independent reviews approve 3c68dfb; 47/47 local tests, 29/29 gates and the 107-file package dry-run pass. GitHub Actions run 33953726257 passes all six OS/Node cells plus the site at final pushed tip fb6f034 |
Closed — 2026-09-05 by WI-069
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-040 | A same-version rungs upgrade --apply dirtied a freshly installed consumer by consuming gate-block separators and the registry’s terminal newline while reporting zero work | fixed | mergeBlock marker regexes now restrict marker whitespace to one line, preserving every byte outside the exact managed marker range. Unit tests cover unchanged middle/final blocks and a real replacement; WI-068’s unchanged packed consumer journey proves two same-version applies leave the committed tree byte-identical and clean |
Closed — 2026-08-18 while preparing the extension seam
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-037 | [provenance] has no representation for a module that was designed rather than extracted, so an author outside this package must write prose into three fields whose names assert extraction — and nothing distinguishes an honest “none” from a fabricated incident, which is the one thing the evidence rule exists to prevent | fixed | 2026-08-18. [provenance] now declares a kind: extracted — the default, so all fifteen bundled manifests are untouched — or designed, which requires a first-person rationale and refuses sources and incident outright, because half a claim reads to a later reader exactly like a whole one. This row’s own evidence was wrong in the part that mattered, and checking it became the fix’s design input: it claimed ADR-0005 tier B quotes a module’s incident, and tier B quotes the gate’s why, which add copies into .ai/gates.toml (src/cli.ts, the never-fired branch). [provenance] is validated at load and read by no other code path at all — so a kind nobody printed would have been F-038 in a new costume. That is why the two surfaces are part of this fix and not a follow-up: rungs modules marks a designed module beside its name and prints the rationale, and rungs add prints one line at install. modules/README.md rule 3 carried the same wrong justification and now carries the correction. Demonstrated end to end on a temporary sixteenth module — it loaded as designed, the listing marked it, and four malformed variants were refused (no rationale · an incident · sources · an unknown kind); test/core.test.js asserts all four plus the unchanged extracted default |
| F-038 | [conflicts] is parsed into the manifest and read by nothing, so a module declaring an incompatibility gets silence rather than a refusal | fixed | 2026-08-18. add honours [conflicts]: it names both modules and which one declared the conflict, drops the dependency closure that was pulled in only for the refused module, and exits 1 having written nothing — the shape ADR-0004 state 5 already set, overridable with --confirm-conflict. Enforced symmetrically although the declaration is one-sided, which is the whole point: a module authored outside this package can name backlog, backlog will never name it back, and requiring both to declare would make the field useless for the only case it exists for. The refusal travels up dependency edges like the paradigm one. The override was silent on the first end-to-end run and now states what it overruled — the paradigm path’s own comment forbids exactly that (an override that prints nothing is indistinguishable from a detection that found nothing), and the new code had reproduced the defect the old code documented. Verified on a scratch consumer holding backlog: refusal exits 1 with no entry in .ai/rungs.toml, and --confirm-conflict installs while saying it is overruling the author. Tests cover the declaring side, the declared-about side, both requested at once, and a repo where nothing conflicts |
Closed — 2026-08-17 by the v0.3.1 release
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-035 | v0.3.0 was published while every CI run was red, including the run for the release commit, which had already failed ten minutes before npm publish. The matrix was added so this could be checked, and then nobody checked it | fixed | 2026-08-17, as a procedure change rather than a code one — the code was never the problem. Runbook §2 gates on a local rungs check, and a local run is green on exactly one platform, so the failures were invisible to the step that was actually performed. §6 now opens with read the CI run for the commit you are about to tag, and says a red run is not automatically a stop — not looking is. Followed for v0.3.1: run 32034373692 confirmed green on 0ac4c32 by SHA before the tag. Checking by SHA rather than “the latest run” also mattered — the first wait loop caught the previous commit’s green result and would have reported the release commit as passing before its run had started, which is the same class of error as the defects this release fixes |
Closed — 2026-08-17 by the first CI matrix run
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-031 | rungs had no CI of its own — a tool whose product is “gates that run on every change” ran its own only when a person remembered, so every claim here about what passes was a claim about one operator’s Windows machine | fixed | 2026-08-17. {ubuntu, macos, windows} × node {22.18, 22} plus a site job, fail-fast: false so a red cell does not delete the answer about the others. Its first run immediately paid for itself, and the row’s own prediction was wrong in an interesting way: it guessed the failures would be in the new git-driving commands, and the real one was F-033 — a gate shipped in four of five profiles that had never worked off Windows since it was written. The workflow also shipped its own bug, running the site link check before the build that produces the dist/ it walks; that passed locally only because a previous build had left dist/ behind, which is the leftover-state class this file exists to catch |
| F-036 | Three more gates had never worked off Windows, for one shared reason: new URL(import.meta.url).pathname.slice(1) in three places. The .slice(1) strips a leading / — right for /C:/…, and on Linux it turns /home/runner/… into the relative home/runner/…. Seen on the first Linux gate run, 2026-08-17 (run 32033015232 — earlier runs died in npm test and never reached the gates): loadAllModules resolved nothing, so skills-spec-pure and skills-description-routes reported the WI-058 opt-in as non-spec key 'disable-model-invocation', and gates-self-tests-both-directions reported release-version-consistent and release-fragment-current as having no fixtures when both have four | fixed | 2026-08-17. One CLI_MODULES constant built with fileURLToPath — which the rest of the codebase already used — replaces all three. The shape is the same as F-033: a Windows-only path assumption, invisible to every local run, in gates whose whole job is telling a repo the truth about itself. Also fixed alongside: changelog.d/ became empty when the 0.3.0 fragment was consumed, git does not track empty directories, and the runbook links to it — so gates-links-resolve went red on a fresh checkout while passing locally where the empty directory lingers. A tracked .gitkeep explains why it exists |
| F-033 | backlog-merged-status had never worked on Linux or macOS. It ran git branch --merged <b> --format=%(refname:short) through a shell, and unquoted parentheses are a bash syntax error — so the command threw, the catch fired, and the gate reported “cannot read git branches; status not reconciled” as a finding on every non-Windows repo | fixed | 2026-08-17, found by the CI matrix on its first run. The gate ships in backlog, rung 1, present in four of five profiles — so every non-Windows repo installed at tracked or above carried a permanently failing gate, and this repo could not see it because execSync on Windows goes through cmd.exe where parentheses are ordinary characters. Every shell-invoked git in engines2, engines3 and lifecycle is now execFileSync with an argv array, which also stops branch names and merge-driver names from reaching a shell as data. This is the single most impactful defect the matrix could have found, and it was found in 43 seconds |
Closed — 2026-08-17 by the backlog sweep
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-030 | The ci module shipped a workflow running check --tier full --reporter github. --tier is not a flag and --reporter does not exist, so both values were parsed as positionals: every run checked a directory named full and exited 1 with “no gates registered — is this a rungs repo?” | fixed | 2026-08-17. ci is in three of five profiles, so every repo installed at disciplined or above had a CI job that failed on every push and blamed the repo for it. Template now runs plain check; modules/ci 1.0.0 → 1.1.0. The gate shipped hours earlier did not catch this, which is the more useful half: module-commands-exist looked only at code spans in .md/.toml, and a workflow template is neither. Widened to (a) read every file under modules/, treating a bare rungs … in a non-prose file as a command claim because that file is executed, and (b) validate flags against the FLAGS table in src/cli.ts. Both real defects here were flag defects, not command defects. Proven by restoring the broken line and watching the gate name both flags |
| F-032 | rungs backlog archive could never archive an epic whose children had all landed, and skipped any item whose filename ends in -template.md | fixed | 2026-08-17, two independent defects in one command, both found by running it rather than reading it. The epic hold searched only items/ for children, so an archived child read as unfinished — WI-037 was held for “unfinished children: WI-038, WI-039, WI-040, WI-042, WI-043”, all five done in archive/. The more of an epic that landed, the more stuck it got. The skip regex was /TEMPLATE\.md$/i against the whole path, matching WI-010-framework-extraction-template.md; that item was done and invisible on every run since the command shipped, while the command reported “nothing to archive”. A regex anchored to the wrong end reads as careful and is not. Both have regression tests, including one asserting a genuinely missing child still holds — an unknown is not evidence of completion |
Closed — 2026-08-17 by the command audit
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-026 | The concurrency module’s documented workflow is four commands that do not exist — session start, land, preflight, worktrees — including in fragments/AGENTS.md, which merges into the consumer’s agent entry document and tells the agent “never git merge by hand” | fixed | Promoted to WI-062 2026-08-17 and closed the same day by building all four, against the recommendation recorded in the item. The manual-rewrite option was cheaper and was rejected on the module’s own terms: rung 5 is justified by the loop being mechanised, and a manual protocol at rung 5 is a document. The decision is ADR-0009 — the first time rungs drives git — with three rules that bound it: verify before you advance, never destroy (only refuse), never hold the integration branch. Exercised end to end in a scratch repo: a red merged tree left main bit-for-bit unchanged and parked the merge on integ/feature/wi-002; a conflict refused and named the file; a stale lock was taken over and said so; the green ref moved only after a clean gate run, and the next session start cut from it. module-commands-exist now derives both the command list and the subcommand guards from src/cli.ts, so the class cannot recur — and it caught a real span on its first run |
| F-027 | rungs setup <path> reported success and wrote git config into the current directory instead, because the path sat in the unvalidated subcommand slot | fixed | 2026-08-17. setup now refuses any subcommand but git and names the correct form, exactly as backlog has always done for archive — the asymmetry between the two subcommand-taking commands was the entire cause. Regression test in test/package.test.js asserts the mistaken form exits 1, that neither repo is configured by it, and that setup git <path> still targets what it names; verified by disabling the check and watching it fail |
| F-028 | --set refused a malformed key but not an unknown module or parameter name, then printed set nosuch.param = 1 as though it had applied | fixed | 2026-08-17. Both names are checked against the loaded module set, which was already in scope — an unknown module lists the fifteen, an unknown parameter lists what that module actually takes, so a mistyped core_budgets sees core_budget in the reply. The existing malformed-key check already carried the reasoning (a dropped --set “proceeded with the default and looked successful”); this is the same failure one level up. Regression test covers both rejections and that a real override still applies |
Closed — 2026-08-17 by WI-060
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-023 | release-version-consistent globs */package.json under all-agree, so a sibling deliberately versioned on its own reads as a disagreement. It is why the gate was held back at the 0.2.0 cut | fixed | 2026-08-17, both halves. The engine now takes an exclude list, empty by default — which packages share a version is a judgement about the repo, and guessing it is what turns a healthy layout red. all-agree is untouched, so the gate still needs no opinion about which file is right. This repo took the other branch: rather than excluding its docs site, the site version is aligned to the package, so the gate genuinely checks two manifests instead of being told to look away. The cost is a second bump every release and the gate is what makes forgetting it impossible; the runbook §4 table carries the row. Four self-test fixtures, all executing — including one asserting that excluding one sibling does not blind the gate to a different one, since an exclude that silently widened would be worse than no gate. Proven by pointing the exclusion at a non-matching path and watching the pass fixture report a mismatch |
| F-024 | release-version-consistent’s finding told whoever hit it to run \rungs release sync-version“ — a command that does not exist and never has, in the message a releaser sees at the moment they are least able to check | fixed | 2026-08-17, found while fixing F-023. The autofix key is removed rather than the command written: a fix-it command is a real feature with its own design, and shipping a message that names a missing one is strictly worse than a message that names none. The finding now prints each file beside the version it states — package.json=1.2.0, web/package.json=1.1.0 — which is what you need to repair it by hand, and it suggests exclude when none is set. The engine appends an autofix hint only when one is actually declared |
| F-020 | The shipped cut-release skill told every consumer to gate a release with rungs check --tier full — a flag the CLI does not accept, naming a tier no gate carried, so it selected zero gates and reported no gates registered — is this a rungs repo? about a repo holding 25 | fixed | 2026-08-17, all three parts. The message now names the tier and the count it declined to run; cut-release §2 says rungs check and says why the tier form was wrong; and the semantics are settled in ADR-0008 — a tier is an ordered level, so full is a superset of fast, an untiered gate runs in every tier, and a tier the registry does not declare is refused with a non-zero exit instead of passing empty. The original triage called part 1 “probably an ADR” and deferred it out of the release; it was pulled back in because the release is exactly what the defect breaks. Regression coverage in test/package.test.js asserts the superset, the refusal, and that the no-registry message still exists for the repo that genuinely has none — verified by disabling the branch and watching it fail |
| F-021 | Every prose version and count claim on the public surfaces was hand-typed and ungated, and all of them had drifted — the README and roadmap named v0.1.2 as public latest two days after v0.1.3 reached npm | fixed | 2026-08-17, by splitting the claim in two rather than gating all of it. Derivable claims are gated: docs-version-claims (scripts/check-doc-claims.mjs) holds the prepared version, the CLI size and the command count in README.md and docs/roadmap.md against package.json, wc -l src/*.ts and rungs --help. Each claim states which sentence it is, as a pattern, because a scan for version-shaped strings would fire on “first published 2026-08-14 at v0.1.0” — history, and correct; a pattern that stops matching fails too, so a claim reworded out of existence cannot silently become unverified. Version claims are exact, size claims carry a 10% tolerance — an exact line count would fail the build on every source edit, which is how a gate teaches people to bypass it, and the count moved 4,523 → 4,715 inside this change. The underivable claim was de-duplicated instead: the published version needs the registry, the runner does no network, so README and roadmap no longer restate it and link to the versions page, which is now the only place that tracks it. Verifying that one against npm view dist-tags stays a runbook step, and check-doc-claims.mjs says so in its own header so green is not read as “every number here is verified” |
| F-022 | A changelog fragment consumed by an earlier release and never deleted reads as unreleased work in the next one; changelog.d/0.1.1.md survived two releases | fixed | 2026-08-17. New changelog-freshness engine and the release-fragment-current gate it backs, shipped in the release module (1.0.0 → 1.1.0) and registered in this repo so rungs gates its own release with it. A fragment naming a version below the one being prepared fails; one at or above it passes, because a fragment for a higher version is a branch that decided it is a minor before the bump. Filenames that are not versions are ignored — the module’s own fixtures use 42.feature.md, and a gate that refuses a naming convention it was not asked about is one people turn off. Four self-test fixtures, and they execute: closing this required a fixture builder for the shape and a deparam bridge, because the self-test runner reads the module’s raw table where paths are still {{changelog_dir}}. It also exposed that tableKeyFor is a hand-kept map with a ?? engine fallback, so a missing entry silently handed the engine the whole table and the gate reported “did not fire” about its own harness |
Closed — 2026-08-16 by WI-059
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-018 | Self-test fixtures were declared and never executed, and three rounds of fixing mismatches never reached zero, so the runner stayed unwired | promoted | WI-059, 2026-08-16. This row’s own diagnosis was wrong and checking it was the first step: it claimed the runner was sound and the wiring broken, on evidence from a direct call made with engine = 'sections' — inferred from the gate’s name, where the registry says frontmatter-schema. The two paths were never running the same thing. Four defects then fell out, all the same family: session-sections-present declared an engine whose table its module does not have, so it passed by examining nothing on every run since it shipped — pass … 0ms with no examined count, printed all along; [register_schema.open] was read by nothing, so the Open table’s Sev/Pri/Evidence rules had never been enforced; the table matcher was a substring test, so resolve-open-findings in a filename made a Closed section match the Open schema; and the runner did not bridge opted_in. Now ok 17 · mismatch 0 · unrun 45, wired in, and verified by flipping a fixture’s expectation and watching the gate fail. This also completes F-006, which was promoted to WI-045 under WI-044 and is what started the chain: fixtures now execute on every rungs check, and the 45 that cannot be reproduced are named on every run rather than silently skipped |
Closed — 2026-08-16 by WI-058
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-019 | extensions_allowed_from was declared in the skills gate table and implemented nowhere — and [skills.<name>] was not parsed into the manifest at all, so the opt-in meant to attach a portability cost to a decision attached to nothing. work-item creates branches and merges, and its manifest reason for opting out of model invocation had been inert since it was written | promoted | WI-058, 2026-08-16, fixed the same day. It was missing at four layers, not one: not parsed, not emitted into the SKILL.md, not read by the gate, and documented in modules/README.md regardless. Implemented at all four. Injection happens at emit rather than in the source skill, so the file stays spec-pure per ADR-0001 and the extension stays attached to the module that took the portability cost. Two code paths emit skills — emittedFiles and addModule — and patching only the first left add writing the un-extended file, so an install and an upgrade would have produced different content for one skill; caught by installing into a scratch repo and finding the key still absent. The gate now accepts an opted-in key, still refuses one nobody opted into, and refuses it again when the rule is absent |
Closed — 2026-08-16 by WI-055
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-017 | rungs upgrade --apply did not update .ai/rungs.toml, so a repo on 1.2.0 described itself to its owner as 1.1.0 and planUpgrade offered the same move forever | promoted | WI-055, 2026-08-16, fixed the same day. Surgical line-level update — the version line per upgraded module, plus hash entries only for files that run actually rewrote — rather than writeInstallRecord, which re-derives the whole record and would stamp our hash onto a diverged file, flipping it to current so the next upgrade overwrote an edit ADR-0004 promises never to touch. Verified end to end with the divergence in place: version moved, the diverged file’s hash was unchanged, and it was still reported as diverged afterwards — both halves, because a surviving hash alone would not prove the classification held |
Closed — 2026-08-16 by WI-054
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-016 | rungs upgrade --apply rewrote a module’s files and never its gates, so a version that added, removed or renamed one left .ai/gates.toml on the old block and reported success. Every consumer repo that upgraded a module silently kept the old gate set | promoted | WI-054, 2026-08-16, fixed the same day. Reproduced end to end on a scratch consumer before any change — session 1.1.0 → 1.2.0 with a new gate: registry 20 → 21 entries, block moved to session@1.2.0, and rungs check on that repo went 19 → 20, so the gate runs rather than merely appearing. The row named one defect and the reproduction found two: the apply step was also guarded by if (apply && stale), and a version that only adds a gate has no stale file, so nothing ran at all — fixing the first without the second would have left the reported case broken. Removal was tested rather than assumed: a gate deleted from a manifest leaves the registry. The record half is F-017 |
Closed — 2026-08-16 by WI-047
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-015 | rungs backlog archive was named in three files shipped into every consumer repo — two of them saying “never by hand” — and the command did not exist. 39 done items sat in items/; archive/ held only its README | promoted | WI-047, 2026-08-16, implemented the same day. Implemented rather than documented away: rewriting §8 to describe a manual move would delete a capability from every scaffolded repo at once to make one sentence true. Links are resolved from the citing file’s directory rather than pattern-matched, because the same target appears as items/X.md, ../items/X.md and X.md. Trialled on two clones first, which caught a doubled destination path and a version that wanted to rewrite 334 links across 58 files including module templates — a repo-wide reflow disguised as an archive. Final run: 39 items, 135 links, 37 files; rungs check 21 pass and the site’s 1,721 links still 0 broken |
Closed — 2026-08-16 by WI-044
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-001 | backlog-merged-status fired on a branch cut but not yet committed to — trivially an ancestor of main, so it read as merged. Hit four times in two days, always on the happy path between git switch -c and the first commit | fixed | Merged now means is an ancestor and is some merge commit’s second parent. The fix this row proposed was wrong and measuring it is what caught that: implemented as “has commits ahead of base” it never fires again, because after any merge a branch is zero ahead — it deletes the gate while looking like a fix. Verified three ways in a throwaway repo (cut-and-silent, cut-with-main-advancing-and-silent, merged-with-work-and-fires) plus a unit test. Known gap stated in the code: a fast-forward merge that keeps the branch reads as having landed nothing |
| F-003 | Nothing verified the shas site/src/design-system/VENDORED.md records, so a directory saying “do not edit” could be edited and its own provenance line would not say so | fixed | site/scripts/check-vendored.mjs re-hashes all 33 listed files; registered as the site-vendored-unedited command gate, placed outside every managed block so upgrade cannot rewrite it. The hash function is duplicated from the generator rather than imported — a checker sharing the producer’s hashing cannot catch the producer hashing wrongly, which is the mistake WI-042 was opened for. All 33 currently match |
| F-006 | Gate self-tests are declared and never executed. gateMeta string-matches the table to confirm both expect blocks exist; nothing runs a fixture | promoted | WI-045, 2026-08-16. Not fixed here because it is two problems: 27 fixtures are runnable text, and the rest are ~8 bespoke structured shapes ({workflows, similarity}, {worktrees}, {values}, {matching_files}) each needing its own synthesizer. A runner covering only the text half would report a passing self-test suite while most fixtures stayed unrun — this finding again, one level down |
| F-007 | backticked_paths was named in link_integrity.check and implemented nowhere, so gates-paths-exist silently ran the same markdown-link scan as gates-links-resolve and reported every finding twice | fixed | Implemented, not collapsed. This row called the collapse “smaller and honest”; the collapse would also have deleted gates-paths-exist’s distinct provenance — hexguard’s instruction files naming paths across 105 packages — which the link scan does not cover. First run produced ten findings on this repo, all ten false (slash commands, WI-### placeholders, illustrative directories, bare filenames); narrowed to the incident’s shape — contains /, has a file extension, resolved against both repo root and the citing file’s directory — which takes it to zero while still catching a real stale path. It fired immediately on real content: F-003’s own gate description, in the same branch, cited src/design-system/VENDORED.md for a file at site/src/design-system/VENDORED.md. The table is now one entry per gate id, so the double-reporting is gone at source |
| F-011 | The Console component renders real output · <command> for text nothing verifies. Two of the three landing-page blocks were fabricated | promoted | WI-046, 2026-08-16. The interim this row proposed — rename source — turns out not to be available: site/src/design-system/ is vendored, says “do not edit”, is now sha-gated by F-003’s own fix, and its export is gitignored and absent from this checkout. So any fix is a re-vendor or a call-site mechanism, which is a design decision rather than a rename |
| F-012 | README and ADR-0005 Tier B say doctor quotes a never-fired gate’s incident; the implementation printed it from check | fixed | Moved to doctor. The ADR does not merely name the command, it gives the reason — “They must be pull (doctor), never push; no output during normal runs” — and check is the normal run, so Tier B was pushing inside the feature that forbade pushing. Verified: check output contains no Ledger questions, doctor output does. The README’s example also named check-findings-register, which is not a gate id here; now audit-output-is-rows, which is |
| F-013 | astro dev served no stylesheet on Windows — 49 consecutive 403s on one page load, so the site was unstyled and could not be reviewed locally | fixed | fileURLToPath(new URL("../", import.meta.url)) replaces .pathname, which on Windows returns /C:/… and resolved to C:/C:/…. Verified in the browser after the change: 3 stylesheets load, h1 computes to Barlow 46px on the themed background, 0 console errors |
Closed — earlier
| Id | What | Disposition | Reason |
|---|---|---|---|
| F-014 | ADR-0004 state 5 — “add prints the comparison and stops” — had no implementation. paradigm was read by doctor and by nothing else, so add wrote straight over a repo that solves the problem another way. Not new to WI-039: the milestones paradigm had the same hole since the CLI shipped, so no paradigm had ever stopped an install | promoted | WI-043, 2026-08-16, merged the same day. It took the whole mechanism — refuse by default with the note and compare printed, --confirm-paradigm to override, refusal propagating along dependency edges (audit → findings → backlog), and the refusal applying under --dry-run, which --confirm-threshold beside it does not do. It did not take that threshold asymmetry: --confirm-threshold still installs under --dry-run, which looks like the same bug and is unexamined |
| F-008 | F-004 was marked fixed on main, but its site-schema fix existed only on the unmerged feature/site-logo-and-icons branch, so builds from main still rejected backlog lifecycle statuses | fixed | Commit 655eacb cherry-picked the existing fix 4acb73d onto main on 2026-08-15. npm run build && npm run check then built 53 routes and checked 469 internal links with 0 broken on main; after WI-010 rebased, the same commands built 55 routes and checked 497 internal links with 0 broken. F-004’s disposition is now true in git. |
| F-002 | npm test pointed at a missing test directory and could not verify the CLI | fixed | WI-029 added focused tests for substitution, parameter resolution, managed blocks, marker syntax, and the module manifest audit under test/core.test.js, and changed the script to execute test/*.test.js; npm test now passes 5/5. |
| F-005 | A markdown file containing any {{token}} had every link exempted from gates-links-resolve and gates-paths-exist — 16 non-excluded files, eight of which ship to consumer repos | promoted | WI-008, 2026-08-15. The item took the full mechanism — move the token test from the file to the individual link, and stop matching links quoted inside code spans, which the file-level skip had been masking. It did not take the two things the investigation surfaced alongside: self-tests being declared but never run (F-006) and backticked_paths being configured but unimplemented (F-007) |
| F-004 | The wiki’s content schema accepted only the four ADR statuses, so the first backlog item to reach status: done failed the whole site build — astro build aborted on WI-001 with “Invalid enum value … received ‘done’” (2026-08-15). Backlog items joined the wiki after the enum was written; marking one done was enough | fixed | site/src/content.config.ts now enumerates the union of both vocabularies — the ADR statuses plus the lifecycle declared in docs/backlog/TEMPLATE.md (deferred, planned, in_progress, review, done). Left as an enum rather than z.string() so a typo’d status still fails the build. Found while re-vendoring the design system, on a build that was already broken at a006b5c |
| F-009 | The site lock resolved Astro and transitive Sharp/esbuild versions covered by three advisories | fixed | WI-034 upgraded Astro to 7.2.2 with compatible @astrojs/markdown-remark, @astrojs/react, and @astrojs/check integrations, refreshed the lock, ran npm audit fix, and verified npm audit reports 0 vulnerabilities plus a passing build/check. |
| F-010 | Astro’s content loader warned that duplicate collection ids were being overwritten | fixed | WI-034 moved the wiki to Astro 7’s unified markdown processor and made the source-map root resolve from the site package cwd so bundled builds read the repository corpus. The current build emits no duplicate-id warnings; npm run check reports 0 diagnostics and 1,203 links with 0 broken. |
Recording one
Use /record-finding, or add a row directly. Required:
- Sev —
high(wrong output, data loss, security) ·medium(wrong behaviour, contained) ·low(cost, clarity, tidiness) - Pri —
now·next·someday. Severity is about the problem; priority is about us. - Evidence — a path, a command, a count. A finding with no evidence is a hunch, and the next reader cannot tell the difference. If you cannot produce evidence, say so in the row.
- When to act — the trigger, not a date. “Before the next release”, “if this recurs”.
- How to fix — enough that someone else could, or an explicit “unknown”.
Closing one
Every finding leaves the Open table by one of three dispositions, and each carries a written reason:
| Disposition | Means | Reason must say |
|---|---|---|
| promoted | It became a work item | Which item, and what scope it took |
| fixed | It was resolved directly | What changed, and where |
| dismissed | It is not a problem, or not one worth solving | Why not — this is the one people skip, and it is the one that stops the same observation being recorded again next month |
rungs check refuses a closed finding with no reason.
What this register does not do
- It does not prioritise. A
nowpriority is a claim by whoever typed it. - It does not prove anything is fixed.
fixedmeans someone said so. - It counts what was recorded, not what was noticed. A quiet register and an unobserved repo look identical from here.