@kylecheng3146/agent-ops 0.10.2 → 0.11.1

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 (56) hide show
  1. package/README.md +13 -0
  2. package/dist/packages/cli/src/args.js +18 -44
  3. package/dist/packages/cli/src/bin.js +37 -29
  4. package/dist/packages/cli/src/cli.js +16 -14
  5. package/dist/packages/cli/src/codex-loop-process.js +0 -6
  6. package/dist/packages/cli/src/commands/review.js +5 -10
  7. package/dist/packages/cli/src/commands/run.js +3 -1
  8. package/dist/packages/cli/src/commands/task.js +27 -54
  9. package/dist/packages/cli/src/commands/verify.js +7 -3
  10. package/dist/packages/cli/src/criteria-review.js +63 -0
  11. package/dist/packages/cli/src/default-base.js +26 -0
  12. package/dist/packages/cli/src/hook-process.js +19 -83
  13. package/dist/packages/cli/src/owned-run-step.js +1 -1
  14. package/dist/packages/cli/src/run-entry.js +203 -115
  15. package/dist/runtime/src/adapters/agy/config.js +2 -3
  16. package/dist/runtime/src/adapters/claude/config.js +2 -2
  17. package/dist/runtime/src/adapters/codex/config.js +2 -5
  18. package/dist/runtime/src/hooks/completion-gate.js +0 -15
  19. package/dist/runtime/src/install/harness.js +4 -4
  20. package/dist/runtime/src/install/plan.js +3 -3
  21. package/dist/runtime/src/install/profiles.js +0 -2
  22. package/dist/runtime/src/logging/local-log.js +12 -0
  23. package/dist/runtime/src/office/snapshot.js +4 -1
  24. package/dist/runtime/src/parallel/manage.js +0 -3
  25. package/dist/runtime/src/review/criteria.js +252 -0
  26. package/dist/runtime/src/review/execute.js +68 -4
  27. package/dist/runtime/src/review/invocation.js +3 -3
  28. package/dist/runtime/src/run/verification.js +2 -2
  29. package/dist/runtime/src/schema/validate.js +10 -5
  30. package/dist/runtime/src/task/completion.js +4 -42
  31. package/dist/runtime/src/task/contract.js +11 -0
  32. package/dist/runtime/src/task/service.js +34 -57
  33. package/dist/runtime/src/task/store.js +3 -7
  34. package/dist/runtime/src/verify/acceptance-coverage.js +2 -3
  35. package/dist/runtime/src/verify/acceptance-protocol.js +4 -3
  36. package/dist/runtime/src/verify/acceptance-replay.js +4 -7
  37. package/dist/runtime/src/verify/service.js +6 -23
  38. package/dist/runtime/src/verify/source-fingerprint.js +17 -4
  39. package/dist/runtime/src/verify/spawn.js +3 -2
  40. package/docs/en/guides/task-debug.md +39 -133
  41. package/docs/en/spec/acceptance-and-evidence.md +31 -23
  42. package/docs/en/spec/harness-adapters.md +9 -11
  43. package/docs/en/spec/loop-engineering.md +11 -5
  44. package/docs/en/spec/review.md +16 -0
  45. package/docs/zh-TW/guides/task-debug.md +27 -73
  46. package/docs/zh-TW/spec/acceptance-and-evidence.md +18 -18
  47. package/docs/zh-TW/spec/harness-adapters.md +7 -8
  48. package/docs/zh-TW/spec/loop-engineering.md +9 -4
  49. package/docs/zh-TW/spec/review.md +17 -1
  50. package/package.json +1 -1
  51. package/schemas/config.schema.json +1 -21
  52. package/dist/packages/cli/src/debug-intake.js +0 -245
  53. package/dist/runtime/src/task/debug-intake.js +0 -15
  54. package/dist/runtime/src/task/debug-state.js +0 -135
  55. package/dist/runtime/src/task/debug-user-event.js +0 -160
  56. package/dist/runtime/src/task/debug.js +0 -632
@@ -1,3 +1,5 @@
1
+ import { criteriaReviewStatus } from "./criteria-review.js";
2
+ import { renderCriteriaReview } from "../../../runtime/src/review/criteria.js";
1
3
  import { stat } from "node:fs/promises";
2
4
  import { recoverRunIntegrationAfterCleanup, validateRunIntegrationReceipt } from "../../../runtime/src/run/integration.js";
3
5
  import { pendingFindingPins } from "../../../runtime/src/run/ratchet.js";
@@ -43,25 +45,69 @@ const exec = promisify(execFile);
43
45
  const plain = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
44
46
  const active = (worker) => ["assigned", "starting", "running", "idle", "handing-off"].includes(worker.status);
45
47
  const cli = fileURLToPath(new URL("./bin.js", import.meta.url));
46
- /** Release the sole native slot only after confirmed coordinator death. */
48
+ const pendingChildren = (state) => state.tasks.filter(task => task.taskId !== state.rootTaskId && !["delivered", "complete", "blocked"].includes(task.status));
49
+ // Only a coordinator wait stops the coordinator with a handoff reason; it never delivers itself.
50
+ const yielded = (worker) => worker.status === "stopped" && worker.stopIntent?.reason === "handoff";
51
+ /**
52
+ * Stop a coordinator that asked to wait for its children, instead of letting it
53
+ * poll run status. Its native thread is dropped so the wake starts fresh.
54
+ */
55
+ export async function waitForChildren(repository, supervisor, runId, workerId, generation) {
56
+ const current = await repository.read(runId);
57
+ if (current === null || workerId !== current.coordinatorId)
58
+ throw new AgentOpsError("RUN_COORDINATOR_REQUIRED", "Only the coordinator can wait for child deliveries.");
59
+ if (current.rootTaskId === null || pendingChildren(current).length === 0)
60
+ throw new AgentOpsError("RUN_WAIT_NOTHING_PENDING", "Wait requires at least one planned child that has not finished.");
61
+ await yieldCoordinator(repository, supervisor, runId, workerId, generation, current.rootTaskId);
62
+ }
63
+ async function yieldCoordinator(repository, supervisor, runId, workerId, generation, taskId) {
64
+ await supervisor.stopWorker(runId, workerId, generation, "handoff");
65
+ 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) }));
66
+ await repository.appendEvent(runId, { type: "diagnostic", code: "RUN_COORDINATOR_YIELDED", workerId, taskId,
67
+ detail: "Coordinator waited; process death confirmed and its slot released until every child finishes." });
68
+ }
69
+ /** Finish an unconfirmed wait, then return the stopped coordinator once every child has finished. */
47
70
  export async function prepareSerialSchedule(repository, supervisor, runId) {
48
71
  const current = await repository.read(runId);
49
- if (current === null || current.jobs !== 1 || current.rootTaskId === null || current.status !== "active" || current.disableRestart || current.awaitingResume)
72
+ if (current === null || current.rootTaskId === null || current.status !== "active" || current.disableRestart || current.awaitingResume)
50
73
  return null;
51
- const children = current.tasks.filter(task => task.taskId !== current.rootTaskId);
52
74
  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." });
75
+ if (coordinator?.status === "handing-off" && coordinator.stopIntent?.reason === "handoff") {
76
+ try {
77
+ await yieldCoordinator(repository, supervisor, runId, coordinator.workerId, coordinator.generation, current.rootTaskId);
78
+ }
79
+ catch (cause) {
80
+ // The slot stays held until death is confirmed; retry on the next supervisor pass.
81
+ if (cause instanceof AgentOpsError && ["RUN_HANDOFF_PROCESS_ALIVE", "RUN_WRITER_STILL_ALIVE"].includes(cause.code))
82
+ return null;
83
+ throw cause;
60
84
  }
61
85
  return null;
62
86
  }
87
+ if (coordinator === undefined || current.tasks.every(task => task.taskId === current.rootTaskId) || pendingChildren(current).length > 0)
88
+ return null;
63
89
  return coordinator.status === "stopped" && !current.workers.some(worker => worker.workerId !== coordinator.workerId && active(worker)) ? coordinator : null;
64
90
  }
91
+ /** Recovery leaves a waiting coordinator stopped, and with jobs=1 starts only a child whose dependencies are delivered. */
92
+ export function recoverySkips(state, worker) {
93
+ if (state.rootTaskId === null)
94
+ return false;
95
+ const pending = pendingChildren(state);
96
+ if (pending.length === 0)
97
+ return false;
98
+ if (worker.workerId === state.coordinatorId)
99
+ return yielded(worker);
100
+ const node = pending.find(task => task.taskId === worker.taskId);
101
+ return state.jobs === 1 && (node === undefined ||
102
+ node.dependencies.some(id => !state.tasks.some(task => task.taskId === id && ["delivered", "complete"].includes(task.status))));
103
+ }
104
+ /** Child status handed to a woken coordinator, which may have lost its conversation. */
105
+ export function coordinatorWakeNote(state, coordinator) {
106
+ const children = state.tasks.filter(task => task.taskId !== state.rootTaskId)
107
+ .map(task => `${task.taskId} ${task.status}${task.sourceCommit === null ? "" : " " + task.sourceCommit}`);
108
+ return (coordinator.nativeSessionId === null ? "Fresh native session after wait; the earlier conversation is unavailable. " : "") +
109
+ `Children: ${children.join("; ")}. Review deliveries, then plan follow-up workers and wait again, or request finalization.`;
110
+ }
65
111
  /** Retain the structured child failure instead of hiding it behind recovery text. */
66
112
  export function runStepError(result, fallback) {
67
113
  const details = Array.isArray(result.errors) ? result.errors.filter(plain)
@@ -187,11 +233,14 @@ export async function superviseNativeRun(root, runId) {
187
233
  throw new AgentOpsError("RUN_ANSWER_CHANGED", "The saved answer no longer matches its recorded digest.");
188
234
  answers.push({ questionId: question.questionId, prompt: question.prompt, answer: answer.answer });
189
235
  }
190
- const content = { runId, workerId: worker.workerId, generation: worker.generation, contractHash: current.currentContractHash,
236
+ const localTask = worker.taskId === null ? undefined : await new TaskService(new FileTaskStore(join(worker.worktree, ".agent-ops/tasks/state.json"), worker.worktree)).status({ taskId: worker.taskId }).catch(() => undefined);
237
+ const criteriaAdvice = localTask === undefined ? null : await criteriaReviewStatus(worker.worktree, localTask.task);
238
+ const content = { criteriaAdvisory: criteriaAdvice === null ? null : renderCriteriaReview(criteriaAdvice), runId, workerId: worker.workerId, generation: worker.generation, contractHash: current.currentContractHash,
191
239
  originalGoal: current.goal, answers, rootTaskId: current.rootTaskId, taskId: worker.taskId, cli, ownerSessionId: worker.ownerSessionId,
192
240
  policyConfigHash: current.policyBinding?.configHash,
193
241
  instructions: ["Keep the original goal fixed. Read this repository's AGENTS.md and use its existing verifier authority.",
194
242
  "Plan 2–5 substantive acceptance criteria. Submit a plan request before changing product files; the supervisor creates the task with the fixed goal.",
243
+ "Read the criteria advisory before implementation. Decide which suggestions to adopt and use task revise with reasons (including rejected advice). A revision does not automatically rerun criteria review; formal final proof remains required.",
195
244
  "For every mechanically testable requirement use a configured acceptance runner; explicit review-only fallback requires a recorded reason and fresh goal review.",
196
245
  "Implement, commit and verify. Repair failed checks and pin mechanically testable review findings into regression criteria. Never disable a mandatory policy verifier.",
197
246
  "Only registered run workers may write. Use isolated worktrees and supervisor-issued leases; do not launch an unregistered writing subagent.",
@@ -200,11 +249,11 @@ export async function superviseNativeRun(root, runId) {
200
249
  "Requests require requestId (unique alphanumeric or hyphen, max 64), workerId, generation and current contractHash.",
201
250
  "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
251
  "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.",
252
+ "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
253
  "The supervisor, after stopping writers, runs final verification, two fresh reviews and finish. A native completed goal is never final proof."], diagnostic };
205
254
  const path = join(worker.worktree, ".agent-ops", "tasks", "run-instructions.json");
206
255
  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.";
256
+ 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
257
  }
209
258
  async function start(worker, diagnostic, resumeAt) {
210
259
  if (await discoverPendingPolicyTransition(repository, runId) !== null)
@@ -243,7 +292,7 @@ export async function superviseNativeRun(root, runId) {
243
292
  async function schedule() {
244
293
  const coordinator = await prepareSerialSchedule(repository, supervisor, runId);
245
294
  if (coordinator !== null) {
246
- await start(coordinator);
295
+ await start(coordinator, coordinatorWakeNote((await repository.read(runId)), coordinator));
247
296
  return;
248
297
  }
249
298
  const current = (await repository.read(runId));
@@ -297,7 +346,8 @@ export async function superviseNativeRun(root, runId) {
297
346
  });
298
347
  await service.create({ title: plan.title, intent: plan.intent, criteria: plan.criteria, goal: current.goal,
299
348
  sessionId: ownerSessionId, policyConfigHash: calculateConfigHash(config), ...(plan.covers === undefined ? {} : { surface: { covers: plan.covers } }) });
300
- await start((await repository.read(runId)).workers.find(w => w.workerId === registration.workerId));
349
+ const advisory = await criteriaStep(workerRoot, node.taskId);
350
+ await start((await repository.read(runId)).workers.find(w => w.workerId === registration.workerId), JSON.stringify(advisory));
301
351
  }
302
352
  catch (cause) {
303
353
  const latest = (await repository.read(runId));
@@ -341,97 +391,10 @@ export async function superviseNativeRun(root, runId) {
341
391
  return result.data.receipt;
342
392
  }
343
393
  async function step(cwd, args, saved) {
344
- const remaining = remainingRunTimeMs(saved);
345
- if (remaining <= 0)
346
- throw new AgentOpsError("RUN_BUDGET_EXHAUSTED", "No active time remains for final proof.");
347
- return await new Promise((resolve, reject) => {
348
- const env = { ...process.env, AGENT_OPS_HOST: saved.host,
349
- AGENT_OPS_SESSION_ID: saved.workers.find(worker => worker.worktree === cwd)?.ownerSessionId ?? saved.ownerSessionId,
350
- AGENT_OPS_RUN_ID: saved.runId,
351
- AGENT_OPS_WORKER_ID: saved.workers.find(worker => worker.worktree === cwd)?.workerId ?? saved.coordinatorId,
352
- AGENT_OPS_WORKER_GENERATION: String(saved.workers.find(worker => worker.worktree === cwd)?.generation ?? saved.workers.find(worker => worker.workerId === saved.coordinatorId)?.generation) };
353
- delete env.CODEX_THREAD_ID;
354
- delete env.AGENT_OPS_AGENT_ID;
355
- delete env.CODEX_SANDBOX_NETWORK_DISABLED;
356
- const child = spawn(process.execPath, [fileURLToPath(new URL("./run-step-entry.js", import.meta.url)), cli, ...args, "--json"], { cwd, env, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
357
- let output = "";
358
- let closed = false;
359
- let escalation;
360
- const terminate = () => {
361
- if (child.pid === undefined || escalation !== undefined)
362
- return;
363
- try {
364
- process.kill(process.platform === "win32" ? child.pid : -child.pid, "SIGTERM");
365
- }
366
- catch { }
367
- escalation = setTimeout(() => {
368
- if (!closed)
369
- try {
370
- process.kill(process.platform === "win32" ? child.pid : -child.pid, "SIGKILL");
371
- }
372
- catch { }
373
- }, 3000);
374
- };
375
- const timeout = Number.isFinite(remaining) ? setTimeout(terminate, remaining) : undefined;
376
- const registration = (async () => {
377
- const identity = child.pid === undefined ? null : await nativeProcessIdentity(child.pid);
378
- if (identity === null || closed) {
379
- terminate();
380
- return;
381
- }
382
- await repository.mutate(runId, current => {
383
- if (current.status !== "active" || current.disableRestart) {
384
- terminate();
385
- throw new AgentOpsError("RUN_PROOF_STOPPED", "Stop raced final proof registration.");
386
- }
387
- return { ...current, proofProcess: { processId: child.pid, processIdentity: identity } };
388
- });
389
- child.stdin.end("start\n");
390
- })();
391
- child.stdin.on("error", terminate);
392
- void registration.catch(() => terminate());
393
- let checking = false;
394
- const control = setInterval(() => {
395
- if (checking)
396
- return;
397
- checking = true;
398
- void repository.read(runId).then(current => {
399
- if (current === null || current.disableRestart || current.status !== "active")
400
- terminate();
401
- }).catch(terminate).finally(() => { checking = false; });
402
- }, 250);
403
- child.stdout.on("data", chunk => { output += chunk.toString(); if (Buffer.byteLength(output) > 4 * 1024 * 1024)
404
- terminate(); });
405
- child.stderr.resume();
406
- child.once("error", cause => { closed = true; clearTimeout(timeout); clearInterval(control); reject(cause); });
407
- child.once("close", async () => {
408
- closed = true;
409
- clearTimeout(timeout);
410
- clearTimeout(escalation);
411
- clearInterval(control);
412
- try {
413
- await registration;
414
- await repository.mutate(runId, current => ({ ...current,
415
- ...(current.proofProcess?.processId === child.pid ? { proofProcess: null } : {}) }));
416
- const result = JSON.parse(output);
417
- if (!plain(result) || !["ok", "error"].includes(String(result.status)) || typeof result.code !== "string")
418
- throw new Error();
419
- const coordinatorRoot = saved.workers[0]?.worktree;
420
- if (result.status === "error" && coordinatorRoot != null)
421
- await recordSavedReviewRunUsage(repository, runId, coordinatorRoot);
422
- if (plain(result.data) && plain(result.data.result) && typeof result.data.result.sourceFingerprint === "string" &&
423
- typeof result.data.result.taskId === "string") {
424
- const artifact = await readReviewReportArtifact(cwd, result.data.result.sourceFingerprint, result.data.result.taskId);
425
- if (artifact !== null)
426
- await recordReviewRunUsage(repository, runId, artifact);
427
- }
428
- resolve(result);
429
- }
430
- catch {
431
- reject(new AgentOpsError("RUN_FINAL_PROOF_INTERRUPTED", "Final gate did not return a complete result; inspect target and receipt before retrying."));
432
- }
433
- });
434
- });
394
+ return await runControlledStep(repository, cli, cwd, args, saved);
395
+ }
396
+ async function criteriaStep(cwd, taskId) {
397
+ return await runCriteriaStep(repository, runId, cwd, taskId, step);
435
398
  }
436
399
  async function stopAll(reason, closeBudget = true) {
437
400
  const current = (await repository.read(runId));
@@ -682,18 +645,13 @@ export async function superviseNativeRun(root, runId) {
682
645
  await synchronizePolicy();
683
646
  state = (await repository.read(runId));
684
647
  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
- }
648
+ if (recoverySkips(state, worker))
649
+ continue;
692
650
  if (worker.workerId !== state.coordinatorId && (worker.worktree === null ||
693
651
  !(await deps.tasks(worker.worktree).list()).some(r => r.task.id === worker.taskId)))
694
652
  continue;
695
653
  try {
696
- await start(worker, undefined, manualResumeAt);
654
+ await start(worker, worker.workerId === state.coordinatorId && yielded(worker) ? coordinatorWakeNote(state, worker) : undefined, manualResumeAt);
697
655
  if (state.jobs === 1)
698
656
  break;
699
657
  }
@@ -855,7 +813,8 @@ export async function superviseNativeRun(root, runId) {
855
813
  await repository.mutate(runId, saved => ({ ...saved, rootTaskId: task.task.id, currentContractHash: treeContractHash([task.task]),
856
814
  tasks: saved.tasks.map(node => node.taskId === worker.taskId ? { ...node, taskId: task.task.id } : node),
857
815
  workers: saved.workers.map(w => w.workerId === worker.workerId ? { ...w, taskId: task.task.id } : w) }));
858
- response = { requestId: request.requestId, code: "RUN_PLAN_CREATED", task: task.task, contractHash: treeContractHash([task.task]) };
816
+ const advisory = await criteriaStep(worker.worktree, task.task.id);
817
+ response = { advisory, requestId: request.requestId, code: "RUN_PLAN_CREATED", task: task.task, contractHash: treeContractHash([task.task]) };
859
818
  await recordRunPhase(repository, runId, worker.workerId, "implementing", { taskId: task.task.id, update: { passed: 0, total: task.task.criteria.length } });
860
819
  await instructions((await repository.read(runId)).workers.find(w => w.workerId === worker.workerId));
861
820
  }
@@ -927,6 +886,12 @@ export async function superviseNativeRun(root, runId) {
927
886
  { questionId: request.requestId, prompt: request.prompt, askedAt: new Date().toISOString(), answeredAt: null, answerDigest: null }] }));
928
887
  response = { requestId: request.requestId, code: "RUN_AWAITING_INPUT" };
929
888
  }
889
+ else if (request.action === "wait") {
890
+ if ((await collectChangeSurface(gitRunner(worker.worktree))).paths.length > 0)
891
+ throw new AgentOpsError("RUN_WAIT_DIRTY", "Commit coordinator changes before waiting; a stopped checkout must be clean to restart.");
892
+ await waitForChildren(repository, supervisor, runId, worker.workerId, worker.generation);
893
+ response = { requestId: request.requestId, code: "RUN_COORDINATOR_WAITING" };
894
+ }
930
895
  else if (request.action === "finalize") {
931
896
  if (worker.workerId !== current.coordinatorId || current.tasks.some(task => task.taskId !== current.rootTaskId && task.status !== "delivered"))
932
897
  throw new AgentOpsError("RUN_DELIVERIES_PENDING", "The coordinator must wait for every planned child delivery before final integration.");
@@ -1011,3 +976,126 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
1011
976
  throw new AgentOpsError("RUN_SUPERVISOR_ARGUMENTS", "Supervisor requires canonical repository root and run ID.");
1012
977
  await superviseNativeRun(process.argv[2], process.argv[3]);
1013
978
  }
979
+ export async function runControlledStep(repository, cli, cwd, args, saved) {
980
+ const runId = saved.runId;
981
+ const criteria = args[0] === "review" && args.includes("--criteria");
982
+ const remaining = criteria ? Infinity : remainingRunTimeMs(saved);
983
+ if (remaining <= 0)
984
+ throw new AgentOpsError("RUN_BUDGET_EXHAUSTED", "No active time remains for final proof.");
985
+ return await new Promise((resolve, reject) => {
986
+ const env = { ...process.env, AGENT_OPS_HOST: saved.host,
987
+ AGENT_OPS_SESSION_ID: saved.workers.find(worker => worker.worktree === cwd)?.ownerSessionId ?? saved.ownerSessionId,
988
+ AGENT_OPS_RUN_ID: saved.runId,
989
+ AGENT_OPS_WORKER_ID: saved.workers.find(worker => worker.worktree === cwd)?.workerId ?? saved.coordinatorId,
990
+ AGENT_OPS_WORKER_GENERATION: String(saved.workers.find(worker => worker.worktree === cwd)?.generation ?? saved.workers.find(worker => worker.workerId === saved.coordinatorId)?.generation) };
991
+ delete env.CODEX_THREAD_ID;
992
+ delete env.AGENT_OPS_AGENT_ID;
993
+ delete env.CODEX_SANDBOX_NETWORK_DISABLED;
994
+ const child = spawn(process.execPath, [fileURLToPath(new URL("./run-step-entry.js", import.meta.url)), cli, ...args, "--json"], { cwd, env, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
995
+ let output = "";
996
+ let closed = false;
997
+ let escalation;
998
+ const terminate = () => {
999
+ if (child.pid === undefined || escalation !== undefined)
1000
+ return;
1001
+ try {
1002
+ process.kill(process.platform === "win32" ? child.pid : -child.pid, "SIGTERM");
1003
+ }
1004
+ catch { }
1005
+ escalation = setTimeout(() => {
1006
+ if (!closed)
1007
+ try {
1008
+ process.kill(process.platform === "win32" ? child.pid : -child.pid, "SIGKILL");
1009
+ }
1010
+ catch { }
1011
+ }, 3000);
1012
+ };
1013
+ const timeout = Number.isFinite(remaining) ? setTimeout(terminate, remaining) : undefined;
1014
+ const registration = (async () => {
1015
+ const identity = child.pid === undefined ? null : await nativeProcessIdentity(child.pid);
1016
+ if (identity === null || closed) {
1017
+ terminate();
1018
+ return;
1019
+ }
1020
+ await repository.mutate(runId, current => {
1021
+ if (current.status !== "active" || current.disableRestart) {
1022
+ terminate();
1023
+ throw new AgentOpsError("RUN_PROOF_STOPPED", "Stop raced final proof registration.");
1024
+ }
1025
+ if (criteria && current.proofProcess != null) {
1026
+ terminate();
1027
+ throw new AgentOpsError("CRITERIA_REVIEW_PROCESS_BUSY", "Another controlled process is already registered.");
1028
+ }
1029
+ return { ...current, proofProcess: { processId: child.pid, processIdentity: identity } };
1030
+ });
1031
+ child.stdin.end("start\n");
1032
+ })();
1033
+ child.stdin.on("error", terminate);
1034
+ void registration.catch(() => terminate());
1035
+ let checking = false;
1036
+ const control = setInterval(() => {
1037
+ if (checking)
1038
+ return;
1039
+ checking = true;
1040
+ void repository.read(runId).then(current => {
1041
+ if (current === null || current.disableRestart || current.status !== "active")
1042
+ terminate();
1043
+ }).catch(terminate).finally(() => { checking = false; });
1044
+ }, 250);
1045
+ child.stdout.on("data", chunk => { output += chunk.toString(); if (Buffer.byteLength(output) > 4 * 1024 * 1024)
1046
+ terminate(); });
1047
+ child.stderr.resume();
1048
+ child.once("error", cause => { closed = true; clearTimeout(timeout); clearInterval(control); reject(cause); });
1049
+ child.once("close", async () => {
1050
+ closed = true;
1051
+ clearTimeout(timeout);
1052
+ clearTimeout(escalation);
1053
+ clearInterval(control);
1054
+ try {
1055
+ await registration;
1056
+ await repository.mutate(runId, current => ({ ...current,
1057
+ ...(current.proofProcess?.processId === child.pid ? { proofProcess: null } : {}) }));
1058
+ const result = JSON.parse(output);
1059
+ if (!plain(result) || !["ok", "error"].includes(String(result.status)) || typeof result.code !== "string")
1060
+ throw new Error();
1061
+ const coordinatorRoot = saved.workers[0]?.worktree;
1062
+ if (!criteria && result.status === "error" && coordinatorRoot != null)
1063
+ await recordSavedReviewRunUsage(repository, runId, coordinatorRoot);
1064
+ if (plain(result.data) && plain(result.data.result) && typeof result.data.result.sourceFingerprint === "string" &&
1065
+ typeof result.data.result.taskId === "string") {
1066
+ const artifact = await readReviewReportArtifact(cwd, result.data.result.sourceFingerprint, result.data.result.taskId);
1067
+ if (artifact !== null)
1068
+ await recordReviewRunUsage(repository, runId, artifact);
1069
+ }
1070
+ resolve(result);
1071
+ }
1072
+ catch {
1073
+ reject(new AgentOpsError("RUN_FINAL_PROOF_INTERRUPTED", "Final gate did not return a complete result; inspect target and receipt before retrying."));
1074
+ }
1075
+ });
1076
+ });
1077
+ }
1078
+ export async function runCriteriaStep(repository, runId, cwd, taskId, step) {
1079
+ const current = (await repository.read(runId));
1080
+ if (current.status !== "active" || current.disableRestart)
1081
+ throw new AgentOpsError("RUN_CRITERIA_STOPPED", "Run stopped before criteria review.");
1082
+ try {
1083
+ if (current.proofProcess != null)
1084
+ throw new Error("Controlled process busy");
1085
+ const result = await step(cwd, ["review", "--criteria", "--task", taskId, "--yes"], current);
1086
+ const latest = (await repository.read(runId));
1087
+ if (latest.status !== "active" || latest.disableRestart || result.code === "CRITERIA_REVIEW_CANCELLED")
1088
+ throw new AgentOpsError("RUN_CRITERIA_STOPPED", "Criteria review cancelled; do not start implementation.");
1089
+ if (result.status === "ok")
1090
+ return result.data;
1091
+ }
1092
+ catch (cause) {
1093
+ const latest = await repository.read(runId);
1094
+ if (latest === null || latest.status !== "active" || latest.disableRestart ||
1095
+ cause instanceof AgentOpsError && cause.code === "RUN_CRITERIA_STOPPED")
1096
+ throw cause;
1097
+ }
1098
+ await repository.appendEvent(runId, { type: "diagnostic", code: "CRITERIA_REVIEW_NOT_RUN", workerId: null, taskId,
1099
+ detail: "Pre-edit advisory unavailable; final evidence gates still apply." }).catch(() => undefined);
1100
+ return { code: "CRITERIA_REVIEW_NOT_RUN", text: "Criteria advisory NOT_RUN (process unavailable); final evidence gates still apply." };
1101
+ }
@@ -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,10 +364,10 @@ 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
- 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.", "");
370
+ lines.push(" Every production task creation waits for one independent criteria advisory", " before implementation (also DEBUG and native run parents/children). It has", " no time/token budget; real launch/auth/output errors report NOT_RUN and", " allow work, while explicit cancellation means stop. Read the private report,", " decide which advice to adopt, and use task revise with reasons (including", " rejections). Revise does not rerun it. Explicit rerun only:", " `agent-ops review --criteria --task <id> --rerun --yes`.", " Start task creation/criteria review through the trusted outer host when", " the host sandbox blocks reviewer auth/network, using the same outer-host", " invocation described below for review. Never bypass a native denial.", " This advisory never replaces verify, final two-round review or finish.", "", "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
  }
372
372
  if (context.capabilities.includes("task")) {
373
373
  lines.push("Split a change that exceeds five acceptance criteria into subtasks:", "`agent-ops task create --parent <task-id>` records one, and", "`agent-ops task status --parent <task-id>` lists them. Each subtask", "carries its own criteria and verification; final review covers all", "tasks together when their worktrees are integrated;", "completing one never completes its parent.", "", ...autoRunInstruction(descriptor, context), "To review every active subtask of a parent together, run", "`agent-ops batch --parent <task-id> --yes` instead of one review per", "subtask. It needs what review needs: start it through the trusted outer", "host runner as its first and only invocation, with the same Codex", "elevation, and keep `--output <file>` instead of a shell redirect. It", "verifies, one task at a time, each task that lacks fresh evidence, and", "reviews a few tasks at once. Do not commit or edit the worktree while it", "runs: a moved HEAD voids every verify and review in the batch.", "For subagents in separate worktrees, use `task advance` instead.", "", "Delegate a subtask whose scope spans unknown files, broad scans, or an", "independent workstream instead of doing it inline. Name the bounded", "scope, the artifact to return, and the acceptance criteria it serves.", subagentInstruction(descriptor), "Read every returned artifact back yourself: integration, verification,", "and review stay with the coordinator.", "", "One writer at a time. `agent-ops verify` and `agent-ops review`", "fingerprint the working tree and report UNKNOWN when anything else", "writes it during the run, so delegate edits to one subagent at a time,", "or give each writer its own Git worktree and merge before verifying.", "Read-only delegation has no such limit and may run in parallel.", "", "Parallel conversations in one repository each need their own worktree.", "When `.agent-ops/config.json` sets `worktree.mode` to `auto`, run", "`agent-ops task create` from the main checkout before your first edit:", "it creates this session's worktree and puts the task there (`agent-ops", "worktree add <name> --session <session-id>` does the same without a", "task). Work only inside the printed path", "(Claude Code: EnterWorktree with that path; otherwise use it as every", "command's working directory). Commit, then run verify and review for", "each task with the printed `--base`; do not run `task complete` in the", "worktree. Leave it (Claude Code: ExitWorktree with action keep) and run", "`agent-ops worktree finish <name>` from the main checkout: it completes", "every task in the worktree, subtasks first, then merges. A finish that", "reports an incomplete task names it; verify or review that task again", "in the worktree and finish again. A finish that reports a conflict is", "resolved once, as a new task whose criteria cover both intents; if its", "verification or review fails, reset as instructed and report.", "", "For a session with writing subagents, record each child task's complete", "modification intent with `task create --intent <text>` before editing.", "Each child commits and runs local `verify --task` in its own worktree.", "From the main checkout run `agent-ops task advance --task <parent-id>", "--session <session-id> --yes` through the trusted outer host runner,", "unsetting `CODEX_SANDBOX_NETWORK_DISABLED` on Codex. Advance integrates", "child commits and intent into the coordinator worktree, verifies every", "task on the final candidate, then runs one two-round tree review.", "Only `NOT_RUN / scope-too-large` falls back to per-task two-round review;", "a substantive FAIL must be fixed. After PASS, advance writes a local", "receipt under the main checkout's Git common directory and calls finish.", "If the target moves, it retries the final gate once. An unresolved", "failure preserves worktrees and cannot be reported as finished.", "");
@@ -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) {
@@ -29,6 +29,18 @@ function sanitizeEvent(value) {
29
29
  if (!isRecord(value) || typeof value.type !== "string") {
30
30
  return invalidEvent();
31
31
  }
32
+ if (value.type === "criteria-review") {
33
+ if (!hasExactKeys(value, ["type", "taskId", "attemptId", "status", "code", "durationMs", "invocations", "recommendations", "report", "usage"]) ||
34
+ typeof value.taskId !== "string" || !ID_PATTERN.test(value.taskId) ||
35
+ typeof value.attemptId !== "string" || !/^[a-f0-9-]{36}$/u.test(value.attemptId) ||
36
+ !["RUNNING", "COMPLETED", "NOT_RUN", "CANCELLED"].includes(String(value.status)) ||
37
+ typeof value.code !== "string" || !ID_PATTERN.test(value.code) || !isBoundedString(value.report) ||
38
+ ![value.durationMs, value.invocations, value.recommendations].every(n => Number.isSafeInteger(n) && n >= 0) ||
39
+ !isRecord(value.usage) || Object.entries(value.usage).some(([key, n]) => !["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "totalTokens", "costUsd"].includes(key) ||
40
+ typeof n !== "number" || !Number.isFinite(n) || n < 0))
41
+ return invalidEvent();
42
+ return { ...value, report: redactSecrets(value.report) };
43
+ }
32
44
  if (value.type === "diagnostic") {
33
45
  if (!hasExactKeys(value, ["code", "type"]) ||
34
46
  typeof value.code !== "string" ||