@kylecheng3146/agent-ops 0.1.6 → 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 +27 -0
- package/dist/packages/cli/src/args.js +47 -0
- package/dist/packages/cli/src/bin.js +74 -25
- package/dist/packages/cli/src/cli.js +13 -1
- package/dist/packages/cli/src/commands/init.js +4 -1
- package/dist/packages/cli/src/commands/review.js +371 -27
- 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/version.js +1 -1
- package/dist/packages/cli/src/wizard.js +62 -3
- package/dist/runtime/src/config/merge.js +17 -2
- package/dist/runtime/src/contracts.js +1 -1
- package/dist/runtime/src/install/doctor.js +42 -1
- package/dist/runtime/src/install/plan.js +11 -5
- package/dist/runtime/src/review/execute.js +180 -0
- package/dist/runtime/src/review/extract.js +69 -0
- package/dist/runtime/src/review/invocation.js +116 -0
- package/dist/runtime/src/review/packet.js +42 -5
- package/dist/runtime/src/review/probe.js +72 -0
- package/dist/runtime/src/review/render.js +62 -0
- package/dist/runtime/src/review/report.js +183 -0
- package/dist/runtime/src/review/result.js +2 -2
- package/dist/runtime/src/review/roles.js +35 -0
- package/dist/runtime/src/review/runner.js +98 -12
- package/dist/runtime/src/review/scope.js +123 -0
- package/dist/runtime/src/schema/validate.js +80 -0
- package/dist/runtime/src/task/service.js +46 -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 +68 -0
- package/docs/en/spec/review.md +37 -4
- package/docs/zh-TW/guides/configuration.md +60 -0
- package/docs/zh-TW/spec/review.md +33 -3
- package/package.json +1 -1
- package/schemas/config.schema.json +29 -0
- package/schemas/evidence.schema.json +16 -1
- package/schemas/review-report.schema.json +48 -0
|
@@ -1,20 +1,222 @@
|
|
|
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
|
-
* Review runs against one
|
|
12
|
+
* Review runs against one target. Argument parsing already rejects a
|
|
7
13
|
* multi-harness selection here, so the first entry is the whole selection.
|
|
14
|
+
* `opencode` is not a review target — it has no read-only flag — so selecting
|
|
15
|
+
* it leaves the target unresolved and the configured chain decides.
|
|
8
16
|
*/
|
|
9
17
|
function harness(value) {
|
|
10
|
-
|
|
18
|
+
const selected = value?.[0];
|
|
19
|
+
return selected === undefined || selected === "opencode"
|
|
20
|
+
? undefined
|
|
21
|
+
: selected;
|
|
11
22
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Criterion descriptions come from the task store, never from the id. A
|
|
25
|
+
* reviewer handed `criterion: tests` cannot review anything, so a review with
|
|
26
|
+
* no task context is reported as not run rather than run meaninglessly.
|
|
27
|
+
*/
|
|
28
|
+
async function taskContext(options) {
|
|
29
|
+
const tasks = options.tasks;
|
|
30
|
+
if (tasks === undefined) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const query = options.taskId !== undefined
|
|
34
|
+
? { taskId: options.taskId }
|
|
35
|
+
: options.sessionId === undefined
|
|
36
|
+
? undefined
|
|
37
|
+
: { sessionId: options.sessionId };
|
|
38
|
+
if (query === undefined) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
let record;
|
|
42
|
+
try {
|
|
43
|
+
record = await tasks.status(query);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const requested = options.args.criteria ?? [];
|
|
49
|
+
const criteria = record.task.criteria
|
|
50
|
+
.filter((criterion) => requested.length === 0 || requested.includes(criterion.id))
|
|
51
|
+
.map((criterion) => ({
|
|
52
|
+
id: criterion.id,
|
|
53
|
+
description: criterion.description,
|
|
54
|
+
verifierIds: [...criterion.verifierIds]
|
|
17
55
|
}));
|
|
56
|
+
if (criteria.length === 0 ||
|
|
57
|
+
(requested.length > 0 && criteria.length !== requested.length)) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
taskId: record.task.id,
|
|
62
|
+
title: record.task.title,
|
|
63
|
+
active: record.status === "active",
|
|
64
|
+
policyConfigHash: record.policyConfigHash,
|
|
65
|
+
evidence: record.evidence,
|
|
66
|
+
failureFingerprint: record.failureFingerprint,
|
|
67
|
+
criteria
|
|
68
|
+
};
|
|
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
|
+
}
|
|
185
|
+
function notRunEnvelope(result) {
|
|
186
|
+
const message = "Independent review was not run.";
|
|
187
|
+
return {
|
|
188
|
+
code: "REVIEW_NOT_RUN",
|
|
189
|
+
status: "error",
|
|
190
|
+
data: {
|
|
191
|
+
message,
|
|
192
|
+
result,
|
|
193
|
+
text: renderReviewResult(result)
|
|
194
|
+
},
|
|
195
|
+
errors: [{ code: "REVIEW_NOT_RUN", message }]
|
|
196
|
+
};
|
|
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
|
+
}
|
|
215
|
+
export async function runReviewCommand(options) {
|
|
216
|
+
const role = resolveReviewRole(options.role ?? "independent-review", options.roles ?? []);
|
|
217
|
+
const selectedHarness = harness(options.args.harness);
|
|
218
|
+
const target = role?.targets[0] ?? selectedHarness ?? "codex";
|
|
219
|
+
const context = await taskContext(options);
|
|
18
220
|
const evidenceRequirements = (options.args.evidence ?? []).map((value) => {
|
|
19
221
|
const separator = value.indexOf("=");
|
|
20
222
|
return {
|
|
@@ -22,19 +224,115 @@ export async function runReviewCommand(options) {
|
|
|
22
224
|
requirement: separator < 0 ? value : value.slice(separator + 1)
|
|
23
225
|
};
|
|
24
226
|
});
|
|
25
|
-
|
|
26
|
-
|
|
227
|
+
if (context === undefined) {
|
|
228
|
+
return notRunEnvelope({
|
|
229
|
+
status: "NOT_RUN",
|
|
230
|
+
reason: "no-task-context",
|
|
231
|
+
harness: target,
|
|
232
|
+
model: role?.model ?? options.model ?? "configured",
|
|
233
|
+
effort: role?.effort ?? options.effort ?? "configured",
|
|
234
|
+
prompt: ""
|
|
235
|
+
});
|
|
236
|
+
}
|
|
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
|
+
}
|
|
27
328
|
const result = await runIndependentReview({
|
|
28
329
|
invocation: {
|
|
29
|
-
harness:
|
|
330
|
+
harness: target,
|
|
30
331
|
model: role?.model ?? options.model ?? "configured",
|
|
31
332
|
effort: role?.effort ?? options.effort ?? "configured",
|
|
32
|
-
packet
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
artifactRefs: [],
|
|
36
|
-
evidenceRequirements
|
|
37
|
-
})
|
|
333
|
+
packet,
|
|
334
|
+
...(scope === undefined ? {} : { scope }),
|
|
335
|
+
...(verification === undefined ? {} : { verification })
|
|
38
336
|
},
|
|
39
337
|
authorized: options.authorized,
|
|
40
338
|
execute: options.execute ?? (async () => ({
|
|
@@ -42,6 +340,62 @@ export async function runReviewCommand(options) {
|
|
|
42
340
|
reason: "missing-cli"
|
|
43
341
|
}))
|
|
44
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
|
+
}
|
|
388
|
+
// Evidence is only appended while the task is active: a completed record
|
|
389
|
+
// must stay exactly as it was verified.
|
|
390
|
+
if (options.tasks !== undefined &&
|
|
391
|
+
result.status === "PASS" &&
|
|
392
|
+
context.active &&
|
|
393
|
+
result.results !== undefined) {
|
|
394
|
+
await options.tasks.recordEvidence(context.taskId, Object.fromEntries(result.results.map((item) => [
|
|
395
|
+
item.criterionId,
|
|
396
|
+
item.evidence.map((reference) => `review:${result.harness}:${reference}`)
|
|
397
|
+
])));
|
|
398
|
+
}
|
|
45
399
|
const message = result.status === "PASS"
|
|
46
400
|
? "Independent review passed."
|
|
47
401
|
: result.status === "FAIL"
|
|
@@ -50,17 +404,7 @@ export async function runReviewCommand(options) {
|
|
|
50
404
|
const data = {
|
|
51
405
|
message,
|
|
52
406
|
result,
|
|
53
|
-
text:
|
|
54
|
-
message,
|
|
55
|
-
`Status: ${result.status}`,
|
|
56
|
-
`Harness: ${result.harness}; model: ${result.model}; effort: ${result.effort}.`,
|
|
57
|
-
...(result.reason === undefined ? [] : [`Reason: ${result.reason}.`]),
|
|
58
|
-
...(result.results === undefined
|
|
59
|
-
? []
|
|
60
|
-
: result.results.map((item) => `${item.criterionId}: ${item.status} [${item.evidence.join(", ")}]`)),
|
|
61
|
-
result.prompt,
|
|
62
|
-
""
|
|
63
|
-
].join("\n")
|
|
407
|
+
text: renderReviewResult(result)
|
|
64
408
|
};
|
|
65
409
|
if (result.status === "PASS") {
|
|
66
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) {
|
|
@@ -1,9 +1,49 @@
|
|
|
1
1
|
import { CliArgumentError } from "./args.js";
|
|
2
|
+
import { DEFAULT_REVIEW_TARGETS } from "../../../runtime/src/review/roles.js";
|
|
2
3
|
import { HARNESS_IDS, resolveHarnessSelection } from "../../../runtime/src/install/harness.js";
|
|
3
4
|
import { selectOption, selectOptions } from "./ui.js";
|
|
4
5
|
const SCOPES = new Set(["project", "user"]);
|
|
5
6
|
const PROFILES = new Set(["advisory", "core", "guardrails", "loop"]);
|
|
6
7
|
const DEFAULT_HARNESS = [];
|
|
8
|
+
const REVIEW_TARGET_SET = new Set(DEFAULT_REVIEW_TARGETS);
|
|
9
|
+
const REVIEW_TARGET_CHOICES = DEFAULT_REVIEW_TARGETS.map((id) => ({
|
|
10
|
+
label: id,
|
|
11
|
+
value: id,
|
|
12
|
+
description: id === "codex"
|
|
13
|
+
? "Retained for future isolation support; currently not auto-run."
|
|
14
|
+
: id === "agy"
|
|
15
|
+
? "Retained for future isolation support; currently not auto-run."
|
|
16
|
+
: "Runs in fresh safe mode with context isolation."
|
|
17
|
+
}));
|
|
18
|
+
function selectReviewTargets(raw) {
|
|
19
|
+
const values = raw
|
|
20
|
+
.split(",")
|
|
21
|
+
.map((value) => value.trim())
|
|
22
|
+
.filter((value) => value.length > 0);
|
|
23
|
+
for (const value of values) {
|
|
24
|
+
if (!REVIEW_TARGET_SET.has(value)) {
|
|
25
|
+
throw new CliArgumentError("CLI_INVALID_VALUE", `Invalid review target: ${value}`, "--review-target");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Declared order wins: the chain order is the option list, not click order.
|
|
29
|
+
return DEFAULT_REVIEW_TARGETS.filter((target) => values.includes(target));
|
|
30
|
+
}
|
|
31
|
+
function affirmative(raw) {
|
|
32
|
+
return /^(y|yes)$/i.test(raw.trim());
|
|
33
|
+
}
|
|
34
|
+
async function probeReviewTargets(targets, setup) {
|
|
35
|
+
const probe = setup.probeReviewTarget;
|
|
36
|
+
if (probe === undefined) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
for (const target of targets) {
|
|
40
|
+
if (!(await probe(target))) {
|
|
41
|
+
setup.warn?.(`${target} is not usable yet (missing or unauthenticated). ` +
|
|
42
|
+
`Install it or run: ${target} login, ` +
|
|
43
|
+
"then: agent-ops doctor --check-auth");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
7
47
|
const SCOPE_CHOICES = [
|
|
8
48
|
{ label: "project", value: "project" },
|
|
9
49
|
{ label: "user", value: "user" }
|
|
@@ -83,7 +123,7 @@ function selectProfiles(raw) {
|
|
|
83
123
|
}
|
|
84
124
|
return values;
|
|
85
125
|
}
|
|
86
|
-
export async function completeInitChoices(args, io) {
|
|
126
|
+
export async function completeInitChoices(args, io, setup = {}) {
|
|
87
127
|
if (args.command !== "init" ||
|
|
88
128
|
(args.scope !== undefined &&
|
|
89
129
|
args.harness !== undefined &&
|
|
@@ -117,11 +157,25 @@ export async function completeInitChoices(args, io) {
|
|
|
117
157
|
selectAllLabel: "Select all",
|
|
118
158
|
selectAllDescription: "Enable core, advisory, guardrails, and loop together."
|
|
119
159
|
});
|
|
160
|
+
const enabled = args.reviewTargets !== undefined ||
|
|
161
|
+
(await selectOption("External review: call another agent CLI to review your work?", [
|
|
162
|
+
{ label: "no", value: false, description: "Default. Nothing is spawned." },
|
|
163
|
+
{
|
|
164
|
+
label: "yes",
|
|
165
|
+
value: true,
|
|
166
|
+
description: "Pick target CLIs; each is probed for authentication."
|
|
167
|
+
}
|
|
168
|
+
], selectorIo));
|
|
169
|
+
const reviewTargets = args.reviewTargets ?? (enabled
|
|
170
|
+
? await selectOptions("Review targets (multi-select: tried in listed order)", REVIEW_TARGET_CHOICES, selectorIo, [])
|
|
171
|
+
: []);
|
|
172
|
+
await probeReviewTargets(reviewTargets, setup);
|
|
120
173
|
return {
|
|
121
174
|
...args,
|
|
122
175
|
scope,
|
|
123
176
|
harness,
|
|
124
|
-
profiles
|
|
177
|
+
profiles,
|
|
178
|
+
...(reviewTargets.length === 0 ? {} : { reviewTargets })
|
|
125
179
|
};
|
|
126
180
|
}
|
|
127
181
|
const session = await createPromptSession(io);
|
|
@@ -133,11 +187,16 @@ export async function completeInitChoices(args, io) {
|
|
|
133
187
|
const profiles = args.profiles.length > 0
|
|
134
188
|
? args.profiles
|
|
135
189
|
: selectProfiles(await session.question("Profiles (core,advisory,guardrails,loop) [core]: "));
|
|
190
|
+
const reviewTargets = args.reviewTargets ?? (affirmative(await session.question("Enable external review by another agent CLI? [y/N]: "))
|
|
191
|
+
? selectReviewTargets(await session.question(`Review targets (${DEFAULT_REVIEW_TARGETS.join(",")}): `))
|
|
192
|
+
: []);
|
|
193
|
+
await probeReviewTargets(reviewTargets, setup);
|
|
136
194
|
return {
|
|
137
195
|
...args,
|
|
138
196
|
scope,
|
|
139
197
|
harness,
|
|
140
|
-
profiles
|
|
198
|
+
profiles,
|
|
199
|
+
...(reviewTargets.length === 0 ? {} : { reviewTargets })
|
|
141
200
|
};
|
|
142
201
|
}
|
|
143
202
|
finally {
|
|
@@ -107,6 +107,7 @@ export function mergeConfigLayers(inputLayers) {
|
|
|
107
107
|
const commands = new Map();
|
|
108
108
|
const mappings = new Map();
|
|
109
109
|
const exceptions = new Map();
|
|
110
|
+
const reviewRoles = new Map();
|
|
110
111
|
let schemaVersion;
|
|
111
112
|
let features;
|
|
112
113
|
for (const layer of layers) {
|
|
@@ -128,6 +129,12 @@ export function mergeConfigLayers(inputLayers) {
|
|
|
128
129
|
}
|
|
129
130
|
mappings.set(key, effective(mapping, layer));
|
|
130
131
|
}
|
|
132
|
+
// Keyed by role so a project can override one role without inheriting the
|
|
133
|
+
// rest. Review targets are a capability choice, not a guardrail, so no
|
|
134
|
+
// monotonic restriction applies.
|
|
135
|
+
for (const reviewRole of layer.config.reviewRoles ?? []) {
|
|
136
|
+
reviewRoles.set(reviewRole.role, effective(reviewRole, layer));
|
|
137
|
+
}
|
|
131
138
|
for (const securityException of layer.config.securityExceptions) {
|
|
132
139
|
const key = exceptionKey(securityException);
|
|
133
140
|
const existing = exceptions.get(key);
|
|
@@ -150,7 +157,8 @@ export function mergeConfigLayers(inputLayers) {
|
|
|
150
157
|
profiles: [...profiles.values()],
|
|
151
158
|
verificationCommands: [...commands.values()],
|
|
152
159
|
pathMappings: [...mappings.values()],
|
|
153
|
-
securityExceptions: [...exceptions.values()]
|
|
160
|
+
securityExceptions: [...exceptions.values()],
|
|
161
|
+
reviewRoles: [...reviewRoles.values()]
|
|
154
162
|
};
|
|
155
163
|
const config = {
|
|
156
164
|
schemaVersion: schemaVersion.value,
|
|
@@ -160,7 +168,14 @@ export function mergeConfigLayers(inputLayers) {
|
|
|
160
168
|
},
|
|
161
169
|
features: provenance.features.value,
|
|
162
170
|
pathMappings: provenance.pathMappings.map(({ value }) => value),
|
|
163
|
-
securityExceptions: provenance.securityExceptions.map(({ value }) => value)
|
|
171
|
+
securityExceptions: provenance.securityExceptions.map(({ value }) => value),
|
|
172
|
+
// Absent, not empty: an empty array would read as "configured with no
|
|
173
|
+
// targets" rather than "external review disabled".
|
|
174
|
+
...(provenance.reviewRoles.length === 0
|
|
175
|
+
? {}
|
|
176
|
+
: {
|
|
177
|
+
reviewRoles: provenance.reviewRoles.map(({ value }) => value)
|
|
178
|
+
})
|
|
164
179
|
};
|
|
165
180
|
const validation = validateConfig(config);
|
|
166
181
|
if (!validation.ok) {
|
|
@@ -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
|
/**
|