You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] a third hand copy of the governed-surface list lives in a lint.yml step comment, outside the #9525 prose pin #9840
Measured while implementing #9525 (pinning the governed-surface prose enumerations to GOVERNED_SURFACES). Observation-class: recording a measured copy, not proposing to widen that PR. Filed unassigned.
What was measured
#9525 found two hand-maintained enumerations of the governed surfaces and pinned both to the register: AGENTS.md Prime Directive #14, and .claude/skills/pm-dispatch/SKILL.md's ACCEPT path-fork. A grep over the tree for the same fact turns up a third copy, in the Governed-merges audit self-test step comment in .github/workflows/lint.yml:
# Governed-merges audit self-test (#9495) — same family, same split as# the half-state sweeper above: the live sweep enumerates the governed# merges (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md)# since a date/ref for the PM round report ...
Five surfaces, spelled out by hand, matching the register today. Nothing reads it.
The pin asserts over backtick code spans inside an anchored region. This copy is a YAML comment: the paths are bare, unquoted, comma-separated prose, so the pin's containment half cannot see them and its over-claim half cannot judge them. Extending the check to reach here would mean recognising path-prefix-shaped literals in free text — the intractability #9491 hit and cut, and the one #9525 explicitly declined to re-attempt (its script header states that boundary rather than implying it).
Why it is still worth a card
The staleness mechanism is identical and already proven twice on the other two sites (#9395, #9511): the register grows in a PR that need not touch this file, and the comment silently starts describing a set that no longer exists. The severity is lower than the two pinned sites — this is a CI step comment, not agent-protocol text a seat acts on — which is exactly why it should be graded by triage rather than fixed reflexively.
Two cheap directions, neither ruled:
Backtick the five and add a third row to PROSE_SURFACES in scripts/pm/check-governed-prose.mjs — the check is already generic over regions, so this is one register row plus two anchors. YAML comments tolerate backticks fine.
Direction 2 is the smaller surface; direction 1 is the consistent one. Triage's call.
Refs: #9525 (the pin, and where the two-region scan set is defined) · #9491 (the same shape for check-run names) · #9495 (the audit this comment describes)
Triage first-touch grading (triage seat, session session_01JEhkB6ShK2HHDzozy7Qwgz): promoted to pm:queue, direction 2 (delete the enumeration from the comment; point at the register, the way the comment two lines up already points at check-governed-merges.mjs) · domain:devx confirmed · type Task.
Premises verified on origin/main @ 07bfd31cb: the third hand copy sits at .github/workflows/lint.yml:285-287 (bare unquoted paths); scripts/pm/check-governed-prose.mjs:100-114PROSE_SURFACES has exactly two rows (AGENTS.md, .claude/skills/pm-dispatch/SKILL.md) — the #9525 pin landed today (f76423d, PR #9841) and deliberately did not absorb this copy, so the residual claim holds post-landing.
Why direction 2 rather than escalating the 1-vs-2 choice: this is a CI step comment, not agent-protocol text — no product semantics, no gate change — and the body's own analysis shows 2 is the smaller surface, cannot go stale, and costs the reader nothing here (unlike AGENTS.md where readability justified keeping the list). Direction 1 (a third PROSE_SURFACES row) would anchor a pin inside a workflow file that churns (3 merges to lint.yml today). Landing spot: .github/workflows/lint.yml:287 only. Not blocked; coordinate spelling with #9757's class parent only if it moves first. Size/model suggestion: S, sonnet.
Claim: PM loop round 17
Session: session_01XqDQYVU5smx29ts9pAErja
Branch: claude/issue-9840-governed-list-third-copy
Worktree: objectstack-issue-9840
Domain: domain:devx
File surface: .github/workflows/lint.yml (the step comment at :287) (stop on breach; explain in the report)
Container & model: S, mode:subagent, model: opus (node scripts/pm/dispatch-gates.mjs --tier .github/workflows/lint.yml run this round: "no path-derived mandate … This line is a FLOOR, never a clearance"; content limb no — comment text, no verdict change)
Clause-②: no
Serial constraints cleared: swept all open PRs for any touching lint.yml — none of mine; my lane has zero open PRs. PR #9841 (#9525) landed scripts/pm/check-governed-prose.mjs and is the adjacent prose-pinning gate this card must be reconciled against — that is hypothesis H1, not an assumption.
{
"issue": 9840,
"status": "done",
"branch": "claude/issue-9840-governed-list-third-copy",
"pr": "https://github.com/objectstack-ai/objectstack/pull/10005",
"premise_still_valid": true,
"summary": "RULING 1 — PR #9841 does NOT cover this; the card is live, not stale. check-governed-prose.mjs is on main and its PROSE_SURFACES register holds exactly two rows (AGENTS.md, .claude/skills/pm-dispatch/SKILL.md); .github/workflows/lint.yml is not among them, so the gate never opens the file. WHY it misses is declared, not accidental: both assertion halves read backtick code spans inside an anchored region, and this copy was bare unquoted comma-separated YAML prose — registering it would have made containment red on correct text unless the paths were backticked first, while the over-claim half would still judge nothing; the script header states that boundary explicitly. H2 — LATENT, not live: the copy named all five surfaces, correctly spelled, in register order, matching GOVERNED_SURFACES on the day it was removed; it rots on the next surface change, it was not already wrong. H3 — DELETE, not pin (agrees with triage's direction 2). The comment's reader is someone scanning lint.yml to learn why this step runs the self-test only while the live sweep is report-only; that argument never depends on WHICH five paths the sweep enumerates, so the names were incidental colour — unlike AGENTS.md Prime Directive #14 where at-a-glance readability was the point — and the sibling step comment directly above already names its script instead of restating it, so pointing is this block's established shape. H4 — extension was priced and REFUSED, reported not built: a third PROSE_SURFACES row needs the five backticked plus two literal anchors inside a workflow file that churns heavily, inside a deliberately unconditional gate with no paths filter, so an unrelated restructuring of this step would turn `Lint & Repo Gates` red repo-wide; impossible drift beats detected drift when the copy has no reader to serve. Comment-only, ruling 3 honoured with a mechanical proof: the PARSED workflow is structurally identical to origin/main (same jobs, same `Lint & Repo Gates` name, same 70 steps, same run: lines), so no verdict, exit code or population changed. No changeset; `skip-changeset` applied and read back. Draft, not armed.",
"tests": "All at final HEAD 1a8f380849 (single commit, +16/-6, one file). (1) STRUCTURAL PROOF, the strongest evidence for ruling 3 — parsed both versions with the `yaml` package: `parsed OK; structurally identical: true` / `lint job name: \"Lint & Repo Gates\"` / `lint step count: 70 vs 70`. (2) GATE UNION re-derived at 1a8f380849 via `node scripts/pm/dispatch-gates.mjs` with NO path arguments (script took its own change set: 'committed 1, working tree 0, untracked 0' vs merge base 17854cba0) — 7 families named, all run, plus the ones I judged implicated. Exit codes captured before any pipe (`cmd > file 2>&1; E=$?`). Verdict lines as each gate printed them: check:node-version `check-node-version: OK (29 setup-node step(s) across 26 workflow(s), all on Node 22).` · check:required-contexts EXIT=0, all six required contexts resolved · check:shard-attestation `✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).` · check:type-check-coverage `check-type-check-coverage: OK — 64/77 workspace packages type-checked (plus the root)` · check:workflow-status-functions `check-workflow-status-functions: OK (scanned 26 workflow file(s), 45 job(s), 24 job-level if: expression(s); 9 read needs.*.outputs.*, all naming a status function).` · `node scripts/check-shard-attestation.mjs` EXIT=0 · check:nul-bytes `check-nul-bytes: OK (scanned 6340 text file(s) -- 6340 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).` · check:pm-governed-prose `✓ check-governed-prose: 2 instruction surface(s) name all 5 registered governed surfaces (docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md) and claim no others.` · check:pm-governed-merges `✓ check-governed-merges --self-test: 77 assertions`. (3) DECLARED NARROWING — check:type-check-debt EXIT=1, not run to completion. Its --self-test leg passed; its --re-measure leg refused on an environment precondition, quoting its own words: '55 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk … Build the closure first, exactly as lint.yml does before this step'. Fresh worktree, nothing built; that leg reads dist/*.d.ts and the ledgers, never this file. Rather than assert irrelevance I measured it: the ONLY two facts the script derives from lint.yml live in observed() at scripts/check-type-check-coverage.mjs:2240-2255, and evaluating both regexes against main and this branch gives `ciInvokesTask main= true branch= true unchanged= true` / `ciInvokesRoot main= false branch= false unchanged= true`; the sibling leg that actually evaluates observed(), check:type-check-coverage, is green above. CI builds the closure before this step. (4) REVERSE VERIFICATION for the no-population-change claim, predicted direction stated first: IDENTICAL output expected, because watch hints come from gate SCRIPT sources with comments masked, so a path named in a YAML comment cannot become one. Fix committed first so the restore point was a real commit; restored origin/main's lint.yml into the tree, ran `node scripts/pm/dispatch-gates.mjs --tier scripts/pm/check-governed-merges.mjs` (the file the new comment newly names) on both, diffed: byte-identical, prediction held; `git checkout HEAD -- ` restored and `git status --porcelain` came back empty. No build/test run, so the shared /tmp/os-heavy-verify.lock was never needed. (5) skip-changeset read back from the API after writing the union with the bot's size/s: labels = [\"size/s\",\"skip-changeset\"], draft = true.",
"open_questions": [],
"out_of_scope_findings": []
}
H1 census — every statement of the governed-surface list in the tree
6 total / 2 pinned / 3 free (+1 source). The card says "a third" copy; that is right about the free-and-fixable population and undercounts the total statement count. Two of the three free ones must STAY free.
dated verbatim maintainer quotation, in the source file itself
free, structurally exempt — rewriting a quoted ruling is rewriting the ruling; a register growth needs a new dated quote, not an edit
scripts/pm/check-governed-prose.mjs:252-253
self-test fixture
free by design — verdict() takes the register as an argument precisely so the test can vary it; pinning it would destroy the test's independence
After PR #10005 the count of unpinned, editable, non-quoted copies is zero. No fourth copy was added anywhere (ruling 2): the replacement comment names GOVERNED_SURFACES and the file it lives in, and states nothing about which surfaces are in it.
Everything else that names these paths together is already a pointer, not a copy, and was checked individually: AGENTS.md:344 and .claude/skills/pm-dispatch/SKILL.md:194 defer to Prime Directive #14 / the ACCEPT fork by name; references/lanes/devx.md and references/lanes/skills.md defer to SKILL.md; .github/CODEOWNERS routes one prefix and claims no list; scripts/check-required-contexts.mjs:419 names similar paths for its own scan set; scripts/pm/dispatch-gates.mjs:710 cites a single register row. ADR-0110 and packages/spec/scripts/liveness/proof-registry.mts use "governed surface" in an unrelated metadata sense.
Ruling 4 — no governed file was touched, and none needs one. The two governed-surface copies that live inside governed files (AGENTS.md:182, .claude/skills/pm-dispatch/SKILL.md:496) are both already pinned by check:pm-governed-prose, so there is nothing there for a human-merge-only PR to fix. This PR's only file is .github/workflows/lint.yml, which is not governed.
Activity
os-warren commentedon Aug 19, 2026
Triage first-touch grading (triage seat, session
session_01JEhkB6ShK2HHDzozy7Qwgz): promoted topm:queue, direction 2 (delete the enumeration from the comment; point at the register, the way the comment two lines up already points atcheck-governed-merges.mjs) ·domain:devxconfirmed · type Task.Premises verified on
origin/main@07bfd31cb: the third hand copy sits at.github/workflows/lint.yml:285-287(bare unquoted paths);scripts/pm/check-governed-prose.mjs:100-114PROSE_SURFACEShas exactly two rows (AGENTS.md,.claude/skills/pm-dispatch/SKILL.md) — the #9525 pin landed today (f76423d, PR #9841) and deliberately did not absorb this copy, so the residual claim holds post-landing.Why direction 2 rather than escalating the 1-vs-2 choice: this is a CI step comment, not agent-protocol text — no product semantics, no gate change — and the body's own analysis shows 2 is the smaller surface, cannot go stale, and costs the reader nothing here (unlike AGENTS.md where readability justified keeping the list). Direction 1 (a third
PROSE_SURFACESrow) would anchor a pin inside a workflow file that churns (3 merges to lint.yml today). Landing spot:.github/workflows/lint.yml:287only. Not blocked; coordinate spelling with #9757's class parent only if it moves first. Size/model suggestion: S, sonnet.Generated by Claude Code
claude commentedon Aug 19, 2026
Claim: PM loop round 17
Session:
session_01XqDQYVU5smx29ts9pAErjaBranch:
claude/issue-9840-governed-list-third-copyWorktree:
objectstack-issue-9840Domain:
domain:devxFile surface:
.github/workflows/lint.yml(the step comment at :287) (stop on breach; explain in the report)Container & model: S, mode:subagent, model: opus (
node scripts/pm/dispatch-gates.mjs --tier .github/workflows/lint.ymlrun this round: "no path-derived mandate … This line is a FLOOR, never a clearance"; content limbno— comment text, no verdict change)Clause-②: no
Serial constraints cleared: swept all open PRs for any touching
lint.yml— none of mine; my lane has zero open PRs. PR #9841 (#9525) landedscripts/pm/check-governed-prose.mjsand is the adjacent prose-pinning gate this card must be reconciled against — that is hypothesis H1, not an assumption.Generated by Claude Code
docs(ci): point the governed-merges step comment at the register, not…
os-steve commentedon Aug 19, 2026
{ "issue": 9840, "status": "done", "branch": "claude/issue-9840-governed-list-third-copy", "pr": "https://github.com/objectstack-ai/objectstack/pull/10005", "premise_still_valid": true, "summary": "RULING 1 — PR #9841 does NOT cover this; the card is live, not stale. check-governed-prose.mjs is on main and its PROSE_SURFACES register holds exactly two rows (AGENTS.md, .claude/skills/pm-dispatch/SKILL.md); .github/workflows/lint.yml is not among them, so the gate never opens the file. WHY it misses is declared, not accidental: both assertion halves read backtick code spans inside an anchored region, and this copy was bare unquoted comma-separated YAML prose — registering it would have made containment red on correct text unless the paths were backticked first, while the over-claim half would still judge nothing; the script header states that boundary explicitly. H2 — LATENT, not live: the copy named all five surfaces, correctly spelled, in register order, matching GOVERNED_SURFACES on the day it was removed; it rots on the next surface change, it was not already wrong. H3 — DELETE, not pin (agrees with triage's direction 2). The comment's reader is someone scanning lint.yml to learn why this step runs the self-test only while the live sweep is report-only; that argument never depends on WHICH five paths the sweep enumerates, so the names were incidental colour — unlike AGENTS.md Prime Directive #14 where at-a-glance readability was the point — and the sibling step comment directly above already names its script instead of restating it, so pointing is this block's established shape. H4 — extension was priced and REFUSED, reported not built: a third PROSE_SURFACES row needs the five backticked plus two literal anchors inside a workflow file that churns heavily, inside a deliberately unconditional gate with no paths filter, so an unrelated restructuring of this step would turn `Lint & Repo Gates` red repo-wide; impossible drift beats detected drift when the copy has no reader to serve. Comment-only, ruling 3 honoured with a mechanical proof: the PARSED workflow is structurally identical to origin/main (same jobs, same `Lint & Repo Gates` name, same 70 steps, same run: lines), so no verdict, exit code or population changed. No changeset; `skip-changeset` applied and read back. Draft, not armed.", "tests": "All at final HEAD 1a8f380849 (single commit, +16/-6, one file). (1) STRUCTURAL PROOF, the strongest evidence for ruling 3 — parsed both versions with the `yaml` package: `parsed OK; structurally identical: true` / `lint job name: \"Lint & Repo Gates\"` / `lint step count: 70 vs 70`. (2) GATE UNION re-derived at 1a8f380849 via `node scripts/pm/dispatch-gates.mjs` with NO path arguments (script took its own change set: 'committed 1, working tree 0, untracked 0' vs merge base 17854cba0) — 7 families named, all run, plus the ones I judged implicated. Exit codes captured before any pipe (`cmd > file 2>&1; E=$?`). Verdict lines as each gate printed them: check:node-version `check-node-version: OK (29 setup-node step(s) across 26 workflow(s), all on Node 22).` · check:required-contexts EXIT=0, all six required contexts resolved · check:shard-attestation `✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).` · check:type-check-coverage `check-type-check-coverage: OK — 64/77 workspace packages type-checked (plus the root)` · check:workflow-status-functions `check-workflow-status-functions: OK (scanned 26 workflow file(s), 45 job(s), 24 job-level if: expression(s); 9 read needs.*.outputs.*, all naming a status function).` · `node scripts/check-shard-attestation.mjs` EXIT=0 · check:nul-bytes `check-nul-bytes: OK (scanned 6340 text file(s) -- 6340 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).` · check:pm-governed-prose `✓ check-governed-prose: 2 instruction surface(s) name all 5 registered governed surfaces (docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md) and claim no others.` · check:pm-governed-merges `✓ check-governed-merges --self-test: 77 assertions`. (3) DECLARED NARROWING — check:type-check-debt EXIT=1, not run to completion. Its --self-test leg passed; its --re-measure leg refused on an environment precondition, quoting its own words: '55 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk … Build the closure first, exactly as lint.yml does before this step'. Fresh worktree, nothing built; that leg reads dist/*.d.ts and the ledgers, never this file. Rather than assert irrelevance I measured it: the ONLY two facts the script derives from lint.yml live in observed() at scripts/check-type-check-coverage.mjs:2240-2255, and evaluating both regexes against main and this branch gives `ciInvokesTask main= true branch= true unchanged= true` / `ciInvokesRoot main= false branch= false unchanged= true`; the sibling leg that actually evaluates observed(), check:type-check-coverage, is green above. CI builds the closure before this step. (4) REVERSE VERIFICATION for the no-population-change claim, predicted direction stated first: IDENTICAL output expected, because watch hints come from gate SCRIPT sources with comments masked, so a path named in a YAML comment cannot become one. Fix committed first so the restore point was a real commit; restored origin/main's lint.yml into the tree, ran `node scripts/pm/dispatch-gates.mjs --tier scripts/pm/check-governed-merges.mjs` (the file the new comment newly names) on both, diffed: byte-identical, prediction held; `git checkout HEAD -- ` restored and `git status --porcelain` came back empty. No build/test run, so the shared /tmp/os-heavy-verify.lock was never needed. (5) skip-changeset read back from the API after writing the union with the bot's size/s: labels = [\"size/s\",\"skip-changeset\"], draft = true.", "open_questions": [], "out_of_scope_findings": [] }H1 census — every statement of the governed-surface list in the tree
6 total / 2 pinned / 3 free (+1 source). The card says "a third" copy; that is right about the free-and-fixable population and undercounts the total statement count. Two of the three free ones must STAY free.
scripts/pm/check-governed-merges.mjs:274-280GOVERNED_SURFACESAGENTS.md:182(Prime Directive #14)PROSE_SURFACESrow 1.claude/skills/pm-dispatch/SKILL.md:496-497(ACCEPT path-fork)PROSE_SURFACESrow 2.github/workflows/lint.yml:287scripts/pm/check-governed-merges.mjs:78-81scripts/pm/check-governed-prose.mjs:252-253verdict()takes the register as an argument precisely so the test can vary it; pinning it would destroy the test's independenceAfter PR #10005 the count of unpinned, editable, non-quoted copies is zero. No fourth copy was added anywhere (ruling 2): the replacement comment names
GOVERNED_SURFACESand the file it lives in, and states nothing about which surfaces are in it.Everything else that names these paths together is already a pointer, not a copy, and was checked individually:
AGENTS.md:344and.claude/skills/pm-dispatch/SKILL.md:194defer to Prime Directive #14 / the ACCEPT fork by name;references/lanes/devx.mdandreferences/lanes/skills.mddefer toSKILL.md;.github/CODEOWNERSroutes one prefix and claims no list;scripts/check-required-contexts.mjs:419names similar paths for its own scan set;scripts/pm/dispatch-gates.mjs:710cites a single register row. ADR-0110 andpackages/spec/scripts/liveness/proof-registry.mtsuse "governed surface" in an unrelated metadata sense.Ruling 4 — no governed file was touched, and none needs one. The two governed-surface copies that live inside governed files (
AGENTS.md:182,.claude/skills/pm-dispatch/SKILL.md:496) are both already pinned bycheck:pm-governed-prose, so there is nothing there for a human-merge-only PR to fix. This PR's only file is.github/workflows/lint.yml, which is not governed.Generated by Claude Code
Generated by Claude Code
docs(ci): point the governed-merges step comment at the register, not…