@kylecheng3146/agent-ops 0.10.0 → 0.11.0

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.
Files changed (76) hide show
  1. package/README.md +3 -2
  2. package/dist/packages/cli/src/args.js +18 -53
  3. package/dist/packages/cli/src/bin.js +28 -29
  4. package/dist/packages/cli/src/cli.js +25 -17
  5. package/dist/packages/cli/src/codex-loop-process.js +0 -6
  6. package/dist/packages/cli/src/commands/advance.js +2 -1
  7. package/dist/packages/cli/src/commands/parallel.js +1 -1
  8. package/dist/packages/cli/src/commands/review.js +8 -11
  9. package/dist/packages/cli/src/commands/run.js +12 -3
  10. package/dist/packages/cli/src/commands/task.js +6 -50
  11. package/dist/packages/cli/src/commands/verify.js +7 -3
  12. package/dist/packages/cli/src/context.js +22 -4
  13. package/dist/packages/cli/src/default-base.js +26 -0
  14. package/dist/packages/cli/src/hook-process.js +19 -83
  15. package/dist/packages/cli/src/office-entry.js +8 -4
  16. package/dist/packages/cli/src/owned-run-step.js +4 -4
  17. package/dist/packages/cli/src/parallel-deps.js +0 -2
  18. package/dist/packages/cli/src/run-deps.js +12 -9
  19. package/dist/packages/cli/src/run-entry.js +123 -19
  20. package/dist/packages/cli/src/run-setup-entry.js +2 -2
  21. package/dist/runtime/src/adapters/agy/config.js +2 -3
  22. package/dist/runtime/src/adapters/claude/config.js +2 -2
  23. package/dist/runtime/src/adapters/codex/config.js +2 -5
  24. package/dist/runtime/src/hooks/completion-gate.js +0 -15
  25. package/dist/runtime/src/install/harness.js +7 -4
  26. package/dist/runtime/src/install/plan.js +3 -3
  27. package/dist/runtime/src/install/profiles.js +0 -2
  28. package/dist/runtime/src/office/scene.js +1 -1
  29. package/dist/runtime/src/office/server.js +43 -16
  30. package/dist/runtime/src/office/snapshot.js +5 -2
  31. package/dist/runtime/src/parallel/finish.js +139 -65
  32. package/dist/runtime/src/parallel/manage.js +0 -3
  33. package/dist/runtime/src/parallel/service.js +5 -1
  34. package/dist/runtime/src/review/execute.js +77 -11
  35. package/dist/runtime/src/review/packet.js +17 -6
  36. package/dist/runtime/src/review/runner.js +2 -0
  37. package/dist/runtime/src/run/controls.js +99 -1
  38. package/dist/runtime/src/run/hosts/codex.js +28 -1
  39. package/dist/runtime/src/run/hosts/protocol.js +5 -2
  40. package/dist/runtime/src/run/integration.js +35 -13
  41. package/dist/runtime/src/run/policy.js +65 -10
  42. package/dist/runtime/src/run/scheduler.js +10 -5
  43. package/dist/runtime/src/run/service.js +68 -21
  44. package/dist/runtime/src/run/supervisor.js +11 -2
  45. package/dist/runtime/src/run/transport.js +1 -0
  46. package/dist/runtime/src/run/verification.js +2 -2
  47. package/dist/runtime/src/schema/validate.js +10 -5
  48. package/dist/runtime/src/security/permissions.js +85 -0
  49. package/dist/runtime/src/task/completion.js +4 -42
  50. package/dist/runtime/src/task/contract.js +11 -0
  51. package/dist/runtime/src/task/service.js +34 -57
  52. package/dist/runtime/src/task/store.js +3 -7
  53. package/dist/runtime/src/verify/acceptance-coverage.js +2 -3
  54. package/dist/runtime/src/verify/acceptance-protocol.js +4 -3
  55. package/dist/runtime/src/verify/acceptance-replay.js +4 -7
  56. package/dist/runtime/src/verify/service.js +6 -23
  57. package/dist/runtime/src/verify/source-fingerprint.js +17 -4
  58. package/docs/en/guides/configuration.md +8 -4
  59. package/docs/en/guides/task-debug.md +39 -121
  60. package/docs/en/spec/acceptance-and-evidence.md +31 -19
  61. package/docs/en/spec/harness-adapters.md +35 -12
  62. package/docs/en/spec/loop-engineering.md +40 -6
  63. package/docs/en/spec/review.md +6 -0
  64. package/docs/zh-TW/guides/configuration.md +6 -3
  65. package/docs/zh-TW/guides/task-debug.md +27 -66
  66. package/docs/zh-TW/spec/acceptance-and-evidence.md +19 -18
  67. package/docs/zh-TW/spec/harness-adapters.md +28 -9
  68. package/docs/zh-TW/spec/loop-engineering.md +14 -3
  69. package/docs/zh-TW/spec/review.md +5 -0
  70. package/package.json +1 -1
  71. package/schemas/config.schema.json +1 -21
  72. package/dist/packages/cli/src/debug-intake.js +0 -245
  73. package/dist/runtime/src/task/debug-intake.js +0 -15
  74. package/dist/runtime/src/task/debug-state.js +0 -129
  75. package/dist/runtime/src/task/debug-user-event.js +0 -95
  76. package/dist/runtime/src/task/debug.js +0 -612
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 --time-budget 60m --jobs 2
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. This authorization is bound to the run, runtime, lease and budget;
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;
@@ -66,6 +66,10 @@ function parseHookTarget(option, value) {
66
66
  export function parseArgs(argv) {
67
67
  if (argv[0] === "run")
68
68
  return parseRunArgs(argv.slice(1));
69
+ if (argv[0] === "task" && argv[1] === "debug")
70
+ throw new CliArgumentError("TASK_DEBUG_REMOVED", "task debug was removed in 0.11.0. A DEBUG task is task create --debug with a behavioral " +
71
+ "criterion whose redCheckIds fail at the baseline and pass on the candidate; then verify, review and finish as usual. " +
72
+ "Declare an escape origin, if known, with task revise --escape-of.");
69
73
  let command;
70
74
  let action;
71
75
  let scope;
@@ -78,10 +82,6 @@ export function parseArgs(argv) {
78
82
  let intent;
79
83
  let taskGoal;
80
84
  let debug = false;
81
- let debugAction;
82
- let payloadFile;
83
- let commandId;
84
- let eventId;
85
85
  let sessionId;
86
86
  let base;
87
87
  let parentBase;
@@ -233,30 +233,6 @@ export function parseArgs(argv) {
233
233
  duplicate(token);
234
234
  debug = true;
235
235
  break;
236
- case "--debug-action": {
237
- if (debugAction !== undefined)
238
- duplicate(token);
239
- const value = readOptionValue(argv, index++, token);
240
- if (!["status", "hypothesis", "strategy", "investigate", "checkpoint", "resume", "extend", "origin"].includes(value))
241
- invalidValue(token, value);
242
- debugAction = value;
243
- break;
244
- }
245
- case "--payload-file":
246
- if (payloadFile !== undefined)
247
- duplicate(token);
248
- payloadFile = readOptionValue(argv, index++, token);
249
- break;
250
- case "--command-id":
251
- if (commandId !== undefined)
252
- duplicate(token);
253
- commandId = readOptionValue(argv, index++, token);
254
- break;
255
- case "--event-id":
256
- if (eventId !== undefined)
257
- duplicate(token);
258
- eventId = readOptionValue(argv, index++, token);
259
- break;
260
236
  case "--tree": {
261
237
  if (tree)
262
238
  duplicate(token);
@@ -530,7 +506,7 @@ export function parseArgs(argv) {
530
506
  "complete",
531
507
  "create",
532
508
  "export",
533
- "status", "revise", "replan", "pin-finding", "debug"
509
+ "status", "revise", "replan", "pin-finding"
534
510
  ].includes(token)) {
535
511
  action = token;
536
512
  break;
@@ -674,8 +650,8 @@ export function parseArgs(argv) {
674
650
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--target-version may be used only with update.");
675
651
  }
676
652
  if (base !== undefined && command !== "verify" && command !== "review" &&
677
- command !== "batch" && !(command === "task" && (action === "complete" || action === "debug"))) {
678
- throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--base may be used only with verify, review, batch, task complete or task debug.");
653
+ command !== "batch" && !(command === "task" && action === "complete")) {
654
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--base may be used only with verify, review, batch or task complete.");
679
655
  }
680
656
  if ((parentBase !== undefined || width !== undefined) && command !== "batch") {
681
657
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", `${parentBase !== undefined ? "--parent-base" : "--width"} may be used only with batch.`);
@@ -714,13 +690,6 @@ export function parseArgs(argv) {
714
690
  const revisionAction = command === "task" && ["revise", "replan", "pin-finding"].includes(action ?? "");
715
691
  if ((taskGoal !== undefined || debug) && !(command === "task" && action === "create"))
716
692
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--goal and --debug apply only to task create.");
717
- if ((debugAction !== undefined || commandId !== undefined || eventId !== undefined || payloadFile !== undefined) && !(command === "task" && action === "debug"))
718
- throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Debug operation options apply only to task debug.");
719
- if (command === "task" && action === "debug" && (taskId === undefined || debugAction === undefined || title !== undefined || intent !== undefined ||
720
- parentTaskId !== undefined || criteria.length > 0 || evidence.length > 0 ||
721
- ((debugAction === "hypothesis" || debugAction === "strategy" || debugAction === "origin") !== (payloadFile !== undefined)) ||
722
- ((debugAction === "investigate") !== (commandId !== undefined)) || ((debugAction === "extend") !== (eventId !== undefined))))
723
- throw new CliArgumentError("CLI_MISSING_VALUE", "Task debug requires --task, --debug-action and the operation's exact payload/command/event options.");
724
693
  if ((criterionFiles.length > 0 && !(command === "task" && (action === "create" || action === "pin-finding"))) ||
725
694
  (criteriaFile !== undefined && !(command === "task" && action === "revise")) ||
726
695
  (surfaceFile !== undefined && !(command === "task" && (action === "create" || action === "revise"))) ||
@@ -739,7 +708,7 @@ export function parseArgs(argv) {
739
708
  if (command === "escapes" && argv.some(token => token !== "escapes" && token !== "--json"))
740
709
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "escapes accepts only --json; it is a read-only report.");
741
710
  if (revisionAction && (taskId === undefined || evidence.length > 0 || title !== undefined || intent !== undefined || parentTaskId !== undefined || sessionId !== undefined || criteria.length > 0 || base !== undefined ||
742
- (action === "revise" && (expectedContract === undefined || (criteriaFile === undefined && surfaceFile === undefined && escapeOf === undefined) || reason === undefined)) ||
711
+ (action === "revise" && (expectedContract === undefined || (criteriaFile === undefined && surfaceFile === undefined && escapeOf === undefined && baseline === undefined) || reason === undefined)) ||
743
712
  (action === "replan" && (expectedTreeContract === undefined || planFile === undefined || reason === undefined)) ||
744
713
  (action === "pin-finding" && (expectedContract === undefined || findingReference === undefined || criterionFiles.length !== 1))))
745
714
  throw new CliArgumentError("CLI_MISSING_VALUE", "Acceptance revision requires task, expected contract, payload and reason.");
@@ -902,10 +871,6 @@ export function parseArgs(argv) {
902
871
  ...(intent === undefined ? {} : { intent }),
903
872
  ...(taskGoal === undefined ? {} : { goal: taskGoal }),
904
873
  ...(debug ? { debug: true } : {}),
905
- ...(debugAction === undefined ? {} : { debugAction }),
906
- ...(payloadFile === undefined ? {} : { payloadFile }),
907
- ...(commandId === undefined ? {} : { commandId }),
908
- ...(eventId === undefined ? {} : { eventId }),
909
874
  ...(reviewTargets.length === 0 ? {} : { reviewTargets }),
910
875
  ...(completionGate === undefined ? {} : { completionGate }),
911
876
  ...(office === undefined ? {} : { office }),
@@ -960,7 +925,7 @@ function parseRunArgs(argv) {
960
925
  positional = token;
961
926
  continue;
962
927
  }
963
- if (seen.has(token))
928
+ if (seen.has(token) && token !== "--retry-approval")
964
929
  duplicate(token);
965
930
  seen.add(token);
966
931
  if (token === "--json") {
@@ -971,10 +936,15 @@ function parseRunArgs(argv) {
971
936
  args.wait = true;
972
937
  continue;
973
938
  }
974
- if (!["--host", "--goal-file", "--time-budget", "--jobs", "--question-id", "--answer", "--answer-file"].includes(token))
939
+ if (!["--host", "--goal-file", "--time-budget", "--jobs", "--question-id", "--answer", "--answer-file", "--retry-approval"].includes(token))
975
940
  throw new CliArgumentError("CLI_UNKNOWN_OPTION", `Unknown run option: ${token}`, token);
976
941
  const value = readOptionValue(argv, i++, token);
977
942
  switch (token) {
943
+ case "--retry-approval":
944
+ if (!/^auth-[a-f0-9]{64}$/u.test(value) || args.retryApprovalIds?.includes(value))
945
+ invalidValue(token, value);
946
+ args.retryApprovalIds = [...(args.retryApprovalIds ?? []), value];
947
+ break;
978
948
  case "--host":
979
949
  if (value !== "claude" && value !== "codex")
980
950
  invalidValue(token, value);
@@ -998,17 +968,12 @@ function parseRunArgs(argv) {
998
968
  args.jobs = Number(value);
999
969
  break;
1000
970
  case "--time-budget": {
1001
- const match = value.match(/^([1-9][0-9]*)(s|m|h)$/u);
1002
- if (match === null)
1003
- invalidValue(token, value);
1004
- const ms = Number(match[1]) * ({ s: 1000, m: 60000, h: 3600000 }[match[2]]);
1005
- if (!Number.isSafeInteger(ms) || ms > 86400000)
1006
- invalidValue(token, value);
1007
- args.timeBudgetMs = ms;
1008
- break;
971
+ throw new CliArgumentError("RUN_TIME_BUDGET_UNSUPPORTED", "New runs have no total time cap; omit --time-budget. Command and review timeouts remain effective.", token);
1009
972
  }
1010
973
  }
1011
974
  }
975
+ if (args.retryApprovalIds !== undefined && action !== "resume")
976
+ throw new CliArgumentError("CLI_INVALID_OPTION", "--retry-approval requires run resume.");
1012
977
  if (action === "start") {
1013
978
  args.goal = positional;
1014
979
  if ((args.goal === undefined) === (args.goalFile === undefined))
@@ -47,9 +47,11 @@ import { memoizePreflight } from "../../../runtime/src/review/batch.js";
47
47
  import { createReviewExecutor, ReviewInterruptedError } from "../../../runtime/src/review/execute.js";
48
48
  import { probeReviewTargetDetailed } from "../../../runtime/src/review/probe.js";
49
49
  import { resolveReviewScope } from "../../../runtime/src/review/scope.js";
50
+ import { runSurfaceGate } from "../../../runtime/src/surface/discovery.js";
50
51
  import { resolveReviewRole } from "../../../runtime/src/review/roles.js";
51
52
  import { runTrustCommand } from "./commands/trust.js";
52
53
  import { runVerifyCommand } from "./commands/verify.js";
54
+ import { defaultBase } from "./default-base.js";
53
55
  import { runAdvanceCommand } from "./commands/advance.js";
54
56
  import { runReviewShowCommand } from "./commands/review-show.js";
55
57
  import { runEscapesCommand } from "./commands/escapes.js";
@@ -255,6 +257,16 @@ else {
255
257
  const root = args.scope === "user"
256
258
  ? process.env.AGENT_OPS_HOME ?? homedir()
257
259
  : process.cwd();
260
+ // Committed work is judged as a range by default; see defaultBase.
261
+ if ((args.command === "verify" || (args.command === "review" && args.action !== "show")) && args.base === undefined && args.scope !== "user") {
262
+ const taskId = args.taskId;
263
+ const base = await defaultBase(root, gitRunner(root), async () => taskId === undefined ? undefined :
264
+ (await new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root)).status({ taskId })).createdSourceCommit)
265
+ // Only a default: any failure leaves the command's own scope handling and errors in charge.
266
+ .catch(() => undefined);
267
+ if (base !== undefined)
268
+ args = { ...args, base };
269
+ }
258
270
  const isTTY = !args.json &&
259
271
  process.stdin.isTTY === true &&
260
272
  process.stdout.isTTY === true;
@@ -495,7 +507,6 @@ else {
495
507
  return await runTaskCommand({
496
508
  args,
497
509
  service: taskService,
498
- ...(args.action !== "debug" ? {} : { trusted: await (async () => { const binding = await plannedTrustBinding(root, (await loadEffectiveConfig(root, "project")).config); return binding !== null && (await trustStore().status(binding)).status === "TRUSTED"; })() }),
499
510
  ...(policyConfigHash === undefined ? {} : { policyConfigHash }),
500
511
  ...(sessionId === undefined ? {} : { sessionId }),
501
512
  ...(fromMain
@@ -553,41 +564,16 @@ else {
553
564
  reviewActivity = undefined;
554
565
  }
555
566
  });
556
- const debugRecord = reviewArgs.taskId === undefined ? undefined : await taskService.status({ taskId: reviewArgs.taskId });
557
- const inDebug = (executor) => debugRecord?.task.debug === undefined ? executor : async (request) => await taskService.debugService().review(debugRecord.task.id, () => executor(request));
558
567
  const inSlot = (executor) => slotDir === undefined
559
- ? inDebug(executor)
568
+ ? executor
560
569
  : async (request) => await withReviewSlot({
561
570
  dir: slotDir,
562
571
  signal,
563
572
  holder: { root, ...(reviewArgs.taskId === undefined ? {} : { taskId: reviewArgs.taskId }) },
564
- ...(debugRecord?.task.debug === undefined || reviewSessionId === undefined
565
- ? {}
566
- : (() => {
567
- let paused = false;
568
- const debug = taskService.debugService();
569
- return {
570
- onWaitStart: async () => {
571
- if (paused)
572
- return;
573
- const current = await debug.root(debugRecord.task.id).catch(() => null);
574
- if (current?.debug.foreground?.sessionId !== reviewSessionId)
575
- return;
576
- await debug.foregroundStop(debugRecord.task.id, reviewSessionId);
577
- paused = true;
578
- },
579
- onWaitEnd: async () => {
580
- if (!paused)
581
- return;
582
- await debug.foregroundStart(debugRecord.task.id, reviewSessionId);
583
- paused = false;
584
- }
585
- };
586
- })()),
587
573
  onWait: (line) => {
588
574
  process.stderr.write(`${progressPrefix}${line}\n`);
589
575
  }
590
- }, async () => await inDebug(executor)(request));
576
+ }, async () => await executor(request));
591
577
  return {
592
578
  args: reviewArgs,
593
579
  authorized: reviewArgs.yes,
@@ -607,7 +593,6 @@ else {
607
593
  config: reviewConfig,
608
594
  evidenceStore: new FileEvidenceStore(root, root),
609
595
  execute: inActivity(inSlot(createReviewExecutor({
610
- ...(debugRecord?.task.debug === undefined ? {} : { remainingBudgetMs: () => taskService.debugService().remaining(debugRecord.task.id) }),
611
596
  runner: registeredRunProofRunner(),
612
597
  targets: configuredReviewTargets,
613
598
  cwd: root,
@@ -749,6 +734,20 @@ else {
749
734
  return await runVerifyCommand({
750
735
  args,
751
736
  taskService,
737
+ surface: async (taskId) => {
738
+ const record = await taskService.status({ taskId });
739
+ // A subtask's surface is its parent's; review checks the tree, so verify stays quiet here.
740
+ if (record.task.parentTaskId !== undefined)
741
+ return [];
742
+ const scope = await resolveReviewScope({ root, runner: gitRunner(root), ...(args.base === undefined ? {} : { base: args.base }) });
743
+ const gate = await runSurfaceGate({ root, taskId, surface: record.task.surface, required: record.task.surfaceRequired === true,
744
+ ...(scope.mode === "base" ? { base: scope.resolvedBase } : {}), changedFiles: scope.changedFiles, configHash: calculateConfigHash(config) });
745
+ if (gate.status === "PASS")
746
+ return ["Surface: PASS"];
747
+ const paths = gate.details.flatMap(d => typeof d === "object" && d !== null && "path" in d ? [String(d.path)] : []);
748
+ return [`Surface (advisory; review would stop with ${gate.code}): ${gate.message}`,
749
+ ...paths.slice(0, 20).map(path => ` - ${path}`), ...(paths.length > 20 ? [` ... ${paths.length - 20} more`] : [])];
750
+ },
752
751
  activity: { commonDir: await officeCommonDir(root), root, ...(verifySessionId === undefined ? {} : { sessionId: verifySessionId }) },
753
752
  service: { verify: async (taskId) => {
754
753
  const run = await runPolicyContext(root);
@@ -25,7 +25,7 @@ Commands:
25
25
  doctor Diagnose an installation
26
26
  update Update managed artifacts
27
27
  uninstall Remove managed artifacts
28
- task <create|status|attach|revise|replan|pin-finding|debug|advance|complete|archive|export>
28
+ task <create|status|attach|revise|replan|pin-finding|advance|complete|archive|export>
29
29
  Manage independent task acceptance state
30
30
  verify Run configured verification
31
31
  review Run an independent review or show recorded reports
@@ -98,13 +98,22 @@ browser once; later sessions reuse it. It exits after 10 minutes without active
98
98
  runs or sessions. Recently completed rooms remain for at most 2 hours.
99
99
  The URL carries an access token; do not share it.
100
100
  `,
101
- run: `Usage: agent-ops run [start] "<goal>" --host <claude|codex> [--jobs <1|2>] [--time-budget <60m>] [--wait] [--json]
101
+ run: `Usage: agent-ops run [start] "<goal>" --host <claude|codex> [--jobs <1|2>] [--wait] [--json]
102
102
  agent-ops run [start] --goal-file <path> --host <claude|codex>
103
103
  agent-ops run <status|logs|resume|stop> <run-id> [--json]
104
+ agent-ops run resume <run-id> [--retry-approval <auth-event-id>]... [--json]
104
105
  agent-ops run respond <run-id> --question-id <id> <--answer <text>|--answer-file <path>> [--json]
105
106
 
107
+ New runs have no total time cap. Manual resume upgrades legacy finite runs and keeps accounting.
108
+ Command/review timeouts, provider limits and stop controls remain effective.
106
109
  Native goal state is observational. Completion requires current verification,
107
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.
108
117
  `,
109
118
  init: `Usage: agent-ops init [options]
110
119
 
@@ -179,7 +188,7 @@ Options:
179
188
  --json
180
189
  --yes
181
190
  `,
182
- task: `Usage: agent-ops task <create|status|attach|revise|replan|pin-finding|debug|advance|complete|archive|export> [options]
191
+ task: `Usage: agent-ops task <create|status|attach|revise|replan|pin-finding|advance|complete|archive|export> [options]
183
192
 
184
193
  Manage independent task acceptance state. Task commands accept none of
185
194
  --harness, --profile or --dry-run. Only advance accepts --yes.
@@ -187,16 +196,12 @@ Manage independent task acceptance state. Task commands accept none of
187
196
  Options:
188
197
  --title <text> create
189
198
  --goal <text> create: immutable original issue/goal
190
- --debug create: task-level investigation tag
191
- --debug-action <status|hypothesis|strategy|investigate|checkpoint|resume|extend|origin>
192
- --payload-file <path> debug hypothesis/strategy/origin: bounded JSON
193
- --command-id <id> debug investigate: trusted configured command
194
- --event-id <native-event-id|latest> debug extend: authentic human continue event
199
+ --debug create: bug fix; needs --goal and a behavioral criterion with redCheckIds
195
200
  --intent <text> create: intended behavior and constraints before editing
196
201
  --criterion <json> create, repeatable, two to five total
197
202
  --criterion-file <path> create: one criterion object, repeatable; pin-finding: exactly one
198
203
  --criteria-file <path> revise: replacement criterion array
199
- --surface-file <path> create (required unless --debug), revise: EVIDENCE-SURFACE-001
204
+ --surface-file <path> create (required), revise: EVIDENCE-SURFACE-001
200
205
  audit JSON, or {"covers":[surface-id]} under a surfaced parent
201
206
  --escape-of <commit> create, revise: the commit this bug escaped from (Escape Loop)
202
207
  --escape-label <label> with --escape-of: requirement|surface|scenario|criteria|verify|review|flagged|out-of-model
@@ -207,12 +212,12 @@ Options:
207
212
  --expected-contract <hash> revise, pin-finding: current task contract from status
208
213
  --expected-tree-contract <hash> replan: current tree contract from status
209
214
  --reason <text> required for revise/replan; optional for pin-finding
210
- --baseline <git-ref> create/revise: baseline for new mechanical requirements
215
+ --baseline <git-ref> create: baseline for typed criteria; revise: move every typed baseline
211
216
  --parent <task-id> create: record a subtask; status: list subtasks
212
217
  --task <id> status, attach, revise, replan, pin-finding, complete, archive, export, advance
213
- --session <id> create, attach, status, advance, debug
218
+ --session <id> create, attach, status, advance
214
219
  --evidence <criterion-id=reference> complete, repeatable
215
- --base <git-ref> complete, debug: a clean committed range
220
+ --base <git-ref> complete: a clean committed range
216
221
  --yes advance: authorize the two review sessions
217
222
  --json
218
223
 
@@ -224,7 +229,8 @@ Each legacy criterion needs at least one verifierIds entry naming a verification
224
229
  command id configured in .agent-ops/config.json. Typed criteria additionally
225
230
  accept an acceptance object: mode (behavioral, invariant or review-only),
226
231
  bindings and optional baselineCommit/reviewOnlyReason. Mechanical bindings name
227
- configured acceptance runner IDs, checkIds, committed materials and optional
232
+ configured acceptance runner IDs, checkIds (the checks this criterion needs;
233
+ other runner output is ignored), committed materials and optional
228
234
  redCheckIds; tasks cannot supply commands. Creation freezes the baseline.
229
235
  Review-only example (mandatory repository policy commands still run):
230
236
 
@@ -232,7 +238,8 @@ Review-only example (mandatory repository policy commands still run):
232
238
 
233
239
  Pin a saved finding with task pin-finding --task <id> <report-digest:index>
234
240
  --expected-contract <hash> --criterion-file <path>. Revise/replan preserve the
235
- original goal and immutable existing baselines; obtain new verify and review.
241
+ original goal and existing baselines unless revise --baseline moves them all.
242
+ A revise that keeps the criteria keeps verification; every revise needs review.
236
243
  `,
237
244
  verify: `Usage: agent-ops verify [options]
238
245
 
@@ -308,9 +315,10 @@ voiding each other's verification and review. Run it from the main checkout.
308
315
  before Git runs; see agent-ops worktree list.
309
316
  finish <name>
310
317
  Once its task is complete, fast-forward the main checkout's
311
- branch to the worktree's (after a clean rebase and
312
- re-verification if the branch moved), record the task in
313
- git notes, and remove the worktree. One finish runs at a time.
318
+ branch to the worktree's, record the task in git notes, and
319
+ remove the worktree. If the branch moved, it rebases the
320
+ worktree and stops: verify and review again with the printed
321
+ --base, then finish again. One finish runs at a time.
314
322
  commit Stage every change in the current worktree and commit it with
315
323
  --message. Refuses the main checkout and a worktree another
316
324
  session owns, and needs no Git in the shell.
@@ -1,6 +1,5 @@
1
1
  import { PROJECT_LOOP_EVENTS, resolveProjectLoopRoot, runProjectLoop } from "../../../runtime/src/hooks/codex-loop.js";
2
2
  import { officeEnabled, officeLoopPhaseHint } from "./office-entry.js";
3
- import { routeDebugPrompt } from "./debug-intake.js";
4
3
  const MAX_LOOP_INPUT_BYTES = 64 * 1024;
5
4
  function isLoopHarness(value) {
6
5
  return value === "claude" || value === "codex";
@@ -74,11 +73,6 @@ export async function runLoopProcess(argv, io, dependencies = {}) {
74
73
  ? {}
75
74
  : { telemetryMaxBytes: dependencies.telemetryMaxBytes })
76
75
  });
77
- if (event === "UserPromptSubmit" && managedRoot !== null && result.exitCode === 0 && result.stdout === "" && result.stderr === "") {
78
- const context = await routeDebugPrompt(managedRoot, input, harness).catch(error => `DEBUG intake unavailable: ${error instanceof Error ? error.message : "preserve current state"}`);
79
- if (context !== null)
80
- io.writeStdout(harness === "claude" ? JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: context } }) : context + "\n");
81
- }
82
76
  if (result.stdout.length > 0) {
83
77
  io.writeStdout(result.stdout);
84
78
  }
@@ -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
  }
@@ -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}${result.rebased ? " after a clean rebase and re-verification" : ""}.`,
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}`]),
@@ -4,7 +4,7 @@ import { renderReviewResult } from "../../../../runtime/src/review/render.js";
4
4
  import { reviewReportResults } from "../../../../runtime/src/review/report.js";
5
5
  import { findPriorFailingFindings, findReviewAttestation, invalidateReviewAttestation, readReviewReportArtifact, reviewReportDigest, saveReviewAttestation, saveReviewReportArtifact } from "../../../../runtime/src/review/attestation.js";
6
6
  import { detectHostTarget, planReviewTargets, resolveReviewRole } from "../../../../runtime/src/review/roles.js";
7
- import { taskContractHash, treeContractHash, goalHash } from "../../../../runtime/src/task/contract.js";
7
+ import { evidenceMatchesContract, taskContractHash, treeContractHash, goalHash } from "../../../../runtime/src/task/contract.js";
8
8
  import { acceptanceCoverage, coverageDigest } from "../../../../runtime/src/verify/acceptance-coverage.js";
9
9
  import { resolveGitCommit } from "../../../../runtime/src/verify/change-surface.js";
10
10
  import { sha256 } from "../../../../runtime/src/fs/hash.js";
@@ -15,7 +15,6 @@ import { assertSafeSupportingPaths, isReviewerPolicyPath, resolveReviewScope, re
15
15
  import { calculateSourceFingerprint } from "../../../../runtime/src/verify/source-fingerprint.js";
16
16
  import { calculateConfigHash, isPassingVerificationEvidence } from "../../../../runtime/src/verify/evidence.js";
17
17
  import { validateEvidence } from "../../../../runtime/src/schema/validate.js";
18
- import { loadDebugProof, debugProofDigest } from "../../../../runtime/src/task/debug-state.js";
19
18
  import { logSurfaceResidualRisks, runSurfaceGate } from "../../../../runtime/src/surface/discovery.js";
20
19
  import { escapeReviewData } from "../../../../runtime/src/escape/link.js";
21
20
  import { logGateCatch } from "../../../../runtime/src/logging/local-log.js";
@@ -187,7 +186,7 @@ async function currentEvidence(options, context, criterionId, commandId, configH
187
186
  evidence.sourceFingerprint !== sourceFingerprint ||
188
187
  (context.records.some(r => r.task.id === (owner?.taskId ?? context.taskId) &&
189
188
  (r.task.goal !== undefined || (r.task.contractRevision ?? 0) > 0 || r.task.criteria.some(c => c.acceptance !== undefined))) &&
190
- evidence.taskContractHash !== taskContractHash(context.records.find(r => r.task.id === (owner?.taskId ?? context.taskId)).task))) {
189
+ !evidenceMatchesContract(evidence.taskContractHash, context.records.find(r => r.task.id === (owner?.taskId ?? context.taskId)).task))) {
191
190
  stale = true;
192
191
  continue;
193
192
  }
@@ -343,7 +342,7 @@ async function reviewEscapes(options, context, scope) {
343
342
  const origin = record.task.origin;
344
343
  if (origin === undefined)
345
344
  continue;
346
- const base = record.debug?.baselineCommit ?? record.createdSourceCommit ?? (scope?.mode === "base" ? scope.resolvedBase : undefined);
345
+ const base = record.createdSourceCommit ?? (scope?.mode === "base" ? scope.resolvedBase : undefined);
347
346
  try {
348
347
  if (base === undefined)
349
348
  throw new Error("no base");
@@ -770,7 +769,6 @@ export async function runReviewCommand(options) {
770
769
  }
771
770
  context.coverage = rows;
772
771
  context.binding = {
773
- ...(context.records.every(r => r.task.debug === undefined) || options.root === undefined ? {} : { debugProofs: Object.fromEntries(await Promise.all(context.records.filter(r => r.task.debug !== undefined).map(async (r) => [r.task.id, debugProofDigest(await loadDebugProof(options.root, r.task.debug.rootTaskId))]))) }),
774
772
  taskContractHash: taskContractHash(context.records.find(r => r.task.id === context.taskId).task),
775
773
  taskContracts: Object.fromEntries(context.records.map(r => [r.task.id, taskContractHash(r.task)])),
776
774
  ...(context.treeTasks === undefined ? {} : { treeContractHash: treeContractHash(context.records.map(r => r.task)) }),
@@ -816,12 +814,13 @@ export async function runReviewCommand(options) {
816
814
  contractArtifacts.push({ path: executionPath, digest: sha256(execution), content: execution });
817
815
  }
818
816
  }
817
+ // Compact: indentation alone pushed a 494 KiB 0.10.2 manifest past the 512 KiB bound.
819
818
  const content = redactSecrets(JSON.stringify({ binding: context.binding, coverage: context.coverage,
820
819
  executionPolicy: options.config,
820
+ ...(surfacePacket === undefined ? {} : { surface: surfacePacket }),
821
821
  artifacts: contractArtifacts.map(({ path, digest }) => ({ path, digest })),
822
- tasks: await Promise.all(context.records.map(async (r) => ({ task: r.task, revisions: r.revisions ?? [],
823
- ...(r.task.debug === undefined ? {} : { debugProof: await loadDebugProof(options.root, r.task.debug.rootTaskId) }) }))),
824
- superseded: context.historyRecords ?? [] }, null, 2));
822
+ tasks: context.records.map(r => ({ task: r.task, revisions: r.revisions ?? [] })),
823
+ superseded: context.historyRecords ?? [] }));
825
824
  const digest = sha256(content);
826
825
  const path = ".agent-ops/tasks/review-contracts/" + digest + ".json";
827
826
  contractManifest = { path, digest, content };
@@ -924,9 +923,7 @@ export async function runReviewCommand(options) {
924
923
  await options.tasks.treeContract(context.taskId) !== context.binding.treeContractHash) ||
925
924
  (context !== undefined && options.tasks !== undefined &&
926
925
  (await Promise.all(context.records.map(async (r) => taskContractHash((await options.tasks.status({ taskId: r.task.id })).task))))
927
- .some((hash, i) => hash !== taskContractHash(context.records[i].task))) ||
928
- (context?.binding?.debugProofs !== undefined && options.root !== undefined &&
929
- (await Promise.all(context.records.filter(r => r.task.debug !== undefined).map(async (r) => debugProofDigest(await loadDebugProof(options.root, r.task.debug.rootTaskId)) !== context.binding.debugProofs[r.task.id]))).some(Boolean))) {
926
+ .some((hash, i) => hash !== taskContractHash(context.records[i].task)))) {
930
927
  const changed = await persistReviewEvidence(options, sourceChangedResult(result), sourceFingerprint, context);
931
928
  return notRunEnvelope(changed);
932
929
  }
@@ -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,15 @@ 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
+ const { events, goal: _goal, ...brief } = state;
51
+ return { action, runId: state.runId, state: { ...brief, eventCount: events.length }, recovery: describeRunRecovery(state), message,
52
+ text: textFor(action, state, message) };
44
53
  }
45
54
  async function goalFromArgs(args, root) {
46
55
  if (args.goal !== undefined && args.goalFile !== undefined)
@@ -99,7 +108,7 @@ export async function runRunCommand(options) {
99
108
  return okEnvelope("RUN_LOGS", { ...stateData(action, state, `Read ${events.length} event(s) for run ${runId}.`), events });
100
109
  }
101
110
  if (action === "resume")
102
- return okEnvelope("RUN_RESUMED", data(action, await options.service.resume(runId)));
111
+ return okEnvelope("RUN_RESUMED", data(action, await options.service.resume(runId, options.args.retryApprovalIds)));
103
112
  if (action === "stop")
104
113
  return okEnvelope("RUN_STOP_REQUESTED", data(action, await options.service.stop(runId, options.args.answer ?? "user requested stop")));
105
114
  const questionId = options.args.questionId;