@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.
- package/README.md +9 -4
- package/dist/packages/cli/src/args.js +15 -0
- package/dist/packages/cli/src/bin.js +36 -22
- package/dist/packages/cli/src/cli.js +1 -0
- package/dist/packages/cli/src/commands/review.js +286 -29
- package/dist/packages/cli/src/commands/task.js +4 -1
- package/dist/packages/cli/src/commands/verify.js +13 -1
- package/dist/packages/cli/src/wizard.js +3 -3
- package/dist/runtime/src/contracts.js +1 -1
- package/dist/runtime/src/review/execute.js +143 -83
- package/dist/runtime/src/review/extract.js +20 -22
- package/dist/runtime/src/review/invocation.js +66 -2
- package/dist/runtime/src/review/packet.js +42 -5
- package/dist/runtime/src/review/probe.js +50 -26
- package/dist/runtime/src/review/render.js +62 -0
- package/dist/runtime/src/review/report.js +183 -0
- package/dist/runtime/src/review/runner.js +85 -33
- package/dist/runtime/src/review/scope.js +123 -0
- package/dist/runtime/src/schema/validate.js +18 -0
- package/dist/runtime/src/task/service.js +6 -1
- package/dist/runtime/src/task/store.js +16 -4
- package/dist/runtime/src/verify/change-surface.js +38 -2
- package/dist/runtime/src/verify/command-executor.js +4 -1
- package/dist/runtime/src/verify/evidence.js +36 -0
- package/dist/runtime/src/verify/scope.js +1 -2
- package/dist/runtime/src/verify/service.js +66 -9
- package/dist/runtime/src/verify/source-fingerprint.js +49 -0
- package/dist/runtime/src/verify/spawn.js +9 -3
- package/docs/en/guides/configuration.md +17 -9
- package/docs/zh-TW/guides/configuration.md +15 -8
- package/package.json +1 -1
- package/schemas/evidence.schema.json +16 -1
- package/schemas/review-report.schema.json +48 -0
package/README.md
CHANGED
|
@@ -265,10 +265,15 @@ the project configuration defines those workflows.
|
|
|
265
265
|
|
|
266
266
|
`agent-ops review` can hand the review to another agent CLI, so the work is not
|
|
267
267
|
judged by the agent that produced it. Enable it during `agent-ops init` (the
|
|
268
|
-
default is off)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
268
|
+
default is off). Reviews require an attached task, current required
|
|
269
|
+
verification evidence, and a deterministic worktree (or `--base`) scope. The
|
|
270
|
+
native-schema detailed report is displayed but not persisted.
|
|
271
|
+
|
|
272
|
+
Each attempt uses a fresh temporary cwd, a small allowlisted environment, and a
|
|
273
|
+
target-native read-only/context-isolation mode. Currently only Claude safe mode
|
|
274
|
+
meets the full isolation contract; configured Codex and Agy entries return
|
|
275
|
+
`capability-unavailable` rather than run with a weaker boundary. `opencode` is
|
|
276
|
+
not a review target.
|
|
272
277
|
|
|
273
278
|
The first target that actually runs produces the verdict. A `FAIL` is final:
|
|
274
279
|
the chain never retries elsewhere after a real verdict. `--yes` is still
|
|
@@ -66,6 +66,7 @@ export function parseArgs(argv) {
|
|
|
66
66
|
let targetVersion;
|
|
67
67
|
let title;
|
|
68
68
|
let sessionId;
|
|
69
|
+
let base;
|
|
69
70
|
const profiles = [];
|
|
70
71
|
const reviewTargets = [];
|
|
71
72
|
const criteria = [];
|
|
@@ -179,6 +180,14 @@ export function parseArgs(argv) {
|
|
|
179
180
|
index += 1;
|
|
180
181
|
break;
|
|
181
182
|
}
|
|
183
|
+
case "--base": {
|
|
184
|
+
if (base !== undefined) {
|
|
185
|
+
duplicate(token);
|
|
186
|
+
}
|
|
187
|
+
base = readOptionValue(argv, index, token);
|
|
188
|
+
index += 1;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
182
191
|
case "--check-auth":
|
|
183
192
|
if (checkAuth) {
|
|
184
193
|
duplicate(token);
|
|
@@ -273,6 +282,7 @@ export function parseArgs(argv) {
|
|
|
273
282
|
evidence.length > 0 ||
|
|
274
283
|
reviewTargets.length > 0 ||
|
|
275
284
|
sessionId !== undefined ||
|
|
285
|
+
base !== undefined ||
|
|
276
286
|
checkAuth ||
|
|
277
287
|
dryRun ||
|
|
278
288
|
yes) {
|
|
@@ -303,6 +313,9 @@ export function parseArgs(argv) {
|
|
|
303
313
|
if (command !== "update" && targetVersion !== undefined) {
|
|
304
314
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--target-version may be used only with update.");
|
|
305
315
|
}
|
|
316
|
+
if (base !== undefined && command !== "verify" && command !== "review") {
|
|
317
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--base may be used only with verify or review.");
|
|
318
|
+
}
|
|
306
319
|
if (hookTargets.length > 0 &&
|
|
307
320
|
command !== "init" &&
|
|
308
321
|
command !== "update") {
|
|
@@ -355,6 +368,7 @@ export function parseArgs(argv) {
|
|
|
355
368
|
evidence.length > 0 ||
|
|
356
369
|
dryRun ||
|
|
357
370
|
yes ||
|
|
371
|
+
base !== undefined ||
|
|
358
372
|
(taskId !== undefined && sessionId !== undefined))) {
|
|
359
373
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Verify accepts only scope, task or session, and json options.");
|
|
360
374
|
}
|
|
@@ -379,6 +393,7 @@ export function parseArgs(argv) {
|
|
|
379
393
|
...(criteria.length === 0 ? {} : { criteria }),
|
|
380
394
|
...(evidence.length === 0 ? {} : { evidence }),
|
|
381
395
|
...(sessionId === undefined ? {} : { sessionId }),
|
|
396
|
+
...(base === undefined ? {} : { base }),
|
|
382
397
|
...(checkAuth ? { checkAuth } : {}),
|
|
383
398
|
dryRun,
|
|
384
399
|
json,
|
|
@@ -70,6 +70,29 @@ async function installedManifest(root) {
|
|
|
70
70
|
async function installedHarness(root) {
|
|
71
71
|
return (await installedManifest(root))?.harness ?? [...HARNESS_IDS];
|
|
72
72
|
}
|
|
73
|
+
function gitRunner(root) {
|
|
74
|
+
return {
|
|
75
|
+
run: async (gitArgs) => {
|
|
76
|
+
try {
|
|
77
|
+
return {
|
|
78
|
+
exitCode: 0,
|
|
79
|
+
stdout: execFileSync("git", [...gitArgs], {
|
|
80
|
+
cwd: root,
|
|
81
|
+
encoding: "buffer",
|
|
82
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
83
|
+
})
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const failure = error;
|
|
88
|
+
return {
|
|
89
|
+
exitCode: failure.status ?? 1,
|
|
90
|
+
stdout: failure.stdout ?? new Uint8Array()
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
73
96
|
async function confirmInit(plan) {
|
|
74
97
|
writeBanner({
|
|
75
98
|
isTTY: process.stdout.isTTY === true,
|
|
@@ -179,9 +202,13 @@ else {
|
|
|
179
202
|
const taskService = new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root));
|
|
180
203
|
if (args.command === "task") {
|
|
181
204
|
const sessionId = process.env.AGENT_OPS_SESSION_ID;
|
|
205
|
+
const policyConfigHash = args.action === "create"
|
|
206
|
+
? calculateConfigHash((await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config)
|
|
207
|
+
: undefined;
|
|
182
208
|
return await runTaskCommand({
|
|
183
209
|
args,
|
|
184
210
|
service: taskService,
|
|
211
|
+
...(policyConfigHash === undefined ? {} : { policyConfigHash }),
|
|
185
212
|
...(sessionId === undefined ? {} : { sessionId })
|
|
186
213
|
});
|
|
187
214
|
}
|
|
@@ -199,6 +226,12 @@ else {
|
|
|
199
226
|
...(reviewConfig.reviewRoles === undefined
|
|
200
227
|
? {}
|
|
201
228
|
: { roles: reviewConfig.reviewRoles }),
|
|
229
|
+
root,
|
|
230
|
+
gitRunner: gitRunner(root),
|
|
231
|
+
policyConfigHash: calculateConfigHash(reviewConfig),
|
|
232
|
+
currentPolicyConfigHash: async () => calculateConfigHash((await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config),
|
|
233
|
+
config: reviewConfig,
|
|
234
|
+
evidenceStore: new FileEvidenceStore(root, root),
|
|
202
235
|
execute: createReviewExecutor({
|
|
203
236
|
targets: reviewRole?.targets ?? [],
|
|
204
237
|
cwd: root,
|
|
@@ -233,31 +266,12 @@ else {
|
|
|
233
266
|
root,
|
|
234
267
|
scope: args.scope === "user" ? "user" : "project",
|
|
235
268
|
config,
|
|
236
|
-
gitRunner:
|
|
237
|
-
run: async (gitArgs) => {
|
|
238
|
-
try {
|
|
239
|
-
return {
|
|
240
|
-
exitCode: 0,
|
|
241
|
-
stdout: execFileSync("git", [...gitArgs], {
|
|
242
|
-
cwd: root,
|
|
243
|
-
encoding: "buffer",
|
|
244
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
245
|
-
})
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
catch (error) {
|
|
249
|
-
const failure = error;
|
|
250
|
-
return {
|
|
251
|
-
exitCode: failure.status ?? 1,
|
|
252
|
-
stdout: failure.stdout ?? new Uint8Array()
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
},
|
|
269
|
+
gitRunner: gitRunner(root),
|
|
257
270
|
processRunner: new NodeVerificationProcessRunner(),
|
|
258
271
|
taskService,
|
|
259
272
|
evidenceStore: new FileEvidenceStore(root, root),
|
|
260
|
-
trusted: trustStatus === "TRUSTED"
|
|
273
|
+
trusted: trustStatus === "TRUSTED",
|
|
274
|
+
...(args.base === undefined ? {} : { base: args.base })
|
|
261
275
|
})
|
|
262
276
|
});
|
|
263
277
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { buildReviewPacket } from "../../../../runtime/src/review/packet.js";
|
|
2
2
|
import { runIndependentReview } from "../../../../runtime/src/review/runner.js";
|
|
3
|
+
import { renderReviewResult } from "../../../../runtime/src/review/render.js";
|
|
3
4
|
import { resolveReviewRole } from "../../../../runtime/src/review/roles.js";
|
|
5
|
+
import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
|
|
6
|
+
import { assertSafeSupportingPaths, isReviewerPolicyPath, resolveReviewScope, reviewScopeSignature } from "../../../../runtime/src/review/scope.js";
|
|
7
|
+
import { calculateSourceFingerprint } from "../../../../runtime/src/verify/source-fingerprint.js";
|
|
8
|
+
import { calculateConfigHash, isPassingVerificationEvidence } from "../../../../runtime/src/verify/evidence.js";
|
|
9
|
+
import { validateEvidence } from "../../../../runtime/src/schema/validate.js";
|
|
4
10
|
import { okEnvelope } from "../output.js";
|
|
5
11
|
/**
|
|
6
12
|
* Review runs against one target. Argument parsing already rejects a
|
|
@@ -53,10 +59,129 @@ async function taskContext(options) {
|
|
|
53
59
|
}
|
|
54
60
|
return {
|
|
55
61
|
taskId: record.task.id,
|
|
62
|
+
title: record.task.title,
|
|
56
63
|
active: record.status === "active",
|
|
64
|
+
policyConfigHash: record.policyConfigHash,
|
|
65
|
+
evidence: record.evidence,
|
|
66
|
+
failureFingerprint: record.failureFingerprint,
|
|
57
67
|
criteria
|
|
58
68
|
};
|
|
59
69
|
}
|
|
70
|
+
function newestEvidence(values) {
|
|
71
|
+
return [...values].sort((left, right) => {
|
|
72
|
+
const leftTime = Date.parse(left.evidence.finishedAt);
|
|
73
|
+
const rightTime = Date.parse(right.evidence.finishedAt);
|
|
74
|
+
return rightTime - leftTime || left.reference.localeCompare(right.reference);
|
|
75
|
+
})[0];
|
|
76
|
+
}
|
|
77
|
+
async function currentEvidence(options, context, criterionId, commandId, configHash, sourceFingerprint) {
|
|
78
|
+
const current = [];
|
|
79
|
+
let hasReference = false;
|
|
80
|
+
let unreadable = false;
|
|
81
|
+
let stale = false;
|
|
82
|
+
for (const reference of context.evidence[criterionId] ?? []) {
|
|
83
|
+
if (reference.startsWith("review:")) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
hasReference = true;
|
|
87
|
+
let stored;
|
|
88
|
+
try {
|
|
89
|
+
stored = await options.evidenceStore?.load(reference) ?? null;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
unreadable = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const validation = validateEvidence(stored);
|
|
96
|
+
if (!validation.ok) {
|
|
97
|
+
unreadable = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const evidence = validation.value;
|
|
101
|
+
if (evidence.schemaVersion !== 2 ||
|
|
102
|
+
evidence.taskId !== context.taskId ||
|
|
103
|
+
evidence.criterionId !== criterionId ||
|
|
104
|
+
evidence.commandId !== commandId ||
|
|
105
|
+
evidence.configHash !== configHash ||
|
|
106
|
+
evidence.sourceFingerprint !== sourceFingerprint) {
|
|
107
|
+
stale = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
current.push({ reference, evidence });
|
|
111
|
+
}
|
|
112
|
+
return { current, hasReference, unreadable, stale };
|
|
113
|
+
}
|
|
114
|
+
async function preflightReview(options, context, scope) {
|
|
115
|
+
if (options.config === undefined ||
|
|
116
|
+
options.evidenceStore === undefined ||
|
|
117
|
+
options.root === undefined ||
|
|
118
|
+
options.gitRunner === undefined ||
|
|
119
|
+
context.failureFingerprint !== null) {
|
|
120
|
+
return { ok: false, reason: "stale-verification" };
|
|
121
|
+
}
|
|
122
|
+
const configHash = calculateConfigHash(options.config);
|
|
123
|
+
const sourceFingerprint = await calculateSourceFingerprint(options.root, scope, options.gitRunner);
|
|
124
|
+
const commands = [];
|
|
125
|
+
for (const criterion of context.criteria) {
|
|
126
|
+
for (const commandId of criterion.verifierIds ?? []) {
|
|
127
|
+
const command = options.config.verification.commands.find((candidate) => candidate.id === commandId);
|
|
128
|
+
if (command === undefined) {
|
|
129
|
+
return { ok: false, reason: "missing-verification-evidence" };
|
|
130
|
+
}
|
|
131
|
+
const found = await currentEvidence(options, context, criterion.id, commandId, configHash, sourceFingerprint);
|
|
132
|
+
const selected = newestEvidence(found.current);
|
|
133
|
+
if (command.required !== true) {
|
|
134
|
+
if (selected !== undefined) {
|
|
135
|
+
commands.push({
|
|
136
|
+
criterionId: criterion.id,
|
|
137
|
+
commandId,
|
|
138
|
+
required: false,
|
|
139
|
+
status: selected.evidence.status,
|
|
140
|
+
evidenceReference: selected.reference
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (selected === undefined) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
reason: found.unreadable
|
|
149
|
+
? "unreadable-verification-evidence"
|
|
150
|
+
: found.stale
|
|
151
|
+
? "stale-verification"
|
|
152
|
+
: found.hasReference
|
|
153
|
+
? "missing-verification-evidence"
|
|
154
|
+
: "missing-verification-evidence"
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (!isPassingVerificationEvidence(command, selected.evidence)) {
|
|
158
|
+
return { ok: false, reason: "verification-not-passed" };
|
|
159
|
+
}
|
|
160
|
+
commands.push({
|
|
161
|
+
criterionId: criterion.id,
|
|
162
|
+
commandId,
|
|
163
|
+
required: true,
|
|
164
|
+
status: "PASS",
|
|
165
|
+
evidenceReference: selected.reference
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
summary: { status: "PASS", sourceFingerprint, commands }
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function scopeReason(error) {
|
|
175
|
+
if (!(error instanceof AgentOpsError)) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
REVIEW_UNSAFE_PATH: "unsafe-review-path",
|
|
180
|
+
REVIEW_NO_CHANGE_SURFACE: "no-change-surface",
|
|
181
|
+
REVIEW_DIRTY_WORKTREE: "dirty-worktree",
|
|
182
|
+
REVIEW_INVALID_BASE: "invalid-base"
|
|
183
|
+
}[error.code];
|
|
184
|
+
}
|
|
60
185
|
function notRunEnvelope(result) {
|
|
61
186
|
const message = "Independent review was not run.";
|
|
62
187
|
return {
|
|
@@ -65,11 +190,28 @@ function notRunEnvelope(result) {
|
|
|
65
190
|
data: {
|
|
66
191
|
message,
|
|
67
192
|
result,
|
|
68
|
-
text:
|
|
193
|
+
text: renderReviewResult(result)
|
|
69
194
|
},
|
|
70
195
|
errors: [{ code: "REVIEW_NOT_RUN", message }]
|
|
71
196
|
};
|
|
72
197
|
}
|
|
198
|
+
function sourceChangedResult(result) {
|
|
199
|
+
return {
|
|
200
|
+
status: "NOT_RUN",
|
|
201
|
+
reason: "source-changed-during-review",
|
|
202
|
+
harness: result.harness,
|
|
203
|
+
model: result.model,
|
|
204
|
+
effort: result.effort,
|
|
205
|
+
prompt: result.prompt,
|
|
206
|
+
...(result.scope === undefined ? {} : { scope: result.scope }),
|
|
207
|
+
...(result.independence === undefined
|
|
208
|
+
? {}
|
|
209
|
+
: { independence: result.independence }),
|
|
210
|
+
...(result.verification === undefined
|
|
211
|
+
? {}
|
|
212
|
+
: { verification: result.verification })
|
|
213
|
+
};
|
|
214
|
+
}
|
|
73
215
|
export async function runReviewCommand(options) {
|
|
74
216
|
const role = resolveReviewRole(options.role ?? "independent-review", options.roles ?? []);
|
|
75
217
|
const selectedHarness = harness(options.args.harness);
|
|
@@ -82,7 +224,7 @@ export async function runReviewCommand(options) {
|
|
|
82
224
|
requirement: separator < 0 ? value : value.slice(separator + 1)
|
|
83
225
|
};
|
|
84
226
|
});
|
|
85
|
-
if (
|
|
227
|
+
if (context === undefined) {
|
|
86
228
|
return notRunEnvelope({
|
|
87
229
|
status: "NOT_RUN",
|
|
88
230
|
reason: "no-task-context",
|
|
@@ -92,20 +234,105 @@ export async function runReviewCommand(options) {
|
|
|
92
234
|
prompt: ""
|
|
93
235
|
});
|
|
94
236
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
237
|
+
let scope;
|
|
238
|
+
let verification;
|
|
239
|
+
if (options.root !== undefined && options.gitRunner !== undefined) {
|
|
240
|
+
try {
|
|
241
|
+
scope = await resolveReviewScope({
|
|
242
|
+
root: options.root,
|
|
243
|
+
runner: options.gitRunner,
|
|
244
|
+
...(options.args.base === undefined ? {} : { base: options.args.base })
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
const reason = scopeReason(error);
|
|
249
|
+
if (reason !== undefined) {
|
|
250
|
+
return notRunEnvelope({
|
|
251
|
+
status: "NOT_RUN",
|
|
252
|
+
reason,
|
|
253
|
+
harness: target,
|
|
254
|
+
model: role?.model ?? options.model ?? "configured",
|
|
255
|
+
effort: role?.effort ?? options.effort ?? "configured",
|
|
256
|
+
prompt: ""
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
if (scope.changedFiles.some(isReviewerPolicyPath)) {
|
|
262
|
+
return notRunEnvelope({
|
|
263
|
+
status: "NOT_RUN",
|
|
264
|
+
reason: "reviewer-policy-changed",
|
|
265
|
+
harness: target,
|
|
266
|
+
model: role?.model ?? options.model ?? "configured",
|
|
267
|
+
effort: role?.effort ?? options.effort ?? "configured",
|
|
268
|
+
prompt: "",
|
|
269
|
+
scope
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (options.policyConfigHash !== undefined) {
|
|
273
|
+
if (context.policyConfigHash === null) {
|
|
274
|
+
return notRunEnvelope({
|
|
275
|
+
status: "NOT_RUN", reason: "reviewer-policy-baseline-missing",
|
|
276
|
+
harness: target, model: role?.model ?? options.model ?? "configured",
|
|
277
|
+
effort: role?.effort ?? options.effort ?? "configured", prompt: "", scope
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (context.policyConfigHash !== options.policyConfigHash) {
|
|
281
|
+
return notRunEnvelope({
|
|
282
|
+
status: "NOT_RUN", reason: "reviewer-policy-changed",
|
|
283
|
+
harness: target, model: role?.model ?? options.model ?? "configured",
|
|
284
|
+
effort: role?.effort ?? options.effort ?? "configured", prompt: "", scope
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const preflight = await preflightReview(options, context, scope);
|
|
289
|
+
if (!preflight.ok) {
|
|
290
|
+
return notRunEnvelope({
|
|
291
|
+
status: "NOT_RUN", reason: preflight.reason,
|
|
292
|
+
harness: target, model: role?.model ?? options.model ?? "configured",
|
|
293
|
+
effort: role?.effort ?? options.effort ?? "configured", prompt: "", scope
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
verification = preflight.summary;
|
|
297
|
+
}
|
|
298
|
+
const criteria = [...context.criteria];
|
|
299
|
+
let packet;
|
|
300
|
+
try {
|
|
301
|
+
packet = buildReviewPacket({
|
|
302
|
+
request: context.title,
|
|
303
|
+
criteria,
|
|
304
|
+
artifactRefs: scope?.changedFiles ?? [],
|
|
305
|
+
evidenceRequirements
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
if (error instanceof AgentOpsError) {
|
|
310
|
+
const reason = error.code === "REVIEW_SENSITIVE_INPUT"
|
|
311
|
+
? "sensitive-review-input"
|
|
312
|
+
: error.code === "REVIEW_SCOPE_TOO_LARGE"
|
|
313
|
+
? "scope-too-large"
|
|
314
|
+
: undefined;
|
|
315
|
+
if (reason !== undefined) {
|
|
316
|
+
return notRunEnvelope({
|
|
317
|
+
status: "NOT_RUN",
|
|
318
|
+
reason,
|
|
319
|
+
harness: target,
|
|
320
|
+
model: role?.model ?? options.model ?? "configured",
|
|
321
|
+
effort: role?.effort ?? options.effort ?? "configured",
|
|
322
|
+
prompt: ""
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
98
328
|
const result = await runIndependentReview({
|
|
99
329
|
invocation: {
|
|
100
330
|
harness: target,
|
|
101
331
|
model: role?.model ?? options.model ?? "configured",
|
|
102
332
|
effort: role?.effort ?? options.effort ?? "configured",
|
|
103
|
-
packet
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
artifactRefs: [],
|
|
107
|
-
evidenceRequirements
|
|
108
|
-
})
|
|
333
|
+
packet,
|
|
334
|
+
...(scope === undefined ? {} : { scope }),
|
|
335
|
+
...(verification === undefined ? {} : { verification })
|
|
109
336
|
},
|
|
110
337
|
authorized: options.authorized,
|
|
111
338
|
execute: options.execute ?? (async () => ({
|
|
@@ -113,15 +340,60 @@ export async function runReviewCommand(options) {
|
|
|
113
340
|
reason: "missing-cli"
|
|
114
341
|
}))
|
|
115
342
|
});
|
|
343
|
+
if (scope !== undefined && result.report !== undefined && options.root !== undefined) {
|
|
344
|
+
try {
|
|
345
|
+
if (result.report.changedFilesInspected.length !== scope.changedFiles.length ||
|
|
346
|
+
result.report.changedFilesInspected.some((path) => !scope?.changedFiles.includes(path))) {
|
|
347
|
+
return notRunEnvelope({
|
|
348
|
+
...result,
|
|
349
|
+
status: "NOT_RUN",
|
|
350
|
+
reason: "incomplete-scope"
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
await assertSafeSupportingPaths(options.root, result.report.supportingFilesInspected);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (scopeReason(error) === "unsafe-review-path") {
|
|
358
|
+
return notRunEnvelope({
|
|
359
|
+
...result,
|
|
360
|
+
status: "NOT_RUN",
|
|
361
|
+
reason: "unsafe-review-path"
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
const postflight = await resolveReviewScope({
|
|
367
|
+
root: options.root,
|
|
368
|
+
runner: options.gitRunner,
|
|
369
|
+
...(options.args.base === undefined ? {} : { base: options.args.base })
|
|
370
|
+
});
|
|
371
|
+
const currentHash = options.currentPolicyConfigHash === undefined
|
|
372
|
+
? options.policyConfigHash
|
|
373
|
+
: await options.currentPolicyConfigHash();
|
|
374
|
+
const postflightFingerprint = verification === undefined
|
|
375
|
+
? undefined
|
|
376
|
+
: await calculateSourceFingerprint(options.root, postflight, options.gitRunner);
|
|
377
|
+
if (reviewScopeSignature(scope) !== reviewScopeSignature(postflight) ||
|
|
378
|
+
(options.policyConfigHash !== undefined && currentHash !== options.policyConfigHash) ||
|
|
379
|
+
(verification !== undefined &&
|
|
380
|
+
postflightFingerprint !== verification.sourceFingerprint)) {
|
|
381
|
+
return notRunEnvelope(sourceChangedResult(result));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return notRunEnvelope(sourceChangedResult(result));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
116
388
|
// Evidence is only appended while the task is active: a completed record
|
|
117
389
|
// must stay exactly as it was verified.
|
|
118
390
|
if (options.tasks !== undefined &&
|
|
119
|
-
|
|
391
|
+
result.status === "PASS" &&
|
|
120
392
|
context.active &&
|
|
121
393
|
result.results !== undefined) {
|
|
122
394
|
await options.tasks.recordEvidence(context.taskId, Object.fromEntries(result.results.map((item) => [
|
|
123
395
|
item.criterionId,
|
|
124
|
-
item.evidence.map((reference) => `review:${
|
|
396
|
+
item.evidence.map((reference) => `review:${result.harness}:${reference}`)
|
|
125
397
|
])));
|
|
126
398
|
}
|
|
127
399
|
const message = result.status === "PASS"
|
|
@@ -132,22 +404,7 @@ export async function runReviewCommand(options) {
|
|
|
132
404
|
const data = {
|
|
133
405
|
message,
|
|
134
406
|
result,
|
|
135
|
-
text:
|
|
136
|
-
message,
|
|
137
|
-
`Status: ${result.status}`,
|
|
138
|
-
`Harness: ${result.harness}; model: ${result.model}; effort: ${result.effort}.`,
|
|
139
|
-
...(result.reason === undefined ? [] : [`Reason: ${result.reason}.`]),
|
|
140
|
-
...(result.status === "NOT_RUN"
|
|
141
|
-
? [
|
|
142
|
-
"Run: agent-ops doctor --check-auth to verify target authentication."
|
|
143
|
-
]
|
|
144
|
-
: []),
|
|
145
|
-
...(result.results === undefined
|
|
146
|
-
? []
|
|
147
|
-
: result.results.map((item) => `${item.criterionId}: ${item.status} [${item.evidence.join(", ")}]`)),
|
|
148
|
-
result.prompt,
|
|
149
|
-
""
|
|
150
|
-
].join("\n")
|
|
407
|
+
text: renderReviewResult(result)
|
|
151
408
|
};
|
|
152
409
|
if (result.status === "PASS") {
|
|
153
410
|
return okEnvelope("REVIEW_RESULT", data);
|
|
@@ -89,7 +89,10 @@ export async function runTaskCommand(options) {
|
|
|
89
89
|
}
|
|
90
90
|
const record = await options.service.create({
|
|
91
91
|
title: options.args.title,
|
|
92
|
-
criteria: (options.args.criteria ?? []).map(parseCriterion)
|
|
92
|
+
criteria: (options.args.criteria ?? []).map(parseCriterion),
|
|
93
|
+
...(options.policyConfigHash === undefined
|
|
94
|
+
? {}
|
|
95
|
+
: { policyConfigHash: options.policyConfigHash })
|
|
93
96
|
});
|
|
94
97
|
return taskEnvelope(action, "TASK_CREATED", `Created task ${record.task.id}.`, record);
|
|
95
98
|
}
|
|
@@ -41,7 +41,19 @@ function publicReport(report) {
|
|
|
41
41
|
testCount: result.testCount,
|
|
42
42
|
evidenceReferences: result.evidenceReferences.map(redactSecrets)
|
|
43
43
|
})),
|
|
44
|
-
signal: report.signal
|
|
44
|
+
signal: report.signal,
|
|
45
|
+
reviewScope: report.reviewScope.mode === "base"
|
|
46
|
+
? {
|
|
47
|
+
mode: "base",
|
|
48
|
+
baseRef: redactSecrets(report.reviewScope.baseRef),
|
|
49
|
+
resolvedBase: report.reviewScope.resolvedBase,
|
|
50
|
+
changedFiles: report.reviewScope.changedFiles.map(redactSecrets)
|
|
51
|
+
}
|
|
52
|
+
: {
|
|
53
|
+
mode: "worktree",
|
|
54
|
+
changedFiles: report.reviewScope.changedFiles.map(redactSecrets)
|
|
55
|
+
},
|
|
56
|
+
sourceFingerprint: report.sourceFingerprint
|
|
45
57
|
};
|
|
46
58
|
}
|
|
47
59
|
function formatResult(result) {
|
|
@@ -10,10 +10,10 @@ const REVIEW_TARGET_CHOICES = DEFAULT_REVIEW_TARGETS.map((id) => ({
|
|
|
10
10
|
label: id,
|
|
11
11
|
value: id,
|
|
12
12
|
description: id === "codex"
|
|
13
|
-
? "
|
|
13
|
+
? "Retained for future isolation support; currently not auto-run."
|
|
14
14
|
: id === "agy"
|
|
15
|
-
? "
|
|
16
|
-
: "Runs
|
|
15
|
+
? "Retained for future isolation support; currently not auto-run."
|
|
16
|
+
: "Runs in fresh safe mode with context isolation."
|
|
17
17
|
}));
|
|
18
18
|
function selectReviewTargets(raw) {
|
|
19
19
|
const values = raw
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const CONFIG_SCHEMA_VERSION = 2;
|
|
2
2
|
export const TASK_SCHEMA_VERSION = 1;
|
|
3
|
-
export const EVIDENCE_SCHEMA_VERSION =
|
|
3
|
+
export const EVIDENCE_SCHEMA_VERSION = 2;
|
|
4
4
|
/** @deprecated Use the document-specific schema version constants. */
|
|
5
5
|
export const SCHEMA_VERSION = CONFIG_SCHEMA_VERSION;
|
|
6
6
|
/**
|