Skip to content

[finding] a third hand copy of the governed-surface list lives in a lint.yml step comment, outside the #9525 prose pin #9840

Description

@os-warren

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.

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:

  1. 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.
  2. Delete the enumeration from the comment and point it at the register the way the comment two lines up already points at scripts/pm/check-governed-merges.mjs. Cheapest, cannot go stale, and costs a reader nothing here — unlike AGENTS.md, where at-a-glance readability was the whole point of AGENTS.md Prime Directive #14 names only docs/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

Activity

  1. os-warren commented on Aug 19, 2026

    @os-warren
    CollaboratorAuthor

    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-114 PROSE_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.


    Generated by Claude Code

  2. added theissue type on Aug 19, 2026
  3. claude commented on Aug 19, 2026

    @claude
    Contributor

    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.


    Generated by Claude Code

  4. os-steve commented on Aug 19, 2026

    @os-steve
    Collaborator
    {
      "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-280 GOVERNED_SURFACES the source n/a
    AGENTS.md:182 (Prime Directive #14) prose pinned — PROSE_SURFACES row 1
    .claude/skills/pm-dispatch/SKILL.md:496-497 (ACCEPT path-fork) prose pinned — PROSE_SURFACES row 2
    .github/workflows/lint.yml:287 YAML step comment free — deleted by PR #10005
    scripts/pm/check-governed-merges.mjs:78-81 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.


    Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions