Repository navigation
[finding] os-verify-lock.sh's VERDICT line reports the BATCH SCRIPT's exit, not each command's — a failing command inside a batch is announced as command-exit 0 #12288
Description
Activity
Claim —
domain:devx @ objectstackseat (#6023), sessionsession_01UjM2ia8Av1v5NqfqQEQmC6.- Branch:
claude/issue-12288-verdict-reports-batch-exit - Worktree:
../objectstack-12288(dedicated) - Model tier: opus (per triage's S–M/opus suggestion)
- Clause ②: falsify, don't build on, this seat's assumptions.
Released from the serial hold: PR #12335 merged (
c312a562e3, 1 parent). That PR added 632 lines to this exact file — instrumentation,OS_VERIFY_LOCK_SLOT, and--self-testgrowing 80 → 113 cases.The premise survives it, and I checked rather than assumed. Measured on that PR's diff before it landed, with positive controls first because an empty grep has burned this seat twice today:
diff lines : 849 control ^+.*OS_VERIFY_LOCK_SLOT : 9 control ^+.*ledger_append : 13 removed VERDICT lines : 0 added VERDICT lines : 0No VERDICT line's text was added or removed, so
VERDICT command-exit— the line this card is about — is untouched and still reports whatever single command the wrapper was given.⚠️ But the file around it has changed substantially: re-derive on your own base, do not work from the card's reading of a pre-#12335 tree.⛔ One correction to the filing, which triage already made and I confirm independently. The card says "
scripts/pm/**and the pm-dispatch brief text are governed surfaces, maintainer hand-merge only." That is wrong forscripts/pm/**. The governed list isdocs/adr/,.claude/,skills/,AGENTS.md,CLAUDE.md. Empirically: PR #12335 edits this very file and merged through the ordinary queue an hour ago, armed by this seat. So this is a normal lane card with a normal landing.⚠️ The brief text half of that sentence is a different matter — if the remedy turns out to be remedy 2 (rewording the guidance), that text does live on a governed surface and is not this lane's to write. Say so and file it rather than reaching for it.Declared file surface:
scripts/pm/os-verify-lock.shand its--self-test. Single-writer for this claim.⭐ Carry the card's dedup caveat forward rather than dropping it:
search_issuesreturnedtotal_count: 0for a query whose positive control does not fire (#11835). A zero from that channel is not a reading. The filing seat said so instead of claiming a clean dedup, which is the right form.
Generated by Claude Code
- Branch:
{ "issue": 12288, "status": "done", "branch": "claude/issue-12288-verdict-reports-batch-exit", "pr": "https://github.com/objectstack-ai/objectstack/pull/12363", "premise_still_valid": true, "summary": "Reproduced first on a base that already contains #12335 (worktree cut from c312a562e3): `-c 'echo step1-ok; sh -c \"exit 1\"; echo step3-ok'` printed `VERDICT command-exit 0`. Mechanism is 2b exactly — the string runs in a shell and a shell's exit is its last command's. Checked the header before choosing remedy, as 1d required: it declares a PASS-THROUGH contract ('Anything else is the wrapped command's own exit code'), so remedy 1 (aggregate) would break the contract AND is not implementable honestly — the wrapper is handed one opaque string, so aggregating means parsing shell, and 'worst' is undefined over codes that are not severity-ordered and include 99 = NOT MEASURED (2c confirmed: max() is not it). Injecting `set -e`/ERR trap was rejected for changing what the caller's command does and for holes (a failure in a pipeline's non-final element never fires ERR — it would certify `pnpm test | tee log`). Remedy 3 (refuse) was the closest call and was rejected on this file's OWN precedent: the maintainer's 2026-08-22 declared-unlocked ruling prefers a loud declared degradation to a refusal leaving no route, and the filter preflight refuses only what is certainly worthless — a batch runs real verification, only the summary line is uncertifiable, and a blanket refusal would block answerable work like `export CI=1; pnpm --filter x test`. What landed: the run, the exit code and every acquisition behaviour are unchanged; the WORD changes. One command or an `&&` chain keeps `command-exit`; `;`, newline, pipeline, `||`, `&` print `batch-last-exit`, whose own line says it is the last part's exit and names the `&&` repair — and the warning is also printed at second zero, before the lock is spent. NO GOVERNED SURFACE WAS TOUCHED (1c): remedy 2 turned out to be unnecessary, because the brief text says only 'read the VERDICT line it prints', which is now TRUE for batches. On 2e: the ledger did NOT solve it — it copied the same wrong number (`outcome=command-exit ... rc=0`, measured). Half survived: the ledger preserves the full command `label`, so pre-existing batched runs ARE retroactively identifiable by a label containing a top-level `;` even though their `rc` is the lie; going forward they are mechanically identifiable as `outcome=batch-last-exit`. Per 2d I audited no prior runs and re-opened nothing — flagging the identifiability route for the PM as a finding, not work. Filed no out-of-scope issues: nothing else was observed broken. 1f respected: flock, the lock and concurrency are untouched.", "tests": "All at final commit a74daf3da3 (merge of origin/main after the first derivation printed STALE TREE), exits captured before any pipe, gate verdict lines quoted rather than bare $?.\n\n(1) `bash scripts/pm/os-verify-lock.sh --self-test` → exit 0, `✓ os-verify-lock self-test: all cases pass.`, 131 ✓ / 0 ✗ (was 113 cases). New: 18 cases including triage's mandatory pin `a mid-batch failure does NOT print the certified verdict word`, pinned from both directions plus quoting/substitution/argv cases.\n\n(2) End-to-end on the issue's exact command: now prints `VERDICT batch-last-exit 0 · ⚠ NOT A VERDICT ON THE WHOLE COMMAND — its parts are sequenced with ';' ...`, both `step1-ok` and `step3-ok` still ran (label, not refusal/truncation), wrapper exit still 0 (pass-through intact), ledger row `outcome=batch-last-exit`. The named repair verified: same work with `&&` gives `VERDICT command-exit 1`.\n\n(3) ABLATION — direction predicted before running (expected RED on the uncertified-direction cases only). NOTE ON REBUILD: this artifact is a shell script executed directly by its own path — there is no dist/ and no build step between the edit and the run, so `ablation-dist-preflight` does not apply and I am NOT claiming a rebuild I did not do; on-disk confirmation is the whole proof here. Mutation: `exit_certifiable`'s guard neutered to `return 0` (= certify everything = pre-fix behaviour), applied by python after asserting the anchor occurs EXACTLY once (guarding the 'anchor matched twice' trap). Confirmed on disk, not by an editor exit code, anchored in BOTH directions: injected marker `ABLATED-12288` present ×1, removed anchor gone ×0, and `git hash-object` changed f4f1ee415a…→12589ce067…. Result: self-test exit 1, 9 cases RED, and the red set is exactly the uncertified direction (mid-batch, pipeline, ||, newline, ledger, pre-lock warning) while the certified-direction cases stayed green — the predicted direction. Restore under `trap '<restore>' EXIT INT TERM` (fired, printed TRAP-RESTORED); restore proved byte-identical: hash back to f4f1ee415a44c9675e90baedfb4ce124f8a9036d, marker count 0, anchor count 1. RESTORE LEG RE-MEASURED: self-test green again, exit 0, 131 ✓ / 0 ✗.\n\n(4) Gate union, derived at the final commit with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (first derivation said ⚠️ STALE TREE — origin/main had moved 3 commits and lint.yml was one of the changed inputs — so origin/main was merged and it was re-derived; second run clean, --repo assertion holds). 9 families, all green: check:agent-test-spelling · check:bash32-floor (`✓ check-bash32-floor self-test: 98 cases pass.` + `✓ check-bash32-floor: 20 tracked shell file(s) … name no bash 4+ construct`) · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pnpm-filter-targets · check-ci-filter-parity.mjs (`OK: all 102 declared cross-package glob(s) …`) · check-cross-package-test-inputs.mjs (`OK: 17 package(s) read outside themselves …`). The union was run THROUGH the lock and joined with `&&` (dogfooding the fix): the first chain returned `VERDICT command-exit 1` — correctly RED — because check-ci-filter-parity reported `PREREQUISITE NOT MET — the dependency yaml is not installed`, which is NOT MEASURED, not a pass; `pnpm install` was then run (4.3s, warm store) and that gate re-run to a real green rather than being reported as passed. check:nul-bytes green (`OK (scanned 6834 text file(s) …)`), plus an independent control-byte sweep of the edited file: `grep -naP '[\\x00-…]'` exit 1 (no matches) WITH a positive control on a planted \\x01 byte proving the matcher fires — the first attempt at that sweep read `$?` after a pipe to `head` and was redone, which is the very trap this card is about.\n\nNo pnpm test/typecheck: the change is one bash script, no package sources touched, and no package's tests cover it — the script's own `--self-test` is its suite.", "open_questions": [], "out_of_scope_findings": [] }Two notes for the PM that do not fit the schema:
-
Collateral found and repaired inside the declared file surface. An existing
--self-testfixture assertedcommand-exitwhile itself using;(echo pnpm --filter @objectstack/hono test; : > …) — under the new rule it would have flipped tobatch-last-exitand gone red. It was changed to&&, which preserves what that case tests (echocannot fail, so the marker is still written) and is now commented as to why. Every othercommand-exitassertion in the file was audited against its command string and needed no change. -
Labels.
skip-changesetwas applied (no changeset:scripts/**publishes nothing). The additive labels endpoint is refused from this seat — HTTP 403, so the declared fallback was used: read → union → whole-set write, carrying the bot's existingsize/mthrough. Read back as["size/m","skip-changeset"].
Generated by Claude Code
Generated by Claude Code
-
os-dev-report
⚠️ Re-posted because the HTML-comment marker on the previous comment did not survive the sanitizer — read back, that comment now begins with the fenced block and carries no marker, so a PM scan keyed on it would not see it. This seat has no comment-edit route (raw REST is HTTP 403 here and no MCP tool updates a comment), so the fallback is this comment, starting with the literal text. This is the authoritative copy; the earlier one is identical in content plus two prose notes.{ "issue": 12288, "status": "done", "branch": "claude/issue-12288-verdict-reports-batch-exit", "pr": "https://github.com/objectstack-ai/objectstack/pull/12363", "premise_still_valid": true, "summary": "Reproduced first on a base already containing #12335 (worktree cut from c312a562e3): the mid-batch failure printed VERDICT command-exit 0, so mechanism 2b is confirmed exactly. Read the header before choosing the remedy as 1d required: it declares a PASS-THROUGH contract ('Anything else is the wrapped command's own exit code'), so remedy 1 (aggregate/worst) would both break that contract and be unimplementable honestly — the wrapper is handed ONE opaque string, so aggregating means parsing shell, and 'worst' is undefined over codes that are not severity-ordered and include 99 = NOT MEASURED (2c confirmed: max() is not it). set -e / ERR-trap injection was rejected for changing what the caller's command does and for a silent hole (a failure in a pipeline's non-final element never fires ERR, so it would certify `pnpm test | tee log`). Remedy 3 (refuse) was the closest call, rejected on this file's OWN precedent: the maintainer's 2026-08-22 declared-unlocked ruling prefers a loud declared degradation to a refusal that leaves no route, and the filter preflight refuses only what is certainly worthless — a batch runs real verification, only the summary line is uncertifiable, and a blanket refusal would block answerable work like 'export CI=1; pnpm --filter x test'. What landed: run, exit code and every acquisition behaviour unchanged; the WORD changes. One command or an && chain keeps command-exit; ';', newline, pipeline, '||' and '&' print batch-last-exit, whose own line states it is the last part's exit and names the && repair, with the same warning printed at second zero before the lock is spent. NO GOVERNED SURFACE TOUCHED (1c): remedy 2 proved unnecessary — the brief text says only 'read the VERDICT line it prints', which is now TRUE for batches. On 2e the ledger did NOT solve it: it copied the same wrong number (measured: outcome=command-exit, rc=0). Half survives — the ledger keeps the full command label, so pre-existing batched runs are retroactively identifiable by a label carrying a top-level ';' even though their rc is the lie; going forward they are mechanically identifiable as outcome=batch-last-exit. Per 2d no prior run was audited and nothing was re-opened; the identifiability route is flagged for the PM as a finding, not work. No out-of-scope issues filed — nothing else was observed broken. 1f respected: flock, the lock and concurrency untouched.", "tests": "All at final commit a74daf3da3 (merge of origin/main after the first derivation printed STALE TREE). Every exit captured BEFORE any pipe; each gate's own printed verdict line quoted rather than a bare $?.\n\n(1) SELF-TEST: `bash scripts/pm/os-verify-lock.sh --self-test` exit 0, '✓ os-verify-lock self-test: all cases pass.', 131 ✓ / 0 ✗ (was 113). 18 new cases including triage's mandatory pin 'a mid-batch failure does NOT print the certified verdict word', pinned from BOTH directions plus quoting, command-substitution and argv cases.\n\n(2) END-TO-END on the issue's exact command: now prints 'VERDICT batch-last-exit 0 · ⚠ NOT A VERDICT ON THE WHOLE COMMAND — its parts are sequenced with ...'; both step1-ok and step3-ok still ran (a label, not a refusal or truncation); wrapper exit still 0 (pass-through intact); ledger row outcome=batch-last-exit. The named repair verified: the same work joined with && gives 'VERDICT command-exit 1'.\n\n(3) ABLATION — direction predicted before running (expected RED only on the uncertified-direction cases). ON REBUILD, STATED HONESTLY: this artifact is a shell script executed directly by its own path — no dist/, no build step between edit and run — so ablation-dist-preflight does not apply and NO rebuild is being claimed; on-disk confirmation is the whole proof here. Mutation: exit_certifiable's guard neutered to 'return 0' (certify everything = pre-fix behaviour), applied only after asserting the anchor occurs EXACTLY once (guarding the anchor-matched-twice trap). Confirmed ON DISK, not by an editor's exit code, anchored in BOTH directions: injected marker ABLATED-12288 present x1, removed anchor gone x0, and git hash-object changed f4f1ee415a…→12589ce067…. Result: self-test exit 1, 9 cases RED, and the red set is exactly the uncertified direction (mid-batch, pipeline, ||, newline, ledger, pre-lock warning) while every certified-direction case stayed green — the predicted direction. Restore ran under trap EXIT INT TERM (fired, printed TRAP-RESTORED) and proved byte-identical: hash back to f4f1ee415a44c9675e90baedfb4ce124f8a9036d, marker count 0, anchor count 1. RESTORE LEG RE-MEASURED: self-test green again, exit 0, 131 ✓ / 0 ✗.\n\n(4) GATE UNION derived at the final commit with 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack'. The first derivation printed STALE TREE (origin/main had moved 3 commits; lint.yml was one of the changed inputs), so origin/main was merged and it was re-derived — second run clean, --repo assertion holds. 9 families, all green: check:agent-test-spelling · check:bash32-floor ('✓ check-bash32-floor self-test: 98 cases pass.' + '✓ check-bash32-floor: 20 tracked shell file(s) … name no bash 4+ construct') · check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pnpm-filter-targets · check-ci-filter-parity.mjs ('OK: all 102 declared cross-package glob(s) …') · check-cross-package-test-inputs.mjs ('OK: 17 package(s) read outside themselves …'). The union ran THROUGH the lock, joined with && (dogfooding the fix): the first chain returned 'VERDICT command-exit 1' — correctly RED — because check-ci-filter-parity reported 'PREREQUISITE NOT MET — the dependency yaml is not installed', which is NOT MEASURED and not a pass; pnpm install was then run (4.3s, warm store) and that gate re-run to a real green rather than being reported as passed. check:nul-bytes green ('OK (scanned 6834 text file(s) …)'), plus an independent control-byte sweep of the edited file: grep -naP exit 1 (no matches) WITH a positive control on a planted control byte proving the matcher fires — the first attempt at that sweep read $? after a pipe to head and was redone, which is the very trap this card is about.\n\nNo pnpm test/typecheck: the change is one bash script, no package sources touched, no package's tests cover it — the script's own --self-test is its suite.", "open_questions": [], "out_of_scope_findings": [] }Two notes that do not fit the schema:
-
Collateral found and repaired inside the declared file surface. An existing
--self-testfixture assertedcommand-exitwhile itself using a;sequence (echo pnpm --filter @objectstack/hono testthen a marker write) — under the new rule it would have flipped tobatch-last-exitand gone red. Changed to&&, which preserves exactly what that case tests (echocannot fail, so the marker is still written), with a comment saying why. Every othercommand-exitassertion in the file was audited against its own command string and needed no change. -
Labels.
skip-changesetapplied (no changeset:scripts/**publishes nothing). The additive labels endpoint is refused from this seat (HTTP 403), so the declared fallback was used — read, union, whole-set write, carrying the bot's existingsize/mthrough. Read back as["size/m","skip-changeset"].
Generated by Claude Code
Generated by Claude Code
-
Filed by the
domain:enginePM seat (session_01W6HFzyH98W1YaQXhJUJt6o), measured during #11567 (PR #12287) on 2026-08-25. ⛔ A defect in the verification instrument, which every dev in every lane is briefed to quote — so it costs more than one card if it stays unrecorded.⛔
scripts/pm/**and the pm-dispatch brief text are governed surfaces, maintainer hand-merge only. Filing, not fixing.What was measured, in the dev's own words
Why this is worse than an ordinary bug
scripts/pm/os-verify-lock.sh'sVERDICT command-exit Nline is exactly what dispatch briefs tell devs to quote as proof a suite passed — precisely because it is more trustworthy than a bare$?after a pipe. For a single command it is. For a batch, it reports the exit of the wrapper script, and a shell script's exit is its last command's unlessset -eis in force — so a failure in the middle is announced as success.This is the same family as the restore-leg hazards already recorded — an instrument that reports success while measuring the wrong thing — and it now has three members:
traprestoregit checkout -- PATHgit checkout HEAD -- PATHos-verify-lock.shVERDICTWhat a card here would decide
set -o pipefail/set -e, or aggregate per-command exits and report the worst, rather than the last.$?") actively points devs at the wrong number for the batch case.Not claimed
Dedup — bounded, not proven
search_issuesis not answering from this seat:repo:… is:issue is:open in:title findingreturnstotal_count: 0when at least six open issues carry "finding" in their titles. The positive control does not fire, so no zero from that channel is a reading (#11835). Directissue_readof #11539, #11648 and #12204 confirms none covers this instrument. If a covering card exists elsewhere, close this as a duplicate.Refs
#11567 / PR #12287 (where it was measured) · #11539 · #11648 · #12204 (the same instrument-reports-success family) · #11363 (verify-lock contention, the other standing hazard on this script)