@kylecheng3146/agent-ops 0.3.7 → 0.4.1
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 +35 -0
- package/dist/packages/cli/src/args.js +93 -9
- package/dist/packages/cli/src/bin.js +151 -104
- package/dist/packages/cli/src/cli.js +54 -5
- package/dist/packages/cli/src/commands/batch.js +106 -0
- package/dist/packages/cli/src/commands/hook.js +4 -1
- package/dist/packages/cli/src/commands/init.js +1 -0
- package/dist/packages/cli/src/commands/parallel.js +16 -3
- package/dist/packages/cli/src/commands/review.js +22 -0
- package/dist/packages/cli/src/commands/uninstall.js +3 -2
- package/dist/packages/cli/src/commands/update.js +1 -0
- package/dist/packages/cli/src/context.js +28 -3
- package/dist/packages/cli/src/hook-process.js +14 -3
- package/dist/packages/cli/src/public-plan.js +32 -2
- package/dist/runtime/src/adapters/claude/config.js +1 -1
- package/dist/runtime/src/adapters/claude/input.js +17 -0
- package/dist/runtime/src/adapters/claude/output.js +9 -0
- package/dist/runtime/src/hooks/codex-loop.js +67 -7
- package/dist/runtime/src/hooks/completion-gate.js +27 -2
- package/dist/runtime/src/hooks/dispatch.js +5 -0
- package/dist/runtime/src/install/apply.js +2 -0
- package/dist/runtime/src/install/doctor.js +8 -1
- package/dist/runtime/src/install/harness.js +17 -6
- package/dist/runtime/src/install/plan.js +9 -0
- package/dist/runtime/src/install/preauth.js +276 -0
- package/dist/runtime/src/install/probes.js +32 -0
- package/dist/runtime/src/install/uninstall.js +29 -11
- package/dist/runtime/src/install/update.js +1 -0
- package/dist/runtime/src/parallel/enter.js +43 -0
- package/dist/runtime/src/parallel/finish.js +2 -1
- package/dist/runtime/src/parallel/guard.js +20 -1
- package/dist/runtime/src/parallel/manage.js +38 -0
- package/dist/runtime/src/parallel/service.js +3 -1
- package/dist/runtime/src/review/batch-guard.js +11 -0
- package/dist/runtime/src/review/batch-select.js +37 -0
- package/dist/runtime/src/review/batch.js +202 -0
- package/dist/runtime/src/review/execute.js +10 -7
- package/dist/runtime/src/review/host-sandbox.js +9 -0
- package/dist/runtime/src/review/invocation.js +15 -2
- package/dist/runtime/src/review/probe.js +51 -15
- package/dist/runtime/src/review/render.js +4 -0
- package/docs/en/guides/configuration.md +51 -1
- package/docs/en/spec/review.md +70 -0
- package/docs/zh-TW/guides/configuration.md +41 -0
- package/docs/zh-TW/spec/review.md +71 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -345,6 +345,41 @@ agent-ops doctor # presence only: no tokens, no network
|
|
|
345
345
|
agent-ops doctor --check-auth # one real print call per configured target
|
|
346
346
|
```
|
|
347
347
|
|
|
348
|
+
### Batch review
|
|
349
|
+
|
|
350
|
+
A change split into subtasks needs one verify and one review per subtask.
|
|
351
|
+
`agent-ops batch --parent <task-id> --yes` runs them together for the parent's
|
|
352
|
+
active subtasks and the parent itself, instead of one `review` at a time:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
agent-ops batch --parent <task-id> --yes --output batch.json
|
|
356
|
+
agent-ops batch --parent <task-id> --yes --base <ref> --parent-base <ref> --width 3
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
- Each task is verified and reviewed against its own base: `--base` for the
|
|
360
|
+
subtasks and `--parent-base` for the parent, otherwise the base recorded by
|
|
361
|
+
its last PASS review, otherwise the worktree base.
|
|
362
|
+
- A task that already has fresh PASS verification evidence skips verify. The
|
|
363
|
+
rest are verified one at a time, and each task's review starts as soon as its
|
|
364
|
+
own verify passes, so the next verify overlaps it. A task whose verify fails
|
|
365
|
+
gets no review; the others continue.
|
|
366
|
+
- At most `--width` reviews run at once (default 2). A transient `NOT_RUN`
|
|
367
|
+
(`probe-failed`, `stalled`, `timeout`, `quota-exhausted`,
|
|
368
|
+
`network-unreachable`) lowers the width to 1 for the rest and earns that task
|
|
369
|
+
one retry, alone, after the others finish. Other results are final.
|
|
370
|
+
- Each review target is probed once per batch while the probe succeeds; a
|
|
371
|
+
failed probe is forgotten so the retry probes again.
|
|
372
|
+
- Nothing may commit or edit the worktree while it runs. HEAD is part of every
|
|
373
|
+
fingerprint, so a change aborts the batch and reports the unfinished tasks
|
|
374
|
+
as `aborted`.
|
|
375
|
+
- Exit code 0 means every task passed, 1 means a review or verify failed, and 2
|
|
376
|
+
means the rest could not run and may be retried. The JSON envelope lists each
|
|
377
|
+
task; the full reports stay under `.agent-ops/reviews/`.
|
|
378
|
+
|
|
379
|
+
`batch` is started exactly like `review`: through the trusted outer host runner
|
|
380
|
+
as its first and only invocation, with `--output` instead of a shell redirect.
|
|
381
|
+
`agent-ops init` and `update` pre-authorize it beside `review`.
|
|
382
|
+
|
|
348
383
|
See [Configuration](docs/en/guides/configuration.md) for the full contract.
|
|
349
384
|
|
|
350
385
|
## Project principles
|
|
@@ -9,6 +9,7 @@ export const COMMAND_NAMES = [
|
|
|
9
9
|
"task",
|
|
10
10
|
"verify",
|
|
11
11
|
"review",
|
|
12
|
+
"batch",
|
|
12
13
|
"allow-stop",
|
|
13
14
|
"worktree"
|
|
14
15
|
];
|
|
@@ -17,7 +18,7 @@ const SCOPES = new Set(["project", "user"]);
|
|
|
17
18
|
const PROFILES = new Set(["advisory", "core", "guardrails", "loop"]);
|
|
18
19
|
// opencode is absent: it has no read-only flag, so it cannot review.
|
|
19
20
|
const REVIEW_TARGETS = new Set(["agy", "claude", "codex"]);
|
|
20
|
-
export const WORKTREE_ACTIONS = ["add", "finish", "list", "resume", "remove"];
|
|
21
|
+
export const WORKTREE_ACTIONS = ["add", "commit", "finish", "list", "resume", "remove"];
|
|
21
22
|
export class CliArgumentError extends Error {
|
|
22
23
|
code;
|
|
23
24
|
option;
|
|
@@ -71,6 +72,8 @@ export function parseArgs(argv) {
|
|
|
71
72
|
let title;
|
|
72
73
|
let sessionId;
|
|
73
74
|
let base;
|
|
75
|
+
let parentBase;
|
|
76
|
+
let width;
|
|
74
77
|
const profiles = [];
|
|
75
78
|
const reviewTargets = [];
|
|
76
79
|
const criteria = [];
|
|
@@ -87,6 +90,8 @@ export function parseArgs(argv) {
|
|
|
87
90
|
let worktreeName;
|
|
88
91
|
let force = false;
|
|
89
92
|
let worktreeFrom;
|
|
93
|
+
let worktreeMessage;
|
|
94
|
+
let output;
|
|
90
95
|
let worktreeTargetBranch;
|
|
91
96
|
let worktree;
|
|
92
97
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -208,6 +213,26 @@ export function parseArgs(argv) {
|
|
|
208
213
|
index += 1;
|
|
209
214
|
break;
|
|
210
215
|
}
|
|
216
|
+
case "--parent-base": {
|
|
217
|
+
if (parentBase !== undefined) {
|
|
218
|
+
duplicate(token);
|
|
219
|
+
}
|
|
220
|
+
parentBase = readOptionValue(argv, index, token);
|
|
221
|
+
index += 1;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case "--width": {
|
|
225
|
+
if (width !== undefined) {
|
|
226
|
+
duplicate(token);
|
|
227
|
+
}
|
|
228
|
+
const value = readOptionValue(argv, index, token);
|
|
229
|
+
if (!/^[1-9][0-9]{0,2}$/u.test(value)) {
|
|
230
|
+
invalidValue(token, value);
|
|
231
|
+
}
|
|
232
|
+
width = Number(value);
|
|
233
|
+
index += 1;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
211
236
|
case "--check-auth":
|
|
212
237
|
if (checkAuth) {
|
|
213
238
|
duplicate(token);
|
|
@@ -276,6 +301,23 @@ export function parseArgs(argv) {
|
|
|
276
301
|
index += 1;
|
|
277
302
|
break;
|
|
278
303
|
}
|
|
304
|
+
case "-m":
|
|
305
|
+
case "--message": {
|
|
306
|
+
if (worktreeMessage !== undefined) {
|
|
307
|
+
duplicate(token);
|
|
308
|
+
}
|
|
309
|
+
worktreeMessage = readOptionValue(argv, index, token);
|
|
310
|
+
index += 1;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
case "--output": {
|
|
314
|
+
if (output !== undefined) {
|
|
315
|
+
duplicate(token);
|
|
316
|
+
}
|
|
317
|
+
output = readOptionValue(argv, index, token);
|
|
318
|
+
index += 1;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
279
321
|
case "--target-branch": {
|
|
280
322
|
if (worktreeTargetBranch !== undefined) {
|
|
281
323
|
duplicate(token);
|
|
@@ -339,7 +381,11 @@ export function parseArgs(argv) {
|
|
|
339
381
|
action = token;
|
|
340
382
|
break;
|
|
341
383
|
}
|
|
342
|
-
if (command === "worktree" &&
|
|
384
|
+
if (command === "worktree" &&
|
|
385
|
+
action !== undefined &&
|
|
386
|
+
action !== "list" &&
|
|
387
|
+
action !== "commit" &&
|
|
388
|
+
worktreeName === undefined) {
|
|
343
389
|
worktreeName = token;
|
|
344
390
|
break;
|
|
345
391
|
}
|
|
@@ -393,6 +439,8 @@ export function parseArgs(argv) {
|
|
|
393
439
|
completionGate !== undefined ||
|
|
394
440
|
sessionId !== undefined ||
|
|
395
441
|
base !== undefined ||
|
|
442
|
+
parentBase !== undefined ||
|
|
443
|
+
width !== undefined ||
|
|
396
444
|
checkAuth ||
|
|
397
445
|
checkAuthTargets.length > 0 ||
|
|
398
446
|
dryRun ||
|
|
@@ -400,6 +448,8 @@ export function parseArgs(argv) {
|
|
|
400
448
|
rerun ||
|
|
401
449
|
worktree !== undefined ||
|
|
402
450
|
worktreeFrom !== undefined ||
|
|
451
|
+
worktreeMessage !== undefined ||
|
|
452
|
+
output !== undefined ||
|
|
403
453
|
worktreeTargetBranch !== undefined ||
|
|
404
454
|
force) {
|
|
405
455
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Only --json may be combined with global help or version.");
|
|
@@ -415,9 +465,15 @@ export function parseArgs(argv) {
|
|
|
415
465
|
if (command === "worktree" && action === undefined) {
|
|
416
466
|
throw new CliArgumentError("CLI_ACTION_REQUIRED", `The worktree command requires one of: ${WORKTREE_ACTIONS.join(", ")}.`);
|
|
417
467
|
}
|
|
418
|
-
if (command === "worktree" && action !== "list" && worktreeName === undefined) {
|
|
468
|
+
if (command === "worktree" && action !== "list" && action !== "commit" && worktreeName === undefined) {
|
|
419
469
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", `worktree ${action} requires a worktree name.`);
|
|
420
470
|
}
|
|
471
|
+
if (command === "worktree" && action === "commit" && (worktreeMessage === undefined || worktreeMessage.trim() === "")) {
|
|
472
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "worktree commit requires --message (-m).", "--message");
|
|
473
|
+
}
|
|
474
|
+
if (worktreeMessage !== undefined && !(command === "worktree" && action === "commit")) {
|
|
475
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--message may be used only with worktree commit.", "--message");
|
|
476
|
+
}
|
|
421
477
|
if (command === "task" && action === undefined) {
|
|
422
478
|
throw new CliArgumentError("CLI_ACTION_REQUIRED", "The task command requires one of: create, status, attach, complete, archive, export.");
|
|
423
479
|
}
|
|
@@ -434,15 +490,18 @@ export function parseArgs(argv) {
|
|
|
434
490
|
(hasTaskTargetOptions || hasTaskMutationOptions)) {
|
|
435
491
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Task target options may be used only with task or verify.");
|
|
436
492
|
}
|
|
437
|
-
if (command !== "task" && parentTaskId !== undefined) {
|
|
438
|
-
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--parent may be used only with task create
|
|
493
|
+
if (command !== "task" && command !== "batch" && parentTaskId !== undefined) {
|
|
494
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--parent may be used only with task create, task status or batch.");
|
|
439
495
|
}
|
|
440
496
|
if (command !== "update" && targetVersion !== undefined) {
|
|
441
497
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--target-version may be used only with update.");
|
|
442
498
|
}
|
|
443
499
|
if (base !== undefined && command !== "verify" && command !== "review" &&
|
|
444
|
-
!(command === "task" && action === "complete")) {
|
|
445
|
-
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--base may be used only with verify, review or task complete.");
|
|
500
|
+
command !== "batch" && !(command === "task" && action === "complete")) {
|
|
501
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--base may be used only with verify, review, batch or task complete.");
|
|
502
|
+
}
|
|
503
|
+
if ((parentBase !== undefined || width !== undefined) && command !== "batch") {
|
|
504
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", `${parentBase !== undefined ? "--parent-base" : "--width"} may be used only with batch.`);
|
|
446
505
|
}
|
|
447
506
|
if (hookTargets.length > 0 &&
|
|
448
507
|
command !== "init" &&
|
|
@@ -535,6 +594,16 @@ export function parseArgs(argv) {
|
|
|
535
594
|
if (command === "review" && (criteria.length > 0 || evidence.length > 0)) {
|
|
536
595
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Complete review covers every task criterion; omit --criterion and --evidence.");
|
|
537
596
|
}
|
|
597
|
+
if (command === "batch" && parentTaskId === undefined) {
|
|
598
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Batch requires --parent to name the task whose subtasks it covers.");
|
|
599
|
+
}
|
|
600
|
+
if (command === "batch" && !yes) {
|
|
601
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Batch requires --yes to authorize the reviewer sessions.");
|
|
602
|
+
}
|
|
603
|
+
if (command === "batch" &&
|
|
604
|
+
(harness !== undefined || profiles.length > 0 || dryRun || rerun)) {
|
|
605
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Batch accepts parent, base, parent-base, width, json, output and yes options.");
|
|
606
|
+
}
|
|
538
607
|
if (command === "allow-stop" &&
|
|
539
608
|
(sessionId === undefined ||
|
|
540
609
|
scope !== undefined ||
|
|
@@ -556,7 +625,13 @@ export function parseArgs(argv) {
|
|
|
556
625
|
dryRun ||
|
|
557
626
|
yes ||
|
|
558
627
|
rerun)) {
|
|
559
|
-
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "worktree accepts only a name, --session, --from, --target-branch and --json.");
|
|
628
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "worktree accepts only a name, --session, --from, --target-branch, --message and --json.");
|
|
629
|
+
}
|
|
630
|
+
if (output !== undefined && command !== "review" && command !== "batch" && command !== "doctor") {
|
|
631
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--output may be used only with review, batch or doctor.", "--output");
|
|
632
|
+
}
|
|
633
|
+
if (output !== undefined && (output.trim() === "" || output.includes("\0"))) {
|
|
634
|
+
throw new CliArgumentError("CLI_INVALID_VALUE", "Invalid value for --output.", "--output");
|
|
560
635
|
}
|
|
561
636
|
if (force && !(command === "worktree" && action === "remove")) {
|
|
562
637
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--force may be used only with worktree remove.");
|
|
@@ -572,7 +647,12 @@ export function parseArgs(argv) {
|
|
|
572
647
|
!/^[A-Za-z0-9._/-]{1,128}$/u.test(worktreeTargetBranch)) {
|
|
573
648
|
throw new CliArgumentError("CLI_INVALID_VALUE", `Invalid value for --target-branch: ${worktreeTargetBranch}`, "--target-branch");
|
|
574
649
|
}
|
|
575
|
-
|
|
650
|
+
// commit acts as a session (it refuses another's worktree), so it may be told which.
|
|
651
|
+
if (command === "worktree" &&
|
|
652
|
+
action !== "add" &&
|
|
653
|
+
action !== "resume" &&
|
|
654
|
+
action !== "commit" &&
|
|
655
|
+
sessionId !== undefined) {
|
|
576
656
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", `worktree ${action} takes the session from the worktree; omit --session.`);
|
|
577
657
|
}
|
|
578
658
|
return {
|
|
@@ -594,7 +674,11 @@ export function parseArgs(argv) {
|
|
|
594
674
|
...(evidence.length === 0 ? {} : { evidence }),
|
|
595
675
|
...(sessionId === undefined ? {} : { sessionId }),
|
|
596
676
|
...(base === undefined ? {} : { base }),
|
|
677
|
+
...(parentBase === undefined ? {} : { parentBase }),
|
|
678
|
+
...(width === undefined ? {} : { width }),
|
|
597
679
|
...(worktreeFrom === undefined ? {} : { worktreeFrom }),
|
|
680
|
+
...(worktreeMessage === undefined ? {} : { worktreeMessage }),
|
|
681
|
+
...(output === undefined ? {} : { output }),
|
|
598
682
|
...(worktreeTargetBranch === undefined ? {} : { worktreeTargetBranch }),
|
|
599
683
|
...(checkAuth ? { checkAuth } : {}),
|
|
600
684
|
...(checkAuthTargets.length === 0 ? {} : { checkAuthTargets }),
|
|
@@ -6,9 +6,10 @@ import { join } from "node:path";
|
|
|
6
6
|
import { execFileSync, spawn } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { commonHarnessAdapters, harnessHookPath, HARNESS_IDS } from "../../../runtime/src/install/harness.js";
|
|
9
|
-
import { agyRuntimeStatus, agyVersionSupported, hookRegistrationDrift,
|
|
9
|
+
import { agyRuntimeStatus, agyVersionSupported, hookRegistrationDrift, repositoryTrustProbe, smokeAvailabilityStatus } from "../../../runtime/src/install/probes.js";
|
|
10
10
|
import { parseInstallManifest } from "../../../runtime/src/fs/manifest.js";
|
|
11
|
-
import { readRecordedSessionId } from "../../../runtime/src/hooks/codex-loop.js";
|
|
11
|
+
import { readRecordedSessionId, resolveCommandSessionId, sessionIdFromEnvironment } from "../../../runtime/src/hooks/codex-loop.js";
|
|
12
|
+
import { codexHomeDirectory } from "../../../runtime/src/install/preauth.js";
|
|
12
13
|
import { NpmRegistryClient } from "../../../runtime/src/registry/npm.js";
|
|
13
14
|
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
14
15
|
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
@@ -17,6 +18,7 @@ import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
|
|
|
17
18
|
import { calculateSourceFingerprint } from "../../../runtime/src/verify/source-fingerprint.js";
|
|
18
19
|
import { VerificationService } from "../../../runtime/src/verify/service.js";
|
|
19
20
|
import { NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn.js";
|
|
21
|
+
import { COMMAND_NAMES, parseArgs } from "./args.js";
|
|
20
22
|
import { runCli } from "./cli.js";
|
|
21
23
|
import { loadEffectiveConfig, repositoryTrust, repositoryTrustBinding } from "./context.js";
|
|
22
24
|
import { runHookProcess } from "./hook-process.js";
|
|
@@ -29,9 +31,12 @@ import { runDoctorCommand } from "./commands/doctor.js";
|
|
|
29
31
|
import { formatInstallPlan, runInitCommand } from "./commands/init.js";
|
|
30
32
|
import { formatUninstallPlan, runUninstallCommand } from "./commands/uninstall.js";
|
|
31
33
|
import { runTaskCommand } from "./commands/task.js";
|
|
32
|
-
import { runReviewCommand } from "./commands/review.js";
|
|
34
|
+
import { hasFreshVerification, runReviewCommand } from "./commands/review.js";
|
|
35
|
+
import { runBatchCommand } from "./commands/batch.js";
|
|
36
|
+
import { createSourceGuard } from "../../../runtime/src/review/batch-guard.js";
|
|
37
|
+
import { memoizePreflight } from "../../../runtime/src/review/batch.js";
|
|
33
38
|
import { createReviewExecutor, ReviewInterruptedError } from "../../../runtime/src/review/execute.js";
|
|
34
|
-
import {
|
|
39
|
+
import { probeReviewTargetDetailed } from "../../../runtime/src/review/probe.js";
|
|
35
40
|
import { resolveReviewScope } from "../../../runtime/src/review/scope.js";
|
|
36
41
|
import { resolveReviewRole } from "../../../runtime/src/review/roles.js";
|
|
37
42
|
import { runTrustCommand } from "./commands/trust.js";
|
|
@@ -42,7 +47,7 @@ import { formatUpdatePlan, runUpdateCommand } from "./commands/update.js";
|
|
|
42
47
|
import { errorEnvelope } from "./output.js";
|
|
43
48
|
import { runAgyHeadless } from "./agy-headless.js";
|
|
44
49
|
import { runWorktreeCommand } from "./commands/parallel.js";
|
|
45
|
-
import { ensureSessionWorktree, resolveCheckouts } from "../../../runtime/src/parallel/service.js";
|
|
50
|
+
import { ensureSessionWorktree, readWorktreeRecord, resolveCheckouts } from "../../../runtime/src/parallel/service.js";
|
|
46
51
|
import { listWorktrees, branchLockDoctorResult, worktreeDoctorResult } from "../../../runtime/src/parallel/manage.js";
|
|
47
52
|
import { detectGhostFiles, ghostFilesDoctorResult } from "../../../runtime/src/install/doctor.js";
|
|
48
53
|
const HOOK_RUNTIME_PATH = fileURLToPath(new URL("./hook-entry.js", import.meta.url));
|
|
@@ -206,19 +211,10 @@ else {
|
|
|
206
211
|
writeStderr: (value) => process.stderr.write(value)
|
|
207
212
|
}, {
|
|
208
213
|
version: CLI_VERSION,
|
|
209
|
-
registry: createCommandRegistry(Object.fromEntries(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
"doctor",
|
|
214
|
-
"update",
|
|
215
|
-
"uninstall",
|
|
216
|
-
"task",
|
|
217
|
-
"verify",
|
|
218
|
-
"review",
|
|
219
|
-
"allow-stop",
|
|
220
|
-
"worktree"
|
|
221
|
-
].map((command) => [command, async (args) => {
|
|
214
|
+
registry: createCommandRegistry(Object.fromEntries(
|
|
215
|
+
// Every command the parser knows is registered: a hand-kept list here
|
|
216
|
+
// once left a parsed command answering CLI_COMMAND_UNAVAILABLE.
|
|
217
|
+
COMMAND_NAMES.map((command) => [command, async (args) => {
|
|
222
218
|
const root = args.scope === "user"
|
|
223
219
|
? process.env.AGENT_OPS_HOME ?? homedir()
|
|
224
220
|
: process.cwd();
|
|
@@ -234,6 +230,7 @@ else {
|
|
|
234
230
|
isTTY,
|
|
235
231
|
toolkitVersion: CLI_VERSION,
|
|
236
232
|
hookRuntimePath: HOOK_RUNTIME_PATH,
|
|
233
|
+
codexHome: codexHomeDirectory(),
|
|
237
234
|
agyWarning: () => {
|
|
238
235
|
try {
|
|
239
236
|
const output = runAgy(["--version"], { timeout: 5_000 });
|
|
@@ -304,34 +301,7 @@ else {
|
|
|
304
301
|
};
|
|
305
302
|
},
|
|
306
303
|
repositoryTrust: async () => {
|
|
307
|
-
|
|
308
|
-
const status = repositoryTrustStatus(trust);
|
|
309
|
-
if (trust === "STALE") {
|
|
310
|
-
return {
|
|
311
|
-
status,
|
|
312
|
-
message: "Repository trust binding is stale.",
|
|
313
|
-
code: "TRUST_REQUIRED",
|
|
314
|
-
remediation: "Run `agent-ops trust grant`."
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
if (trust === "UNTRUSTED") {
|
|
318
|
-
const verificationConfigured = config.verification.commands.length > 0;
|
|
319
|
-
return {
|
|
320
|
-
status,
|
|
321
|
-
message: verificationConfigured
|
|
322
|
-
? "Repository is not trusted; Stop verification will not run."
|
|
323
|
-
: "Repository is not trusted.",
|
|
324
|
-
...(verificationConfigured
|
|
325
|
-
? {
|
|
326
|
-
code: "TRUST_REQUIRED",
|
|
327
|
-
remediation: "Run `agent-ops trust grant`."
|
|
328
|
-
}
|
|
329
|
-
: {
|
|
330
|
-
remediation: "No action needed; trust is only required once verification.commands is set."
|
|
331
|
-
})
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
return { status };
|
|
304
|
+
return repositoryTrustProbe(await repositoryTrust(root, config, CLI_VERSION), config.verification.commands.length > 0);
|
|
335
305
|
},
|
|
336
306
|
smokeAvailability: () => {
|
|
337
307
|
const status = smokeAvailabilityStatus(config);
|
|
@@ -343,7 +313,7 @@ else {
|
|
|
343
313
|
}
|
|
344
314
|
: { status };
|
|
345
315
|
},
|
|
346
|
-
reviewTarget: async (target, deep) => await
|
|
316
|
+
reviewTarget: async (target, deep) => await probeReviewTargetDetailed(target, { cwd: root, deep }),
|
|
347
317
|
worktrees: async () => await worktreeDoctorProbe(root),
|
|
348
318
|
worktreeBranchLock: async () => await worktreeBranchLockProbe(root),
|
|
349
319
|
rootGhostFiles: async () => await rootGhostFilesProbe(root)
|
|
@@ -362,6 +332,7 @@ else {
|
|
|
362
332
|
args,
|
|
363
333
|
root,
|
|
364
334
|
isTTY,
|
|
335
|
+
codexHome: codexHomeDirectory(),
|
|
365
336
|
trustStore: store,
|
|
366
337
|
calculateTrustBinding: async () => await plannedTrustBinding(root),
|
|
367
338
|
confirm: async (plan, trust) => await confirmPlan(formatUninstallPlan(plan, trust))
|
|
@@ -387,6 +358,7 @@ else {
|
|
|
387
358
|
isTTY,
|
|
388
359
|
toolkitVersion: CLI_VERSION,
|
|
389
360
|
hookRuntimePath: HOOK_RUNTIME_PATH,
|
|
361
|
+
codexHome: codexHomeDirectory(),
|
|
390
362
|
...(updateArgs.hookTargets === undefined
|
|
391
363
|
? {}
|
|
392
364
|
: { hookTargets: updateArgs.hookTargets }),
|
|
@@ -422,8 +394,13 @@ else {
|
|
|
422
394
|
// Explicit `--session` wins, then an injected identity, then the
|
|
423
395
|
// id a SessionStart hook recorded for this checkout: a command
|
|
424
396
|
// run inside a session is never told which session it is in.
|
|
425
|
-
|
|
426
|
-
|
|
397
|
+
// Only create, attach and status act as a session; a contested
|
|
398
|
+
// record refuses those rather than guess, and leaves the rest.
|
|
399
|
+
const actsAsSession = args.action === "create" ||
|
|
400
|
+
args.action === "attach" || args.action === "status";
|
|
401
|
+
const sessionId = args.sessionId ?? (actsAsSession
|
|
402
|
+
? await resolveCommandSessionId(root)
|
|
403
|
+
: sessionIdFromEnvironment() ?? await readRecordedSessionId(root));
|
|
427
404
|
const createConfig = args.action === "create"
|
|
428
405
|
? (await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config
|
|
429
406
|
: undefined;
|
|
@@ -460,12 +437,68 @@ else {
|
|
|
460
437
|
: {})
|
|
461
438
|
});
|
|
462
439
|
}
|
|
463
|
-
|
|
440
|
+
const probeTarget = async (target, budget) => await probeReviewTargetDetailed(target, {
|
|
441
|
+
cwd: root,
|
|
442
|
+
deep: true,
|
|
443
|
+
// The probe answers within the chain's remaining budget
|
|
444
|
+
// or not at all; its own default would outlast it.
|
|
445
|
+
...(budget === undefined ? {} : { timeoutMs: budget.timeoutMs })
|
|
446
|
+
});
|
|
447
|
+
// One place builds a review's options, so `review` and each task of
|
|
448
|
+
// a `batch` are reviewed identically.
|
|
449
|
+
const buildReviewOptions = async (reviewArgs, signal, preflightTarget = probeTarget, progressPrefix = "") => {
|
|
464
450
|
const reviewSessionId = process.env.AGENT_OPS_SESSION_ID;
|
|
465
451
|
const reviewGit = gitRunner(root);
|
|
466
|
-
const reviewConfig = (await loadEffectiveConfig(root,
|
|
452
|
+
const reviewConfig = (await loadEffectiveConfig(root, reviewArgs.scope === "user" ? "user" : "project")).config;
|
|
467
453
|
const reviewRole = resolveReviewRole("independent-review", reviewConfig.reviewRoles ?? []);
|
|
468
454
|
const configuredReviewTargets = reviewRole?.targets ?? [];
|
|
455
|
+
return {
|
|
456
|
+
args: reviewArgs,
|
|
457
|
+
authorized: reviewArgs.yes,
|
|
458
|
+
tasks: taskService,
|
|
459
|
+
...(reviewArgs.taskId === undefined ? {} : { taskId: reviewArgs.taskId }),
|
|
460
|
+
...(reviewSessionId === undefined
|
|
461
|
+
? {}
|
|
462
|
+
: { sessionId: reviewSessionId }),
|
|
463
|
+
...(reviewConfig.reviewRoles === undefined
|
|
464
|
+
? {}
|
|
465
|
+
: { roles: reviewConfig.reviewRoles }),
|
|
466
|
+
targets: configuredReviewTargets,
|
|
467
|
+
root,
|
|
468
|
+
gitRunner: reviewGit,
|
|
469
|
+
policyConfigHash: calculateConfigHash(reviewConfig),
|
|
470
|
+
currentPolicyConfigHash: async () => calculateConfigHash((await loadEffectiveConfig(root, reviewArgs.scope === "user" ? "user" : "project")).config),
|
|
471
|
+
config: reviewConfig,
|
|
472
|
+
evidenceStore: new FileEvidenceStore(root, root),
|
|
473
|
+
execute: createReviewExecutor({
|
|
474
|
+
targets: configuredReviewTargets,
|
|
475
|
+
cwd: root,
|
|
476
|
+
...(reviewRole?.model === undefined
|
|
477
|
+
? {}
|
|
478
|
+
: { model: reviewRole.model }),
|
|
479
|
+
...(reviewRole?.effort === undefined
|
|
480
|
+
? {}
|
|
481
|
+
: { effort: reviewRole.effort }),
|
|
482
|
+
...(reviewRole?.timeoutMs === undefined
|
|
483
|
+
? {}
|
|
484
|
+
: { timeoutMs: reviewRole.timeoutMs }),
|
|
485
|
+
preflightTarget,
|
|
486
|
+
verifySourceFingerprint: async (expected) => {
|
|
487
|
+
const currentScope = await resolveReviewScope({
|
|
488
|
+
root,
|
|
489
|
+
runner: reviewGit,
|
|
490
|
+
...(reviewArgs.base === undefined ? {} : { base: reviewArgs.base })
|
|
491
|
+
});
|
|
492
|
+
return await calculateSourceFingerprint(root, currentScope, reviewGit) === expected;
|
|
493
|
+
},
|
|
494
|
+
signal,
|
|
495
|
+
onProgress: (line) => {
|
|
496
|
+
process.stderr.write(`${progressPrefix}${line}\n`);
|
|
497
|
+
}
|
|
498
|
+
})
|
|
499
|
+
};
|
|
500
|
+
};
|
|
501
|
+
if (args.command === "review") {
|
|
469
502
|
const controller = new AbortController();
|
|
470
503
|
let interruptedBy;
|
|
471
504
|
const interrupt = (signal) => {
|
|
@@ -477,59 +510,7 @@ else {
|
|
|
477
510
|
process.once("SIGINT", onSigint);
|
|
478
511
|
process.once("SIGTERM", onSigterm);
|
|
479
512
|
try {
|
|
480
|
-
return await runReviewCommand(
|
|
481
|
-
args,
|
|
482
|
-
authorized: args.yes,
|
|
483
|
-
tasks: taskService,
|
|
484
|
-
...(args.taskId === undefined ? {} : { taskId: args.taskId }),
|
|
485
|
-
...(reviewSessionId === undefined
|
|
486
|
-
? {}
|
|
487
|
-
: { sessionId: reviewSessionId }),
|
|
488
|
-
...(reviewConfig.reviewRoles === undefined
|
|
489
|
-
? {}
|
|
490
|
-
: { roles: reviewConfig.reviewRoles }),
|
|
491
|
-
targets: configuredReviewTargets,
|
|
492
|
-
root,
|
|
493
|
-
gitRunner: reviewGit,
|
|
494
|
-
policyConfigHash: calculateConfigHash(reviewConfig),
|
|
495
|
-
currentPolicyConfigHash: async () => calculateConfigHash((await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config),
|
|
496
|
-
config: reviewConfig,
|
|
497
|
-
evidenceStore: new FileEvidenceStore(root, root),
|
|
498
|
-
execute: createReviewExecutor({
|
|
499
|
-
targets: configuredReviewTargets,
|
|
500
|
-
cwd: root,
|
|
501
|
-
...(reviewRole?.model === undefined
|
|
502
|
-
? {}
|
|
503
|
-
: { model: reviewRole.model }),
|
|
504
|
-
...(reviewRole?.effort === undefined
|
|
505
|
-
? {}
|
|
506
|
-
: { effort: reviewRole.effort }),
|
|
507
|
-
...(reviewRole?.timeoutMs === undefined
|
|
508
|
-
? {}
|
|
509
|
-
: { timeoutMs: reviewRole.timeoutMs }),
|
|
510
|
-
preflightTarget: async (target, budget) => await probeReviewTarget(target, {
|
|
511
|
-
cwd: root,
|
|
512
|
-
deep: true,
|
|
513
|
-
// The probe answers within the chain's remaining budget
|
|
514
|
-
// or not at all; its own default would outlast it.
|
|
515
|
-
...(budget === undefined
|
|
516
|
-
? {}
|
|
517
|
-
: { timeoutMs: budget.timeoutMs })
|
|
518
|
-
}),
|
|
519
|
-
verifySourceFingerprint: async (expected) => {
|
|
520
|
-
const currentScope = await resolveReviewScope({
|
|
521
|
-
root,
|
|
522
|
-
runner: reviewGit,
|
|
523
|
-
...(args.base === undefined ? {} : { base: args.base })
|
|
524
|
-
});
|
|
525
|
-
return await calculateSourceFingerprint(root, currentScope, reviewGit) === expected;
|
|
526
|
-
},
|
|
527
|
-
signal: controller.signal,
|
|
528
|
-
onProgress: (line) => {
|
|
529
|
-
process.stderr.write(`${line}\n`);
|
|
530
|
-
}
|
|
531
|
-
})
|
|
532
|
-
});
|
|
513
|
+
return await runReviewCommand(await buildReviewOptions(args, controller.signal));
|
|
533
514
|
}
|
|
534
515
|
catch (error) {
|
|
535
516
|
if (error instanceof ReviewInterruptedError &&
|
|
@@ -543,6 +524,72 @@ else {
|
|
|
543
524
|
process.removeListener("SIGTERM", onSigterm);
|
|
544
525
|
}
|
|
545
526
|
}
|
|
527
|
+
if (args.command === "batch") {
|
|
528
|
+
const controller = new AbortController();
|
|
529
|
+
let interruptedBy;
|
|
530
|
+
const interrupt = (signal) => {
|
|
531
|
+
interruptedBy ??= signal;
|
|
532
|
+
controller.abort(signal);
|
|
533
|
+
};
|
|
534
|
+
const onSigint = () => interrupt("SIGINT");
|
|
535
|
+
const onSigterm = () => interrupt("SIGTERM");
|
|
536
|
+
process.once("SIGINT", onSigint);
|
|
537
|
+
process.once("SIGTERM", onSigterm);
|
|
538
|
+
try {
|
|
539
|
+
const batchScope = args.scope === "user" ? "user" : "project";
|
|
540
|
+
const batchConfig = (await loadEffectiveConfig(root, batchScope)).config;
|
|
541
|
+
const batchGit = gitRunner(root);
|
|
542
|
+
const trusted = await repositoryTrust(root, batchConfig, CLI_VERSION) === "TRUSTED";
|
|
543
|
+
const guard = await createSourceGuard(batchGit);
|
|
544
|
+
const preflight = memoizePreflight(probeTarget);
|
|
545
|
+
const taskArgs = (taskId, base) => parseArgs(["review", "--task", taskId, "--base", base, "--yes"]);
|
|
546
|
+
const worktreeRecord = await readWorktreeRecord(root);
|
|
547
|
+
const envelope = await runBatchCommand({
|
|
548
|
+
args,
|
|
549
|
+
tasks: taskService,
|
|
550
|
+
...(worktreeRecord === null ? {} : { worktreeBase: worktreeRecord.base }),
|
|
551
|
+
guard,
|
|
552
|
+
isVerified: async (taskId, base) => await hasFreshVerification(await buildReviewOptions(taskArgs(taskId, base), controller.signal, preflight), taskId, base),
|
|
553
|
+
verify: async (taskId, base) => {
|
|
554
|
+
const report = await new VerificationService({
|
|
555
|
+
root,
|
|
556
|
+
scope: batchScope,
|
|
557
|
+
config: batchConfig,
|
|
558
|
+
gitRunner: batchGit,
|
|
559
|
+
processRunner: new NodeVerificationProcessRunner(),
|
|
560
|
+
taskService,
|
|
561
|
+
evidenceStore: new FileEvidenceStore(root, root),
|
|
562
|
+
trusted,
|
|
563
|
+
base
|
|
564
|
+
}).verify(taskId);
|
|
565
|
+
return report.status === "PASS" ? "PASS" : "FAIL";
|
|
566
|
+
},
|
|
567
|
+
review: async (taskId, base, signal) => {
|
|
568
|
+
const reviewed = await runReviewCommand(await buildReviewOptions(taskArgs(taskId, base), signal, preflight, `${taskId.slice(5, 13)}: `));
|
|
569
|
+
const result = reviewed.data?.result;
|
|
570
|
+
return result === undefined
|
|
571
|
+
? { status: "NOT_RUN", reason: reviewed.errors[0]?.code ?? "review-error" }
|
|
572
|
+
: {
|
|
573
|
+
status: result.status,
|
|
574
|
+
...(result.reason === undefined ? {} : { reason: result.reason }),
|
|
575
|
+
...(result.reused === true ? { reused: true } : {})
|
|
576
|
+
};
|
|
577
|
+
},
|
|
578
|
+
signal: controller.signal,
|
|
579
|
+
onProgress: (line) => {
|
|
580
|
+
process.stderr.write(`batch: ${line}\n`);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
if (interruptedBy !== undefined) {
|
|
584
|
+
process.exit(interruptedBy === "SIGINT" ? 130 : 143);
|
|
585
|
+
}
|
|
586
|
+
return envelope;
|
|
587
|
+
}
|
|
588
|
+
finally {
|
|
589
|
+
process.removeListener("SIGINT", onSigint);
|
|
590
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
546
593
|
if (args.command === "config") {
|
|
547
594
|
return explainConfigCommand(await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project"));
|
|
548
595
|
}
|