@kylecheng3146/agent-ops 0.8.0 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/packages/cli/src/args.js +56 -1
  2. package/dist/packages/cli/src/bin.js +29 -2
  3. package/dist/packages/cli/src/cli.js +9 -3
  4. package/dist/packages/cli/src/codex-loop-process.js +6 -0
  5. package/dist/packages/cli/src/commands/hook.js +4 -1
  6. package/dist/packages/cli/src/commands/review.js +9 -3
  7. package/dist/packages/cli/src/commands/task.js +44 -2
  8. package/dist/packages/cli/src/debug-intake.js +245 -0
  9. package/dist/packages/cli/src/hook-process.js +110 -37
  10. package/dist/runtime/src/adapters/agy/config.js +3 -2
  11. package/dist/runtime/src/adapters/claude/config.js +4 -3
  12. package/dist/runtime/src/adapters/codex/config.js +7 -3
  13. package/dist/runtime/src/adapters/codex/events.js +2 -1
  14. package/dist/runtime/src/adapters/codex/input.js +20 -2
  15. package/dist/runtime/src/adapters/codex/output.js +4 -1
  16. package/dist/runtime/src/config/merge.js +1 -1
  17. package/dist/runtime/src/hooks/completion-gate.js +15 -0
  18. package/dist/runtime/src/install/harness.js +2 -0
  19. package/dist/runtime/src/install/plan.js +3 -3
  20. package/dist/runtime/src/install/profiles.js +2 -0
  21. package/dist/runtime/src/office/art.js +2 -1
  22. package/dist/runtime/src/office/layout.js +6 -3
  23. package/dist/runtime/src/office/page.js +133 -9
  24. package/dist/runtime/src/office/scene.js +13 -4
  25. package/dist/runtime/src/office/snapshot.js +12 -0
  26. package/dist/runtime/src/parallel/manage.js +19 -2
  27. package/dist/runtime/src/review/attestation.js +4 -1
  28. package/dist/runtime/src/review/execute.js +1 -1
  29. package/dist/runtime/src/review/slots.js +59 -44
  30. package/dist/runtime/src/run/hosts/codex.js +1 -1
  31. package/dist/runtime/src/run/transport.js +3 -0
  32. package/dist/runtime/src/schema/validate.js +8 -2
  33. package/dist/runtime/src/security/permissions.js +3 -1
  34. package/dist/runtime/src/task/completion.js +20 -1
  35. package/dist/runtime/src/task/contract.js +1 -1
  36. package/dist/runtime/src/task/debug-intake.js +12 -0
  37. package/dist/runtime/src/task/debug-state.js +130 -0
  38. package/dist/runtime/src/task/debug-user-event.js +93 -0
  39. package/dist/runtime/src/task/debug.js +586 -0
  40. package/dist/runtime/src/task/service.js +46 -0
  41. package/dist/runtime/src/task/store.js +8 -1
  42. package/dist/runtime/src/verify/acceptance-replay.js +9 -4
  43. package/dist/runtime/src/verify/service.js +21 -2
  44. package/docs/en/guides/task-debug.md +111 -0
  45. package/docs/en/spec/harness-adapters.md +15 -2
  46. package/docs/zh-TW/guides/task-debug.md +66 -0
  47. package/docs/zh-TW/spec/harness-adapters.md +11 -2
  48. package/package.json +1 -1
  49. package/schemas/config.schema.json +30 -2
  50. package/schemas/manifest.schema.json +1 -0
  51. package/schemas/task.schema.json +62 -2
@@ -75,6 +75,12 @@ export function parseArgs(argv) {
75
75
  let targetVersion;
76
76
  let title;
77
77
  let intent;
78
+ let taskGoal;
79
+ let debug = false;
80
+ let debugAction;
81
+ let payloadFile;
82
+ let commandId;
83
+ let eventId;
78
84
  let sessionId;
79
85
  let base;
80
86
  let parentBase;
@@ -210,6 +216,40 @@ export function parseArgs(argv) {
210
216
  index += 1;
211
217
  break;
212
218
  }
219
+ case "--goal":
220
+ if (taskGoal !== undefined)
221
+ duplicate(token);
222
+ taskGoal = readOptionValue(argv, index++, token);
223
+ break;
224
+ case "--debug":
225
+ if (debug)
226
+ duplicate(token);
227
+ debug = true;
228
+ break;
229
+ case "--debug-action": {
230
+ if (debugAction !== undefined)
231
+ duplicate(token);
232
+ const value = readOptionValue(argv, index++, token);
233
+ if (!["status", "hypothesis", "strategy", "investigate", "checkpoint", "resume", "extend"].includes(value))
234
+ invalidValue(token, value);
235
+ debugAction = value;
236
+ break;
237
+ }
238
+ case "--payload-file":
239
+ if (payloadFile !== undefined)
240
+ duplicate(token);
241
+ payloadFile = readOptionValue(argv, index++, token);
242
+ break;
243
+ case "--command-id":
244
+ if (commandId !== undefined)
245
+ duplicate(token);
246
+ commandId = readOptionValue(argv, index++, token);
247
+ break;
248
+ case "--event-id":
249
+ if (eventId !== undefined)
250
+ duplicate(token);
251
+ eventId = readOptionValue(argv, index++, token);
252
+ break;
213
253
  case "--tree": {
214
254
  if (tree)
215
255
  duplicate(token);
@@ -453,7 +493,7 @@ export function parseArgs(argv) {
453
493
  "complete",
454
494
  "create",
455
495
  "export",
456
- "status", "revise", "replan", "pin-finding"
496
+ "status", "revise", "replan", "pin-finding", "debug"
457
497
  ].includes(token)) {
458
498
  action = token;
459
499
  break;
@@ -635,6 +675,15 @@ export function parseArgs(argv) {
635
675
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--worktree may be used only with init or update.");
636
676
  }
637
677
  const revisionAction = command === "task" && ["revise", "replan", "pin-finding"].includes(action ?? "");
678
+ if ((taskGoal !== undefined || debug) && !(command === "task" && action === "create"))
679
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--goal and --debug apply only to task create.");
680
+ if ((debugAction !== undefined || commandId !== undefined || eventId !== undefined || payloadFile !== undefined) && !(command === "task" && action === "debug"))
681
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Debug operation options apply only to task debug.");
682
+ if (command === "task" && action === "debug" && (taskId === undefined || debugAction === undefined || title !== undefined || intent !== undefined ||
683
+ parentTaskId !== undefined || criteria.length > 0 || evidence.length > 0 ||
684
+ ((debugAction === "hypothesis" || debugAction === "strategy") !== (payloadFile !== undefined)) ||
685
+ ((debugAction === "investigate") !== (commandId !== undefined)) || ((debugAction === "extend") !== (eventId !== undefined))))
686
+ throw new CliArgumentError("CLI_MISSING_VALUE", "Task debug requires --task, --debug-action and the operation's exact payload/command/event options.");
638
687
  if ((criterionFiles.length > 0 && !(command === "task" && (action === "create" || action === "pin-finding"))) ||
639
688
  (criteriaFile !== undefined && !(command === "task" && action === "revise")) ||
640
689
  (planFile !== undefined && !(command === "task" && action === "replan")) ||
@@ -805,6 +854,12 @@ export function parseArgs(argv) {
805
854
  ...(targetVersion === undefined ? {} : { targetVersion }),
806
855
  ...(title === undefined ? {} : { title }),
807
856
  ...(intent === undefined ? {} : { intent }),
857
+ ...(taskGoal === undefined ? {} : { goal: taskGoal }),
858
+ ...(debug ? { debug: true } : {}),
859
+ ...(debugAction === undefined ? {} : { debugAction }),
860
+ ...(payloadFile === undefined ? {} : { payloadFile }),
861
+ ...(commandId === undefined ? {} : { commandId }),
862
+ ...(eventId === undefined ? {} : { eventId }),
808
863
  ...(reviewTargets.length === 0 ? {} : { reviewTargets }),
809
864
  ...(completionGate === undefined ? {} : { completionGate }),
810
865
  ...(office === undefined ? {} : { office }),
@@ -488,6 +488,7 @@ else {
488
488
  return await runTaskCommand({
489
489
  args,
490
490
  service: taskService,
491
+ ...(args.action !== "debug" ? {} : { trusted: await (async () => { const binding = await plannedTrustBinding(root, (await loadEffectiveConfig(root, "project")).config); return binding !== null && (await trustStore().status(binding)).status === "TRUSTED"; })() }),
491
492
  ...(policyConfigHash === undefined ? {} : { policyConfigHash }),
492
493
  ...(sessionId === undefined ? {} : { sessionId }),
493
494
  ...(fromMain
@@ -545,16 +546,41 @@ else {
545
546
  reviewActivity = undefined;
546
547
  }
547
548
  });
549
+ const debugRecord = reviewArgs.taskId === undefined ? undefined : await taskService.status({ taskId: reviewArgs.taskId });
550
+ const inDebug = (executor) => debugRecord?.task.debug === undefined ? executor : async (request) => await taskService.debugService().review(debugRecord.task.id, () => executor(request));
548
551
  const inSlot = (executor) => slotDir === undefined
549
- ? executor
552
+ ? inDebug(executor)
550
553
  : async (request) => await withReviewSlot({
551
554
  dir: slotDir,
552
555
  signal,
553
556
  holder: { root, ...(reviewArgs.taskId === undefined ? {} : { taskId: reviewArgs.taskId }) },
557
+ ...(debugRecord?.task.debug === undefined || reviewSessionId === undefined
558
+ ? {}
559
+ : (() => {
560
+ let paused = false;
561
+ const debug = taskService.debugService();
562
+ return {
563
+ onWaitStart: async () => {
564
+ if (paused)
565
+ return;
566
+ const current = await debug.root(debugRecord.task.id).catch(() => null);
567
+ if (current?.debug.foreground?.sessionId !== reviewSessionId)
568
+ return;
569
+ await debug.foregroundStop(debugRecord.task.id, reviewSessionId);
570
+ paused = true;
571
+ },
572
+ onWaitEnd: async () => {
573
+ if (!paused)
574
+ return;
575
+ await debug.foregroundStart(debugRecord.task.id, reviewSessionId);
576
+ paused = false;
577
+ }
578
+ };
579
+ })()),
554
580
  onWait: (line) => {
555
581
  process.stderr.write(`${progressPrefix}${line}\n`);
556
582
  }
557
- }, async () => await executor(request));
583
+ }, async () => await inDebug(executor)(request));
558
584
  return {
559
585
  args: reviewArgs,
560
586
  authorized: reviewArgs.yes,
@@ -574,6 +600,7 @@ else {
574
600
  config: reviewConfig,
575
601
  evidenceStore: new FileEvidenceStore(root, root),
576
602
  execute: inActivity(inSlot(createReviewExecutor({
603
+ ...(debugRecord?.task.debug === undefined ? {} : { remainingBudgetMs: () => taskService.debugService().remaining(debugRecord.task.id) }),
577
604
  runner: registeredRunProofRunner(),
578
605
  targets: configuredReviewTargets,
579
606
  cwd: root,
@@ -25,7 +25,7 @@ Commands:
25
25
  doctor Diagnose an installation
26
26
  update Update managed artifacts
27
27
  uninstall Remove managed artifacts
28
- task <create|status|attach|revise|replan|pin-finding|advance|complete|archive|export>
28
+ task <create|status|attach|revise|replan|pin-finding|debug|advance|complete|archive|export>
29
29
  Manage independent task acceptance state
30
30
  verify Run configured verification
31
31
  review Run an independent review or show recorded reports
@@ -170,13 +170,19 @@ Options:
170
170
  --json
171
171
  --yes
172
172
  `,
173
- task: `Usage: agent-ops task <create|status|attach|revise|replan|pin-finding|advance|complete|archive|export> [options]
173
+ task: `Usage: agent-ops task <create|status|attach|revise|replan|pin-finding|debug|advance|complete|archive|export> [options]
174
174
 
175
175
  Manage independent task acceptance state. Task commands accept none of
176
176
  --harness, --profile or --dry-run. Only advance accepts --yes.
177
177
 
178
178
  Options:
179
179
  --title <text> create
180
+ --goal <text> create: immutable original issue/goal
181
+ --debug create: task-level investigation tag
182
+ --debug-action <status|hypothesis|strategy|investigate|checkpoint|resume|extend>
183
+ --payload-file <path> debug hypothesis/strategy: bounded JSON
184
+ --command-id <id> debug investigate: trusted configured command
185
+ --event-id <native-event-id|latest> debug extend: authentic human continue event
180
186
  --intent <text> create: intended behavior and constraints before editing
181
187
  --criterion <json> create, repeatable, two to five total
182
188
  --criterion-file <path> create: one criterion object, repeatable; pin-finding: exactly one
@@ -188,7 +194,7 @@ Options:
188
194
  --baseline <git-ref> create/revise: baseline for new mechanical requirements
189
195
  --parent <task-id> create: record a subtask; status: list subtasks
190
196
  --task <id> status, attach, revise, replan, pin-finding, complete, archive, export, advance
191
- --session <id> create, attach, status, advance
197
+ --session <id> create, attach, status, advance, debug
192
198
  --evidence <criterion-id=reference> complete, repeatable
193
199
  --base <git-ref> complete: a clean committed range
194
200
  --yes advance: authorize the two review sessions
@@ -1,5 +1,6 @@
1
1
  import { PROJECT_LOOP_EVENTS, resolveProjectLoopRoot, runProjectLoop } from "../../../runtime/src/hooks/codex-loop.js";
2
2
  import { officeEnabled, officeLoopPhaseHint } from "./office-entry.js";
3
+ import { routeDebugPrompt } from "./debug-intake.js";
3
4
  const MAX_LOOP_INPUT_BYTES = 64 * 1024;
4
5
  function isLoopHarness(value) {
5
6
  return value === "claude" || value === "codex";
@@ -73,6 +74,11 @@ export async function runLoopProcess(argv, io, dependencies = {}) {
73
74
  ? {}
74
75
  : { telemetryMaxBytes: dependencies.telemetryMaxBytes })
75
76
  });
77
+ if (event === "UserPromptSubmit" && managedRoot !== null && result.exitCode === 0 && result.stdout === "" && result.stderr === "") {
78
+ const context = await routeDebugPrompt(managedRoot, input, harness).catch(error => `DEBUG intake unavailable: ${error instanceof Error ? error.message : "preserve current state"}`);
79
+ if (context !== null)
80
+ io.writeStdout(harness === "claude" ? JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: context } }) : context + "\n");
81
+ }
76
82
  if (result.stdout.length > 0) {
77
83
  io.writeStdout(result.stdout);
78
84
  }
@@ -28,7 +28,9 @@ export async function runHookCommand(options) {
28
28
  const { capabilities } = options.config.profiles.length === 0
29
29
  ? { capabilities: [] }
30
30
  : resolveCapabilities(options.config);
31
- const input = JSON.parse(options.stdin);
31
+ const input = options.input === undefined
32
+ ? JSON.parse(options.stdin)
33
+ : options.input;
32
34
  const descriptor = harnessDescriptor(options.harness);
33
35
  const normalized = normalizeHookInput(options.harness, input);
34
36
  if (normalized === null ||
@@ -55,6 +57,7 @@ export async function runHookCommand(options) {
55
57
  ? {}
56
58
  : { worktreeEnter: options.worktreeEnter })
57
59
  });
60
+ options.onResult?.(result);
58
61
  return descriptor.runtime.formatOutput(options.event, result);
59
62
  }
60
63
  catch {
@@ -15,6 +15,7 @@ import { assertSafeSupportingPaths, isReviewerPolicyPath, resolveReviewScope, re
15
15
  import { calculateSourceFingerprint } from "../../../../runtime/src/verify/source-fingerprint.js";
16
16
  import { calculateConfigHash, isPassingVerificationEvidence } from "../../../../runtime/src/verify/evidence.js";
17
17
  import { validateEvidence } from "../../../../runtime/src/schema/validate.js";
18
+ import { loadDebugProof, debugProofDigest } from "../../../../runtime/src/task/debug-state.js";
18
19
  import { okEnvelope } from "../output.js";
19
20
  /**
20
21
  * Review runs against one target. Argument parsing already rejects a
@@ -640,6 +641,7 @@ export async function runReviewCommand(options) {
640
641
  }
641
642
  context.coverage = rows;
642
643
  context.binding = {
644
+ ...(context.records.every(r => r.task.debug === undefined) || options.root === undefined ? {} : { debugProofs: Object.fromEntries(await Promise.all(context.records.filter(r => r.task.debug !== undefined).map(async (r) => [r.task.id, debugProofDigest(await loadDebugProof(options.root, r.task.debug.rootTaskId))]))) }),
643
645
  taskContractHash: taskContractHash(context.records.find(r => r.task.id === context.taskId).task),
644
646
  taskContracts: Object.fromEntries(context.records.map(r => [r.task.id, taskContractHash(r.task)])),
645
647
  ...(context.treeTasks === undefined ? {} : { treeContractHash: treeContractHash(context.records.map(r => r.task)) }),
@@ -682,7 +684,8 @@ export async function runReviewCommand(options) {
682
684
  const content = redactSecrets(JSON.stringify({ binding: context.binding, coverage: context.coverage,
683
685
  executionPolicy: options.config,
684
686
  artifacts: contractArtifacts.map(({ path, digest }) => ({ path, digest })),
685
- tasks: context.records.map(r => ({ task: r.task, revisions: r.revisions ?? [] })),
687
+ tasks: await Promise.all(context.records.map(async (r) => ({ task: r.task, revisions: r.revisions ?? [],
688
+ ...(r.task.debug === undefined ? {} : { debugProof: await loadDebugProof(options.root, r.task.debug.rootTaskId) }) }))),
686
689
  superseded: context.historyRecords ?? [] }, null, 2));
687
690
  const digest = sha256(content);
688
691
  const path = ".agent-ops/tasks/review-contracts/" + digest + ".json";
@@ -719,7 +722,7 @@ export async function runReviewCommand(options) {
719
722
  const priorFindings = options.root === undefined || context === undefined
720
723
  ? []
721
724
  : await findPriorFailingFindings(options.root, context.taskId);
722
- const result = await runIndependentReview({
725
+ const executeReview = async () => await runIndependentReview({
723
726
  invocation: {
724
727
  harness: target,
725
728
  plannedTargets,
@@ -738,6 +741,7 @@ export async function runReviewCommand(options) {
738
741
  reason: "missing-cli"
739
742
  }))
740
743
  });
744
+ const result = await executeReview();
741
745
  if (scope !== undefined && result.report !== undefined && options.root !== undefined) {
742
746
  try {
743
747
  if (result.report.changedFilesInspected.length !== scope.changedFiles.length ||
@@ -781,7 +785,9 @@ export async function runReviewCommand(options) {
781
785
  await options.tasks.treeContract(context.taskId) !== context.binding.treeContractHash) ||
782
786
  (context !== undefined && options.tasks !== undefined &&
783
787
  (await Promise.all(context.records.map(async (r) => taskContractHash((await options.tasks.status({ taskId: r.task.id })).task))))
784
- .some((hash, i) => hash !== taskContractHash(context.records[i].task)))) {
788
+ .some((hash, i) => hash !== taskContractHash(context.records[i].task))) ||
789
+ (context?.binding?.debugProofs !== undefined && options.root !== undefined &&
790
+ (await Promise.all(context.records.filter(r => r.task.debug !== undefined).map(async (r) => debugProofDigest(await loadDebugProof(options.root, r.task.debug.rootTaskId)) !== context.binding.debugProofs[r.task.id]))).some(Boolean))) {
785
791
  const changed = await persistReviewEvidence(options, sourceChangedResult(result), sourceFingerprint, context);
786
792
  return notRunEnvelope(changed);
787
793
  }
@@ -60,7 +60,7 @@ function taskAction(args) {
60
60
  "complete",
61
61
  "create",
62
62
  "export",
63
- "status", "revise", "replan", "pin-finding"
63
+ "status", "revise", "replan", "pin-finding", "debug"
64
64
  ].includes(args.action)) {
65
65
  throw new AgentOpsError("TASK_ACTION_INVALID", "A supported task action is required.");
66
66
  }
@@ -72,7 +72,7 @@ function taskEnvelope(action, code, message, record) {
72
72
  message,
73
73
  record,
74
74
  contractHash: taskContractHash(record.task),
75
- text: renderTaskMarkdown(record) + "\nContract: " + taskContractHash(record.task)
75
+ text: renderTaskMarkdown(record) + "\nContract: " + taskContractHash(record.task) + (record.debug === undefined ? "" : "\nDEBUG: " + JSON.stringify(record.debug))
76
76
  });
77
77
  }
78
78
  function renderTaskList(records) {
@@ -91,11 +91,15 @@ export async function runTaskCommand(options) {
91
91
  if (options.args.title === undefined) {
92
92
  throw new AgentOpsError("TASK_TITLE_REQUIRED", "Task creation requires --title.");
93
93
  }
94
+ if (options.args.debug)
95
+ await options.service.assertDebugBaseline();
94
96
  const worktree = sessionId === undefined || options.sessionWorktree === undefined
95
97
  ? undefined
96
98
  : await options.sessionWorktree(sessionId);
97
99
  const record = await (worktree?.service ?? options.service).create({
98
100
  title: options.args.title,
101
+ ...(options.args.goal === undefined ? {} : { goal: options.args.goal }),
102
+ ...(options.args.debug ? { debug: {} } : {}),
99
103
  ...(options.args.intent === undefined ? {} : { intent: options.args.intent }),
100
104
  criteria: [...(options.args.criteria ?? []).map(parseCriterion),
101
105
  ...await Promise.all((options.args.criterionFiles ?? []).map(async (path) => await jsonFile(path)))]
@@ -109,6 +113,12 @@ export async function runTaskCommand(options) {
109
113
  : { parentTaskId: options.args.parentTaskId }),
110
114
  ...(sessionId === undefined ? {} : { sessionId })
111
115
  });
116
+ // An explicit CLI DEBUG intake may be the only start signal on hosts
117
+ // without a prompt hook (notably agy). The native session owns the clock;
118
+ // foregroundStart is idempotent for a repeated hook or command.
119
+ if (options.args.debug && sessionId !== undefined) {
120
+ await (worktree?.service ?? options.service).debugService().foregroundStart(record.task.id, sessionId);
121
+ }
112
122
  const created = taskEnvelope(action, "TASK_CREATED", sessionId === undefined
113
123
  ? `Created task ${record.task.id}.`
114
124
  : record.task.parentTaskId === undefined
@@ -134,6 +144,38 @@ export async function runTaskCommand(options) {
134
144
  }
135
145
  };
136
146
  }
147
+ if (action === "debug") {
148
+ const id = requireTaskId(options.args), service = options.service.debugService();
149
+ switch (options.args.debugAction) {
150
+ case "hypothesis":
151
+ await service.hypothesis(id, await jsonFile(options.args.payloadFile));
152
+ break;
153
+ case "strategy":
154
+ await service.strategy(id, await jsonFile(options.args.payloadFile));
155
+ break;
156
+ case "investigate":
157
+ await service.investigate(id, options.args.commandId, options.trusted === true);
158
+ break;
159
+ case "checkpoint":
160
+ if (sessionId === undefined)
161
+ throw new AgentOpsError("TASK_SESSION_REQUIRED", "Debug checkpoint requires a session.");
162
+ await service.checkpoint(id, sessionId);
163
+ break;
164
+ case "resume":
165
+ if (sessionId === undefined)
166
+ throw new AgentOpsError("TASK_SESSION_REQUIRED", "Debug resume requires a session.");
167
+ await service.resume(id, sessionId);
168
+ break;
169
+ case "extend":
170
+ if (sessionId === undefined)
171
+ throw new AgentOpsError("TASK_SESSION_REQUIRED", "Debug extension requires a native session.");
172
+ await service.extend(id, sessionId, options.args.eventId);
173
+ break;
174
+ case "status": break;
175
+ default: throw new AgentOpsError("TASK_DEBUG_ACTION_INVALID", "A supported debug operation is required.");
176
+ }
177
+ return taskEnvelope(action, "TASK_DEBUG_UPDATED", "Original issue debug state preserved.", await service.root(id));
178
+ }
137
179
  if (action === "revise") {
138
180
  const payload = await jsonFile(options.args.criteriaFile);
139
181
  if (!Array.isArray(payload))
@@ -0,0 +1,245 @@
1
+ import { join } from "node:path";
2
+ import { TaskService } from "../../../runtime/src/task/service.js";
3
+ import { FileTaskStore } from "../../../runtime/src/task/store.js";
4
+ import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
5
+ import { evaluateGuardrail } from "../../../runtime/src/guardrails/evaluate.js";
6
+ import { isDebugIssue } from "../../../runtime/src/task/debug-intake.js";
7
+ import { ensureSessionWorktree, readWorktreeRecord, sessionWorktreeName } from "../../../runtime/src/parallel/service.js";
8
+ import { listWorktrees, resumeWorktree } from "../../../runtime/src/parallel/manage.js";
9
+ import { collectChangeSurface } from "../../../runtime/src/verify/change-surface.js";
10
+ import { calculateSourceFingerprint } from "../../../runtime/src/verify/source-fingerprint.js";
11
+ import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
12
+ import { loadEffectiveConfig } from "./context.js";
13
+ import { gitRunner, worktreeDependencies } from "./parallel-deps.js";
14
+ const foregroundClockSupported = (host) => host !== "opencode";
15
+ function nativeSessionId(input) {
16
+ const fromInput = [input.session_id, input.sessionId, input.conversationId, input.thread_id, input.threadId]
17
+ .find((value) => typeof value === "string" && value.length > 0);
18
+ return fromInput ?? [process.env.AGENT_OPS_SESSION_ID, process.env.CODEX_THREAD_ID]
19
+ .find((value) => value !== undefined && value.length > 0);
20
+ }
21
+ /** Start only an already attached DEBUG issue; native hooks never create work. */
22
+ export async function startDebugSession(root, sessionId, host) {
23
+ if (!foregroundClockSupported(host) || sessionId.length === 0)
24
+ return;
25
+ const config = (await loadEffectiveConfig(root, "project")).config;
26
+ const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId)));
27
+ const contextRoot = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
28
+ const contextConfig = contextRoot === root ? config : (await loadEffectiveConfig(contextRoot, "project")).config;
29
+ const tasks = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: gitRunner(contextRoot), loadConfig: async () => contextConfig } });
30
+ const attached = await tasks.status({ sessionId }).catch(() => null);
31
+ if (attached?.status === "active" && attached.task.debug !== undefined)
32
+ await tasks.debugService().foregroundStart(attached.task.id, sessionId);
33
+ }
34
+ export async function routeDebugPrompt(root, input, host = "claude") {
35
+ if (typeof input !== "object" || input === null || Array.isArray(input))
36
+ return null;
37
+ const fields = input;
38
+ if (typeof fields.prompt !== "string")
39
+ return null;
40
+ const continuation = /^(?:繼續|继续|continue)[。.!!]?$/iu.test(fields.prompt.trim());
41
+ const debugIssue = isDebugIssue(fields.prompt);
42
+ const config = (await loadEffectiveConfig(root, "project")).config;
43
+ if (config.features.debug?.enabled !== true)
44
+ return null;
45
+ if (evaluateGuardrail({ kind: "content", content: fields.prompt, scope: root }).action === "block")
46
+ return null;
47
+ const session = nativeSessionId(fields);
48
+ if (session === undefined) {
49
+ if (!debugIssue && !continuation)
50
+ return null;
51
+ return "DEBUG intake unsupported: this hook has no reliable native session identity. Use task create --debug --goal with the original issue in the native conversation.";
52
+ }
53
+ const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(session)));
54
+ const contextRoot = saved?.mainRoot === root && saved.sessionId === session ? saved.path : root;
55
+ const contextConfig = contextRoot === root ? config : (await loadEffectiveConfig(contextRoot, "project")).config;
56
+ const original = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: gitRunner(contextRoot), loadConfig: async () => contextConfig } });
57
+ const attached = await original.status({ sessionId: session }).catch(() => null);
58
+ if (attached?.status === "active" && attached.task.debug !== undefined) {
59
+ await original.debugService().syncNativeControl(attached.task.id, session);
60
+ const control = (await original.debugService().root(attached.task.id)).debug.control;
61
+ if (control?.paused === true)
62
+ return `DEBUG original issue ${attached.task.id} is paused by the human. Preserve its task, evidence and remaining budget; stop after saving progress. Only a native human continue event resumes it.`;
63
+ if (continuation) {
64
+ let message = `DEBUG continue preserved original issue ${attached.task.id}: native user event did not extend time; other limits remain authoritative.`;
65
+ try {
66
+ await original.debugService().extend(attached.task.id, session, "latest");
67
+ message = `DEBUG original issue ${attached.task.id}: native human time grant applied once; other limits remain authoritative.`;
68
+ }
69
+ catch (error) {
70
+ message = `DEBUG continue preserved original issue ${attached.task.id}: ${error instanceof Error ? error.message : "Native user approval unavailable"}`;
71
+ }
72
+ if (foregroundClockSupported(host)) {
73
+ try {
74
+ await original.debugService().foregroundStart(attached.task.id, session);
75
+ }
76
+ catch (error) {
77
+ message += ` Foreground clock unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}`;
78
+ }
79
+ }
80
+ else
81
+ message += " Foreground clock unsupported on this host; preserve the issue and use a host with a trustworthy end-of-turn signal.";
82
+ if (control?.mode === "diagnose")
83
+ message += " Diagnosis-only scope remains active; continue discriminating evidence without product repairs until a proven diagnosis or runtime blocker. Only a native human resume repair event restores the full loop.";
84
+ return message;
85
+ }
86
+ if (foregroundClockSupported(host)) {
87
+ try {
88
+ await original.debugService().foregroundStart(attached.task.id, session);
89
+ }
90
+ catch (error) {
91
+ return `DEBUG original issue ${attached.task.id}: foreground clock unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}`;
92
+ }
93
+ }
94
+ if (control?.mode === "diagnose")
95
+ return `DEBUG original issue ${attached.task.id}: diagnosis only. Continue reproduction and hypothesis evidence until a supported diagnosis or a runtime blocker. Do not repair product behavior. A failure to reproduce is not a stop condition; a human resume repair event restores the full loop.`;
96
+ if (!debugIssue)
97
+ return null;
98
+ return `Continue original DEBUG issue ${attached.task.id}; retain its existing budget, evidence and hypotheses.`;
99
+ }
100
+ if (!debugIssue && !continuation)
101
+ return null;
102
+ if (continuation) {
103
+ if (attached?.status !== "active" || attached.task.debug === undefined)
104
+ return null;
105
+ try {
106
+ await original.debugService().extend(attached.task.id, session, "latest");
107
+ return `DEBUG original issue ${attached.task.id}: native human time grant applied once; other limits remain authoritative.`;
108
+ }
109
+ catch (error) {
110
+ return `DEBUG continue preserved original issue ${attached.task.id}: ${error instanceof Error ? error.message : "Native user approval unavailable"}`;
111
+ }
112
+ }
113
+ if (attached?.status === "active")
114
+ return attached.task.debug === undefined ? null : `Continue original DEBUG issue ${attached.task.id}; retain its existing budget, evidence and hypotheses.`;
115
+ // Another session's worktree may already hold this issue; reusing it keeps
116
+ // its consumed budget and rounds instead of starting a fresh task.
117
+ const holders = await listWorktrees(worktreeDependencies(), root).catch(() => []);
118
+ for (const { record: held } of holders) {
119
+ if (held.sessionId === session)
120
+ continue;
121
+ const heldConfig = (await loadEffectiveConfig(held.path, "project").catch(() => null))?.config;
122
+ if (heldConfig === undefined)
123
+ continue;
124
+ const issue = await new TaskService(new FileTaskStore(join(held.path, ".agent-ops/tasks/state.json"), held.path), { completion: { root: held.path, gitRunner: gitRunner(held.path), loadConfig: async () => heldConfig } }).activeDebugRoot(fields.prompt).catch(() => null);
125
+ if (issue === null)
126
+ continue;
127
+ try {
128
+ await resumeWorktree(worktreeDependencies(), { cwd: root, name: held.name, sessionId: session });
129
+ }
130
+ catch (error) {
131
+ return `DEBUG original issue ${issue.task.id} is still active in ${held.path}; a new task would reset its budget. ${error instanceof Error ? error.message : "Resume needs confirmation."} After the user confirms, run agent-ops worktree resume ${held.name} --session ${session}.`;
132
+ }
133
+ return `Continue original DEBUG issue ${issue.task.id} in ${held.path}; its budget, rounds and evidence are preserved.`;
134
+ }
135
+ await original.assertDebugBaseline();
136
+ const record = config.worktree?.mode === "auto" && await readWorktreeRecord(root) === null ?
137
+ await ensureSessionWorktree(worktreeDependencies(), { cwd: root, sessionId: session }) : null;
138
+ const path = record?.path ?? root;
139
+ const effective = (await loadEffectiveConfig(path, "project")).config;
140
+ const tasks = new TaskService(new FileTaskStore(join(path, ".agent-ops/tasks/state.json"), path), { completion: { root: path, gitRunner: gitRunner(path), loadConfig: async () => effective } });
141
+ const existing = await tasks.status({ sessionId: session }).catch(() => null);
142
+ if (existing?.status === "active")
143
+ return existing.task.debug === undefined ? null : `Continue original DEBUG issue ${existing.task.id}.`;
144
+ const held = await tasks.activeDebugRoot(fields.prompt);
145
+ if (held !== null) {
146
+ await tasks.attach(session, held.task.id);
147
+ let message = `Continue original DEBUG issue ${held.task.id}; its budget, rounds and evidence are preserved.`;
148
+ if (foregroundClockSupported(host)) {
149
+ try {
150
+ await tasks.debugService().foregroundStart(held.task.id, session);
151
+ }
152
+ catch (error) {
153
+ message += ` Foreground clock unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}`;
154
+ }
155
+ }
156
+ return message;
157
+ }
158
+ const ids = effective.verification.commands.filter(c => c.required).map(c => c.id);
159
+ const baseline = (await gitRunner(path).run(["rev-parse", "--verify", "HEAD"])).stdout.toString().trim();
160
+ const criteria = ["issue-regression", "related-checks"].map(id => ({ id, description: id === "issue-regression" ? "Reproduce and fix the original reported issue with relevant regression evidence." : "Pass necessary related checks and two fresh independent reviews.", verifierIds: ids,
161
+ acceptance: { mode: "review-only", baselineCommit: baseline, bindings: [], reviewOnlyReason: "Provisional investigation intake only. Identify and revise to executable original-issue regression before debug success; this criterion cannot prove reproduction." } }));
162
+ const created = await tasks.create({ title: fields.prompt.split("\n")[0].slice(0, 160), goal: fields.prompt, intent: "Investigate and fix the original issue through authoritative task debug evidence.", debug: { facts: ["User-reported symptom is unverified."], unknowns: ["Baseline reproduction", "Root cause", "Relevant regression and related checks"] }, criteria, sessionId: session, policyConfigHash: calculateConfigHash(effective) });
163
+ let clockNote = "";
164
+ if (foregroundClockSupported(host)) {
165
+ try {
166
+ await tasks.debugService().foregroundStart(created.task.id, session);
167
+ }
168
+ catch (error) {
169
+ return `DEBUG issue ${created.task.id} created in ${path}, but its foreground clock is unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}. Preserve the issue and do not claim timed completion.`;
170
+ }
171
+ }
172
+ else
173
+ clockNote = " Native foreground timing is unsupported on this host; controlled operations remain evidence-timed and do not claim a complete active-time budget.";
174
+ return `DEBUG issue ${created.task.id} created in ${path}. Preserve the original goal. Investigate, record falsifiable hypotheses, revise provisional criteria to relevant behavioral regression, repair, verify, obtain two fresh independent reviews, then finish. A clean HEAD baseline is fixed automatically; 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. Defaults: 60 active minutes / 5 material repair candidates. No user-authored debug plan is required.${clockNote}`;
175
+ }
176
+ export async function interruptDebugSession(root, sessionId, nativeInput) {
177
+ const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId)));
178
+ const path = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
179
+ const tasks = new TaskService(new FileTaskStore(join(path, ".agent-ops/tasks/state.json"), path), { completion: { root: path, gitRunner: gitRunner(path), loadConfig: async () => (await loadEffectiveConfig(path, "project")).config } });
180
+ const attached = await tasks.status({ sessionId }).catch(error => {
181
+ if (error instanceof AgentOpsError && error.code === "TASK_SESSION_UNATTACHED")
182
+ return null;
183
+ throw error;
184
+ });
185
+ if (attached?.status === "active" && attached.task.debug !== undefined)
186
+ await tasks.debugService().interrupt(attached.task.id, sessionId, nativeInput);
187
+ }
188
+ /** Close only the task attached to a native host's verified session. */
189
+ export async function closeDebugSession(root, sessionId, host = "claude") {
190
+ if (!foregroundClockSupported(host))
191
+ return;
192
+ if (sessionId.length === 0)
193
+ return;
194
+ const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId)));
195
+ const contextRoot = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
196
+ const tasks = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: gitRunner(contextRoot), loadConfig: async () => (await loadEffectiveConfig(contextRoot, "project")).config } });
197
+ const attached = await tasks.status({ sessionId }).catch(() => null);
198
+ if (attached?.status === "active" && attached.task.debug !== undefined)
199
+ await tasks.debugService().foregroundStop(attached.task.id, sessionId);
200
+ }
201
+ /**
202
+ * Without the completion gate, an active DEBUG issue still cannot end a turn
203
+ * unless a current runtime stop checkpoint exists. Sessions without one pass.
204
+ */
205
+ export function debugStopGate(root, rootConfig) {
206
+ return { handle: async (event) => {
207
+ if (event.event !== "stop" || event.sessionId === undefined || event.sessionId.length === 0 ||
208
+ event.terminationReason !== "model_stop" || event.fullyIdle !== true)
209
+ return null;
210
+ const sessionId = event.sessionId;
211
+ const evaluate = async (attempt) => {
212
+ const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId))).catch(() => null);
213
+ const contextRoot = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
214
+ const config = contextRoot === root && attempt === 0 ? rootConfig : (await loadEffectiveConfig(contextRoot, "project")).config;
215
+ const runner = gitRunner(contextRoot);
216
+ const tasks = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: runner, loadConfig: async () => config } });
217
+ const attached = await tasks.status({ sessionId }).catch(error => {
218
+ if (error instanceof AgentOpsError && error.code === "TASK_SESSION_UNATTACHED")
219
+ return null;
220
+ throw error;
221
+ });
222
+ if (attached?.status !== "active" || attached.task.debug === undefined)
223
+ return null;
224
+ await tasks.debugService().prepareStop(attached.task.id, sessionId);
225
+ const surface = await collectChangeSurface(runner);
226
+ const fingerprint = await calculateSourceFingerprint(contextRoot, { mode: "worktree", changedFiles: surface.paths }, runner);
227
+ return await tasks.debugService().validCheckpoint(attached.task.id, sessionId, fingerprint)
228
+ ? { action: "continue", status: "UNKNOWN", code: "COMPLETION_GATE_TASK_DEBUG_STOPPED", remedy: "Original issue remains active with a runtime-bound non-success checkpoint." }
229
+ : { action: "block", status: "FAIL", code: "COMPLETION_GATE_TASK_DEBUG_ACTIVE", remedy: "Continue the original DEBUG investigation, repair, verification and review loop. Failure to reproduce and an arbitrary checkpoint do not permit stopping. Preserve the task and budgets; record a current runtime stop checkpoint only for a proven blocker or exhausted limits." };
230
+ };
231
+ // Reload the task/config once for a transient gate fault; never rewrite bad evidence.
232
+ for (let attempt = 0; attempt < 2; attempt++) {
233
+ try {
234
+ return await evaluate(attempt);
235
+ }
236
+ catch (error) {
237
+ if (attempt === 0)
238
+ continue;
239
+ const code = error instanceof AgentOpsError ? error.code : "IO_ERROR";
240
+ return { action: "continue", status: "UNKNOWN", code: "TASK_DEBUG_GATE_UNAVAILABLE", remedy: `DEBUG remains incomplete: gate ${code} failed after one targeted reload. Preserve the original task, evidence and budgets; run agent-ops doctor and restore readable task/config evidence before resuming. No completion is attested.` };
241
+ }
242
+ }
243
+ return null;
244
+ } };
245
+ }