@kylecheng3146/agent-ops 0.9.0 → 0.10.2
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/README.md +3 -2
- package/dist/packages/cli/src/args.js +58 -14
- package/dist/packages/cli/src/bin.js +10 -3
- package/dist/packages/cli/src/cli.js +30 -6
- package/dist/packages/cli/src/commands/advance.js +2 -1
- package/dist/packages/cli/src/commands/escapes.js +109 -0
- package/dist/packages/cli/src/commands/parallel.js +1 -1
- package/dist/packages/cli/src/commands/review-show.js +1 -1
- package/dist/packages/cli/src/commands/review.js +63 -11
- package/dist/packages/cli/src/commands/run.js +10 -3
- package/dist/packages/cli/src/commands/task.js +41 -3
- package/dist/packages/cli/src/commands/update.js +16 -4
- package/dist/packages/cli/src/commands/verify.js +4 -1
- package/dist/packages/cli/src/context.js +22 -4
- package/dist/packages/cli/src/debug-intake.js +1 -1
- package/dist/packages/cli/src/office-entry.js +8 -4
- package/dist/packages/cli/src/owned-run-step.js +4 -4
- package/dist/packages/cli/src/parallel-deps.js +0 -2
- package/dist/packages/cli/src/run-deps.js +12 -9
- package/dist/packages/cli/src/run-entry.js +76 -17
- package/dist/packages/cli/src/run-setup-entry.js +2 -2
- package/dist/runtime/src/escape/link.js +317 -0
- package/dist/runtime/src/escape/origin.js +20 -0
- package/dist/runtime/src/escape/rules.js +252 -0
- package/dist/runtime/src/fs/temporary.js +10 -0
- package/dist/runtime/src/install/harness.js +7 -4
- package/dist/runtime/src/logging/local-log.js +44 -1
- package/dist/runtime/src/office/scene.js +1 -1
- package/dist/runtime/src/office/server.js +43 -16
- package/dist/runtime/src/office/snapshot.js +1 -1
- package/dist/runtime/src/parallel/finish.js +139 -65
- package/dist/runtime/src/parallel/manage.js +51 -1
- package/dist/runtime/src/parallel/receipt.js +3 -1
- package/dist/runtime/src/parallel/service.js +5 -1
- package/dist/runtime/src/review/execute.js +20 -12
- package/dist/runtime/src/review/packet.js +20 -7
- package/dist/runtime/src/review/probe.js +3 -2
- package/dist/runtime/src/review/runner.js +26 -3
- package/dist/runtime/src/review/slots.js +1 -1
- package/dist/runtime/src/run/controls.js +99 -1
- package/dist/runtime/src/run/hosts/codex.js +28 -1
- package/dist/runtime/src/run/hosts/protocol.js +5 -2
- package/dist/runtime/src/run/integration.js +35 -13
- package/dist/runtime/src/run/policy.js +65 -10
- package/dist/runtime/src/run/scheduler.js +10 -5
- package/dist/runtime/src/run/service.js +68 -21
- package/dist/runtime/src/run/supervisor.js +11 -2
- package/dist/runtime/src/run/transport.js +1 -0
- package/dist/runtime/src/schema/validate.js +6 -1
- package/dist/runtime/src/security/permissions.js +85 -0
- package/dist/runtime/src/surface/discovery.js +79 -2
- package/dist/runtime/src/surface/schema.js +43 -2
- package/dist/runtime/src/task/completion.js +20 -0
- package/dist/runtime/src/task/contract.js +3 -1
- package/dist/runtime/src/task/debug-state.js +9 -3
- package/dist/runtime/src/task/debug-user-event.js +82 -17
- package/dist/runtime/src/task/debug.js +59 -16
- package/dist/runtime/src/task/service.js +58 -6
- package/dist/runtime/src/verify/acceptance-replay.js +24 -11
- package/docs/en/guides/configuration.md +24 -5
- package/docs/en/guides/task-debug.md +26 -2
- package/docs/en/spec/acceptance-and-evidence.md +104 -2
- package/docs/en/spec/harness-adapters.md +26 -1
- package/docs/en/spec/loop-engineering.md +34 -6
- package/docs/en/spec/review.md +6 -0
- package/docs/zh-TW/guides/configuration.md +20 -4
- package/docs/zh-TW/guides/task-debug.md +19 -2
- package/docs/zh-TW/spec/acceptance-and-evidence.md +84 -4
- package/docs/zh-TW/spec/harness-adapters.md +21 -1
- package/docs/zh-TW/spec/loop-engineering.md +9 -3
- package/docs/zh-TW/spec/review.md +5 -0
- package/package.json +1 -1
- package/schemas/task.schema.json +8 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ timeouts produce UNKNOWN. Review examines the checks themselves and every fallba
|
|
|
32
32
|
On macOS, a trusted clean main checkout can start an isolated native goal run:
|
|
33
33
|
|
|
34
34
|
```sh
|
|
35
|
-
agent-ops run "Implement the requested behavior" --host codex --
|
|
35
|
+
agent-ops run "Implement the requested behavior" --host codex --jobs 2
|
|
36
36
|
agent-ops run status <run-id>
|
|
37
37
|
agent-ops run logs <run-id>
|
|
38
38
|
agent-ops run respond <run-id> --question-id <id> --answer "Clarification"
|
|
@@ -49,7 +49,8 @@ verification, two fresh reviews, completed tasks and a receipt bound to the targ
|
|
|
49
49
|
Setup runs in the background under registered process groups. Stop cancels native
|
|
50
50
|
writers, local verification and setup; interrupted setup requires explicit resume.
|
|
51
51
|
The coordinator can assess additional non-dangerous fixed commands through a
|
|
52
|
-
policy request.
|
|
52
|
+
policy request. New runs have no total time cap; manual resume upgrades legacy finite runs while keeping accounting.
|
|
53
|
+
This finite authorization is bound to the run, runtime and lease;
|
|
53
54
|
it creates no permanent repository trust. Policy transitions fence writers and
|
|
54
55
|
journal delivery imports and contract synchronization before starting new leases.
|
|
55
56
|
Native authorization decisions retain redacted command scope and rationale;
|
|
@@ -13,7 +13,8 @@ export const COMMAND_NAMES = [
|
|
|
13
13
|
"allow-stop",
|
|
14
14
|
"worktree",
|
|
15
15
|
"run",
|
|
16
|
-
"office"
|
|
16
|
+
"office",
|
|
17
|
+
"escapes"
|
|
17
18
|
];
|
|
18
19
|
const COMMAND_SET = new Set(COMMAND_NAMES);
|
|
19
20
|
const SCOPES = new Set(["project", "user"]);
|
|
@@ -91,6 +92,11 @@ export function parseArgs(argv) {
|
|
|
91
92
|
const criterionFiles = [];
|
|
92
93
|
let criteriaFile;
|
|
93
94
|
let surfaceFile;
|
|
95
|
+
let escapeOf;
|
|
96
|
+
let escapeLabel;
|
|
97
|
+
let escapeReceipt;
|
|
98
|
+
let escapeFix;
|
|
99
|
+
let escapePromote;
|
|
94
100
|
let planFile;
|
|
95
101
|
let expectedContract;
|
|
96
102
|
let expectedTreeContract;
|
|
@@ -231,7 +237,7 @@ export function parseArgs(argv) {
|
|
|
231
237
|
if (debugAction !== undefined)
|
|
232
238
|
duplicate(token);
|
|
233
239
|
const value = readOptionValue(argv, index++, token);
|
|
234
|
-
if (!["status", "hypothesis", "strategy", "investigate", "checkpoint", "resume", "extend"].includes(value))
|
|
240
|
+
if (!["status", "hypothesis", "strategy", "investigate", "checkpoint", "resume", "extend", "origin"].includes(value))
|
|
235
241
|
invalidValue(token, value);
|
|
236
242
|
debugAction = value;
|
|
237
243
|
break;
|
|
@@ -270,6 +276,31 @@ export function parseArgs(argv) {
|
|
|
270
276
|
duplicate(token);
|
|
271
277
|
surfaceFile = readOptionValue(argv, index++, token);
|
|
272
278
|
break;
|
|
279
|
+
case "--escape-of":
|
|
280
|
+
if (escapeOf !== undefined)
|
|
281
|
+
duplicate(token);
|
|
282
|
+
escapeOf = readOptionValue(argv, index++, token);
|
|
283
|
+
break;
|
|
284
|
+
case "--escape-label":
|
|
285
|
+
if (escapeLabel !== undefined)
|
|
286
|
+
duplicate(token);
|
|
287
|
+
escapeLabel = readOptionValue(argv, index++, token);
|
|
288
|
+
break;
|
|
289
|
+
case "--escape-receipt":
|
|
290
|
+
if (escapeReceipt !== undefined)
|
|
291
|
+
duplicate(token);
|
|
292
|
+
escapeReceipt = readOptionValue(argv, index++, token);
|
|
293
|
+
break;
|
|
294
|
+
case "--escape-fix":
|
|
295
|
+
if (escapeFix !== undefined)
|
|
296
|
+
duplicate(token);
|
|
297
|
+
escapeFix = readOptionValue(argv, index++, token);
|
|
298
|
+
break;
|
|
299
|
+
case "--escape-promote":
|
|
300
|
+
if (escapePromote !== undefined)
|
|
301
|
+
duplicate(token);
|
|
302
|
+
escapePromote = readOptionValue(argv, index++, token);
|
|
303
|
+
break;
|
|
273
304
|
case "--plan-file":
|
|
274
305
|
if (planFile !== undefined)
|
|
275
306
|
duplicate(token);
|
|
@@ -687,7 +718,7 @@ export function parseArgs(argv) {
|
|
|
687
718
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Debug operation options apply only to task debug.");
|
|
688
719
|
if (command === "task" && action === "debug" && (taskId === undefined || debugAction === undefined || title !== undefined || intent !== undefined ||
|
|
689
720
|
parentTaskId !== undefined || criteria.length > 0 || evidence.length > 0 ||
|
|
690
|
-
((debugAction === "hypothesis" || debugAction === "strategy") !== (payloadFile !== undefined)) ||
|
|
721
|
+
((debugAction === "hypothesis" || debugAction === "strategy" || debugAction === "origin") !== (payloadFile !== undefined)) ||
|
|
691
722
|
((debugAction === "investigate") !== (commandId !== undefined)) || ((debugAction === "extend") !== (eventId !== undefined))))
|
|
692
723
|
throw new CliArgumentError("CLI_MISSING_VALUE", "Task debug requires --task, --debug-action and the operation's exact payload/command/event options.");
|
|
693
724
|
if ((criterionFiles.length > 0 && !(command === "task" && (action === "create" || action === "pin-finding"))) ||
|
|
@@ -699,8 +730,16 @@ export function parseArgs(argv) {
|
|
|
699
730
|
(reason !== undefined && !revisionAction) ||
|
|
700
731
|
(baseline !== undefined && !(command === "task" && (action === "create" || action === "revise"))))
|
|
701
732
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Acceptance options are not supported by this command.");
|
|
733
|
+
if ((escapeOf !== undefined || escapeLabel !== undefined || escapeReceipt !== undefined || escapeFix !== undefined || escapePromote !== undefined) &&
|
|
734
|
+
!(command === "task" && (action === "create" || action === "revise")))
|
|
735
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Escape options apply only to task create and revise.");
|
|
736
|
+
if ((escapeOf === undefined) !== (escapeLabel === undefined) ||
|
|
737
|
+
((escapeReceipt !== undefined || escapeFix !== undefined || escapePromote !== undefined) && escapeOf === undefined))
|
|
738
|
+
throw new CliArgumentError("CLI_MISSING_VALUE", "--escape-of needs --escape-label; --escape-label, --escape-receipt, --escape-fix and --escape-promote need --escape-of.");
|
|
739
|
+
if (command === "escapes" && argv.some(token => token !== "escapes" && token !== "--json"))
|
|
740
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "escapes accepts only --json; it is a read-only report.");
|
|
702
741
|
if (revisionAction && (taskId === undefined || evidence.length > 0 || title !== undefined || intent !== undefined || parentTaskId !== undefined || sessionId !== undefined || criteria.length > 0 || base !== undefined ||
|
|
703
|
-
(action === "revise" && (expectedContract === undefined || (criteriaFile === undefined && surfaceFile === undefined) || reason === undefined)) ||
|
|
742
|
+
(action === "revise" && (expectedContract === undefined || (criteriaFile === undefined && surfaceFile === undefined && escapeOf === undefined) || reason === undefined)) ||
|
|
704
743
|
(action === "replan" && (expectedTreeContract === undefined || planFile === undefined || reason === undefined)) ||
|
|
705
744
|
(action === "pin-finding" && (expectedContract === undefined || findingReference === undefined || criterionFiles.length !== 1))))
|
|
706
745
|
throw new CliArgumentError("CLI_MISSING_VALUE", "Acceptance revision requires task, expected contract, payload and reason.");
|
|
@@ -875,6 +914,11 @@ export function parseArgs(argv) {
|
|
|
875
914
|
...(criterionFiles.length === 0 ? {} : { criterionFiles }),
|
|
876
915
|
...(criteriaFile === undefined ? {} : { criteriaFile }),
|
|
877
916
|
...(surfaceFile === undefined ? {} : { surfaceFile }),
|
|
917
|
+
...(escapeOf === undefined ? {} : { escapeOf }),
|
|
918
|
+
...(escapeLabel === undefined ? {} : { escapeLabel }),
|
|
919
|
+
...(escapeReceipt === undefined ? {} : { escapeReceipt }),
|
|
920
|
+
...(escapeFix === undefined ? {} : { escapeFix }),
|
|
921
|
+
...(escapePromote === undefined ? {} : { escapePromote }),
|
|
878
922
|
...(planFile === undefined ? {} : { planFile }),
|
|
879
923
|
...(expectedContract === undefined ? {} : { expectedContract }),
|
|
880
924
|
...(expectedTreeContract === undefined ? {} : { expectedTreeContract }),
|
|
@@ -916,7 +960,7 @@ function parseRunArgs(argv) {
|
|
|
916
960
|
positional = token;
|
|
917
961
|
continue;
|
|
918
962
|
}
|
|
919
|
-
if (seen.has(token))
|
|
963
|
+
if (seen.has(token) && token !== "--retry-approval")
|
|
920
964
|
duplicate(token);
|
|
921
965
|
seen.add(token);
|
|
922
966
|
if (token === "--json") {
|
|
@@ -927,10 +971,15 @@ function parseRunArgs(argv) {
|
|
|
927
971
|
args.wait = true;
|
|
928
972
|
continue;
|
|
929
973
|
}
|
|
930
|
-
if (!["--host", "--goal-file", "--time-budget", "--jobs", "--question-id", "--answer", "--answer-file"].includes(token))
|
|
974
|
+
if (!["--host", "--goal-file", "--time-budget", "--jobs", "--question-id", "--answer", "--answer-file", "--retry-approval"].includes(token))
|
|
931
975
|
throw new CliArgumentError("CLI_UNKNOWN_OPTION", `Unknown run option: ${token}`, token);
|
|
932
976
|
const value = readOptionValue(argv, i++, token);
|
|
933
977
|
switch (token) {
|
|
978
|
+
case "--retry-approval":
|
|
979
|
+
if (!/^auth-[a-f0-9]{64}$/u.test(value) || args.retryApprovalIds?.includes(value))
|
|
980
|
+
invalidValue(token, value);
|
|
981
|
+
args.retryApprovalIds = [...(args.retryApprovalIds ?? []), value];
|
|
982
|
+
break;
|
|
934
983
|
case "--host":
|
|
935
984
|
if (value !== "claude" && value !== "codex")
|
|
936
985
|
invalidValue(token, value);
|
|
@@ -954,17 +1003,12 @@ function parseRunArgs(argv) {
|
|
|
954
1003
|
args.jobs = Number(value);
|
|
955
1004
|
break;
|
|
956
1005
|
case "--time-budget": {
|
|
957
|
-
|
|
958
|
-
if (match === null)
|
|
959
|
-
invalidValue(token, value);
|
|
960
|
-
const ms = Number(match[1]) * ({ s: 1000, m: 60000, h: 3600000 }[match[2]]);
|
|
961
|
-
if (!Number.isSafeInteger(ms) || ms > 86400000)
|
|
962
|
-
invalidValue(token, value);
|
|
963
|
-
args.timeBudgetMs = ms;
|
|
964
|
-
break;
|
|
1006
|
+
throw new CliArgumentError("RUN_TIME_BUDGET_UNSUPPORTED", "New runs have no total time cap; omit --time-budget. Command and review timeouts remain effective.", token);
|
|
965
1007
|
}
|
|
966
1008
|
}
|
|
967
1009
|
}
|
|
1010
|
+
if (args.retryApprovalIds !== undefined && action !== "resume")
|
|
1011
|
+
throw new CliArgumentError("CLI_INVALID_OPTION", "--retry-approval requires run resume.");
|
|
968
1012
|
if (action === "start") {
|
|
969
1013
|
args.goal = positional;
|
|
970
1014
|
if ((args.goal === undefined) === (args.goalFile === undefined))
|
|
@@ -52,14 +52,15 @@ import { runTrustCommand } from "./commands/trust.js";
|
|
|
52
52
|
import { runVerifyCommand } from "./commands/verify.js";
|
|
53
53
|
import { runAdvanceCommand } from "./commands/advance.js";
|
|
54
54
|
import { runReviewShowCommand } from "./commands/review-show.js";
|
|
55
|
+
import { runEscapesCommand } from "./commands/escapes.js";
|
|
55
56
|
import { runAllowStopCommand } from "./commands/allow-stop.js";
|
|
56
57
|
import { CompletionGateService } from "../../../runtime/src/hooks/completion-gate.js";
|
|
57
|
-
import { formatUpdatePlan, runUpdateCommand } from "./commands/update.js";
|
|
58
|
+
import { formatUpdatePlan, runUpdateCommand, worktreeSyncLine } from "./commands/update.js";
|
|
58
59
|
import { errorEnvelope } from "./output.js";
|
|
59
60
|
import { runAgyHeadless } from "./agy-headless.js";
|
|
60
61
|
import { runWorktreeCommand } from "./commands/parallel.js";
|
|
61
62
|
import { ensureSessionWorktree, readWorktreeRecord, resolveCheckouts } from "../../../runtime/src/parallel/service.js";
|
|
62
|
-
import { listWorktrees, branchLockDoctorResult, worktreeDoctorResult } from "../../../runtime/src/parallel/manage.js";
|
|
63
|
+
import { listWorktrees, branchLockDoctorResult, sessionWorktreeNames, syncSessionWorktrees, worktreeDoctorResult } from "../../../runtime/src/parallel/manage.js";
|
|
63
64
|
import { detectGhostFiles, ghostFilesDoctorResult } from "../../../runtime/src/install/doctor.js";
|
|
64
65
|
/** The repository's common Git directory, where display-only office records live; undefined when git cannot say. */
|
|
65
66
|
async function officeCommonDir(dir) {
|
|
@@ -264,6 +265,8 @@ else {
|
|
|
264
265
|
const context = await productionRunContext(root);
|
|
265
266
|
return await runRunCommand({ args, ...context, root: context.mainRoot, office: async () => await ensureBackgroundOffice(root, undefined, openOfficeBrowser) });
|
|
266
267
|
}
|
|
268
|
+
if (args.command === "escapes")
|
|
269
|
+
return await runEscapesCommand({ root });
|
|
267
270
|
if (args.command === "office") {
|
|
268
271
|
return await runOfficeCommand({ ensure: async () => await ensureBackgroundOffice(root, undefined, openOfficeBrowser),
|
|
269
272
|
foreground: async (onUrl) => await serveOffice(root, onUrl), writeStdout: (text) => { process.stdout.write(text); } });
|
|
@@ -422,7 +425,11 @@ else {
|
|
|
422
425
|
: { hookTargets: updateArgs.hookTargets }),
|
|
423
426
|
trustStore: store,
|
|
424
427
|
calculateTrustBinding: async (config) => await plannedTrustBinding(root, config),
|
|
425
|
-
confirm: async (plan, trust) => await confirmPlan(formatUpdatePlan(plan, trust)),
|
|
428
|
+
confirm: async (plan, trust, worktrees) => await confirmPlan(formatUpdatePlan(plan, trust) + worktreeSyncLine(worktrees)),
|
|
429
|
+
sessionWorktrees: {
|
|
430
|
+
list: async () => await sessionWorktreeNames(worktreeDependencies(), root),
|
|
431
|
+
sync: async () => await syncSessionWorktrees(worktreeDependencies(), root)
|
|
432
|
+
},
|
|
426
433
|
promptOffice: async (message, enabled) => await selectYesNo(message, { input: process.stdin, output: process.stdout }, enabled),
|
|
427
434
|
promptAutoRun: async (message, enabled) => await selectYesNo(message, { input: process.stdin, output: process.stdout }, enabled),
|
|
428
435
|
promptWorktree: async (message) => await selectYesNo(message, { input: process.stdin, output: process.stdout }, true),
|
|
@@ -31,6 +31,7 @@ Commands:
|
|
|
31
31
|
review Run an independent review or show recorded reports
|
|
32
32
|
run Start or control a supervised Claude/Codex native goal
|
|
33
33
|
office Open the local pixel office (Preview; opt-in)
|
|
34
|
+
escapes Report escaped bugs, gate catches and learned rules (read-only)
|
|
34
35
|
batch Verify and review a parent task's subtasks together
|
|
35
36
|
allow-stop Grant one fingerprint-bound completion-gate Stop permit (requires --session)
|
|
36
37
|
agy-run Run headless agy with a process-exit completion recheck
|
|
@@ -75,6 +76,14 @@ Options:
|
|
|
75
76
|
* rejected.
|
|
76
77
|
*/
|
|
77
78
|
export const COMMAND_HELP_TEXT = {
|
|
79
|
+
escapes: `Usage: agent-ops escapes [--json]
|
|
80
|
+
|
|
81
|
+
Read-only Escape Loop report: misses per gate from the tracked
|
|
82
|
+
.agent-ops-escapes ledger, gate catches from this machine's local log, origin
|
|
83
|
+
and link-state mix, and per-rule fire/included/excluded counts with retirement
|
|
84
|
+
candidates. Counts are raw with n; fewer than 10 observations are marked
|
|
85
|
+
insufficient-data. It never writes files and fails open.
|
|
86
|
+
`,
|
|
78
87
|
office: `Usage: agent-ops office [--json]
|
|
79
88
|
|
|
80
89
|
Office (Preview) is opt-in: select it during init/update or run
|
|
@@ -89,13 +98,22 @@ browser once; later sessions reuse it. It exits after 10 minutes without active
|
|
|
89
98
|
runs or sessions. Recently completed rooms remain for at most 2 hours.
|
|
90
99
|
The URL carries an access token; do not share it.
|
|
91
100
|
`,
|
|
92
|
-
run: `Usage: agent-ops run [start] "<goal>" --host <claude|codex> [--jobs <1|2>] [--
|
|
101
|
+
run: `Usage: agent-ops run [start] "<goal>" --host <claude|codex> [--jobs <1|2>] [--wait] [--json]
|
|
93
102
|
agent-ops run [start] --goal-file <path> --host <claude|codex>
|
|
94
103
|
agent-ops run <status|logs|resume|stop> <run-id> [--json]
|
|
104
|
+
agent-ops run resume <run-id> [--retry-approval <auth-event-id>]... [--json]
|
|
95
105
|
agent-ops run respond <run-id> --question-id <id> <--answer <text>|--answer-file <path>> [--json]
|
|
96
106
|
|
|
107
|
+
New runs have no total time cap. Manual resume upgrades legacy finite runs and keeps accounting.
|
|
108
|
+
Command/review timeouts, provider limits, DEBUG budgets and stop controls remain effective.
|
|
97
109
|
Native goal state is observational. Completion requires current verification,
|
|
98
110
|
two fresh reviews, task completion and a target-bound integration receipt.
|
|
111
|
+
--retry-approval explicitly retries an intact Codex approval-review usage-limit
|
|
112
|
+
failure. Original denials remain recorded; every command needs fresh approval.
|
|
113
|
+
Status includes phase, original cause, recorded sealed proof, cleanup state,
|
|
114
|
+
resumability and remediation. Resume revalidates authority and sealed artifacts;
|
|
115
|
+
receipt-bound recovery executes only missing cleanup steps. Resolve the saved
|
|
116
|
+
blocker before retrying: unchanged cleanup inputs are refused.
|
|
99
117
|
`,
|
|
100
118
|
init: `Usage: agent-ops init [options]
|
|
101
119
|
|
|
@@ -179,8 +197,8 @@ Options:
|
|
|
179
197
|
--title <text> create
|
|
180
198
|
--goal <text> create: immutable original issue/goal
|
|
181
199
|
--debug create: task-level investigation tag
|
|
182
|
-
--debug-action <status|hypothesis|strategy|investigate|checkpoint|resume|extend>
|
|
183
|
-
--payload-file <path> debug hypothesis/strategy: bounded JSON
|
|
200
|
+
--debug-action <status|hypothesis|strategy|investigate|checkpoint|resume|extend|origin>
|
|
201
|
+
--payload-file <path> debug hypothesis/strategy/origin: bounded JSON
|
|
184
202
|
--command-id <id> debug investigate: trusted configured command
|
|
185
203
|
--event-id <native-event-id|latest> debug extend: authentic human continue event
|
|
186
204
|
--intent <text> create: intended behavior and constraints before editing
|
|
@@ -189,6 +207,11 @@ Options:
|
|
|
189
207
|
--criteria-file <path> revise: replacement criterion array
|
|
190
208
|
--surface-file <path> create (required unless --debug), revise: EVIDENCE-SURFACE-001
|
|
191
209
|
audit JSON, or {"covers":[surface-id]} under a surfaced parent
|
|
210
|
+
--escape-of <commit> create, revise: the commit this bug escaped from (Escape Loop)
|
|
211
|
+
--escape-label <label> with --escape-of: requirement|surface|scenario|criteria|verify|review|flagged|out-of-model
|
|
212
|
+
--escape-receipt <id> with --escape-of: link this local finish receipt instead of resolving one
|
|
213
|
+
--escape-fix <commit> with --escape-of: backfill; missed paths come from this past fix commit
|
|
214
|
+
--escape-promote <runner:check> with --escape-of: the red-to-green check proving a criteria/verify escape
|
|
192
215
|
--plan-file <path> replan: split-task array with requirement mappings
|
|
193
216
|
--expected-contract <hash> revise, pin-finding: current task contract from status
|
|
194
217
|
--expected-tree-contract <hash> replan: current tree contract from status
|
|
@@ -294,9 +317,10 @@ voiding each other's verification and review. Run it from the main checkout.
|
|
|
294
317
|
before Git runs; see agent-ops worktree list.
|
|
295
318
|
finish <name>
|
|
296
319
|
Once its task is complete, fast-forward the main checkout's
|
|
297
|
-
branch to the worktree's
|
|
298
|
-
|
|
299
|
-
|
|
320
|
+
branch to the worktree's, record the task in git notes, and
|
|
321
|
+
remove the worktree. If the branch moved, it rebases the
|
|
322
|
+
worktree and stops: verify and review again with the printed
|
|
323
|
+
--base, then finish again. One finish runs at a time.
|
|
300
324
|
commit Stage every change in the current worktree and commit it with
|
|
301
325
|
--message. Refuses the main checkout and a worktree another
|
|
302
326
|
session owns, and needs no Git in the shell.
|
|
@@ -90,7 +90,8 @@ export async function runAdvanceCommand(options) {
|
|
|
90
90
|
throw new AgentOpsError("ADVANCE_WORKTREE_MISSING", `No coordinator worktree for session ${options.sessionId}.`);
|
|
91
91
|
}
|
|
92
92
|
const savedProof = await readRunIntegrationProof(commonDir, record);
|
|
93
|
-
if (savedProof !== null && await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) === savedProof.head
|
|
93
|
+
if (savedProof !== null && (await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) === savedProof.head ||
|
|
94
|
+
(await deps.git(mainRoot, ["merge-base", "--is-ancestor", savedProof.head, record.targetBranch])).exitCode === 0)) {
|
|
94
95
|
const finished = await finishWorktree(deps, { cwd: mainRoot, name, finalProof: { ...savedProof, recovery: true } });
|
|
95
96
|
return okEnvelope("TASK_ADVANCED", { ...finished, text: "Recovered the sealed integration without moving the target again." });
|
|
96
97
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { LEDGER_DIRECTORY } from "../../../../runtime/src/escape/link.js";
|
|
4
|
+
import { loadRules } from "../../../../runtime/src/escape/rules.js";
|
|
5
|
+
import { readLocalLog } from "../../../../runtime/src/logging/local-log.js";
|
|
6
|
+
import { okEnvelope } from "../output.js";
|
|
7
|
+
/** Below this many observations a count is shown but not trusted. */
|
|
8
|
+
export const MIN_SAMPLE = 10;
|
|
9
|
+
const status = (n) => n < MIN_SAMPLE ? "insufficient-data" : "ok";
|
|
10
|
+
function section(rows) {
|
|
11
|
+
const counts = {};
|
|
12
|
+
for (const row of rows)
|
|
13
|
+
counts[row.key] = (counts[row.key] ?? 0) + 1;
|
|
14
|
+
const times = rows.flatMap(row => row.at === undefined ? [] : [row.at]).sort();
|
|
15
|
+
return { n: rows.length, status: status(rows.length), window: { first: times[0] ?? null, last: times.at(-1) ?? null }, counts };
|
|
16
|
+
}
|
|
17
|
+
/** Tracked ledger records; a symlinked or unreadable entry is skipped with a warning, never followed. */
|
|
18
|
+
async function ledger(root, warnings) {
|
|
19
|
+
const directory = join(root, LEDGER_DIRECTORY);
|
|
20
|
+
try {
|
|
21
|
+
if (!(await lstat(directory)).isDirectory()) {
|
|
22
|
+
warnings.push(`${LEDGER_DIRECTORY} is not a directory.`);
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const records = [];
|
|
30
|
+
for (const name of (await readdir(directory)).filter(n => n.endsWith(".json")).sort()) {
|
|
31
|
+
try {
|
|
32
|
+
if (!(await lstat(join(directory, name))).isFile())
|
|
33
|
+
throw new Error("not a regular file");
|
|
34
|
+
const value = JSON.parse(await readFile(join(directory, name), "utf8"));
|
|
35
|
+
if (typeof value.label === "string" && typeof value.link?.state === "string")
|
|
36
|
+
records.push(value);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
warnings.push(`Skipped unreadable ledger record ${name}.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return records;
|
|
43
|
+
}
|
|
44
|
+
/** Advisory, read-only and fail-open: every source that cannot be read becomes a warning. */
|
|
45
|
+
export async function buildEscapesReport(root, home) {
|
|
46
|
+
const warnings = [];
|
|
47
|
+
const records = await ledger(root, warnings).catch(() => { warnings.push("Ledger unreadable."); return []; });
|
|
48
|
+
const events = await readLocalLog(home);
|
|
49
|
+
let origins = [];
|
|
50
|
+
try {
|
|
51
|
+
const state = JSON.parse(await readFile(join(root, ".agent-ops", "tasks", "state.json"), "utf8"));
|
|
52
|
+
origins = (state.tasks ?? []).filter(r => r.task?.origin !== undefined || r.task?.debug !== undefined)
|
|
53
|
+
.map(r => ({ ...(r.createdAt === undefined ? {} : { at: r.createdAt }), key: r.task?.origin?.kind ?? "undeclared" }));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
warnings.push("Task state unreadable; origin mix is empty.");
|
|
57
|
+
}
|
|
58
|
+
let rules = [];
|
|
59
|
+
try {
|
|
60
|
+
rules = await loadRules(root);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
warnings.push(`Rules unreadable: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
64
|
+
}
|
|
65
|
+
const ruleEvents = events.flatMap(e => e.event.type === "escape-rule" ? [e.event] : []);
|
|
66
|
+
const ruleRows = rules.map(({ rule }) => {
|
|
67
|
+
const fired = ruleEvents.filter(e => e.ruleId === rule.id);
|
|
68
|
+
return { id: rule.id, label: rule.label, noExclude: rule.noExclude, fires: fired.length,
|
|
69
|
+
included: fired.reduce((n, e) => n + e.included, 0), excluded: fired.reduce((n, e) => n + e.excluded, 0), status: status(fired.length) };
|
|
70
|
+
});
|
|
71
|
+
const links = section(records.map(r => ({ at: r.declaredAt, key: r.link.state })));
|
|
72
|
+
const reasons = {};
|
|
73
|
+
for (const r of records)
|
|
74
|
+
if (r.link.state === "unresolved")
|
|
75
|
+
reasons[r.link.reason] = (reasons[r.link.reason] ?? 0) + 1;
|
|
76
|
+
return {
|
|
77
|
+
misses: section(records.map(r => ({ at: r.declaredAt, key: r.label }))),
|
|
78
|
+
catches: section(events.flatMap(e => e.event.type === "gate-catch" ? [{ at: e.timestamp, key: e.event.code }] : [])),
|
|
79
|
+
origins: section(origins),
|
|
80
|
+
links: { ...links, reasons },
|
|
81
|
+
rules: ruleRows,
|
|
82
|
+
// Retirement is never automatic: only rules with enough fires that never surfaced an included place are listed.
|
|
83
|
+
retirementCandidates: ruleRows.filter(r => r.fires >= MIN_SAMPLE && r.included === 0)
|
|
84
|
+
.map(r => ({ id: r.id, reason: `fired ${r.fires} times; every hit was excluded (${r.excluded}) or none was included.` })),
|
|
85
|
+
warnings
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function render(report) {
|
|
89
|
+
const line = (name, s) => `${name}: n=${s.n}${s.status === "ok" ? "" : " (insufficient-data)"}` +
|
|
90
|
+
`${s.window.first === null ? "" : ` ${s.window.first} .. ${s.window.last}`}` +
|
|
91
|
+
`${Object.keys(s.counts).length === 0 ? "" : ` - ${Object.entries(s.counts).sort().map(([k, v]) => `${k} ${v}`).join(", ")}`}`;
|
|
92
|
+
return [
|
|
93
|
+
"Escape Loop report (advisory; raw counts; catches and rule fires come from this machine's local log)",
|
|
94
|
+
line("Misses by gate (ledger)", report.misses),
|
|
95
|
+
line("Catches by code (local log)", report.catches),
|
|
96
|
+
line("Origin mix (tasks)", report.origins),
|
|
97
|
+
line("Link states (ledger)", report.links) +
|
|
98
|
+
(Object.keys(report.links.reasons).length === 0 ? "" : `; unresolved reasons: ${Object.entries(report.links.reasons).sort().map(([k, v]) => `${k} ${v}`).join(", ")}`),
|
|
99
|
+
`Rules: ${report.rules.length}`,
|
|
100
|
+
...report.rules.map(r => `- ${r.id} (${r.label}${r.noExclude ? ", noExclude" : ""}): fires ${r.fires}, included ${r.included}, excluded ${r.excluded}` +
|
|
101
|
+
`${r.status === "ok" ? "" : " (insufficient-data)"}`),
|
|
102
|
+
`Retirement candidates: ${report.retirementCandidates.length === 0 ? "none" : report.retirementCandidates.map(c => `${c.id} (${c.reason})`).join("; ")}`,
|
|
103
|
+
...report.warnings.map(w => `Warning: ${w}`)
|
|
104
|
+
].join("\n") + "\n";
|
|
105
|
+
}
|
|
106
|
+
export async function runEscapesCommand(options) {
|
|
107
|
+
const report = await buildEscapesReport(options.root, options.home);
|
|
108
|
+
return okEnvelope("ESCAPES_REPORT", { ...report, text: render(report) });
|
|
109
|
+
}
|
|
@@ -41,7 +41,7 @@ function addText(result) {
|
|
|
41
41
|
function finishText(result) {
|
|
42
42
|
const { record } = result;
|
|
43
43
|
return [
|
|
44
|
-
`Merged ${record.branch} into ${record.targetBranch} at ${result.mergedHead}
|
|
44
|
+
`Merged ${record.branch} into ${record.targetBranch} at ${result.mergedHead}.`,
|
|
45
45
|
`Completed and recorded in git notes (refs/notes/agent-ops): ${result.taskIds.join(", ")}.`,
|
|
46
46
|
`Removed ${record.path}.`,
|
|
47
47
|
...(result.receipt === undefined ? [] : [`Receipt: ${result.receipt}`]),
|
|
@@ -58,7 +58,7 @@ export async function readFinishedReview(deps, mainRoot, commonDir, path) {
|
|
|
58
58
|
catch {
|
|
59
59
|
return invalid("Final review receipt is malformed.");
|
|
60
60
|
}
|
|
61
|
-
if (receipt.schemaVersion !== 1 || !/^[a-f0-9]{40,64}$/u.test(receipt.candidateHead) ||
|
|
61
|
+
if ((receipt.schemaVersion !== 1 && receipt.schemaVersion !== 2) || !/^[a-f0-9]{40,64}$/u.test(receipt.candidateHead) ||
|
|
62
62
|
!Array.isArray(receipt.tasks) || receipt.tasks.length === 0 ||
|
|
63
63
|
!["tree", "per-task-fallback"].includes(receipt.reviewMode) ||
|
|
64
64
|
typeof receipt.reviews !== "object" || receipt.reviews === null) {
|
|
@@ -17,7 +17,9 @@ import { calculateConfigHash, isPassingVerificationEvidence } from "../../../../
|
|
|
17
17
|
import { validateEvidence } from "../../../../runtime/src/schema/validate.js";
|
|
18
18
|
import { loadDebugProof, debugProofDigest } from "../../../../runtime/src/task/debug-state.js";
|
|
19
19
|
import { logSurfaceResidualRisks, runSurfaceGate } from "../../../../runtime/src/surface/discovery.js";
|
|
20
|
-
import {
|
|
20
|
+
import { escapeReviewData } from "../../../../runtime/src/escape/link.js";
|
|
21
|
+
import { logGateCatch } from "../../../../runtime/src/logging/local-log.js";
|
|
22
|
+
import { isCovers, surfaceDelta } from "../../../../runtime/src/surface/schema.js";
|
|
21
23
|
import { okEnvelope } from "../output.js";
|
|
22
24
|
/**
|
|
23
25
|
* Review runs against one target. Argument parsing already rejects a
|
|
@@ -318,8 +320,45 @@ const SURFACE_REASONS = {
|
|
|
318
320
|
SURFACE_EXCLUSION_TOO_BROAD: "surface-exclusion-too-broad",
|
|
319
321
|
SCENARIO_MISSING: "scenario-missing",
|
|
320
322
|
SURFACE_BUDGET_EXCEEDED: "surface-budget-exceeded",
|
|
321
|
-
SURFACE_UNASSIGNED: "surface-unassigned"
|
|
323
|
+
SURFACE_UNASSIGNED: "surface-unassigned",
|
|
324
|
+
SURFACE_DISMISSAL_TOO_BROAD: "surface-dismissal-too-broad",
|
|
325
|
+
SURFACE_DISMISSAL_INVALID: "surface-dismissal-invalid",
|
|
326
|
+
ESCAPE_RULE_INVALID: "escape-rule-invalid",
|
|
327
|
+
ESCAPE_RULE_UNSAFE: "escape-rule-unsafe",
|
|
328
|
+
ESCAPE_RULE_NO_EXCLUDE: "escape-rule-no-exclude"
|
|
322
329
|
};
|
|
330
|
+
/** Escape Loop: a declared escape-of origin gives a wrong label a criterion a blocking finding can name. */
|
|
331
|
+
export const ESCAPE_LABEL = {
|
|
332
|
+
id: "escape-label",
|
|
333
|
+
description: "Each declared escape's primary label names the earliest gate that could have caught it (requirement, surface, " +
|
|
334
|
+
"scenario, criteria, verify, review; flagged if a residual risk or finding already said it; out-of-model if no gate could), " +
|
|
335
|
+
"is consistent with the mechanical surface facts, and its tracked .agent-ops-escapes record is current."
|
|
336
|
+
};
|
|
337
|
+
/** Escape Loop packet rows for every task in scope that declares an origin; linking stays advisory. */
|
|
338
|
+
async function reviewEscapes(options, context, scope) {
|
|
339
|
+
if (options.root === undefined || options.gitRunner === undefined)
|
|
340
|
+
return [];
|
|
341
|
+
const rows = [];
|
|
342
|
+
for (const record of context.records) {
|
|
343
|
+
const origin = record.task.origin;
|
|
344
|
+
if (origin === undefined)
|
|
345
|
+
continue;
|
|
346
|
+
const base = record.debug?.baselineCommit ?? record.createdSourceCommit ?? (scope?.mode === "base" ? scope.resolvedBase : undefined);
|
|
347
|
+
try {
|
|
348
|
+
if (base === undefined)
|
|
349
|
+
throw new Error("no base");
|
|
350
|
+
rows.push(await escapeReviewData({ root: options.root, git: options.gitRunner, taskId: record.task.id, origin, base }));
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
rows.push({ taskId: record.task.id, origin, link: { state: "unresolved", reason: "link-unavailable" } });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return rows;
|
|
357
|
+
}
|
|
358
|
+
async function logCatch(options, code, taskId) {
|
|
359
|
+
if (taskId !== undefined && options.config?.profiles.includes("advisory") === true)
|
|
360
|
+
await logGateCatch(code, taskId);
|
|
361
|
+
}
|
|
323
362
|
/** Review-only: gives an omission a criterion that a blocking finding can name. */
|
|
324
363
|
export const SURFACE_COVERAGE = {
|
|
325
364
|
id: "surface-coverage",
|
|
@@ -359,8 +398,11 @@ async function reviewSurface(options, context, scope) {
|
|
|
359
398
|
return { gate };
|
|
360
399
|
return { gate, packet: {
|
|
361
400
|
audit: holder.task.surface,
|
|
362
|
-
|
|
363
|
-
|
|
401
|
+
// Deltas, not whole previous audits: a large audit revised a few times must still fit the review packet.
|
|
402
|
+
history: (holder.revisions ?? []).map((revision, i, all) => ({ at: revision.at, reason: revision.reason,
|
|
403
|
+
change: surfaceDelta(revision.previousTask.surface, (all[i + 1]?.previousTask ?? holder.task).surface) })).slice(-10),
|
|
404
|
+
discovery: { cached: gate.discovery.cached, sources: gate.discovery.sources, skipped: gate.discovery.skipped,
|
|
405
|
+
dismissed: gate.discovery.dismissed },
|
|
364
406
|
postBlockExclusions: gate.discovery.postBlockExclusions
|
|
365
407
|
} };
|
|
366
408
|
}
|
|
@@ -658,16 +700,19 @@ export async function runReviewCommand(options) {
|
|
|
658
700
|
});
|
|
659
701
|
}
|
|
660
702
|
if (context.policyConfigHash !== options.policyConfigHash) {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
703
|
+
const result = { ...resultBase, status: "NOT_RUN", reason: "reviewer-policy-changed", scope };
|
|
704
|
+
const own = context.records.find(r => r.task.id === context.taskId).task;
|
|
705
|
+
const message = "Independent review was not run.";
|
|
706
|
+
return { code: "REVIEW_NOT_RUN", status: "error", errors: [{ code: "REVIEW_NOT_RUN", message }], data: { message, result,
|
|
707
|
+
text: renderReviewResult(result) +
|
|
708
|
+
`The configuration changed after task ${context.taskId} was created. Rebind it, then verify again:\n` +
|
|
709
|
+
` agent-ops task revise --task ${context.taskId} --expected-contract ${taskContractHash(own)} --reason "Rebind to the current configuration"\n` } };
|
|
666
710
|
}
|
|
667
711
|
}
|
|
668
712
|
if (context !== undefined && options.tasks !== undefined) {
|
|
669
713
|
const surface = await reviewSurface(options, context, scope);
|
|
670
714
|
if (surface.gate.status === "NOT_RUN") {
|
|
715
|
+
await logCatch(options, surface.gate.code, context.taskId);
|
|
671
716
|
return surfaceNotRunEnvelope({
|
|
672
717
|
...resultBase,
|
|
673
718
|
status: "NOT_RUN",
|
|
@@ -733,9 +778,11 @@ export async function runReviewCommand(options) {
|
|
|
733
778
|
candidateCommit: await resolveGitCommit(options.gitRunner, "HEAD"), coverageDigest: coverageDigest(rows)
|
|
734
779
|
};
|
|
735
780
|
}
|
|
781
|
+
const escapes = context === undefined ? [] : await reviewEscapes(options, context, scope);
|
|
736
782
|
const criteria = [
|
|
737
783
|
...(context?.criteria ?? GENERIC_CRITERIA),
|
|
738
|
-
...(surfacePacket === undefined || context?.criteria.some(c => c.id === SURFACE_COVERAGE.id) === true ? [] : [SURFACE_COVERAGE])
|
|
784
|
+
...(surfacePacket === undefined || context?.criteria.some(c => c.id === SURFACE_COVERAGE.id) === true ? [] : [SURFACE_COVERAGE]),
|
|
785
|
+
...(!context?.records.some(r => r.task.origin?.kind === "escape-of") || context.criteria.some(c => c.id === ESCAPE_LABEL.id) ? [] : [ESCAPE_LABEL])
|
|
739
786
|
];
|
|
740
787
|
// Tree attestations bind the exact criterion list the reviewer answered.
|
|
741
788
|
if (context !== undefined)
|
|
@@ -769,12 +816,14 @@ export async function runReviewCommand(options) {
|
|
|
769
816
|
contractArtifacts.push({ path: executionPath, digest: sha256(execution), content: execution });
|
|
770
817
|
}
|
|
771
818
|
}
|
|
819
|
+
// Compact: indentation alone pushed a 494 KiB 0.10.2 manifest past the 512 KiB bound.
|
|
772
820
|
const content = redactSecrets(JSON.stringify({ binding: context.binding, coverage: context.coverage,
|
|
773
821
|
executionPolicy: options.config,
|
|
822
|
+
...(surfacePacket === undefined ? {} : { surface: surfacePacket }),
|
|
774
823
|
artifacts: contractArtifacts.map(({ path, digest }) => ({ path, digest })),
|
|
775
824
|
tasks: await Promise.all(context.records.map(async (r) => ({ task: r.task, revisions: r.revisions ?? [],
|
|
776
825
|
...(r.task.debug === undefined ? {} : { debugProof: await loadDebugProof(options.root, r.task.debug.rootTaskId) }) }))),
|
|
777
|
-
superseded: context.historyRecords ?? [] }
|
|
826
|
+
superseded: context.historyRecords ?? [] }));
|
|
778
827
|
const digest = sha256(content);
|
|
779
828
|
const path = ".agent-ops/tasks/review-contracts/" + digest + ".json";
|
|
780
829
|
contractManifest = { path, digest, content };
|
|
@@ -784,6 +833,7 @@ export async function runReviewCommand(options) {
|
|
|
784
833
|
}
|
|
785
834
|
packet = buildReviewPacket({
|
|
786
835
|
...(surfacePacket === undefined ? {} : { surface: surfacePacket }),
|
|
836
|
+
...(escapes.length === 0 ? {} : { escape: escapes }),
|
|
787
837
|
...(contractManifest === undefined ? {} : { contractManifest, contractArtifacts }),
|
|
788
838
|
request: context?.title ?? GENERIC_REQUEST,
|
|
789
839
|
criteria,
|
|
@@ -922,6 +972,8 @@ export async function runReviewCommand(options) {
|
|
|
922
972
|
if (finalResult.status === "PASS") {
|
|
923
973
|
return okEnvelope("REVIEW_RESULT", data);
|
|
924
974
|
}
|
|
975
|
+
if (finalResult.status === "FAIL")
|
|
976
|
+
await logCatch(options, "REVIEW_FAIL", context?.taskId);
|
|
925
977
|
const code = finalResult.status === "FAIL" ? "REVIEW_FAILED" : "REVIEW_NOT_RUN";
|
|
926
978
|
return {
|
|
927
979
|
code,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { AgentOpsError, resolveContainedPath } from "../../../../runtime/src/fs/paths.js";
|
|
3
|
+
import { describeRunRecovery } from "../../../../runtime/src/run/service.js";
|
|
3
4
|
import { errorEnvelope, okEnvelope } from "../output.js";
|
|
4
5
|
function dynamic(args) {
|
|
5
6
|
return args;
|
|
@@ -19,13 +20,18 @@ function runIdOf(args) {
|
|
|
19
20
|
return runId;
|
|
20
21
|
}
|
|
21
22
|
function textFor(action, state, message) {
|
|
23
|
+
const recovery = describeRunRecovery(state);
|
|
22
24
|
const active = state.workers.filter((worker) => ["assigned", "starting", "running", "idle", "handing-off"].includes(worker.status)).length;
|
|
23
25
|
return [
|
|
24
26
|
message,
|
|
25
27
|
`Status: ${state.status}`,
|
|
28
|
+
`Phase: ${recovery.phase}`,
|
|
29
|
+
`Cause: ${recovery.cause === null ? "none recorded" : recovery.cause.code + ": " + recovery.cause.detail}`,
|
|
30
|
+
`Sealed proof: ${recovery.sealedProof}; cleanup: ${recovery.cleanup}; resumable: ${recovery.resumable ? "yes, after reconciliation" : "no"}`,
|
|
31
|
+
`Remediation: ${recovery.remediation}`,
|
|
26
32
|
`Host: ${state.host}`,
|
|
27
33
|
`Workers: ${active}/${state.jobs}`,
|
|
28
|
-
`Budget: ${Math.max(0, state.budget.limitMs - state.budget.accumulatedMs)}ms remaining`,
|
|
34
|
+
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)`,
|
|
29
35
|
`Usage: ${state.budget.usage.tokens ?? "unknown"} tokens; ${state.budget.usage.usd === null ? "unknown" : "$" + state.budget.usage.usd} cost (${state.budget.usage.completeness})`,
|
|
30
36
|
action === "logs" ? "" : "Native goal completion is not final proof; verify, review, task state, and receipt are still required."
|
|
31
37
|
].filter((line) => line.length > 0).join("\n");
|
|
@@ -35,12 +41,13 @@ function data(action, result) {
|
|
|
35
41
|
action,
|
|
36
42
|
runId: result.state.runId,
|
|
37
43
|
state: result.state,
|
|
44
|
+
recovery: describeRunRecovery(result.state),
|
|
38
45
|
message: result.message,
|
|
39
46
|
text: textFor(action, result.state, result.message)
|
|
40
47
|
};
|
|
41
48
|
}
|
|
42
49
|
function stateData(action, state, message) {
|
|
43
|
-
return { action, runId: state.runId, state, message, text: textFor(action, state, message) };
|
|
50
|
+
return { action, runId: state.runId, state, recovery: describeRunRecovery(state), message, text: textFor(action, state, message) };
|
|
44
51
|
}
|
|
45
52
|
async function goalFromArgs(args, root) {
|
|
46
53
|
if (args.goal !== undefined && args.goalFile !== undefined)
|
|
@@ -99,7 +106,7 @@ export async function runRunCommand(options) {
|
|
|
99
106
|
return okEnvelope("RUN_LOGS", { ...stateData(action, state, `Read ${events.length} event(s) for run ${runId}.`), events });
|
|
100
107
|
}
|
|
101
108
|
if (action === "resume")
|
|
102
|
-
return okEnvelope("RUN_RESUMED", data(action, await options.service.resume(runId)));
|
|
109
|
+
return okEnvelope("RUN_RESUMED", data(action, await options.service.resume(runId, options.args.retryApprovalIds)));
|
|
103
110
|
if (action === "stop")
|
|
104
111
|
return okEnvelope("RUN_STOP_REQUESTED", data(action, await options.service.stop(runId, options.args.answer ?? "user requested stop")));
|
|
105
112
|
const questionId = options.args.questionId;
|