@kylecheng3146/agent-ops 0.1.7 → 0.1.8

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 (33) hide show
  1. package/README.md +9 -4
  2. package/dist/packages/cli/src/args.js +15 -0
  3. package/dist/packages/cli/src/bin.js +36 -22
  4. package/dist/packages/cli/src/cli.js +1 -0
  5. package/dist/packages/cli/src/commands/review.js +286 -29
  6. package/dist/packages/cli/src/commands/task.js +4 -1
  7. package/dist/packages/cli/src/commands/verify.js +13 -1
  8. package/dist/packages/cli/src/wizard.js +3 -3
  9. package/dist/runtime/src/contracts.js +1 -1
  10. package/dist/runtime/src/review/execute.js +143 -83
  11. package/dist/runtime/src/review/extract.js +20 -22
  12. package/dist/runtime/src/review/invocation.js +66 -2
  13. package/dist/runtime/src/review/packet.js +42 -5
  14. package/dist/runtime/src/review/probe.js +50 -26
  15. package/dist/runtime/src/review/render.js +62 -0
  16. package/dist/runtime/src/review/report.js +183 -0
  17. package/dist/runtime/src/review/runner.js +85 -33
  18. package/dist/runtime/src/review/scope.js +123 -0
  19. package/dist/runtime/src/schema/validate.js +18 -0
  20. package/dist/runtime/src/task/service.js +6 -1
  21. package/dist/runtime/src/task/store.js +16 -4
  22. package/dist/runtime/src/verify/change-surface.js +38 -2
  23. package/dist/runtime/src/verify/command-executor.js +4 -1
  24. package/dist/runtime/src/verify/evidence.js +36 -0
  25. package/dist/runtime/src/verify/scope.js +1 -2
  26. package/dist/runtime/src/verify/service.js +66 -9
  27. package/dist/runtime/src/verify/source-fingerprint.js +49 -0
  28. package/dist/runtime/src/verify/spawn.js +9 -3
  29. package/docs/en/guides/configuration.md +17 -9
  30. package/docs/zh-TW/guides/configuration.md +15 -8
  31. package/package.json +1 -1
  32. package/schemas/evidence.schema.json +16 -1
  33. package/schemas/review-report.schema.json +48 -0
@@ -1,6 +1,10 @@
1
+ import { mkdtemp, realpath, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import { runVerificationCommand } from "../verify/spawn.js";
2
- import { extractFinalMessage, extractJsonObject } from "./extract.js";
5
+ import { extractReviewObject } from "./extract.js";
3
6
  import { buildTargetInvocation } from "./invocation.js";
7
+ import { reviewReportResults, reviewReportStatus, validateReviewReport } from "./report.js";
4
8
  import { detectHostTarget, orderChain } from "./roles.js";
5
9
  import { buildReviewPrompt } from "./runner.js";
6
10
  /**
@@ -8,6 +12,41 @@ import { buildReviewPrompt } from "./runner.js";
8
12
  * chain, so the worst case is targets x timeout.
9
13
  */
10
14
  export const DEFAULT_REVIEW_TIMEOUT_MS = 120_000;
15
+ const EXECUTION_ENV = [
16
+ "PATH", "PATHEXT", "SystemRoot", "SYSTEMROOT", "WINDIR", "COMSPEC",
17
+ "LANG", "LC_ALL", "TERM", "TMPDIR", "TEMP", "TMP"
18
+ ];
19
+ const AUTH_ENV = {
20
+ claude: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
21
+ codex: ["OPENAI_API_KEY"],
22
+ agy: ["AGY_API_KEY"]
23
+ };
24
+ export function isolatedReviewEnvironment(target, directory, source) {
25
+ const env = {};
26
+ for (const key of [...EXECUTION_ENV, ...AUTH_ENV[target]]) {
27
+ const value = source[key];
28
+ if (value !== undefined) {
29
+ env[key] = value;
30
+ }
31
+ }
32
+ env.HOME = directory;
33
+ env.USERPROFILE = directory;
34
+ env.XDG_CONFIG_HOME = join(directory, "config");
35
+ env.XDG_CACHE_HOME = join(directory, "cache");
36
+ return env;
37
+ }
38
+ /** Codex and agy currently lack documented instruction/customization isolation. */
39
+ export function hasRequiredReviewIsolation(target) {
40
+ return target === "claude";
41
+ }
42
+ const REQUIRED_HELP_FLAGS = {
43
+ claude: [
44
+ "--add-dir", "--permission-mode", "--no-session-persistence",
45
+ "--safe-mode", "--disable-slash-commands", "--json-schema"
46
+ ],
47
+ codex: [],
48
+ agy: []
49
+ };
11
50
  /**
12
51
  * Failure classes that mean no review happened, so trying the next target is
13
52
  * not review shopping. Everything else — including FAIL — is terminal.
@@ -17,47 +56,6 @@ const ADVANCING = new Set([
17
56
  "spawn-failed",
18
57
  "timeout"
19
58
  ]);
20
- function statusOf(value) {
21
- return value === "PASS" || value === "FAIL" ? value : undefined;
22
- }
23
- /**
24
- * The response must name every requested criterion exactly once, with at least
25
- * one non-blank evidence reference. A response that breaks the contract is
26
- * unparseable output, never a FAIL verdict: FAIL has to keep meaning "the
27
- * reviewer looked and judged it inadequate".
28
- */
29
- function parseResults(payload, expected) {
30
- const raw = payload.results;
31
- if (!Array.isArray(raw) || raw.length !== expected.length) {
32
- return undefined;
33
- }
34
- const results = [];
35
- const seen = new Set();
36
- for (const entry of raw) {
37
- if (typeof entry !== "object" || entry === null) {
38
- return undefined;
39
- }
40
- const item = entry;
41
- const criterionId = item.criterionId;
42
- const status = statusOf(item.status);
43
- if (typeof criterionId !== "string" ||
44
- status === undefined ||
45
- !expected.includes(criterionId) ||
46
- seen.has(criterionId) ||
47
- !Array.isArray(item.evidence) ||
48
- item.evidence.length === 0 ||
49
- !item.evidence.every((reference) => typeof reference === "string" && reference.trim().length > 0)) {
50
- return undefined;
51
- }
52
- seen.add(criterionId);
53
- results.push({
54
- criterionId,
55
- status,
56
- evidence: item.evidence.map((reference) => String(reference))
57
- });
58
- }
59
- return results;
60
- }
61
59
  /**
62
60
  * Builds the `execute` callback `runIndependentReview` expects: walk the
63
61
  * configured targets in order and return the first real verdict.
@@ -69,52 +67,114 @@ export function createReviewExecutor(options) {
69
67
  return async (request) => {
70
68
  const expected = request.invocation.packet.criteria.map((criterion) => criterion.id);
71
69
  const prompt = buildReviewPrompt(request.invocation);
70
+ const repositoryRoot = await realpath(options.cwd);
71
+ let unavailable = false;
72
72
  for (const [index, target] of chain.entries()) {
73
- const invocation = buildTargetInvocation({
74
- target,
75
- prompt,
76
- ...(options.model === undefined ? {} : { model: options.model }),
77
- ...(options.effort === undefined ? {} : { effort: options.effort })
78
- });
79
- if (invocation === undefined) {
80
- report(`${target}: no read-only mode available → skipping`);
81
- continue;
82
- }
83
- if (target === host) {
84
- report(`${target}: reviewer == host; no independent target configured`);
85
- }
86
- const spawned = await runVerificationCommand({
87
- id: `review-${target}-${index}`,
88
- command: invocation.command,
89
- args: [...invocation.args],
90
- cwd: options.cwd,
91
- required: true,
92
- evidence: { kind: "exit-code" },
93
- timeoutMs: options.timeoutMs ?? DEFAULT_REVIEW_TIMEOUT_MS
94
- }, {
95
- cwd: options.cwd,
96
- ...(options.runner === undefined ? {} : { runner: options.runner }),
97
- ...(options.outputLimitBytes === undefined
98
- ? {}
99
- : { outputLimitBytes: options.outputLimitBytes })
100
- });
101
- if (ADVANCING.has(spawned.failureClass)) {
102
- report(`${target}: ${spawned.failureClass} → trying next target`);
73
+ if (!hasRequiredReviewIsolation(target)) {
74
+ unavailable = true;
75
+ report(`${target}: required context-isolation controls unavailable → skipping`);
103
76
  continue;
104
77
  }
105
- if (spawned.stdoutTruncated) {
106
- return { status: "NOT_RUN", reason: "unparseable-output" };
78
+ const attemptDirectory = await mkdtemp(join(tmpdir(), "agent-ops-review-"));
79
+ try {
80
+ const invocation = buildTargetInvocation({
81
+ target,
82
+ prompt,
83
+ repositoryRoot,
84
+ ...(options.model === undefined ? {} : { model: options.model }),
85
+ ...(options.effort === undefined ? {} : { effort: options.effort })
86
+ });
87
+ if (invocation === undefined) {
88
+ unavailable = true;
89
+ report(`${target}: no read-only mode available → skipping`);
90
+ continue;
91
+ }
92
+ if (target === host) {
93
+ report(`${target}: reviewer == host; no independent target configured`);
94
+ }
95
+ const environment = isolatedReviewEnvironment(target, attemptDirectory, options.env ?? process.env);
96
+ const capability = await runVerificationCommand({
97
+ id: `review-capability-${target}-${index}`,
98
+ command: invocation.command,
99
+ args: ["--help"],
100
+ cwd: attemptDirectory,
101
+ required: true,
102
+ evidence: { kind: "exit-code" },
103
+ timeoutMs: Math.min(options.timeoutMs ?? DEFAULT_REVIEW_TIMEOUT_MS, 10_000)
104
+ }, {
105
+ cwd: attemptDirectory,
106
+ ...(options.runner === undefined ? {} : { runner: options.runner }),
107
+ env: environment,
108
+ replaceEnv: true
109
+ });
110
+ if (capability.status !== "PASS" ||
111
+ capability.stdoutTruncated ||
112
+ capability.stderrTruncated ||
113
+ REQUIRED_HELP_FLAGS[target].some((flag) => !capability.stdout.includes(flag))) {
114
+ unavailable = true;
115
+ report(`${target}: required CLI capabilities unavailable → skipping`);
116
+ continue;
117
+ }
118
+ const spawned = await runVerificationCommand({
119
+ id: `review-${target}-${index}`,
120
+ command: invocation.command,
121
+ args: [...invocation.args],
122
+ cwd: attemptDirectory,
123
+ required: true,
124
+ evidence: { kind: "exit-code" },
125
+ timeoutMs: options.timeoutMs ?? DEFAULT_REVIEW_TIMEOUT_MS
126
+ }, {
127
+ cwd: attemptDirectory,
128
+ ...(options.runner === undefined ? {} : { runner: options.runner }),
129
+ ...(options.outputLimitBytes === undefined
130
+ ? {}
131
+ : { outputLimitBytes: options.outputLimitBytes }),
132
+ stdin: invocation.stdin,
133
+ env: environment,
134
+ replaceEnv: true
135
+ });
136
+ if (ADVANCING.has(spawned.failureClass)) {
137
+ report(`${target}: ${spawned.failureClass} → trying next target`);
138
+ continue;
139
+ }
140
+ if (spawned.stdoutTruncated || spawned.stderrTruncated) {
141
+ return { status: "NOT_RUN", reason: "output-too-large", harness: target };
142
+ }
143
+ const payload = extractReviewObject(target, spawned.stdout);
144
+ const parsed = payload === undefined
145
+ ? undefined
146
+ : validateReviewReport(payload, expected, request.invocation.scope?.changedFiles);
147
+ if (parsed === undefined || !parsed.ok) {
148
+ return {
149
+ status: "NOT_RUN",
150
+ reason: parsed?.errors.some((error) => error.code === "INCOMPLETE_SCOPE")
151
+ ? "incomplete-scope"
152
+ : "unparseable-output",
153
+ harness: target,
154
+ ...(parsed === undefined ? {} : { validationErrors: parsed.errors })
155
+ };
156
+ }
157
+ const reportValue = parsed.value;
158
+ const results = reviewReportResults(reportValue);
159
+ return {
160
+ status: reviewReportStatus(reportValue),
161
+ results,
162
+ report: reportValue,
163
+ harness: target,
164
+ independence: host === undefined
165
+ ? "unknown"
166
+ : host === target
167
+ ? "same-target"
168
+ : "different-target"
169
+ };
107
170
  }
108
- const message = extractFinalMessage(target, spawned.stdout);
109
- const payload = message === undefined ? undefined : extractJsonObject(message);
110
- const results = payload === undefined ? undefined : parseResults(payload, expected);
111
- if (results === undefined) {
112
- return { status: "NOT_RUN", reason: "unparseable-output" };
171
+ finally {
172
+ await rm(attemptDirectory, { recursive: true, force: true });
113
173
  }
114
- return results.every((result) => result.status === "PASS")
115
- ? { status: "PASS", results }
116
- : { status: "FAIL", results };
117
174
  }
118
- return { status: "NOT_RUN", reason: "missing-cli" };
175
+ return {
176
+ status: "NOT_RUN",
177
+ reason: unavailable ? "capability-unavailable" : "missing-cli"
178
+ };
119
179
  };
120
180
  }
@@ -18,6 +18,9 @@ function parseObject(text) {
18
18
  ? parsed
19
19
  : undefined;
20
20
  }
21
+ function isRecord(value) {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
21
24
  /**
22
25
  * The model's answer as text, before any JSON contract is applied. Returns
23
26
  * undefined rather than throwing so the caller can report
@@ -45,27 +48,22 @@ export function extractFinalMessage(target, stdout) {
45
48
  * raw stdout, so it cannot capture a transport envelope.
46
49
  */
47
50
  export function extractJsonObject(text) {
48
- for (let end = text.lastIndexOf("}"); end !== -1; end = text.lastIndexOf("}", end - 1)) {
49
- let depth = 0;
50
- for (let start = end; start >= 0; start -= 1) {
51
- const character = text[start];
52
- if (character === "}") {
53
- depth += 1;
54
- continue;
55
- }
56
- if (character !== "{") {
57
- continue;
58
- }
59
- depth -= 1;
60
- if (depth !== 0) {
61
- continue;
62
- }
63
- const candidate = parseObject(text.slice(start, end + 1));
64
- if (candidate !== undefined) {
65
- return candidate;
66
- }
67
- break;
68
- }
51
+ return parseObject(text.trim());
52
+ }
53
+ /**
54
+ * Review results use a strict native structured-output transport. Unlike the
55
+ * legacy probe parser above, this path neither recovers fenced JSON nor accepts
56
+ * a provider's generic text result field.
57
+ */
58
+ export function extractReviewObject(target, stdout) {
59
+ if (target === "codex") {
60
+ return extractJsonObject(stdout);
61
+ }
62
+ const envelope = parseObject(stdout);
63
+ const key = target === "claude" ? "structured_output" : "response";
64
+ const value = envelope?.[key];
65
+ if (isRecord(value)) {
66
+ return value;
69
67
  }
70
- return undefined;
68
+ return typeof value === "string" ? extractJsonObject(value) : undefined;
71
69
  }
@@ -1,3 +1,19 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ function reviewSchemaPath() {
5
+ const packaged = fileURLToPath(new URL("../../../../schemas/review-report.schema.json", import.meta.url));
6
+ if (existsSync(packaged)) {
7
+ return packaged;
8
+ }
9
+ const source = fileURLToPath(new URL("../../../schemas/review-report.schema.json", import.meta.url));
10
+ return existsSync(source)
11
+ ? source
12
+ : resolve(process.cwd(), "schemas", "review-report.schema.json");
13
+ }
14
+ function reviewSchemaText() {
15
+ return readFileSync(reviewSchemaPath(), "utf8");
16
+ }
1
17
  /**
2
18
  * Read-only enforcement per target, verified against each CLI's own help
3
19
  * output. A target absent from this table is ineligible: review never runs an
@@ -42,11 +58,59 @@ export function buildTargetInvocation(request) {
42
58
  // directory, which a caller would otherwise read as "not authenticated".
43
59
  return {
44
60
  command: "codex",
45
- args: ["exec", request.prompt, "--skip-git-repo-check", ...shared]
61
+ args: [
62
+ "exec",
63
+ "-",
64
+ "--skip-git-repo-check",
65
+ "--output-schema",
66
+ reviewSchemaPath(),
67
+ ...(request.repositoryRoot === undefined
68
+ ? []
69
+ : ["--add-dir", request.repositoryRoot, "--ephemeral", "--ignore-rules"]),
70
+ ...shared
71
+ ],
72
+ stdin: request.prompt
73
+ };
74
+ }
75
+ const isolation = request.target === "claude"
76
+ ? ["--no-session-persistence", "--safe-mode", "--disable-slash-commands"]
77
+ : [];
78
+ return {
79
+ command: request.target,
80
+ args: [
81
+ "-p",
82
+ "--output-format",
83
+ "json",
84
+ "--json-schema",
85
+ reviewSchemaText(),
86
+ ...(request.repositoryRoot === undefined
87
+ ? []
88
+ : ["--add-dir", request.repositoryRoot]),
89
+ ...isolation,
90
+ ...shared
91
+ ],
92
+ stdin: request.prompt
93
+ };
94
+ }
95
+ /** A deep doctor probe uses stdin but keeps its simple text response contract. */
96
+ export function buildProbeInvocation(request) {
97
+ const readOnly = READ_ONLY_ARGS[request.target];
98
+ if (readOnly === undefined || readOnly.length === 0) {
99
+ return undefined;
100
+ }
101
+ if (request.target === "codex") {
102
+ return {
103
+ command: "codex",
104
+ args: ["exec", "-", "--skip-git-repo-check", ...readOnly],
105
+ stdin: request.prompt
46
106
  };
47
107
  }
108
+ const isolation = request.target === "claude"
109
+ ? ["--no-session-persistence", "--safe-mode", "--disable-slash-commands"]
110
+ : [];
48
111
  return {
49
112
  command: request.target,
50
- args: ["-p", request.prompt, "--output-format", "json", ...shared]
113
+ args: ["-p", "--output-format", "json", ...isolation, ...readOnly],
114
+ stdin: request.prompt
51
115
  };
52
116
  }
@@ -1,10 +1,47 @@
1
+ const MAX_PACKET_BYTES = 64 * 1024;
2
+ function safe(value) {
3
+ return safeTaskText(redactSecrets(value));
4
+ }
5
+ function checkSensitive(value) {
6
+ const decision = evaluateGuardrail({
7
+ kind: "content",
8
+ content: value,
9
+ scope: "review-packet"
10
+ });
11
+ if (decision.action === "block") {
12
+ throw new AgentOpsError("REVIEW_SENSITIVE_INPUT", "Review input contains credential-shaped content.");
13
+ }
14
+ }
1
15
  export function buildReviewPacket(input) {
2
- return {
3
- request: input.request,
4
- criteria: input.criteria.map((criterion) => ({ ...criterion })),
5
- artifactRefs: [...input.artifactRefs],
16
+ for (const value of [
17
+ input.request,
18
+ ...input.criteria.flatMap((criterion) => [criterion.id, criterion.description, ...(criterion.verifierIds ?? [])]),
19
+ ...input.artifactRefs,
20
+ ...input.evidenceRequirements.flatMap((requirement) => [requirement.criterionId, requirement.requirement])
21
+ ]) {
22
+ checkSensitive(value);
23
+ }
24
+ const packet = {
25
+ request: safe(input.request),
26
+ criteria: input.criteria.map((criterion) => ({
27
+ id: safe(criterion.id),
28
+ description: safe(criterion.description),
29
+ ...(criterion.verifierIds === undefined
30
+ ? {}
31
+ : { verifierIds: criterion.verifierIds.map(safe) })
32
+ })),
33
+ artifactRefs: input.artifactRefs.map(safe),
6
34
  evidenceRequirements: input.evidenceRequirements.map((requirement) => ({
7
- ...requirement
35
+ criterionId: safe(requirement.criterionId),
36
+ requirement: safe(requirement.requirement)
8
37
  }))
9
38
  };
39
+ if (Buffer.byteLength(JSON.stringify(packet), "utf8") > MAX_PACKET_BYTES) {
40
+ throw new AgentOpsError("REVIEW_SCOPE_TOO_LARGE", "Review packet exceeds the 64 KiB limit.");
41
+ }
42
+ return packet;
10
43
  }
44
+ import { evaluateGuardrail } from "../guardrails/evaluate.js";
45
+ import { AgentOpsError } from "../fs/paths.js";
46
+ import { redactSecrets } from "../security/redact.js";
47
+ import { safeTaskText } from "../task/render.js";
@@ -1,6 +1,10 @@
1
1
  import { runVerificationCommand } from "../verify/spawn.js";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
2
5
  import { extractFinalMessage } from "./extract.js";
3
- import { buildTargetInvocation } from "./invocation.js";
6
+ import { hasRequiredReviewIsolation, isolatedReviewEnvironment } from "./execute.js";
7
+ import { buildProbeInvocation } from "./invocation.js";
4
8
  const PROBE_PROMPT = "Reply with the single word OK and nothing else.";
5
9
  /**
6
10
  * Matches the review timeout rather than being "quick": codex at high
@@ -15,34 +19,54 @@ const PROBE_TIMEOUT_MS = 120_000;
15
19
  * is not evidence at all.
16
20
  */
17
21
  export async function probeReviewTarget(target, options) {
18
- const invocation = buildTargetInvocation({ target, prompt: PROBE_PROMPT });
19
- if (invocation === undefined) {
22
+ const deep = options.deep === true;
23
+ if (deep && !hasRequiredReviewIsolation(target)) {
20
24
  return "ineligible";
21
25
  }
22
- const deep = options.deep === true;
23
- const spawned = await runVerificationCommand({
24
- id: `review-probe-${target}`,
25
- command: invocation.command,
26
- args: deep ? [...invocation.args] : ["--version"],
27
- cwd: options.cwd,
28
- required: true,
29
- evidence: { kind: "exit-code" },
30
- timeoutMs: options.timeoutMs ?? PROBE_TIMEOUT_MS
31
- }, {
32
- cwd: options.cwd,
33
- ...(options.runner === undefined ? {} : { runner: options.runner })
34
- });
35
- if (spawned.failureClass === "missing-executable") {
36
- return "missing-executable";
26
+ const invocation = buildProbeInvocation({ target, prompt: PROBE_PROMPT });
27
+ if (invocation === undefined) {
28
+ return "ineligible";
37
29
  }
38
- if (spawned.timedOut) {
39
- return "timeout";
30
+ const directory = deep
31
+ ? await mkdtemp(join(tmpdir(), "agent-ops-review-probe-"))
32
+ : options.cwd;
33
+ try {
34
+ const spawned = await runVerificationCommand({
35
+ id: `review-probe-${target}`,
36
+ command: invocation.command,
37
+ args: deep ? [...invocation.args] : ["--version"],
38
+ cwd: directory,
39
+ required: true,
40
+ evidence: { kind: "exit-code" },
41
+ timeoutMs: options.timeoutMs ?? PROBE_TIMEOUT_MS
42
+ }, {
43
+ cwd: directory,
44
+ ...(options.runner === undefined ? {} : { runner: options.runner }),
45
+ ...(deep
46
+ ? {
47
+ stdin: invocation.stdin,
48
+ env: isolatedReviewEnvironment(target, directory, process.env),
49
+ replaceEnv: true
50
+ }
51
+ : {})
52
+ });
53
+ if (spawned.failureClass === "missing-executable") {
54
+ return "missing-executable";
55
+ }
56
+ if (spawned.timedOut) {
57
+ return "timeout";
58
+ }
59
+ if (!deep) {
60
+ return spawned.status === "PASS" ? "ok" : "unauthenticated";
61
+ }
62
+ return spawned.status === "PASS" &&
63
+ extractFinalMessage(target, spawned.stdout) !== undefined
64
+ ? "ok"
65
+ : "unauthenticated";
40
66
  }
41
- if (!deep) {
42
- return spawned.status === "PASS" ? "ok" : "unauthenticated";
67
+ finally {
68
+ if (deep) {
69
+ await rm(directory, { recursive: true, force: true });
70
+ }
43
71
  }
44
- return spawned.status === "PASS" &&
45
- extractFinalMessage(target, spawned.stdout) !== undefined
46
- ? "ok"
47
- : "unauthenticated";
48
72
  }
@@ -0,0 +1,62 @@
1
+ import { redactSecrets } from "../security/redact.js";
2
+ import { safeTaskText } from "../task/render.js";
3
+ function safe(value) {
4
+ return safeTaskText(redactSecrets(value));
5
+ }
6
+ function lineList(values) {
7
+ return values.length === 0 ? ["- none"] : values.map((value) => `- ${safe(value)}`);
8
+ }
9
+ export function renderReviewResult(result) {
10
+ const lines = [
11
+ `Independent review: ${result.status}`,
12
+ `Reviewer: ${result.harness}; model: ${safe(result.model)}; effort: ${safe(result.effort)}.`
13
+ ];
14
+ if (result.scope !== undefined) {
15
+ lines.push(result.scope.mode === "base"
16
+ ? `Scope: ${result.scope.mode} ${safe(result.scope.baseRef)} (${safe(result.scope.resolvedBase)}).`
17
+ : "Scope: worktree.");
18
+ }
19
+ if (result.independence !== undefined) {
20
+ lines.push(`Independence: ${result.independence}.`);
21
+ }
22
+ if (result.verification !== undefined) {
23
+ lines.push("Machine verification:");
24
+ for (const command of result.verification.commands) {
25
+ lines.push(`- ${safe(command.criterionId)}/${safe(command.commandId)}: ${command.status}` +
26
+ `${command.required ? " (required)" : " (optional)"}` +
27
+ `${command.evidenceReference === undefined ? "" : ` — ${safe(command.evidenceReference)}`}`);
28
+ }
29
+ }
30
+ if (result.report === undefined) {
31
+ lines.push(`Reason: ${result.reason ?? "unknown"}.`);
32
+ for (const error of result.validationErrors ?? []) {
33
+ lines.push(`- ${safe(error.path)}: ${safe(error.code)} — ${safe(error.message)}`);
34
+ }
35
+ lines.push("Run: agent-ops doctor --check-auth to verify target authentication.");
36
+ return `${lines.join("\n")}\n`;
37
+ }
38
+ const report = result.report;
39
+ const nonBlocking = report.findings.filter((finding) => !finding.blocking).length;
40
+ lines.push(`Non-blocking findings: ${nonBlocking}.`, "", "Summary:", safe(report.summary), "", "Criteria:");
41
+ for (const item of report.results) {
42
+ lines.push(`- ${safe(item.criterionId)}: ${item.status} — ${safe(item.summary)}`);
43
+ lines.push(...item.evidence.map((evidence) => ` - ${safe(evidence)}`));
44
+ }
45
+ lines.push("", "Findings:");
46
+ if (report.findings.length === 0) {
47
+ lines.push("- none");
48
+ }
49
+ else {
50
+ for (const finding of report.findings) {
51
+ lines.push(`- [${finding.severity}] ${finding.blocking ? "blocking" : "non-blocking"}: ${safe(finding.title)}`);
52
+ lines.push(` ${safe(finding.details)}`);
53
+ lines.push(` Recommendation: ${safe(finding.recommendation)}`);
54
+ lines.push(...finding.locations.map((location) => ` Location: ${safe(location.path)}${location.line === undefined ? "" : `:${location.line}`}`));
55
+ lines.push(...finding.evidence.map((evidence) => ` Evidence: ${safe(evidence)}`));
56
+ }
57
+ }
58
+ lines.push("", "Residual risks:", ...lineList(report.residualRisks));
59
+ lines.push("", "Changed files inspected:", ...lineList(report.changedFilesInspected));
60
+ lines.push("", "Supporting files inspected:", ...lineList(report.supportingFilesInspected));
61
+ return `${lines.join("\n")}\n`;
62
+ }