Skip to content

F1b: runner coordinator for attempts, slots and shutdown - #74

Merged
mchwang merged 11 commits into
mainfrom
feat/f1b-coordinator
Sep 29, 2026
Merged

mchwang merged 11 commits into
mainfrom
feat/f1b-coordinator

Conversation

@mchwang

@mchwang mchwang commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Lane F, step F1, slice F1b: runner coordinator. Related: #22, #51.

This replaces #56. GitHub closed #56 when #53 (F1a) merged and deleted #56's base branch, and a PR can't be reopened once its base is gone. This branch is #56 rebased onto main. The review history is on #56.

Changes since #56

  • Rebased onto main. The unsquashed F1a commit and the contract round 35 commit were dropped, because F1a: persist the runner lifecycle in the Store #53's squash already contains both. The F1b commit applied without conflicts.
  • New commit: pass D's runnerOwner through the coordinator. Lane D (D: label every resource with its runner, attempt and allocation (#51 item 3) #71) made InvocationInput.runnerOwner required, and D refuses task storage that belongs to another runner. RunnerDeps now has a runnerOwner, and the coordinator passes it to captureInvocation. The injected D prepares storage and starts invocations under that same token. F1d replaces the source with the per-database runner token.
  • New commit: review fixes (26d9678).
    • Unreleased resources. When D settles with unreleased, the runner refuses all new work until restart, as Ask does. The resources are kept in memory and exposed as unreleased. They are not yet saved to the database: F1d adds that together with startup recovery.
    • No stranded handle. Before, if reading the attempt failed after a refused markRunning, the running D handle was dropped. Now the coordinator still cancels the handle and waits for it to settle.
    • No stranded job. Before, if reading the task budget failed right after admission, the job stayed in memory with no work and held its slot. Now the timers are set up inside the job, and a failed read ends the attempt as failed.
  • New commits: review rounds 3–9 (7491e6b … db1d746).
    • Preparation files after launch. They are removed once D has settled and the terminal write succeeds. A failed terminal write leaves them for startup recovery.
    • Stale cause. stop(..., 'stale', cause) keeps the cause. The agent's stderr is never recorded as the reason an attempt went stale.
    • Preparation timeout. It ends failed "Timed out while preparing.", as the spec says. A later shutdown, time limit or stop can't replace it.
    • Launch check.
      • A context change is tested before the time checks, so it ends stale.
      • An attempt admitted before D reported unreleased is not started.
    • Fixed outcomes. Once a job is ending before launch, later stops are refused, so "stale, then shutdown" stays stale and isn't requeued. The same holds after the terminal write.
    • Cancel task and unsaved reasons.
      • An earlier stop reason that failed to save is saved before the Store writes cancelled. This runs only when the caller's state version is current.
      • A cancel the Store recorded on a job that no longer takes stops is shown in status() only.
      • The Store's rule stands: a pending cancel task wins.
    • Calls inside userAction. Retry and cancel are meant to run inside userAction.
      • A job starts its work only after the caller's transaction ends. If admission rolled back, it gives back the slot and prepares nothing.
      • A reason write that rolled back shows as unsaved.
      • A rolled-back cancel task never changes the outcome.
    • Fail-closed marker. It is set before the job is removed, so the slot is never free in between.
  • New commit: Copilot review fixes (4b69ea3).
    • Only this attempt's result. A settled result whose attempt ID or captured context doesn't match is never validated or saved. The attempt fails closed, as the question path does.
    • Preparation files that can't be removed. If removing them fails, before launch or after the terminal write, the slot stays held under a preparation-not-removed marker until restart, and admission for the task says why.
    • Deferred to F1: apply a stop only after the caller's transaction commits #79. A stop inside a userAction transaction that rolls back still cancels D. Fixing it needs a Store post-commit hook.

What this does

Adds runner/coordinator.ts, the per-process owner of in-memory jobs, slots and unresolved markers, as specified in docs/implementation/runner-lifecycle.md. D is injected (RunnerDeps.start), so this runs against a fake D until #51 lands; nothing is wired to the server yet.

  • Admission in two steps. In one synchronous turn, the coordinator checks it is open, that the task has no job or marker, and that a slot is free, then reserves the slot. Then Store.admitAttempt runs. A refused transaction releases the reservation in the same turn. Writable kinds share one slot and read-only kinds share another; both limits are configurable.
  • Preparation. Host-side preparation gets an AbortSignal. The task budget timer records time-limit. The attempt deadline, before launch, fails the attempt with "Timed out" and records no first reason.
  • Launch check. In one synchronous turn: the row is still pending, there is no first reason (saved or in memory), there is time left and the context is current. Then captureInvocation, then D's start. A context change during preparation ends stale without calling D. A stop during launch ends from its first reason. A start error ends failed with the launch error.
  • Running. If markRunning is refused because a first reason exists, the handle is cancelled and settles normally. If the write fails with a storage error, the handle is cancelled with capture-failure, settled, and the slot stays held under a start-not-saved marker.
  • Stops. The first reason wins. It is kept in memory and saved when possible; status().stopRequested.saved shows when saving failed. The slot is never freed on cancel, only after settled.
  • Settlement. A clean result is validated, then the Store chooses the terminal state from the first reason, D's result and context currency. A failed terminal write leaves a result-not-saved marker that holds the slot until restart.
  • Cancel task and shutdown. cancelTask goes through the Store and stops the running work. close() rejects admission, records shutdown only where no reason exists, and awaits every job with no timer. It leaves the Store open for the caller to close.

Validation (head 4b69ea3)

  • npm run typecheck: passes.

  • CI's unit set (npm test minus the Docker suites): 707 passed, 0 failed. The total rose from 488 because main gained tests. 45 of the tests are new, in test/runner-coordinator.test.ts, using a fake D whose promises the test controls, and real SQLite. The first test now also checks that D receives the injected runnerOwner. Each test added for a review fix fails without that fix (checked against the commit before it). The one exception is the case where saving the result and removing the files both fail, which guards existing behaviour. test/review.test.ts failed once in about ten runs; it doesn't touch the runner code.

  • Mutation check (run on F1b: runner coordinator for attempts, slots and shutdown #56's head 3767065): six guards were broken one at a time, and each was caught:

    • a stop freeing the slot early;
    • the launch check skipping the context comparison;
    • no marker after a failed terminal write;
    • close() not waiting;
    • a later reason overwriting the first;
    • the reservation kept after a Store refusal.
  • AGENTS.md race regressions covered here:

    • lease expiry or a clock jump, then a retry while the original still runs;
    • a timeout, then the provider stays unsettled, then a retry;
    • shutdown, then a new request;
    • an abort, then the subprocess settles later;
    • a context change before launch.

    The Store-level cases (an old attempt settling after a retry, two processes admitting at once) are in F1a.

  • Browser tests weren't rerun: this slice doesn't touch web/.

Not in this slice

  • F1c: the Store write gate, coordinator barriers and server wiring, /api/runner, and the merge-coordinator catches.
  • F1d: startup recovery, the requeue claim and the OS lock.
  • F1e: the planning endpoints, and feedback wiring into review actions.
  • Task storage. Removing it after the terminal write is F2 (writable attempts).
  • Recorded preparation process group. Saving preparation_pgid is part of F1d, together with the abortable D helpers from D follow-ups required by the F1 runner lifecycle contract #51.

🤖 Generated with Claude Code

mchwang and others added 2 commits September 28, 2026 15:36
In-memory jobs with two-step admission and a slot reservation;
abortable preparation with task-budget and deadline timers; a
synchronous launch check before D's start; first-reason stops that keep
the slot until D settles; Store-chosen terminal states; unresolved markers
on storage failure; cancel task; and shutdown that keeps existing reasons
and waits for every job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane D (#71) made runnerOwner a required InvocationInput field and refuses
task storage owned by another runner. The injected D now supplies the
token in RunnerDeps, so preparation and the start call use the same one.
F1d replaces the source with the per-database runner token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang and others added 8 commits September 28, 2026 17:05
…e or an admitted job

- D settling with `unreleased` closes the runner to new work until restart, as Ask does.
- A failed read after a refused markRunning no longer abandons the running handle.
- Arming the timers moves inside the job, so a failed budget read ends the attempt instead of leaving it stuck.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cause and the preparation timeout text

- After D settles and the terminal write succeeds, remove the host-side preparation files before freeing the slot.
  A failed terminal write leaves them for startup recovery.
- stop(..., 'stale', cause) keeps the cause; the agent's stderr is never recorded as the reason an attempt went stale.
- A preparation timeout passes no D stop reason, so the row keeps "Timed out while preparing." as the spec requires.
- Tests: the shutdown-keeps-reason case now covers an unsaved reason, which the durable row cannot repair.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ck the context first

- The launch check refuses to start D once unreleased resources were reported, even for an already admitted attempt.
- After the attempt deadline passes before launch, a later stop (shutdown, time limit) cannot replace the timeout.
- The launch check tests the context before the time checks, so a changed context ends stale, as the spec orders it.
- A stop after the terminal write, while only cleanup remains, is refused instead of setting a reason on a settled attempt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cover preparation failure and the held slot

- A cancel task during a timed-out preparation is written onto the row by the Store; the job now shows it too.
- Tests: preparation failure (spec regression round 7), and the start-not-saved marker keeping its slot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed reason before cancel task

- A context change at the launch check records stale, and a job that is ending before launch (cleanup, then the
  terminal write) refuses later stops, so shutdown or the budget cannot turn a stale or failed outcome into a requeue.
- cancelTask first saves an earlier unsaved first reason, so the Store's own cancelled write does not replace it.
  The save bumps the state version, so it runs only when the caller's version is current.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…writes; never free the slot before the marker

- Admission can be part of a userAction transaction. The job now starts after that transaction ends, and if it rolled
  back, the job gives back its reservation without preparing anything.
- A reason write that rolled back with its transaction now shows as unsaved, so the next cancel task saves it again.
- An unexpected failure sets its marker before the job is removed, so the slot is never free in between.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cancelTask mirrors the Store's cancelled write onto a job that no longer takes stops. If the caller's transaction
rolls back, that reason is dropped again, so the attempt keeps its own outcome (for example the launch failure).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The adopted cancelled reason could reach the terminal write before the rollback check ran. It is now a status-only
flag; the terminal write reads the row's own reason, so a committed cancel task still wins and a rolled-back one
leaves the attempt's outcome alone at any microtask ordering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mchwang
mchwang marked this pull request as ready for review September 29, 2026 03:31
@mchwang
mchwang requested a balanced review from Copilot September 29, 2026 03:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Transaction rollbacks can still cancel live work, and cleanup failures can release ownership prematurely.

Review effort: Balanced
Findings: 2 High severity · 1 Medium severity

Open (3)
What changed in this PR

Adds the in-memory runner coordinator for attempt admission, execution, cancellation, settlement, slot ownership, and shutdown.

Changes:

  • Coordinates writable and read-only attempt slots.
  • Handles preparation, launch, stops, settlement, and unreleased resources.
  • Adds comprehensive lifecycle and race regression tests.
File Description
runner/​coordinator.ts Implements runner lifecycle coordination.
test/​runner-coordinator.test.ts Tests admission, races, failures, cancellation, and shutdown.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread runner/coordinator.ts
Comment thread runner/coordinator.ts
Comment thread runner/coordinator.ts Outdated
…les remain

- A settled result whose attempt ID or captured context is not this attempt's is never validated or saved; the
  attempt fails closed, as the question path does (Copilot review).
- A failed removal of host-side preparation files, before launch or after the terminal write, keeps the slot held
  under a preparation-not-removed marker until startup recovery removes the attempt directory (Copilot review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mchwang
mchwang merged commit 256c336 into main Sep 29, 2026
1 check passed
mchwang added a commit that referenced this pull request Sep 29, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every
invocation and checks against the task storage owner. executionDeps takes
the database's token and the workspace allocates storage under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang added a commit that referenced this pull request Sep 29, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every
invocation and checks against the task storage owner. executionDeps takes
the database's token and the workspace allocates storage under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang added a commit that referenced this pull request Sep 29, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every
invocation and checks against the task storage owner. executionDeps takes
the database's token and the workspace allocates storage under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang added a commit that referenced this pull request Sep 29, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every
invocation and checks against the task storage owner. executionDeps takes
the database's token and the workspace allocates storage under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang added a commit that referenced this pull request Sep 30, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every
invocation and checks against the task storage owner. executionDeps takes
the database's token and the workspace allocates storage under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang added a commit that referenced this pull request Sep 30, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every
invocation and checks against the task storage owner. executionDeps takes
the database's token and the workspace allocates storage under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang added a commit that referenced this pull request Sep 30, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every
invocation and checks against the task storage owner. executionDeps takes
the database's token and the workspace allocates storage under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang added a commit that referenced this pull request Sep 30, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every
invocation and checks against the task storage owner. executionDeps takes
the database's token and the workspace allocates storage under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang added a commit that referenced this pull request Oct 1, 2026
* F2b: per-item execution and the runner's commit step

ItemExecutor runs a task's plan items in order as execute attempts.
executionDeps materializes a fresh workspace and snapshots declared links,
builds the prompt with prepareExecution, and in finish() inspects changes,
audits them with auditRun, and makes the runner's own commit with
Plan-Item/Plan-Revision trailers. The coordinator gains an async finish step
and a release step after the terminal write; settleAttempt records the
owned ledger entry with completed in the same transaction. A safety violation
moves the task to needs human; out-of-scope files are committed and pause it
in needs amendment with a checkpoint. The workspace is D's (#66), faked here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Pass the runner owner token into execution deps

RunnerDeps now carries runnerOwner (#74), which D requires on every
invocation and checks against the task storage owner. executionDeps takes
the database's token and the workspace allocates storage under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cover task storage release on the foreign-result and launch-failure paths

After the rebase onto main, the coordinator releases task storage after
the terminal write on main's foreign-result path too. Neither that path
nor the launch-failure path had a test that failed without the release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix the independent review of F2b: runner-owned findings, storage on every path, stable plan, atomic pause

- Safety violations are recorded by the runner's own audit (SafetyFindings),
  never read back from diagnostic text, so agent stderr cannot move a task to
  needs human, and a violation survives a stale or stop outcome.
- auditRun treats any agent commit as a violation, per #66 decision 2
  (no undo path). The TaskWorkspace.commit comment now says so.
- An inspection that refuses sends the task to needs human (contract,
  Publishing step 2); an aborted one is the stop, not a finding.
- Preparation that fails after allocating task storage hands it to the
  coordinator (PreparationFailure), which removes it after the terminal write.
- The executor stops before the next item when the plan gets a new revision,
  and binds a checkpoint to the revision the item ran against.
- The checkpoint and the move to needs amendment commit in one transaction
  (Store.pauseForAmendment), through the shutdown capability; a refused pause
  or status change returns a stopped outcome instead of throwing.
- A rename stages both paths in the runner commit.
- A storage-removal failure holds the slot as 'storage-not-removed', not
  'result-not-saved'.
- runTask returns stopped, with the items it completed, when admission is
  refused or the terminal write failed.
- A test covers that storage is never removed when the terminal write fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 2 of the F2b review: keep scope findings, check before the commit, fail closed on bad reports

- A scope finding always pauses the task: the checkpoint is bound to the
  revision the item ran against and the snapshot its own commit created
  (Store.snapshotWithHead), not to what is current, so a revision saved
  during release no longer drops the pause. The round-1 fix had turned
  that case into a stopped outcome, which a later run skipped past.
- pauseForAmendment validates against the item's own revision; only a
  refused status change (a closed task) returns stopped, other errors throw.
- Before the runner commit, after the last await, finish re-checks the stop
  signal and the captured context; a change makes no commit.
- A change report missing any list fails closed as a safety violation, and an
  audit that throws is a violation too.
- The foreign-result path removes host-side preparation files after the
  terminal write, like every other path.
- snapshotDeclaredLinks gets every path the item declares, so D checks the
  actual entries (an earlier item may have renamed or added links).
- Tests: the pause is atomic (a refused pause leaves no checkpoint), an
  aborted inspection is the stop, a stop or context change during the audit
  makes no commit, malformed reports, foreign-result cleanup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 3 of the F2b review: fail-closed manifests, a pause that cannot be skipped, whole-context checks

- auditRun validates every field it reads before reading any: the metadata
  flag, each change's kind, entry types, underGit, rename old path, link
  target and traversal flag, and the total path bytes. A partial record fails
  closed as a violation instead of reading as clean (AGENTS.md).
- A scope finding whose pause was never recorded (a failed write, the write
  gate, a crash) is paused at the start of the next run, before any item, so
  no run goes past it. A closed write gate is no longer taken for a refusal.
- Between items the executor checks the whole context: the snapshot must be
  the one the previous item's commit created, and the assignment and
  referenced code unchanged, not only the plan revision.
- An invalid commit ID from the workspace fails the attempt instead of
  breaking the terminal write.
- Only the stop's own abort error counts as the stop; another inspection
  refusal stays a safety finding even when a stop is pending.
- Pausing and escalating keep a status someone set during release: both
  require the task to still be running.
- Tests for each, and for the audit-throws, rename-snapshot and checkpoint
  snapshot claims that had none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 4 of the F2b review: pauses that neither stick nor get skipped

- A pause owed from an earlier run is paid when the task is queued again,
  not only while it runs, so a task can no longer get stuck on it.
- A recorded pause holds until a person approves continuing on the current
  revision (continuationRevision); the continued run must name the next item,
  not one the checkpoint already completed.
- A checkpoint is found by its item and its commit's head, not by the latest
  snapshot, so a later snapshot with the same head cannot make an approved
  pause owed again.
- auditRun refuses a change path of "." or "..", and an old path on any
  kind but rename; the path-size limit counts only the saved paths.
- finish refuses a commit equal to the base for a changed item.
- A slot held under a marker is reported with that marker's real cause.
- Tests for each, and for the referenced-code-only context check, the
  AbortError stop, the per-kind entry rules and foreign-result cleanup only
  after a saved terminal write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 5 of the F2b review: fail closed after a scope pause, canonical paths, JSON-sized limit

- A task with a scope checkpoint runs no further items. Continuing after a
  scope pause needs its own design (reconcile the prefix, validate the rest
  from the checkpoint, consume the approval), tracked in #88; round 4's
  partial approval gate could block an approved continuation forever or skip
  items. Before round 4 the executor ran past the pause.
- A checkpoint is found by its commit's head alone; two items cannot share
  the head of a scope commit.
- This run's own pause and escalation keep any status someone set during
  release, queued included; only a pause owed from an earlier run is paid
  from queued.
- auditRun refuses a path not in canonical form (./, a/../, //, a trailing
  /) and a .git part of any case at any depth.
- The path-size limit measures each saved path as the JSON that stores it.
- Tests for each, and for keeping task storage when a foreign result's
  terminal write fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 6 of the F2b review: escalate violations over any open status, stricter audit findings

- A safety violation moves the task to needs human over any status someone
  set during release (queued or a human gate), since a run from there would
  launch the item again; only a closed task is left as it is.
- A declared link retargeted into .git in any case or at any depth is
  refused, as paths already are.
- A rename from an undeclared path records that source path as out of scope,
  not only the declared destination.
- A change report that lists one path twice fails closed.
- Agent-controlled paths in findings are quoted and lists cut short, and the
  finding text is bounded (AGENTS.md).
- Tests for each, for task storage removed after the terminal write when a
  stop, a stale context or a cancel task lands during preparation, and for
  pauseForAmendment's plan-prefix check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 7 of the F2b review: owed safety findings, human gates kept, undeclared link removals

- A safety finding is settled only once acted on. If moving the task to
  needs human fails, or the task sits at a human gate, the finding stays owed
  and the task's next run escalates it before launching anything.
- Escalation moves only a running or queued task: leaving a human-gated or
  review status needs its own user action (runner-lifecycle.md). Round 6 had
  escalated over any open status.
- Between items, a status someone changed during release stops the run.
- Removing a pre-existing symlink, or turning it into a file, at an
  undeclared path is a safety violation.
- The path-size limit counts a rename's old path, which round 6 made a saved
  scope finding when undeclared. Duplicate paths are found under the plan's
  path identity.
- A refused runner commit fails with a quoted message.
- Tests for each, and for the owed-pause status rule and the prefix check
  against the item's own revision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 8 of the F2b review: review statuses escalate, Git's .git spellings, undeclared link renames

- A safety finding moves a review status (in review, approved but merge
  blocked) to needs human, so the task cannot be merged past it; only the
  human gates keep the finding owed. A task already in needs human settles
  the finding, so it is not escalated a second time.
- Renaming a pre-existing symlink from an undeclared path is a violation,
  like removing or replacing it.
- isDotGit refuses every spelling Git treats as .git (any case; NTFS
  trailing dots or spaces and git~1; HFS ignorable code points), in paths
  and link targets.
- A case-only rename counts once under a case-folding identity.
- runner-lifecycle.md lists every unresolved marker reason.
- Tests for each, and for escalation refused by a merge in progress (the
  finding stays owed), escalation through the capability after the gate, an
  owed finding settled on a closed task, and completed plus the ledger entry
  being one transaction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 9 of the F2b review: scope pauses over review statuses, NTFS .git names, links to the root

- A scope pause is recorded over a review status someone set during release
  (in review, approved but merge blocked), in the run that found it and when
  owed, so a merge cannot go past the finding; round 8 did this only for
  safety findings. A queued status set during release is still kept, and the
  next run pays the pause.
- isDotGit ends a name where NTFS does (a stream separator or a backslash)
  before comparing it with .git.
- A declared link retargeted to the repository root (., ./, a/..) is refused:
  the root contains .git.
- A change report without a digest fails closed.
- Tests for each, and for a declared link renamed to an undeclared path, a
  directory entry, an absolute path, every ignorable code point range, and
  escalation over approved but merge blocked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 10 of the F2b review: a split case-only rename is one path

- A case-only rename that Git reports as a delete and an add (the file also
  changed a lot) counts as one path under a case-folding identity, instead of
  a duplicate that sent the task to needs human. Undeclared, the same pair is
  a scope finding; two adds of one folded path are still refused.
- The runner commit's error handling drops a redundant abort special case: a
  stop records its first reason before it aborts, so the outcome is that stop.
- Tests for an owed pause paid from a review status, a pause never
  overwriting needs human, possibly already fixed as a gate, task storage kept
  before launch when the terminal write fails, checkpointAtHead, and the
  unknown-snapshot check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 11 of the F2b review: a split rename is exactly one delete and one add

- The duplicate rule keeps every entry per folded path and allows a second
  entry only for a case-only rename reported as exactly one delete and one
  add with different spellings. Round 10 compared each entry only with the
  last one, so add, delete, add (two adds of one path) got through.
- The runner commit message writes the plan title on one line, so it cannot
  open a trailer block that forges Plan-Item or Plan-Revision.
- Looking for .git reads a backslash as a directory separator (NTFS).
- The completed-list comment says what a thrown error carries.
- Tests for each, and for an assignment-only change between items, D's
  underGit flag on its own, task storage removed when the budget is spent at
  the launch check, and checkpointAtHead finding an older checkpoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 12 of the F2b review: raw link targets, quoted preparation errors, remaining audit tests

- A link target is checked for .git as written as well as resolved, so a
  .git part that a later .. cancels (.git/../src) is refused, as non-canonical
  changed paths already are.
- A preparation error from the workspace is quoted in the diagnostic, like the
  inspection and commit refusals.
- Tests for needs amendment as a human gate, snapshotWithHead picking the
  latest snapshot, .git behind a backslash in a link target, empty and NUL
  link targets, a NUL in a path, directory, other and gitlink old entries, the
  full HFS ignorable ranges, the 300-character quote cut, and an empty digest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 13 of the F2b review: quote every preparation error, test shutdown between items

- The coordinator quotes every preparation error in the diagnostic, so a
  materialize error that names an agent-chosen path cannot forge a second
  line. Round 12 had quoted only the declared-link snapshot's errors.
- Tests for runTask returning stopped with the completed items when shutdown
  refuses the next item's admission, and for a pause's executed prefix being
  built from the plan the item ran against when a revision inserts an item
  before it. The existing revision-during-release test now asserts that the
  revision really changed, since a failed import in release is absorbed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 14 of the F2b review: quote agent stderr, harden release-hook tests

- The agent's stderr is quoted in the attempt diagnostic, so it cannot forge
  a runner line such as "Safety violation:" in the stopped reason. Round 13
  quoted preparation errors for the same threat.
- Tests that set up a change inside the release hook now also assert that
  no storage marker was left and the stop names the change, so a failing
  setup there cannot make them pass vacuously.
- Tests for a rename with both sides undeclared and for an AbortError from
  the inspection with no stop pending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 15 of the F2b review: refuse Windows path forms in link targets

A declared link retargeted with a backslash or a drive prefix (..\..\outside,
C:\Windows) passed the "leaves the repository" and "absolute" checks, which
use POSIX paths, although the audit already reads a backslash as a separator
when it looks for .git. Such link text is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 16 of the F2b review: a new run's start settles nothing of a run still finishing

- runTask refuses to start while an earlier run of the task still has an
  active job (its storage release), so a second run cannot pay the first
  run's scope pause or safety finding and change that run's outcome.
- Paying an owed finding or pause at the start of a run is that run's own
  decision, not settlement: it writes without the shutdown capability, and a
  closed write gate leaves it owed.
- A finding whose terminal write failed reports the needs-restart cause and
  stays owed, instead of "An attempt is still active".
- The coordinator's log lines quote D's error text.
- Tests for each, and for a stop that lands before a rejected commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 17 of the F2b review: find every owed scope pause, cover the remaining claims

- The owed-pause check looks at every completed execute attempt, not only
  the latest, so a later clean attempt cannot hide an earlier finding whose
  pause was never recorded.
- Tests for an owed scope pause left owed (not paid through the shutdown
  capability) once the write gate closed, the ledger record's base, and the
  coordinator's log lines quoting D's error text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 18 of the F2b review: compare the whole context between items, stop after shutdown began

- Between items the executor compares the whole context with what the
  previous item left: its commit's snapshot and a context generation raised by
  exactly that commit. A change that only bumps the generation (for example a
  same-valued reassignment) now stops the run too.
- A run started after shutdown began pays nothing owed and starts nothing.
- The needs-human settle test checks that no extra write happened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 19 of the F2b review: one run per task at a time, owed outcomes report not started

- ItemExecutor holds a per-task in-flight flag from the start of runTask to
  its return, so a second run can never pay the first run's pause or finding,
  even in the microtasks between the coordinator dropping the job and the
  first run resuming (which the isActive check alone left open).
- Paying or keeping an owed finding or pause reports the earlier item with
  state "not started", not that attempt's old state.
- runner-lifecycle.md says when preparation-not-removed happens on each path.
- Tests: a second run started at every microtask offset in the first run's
  release never pays its pause; owed outcomes report not started.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cover round 20 of the F2b review: every owed not-started path, the outside-job check, line separators

Round 20 found no correctness bug. These tests close its gaps: an owed
finding at a human gate, on a closed task and with a failed terminal
write, and an owed pause refused at a gate, each reported as not started;
runTask refusing while a job started outside the executor is still
releasing its storage; and U+2028 in a plan title.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cover round 21 of the F2b review: a pause at a later item, an older owed finding

Round 21 found no correctness bug. Tests now cover a scope pause at P2
(the checkpoint names the whole executed prefix) and an older owed finding
escalated although a later attempt completed cleanly. The ItemExecutor
docs state that its one-run-per-task guard needs one executor per Store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 22 of the F2b review: no control characters in the commit title, test a link to ..

- The runner commit's title drops every control character, not only line
  breaks, so an agent-written plan title cannot put terminal escapes into
  git log. (A NUL is already refused earlier, by the prompt builder.)
- A test covers a declared link retargeted to "..", the directory that
  holds the repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix round 23 of the F2b review: no bidi or format characters in the commit title, two more tests

Round 23 found no correctness bug.
- The runner commit's title also drops bidi and invisible format characters
  (U+200B-U+200F, U+202A-U+202E, U+2060-U+206F, U+FEFF), so an agent-written
  plan title cannot reorder how git log shows it.
- Tests cover the C1 control range in that filter, and this run's own
  escalation rethrowing a closed write gate's error when it has no capability.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants