@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.
- package/README.md +38 -12
- package/dist/packages/cli/src/agy-headless.js +18 -0
- package/dist/packages/cli/src/args.js +29 -3
- package/dist/packages/cli/src/bin.js +113 -12
- package/dist/packages/cli/src/cli.js +4 -1
- package/dist/packages/cli/src/commands/allow-stop.js +11 -0
- package/dist/packages/cli/src/commands/hook.js +12 -1
- package/dist/packages/cli/src/commands/init.js +43 -10
- package/dist/packages/cli/src/commands/review.js +3 -0
- package/dist/packages/cli/src/commands/task.js +5 -2
- package/dist/packages/cli/src/commands/uninstall.js +20 -3
- package/dist/packages/cli/src/context.js +4 -1
- package/dist/packages/cli/src/hook-process.js +59 -5
- package/dist/packages/cli/src/ui.js +3 -1
- package/dist/packages/cli/src/wizard.js +36 -6
- package/dist/runtime/src/adapters/agy/config.js +115 -0
- package/dist/runtime/src/adapters/agy/events.js +35 -0
- package/dist/runtime/src/adapters/agy/input.js +48 -0
- package/dist/runtime/src/adapters/agy/output.js +28 -0
- package/dist/runtime/src/adapters/agy/surfaces.js +9 -0
- package/dist/runtime/src/config/explain.js +5 -0
- package/dist/runtime/src/config/migrate.js +15 -1
- package/dist/runtime/src/contracts.js +1 -1
- package/dist/runtime/src/hooks/completion-gate.js +209 -0
- package/dist/runtime/src/hooks/dispatch.js +6 -0
- package/dist/runtime/src/install/codex-loop.js +6 -2
- package/dist/runtime/src/install/doctor.js +17 -0
- package/dist/runtime/src/install/harness.js +56 -1
- package/dist/runtime/src/install/hooks.js +6 -1
- package/dist/runtime/src/install/ownership.js +15 -4
- package/dist/runtime/src/install/plan.js +21 -6
- package/dist/runtime/src/install/probes.js +61 -0
- package/dist/runtime/src/install/profiles.js +3 -0
- package/dist/runtime/src/install/surface-inspection.js +10 -1
- package/dist/runtime/src/install/uninstall.js +337 -6
- package/dist/runtime/src/review/execute.js +23 -20
- package/dist/runtime/src/review/extract.js +5 -11
- package/dist/runtime/src/review/render.js +6 -1
- package/dist/runtime/src/review/roles.js +4 -0
- package/dist/runtime/src/review/runner.js +7 -0
- package/dist/runtime/src/schema/validate.js +13 -3
- package/dist/runtime/src/task/service.js +17 -0
- package/docs/en/guides/configuration.md +46 -12
- package/docs/en/spec/README.md +7 -4
- package/docs/en/spec/harness-adapters.md +24 -9
- package/docs/en/spec/review.md +6 -0
- package/docs/zh-TW/guides/configuration.md +41 -11
- package/docs/zh-TW/spec/README.md +6 -4
- package/docs/zh-TW/spec/harness-adapters.md +21 -10
- package/docs/zh-TW/spec/review.md +5 -0
- package/package.json +2 -2
- package/schemas/config.schema.json +12 -2
- 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
|
|
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
|
|
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
|
-
|
|
251
|
-
|
|
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 `
|
|
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
|
|
290
|
-
target-native read-only mode. Claude
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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
|
-
...(
|
|
345
|
+
...(updateArgs.hookTargets === undefined
|
|
258
346
|
? {}
|
|
259
|
-
: { 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
|
-
...(
|
|
351
|
+
...(updateArgs.targetVersion === undefined
|
|
264
352
|
? {}
|
|
265
|
-
: { 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: {
|
|
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
|
-
|
|
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:
|
|
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",
|
|
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) {
|