@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.
- package/dist/packages/cli/src/args.js +56 -1
- package/dist/packages/cli/src/bin.js +29 -2
- package/dist/packages/cli/src/cli.js +9 -3
- package/dist/packages/cli/src/codex-loop-process.js +6 -0
- package/dist/packages/cli/src/commands/hook.js +4 -1
- package/dist/packages/cli/src/commands/review.js +9 -3
- package/dist/packages/cli/src/commands/task.js +44 -2
- package/dist/packages/cli/src/debug-intake.js +245 -0
- package/dist/packages/cli/src/hook-process.js +110 -37
- package/dist/runtime/src/adapters/agy/config.js +3 -2
- package/dist/runtime/src/adapters/claude/config.js +4 -3
- package/dist/runtime/src/adapters/codex/config.js +7 -3
- package/dist/runtime/src/adapters/codex/events.js +2 -1
- package/dist/runtime/src/adapters/codex/input.js +20 -2
- package/dist/runtime/src/adapters/codex/output.js +4 -1
- package/dist/runtime/src/config/merge.js +1 -1
- package/dist/runtime/src/hooks/completion-gate.js +15 -0
- package/dist/runtime/src/install/harness.js +2 -0
- package/dist/runtime/src/install/plan.js +3 -3
- package/dist/runtime/src/install/profiles.js +2 -0
- package/dist/runtime/src/office/art.js +2 -1
- package/dist/runtime/src/office/layout.js +6 -3
- package/dist/runtime/src/office/page.js +133 -9
- package/dist/runtime/src/office/scene.js +13 -4
- package/dist/runtime/src/office/snapshot.js +12 -0
- package/dist/runtime/src/parallel/manage.js +19 -2
- package/dist/runtime/src/review/attestation.js +4 -1
- package/dist/runtime/src/review/execute.js +1 -1
- package/dist/runtime/src/review/slots.js +59 -44
- package/dist/runtime/src/run/hosts/codex.js +1 -1
- package/dist/runtime/src/run/transport.js +3 -0
- package/dist/runtime/src/schema/validate.js +8 -2
- package/dist/runtime/src/security/permissions.js +3 -1
- package/dist/runtime/src/task/completion.js +20 -1
- package/dist/runtime/src/task/contract.js +1 -1
- package/dist/runtime/src/task/debug-intake.js +12 -0
- package/dist/runtime/src/task/debug-state.js +130 -0
- package/dist/runtime/src/task/debug-user-event.js +93 -0
- package/dist/runtime/src/task/debug.js +586 -0
- package/dist/runtime/src/task/service.js +46 -0
- package/dist/runtime/src/task/store.js +8 -1
- package/dist/runtime/src/verify/acceptance-replay.js +9 -4
- package/dist/runtime/src/verify/service.js +21 -2
- package/docs/en/guides/task-debug.md +111 -0
- package/docs/en/spec/harness-adapters.md +15 -2
- package/docs/zh-TW/guides/task-debug.md +66 -0
- package/docs/zh-TW/spec/harness-adapters.md +11 -2
- package/package.json +1 -1
- package/schemas/config.schema.json +30 -2
- package/schemas/manifest.schema.json +1 -0
- 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 =
|
|
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
|
|
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
|
+
}
|