@kylecheng3146/agent-ops 0.7.3 → 0.8.3

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 (65) hide show
  1. package/README.md +2 -0
  2. package/dist/packages/cli/src/args.js +72 -1
  3. package/dist/packages/cli/src/bin.js +69 -10
  4. package/dist/packages/cli/src/cli.js +11 -3
  5. package/dist/packages/cli/src/codex-loop-process.js +6 -0
  6. package/dist/packages/cli/src/commands/batch.js +3 -1
  7. package/dist/packages/cli/src/commands/hook.js +4 -1
  8. package/dist/packages/cli/src/commands/review.js +9 -3
  9. package/dist/packages/cli/src/commands/task.js +44 -2
  10. package/dist/packages/cli/src/commands/update.js +18 -0
  11. package/dist/packages/cli/src/commands/verify.js +3 -1
  12. package/dist/packages/cli/src/debug-intake.js +245 -0
  13. package/dist/packages/cli/src/hook-process.js +155 -34
  14. package/dist/packages/cli/src/office-entry.js +34 -6
  15. package/dist/packages/cli/src/run-deps.js +8 -0
  16. package/dist/runtime/src/adapters/agy/config.js +3 -2
  17. package/dist/runtime/src/adapters/claude/config.js +7 -3
  18. package/dist/runtime/src/adapters/codex/config.js +10 -3
  19. package/dist/runtime/src/adapters/codex/events.js +2 -1
  20. package/dist/runtime/src/adapters/codex/input.js +20 -2
  21. package/dist/runtime/src/adapters/codex/output.js +4 -1
  22. package/dist/runtime/src/config/merge.js +1 -1
  23. package/dist/runtime/src/hooks/completion-gate.js +15 -0
  24. package/dist/runtime/src/install/harness.js +2 -0
  25. package/dist/runtime/src/install/plan.js +3 -3
  26. package/dist/runtime/src/install/profiles.js +2 -0
  27. package/dist/runtime/src/install/update.js +10 -2
  28. package/dist/runtime/src/office/activity.js +105 -0
  29. package/dist/runtime/src/office/art.js +1000 -0
  30. package/dist/runtime/src/office/collect.js +137 -10
  31. package/dist/runtime/src/office/layout.js +203 -0
  32. package/dist/runtime/src/office/page.js +883 -644
  33. package/dist/runtime/src/office/scene.js +38 -33
  34. package/dist/runtime/src/office/sessions.js +18 -1
  35. package/dist/runtime/src/office/snapshot.js +74 -1
  36. package/dist/runtime/src/parallel/finish.js +4 -2
  37. package/dist/runtime/src/parallel/manage.js +19 -2
  38. package/dist/runtime/src/review/attestation.js +4 -1
  39. package/dist/runtime/src/review/batch.js +10 -0
  40. package/dist/runtime/src/review/execute.js +10 -1
  41. package/dist/runtime/src/review/slots.js +59 -44
  42. package/dist/runtime/src/run/hosts/codex.js +1 -1
  43. package/dist/runtime/src/run/transport.js +3 -0
  44. package/dist/runtime/src/schema/validate.js +8 -2
  45. package/dist/runtime/src/security/permissions.js +3 -1
  46. package/dist/runtime/src/task/completion.js +20 -1
  47. package/dist/runtime/src/task/contract.js +1 -1
  48. package/dist/runtime/src/task/debug-intake.js +12 -0
  49. package/dist/runtime/src/task/debug-state.js +130 -0
  50. package/dist/runtime/src/task/debug-user-event.js +93 -0
  51. package/dist/runtime/src/task/debug.js +586 -0
  52. package/dist/runtime/src/task/service.js +46 -0
  53. package/dist/runtime/src/task/store.js +8 -1
  54. package/dist/runtime/src/verify/acceptance-replay.js +9 -4
  55. package/dist/runtime/src/verify/service.js +21 -2
  56. package/docs/en/guides/configuration.md +7 -0
  57. package/docs/en/guides/task-debug.md +111 -0
  58. package/docs/en/spec/harness-adapters.md +31 -4
  59. package/docs/zh-TW/guides/configuration.md +6 -0
  60. package/docs/zh-TW/guides/task-debug.md +66 -0
  61. package/docs/zh-TW/spec/harness-adapters.md +23 -4
  62. package/package.json +1 -1
  63. package/schemas/config.schema.json +30 -2
  64. package/schemas/manifest.schema.json +1 -0
  65. package/schemas/task.schema.json +62 -2
package/README.md CHANGED
@@ -257,6 +257,8 @@ Init and update pre-authorize `agent-ops run` for Claude Code and Codex.
257
257
 
258
258
  ```bash
259
259
  agent-ops init --scope project --harness codex,claude --profile run --yes
260
+ # an existing installation: add it, or later remove it (loop stays)
261
+ agent-ops update --auto-run on
260
262
  ```
261
263
 
262
264
  ## Quick start from a source checkout
@@ -75,6 +75,12 @@ export function parseArgs(argv) {
75
75
  let targetVersion;
76
76
  let title;
77
77
  let intent;
78
+ let taskGoal;
79
+ let debug = false;
80
+ let debugAction;
81
+ let payloadFile;
82
+ let commandId;
83
+ let eventId;
78
84
  let sessionId;
79
85
  let base;
80
86
  let parentBase;
@@ -95,6 +101,7 @@ export function parseArgs(argv) {
95
101
  const checkAuthTargets = [];
96
102
  let completionGate;
97
103
  let office;
104
+ let autoRun;
98
105
  let dryRun = false;
99
106
  let json = false;
100
107
  let yes = false;
@@ -209,6 +216,40 @@ export function parseArgs(argv) {
209
216
  index += 1;
210
217
  break;
211
218
  }
219
+ case "--goal":
220
+ if (taskGoal !== undefined)
221
+ duplicate(token);
222
+ taskGoal = readOptionValue(argv, index++, token);
223
+ break;
224
+ case "--debug":
225
+ if (debug)
226
+ duplicate(token);
227
+ debug = true;
228
+ break;
229
+ case "--debug-action": {
230
+ if (debugAction !== undefined)
231
+ duplicate(token);
232
+ const value = readOptionValue(argv, index++, token);
233
+ if (!["status", "hypothesis", "strategy", "investigate", "checkpoint", "resume", "extend"].includes(value))
234
+ invalidValue(token, value);
235
+ debugAction = value;
236
+ break;
237
+ }
238
+ case "--payload-file":
239
+ if (payloadFile !== undefined)
240
+ duplicate(token);
241
+ payloadFile = readOptionValue(argv, index++, token);
242
+ break;
243
+ case "--command-id":
244
+ if (commandId !== undefined)
245
+ duplicate(token);
246
+ commandId = readOptionValue(argv, index++, token);
247
+ break;
248
+ case "--event-id":
249
+ if (eventId !== undefined)
250
+ duplicate(token);
251
+ eventId = readOptionValue(argv, index++, token);
252
+ break;
212
253
  case "--tree": {
213
254
  if (tree)
214
255
  duplicate(token);
@@ -328,6 +369,16 @@ export function parseArgs(argv) {
328
369
  index += 1;
329
370
  break;
330
371
  }
372
+ case "--auto-run": {
373
+ if (autoRun !== undefined)
374
+ duplicate(token);
375
+ const value = readOptionValue(argv, index, token);
376
+ if (value !== "on" && value !== "off")
377
+ invalidValue(token, value);
378
+ autoRun = value;
379
+ index += 1;
380
+ break;
381
+ }
331
382
  case "--worktree": {
332
383
  if (worktree !== undefined) {
333
384
  duplicate(token);
@@ -442,7 +493,7 @@ export function parseArgs(argv) {
442
493
  "complete",
443
494
  "create",
444
495
  "export",
445
- "status", "revise", "replan", "pin-finding"
496
+ "status", "revise", "replan", "pin-finding", "debug"
446
497
  ].includes(token)) {
447
498
  action = token;
448
499
  break;
@@ -523,6 +574,7 @@ export function parseArgs(argv) {
523
574
  reviewTargets.length > 0 ||
524
575
  completionGate !== undefined ||
525
576
  office !== undefined ||
577
+ autoRun !== undefined ||
526
578
  sessionId !== undefined ||
527
579
  base !== undefined ||
528
580
  parentBase !== undefined ||
@@ -616,10 +668,22 @@ export function parseArgs(argv) {
616
668
  if (office !== undefined && command !== "init" && command !== "update") {
617
669
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--office may be used only with init or update.");
618
670
  }
671
+ if (autoRun !== undefined && command !== "update") {
672
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--auto-run may be used only with update; init selects the run profile.");
673
+ }
619
674
  if (worktree !== undefined && command !== "init" && command !== "update") {
620
675
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--worktree may be used only with init or update.");
621
676
  }
622
677
  const revisionAction = command === "task" && ["revise", "replan", "pin-finding"].includes(action ?? "");
678
+ if ((taskGoal !== undefined || debug) && !(command === "task" && action === "create"))
679
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--goal and --debug apply only to task create.");
680
+ if ((debugAction !== undefined || commandId !== undefined || eventId !== undefined || payloadFile !== undefined) && !(command === "task" && action === "debug"))
681
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Debug operation options apply only to task debug.");
682
+ if (command === "task" && action === "debug" && (taskId === undefined || debugAction === undefined || title !== undefined || intent !== undefined ||
683
+ parentTaskId !== undefined || criteria.length > 0 || evidence.length > 0 ||
684
+ ((debugAction === "hypothesis" || debugAction === "strategy") !== (payloadFile !== undefined)) ||
685
+ ((debugAction === "investigate") !== (commandId !== undefined)) || ((debugAction === "extend") !== (eventId !== undefined))))
686
+ throw new CliArgumentError("CLI_MISSING_VALUE", "Task debug requires --task, --debug-action and the operation's exact payload/command/event options.");
623
687
  if ((criterionFiles.length > 0 && !(command === "task" && (action === "create" || action === "pin-finding"))) ||
624
688
  (criteriaFile !== undefined && !(command === "task" && action === "revise")) ||
625
689
  (planFile !== undefined && !(command === "task" && action === "replan")) ||
@@ -790,9 +854,16 @@ export function parseArgs(argv) {
790
854
  ...(targetVersion === undefined ? {} : { targetVersion }),
791
855
  ...(title === undefined ? {} : { title }),
792
856
  ...(intent === undefined ? {} : { intent }),
857
+ ...(taskGoal === undefined ? {} : { goal: taskGoal }),
858
+ ...(debug ? { debug: true } : {}),
859
+ ...(debugAction === undefined ? {} : { debugAction }),
860
+ ...(payloadFile === undefined ? {} : { payloadFile }),
861
+ ...(commandId === undefined ? {} : { commandId }),
862
+ ...(eventId === undefined ? {} : { eventId }),
793
863
  ...(reviewTargets.length === 0 ? {} : { reviewTargets }),
794
864
  ...(completionGate === undefined ? {} : { completionGate }),
795
865
  ...(office === undefined ? {} : { office }),
866
+ ...(autoRun === undefined ? {} : { autoRun }),
796
867
  ...(criteria.length === 0 ? {} : { criteria }),
797
868
  ...(criterionFiles.length === 0 ? {} : { criterionFiles }),
798
869
  ...(criteriaFile === undefined ? {} : { criteriaFile }),
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { productionRunContext, runPhaseObserver } from "./run-deps.js";
2
+ import { advancePhaseObserver, productionRunContext } from "./run-deps.js";
3
3
  import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
4
4
  import { recordRunVerification } from "../../../runtime/src/run/verification.js";
5
5
  import { runOwnedLocalProof } from "./owned-run-step.js";
6
6
  import { runRunCommand } from "./commands/run.js";
7
7
  import { runOfficeCommand } from "./commands/office.js";
8
- import { ensureBackgroundOffice, observeOfficeSession, openOfficeBrowser, serveOffice } from "./office-entry.js";
8
+ import { ensureBackgroundOffice, observeOfficeSession, officePhaseReporter, openOfficeBrowser, serveOffice } from "./office-entry.js";
9
9
  import { readFile, readdir, stat } from "node:fs/promises";
10
10
  import { randomUUID } from "node:crypto";
11
11
  import { homedir } from "node:os";
@@ -42,6 +42,7 @@ import { hasFreshVerification, runReviewCommand } from "./commands/review.js";
42
42
  import { runBatchCommand } from "./commands/batch.js";
43
43
  import { createSourceGuard } from "../../../runtime/src/review/batch-guard.js";
44
44
  import { withReviewSlot } from "../../../runtime/src/review/slots.js";
45
+ import { withActivity } from "../../../runtime/src/office/activity.js";
45
46
  import { memoizePreflight } from "../../../runtime/src/review/batch.js";
46
47
  import { createReviewExecutor, ReviewInterruptedError } from "../../../runtime/src/review/execute.js";
47
48
  import { probeReviewTargetDetailed } from "../../../runtime/src/review/probe.js";
@@ -60,6 +61,16 @@ import { runWorktreeCommand } from "./commands/parallel.js";
60
61
  import { ensureSessionWorktree, readWorktreeRecord, resolveCheckouts } from "../../../runtime/src/parallel/service.js";
61
62
  import { listWorktrees, branchLockDoctorResult, worktreeDoctorResult } from "../../../runtime/src/parallel/manage.js";
62
63
  import { detectGhostFiles, ghostFilesDoctorResult } from "../../../runtime/src/install/doctor.js";
64
+ /** The repository's common Git directory, where display-only office records live; undefined when git cannot say. */
65
+ async function officeCommonDir(dir) {
66
+ try {
67
+ const result = await gitRunner(dir).run(["rev-parse", "--path-format=absolute", "--git-common-dir"]);
68
+ return result.exitCode === 0 ? new TextDecoder().decode(result.stdout).trim() || undefined : undefined;
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ }
63
74
  const HOOK_RUNTIME_PATH = fileURLToPath(new URL("./hook-entry.js", import.meta.url));
64
75
  async function readOptionalText(path) {
65
76
  try {
@@ -413,6 +424,7 @@ else {
413
424
  calculateTrustBinding: async (config) => await plannedTrustBinding(root, config),
414
425
  confirm: async (plan, trust) => await confirmPlan(formatUpdatePlan(plan, trust)),
415
426
  promptOffice: async (message, enabled) => await selectYesNo(message, { input: process.stdin, output: process.stdout }, enabled),
427
+ promptAutoRun: async (message, enabled) => await selectYesNo(message, { input: process.stdin, output: process.stdout }, enabled),
416
428
  promptWorktree: async (message) => await selectYesNo(message, { input: process.stdin, output: process.stdout }, true),
417
429
  ...(updateArgs.targetVersion === undefined
418
430
  ? {}
@@ -450,13 +462,13 @@ else {
450
462
  ? await resolveCommandSessionId(root)
451
463
  : sessionIdFromEnvironment() ?? await readRecordedSessionId(root));
452
464
  if (args.action === "advance") {
465
+ const advanceSession = sessionId ?? await resolveCommandSessionId(root);
453
466
  return await runAdvanceCommand({
454
467
  cwd: root,
455
- sessionId: sessionId ?? await resolveCommandSessionId(root),
468
+ sessionId: advanceSession,
456
469
  parentTaskId: args.taskId,
457
470
  deps: worktreeDependencies(),
458
- ...(process.env.AGENT_OPS_RUN_ID === undefined || process.env.AGENT_OPS_WORKER_ID === undefined ? {} :
459
- { onPhase: await runPhaseObserver(root, process.env.AGENT_OPS_RUN_ID, process.env.AGENT_OPS_WORKER_ID) })
471
+ onPhase: await advancePhaseObserver(root, advanceSession)
460
472
  });
461
473
  }
462
474
  const createConfig = args.action === "create"
@@ -476,6 +488,7 @@ else {
476
488
  return await runTaskCommand({
477
489
  args,
478
490
  service: taskService,
491
+ ...(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"; })() }),
479
492
  ...(policyConfigHash === undefined ? {} : { policyConfigHash }),
480
493
  ...(sessionId === undefined ? {} : { sessionId }),
481
494
  ...(fromMain
@@ -521,16 +534,53 @@ else {
521
534
  const slotDir = commonDirResult.exitCode === 0
522
535
  ? new TextDecoder().decode(commonDirResult.stdout).trim()
523
536
  : undefined;
537
+ let reviewActivity;
538
+ const inActivity = (executor) => async (request) => await withActivity(slotDir, { kind: "review", root,
539
+ ...(reviewArgs.taskId === undefined ? {} : { taskId: reviewArgs.taskId }),
540
+ ...(reviewSessionId === undefined ? {} : { sessionId: reviewSessionId }) }, async (handle) => {
541
+ reviewActivity = handle;
542
+ try {
543
+ return await executor(request);
544
+ }
545
+ finally {
546
+ reviewActivity = undefined;
547
+ }
548
+ });
549
+ const debugRecord = reviewArgs.taskId === undefined ? undefined : await taskService.status({ taskId: reviewArgs.taskId });
550
+ const inDebug = (executor) => debugRecord?.task.debug === undefined ? executor : async (request) => await taskService.debugService().review(debugRecord.task.id, () => executor(request));
524
551
  const inSlot = (executor) => slotDir === undefined
525
- ? executor
552
+ ? inDebug(executor)
526
553
  : async (request) => await withReviewSlot({
527
554
  dir: slotDir,
528
555
  signal,
529
556
  holder: { root, ...(reviewArgs.taskId === undefined ? {} : { taskId: reviewArgs.taskId }) },
557
+ ...(debugRecord?.task.debug === undefined || reviewSessionId === undefined
558
+ ? {}
559
+ : (() => {
560
+ let paused = false;
561
+ const debug = taskService.debugService();
562
+ return {
563
+ onWaitStart: async () => {
564
+ if (paused)
565
+ return;
566
+ const current = await debug.root(debugRecord.task.id).catch(() => null);
567
+ if (current?.debug.foreground?.sessionId !== reviewSessionId)
568
+ return;
569
+ await debug.foregroundStop(debugRecord.task.id, reviewSessionId);
570
+ paused = true;
571
+ },
572
+ onWaitEnd: async () => {
573
+ if (!paused)
574
+ return;
575
+ await debug.foregroundStart(debugRecord.task.id, reviewSessionId);
576
+ paused = false;
577
+ }
578
+ };
579
+ })()),
530
580
  onWait: (line) => {
531
581
  process.stderr.write(`${progressPrefix}${line}\n`);
532
582
  }
533
- }, async () => await executor(request));
583
+ }, async () => await inDebug(executor)(request));
534
584
  return {
535
585
  args: reviewArgs,
536
586
  authorized: reviewArgs.yes,
@@ -549,7 +599,8 @@ else {
549
599
  currentPolicyConfigHash: async () => calculateConfigHash((await loadEffectiveConfig(root, reviewArgs.scope === "user" ? "user" : "project")).config),
550
600
  config: reviewConfig,
551
601
  evidenceStore: new FileEvidenceStore(root, root),
552
- execute: inSlot(createReviewExecutor({
602
+ execute: inActivity(inSlot(createReviewExecutor({
603
+ ...(debugRecord?.task.debug === undefined ? {} : { remainingBudgetMs: () => taskService.debugService().remaining(debugRecord.task.id) }),
553
604
  runner: registeredRunProofRunner(),
554
605
  targets: configuredReviewTargets,
555
606
  cwd: root,
@@ -575,8 +626,11 @@ else {
575
626
  signal,
576
627
  onProgress: (line) => {
577
628
  process.stderr.write(`${progressPrefix}${line}\n`);
629
+ },
630
+ onRound: (round, target, targets) => {
631
+ void reviewActivity?.update({ round, target, targets });
578
632
  }
579
- }))
633
+ })))
580
634
  };
581
635
  };
582
636
  if (args.command === "review") {
@@ -662,7 +716,10 @@ else {
662
716
  signal: controller.signal,
663
717
  onProgress: (line) => {
664
718
  process.stderr.write(`batch: ${line}\n`);
665
- }
719
+ },
720
+ // The batch's tasks name the sessions whose people follow its steps.
721
+ phaseReporter: async (taskIds) => await officePhaseReporter(root, (await new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root).read()).sessions
722
+ .filter(({ taskId }) => taskIds.includes(taskId)).map(({ sessionId }) => sessionId))
666
723
  });
667
724
  if (interruptedBy !== undefined) {
668
725
  process.exit(interruptedBy === "SIGINT" ? 130 : 143);
@@ -681,9 +738,11 @@ else {
681
738
  const merged = await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project");
682
739
  const config = merged.config;
683
740
  const trustStatus = await repositoryTrust(root, config, CLI_VERSION);
741
+ const verifySessionId = sessionIdFromEnvironment();
684
742
  return await runVerifyCommand({
685
743
  args,
686
744
  taskService,
745
+ activity: { commonDir: await officeCommonDir(root), root, ...(verifySessionId === undefined ? {} : { sessionId: verifySessionId }) },
687
746
  service: { verify: async (taskId) => {
688
747
  const run = await runPolicyContext(root);
689
748
  const report = await new VerificationService({
@@ -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|advance|complete|archive|export>
28
+ task <create|status|attach|revise|replan|pin-finding|debug|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
@@ -45,6 +45,7 @@ Options:
45
45
  --review-target <codex|agy|claude> Repeatable init option; review pair order
46
46
  --completion-gate Init only: enable the project-loop completion gate
47
47
  --office <on|off> Init/update: enable or disable Office (Preview)
48
+ --auto-run <on|off> Update: add or remove the run profile (loop stays)
48
49
  --worktree <auto|off> Init/update: configure session worktree isolation
49
50
  --check-auth Doctor only: probe selected review targets'
50
51
  authentication with one real call
@@ -152,6 +153,7 @@ Options:
152
153
  --harness <all|both|agy|claude|codex|opencode|comma-separated>
153
154
  --target-version <version> Offline-capable update target
154
155
  --office <on|off> Office (Preview); preserve choice unless supplied
156
+ --auto-run <on|off> Add or remove the run profile (loop stays); preserve unless supplied
155
157
  --worktree <auto|off> Configure session worktree isolation
156
158
  --dry-run
157
159
  --json
@@ -168,13 +170,19 @@ Options:
168
170
  --json
169
171
  --yes
170
172
  `,
171
- task: `Usage: agent-ops task <create|status|attach|revise|replan|pin-finding|advance|complete|archive|export> [options]
173
+ task: `Usage: agent-ops task <create|status|attach|revise|replan|pin-finding|debug|advance|complete|archive|export> [options]
172
174
 
173
175
  Manage independent task acceptance state. Task commands accept none of
174
176
  --harness, --profile or --dry-run. Only advance accepts --yes.
175
177
 
176
178
  Options:
177
179
  --title <text> create
180
+ --goal <text> create: immutable original issue/goal
181
+ --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
184
+ --command-id <id> debug investigate: trusted configured command
185
+ --event-id <native-event-id|latest> debug extend: authentic human continue event
178
186
  --intent <text> create: intended behavior and constraints before editing
179
187
  --criterion <json> create, repeatable, two to five total
180
188
  --criterion-file <path> create: one criterion object, repeatable; pin-finding: exactly one
@@ -186,7 +194,7 @@ Options:
186
194
  --baseline <git-ref> create/revise: baseline for new mechanical requirements
187
195
  --parent <task-id> create: record a subtask; status: list subtasks
188
196
  --task <id> status, attach, revise, replan, pin-finding, complete, archive, export, advance
189
- --session <id> create, attach, status, advance
197
+ --session <id> create, attach, status, advance, debug
190
198
  --evidence <criterion-id=reference> complete, repeatable
191
199
  --base <git-ref> complete: a clean committed range
192
200
  --yes advance: authorize the two review sessions
@@ -1,5 +1,6 @@
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";
3
4
  const MAX_LOOP_INPUT_BYTES = 64 * 1024;
4
5
  function isLoopHarness(value) {
5
6
  return value === "claude" || value === "codex";
@@ -73,6 +74,11 @@ export async function runLoopProcess(argv, io, dependencies = {}) {
73
74
  ? {}
74
75
  : { telemetryMaxBytes: dependencies.telemetryMaxBytes })
75
76
  });
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
+ }
76
82
  if (result.stdout.length > 0) {
77
83
  io.writeStdout(result.stdout);
78
84
  }
@@ -66,6 +66,7 @@ export async function runBatchCommand(options) {
66
66
  };
67
67
  }
68
68
  const width = args.width ?? DEFAULT_BATCH_WIDTH;
69
+ const report = await options.phaseReporter?.(plan.map((item) => item.record.task.id)).catch(() => undefined);
69
70
  const outcomes = await runBatch(plan.map((item) => item.record.task.id), {
70
71
  isVerified: (id) => options.isVerified(id, base(id)),
71
72
  verify: (id, signal) => options.verify(id, base(id), signal),
@@ -74,7 +75,8 @@ export async function runBatchCommand(options) {
74
75
  }, {
75
76
  width,
76
77
  ...(options.signal === undefined ? {} : { signal: options.signal }),
77
- ...(options.onProgress === undefined ? {} : { onProgress: options.onProgress })
78
+ ...(options.onProgress === undefined ? {} : { onProgress: options.onProgress }),
79
+ ...(report === undefined ? {} : { onPhase: (phase) => void report(phase).catch(() => { }) })
78
80
  });
79
81
  const byId = new Map(outcomes.map((outcome) => [outcome.id, outcome]));
80
82
  const tasks = plan.map((item) => {
@@ -28,7 +28,9 @@ export async function runHookCommand(options) {
28
28
  const { capabilities } = options.config.profiles.length === 0
29
29
  ? { capabilities: [] }
30
30
  : resolveCapabilities(options.config);
31
- const input = JSON.parse(options.stdin);
31
+ const input = options.input === undefined
32
+ ? JSON.parse(options.stdin)
33
+ : options.input;
32
34
  const descriptor = harnessDescriptor(options.harness);
33
35
  const normalized = normalizeHookInput(options.harness, input);
34
36
  if (normalized === null ||
@@ -55,6 +57,7 @@ export async function runHookCommand(options) {
55
57
  ? {}
56
58
  : { worktreeEnter: options.worktreeEnter })
57
59
  });
60
+ options.onResult?.(result);
58
61
  return descriptor.runtime.formatOutput(options.event, result);
59
62
  }
60
63
  catch {
@@ -15,6 +15,7 @@ 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";
18
19
  import { okEnvelope } from "../output.js";
19
20
  /**
20
21
  * Review runs against one target. Argument parsing already rejects a
@@ -640,6 +641,7 @@ export async function runReviewCommand(options) {
640
641
  }
641
642
  context.coverage = rows;
642
643
  context.binding = {
644
+ ...(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))]))) }),
643
645
  taskContractHash: taskContractHash(context.records.find(r => r.task.id === context.taskId).task),
644
646
  taskContracts: Object.fromEntries(context.records.map(r => [r.task.id, taskContractHash(r.task)])),
645
647
  ...(context.treeTasks === undefined ? {} : { treeContractHash: treeContractHash(context.records.map(r => r.task)) }),
@@ -682,7 +684,8 @@ export async function runReviewCommand(options) {
682
684
  const content = redactSecrets(JSON.stringify({ binding: context.binding, coverage: context.coverage,
683
685
  executionPolicy: options.config,
684
686
  artifacts: contractArtifacts.map(({ path, digest }) => ({ path, digest })),
685
- tasks: context.records.map(r => ({ task: r.task, revisions: r.revisions ?? [] })),
687
+ tasks: await Promise.all(context.records.map(async (r) => ({ task: r.task, revisions: r.revisions ?? [],
688
+ ...(r.task.debug === undefined ? {} : { debugProof: await loadDebugProof(options.root, r.task.debug.rootTaskId) }) }))),
686
689
  superseded: context.historyRecords ?? [] }, null, 2));
687
690
  const digest = sha256(content);
688
691
  const path = ".agent-ops/tasks/review-contracts/" + digest + ".json";
@@ -719,7 +722,7 @@ export async function runReviewCommand(options) {
719
722
  const priorFindings = options.root === undefined || context === undefined
720
723
  ? []
721
724
  : await findPriorFailingFindings(options.root, context.taskId);
722
- const result = await runIndependentReview({
725
+ const executeReview = async () => await runIndependentReview({
723
726
  invocation: {
724
727
  harness: target,
725
728
  plannedTargets,
@@ -738,6 +741,7 @@ export async function runReviewCommand(options) {
738
741
  reason: "missing-cli"
739
742
  }))
740
743
  });
744
+ const result = await executeReview();
741
745
  if (scope !== undefined && result.report !== undefined && options.root !== undefined) {
742
746
  try {
743
747
  if (result.report.changedFilesInspected.length !== scope.changedFiles.length ||
@@ -781,7 +785,9 @@ export async function runReviewCommand(options) {
781
785
  await options.tasks.treeContract(context.taskId) !== context.binding.treeContractHash) ||
782
786
  (context !== undefined && options.tasks !== undefined &&
783
787
  (await Promise.all(context.records.map(async (r) => taskContractHash((await options.tasks.status({ taskId: r.task.id })).task))))
784
- .some((hash, i) => hash !== taskContractHash(context.records[i].task)))) {
788
+ .some((hash, i) => hash !== taskContractHash(context.records[i].task))) ||
789
+ (context?.binding?.debugProofs !== undefined && options.root !== undefined &&
790
+ (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))) {
785
791
  const changed = await persistReviewEvidence(options, sourceChangedResult(result), sourceFingerprint, context);
786
792
  return notRunEnvelope(changed);
787
793
  }
@@ -60,7 +60,7 @@ function taskAction(args) {
60
60
  "complete",
61
61
  "create",
62
62
  "export",
63
- "status", "revise", "replan", "pin-finding"
63
+ "status", "revise", "replan", "pin-finding", "debug"
64
64
  ].includes(args.action)) {
65
65
  throw new AgentOpsError("TASK_ACTION_INVALID", "A supported task action is required.");
66
66
  }
@@ -72,7 +72,7 @@ function taskEnvelope(action, code, message, record) {
72
72
  message,
73
73
  record,
74
74
  contractHash: taskContractHash(record.task),
75
- text: renderTaskMarkdown(record) + "\nContract: " + taskContractHash(record.task)
75
+ text: renderTaskMarkdown(record) + "\nContract: " + taskContractHash(record.task) + (record.debug === undefined ? "" : "\nDEBUG: " + JSON.stringify(record.debug))
76
76
  });
77
77
  }
78
78
  function renderTaskList(records) {
@@ -91,11 +91,15 @@ export async function runTaskCommand(options) {
91
91
  if (options.args.title === undefined) {
92
92
  throw new AgentOpsError("TASK_TITLE_REQUIRED", "Task creation requires --title.");
93
93
  }
94
+ if (options.args.debug)
95
+ await options.service.assertDebugBaseline();
94
96
  const worktree = sessionId === undefined || options.sessionWorktree === undefined
95
97
  ? undefined
96
98
  : await options.sessionWorktree(sessionId);
97
99
  const record = await (worktree?.service ?? options.service).create({
98
100
  title: options.args.title,
101
+ ...(options.args.goal === undefined ? {} : { goal: options.args.goal }),
102
+ ...(options.args.debug ? { debug: {} } : {}),
99
103
  ...(options.args.intent === undefined ? {} : { intent: options.args.intent }),
100
104
  criteria: [...(options.args.criteria ?? []).map(parseCriterion),
101
105
  ...await Promise.all((options.args.criterionFiles ?? []).map(async (path) => await jsonFile(path)))]
@@ -109,6 +113,12 @@ export async function runTaskCommand(options) {
109
113
  : { parentTaskId: options.args.parentTaskId }),
110
114
  ...(sessionId === undefined ? {} : { sessionId })
111
115
  });
116
+ // An explicit CLI DEBUG intake may be the only start signal on hosts
117
+ // without a prompt hook (notably agy). The native session owns the clock;
118
+ // foregroundStart is idempotent for a repeated hook or command.
119
+ if (options.args.debug && sessionId !== undefined) {
120
+ await (worktree?.service ?? options.service).debugService().foregroundStart(record.task.id, sessionId);
121
+ }
112
122
  const created = taskEnvelope(action, "TASK_CREATED", sessionId === undefined
113
123
  ? `Created task ${record.task.id}.`
114
124
  : record.task.parentTaskId === undefined
@@ -134,6 +144,38 @@ export async function runTaskCommand(options) {
134
144
  }
135
145
  };
136
146
  }
147
+ if (action === "debug") {
148
+ const id = requireTaskId(options.args), service = options.service.debugService();
149
+ switch (options.args.debugAction) {
150
+ case "hypothesis":
151
+ await service.hypothesis(id, await jsonFile(options.args.payloadFile));
152
+ break;
153
+ case "strategy":
154
+ await service.strategy(id, await jsonFile(options.args.payloadFile));
155
+ break;
156
+ case "investigate":
157
+ await service.investigate(id, options.args.commandId, options.trusted === true);
158
+ break;
159
+ case "checkpoint":
160
+ if (sessionId === undefined)
161
+ throw new AgentOpsError("TASK_SESSION_REQUIRED", "Debug checkpoint requires a session.");
162
+ await service.checkpoint(id, sessionId);
163
+ break;
164
+ case "resume":
165
+ if (sessionId === undefined)
166
+ throw new AgentOpsError("TASK_SESSION_REQUIRED", "Debug resume requires a session.");
167
+ await service.resume(id, sessionId);
168
+ break;
169
+ case "extend":
170
+ if (sessionId === undefined)
171
+ throw new AgentOpsError("TASK_SESSION_REQUIRED", "Debug extension requires a native session.");
172
+ await service.extend(id, sessionId, options.args.eventId);
173
+ break;
174
+ case "status": break;
175
+ default: throw new AgentOpsError("TASK_DEBUG_ACTION_INVALID", "A supported debug operation is required.");
176
+ }
177
+ return taskEnvelope(action, "TASK_DEBUG_UPDATED", "Original issue debug state preserved.", await service.root(id));
178
+ }
137
179
  if (action === "revise") {
138
180
  const payload = await jsonFile(options.args.criteriaFile);
139
181
  if (!Array.isArray(payload))
@@ -1,6 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { loadConfigFile } from "../../../../runtime/src/config/load.js";
4
+ import { parseInstallManifest } from "../../../../runtime/src/fs/manifest.js";
4
5
  import { detectWorktreeSetup } from "../../../../runtime/src/install/parallel-setup.js";
5
6
  import { applyUpdatePlan, createUpdatePlan } from "../../../../runtime/src/install/update.js";
6
7
  import { okEnvelope } from "../output.js";
@@ -17,6 +18,7 @@ export function formatUpdatePlan(plan, trust) {
17
18
  metadata: [
18
19
  `Target version: ${plan.targetVersion}`,
19
20
  `Office (Preview): ${plan.installation.config.features.office?.enabled === true ? "enabled" : "disabled"}`,
21
+ `Auto-run: ${plan.installation.config.profiles.includes("run") ? "enabled" : "disabled"}`,
20
22
  `Harness: ${plan.installation.harness.join(", ")}`,
21
23
  ...(plan.installation.config.worktree === undefined
22
24
  ? []
@@ -73,6 +75,14 @@ async function existingConfigHasWorktree(root) {
73
75
  return false;
74
76
  }
75
77
  }
78
+ async function installedHarness(root) {
79
+ try {
80
+ return parseInstallManifest(await readFile(join(root, ".agent-ops", "manifest.json"), "utf8")).harness;
81
+ }
82
+ catch {
83
+ return [];
84
+ }
85
+ }
76
86
  export async function runUpdateCommand(options) {
77
87
  let worktree;
78
88
  if (options.args.worktree === "auto") {
@@ -102,10 +112,18 @@ export async function runUpdateCommand(options) {
102
112
  const { config } = await loadConfigFile(join(options.root, ".agent-ops", "config.json"));
103
113
  officeEnabled = await options.promptOffice("Enable Office (Preview)?", config.features.office?.enabled === true);
104
114
  }
115
+ let autoRun = options.args.autoRun === undefined ? undefined : options.args.autoRun === "on";
116
+ if (autoRun === undefined && options.promptAutoRun !== undefined && options.isTTY && !options.args.yes &&
117
+ (options.platform ?? process.platform) === "darwin" &&
118
+ (options.args.harness ?? await installedHarness(options.root)).some((harness) => harness === "claude" || harness === "codex")) {
119
+ const { config } = await loadConfigFile(join(options.root, ".agent-ops", "config.json"));
120
+ autoRun = await options.promptAutoRun("Enable auto-run (hand changes needing more than five acceptance criteria to agent-ops run)?", config.profiles.includes("run"));
121
+ }
105
122
  const plan = await createUpdatePlan({
106
123
  root: options.root,
107
124
  adapters: options.adapters,
108
125
  ...(officeEnabled === undefined ? {} : { officeEnabled }),
126
+ ...(autoRun === undefined ? {} : { autoRun }),
109
127
  ...(options.args.harness === undefined
110
128
  ? {}
111
129
  : { harness: options.args.harness }),
@@ -1,3 +1,4 @@
1
+ import { withActivity } from "../../../../runtime/src/office/activity.js";
1
2
  import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
2
3
  import { redactSecrets } from "../../../../runtime/src/security/redact.js";
3
4
  import { safeTaskText } from "../../../../runtime/src/task/render.js";
@@ -90,7 +91,8 @@ async function resolveTaskId(options) {
90
91
  }
91
92
  export async function runVerifyCommand(options) {
92
93
  try {
93
- const report = publicReport(await options.service.verify(await resolveTaskId(options)));
94
+ const taskId = await resolveTaskId(options), activity = options.activity;
95
+ const report = publicReport(await withActivity(activity?.commonDir, { kind: "verify", root: activity?.root ?? "", taskId, ...(activity?.sessionId === undefined ? {} : { sessionId: activity.sessionId }) }, async () => await options.service.verify(taskId)));
94
96
  const text = formatReport(report);
95
97
  if (report.status === "PASS") {
96
98
  return okEnvelope("VERIFICATION_PASSED", {