@kylecheng3146/agent-ops 0.5.2 → 0.6.0

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 (103) hide show
  1. package/README.md +64 -0
  2. package/dist/packages/cli/src/args.js +214 -10
  3. package/dist/packages/cli/src/bin.js +77 -17
  4. package/dist/packages/cli/src/cli.js +42 -10
  5. package/dist/packages/cli/src/commands/advance.js +173 -0
  6. package/dist/packages/cli/src/commands/review-show.js +189 -0
  7. package/dist/packages/cli/src/commands/review.js +201 -34
  8. package/dist/packages/cli/src/commands/run.js +111 -0
  9. package/dist/packages/cli/src/commands/task.js +62 -17
  10. package/dist/packages/cli/src/commands/verify.js +1 -0
  11. package/dist/packages/cli/src/context.js +63 -1
  12. package/dist/packages/cli/src/hook-process.js +31 -1
  13. package/dist/packages/cli/src/owned-run-step.js +65 -0
  14. package/dist/packages/cli/src/parallel-deps.js +18 -5
  15. package/dist/packages/cli/src/run-deps.js +314 -0
  16. package/dist/packages/cli/src/run-entry.js +943 -0
  17. package/dist/packages/cli/src/run-setup-entry.js +32 -0
  18. package/dist/packages/cli/src/run-step-entry.js +58 -0
  19. package/dist/runtime/src/config/explain.js +6 -0
  20. package/dist/runtime/src/config/hash.js +1 -1
  21. package/dist/runtime/src/config/merge.js +9 -1
  22. package/dist/runtime/src/contracts.js +2 -2
  23. package/dist/runtime/src/hooks/completion-gate.js +8 -2
  24. package/dist/runtime/src/install/doctor.js +2 -0
  25. package/dist/runtime/src/install/harness.js +1 -1
  26. package/dist/runtime/src/install/preauth.js +10 -1
  27. package/dist/runtime/src/parallel/finish.js +217 -39
  28. package/dist/runtime/src/parallel/guard.js +5 -1
  29. package/dist/runtime/src/parallel/integrate.js +416 -0
  30. package/dist/runtime/src/parallel/manage.js +4 -1
  31. package/dist/runtime/src/parallel/receipt.js +153 -0
  32. package/dist/runtime/src/parallel/service.js +45 -3
  33. package/dist/runtime/src/review/attestation.js +60 -11
  34. package/dist/runtime/src/review/execute.js +24 -1
  35. package/dist/runtime/src/review/packet.js +35 -1
  36. package/dist/runtime/src/review/render.js +71 -0
  37. package/dist/runtime/src/review/runner.js +12 -1
  38. package/dist/runtime/src/review/scope.js +18 -1
  39. package/dist/runtime/src/review/slots.js +35 -10
  40. package/dist/runtime/src/run/authorization.js +60 -0
  41. package/dist/runtime/src/run/budget.js +30 -0
  42. package/dist/runtime/src/run/controls.js +139 -0
  43. package/dist/runtime/src/run/convergence.js +177 -0
  44. package/dist/runtime/src/run/hosts/claude.js +180 -0
  45. package/dist/runtime/src/run/hosts/codex.js +279 -0
  46. package/dist/runtime/src/run/hosts/index.js +4 -0
  47. package/dist/runtime/src/run/hosts/protocol.js +254 -0
  48. package/dist/runtime/src/run/hosts/types.js +1 -0
  49. package/dist/runtime/src/run/hosts/util.js +136 -0
  50. package/dist/runtime/src/run/integration.js +204 -0
  51. package/dist/runtime/src/run/macOS.js +189 -0
  52. package/dist/runtime/src/run/ownership.js +22 -0
  53. package/dist/runtime/src/run/planning.js +25 -0
  54. package/dist/runtime/src/run/policy-transition.js +1076 -0
  55. package/dist/runtime/src/run/policy.js +176 -0
  56. package/dist/runtime/src/run/ratchet.js +71 -0
  57. package/dist/runtime/src/run/scheduler.js +260 -0
  58. package/dist/runtime/src/run/service.js +425 -0
  59. package/dist/runtime/src/run/supervisor.js +635 -0
  60. package/dist/runtime/src/run/transport.js +159 -0
  61. package/dist/runtime/src/run/types.js +1 -0
  62. package/dist/runtime/src/run/usage.js +81 -0
  63. package/dist/runtime/src/run/verification.js +64 -0
  64. package/dist/runtime/src/run/worker-gate.js +48 -0
  65. package/dist/runtime/src/schema/acceptance.js +91 -0
  66. package/dist/runtime/src/schema/validate.js +72 -7
  67. package/dist/runtime/src/task/completion.js +26 -6
  68. package/dist/runtime/src/task/contract.js +17 -0
  69. package/dist/runtime/src/task/pin-finding.js +81 -0
  70. package/dist/runtime/src/task/render.js +3 -0
  71. package/dist/runtime/src/task/service.js +361 -10
  72. package/dist/runtime/src/task/store.js +57 -7
  73. package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
  74. package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
  75. package/dist/runtime/src/verify/acceptance-replay.js +239 -0
  76. package/dist/runtime/src/verify/adapters/index.js +5 -0
  77. package/dist/runtime/src/verify/adapters/jest.js +142 -0
  78. package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
  79. package/dist/runtime/src/verify/adapters/node.js +254 -0
  80. package/dist/runtime/src/verify/adapters/pytest.js +188 -0
  81. package/dist/runtime/src/verify/adapters/rust.js +84 -0
  82. package/dist/runtime/src/verify/adapters/shared.js +82 -0
  83. package/dist/runtime/src/verify/adapters/vitest.js +218 -0
  84. package/dist/runtime/src/verify/evidence.js +5 -2
  85. package/dist/runtime/src/verify/service.js +24 -8
  86. package/dist/runtime/src/verify/source-fingerprint.js +1 -1
  87. package/dist/runtime/src/verify/spawn.js +13 -2
  88. package/docs/en/guides/configuration.md +10 -0
  89. package/docs/en/spec/acceptance-and-evidence.md +58 -0
  90. package/docs/en/spec/delegation.md +4 -4
  91. package/docs/en/spec/loop-engineering.md +49 -0
  92. package/docs/en/spec/review.md +24 -2
  93. package/docs/zh-TW/guides/configuration.md +9 -0
  94. package/docs/zh-TW/spec/acceptance-and-evidence.md +31 -1
  95. package/docs/zh-TW/spec/delegation.md +4 -4
  96. package/docs/zh-TW/spec/loop-engineering.md +22 -0
  97. package/docs/zh-TW/spec/review.md +24 -2
  98. package/package.json +1 -1
  99. package/schemas/config.schema.json +221 -19
  100. package/schemas/evidence.schema.json +129 -6
  101. package/schemas/task.schema.json +268 -6
  102. package/templates/acceptance/agent_ops_acceptance.rs +36 -0
  103. package/templates/acceptance/agent_ops_pytest.py +46 -0
@@ -25,10 +25,11 @@ Commands:
25
25
  doctor Diagnose an installation
26
26
  update Update managed artifacts
27
27
  uninstall Remove managed artifacts
28
- task <create|status|attach|complete|archive|export>
28
+ task <create|status|attach|revise|replan|pin-finding|advance|complete|archive|export>
29
29
  Manage independent task acceptance state
30
30
  verify Run configured verification
31
- review Run an independent review
31
+ review Run an independent review or show recorded reports
32
+ run Start or control a supervised Claude/Codex native goal
32
33
  batch Verify and review a parent task's subtasks together
33
34
  allow-stop Grant one fingerprint-bound completion-gate Stop permit (requires --session)
34
35
  agy-run Run headless agy with a process-exit completion recheck
@@ -71,6 +72,14 @@ Options:
71
72
  * rejected.
72
73
  */
73
74
  export const COMMAND_HELP_TEXT = {
75
+ run: `Usage: agent-ops run [start] "<goal>" --host <claude|codex> [--jobs <1|2>] [--time-budget <60m>] [--wait] [--json]
76
+ agent-ops run [start] --goal-file <path> --host <claude|codex>
77
+ agent-ops run <status|logs|resume|stop> <run-id> [--json]
78
+ agent-ops run respond <run-id> --question-id <id> <--answer <text>|--answer-file <path>> [--json]
79
+
80
+ Native goal state is observational. Completion requires current verification,
81
+ two fresh reviews, task completion and a target-bound integration receipt.
82
+ `,
74
83
  init: `Usage: agent-ops init [options]
75
84
 
76
85
  Plan or install agent-ops into this repository.
@@ -141,27 +150,47 @@ Options:
141
150
  --json
142
151
  --yes
143
152
  `,
144
- task: `Usage: agent-ops task <create|status|attach|complete|archive|export> [options]
153
+ task: `Usage: agent-ops task <create|status|attach|revise|replan|pin-finding|advance|complete|archive|export> [options]
145
154
 
146
155
  Manage independent task acceptance state. Task commands accept none of
147
- --harness, --profile, --dry-run or --yes.
156
+ --harness, --profile or --dry-run. Only advance accepts --yes.
148
157
 
149
158
  Options:
150
159
  --title <text> create
160
+ --intent <text> create: intended behavior and constraints before editing
151
161
  --criterion <json> create, repeatable, two to five total
162
+ --criterion-file <path> create: one criterion object, repeatable; pin-finding: exactly one
163
+ --criteria-file <path> revise: replacement criterion array
164
+ --plan-file <path> replan: split-task array with requirement mappings
165
+ --expected-contract <hash> revise, pin-finding: current task contract from status
166
+ --expected-tree-contract <hash> replan: current tree contract from status
167
+ --reason <text> required for revise/replan; optional for pin-finding
168
+ --baseline <git-ref> create/revise: baseline for new mechanical requirements
152
169
  --parent <task-id> create: record a subtask; status: list subtasks
153
- --task <id> status, attach, complete, archive, export
154
- --session <id> attach, status
170
+ --task <id> status, attach, revise, replan, pin-finding, complete, archive, export, advance
171
+ --session <id> create, attach, status, advance
155
172
  --evidence <criterion-id=reference> complete, repeatable
156
173
  --base <git-ref> complete: a clean committed range
174
+ --yes advance: authorize the two review sessions
157
175
  --json
158
176
 
159
- Each --criterion is one JSON object with exactly these keys:
177
+ Each --criterion or --criterion-file supplies one criterion object. Legacy example:
160
178
 
161
179
  {"id":"kebab-id","description":"what must hold","verifierIds":["node-test"]}
162
180
 
163
- Every criterion needs at least one verifierIds entry naming a verification
164
- command id configured in .agent-ops/config.json. No other key is accepted.
181
+ Each legacy criterion needs at least one verifierIds entry naming a verification
182
+ command id configured in .agent-ops/config.json. Typed criteria additionally
183
+ accept an acceptance object: mode (behavioral, invariant or review-only),
184
+ bindings and optional baselineCommit/reviewOnlyReason. Mechanical bindings name
185
+ configured acceptance runner IDs, checkIds, committed materials and optional
186
+ redCheckIds; tasks cannot supply commands. Creation freezes the baseline.
187
+ Review-only example (mandatory repository policy commands still run):
188
+
189
+ {"id":"judgment","description":"Meets the original goal","verifierIds":[],"acceptance":{"mode":"review-only","bindings":[],"reviewOnlyReason":"Requires independent judgment"}}
190
+
191
+ Pin a saved finding with task pin-finding --task <id> <report-digest:index>
192
+ --expected-contract <hash> --criterion-file <path>. Revise/replan preserve the
193
+ original goal and immutable existing baselines; obtain new verify and review.
165
194
  `,
166
195
  verify: `Usage: agent-ops verify [options]
167
196
 
@@ -174,12 +203,15 @@ Options:
174
203
  --json
175
204
  `,
176
205
  review: `Usage: agent-ops review --task <id> --yes [options]
206
+ agent-ops review show --task <id> [--json]
177
207
 
178
208
  Run the complete task-bound review: one necessary reviewer followed by one
179
- fresh adversarial reviewer from the configured pair.
209
+ fresh adversarial reviewer from the configured pair. Show reads recorded
210
+ reports without running a reviewer, including from the main checkout.
180
211
 
181
212
  Options:
182
213
  --task <id> Required task whose original criteria are reviewed
214
+ --tree Cover this task and every non-archived descendant in one two-round review
183
215
  --base <git-ref> Review a clean committed range
184
216
  --output <file> Write the JSON envelope to <file> (mode 0600) and print
185
217
  one summary line, so no shell redirect is needed
@@ -0,0 +1,173 @@
1
+ import { spawn } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
4
+ import { finishWorktree } from "../../../../runtime/src/parallel/finish.js";
5
+ import { integrateSessionChildren } from "../../../../runtime/src/parallel/integrate.js";
6
+ import { readWorktreeRecord, resolveCheckouts, sessionWorktreeName, worktreePath } from "../../../../runtime/src/parallel/service.js";
7
+ import { resolveReviewScope } from "../../../../runtime/src/review/scope.js";
8
+ import { renderReviewDetails, renderReviewHighlights } from "../../../../runtime/src/review/render.js";
9
+ import { calculateSourceFingerprint } from "../../../../runtime/src/verify/source-fingerprint.js";
10
+ import { okEnvelope } from "../output.js";
11
+ import { readFinishedReview } from "./review-show.js";
12
+ import { prepareRunIntegration, readRunIntegrationProof, abandonPreparedRunIntegration } from "../../../../runtime/src/run/integration.js";
13
+ /** The child process runs the existing CLI in the candidate checkout. */
14
+ export async function runAdvanceStep(cwd, args) {
15
+ const bin = process.argv[1];
16
+ if (bin === undefined)
17
+ throw new AgentOpsError("ADVANCE_CLI_MISSING", "Cannot locate the agent-ops CLI entry point.");
18
+ return await new Promise((resolveStep, reject) => {
19
+ const child = spawn(process.execPath, [resolve(bin), ...args, "--json"], {
20
+ cwd, env: process.env, stdio: ["ignore", "pipe", "inherit"]
21
+ });
22
+ const chunks = [];
23
+ let bytes = 0;
24
+ child.stdout.on("data", (chunk) => {
25
+ bytes += chunk.length;
26
+ if (bytes > 4 * 1024 * 1024)
27
+ child.kill();
28
+ else
29
+ chunks.push(chunk);
30
+ });
31
+ child.once("error", reject);
32
+ child.once("close", () => {
33
+ try {
34
+ const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
35
+ if (value.status !== "ok" && value.status !== "error")
36
+ throw new Error("Invalid CLI envelope.");
37
+ resolveStep(value);
38
+ }
39
+ catch {
40
+ reject(new AgentOpsError("ADVANCE_STEP_UNREADABLE", `Could not read ${args[0]} result from the candidate checkout.`));
41
+ }
42
+ });
43
+ });
44
+ }
45
+ function runner(deps, cwd) {
46
+ return { run: async (args) => {
47
+ const result = await deps.git(cwd, args);
48
+ return { exitCode: result.exitCode, stdout: Buffer.from(result.stdout, "utf8") };
49
+ } };
50
+ }
51
+ async function git(deps, cwd, args) {
52
+ const result = await deps.git(cwd, args);
53
+ if (result.exitCode !== 0)
54
+ throw new AgentOpsError("ADVANCE_GIT_FAILED", `${args.join(" ")}: ${result.stderr.trim()}`);
55
+ return result.stdout.trim();
56
+ }
57
+ function requirePass(step, label) {
58
+ if (step.status === "ok" && (step.data?.result?.status === "PASS" || step.data?.report?.status === "PASS"))
59
+ return;
60
+ const result = step.data?.result;
61
+ const details = result?.report !== undefined || result?.attempts !== undefined
62
+ ? `\n${renderReviewDetails([{ taskId: result.taskId ?? label, title: label, artifact: result }], "active")}`
63
+ : step.data?.text ?? "";
64
+ throw new AgentOpsError(step.code, `${label}: ${step.errors?.[0]?.message ?? result?.reason ?? "proof did not pass"}${details ? `\n${details}` : ""}`);
65
+ }
66
+ export async function reviewFinalTree(step, cwd, parentTaskId, taskIds, base) {
67
+ const reviewed = await step(cwd, ["review", "--task", parentTaskId, "--tree", "--base", base, "--yes"]);
68
+ if (reviewed.status === "error" && reviewed.data?.result?.status === "NOT_RUN" &&
69
+ reviewed.data.result.reason === "scope-too-large") {
70
+ for (const taskId of taskIds) {
71
+ requirePass(await step(cwd, ["review", "--task", taskId, "--base", base, "--yes"]), `Review ${taskId}`);
72
+ }
73
+ return "per-task-fallback";
74
+ }
75
+ requirePass(reviewed, "Tree review");
76
+ return "tree";
77
+ }
78
+ export async function runAdvanceCommand(options) {
79
+ const { deps, cwd } = options;
80
+ if (options.sessionId === undefined || options.parentTaskId === undefined) {
81
+ throw new AgentOpsError("ADVANCE_TARGET_REQUIRED", "task advance requires --task and a current or explicit --session.");
82
+ }
83
+ const { mainRoot, currentRoot, commonDir } = await resolveCheckouts(deps, cwd);
84
+ if (mainRoot !== currentRoot)
85
+ throw new AgentOpsError("WORKTREE_NESTED", `Run task advance from ${mainRoot}.`);
86
+ const name = sessionWorktreeName(options.sessionId);
87
+ const record = await readWorktreeRecord(worktreePath(mainRoot, name));
88
+ if (record === null || record.sessionId !== options.sessionId || record.agentId !== undefined) {
89
+ throw new AgentOpsError("ADVANCE_WORKTREE_MISSING", `No coordinator worktree for session ${options.sessionId}.`);
90
+ }
91
+ const savedProof = await readRunIntegrationProof(commonDir, record);
92
+ if (savedProof !== null && await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) === savedProof.head) {
93
+ const finished = await finishWorktree(deps, { cwd: mainRoot, name, finalProof: { ...savedProof, recovery: true } });
94
+ return okEnvelope("TASK_ADVANCED", { ...finished, text: "Recovered the sealed integration without moving the target again." });
95
+ }
96
+ if (savedProof !== null && await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) !== savedProof.target)
97
+ await abandonPreparedRunIntegration(commonDir, record, deps.git);
98
+ const parent = await deps.tasks(record.path).status({ taskId: options.parentTaskId });
99
+ if (parent.status !== "active" || parent.task.parentTaskId !== undefined) {
100
+ throw new AgentOpsError("ADVANCE_PARENT_INVALID", "Advance requires an active root task in the coordinator worktree.");
101
+ }
102
+ const children = await integrateSessionChildren(deps, record, parent.task.id);
103
+ const step = options.step ?? runAdvanceStep;
104
+ for (let attempt = 0; attempt < 2; attempt += 1) {
105
+ const target = await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]);
106
+ const branchHead = await git(deps, record.path, ["rev-parse", "HEAD"]);
107
+ if ((await deps.git(record.path, ["merge-base", "--is-ancestor", target, "HEAD"])).exitCode !== 0) {
108
+ const rebase = await deps.git(record.path, ["rebase", "--rebase-merges", "--no-autostash", target]);
109
+ if (rebase.exitCode !== 0) {
110
+ const files = (await deps.git(record.path, ["diff", "--name-only", "--diff-filter=U"])).stdout.trim();
111
+ await deps.git(record.path, ["rebase", "--abort"]);
112
+ throw new AgentOpsError("WORKTREE_REBASE_CONFLICT", `Target moved; rebase conflicted in ${files || "unknown files"}. Candidate remains at ${branchHead}.`);
113
+ }
114
+ }
115
+ const head = await git(deps, record.path, ["rev-parse", "HEAD"]);
116
+ const all = (await deps.tasks(record.path).list()).filter(({ status }) => status !== "archived");
117
+ const selected = new Set([parent.task.id]);
118
+ for (let changed = true; changed;) {
119
+ changed = false;
120
+ for (const item of all) {
121
+ if (item.task.parentTaskId !== undefined && selected.has(item.task.parentTaskId) && !selected.has(item.task.id)) {
122
+ selected.add(item.task.id);
123
+ changed = true;
124
+ }
125
+ }
126
+ }
127
+ if (selected.size !== all.length || all.some(({ task, status }) => !selected.has(task.id) || status !== "active")) {
128
+ throw new AgentOpsError("ADVANCE_TASK_TREE_INVALID", "The candidate has unrelated or already completed tasks; final proof must cover one active tree.");
129
+ }
130
+ const ordered = all.filter(({ task }) => selected.has(task.id));
131
+ const noChangePaths = head === target ? parent.noChangePaths : undefined;
132
+ if (noChangePaths !== undefined)
133
+ for (const task of ordered)
134
+ await deps.tasks(record.path).recordEvidence(task.task.id, {}, undefined, noChangePaths);
135
+ for (const task of ordered) {
136
+ requirePass(await step(record.path, ["verify", "--task", task.task.id, "--base", target]), `Verify ${task.task.id}`);
137
+ }
138
+ const reviewMode = await reviewFinalTree(step, record.path, parent.task.id, ordered.map(({ task }) => task.id), target);
139
+ const scope = await resolveReviewScope({ root: record.path, runner: runner(deps, record.path), base: target,
140
+ ...(noChangePaths === undefined ? {} : { noChangePaths }) });
141
+ const sourceFingerprint = await calculateSourceFingerprint(record.path, scope, runner(deps, record.path));
142
+ const finalProof = { target, head, sourceFingerprint, children,
143
+ ...(noChangePaths === undefined ? {} : { noChange: { sourceCommit: head,
144
+ contractDigest: await deps.tasks(record.path).treeContract(parent.task.id), reviewScope: scope,
145
+ artifactRefs: Object.values((await deps.tasks(record.path).status({ taskId: parent.task.id })).evidence).flat() } }) };
146
+ await prepareRunIntegration(commonDir, record, finalProof);
147
+ try {
148
+ const finished = await finishWorktree(deps, {
149
+ cwd: mainRoot, name,
150
+ finalProof
151
+ });
152
+ if (finished.receipt === undefined)
153
+ throw new AgentOpsError("WORKTREE_RECEIPT_MISSING", "Finish returned without a final review receipt.");
154
+ const review = await readFinishedReview(deps, mainRoot, commonDir, finished.receipt);
155
+ const highlights = renderReviewHighlights(review.entries);
156
+ return okEnvelope("TASK_ADVANCED", {
157
+ parentTaskId: parent.task.id,
158
+ reviewMode,
159
+ sourceFingerprint,
160
+ ...finished,
161
+ text: `Final candidate ${head} passed all task verifiers and ${reviewMode} review, then finished into ${record.targetBranch}. Receipt: ${finished.receipt}.\n${highlights}Full reports: agent-ops review show --task ${parent.task.id}`
162
+ });
163
+ }
164
+ catch (failure) {
165
+ if (failure instanceof AgentOpsError && failure.code === "WORKTREE_TARGET_MOVED" && attempt === 0) {
166
+ await abandonPreparedRunIntegration(commonDir, record, deps.git);
167
+ continue;
168
+ }
169
+ throw failure;
170
+ }
171
+ }
172
+ throw new AgentOpsError("WORKTREE_TARGET_MOVED", "The target moved again; preserve the candidate and rerun after coordination.");
173
+ }
@@ -0,0 +1,189 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { sha256 } from "../../../../runtime/src/fs/hash.js";
4
+ import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
5
+ import { listWorktrees } from "../../../../runtime/src/parallel/manage.js";
6
+ import { NOTES_REF, resolveCheckouts } from "../../../../runtime/src/parallel/service.js";
7
+ import { REVIEW_ATTESTATION_DIRECTORY, REVIEW_REPORT_ARTIFACT_SUFFIX } from "../../../../runtime/src/review/attestation.js";
8
+ import { renderReviewDetails } from "../../../../runtime/src/review/render.js";
9
+ import { validateReviewReport } from "../../../../runtime/src/review/report.js";
10
+ import { resolveReviewScope } from "../../../../runtime/src/review/scope.js";
11
+ import { readPrivateFile } from "../../../../runtime/src/security/permissions.js";
12
+ import { calculateSourceFingerprint } from "../../../../runtime/src/verify/source-fingerprint.js";
13
+ import { okEnvelope } from "../output.js";
14
+ function invalid(message) {
15
+ throw new AgentOpsError("REVIEW_RECORD_INVALID", message);
16
+ }
17
+ function checkedArtifact(value, fingerprint, taskId) {
18
+ if (typeof value !== "object" || value === null)
19
+ invalid("Review artifact is unreadable.");
20
+ const artifact = value;
21
+ if (![1, 2].includes(artifact.schemaVersion) || artifact.sourceFingerprint !== fingerprint ||
22
+ artifact.taskId !== taskId || !["PASS", "FAIL", "NOT_RUN"].includes(artifact.status) ||
23
+ !Array.isArray(artifact.attempts) ||
24
+ (artifact.report !== undefined && !validReport(artifact.report, artifact)) ||
25
+ (artifact.adversarial !== undefined && !validReport(artifact.adversarial.report, artifact))) {
26
+ invalid(`Review artifact for ${taskId} does not match the candidate.`);
27
+ }
28
+ return artifact;
29
+ }
30
+ function validReport(report, artifact) {
31
+ const ids = artifact.tree?.criterionIds ?? report?.results?.map(({ criterionId }) => criterionId);
32
+ return ids !== undefined && validateReviewReport(report, ids).ok;
33
+ }
34
+ function entriesFromReceipt(receipt) {
35
+ const records = receipt.reviewMode === "tree"
36
+ ? [receipt.tasks.find(({ task }) => task.parentTaskId === undefined) ?? receipt.tasks[0]]
37
+ : receipt.tasks;
38
+ return records.map(({ task }) => {
39
+ const sealed = receipt.reviews[task.id]?.report;
40
+ if (sealed === undefined || sha256(JSON.stringify(sealed.value)) !== sealed.digest) {
41
+ invalid(`Review evidence for ${task.id} is missing or changed.`);
42
+ }
43
+ return { taskId: task.id, title: task.title,
44
+ artifact: checkedArtifact(sealed.value, receipt.sourceFingerprint, task.id) };
45
+ });
46
+ }
47
+ /** The note binds the private receipt to the merged commit and its exact bytes. */
48
+ export async function readFinishedReview(deps, mainRoot, commonDir, path) {
49
+ if (dirname(path) !== join(commonDir, "agent-ops", "receipts"))
50
+ invalid("Final review receipt path is outside the receipt directory.");
51
+ const source = await readPrivateFile(path, commonDir);
52
+ if (source === null)
53
+ invalid("Final review receipt is missing.");
54
+ let receipt;
55
+ try {
56
+ receipt = JSON.parse(source);
57
+ }
58
+ catch {
59
+ return invalid("Final review receipt is malformed.");
60
+ }
61
+ if (receipt.schemaVersion !== 1 || !/^[a-f0-9]{40,64}$/u.test(receipt.candidateHead) ||
62
+ !Array.isArray(receipt.tasks) || receipt.tasks.length === 0 ||
63
+ !["tree", "per-task-fallback"].includes(receipt.reviewMode) ||
64
+ typeof receipt.reviews !== "object" || receipt.reviews === null) {
65
+ invalid("Final review receipt is invalid.");
66
+ }
67
+ const note = await deps.git(mainRoot, ["notes", `--ref=${NOTES_REF}`, "show", receipt.candidateHead]);
68
+ const binding = `\nreceipt: ${path}\nreceipt-sha256: ${sha256(source)}`;
69
+ if (note.exitCode !== 0 || !note.stdout.trimEnd().endsWith(binding)) {
70
+ invalid("Final review receipt does not match its Git note.");
71
+ }
72
+ return { mode: receipt.reviewMode, candidateHead: receipt.candidateHead,
73
+ sourceFingerprint: receipt.sourceFingerprint, entries: entriesFromReceipt(receipt), receipt: path };
74
+ }
75
+ async function finishedPath(commonDir, taskId) {
76
+ const directory = join(commonDir, "agent-ops", "receipts");
77
+ let names;
78
+ try {
79
+ names = await readdir(directory);
80
+ }
81
+ catch (error) {
82
+ if (error.code === "ENOENT")
83
+ return null;
84
+ throw error;
85
+ }
86
+ let latest = null;
87
+ for (const name of names.filter((item) => /^[a-f0-9]{64}-[a-f0-9]{40,64}\.json$/u.test(item))) {
88
+ const path = join(directory, name);
89
+ const source = await readPrivateFile(path, commonDir);
90
+ if (source === null)
91
+ continue;
92
+ try {
93
+ const receipt = JSON.parse(source);
94
+ if (!Array.isArray(receipt.tasks) || !receipt.tasks.some(({ task }) => task.id === taskId))
95
+ continue;
96
+ if (latest === null || receipt.createdAt > latest.createdAt)
97
+ latest = { path, createdAt: receipt.createdAt };
98
+ }
99
+ catch { /* An unrelated malformed receipt cannot name this task. */ }
100
+ }
101
+ return latest?.path ?? null;
102
+ }
103
+ function runner(deps, cwd) {
104
+ return { run: async (args) => {
105
+ const result = await deps.git(cwd, args);
106
+ return { exitCode: result.exitCode, stdout: Buffer.from(result.stdout, "utf8") };
107
+ } };
108
+ }
109
+ async function activeReview(deps, mainRoot, taskId) {
110
+ const matching = [];
111
+ for (const { record } of await listWorktrees(deps, mainRoot)) {
112
+ const records = await deps.tasks(record.path).list();
113
+ if (records.some(({ task }) => task.id === taskId))
114
+ matching.push({ record, records });
115
+ }
116
+ if (matching.length > 1)
117
+ invalid(`Task ${taskId} exists in more than one active worktree.`);
118
+ const current = matching[0];
119
+ if (current === undefined)
120
+ return null;
121
+ const { record, records } = current;
122
+ const headResult = await deps.git(record.path, ["rev-parse", "HEAD"]);
123
+ if (headResult.exitCode !== 0)
124
+ invalid("Cannot read the active candidate HEAD.");
125
+ const candidateHead = headResult.stdout.trim();
126
+ const selected = new Set([taskId]);
127
+ for (let changed = true; changed;) {
128
+ changed = false;
129
+ for (const { task } of records) {
130
+ if (task.parentTaskId !== undefined && selected.has(task.parentTaskId) && !selected.has(task.id)) {
131
+ selected.add(task.id);
132
+ changed = true;
133
+ }
134
+ }
135
+ }
136
+ const tree = [records.find(({ task }) => task.id === taskId),
137
+ ...records.filter(({ task }) => selected.has(task.id) && task.id !== taskId)];
138
+ const target = await deps.git(mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]);
139
+ if (target.exitCode !== 0)
140
+ invalid("Cannot read the target branch for the active candidate.");
141
+ let fingerprint;
142
+ try {
143
+ const scope = await resolveReviewScope({ root: record.path, runner: runner(deps, record.path), base: target.stdout.trim() });
144
+ fingerprint = await calculateSourceFingerprint(record.path, scope, runner(deps, record.path));
145
+ }
146
+ catch (error) {
147
+ if (error instanceof AgentOpsError && error.code === "REVIEW_DIRTY_WORKTREE") {
148
+ try {
149
+ const scope = await resolveReviewScope({ root: record.path, runner: runner(deps, record.path) });
150
+ fingerprint = await calculateSourceFingerprint(record.path, scope, runner(deps, record.path));
151
+ }
152
+ catch { /* No report for this source state. */ }
153
+ }
154
+ }
155
+ const entries = [];
156
+ for (const { task } of tree) {
157
+ let artifact = null;
158
+ if (fingerprint !== undefined) {
159
+ const source = await readPrivateFile(join(record.path, REVIEW_ATTESTATION_DIRECTORY, `${fingerprint}.${task.id}${REVIEW_REPORT_ARTIFACT_SUFFIX}`), record.path);
160
+ if (source !== null) {
161
+ try {
162
+ artifact = checkedArtifact(JSON.parse(source), fingerprint, task.id);
163
+ }
164
+ catch (error) {
165
+ if (error instanceof AgentOpsError)
166
+ throw error;
167
+ invalid(`Review artifact for ${task.id} is malformed.`);
168
+ }
169
+ }
170
+ }
171
+ entries.push({ taskId: task.id, title: task.title, artifact });
172
+ }
173
+ // A failed whole-tree review writes one parent artifact; its criterion IDs
174
+ // already cover the whole tree, so do not print empty child aliases.
175
+ const first = entries[0];
176
+ const wholeTree = first?.artifact?.report?.results.some(({ criterionId }) => criterionId.includes(":"));
177
+ return { mode: "active", candidateHead, ...(fingerprint === undefined ? {} : { sourceFingerprint: fingerprint }),
178
+ entries: wholeTree ? [first] : entries };
179
+ }
180
+ export async function runReviewShowCommand(options) {
181
+ const { mainRoot, commonDir } = await resolveCheckouts(options.deps, options.cwd);
182
+ const active = await activeReview(options.deps, mainRoot, options.taskId);
183
+ const path = active === null ? await finishedPath(commonDir, options.taskId) : null;
184
+ const view = active ?? (path === null ? null : await readFinishedReview(options.deps, mainRoot, commonDir, path));
185
+ if (view === null)
186
+ throw new AgentOpsError("REVIEW_TASK_NOT_FOUND", `No active or finished review task ${options.taskId}.`);
187
+ return okEnvelope("REVIEW_SHOWN", { ...view,
188
+ text: `Candidate: ${view.candidateHead}\n${renderReviewDetails(view.entries, view.mode)}` });
189
+ }