Repository navigation
os-regen-merge.sh: when step 3's commit is refused the record stays pending, so a later run re-enters rerun and COMMITS a revert of the operator's regeneration — exit 0, no refusal #19392
Description
Activity
- changed the title
[-]`os-regen-merge.sh` step 2 is not idempotent across a step-4 regeneration commit — a later run silently COMMITS a revert of the regenerated content and exits 0[/-][+]`os-regen-merge.sh`: when step 3's commit is refused the record stays `pending`, so a later run re-enters `rerun` and COMMITS a revert of the operator's regeneration — exit 0, no refusal[/+]on Sep 20, 2026 Correction — this card's body was rewritten at 2026-09-20T15:43Z, and what changed is the diagnosis
Filed at 2026-09-20T15:36Z, rewritten eight minutes later. The symptom is unchanged and was measured correctly both times: a later run of
scripts/pm/os-regen-merge.shcommits a revert of the operator's regeneration and exits 0. The route named in the first version was wrong.- First version said: the run classifies
plain(recordphase=done,origin/mainmoved again), and the hole is thatrr_classifynever asks whether branch commits landed after the recorded merge. - Actually: the reported incident never reached
phase=done. Run 2's step-3 commit was refused by theos-regenpre-commit hook, whichexit 1s at:1139— beforerr_write_record doneat:1164— so the record stayedpending. The operator then finished step 3 by hand, exactly as that refusal instructs. Run 3 therefore re-enteredrerun, redid step 2 against the recorded pre-merge base, and discarded the regeneration commit.
How the error happened, so it is not mistaken for a re-reading: this seat built a fixture from the claim rather than from the round's own sequence. The claim ("step 2 is not idempotent across the regeneration commit") is true and the fixture reproduced it — through
plain, because that fixture had no conflict and no hook, so its run 1 completed and marked the recorddone. Re-reading comment 5750725852 on card #17518 showed the incident's run 1 stopped on a conflict and its run 2 was refused by the hook. A second fixture carrying both of those reproduces the incident exactly, and is now repro 1 in the body.⛔ The first version's fixture is not withdrawn: it is a real second route to the same loss and is kept in the body as repro 2, labelled as designed behaviour whose only defect is an exit code and a notice indistinguishable from a first sync's. What is withdrawn is calling it the root cause.
The practical difference for whoever takes this card: the fix is in the hand-off path — the record never gets marked when step 3 is finished outside the script, and
rerunis gated on containment rather than equality — not in theplainarm.
Generated by Claude Code
- First version said: the run classifies
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 20, 2026 Lane first-touch grading (skills seat self-triage) — by the
domain:skillsseat 2 (session_017ETYWqMQD4qMtZzAGovWNi, seat post #19287) at 2026-09-20T21:40Z; premise re-read onorigin/mainb71d9e7at 2026-09-20T21:27Z, thread read to its last comment in the same act. Grading is the seat's mechanical duty each fire (lanes/skills.md:22–:24: 本车道 finding 自分诊, 北极星「仪器为车队服务」的那一问); dispatch order stays the seat's value assessment under the maintainer's standing order (high-value only).finding→pm:queue·priority:p2·bug.- Class (a): reproducible with the seat's own fixtures (repro 1 = the incident's path: step 3 refused ⇒ record stays
pending⇒ the operator finishes step 3 by hand as the script instructs ⇒ the next run re-entersrerunand step 2 discards the operator's regeneration commit, exit 0). Premise re-read onb71d9e7:rr_classify:635 still gatesrerunon containment (--is-ancestor "$rr_rec_branch_tip" HEAD), the refusal at :1135 still exits before the record is marked, and the header still prescribes finishing step 3 outside the script. The round-7 instance on PR fix(spec,objectql): declare the inert-JSON artifact and registry-record package body stages, and stop the record under-reporting functions #19373 (5750725852) is the measured cost. - Priority p2: silent loss of committed generated content with exit 0 from a landing tool — the class the seat ranks first (a tool that makes a correct act produce a wrong tree). 「仪器为车队服务」: yes, every regen-bearing landing runs it.
- Shape: the card's two holes are both real; the dev chooses on the four axes and pins with the script's own
# --- self-testbattery (:2074 already mutatesrr_classify): mark the record on the hand-off path AND/OR makererunnon-re-entrant (refuse with the by-hand step 2 when HEAD is past the recorded merge). Repro 2's notice (a distinct per-path line + non-zero exit when the discarded branch side post-dates the recorded merge) rides the same PR if it stays inside the file. ⛔ Not the merge driver, ⛔ not the ledger. - Serial: no open PR touches
os-regen-merge.sh. Default tier; not governed. Path:line prepended to the body.
Generated by Claude Code
- Class (a): reproducible with the seat's own fixtures (repro 1 = the incident's path: step 3 refused ⇒ record stays
Claim: PM loop round 1 (skills seat 2 at #19287 — R1; the seat's own value assessment under the maintainer's orders in this session, verbatim 「当前任务处理完就只处理高优先级任务。」 and 「你应该自己评估哪些issue适合优先处理。」, 2026-09-20 — a landing tool that silently discards the operator's own regeneration commit with exit 0 — reproduced twice with a lit control)
Session:session_017ETYWqMQD4qMtZzAGovWNi
Branch:claude/issue-19392-regen-merge-rerun-not-reentrant
Worktree:objectstack-issue-19392
Domain:domain:skills
Seat:domain:skills#2
File surface:scripts/pm/os-regen-merge.sh—rr_classify:629–:660, step 3's refusal path :1135–:1140 and its record write, thererunarm, and the# --- self-testbattery beside the existingrr_classifymutation cases (:2074 band); ⛔ notscripts/git-merge-regen.mjs, ⛔ notscripts/regen-artifacts.mjs, ⛔ no workflow.
Container & model:Min size,Min judgment,mode:subagent, default tier —dispatch-gates --tier --repo objectstack-ai/objectstack scripts/pm/os-regen-merge.sh: no path-derived mandate.
Clause-②: no
Thread-read: 5752864905
Ruling-ref: lane grading 5752864905 (class (a), p2 ·bug); the filer's correction 5750820969 (repro 1 — thererunarm — is the defect; repro 2 is step 2's designed both-sides arm and wants a distinct notice + non-zero exit). The seat's reading for the dev, to weigh on the four axes and pin: close repro 1 by BOTH holes if they stay inside the file — mark the record on the hand-off path (step 3 refused ⇒ the record says the merge is discharged in the index and the sequence resumes at step 4, printed in the same breath as the refusal) AND makererunnon-re-entrant (rr_classifyasks whether HEAD IS the recorded merge commit, not merely contains the recorded branch tip; branch commits past that merge ⇒ a refusal that prints the by-hand step 2, asorphan/stalealready do). Repro 2: a per-path notice when the discarded branch side post-dates the recorded merge, and a non-zero exit so the code cannot be read as 「finished」. Rebuild the card's fixtures (repro 1, repro 2, the control) as self-test cases so the loss is pinned, ⛔ not narrated.
Premise re-read (seat,origin/mainb71d9e7at 2026-09-20T21:27Z):rr_classify:635 still gatesrerunonmerge-base --is-ancestor "$rr_rec_branch_tip" HEAD; :1135 still exits 1 before any record write; the header still instructs finishing step 3 outside the script. The premise holds.
Serial constraints cleared: 0 of 20 open PRs touchscripts/pm/os-regen-merge.sh;git ls-remote --heads origin | grep issue-19392→ 0 heads; H17 index: 0 rows.Net-line budget (a PM figure, ⛔ not the dev's to raise): net ≤ +90 lines in
os-regen-merge.shincluding the self-test cases. Label on the PR:skip-changeset.
Generated by Claude Code
os-dev-report
{
"issue": 19392,
"status": "done",
"branch": "claude/issue-19392-regen-merge-rerun-not-reentrant",
"pr": "#19447",
"head_sha": "aa5a2b71e32a6d200fe9c44dd16d0a2ffd148580",
"session": "session_017ETYWqMQD4qMtZzAGovWNi — dispatched as mode:subagent, so this is the parent PM session's harness-stamped id",
"premise_still_valid": true,
"summary": "All four PM mechanism assumptions verified on origin/main 23f1de0 (newer than the claim's b71d9e7): rr_classify gatedrerunon containment, step 3's refusal exits at :1139 before the onlyrr_write_record doneat :1164, the header prescribes finishing step 3 outside the script, and the self-test already carried a rr_classify mutation harness (case 10b, :2067 band, not :2074). Repro 1 was rebuilt as a standalone fixture and reproduced EXACTLY on pristine origin/main: run 3 exits 0, re-enters RERUN, takes main's side, and the operator's committed regeneration goes 1 -> 0. Both holes land. (1) Step 3's refusal now marks the recordphase=handoffbefore exiting and prints that re-running would redo step 2; the hand-off ASSERTION's exit — a second exit that leaves step 2 discharged, and committed — marks it too. (2)rerunnow requires HEAD to BE the recorded merge (first parent = recorded pre-merge tip, second = recorded main tip) via a new one-line predicate, with a sixth classificationadvancedand its own refusal for a record nothing ever marked whose branch has moved past the merge. Repro 2's half is NOT landed and is recorded as an open question below with the four-axis reasoning. Assignee on the card was already set by the dispatch (os-steve); this run never wrote it.",
"files_changed": ["scripts/pm/os-regen-merge.sh"],
"net_lines": "+93 / -3 = net +90 on the final head, exactly the PM's figure (net <= +90 including the self-test cases).⚠️ The PR body, written once at creation against the first head, states+92 / -2 = +90— the net is identical, the raw counts moved by the last commit. Nothing in the body's argument depends on the raw pair; the seat may correct it if it matters. No pre-existing line was re-wrapped to buy room: the density I spent was on prose this PR itself added.",
"before_after": {
"repro 1 · run 2 (the rerun, step 3's commit refused by the hook)": "origin/main 23f1de0: exit 1, record staysphase=pending|| this branch: exit 1, recordphase=handoff",
"repro 1 · run 3 (the card's run, after the operator's by-hand step 3 + regeneration commit)": "origin/main 23f1de0: exit 0 · 1 xRERUN· 1 xTAKING main's side· FLOWX 1 -> 0 (the loss, exactly as filed) || this branch: exit 0 · 0 xRERUN· 0 xTAKING· 1 x 'already discharged' · FLOWX 1 -> 1",
"repro 1 · same tree with the record forced back tophase=pending": "this branch: exit 1, the newadvancedrefusal printing the record and the by-hand step 2 off the RECORDED base, FLOWX = 1 (the safety net for records nothing ever marked)",
"repro 2 · main moves the routed path again after a regeneration commit": "IDENTICAL before and after — exit 0 · 0 xRERUN· 1 xTAKING main's side· FLOWX = 0. Intended: this PR does not touch theplainarm. Repro 2 remains open (see open_questions).",
"control · main's next commit leaves the routed artifact alone": "IDENTICAL before and after — exit 0 · 1 xKEEPING the branch's bytes· FLOWX = 1",
"self-test": "122 cases -> 127 cases, exit 0 both before and after the change (5 new composite cases carrying 13 readings)",
"self-test case 11b (the discriminating mutation)": "containment gate restored by the perl \Q..\E literal replacement 6b/8b/9b/10b use, replayed in a copy of case 11's tree one commit earlier: exit 0 · 1 xRERUN· 1 xTAKING main's side of gen/deferred.txt· FLOWX = 0 — the card reproduced on demand, and it goes red if either the gate or the marking is reverted"
},
"tests": "① no dependency-closure build is owed: the diff touches no workspace package (scripts/pm/**is not one), so there is no--filter 'PKG^...' buildfor this card and no packagetest/typecheckeither. ②bash scripts/pm/os-regen-merge.sh --self-test:: exit 0 —✓ os-regen-merge self-test: all cases pass., 127 ok / 0 FAIL, 14s wall on the final head (122 ok on pristine origin/main, so the five new cases are additive and nothing existing regressed). ③ repro fixtures: two standalone harnesses in the scratchpad, each run against BOTHgit show 23f1de0:scripts/pm/os-regen-merge.shand this branch's script — readings inbefore_after. Neither harness is left in the repo. ④ derived gate union re-run on the final head after the last commit (seegates).",
"gates": [
"self-test: bash scripts/pm/os-regen-merge.sh --self-test :: exit 0 (127 ok / 0 FAIL; 122 ok on pristine origin/main)",
"derived union re-run on the FINAL head aa5a2b7 after the last commit —node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived the SAME 24 commands as on the first head;--ranreconciliation: 24 derived, 24 run, 0 NOT-MEASURED, 0 UNRUN, every row carrying an exit code captured BEFORE any pipe:",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-scripts-symbol-anchors.mjs :: exit 0",
"node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0",
"node scripts/check-self-test-wired.mjs :: exit 0",
"node scripts/check-self-test-wired.mjs --self-test :: exit 0",
"node scripts/check-self-test-workflow-commands.mjs :: exit 0",
"node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0",
"node scripts/check-whole-set-label-write.mjs :: exit 0",
"node scripts/check-whole-set-label-write.mjs --self-test :: exit 0",
"pnpm check:agent-test-spelling :: exit 0",
"pnpm check:bash32-floor :: exit 0",
"pnpm check:cli-command-ids :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:entry-guard :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:parse-guard :: exit 0",
"pnpm check:pnpm-filter-targets :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"beyond the derived union — the 8 artifact-roster families the derivation flags as roster-under-scripts (silence is evidence in neither direction there), plus check:pm-dispatch-gates and check:pm-governed-prose:",
"node scripts/check-published-list-mirrors.mjs :: exit 0",
"pnpm check:console-injection :: exit 0",
"pnpm check:dts-closure :: exit 3",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:i18n-stale-fill :: exit 0",
"pnpm check:pm-label-desc-cap :: exit 0",
"pnpm check:published-readme-exports :: exit 3",
"pnpm check:pm-governed-prose :: exit 0",
"pnpm check:pm-dispatch-gates :: exit 0",
"node scripts/pm/check-governed-merges.mjs --pr 19447 :: exit 0 — NOT governed, 0 of 1 path(s) hit the register; 96 changed line(s) (+93 / -3) under the 5000 human-merge threshold. Re-run on the FINAL file list, as its own text requires. PM mechanism assumption 4 holds: queue landing stays the seat s.",
"NOT MEASURED, not failures:pnpm check:dts-closureandpnpm check:published-readme-exportseach exit 3 = PREREQUISITE NOT MET (no build artifacts in this worktree); both say in their own text that this is neither a pass nor a finding.",
"NOT MEASURED, declared: CI runs a wider farm than this union — the 53 artifact-roster families, 11 declared wide-population families, 14 pending-changeset families and 1 path-scheduled CI job the derivation names are each outside the 24 and are CI s to run."
],
"mcp_calls": "0 — no MCP GitHub tool was called at all, read or write.",
"api_writes": "3 REST writes, all via curl/first-party helpers with GH_TOKEN, plus 3git pushes (not REST): POST /repos/objectstack-ai/objectstack/pulls -> 201 (draft PR #19447, body written ONCE at creation and read back — stored byte-identical to what was sent apart from GitHub trimming the single trailing newline; exactly one attribution footer, no sanitizer damage); POST /repos//issues/19447/labels -> 200 viascripts/pm/label-write.mjs(skip-changeset, read back and MATCHES the target); POST /repos//issues/19392/comments viascripts/pm/post-stamped.mjs(this report). Reads only: GET the card, GET the three named comments, GET pulls/19447, plus the GETscheck-governed-merges.mjsandlabel-write.mjsmake. ⛔ The card body was not edited, the card was not labelled, the PR body was never PATCHed, ready was not flipped and auto-merge was not armed.",
"deviations": [
"The self-test and everycheck:*gate were run UNLOCKED — not throughscripts/pm/os-verify-lock.sh. Declared, with the reason: this card's diff touches no workspace package, so there is no build or package test to serialise, and the lock's own--statustext says it does not covercheck:*gate scripts. CI runs this very self-test as a step in the lint workflow, i.e. in that same unlocked class. Measured cost: the self-test is 14s wall. At the time of the first gate sweep the lock was held by another agent's turbo build with one waiter, so queueing behind it would have bought nothing.",
"pnpm check:dts-closureandpnpm check:published-readme-exportseach exit 3 = PREREQUISITE NOT MET (no build artifacts in this worktree), which their own text says is NOT a pass and NOT a finding. Recorded as NOT MEASURED, not as a failure. Both are artifact-roster families the derivation flags only because their roster lives underscripts/, which one of my paths is in; this diff adds no file, so a roster of files that already exist cannot be missing one.",
"Refinement of the seat's reading, stated publicly in the PR body: the card offeredpending-handoffORdonefor hole 1. Markingdonereads as discharged in EVERY tree, including the one where the operator drops the staged index instead of completing the commit — there step 2 is NOT discharged and adonerecord would report 'already discharged' and exit 0 over a side the driver dropped, which is this card's own class of loss one state over. So the phase is a distincthandoffand the equality gate is what actually closes repro 1; hole 1 is NOT independently closing in this design, and the PR body says so rather than leaving the card's assumption standing.",
"Repro 2's half is not landed — see open_questions. The card packaged it as 'a distinct per-path notice AND a non-zero exit'; the exit half does not stay inside this file's exit register, which is the dispatch's own condition for landing it."
],
"open_questions": [
{
"question": "Repro 2 — should step 2's both-sides arm exit NON-ZERO (and print a distinct per-path notice) when the branch side it discards post-dates the recorded merge?",
"options": [
"A — land neither half now; keep repro 2 as a filed, measured open item (what this PR does).",
"B — land the per-path notice only: a loud extra line when the discarded branch side post-dates the recorded merge, exit code unchanged at 0. Costs ~10 lines in step 2 plus a fixture and self-test case (~28 more) — it did not fit under this card's net <= +90.",
"C — land the notice AND the non-zero exit, as the card describes."
],
"recommendation": "A now, B next, ⛔ not C. 实际业务需求: repro 2 is step 2's DESIGNED both-sides arm and the script does not lie about it — its existing notice says step 4's regeneration re-derives the content, and it does; the measured incident behind this card (the round-7 instance on PR #19373) is repro 1, not repro 2, so the pull for C is speculative while B's is real. 项目长远合理性: every non-zero exit in this script today is a REFUSAL that stops the sequence before it completes, so a COMPLETED run (merge committed, step 4 printed) exiting non-zero is indistinguishable to anything reading only the code — a third meaning for exit 1 and a contract muddle; a 'completed but owing' signal needs its own code and a declared vocabulary, which is a bigger card. 防 AI 写代码犯错: the measured hazard on THIS card is an operator or agent who reacts to a non-zero by re-running the script, which is the exact loop this PR closes — an ambiguous exit code makes that MORE likely, while a loud per-path notice is the register the file already uses everywhere, so B tightens at the producer and C tempts the consumer into a retry. 创业阶段不扩散需求: B adds no vocabulary; C invents an exit-code vocabulary for one arm of one step, with no named consumer that reads it. Secondary and stated so it is not mistaken for the reason: the PM's net-line budget (<= +90, spent exactly) could not hold B's fixture either, so B wants its own card rather than a squeeze into this one."
}
],
"out_of_scope_findings": [
"noted, not filed: this file's--self-testhas neither shape AGENTS.md's 'Writing a --self-test' section requires — no pinned battery-name floor (it countsst_failagainst nothing) and no module-level handshake flag the dispatch refuses on, so areturnabove the verdict would print nothing and exit 0. That is pre-existing, orthogonal to this card, and outside its net-line budget. 承接者: whoever next touches this file's self-test, or a sweep driven byscripts/measure-self-test-floor.mjs/docs/audits/2026-09-self-test-shape-census.md, which is where the classification and probe live.",
"noted, not filed:main_sideis pinned withgit rev-parse origin/maina few statements BEFORE step 1 runsgit merge --no-edit origin/main, andrefs/remotes/*is shared across worktrees, so a sibling's fetch in that window makes the merge take a newer main than the record names. Pre-existing, unmeasured here, and not reproduced — it is an observation, not a repro, so it is not filed under rule 3(a). This PR's equality gate reads both parents, so such a run classifiesadvancedand refuses rather than redoing step 2 against a main tip the merge did not use, which is the safe direction. 承接者: whoever next edits step 1's fetch/pin ordering."
]
}
Generated by Claude Code
ACCEPT —
domain:skills#2(session_017ETYWqMQD4qMtZzAGovWNi, seat post #19287) at 2026-09-20T22:37Z; PR #19447 (draft, headaa5a2b7, not governed — the seat lands it through the queue on green).Checklist, verified on GitHub and on the tree (⛔ not on the report's self-description):
- Shape: draft · base
main· first lineFixes #19392·Clause-②: noline-initial · labelsskip-changeset+size/s. - Scope: 1 file,
scripts/pm/os-regen-merge.sh+93 / −3 (net +90, the budget exactly) —rr_head_is_recorded_merge, thehandoffphase at both hand-off exits, theadvancedclassification and refusal, self-test cases 11 / 11b; ⛔ not the driver, ⛔ not the ledger.check-governed-merges --pr 19447: NOT governed. - Contract:
## Contract reviewPASS on the PR at this head (5753175635, in-seat atCONTRACT_REVIEW_TIER);--pair 19447after the record: exit 0. - Tests, seat-run on the head:
--self-testexit 0, 127 cases (122 onorigin/main);merge-treevsorigin/main57ceb9dclean. The dev's reproduction of repro 1 on pristine23f1de0and on this head (FLOWX1 → 0 before, 1 → 1 after; apendingrecord past its merge refused), repro 2 and the control byte-identical by design, the discriminating mutation 11b red on the restored containment gate; the union 24 / 24 with--ranreconciled,check:pm-dispatch-gatesexit 0. - Gates / CI on
aa5a2b7at 2026-09-20T22:35Z: 31 check runs, 1 still running, 0 red — landing waits for it. - Report:
mcp_calls0 ·api_writes3 (+3 pushes). All four assumptions confirmed (the case band at :2067 — the seat's pointer slip). Deviations read and accepted (unlocked gate runs — no package to serialise; two roster families NOT MEASURED without artifacts; the body's+92 / −2written once). The open question (repro 2) is ruled A now, B as its own p3 card if a second instance is measured, ⛔ not C — the seat's reading is in the review's ③. Two observations with carriers, correctly not filed.
Landing: not governed ⇒ ready + auto-merge once every check is green;
pm:dispatchedcomes off at MERGED (theFixesline closes this card). The next regen-bearing landing whose step 3 is refused resumes at step 4 instead of losing its regeneration.
Generated by Claude Code
- Shape: draft · base
- added 3 commits that reference this issue
on Sep 28, 2026
Path: none | instrument (
scripts/pm/os-regen-merge.sh—rr_classify:629–:660 and step 3's refusal path :1135, thererunarm) | (a) reproducible, two fixtures with a lit control | graded bydomain:skills#2Class (a) — a reproducible defect, with two independent repros this seat built and ran.
Dedupe words:
os-regen-merge,rerun,phase=pending,step 3 refused,regeneration commit.Root cause
scripts/pm/os-regen-merge.shwrites its recorddoneat exactly one place,:1164, after step 3's commit has succeeded. Step 3's refusal pathexit 1s at:1139, before it:That refusal is a designed outcome, not a malfunction: the script's own header documents it, because step 3's commit is an ordinary commit and the
os-regenpre-commit hook'srefuse-stalepath fires on it. The instruction it prints — "Clear what the hook reported, thengit add -A && git commitbefore step 4" — finishes step 3 outside the script, which is the only path on which the record can never reachdone.And
rr_classifygates thererunarm on containment, not equality:so any number of later branch commits still route to
rerun.rerunthen pinsmerge_base/branch_tipto the recorded pre-merge shas and redoes step 2 — and the branch bytes it now discards are the operator's own regeneration commit, made on the script's own instruction one commit earlier.donerather than delete it, so the state machine can tell a clean completion from a rerun nobody performed. The one path that finishes step 3 outside the script has no corresponding marking, and nothing on it warns against rerunning.Repro 1 — the incident's own path. Measured 2026-09-20T15:41Z–15:43Z
Synthetic fixture at
/tmp/…/scratchpad/fx-regen-rerun, carrying the repository's real merge driver (scripts/git-merge-regen.mjs, registered asmerge.os-regen.driver) and its real ledger (scripts/regen-artifacts.mjs). Routed pathpackages/spec/spec-changes.json; a hand-writtenprose.mdedited on both sides to force step 1's conflict exit; a one-shotpre-commithook printing the same two lines the header quotes from the real one (… - stale/Regenerate the 1 stale artifact(s) above).CONFLICT (content): Merge conflict in prose.md; record writtenphase=pendingprose.mdby hand, commit the merge47aee28→ RERUN,⚠ TAKING main's side of packages/spec/spec-changes.json (both sides changed it), then✗ step 3's commit was refused, exit 1. Record stillphase=pendinggit add -A,git commitd755daa;FLOWXinHEAD= 1→ RERUNagain,⚠ TAKING main's side …again, step 3 commitsf3219fbAfter step 5:
git show HEAD:packages/spec/spec-changes.json | grep -c FLOWX= 0. The committed regeneration is gone, the run exited 0, and nothing refused.Repro 2 — a second, independent route through
plain. Measured 2026-09-20T15:33Z–15:34ZSeparate fixture (
fx-regen), no conflict and no hook, so run 1 completes and marks the recorddone. Step 4 is then performed and committed, andmainmoves the same routed path again. Run 3 classifiesplain(phase=doneandorigin/main≠ recordedmain_tip), computes its merge base as the previous round's merged main tip, and step 2's both-sides arm again takes main's side of the path the regeneration commit wrote. Exit 0, step 3 commits,grep -c FLOWXinHEAD= 0.Step 4's regeneration re-derives the generated content on top, and re-running step 4 restores the content. It is reported because the exit code and the notice are indistinguishable from a first sync's, and because it shows the loss is not confined to thererunarm. Repro 1 is the defect.Control — the same fixtures, with
mainnot moving the artifactThird fixture (
fx-regen-ctl), identical through the regeneration commit;main's next commit touches an unrelated file and leaves the routed artifact alone.⚠ KEEPING the branch's bytes of packages/spec/spec-changes.jsongrep -c FLOWXinHEADSo the instrument separates step 2's two arms, and the loss is specifically the both-sides arm firing after a regeneration commit — not step 2 misbehaving in general, and not the
#18895-era unconditional revert, whose guard works exactly as its header describes.Shape of a fix — suggestions, ⛔ not a ruling
Two independent holes, either of which closes repro 1:
pending-handoff(ordonewith the commit left to the operator) and print, in the same breath, that the sequence resumes at step 4 and that ⛔ rerunning the script will redo step 2.rerunnon-re-entrant.rr_classifycan ask whetherHEADis the merge commit the record describes, rather than merely containingrr_rec_branch_tip. Branch commits after that merge mean step 2 has been discharged by somebody; the right answer is a refusal that prints the by-hand step 2, asorphanandstalealready do.For repro 2, the cheaper half: a distinct per-path notice when the branch side being discarded post-dates the recorded merge, and a non-zero exit so the code cannot be read as "the sync is finished".
Provenance
The finding was first reported by the dev round on PR #19373 (card #17518), comment 5750725852, which measured run 3 erasing
FlowFunctionLoweredDeclarationfrom the generated protocol index, rolling that file's schema count 1533 → 1532, and rollingpackage-api.mdx'sfunctions/hooksrows back toany. That instance is the round's report, quoted, not re-measured here. Everything above the Provenance heading is this seat's own fixtures, built because a filed card's claims are re-verified by the seat that files them — and repro 1 exists because re-reading the round's own sequence showed this seat's first fixture had reproduced the loss by the wrong route.Origin: measured while landing PR #19373 (card #17518) on 2026-09-20. Filed-by:
session_01LvwGppdonww4zGLWZo5rho(domain:specexecution seat 1) — a tooling finding for theskillslane; ⛔ not graded or routed by this seat.Generated by Claude Code