Skip to content

tooling(pm): render a value-bearing CI invocation instead of falling back to a bare key - #15114

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-15083-value-bearing-argv
Sep 4, 2026
Merged

tooling(pm): render a value-bearing CI invocation instead of falling back to a bare key#15114
os-steve merged 1 commit into
mainfrom
claude/issue-15083-value-bearing-argv

Conversation

@claude

@claude claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #15083

dispatch-gates keys the derivation on (script, args) and, since PR #15081, admitted the argv half only when the tail was a complete run of flag-shaped tokens. Nine scripts whose only CI invocations carry a value or a line continuation therefore kept a bare path key CI never runs bare. This renders those invocations instead, classifying each argv token from the workflow text.

Verification head sha: e98bfc87. Every gate below and every number in this body was run or read at that commit.

1. The measurement — every CI invocation of the nine, token by token

Read from .github/workflows/*.yml at origin/main (369da918). A literal appears verbatim in the workflow and is the same on every run; a variable is a GitHub expression, a shell expansion, a step output, or a path an earlier step produced.

Script Workflow · job / step Invocation as CI runs it Value tokens Class
check-adr-0087-registration pr-automation.yml · "Require an ADR-0087 disposition…" --self-test none LITERAL
pr-automation.yml · same step --base "$MERGE_BASE" $MERGE_BASE (env from steps.diffbase.outputs.merge_base) VARIABLE
cut-rc.yml · "Gate — this cut introduces no unregistered breaking change" --self-test none LITERAL
cut-rc.yml · same step --base "$SNAPSHOT_SHA" $SNAPSHOT_SHA VARIABLE
check-changeset-no-major pr-automation.yml · "…no major bump" (one-liner) --base "$MERGE_BASE" $MERGE_BASE VARIABLE
cut-rc.yml · "Gate — this cut introduces no major bump" --self-test none LITERAL
cut-rc.yml · same step --base "$SNAPSHOT_SHA" $SNAPSHOT_SHA VARIABLE
check-empty-changeset pr-automation.yml · "…no empty-frontmatter changeset" --self-test none LITERAL
pr-automation.yml · same step --base "$MERGE_BASE" $MERGE_BASE VARIABLE
cut-rc.yml · "Gate — this cut introduces no empty-frontmatter changeset" --self-test none LITERAL
cut-rc.yml · same step --base "$SNAPSHOT_SHA" $SNAPSHOT_SHA VARIABLE
check-engine-split-ratio lint.yml · shallow-history guard collector --self-test none LITERAL
engine-split-metric.yml · "Compute ratio (90-day window)" --days 90 90 written in the workflow LITERAL
check-test-completeness ci.yml · Test Core "Test completeness guard" "$RUNNER_TEMP/test-core.log" --scheduled "$RUNNER_TEMP/shard-packages.txt" --package-list "$RUNNER_TEMP/turbo-ls.json" (continued over 3 lines) $RUNNER_TEMP VARIABLE
ci.yml · Dogfood "Test completeness guard" "$RUNNER_TEMP/dogfood.log" $RUNNER_TEMP VARIABLE
check-shard-attestation ci.yml · test "Attest this shard ran and passed" --emit --job test --shard ${{ matrix.shard }} --total 6 --out "$RUNNER_TEMP/shard-attestation" ${{ matrix.shard }}, $RUNNER_TEMP (test, 6 are literals) VARIABLE
ci.yml · dogfood "Attest this shard ran and passed" --emit --job dogfood --shard ${{ matrix.shard }} --total 3 --out "$RUNNER_TEMP/shard-attestation" ${{ matrix.shard }}, $RUNNER_TEMP VARIABLE
ci.yml · dogfood-verify "Attest this leg ran and passed" --emit --job dogfood-verify --shard 1 --total 1 --out "$RUNNER_TEMP/shard-attestation" $RUNNER_TEMP only (1, 1 are literals) VARIABLE
ci.yml · test-gate "Verify test shard results" --verify --gate 'Test Core' --dir "$OS_ATTEST_DIR" --filter-result "$OS_FILTER_RESULT" --download-outcome "$OS_DOWNLOAD_OUTCOME" --leg "test/6:$OS_TEST_RESULT" 4 env vars ('Test Core' is a literal) VARIABLE
ci.yml · dogfood-gate "Verify dogfood shard results" --verify --gate 'Dogfood Regression Gate' --dir … --leg "dogfood/3:$OS_DOGFOOD_RESULT" --leg "dogfood-verify/1:$OS_VERIFY_RESULT" 5 env vars VARIABLE
check-required-contexts required-set-patrol.yml · "Run the live required-set sweep" --verify-required-set (continued, then two redirections) none — the continuation carries only redirections LITERAL
check-cross-package-test-inputs ci.yml · filter job, affected-set union --union-into "$RUNNER_TEMP/turbo-ls.json" --changed "$RUNNER_TEMP/changed-files.txt" (continued over 3 lines) $RUNNER_TEMP VARIABLE
check-prerelease-pin-watch prerelease-pin-watch.yml · "Self-test the probe" --self-test none LITERAL
prerelease-pin-watch.yml · "Probe npm for a stable release" --verbose then 2 redirected to 1 none — the 2 was the redirection's own file descriptor LITERAL

Three of the nine are not value-bearing at all. check-engine-split-ratio --days 90 writes its value down; check-required-contexts --verify-required-set and check-prerelease-pin-watch --verbose have complete argv that were refused only because a continuation and a redirection file descriptor stood behind them. Those three now render as runnable commands.

The split reads off the workflow text for all fifteen invocations, so the triage's ⛔ holds: no per-script declaration table is owed.

Two members the card's table does not name

Re-measured at origin/main rather than taken from the card:

  • scripts/pm/check-half-states.mjs is a tenth member of the same class. half-state-patrol.yml runs it --format=markdown --provenance="$PROVENANCE" (continued, then redirected) and nowhere else, where PROVENANCE is a workflow env built from github.run_id / github.sha / github.event_name. It derived under a bare key too.
  • check-release-section-coverage.mjs --strict is a second invocation in release-coverage-patrol.yml that had no entry of its own — a continuation hid it, and the script's bare key (which lint.yml really does run) absorbed it. It is now its own runnable family, and the bare key stays where CI runs it bare.

What the bare keys actually did, run at origin/main

This is why the bare key is not a neutral omission:

Bare invocation exit first line
node scripts/check-test-completeness.mjs 3 PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named.
node scripts/check-required-contexts.mjs 0 runs the static pin check — required-set-patrol.yml runs the live --verify-required-set sweep
node scripts/check-shard-attestation.mjs 0 duplicates what pnpm check:shard-attestation (a separate family) already runs; ci.yml's --emit/--verify runs are a different program
node scripts/check-cross-package-test-inputs.mjs 0 same shape — pnpm check:cross-package-test-inputs already covers it
the three --base gates 0 diff against their default base, while CI pins the merge base or the cut snapshot
node scripts/check-engine-split-ratio.mjs 2 shallow-clone refusal in this container; engine-split-metric.yml checks out with fetch-depth: 0

2. What changed in scripts/pm/dispatch-gates.mjs

  • joinLineContinuations splices a continued command into one line before any matcher runs, so the captured tail is the whole argv rather than the first physical line of it. ⛔ A comment line is never joined — a # comment runs to the newline, so its trailing backslash is comment text (measured: 107 continuation lines inside run: bodies on this tree, zero of them comments).
  • tailBeforeRedirection drops a redirection's file descriptor. DIRECT_CHECK_INVOCATION correctly stops the tail before the redirection operator, but 2 redirected to 1 puts its fd on the argv side of that boundary, so --verbose captured as --verbose 2. Discriminator is the shell's own: digits touching the operator are a descriptor, digits with a space before it are an argument.
  • argvTokens is a quote-aware tokenizer that also holds a ${{ … }} expression together — both spellings are live in ci.yml (--gate 'Test Core', --shard ${{ matrix.shard }}) and a bare whitespace split would classify their pieces separately.
  • renderedArgv replaces renderableArgv. It renders every tail and returns the workflow values it carries, so the caller decides what to do rather than the classifier refusing.
  • entry.notRunnable is the second not-runnable-here classification beside entry.ciOnly, and is deliberately kept separate from it: one family cannot run here because it needs the workflow event payload, the other because it needs a workflow value, and folding them would print one count for two omissions.
  • Renderings: a Value-bearing argv — matched by path, and NOT runnable here heading in the human output (below the blank line the published harvest stops at, so it cannot reach a harvest of the pasted block), a notRunnable field on the --json matched row, a stated omission on --commands stderr, and its own term in the reconciliation.

The usage line needed nothing. --commands promises "one runnable command per line on stdout, nothing else", and that promise is now more true, not less — the entries this adds are the ones it must not carry. PR #15105's --changed --commands line is untouched.

3. The nine entries, before and after

Read from discoverFamilies() at 369da918 and at e98bfc87.

Before (bare key CI never runs) After
scripts/check-adr-0087-registration.mjs … --base "$MERGE_BASE" [pr-automation.yml] NOT RUNNABLE · … --base "$SNAPSHOT_SHA" [cut-rc.yml] NOT RUNNABLE
scripts/check-changeset-no-major.mjs … --base "$MERGE_BASE" NOT RUNNABLE · … --base "$SNAPSHOT_SHA" NOT RUNNABLE
scripts/check-empty-changeset.mjs … --base "$MERGE_BASE" NOT RUNNABLE · … --base "$SNAPSHOT_SHA" NOT RUNNABLE
scripts/check-engine-split-ratio.mjs node scripts/check-engine-split-ratio.mjs --days 90runnable, in --commands
scripts/check-test-completeness.mjs … "$RUNNER_TEMP/test-core.log" --scheduled … --package-list … NOT RUNNABLE · … "$RUNNER_TEMP/dogfood.log" NOT RUNNABLE
scripts/check-shard-attestation.mjs five entries, one per ci.yml invocation, all NOT RUNNABLE (pnpm check:shard-attestation in lint.yml is a separate family and stays runnable)
scripts/check-required-contexts.mjs node scripts/check-required-contexts.mjs --verify-required-setrunnable, in --commands
scripts/check-cross-package-test-inputs.mjs … --union-into "$RUNNER_TEMP/turbo-ls.json" --changed "$RUNNER_TEMP/changed-files.txt" NOT RUNNABLE (pnpm check:cross-package-test-inputs stays runnable)
scripts/check-prerelease-pin-watch.mjs node scripts/check-prerelease-pin-watch.mjs --verboserunnable, in --commands
scripts/pm/check-half-states.mjs (tenth member) … --format=markdown --provenance="$PROVENANCE" NOT RUNNABLE
scripts/check-release-section-coverage.mjs (bare; the --strict run had no entry) bare key kept (lint.yml runs it bare) plus … --strictrunnable, in --commands

The price, measured the way PR #15081 priced its own

Over the tracked tree, before to after:

check families discovered      240 -> 249    (+9, ZERO lost)
watch-hint (gate, file) pairs  1499 -> 1528  (+29, ZERO lost)
re-attributed                  0 — every family present in both derivations keeps
                               its exact workflow set and its exact hint count

Verified as a set difference, not as a count: the only keys removed are the ten bare ones above, and every key added is one of the rendered invocations.

Live rendering, node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack .github/workflows/ci.yml

Before, in the pasted block (four commands CI never runs):

  - node scripts/check-cross-package-test-inputs.mjs   [ci.yml]   matched via …
  - node scripts/check-required-contexts.mjs   [required-set-patrol.yml]   matched via …
  - node scripts/check-shard-attestation.mjs   [ci.yml]   matched via …
  - node scripts/check-test-completeness.mjs   [ci.yml]   matched via …
Reconciliation — 40 famil(ies)

After — one of them promoted into the pasted block as the invocation CI really runs, the other eight under their own heading:

  - node scripts/check-required-contexts.mjs --verify-required-set   [required-set-patrol.yml]   matched via …

Value-bearing argv — matched by path, and NOT runnable here (8 famil(ies)):
  - node scripts/check-test-completeness.mjs "$RUNNER_TEMP/test-core.log" --scheduled "$RUNNER_TEMP/shard-packages.txt" --package-list "$RUNNER_TEMP/turbo-ls.json"   [ci.yml]   matched via …
      ⛔ NOT RUNNABLE LOCALLY — 1 value(s) come from the workflow: $RUNNER_TEMP
  - node scripts/check-shard-attestation.mjs --emit --job test --shard ${{ matrix.shard }} --total 6 --out "$RUNNER_TEMP/shard-attestation"   [ci.yml]   matched via …
      ⛔ NOT RUNNABLE LOCALLY — 2 value(s) come from the workflow: ${{ matrix.shard }}, $RUNNER_TEMP
  … 6 more
  ⇒ Printed as CI spells it, variable names in the value positions, so nothing above reads as a command to paste: these are ⛔ NOT in --commands, and a BARE run of any of these scripts is an invocation CI never makes.
  ⇒ Read from the workflow text, not from a table in this script: a token carrying ${{ … }} or a shell expansion is a variable, everything else is a literal and renders in full under the heading above.

Reconciliation — 37 famil(ies)
  + 8 famil(ies) this card's paths reach take a VALUE FROM THE WORKFLOW and sit OUTSIDE this total — …

--commands on those paths: 37 lines on stdout, zero containing a workflow variable, with the 8 stated on stderr. --json carries notRunnable on all 8 matched rows and none of their commands appear in commands.

4. Self-test — 1338 cases pass

Cases added (all PASS):

Case What it pins
an invocation whose tail carries a VARIABLE renders in FULL, and the bare key CI never runs is gone the variable kind, quoted from pr-automation.yml
…and it carries the variable it takes from the workflow $MERGE_BASE reaches the row label
an invocation CONTINUED across two lines renders as ONE command, values and all the ci.yml attestation, three physical lines
…with BOTH of its workflow variables named, the expression and the shell expansion ${{ matrix.shard }} and $RUNNER_TEMP
…and ⛔ no bare key survives beside it the bare key is gone, not merely joined
an invocation whose every token is a LITERAL renders as the command CI runs, value included --days 90, --verify-required-set, --verbose
…and every one of them is runnable — no variable, so nothing to label the literal kind stays in --commands
…and none of the three keeps a bare key CI never runs the control in the other direction
⛔ no derived key is a TRUNCATED argv no continuation backslash, no redirection fd as an argument
renderedArgv renders every tail and reports which values come from the workflow the classifier directly
argvTokens holds a quoted value and a ${{ … }} expression together the tokenizer
joinLineContinuations splices a continued command into one line, and ⛔ never joins a COMMENT the join and its shell-correct refusal
the live tree really carries N value-bearing invocation(s) across M script(s) the class is live, count read not typed
the card's named specimens all classify as VARIABLE from the workflow text no per-script table was needed
…and the three the card called value-bearing that are really LITERAL render as runnable commands instead the card's own table corrected by measurement
⛔ no live key survives as a bare path for a script CI only ever invokes WITH argv the defect, closed on the live tree
⛔ and no live key is truncated across all live workflows
a script CI invokes bare AND with argv keeps BOTH entries the third clause, with check-release-section-coverage as the live specimen
…each under the argv that file really runs it with, merge base and all the keyed half of the live pr-automation cases

Cases rewritten — six, and each is named because "existing cases keep their verdicts" deserves an honest accounting. All six still PASS and each keeps its subject:

  • Two pinned the refusal this card retires (an invocation whose tail carries a VALUE keeps the bare path key, an invocation CONTINUED onto the next line keeps the bare key). Same fixture, same tail — the expectation moved from "keeps the bare key" to "renders in full and says whether it is runnable", because that bare key is the defect.
  • renderableArgv keeps a complete flag run and refuses everything else became renderedArgv renders every tail and reports which values come from the workflow — the function it pins was replaced.
  • every derived key is either the bare script path or that path plus a complete flag run became …plus the WHOLE argv, re-tokenising to itself — the same "never a truncated argv" property, restated for a renderer.
  • Three live cases (the live Check Changeset job discovers its ADR-0087 gate and its two siblings) asserted discovery by bare key; they now assert it by script, which is what their own comment says they are about, with a new case beside them pinning the keyed half so a rewrite of the step cannot satisfy them vacuously.
  • One pending-changeset case counted node scripts/check-empty-changeset.mjs exactly once; it now counts node scripts/check-empty-changeset.mjs --base "$MERGE_BASE", which is the family that section is about.

Cases untouched and still passing: everything else, including the two census cases PR #15081 added and the redirection case (a complete flag run followed by a redirection IS keyed).

5. scripts/pm/bare-root-worklist.mjs — the pre-authorised second file

node scripts/pm/bare-root-worklist.mjs --self-test reddened exactly as the dispatch anticipated, with 3 STALE and 6 FRESH rows, all for check-adr-0087-registration's re-keying. Its own printed remedy was followed and nothing else in the file was touched: the three rows keyed on the retired bare path are deleted, and six second-key twins recorded in their place (one per workflow invocation), verdict SPELLABLE-UNDECLARED and spelling carried whole from the retired row, with no count restated — this pass measured none, which is that file's own rule.

Green after: OK self-test: 69 live row(s), 61 unreachable as spelled, 61 recorded verdict(s) — none stale, none missing, none contradicted.

6. Gates — every one derived for this diff, at e98bfc87

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths) after the commit. Exit code captured by redirecting first, never across a pipe; each row quotes the gate's own verdict line.

Gate exit verdict line
node scripts/check-ci-filter-parity.mjs 0 OK: all 143 declared cross-package glob(s) (99 unique) are covered by core or crosspkg…
node scripts/check-closing-keyword-parity.mjs 0 check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords…, all registered).
node scripts/check-closing-keyword-parity.mjs --self-test 0 ✓ check-closing-keyword-parity --self-test: 24 assertions, 5 mutations of the shipped parsers each driven to red.
node scripts/check-comment-mask-corpus.mjs 0 ✓ comment-mask corpus sweep: 5853 files, 0 disagree, 0 unparseable, 44.4s
node scripts/check-self-test-wired.mjs 0 ✓ check-self-test-wired: every one of the 166 script(s) CI runs that ship a --self-test has that self-test run by CI.
node scripts/check-self-test-wired.mjs --self-test 0 check-self-test-wired --self-test: 4 live ledger row(s) verified… every battery at or above its pinned floor.
node scripts/check-whole-set-label-write.mjs 0 ✓ check-whole-set-label-write: 0 violations — 257 file(s) over 3 root(s)…
node scripts/check-whole-set-label-write.mjs --self-test 0 ✓ check-whole-set-label-write --self-test: all cases pass (24 fixture trees + 5 refusals + 1 allowlist hatch)
node scripts/pm/bare-root-worklist.mjs --self-test 0 OK self-test: 69 live row(s)… none stale, none missing, none contradicted.
pnpm check:agent-test-spelling 0 ✓ check-agent-test-spelling: 0 violations — 437 file(s)…
pnpm check:bash32-floor 0 ✓ check-bash32-floor: 26 tracked shell file(s)… name no bash 4+ construct…
pnpm check:cli-command-ids 0 ✓ check-cli-command-ids: 324 command-id literal(s) across 114 file(s)… all resolve
pnpm check:cross-package-test-inputs 0 OK: 26 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
pnpm check:declared-population-live 0 ✓ check:declared-population-live — 198 of 249 famil(ies) declare a path population, and every one of them reaches this tree's 8230 tracked file(s).
pnpm check:entry-guard 0 ✓ check:entry-guard: 206 scripts/ file(s) — every entry guard goes through invoked-as.mjs…
pnpm check:nul-bytes 0 check-nul-bytes: OK (scanned 8223 text file(s)… no raw ASCII control bytes).
pnpm check:parse-guard 0 ✓ check:parse-guard: 205 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
pnpm check:pm-dispatch-gates 0 ✓ dispatch-gates self-test: 1338 cases pass.
pnpm check:pnpm-filter-targets 0 ✓ check:pnpm-filter-targets: 142/181 --filter occurrence(s) across 33 file(s) resolve…
pnpm check:refd-timer-probe 0 OK check-refd-timer-probe: 5848 source file(s) swept…
pnpm check:watch-hint-literal 0 ✓ check-watch-hint-literal: 48 declaration(s) across 4 rostered name(s)… no unrostered spelling of the idiom in the tree.
pnpm lint (repo-wide eslint . --no-inline-config) 0 clean run, no output — run in full, not narrowed (60s under the shared verify lock)

The two long runs went through scripts/pm/os-verify-lock.sh: pm-dispatch-gates VERDICT command-exit 0 · held the lock 250s, lint VERDICT command-exit 0 · held the lock 60s.

Two notes on scope of measurement, so nothing reads as verified that is not:

  • check-test-completeness is NOT MEASURED here, in its own words — it is not in this diff's derived gate list, and a bare run of it prints PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named. (exit 3). That refusal is now the reason it leaves --commands, not a gate this PR skipped.
  • No vitest suite exercises either edited script. git grep for dispatch-gates / bare-root-worklist across *.test.* returns five files, and all five hits are prose comments citing dispatch-gates.mjs's own self-test as the thing that pins their property. That self-test is pnpm check:pm-dispatch-gates above.

7. The design call, on the four axes

The triage left one design choice open: how to render an invocation whose values are workflow variables. Options were (A) classify each token from the workflow text — literals render, variables render with the variable named and the entry marked not runnable; (B) a per-script declaration table of which flags are value-bearing; (C) keep the bare key.

  • Real business need. Measured, not supposed: fifteen live invocations across nine scripts, ten with a tenth found at head. One bare key returns a non-verdict (PREREQUISITE NOT MET, exit 3), four run a different program than CI runs, three answer against a different base. The consumer is real — the PM's dispatch brief and --ran both read this list. (C) keeps a measured defect; (B) buys nothing (A) does not, at the cost of a declaration surface.
  • Long-term soundness. (A) reads the workflow, which is the single source of what CI runs; it cannot drift, because the thing it reads is the thing it describes. (B) is a second source that must be kept in sync by hand — a de-facto second contract, which is what contract-first refuses. No workaround: the continuation and the redirection fd are fixed at the parse layer where they belong, not patched around at the classifier.
  • Making it structurally harder for an AI to get wrong. The rejected direction is the one that reads best: emitting the variable-bearing command into --commands with the variable unset. It runs, and it answers a question CI never asked — the tolerant-fallback shape, one layer up. (A) refuses loudly instead: the entry is named, its variables are named, and it is kept out of the list whose caption promises runnable. A missing lead, never a fabricated one.
  • Startup focus / no capability spread. (A) adds no flag, no output mode, no declaration surface and no configuration. It reuses the ciOnly channel's exact shape for a second measurement. (B) would have added a per-script table that grows with the workflow corpus forever.

Recommendation taken: (A). The triage's ⛔ on (B) is discharged by the measurement in section 1 rather than by assertion — every one of the fifteen invocations classifies from the workflow text alone.

Bounded scope

scripts/pm/dispatch-gates.mjs plus the pre-authorised verdict rows in scripts/pm/bare-root-worklist.mjs. No workflow edits, no .md, no other script. skip-changeset: nothing is published from these paths. Not a governed surface — draft PR for the seat's contract-tier review.


Generated by Claude Code

…back to a bare key

`dispatch-gates` keyed the argv half of `(script, args)` only when the tail was
a complete run of flag-shaped tokens. Every other invocation kept a BARE path
key — and for nine scripts that bare key is an invocation CI never makes.
Measured on this tree: `node scripts/check-test-completeness.mjs` exits 3 with
`PREREQUISITE NOT MET`; `check-required-contexts` runs the static pin check
while `required-set-patrol.yml` runs `--verify-required-set`; the three
`--base` gates answer against their default base while CI pins the merge base
or the cut snapshot.

Each argv token is now classified from the workflow text as a LITERAL (verbatim
in the workflow, same on every run) or a VARIABLE (`${{ … }}`, a shell
expansion, a step output). An all-literal invocation renders in full and enters
`--commands`; one carrying a variable renders with the variable's own name in
the value position, is marked NOT RUNNABLE LOCALLY on its own labelled line
under its own heading, and is kept out of `--commands`, out of the pasted block
and out of the published harvest — the same treatment `ciOnly` already gets,
reached by a different measurement. No per-script declaration table: the split
reads off the workflow for all fifteen live invocations.

Two mechanisms make the classification possible. `joinLineContinuations`
splices a continued command into one line before any matcher runs, so the tail
is the WHOLE argv rather than the first physical line of it (comments are never
joined — a `#` runs to the newline, so its backslash is comment text).
`tailBeforeRedirection` drops a redirection's file descriptor, which `2>&1` had
been leaving on the argv side of the boundary.

Measured, before -> after over the tracked tree:

  check families discovered      240 -> 249   (+9, ZERO lost)
  watch-hint (gate, file) pairs  1499 -> 1528 (+29, ZERO lost)
  re-attributed                  0 — every surviving family keeps its exact
                                 workflow set and hint count

Beyond the nine the card named, the same sweep at this commit finds
`scripts/pm/check-half-states.mjs` (a tenth member: `half-state-patrol.yml`
runs it `--format=markdown --provenance="$PROVENANCE"` and nowhere else), and
`check-release-section-coverage.mjs --strict` — a second invocation in
`release-coverage-patrol.yml` that had no entry of its own because a
continuation hid it.

Self-test: 1338 cases pass. Two cases that pinned the retired refusal are
rewritten on the same fixtures to the new expectation; four that named a bare
key now name the script or the keyed invocation. New cases pin one literal
kind, one variable kind, the bare-invocation control, the tokenizer, the
continuation join and the live class read from the real workflows.

`scripts/pm/bare-root-worklist.mjs` records the verdict rows this re-keying
owes, per that script's own printed remedy: three rows for the retired bare
`check-adr-0087-registration` key are deleted and six second-key twins recorded
in their place, one per workflow invocation. Verdict and spelling carried
whole; no count restated, because this pass measured none.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026
@github-actions github-actions Bot added the size/l label Sep 4, 2026
@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Side effect worth a triage read: the standing finding in #14899

That card measures node scripts/pm/check-half-states.mjs being derived into the per-PR gate family for any diff carrying a changeset — the scheduled live board sweep, which CI never runs on a PR, and which sat for 3m09s on a dev container blocking the rest of the derived list behind it.

This PR's classifier reaches it as the tenth member of the value-bearing class, so the behaviour that card measures changes. Measured on this branch at e98bfc87, node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands .changeset/EXAMPLE.md:

pnpm check:pm-half-states          [still there: the offline self-test lint.yml runs]
(no `node scripts/pm/check-half-states.mjs` line at all)

and in the human rendering it now appears once, under the new heading, as what half-state-patrol.yml really runs:

Value-bearing argv — matched by path, and NOT runnable here:
  - node scripts/pm/check-half-states.mjs --format=markdown --provenance="$PROVENANCE"   [half-state-patrol.yml]   matched via .changeset/EXAMPLE.md ⇢ gate source '.changeset'
      ⛔ NOT RUNNABLE LOCALLY — 1 value(s) come from the workflow: $PROVENANCE

Two boundaries on that claim, stated so nothing here reads as more verified than it is:

  • The after state is measured on this branch. The before state is taken from that card's own quoted output plus this branch's baseline family reading at 369da918, where the family exists under the bare key scripts/pm/check-half-states.mjs with no not-runnable classification and therefore inside --commands. I did not re-run the base tool against that path.
  • ⛔ This PR makes no claim on that card and carries no closing keyword for it. It was filed against a defect that spans more than argv rendering (the routing question — a scheduled sweep reached through a .changeset watch hint — is untouched here), and its disposition is triage's, not this branch's.

Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 4, 2026 00:39
@os-steve
os-steve enabled auto-merge September 4, 2026 00:39
@os-steve
os-steve added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit f594e70 Sep 4, 2026
36 checks passed
@os-steve
os-steve deleted the claude/issue-15083-value-bearing-argv branch September 4, 2026 01:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants