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
Description
Activity
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
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.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 touchinglint.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
- added a commit that references this issue
on 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.
Site Kind Held by a gate? scripts/pm/check-governed-merges.mjs:274-280GOVERNED_SURFACESthe source n/a AGENTS.md:182(Prime Directive #14)prose pinned — PROSE_SURFACESrow 1.claude/skills/pm-dispatch/SKILL.md:496-497(ACCEPT path-fork)prose pinned — PROSE_SURFACESrow 2.github/workflows/lint.yml:287YAML step comment free — deleted by PR #10005 scripts/pm/check-governed-merges.mjs:78-81dated 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-253self-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 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
- added a commit that references this issue
on Aug 23, 2026
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.mdPrime 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 theGoverned-merges audit self-teststep comment in.github/workflows/lint.yml:Five surfaces, spelled out by hand, matching the register today. Nothing reads it.
Why it is outside the #9525 pin, deliberately
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:
PROSE_SURFACESinscripts/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.scripts/pm/check-governed-merges.mjs. Cheapest, cannot go stale, and costs a reader nothing here — unlikeAGENTS.md, where at-a-glance readability was the whole point of AGENTS.md Prime Directive #14 names onlydocs/adr/**, but the gate it describes now also holds.claude/skills/**#9403.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)
Generated by Claude Code