@kylecheng3146/agent-ops 0.11.1 → 0.11.3
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 +9 -1
- package/dist/packages/cli/src/args.js +68 -2
- package/dist/packages/cli/src/bin.js +11 -1
- package/dist/packages/cli/src/cli.js +29 -0
- package/dist/packages/cli/src/commands/advance.js +3 -2
- package/dist/packages/cli/src/commands/followup.js +74 -0
- package/dist/packages/cli/src/commands/parallel.js +4 -0
- package/dist/packages/cli/src/commands/review.js +15 -1
- package/dist/packages/cli/src/commands/run.js +1 -1
- package/dist/packages/cli/src/commands/task.js +8 -2
- package/dist/packages/cli/src/context.js +13 -9
- package/dist/packages/cli/src/criteria-review.js +8 -3
- package/dist/packages/cli/src/followup-deps.js +56 -0
- package/dist/packages/cli/src/run-deps.js +16 -5
- package/dist/packages/cli/src/run-entry.js +159 -64
- package/dist/runtime/src/followup/intake.js +75 -0
- package/dist/runtime/src/followup/process.js +273 -0
- package/dist/runtime/src/followup/receipt.js +116 -0
- package/dist/runtime/src/followup/store.js +133 -0
- package/dist/runtime/src/install/harness.js +1 -1
- package/dist/runtime/src/install/preauth.js +11 -4
- package/dist/runtime/src/parallel/finish.js +10 -2
- package/dist/runtime/src/review/attestation.js +45 -13
- package/dist/runtime/src/review/criteria.js +22 -5
- package/dist/runtime/src/review/followup.js +233 -0
- package/dist/runtime/src/review/report.js +8 -4
- package/dist/runtime/src/review/runner.js +30 -2
- package/dist/runtime/src/run/service.js +59 -6
- package/dist/runtime/src/run/usage.js +5 -2
- package/dist/runtime/src/security/permissions.js +16 -1
- package/dist/runtime/src/verify/acceptance-coverage.js +2 -2
- package/dist/runtime/src/verify/service.js +62 -2
- package/dist/runtime/src/verify/spawn.js +64 -2
- package/docs/en/guides/configuration.md +5 -0
- package/docs/en/guides/task-debug.md +36 -0
- package/docs/en/spec/acceptance-and-evidence.md +54 -0
- package/docs/en/spec/loop-engineering.md +36 -0
- package/docs/en/spec/review.md +37 -3
- package/docs/zh-TW/guides/configuration.md +3 -0
- package/docs/zh-TW/guides/task-debug.md +31 -0
- package/docs/zh-TW/spec/acceptance-and-evidence.md +39 -0
- package/docs/zh-TW/spec/loop-engineering.md +21 -0
- package/docs/zh-TW/spec/review.md +24 -4
- package/package.json +1 -1
- package/schemas/review-report.schema.json +2 -1
package/README.md
CHANGED
|
@@ -17,7 +17,9 @@ command behavior may change before 1.0.
|
|
|
17
17
|
Every new task, including DEBUG and native run parent/child tasks, waits for one
|
|
18
18
|
independent advisory review of its goal, criteria and assigned surface before
|
|
19
19
|
implementation. Read the suggestions, decide which to adopt, and record reasons
|
|
20
|
-
through `task revise`.
|
|
20
|
+
through `task revise`. The first revision that changes criteria, intent or
|
|
21
|
+
goal reruns the advisory once; surface-only revisions do not, and later
|
|
22
|
+
reruns are explicit (`review --criteria --rerun`).
|
|
21
23
|
|
|
22
24
|
The advisory has no time/token budget; explicit cancellation stops work. Genuine
|
|
23
25
|
execution errors report NOT_RUN and allow work. Detailed reports remain private;
|
|
@@ -521,6 +523,12 @@ as its first and only invocation, with `--output` instead of a shell redirect.
|
|
|
521
523
|
|
|
522
524
|
See [Configuration](docs/en/guides/configuration.md) for the full contract.
|
|
523
525
|
|
|
526
|
+
Retained nonblocking review findings and residual risks are collected privately
|
|
527
|
+
across linked worktrees. Use `agent-ops followup list` / `show`, metadata-only
|
|
528
|
+
`import`, reasoned `accept` / `defer`, and explicit `process <followup-id> --host
|
|
529
|
+
codex` for current-source revalidation and bounded repair. Observations need
|
|
530
|
+
`clarify`; resolution needs finding-specific proof. See [followup guidance](docs/en/guides/task-debug.md#retained-review-followups).
|
|
531
|
+
|
|
524
532
|
## Project principles
|
|
525
533
|
|
|
526
534
|
- Define verifiable success before making changes.
|
|
@@ -14,7 +14,8 @@ export const COMMAND_NAMES = [
|
|
|
14
14
|
"worktree",
|
|
15
15
|
"run",
|
|
16
16
|
"office",
|
|
17
|
-
"escapes"
|
|
17
|
+
"escapes",
|
|
18
|
+
"followup"
|
|
18
19
|
];
|
|
19
20
|
const COMMAND_SET = new Set(COMMAND_NAMES);
|
|
20
21
|
const SCOPES = new Set(["project", "user"]);
|
|
@@ -64,6 +65,8 @@ function parseHookTarget(option, value) {
|
|
|
64
65
|
return { harness: harness, surfaceId };
|
|
65
66
|
}
|
|
66
67
|
export function parseArgs(argv) {
|
|
68
|
+
if (argv[0] === "followup")
|
|
69
|
+
return parseFollowupArgs(argv.slice(1));
|
|
67
70
|
if (argv[0] === "run")
|
|
68
71
|
return parseRunArgs(argv.slice(1));
|
|
69
72
|
if (argv[0] === "task" && argv[1] === "debug")
|
|
@@ -945,10 +948,15 @@ function parseRunArgs(argv) {
|
|
|
945
948
|
args.wait = true;
|
|
946
949
|
continue;
|
|
947
950
|
}
|
|
948
|
-
if (!["--host", "--goal-file", "--time-budget", "--jobs", "--question-id", "--answer", "--answer-file", "--retry-approval"].includes(token))
|
|
951
|
+
if (!["--host", "--goal-file", "--time-budget", "--jobs", "--question-id", "--answer", "--answer-file", "--retry-approval", "--retry-delivery"].includes(token))
|
|
949
952
|
throw new CliArgumentError("CLI_UNKNOWN_OPTION", `Unknown run option: ${token}`, token);
|
|
950
953
|
const value = readOptionValue(argv, i++, token);
|
|
951
954
|
switch (token) {
|
|
955
|
+
case "--retry-delivery":
|
|
956
|
+
if (!/^task-[a-z0-9-]{1,128}$/u.test(value))
|
|
957
|
+
invalidValue(token, value);
|
|
958
|
+
args.retryDeliveryTaskId = value;
|
|
959
|
+
break;
|
|
952
960
|
case "--retry-approval":
|
|
953
961
|
if (!/^auth-[a-f0-9]{64}$/u.test(value) || args.retryApprovalIds?.includes(value))
|
|
954
962
|
invalidValue(token, value);
|
|
@@ -983,6 +991,8 @@ function parseRunArgs(argv) {
|
|
|
983
991
|
}
|
|
984
992
|
if (args.retryApprovalIds !== undefined && action !== "resume")
|
|
985
993
|
throw new CliArgumentError("CLI_INVALID_OPTION", "--retry-approval requires run resume.");
|
|
994
|
+
if (args.retryDeliveryTaskId !== undefined && (action !== "resume" || args.retryApprovalIds !== undefined))
|
|
995
|
+
throw new CliArgumentError("CLI_INVALID_OPTION", "--retry-delivery requires run resume without --retry-approval.");
|
|
986
996
|
if (action === "start") {
|
|
987
997
|
args.goal = positional;
|
|
988
998
|
if ((args.goal === undefined) === (args.goalFile === undefined))
|
|
@@ -1007,3 +1017,59 @@ function parseRunArgs(argv) {
|
|
|
1007
1017
|
}
|
|
1008
1018
|
return args;
|
|
1009
1019
|
}
|
|
1020
|
+
function parseFollowupArgs(argv) {
|
|
1021
|
+
const args = { command: "followup", profiles: [], dryRun: false, json: false, yes: false, rerun: false };
|
|
1022
|
+
if (argv.includes("--help") || argv.includes("-h"))
|
|
1023
|
+
return { ...args, command: "help", helpTopic: "followup", json: argv.includes("--json") };
|
|
1024
|
+
const actions = ["list", "show", "import", "sync", "clarify", "accept", "defer", "reopen", "process"];
|
|
1025
|
+
const action = argv[0] === undefined || argv[0].startsWith("-") ? "list" : argv[0];
|
|
1026
|
+
if (!actions.includes(action))
|
|
1027
|
+
invalidValue("followup action", action);
|
|
1028
|
+
args.action = action;
|
|
1029
|
+
const seen = new Set();
|
|
1030
|
+
for (let i = argv[0] === action ? 1 : 0; i < argv.length; i++) {
|
|
1031
|
+
const token = argv[i];
|
|
1032
|
+
if (!token.startsWith("-")) {
|
|
1033
|
+
if (args.followupId !== undefined)
|
|
1034
|
+
throw new CliArgumentError("CLI_UNEXPECTED_ARGUMENT", "Select exactly one followup identity.");
|
|
1035
|
+
if (!/^followup-[a-f0-9]{64}$/u.test(token))
|
|
1036
|
+
invalidValue("followup identity", token);
|
|
1037
|
+
args.followupId = token;
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
if (seen.has(token))
|
|
1041
|
+
duplicate(token);
|
|
1042
|
+
seen.add(token);
|
|
1043
|
+
if (token === "--json") {
|
|
1044
|
+
args.json = true;
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
if (!["--reason", "--finding-file", "--host", "--review-only-reason"].includes(token))
|
|
1048
|
+
throw new CliArgumentError("CLI_UNKNOWN_OPTION", `Unknown followup option: ${token}`, token);
|
|
1049
|
+
const value = readOptionValue(argv, i++, token);
|
|
1050
|
+
if (value.trim() === "" || value.includes("\0"))
|
|
1051
|
+
invalidValue(token, value);
|
|
1052
|
+
if (token === "--reason")
|
|
1053
|
+
args.reason = value;
|
|
1054
|
+
if (token === "--finding-file")
|
|
1055
|
+
args.findingFile = value;
|
|
1056
|
+
if (token === "--review-only-reason")
|
|
1057
|
+
args.reviewOnlyReason = value;
|
|
1058
|
+
if (token === "--host") {
|
|
1059
|
+
if (value !== "claude" && value !== "codex")
|
|
1060
|
+
invalidValue(token, value);
|
|
1061
|
+
args.host = value;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
const selectsItem = !["list", "import", "sync"].includes(action);
|
|
1065
|
+
if (selectsItem !== (args.followupId !== undefined))
|
|
1066
|
+
throw new CliArgumentError("CLI_MISSING_VALUE", "This action requires exactly one followup identity; list/import/sync take none.");
|
|
1067
|
+
if ((args.reason !== undefined && !["accept", "defer", "reopen", "clarify"].includes(action)) ||
|
|
1068
|
+
(args.findingFile !== undefined && action !== "clarify") ||
|
|
1069
|
+
((args.host !== undefined || args.reviewOnlyReason !== undefined) && action !== "process"))
|
|
1070
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Option does not apply to this followup action.");
|
|
1071
|
+
if ((["accept", "defer", "clarify"].includes(action) && args.reason === undefined) ||
|
|
1072
|
+
(action === "clarify" && args.findingFile === undefined) || (action === "process" && args.host === undefined))
|
|
1073
|
+
throw new CliArgumentError("CLI_MISSING_VALUE", "Accept/defer/clarify need --reason; clarify needs --finding-file; process needs --host.");
|
|
1074
|
+
return args;
|
|
1075
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { reviewCreatedCriteria, criteriaReviewStatus } from "./criteria-review.js";
|
|
2
|
+
import { reviewCreatedCriteria, reviewRevisedCriteria, criteriaReviewStatus } from "./criteria-review.js";
|
|
3
3
|
import { renderCriteriaReview } from "../../../runtime/src/review/criteria.js";
|
|
4
4
|
import { advancePhaseObserver, productionRunContext } from "./run-deps.js";
|
|
5
5
|
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
6
6
|
import { recordRunVerification } from "../../../runtime/src/run/verification.js";
|
|
7
7
|
import { runOwnedLocalProof } from "./owned-run-step.js";
|
|
8
8
|
import { runRunCommand } from "./commands/run.js";
|
|
9
|
+
import { runFollowupCommand } from "./commands/followup.js";
|
|
10
|
+
import { reconcileRepositoryFollowups } from "./followup-deps.js";
|
|
9
11
|
import { runOfficeCommand } from "./commands/office.js";
|
|
10
12
|
import { ensureBackgroundOffice, observeOfficeSession, officePhaseReporter, openOfficeBrowser, serveOffice } from "./office-entry.js";
|
|
11
13
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
@@ -259,6 +261,11 @@ else {
|
|
|
259
261
|
const root = args.scope === "user"
|
|
260
262
|
? process.env.AGENT_OPS_HOME ?? homedir()
|
|
261
263
|
: process.cwd();
|
|
264
|
+
if (args.scope !== "user" && !["init", "update", "uninstall", "followup", "escapes", "office"].includes(args.command)) {
|
|
265
|
+
const collected = await reconcileRepositoryFollowups(root);
|
|
266
|
+
for (const warning of collected.warnings)
|
|
267
|
+
process.stderr.write(`Warning: ${warning}\n`);
|
|
268
|
+
}
|
|
262
269
|
// Committed work is judged as a range by default; see defaultBase.
|
|
263
270
|
if ((args.command === "verify" || (args.command === "review" && args.action !== "show" && !args.criteriaReview)) && args.base === undefined && args.scope !== "user") {
|
|
264
271
|
const taskId = args.taskId;
|
|
@@ -279,6 +286,8 @@ else {
|
|
|
279
286
|
const context = await productionRunContext(root);
|
|
280
287
|
return await runRunCommand({ args, ...context, root: context.mainRoot, office: async () => await ensureBackgroundOffice(root, undefined, openOfficeBrowser) });
|
|
281
288
|
}
|
|
289
|
+
if (args.command === "followup")
|
|
290
|
+
return await runFollowupCommand({ args, cwd: root });
|
|
282
291
|
if (args.command === "escapes")
|
|
283
292
|
return await runEscapesCommand({ root });
|
|
284
293
|
if (args.command === "office") {
|
|
@@ -511,6 +520,7 @@ else {
|
|
|
511
520
|
service: taskService,
|
|
512
521
|
onTaskCreated: async (record, createdRoot) => await reviewCreatedCriteria(createdRoot ?? root, record.task),
|
|
513
522
|
criteriaStatus: async (record) => await criteriaReviewStatus(root, record.task),
|
|
523
|
+
onTaskRevised: async (record) => await reviewRevisedCriteria(root, record.task),
|
|
514
524
|
...(policyConfigHash === undefined ? {} : { policyConfigHash }),
|
|
515
525
|
...(sessionId === undefined ? {} : { sessionId }),
|
|
516
526
|
...(fromMain
|
|
@@ -31,6 +31,7 @@ Commands:
|
|
|
31
31
|
review Run an independent review or show recorded reports
|
|
32
32
|
run Start or control a supervised Claude/Codex native goal
|
|
33
33
|
office Open the local pixel office (Preview; opt-in)
|
|
34
|
+
followup Collect and process retained review findings and risk observations
|
|
34
35
|
escapes Report escaped bugs, gate catches and learned rules (read-only)
|
|
35
36
|
batch Verify and review a parent task's subtasks together
|
|
36
37
|
allow-stop Grant one fingerprint-bound completion-gate Stop permit (requires --session)
|
|
@@ -77,6 +78,30 @@ Options:
|
|
|
77
78
|
* rejected.
|
|
78
79
|
*/
|
|
79
80
|
export const COMMAND_HELP_TEXT = {
|
|
81
|
+
followup: `Usage: agent-ops followup [list] [--json]
|
|
82
|
+
agent-ops followup <show|reopen> <followup-id> [--json]
|
|
83
|
+
agent-ops followup <import|sync> [--json]
|
|
84
|
+
agent-ops followup <accept|defer> <followup-id> --reason <text> [--json]
|
|
85
|
+
agent-ops followup clarify <followup-id> --finding-file <json> --reason <text> [--json]
|
|
86
|
+
agent-ops followup process <followup-id> --host <claude|codex> [--review-only-reason <text>] [--json]
|
|
87
|
+
|
|
88
|
+
Receipt collection and history import are metadata-only: no model calls or repairs.
|
|
89
|
+
State is private under the Git common directory, shared by linked worktrees.
|
|
90
|
+
Text observations require a concrete nonblocking ReviewFinding JSON clarification
|
|
91
|
+
(severity, blocking=false, title, details, locations, evidence, recommendation,
|
|
92
|
+
criterionIds). Accept/defer require a reason; neither resolves the finding.
|
|
93
|
+
Reopen retains history. Process again to revalidate a selected candidate.
|
|
94
|
+
Process authorizes independent current-source assessment and a new bounded native
|
|
95
|
+
repair only when conclusively PRESENT. UNKNOWN stays unresolved. Native start
|
|
96
|
+
refusals retain the item and exact reason; macOS, clean main, checked-out target,
|
|
97
|
+
trust, run policy and native controls still apply. Run through the trusted outer
|
|
98
|
+
host as: env -u CODEX_SANDBOX_NETWORK_DISABLED agent-ops followup process ...
|
|
99
|
+
ABSENT closure requires an explicit justified review-only contract and two fresh
|
|
100
|
+
reviews. Reproducible bugs require corresponding behavioral red-to-green proof.
|
|
101
|
+
Closure records the exact commit and finding-specific evidence; generic PASS,
|
|
102
|
+
changed paths, completed run status, acceptance and deferral cannot resolve.
|
|
103
|
+
Collection warnings are advisory and retryable with followup import.
|
|
104
|
+
`,
|
|
80
105
|
escapes: `Usage: agent-ops escapes [--json]
|
|
81
106
|
|
|
82
107
|
Read-only Escape Loop report: misses per gate from the tracked
|
|
@@ -103,6 +128,7 @@ The URL carries an access token; do not share it.
|
|
|
103
128
|
agent-ops run [start] --goal-file <path> --host <claude|codex>
|
|
104
129
|
agent-ops run <status|logs|resume|stop> <run-id> [--json]
|
|
105
130
|
agent-ops run resume <run-id> [--retry-approval <auth-event-id>]... [--json]
|
|
131
|
+
agent-ops run resume <run-id> --retry-delivery <task-id> [--json]
|
|
106
132
|
agent-ops run respond <run-id> --question-id <id> <--answer <text>|--answer-file <path>> [--json]
|
|
107
133
|
|
|
108
134
|
New runs have no total time cap. Manual resume upgrades legacy finite runs and keeps accounting.
|
|
@@ -111,6 +137,9 @@ Native goal state is observational. Completion requires current verification,
|
|
|
111
137
|
two fresh reviews, task completion and a target-bound integration receipt.
|
|
112
138
|
--retry-approval explicitly retries an intact Codex approval-review usage-limit
|
|
113
139
|
failure. Original denials remain recorded; every command needs fresh approval.
|
|
140
|
+
--retry-delivery requeues one stopped child blocked by interrupted handoff proof.
|
|
141
|
+
Inspect and repair the harness cause first. History is retained; fresh native
|
|
142
|
+
generation, verification and delivery are required. It grants no command approval.
|
|
114
143
|
Status includes phase, original cause, recorded sealed proof, cleanup state,
|
|
115
144
|
resumability and remediation. Resume revalidates authority and sealed artifacts;
|
|
116
145
|
receipt-bound recovery executes only missing cleanup steps. Resolve the saved
|
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
|
|
4
4
|
import { finishWorktree } from "../../../../runtime/src/parallel/finish.js";
|
|
5
|
+
import { followupFinishText } from "./parallel.js";
|
|
5
6
|
import { integrateSessionChildren } from "../../../../runtime/src/parallel/integrate.js";
|
|
6
7
|
import { readWorktreeRecord, resolveCheckouts, sessionWorktreeName, worktreePath } from "../../../../runtime/src/parallel/service.js";
|
|
7
8
|
import { resolveReviewScope } from "../../../../runtime/src/review/scope.js";
|
|
@@ -93,7 +94,7 @@ export async function runAdvanceCommand(options) {
|
|
|
93
94
|
if (savedProof !== null && (await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) === savedProof.head ||
|
|
94
95
|
(await deps.git(mainRoot, ["merge-base", "--is-ancestor", savedProof.head, record.targetBranch])).exitCode === 0)) {
|
|
95
96
|
const finished = await finishWorktree(deps, { cwd: mainRoot, name, finalProof: { ...savedProof, recovery: true } });
|
|
96
|
-
return okEnvelope("TASK_ADVANCED", { ...finished, text:
|
|
97
|
+
return okEnvelope("TASK_ADVANCED", { ...finished, text: `Recovered the sealed integration without moving the target again.\n${followupFinishText(finished)}${finished.warnings.join("\n")}` });
|
|
97
98
|
}
|
|
98
99
|
if (savedProof !== null && await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) !== savedProof.target)
|
|
99
100
|
await abandonPreparedRunIntegration(commonDir, record, deps.git);
|
|
@@ -176,7 +177,7 @@ export async function runAdvanceCommand(options) {
|
|
|
176
177
|
reviewMode,
|
|
177
178
|
sourceFingerprint,
|
|
178
179
|
...finished,
|
|
179
|
-
text: `Final candidate ${head} passed all task verifiers and ${reviewMode} review, then finished into ${record.targetBranch}. Receipt: ${finished.receipt}.\n${highlights}Full reports: agent-ops review show --task ${parent.task.id}`
|
|
180
|
+
text: `Final candidate ${head} passed all task verifiers and ${reviewMode} review, then finished into ${record.targetBranch}. Receipt: ${finished.receipt}.\n${followupFinishText(finished)}${finished.warnings.map(w => `Warning: ${w}\n`).join("")}${highlights}Full reports: agent-ops review show --task ${parent.task.id}`
|
|
180
181
|
});
|
|
181
182
|
}
|
|
182
183
|
catch (failure) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFile, lstat } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
|
|
4
|
+
import { reconcileFollowups } from "../../../../runtime/src/followup/intake.js";
|
|
5
|
+
import { readFollowupProgress } from "../../../../runtime/src/followup/process.js";
|
|
6
|
+
import { FileFollowupStore, clarifyFollowup, setFollowupDisposition } from "../../../../runtime/src/followup/store.js";
|
|
7
|
+
import { readFollowupAssessment } from "../../../../runtime/src/review/followup.js";
|
|
8
|
+
import { redactSecrets } from "../../../../runtime/src/security/redact.js";
|
|
9
|
+
import { safeTaskText } from "../../../../runtime/src/task/render.js";
|
|
10
|
+
import { productionFollowupContext, followupRepositoryContext } from "../followup-deps.js";
|
|
11
|
+
import { errorEnvelope, okEnvelope } from "../output.js";
|
|
12
|
+
/** Redact strings individually so multiline credentials cannot hide behind JSON escapes. */
|
|
13
|
+
function display(value) {
|
|
14
|
+
if (typeof value === "string")
|
|
15
|
+
return safeTaskText(redactSecrets(value));
|
|
16
|
+
if (Array.isArray(value))
|
|
17
|
+
return value.map(display);
|
|
18
|
+
if (value !== null && typeof value === "object")
|
|
19
|
+
return Object.fromEntries(Object.entries(value).map(([key, field]) => [key, display(field)]));
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
export async function runFollowupCommand(options) {
|
|
23
|
+
const { args, cwd } = options;
|
|
24
|
+
const source = await followupRepositoryContext(cwd);
|
|
25
|
+
const collection = await reconcileFollowups(source);
|
|
26
|
+
const store = new FileFollowupStore(source.commonDir);
|
|
27
|
+
const action = args.action ?? "list";
|
|
28
|
+
const warnings = collection.warnings.map(w => `Warning: ${w}`);
|
|
29
|
+
if (action === "import" || action === "sync")
|
|
30
|
+
return okEnvelope("FOLLOWUP_IMPORTED", { collection,
|
|
31
|
+
text: [`Followup: collected ${collection.candidates} candidate(s), ${collection.observations} observation(s).`, ...warnings].join("\n") });
|
|
32
|
+
if (action === "list") {
|
|
33
|
+
const items = await Promise.all((await store.read()).items.map(async (item) => {
|
|
34
|
+
const progress = await readFollowupProgress(source.commonDir, item.id, item);
|
|
35
|
+
return { id: item.id, kind: item.kind, disposition: item.disposition, status: progress.status, code: progress.code,
|
|
36
|
+
title: typeof item.original === "string" ? item.original : item.original.title };
|
|
37
|
+
}));
|
|
38
|
+
const safe = display(items);
|
|
39
|
+
return okEnvelope("FOLLOWUP_LIST", { items: safe, collection,
|
|
40
|
+
text: [safe.length === 0 ? "No followup items." : safe.map(i => `${i.id} ${i.kind} ${i.disposition} ${i.status} (${i.code})\n ${i.title}`).join("\n"), ...warnings].join("\n") });
|
|
41
|
+
}
|
|
42
|
+
const id = args.followupId;
|
|
43
|
+
if (action === "clarify") {
|
|
44
|
+
const path = resolve(cwd, args.findingFile);
|
|
45
|
+
if (!(await lstat(path)).isFile() || (await lstat(path)).size > 128 * 1024)
|
|
46
|
+
throw new AgentOpsError("FOLLOWUP_CLARIFICATION_INVALID", "Finding JSON must be a regular file no larger than 128 KiB.");
|
|
47
|
+
let finding;
|
|
48
|
+
try {
|
|
49
|
+
finding = JSON.parse(await readFile(path, "utf8"));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new AgentOpsError("FOLLOWUP_CLARIFICATION_INVALID", "Finding file must contain concrete ReviewFinding JSON.");
|
|
53
|
+
}
|
|
54
|
+
await clarifyFollowup(store, id, finding, args.reason);
|
|
55
|
+
}
|
|
56
|
+
else if (action === "accept" || action === "defer" || action === "reopen") {
|
|
57
|
+
await setFollowupDisposition(store, id, action, args.reason);
|
|
58
|
+
}
|
|
59
|
+
else if (action === "process") {
|
|
60
|
+
const progress = await (await productionFollowupContext(cwd)).process(id, args.host, args.reviewOnlyReason);
|
|
61
|
+
// A persisted refusal remains an error to scripts, with the durable state available to show.
|
|
62
|
+
if (progress.attempts.at(-1)?.code === progress.code)
|
|
63
|
+
return { ...errorEnvelope(progress.code, `Repair start refused: ${progress.code}. Item retained; inspect with followup show ${id}.`),
|
|
64
|
+
data: display({ progress, collection }) };
|
|
65
|
+
}
|
|
66
|
+
const item = await store.get(id), progress = await readFollowupProgress(source.commonDir, id, item);
|
|
67
|
+
const assessment = progress.assessment === undefined ? undefined : await readFollowupAssessment(source.commonDir, progress.assessment);
|
|
68
|
+
const safe = display({ item, progress, ...(assessment === undefined ? {} : {
|
|
69
|
+
assessment: { conclusion: assessment.conclusion, reason: assessment.reason, binding: assessment.binding }
|
|
70
|
+
}) });
|
|
71
|
+
return okEnvelope("FOLLOWUP_DETAIL", { ...safe, collection,
|
|
72
|
+
text: [`${id}: ${safe.item.kind}, ${safe.item.disposition}, ${progress.status} (${progress.code})`,
|
|
73
|
+
JSON.stringify(safe, null, 2), ...warnings].join("\n") });
|
|
74
|
+
}
|
|
@@ -38,6 +38,9 @@ function addText(result) {
|
|
|
38
38
|
"(Claude Code: ExitWorktree with action keep before finishing; do not run task complete in the worktree)."
|
|
39
39
|
].join("\n");
|
|
40
40
|
}
|
|
41
|
+
export function followupFinishText(result) {
|
|
42
|
+
return result.followup === undefined ? "" : `Followup: collected ${result.followup.candidates} candidate(s), ${result.followup.observations} observation(s). Inspect with agent-ops followup list.\n`;
|
|
43
|
+
}
|
|
41
44
|
function finishText(result) {
|
|
42
45
|
const { record } = result;
|
|
43
46
|
return [
|
|
@@ -45,6 +48,7 @@ function finishText(result) {
|
|
|
45
48
|
`Completed and recorded in git notes (refs/notes/agent-ops): ${result.taskIds.join(", ")}.`,
|
|
46
49
|
`Removed ${record.path}.`,
|
|
47
50
|
...(result.receipt === undefined ? [] : [`Receipt: ${result.receipt}`]),
|
|
51
|
+
...followupFinishText(result).trimEnd().split("\n").filter(Boolean),
|
|
48
52
|
...(result.residualRisks === undefined || result.residualRisks.length === 0 ? [] :
|
|
49
53
|
["Residual risks from review:", ...result.residualRisks.map((risk) => `- ${safeTaskText(redactSecrets(risk))}`)]),
|
|
50
54
|
...result.warnings.map((warning) => `Warning: ${warning}`)
|
|
@@ -2,7 +2,7 @@ import { buildReviewPacket } from "../../../../runtime/src/review/packet.js";
|
|
|
2
2
|
import { runIndependentReview } from "../../../../runtime/src/review/runner.js";
|
|
3
3
|
import { renderReviewResult } from "../../../../runtime/src/review/render.js";
|
|
4
4
|
import { reviewReportResults } from "../../../../runtime/src/review/report.js";
|
|
5
|
-
import { findPriorFailingFindings, findReviewAttestation, invalidateReviewAttestation, readReviewReportArtifact, reviewReportDigest, saveReviewAttestation, saveReviewReportArtifact } from "../../../../runtime/src/review/attestation.js";
|
|
5
|
+
import { countFailedReviewRounds, findPriorFailingFindings, REVIEW_ROUND_CAP, 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
7
|
import { evidenceMatchesContract, taskContractHash, treeContractHash, goalHash } from "../../../../runtime/src/task/contract.js";
|
|
8
8
|
import { acceptanceCoverage, coverageDigest } from "../../../../runtime/src/verify/acceptance-coverage.js";
|
|
@@ -649,6 +649,20 @@ export async function runReviewCommand(options) {
|
|
|
649
649
|
? await treeContext(options)
|
|
650
650
|
: await taskContext(options);
|
|
651
651
|
options = { ...options, noChangePaths: options.noChangePaths ?? context?.records.find(r => r.task.id === context.taskId)?.noChangePaths };
|
|
652
|
+
// Before any scope, surface or reviewer cost: a contract that keeps failing
|
|
653
|
+
// needs a new approach, not another round of one-by-one fixes.
|
|
654
|
+
if (context !== undefined && options.root !== undefined) {
|
|
655
|
+
const own = context.records.find(r => r.task.id === context.taskId).task;
|
|
656
|
+
if (await countFailedReviewRounds(options.root, context.taskId, taskContractHash(own)) >= REVIEW_ROUND_CAP) {
|
|
657
|
+
const result = { ...resultBase, status: "NOT_RUN", reason: "round-cap", taskId: context.taskId };
|
|
658
|
+
const message = "Independent review was not run.";
|
|
659
|
+
return { code: "REVIEW_NOT_RUN", status: "error", errors: [{ code: "REVIEW_NOT_RUN", message }], data: { message, result,
|
|
660
|
+
text: renderReviewResult(result) +
|
|
661
|
+
`Review failed ${REVIEW_ROUND_CAP} times under this contract. Stop fixing findings one by one: reconsider ` +
|
|
662
|
+
"the approach, then record the new plan (criteria or intent) with a reason, or ask the user how to proceed:\n" +
|
|
663
|
+
` agent-ops task revise --task ${context.taskId} --expected-contract ${taskContractHash(own)} --reason "<what changed in the approach>"\n` } };
|
|
664
|
+
}
|
|
665
|
+
}
|
|
652
666
|
const evidenceRequirements = (options.args.evidence ?? []).map((value) => {
|
|
653
667
|
const separator = value.indexOf("=");
|
|
654
668
|
return {
|
|
@@ -108,7 +108,7 @@ export async function runRunCommand(options) {
|
|
|
108
108
|
return okEnvelope("RUN_LOGS", { ...stateData(action, state, `Read ${events.length} event(s) for run ${runId}.`), events });
|
|
109
109
|
}
|
|
110
110
|
if (action === "resume")
|
|
111
|
-
return okEnvelope("RUN_RESUMED", data(action, await options.service.resume(runId, options.args.retryApprovalIds)));
|
|
111
|
+
return okEnvelope("RUN_RESUMED", data(action, await options.service.resume(runId, options.args.retryApprovalIds, true, options.args.retryDeliveryTaskId)));
|
|
112
112
|
if (action === "stop")
|
|
113
113
|
return okEnvelope("RUN_STOP_REQUESTED", data(action, await options.service.stop(runId, options.args.answer ?? "user requested stop")));
|
|
114
114
|
const questionId = options.args.questionId;
|
|
@@ -6,7 +6,12 @@ import { renderTaskMarkdown, safeTaskText } from "../../../../runtime/src/task/r
|
|
|
6
6
|
import { errorEnvelope, okEnvelope } from "../output.js";
|
|
7
7
|
async function withCriteriaStatus(envelope, options) {
|
|
8
8
|
const item = envelope.data?.record;
|
|
9
|
-
|
|
9
|
+
// A status read is best effort; a cancelled advisory round must still stop the caller.
|
|
10
|
+
const advice = item === undefined ? null : await options.criteriaStatus?.(item).catch((error) => {
|
|
11
|
+
if (error instanceof AgentOpsError && error.code === "CRITERIA_REVIEW_CANCELLED")
|
|
12
|
+
throw error;
|
|
13
|
+
return null;
|
|
14
|
+
});
|
|
10
15
|
return advice == null || envelope.data === null ? envelope : { ...envelope, data: { ...envelope.data, criteriaReview: advice,
|
|
11
16
|
text: envelope.data.text + "\n\n" + renderCriteriaReview(advice) } };
|
|
12
17
|
}
|
|
@@ -202,7 +207,8 @@ export async function runTaskCommand(options) {
|
|
|
202
207
|
...(origin === undefined ? {} : { origin }),
|
|
203
208
|
reason: options.args.reason, ...(options.args.baseline === undefined ? {} : { baseline: options.args.baseline })
|
|
204
209
|
});
|
|
205
|
-
|
|
210
|
+
// The revision is saved first: an advisory NOT_RUN keeps TASK_REVISED, and a cancelled round throws with it kept.
|
|
211
|
+
return await withCriteriaStatus(await withEscape(taskEnvelope(action, "TASK_REVISED", "Contract revised; obtain a fresh review, and new verification if the criteria or configuration changed.", record), origin === undefined ? undefined : await options.service.declareEscape(record.task.id), options.service), options.onTaskRevised === undefined ? options : { ...options, criteriaStatus: options.onTaskRevised });
|
|
206
212
|
}
|
|
207
213
|
if (action === "replan") {
|
|
208
214
|
const payload = await jsonFile(options.args.planFile);
|
|
@@ -236,22 +236,26 @@ export async function runPolicyContext(root, renew = false) {
|
|
|
236
236
|
return null;
|
|
237
237
|
const here = realpathSync(root);
|
|
238
238
|
const worker = saved.workers.find(w => w.worktree === here);
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
const matchesProof = (registered) => {
|
|
240
|
+
if (registered == null || String(registered.processId) !== process.env.AGENT_OPS_RUN_PROOF_PID)
|
|
241
|
+
return false;
|
|
242
242
|
try {
|
|
243
|
-
|
|
244
|
-
execFileSync("ps", ["-o", "lstart=", "-p", String(
|
|
243
|
+
return execFileSync("ps", ["-o", "pgid=", "-p", String(process.pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim() === String(registered.processId) &&
|
|
244
|
+
execFileSync("ps", ["-o", "lstart=", "-p", String(registered.processId)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim() === registered.processIdentity;
|
|
245
245
|
}
|
|
246
246
|
catch {
|
|
247
|
-
|
|
247
|
+
return false;
|
|
248
248
|
}
|
|
249
|
-
}
|
|
249
|
+
};
|
|
250
|
+
const globalProof = matchesProof(saved.proofProcess);
|
|
251
|
+
const proof = globalProof || matchesProof(worker?.proofProcess);
|
|
252
|
+
// Reading another registered checkout does not change a run-level proof's coordinator actor.
|
|
253
|
+
const actor = globalProof || worker === undefined ? saved.workers.find(w => w.workerId === saved.coordinatorId) : worker;
|
|
250
254
|
if ((!proof && (worker === undefined || record?.runId !== runId || record.workerId !== worker.workerId ||
|
|
251
255
|
record.ownerSessionId !== worker.ownerSessionId || record.coordinatorId !== saved.coordinatorId || record.workerGeneration !== worker.generation)) ||
|
|
252
256
|
(worker === undefined && !(proof && saved.root === here)) ||
|
|
253
|
-
(process.env.AGENT_OPS_WORKER_ID !== undefined && (
|
|
254
|
-
process.env.
|
|
257
|
+
((globalProof || process.env.AGENT_OPS_WORKER_ID !== undefined) && (actor === undefined ||
|
|
258
|
+
process.env.AGENT_OPS_WORKER_ID !== actor.workerId || process.env.AGENT_OPS_WORKER_GENERATION !== String(actor.generation))))
|
|
255
259
|
throw new AgentOpsError("RUN_WORKER_STALE", "Run policy requires the current registered worker generation or proof process.");
|
|
256
260
|
if (saved.status !== "active" || saved.disableRestart || saved.awaitingResume)
|
|
257
261
|
throw new AgentOpsError("RUN_POLICY_DISABLED", "Run execution policy is inactive; explicit resume is required.");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
4
|
-
import { runCriteriaReview, readCriteriaReview, renderCriteriaReview } from "../../../runtime/src/review/criteria.js";
|
|
4
|
+
import { runCriteriaReview, readCriteriaReview, renderCriteriaReview, revisionReviewDue } from "../../../runtime/src/review/criteria.js";
|
|
5
5
|
import { registeredRunProofRunner } from "../../../runtime/src/verify/spawn.js";
|
|
6
6
|
import { appendLocalLog } from "../../../runtime/src/logging/local-log.js";
|
|
7
7
|
import { localStatePaths } from "../../../runtime/src/security/permissions.js";
|
|
@@ -24,7 +24,7 @@ export async function criteriaReviewStatus(root, task) {
|
|
|
24
24
|
return await readCriteriaReview(root, task, await parentScope(root, task).catch(() => undefined));
|
|
25
25
|
}
|
|
26
26
|
/** Only production creation/explicit review installs this hook; TaskService stays model-free. */
|
|
27
|
-
export async function reviewCreatedCriteria(root, task, rerun = false) {
|
|
27
|
+
export async function reviewCreatedCriteria(root, task, rerun = false, trigger) {
|
|
28
28
|
const controller = new AbortController();
|
|
29
29
|
const interrupt = (signal) => { controller.abort(signal); };
|
|
30
30
|
const int = () => interrupt("SIGINT"), term = () => interrupt("SIGTERM");
|
|
@@ -38,7 +38,7 @@ export async function reviewCreatedCriteria(root, task, rerun = false) {
|
|
|
38
38
|
const parent = await parentScope(root, task).catch(() => undefined);
|
|
39
39
|
process.stderr.write(`Criteria advisory for ${task.id}: waiting for one independent reviewer (no time budget; Ctrl-C cancels).\n`);
|
|
40
40
|
let result = await runCriteriaReview({ root, task, parent, target, model: role?.model, effort: role?.effort,
|
|
41
|
-
rerun, signal: controller.signal, runner: registeredRunProofRunner(),
|
|
41
|
+
rerun, ...(trigger === undefined ? {} : { trigger }), signal: controller.signal, runner: registeredRunProofRunner(),
|
|
42
42
|
unavailableCode: config === undefined ? "CRITERIA_REVIEW_CONFIG_UNAVAILABLE" : target === undefined && role !== undefined ? "CRITERIA_REVIEW_NO_INDEPENDENT_TARGET" : "CRITERIA_REVIEW_UNCONFIGURED",
|
|
43
43
|
log: async (report, path) => {
|
|
44
44
|
const state = localStatePaths(process.env.AGENT_OPS_HOME ?? homedir());
|
|
@@ -61,3 +61,8 @@ export async function reviewCreatedCriteria(root, task, rerun = false) {
|
|
|
61
61
|
process.removeListener("SIGTERM", term);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
/** A revise that changed what the advice judged reruns it once; otherwise it reports the current advisory. */
|
|
65
|
+
export async function reviewRevisedCriteria(root, task) {
|
|
66
|
+
const view = await criteriaReviewStatus(root, task);
|
|
67
|
+
return revisionReviewDue(view) ? await reviewCreatedCriteria(root, task, true, "revise") : view;
|
|
68
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
2
|
+
import { processFollowup, reconcileFollowupRepairs } from "../../../runtime/src/followup/process.js";
|
|
3
|
+
import { isFollowupAssessmentCurrent } from "../../../runtime/src/review/followup.js";
|
|
4
|
+
import { resolveReviewRole } from "../../../runtime/src/review/roles.js";
|
|
5
|
+
import { worktreeDependencies, gitRunner } from "./parallel-deps.js";
|
|
6
|
+
import { productionRunContext } from "./run-deps.js";
|
|
7
|
+
import { resolveCheckouts } from "../../../runtime/src/parallel/service.js";
|
|
8
|
+
import { reconcileFollowups } from "../../../runtime/src/followup/intake.js";
|
|
9
|
+
export async function followupRepositoryContext(cwd) {
|
|
10
|
+
const deps = worktreeDependencies(), checkouts = await resolveCheckouts(deps, cwd);
|
|
11
|
+
return { git: deps.git, mainRoot: checkouts.mainRoot, commonDir: checkouts.commonDir,
|
|
12
|
+
config: await deps.loadConfig(checkouts.mainRoot), loadConfig: async () => await deps.loadConfig(checkouts.mainRoot) };
|
|
13
|
+
}
|
|
14
|
+
/** Ordinary CLI reconciliation is advisory and never launches a provider or repair. */
|
|
15
|
+
export async function reconcileRepositoryFollowups(cwd) {
|
|
16
|
+
try {
|
|
17
|
+
return await reconcileFollowups(await followupRepositoryContext(cwd));
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const code = error instanceof AgentOpsError && /^[A-Z0-9_]{1,80}$/u.test(error.code) ? error.code : "FOLLOWUP_COLLECTION_FAILED";
|
|
21
|
+
return { added: 0, candidates: 0, observations: 0,
|
|
22
|
+
warnings: code === "WORKTREE_NOT_GIT" ? [] : [`followup: ${code}; retry with followup import.`] };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Uses the ordinary production authority, including its exact native/platform/trust refusals. */
|
|
26
|
+
export async function productionFollowupContext(cwd) {
|
|
27
|
+
const deps = worktreeDependencies(), checkouts = await resolveCheckouts(deps, cwd);
|
|
28
|
+
const branch = await deps.git(checkouts.currentRoot, ["symbolic-ref", "--short", "HEAD"]);
|
|
29
|
+
if (branch.exitCode !== 0)
|
|
30
|
+
throw new AgentOpsError("RUN_TARGET_REQUIRED", "Followup processing requires a checked-out target branch.");
|
|
31
|
+
const loadConfig = async () => await deps.loadConfig(checkouts.currentRoot);
|
|
32
|
+
const assessmentOptions = async (itemId, reviewOnlyReason) => {
|
|
33
|
+
const config = await loadConfig(), role = resolveReviewRole("independent-review", config.reviewRoles ?? []);
|
|
34
|
+
return {
|
|
35
|
+
root: checkouts.currentRoot, commonDir: checkouts.commonDir, itemId, targetBranch: branch.stdout.trim(),
|
|
36
|
+
gitRunner: gitRunner(checkouts.currentRoot), loadConfig,
|
|
37
|
+
review: { targets: role?.targets ?? [], ...(role?.timeoutMs === undefined ? {} : { timeoutMs: role.timeoutMs }) },
|
|
38
|
+
...(reviewOnlyReason === undefined ? {} : { reviewOnlyReason })
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
return { ...checkouts,
|
|
42
|
+
assessmentOptions,
|
|
43
|
+
process: async (itemId, host, reviewOnlyReason) => {
|
|
44
|
+
const options = await assessmentOptions(itemId, reviewOnlyReason);
|
|
45
|
+
return await processFollowup({ ...options, host, start: async (input, proof) => {
|
|
46
|
+
const run = await productionRunContext(cwd, { beforeStart: async () => {
|
|
47
|
+
if (!await isFollowupAssessmentCurrent(options, proof))
|
|
48
|
+
throw new AgentOpsError("FOLLOWUP_SOURCE_CHANGED", "The finding, target, source or config changed before repair startup.");
|
|
49
|
+
} });
|
|
50
|
+
return (await run.service.start({ ...input, root: run.mainRoot, commonDir: run.commonDir,
|
|
51
|
+
targetBranch: run.targetBranch, ownerSessionId: run.ownerSessionId })).state;
|
|
52
|
+
} });
|
|
53
|
+
},
|
|
54
|
+
reconcile: async () => await reconcileFollowupRepairs({ git: deps.git, mainRoot: checkouts.mainRoot,
|
|
55
|
+
commonDir: checkouts.commonDir, config: await loadConfig(), loadConfig }) };
|
|
56
|
+
}
|
|
@@ -7,9 +7,9 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { setTimeout as delay } from "node:timers/promises";
|
|
8
8
|
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
9
9
|
import { sha256 } from "../../../runtime/src/fs/hash.js";
|
|
10
|
-
import { resolveCheckouts, sessionWorktreeName } from "../../../runtime/src/parallel/service.js";
|
|
10
|
+
import { resolveCheckouts, readWorktreeRecord, sessionWorktreeName } from "../../../runtime/src/parallel/service.js";
|
|
11
11
|
import { createLaunchdDescriptor, LaunchdController, readBootIdentity, readGuiLoginIdentity } from "../../../runtime/src/run/macOS.js";
|
|
12
|
-
import { FileRunRepository, RunService } from "../../../runtime/src/run/service.js";
|
|
12
|
+
import { FileRunRepository, RunService, validateDeliveryRetry } from "../../../runtime/src/run/service.js";
|
|
13
13
|
import { readPrivateFile, writePrivateFile } from "../../../runtime/src/security/permissions.js";
|
|
14
14
|
import { collectChangeSurface } from "../../../runtime/src/verify/change-surface.js";
|
|
15
15
|
import { worktreeDependencies, gitRunner, trustStore } from "./parallel-deps.js";
|
|
@@ -182,8 +182,10 @@ export async function productionRunContext(cwd, options = {}) {
|
|
|
182
182
|
if (await deps.trust.status(checkouts.mainRoot, config) !== "TRUSTED")
|
|
183
183
|
throw new AgentOpsError("RUN_REPO_UNTRUSTED", "The run requires current repository verifier trust.");
|
|
184
184
|
const ownerSessionId = randomUUID();
|
|
185
|
+
const bootIdentity = await readBootIdentity(), loginDomain = await readGuiLoginIdentity();
|
|
186
|
+
await options.beforeStart?.();
|
|
185
187
|
const result = await service.start({ ...input, root: checkouts.mainRoot, commonDir: checkouts.commonDir,
|
|
186
|
-
targetBranch: target.stdout.trim(), ownerSessionId, bootIdentity
|
|
188
|
+
targetBranch: target.stdout.trim(), ownerSessionId, bootIdentity, loginDomain });
|
|
187
189
|
try {
|
|
188
190
|
await bindRunPolicy(repository, result.state.runId, { config,
|
|
189
191
|
baseTrustBinding: await repositoryTrustBinding(checkouts.mainRoot, config, CLI_VERSION), runtimeHash: await runRuntimeHash() });
|
|
@@ -209,8 +211,9 @@ export async function productionRunContext(cwd, options = {}) {
|
|
|
209
211
|
throw cause;
|
|
210
212
|
}
|
|
211
213
|
};
|
|
212
|
-
const resume = async (runId, retryApprovalIds = [], upgradeBudget = true) => {
|
|
214
|
+
const resume = async (runId, retryApprovalIds = [], upgradeBudget = true, retryDeliveryTaskId) => {
|
|
213
215
|
const before = await service.status(runId);
|
|
216
|
+
const retry = retryDeliveryTaskId === undefined ? null : validateDeliveryRetry(before, retryDeliveryTaskId);
|
|
214
217
|
const runtimeHash = await runRuntimeHash();
|
|
215
218
|
const policy = await readRunPolicy(before, before.policyBinding?.runtimeHash ?? runtimeHash, Date.now(), true);
|
|
216
219
|
if (policy !== null && await deps.trust.status(checkouts.mainRoot, policy.baseConfig) !== "TRUSTED")
|
|
@@ -236,7 +239,15 @@ export async function productionRunContext(cwd, options = {}) {
|
|
|
236
239
|
if (exists === null || (await collectChangeSurface(gitRunner(worker.worktree))).paths.length > 0)
|
|
237
240
|
throw new AgentOpsError("RUN_RECOVERY_DIRTY", "Commit or preserve every writer checkout before explicit resume.");
|
|
238
241
|
}
|
|
239
|
-
|
|
242
|
+
if (retry !== null) {
|
|
243
|
+
const record = retry.worktree === null ? null : await readWorktreeRecord(retry.worktree);
|
|
244
|
+
const head = retry.worktree === null ? null : await deps.git(retry.worktree, ["rev-parse", "HEAD"]);
|
|
245
|
+
if (record === null || record.runId !== runId || record.workerId !== retry.workerId ||
|
|
246
|
+
record.ownerSessionId !== retry.ownerSessionId || record.workerGeneration !== retry.generation ||
|
|
247
|
+
head?.exitCode !== 0 || head.stdout.trim() !== retry.stopIntent.sourceCommit)
|
|
248
|
+
throw new AgentOpsError("RUN_DELIVERY_RETRY_INVALID", "Interrupted checkout ownership or source changed; preserve it and reconcile before retrying.");
|
|
249
|
+
}
|
|
250
|
+
const result = await service.resume(runId, retryApprovalIds, upgradeBudget, retryDeliveryTaskId, retry?.lastFailure?.id);
|
|
240
251
|
try {
|
|
241
252
|
if (pending !== null)
|
|
242
253
|
await renewPendingRunPolicy(repository, runId, pending);
|