@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.
- package/README.md +103 -1
- package/dist/packages/cli/src/args.js +183 -3
- package/dist/packages/cli/src/bin.js +76 -19
- package/dist/packages/cli/src/cli.js +53 -8
- package/dist/packages/cli/src/codex-loop-process.js +19 -2
- package/dist/packages/cli/src/commands/advance.js +42 -5
- package/dist/packages/cli/src/commands/init.js +2 -0
- package/dist/packages/cli/src/commands/office.js +20 -0
- 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 +115 -0
- package/dist/packages/cli/src/commands/task.js +61 -17
- package/dist/packages/cli/src/commands/update.js +8 -0
- package/dist/packages/cli/src/commands/verify.js +1 -0
- package/dist/packages/cli/src/context.js +64 -1
- package/dist/packages/cli/src/hook-entry.js +2 -1
- package/dist/packages/cli/src/hook-process.js +54 -3
- package/dist/packages/cli/src/loop-entry.js +2 -1
- package/dist/packages/cli/src/office-entry.js +230 -0
- 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 +321 -0
- package/dist/packages/cli/src/run-entry.js +952 -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/packages/cli/src/wizard.js +14 -2
- package/dist/runtime/src/adapters/agy/config.js +4 -3
- package/dist/runtime/src/adapters/agy/events.js +9 -0
- package/dist/runtime/src/adapters/claude/config.js +3 -3
- package/dist/runtime/src/adapters/claude/events.js +8 -0
- package/dist/runtime/src/adapters/codex/config.js +3 -3
- package/dist/runtime/src/adapters/codex/events.js +8 -0
- package/dist/runtime/src/adapters/opencode/config.js +5 -4
- package/dist/runtime/src/adapters/opencode/events.js +8 -0
- package/dist/runtime/src/config/explain.js +11 -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/codex-loop.js +4 -0
- package/dist/runtime/src/hooks/completion-gate.js +8 -2
- package/dist/runtime/src/install/doctor.js +2 -0
- package/dist/runtime/src/install/harness.js +44 -1
- package/dist/runtime/src/install/plan.js +16 -11
- package/dist/runtime/src/install/preauth.js +44 -18
- package/dist/runtime/src/install/profiles.js +8 -2
- package/dist/runtime/src/install/update.js +1 -0
- package/dist/runtime/src/office/collect.js +233 -0
- package/dist/runtime/src/office/page.js +873 -0
- package/dist/runtime/src/office/scene.js +168 -0
- package/dist/runtime/src/office/server.js +139 -0
- package/dist/runtime/src/office/sessions.js +115 -0
- package/dist/runtime/src/office/snapshot.js +107 -0
- package/dist/runtime/src/parallel/finish.js +85 -22
- package/dist/runtime/src/parallel/guard.js +12 -2
- 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/phase.js +37 -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 +433 -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 +80 -9
- 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/harness-adapters.md +113 -1
- 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/harness-adapters.md +93 -1
- package/docs/zh-TW/spec/loop-engineering.md +22 -0
- package/package.json +1 -1
- package/schemas/config.schema.json +230 -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
|
@@ -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
|
|
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
|
|
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
|
-
|
|
166
|
-
command id configured in .agent-ops/config.json.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|