@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
|
@@ -3,6 +3,7 @@ import { EVIDENCE_SCHEMA_VERSION } from "../contracts.js";
|
|
|
3
3
|
import { sha256 } from "../fs/hash.js";
|
|
4
4
|
import { AgentOpsError } from "../fs/paths.js";
|
|
5
5
|
import { calculateConfigHash } from "../config/hash.js";
|
|
6
|
+
import { evaluateTestCount } from "./test-count.js";
|
|
6
7
|
export { calculateConfigHash } from "../config/hash.js";
|
|
7
8
|
import { validateEvidence } from "../schema/validate.js";
|
|
8
9
|
import { readPrivateFile, writePrivateFile } from "../security/permissions.js";
|
|
@@ -34,10 +35,24 @@ export function buildVerificationEvidence(input) {
|
|
|
34
35
|
finishedAt: input.finishedAt,
|
|
35
36
|
exitCode: input.exitCode,
|
|
36
37
|
testCount: input.testCount,
|
|
38
|
+
status: input.status,
|
|
39
|
+
failureClass: redactSecrets(input.failureClass),
|
|
40
|
+
sourceFingerprint: input.sourceFingerprint,
|
|
37
41
|
toolVersions: redactRecord(input.toolVersions),
|
|
38
42
|
configHash: calculateConfigHash(input.config)
|
|
39
43
|
});
|
|
40
44
|
}
|
|
45
|
+
/** Confirms that a persisted PASS still matches the current command contract. */
|
|
46
|
+
export function isPassingVerificationEvidence(command, evidence) {
|
|
47
|
+
if (evidence.status !== "PASS" ||
|
|
48
|
+
evidence.exitCode !== 0 ||
|
|
49
|
+
evidence.failureClass !== "none" ||
|
|
50
|
+
command.evidence.kind === "file") {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return command.evidence.kind !== "test-count" ||
|
|
54
|
+
evaluateTestCount(evidence.testCount, command.evidence.minimum).status === "PASS";
|
|
55
|
+
}
|
|
41
56
|
export class FileEvidenceStore {
|
|
42
57
|
#root;
|
|
43
58
|
#anchorDirectory;
|
|
@@ -66,4 +81,25 @@ export class FileEvidenceStore {
|
|
|
66
81
|
}
|
|
67
82
|
return relativePath;
|
|
68
83
|
}
|
|
84
|
+
async load(reference) {
|
|
85
|
+
const segments = reference.split("/");
|
|
86
|
+
if (!reference.startsWith(".agent-ops/tasks/evidence/") ||
|
|
87
|
+
segments.some((segment) => segment.length === 0 ||
|
|
88
|
+
segment === "." ||
|
|
89
|
+
segment === ".." ||
|
|
90
|
+
!/^[A-Za-z0-9._-]+$/u.test(segment))) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const path = join(this.#root, ...segments);
|
|
94
|
+
const source = await readPrivateFile(path, this.#anchorDirectory);
|
|
95
|
+
if (source === null) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(source);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
throw new AgentOpsError("EVIDENCE_INVALID", "Stored evidence is not valid JSON.");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
69
105
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { AgentOpsError } from "../fs/paths.js";
|
|
2
1
|
function sortedUnique(values) {
|
|
3
2
|
return [...new Set(values)].sort();
|
|
4
3
|
}
|
|
@@ -18,7 +17,7 @@ function sameVerifierIds(left, right) {
|
|
|
18
17
|
}
|
|
19
18
|
function fallbackSelection(reason, evidence) {
|
|
20
19
|
if (evidence.requiredVerifierIds.length === 0) {
|
|
21
|
-
|
|
20
|
+
return { verifierIds: [], fallback: true, reason, evidence };
|
|
22
21
|
}
|
|
23
22
|
return {
|
|
24
23
|
verifierIds: evidence.requiredVerifierIds,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { AgentOpsError, resolveContainedPath } from "../fs/paths.js";
|
|
2
2
|
import { validateTaskAgainstConfig } from "../schema/validate.js";
|
|
3
3
|
import { collectChangeSurface } from "./change-surface.js";
|
|
4
|
+
import { resolveReviewScope, reviewScopeSignature } from "../review/scope.js";
|
|
5
|
+
import { calculateSourceFingerprint } from "./source-fingerprint.js";
|
|
4
6
|
import { buildVerificationEvidence } from "./evidence.js";
|
|
5
7
|
import { createFailureFingerprint } from "./fingerprint.js";
|
|
6
8
|
import { aggregateVerificationStatus, executeConfiguredCommand } from "./command-executor.js";
|
|
@@ -41,7 +43,7 @@ export class VerificationService {
|
|
|
41
43
|
}
|
|
42
44
|
return await resolveContainedPath(this.#options.root, command.cwd);
|
|
43
45
|
}
|
|
44
|
-
async #persistEvidence(task, command, startedAt, finishedAt, result,
|
|
46
|
+
async #persistEvidence(task, command, startedAt, finishedAt, result, sourceFingerprint) {
|
|
45
47
|
const references = [];
|
|
46
48
|
for (const criterion of relevantCriteria(task, command.id)) {
|
|
47
49
|
const evidence = buildVerificationEvidence({
|
|
@@ -51,8 +53,11 @@ export class VerificationService {
|
|
|
51
53
|
scope: this.#options.scope,
|
|
52
54
|
startedAt,
|
|
53
55
|
finishedAt,
|
|
54
|
-
exitCode,
|
|
56
|
+
exitCode: result.exitCode,
|
|
55
57
|
testCount: result.testCount,
|
|
58
|
+
status: result.status,
|
|
59
|
+
failureClass: result.failureClass,
|
|
60
|
+
sourceFingerprint,
|
|
56
61
|
toolVersions: this.#options.toolVersions ?? {},
|
|
57
62
|
config: this.#options.config
|
|
58
63
|
});
|
|
@@ -84,7 +89,6 @@ export class VerificationService {
|
|
|
84
89
|
diagnostics: result.diagnostic
|
|
85
90
|
});
|
|
86
91
|
const finishedAt = (this.#options.now ?? (() => new Date().toISOString()))();
|
|
87
|
-
const evidenceReferences = await this.#persistEvidence(task, command, startedAt, finishedAt, result, result.exitCode);
|
|
88
92
|
return {
|
|
89
93
|
commandId: command.id,
|
|
90
94
|
required: command.required,
|
|
@@ -94,7 +98,9 @@ export class VerificationService {
|
|
|
94
98
|
timedOut: result.timedOut,
|
|
95
99
|
testCount: result.testCount,
|
|
96
100
|
diagnostic: fingerprint?.diagnostics ?? "",
|
|
97
|
-
evidenceReferences
|
|
101
|
+
evidenceReferences: [],
|
|
102
|
+
startedAt,
|
|
103
|
+
finishedAt
|
|
98
104
|
};
|
|
99
105
|
}
|
|
100
106
|
async verify(taskId) {
|
|
@@ -107,23 +113,72 @@ export class VerificationService {
|
|
|
107
113
|
throw verificationError("VERIFICATION_INPUT_INVALID", validation.errors[0]?.message ??
|
|
108
114
|
"Task and verification configuration are incompatible.");
|
|
109
115
|
}
|
|
110
|
-
const
|
|
116
|
+
const reviewScope = await resolveReviewScope({
|
|
117
|
+
root: this.#options.root,
|
|
118
|
+
runner: this.#options.gitRunner,
|
|
119
|
+
...(this.#options.base === undefined ? {} : { base: this.#options.base })
|
|
120
|
+
});
|
|
121
|
+
const sourceFingerprint = await calculateSourceFingerprint(this.#options.root, reviewScope, this.#options.gitRunner);
|
|
122
|
+
const worktreeSurface = reviewScope.mode === "worktree"
|
|
123
|
+
? await collectChangeSurface(this.#options.gitRunner)
|
|
124
|
+
: { staged: [], unstaged: [], untracked: [], paths: reviewScope.changedFiles };
|
|
125
|
+
const surface = worktreeSurface;
|
|
111
126
|
const selection = selectVerificationScope(surface.paths, this.#options.config);
|
|
112
127
|
const results = [];
|
|
113
128
|
for (const commandId of selection.verifierIds) {
|
|
114
129
|
results.push(await this.#runCommand(validation.value, commandById(this.#options.config, commandId)));
|
|
115
130
|
}
|
|
116
|
-
|
|
131
|
+
let status = aggregateVerificationStatus(results);
|
|
132
|
+
let sourceChanged = false;
|
|
133
|
+
const postflightScope = await resolveReviewScope({
|
|
134
|
+
root: this.#options.root,
|
|
135
|
+
runner: this.#options.gitRunner,
|
|
136
|
+
...(this.#options.base === undefined ? {} : { base: this.#options.base })
|
|
137
|
+
});
|
|
138
|
+
const postflightFingerprint = await calculateSourceFingerprint(this.#options.root, postflightScope, this.#options.gitRunner);
|
|
139
|
+
if (reviewScopeSignature(reviewScope) !== reviewScopeSignature(postflightScope) ||
|
|
140
|
+
sourceFingerprint !== postflightFingerprint) {
|
|
141
|
+
status = "UNKNOWN";
|
|
142
|
+
sourceChanged = true;
|
|
143
|
+
}
|
|
117
144
|
let signal = null;
|
|
118
145
|
if (status === "PASS") {
|
|
146
|
+
const taskEvidence = {};
|
|
147
|
+
for (const [index, result] of results.entries()) {
|
|
148
|
+
const command = commandById(this.#options.config, result.commandId);
|
|
149
|
+
const references = await this.#persistEvidence(validation.value, command, result.startedAt, result.finishedAt, result, sourceFingerprint);
|
|
150
|
+
results[index] = { ...result, evidenceReferences: references };
|
|
151
|
+
for (const [criterionIndex, criterion] of relevantCriteria(validation.value, result.commandId).entries()) {
|
|
152
|
+
const reference = references[criterionIndex];
|
|
153
|
+
if (reference !== undefined) {
|
|
154
|
+
taskEvidence[criterion.id] = [
|
|
155
|
+
...(taskEvidence[criterion.id] ?? []),
|
|
156
|
+
reference
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (Object.keys(taskEvidence).length > 0) {
|
|
162
|
+
await this.#options.taskService.recordEvidence(taskId, taskEvidence);
|
|
163
|
+
}
|
|
119
164
|
await this.#options.taskService.clearFailure(taskId);
|
|
120
165
|
}
|
|
121
166
|
else {
|
|
122
167
|
const required = results.filter((result) => result.required);
|
|
123
|
-
const gating = required
|
|
168
|
+
const gating = required;
|
|
124
169
|
const failed = gating.find((result) => result.status !== "PASS");
|
|
125
170
|
if (failed === undefined) {
|
|
126
|
-
|
|
171
|
+
if (!sourceChanged) {
|
|
172
|
+
throw verificationError("VERIFICATION_RESULT_INVALID", "Verification failed without a required result.");
|
|
173
|
+
}
|
|
174
|
+
const advanced = await this.#options.taskService.recordFailure(taskId, createFailureFingerprint({
|
|
175
|
+
commandId: "source-snapshot",
|
|
176
|
+
failureClass: "source-changed-during-verification",
|
|
177
|
+
exitCategory: "no-exit",
|
|
178
|
+
diagnostics: "source changed during verification"
|
|
179
|
+
}));
|
|
180
|
+
signal = advanced.signal;
|
|
181
|
+
return { taskId, status, surface, selection, results, signal, reviewScope, sourceFingerprint };
|
|
127
182
|
}
|
|
128
183
|
const advanced = await this.#options.taskService.recordFailure(taskId, createFailureFingerprint({
|
|
129
184
|
commandId: failed.commandId,
|
|
@@ -139,7 +194,9 @@ export class VerificationService {
|
|
|
139
194
|
surface,
|
|
140
195
|
selection,
|
|
141
196
|
results,
|
|
142
|
-
signal
|
|
197
|
+
signal,
|
|
198
|
+
reviewScope,
|
|
199
|
+
sourceFingerprint
|
|
143
200
|
};
|
|
144
201
|
}
|
|
145
202
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFile, lstat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { sha256 } from "../fs/hash.js";
|
|
4
|
+
import { AgentOpsError } from "../fs/paths.js";
|
|
5
|
+
import { resolveGitCommit } from "./change-surface.js";
|
|
6
|
+
export async function calculateSourceFingerprint(root, scope, runner) {
|
|
7
|
+
const head = await resolveGitCommit(runner, "HEAD");
|
|
8
|
+
if (scope.mode === "base") {
|
|
9
|
+
return sha256(JSON.stringify({
|
|
10
|
+
domain: "agent-ops-source-v1",
|
|
11
|
+
mode: "base",
|
|
12
|
+
head,
|
|
13
|
+
base: scope.resolvedBase,
|
|
14
|
+
paths: [...scope.changedFiles]
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
const paths = [];
|
|
18
|
+
for (const path of scope.changedFiles) {
|
|
19
|
+
const absolute = join(root, ...path.split("/"));
|
|
20
|
+
let entry;
|
|
21
|
+
try {
|
|
22
|
+
entry = await lstat(absolute);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
26
|
+
paths.push({ path, kind: "deleted" });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
if (!entry.isFile()) {
|
|
32
|
+
throw new AgentOpsError("SOURCE_SNAPSHOT_UNAVAILABLE", "Review source contains an unsupported entry.");
|
|
33
|
+
}
|
|
34
|
+
const bytes = await readFile(absolute);
|
|
35
|
+
paths.push({
|
|
36
|
+
path,
|
|
37
|
+
kind: "file",
|
|
38
|
+
executable: (entry.mode & 0o111) !== 0,
|
|
39
|
+
hash: sha256(bytes)
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return sha256(JSON.stringify({
|
|
43
|
+
domain: "agent-ops-source-v1",
|
|
44
|
+
mode: "worktree",
|
|
45
|
+
head,
|
|
46
|
+
base: null,
|
|
47
|
+
paths
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
@@ -139,13 +139,17 @@ export class NodeVerificationProcessRunner {
|
|
|
139
139
|
cwd: request.cwd,
|
|
140
140
|
detached: this.#platform !== "win32",
|
|
141
141
|
env: {
|
|
142
|
-
...process.env,
|
|
142
|
+
...(request.replaceEnv === true ? {} : process.env),
|
|
143
143
|
...(request.env ?? {})
|
|
144
144
|
},
|
|
145
145
|
shell: request.shell,
|
|
146
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
146
|
+
stdio: [request.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
147
147
|
windowsHide: true
|
|
148
148
|
});
|
|
149
|
+
if (request.stdin !== undefined && child.stdin !== null) {
|
|
150
|
+
child.stdin.on("error", () => { });
|
|
151
|
+
child.stdin.end(request.stdin);
|
|
152
|
+
}
|
|
149
153
|
let settled = false;
|
|
150
154
|
const completion = new Promise((resolve) => {
|
|
151
155
|
child.once("error", (error) => {
|
|
@@ -263,7 +267,9 @@ export async function runVerificationCommand(command, options) {
|
|
|
263
267
|
args: [...command.args],
|
|
264
268
|
cwd: options.cwd,
|
|
265
269
|
shell: command.shell === true,
|
|
266
|
-
...(options.env === undefined ? {} : { env: options.env })
|
|
270
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
271
|
+
...(options.replaceEnv === true ? { replaceEnv: true } : {}),
|
|
272
|
+
...(options.stdin === undefined ? {} : { stdin: options.stdin })
|
|
267
273
|
});
|
|
268
274
|
}
|
|
269
275
|
catch {
|
|
@@ -41,19 +41,25 @@ Enable it during `agent-ops init`, or by hand:
|
|
|
41
41
|
```json
|
|
42
42
|
{
|
|
43
43
|
"reviewRoles": [
|
|
44
|
-
{ "role": "independent-review", "targets": ["
|
|
44
|
+
{ "role": "independent-review", "targets": ["claude"] }
|
|
45
45
|
]
|
|
46
46
|
}
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
`targets` is an **ordered fallback chain**.
|
|
50
|
-
|
|
49
|
+
`targets` is an **ordered fallback chain**. Every review locks to the
|
|
50
|
+
staged/unstaged/untracked surface (or a clean `--base <ref>...HEAD` range),
|
|
51
|
+
requires fresh PASS evidence for required checks, and prints a complete native
|
|
52
|
+
schema report without persisting it.
|
|
53
|
+
|
|
54
|
+
Every attempt starts from a fresh temporary cwd with a narrow environment. The
|
|
55
|
+
following target identities may be configured; only Claude currently meets the
|
|
56
|
+
complete context-isolation contract and can execute automatically:
|
|
51
57
|
|
|
52
58
|
| Target | Invocation | Read-only |
|
|
53
59
|
| --- | --- | --- |
|
|
54
|
-
| `codex` | `codex exec` |
|
|
55
|
-
| `agy` (Antigravity) | `agy -p` |
|
|
56
|
-
| `claude` | `claude -p` | `--permission-mode plan` |
|
|
60
|
+
| `codex` | `codex exec` | retained; `capability-unavailable` |
|
|
61
|
+
| `agy` (Antigravity) | `agy -p` | retained; `capability-unavailable` |
|
|
62
|
+
| `claude` | `claude -p` | `--permission-mode plan --safe-mode` |
|
|
57
63
|
|
|
58
64
|
`opencode` is **not** a review target even though it is a supported harness.
|
|
59
65
|
Its `--agent plan` is rejected as a subagent and silently falls back to a
|
|
@@ -72,9 +78,11 @@ of the chain. It still runs when it is the only configured target, with a
|
|
|
72
78
|
`reviewer == host` warning.
|
|
73
79
|
|
|
74
80
|
Criterion descriptions come from the task bound to the current session, so a
|
|
75
|
-
review needs an attached task
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
review needs an attached task created under the current policy configuration;
|
|
82
|
+
`--criterion` filters those ids. Run `agent-ops verify` first: review rejects
|
|
83
|
+
stale, failed, or source-mismatched required evidence before any model call.
|
|
84
|
+
Compact PASS evidence is appended with a `review:<target>:` prefix; the full
|
|
85
|
+
human-readable report is transient. Completed tasks are never rewritten.
|
|
78
86
|
|
|
79
87
|
`--yes` is still required for every review run: init selection decides which
|
|
80
88
|
targets are permitted, `--yes` decides whether to spend money now.
|
|
@@ -36,18 +36,23 @@ Claude 與 Codex lifecycle support 為 `supported`,OpenCode 從 app initializa
|
|
|
36
36
|
```json
|
|
37
37
|
{
|
|
38
38
|
"reviewRoles": [
|
|
39
|
-
{ "role": "independent-review", "targets": ["
|
|
39
|
+
{ "role": "independent-review", "targets": ["claude"] }
|
|
40
40
|
]
|
|
41
41
|
}
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
`targets`
|
|
44
|
+
`targets` 是**有序的後備鏈**。每次 review 都鎖定 staged/unstaged/untracked
|
|
45
|
+
變更(或乾淨的 `--base <ref>...HEAD`),並要求必要驗證的最新 PASS evidence;
|
|
46
|
+
完整原生 schema report 會顯示給人看,但不會持久化。
|
|
47
|
+
|
|
48
|
+
每次嘗試都從新的暫存 cwd 與最小環境啟動。目前只有 Claude 具備完整的
|
|
49
|
+
context-isolation 合約,能自動執行:
|
|
45
50
|
|
|
46
51
|
| 目標 | 呼叫方式 | 唯讀 |
|
|
47
52
|
| --- | --- | --- |
|
|
48
|
-
| `codex` | `codex exec` |
|
|
49
|
-
| `agy`(Antigravity)| `agy -p` |
|
|
50
|
-
| `claude` | `claude -p` | `--permission-mode plan` |
|
|
53
|
+
| `codex` | `codex exec` | 保留設定;`capability-unavailable` |
|
|
54
|
+
| `agy`(Antigravity)| `agy -p` | 保留設定;`capability-unavailable` |
|
|
55
|
+
| `claude` | `claude -p` | `--permission-mode plan --safe-mode` |
|
|
51
56
|
|
|
52
57
|
`opencode` **不是** review 目標,即使它是支援的 harness。它的 `--agent plan`
|
|
53
58
|
會被判定為 subagent 而遭拒,並靜默退回可寫入的 agent,因此無法滿足唯讀前置
|
|
@@ -61,9 +66,11 @@ Claude 與 Codex lifecycle support 為 `supported`,OpenCode 從 app initializa
|
|
|
61
66
|
若 host 是 Claude Code(`CLAUDECODE` 已設定),`claude` 會被移到鏈尾。
|
|
62
67
|
當它是唯一設定的目標時仍會執行,並附上 `reviewer == host` 警告。
|
|
63
68
|
|
|
64
|
-
criterion 描述來自當前 session 綁定的 task,所以 review
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
criterion 描述來自當前 session 綁定的 task,所以 review 需要已附加、且建立時的
|
|
70
|
+
policy 設定仍相同的 task;`--criterion` 用來篩選 id。請先執行
|
|
71
|
+
`agent-ops verify`:必要 evidence 若失敗、過期或來源不符,review 會在 model 呼叫前停止。
|
|
72
|
+
僅 compact PASS evidence 會以 `review:<target>:` 附加;完整人類可讀 report 是暫存的。
|
|
73
|
+
已完成的 task 絕不改寫。
|
|
67
74
|
|
|
68
75
|
每次執行 review 仍需 `--yes`:init 的勾選決定「允許哪些目標」,
|
|
69
76
|
`--yes` 決定「現在是否要花錢」。
|
package/package.json
CHANGED
|
@@ -15,13 +15,16 @@
|
|
|
15
15
|
"startedAt",
|
|
16
16
|
"finishedAt",
|
|
17
17
|
"exitCode",
|
|
18
|
+
"status",
|
|
19
|
+
"failureClass",
|
|
20
|
+
"sourceFingerprint",
|
|
18
21
|
"testCount",
|
|
19
22
|
"toolVersions",
|
|
20
23
|
"configHash"
|
|
21
24
|
],
|
|
22
25
|
"properties": {
|
|
23
26
|
"schemaVersion": {
|
|
24
|
-
"const":
|
|
27
|
+
"const": 2
|
|
25
28
|
},
|
|
26
29
|
"taskId": {
|
|
27
30
|
"$ref": "#/$defs/id"
|
|
@@ -62,6 +65,18 @@
|
|
|
62
65
|
"minimum": 0,
|
|
63
66
|
"maximum": 9007199254740991
|
|
64
67
|
},
|
|
68
|
+
"status": {
|
|
69
|
+
"enum": ["PASS", "FAIL", "UNKNOWN"]
|
|
70
|
+
},
|
|
71
|
+
"failureClass": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"minLength": 1,
|
|
74
|
+
"maxLength": 256,
|
|
75
|
+
"pattern": "^[^\\u0000]*$"
|
|
76
|
+
},
|
|
77
|
+
"sourceFingerprint": {
|
|
78
|
+
"$ref": "#/$defs/hash"
|
|
79
|
+
},
|
|
65
80
|
"toolVersions": {
|
|
66
81
|
"type": "object",
|
|
67
82
|
"additionalProperties": {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/kylecheng3146/agent-ops/schemas/review-report.schema.json",
|
|
4
|
+
"title": "Agent Ops detailed independent review report",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["summary", "results", "findings", "residualRisks", "changedFilesInspected", "supportingFilesInspected"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"summary": { "$ref": "#/$defs/text" },
|
|
10
|
+
"results": { "type": "array", "items": { "$ref": "#/$defs/result" } },
|
|
11
|
+
"findings": { "type": "array", "items": { "$ref": "#/$defs/finding" } },
|
|
12
|
+
"residualRisks": { "type": "array", "items": { "$ref": "#/$defs/text" } },
|
|
13
|
+
"changedFilesInspected": { "type": "array", "items": { "$ref": "#/$defs/path" }, "uniqueItems": true },
|
|
14
|
+
"supportingFilesInspected": { "type": "array", "items": { "$ref": "#/$defs/path" }, "uniqueItems": true }
|
|
15
|
+
},
|
|
16
|
+
"$defs": {
|
|
17
|
+
"text": { "type": "string", "minLength": 1, "maxLength": 16384, "pattern": "[^\\s\\u0000]" },
|
|
18
|
+
"path": { "type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^(?!/)(?!.*\\\\)(?!.*(?:^|/)\\.\\.(?:/|$))[A-Za-z0-9._/-]+$" },
|
|
19
|
+
"result": {
|
|
20
|
+
"type": "object", "additionalProperties": false,
|
|
21
|
+
"required": ["criterionId", "status", "summary", "evidence"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"criterionId": { "$ref": "#/$defs/text" },
|
|
24
|
+
"status": { "enum": ["PASS", "FAIL"] },
|
|
25
|
+
"summary": { "$ref": "#/$defs/text" },
|
|
26
|
+
"evidence": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/text" } }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"location": {
|
|
30
|
+
"type": "object", "additionalProperties": false, "required": ["path"],
|
|
31
|
+
"properties": { "path": { "$ref": "#/$defs/path" }, "line": { "type": "integer", "minimum": 1 } }
|
|
32
|
+
},
|
|
33
|
+
"finding": {
|
|
34
|
+
"type": "object", "additionalProperties": false,
|
|
35
|
+
"required": ["severity", "blocking", "title", "details", "locations", "evidence", "recommendation", "criterionIds"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"severity": { "enum": ["critical", "important", "minor"] },
|
|
38
|
+
"blocking": { "type": "boolean" },
|
|
39
|
+
"title": { "$ref": "#/$defs/text" },
|
|
40
|
+
"details": { "$ref": "#/$defs/text" },
|
|
41
|
+
"locations": { "type": "array", "items": { "$ref": "#/$defs/location" } },
|
|
42
|
+
"evidence": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/text" } },
|
|
43
|
+
"recommendation": { "$ref": "#/$defs/text" },
|
|
44
|
+
"criterionIds": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/text" } }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|