@kylecheng3146/agent-ops 0.7.3 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +2 -0
  2. package/dist/packages/cli/src/args.js +72 -1
  3. package/dist/packages/cli/src/bin.js +69 -10
  4. package/dist/packages/cli/src/cli.js +11 -3
  5. package/dist/packages/cli/src/codex-loop-process.js +6 -0
  6. package/dist/packages/cli/src/commands/batch.js +3 -1
  7. package/dist/packages/cli/src/commands/hook.js +4 -1
  8. package/dist/packages/cli/src/commands/review.js +9 -3
  9. package/dist/packages/cli/src/commands/task.js +44 -2
  10. package/dist/packages/cli/src/commands/update.js +18 -0
  11. package/dist/packages/cli/src/commands/verify.js +3 -1
  12. package/dist/packages/cli/src/debug-intake.js +245 -0
  13. package/dist/packages/cli/src/hook-process.js +155 -34
  14. package/dist/packages/cli/src/office-entry.js +34 -6
  15. package/dist/packages/cli/src/run-deps.js +8 -0
  16. package/dist/runtime/src/adapters/agy/config.js +3 -2
  17. package/dist/runtime/src/adapters/claude/config.js +7 -3
  18. package/dist/runtime/src/adapters/codex/config.js +10 -3
  19. package/dist/runtime/src/adapters/codex/events.js +2 -1
  20. package/dist/runtime/src/adapters/codex/input.js +20 -2
  21. package/dist/runtime/src/adapters/codex/output.js +4 -1
  22. package/dist/runtime/src/config/merge.js +1 -1
  23. package/dist/runtime/src/hooks/completion-gate.js +15 -0
  24. package/dist/runtime/src/install/harness.js +2 -0
  25. package/dist/runtime/src/install/plan.js +3 -3
  26. package/dist/runtime/src/install/profiles.js +2 -0
  27. package/dist/runtime/src/install/update.js +10 -2
  28. package/dist/runtime/src/office/activity.js +105 -0
  29. package/dist/runtime/src/office/art.js +1000 -0
  30. package/dist/runtime/src/office/collect.js +137 -10
  31. package/dist/runtime/src/office/layout.js +203 -0
  32. package/dist/runtime/src/office/page.js +883 -644
  33. package/dist/runtime/src/office/scene.js +38 -33
  34. package/dist/runtime/src/office/sessions.js +18 -1
  35. package/dist/runtime/src/office/snapshot.js +74 -1
  36. package/dist/runtime/src/parallel/finish.js +4 -2
  37. package/dist/runtime/src/parallel/manage.js +19 -2
  38. package/dist/runtime/src/review/attestation.js +4 -1
  39. package/dist/runtime/src/review/batch.js +10 -0
  40. package/dist/runtime/src/review/execute.js +10 -1
  41. package/dist/runtime/src/review/slots.js +59 -44
  42. package/dist/runtime/src/run/hosts/codex.js +1 -1
  43. package/dist/runtime/src/run/transport.js +3 -0
  44. package/dist/runtime/src/schema/validate.js +8 -2
  45. package/dist/runtime/src/security/permissions.js +3 -1
  46. package/dist/runtime/src/task/completion.js +20 -1
  47. package/dist/runtime/src/task/contract.js +1 -1
  48. package/dist/runtime/src/task/debug-intake.js +12 -0
  49. package/dist/runtime/src/task/debug-state.js +130 -0
  50. package/dist/runtime/src/task/debug-user-event.js +93 -0
  51. package/dist/runtime/src/task/debug.js +586 -0
  52. package/dist/runtime/src/task/service.js +46 -0
  53. package/dist/runtime/src/task/store.js +8 -1
  54. package/dist/runtime/src/verify/acceptance-replay.js +9 -4
  55. package/dist/runtime/src/verify/service.js +21 -2
  56. package/docs/en/guides/configuration.md +7 -0
  57. package/docs/en/guides/task-debug.md +111 -0
  58. package/docs/en/spec/harness-adapters.md +31 -4
  59. package/docs/zh-TW/guides/configuration.md +6 -0
  60. package/docs/zh-TW/guides/task-debug.md +66 -0
  61. package/docs/zh-TW/spec/harness-adapters.md +23 -4
  62. package/package.json +1 -1
  63. package/schemas/config.schema.json +30 -2
  64. package/schemas/manifest.schema.json +1 -0
  65. package/schemas/task.schema.json +62 -2
@@ -1,3 +1,4 @@
1
+ import { validDebugSettings, validDebugIntake } from "../task/debug-state.js";
1
2
  import { CONFIG_SCHEMA_VERSION, EVIDENCE_SCHEMA_VERSION, MANIFEST_SCHEMA_VERSION, TASK_SCHEMA_VERSION } from "../contracts.js";
2
3
  import { acceptanceError, runnerError, commitIdentity } from "./acceptance.js";
3
4
  const ID_PATTERN = /^[a-z][a-z0-9-]{0,127}$/;
@@ -29,6 +30,7 @@ const HOOK_EVENT_VALUES = new Set([
29
30
  "SubagentStart",
30
31
  "SubagentStop",
31
32
  "Stop",
33
+ "Interrupt",
32
34
  "SessionEnd"
33
35
  ]);
34
36
  const MAX_TIMEOUT_MS = 2_147_483_647;
@@ -331,10 +333,12 @@ export function validateConfig(value) {
331
333
  if (!isRecord(root.features)) {
332
334
  return failure("INVALID_TYPE", "$.features", "features must be an object.");
333
335
  }
334
- const featuresUnknown = unknownFieldFailure(root.features, ["completionGate", "stopVerification", "office"], "$.features");
336
+ const featuresUnknown = unknownFieldFailure(root.features, ["completionGate", "stopVerification", "office", "debug"], "$.features");
335
337
  if (featuresUnknown !== undefined) {
336
338
  return featuresUnknown;
337
339
  }
340
+ if (root.features.debug !== undefined && !validDebugSettings(root.features.debug))
341
+ return failure("INVALID_FEATURE", "$.features.debug", "Debug requires enabled, bounded budgetMs and maxRounds.");
338
342
  if (root.features.office !== undefined) {
339
343
  if (!isRecord(root.features.office)) {
340
344
  return failure("INVALID_TYPE", "$.features.office", "office must be an object.");
@@ -566,10 +570,12 @@ export function validateCriterion(value, path) {
566
570
  return success(value);
567
571
  }
568
572
  export function validateTask(value) {
569
- const root = validateRoot(value, ["criteria", "id", "intent", "goal", "contractRevision", "parentTaskId", "schemaVersion", "title"], isRecord(value) && value.schemaVersion === 1 ? 1 : TASK_SCHEMA_VERSION);
573
+ const root = validateRoot(value, ["criteria", "id", "intent", "goal", "contractRevision", "parentTaskId", "schemaVersion", "title", "debug"], isRecord(value) && value.schemaVersion === 1 ? 1 : TASK_SCHEMA_VERSION);
570
574
  if (isFailure(root)) {
571
575
  return root;
572
576
  }
577
+ if (root.debug !== undefined && (!validDebugIntake(root.debug) || root.schemaVersion !== TASK_SCHEMA_VERSION || root.goal === undefined))
578
+ return failure("INVALID_DEBUG_TASK", "$.debug", "Debug tasks require immutable goal and bounded root intake.");
573
579
  if (root.contractRevision !== undefined && (!Number.isSafeInteger(root.contractRevision) || root.contractRevision < 0))
574
580
  return failure("INVALID_CONTRACT_REVISION", "$.contractRevision", "Invalid contract revision.");
575
581
  if (root.goal !== undefined && (!isNonEmptyString(root.goal) || Buffer.byteLength(root.goal, "utf8") > 65536))
@@ -232,7 +232,9 @@ async function readPosixProcessIdentity(processId) {
232
232
  return null;
233
233
  }
234
234
  }
235
- async function readWindowsProcessIdentity(processId) {
235
+ export async function readWindowsProcessIdentity(processId) {
236
+ if (!Number.isSafeInteger(processId) || processId < 1)
237
+ return null;
236
238
  try {
237
239
  const script = [
238
240
  `$process = Get-Process -Id ${processId} -ErrorAction Stop`,
@@ -3,12 +3,31 @@ import { findReviewAttestation } from "../review/attestation.js";
3
3
  import { validateEvidence, validateTaskAgainstConfig } from "../schema/validate.js";
4
4
  import { isPassingVerificationEvidence } from "../verify/evidence.js";
5
5
  import { taskContractHash } from "./contract.js";
6
- import { acceptanceCoverage } from "../verify/acceptance-coverage.js";
6
+ import { acceptanceCoverage, coverageDigest } from "../verify/acceptance-coverage.js";
7
+ import { loadDebugProof, debugProofDigest, debugRemainingMs } from "./debug-state.js";
7
8
  export async function checkTaskCompletionEvidence(stored, options) {
9
+ const debug = stored.task.debug === undefined ? undefined : await loadDebugProof(options.root, stored.task.debug.rootTaskId);
10
+ if (debug?.control?.paused === true || debug?.control?.mode === "diagnose")
11
+ return { status: "FAIL", code: "TASK_DEBUG_SCOPE_RESTRICTED", remedy: "The human restricted this issue to pause or diagnosis. Preserve the active task; only a native human resume repair event authorizes repair completion." };
8
12
  const problem = await checkTaskVerificationEvidence(stored, options);
9
13
  if (problem !== null)
10
14
  return problem;
11
15
  const attestation = await findReviewAttestation(options.root, options.sourceFingerprint, stored.task.id);
16
+ if (debug !== undefined) {
17
+ const coverage = await acceptanceCoverage(stored, options.config, options.sourceFingerprint, options.evidenceStore);
18
+ const { FileTaskStore } = await import("./store.js");
19
+ const { join } = await import("node:path");
20
+ const reviewedRecords = (await new FileTaskStore(join(options.root, ".agent-ops/tasks/state.json"), options.root).read()).tasks.filter(r => attestation?.taskContracts?.[r.task.id] !== undefined);
21
+ const currentCoverage = [];
22
+ for (const record of reviewedRecords) {
23
+ const rows = await acceptanceCoverage(record, options.config, options.sourceFingerprint, options.evidenceStore);
24
+ currentCoverage.push(...rows.map(row => ({ ...row, criterionId: attestation?.tree === undefined ? row.criterionId : record.task.id + ":" + row.criterionId })));
25
+ }
26
+ const refs = new Set(coverage.filter(c => c.mode === "behavioral" && c.status === "proven").flatMap(c => c.evidenceReferences));
27
+ if (attestation?.coverageDigest !== coverageDigest(currentCoverage) || debug.operation !== null || debugRemainingMs(debug) === 0 || !debug.hypotheses.some(h => h.status === "confirmed" && h.supporting.some(ref => refs.has(ref))) ||
28
+ attestation?.debugProofs?.[stored.task.id] !== debugProofDigest(debug))
29
+ return { status: "FAIL", code: "TASK_DEBUG_PROOF_REQUIRED", remedy: "Confirm the original cause with current discriminating regression proof, then obtain two fresh independent reviews bound to that debug proof." };
30
+ }
12
31
  if (attestation === null || attestation.taskId !== stored.task.id ||
13
32
  ((stored.task.criteria.some(c => c.acceptance !== undefined) || (stored.task.contractRevision ?? 0) > 0 || stored.task.goal !== undefined) &&
14
33
  (attestation.schemaVersion !== 3 || attestation.taskContracts?.[stored.task.id] !== taskContractHash(stored.task))) ||
@@ -9,7 +9,7 @@ export function criterionContractHash(criterion) {
9
9
  export function taskContractHash(task) {
10
10
  return sha256(canonicalJson({ goalHash: goalHash(task), title: task.title,
11
11
  intent: task.intent ?? null, revision: task.contractRevision ?? 0,
12
- criteria: task.criteria.map(criterionContractHash) }));
12
+ criteria: task.criteria.map(criterionContractHash), ...(task.debug === undefined ? {} : { debug: task.debug }) }));
13
13
  }
14
14
  export function treeContractHash(tasks) {
15
15
  return sha256(canonicalJson(tasks.map(task => ({ id: task.id, hash: taskContractHash(task) }))
@@ -0,0 +1,12 @@
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
+ const value = prompt.normalize("NFKC").toLowerCase();
6
+ if (/\b(?:explain|describe|what is|how does|refactor|feature|implement|design)\b|說明|解释|解釋|重構|新增功能|設計|實作功能/u.test(value))
7
+ return false;
8
+ return /\b(?:bug|error|fail(?:s|ed|ure)?|exception|crash|regression|500)\b|錯誤|错误|失敗|失败|異常|异常|問題|问题|回傳\s*500/u.test(value) &&
9
+ /\b(?:fix|debug|investigate|reproduce|diagnose|resolve|repair)\b|修復|修复|調查|调查|重現|重现|診斷|诊断|查明|找出原因|解決|解决/u.test(value);
10
+ }
11
+ /** One issue or cause in different spacing or case is still the same one. */
12
+ export const sameDebugText = (left, right) => left.normalize("NFKC").trim().replace(/\s+/gu, " ").toLowerCase() === right.normalize("NFKC").trim().replace(/\s+/gu, " ").toLowerCase();
@@ -0,0 +1,130 @@
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"]) || !text(g.eventId) || !text(g.sessionId) || !hash(g.sourceDigest) || g.grantedMs !== DEBUG_EXTENSION_MS)
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 debugRemainingMs(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 Math.max(0, state.budgetMs + state.grants.reduce((n, g) => n + g.grantedMs, 0) - state.consumedMs - active);
86
+ }
87
+ export function debugStopCause(state, now = Date.now()) {
88
+ if (state.control?.paused === true)
89
+ return "user-pause";
90
+ const latest = state.receipts.at(-1);
91
+ if (latest?.cause !== null && latest?.cause !== undefined)
92
+ return latest.cause;
93
+ if (debugRemainingMs(state, now) === 0)
94
+ return "budget";
95
+ if (debugRounds(state) >= state.maxRounds)
96
+ return "iterations";
97
+ const rounds = state.receipts.filter(r => r.round);
98
+ const since = rounds.slice(state.strategy?.afterRound ?? 0);
99
+ let stagnant = 0;
100
+ for (const round of [...since].reverse()) {
101
+ if (round.progress.length > 0)
102
+ break;
103
+ stagnant++;
104
+ }
105
+ return stagnant >= 2 ? state.strategy === null ? "strategy-required" : "stagnation" : null;
106
+ }
107
+ /** Only reviewed reasoning/proof belongs in the digest; operations and clocks do not. */
108
+ export function debugProofDigest(state) {
109
+ return sha256(canonicalJson({ rootTaskId: state.rootTaskId, baselineCommit: state.baselineCommit, regressionChecks: state.regressionChecks, hypotheses: state.hypotheses,
110
+ strategy: state.strategy, proof: state.receipts.filter(r => r.kind === "verify").map(r => ({ taskId: r.taskId, contractHash: r.contractHash,
111
+ configHash: r.configHash, sourceFingerprint: r.sourceFingerprint, productDigest: r.productDigest, references: r.references, outcome: r.outcome })) }));
112
+ }
113
+ export function assertDebugState(value) {
114
+ if (!validDebugState(value))
115
+ throw new AgentOpsError("TASK_DEBUG_STATE_INVALID", "Debug state must be strict, bounded and root-bound.");
116
+ }
117
+ export async function loadDebugProof(root, rootTaskId) {
118
+ const { FileTaskStore } = await import("./store.js");
119
+ const { join } = await import("node:path");
120
+ const record = (await new FileTaskStore(join(root, ".agent-ops/tasks/state.json"), root).read()).tasks.find(r => r.task.id === rootTaskId);
121
+ if (record?.debug === undefined || record.task.debug?.rootTaskId !== rootTaskId)
122
+ throw new AgentOpsError("TASK_DEBUG_ROOT_MISMATCH", "Original issue proof is unavailable; do not reset its limits.");
123
+ if (record.debug.ownerRoot !== root) {
124
+ 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);
125
+ if (owned?.debug === undefined || owned.debug.ownerRoot !== record.debug.ownerRoot || owned.task.goal !== record.task.goal)
126
+ throw new AgentOpsError("TASK_DEBUG_ROOT_MISMATCH", "Original owned issue state is unavailable; do not recreate its budget.");
127
+ return owned.debug;
128
+ }
129
+ return record.debug;
130
+ }
@@ -0,0 +1,93 @@
1
+ import { lstat, readFile, readdir, realpath } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { isAbsolute, join, relative, sep } from "node:path";
4
+ import { sha256 } from "../fs/hash.js";
5
+ import { AgentOpsError } from "../fs/paths.js";
6
+ export function debugUserControl(text) {
7
+ const value = text.trim();
8
+ if (/^(?:暫停|暂停|先暫停|先暂停|停止|stop|pause)[。.!!]?$/iu.test(value))
9
+ return "pause";
10
+ if (/^(?:先)?只診斷(?:,等我補失敗時證據)?[。.!!]?$/u.test(value) || /^(?:先)?只诊断[。.!!]?$/u.test(value) || /^diagnos(?:e|is)[ -]only[.!]?$/iu.test(value))
11
+ return "diagnose";
12
+ if (/^(?:繼續|继续|continue|resume)[。.!!]?$/iu.test(value))
13
+ return "resume";
14
+ if (/^(?:繼續修復|继续修复|開始修復|开始修复|resume repair)[。.!!]?$/iu.test(value))
15
+ return "repair";
16
+ return null;
17
+ }
18
+ function codexUserText(row) {
19
+ if (row?.type === "event_msg" && row.payload?.type === "user_message")
20
+ return row.payload.message;
21
+ const message = row?.payload;
22
+ if (row?.type !== "response_item" || message?.type !== "message" || message.role !== "user")
23
+ return undefined;
24
+ const kinds = message.internal_chat_message_metadata_passthrough?.content_item_kinds;
25
+ if (!Array.isArray(kinds) || kinds.length === 0 || !kinds.every(k => k === "user.text") || !Array.isArray(message.content) ||
26
+ message.content.length !== kinds.length || !message.content.every((c) => c?.type === "input_text" && typeof c.text === "string"))
27
+ return undefined;
28
+ const text = message.content.map((c) => c.text).join("\n");
29
+ const reply = /^<send_user_message_question_reply>\s*([\s\S]*)\s*<\/send_user_message_question_reply>$/u.exec(text.trim());
30
+ if (reply === null)
31
+ return text;
32
+ try {
33
+ const answers = JSON.parse(reply[1]);
34
+ if (Array.isArray(answers) && answers.length === 1 && typeof answers[0]?.answer === "string")
35
+ return answers[0].answer;
36
+ }
37
+ catch { /* An ambiguous reply cannot authorize a control change. */ }
38
+ return undefined;
39
+ }
40
+ /** Host transcripts are the user-event authority; CLI actor/text fields are never accepted. */
41
+ export async function readDebugUserEvent(root, sessionId, home = homedir(), kind = "latest") {
42
+ if (!/^[a-f0-9-]{36}$/u.test(sessionId))
43
+ throw new AgentOpsError("TASK_DEBUG_GRANT_UNSUPPORTED", "This host has no identifiable native user-event source.");
44
+ const canonicalHome = await realpath(home).catch(() => home);
45
+ const paths = [join(home, ".claude/projects", root.replace(/[^a-zA-Z0-9]/gu, "-"), sessionId + ".jsonl")];
46
+ const codex = join(home, ".codex/sessions");
47
+ async function visit(path, depth) {
48
+ if (paths.length > 4096)
49
+ return;
50
+ try {
51
+ for (const entry of await readdir(path, { withFileTypes: true })) {
52
+ if (entry.isDirectory() && depth > 0)
53
+ await visit(join(path, entry.name), depth - 1);
54
+ else if (entry.isFile() && entry.name.endsWith(sessionId + ".jsonl"))
55
+ paths.push(join(path, entry.name));
56
+ }
57
+ }
58
+ catch { /* The host need not be installed. */ }
59
+ }
60
+ await visit(codex, 3);
61
+ for (const path of paths) {
62
+ try {
63
+ const stat = await lstat(path), canonical = await realpath(path);
64
+ const fromHome = relative(canonicalHome, canonical);
65
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024 * 1024 || fromHome.startsWith("..") || isAbsolute(fromHome))
66
+ continue;
67
+ const lines = (await readFile(path, "utf8")).split("\n");
68
+ const rows = lines.map(line => { try {
69
+ return JSON.parse(line);
70
+ }
71
+ catch {
72
+ return null;
73
+ } });
74
+ const codexSource = path.startsWith(codex + sep);
75
+ if (codexSource && !rows.some(row => row?.type === "session_meta" && row.payload?.id === sessionId && row.payload?.cwd === root))
76
+ continue;
77
+ for (let index = rows.length - 1; index >= 0; index--) {
78
+ const row = rows[index];
79
+ const text = codexSource ? codexUserText(row) :
80
+ row?.type === "user" && row.sessionId === sessionId && row.cwd === root && row.message?.role === "user" ? row.message.content : undefined;
81
+ if (typeof text !== "string" || text.length > 64 * 1024 || (kind === "control" && debugUserControl(text) === null))
82
+ continue;
83
+ const timestamp = Date.parse(row.timestamp);
84
+ if (!Number.isFinite(timestamp))
85
+ break;
86
+ const sourceDigest = sha256(lines[index]);
87
+ return { id: sha256(relative(home, path) + ":" + index + ":" + sourceDigest), sourceDigest, text, timestamp };
88
+ }
89
+ }
90
+ catch { /* An unavailable native source cannot grant time. */ }
91
+ }
92
+ 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.");
93
+ }