Repository navigation
os-verify-lock.sh reads the & in a 2>&1 redirection as a background operator, downgrading a correctly &&-joined command to batch-last-exit #12518
Description
Activity
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsTriage: lands in
scripts/pm/os-verify-lock.sh→domain:skills,pm:queue, type Bug — the scanner's declared semantics (flag backgrounding, certify&&-joined commands) are contradicted by its own behavior on the2>&1redirection form, measured and isolated to one variable in the card. Lane call: the script's SUBJECT is agent command-exit certification discipline (pm-dispatch loop machinery), which is agent-workflow governance, not code/docs quality — per the devx/skills boundary that puts governance-of-agent-conduct gates with skills; the card itself names the closed guard-main-checkout-bash.sh precedent (same family, skills territory) and says the fix should rhyme with it.Size/model suggestion: S (mechanical parser fix + regression pin in the existing
st_caseself-test suite; the card already states the fix shape — look back for an unescaped>before the&). Dispatch note:scripts/pm/**is a pm-dispatch governance face — per SKILL.md clause ① this is a mechanical line-level change eligible for the opus-build path with contract-review-tier review on the skills seat's side.Dedup: #10570 is the named sibling (different script, closed) — not a duplicate. No other open card names os-verify-lock.sh's operator scan.
Generated by Claude Code
Claim: PM loop round R1 W1
Session:session_01MnijPVVDakqK2J335JoJtq
Branch:claude/issue-12518-verify-lock-redirection
Worktree:objectstack-issue-12518
Domain:domain:skills
File surface:scripts/pm/os-verify-lock.sh(the scanner fix + its ownst_caseself-test suite) (stop on breach; explain in the report)
Container & model: S (mechanical parser fix + regression pins),mode:subagent,model: opus— this dispatch'snode scripts/pm/dispatch-gates.mjs --tier scripts/pm/os-verify-lock.sh(tree0043c92): "no path-derived mandate … floor sonnet · default opus · ceiling fable"; the judgment face is a shell-tokenizer edge case, so default opus rather than the sonnet floor.
Clause-②: no — internal PM-loop tool parser fix; no contract accept/reject behaviour change, no public surface widened.
Serial constraints cleared: no open PR and no in-flight claim touchesscripts/pm/os-verify-lock.sh(all 11 open PRs walked by head branch at dispatch time; lane in-flight #12220-chain and #11996 are disjoint surfaces). No sibling card's pins assert this script's behaviour.
Generated by Claude Code
Dev claim (mode:subagent — identity is the branch, not the assignee field)
Branch:
claude/issue-12518-verify-lock-redirection
Worktree:objectstack-issue-12518(base0043c92)
File surface:scripts/pm/os-verify-lock.shonly (scanner + its ownst_caseself-test suite). Stop on breach.Plan: reproduce the two card commands as a before reading, enumerate the redirection forms the card does not prescribe (
&>,&>>,N>&M,>&-,>&2) against real bash semantics before choosing the implementation, then pin the regression in the existingst_casesuite (true > /dev/null 2>&1 && true⇒command-exit;sleep 1 &⇒batch-last-exit). Existing;/|/||/ true-background verdicts are not weakened.
Generated by Claude Code
os-dev-report
{ "issue": 12518, "status": "done", "branch": "claude/issue-12518-verify-lock-redirection", "pr": "https://github.com/objectstack-ai/objectstack/pull/12635", "premise_still_valid": true, "summary": "exit_certifiable()'s scanner now carries one character of look-behind (the redirection operator it just passed), so a redirection's ampersand is consumed instead of read as a background operator. The card's exact repro flips from batch-last-exit to command-exit and the warning block disappears entirely. Scope inside the declared file grew by two rows of the SAME defect class, both named in the PR body with evidence: the `<&` input-duplication forms (the card names only the right-angle side) and `>|` noclobber (misread as a PIPELINE, not as backgrounding). One form is deliberately left UNCERTIFIED: the append-both redirection is bash 4.0, and on this file's declared bash 3.2 floor it really does background, so certifying it would be an under-label -- the one direction this scanner's doctrine forbids. check:bash32-floor caught that on the first draft and was right. Existing `;` / `|` / `||` / true-background verdicts are untouched, pinned from both directions.", "tests": "All at final commit f55ee39d1 (single-file diff, 184 insertions / 3 deletions). (1) `bash scripts/pm/os-verify-lock.sh --self-test` -> exit 0, its own line: '✓ os-verify-lock self-test: all cases pass.' -- 151 pass marks, 0 fail marks, including 17 new (g3) cases. (2) Card repro, verbatim both commands: BEFORE (0043c922) 'VERDICT batch-last-exit 0 · ⚠ NOT A VERDICT ON THE WHOLE COMMAND'; AFTER (f55ee39d) 'VERDICT command-exit 0 · held the lock 0s' with no warning block. (3) Full 21-row verdict matrix driven through the real script on an isolated lock file, before and after -- 10 certifiable forms all flip to command-exit, 11 genuinely-uncertifiable forms all unchanged. (4) ABLATION from the committed state: no build or dist is involved (the shell script is executed directly, so there is no artifact between source and behaviour and nothing to rebuild -- the reading is taken from the edited file itself). Mutation applied to the three repaired guards, proven ON DISK by anchored text counts rather than an edit tool's exit code: injected marker occurrences 3 (expect 3), removed guard occurrences 0 (expect 0), on-disk hash ca9509b2 vs HEAD blob 23ca638a. Self-test then exit 1 with exactly the 10 certified cases red and the 7 negative-direction cases plus every pre-existing case still green. Restore leg proven by bytes: on-disk hash back to 23ca638a == HEAD blob, `git diff HEAD` empty, 0 residual markers; trap-protected with absolute paths. An EARLIER ablation attempt refused to run and declared its reading void ('MUTATION DID NOT APPLY') because the anchors had moved with the implementation -- recorded rather than silently re-run. (5) Derived gate family, all green, quoted at their own verdict lines -- see gates below.", "gates": { "derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (change set read from git, not hand-listed; --repo assertion held against this checkout's origin)", "run_at_commit": "f55ee39d1", "run_under_verify_lock": "yes -- VERDICT command-exit 0 · held the lock 29s · waited 0s", "result": "10/10 PASS, 0 failing", "families": [ "check:nul-bytes -- OK (scanned 6986 text file(s); no raw ASCII control bytes)", "check:agent-test-spelling -- PASS", "check:bash32-floor -- 20 tracked shell file(s) name no bash 4+ construct; 15 constructs checked, floor bash 3.2 (RED on first draft, see deviations)", "check:cli-command-ids -- 285 command-id literal(s) across 103 file(s) resolve", "check:cross-package-test-inputs -- 20 package(s) read outside themselves, all declared", "check:entry-guard -- 170 scripts/ file(s), every entry guard via invoked-as.mjs", "check:parse-guard -- PASS", "check:pnpm-filter-targets -- 140/177 --filter occurrence(s) across 30 file(s) resolve", "scripts/check-ci-filter-parity.mjs -- all 109 declared cross-package glob(s) covered", "scripts/check-cross-package-test-inputs.mjs -- PASS" ], "tool_script_own_suite": "the edited file IS a tool script, so its own suite was run, not just the derived family: `--self-test`, 151 cases green. No *.test.ts anywhere names os-verify-lock.sh (checked by git grep across tests, scripts and .github).", "ci_status_at_report_time": "in_progress -- reported at draft-PR time per the standing contract; CI convergence is the PM's to read" }, "line_budget": "not applicable -- no skills/** path in the diff. Actual delta for the record: scripts/pm/os-verify-lock.sh +184 / -3, one file, whole-repo diff surface unchanged otherwise.", "files_changed": ["scripts/pm/os-verify-lock.sh"], "deviations": [ "CHANNEL SWITCH (declared): a repo-scoped REST read returned HTTP 403 in this container ('GitHub access is not enabled for this session'), the measured container-dev-seat shape, so dedup for the out-of-scope finding used ONE targeted MCP search_issues instead of REST list + local grep. Control satisfied in the same call: the search returned #12518 itself, a known hit, so the empty result for the pipe-both gap is a real read and not a dead channel.", "NARROWING (declared): the --self-test suite was NOT wrapped in os-verify-lock.sh. It cannot be: its own cases invoke the script against the real shared lock, so an outer holder would block every inner call against the 540s budget -- a deadlock, not a slow run. Everything else heavy (the gate family) DID run under the lock.", "NARROWING (declared, and measured rather than asserted): repo-wide `pnpm lint` was not run. Proven not to exclude anything -- (i) population read from eslint's own config resolution, which reports the changed file as 'File ignored because no matching configuration was supplied'; (ii) `--format json` counts 1 file examined, 0 errors, 1 warning and that warning IS the not-in-population notice; (iii) invariance: every `files:` pattern in the flat config names TS/JS extensions only, and the diff adds no JS/TS and no eslint config, so no untouched file's verdict can move.", "SCOPE, inside the declared file surface and named in the PR body: two rows beyond the card's literal ampersand framing were repaired as the same defect class (a redirection operator's second character read as a control operator, same scan loop, same one-character state) -- the `<&` input-duplication forms and `>|` noclobber. The declared FILE surface did not change, so no claim amendment was owed; the widened reading is evidenced in the PR body with bash's own parse output.", "DELIBERATE NON-FIX: the append-both redirection stays uncertified (bash 4.0 vs the file's declared 3.2 floor, where it genuinely backgrounds). Two draft pins were REMOVED for this -- an append-both case and a pipe-both case -- because writing either token in this repo's shell is itself a check:bash32-floor violation, and that gate fires on quoted occurrences too. Recorded in a full-line comment above exit_certifiable, where the token is exempt. It carries its own actionable note naming the portable spelling, which this change certifies." ], "mcp_calls": "12 -- 2 issue reads, 2 issue comments (claim + this report), 1 search_issues (dedup), 1 issue_write create (#12634), 1 create_pull_request, 1 pull_request_read (body read-back), 2 search_pull_requests (label read + read-back), 1 issue_write update (labels), 1 failed issue_read get_labels (GraphQL Issue type does not resolve a PR number)", "labels": "skip-changeset applied and READ BACK; union write preserved size/m (additive REST refused with 403, so the measured MCP read-union-write-readback fallback was used). No changeset written: scripts/pm/** is internal PM-loop tooling and publishes nothing.", "open_questions": [], "out_of_scope_findings": [ "filed as #12634: check-bash32-floor's construct table carries the append-both entry but has no pipe-both entry, though both operators arrived in bash 4.0 -- so one is caught and the other is not. Found because the gate caught one of them in this very diff and let the other through; on bash 3.2 the uncaught one is a syntax error, arguably the louder failure. Filed unassigned, no label, for PM triage." ] }
Generated by Claude Code
ACCEPT (reviewer of record: skills seat, session
session_01MnijPVVDakqK2J335JoJtq) — PR #12635. Verified against GitHub and the tree, not the report's narrative: ① PR shape — draft, basemain,Fixes #12518first line, file surface exactlyscripts/pm/os-verify-lock.sh(matches the claim),skip-changesetcorrect for unpublished tooling; ② this seat's own spot-check on the PR head: the card's repro printsVERDICT command-exit 0with the warning block gone, and a genuine background (sleep 0.1 & wait) still printsbatch-last-exitwith the backgrounding banner — both directions independently reproduced; ③ diff read: the look-behind is scanner state (cleared on quoted/escaped paths — theecho \>&true/ quoted->traps are pinned red), the append-both refusal on the bash-3.2 floor is the doctrinally correct direction (refuse-to-certify, with an actionable remedy the old banner lacked), and both pin directions exist with an on-disk-verified ablation. Deviations reviewed and accepted: in-file scope widening to the same defect class (evidenced from bash's own parser), the two declared narrowings, and the unwritable-pin record for the 4.0 tokens. Out-of-scope finding #12634 stays unlabelled for central triage (its gate's subject is shell code quality — devx territory, not this lane's to grade).Landing path: pure code, no governed path ⇒ seat-reviewed, lands via the queue. CI is converging (no red; five jobs in flight at review time) — ready + enqueue happens when every check on the head is green, not before.
Generated by Claude Code
scripts/pm/os-verify-lock.sh's certifiability scanner treats the&in a2>&1redirection as a background operator, so a correctly&&-joined command is downgraded fromVERDICT command-exittoVERDICT batch-last-exitwith the "⚠ THIS COMMAND'S EXIT CODE CANNOT CERTIFY IT" banner.Found incidentally while running the derived gate union for #11969 (ADR conversion); unrelated to that card's surface, filed rather than fixed.
Measured, isolated to one variable
Same joiner, same trivial parts, run in this container against the real script:
The only difference between the two is the
2>&1. Nothing is backgrounded in either.Mechanism
The scanner's
'&'case reads exactly one character ahead:In
2>&1the&is preceded by>and followed by1, sonxis1, the&&branch is not taken, and it falls through to the backgrounding flag. There is no case for the>&/>>&/N>&Mredirection forms. The look-ahead is the wrong direction for this token: what disambiguates it is the character before the&, not the one after.Why it is worth fixing rather than tolerating
&&-joined. Following the advice cannot clear the warning, which is the shape that teaches agents to ignore the banner generally, including on the real;/|/&cases where it is correct.tail's exit code.cmd > out.log 2>&1 && nextis that recommended form, and it is exactly the form that trips this.batch-last-exitis documented as the honest label for an uncertifiable command; applying it to a certifiable one inverts the signal the label exists to carry.Same family as a closed precedent
#10570 —
guard-main-checkout-bash.shread a>inside a shell comment as a redirect and false-blocked the command. Same root shape: a single-character shell-token scan that does not model the surrounding context. Different script, so not a duplicate, but the fix and the self-test shape should probably rhyme.Suggested shape (not prescribing)
Consume the redirection forms before the background check — on seeing
&, look back for an unescaped>(optionally preceded by a digit or>), and treat that as a redirect rather than a joiner.scripts/pm/os-verify-lock.shalready carries anst_caseself-test suite covering the;,|,||and&verdicts (around theVERDICT batch-last-exitcases), so a regression pin belongs there: assertcommand-exitfortrue > /dev/null 2>&1 && trueand that a genuinesleep 1 &still earnsbatch-last-exit.Filed unassigned for triage; no fix attempted here.
Generated by Claude Code
Generated by Claude Code