@kylecheng3146/agent-ops 0.5.3 → 0.7.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 (124) hide show
  1. package/README.md +103 -1
  2. package/dist/packages/cli/src/args.js +183 -3
  3. package/dist/packages/cli/src/bin.js +76 -19
  4. package/dist/packages/cli/src/cli.js +53 -8
  5. package/dist/packages/cli/src/codex-loop-process.js +19 -2
  6. package/dist/packages/cli/src/commands/advance.js +42 -5
  7. package/dist/packages/cli/src/commands/init.js +2 -0
  8. package/dist/packages/cli/src/commands/office.js +20 -0
  9. package/dist/packages/cli/src/commands/review-show.js +1 -1
  10. package/dist/packages/cli/src/commands/review.js +100 -9
  11. package/dist/packages/cli/src/commands/run.js +115 -0
  12. package/dist/packages/cli/src/commands/task.js +61 -17
  13. package/dist/packages/cli/src/commands/update.js +8 -0
  14. package/dist/packages/cli/src/commands/verify.js +1 -0
  15. package/dist/packages/cli/src/context.js +64 -1
  16. package/dist/packages/cli/src/hook-entry.js +2 -1
  17. package/dist/packages/cli/src/hook-process.js +54 -3
  18. package/dist/packages/cli/src/loop-entry.js +2 -1
  19. package/dist/packages/cli/src/office-entry.js +230 -0
  20. package/dist/packages/cli/src/owned-run-step.js +65 -0
  21. package/dist/packages/cli/src/parallel-deps.js +18 -5
  22. package/dist/packages/cli/src/run-deps.js +321 -0
  23. package/dist/packages/cli/src/run-entry.js +952 -0
  24. package/dist/packages/cli/src/run-setup-entry.js +32 -0
  25. package/dist/packages/cli/src/run-step-entry.js +58 -0
  26. package/dist/packages/cli/src/wizard.js +14 -2
  27. package/dist/runtime/src/adapters/agy/config.js +4 -3
  28. package/dist/runtime/src/adapters/agy/events.js +9 -0
  29. package/dist/runtime/src/adapters/claude/config.js +3 -3
  30. package/dist/runtime/src/adapters/claude/events.js +8 -0
  31. package/dist/runtime/src/adapters/codex/config.js +3 -3
  32. package/dist/runtime/src/adapters/codex/events.js +8 -0
  33. package/dist/runtime/src/adapters/opencode/config.js +5 -4
  34. package/dist/runtime/src/adapters/opencode/events.js +8 -0
  35. package/dist/runtime/src/config/explain.js +11 -0
  36. package/dist/runtime/src/config/hash.js +1 -1
  37. package/dist/runtime/src/config/merge.js +9 -1
  38. package/dist/runtime/src/contracts.js +2 -2
  39. package/dist/runtime/src/hooks/codex-loop.js +4 -0
  40. package/dist/runtime/src/hooks/completion-gate.js +8 -2
  41. package/dist/runtime/src/install/doctor.js +2 -0
  42. package/dist/runtime/src/install/harness.js +44 -1
  43. package/dist/runtime/src/install/plan.js +16 -11
  44. package/dist/runtime/src/install/preauth.js +44 -18
  45. package/dist/runtime/src/install/profiles.js +8 -2
  46. package/dist/runtime/src/install/update.js +1 -0
  47. package/dist/runtime/src/office/collect.js +233 -0
  48. package/dist/runtime/src/office/page.js +873 -0
  49. package/dist/runtime/src/office/scene.js +168 -0
  50. package/dist/runtime/src/office/server.js +139 -0
  51. package/dist/runtime/src/office/sessions.js +115 -0
  52. package/dist/runtime/src/office/snapshot.js +107 -0
  53. package/dist/runtime/src/parallel/finish.js +85 -22
  54. package/dist/runtime/src/parallel/guard.js +12 -2
  55. package/dist/runtime/src/parallel/integrate.js +288 -10
  56. package/dist/runtime/src/parallel/manage.js +4 -1
  57. package/dist/runtime/src/parallel/receipt.js +71 -5
  58. package/dist/runtime/src/parallel/service.js +41 -1
  59. package/dist/runtime/src/review/attestation.js +36 -11
  60. package/dist/runtime/src/review/execute.js +24 -1
  61. package/dist/runtime/src/review/packet.js +35 -1
  62. package/dist/runtime/src/review/runner.js +12 -1
  63. package/dist/runtime/src/review/scope.js +18 -1
  64. package/dist/runtime/src/review/slots.js +35 -10
  65. package/dist/runtime/src/run/authorization.js +60 -0
  66. package/dist/runtime/src/run/budget.js +30 -0
  67. package/dist/runtime/src/run/controls.js +139 -0
  68. package/dist/runtime/src/run/convergence.js +177 -0
  69. package/dist/runtime/src/run/hosts/claude.js +180 -0
  70. package/dist/runtime/src/run/hosts/codex.js +279 -0
  71. package/dist/runtime/src/run/hosts/index.js +4 -0
  72. package/dist/runtime/src/run/hosts/protocol.js +254 -0
  73. package/dist/runtime/src/run/hosts/types.js +1 -0
  74. package/dist/runtime/src/run/hosts/util.js +136 -0
  75. package/dist/runtime/src/run/integration.js +204 -0
  76. package/dist/runtime/src/run/macOS.js +189 -0
  77. package/dist/runtime/src/run/ownership.js +22 -0
  78. package/dist/runtime/src/run/phase.js +37 -0
  79. package/dist/runtime/src/run/planning.js +25 -0
  80. package/dist/runtime/src/run/policy-transition.js +1076 -0
  81. package/dist/runtime/src/run/policy.js +176 -0
  82. package/dist/runtime/src/run/ratchet.js +71 -0
  83. package/dist/runtime/src/run/scheduler.js +260 -0
  84. package/dist/runtime/src/run/service.js +433 -0
  85. package/dist/runtime/src/run/supervisor.js +635 -0
  86. package/dist/runtime/src/run/transport.js +159 -0
  87. package/dist/runtime/src/run/types.js +1 -0
  88. package/dist/runtime/src/run/usage.js +81 -0
  89. package/dist/runtime/src/run/verification.js +64 -0
  90. package/dist/runtime/src/run/worker-gate.js +48 -0
  91. package/dist/runtime/src/schema/acceptance.js +91 -0
  92. package/dist/runtime/src/schema/validate.js +80 -9
  93. package/dist/runtime/src/task/completion.js +14 -2
  94. package/dist/runtime/src/task/contract.js +17 -0
  95. package/dist/runtime/src/task/pin-finding.js +81 -0
  96. package/dist/runtime/src/task/service.js +316 -13
  97. package/dist/runtime/src/task/store.js +57 -7
  98. package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
  99. package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
  100. package/dist/runtime/src/verify/acceptance-replay.js +239 -0
  101. package/dist/runtime/src/verify/adapters/index.js +5 -0
  102. package/dist/runtime/src/verify/adapters/jest.js +142 -0
  103. package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
  104. package/dist/runtime/src/verify/adapters/node.js +254 -0
  105. package/dist/runtime/src/verify/adapters/pytest.js +188 -0
  106. package/dist/runtime/src/verify/adapters/rust.js +84 -0
  107. package/dist/runtime/src/verify/adapters/shared.js +82 -0
  108. package/dist/runtime/src/verify/adapters/vitest.js +218 -0
  109. package/dist/runtime/src/verify/evidence.js +5 -2
  110. package/dist/runtime/src/verify/service.js +24 -8
  111. package/dist/runtime/src/verify/source-fingerprint.js +1 -1
  112. package/dist/runtime/src/verify/spawn.js +13 -2
  113. package/docs/en/spec/acceptance-and-evidence.md +48 -0
  114. package/docs/en/spec/harness-adapters.md +113 -1
  115. package/docs/en/spec/loop-engineering.md +49 -0
  116. package/docs/zh-TW/spec/acceptance-and-evidence.md +21 -1
  117. package/docs/zh-TW/spec/harness-adapters.md +93 -1
  118. package/docs/zh-TW/spec/loop-engineering.md +22 -0
  119. package/package.json +1 -1
  120. package/schemas/config.schema.json +230 -19
  121. package/schemas/evidence.schema.json +129 -6
  122. package/schemas/task.schema.json +265 -6
  123. package/templates/acceptance/agent_ops_acceptance.rs +36 -0
  124. package/templates/acceptance/agent_ops_pytest.py +46 -0
@@ -25,10 +25,12 @@ 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
33
+ office Open the local pixel office (Preview; opt-in)
32
34
  batch Verify and review a parent task's subtasks together
33
35
  allow-stop Grant one fingerprint-bound completion-gate Stop permit (requires --session)
34
36
  agy-run Run headless agy with a process-exit completion recheck
@@ -39,9 +41,10 @@ Options:
39
41
  --scope <project|user>
40
42
  --harness <all|both|agy|claude|codex|opencode|comma-separated> Init/update/uninstall; review uses configured targets
41
43
  --hook-target <harness=surface-id> Repeatable advanced init/update option
42
- --profile <core|advisory|guardrails|loop> Repeatable
44
+ --profile <core|advisory|guardrails|loop|run> Repeatable
43
45
  --review-target <codex|agy|claude> Repeatable init option; review pair order
44
46
  --completion-gate Init only: enable the project-loop completion gate
47
+ --office <on|off> Init/update: enable or disable Office (Preview)
45
48
  --worktree <auto|off> Init/update: configure session worktree isolation
46
49
  --check-auth Doctor only: probe selected review targets'
47
50
  authentication with one real call
@@ -71,6 +74,28 @@ Options:
71
74
  * rejected.
72
75
  */
73
76
  export const COMMAND_HELP_TEXT = {
77
+ office: `Usage: agent-ops office [--json]
78
+
79
+ Office (Preview) is opt-in: select it during init/update or run
80
+ agent-ops update --office on. Disable with --office off. A missing choice is off.
81
+
82
+ Print the URL of this repository's office: a read-only pixel page on
83
+ 127.0.0.1. All run teams and ordinary sessions appear in a single-screen room
84
+ overview; walk the Supervisor with arrows/WASD or click a room for details.
85
+ Switch Chinese/English in the header. Movement and animation run locally.
86
+ When enabled, managed sessions start the shared server automatically. A new Office opens the
87
+ browser once; later sessions reuse it. It exits after 10 minutes without active
88
+ runs or sessions. Recently completed rooms remain for at most 2 hours.
89
+ The URL carries an access token; do not share it.
90
+ `,
91
+ run: `Usage: agent-ops run [start] "<goal>" --host <claude|codex> [--jobs <1|2>] [--time-budget <60m>] [--wait] [--json]
92
+ agent-ops run [start] --goal-file <path> --host <claude|codex>
93
+ agent-ops run <status|logs|resume|stop> <run-id> [--json]
94
+ agent-ops run respond <run-id> --question-id <id> <--answer <text>|--answer-file <path>> [--json]
95
+
96
+ Native goal state is observational. Completion requires current verification,
97
+ two fresh reviews, task completion and a target-bound integration receipt.
98
+ `,
74
99
  init: `Usage: agent-ops init [options]
75
100
 
76
101
  Plan or install agent-ops into this repository.
@@ -79,9 +104,10 @@ Options:
79
104
  --scope <project|user>
80
105
  --harness <all|both|agy|claude|codex|opencode|comma-separated>
81
106
  --hook-target <harness=surface-id> Repeatable
82
- --profile <core|advisory|guardrails|loop> Repeatable
107
+ --profile <core|advisory|guardrails|loop|run> Repeatable
83
108
  --review-target <codex|agy|claude> Repeatable, in fallback-chain order
84
109
  --completion-gate Enable the project-loop completion gate
110
+ --office <on|off> Office (Preview); default off, update preserves choice
85
111
  --worktree <auto|off> Configure session worktree isolation
86
112
  --dry-run Print the plan without writing
87
113
  --json
@@ -125,6 +151,7 @@ Options:
125
151
  --scope <project|user>
126
152
  --harness <all|both|agy|claude|codex|opencode|comma-separated>
127
153
  --target-version <version> Offline-capable update target
154
+ --office <on|off> Office (Preview); preserve choice unless supplied
128
155
  --worktree <auto|off> Configure session worktree isolation
129
156
  --dry-run
130
157
  --json
@@ -141,7 +168,7 @@ Options:
141
168
  --json
142
169
  --yes
143
170
  `,
144
- task: `Usage: agent-ops task <create|status|attach|complete|archive|export|advance> [options]
171
+ task: `Usage: agent-ops task <create|status|attach|revise|replan|pin-finding|advance|complete|archive|export> [options]
145
172
 
146
173
  Manage independent task acceptance state. Task commands accept none of
147
174
  --harness, --profile or --dry-run. Only advance accepts --yes.
@@ -150,20 +177,38 @@ Options:
150
177
  --title <text> create
151
178
  --intent <text> create: intended behavior and constraints before editing
152
179
  --criterion <json> create, repeatable, two to five total
180
+ --criterion-file <path> create: one criterion object, repeatable; pin-finding: exactly one
181
+ --criteria-file <path> revise: replacement criterion array
182
+ --plan-file <path> replan: split-task array with requirement mappings
183
+ --expected-contract <hash> revise, pin-finding: current task contract from status
184
+ --expected-tree-contract <hash> replan: current tree contract from status
185
+ --reason <text> required for revise/replan; optional for pin-finding
186
+ --baseline <git-ref> create/revise: baseline for new mechanical requirements
153
187
  --parent <task-id> create: record a subtask; status: list subtasks
154
- --task <id> status, attach, complete, archive, export, advance
188
+ --task <id> status, attach, revise, replan, pin-finding, complete, archive, export, advance
155
189
  --session <id> create, attach, status, advance
156
190
  --evidence <criterion-id=reference> complete, repeatable
157
191
  --base <git-ref> complete: a clean committed range
158
192
  --yes advance: authorize the two review sessions
159
193
  --json
160
194
 
161
- Each --criterion is one JSON object with exactly these keys:
195
+ Each --criterion or --criterion-file supplies one criterion object. Legacy example:
162
196
 
163
197
  {"id":"kebab-id","description":"what must hold","verifierIds":["node-test"]}
164
198
 
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.
199
+ Each legacy criterion needs at least one verifierIds entry naming a verification
200
+ command id configured in .agent-ops/config.json. Typed criteria additionally
201
+ accept an acceptance object: mode (behavioral, invariant or review-only),
202
+ bindings and optional baselineCommit/reviewOnlyReason. Mechanical bindings name
203
+ configured acceptance runner IDs, checkIds, committed materials and optional
204
+ redCheckIds; tasks cannot supply commands. Creation freezes the baseline.
205
+ Review-only example (mandatory repository policy commands still run):
206
+
207
+ {"id":"judgment","description":"Meets the original goal","verifierIds":[],"acceptance":{"mode":"review-only","bindings":[],"reviewOnlyReason":"Requires independent judgment"}}
208
+
209
+ Pin a saved finding with task pin-finding --task <id> <report-digest:index>
210
+ --expected-contract <hash> --criterion-file <path>. Revise/replan preserve the
211
+ original goal and immutable existing baselines; obtain new verify and review.
167
212
  `,
168
213
  verify: `Usage: agent-ops verify [options]
169
214
 
@@ -1,4 +1,5 @@
1
- import { PROJECT_LOOP_EVENTS, runProjectLoop } from "../../../runtime/src/hooks/codex-loop.js";
1
+ import { PROJECT_LOOP_EVENTS, resolveProjectLoopRoot, runProjectLoop } from "../../../runtime/src/hooks/codex-loop.js";
2
+ import { officeEnabled, officeLoopPhaseHint } from "./office-entry.js";
2
3
  const MAX_LOOP_INPUT_BYTES = 64 * 1024;
3
4
  function isLoopHarness(value) {
4
5
  return value === "claude" || value === "codex";
@@ -43,10 +44,26 @@ export async function runLoopProcess(argv, io, dependencies = {}) {
43
44
  return 0;
44
45
  }
45
46
  try {
47
+ const input = parseInput(await readStdin(io.stdin));
48
+ const inputIsObject = typeof input === "object" && input !== null && !Array.isArray(input);
49
+ const managedRoot = inputIsObject
50
+ ? await resolveProjectLoopRoot(input, dependencies.root ?? process.cwd(), harness).catch(() => null)
51
+ : null;
52
+ if (dependencies.office !== undefined && managedRoot !== null && await officeEnabled(managedRoot)) {
53
+ const phase = officeLoopPhaseHint(input, managedRoot);
54
+ void dependencies.office({
55
+ root: managedRoot,
56
+ harness,
57
+ event,
58
+ input,
59
+ validated: true,
60
+ ...(phase === undefined ? {} : { phase })
61
+ }).catch(() => undefined);
62
+ }
46
63
  const result = await runProjectLoop({
47
64
  harness,
48
65
  event,
49
- input: parseInput(await readStdin(io.stdin)),
66
+ input,
50
67
  root: dependencies.root ?? process.cwd(),
51
68
  ...(dependencies.now === undefined ? {} : { now: dependencies.now }),
52
69
  ...(dependencies.gitStatus === undefined
@@ -9,6 +9,8 @@ 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 { criteriaProgress } from "../../../../runtime/src/run/phase.js";
13
+ import { prepareRunIntegration, readRunIntegrationProof, abandonPreparedRunIntegration } from "../../../../runtime/src/run/integration.js";
12
14
  /** The child process runs the existing CLI in the candidate checkout. */
13
15
  export async function runAdvanceStep(cwd, args) {
14
16
  const bin = process.argv[1];
@@ -87,6 +89,13 @@ export async function runAdvanceCommand(options) {
87
89
  if (record === null || record.sessionId !== options.sessionId || record.agentId !== undefined) {
88
90
  throw new AgentOpsError("ADVANCE_WORKTREE_MISSING", `No coordinator worktree for session ${options.sessionId}.`);
89
91
  }
92
+ const savedProof = await readRunIntegrationProof(commonDir, record);
93
+ if (savedProof !== null && await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) === savedProof.head) {
94
+ const finished = await finishWorktree(deps, { cwd: mainRoot, name, finalProof: { ...savedProof, recovery: true } });
95
+ return okEnvelope("TASK_ADVANCED", { ...finished, text: "Recovered the sealed integration without moving the target again." });
96
+ }
97
+ if (savedProof !== null && await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) !== savedProof.target)
98
+ await abandonPreparedRunIntegration(commonDir, record, deps.git);
90
99
  const parent = await deps.tasks(record.path).status({ taskId: options.parentTaskId });
91
100
  if (parent.status !== "active" || parent.task.parentTaskId !== undefined) {
92
101
  throw new AgentOpsError("ADVANCE_PARENT_INVALID", "Advance requires an active root task in the coordinator worktree.");
@@ -120,16 +129,42 @@ export async function runAdvanceCommand(options) {
120
129
  throw new AgentOpsError("ADVANCE_TASK_TREE_INVALID", "The candidate has unrelated or already completed tasks; final proof must cover one active tree.");
121
130
  }
122
131
  const ordered = all.filter(({ task }) => selected.has(task.id));
132
+ const noChangePaths = head === target ? parent.noChangePaths : undefined;
133
+ if (noChangePaths !== undefined)
134
+ for (const task of ordered)
135
+ await deps.tasks(record.path).recordEvidence(task.task.id, {}, undefined, noChangePaths);
136
+ const onPhase = options.onPhase ?? (async () => { });
137
+ await onPhase("verifying");
123
138
  for (const task of ordered) {
124
- requirePass(await step(record.path, ["verify", "--task", task.task.id, "--base", target]), `Verify ${task.task.id}`);
139
+ const verified = await step(record.path, ["verify", "--task", task.task.id, "--base", target]);
140
+ await onPhase("verifying", { taskId: task.task.id, update: { verify: verified.status === "ok" ? "PASS" : "FAIL",
141
+ ...criteriaProgress(task.task.criteria, verified.data?.report ?? {}) } });
142
+ requirePass(verified, `Verify ${task.task.id}`);
143
+ }
144
+ await onPhase("reviewing");
145
+ let reviewMode;
146
+ try {
147
+ reviewMode = await reviewFinalTree(step, record.path, parent.task.id, ordered.map(({ task }) => task.id), target);
125
148
  }
126
- 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 });
149
+ catch (failure) {
150
+ for (const { task } of ordered)
151
+ await onPhase("reviewing", { taskId: task.id, update: { review: "FAIL" } });
152
+ throw failure;
153
+ }
154
+ for (const { task } of ordered)
155
+ await onPhase("integrating", { taskId: task.id, update: { review: "PASS", passed: task.criteria.length, total: task.criteria.length } });
156
+ const scope = await resolveReviewScope({ root: record.path, runner: runner(deps, record.path), base: target,
157
+ ...(noChangePaths === undefined ? {} : { noChangePaths }) });
128
158
  const sourceFingerprint = await calculateSourceFingerprint(record.path, scope, runner(deps, record.path));
159
+ const finalProof = { target, head, sourceFingerprint, children,
160
+ ...(noChangePaths === undefined ? {} : { noChange: { sourceCommit: head,
161
+ contractDigest: await deps.tasks(record.path).treeContract(parent.task.id), reviewScope: scope,
162
+ artifactRefs: Object.values((await deps.tasks(record.path).status({ taskId: parent.task.id })).evidence).flat() } }) };
163
+ await prepareRunIntegration(commonDir, record, finalProof);
129
164
  try {
130
165
  const finished = await finishWorktree(deps, {
131
166
  cwd: mainRoot, name,
132
- finalProof: { target, head, sourceFingerprint, children }
167
+ finalProof
133
168
  });
134
169
  if (finished.receipt === undefined)
135
170
  throw new AgentOpsError("WORKTREE_RECEIPT_MISSING", "Finish returned without a final review receipt.");
@@ -144,8 +179,10 @@ export async function runAdvanceCommand(options) {
144
179
  });
145
180
  }
146
181
  catch (failure) {
147
- if (failure instanceof AgentOpsError && failure.code === "WORKTREE_TARGET_MOVED" && attempt === 0)
182
+ if (failure instanceof AgentOpsError && failure.code === "WORKTREE_TARGET_MOVED" && attempt === 0) {
183
+ await abandonPreparedRunIntegration(commonDir, record, deps.git);
148
184
  continue;
185
+ }
149
186
  throw failure;
150
187
  }
151
188
  }
@@ -13,6 +13,7 @@ export function formatInstallPlan(plan, trust, warnings = []) {
13
13
  title: "Installation plan",
14
14
  metadata: [
15
15
  `Scope: ${plan.scope}`,
16
+ `Office (Preview): ${plan.config.features.office?.enabled === true ? "enabled" : "disabled"}`,
16
17
  `Harness: ${plan.harness}`,
17
18
  `Profiles: ${plan.profiles.join(", ")}`,
18
19
  ...(plan.config.worktree === undefined
@@ -103,6 +104,7 @@ export async function runInitCommand(options) {
103
104
  scope: args.scope,
104
105
  harness: args.harness,
105
106
  profiles: args.profiles,
107
+ ...(args.office === undefined ? {} : { officeEnabled: args.office === "on" }),
106
108
  adapters: options.adapters,
107
109
  ...(options.toolkitVersion === undefined
108
110
  ? {}
@@ -0,0 +1,20 @@
1
+ import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
2
+ import { errorEnvelope, okEnvelope } from "../output.js";
3
+ export async function runOfficeCommand(options) {
4
+ try {
5
+ const url = await options.ensure();
6
+ if (url !== null)
7
+ return okEnvelope("OFFICE_READY", { url, text: `Office (Preview): ${url}` });
8
+ let shown = "";
9
+ await options.foreground((served) => {
10
+ shown = served;
11
+ options.writeStdout(`Office (Preview): ${served}\nServing in the foreground; it exits 10 minutes after no run or session is active.\n`);
12
+ });
13
+ return okEnvelope("OFFICE_CLOSED", { url: shown, text: "Office closed: no run was active for 10 minutes." });
14
+ }
15
+ catch (cause) {
16
+ if (cause instanceof AgentOpsError)
17
+ return errorEnvelope(cause.code, cause.message);
18
+ throw cause;
19
+ }
20
+ }
@@ -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)) ||
@@ -4,6 +4,12 @@ import { renderReviewResult } from "../../../../runtime/src/review/render.js";
4
4
  import { reviewReportResults } from "../../../../runtime/src/review/report.js";
5
5
  import { findPriorFailingFindings, findReviewAttestation, invalidateReviewAttestation, readReviewReportArtifact, reviewReportDigest, saveReviewAttestation, saveReviewReportArtifact } from "../../../../runtime/src/review/attestation.js";
6
6
  import { detectHostTarget, planReviewTargets, resolveReviewRole } from "../../../../runtime/src/review/roles.js";
7
+ import { taskContractHash, treeContractHash, goalHash } from "../../../../runtime/src/task/contract.js";
8
+ import { acceptanceCoverage, coverageDigest } from "../../../../runtime/src/verify/acceptance-coverage.js";
9
+ import { resolveGitCommit } from "../../../../runtime/src/verify/change-surface.js";
10
+ import { sha256 } from "../../../../runtime/src/fs/hash.js";
11
+ import { readPrivateFile, writePrivateFile } from "../../../../runtime/src/security/permissions.js";
12
+ import { redactSecrets } from "../../../../runtime/src/security/redact.js";
7
13
  import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
8
14
  import { assertSafeSupportingPaths, isReviewerPolicyPath, resolveReviewScope, reviewScopeSignature } from "../../../../runtime/src/review/scope.js";
9
15
  import { calculateSourceFingerprint } from "../../../../runtime/src/verify/source-fingerprint.js";
@@ -70,6 +76,7 @@ async function taskContext(options) {
70
76
  throw new AgentOpsError("REVIEW_CRITERIA_NOT_FOUND", "Every requested review criterion must exist on the selected task.");
71
77
  }
72
78
  return {
79
+ records: [record],
73
80
  taskId: record.task.id,
74
81
  title: record.task.intent === undefined
75
82
  ? record.task.title
@@ -114,7 +121,16 @@ async function treeContext(options) {
114
121
  if (treeTasks.some(({ policyConfigHash }) => policyConfigHash !== root.policyConfigHash)) {
115
122
  throw new AgentOpsError("REVIEW_TREE_POLICY_MISMATCH", "Tree tasks must share one verifier policy.");
116
123
  }
124
+ const historical = new Set([root.task.id]);
125
+ for (let size = 0; size !== historical.size;) {
126
+ size = historical.size;
127
+ for (const record of all)
128
+ if (record.task.parentTaskId !== undefined && historical.has(record.task.parentTaskId))
129
+ historical.add(record.task.id);
130
+ }
117
131
  return {
132
+ historyRecords: all.filter(record => historical.has(record.task.id) && record.supersededBy !== undefined),
133
+ records: treeTasks,
118
134
  taskId: root.task.id,
119
135
  title: [root.task.title, ...treeTasks.map(({ task }) => `Task ${task.id}${task.parentTaskId === undefined ? "" : ` under ${task.parentTaskId}`}: ${task.title}\nModification intent: ${task.intent ?? task.title}`)].join("\n\n"),
120
136
  active: treeTasks.every(({ status }) => status === "active"),
@@ -163,7 +179,10 @@ async function currentEvidence(options, context, criterionId, commandId, configH
163
179
  evidence.criterionId !== (owner?.criterionId ?? criterionId) ||
164
180
  evidence.commandId !== commandId ||
165
181
  evidence.configHash !== configHash ||
166
- evidence.sourceFingerprint !== sourceFingerprint) {
182
+ evidence.sourceFingerprint !== sourceFingerprint ||
183
+ (context.records.some(r => r.task.id === (owner?.taskId ?? context.taskId) &&
184
+ (r.task.goal !== undefined || (r.task.contractRevision ?? 0) > 0 || r.task.criteria.some(c => c.acceptance !== undefined))) &&
185
+ evidence.taskContractHash !== taskContractHash(context.records.find(r => r.task.id === (owner?.taskId ?? context.taskId)).task))) {
167
186
  stale = true;
168
187
  continue;
169
188
  }
@@ -190,14 +209,18 @@ async function preflightReview(options, context, sourceFingerprint) {
190
209
  const configHash = calculateConfigHash(options.config);
191
210
  const commands = [];
192
211
  for (const criterion of context.criteria) {
193
- for (const commandId of criterion.verifierIds ?? []) {
212
+ const owner = context.criterionOwners?.get(criterion.id);
213
+ const typed = context.records.find(r => r.task.id === (owner?.taskId ?? context.taskId))?.task.criteria
214
+ .find(c => c.id === (owner?.criterionId ?? criterion.id))?.acceptance !== undefined;
215
+ for (const commandId of new Set([...(criterion.verifierIds ?? []),
216
+ ...(typed ? options.config.verification.commands.filter(c => c.required).map(c => c.id) : [])])) {
194
217
  const command = options.config.verification.commands.find((candidate) => candidate.id === commandId);
195
218
  if (command === undefined) {
196
219
  return { ok: false, reason: "missing-verification-evidence" };
197
220
  }
198
221
  const found = await currentEvidence(options, context, criterion.id, commandId, configHash, sourceFingerprint);
199
222
  const compatible = command.evidence.kind === "test-count"
200
- ? found.current.filter(({ evidence }) => evidence.schemaVersion === 3)
223
+ ? found.current.filter(({ evidence }) => evidence.schemaVersion >= 3)
201
224
  : found.current;
202
225
  if (command.required && found.current.length > 0 && compatible.length === 0) {
203
226
  return { ok: false, reason: "stale-verification" };
@@ -258,9 +281,11 @@ export async function hasFreshVerification(options, taskId, base) {
258
281
  if (context === undefined) {
259
282
  return false;
260
283
  }
284
+ options = { ...options, noChangePaths: options.noChangePaths ?? context.records.find(r => r.task.id === context.taskId)?.noChangePaths };
261
285
  const scope = await resolveReviewScope({
262
286
  root: options.root,
263
287
  runner: options.gitRunner,
288
+ ...(options.noChangePaths === undefined ? {} : { noChangePaths: options.noChangePaths }),
264
289
  ...(base === undefined ? {} : { base })
265
290
  });
266
291
  const fingerprint = await calculateSourceFingerprint(options.root, scope, options.gitRunner);
@@ -352,7 +377,10 @@ async function reusableReview(options, sourceFingerprint, context) {
352
377
  return null;
353
378
  }
354
379
  const attestation = await findReviewAttestation(options.root, sourceFingerprint, context?.taskId);
355
- if (attestation === null || attestation.taskId !== context?.taskId) {
380
+ if (attestation === null || attestation.taskId !== context?.taskId ||
381
+ (context?.binding !== undefined && (attestation.schemaVersion !== 3 ||
382
+ JSON.stringify(attestation.taskContracts) !== JSON.stringify(context.binding.taskContracts) ||
383
+ attestation.coverageDigest !== context.binding.coverageDigest))) {
356
384
  return null;
357
385
  }
358
386
  if (context?.treeTasks !== undefined &&
@@ -420,7 +448,7 @@ async function persistReviewEvidence(options, result, sourceFingerprint, context
420
448
  const reportArtifacts = [];
421
449
  try {
422
450
  for (const taskId of taskIds) {
423
- reportArtifacts.push({ taskId, path: await saveReviewReportArtifact(options.root, artifactResult, sourceFingerprint, taskId, complete ? tree : undefined) });
451
+ reportArtifacts.push({ taskId, path: await saveReviewReportArtifact(options.root, artifactResult, sourceFingerprint, taskId, complete ? tree : undefined, context?.binding === undefined ? undefined : { ...context.binding, taskContractHash: context.binding.taskContracts[taskId ?? context.taskId] }, context?.coverage) });
424
452
  }
425
453
  }
426
454
  catch {
@@ -441,7 +469,8 @@ async function persistReviewEvidence(options, result, sourceFingerprint, context
441
469
  const plannedTargets = result.plannedTargets;
442
470
  for (const artifact of reportArtifacts) {
443
471
  await saveReviewAttestation(options.root, {
444
- schemaVersion: 2,
472
+ schemaVersion: context?.binding === undefined ? 2 : 3,
473
+ ...(context?.binding === undefined ? {} : { ...context.binding, taskContractHash: context.binding.taskContracts[artifact.taskId ?? context.taskId] }),
445
474
  ...(artifact.taskId === undefined ? {} : { taskId: artifact.taskId }),
446
475
  ...(tree === undefined ? {} : { tree }),
447
476
  harness: result.harness,
@@ -502,6 +531,7 @@ export async function runReviewCommand(options) {
502
531
  const context = options.args.tree === true
503
532
  ? await treeContext(options)
504
533
  : await taskContext(options);
534
+ options = { ...options, noChangePaths: options.noChangePaths ?? context?.records.find(r => r.task.id === context.taskId)?.noChangePaths };
505
535
  const evidenceRequirements = (options.args.evidence ?? []).map((value) => {
506
536
  const separator = value.indexOf("=");
507
537
  return {
@@ -517,6 +547,7 @@ export async function runReviewCommand(options) {
517
547
  scope = await resolveReviewScope({
518
548
  root: options.root,
519
549
  runner: options.gitRunner,
550
+ ...(options.noChangePaths === undefined ? {} : { noChangePaths: options.noChangePaths }),
520
551
  ...(options.args.base === undefined ? {} : { base: options.args.base })
521
552
  });
522
553
  }
@@ -574,7 +605,7 @@ export async function runReviewCommand(options) {
574
605
  // source was reviewed; it says nothing about the policy in force now or
575
606
  // about verification that has failed since, and both of those are
576
607
  // decided above.
577
- const reused = options.args.rerun || context?.treeTasks !== undefined
608
+ const reused = options.args.rerun || context?.treeTasks !== undefined || context?.records.some(r => r.task.goal !== undefined || r.task.criteria.some(c => c.acceptance !== undefined) || (r.task.contractRevision ?? 0) > 0)
578
609
  ? null
579
610
  : await reusableReview(options, sourceFingerprint, context);
580
611
  if (reused !== null) {
@@ -593,6 +624,23 @@ export async function runReviewCommand(options) {
593
624
  pendingInvalidation = false;
594
625
  }
595
626
  }
627
+ if (context !== undefined && sourceFingerprint !== undefined && options.config !== undefined &&
628
+ options.evidenceStore !== undefined && options.gitRunner !== undefined) {
629
+ const rows = [];
630
+ for (const record of context.records) {
631
+ const coverage = await acceptanceCoverage(record, options.config, sourceFingerprint, options.evidenceStore);
632
+ rows.push(...coverage.map(row => ({ ...row, criterionId: context.treeTasks === undefined
633
+ ? row.criterionId : record.task.id + ":" + row.criterionId })));
634
+ }
635
+ context.coverage = rows;
636
+ context.binding = {
637
+ taskContractHash: taskContractHash(context.records.find(r => r.task.id === context.taskId).task),
638
+ taskContracts: Object.fromEntries(context.records.map(r => [r.task.id, taskContractHash(r.task)])),
639
+ ...(context.treeTasks === undefined ? {} : { treeContractHash: treeContractHash(context.records.map(r => r.task)) }),
640
+ goalHash: goalHash(context.records.find(r => r.task.id === context.taskId).task),
641
+ candidateCommit: await resolveGitCommit(options.gitRunner, "HEAD"), coverageDigest: coverageDigest(rows)
642
+ };
643
+ }
596
644
  const criteria = [
597
645
  ...(context?.criteria ?? GENERIC_CRITERIA)
598
646
  ];
@@ -601,7 +649,44 @@ export async function runReviewCommand(options) {
601
649
  }
602
650
  let packet;
603
651
  try {
652
+ let contractManifest;
653
+ const contractArtifacts = [];
654
+ if (context?.binding !== undefined && options.root !== undefined) {
655
+ const historicalRefs = new Set([
656
+ ...[...context.records, ...(context.historyRecords ?? [])].flatMap(r => (r.revisions ?? []).flatMap(revision => Object.values(revision.previousEvidence).flat())),
657
+ ...(context.historyRecords ?? []).flatMap(r => Object.values(r.evidence).flat())
658
+ ]);
659
+ const refs = new Set([...context.records.flatMap(r => Object.values(r.evidence).flat()), ...historicalRefs].filter(ref => !ref.startsWith("review:")));
660
+ for (const path of refs) {
661
+ const loaded = await options.evidenceStore?.load(path);
662
+ if (loaded == null)
663
+ continue;
664
+ if (!historicalRefs.has(path) && loaded.sourceFingerprint !== sourceFingerprint)
665
+ continue;
666
+ const content = await readPrivateFile(options.root + "/" + path, options.root);
667
+ if (content !== null)
668
+ contractArtifacts.push({ path, digest: sha256(content), content });
669
+ const executionPath = loaded.acceptance?.executionArtifact;
670
+ if (executionPath !== undefined && !contractArtifacts.some(artifact => artifact.path === executionPath)) {
671
+ const execution = await readPrivateFile(options.root + "/" + executionPath, options.root);
672
+ if (execution !== null)
673
+ contractArtifacts.push({ path: executionPath, digest: sha256(execution), content: execution });
674
+ }
675
+ }
676
+ const content = redactSecrets(JSON.stringify({ binding: context.binding, coverage: context.coverage,
677
+ executionPolicy: options.config,
678
+ artifacts: contractArtifacts.map(({ path, digest }) => ({ path, digest })),
679
+ tasks: context.records.map(r => ({ task: r.task, revisions: r.revisions ?? [] })),
680
+ superseded: context.historyRecords ?? [] }, null, 2));
681
+ const digest = sha256(content);
682
+ const path = ".agent-ops/tasks/review-contracts/" + digest + ".json";
683
+ contractManifest = { path, digest, content };
684
+ // Validated before any manifest is written or handed to a snapshot.
685
+ buildReviewPacket({ request: context.title, criteria, artifactRefs: [], evidenceRequirements, contractManifest, contractArtifacts });
686
+ await writePrivateFile(options.root + "/" + path, content, options.root);
687
+ }
604
688
  packet = buildReviewPacket({
689
+ ...(contractManifest === undefined ? {} : { contractManifest, contractArtifacts }),
605
690
  request: context?.title ?? GENERIC_REQUEST,
606
691
  criteria,
607
692
  artifactRefs: scope?.changedFiles ?? [],
@@ -676,6 +761,7 @@ export async function runReviewCommand(options) {
676
761
  const postflight = await resolveReviewScope({
677
762
  root: options.root,
678
763
  runner: options.gitRunner,
764
+ ...(options.noChangePaths === undefined ? {} : { noChangePaths: options.noChangePaths }),
679
765
  ...(options.args.base === undefined ? {} : { base: options.args.base })
680
766
  });
681
767
  const currentHash = options.currentPolicyConfigHash === undefined
@@ -684,7 +770,12 @@ export async function runReviewCommand(options) {
684
770
  const postflightFingerprint = await calculateSourceFingerprint(options.root, postflight, options.gitRunner);
685
771
  if (reviewScopeSignature(scope) !== reviewScopeSignature(postflight) ||
686
772
  (options.policyConfigHash !== undefined && currentHash !== options.policyConfigHash) ||
687
- postflightFingerprint !== sourceFingerprint) {
773
+ postflightFingerprint !== sourceFingerprint ||
774
+ (context?.binding?.treeContractHash !== undefined && options.tasks !== undefined &&
775
+ await options.tasks.treeContract(context.taskId) !== context.binding.treeContractHash) ||
776
+ (context !== undefined && options.tasks !== undefined &&
777
+ (await Promise.all(context.records.map(async (r) => taskContractHash((await options.tasks.status({ taskId: r.task.id })).task))))
778
+ .some((hash, i) => hash !== taskContractHash(context.records[i].task)))) {
688
779
  const changed = await persistReviewEvidence(options, sourceChangedResult(result), sourceFingerprint, context);
689
780
  return notRunEnvelope(changed);
690
781
  }
@@ -712,7 +803,7 @@ export async function runReviewCommand(options) {
712
803
  ? criterion.id : `${record.task.id}:${criterion.id}`;
713
804
  const result = finalResult.results?.find((item) => item.criterionId === id);
714
805
  return [criterion.id, (result?.evidence ?? []).map((reference) => `review:${finalResult.harness}:${reference}`)];
715
- })), scope?.mode === "base" ? scope.resolvedBase : null);
806
+ })), scope?.mode === "base" ? scope.resolvedBase : null, scope?.mode === "base" && scope.noChange === true ? scope.changedFiles : undefined);
716
807
  }
717
808
  }
718
809
  const message = finalResult.status === "PASS"
@@ -0,0 +1,115 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { AgentOpsError, resolveContainedPath } from "../../../../runtime/src/fs/paths.js";
3
+ import { errorEnvelope, okEnvelope } from "../output.js";
4
+ function dynamic(args) {
5
+ return args;
6
+ }
7
+ function actionOf(args) {
8
+ const value = dynamic(args).runAction ?? dynamic(args).action;
9
+ if (value === undefined || value === "start")
10
+ return "start";
11
+ if (["status", "logs", "resume", "stop", "respond"].includes(String(value)))
12
+ return value;
13
+ throw new AgentOpsError("RUN_ACTION_INVALID", `Unsupported run action: ${String(value)}`);
14
+ }
15
+ function runIdOf(args) {
16
+ const runId = args.runId ?? dynamic(args).taskId;
17
+ if (typeof runId !== "string" || runId.length === 0)
18
+ throw new AgentOpsError("RUN_ID_REQUIRED", "This run action requires a run id.");
19
+ return runId;
20
+ }
21
+ function textFor(action, state, message) {
22
+ const active = state.workers.filter((worker) => ["assigned", "starting", "running", "idle", "handing-off"].includes(worker.status)).length;
23
+ return [
24
+ message,
25
+ `Status: ${state.status}`,
26
+ `Host: ${state.host}`,
27
+ `Workers: ${active}/${state.jobs}`,
28
+ `Budget: ${Math.max(0, state.budget.limitMs - state.budget.accumulatedMs)}ms remaining`,
29
+ `Usage: ${state.budget.usage.tokens ?? "unknown"} tokens; ${state.budget.usage.usd === null ? "unknown" : "$" + state.budget.usage.usd} cost (${state.budget.usage.completeness})`,
30
+ action === "logs" ? "" : "Native goal completion is not final proof; verify, review, task state, and receipt are still required."
31
+ ].filter((line) => line.length > 0).join("\n");
32
+ }
33
+ function data(action, result) {
34
+ return {
35
+ action,
36
+ runId: result.state.runId,
37
+ state: result.state,
38
+ message: result.message,
39
+ text: textFor(action, result.state, result.message)
40
+ };
41
+ }
42
+ function stateData(action, state, message) {
43
+ return { action, runId: state.runId, state, message, text: textFor(action, state, message) };
44
+ }
45
+ async function goalFromArgs(args, root) {
46
+ if (args.goal !== undefined && args.goalFile !== undefined)
47
+ throw new AgentOpsError("RUN_GOAL_CONFLICT", "Use either a positional goal or --goal-file, not both.");
48
+ if (args.goalFile !== undefined) {
49
+ const path = await resolveContainedPath(root, args.goalFile);
50
+ return (await readFile(path, "utf8")).trim();
51
+ }
52
+ if (args.goal === undefined || args.goal.trim() === "")
53
+ throw new AgentOpsError("RUN_GOAL_REQUIRED", "Run start requires a non-empty goal.");
54
+ return args.goal.trim();
55
+ }
56
+ async function answerFromArgs(args, root) {
57
+ if (args.answer !== undefined && args.answerFile !== undefined)
58
+ throw new AgentOpsError("RUN_ANSWER_CONFLICT", "Use either --answer or --answer-file, not both.");
59
+ if (args.answerFile !== undefined)
60
+ return (await readFile(await resolveContainedPath(root, args.answerFile), "utf8")).trim();
61
+ if (args.answer === undefined || args.answer.trim() === "")
62
+ throw new AgentOpsError("RUN_ANSWER_REQUIRED", "Run respond requires a non-empty answer.");
63
+ return args.answer.trim();
64
+ }
65
+ /**
66
+ * External lifecycle surface. The command only starts/controls a supervisor;
67
+ * it never marks the task complete or treats a native process exit as proof.
68
+ */
69
+ async function withOffice(options, response) {
70
+ const url = await options.office?.().catch(() => null) ?? null;
71
+ return url === null ? response : { ...response, officeUrl: url, text: `${response.text}\nOffice (Preview): ${url}` };
72
+ }
73
+ export async function runRunCommand(options) {
74
+ const action = actionOf(options.args);
75
+ try {
76
+ if (action === "start") {
77
+ const result = await options.service.start({
78
+ root: options.root,
79
+ commonDir: options.commonDir,
80
+ targetBranch: options.targetBranch,
81
+ goal: await goalFromArgs(options.args, options.root),
82
+ host: options.args.host ?? "codex",
83
+ ownerSessionId: options.ownerSessionId,
84
+ jobs: options.args.jobs,
85
+ timeBudgetMs: options.args.timeBudgetMs
86
+ });
87
+ const state = options.args.wait === true && options.waitForCompletion !== undefined
88
+ ? await options.waitForCompletion(result.state.runId)
89
+ : result.state;
90
+ const response = await withOffice(options, data(action, { ...result, state }));
91
+ return okEnvelope(options.args.wait === true ? "RUN_FINISHED" : "RUN_STARTED", response);
92
+ }
93
+ const runId = runIdOf(options.args);
94
+ if (action === "status")
95
+ return okEnvelope("RUN_STATUS", await withOffice(options, stateData(action, await options.service.status(runId), `Read run ${runId}.`)));
96
+ if (action === "logs") {
97
+ const state = await options.service.status(runId);
98
+ const events = await options.service.logs(runId);
99
+ return okEnvelope("RUN_LOGS", { ...stateData(action, state, `Read ${events.length} event(s) for run ${runId}.`), events });
100
+ }
101
+ if (action === "resume")
102
+ return okEnvelope("RUN_RESUMED", data(action, await options.service.resume(runId)));
103
+ if (action === "stop")
104
+ return okEnvelope("RUN_STOP_REQUESTED", data(action, await options.service.stop(runId, options.args.answer ?? "user requested stop")));
105
+ const questionId = options.args.questionId;
106
+ if (questionId === undefined || questionId.trim() === "")
107
+ throw new AgentOpsError("RUN_QUESTION_REQUIRED", "Run respond requires --question-id.");
108
+ return okEnvelope("RUN_RESPONDED", data(action, await options.service.respond(runId, questionId, await answerFromArgs(options.args, options.root))));
109
+ }
110
+ catch (cause) {
111
+ if (cause instanceof AgentOpsError)
112
+ return errorEnvelope(cause.code, cause.message);
113
+ throw cause;
114
+ }
115
+ }