fix(pm): dispatch-gates scores a command whose workflow step passes an expression through env: as NOT MEASURED, like an argv-carried one (#15761) - #15830
Conversation
…as value-bearing The "takes a VALUE FROM THE WORKFLOW" detector read the `run:` argv only, so a step that passes its input through `env:` — with a bare argv — scored as a command a dev can paste. `.github/workflows/partof-closing-keyword-guard.yml` is the specimen: PR_BODY / PR_NUMBER arrive through `env:` and `node scripts/check-partof-closing-keyword.mjs` was offered as runnable, while the gate's own refusal calls that run NOT WIRED and exits 2. `runCommandSteps` now returns each `run:` step with the `env:` names whose value is an Actions expression, minus the ones the command line spells for itself (those are argv-carried and already read). `workflowEnvValues` applies the same shape of conjunction #14004 used for the event payload, and the union feeds the existing NOT-MEASURED bucket unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…478 -> 1496) Fixture pins for the specimen shape, the literal-`env:` negative, the argv-carried regression control, the one-value-one-carrier filter that keeps #15441's repaired `--base` families runnable, and the four limbs of `workflowEnvValues`. Plus the live pin on the specimen workflow's own change set: the bare invocation leaves `--commands` and is named NOT MEASURED, while the `pnpm check:` form stays. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…spatch-gates-env-carried-values
|
Flipped ready + auto-merge enabled (10:04Z, Generated by Claude Code |
Fixes #15761
dispatch-gates' "takes a VALUE FROM THE WORKFLOW" detector read therun:argv and nothing else, so a step that hands its command the input throughenv:— with a bare argv — scored as a command a dev can paste.The specimen
.github/workflows/partof-closing-keyword-guard.yml:91-94Positive control, the gate's own refusal (
scripts/check-partof-closing-keyword.mjs:264-266, run here at HEAD):⛔ The repair REFUSED: moving
PR_BODYinto the argv so the existing detector sees it. Thatenv:spelling is a security decision the workflow states at length (:65-82); reopening a deliberately closed injection surface to fix a display bug trades the two the wrong way round. The repair is on the reader's side.What changed
runCommandSteps(workflowText)returns eachrun:step as{ text, envVariables }— the command textrunCommandTextshas always returned, plus theenv:names whose value is an Actions expression.runCommandTextsis now a.map()over it and is byte-identical in behaviour.envNamesNotSpelledInCommanddrops the env names the command line spells for itself (--base "$MERGE_BASE"): one value, one carrier. This is what keeps [finding] dispatch-gates offerscheck-adr-0087-registration --self-testas the runnable member and files the REAL check as NOT RUNNABLE LOCALLY — but the script's own usage line defaults--basetoorigin/main#15441's three repaired--basefamilies runnable.workflowEnvValues(entry)is the generalisation of [finding] dispatch-gates lists check-governed-queue-guard among the LOCAL gates, but it can only ever exit 1 outside a workflow run #14004'sciOnlyMeasurement, same shape of conjunction:selfTest/direct/ciOnlylimbs, each with a live case on this tree. Its result is unioned into the existingnotRunnableclassification — same bucket, same rendering, same--jsonkey — with the env names spelledenv NAMEso the two carriers stay legible in one list.--commandsstderr accounting line, the--ranreconciliation bucket, the human heading, the whole-tree row).env:is read only at the step's own key column, only its direct children, block form only; a job-level or flow-mappingenv:is not read. Every one of those under-reports — the loud direction — and none appears in this tree.The class is three families wide, not one
Measured on this tree: 405
run:steps, 52 carrying anenv:expression, 13 of those invoking a check. Eight spell every env name in the command itself and are filtered out. Of the five reaching the limbs, the conjunction selects three — each verified by running it here, each refusing rather than judging:node scripts/check-partof-closing-keyword.mjsNOT WIRED — neither PR_BODY nor PR_NUMBER is setnode scripts/check-single-claim-paths.mjsNOT WIRED — PR_NUMBER is not setnode scripts/check-required-contexts.mjs --verify-required-setNOT VERIFIED — … answered HTTP 401A fourth,
scripts/pm/check-half-states.mjs --format=markdown --provenance="$PROVENANCE", was already NOT MEASURED through its argv and only gains the two env names it also carries.The three limbs each keep a live family OUT of the classification:
render-release-coverage-anchor.mjs --self-test(selfTest),check:console-injection(direct — its key drops the argv, andci.yml/release.yml/the root manifest run it in different environments),check-governed-queue-guard.mjs(ciOnly — #14004's own specimen, which would otherwise be printed as two omissions).Live before/after
node scripts/pm/dispatch-gates.mjs --commands scripts/pm/check-half-states.mjs --repo objectstack-ai/objectstack— the card's own change set, exit 0 both sides:31 commands → 30. stderr, before:
after:
pnpm check:partof-closing-keyword— the form that takes nothing from the workflow — is still in stdout, line 24. The human rendering names the row:Pins — self-test 1478 → 1496,
1496 cases passFixture: the specimen shape (bare argv +
env:expression, read on either side ofrun:); a LITERALenv:value stays runnable ($HOME/not-an-expressionincluded — Actions does not substitute shell forms in an env value); the argv-carried regression control; the one-value-one-carrier filter; anenv:nested underwith:is not the step's own; the classic and compact fixtures above still carry no env expression. Unit: the four limbs ofworkflowEnvValues. Live, on the specimen workflow's own change set: the bare invocation is no longer offered as runnable, it is named⊘ NOT MEASUREDon stderr, andpnpm check:partof-closing-keywordis still in the list.Ablation — trap-guarded, blob-hash restore
stepEnvExpressionVariablesmade toreturn [](marker injected,grep -c= 1,git hash-objectmoved off the HEAD blob before the run):— the live defect reproduced under the mutation. The two negative controls stayed green under it, which is correct: they assert an ABSENCE the mutation trivially satisfies. Restored
git checkout HEAD -- scripts/pm/dispatch-gates.mjs:git diff HEAD0 bytes,git status --porcelainempty,git hash-object=2ad8471ac495f7e6f763c14c21513dd3c413297b=git rev-parse HEAD:scripts/pm/dispatch-gates.mjs.Verification
Derived at this head:
node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack→ exit 0, 28 commands, all run. 24 exit 0. Four are NOT MEASURED in this container, by name, all one cause — this worktree runsdispatch-gatesfrom a bare checkout with nopnpm install:node packages/lint/scripts/check-reference-carrier-shape.mjs— exit 3,ts-parse: PREREQUISITE NOT MET — the dependency typescript is not installednode packages/lint/scripts/check-reference-carrier-shape.mjs --self-test— exit 3, samepnpm check:driver-memory-census— exit 3, samenode scripts/check-comment-mask-corpus.mjs— exit 1,ERR_MODULE_NOT_FOUND: Cannot find package '@typescript-eslint/parser'— the same absence, reported before the gate body runs, so it is NOT a red gatenode scripts/check-nul-bytes.mjs→ exit 0,OK (scanned 7624 text file(s) … no raw ASCII control bytes); plus a directgrep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over the edited file, no match.node scripts/pm/check-governed-merges.mjs --test scripts/pm/dispatch-gates.mjs→ exit 0,✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list.Changeset
skip-changeset: the diff is one repo-internal tool file underscripts/pm/and publishes nothing from any released package — AGENTS.md, Post-Task Checklist §3: "that label is for a diff that publishes nothing from any released package."Serial file
scripts/pm/dispatch-gates.mjsis also touched by #15806 (comment-only citation migrations plus one residue-ledger reason string). At the time of writing #15806 is open, so the textual probe was run instead of a merge:git merge-tree --write-tree origin/claude/issue-15765-scripts-symbol-anchor-corpus HEAD→ exit 0, tree95de6d67c66338cc8a203bc3708bbb19d97e9583. No conflict. This branch already carries a merge oforigin/mainat54bb2f125(merge commit, no rebase), and the self-test was re-run on the merged tree:✓ dispatch-gates self-test: 1496 cases pass.🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code