@kylecheng3146/agent-ops 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/packages/cli/src/args.js +10 -10
- package/dist/packages/cli/src/cli.js +14 -4
- package/dist/packages/cli/src/commands/advance.js +2 -1
- package/dist/packages/cli/src/commands/parallel.js +1 -1
- package/dist/packages/cli/src/commands/review.js +3 -1
- package/dist/packages/cli/src/commands/run.js +10 -3
- package/dist/packages/cli/src/context.js +22 -4
- package/dist/packages/cli/src/office-entry.js +8 -4
- package/dist/packages/cli/src/owned-run-step.js +4 -4
- package/dist/packages/cli/src/parallel-deps.js +0 -2
- package/dist/packages/cli/src/run-deps.js +12 -9
- package/dist/packages/cli/src/run-entry.js +76 -17
- package/dist/packages/cli/src/run-setup-entry.js +2 -2
- package/dist/runtime/src/install/harness.js +5 -2
- package/dist/runtime/src/office/scene.js +1 -1
- package/dist/runtime/src/office/server.js +43 -16
- package/dist/runtime/src/office/snapshot.js +1 -1
- package/dist/runtime/src/parallel/finish.js +139 -65
- package/dist/runtime/src/parallel/service.js +5 -1
- package/dist/runtime/src/review/execute.js +9 -7
- package/dist/runtime/src/review/packet.js +17 -6
- package/dist/runtime/src/review/runner.js +2 -0
- package/dist/runtime/src/run/controls.js +99 -1
- package/dist/runtime/src/run/hosts/codex.js +28 -1
- package/dist/runtime/src/run/hosts/protocol.js +5 -2
- package/dist/runtime/src/run/integration.js +35 -13
- package/dist/runtime/src/run/policy.js +65 -10
- package/dist/runtime/src/run/scheduler.js +10 -5
- package/dist/runtime/src/run/service.js +68 -21
- package/dist/runtime/src/run/supervisor.js +11 -2
- package/dist/runtime/src/run/transport.js +1 -0
- package/dist/runtime/src/security/permissions.js +85 -0
- package/dist/runtime/src/task/debug-state.js +9 -3
- package/dist/runtime/src/task/debug-user-event.js +82 -17
- package/dist/runtime/src/task/debug.js +35 -15
- package/docs/en/guides/configuration.md +8 -4
- package/docs/en/guides/task-debug.md +14 -2
- package/docs/en/spec/acceptance-and-evidence.md +4 -0
- package/docs/en/spec/harness-adapters.md +26 -1
- package/docs/en/spec/loop-engineering.md +34 -6
- package/docs/en/spec/review.md +6 -0
- package/docs/zh-TW/guides/configuration.md +6 -3
- package/docs/zh-TW/guides/task-debug.md +9 -2
- package/docs/zh-TW/spec/acceptance-and-evidence.md +2 -1
- package/docs/zh-TW/spec/harness-adapters.md +21 -1
- package/docs/zh-TW/spec/loop-engineering.md +9 -3
- package/docs/zh-TW/spec/review.md +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ timeouts produce UNKNOWN. Review examines the checks themselves and every fallba
|
|
|
32
32
|
On macOS, a trusted clean main checkout can start an isolated native goal run:
|
|
33
33
|
|
|
34
34
|
```sh
|
|
35
|
-
agent-ops run "Implement the requested behavior" --host codex --
|
|
35
|
+
agent-ops run "Implement the requested behavior" --host codex --jobs 2
|
|
36
36
|
agent-ops run status <run-id>
|
|
37
37
|
agent-ops run logs <run-id>
|
|
38
38
|
agent-ops run respond <run-id> --question-id <id> --answer "Clarification"
|
|
@@ -49,7 +49,8 @@ verification, two fresh reviews, completed tasks and a receipt bound to the targ
|
|
|
49
49
|
Setup runs in the background under registered process groups. Stop cancels native
|
|
50
50
|
writers, local verification and setup; interrupted setup requires explicit resume.
|
|
51
51
|
The coordinator can assess additional non-dangerous fixed commands through a
|
|
52
|
-
policy request.
|
|
52
|
+
policy request. New runs have no total time cap; manual resume upgrades legacy finite runs while keeping accounting.
|
|
53
|
+
This finite authorization is bound to the run, runtime and lease;
|
|
53
54
|
it creates no permanent repository trust. Policy transitions fence writers and
|
|
54
55
|
journal delivery imports and contract synchronization before starting new leases.
|
|
55
56
|
Native authorization decisions retain redacted command scope and rationale;
|
|
@@ -960,7 +960,7 @@ function parseRunArgs(argv) {
|
|
|
960
960
|
positional = token;
|
|
961
961
|
continue;
|
|
962
962
|
}
|
|
963
|
-
if (seen.has(token))
|
|
963
|
+
if (seen.has(token) && token !== "--retry-approval")
|
|
964
964
|
duplicate(token);
|
|
965
965
|
seen.add(token);
|
|
966
966
|
if (token === "--json") {
|
|
@@ -971,10 +971,15 @@ function parseRunArgs(argv) {
|
|
|
971
971
|
args.wait = true;
|
|
972
972
|
continue;
|
|
973
973
|
}
|
|
974
|
-
if (!["--host", "--goal-file", "--time-budget", "--jobs", "--question-id", "--answer", "--answer-file"].includes(token))
|
|
974
|
+
if (!["--host", "--goal-file", "--time-budget", "--jobs", "--question-id", "--answer", "--answer-file", "--retry-approval"].includes(token))
|
|
975
975
|
throw new CliArgumentError("CLI_UNKNOWN_OPTION", `Unknown run option: ${token}`, token);
|
|
976
976
|
const value = readOptionValue(argv, i++, token);
|
|
977
977
|
switch (token) {
|
|
978
|
+
case "--retry-approval":
|
|
979
|
+
if (!/^auth-[a-f0-9]{64}$/u.test(value) || args.retryApprovalIds?.includes(value))
|
|
980
|
+
invalidValue(token, value);
|
|
981
|
+
args.retryApprovalIds = [...(args.retryApprovalIds ?? []), value];
|
|
982
|
+
break;
|
|
978
983
|
case "--host":
|
|
979
984
|
if (value !== "claude" && value !== "codex")
|
|
980
985
|
invalidValue(token, value);
|
|
@@ -998,17 +1003,12 @@ function parseRunArgs(argv) {
|
|
|
998
1003
|
args.jobs = Number(value);
|
|
999
1004
|
break;
|
|
1000
1005
|
case "--time-budget": {
|
|
1001
|
-
|
|
1002
|
-
if (match === null)
|
|
1003
|
-
invalidValue(token, value);
|
|
1004
|
-
const ms = Number(match[1]) * ({ s: 1000, m: 60000, h: 3600000 }[match[2]]);
|
|
1005
|
-
if (!Number.isSafeInteger(ms) || ms > 86400000)
|
|
1006
|
-
invalidValue(token, value);
|
|
1007
|
-
args.timeBudgetMs = ms;
|
|
1008
|
-
break;
|
|
1006
|
+
throw new CliArgumentError("RUN_TIME_BUDGET_UNSUPPORTED", "New runs have no total time cap; omit --time-budget. Command and review timeouts remain effective.", token);
|
|
1009
1007
|
}
|
|
1010
1008
|
}
|
|
1011
1009
|
}
|
|
1010
|
+
if (args.retryApprovalIds !== undefined && action !== "resume")
|
|
1011
|
+
throw new CliArgumentError("CLI_INVALID_OPTION", "--retry-approval requires run resume.");
|
|
1012
1012
|
if (action === "start") {
|
|
1013
1013
|
args.goal = positional;
|
|
1014
1014
|
if ((args.goal === undefined) === (args.goalFile === undefined))
|
|
@@ -98,13 +98,22 @@ browser once; later sessions reuse it. It exits after 10 minutes without active
|
|
|
98
98
|
runs or sessions. Recently completed rooms remain for at most 2 hours.
|
|
99
99
|
The URL carries an access token; do not share it.
|
|
100
100
|
`,
|
|
101
|
-
run: `Usage: agent-ops run [start] "<goal>" --host <claude|codex> [--jobs <1|2>] [--
|
|
101
|
+
run: `Usage: agent-ops run [start] "<goal>" --host <claude|codex> [--jobs <1|2>] [--wait] [--json]
|
|
102
102
|
agent-ops run [start] --goal-file <path> --host <claude|codex>
|
|
103
103
|
agent-ops run <status|logs|resume|stop> <run-id> [--json]
|
|
104
|
+
agent-ops run resume <run-id> [--retry-approval <auth-event-id>]... [--json]
|
|
104
105
|
agent-ops run respond <run-id> --question-id <id> <--answer <text>|--answer-file <path>> [--json]
|
|
105
106
|
|
|
107
|
+
New runs have no total time cap. Manual resume upgrades legacy finite runs and keeps accounting.
|
|
108
|
+
Command/review timeouts, provider limits, DEBUG budgets and stop controls remain effective.
|
|
106
109
|
Native goal state is observational. Completion requires current verification,
|
|
107
110
|
two fresh reviews, task completion and a target-bound integration receipt.
|
|
111
|
+
--retry-approval explicitly retries an intact Codex approval-review usage-limit
|
|
112
|
+
failure. Original denials remain recorded; every command needs fresh approval.
|
|
113
|
+
Status includes phase, original cause, recorded sealed proof, cleanup state,
|
|
114
|
+
resumability and remediation. Resume revalidates authority and sealed artifacts;
|
|
115
|
+
receipt-bound recovery executes only missing cleanup steps. Resolve the saved
|
|
116
|
+
blocker before retrying: unchanged cleanup inputs are refused.
|
|
108
117
|
`,
|
|
109
118
|
init: `Usage: agent-ops init [options]
|
|
110
119
|
|
|
@@ -308,9 +317,10 @@ voiding each other's verification and review. Run it from the main checkout.
|
|
|
308
317
|
before Git runs; see agent-ops worktree list.
|
|
309
318
|
finish <name>
|
|
310
319
|
Once its task is complete, fast-forward the main checkout's
|
|
311
|
-
branch to the worktree's
|
|
312
|
-
|
|
313
|
-
|
|
320
|
+
branch to the worktree's, record the task in git notes, and
|
|
321
|
+
remove the worktree. If the branch moved, it rebases the
|
|
322
|
+
worktree and stops: verify and review again with the printed
|
|
323
|
+
--base, then finish again. One finish runs at a time.
|
|
314
324
|
commit Stage every change in the current worktree and commit it with
|
|
315
325
|
--message. Refuses the main checkout and a worktree another
|
|
316
326
|
session owns, and needs no Git in the shell.
|
|
@@ -90,7 +90,8 @@ export async function runAdvanceCommand(options) {
|
|
|
90
90
|
throw new AgentOpsError("ADVANCE_WORKTREE_MISSING", `No coordinator worktree for session ${options.sessionId}.`);
|
|
91
91
|
}
|
|
92
92
|
const savedProof = await readRunIntegrationProof(commonDir, record);
|
|
93
|
-
if (savedProof !== null && await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) === savedProof.head
|
|
93
|
+
if (savedProof !== null && (await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) === savedProof.head ||
|
|
94
|
+
(await deps.git(mainRoot, ["merge-base", "--is-ancestor", savedProof.head, record.targetBranch])).exitCode === 0)) {
|
|
94
95
|
const finished = await finishWorktree(deps, { cwd: mainRoot, name, finalProof: { ...savedProof, recovery: true } });
|
|
95
96
|
return okEnvelope("TASK_ADVANCED", { ...finished, text: "Recovered the sealed integration without moving the target again." });
|
|
96
97
|
}
|
|
@@ -41,7 +41,7 @@ function addText(result) {
|
|
|
41
41
|
function finishText(result) {
|
|
42
42
|
const { record } = result;
|
|
43
43
|
return [
|
|
44
|
-
`Merged ${record.branch} into ${record.targetBranch} at ${result.mergedHead}
|
|
44
|
+
`Merged ${record.branch} into ${record.targetBranch} at ${result.mergedHead}.`,
|
|
45
45
|
`Completed and recorded in git notes (refs/notes/agent-ops): ${result.taskIds.join(", ")}.`,
|
|
46
46
|
`Removed ${record.path}.`,
|
|
47
47
|
...(result.receipt === undefined ? [] : [`Receipt: ${result.receipt}`]),
|
|
@@ -816,12 +816,14 @@ export async function runReviewCommand(options) {
|
|
|
816
816
|
contractArtifacts.push({ path: executionPath, digest: sha256(execution), content: execution });
|
|
817
817
|
}
|
|
818
818
|
}
|
|
819
|
+
// Compact: indentation alone pushed a 494 KiB 0.10.2 manifest past the 512 KiB bound.
|
|
819
820
|
const content = redactSecrets(JSON.stringify({ binding: context.binding, coverage: context.coverage,
|
|
820
821
|
executionPolicy: options.config,
|
|
822
|
+
...(surfacePacket === undefined ? {} : { surface: surfacePacket }),
|
|
821
823
|
artifacts: contractArtifacts.map(({ path, digest }) => ({ path, digest })),
|
|
822
824
|
tasks: await Promise.all(context.records.map(async (r) => ({ task: r.task, revisions: r.revisions ?? [],
|
|
823
825
|
...(r.task.debug === undefined ? {} : { debugProof: await loadDebugProof(options.root, r.task.debug.rootTaskId) }) }))),
|
|
824
|
-
superseded: context.historyRecords ?? [] }
|
|
826
|
+
superseded: context.historyRecords ?? [] }));
|
|
825
827
|
const digest = sha256(content);
|
|
826
828
|
const path = ".agent-ops/tasks/review-contracts/" + digest + ".json";
|
|
827
829
|
contractManifest = { path, digest, content };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { AgentOpsError, resolveContainedPath } from "../../../../runtime/src/fs/paths.js";
|
|
3
|
+
import { describeRunRecovery } from "../../../../runtime/src/run/service.js";
|
|
3
4
|
import { errorEnvelope, okEnvelope } from "../output.js";
|
|
4
5
|
function dynamic(args) {
|
|
5
6
|
return args;
|
|
@@ -19,13 +20,18 @@ function runIdOf(args) {
|
|
|
19
20
|
return runId;
|
|
20
21
|
}
|
|
21
22
|
function textFor(action, state, message) {
|
|
23
|
+
const recovery = describeRunRecovery(state);
|
|
22
24
|
const active = state.workers.filter((worker) => ["assigned", "starting", "running", "idle", "handing-off"].includes(worker.status)).length;
|
|
23
25
|
return [
|
|
24
26
|
message,
|
|
25
27
|
`Status: ${state.status}`,
|
|
28
|
+
`Phase: ${recovery.phase}`,
|
|
29
|
+
`Cause: ${recovery.cause === null ? "none recorded" : recovery.cause.code + ": " + recovery.cause.detail}`,
|
|
30
|
+
`Sealed proof: ${recovery.sealedProof}; cleanup: ${recovery.cleanup}; resumable: ${recovery.resumable ? "yes, after reconciliation" : "no"}`,
|
|
31
|
+
`Remediation: ${recovery.remediation}`,
|
|
26
32
|
`Host: ${state.host}`,
|
|
27
33
|
`Workers: ${active}/${state.jobs}`,
|
|
28
|
-
`Budget: ${Math.max(0, state.budget.limitMs - state.budget.accumulatedMs)}ms remaining`,
|
|
34
|
+
state.budget.limitMs === null ? `Budget: unlimited; ${state.budget.accumulatedMs}ms active time recorded` : `Budget: ${Math.max(0, state.budget.limitMs - state.budget.accumulatedMs)}ms remaining (legacy; manual resume upgrades)`,
|
|
29
35
|
`Usage: ${state.budget.usage.tokens ?? "unknown"} tokens; ${state.budget.usage.usd === null ? "unknown" : "$" + state.budget.usage.usd} cost (${state.budget.usage.completeness})`,
|
|
30
36
|
action === "logs" ? "" : "Native goal completion is not final proof; verify, review, task state, and receipt are still required."
|
|
31
37
|
].filter((line) => line.length > 0).join("\n");
|
|
@@ -35,12 +41,13 @@ function data(action, result) {
|
|
|
35
41
|
action,
|
|
36
42
|
runId: result.state.runId,
|
|
37
43
|
state: result.state,
|
|
44
|
+
recovery: describeRunRecovery(result.state),
|
|
38
45
|
message: result.message,
|
|
39
46
|
text: textFor(action, result.state, result.message)
|
|
40
47
|
};
|
|
41
48
|
}
|
|
42
49
|
function stateData(action, state, message) {
|
|
43
|
-
return { action, runId: state.runId, state, message, text: textFor(action, state, message) };
|
|
50
|
+
return { action, runId: state.runId, state, recovery: describeRunRecovery(state), message, text: textFor(action, state, message) };
|
|
44
51
|
}
|
|
45
52
|
async function goalFromArgs(args, root) {
|
|
46
53
|
if (args.goal !== undefined && args.goalFile !== undefined)
|
|
@@ -99,7 +106,7 @@ export async function runRunCommand(options) {
|
|
|
99
106
|
return okEnvelope("RUN_LOGS", { ...stateData(action, state, `Read ${events.length} event(s) for run ${runId}.`), events });
|
|
100
107
|
}
|
|
101
108
|
if (action === "resume")
|
|
102
|
-
return okEnvelope("RUN_RESUMED", data(action, await options.service.resume(runId)));
|
|
109
|
+
return okEnvelope("RUN_RESUMED", data(action, await options.service.resume(runId, options.args.retryApprovalIds)));
|
|
103
110
|
if (action === "stop")
|
|
104
111
|
return okEnvelope("RUN_STOP_REQUESTED", data(action, await options.service.stop(runId, options.args.answer ?? "user requested stop")));
|
|
105
112
|
const questionId = options.args.questionId;
|
|
@@ -9,7 +9,8 @@ import { mergeConfigLayers } from "../../../runtime/src/config/merge.js";
|
|
|
9
9
|
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
10
10
|
import { localStatePaths, readPrivateFile } from "../../../runtime/src/security/permissions.js";
|
|
11
11
|
import { FileRunRepository } from "../../../runtime/src/run/service.js";
|
|
12
|
-
import { readRunPolicy, runRuntimeHash } from "../../../runtime/src/run/policy.js";
|
|
12
|
+
import { readRunPolicy, renewRunPolicy, runRuntimeHash } from "../../../runtime/src/run/policy.js";
|
|
13
|
+
import { CLI_VERSION } from "./version.js";
|
|
13
14
|
import { calculateTrustBinding, FileTrustStore } from "../../../runtime/src/security/trust.js";
|
|
14
15
|
export const DEFAULT_CONFIG = {
|
|
15
16
|
schemaVersion: 3,
|
|
@@ -83,6 +84,10 @@ export async function loadEffectiveConfig(root, scope, projectOverride) {
|
|
|
83
84
|
config: run.policy.config
|
|
84
85
|
}]);
|
|
85
86
|
}
|
|
87
|
+
return await loadRepositoryConfig(root, scope, projectOverride);
|
|
88
|
+
}
|
|
89
|
+
/** Read the real config layers when checking renewal, without consulting the run overlay. */
|
|
90
|
+
async function loadRepositoryConfig(root, scope, projectOverride) {
|
|
86
91
|
const home = process.env.AGENT_OPS_HOME ?? homedir();
|
|
87
92
|
const userPath = join(home, ".agent-ops", "config.json");
|
|
88
93
|
const projectPath = join(scope === "user" ? root : projectConfigRoot(root), ".agent-ops", "config.json");
|
|
@@ -212,7 +217,7 @@ export async function repositoryTrust(root, config, cliVersion) {
|
|
|
212
217
|
}
|
|
213
218
|
}
|
|
214
219
|
/** A run-private capability is usable only by its registered checkout or proof group. */
|
|
215
|
-
export async function runPolicyContext(root) {
|
|
220
|
+
export async function runPolicyContext(root, renew = false) {
|
|
216
221
|
let common;
|
|
217
222
|
try {
|
|
218
223
|
common = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim();
|
|
@@ -250,10 +255,23 @@ export async function runPolicyContext(root) {
|
|
|
250
255
|
throw new AgentOpsError("RUN_WORKER_STALE", "Run policy requires the current registered worker generation or proof process.");
|
|
251
256
|
if (saved.status !== "active" || saved.disableRestart || saved.awaitingResume)
|
|
252
257
|
throw new AgentOpsError("RUN_POLICY_DISABLED", "Run execution policy is inactive; explicit resume is required.");
|
|
253
|
-
const
|
|
258
|
+
const state = renew ? await ensureRunPolicy(repository, saved) : saved;
|
|
259
|
+
const policy = await readRunPolicy(state, await runRuntimeHash());
|
|
254
260
|
if (policy === null)
|
|
255
261
|
return null;
|
|
256
|
-
return { repository, state
|
|
262
|
+
return { repository, state, policy, worker };
|
|
263
|
+
}
|
|
264
|
+
/** Only active execution paths call this; status, installation and recovery reads never renew. */
|
|
265
|
+
export async function ensureRunPolicy(repository, state) {
|
|
266
|
+
if (state.policyBinding === undefined || Date.parse(state.policyBinding.expiresAt) > Date.now())
|
|
267
|
+
return state;
|
|
268
|
+
return await renewRunPolicy(repository, state.runId, { authority: async () => {
|
|
269
|
+
const config = (await loadRepositoryConfig(state.root, "project")).config;
|
|
270
|
+
const baseTrustBinding = await repositoryTrustBinding(state.root, config, CLI_VERSION);
|
|
271
|
+
const paths = localStatePaths(process.env.AGENT_OPS_HOME ?? homedir());
|
|
272
|
+
const trusted = (await new FileTrustStore(paths.trustStore, paths.anchorDirectory).status(baseTrustBinding)).status === "TRUSTED";
|
|
273
|
+
return { config, baseTrustBinding, trusted, runtimeHash: await runRuntimeHash() };
|
|
274
|
+
} });
|
|
257
275
|
}
|
|
258
276
|
export async function repositoryTrustBinding(root, config, cliVersion) {
|
|
259
277
|
return await calculateTrustBinding({
|
|
@@ -5,7 +5,7 @@ import { rm } from "node:fs/promises";
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import { collectOfficeBuilding } from "../../../runtime/src/office/collect.js";
|
|
8
|
-
import { claimOffice, createOfficeServer, ensureOffice, legacyOfficeHome, officeRecordPath, readLiveOffice,
|
|
8
|
+
import { claimOffice, createOfficeServer, ensureOffice, legacyOfficeHome, officeRecordPath, readLiveOffice, readOfficeRecord, officeUrl, userOfficeHome } from "../../../runtime/src/office/server.js";
|
|
9
9
|
import { registerOfficeRepo } from "../../../runtime/src/office/repos.js";
|
|
10
10
|
import { recordOfficeSession, recordOfficeSessionPhase } from "../../../runtime/src/office/sessions.js";
|
|
11
11
|
import { resolveCheckouts } from "../../../runtime/src/parallel/service.js";
|
|
@@ -254,7 +254,6 @@ export async function serveOffice(cwd, onUrl = () => { }, openBrowser) {
|
|
|
254
254
|
}, 15_000);
|
|
255
255
|
await idle;
|
|
256
256
|
clearInterval(timer);
|
|
257
|
-
await releaseOffice(home, record);
|
|
258
257
|
await office.close();
|
|
259
258
|
}
|
|
260
259
|
/** Reuse the user's live server or start one under launchd; null where launchd is unavailable. */
|
|
@@ -267,10 +266,13 @@ export async function ensureBackgroundOffice(cwd, launchd = new LaunchdControlle
|
|
|
267
266
|
const live = await readLiveOffice(home);
|
|
268
267
|
if (live !== null)
|
|
269
268
|
return officeUrl(live);
|
|
269
|
+
let previousUrl = null;
|
|
270
270
|
let started = false;
|
|
271
271
|
const args = [entry, mainRoot];
|
|
272
272
|
if (!launchd.supported) {
|
|
273
273
|
const url = await ensureOffice(home, async () => {
|
|
274
|
+
const previous = await readOfficeRecord(home);
|
|
275
|
+
previousUrl = previous === null ? null : officeUrl(previous);
|
|
274
276
|
started = true;
|
|
275
277
|
await new Promise((resolve, reject) => {
|
|
276
278
|
const child = spawn(process.execPath, args, { cwd: mainRoot, detached: true, stdio: "ignore", windowsHide: true });
|
|
@@ -278,7 +280,7 @@ export async function ensureBackgroundOffice(cwd, launchd = new LaunchdControlle
|
|
|
278
280
|
child.once("spawn", () => { child.unref(); resolve(); });
|
|
279
281
|
});
|
|
280
282
|
});
|
|
281
|
-
if (started && openBrowser !== undefined)
|
|
283
|
+
if (started && url !== previousUrl && openBrowser !== undefined)
|
|
282
284
|
void openBrowser(url).catch(() => undefined);
|
|
283
285
|
return url;
|
|
284
286
|
}
|
|
@@ -289,6 +291,8 @@ export async function ensureBackgroundOffice(cwd, launchd = new LaunchdControlle
|
|
|
289
291
|
cwd: mainRoot, pathEnvironment: process.env.PATH
|
|
290
292
|
});
|
|
291
293
|
const url = await ensureOffice(home, async () => {
|
|
294
|
+
const previous = await readOfficeRecord(home);
|
|
295
|
+
previousUrl = previous === null ? null : officeUrl(previous);
|
|
292
296
|
started = true;
|
|
293
297
|
// The descriptor write anchors on this directory, so it must exist first.
|
|
294
298
|
await ensurePrivateDirectory(privateDirectory, home.anchor);
|
|
@@ -297,7 +301,7 @@ export async function ensureBackgroundOffice(cwd, launchd = new LaunchdControlle
|
|
|
297
301
|
await launchd.bootout(descriptor).catch(() => { });
|
|
298
302
|
await launchd.bootstrap(descriptor);
|
|
299
303
|
});
|
|
300
|
-
if (started && openBrowser !== undefined)
|
|
304
|
+
if (started && url !== previousUrl && openBrowser !== undefined)
|
|
301
305
|
void openBrowser(url).catch(() => undefined);
|
|
302
306
|
return url;
|
|
303
307
|
}
|
|
@@ -4,13 +4,13 @@ import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
|
4
4
|
import { nativeProcessIdentity, NativeRunTransport } from "../../../runtime/src/run/transport.js";
|
|
5
5
|
import { ClaudeGoalHost } from "../../../runtime/src/run/hosts/claude.js";
|
|
6
6
|
import { CodexGoalHost } from "../../../runtime/src/run/hosts/codex.js";
|
|
7
|
-
import {
|
|
7
|
+
import { remainingRunTimeMs } from "../../../runtime/src/run/scheduler.js";
|
|
8
8
|
import { runPolicyContext } from "./context.js";
|
|
9
9
|
/** Native-issued proof commands use the same dormant group as final evidence. */
|
|
10
10
|
export async function runOwnedLocalProof(argv) {
|
|
11
11
|
if (!["verify", "review", "batch"].includes(argv[0] ?? "") || argv.includes("--help") || process.env.AGENT_OPS_RUN_PROOF_PID !== undefined)
|
|
12
12
|
return null;
|
|
13
|
-
const context = await runPolicyContext(process.cwd());
|
|
13
|
+
const context = await runPolicyContext(process.cwd(), true);
|
|
14
14
|
if (context === null)
|
|
15
15
|
return null;
|
|
16
16
|
const { repository, state, worker } = context;
|
|
@@ -22,7 +22,7 @@ export async function runOwnedLocalProof(argv) {
|
|
|
22
22
|
if (existing.processAlive)
|
|
23
23
|
throw new AgentOpsError("RUN_WORKER_PROOF_ACTIVE", "A registered proof command is already running for this worker.");
|
|
24
24
|
}
|
|
25
|
-
const remaining =
|
|
25
|
+
const remaining = remainingRunTimeMs(state);
|
|
26
26
|
if (remaining <= 0)
|
|
27
27
|
throw new AgentOpsError("RUN_BUDGET_EXHAUSTED", "No active run time remains for local proof.");
|
|
28
28
|
const cli = fileURLToPath(new URL("./bin.js", import.meta.url));
|
|
@@ -34,7 +34,7 @@ export async function runOwnedLocalProof(argv) {
|
|
|
34
34
|
process.kill(process.platform === "win32" ? child.pid : -child.pid, "SIGKILL");
|
|
35
35
|
}
|
|
36
36
|
catch { } };
|
|
37
|
-
const deadline = setTimeout(kill, remaining);
|
|
37
|
+
const deadline = Number.isFinite(remaining) ? setTimeout(kill, remaining) : undefined;
|
|
38
38
|
try {
|
|
39
39
|
const identity = child.pid === undefined ? null : await nativeProcessIdentity(child.pid);
|
|
40
40
|
if (identity === null)
|
|
@@ -9,7 +9,6 @@ import { FileTrustStore } from "../../../runtime/src/security/trust.js";
|
|
|
9
9
|
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
10
10
|
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
11
11
|
import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
|
|
12
|
-
import { registeredRunProofRunner } from "../../../runtime/src/verify/spawn.js";
|
|
13
12
|
import { loadEffectiveConfig, repositoryTrust, runPolicyContext, repositoryTrustBinding } from "./context.js";
|
|
14
13
|
import { CLI_VERSION } from "./version.js";
|
|
15
14
|
// Shared by the CLI and the hook entry point: Claude Code runs hooks through
|
|
@@ -84,7 +83,6 @@ export function worktreeDependencies() {
|
|
|
84
83
|
...(noChangePaths === undefined ? {} : { noChangePaths }),
|
|
85
84
|
loadConfig: async () => (await loadEffectiveConfig(root, "project")).config
|
|
86
85
|
} }),
|
|
87
|
-
processRunner: registeredRunProofRunner(),
|
|
88
86
|
runSetup: async (cwd, step) => await new Promise((resolve) => {
|
|
89
87
|
execFile(step.command, [...step.args], {
|
|
90
88
|
cwd,
|
|
@@ -18,9 +18,9 @@ import { NativeRunTransport } from "../../../runtime/src/run/transport.js";
|
|
|
18
18
|
import { ClaudeGoalHost } from "../../../runtime/src/run/hosts/claude.js";
|
|
19
19
|
import { CodexGoalHost } from "../../../runtime/src/run/hosts/codex.js";
|
|
20
20
|
import { RunSupervisor } from "../../../runtime/src/run/supervisor.js";
|
|
21
|
-
import { activeWallTimeMs } from "../../../runtime/src/run/scheduler.js";
|
|
21
|
+
import { activeWallTimeMs, remainingRunTimeMs } from "../../../runtime/src/run/scheduler.js";
|
|
22
22
|
import { bindRunPolicy, readRunPolicy, runRuntimeHash, runPolicyAssessment } from "../../../runtime/src/run/policy.js";
|
|
23
|
-
import { repositoryTrustBinding } from "./context.js";
|
|
23
|
+
import { repositoryTrustBinding, ensureRunPolicy } from "./context.js";
|
|
24
24
|
import { CLI_VERSION } from "./version.js";
|
|
25
25
|
import { calculateConfigHash, canonicalJson } from "../../../runtime/src/config/hash.js";
|
|
26
26
|
import { discoverPendingPolicyTransition, renewBoundPolicyTransition, currentPolicyArtifact } from "../../../runtime/src/run/policy-transition.js";
|
|
@@ -30,6 +30,7 @@ import { RunControlService } from "../../../runtime/src/run/controls.js";
|
|
|
30
30
|
import { recordRunPhase } from "../../../runtime/src/run/phase.js";
|
|
31
31
|
/** Worktree construction happens before a native lease exists; it must never write global trust. */
|
|
32
32
|
export async function runWorktreeDependencies(deps, state) {
|
|
33
|
+
state = await ensureRunPolicy(new FileRunRepository(join(state.commonDir, "agent-ops/runs"), state.commonDir), state);
|
|
33
34
|
const policy = await readRunPolicy(state, await runRuntimeHash());
|
|
34
35
|
if (policy === null || (await trustStore().status(policy.baseTrustBinding)).status !== "TRUSTED")
|
|
35
36
|
throw new AgentOpsError("RUN_REPO_UNTRUSTED", "Run worktree setup requires the original trusted repository and current scoped policy.");
|
|
@@ -40,7 +41,9 @@ export async function runWorktreeDependencies(deps, state) {
|
|
|
40
41
|
if (index < 0)
|
|
41
42
|
throw new AgentOpsError("RUN_SETUP_UNAUTHORIZED", "Setup must select an exact fixed policy capability.");
|
|
42
43
|
const repository = new FileRunRepository(join(state.commonDir, "agent-ops/runs"), state.commonDir);
|
|
43
|
-
const current = (await repository.read(state.runId));
|
|
44
|
+
const current = await ensureRunPolicy(repository, (await repository.read(state.runId)));
|
|
45
|
+
if (current.policyBinding?.configHash !== state.policyBinding.configHash)
|
|
46
|
+
throw new AgentOpsError("RUN_SETUP_STALE", "Policy capability changed after setup was selected.");
|
|
44
47
|
const coordinator = current.workers.find(w => w.workerId === current.coordinatorId);
|
|
45
48
|
const command = canonicalJson(step), resource = canonicalJson({ checkout: root, capabilityId: "setup:" + index });
|
|
46
49
|
const approved = await runPolicyAssessment(current, policy, "setup:" + index);
|
|
@@ -49,11 +52,11 @@ export async function runWorktreeDependencies(deps, state) {
|
|
|
49
52
|
command, resource, commandDigest: sha256(command), resourceDigest: sha256(resource), reason: approved?.reason ?? "The original user trust authorizes this fixed repository setup capability in an isolated checkout.",
|
|
50
53
|
result: "auto-approved", source: approved === null ? "repo-trust" : "coordinator-policy-review", policyArtifactDigest: current.policyBinding.artifactDigest,
|
|
51
54
|
policyConfigHash: current.policyBinding.configHash, runtimeHash: current.policyBinding.runtimeHash, expiresAt: current.policyBinding.expiresAt });
|
|
52
|
-
const remaining =
|
|
55
|
+
const remaining = remainingRunTimeMs(current);
|
|
53
56
|
if (remaining <= 0)
|
|
54
57
|
throw new AgentOpsError("RUN_BUDGET_EXHAUSTED", "No active time remains for setup.");
|
|
55
58
|
const child = spawn(process.execPath, [fileURLToPath(new URL("./run-step-entry.js", import.meta.url)),
|
|
56
|
-
fileURLToPath(new URL("./run-setup-entry.js", import.meta.url)), state.commonDir, state.runId, root, String(index),
|
|
59
|
+
fileURLToPath(new URL("./run-setup-entry.js", import.meta.url)), state.commonDir, state.runId, root, String(index), current.policyBinding.artifactDigest], { cwd: root, env: process.env, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
|
|
57
60
|
let output = "";
|
|
58
61
|
const completion = new Promise((resolve, reject) => { child.once("error", reject); child.once("close", resolve); });
|
|
59
62
|
const kill = () => { if (child.pid !== undefined)
|
|
@@ -70,7 +73,7 @@ export async function runWorktreeDependencies(deps, state) {
|
|
|
70
73
|
if (identity === null)
|
|
71
74
|
throw new AgentOpsError("RUN_PROCESS_IDENTITY_REQUIRED", "Cannot register setup process identity.");
|
|
72
75
|
await repository.mutate(state.runId, saved => {
|
|
73
|
-
if (saved.status !== "active" || saved.disableRestart || saved.proofProcess != null || saved.policyBinding?.artifactDigest !==
|
|
76
|
+
if (saved.status !== "active" || saved.disableRestart || saved.proofProcess != null || saved.policyBinding?.artifactDigest !== current.policyBinding.artifactDigest)
|
|
74
77
|
throw new AgentOpsError("RUN_SETUP_STALE", "Stop or another proof process raced setup registration.");
|
|
75
78
|
return { ...saved, proofProcess: { processId: child.pid, processIdentity: identity } };
|
|
76
79
|
});
|
|
@@ -206,7 +209,7 @@ export async function productionRunContext(cwd, options = {}) {
|
|
|
206
209
|
throw cause;
|
|
207
210
|
}
|
|
208
211
|
};
|
|
209
|
-
const resume = async (runId) => {
|
|
212
|
+
const resume = async (runId, retryApprovalIds = [], upgradeBudget = true) => {
|
|
210
213
|
const before = await service.status(runId);
|
|
211
214
|
const runtimeHash = await runRuntimeHash();
|
|
212
215
|
const policy = await readRunPolicy(before, before.policyBinding?.runtimeHash ?? runtimeHash, Date.now(), true);
|
|
@@ -233,7 +236,7 @@ export async function productionRunContext(cwd, options = {}) {
|
|
|
233
236
|
if (exists === null || (await collectChangeSurface(gitRunner(worker.worktree))).paths.length > 0)
|
|
234
237
|
throw new AgentOpsError("RUN_RECOVERY_DIRTY", "Commit or preserve every writer checkout before explicit resume.");
|
|
235
238
|
}
|
|
236
|
-
const result = await service.resume(runId);
|
|
239
|
+
const result = await service.resume(runId, retryApprovalIds, upgradeBudget);
|
|
237
240
|
try {
|
|
238
241
|
if (pending !== null)
|
|
239
242
|
await renewPendingRunPolicy(repository, runId, pending);
|
|
@@ -308,7 +311,7 @@ export async function productionRunContext(cwd, options = {}) {
|
|
|
308
311
|
await writePrivateFile(join(state.commonDir, "agent-ops", "runs", state.runId, "answers", sha256(questionId) + ".json"), JSON.stringify({ questionId, answer: answer.trim() }), state.commonDir);
|
|
309
312
|
const result = await service.respond(runId, questionId, answer);
|
|
310
313
|
try {
|
|
311
|
-
const resumed = await resume(runId);
|
|
314
|
+
const resumed = await resume(runId, [], false);
|
|
312
315
|
return { ...result, state: resumed.state };
|
|
313
316
|
}
|
|
314
317
|
catch (cause) {
|