@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
|
@@ -14,6 +14,7 @@ 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";
|
|
17
18
|
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
18
19
|
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
19
20
|
import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
|
|
@@ -21,7 +22,6 @@ import { readWorktreeRecord } from "../../../runtime/src/parallel/service.js";
|
|
|
21
22
|
import { FileRunRepository } from "../../../runtime/src/run/service.js";
|
|
22
23
|
import { validateWorkerStopHandoff } from "../../../runtime/src/run/worker-gate.js";
|
|
23
24
|
import { evaluateGuardrail } from "../../../runtime/src/guardrails/evaluate.js";
|
|
24
|
-
import { resolveCapabilities } from "../../../runtime/src/install/profiles.js";
|
|
25
25
|
import { STOP_VERIFICATION_ENV, StopVerificationService } from "../../../runtime/src/hooks/stop-service.js";
|
|
26
26
|
import { NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn.js";
|
|
27
27
|
import { normalizeHookInput, runHookCommand, HOOK_EVENTS } from "./commands/hook.js";
|
|
@@ -55,13 +55,19 @@ function parseInput(source) {
|
|
|
55
55
|
}
|
|
56
56
|
function withInjectedSession(harness, input) {
|
|
57
57
|
const sessionId = process.env.AGENT_OPS_SESSION_ID;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
typeof input
|
|
61
|
-
input
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
const codexSession = sessionId ?? process.env.CODEX_THREAD_ID;
|
|
59
|
+
if ((harness !== "agy" && harness !== "codex") ||
|
|
60
|
+
typeof input !== "object" || input === null || Array.isArray(input))
|
|
61
|
+
return input;
|
|
62
|
+
const fields = input;
|
|
63
|
+
if (harness === "agy") {
|
|
64
|
+
return (typeof fields.conversationId === "string" && fields.conversationId.length > 0) || sessionId === undefined
|
|
65
|
+
? input
|
|
66
|
+
: { ...fields, conversationId: sessionId };
|
|
67
|
+
}
|
|
68
|
+
return (typeof fields.session_id === "string" && fields.session_id.length > 0) || codexSession === undefined
|
|
69
|
+
? input
|
|
70
|
+
: { ...fields, session_id: codexSession };
|
|
65
71
|
}
|
|
66
72
|
function writeHookOutput(io, output) {
|
|
67
73
|
if (output.stdout.length > 0) {
|
|
@@ -227,7 +233,7 @@ function completionGateFor(root, config, gitRunner, harness) {
|
|
|
227
233
|
config,
|
|
228
234
|
gitRunner,
|
|
229
235
|
...(harness === undefined ? {} : { harness }),
|
|
230
|
-
taskService: new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root)),
|
|
236
|
+
taskService: new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root), { completion: { root, gitRunner, loadConfig: async () => config } }),
|
|
231
237
|
evidenceStore: new FileEvidenceStore(root, root),
|
|
232
238
|
forRoot: async (worktree) => {
|
|
233
239
|
const outcome = await loadProjectHookConfig(worktree);
|
|
@@ -248,7 +254,7 @@ function completionGateFor(root, config, gitRunner, harness) {
|
|
|
248
254
|
*/
|
|
249
255
|
async function closeOfficeSession(io, dependencies) {
|
|
250
256
|
try {
|
|
251
|
-
if (process.env.AGENT_OPS_DISABLE === "1"
|
|
257
|
+
if (process.env.AGENT_OPS_DISABLE === "1")
|
|
252
258
|
return 0;
|
|
253
259
|
const input = parseInput(await readStdin(io.stdin));
|
|
254
260
|
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
@@ -256,12 +262,15 @@ async function closeOfficeSession(io, dependencies) {
|
|
|
256
262
|
const fields = input;
|
|
257
263
|
const root = dependencies.root ?? (typeof fields.cwd === "string" ? fields.cwd : process.cwd());
|
|
258
264
|
const outcome = await hookConfigOutcome(root, dependencies.loadConfig);
|
|
259
|
-
if (outcome.kind === "invalid"
|
|
265
|
+
if (outcome.kind === "invalid")
|
|
260
266
|
return 0;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
267
|
+
if (dependencies.office !== undefined && outcome.config.features.office?.enabled === true)
|
|
268
|
+
await dependencies.office({
|
|
269
|
+
root, harness: "claude", event: "SessionEnd", input, validated: true,
|
|
270
|
+
...(typeof fields.session_id === "string" ? { sessionId: fields.session_id } : {})
|
|
271
|
+
});
|
|
272
|
+
if (typeof fields.session_id === "string")
|
|
273
|
+
await closeDebugSession(root, fields.session_id, "claude").catch(() => undefined);
|
|
265
274
|
}
|
|
266
275
|
catch {
|
|
267
276
|
// Office never blocks a session, least of all one that is ending.
|
|
@@ -292,13 +301,19 @@ async function promptSubmitted(harness, io, dependencies) {
|
|
|
292
301
|
...(typeof fields.session_id === "string" ? { sessionId: fields.session_id } : {})
|
|
293
302
|
}).catch(() => undefined);
|
|
294
303
|
}
|
|
295
|
-
if (config.profiles.length === 0
|
|
304
|
+
if (config.profiles.length === 0)
|
|
296
305
|
return 0;
|
|
297
306
|
// ponytail: the loop's 64 KiB prompt bound; a larger prompt goes unchecked.
|
|
298
307
|
const prompt = fields.prompt;
|
|
299
308
|
if (typeof prompt !== "string" || prompt.length === 0 || prompt.length > 64 * 1024 || prompt.includes("\0"))
|
|
300
309
|
return 0;
|
|
301
|
-
if (evaluateGuardrail({ kind: "content", content: prompt, scope: root }).action !== "block")
|
|
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");
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
if (!config.profiles.includes("guardrails") && !config.profiles.includes("loop") && !config.profiles.includes("run"))
|
|
302
317
|
return 0;
|
|
303
318
|
const reason = "agent-ops blocked a suspected secret.";
|
|
304
319
|
if (harness === "claude") {
|
|
@@ -314,14 +329,28 @@ async function promptSubmitted(harness, io, dependencies) {
|
|
|
314
329
|
}
|
|
315
330
|
export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
316
331
|
const [harness, event] = argv;
|
|
332
|
+
if (harness === "codex" && event === "Interrupt") {
|
|
333
|
+
try {
|
|
334
|
+
const raw = await readStdin(io.stdin), input = parseInput(raw);
|
|
335
|
+
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
336
|
+
return 0;
|
|
337
|
+
const fields = input;
|
|
338
|
+
if (typeof fields.session_id !== "string" || fields.session_id.length === 0)
|
|
339
|
+
return 0;
|
|
340
|
+
const root = dependencies.root ?? (typeof fields.cwd === "string" ? fields.cwd : process.cwd());
|
|
341
|
+
await interruptDebugSession(root, fields.session_id, raw);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
io.writeStdout(JSON.stringify({ systemMessage: "agent-ops: DEBUG interrupt progress could not be sealed; preserve the task and run agent-ops doctor before resuming." }));
|
|
345
|
+
}
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
317
348
|
if (harness === "claude" && event === "SessionEnd")
|
|
318
349
|
return await closeOfficeSession(io, dependencies);
|
|
319
350
|
if ((harness === "claude" || harness === "codex") && event === "UserPromptSubmit")
|
|
320
351
|
return await promptSubmitted(harness, io, dependencies);
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
// escape hatch could not be user-approved; opencode can only deny a tool
|
|
324
|
-
// call, never a stop.
|
|
352
|
+
// The generic completion gate requires an approval escape hatch. Codex
|
|
353
|
+
// only enforces DEBUG through runtime checkpoints and native human control.
|
|
325
354
|
const completionGateInstalled = (harness === "agy" || harness === "claude") &&
|
|
326
355
|
event === "Stop" &&
|
|
327
356
|
argv.includes("--completion-gate");
|
|
@@ -370,8 +399,19 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
370
399
|
root = inputRoot;
|
|
371
400
|
}
|
|
372
401
|
}
|
|
373
|
-
|
|
402
|
+
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);
|
|
374
406
|
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
|
+
}
|
|
375
415
|
if (completionGateInstalled) {
|
|
376
416
|
writeHookOutput(io, harnessDescriptor(harness).runtime.formatOutput("Stop", {
|
|
377
417
|
action: "block",
|
|
@@ -394,6 +434,15 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
394
434
|
return 0;
|
|
395
435
|
}
|
|
396
436
|
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
|
+
}
|
|
397
446
|
if (completionGateInstalled && !config.features.completionGate.enabled) {
|
|
398
447
|
writeHookOutput(io, harnessDescriptor(harness).runtime.formatOutput("Stop", {
|
|
399
448
|
action: "block",
|
|
@@ -407,8 +456,10 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
407
456
|
? await repositoryTrust(root, config, cliVersion)
|
|
408
457
|
: await dependencies.trust(root, config, cliVersion);
|
|
409
458
|
const trusted = trustStatus === "TRUSTED";
|
|
459
|
+
const normalized = normalizeHookInput(harnessId, parsedInput);
|
|
460
|
+
if (normalized?.event === "session-start" && normalized.sessionId !== undefined)
|
|
461
|
+
await startDebugSession(root, normalized.sessionId, harnessId).catch(() => undefined);
|
|
410
462
|
if (dependencies.office !== undefined && config.features.office?.enabled === true) {
|
|
411
|
-
const normalized = normalizeHookInput(harnessId, parsedInput);
|
|
412
463
|
if (normalized !== null) {
|
|
413
464
|
const phase = officePhaseHint(normalized);
|
|
414
465
|
void dependencies.office({
|
|
@@ -500,21 +551,43 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
500
551
|
await recordSessionOrigin(dependencies.worktree, { cwd: root, sessionId }).catch(() => undefined);
|
|
501
552
|
}
|
|
502
553
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
config
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
554
|
+
let hookResult;
|
|
555
|
+
// Without the gate, DEBUG still refuses a Stop that has no valid stop
|
|
556
|
+
// checkpoint. Its own failure stays advisory, so it never blocks other work.
|
|
557
|
+
const debugStop = completionGate === undefined && hookEvent === "Stop" && normalized !== null &&
|
|
558
|
+
(harnessId === "agy" || harnessId === "claude" || harnessId === "codex") && config.features.debug?.enabled === true
|
|
559
|
+
? await debugStopGate(root, config).handle(normalized)
|
|
560
|
+
: null;
|
|
561
|
+
if (debugStop !== null)
|
|
562
|
+
hookResult = debugStop;
|
|
563
|
+
const output = debugStop !== null
|
|
564
|
+
? harnessDescriptor(harnessId).runtime.formatOutput("Stop", debugStop)
|
|
565
|
+
: await runHookCommand({
|
|
566
|
+
harness: harness,
|
|
567
|
+
event: hookEvent,
|
|
568
|
+
stdin: rawInput,
|
|
569
|
+
input: parsedInput,
|
|
570
|
+
config,
|
|
571
|
+
trusted,
|
|
572
|
+
onResult: result => { hookResult = result; },
|
|
573
|
+
...(worktreeGuard === undefined ? {} : { worktreeGuard }),
|
|
574
|
+
...(worktreeEnter === undefined ? {} : { worktreeEnter }),
|
|
575
|
+
...(dependencies.advisory === undefined
|
|
576
|
+
? {}
|
|
577
|
+
: { advisory: dependencies.advisory }),
|
|
578
|
+
...(stopVerification === undefined ? {} : { stopVerification }),
|
|
579
|
+
...(completionGate === undefined ? {} : { completionGate })
|
|
580
|
+
});
|
|
517
581
|
writeHookOutput(io, output);
|
|
582
|
+
// A Stop can be advisory or gated, but the foreground clock only ends on
|
|
583
|
+
// the same trusted idle model_stop that the native decision accepted.
|
|
584
|
+
if (normalized?.event === "stop" &&
|
|
585
|
+
normalized.sessionId !== undefined &&
|
|
586
|
+
normalized.terminationReason === "model_stop" &&
|
|
587
|
+
normalized.fullyIdle === true &&
|
|
588
|
+
hookResult?.action === "continue") {
|
|
589
|
+
await closeDebugSession(root, normalized.sessionId, harnessId).catch(() => undefined);
|
|
590
|
+
}
|
|
518
591
|
}
|
|
519
592
|
catch {
|
|
520
593
|
if (completionGateInstalled) {
|
|
@@ -54,7 +54,7 @@ export function buildAgyHookSettings(capabilities, runtimePath, platform = proce
|
|
|
54
54
|
throw new AgentOpsError("AGY_HOOK_PATH_INVALID", "agy hook runtime path is invalid.");
|
|
55
55
|
}
|
|
56
56
|
const hooks = {};
|
|
57
|
-
if ((capabilities.includes("lifecycle-summary") || capabilities.includes("office-presence")) || capabilities.includes("project-loop")) {
|
|
57
|
+
if ((capabilities.includes("lifecycle-summary") || capabilities.includes("office-presence") || capabilities.includes("task-debug")) || 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,7 +62,8 @@ 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")
|
|
65
|
+
capabilities.includes("completion-gate") ||
|
|
66
|
+
capabilities.includes("task-debug")) {
|
|
66
67
|
hooks.Stop = [handler(runtimePath, "Stop", platform, capabilities.includes("completion-gate"))];
|
|
67
68
|
}
|
|
68
69
|
return { hooks };
|
|
@@ -115,9 +115,10 @@ 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") || capabilities.includes("task-debug")) {
|
|
119
119
|
hooks.UserPromptSubmit = [matcherGroup("UserPromptSubmit", runtimePath)];
|
|
120
|
-
|
|
120
|
+
if (capabilities.includes("command-policy") || capabilities.includes("office-presence"))
|
|
121
|
+
hooks.PreToolUse = [matcherGroup("PreToolUse", runtimePath)];
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
// The gate supersedes report-only Stop verification: one managed Stop
|
|
@@ -125,7 +126,7 @@ export function buildClaudeHookSettings(capabilities, runtimePath, platform = pr
|
|
|
125
126
|
if (capabilities.includes("completion-gate")) {
|
|
126
127
|
hooks.Stop = [matcherGroup("Stop", runtimePath, true)];
|
|
127
128
|
}
|
|
128
|
-
else if (capabilities.includes("optional-stop-verify") || capabilities.includes("office-presence")) {
|
|
129
|
+
else if (capabilities.includes("task-debug") || capabilities.includes("optional-stop-verify") || capabilities.includes("office-presence")) {
|
|
129
130
|
hooks.Stop = [matcherGroup("Stop", runtimePath)];
|
|
130
131
|
}
|
|
131
132
|
// Stop only ends a turn. SessionEnd is the one signal that the conversation
|
|
@@ -84,14 +84,18 @@ 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") || capabilities.includes("task-debug")) {
|
|
88
88
|
hooks.UserPromptSubmit = [matcherGroup("UserPromptSubmit", runtimePath)];
|
|
89
|
-
|
|
89
|
+
if (capabilities.includes("command-policy") || capabilities.includes("office-presence"))
|
|
90
|
+
hooks.PreToolUse = [matcherGroup("PreToolUse", runtimePath)];
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
|
-
if (capabilities.includes("optional-stop-verify") || capabilities.includes("office-presence")) {
|
|
93
|
+
if (capabilities.includes("task-debug") || capabilities.includes("optional-stop-verify") || capabilities.includes("office-presence")) {
|
|
93
94
|
hooks.Stop = [matcherGroup("Stop", runtimePath)];
|
|
94
95
|
}
|
|
96
|
+
if (capabilities.includes("task-debug")) {
|
|
97
|
+
hooks.Interrupt = [{ hooks: [{ ...commandHook("Interrupt", runtimePath), timeout: 3 }] }];
|
|
98
|
+
}
|
|
95
99
|
return {
|
|
96
100
|
description: "agent-ops lifecycle hooks.",
|
|
97
101
|
hooks
|
|
@@ -15,17 +15,35 @@ export function normalizeCodexHookInput(input) {
|
|
|
15
15
|
return normalizeHookEvent(input);
|
|
16
16
|
}
|
|
17
17
|
const projectRoot = input.cwd;
|
|
18
|
+
const sessionId = typeof input.session_id === "string" && input.session_id.length > 0
|
|
19
|
+
? input.session_id
|
|
20
|
+
: undefined;
|
|
18
21
|
if (input.hook_event_name === "SessionStart") {
|
|
19
|
-
|
|
22
|
+
const start = normalizeHookEvent({
|
|
20
23
|
event: "session-start",
|
|
21
24
|
projectRoot
|
|
22
25
|
});
|
|
26
|
+
return {
|
|
27
|
+
event: "session-start",
|
|
28
|
+
projectRoot: start.projectRoot,
|
|
29
|
+
...(sessionId === undefined ? {} : { sessionId })
|
|
30
|
+
};
|
|
23
31
|
}
|
|
24
32
|
if (input.hook_event_name === "Stop") {
|
|
25
|
-
|
|
33
|
+
const stop = normalizeHookEvent({
|
|
26
34
|
event: "stop",
|
|
27
35
|
projectRoot
|
|
28
36
|
});
|
|
37
|
+
return {
|
|
38
|
+
event: "stop",
|
|
39
|
+
projectRoot: stop.projectRoot,
|
|
40
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
41
|
+
terminationReason: typeof input.termination_reason === "string"
|
|
42
|
+
? input.termination_reason
|
|
43
|
+
: "model_stop",
|
|
44
|
+
// stop_hook_active means a prior continuation, not a non-idle Stop.
|
|
45
|
+
fullyIdle: true
|
|
46
|
+
};
|
|
29
47
|
}
|
|
30
48
|
if (input.hook_event_name === "PreToolUse" &&
|
|
31
49
|
input.tool_name === "Bash") {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export const CODEX_NON_ZERO_EXIT_BEHAVIOR = "UNKNOWN";
|
|
2
2
|
export const CODEX_PRE_TOOL_BLOCKING = "UNKNOWN";
|
|
3
3
|
export function codexHookOutput(event, result) {
|
|
4
|
+
if (event === "Stop" && result.action === "block") {
|
|
5
|
+
return { exitCode: 0, stdout: JSON.stringify({ decision: "block", reason: `agent-ops: ${result.code}. ${result.remedy ?? "Continue the unfinished task."}` }) };
|
|
6
|
+
}
|
|
4
7
|
if (event === "Stop" && result.evidence !== undefined) {
|
|
5
8
|
return {
|
|
6
9
|
exitCode: 0,
|
|
@@ -28,7 +31,7 @@ export function codexHookOutput(event, result) {
|
|
|
28
31
|
continue: result.action !== "block",
|
|
29
32
|
...(result.action === "block"
|
|
30
33
|
? { stopReason: result.code }
|
|
31
|
-
: { systemMessage: `agent-ops: ${result.code}` })
|
|
34
|
+
: { systemMessage: `agent-ops: ${result.code}${result.remedy === undefined ? "" : ". " + result.remedy}` })
|
|
32
35
|
})
|
|
33
36
|
};
|
|
34
37
|
}
|
|
@@ -114,7 +114,7 @@ export function mergeConfigLayers(inputLayers) {
|
|
|
114
114
|
let worktree;
|
|
115
115
|
for (const layer of layers) {
|
|
116
116
|
schemaVersion = effective(layer.config.schemaVersion, layer);
|
|
117
|
-
features = effective(layer.config.features, layer);
|
|
117
|
+
features = effective({ ...layer.config.features, ...(layer.config.features.debug === undefined && features?.value.debug !== undefined ? { debug: features.value.debug } : {}) }, layer);
|
|
118
118
|
// Whole-block override: a workflow choice, not a guardrail, so a project
|
|
119
119
|
// may turn a user-level `auto` off.
|
|
120
120
|
if (layer.config.worktree !== undefined) {
|
|
@@ -260,6 +260,9 @@ 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.");
|
|
263
266
|
const fingerprint = await this.#fingerprint();
|
|
264
267
|
const granted = await this.#store.mutate(sessionId, (state) => {
|
|
265
268
|
if (state === null) {
|
|
@@ -485,6 +488,18 @@ export class CompletionGateService {
|
|
|
485
488
|
}
|
|
486
489
|
return last;
|
|
487
490
|
}
|
|
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
|
+
}
|
|
488
503
|
if (changed) {
|
|
489
504
|
const failure = await this.#validateTask(sessionId);
|
|
490
505
|
if (failure !== null)
|
|
@@ -361,6 +361,8 @@ export function managedRules(descriptor, context) {
|
|
|
361
361
|
: [`Toolkit version: ${context.toolkitVersion}`]),
|
|
362
362
|
""
|
|
363
363
|
];
|
|
364
|
+
if (context.capabilities.includes("task-debug"))
|
|
365
|
+
lines.push("Task DEBUG routing is enabled for investigation/bugfix issues. Native prompt hooks create the issue task when a session identity is available; otherwise use agent-ops task create --debug --goal <verbatim issue> with two to five issue-specific criteria. Explain/read-only, feature and refactor requests use the existing normal task flow.", "For DEBUG: understand the issue, investigate with task debug --debug-action investigate --command-id <trusted verifier>, record falsifiable hypotheses with --debug-action hypothesis --payload-file <json>, reproduce, fix, verify, then obtain two fresh independent reviews and finish automatically. A clean HEAD is the automatic baseline; do not ask permission for it or offer diagnosis-only as a fallback. Failure to reproduce alone is not a stop condition: continue discriminating checks within the budget. Replace provisional intake criteria with relevant behavioral acceptance; baseline setup/skip/timeout/UNKNOWN is never reproduction. Never report success from edits or unrelated green checks.", "DEBUG defaults are 60 cumulative active minutes and 5 material repair candidates across the original issue, revisions, sessions and children. After two stagnant candidates document one substantive strategy switch; two further stagnant candidates block. Save task debug --debug-action checkpoint before stopping. Resume preserves budgets and blockers. A human continue event may authorize --debug-action extend --event-id latest; unsupported native event sources fail closed. Never use allow-stop for DEBUG.", "DEBUG human control comes only from native human events: '只診斷' / 'diagnosis only' keeps investigating without product repairs until current sealed baseline assertion FAIL confirms a hypothesis or runtime limits/blockers permit non-success Stop. '暫停' / 'pause' saves progress and stops immediately. '繼續' / 'continue' resumes the existing scope; '繼續修復' / 'resume repair' restores the full loop. Codex Interrupt is a pause. Preserve the original task, evidence, consumed time and rounds; diagnosis or pause never marks repair complete. CLI actor/text fields and synthetic instructions cannot authorize these changes.", "DEBUG is foreground work. On Claude, agy and native Codex, Stop refuses an active DEBUG issue without a valid stop checkpoint, with or without the completion gate. Codex uses decision:block to continue the original task even on repeated Stop; stop_hook_active does not waive it. Codex's generic completion gate remains unsupported; opencode has no supported foreground lifecycle. DEBUG gate faults get one targeted task/config reload, then an explicit UNKNOWN/incomplete diagnostic and non-success Stop, preserving state. Without reliable native session identity, automatic hook intake is unsupported; the existing native Agent can use the task CLI. Only a matching owned debug worktree checkpoint permits dirty recovery; preserve all original user edits.", "");
|
|
364
366
|
if (context.capabilities.includes("rules")) {
|
|
365
367
|
lines.push("For every change:", "", "1. Define two to five mechanically verifiable acceptance criteria.", "2. Inspect the smallest relevant scope 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.", "");
|
|
366
368
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { lstat, readFile } from "node:fs/promises";
|
|
2
|
+
import { DEBUG_DEFAULTS } from "../task/debug-state.js";
|
|
2
3
|
import { CONFIG_SCHEMA_VERSION, MANIFEST_SCHEMA_VERSION } from "../contracts.js";
|
|
3
4
|
import { discoverProject } from "../discovery/index.js";
|
|
4
5
|
import { sha256 } from "../fs/hash.js";
|
|
@@ -97,6 +98,7 @@ function buildConfig(profiles, existing, reviewTargets = [], detectedCommands =
|
|
|
97
98
|
profiles: [...profiles],
|
|
98
99
|
verification,
|
|
99
100
|
features: {
|
|
101
|
+
debug: existing?.features.debug ?? { ...DEBUG_DEFAULTS },
|
|
100
102
|
...(existing?.features ?? {
|
|
101
103
|
stopVerification: { enabled: false },
|
|
102
104
|
completionGate: { enabled: completionGateEnabled }
|
|
@@ -439,9 +441,7 @@ export async function createInstallPlan(options) {
|
|
|
439
441
|
const existing = await readExistingManifest(options.root);
|
|
440
442
|
assertCompatibleManifest(existing?.manifest ?? null, options.scope, options.harness, options.allowHarnessChange === true);
|
|
441
443
|
const config = await planConfig(options.root, resolved.profiles, existing?.manifest ?? null, options.existingConfig, options.reviewTargets ?? [], completionGateEnabled, options.worktree, options.officeEnabled);
|
|
442
|
-
resolved.capabilities =
|
|
443
|
-
if (config.config.features.office?.enabled === true)
|
|
444
|
-
resolved.capabilities.push("office-presence");
|
|
444
|
+
resolved.capabilities = resolveCapabilities(config.config).capabilities;
|
|
445
445
|
const existingOpencodePluginPath = existing?.manifest.artifacts.find(({ id }) => id === "opencode-plugin")?.path;
|
|
446
446
|
const explicitHookTargets = new Map();
|
|
447
447
|
for (const target of options.hookTargets ?? []) {
|
|
@@ -34,6 +34,8 @@ 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");
|
|
37
39
|
if (config.features.office?.enabled === true)
|
|
38
40
|
resolved.capabilities.push("office-presence");
|
|
39
41
|
if (config.features.stopVerification.enabled) {
|
|
@@ -293,7 +293,8 @@ export function avatarColors(identity, role) {
|
|
|
293
293
|
let hash = 0;
|
|
294
294
|
for (let i = 0; i < identity.length; i++)
|
|
295
295
|
hash = (Math.imul(hash, 31) + identity.charCodeAt(i)) >>> 0;
|
|
296
|
-
|
|
296
|
+
// The viewer always wears the lightest skin; everyone else follows their identity.
|
|
297
|
+
const hair = hairs[hash % 3], skin = skins[role === "viewer" ? 0 : Math.floor(hash / 3) % 3], shirt = clothes[role] ?? clothes.visitor;
|
|
297
298
|
return { O: "#292633", H: hair[0], I: hair[1], J: hair[2], S: skin[0], T: skin[1], U: skin[2],
|
|
298
299
|
C: shirt[0], D: shirt[1], E: shirt[2], P: "#404958", Q: "#626e7f", B: "#2e3443", K: "#292633", R: "#e39a86" };
|
|
299
300
|
}
|
|
@@ -116,10 +116,13 @@ export function officeLayout() {
|
|
|
116
116
|
board: { x: 118, y: 42 },
|
|
117
117
|
staff: {
|
|
118
118
|
stations: { qa: rooms[2].slots[0], desk: rooms[3].slots[0], armchair: rooms[3].slots[1], integrator: rooms[4].slots[0] },
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
// Where idle staff spend time: the sofa, both sides of the cafe table, then the cooler,
|
|
120
|
+
// vending machine, fridge and counter, seen from behind.
|
|
121
|
+
spots: [seat(83, 212, "sit", 262, { x: 100, y: 268 }), seat(101, 212, "sit", 262, { x: 118, y: 268 }),
|
|
122
|
+
seat(88, 241, "right0", 291, { x: 106, y: 290 }), seat(148, 241, "left0", 291, { x: 166, y: 290 }),
|
|
123
|
+
stand(137, 183, "up0"), stand(113, 181, "up0"), stand(87, 181, "up0"), stand(49, 181, "up0")]
|
|
122
124
|
},
|
|
125
|
+
look: { board: stand(134, 64, "up0"), status: stand(414, 100, "up0") },
|
|
123
126
|
hotspots: [
|
|
124
127
|
{ kind: "task", room: "planning", rects: [area("board", 118, 42)] },
|
|
125
128
|
// The monitor sits at (30, 1) inside a cubicle, one pixel further in once outlined.
|