@kylecheng3146/agent-ops 0.4.0 → 0.5.0
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/LICENSE +91 -21
- package/README.md +39 -1
- package/dist/packages/cli/src/args.js +46 -6
- package/dist/packages/cli/src/bin.js +135 -70
- package/dist/packages/cli/src/cli.js +27 -3
- package/dist/packages/cli/src/commands/batch.js +106 -0
- package/dist/packages/cli/src/commands/review.js +22 -0
- package/dist/runtime/src/install/harness.js +1 -1
- package/dist/runtime/src/install/preauth.js +49 -6
- 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/invocation.js +15 -2
- package/dist/runtime/src/review/probe.js +13 -0
- package/docs/en/guides/configuration.md +6 -3
- package/docs/en/spec/review.md +70 -0
- package/docs/zh-TW/guides/configuration.md +8 -5
- package/docs/zh-TW/spec/review.md +71 -1
- package/package.json +2 -2
package/LICENSE
CHANGED
|
@@ -1,21 +1,91 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
Required Notice: Copyright (c) 2026 Kyle Cheng (https://github.com/kylecheng3146/agent-ops)
|
|
2
|
+
|
|
3
|
+
# PolyForm Shield License 1.0.0
|
|
4
|
+
|
|
5
|
+
<https://polyformproject.org/licenses/shield/1.0.0>
|
|
6
|
+
|
|
7
|
+
## Acceptance
|
|
8
|
+
|
|
9
|
+
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
|
|
14
|
+
|
|
15
|
+
## Distribution License
|
|
16
|
+
|
|
17
|
+
The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
|
|
18
|
+
|
|
19
|
+
## Notices
|
|
20
|
+
|
|
21
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
|
|
22
|
+
|
|
23
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
24
|
+
|
|
25
|
+
## Changes and New Works License
|
|
26
|
+
|
|
27
|
+
The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
|
|
28
|
+
|
|
29
|
+
## Patent License
|
|
30
|
+
|
|
31
|
+
The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
|
|
32
|
+
|
|
33
|
+
## Noncompete
|
|
34
|
+
|
|
35
|
+
Any purpose is a permitted purpose, except for providing any product that competes with the software or any product the licensor or any of its affiliates provides using the software.
|
|
36
|
+
|
|
37
|
+
## Competition
|
|
38
|
+
|
|
39
|
+
Goods and services compete even when they provide functionality through different kinds of interfaces or for different technical platforms. Applications can compete with services, libraries with plugins, frameworks with development tools, and so on, even if they're written in different programming languages or for different computer architectures. Goods and services compete even when provided free of charge. If you market a product as a practical substitute for the software or another product, it definitely competes.
|
|
40
|
+
|
|
41
|
+
## New Products
|
|
42
|
+
|
|
43
|
+
If you are using the software to provide a product that does not compete, but the licensor or any of its affiliates brings your product into competition by providing a new version of the software or another product using the software, you may continue using versions of the software available under these terms beforehand to provide your competing product, but not any later versions.
|
|
44
|
+
|
|
45
|
+
## Discontinued Products
|
|
46
|
+
|
|
47
|
+
You may begin using the software to compete with a product or service that the licensor or any of its affiliates has stopped providing, unless the licensor includes a plain-text line beginning with `Licensor Line of Business:` with the software that mentions that line of business. For example:
|
|
48
|
+
|
|
49
|
+
> Licensor Line of Business: YoyodyneCMS Content Management System (http://example.com/cms)
|
|
50
|
+
|
|
51
|
+
## Sales of Business
|
|
52
|
+
|
|
53
|
+
If the licensor or any of its affiliates sells a line of business developing the software or using the software to provide a product, the buyer can also enforce [Noncompete](#noncompete) for that product.
|
|
54
|
+
|
|
55
|
+
## Fair Use
|
|
56
|
+
|
|
57
|
+
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
|
58
|
+
|
|
59
|
+
## No Other Rights
|
|
60
|
+
|
|
61
|
+
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
|
62
|
+
|
|
63
|
+
## Patent Defense
|
|
64
|
+
|
|
65
|
+
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
|
66
|
+
|
|
67
|
+
## Violations
|
|
68
|
+
|
|
69
|
+
The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
|
|
70
|
+
|
|
71
|
+
## No Liability
|
|
72
|
+
|
|
73
|
+
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
|
74
|
+
|
|
75
|
+
## Definitions
|
|
76
|
+
|
|
77
|
+
The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
|
|
78
|
+
|
|
79
|
+
A **product** can be a good or service, or a combination of them.
|
|
80
|
+
|
|
81
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
82
|
+
|
|
83
|
+
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all its affiliates.
|
|
84
|
+
|
|
85
|
+
**Affiliates** means the other organizations than an organization has control over, is under the control of, or is under common control with.
|
|
86
|
+
|
|
87
|
+
**Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
88
|
+
|
|
89
|
+
**Your licenses** are all the licenses granted to you for the software under these terms.
|
|
90
|
+
|
|
91
|
+
**Use** means anything you do with the software requiring one of your licenses.
|
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
|
|
@@ -377,4 +412,7 @@ Documentation:
|
|
|
377
412
|
|
|
378
413
|
## License
|
|
379
414
|
|
|
380
|
-
Copyright (c) 2026 Kyle Cheng. Released under the
|
|
415
|
+
Copyright (c) 2026 Kyle Cheng. Released under the
|
|
416
|
+
[PolyForm Shield License 1.0.0](LICENSE): free to use, change and share,
|
|
417
|
+
including inside a company, but not to build a product or service that competes
|
|
418
|
+
with agent-ops. Versions up to 0.4.1 were released under, and remain under, the MIT License.
|
|
@@ -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
|
];
|
|
@@ -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 = [];
|
|
@@ -210,6 +213,26 @@ export function parseArgs(argv) {
|
|
|
210
213
|
index += 1;
|
|
211
214
|
break;
|
|
212
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
|
+
}
|
|
213
236
|
case "--check-auth":
|
|
214
237
|
if (checkAuth) {
|
|
215
238
|
duplicate(token);
|
|
@@ -416,6 +439,8 @@ export function parseArgs(argv) {
|
|
|
416
439
|
completionGate !== undefined ||
|
|
417
440
|
sessionId !== undefined ||
|
|
418
441
|
base !== undefined ||
|
|
442
|
+
parentBase !== undefined ||
|
|
443
|
+
width !== undefined ||
|
|
419
444
|
checkAuth ||
|
|
420
445
|
checkAuthTargets.length > 0 ||
|
|
421
446
|
dryRun ||
|
|
@@ -465,15 +490,18 @@ export function parseArgs(argv) {
|
|
|
465
490
|
(hasTaskTargetOptions || hasTaskMutationOptions)) {
|
|
466
491
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Task target options may be used only with task or verify.");
|
|
467
492
|
}
|
|
468
|
-
if (command !== "task" && parentTaskId !== undefined) {
|
|
469
|
-
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.");
|
|
470
495
|
}
|
|
471
496
|
if (command !== "update" && targetVersion !== undefined) {
|
|
472
497
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--target-version may be used only with update.");
|
|
473
498
|
}
|
|
474
499
|
if (base !== undefined && command !== "verify" && command !== "review" &&
|
|
475
|
-
!(command === "task" && action === "complete")) {
|
|
476
|
-
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.`);
|
|
477
505
|
}
|
|
478
506
|
if (hookTargets.length > 0 &&
|
|
479
507
|
command !== "init" &&
|
|
@@ -566,6 +594,16 @@ export function parseArgs(argv) {
|
|
|
566
594
|
if (command === "review" && (criteria.length > 0 || evidence.length > 0)) {
|
|
567
595
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Complete review covers every task criterion; omit --criterion and --evidence.");
|
|
568
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
|
+
}
|
|
569
607
|
if (command === "allow-stop" &&
|
|
570
608
|
(sessionId === undefined ||
|
|
571
609
|
scope !== undefined ||
|
|
@@ -589,8 +627,8 @@ export function parseArgs(argv) {
|
|
|
589
627
|
rerun)) {
|
|
590
628
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "worktree accepts only a name, --session, --from, --target-branch, --message and --json.");
|
|
591
629
|
}
|
|
592
|
-
if (output !== undefined && command !== "review" && command !== "doctor") {
|
|
593
|
-
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--output may be used only with review or doctor.", "--output");
|
|
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");
|
|
594
632
|
}
|
|
595
633
|
if (output !== undefined && (output.trim() === "" || output.includes("\0"))) {
|
|
596
634
|
throw new CliArgumentError("CLI_INVALID_VALUE", "Invalid value for --output.", "--output");
|
|
@@ -636,6 +674,8 @@ export function parseArgs(argv) {
|
|
|
636
674
|
...(evidence.length === 0 ? {} : { evidence }),
|
|
637
675
|
...(sessionId === undefined ? {} : { sessionId }),
|
|
638
676
|
...(base === undefined ? {} : { base }),
|
|
677
|
+
...(parentBase === undefined ? {} : { parentBase }),
|
|
678
|
+
...(width === undefined ? {} : { width }),
|
|
639
679
|
...(worktreeFrom === undefined ? {} : { worktreeFrom }),
|
|
640
680
|
...(worktreeMessage === undefined ? {} : { worktreeMessage }),
|
|
641
681
|
...(output === undefined ? {} : { output }),
|
|
@@ -18,6 +18,7 @@ import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
|
|
|
18
18
|
import { calculateSourceFingerprint } from "../../../runtime/src/verify/source-fingerprint.js";
|
|
19
19
|
import { VerificationService } from "../../../runtime/src/verify/service.js";
|
|
20
20
|
import { NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn.js";
|
|
21
|
+
import { COMMAND_NAMES, parseArgs } from "./args.js";
|
|
21
22
|
import { runCli } from "./cli.js";
|
|
22
23
|
import { loadEffectiveConfig, repositoryTrust, repositoryTrustBinding } from "./context.js";
|
|
23
24
|
import { runHookProcess } from "./hook-process.js";
|
|
@@ -30,7 +31,10 @@ import { runDoctorCommand } from "./commands/doctor.js";
|
|
|
30
31
|
import { formatInstallPlan, runInitCommand } from "./commands/init.js";
|
|
31
32
|
import { formatUninstallPlan, runUninstallCommand } from "./commands/uninstall.js";
|
|
32
33
|
import { runTaskCommand } from "./commands/task.js";
|
|
33
|
-
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";
|
|
34
38
|
import { createReviewExecutor, ReviewInterruptedError } from "../../../runtime/src/review/execute.js";
|
|
35
39
|
import { probeReviewTargetDetailed } from "../../../runtime/src/review/probe.js";
|
|
36
40
|
import { resolveReviewScope } from "../../../runtime/src/review/scope.js";
|
|
@@ -43,7 +47,7 @@ import { formatUpdatePlan, runUpdateCommand } from "./commands/update.js";
|
|
|
43
47
|
import { errorEnvelope } from "./output.js";
|
|
44
48
|
import { runAgyHeadless } from "./agy-headless.js";
|
|
45
49
|
import { runWorktreeCommand } from "./commands/parallel.js";
|
|
46
|
-
import { ensureSessionWorktree, resolveCheckouts } from "../../../runtime/src/parallel/service.js";
|
|
50
|
+
import { ensureSessionWorktree, readWorktreeRecord, resolveCheckouts } from "../../../runtime/src/parallel/service.js";
|
|
47
51
|
import { listWorktrees, branchLockDoctorResult, worktreeDoctorResult } from "../../../runtime/src/parallel/manage.js";
|
|
48
52
|
import { detectGhostFiles, ghostFilesDoctorResult } from "../../../runtime/src/install/doctor.js";
|
|
49
53
|
const HOOK_RUNTIME_PATH = fileURLToPath(new URL("./hook-entry.js", import.meta.url));
|
|
@@ -207,19 +211,10 @@ else {
|
|
|
207
211
|
writeStderr: (value) => process.stderr.write(value)
|
|
208
212
|
}, {
|
|
209
213
|
version: CLI_VERSION,
|
|
210
|
-
registry: createCommandRegistry(Object.fromEntries(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
"doctor",
|
|
215
|
-
"update",
|
|
216
|
-
"uninstall",
|
|
217
|
-
"task",
|
|
218
|
-
"verify",
|
|
219
|
-
"review",
|
|
220
|
-
"allow-stop",
|
|
221
|
-
"worktree"
|
|
222
|
-
].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) => {
|
|
223
218
|
const root = args.scope === "user"
|
|
224
219
|
? process.env.AGENT_OPS_HOME ?? homedir()
|
|
225
220
|
: process.cwd();
|
|
@@ -442,12 +437,68 @@ else {
|
|
|
442
437
|
: {})
|
|
443
438
|
});
|
|
444
439
|
}
|
|
445
|
-
|
|
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 = "") => {
|
|
446
450
|
const reviewSessionId = process.env.AGENT_OPS_SESSION_ID;
|
|
447
451
|
const reviewGit = gitRunner(root);
|
|
448
|
-
const reviewConfig = (await loadEffectiveConfig(root,
|
|
452
|
+
const reviewConfig = (await loadEffectiveConfig(root, reviewArgs.scope === "user" ? "user" : "project")).config;
|
|
449
453
|
const reviewRole = resolveReviewRole("independent-review", reviewConfig.reviewRoles ?? []);
|
|
450
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") {
|
|
451
502
|
const controller = new AbortController();
|
|
452
503
|
let interruptedBy;
|
|
453
504
|
const interrupt = (signal) => {
|
|
@@ -459,59 +510,7 @@ else {
|
|
|
459
510
|
process.once("SIGINT", onSigint);
|
|
460
511
|
process.once("SIGTERM", onSigterm);
|
|
461
512
|
try {
|
|
462
|
-
return await runReviewCommand(
|
|
463
|
-
args,
|
|
464
|
-
authorized: args.yes,
|
|
465
|
-
tasks: taskService,
|
|
466
|
-
...(args.taskId === undefined ? {} : { taskId: args.taskId }),
|
|
467
|
-
...(reviewSessionId === undefined
|
|
468
|
-
? {}
|
|
469
|
-
: { sessionId: reviewSessionId }),
|
|
470
|
-
...(reviewConfig.reviewRoles === undefined
|
|
471
|
-
? {}
|
|
472
|
-
: { roles: reviewConfig.reviewRoles }),
|
|
473
|
-
targets: configuredReviewTargets,
|
|
474
|
-
root,
|
|
475
|
-
gitRunner: reviewGit,
|
|
476
|
-
policyConfigHash: calculateConfigHash(reviewConfig),
|
|
477
|
-
currentPolicyConfigHash: async () => calculateConfigHash((await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project")).config),
|
|
478
|
-
config: reviewConfig,
|
|
479
|
-
evidenceStore: new FileEvidenceStore(root, root),
|
|
480
|
-
execute: createReviewExecutor({
|
|
481
|
-
targets: configuredReviewTargets,
|
|
482
|
-
cwd: root,
|
|
483
|
-
...(reviewRole?.model === undefined
|
|
484
|
-
? {}
|
|
485
|
-
: { model: reviewRole.model }),
|
|
486
|
-
...(reviewRole?.effort === undefined
|
|
487
|
-
? {}
|
|
488
|
-
: { effort: reviewRole.effort }),
|
|
489
|
-
...(reviewRole?.timeoutMs === undefined
|
|
490
|
-
? {}
|
|
491
|
-
: { timeoutMs: reviewRole.timeoutMs }),
|
|
492
|
-
preflightTarget: async (target, budget) => await probeReviewTargetDetailed(target, {
|
|
493
|
-
cwd: root,
|
|
494
|
-
deep: true,
|
|
495
|
-
// The probe answers within the chain's remaining budget
|
|
496
|
-
// or not at all; its own default would outlast it.
|
|
497
|
-
...(budget === undefined
|
|
498
|
-
? {}
|
|
499
|
-
: { timeoutMs: budget.timeoutMs })
|
|
500
|
-
}),
|
|
501
|
-
verifySourceFingerprint: async (expected) => {
|
|
502
|
-
const currentScope = await resolveReviewScope({
|
|
503
|
-
root,
|
|
504
|
-
runner: reviewGit,
|
|
505
|
-
...(args.base === undefined ? {} : { base: args.base })
|
|
506
|
-
});
|
|
507
|
-
return await calculateSourceFingerprint(root, currentScope, reviewGit) === expected;
|
|
508
|
-
},
|
|
509
|
-
signal: controller.signal,
|
|
510
|
-
onProgress: (line) => {
|
|
511
|
-
process.stderr.write(`${line}\n`);
|
|
512
|
-
}
|
|
513
|
-
})
|
|
514
|
-
});
|
|
513
|
+
return await runReviewCommand(await buildReviewOptions(args, controller.signal));
|
|
515
514
|
}
|
|
516
515
|
catch (error) {
|
|
517
516
|
if (error instanceof ReviewInterruptedError &&
|
|
@@ -525,6 +524,72 @@ else {
|
|
|
525
524
|
process.removeListener("SIGTERM", onSigterm);
|
|
526
525
|
}
|
|
527
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
|
+
}
|
|
528
593
|
if (args.command === "config") {
|
|
529
594
|
return explainConfigCommand(await loadEffectiveConfig(root, args.scope === "user" ? "user" : "project"));
|
|
530
595
|
}
|
|
@@ -29,6 +29,7 @@ Commands:
|
|
|
29
29
|
Manage independent task acceptance state
|
|
30
30
|
verify Run configured verification
|
|
31
31
|
review Run an independent review
|
|
32
|
+
batch Verify and review a parent task's subtasks together
|
|
32
33
|
allow-stop Grant one fingerprint-bound completion-gate Stop permit (requires --session)
|
|
33
34
|
agy-run Run headless agy with a process-exit completion recheck
|
|
34
35
|
worktree <add|finish|list|resume|remove>
|
|
@@ -47,13 +48,16 @@ Options:
|
|
|
47
48
|
--check-auth-target <target> Repeatable doctor filter with --check-auth
|
|
48
49
|
--task <id>
|
|
49
50
|
--parent <task-id> Task create: record a subtask of this
|
|
50
|
-
task; task status: list its subtasks
|
|
51
|
+
task; task status: list its subtasks;
|
|
52
|
+
batch: the task whose subtasks it covers
|
|
53
|
+
--parent-base <git-ref> Batch only: base for the parent task
|
|
54
|
+
--width <n> Batch only: reviews allowed at once (default 2)
|
|
51
55
|
--target-version <version> Update target version (offline-capable)
|
|
52
56
|
--title <text>
|
|
53
57
|
--criterion <json> Repeatable
|
|
54
58
|
--evidence <criterion-id=reference> Repeatable
|
|
55
59
|
--session <id>
|
|
56
|
-
--base <git-ref> Verify/review/complete a clean committed range
|
|
60
|
+
--base <git-ref> Verify/review/batch/complete a clean committed range
|
|
57
61
|
--dry-run
|
|
58
62
|
--json
|
|
59
63
|
--yes
|
|
@@ -181,6 +185,26 @@ Options:
|
|
|
181
185
|
one summary line, so no shell redirect is needed
|
|
182
186
|
--json
|
|
183
187
|
--yes Required authorization for both reviewer sessions
|
|
188
|
+
`,
|
|
189
|
+
batch: `Usage: agent-ops batch --parent <task-id> --yes [options]
|
|
190
|
+
|
|
191
|
+
Verify and review the active subtasks of a parent task, and the parent itself,
|
|
192
|
+
together. Verify runs one task at a time and skips a task that already has
|
|
193
|
+
fresh PASS evidence; each review starts as soon as its own verify passes.
|
|
194
|
+
Nothing may commit or edit the worktree while it runs.
|
|
195
|
+
|
|
196
|
+
Options:
|
|
197
|
+
--parent <task-id> Required task whose active subtasks and itself are covered
|
|
198
|
+
--base <git-ref> Base for the subtasks (default: each task's recorded
|
|
199
|
+
review base, then the worktree base)
|
|
200
|
+
--parent-base <git-ref>
|
|
201
|
+
Base for the parent task (same default)
|
|
202
|
+
--width <n> Reviews allowed at once (default 2); a transient
|
|
203
|
+
NOT_RUN lowers it to 1 and earns one retry
|
|
204
|
+
--output <file> Write the JSON envelope to <file> (mode 0600) and print
|
|
205
|
+
one summary line, so no shell redirect is needed
|
|
206
|
+
--json
|
|
207
|
+
--yes Required authorization for the reviewer sessions
|
|
184
208
|
`,
|
|
185
209
|
"allow-stop": `Usage: agent-ops allow-stop --session <id> [options]
|
|
186
210
|
|
|
@@ -289,7 +313,7 @@ export async function runCli(argv, io, services) {
|
|
|
289
313
|
const result = await execute(args);
|
|
290
314
|
const exitCode = result.status === "ok"
|
|
291
315
|
? 0
|
|
292
|
-
: result.code === "REVIEW_NOT_RUN"
|
|
316
|
+
: result.code === "REVIEW_NOT_RUN" || result.code === "BATCH_NOT_RUN"
|
|
293
317
|
? 2
|
|
294
318
|
: 1;
|
|
295
319
|
if (args.output !== undefined) {
|