Skip to content

feat: --force/--overwrite for --save-script + arm-time EEXIST preflight#1266

Merged
thymikee merged 5 commits into
mainfrom
feat/save-script-force-and-arm-preflight
Jul 15, 2026
Merged

feat: --force/--overwrite for --save-script + arm-time EEXIST preflight#1266
thymikee merged 5 commits into
mainfrom
feat/save-script-force-and-arm-preflight

Conversation

@thymikee

Copy link
Copy Markdown
Member

Summary

Closes #1258.

  • --force/--overwrite flag. fix(replay): repair-transaction lifecycle — keep-alive, no-partial-emit, close-as-lifecycle, atomic publish #1235 made healed-script publication refuse-on-exist (publishHealedScriptAtomically throws COMMAND_FAILED on an existing target). This PR adds --force (alias --overwrite) to replay, close, and open — when set, the publish primitive atomically REPLACES an existing target (renameSync, same-directory atomic replace) instead of refusing. Default (flag absent) is unchanged: refuse-on-exist.

    Threaded CLI → daemon request → publish primitive:

    • Reused the existing global force CLI flag (already used by connect --force) rather than adding a second flag under the same name — flag names are a single global namespace in this codebase, and --force was already taken. Added --overwrite as an alias on that same flag definition and broadened its help text to cover both meanings.
    • force is captured on SessionState.saveScriptForce at arm time (open --save-script --force, or a repair's first replay --save-script --force), mirroring how saveScriptPath is persisted — so a later write that doesn't repeat the flag (a bare close finishing a session opened with open --save-script --force, a --from continuation leg, or an unattended auto-commit teardown with no live request at all) still honors the overwrite.
    • The effective decision at every write site is req.flags?.force || session.saveScriptForce.
  • Arm-time EEXIST preflight (second half of Add --force/--overwrite flag for --save-script now that publish refuses any existing target #1258's follow-up need). Before this change, a replay --save-script repair run only discovered an existing target at publish time (close/completion) — an agent could run a whole repair (corrective steps included) only to hit EEXIST at the very end. session-replay-runtime.ts now resolves the same target the armer would (explicit --save-script=<path>, an already-armed session's existing path on a --from continuation, or the default <stem>.healed.ad sibling) before the step loop starts, and fails fast with the refuse-on-exist error before any step — including the first open — dispatches. --force skips this preflight (the caller already opted into overwriting).

Test plan

  • npx tsc --noEmit
  • npx oxlint --deny-warnings
  • npx oxfmt --check
  • npx vitest run — full src/daemon, src/commands, src/client, src/cli suites (192 files / 1790 tests), plus the connect/--force CLI-parsing tests, all pass
  • New/updated coverage:
    • session-script-writer.test.ts: write(session, { force: true }) overwrites an existing target atomically, for both a repair-armed COMPLETE artifact and an ordinary (non-repair) recording; default (no force) still refuses
    • session-replay-repair-transaction.test.ts: arm-time preflight rejects an existing --save-script target with zero steps dispatched (verified via a request spy); --force skips the preflight and the replay proceeds; close --save-script --force overwrites an existing COMPLETE healed .ad
  • pnpm check:fallow --base origin/main — flags one pre-existing complexity finding on SessionScriptWriter.write (cyclomatic 10, unchanged branch structure vs origin/main); verified via direct diff that my change only added a parameter and a pass-through line, no new branches — not a regression introduced by this PR

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.7 MB 1.7 MB +1.5 kB
JS gzip 550.4 kB 550.9 kB +509 B
npm tarball 663.7 kB 664.4 kB +708 B
npm unpacked 2.3 MB 2.3 MB +2.6 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 27.3 ms 28.3 ms +1.1 ms
CLI --help 59.2 ms 60.6 ms +1.5 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/session.js +674 B +230 B
dist/src/snapshot-diagnostics.js +382 B +141 B
dist/src/registry.js +383 B +120 B
dist/src/agent-device-client.js +62 B +18 B

@thymikee

Copy link
Copy Markdown
Member Author

Review of bb4cfa034: design conforms to the spec; one CI blocker, one design question, one nit.

Conformance verified (traced, not trusted):

  • The arm-time preflight is advisory-only and the publish primitive stays authoritative — correct division. It resolves the SAME target the armer would (verified the string/boolean/continuation branches match armReplaySaveScriptStep's else if (saveScriptPath === undefined) logic), and the test proving zero dispatches + no arm side-effects (saveScriptBoundary never stamped) is exactly the right fidelity bar — fail-fast-before-step-1, not fail-fast-after-open.
  • force reaches all three teardown paths: explicit close (req.flags.force || session.saveScriptForce), --from continuations (persisted at arm), and unattended finalizeRepairTeardown (persisted only — correctly documented as having no live request).
  • The recorder persists saveScriptForce only inside the entry.flags.saveScript gate, so an unrelated recorded action carrying force (e.g. connect semantics) cannot authorize an overwrite. Good.
  • renameSync for the force path is the right primitive (atomic replace, no missing-file window); the temp-cleanup no-op note is accurate.

Blocker (CI): Fallow's complexity gate is red — runReplayScriptFile is now 259 lines and this PR's additions tipped it. Extract the pre-loop preamble (C2 reset + preflight + armer construction) into a helper rather than re-baselining; the function was already the file's hotspot.

Design question (non-blocking, wants an explicit decision): saveScriptForce is sticky for the session's lifetime and survives RETARGETING — a session armed --save-script=a.ad --force that later re-arms --save-script=b.ad (no force) will silently overwrite b.ad. The opt-in was arguably per-target, not per-session. Either clear saveScriptForce when saveScriptPath changes to a different path without a live force, or document in the SessionState comment why sticky-across-retarget is intended. Low severity (sessions are short-lived), but it's the one place the current semantics can overwrite a file nobody opted to overwrite.

Nit: the shared flag definition means connect --overwrite is now also valid (as an alias for its existing --force connection-replacement semantics). Harmless, but the usage string is doing double duty — fine as long as it's deliberate.

With the Fallow extraction done, this closes both repair-loop UX gaps (#1258 + the arm-time discard trap) as specified.

thymikee added a commit that referenced this pull request Jul 14, 2026
…ilure

Pure structural refactor, no behavior change: moves the diagnose +
classify-and-return / AppError-rethrow logic out of SessionScriptWriter.write
into a module-level helper. Drops write()'s cyclomatic below Fallow's
threshold (the finding CI flagged on #1266) — the extracted throw still
propagates out of the catch exactly as before.
@thymikee

Copy link
Copy Markdown
Member Author

Fixed the red Fallow Code Quality check (a5234e5).

SessionScriptWriter.write was over Fallow's complexity threshold (cyclomatic 10 / cognitive 11 / CRAP 31.6). Extracted the catch-block's diagnose-and-classify logic (the emitDiagnostic call plus the repair-armed / AppError-rethrow routing) verbatim into a module-level handleSessionScriptWriteFailure helper. write()'s catch now just delegates to it.

Pure structural refactor, zero behavior change — the extracted throw error still propagates out of the catch exactly as before (the ordinary-recording fail-loud path), and the repair-armed { written: false, error } return is unchanged.

Verification:

  • pnpm check:fallow --base origin/main✓ No issues in 12 changed files (green)
  • npx tsc --noEmit, npx oxlint --deny-warnings, npx oxfmt --check all clean
  • session-script-writer / session-replay-repair-transaction / session-close-shutdown / session-replay-runtime suites: 65/65 pass, behavior identical

Pushed head: a5234e5ae7a733fe082e31267aeadc1fe360c677

@thymikee

Copy link
Copy Markdown
Member Author

Re-review of exact head a5234e5: Fallow is fixed, but persisted overwrite authorization is inconsistent.

runReplayScriptFile passes only the live request force flag to the EEXIST preflight. A repair armed with save-script plus force and later continued with replay from plus save-script, without repeating force, is rejected by preflight when the target exists, even though the session persisted saveScriptForce and every eventual write path honors it. Use the same effective decision as publication, live force or preRunSession.saveScriptForce, and add the continuation regression.

The earlier per-target question also remains: re-arming from forced a.ad to b.ad without a live force currently carries sticky authorization and may overwrite b.ad. Clear force when retargeting without an explicit opt-in, or document and test that broader session-level contract explicitly.

All checks are green, but this head is not merge-ready.

@thymikee

Copy link
Copy Markdown
Member Author

Both persisted-force inconsistencies fixed (7f3accd28).

Point 1 — preflight now honors persisted force. runReplayScriptFile passed only req.flags?.force to the arm-time EEXIST preflight, so a repair armed with --save-script --force and continued via replay --from … --save-script (no repeated --force) was wrongly rejected on a target the earlier forced leg already authorized. Changed the preflight's force argument to the SAME effective decision publication uses: Boolean(req.flags?.force || preRunSession?.saveScriptForce).

  • Regression added: two-leg flow — leg 1 --save-script --force (target pre-exists) diverges and persists saveScriptForce; leg 2 --from … --save-script (no --force) against the still-existing target is NOT preflight-rejected and reaches completion. Confirmed it fails (leg2.ok === false, "already exists") when the fix is reverted.

Point 2 — force is per-target, cleared on retarget (chose your preferred option (a)). Re-arming a different --save-script=<b> without a live --force now DROPS the saveScriptForce persisted for <a>, so a retarget can never silently overwrite a file nobody opted into. Retarget detection is the single point where the explicit save-script path is (re)assigned — extracted into a shared applySaveScriptRetarget(session, nextPath, liveForce) used by BOTH re-arming paths (armReplaySaveScriptStep for replay, recordActionEntry for close --save-script): it clears saveScriptForce iff the path changes to a different value AND no live force accompanies it. A live --force on the retarget re-grants it (set after the clear, so it always wins). Updated the SessionState.saveScriptForce doc to state the per-target contract explicitly.

  • Regressions added: close --save-script=<b> (no --force) on a session forced for <a> REFUSES an existing <b> and keeps the session for retry (saveScriptForce cleared); the --force contrast overwrites <b>. Confirmed the refuse-case fails (silently overwrites) when the clear-on-retarget is reverted.

Nit (connect --overwrite alias): deliberate — it's the one shared global force flag, and --overwrite reads naturally for connection-state replacement too. Left as-is.

Verification: npx tsc --noEmit, npx oxlint --deny-warnings, npx oxfmt --check clean; pnpm check:fallow --base origin/main✓ No issues in 12 changed files (green, no waiver — confirmed the two fallow-ignore comments in session-replay-runtime.ts are pre-existing on origin/main, none added here); session-script-writer / session-replay-repair-transaction / session-close-shutdown / session-replay-runtime suites: 68/68 pass.

Pushed head: 7f3accd286d45d4e2f0b11f54cd4e8c8ff1c304a

@thymikee

Copy link
Copy Markdown
Member Author

Re-review of exact head 7f3accd: the persisted-force continuation and close-retarget cases are fixed, but replay retarget preflight still has an ordering mismatch.

The preflight accepts persisted saveScriptForce before determining whether this request explicitly retargets from forced a.ad to b.ad. Later, applySaveScriptRetarget clears that authorization for b.ad, so replay steps can execute and publication then refuses existing b.ad. Persisted force should bypass preflight only when the computed target is unchanged, unless the current request carries live force.

Add a replay continuation regression for prior forced a.ad plus explicit existing b.ad without live force: it must fail before dispatch and must not mutate the session target.

Not merge-ready yet.

@thymikee

Copy link
Copy Markdown
Member Author

Preflight ordering mismatch fixed (6618d4f88).

The bug you caught: the preflight computed Boolean(req.flags?.force || preRunSession?.saveScriptForce) — it accepted persisted force regardless of whether THIS request retargets. So a --from continuation with explicit --save-script=b.ad (no live force) on a session forced for a.ad would PASS the preflight → run every step → applySaveScriptRetarget then clears the force for b.ad → publication refuses the existing b.ad. The run executed (mutating the session mid-flight) only to fail at the end — exactly what the arm-time preflight exists to prevent.

The fix: the effective-force decision now lives inside preflightSaveScriptTarget, computed against the targetPath this request resolves to (the same path the armer will set), matching applySaveScriptRetarget's per-target contract:

effectiveForce = Boolean(liveForce) || (Boolean(persistedForce) && targetPath === existingSaveScriptPath)
  • Live --force/--overwrite → always bypasses.
  • Persisted saveScriptForce → bypasses ONLY when targetPath === preRunSession.saveScriptPath (same target it was granted for).
  • Explicit retarget to a different path without live force → does NOT bypass → refused before any step dispatches.

resolvedTarget was already computed inside the function (the existence check used it), so no extra plumbing — I just pass liveForce/persistedForce in separately instead of pre-combining them at the call site. The preflight stays read-only: it runs before applySaveScriptRetarget, receives only existingSaveScriptPath (not the session), and never mutates anything.

Regression added: a --from continuation retargeting to an existing b.ad without live force — leg fails at the preflight (COMMAND_FAILED / "already exists"), dispatches zero steps (asserted via request spy), and leaves saveScriptPath/saveScriptForce unchanged (still armed/forced for a.ad), with b.ad byte-for-byte untouched. Confirmed it fails (leg wrongly passes the preflight and runs) when the ordering fix is reverted.

Unchanged cases stay correct (all still green): same-target --from continuation → persisted force still bypasses; retarget WITH live force → still overwrites; close-retarget without force → still cleared+refused.

Verification: npx tsc --noEmit, npx oxlint --deny-warnings, npx oxfmt --check clean; pnpm check:fallow --base origin/main✓ No issues in 12 changed files (green, no waiver); session-script-writer / session-replay-repair-transaction / session-close-shutdown / session-replay-runtime: 69/69 pass.

Pushed head: 6618d4f8830d036923a772f84f7e72df6fa35507

thymikee added a commit that referenced this pull request Jul 14, 2026
…ilure

Pure structural refactor, no behavior change: moves the diagnose +
classify-and-return / AppError-rethrow logic out of SessionScriptWriter.write
into a module-level helper. Drops write()'s cyclomatic below Fallow's
threshold (the finding CI flagged on #1266) — the extracted throw still
propagates out of the catch exactly as before.
@thymikee thymikee force-pushed the feat/save-script-force-and-arm-preflight branch from 6618d4f to 83d189f Compare July 14, 2026 19:20
@thymikee

Copy link
Copy Markdown
Member Author

Rebased onto origin/main (now includes #1267, 0436793c25).

Clean rebase — no conflicts. All 4 commits (bcdd95ac183d189fa9) replayed without conflict:

Re-verified against the merged code:

  • npx tsc --noEmit, npx oxlint --deny-warnings, npx oxfmt --check (my 12 files) all clean
  • pnpm check:fallow --base origin/main✓ No issues in 12 changed files (green, no waiver)
  • Suites 147/147 pass, including the fix(replay): extend resume.from record-and-heal shape to caution/manual #1267-touched ones: session-replay-resume (27), session-replay-runtime-plan (6), plus session-script-writer (26), session-replay-repair-transaction (17, incl. all force/preflight regressions), session-close-shutdown (19), session-replay-runtime (7), app/replay/cli-help facets (45)

New head: 83d189fa90d6b722ad6cbf52a9fa21eddb0b61ae

@thymikee

Copy link
Copy Markdown
Member Author

Re-review of exact head 83d189f found one remaining correctness bug.

The retarget EEXIST preflight now computes per-target force correctly, but saveScriptComplete is cleared before that preflight runs. If the session already holds a completed repair, including a valid #1267 empty-tail completion, a rejected retarget returns before dispatch while leaving saveScriptComplete false. A later close can then treat the repair as incomplete and tear it down without publishing.

Move the completion reset after the save-script preflight, or restore the prior value on rejection. Extend the existing no-dispatch and no-mutation regression to assert saveScriptComplete remains true and that a subsequent close can still commit the original target.

The current iOS smoke job has also failed and macOS is still running, so this head is not merge-ready.

thymikee added a commit that referenced this pull request Jul 14, 2026
…ilure

Pure structural refactor, no behavior change: moves the diagnose +
classify-and-return / AppError-rethrow logic out of SessionScriptWriter.write
into a module-level helper. Drops write()'s cyclomatic below Fallow's
threshold (the finding CI flagged on #1266) — the extracted throw still
propagates out of the catch exactly as before.
@thymikee thymikee force-pushed the feat/save-script-force-and-arm-preflight branch from 83d189f to fe87050 Compare July 14, 2026 19:31
@thymikee

Copy link
Copy Markdown
Member Author

Rebased onto origin/main (now includes #1257 ADR-0014 session ref-frame lifetime, 54977f3b87).

Clean rebase — git auto-merged the overlap, and I verified both sides coexist (not one silently dropping the other) on all three flagged files:

No conflict needed a design call — the two changesets touch disjoint concerns (ref-frame lifecycle vs save-script force) in the same files.

Re-verified against the merged base:

  • npx tsc --noEmit, npx oxlint --deny-warnings, npx oxfmt --check (my 12 files) all clean
  • pnpm check:fallow --base origin/main✓ No issues in 12 changed files (green, no waiver)
  • Full daemon-handlers suite: 850/850 pass (76 files), including session-script-writer (26), session-replay-repair-transaction (17, all force/preflight regressions), session-close-shutdown (19), session-replay-runtime (7), and the ADR-0014 ref-frame suites

(Prior head's macOS Smoke Test flake noted — infra, unrelated; the fresh CI run should clear it.)

New head: fe870503171030cbcaef93179e8387d53ae06958

thymikee added 4 commits July 14, 2026 21:49
…light

#1235 made healed-script publication refuse-on-exist. #1258 adds an escape
hatch: --force (alias --overwrite) on open/close/replay makes
publishHealedScriptAtomically atomically REPLACE an existing target
(renameSync) instead of refusing. The flag threads CLI -> daemon request ->
SessionState.saveScriptForce (persisted at arm time, like saveScriptPath, so
a later close/auto-commit that doesn't repeat the flag still honors it) ->
the publish primitive. Default (flag absent) is unchanged: refuse-on-exist.

Second half: an arm-time EEXIST preflight in session-replay-runtime.ts now
fails a repair-armed replay --save-script BEFORE any step dispatches when
its target already exists and --force is not set, instead of only failing
at publish time after the whole repair run (and its corrective steps) has
already executed against the device.
…ilure

Pure structural refactor, no behavior change: moves the diagnose +
classify-and-return / AppError-rethrow logic out of SessionScriptWriter.write
into a module-level helper. Drops write()'s cyclomatic below Fallow's
threshold (the finding CI flagged on #1266) — the extracted throw still
propagates out of the catch exactly as before.
…get)

Addresses two authorization inconsistencies in the #1258 --force work
flagged in re-review:

1. Arm-time EEXIST preflight now uses the SAME effective force decision as
   publication — `req.flags?.force || preRunSession.saveScriptForce` — instead
   of the live flag alone. A repair armed with `--save-script --force` and
   continued via `replay --from … --save-script` (without repeating --force)
   is no longer rejected on a target the earlier forced leg authorized.

2. Force is now per-target, not sticky-across-retarget. Re-arming a DIFFERENT
   `--save-script=<other>` without a live --force CLEARS the persisted
   `saveScriptForce` (new shared `applySaveScriptRetarget`, used by both the
   replay armer and `recordActionEntry`/close), so a retarget can never
   silently overwrite a file nobody opted into. A live --force on the retarget
   re-grants it for the new target. Updated the SessionState doc accordingly.

Regressions: persisted-force `--from` continuation is not preflight-rejected;
retarget-without-force refuses an existing target (and the --force contrast
overwrites it). Both verified to fail without their respective fix.
Re-review ordering-mismatch fix. The preflight computed effective force as
`req.flags?.force || preRunSession.saveScriptForce`, accepting persisted force
regardless of whether THIS request retargets. So a `--from` continuation with
explicit `--save-script=b.ad` (no live force) on a session forced for a.ad
would PASS the preflight, run every step, then have `applySaveScriptRetarget`
clear the force for b.ad, and finally refuse the existing b.ad at publish time
— defeating the arm-time-preflight point and mutating the session mid-run.

Now the effective-force decision is computed inside `preflightSaveScriptTarget`
against the target THIS request resolves to (the same one the armer will set),
matching `applySaveScriptRetarget`'s per-target contract: live force always
bypasses; persisted force bypasses ONLY when `targetPath === existingSaveScriptPath`
(same target). A retarget to a different path without live force is now refused
BEFORE any step dispatches. The preflight stays read-only (runs before the
armer; never mutates the session).

Regression: a --from continuation retargeting to an existing b.ad without live
force fails at the preflight, dispatches zero steps, and leaves saveScriptPath/
saveScriptForce unchanged. Verified it fails (leg passes preflight, runs) when
the fix is reverted. Same-target continuation and live-force retarget stay
correct.
@thymikee thymikee force-pushed the feat/save-script-force-and-arm-preflight branch from fe87050 to 20e0542 Compare July 14, 2026 19:50
@thymikee

Copy link
Copy Markdown
Member Author

Rebased onto origin/main (now includes #1265, 6efe54451) — conflict-free base-move (zero file overlap, as expected); tsc/oxlint/oxfmt clean on all 12 changed files. New head: 20e054259f9b74270e34c045e62c1bd76b29d698 (origin/main confirmed as ancestor).

@thymikee

Copy link
Copy Markdown
Member Author

Independent re-review of exact head 20e054259 found one additional client-contract gap.

The PR adds saveScript/force inputs to both typed close APIs, but sessions.close() and apps.close() still normalize away the daemon savedScript response. The daemon returns the committed artifact path, yet neither SessionCloseResult nor AppCloseResult exposes it, so Node clients can request publication but cannot learn where it landed.

Please add savedScript?: string to both result types, project it with readOptionalString(data, "savedScript") in both close normalizers, and cover the public client results.

The previously reported P1 also remains on this rebased head: saveScriptComplete is reset before the EEXIST preflight, and the retarget rejection regression still does not assert completion survives or that a later close commits the original target. CI is green, but this head is not merge-ready.

…et reject

Two re-review blockers.

BLOCKER 1 (client-contract gap): the typed close APIs accept --save-script/
--force inputs but dropped the daemon's `savedScript` response, so a Node
client could request publication but not learn where the file landed. Add
`savedScript?: string` to SessionCloseResult and AppCloseResult, and project it
via readOptionalString in both close normalizers (sessions.close, apps.close).
New public-client coverage asserts it round-trips (and is absent when the
daemon published nothing).

BLOCKER 2 (P1 ordering): the C2 `saveScriptComplete = false` reset ran BEFORE
the EEXIST preflight's early-return, so a retarget REJECTION corrupted a prior
COMPLETE transaction — a later close would then refuse to commit the original
target. Move the reset to AFTER `if (saveScriptPreflight) return ...` (verified
nothing between the two positions reads saveScriptComplete; applySaveScriptRetarget
already runs later, so the original saveScriptPath is preserved). The reset is
correct only when the run proceeds to re-arm.

Strengthened the retarget-rejection regression to assert (a) saveScriptComplete
survives the rejection and (b) a later bare close commits the ORIGINAL target,
not the rejected new one. Both verified to fail without their respective fix.
@thymikee

Copy link
Copy Markdown
Member Author

Both re-review blockers fixed (b4d0b1765).

BLOCKER 1 — close savedScript now reaches clients.

  • Added savedScript?: string to both SessionCloseResult and AppCloseResult in client-types.ts.
  • Projected it via readOptionalString(data, 'savedScript') in BOTH close normalizers in agent-device-client.ts (sessions.close() and apps.close()).
  • Public-client coverage in client.test.ts: one test asserts the committed path round-trips through both sessions.close({ saveScript: true }) and apps.close({ saveScript: true }) when the daemon returns it; a second asserts it's undefined when the daemon published nothing. Confirmed the round-trip test fails (savedScript === undefined) when the projection is removed.

BLOCKER 2 — P1 ordering: retarget rejection no longer corrupts a COMPLETE transaction.

  • Moved the C2 if (preRunSession?.saveScriptBoundary !== undefined) preRunSession.saveScriptComplete = false; reset to AFTER if (saveScriptPreflight) return saveScriptPreflight; (was before it, ~line 253). A preflight REJECTION now returns before the reset, so a prior COMPLETE transaction stays committable at its original target.
  • Verified nothing between the old and new position reads saveScriptComplete: the only code there is preflightSaveScriptTarget, which reads saveScript/liveForce/persistedForce/sourcePath/existingSaveScriptPath and computes existence — never saveScriptComplete. applySaveScriptRetarget already runs later (in the armer), so saveScriptPath is untouched on rejection. The reset is the sole corruption, and it's correct only when the run proceeds to re-arm.

Retarget-rejection regression strengthened — now explicitly asserts BOTH (confirmed to fail without the fix):

  • (a) completion-survival: the transaction is COMPLETE for <a> before the rejected leg; after the retarget rejection, saveScriptComplete is still true.
  • (b) original-target commit: a later bare close commits the ORIGINAL <a>closeResponse.data.savedScript === <a>, the file exists with the healed [open, press, close] actions, and the rejected <b> stays byte-for-byte untouched throughout.

Verification: npx tsc --noEmit, npx oxlint --deny-warnings, npx oxfmt --check clean; pnpm check:fallow --base origin/main✓ No issues in 14 changed files (green, no waiver); FULL daemon-handlers + client + session-script-writer suites: 862/862 pass (74 files).

New head: b4d0b1765735a2de4bee638a112759c4df3feceb (origin/main confirmed as ancestor).

@thymikee

Copy link
Copy Markdown
Member Author

Re-review of exact head b4d0b1765: both blockers are resolved. The EEXIST rejection now returns before the completion reset, and the strengthened regression proves the previous ordering breaks completion survival and original-target commit. Both typed close clients now expose savedScript, with public-client regressions that fail when either projection is removed. No new findings in the follow-up surface; all 22 checks pass. Ready for human merge judgment.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Jul 15, 2026
@thymikee thymikee merged commit 95c1e20 into main Jul 15, 2026
33 of 35 checks passed
@thymikee thymikee deleted the feat/save-script-force-and-arm-preflight branch July 15, 2026 08:16
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-15 08:17 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add --force/--overwrite flag for --save-script now that publish refuses any existing target

1 participant