@kylecheng3146/agent-ops 0.1.19 → 0.1.21

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 (53) hide show
  1. package/README.md +38 -12
  2. package/dist/packages/cli/src/agy-headless.js +18 -0
  3. package/dist/packages/cli/src/args.js +29 -3
  4. package/dist/packages/cli/src/bin.js +113 -12
  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 +43 -10
  9. package/dist/packages/cli/src/commands/review.js +3 -0
  10. package/dist/packages/cli/src/commands/task.js +5 -2
  11. package/dist/packages/cli/src/commands/uninstall.js +20 -3
  12. package/dist/packages/cli/src/context.js +4 -1
  13. package/dist/packages/cli/src/hook-process.js +59 -5
  14. package/dist/packages/cli/src/ui.js +3 -1
  15. package/dist/packages/cli/src/wizard.js +36 -6
  16. package/dist/runtime/src/adapters/agy/config.js +115 -0
  17. package/dist/runtime/src/adapters/agy/events.js +35 -0
  18. package/dist/runtime/src/adapters/agy/input.js +48 -0
  19. package/dist/runtime/src/adapters/agy/output.js +28 -0
  20. package/dist/runtime/src/adapters/agy/surfaces.js +9 -0
  21. package/dist/runtime/src/config/explain.js +5 -0
  22. package/dist/runtime/src/config/migrate.js +15 -1
  23. package/dist/runtime/src/contracts.js +1 -1
  24. package/dist/runtime/src/hooks/completion-gate.js +209 -0
  25. package/dist/runtime/src/hooks/dispatch.js +6 -0
  26. package/dist/runtime/src/install/codex-loop.js +6 -2
  27. package/dist/runtime/src/install/doctor.js +17 -0
  28. package/dist/runtime/src/install/harness.js +56 -1
  29. package/dist/runtime/src/install/hooks.js +6 -1
  30. package/dist/runtime/src/install/ownership.js +15 -4
  31. package/dist/runtime/src/install/plan.js +21 -6
  32. package/dist/runtime/src/install/probes.js +61 -0
  33. package/dist/runtime/src/install/profiles.js +3 -0
  34. package/dist/runtime/src/install/surface-inspection.js +10 -1
  35. package/dist/runtime/src/install/uninstall.js +337 -6
  36. package/dist/runtime/src/review/execute.js +23 -20
  37. package/dist/runtime/src/review/extract.js +5 -11
  38. package/dist/runtime/src/review/render.js +6 -1
  39. package/dist/runtime/src/review/roles.js +4 -0
  40. package/dist/runtime/src/review/runner.js +7 -0
  41. package/dist/runtime/src/schema/validate.js +13 -3
  42. package/dist/runtime/src/task/service.js +17 -0
  43. package/docs/en/guides/configuration.md +46 -12
  44. package/docs/en/spec/README.md +7 -4
  45. package/docs/en/spec/harness-adapters.md +24 -9
  46. package/docs/en/spec/review.md +6 -0
  47. package/docs/zh-TW/guides/configuration.md +41 -11
  48. package/docs/zh-TW/spec/README.md +6 -4
  49. package/docs/zh-TW/spec/harness-adapters.md +21 -10
  50. package/docs/zh-TW/spec/review.md +5 -0
  51. package/package.json +2 -2
  52. package/schemas/config.schema.json +12 -2
  53. package/schemas/manifest.schema.json +2 -2
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Loop Engineering Toolkit
2
2
 
3
3
  Loop Engineering Toolkit is an evidence-driven development-loop toolkit for
4
- Codex, Claude Code, and opencode. It is designed to turn acceptance criteria, explicit
4
+ agy, Codex, Claude Code, and opencode. It is designed to turn acceptance criteria, explicit
5
5
  verification, safe lifecycle hooks, and independent review into a repeatable
6
6
  engineering workflow.
7
7
 
@@ -34,9 +34,9 @@ automation. The wizard never writes files until you review and confirm its
34
34
  installation plan.
35
35
 
36
36
  The interactive multi-select screens start with no harness or profile selected.
37
- Choose at least one of `codex`, `claude`, and `opencode`, and at least one of
37
+ Choose at least one of `agy`, `codex`, `claude`, and `opencode`, and at least one of
38
38
  the `core`, `advisory`, `guardrails`, and `loop` profiles before confirming. For
39
- scripted use, `--harness all` selects all three harnesses; comma-separated
39
+ scripted use, `--harness all` selects all four harnesses; comma-separated
40
40
  selections such as `codex,opencode` are also supported. The legacy `both` value
41
41
  remains an alias for `codex,claude`.
42
42
 
@@ -66,7 +66,11 @@ agent-ops uninstall --dry-run --json
66
66
 
67
67
  ### Project-local loop
68
68
 
69
- `loop` is an explicit, project-only profile for Codex, Claude Code, or both.
69
+ `loop` is an explicit, project-only profile for agy, Codex, and Claude Code.
70
+ agy installs its native `PreInvocation`, `PreToolUse(run_command)`, and optional
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
73
+ degraded because prompt, permission, compact, and subagent events are unavailable.
70
74
  Claude Code supports native Windows through a generated PowerShell launcher;
71
75
  Codex's loop launcher still requires POSIX-compatible `bash`. Preview it
72
76
  first, then install only after reviewing the plan. `loop` also implies the
@@ -77,6 +81,24 @@ agent-ops init --dry-run --scope project --harness codex,claude --profile loop -
77
81
  agent-ops init --scope project --harness codex,claude --profile loop --yes
78
82
  ```
79
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
+
80
102
  On native Windows, select `--harness claude` unless Codex is running in a
81
103
  POSIX environment; the Codex loop still invokes `bash`.
82
104
 
@@ -247,10 +269,10 @@ After changing Stop configuration, run `agent-ops update`; its approved plan
247
269
  updates native registrations and the exact trust binding together. Stop is
248
270
  report-only: `PASS`, `FAIL`, and `UNKNOWN` continue
249
271
  the harness, emit bounded command evidence, and never complete a task.
250
- Config v1 migrates to config v2 with Stop disabled; migration invalidates the
251
- 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.
252
274
 
253
- 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
254
276
  load the managed baseline while leaving project-specific instructions in those
255
277
  files authoritative. Existing installations with the previous canonical
256
278
  wording are migrated by `agent-ops update`; changed managed blocks still fail
@@ -286,13 +308,17 @@ to a deterministic worktree (or `--base`) scope. `--task` reviews keep their
286
308
  task criteria and require current verification evidence. The detailed report
287
309
  is displayed; a minimal source-fingerprint attestation is persisted after PASS.
288
310
 
289
- Each attempt uses a fresh temporary cwd, a small allowlisted environment, and a
290
- target-native read-only mode. Claude additionally uses its complete safe mode;
291
- Codex ignores user config and persistence, while Agy runs in sandboxed plan
292
- mode against a disposable repository clone. Codex and Agy preserve their
311
+ Each attempt uses a fresh temporary session and disposable repository clone, a
312
+ small allowlisted environment, and a target-native read-only mode. Claude
313
+ additionally uses its complete safe mode; Codex ignores user config and
314
+ persistence, while Agy runs in sandboxed plan mode. Codex and Agy preserve their
293
315
  existing login environment, so their context isolation is intentionally weaker
294
316
  than Claude's. `opencode` is not a review target.
295
317
 
318
+ The chain prefers a reviewer CLI different from the hosting CLI. If none is
319
+ usable, a fresh same-target session is allowed but is rendered as
320
+ `DEGRADED: isolated self-review`; a development session is never resumed.
321
+
296
322
  The first valid `PASS` or `FAIL` is final. Attempts that produce no verdict —
297
323
  including login failures and unparseable output — advance to the next target
298
324
  and remain visible in the result's `attempts`. `--yes` is still required for
@@ -313,7 +339,7 @@ See [Configuration](docs/en/guides/configuration.md) for the full contract.
313
339
  - Treat command output and current filesystem state as evidence.
314
340
  - Keep advisory automation separate from blocking guardrails.
315
341
  - Preserve user configuration through managed, reversible updates.
316
- - Support Codex, Claude Code, and opencode without project-specific assumptions.
342
+ - Support agy, Codex, Claude Code, and opencode without project-specific assumptions.
317
343
  - Collect no network telemetry.
318
344
 
319
345
  ## Project status
@@ -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
  }
@@ -340,6 +350,9 @@ export function parseArgs(argv) {
340
350
  if (reviewTargets.length > 0 && command !== "init") {
341
351
  throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--review-target may be used only with init.");
342
352
  }
353
+ if (completionGate !== undefined && command !== "init") {
354
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--completion-gate may be used only with init.");
355
+ }
343
356
  if (command === "task") {
344
357
  if (harness !== undefined ||
345
358
  profiles.length > 0 ||
@@ -349,8 +362,7 @@ export function parseArgs(argv) {
349
362
  }
350
363
  const taskOptionInvalid = (action === "create" &&
351
364
  (taskId !== undefined ||
352
- evidence.length > 0 ||
353
- sessionId !== undefined)) ||
365
+ evidence.length > 0)) ||
354
366
  (action === "status" &&
355
367
  (title !== undefined ||
356
368
  criteria.length > 0 ||
@@ -403,6 +415,19 @@ export function parseArgs(argv) {
403
415
  if (command === "review" && harness !== undefined && harness.length !== 1) {
404
416
  invalidValue("--harness", harness.join(","));
405
417
  }
418
+ if (command === "allow-stop" &&
419
+ (sessionId === undefined ||
420
+ scope !== undefined ||
421
+ harness !== undefined ||
422
+ taskId !== undefined ||
423
+ profiles.length > 0 ||
424
+ title !== undefined ||
425
+ criteria.length > 0 ||
426
+ evidence.length > 0 ||
427
+ dryRun ||
428
+ yes)) {
429
+ throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "allow-stop requires only --session (and optional --json).");
430
+ }
406
431
  return {
407
432
  command,
408
433
  ...(action === undefined ? {} : { action }),
@@ -415,6 +440,7 @@ export function parseArgs(argv) {
415
440
  ...(targetVersion === undefined ? {} : { targetVersion }),
416
441
  ...(title === undefined ? {} : { title }),
417
442
  ...(reviewTargets.length === 0 ? {} : { reviewTargets }),
443
+ ...(completionGate === undefined ? {} : { completionGate }),
418
444
  ...(criteria.length === 0 ? {} : { criteria }),
419
445
  ...(evidence.length === 0 ? {} : { evidence }),
420
446
  ...(sessionId === undefined ? {} : { sessionId }),
@@ -1,11 +1,12 @@
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
- import { hookRegistrationDrift, repositoryTrustStatus, smokeAvailabilityStatus } from "../../../runtime/src/install/probes.js";
9
+ import { agyRuntimeStatus, agyVersionSupported, hookRegistrationDrift, repositoryTrustStatus, smokeAvailabilityStatus } from "../../../runtime/src/install/probes.js";
9
10
  import { parseInstallManifest } from "../../../runtime/src/fs/manifest.js";
10
11
  import { NpmRegistryClient } from "../../../runtime/src/registry/npm.js";
11
12
  import { TaskService } from "../../../runtime/src/task/service.js";
@@ -33,8 +34,11 @@ import { probeReviewTarget } from "../../../runtime/src/review/probe.js";
33
34
  import { 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 {
@@ -92,13 +96,13 @@ function gitRunner(root) {
92
96
  }
93
97
  };
94
98
  }
95
- async function confirmInit(plan, trust) {
99
+ async function confirmInit(plan, trust, warnings = []) {
96
100
  writeBanner({
97
101
  isTTY: process.stdout.isTTY === true,
98
102
  columns: process.stdout.columns,
99
103
  write: (value) => process.stdout.write(value)
100
104
  });
101
- return await confirmPlan(formatInstallPlan(plan, trust));
105
+ return await confirmPlan(formatInstallPlan(plan, trust, warnings));
102
106
  }
103
107
  async function confirmPlan(text) {
104
108
  process.stdout.write(text);
@@ -115,7 +119,47 @@ async function plannedTrustBinding(root, projectConfig) {
115
119
  : await repositoryTrustBinding(root, config, CLI_VERSION);
116
120
  }
117
121
  const argv = process.argv.slice(2);
118
- if (argv[0] === "hook") {
122
+ function runAgy(args, options = {}) {
123
+ return execFileSync("agy", [...args], {
124
+ ...options,
125
+ encoding: "utf8",
126
+ ...(process.platform === "win32" ? { shell: true } : {})
127
+ });
128
+ }
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") {
119
163
  process.exitCode = await runHookProcess(argv.slice(1), {
120
164
  stdin: process.stdin,
121
165
  writeStdout: (value) => process.stdout.write(value),
@@ -140,7 +184,8 @@ else {
140
184
  "uninstall",
141
185
  "task",
142
186
  "verify",
143
- "review"
187
+ "review",
188
+ "allow-stop"
144
189
  ].map((command) => [command, async (args) => {
145
190
  const root = args.scope === "user"
146
191
  ? process.env.AGENT_OPS_HOME ?? homedir()
@@ -157,20 +202,53 @@ else {
157
202
  isTTY,
158
203
  toolkitVersion: CLI_VERSION,
159
204
  hookRuntimePath: HOOK_RUNTIME_PATH,
205
+ agyWarning: () => {
206
+ try {
207
+ const output = runAgy(["--version"], { timeout: 5_000 });
208
+ return agyVersionSupported(output)
209
+ ? undefined
210
+ : "agy is installed, but version 1.1.12 or newer is required; run `agent-ops doctor` after updating.";
211
+ }
212
+ catch {
213
+ return "agy is not installed or could not be started; install agy 1.1.12 or newer, then run `agent-ops doctor`.";
214
+ }
215
+ },
160
216
  ...(args.hookTargets === undefined
161
217
  ? {}
162
218
  : { hookTargets: args.hookTargets }),
163
219
  trustStore: store,
164
220
  calculateTrustBinding: async (config) => await plannedTrustBinding(root, config),
165
- confirm: async (plan, trust) => await confirmInit(plan, trust)
221
+ confirm: async (plan, trust, warnings) => await confirmInit(plan, trust, warnings)
166
222
  });
167
223
  }
168
224
  if (args.command === "doctor") {
225
+ const doctorManifest = await installedManifest(root);
169
226
  const config = (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config;
170
227
  return await runDoctorCommand({
171
228
  root,
172
229
  toolkitVersion: CLI_VERSION,
173
230
  probes: {
231
+ ...(doctorManifest?.harness.includes("agy") === true
232
+ ? {
233
+ agyRuntime: () => {
234
+ try {
235
+ return agyRuntimeStatus(runAgy(["--version"]), runAgy([
236
+ "-p", "/hooks", "--output-format", "json",
237
+ ...(doctorManifest.scope === "project"
238
+ ? ["--new-project"]
239
+ : [])
240
+ ], { cwd: root }), doctorManifest.hooks?.find(({ harness }) => harness === "agy")?.events ?? []);
241
+ }
242
+ catch {
243
+ return {
244
+ status: "FAIL",
245
+ message: "agy is missing or could not report its loaded hooks.",
246
+ remediation: "Install agy 1.1.12 or newer and run `agent-ops doctor` again."
247
+ };
248
+ }
249
+ }
250
+ }
251
+ : {}),
174
252
  hookRegistration: async () => {
175
253
  const drifted = hookRegistrationDrift({
176
254
  harness: await installedHarness(root),
@@ -246,26 +324,49 @@ else {
246
324
  }
247
325
  if (args.command === "update") {
248
326
  const store = trustStore();
327
+ const manifest = await installedManifest(root);
328
+ const addAgy = isTTY &&
329
+ !args.yes &&
330
+ args.harness === undefined &&
331
+ manifest !== null &&
332
+ !manifest.harness.includes("agy") &&
333
+ await selectYesNo("Add newly supported harness: agy?", { input: process.stdin, output: process.stdout }, false);
334
+ const updateArgs = addAgy
335
+ ? { ...args, harness: [...(manifest?.harness ?? []), "agy"] }
336
+ : args;
249
337
  return await runUpdateCommand({
250
- args,
338
+ args: updateArgs,
251
339
  root,
252
340
  adapters: commonHarnessAdapters(),
253
341
  registry: new NpmRegistryClient(),
254
342
  isTTY,
255
343
  toolkitVersion: CLI_VERSION,
256
344
  hookRuntimePath: HOOK_RUNTIME_PATH,
257
- ...(args.hookTargets === undefined
345
+ ...(updateArgs.hookTargets === undefined
258
346
  ? {}
259
- : { hookTargets: args.hookTargets }),
347
+ : { hookTargets: updateArgs.hookTargets }),
260
348
  trustStore: store,
261
349
  calculateTrustBinding: async (config) => await plannedTrustBinding(root, config),
262
350
  confirm: async (plan, trust) => await confirmPlan(formatUpdatePlan(plan, trust)),
263
- ...(args.targetVersion === undefined
351
+ ...(updateArgs.targetVersion === undefined
264
352
  ? {}
265
- : { targetVersion: args.targetVersion })
353
+ : { targetVersion: updateArgs.targetVersion })
266
354
  });
267
355
  }
268
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
+ }
269
370
  if (args.command === "task") {
270
371
  const sessionId = process.env.AGENT_OPS_SESSION_ID;
271
372
  const policyConfigHash = args.action === "create"
@@ -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|claude|codex|opencode|comma-separated> Init/update
35
+ --harness <all|both|agy|claude|codex|opencode|comma-separated> Init/update/uninstall
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
  }
@@ -5,7 +5,7 @@ import { okEnvelope } from "../output.js";
5
5
  import { formatOperationPlan } from "../plan-output.js";
6
6
  import { toPublicInstallPlan } from "../public-plan.js";
7
7
  import { formatTrustChange, planTrustGrant } from "./trust.js";
8
- export function formatInstallPlan(plan, trust) {
8
+ export function formatInstallPlan(plan, trust, warnings = []) {
9
9
  const hooks = plan.manifest.hooks ?? [];
10
10
  const detectedVerification = plan.detectedVerification;
11
11
  return formatOperationPlan({
@@ -14,6 +14,12 @@ export function formatInstallPlan(plan, trust) {
14
14
  `Scope: ${plan.scope}`,
15
15
  `Harness: ${plan.harness}`,
16
16
  `Profiles: ${plan.profiles.join(", ")}`,
17
+ ...(plan.scope === "user" && plan.harness.includes("agy")
18
+ ? ["Notice: .gemini/GEMINI.md is a shared Gemini rule surface."]
19
+ : []),
20
+ ...(warnings.length === 0
21
+ ? []
22
+ : ["Warnings:", ...warnings.map((warning) => ` - ${warning}`)]),
17
23
  ...(trust === undefined ? [] : formatTrustChange(trust)),
18
24
  ...(hooks.length === 0
19
25
  ? ["Hooks: none selected"]
@@ -42,11 +48,19 @@ function appliedMessage(plan) {
42
48
  ...hooks.map((hook) => `- ${hook.harness}: ${hook.path} (${hook.events.join(", ")})`)
43
49
  ].join("\n");
44
50
  }
45
- function initError(code, message, plan, trust, applied = false) {
51
+ function initError(code, message, plan, trust, warnings = [], applied = false) {
52
+ const displayMessage = warnings.length === 0
53
+ ? message
54
+ : `${message}\nWarnings:\n${warnings.map((warning) => `- ${warning}`).join("\n")}`;
46
55
  return {
47
56
  code,
48
57
  status: "error",
49
- data: { applied, plan: toPublicInstallPlan(plan, trust), message },
58
+ data: {
59
+ applied,
60
+ plan: toPublicInstallPlan(plan, trust),
61
+ message: displayMessage,
62
+ ...(warnings.length === 0 ? {} : { warnings })
63
+ },
50
64
  errors: [{ code, message }]
51
65
  };
52
66
  }
@@ -88,24 +102,39 @@ export async function runInitCommand(options) {
88
102
  : { hookTargets: options.hookTargets ?? args.hookTargets }),
89
103
  ...(args.reviewTargets === undefined
90
104
  ? {}
91
- : { reviewTargets: args.reviewTargets })
105
+ : { reviewTargets: args.reviewTargets }),
106
+ ...(args.completionGate === undefined
107
+ ? {}
108
+ : { completionGateEnabled: args.completionGate })
92
109
  });
93
110
  const trust = await trustChange(options, plan);
111
+ const warnings = plan.harness.includes("agy") && options.agyWarning !== undefined
112
+ ? (() => {
113
+ try {
114
+ const warning = options.agyWarning();
115
+ return warning === undefined ? [] : [warning];
116
+ }
117
+ catch {
118
+ return ["agy could not be probed; run `agent-ops doctor` to verify it."];
119
+ }
120
+ })()
121
+ : [];
94
122
  if (args.dryRun) {
95
123
  return okEnvelope("INIT_PLAN_READY", {
96
124
  applied: false,
97
125
  plan: toPublicInstallPlan(plan, trust),
98
126
  message: "Installation plan calculated; no files were written.",
99
- text: formatInstallPlan(plan, trust)
127
+ ...(warnings.length === 0 ? {} : { warnings }),
128
+ text: formatInstallPlan(plan, trust, warnings)
100
129
  });
101
130
  }
102
131
  if (!args.yes && !options.isTTY) {
103
- return initError("INIT_CONFIRMATION_REQUIRED", "Non-interactive init requires --yes after all choices are explicit.", plan, trust);
132
+ return initError("INIT_CONFIRMATION_REQUIRED", "Non-interactive init requires --yes after all choices are explicit.", plan, trust, warnings);
104
133
  }
105
134
  if (!args.yes &&
106
135
  options.isTTY &&
107
- !(await options.confirm(plan, trust))) {
108
- return initError("INIT_CANCELLED", "Installation was cancelled; no files were written.", plan, trust);
136
+ !(await options.confirm(plan, trust, warnings))) {
137
+ return initError("INIT_CANCELLED", "Installation was cancelled; no files were written.", plan, trust, warnings);
109
138
  }
110
139
  await applyInstallPlan(options.root, plan);
111
140
  if (trust.action === "grant") {
@@ -116,12 +145,16 @@ export async function runInitCommand(options) {
116
145
  await options.trustStore.grant(trust.binding);
117
146
  }
118
147
  catch {
119
- return initError("INIT_TRUST_FAILED", "Installation was applied, but repository trust was not granted. Run `agent-ops trust grant --yes`.", plan, trust, true);
148
+ return initError("INIT_TRUST_FAILED", "Installation was applied, but repository trust was not granted. Run `agent-ops trust grant --yes`.", plan, trust, warnings, true);
120
149
  }
121
150
  }
151
+ const message = appliedMessage(plan);
122
152
  return okEnvelope("INIT_APPLIED", {
123
153
  applied: true,
124
154
  plan: toPublicInstallPlan(plan, trust),
125
- message: appliedMessage(plan)
155
+ message: warnings.length === 0
156
+ ? message
157
+ : `${message}\nWarnings:\n${warnings.map((warning) => `- ${warning}`).join("\n")}`,
158
+ ...(warnings.length === 0 ? {} : { warnings })
126
159
  });
127
160
  }
@@ -217,6 +217,9 @@ function sourceChangedResult(result) {
217
217
  ...(result.independence === undefined
218
218
  ? {}
219
219
  : { independence: result.independence }),
220
+ ...(result.sessionIsolation === undefined
221
+ ? {}
222
+ : { sessionIsolation: result.sessionIsolation }),
220
223
  ...(result.attempts === undefined ? {} : { attempts: result.attempts }),
221
224
  ...(result.verification === undefined
222
225
  ? {}
@@ -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) {