@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.
Files changed (43) hide show
  1. package/README.md +23 -4
  2. package/dist/packages/cli/src/agy-headless.js +18 -0
  3. package/dist/packages/cli/src/args.js +35 -5
  4. package/dist/packages/cli/src/bin.js +115 -37
  5. package/dist/packages/cli/src/cli.js +4 -1
  6. package/dist/packages/cli/src/commands/allow-stop.js +11 -0
  7. package/dist/packages/cli/src/commands/hook.js +12 -1
  8. package/dist/packages/cli/src/commands/init.js +4 -1
  9. package/dist/packages/cli/src/commands/review.js +31 -21
  10. package/dist/packages/cli/src/commands/task.js +5 -2
  11. package/dist/packages/cli/src/context.js +4 -1
  12. package/dist/packages/cli/src/hook-process.js +59 -5
  13. package/dist/packages/cli/src/wizard.js +24 -0
  14. package/dist/runtime/src/adapters/agy/config.js +17 -7
  15. package/dist/runtime/src/adapters/agy/events.js +8 -0
  16. package/dist/runtime/src/adapters/agy/input.js +21 -3
  17. package/dist/runtime/src/adapters/agy/output.js +8 -4
  18. package/dist/runtime/src/config/explain.js +5 -0
  19. package/dist/runtime/src/config/migrate.js +15 -1
  20. package/dist/runtime/src/contracts.js +1 -1
  21. package/dist/runtime/src/hooks/completion-gate.js +209 -0
  22. package/dist/runtime/src/hooks/dispatch.js +6 -0
  23. package/dist/runtime/src/install/doctor.js +3 -1
  24. package/dist/runtime/src/install/harness.js +5 -1
  25. package/dist/runtime/src/install/ownership.js +9 -2
  26. package/dist/runtime/src/install/plan.js +19 -4
  27. package/dist/runtime/src/install/profiles.js +3 -0
  28. package/dist/runtime/src/review/execute.js +40 -7
  29. package/dist/runtime/src/review/render.js +3 -1
  30. package/dist/runtime/src/review/runner.js +1 -0
  31. package/dist/runtime/src/schema/validate.js +11 -1
  32. package/dist/runtime/src/task/service.js +17 -0
  33. package/dist/runtime/src/verify/spawn.js +30 -8
  34. package/docs/en/guides/configuration.md +37 -7
  35. package/docs/en/spec/README.md +3 -3
  36. package/docs/en/spec/harness-adapters.md +7 -3
  37. package/docs/en/spec/review.md +11 -0
  38. package/docs/zh-TW/guides/configuration.md +32 -6
  39. package/docs/zh-TW/spec/README.md +4 -4
  40. package/docs/zh-TW/spec/harness-adapters.md +6 -3
  41. package/docs/zh-TW/spec/review.md +10 -0
  42. package/package.json +1 -1
  43. 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 suppressed under --json,
347
- // and without this a PASS with no `adversarial` field cannot be told
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 = true;
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 = timedOut
350
+ const classified = wasAborted
334
351
  ? {
335
352
  status: terminationFailed ? "UNKNOWN" : "FAIL",
336
- failureClass: terminationFailed
337
- ? "termination-failed"
338
- : "timeout"
353
+ failureClass: "aborted"
339
354
  }
340
- : classifyCompletion(completion, capturedStdout.failed || capturedStderr.failed);
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, Codex, and opencode installations share the managed supplemental
10
- `AGENTS.md` routing block and the `.agent-ops/AGENTS.md` rules artifact. The
11
- block loads the managed baseline while project-specific instructions remain
12
- authoritative. Claude uses the corresponding `CLAUDE.md` route and
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 `Stop` failure paths stay
187
- fail-open for every adapter.
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-v2 feature and must be explicitly enabled with at least
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
@@ -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 shares project `AGENTS.md` routing, uses native hooks, and
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. Config v1 migrates
12
- to config v2 with Stop verification disabled; changing the capability requires
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 `AGENTS.md` routing alongside Codex and OpenCode;
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 never force a Stop
125
- continuation. On Windows the generated command is invoked through `cmd /c`.
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
@@ -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、Codex opencode installation 共用 managed supplemental
10
- `AGENTS.md` routing block 與 `.agent-ops/AGENTS.md` rules artifact。該 block
11
- 只載入 managed baseline,並保留 project-specific instructions 的權威性。
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。所有 adapter 的 `SessionStart` `Stop` failure path
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 v2 feature,必須明確啟用且至少提供一個已確認的 command:
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
- `AGENTS.md` routing、使用原生 hooks,對不足以完整支援 loop 的 lifecycle
6
+ 產生的 local plugin,不管理 `opencode.json`。agy 在 project scope 使用
7
+ `GEMINI.md` routing 與原生 hooks,對不足以完整支援 loop 的 lifecycle
8
8
  能力明確標示 degraded。
9
9
 
10
- Configuration 與 manifest 分開版本化。Config v1 會遷移為預設 disabled Stop
11
- verification 的 config v2;變更 capability 後必須執行經確認的 project
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 Codex、OpenCode 共用 `AGENTS.md` routing;user
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"`,且不會強迫 Stop 繼續。在 Windows 透過 `cmd /c` 呼叫產生的
108
- command。
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,永遠