Proposal (rationale)
F-058 (the user’s finding, 2026-09-06): add and init write module files one by one with direct
writeFileSync, skip any file that already exists, and save the install record only after every
module, gate and hook phase. An interruption — a killed process, a full disk, a path that turns out
unwritable — leaves the files written so far with no record. A retry then sees them as
“already present — left alone”, classifies them as kept, and never hashes them: the module’s own
files become user-owned forever, upgrade skips them, doctor calls them yours. A process killed
mid-write can also leave a truncated file that the retry keeps. Nothing exposes the interrupted state,
and nothing tests any of it.
Decision
accepted — 2026-09-06, at the user’s request. Recoverable, not transactional: full rollback of
shared files somebody else may have edited between the interruption and the retry is a promise this
tool cannot keep. The promise it can keep is that an interrupted install is visible, completable
without losing ownership, and never leaves a half-written file behind.
Plan
Requirements
- Before the first write, the install writes a journal under
.ai/naming the run’s modules, harnesses, start stamp, and every planned file emission with whether it pre-existed.--dry-runwrites no journal. - Every module file is written atomically: to a sibling temporary name, then renamed into place.
- The journal is removed after the install record is written and the renderings are applied; its absence means “no install in flight”.
- A retry of the same modules with a journal present treats files the journal marks as created by the interrupted run as ours — rewritten and hashed — and files it marks as pre-existing as kept. A different module set with a journal present is refused with a message naming the interrupted run and the command that completes it.
doctorreports an interrupted install read-only: start stamp, modules, files written and not yet recorded, and the completing command.
Impacts
src/add.ts(addModulewrite path, journal helpers),src/cli.ts(cmdAddphases,cmdDoctor),test/core.test.js, the packed journey’sgit statusassertions (must stay clean: the journal is gone by then).
Approach
Journal + atomic rename + retry-aware classification. Alternatives: (a) write everything to a staging
directory and move at the end — rejected, a directory move is not atomic on Windows and the shared
fragment files (AGENTS.md, .ai/gates.toml) are merged, not moved; (b) full rollback on failure —
rejected, see Decision; (c) hash every existing file as ours on retry — rejected, that is exactly how
a user’s pre-existing file would be silently reclassified (the WI-093 / F-017 failure).
Acceptance criteria / tests
- A core test makes a later emission target unwritable (a directory where a file belongs) so
initfails after earlier files were written, then asserts: journal present, no install record, the earlier files present, no temporary files left;doctornames the interrupted install and the completing command; after removing the obstacle, the sameinitcompletes, the record hashes the earlier files as ours (not kept), the journal is gone, anddoctorreports every module ours. - The same test asserts a different module set is refused while the journal exists, and that
--dry-runwrites no journal. - Serial suite, packed journey and
rungs checkpass.
Out of scope
- Rolling back shared-file merges or gate registrations; nothing else deferred.
Execution
Branch feature/WI-098-recoverable-install from 33390651, 2026-09-06. Two choices the plan left
open, recorded here:
- Fault injection over obstacle files. The plan proposed making a later target unwritable. The
path preflight refuses such a tree before the first write — which is correct, and means it cannot
produce a partial install. The regression instead sets
RUNGS_FAULT_INJECT_WRITE=<target>, a documented test-only environment variable that makes the write of that one file throw; nothing reads it otherwise. - The journal is JSON, not TOML. It is machine-written and machine-read, transient, and never hand-edited; the install record stays TOML because people read it.
The journal lives at .ai/rungs-install.journal.json; --dry-run never writes it; doctor reads it
read-only; add refuses a different module set while it exists and resumes the same set.
Review
node --test --test-name-pattern '^an interrupted install is journaled' test/core.test.js: 1/1 (2026-09-06). A trackedinitinterrupted ondocs/backlog/README.mdexits 1 naming the module and the fault; the journal exists with.ai/rungs.mjsand the failed file both markedcreate; no record; the earlier files present; no.rungs-tmpanywhere.doctorprints “Interrupted install — started 2026-09-06: instructions, gates, backlog…” with the completing command.add ciis refused naming the four modules it does not include and writes nothing. A--dry-runinit on a fresh directory writes no journal. The retriedinitprints “resuming the install interrupted on 2026-09-06”, exits 0, clears the journal, and the record hashes.ai/rungs.mjsanddocs/backlog/README.mdas ours with neither in a kept list;doctorthen reports every module ours.npm test: 158 tests, 155 pass, 0 fail, 3 skipped, 281 s (the packed journey’sgit statusassertions hold: the journal is gone before they run).node src/cli.ts check: 32 pass.- Not proven: a real process kill mid-write. The atomic write is a rename, whose guarantee is the filesystem’s, and the fault injection exercises the interruption path around it rather than the kill itself.