@kylecheng3146/agent-ops 0.1.21 → 0.1.24
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/dist/packages/cli/src/args.js +10 -6
- package/dist/packages/cli/src/bin.js +67 -35
- package/dist/packages/cli/src/cli.js +2 -2
- package/dist/packages/cli/src/commands/hook.js +7 -12
- package/dist/packages/cli/src/commands/review.js +39 -24
- package/dist/packages/cli/src/hook-process.js +9 -0
- package/dist/runtime/src/hooks/completion-gate.js +18 -54
- package/dist/runtime/src/hooks/dispatch.js +11 -3
- package/dist/runtime/src/install/ownership.js +7 -0
- package/dist/runtime/src/install/plan.js +1 -0
- package/dist/runtime/src/install/uninstall.js +2 -0
- package/dist/runtime/src/review/attestation.js +12 -1
- package/dist/runtime/src/review/execute.js +40 -7
- package/dist/runtime/src/review/render.js +3 -1
- package/dist/runtime/src/review/runner.js +1 -0
- package/dist/runtime/src/security/trust.js +1 -2
- package/dist/runtime/src/task/completion.js +62 -0
- package/dist/runtime/src/task/service.js +71 -7
- package/dist/runtime/src/verify/change-surface.js +11 -2
- package/dist/runtime/src/verify/evidence.js +9 -1
- package/dist/runtime/src/verify/service.js +9 -1
- package/dist/runtime/src/verify/spawn.js +30 -8
- package/docs/en/guides/configuration.md +12 -0
- package/docs/en/spec/acceptance-and-evidence.md +25 -0
- package/docs/en/spec/review.md +11 -0
- package/docs/zh-TW/guides/configuration.md +10 -0
- package/docs/zh-TW/spec/acceptance-and-evidence.md +26 -1
- package/docs/zh-TW/spec/review.md +10 -0
- package/package.json +1 -1
|
@@ -15,6 +15,14 @@ import { buildAdversarialPrompt, buildReviewPrompt } from "./runner.js";
|
|
|
15
15
|
* review while looking like an unavailable target.
|
|
16
16
|
*/
|
|
17
17
|
export const DEFAULT_REVIEW_TIMEOUT_MS = 900_000;
|
|
18
|
+
export class ReviewInterruptedError extends Error {
|
|
19
|
+
signal;
|
|
20
|
+
constructor(signal) {
|
|
21
|
+
super("Independent review was interrupted.");
|
|
22
|
+
this.name = "ReviewInterruptedError";
|
|
23
|
+
this.signal = signal;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
18
26
|
// USER is load-bearing, not cosmetic: a credential store keyed by account name
|
|
19
27
|
// — the macOS keychain claude reads — cannot be opened without it, and its
|
|
20
28
|
// absence surfaces as "Not logged in" on an install that is logged in.
|
|
@@ -81,8 +89,7 @@ const REQUIRED_HELP_FLAGS = {
|
|
|
81
89
|
*/
|
|
82
90
|
const ADVANCING = new Set([
|
|
83
91
|
"missing-executable",
|
|
84
|
-
"spawn-failed"
|
|
85
|
-
"timeout"
|
|
92
|
+
"spawn-failed"
|
|
86
93
|
]);
|
|
87
94
|
const DIAGNOSTIC_MAX_CHARS = 200;
|
|
88
95
|
/**
|
|
@@ -114,6 +121,16 @@ function rejectedCallReason(output) {
|
|
|
114
121
|
}
|
|
115
122
|
return "capability-unavailable";
|
|
116
123
|
}
|
|
124
|
+
function throwIfInterrupted(target, options, failureClass) {
|
|
125
|
+
if (failureClass !== "aborted" && options.signal?.aborted !== true) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const signal = typeof options.signal?.reason === "string"
|
|
129
|
+
? options.signal.reason
|
|
130
|
+
: undefined;
|
|
131
|
+
options.onProgress?.(`${target}: review interrupted${signal === undefined ? "" : ` by ${signal}`}`);
|
|
132
|
+
throw new ReviewInterruptedError(signal);
|
|
133
|
+
}
|
|
117
134
|
async function snapshotRepository(request, destination, options) {
|
|
118
135
|
const cloned = await runVerificationCommand({
|
|
119
136
|
id: `review-snapshot-${request.label}`,
|
|
@@ -125,8 +142,10 @@ async function snapshotRepository(request, destination, options) {
|
|
|
125
142
|
timeoutMs: Math.min(options.timeoutMs ?? DEFAULT_REVIEW_TIMEOUT_MS, 60_000)
|
|
126
143
|
}, {
|
|
127
144
|
cwd: dirname(destination),
|
|
128
|
-
...(options.runner === undefined ? {} : { runner: options.runner })
|
|
145
|
+
...(options.runner === undefined ? {} : { runner: options.runner }),
|
|
146
|
+
...(options.signal === undefined ? {} : { signal: options.signal })
|
|
129
147
|
});
|
|
148
|
+
throwIfInterrupted(request.target, options, cloned.failureClass);
|
|
130
149
|
if (cloned.status !== "PASS") {
|
|
131
150
|
return firstComplaint(cloned.stderr, cloned.stdout) ??
|
|
132
151
|
`git clone failed (${cloned.failureClass})`;
|
|
@@ -184,6 +203,8 @@ async function attemptTarget(request, options) {
|
|
|
184
203
|
return skip("capability-unavailable", "no read-only mode is available for this target", "skipping");
|
|
185
204
|
}
|
|
186
205
|
const environment = isolatedReviewEnvironment(target, attemptDirectory, options.env ?? process.env);
|
|
206
|
+
throwIfInterrupted(target, options);
|
|
207
|
+
options.onProgress?.(`${target}: checking reviewer capability`);
|
|
187
208
|
const capability = await runVerificationCommand({
|
|
188
209
|
id: `review-capability-${request.label}`,
|
|
189
210
|
command: invocation.command,
|
|
@@ -196,8 +217,10 @@ async function attemptTarget(request, options) {
|
|
|
196
217
|
cwd: attemptDirectory,
|
|
197
218
|
...(options.runner === undefined ? {} : { runner: options.runner }),
|
|
198
219
|
env: environment,
|
|
199
|
-
replaceEnv: true
|
|
220
|
+
replaceEnv: true,
|
|
221
|
+
...(options.signal === undefined ? {} : { signal: options.signal })
|
|
200
222
|
});
|
|
223
|
+
throwIfInterrupted(target, options, capability.failureClass);
|
|
201
224
|
const help = `${capability.stdout}\n${capability.stderr}`;
|
|
202
225
|
const missingFlags = (REQUIRED_HELP_FLAGS[target] ?? []).filter((flag) => !help.includes(flag));
|
|
203
226
|
if (capability.status !== "PASS" ||
|
|
@@ -238,6 +261,8 @@ async function attemptTarget(request, options) {
|
|
|
238
261
|
if (invocation === undefined) {
|
|
239
262
|
return skip("capability-unavailable", "review invocation disappeared", "skipping");
|
|
240
263
|
}
|
|
264
|
+
throwIfInterrupted(target, options);
|
|
265
|
+
options.onProgress?.(`${target}: review started (timeout: ${Math.ceil((options.timeoutMs ?? DEFAULT_REVIEW_TIMEOUT_MS) / 1_000)}s)`);
|
|
241
266
|
const spawned = await runVerificationCommand({
|
|
242
267
|
id: `review-${request.label}`,
|
|
243
268
|
command: invocation.command,
|
|
@@ -254,8 +279,13 @@ async function attemptTarget(request, options) {
|
|
|
254
279
|
: { outputLimitBytes: options.outputLimitBytes }),
|
|
255
280
|
stdin: invocation.stdin,
|
|
256
281
|
env: environment,
|
|
257
|
-
replaceEnv: true
|
|
282
|
+
replaceEnv: true,
|
|
283
|
+
...(options.signal === undefined ? {} : { signal: options.signal })
|
|
258
284
|
});
|
|
285
|
+
throwIfInterrupted(target, options, spawned.failureClass);
|
|
286
|
+
if (spawned.failureClass === "timeout") {
|
|
287
|
+
return skip("timeout", "the reviewer exceeded its timeout");
|
|
288
|
+
}
|
|
259
289
|
if (ADVANCING.has(spawned.failureClass)) {
|
|
260
290
|
return {
|
|
261
291
|
...skip("missing-cli", `the process did not complete: ${spawned.failureClass}`),
|
|
@@ -273,6 +303,9 @@ async function attemptTarget(request, options) {
|
|
|
273
303
|
return skip(rejectedCallReason(output), firstComplaint(spawned.stderr, spawned.stdout) ??
|
|
274
304
|
`the call was rejected with exit ${spawned.exitCode ?? "unknown"} and no output`);
|
|
275
305
|
}
|
|
306
|
+
if (spawned.failureClass === "signal-exit") {
|
|
307
|
+
return skip("capability-unavailable", `the reviewer exited after ${spawned.signal ?? "an unknown signal"}`);
|
|
308
|
+
}
|
|
276
309
|
const payload = extractReviewObject(target, spawned.stdout);
|
|
277
310
|
const parsed = payload === undefined
|
|
278
311
|
? undefined
|
|
@@ -343,8 +376,8 @@ export function createReviewExecutor(options) {
|
|
|
343
376
|
prompt: buildAdversarialPrompt({ ...request.invocation, harness: target }, primary)
|
|
344
377
|
}, options);
|
|
345
378
|
if (outcome.kind === "skip") {
|
|
346
|
-
// Recorded, not just reported: progress is
|
|
347
|
-
//
|
|
379
|
+
// Recorded, not just reported: stderr progress is transient, and
|
|
380
|
+
// without this a PASS with no `adversarial` field cannot be told
|
|
348
381
|
// apart from a PASS that had no second target to challenge it.
|
|
349
382
|
attempts.push({
|
|
350
383
|
target,
|
|
@@ -7,9 +7,11 @@ function lineList(values) {
|
|
|
7
7
|
return values.length === 0 ? ["- none"] : values.map((value) => `- ${safe(value)}`);
|
|
8
8
|
}
|
|
9
9
|
export function renderReviewResult(result) {
|
|
10
|
+
const plannedTargets = result.plannedTargets ?? [result.harness];
|
|
10
11
|
const lines = [
|
|
11
12
|
`Independent review: ${result.status}`,
|
|
12
|
-
`Reviewer: ${result.harness}; model: ${safe(result.model)}; effort: ${safe(result.effort)}
|
|
13
|
+
`Reviewer: ${result.harness}; model: ${safe(result.model)}; effort: ${safe(result.effort)}.`,
|
|
14
|
+
`Planned reviewers: ${plannedTargets.length === 0 ? "none" : plannedTargets.join(" → ")}.`
|
|
13
15
|
];
|
|
14
16
|
if (result.scope !== undefined) {
|
|
15
17
|
lines.push(result.scope.mode === "base"
|
|
@@ -160,6 +160,7 @@ function safeReport(report) {
|
|
|
160
160
|
export async function runIndependentReview(options) {
|
|
161
161
|
const base = {
|
|
162
162
|
harness: options.invocation.harness,
|
|
163
|
+
plannedTargets: options.invocation.plannedTargets ?? [options.invocation.harness],
|
|
163
164
|
model: options.invocation.model,
|
|
164
165
|
effort: options.invocation.effort,
|
|
165
166
|
prompt: buildReviewPrompt(options.invocation),
|
|
@@ -188,8 +188,7 @@ export class FileTrustStore {
|
|
|
188
188
|
}
|
|
189
189
|
await withPrivateFileLock(this.#path, this.#anchorDirectory, async () => {
|
|
190
190
|
const store = await this.#read();
|
|
191
|
-
const records = store.records.filter((record) => record.binding.canonicalPath !== binding.canonicalPath
|
|
192
|
-
record.binding.remoteIdentity !== binding.remoteIdentity);
|
|
191
|
+
const records = store.records.filter((record) => record.binding.canonicalPath !== binding.canonicalPath);
|
|
193
192
|
records.push({ binding: structuredClone(binding), grantedAt });
|
|
194
193
|
await this.#write({ schemaVersion: 1, records });
|
|
195
194
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { calculateConfigHash } from "../config/hash.js";
|
|
2
|
+
import { findReviewAttestation } from "../review/attestation.js";
|
|
3
|
+
import { validateEvidence, validateTaskAgainstConfig } from "../schema/validate.js";
|
|
4
|
+
import { isPassingVerificationEvidence } from "../verify/evidence.js";
|
|
5
|
+
export async function checkTaskCompletionEvidence(stored, options) {
|
|
6
|
+
const { root, config, sourceFingerprint, evidenceStore } = options;
|
|
7
|
+
if (stored.failureFingerprint !== null) {
|
|
8
|
+
return { status: "FAIL", code: "VERIFICATION_FAILED", remedy: "Resolve the latest verification failure before completing the task." };
|
|
9
|
+
}
|
|
10
|
+
const configHash = calculateConfigHash(config);
|
|
11
|
+
if (!validateTaskAgainstConfig(stored.task, config).ok || stored.policyConfigHash !== configHash) {
|
|
12
|
+
return { status: "FAIL", code: "TASK_STALE", remedy: "Recreate the task against the current config, then verify and review it." };
|
|
13
|
+
}
|
|
14
|
+
for (const criterion of stored.task.criteria) {
|
|
15
|
+
const commands = config.verification.commands.filter(({ id, required }) => criterion.verifierIds.includes(id) && required);
|
|
16
|
+
if (commands.length === 0) {
|
|
17
|
+
return { status: "FAIL", code: "REQUIRED_VERIFIER_MISSING", remedy: "Configure a required verifier for every criterion, then recreate, verify and review the task." };
|
|
18
|
+
}
|
|
19
|
+
for (const command of commands) {
|
|
20
|
+
let latest;
|
|
21
|
+
for (const reference of stored.evidence[criterion.id] ?? []) {
|
|
22
|
+
if (reference.startsWith("review:"))
|
|
23
|
+
continue;
|
|
24
|
+
const validation = validateEvidence(await evidenceStore.load(reference));
|
|
25
|
+
if (!validation.ok)
|
|
26
|
+
continue;
|
|
27
|
+
const evidence = validation.value;
|
|
28
|
+
if (evidence.taskId === stored.task.id && evidence.criterionId === criterion.id &&
|
|
29
|
+
evidence.commandId === command.id && evidence.configHash === configHash &&
|
|
30
|
+
evidence.sourceFingerprint === sourceFingerprint &&
|
|
31
|
+
(latest === undefined || Date.parse(evidence.finishedAt) > Date.parse(latest.finishedAt) ||
|
|
32
|
+
(Date.parse(evidence.finishedAt) === Date.parse(latest.finishedAt) && !isPassingVerificationEvidence(command, evidence)))) {
|
|
33
|
+
latest = evidence;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (latest === undefined || !isPassingVerificationEvidence(command, latest)) {
|
|
37
|
+
return { status: "FAIL", code: "EVIDENCE_REQUIRED", remedy: "Run agent-ops verify and supply current PASS evidence for every required verifier." };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const attestation = await findReviewAttestation(root, sourceFingerprint);
|
|
42
|
+
if (attestation === null || attestation.taskId !== stored.task.id) {
|
|
43
|
+
return { status: "FAIL", code: "REVIEW_REQUIRED", remedy: "Run agent-ops review --yes for the whole task and current source." };
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/** Include descendants so legacy completed children cannot hide unfinished work. */
|
|
48
|
+
export function findIncompleteSubtask(records, taskId) {
|
|
49
|
+
const descendants = new Set([taskId]);
|
|
50
|
+
let previousSize = 0;
|
|
51
|
+
while (previousSize !== descendants.size) {
|
|
52
|
+
previousSize = descendants.size;
|
|
53
|
+
for (const record of records) {
|
|
54
|
+
if (record.task.parentTaskId !== undefined && descendants.has(record.task.parentTaskId)) {
|
|
55
|
+
descendants.add(record.task.id);
|
|
56
|
+
if (record.status === "active" || record.completedAt === null || record.failureFingerprint !== null)
|
|
57
|
+
return record;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { TASK_SCHEMA_VERSION } from "../contracts.js";
|
|
3
3
|
import { AgentOpsError } from "../fs/paths.js";
|
|
4
|
-
import { validateTask } from "../schema/validate.js";
|
|
4
|
+
import { validateEvidence, validateTask } from "../schema/validate.js";
|
|
5
5
|
import { renderTaskMarkdown } from "./render.js";
|
|
6
|
+
import { checkTaskCompletionEvidence, findIncompleteSubtask } from "./completion.js";
|
|
7
|
+
import { calculateConfigHash, FileEvidenceStore } from "../verify/evidence.js";
|
|
8
|
+
import { resolveReviewScope } from "../review/scope.js";
|
|
9
|
+
import { calculateSourceFingerprint } from "../verify/source-fingerprint.js";
|
|
6
10
|
import { advanceFailureFingerprint } from "../verify/fingerprint.js";
|
|
7
11
|
const SESSION_ID_PATTERN = /^[^\0\r\n]{1,256}$/u;
|
|
8
12
|
function defaultTaskId() {
|
|
@@ -67,10 +71,12 @@ export class TaskService {
|
|
|
67
71
|
#store;
|
|
68
72
|
#generateId;
|
|
69
73
|
#now;
|
|
74
|
+
#completion;
|
|
70
75
|
constructor(store, options = {}) {
|
|
71
76
|
this.#store = store;
|
|
72
77
|
this.#generateId = options.generateId ?? defaultTaskId;
|
|
73
78
|
this.#now = options.now ?? (() => new Date().toISOString());
|
|
79
|
+
this.#completion = options.completion;
|
|
74
80
|
}
|
|
75
81
|
async create(input) {
|
|
76
82
|
if (input.policyConfigHash !== undefined &&
|
|
@@ -105,8 +111,8 @@ export class TaskService {
|
|
|
105
111
|
if (parent === undefined) {
|
|
106
112
|
throw taskError("TASK_PARENT_NOT_FOUND", `Parent task not found: ${input.parentTaskId}`);
|
|
107
113
|
}
|
|
108
|
-
if (parent.status
|
|
109
|
-
throw taskError("TASK_PARENT_NOT_ACTIVE", "
|
|
114
|
+
if (parent.status !== "active") {
|
|
115
|
+
throw taskError("TASK_PARENT_NOT_ACTIVE", "Only an active task can take new subtasks.");
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
const record = {
|
|
@@ -196,12 +202,70 @@ export class TaskService {
|
|
|
196
202
|
}
|
|
197
203
|
async complete(taskId, evidenceInput) {
|
|
198
204
|
const now = assertTimestamp(this.#now());
|
|
205
|
+
const snapshot = await this.#store.read();
|
|
206
|
+
const current = findTask(snapshot, taskId);
|
|
207
|
+
if (current.status === "archived") {
|
|
208
|
+
throw taskError("TASK_NOT_ACTIVE", "An archived task cannot be completed.");
|
|
209
|
+
}
|
|
210
|
+
const submitted = normalizeEvidence(current.task, evidenceInput);
|
|
211
|
+
// A caller cannot hide a recorded failure by submitting only older PASS references.
|
|
212
|
+
const evidence = normalizeEvidence(current.task, Object.fromEntries(Object.entries(submitted).map(([criterionId, references]) => [criterionId,
|
|
213
|
+
[...new Set([...(current.evidence[criterionId] ?? []), ...references])]])));
|
|
214
|
+
const unfinished = findIncompleteSubtask(snapshot.tasks, taskId);
|
|
215
|
+
if (unfinished !== undefined) {
|
|
216
|
+
throw taskError("TASK_SUBTASK_INCOMPLETE", `Complete subtask ${unfinished.task.id} before its parent; archiving unfinished work does not satisfy completion.`);
|
|
217
|
+
}
|
|
218
|
+
const completion = this.#completion;
|
|
219
|
+
if (completion === undefined) {
|
|
220
|
+
throw taskError("TASK_COMPLETION_UNAVAILABLE", "Task completion requires repository config, source, verification evidence and review validation.");
|
|
221
|
+
}
|
|
222
|
+
const evidenceStore = new FileEvidenceStore(completion.root, completion.root);
|
|
223
|
+
for (const [criterionId, references] of Object.entries(evidence)) {
|
|
224
|
+
for (const reference of references) {
|
|
225
|
+
if (reference.startsWith("review:") && current.evidence[criterionId]?.includes(reference))
|
|
226
|
+
continue;
|
|
227
|
+
const validation = validateEvidence(await evidenceStore.load(reference));
|
|
228
|
+
if (!validation.ok || validation.value.taskId !== taskId || validation.value.criterionId !== criterionId) {
|
|
229
|
+
throw taskError("TASK_EVIDENCE_INVALID", `Criterion ${criterionId} requires resolvable evidence belonging to this task and criterion.`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const fingerprint = async () => {
|
|
234
|
+
try {
|
|
235
|
+
const scope = await resolveReviewScope({ root: completion.root, runner: completion.gitRunner,
|
|
236
|
+
...(completion.base === undefined ? {} : { base: completion.base }) });
|
|
237
|
+
return await calculateSourceFingerprint(completion.root, scope, completion.gitRunner);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (error instanceof AgentOpsError && error.code === "REVIEW_NO_CHANGE_SURFACE") {
|
|
241
|
+
throw taskError("TASK_COMPLETION_SCOPE_REQUIRED", completion.base === undefined
|
|
242
|
+
? "No changed worktree scope. For committed work, run verify, review and task complete with the same --base <git-ref>."
|
|
243
|
+
: "The requested base range has no changed paths; choose a base that precedes the committed work.");
|
|
244
|
+
}
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
const config = await completion.loadConfig();
|
|
249
|
+
const sourceFingerprint = await fingerprint();
|
|
250
|
+
const problem = await checkTaskCompletionEvidence({ ...current, evidence }, {
|
|
251
|
+
root: completion.root, config, sourceFingerprint,
|
|
252
|
+
evidenceStore
|
|
253
|
+
});
|
|
254
|
+
if (problem !== null) {
|
|
255
|
+
throw taskError(`TASK_COMPLETION_${problem.code}`, problem.remedy);
|
|
256
|
+
}
|
|
257
|
+
if (sourceFingerprint !== await fingerprint() ||
|
|
258
|
+
calculateConfigHash(config) !== calculateConfigHash(await completion.loadConfig())) {
|
|
259
|
+
throw taskError("TASK_COMPLETION_SOURCE_CHANGED", "Source or config changed during completion; verify and review again.");
|
|
260
|
+
}
|
|
199
261
|
return await this.#store.mutate((state) => {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
262
|
+
if (JSON.stringify(findTask(state, taskId)) !== JSON.stringify(current)) {
|
|
263
|
+
throw taskError("TASK_COMPLETION_STATE_CHANGED", "Task changed during completion; retry against its current evidence and status.");
|
|
264
|
+
}
|
|
265
|
+
const unfinished = findIncompleteSubtask(state.tasks, taskId);
|
|
266
|
+
if (unfinished !== undefined) {
|
|
267
|
+
throw taskError("TASK_SUBTASK_INCOMPLETE", `Complete subtask ${unfinished.task.id} before its parent; archiving unfinished work does not satisfy completion.`);
|
|
203
268
|
}
|
|
204
|
-
const evidence = normalizeEvidence(current.task, evidenceInput);
|
|
205
269
|
if (current.status === "complete") {
|
|
206
270
|
if (JSON.stringify(current.evidence) !== JSON.stringify(evidence)) {
|
|
207
271
|
throw taskError("TASK_ALREADY_COMPLETE", "Completed task evidence cannot be replaced.");
|
|
@@ -60,6 +60,9 @@ async function collectPaths(runner, args) {
|
|
|
60
60
|
return parseNulPaths(result.stdout);
|
|
61
61
|
}
|
|
62
62
|
export async function collectChangeSurface(runner) {
|
|
63
|
+
const trackedRuntime = await collectPaths(runner, [
|
|
64
|
+
"ls-files", "--cached", "-z", "--", ".agent-ops/tasks/", ".agent-ops/reviews/"
|
|
65
|
+
]);
|
|
63
66
|
const staged = await collectPaths(runner, [
|
|
64
67
|
"diff",
|
|
65
68
|
"--cached",
|
|
@@ -69,6 +72,9 @@ export async function collectChangeSurface(runner) {
|
|
|
69
72
|
"--no-textconv",
|
|
70
73
|
"-z"
|
|
71
74
|
]);
|
|
75
|
+
if (trackedRuntime.length > 0 || staged.some((path) => path.startsWith(".agent-ops/tasks/") || path.startsWith(".agent-ops/reviews/"))) {
|
|
76
|
+
throw new AgentOpsError("CHANGE_SURFACE_TRACKED_RUNTIME", "Runtime output is staged or tracked under .agent-ops/tasks/ or .agent-ops/reviews/. Move any source files out of these runtime directories, add ignore rules (agent-ops update), then remove runtime files from the Git index with git rm -r --cached --ignore-unmatch -- .agent-ops/tasks/ .agent-ops/reviews/; keep local files, commit any staged runtime removals, then rerun verify/review.");
|
|
77
|
+
}
|
|
72
78
|
const unstaged = await collectPaths(runner, [
|
|
73
79
|
"diff",
|
|
74
80
|
"--name-only",
|
|
@@ -77,12 +83,15 @@ export async function collectChangeSurface(runner) {
|
|
|
77
83
|
"--no-textconv",
|
|
78
84
|
"-z"
|
|
79
85
|
]);
|
|
80
|
-
const untracked = await collectPaths(runner, [
|
|
86
|
+
const untracked = (await collectPaths(runner, [
|
|
81
87
|
"ls-files",
|
|
82
88
|
"--others",
|
|
83
89
|
"--exclude-standard",
|
|
84
90
|
"-z"
|
|
85
|
-
])
|
|
91
|
+
])).filter(
|
|
92
|
+
// Indexed runtime was rejected above; only generated, untracked output is excluded.
|
|
93
|
+
(path) => !path.startsWith(".agent-ops/tasks/") &&
|
|
94
|
+
!path.startsWith(".agent-ops/reviews/"));
|
|
86
95
|
return {
|
|
87
96
|
staged,
|
|
88
97
|
unstaged,
|
|
@@ -47,6 +47,9 @@ export function isPassingVerificationEvidence(command, evidence) {
|
|
|
47
47
|
if (evidence.status !== "PASS" ||
|
|
48
48
|
evidence.exitCode !== 0 ||
|
|
49
49
|
evidence.failureClass !== "none" ||
|
|
50
|
+
evidence.cwd !== command.cwd ||
|
|
51
|
+
JSON.stringify(evidence.argv) !==
|
|
52
|
+
JSON.stringify([command.command, ...command.args].map(redactSecrets)) ||
|
|
50
53
|
command.evidence.kind === "file") {
|
|
51
54
|
return false;
|
|
52
55
|
}
|
|
@@ -96,7 +99,12 @@ export class FileEvidenceStore {
|
|
|
96
99
|
return null;
|
|
97
100
|
}
|
|
98
101
|
try {
|
|
99
|
-
|
|
102
|
+
const validation = validateEvidence(JSON.parse(source));
|
|
103
|
+
if (!validation.ok)
|
|
104
|
+
return null;
|
|
105
|
+
const evidence = validation.value;
|
|
106
|
+
const expected = `.agent-ops/tasks/evidence/${evidence.taskId}/${evidence.commandId}-${sha256(source).slice(0, 16)}.json`;
|
|
107
|
+
return reference === expected ? evidence : null;
|
|
100
108
|
}
|
|
101
109
|
catch {
|
|
102
110
|
throw new AgentOpsError("EVIDENCE_INVALID", "Stored evidence is not valid JSON.");
|
|
@@ -123,7 +123,15 @@ export class VerificationService {
|
|
|
123
123
|
? await collectChangeSurface(this.#options.gitRunner)
|
|
124
124
|
: { staged: [], unstaged: [], untracked: [], paths: reviewScope.changedFiles };
|
|
125
125
|
const surface = worktreeSurface;
|
|
126
|
-
const
|
|
126
|
+
const mappedSelection = selectVerificationScope(surface.paths, this.#options.config);
|
|
127
|
+
const taskVerifierIds = new Set(validation.value.criteria.flatMap((criterion) => criterion.verifierIds));
|
|
128
|
+
const selection = {
|
|
129
|
+
...mappedSelection,
|
|
130
|
+
verifierIds: [...new Set([...mappedSelection.verifierIds,
|
|
131
|
+
...this.#options.config.verification.commands
|
|
132
|
+
.filter(({ id, required }) => required && taskVerifierIds.has(id))
|
|
133
|
+
.map(({ id }) => id)])]
|
|
134
|
+
};
|
|
127
135
|
const results = [];
|
|
128
136
|
for (const commandId of selection.verifierIds) {
|
|
129
137
|
results.push(await this.#runCommand(validation.value, commandById(this.#options.config, commandId)));
|
|
@@ -270,6 +270,9 @@ function hasAcknowledgedShell(command) {
|
|
|
270
270
|
export async function runVerificationCommand(command, options) {
|
|
271
271
|
const now = options.now ?? Date.now;
|
|
272
272
|
const startedAt = now();
|
|
273
|
+
if (options.signal?.aborted === true) {
|
|
274
|
+
return emptyResult(command.id, "aborted", elapsedMilliseconds(startedAt, now()));
|
|
275
|
+
}
|
|
273
276
|
if (!hasAcknowledgedShell(command)) {
|
|
274
277
|
return emptyResult(command.id, "shell-risk-unacknowledged", elapsedMilliseconds(startedAt, now()));
|
|
275
278
|
}
|
|
@@ -298,21 +301,35 @@ export async function runVerificationCommand(command, options) {
|
|
|
298
301
|
const timeout = new Promise((resolve) => {
|
|
299
302
|
timer = setTimeout(() => resolve({ kind: "timeout" }), timeoutMs);
|
|
300
303
|
});
|
|
304
|
+
let removeAbortListener = () => { };
|
|
305
|
+
const aborted = new Promise((resolve) => {
|
|
306
|
+
const signal = options.signal;
|
|
307
|
+
if (signal === undefined) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const onAbort = () => resolve({ kind: "abort" });
|
|
311
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
312
|
+
removeAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
313
|
+
});
|
|
301
314
|
const outcome = await Promise.race([
|
|
302
315
|
running.completion.then((completion) => ({
|
|
303
316
|
kind: "completion",
|
|
304
317
|
completion
|
|
305
318
|
})),
|
|
306
|
-
timeout
|
|
319
|
+
timeout,
|
|
320
|
+
aborted
|
|
307
321
|
]);
|
|
322
|
+
removeAbortListener();
|
|
308
323
|
if (timer !== undefined) {
|
|
309
324
|
clearTimeout(timer);
|
|
310
325
|
}
|
|
311
326
|
let completion;
|
|
312
327
|
let timedOut = false;
|
|
328
|
+
let wasAborted = false;
|
|
313
329
|
let terminationFailed = false;
|
|
314
|
-
if (outcome.kind === "timeout") {
|
|
315
|
-
timedOut =
|
|
330
|
+
if (outcome.kind === "timeout" || outcome.kind === "abort") {
|
|
331
|
+
timedOut = outcome.kind === "timeout";
|
|
332
|
+
wasAborted = outcome.kind === "abort";
|
|
316
333
|
try {
|
|
317
334
|
await running.terminateTree(terminationGrace);
|
|
318
335
|
}
|
|
@@ -330,14 +347,19 @@ export async function runVerificationCommand(command, options) {
|
|
|
330
347
|
settleCapturedOutput(stdout, terminationGrace),
|
|
331
348
|
settleCapturedOutput(stderr, terminationGrace)
|
|
332
349
|
]);
|
|
333
|
-
const classified =
|
|
350
|
+
const classified = wasAborted
|
|
334
351
|
? {
|
|
335
352
|
status: terminationFailed ? "UNKNOWN" : "FAIL",
|
|
336
|
-
failureClass:
|
|
337
|
-
? "termination-failed"
|
|
338
|
-
: "timeout"
|
|
353
|
+
failureClass: "aborted"
|
|
339
354
|
}
|
|
340
|
-
:
|
|
355
|
+
: timedOut
|
|
356
|
+
? {
|
|
357
|
+
status: terminationFailed ? "UNKNOWN" : "FAIL",
|
|
358
|
+
failureClass: terminationFailed
|
|
359
|
+
? "termination-failed"
|
|
360
|
+
: "timeout"
|
|
361
|
+
}
|
|
362
|
+
: classifyCompletion(completion, capturedStdout.failed || capturedStderr.failed);
|
|
341
363
|
return {
|
|
342
364
|
commandId: command.id,
|
|
343
365
|
status: classified.status,
|
|
@@ -55,6 +55,12 @@ A bare review uses the built-in `change-quality` criterion; `--task` uses the
|
|
|
55
55
|
task criteria and requires fresh PASS evidence for required checks. The full
|
|
56
56
|
report is printed, and PASS persists only a source-fingerprint attestation.
|
|
57
57
|
|
|
58
|
+
`--review-target` belongs to `init` and configures that persistent chain.
|
|
59
|
+
For one run, `review --harness <target>` narrows the chain to exactly one
|
|
60
|
+
already-configured target; it never enables a target absent from project
|
|
61
|
+
policy. The configured model, effort, and timeout still apply. Review JSON
|
|
62
|
+
includes `plannedTargets` in the actual host-adjusted order.
|
|
63
|
+
|
|
58
64
|
Every attempt starts from a fresh session and disposable repository clone with
|
|
59
65
|
native read-only mode.
|
|
60
66
|
Claude uses complete safe-mode isolation. Codex and Agy preserve their existing
|
|
@@ -88,6 +94,12 @@ failure, oversized output, or unparseable output. Every attempt and reason is
|
|
|
88
94
|
preserved in human and JSON output. A `PASS` or `FAIL` verdict is **terminal**,
|
|
89
95
|
so the chain cannot shop for a passing review.
|
|
90
96
|
|
|
97
|
+
Capability checks and model starts are reported on stderr, including under
|
|
98
|
+
`--json`; stdout remains one final JSON envelope and raw reviewer output is
|
|
99
|
+
never streamed. SIGINT or SIGTERM terminates the active reviewer process tree,
|
|
100
|
+
does not advance the fallback chain, and never writes an attestation. An
|
|
101
|
+
exhausted timeout chain reports `timeout`, not `missing-cli`.
|
|
102
|
+
|
|
91
103
|
If Claude Code is the host (`CLAUDECODE` is set), `claude` is moved to the end
|
|
92
104
|
of the chain. It still runs when it is the only configured target, with a
|
|
93
105
|
`reviewer == host` warning.
|
|
@@ -19,3 +19,28 @@ The verifier MUST return FAIL when a criterion is missing, duplicated, unknown,
|
|
|
19
19
|
- Evidence: The aggregate lists the criterion IDs and their evidence references.
|
|
20
20
|
- Positive: `tests PASS [report.json]; scope PASS [diff.txt]`.
|
|
21
21
|
- Negative: `tests PASS; tests PASS; extra PASS`.
|
|
22
|
+
|
|
23
|
+
## EVIDENCE-COMPLETION-001
|
|
24
|
+
|
|
25
|
+
`agent-ops task complete` MUST validate completion independently of the host, active profiles, and Stop hooks.
|
|
26
|
+
|
|
27
|
+
- Trigger: Completing a task, including repeated completion requests.
|
|
28
|
+
- Action: Check required verifiers, current evidence, whole-task review, and subtask completion before writing state.
|
|
29
|
+
- Evidence: `TASK_COMPLETED` is returned only after these checks pass; rejected requests preserve state.
|
|
30
|
+
- Positive: Current verification evidence plus a task-bound review allows an otherwise complete task to finish.
|
|
31
|
+
- Negative: Supplying `criterion=PASS`, an old review, or archiving an unfinished child to bypass completion.
|
|
32
|
+
|
|
33
|
+
- Every criterion needs at least one required verifier and current PASS evidence for every required verifier it names. Verify always includes those task-required commands, even when path mappings select a narrower set. Supply the evidence file references produced by `agent-ops verify --task <id>`, not command strings or a written assertion of PASS.
|
|
34
|
+
- The task's config baseline and evidence must match the current effective config and source fingerprint in the selected scope. Outstanding verification failures block completion. A newer failing result cannot be replaced by an older PASS; contradictory results at the same timestamp fail closed.
|
|
35
|
+
- Run `agent-ops review --task <id> --yes` for the whole task. Completion requires its current, task-bound PASS attestation; generic reviews and partial-criterion reviews do not satisfy it.
|
|
36
|
+
- Starting an authorized review invalidates the previous attestation for that source fingerprint. If the reviewer cannot run or does not pass, run a fresh successful whole-task review before completion.
|
|
37
|
+
- All subtasks and descendants must have completed without an outstanding failure. Archiving unfinished work does not satisfy this requirement. A completed or archived parent cannot accept new subtasks. If a subtask is abandoned, recreate the parent task without that cancelled branch and verify/review the new task tree.
|
|
38
|
+
- Evidence, source and config validation run outside the task store lock. After rechecking source and config, completion briefly locks state, rejects a changed task snapshot (`TASK_COMPLETION_STATE_CHANGED`), rechecks descendants, and writes completion atomically. Slow validation does not block task reads or other task mutations; a concurrent mutation remains intact when completion is rejected.
|
|
39
|
+
|
|
40
|
+
Existing task files remain readable without migration. Legacy tasks without a config baseline, arbitrary evidence references, or unfinished archived children must be recreated and verified/reviewed before completion. Repeating completion also revalidates the evidence; an old completed status alone is insufficient. Runtime callers must supply `TaskServiceOptions.completion`; omitting this repository context fails closed for completion while leaving read-only task operations available.
|
|
41
|
+
|
|
42
|
+
Before committing, use the default worktree scope for verify, review and completion. After committing, use the same `--base <git-ref>` on all three commands to produce fresh proof for that committed range; `TaskServiceOptions.completion.base` selects it for runtime callers. Base mode requires a clean worktree. Without a worktree change surface or an explicit base, completion returns `TASK_COMPLETION_SCOPE_REQUIRED`. The agy Stop gate still checks worktree-scoped proof: complete and stop before committing, or use its user-approved one-time permit after inspecting a completed base-scoped task.
|
|
43
|
+
|
|
44
|
+
This enforces the toolkit's completion command, not a host's natural-language completion claim. It does not prevent a process with the same filesystem authority from editing task/evidence files directly, and it does not lock Git or external source writers.
|
|
45
|
+
|
|
46
|
+
Project init/update installs `.agent-ops/.gitignore` for `/tasks/` and `/reviews/` on every harness and profile. An existing user-owned `.agent-ops/.gitignore` blocks init/update with `UNMANAGED_INSTALL_PATH`: first back it up outside that path and move its custom rules to the root `.gitignore` (prefix them with `/.agent-ops/`), then retry. Git collection fails with `CHANGE_SURFACE_TRACKED_RUNTIME` if these directories contain indexed files (even clean tracked files), or staged runtime removals. Move any real source files out of the runtime directories, install ignore rules, then use `git rm -r --cached --ignore-unmatch -- .agent-ops/tasks/ .agent-ops/reviews/` to keep local files while removing them from the index. Commit any staged removals before producing fresh verification/review evidence. The active completion Stop gate also blocks this condition with `COMPLETION_GATE_TRACKED_RUNTIME` and the recovery message. Run verify/review from the Git repository root; nested project roots are not supported by the current path/fingerprint collector. Uninstall removes the managed ignore file but preserves runtime output.
|
package/docs/en/spec/review.md
CHANGED
|
@@ -31,6 +31,12 @@ when an installation supports multiple harnesses.
|
|
|
31
31
|
- Positive: `review --harness claude` resolves one target.
|
|
32
32
|
- Negative: `Run one review invocation against every installed harness implicitly.`
|
|
33
33
|
|
|
34
|
+
The explicit target MUST already exist in the configured independent-review
|
|
35
|
+
role. It narrows the configured chain to one target while preserving model,
|
|
36
|
+
effort, and timeout policy. Without `--harness`, host-aware ordering applies to
|
|
37
|
+
the complete configured chain. Every result carries that order as
|
|
38
|
+
`plannedTargets`.
|
|
39
|
+
|
|
34
40
|
## REVIEW-READONLY-001
|
|
35
41
|
|
|
36
42
|
A review target MUST be launched with its own read-only mechanism, and a target
|
|
@@ -48,6 +54,11 @@ different CLI from the hosting CLI; when no other usable target exists,
|
|
|
48
54
|
same-target fresh review is allowed but MUST render as `DEGRADED: isolated
|
|
49
55
|
self-review`. A resumed development session is never an independent review.
|
|
50
56
|
|
|
57
|
+
Capability and model-start progress goes to stderr even when stdout is JSON.
|
|
58
|
+
Raw reviewer output remains bounded and unstreamed. SIGINT or SIGTERM aborts
|
|
59
|
+
the active process tree without fallback or attestation; timeout remains a
|
|
60
|
+
distinct NOT_RUN reason rather than being flattened to `missing-cli`.
|
|
61
|
+
|
|
51
62
|
## REVIEW-CHAIN-001
|
|
52
63
|
|
|
53
64
|
Configured targets form an ordered fallback chain that MUST advance only when
|
|
@@ -49,6 +49,11 @@ Claude 與 Codex lifecycle support 為 `supported`,OpenCode 從 app initializa
|
|
|
49
49
|
`--task` 使用 task criteria,並要求必要驗證的最新 PASS evidence。完整 report
|
|
50
50
|
會顯示給人看,PASS 後只持久化 source-fingerprint attestation。
|
|
51
51
|
|
|
52
|
+
`--review-target` 只屬於 `init`,用來設定持久 fallback chain。單次執行可用
|
|
53
|
+
`review --harness <target>`,將 chain 縮窄為一個已在 project policy 啟用的
|
|
54
|
+
target;它不會臨時啟用未設定的 reviewer。既有 model、effort 與 timeout 仍會
|
|
55
|
+
沿用,review JSON 的 `plannedTargets` 會列出經 host 調整後的實際順序。
|
|
56
|
+
|
|
52
57
|
每次嘗試都從全新 session、一次性 repository clone 與原生唯讀模式啟動。Claude 使用完整 safe-mode
|
|
53
58
|
隔離;Codex 與 Agy 為了支援既有 OAuth 登入而保留登入環境,因此 context
|
|
54
59
|
隔離較弱。Agy 會取得一次性 clone,即使 sandboxed plan mode 寫入 cwd,也無法
|
|
@@ -77,6 +82,11 @@ agent-ops 刻意不傳會繞過權限邊界的 `--dangerously-skip-permissions`
|
|
|
77
82
|
都會保留每次 attempt 及原因。`PASS` 或 `FAIL` 判定是**終局**,因此不會產生
|
|
78
83
|
自動化的 review shopping。
|
|
79
84
|
|
|
85
|
+
Capability check 與模型啟動進度都寫到 stderr,包括 `--json` 模式;stdout
|
|
86
|
+
仍只有最終 JSON envelope,且不會串流 reviewer 原始輸出。SIGINT 或 SIGTERM
|
|
87
|
+
會終止目前 reviewer 的完整 process tree、不進入 fallback,也不寫入
|
|
88
|
+
attestation。整條 chain 逾時時回報 `timeout`,不會誤報 `missing-cli`。
|
|
89
|
+
|
|
80
90
|
若 host 是 Claude Code(`CLAUDECODE` 已設定),`claude` 會被移到鏈尾。
|
|
81
91
|
當它是唯一設定的目標時仍會執行,並附上 `reviewer == host` 警告。
|
|
82
92
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 驗收與證據
|
|
2
2
|
|
|
3
|
-
English source version: 2026-07
|
|
3
|
+
English source version: 2026-09-07. Revalidate: when the English specification changes.
|
|
4
4
|
|
|
5
5
|
## EVIDENCE-CRITERION-001
|
|
6
6
|
|
|
@@ -21,3 +21,28 @@ English source version: 2026-07-23. Revalidate: when the English specification c
|
|
|
21
21
|
- Evidence: 聚合結果列出 criterion ID 與證據參照。
|
|
22
22
|
- Positive: `tests PASS [report.json];scope PASS [diff.txt]`。
|
|
23
23
|
- Negative: `tests PASS;tests PASS;extra PASS`。
|
|
24
|
+
|
|
25
|
+
## EVIDENCE-COMPLETION-001
|
|
26
|
+
|
|
27
|
+
`agent-ops task complete` MUST 獨立於 host、啟用的 profile 與 Stop hook,驗證任務是否可完成。
|
|
28
|
+
|
|
29
|
+
- Trigger: 完成任務,包括重複提交完成請求。
|
|
30
|
+
- Action: 寫入狀態前檢查 required verifier、目前有效的證據、全任務審查與子任務完成狀態。
|
|
31
|
+
- Evidence: 僅在檢查通過後回傳 `TASK_COMPLETED`;拒絕請求時保留原狀態。
|
|
32
|
+
- Positive: 有目前有效的驗證證據與 task-bound review,且子任務已完成,才允許收口。
|
|
33
|
+
- Negative: 填入 `criterion=PASS`、提供舊 review,或封存未完成子任務來繞過檢查。
|
|
34
|
+
|
|
35
|
+
- 每項 criterion 至少需要一個 required verifier,且其列出的每個 required verifier 都要有目前有效的 PASS evidence。即使 path mapping 選出較小範圍,verify 也會包含 task 明列的 required 命令。請提供 `agent-ops verify --task <id>` 產生的證據檔參照,不能只填命令文字或自行宣稱 PASS。
|
|
36
|
+
- 任務的 config baseline、evidence 必須符合目前有效設定與所選 scope 的來源指紋。尚未解決的驗證失敗會阻擋完成;舊 PASS 不能蓋過較新的失敗,同時間戳的矛盾結果也會阻擋。
|
|
37
|
+
- 使用 `agent-ops review --task <id> --yes` 審查整個任務。完成時必須有目前有效且綁定該 task 的 PASS attestation;generic review 與只涵蓋部分 criterion 的 review 不算。
|
|
38
|
+
- 開始一次已授權的 review 會撤銷該來源指紋先前的 attestation。若 reviewer 未能執行或未通過,完成前必須重新取得成功的全任務 review。
|
|
39
|
+
- 所有子任務與後代任務都必須已完成,且沒有尚未解決的失敗。封存未完成工作不算完成;已完成或封存的 parent 不能新增子任務。若放棄某個 subtask,請重建不含該取消分支的 parent task,並重新驗證與審查新的 task tree。
|
|
40
|
+
- 證據、來源與設定檢查在 task store 鎖外執行。再次確認來源與設定後,完成操作才短暫鎖住狀態,比對 task 快照(改變時回傳 `TASK_COMPLETION_STATE_CHANGED`)、重查後代任務並原子寫入。慢速檢查不會阻擋 task 讀取或其他修改;完成被拒絕時,併發修改仍會保留。
|
|
41
|
+
|
|
42
|
+
既有 task 檔案仍可直接讀取,不需資料格式遷移。缺少 config baseline、使用任意文字證據,或含有未完成但已封存子任務的舊任務,必須重建並完成驗證與審查。重複執行 complete 也會重新驗證證據;舊的 completed 狀態本身不夠。Runtime 呼叫端必須提供 `TaskServiceOptions.completion`;缺少此 repository context 時,complete 會拒絕執行,但仍可讀取任務。
|
|
43
|
+
|
|
44
|
+
Commit 前,verify、review、complete 使用預設 worktree scope。Commit 後,三個命令都使用相同的 `--base <git-ref>`,為已提交範圍重新產生證據;runtime 呼叫端使用 `TaskServiceOptions.completion.base`。Base 模式要求 worktree 乾淨。沒有 worktree 變更又未指定 base 時,complete 會回傳 `TASK_COMPLETION_SCOPE_REQUIRED`。agy Stop gate 仍檢查 worktree scope 的證據:請在 commit 前完成並停止,或由使用者檢查已完成的 base-scope 任務後,核准一次性的 Stop permit。
|
|
45
|
+
|
|
46
|
+
這項機制強制的是 toolkit 的完成命令,無法阻止 host 直接用自然語言宣稱完成,也無法阻止具有相同檔案權限的 process 直接改寫 task/evidence 檔案;它不會鎖住 Git 或其他來源檔案寫入者。
|
|
47
|
+
|
|
48
|
+
Project init/update 在所有 harness 與 profile 安裝 `.agent-ops/.gitignore`,忽略 `/tasks/` 與 `/reviews/`;若已有使用者自建的 `.agent-ops/.gitignore`,init/update 會回傳 `UNMANAGED_INSTALL_PATH`:先將原檔備份到其他位置,並將自訂規則移到根目錄 `.gitignore`(加上 `/.agent-ops/` 前綴),再重試。這些目錄若包含 index 中的檔案(即使 tracked 且未修改)或 staged 的 runtime 刪除,Git 範圍收集會回傳 `CHANGE_SURFACE_TRACKED_RUNTIME`。請先移出真正的程式來源、安裝 ignore 規則,再執行 `git rm -r --cached --ignore-unmatch -- .agent-ops/tasks/ .agent-ops/reviews/`,保留本機檔案並解除追蹤。若有 staged 刪除,先提交清理 commit,再重新產生 verify/review 證據。啟用的 completion Stop gate 也會以 `COMPLETION_GATE_TRACKED_RUNTIME` 阻擋此狀況並提供修復訊息。請從 Git repository 根目錄執行 verify/review;目前路徑與指紋收集不支援巢狀 project root。Uninstall 會移除 managed ignore 檔,但保留 runtime 輸出。
|