@kylecheng3146/agent-ops 0.10.2 → 0.11.1
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 +13 -0
- package/dist/packages/cli/src/args.js +18 -44
- package/dist/packages/cli/src/bin.js +37 -29
- package/dist/packages/cli/src/cli.js +16 -14
- package/dist/packages/cli/src/codex-loop-process.js +0 -6
- package/dist/packages/cli/src/commands/review.js +5 -10
- package/dist/packages/cli/src/commands/run.js +3 -1
- package/dist/packages/cli/src/commands/task.js +27 -54
- package/dist/packages/cli/src/commands/verify.js +7 -3
- package/dist/packages/cli/src/criteria-review.js +63 -0
- package/dist/packages/cli/src/default-base.js +26 -0
- package/dist/packages/cli/src/hook-process.js +19 -83
- package/dist/packages/cli/src/owned-run-step.js +1 -1
- package/dist/packages/cli/src/run-entry.js +203 -115
- package/dist/runtime/src/adapters/agy/config.js +2 -3
- package/dist/runtime/src/adapters/claude/config.js +2 -2
- package/dist/runtime/src/adapters/codex/config.js +2 -5
- package/dist/runtime/src/hooks/completion-gate.js +0 -15
- package/dist/runtime/src/install/harness.js +4 -4
- package/dist/runtime/src/install/plan.js +3 -3
- package/dist/runtime/src/install/profiles.js +0 -2
- package/dist/runtime/src/logging/local-log.js +12 -0
- package/dist/runtime/src/office/snapshot.js +4 -1
- package/dist/runtime/src/parallel/manage.js +0 -3
- package/dist/runtime/src/review/criteria.js +252 -0
- package/dist/runtime/src/review/execute.js +68 -4
- package/dist/runtime/src/review/invocation.js +3 -3
- package/dist/runtime/src/run/verification.js +2 -2
- package/dist/runtime/src/schema/validate.js +10 -5
- package/dist/runtime/src/task/completion.js +4 -42
- package/dist/runtime/src/task/contract.js +11 -0
- package/dist/runtime/src/task/service.js +34 -57
- package/dist/runtime/src/task/store.js +3 -7
- package/dist/runtime/src/verify/acceptance-coverage.js +2 -3
- package/dist/runtime/src/verify/acceptance-protocol.js +4 -3
- package/dist/runtime/src/verify/acceptance-replay.js +4 -7
- package/dist/runtime/src/verify/service.js +6 -23
- package/dist/runtime/src/verify/source-fingerprint.js +17 -4
- package/dist/runtime/src/verify/spawn.js +3 -2
- package/docs/en/guides/task-debug.md +39 -133
- package/docs/en/spec/acceptance-and-evidence.md +31 -23
- package/docs/en/spec/harness-adapters.md +9 -11
- package/docs/en/spec/loop-engineering.md +11 -5
- package/docs/en/spec/review.md +16 -0
- package/docs/zh-TW/guides/task-debug.md +27 -73
- package/docs/zh-TW/spec/acceptance-and-evidence.md +18 -18
- package/docs/zh-TW/spec/harness-adapters.md +7 -8
- package/docs/zh-TW/spec/loop-engineering.md +9 -4
- package/docs/zh-TW/spec/review.md +17 -1
- package/package.json +1 -1
- package/schemas/config.schema.json +1 -21
- package/dist/packages/cli/src/debug-intake.js +0 -245
- package/dist/runtime/src/task/debug-intake.js +0 -15
- package/dist/runtime/src/task/debug-state.js +0 -135
- package/dist/runtime/src/task/debug-user-event.js +0 -160
- package/dist/runtime/src/task/debug.js +0 -632
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { renderCriteriaReview } from "../../../../runtime/src/review/criteria.js";
|
|
1
2
|
import { lstat, readFile } from "node:fs/promises";
|
|
2
3
|
import { taskContractHash } from "../../../../runtime/src/task/contract.js";
|
|
3
4
|
import { AgentOpsError } from "../../../../runtime/src/fs/paths.js";
|
|
4
5
|
import { renderTaskMarkdown, safeTaskText } from "../../../../runtime/src/task/render.js";
|
|
5
6
|
import { errorEnvelope, okEnvelope } from "../output.js";
|
|
7
|
+
async function withCriteriaStatus(envelope, options) {
|
|
8
|
+
const item = envelope.data?.record;
|
|
9
|
+
const advice = item === undefined ? null : await options.criteriaStatus?.(item).catch(() => null);
|
|
10
|
+
return advice == null || envelope.data === null ? envelope : { ...envelope, data: { ...envelope.data, criteriaReview: advice,
|
|
11
|
+
text: envelope.data.text + "\n\n" + renderCriteriaReview(advice) } };
|
|
12
|
+
}
|
|
6
13
|
function isRecord(value) {
|
|
7
14
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
15
|
}
|
|
@@ -47,10 +54,10 @@ async function withEscape(envelope, escape, service) {
|
|
|
47
54
|
...facts.map(fact => `- ${fact.path}: ${fact.surface}`),
|
|
48
55
|
...(proof === undefined ? [] : [proof.status === "proven"
|
|
49
56
|
? `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}
|
|
57
|
+
: proof.status === "unproven" ? `Proof: ${proof.code} - ${proof.reason} Advisory: finish does not wait for it; add a tracked rule to prove it.`
|
|
51
58
|
: 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}
|
|
59
|
+
: proof.check === null ? "Proof: ESCAPE_RULE_UNPROVEN (advisory) - name the red-to-green check with --escape-promote <runnerId:checkId>."
|
|
60
|
+
: `Proof: promoted check ${proof.check} should be a proven redCheckId of this task (advisory). Config proposal (not written):\n${JSON.stringify(proposal, null, 2)}`]),
|
|
54
61
|
`Ledger record: ${escape.path} (commit it with the fix).`,
|
|
55
62
|
escape.blame.length === 0 ? "Blame hints: none (the fix only adds lines; an omission has no author)."
|
|
56
63
|
: `Blame hints (a hint only): ${escape.blame.map(hint => `${hint.commit.slice(0, 12)} (${hint.lines} line${hint.lines === 1 ? "" : "s"})`).join(", ")}`
|
|
@@ -98,7 +105,7 @@ function taskAction(args) {
|
|
|
98
105
|
"complete",
|
|
99
106
|
"create",
|
|
100
107
|
"export",
|
|
101
|
-
"status", "revise", "replan", "pin-finding"
|
|
108
|
+
"status", "revise", "replan", "pin-finding"
|
|
102
109
|
].includes(args.action)) {
|
|
103
110
|
throw new AgentOpsError("TASK_ACTION_INVALID", "A supported task action is required.");
|
|
104
111
|
}
|
|
@@ -110,7 +117,7 @@ function taskEnvelope(action, code, message, record) {
|
|
|
110
117
|
message,
|
|
111
118
|
record,
|
|
112
119
|
contractHash: taskContractHash(record.task),
|
|
113
|
-
text: renderTaskMarkdown(record) + "\nContract: " + taskContractHash(record.task)
|
|
120
|
+
text: renderTaskMarkdown(record) + "\nContract: " + taskContractHash(record.task)
|
|
114
121
|
});
|
|
115
122
|
}
|
|
116
123
|
function renderTaskList(records) {
|
|
@@ -129,8 +136,6 @@ export async function runTaskCommand(options) {
|
|
|
129
136
|
if (options.args.title === undefined) {
|
|
130
137
|
throw new AgentOpsError("TASK_TITLE_REQUIRED", "Task creation requires --title.");
|
|
131
138
|
}
|
|
132
|
-
if (options.args.debug)
|
|
133
|
-
await options.service.assertDebugBaseline();
|
|
134
139
|
const worktree = sessionId === undefined || options.sessionWorktree === undefined
|
|
135
140
|
? undefined
|
|
136
141
|
: await options.sessionWorktree(sessionId);
|
|
@@ -156,17 +161,15 @@ export async function runTaskCommand(options) {
|
|
|
156
161
|
requireSurface: true
|
|
157
162
|
});
|
|
158
163
|
const escape = origin === undefined ? undefined : await (worktree?.service ?? options.service).declareEscape(record.task.id);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// foregroundStart is idempotent for a repeated hook or command.
|
|
162
|
-
if (options.args.debug && sessionId !== undefined) {
|
|
163
|
-
await (worktree?.service ?? options.service).debugService().foregroundStart(record.task.id, sessionId);
|
|
164
|
-
}
|
|
165
|
-
const created = await withEscape(taskEnvelope(action, "TASK_CREATED", sessionId === undefined
|
|
164
|
+
const initialAdvice = await options.onTaskCreated?.(record, worktree?.path);
|
|
165
|
+
let created = await withEscape(taskEnvelope(action, "TASK_CREATED", sessionId === undefined
|
|
166
166
|
? `Created task ${record.task.id}.`
|
|
167
167
|
: record.task.parentTaskId === undefined
|
|
168
168
|
? `Created and attached task ${record.task.id}.`
|
|
169
169
|
: `Created subtask ${record.task.id}; the session stays on the top of its task tree.`, record), escape, worktree?.service ?? options.service);
|
|
170
|
+
if (initialAdvice !== undefined && created.data !== null)
|
|
171
|
+
created = { ...created, data: { ...created.data, criteriaReview: initialAdvice,
|
|
172
|
+
text: created.data.text + "\n\n" + renderCriteriaReview(initialAdvice) } };
|
|
170
173
|
if (worktree === undefined || created.data === null)
|
|
171
174
|
return created;
|
|
172
175
|
return {
|
|
@@ -187,42 +190,6 @@ export async function runTaskCommand(options) {
|
|
|
187
190
|
}
|
|
188
191
|
};
|
|
189
192
|
}
|
|
190
|
-
if (action === "debug") {
|
|
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
|
-
}
|
|
196
|
-
switch (options.args.debugAction) {
|
|
197
|
-
case "hypothesis":
|
|
198
|
-
await service.hypothesis(id, await jsonFile(options.args.payloadFile));
|
|
199
|
-
break;
|
|
200
|
-
case "strategy":
|
|
201
|
-
await service.strategy(id, await jsonFile(options.args.payloadFile));
|
|
202
|
-
break;
|
|
203
|
-
case "investigate":
|
|
204
|
-
await service.investigate(id, options.args.commandId, options.trusted === true);
|
|
205
|
-
break;
|
|
206
|
-
case "checkpoint":
|
|
207
|
-
if (sessionId === undefined)
|
|
208
|
-
throw new AgentOpsError("TASK_SESSION_REQUIRED", "Debug checkpoint requires a session.");
|
|
209
|
-
await service.checkpoint(id, sessionId);
|
|
210
|
-
break;
|
|
211
|
-
case "resume":
|
|
212
|
-
if (sessionId === undefined)
|
|
213
|
-
throw new AgentOpsError("TASK_SESSION_REQUIRED", "Debug resume requires a session.");
|
|
214
|
-
await service.resume(id, sessionId);
|
|
215
|
-
break;
|
|
216
|
-
case "extend":
|
|
217
|
-
if (sessionId === undefined)
|
|
218
|
-
throw new AgentOpsError("TASK_SESSION_REQUIRED", "Debug extension requires a native session.");
|
|
219
|
-
await service.extend(id, sessionId, options.args.eventId);
|
|
220
|
-
break;
|
|
221
|
-
case "status": break;
|
|
222
|
-
default: throw new AgentOpsError("TASK_DEBUG_ACTION_INVALID", "A supported debug operation is required.");
|
|
223
|
-
}
|
|
224
|
-
return taskEnvelope(action, "TASK_DEBUG_UPDATED", "Original issue debug state preserved.", await service.root(id));
|
|
225
|
-
}
|
|
226
193
|
if (action === "revise") {
|
|
227
194
|
const payload = options.args.criteriaFile === undefined ? undefined : await jsonFile(options.args.criteriaFile);
|
|
228
195
|
if (payload !== undefined && !Array.isArray(payload))
|
|
@@ -235,7 +202,7 @@ export async function runTaskCommand(options) {
|
|
|
235
202
|
...(origin === undefined ? {} : { origin }),
|
|
236
203
|
reason: options.args.reason, ...(options.args.baseline === undefined ? {} : { baseline: options.args.baseline })
|
|
237
204
|
});
|
|
238
|
-
return await withEscape(taskEnvelope(action, "TASK_REVISED", "Contract revised; obtain new verification
|
|
205
|
+
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);
|
|
239
206
|
}
|
|
240
207
|
if (action === "replan") {
|
|
241
208
|
const payload = await jsonFile(options.args.planFile);
|
|
@@ -245,7 +212,13 @@ export async function runTaskCommand(options) {
|
|
|
245
212
|
expectedTreeContractHash: options.args.expectedTreeContract, reason: options.args.reason,
|
|
246
213
|
tasks: payload
|
|
247
214
|
});
|
|
248
|
-
|
|
215
|
+
const advice = [];
|
|
216
|
+
for (const record of records) {
|
|
217
|
+
const result = await options.onTaskCreated?.(record);
|
|
218
|
+
if (result !== undefined)
|
|
219
|
+
advice.push(renderCriteriaReview(result));
|
|
220
|
+
}
|
|
221
|
+
return okEnvelope("TASK_REPLANNED", { action, message: "Task tree split atomically; reverify and review.", records, text: [renderTaskList(records), ...advice].join("\n\n") });
|
|
249
222
|
}
|
|
250
223
|
if (action === "pin-finding") {
|
|
251
224
|
const criterion = parseCriterion(JSON.stringify(await jsonFile(options.args.criterionFiles[0])));
|
|
@@ -278,9 +251,9 @@ export async function runTaskCommand(options) {
|
|
|
278
251
|
const reviewOnly = coverage.rows.filter(row => row.mode === "review-only").length;
|
|
279
252
|
const legacy = coverage.rows.filter(row => row.mode === "legacy").length;
|
|
280
253
|
const envelope = taskEnvelope(action, "TASK_STATUS", `Read task ${record.task.id}.`, record);
|
|
281
|
-
return { ...envelope, data: { ...envelope.data, coverage, treeContractHash: await options.service.treeContract(record.task.id),
|
|
254
|
+
return await withCriteriaStatus({ ...envelope, data: { ...envelope.data, coverage, treeContractHash: await options.service.treeContract(record.task.id),
|
|
282
255
|
text: envelope.data.text + "\nAcceptance: " + proven + "/" + mechanical + " mechanical proven; " + reviewOnly + " review-only; " + legacy + " legacy; originally mechanical " + coverage.originalMechanical +
|
|
283
|
-
"; reviewed " + coverage.reviewed + "; undischarged " + coverage.rows.filter(row => row.status !== "proven" && !coverage.reviewed).length } };
|
|
256
|
+
"; reviewed " + coverage.reviewed + "; undischarged " + coverage.rows.filter(row => row.status !== "proven" && !coverage.reviewed).length } }, options);
|
|
284
257
|
}
|
|
285
258
|
if (action === "attach") {
|
|
286
259
|
if (sessionId === undefined) {
|
|
@@ -96,12 +96,16 @@ export async function runVerifyCommand(options) {
|
|
|
96
96
|
try {
|
|
97
97
|
const taskId = await resolveTaskId(options), activity = options.activity;
|
|
98
98
|
const report = publicReport(await withActivity(activity?.commonDir, { kind: "verify", root: activity?.root ?? "", taskId, ...(activity?.sessionId === undefined ? {} : { sessionId: activity.sessionId }) }, async () => await options.service.verify(taskId)));
|
|
99
|
-
|
|
99
|
+
// A broken surface check must not hide the verification result.
|
|
100
|
+
const surface = options.surface === undefined ? [] : await options.surface(taskId).catch(() => []);
|
|
101
|
+
const text = formatReport(report) + surface.map(line => `${safe(line)}\n`).join("");
|
|
102
|
+
const advisory = surface.length === 0 ? {} : { surface };
|
|
100
103
|
if (report.status === "PASS") {
|
|
101
104
|
return okEnvelope("VERIFICATION_PASSED", {
|
|
102
105
|
message: "Required verification passed.",
|
|
103
106
|
report,
|
|
104
|
-
text
|
|
107
|
+
text,
|
|
108
|
+
...advisory
|
|
105
109
|
});
|
|
106
110
|
}
|
|
107
111
|
const code = report.status === "FAIL"
|
|
@@ -113,7 +117,7 @@ export async function runVerifyCommand(options) {
|
|
|
113
117
|
return {
|
|
114
118
|
code,
|
|
115
119
|
status: "error",
|
|
116
|
-
data: { message, report, text },
|
|
120
|
+
data: { message, report, text, ...advisory },
|
|
117
121
|
errors: [{ code, message }]
|
|
118
122
|
};
|
|
119
123
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
4
|
+
import { runCriteriaReview, readCriteriaReview, renderCriteriaReview } from "../../../runtime/src/review/criteria.js";
|
|
5
|
+
import { registeredRunProofRunner } from "../../../runtime/src/verify/spawn.js";
|
|
6
|
+
import { appendLocalLog } from "../../../runtime/src/logging/local-log.js";
|
|
7
|
+
import { localStatePaths } from "../../../runtime/src/security/permissions.js";
|
|
8
|
+
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
9
|
+
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
10
|
+
import { loadEffectiveConfig, runPolicyContext } from "./context.js";
|
|
11
|
+
async function parentScope(root, task) {
|
|
12
|
+
const read = async (path, id) => (await new TaskService(new FileTaskStore(join(path, ".agent-ops/tasks/state.json"), path)).status({ taskId: id })).task;
|
|
13
|
+
if (task.parentTaskId !== undefined)
|
|
14
|
+
return await read(root, task.parentTaskId);
|
|
15
|
+
if (task.surface !== undefined && "covers" in task.surface) {
|
|
16
|
+
const run = await runPolicyContext(root);
|
|
17
|
+
const coordinator = run?.state.workers.find(worker => worker.workerId === run.state.coordinatorId);
|
|
18
|
+
if (run?.state.rootTaskId != null && coordinator?.worktree != null)
|
|
19
|
+
return await read(coordinator.worktree, run.state.rootTaskId);
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
export async function criteriaReviewStatus(root, task) {
|
|
24
|
+
return await readCriteriaReview(root, task, await parentScope(root, task).catch(() => undefined));
|
|
25
|
+
}
|
|
26
|
+
/** Only production creation/explicit review installs this hook; TaskService stays model-free. */
|
|
27
|
+
export async function reviewCreatedCriteria(root, task, rerun = false) {
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const interrupt = (signal) => { controller.abort(signal); };
|
|
30
|
+
const int = () => interrupt("SIGINT"), term = () => interrupt("SIGTERM");
|
|
31
|
+
process.once("SIGINT", int);
|
|
32
|
+
process.once("SIGTERM", term);
|
|
33
|
+
try {
|
|
34
|
+
const config = await loadEffectiveConfig(root, "project").catch(() => undefined);
|
|
35
|
+
const role = config?.config.reviewRoles?.find(role => role.role === "independent-review");
|
|
36
|
+
const host = process.env.AGENT_OPS_HOST;
|
|
37
|
+
const target = role?.targets.find(target => target !== host) ?? role?.targets[0];
|
|
38
|
+
const parent = await parentScope(root, task).catch(() => undefined);
|
|
39
|
+
process.stderr.write(`Criteria advisory for ${task.id}: waiting for one independent reviewer (no time budget; Ctrl-C cancels).\n`);
|
|
40
|
+
let result = await runCriteriaReview({ root, task, parent, target, model: role?.model, effort: role?.effort,
|
|
41
|
+
rerun, signal: controller.signal, runner: registeredRunProofRunner(),
|
|
42
|
+
unavailableCode: config === undefined ? "CRITERIA_REVIEW_CONFIG_UNAVAILABLE" : target === undefined && role !== undefined ? "CRITERIA_REVIEW_NO_INDEPENDENT_TARGET" : "CRITERIA_REVIEW_UNCONFIGURED",
|
|
43
|
+
log: async (report, path) => {
|
|
44
|
+
const state = localStatePaths(process.env.AGENT_OPS_HOME ?? homedir());
|
|
45
|
+
await appendLocalLog(join(state.logDirectory, "events.ndjson"), { type: "criteria-review", taskId: task.id,
|
|
46
|
+
attemptId: report.attemptId, status: report.status, code: report.code, durationMs: report.durationMs,
|
|
47
|
+
invocations: report.invocations, recommendations: report.advice?.recommendations.length ?? 0,
|
|
48
|
+
report: path, usage: { ...report.usage } }, { anchorDirectory: state.anchorDirectory });
|
|
49
|
+
} });
|
|
50
|
+
const latest = await new TaskService(new FileTaskStore(join(root, ".agent-ops/tasks/state.json"), root)).status({ taskId: task.id }).catch(() => undefined);
|
|
51
|
+
const currentView = latest === undefined ? null : await criteriaReviewStatus(root, latest.task);
|
|
52
|
+
if (currentView?.report.attemptId === result.report.attemptId)
|
|
53
|
+
result = { ...result, stale: currentView.stale };
|
|
54
|
+
if (controller.signal.aborted)
|
|
55
|
+
throw new AgentOpsError("CRITERIA_REVIEW_CANCELLED", "Criteria review cancelled; stop before implementation.");
|
|
56
|
+
process.stderr.write(renderCriteriaReview(result) + "\n");
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
process.removeListener("SIGINT", int);
|
|
61
|
+
process.removeListener("SIGTERM", term);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readWorktreeRecord } from "../../../runtime/src/parallel/service.js";
|
|
2
|
+
import { collectChangeSurface, resolveGitCommit } from "../../../runtime/src/verify/change-surface.js";
|
|
3
|
+
/**
|
|
4
|
+
* The base verify and review use when `--base` is absent, so committed work is
|
|
5
|
+
* judged as a range without the agent having to copy it. A dirty tree keeps the
|
|
6
|
+
* working-tree mode; a session worktree compares against its merge-base with the
|
|
7
|
+
* target branch (which follows a finish rebase); elsewhere the task's creation
|
|
8
|
+
* commit counts when it is an ancestor of HEAD.
|
|
9
|
+
*/
|
|
10
|
+
export async function defaultBase(root, runner, createdSourceCommit) {
|
|
11
|
+
if ((await collectChangeSurface(runner)).paths.length > 0)
|
|
12
|
+
return undefined;
|
|
13
|
+
const head = await resolveGitCommit(runner, "HEAD");
|
|
14
|
+
const record = await readWorktreeRecord(root);
|
|
15
|
+
let base;
|
|
16
|
+
if (record !== null) {
|
|
17
|
+
const merge = await runner.run(["merge-base", "HEAD", record.targetBranch]);
|
|
18
|
+
base = merge.exitCode === 0 ? Buffer.from(merge.stdout).toString("utf8").trim() : record.base;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const created = await createdSourceCommit().catch(() => undefined);
|
|
22
|
+
if (created !== undefined && (await runner.run(["merge-base", "--is-ancestor", created, "HEAD"])).exitCode === 0)
|
|
23
|
+
base = created;
|
|
24
|
+
}
|
|
25
|
+
return base === undefined || base === "" || base === head ? undefined : base;
|
|
26
|
+
}
|
|
@@ -14,7 +14,6 @@ import { evaluateWorktreeEnter } from "../../../runtime/src/parallel/enter.js";
|
|
|
14
14
|
import { evaluateWorktreeWrite, resolveMainRoot } from "../../../runtime/src/parallel/guard.js";
|
|
15
15
|
import { ensureSessionWorktree, recordSessionOrigin } from "../../../runtime/src/parallel/service.js";
|
|
16
16
|
import { CompletionGateService } from "../../../runtime/src/hooks/completion-gate.js";
|
|
17
|
-
import { closeDebugSession, debugStopGate, interruptDebugSession, routeDebugPrompt, startDebugSession } from "./debug-intake.js";
|
|
18
17
|
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
19
18
|
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
20
19
|
import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
|
|
@@ -269,8 +268,6 @@ async function closeOfficeSession(io, dependencies) {
|
|
|
269
268
|
root, harness: "claude", event: "SessionEnd", input, validated: true,
|
|
270
269
|
...(typeof fields.session_id === "string" ? { sessionId: fields.session_id } : {})
|
|
271
270
|
});
|
|
272
|
-
if (typeof fields.session_id === "string")
|
|
273
|
-
await closeDebugSession(root, fields.session_id, "claude").catch(() => undefined);
|
|
274
271
|
}
|
|
275
272
|
catch {
|
|
276
273
|
// Office never blocks a session, least of all one that is ending.
|
|
@@ -307,12 +304,8 @@ async function promptSubmitted(harness, io, dependencies) {
|
|
|
307
304
|
const prompt = fields.prompt;
|
|
308
305
|
if (typeof prompt !== "string" || prompt.length === 0 || prompt.length > 64 * 1024 || prompt.includes("\0"))
|
|
309
306
|
return 0;
|
|
310
|
-
if (evaluateGuardrail({ kind: "content", content: prompt, scope: root }).action !== "block")
|
|
311
|
-
const context = await routeDebugPrompt(root, input, harness).catch(error => `DEBUG intake unavailable: ${error instanceof Error ? error.message : "preserve current state"}`);
|
|
312
|
-
if (context !== null)
|
|
313
|
-
io.writeStdout(harness === "claude" ? JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: context } }) : context + "\n");
|
|
307
|
+
if (evaluateGuardrail({ kind: "content", content: prompt, scope: root }).action !== "block")
|
|
314
308
|
return 0;
|
|
315
|
-
}
|
|
316
309
|
if (!config.profiles.includes("guardrails") && !config.profiles.includes("loop") && !config.profiles.includes("run"))
|
|
317
310
|
return 0;
|
|
318
311
|
const reason = "agent-ops blocked a suspected secret.";
|
|
@@ -329,28 +322,14 @@ async function promptSubmitted(harness, io, dependencies) {
|
|
|
329
322
|
}
|
|
330
323
|
export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
331
324
|
const [harness, event] = argv;
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const raw = await readStdin(io.stdin), input = parseInput(raw);
|
|
335
|
-
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
336
|
-
return 0;
|
|
337
|
-
const fields = input;
|
|
338
|
-
if (typeof fields.session_id !== "string" || fields.session_id.length === 0)
|
|
339
|
-
return 0;
|
|
340
|
-
const root = dependencies.root ?? (typeof fields.cwd === "string" ? fields.cwd : process.cwd());
|
|
341
|
-
await interruptDebugSession(root, fields.session_id, raw);
|
|
342
|
-
}
|
|
343
|
-
catch {
|
|
344
|
-
io.writeStdout(JSON.stringify({ systemMessage: "agent-ops: DEBUG interrupt progress could not be sealed; preserve the task and run agent-ops doctor before resuming." }));
|
|
345
|
-
}
|
|
325
|
+
// Pre-0.11 installs registered Interrupt only for DEBUG; it is now a no-op until update removes it.
|
|
326
|
+
if (harness === "codex" && event === "Interrupt")
|
|
346
327
|
return 0;
|
|
347
|
-
}
|
|
348
328
|
if (harness === "claude" && event === "SessionEnd")
|
|
349
329
|
return await closeOfficeSession(io, dependencies);
|
|
350
330
|
if ((harness === "claude" || harness === "codex") && event === "UserPromptSubmit")
|
|
351
331
|
return await promptSubmitted(harness, io, dependencies);
|
|
352
|
-
// The generic completion gate requires an approval escape hatch
|
|
353
|
-
// only enforces DEBUG through runtime checkpoints and native human control.
|
|
332
|
+
// The generic completion gate requires an approval escape hatch, which Codex lacks.
|
|
354
333
|
const completionGateInstalled = (harness === "agy" || harness === "claude") &&
|
|
355
334
|
event === "Stop" &&
|
|
356
335
|
argv.includes("--completion-gate");
|
|
@@ -400,18 +379,7 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
400
379
|
}
|
|
401
380
|
}
|
|
402
381
|
let configOutcome = await hookConfigOutcome(root, dependencies.loadConfig);
|
|
403
|
-
const nativeStop = normalizeHookInput(harnessId, parsedInput);
|
|
404
|
-
if (harnessId === "codex" && hookEvent === "Stop" && configOutcome.kind === "invalid")
|
|
405
|
-
configOutcome = await hookConfigOutcome(root, dependencies.loadConfig);
|
|
406
382
|
if (configOutcome.kind === "invalid") {
|
|
407
|
-
if (harnessId === "codex" && nativeStop?.event === "stop" && nativeStop.sessionId !== undefined) {
|
|
408
|
-
writeHookOutput(io, harnessDescriptor(harnessId).runtime.formatOutput("Stop", {
|
|
409
|
-
action: "continue", status: "UNKNOWN", code: "TASK_DEBUG_GATE_UNAVAILABLE",
|
|
410
|
-
remedy: "DEBUG remains incomplete: configuration failed after one targeted reload. Preserve the task, evidence and budgets; fix .agent-ops/config.json and run agent-ops doctor. No completion is attested."
|
|
411
|
-
}));
|
|
412
|
-
await closeDebugSession(root, nativeStop.sessionId, harnessId).catch(() => undefined);
|
|
413
|
-
return 0;
|
|
414
|
-
}
|
|
415
383
|
if (completionGateInstalled) {
|
|
416
384
|
writeHookOutput(io, harnessDescriptor(harness).runtime.formatOutput("Stop", {
|
|
417
385
|
action: "block",
|
|
@@ -434,15 +402,6 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
434
402
|
return 0;
|
|
435
403
|
}
|
|
436
404
|
const config = configOutcome.config;
|
|
437
|
-
if (harnessId === "codex" && hookEvent === "Stop" && nativeStop !== null && config.features.debug?.enabled === true) {
|
|
438
|
-
const result = await debugStopGate(root, config).handle(nativeStop);
|
|
439
|
-
if (result !== null) {
|
|
440
|
-
writeHookOutput(io, harnessDescriptor(harnessId).runtime.formatOutput("Stop", result));
|
|
441
|
-
if (result.action === "continue" && nativeStop.sessionId !== undefined)
|
|
442
|
-
await closeDebugSession(root, nativeStop.sessionId, harnessId).catch(() => undefined);
|
|
443
|
-
return 0;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
405
|
if (completionGateInstalled && !config.features.completionGate.enabled) {
|
|
447
406
|
writeHookOutput(io, harnessDescriptor(harness).runtime.formatOutput("Stop", {
|
|
448
407
|
action: "block",
|
|
@@ -457,8 +416,6 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
457
416
|
: await dependencies.trust(root, config, cliVersion);
|
|
458
417
|
const trusted = trustStatus === "TRUSTED";
|
|
459
418
|
const normalized = normalizeHookInput(harnessId, parsedInput);
|
|
460
|
-
if (normalized?.event === "session-start" && normalized.sessionId !== undefined)
|
|
461
|
-
await startDebugSession(root, normalized.sessionId, harnessId).catch(() => undefined);
|
|
462
419
|
if (dependencies.office !== undefined && config.features.office?.enabled === true) {
|
|
463
420
|
if (normalized !== null) {
|
|
464
421
|
const phase = officePhaseHint(normalized) ?? (harnessId === "codex" ? officeLoopPhaseHint(parsedInput, root) : undefined);
|
|
@@ -554,43 +511,22 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
554
511
|
await recordSessionOrigin(dependencies.worktree, { cwd: root, sessionId }).catch(() => undefined);
|
|
555
512
|
}
|
|
556
513
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
input: parsedInput,
|
|
573
|
-
config,
|
|
574
|
-
trusted,
|
|
575
|
-
onResult: result => { hookResult = result; },
|
|
576
|
-
...(worktreeGuard === undefined ? {} : { worktreeGuard }),
|
|
577
|
-
...(worktreeEnter === undefined ? {} : { worktreeEnter }),
|
|
578
|
-
...(dependencies.advisory === undefined
|
|
579
|
-
? {}
|
|
580
|
-
: { advisory: dependencies.advisory }),
|
|
581
|
-
...(stopVerification === undefined ? {} : { stopVerification }),
|
|
582
|
-
...(completionGate === undefined ? {} : { completionGate })
|
|
583
|
-
});
|
|
514
|
+
const output = await runHookCommand({
|
|
515
|
+
harness: harness,
|
|
516
|
+
event: hookEvent,
|
|
517
|
+
stdin: rawInput,
|
|
518
|
+
input: parsedInput,
|
|
519
|
+
config,
|
|
520
|
+
trusted,
|
|
521
|
+
...(worktreeGuard === undefined ? {} : { worktreeGuard }),
|
|
522
|
+
...(worktreeEnter === undefined ? {} : { worktreeEnter }),
|
|
523
|
+
...(dependencies.advisory === undefined
|
|
524
|
+
? {}
|
|
525
|
+
: { advisory: dependencies.advisory }),
|
|
526
|
+
...(stopVerification === undefined ? {} : { stopVerification }),
|
|
527
|
+
...(completionGate === undefined ? {} : { completionGate })
|
|
528
|
+
});
|
|
584
529
|
writeHookOutput(io, output);
|
|
585
|
-
// A Stop can be advisory or gated, but the foreground clock only ends on
|
|
586
|
-
// the same trusted idle model_stop that the native decision accepted.
|
|
587
|
-
if (normalized?.event === "stop" &&
|
|
588
|
-
normalized.sessionId !== undefined &&
|
|
589
|
-
normalized.terminationReason === "model_stop" &&
|
|
590
|
-
normalized.fullyIdle === true &&
|
|
591
|
-
hookResult?.action === "continue") {
|
|
592
|
-
await closeDebugSession(root, normalized.sessionId, harnessId).catch(() => undefined);
|
|
593
|
-
}
|
|
594
530
|
}
|
|
595
531
|
catch {
|
|
596
532
|
if (completionGateInstalled) {
|
|
@@ -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 = remainingRunTimeMs(state);
|
|
25
|
+
const remaining = argv[0] === "review" && argv.includes("--criteria") ? Infinity : 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));
|