docs: the agent lifecycle in this repo, traced from the configs themselves - #1638
docs: the agent lifecycle in this repo, traced from the configs themselves#1638njrini99-code wants to merge 7 commits into
Conversation
Two owner-requested config changes. AUTO MEMORY ON. `.claude/settings.json` had `autoMemoryEnabled: false`, which overrode the user-level `true`. It was set by commit e8f3b77 — a commit about consolidating two constitutions whose message never mentions auto memory, so it was an undocumented side-effect rather than a decision. Turning it back on is deliberate this time: auto memory is capped at 200 lines, self-prunes, and holds exactly the category (corrections, project context not derivable from code) that `memory/`'s hand-written prose was trying to be. Takes effect next session. IGNORE graphify-out/. `graphify-out/graph.json` is the single largest object in this repo's history at 53.5 MiB — git-sizer flags it at its highest concern level. It is not tracked today but was not ignored either, so nothing prevented it being committed again. The existing blob stays in history; this only stops a recurrence. Verified with a probe file that the pattern matches. Neither change is recoverable from a doc: both are settings whose effect is invisible until something goes wrong, which is why the reasoning is here rather than in a comment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH
~8.6 MiB of incident-mining JSON in history (_staging-snapshot/os-stage/ incidents/mining/prs-merged.json 4.4M, prs-pre.json 4.2M) and nothing prevented it recurring. Same class as the graphify-out ignore in 7f80aed. Found by ranking every blob in history rather than trusting git-sizer's single biggest-object line, which named only graphify-out/graph.json and hid that graphify-out is actually THREE blobs totalling 155M and that .ultracode/baseballhelm/events.ndjson appears three times at 29.9M each. Verified with a probe file, because `git check-ignore` on an absent path cannot classify a trailing-slash pattern and reports NOT IGNORED for a rule that works. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH
Written by inspecting the live system: settings.json's hook wiring, the guards' actual block lists, ci.yml's aggregate `needs`, vitest.config.ts's projects, and the git object model as this repo is configured. Where it contradicts an existing doc, this one was checked. Covers the six places an agent writes bytes (only one is the repo), the session-start load order and why imports do not save context, all eleven hooks with their matchers, the branch/tree/index/refs model, what worktrees share and what they do not, why squash-merge makes `git branch --merged` useless here, the full edit-to-merged path, what CI actually gates versus what merely runs, the three separate test systems, and the ratchets. WRITING IT HIT THE DEFECT IT DOCUMENTS. The markdown ratchet failed with +306 MD013. I rewrote the doc twice to satisfy it — narrowing tables, hard-wrapping to 80 columns — and the delta did not move. Removing the file entirely produced IDENTICAL deltas: my contribution was zero. The failure is the 21 gitignored `docs/redesign/*.md` files this checkout has on disk, which main's ratchet counts because it walks the filesystem instead of `git ls-files`. That is exactly the reproducibility defect diagnosed and fixed on docs/consolidation-2026-08-27 — but the fix is on that branch, not on main, so it bit again here. Third time tonight, and the first where I had already written the fix. It is Part 7 of the doc for that reason. The doc itself is clean: 440 lines, ZERO over 80 columns, zero markdownlint violations in isolation. Verified with the file present and absent to prove the attribution rather than assert it. Also recorded in Part 7: the ratchet grandfathers ~30,000 existing violations while holding new files to zero, so adding any substantial doc to docs/ means writing it violation-free. That is a real tax on documentation and worth naming. Verified: docs:path-drift, docs:schema-drift, knowledge:globs PASS. markdown:ratchet fails on main for the pre-existing reason above, not this file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH
…s together Expands docs/AGENT_LIFECYCLE.md from 440 to 752 lines so the machine and what auditing it turned up live in one file rather than two. PARTS 1-10, HOW IT WORKS, now complete rather than summarised: - All SIX places an agent writes bytes, only one of which is the repo: session scratchpad, session-state JSONL, transcripts, auto-memory, the working tree, and .git/objects. Plus the repo directories that matter. - Session-start load order, the import trap, and the path-scoping test. - All ELEVEN hooks with their exact matchers and line counts, why bypassPermissions makes them the only enforcement, and what each blocks. - Git mechanically: branch as a 41-byte file, commit as a snapshot not a diff, the four places a file exists, what worktrees share vs keep private, and why squash-merge makes `git branch --merged` useless here. - CI: ALL 43 jobs across 13 workflows, marked by whether they actually gate. ci.yml 18 jobs / 15 in `needs`; review-gate.yml 12 / 11. The two deliberately excluded by owner decision. The skipped-need trap. Why required checks are matched by NAME and fail silently. - Tests as three systems: five vitest projects with their includes, pgTAP as the real RLS coverage, Playwright's PR-vs-main split. - The nine ratchets, per-rule not net, and the git ls-files rule. PARTS 11-16, WHAT IT FOUND: the five defects that turned out to be one defect; the four retractions this document's author owes, including wiring a gate that cannot fail inside the change meant to remove gates that do not gate; repo weight measured by ranking every blob rather than trusting git-sizer's single line; a live production bug as illustration; and the caveat that a rule being loaded is not a rule being followed. Written violation-free because the ratchet grandfathers ~30,000 existing violations while holding new files to zero: 752 lines, ZERO over 80 columns, ZERO markdownlint violations in isolation. Verified. Verified: docs:path-drift, docs:schema-drift PASS. markdown:ratchet fails on main for the pre-existing docs/redesign reason documented in Part 11.2, not for this file — proven earlier by removing the file and getting identical deltas. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH
Two additions, plus the numbering defects they exposed. PART 2 — THE STACK, and where everything comes from. Runtime pinned three ways (.node-version 22, .nvmrc 22, engines >=22.0.0, running v22.23.2 / npm 10.9.8, Deno 2.9.5 for edge functions) and NO packageManager field, so nothing pins npm itself. Core versions with supabase-js pinned EXACTLY while everything else floats. noUncheckedIndexedAccess and why the house convention is guard-then-assert. Five env files and why .worktreeinclude excludes every one. vercel.json's build/install/ignore commands. And the thing missing entirely: PRODUCTION CRONS ARE DECLARED IN vercel.json, running against production on Vercel's schedule, independent of CI. Plus npm's prebuild/postbuild lifecycle hooks — callers that no grep for a script name will ever find. PART 11 — SUPABASE AND VERCEL TOOL RULES, because they are the only tools that touch production and both are defended in THREE independent layers that fail differently: permissions.deny (literal command PREFIX, so `vercel --cwd . deploy --prod` dodges it), the guards (regex over the whole command and over MCP payloads, not suspended by allow rules or bypassPermissions), and the Review Gate at merge. settings.json is version-controlled and therefore branch-scoped — a checkout of an older branch silently removes its deny rules, which is why the important blocks are duplicated into guard-bash.sh. Supabase: one project, one SHARED production database, no staging copy. The four shapes guard-sql.sh blocks and why each matters. Why its normalizer is quote-aware — a naive comment-stripper turns `SELECT '--' as marker; DELETE FROM golf_players;` into `SELECT '`, and the unscoped DELETE vanishes before the check runs, turning a block into an allow. Why config push, db push, migration up and db reset are denied in four spellings each. Why the MCP warning is always-on rather than path-scoped. Vercel: deploymentEnabled false, so pushing is not deploying and production is an on-demand human promote. The five denied shapes in four spellings, including `alias set` because domain routing is production state even though it deploys nothing. The unenforced practical rules — --archive=tgz for the 15,000-file cap, .vercelignore REPLACING rather than extending the default set, and team-scoped env vars not appearing in `vercel env ls`. Both close on the R3 rule: an agent investigates and prepares; the owner executes. TWO NUMBERING DEFECTS I INTRODUCED AND CAUGHT. Inserting a part renumbered the Part headings but not the sub-headings (13.1, 14.2 …), which do not contain the word "Part" — leaving sections whose children disagreed with them. And my first insert landed the new part AFTER Traps, so the file read 10, 12, 11, 13. Verified by listing headings in order rather than trusting the regex, then reordered and the cross-reference repointed from 13.2 to 14.2, which I checked resolves to the section I meant. 1,015 lines, ZERO over 80 columns, ZERO markdownlint violations. One wide table became a list to stay under 80 rather than be exempted. Verified: docs:path-drift, docs:schema-drift PASS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH
…ents
"The blocking hard rules live in the custom packs" is not something anyone can
act on. Traced and enumerated instead.
AST-GREP, 10 rules. Six block: service-role key outside admin paths, bare
unprefixed table names, deep type imports from paths that do not exist,
`.limit()` above PostgREST's 1000 cap (SILENTLY truncated — you get 1000 rows
and no error), `process.env` in Deno edge functions, and a catch clause whose
whole body is a bare return. Four warn: console.log, explicit any, untagged
FIXME, getByTestId over getByRole.
SEMGREP, 11 rules. Eight block: service-role by path, logging Supabase tokens
(full user impersonation until expiry), a server action hitting Supabase
without auth.getUser() first, DELETE-then-INSERT in a save path, importing
@/lib/supabase/server into a 'use client' file, SECURITY DEFINER without a
pinned search_path, a new table without RLS in the same migration, and
hardcoded credentials in the Python helpers under tools/. Three warn: PII in
Datadog/OTel spans, a mutating action with no revalidatePath, and Capacitor
plugins missing their NS*UsageDescription.
TWO MECHANICS THAT DECIDE WHETHER A RULE EVER FIRES, and neither was written
down anywhere:
- Both packs scan CHANGED FILES ONLY, via .github/scripts/changed-files.sh.
A pre-existing violation in a file your PR does not touch will not block you
and will not be reported. These are diff-scoped gates, not ratchets — a
genuinely different contract from the nine baselines, and easy to confuse.
- The .coderabbit/semgrep/__test__/ fixtures are excluded on purpose. They are
intentionally-broken code proving the rules fire; without the exclusion every
PR touching the rule packs would block on its own fixtures.
ast-grep is downloaded at job time from a pinned GitHub release (0.44.0), not
from node_modules.
A RULE THAT EXISTS FOR THE CLASS BUT NOT THE SHAPE. no-silent-catch-fallback
selects on `catch_clause`. The live messaging bug in Part 16 is not a catch
clause — it is `if (error) { logError(...) }` with NO return, so execution
falls through and the UI renders partial data silently. Same failure class,
different syntax, rule blind to it. Worth knowing before assuming a pack covers
a category: it covers the shapes someone thought to write down.
THE SIX SUBAGENTS, with tool allowlists and models. The four reviewers are
read-only by construction — no Write, no Edit. db-migration-reviewer is the
only one on opus, and the engineering OS makes its review mandatory for schema
changes. debugger and verifier inherit the full tool set so they can run
things; verifier exists to check a completion claim by running commands rather
than trusting a summary.
Inserted without renumbering, deliberately — renumbering introduced two defects
in the previous two commits. Verified the Part sequence is unchanged 1..18 and
that the Part 16 cross-reference resolves.
1,106 lines, ZERO over 80 columns, ZERO markdownlint violations.
docs:path-drift and docs:schema-drift PASS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH
`9dea37c56` (#1635) landed on main hours after Part 16 was written, and it fixed both halves. Merging a findings doc that asserts a fixed bug is live is the exact failure class the document is about, so this corrects it — recording the correction rather than rewriting the analysis, because the gap between what I proposed and what shipped is the useful part. WHAT SHIPPED. The call site now uses toPostgrestError() and postgrestErrorContext(). The Postgres code rides on `.name`, which is the only channel the client path has: /api/log-error lifts context.error.name into metadata.errorCode, which is where extractErrorCode() reads. A context-level errorCode would have been inert. `details` and `hint` deliberately stay OUT of `.message` because fingerprints hash the message and `details` carries row-specific text, so folding it in would mint a new incident group per occurrence. MY PRESCRIPTION WAS WRONG. I wrote "no early return". The fix does not add one, and should not: the team-chat query SUPPLEMENTS the RPC, so returning early would blank a rail whose DMs loaded fine. The terminal check became `(rpcError ?? groupConvsError)`. The defect was real; the fix I implied would have introduced a different one. AND A COUNT I SHOULD NOT HAVE WRITTEN. "Ten wrappers remain, three in that one file" is not reproducible, and does not agree with the fix author's "~47 other sites" — we were counting different shapes and neither of us said which. Replaced with the query and what it returns today: 76 hits, 74 outside tests, 12 that report rather than throw, of which 3 are unrelated normalizers, and ZERO left in use-golf-messages.ts. Most of the remainder are `throw`, a different shape — a throw preserves control flow. shipping.md section 1 says never write a count into prose; I wrote one six pages after quoting the rule. Also retitled Part 16 and fixed the Part 5 cross-reference, which called the bug "live". The ast-grep blind spot that cross-reference illustrates is unchanged — `helmv3-no-silent-catch-fallback` still selects on `catch_clause` and still cannot see this shape. That instance was fixed by hand; the rule was never involved. Verified: docs:path-drift PASS (44, baseline 44), docs:schema-drift PASS (59, baseline 59), markdownlint on this file 0 issues, 0 lines over 80 columns. markdown:ratchet is RED locally and it is NOT this change. Proven, not assumed: the ratchet enumerates with readdirSync, so it walks `docs/redesign/` — 21 gitignored files holding 1,850 violations. Scoped to `git ls-files` instead, with this file included, ZERO rules sit above baseline and the total is 1,457 BELOW it. CI checks out clean, so those files do not exist there. This is the reproducibility defect the consolidation branch fixes in a3afec2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH
|
ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing |
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
) Two days of repairs left the pieces better and unable to check each other. This makes drift observable, so the next forensic pass is a command instead of a week. THE PATTERN, STATED ONCE control written -> documented -> assumed -> stops running -> docs remain -> agent trusts docs -> failure Observed, all of it: guard-bash.sh existed, unwired, deleted guard-sql.sh existed, unwired, deleted retire-worktrees.sh existed, never invoked four rule files claimed hooks that were gone worktree GC keyed on a ref GitHub deletes on merge Docs Regen opened a PR that sat open while main went stale a fixture test operated on the real repository Not one was a missing control. Every one was a control that stopped running while its documentation stayed put. WHAT THIS ADDS npm run control-plane:verify static + runtime npm run control-plane:verify:static CI-safe, wired as its own required job READ-ONLY by construction. It regenerates nothing, deletes nothing, mutates no config. The previous docs gate re-ran the generator and diffed, so the only way to learn the docs were stale was to have already fixed them — a verifier that repairs what it inspects cannot tell you what was wrong. 0 VERIFIED passed, or a named acknowledged gap 1 CONTROL FAILURE a control that should hold does not 2 UNKNOWN a required state could not be established UNKNOWN outranks FAIL and never becomes PASS. If something could not be established, "these specific things are broken" would imply everything else was checked. ACKNOWLEDGED GAP != UNKNOWN. Four gaps carry an id, an owner, a date, a reason and a closing condition in config/control-plane-gaps.json, and every run prints them so they cannot fade. MUTATION BUDGET, ENFORCED BEFORE ALLOCATION HELM_MAX_MUTATION_WORKTREES defaults to 1. Refused before `git worktree add` and before any install, so a refusal costs nothing. Classification FAILS TOWARD MUTATION: an unreadable or undeclared workspace counts. A declared release workspace escapes the budget only if it is ALSO detached — on a branch it can still be committed to. The disk reserve stopped a catastrophe; it never stopped waste. Six worktrees in one day was a concurrency problem that only became visible as a disk problem. BRANCH-GC AUTHORIZATION, RECORDED IN THE TOOL ITSELF Verdict renamed DELETE_SAFE -> DELETE_MERGED_EXACT so it states its own evidence, and the grant lives in the authority, not in a paragraph elsewhere: PR MERGED + local tip === PR head OID + not protected + not checked out Nine verdicts are explicitly excluded. The sharpest is new: NO_UPSTREAM_UNIQUE_WORK. Measured — ten branches hold up to 19 commits with no upstream, existing nowhere else. Folding those into UNKNOWN_PR conflated "the lookup failed" with "this is the only copy", and only one of those is permanent. THE GATE-LIE FROM #1672, ACTUALLY CLOSED #1672 raised the timeout and its own PR said that makes the unknown RARER, not DISTINGUISHABLE. The eleven repo sweeps now run in a dedicated `guards` project under scripts/repo-guards.mjs: PASS ran, found nothing POLICY_FAILURE ran, found violations exit 1 INFRASTRUCTURE_FAILURE did not finish — UNKNOWN exit 2 Proven end to end by injecting a real 200s hang into a real guard: exit 2, never reported as a violation. The guards are UNTOUCHED. They are 116-357 lines of bespoke matching logic each; rewriting them to fix a REPORTING defect would risk weakening the guards themselves. The lie was at the reporting boundary and is fixed there. Worth recording: vitest's JSON reporter does not carry the timeout text — a real timeout arrives as `Error: STACK_TRACE_ERROR`. A first draft scraped the human-readable stream to recover it. That did not match, and it was the wrong idea: parsing a tool's console output to make a safety decision is the brittle-regex pattern this repo has already deleted guards for. The guarantee is the DEFAULT — an unrecognised failure is INFRASTRUCTURE_FAILURE — not a pattern. TOOL AUTHORITY, AND EVIDENCE THAT EXPIRES docs/TOOL_AUTHORITY_MATRIX.md, generated. Five words that are not synonyms: CONFIGURED, CONNECTED, EXPOSED, ALLOWED, EXERCISED. ET-4 lost two days reading the first as the last. Probed live, not inferred: Sentry BOTH namespaces connected, find_organizations byte-identical. Redundant -> mcp__claude_ai_Sentry__* is authority, plugin denied. Reversible: one deny line restores it. Vercel plugin exposes ONLY authenticate -> CONFIGURED, not CONNECTED. Account connector is authority; dead namespace denied. GitHub the gh CLI is the lifecycle path, exercised three ways: MERGED #1676, OPEN #1659, and a branch with no PR. A failed lookup stays UNKNOWN_PR (#1668). Supabase sanctioned mcp__supabase__* is installed but NOT connected. Every observation carries a fingerprint of the configuration that produced it. Change a deny rule and the matching EXERCISED claims become STALE on the next regeneration. Nobody has to remember to invalidate them. Two bugs found by running it: Sentry and GitHub produced an IDENTICAL fingerprint (both slices empty), which would have made drift undetectable for both; and a service governed by nothing now says `ungoverned` rather than implying coverage it does not have. SUPABASE — A STRUCTURAL FINDING Starting the sanctioned server's OAuth flow shows its requested scopes: organizations:read projects:read database:read analytics:read secrets:read edge_functions:read environment:read storage:read Every scope is :read. That is connector-enforced capability restriction — real evidence, not a URL string, and obtainable without any write probe. It also means mcp__supabase__apply_migration almost certainly CANNOT function under this grant, while an explicit owner authorization says it may. Surfaced as gap SUPABASE_MIGRATION_GRANT_VS_READ_SCOPES rather than resolved, because resolving it means attempting a production migration to answer a documentation question. USER-GLOBAL, UNDER EXPLICIT AUTHORIZATION AND WITH A BACKUP removed mcp__plugin_supabase_supabase__{execute_sql,apply_migration} — no such plugin is installed; a grant waiting to activate on install is a trap, not a capability removed mcp__claude_ai_Supabase__apply_migration — account-WIDE mutation nobody authorized kept mcp__supabase__apply_migration — the owner's explicit grant kept mcp__claude_ai_Supabase__execute_sql as TEMPORARY_FALLBACK, named UNENFORCED, because it is the only working query path until the sanctioned one connects fixed the autoMode prose claiming a PreToolUse hook blocks destructive SQL. It never did; it had been copied from this repo's own docs. SANDBOX — MEASURED, NOT ASSUMED, AND NOT ENABLED Ten disposable probes: writes inside the project, outside it, into .claude/settings.json and .claude/hooks, reads of .env.local, and ../ traversal ALL SUCCEEDED — including paths the config lists under denyRead and denyWithinAllow. sandbox.filesystem.disabled is true and authoritative. Network sandboxing IS enforced (observed: deny network-outbound analytics.brew.sh:443). NOT enabled. The configured allowWrite covers /private/tmp/helmv3-*, ~/.npm and ~/Library/Caches/deno — not the repository — so flipping it as configured would block every worktree write, user-globally, for every project and any concurrent session. Recorded as gap SANDBOX_FILESYSTEM_DISABLED with the closing condition. No fake Bash parser was added as compensation. DOCS REGEN — created != integrated #1623 existed because main carried a stale route count (234 vs 235, after /admin/self-heal shipped) and the fix lived in a bot PR that nothing merged. That workflow's own header recorded that GITHUB_TOKEN PRs may receive no required CI, so it could not be merged confidently either. regen-docs.mjs gains a genuinely NON-MUTATING --check that computes expected blocks, diffs them, names the first differing line, and leaves the tree byte-identical. It is now a required PR-time gate. The auto-PR workflow is verification-only and creates nothing. #1623's diff is byte-for-byte what regeneration produces here, so it closes as superseded. FAILURE INJECTION — 15 cases, every one in a disposable COPY delete a configured hook -> FAIL point a hook at a nonexistent script -> FAIL break the blocking hook's matcher -> FAIL reintroduce a false safety claim -> FAIL the same claim, QUOTED -> PASS (corrections stay writable) declare a namespace never observed -> FAIL move the budget check after allocation -> FAIL remove the budget check -> FAIL change a service's config -> its observations STALE change one service -> others NOT staled a service governed by nothing -> ungoverned, not fresh inject a real 200s guard hang -> INFRASTRUCTURE_FAILURE, exit 2 inject a real banned import -> POLICY_FAILURE, exit 1 Plus a SENTINEL proving the injections cannot reach the live checkout. That is the direct regression for #1676, where a fixture test resolved its target back to the real repository and parked a live worktree. THREE THINGS THE VERIFIER CAUGHT IN ITS OWN AUTHORS' WORK 1. It reported mutation-budget-enforced FAIL against a correctly-ordered script, because indexOf matched `git worktree add` in the script's own HELP TEXT at line 15. Substring is not mechanism — the same error the enforcement generator made on its first run. Now line-based and comment-aware. 2. The matrix rendered the whole Supabase namespace as DENIED when only its ten mutators are, and folded DENIED_BY_POLICY and NOT_EXERCISED into "FAILED" — which reads as "we tried and it broke", untrue of both. 3. #1672's own test asserted `unit` must be no stricter than its siblings, because `unit` held the sweeps. They moved. The failure was the premise announcing itself, and the test now pins the invariant that survives: whichever project runs the sweeps must be the most generous. DELIBERATELY NOT DONE No UNKNOWN or no-upstream branch deleted. No production SQL write as a probe. No regex Bash/SQL pseudo-guard. #1638 not merged. sandbox.filesystem not enabled. The Repair LaunchAgent not re-armed — a green control plane is not evidence about a different unresolved runtime problem. Verified: npm run preflight 0; npm test 1265 files / 12049 passed; control-plane:verify 0 (20 pass, 0 fail, 0 unknown, 1 acknowledged gap); control-plane:verify:static 0; enforcement:check 0; docs:inventory-check 0; tool-authority:check 0; guards 32/32; check:cycles 0; shellcheck 0; repo:doctor 0. Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Closed as superseded by #1677, not merged. Two reasons it cannot land as-is:
EXTRACTED, because it was still true: your .gitignore additions for |
…lsof's silence (#1683) * fix(lifecycle): an OPEN PR's checkout needs its owner's consent, not lsof's silence Reproduced 2026-08-30: `npm run worktrees:retire` removed a concurrent session's checkout (agent/round-type-reclassify, PR #1681, OPEN). Every mechanical signal said disposable — clean, tip identical to its pushed remote, and no process whose cwd `lsof` could see. Nothing was lost, because parking is defined to keep the branch and PARKABLE already required the tip to match its remote. But the checkout had an owner and the tool could not tell, so this is a classifier defect, not a slip. The unsound step is reading silence as absence. `lsof +D` samples one instant: hasLiveProcess === true proof of activity — a sound veto hasLiveProcess === false NOT proof of inactivity — an agent session between two tool calls has no visible cwd classifyWorktree's own header stated the defect as design — "parking does NOT consult the PR at all" — so the comment is rewritten in the same commit. A file whose prose contradicts its code is the failure this program exists to remove. WHAT CHANGED New worktree verdict KEEP_PR_OWNER_INTENT_REQUIRED, deliberately neither ACTIVE (nothing proved anyone is using it) nor UNKNOWN (the PR read fine; what is missing is a decision). It is in REQUIRES_HUMAN_VERDICTS, so the standing authorization already excludes it. config/open-pr-dispositions.json gains `worktree_policy`, because the disposition LABEL was being asked to imply an ACTION readers had to infer: KEEP never parked automatically PARK_IF_REPRODUCIBLE park once clean and pushed; the branch stays A missing row, an unrecognised policy, or a disposition of ACTIVE/UNKNOWN all KEEP. #1659 — an open PR waiting on a physical-device test — is released explicitly by its owner and stays parkable, which is the case the park/retire split exists for. The registry is now current state in BOTH directions. It carried ACTIVE rows for #1623, #1638, #1679 and #1680 after they closed or merged; control-plane -verify now fails on a stale row as well as a missing one, and on a worktree_policy outside the vocabulary. That vocabulary check lives in the verifier because the lifecycle tool must fail SAFE on a malformed entry (unrecognised policy => KEEP) and would therefore never report one. #1681's row is recorded ACTIVE/KEEP, transcribed from observable state — PR OPEN, worktree checked out — by this session and not by its owner, who should confirm it. Recorded rather than left blank because leaving it blank is exactly what let --retire park it. WHAT THIS DOES NOT COVER A worktree whose branch has NO PR is still parked on reproducibility alone — the same defect class, with no PR to key a disposition on. Requiring recorded intent for every branch would make --park unable to act at all, which defeats the reason parking exists. Registered as WORKTREE_PARK_NO_PR_OWNERSHIP rather than quietly accepted. VERIFIED preflight 0 · 1269 files / 12,101 tests 0 · docs:check 0 · knowledge:check 0 · markdown ratchet 30515, unchanged · control-plane:verify VERIFIED exit 0 (was CONTROL FAILURE on open-pr-residue) · lifecycle suite 60 passed. Failure injection: disabling the ownership gate turns 8 of them red, including the end-to-end CLI case where --park refuses a fixture worktree whose PR is OPEN with no disposition. The lib was restored from a hashed backup and re-verified byte-identical. Assumption stated once: this ran in the canonical checkout rather than a worktree. HELM_MAX_MUTATION_WORKTREES=1 and the other session holds the only mutation workspace, so new-worktree.sh refuses by design; AGENTS.md permits a single session in canonical, and the concurrency hazard the worktree rule addresses does not apply because the other session is isolated in its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH * chore(dispositions): record this PR's own row — the new rule's first cost Opening #1683 made control-plane:verify red on open-pr-residue immediately, because the key set must now equal the live open-PR set exactly. That is the chosen tradeoff, not a surprise: a PR number does not exist until the PR does, so the row can only ever land in a follow-up commit. KEEP rather than PARK_IF_REPRODUCIBLE: this work runs in the canonical checkout, which classifyWorktree holds ACTIVE before consulting anything else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The registry is CURRENT STATE, so a merge makes its own row stale — this is the two-ended cost ADR-2026-08-30 records, paid on schedule. control-plane:verify reported it as a CONTROL FAILURE the moment #1687 merged (open-pr-residue: "disposition rows for PRs no longer open: #1687"), which is the check working: a current-state registry that only failed on MISSING rows would have carried an ACTIVE row for a merged PR indefinitely, exactly as this file did for #1623, #1638, #1679 and #1680 before this run. After: 3 open PRs (#1659, #1678, #1681), all owned elsewhere, all classified. control-plane:verify VERIFIED, exit 0 — 21 pass, 0 fail, 0 unknown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH
Documentation only. No behavior change, no source files touched.
Traces what actually happens between a prompt and a merged commit in this
repo — read from the configs, hooks, workflows and scripts themselves rather
than from the docs describing them. 18 parts, 1,150 lines,
docs/AGENT_LIFECYCLE.md.Why this exists
Every mechanism below is real and enforced, and most of it was written down
nowhere. Several of the docs that did describe it were wrong in ways that
change what an agent does.
The findings a reader acts on
The Review Gate packs scan changed files only. Both
.coderabbit/ast-grep/and
.coderabbit/semgrep/run through.github/scripts/changed-files.sh. Apre-existing violation in a file your PR does not touch will not block you and
will not be reported. That is a genuinely different contract from the nine
ratchets, and nothing said so.
A rule can exist for a class but miss the shape.
helmv3-no-silent-catch-fallbackselects oncatch_clause. The messagingdefect this PR documents in Part 16 was
if (error) { logError(...) }with noreturn — same failure class, invisible to the rule. A pack covers the shapes
someone thought to write down.
settings.jsondeny rules are branch-scoped. They are version-controlled,so a
git checkoutof an older branch silently removes them. The hooks do notmove with the branch. That is why the Supabase and Vercel blocks are duplicated
into
guard-bash.shrather than trusted to permissions alone — and it is worthknowing which layer is actually stopping you, because they fail differently.
Some gates are not reproducible.
markdown:ratchetenumerates withreaddirSync, so it walks gitignored trees. Scoped togit ls-filesinstead,zero rules sit above baseline and the total is 1,457 below it. The red is
caused by 21 untracked files. See "Gates" below.
npm lifecycle hooks are callers no grep finds.
prebuildandpostbuildfire by naming convention, and
vercel.jsoninvokes the build through npm, soprebuildruns on every production build. Searching for a script by name willnot find what invokes it.
What is traced
Every place bytes get written · the stack and where it comes from (Node pinned
three ways, exact vs caret deps,
noUncheckedIndexedAccess, thepostbuildtsconfig stripper) · session start · all eleven hooks · git mechanically ·
edit → merged · all 43 CI jobs and which of them actually gate · three test
systems, not one · the nine ratchets · worktrees in practice · Supabase and
Vercel tool rules · traps ranked by cost · then the audit findings,
retractions, measured repo weight, and what is still open.
Two corrections to this document, in this PR
Part 16 documented a live production bug.
9dea37c56(#1635) fixed bothhalves of it hours after the paragraphs were written. Merging a findings doc
that asserts a fixed bug is live is the exact failure class the document is
about, so the last commit corrects it — recording the correction rather than
rewriting the analysis.
Worth reading on its own: the fix did not take the shape this document
proposed. I wrote "no early return". The shipped fix does not add one, and
should not — the team-chat query supplements the RPC, so returning early would
blank a rail whose DMs loaded fine. The terminal check became
(rpcError ?? groupConvsError). The defect was real; the prescription attachedto it would have introduced a different one.
And a count I should not have written: "ten wrappers remain, three in that one
file" was not reproducible and disagreed with the fix author's "~47 other
sites" — we were counting different shapes and neither of us said which.
Replaced with the query and its output.
shipping.mdsection 1 says neverwrite a count into prose; I wrote one six pages after quoting the rule.
Gates
docs:path-driftdocs:schema-driftmarkdown:ratchetThe ratchet red is proven, not assumed. It enumerates with
readdirSync, so itwalks
docs/redesign/— 21 gitignored files holding 1,850 violations. Scopedto
git ls-files, with this file included, zero rules are above baseline andthe total is 1,457 below it. CI checks out clean, so those files do not exist
there. This is the reproducibility defect
a3afec27dfixes ondocs/consolidation-2026-08-27, which is not in this PR.Not run: typecheck, lint, vitest, build. No TypeScript changed.
Also in this PR
Two small config commits from the same audit:
.claude/settings.json—autoMemoryEnabledfalse to true.gitignore— six regenerable artifact trees that were showing as untracked(
graphify-out/,_staging-snapshot/,.ruff_cache/,CLAUDE-SECURITY-*/,feature-awareness-report/,changed-files.txt)Not in this PR, and worth knowing
Two local branches are configured with
upstream = origin/main—docs/consolidation-2026-08-27andfix/incident-fingerprint-stability. Abare push from either targets
maindirectly. That is the trapshipping.mddocuments, currently live on two branches with five worktrees active. Left
alone deliberately; flagging it here because a PR body is the right place for
something I am not fixing.
docs/consolidation-2026-08-27is 23 commits and unpushed — the structuralconsolidation (1,300 files, minus 222k lines, evicting the archive trees).
Different change, separate review.
🤖 Generated with Claude Code
https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH