@kylecheng3146/agent-ops 0.5.3 → 0.6.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/README.md +48 -0
- package/dist/packages/cli/src/args.js +165 -2
- package/dist/packages/cli/src/bin.js +64 -17
- package/dist/packages/cli/src/cli.js +33 -6
- package/dist/packages/cli/src/commands/advance.js +23 -3
- package/dist/packages/cli/src/commands/review-show.js +1 -1
- package/dist/packages/cli/src/commands/review.js +100 -9
- package/dist/packages/cli/src/commands/run.js +111 -0
- package/dist/packages/cli/src/commands/task.js +61 -17
- package/dist/packages/cli/src/commands/verify.js +1 -0
- package/dist/packages/cli/src/context.js +63 -1
- package/dist/packages/cli/src/hook-process.js +31 -1
- package/dist/packages/cli/src/owned-run-step.js +65 -0
- package/dist/packages/cli/src/parallel-deps.js +18 -5
- package/dist/packages/cli/src/run-deps.js +314 -0
- package/dist/packages/cli/src/run-entry.js +943 -0
- package/dist/packages/cli/src/run-setup-entry.js +32 -0
- package/dist/packages/cli/src/run-step-entry.js +58 -0
- package/dist/runtime/src/config/explain.js +6 -0
- package/dist/runtime/src/config/hash.js +1 -1
- package/dist/runtime/src/config/merge.js +9 -1
- package/dist/runtime/src/contracts.js +2 -2
- package/dist/runtime/src/hooks/completion-gate.js +8 -2
- package/dist/runtime/src/install/doctor.js +2 -0
- package/dist/runtime/src/parallel/finish.js +85 -22
- package/dist/runtime/src/parallel/guard.js +5 -1
- package/dist/runtime/src/parallel/integrate.js +288 -10
- package/dist/runtime/src/parallel/manage.js +4 -1
- package/dist/runtime/src/parallel/receipt.js +71 -5
- package/dist/runtime/src/parallel/service.js +41 -1
- package/dist/runtime/src/review/attestation.js +36 -11
- package/dist/runtime/src/review/execute.js +24 -1
- package/dist/runtime/src/review/packet.js +35 -1
- package/dist/runtime/src/review/runner.js +12 -1
- package/dist/runtime/src/review/scope.js +18 -1
- package/dist/runtime/src/review/slots.js +35 -10
- package/dist/runtime/src/run/authorization.js +60 -0
- package/dist/runtime/src/run/budget.js +30 -0
- package/dist/runtime/src/run/controls.js +139 -0
- package/dist/runtime/src/run/convergence.js +177 -0
- package/dist/runtime/src/run/hosts/claude.js +180 -0
- package/dist/runtime/src/run/hosts/codex.js +279 -0
- package/dist/runtime/src/run/hosts/index.js +4 -0
- package/dist/runtime/src/run/hosts/protocol.js +254 -0
- package/dist/runtime/src/run/hosts/types.js +1 -0
- package/dist/runtime/src/run/hosts/util.js +136 -0
- package/dist/runtime/src/run/integration.js +204 -0
- package/dist/runtime/src/run/macOS.js +189 -0
- package/dist/runtime/src/run/ownership.js +22 -0
- package/dist/runtime/src/run/planning.js +25 -0
- package/dist/runtime/src/run/policy-transition.js +1076 -0
- package/dist/runtime/src/run/policy.js +176 -0
- package/dist/runtime/src/run/ratchet.js +71 -0
- package/dist/runtime/src/run/scheduler.js +260 -0
- package/dist/runtime/src/run/service.js +425 -0
- package/dist/runtime/src/run/supervisor.js +635 -0
- package/dist/runtime/src/run/transport.js +159 -0
- package/dist/runtime/src/run/types.js +1 -0
- package/dist/runtime/src/run/usage.js +81 -0
- package/dist/runtime/src/run/verification.js +64 -0
- package/dist/runtime/src/run/worker-gate.js +48 -0
- package/dist/runtime/src/schema/acceptance.js +91 -0
- package/dist/runtime/src/schema/validate.js +67 -7
- package/dist/runtime/src/task/completion.js +14 -2
- package/dist/runtime/src/task/contract.js +17 -0
- package/dist/runtime/src/task/pin-finding.js +81 -0
- package/dist/runtime/src/task/service.js +316 -13
- package/dist/runtime/src/task/store.js +57 -7
- package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
- package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
- package/dist/runtime/src/verify/acceptance-replay.js +239 -0
- package/dist/runtime/src/verify/adapters/index.js +5 -0
- package/dist/runtime/src/verify/adapters/jest.js +142 -0
- package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
- package/dist/runtime/src/verify/adapters/node.js +254 -0
- package/dist/runtime/src/verify/adapters/pytest.js +188 -0
- package/dist/runtime/src/verify/adapters/rust.js +84 -0
- package/dist/runtime/src/verify/adapters/shared.js +82 -0
- package/dist/runtime/src/verify/adapters/vitest.js +218 -0
- package/dist/runtime/src/verify/evidence.js +5 -2
- package/dist/runtime/src/verify/service.js +24 -8
- package/dist/runtime/src/verify/source-fingerprint.js +1 -1
- package/dist/runtime/src/verify/spawn.js +13 -2
- package/docs/en/spec/acceptance-and-evidence.md +48 -0
- package/docs/en/spec/loop-engineering.md +49 -0
- package/docs/zh-TW/spec/acceptance-and-evidence.md +21 -1
- package/docs/zh-TW/spec/loop-engineering.md +22 -0
- package/package.json +1 -1
- package/schemas/config.schema.json +221 -19
- package/schemas/evidence.schema.json +129 -6
- package/schemas/task.schema.json +265 -6
- package/templates/acceptance/agent_ops_acceptance.rs +36 -0
- package/templates/acceptance/agent_ops_pytest.py +46 -0
package/README.md
CHANGED
|
@@ -12,6 +12,54 @@ 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
|
+
## Executable acceptance and native goal runs (0.6.0)
|
|
16
|
+
|
|
17
|
+
Criteria can use `behavioral`, `invariant` or `review-only` acceptance contracts.
|
|
18
|
+
Behavioral proof requires each designated red check to fail by assertion on its
|
|
19
|
+
immutable baseline and every required check to pass on the candidate. Invariants
|
|
20
|
+
can pass on both versions. Review-only requirements still need two fresh reviews
|
|
21
|
+
against the original goal and an explicit `reviewOnlyReason` in the acceptance
|
|
22
|
+
contract. Legacy criteria keep their existing verifier behavior.
|
|
23
|
+
|
|
24
|
+
Configure and trust `verification.acceptanceRunners` in the repository. Tasks bind
|
|
25
|
+
stable check IDs and explicit committed test/fixture/helper materials; they cannot
|
|
26
|
+
override runner executables or arguments. Each version installs and builds its own
|
|
27
|
+
declared dependencies. Fixture errors, skipped/retried checks, incomplete output and
|
|
28
|
+
timeouts produce UNKNOWN. Review examines the checks themselves and every fallback.
|
|
29
|
+
`task revise`, `task replan` and `task pin-finding` preserve goal and evidence history;
|
|
30
|
+
`task status` and `doctor` distinguish mechanical coverage from review-only coverage.
|
|
31
|
+
|
|
32
|
+
On macOS, a trusted clean main checkout can start an isolated native goal run:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
agent-ops run "Implement the requested behavior" --host codex --time-budget 60m --jobs 2
|
|
36
|
+
agent-ops run status <run-id>
|
|
37
|
+
agent-ops run logs <run-id>
|
|
38
|
+
agent-ops run respond <run-id> --question-id <id> --answer "Clarification"
|
|
39
|
+
agent-ops run stop <run-id>
|
|
40
|
+
agent-ops run resume <run-id>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`--host claude` uses Claude Code; `--wait` waits for a terminal or input state.
|
|
44
|
+
The coordinator counts toward the two writer slots. Native processes write only
|
|
45
|
+
under current leases in separate worktrees. Active time counts once across parallel
|
|
46
|
+
workers and continues through proof and integration; resume retains earlier usage
|
|
47
|
+
epochs. Missing token or cost counters remain unknown. Completion requires current
|
|
48
|
+
verification, two fresh reviews, completed tasks and a receipt bound to the target.
|
|
49
|
+
Setup runs in the background under registered process groups. Stop cancels native
|
|
50
|
+
writers, local verification and setup; interrupted setup requires explicit resume.
|
|
51
|
+
The coordinator can assess additional non-dangerous fixed commands through a
|
|
52
|
+
policy request. This authorization is bound to the run, runtime, lease and budget;
|
|
53
|
+
it creates no permanent repository trust. Policy transitions fence writers and
|
|
54
|
+
journal delivery imports and contract synchronization before starting new leases.
|
|
55
|
+
Native authorization decisions retain redacted command scope and rationale;
|
|
56
|
+
missing provider decisions remain unknown. Repair compares actual check outcomes
|
|
57
|
+
and outstanding finding pins rather than source fingerprints.
|
|
58
|
+
|
|
59
|
+
Native lifecycle reachability and framework probe conditions are recorded in
|
|
60
|
+
[the capability probe](docs/harness/native-goal-probe.md); these observations do not
|
|
61
|
+
establish external adoption or a reduction in review cost.
|
|
62
|
+
|
|
15
63
|
## Quick start from npm
|
|
16
64
|
|
|
17
65
|
Requires Node.js `>=22.14.0`. Install the published CLI globally:
|
|
@@ -11,7 +11,8 @@ export const COMMAND_NAMES = [
|
|
|
11
11
|
"review",
|
|
12
12
|
"batch",
|
|
13
13
|
"allow-stop",
|
|
14
|
-
"worktree"
|
|
14
|
+
"worktree",
|
|
15
|
+
"run"
|
|
15
16
|
];
|
|
16
17
|
const COMMAND_SET = new Set(COMMAND_NAMES);
|
|
17
18
|
const SCOPES = new Set(["project", "user"]);
|
|
@@ -61,6 +62,8 @@ function parseHookTarget(option, value) {
|
|
|
61
62
|
return { harness: harness, surfaceId };
|
|
62
63
|
}
|
|
63
64
|
export function parseArgs(argv) {
|
|
65
|
+
if (argv[0] === "run")
|
|
66
|
+
return parseRunArgs(argv.slice(1));
|
|
64
67
|
let command;
|
|
65
68
|
let action;
|
|
66
69
|
let scope;
|
|
@@ -78,6 +81,14 @@ export function parseArgs(argv) {
|
|
|
78
81
|
const profiles = [];
|
|
79
82
|
const reviewTargets = [];
|
|
80
83
|
const criteria = [];
|
|
84
|
+
const criterionFiles = [];
|
|
85
|
+
let criteriaFile;
|
|
86
|
+
let planFile;
|
|
87
|
+
let expectedContract;
|
|
88
|
+
let expectedTreeContract;
|
|
89
|
+
let reason;
|
|
90
|
+
let baseline;
|
|
91
|
+
let findingReference;
|
|
81
92
|
const evidence = [];
|
|
82
93
|
let checkAuth = false;
|
|
83
94
|
const checkAuthTargets = [];
|
|
@@ -202,6 +213,39 @@ export function parseArgs(argv) {
|
|
|
202
213
|
tree = true;
|
|
203
214
|
break;
|
|
204
215
|
}
|
|
216
|
+
case "--criterion-file":
|
|
217
|
+
criterionFiles.push(readOptionValue(argv, index++, token));
|
|
218
|
+
break;
|
|
219
|
+
case "--criteria-file":
|
|
220
|
+
if (criteriaFile !== undefined)
|
|
221
|
+
duplicate(token);
|
|
222
|
+
criteriaFile = readOptionValue(argv, index++, token);
|
|
223
|
+
break;
|
|
224
|
+
case "--plan-file":
|
|
225
|
+
if (planFile !== undefined)
|
|
226
|
+
duplicate(token);
|
|
227
|
+
planFile = readOptionValue(argv, index++, token);
|
|
228
|
+
break;
|
|
229
|
+
case "--expected-contract":
|
|
230
|
+
if (expectedContract !== undefined)
|
|
231
|
+
duplicate(token);
|
|
232
|
+
expectedContract = readOptionValue(argv, index++, token);
|
|
233
|
+
break;
|
|
234
|
+
case "--expected-tree-contract":
|
|
235
|
+
if (expectedTreeContract !== undefined)
|
|
236
|
+
duplicate(token);
|
|
237
|
+
expectedTreeContract = readOptionValue(argv, index++, token);
|
|
238
|
+
break;
|
|
239
|
+
case "--reason":
|
|
240
|
+
if (reason !== undefined)
|
|
241
|
+
duplicate(token);
|
|
242
|
+
reason = readOptionValue(argv, index++, token);
|
|
243
|
+
break;
|
|
244
|
+
case "--baseline":
|
|
245
|
+
if (baseline !== undefined)
|
|
246
|
+
duplicate(token);
|
|
247
|
+
baseline = readOptionValue(argv, index++, token);
|
|
248
|
+
break;
|
|
205
249
|
case "--criterion": {
|
|
206
250
|
criteria.push(readOptionValue(argv, index, token));
|
|
207
251
|
index += 1;
|
|
@@ -386,7 +430,7 @@ export function parseArgs(argv) {
|
|
|
386
430
|
"complete",
|
|
387
431
|
"create",
|
|
388
432
|
"export",
|
|
389
|
-
"status"
|
|
433
|
+
"status", "revise", "replan", "pin-finding"
|
|
390
434
|
].includes(token)) {
|
|
391
435
|
action = token;
|
|
392
436
|
break;
|
|
@@ -409,6 +453,14 @@ export function parseArgs(argv) {
|
|
|
409
453
|
worktreeName = token;
|
|
410
454
|
break;
|
|
411
455
|
}
|
|
456
|
+
if (command === "task" && ["revise", "replan", "pin-finding"].includes(action ?? "") && taskId === undefined) {
|
|
457
|
+
taskId = token;
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
if (command === "task" && action === "pin-finding" && findingReference === undefined) {
|
|
461
|
+
findingReference = token;
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
412
464
|
if (command !== undefined) {
|
|
413
465
|
throw new CliArgumentError("CLI_UNEXPECTED_ARGUMENT", `Unexpected argument: ${token}`);
|
|
414
466
|
}
|
|
@@ -551,6 +603,20 @@ export function parseArgs(argv) {
|
|
|
551
603
|
if (worktree !== undefined && command !== "init" && command !== "update") {
|
|
552
604
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--worktree may be used only with init or update.");
|
|
553
605
|
}
|
|
606
|
+
const revisionAction = command === "task" && ["revise", "replan", "pin-finding"].includes(action ?? "");
|
|
607
|
+
if ((criterionFiles.length > 0 && !(command === "task" && (action === "create" || action === "pin-finding"))) ||
|
|
608
|
+
(criteriaFile !== undefined && !(command === "task" && action === "revise")) ||
|
|
609
|
+
(planFile !== undefined && !(command === "task" && action === "replan")) ||
|
|
610
|
+
(expectedContract !== undefined && !(command === "task" && (action === "revise" || action === "pin-finding"))) ||
|
|
611
|
+
(expectedTreeContract !== undefined && !(command === "task" && action === "replan")) ||
|
|
612
|
+
(reason !== undefined && !revisionAction) ||
|
|
613
|
+
(baseline !== undefined && !(command === "task" && (action === "create" || action === "revise"))))
|
|
614
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Acceptance options are not supported by this command.");
|
|
615
|
+
if (revisionAction && (taskId === undefined || evidence.length > 0 || title !== undefined || intent !== undefined || parentTaskId !== undefined || sessionId !== undefined || criteria.length > 0 || base !== undefined ||
|
|
616
|
+
(action === "revise" && (expectedContract === undefined || criteriaFile === undefined || reason === undefined)) ||
|
|
617
|
+
(action === "replan" && (expectedTreeContract === undefined || planFile === undefined || reason === undefined)) ||
|
|
618
|
+
(action === "pin-finding" && (expectedContract === undefined || findingReference === undefined || criterionFiles.length !== 1))))
|
|
619
|
+
throw new CliArgumentError("CLI_MISSING_VALUE", "Acceptance revision requires task, expected contract, payload and reason.");
|
|
554
620
|
if (command === "task") {
|
|
555
621
|
if (harness !== undefined ||
|
|
556
622
|
profiles.length > 0 ||
|
|
@@ -711,6 +777,14 @@ export function parseArgs(argv) {
|
|
|
711
777
|
...(reviewTargets.length === 0 ? {} : { reviewTargets }),
|
|
712
778
|
...(completionGate === undefined ? {} : { completionGate }),
|
|
713
779
|
...(criteria.length === 0 ? {} : { criteria }),
|
|
780
|
+
...(criterionFiles.length === 0 ? {} : { criterionFiles }),
|
|
781
|
+
...(criteriaFile === undefined ? {} : { criteriaFile }),
|
|
782
|
+
...(planFile === undefined ? {} : { planFile }),
|
|
783
|
+
...(expectedContract === undefined ? {} : { expectedContract }),
|
|
784
|
+
...(expectedTreeContract === undefined ? {} : { expectedTreeContract }),
|
|
785
|
+
...(reason === undefined ? {} : { reason }),
|
|
786
|
+
...(baseline === undefined ? {} : { baseline }),
|
|
787
|
+
...(findingReference === undefined ? {} : { findingReference }),
|
|
714
788
|
...(evidence.length === 0 ? {} : { evidence }),
|
|
715
789
|
...(sessionId === undefined ? {} : { sessionId }),
|
|
716
790
|
...(base === undefined ? {} : { base }),
|
|
@@ -730,3 +804,92 @@ export function parseArgs(argv) {
|
|
|
730
804
|
...(worktree === undefined ? {} : { worktree })
|
|
731
805
|
};
|
|
732
806
|
}
|
|
807
|
+
function parseRunArgs(argv) {
|
|
808
|
+
const actions = ["start", "status", "logs", "resume", "stop", "respond"];
|
|
809
|
+
if (argv.includes("--help") || argv.includes("-h"))
|
|
810
|
+
return { command: "help", helpTopic: "run", profiles: [], dryRun: false, json: false, yes: false, rerun: false };
|
|
811
|
+
const action = actions.includes(argv[0]) ? argv[0] : "start";
|
|
812
|
+
const args = { command: "run", action, runAction: action, profiles: [], dryRun: false, json: false, yes: false, rerun: false };
|
|
813
|
+
const seen = new Set();
|
|
814
|
+
let positional;
|
|
815
|
+
for (let i = actions.includes(argv[0]) ? 1 : 0; i < argv.length; i++) {
|
|
816
|
+
const token = argv[i];
|
|
817
|
+
if (!token.startsWith("-")) {
|
|
818
|
+
if (positional !== undefined)
|
|
819
|
+
throw new CliArgumentError("CLI_UNEXPECTED_ARGUMENT", "Quote the goal as one argument; control actions take one run ID.");
|
|
820
|
+
positional = token;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (seen.has(token))
|
|
824
|
+
duplicate(token);
|
|
825
|
+
seen.add(token);
|
|
826
|
+
if (token === "--json") {
|
|
827
|
+
args.json = true;
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
if (token === "--wait") {
|
|
831
|
+
args.wait = true;
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
if (!["--host", "--goal-file", "--time-budget", "--jobs", "--question-id", "--answer", "--answer-file"].includes(token))
|
|
835
|
+
throw new CliArgumentError("CLI_UNKNOWN_OPTION", `Unknown run option: ${token}`, token);
|
|
836
|
+
const value = readOptionValue(argv, i++, token);
|
|
837
|
+
switch (token) {
|
|
838
|
+
case "--host":
|
|
839
|
+
if (value !== "claude" && value !== "codex")
|
|
840
|
+
invalidValue(token, value);
|
|
841
|
+
args.host = value;
|
|
842
|
+
break;
|
|
843
|
+
case "--goal-file":
|
|
844
|
+
args.goalFile = value;
|
|
845
|
+
break;
|
|
846
|
+
case "--question-id":
|
|
847
|
+
args.questionId = value;
|
|
848
|
+
break;
|
|
849
|
+
case "--answer":
|
|
850
|
+
args.answer = value;
|
|
851
|
+
break;
|
|
852
|
+
case "--answer-file":
|
|
853
|
+
args.answerFile = value;
|
|
854
|
+
break;
|
|
855
|
+
case "--jobs":
|
|
856
|
+
if (value !== "1" && value !== "2")
|
|
857
|
+
invalidValue(token, value);
|
|
858
|
+
args.jobs = Number(value);
|
|
859
|
+
break;
|
|
860
|
+
case "--time-budget": {
|
|
861
|
+
const match = value.match(/^([1-9][0-9]*)(s|m|h)$/u);
|
|
862
|
+
if (match === null)
|
|
863
|
+
invalidValue(token, value);
|
|
864
|
+
const ms = Number(match[1]) * ({ s: 1000, m: 60000, h: 3600000 }[match[2]]);
|
|
865
|
+
if (!Number.isSafeInteger(ms) || ms > 86400000)
|
|
866
|
+
invalidValue(token, value);
|
|
867
|
+
args.timeBudgetMs = ms;
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (action === "start") {
|
|
873
|
+
args.goal = positional;
|
|
874
|
+
if ((args.goal === undefined) === (args.goalFile === undefined))
|
|
875
|
+
throw new CliArgumentError("RUN_GOAL_REQUIRED", "Provide exactly one quoted goal or --goal-file.");
|
|
876
|
+
if (args.host === undefined)
|
|
877
|
+
throw new CliArgumentError("RUN_HOST_REQUIRED", "Choose --host claude or --host codex.");
|
|
878
|
+
if (args.questionId !== undefined || args.answer !== undefined || args.answerFile !== undefined)
|
|
879
|
+
throw new CliArgumentError("CLI_INVALID_OPTION", "Answer options require run respond.");
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
if (positional === undefined || !/^[a-z][a-z0-9-]{7,63}$/u.test(positional))
|
|
883
|
+
throw new CliArgumentError("RUN_ID_REQUIRED", "Control actions require a valid run ID.");
|
|
884
|
+
args.runId = positional;
|
|
885
|
+
if (args.host !== undefined || args.goalFile !== undefined || args.jobs !== undefined || args.timeBudgetMs !== undefined || args.wait)
|
|
886
|
+
throw new CliArgumentError("CLI_INVALID_OPTION", "Start options are not accepted by run control actions.");
|
|
887
|
+
if (action === "respond") {
|
|
888
|
+
if (args.questionId === undefined || (args.answer === undefined) === (args.answerFile === undefined))
|
|
889
|
+
throw new CliArgumentError("RUN_ANSWER_REQUIRED", "Respond requires --question-id and exactly one --answer or --answer-file.");
|
|
890
|
+
}
|
|
891
|
+
else if (args.questionId !== undefined || args.answer !== undefined || args.answerFile !== undefined)
|
|
892
|
+
throw new CliArgumentError("CLI_INVALID_OPTION", "Answer options require run respond.");
|
|
893
|
+
}
|
|
894
|
+
return args;
|
|
895
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { productionRunContext } from "./run-deps.js";
|
|
3
|
+
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
4
|
+
import { recordRunVerification } from "../../../runtime/src/run/verification.js";
|
|
5
|
+
import { runOwnedLocalProof } from "./owned-run-step.js";
|
|
6
|
+
import { runRunCommand } from "./commands/run.js";
|
|
2
7
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
3
8
|
import { randomUUID } from "node:crypto";
|
|
4
9
|
import { homedir } from "node:os";
|
|
@@ -17,10 +22,10 @@ import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
|
|
|
17
22
|
import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
|
|
18
23
|
import { calculateSourceFingerprint } from "../../../runtime/src/verify/source-fingerprint.js";
|
|
19
24
|
import { VerificationService } from "../../../runtime/src/verify/service.js";
|
|
20
|
-
import {
|
|
25
|
+
import { registeredRunProofRunner } from "../../../runtime/src/verify/spawn.js";
|
|
21
26
|
import { COMMAND_NAMES, parseArgs } from "./args.js";
|
|
22
27
|
import { runCli } from "./cli.js";
|
|
23
|
-
import { loadEffectiveConfig, repositoryTrust, repositoryTrustBinding } from "./context.js";
|
|
28
|
+
import { loadEffectiveConfig, runPolicyContext, repositoryTrust, repositoryTrustBinding } from "./context.js";
|
|
24
29
|
import { runHookProcess } from "./hook-process.js";
|
|
25
30
|
import { gitRunner, trustStore, worktreeDependencies } from "./parallel-deps.js";
|
|
26
31
|
import { selectYesNo, writeBanner } from "./ui.js";
|
|
@@ -165,7 +170,22 @@ function runAgy(args, options = {}) {
|
|
|
165
170
|
...(process.platform === "win32" ? { shell: true } : {})
|
|
166
171
|
});
|
|
167
172
|
}
|
|
168
|
-
|
|
173
|
+
let ownedProofExit = null;
|
|
174
|
+
try {
|
|
175
|
+
ownedProofExit = await runOwnedLocalProof(argv);
|
|
176
|
+
}
|
|
177
|
+
catch (cause) {
|
|
178
|
+
const code = cause instanceof AgentOpsError ? cause.code : "RUN_LOCAL_PROOF_FAILED";
|
|
179
|
+
const message = cause instanceof AgentOpsError ? cause.message : "Local proof could not be registered safely.";
|
|
180
|
+
if (argv.includes("--json"))
|
|
181
|
+
process.stdout.write(JSON.stringify(errorEnvelope(code, message)) + "\n");
|
|
182
|
+
else
|
|
183
|
+
process.stderr.write(message + "\n");
|
|
184
|
+
ownedProofExit = 2;
|
|
185
|
+
}
|
|
186
|
+
if (ownedProofExit !== null)
|
|
187
|
+
process.exitCode = ownedProofExit;
|
|
188
|
+
else if (argv[0] === "agy-run") {
|
|
169
189
|
try {
|
|
170
190
|
const root = process.cwd();
|
|
171
191
|
const config = (await loadEffectiveConfig(root, "project")).config;
|
|
@@ -224,6 +244,13 @@ else {
|
|
|
224
244
|
const isTTY = !args.json &&
|
|
225
245
|
process.stdin.isTTY === true &&
|
|
226
246
|
process.stdout.isTTY === true;
|
|
247
|
+
if ((["init", "update"].includes(args.command) || (args.command === "trust" && args.action !== "status")) &&
|
|
248
|
+
(process.env.AGENT_OPS_RUN_ID !== undefined || await runPolicyContext(process.cwd()) !== null))
|
|
249
|
+
throw new AgentOpsError("RUN_POLICY_COORDINATOR_REQUIRED", "Run workers must request scoped policy review from the coordinator; permanent trust and host rules cannot be changed by the run.");
|
|
250
|
+
if (args.command === "run") {
|
|
251
|
+
const context = await productionRunContext(root);
|
|
252
|
+
return await runRunCommand({ args, ...context, root: context.mainRoot });
|
|
253
|
+
}
|
|
227
254
|
if (args.command === "init") {
|
|
228
255
|
const store = trustStore();
|
|
229
256
|
return await runInitCommand({
|
|
@@ -267,6 +294,17 @@ else {
|
|
|
267
294
|
root,
|
|
268
295
|
toolkitVersion: CLI_VERSION,
|
|
269
296
|
probes: {
|
|
297
|
+
acceptanceCoverage: async () => {
|
|
298
|
+
const service = new TaskService(new FileTaskStore(join(root, ".agent-ops/tasks/state.json"), root), { completion: { root, gitRunner: gitRunner(root), loadConfig: async () => config } });
|
|
299
|
+
const records = (await service.list()).filter(r => r.status !== "archived");
|
|
300
|
+
const rows = await Promise.all(records.map(r => service.coverage(r.task.id)));
|
|
301
|
+
const criteria = rows.flatMap(row => row.rows);
|
|
302
|
+
const mechanical = criteria.filter(c => c.mode === "behavioral" || c.mode === "invariant");
|
|
303
|
+
return { status: "PASS", message: criteria.length + " criteria; " +
|
|
304
|
+
criteria.filter(c => c.mode === "legacy").length + " legacy; " + mechanical.filter(c => c.status === "proven").length + "/" + mechanical.length +
|
|
305
|
+
" mechanical proven; " + criteria.filter(c => c.mode === "review-only").length + " review-only; originally mechanical " + rows.reduce((n, r) => n + r.originalMechanical, 0) +
|
|
306
|
+
"; undischarged " + rows.reduce((n, r) => n + r.rows.filter(c => c.status !== "proven" && !r.reviewed).length, 0) };
|
|
307
|
+
},
|
|
270
308
|
...(doctorManifest?.harness.includes("agy") === true
|
|
271
309
|
? {
|
|
272
310
|
agyRuntime: () => {
|
|
@@ -441,7 +479,8 @@ else {
|
|
|
441
479
|
return {
|
|
442
480
|
path: record.path,
|
|
443
481
|
base: record.base,
|
|
444
|
-
service: new TaskService(new FileTaskStore(join(record.path, ".agent-ops", "tasks", "state.json"), record.path))
|
|
482
|
+
service: new TaskService(new FileTaskStore(join(record.path, ".agent-ops", "tasks", "state.json"), record.path), { completion: { root: record.path, gitRunner: gitRunner(record.path),
|
|
483
|
+
loadConfig: async () => (await loadEffectiveConfig(record.path, "project")).config } })
|
|
445
484
|
};
|
|
446
485
|
}
|
|
447
486
|
}
|
|
@@ -501,6 +540,7 @@ else {
|
|
|
501
540
|
config: reviewConfig,
|
|
502
541
|
evidenceStore: new FileEvidenceStore(root, root),
|
|
503
542
|
execute: inSlot(createReviewExecutor({
|
|
543
|
+
runner: registeredRunProofRunner(),
|
|
504
544
|
targets: configuredReviewTargets,
|
|
505
545
|
cwd: root,
|
|
506
546
|
...(reviewRole?.model === undefined
|
|
@@ -513,10 +553,11 @@ else {
|
|
|
513
553
|
? {}
|
|
514
554
|
: { timeoutMs: reviewRole.timeoutMs }),
|
|
515
555
|
preflightTarget,
|
|
516
|
-
verifySourceFingerprint: async (expected) => {
|
|
556
|
+
verifySourceFingerprint: async (expected, scope) => {
|
|
517
557
|
const currentScope = await resolveReviewScope({
|
|
518
558
|
root,
|
|
519
559
|
runner: reviewGit,
|
|
560
|
+
...(scope?.mode === "base" && scope.noChange === true ? { noChangePaths: scope.changedFiles } : {}),
|
|
520
561
|
...(reviewArgs.base === undefined ? {} : { base: reviewArgs.base })
|
|
521
562
|
});
|
|
522
563
|
return await calculateSourceFingerprint(root, currentScope, reviewGit) === expected;
|
|
@@ -589,7 +630,7 @@ else {
|
|
|
589
630
|
scope: batchScope,
|
|
590
631
|
config: batchConfig,
|
|
591
632
|
gitRunner: batchGit,
|
|
592
|
-
processRunner:
|
|
633
|
+
processRunner: registeredRunProofRunner(),
|
|
593
634
|
taskService,
|
|
594
635
|
evidenceStore: new FileEvidenceStore(root, root),
|
|
595
636
|
trusted,
|
|
@@ -633,17 +674,23 @@ else {
|
|
|
633
674
|
return await runVerifyCommand({
|
|
634
675
|
args,
|
|
635
676
|
taskService,
|
|
636
|
-
service:
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
677
|
+
service: { verify: async (taskId) => {
|
|
678
|
+
const run = await runPolicyContext(root);
|
|
679
|
+
const report = await new VerificationService({
|
|
680
|
+
root,
|
|
681
|
+
scope: args.scope === "user" ? "user" : "project",
|
|
682
|
+
config,
|
|
683
|
+
gitRunner: gitRunner(root),
|
|
684
|
+
processRunner: registeredRunProofRunner(),
|
|
685
|
+
taskService,
|
|
686
|
+
evidenceStore: new FileEvidenceStore(root, root),
|
|
687
|
+
trusted: trustStatus === "TRUSTED",
|
|
688
|
+
...(args.base === undefined ? {} : { base: args.base })
|
|
689
|
+
}).verify(taskId);
|
|
690
|
+
if (run !== null)
|
|
691
|
+
await recordRunVerification(run.repository, run.state, run.worker?.workerId ?? run.state.coordinatorId, await taskService.status({ taskId }), report);
|
|
692
|
+
return report;
|
|
693
|
+
} }
|
|
647
694
|
});
|
|
648
695
|
}
|
|
649
696
|
if (args.command === "trust") {
|
|
@@ -25,10 +25,11 @@ Commands:
|
|
|
25
25
|
doctor Diagnose an installation
|
|
26
26
|
update Update managed artifacts
|
|
27
27
|
uninstall Remove managed artifacts
|
|
28
|
-
task <create|status|attach|complete|archive|export>
|
|
28
|
+
task <create|status|attach|revise|replan|pin-finding|advance|complete|archive|export>
|
|
29
29
|
Manage independent task acceptance state
|
|
30
30
|
verify Run configured verification
|
|
31
31
|
review Run an independent review or show recorded reports
|
|
32
|
+
run Start or control a supervised Claude/Codex native goal
|
|
32
33
|
batch Verify and review a parent task's subtasks together
|
|
33
34
|
allow-stop Grant one fingerprint-bound completion-gate Stop permit (requires --session)
|
|
34
35
|
agy-run Run headless agy with a process-exit completion recheck
|
|
@@ -71,6 +72,14 @@ Options:
|
|
|
71
72
|
* rejected.
|
|
72
73
|
*/
|
|
73
74
|
export const COMMAND_HELP_TEXT = {
|
|
75
|
+
run: `Usage: agent-ops run [start] "<goal>" --host <claude|codex> [--jobs <1|2>] [--time-budget <60m>] [--wait] [--json]
|
|
76
|
+
agent-ops run [start] --goal-file <path> --host <claude|codex>
|
|
77
|
+
agent-ops run <status|logs|resume|stop> <run-id> [--json]
|
|
78
|
+
agent-ops run respond <run-id> --question-id <id> <--answer <text>|--answer-file <path>> [--json]
|
|
79
|
+
|
|
80
|
+
Native goal state is observational. Completion requires current verification,
|
|
81
|
+
two fresh reviews, task completion and a target-bound integration receipt.
|
|
82
|
+
`,
|
|
74
83
|
init: `Usage: agent-ops init [options]
|
|
75
84
|
|
|
76
85
|
Plan or install agent-ops into this repository.
|
|
@@ -141,7 +150,7 @@ Options:
|
|
|
141
150
|
--json
|
|
142
151
|
--yes
|
|
143
152
|
`,
|
|
144
|
-
task: `Usage: agent-ops task <create|status|attach|complete|archive|export
|
|
153
|
+
task: `Usage: agent-ops task <create|status|attach|revise|replan|pin-finding|advance|complete|archive|export> [options]
|
|
145
154
|
|
|
146
155
|
Manage independent task acceptance state. Task commands accept none of
|
|
147
156
|
--harness, --profile or --dry-run. Only advance accepts --yes.
|
|
@@ -150,20 +159,38 @@ Options:
|
|
|
150
159
|
--title <text> create
|
|
151
160
|
--intent <text> create: intended behavior and constraints before editing
|
|
152
161
|
--criterion <json> create, repeatable, two to five total
|
|
162
|
+
--criterion-file <path> create: one criterion object, repeatable; pin-finding: exactly one
|
|
163
|
+
--criteria-file <path> revise: replacement criterion array
|
|
164
|
+
--plan-file <path> replan: split-task array with requirement mappings
|
|
165
|
+
--expected-contract <hash> revise, pin-finding: current task contract from status
|
|
166
|
+
--expected-tree-contract <hash> replan: current tree contract from status
|
|
167
|
+
--reason <text> required for revise/replan; optional for pin-finding
|
|
168
|
+
--baseline <git-ref> create/revise: baseline for new mechanical requirements
|
|
153
169
|
--parent <task-id> create: record a subtask; status: list subtasks
|
|
154
|
-
--task <id> status, attach, complete, archive, export, advance
|
|
170
|
+
--task <id> status, attach, revise, replan, pin-finding, complete, archive, export, advance
|
|
155
171
|
--session <id> create, attach, status, advance
|
|
156
172
|
--evidence <criterion-id=reference> complete, repeatable
|
|
157
173
|
--base <git-ref> complete: a clean committed range
|
|
158
174
|
--yes advance: authorize the two review sessions
|
|
159
175
|
--json
|
|
160
176
|
|
|
161
|
-
Each --criterion
|
|
177
|
+
Each --criterion or --criterion-file supplies one criterion object. Legacy example:
|
|
162
178
|
|
|
163
179
|
{"id":"kebab-id","description":"what must hold","verifierIds":["node-test"]}
|
|
164
180
|
|
|
165
|
-
|
|
166
|
-
command id configured in .agent-ops/config.json.
|
|
181
|
+
Each legacy criterion needs at least one verifierIds entry naming a verification
|
|
182
|
+
command id configured in .agent-ops/config.json. Typed criteria additionally
|
|
183
|
+
accept an acceptance object: mode (behavioral, invariant or review-only),
|
|
184
|
+
bindings and optional baselineCommit/reviewOnlyReason. Mechanical bindings name
|
|
185
|
+
configured acceptance runner IDs, checkIds, committed materials and optional
|
|
186
|
+
redCheckIds; tasks cannot supply commands. Creation freezes the baseline.
|
|
187
|
+
Review-only example (mandatory repository policy commands still run):
|
|
188
|
+
|
|
189
|
+
{"id":"judgment","description":"Meets the original goal","verifierIds":[],"acceptance":{"mode":"review-only","bindings":[],"reviewOnlyReason":"Requires independent judgment"}}
|
|
190
|
+
|
|
191
|
+
Pin a saved finding with task pin-finding --task <id> <report-digest:index>
|
|
192
|
+
--expected-contract <hash> --criterion-file <path>. Revise/replan preserve the
|
|
193
|
+
original goal and immutable existing baselines; obtain new verify and review.
|
|
167
194
|
`,
|
|
168
195
|
verify: `Usage: agent-ops verify [options]
|
|
169
196
|
|
|
@@ -9,6 +9,7 @@ import { renderReviewDetails, renderReviewHighlights } from "../../../../runtime
|
|
|
9
9
|
import { calculateSourceFingerprint } from "../../../../runtime/src/verify/source-fingerprint.js";
|
|
10
10
|
import { okEnvelope } from "../output.js";
|
|
11
11
|
import { readFinishedReview } from "./review-show.js";
|
|
12
|
+
import { prepareRunIntegration, readRunIntegrationProof, abandonPreparedRunIntegration } from "../../../../runtime/src/run/integration.js";
|
|
12
13
|
/** The child process runs the existing CLI in the candidate checkout. */
|
|
13
14
|
export async function runAdvanceStep(cwd, args) {
|
|
14
15
|
const bin = process.argv[1];
|
|
@@ -87,6 +88,13 @@ export async function runAdvanceCommand(options) {
|
|
|
87
88
|
if (record === null || record.sessionId !== options.sessionId || record.agentId !== undefined) {
|
|
88
89
|
throw new AgentOpsError("ADVANCE_WORKTREE_MISSING", `No coordinator worktree for session ${options.sessionId}.`);
|
|
89
90
|
}
|
|
91
|
+
const savedProof = await readRunIntegrationProof(commonDir, record);
|
|
92
|
+
if (savedProof !== null && await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) === savedProof.head) {
|
|
93
|
+
const finished = await finishWorktree(deps, { cwd: mainRoot, name, finalProof: { ...savedProof, recovery: true } });
|
|
94
|
+
return okEnvelope("TASK_ADVANCED", { ...finished, text: "Recovered the sealed integration without moving the target again." });
|
|
95
|
+
}
|
|
96
|
+
if (savedProof !== null && await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) !== savedProof.target)
|
|
97
|
+
await abandonPreparedRunIntegration(commonDir, record, deps.git);
|
|
90
98
|
const parent = await deps.tasks(record.path).status({ taskId: options.parentTaskId });
|
|
91
99
|
if (parent.status !== "active" || parent.task.parentTaskId !== undefined) {
|
|
92
100
|
throw new AgentOpsError("ADVANCE_PARENT_INVALID", "Advance requires an active root task in the coordinator worktree.");
|
|
@@ -120,16 +128,26 @@ export async function runAdvanceCommand(options) {
|
|
|
120
128
|
throw new AgentOpsError("ADVANCE_TASK_TREE_INVALID", "The candidate has unrelated or already completed tasks; final proof must cover one active tree.");
|
|
121
129
|
}
|
|
122
130
|
const ordered = all.filter(({ task }) => selected.has(task.id));
|
|
131
|
+
const noChangePaths = head === target ? parent.noChangePaths : undefined;
|
|
132
|
+
if (noChangePaths !== undefined)
|
|
133
|
+
for (const task of ordered)
|
|
134
|
+
await deps.tasks(record.path).recordEvidence(task.task.id, {}, undefined, noChangePaths);
|
|
123
135
|
for (const task of ordered) {
|
|
124
136
|
requirePass(await step(record.path, ["verify", "--task", task.task.id, "--base", target]), `Verify ${task.task.id}`);
|
|
125
137
|
}
|
|
126
138
|
const reviewMode = await reviewFinalTree(step, record.path, parent.task.id, ordered.map(({ task }) => task.id), target);
|
|
127
|
-
const scope = await resolveReviewScope({ root: record.path, runner: runner(deps, record.path), base: target
|
|
139
|
+
const scope = await resolveReviewScope({ root: record.path, runner: runner(deps, record.path), base: target,
|
|
140
|
+
...(noChangePaths === undefined ? {} : { noChangePaths }) });
|
|
128
141
|
const sourceFingerprint = await calculateSourceFingerprint(record.path, scope, runner(deps, record.path));
|
|
142
|
+
const finalProof = { target, head, sourceFingerprint, children,
|
|
143
|
+
...(noChangePaths === undefined ? {} : { noChange: { sourceCommit: head,
|
|
144
|
+
contractDigest: await deps.tasks(record.path).treeContract(parent.task.id), reviewScope: scope,
|
|
145
|
+
artifactRefs: Object.values((await deps.tasks(record.path).status({ taskId: parent.task.id })).evidence).flat() } }) };
|
|
146
|
+
await prepareRunIntegration(commonDir, record, finalProof);
|
|
129
147
|
try {
|
|
130
148
|
const finished = await finishWorktree(deps, {
|
|
131
149
|
cwd: mainRoot, name,
|
|
132
|
-
finalProof
|
|
150
|
+
finalProof
|
|
133
151
|
});
|
|
134
152
|
if (finished.receipt === undefined)
|
|
135
153
|
throw new AgentOpsError("WORKTREE_RECEIPT_MISSING", "Finish returned without a final review receipt.");
|
|
@@ -144,8 +162,10 @@ export async function runAdvanceCommand(options) {
|
|
|
144
162
|
});
|
|
145
163
|
}
|
|
146
164
|
catch (failure) {
|
|
147
|
-
if (failure instanceof AgentOpsError && failure.code === "WORKTREE_TARGET_MOVED" && attempt === 0)
|
|
165
|
+
if (failure instanceof AgentOpsError && failure.code === "WORKTREE_TARGET_MOVED" && attempt === 0) {
|
|
166
|
+
await abandonPreparedRunIntegration(commonDir, record, deps.git);
|
|
148
167
|
continue;
|
|
168
|
+
}
|
|
149
169
|
throw failure;
|
|
150
170
|
}
|
|
151
171
|
}
|
|
@@ -18,7 +18,7 @@ function checkedArtifact(value, fingerprint, taskId) {
|
|
|
18
18
|
if (typeof value !== "object" || value === null)
|
|
19
19
|
invalid("Review artifact is unreadable.");
|
|
20
20
|
const artifact = value;
|
|
21
|
-
if (artifact.schemaVersion
|
|
21
|
+
if (![1, 2].includes(artifact.schemaVersion) || artifact.sourceFingerprint !== fingerprint ||
|
|
22
22
|
artifact.taskId !== taskId || !["PASS", "FAIL", "NOT_RUN"].includes(artifact.status) ||
|
|
23
23
|
!Array.isArray(artifact.attempts) ||
|
|
24
24
|
(artifact.report !== undefined && !validReport(artifact.report, artifact)) ||
|