Skip to content

docs: the agent lifecycle in this repo, traced from the configs themselves - #1638

Closed
njrini99-code wants to merge 7 commits into
mainfrom
docs/agent-lifecycle-trace
Closed

docs: the agent lifecycle in this repo, traced from the configs themselves#1638
njrini99-code wants to merge 7 commits into
mainfrom
docs/agent-lifecycle-trace

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

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. A
pre-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-fallback selects on catch_clause. The messaging
defect this PR documents in Part 16 was if (error) { logError(...) } with no
return — same failure class, invisible to the rule. A pack covers the shapes
someone thought to write down.

settings.json deny rules are branch-scoped. They are version-controlled,
so a git checkout of an older branch silently removes them. The hooks do not
move with the branch. That is why the Supabase and Vercel blocks are duplicated
into guard-bash.sh rather than trusted to permissions alone — and it is worth
knowing which layer is actually stopping you, because they fail differently.

Some gates are not reproducible. markdown:ratchet enumerates with
readdirSync, so it walks gitignored trees. Scoped to git ls-files instead,
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. prebuild and postbuild
fire by naming convention, and vercel.json invokes the build through npm, so
prebuild runs on every production build. Searching for a script by name will
not 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, the postbuild
tsconfig 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 both
halves 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 attached
to 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.md section 1 says never
write a count into prose; I wrote one six pages after quoting the rule.

Gates

Gate Result
docs:path-drift PASS — 44, baseline 44
docs:schema-drift PASS — 59, baseline 59
markdownlint on the new file 0 issues
lines over 80 columns 0
markdown:ratchet RED locally — not this change

The ratchet red is proven, not assumed. It enumerates with readdirSync, so it
walks docs/redesign/ — 21 gitignored files holding 1,850 violations. Scoped
to git ls-files, with this file included, zero rules are 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 a3afec27d fixes on
docs/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.jsonautoMemoryEnabled false 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-27 and fix/incident-fingerprint-stability. A
bare push from either targets main directly. That is the trap shipping.md
documents, 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-27 is 23 commits and unpushed — the structural
consolidation (1,300 files, minus 222k lines, evicting the archive trees).
Different change, separate review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NbDxyygyXRUEERuGocpZZH

njrini99-code and others added 7 commits August 27, 2026 12:40
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-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@supabase

supabase Bot commented Aug 27, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project qmnssrrolpinvwjjnufo because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
helmv3 Ignored Ignored Preview Aug 27, 2026 4:47pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 60f156cf-f1e5-4c02-9370-aa5f986617d1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

njrini99-code added a commit that referenced this pull request Aug 29, 2026
)

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>
@njrini99-code

Copy link
Copy Markdown
Owner Author

Closed as superseded by #1677, not merged.

Two reasons it cannot land as-is:

  1. It flips autoMemoryEnabled false -> true. This project deliberately sets it false — .claude/rules/shipping.md §1b records why: the repo already has an explicit git-backed memory architecture, and a machine-local store that can disagree with committed state is a second authority for engineering truth.

  2. Its 1150-line docs/AGENT_LIFECYCLE.md predates PARK vs RETIRE, the branch-GC classifier, the generated enforcement inventory and the tool authority matrix. It describes a lifecycle that no longer exists, and an out-of-date lifecycle manual is exactly the failure class feat(control-plane): the control plane verifies the control plane #1677 exists to close.

EXTRACTED, because it was still true: your .gitignore additions for graphify-out/ and _staging-snapshot/, with the measured justification (53.5 MiB largest object in history per git-sizer). Those are in #1677 and active.

njrini99-code added a commit that referenced this pull request Aug 30, 2026
…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>
njrini99-code added a commit that referenced this pull request Aug 30, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant