Skip to content

fix(pm): dispatch-gates runs the diff-scoped issue-citation verdict locally, off the checker's own local-env declaration - #20314

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20278-issue-citations-family
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20278-issue-citations-family

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20278

Clause-②: no

What this changes

dispatch-gates --commands now lists the diff-scoped issue-citation verdict, bare node scripts/check-issue-citations.mjs. That is the run Lint & Repo Gates blocks on, and before this change the tool printed it as ⊘ NOT MEASURED, so a dev's local sweep could reconcile clean while CI went red (measured on #20268's CI run). The census and every other invocation of the same script keep exactly the classification they had.

This implements option A of the seat ruling on the card (ruling comment 5858896081): two files, one declaration and one reader.

Why the family was NOT MEASURED

The && in pnpm check:issue-citations && node scripts/check-issue-citations.mjs was already split into two families. The cause is the env carrier: the lint.yml step passes GITHUB_TOKEN: ${{ github.token }} and OS_GATE_MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }} through env:. workflowEnvValues treated both as values with none outside CI, so discoverFamilies set notRunnable and commandsFor dropped the family. The script needs neither on a pull-request run:

  • The merge-group base renders empty on pull_request, and the checker then uses merge-base origin/main HEAD.
  • The token is used when present, and the diff mode reads the public board with one probe per added number.

A rule about the variables alone cannot fix this. Dropping the job token and the merge-group base generically was measured to also admit the same script's --census (a report-only enumeration of the whole board) and check-required-contexts.mjs --verify-required-set (exit 2 here). The fact belongs to the program and to the mode, so the program declares it.

The change (2 files)

  1. scripts/check-issue-citations.mjs: one whole-line comment in the existing dispatch-gates: marker idiom: // dispatch-gates: local-env GITHUB_TOKEN OS_GATE_MERGE_GROUP_BASE_SHA -- REASON. The reason states the file's own facts. It covers the bare invocation only; the census needs the token and is not declared.
  2. scripts/pm/dispatch-gates.mjs:
    • local-env joins PATH_LIST_MARKER_KEYS, the LIST-then-reason grammar. It gets the wholeness reading, the lookalike probe and the census by construction. There is no new grammar copy, no table and no gate.
    • declaredLocalEnv reads the declaration. It refuses a cut reason and any token that is not an env name.
    • localEnvRefusal grades the declaration against the tree's workflows. It throws, so the CLI answers derivation failed (exit 2), when either:
      • a declared name is not passed by every step running the bare invocation as an env: workflow value;
      • no workflow runs the bare invocation.
    • localEnvAdmitted spends the declaration on the bare key alone. --census, --self-test, check:* keys and every other argv admit nothing.
    • workflowEnvValues gains one per-NAME limb: only the declared names leave the list, so an undeclared name still keeps the family out.
    • The matched row carries localEnv, in --json and in a same-line note in the human block, so no rendering lists the command without saying why.
    • The docblock states what the reader does NOT do: guess; admit a name the workflow does not pass; admit an argv no workflow runs; touch any other invocation of the same script; repair an argv value.
    • Self-test:
      • Direction pins on the lint.yml step text: the bare command is in the runnable union and not in the not-runnable set.
      • The census invocation stays not-runnable on GITHUB_TOKEN.
      • Controls: an undeclared script keeps the old reading, and a partial declaration keeps the undeclared name.
      • Refusals for a stale name, a name only some steps pass, and a missing bare run.
      • Live in-process pins on this tree for the rls.zod.ts specimen.
      • The key roster and marker census moved from six to seven keys and from seven to eight declarations, each row named.
      • No absolute counts.

package.json is untouched. Renaming check:issue-citations was optional in the triage notes and would not make the diff-scoped run runnable: the derivation keys on what lint.yml runs, not on the alias name.

Evidence

--commands for packages/spec/src/security/rls.zod.ts. Before is base 3f86dc52, after is head 15ab041f. Stdout gains exactly one line; nothing else on stdout moves.

before  dispatch-gates --commands: 71 command(s) — 42 pnpm, 29 direct node (65 matched by path, ...)
before    + 7 famil(ies) matched by path take a VALUE FROM THE WORKFLOW and are NOT above ...
before        ⊘ NOT MEASURED — scripts/check-issue-citations.mjs --census
before        ⊘ NOT MEASURED — scripts/check-issue-citations.mjs
after   stdout line 12: node scripts/check-issue-citations.mjs
after   dispatch-gates --commands: 72 command(s) — 42 pnpm, 30 direct node (66 matched by path, ...)
after     + 6 famil(ies) matched by path take a VALUE FROM THE WORKFLOW and are NOT above ...
after         ⊘ NOT MEASURED — scripts/check-issue-citations.mjs --census

The human block now renders the row as - node scripts/check-issue-citations.mjs [lint.yml] matched via packages/spec/src/security/rls.zod.ts ⇢ gate source 'packages/**' · runs here without GITHUB_TOKEN, OS_GATE_MERGE_GROUP_BASE_SHA: its script declares local-env for this bare invocation, and CI passes them through the step's env:.

The command it now lists answers the question here. Measured in a detached probe worktree at 3f86dc52 with an uncommitted edit, since restored and removed:

Self-test, through the verify lock.

  • pnpm check:pm-dispatch-gates at 15ab041f: os-verify-lock: VERDICT command-exit 0 · held the lock 975s (16m15s) · waited 0s · ⚠ SHARED-BOX SECONDS. Its log reads ✓ dispatch-gates self-test: 1976 cases pass. and ✓ check:pm-dispatch-gates --self-test: the exit contract holds in all three directions.
  • An earlier node scripts/pm/dispatch-gates.mjs --self-test at 23973549 also answered VERDICT command-exit 0, 1976 cases.

Ablations. Both legs used scripts/ablation-replace.mjs, which checks the anchor hit and that the blob changed, then restores with blob == HEAD and an empty git diff HEAD.

  • Leg 1, declaration disabled (// dispatch-gates: local-env GITHUB_TOKEN → // ablated-marker: local-env GITHUB_TOKEN, blob 49bf630d → 64ab1b63): --commands for rls.zod.ts loses node scripts/check-issue-citations.mjs from stdout (0 hits), and stderr prints ⊘ NOT MEASURED — scripts/check-issue-citations.mjs again. Restored: blob == HEAD 49bf630d.
  • Leg 2, stale name planted (... OS_GATE_MERGE_GROUP_BASE_SHA PR_BODY --): the CLI exits 2 with derivation failed — dispatch-gates: scripts/check-issue-citations.mjs:204 declares local-env PR_BODY, which the step(s) running node scripts/check-issue-citations.mjs do not all pass as a workflow value through env: .... Restored: blob == HEAD.

Derived gate sweep for this diff. Ran node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands with no paths, 2 paths against merge base 3f86dc52. It derived 33 families, and each was run with its exit code captured before any pipe. Reconciled at head 15ab041f:

Run reconciliation — 33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 33 derived famil(ies) accounted for — 33 run, 0 NOT-MEASURED (a DERIVED zero — all 33 recorded an exit code and none of them is 3).

All 33 exit 0 at 15ab041f, among them node scripts/check-issue-citations.mjs, pnpm check:issue-citations, pnpm check:pm-dispatch-gates, pnpm check:watch-hint-literal, pnpm check:nul-bytes and pnpm check:declared-population-live. The first sweep, at 23973549, was red on one family: pnpm check:watch-hint-literal exit 1, every live declaration is a literal -- scripts/pm/dispatch-gates.mjs:ROOT_DIR_WATCH_HINTS. A self-test fixture string used that rostered name. 15ab041f renames the fixture constant, and the whole sweep was then re-run on that head. pnpm check:pm-skill-id-lint (named by the dispatch) also ran and exited 0: 34 file(s) clean.

Lint, a narrowed run. eslint --no-inline-config --format json on the two changed files at 15ab041f exited 0. The JSON reports 2 files, 0 errors and 0 warnings. The config --print-config reports for scripts/pm/dispatch-gates.mjs carries no parserOptions.project and no projectService, so linting is not type-aware and this diff cannot move the verdict on any untouched file. Repo-wide pnpm lint is CI's.

Changeset: skip-changeset.

  • Both files are in the repo-root package, which is "private": true with no files[].
  • git grep for declaredLocalEnv, localEnvRefusal and the marker over packages/** and apps/**: 0 hits.
  • Positive control: defineStack hits packages/spec/src/stack.zod.ts, which @objectstack/spec's files[] ships (src/**/*.zod.ts).

Line budget.

  • scripts/pm/dispatch-gates.mjs: 29507 → 29866 lines (1,746,809 → 1,768,400 bytes).
  • scripts/check-issue-citations.mjs: 1213 → 1215 lines (65,982 → 66,904 bytes).

No gate added, no ceiling moved, no table added.

Acceptance notes

  • The census row, node scripts/check-issue-citations.mjs --census, is still printed NOT MEASURED on every card in the checker's packages/** population. Its only workflow, half-state-patrol.yml, is path-filtered to the checker and patrol files, and the step is report-only. The ruling records this as a note, and it is unchanged here.
  • check-required-contexts.mjs --verify-required-set refuses here for a proxy-route reason (exit 2, HTTP 401), while dispatch-gates names env GITHUB_TOKEN. The ruling records this as a note, and it is unchanged here.
  • The census row's existing sibling note ("a DIFFERENT invocation of the same script …") now names the bare diff run as that sibling. The wording holds as written: a different argv asks another question.

Generated by Claude Code

…ocally, off the checker's own local-env declaration

The lint.yml step that runs `node scripts/check-issue-citations.mjs` passes
GITHUB_TOKEN and OS_GATE_MERGE_GROUP_BASE_SHA through `env:`, so the env
carrier classified the bare run NOT RUNNABLE LOCALLY and `--commands` offered
only its self-test. The checker now declares, in the existing dispatch-gates
marker idiom, that its bare invocation needs neither; dispatch-gates reads the
declaration (`declaredLocalEnv`, path-list grammar), grades it against the
workflows (`localEnvRefusal`: a name the step does not pass, or a bare run no
workflow makes, is refused) and spends it on the bare key alone
(`localEnvAdmitted`). The census and every other invocation keep their
classification.

Claude-Session: https://claude.ai/code/session_01MjvgiFAmjHqsxy1XLiVYfH
Co-authored-by: Claude <noreply@anthropic.com>
…k:watch-hint-literal does not read it as a live declaration

Measured by the derived sweep for this diff: `pnpm check:watch-hint-literal`
exit 1, "every live declaration is a literal -- scripts/pm/dispatch-gates.mjs:
ROOT_DIR_WATCH_HINTS". The fixture needed no watch-hint idiom at all; its first
line is now an unrostered constant, and the case still pins the declaration on
line 3.

Claude-Session: https://claude.ai/code/session_01MjvgiFAmjHqsxy1XLiVYfH
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 15ab041f
Local-runs: none

Face: scripts/pm/dispatch-gates.mjs (PM-loop tooling, the lane's own; not a governed surface — check-governed-merges --pr 20314: 0 of 2 paths hit the register) and scripts/check-issue-citations.mjs (a CI gate script; the one-file widening ruled A in 5858896081, a cross-lane touch declared on the seat post). Reviewed in-seat at tier by the dispatching seat as the compensating control for a default-tier dev, on the diff (two files, +372 / −11), the card and the head's check-runs.

① Derived judgments

  • The checker's declaration (:204) is one whole-line comment in the file's existing dispatch-gates: marker idiom, scoped to the bare invocation, naming exactly the two step-env: values lint.yml:4958–:4971 passes, with the reason stated as the file's own facts (token used when present, public board probed per number, proxy re-armed by the script; the merge-group base empty on pull_request, baseSpellings then taking the merge base against origin/main). The census is not declared. Matches ruling item 1.
  • dispatch-gates: local-env joins PATH_LIST_MARKER_KEYS, so the shared LIST-then-reason grammar, the cut-reason refusal ([finding] dispatch-gates' population markers capture only the FIRST line of a declaration's reason, and no refusal checks the reason is whole — a wrapped reason reaches a seat cut off mid-sentence, with nothing red #18422) and the live marker census cover it by construction rather than by a copy. declaredLocalEnv reads beside the two path-list readers and refuses a token that is not an env name (the --census lookalike is pinned). localEnvRefusal is one pure reading used by discovery and by the self-test: a name no step passes, a name only some steps pass, or a bare run no workflow makes, throws, and the CLI answers derivation failed (exit 2) — loud, naming file, line and name, in the posture the file already takes for a cut reason; a declaration the workflow outgrows cannot vouch quietly. localEnvAdmitted spends the declaration on the bare key alone; --census, --self-test and check:* keys admit nothing (pinned). workflowEnvValues gains a per-name limb, so an undeclared name still keeps the family out (pinned). The row carries localEnv in --json and as a same-line note in the human block (the harvest reads one line per row — kept). The docblock states the five things it does not do. Matches ruling item 2; no table, no gate, no ceiling; package.json, workflows and check-required-contexts.mjs untouched.
  • Direction, not counts: the fixture pins put the bare command in --commands and out of the not-runnable set on the lint.yml step text while the census stays not-runnable on its own token; the live in-process pins do the same for rls.zod.ts off the discovery the self-test already holds. The roster and census pins moved to seven keys / eight declarations, each named. The measured result in the PR body: base 3f86dc52 71 commands with both invocations NOT MEASURED; head 72 commands, the added line node scripts/check-issue-citations.mjs, only the census still NOT MEASURED, nothing else on stdout moved. Two ablations read back (declaration disabled ⇒ the bare command drops out and NOT MEASURED returns; a planted stale name ⇒ exit 2 naming :204 and the name), both restored to the HEAD blob. Matches ruling item 3.
  • Reach: PM-loop tooling and one comment line in a gate script; no runtime, spec or public surface change; CI's own commands unchanged. Second commit 15ab041f renames a self-test fixture constant so check:watch-hint-literal does not read a fixture string as a live ROOT_DIR_WATCH_HINTS declaration — the right fix (the fixture, not the gate). The two out_of_scope_findings are the notes already recorded by the ruling; unchanged by this PR.

② Semver level

Both files sit in the private root package (no files[]); git grep of the new symbols over packages/** and apps/** gives 0 hits, with defineStack as the positive control ⇒ no changeset; skip-changeset correct; consistent with Clause-②: no.

③ Boundary flags

No open_questions. Deviations accepted: the self-test's foreground wait past the Bash tool's budget was done by waiting on the lock holder's pid and reading the VERDICT line (pnpm check:pm-dispatch-gates through the verify lock: command-exit 0, 1976 cases; the derived sweep 33 / 33 run, 0 NOT MEASURED, a DERIVED zero at 15ab041f); check-dispatch-gates.mjs not edited because its header says it runs the tool's own --self-test (measured); the dispatch order's line references had moved (mapped in the round-1 report). No flag remains open.

Implemented-by: claude/issue-20278-issue-citations-family
Reviewed-by: session_01MjvgiFAmjHqsxy1XLiVYfH

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 20:58
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 7338efe Sep 27, 2026
38 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20278-issue-citations-family branch September 27, 2026 21:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants