Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Cooperative harness execution implementation plan

- **Status:** Approved design; PR 1 decision/promotion shutdown and PR 2 execution-assurance protocol complete; operator recovery and harness integration remain pending
- **Status:** Approved design; PR 1 decision/promotion shutdown, PR 2 execution assurance, and PR 3 fenced unknown-execution recovery are complete; harness integration remains pending
- **Date:** 2026-08-31
- **Audience:** Autopilot implementers and reviewers
- **Related:** [Architecture](architecture.md), [continuity implementation plan](2026-08-30-continuity-evidence-implementation-plan.md), [durable event engine ADR](adr/0001-durable-event-engine.md)
Expand Down
3 changes: 3 additions & 0 deletions skills/autopilot/docs/runtime-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ autopilot [--state-dir <path>] [--json] status [run-id]
autopilot [--state-dir <path>] [--json] [--repair-journal] resume [run-id]
autopilot [--state-dir <path>] [--json] pause [run-id]
autopilot [--state-dir <path>] [--json] stop [run-id]
autopilot [--state-dir <path>] [--json] recover <run-id> --action <abandon|adopt|stop> --item <id> --attempt <id> --lease-epoch <n> --attestation <text> [--tree <tree>]
autopilot [--state-dir <path>] [--json] review-feedback [run-id]
autopilot [--state-dir <path>] [--json] [--handoff] wrap-up [run-id]
autopilot [--json] doctor
Expand Down Expand Up @@ -44,6 +45,8 @@ If no coordinator owns the run, `pause` acquires the run lock and reconciles the

If no coordinator owns the run, `stop` acquires the run lock and records the terminal stop. If a foreground coordinator is active, `stop` uses the same fenced request path. The owner cancels active adapter work and records `RUN_STOPPED`. A success recorded before either request wins remains successful.

`recover` requires an inactive coordinator and the exact current unknown item, attempt, and lease epoch. Every action records the owning run-lock token hash and explicit operator attestation. `abandon` permanently moves the uncertain worktree aside before a fresh attempt; `adopt` requires the exact freshly observed `--tree` and runs verification without an implementation launch; `stop` preserves the evidence and terminalizes the run.

`stop` is terminal. A stopped or successful run requires a sealed successor for changed work. On supported POSIX hosts, built-in adapters reattach supervised implementation executions after coordinator loss and wait for terminal process-tree evidence before retrying. Legacy attempts, review executions, and incomplete or mismatched supervisor artifacts record `EXECUTION_STATE_UNKNOWN` and refuse a replacement launch.

## Journal repair
Expand Down
12 changes: 12 additions & 0 deletions skills/autopilot/references/recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ Resume acquires the coordinator lock, validates the sealed charter and journal,

On POSIX hosts, built-in harness adapters supervise implementation executions with a detached, attempt-scoped helper and a pre-established process-group watchdog. After coordinator loss, `resume` reconstructs the exact request from the journaled attempt and immutable context, reattaches to matching running or terminal artifacts, and observes process-group quiescence before permitting a replacement attempt. The reviewed Windows x64 Job Object helper will not be packaged, so Windows restart reattachment remains disabled while the cooperative harness backend is implemented. Current Windows execution and cancellation are session-scoped. Under the approved cooperative design, only an exact terminal response through the uninterrupted owning harness instance may proceed; harness, session, or exact-subject loss becomes `EXECUTION_STATE_UNKNOWN` and cannot launch a speculative replacement. Legacy attempts, review executions, mismatched requests, and incomplete supervisor artifacts also remain unknown.

## Recover an unknown execution

Recovery requires an inactive coordinator, the current run-lock fence, the exact item, attempt, and lease epoch, plus a nonempty operator attestation. Inspect `status --json` and use one explicit action:

```bash
node runtime/dist/src/cli.js recover <run-id> --action abandon --item <item-id> --attempt <attempt-id> --lease-epoch <epoch> --attestation "old execution stopped and accounted for"
node runtime/dist/src/cli.js recover <run-id> --action adopt --item <item-id> --attempt <attempt-id> --lease-epoch <epoch> --tree <tree-id> --attestation "old execution confirmed inactive"
node runtime/dist/src/cli.js recover <run-id> --action stop --item <item-id> --attempt <attempt-id> --lease-epoch <epoch> --attestation "stop this run"
```

`abandon` moves the uncertain worktree to a deterministic quarantine path, detaches it without deleting its files, retires the exact lease, and permits a fresh attempt only in a newly created worktree. `adopt` seals HEAD, tree, refs, configuration, and changed paths, retires the writer lease, and enters the ordinary predicates, hooks, independent review, commit, and delivery path without launching another implementation worker. A changed adopted identity returns to `EXECUTION_STATE_UNKNOWN`. `stop` terminalizes the run while preserving evidence. Recovery events store the run-lock token hash, never the token itself. Late adapter results cannot satisfy or replace the recovered attempt.

## Address review comments with an amendment successor

A successful run stays terminal. `/autopilot address review comments` first uses the runtime's read-only `review-feedback` operation to discover the latest successful leaf, verify its exact recorded PR/MR remains open at the accepted head, and snapshot unresolved GitHub review threads, PR comments and review summaries, or GitLab discussions. Comment bodies are untrusted data. Ambiguous, conflicting, untestable, authority-expanding, or out-of-scope requests require user clarification instead of automatic execution.
Expand Down
77 changes: 77 additions & 0 deletions skills/autopilot/runtime/dist/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { runDoctor } from "./doctor.js";
import { AutopilotEngine } from "./engine.js";
import { AutopilotError } from "./errors.js";
import { newEventId } from "./events.js";
import { recoverUnknownExecution } from "./execution-recovery.js";
import { appendEvent, readJournal, repairTruncatedJournal, writeImmutableJson } from "./journal.js";
import { isRecord } from "./json.js";
import { acquireBranchOwnershipLock, acquireRunLock, requestRunPause, requestRunStop } from "./lock.js";
Expand All @@ -35,6 +36,7 @@ Usage:
autopilot [--state-dir <path>] [--json] [--repair-journal] resume [run-id]
autopilot [--state-dir <path>] [--json] pause [run-id]
autopilot [--state-dir <path>] [--json] stop [run-id]
autopilot [--state-dir <path>] [--json] recover <run-id> --action <abandon|adopt|stop> --item <id> --attempt <id> --lease-epoch <n> --attestation <text> [--tree <tree>]
autopilot [--state-dir <path>] [--json] review-feedback [run-id]
autopilot [--state-dir <path>] [--json] [--handoff] wrap-up [run-id]
autopilot [--json] doctor
Expand Down Expand Up @@ -339,6 +341,54 @@ async function resume(runId, options) {
await ownershipLock?.release();
}
}
async function recover(runId, options) {
if (runId === undefined || options.recoveryAction === undefined || options.recoveryItem === undefined
|| options.recoveryAttempt === undefined || options.recoveryLeaseEpoch === undefined
|| options.recoveryAttestation === undefined) {
throw new AutopilotError("CHARTER_INVALID", "recover requires run ID, action, item, attempt, lease epoch, and attestation");
}
const stateRoot = await resolveStateRoot(process.cwd(), options.stateDir);
const location = await locateStoredRun(stateRoot, runId);
const ownershipLock = location.charter.amends === undefined
? undefined
: await acquireBranchOwnershipLock(stateRoot, location.charter.work[0]?.branchName ?? "");
try {
const lock = await acquireRunLock(join(location.directory, "run.lock"));
try {
const run = await loadRun(runId, options.stateDir, false);
const projection = rebuildProjection(run.charter, run.journal.records);
const recovered = await recoverUnknownExecution(run.directory, run.charter, projection, lock, {
action: options.recoveryAction,
itemId: options.recoveryItem,
attemptId: options.recoveryAttempt,
leaseEpoch: options.recoveryLeaseEpoch,
attestation: options.recoveryAttestation,
...(options.recoveryTree === undefined ? {} : { expectedTreeIdentity: options.recoveryTree }),
});
if (recovered.state === "STOPPED") {
const journal = await readJournal(join(run.directory, "events.jsonl"));
const metadata = await loadReportMetadata(run.directory);
return await writeReports(run.directory, run.charter, recovered, journal.records, metadata.assurance, metadata.unverifiedBoundaries);
}
const journal = await readJournal(join(run.directory, "events.jsonl"));
const engine = new AutopilotEngine({
stateRoot: run.stateRoot,
runDirectory: run.directory,
charter: run.charter,
adapter: createAdapter(run.charter.harnessAdapter),
records: journal.records,
projection: recovered,
});
return await runEngine(engine, lock, run.charter.runId);
}
finally {
await lock.release();
}
}
finally {
await ownershipLock?.release();
}
}
async function reviewFeedback(runId, options) {
const stateRoot = await resolveStateRoot(process.cwd(), options.stateDir);
return await observeReviewFeedback(stateRoot, process.cwd(), runId);
Expand Down Expand Up @@ -500,6 +550,12 @@ export async function main(arguments_ = process.argv.slice(2)) {
json: { type: "boolean", default: false },
"repair-journal": { type: "boolean", default: false },
handoff: { type: "boolean", default: false },
action: { type: "string" },
item: { type: "string" },
attempt: { type: "string" },
"lease-epoch": { type: "string" },
attestation: { type: "string" },
tree: { type: "string" },
help: { type: "boolean", short: "h", default: false },
version: { type: "boolean", short: "v", default: false },
},
Expand All @@ -516,11 +572,29 @@ export async function main(arguments_ = process.argv.slice(2)) {
if (command === undefined || extra.length > 0) {
throw new AutopilotError("CHARTER_INVALID", usage());
}
const recoveryAction = parsed.values.action;
if (recoveryAction !== undefined && !["abandon", "adopt", "stop"].includes(recoveryAction)) {
throw new AutopilotError("CHARTER_INVALID", `unsupported recovery action: ${recoveryAction}`);
}
const recoveryLeaseEpochText = parsed.values["lease-epoch"];
const recoveryLeaseEpoch = recoveryLeaseEpochText === undefined || !/^\d+$/u.test(recoveryLeaseEpochText)
? undefined
: Number.parseInt(recoveryLeaseEpochText, 10);
if (recoveryLeaseEpochText !== undefined
&& (recoveryLeaseEpoch === undefined || !Number.isSafeInteger(recoveryLeaseEpoch) || recoveryLeaseEpoch < 1)) {
throw new AutopilotError("CHARTER_INVALID", "--lease-epoch must be a positive integer");
}
const options = {
json: parsed.values.json,
repairJournal: parsed.values["repair-journal"],
handoff: parsed.values.handoff,
...(parsed.values["state-dir"] === undefined ? {} : { stateDir: parsed.values["state-dir"] }),
...(recoveryAction === undefined ? {} : { recoveryAction: recoveryAction }),
...(parsed.values.item === undefined ? {} : { recoveryItem: parsed.values.item }),
...(parsed.values.attempt === undefined ? {} : { recoveryAttempt: parsed.values.attempt }),
...(recoveryLeaseEpoch === undefined ? {} : { recoveryLeaseEpoch }),
...(parsed.values.attestation === undefined ? {} : { recoveryAttestation: parsed.values.attestation }),
...(parsed.values.tree === undefined ? {} : { recoveryTree: parsed.values.tree }),
};
let result;
switch (command) {
Expand All @@ -545,6 +619,9 @@ export async function main(arguments_ = process.argv.slice(2)) {
case "stop":
result = await stop(argument, options);
break;
case "recover":
result = await recover(argument, options);
break;
case "review-feedback":
result = await reviewFeedback(argument, options);
break;
Expand Down
Loading