@kylecheng3146/agent-ops 0.10.2 → 0.11.1

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 (56) hide show
  1. package/README.md +13 -0
  2. package/dist/packages/cli/src/args.js +18 -44
  3. package/dist/packages/cli/src/bin.js +37 -29
  4. package/dist/packages/cli/src/cli.js +16 -14
  5. package/dist/packages/cli/src/codex-loop-process.js +0 -6
  6. package/dist/packages/cli/src/commands/review.js +5 -10
  7. package/dist/packages/cli/src/commands/run.js +3 -1
  8. package/dist/packages/cli/src/commands/task.js +27 -54
  9. package/dist/packages/cli/src/commands/verify.js +7 -3
  10. package/dist/packages/cli/src/criteria-review.js +63 -0
  11. package/dist/packages/cli/src/default-base.js +26 -0
  12. package/dist/packages/cli/src/hook-process.js +19 -83
  13. package/dist/packages/cli/src/owned-run-step.js +1 -1
  14. package/dist/packages/cli/src/run-entry.js +203 -115
  15. package/dist/runtime/src/adapters/agy/config.js +2 -3
  16. package/dist/runtime/src/adapters/claude/config.js +2 -2
  17. package/dist/runtime/src/adapters/codex/config.js +2 -5
  18. package/dist/runtime/src/hooks/completion-gate.js +0 -15
  19. package/dist/runtime/src/install/harness.js +4 -4
  20. package/dist/runtime/src/install/plan.js +3 -3
  21. package/dist/runtime/src/install/profiles.js +0 -2
  22. package/dist/runtime/src/logging/local-log.js +12 -0
  23. package/dist/runtime/src/office/snapshot.js +4 -1
  24. package/dist/runtime/src/parallel/manage.js +0 -3
  25. package/dist/runtime/src/review/criteria.js +252 -0
  26. package/dist/runtime/src/review/execute.js +68 -4
  27. package/dist/runtime/src/review/invocation.js +3 -3
  28. package/dist/runtime/src/run/verification.js +2 -2
  29. package/dist/runtime/src/schema/validate.js +10 -5
  30. package/dist/runtime/src/task/completion.js +4 -42
  31. package/dist/runtime/src/task/contract.js +11 -0
  32. package/dist/runtime/src/task/service.js +34 -57
  33. package/dist/runtime/src/task/store.js +3 -7
  34. package/dist/runtime/src/verify/acceptance-coverage.js +2 -3
  35. package/dist/runtime/src/verify/acceptance-protocol.js +4 -3
  36. package/dist/runtime/src/verify/acceptance-replay.js +4 -7
  37. package/dist/runtime/src/verify/service.js +6 -23
  38. package/dist/runtime/src/verify/source-fingerprint.js +17 -4
  39. package/dist/runtime/src/verify/spawn.js +3 -2
  40. package/docs/en/guides/task-debug.md +39 -133
  41. package/docs/en/spec/acceptance-and-evidence.md +31 -23
  42. package/docs/en/spec/harness-adapters.md +9 -11
  43. package/docs/en/spec/loop-engineering.md +11 -5
  44. package/docs/en/spec/review.md +16 -0
  45. package/docs/zh-TW/guides/task-debug.md +27 -73
  46. package/docs/zh-TW/spec/acceptance-and-evidence.md +18 -18
  47. package/docs/zh-TW/spec/harness-adapters.md +7 -8
  48. package/docs/zh-TW/spec/loop-engineering.md +9 -4
  49. package/docs/zh-TW/spec/review.md +17 -1
  50. package/package.json +1 -1
  51. package/schemas/config.schema.json +1 -21
  52. package/dist/packages/cli/src/debug-intake.js +0 -245
  53. package/dist/runtime/src/task/debug-intake.js +0 -15
  54. package/dist/runtime/src/task/debug-state.js +0 -135
  55. package/dist/runtime/src/task/debug-user-event.js +0 -160
  56. package/dist/runtime/src/task/debug.js +0 -632
@@ -1,245 +0,0 @@
1
- import { join } from "node:path";
2
- import { TaskService } from "../../../runtime/src/task/service.js";
3
- import { FileTaskStore } from "../../../runtime/src/task/store.js";
4
- import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
5
- import { evaluateGuardrail } from "../../../runtime/src/guardrails/evaluate.js";
6
- import { isDebugIssue } from "../../../runtime/src/task/debug-intake.js";
7
- import { ensureSessionWorktree, readWorktreeRecord, sessionWorktreeName } from "../../../runtime/src/parallel/service.js";
8
- import { listWorktrees, resumeWorktree } from "../../../runtime/src/parallel/manage.js";
9
- import { collectChangeSurface } from "../../../runtime/src/verify/change-surface.js";
10
- import { calculateSourceFingerprint } from "../../../runtime/src/verify/source-fingerprint.js";
11
- import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
12
- import { loadEffectiveConfig } from "./context.js";
13
- import { gitRunner, worktreeDependencies } from "./parallel-deps.js";
14
- const foregroundClockSupported = (host) => host !== "opencode";
15
- function nativeSessionId(input) {
16
- const fromInput = [input.session_id, input.sessionId, input.conversationId, input.thread_id, input.threadId]
17
- .find((value) => typeof value === "string" && value.length > 0);
18
- return fromInput ?? [process.env.AGENT_OPS_SESSION_ID, process.env.CODEX_THREAD_ID]
19
- .find((value) => value !== undefined && value.length > 0);
20
- }
21
- /** Start only an already attached DEBUG issue; native hooks never create work. */
22
- export async function startDebugSession(root, sessionId, host) {
23
- if (!foregroundClockSupported(host) || sessionId.length === 0)
24
- return;
25
- const config = (await loadEffectiveConfig(root, "project")).config;
26
- const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId)));
27
- const contextRoot = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
28
- const contextConfig = contextRoot === root ? config : (await loadEffectiveConfig(contextRoot, "project")).config;
29
- const tasks = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: gitRunner(contextRoot), loadConfig: async () => contextConfig } });
30
- const attached = await tasks.status({ sessionId }).catch(() => null);
31
- if (attached?.status === "active" && attached.task.debug !== undefined)
32
- await tasks.debugService().foregroundStart(attached.task.id, sessionId);
33
- }
34
- export async function routeDebugPrompt(root, input, host = "claude") {
35
- if (typeof input !== "object" || input === null || Array.isArray(input))
36
- return null;
37
- const fields = input;
38
- if (typeof fields.prompt !== "string")
39
- return null;
40
- const continuation = /^(?:繼續|继续|continue)[。.!!]?$/iu.test(fields.prompt.trim());
41
- const debugIssue = isDebugIssue(fields.prompt);
42
- const config = (await loadEffectiveConfig(root, "project")).config;
43
- if (config.features.debug?.enabled !== true)
44
- return null;
45
- if (evaluateGuardrail({ kind: "content", content: fields.prompt, scope: root }).action === "block")
46
- return null;
47
- const session = nativeSessionId(fields);
48
- if (session === undefined) {
49
- if (!debugIssue && !continuation)
50
- return null;
51
- return "DEBUG intake unsupported: this hook has no reliable native session identity. Use task create --debug --goal with the original issue in the native conversation.";
52
- }
53
- const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(session)));
54
- const contextRoot = saved?.mainRoot === root && saved.sessionId === session ? saved.path : root;
55
- const contextConfig = contextRoot === root ? config : (await loadEffectiveConfig(contextRoot, "project")).config;
56
- const original = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: gitRunner(contextRoot), loadConfig: async () => contextConfig } });
57
- const attached = await original.status({ sessionId: session }).catch(() => null);
58
- if (attached?.status === "active" && attached.task.debug !== undefined) {
59
- await original.debugService().syncNativeControl(attached.task.id, session);
60
- const control = (await original.debugService().root(attached.task.id)).debug.control;
61
- if (control?.paused === true)
62
- return `DEBUG original issue ${attached.task.id} is paused by the human. Preserve its task, evidence and remaining budget; stop after saving progress. Only a native human continue event resumes it.`;
63
- if (continuation) {
64
- let message = `DEBUG continue preserved original issue ${attached.task.id}: native user event did not extend time; other limits remain authoritative.`;
65
- try {
66
- await original.debugService().extend(attached.task.id, session, "latest");
67
- message = `DEBUG original issue ${attached.task.id}: native human time grant applied once; other limits remain authoritative.`;
68
- }
69
- catch (error) {
70
- message = `DEBUG continue preserved original issue ${attached.task.id}: ${error instanceof Error ? error.message : "Native user approval unavailable"}`;
71
- }
72
- if (foregroundClockSupported(host)) {
73
- try {
74
- await original.debugService().foregroundStart(attached.task.id, session);
75
- }
76
- catch (error) {
77
- message += ` Foreground clock unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}`;
78
- }
79
- }
80
- else
81
- message += " Foreground clock unsupported on this host; preserve the issue and use a host with a trustworthy end-of-turn signal.";
82
- if (control?.mode === "diagnose")
83
- message += " Diagnosis-only scope remains active; continue discriminating evidence without product repairs until a proven diagnosis or runtime blocker. Only a native human resume repair event restores the full loop.";
84
- return message;
85
- }
86
- if (foregroundClockSupported(host)) {
87
- try {
88
- await original.debugService().foregroundStart(attached.task.id, session);
89
- }
90
- catch (error) {
91
- return `DEBUG original issue ${attached.task.id}: foreground clock unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}`;
92
- }
93
- }
94
- if (control?.mode === "diagnose")
95
- return `DEBUG original issue ${attached.task.id}: diagnosis only. Continue reproduction and hypothesis evidence until a supported diagnosis or a runtime blocker. Do not repair product behavior. A failure to reproduce is not a stop condition; a human resume repair event restores the full loop.`;
96
- if (!debugIssue)
97
- return null;
98
- return `Continue original DEBUG issue ${attached.task.id}; retain its existing budget, evidence and hypotheses.`;
99
- }
100
- if (!debugIssue && !continuation)
101
- return null;
102
- if (continuation) {
103
- if (attached?.status !== "active" || attached.task.debug === undefined)
104
- return null;
105
- try {
106
- await original.debugService().extend(attached.task.id, session, "latest");
107
- return `DEBUG original issue ${attached.task.id}: native human time grant applied once; other limits remain authoritative.`;
108
- }
109
- catch (error) {
110
- return `DEBUG continue preserved original issue ${attached.task.id}: ${error instanceof Error ? error.message : "Native user approval unavailable"}`;
111
- }
112
- }
113
- if (attached?.status === "active")
114
- return attached.task.debug === undefined ? null : `Continue original DEBUG issue ${attached.task.id}; retain its existing budget, evidence and hypotheses.`;
115
- // Another session's worktree may already hold this issue; reusing it keeps
116
- // its consumed budget and rounds instead of starting a fresh task.
117
- const holders = await listWorktrees(worktreeDependencies(), root).catch(() => []);
118
- for (const { record: held } of holders) {
119
- if (held.sessionId === session)
120
- continue;
121
- const heldConfig = (await loadEffectiveConfig(held.path, "project").catch(() => null))?.config;
122
- if (heldConfig === undefined)
123
- continue;
124
- const issue = await new TaskService(new FileTaskStore(join(held.path, ".agent-ops/tasks/state.json"), held.path), { completion: { root: held.path, gitRunner: gitRunner(held.path), loadConfig: async () => heldConfig } }).activeDebugRoot(fields.prompt).catch(() => null);
125
- if (issue === null)
126
- continue;
127
- try {
128
- await resumeWorktree(worktreeDependencies(), { cwd: root, name: held.name, sessionId: session });
129
- }
130
- catch (error) {
131
- return `DEBUG original issue ${issue.task.id} is still active in ${held.path}; a new task would reset its budget. ${error instanceof Error ? error.message : "Resume needs confirmation."} After the user confirms, run agent-ops worktree resume ${held.name} --session ${session}.`;
132
- }
133
- return `Continue original DEBUG issue ${issue.task.id} in ${held.path}; its budget, rounds and evidence are preserved.`;
134
- }
135
- await original.assertDebugBaseline();
136
- const record = config.worktree?.mode === "auto" && await readWorktreeRecord(root) === null ?
137
- await ensureSessionWorktree(worktreeDependencies(), { cwd: root, sessionId: session }) : null;
138
- const path = record?.path ?? root;
139
- const effective = (await loadEffectiveConfig(path, "project")).config;
140
- const tasks = new TaskService(new FileTaskStore(join(path, ".agent-ops/tasks/state.json"), path), { completion: { root: path, gitRunner: gitRunner(path), loadConfig: async () => effective } });
141
- const existing = await tasks.status({ sessionId: session }).catch(() => null);
142
- if (existing?.status === "active")
143
- return existing.task.debug === undefined ? null : `Continue original DEBUG issue ${existing.task.id}.`;
144
- const held = await tasks.activeDebugRoot(fields.prompt);
145
- if (held !== null) {
146
- await tasks.attach(session, held.task.id);
147
- let message = `Continue original DEBUG issue ${held.task.id}; its budget, rounds and evidence are preserved.`;
148
- if (foregroundClockSupported(host)) {
149
- try {
150
- await tasks.debugService().foregroundStart(held.task.id, session);
151
- }
152
- catch (error) {
153
- message += ` Foreground clock unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}`;
154
- }
155
- }
156
- return message;
157
- }
158
- const ids = effective.verification.commands.filter(c => c.required).map(c => c.id);
159
- const baseline = (await gitRunner(path).run(["rev-parse", "--verify", "HEAD"])).stdout.toString().trim();
160
- const criteria = ["issue-regression", "related-checks"].map(id => ({ id, description: id === "issue-regression" ? "Reproduce and fix the original reported issue with relevant regression evidence." : "Pass necessary related checks and two fresh independent reviews.", verifierIds: ids,
161
- acceptance: { mode: "review-only", baselineCommit: baseline, bindings: [], reviewOnlyReason: "Provisional investigation intake only. Identify and revise to executable original-issue regression before debug success; this criterion cannot prove reproduction." } }));
162
- const created = await tasks.create({ title: fields.prompt.split("\n")[0].slice(0, 160), goal: fields.prompt, intent: "Investigate and fix the original issue through authoritative task debug evidence.", debug: { facts: ["User-reported symptom is unverified."], unknowns: ["Baseline reproduction", "Root cause", "Relevant regression and related checks"] }, criteria, sessionId: session, policyConfigHash: calculateConfigHash(effective), requireSurface: true });
163
- let clockNote = "";
164
- if (foregroundClockSupported(host)) {
165
- try {
166
- await tasks.debugService().foregroundStart(created.task.id, session);
167
- }
168
- catch (error) {
169
- return `DEBUG issue ${created.task.id} created in ${path}, but its foreground clock is unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}. Preserve the issue and do not claim timed completion.`;
170
- }
171
- }
172
- else
173
- clockNote = " Native foreground timing is unsupported on this host; controlled operations remain evidence-timed and do not claim a complete active-time budget.";
174
- return `DEBUG issue ${created.task.id} created in ${path}. Preserve the original goal. Investigate, record falsifiable hypotheses, revise provisional criteria to relevant behavioral regression, add the surface audit (task revise --surface-file) before the first repair, repair, declare the origin (task debug --debug-action origin) once a hypothesis is confirmed, verify, obtain two fresh independent reviews, then finish. A clean HEAD baseline is fixed automatically; do not ask permission for it or offer diagnosis-only as a fallback. Failure to reproduce alone is not a stop condition: continue discriminating checks within the budget. Defaults: 60 active minutes / 5 material repair candidates. No user-authored debug plan is required.${clockNote}`;
175
- }
176
- export async function interruptDebugSession(root, sessionId, nativeInput) {
177
- const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId)));
178
- const path = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
179
- const tasks = new TaskService(new FileTaskStore(join(path, ".agent-ops/tasks/state.json"), path), { completion: { root: path, gitRunner: gitRunner(path), loadConfig: async () => (await loadEffectiveConfig(path, "project")).config } });
180
- const attached = await tasks.status({ sessionId }).catch(error => {
181
- if (error instanceof AgentOpsError && error.code === "TASK_SESSION_UNATTACHED")
182
- return null;
183
- throw error;
184
- });
185
- if (attached?.status === "active" && attached.task.debug !== undefined)
186
- await tasks.debugService().interrupt(attached.task.id, sessionId, nativeInput);
187
- }
188
- /** Close only the task attached to a native host's verified session. */
189
- export async function closeDebugSession(root, sessionId, host = "claude") {
190
- if (!foregroundClockSupported(host))
191
- return;
192
- if (sessionId.length === 0)
193
- return;
194
- const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId)));
195
- const contextRoot = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
196
- const tasks = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: gitRunner(contextRoot), loadConfig: async () => (await loadEffectiveConfig(contextRoot, "project")).config } });
197
- const attached = await tasks.status({ sessionId }).catch(() => null);
198
- if (attached?.status === "active" && attached.task.debug !== undefined)
199
- await tasks.debugService().foregroundStop(attached.task.id, sessionId);
200
- }
201
- /**
202
- * Without the completion gate, an active DEBUG issue still cannot end a turn
203
- * unless a current runtime stop checkpoint exists. Sessions without one pass.
204
- */
205
- export function debugStopGate(root, rootConfig) {
206
- return { handle: async (event) => {
207
- if (event.event !== "stop" || event.sessionId === undefined || event.sessionId.length === 0 ||
208
- event.terminationReason !== "model_stop" || event.fullyIdle !== true)
209
- return null;
210
- const sessionId = event.sessionId;
211
- const evaluate = async (attempt) => {
212
- const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId))).catch(() => null);
213
- const contextRoot = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
214
- const config = contextRoot === root && attempt === 0 ? rootConfig : (await loadEffectiveConfig(contextRoot, "project")).config;
215
- const runner = gitRunner(contextRoot);
216
- const tasks = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: runner, loadConfig: async () => config } });
217
- const attached = await tasks.status({ sessionId }).catch(error => {
218
- if (error instanceof AgentOpsError && error.code === "TASK_SESSION_UNATTACHED")
219
- return null;
220
- throw error;
221
- });
222
- if (attached?.status !== "active" || attached.task.debug === undefined)
223
- return null;
224
- await tasks.debugService().prepareStop(attached.task.id, sessionId);
225
- const surface = await collectChangeSurface(runner);
226
- const fingerprint = await calculateSourceFingerprint(contextRoot, { mode: "worktree", changedFiles: surface.paths }, runner);
227
- return await tasks.debugService().validCheckpoint(attached.task.id, sessionId, fingerprint)
228
- ? { action: "continue", status: "UNKNOWN", code: "COMPLETION_GATE_TASK_DEBUG_STOPPED", remedy: "Original issue remains active with a runtime-bound non-success checkpoint." }
229
- : { action: "block", status: "FAIL", code: "COMPLETION_GATE_TASK_DEBUG_ACTIVE", remedy: "Continue the original DEBUG investigation, repair, verification and review loop. Failure to reproduce and an arbitrary checkpoint do not permit stopping. Preserve the task and budgets; record a current runtime stop checkpoint only for a proven blocker or exhausted limits." };
230
- };
231
- // Reload the task/config once for a transient gate fault; never rewrite bad evidence.
232
- for (let attempt = 0; attempt < 2; attempt++) {
233
- try {
234
- return await evaluate(attempt);
235
- }
236
- catch (error) {
237
- if (attempt === 0)
238
- continue;
239
- const code = error instanceof AgentOpsError ? error.code : "IO_ERROR";
240
- return { action: "continue", status: "UNKNOWN", code: "TASK_DEBUG_GATE_UNAVAILABLE", remedy: `DEBUG remains incomplete: gate ${code} failed after one targeted reload. Preserve the original task, evidence and budgets; run agent-ops doctor and restore readable task/config evidence before resuming. No completion is attested.` };
241
- }
242
- }
243
- return null;
244
- } };
245
- }
@@ -1,15 +0,0 @@
1
- /** Conservative intake only; the existing native Agent reasons about the issue. */
2
- export function isDebugIssue(prompt) {
3
- if (prompt.length === 0 || prompt.length > 64 * 1024 || prompt.includes("\0"))
4
- return false;
5
- // Harness-injected turns (subagent hand-backs, task notifications) are not a human report.
6
- if (/^\s*<(?:agent-message|task-notification|teammate-message)\b/u.test(prompt))
7
- return false;
8
- const value = prompt.normalize("NFKC").toLowerCase();
9
- if (/\b(?:explain|describe|what is|how does|refactor|feature|implement|design)\b|說明|解释|解釋|重構|新增功能|設計|實作功能/u.test(value))
10
- return false;
11
- return /\b(?:bug|error|fail(?:s|ed|ure)?|exception|crash|regression|500)\b|錯誤|错误|失敗|失败|異常|异常|問題|问题|回傳\s*500/u.test(value) &&
12
- /\b(?:fix|debug|investigate|reproduce|diagnose|resolve|repair)\b|修復|修复|調查|调查|重現|重现|診斷|诊断|查明|找出原因|解決|解决/u.test(value);
13
- }
14
- /** One issue or cause in different spacing or case is still the same one. */
15
- export const sameDebugText = (left, right) => left.normalize("NFKC").trim().replace(/\s+/gu, " ").toLowerCase() === right.normalize("NFKC").trim().replace(/\s+/gu, " ").toLowerCase();
@@ -1,135 +0,0 @@
1
- import { canonicalJson } from "../config/hash.js";
2
- import { sha256 } from "../fs/hash.js";
3
- import { AgentOpsError } from "../fs/paths.js";
4
- export const DEBUG_DEFAULTS = { enabled: true, budgetMs: 60 * 60_000, maxRounds: 5 };
5
- export const DEBUG_EXTENSION_MS = 20 * 60_000;
6
- const object = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
7
- const text = (v) => typeof v === "string" && v.length > 0 && v.length <= 4096 && !v.includes("\0");
8
- const hash = (v) => typeof v === "string" && /^[a-f0-9]{64}$/u.test(v);
9
- const integer = (v) => Number.isSafeInteger(v) && v >= 0;
10
- const strings = (v) => Array.isArray(v) && v.length <= 128 && v.every(text) && new Set(v).size === v.length;
11
- const exact = (v, names) => object(v) && Object.keys(v).length === names.length && names.every(k => Object.hasOwn(v, k));
12
- const statuses = ["proposed", "investigating", "confirmed", "rejected", "inconclusive"];
13
- export const debugCauses = ["budget", "iterations", "strategy-required", "stagnation", "environment", "permission", "user-pause", "diagnosed"];
14
- export function validDebugSettings(v) {
15
- return exact(v, ["enabled", "budgetMs", "maxRounds"]) && typeof v.enabled === "boolean" &&
16
- integer(v.budgetMs) && v.budgetMs > 0 && v.budgetMs <= 86_400_000 &&
17
- integer(v.maxRounds) && v.maxRounds > 0 && v.maxRounds <= 100;
18
- }
19
- export function validDebugIntake(v) {
20
- return exact(v, ["rootTaskId", "facts", "unknowns"]) && text(v.rootTaskId) && strings(v.facts) && strings(v.unknowns);
21
- }
22
- export function validDebugState(v) {
23
- const baseKeys = ["schemaVersion", "rootTaskId", "ownerRoot", "baselineCommit", "budgetMs", "maxRounds", "consumedMs", "regressionChecks", "hypotheses", "receipts", "strategy", "grants", "operation", "checkpoint"];
24
- if (!object(v) || Object.keys(v).some(k => !baseKeys.includes(k) && k !== "foreground" && k !== "control") || !baseKeys.every(k => Object.hasOwn(v, k)) ||
25
- v.schemaVersion !== 1 || !text(v.rootTaskId) || !text(v.ownerRoot) || typeof v.baselineCommit !== "string" || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(v.baselineCommit) ||
26
- !validDebugSettings({ enabled: true, budgetMs: v.budgetMs, maxRounds: v.maxRounds }) || !integer(v.consumedMs) || !strings(v.regressionChecks) ||
27
- !Array.isArray(v.hypotheses) || v.hypotheses.length > 32 || !Array.isArray(v.receipts) || v.receipts.length > 256 ||
28
- !Array.isArray(v.grants) || v.grants.length > 128)
29
- return false;
30
- if (Object.hasOwn(v, "control") && (!exact(v.control, ["mode", "paused", "sessionId", "eventId", "sourceDigest", "timestamp"]) ||
31
- !["full", "diagnose"].includes(String(v.control.mode)) || typeof v.control.paused !== "boolean" || !text(v.control.sessionId) ||
32
- v.control.sessionId.length > 256 || !hash(v.control.eventId) || !hash(v.control.sourceDigest) || !integer(v.control.timestamp)))
33
- return false;
34
- for (const h of v.hypotheses) {
35
- if (!exact(h, ["id", "statement", "method", "expected", "checkId", "expectedStatus", "status", "supporting", "opposing", "actual", "history"]) ||
36
- ![h.id, h.statement, h.method, h.expected, h.checkId].every(text) || !["PASS", "FAIL"].includes(String(h.expectedStatus)) || !statuses.includes(String(h.status)) ||
37
- !strings(h.supporting) || !strings(h.opposing) || typeof h.actual !== "string" || h.actual.length > 4096 ||
38
- !Array.isArray(h.history) || h.history.length > 128 || h.history.some(entry => !exact(entry, ["status", "actual", "references", "reason"]) ||
39
- !statuses.includes(String(entry.status)) || typeof entry.actual !== "string" || entry.actual.length > 4096 || !strings(entry.references) || !text(entry.reason)))
40
- return false;
41
- }
42
- if (new Set(v.hypotheses.map(h => h.id)).size !== v.hypotheses.length)
43
- return false;
44
- for (const r of v.receipts)
45
- if (!exact(r, ["id", "taskId", "kind", "contractHash", "configHash", "sourceFingerprint", "productDigest", "durationMs", "finishedAt", "round", "progress", "references", "outcome", "cause"]) ||
46
- !text(r.id) || !text(r.taskId) || !["investigate", "verify", "review"].includes(String(r.kind)) ||
47
- ![r.contractHash, r.configHash, r.sourceFingerprint, r.productDigest].every(hash) || !integer(r.durationMs) || !integer(r.finishedAt) || typeof r.round !== "boolean" ||
48
- !strings(r.progress) || !strings(r.references) || !["PASS", "FAIL", "UNKNOWN"].includes(String(r.outcome)) || ![null, "environment", "permission"].includes(r.cause))
49
- return false;
50
- if (new Set(v.receipts.map(r => r.id)).size !== v.receipts.length || v.receipts.filter(r => r.round).length > v.maxRounds)
51
- return false;
52
- for (const g of v.grants)
53
- if (!(exact(g, ["eventId", "sessionId", "sourceDigest", "grantedMs"]) || (exact(g, ["eventId", "sessionId", "sourceDigest", "grantedMs", "spentAtGrantMs"]) && integer(g.spentAtGrantMs))) || !text(g.eventId) || !text(g.sessionId) || !hash(g.sourceDigest) || (g.grantedMs !== DEBUG_EXTENSION_MS && g.grantedMs !== null))
54
- return false;
55
- if (new Set(v.grants.map(g => g.eventId)).size !== v.grants.length)
56
- return false;
57
- if (v.strategy !== null && (!exact(v.strategy, ["from", "to", "reason", "references", "afterRound"]) ||
58
- ![v.strategy.from, v.strategy.to, v.strategy.reason].every(text) || v.strategy.from === v.strategy.to ||
59
- !strings(v.strategy.references) || !integer(v.strategy.afterRound) || v.strategy.afterRound < 2))
60
- return false;
61
- if (v.foreground !== undefined && v.foreground !== null && (!exact(v.foreground, ["sessionId", "startedAt"]) || !text(v.foreground.sessionId) || v.foreground.sessionId.length > 256 || !integer(v.foreground.startedAt)))
62
- return false;
63
- if (v.operation !== null && (!((exact(v.operation, ["id", "startedAt", "taskId", "contractHash", "configHash", "sourceFingerprint", "productDigest", "kind"])) ||
64
- exact(v.operation, ["id", "startedAt", "taskId", "contractHash", "configHash", "sourceFingerprint", "productDigest", "kind", "processId", "processIdentity"])) ||
65
- !text(v.operation.id) || !integer(v.operation.startedAt) || !text(v.operation.taskId) ||
66
- ![v.operation.contractHash, v.operation.configHash, v.operation.sourceFingerprint, v.operation.productDigest].every(hash) ||
67
- !["investigate", "verify", "review"].includes(String(v.operation.kind)) ||
68
- (Object.hasOwn(v.operation, "processId") && (!integer(v.operation.processId) || v.operation.processId < 1)) ||
69
- (Object.hasOwn(v.operation, "processIdentity") && (!text(v.operation.processIdentity) || v.operation.processIdentity.length > 256))))
70
- return false;
71
- if (v.checkpoint !== null && (!exact(v.checkpoint, ["taskId", "sessionId", "root", "sourceFingerprint", "contractHash", "configHash", "cause", "receiptId"]) ||
72
- ![v.checkpoint.taskId, v.checkpoint.sessionId, v.checkpoint.root, v.checkpoint.receiptId].every(text) ||
73
- ![v.checkpoint.sourceFingerprint, v.checkpoint.contractHash, v.checkpoint.configHash].every(hash) || (v.checkpoint.cause !== null && !debugCauses.includes(v.checkpoint.cause))))
74
- return false;
75
- return true;
76
- }
77
- export function createDebugState(rootTaskId, baselineCommit, settings = DEBUG_DEFAULTS, ownerRoot = ".") {
78
- return { schemaVersion: 1, rootTaskId, ownerRoot, baselineCommit, budgetMs: settings.budgetMs, maxRounds: settings.maxRounds, consumedMs: 0, regressionChecks: [],
79
- hypotheses: [], receipts: [], strategy: null, grants: [], foreground: null, operation: null, checkpoint: null };
80
- }
81
- export function debugRounds(state) { return state.receipts.filter(r => r.round).length; }
82
- export function debugSpentMs(state, now = Date.now()) {
83
- const starts = [state.foreground?.startedAt, state.operation?.startedAt].filter((startedAt) => startedAt !== undefined);
84
- const active = starts.length === 0 ? 0 : Math.max(0, now - Math.min(...starts));
85
- return state.consumedMs + active;
86
- }
87
- export function debugRemainingMs(state, now = Date.now()) {
88
- if (state.grants.some(g => g.grantedMs === null))
89
- return Number.POSITIVE_INFINITY;
90
- const limit = state.grants.reduce((n, g) => Math.max(n, g.spentAtGrantMs ?? n) + (g.grantedMs ?? 0), state.budgetMs);
91
- return Math.max(0, limit - debugSpentMs(state, now));
92
- }
93
- export function debugStopCause(state, now = Date.now()) {
94
- if (state.control?.paused === true)
95
- return "user-pause";
96
- if (debugRemainingMs(state, now) === 0)
97
- return "budget";
98
- if (debugRounds(state) >= state.maxRounds)
99
- return "iterations";
100
- const rounds = state.receipts.filter(r => r.round);
101
- const since = rounds.slice(state.strategy?.afterRound ?? 0);
102
- let stagnant = 0;
103
- for (const round of [...since].reverse()) {
104
- if (round.progress.length > 0)
105
- break;
106
- stagnant++;
107
- }
108
- if (stagnant >= 2)
109
- return state.strategy === null ? "strategy-required" : "stagnation";
110
- return state.receipts.at(-1)?.cause ?? null;
111
- }
112
- /** Only reviewed reasoning/proof belongs in the digest; operations and clocks do not. */
113
- export function debugProofDigest(state) {
114
- return sha256(canonicalJson({ rootTaskId: state.rootTaskId, baselineCommit: state.baselineCommit, regressionChecks: state.regressionChecks, hypotheses: state.hypotheses,
115
- strategy: state.strategy, proof: state.receipts.filter(r => r.kind === "verify").map(r => ({ taskId: r.taskId, contractHash: r.contractHash,
116
- configHash: r.configHash, sourceFingerprint: r.sourceFingerprint, productDigest: r.productDigest, references: r.references, outcome: r.outcome })) }));
117
- }
118
- export function assertDebugState(value) {
119
- if (!validDebugState(value))
120
- throw new AgentOpsError("TASK_DEBUG_STATE_INVALID", "Debug state must be strict, bounded and root-bound.");
121
- }
122
- export async function loadDebugProof(root, rootTaskId) {
123
- const { FileTaskStore } = await import("./store.js");
124
- const { join } = await import("node:path");
125
- const record = (await new FileTaskStore(join(root, ".agent-ops/tasks/state.json"), root).read()).tasks.find(r => r.task.id === rootTaskId);
126
- if (record?.debug === undefined || record.task.debug?.rootTaskId !== rootTaskId)
127
- throw new AgentOpsError("TASK_DEBUG_ROOT_MISMATCH", "Original issue proof is unavailable; do not reset its limits.");
128
- if (record.debug.ownerRoot !== root) {
129
- const owned = (await new FileTaskStore(join(record.debug.ownerRoot, ".agent-ops/tasks/state.json"), record.debug.ownerRoot).read()).tasks.find(r => r.task.id === rootTaskId);
130
- if (owned?.debug === undefined || owned.debug.ownerRoot !== record.debug.ownerRoot || owned.task.goal !== record.task.goal)
131
- throw new AgentOpsError("TASK_DEBUG_ROOT_MISMATCH", "Original owned issue state is unavailable; do not recreate its budget.");
132
- return owned.debug;
133
- }
134
- return record.debug;
135
- }
@@ -1,160 +0,0 @@
1
- import { constants } from "node:fs";
2
- import { lstat, open, readdir, realpath } from "node:fs/promises";
3
- import { homedir } from "node:os";
4
- import { isAbsolute, join, relative, sep } from "node:path";
5
- import { sha256 } from "../fs/hash.js";
6
- import { AgentOpsError } from "../fs/paths.js";
7
- export function debugUserControl(text) {
8
- const value = text.trim();
9
- if (/^(?:不要給預算時間(?:\s+直接完整驗證與review)?|取消時間限制|no time limit)[。.!!]?$/iu.test(value))
10
- return "unlimited";
11
- if (/^(?:暫停|暂停|先暫停|先暂停|停止|stop|pause)[。.!!]?$/iu.test(value))
12
- return "pause";
13
- if (/^(?:先)?只診斷(?:,等我補失敗時證據)?[。.!!]?$/u.test(value) || /^(?:先)?只诊断[。.!!]?$/u.test(value) || /^diagnos(?:e|is)[ -]only[.!]?$/iu.test(value))
14
- return "diagnose";
15
- if (/^(?:繼續|继续|continue|resume)[。.!!]?$/iu.test(value))
16
- return "resume";
17
- if (/^(?:繼續修復|继续修复|開始修復|开始修复|resume repair)[。.!!]?$/iu.test(value))
18
- return "repair";
19
- return null;
20
- }
21
- function codexUserText(row) {
22
- if (row?.type === "event_msg" && row.payload?.type === "user_message")
23
- return row.payload.message;
24
- const message = row?.payload;
25
- if (row?.type !== "response_item" || message?.type !== "message" || message.role !== "user")
26
- return undefined;
27
- const kinds = message.internal_chat_message_metadata_passthrough?.content_item_kinds;
28
- if (!Array.isArray(kinds) || kinds.length === 0 || !kinds.every(k => k === "user.text") || !Array.isArray(message.content) ||
29
- message.content.length !== kinds.length || !message.content.every((c) => c?.type === "input_text" && typeof c.text === "string"))
30
- return undefined;
31
- const text = message.content.map((c) => c.text).join("\n");
32
- const reply = /^<send_user_message_question_reply>\s*([\s\S]*)\s*<\/send_user_message_question_reply>$/u.exec(text.trim());
33
- if (reply === null)
34
- return text;
35
- try {
36
- const answers = JSON.parse(reply[1]);
37
- if (Array.isArray(answers) && answers.length === 1 && typeof answers[0]?.answer === "string")
38
- return answers[0].answer;
39
- }
40
- catch { /* An ambiguous reply cannot authorize a control change. */ }
41
- return undefined;
42
- }
43
- /** Bound each row, not the transcript: assistant/tool output must not hide later human input. */
44
- async function* nativeLines(path) {
45
- const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
46
- try {
47
- if (!(await file.stat()).isFile())
48
- return;
49
- let pending = Buffer.alloc(0), oversized = false, index = 0;
50
- const started = Date.now();
51
- for await (const chunk of file.createReadStream({ autoClose: false })) {
52
- if (Date.now() - started > 10_000)
53
- throw new Error("Native transcript read timed out");
54
- const bytes = chunk;
55
- let start = 0;
56
- while (start < bytes.length) {
57
- const end = bytes.indexOf(10, start), stop = end < 0 ? bytes.length : end;
58
- if (!oversized) {
59
- if (pending.length + stop - start > 1024 * 1024) {
60
- pending = Buffer.alloc(0);
61
- oversized = true;
62
- }
63
- else
64
- pending = Buffer.concat([pending, bytes.subarray(start, stop)]);
65
- }
66
- if (end < 0)
67
- break;
68
- if (!oversized)
69
- yield { line: pending.toString("utf8"), index };
70
- index++;
71
- pending = Buffer.alloc(0);
72
- oversized = false;
73
- start = end + 1;
74
- }
75
- }
76
- if (!oversized && pending.length > 0)
77
- yield { line: pending.toString("utf8"), index };
78
- }
79
- finally {
80
- await file.close();
81
- }
82
- }
83
- async function ownedNativeSession(root, worktreeRoot, sessionId) {
84
- if (process.env.CODEX_THREAD_ID !== sessionId || worktreeRoot === undefined)
85
- return false;
86
- const { readWorktreeRecord } = await import("../parallel/service.js");
87
- const record = await readWorktreeRecord(worktreeRoot);
88
- if (record === null || record.sessionId !== sessionId || record.agentId !== undefined || record.runId !== undefined)
89
- return false;
90
- const [main, worktree, recordMain, recordPath] = await Promise.all([root, worktreeRoot, record.mainRoot, record.path].map(path => realpath(path)));
91
- const inside = relative(join(main, ".worktrees"), worktree);
92
- return main === recordMain && worktree === recordPath && inside.length > 0 && !inside.startsWith("..") && !isAbsolute(inside);
93
- }
94
- /** Host transcripts are the user-event authority; CLI actor/text fields are never accepted. */
95
- /** `worktreeRoot` is the session's owned worktree: Claude stamps each row with the shell's cwd, which moves there after `cd`. */
96
- export async function readDebugUserEvent(root, sessionId, home = homedir(), kind = "latest", worktreeRoot) {
97
- if (!/^[a-f0-9-]{36}$/u.test(sessionId))
98
- throw new AgentOpsError("TASK_DEBUG_GRANT_UNSUPPORTED", "This host has no identifiable native user-event source.");
99
- const canonicalHome = await realpath(home).catch(() => home);
100
- const paths = [join(home, ".claude/projects", root.replace(/[^a-zA-Z0-9]/gu, "-"), sessionId + ".jsonl")];
101
- const codex = join(home, ".codex/sessions");
102
- async function visit(path, depth) {
103
- if (paths.length > 4096)
104
- return;
105
- try {
106
- for (const entry of await readdir(path, { withFileTypes: true })) {
107
- if (entry.isDirectory() && depth > 0)
108
- await visit(join(path, entry.name), depth - 1);
109
- else if (entry.isFile() && entry.name.endsWith(sessionId + ".jsonl"))
110
- paths.push(join(path, entry.name));
111
- }
112
- }
113
- catch { /* The host need not be installed. */ }
114
- }
115
- await visit(codex, 3);
116
- const owned = await ownedNativeSession(root, worktreeRoot, sessionId).catch(() => false);
117
- for (const path of paths) {
118
- try {
119
- const stat = await lstat(path), canonical = await realpath(path);
120
- const fromHome = relative(canonicalHome, canonical);
121
- const codexSource = path.startsWith(codex + sep);
122
- if (!stat.isFile() || stat.isSymbolicLink() || (!codexSource && stat.size > 16 * 1024 * 1024) || fromHome.startsWith("..") || isAbsolute(fromHome))
123
- continue;
124
- let matchedSession = false, event;
125
- for await (const { line, index } of nativeLines(path)) {
126
- let row;
127
- try {
128
- row = JSON.parse(line);
129
- }
130
- catch {
131
- continue;
132
- }
133
- if (codexSource && row?.type === "session_meta") {
134
- if (row.payload?.id !== sessionId || (row.payload?.cwd !== root && !owned)) {
135
- event = undefined;
136
- matchedSession = false;
137
- break;
138
- }
139
- matchedSession = true;
140
- }
141
- const text = codexSource ? codexUserText(row) :
142
- row?.type === "user" && row.sessionId === sessionId && (row.cwd === root || (worktreeRoot !== undefined && row.cwd === worktreeRoot)) && row.message?.role === "user" &&
143
- row.isMeta !== true && (row.origin === undefined || row.origin?.kind === "human") ? row.message.content : undefined;
144
- if (typeof text !== "string" || text.length > 64 * 1024 || (kind === "control" && debugUserControl(text) === null))
145
- continue;
146
- const timestamp = Date.parse(row.timestamp);
147
- if (!Number.isFinite(timestamp)) {
148
- event = undefined;
149
- continue;
150
- }
151
- const sourceDigest = sha256(line);
152
- event = { id: sha256(relative(home, path) + ":" + index + ":" + sourceDigest), sourceDigest, text, timestamp };
153
- }
154
- if (event !== undefined && (!codexSource || matchedSession))
155
- return event;
156
- }
157
- catch { /* An unavailable native source cannot grant time. */ }
158
- }
159
- throw new AgentOpsError("TASK_DEBUG_GRANT_UNSUPPORTED", "No current native human event can authorize this extension. Request explicit host approval; this CLI cannot self-grant it.");
160
- }