@kylecheng3146/agent-ops 0.8.0 → 0.8.4
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 +4 -4
- 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 +129 -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/change-surface.js +4 -1
- 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
|
@@ -6,6 +6,7 @@ import { validateEvidence, validateTask, validateTaskAgainstConfig } from "../sc
|
|
|
6
6
|
import { renderTaskMarkdown } from "./render.js";
|
|
7
7
|
import { checkTaskCompletionEvidence, findIncompleteSubtask } from "./completion.js";
|
|
8
8
|
import { calculateConfigHash, FileEvidenceStore, isPassingVerificationEvidence } from "../verify/evidence.js";
|
|
9
|
+
import { collectChangeSurface } from "../verify/change-surface.js";
|
|
9
10
|
import { resolveReviewScope } from "../review/scope.js";
|
|
10
11
|
import { calculateSourceFingerprint } from "../verify/source-fingerprint.js";
|
|
11
12
|
import { acceptanceCoverage } from "../verify/acceptance-coverage.js";
|
|
@@ -14,6 +15,9 @@ import { findReviewAttestation } from "../review/attestation.js";
|
|
|
14
15
|
import { advanceFailureFingerprint } from "../verify/fingerprint.js";
|
|
15
16
|
import { taskContractHash, treeContractHash } from "./contract.js";
|
|
16
17
|
import { commitIdentity } from "../schema/acceptance.js";
|
|
18
|
+
import { createDebugState, DEBUG_DEFAULTS } from "./debug-state.js";
|
|
19
|
+
import { DebugService } from "./debug.js";
|
|
20
|
+
import { sameDebugText } from "./debug-intake.js";
|
|
17
21
|
const SESSION_ID_PATTERN = /^[^\0\r\n]{1,256}$/u;
|
|
18
22
|
function defaultTaskId() {
|
|
19
23
|
return `task-${randomUUID()}`;
|
|
@@ -84,6 +88,15 @@ export class TaskService {
|
|
|
84
88
|
this.#now = options.now ?? (() => new Date().toISOString());
|
|
85
89
|
this.#completion = options.completion;
|
|
86
90
|
}
|
|
91
|
+
debugService() {
|
|
92
|
+
if (this.#completion === undefined)
|
|
93
|
+
throw taskError("TASK_DEBUG_CONTEXT_REQUIRED", "Debug operations require repository context.");
|
|
94
|
+
return new DebugService({ store: this.#store, tasks: this, ...this.#completion });
|
|
95
|
+
}
|
|
96
|
+
async assertDebugBaseline() {
|
|
97
|
+
if (this.#completion === undefined || (await collectChangeSurface(this.#completion.gitRunner)).paths.length > 0)
|
|
98
|
+
throw taskError("TASK_DEBUG_BASELINE_DIRTY", "Preserve user edits; debug intake needs a clean committed baseline before worktree allocation.");
|
|
99
|
+
}
|
|
87
100
|
async create(input) {
|
|
88
101
|
if (input.policyConfigHash !== undefined &&
|
|
89
102
|
!/^[a-f0-9]{64}$/u.test(input.policyConfigHash)) {
|
|
@@ -92,6 +105,12 @@ export class TaskService {
|
|
|
92
105
|
if (input.sessionId !== undefined) {
|
|
93
106
|
assertSessionId(input.sessionId);
|
|
94
107
|
}
|
|
108
|
+
// A new session must not restart an active issue's budget and rounds.
|
|
109
|
+
if (input.debug !== undefined && input.parentTaskId === undefined && input.goal !== undefined) {
|
|
110
|
+
const existing = await this.activeDebugRoot(input.goal);
|
|
111
|
+
if (existing !== null)
|
|
112
|
+
return input.sessionId === undefined ? existing : await this.attach(input.sessionId, existing.task.id);
|
|
113
|
+
}
|
|
95
114
|
let createdSourceCommit = input.createdSourceCommit;
|
|
96
115
|
if (createdSourceCommit === undefined && this.#completion !== undefined) {
|
|
97
116
|
const head = await this.#completion.gitRunner.run(["rev-parse", "--verify", "HEAD"]);
|
|
@@ -115,6 +134,18 @@ export class TaskService {
|
|
|
115
134
|
? {}
|
|
116
135
|
: { parentTaskId: input.parentTaskId })
|
|
117
136
|
};
|
|
137
|
+
let debugSettings;
|
|
138
|
+
if (input.debug !== undefined) {
|
|
139
|
+
if (input.goal === undefined || createdSourceCommit === undefined || this.#completion === undefined)
|
|
140
|
+
throw taskError("TASK_DEBUG_CONTEXT_REQUIRED", "Debug intake requires the original issue and a committed baseline.");
|
|
141
|
+
if ((await collectChangeSurface(this.#completion.gitRunner)).paths.length > 0)
|
|
142
|
+
throw taskError("TASK_DEBUG_BASELINE_DIRTY", "Preserve original user edits; investigate read-only or ask for an explicit committed baseline.");
|
|
143
|
+
const config = await this.#completion.loadConfig();
|
|
144
|
+
if (config.features.debug?.enabled === false)
|
|
145
|
+
throw taskError("TASK_DEBUG_DISABLED", "Task debug routing is explicitly disabled.");
|
|
146
|
+
debugSettings = config.features.debug ?? DEBUG_DEFAULTS;
|
|
147
|
+
task.debug = { rootTaskId: task.id, facts: [...(input.debug.facts ?? [])], unknowns: [...(input.debug.unknowns ?? [])] };
|
|
148
|
+
}
|
|
118
149
|
const validation = validateTask(task);
|
|
119
150
|
if (!validation.ok) {
|
|
120
151
|
throw taskError("TASK_INVALID", validation.errors[0]?.message ?? "Task input is invalid.");
|
|
@@ -149,9 +180,15 @@ export class TaskService {
|
|
|
149
180
|
throw taskError("TASK_GOAL_IMMUTABLE", "Subtasks must retain their parent original goal.");
|
|
150
181
|
validation.value.goal = parent.task.goal;
|
|
151
182
|
}
|
|
183
|
+
if (parent.task.debug !== undefined) {
|
|
184
|
+
if (input.debug !== undefined)
|
|
185
|
+
throw taskError("TASK_DEBUG_ROOT_IMMUTABLE", "A child cannot create a new issue budget.");
|
|
186
|
+
validation.value.debug = structuredClone(parent.task.debug);
|
|
187
|
+
}
|
|
152
188
|
}
|
|
153
189
|
const record = {
|
|
154
190
|
task: validation.value,
|
|
191
|
+
...(input.debug === undefined ? {} : { debug: createDebugState(task.id, createdSourceCommit, debugSettings, this.#completion.root) }),
|
|
155
192
|
...(createdSourceCommit === undefined ? {} : { createdSourceCommit }),
|
|
156
193
|
status: "active",
|
|
157
194
|
evidence: {},
|
|
@@ -326,6 +363,8 @@ export class TaskService {
|
|
|
326
363
|
const task = { schemaVersion: TASK_SCHEMA_VERSION, id: planned.id ?? this.#generateId(),
|
|
327
364
|
title: planned.title, intent: planned.intent, goal: root.task.goal ?? root.task.intent ?? root.task.title,
|
|
328
365
|
criteria: structuredClone(planned.criteria), parentTaskId: taskId };
|
|
366
|
+
if (root.task.debug !== undefined)
|
|
367
|
+
task.debug = structuredClone(root.task.debug);
|
|
329
368
|
const checked = config === undefined ? validateTask(task) : validateTaskAgainstConfig(task, config);
|
|
330
369
|
if (!checked.ok)
|
|
331
370
|
throw taskError("TASK_INVALID", checked.errors[0]?.message ?? "Invalid split task.");
|
|
@@ -388,6 +427,8 @@ export class TaskService {
|
|
|
388
427
|
if (record.policyConfigHash !== parent.policyConfigHash || !validateTask(task).ok) {
|
|
389
428
|
throw taskError("TASK_DELIVERY_INVALID", `Task ${task.id} has an incompatible policy or definition.`);
|
|
390
429
|
}
|
|
430
|
+
if (parent.task.debug !== undefined && (task.goal !== parent.task.goal || JSON.stringify(task.debug) !== JSON.stringify(parent.task.debug) || record.debug !== undefined))
|
|
431
|
+
throw taskError("TASK_DEBUG_ROOT_MISMATCH", "A delivery must retain the original issue identity and cannot import an independent debug budget.");
|
|
391
432
|
const existing = state.tasks.find((item) => item.task.id === task.id);
|
|
392
433
|
if (existing !== undefined) {
|
|
393
434
|
if (JSON.stringify(existing.task) !== JSON.stringify(task)) {
|
|
@@ -413,6 +454,11 @@ export class TaskService {
|
|
|
413
454
|
}
|
|
414
455
|
});
|
|
415
456
|
}
|
|
457
|
+
/** The active DEBUG root already holding this original issue, if any. */
|
|
458
|
+
async activeDebugRoot(goal) {
|
|
459
|
+
return (await this.list()).find((record) => record.status === "active" && record.task.debug?.rootTaskId === record.task.id &&
|
|
460
|
+
record.task.goal !== undefined && sameDebugText(record.task.goal, goal)) ?? null;
|
|
461
|
+
}
|
|
416
462
|
async list(filter = {}) {
|
|
417
463
|
const state = await this.#store.read();
|
|
418
464
|
return state.tasks
|
|
@@ -4,6 +4,7 @@ import { AgentOpsError } from "../fs/paths.js";
|
|
|
4
4
|
import { validateTask } from "../schema/validate.js";
|
|
5
5
|
import { readPrivateFile, withPrivateFileLock, writePrivateFile } from "../security/permissions.js";
|
|
6
6
|
import { taskContractHash } from "./contract.js";
|
|
7
|
+
import { assertDebugState } from "./debug-state.js";
|
|
7
8
|
import { commitIdentity, acceptanceSourcePath } from "../schema/acceptance.js";
|
|
8
9
|
const DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
9
10
|
const SESSION_ID_PATTERN = /^[^\0\r\n]{1,256}$/u;
|
|
@@ -68,7 +69,7 @@ function parseTaskRecord(value) {
|
|
|
68
69
|
];
|
|
69
70
|
// Every record written before an optional field existed must still parse, so
|
|
70
71
|
// the accepted shapes are every combination of them.
|
|
71
|
-
const optionalKeys = ["completionBase", "failureFingerprint", "policyConfigHash", "reviewBase", "createdSourceCommit", "revisions", "supersededBy", "noChangePaths"];
|
|
72
|
+
const optionalKeys = ["completionBase", "failureFingerprint", "policyConfigHash", "reviewBase", "createdSourceCommit", "revisions", "supersededBy", "noChangePaths", "debug"];
|
|
72
73
|
if (!isRecord(value) || !baseKeys.every(k => Object.hasOwn(value, k)) ||
|
|
73
74
|
Object.keys(value).some(k => !baseKeys.includes(k) && !optionalKeys.includes(k))) {
|
|
74
75
|
return invalidState("Task state contains an invalid task record.");
|
|
@@ -77,6 +78,11 @@ function parseTaskRecord(value) {
|
|
|
77
78
|
if (!task.ok) {
|
|
78
79
|
return invalidState("Task state contains an invalid task.");
|
|
79
80
|
}
|
|
81
|
+
if (value.debug !== undefined) {
|
|
82
|
+
assertDebugState(value.debug);
|
|
83
|
+
if (value.debug.rootTaskId !== task.value.id || task.value.debug?.rootTaskId !== task.value.id)
|
|
84
|
+
return invalidState("Debug counters belong only to the original issue root.");
|
|
85
|
+
}
|
|
80
86
|
if (!["active", "archived", "complete"].includes(String(value.status)) ||
|
|
81
87
|
!isTimestamp(value.createdAt) ||
|
|
82
88
|
!isTimestamp(value.updatedAt) ||
|
|
@@ -182,6 +188,7 @@ function parseTaskRecord(value) {
|
|
|
182
188
|
}
|
|
183
189
|
return {
|
|
184
190
|
task: task.value,
|
|
191
|
+
...(value.debug === undefined ? {} : { debug: value.debug }),
|
|
185
192
|
...(value.noChangePaths === undefined ? {} : { noChangePaths: value.noChangePaths }),
|
|
186
193
|
...(value.createdSourceCommit === undefined ? {} : { createdSourceCommit: value.createdSourceCommit }),
|
|
187
194
|
...(value.revisions === undefined ? {} : { revisions: value.revisions }),
|
|
@@ -113,9 +113,14 @@ export async function replayAcceptance(options) {
|
|
|
113
113
|
const temp = await mkdtemp(join(tmpdir(), "agent-ops-acceptance-"));
|
|
114
114
|
const checkout = join(temp, "repository");
|
|
115
115
|
const env = replayEnvironment(checkout, phase, executionId);
|
|
116
|
-
const command = async (executable, args, cwd, timeoutMs = 60_000) =>
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
const command = async (executable, args, cwd, timeoutMs = 60_000) => {
|
|
117
|
+
const remaining = await options.remainingMs?.();
|
|
118
|
+
if (remaining === 0)
|
|
119
|
+
throw new AgentOpsError("TASK_DEBUG_BUDGET_EXHAUSTED", "The original issue's active budget is exhausted.");
|
|
120
|
+
return await runVerificationCommand({ id: group.runner.id, command: executable, args: [...args], cwd: ".", required: true,
|
|
121
|
+
evidence: { kind: "exit-code" }, timeoutMs: Math.min(timeoutMs, remaining ?? Infinity) }, { cwd, runner: options.processRunner, env, replaceEnv: true,
|
|
122
|
+
outputLimitBytes: 1024 * 1024, ...(options.signal === undefined ? {} : { signal: options.signal }) });
|
|
123
|
+
};
|
|
119
124
|
let aggregate;
|
|
120
125
|
let failure = "replay-unavailable";
|
|
121
126
|
let output;
|
|
@@ -200,7 +205,7 @@ export async function replayAcceptance(options) {
|
|
|
200
205
|
failure = aggregate?.failureClass ?? output.failureClass;
|
|
201
206
|
}
|
|
202
207
|
catch (error) {
|
|
203
|
-
failure = error instanceof Error && /^[a-z-]+$/u.test(error.message) ? error.message : "replay-unavailable";
|
|
208
|
+
failure = error instanceof AgentOpsError && error.code === "TASK_DEBUG_BUDGET_EXHAUSTED" ? "debug-budget-exhausted" : error instanceof Error && /^[a-z-]+$/u.test(error.message) ? error.message : "replay-unavailable";
|
|
204
209
|
}
|
|
205
210
|
finally {
|
|
206
211
|
await rm(temp, { recursive: true, force: true });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TextDecoder } from "node:util";
|
|
2
2
|
import { AgentOpsError } from "../fs/paths.js";
|
|
3
3
|
const WINDOWS_RESERVED_SEGMENT = /^(?:aux|com[1-9]|con|lpt[1-9]|nul|prn)(?:\..*)?$/i;
|
|
4
|
+
const HARNESS_PROCESS_HEARTBEAT = /^\.(?:claude|codex)\/\.agent-ops-process-[1-9]\d*-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
4
5
|
function sortedUnique(values) {
|
|
5
6
|
return [...new Set(values)].sort();
|
|
6
7
|
}
|
|
@@ -91,7 +92,9 @@ export async function collectChangeSurface(runner) {
|
|
|
91
92
|
])).filter(
|
|
92
93
|
// Indexed runtime was rejected above; only generated, untracked output is excluded.
|
|
93
94
|
(path) => !path.startsWith(".agent-ops/tasks/") &&
|
|
94
|
-
!path.startsWith(".agent-ops/reviews/")
|
|
95
|
+
!path.startsWith(".agent-ops/reviews/") &&
|
|
96
|
+
// Private marker locks keep these runtime heartbeats until process exit.
|
|
97
|
+
!HARNESS_PROCESS_HEARTBEAT.test(path));
|
|
95
98
|
return {
|
|
96
99
|
staged,
|
|
97
100
|
unstaged,
|
|
@@ -70,7 +70,10 @@ export class VerificationService {
|
|
|
70
70
|
}
|
|
71
71
|
async #runCommand(task, command) {
|
|
72
72
|
const startedAt = (this.#options.now ?? (() => new Date().toISOString()))();
|
|
73
|
-
const
|
|
73
|
+
const remaining = task.debug === undefined ? undefined : await this.#options.taskService.debugService().remaining(task.id);
|
|
74
|
+
if (remaining === 0)
|
|
75
|
+
throw verificationError("TASK_DEBUG_BUDGET_EXHAUSTED", "The original issue's active budget is exhausted.");
|
|
76
|
+
const result = await executeConfiguredCommand(remaining === undefined ? command : { ...command, timeoutMs: Math.min(command.timeoutMs ?? 300_000, remaining) }, {
|
|
74
77
|
cwd: await this.#commandCwd(command),
|
|
75
78
|
runner: this.#options.processRunner,
|
|
76
79
|
trusted: this.#options.trusted
|
|
@@ -107,6 +110,11 @@ export class VerificationService {
|
|
|
107
110
|
};
|
|
108
111
|
}
|
|
109
112
|
async verify(taskId) {
|
|
113
|
+
const stored = await this.#options.taskService.status({ taskId });
|
|
114
|
+
return stored.task.debug === undefined ? await this.#verify(taskId) :
|
|
115
|
+
await this.#options.taskService.debugService().verification(taskId, () => this.#verify(taskId));
|
|
116
|
+
}
|
|
117
|
+
async #verify(taskId) {
|
|
110
118
|
const stored = await this.#options.taskService.status({ taskId });
|
|
111
119
|
if (stored.status === "archived") {
|
|
112
120
|
throw verificationError("TASK_NOT_ACTIVE", "An archived task cannot be verified.");
|
|
@@ -145,7 +153,8 @@ export class VerificationService {
|
|
|
145
153
|
results.push(await this.#runCommand(validation.value, commandById(this.#options.config, commandId)));
|
|
146
154
|
}
|
|
147
155
|
const acceptance = validation.value.criteria.some(c => c.acceptance !== undefined && c.acceptance.mode !== "review-only")
|
|
148
|
-
? await replayAcceptance({ ...this.#options, task: validation.value, sourceFingerprint
|
|
156
|
+
? await replayAcceptance({ ...this.#options, task: validation.value, sourceFingerprint,
|
|
157
|
+
...(stored.task.debug === undefined ? {} : { remainingMs: () => this.#options.taskService.debugService().remaining(taskId) }) }) : [];
|
|
149
158
|
let status = aggregateVerificationStatus([...results, ...acceptance.map(a => ({ required: true, status: a.status }))]);
|
|
150
159
|
let sourceChanged = false;
|
|
151
160
|
const postflightScope = await resolveReviewScope({
|
|
@@ -188,6 +197,16 @@ export class VerificationService {
|
|
|
188
197
|
}
|
|
189
198
|
}
|
|
190
199
|
else {
|
|
200
|
+
if (stored.task.debug !== undefined && !sourceChanged) {
|
|
201
|
+
const failures = {};
|
|
202
|
+
for (const result of results) {
|
|
203
|
+
const references = await this.#persistEvidence(validation.value, commandById(this.#options.config, result.commandId), result.startedAt, result.finishedAt, result, sourceFingerprint);
|
|
204
|
+
relevantCriteria(validation.value, result.commandId).forEach((criterion, index) => { const reference = references[index]; if (reference !== undefined)
|
|
205
|
+
failures[criterion.id] = [...(failures[criterion.id] ?? []), reference]; });
|
|
206
|
+
}
|
|
207
|
+
if (Object.keys(failures).length > 0)
|
|
208
|
+
await this.#options.taskService.recordEvidence(taskId, failures);
|
|
209
|
+
}
|
|
191
210
|
const required = [...results.filter((result) => result.required), ...acceptance.map(a => ({ commandId: a.runnerId, required: true,
|
|
192
211
|
status: a.status, failureClass: a.failureClass, exitCode: null, timedOut: false, testCount: null,
|
|
193
212
|
diagnostic: a.criterionId + ":" + a.phase + ":" + a.failureClass, evidenceReferences: [a.reference], startedAt: "", finishedAt: "" }))];
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Task DEBUG investigations
|
|
2
|
+
|
|
3
|
+
Project `init` and `update` add `features.debug` by default:
|
|
4
|
+
`{"enabled":true,"budgetMs":3600000,"maxRounds":5}` (sixty minutes). Explicit disabled and custom
|
|
5
|
+
settings survive subsequent installs. Old configurations and ordinary tasks
|
|
6
|
+
remain readable. DEBUG is an optional task intake field, not a new run mode.
|
|
7
|
+
|
|
8
|
+
Claude and Codex native prompt hooks conservatively recognize investigation and
|
|
9
|
+
bug-fix requests. They retain the issue verbatim as `goal`, record uncertain
|
|
10
|
+
facts/unknowns, and create an existing isolated task. Explain, feature and
|
|
11
|
+
refactor requests retain their ordinary behavior. A reliable native session ID
|
|
12
|
+
is required; agy does not provide this prompt intake. Its native agent can use
|
|
13
|
+
`task create --debug --goal '<original issue>'` with two to five criteria, which
|
|
14
|
+
starts the same foreground clock when the native session is available.
|
|
15
|
+
An original dirty checkout is refused before any worktree snapshot is created.
|
|
16
|
+
|
|
17
|
+
The intake criteria are provisional. The native agent investigates via trusted
|
|
18
|
+
configured commands, records hypotheses, replaces the criteria with relevant
|
|
19
|
+
behavioral acceptance, fixes the product, and uses existing `verify`, `review`
|
|
20
|
+
and worktree finish commands. There is no additional model or background worker.
|
|
21
|
+
The default is the full repair loop. A clean HEAD fixes the baseline automatically;
|
|
22
|
+
do not request redundant approval or offer diagnosis-only as a fallback. Failure
|
|
23
|
+
to reproduce alone does not permit Stop: continue discriminating checks within
|
|
24
|
+
the original budget.
|
|
25
|
+
|
|
26
|
+
## Human control
|
|
27
|
+
|
|
28
|
+
Native human transcript events control the existing task; CLI actor/text fields,
|
|
29
|
+
assistant messages and injected instructions cannot authorize a scope change.
|
|
30
|
+
Codex supports current `response_item` human messages and native async answers,
|
|
31
|
+
as well as legacy `event_msg` transcripts.
|
|
32
|
+
|
|
33
|
+
- `只診斷` / `diagnosis only`: investigate without product repairs. Non-success
|
|
34
|
+
Stop requires a confirmed hypothesis supported by a current sealed baseline
|
|
35
|
+
assertion FAIL for the original regression, or an existing runtime blocker/limit.
|
|
36
|
+
- `暫停` / `pause`: save progress and stop immediately. Native Codex `Interrupt`
|
|
37
|
+
also pauses, with a three-second hook timeout; it never blocks or restarts an interrupt.
|
|
38
|
+
- `繼續` / `continue`: resume the same task and scope. An exhausted time budget
|
|
39
|
+
still needs the existing native time grant; other blockers remain authoritative.
|
|
40
|
+
- `繼續修復` / `resume repair`: restore the full repair loop on the existing task.
|
|
41
|
+
|
|
42
|
+
Pauses retain evidence, consumed time and rounds, exclude waiting time, and do not
|
|
43
|
+
reset budgets. Diagnosis and pause cannot complete a repair task. Ambiguous free
|
|
44
|
+
text is not inferred as permission; use an explicit command above.
|
|
45
|
+
|
|
46
|
+
Use `task debug --task <id> --debug-action <action>`:
|
|
47
|
+
|
|
48
|
+
- `status` displays root-wide hypotheses, receipts and remaining state.
|
|
49
|
+
- `investigate --command-id <id>` runs a trusted configured verifier. Actual
|
|
50
|
+
subprocess outcomes/time are recorded; reproduction-only work is not a fix round.
|
|
51
|
+
- `hypothesis --payload-file <json>` records `id`, `statement`, `method`,
|
|
52
|
+
`expected`, `checkId`, `expectedStatus` (`PASS` or `FAIL`). Add `reference` to
|
|
53
|
+
resolve it from actual sealed acceptance evidence. `--action investigate` may
|
|
54
|
+
only run a declared unresolved behavioral red check; `--action observe` may
|
|
55
|
+
only classify the current matching runtime `UNKNOWN` receipt as inconclusive.
|
|
56
|
+
Neither action lets an agent assign a status. A rejected hypothesis needs a
|
|
57
|
+
new reference and `reason`.
|
|
58
|
+
- `strategy --payload-file <json>` records a different `from`/`to` method,
|
|
59
|
+
substantive `reason` and discriminating `reference`. Only one switch is allowed.
|
|
60
|
+
- `checkpoint --session <id>` saves the current bound source/policy/stop cause.
|
|
61
|
+
- `resume --session <id>` recovers a matching owned worktree checkpoint. Mismatches
|
|
62
|
+
require human confirmation and remain preserved; this command cannot waive them.
|
|
63
|
+
- `extend --session <native-id> --event-id latest` reads the current native human
|
|
64
|
+
continue event. It grants exactly twenty additional minutes once, only after
|
|
65
|
+
time exhaustion and without waiving any other blocker. No actor/text flag can
|
|
66
|
+
authorize a grant. Missing native transcripts fail closed as unsupported.
|
|
67
|
+
|
|
68
|
+
Limits belong to the original root across revisions, children and sessions.
|
|
69
|
+
Repeating an active issue's text from a new session reattaches that root (or
|
|
70
|
+
resumes the worktree holding it) instead of starting a fresh budget. A
|
|
71
|
+
resolved hypothesis cannot return under a new id with the same statement and
|
|
72
|
+
method; reopen the original with new discriminating evidence.
|
|
73
|
+
Controlled investigation, verification and acquired review execution consume
|
|
74
|
+
active time; observable review-slot waiting is excluded. Claude, Codex, and agy
|
|
75
|
+
use their native session lifecycle to account foreground reasoning, tool use,
|
|
76
|
+
and edits. Accepted idle Stop closes the foreground clock. Rejected Codex Stop
|
|
77
|
+
keeps the clock running, including repeated Stop (`stop_hook_active: true`). OpenCode has no supported
|
|
78
|
+
foreground lifecycle and remains unsupported. Time between stopped turns is
|
|
79
|
+
paused. An interrupted unsealed operation remains blocked until its persisted process
|
|
80
|
+
identity proves death; only then is an UNKNOWN environment receipt sealed and
|
|
81
|
+
the elapsed interval retained.
|
|
82
|
+
|
|
83
|
+
Only changed product candidates followed by verification count toward repair
|
|
84
|
+
rounds. Test/fixture edits do not. New paired original-regression outcomes or
|
|
85
|
+
new discriminating hypothesis outcomes count as progress; renaming a task or
|
|
86
|
+
hypothesis does not. Two stagnant rounds require the strategy switch; two more
|
|
87
|
+
stop, subject to the global round/time ceilings.
|
|
88
|
+
|
|
89
|
+
Success requires baseline assertion red and candidate green for the original
|
|
90
|
+
regression, required related checks, a supported confirmed hypothesis, and two
|
|
91
|
+
fresh independent reviews bound to the current source, contract, config and
|
|
92
|
+
meaningful DEBUG proof. Missing IDs, setup failure, skip, timeout and UNKNOWN
|
|
93
|
+
never prove reproduction. Proof changes invalidate review. Completed tasks and
|
|
94
|
+
worktree finish retain the existing attestation/integration requirements.
|
|
95
|
+
|
|
96
|
+
On Claude, agy and native Codex, Stop checks an active DEBUG issue even without Git net diff,
|
|
97
|
+
with or without the completion gate. Without the gate this check alone runs,
|
|
98
|
+
with one targeted task/config reload on a gate fault. A second failure emits an
|
|
99
|
+
explicit `UNKNOWN` / incomplete diagnostic with `agent-ops doctor` recovery,
|
|
100
|
+
permits non-success Stop and preserves the task, evidence and budgets. It never
|
|
101
|
+
attests completion or retries indefinitely.
|
|
102
|
+
A valid runtime stop checkpoint permits non-success Stop while retaining the
|
|
103
|
+
active task. `allow-stop` cannot waive DEBUG. Gates are never automatically
|
|
104
|
+
enabled for unsupported hosts: the generic Codex completion gate remains
|
|
105
|
+
unsupported, while its DEBUG-specific Stop uses native `decision: "block"`
|
|
106
|
+
with a continuation reason. `continue: false` would stop the harness and is not
|
|
107
|
+
used to continue DEBUG. OpenCode Stop is unsupported. This feature
|
|
108
|
+
persists foreground progress, and does not promise
|
|
109
|
+
execution after the native conversation closes. Native transcripts are trusted
|
|
110
|
+
host evidence, not protection against an attacker with the same filesystem
|
|
111
|
+
authority.
|
|
@@ -118,7 +118,20 @@ installed configuration; the managed OpenCode `tool.execute.before` plugin can
|
|
|
118
118
|
throw its documented denial or unavailable-runtime error for its supported Bash
|
|
119
119
|
surface. Codex remains `unknown` and never emits a denial. Fixture tests assert
|
|
120
120
|
these wire and plugin shapes only; they do not prove that a host honors a
|
|
121
|
-
denial.
|
|
121
|
+
denial. `SessionStart` and ordinary `Stop` failures remain fail-open. Active
|
|
122
|
+
DEBUG Stop faults instead retry one targeted reload, then explicitly report
|
|
123
|
+
UNKNOWN/incomplete and preserve the original state before non-success Stop.
|
|
124
|
+
|
|
125
|
+
Codex DEBUG lifecycle semantics were rechecked on 2026-10-11 against the
|
|
126
|
+
[official hooks reference](https://learn.chatgpt.com/docs/hooks#stop): Stop
|
|
127
|
+
`decision: "block"` with `reason` schedules a continuation; `continue: false`
|
|
128
|
+
halts it. `stop_hook_active` identifies a prior continuation and cannot waive
|
|
129
|
+
the DEBUG check. Native `Interrupt` pauses with a three-second timeout and
|
|
130
|
+
never blocks or restarts the human interruption. This DEBUG-specific policy
|
|
131
|
+
does not enable Codex's generic completion gate or optional Stop verifier.
|
|
132
|
+
Codex separately trusts each exact native hook definition. After managed hook
|
|
133
|
+
updates, review new or changed definitions in `/hooks`; agent-ops repository
|
|
134
|
+
trust alone does not make an unreviewed native hook active.
|
|
122
135
|
|
|
123
136
|
The agy adapter uses native project `GEMINI.md` routing to
|
|
124
137
|
`.agent-ops/GEMINI.md`;
|
|
@@ -130,7 +143,7 @@ unproven final changed conversation. Read-only conversations stop normally.
|
|
|
130
143
|
On Windows the generated command is invoked through `cmd /c`.
|
|
131
144
|
|
|
132
145
|
Stop verification is explicit, trusted, report-only, and disabled by default.
|
|
133
|
-
Every Stop result continues the native harness and may carry only bounded
|
|
146
|
+
Every optional Stop verification result continues the native harness and may carry only bounded
|
|
134
147
|
command evidence; it is never task-completion evidence.
|
|
135
148
|
|
|
136
149
|
The `loop` profile is separate from the ordinary capability matrix above. It
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Task DEBUG 調查
|
|
2
|
+
|
|
3
|
+
`init` / `update` 預設加入 `features.debug`:啟用、60 分鐘、5 輪。
|
|
4
|
+
之後保留明確關閉與自訂設定;舊設定和一般 task 保持相容。
|
|
5
|
+
|
|
6
|
+
Claude / Codex prompt hook 會保守辨識調查與修復問題,保留原始 `goal`,
|
|
7
|
+
建立既有隔離 task。說明、功能與重構請求照原流程。
|
|
8
|
+
缺少可靠 session ID,或 agy 沒有 prompt 事件時,使用原生 Agent 執行
|
|
9
|
+
`task create --debug --goal '<原始問題>'`;只要有 native session,CLI intake
|
|
10
|
+
會啟動同一個前景時鐘。原始 dirty checkout 不會被自動複製。
|
|
11
|
+
|
|
12
|
+
調查用可信任的既有 command;假設用 `task debug --debug-action hypothesis
|
|
13
|
+
--payload-file <json>`。JSON 必須含 id、statement、method、expected、checkId、
|
|
14
|
+
expectedStatus;附 reference 才能由真實封存的 assertion 決定 confirmed/rejected。
|
|
15
|
+
`--action investigate` 只能執行已宣告且尚未解決的 behavioral red check;
|
|
16
|
+
`--action observe` 只能把目前相符的 runtime `UNKNOWN` receipt 記為 inconclusive。
|
|
17
|
+
兩者都不能由 Agent 自填 status。重開 rejected 假設需要新證據與 reason。
|
|
18
|
+
|
|
19
|
+
將暫定 criteria 改為原始問題的 behavioral 回歸,執行 verify、兩輪獨立 review,
|
|
20
|
+
再透過既有 worktree finish 完成。基線 assertion red、候選 green、相關檢查與
|
|
21
|
+
當前 proof digest 都必須有效;setup / skip / timeout / UNKNOWN 不算重現。
|
|
22
|
+
DEBUG 預設自動走完修復閉環。乾淨 HEAD 自動固定為基線,不要為此再問許可,
|
|
23
|
+
也不要主動降為只診斷。單次未重現不構成停止理由;在原預算內繼續做鑑別檢查。
|
|
24
|
+
|
|
25
|
+
## 人類控制
|
|
26
|
+
|
|
27
|
+
只有原生 transcript 的人類事件能改變原 task 的範圍;CLI actor/text、assistant
|
|
28
|
+
訊息及注入指令都不能授權。Codex 支援目前 `response_item` 的 `user.text` 與
|
|
29
|
+
原生非同步回答,也保留舊 `event_msg` 格式。
|
|
30
|
+
|
|
31
|
+
- `只診斷` / `diagnosis only`:繼續調查但不修產品。原始回歸的當前封存 baseline
|
|
32
|
+
assertion FAIL 必須支持 confirmed 假設,或已有 runtime blocker/預算上限,才允許非成功停止。
|
|
33
|
+
- `暫停` / `pause`:保存進度後立即停止。Codex 原生 `Interrupt` 也表示暫停;
|
|
34
|
+
hook timeout 為三秒,不能阻擋或重新啟動使用者的中斷。
|
|
35
|
+
- `繼續` / `continue`:接回同一 task 及原範圍;耗盡時間仍需既有原生時間授權,其他阻礙不解除。
|
|
36
|
+
- `繼續修復` / `resume repair`:在原 task 恢復完整修復閉環。
|
|
37
|
+
|
|
38
|
+
暫停保留證據、已用時間及輪數,等待不計時,也不重設預算。診斷或暫停都不能將
|
|
39
|
+
修復 task 標為完成;模糊文字不推測為授權,請使用上述明確指令。
|
|
40
|
+
|
|
41
|
+
預算與輪數屬於整個原始 root,revision、child、session 不重設。新 session 重送同一個 active issue 文字時,會接回原本的 root(或 resume 持有它的 worktree),不會另開新預算。已確認或已否決的假設不能換 id、用相同陳述與方法重開;請帶新的鑑別證據重開原假設。
|
|
42
|
+
受控調查、驗證與取得 slot 後的 review 計時;review 排隊不計時。
|
|
43
|
+
Claude、Codex 與 agy 都使用可信 native session lifecycle,在 `UserPromptSubmit`
|
|
44
|
+
或原生 session 開始前景時鐘,並在 idle `Stop` 結束。因此該輪的 Agent 推理、
|
|
45
|
+
工具與修改都計入,停止回合之間的使用者等待不計入。Codex 被拒絕的 Stop 仍繼續
|
|
46
|
+
計時,重複 Stop 的 `stop_hook_active: true` 不會繞過 DEBUG gate。opencode 沒有支援
|
|
47
|
+
的前景 lifecycle,不可宣稱完整的 active-time 預算。
|
|
48
|
+
中斷且沒有封存結果的操作,必須等持久化 process identity 證實程序已死亡後,
|
|
49
|
+
才封存 UNKNOWN environment receipt;這段經過時間仍會保留,不會退還不明時間。
|
|
50
|
+
測試材料修改不算修復輪;兩輪停滯需要一次實質策略切換,再兩輪停滯停止。
|
|
51
|
+
|
|
52
|
+
checkpoint 保存 task/session/source/contract/config/cause;resume 只接受同一
|
|
53
|
+
擁有者 worktree 的匹配 checkpoint,保留 dirty 內容及預算。失配需要人類確認。
|
|
54
|
+
時間耗盡後明確「繼續」可透過 `extend --event-id latest` 讀取原生人類事件,
|
|
55
|
+
一次增加 20 分鐘;沒有可信事件来源則回報 unsupported,其他阻礙不被解除。
|
|
56
|
+
|
|
57
|
+
Claude / agy / 原生 Codex 的 Stop 都會檢查無 Git 差異的 active DEBUG task,不論是否安裝 completion gate。
|
|
58
|
+
DEBUG gate 故障先針對 task/config 重新載入一次;仍失敗則明確回報 `UNKNOWN/未完成`
|
|
59
|
+
與 `agent-ops doctor` 恢復方法,允許非成功停止並保留 task、證據及預算,不無限重試。
|
|
60
|
+
有效停止 checkpoint 只允許非成功停止,task 仍 active;allow-stop 不能繞過。
|
|
61
|
+
Codex 的一般 completion gate 仍 unsupported;DEBUG 專用 Stop 以原生
|
|
62
|
+
`decision: "block"` 與 reason 續跑,不能使用會停止 harness 的 `continue: false`。
|
|
63
|
+
已接受的 Stop 結束前景時鐘;opencode Stop unsupported。
|
|
64
|
+
只承諾前景對話與持久化進度,不承諾對話關閉後繼續,也不宣稱抵擋同檔案權限攻擊。
|
|
65
|
+
|
|
66
|
+
完整命令及細節見英文 [Task DEBUG](../../en/guides/task-debug.md)。
|
|
@@ -101,7 +101,16 @@ Runtime-failure 處理中,只有 `command-policy` 為 fail-closed。當已安
|
|
|
101
101
|
OpenCode `tool.execute.before` plugin 可在其支援的 Bash surface 上 throw 文件化的
|
|
102
102
|
denial 或 unavailable-runtime error。Codex 維持 `unknown` 且絕不輸出 denial。
|
|
103
103
|
Fixture test 只斷言這些 wire 與 plugin shape;它們不證明 host 會實際遵守 denial。
|
|
104
|
-
|
|
104
|
+
`SessionStart` 與一般 `Stop` failure 維持 fail-open。active DEBUG Stop 故障則
|
|
105
|
+
針對 task/config 重新載入一次,再明確回報 UNKNOWN/未完成並保留原狀態,才允許非成功停止。
|
|
106
|
+
|
|
107
|
+
Codex DEBUG lifecycle 語意於 2026-10-11 依[官方 hook 文件](https://learn.chatgpt.com/docs/hooks#stop)
|
|
108
|
+
重新檢查:Stop 的 `decision: "block"` 加 `reason` 會排程續跑;`continue: false`
|
|
109
|
+
會停止。`stop_hook_active` 表示已經續跑過,不能繞過 DEBUG 檢查。
|
|
110
|
+
原生 `Interrupt` 暫停採三秒 timeout,不得阻擋或重啟人類中斷。
|
|
111
|
+
此 DEBUG 專用政策不會啟用 Codex 的一般 completion gate 或 optional Stop verifier。
|
|
112
|
+
Codex 另以每個原生 hook 的精確定義保存信任。更新受管理 hook 後,必須在 `/hooks`
|
|
113
|
+
審核新增或變更的定義;agent-ops repository trust 不會啟用尚未審核的原生 hook。
|
|
105
114
|
|
|
106
115
|
agy adapter 在 project scope 使用原生 `GEMINI.md` routing 到
|
|
107
116
|
`.agent-ops/GEMINI.md`;user
|
|
@@ -112,7 +121,7 @@ changed conversation 才回傳 `decision: "continue"`;唯讀 conversation 正
|
|
|
112
121
|
結束。在 Windows 透過 `cmd /c` 呼叫產生的 command。
|
|
113
122
|
|
|
114
123
|
Stop verification 必須明確啟用、具備 trust、為 report-only 且預設 disabled。
|
|
115
|
-
每個 Stop 結果都會讓 native harness 繼續,最多攜帶有界 command evidence,永遠
|
|
124
|
+
每個 optional Stop verification 結果都會讓 native harness 繼續,最多攜帶有界 command evidence,永遠
|
|
116
125
|
不是 task-completion evidence。
|
|
117
126
|
|
|
118
127
|
`loop` profile 與上方 ordinary capability matrix 分離。它只保存有界的 local
|
package/package.json
CHANGED
|
@@ -247,9 +247,13 @@
|
|
|
247
247
|
"office": {
|
|
248
248
|
"type": "object",
|
|
249
249
|
"additionalProperties": false,
|
|
250
|
-
"required": [
|
|
250
|
+
"required": [
|
|
251
|
+
"enabled"
|
|
252
|
+
],
|
|
251
253
|
"properties": {
|
|
252
|
-
"enabled": {
|
|
254
|
+
"enabled": {
|
|
255
|
+
"type": "boolean"
|
|
256
|
+
}
|
|
253
257
|
}
|
|
254
258
|
},
|
|
255
259
|
"completionGate": {
|
|
@@ -275,6 +279,30 @@
|
|
|
275
279
|
"type": "boolean"
|
|
276
280
|
}
|
|
277
281
|
}
|
|
282
|
+
},
|
|
283
|
+
"debug": {
|
|
284
|
+
"type": "object",
|
|
285
|
+
"additionalProperties": false,
|
|
286
|
+
"required": [
|
|
287
|
+
"enabled",
|
|
288
|
+
"budgetMs",
|
|
289
|
+
"maxRounds"
|
|
290
|
+
],
|
|
291
|
+
"properties": {
|
|
292
|
+
"enabled": {
|
|
293
|
+
"type": "boolean"
|
|
294
|
+
},
|
|
295
|
+
"budgetMs": {
|
|
296
|
+
"type": "integer",
|
|
297
|
+
"minimum": 1,
|
|
298
|
+
"maximum": 86400000
|
|
299
|
+
},
|
|
300
|
+
"maxRounds": {
|
|
301
|
+
"type": "integer",
|
|
302
|
+
"minimum": 1,
|
|
303
|
+
"maximum": 100
|
|
304
|
+
}
|
|
305
|
+
}
|
|
278
306
|
}
|
|
279
307
|
}
|
|
280
308
|
},
|
package/schemas/task.schema.json
CHANGED
|
@@ -180,9 +180,13 @@
|
|
|
180
180
|
"type": "object"
|
|
181
181
|
},
|
|
182
182
|
"then": {
|
|
183
|
-
"required": [
|
|
183
|
+
"required": [
|
|
184
|
+
"reviewOnlyReason"
|
|
185
|
+
],
|
|
184
186
|
"properties": {
|
|
185
|
-
"reviewOnlyReason": {
|
|
187
|
+
"reviewOnlyReason": {
|
|
188
|
+
"type": "string"
|
|
189
|
+
},
|
|
186
190
|
"bindings": {
|
|
187
191
|
"maxItems": 0,
|
|
188
192
|
"type": "array"
|
|
@@ -276,6 +280,42 @@
|
|
|
276
280
|
"contractRevision": {
|
|
277
281
|
"type": "integer",
|
|
278
282
|
"minimum": 0
|
|
283
|
+
},
|
|
284
|
+
"debug": {
|
|
285
|
+
"type": "object",
|
|
286
|
+
"additionalProperties": false,
|
|
287
|
+
"required": [
|
|
288
|
+
"rootTaskId",
|
|
289
|
+
"facts",
|
|
290
|
+
"unknowns"
|
|
291
|
+
],
|
|
292
|
+
"properties": {
|
|
293
|
+
"rootTaskId": {
|
|
294
|
+
"type": "string",
|
|
295
|
+
"minLength": 1,
|
|
296
|
+
"maxLength": 4096
|
|
297
|
+
},
|
|
298
|
+
"facts": {
|
|
299
|
+
"type": "array",
|
|
300
|
+
"maxItems": 128,
|
|
301
|
+
"uniqueItems": true,
|
|
302
|
+
"items": {
|
|
303
|
+
"type": "string",
|
|
304
|
+
"minLength": 1,
|
|
305
|
+
"maxLength": 4096
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
"unknowns": {
|
|
309
|
+
"type": "array",
|
|
310
|
+
"maxItems": 128,
|
|
311
|
+
"uniqueItems": true,
|
|
312
|
+
"items": {
|
|
313
|
+
"type": "string",
|
|
314
|
+
"minLength": 1,
|
|
315
|
+
"maxLength": 4096
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
279
319
|
}
|
|
280
320
|
},
|
|
281
321
|
"$defs": {
|
|
@@ -315,6 +355,26 @@
|
|
|
315
355
|
"type": "object"
|
|
316
356
|
},
|
|
317
357
|
"type": "array"
|
|
358
|
+
},
|
|
359
|
+
"debug": false
|
|
360
|
+
},
|
|
361
|
+
"type": "object"
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"if": {
|
|
366
|
+
"required": [
|
|
367
|
+
"debug"
|
|
368
|
+
],
|
|
369
|
+
"type": "object"
|
|
370
|
+
},
|
|
371
|
+
"then": {
|
|
372
|
+
"required": [
|
|
373
|
+
"goal"
|
|
374
|
+
],
|
|
375
|
+
"properties": {
|
|
376
|
+
"schemaVersion": {
|
|
377
|
+
"const": 2
|
|
318
378
|
}
|
|
319
379
|
},
|
|
320
380
|
"type": "object"
|