@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.
Files changed (52) hide show
  1. package/dist/packages/cli/src/args.js +56 -1
  2. package/dist/packages/cli/src/bin.js +29 -2
  3. package/dist/packages/cli/src/cli.js +9 -3
  4. package/dist/packages/cli/src/codex-loop-process.js +6 -0
  5. package/dist/packages/cli/src/commands/hook.js +4 -1
  6. package/dist/packages/cli/src/commands/review.js +9 -3
  7. package/dist/packages/cli/src/commands/task.js +44 -2
  8. package/dist/packages/cli/src/debug-intake.js +245 -0
  9. package/dist/packages/cli/src/hook-process.js +110 -37
  10. package/dist/runtime/src/adapters/agy/config.js +3 -2
  11. package/dist/runtime/src/adapters/claude/config.js +4 -3
  12. package/dist/runtime/src/adapters/codex/config.js +7 -3
  13. package/dist/runtime/src/adapters/codex/events.js +2 -1
  14. package/dist/runtime/src/adapters/codex/input.js +20 -2
  15. package/dist/runtime/src/adapters/codex/output.js +4 -1
  16. package/dist/runtime/src/config/merge.js +1 -1
  17. package/dist/runtime/src/hooks/completion-gate.js +15 -0
  18. package/dist/runtime/src/install/harness.js +2 -0
  19. package/dist/runtime/src/install/plan.js +4 -4
  20. package/dist/runtime/src/install/profiles.js +2 -0
  21. package/dist/runtime/src/office/art.js +2 -1
  22. package/dist/runtime/src/office/layout.js +6 -3
  23. package/dist/runtime/src/office/page.js +133 -9
  24. package/dist/runtime/src/office/scene.js +13 -4
  25. package/dist/runtime/src/office/snapshot.js +12 -0
  26. package/dist/runtime/src/parallel/manage.js +19 -2
  27. package/dist/runtime/src/review/attestation.js +4 -1
  28. package/dist/runtime/src/review/execute.js +1 -1
  29. package/dist/runtime/src/review/slots.js +59 -44
  30. package/dist/runtime/src/run/hosts/codex.js +1 -1
  31. package/dist/runtime/src/run/transport.js +3 -0
  32. package/dist/runtime/src/schema/validate.js +8 -2
  33. package/dist/runtime/src/security/permissions.js +3 -1
  34. package/dist/runtime/src/task/completion.js +20 -1
  35. package/dist/runtime/src/task/contract.js +1 -1
  36. package/dist/runtime/src/task/debug-intake.js +12 -0
  37. package/dist/runtime/src/task/debug-state.js +129 -0
  38. package/dist/runtime/src/task/debug-user-event.js +93 -0
  39. package/dist/runtime/src/task/debug.js +586 -0
  40. package/dist/runtime/src/task/service.js +46 -0
  41. package/dist/runtime/src/task/store.js +8 -1
  42. package/dist/runtime/src/verify/acceptance-replay.js +9 -4
  43. package/dist/runtime/src/verify/change-surface.js +4 -1
  44. package/dist/runtime/src/verify/service.js +21 -2
  45. package/docs/en/guides/task-debug.md +111 -0
  46. package/docs/en/spec/harness-adapters.md +15 -2
  47. package/docs/zh-TW/guides/task-debug.md +66 -0
  48. package/docs/zh-TW/spec/harness-adapters.md +11 -2
  49. package/package.json +1 -1
  50. package/schemas/config.schema.json +30 -2
  51. package/schemas/manifest.schema.json +1 -0
  52. 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) => await runVerificationCommand({ id: group.runner.id, command: executable, args: [...args], cwd: ".", required: true,
117
- evidence: { kind: "exit-code" }, timeoutMs }, { cwd, runner: options.processRunner, env, replaceEnv: true,
118
- outputLimitBytes: 1024 * 1024, ...(options.signal === undefined ? {} : { signal: options.signal }) });
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 result = await executeConfiguredCommand(command, {
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. Every `SessionStart` and `Stop` failure path remains fail-open.
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
- 每個 `SessionStart` 與 `Stop` failure path 都維持 fail-open。
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kylecheng3146/agent-ops",
3
- "version": "0.8.0",
3
+ "version": "0.8.4",
4
4
  "description": "Evidence-driven development loops for agy, Codex, Claude Code, and opencode",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -247,9 +247,13 @@
247
247
  "office": {
248
248
  "type": "object",
249
249
  "additionalProperties": false,
250
- "required": ["enabled"],
250
+ "required": [
251
+ "enabled"
252
+ ],
251
253
  "properties": {
252
- "enabled": { "type": "boolean" }
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
  },
@@ -145,6 +145,7 @@
145
145
  "SubagentStart",
146
146
  "SubagentStop",
147
147
  "Stop",
148
+ "Interrupt",
148
149
  "SessionEnd"
149
150
  ]
150
151
  }
@@ -180,9 +180,13 @@
180
180
  "type": "object"
181
181
  },
182
182
  "then": {
183
- "required": ["reviewOnlyReason"],
183
+ "required": [
184
+ "reviewOnlyReason"
185
+ ],
184
186
  "properties": {
185
- "reviewOnlyReason": {"type": "string"},
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"