@kylecheng3146/agent-ops 0.10.2 → 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 (47) hide show
  1. package/dist/packages/cli/src/args.js +8 -43
  2. package/dist/packages/cli/src/bin.js +28 -29
  3. package/dist/packages/cli/src/cli.js +12 -14
  4. package/dist/packages/cli/src/codex-loop-process.js +0 -6
  5. package/dist/packages/cli/src/commands/review.js +5 -10
  6. package/dist/packages/cli/src/commands/run.js +3 -1
  7. package/dist/packages/cli/src/commands/task.js +6 -50
  8. package/dist/packages/cli/src/commands/verify.js +7 -3
  9. package/dist/packages/cli/src/default-base.js +26 -0
  10. package/dist/packages/cli/src/hook-process.js +19 -83
  11. package/dist/packages/cli/src/run-entry.js +66 -21
  12. package/dist/runtime/src/adapters/agy/config.js +2 -3
  13. package/dist/runtime/src/adapters/claude/config.js +2 -2
  14. package/dist/runtime/src/adapters/codex/config.js +2 -5
  15. package/dist/runtime/src/hooks/completion-gate.js +0 -15
  16. package/dist/runtime/src/install/harness.js +3 -3
  17. package/dist/runtime/src/install/plan.js +3 -3
  18. package/dist/runtime/src/install/profiles.js +0 -2
  19. package/dist/runtime/src/office/snapshot.js +4 -1
  20. package/dist/runtime/src/parallel/manage.js +0 -3
  21. package/dist/runtime/src/review/execute.js +68 -4
  22. package/dist/runtime/src/run/verification.js +2 -2
  23. package/dist/runtime/src/schema/validate.js +10 -5
  24. package/dist/runtime/src/task/completion.js +4 -42
  25. package/dist/runtime/src/task/contract.js +11 -0
  26. package/dist/runtime/src/task/service.js +34 -57
  27. package/dist/runtime/src/task/store.js +3 -7
  28. package/dist/runtime/src/verify/acceptance-coverage.js +2 -3
  29. package/dist/runtime/src/verify/acceptance-protocol.js +4 -3
  30. package/dist/runtime/src/verify/acceptance-replay.js +4 -7
  31. package/dist/runtime/src/verify/service.js +6 -23
  32. package/dist/runtime/src/verify/source-fingerprint.js +17 -4
  33. package/docs/en/guides/task-debug.md +39 -133
  34. package/docs/en/spec/acceptance-and-evidence.md +31 -23
  35. package/docs/en/spec/harness-adapters.md +9 -11
  36. package/docs/en/spec/loop-engineering.md +11 -5
  37. package/docs/zh-TW/guides/task-debug.md +27 -73
  38. package/docs/zh-TW/spec/acceptance-and-evidence.md +18 -18
  39. package/docs/zh-TW/spec/harness-adapters.md +7 -8
  40. package/docs/zh-TW/spec/loop-engineering.md +9 -4
  41. package/package.json +1 -1
  42. package/schemas/config.schema.json +1 -21
  43. package/dist/packages/cli/src/debug-intake.js +0 -245
  44. package/dist/runtime/src/task/debug-intake.js +0 -15
  45. package/dist/runtime/src/task/debug-state.js +0 -135
  46. package/dist/runtime/src/task/debug-user-event.js +0 -160
  47. package/dist/runtime/src/task/debug.js +0 -632
@@ -14,7 +14,6 @@ import { evaluateWorktreeEnter } from "../../../runtime/src/parallel/enter.js";
14
14
  import { evaluateWorktreeWrite, resolveMainRoot } from "../../../runtime/src/parallel/guard.js";
15
15
  import { ensureSessionWorktree, recordSessionOrigin } from "../../../runtime/src/parallel/service.js";
16
16
  import { CompletionGateService } from "../../../runtime/src/hooks/completion-gate.js";
17
- import { closeDebugSession, debugStopGate, interruptDebugSession, routeDebugPrompt, startDebugSession } from "./debug-intake.js";
18
17
  import { TaskService } from "../../../runtime/src/task/service.js";
19
18
  import { FileTaskStore } from "../../../runtime/src/task/store.js";
20
19
  import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
@@ -269,8 +268,6 @@ async function closeOfficeSession(io, dependencies) {
269
268
  root, harness: "claude", event: "SessionEnd", input, validated: true,
270
269
  ...(typeof fields.session_id === "string" ? { sessionId: fields.session_id } : {})
271
270
  });
272
- if (typeof fields.session_id === "string")
273
- await closeDebugSession(root, fields.session_id, "claude").catch(() => undefined);
274
271
  }
275
272
  catch {
276
273
  // Office never blocks a session, least of all one that is ending.
@@ -307,12 +304,8 @@ async function promptSubmitted(harness, io, dependencies) {
307
304
  const prompt = fields.prompt;
308
305
  if (typeof prompt !== "string" || prompt.length === 0 || prompt.length > 64 * 1024 || prompt.includes("\0"))
309
306
  return 0;
310
- if (evaluateGuardrail({ kind: "content", content: prompt, scope: root }).action !== "block") {
311
- const context = await routeDebugPrompt(root, input, harness).catch(error => `DEBUG intake unavailable: ${error instanceof Error ? error.message : "preserve current state"}`);
312
- if (context !== null)
313
- io.writeStdout(harness === "claude" ? JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: context } }) : context + "\n");
307
+ if (evaluateGuardrail({ kind: "content", content: prompt, scope: root }).action !== "block")
314
308
  return 0;
315
- }
316
309
  if (!config.profiles.includes("guardrails") && !config.profiles.includes("loop") && !config.profiles.includes("run"))
317
310
  return 0;
318
311
  const reason = "agent-ops blocked a suspected secret.";
@@ -329,28 +322,14 @@ async function promptSubmitted(harness, io, dependencies) {
329
322
  }
330
323
  export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
331
324
  const [harness, event] = argv;
332
- if (harness === "codex" && event === "Interrupt") {
333
- try {
334
- const raw = await readStdin(io.stdin), input = parseInput(raw);
335
- if (typeof input !== "object" || input === null || Array.isArray(input))
336
- return 0;
337
- const fields = input;
338
- if (typeof fields.session_id !== "string" || fields.session_id.length === 0)
339
- return 0;
340
- const root = dependencies.root ?? (typeof fields.cwd === "string" ? fields.cwd : process.cwd());
341
- await interruptDebugSession(root, fields.session_id, raw);
342
- }
343
- catch {
344
- io.writeStdout(JSON.stringify({ systemMessage: "agent-ops: DEBUG interrupt progress could not be sealed; preserve the task and run agent-ops doctor before resuming." }));
345
- }
325
+ // Pre-0.11 installs registered Interrupt only for DEBUG; it is now a no-op until update removes it.
326
+ if (harness === "codex" && event === "Interrupt")
346
327
  return 0;
347
- }
348
328
  if (harness === "claude" && event === "SessionEnd")
349
329
  return await closeOfficeSession(io, dependencies);
350
330
  if ((harness === "claude" || harness === "codex") && event === "UserPromptSubmit")
351
331
  return await promptSubmitted(harness, io, dependencies);
352
- // The generic completion gate requires an approval escape hatch. Codex
353
- // only enforces DEBUG through runtime checkpoints and native human control.
332
+ // The generic completion gate requires an approval escape hatch, which Codex lacks.
354
333
  const completionGateInstalled = (harness === "agy" || harness === "claude") &&
355
334
  event === "Stop" &&
356
335
  argv.includes("--completion-gate");
@@ -400,18 +379,7 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
400
379
  }
401
380
  }
402
381
  let configOutcome = await hookConfigOutcome(root, dependencies.loadConfig);
403
- const nativeStop = normalizeHookInput(harnessId, parsedInput);
404
- if (harnessId === "codex" && hookEvent === "Stop" && configOutcome.kind === "invalid")
405
- configOutcome = await hookConfigOutcome(root, dependencies.loadConfig);
406
382
  if (configOutcome.kind === "invalid") {
407
- if (harnessId === "codex" && nativeStop?.event === "stop" && nativeStop.sessionId !== undefined) {
408
- writeHookOutput(io, harnessDescriptor(harnessId).runtime.formatOutput("Stop", {
409
- action: "continue", status: "UNKNOWN", code: "TASK_DEBUG_GATE_UNAVAILABLE",
410
- remedy: "DEBUG remains incomplete: configuration failed after one targeted reload. Preserve the task, evidence and budgets; fix .agent-ops/config.json and run agent-ops doctor. No completion is attested."
411
- }));
412
- await closeDebugSession(root, nativeStop.sessionId, harnessId).catch(() => undefined);
413
- return 0;
414
- }
415
383
  if (completionGateInstalled) {
416
384
  writeHookOutput(io, harnessDescriptor(harness).runtime.formatOutput("Stop", {
417
385
  action: "block",
@@ -434,15 +402,6 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
434
402
  return 0;
435
403
  }
436
404
  const config = configOutcome.config;
437
- if (harnessId === "codex" && hookEvent === "Stop" && nativeStop !== null && config.features.debug?.enabled === true) {
438
- const result = await debugStopGate(root, config).handle(nativeStop);
439
- if (result !== null) {
440
- writeHookOutput(io, harnessDescriptor(harnessId).runtime.formatOutput("Stop", result));
441
- if (result.action === "continue" && nativeStop.sessionId !== undefined)
442
- await closeDebugSession(root, nativeStop.sessionId, harnessId).catch(() => undefined);
443
- return 0;
444
- }
445
- }
446
405
  if (completionGateInstalled && !config.features.completionGate.enabled) {
447
406
  writeHookOutput(io, harnessDescriptor(harness).runtime.formatOutput("Stop", {
448
407
  action: "block",
@@ -457,8 +416,6 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
457
416
  : await dependencies.trust(root, config, cliVersion);
458
417
  const trusted = trustStatus === "TRUSTED";
459
418
  const normalized = normalizeHookInput(harnessId, parsedInput);
460
- if (normalized?.event === "session-start" && normalized.sessionId !== undefined)
461
- await startDebugSession(root, normalized.sessionId, harnessId).catch(() => undefined);
462
419
  if (dependencies.office !== undefined && config.features.office?.enabled === true) {
463
420
  if (normalized !== null) {
464
421
  const phase = officePhaseHint(normalized) ?? (harnessId === "codex" ? officeLoopPhaseHint(parsedInput, root) : undefined);
@@ -554,43 +511,22 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
554
511
  await recordSessionOrigin(dependencies.worktree, { cwd: root, sessionId }).catch(() => undefined);
555
512
  }
556
513
  }
557
- let hookResult;
558
- // Without the gate, DEBUG still refuses a Stop that has no valid stop
559
- // checkpoint. Its own failure stays advisory, so it never blocks other work.
560
- const debugStop = completionGate === undefined && hookEvent === "Stop" && normalized !== null &&
561
- (harnessId === "agy" || harnessId === "claude" || harnessId === "codex") && config.features.debug?.enabled === true
562
- ? await debugStopGate(root, config).handle(normalized)
563
- : null;
564
- if (debugStop !== null)
565
- hookResult = debugStop;
566
- const output = debugStop !== null
567
- ? harnessDescriptor(harnessId).runtime.formatOutput("Stop", debugStop)
568
- : await runHookCommand({
569
- harness: harness,
570
- event: hookEvent,
571
- stdin: rawInput,
572
- input: parsedInput,
573
- config,
574
- trusted,
575
- onResult: result => { hookResult = result; },
576
- ...(worktreeGuard === undefined ? {} : { worktreeGuard }),
577
- ...(worktreeEnter === undefined ? {} : { worktreeEnter }),
578
- ...(dependencies.advisory === undefined
579
- ? {}
580
- : { advisory: dependencies.advisory }),
581
- ...(stopVerification === undefined ? {} : { stopVerification }),
582
- ...(completionGate === undefined ? {} : { completionGate })
583
- });
514
+ const output = await runHookCommand({
515
+ harness: harness,
516
+ event: hookEvent,
517
+ stdin: rawInput,
518
+ input: parsedInput,
519
+ config,
520
+ trusted,
521
+ ...(worktreeGuard === undefined ? {} : { worktreeGuard }),
522
+ ...(worktreeEnter === undefined ? {} : { worktreeEnter }),
523
+ ...(dependencies.advisory === undefined
524
+ ? {}
525
+ : { advisory: dependencies.advisory }),
526
+ ...(stopVerification === undefined ? {} : { stopVerification }),
527
+ ...(completionGate === undefined ? {} : { completionGate })
528
+ });
584
529
  writeHookOutput(io, output);
585
- // A Stop can be advisory or gated, but the foreground clock only ends on
586
- // the same trusted idle model_stop that the native decision accepted.
587
- if (normalized?.event === "stop" &&
588
- normalized.sessionId !== undefined &&
589
- normalized.terminationReason === "model_stop" &&
590
- normalized.fullyIdle === true &&
591
- hookResult?.action === "continue") {
592
- await closeDebugSession(root, normalized.sessionId, harnessId).catch(() => undefined);
593
- }
594
530
  }
595
531
  catch {
596
532
  if (completionGateInstalled) {
@@ -43,25 +43,69 @@ const exec = promisify(execFile);
43
43
  const plain = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
44
44
  const active = (worker) => ["assigned", "starting", "running", "idle", "handing-off"].includes(worker.status);
45
45
  const cli = fileURLToPath(new URL("./bin.js", import.meta.url));
46
- /** Release the sole native slot only after confirmed coordinator death. */
46
+ const pendingChildren = (state) => state.tasks.filter(task => task.taskId !== state.rootTaskId && !["delivered", "complete", "blocked"].includes(task.status));
47
+ // Only a coordinator wait stops the coordinator with a handoff reason; it never delivers itself.
48
+ const yielded = (worker) => worker.status === "stopped" && worker.stopIntent?.reason === "handoff";
49
+ /**
50
+ * Stop a coordinator that asked to wait for its children, instead of letting it
51
+ * poll run status. Its native thread is dropped so the wake starts fresh.
52
+ */
53
+ export async function waitForChildren(repository, supervisor, runId, workerId, generation) {
54
+ const current = await repository.read(runId);
55
+ if (current === null || workerId !== current.coordinatorId)
56
+ throw new AgentOpsError("RUN_COORDINATOR_REQUIRED", "Only the coordinator can wait for child deliveries.");
57
+ if (current.rootTaskId === null || pendingChildren(current).length === 0)
58
+ throw new AgentOpsError("RUN_WAIT_NOTHING_PENDING", "Wait requires at least one planned child that has not finished.");
59
+ await yieldCoordinator(repository, supervisor, runId, workerId, generation, current.rootTaskId);
60
+ }
61
+ async function yieldCoordinator(repository, supervisor, runId, workerId, generation, taskId) {
62
+ await supervisor.stopWorker(runId, workerId, generation, "handoff");
63
+ await repository.mutate(runId, saved => ({ ...saved, workers: saved.workers.map(w => w.workerId === workerId && w.generation === generation && w.status === "stopped" ? { ...w, nativeSessionId: null, nativeJobId: null } : w) }));
64
+ await repository.appendEvent(runId, { type: "diagnostic", code: "RUN_COORDINATOR_YIELDED", workerId, taskId,
65
+ detail: "Coordinator waited; process death confirmed and its slot released until every child finishes." });
66
+ }
67
+ /** Finish an unconfirmed wait, then return the stopped coordinator once every child has finished. */
47
68
  export async function prepareSerialSchedule(repository, supervisor, runId) {
48
69
  const current = await repository.read(runId);
49
- if (current === null || current.jobs !== 1 || current.rootTaskId === null || current.status !== "active" || current.disableRestart || current.awaitingResume)
70
+ if (current === null || current.rootTaskId === null || current.status !== "active" || current.disableRestart || current.awaitingResume)
50
71
  return null;
51
- const children = current.tasks.filter(task => task.taskId !== current.rootTaskId);
52
72
  const coordinator = current.workers.find(worker => worker.workerId === current.coordinatorId);
53
- if (children.length === 0 || coordinator === undefined)
54
- return null;
55
- if (children.some(task => !["delivered", "complete", "blocked"].includes(task.status))) {
56
- if (active(coordinator)) {
57
- await supervisor.stopWorker(runId, coordinator.workerId, coordinator.generation, "handoff");
58
- await repository.appendEvent(runId, { type: "diagnostic", code: "RUN_COORDINATOR_YIELDED", workerId: coordinator.workerId,
59
- taskId: coordinator.taskId, detail: "Coordinator process death confirmed before assigning the sole slot to children." });
73
+ if (coordinator?.status === "handing-off" && coordinator.stopIntent?.reason === "handoff") {
74
+ try {
75
+ await yieldCoordinator(repository, supervisor, runId, coordinator.workerId, coordinator.generation, current.rootTaskId);
76
+ }
77
+ catch (cause) {
78
+ // The slot stays held until death is confirmed; retry on the next supervisor pass.
79
+ if (cause instanceof AgentOpsError && ["RUN_HANDOFF_PROCESS_ALIVE", "RUN_WRITER_STILL_ALIVE"].includes(cause.code))
80
+ return null;
81
+ throw cause;
60
82
  }
61
83
  return null;
62
84
  }
85
+ if (coordinator === undefined || current.tasks.every(task => task.taskId === current.rootTaskId) || pendingChildren(current).length > 0)
86
+ return null;
63
87
  return coordinator.status === "stopped" && !current.workers.some(worker => worker.workerId !== coordinator.workerId && active(worker)) ? coordinator : null;
64
88
  }
89
+ /** Recovery leaves a waiting coordinator stopped, and with jobs=1 starts only a child whose dependencies are delivered. */
90
+ export function recoverySkips(state, worker) {
91
+ if (state.rootTaskId === null)
92
+ return false;
93
+ const pending = pendingChildren(state);
94
+ if (pending.length === 0)
95
+ return false;
96
+ if (worker.workerId === state.coordinatorId)
97
+ return yielded(worker);
98
+ const node = pending.find(task => task.taskId === worker.taskId);
99
+ return state.jobs === 1 && (node === undefined ||
100
+ node.dependencies.some(id => !state.tasks.some(task => task.taskId === id && ["delivered", "complete"].includes(task.status))));
101
+ }
102
+ /** Child status handed to a woken coordinator, which may have lost its conversation. */
103
+ export function coordinatorWakeNote(state, coordinator) {
104
+ const children = state.tasks.filter(task => task.taskId !== state.rootTaskId)
105
+ .map(task => `${task.taskId} ${task.status}${task.sourceCommit === null ? "" : " " + task.sourceCommit}`);
106
+ return (coordinator.nativeSessionId === null ? "Fresh native session after wait; the earlier conversation is unavailable. " : "") +
107
+ `Children: ${children.join("; ")}. Review deliveries, then plan follow-up workers and wait again, or request finalization.`;
108
+ }
65
109
  /** Retain the structured child failure instead of hiding it behind recovery text. */
66
110
  export function runStepError(result, fallback) {
67
111
  const details = Array.isArray(result.errors) ? result.errors.filter(plain)
@@ -200,11 +244,11 @@ export async function superviseNativeRun(root, runId) {
200
244
  "Requests require requestId (unique alphanumeric or hyphen, max 64), workerId, generation and current contractHash.",
201
245
  "Plan fields: action=plan,title,intent,criteria,surface (the EVIDENCE-SURFACE-001 audit object, same shape as task create --surface-file). Worker fields: action=worker,title,intent,criteria,dependencies (known worker task IDs; omit all acceptance baselines until dependencies are delivered),covers (parent included surface IDs this worker delivers; every included surface needs one worker).",
202
246
  "Only the coordinator may change execution policy: submit action=policy, configFile (relative proposed JSON), expectedPolicyHash, approvals (each changed capabilityId command:<id>/runner:<id>/setup:<index>, classification=non-dangerous, reason assessing the entire fixed definition including setup/build). Commit source changes and wait until every writer is clean before requesting policy synchronization. Never use init/update/trust grant to create permanent execution authorization. Explicit native or organization denial must not be bypassed.",
203
- "Worker task baselines are frozen after dependency commits enter its checkout. Handoff fields: action=handoff,sourceFiles (explicit existing source paths when unchanged). Question fields: action=question,prompt. Coordinator finalize fields: action=finalize.",
247
+ "Worker task baselines are frozen after dependency commits enter its checkout. Handoff fields: action=handoff,sourceFiles (explicit existing source paths when unchanged). Question fields: action=question,prompt. Coordinator wait fields: action=wait, sent once every worker is planned instead of polling run status; the supervisor stops the coordinator and starts it again, in a fresh session with a child-status diagnostic, after every child is delivered, complete or blocked. Coordinator finalize fields: action=finalize.",
204
248
  "The supervisor, after stopping writers, runs final verification, two fresh reviews and finish. A native completed goal is never final proof."], diagnostic };
205
249
  const path = join(worker.worktree, ".agent-ops", "tasks", "run-instructions.json");
206
250
  await writePrivateFile(path, JSON.stringify(content, null, 2), worker.worktree);
207
- return "Read .agent-ops/tasks/run-instructions.json and carry its immutable original goal through the evidence loop. Submit a plan if rootTaskId is null; otherwise implement and repair the assigned task. A child requests handoff after local green; the coordinator waits for all child deliveries, then requests finalization. Never substitute native goal completion for agent-ops evidence.";
251
+ return "Read .agent-ops/tasks/run-instructions.json and carry its immutable original goal through the evidence loop. Submit a plan if rootTaskId is null; otherwise implement and repair the assigned task. A child requests handoff after local green; the coordinator submits action=wait after planning its children, then requests finalization once all have delivered. Never substitute native goal completion for agent-ops evidence.";
208
252
  }
209
253
  async function start(worker, diagnostic, resumeAt) {
210
254
  if (await discoverPendingPolicyTransition(repository, runId) !== null)
@@ -243,7 +287,7 @@ export async function superviseNativeRun(root, runId) {
243
287
  async function schedule() {
244
288
  const coordinator = await prepareSerialSchedule(repository, supervisor, runId);
245
289
  if (coordinator !== null) {
246
- await start(coordinator);
290
+ await start(coordinator, coordinatorWakeNote((await repository.read(runId)), coordinator));
247
291
  return;
248
292
  }
249
293
  const current = (await repository.read(runId));
@@ -682,18 +726,13 @@ export async function superviseNativeRun(root, runId) {
682
726
  await synchronizePolicy();
683
727
  state = (await repository.read(runId));
684
728
  for (const worker of state.workers.filter(w => active(w) || w.status === "stopped")) {
685
- if (state.jobs === 1 && state.rootTaskId !== null) {
686
- const pending = state.tasks.filter(task => task.taskId !== state.rootTaskId && !["delivered", "complete", "blocked"].includes(task.status));
687
- const node = pending.find(task => task.taskId === worker.taskId);
688
- if (pending.length > 0 && (worker.workerId === state.coordinatorId || node === undefined ||
689
- node.dependencies.some(id => !state.tasks.some(task => task.taskId === id && ["delivered", "complete"].includes(task.status)))))
690
- continue;
691
- }
729
+ if (recoverySkips(state, worker))
730
+ continue;
692
731
  if (worker.workerId !== state.coordinatorId && (worker.worktree === null ||
693
732
  !(await deps.tasks(worker.worktree).list()).some(r => r.task.id === worker.taskId)))
694
733
  continue;
695
734
  try {
696
- await start(worker, undefined, manualResumeAt);
735
+ await start(worker, worker.workerId === state.coordinatorId && yielded(worker) ? coordinatorWakeNote(state, worker) : undefined, manualResumeAt);
697
736
  if (state.jobs === 1)
698
737
  break;
699
738
  }
@@ -927,6 +966,12 @@ export async function superviseNativeRun(root, runId) {
927
966
  { questionId: request.requestId, prompt: request.prompt, askedAt: new Date().toISOString(), answeredAt: null, answerDigest: null }] }));
928
967
  response = { requestId: request.requestId, code: "RUN_AWAITING_INPUT" };
929
968
  }
969
+ else if (request.action === "wait") {
970
+ if ((await collectChangeSurface(gitRunner(worker.worktree))).paths.length > 0)
971
+ throw new AgentOpsError("RUN_WAIT_DIRTY", "Commit coordinator changes before waiting; a stopped checkout must be clean to restart.");
972
+ await waitForChildren(repository, supervisor, runId, worker.workerId, worker.generation);
973
+ response = { requestId: request.requestId, code: "RUN_COORDINATOR_WAITING" };
974
+ }
930
975
  else if (request.action === "finalize") {
931
976
  if (worker.workerId !== current.coordinatorId || current.tasks.some(task => task.taskId !== current.rootTaskId && task.status !== "delivered"))
932
977
  throw new AgentOpsError("RUN_DELIVERIES_PENDING", "The coordinator must wait for every planned child delivery before final integration.");
@@ -54,7 +54,7 @@ export function buildAgyHookSettings(capabilities, runtimePath, platform = proce
54
54
  throw new AgentOpsError("AGY_HOOK_PATH_INVALID", "agy hook runtime path is invalid.");
55
55
  }
56
56
  const hooks = {};
57
- if ((capabilities.includes("lifecycle-summary") || capabilities.includes("office-presence") || capabilities.includes("task-debug")) || capabilities.includes("project-loop")) {
57
+ if ((capabilities.includes("lifecycle-summary") || capabilities.includes("office-presence")) || capabilities.includes("project-loop")) {
58
58
  hooks.PreInvocation = [handler(runtimePath, "SessionStart", platform)];
59
59
  }
60
60
  if ((capabilities.includes("command-policy") || capabilities.includes("office-presence")) || capabilities.includes("project-loop")) {
@@ -62,8 +62,7 @@ export function buildAgyHookSettings(capabilities, runtimePath, platform = proce
62
62
  }
63
63
  if (capabilities.includes("office-presence") ||
64
64
  capabilities.includes("optional-stop-verify") ||
65
- capabilities.includes("completion-gate") ||
66
- capabilities.includes("task-debug")) {
65
+ capabilities.includes("completion-gate")) {
67
66
  hooks.Stop = [handler(runtimePath, "Stop", platform, capabilities.includes("completion-gate"))];
68
67
  }
69
68
  return { hooks };
@@ -115,7 +115,7 @@ export function buildClaudeHookSettings(capabilities, runtimePath, platform = pr
115
115
  }
116
116
  // The loop has its own prompt hook. Outside it, a prompt is both the
117
117
  // turn's start for Office and content for command policy.
118
- if (capabilities.includes("command-policy") || capabilities.includes("office-presence") || capabilities.includes("task-debug")) {
118
+ if (capabilities.includes("command-policy") || capabilities.includes("office-presence")) {
119
119
  hooks.UserPromptSubmit = [matcherGroup("UserPromptSubmit", runtimePath)];
120
120
  if (capabilities.includes("command-policy") || capabilities.includes("office-presence"))
121
121
  hooks.PreToolUse = [matcherGroup("PreToolUse", runtimePath)];
@@ -126,7 +126,7 @@ export function buildClaudeHookSettings(capabilities, runtimePath, platform = pr
126
126
  if (capabilities.includes("completion-gate")) {
127
127
  hooks.Stop = [matcherGroup("Stop", runtimePath, true)];
128
128
  }
129
- else if (capabilities.includes("task-debug") || capabilities.includes("optional-stop-verify") || capabilities.includes("office-presence")) {
129
+ else if (capabilities.includes("optional-stop-verify") || capabilities.includes("office-presence")) {
130
130
  hooks.Stop = [matcherGroup("Stop", runtimePath)];
131
131
  }
132
132
  // Stop only ends a turn. SessionEnd is the one signal that the conversation
@@ -84,18 +84,15 @@ export function buildCodexHookConfig(capabilities, runtimePath) {
84
84
  }
85
85
  // The loop has its own prompt hook. Outside it, a prompt is both the
86
86
  // turn's start for Office and content for command policy.
87
- if (capabilities.includes("command-policy") || capabilities.includes("office-presence") || capabilities.includes("task-debug")) {
87
+ if (capabilities.includes("command-policy") || capabilities.includes("office-presence")) {
88
88
  hooks.UserPromptSubmit = [matcherGroup("UserPromptSubmit", runtimePath)];
89
89
  if (capabilities.includes("command-policy") || capabilities.includes("office-presence"))
90
90
  hooks.PreToolUse = [matcherGroup("PreToolUse", runtimePath)];
91
91
  }
92
92
  }
93
- if (capabilities.includes("task-debug") || capabilities.includes("optional-stop-verify") || capabilities.includes("office-presence")) {
93
+ if (capabilities.includes("optional-stop-verify") || capabilities.includes("office-presence")) {
94
94
  hooks.Stop = [matcherGroup("Stop", runtimePath)];
95
95
  }
96
- if (capabilities.includes("task-debug")) {
97
- hooks.Interrupt = [{ hooks: [{ ...commandHook("Interrupt", runtimePath), timeout: 3 }] }];
98
- }
99
96
  if (capabilities.includes("office-presence") && hooks.PreToolUse !== undefined) {
100
97
  hooks.PreToolUse = hooks.PreToolUse.map(group => ({ ...group, matcher: "^(Bash|apply_patch)$" }));
101
98
  }
@@ -260,9 +260,6 @@ export class CompletionGateService {
260
260
  }
261
261
  }
262
262
  async grantPermit(sessionId) {
263
- const attached = await this.#options.taskService.status({ sessionId }).catch(() => null);
264
- if (attached?.task.debug !== undefined && attached.status === "active")
265
- throw new AgentOpsError("TASK_DEBUG_CHECKPOINT_REQUIRED", "Active debug issues require a runtime stop checkpoint; allow-stop does not grant budget or success.");
266
263
  const fingerprint = await this.#fingerprint();
267
264
  const granted = await this.#store.mutate(sessionId, (state) => {
268
265
  if (state === null) {
@@ -488,18 +485,6 @@ export class CompletionGateService {
488
485
  }
489
486
  return last;
490
487
  }
491
- const debugTask = await this.#options.taskService.status({ sessionId }).catch(() => null);
492
- if (debugTask?.task.debug !== undefined) {
493
- if (debugTask.status === "active") {
494
- await this.#options.taskService.debugService().prepareStop(debugTask.task.id, sessionId);
495
- const valid = await this.#options.taskService.debugService().validCheckpoint(debugTask.task.id, sessionId, fingerprint);
496
- return valid ? gateResult("continue", "UNKNOWN", "COMPLETION_GATE_TASK_DEBUG_STOPPED", "Original issue remains active with a runtime-bound non-success checkpoint.") :
497
- gateResult("block", "FAIL", "COMPLETION_GATE_TASK_DEBUG_ACTIVE", "Continue the investigation/evidence loop or record a current runtime stop checkpoint, even when Git has no net diff.");
498
- }
499
- const failure = await this.#validateTask(sessionId);
500
- if (failure !== null)
501
- return failure;
502
- }
503
488
  if (changed) {
504
489
  const failure = await this.#validateTask(sessionId);
505
490
  if (failure !== null)
@@ -342,7 +342,7 @@ function autoRunInstruction(descriptor, context) {
342
342
  "`RUN_REPO_UNTRUSTED` means stop and ask the user; never work",
343
343
  "around trust. New runs have no total time cap; legacy finite runs upgrade",
344
344
  "only on manual resume, preserving accounting and independent blockers.",
345
- "Command/review timeouts, provider limits and DEBUG budgets remain effective.",
345
+ "Command/review timeouts and provider limits remain effective.",
346
346
  "If the run ends without completing (blocked, stopped, failing review),",
347
347
  "report the `agent-ops run status",
348
348
  "<id>` state and code, the last `agent-ops run logs <id>` events, and the",
@@ -364,8 +364,8 @@ export function managedRules(descriptor, context) {
364
364
  : [`Toolkit version: ${context.toolkitVersion}`]),
365
365
  ""
366
366
  ];
367
- if (context.capabilities.includes("task-debug"))
368
- lines.push("Task DEBUG routing is enabled for investigation/bugfix issues. Native prompt hooks create the issue task when a session identity is available; otherwise use agent-ops task create --debug --goal <verbatim issue> with two to five issue-specific criteria. Explain/read-only, feature and refactor requests use the existing normal task flow.", "For DEBUG: understand the issue, investigate with task debug --debug-action investigate --command-id <trusted verifier>, record falsifiable hypotheses with --debug-action hypothesis --payload-file <json>, reproduce, fix, then once a hypothesis is confirmed declare the origin with --debug-action origin --payload-file <json> (escape-of <commit> with the earliest-gate label, or pre-existing/external with a reason; finish refuses ESCAPE_ORIGIN_MISSING without it; a surface label also needs a tracked .agent-ops-escapes/rules/<id>.json rule that replays at the escaped commit, and a criteria/verify label payload promote or --escape-promote <runnerId:checkId>, else ESCAPE_RULE_UNPROVEN), verify, re-record the confirmed hypothesis, commit the .agent-ops-escapes record with the fix, then obtain two fresh independent reviews and finish automatically. A normal task fixing an escaped bug may declare task create --escape-of <commit> --escape-label <label>. A clean HEAD is the automatic baseline; do not ask permission for it or offer diagnosis-only as a fallback. Failure to reproduce alone is not a stop condition: continue discriminating checks within the budget. Replace provisional intake criteria with relevant behavioral acceptance; baseline setup/skip/timeout/UNKNOWN is never reproduction. Never report success from edits or unrelated green checks.", "DEBUG defaults are 60 cumulative active minutes and 5 material repair candidates across the original issue, revisions, sessions and children. After two stagnant candidates document one substantive strategy switch; two further stagnant candidates block. Save task debug --debug-action checkpoint before stopping. Resume preserves budgets and blockers. A human continue event may authorize --debug-action extend --event-id latest; unsupported native event sources fail closed. Never use allow-stop for DEBUG.", "DEBUG human control comes only from native human events: '只診斷' / 'diagnosis only' keeps investigating without product repairs until current sealed baseline assertion FAIL confirms a hypothesis or runtime limits/blockers permit non-success Stop. '暫停' / 'pause' saves progress and stops immediately. '繼續' / 'continue' resumes the existing scope; '繼續修復' / 'resume repair' restores the full loop. Codex Interrupt is a pause. Preserve the original task, evidence, consumed time and rounds; diagnosis or pause never marks repair complete. CLI actor/text fields and synthetic instructions cannot authorize these changes.", "DEBUG is foreground work. On Claude, agy and native Codex, Stop refuses an active DEBUG issue without a valid stop checkpoint, with or without the completion gate. Codex uses decision:block to continue the original task even on repeated Stop; stop_hook_active does not waive it. Codex's generic completion gate remains unsupported; opencode has no supported foreground lifecycle. DEBUG gate faults get one targeted task/config reload, then an explicit UNKNOWN/incomplete diagnostic and non-success Stop, preserving state. Without reliable native session identity, automatic hook intake is unsupported; the existing native Agent can use the task CLI. Only a matching owned debug worktree checkpoint permits dirty recovery; preserve all original user edits.", "");
367
+ if (context.capabilities.includes("task"))
368
+ lines.push("To fix a bug, run `agent-ops task create --debug --goal <verbatim issue>` with a", "behavioral criterion whose `redCheckIds` fail at the baseline and pass on the", "candidate, then verify, review and finish as for any task. If you know where the", "bug came from, record it with `task revise --escape-of <commit> --escape-label", "<label>`; the origin is advisory and never blocks finish.", "");
369
369
  if (context.capabilities.includes("rules")) {
370
370
  lines.push("For every change:", "", "1. Audit the change's surfaces first (EVIDENCE-SURFACE-001): dependents,", " duplicates, other paths to the same concept, and shared state/data", " contracts, each with recorded fixed-string queries or n/a: reason; pass", " them with `task create --surface-file <json>`. An exclusion says why", " the surface is unaffected; \"out of scope\" is not a reason. A derived", " discovery query whose hits are all incidental may be dismissed once with", " a reason in `dismissed` instead of excluding each file. Then define", " two to five mechanically verifiable acceptance criteria.", "2. Inspect the smallest relevant scope within the audited surfaces and", " preserve unrelated changes.", "3. Apply the smallest safe change.", "4. Run evidence-producing verification for every criterion.", "5. Obtain complete independent review before claiming completion, via", " `agent-ops review --task <task-id> --yes` (or the CLI's equivalent invocation).", " Never call a", " review-target CLI (agy, codex, claude) directly — direct calls skip", " the enforced read-only sandbox flags and can hang or fail on command", " permission prompts.", " Start this command through the trusted outer host runner as its first", " and only invocation. On Codex, the outer tool request must use", " `sandbox_permissions: \"require_escalated\"` so network and loopback", " are available; in that same request, unset", " `CODEX_SANDBOX_NETWORK_DISABLED` and set `AGENT_OPS_HOST` when three", " targets are configured. The Codex shell form is", " `env -u CODEX_SANDBOX_NETWORK_DISABLED AGENT_OPS_HOST=<current-host> agent-ops review --task <task-id> --yes`.", " Keep it a plain command line: to keep the output add `--output <file>`", " (the JSON envelope is written there and one line is printed) instead of", " a shell redirect, which the host can only allow by matching the whole", " command string, task id included.", " Do not run it once in the restricted sandbox and retry; changing the", " variable inside that sandbox is not elevation.", " Review runs exactly two fresh sessions: with three configured targets,", " set `AGENT_OPS_HOST` to the current CLI id so it is excluded; with two", " targets both run in configured order; with one target it runs twice.", " The first session is necessary; a PASS then requires an adversarial", " re-check carrying the first report. A first FAIL or NOT_RUN stops.", "", "Treat `.agent-ops/config.json` as verifier authority. Discovery output is", "only a proposal until a user confirms it. Repository commands require an", "exact matching trust record. Confirmed project init/update grants it", "automatically when verification commands are configured.", "");
371
371
  }
@@ -1,5 +1,4 @@
1
1
  import { lstat, readFile } from "node:fs/promises";
2
- import { DEBUG_DEFAULTS } from "../task/debug-state.js";
3
2
  import { CONFIG_SCHEMA_VERSION, MANIFEST_SCHEMA_VERSION } from "../contracts.js";
4
3
  import { discoverProject } from "../discovery/index.js";
5
4
  import { sha256 } from "../fs/hash.js";
@@ -90,6 +89,8 @@ function buildConfig(profiles, existing, reviewTargets = [], detectedCommands =
90
89
  existing.verification.commands.length > 0
91
90
  ? existing.verification
92
91
  : { commands: [...detectedCommands] };
92
+ // Pre-0.11 DEBUG budget settings are dropped on rewrite.
93
+ const existingFeatures = existing === undefined ? undefined : (({ debug: _legacyDebug, ...rest }) => rest)(existing.features);
93
94
  const effectiveWorktree = worktree !== undefined
94
95
  ? (worktree === null ? undefined : structuredClone(worktree))
95
96
  : (existing?.worktree === undefined ? undefined : structuredClone(existing.worktree));
@@ -98,8 +99,7 @@ function buildConfig(profiles, existing, reviewTargets = [], detectedCommands =
98
99
  profiles: [...profiles],
99
100
  verification,
100
101
  features: {
101
- debug: existing?.features.debug ?? { ...DEBUG_DEFAULTS },
102
- ...(existing?.features ?? {
102
+ ...(existingFeatures ?? {
103
103
  stopVerification: { enabled: false },
104
104
  completionGate: { enabled: completionGateEnabled }
105
105
  }),
@@ -34,8 +34,6 @@ export function resolveProfiles(inputProfiles) {
34
34
  }
35
35
  export function resolveCapabilities(config) {
36
36
  const resolved = resolveProfiles(config.profiles);
37
- if (config.features.debug?.enabled === true)
38
- resolved.capabilities.push("task-debug");
39
37
  if (config.features.office?.enabled === true)
40
38
  resolved.capabilities.push("office-presence");
41
39
  if (config.features.stopVerification.enabled) {
@@ -1,6 +1,9 @@
1
1
  import { activeWallTimeMs } from "../run/scheduler.js";
2
2
  const COMPLETED_RETENTION_MS = 2 * 60 * 60 * 1000;
3
3
  const LIVE = new Set(["assigned", "starting", "running", "idle", "handing-off", "fenced", "delivered", "blocked"]);
4
+ // Every other run status is idle: nothing moves a paused, stopping, blocked or
5
+ // budget-limited run on, so it leaves the Office like a finished one.
6
+ const RUNNING = new Set(["active", "awaiting-input"]);
4
7
  const PHASES = new Set(["planning", "implementing", "verifying", "reviewing", "integrating", "unknown"]);
5
8
  function phaseOf(value) {
6
9
  return value !== undefined && PHASES.has(value) ? value : "unknown";
@@ -23,7 +26,7 @@ function quote(text) {
23
26
  export function buildOfficeSnapshot(input) {
24
27
  const byPath = new Map(input.worktrees.map(w => [w.path, w]));
25
28
  const claimed = new Set();
26
- const runs = input.runs.filter(run => !["complete", "failed"].includes(run.status) || input.now - Date.parse(run.updatedAt) < COMPLETED_RETENTION_MS)
29
+ const runs = input.runs.filter(run => RUNNING.has(run.status) || input.now - Date.parse(run.updatedAt) < COMPLETED_RETENTION_MS)
27
30
  .map((run) => {
28
31
  const roots = new Set(run.workers.map(w => w.worktree).filter((p) => p !== null));
29
32
  const reviewers = input.reviews.filter(r => r.root !== null && roots.has(r.root)).slice(0, 2);
@@ -192,11 +192,8 @@ export async function resumeWorktree(deps, options) {
192
192
  const sessionId = assertSessionId(options.sessionId);
193
193
  const mainRoot = await mainCheckout(deps, options.cwd);
194
194
  const previous = await requireRecord(mainRoot, name);
195
- // A base gives the service repository context, which debug resume needs.
196
195
  const tasks = deps.tasks(previous.path, previous.base);
197
196
  const attached = await tasks.status({ sessionId: previous.sessionId }).catch(() => null);
198
- if (attached?.task.debug !== undefined)
199
- await tasks.debugService().resume(attached.task.id, sessionId);
200
197
  if (attached !== null)
201
198
  await tasks.attach(sessionId, attached.task.id);
202
199
  const record = { ...previous, sessionId };