@kylecheng3146/agent-ops 0.1.20 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -4
- package/dist/packages/cli/src/agy-headless.js +18 -0
- package/dist/packages/cli/src/args.js +35 -5
- package/dist/packages/cli/src/bin.js +115 -37
- package/dist/packages/cli/src/cli.js +4 -1
- package/dist/packages/cli/src/commands/allow-stop.js +11 -0
- package/dist/packages/cli/src/commands/hook.js +12 -1
- package/dist/packages/cli/src/commands/init.js +4 -1
- package/dist/packages/cli/src/commands/review.js +31 -21
- package/dist/packages/cli/src/commands/task.js +5 -2
- package/dist/packages/cli/src/context.js +4 -1
- package/dist/packages/cli/src/hook-process.js +59 -5
- package/dist/packages/cli/src/wizard.js +24 -0
- package/dist/runtime/src/adapters/agy/config.js +17 -7
- package/dist/runtime/src/adapters/agy/events.js +8 -0
- package/dist/runtime/src/adapters/agy/input.js +21 -3
- package/dist/runtime/src/adapters/agy/output.js +8 -4
- package/dist/runtime/src/config/explain.js +5 -0
- package/dist/runtime/src/config/migrate.js +15 -1
- package/dist/runtime/src/contracts.js +1 -1
- package/dist/runtime/src/hooks/completion-gate.js +209 -0
- package/dist/runtime/src/hooks/dispatch.js +6 -0
- package/dist/runtime/src/install/doctor.js +3 -1
- package/dist/runtime/src/install/harness.js +5 -1
- package/dist/runtime/src/install/ownership.js +9 -2
- package/dist/runtime/src/install/plan.js +19 -4
- package/dist/runtime/src/install/profiles.js +3 -0
- package/dist/runtime/src/review/execute.js +40 -7
- package/dist/runtime/src/review/render.js +3 -1
- package/dist/runtime/src/review/runner.js +1 -0
- package/dist/runtime/src/schema/validate.js +11 -1
- package/dist/runtime/src/task/service.js +17 -0
- package/dist/runtime/src/verify/spawn.js +30 -8
- package/docs/en/guides/configuration.md +37 -7
- package/docs/en/spec/README.md +3 -3
- package/docs/en/spec/harness-adapters.md +7 -3
- package/docs/en/spec/review.md +11 -0
- package/docs/zh-TW/guides/configuration.md +32 -6
- package/docs/zh-TW/spec/README.md +4 -4
- package/docs/zh-TW/spec/harness-adapters.md +6 -3
- package/docs/zh-TW/spec/review.md +10 -0
- package/package.json +1 -1
- package/schemas/config.schema.json +12 -2
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
|
|
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
|
-
|
|
254
|
-
|
|
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 `
|
|
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",
|
|
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" &&
|
|
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] === "
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
...(
|
|
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
|
-
: {
|
|
361
|
-
...(
|
|
408
|
+
: { sessionId: reviewSessionId }),
|
|
409
|
+
...(reviewConfig.reviewRoles === undefined
|
|
362
410
|
? {}
|
|
363
|
-
: {
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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:
|
|
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: [],
|