@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.
Files changed (93) hide show
  1. package/README.md +48 -0
  2. package/dist/packages/cli/src/args.js +165 -2
  3. package/dist/packages/cli/src/bin.js +64 -17
  4. package/dist/packages/cli/src/cli.js +33 -6
  5. package/dist/packages/cli/src/commands/advance.js +23 -3
  6. package/dist/packages/cli/src/commands/review-show.js +1 -1
  7. package/dist/packages/cli/src/commands/review.js +100 -9
  8. package/dist/packages/cli/src/commands/run.js +111 -0
  9. package/dist/packages/cli/src/commands/task.js +61 -17
  10. package/dist/packages/cli/src/commands/verify.js +1 -0
  11. package/dist/packages/cli/src/context.js +63 -1
  12. package/dist/packages/cli/src/hook-process.js +31 -1
  13. package/dist/packages/cli/src/owned-run-step.js +65 -0
  14. package/dist/packages/cli/src/parallel-deps.js +18 -5
  15. package/dist/packages/cli/src/run-deps.js +314 -0
  16. package/dist/packages/cli/src/run-entry.js +943 -0
  17. package/dist/packages/cli/src/run-setup-entry.js +32 -0
  18. package/dist/packages/cli/src/run-step-entry.js +58 -0
  19. package/dist/runtime/src/config/explain.js +6 -0
  20. package/dist/runtime/src/config/hash.js +1 -1
  21. package/dist/runtime/src/config/merge.js +9 -1
  22. package/dist/runtime/src/contracts.js +2 -2
  23. package/dist/runtime/src/hooks/completion-gate.js +8 -2
  24. package/dist/runtime/src/install/doctor.js +2 -0
  25. package/dist/runtime/src/parallel/finish.js +85 -22
  26. package/dist/runtime/src/parallel/guard.js +5 -1
  27. package/dist/runtime/src/parallel/integrate.js +288 -10
  28. package/dist/runtime/src/parallel/manage.js +4 -1
  29. package/dist/runtime/src/parallel/receipt.js +71 -5
  30. package/dist/runtime/src/parallel/service.js +41 -1
  31. package/dist/runtime/src/review/attestation.js +36 -11
  32. package/dist/runtime/src/review/execute.js +24 -1
  33. package/dist/runtime/src/review/packet.js +35 -1
  34. package/dist/runtime/src/review/runner.js +12 -1
  35. package/dist/runtime/src/review/scope.js +18 -1
  36. package/dist/runtime/src/review/slots.js +35 -10
  37. package/dist/runtime/src/run/authorization.js +60 -0
  38. package/dist/runtime/src/run/budget.js +30 -0
  39. package/dist/runtime/src/run/controls.js +139 -0
  40. package/dist/runtime/src/run/convergence.js +177 -0
  41. package/dist/runtime/src/run/hosts/claude.js +180 -0
  42. package/dist/runtime/src/run/hosts/codex.js +279 -0
  43. package/dist/runtime/src/run/hosts/index.js +4 -0
  44. package/dist/runtime/src/run/hosts/protocol.js +254 -0
  45. package/dist/runtime/src/run/hosts/types.js +1 -0
  46. package/dist/runtime/src/run/hosts/util.js +136 -0
  47. package/dist/runtime/src/run/integration.js +204 -0
  48. package/dist/runtime/src/run/macOS.js +189 -0
  49. package/dist/runtime/src/run/ownership.js +22 -0
  50. package/dist/runtime/src/run/planning.js +25 -0
  51. package/dist/runtime/src/run/policy-transition.js +1076 -0
  52. package/dist/runtime/src/run/policy.js +176 -0
  53. package/dist/runtime/src/run/ratchet.js +71 -0
  54. package/dist/runtime/src/run/scheduler.js +260 -0
  55. package/dist/runtime/src/run/service.js +425 -0
  56. package/dist/runtime/src/run/supervisor.js +635 -0
  57. package/dist/runtime/src/run/transport.js +159 -0
  58. package/dist/runtime/src/run/types.js +1 -0
  59. package/dist/runtime/src/run/usage.js +81 -0
  60. package/dist/runtime/src/run/verification.js +64 -0
  61. package/dist/runtime/src/run/worker-gate.js +48 -0
  62. package/dist/runtime/src/schema/acceptance.js +91 -0
  63. package/dist/runtime/src/schema/validate.js +67 -7
  64. package/dist/runtime/src/task/completion.js +14 -2
  65. package/dist/runtime/src/task/contract.js +17 -0
  66. package/dist/runtime/src/task/pin-finding.js +81 -0
  67. package/dist/runtime/src/task/service.js +316 -13
  68. package/dist/runtime/src/task/store.js +57 -7
  69. package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
  70. package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
  71. package/dist/runtime/src/verify/acceptance-replay.js +239 -0
  72. package/dist/runtime/src/verify/adapters/index.js +5 -0
  73. package/dist/runtime/src/verify/adapters/jest.js +142 -0
  74. package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
  75. package/dist/runtime/src/verify/adapters/node.js +254 -0
  76. package/dist/runtime/src/verify/adapters/pytest.js +188 -0
  77. package/dist/runtime/src/verify/adapters/rust.js +84 -0
  78. package/dist/runtime/src/verify/adapters/shared.js +82 -0
  79. package/dist/runtime/src/verify/adapters/vitest.js +218 -0
  80. package/dist/runtime/src/verify/evidence.js +5 -2
  81. package/dist/runtime/src/verify/service.js +24 -8
  82. package/dist/runtime/src/verify/source-fingerprint.js +1 -1
  83. package/dist/runtime/src/verify/spawn.js +13 -2
  84. package/docs/en/spec/acceptance-and-evidence.md +48 -0
  85. package/docs/en/spec/loop-engineering.md +49 -0
  86. package/docs/zh-TW/spec/acceptance-and-evidence.md +21 -1
  87. package/docs/zh-TW/spec/loop-engineering.md +22 -0
  88. package/package.json +1 -1
  89. package/schemas/config.schema.json +221 -19
  90. package/schemas/evidence.schema.json +129 -6
  91. package/schemas/task.schema.json +265 -6
  92. package/templates/acceptance/agent_ops_acceptance.rs +36 -0
  93. 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 { NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn.js";
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
- if (argv[0] === "agy-run") {
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: new NodeVerificationProcessRunner(),
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: new VerificationService({
637
- root,
638
- scope: args.scope === "user" ? "user" : "project",
639
- config,
640
- gitRunner: gitRunner(root),
641
- processRunner: new NodeVerificationProcessRunner(),
642
- taskService,
643
- evidenceStore: new FileEvidenceStore(root, root),
644
- trusted: trustStatus === "TRUSTED",
645
- ...(args.base === undefined ? {} : { base: args.base })
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|advance> [options]
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 is one JSON object with exactly these keys:
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
- Every criterion needs at least one verifierIds entry naming a verification
166
- command id configured in .agent-ops/config.json. No other key is accepted.
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: { target, head, sourceFingerprint, children }
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 !== 1 || artifact.sourceFingerprint !== fingerprint ||
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)) ||