@kylecheng3146/agent-ops 0.1.20 → 0.1.22
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 +23 -4
- package/dist/packages/cli/src/agy-headless.js +18 -0
- package/dist/packages/cli/src/args.js +35 -5
- package/dist/packages/cli/src/bin.js +115 -37
- package/dist/packages/cli/src/cli.js +4 -1
- package/dist/packages/cli/src/commands/allow-stop.js +11 -0
- package/dist/packages/cli/src/commands/hook.js +12 -1
- package/dist/packages/cli/src/commands/init.js +4 -1
- package/dist/packages/cli/src/commands/review.js +31 -21
- package/dist/packages/cli/src/commands/task.js +5 -2
- package/dist/packages/cli/src/context.js +4 -1
- package/dist/packages/cli/src/hook-process.js +59 -5
- package/dist/packages/cli/src/wizard.js +24 -0
- package/dist/runtime/src/adapters/agy/config.js +17 -7
- package/dist/runtime/src/adapters/agy/events.js +8 -0
- package/dist/runtime/src/adapters/agy/input.js +21 -3
- package/dist/runtime/src/adapters/agy/output.js +8 -4
- package/dist/runtime/src/config/explain.js +5 -0
- package/dist/runtime/src/config/migrate.js +15 -1
- package/dist/runtime/src/contracts.js +1 -1
- package/dist/runtime/src/hooks/completion-gate.js +209 -0
- package/dist/runtime/src/hooks/dispatch.js +6 -0
- package/dist/runtime/src/install/doctor.js +3 -1
- package/dist/runtime/src/install/harness.js +5 -1
- package/dist/runtime/src/install/ownership.js +9 -2
- package/dist/runtime/src/install/plan.js +19 -4
- package/dist/runtime/src/install/profiles.js +3 -0
- 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/schema/validate.js +11 -1
- package/dist/runtime/src/task/service.js +17 -0
- package/dist/runtime/src/verify/spawn.js +30 -8
- package/docs/en/guides/configuration.md +37 -7
- package/docs/en/spec/README.md +3 -3
- package/docs/en/spec/harness-adapters.md +7 -3
- package/docs/en/spec/review.md +11 -0
- package/docs/zh-TW/guides/configuration.md +32 -6
- package/docs/zh-TW/spec/README.md +4 -4
- package/docs/zh-TW/spec/harness-adapters.md +6 -3
- package/docs/zh-TW/spec/review.md +10 -0
- package/package.json +1 -1
- package/schemas/config.schema.json +12 -2
|
@@ -71,7 +71,7 @@ async function detectVerificationCommands(root) {
|
|
|
71
71
|
.filter((proposal) => proposal.confidence === "high")
|
|
72
72
|
.map(verificationCommandFromProposal);
|
|
73
73
|
}
|
|
74
|
-
function buildConfig(profiles, existing, reviewTargets = [], detectedCommands = []) {
|
|
74
|
+
function buildConfig(profiles, existing, reviewTargets = [], detectedCommands = [], completionGateEnabled = false) {
|
|
75
75
|
// Absent reviewRoles means external review is disabled; an empty selection
|
|
76
76
|
// must therefore omit the field rather than write an empty array.
|
|
77
77
|
const reviewRoles = reviewTargets.length > 0
|
|
@@ -88,6 +88,9 @@ function buildConfig(profiles, existing, reviewTargets = [], detectedCommands =
|
|
|
88
88
|
features: existing?.features ?? {
|
|
89
89
|
stopVerification: {
|
|
90
90
|
enabled: false
|
|
91
|
+
},
|
|
92
|
+
completionGate: {
|
|
93
|
+
enabled: completionGateEnabled
|
|
91
94
|
}
|
|
92
95
|
},
|
|
93
96
|
pathMappings: existing?.pathMappings ?? [],
|
|
@@ -95,7 +98,7 @@ function buildConfig(profiles, existing, reviewTargets = [], detectedCommands =
|
|
|
95
98
|
...(reviewRoles === undefined ? {} : { reviewRoles: [...reviewRoles] })
|
|
96
99
|
};
|
|
97
100
|
}
|
|
98
|
-
async function planConfig(root, profiles, existingManifest, suppliedConfig, reviewTargets = []) {
|
|
101
|
+
async function planConfig(root, profiles, existingManifest, suppliedConfig, reviewTargets = [], completionGateEnabled = false) {
|
|
99
102
|
const current = await readCurrentFile(root, CONFIG_PATH);
|
|
100
103
|
const owned = findOwnedArtifact(existingManifest, CONFIG_PATH);
|
|
101
104
|
if (current !== null && owned === undefined) {
|
|
@@ -131,7 +134,7 @@ async function planConfig(root, profiles, existingManifest, suppliedConfig, revi
|
|
|
131
134
|
existingConfig.verification.commands.length === 0
|
|
132
135
|
? await detectVerificationCommands(root)
|
|
133
136
|
: [];
|
|
134
|
-
const config = buildConfig(profiles, existingConfig, reviewTargets, detectedCommands);
|
|
137
|
+
const config = buildConfig(profiles, existingConfig, reviewTargets, detectedCommands, completionGateEnabled);
|
|
135
138
|
const content = `${JSON.stringify(config, null, 2)}\n`;
|
|
136
139
|
return {
|
|
137
140
|
operation: {
|
|
@@ -376,6 +379,18 @@ export async function createInstallPlan(options) {
|
|
|
376
379
|
? resolveProfiles(options.profiles)
|
|
377
380
|
: resolveCapabilities(options.existingConfig.value);
|
|
378
381
|
assertLoopProfileSupport(options.scope, options.harness, resolved.capabilities);
|
|
382
|
+
const completionGateEnabled = options.existingConfig?.value.features.completionGate.enabled ??
|
|
383
|
+
options.completionGateEnabled === true;
|
|
384
|
+
if (completionGateEnabled &&
|
|
385
|
+
(options.scope !== "project" ||
|
|
386
|
+
!options.harness.includes("agy") ||
|
|
387
|
+
!resolved.capabilities.includes("project-loop"))) {
|
|
388
|
+
throw new AgentOpsError("COMPLETION_GATE_UNSUPPORTED", "The completion gate requires project scope with the agy harness and loop profile.");
|
|
389
|
+
}
|
|
390
|
+
if (completionGateEnabled &&
|
|
391
|
+
!resolved.capabilities.includes("completion-gate")) {
|
|
392
|
+
resolved.capabilities.push("completion-gate");
|
|
393
|
+
}
|
|
379
394
|
await assertCodexLoopConfiguration(options.root, options.harness, resolved.capabilities);
|
|
380
395
|
const existing = await readExistingManifest(options.root);
|
|
381
396
|
assertCompatibleManifest(existing?.manifest ?? null, options.scope, options.harness, options.allowHarnessChange === true);
|
|
@@ -461,7 +476,7 @@ export async function createInstallPlan(options) {
|
|
|
461
476
|
: [];
|
|
462
477
|
const operations = [];
|
|
463
478
|
const artifacts = [];
|
|
464
|
-
const config = await planConfig(options.root, resolved.profiles, existing?.manifest ?? null, options.existingConfig, options.reviewTargets ?? []);
|
|
479
|
+
const config = await planConfig(options.root, resolved.profiles, existing?.manifest ?? null, options.existingConfig, options.reviewTargets ?? [], completionGateEnabled);
|
|
465
480
|
operations.push(config.operation);
|
|
466
481
|
artifacts.push(config.record);
|
|
467
482
|
for (const artifact of contribution.artifacts) {
|
|
@@ -33,5 +33,8 @@ export function resolveCapabilities(config) {
|
|
|
33
33
|
if (config.features.stopVerification.enabled) {
|
|
34
34
|
resolved.capabilities.push("optional-stop-verify");
|
|
35
35
|
}
|
|
36
|
+
if (config.features.completionGate.enabled) {
|
|
37
|
+
resolved.capabilities.push("completion-gate");
|
|
38
|
+
}
|
|
36
39
|
return resolved;
|
|
37
40
|
}
|
|
@@ -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),
|
|
@@ -328,7 +328,7 @@ export function validateConfig(value) {
|
|
|
328
328
|
if (!isRecord(root.features)) {
|
|
329
329
|
return failure("INVALID_TYPE", "$.features", "features must be an object.");
|
|
330
330
|
}
|
|
331
|
-
const featuresUnknown = unknownFieldFailure(root.features, ["stopVerification"], "$.features");
|
|
331
|
+
const featuresUnknown = unknownFieldFailure(root.features, ["completionGate", "stopVerification"], "$.features");
|
|
332
332
|
if (featuresUnknown !== undefined) {
|
|
333
333
|
return featuresUnknown;
|
|
334
334
|
}
|
|
@@ -342,6 +342,16 @@ export function validateConfig(value) {
|
|
|
342
342
|
if (typeof root.features.stopVerification.enabled !== "boolean") {
|
|
343
343
|
return failure("INVALID_FEATURE", "$.features.stopVerification.enabled", "stopVerification.enabled must be a boolean.");
|
|
344
344
|
}
|
|
345
|
+
if (!isRecord(root.features.completionGate)) {
|
|
346
|
+
return failure("INVALID_TYPE", "$.features.completionGate", "completionGate must be an object.");
|
|
347
|
+
}
|
|
348
|
+
const completionGateUnknown = unknownFieldFailure(root.features.completionGate, ["enabled"], "$.features.completionGate");
|
|
349
|
+
if (completionGateUnknown !== undefined) {
|
|
350
|
+
return completionGateUnknown;
|
|
351
|
+
}
|
|
352
|
+
if (typeof root.features.completionGate.enabled !== "boolean") {
|
|
353
|
+
return failure("INVALID_FEATURE", "$.features.completionGate.enabled", "completionGate.enabled must be a boolean.");
|
|
354
|
+
}
|
|
345
355
|
if (!isRecord(root.verification)) {
|
|
346
356
|
return failure("INVALID_TYPE", "$.verification", "verification must be an object.");
|
|
347
357
|
}
|
|
@@ -77,6 +77,9 @@ export class TaskService {
|
|
|
77
77
|
!/^[a-f0-9]{64}$/u.test(input.policyConfigHash)) {
|
|
78
78
|
throw taskError("TASK_POLICY_CONFIG_INVALID", "Policy config hash must be a lowercase SHA-256 digest.");
|
|
79
79
|
}
|
|
80
|
+
if (input.sessionId !== undefined) {
|
|
81
|
+
assertSessionId(input.sessionId);
|
|
82
|
+
}
|
|
80
83
|
const task = {
|
|
81
84
|
schemaVersion: TASK_SCHEMA_VERSION,
|
|
82
85
|
id: this.#generateId(),
|
|
@@ -118,6 +121,20 @@ export class TaskService {
|
|
|
118
121
|
policyConfigHash: input.policyConfigHash ?? null
|
|
119
122
|
};
|
|
120
123
|
state.tasks.push(record);
|
|
124
|
+
if (input.sessionId !== undefined) {
|
|
125
|
+
const currentIndex = state.sessions.findIndex(({ sessionId }) => sessionId === input.sessionId);
|
|
126
|
+
const attachment = {
|
|
127
|
+
sessionId: input.sessionId,
|
|
128
|
+
taskId: record.task.id,
|
|
129
|
+
attachedAt: now
|
|
130
|
+
};
|
|
131
|
+
if (currentIndex === -1) {
|
|
132
|
+
state.sessions.push(attachment);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
state.sessions[currentIndex] = attachment;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
121
138
|
return cloneRecord(record);
|
|
122
139
|
});
|
|
123
140
|
}
|
|
@@ -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,
|
|
@@ -6,10 +6,12 @@ Use `--harness all` to select agy, Codex, Claude Code, and opencode, or pass a
|
|
|
6
6
|
comma-separated subset such as `codex,opencode`. `both` remains an input alias
|
|
7
7
|
for the legacy Codex plus Claude selection.
|
|
8
8
|
|
|
9
|
-
Project agy
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
Project agy uses a managed supplemental `GEMINI.md` routing block and
|
|
10
|
+
`.agent-ops/GEMINI.md` baseline. This follows the official agy CLI rule that a
|
|
11
|
+
workspace-root `GEMINI.md` or `AGENTS.md` is loaded at startup. Codex and
|
|
12
|
+
opencode share the corresponding `AGENTS.md` route and `.agent-ops/AGENTS.md`
|
|
13
|
+
artifact. Each block loads the managed baseline while project-specific
|
|
14
|
+
instructions remain authoritative. Claude uses the corresponding `CLAUDE.md` route and
|
|
13
15
|
`.agent-ops/CLAUDE.md` artifact. Opencode additionally gets
|
|
14
16
|
the agent-ops-owned `.opencode/plugins/agent-ops.js` file; `opencode.json` is
|
|
15
17
|
never modified. The plugin is generated with the installed absolute runtime
|
|
@@ -53,6 +55,12 @@ A bare review uses the built-in `change-quality` criterion; `--task` uses the
|
|
|
53
55
|
task criteria and requires fresh PASS evidence for required checks. The full
|
|
54
56
|
report is printed, and PASS persists only a source-fingerprint attestation.
|
|
55
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
|
+
|
|
56
64
|
Every attempt starts from a fresh session and disposable repository clone with
|
|
57
65
|
native read-only mode.
|
|
58
66
|
Claude uses complete safe-mode isolation. Codex and Agy preserve their existing
|
|
@@ -86,6 +94,12 @@ failure, oversized output, or unparseable output. Every attempt and reason is
|
|
|
86
94
|
preserved in human and JSON output. A `PASS` or `FAIL` verdict is **terminal**,
|
|
87
95
|
so the chain cannot shop for a passing review.
|
|
88
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
|
+
|
|
89
103
|
If Claude Code is the host (`CLAUDECODE` is set), `claude` is moved to the end
|
|
90
104
|
of the chain. It still runs when it is the only configured target, with a
|
|
91
105
|
`reviewer == host` warning.
|
|
@@ -147,6 +161,21 @@ user hooks live in `.gemini/config/hooks.json`. User-scope rules modify the
|
|
|
147
161
|
shared Gemini rule surface at `.gemini/GEMINI.md`. agy 1.1.12 or newer is
|
|
148
162
|
required for machine-readable `/hooks` diagnostics.
|
|
149
163
|
|
|
164
|
+
For `agy` plus `loop`, the interactive installer recommends
|
|
165
|
+
`features.completionGate.enabled`; non-interactive installs require the explicit
|
|
166
|
+
`--completion-gate` flag. The gate uses the documented `conversationId`,
|
|
167
|
+
`terminationReason`, and `fullyIdle` Stop fields and returns the documented
|
|
168
|
+
`decision: "continue"` only for a final changed conversation that lacks current
|
|
169
|
+
task, verification, or review proof. Pure Q&A, analysis, read-only diagnostics,
|
|
170
|
+
error stops, max-step stops, and non-idle stops continue normally. It does not
|
|
171
|
+
change Codex, Claude Code, or OpenCode Stop behavior. For headless execution use
|
|
172
|
+
`agent-ops agy-run -- <agy arguments>`; a user-approved one-time escape is
|
|
173
|
+
`agent-ops allow-stop --session <conversationId>` and is guarded by agy's
|
|
174
|
+
documented `force_ask` decision.
|
|
175
|
+
|
|
176
|
+
Official references: [agy CLI workspace rule files](https://www.antigravity.google/docs/cli/best-practices/)
|
|
177
|
+
and [Antigravity hook contracts](https://www.antigravity.google/docs/hooks/).
|
|
178
|
+
|
|
150
179
|
The full Codex/Claude loop runs `SessionStart`, `UserPromptSubmit`, `PreToolUse`,
|
|
151
180
|
`PermissionRequest`, `PostToolUse`, `PreCompact`, `PostCompact`,
|
|
152
181
|
`SubagentStart`, and `SubagentStop`, but never adds `Stop`. It blocks only
|
|
@@ -183,8 +212,9 @@ classified invalid installed configuration. The managed OpenCode
|
|
|
183
212
|
`tool.execute.before` plugin can throw its documented command-policy denial or
|
|
184
213
|
unavailable-runtime error for its supported Bash surface. Codex is explicitly
|
|
185
214
|
non-enforcing (`unknown`). These are agent-ops output and plugin contracts, not
|
|
186
|
-
proof that a host honors a denial. `SessionStart` and
|
|
187
|
-
fail-open
|
|
215
|
+
proof that a host honors a denial. `SessionStart` and ordinary Stop verification
|
|
216
|
+
failure paths stay fail-open. Only the explicitly enabled agy completion gate
|
|
217
|
+
fails closed at final Stop.
|
|
188
218
|
|
|
189
219
|
Claude's invalid-config fallback has four safeguards: (1) an absent project
|
|
190
220
|
configuration stays fail-open, so only an invalid `.agent-ops/config.json` can
|
|
@@ -196,7 +226,7 @@ that shell variable. The variable is read only from the hook-process environment
|
|
|
196
226
|
and cannot be set in agent-ops configuration, a manifest, or managed files.
|
|
197
227
|
|
|
198
228
|
`guardrails` installs command policy but does not enable Stop verification. Stop
|
|
199
|
-
is a separate config-
|
|
229
|
+
is a separate config-v3 feature and must be explicitly enabled with at least
|
|
200
230
|
one confirmed command:
|
|
201
231
|
|
|
202
232
|
```json
|
package/docs/en/spec/README.md
CHANGED
|
@@ -4,12 +4,12 @@ This is the normative English specification for bounded, evidence-driven work.
|
|
|
4
4
|
|
|
5
5
|
The harness adapter rules cover agy, Codex, Claude Code, and opencode. The
|
|
6
6
|
opencode integration is a generated local plugin; it does not manage
|
|
7
|
-
`opencode.json`. agy
|
|
7
|
+
`opencode.json`. agy uses project `GEMINI.md` routing, uses native hooks, and
|
|
8
8
|
is explicitly degraded where its lifecycle surface is smaller than the full
|
|
9
9
|
loop.
|
|
10
10
|
|
|
11
|
-
Configuration is versioned independently from the manifest.
|
|
12
|
-
to config
|
|
11
|
+
Configuration is versioned independently from the manifest. Older configs migrate
|
|
12
|
+
to config v3 with Stop verification and the agy completion gate disabled; changing a capability requires
|
|
13
13
|
a confirmed project `agent-ops update`, which also refreshes trust when
|
|
14
14
|
verifiers exist. Stop verification is
|
|
15
15
|
explicit, trusted, report-only, and never completes a task. Dry-run plans keep
|
|
@@ -108,6 +108,7 @@ The current registration matrix is intentionally asymmetric:
|
|
|
108
108
|
| --- | --- | --- | --- | --- |
|
|
109
109
|
| lifecycle-summary | degraded | supported | supported | degraded |
|
|
110
110
|
| command-policy | supported | unknown | supported | supported |
|
|
111
|
+
| completion-gate | supported | unsupported | unsupported | unsupported |
|
|
111
112
|
| optional-stop-verify | degraded | unsupported | supported | degraded |
|
|
112
113
|
|
|
113
114
|
For runtime-failure handling, only `command-policy` is fail-closed. Claude
|
|
@@ -118,11 +119,14 @@ surface. Codex remains `unknown` and never emits a denial. Fixture tests assert
|
|
|
118
119
|
these wire and plugin shapes only; they do not prove that a host honors a
|
|
119
120
|
denial. Every `SessionStart` and `Stop` failure path remains fail-open.
|
|
120
121
|
|
|
121
|
-
The agy adapter uses project `
|
|
122
|
+
The agy adapter uses native project `GEMINI.md` routing to
|
|
123
|
+
`.agent-ops/GEMINI.md`;
|
|
122
124
|
at user scope it manages `.agent-ops/GEMINI.md` and a managed block in the
|
|
123
125
|
shared `.gemini/GEMINI.md` rule surface. Its native hooks use camelCase input,
|
|
124
|
-
return `decision: "deny"` for command-policy blocks, and
|
|
125
|
-
|
|
126
|
+
return `decision: "deny"` for command-policy blocks, and, only when the
|
|
127
|
+
completion gate is explicitly enabled, return `decision: "continue"` for an
|
|
128
|
+
unproven final changed conversation. Read-only conversations stop normally.
|
|
129
|
+
On Windows the generated command is invoked through `cmd /c`.
|
|
126
130
|
|
|
127
131
|
Stop verification is explicit, trusted, report-only, and disabled by default.
|
|
128
132
|
Every Stop result continues the native harness and may carry only bounded
|
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
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
`codex,opencode` 這類逗號分隔的子集。`both` 仍是 legacy Codex 加 Claude
|
|
7
7
|
selection 的 input alias。
|
|
8
8
|
|
|
9
|
-
Project agy
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
Project agy 使用 managed supplemental `GEMINI.md` routing block 與
|
|
10
|
+
`.agent-ops/GEMINI.md` baseline;這符合 agy CLI 官方文件所述,啟動時會讀取
|
|
11
|
+
workspace root 的 `GEMINI.md` 或 `AGENTS.md`。Codex 與 opencode 共用
|
|
12
|
+
`AGENTS.md` route 與 `.agent-ops/AGENTS.md` artifact。這些 block 只載入
|
|
13
|
+
managed baseline,並保留 project-specific instructions 的權威性。
|
|
12
14
|
Claude 使用對應的 `CLAUDE.md` route 與 `.agent-ops/CLAUDE.md` artifact。Opencode 另外取得
|
|
13
15
|
agent-ops 擁有的 `.opencode/plugins/agent-ops.js`;不會修改 `opencode.json`。
|
|
14
16
|
Plugin 使用安裝時的 absolute runtime path 產生,因此請透過
|
|
@@ -47,6 +49,11 @@ Claude 與 Codex lifecycle support 為 `supported`,OpenCode 從 app initializa
|
|
|
47
49
|
`--task` 使用 task criteria,並要求必要驗證的最新 PASS evidence。完整 report
|
|
48
50
|
會顯示給人看,PASS 後只持久化 source-fingerprint attestation。
|
|
49
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
|
+
|
|
50
57
|
每次嘗試都從全新 session、一次性 repository clone 與原生唯讀模式啟動。Claude 使用完整 safe-mode
|
|
51
58
|
隔離;Codex 與 Agy 為了支援既有 OAuth 登入而保留登入環境,因此 context
|
|
52
59
|
隔離較弱。Agy 會取得一次性 clone,即使 sandboxed plan mode 寫入 cwd,也無法
|
|
@@ -75,6 +82,11 @@ agent-ops 刻意不傳會繞過權限邊界的 `--dangerously-skip-permissions`
|
|
|
75
82
|
都會保留每次 attempt 及原因。`PASS` 或 `FAIL` 判定是**終局**,因此不會產生
|
|
76
83
|
自動化的 review shopping。
|
|
77
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
|
+
|
|
78
90
|
若 host 是 Claude Code(`CLAUDECODE` 已設定),`claude` 會被移到鏈尾。
|
|
79
91
|
當它是唯一設定的目標時仍會執行,並附上 `reviewer == host` 警告。
|
|
80
92
|
|
|
@@ -131,6 +143,20 @@ agy 會安裝原生 `PreInvocation` 與 `PreToolUse(run_command)` 子集,docto
|
|
|
131
143
|
位於 `.gemini/config/hooks.json`;user scope 會修改共享 Gemini rule surface
|
|
132
144
|
`.gemini/GEMINI.md`。機器可讀的 `/hooks` 診斷要求 agy 1.1.12 以上。
|
|
133
145
|
|
|
146
|
+
`agy` 搭配 `loop` 時,互動式 installer 會建議啟用
|
|
147
|
+
`features.completionGate.enabled`;非互動安裝必須明確傳入
|
|
148
|
+
`--completion-gate`。閘門使用官方定義的 `conversationId`、
|
|
149
|
+
`terminationReason` 與 `fullyIdle` Stop 欄位,只有在本次 conversation 產生
|
|
150
|
+
Git-visible net change 且缺少當前 task、驗證或 review 證據時,才回傳官方定義的
|
|
151
|
+
`decision: "continue"`。純問答、分析、唯讀診斷、錯誤、max-step 與 non-idle Stop
|
|
152
|
+
都正常結束;本版不改變 Codex、Claude Code 或 OpenCode 的 Stop 行為。Headless
|
|
153
|
+
請使用 `agent-ops agy-run -- <agy arguments>`;使用者可核准一次
|
|
154
|
+
`agent-ops allow-stop --session <conversationId>`,該命令由官方定義的
|
|
155
|
+
`force_ask` 強制詢問。
|
|
156
|
+
|
|
157
|
+
官方依據:[agy CLI workspace rule file](https://www.antigravity.google/docs/cli/best-practices/)
|
|
158
|
+
與 [Antigravity hook contract](https://www.antigravity.google/docs/hooks/)。
|
|
159
|
+
|
|
134
160
|
完整 Codex/Claude loop 會執行 `SessionStart`、`UserPromptSubmit`、`PreToolUse`、
|
|
135
161
|
`PermissionRequest`、`PostToolUse`、`PreCompact`、`PostCompact`、
|
|
136
162
|
`SubagentStart` 與 `SubagentStop`,但永遠不加入 `Stop`。它只攔截
|
|
@@ -164,8 +190,8 @@ hook 文件](https://code.claude.com/docs/en/hooks)。
|
|
|
164
190
|
OpenCode `tool.execute.before` plugin 可在其支援的 Bash surface
|
|
165
191
|
上 throw 文件化的 command-policy denial 或 unavailable-runtime error。Codex 明確
|
|
166
192
|
不執行強制措施(`unknown`)。這些是 agent-ops 的 output 與 plugin contract,不
|
|
167
|
-
證明 host 會實際遵守 denial。所有
|
|
168
|
-
都維持 fail-open。
|
|
193
|
+
證明 host 會實際遵守 denial。所有 `SessionStart` 與一般 Stop verification failure
|
|
194
|
+
path 都維持 fail-open;只有明確啟用的 agy completion gate 會在 final Stop fail-closed。
|
|
169
195
|
|
|
170
196
|
Claude 的無效 config fallback 有四項防護:(1) 缺少 project configuration 時保持
|
|
171
197
|
fail-open,因此只有無效的 `.agent-ops/config.json` 能進入 fallback;(2) manifest
|
|
@@ -176,7 +202,7 @@ hook-process environment 讀取,不能由 agent-ops configuration、manifest
|
|
|
176
202
|
managed file 設定。
|
|
177
203
|
|
|
178
204
|
`guardrails` 只安裝 command policy,不會啟用 Stop verification。Stop 是獨立的
|
|
179
|
-
config
|
|
205
|
+
config v3 feature,必須明確啟用且至少提供一個已確認的 command:
|
|
180
206
|
|
|
181
207
|
```json
|
|
182
208
|
{
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
此目錄是英文規範的繁體中文導讀;規範 rule ID 以英文版本為準。
|
|
4
4
|
|
|
5
5
|
Harness adapter 規則涵蓋 agy、Codex、Claude Code 與 opencode。opencode 整合是
|
|
6
|
-
產生的 local plugin,不管理 `opencode.json`。agy 在 project scope
|
|
7
|
-
`
|
|
6
|
+
產生的 local plugin,不管理 `opencode.json`。agy 在 project scope 使用
|
|
7
|
+
`GEMINI.md` routing 與原生 hooks,對不足以完整支援 loop 的 lifecycle
|
|
8
8
|
能力明確標示 degraded。
|
|
9
9
|
|
|
10
|
-
Configuration 與 manifest
|
|
11
|
-
|
|
10
|
+
Configuration 與 manifest 分開版本化。舊 config 會遷移為 Stop verification 與
|
|
11
|
+
agy completion gate 預設 disabled 的 config v3;變更 capability 後必須執行經確認的 project
|
|
12
12
|
`agent-ops update`,有 verifier 時會一併更新 trust。Stop verification 必須明確啟用、具備 trust、
|
|
13
13
|
為 report-only,且永遠不會完成 task。Dry-run plan 會隱藏 foreign settings
|
|
14
14
|
內容;routing migration 一旦套用即為單向。
|
|
@@ -92,6 +92,7 @@ script,也不得改變一般 permission request。
|
|
|
92
92
|
| --- | --- | --- | --- | --- |
|
|
93
93
|
| lifecycle-summary | degraded | supported | supported | degraded |
|
|
94
94
|
| command-policy | supported | unknown | supported | supported |
|
|
95
|
+
| completion-gate | supported | unsupported | unsupported | unsupported |
|
|
95
96
|
| optional-stop-verify | degraded | unsupported | supported | degraded |
|
|
96
97
|
|
|
97
98
|
Runtime-failure 處理中,只有 `command-policy` 為 fail-closed。當已安裝的 config
|
|
@@ -101,11 +102,13 @@ denial 或 unavailable-runtime error。Codex 維持 `unknown` 且絕不輸出 de
|
|
|
101
102
|
Fixture test 只斷言這些 wire 與 plugin shape;它們不證明 host 會實際遵守 denial。
|
|
102
103
|
每個 `SessionStart` 與 `Stop` failure path 都維持 fail-open。
|
|
103
104
|
|
|
104
|
-
agy adapter 在 project scope
|
|
105
|
+
agy adapter 在 project scope 使用原生 `GEMINI.md` routing 到
|
|
106
|
+
`.agent-ops/GEMINI.md`;user
|
|
105
107
|
scope 管理 `.agent-ops/GEMINI.md` 與 shared `.gemini/GEMINI.md` rule surface。
|
|
106
108
|
其 native hook 使用 camelCase input,command-policy block 回傳
|
|
107
|
-
`decision: "deny"
|
|
108
|
-
|
|
109
|
+
`decision: "deny"`。只有明確啟用 completion gate 時,未具完備證據的 final
|
|
110
|
+
changed conversation 才回傳 `decision: "continue"`;唯讀 conversation 正常
|
|
111
|
+
結束。在 Windows 透過 `cmd /c` 呼叫產生的 command。
|
|
109
112
|
|
|
110
113
|
Stop verification 必須明確啟用、具備 trust、為 report-only 且預設 disabled。
|
|
111
114
|
每個 Stop 結果都會讓 native harness 繼續,最多攜帶有界 command evidence,永遠
|