@kylecheng3146/agent-ops 0.11.5 → 0.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/cli/src/bin.js +6 -0
- package/dist/packages/cli/src/commands/advance.js +18 -0
- package/dist/packages/cli/src/commands/run.js +2 -0
- package/dist/packages/cli/src/context.js +2 -1
- package/dist/packages/cli/src/run-entry.js +66 -10
- package/dist/runtime/src/review/execute.js +92 -13
- package/dist/runtime/src/review/render.js +4 -0
- package/dist/runtime/src/run/proof-recovery.js +167 -0
- package/dist/runtime/src/run/service.js +9 -0
- package/docs/en/spec/review.md +13 -0
- package/docs/zh-TW/spec/review.md +11 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { renderCriteriaReview } from "../../../runtime/src/review/criteria.js";
|
|
|
4
4
|
import { advancePhaseObserver, productionRunContext } from "./run-deps.js";
|
|
5
5
|
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
6
6
|
import { recordRunVerification } from "../../../runtime/src/run/verification.js";
|
|
7
|
+
import { runReviewCheckpoints } from "../../../runtime/src/run/proof-recovery.js";
|
|
7
8
|
import { runOwnedLocalProof } from "./owned-run-step.js";
|
|
8
9
|
import { runRunCommand } from "./commands/run.js";
|
|
9
10
|
import { runFollowupCommand } from "./commands/followup.js";
|
|
@@ -557,6 +558,10 @@ else {
|
|
|
557
558
|
// a `batch` are reviewed identically.
|
|
558
559
|
const buildReviewOptions = async (reviewArgs, signal, preflightTarget = probeTarget, progressPrefix = "") => {
|
|
559
560
|
const reviewSessionId = process.env.AGENT_OPS_SESSION_ID;
|
|
561
|
+
// Only the coordinator's registered run proof group, during a final proof, resumes saved rounds.
|
|
562
|
+
const run = await runPolicyContext(root).catch(() => null);
|
|
563
|
+
const checkpoints = run?.proof === true && run.worker?.workerId === run.state.coordinatorId &&
|
|
564
|
+
run.state.finalProof?.pending === true ? runReviewCheckpoints(run.repository, run.state) : undefined;
|
|
560
565
|
const reviewGit = gitRunner(root);
|
|
561
566
|
const reviewConfig = (await loadEffectiveConfig(root, reviewArgs.scope === "user" ? "user" : "project")).config;
|
|
562
567
|
const reviewRole = resolveReviewRole("independent-review", reviewConfig.reviewRoles ?? []);
|
|
@@ -625,6 +630,7 @@ else {
|
|
|
625
630
|
? {}
|
|
626
631
|
: { timeoutMs: reviewRole.timeoutMs }),
|
|
627
632
|
preflightTarget,
|
|
633
|
+
...(checkpoints === undefined ? {} : { checkpoints }),
|
|
628
634
|
verifySourceFingerprint: async (expected, scope) => {
|
|
629
635
|
const currentScope = await resolveReviewScope({
|
|
630
636
|
root,
|
|
@@ -8,6 +8,9 @@ import { readWorktreeRecord, resolveCheckouts, sessionWorktreeName, worktreePath
|
|
|
8
8
|
import { resolveReviewScope } from "../../../../runtime/src/review/scope.js";
|
|
9
9
|
import { renderReviewDetails, renderReviewHighlights } from "../../../../runtime/src/review/render.js";
|
|
10
10
|
import { calculateSourceFingerprint } from "../../../../runtime/src/verify/source-fingerprint.js";
|
|
11
|
+
import { FileEvidenceStore } from "../../../../runtime/src/verify/evidence.js";
|
|
12
|
+
import { parseArgs } from "../args.js";
|
|
13
|
+
import { hasFreshVerification } from "./review.js";
|
|
11
14
|
import { okEnvelope } from "../output.js";
|
|
12
15
|
import { readFinishedReview } from "./review-show.js";
|
|
13
16
|
import { criteriaProgress } from "../../../../runtime/src/run/phase.js";
|
|
@@ -50,6 +53,17 @@ function runner(deps, cwd) {
|
|
|
50
53
|
return { exitCode: result.exitCode, stdout: Buffer.from(result.stdout, "utf8") };
|
|
51
54
|
} };
|
|
52
55
|
}
|
|
56
|
+
/** Current passing evidence for this candidate, by the rule review's own preflight applies; anything unreadable re-verifies. */
|
|
57
|
+
async function freshlyVerified(deps, cwd, taskId, base) {
|
|
58
|
+
try {
|
|
59
|
+
return await hasFreshVerification({ args: parseArgs(["review", "--task", taskId, "--base", base, "--yes"]), authorized: true,
|
|
60
|
+
tasks: deps.tasks(cwd), root: cwd, gitRunner: runner(deps, cwd), config: await deps.loadConfig(cwd),
|
|
61
|
+
evidenceStore: new FileEvidenceStore(cwd, cwd) }, taskId, base);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
53
67
|
async function git(deps, cwd, args) {
|
|
54
68
|
const result = await deps.git(cwd, args);
|
|
55
69
|
if (result.exitCode !== 0)
|
|
@@ -138,6 +152,10 @@ export async function runAdvanceCommand(options) {
|
|
|
138
152
|
const onPhase = options.onPhase ?? (async () => { });
|
|
139
153
|
await onPhase("verifying");
|
|
140
154
|
for (const task of ordered) {
|
|
155
|
+
if (await freshlyVerified(deps, record.path, task.task.id, target)) {
|
|
156
|
+
await onPhase("verifying", { taskId: task.task.id, update: { verify: "PASS" } });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
141
159
|
const verified = await step(record.path, ["verify", "--task", task.task.id, "--base", target]);
|
|
142
160
|
await onPhase("verifying", { taskId: task.task.id, update: { verify: verified.status === "ok" ? "PASS" : "FAIL",
|
|
143
161
|
...criteriaProgress(task.task.criteria, verified.data?.report ?? {}) } });
|
|
@@ -29,6 +29,8 @@ function textFor(action, state, message) {
|
|
|
29
29
|
`Cause: ${recovery.cause === null ? "none recorded" : recovery.cause.code + ": " + recovery.cause.detail}`,
|
|
30
30
|
`Sealed proof: ${recovery.sealedProof}; cleanup: ${recovery.cleanup}; resumable: ${recovery.resumable ? "yes, after reconciliation" : "no"}`,
|
|
31
31
|
`Remediation: ${recovery.remediation}`,
|
|
32
|
+
state.finalProof == null ? "" : `Final proof recovery: ${state.finalProof.recoveries}/1 used; first failure ${state.finalProof.failures[0]?.code ?? "none"}; ` +
|
|
33
|
+
`last failure ${state.finalProof.failures.at(-1)?.code ?? "none"}; automatic recovery ${state.finalProof.stopped ? "stopped" : state.finalProof.pending ? "in progress" : "available"}`,
|
|
32
34
|
`Host: ${state.host}`,
|
|
33
35
|
`Workers: ${active}/${state.jobs}`,
|
|
34
36
|
state.budget.limitMs === null ? `Budget: unlimited; ${state.budget.accumulatedMs}ms active time recorded` : `Budget: ${Math.max(0, state.budget.limitMs - state.budget.accumulatedMs)}ms remaining (legacy; manual resume upgrades)`,
|
|
@@ -263,7 +263,8 @@ export async function runPolicyContext(root, renew = false) {
|
|
|
263
263
|
const policy = await readRunPolicy(state, await runRuntimeHash());
|
|
264
264
|
if (policy === null)
|
|
265
265
|
return null;
|
|
266
|
-
|
|
266
|
+
// `proof`: this process runs inside the supervisor's registered run-level proof group.
|
|
267
|
+
return { repository, state, policy, worker, proof: globalProof };
|
|
267
268
|
}
|
|
268
269
|
/** Only active execution paths call this; status, installation and recovery reads never renew. */
|
|
269
270
|
export async function ensureRunPolicy(repository, state) {
|
|
@@ -8,6 +8,7 @@ import { resolveReviewScope } from "../../../runtime/src/review/scope.js";
|
|
|
8
8
|
import { writeNoChangeDelivery, integrateSessionChildren } from "../../../runtime/src/parallel/integrate.js";
|
|
9
9
|
import { recordNativeRunUsage, recordReviewRunUsage, recordSavedReviewRunUsage } from "../../../runtime/src/run/usage.js";
|
|
10
10
|
import { recordNativeRunAuthorization } from "../../../runtime/src/run/authorization.js";
|
|
11
|
+
import { discardReviewCheckpoints, finalProofAttemptKey, InterruptedStepError, processGroupAlive, runFinalProof } from "../../../runtime/src/run/proof-recovery.js";
|
|
11
12
|
import { observeRunFailure } from "../../../runtime/src/run/verification.js";
|
|
12
13
|
import { criteriaProgress, recordRunPhase } from "../../../runtime/src/run/phase.js";
|
|
13
14
|
import { compareFailureObservations } from "../../../runtime/src/run/convergence.js";
|
|
@@ -435,6 +436,27 @@ export async function superviseNativeRun(root, runId) {
|
|
|
435
436
|
async function advance(saved) {
|
|
436
437
|
return await step(root, ["task", "advance", "--task", saved.rootTaskId, "--session", saved.ownerSessionId, "--yes"], saved);
|
|
437
438
|
}
|
|
439
|
+
async function attemptKey(saved) {
|
|
440
|
+
const coordinator = saved.workers.find(w => w.workerId === saved.coordinatorId);
|
|
441
|
+
if (coordinator?.worktree == null)
|
|
442
|
+
throw new AgentOpsError("RUN_WORKTREE_MISSING", "Final proof requires the coordinator checkout.");
|
|
443
|
+
return finalProofAttemptKey({ candidate: (await deps.git(coordinator.worktree, ["rev-parse", "HEAD"])).stdout.trim(),
|
|
444
|
+
target: (await deps.git(root, ["rev-parse", saved.targetBranch + "^{commit}"])).stdout.trim(),
|
|
445
|
+
contractHash: saved.currentContractHash, configHash: saved.policyBinding?.configHash ?? null,
|
|
446
|
+
runtimeHash: saved.policyBinding?.runtimeHash ?? null });
|
|
447
|
+
}
|
|
448
|
+
async function finalProof(restartedKey) {
|
|
449
|
+
return await runFinalProof(repository, runId, {
|
|
450
|
+
advance: async () => await advance((await repository.read(runId))),
|
|
451
|
+
attemptKey: async () => await attemptKey((await repository.read(runId))),
|
|
452
|
+
untouched: async () => {
|
|
453
|
+
const latest = (await repository.read(runId));
|
|
454
|
+
const coordinator = latest.workers.find(w => w.workerId === latest.coordinatorId);
|
|
455
|
+
return coordinator?.worktree != null && latest.integration === null &&
|
|
456
|
+
(await collectChangeSurface(gitRunner(coordinator.worktree))).paths.length === 0;
|
|
457
|
+
}
|
|
458
|
+
}, restartedKey);
|
|
459
|
+
}
|
|
438
460
|
async function recoverIntegration(saved) {
|
|
439
461
|
const coordinator = saved.workers.find(worker => worker.workerId === saved.coordinatorId);
|
|
440
462
|
if (coordinator?.worktree == null)
|
|
@@ -662,7 +684,9 @@ export async function superviseNativeRun(root, runId) {
|
|
|
662
684
|
await recordReviewRunUsage(repository, runId, review.report.value);
|
|
663
685
|
const finalContract = treeContractHash(receipt.tasks.map(record => record.task));
|
|
664
686
|
await repository.mutate(runId, current => ({ ...current, currentContractHash: finalContract,
|
|
665
|
-
contractRevision: current.contractRevision + (current.currentContractHash === finalContract ? 0 : 1)
|
|
687
|
+
contractRevision: current.contractRevision + (current.currentContractHash === finalContract ? 0 : 1),
|
|
688
|
+
...(current.finalProof == null ? {} : { finalProof: { ...current.finalProof, pending: false, checkpoints: undefined } }) }));
|
|
689
|
+
await discardReviewCheckpoints(current).catch(() => { });
|
|
666
690
|
await supervisor.finalize(runId, { targetCommit: receipt.candidateHead, candidateCommit: receipt.candidateHead,
|
|
667
691
|
verificationPass: true, reviewPass: true, taskStateComplete: true, receiptWritten: true, receiptDigest: sha256(source) });
|
|
668
692
|
try {
|
|
@@ -726,6 +750,32 @@ export async function superviseNativeRun(root, runId) {
|
|
|
726
750
|
else if (state.policyBinding !== undefined)
|
|
727
751
|
await synchronizePolicy();
|
|
728
752
|
state = (await repository.read(runId));
|
|
753
|
+
// A final proof the previous supervisor left pending resumes before any writer starts.
|
|
754
|
+
let proofNote;
|
|
755
|
+
if (state.finalProof?.pending === true && state.rootTaskId !== null) {
|
|
756
|
+
try {
|
|
757
|
+
const result = await finalProof(state.finalProof.attemptKey);
|
|
758
|
+
if (result.status !== "ok")
|
|
759
|
+
throw runStepError(result, "Final proof failed after a supervisor restart");
|
|
760
|
+
if (!plain(result.data) || typeof result.data.receipt !== "string")
|
|
761
|
+
throw new AgentOpsError("RUN_RECEIPT_REQUIRED", "Finalization requires the receipt returned by the existing finish gate.");
|
|
762
|
+
await acceptReceipt(state, result.data.receipt);
|
|
763
|
+
await writePrivateFile(join(directory, runId, "final-response.json"), JSON.stringify({ code: "RUN_COMPLETE", receipt: result.data.receipt }), commonDir);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
catch (cause) {
|
|
767
|
+
// A sealed transaction keeps its manual reconciliation path.
|
|
768
|
+
if ((await repository.read(runId)).integration !== null)
|
|
769
|
+
throw cause;
|
|
770
|
+
const coordinator = state.workers.find(w => w.workerId === state.coordinatorId);
|
|
771
|
+
await writePrivateFile(join(coordinator.worktree, ".agent-ops", "tasks", "run-last-failure.json"), JSON.stringify({
|
|
772
|
+
code: cause instanceof AgentOpsError ? cause.code : "RUN_PROOF_FAILED",
|
|
773
|
+
error: cause instanceof AgentOpsError ? cause.message : "Final proof failed; inspect local diagnostics."
|
|
774
|
+
}), coordinator.worktree);
|
|
775
|
+
proofNote = "Final proof did not complete after a supervisor restart. Read run-last-failure.json and saved review/verification reports, fix substantive findings, then request a new evidence gate.";
|
|
776
|
+
}
|
|
777
|
+
state = (await repository.read(runId));
|
|
778
|
+
}
|
|
729
779
|
for (const worker of state.workers.filter(w => active(w) || w.status === "stopped")) {
|
|
730
780
|
if (recoverySkips(state, worker))
|
|
731
781
|
continue;
|
|
@@ -733,7 +783,8 @@ export async function superviseNativeRun(root, runId) {
|
|
|
733
783
|
!(await deps.tasks(worker.worktree).list()).some(r => r.task.id === worker.taskId)))
|
|
734
784
|
continue;
|
|
735
785
|
try {
|
|
736
|
-
await start(worker, worker.workerId === state.coordinatorId
|
|
786
|
+
await start(worker, worker.workerId === state.coordinatorId
|
|
787
|
+
? proofNote ?? (yielded(worker) ? coordinatorWakeNote(state, worker) : undefined) : undefined, manualResumeAt);
|
|
737
788
|
if (state.jobs === 1)
|
|
738
789
|
break;
|
|
739
790
|
}
|
|
@@ -964,7 +1015,7 @@ export async function superviseNativeRun(root, runId) {
|
|
|
964
1015
|
}
|
|
965
1016
|
await stopAll("stop", false);
|
|
966
1017
|
await recordRunPhase(repository, runId, worker.workerId, "verifying");
|
|
967
|
-
const result = await
|
|
1018
|
+
const result = await finalProof();
|
|
968
1019
|
if (result.status !== "ok")
|
|
969
1020
|
throw runStepError(result, "Final proof failed; inspect saved verifier and fresh review reports before repair");
|
|
970
1021
|
const data = result.data;
|
|
@@ -1066,7 +1117,10 @@ export async function runControlledStep(repository, cli, cwd, args, saved) {
|
|
|
1066
1117
|
catch { }
|
|
1067
1118
|
}, 3000);
|
|
1068
1119
|
};
|
|
1069
|
-
|
|
1120
|
+
// Why the supervisor itself ended the step; any such end is never replayed automatically.
|
|
1121
|
+
let ended;
|
|
1122
|
+
const end = (why) => { ended ??= why; terminate(); };
|
|
1123
|
+
const timeout = Number.isFinite(remaining) ? setTimeout(() => end("deadline"), remaining) : undefined;
|
|
1070
1124
|
const registration = (async () => {
|
|
1071
1125
|
const identity = child.pid === undefined ? null : await nativeProcessIdentity(child.pid);
|
|
1072
1126
|
if (identity === null || closed) {
|
|
@@ -1095,8 +1149,8 @@ export async function runControlledStep(repository, cli, cwd, args, saved) {
|
|
|
1095
1149
|
checking = true;
|
|
1096
1150
|
void repository.read(runId).then(current => {
|
|
1097
1151
|
if (current === null || current.disableRestart || current.status !== "active")
|
|
1098
|
-
|
|
1099
|
-
}).catch(
|
|
1152
|
+
end("stop");
|
|
1153
|
+
}).catch(() => end("state")).finally(() => { checking = false; });
|
|
1100
1154
|
}, 250);
|
|
1101
1155
|
child.stdout.on("data", (chunk) => {
|
|
1102
1156
|
const room = 4 * 1024 * 1024 - output.length;
|
|
@@ -1104,7 +1158,7 @@ export async function runControlledStep(repository, cli, cwd, args, saved) {
|
|
|
1104
1158
|
output = Buffer.concat([output, chunk.subarray(0, room)]);
|
|
1105
1159
|
if (chunk.length > room) {
|
|
1106
1160
|
outputOverflow = true;
|
|
1107
|
-
|
|
1161
|
+
end("overflow");
|
|
1108
1162
|
}
|
|
1109
1163
|
});
|
|
1110
1164
|
child.stderr.on("data", (chunk) => {
|
|
@@ -1112,7 +1166,7 @@ export async function runControlledStep(repository, cli, cwd, args, saved) {
|
|
|
1112
1166
|
if (room > 0)
|
|
1113
1167
|
diagnostic = Buffer.concat([diagnostic, chunk.subarray(0, room)]);
|
|
1114
1168
|
});
|
|
1115
|
-
child.once("error", cause => { spawnError = cause;
|
|
1169
|
+
child.once("error", cause => { spawnError = cause; end("spawn"); });
|
|
1116
1170
|
child.once("close", async (exitCode, signal) => {
|
|
1117
1171
|
closed = true;
|
|
1118
1172
|
clearTimeout(timeout);
|
|
@@ -1160,12 +1214,14 @@ export async function runControlledStep(repository, cli, cwd, args, saved) {
|
|
|
1160
1214
|
catch (cause) {
|
|
1161
1215
|
const excerpt = (value) => redactSecrets(value)
|
|
1162
1216
|
.replace(/-----BEGIN (?:[A-Z0-9]+ )?PRIVATE KEY-----[\s\S]*$/u, "[REDACTED_PRIVATE_KEY]").slice(0, 3500);
|
|
1163
|
-
const
|
|
1217
|
+
const replayable = ["process", "result"].includes(stage) && ended === undefined && spawnError === undefined &&
|
|
1218
|
+
child.pid !== undefined && !processGroupAlive(child.pid);
|
|
1219
|
+
const detail = JSON.stringify({ exitCode, signal, stage, outputOverflow, ended: ended ?? null, replayable,
|
|
1164
1220
|
cause: excerpt(cause instanceof Error ? cause.message : String(cause)),
|
|
1165
1221
|
stderr: excerpt(diagnostic.toString("utf8")), stdout: excerpt(output.toString("utf8").slice(0, 4096)) }).slice(0, 9500);
|
|
1166
1222
|
await repository.appendEvent(runId, { type: "diagnostic", code: "RUN_CONTROLLED_STEP_FAILED",
|
|
1167
1223
|
workerId: env.AGENT_OPS_WORKER_ID ?? null, taskId: saved.workers.find(w => w.worktree === cwd)?.taskId ?? null, detail }).catch(() => undefined);
|
|
1168
|
-
reject(new
|
|
1224
|
+
reject(new InterruptedStepError("Final gate did not return a complete result; inspect target and receipt before retrying. " + detail, replayable));
|
|
1169
1225
|
}
|
|
1170
1226
|
});
|
|
1171
1227
|
});
|
|
@@ -10,7 +10,7 @@ import { reviewReportResults, reviewReportStatus, validateReviewReport } from ".
|
|
|
10
10
|
import { detectHostTarget, planReviewTargets } from "./roles.js";
|
|
11
11
|
import { detectHostRestriction, isLoginFailure, isNetworkFailure } from "./host-sandbox.js";
|
|
12
12
|
import { buildAdversarialPrompt, buildReviewPrompt } from "./runner.js";
|
|
13
|
-
import { MAX_ADVERSARIAL_PROMPT_BYTES } from "./runner.js";
|
|
13
|
+
import { MAX_ADVERSARIAL_PROMPT_BYTES, redactReviewReport } from "./runner.js";
|
|
14
14
|
import { extractUsage, sumUsage } from "./usage.js";
|
|
15
15
|
/**
|
|
16
16
|
* Full repository reviews need far more headroom than the lightweight auth
|
|
@@ -754,7 +754,7 @@ async function attemptTarget(request, options) {
|
|
|
754
754
|
*/
|
|
755
755
|
async function attemptTargetWithRetry(request, options) {
|
|
756
756
|
const first = await attemptTarget(request, options);
|
|
757
|
-
if (first.kind !== "skip" || first.transient !== true || budgetSpent(options)) {
|
|
757
|
+
if (first.kind !== "skip" || first.transient !== true || budgetSpent(options) || options.checkpoints !== undefined) {
|
|
758
758
|
return first;
|
|
759
759
|
}
|
|
760
760
|
options.onProgress?.(`${request.target}: ${first.diagnostic} → retrying once`);
|
|
@@ -817,9 +817,59 @@ export function createReviewExecutor(options) {
|
|
|
817
817
|
attempts: []
|
|
818
818
|
};
|
|
819
819
|
}
|
|
820
|
+
const expectedCriterionIds = request.invocation.packet.criteria.map((criterion) => criterion.id);
|
|
821
|
+
const primaryTarget = plannedTargets[0];
|
|
822
|
+
const adversarialTarget = plannedTargets[1];
|
|
823
|
+
const primaryPrompt = buildReviewPrompt({ ...request.invocation, harness: primaryTarget });
|
|
824
|
+
const binding = sha256(JSON.stringify({ primaryPrompt, targets: [primaryTarget, adversarialTarget],
|
|
825
|
+
model: options.model ?? null, effort: options.effort ?? null }));
|
|
826
|
+
const journal = options.checkpoints;
|
|
827
|
+
let saved = null;
|
|
828
|
+
if (journal !== undefined) {
|
|
829
|
+
const valid = (round, target) => round !== undefined && round.target === target && typeof round.sessionId === "string" && round.sessionId.length > 0 &&
|
|
830
|
+
validateReviewReport(round.report, expectedCriterionIds, request.invocation.scope?.changedFiles).ok;
|
|
831
|
+
try {
|
|
832
|
+
saved = await journal.load(binding);
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
report("checkpoint: the saved review checkpoint is unreadable or altered; stopping review");
|
|
836
|
+
return { status: "NOT_RUN", reason: "checkpoint-invalid", attempts: [] };
|
|
837
|
+
}
|
|
838
|
+
if (saved !== null && (saved.binding !== binding || !valid(saved.primary, primaryTarget) ||
|
|
839
|
+
reviewReportStatus(saved.primary.report) !== "PASS" ||
|
|
840
|
+
(saved.adversarial !== undefined && !valid(saved.adversarial, adversarialTarget)))) {
|
|
841
|
+
report("checkpoint: the saved review checkpoint does not match this review; stopping review");
|
|
842
|
+
return { status: "NOT_RUN", reason: "checkpoint-invalid", attempts: [] };
|
|
843
|
+
}
|
|
844
|
+
if (saved?.adversarial !== undefined && reviewReportStatus(saved.adversarial.report) === "FAIL")
|
|
845
|
+
saved = null;
|
|
846
|
+
if (saved?.adversarial !== undefined) {
|
|
847
|
+
report("checkpoint: both rounds already answered for this exact review; no reviewer is called");
|
|
848
|
+
const { primary, adversarial } = saved;
|
|
849
|
+
return {
|
|
850
|
+
status: "PASS",
|
|
851
|
+
results: reviewReportResults(primary.report),
|
|
852
|
+
report: primary.report,
|
|
853
|
+
harness: primaryTarget,
|
|
854
|
+
attempts: [
|
|
855
|
+
{ target: primaryTarget, status: "PASS", sessionId: primary.sessionId, metrics: primary.metrics },
|
|
856
|
+
{ target: adversarialTarget, status: "PASS", sessionId: adversarial.sessionId, metrics: adversarial.metrics }
|
|
857
|
+
],
|
|
858
|
+
preflight: [],
|
|
859
|
+
independence: primaryTarget === adversarialTarget ? "same-target" : "different-target",
|
|
860
|
+
sessionIsolation: "fresh",
|
|
861
|
+
adversarial: { target: adversarialTarget, refuted: false, report: adversarial.report }
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const signalTransient = async (outcome) => {
|
|
866
|
+
if (outcome.kind === "skip" && outcome.transient === true)
|
|
867
|
+
await journal?.transient().catch(() => { });
|
|
868
|
+
};
|
|
820
869
|
const preflight = [];
|
|
821
870
|
if (options.preflightTarget !== undefined) {
|
|
822
|
-
|
|
871
|
+
// A resumed primary needs no probe: its round is not run again.
|
|
872
|
+
for (const target of new Set(saved === null ? plannedTargets : [adversarialTarget])) {
|
|
823
873
|
if (outOfTime()) {
|
|
824
874
|
const diagnostic = "the review deadline was reached during target " +
|
|
825
875
|
"preflight";
|
|
@@ -859,7 +909,6 @@ export function createReviewExecutor(options) {
|
|
|
859
909
|
};
|
|
860
910
|
}
|
|
861
911
|
}
|
|
862
|
-
const expectedCriterionIds = request.invocation.packet.criteria.map((criterion) => criterion.id);
|
|
863
912
|
const repositoryRoot = await realpath(options.cwd);
|
|
864
913
|
const shared = {
|
|
865
914
|
repositoryRoot,
|
|
@@ -874,20 +923,26 @@ export function createReviewExecutor(options) {
|
|
|
874
923
|
: {})
|
|
875
924
|
};
|
|
876
925
|
const attempts = [];
|
|
877
|
-
const primaryTarget = plannedTargets[0];
|
|
878
|
-
const adversarialTarget = plannedTargets[1];
|
|
879
926
|
if (outOfTime()) {
|
|
880
927
|
report("chain: review deadline reached before the first round");
|
|
881
928
|
return { status: "NOT_RUN", reason: "timeout", attempts: [] };
|
|
882
929
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
930
|
+
let primaryOutcome;
|
|
931
|
+
if (saved !== null) {
|
|
932
|
+
report(`${primaryTarget}: necessary review already passed for this exact review; resuming at the adversarial round`);
|
|
933
|
+
primaryOutcome = { kind: "verdict", report: saved.primary.report, sessionId: saved.primary.sessionId, metrics: saved.primary.metrics };
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
announceRound(options, 1, primaryTarget, plannedTargets);
|
|
937
|
+
primaryOutcome = await attemptTargetWithRetry({
|
|
938
|
+
...shared,
|
|
939
|
+
target: primaryTarget,
|
|
940
|
+
label: `${primaryTarget}-primary`,
|
|
941
|
+
prompt: primaryPrompt
|
|
942
|
+
}, staged());
|
|
943
|
+
}
|
|
890
944
|
if (primaryOutcome.kind === "skip") {
|
|
945
|
+
await signalTransient(primaryOutcome);
|
|
891
946
|
attempts.push({
|
|
892
947
|
target: primaryTarget,
|
|
893
948
|
status: "NOT_RUN",
|
|
@@ -949,6 +1004,17 @@ export function createReviewExecutor(options) {
|
|
|
949
1004
|
};
|
|
950
1005
|
}
|
|
951
1006
|
}
|
|
1007
|
+
if (journal !== undefined && saved === null) {
|
|
1008
|
+
try {
|
|
1009
|
+
await journal.save({ binding, primary: { target: primaryTarget, sessionId: primaryOutcome.sessionId,
|
|
1010
|
+
report: redactReviewReport(primaryReport), metrics: primaryOutcome.metrics } });
|
|
1011
|
+
}
|
|
1012
|
+
catch {
|
|
1013
|
+
report("checkpoint: the necessary review PASS could not be saved; stopping before the adversarial round");
|
|
1014
|
+
return { status: "NOT_RUN", reason: "evidence-write-failed", report: primaryReport, preflight,
|
|
1015
|
+
independence, sessionIsolation: "fresh", attempts };
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
952
1018
|
const adversarialPrompt = buildAdversarialPrompt({ ...request.invocation, harness: adversarialTarget }, primaryReport);
|
|
953
1019
|
if (Buffer.byteLength(adversarialPrompt, "utf8") > MAX_ADVERSARIAL_PROMPT_BYTES) {
|
|
954
1020
|
const diagnostic = `the redacted primary report exceeds ${MAX_ADVERSARIAL_PROMPT_BYTES} bytes`;
|
|
@@ -998,6 +1064,7 @@ export function createReviewExecutor(options) {
|
|
|
998
1064
|
prompt: adversarialPrompt
|
|
999
1065
|
}, staged());
|
|
1000
1066
|
if (adversarialOutcome.kind === "skip") {
|
|
1067
|
+
await signalTransient(adversarialOutcome);
|
|
1001
1068
|
attempts.push({
|
|
1002
1069
|
target: adversarialTarget,
|
|
1003
1070
|
status: "NOT_RUN",
|
|
@@ -1039,6 +1106,18 @@ export function createReviewExecutor(options) {
|
|
|
1039
1106
|
attempts
|
|
1040
1107
|
};
|
|
1041
1108
|
}
|
|
1109
|
+
if (journal !== undefined) {
|
|
1110
|
+
try {
|
|
1111
|
+
await journal.save({ binding, primary: { target: primaryTarget, sessionId: primaryOutcome.sessionId,
|
|
1112
|
+
report: redactReviewReport(primaryReport), metrics: primaryOutcome.metrics }, adversarial: { target: adversarialTarget,
|
|
1113
|
+
sessionId: adversarialOutcome.sessionId, report: redactReviewReport(adversarialReport), metrics: adversarialOutcome.metrics } });
|
|
1114
|
+
}
|
|
1115
|
+
catch {
|
|
1116
|
+
report("checkpoint: the answered pair could not be saved; its verdict is not accepted");
|
|
1117
|
+
return { status: "NOT_RUN", reason: "evidence-write-failed", report: primaryReport, preflight,
|
|
1118
|
+
independence, sessionIsolation: "fresh", attempts };
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1042
1121
|
report(`${adversarialTarget}: adversarial re-check ${refuted ? "refuted the PASS" : "upheld the PASS"}`);
|
|
1043
1122
|
return {
|
|
1044
1123
|
status: refuted ? "FAIL" : "PASS",
|
|
@@ -121,6 +121,10 @@ export function renderReviewResult(result) {
|
|
|
121
121
|
lines.push("Three configured targets require AGENT_OPS_HOST=agy|claude|codex; " +
|
|
122
122
|
"the host identity was not guessed.");
|
|
123
123
|
}
|
|
124
|
+
if (result.reason === "checkpoint-invalid") {
|
|
125
|
+
lines.push("The saved final-proof review checkpoint is unreadable or does not match " +
|
|
126
|
+
"this review, so no round was resumed or repeated automatically.");
|
|
127
|
+
}
|
|
124
128
|
if (result.reason === "evidence-write-failed") {
|
|
125
129
|
lines.push("Reviewers may have completed, but the required evidence could not be " +
|
|
126
130
|
"written; the completion gate remains blocked.");
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { sha256 } from "../fs/hash.js";
|
|
5
|
+
import { AgentOpsError } from "../fs/paths.js";
|
|
6
|
+
import { readPrivateFile, writePrivateFile } from "../security/permissions.js";
|
|
7
|
+
const MAX_CHECKPOINT_BYTES = 4 * 1024 * 1024;
|
|
8
|
+
const BINDING = /^[a-f0-9]{64}$/u;
|
|
9
|
+
/** Matches the run state validator; the oldest record is dropped first. */
|
|
10
|
+
const MAX_CHECKPOINTS = 64;
|
|
11
|
+
/** One immutable final attempt: a new candidate, target, contract or policy starts a new allowance. */
|
|
12
|
+
export function finalProofAttemptKey(parts) {
|
|
13
|
+
return sha256(JSON.stringify([parts.candidate, parts.target, parts.contractHash, parts.configHash, parts.runtimeHash]));
|
|
14
|
+
}
|
|
15
|
+
function checkpointDirectory(state) {
|
|
16
|
+
return join(state.commonDir, "agent-ops", "runs", state.runId, "final-review");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Saved rounds live beside the run state, and each file is trusted only
|
|
20
|
+
* through the digest the proof recorded in the locked run state, like run
|
|
21
|
+
* answers and plans: a file nobody recorded is ignored, so a writer that can
|
|
22
|
+
* reach the directory cannot plant a PASS. `transient` marks the same record.
|
|
23
|
+
*/
|
|
24
|
+
export function runReviewCheckpoints(repository, state) {
|
|
25
|
+
const path = (binding) => {
|
|
26
|
+
if (!BINDING.test(binding))
|
|
27
|
+
throw new AgentOpsError("RUN_REVIEW_CHECKPOINT_INVALID", "Checkpoint binding must be a sha256 digest.");
|
|
28
|
+
return join(checkpointDirectory(state), binding + ".json");
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
async load(binding) {
|
|
32
|
+
const file = path(binding);
|
|
33
|
+
const recorded = (await repository.read(state.runId))?.finalProof?.checkpoints?.[binding];
|
|
34
|
+
if (recorded === undefined)
|
|
35
|
+
return null;
|
|
36
|
+
const source = await readPrivateFile(file, state.commonDir);
|
|
37
|
+
const saved = source === null || Buffer.byteLength(source) > MAX_CHECKPOINT_BYTES || sha256(source) !== recorded ? null
|
|
38
|
+
: JSON.parse(source);
|
|
39
|
+
if (saved === null || typeof saved !== "object" || saved.schemaVersion !== 1 ||
|
|
40
|
+
typeof saved.checkpoint !== "object" || saved.checkpoint === null)
|
|
41
|
+
throw new AgentOpsError("RUN_REVIEW_CHECKPOINT_INVALID", "Saved review checkpoint is missing, altered or does not match its run state record.");
|
|
42
|
+
return saved.checkpoint;
|
|
43
|
+
},
|
|
44
|
+
async save(checkpoint) {
|
|
45
|
+
const file = path(checkpoint.binding);
|
|
46
|
+
const content = JSON.stringify({ schemaVersion: 1, checkpoint });
|
|
47
|
+
if (Buffer.byteLength(content) > MAX_CHECKPOINT_BYTES)
|
|
48
|
+
throw new AgentOpsError("RUN_REVIEW_CHECKPOINT_TOO_LARGE", "Review checkpoint exceeds 4 MiB.");
|
|
49
|
+
await writePrivateFile(file, content, state.commonDir);
|
|
50
|
+
await repository.mutate(state.runId, current => {
|
|
51
|
+
if (current.finalProof == null)
|
|
52
|
+
throw new AgentOpsError("RUN_REVIEW_CHECKPOINT_UNBOUND", "A review checkpoint needs the run's final-proof record.");
|
|
53
|
+
const kept = Object.entries(current.finalProof.checkpoints ?? {}).filter(([binding]) => binding !== checkpoint.binding)
|
|
54
|
+
.slice(-(MAX_CHECKPOINTS - 1));
|
|
55
|
+
return { ...current, finalProof: { ...current.finalProof,
|
|
56
|
+
checkpoints: Object.fromEntries([...kept, [checkpoint.binding, sha256(content)]]) } };
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
async transient() {
|
|
60
|
+
await repository.mutate(state.runId, current => current.finalProof == null ? current
|
|
61
|
+
: { ...current, finalProof: { ...current.finalProof, reviewTransient: true } });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** A delivered run keeps no resumable review rounds. */
|
|
66
|
+
export async function discardReviewCheckpoints(state) {
|
|
67
|
+
await rm(checkpointDirectory(state), { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
/** Starts a proof step for `attemptKey`. The same key keeps its spent allowance; restarts never refund it. */
|
|
70
|
+
export async function beginFinalProof(repository, runId, attemptKey) {
|
|
71
|
+
const state = await repository.mutate(runId, current => ({ ...current, finalProof: current.finalProof?.attemptKey === attemptKey
|
|
72
|
+
? { ...current.finalProof, pending: true, reviewTransient: false }
|
|
73
|
+
: { attemptKey, recoveries: 0, pending: true, reviewTransient: false, failures: [], stopped: false } }));
|
|
74
|
+
return state.finalProof;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Records a failed or interrupted proof step and decides under the run lock
|
|
78
|
+
* whether the one automatic recovery is spent on it. Only an eligible failure
|
|
79
|
+
* of the same attempt in an active run consumes it; anything else stops
|
|
80
|
+
* automatic recovery for this attempt. The first failure is never overwritten.
|
|
81
|
+
*/
|
|
82
|
+
export async function settleFinalProofFailure(repository, runId, attemptKey, code, eligible) {
|
|
83
|
+
let decision = "stop";
|
|
84
|
+
await repository.mutate(runId, current => {
|
|
85
|
+
const record = current.finalProof;
|
|
86
|
+
if (record == null || record.attemptKey !== attemptKey)
|
|
87
|
+
return current;
|
|
88
|
+
const replay = eligible && record.recoveries === 0 && !record.stopped && current.status === "active" && !current.disableRestart;
|
|
89
|
+
decision = replay ? "replay" : "stop";
|
|
90
|
+
return { ...current, finalProof: { ...record, recoveries: replay ? 1 : record.recoveries, pending: replay, reviewTransient: false,
|
|
91
|
+
stopped: !replay, failures: [...record.failures.slice(0, 1), { code: code.slice(0, 128), at: new Date().toISOString() }] } };
|
|
92
|
+
});
|
|
93
|
+
return decision;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A controlled step that ended without a complete result. Replayable only when
|
|
97
|
+
* no stop, deadline, limit or spawn failure ended it and nothing of its
|
|
98
|
+
* process group is still running.
|
|
99
|
+
*/
|
|
100
|
+
export class InterruptedStepError extends AgentOpsError {
|
|
101
|
+
replayable;
|
|
102
|
+
constructor(message, replayable) {
|
|
103
|
+
super("RUN_FINAL_PROOF_INTERRUPTED", message);
|
|
104
|
+
this.replayable = replayable;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Coordinator final proof with at most one automatic recovery per immutable
|
|
109
|
+
* attempt. `restartedKey` resumes a proof a previous supervisor left pending;
|
|
110
|
+
* the crash itself is the failure that may spend the allowance.
|
|
111
|
+
*/
|
|
112
|
+
export async function runFinalProof(repository, runId, steps, restartedKey) {
|
|
113
|
+
const key = restartedKey ?? await steps.attemptKey();
|
|
114
|
+
const replay = async (code, replayable) => {
|
|
115
|
+
let eligible = false;
|
|
116
|
+
try {
|
|
117
|
+
eligible = replayable && await steps.attemptKey() === key && await steps.untouched();
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
eligible = false;
|
|
121
|
+
}
|
|
122
|
+
const replaying = await settleFinalProofFailure(repository, runId, key, code, eligible) === "replay";
|
|
123
|
+
const latest = (await repository.read(runId));
|
|
124
|
+
await repository.appendEvent(runId, { type: "diagnostic", code: replaying ? "RUN_FINAL_PROOF_RECOVERING" : "RUN_FINAL_PROOF_RECOVERY_STOPPED",
|
|
125
|
+
workerId: latest.coordinatorId, taskId: latest.rootTaskId, detail: replaying
|
|
126
|
+
? `${code}: replaying final proof once with no writer started; a valid saved primary review and current verification are reused.`
|
|
127
|
+
: `${code}: automatic recovery ${replayable ? "is spent or no longer applies to this candidate" : "does not apply to this failure"}; the checkout is preserved.` });
|
|
128
|
+
return replaying;
|
|
129
|
+
};
|
|
130
|
+
if (restartedKey === undefined)
|
|
131
|
+
await beginFinalProof(repository, runId, key);
|
|
132
|
+
else if (!await replay("RUN_SUPERVISOR_RESTARTED", true))
|
|
133
|
+
throw new AgentOpsError("RUN_FINAL_PROOF_INTERRUPTED", "Final proof was interrupted by a supervisor restart and cannot be replayed automatically.");
|
|
134
|
+
for (;;) {
|
|
135
|
+
let result;
|
|
136
|
+
let failure;
|
|
137
|
+
try {
|
|
138
|
+
result = await steps.advance();
|
|
139
|
+
if (result.status === "ok")
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
catch (cause) {
|
|
143
|
+
failure = cause;
|
|
144
|
+
}
|
|
145
|
+
const replayable = failure === undefined ? (await repository.read(runId)).finalProof?.reviewTransient === true
|
|
146
|
+
: failure instanceof InterruptedStepError && failure.replayable;
|
|
147
|
+
const code = failure instanceof AgentOpsError ? failure.code : result?.code ?? "RUN_FINAL_PROOF_FAILED";
|
|
148
|
+
if (await replay(code, replayable))
|
|
149
|
+
continue;
|
|
150
|
+
if (failure !== undefined)
|
|
151
|
+
throw failure;
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Whether any process still belongs to the group: a proof group can outlive
|
|
157
|
+
* its leader. An unreadable process table counts as alive.
|
|
158
|
+
*/
|
|
159
|
+
export function processGroupAlive(group) {
|
|
160
|
+
try {
|
|
161
|
+
return execFileSync("ps", ["-axo", "pgid="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
|
|
162
|
+
.split("\n").some(line => Number(line.trim()) === group);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -159,6 +159,15 @@ export function assertRunState(value) {
|
|
|
159
159
|
state.landing.processId < 1 || typeof state.landing.hostname !== "string" || state.landing.hostname.length > 256 ||
|
|
160
160
|
!Number.isFinite(Date.parse(String(state.landing.startedAt)))))
|
|
161
161
|
return invalid("Invalid run landing marker.");
|
|
162
|
+
if (state.finalProof != null && (!plain(state.finalProof) || !SHA256.test(String(state.finalProof.attemptKey)) ||
|
|
163
|
+
![0, 1].includes(state.finalProof.recoveries) || typeof state.finalProof.pending !== "boolean" ||
|
|
164
|
+
typeof state.finalProof.reviewTransient !== "boolean" || typeof state.finalProof.stopped !== "boolean" ||
|
|
165
|
+
!boundedArray(state.finalProof.failures, 2) || state.finalProof.failures.some(f => !plain(f) ||
|
|
166
|
+
typeof f.code !== "string" || f.code.length === 0 || f.code.length > 128 || !Number.isFinite(Date.parse(String(f.at)))) ||
|
|
167
|
+
(state.finalProof.checkpoints !== undefined && (!plain(state.finalProof.checkpoints) ||
|
|
168
|
+
Object.keys(state.finalProof.checkpoints).length > 64 ||
|
|
169
|
+
Object.entries(state.finalProof.checkpoints).some(([binding, digest]) => !SHA256.test(binding) || !SHA256.test(String(digest)))))))
|
|
170
|
+
return invalid("Invalid final proof recovery record.");
|
|
162
171
|
if (state.proofProcess != null && (!plain(state.proofProcess) || !Number.isSafeInteger(state.proofProcess.processId) ||
|
|
163
172
|
state.proofProcess.processId < 1 || typeof state.proofProcess.processIdentity !== "string" ||
|
|
164
173
|
state.proofProcess.processIdentity.length === 0 || state.proofProcess.processIdentity.length > 256))
|
package/docs/en/spec/review.md
CHANGED
|
@@ -280,3 +280,16 @@ A batch result MUST report every task's outcome and MUST exit 0 only when every
|
|
|
280
280
|
- Evidence: The envelope code and the exit code agree with the per-task outcomes.
|
|
281
281
|
- Positive: `One FAIL and one NOT_RUN exit 1, so the defect is handled first.`
|
|
282
282
|
- Negative: `Exit 0 while a task is still NOT_RUN.`
|
|
283
|
+
|
|
284
|
+
## REVIEW-RESUME-001
|
|
285
|
+
|
|
286
|
+
A native run's coordinator final proof MUST NOT pay again for a review round
|
|
287
|
+
that already answered the exact same review, and MUST retry automatically at
|
|
288
|
+
most once per immutable final attempt.
|
|
289
|
+
|
|
290
|
+
- Trigger: A run's final proof is interrupted: its proof process group was killed or returned a broken result (not by a stop, the run deadline, the output limit or a spawn failure, and with no process of the group left), its review chain ended on a transient attempt (a dropped call or a lost agy turn), or the supervisor restarted with the proof pending.
|
|
291
|
+
- Action: Inside the supervisor's registered proof group, review saves the necessary PASS after the source recheck and before the adversarial round, then the answered pair, under the run's private directory, bound to the primary prompt, the planned pair, model and effort, with each file's digest recorded in the locked run state like run answers and plans: a file without that record is ignored, so a planted PASS pair cannot skip review; the reviewer's in-process retry is off there. The supervisor keys each attempt by candidate, target, tree contract and policy config and runtime, and spends its one recovery only while the coordinator checkout is clean, no integration transaction exists and the run is active. The replay starts no writer, resumes a saved PASS with a fresh adversarial session, rebuilds a saved PASS pair without any model call, and still needs the standard attestation, finish and receipt. `task advance`, for every caller, skips `verify` for a task whose passing evidence still holds for the candidate by the rule review's preflight applies.
|
|
292
|
+
- Evidence: `run status` prints `Final proof recovery: <n>/1 used; first failure …; last failure …; automatic recovery stopped|in progress|available`, and the run events carry `RUN_FINAL_PROOF_RECOVERING` or `RUN_FINAL_PROOF_RECOVERY_STOPPED`. A recorded checkpoint that is corrupt, missing or mismatched ends the review NOT_RUN `checkpoint-invalid` without calling any reviewer. A refuted pair is never resumed. Checkpoints are deleted once the run's receipt is accepted.
|
|
293
|
+
- Positive: `The second round lost its connection; the supervisor replayed once, the necessary reviewer was not called again, and the run finished with a receipt.`
|
|
294
|
+
- Negative: `Retry until the adversarial reviewer passes, or replay after a stop, a timeout or a substantive FAIL.`
|
|
295
|
+
- Note: Standalone `review`, `batch` and child handoff keep their in-process retry and never read or write checkpoints. An uncommitted coordinator checkout is never replayed; it is left untouched for the existing repair.
|
|
@@ -250,3 +250,14 @@ permission bypass。外部 host runner 不可用時,結果 MUST 保持 NOT_RUN
|
|
|
250
250
|
- Evidence: envelope code 與 exit code 與各 task 的結果一致。
|
|
251
251
|
- Positive: `一個 FAIL 與一個 NOT_RUN 以 1 結束,先處理缺陷。`
|
|
252
252
|
- Negative: `仍有 task 是 NOT_RUN 卻以 0 結束。`
|
|
253
|
+
|
|
254
|
+
## REVIEW-RESUME-001
|
|
255
|
+
|
|
256
|
+
native run 的 coordinator 最終驗收 MUST NOT 為同一份審查中已經回答過的輪次再付一次費用,且每個不可變的最終驗收 attempt MUST 至多自動重試一次。
|
|
257
|
+
|
|
258
|
+
- Trigger: run 的最終驗收中斷:proof process group 被終止或回傳損壞的結果(不是 stop、run 期限、輸出上限或啟動失敗造成,且該 group 已沒有任何程序),審查鏈以 transient attempt(連線中斷或 agy 遺失回合)結束,或 supervisor 在驗收 pending 時重啟。
|
|
259
|
+
- Action: 在 supervisor 已註冊的 proof group 內,審查於來源再檢查之後、adversarial 輪之前保存必要 PASS,之後保存已回答的雙輪,存於 run 的私有目錄,綁定 primary prompt、規劃的雙 target、model 與 effort,並像 run 的 answers 與 plans 一樣把每個檔案的 digest 記在加鎖的 run state:沒有紀錄的檔案一律忽略,植入的 PASS 雙輪無法略過審查;此情境關閉 reviewer 的程序內 retry。supervisor 以 candidate、target、tree contract、policy config 與 runtime 作為 attempt key,只在 coordinator checkout 乾淨、沒有 integration transaction 且 run 仍 active 時花掉那一次恢復。重放不啟動任何 writer,以全新 adversarial session 接續已保存的 PASS,已保存的 PASS 雙輪不呼叫任何模型直接重建,且仍須標準 attestation、finish 與 receipt。`task advance` 對所有呼叫者,若某 task 的 PASS 證據依審查 preflight 的同一規則對 candidate 仍有效,就略過 `verify`。
|
|
260
|
+
- Evidence: `run status` 顯示 `Final proof recovery: <n>/1 used; first failure …; last failure …; automatic recovery stopped|in progress|available`,run 事件記錄 `RUN_FINAL_PROOF_RECOVERING` 或 `RUN_FINAL_PROOF_RECOVERY_STOPPED`。已記錄但損壞、遺失或不相符的 checkpoint 使審查以 NOT_RUN `checkpoint-invalid` 結束,且不呼叫任何 reviewer。被反駁的雙輪永不續用。run 的 receipt 被接受後即刪除 checkpoint。
|
|
261
|
+
- Positive: `第二輪連線中斷;supervisor 重放一次,必要 reviewer 沒有再被呼叫,run 以 receipt 完成。`
|
|
262
|
+
- Negative: `一直重試直到 adversarial reviewer 通過,或在 stop、timeout、實質 FAIL 之後重放。`
|
|
263
|
+
- Note: standalone `review`、`batch` 與 child handoff 保留程序內 retry,永不讀寫 checkpoint。有未提交修改的 coordinator checkout 永不重放,原樣保留給既有 repair 處理。
|
package/package.json
CHANGED