@kylecheng3146/agent-ops 0.9.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 +58 -14
- package/dist/packages/cli/src/bin.js +10 -3
- package/dist/packages/cli/src/cli.js +30 -6
- package/dist/packages/cli/src/commands/advance.js +2 -1
- package/dist/packages/cli/src/commands/escapes.js +109 -0
- package/dist/packages/cli/src/commands/parallel.js +1 -1
- package/dist/packages/cli/src/commands/review-show.js +1 -1
- package/dist/packages/cli/src/commands/review.js +63 -11
- package/dist/packages/cli/src/commands/run.js +10 -3
- package/dist/packages/cli/src/commands/task.js +41 -3
- package/dist/packages/cli/src/commands/update.js +16 -4
- package/dist/packages/cli/src/commands/verify.js +4 -1
- package/dist/packages/cli/src/context.js +22 -4
- package/dist/packages/cli/src/debug-intake.js +1 -1
- 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/escape/link.js +317 -0
- package/dist/runtime/src/escape/origin.js +20 -0
- package/dist/runtime/src/escape/rules.js +252 -0
- package/dist/runtime/src/fs/temporary.js +10 -0
- package/dist/runtime/src/install/harness.js +7 -4
- package/dist/runtime/src/logging/local-log.js +44 -1
- 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/manage.js +51 -1
- package/dist/runtime/src/parallel/receipt.js +3 -1
- package/dist/runtime/src/parallel/service.js +5 -1
- package/dist/runtime/src/review/execute.js +20 -12
- package/dist/runtime/src/review/packet.js +20 -7
- package/dist/runtime/src/review/probe.js +3 -2
- package/dist/runtime/src/review/runner.js +26 -3
- package/dist/runtime/src/review/slots.js +1 -1
- 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/schema/validate.js +6 -1
- package/dist/runtime/src/security/permissions.js +85 -0
- package/dist/runtime/src/surface/discovery.js +79 -2
- package/dist/runtime/src/surface/schema.js +43 -2
- package/dist/runtime/src/task/completion.js +20 -0
- package/dist/runtime/src/task/contract.js +3 -1
- 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 +59 -16
- package/dist/runtime/src/task/service.js +58 -6
- package/dist/runtime/src/verify/acceptance-replay.js +24 -11
- package/docs/en/guides/configuration.md +24 -5
- package/docs/en/guides/task-debug.md +26 -2
- package/docs/en/spec/acceptance-and-evidence.md +104 -2
- 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 +20 -4
- package/docs/zh-TW/guides/task-debug.md +19 -2
- package/docs/zh-TW/spec/acceptance-and-evidence.md +84 -4
- 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/schemas/task.schema.json +8 -1
|
@@ -30,6 +30,35 @@ async function jsonFile(path) {
|
|
|
30
30
|
throw new AgentOpsError("TASK_PAYLOAD_INVALID", "Invalid JSON payload.");
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
async function escapeOrigin(args, service) {
|
|
34
|
+
return args.escapeOf === undefined ? undefined : await service.resolveOrigin({ kind: "escape-of", commit: args.escapeOf,
|
|
35
|
+
label: args.escapeLabel, ...(args.escapeReceipt === undefined ? {} : { receipt: args.escapeReceipt }),
|
|
36
|
+
...(args.escapeFix === undefined ? {} : { fix: args.escapeFix }), ...(args.escapePromote === undefined ? {} : { promote: args.escapePromote }) });
|
|
37
|
+
}
|
|
38
|
+
/** Link state, label and blame hints are printed; the ledger record is committed with the fix. */
|
|
39
|
+
async function withEscape(envelope, escape, service) {
|
|
40
|
+
if (escape === undefined || envelope.data === null)
|
|
41
|
+
return envelope;
|
|
42
|
+
const { link, label, agentLabel, facts, proof } = escape.record;
|
|
43
|
+
const proposal = proof?.status === "check" && proof.check !== null ? await service.escapeConfigProposal(proof.check) : undefined;
|
|
44
|
+
const lines = [
|
|
45
|
+
`Escape of ${escape.record.escapeOf}: ${link.state}${link.state === "unresolved" ? ` (${link.reason})` : ""}` +
|
|
46
|
+
`${link.state === "gated" ? ` via receipt ${link.receipt} (${link.matchedBy})` : ""}; label ${label}${label === agentLabel ? "" : ` (agent proposed ${agentLabel})`}.`,
|
|
47
|
+
...facts.map(fact => `- ${fact.path}: ${fact.surface}`),
|
|
48
|
+
...(proof === undefined ? [] : [proof.status === "proven"
|
|
49
|
+
? `Proof: rule ${proof.rule} replays at the escaped commit; backtest ${proof.backtest.fires}/${proof.backtest.commits} commits (${proof.backtest.rate}).`
|
|
50
|
+
: proof.status === "unproven" ? `Proof: ${proof.code} - ${proof.reason} Finish refuses this until a tracked rule proves it.`
|
|
51
|
+
: proof.status === "lesson" ? `Lesson (unproven, does not block): ${proof.reason}`
|
|
52
|
+
: proof.check === null ? "Proof: ESCAPE_RULE_UNPROVEN - name the red-to-green check with --escape-promote <runnerId:checkId>."
|
|
53
|
+
: `Proof: promoted check ${proof.check} must be a proven redCheckId of this task. Config proposal (not written):\n${JSON.stringify(proposal, null, 2)}`]),
|
|
54
|
+
`Ledger record: ${escape.path} (commit it with the fix).`,
|
|
55
|
+
escape.blame.length === 0 ? "Blame hints: none (the fix only adds lines; an omission has no author)."
|
|
56
|
+
: `Blame hints (a hint only): ${escape.blame.map(hint => `${hint.commit.slice(0, 12)} (${hint.lines} line${hint.lines === 1 ? "" : "s"})`).join(", ")}`
|
|
57
|
+
];
|
|
58
|
+
return { ...envelope, data: { ...envelope.data, escape: { ...escape.record, blame: escape.blame, path: escape.path,
|
|
59
|
+
...(proposal === undefined ? {} : { configProposal: proposal }) },
|
|
60
|
+
text: `${envelope.data.text ?? ""}${envelope.data.text?.endsWith("\n") === false ? "\n" : ""}${lines.join("\n")}\n` } };
|
|
61
|
+
}
|
|
33
62
|
/** An unreadable surface file is a missing surface, not a generic payload error. */
|
|
34
63
|
async function surfaceFile(path) {
|
|
35
64
|
try {
|
|
@@ -105,6 +134,7 @@ export async function runTaskCommand(options) {
|
|
|
105
134
|
const worktree = sessionId === undefined || options.sessionWorktree === undefined
|
|
106
135
|
? undefined
|
|
107
136
|
: await options.sessionWorktree(sessionId);
|
|
137
|
+
const origin = await escapeOrigin(options.args, worktree?.service ?? options.service);
|
|
108
138
|
const record = await (worktree?.service ?? options.service).create({
|
|
109
139
|
title: options.args.title,
|
|
110
140
|
...(options.args.goal === undefined ? {} : { goal: options.args.goal }),
|
|
@@ -122,19 +152,21 @@ export async function runTaskCommand(options) {
|
|
|
122
152
|
: { parentTaskId: options.args.parentTaskId }),
|
|
123
153
|
...(sessionId === undefined ? {} : { sessionId }),
|
|
124
154
|
...(options.args.surfaceFile === undefined ? {} : { surface: await surfaceFile(options.args.surfaceFile) }),
|
|
155
|
+
...(origin === undefined ? {} : { origin }),
|
|
125
156
|
requireSurface: true
|
|
126
157
|
});
|
|
158
|
+
const escape = origin === undefined ? undefined : await (worktree?.service ?? options.service).declareEscape(record.task.id);
|
|
127
159
|
// An explicit CLI DEBUG intake may be the only start signal on hosts
|
|
128
160
|
// without a prompt hook (notably agy). The native session owns the clock;
|
|
129
161
|
// foregroundStart is idempotent for a repeated hook or command.
|
|
130
162
|
if (options.args.debug && sessionId !== undefined) {
|
|
131
163
|
await (worktree?.service ?? options.service).debugService().foregroundStart(record.task.id, sessionId);
|
|
132
164
|
}
|
|
133
|
-
const created = taskEnvelope(action, "TASK_CREATED", sessionId === undefined
|
|
165
|
+
const created = await withEscape(taskEnvelope(action, "TASK_CREATED", sessionId === undefined
|
|
134
166
|
? `Created task ${record.task.id}.`
|
|
135
167
|
: record.task.parentTaskId === undefined
|
|
136
168
|
? `Created and attached task ${record.task.id}.`
|
|
137
|
-
: `Created subtask ${record.task.id}; the session stays on the top of its task tree.`, record);
|
|
169
|
+
: `Created subtask ${record.task.id}; the session stays on the top of its task tree.`, record), escape, worktree?.service ?? options.service);
|
|
138
170
|
if (worktree === undefined || created.data === null)
|
|
139
171
|
return created;
|
|
140
172
|
return {
|
|
@@ -157,6 +189,10 @@ export async function runTaskCommand(options) {
|
|
|
157
189
|
}
|
|
158
190
|
if (action === "debug") {
|
|
159
191
|
const id = requireTaskId(options.args), service = options.service.debugService();
|
|
192
|
+
if (options.args.debugAction === "origin") {
|
|
193
|
+
const declared = await service.origin(id, await jsonFile(options.args.payloadFile));
|
|
194
|
+
return await withEscape(taskEnvelope(action, "TASK_DEBUG_ORIGIN_DECLARED", "Origin bound into the contract; re-verify, re-record the confirmed hypothesis, then review.", declared.record), declared.escape, options.service);
|
|
195
|
+
}
|
|
160
196
|
switch (options.args.debugAction) {
|
|
161
197
|
case "hypothesis":
|
|
162
198
|
await service.hypothesis(id, await jsonFile(options.args.payloadFile));
|
|
@@ -191,13 +227,15 @@ export async function runTaskCommand(options) {
|
|
|
191
227
|
const payload = options.args.criteriaFile === undefined ? undefined : await jsonFile(options.args.criteriaFile);
|
|
192
228
|
if (payload !== undefined && !Array.isArray(payload))
|
|
193
229
|
throw new AgentOpsError("TASK_PAYLOAD_INVALID", "Criteria file must contain an array.");
|
|
230
|
+
const origin = await escapeOrigin(options.args, options.service);
|
|
194
231
|
const record = await options.service.revise(requireTaskId(options.args), {
|
|
195
232
|
expectedContractHash: options.args.expectedContract,
|
|
196
233
|
...(payload === undefined ? {} : { criteria: payload }),
|
|
197
234
|
...(options.args.surfaceFile === undefined ? {} : { surface: await surfaceFile(options.args.surfaceFile) }),
|
|
235
|
+
...(origin === undefined ? {} : { origin }),
|
|
198
236
|
reason: options.args.reason, ...(options.args.baseline === undefined ? {} : { baseline: options.args.baseline })
|
|
199
237
|
});
|
|
200
|
-
return taskEnvelope(action, "TASK_REVISED", "Contract revised; obtain new verification and fresh review.", record);
|
|
238
|
+
return await withEscape(taskEnvelope(action, "TASK_REVISED", "Contract revised; obtain new verification and fresh review.", record), origin === undefined ? undefined : await options.service.declareEscape(record.task.id), options.service);
|
|
201
239
|
}
|
|
202
240
|
if (action === "replan") {
|
|
203
241
|
const payload = await jsonFile(options.args.planFile);
|
|
@@ -8,6 +8,11 @@ import { okEnvelope } from "../output.js";
|
|
|
8
8
|
import { formatOperationPlan } from "../plan-output.js";
|
|
9
9
|
import { toPublicUpdatePlan } from "../public-plan.js";
|
|
10
10
|
import { formatTrustChange, planTrustGrant } from "./trust.js";
|
|
11
|
+
/** One plan line, so the user approves the worktrees together with the commands. */
|
|
12
|
+
export function worktreeSyncLine(worktrees) {
|
|
13
|
+
return worktrees.length === 0 ? "" :
|
|
14
|
+
`Session worktrees that receive this config and its trust: ${worktrees.join(", ")}\n`;
|
|
15
|
+
}
|
|
11
16
|
function repairedLines(plan) {
|
|
12
17
|
return plan.installation.repaired.map(({ path, reason }) => `repaired: ${path} (${reason.replace("-", " ")})`);
|
|
13
18
|
}
|
|
@@ -146,19 +151,21 @@ export async function runUpdateCommand(options) {
|
|
|
146
151
|
...(worktree === undefined ? {} : { worktree })
|
|
147
152
|
});
|
|
148
153
|
const trust = await trustChange(options, plan);
|
|
154
|
+
const worktrees = await options.sessionWorktrees?.list().catch(() => []) ?? [];
|
|
149
155
|
if (options.args.dryRun) {
|
|
150
156
|
return okEnvelope("UPDATE_PLAN_READY", {
|
|
151
157
|
applied: false,
|
|
152
158
|
plan: toPublicUpdatePlan(plan, trust),
|
|
153
159
|
message: "Update plan calculated; no files were changed.",
|
|
154
|
-
text: formatUpdatePlan(plan, trust)
|
|
160
|
+
text: formatUpdatePlan(plan, trust) + worktreeSyncLine(worktrees),
|
|
161
|
+
...(worktrees.length === 0 ? {} : { worktrees })
|
|
155
162
|
});
|
|
156
163
|
}
|
|
157
164
|
if (!options.args.yes && !options.isTTY) {
|
|
158
165
|
return updateError("UPDATE_CONFIRMATION_REQUIRED", "Non-interactive update requires --yes.", plan, trust);
|
|
159
166
|
}
|
|
160
167
|
if (!options.args.yes &&
|
|
161
|
-
!(await options.confirm(plan, trust))) {
|
|
168
|
+
!(await options.confirm(plan, trust, worktrees))) {
|
|
162
169
|
return updateError("UPDATE_CANCELLED", "Update was cancelled; no files were changed.", plan, trust);
|
|
163
170
|
}
|
|
164
171
|
await applyUpdatePlan(options.root, plan);
|
|
@@ -173,16 +180,21 @@ export async function runUpdateCommand(options) {
|
|
|
173
180
|
return updateError("UPDATE_TRUST_FAILED", "Update was applied, but repository trust was not granted. Run `agent-ops trust grant --yes`.", plan, trust, true);
|
|
174
181
|
}
|
|
175
182
|
}
|
|
183
|
+
// Fail-open: a worktree that cannot be synced keeps its old trust and says so via trust status.
|
|
184
|
+
const synced = worktrees.length === 0 ? [] : await options.sessionWorktrees?.sync().catch(() => []) ?? [];
|
|
185
|
+
const syncedLines = synced.length === 0 ? [] : [`Synced config and trust into session worktrees: ${synced.join(", ")}.`];
|
|
176
186
|
return okEnvelope("UPDATE_APPLIED", {
|
|
177
187
|
applied: true,
|
|
178
188
|
plan: toPublicUpdatePlan(plan, trust),
|
|
179
189
|
message: `Managed installation updated to ${plan.targetVersion}.`,
|
|
180
|
-
...(
|
|
190
|
+
...(synced.length === 0 ? {} : { worktrees: synced }),
|
|
191
|
+
...(plan.installation.repaired.length === 0 && synced.length === 0
|
|
181
192
|
? {}
|
|
182
193
|
: {
|
|
183
194
|
text: [
|
|
184
195
|
`Managed installation updated to ${plan.targetVersion}.`,
|
|
185
|
-
...repairedLines(plan)
|
|
196
|
+
...repairedLines(plan),
|
|
197
|
+
...syncedLines
|
|
186
198
|
].join("\n")
|
|
187
199
|
})
|
|
188
200
|
});
|
|
@@ -72,7 +72,10 @@ function formatReport(report) {
|
|
|
72
72
|
const lines = [
|
|
73
73
|
`Verification task ${safe(report.taskId)}: ${report.status}`,
|
|
74
74
|
`Scope: ${report.selection.reason} (${report.selection.fallback ? "fallback" : "bounded"})`,
|
|
75
|
-
...report.results.flatMap(formatResult)
|
|
75
|
+
...report.results.flatMap(formatResult),
|
|
76
|
+
// A timed-out runner step says which step and which limit; plain UNKNOWN left people guessing.
|
|
77
|
+
...(report.acceptance ?? []).flatMap(row => row.detail === undefined ? [] :
|
|
78
|
+
[`- ${row.status} ${safe(row.runnerId)} (${row.criterionId}, ${row.phase}): ${safe(row.detail)}`])
|
|
76
79
|
];
|
|
77
80
|
if (report.signal !== null) {
|
|
78
81
|
lines.push(`Signal: ${report.signal}`);
|
|
@@ -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({
|
|
@@ -171,7 +171,7 @@ export async function routeDebugPrompt(root, input, host = "claude") {
|
|
|
171
171
|
}
|
|
172
172
|
else
|
|
173
173
|
clockNote = " Native foreground timing is unsupported on this host; controlled operations remain evidence-timed and do not claim a complete active-time budget.";
|
|
174
|
-
return `DEBUG issue ${created.task.id} created in ${path}. Preserve the original goal. Investigate, record falsifiable hypotheses, revise provisional criteria to relevant behavioral regression, add the surface audit (task revise --surface-file) before the first repair, repair, verify, obtain two fresh independent reviews, then finish. A clean HEAD baseline is fixed automatically; do not ask permission for it or offer diagnosis-only as a fallback. Failure to reproduce alone is not a stop condition: continue discriminating checks within the budget. Defaults: 60 active minutes / 5 material repair candidates. No user-authored debug plan is required.${clockNote}`;
|
|
174
|
+
return `DEBUG issue ${created.task.id} created in ${path}. Preserve the original goal. Investigate, record falsifiable hypotheses, revise provisional criteria to relevant behavioral regression, add the surface audit (task revise --surface-file) before the first repair, repair, declare the origin (task debug --debug-action origin) once a hypothesis is confirmed, verify, obtain two fresh independent reviews, then finish. A clean HEAD baseline is fixed automatically; do not ask permission for it or offer diagnosis-only as a fallback. Failure to reproduce alone is not a stop condition: continue discriminating checks within the budget. Defaults: 60 active minutes / 5 material repair candidates. No user-authored debug plan is required.${clockNote}`;
|
|
175
175
|
}
|
|
176
176
|
export async function interruptDebugSession(root, sessionId, nativeInput) {
|
|
177
177
|
const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId)));
|
|
@@ -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) {
|