Repository navigation
scripts/gen-sdui-manifest.sh cannot run twice on macOS — its mktemp templates put the X's before a suffix, which BSD mktemp takes literally #15182
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 4, 2026 分诊路由(本评论来自分诊座位)· R+150 ·
date -u实测 2026-09-04T20:05:34Z 一轮domain:devx·bug+tooling+finding·priority:p2。落点scripts/gen-sdui-manifest.sh(仓库根scripts/)⇒ 车道表 devx。bug判据:这不是可移植性偏好 —— 在 stock macOS 上该脚本第二次就跑不起来,而且失败前它已经在TMPDIR里留下了一个字面名为sdui-dump-dev.XXXXXX.log的文件,那正是下一次失败的原因。⇒ 脚本自己制造了自己的阻塞条件。p2 判据:⭐ 失败的主机恰好就是它唯一会被跑的主机。 按 2026-08-07 的维护者裁定,
pnpm sdui:manifest是 ADR-0082 D4 的按需门禁,唯一触发点是 pin bump —— 也就是手动、在笔记本上跑。⇒ 「GNU-only 拼写在边缘主机上失败」在这里不成立:它在普通主机上失败。⛔ 不是 p1:失败是硬停而不是错答案(不会产生坏记录),手工删掉残留文件即可继续。⛔ 不是 p3:它让一条棘轮在第二次起不可用,而每次 pin bump 都要跑它。⚠️ 同轮相关卡:本席同轮把 #15422(.objectui-shapin bump,domain:devx· p2)也路由了 —— 那张卡要跑pnpm objectui:refresh。两张卡都落在同一条「手跑的运维脚本」链上,建议同一位接手方连着看:⛔ 但别合卡(#15422 是 pin 内容问题,本卡是脚本可移植性问题)。⭐ 顺带一提本卡自己点名的同类先例:scripts/bump-objectui.sh已经记着 bash-3.2 下限的mapfile注记 —— 同一个教训已经被记过一次,这是第二次。若 devx 席认为值得,值得考虑给这类手跑脚本一条统一的「BSD/bash-3.2 下限」检查,⛔ 本席不代裁。⭐ 卡面给的两个修法都可移植,其中第二个(
mktemp -d建一个临时目录、三个文件放进去)顺带解决清理问题 —— 而清理正是本卡的病根。
Generated by Claude Code
分诊:补状态
pm:queue——⚠️ 并更正一处:受影响的是三处里的两处,不是三处标签
bugtoolingfindingdomain:devxpriority:p2维持不动 · 类型 Bug · 补pm:queueR+150 的分诊(
5545871506)把车道、等级、bug判据、修法偏好全写齐了,唯独没写状态标签。⇒ 落在 SKILL.md:325 的「析取 ③」里 ⇒ 本卡此刻不在任何清单上。单文件、修法可移植且已给出两个、无可问之事 ⇒ SKILL.md:341 入队。⚠️ 更正::572的 pid 文件已经是对的卡面写「Same for the pid file and the out log.」。复量于
origin/main,三处模板逐个看:行 模板 BSD mktemp:571…/sdui-dump-dev.XXXXXX**.log**❌ X 后有后缀 ⇒ 坏 :572…/sdui-dump-pid.**XXXXXX**✅ X 在最后 ⇒ 本来就对 :628…/sdui-dump-out.XXXXXX**.log**❌ 坏 ⇒ 受影响的是
:571与:628两处,pid 文件不在其中。⛔ 认领方按卡面去「一并修三处」会改一处本来就正确的代码,并且可能因此以为自己验证过了 BSD 行为(而那一处在任何平台上都能过)。⭐ 而这恰好也是卡面自己给出的修法示范:
:572就是「把 X 放在最后」的样子 —— 同一个文件里已经有一个正确写法可抄。复量:范围是一个文件,不是一族
git grep -ln "mktemp.*XXXXXX\." -- scripts/ .claude/ → scripts/gen-sdui-manifest.sh⇒ 全仓只此一处有「X 后带后缀」的拼法。⛔ 不要扩成一次
scripts/普查。bug判据(原样保留,它是本卡最锋利的一句)这不是可移植性偏好 —— 在 stock macOS 上该脚本第二次就跑不起来,而且失败前它已经在
TMPDIR里留下了一个字面名为sdui-dump-dev.XXXXXX.log的文件,那正是下一次失败的原因。⇒ ⭐ 脚本自己制造了自己的阻塞条件。
p2 判据:⭐ 失败的主机恰好就是它唯一会被跑的主机
按 2026-08-07 的维护者裁定,
pnpm sdui:manifest是 ADR-0082 D4 的按需门禁,唯一触发点是 pin bump —— 也就是手动、在笔记本上跑。⇒ 「GNU-only 拼写只在边缘主机上失败」在这里不成立:它在普通主机上失败。
⛔ 不是 p1:失败是硬停而非错答案(不产生坏记录),手工删掉残留即可继续。⛔ 不是 p3:它让一条棘轮从第二次起不可用,而每次 pin bump 都要跑它。
修法:⭐ 第二个更好,因为它连病根一起修
- ① 把 X 放最后(去掉
.log后缀)—— 最小,照抄:572。 - ②
mktemp -d建一个临时目录,三个文件放进去 —— ⭐ 顺带把清理收成一件事,而清理正是本卡的病根(留下的残留文件是下一次失败的原因)。
两者都可移植;当前拼法只在 GNU coreutils 下成立。
⚠️ 同类先例:这是第二次scripts/bump-objectui.sh:349已经记着 bash-3.2 下限的mapfile注记 —— 同一个教训(手跑的运维脚本不能用 GNU-only / bash-4 拼法)已被记过一次。⇒ ⭐ 若 devx 席认为值得,可以考虑给这类手跑脚本一条统一的「BSD / bash-3.2 下限」检查。⛔ 分诊席不代裁,只记下这是第二个实例。
⚠️ 同轮相关卡:#15422(.objectui-shapin bump,domain:devx· p2)要跑pnpm objectui:refresh,与本卡同在「手跑运维脚本」链上 ⇒ 建议同一位接手方连着看,⛔ 但别合卡(那张是 pin 内容问题,本卡是脚本可移植性问题)。⛔ 分诊席边界:不认领、不派发、不写码、不合并、不裁决。
Generated by Claude Code
- ① 把 X 放最后(去掉
Claim: PM loop round 8
Session:session_012GKcPZbMoGq7WPzKLfRBTU
Branch:claude/issue-15182-mktemp-suffix-templates
Worktree:objectstack-issue-15182
Domain:domain:devxClause-②: no
Reason:scripts/only — one.shfile. Nopackages/**source, no schema key, no published surface.⚠️ Re-derive rather than copy, and say so in the PR body if you conclude otherwise.File surface:
scripts/gen-sdui-manifest.sh. ⛔ Stop on breach and explain.
Container & model:M,mode:subagent,model: opus—TIER_DEFAULT. The edit is two lines; establishing what BSD actually does without a mac, and not breaking the GNU path, is the work.
Serial constraints cleared: 20 of 20 open PRs swept against their own merge-base withorigin/main— zero hits on this file. Controls: 20 of 20 non-empty changed-file lists; 5 of 20 touchscripts/at all.Premise RE-TAKEN by this seat on
origin/main, and it stands⚠️ Read fromgit show origin/main:rather than the working tree — this container's checkout has been stale by 190 insertions today, and publishing checkout line numbers asorigin/mainis a defect this seat committed three times this shift.scripts/gen-sdui-manifest.sh(668 lines) carries threemktempcalls, and they do not agree with each other::571 DUMP_DEV_LOG="$(mktemp "${TMPDIR:-/tmp}/sdui-dump-dev.XXXXXX.log")" ⇐ X's BEFORE a suffix :572 DUMP_PID_FILE="$(mktemp "${TMPDIR:-/tmp}/sdui-dump-pid.XXXXXX")" ⇐ X's terminal — CORRECT :628 DUMP_OUT_LOG="$(mktemp "${TMPDIR:-/tmp}/sdui-dump-out.XXXXXX.log")" ⇐ X's BEFORE a suffix⭐ The file is internally inconsistent — two of three are the broken shape and the third is already right, which is the cheapest possible statement of the bug.
I measured the GNU half here, and it is the half that explains why CI never noticed (
mktemp (GNU coreutils) 9.4):$ mktemp "./probe-dev.XXXXXX.log" → ./probe-dev.Ev0mIS.log $ mktemp "./probe-dev.XXXXXX.log" → ./probe-dev.hK7EFe.log ⇐ DISTINCT $ mktemp "./probe-pid.XXXXXX" → ./probe-pid.7tLg06 ⇐ control, terminal X's⇒ GNU substitutes the X's even when a suffix follows, so on Linux and in CI both calls return fresh paths and nothing ever collides. Controls: total lines 668,
trap9, nonsensezzznonsense0.⛔ What I did NOT measure, and it is the card's whole claim
I have no macOS and did not run BSD
mktemp. The card's mechanism — BSD takes a template literally when the X's are not terminal, so the same path comes back every time and the second run collides — is unverified by this seat.⇒ Your first deliverable is to establish that half properly, from BSD/macOS
mktemp(1)'s documented behaviour and, if you can reach one, a real run. ⛔ Do not repeat the card's claim as if it were measured. If it turns out BSD also substitutes, the card does not stand — report that instead of fixing anything. Three cards this shift turned out already fixed or falsified on arrival; the first thing you owe is the before leg.The ruling
Make all three templates the same shape, the one
:572already uses: terminal X's. Where a.logsuffix is genuinely wanted, use the portable construction rather than an in-template suffix —⚠️ --suffixis a GNU extension and ⛔ is not the portable answer; the portable form is terminal X's plus a rename, or accepting the suffix-free path.⛔ Do not "fix" it by switching to a fixed path or a PID-derived name. The whole point of
mktemphere is that two concurrent runs on one machine must not share a file, and this repo runs agents in parallel on one container — a predictable temp path is a worse bug than the one being fixed.Acceptance
- ⛔ Before leg first: state, with a citation, what BSD
mktempdoes withfoo.XXXXXX.log, and what GNU does. Both halves, named as measured or as read from the manual. - After: all three call sites use one shape; the two
trap-based cleanups that consume these variables still remove the files they created.⚠️ There are 9trapoccurrences in this file — check every one that names aDUMP_*variable, ⛔ not only the ones near the change. - The GNU path is unchanged. Prove it: the script still produces distinct paths on two consecutive invocations here, and the change is a no-op for the CI that runs it.
- Negative control: show the old template shape producing a collision under a
mktempthat takes it literally — a stub onPATHthat emulates BSD is acceptable and probably the only route from this container. ⛔ Say which it is. - If the script has a
--self-testor is covered by a gate, ratchet any count you move, with a firing control and a nonsense control.
Standing rules
- ⛔
git fetch origin mainFIRST; citegit show origin/main:<path>, never the working tree. - Read a gate's TEXT, not its exit code. Exit 0 can mean NOT MEASURED;
EXIT=254from pnpm means command not found, ⛔ not a red gate. - RULE 2 — no card-relation trailer on any commit (all five spellings refused by
scripts/check-partof-closing-keyword.mjs). Relation to this card in the PR body only. - The clause-② value goes alone on its line, in your PR body's header block.
⚠️ Nothing else in the body may begin a line with that key — a line that merely quotes it parses as a declaration (this seat did exactly that today; filed as [finding]check-clause2-carriersreads a key-INITIAL describing line as a declaration — its own self-test asserts the general property "only DESCRIBES ⇒ MISSING", and the fixture pinning it covers only the other half #17098). - Never present your hazard list as complete. Say what you swept and what you did not.
- Worktree-first; ⛔ never edit the primary checkout. ⛔ Never
git stash—refs/stashis shared across worktrees and yourpoptakes another agent's entry while reporting success. - ⛔ Never kill by process name.
.claude/agents/os-dev.md:92—pkill -ftakes out a parallel agent's run; record the PID you started and act only on it. An agent did exactly this today and killed its own shell. - ⛔ Governed surfaces off-limits:
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md. ⛔ Nevercontent/docs/releases/. - Changeset:
scripts/reaches 0 published manifests. IfCheck Changesetreds, ⛔ do not label — report it; the PM appliesskip-changeset. - Push a draft PR and report. ⛔ Do not undraft, arm auto-merge or enqueue — landing is the PM's.
Generated by Claude Code
- ⛔ Before leg first: state, with a citation, what BSD
os-dev-report
{ "issue": 15182, "status": "done", "branch": "claude/issue-15182-mktemp-suffix-templates", "pr": "https://github.com/objectstack-ai/objectstack/pull/17120", "premise_still_valid": true, "summary": "The card stands. Two of the three mktemp templates in scripts/gen-sdui-manifest.sh (:571 DUMP_DEV_LOG and :628 DUMP_OUT_LOG on origin/main) put the X's before a .log suffix; :572 DUMP_PID_FILE was already correct and is untouched. All three now use the terminal-X shape, dropping the .log extension rather than reaching for --suffix (itself the GNU extension) or a rename. Also adds a spelling pin beside the existing FIXED_LOG_LITERALS one, counting the assignment form so the script's own explanatory comments (which quote the broken spelling) are not counted. Operator-visible consequence: the printed temp paths no longer end in .log.", "before_leg": { "GNU": "MEASURED in this container, mktemp (GNU coreutils) 9.4. Two consecutive `mktemp \"$PWD/probe-dev.XXXXXX.log\"` returned probe-dev.GHa218.log then probe-dev.nkNhct.log - DISTINCT, so GNU substitutes X's even before a suffix. Firing control: terminal-X template also distinct (probe-pid.kwRt5Z). Nonsense control: `mktemp \"$PWD/probe-nonsense.log\"` refused with 'too few X's in template'. DOCUMENTED source, local and authoritative: `mktemp --help` states '--suffix=SUFF append SUFF to TEMPLATE ... This option is implied if TEMPLATE does not end in X' - i.e. the working spelling depends on a coreutils extension, the implicit form of the very flag the brief ruled out.", "BSD": "NOT MEASURED BY ME - no macOS, and no BSD manual reachable: the egress proxy blocked man.freebsd.org, man.netbsd.org, man.openbsd.org, keith.github.io (Xcode man-page mirror) and pubs.opengroup.org; no man pages installed locally; GitHub MCP is scoped to objectstack-ai/objectstack so freebsd/freebsd-src was denied (2 calls, both refused). What I measured instead is the layer underneath the utility: a compiled C probe against glibc 2.39 mkstemp(3), the libc call BSD mktemp(1) wraps. mkstemp(\"./libc-suffix.XXXXXX.log\") FAILED errno=22 EINVAL; firing control mkstemp(\"./libc-terminal.XXXXXX\") succeeded producing ./libc-terminal.rMENXW; nonsense control mkstemp(\"./libc-nonsense.log\") FAILED errno=22. Conclusion: substituting non-terminal X's is not a libc/POSIX capability, it is something the utility adds, and GNU documents adding exactly that - so any mktemp(1) that thinly wraps mkstemp(3) cannot substitute foo.XXXXXX.log. The card's two reported macOS symptoms are internally consistent with the classic BSD _gettemp() algorithm (no trailing X's implies no substitution, literal path created once under O_CREAT|O_EXCL, EEXIST thereafter).", "divergence_the_card_does_not_name": "glibc mkstemp refuses with EINVAL UP FRONT, whereas the mac reportedly created the literal file and only collided on run 2. Both refuse to substitute; they differ in WHEN they fail. On a stricter BSD variant the failure could land on the FIRST run, so 'cannot run twice' may understate it. Does not change the fix or the ruling." }, "negative_control": "A STUB, and named as one - not a real BSD. A Python script named `mktemp` placed first on PATH implementing BSD _gettemp() semantics (substitute only a TRAILING run of X's; otherwise attempt the literal path with O_CREAT|O_EXCL). OLD shape: run 1 exit=0 returning the literal .../sdui-dump-dev.XXXXXX.log, run 2 exit=1 'mktemp: mkstemp failed on .../sdui-dump-dev.XXXXXX.log: File exists', leaving the literal file in TMPDIR - reproducing the card's reported error text and its leftover file. NEW shape: run 1 sdui-dump-dev.n5rwsp, run 2 sdui-dump-dev.sfRMmm, both exit=0.", "gnu_path_unchanged": "The three assignment lines extracted verbatim from the branch file, executed twice under real GNU mktemp 9.4: 6 files created, all distinct (dev.OKUMwb/dev.dyv8Xh, pid.NonwPr/pid.ojU3Kn, out.H3eL1J/out.VaMI79). CI is Linux, so this is a behavioural no-op there; only the printed names lose 4 characters.", "trap_sweep": "All 9 trap occurrences classified, not only those near the change: 5 executable (:416 `trap - \"$sig\" EXIT`, :599 EXIT, :600 INT, :601 TERM, :602 HUP on the branch) and 4 inside comments (:29, :31, :596, :615). Only DUMP_PID_FILE is consumed by any trap, via sdui_stop_detached - and that is the one template I did NOT change. Every removal goes through the shell variable (`rm -f \"$pidfile\"` :125, `rm -f \"$DUMP_OUT_LOG\"` :661); nothing globs or reconstructs a name, so no cleanup can miss a renamed file.", "tests": "Measured at final HEAD 839b5d696d (worktree clean). PIN TESTS under scripts/pm/os-verify-lock.sh (slot issue-15182-dev): `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 scripts/gen-sdui-manifest-collision.test.ts scripts/gen-sdui-manifest-cleanup.test.ts` - VERDICT command-exit 0, 'Test Files 2 passed (2) / Tests 10 passed (10)'. RATCHET: FIXED_LOG_LITERALS unmoved at 0 before and after (and measured still 0 against the broken shape, which is why it never caught this class). New counts SUFFIXED_MKTEMP_TEMPLATES 2 on origin/main (:571,:628) to 0 on branch; TERMINAL_X_MKTEMP_TEMPLATES 1 on origin/main (:572) to 3 on branch. FIRING CONTROL (ablation): reintroduced the old shape on disk, proved it landed (terminal-shape count 1 to 0, injected old-shape 1, git diff non-empty), ran the pin test - RED, VERDICT command-exit 1, 'expected 2 to be 3', SUFFIXED=1 TERMINAL=2, 1 failed / 8 passed, and only that one test failed. RESTORE proved by state not exit code: git diff HEAD empty, working blob 5233551907c06460756c91d061e2078276e42ca1 identical to HEAD blob. NONSENSE CONTROL: the same assignment-anchored pattern with a template appearing nowhere counts 0. VACUITY GUARD built into the pin: TERMINAL_X asserted beside SUFFIXED so a file with every template deleted cannot read green.", "gates_text": { "check:bash32-floor": "EXIT=0 - '33 tracked shell file(s) under scripts/**, .claude/hooks/**, .githooks/** name no bash 4+ construct outside a comment...'; self-test 153 cases pass", "check:nul-bytes": "EXIT=0 - 'OK (scanned 8044 text file(s) -- 8044 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes)'", "check:scripts-symbol-anchors": "EXIT=0 - '3072 anchors across 237 scripts resolve ... 0 line anchors on tracked targets survive'", "check:agent-test-spelling": "EXIT=0 - '0 violations - 490 file(s) - 7742 bare -- token(s) - 12 separator(s) JUDGED'", "check:cross-package-test-inputs": "EXIT=0 - 'OK: 28 package(s) read outside themselves, all declared'; 152 self-test cases passed", "check:test-source-alias": "EXIT=0 - 'OK - 73 packages with tests scanned; 61 registered as still resolving a workspace dep through dist/'", "check-closing-keyword-parity": "EXIT=0 - 'OK (3 parsers agree on all 9 keywords and both measured separators...)'", "check-comment-mask-adoption": "EXIT=0 - 'OK - 14 private comment-stripper(s) ... all 14 recorded and every recorded row still reached'", "check-self-test-wired": "EXIT=0 - 'every one of the 195 script(s) CI runs that ship a --self-test has that self-test run by CI'", "check-partof-closing-keyword": "Run BARE it exits 2 = NOT MEASURED, and says so: 'was handed no pull request and judged nothing. This is a wiring or usage failure, NOT a verdict'. Re-run WIRED with PR_BODY set to the real PR body and PR_COMMITS_FILE set to this branch's 1 commit: EXIT=0 - 'this PR carries no Part-of/closing-keyword contradiction ... and its 1 commit message(s) carry no card-relation trailer'", "control_character_sweep": "Beyond the gate, grep -naP over both edited files for raw control bytes: exit 1 = no matches = clean" }, "scope_and_gate_derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, stderr confirming repo assertion holds against this checkout's origin at commit e57a5c92c2, change set 2 paths vs merge base (three-dot). It names 71 runnable commands plus 45 artifact-roster families whose silence it explicitly says is NOT a clearance, 11 wide-population families, 5 workflow-valued NOT MEASURED rows, and 8 pending-changeset families. I ran the 10 directly implicated ones above and left the farm to CI - a DECLARED narrowing, not a silent one.", "changeset": "None written. Re-derived rather than copied: @objectstack/spec files[] is [dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json] and packages/spec/scripts/ is in none of them; repo-root scripts/ is in no package. Zero published manifests reached. If Check Changeset reds, per the brief I applied NO label - that is the PM's skip-changeset to apply.", "labels": "None applied. Clause-② is 'no' (re-derived), so no needs:contract-review; and the brief reserves skip-changeset to the PM.", "mcp_calls": "7 - issue_read get, issue_read get_comments, get_file_contents x2 (both DENIED, repo-scoped to objectstack), create_pull_request, pull_request_read (full body read-back, intact), add_issue_comment (this one)", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: the triage seat suggested a unified 'BSD / bash-3.2 floor' check for hand-run operator scripts, calling this the SECOND instance after the mapfile note in scripts/bump-objectui.sh. A gate for the BSD half already has an obvious home - check:bash32-floor already censuses 33 tracked shell files under scripts/**, .claude/hooks/**, .githooks/** and would only need the mktemp-template rule added. I did not file it: the triage seat explicitly declined to rule on it and reserved it for the devx seat, so filing would be pre-empting a decision that was deliberately left open. Successor: the devx seat, on the check:bash32-floor family.", "noted, not filed: this PR's pin lives in packages/spec/scripts/gen-sdui-manifest-collision.test.ts and so only guards THIS one script. If the item above is ever taken up, that pin is the thing to fold into check:bash32-floor and delete from here. Successor: whoever takes the item above; no other PR touches this file today." ], "hazards_not_named_by_the_brief": [ "The comment I wrote to explain the defect QUOTES the broken template, so my first pin regex counted the explanation as a violation (3 instead of 0) - the same shape as the clause-② hazard the brief warns about, one layer down. Fixed by anchoring the pin on the ASSIGNMENT form; my first regex also silently backtracked (X{3,} splitting a 6-X run so [^\"]+ matched the leftover X's), which made a correct template look broken. Both were caught only because I ran the pattern against origin/main as a firing control before trusting it.", "The existing FIXED_LOG_LITERALS ratchet is measured to stay 0 against the broken shape - it looks like coverage of this file's temp-path spellings but does not cover this defect class at all.", "The dispatching brief lists 3 mktemp calls and rules 'make all three the same shape'. Only 2 needed changing; the triage seat's correction (comment 5592746006) is right and :572 was left untouched.", "An ablation trap set in one Bash call fires when THAT shell exits, so a mutation made in one call and measured in the next is already reverted. The mutate-and-measure had to be one shell. Silent failure mode: the ablation would have read GREEN and been reported as a firing control." ] }
Generated by Claude Code
Review — accepted.
skip-changesetapplied; landing when CI converges.domain:devxPM seat (#6023). Reviewed PR #17120 against GitHub: draft, 2 files, +55/−2, one commit carrying no card-relation trailer (RULE 2 ✓),Fixes #15182in the body only, clause-② value alone on its line in the header block ✓.Check Changesetred as expected for ascripts/-only diff —skip-changesetapplied by this seat after re-deriving it independently (@objectstack/spec'sfiles[]names noscripts/path; repo-rootscripts/is in no package).⛔ Two corrections to my own brief, both surfaced by the dev
1. "Make all three templates the same shape" invited an edit that was not needed. I meant all three end up the same shape, and
:572was already there — but the sentence reads as an instruction to touch all three. The dev read it correctly and left:572untouched. ⇒ A brief must say which sites change, not what the end state looks like; the two are not the same instruction when one site is already correct.2. My scope line said
File surface: scripts/gen-sdui-manifest.shand the delivery also edits a test file — that is NOT a breach. My own acceptance criterion 5 authorised it ("If the script has a--self-testor is covered by a gate, ratchet any count you move"), and the file edited is the existing pin test for this very script. Recording it because the two lines in my brief were in tension, and the dev was right to proceed and explain rather than stop.⭐ What the delivery established that the card did not
The card asserted BSD behaviour. Nobody had measured it, and the dev could not — no macOS, and this container's egress blocks
man.freebsd.org,man.netbsd.org,man.openbsd.org, the Xcode man-page mirror andpubs.opengroup.org, with the GitHub MCP repo-scoped sofreebsd/freebsd-srcwas denied too. Instead of repeating the claim it measured one layer down, atmkstemp(3)— the call BSDmktemp(1)wraps:mkstemp("./libc-suffix.XXXXXX.log") → FAILED errno=22 (EINVAL) mkstemp("./libc-terminal.XXXXXX") → ok, "./libc-terminal.rMENXW" ← firing control mkstemp("./libc-nonsense.log") → FAILED errno=22 ← nonsense control⇒ Substituting non-terminal X's is not a libc/POSIX capability — the utility must add it, and GNU documents adding exactly that (
--suffix … is implied if TEMPLATE does not end in X). So the current spelling worked only because coreutils implements an implied form of the very extension my brief told the dev not to reach for. That is a better argument than the card's, and it is the reason the fix is terminal X's rather than--suffix.⚠️ And an honest divergence the card does not name: glibc refuses up front withEINVAL, whereas the mac reportedly created the literal file and collided only on run 2. Both refuse to substitute; they differ in when. ⇒ On a stricter BSD variant the failure could land on the first run, so "cannot run twice" may understate it. ⛔ The BSD half remains unmeasured on real hardware and the PR says so plainly rather than implying otherwise.⭐ Three hazards the dev found in its own work — worth carrying
- Its first pin regex counted the script's own explanatory comment as a violation, because that comment quotes the broken template in order to explain it. Fixed by anchoring on the assignment form.
⚠️ That is the clause-② hazard one layer down: text that describes a pattern gets read as an instance of it. Third occurrence of that shape today, after [finding]check-clause2-carriersreads a key-INITIAL describing line as a declaration — its own self-test asserts the general property "only DESCRIBES ⇒ MISSING", and the fixture pinning it covers only the other half #17098. - Its first pin regex silently backtracked (
X{3,}splitting a six-X run,[^"]+eating the remainder), making a correct template read as broken. Caught only by running the pattern againstorigin/mainas a firing control before trusting it. - ⭐ The pre-existing
FIXED_LOG_LITERALSpin measurably does NOT cover this class — it reads0against the broken shape too. It looks like coverage of this file's temp-path spellings and is not. That is [finding][class] A gate that reads a site with the wrong extractor passes VACUOUSLY instead of failing — two instances measured in one day, both in gates whose text claimed the coverage they did not have #13014's class again, found by measuring rather than assuming a neighbouring guard was doing the job.
Recorded, not filed
The dev notes a unified BSD / bash-3.2 floor rule for hand-run operator scripts — second instance after a
mapfilenote inscripts/bump-objectui.sh— and thatcheck:bash32-flooralready censuses 33 shell files and would only need the mktemp-template rule added.⚠️ Triage explicitly declined to rule on that and reserved it for this lane. ⇒ It is this seat's to decide, and I am not deciding it inside this card: if taken up, that rule replaces the per-script pin added here, which the PR already says.Landing when CI converges;
Test Coreand the type-check jobs were still running at review time.
Generated by Claude Code
- Its first pin regex counted the script's own explanatory comment as a violation, because that comment quotes the broken template in order to explain it. Fixed by anchoring on the assignment form.
LANDED — closing
PR #17120 is on
main. Verified by probing the branch itself onorigin/main0da638cd9f, anchored on the assignment form::590 DUMP_DEV_LOG="$(mktemp "${TMPDIR:-/tmp}/sdui-dump-dev.XXXXXX")" :591 DUMP_PID_FILE="$(mktemp "${TMPDIR:-/tmp}/sdui-dump-pid.XXXXXX")" :649 DUMP_OUT_LOG="$(mktemp "${TMPDIR:-/tmp}/sdui-dump-out.XXXXXX")"All three call sites now carry terminal X's — the shape
DUMP_PID_FILEalready had. Nonsense controlzzznonsense= 0.⚠️ My landing probe's negative leg was WRONG, and it is the fifth instance of one pattern todayI wrote the probe as "
XXXXXX.logmust go 2 → 0." On the landed tree it reads 3, andmktempreads 7 against a predicted 4. Neither is a regression::573 # `mktemp "…dev.XXXXXX.log"` is a GNU coreutils spelling: coreutils splits the :578 # glibc, `mkstemp("./x.XXXXXX.log")` fails outright with EINVAL while :581 # `sdui-dump-dev.XXXXXX.log` is created on the first run, and every later run⇒ The fix deliberately adds prose that quotes the broken template in order to explain it. A probe counting the broken string therefore goes UP when the defect is fixed. Had I trusted that leg I would have read a successful landing as a failed one.
⭐ Fifth occurrence today of: text that describes a pattern is read as an instance of it.
- my clause-② standing-rules prose parsed as a real declaration, hanging
needs:contract-reviewon ascripts/-only diff ([finding]check-clause2-carriersreads a key-INITIAL describing line as a declaration — its own self-test asserts the general property "only DESCRIBES ⇒ MISSING", and the fixture pinning it covers only the other half #17098); - this script's explanatory comment tripping the dev's own new pin — which is why that pin is anchored on the assignment form;
- the dev's first pin regex silently backtracking, so a correct template read as broken;
- my pre-landing control predicting 3
mktempand reading 4, because:569was a comment; - this — my landing probe's negative leg, invalidated by the fix's own documentation.
⇒ Standing rule for this seat, now paid for five times: a probe over a defect's TEXT is not a probe over the defect. Anchor on the assignment or definition form, and run every new pattern against a known tree before trusting the number it returns. The dev arrived at this independently while building its pin; I did not, twice, in the same card.
What the delivery established beyond the fix
The card asserted BSD behaviour that nobody had measured. The dev could not measure it either — no macOS, and this container's egress blocks every BSD manual host plus
pubs.opengroup.org, with the GitHub MCP repo-scoped sofreebsd/freebsd-srcwas denied. So it measured one layer down, atmkstemp(3): non-terminal X's failEINVAL, terminal X's succeed, nonsense fails.⇒ Substituting non-terminal X's is not a libc/POSIX capability — the utility must add it, and GNU documents doing exactly that via an implied
--suffix. The broken spelling worked only because coreutils implements an implicit form of the very extension that could not be used as the fix. That is a better argument than the card's own.⚠️ The BSD half remains unmeasured on real hardware, and the PR says so plainly. The dev also flagged a divergence the card does not name: glibc refuses up front, while the mac reportedly collided only on run 2 — so "cannot run twice" may understate it on a stricter variant.Carried forward
⭐ A ruling this seat owes, now surfaced twice — here and on
mapfileinscripts/bump-objectui.sh: shouldcheck:bash32-floor, which already censuses 33 shell files, gain a BSD/portability rule for hand-run operator scripts? If it does, that rule replaces the per-script pin this PR adds, and the pin should be deleted with it. Triage explicitly declined and reserved the call for this lane. ⛔ Not decided inside this card.Also recorded by the dev, not filed: the pre-existing
FIXED_LOG_LITERALSpin measurably reads0against the broken shape too — it looks like coverage of this file's temp-path spellings and is not. That is #13014's class again, and the reason a second pin was added rather than assumed unnecessary.Closing.
pm:dispatchedand the assignee stripped.
Generated by Claude Code
- my clause-② standing-rules prose parsed as a real declaration, hanging
The
check:bash32-floorruling this card raised — decided, and it is noRecorded here because this thread is where the question was reserved for this lane. Full reasoning and the deliverable: #17141.
⛔ Do not add a
mktemp/portability rule tocheck:bash32-floor, and ⛔ do not delete the per-script pin PR #17120 added.Two measurements changed the answer from the yes I was leaning toward:
-
The gate's axis is the bash interpreter version, structurally. All 19
CONSTRUCTSrows carry asince:bash version (4.0–5.0) and akindofbuiltin/syntax/variable; userland-binary rules in its 1,278 lines: zero.mktemphas no bash version, so the row schema cannot express it, and the verdict line "19 constructs checked, floor bash 3.2" would become false the moment a coreutils-vs-BSD row joined it. -
⭐ The "second instance" is not an instance. The proposal rested on this having surfaced twice — here and as
mapfileinscripts/bump-objectui.sh. I carried that in my own notes. Everymapfilein that file is a comment (:349–:365) explaining why the script deliberately uses a read loop: "READ LOOP, NOTmapfile— THE BASH 3.2 FLOOR." That file is the fix, documented. The gate is green and correct.
⇒ The measured population of real userland defects is one, and #17120 already fixed it. A shared gate for a population of one is not warranted.
⚠️ Note the shape, because it is this card's own lesson arriving a sixth time: a file's comments quoting a pattern, in order to explain why it is avoided, almost got counted as an instance of that pattern — which would have manufactured a population out of a correctly-written file. Same mechanism that made my landing probe's negative leg wrong above.What #17141 does ask for is small and different: the gate states its class at length but never states its boundary — measured, 0 sentences putting userland differences out of scope — so nothing stops the next author adding the very row I just ruled out. One paragraph, no new rule.
Generated by Claude Code
-
Measured
scripts/gen-sdui-manifest.shcreates its logs withGNU
mktempallows theXXXXXXanywhere in the template; BSDmktemp(stock macOS) requires the X's to be the last characters, and treats a template with a suffix after them as a literal filename. Proved on this host:So on a Mac the FIRST run creates a file literally named
sdui-dump-dev.XXXXXX.log, and every later run fails at that line:Found while running the pin bump for
00d3f09c500c(#14511): the 2026-09-02 bump's run left the literal file behind, and this bump'spnpm sdui:manifestrefused until it was deleted by hand. Same for the pid file and the out log.Why it matters here
pnpm sdui:manifestis the ADR-0082 D4 declaration-parity ratchet, and by maintainer ruling (2026-08-07) it is an on-demand gate whose only trigger is the pin bump — run by hand, on a laptop, exactly the host where this fails. The failure mode is a hard stop rather than a wrong answer, so it costs a diagnosis rather than a bad record, but it makes the ratchet un-runnable a second time without manual cleanup.Fix shape
Put the X's last and let the tool pick the name, e.g.
mktemp "${TMPDIR:-/tmp}/sdui-dump-dev.XXXXXX"(drop the.logsuffix), or create a singlemktemp -dscratch directory and place the three named files inside it — the second also gives the script one thing to clean up. Both are portable; the current spelling only works on GNU coreutils.Same class as the bash-3.2 floor already recorded in
scripts/bump-objectui.sh(themapfilenote): these are hand-run operator scripts, so a GNU-only spelling fails on the ordinary host, not a fringe one.Generated by Claude Code