diff --git a/autoplan/DRIVER.md b/autoplan/DRIVER.md index 1e8fc3571..48cad0438 100644 --- a/autoplan/DRIVER.md +++ b/autoplan/DRIVER.md @@ -74,7 +74,7 @@ line yourself; gstack never writes the plan. - `gstack-autoplan status|resume --out ` read the manifest (PENDING: lines name open attempts); take ownership after a crash (EXECUTION_UNKNOWN lists unfinished attempts). - `gstack-autoplan resume --out --from ` - restore a hash-verified checkpoint (GSTACK_AUTOPLAN_CHECKPOINT_DIR) into an empty directory and take ownership. + restore the newest valid checkpoint generation (GSTACK_AUTOPLAN_CHECKPOINT_DIR) into an empty directory and take ownership; `checkpoints --from ` lists them. ## Reviewer result format @@ -127,8 +127,8 @@ is the completion check. Copy the whole directory, never single files. - `OWNER_REPLY_REQUIRED`: pass the owner's words exactly with `--owner-reply ""`, `--owner-reply -` (stdin) or `--owner-reply-file ` - `GATE_OWNER_REPLY_MISMATCH`: relay exactly what the owner chose (or their reply itself, e.g. `--reply rec`); ask the owner when their words and the tokens disagree - `PLAN_AMEND_PENDING`: edit the plan, then `gstack-autoplan revise --out --plan --incorporates `; settle a packaging or ordering change with `gstack-autoplan revise --out --settle `; or drop one with `gstack-autoplan revise --out --withdraw ` -- `AMENDMENT_NOT_SETTLEABLE`: edit the plan, then `gstack-autoplan revise --out --plan --incorporates `; settle only packaging or ordering changes on an unchanged plan -- `CHECKPOINT_INVALID`: restore from an intact copy of `//` (its `restore.md` names the command); nothing was written to --out +- `AMENDMENT_NOT_SETTLEABLE`: edit the plan, then `gstack-autoplan revise --out --plan --incorporates ` (add `--settle ` to settle packaging/ordering changes in the same call); settle alone only on the plan the run already reviews +- `CHECKPOINT_INVALID`: run `gstack-autoplan checkpoints --from ` to see each generation's hash status and restore an intact one; nothing was written to --out - `GSTACK_VERSION_CHANGED`: nothing to do for an older run (journaled forward migrations ran); a run written by a newer gstack needs that gstack: upgrade with bin/gstack-upgrade ## Coordinator contract (inherited by every lane) diff --git a/bin/gstack-autoplan b/bin/gstack-autoplan index e2b8766f4..becab9f1d 100755 --- a/bin/gstack-autoplan +++ b/bin/gstack-autoplan @@ -16,6 +16,7 @@ * gstack-autoplan status --out [--json] query: the manifest, no GSTACK_RESULT line * gstack-autoplan export --out [--json] rewrite plan.md, review-record.md, findings/tasks.jsonl and run.json hashes * gstack-autoplan release --out --attempt drop a reservation for an attempt that never ran + * gstack-autoplan checkpoints --from list checkpoint generations, newest first, with hash ok|bad * gstack-autoplan contract print the coordinator contract every lane prompt inherits (plan E2) * * Logic: lib/autoplan-run.ts (next/submit/resume), lib/autoplan-gate.ts (close, gate, answer), @@ -26,8 +27,8 @@ */ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { answerGate, applyGuidance, recommendGate } from '../lib/autoplan-gate'; -import { checkpointRun, restoreCheckpoint } from '../lib/autoplan-checkpoint'; +import { amendmentSteps, answerGate, recommendGate } from '../lib/autoplan-gate'; +import { checkpointLines, checkpointRun, restoreCheckpoint } from '../lib/autoplan-checkpoint'; import { admit, touchInstallLease } from '../lib/autoplan-admission'; import { exportRun } from '../lib/autoplan-export'; import { migrateRun } from '../lib/autoplan-migrate'; @@ -69,7 +70,9 @@ DRIVER: ${driverFile()} recommend --out --gate-rev --item --option --basis "" [--by ] set one item's recommendation before relay (provenance recorded, p1 follows); re-render and relay afterwards resume --out [--from ] [--json] --from restores a hash-verified checkpoint into an empty --out first - (GSTACK_AUTOPLAN_CHECKPOINT_DIR set: every command leaving the run at rest writes // and prints CHECKPOINT:) + (a collection or root restores its newest valid generation; GSTACK_AUTOPLAN_CHECKPOINT_DIR set: every command + leaving the run at rest publishes a generation under // and prints CHECKPOINT: on stderr) + checkpoints --from list checkpoint generations, newest first: run, gate_rev, time, hash ok|bad, current status --out [--json] export --out release --out --attempt contract print lib/coordinator-contract.ts (the block every lane and reviewer prompt inherits word for word) @@ -98,11 +101,7 @@ const resultLineFor = (status: string, out: string) => `GSTACK_RESULT: skill=aut const listFlag = (args: string[], name: string) => args.flatMap((a, i) => (a === name && args[i + 1] ? args[i + 1]!.split(',').map(x => x.trim()).filter(Boolean) : [])); /** The NEXT line after an answer or revise: per pending change, settle it or edit the plan and revise; else next. */ function nextLine(state: RunState, out: string): string { - const pending = pendingAmendments(state); - const settle = pending.filter(a => applyGuidance(a).startsWith('no plan edit')).map(a => a.item ?? a.decision); - const edit = pending.filter(a => !applyGuidance(a).startsWith('no plan edit')).map(a => a.decision); - if (!pending.length) return `NEXT: gstack-autoplan next --out ${out}`; - return `NEXT: ${[edit.length ? `edit the plan, then gstack-autoplan revise --out ${out} --plan ${state.plan_input ?? state.plan_source} --incorporates ${edit.join(',')}` : '', settle.length ? `gstack-autoplan revise --out ${out} --settle ${settle.join(',')}` : ''].filter(Boolean).join('; ')}`; + return pendingAmendments(state).length ? `NEXT: ${amendmentSteps(state, out)}` : `NEXT: gstack-autoplan next --out ${out}`; } /** @@ -128,12 +127,21 @@ function mutating(out: string, json: boolean, fn: () => number): number { if (notices.length) (json ? process.stderr : process.stdout).write(notices.join('\n') + '\n'); const code = fn(); if (admission.deviation && fs.existsSync(path.join(out, FILES.run))) { const state = readState(out); if (record(state)) writeState(state); } - const checkpoint = code === EXIT.ok ? checkpointRun(out) : undefined; - if (checkpoint) process.stderr.write(checkpoint + '\n'); + if (code === EXIT.ok) checkpointQuietly(out); return code; }); } +/** A checkpoint never changes a committed command's outcome: a failure is a stderr warning, the exit code stands. */ +function checkpointQuietly(out: string): void { + try { + const line = checkpointRun(out); + if (line) process.stderr.write(line + '\n'); + } catch (e: any) { + process.stderr.write(`CHECKPOINT: failed for ${out}: ${e instanceof RunError ? `${e.detail} (${e.code})` : e?.message ?? e}; the command's result stands, the previous checkpoint stays current\n`); + } +} + function emit(json: boolean, lines: string[], payload: Record): void { if (json) process.stdout.write(JSON.stringify(payload) + '\n'); else process.stdout.write(lines.join('\n') + '\n'); @@ -238,6 +246,12 @@ async function main(argv: string[]): Promise { const json = rest.includes('--json'); if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { process.stdout.write(HELP + '\n'); return cmd ? EXIT.ok : EXIT.usage; } if (cmd === 'contract') { process.stdout.write(COORDINATOR_CONTRACT + '\n'); return EXIT.ok; } + if (cmd === 'checkpoints') { + const from = flag(rest, '--from'); + if (!from) return usage('checkpoints needs --from '); + try { process.stdout.write(checkpointLines(from).join('\n') + '\n'); return EXIT.ok; } + catch (e: any) { if (!(e instanceof RunError)) throw e; process.stderr.write(resultLine('gstack-autoplan checkpoints', e.code, e.detail) + '\n'); return EXIT.refused; } + } const outFlag = flag(rest, '--out'); if (!outFlag) return usage(`${cmd} needs --out `); const out = path.resolve(outFlag); @@ -256,8 +270,7 @@ async function main(argv: string[]): Promise { const restored = from !== undefined ? restoreCheckpoint(from, out) : []; return withRunLock(out, () => { const r = resumeRun(out); - const checkpoint = checkpointRun(out); - if (checkpoint) process.stderr.write(checkpoint + '\n'); + checkpointQuietly(out); const lines = [...restored, ...r.lines, resultLineFor(r.state.status, out)]; emit(json, lines, { run: r.state.run, status: r.state.status, execution_unknown: r.unknown, rebound: r.rebound, out }); return EXIT.ok; diff --git a/bin/gstack-doctor-components.sh b/bin/gstack-doctor-components.sh index cdff473ed..334148c1f 100644 --- a/bin/gstack-doctor-components.sh +++ b/bin/gstack-doctor-components.sh @@ -192,6 +192,7 @@ gstack_result_anchor() { AMENDMENT_NOT_SETTLEABLE) printf '%s' 'amendment-not-settleable' ;; GATE_REC_STALE) printf '%s' 'gate-rec-stale' ;; CHECKPOINT_INVALID) printf '%s' 'checkpoint-invalid' ;; + CHECKPOINT_OVERLAP) printf '%s' 'checkpoint-overlap' ;; CHECKPOINT_TARGET_EXISTS) printf '%s' 'checkpoint-target-exists' ;; OWNER_REPLY_REQUIRED) printf '%s' 'owner-reply-required' ;; GATE_OWNER_REPLY_MISMATCH) printf '%s' 'gate-owner-reply-mismatch' ;; diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index a4c323f7a..c0b00d065 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -2813,14 +2813,18 @@ pending, revise without `--incorporates`: **Meaning.** `gstack-autoplan revise --settle ` discharges an accepted change without a plan edit, which is honest only for a packaging or ordering change -(the gate item's `changes` is `packaging` or `ordering`) on a plan nobody has -edited since it was reviewed. The named change alters scope, cost or direction, -names no `changes` value, or the plan file's Implementation plan no longer -matches the reviewed snapshot. Nothing was changed. +(the gate item's `changes` is `packaging` or `ordering`) on the plan the run +already reviews: the plan the owner accepted it on, or one a later `revise` +recorded. The named change alters scope, cost or direction, names no `changes` +value, the plan file has edits no `revise` has recorded, or `--settle` came with +a `--plan` that changes the Implementation plan while incorporating nothing. +Nothing was changed. **Fix.** Edit the plan, then -`gstack-autoplan revise --out --plan --incorporates `. -The `APPLY:` line for each change says which path it takes. +`gstack-autoplan revise --out --plan --incorporates `, +adding `--settle ` to settle packaging or ordering changes in the same call +(or settle them after the revise). The `APPLY:` line for each change says which +path it takes, and `NEXT:` lists the settle step first. ### `GATE_REC_STALE` @@ -2829,24 +2833,44 @@ The `APPLY:` line for each change says which path it takes. was rendered, so a `rec` (or `all`) reply the owner gave to the earlier render would take recommendations they never saw. Every render prints `rec_hash=`; once `recommend` has run, a `rec` answer must name the hash of -the render the owner answered. +the render the owner answered. The same refusal meets a `rec`/`all` reply to a +gate written before `rec` existed (a `gate.json` with no `rec_rev`, from +1.91.101.0 or earlier): that render told the owner `all` leaves items +recommended `b` pending, and `all` now takes them. **Fix.** Render again (`gstack-gate render /gate.json --all`), relay it, -and answer with `--rec-hash ` from that render. +and answer with `--rec-hash ` from that render. Explicit `