@kylecheng3146/agent-ops 0.11.0 → 0.11.2
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 +19 -0
- package/dist/packages/cli/src/args.js +78 -3
- package/dist/packages/cli/src/bin.js +19 -1
- package/dist/packages/cli/src/cli.js +33 -0
- package/dist/packages/cli/src/commands/advance.js +3 -2
- package/dist/packages/cli/src/commands/followup.js +74 -0
- package/dist/packages/cli/src/commands/parallel.js +4 -0
- package/dist/packages/cli/src/commands/run.js +1 -1
- package/dist/packages/cli/src/commands/task.js +22 -5
- package/dist/packages/cli/src/context.js +13 -9
- package/dist/packages/cli/src/criteria-review.js +63 -0
- package/dist/packages/cli/src/followup-deps.js +56 -0
- package/dist/packages/cli/src/owned-run-step.js +1 -1
- package/dist/packages/cli/src/run-deps.js +16 -5
- package/dist/packages/cli/src/run-entry.js +283 -145
- package/dist/runtime/src/followup/intake.js +75 -0
- package/dist/runtime/src/followup/process.js +273 -0
- package/dist/runtime/src/followup/receipt.js +116 -0
- package/dist/runtime/src/followup/store.js +133 -0
- package/dist/runtime/src/install/harness.js +1 -1
- package/dist/runtime/src/install/preauth.js +11 -4
- package/dist/runtime/src/logging/local-log.js +12 -0
- package/dist/runtime/src/parallel/finish.js +10 -2
- package/dist/runtime/src/review/attestation.js +10 -0
- package/dist/runtime/src/review/criteria.js +252 -0
- package/dist/runtime/src/review/followup.js +233 -0
- package/dist/runtime/src/review/invocation.js +3 -3
- package/dist/runtime/src/review/runner.js +20 -0
- package/dist/runtime/src/run/service.js +59 -6
- package/dist/runtime/src/run/usage.js +5 -2
- package/dist/runtime/src/security/permissions.js +16 -1
- package/dist/runtime/src/verify/acceptance-coverage.js +2 -2
- package/dist/runtime/src/verify/service.js +62 -2
- package/dist/runtime/src/verify/spawn.js +67 -4
- package/docs/en/guides/configuration.md +5 -0
- package/docs/en/guides/task-debug.md +36 -0
- package/docs/en/spec/acceptance-and-evidence.md +54 -0
- package/docs/en/spec/loop-engineering.md +36 -0
- package/docs/en/spec/review.md +16 -0
- package/docs/zh-TW/guides/configuration.md +3 -0
- package/docs/zh-TW/guides/task-debug.md +31 -0
- package/docs/zh-TW/spec/acceptance-and-evidence.md +39 -0
- package/docs/zh-TW/spec/loop-engineering.md +21 -0
- package/docs/zh-TW/spec/review.md +17 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,19 @@ pre-1.0 interface.
|
|
|
12
12
|
The CLI is published as `@kylecheng3146/agent-ops` as a pre-1.0 interface;
|
|
13
13
|
command behavior may change before 1.0.
|
|
14
14
|
|
|
15
|
+
## Criteria advisory before implementation
|
|
16
|
+
|
|
17
|
+
Every new task, including DEBUG and native run parent/child tasks, waits for one
|
|
18
|
+
independent advisory review of its goal, criteria and assigned surface before
|
|
19
|
+
implementation. Read the suggestions, decide which to adopt, and record reasons
|
|
20
|
+
through `task revise`. Revisions do not automatically re-review.
|
|
21
|
+
|
|
22
|
+
The advisory has no time/token budget; explicit cancellation stops work. Genuine
|
|
23
|
+
execution errors report NOT_RUN and allow work. Detailed reports remain private;
|
|
24
|
+
`task status` shows their snapshot and whether they are historical. Explicitly
|
|
25
|
+
rerun with `agent-ops review --criteria --task <id> --rerun --yes`.
|
|
26
|
+
Final verification, two-round review and finish remain required.
|
|
27
|
+
|
|
15
28
|
## Executable acceptance and native goal runs (0.6.0)
|
|
16
29
|
|
|
17
30
|
Criteria can use `behavioral`, `invariant` or `review-only` acceptance contracts.
|
|
@@ -508,6 +521,12 @@ as its first and only invocation, with `--output` instead of a shell redirect.
|
|
|
508
521
|
|
|
509
522
|
See [Configuration](docs/en/guides/configuration.md) for the full contract.
|
|
510
523
|
|
|
524
|
+
Retained nonblocking review findings and residual risks are collected privately
|
|
525
|
+
across linked worktrees. Use `agent-ops followup list` / `show`, metadata-only
|
|
526
|
+
`import`, reasoned `accept` / `defer`, and explicit `process <followup-id> --host
|
|
527
|
+
codex` for current-source revalidation and bounded repair. Observations need
|
|
528
|
+
`clarify`; resolution needs finding-specific proof. See [followup guidance](docs/en/guides/task-debug.md#retained-review-followups).
|
|
529
|
+
|
|
511
530
|
## Project principles
|
|
512
531
|
|
|
513
532
|
- Define verifiable success before making changes.
|
|
@@ -14,7 +14,8 @@ export const COMMAND_NAMES = [
|
|
|
14
14
|
"worktree",
|
|
15
15
|
"run",
|
|
16
16
|
"office",
|
|
17
|
-
"escapes"
|
|
17
|
+
"escapes",
|
|
18
|
+
"followup"
|
|
18
19
|
];
|
|
19
20
|
const COMMAND_SET = new Set(COMMAND_NAMES);
|
|
20
21
|
const SCOPES = new Set(["project", "user"]);
|
|
@@ -64,6 +65,8 @@ function parseHookTarget(option, value) {
|
|
|
64
65
|
return { harness: harness, surfaceId };
|
|
65
66
|
}
|
|
66
67
|
export function parseArgs(argv) {
|
|
68
|
+
if (argv[0] === "followup")
|
|
69
|
+
return parseFollowupArgs(argv.slice(1));
|
|
67
70
|
if (argv[0] === "run")
|
|
68
71
|
return parseRunArgs(argv.slice(1));
|
|
69
72
|
if (argv[0] === "task" && argv[1] === "debug")
|
|
@@ -113,6 +116,7 @@ export function parseArgs(argv) {
|
|
|
113
116
|
let json = false;
|
|
114
117
|
let yes = false;
|
|
115
118
|
let rerun = false;
|
|
119
|
+
let criteriaReview = false;
|
|
116
120
|
let tree = false;
|
|
117
121
|
let helpSeen = false;
|
|
118
122
|
let versionSeen = false;
|
|
@@ -461,6 +465,11 @@ export function parseArgs(argv) {
|
|
|
461
465
|
index += 1;
|
|
462
466
|
break;
|
|
463
467
|
}
|
|
468
|
+
case "--criteria":
|
|
469
|
+
if (criteriaReview)
|
|
470
|
+
throw new CliArgumentError("CLI_DUPLICATE_OPTION", "--criteria may appear only once.");
|
|
471
|
+
criteriaReview = true;
|
|
472
|
+
break;
|
|
464
473
|
case "--rerun":
|
|
465
474
|
if (rerun) {
|
|
466
475
|
duplicate(token);
|
|
@@ -687,6 +696,8 @@ export function parseArgs(argv) {
|
|
|
687
696
|
if (worktree !== undefined && command !== "init" && command !== "update") {
|
|
688
697
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--worktree may be used only with init or update.");
|
|
689
698
|
}
|
|
699
|
+
if (criteriaReview && (command !== "review" || action !== undefined || tree || base !== undefined || output !== undefined))
|
|
700
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--criteria requires review --task <id> --yes; omit show, tree, base and output.");
|
|
690
701
|
const revisionAction = command === "task" && ["revise", "replan", "pin-finding"].includes(action ?? "");
|
|
691
702
|
if ((taskGoal !== undefined || debug) && !(command === "task" && action === "create"))
|
|
692
703
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--goal and --debug apply only to task create.");
|
|
@@ -786,7 +797,7 @@ export function parseArgs(argv) {
|
|
|
786
797
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--intent may be used only with task create.");
|
|
787
798
|
}
|
|
788
799
|
if (command === "review" && action !== "show" && !yes) {
|
|
789
|
-
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Review requires --yes to authorize both reviewer sessions.");
|
|
800
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", criteriaReview ? "Criteria advisory requires --yes to authorize one reviewer session." : "Review requires --yes to authorize both reviewer sessions.");
|
|
790
801
|
}
|
|
791
802
|
if (command === "review" && harness !== undefined) {
|
|
792
803
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Complete review uses the configured reviewer pair; omit --harness.");
|
|
@@ -872,6 +883,7 @@ export function parseArgs(argv) {
|
|
|
872
883
|
...(taskGoal === undefined ? {} : { goal: taskGoal }),
|
|
873
884
|
...(debug ? { debug: true } : {}),
|
|
874
885
|
...(reviewTargets.length === 0 ? {} : { reviewTargets }),
|
|
886
|
+
...(criteriaReview ? { criteriaReview: true } : {}),
|
|
875
887
|
...(completionGate === undefined ? {} : { completionGate }),
|
|
876
888
|
...(office === undefined ? {} : { office }),
|
|
877
889
|
...(autoRun === undefined ? {} : { autoRun }),
|
|
@@ -936,10 +948,15 @@ function parseRunArgs(argv) {
|
|
|
936
948
|
args.wait = true;
|
|
937
949
|
continue;
|
|
938
950
|
}
|
|
939
|
-
if (!["--host", "--goal-file", "--time-budget", "--jobs", "--question-id", "--answer", "--answer-file", "--retry-approval"].includes(token))
|
|
951
|
+
if (!["--host", "--goal-file", "--time-budget", "--jobs", "--question-id", "--answer", "--answer-file", "--retry-approval", "--retry-delivery"].includes(token))
|
|
940
952
|
throw new CliArgumentError("CLI_UNKNOWN_OPTION", `Unknown run option: ${token}`, token);
|
|
941
953
|
const value = readOptionValue(argv, i++, token);
|
|
942
954
|
switch (token) {
|
|
955
|
+
case "--retry-delivery":
|
|
956
|
+
if (!/^task-[a-z0-9-]{1,128}$/u.test(value))
|
|
957
|
+
invalidValue(token, value);
|
|
958
|
+
args.retryDeliveryTaskId = value;
|
|
959
|
+
break;
|
|
943
960
|
case "--retry-approval":
|
|
944
961
|
if (!/^auth-[a-f0-9]{64}$/u.test(value) || args.retryApprovalIds?.includes(value))
|
|
945
962
|
invalidValue(token, value);
|
|
@@ -974,6 +991,8 @@ function parseRunArgs(argv) {
|
|
|
974
991
|
}
|
|
975
992
|
if (args.retryApprovalIds !== undefined && action !== "resume")
|
|
976
993
|
throw new CliArgumentError("CLI_INVALID_OPTION", "--retry-approval requires run resume.");
|
|
994
|
+
if (args.retryDeliveryTaskId !== undefined && (action !== "resume" || args.retryApprovalIds !== undefined))
|
|
995
|
+
throw new CliArgumentError("CLI_INVALID_OPTION", "--retry-delivery requires run resume without --retry-approval.");
|
|
977
996
|
if (action === "start") {
|
|
978
997
|
args.goal = positional;
|
|
979
998
|
if ((args.goal === undefined) === (args.goalFile === undefined))
|
|
@@ -998,3 +1017,59 @@ function parseRunArgs(argv) {
|
|
|
998
1017
|
}
|
|
999
1018
|
return args;
|
|
1000
1019
|
}
|
|
1020
|
+
function parseFollowupArgs(argv) {
|
|
1021
|
+
const args = { command: "followup", profiles: [], dryRun: false, json: false, yes: false, rerun: false };
|
|
1022
|
+
if (argv.includes("--help") || argv.includes("-h"))
|
|
1023
|
+
return { ...args, command: "help", helpTopic: "followup", json: argv.includes("--json") };
|
|
1024
|
+
const actions = ["list", "show", "import", "sync", "clarify", "accept", "defer", "reopen", "process"];
|
|
1025
|
+
const action = argv[0] === undefined || argv[0].startsWith("-") ? "list" : argv[0];
|
|
1026
|
+
if (!actions.includes(action))
|
|
1027
|
+
invalidValue("followup action", action);
|
|
1028
|
+
args.action = action;
|
|
1029
|
+
const seen = new Set();
|
|
1030
|
+
for (let i = argv[0] === action ? 1 : 0; i < argv.length; i++) {
|
|
1031
|
+
const token = argv[i];
|
|
1032
|
+
if (!token.startsWith("-")) {
|
|
1033
|
+
if (args.followupId !== undefined)
|
|
1034
|
+
throw new CliArgumentError("CLI_UNEXPECTED_ARGUMENT", "Select exactly one followup identity.");
|
|
1035
|
+
if (!/^followup-[a-f0-9]{64}$/u.test(token))
|
|
1036
|
+
invalidValue("followup identity", token);
|
|
1037
|
+
args.followupId = token;
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
if (seen.has(token))
|
|
1041
|
+
duplicate(token);
|
|
1042
|
+
seen.add(token);
|
|
1043
|
+
if (token === "--json") {
|
|
1044
|
+
args.json = true;
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
if (!["--reason", "--finding-file", "--host", "--review-only-reason"].includes(token))
|
|
1048
|
+
throw new CliArgumentError("CLI_UNKNOWN_OPTION", `Unknown followup option: ${token}`, token);
|
|
1049
|
+
const value = readOptionValue(argv, i++, token);
|
|
1050
|
+
if (value.trim() === "" || value.includes("\0"))
|
|
1051
|
+
invalidValue(token, value);
|
|
1052
|
+
if (token === "--reason")
|
|
1053
|
+
args.reason = value;
|
|
1054
|
+
if (token === "--finding-file")
|
|
1055
|
+
args.findingFile = value;
|
|
1056
|
+
if (token === "--review-only-reason")
|
|
1057
|
+
args.reviewOnlyReason = value;
|
|
1058
|
+
if (token === "--host") {
|
|
1059
|
+
if (value !== "claude" && value !== "codex")
|
|
1060
|
+
invalidValue(token, value);
|
|
1061
|
+
args.host = value;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
const selectsItem = !["list", "import", "sync"].includes(action);
|
|
1065
|
+
if (selectsItem !== (args.followupId !== undefined))
|
|
1066
|
+
throw new CliArgumentError("CLI_MISSING_VALUE", "This action requires exactly one followup identity; list/import/sync take none.");
|
|
1067
|
+
if ((args.reason !== undefined && !["accept", "defer", "reopen", "clarify"].includes(action)) ||
|
|
1068
|
+
(args.findingFile !== undefined && action !== "clarify") ||
|
|
1069
|
+
((args.host !== undefined || args.reviewOnlyReason !== undefined) && action !== "process"))
|
|
1070
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Option does not apply to this followup action.");
|
|
1071
|
+
if ((["accept", "defer", "clarify"].includes(action) && args.reason === undefined) ||
|
|
1072
|
+
(action === "clarify" && args.findingFile === undefined) || (action === "process" && args.host === undefined))
|
|
1073
|
+
throw new CliArgumentError("CLI_MISSING_VALUE", "Accept/defer/clarify need --reason; clarify needs --finding-file; process needs --host.");
|
|
1074
|
+
return args;
|
|
1075
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { reviewCreatedCriteria, criteriaReviewStatus } from "./criteria-review.js";
|
|
3
|
+
import { renderCriteriaReview } from "../../../runtime/src/review/criteria.js";
|
|
2
4
|
import { advancePhaseObserver, productionRunContext } from "./run-deps.js";
|
|
3
5
|
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
4
6
|
import { recordRunVerification } from "../../../runtime/src/run/verification.js";
|
|
5
7
|
import { runOwnedLocalProof } from "./owned-run-step.js";
|
|
6
8
|
import { runRunCommand } from "./commands/run.js";
|
|
9
|
+
import { runFollowupCommand } from "./commands/followup.js";
|
|
10
|
+
import { reconcileRepositoryFollowups } from "./followup-deps.js";
|
|
7
11
|
import { runOfficeCommand } from "./commands/office.js";
|
|
8
12
|
import { ensureBackgroundOffice, observeOfficeSession, officePhaseReporter, openOfficeBrowser, serveOffice } from "./office-entry.js";
|
|
9
13
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
@@ -257,8 +261,13 @@ else {
|
|
|
257
261
|
const root = args.scope === "user"
|
|
258
262
|
? process.env.AGENT_OPS_HOME ?? homedir()
|
|
259
263
|
: process.cwd();
|
|
264
|
+
if (args.scope !== "user" && !["init", "update", "uninstall", "followup", "escapes", "office"].includes(args.command)) {
|
|
265
|
+
const collected = await reconcileRepositoryFollowups(root);
|
|
266
|
+
for (const warning of collected.warnings)
|
|
267
|
+
process.stderr.write(`Warning: ${warning}\n`);
|
|
268
|
+
}
|
|
260
269
|
// Committed work is judged as a range by default; see defaultBase.
|
|
261
|
-
if ((args.command === "verify" || (args.command === "review" && args.action !== "show")) && args.base === undefined && args.scope !== "user") {
|
|
270
|
+
if ((args.command === "verify" || (args.command === "review" && args.action !== "show" && !args.criteriaReview)) && args.base === undefined && args.scope !== "user") {
|
|
262
271
|
const taskId = args.taskId;
|
|
263
272
|
const base = await defaultBase(root, gitRunner(root), async () => taskId === undefined ? undefined :
|
|
264
273
|
(await new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root)).status({ taskId })).createdSourceCommit)
|
|
@@ -277,6 +286,8 @@ else {
|
|
|
277
286
|
const context = await productionRunContext(root);
|
|
278
287
|
return await runRunCommand({ args, ...context, root: context.mainRoot, office: async () => await ensureBackgroundOffice(root, undefined, openOfficeBrowser) });
|
|
279
288
|
}
|
|
289
|
+
if (args.command === "followup")
|
|
290
|
+
return await runFollowupCommand({ args, cwd: root });
|
|
280
291
|
if (args.command === "escapes")
|
|
281
292
|
return await runEscapesCommand({ root });
|
|
282
293
|
if (args.command === "office") {
|
|
@@ -507,6 +518,8 @@ else {
|
|
|
507
518
|
return await runTaskCommand({
|
|
508
519
|
args,
|
|
509
520
|
service: taskService,
|
|
521
|
+
onTaskCreated: async (record, createdRoot) => await reviewCreatedCriteria(createdRoot ?? root, record.task),
|
|
522
|
+
criteriaStatus: async (record) => await criteriaReviewStatus(root, record.task),
|
|
510
523
|
...(policyConfigHash === undefined ? {} : { policyConfigHash }),
|
|
511
524
|
...(sessionId === undefined ? {} : { sessionId }),
|
|
512
525
|
...(fromMain
|
|
@@ -527,6 +540,11 @@ else {
|
|
|
527
540
|
: {})
|
|
528
541
|
});
|
|
529
542
|
}
|
|
543
|
+
if (args.command === "review" && args.criteriaReview) {
|
|
544
|
+
const record = await taskService.status({ taskId: args.taskId });
|
|
545
|
+
const criteriaReview = await reviewCreatedCriteria(root, record.task, args.rerun);
|
|
546
|
+
return { status: "ok", code: "CRITERIA_REVIEW_ADVISORY", data: { criteriaReview, text: renderCriteriaReview(criteriaReview) }, errors: [] };
|
|
547
|
+
}
|
|
530
548
|
const probeTarget = async (target, budget) => await probeReviewTargetDetailed(target, {
|
|
531
549
|
cwd: root,
|
|
532
550
|
deep: true,
|
|
@@ -31,6 +31,7 @@ Commands:
|
|
|
31
31
|
review Run an independent review or show recorded reports
|
|
32
32
|
run Start or control a supervised Claude/Codex native goal
|
|
33
33
|
office Open the local pixel office (Preview; opt-in)
|
|
34
|
+
followup Collect and process retained review findings and risk observations
|
|
34
35
|
escapes Report escaped bugs, gate catches and learned rules (read-only)
|
|
35
36
|
batch Verify and review a parent task's subtasks together
|
|
36
37
|
allow-stop Grant one fingerprint-bound completion-gate Stop permit (requires --session)
|
|
@@ -59,6 +60,7 @@ Options:
|
|
|
59
60
|
--width <n> Batch only: reviews allowed at once (default 2)
|
|
60
61
|
--target-version <version> Update target version (offline-capable)
|
|
61
62
|
--title <text>
|
|
63
|
+
--criteria Review only: one pre-edit advisory, no time budget
|
|
62
64
|
--criterion <json> Repeatable
|
|
63
65
|
--evidence <criterion-id=reference> Repeatable
|
|
64
66
|
--session <id>
|
|
@@ -76,6 +78,30 @@ Options:
|
|
|
76
78
|
* rejected.
|
|
77
79
|
*/
|
|
78
80
|
export const COMMAND_HELP_TEXT = {
|
|
81
|
+
followup: `Usage: agent-ops followup [list] [--json]
|
|
82
|
+
agent-ops followup <show|reopen> <followup-id> [--json]
|
|
83
|
+
agent-ops followup <import|sync> [--json]
|
|
84
|
+
agent-ops followup <accept|defer> <followup-id> --reason <text> [--json]
|
|
85
|
+
agent-ops followup clarify <followup-id> --finding-file <json> --reason <text> [--json]
|
|
86
|
+
agent-ops followup process <followup-id> --host <claude|codex> [--review-only-reason <text>] [--json]
|
|
87
|
+
|
|
88
|
+
Receipt collection and history import are metadata-only: no model calls or repairs.
|
|
89
|
+
State is private under the Git common directory, shared by linked worktrees.
|
|
90
|
+
Text observations require a concrete nonblocking ReviewFinding JSON clarification
|
|
91
|
+
(severity, blocking=false, title, details, locations, evidence, recommendation,
|
|
92
|
+
criterionIds). Accept/defer require a reason; neither resolves the finding.
|
|
93
|
+
Reopen retains history. Process again to revalidate a selected candidate.
|
|
94
|
+
Process authorizes independent current-source assessment and a new bounded native
|
|
95
|
+
repair only when conclusively PRESENT. UNKNOWN stays unresolved. Native start
|
|
96
|
+
refusals retain the item and exact reason; macOS, clean main, checked-out target,
|
|
97
|
+
trust, run policy and native controls still apply. Run through the trusted outer
|
|
98
|
+
host as: env -u CODEX_SANDBOX_NETWORK_DISABLED agent-ops followup process ...
|
|
99
|
+
ABSENT closure requires an explicit justified review-only contract and two fresh
|
|
100
|
+
reviews. Reproducible bugs require corresponding behavioral red-to-green proof.
|
|
101
|
+
Closure records the exact commit and finding-specific evidence; generic PASS,
|
|
102
|
+
changed paths, completed run status, acceptance and deferral cannot resolve.
|
|
103
|
+
Collection warnings are advisory and retryable with followup import.
|
|
104
|
+
`,
|
|
79
105
|
escapes: `Usage: agent-ops escapes [--json]
|
|
80
106
|
|
|
81
107
|
Read-only Escape Loop report: misses per gate from the tracked
|
|
@@ -102,6 +128,7 @@ The URL carries an access token; do not share it.
|
|
|
102
128
|
agent-ops run [start] --goal-file <path> --host <claude|codex>
|
|
103
129
|
agent-ops run <status|logs|resume|stop> <run-id> [--json]
|
|
104
130
|
agent-ops run resume <run-id> [--retry-approval <auth-event-id>]... [--json]
|
|
131
|
+
agent-ops run resume <run-id> --retry-delivery <task-id> [--json]
|
|
105
132
|
agent-ops run respond <run-id> --question-id <id> <--answer <text>|--answer-file <path>> [--json]
|
|
106
133
|
|
|
107
134
|
New runs have no total time cap. Manual resume upgrades legacy finite runs and keeps accounting.
|
|
@@ -110,6 +137,9 @@ Native goal state is observational. Completion requires current verification,
|
|
|
110
137
|
two fresh reviews, task completion and a target-bound integration receipt.
|
|
111
138
|
--retry-approval explicitly retries an intact Codex approval-review usage-limit
|
|
112
139
|
failure. Original denials remain recorded; every command needs fresh approval.
|
|
140
|
+
--retry-delivery requeues one stopped child blocked by interrupted handoff proof.
|
|
141
|
+
Inspect and repair the harness cause first. History is retained; fresh native
|
|
142
|
+
generation, verification and delivery are required. It grants no command approval.
|
|
113
143
|
Status includes phase, original cause, recorded sealed proof, cleanup state,
|
|
114
144
|
resumability and remediation. Resume revalidates authority and sealed artifacts;
|
|
115
145
|
receipt-bound recovery executes only missing cleanup steps. Resolve the saved
|
|
@@ -252,11 +282,14 @@ Options:
|
|
|
252
282
|
--json
|
|
253
283
|
`,
|
|
254
284
|
review: `Usage: agent-ops review --task <id> --yes [options]
|
|
285
|
+
agent-ops review --criteria --task <id> --yes [--rerun]
|
|
255
286
|
agent-ops review show --task <id> [--json]
|
|
256
287
|
|
|
257
288
|
Run the complete task-bound review: one necessary reviewer followed by one
|
|
258
289
|
fresh adversarial reviewer from the configured pair. Show reads recorded
|
|
259
290
|
reports without running a reviewer, including from the main checkout.
|
|
291
|
+
Criteria mode runs/reads one pre-edit advisory, with no time/token budget;
|
|
292
|
+
only --rerun starts another attempt. It never replaces final proof.
|
|
260
293
|
|
|
261
294
|
Options:
|
|
262
295
|
--task <id> Required task whose original criteria are reviewed
|
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
|
|
4
4
|
import { finishWorktree } from "../../../../runtime/src/parallel/finish.js";
|
|
5
|
+
import { followupFinishText } from "./parallel.js";
|
|
5
6
|
import { integrateSessionChildren } from "../../../../runtime/src/parallel/integrate.js";
|
|
6
7
|
import { readWorktreeRecord, resolveCheckouts, sessionWorktreeName, worktreePath } from "../../../../runtime/src/parallel/service.js";
|
|
7
8
|
import { resolveReviewScope } from "../../../../runtime/src/review/scope.js";
|
|
@@ -93,7 +94,7 @@ export async function runAdvanceCommand(options) {
|
|
|
93
94
|
if (savedProof !== null && (await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) === savedProof.head ||
|
|
94
95
|
(await deps.git(mainRoot, ["merge-base", "--is-ancestor", savedProof.head, record.targetBranch])).exitCode === 0)) {
|
|
95
96
|
const finished = await finishWorktree(deps, { cwd: mainRoot, name, finalProof: { ...savedProof, recovery: true } });
|
|
96
|
-
return okEnvelope("TASK_ADVANCED", { ...finished, text:
|
|
97
|
+
return okEnvelope("TASK_ADVANCED", { ...finished, text: `Recovered the sealed integration without moving the target again.\n${followupFinishText(finished)}${finished.warnings.join("\n")}` });
|
|
97
98
|
}
|
|
98
99
|
if (savedProof !== null && await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) !== savedProof.target)
|
|
99
100
|
await abandonPreparedRunIntegration(commonDir, record, deps.git);
|
|
@@ -176,7 +177,7 @@ export async function runAdvanceCommand(options) {
|
|
|
176
177
|
reviewMode,
|
|
177
178
|
sourceFingerprint,
|
|
178
179
|
...finished,
|
|
179
|
-
text: `Final candidate ${head} passed all task verifiers and ${reviewMode} review, then finished into ${record.targetBranch}. Receipt: ${finished.receipt}.\n${highlights}Full reports: agent-ops review show --task ${parent.task.id}`
|
|
180
|
+
text: `Final candidate ${head} passed all task verifiers and ${reviewMode} review, then finished into ${record.targetBranch}. Receipt: ${finished.receipt}.\n${followupFinishText(finished)}${finished.warnings.map(w => `Warning: ${w}\n`).join("")}${highlights}Full reports: agent-ops review show --task ${parent.task.id}`
|
|
180
181
|
});
|
|
181
182
|
}
|
|
182
183
|
catch (failure) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFile, lstat } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
|
|
4
|
+
import { reconcileFollowups } from "../../../../runtime/src/followup/intake.js";
|
|
5
|
+
import { readFollowupProgress } from "../../../../runtime/src/followup/process.js";
|
|
6
|
+
import { FileFollowupStore, clarifyFollowup, setFollowupDisposition } from "../../../../runtime/src/followup/store.js";
|
|
7
|
+
import { readFollowupAssessment } from "../../../../runtime/src/review/followup.js";
|
|
8
|
+
import { redactSecrets } from "../../../../runtime/src/security/redact.js";
|
|
9
|
+
import { safeTaskText } from "../../../../runtime/src/task/render.js";
|
|
10
|
+
import { productionFollowupContext, followupRepositoryContext } from "../followup-deps.js";
|
|
11
|
+
import { errorEnvelope, okEnvelope } from "../output.js";
|
|
12
|
+
/** Redact strings individually so multiline credentials cannot hide behind JSON escapes. */
|
|
13
|
+
function display(value) {
|
|
14
|
+
if (typeof value === "string")
|
|
15
|
+
return safeTaskText(redactSecrets(value));
|
|
16
|
+
if (Array.isArray(value))
|
|
17
|
+
return value.map(display);
|
|
18
|
+
if (value !== null && typeof value === "object")
|
|
19
|
+
return Object.fromEntries(Object.entries(value).map(([key, field]) => [key, display(field)]));
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
export async function runFollowupCommand(options) {
|
|
23
|
+
const { args, cwd } = options;
|
|
24
|
+
const source = await followupRepositoryContext(cwd);
|
|
25
|
+
const collection = await reconcileFollowups(source);
|
|
26
|
+
const store = new FileFollowupStore(source.commonDir);
|
|
27
|
+
const action = args.action ?? "list";
|
|
28
|
+
const warnings = collection.warnings.map(w => `Warning: ${w}`);
|
|
29
|
+
if (action === "import" || action === "sync")
|
|
30
|
+
return okEnvelope("FOLLOWUP_IMPORTED", { collection,
|
|
31
|
+
text: [`Followup: collected ${collection.candidates} candidate(s), ${collection.observations} observation(s).`, ...warnings].join("\n") });
|
|
32
|
+
if (action === "list") {
|
|
33
|
+
const items = await Promise.all((await store.read()).items.map(async (item) => {
|
|
34
|
+
const progress = await readFollowupProgress(source.commonDir, item.id, item);
|
|
35
|
+
return { id: item.id, kind: item.kind, disposition: item.disposition, status: progress.status, code: progress.code,
|
|
36
|
+
title: typeof item.original === "string" ? item.original : item.original.title };
|
|
37
|
+
}));
|
|
38
|
+
const safe = display(items);
|
|
39
|
+
return okEnvelope("FOLLOWUP_LIST", { items: safe, collection,
|
|
40
|
+
text: [safe.length === 0 ? "No followup items." : safe.map(i => `${i.id} ${i.kind} ${i.disposition} ${i.status} (${i.code})\n ${i.title}`).join("\n"), ...warnings].join("\n") });
|
|
41
|
+
}
|
|
42
|
+
const id = args.followupId;
|
|
43
|
+
if (action === "clarify") {
|
|
44
|
+
const path = resolve(cwd, args.findingFile);
|
|
45
|
+
if (!(await lstat(path)).isFile() || (await lstat(path)).size > 128 * 1024)
|
|
46
|
+
throw new AgentOpsError("FOLLOWUP_CLARIFICATION_INVALID", "Finding JSON must be a regular file no larger than 128 KiB.");
|
|
47
|
+
let finding;
|
|
48
|
+
try {
|
|
49
|
+
finding = JSON.parse(await readFile(path, "utf8"));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new AgentOpsError("FOLLOWUP_CLARIFICATION_INVALID", "Finding file must contain concrete ReviewFinding JSON.");
|
|
53
|
+
}
|
|
54
|
+
await clarifyFollowup(store, id, finding, args.reason);
|
|
55
|
+
}
|
|
56
|
+
else if (action === "accept" || action === "defer" || action === "reopen") {
|
|
57
|
+
await setFollowupDisposition(store, id, action, args.reason);
|
|
58
|
+
}
|
|
59
|
+
else if (action === "process") {
|
|
60
|
+
const progress = await (await productionFollowupContext(cwd)).process(id, args.host, args.reviewOnlyReason);
|
|
61
|
+
// A persisted refusal remains an error to scripts, with the durable state available to show.
|
|
62
|
+
if (progress.attempts.at(-1)?.code === progress.code)
|
|
63
|
+
return { ...errorEnvelope(progress.code, `Repair start refused: ${progress.code}. Item retained; inspect with followup show ${id}.`),
|
|
64
|
+
data: display({ progress, collection }) };
|
|
65
|
+
}
|
|
66
|
+
const item = await store.get(id), progress = await readFollowupProgress(source.commonDir, id, item);
|
|
67
|
+
const assessment = progress.assessment === undefined ? undefined : await readFollowupAssessment(source.commonDir, progress.assessment);
|
|
68
|
+
const safe = display({ item, progress, ...(assessment === undefined ? {} : {
|
|
69
|
+
assessment: { conclusion: assessment.conclusion, reason: assessment.reason, binding: assessment.binding }
|
|
70
|
+
}) });
|
|
71
|
+
return okEnvelope("FOLLOWUP_DETAIL", { ...safe, collection,
|
|
72
|
+
text: [`${id}: ${safe.item.kind}, ${safe.item.disposition}, ${progress.status} (${progress.code})`,
|
|
73
|
+
JSON.stringify(safe, null, 2), ...warnings].join("\n") });
|
|
74
|
+
}
|
|
@@ -38,6 +38,9 @@ function addText(result) {
|
|
|
38
38
|
"(Claude Code: ExitWorktree with action keep before finishing; do not run task complete in the worktree)."
|
|
39
39
|
].join("\n");
|
|
40
40
|
}
|
|
41
|
+
export function followupFinishText(result) {
|
|
42
|
+
return result.followup === undefined ? "" : `Followup: collected ${result.followup.candidates} candidate(s), ${result.followup.observations} observation(s). Inspect with agent-ops followup list.\n`;
|
|
43
|
+
}
|
|
41
44
|
function finishText(result) {
|
|
42
45
|
const { record } = result;
|
|
43
46
|
return [
|
|
@@ -45,6 +48,7 @@ function finishText(result) {
|
|
|
45
48
|
`Completed and recorded in git notes (refs/notes/agent-ops): ${result.taskIds.join(", ")}.`,
|
|
46
49
|
`Removed ${record.path}.`,
|
|
47
50
|
...(result.receipt === undefined ? [] : [`Receipt: ${result.receipt}`]),
|
|
51
|
+
...followupFinishText(result).trimEnd().split("\n").filter(Boolean),
|
|
48
52
|
...(result.residualRisks === undefined || result.residualRisks.length === 0 ? [] :
|
|
49
53
|
["Residual risks from review:", ...result.residualRisks.map((risk) => `- ${safeTaskText(redactSecrets(risk))}`)]),
|
|
50
54
|
...result.warnings.map((warning) => `Warning: ${warning}`)
|
|
@@ -108,7 +108,7 @@ export async function runRunCommand(options) {
|
|
|
108
108
|
return okEnvelope("RUN_LOGS", { ...stateData(action, state, `Read ${events.length} event(s) for run ${runId}.`), events });
|
|
109
109
|
}
|
|
110
110
|
if (action === "resume")
|
|
111
|
-
return okEnvelope("RUN_RESUMED", data(action, await options.service.resume(runId, options.args.retryApprovalIds)));
|
|
111
|
+
return okEnvelope("RUN_RESUMED", data(action, await options.service.resume(runId, options.args.retryApprovalIds, true, options.args.retryDeliveryTaskId)));
|
|
112
112
|
if (action === "stop")
|
|
113
113
|
return okEnvelope("RUN_STOP_REQUESTED", data(action, await options.service.stop(runId, options.args.answer ?? "user requested stop")));
|
|
114
114
|
const questionId = options.args.questionId;
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { renderCriteriaReview } from "../../../../runtime/src/review/criteria.js";
|
|
1
2
|
import { lstat, readFile } from "node:fs/promises";
|
|
2
3
|
import { taskContractHash } from "../../../../runtime/src/task/contract.js";
|
|
3
4
|
import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
|
|
4
5
|
import { renderTaskMarkdown, safeTaskText } from "../../../../runtime/src/task/render.js";
|
|
5
6
|
import { errorEnvelope, okEnvelope } from "../output.js";
|
|
7
|
+
async function withCriteriaStatus(envelope, options) {
|
|
8
|
+
const item = envelope.data?.record;
|
|
9
|
+
const advice = item === undefined ? null : await options.criteriaStatus?.(item).catch(() => null);
|
|
10
|
+
return advice == null || envelope.data === null ? envelope : { ...envelope, data: { ...envelope.data, criteriaReview: advice,
|
|
11
|
+
text: envelope.data.text + "\n\n" + renderCriteriaReview(advice) } };
|
|
12
|
+
}
|
|
6
13
|
function isRecord(value) {
|
|
7
14
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
15
|
}
|
|
@@ -154,11 +161,15 @@ export async function runTaskCommand(options) {
|
|
|
154
161
|
requireSurface: true
|
|
155
162
|
});
|
|
156
163
|
const escape = origin === undefined ? undefined : await (worktree?.service ?? options.service).declareEscape(record.task.id);
|
|
157
|
-
const
|
|
164
|
+
const initialAdvice = await options.onTaskCreated?.(record, worktree?.path);
|
|
165
|
+
let created = await withEscape(taskEnvelope(action, "TASK_CREATED", sessionId === undefined
|
|
158
166
|
? `Created task ${record.task.id}.`
|
|
159
167
|
: record.task.parentTaskId === undefined
|
|
160
168
|
? `Created and attached task ${record.task.id}.`
|
|
161
169
|
: `Created subtask ${record.task.id}; the session stays on the top of its task tree.`, record), escape, worktree?.service ?? options.service);
|
|
170
|
+
if (initialAdvice !== undefined && created.data !== null)
|
|
171
|
+
created = { ...created, data: { ...created.data, criteriaReview: initialAdvice,
|
|
172
|
+
text: created.data.text + "\n\n" + renderCriteriaReview(initialAdvice) } };
|
|
162
173
|
if (worktree === undefined || created.data === null)
|
|
163
174
|
return created;
|
|
164
175
|
return {
|
|
@@ -191,7 +202,7 @@ export async function runTaskCommand(options) {
|
|
|
191
202
|
...(origin === undefined ? {} : { origin }),
|
|
192
203
|
reason: options.args.reason, ...(options.args.baseline === undefined ? {} : { baseline: options.args.baseline })
|
|
193
204
|
});
|
|
194
|
-
return await withEscape(taskEnvelope(action, "TASK_REVISED", "Contract revised; obtain a fresh review, and new verification if the criteria or configuration changed.", record), origin === undefined ? undefined : await options.service.declareEscape(record.task.id), options.service);
|
|
205
|
+
return await withCriteriaStatus(await withEscape(taskEnvelope(action, "TASK_REVISED", "Contract revised; obtain a fresh review, and new verification if the criteria or configuration changed.", record), origin === undefined ? undefined : await options.service.declareEscape(record.task.id), options.service), options);
|
|
195
206
|
}
|
|
196
207
|
if (action === "replan") {
|
|
197
208
|
const payload = await jsonFile(options.args.planFile);
|
|
@@ -201,7 +212,13 @@ export async function runTaskCommand(options) {
|
|
|
201
212
|
expectedTreeContractHash: options.args.expectedTreeContract, reason: options.args.reason,
|
|
202
213
|
tasks: payload
|
|
203
214
|
});
|
|
204
|
-
|
|
215
|
+
const advice = [];
|
|
216
|
+
for (const record of records) {
|
|
217
|
+
const result = await options.onTaskCreated?.(record);
|
|
218
|
+
if (result !== undefined)
|
|
219
|
+
advice.push(renderCriteriaReview(result));
|
|
220
|
+
}
|
|
221
|
+
return okEnvelope("TASK_REPLANNED", { action, message: "Task tree split atomically; reverify and review.", records, text: [renderTaskList(records), ...advice].join("\n\n") });
|
|
205
222
|
}
|
|
206
223
|
if (action === "pin-finding") {
|
|
207
224
|
const criterion = parseCriterion(JSON.stringify(await jsonFile(options.args.criterionFiles[0])));
|
|
@@ -234,9 +251,9 @@ export async function runTaskCommand(options) {
|
|
|
234
251
|
const reviewOnly = coverage.rows.filter(row => row.mode === "review-only").length;
|
|
235
252
|
const legacy = coverage.rows.filter(row => row.mode === "legacy").length;
|
|
236
253
|
const envelope = taskEnvelope(action, "TASK_STATUS", `Read task ${record.task.id}.`, record);
|
|
237
|
-
return { ...envelope, data: { ...envelope.data, coverage, treeContractHash: await options.service.treeContract(record.task.id),
|
|
254
|
+
return await withCriteriaStatus({ ...envelope, data: { ...envelope.data, coverage, treeContractHash: await options.service.treeContract(record.task.id),
|
|
238
255
|
text: envelope.data.text + "\nAcceptance: " + proven + "/" + mechanical + " mechanical proven; " + reviewOnly + " review-only; " + legacy + " legacy; originally mechanical " + coverage.originalMechanical +
|
|
239
|
-
"; reviewed " + coverage.reviewed + "; undischarged " + coverage.rows.filter(row => row.status !== "proven" && !coverage.reviewed).length } };
|
|
256
|
+
"; reviewed " + coverage.reviewed + "; undischarged " + coverage.rows.filter(row => row.status !== "proven" && !coverage.reviewed).length } }, options);
|
|
240
257
|
}
|
|
241
258
|
if (action === "attach") {
|
|
242
259
|
if (sessionId === undefined) {
|
|
@@ -236,22 +236,26 @@ export async function runPolicyContext(root, renew = false) {
|
|
|
236
236
|
return null;
|
|
237
237
|
const here = realpathSync(root);
|
|
238
238
|
const worker = saved.workers.find(w => w.worktree === here);
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
const matchesProof = (registered) => {
|
|
240
|
+
if (registered == null || String(registered.processId) !== process.env.AGENT_OPS_RUN_PROOF_PID)
|
|
241
|
+
return false;
|
|
242
242
|
try {
|
|
243
|
-
|
|
244
|
-
execFileSync("ps", ["-o", "lstart=", "-p", String(
|
|
243
|
+
return execFileSync("ps", ["-o", "pgid=", "-p", String(process.pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim() === String(registered.processId) &&
|
|
244
|
+
execFileSync("ps", ["-o", "lstart=", "-p", String(registered.processId)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim() === registered.processIdentity;
|
|
245
245
|
}
|
|
246
246
|
catch {
|
|
247
|
-
|
|
247
|
+
return false;
|
|
248
248
|
}
|
|
249
|
-
}
|
|
249
|
+
};
|
|
250
|
+
const globalProof = matchesProof(saved.proofProcess);
|
|
251
|
+
const proof = globalProof || matchesProof(worker?.proofProcess);
|
|
252
|
+
// Reading another registered checkout does not change a run-level proof's coordinator actor.
|
|
253
|
+
const actor = globalProof || worker === undefined ? saved.workers.find(w => w.workerId === saved.coordinatorId) : worker;
|
|
250
254
|
if ((!proof && (worker === undefined || record?.runId !== runId || record.workerId !== worker.workerId ||
|
|
251
255
|
record.ownerSessionId !== worker.ownerSessionId || record.coordinatorId !== saved.coordinatorId || record.workerGeneration !== worker.generation)) ||
|
|
252
256
|
(worker === undefined && !(proof && saved.root === here)) ||
|
|
253
|
-
(process.env.AGENT_OPS_WORKER_ID !== undefined && (
|
|
254
|
-
process.env.
|
|
257
|
+
((globalProof || process.env.AGENT_OPS_WORKER_ID !== undefined) && (actor === undefined ||
|
|
258
|
+
process.env.AGENT_OPS_WORKER_ID !== actor.workerId || process.env.AGENT_OPS_WORKER_GENERATION !== String(actor.generation))))
|
|
255
259
|
throw new AgentOpsError("RUN_WORKER_STALE", "Run policy requires the current registered worker generation or proof process.");
|
|
256
260
|
if (saved.status !== "active" || saved.disableRestart || saved.awaitingResume)
|
|
257
261
|
throw new AgentOpsError("RUN_POLICY_DISABLED", "Run execution policy is inactive; explicit resume is required.");
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
4
|
+
import { runCriteriaReview, readCriteriaReview, renderCriteriaReview } from "../../../runtime/src/review/criteria.js";
|
|
5
|
+
import { registeredRunProofRunner } from "../../../runtime/src/verify/spawn.js";
|
|
6
|
+
import { appendLocalLog } from "../../../runtime/src/logging/local-log.js";
|
|
7
|
+
import { localStatePaths } from "../../../runtime/src/security/permissions.js";
|
|
8
|
+
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
9
|
+
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
10
|
+
import { loadEffectiveConfig, runPolicyContext } from "./context.js";
|
|
11
|
+
async function parentScope(root, task) {
|
|
12
|
+
const read = async (path, id) => (await new TaskService(new FileTaskStore(join(path, ".agent-ops/tasks/state.json"), path)).status({ taskId: id })).task;
|
|
13
|
+
if (task.parentTaskId !== undefined)
|
|
14
|
+
return await read(root, task.parentTaskId);
|
|
15
|
+
if (task.surface !== undefined && "covers" in task.surface) {
|
|
16
|
+
const run = await runPolicyContext(root);
|
|
17
|
+
const coordinator = run?.state.workers.find(worker => worker.workerId === run.state.coordinatorId);
|
|
18
|
+
if (run?.state.rootTaskId != null && coordinator?.worktree != null)
|
|
19
|
+
return await read(coordinator.worktree, run.state.rootTaskId);
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
export async function criteriaReviewStatus(root, task) {
|
|
24
|
+
return await readCriteriaReview(root, task, await parentScope(root, task).catch(() => undefined));
|
|
25
|
+
}
|
|
26
|
+
/** Only production creation/explicit review installs this hook; TaskService stays model-free. */
|
|
27
|
+
export async function reviewCreatedCriteria(root, task, rerun = false) {
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const interrupt = (signal) => { controller.abort(signal); };
|
|
30
|
+
const int = () => interrupt("SIGINT"), term = () => interrupt("SIGTERM");
|
|
31
|
+
process.once("SIGINT", int);
|
|
32
|
+
process.once("SIGTERM", term);
|
|
33
|
+
try {
|
|
34
|
+
const config = await loadEffectiveConfig(root, "project").catch(() => undefined);
|
|
35
|
+
const role = config?.config.reviewRoles?.find(role => role.role === "independent-review");
|
|
36
|
+
const host = process.env.AGENT_OPS_HOST;
|
|
37
|
+
const target = role?.targets.find(target => target !== host) ?? role?.targets[0];
|
|
38
|
+
const parent = await parentScope(root, task).catch(() => undefined);
|
|
39
|
+
process.stderr.write(`Criteria advisory for ${task.id}: waiting for one independent reviewer (no time budget; Ctrl-C cancels).\n`);
|
|
40
|
+
let result = await runCriteriaReview({ root, task, parent, target, model: role?.model, effort: role?.effort,
|
|
41
|
+
rerun, signal: controller.signal, runner: registeredRunProofRunner(),
|
|
42
|
+
unavailableCode: config === undefined ? "CRITERIA_REVIEW_CONFIG_UNAVAILABLE" : target === undefined && role !== undefined ? "CRITERIA_REVIEW_NO_INDEPENDENT_TARGET" : "CRITERIA_REVIEW_UNCONFIGURED",
|
|
43
|
+
log: async (report, path) => {
|
|
44
|
+
const state = localStatePaths(process.env.AGENT_OPS_HOME ?? homedir());
|
|
45
|
+
await appendLocalLog(join(state.logDirectory, "events.ndjson"), { type: "criteria-review", taskId: task.id,
|
|
46
|
+
attemptId: report.attemptId, status: report.status, code: report.code, durationMs: report.durationMs,
|
|
47
|
+
invocations: report.invocations, recommendations: report.advice?.recommendations.length ?? 0,
|
|
48
|
+
report: path, usage: { ...report.usage } }, { anchorDirectory: state.anchorDirectory });
|
|
49
|
+
} });
|
|
50
|
+
const latest = await new TaskService(new FileTaskStore(join(root, ".agent-ops/tasks/state.json"), root)).status({ taskId: task.id }).catch(() => undefined);
|
|
51
|
+
const currentView = latest === undefined ? null : await criteriaReviewStatus(root, latest.task);
|
|
52
|
+
if (currentView?.report.attemptId === result.report.attemptId)
|
|
53
|
+
result = { ...result, stale: currentView.stale };
|
|
54
|
+
if (controller.signal.aborted)
|
|
55
|
+
throw new AgentOpsError("CRITERIA_REVIEW_CANCELLED", "Criteria review cancelled; stop before implementation.");
|
|
56
|
+
process.stderr.write(renderCriteriaReview(result) + "\n");
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
process.removeListener("SIGINT", int);
|
|
61
|
+
process.removeListener("SIGTERM", term);
|
|
62
|
+
}
|
|
63
|
+
}
|