You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
F: wire the runner and startup recovery into the server (#91, part 1) - #102
An optional runner block in review.json turns on the production runner. Without the block, and in every demo, startup is unchanged, and runner actions say the runner is not configured.
With the block, startServer runs setUpRunner (runner/production.ts) after the Store opens and before the server listens. It:
Refuses to start without a github block (the prompt carries the issue) or CLAUDE_CODE_OAUTH_TOKEN.
Calls lock.verify(), then reads the database's runner token for the locked file.
Runs recoverStartup with D's own recoverLeftovers, exportTaskDiff and removeTaskFilesystemsAsync. Every unowned Docker object blocks startup, as runner-lifecycle.md requires. The message lists the exact docker … rm lines.
Builds the agent image after D's recovery has stopped leftover agents, and before any export's deadline starts.
Assembles the execution deps: the real TaskWorkspace, a Claude launcher, and the issue text with collaborators' comments only (GhIssueGateway.issueText).
Any failure closes the Store, releases the lock, and prints only the message.
Other changes:
Schema v9. Each allocation's metadata_baseline and storage_base are saved as soon as D's allocation returns, so recovery can export a recovered storage. A handle is used only if its attempt ID and allocation ID both match a row. Diffs go through saveDiagnostic; retention keeps every diff the same recovery saved, and a cut diff says so.
RunnerDeps.kinds: admission refuses kinds the deps cannot run (execute only).
ItemExecutor.close(): shutdown awaits plan runs before the Store closes.
Every shutdown step runs even when an earlier one fails.
Resuming a task that stopped between items comes in part 2.
/api/runner. It refuses to retry a plan item outside ItemExecutor; its view offers retry only when admission would take it.
CLI:
--release-preparation <attempt ID>, which a recovery message points to.
"Still stopping agents…" on a second Ctrl+C during shutdown.
Ctrl+C during startup: the first waits for startup to finish; the second stops at once.
Refusals are printed as messages, not stack traces.
ReviewService.planContext reads a base commit's tree once, and the runner reuses the issue text across a run's items for up to five minutes, so items do not run git or gh again on the server thread.
Docs: runner-lifecycle.md ("Production wiring (#91)", shutdown step 5a, unowned-resource messages) and the README section "Runner (opt-in)".
Not in this PR (part 2): /api/runnerstart and resume, including a task stopped between items, and the screen's buttons.
Validation
npm run typecheck: clean.
vitest without the six real-Docker files: 55 files, 1,264 tests, all passing, on the merge with main at dd2fdac (head 55d3d5c); 1,292 tests at head 15db8db after the later rounds.
Real-Docker files not run locally: agent-container, agent-gate, agent-network, agent-question, agent-supervisor and runner-workspace. The Docker CLI hangs in the authoring shell. CI's real-docker job is their first run against this branch; runner-workspace covers the new baseline save. Watch it to the end before merging, because that job is not required.
Review rounds
Independent review agents (fresh context, contract checklist), then /code-review high in a loop until a round found nothing new.
Round
Found
Outcome
Agent 1
The schema was unreadable under umask 077; recovery retention deleted diffs from the same pass; tests missing (baseline save, D adapters, close order); retry bypassed the executor; unowned lines could not be pasted; issue-text limits; image built before leftover agents were stopped; shutdown skipped steps after a failure; CLI stack traces
Issue limits exceeded the prompt's 32 KiB data budget; inherited names were accepted as storage limits; README overstated the diagnostics cap; Ctrl+C during startup was untested
The diagnostics directory was shared across databases, so retention could delete another database's diffs; --release-preparation without --config printed help and exited 0; a demo config opened with --config ran its runner block
A git merge with no conflict can still break a changed contract (the tree check). Covered by AGENTS.md: "Treat every behavioural claim… as something to verify."
Two bounds for the same payload that disagree (issue limits and prompt budget). Covered by AGENTS.md: "Align subprocess output limits with every payload the schema accepts…"
in matching inherited keys. One-off: an own-key check plus a test.
State shared across databases when the lock is per database (diagnostics). One-off: diagnostics are namespaced by the runner token.
A decision read from a CLI flag instead of the loaded config (demo). One-off: a child-process test.
An untested signal lifecycle. Covered by AGENTS.md "Required race regressions" (shutdown interleavings).
Copilot 3 (review summary, "previously missed", no inline threads). All are real and low severity, so they are deferred to #105, which builds on this branch and is still open:
An opt-in `runner` block in review.json turns on the production runner. After
the Store opens and before the server listens, setUpRunner verifies the lock,
reads the database's runner token, builds the agent image and runs startup
recovery with D's own recoverLeftovers, exportTaskDiff and removal. It then
assembles the execution deps over the real workspace and a Claude launcher.
- Schema v9 saves each allocation's metadataBaseline and seeded commit, so
recovery can export a recovered storage. Handles must match the row's
attempt and allocation IDs. Diffs go through saveDiagnostic.
- RunnerDeps.kinds: admission refuses attempt kinds the deps cannot run.
- ItemExecutor.close(): shutdown awaits plan runs in progress before the
Store closes.
- GhIssueGateway.issueText: the issue's title, body and collaborators'
comments, bounded, for the execute prompt.
- CLI: --release-preparation; "Still stopping agents…" on a second Ctrl+C;
refused runner startups print their message and exit 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The execute schema is chmod 0444 after writing: a restrictive umask
made it unreadable to the container user, failing every launch.
- Recovery's retention keeps every diff the same recovery saved; before,
the second save could delete the first before finalization referenced it.
- The agent image is built after D's recovery stopped leftover agents.
- Unowned objects are listed one shell line each, the reason as a comment.
- Issue text drops other people's comments before checking their body, and
exactly 1000 comments no longer fails.
- /api/runner refuses to retry an execute attempt outside ItemExecutor.
- A failing shutdown step no longer skips stopping the runner or closing
the Store; --release-preparation prints its error as a message.
- Tests: materialize saves the baseline; D adapter arguments; server close
order with the executor; retry refusal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Build the agent image as soon as D's recovery returns storage, before
recoverStartup arms the first export deadline: the build blocks, so a
cold build used up that deadline and lost the partial output.
- Shutdown runs every step even when one fails, awaiting plan runs before
the Store closes, and reports the first failure, not a later one.
- /api/runner's view no longer offers retry for an execute attempt that
the action refuses.
- CLI refusals before the lock (bad port, config, runner block) print
their message instead of a stack.
- Unowned object names are shell-quoted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d take it (#91)
- retryable also requires the deps to run the attempt's kind and no
unconfirmed cleanup (runner.unreleased), as admission does.
- Later shutdown failures are logged, not dropped behind the first.
- dRecoveryDeps keeps its doc comment; startup order wording fixed in
setUpRunner's comment and the README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ReviewService.planContext reads a base commit's tree once: each runner
item no longer runs blocking git on the server's thread.
- The runner reuses the issue text across a run's items for up to five
minutes instead of rereading every collaborator and comment page.
- A partial diff D cut at its limit ends with a codeboost: notice line.
- A Ctrl+C during startup takes effect after startup recovery finishes.
- removalCommand is shared by the runner's and Ask's leftover messages;
RecoveryBlocked lists its items one per line.
- CLI tests run web/cli.ts: runner refusal, bad port, release-preparation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The issue reuse window runs on the monotonic clock.
- A second Ctrl+C during startup stops at once (recovery reruns at the
next start); the first says so, and that a running image build stops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Demos never run the runner, so telling a demo to add a runner block
was advice that changed nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves the conflicts with #100 (gitlink mounts and the pre-launch tree
check) and passes the tree check through claudeLauncher: D requires it
for an execute start, so without it every production launch would fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ma with D's reader (#91)
A comma or line break in runner.root or diagnosticsDir passed the config
and then failed every launch in D's mount check; it is refused at config
time now. The launcher test also reads schema.json through D's own
readCapturedFile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- issueText checks the issue against the execute prompt's own budget and
serializer (dataJSON, 32 KiB), so an issue it accepts is one a prompt can
carry; a larger one is refused at the fetch with the reason.
- Storage limits must be own keys: inherited names such as toString are
refused like any unknown limit.
- README: the diagnostics cap is a retention target, not a hard limit.
- CLI tests drive Ctrl+C during startup in a child process held by a fake
docker: the first waits and exits 0 after startup, the second exits 130.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Diagnostics live per database, in <diagnosticsDir or root>/<runnerOwner>/
diagnostics: retention reads references from one Store only, so runners
of databases sharing a root never delete each other's diffs.
- --release-preparation without --config reports that it needs one,
instead of printing help and exiting 0.
- A demo configuration ignores its runner block however it is opened
(config.demo, not the --demo flag).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the title and body alone already exceed the prompt budget, this running total is not checked until a collaborator comment is encountered. With no retained comments, the method fetches every comment page before the final serializer rejects; in production those unnecessary calls can instead hit the overall timeout. Reject immediately once the initial total is over the limit.
Quote untrusted filesystem names in CLI diagnostics
web/cli.ts:50
unknownEntries contains filesystem names and Linux names may include newlines or terminal control characters. Interpolating the path directly lets an untrusted entry forge additional CLI output. Quote it as structured text, consistent with other agent-controlled paths and diagnostics.
Route lock verification failures through message-only handling
web/cli.ts:68
The configured-runner message-only handling covers only rejections from startServer. The later lock.verify() can still fail if the database path changes during recovery/startup; its catch rethrows, so this configured runner failure prints a stack despite the PR's refusal contract. Route that catch through the same message-only path after closing the app and releasing the lock.
main's tree equals this branch's part 1 commit 15db8db, which the branch
already contains, so every conflict resolves to this branch's side; the
merged tree is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- issueText refuses a title and body already over the prompt budget
before reading any collaborator or comment page.
- Recovery warnings (recoveryWarnings) quote Docker labels and file
names, so a newline or control character cannot forge a line of output.
- A lock.verify failure after startup prints its message and exits 1,
not a stack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…105)
* F: wire the runner and startup recovery into the server (#91, part 1)
An opt-in `runner` block in review.json turns on the production runner. After
the Store opens and before the server listens, setUpRunner verifies the lock,
reads the database's runner token, builds the agent image and runs startup
recovery with D's own recoverLeftovers, exportTaskDiff and removal. It then
assembles the execution deps over the real workspace and a Claude launcher.
- Schema v9 saves each allocation's metadataBaseline and seeded commit, so
recovery can export a recovered storage. Handles must match the row's
attempt and allocation IDs. Diffs go through saveDiagnostic.
- RunnerDeps.kinds: admission refuses attempt kinds the deps cannot run.
- ItemExecutor.close(): shutdown awaits plan runs in progress before the
Store closes.
- GhIssueGateway.issueText: the issue's title, body and collaborators'
comments, bounded, for the execute prompt.
- CLI: --release-preparation; "Still stopping agents…" on a second Ctrl+C;
refused runner startups print their message and exit 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Close the gaps review round 1 found in the runner wiring (#91)
- The execute schema is chmod 0444 after writing: a restrictive umask
made it unreadable to the container user, failing every launch.
- Recovery's retention keeps every diff the same recovery saved; before,
the second save could delete the first before finalization referenced it.
- The agent image is built after D's recovery stopped leftover agents.
- Unowned objects are listed one shell line each, the reason as a comment.
- Issue text drops other people's comments before checking their body, and
exactly 1000 comments no longer fails.
- /api/runner refuses to retry an execute attempt outside ItemExecutor.
- A failing shutdown step no longer skips stopping the runner or closing
the Store; --release-preparation prints its error as a message.
- Tests: materialize saves the baseline; D adapter arguments; server close
order with the executor; retry refusal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Close the gaps review round 2 found in the runner wiring (#91)
- Build the agent image as soon as D's recovery returns storage, before
recoverStartup arms the first export deadline: the build blocks, so a
cold build used up that deadline and lost the partial output.
- Shutdown runs every step even when one fails, awaiting plan runs before
the Store closes, and reports the first failure, not a later one.
- /api/runner's view no longer offers retry for an execute attempt that
the action refuses.
- CLI refusals before the lock (bad port, config, runner block) print
their message instead of a stack.
- Unowned object names are shell-quoted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review round 3: the runner view offers retry only when admission would take it (#91)
- retryable also requires the deps to run the attempt's kind and no
unconfirmed cleanup (runner.unreleased), as admission does.
- Later shutdown failures are logged, not dropped behind the first.
- dRecoveryDeps keeps its doc comment; startup order wording fixed in
setUpRunner's comment and the README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Code review (high) round 1 fixes for the runner wiring (#91)
- ReviewService.planContext reads a base commit's tree once: each runner
item no longer runs blocking git on the server's thread.
- The runner reuses the issue text across a run's items for up to five
minutes instead of rereading every collaborator and comment page.
- A partial diff D cut at its limit ends with a codeboost: notice line.
- A Ctrl+C during startup takes effect after startup recovery finishes.
- removalCommand is shared by the runner's and Ask's leftover messages;
RecoveryBlocked lists its items one per line.
- CLI tests run web/cli.ts: runner refusal, bad port, release-preparation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Code review (high) round 2 fixes for the runner wiring (#91)
- The issue reuse window runs on the monotonic clock.
- A second Ctrl+C during startup stops at once (recovery reruns at the
next start); the first says so, and that a running image build stops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Code review (high) round 3: demos get their own runner refusal (#91)
Demos never run the runner, so telling a demo to add a runner block
was advice that changed nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: start and resume a task's plan through /api/runner (#91, part 2)
- start runs a task that is in review or queued and has not begun at its
plan revision; an in-review task moves to queued in the same action.
- resume continues a begun or requeued task from its first unfinished
item (completed, failed or stopped last item alike) and claims the
requeue in the admitting transaction, which also covers a task stopped
between items.
- ItemExecutor.begin admits the first item synchronously inside the user
action, throwing the refusal so the action's own writes roll back;
runTask keeps reporting it as not started.
- GET /api/runner adds startable and resumable from the same checks.
- Retrying an execute attempt points to resume.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Close the gaps review round 1 found in start and resume (#91)
- A plan revised after it started running is refused by both actions,
with #88 named, instead of each pointing at the other.
- An owed safety finding settles only once the escalation commits: a
user action rolling back after begin keeps it owed.
- runChoice also refuses a closed task first, a runner that cannot run
execute attempts, an active merge and a spent task budget, so the
view's startable and resumable match the actions.
- Tests: the settled-finding outcome, budget and revision refusals, replay
and stale version, shutdown, begin under a rolled-back action; the
between-items setup saves result as production does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Close the gaps review round 2 found in start and resume (#91)
- A spent budget is refused by admission again, not by runChoice, so the
refusal's effect still moves the idle task to needs human; the view
alone checks the budget.
- The #88 refusal applies only when an earlier revision's items made
runner commits; a revised plan whose earlier attempts committed nothing
resumes from its first item.
- The view and runChoice see a run still finishing in ItemExecutor, and
the view no longer hides a storage error as "not offered".
- Tests: the shutdown case now reaches the action (rejectAdmission with the
server open); budget asserts the needs-human move; the settled finding
is no longer owed after the commit; both revision cases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review round 3: one start/resume rule in code and docs (#91)
- start needs no attempt at the current plan revision; resume needs one
at any revision (or a requeue). An in-review task whose only attempts
were at an earlier, uncommitted revision can start again.
- The doc and runChoice comment state that rule.
- A strict begin throws on a revision change instead of reporting a
settled run.
- Tests: an earlier item completed unchanged is no commit and reruns at
the new revision; a 503 start is not recorded and its action ID is
refused afresh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review round 4: tests for the start rule, doc precision (#91)
- Tests: an in-review task whose only attempts were at an earlier,
uncommitted revision can start; a queued one is offered both actions,
which run the same item; the unchanged-item test checks the revision.
- Doc: start also refuses a pending requeue; a spent budget moves only a
running or queued task to needs human.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Status polls read run progress with one targeted query (#91)
Copilot review on #105: progress() loaded and decoded every attempt
(results up to 1 MiB) twice per /api/runner poll. Store.executeProgress
now aggregates only the execute attempts' item, state, plan revision and
unchanged flag in SQL, and the view computes it once for both flags.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Code review: refuse a runner root Docker cannot mount; check the schema with D's reader (#91)
A comma or line break in runner.root or diagnosticsDir passed the config
and then failed every launch in D's mount check; it is refused at config
time now. The launcher test also reads schema.json through D's own
readCapturedFile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address Copilot review on #102 (#91)
- issueText checks the issue against the execute prompt's own budget and
serializer (dataJSON, 32 KiB), so an issue it accepts is one a prompt can
carry; a larger one is refused at the fetch with the reason.
- Storage limits must be own keys: inherited names such as toString are
refused like any unknown limit.
- README: the diagnostics cap is a retention target, not a hard limit.
- CLI tests drive Ctrl+C during startup in a child process held by a fake
docker: the first waits and exits 0 after startup, the second exits 130.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address Copilot review round 2 on #102 (#91)
- Diagnostics live per database, in <diagnosticsDir or root>/<runnerOwner>/
diagnostics: retention reads references from one Store only, so runners
of databases sharing a root never delete each other's diffs.
- --release-preparation without --config reports that it needs one,
instead of printing help and exiting 0.
- A demo configuration ignores its runner block however it is opened
(config.demo, not the --demo flag).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Code review round 1 on #105: count every writable kind's earlier commit; clearer start refusal; tests that fail when broken (#91)
- earlierCommits counts fix and rebase-fix commits too, as hasRunnerCommit does, and a result that is not valid JSON counts as a commit (fail closed).
- start names the task's status, not resume, for a task resume cannot run either.
- The rollback test now reaches the move to queued (admission refuses a spent budget) and replays the recorded refusal.
- New tests: begin's in-flight guard between items, an owed scope pause on resume, an owed finding on start from in review, a fix attempt's earlier commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Code review round 2 on #105: point a running task's start to resume; tests for the queue move and paying nothing at shutdown (#91)
- start points to resume for a running task that ran only an earlier revision, where resume runs.
- Tests: start from in review queues before it escalates an owed finding; begin pays nothing owed once admission closed.
- Docs: GET /api/runner's field list; when the view withholds start and resume. Comment on how earlierCommits differs from hasRunnerCommit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Code review round 3 on #105: 503 before any start/resume refusal at shutdown; refusals point only to an action that runs (#91)
- During shutdown, start and resume answer 503 before any refusal, so nothing is recorded under the action ID (test: a body that finishes arriving after shutdown began).
- A run still finishing between items is refused as such, not as an active attempt.
- Start no longer points a fully run task to resume; resume names the status of a task that is neither running, queued nor in review.
- Docs: owed work is acted on before admission refuses a spent budget; the full refusal list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Code review round 4 on #105: test the view between items; refusal messages name an action that runs (#91)
- Test: while a run is still finishing between items, the view offers neither action and resume is refused.
- "Still finishing" says try again, not start; resume points to start only where start's checks pass; start names the task's status for a run revision.
- Docs: resume is no longer a later part. Test title matches what the 503 test checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Code review round 5 on #105: wording and docs (#91)
- start states a pending requeue as the reason when nothing ran at this revision.
- Docs list start and resume among user actions; the in-flight comment covers begin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix the three Copilot findings deferred from #102 (#91)
- issueText refuses a title and body already over the prompt budget
before reading any collaborator or comment page.
- Recovery warnings (recoveryWarnings) quote Docker labels and file
names, so a newline or control character cannot forge a line of output.
- A lock.verify failure after startup prints its message and exits 1,
not a stack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address Copilot review on #105: serializer pre-check, terminal quoting (#91)
- issueText runs the prompt's own serializer on the title and body before
reading any collaborator or comment page, so text that only exceeds the
budget once escaped is refused just as early.
- quoteForTerminal (lifecycle.ts) escapes what JSON quoting leaves literal
and a terminal still acts on: C1 controls, U+2028/U+2029, and invisible
format and bidi characters. Recovery warnings use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part 1 of #91, built to the decisions posted on #91: #91 (comment)
What this does
An optional
runnerblock inreview.jsonturns on the production runner. Without the block, and in every demo, startup is unchanged, and runner actions say the runner is not configured.With the block,
startServerrunssetUpRunner(runner/production.ts) after the Store opens and before the server listens. It:githubblock (the prompt carries the issue) orCLAUDE_CODE_OAUTH_TOKEN.lock.verify(), then reads the database's runner token for the locked file.recoverStartupwith D's ownrecoverLeftovers,exportTaskDiffandremoveTaskFilesystemsAsync. Every unowned Docker object blocks startup, as runner-lifecycle.md requires. The message lists the exactdocker … rmlines.TaskWorkspace, a Claude launcher, and the issue text with collaborators' comments only (GhIssueGateway.issueText).Any failure closes the Store, releases the lock, and prints only the message.
Other changes:
metadata_baselineandstorage_baseare saved as soon as D's allocation returns, so recovery can export a recovered storage. A handle is used only if its attempt ID and allocation ID both match a row. Diffs go throughsaveDiagnostic; retention keeps every diff the same recovery saved, and a cut diff says so.RunnerDeps.kinds: admission refuses kinds the deps cannot run (execute only).ItemExecutor.close(): shutdown awaits plan runs before the Store closes./api/runner. It refuses to retry a plan item outsideItemExecutor; its view offers retry only when admission would take it.--release-preparation <attempt ID>, which a recovery message points to.ReviewService.planContextreads a base commit's tree once, and the runner reuses the issue text across a run's items for up to five minutes, so items do not run git orghagain on the server thread.Docs: runner-lifecycle.md ("Production wiring (#91)", shutdown step 5a, unowned-resource messages) and the README section "Runner (opt-in)".
Not in this PR (part 2):
/api/runnerstartandresume, including a task stopped between items, and the screen's buttons.Validation
npm run typecheck: clean.vitestwithout the six real-Docker files: 55 files, 1,264 tests, all passing, on the merge withmainat dd2fdac (head 55d3d5c); 1,292 tests at head 15db8db after the later rounds.agent-container,agent-gate,agent-network,agent-question,agent-supervisorandrunner-workspace. The Docker CLI hangs in the authoring shell. CI's real-docker job is their first run against this branch;runner-workspacecovers the new baseline save. Watch it to the end before merging, because that job is not required.Review rounds
Independent review agents (fresh context, contract checklist), then
/code-review highin a loop until a round found nothing new.retryableoffered a refused retry; CLI stack traces; quotingkindsandunreleased; later shutdown failures were dropped; doc wording/code-review high1docker rmbuilder; awkwardRecoveryBlockedseparator; CLI untested/code-review high2/code-review high3/code-review high4Declined, each fails closed:
prepareExecutionrefuses a mismatch.Review lessons (merge gate)
buildAgentImagebeing synchronous; a test pins the order.Later rounds (after the PR opened)
main(#100, #101)/code-review high5--release-preparationwithout--configprinted help and exited 0; a demo config opened with--configran its runner blockReview lessons from these rounds:
inmatching inherited keys. One-off: an own-key check plus a test.Copilot 3 (review summary, "previously missed", no inline threads). All are real and low severity, so they are deferred to #105, which builds on this branch and is still open:
unknownEntriespaths reach CLI output unquoted, so a name containing a newline can forge a line. Fix in F: start and resume a task's plan through /api/runner (#91, part 2) #105: quote it as other agent-controlled paths are (AGENTS.md).lock.verify()failure after startup prints a stack trace instead of a message. Fix in F: start and resume a task's plan through /api/runner (#91, part 2) #105: send it through the same message-only exit.🤖 Generated with Claude Code