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