@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
package/README.md CHANGED
@@ -68,7 +68,8 @@ agent-ops uninstall --dry-run --json
68
68
 
69
69
  `loop` is an explicit, project-only profile for agy, Codex, and Claude Code.
70
70
  agy installs its native `PreInvocation`, `PreToolUse(run_command)`, and optional
71
- `Stop` subset directly in `.agents/hooks.json`; doctor reports its loop as
71
+ `Stop` subset directly in `.agents/hooks.json`, plus project routing in
72
+ `GEMINI.md` to `.agent-ops/GEMINI.md`; doctor reports its loop as
72
73
  degraded because prompt, permission, compact, and subagent events are unavailable.
73
74
  Claude Code supports native Windows through a generated PowerShell launcher;
74
75
  Codex's loop launcher still requires POSIX-compatible `bash`. Preview it
@@ -80,6 +81,24 @@ agent-ops init --dry-run --scope project --harness codex,claude --profile loop -
80
81
  agent-ops init --scope project --harness codex,claude --profile loop --yes
81
82
  ```
82
83
 
84
+ For a project agy loop, the wizard recommends the hard completion gate. In
85
+ automation, opt in explicitly:
86
+
87
+ ```bash
88
+ agent-ops init --scope project --harness agy --profile loop --completion-gate --yes
89
+ ```
90
+
91
+ The gate lets pure Q&A, analysis, and read-only diagnostics stop normally. It
92
+ activates only after that conversation creates a Git-visible net change, then
93
+ requires an attached completed task with two to five criteria, current PASS
94
+ evidence, and a PASS review for the current source fingerprint. It never runs
95
+ tests or review from `Stop`. Error, max-step, and non-idle stops continue. A
96
+ user can approve one fingerprint-bound escape with
97
+ `agent-ops allow-stop --session <conversationId>`. Use
98
+ `agent-ops agy-run -- <agy arguments>` for a process-exit recheck in headless or
99
+ CI use. Codex, Claude Code, and OpenCode retain their existing non-blocking Stop
100
+ behavior in this release.
101
+
83
102
  On native Windows, select `--harness claude` unless Codex is running in a
84
103
  POSIX environment; the Codex loop still invokes `bash`.
85
104
 
@@ -250,10 +269,10 @@ After changing Stop configuration, run `agent-ops update`; its approved plan
250
269
  updates native registrations and the exact trust binding together. Stop is
251
270
  report-only: `PASS`, `FAIL`, and `UNKNOWN` continue
252
271
  the harness, emit bounded command evidence, and never complete a task.
253
- Config v1 migrates to config v2 with Stop disabled; migration invalidates the
254
- old trust binding, and pre-v1 binaries cannot read the migrated config.
272
+ Older configs migrate to config v3 with both Stop verification and the agy
273
+ completion gate disabled; migration invalidates the old trust binding.
255
274
 
256
- The generated `AGENTS.md` and `CLAUDE.md` routing blocks are supplemental: they
275
+ The generated `AGENTS.md`, `CLAUDE.md`, and agy `GEMINI.md` routing blocks are supplemental: they
257
276
  load the managed baseline while leaving project-specific instructions in those
258
277
  files authoritative. Existing installations with the previous canonical
259
278
  wording are migrated by `agent-ops update`; changed managed blocks still fail
@@ -0,0 +1,18 @@
1
+ /** Headless wrapper: native Stop is bounded by agy, process exit is not. */
2
+ export async function runAgyHeadless(options) {
3
+ await options.gate.initialize(options.sessionId);
4
+ const exitCode = await options.run(options.args, {
5
+ ...(options.env ?? process.env),
6
+ AGENT_OPS_SESSION_ID: options.sessionId
7
+ });
8
+ if (exitCode !== 0)
9
+ return exitCode;
10
+ const result = await options.gate.handle({
11
+ event: "stop",
12
+ projectRoot: options.root,
13
+ sessionId: options.sessionId,
14
+ terminationReason: "model_stop",
15
+ fullyIdle: true
16
+ });
17
+ return result?.action === "block" ? 2 : 0;
18
+ }
@@ -8,7 +8,8 @@ export const COMMAND_NAMES = [
8
8
  "uninstall",
9
9
  "task",
10
10
  "verify",
11
- "review"
11
+ "review",
12
+ "allow-stop"
12
13
  ];
13
14
  const COMMAND_SET = new Set(COMMAND_NAMES);
14
15
  const SCOPES = new Set(["project", "user"]);
@@ -73,6 +74,7 @@ export function parseArgs(argv) {
73
74
  const criteria = [];
74
75
  const evidence = [];
75
76
  let checkAuth = false;
77
+ let completionGate;
76
78
  let dryRun = false;
77
79
  let json = false;
78
80
  let yes = false;
@@ -203,6 +205,12 @@ export function parseArgs(argv) {
203
205
  }
204
206
  checkAuth = true;
205
207
  break;
208
+ case "--completion-gate":
209
+ if (completionGate !== undefined) {
210
+ duplicate(token);
211
+ }
212
+ completionGate = true;
213
+ break;
206
214
  case "--dry-run":
207
215
  if (dryRun) {
208
216
  duplicate(token);
@@ -291,6 +299,7 @@ export function parseArgs(argv) {
291
299
  criteria.length > 0 ||
292
300
  evidence.length > 0 ||
293
301
  reviewTargets.length > 0 ||
302
+ completionGate !== undefined ||
294
303
  sessionId !== undefined ||
295
304
  base !== undefined ||
296
305
  checkAuth ||
@@ -317,6 +326,7 @@ export function parseArgs(argv) {
317
326
  if (command !== "task" &&
318
327
  command !== "verify" &&
319
328
  command !== "review" &&
329
+ command !== "allow-stop" &&
320
330
  (hasTaskTargetOptions || hasTaskMutationOptions)) {
321
331
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Task target options may be used only with task or verify.");
322
332
  }
@@ -338,7 +348,12 @@ export function parseArgs(argv) {
338
348
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--check-auth may be used only with doctor.");
339
349
  }
340
350
  if (reviewTargets.length > 0 && command !== "init") {
341
- throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--review-target may be used only with init.");
351
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", command === "review"
352
+ ? "--review-target configures init; for one review run, use --harness <target>."
353
+ : "--review-target may be used only with init.");
354
+ }
355
+ if (completionGate !== undefined && command !== "init") {
356
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--completion-gate may be used only with init.");
342
357
  }
343
358
  if (command === "task") {
344
359
  if (harness !== undefined ||
@@ -349,8 +364,7 @@ export function parseArgs(argv) {
349
364
  }
350
365
  const taskOptionInvalid = (action === "create" &&
351
366
  (taskId !== undefined ||
352
- evidence.length > 0 ||
353
- sessionId !== undefined)) ||
367
+ evidence.length > 0)) ||
354
368
  (action === "status" &&
355
369
  (title !== undefined ||
356
370
  criteria.length > 0 ||
@@ -400,9 +414,24 @@ export function parseArgs(argv) {
400
414
  (criteria.length > 0 || evidence.length > 0)) {
401
415
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Review criteria and evidence require --task.");
402
416
  }
403
- if (command === "review" && harness !== undefined && harness.length !== 1) {
417
+ if (command === "review" &&
418
+ harness !== undefined &&
419
+ (harness.length !== 1 || !REVIEW_TARGETS.has(harness[0] ?? ""))) {
404
420
  invalidValue("--harness", harness.join(","));
405
421
  }
422
+ if (command === "allow-stop" &&
423
+ (sessionId === undefined ||
424
+ scope !== undefined ||
425
+ harness !== undefined ||
426
+ taskId !== undefined ||
427
+ profiles.length > 0 ||
428
+ title !== undefined ||
429
+ criteria.length > 0 ||
430
+ evidence.length > 0 ||
431
+ dryRun ||
432
+ yes)) {
433
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "allow-stop requires only --session (and optional --json).");
434
+ }
406
435
  return {
407
436
  command,
408
437
  ...(action === undefined ? {} : { action }),
@@ -415,6 +444,7 @@ export function parseArgs(argv) {
415
444
  ...(targetVersion === undefined ? {} : { targetVersion }),
416
445
  ...(title === undefined ? {} : { title }),
417
446
  ...(reviewTargets.length === 0 ? {} : { reviewTargets }),
447
+ ...(completionGate === undefined ? {} : { completionGate }),
418
448
  ...(criteria.length === 0 ? {} : { criteria }),
419
449
  ...(evidence.length === 0 ? {} : { evidence }),
420
450
  ...(sessionId === undefined ? {} : { sessionId }),
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile } from "node:fs/promises";
3
+ import { randomUUID } from "node:crypto";
3
4
  import { homedir } from "node:os";
4
5
  import { join } from "node:path";
5
- import { execFileSync } from "node:child_process";
6
+ import { execFileSync, spawn } from "node:child_process";
6
7
  import { fileURLToPath } from "node:url";
7
8
  import { commonHarnessAdapters, harnessHookPath, HARNESS_IDS } from "../../../runtime/src/install/harness.js";
8
9
  import { agyRuntimeStatus, agyVersionSupported, hookRegistrationDrift, repositoryTrustStatus, smokeAvailabilityStatus } from "../../../runtime/src/install/probes.js";
@@ -28,13 +29,16 @@ import { formatInstallPlan, runInitCommand } from "./commands/init.js";
28
29
  import { formatUninstallPlan, runUninstallCommand } from "./commands/uninstall.js";
29
30
  import { runTaskCommand } from "./commands/task.js";
30
31
  import { runReviewCommand } from "./commands/review.js";
31
- import { createReviewExecutor } from "../../../runtime/src/review/execute.js";
32
+ import { createReviewExecutor, ReviewInterruptedError } from "../../../runtime/src/review/execute.js";
32
33
  import { probeReviewTarget } from "../../../runtime/src/review/probe.js";
33
- import { resolveReviewRole } from "../../../runtime/src/review/roles.js";
34
+ import { detectHostTarget, orderChain, resolveReviewRole } from "../../../runtime/src/review/roles.js";
34
35
  import { runTrustCommand } from "./commands/trust.js";
35
36
  import { runVerifyCommand } from "./commands/verify.js";
37
+ import { runAllowStopCommand } from "./commands/allow-stop.js";
38
+ import { CompletionGateService } from "../../../runtime/src/hooks/completion-gate.js";
36
39
  import { formatUpdatePlan, runUpdateCommand } from "./commands/update.js";
37
40
  import { errorEnvelope } from "./output.js";
41
+ import { runAgyHeadless } from "./agy-headless.js";
38
42
  const HOOK_RUNTIME_PATH = fileURLToPath(new URL("./hook-entry.js", import.meta.url));
39
43
  async function readOptionalText(path) {
40
44
  try {
@@ -122,7 +126,40 @@ function runAgy(args, options = {}) {
122
126
  ...(process.platform === "win32" ? { shell: true } : {})
123
127
  });
124
128
  }
125
- if (argv[0] === "hook") {
129
+ if (argv[0] === "agy-run") {
130
+ try {
131
+ const root = process.cwd();
132
+ const config = (await loadEffectiveConfig(root, "project")).config;
133
+ if (!config.features.completionGate.enabled) {
134
+ throw new Error("agy-run requires features.completionGate.enabled.");
135
+ }
136
+ const taskService = new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root));
137
+ const gate = new CompletionGateService({
138
+ root,
139
+ config,
140
+ gitRunner: gitRunner(root),
141
+ taskService,
142
+ evidenceStore: new FileEvidenceStore(root, root)
143
+ });
144
+ const agyArgs = argv[1] === "--" ? argv.slice(2) : argv.slice(1);
145
+ process.exitCode = await runAgyHeadless({
146
+ root,
147
+ sessionId: `agy-headless-${randomUUID()}`,
148
+ args: agyArgs,
149
+ gate,
150
+ run: async (args, env) => await new Promise((resolve) => {
151
+ const child = spawn("agy", [...args], { cwd: root, env, stdio: "inherit" });
152
+ child.once("error", () => resolve(1));
153
+ child.once("exit", (code) => resolve(code ?? 1));
154
+ })
155
+ });
156
+ }
157
+ catch (error) {
158
+ process.stderr.write(`${error instanceof Error ? error.message : "agy-run failed."}\n`);
159
+ process.exitCode = 2;
160
+ }
161
+ }
162
+ else if (argv[0] === "hook") {
126
163
  process.exitCode = await runHookProcess(argv.slice(1), {
127
164
  stdin: process.stdin,
128
165
  writeStdout: (value) => process.stdout.write(value),
@@ -147,7 +184,8 @@ else {
147
184
  "uninstall",
148
185
  "task",
149
186
  "verify",
150
- "review"
187
+ "review",
188
+ "allow-stop"
151
189
  ].map((command) => [command, async (args) => {
152
190
  const root = args.scope === "user"
153
191
  ? process.env.AGENT_OPS_HOME ?? homedir()
@@ -316,6 +354,19 @@ else {
316
354
  });
317
355
  }
318
356
  const taskService = new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root));
357
+ if (args.command === "allow-stop") {
358
+ const config = (await loadEffectiveConfig(root, "project")).config;
359
+ return await runAllowStopCommand({
360
+ args,
361
+ gate: new CompletionGateService({
362
+ root,
363
+ config,
364
+ gitRunner: gitRunner(root),
365
+ taskService,
366
+ evidenceStore: new FileEvidenceStore(root, root)
367
+ })
368
+ });
369
+ }
319
370
  if (args.command === "task") {
320
371
  const sessionId = process.env.AGENT_OPS_SESSION_ID;
321
372
  const policyConfigHash = args.action === "create"
@@ -332,42 +383,69 @@ else {
332
383
  const reviewSessionId = process.env.AGENT_OPS_SESSION_ID;
333
384
  const reviewConfig = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
334
385
  const reviewRole = resolveReviewRole("independent-review", reviewConfig.reviewRoles ?? []);
335
- return await runReviewCommand({
336
- args,
337
- authorized: args.yes,
338
- tasks: taskService,
339
- ...(args.taskId === undefined ? {} : { taskId: args.taskId }),
340
- ...(reviewSessionId === undefined
341
- ? {}
342
- : { sessionId: reviewSessionId }),
343
- ...(reviewConfig.reviewRoles === undefined
344
- ? {}
345
- : { roles: reviewConfig.reviewRoles }),
346
- root,
347
- gitRunner: gitRunner(root),
348
- policyConfigHash: calculateConfigHash(reviewConfig),
349
- currentPolicyConfigHash: async () => calculateConfigHash((await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config),
350
- config: reviewConfig,
351
- evidenceStore: new FileEvidenceStore(root, root),
352
- execute: createReviewExecutor({
353
- targets: reviewRole?.targets ?? [],
354
- cwd: root,
355
- ...(reviewRole?.model === undefined
356
- ? {}
357
- : { model: reviewRole.model }),
358
- ...(reviewRole?.effort === undefined
386
+ const selectedReviewTarget = args.harness?.[0];
387
+ const plannedReviewTargets = orderChain(selectedReviewTarget === undefined
388
+ ? reviewRole?.targets ?? []
389
+ : [selectedReviewTarget], detectHostTarget(process.env));
390
+ const controller = new AbortController();
391
+ let interruptedBy;
392
+ const interrupt = (signal) => {
393
+ interruptedBy ??= signal;
394
+ controller.abort(signal);
395
+ };
396
+ const onSigint = () => interrupt("SIGINT");
397
+ const onSigterm = () => interrupt("SIGTERM");
398
+ process.once("SIGINT", onSigint);
399
+ process.once("SIGTERM", onSigterm);
400
+ try {
401
+ return await runReviewCommand({
402
+ args,
403
+ authorized: args.yes,
404
+ tasks: taskService,
405
+ ...(args.taskId === undefined ? {} : { taskId: args.taskId }),
406
+ ...(reviewSessionId === undefined
359
407
  ? {}
360
- : { effort: reviewRole.effort }),
361
- ...(reviewRole?.timeoutMs === undefined
408
+ : { sessionId: reviewSessionId }),
409
+ ...(reviewConfig.reviewRoles === undefined
362
410
  ? {}
363
- : { timeoutMs: reviewRole.timeoutMs }),
364
- onProgress: (line) => {
365
- if (!args.json) {
411
+ : { roles: reviewConfig.reviewRoles }),
412
+ targets: plannedReviewTargets,
413
+ root,
414
+ gitRunner: gitRunner(root),
415
+ policyConfigHash: calculateConfigHash(reviewConfig),
416
+ currentPolicyConfigHash: async () => calculateConfigHash((await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config),
417
+ config: reviewConfig,
418
+ evidenceStore: new FileEvidenceStore(root, root),
419
+ execute: createReviewExecutor({
420
+ targets: plannedReviewTargets,
421
+ cwd: root,
422
+ ...(reviewRole?.model === undefined
423
+ ? {}
424
+ : { model: reviewRole.model }),
425
+ ...(reviewRole?.effort === undefined
426
+ ? {}
427
+ : { effort: reviewRole.effort }),
428
+ ...(reviewRole?.timeoutMs === undefined
429
+ ? {}
430
+ : { timeoutMs: reviewRole.timeoutMs }),
431
+ signal: controller.signal,
432
+ onProgress: (line) => {
366
433
  process.stderr.write(`${line}\n`);
367
434
  }
368
- }
369
- })
370
- });
435
+ })
436
+ });
437
+ }
438
+ catch (error) {
439
+ if (error instanceof ReviewInterruptedError &&
440
+ interruptedBy !== undefined) {
441
+ process.exit(interruptedBy === "SIGINT" ? 130 : 143);
442
+ }
443
+ throw error;
444
+ }
445
+ finally {
446
+ process.removeListener("SIGINT", onSigint);
447
+ process.removeListener("SIGTERM", onSigterm);
448
+ }
371
449
  }
372
450
  if (args.command === "config") {
373
451
  return explainConfigCommand(await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project"));
@@ -27,14 +27,17 @@ Commands:
27
27
  Manage independent task acceptance state
28
28
  verify Run configured verification
29
29
  review Run an independent review
30
+ allow-stop Grant one fingerprint-bound agy Stop permit (requires --session)
31
+ agy-run Run headless agy with a process-exit completion recheck
30
32
 
31
33
  Options:
32
34
  --scope <project|user>
33
- --harness <all|both|agy|claude|codex|opencode|comma-separated> Init/update/uninstall
35
+ --harness <all|both|agy|claude|codex|opencode|comma-separated> Init/update/uninstall; review accepts one configured target
34
36
  --hook-target <harness=surface-id> Repeatable advanced init/update option
35
37
  --profile <core|advisory|guardrails|loop> Repeatable
36
38
  --review-target <codex|agy|claude> Repeatable init option; external review
37
39
  targets in fallback-chain order
40
+ --completion-gate Init only: enable the agy project-loop gate
38
41
  --check-auth Doctor only: probe each review target's
39
42
  authentication with one real call
40
43
  --task <id>
@@ -0,0 +1,11 @@
1
+ import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
2
+ import { okEnvelope } from "../output.js";
3
+ export async function runAllowStopCommand(options) {
4
+ if (options.args.command !== "allow-stop" || options.args.sessionId === undefined) {
5
+ throw new AgentOpsError("COMPLETION_GATE_SESSION_REQUIRED", "allow-stop requires --session.");
6
+ }
7
+ await options.gate.grantPermit(options.args.sessionId);
8
+ return okEnvelope("COMPLETION_GATE_PERMIT_GRANTED", {
9
+ message: "One Stop is permitted for this conversation and current source fingerprint."
10
+ });
11
+ }
@@ -49,11 +49,22 @@ export async function runHookCommand(options) {
49
49
  capabilities,
50
50
  trusted: options.trusted,
51
51
  advisory: options.advisory ?? runLifecycleAdvisory,
52
- ...(stopVerification === undefined ? {} : { stopVerification })
52
+ ...(stopVerification === undefined ? {} : { stopVerification }),
53
+ ...(options.completionGate === undefined
54
+ ? {}
55
+ : { completionGate: options.completionGate })
53
56
  });
54
57
  return descriptor.runtime.formatOutput(options.event, result);
55
58
  }
56
59
  catch {
60
+ if (options.harness === "agy" && options.completionGate !== undefined) {
61
+ return harnessDescriptor("agy").runtime.formatOutput(options.event, {
62
+ action: "block",
63
+ status: "UNKNOWN",
64
+ code: "COMPLETION_GATE_UNAVAILABLE",
65
+ remedy: "Run agent-ops doctor; a user-approved one-time permit may be used after diagnosing the failure."
66
+ });
67
+ }
57
68
  return { exitCode: 0, stdout: "", stderr: "" };
58
69
  }
59
70
  }
@@ -102,7 +102,10 @@ export async function runInitCommand(options) {
102
102
  : { hookTargets: options.hookTargets ?? args.hookTargets }),
103
103
  ...(args.reviewTargets === undefined
104
104
  ? {}
105
- : { reviewTargets: args.reviewTargets })
105
+ : { reviewTargets: args.reviewTargets }),
106
+ ...(args.completionGate === undefined
107
+ ? {}
108
+ : { completionGateEnabled: args.completionGate })
106
109
  });
107
110
  const trust = await trustChange(options, plan);
108
111
  const warnings = plan.harness.includes("agy") && options.agyWarning !== undefined
@@ -213,6 +213,9 @@ function sourceChangedResult(result) {
213
213
  model: result.model,
214
214
  effort: result.effort,
215
215
  prompt: result.prompt,
216
+ ...(result.plannedTargets === undefined
217
+ ? {}
218
+ : { plannedTargets: result.plannedTargets }),
216
219
  ...(result.scope === undefined ? {} : { scope: result.scope }),
217
220
  ...(result.independence === undefined
218
221
  ? {}
@@ -229,7 +232,22 @@ function sourceChangedResult(result) {
229
232
  export async function runReviewCommand(options) {
230
233
  const role = resolveReviewRole(options.role ?? "independent-review", options.roles ?? []);
231
234
  const selectedHarness = harness(options.args.harness);
232
- const target = role?.targets[0] ?? selectedHarness ?? "codex";
235
+ const configuredTargets = role?.targets ?? [];
236
+ if (selectedHarness !== undefined &&
237
+ !configuredTargets.includes(selectedHarness)) {
238
+ throw new AgentOpsError("REVIEW_TARGET_NOT_CONFIGURED", `${selectedHarness} is not enabled for independent-review. Configure it with init --review-target ${selectedHarness}.`);
239
+ }
240
+ const plannedTargets = selectedHarness === undefined
241
+ ? options.targets ?? configuredTargets
242
+ : [selectedHarness];
243
+ const target = plannedTargets[0] ?? selectedHarness ?? "codex";
244
+ const resultBase = {
245
+ harness: target,
246
+ plannedTargets,
247
+ model: role?.model ?? options.model ?? "configured",
248
+ effort: role?.effort ?? options.effort ?? "configured",
249
+ prompt: ""
250
+ };
233
251
  const context = await taskContext(options);
234
252
  const evidenceRequirements = (options.args.evidence ?? []).map((value) => {
235
253
  const separator = value.indexOf("=");
@@ -253,24 +271,18 @@ export async function runReviewCommand(options) {
253
271
  const reason = scopeReason(error);
254
272
  if (reason !== undefined) {
255
273
  return notRunEnvelope({
274
+ ...resultBase,
256
275
  status: "NOT_RUN",
257
- reason,
258
- harness: target,
259
- model: role?.model ?? options.model ?? "configured",
260
- effort: role?.effort ?? options.effort ?? "configured",
261
- prompt: ""
276
+ reason
262
277
  });
263
278
  }
264
279
  throw error;
265
280
  }
266
281
  if (scope.changedFiles.some(isReviewerPolicyPath)) {
267
282
  return notRunEnvelope({
283
+ ...resultBase,
268
284
  status: "NOT_RUN",
269
285
  reason: "reviewer-policy-changed",
270
- harness: target,
271
- model: role?.model ?? options.model ?? "configured",
272
- effort: role?.effort ?? options.effort ?? "configured",
273
- prompt: "",
274
286
  scope
275
287
  });
276
288
  }
@@ -278,16 +290,16 @@ export async function runReviewCommand(options) {
278
290
  if (context !== undefined && options.policyConfigHash !== undefined) {
279
291
  if (context.policyConfigHash === null) {
280
292
  return notRunEnvelope({
293
+ ...resultBase,
281
294
  status: "NOT_RUN", reason: "reviewer-policy-baseline-missing",
282
- harness: target, model: role?.model ?? options.model ?? "configured",
283
- effort: role?.effort ?? options.effort ?? "configured", prompt: "", scope
295
+ scope
284
296
  });
285
297
  }
286
298
  if (context.policyConfigHash !== options.policyConfigHash) {
287
299
  return notRunEnvelope({
300
+ ...resultBase,
288
301
  status: "NOT_RUN", reason: "reviewer-policy-changed",
289
- harness: target, model: role?.model ?? options.model ?? "configured",
290
- effort: role?.effort ?? options.effort ?? "configured", prompt: "", scope
302
+ scope
291
303
  });
292
304
  }
293
305
  }
@@ -295,9 +307,9 @@ export async function runReviewCommand(options) {
295
307
  const preflight = await preflightReview(options, context, sourceFingerprint);
296
308
  if (!preflight.ok) {
297
309
  return notRunEnvelope({
310
+ ...resultBase,
298
311
  status: "NOT_RUN", reason: preflight.reason,
299
- harness: target, model: role?.model ?? options.model ?? "configured",
300
- effort: role?.effort ?? options.effort ?? "configured", prompt: "", scope
312
+ scope
301
313
  });
302
314
  }
303
315
  verification = preflight.summary;
@@ -324,12 +336,9 @@ export async function runReviewCommand(options) {
324
336
  : undefined;
325
337
  if (reason !== undefined) {
326
338
  return notRunEnvelope({
339
+ ...resultBase,
327
340
  status: "NOT_RUN",
328
- reason,
329
- harness: target,
330
- model: role?.model ?? options.model ?? "configured",
331
- effort: role?.effort ?? options.effort ?? "configured",
332
- prompt: ""
341
+ reason
333
342
  });
334
343
  }
335
344
  }
@@ -338,6 +347,7 @@ export async function runReviewCommand(options) {
338
347
  const result = await runIndependentReview({
339
348
  invocation: {
340
349
  harness: target,
350
+ plannedTargets,
341
351
  model: role?.model ?? options.model ?? "configured",
342
352
  effort: role?.effort ?? options.effort ?? "configured",
343
353
  packet,
@@ -95,9 +95,12 @@ export async function runTaskCommand(options) {
95
95
  : { policyConfigHash: options.policyConfigHash }),
96
96
  ...(options.args.parentTaskId === undefined
97
97
  ? {}
98
- : { parentTaskId: options.args.parentTaskId })
98
+ : { parentTaskId: options.args.parentTaskId }),
99
+ ...(sessionId === undefined ? {} : { sessionId })
99
100
  });
100
- return taskEnvelope(action, "TASK_CREATED", `Created task ${record.task.id}.`, record);
101
+ return taskEnvelope(action, "TASK_CREATED", sessionId === undefined
102
+ ? `Created task ${record.task.id}.`
103
+ : `Created and attached task ${record.task.id}.`, record);
101
104
  }
102
105
  if (action === "status") {
103
106
  if (options.args.taskId === undefined && sessionId === undefined) {
@@ -9,12 +9,15 @@ import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
9
9
  import { localStatePaths } from "../../../runtime/src/security/permissions.js";
10
10
  import { calculateTrustBinding, FileTrustStore } from "../../../runtime/src/security/trust.js";
11
11
  export const DEFAULT_CONFIG = {
12
- schemaVersion: 2,
12
+ schemaVersion: 3,
13
13
  profiles: [],
14
14
  verification: { commands: [] },
15
15
  features: {
16
16
  stopVerification: {
17
17
  enabled: false
18
+ },
19
+ completionGate: {
20
+ enabled: false
18
21
  }
19
22
  },
20
23
  pathMappings: [],