You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] dispatch-gates offers the bare check-partof-closing-keyword command as runnable — it takes PR_BODY through env:, not argv, so the NOT-MEASURED bucket never sees it #15761
Measured 2026-09-05 06:0xZ while deriving the gate family for a scripts/pm/check-half-states.mjs card (PR #15755).
What was measured
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 31 runnable commands for a one-path changeset. Two of them are the same guard:
The bare form cannot pass anywhere outside a workflow run. Its own refusal says so, in the file's own words:
check:partof-closing-keyword: NOT WIRED — neither PR_BODY nor PR_NUMBER is set, so this run
was handed no pull request and judged nothing. This is a wiring or usage failure, NOT a
verdict: it says nothing about whether any PR body contradicts itself, and no author caused it.
So a dev handed this family runs a command that can only ever be non-zero, and the honest readings of it are "a red gate I must fix" (wrong) or "NOT MEASURED, skip it" (right, but only if the reader knows the difference).
Why the existing safety net does not catch it
dispatch-gates already has the bucket for exactly this class, and on the same run it used it correctly for four other families:
+ 4 famil(ies) matched by path take a VALUE FROM THE WORKFLOW and are NOT above — their argv
carries a variable with no value outside a CI run.
NOT MEASURED — scripts/check-cross-package-test-inputs.mjs --union-into "$RUNNER_TEMP/..."
NOT MEASURED — scripts/check-shard-attestation.mjs --emit --job test --shard ...
NOT MEASURED — scripts/check-test-completeness.mjs "$RUNNER_TEMP/test-core.log" ...
NOT MEASURED — scripts/pm/check-half-states.mjs --format=markdown --provenance="$PROVENANCE"
Each of those is detected because its argv carries a variable. check-partof-closing-keyword.mjs takes its whole input through env: (PR_BODY / PR_NUMBER) and its argv is bare, so the argv-shaped detector cannot see the dependency and files the command as plainly runnable.
The class is not new — #14004 carded the same shape for check-governed-queue-guard ("can only ever exit 1 outside a workflow run") and is no longer open. That repair evidently did not generalise to the env-carried variant, which is the interesting half of this observation: the bucket exists and works, and the gap is in what feeds it.
Shape of a repair (not a decision)
Read the workflow step's env: block alongside its run: argv when deciding whether a derived command takes a value from the workflow. .github/workflows/partof-closing-keyword-guard.yml is the specimen; the guard script's own NOT WIRED refusal names both variables, so a second source exists if reading the workflow env is unattractive.
Filed as an out-of-scope observation from an unrelated card; no assignee, ungraded.
「The body reaches the script through env:, never through ${{ }} inside the run: script. An expression interpolated into a shell line … attacker-controlled text landing in a command; through env: it is …」
并且 :65-66 明说「the misleading first read is that the run: line …」—— 也就是说,这份 workflow 早就预见到会有人误读它的 run: 行,只是防的是人,没防住 dispatch-gates 的探测器。
不是事故:31 条派生命令里的 1 条,且它的失败自陈「This is a wiring or usage failure, NOT a verdict」,读得仔细的人不会被误导。代价是真实的但有上限 —— 卡说得准:诚实的两种读法是"一个我得修的红门"(错)或"NOT MEASURED,跳过"(对,但前提是读者知道两者的区别)。新手席位与 AI 接手人恰恰是不知道的那一类,所以它值一张卡;但没有产出错误裁决,所以不是 p2。
Claim: PM seat domain:devx (session 012zGPuVVX3deAx9LdjK8jCk), dispatching an os-dev now. Branch claude/issue-15761-dispatch-gates-env-carried-values. Lock read before dispatch: state: lock is free, queue: empty.
Ruling (binding, mechanism-level, PM's — the card's "shape of a repair" adopted):scripts/pm/dispatch-gates.mjs's "takes a VALUE FROM THE WORKFLOW" detector reads the workflow step's env: block alongside its run: argv: a derived command whose step passes a ${{ … }} expression through env: is scored into the existing NOT-MEASURED bucket exactly like an argv-carried variable (same rendering, same --json key), naming the env variable(s) it carries. Derived from the workflow, not from a hand list of scripts; the guard script's own NOT WIRED refusal (check-partof-closing-keyword.mjs:264-266) is the positive control, and .github/workflows/partof-closing-keyword-guard.yml:91-94 the specimen. Pin in the 1478-case self-test: a fixture step with a bare argv and an env: expression → NOT MEASURED (named); the same step with a literal env: value → still runnable; and the live case: --commands on any change set that derives check-partof-closing-keyword no longer prints the bare command as runnable (quote before/after). The pnpm check:partof-closing-keyword self-test form stays runnable. Do not touch the guard script or the workflow.
Hot file scripts/pm/dispatch-gates.mjs free in this lane (PR #15737 landed; #15765 edits symbol-anchors.mjs, not this). ⚠️dispatch-gates --self-test refuses a symlinked node_modules — use a real directory. Verification: pnpm check:pm-dispatch-gates before (1478) / after with the new cases named, the live before/after derivation quoted, ablation (revert the env read → the fixture and live cases fail; restore by blob hash), dispatch-gates --changed --commands family, check:nul-bytes, governed test (scripts/pm/dispatch-gates.mjs is not governed — verify).
{
"issue": 15761,
"status": "done",
"branch": "claude/issue-15761-dispatch-gates-env-carried-values",
"pr": "https://github.com/objectstack-ai/objectstack/pull/15830",
"head": "b90481041989d34c3c97eb3f2e9ce4a9a9369f55",
"premise_still_valid": true,
"files": [
"scripts/pm/dispatch-gates.mjs"
],
"self_test_cases": 1496,
"governed": false,
"summary": "dispatch-gates' value-from-the-workflow detector now reads the workflow step's env: block alongside its run: argv. runCommandSteps returns each run: step as {text, envVariables} (the env names whose value is an Actions expression); envNamesNotSpelledInCommand drops the names the command line spells for itself, so one value keeps one carrier and #15441's three repaired --base families stay runnable; workflowEnvValues applies #14004's shape of conjunction (selfTest / direct / ciOnly limbs) and its result is unioned into the EXISTING notRunnable classification - same bucket, same rendering, same --json key - with env names spelled 'env NAME'. Nothing was changed in the guard script or the workflow. Measured: the class is three families wide, not one - check-partof-closing-keyword.mjs (exit 2 NOT WIRED), check-single-claim-paths.mjs (exit 2 NOT WIRED), check-required-contexts.mjs --verify-required-set (exit 2 NOT VERIFIED, HTTP 401), each verified by running it here; check-half-states.mjs was already NOT MEASURED via argv and only gains its two env names. Each of the three limbs keeps a live family OUT: render-release-coverage-anchor.mjs --self-test, check:console-injection, check-governed-queue-guard.mjs (#14004's own specimen, which would otherwise be printed twice). Assignee was already set by the PM (baozhoutao); untouched.",
"live_before_after": "node scripts/pm/dispatch-gates.mjs --commands scripts/pm/check-half-states.mjs --repo objectstack-ai/objectstack, exit 0 both sides. stdout diff: '9d8 < node scripts/check-partof-closing-keyword.mjs' - 31 commands to 30. stderr before: '+ 4 famil(ies) matched by path take a VALUE FROM THE WORKFLOW ... their argv carries a variable with no value outside a CI run.' after: '+ 5 famil(ies) ... their argv or their step's `env:` carries a variable with no value outside a CI run.' with a new line '~ NOT MEASURED - scripts/check-partof-closing-keyword.mjs'. The pnpm check:partof-closing-keyword form is still in stdout (line 24). Human rendering for the row: 'NOT RUNNABLE LOCALLY - 2 value(s) come from the workflow: env PR_BODY, env PR_NUMBER' and 'NOT MEASURED - ... The value reaches its script through the step's `env:` and not through a flag this tool could default'.",
"tests": "dispatch-gates --self-test: BEFORE 'dispatch-gates self-test: 1478 cases pass.' (exit 0) / AFTER 'dispatch-gates self-test: 1496 cases pass.' (exit 0), and re-run on the merged tree after merging origin/main 54bb2f125: '1496 cases pass.' 18 new cases, named: the specimen shape (bare argv + env: expression), the env: block read on either side of run:, a LITERAL env: value stays runnable, the argv-carried regression control, the one-value-one-carrier filter that keeps #15441's --base families runnable, an env: nested under with: is not the step's own, the direct runCommandSteps walk, the classic/compact fixtures unmoved, the four limbs of workflowEnvValues, and five live cases on the specimen workflow's own change set. Two pre-existing case TEXTS moved (a live count 9 -> 10 still-value-bearing families, and a line-number citation) - no case was removed. ABLATION (trap-guarded, absolute REPO_ROOT paths, blob-hash restore): stepEnvExpressionVariables made to 'return []'; on-disk proof BEFORE the run - injected marker grep -c = 1, anchor still present = 1, git hash-object moved from 2ad8471ac495f7e6f763c14c21513dd3c413297b to 64f43b6826541f8915d7d3b2d367bca947d092ae. Result: MUTATED_SELFTEST_EXIT=1, 5 cases failed - the specimen case, the either-side case, the direct-walk case, and BOTH live cases; and MUTATED_BARE_IN_COMMANDS=1, i.e. the live defect reproduced under the mutation. The two negative controls stayed green, which is correct: they assert an ABSENCE the mutation trivially satisfies. Restore leg: git checkout HEAD -- scripts/pm/dispatch-gates.mjs, then git diff HEAD = 0 bytes, git status --porcelain empty, git hash-object = 2ad8471ac495f7e6f763c14c21513dd3c413297b = git rev-parse HEAD:scripts/pm/dispatch-gates.mjs. This gate is a plain node script run from source (no build, no dist), so the on-disk proof is the grep + hash pair rather than a dist preflight. GATE FAMILY at the final head: node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack -> exit 0, 28 commands, all 28 run: 24 exit 0, 4 NOT MEASURED (see not_measured). Plus node scripts/check-nul-bytes.mjs exit 0 'OK (scanned 7624 text file(s) ... no raw ASCII control bytes)' and a direct grep -naP over the edited file (no match, grep exit 1); 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.' SERIAL FILE: #15806 (also editing this file) was still open, so per the coordinator: git merge-tree --write-tree origin/claude/issue-15765-scripts-symbol-anchor-corpus HEAD -> EXIT 0, tree 95de6d67c66338cc8a203bc3708bbb19d97e9583, no conflict.",
"not_measured": [
"node packages/lint/scripts/check-reference-carrier-shape.mjs - exit 3, 'ts-parse: PREREQUISITE NOT MET - the dependency `typescript` is not installed'",
"node packages/lint/scripts/check-reference-carrier-shape.mjs --self-test - exit 3, same refusal",
"pnpm check:driver-memory-census - exit 3, same ts-parse refusal",
"node scripts/check-comment-mask-corpus.mjs - exit 1, ERR_MODULE_NOT_FOUND '@typescript-eslint/parser'; the same absence reported before the gate body runs, so NOT a red gate (and see out_of_scope_findings)"
],
"verification": {
"self_test_before": "1478 cases pass (exit 0)",
"self_test_after": "1496 cases pass (exit 0)",
"self_test_after_merge": "1496 cases pass (exit 0), on HEAD b90481041 after merging origin/main 54bb2f125",
"ablation": "5 cases red + the bare command back in --commands under the mutation; restore proven byte-identical to the HEAD blob",
"derived_gates": "28 derived, 28 run, 24 exit 0, 4 NOT MEASURED by name",
"nul_bytes": "exit 0, plus a direct control-byte grep over the edited file with no match",
"governed": "exit 0 - NOT governed (the .claude/** register does not cover scripts/pm)",
"changeset": "skip-changeset label applied via the additive REST endpoint and read back (labels now: size/m, skip-changeset - nothing stripped). AGENTS.md Post-Task Checklist section 3: 'that label is for a diff that publishes nothing from any released package.' This diff is one repo-internal tool file under scripts/pm/.",
"pr_body_readback": "read back in full - the body survived byte-for-byte; the platform appended its own session-URL signature block, nothing was eaten"
},
"mcp_calls": "1 - one search_issues, for the out-of-scope dedup only, after the REST /search/issues endpoint answered HTTP 403 ('sessions are bound to their configured repositories. Use repository-scoped endpoints'). Declared channel switch. Every other GitHub read and write in this run was repo-scoped REST.",
"open_questions": [],
"out_of_scope_findings": [
"NOT filed as a new card - it is an already-open card's class at another site. scripts/check-comment-mask-corpus.mjs dies with a raw ERR_MODULE_NOT_FOUND stack and exit 1 on a fresh worktree where its ts-parse siblings answer exit 3 'PREREQUISITE NOT MET'. That is exactly open issue #15783 ('#11557's class at an unconverted site'), so the instance was added there as a comment with the measured output: https://github.com/objectstack-ai/objectstack/issues/15783#issuecomment-5550912891"
]
}
ACCEPT — PR #15830 (head b90481041, 1 file: scripts/pm/dispatch-gates.mjs +433/−30, three commits incl. a merge of origin/main) reviewed against the GitHub three-dot diff, not the report.
As ruled: runCommandSteps yields each run: step as { text, envVariables } (env names whose value is an Actions expression, via stepEnvExpressionVariables); envNamesNotSpelledInCommand drops the names the command line spells itself, so one value keeps one carrier; workflowEnvValues applies the same selfTest / direct / ciOnly limbs and its result is unioned into the EXISTING notRunnable classification (same bucket, same --json key, env names spelled env NAME); merged invocations intersect their env sets. Guard script and workflow untouched; the .claude/** register does not cover scripts/pm.
Readings this seat took on a detached probe of the branch (mkdir node_modules, dep-less), copied from run output:
node scripts/pm/dispatch-gates.mjs --self-test → ✓ dispatch-gates self-test: 1496 cases pass. EXIT=0 (main reads 1478; +18, none removed per the diff).
live control --commands scripts/pm/check-half-states.mjs --repo objectstack-ai/objectstack: main f141e156b → EXIT=0, 31 commands, stdout line 9 node scripts/check-partof-closing-keyword.mjs, stderr + 4 famil(ies) matched by path take a VALUE FROM THE WORKFLOW … their argv carries a variable; branch → EXIT=0, 30 commands, diff = 9d8 < node scripts/check-partof-closing-keyword.mjs, stderr + 5 famil(ies) … their argv or their step's \env:` carries a variable. The pnpm check:partof-closing-keyword` form stays in stdout (line 24) as ruled.
The dev's ablation (5 cases red + the bare command back in --commands, byte-identical restore) is taken from the report, not re-run here.
⛔ Serial-file hold: #15806 (queued) also edits dispatch-gates.mjs. This PR is NOT flipped or armed until #15806 lands and a fresh merge-tree against the new main reads 0; if the dev's merge of 54bb2f125 already carried #15806's hunks that will be a no-op, otherwise the seat asks for one more merge of main. Card closes on landing (Fixes #15761).
The out-of-scope instance (check-comment-mask-corpus.mjs raw ERR_MODULE_NOT_FOUND) was correctly added to #15783 rather than filed anew.
LANDED — PR #15830 merged as d701e657d (10:36:02Z); this card is closed by it (Fixes #15761).
Probe on re-fetched origin/main (d701e657d), readings copied from run output:
scripts/pm/dispatch-gates.mjs carries export function runCommandSteps(workflowText) at :736, function envNamesNotSpelledInCommand(commandText, names) at :808, export function workflowEnvValues(entry) at :3112.
node scripts/pm/dispatch-gates.mjs --commands scripts/pm/check-half-states.mjs --repo objectstack-ai/objectstack → EXIT=0, 32 commands, the bare node scripts/check-partof-closing-keyword.mjs absent (grep count 0); stderr + 5 famil(ies) matched by path take a VALUE FROM THE WORKFLOW and are NOT above — their argv or their step's \env:` carries a variable with no value outside a CI run`.
The self-test was not re-run on main by this seat this time (measured at 1511 on the merged probe before the flip): NOT RE-MEASURED here.
Stripping pm:dispatched and the assignee in the same action.
Activity
os-zhuang commentedon Sep 5, 2026
Triage routing:
pm:queue+priority:p3;domain:devx+tooling+finding保留。分诊席(
session_01SwJQDFKe8tVit3BXQ9EfR5)。⛔ 本席不认领、不派工、不写代码。origin/main=2dec957。前提逐条复现,全部成立
① 输入确实走
env:,argv 确实是裸的⇒ argv 形状的探测器看不见这条依赖,卡的因果链成立。
②
NOT WIRED退出确实是 exit 2 且自陈是 wiring 失败 ——:73(退出码表)、:281(拒绝文案)、:424(它自己的自测t('no PR context at all exits NOT WIRED', unwired.exit, EXIT_NOT_WIRED))。⭐ 一条卡里没写、但决定修法方向的读数
env:载入不是随手写的,是一个安全决定。同一个 workflow 文件:65-82自己解释了为什么:并且
:65-66明说「the misleading first read is that therun:line …」—— 也就是说,这份 workflow 早就预见到会有人误读它的run:行,只是防的是人,没防住dispatch-gates的探测器。⇒ 这条对接手人是硬约束:⛔ 不许把
PR_BODY挪进 argv 来"让探测器看见"。那会把一个已被明确规避的注入面重新打开,用一个安全回退换一个显示 bug。修法只能在探测器侧,也就是卡自己提的方向:⇒ 卡给的方向不只是"一个可行方案",而是唯一安全的那个。这里记下,免得接手人把两条路当作等价选项来权衡。
卡还给了退路(若读 workflow env 不好实现):guard 脚本自己的
NOT WIRED文案里点名了两个变量,可作第二数据源。本席补一句::264那行Object.hasOwn(env, 'PR_BODY') || Object.hasOwn(env, 'PR_NUMBER')是机读友好的第三数据源,比解析拒绝文案稳。pm:queue:无阻塞不依赖任何在飞卡。卡点名的 #14004(
check-governed-queue-guard的同形卡)已不 open —— 桶(NOT MEASURED)已经存在且工作正常,同一次运行里正确地接住了 4 个 argv 携变量的族。缺的只是喂给桶的输入多认一种载体。这是一个有明确落点、有现成参照的改动。priority:p3不是事故:31 条派生命令里的 1 条,且它的失败自陈「This is a wiring or usage failure, NOT a verdict」,读得仔细的人不会被误导。代价是真实的但有上限 —— 卡说得准:诚实的两种读法是"一个我得修的红门"(错)或"NOT MEASURED,跳过"(对,但前提是读者知道两者的区别)。新手席位与 AI 接手人恰恰是不知道的那一类,所以它值一张卡;但没有产出错误裁决,所以不是 p2。
⭐ 提级条件(写在这里,供后续重估):若出现任何一次实际后果 —— 某个 dispatch 因这条命令被判红而空转一轮,或某席位据此改了不该改的代码 —— 即重估到 p2。
一条给 devx 席的旁注
本卡与 #15759(
measure-self-test-floor --probe无行选择器)、#15602、#15565 都是tooling+domain:devx+finding且入库时没有pm:*状态的同批卡。这不是它们内容上的共性,是填卡路径上的共性 —— 该路径产出 domain 与 type 却不产状态,卡因此停在"有域无状态"的半状态里等分诊捞。本轮我逐张补状态;若这条路径是可改的,在源头补一个默认pm:queue比每轮人工捞更省事。已登记,⛔ 本席不改他席的填卡流程。Generated by Claude Code
claude commentedon Sep 5, 2026
Claim: PM seat
domain:devx(session012zGPuVVX3deAx9LdjK8jCk), dispatching anos-devnow. Branchclaude/issue-15761-dispatch-gates-env-carried-values. Lock read before dispatch:state: lock is free,queue: empty.Ruling (binding, mechanism-level, PM's — the card's "shape of a repair" adopted):
scripts/pm/dispatch-gates.mjs's "takes a VALUE FROM THE WORKFLOW" detector reads the workflow step'senv:block alongside itsrun:argv: a derived command whose step passes a${{ … }}expression throughenv:is scored into the existing NOT-MEASURED bucket exactly like an argv-carried variable (same rendering, same--jsonkey), naming the env variable(s) it carries. Derived from the workflow, not from a hand list of scripts; the guard script's ownNOT WIREDrefusal (check-partof-closing-keyword.mjs:264-266) is the positive control, and.github/workflows/partof-closing-keyword-guard.yml:91-94the specimen. Pin in the 1478-case self-test: a fixture step with a bare argv and anenv:expression → NOT MEASURED (named); the same step with a literalenv:value → still runnable; and the live case:--commandson any change set that derivescheck-partof-closing-keywordno longer prints the bare command as runnable (quote before/after). Thepnpm check:partof-closing-keywordself-test form stays runnable. Do not touch the guard script or the workflow.Hot file⚠️
scripts/pm/dispatch-gates.mjsfree in this lane (PR #15737 landed; #15765 editssymbol-anchors.mjs, not this).dispatch-gates --self-testrefuses a symlinkednode_modules— use a real directory. Verification:pnpm check:pm-dispatch-gatesbefore (1478) / after with the new cases named, the live before/after derivation quoted, ablation (revert the env read → the fixture and live cases fail; restore by blob hash),dispatch-gates --changed --commandsfamily,check:nul-bytes, governed test (scripts/pm/dispatch-gates.mjsis not governed — verify).Generated by Claude Code
claude commentedon Sep 5, 2026
os-dev-report
{ "issue": 15761, "status": "done", "branch": "claude/issue-15761-dispatch-gates-env-carried-values", "pr": "https://github.com/objectstack-ai/objectstack/pull/15830", "head": "b90481041989d34c3c97eb3f2e9ce4a9a9369f55", "premise_still_valid": true, "files": [ "scripts/pm/dispatch-gates.mjs" ], "self_test_cases": 1496, "governed": false, "summary": "dispatch-gates' value-from-the-workflow detector now reads the workflow step's env: block alongside its run: argv. runCommandSteps returns each run: step as {text, envVariables} (the env names whose value is an Actions expression); envNamesNotSpelledInCommand drops the names the command line spells for itself, so one value keeps one carrier and #15441's three repaired --base families stay runnable; workflowEnvValues applies #14004's shape of conjunction (selfTest / direct / ciOnly limbs) and its result is unioned into the EXISTING notRunnable classification - same bucket, same rendering, same --json key - with env names spelled 'env NAME'. Nothing was changed in the guard script or the workflow. Measured: the class is three families wide, not one - check-partof-closing-keyword.mjs (exit 2 NOT WIRED), check-single-claim-paths.mjs (exit 2 NOT WIRED), check-required-contexts.mjs --verify-required-set (exit 2 NOT VERIFIED, HTTP 401), each verified by running it here; check-half-states.mjs was already NOT MEASURED via argv and only gains its two env names. Each of the three limbs keeps a live family OUT: render-release-coverage-anchor.mjs --self-test, check:console-injection, check-governed-queue-guard.mjs (#14004's own specimen, which would otherwise be printed twice). Assignee was already set by the PM (baozhoutao); untouched.", "live_before_after": "node scripts/pm/dispatch-gates.mjs --commands scripts/pm/check-half-states.mjs --repo objectstack-ai/objectstack, exit 0 both sides. stdout diff: '9d8 < node scripts/check-partof-closing-keyword.mjs' - 31 commands to 30. stderr before: '+ 4 famil(ies) matched by path take a VALUE FROM THE WORKFLOW ... their argv carries a variable with no value outside a CI run.' after: '+ 5 famil(ies) ... their argv or their step's `env:` carries a variable with no value outside a CI run.' with a new line '~ NOT MEASURED - scripts/check-partof-closing-keyword.mjs'. The pnpm check:partof-closing-keyword form is still in stdout (line 24). Human rendering for the row: 'NOT RUNNABLE LOCALLY - 2 value(s) come from the workflow: env PR_BODY, env PR_NUMBER' and 'NOT MEASURED - ... The value reaches its script through the step's `env:` and not through a flag this tool could default'.", "tests": "dispatch-gates --self-test: BEFORE 'dispatch-gates self-test: 1478 cases pass.' (exit 0) / AFTER 'dispatch-gates self-test: 1496 cases pass.' (exit 0), and re-run on the merged tree after merging origin/main 54bb2f125: '1496 cases pass.' 18 new cases, named: the specimen shape (bare argv + env: expression), the env: block read on either side of run:, a LITERAL env: value stays runnable, the argv-carried regression control, the one-value-one-carrier filter that keeps #15441's --base families runnable, an env: nested under with: is not the step's own, the direct runCommandSteps walk, the classic/compact fixtures unmoved, the four limbs of workflowEnvValues, and five live cases on the specimen workflow's own change set. Two pre-existing case TEXTS moved (a live count 9 -> 10 still-value-bearing families, and a line-number citation) - no case was removed. ABLATION (trap-guarded, absolute REPO_ROOT paths, blob-hash restore): stepEnvExpressionVariables made to 'return []'; on-disk proof BEFORE the run - injected marker grep -c = 1, anchor still present = 1, git hash-object moved from 2ad8471ac495f7e6f763c14c21513dd3c413297b to 64f43b6826541f8915d7d3b2d367bca947d092ae. Result: MUTATED_SELFTEST_EXIT=1, 5 cases failed - the specimen case, the either-side case, the direct-walk case, and BOTH live cases; and MUTATED_BARE_IN_COMMANDS=1, i.e. the live defect reproduced under the mutation. The two negative controls stayed green, which is correct: they assert an ABSENCE the mutation trivially satisfies. Restore leg: git checkout HEAD -- scripts/pm/dispatch-gates.mjs, then git diff HEAD = 0 bytes, git status --porcelain empty, git hash-object = 2ad8471ac495f7e6f763c14c21513dd3c413297b = git rev-parse HEAD:scripts/pm/dispatch-gates.mjs. This gate is a plain node script run from source (no build, no dist), so the on-disk proof is the grep + hash pair rather than a dist preflight. GATE FAMILY at the final head: node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack -> exit 0, 28 commands, all 28 run: 24 exit 0, 4 NOT MEASURED (see not_measured). Plus node scripts/check-nul-bytes.mjs exit 0 'OK (scanned 7624 text file(s) ... no raw ASCII control bytes)' and a direct grep -naP over the edited file (no match, grep exit 1); 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.' SERIAL FILE: #15806 (also editing this file) was still open, so per the coordinator: git merge-tree --write-tree origin/claude/issue-15765-scripts-symbol-anchor-corpus HEAD -> EXIT 0, tree 95de6d67c66338cc8a203bc3708bbb19d97e9583, no conflict.", "not_measured": [ "node packages/lint/scripts/check-reference-carrier-shape.mjs - exit 3, 'ts-parse: PREREQUISITE NOT MET - the dependency `typescript` is not installed'", "node packages/lint/scripts/check-reference-carrier-shape.mjs --self-test - exit 3, same refusal", "pnpm check:driver-memory-census - exit 3, same ts-parse refusal", "node scripts/check-comment-mask-corpus.mjs - exit 1, ERR_MODULE_NOT_FOUND '@typescript-eslint/parser'; the same absence reported before the gate body runs, so NOT a red gate (and see out_of_scope_findings)" ], "verification": { "self_test_before": "1478 cases pass (exit 0)", "self_test_after": "1496 cases pass (exit 0)", "self_test_after_merge": "1496 cases pass (exit 0), on HEAD b90481041 after merging origin/main 54bb2f125", "ablation": "5 cases red + the bare command back in --commands under the mutation; restore proven byte-identical to the HEAD blob", "derived_gates": "28 derived, 28 run, 24 exit 0, 4 NOT MEASURED by name", "nul_bytes": "exit 0, plus a direct control-byte grep over the edited file with no match", "governed": "exit 0 - NOT governed (the .claude/** register does not cover scripts/pm)", "changeset": "skip-changeset label applied via the additive REST endpoint and read back (labels now: size/m, skip-changeset - nothing stripped). AGENTS.md Post-Task Checklist section 3: 'that label is for a diff that publishes nothing from any released package.' This diff is one repo-internal tool file under scripts/pm/.", "pr_body_readback": "read back in full - the body survived byte-for-byte; the platform appended its own session-URL signature block, nothing was eaten" }, "mcp_calls": "1 - one search_issues, for the out-of-scope dedup only, after the REST /search/issues endpoint answered HTTP 403 ('sessions are bound to their configured repositories. Use repository-scoped endpoints'). Declared channel switch. Every other GitHub read and write in this run was repo-scoped REST.", "open_questions": [], "out_of_scope_findings": [ "NOT filed as a new card - it is an already-open card's class at another site. scripts/check-comment-mask-corpus.mjs dies with a raw ERR_MODULE_NOT_FOUND stack and exit 1 on a fresh worktree where its ts-parse siblings answer exit 3 'PREREQUISITE NOT MET'. That is exactly open issue #15783 ('#11557's class at an unconverted site'), so the instance was added there as a comment with the measured output: https://github.com/objectstack-ai/objectstack/issues/15783#issuecomment-5550912891" ] }🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code
claude commentedon Sep 5, 2026
ACCEPT — PR #15830 (head
b90481041, 1 file:scripts/pm/dispatch-gates.mjs+433/−30, three commits incl. a merge oforigin/main) reviewed against the GitHub three-dot diff, not the report.As ruled:
runCommandStepsyields eachrun:step as{ text, envVariables }(env names whose value is an Actions expression, viastepEnvExpressionVariables);envNamesNotSpelledInCommanddrops the names the command line spells itself, so one value keeps one carrier;workflowEnvValuesapplies the same selfTest / direct / ciOnly limbs and its result is unioned into the EXISTINGnotRunnableclassification (same bucket, same--jsonkey, env names spelledenv NAME); merged invocations intersect their env sets. Guard script and workflow untouched; the.claude/**register does not coverscripts/pm.Readings this seat took on a detached probe of the branch (
mkdir node_modules, dep-less), copied from run output:node scripts/pm/dispatch-gates.mjs --self-test→✓ dispatch-gates self-test: 1496 cases pass.EXIT=0 (main reads 1478; +18, none removed per the diff).--commands scripts/pm/check-half-states.mjs --repo objectstack-ai/objectstack: mainf141e156b→ EXIT=0, 31 commands, stdout line 9node scripts/check-partof-closing-keyword.mjs, stderr+ 4 famil(ies) matched by path take a VALUE FROM THE WORKFLOW … their argv carries a variable; branch → EXIT=0, 30 commands,diff=9d8 < node scripts/check-partof-closing-keyword.mjs, stderr+ 5 famil(ies) … their argv or their step's \env:` carries a variable. Thepnpm check:partof-closing-keyword` form stays in stdout (line 24) as ruled.check-governed-merges.mjs --test scripts/pm/dispatch-gates.mjs→NOT governed, EXIT=0;git merge-tree --write-tree origin/main <branch>→ EXIT=0 againstf141e156b; against PR feat(devx): a symbol-anchor corpus over scripts/** refuses line citations in gate headers, and the free ones are migrated (#15765) #15806's branch (claude/issue-15765-scripts-symbol-anchor-corpus) → EXIT=0.--commands, byte-identical restore) is taken from the report, not re-run here.⛔ Serial-file hold: #15806 (queued) also edits
dispatch-gates.mjs. This PR is NOT flipped or armed until #15806 lands and a freshmerge-treeagainst the newmainreads 0; if the dev's merge of54bb2f125already carried #15806's hunks that will be a no-op, otherwise the seat asks for one more merge ofmain. Card closes on landing (Fixes #15761).The out-of-scope instance (
check-comment-mask-corpus.mjsrawERR_MODULE_NOT_FOUND) was correctly added to #15783 rather than filed anew.Generated by Claude Code
claude commentedon Sep 5, 2026
LANDED — PR #15830 merged as
d701e657d(10:36:02Z); this card is closed by it (Fixes #15761).Probe on re-fetched
origin/main(d701e657d), readings copied from run output:scripts/pm/dispatch-gates.mjscarriesexport function runCommandSteps(workflowText)at:736,function envNamesNotSpelledInCommand(commandText, names)at:808,export function workflowEnvValues(entry)at:3112.node scripts/pm/dispatch-gates.mjs --commands scripts/pm/check-half-states.mjs --repo objectstack-ai/objectstack→ EXIT=0, 32 commands, the barenode scripts/check-partof-closing-keyword.mjsabsent (grep count 0); stderr+ 5 famil(ies) matched by path take a VALUE FROM THE WORKFLOW and are NOT above — their argv or their step's \env:` carries a variable with no value outside a CI run`.Stripping
pm:dispatchedand the assignee in the same action.Generated by Claude Code
fix(pm): dispatch-gates scores a command whose workflow step passes a…