@kylecheng3146/agent-ops 0.7.3 → 0.8.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 +2 -0
- package/dist/packages/cli/src/args.js +72 -1
- package/dist/packages/cli/src/bin.js +69 -10
- package/dist/packages/cli/src/cli.js +11 -3
- package/dist/packages/cli/src/codex-loop-process.js +6 -0
- package/dist/packages/cli/src/commands/batch.js +3 -1
- package/dist/packages/cli/src/commands/hook.js +4 -1
- package/dist/packages/cli/src/commands/review.js +9 -3
- package/dist/packages/cli/src/commands/task.js +44 -2
- package/dist/packages/cli/src/commands/update.js +18 -0
- package/dist/packages/cli/src/commands/verify.js +3 -1
- package/dist/packages/cli/src/debug-intake.js +245 -0
- package/dist/packages/cli/src/hook-process.js +155 -34
- package/dist/packages/cli/src/office-entry.js +34 -6
- package/dist/packages/cli/src/run-deps.js +8 -0
- package/dist/runtime/src/adapters/agy/config.js +3 -2
- package/dist/runtime/src/adapters/claude/config.js +7 -3
- package/dist/runtime/src/adapters/codex/config.js +10 -3
- package/dist/runtime/src/adapters/codex/events.js +2 -1
- package/dist/runtime/src/adapters/codex/input.js +20 -2
- package/dist/runtime/src/adapters/codex/output.js +4 -1
- package/dist/runtime/src/config/merge.js +1 -1
- package/dist/runtime/src/hooks/completion-gate.js +15 -0
- package/dist/runtime/src/install/harness.js +2 -0
- package/dist/runtime/src/install/plan.js +3 -3
- package/dist/runtime/src/install/profiles.js +2 -0
- package/dist/runtime/src/install/update.js +10 -2
- package/dist/runtime/src/office/activity.js +105 -0
- package/dist/runtime/src/office/art.js +1000 -0
- package/dist/runtime/src/office/collect.js +137 -10
- package/dist/runtime/src/office/layout.js +203 -0
- package/dist/runtime/src/office/page.js +883 -644
- package/dist/runtime/src/office/scene.js +38 -33
- package/dist/runtime/src/office/sessions.js +18 -1
- package/dist/runtime/src/office/snapshot.js +74 -1
- package/dist/runtime/src/parallel/finish.js +4 -2
- package/dist/runtime/src/parallel/manage.js +19 -2
- package/dist/runtime/src/review/attestation.js +4 -1
- package/dist/runtime/src/review/batch.js +10 -0
- package/dist/runtime/src/review/execute.js +10 -1
- package/dist/runtime/src/review/slots.js +59 -44
- package/dist/runtime/src/run/hosts/codex.js +1 -1
- package/dist/runtime/src/run/transport.js +3 -0
- package/dist/runtime/src/schema/validate.js +8 -2
- package/dist/runtime/src/security/permissions.js +3 -1
- package/dist/runtime/src/task/completion.js +20 -1
- package/dist/runtime/src/task/contract.js +1 -1
- package/dist/runtime/src/task/debug-intake.js +12 -0
- package/dist/runtime/src/task/debug-state.js +130 -0
- package/dist/runtime/src/task/debug-user-event.js +93 -0
- package/dist/runtime/src/task/debug.js +586 -0
- package/dist/runtime/src/task/service.js +46 -0
- package/dist/runtime/src/task/store.js +8 -1
- package/dist/runtime/src/verify/acceptance-replay.js +9 -4
- package/dist/runtime/src/verify/service.js +21 -2
- package/docs/en/guides/configuration.md +7 -0
- package/docs/en/guides/task-debug.md +111 -0
- package/docs/en/spec/harness-adapters.md +31 -4
- package/docs/zh-TW/guides/configuration.md +6 -0
- package/docs/zh-TW/guides/task-debug.md +66 -0
- package/docs/zh-TW/spec/harness-adapters.md +23 -4
- package/package.json +1 -1
- package/schemas/config.schema.json +30 -2
- package/schemas/manifest.schema.json +1 -0
- package/schemas/task.schema.json +62 -2
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
3
|
+
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
4
|
+
import { calculateConfigHash } from "../../../runtime/src/config/hash.js";
|
|
5
|
+
import { evaluateGuardrail } from "../../../runtime/src/guardrails/evaluate.js";
|
|
6
|
+
import { isDebugIssue } from "../../../runtime/src/task/debug-intake.js";
|
|
7
|
+
import { ensureSessionWorktree, readWorktreeRecord, sessionWorktreeName } from "../../../runtime/src/parallel/service.js";
|
|
8
|
+
import { listWorktrees, resumeWorktree } from "../../../runtime/src/parallel/manage.js";
|
|
9
|
+
import { collectChangeSurface } from "../../../runtime/src/verify/change-surface.js";
|
|
10
|
+
import { calculateSourceFingerprint } from "../../../runtime/src/verify/source-fingerprint.js";
|
|
11
|
+
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
12
|
+
import { loadEffectiveConfig } from "./context.js";
|
|
13
|
+
import { gitRunner, worktreeDependencies } from "./parallel-deps.js";
|
|
14
|
+
const foregroundClockSupported = (host) => host !== "opencode";
|
|
15
|
+
function nativeSessionId(input) {
|
|
16
|
+
const fromInput = [input.session_id, input.sessionId, input.conversationId, input.thread_id, input.threadId]
|
|
17
|
+
.find((value) => typeof value === "string" && value.length > 0);
|
|
18
|
+
return fromInput ?? [process.env.AGENT_OPS_SESSION_ID, process.env.CODEX_THREAD_ID]
|
|
19
|
+
.find((value) => value !== undefined && value.length > 0);
|
|
20
|
+
}
|
|
21
|
+
/** Start only an already attached DEBUG issue; native hooks never create work. */
|
|
22
|
+
export async function startDebugSession(root, sessionId, host) {
|
|
23
|
+
if (!foregroundClockSupported(host) || sessionId.length === 0)
|
|
24
|
+
return;
|
|
25
|
+
const config = (await loadEffectiveConfig(root, "project")).config;
|
|
26
|
+
const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId)));
|
|
27
|
+
const contextRoot = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
|
|
28
|
+
const contextConfig = contextRoot === root ? config : (await loadEffectiveConfig(contextRoot, "project")).config;
|
|
29
|
+
const tasks = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: gitRunner(contextRoot), loadConfig: async () => contextConfig } });
|
|
30
|
+
const attached = await tasks.status({ sessionId }).catch(() => null);
|
|
31
|
+
if (attached?.status === "active" && attached.task.debug !== undefined)
|
|
32
|
+
await tasks.debugService().foregroundStart(attached.task.id, sessionId);
|
|
33
|
+
}
|
|
34
|
+
export async function routeDebugPrompt(root, input, host = "claude") {
|
|
35
|
+
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
36
|
+
return null;
|
|
37
|
+
const fields = input;
|
|
38
|
+
if (typeof fields.prompt !== "string")
|
|
39
|
+
return null;
|
|
40
|
+
const continuation = /^(?:繼續|继续|continue)[。.!!]?$/iu.test(fields.prompt.trim());
|
|
41
|
+
const debugIssue = isDebugIssue(fields.prompt);
|
|
42
|
+
const config = (await loadEffectiveConfig(root, "project")).config;
|
|
43
|
+
if (config.features.debug?.enabled !== true)
|
|
44
|
+
return null;
|
|
45
|
+
if (evaluateGuardrail({ kind: "content", content: fields.prompt, scope: root }).action === "block")
|
|
46
|
+
return null;
|
|
47
|
+
const session = nativeSessionId(fields);
|
|
48
|
+
if (session === undefined) {
|
|
49
|
+
if (!debugIssue && !continuation)
|
|
50
|
+
return null;
|
|
51
|
+
return "DEBUG intake unsupported: this hook has no reliable native session identity. Use task create --debug --goal with the original issue in the native conversation.";
|
|
52
|
+
}
|
|
53
|
+
const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(session)));
|
|
54
|
+
const contextRoot = saved?.mainRoot === root && saved.sessionId === session ? saved.path : root;
|
|
55
|
+
const contextConfig = contextRoot === root ? config : (await loadEffectiveConfig(contextRoot, "project")).config;
|
|
56
|
+
const original = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: gitRunner(contextRoot), loadConfig: async () => contextConfig } });
|
|
57
|
+
const attached = await original.status({ sessionId: session }).catch(() => null);
|
|
58
|
+
if (attached?.status === "active" && attached.task.debug !== undefined) {
|
|
59
|
+
await original.debugService().syncNativeControl(attached.task.id, session);
|
|
60
|
+
const control = (await original.debugService().root(attached.task.id)).debug.control;
|
|
61
|
+
if (control?.paused === true)
|
|
62
|
+
return `DEBUG original issue ${attached.task.id} is paused by the human. Preserve its task, evidence and remaining budget; stop after saving progress. Only a native human continue event resumes it.`;
|
|
63
|
+
if (continuation) {
|
|
64
|
+
let message = `DEBUG continue preserved original issue ${attached.task.id}: native user event did not extend time; other limits remain authoritative.`;
|
|
65
|
+
try {
|
|
66
|
+
await original.debugService().extend(attached.task.id, session, "latest");
|
|
67
|
+
message = `DEBUG original issue ${attached.task.id}: native human time grant applied once; other limits remain authoritative.`;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
message = `DEBUG continue preserved original issue ${attached.task.id}: ${error instanceof Error ? error.message : "Native user approval unavailable"}`;
|
|
71
|
+
}
|
|
72
|
+
if (foregroundClockSupported(host)) {
|
|
73
|
+
try {
|
|
74
|
+
await original.debugService().foregroundStart(attached.task.id, session);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
message += ` Foreground clock unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
message += " Foreground clock unsupported on this host; preserve the issue and use a host with a trustworthy end-of-turn signal.";
|
|
82
|
+
if (control?.mode === "diagnose")
|
|
83
|
+
message += " Diagnosis-only scope remains active; continue discriminating evidence without product repairs until a proven diagnosis or runtime blocker. Only a native human resume repair event restores the full loop.";
|
|
84
|
+
return message;
|
|
85
|
+
}
|
|
86
|
+
if (foregroundClockSupported(host)) {
|
|
87
|
+
try {
|
|
88
|
+
await original.debugService().foregroundStart(attached.task.id, session);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
return `DEBUG original issue ${attached.task.id}: foreground clock unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (control?.mode === "diagnose")
|
|
95
|
+
return `DEBUG original issue ${attached.task.id}: diagnosis only. Continue reproduction and hypothesis evidence until a supported diagnosis or a runtime blocker. Do not repair product behavior. A failure to reproduce is not a stop condition; a human resume repair event restores the full loop.`;
|
|
96
|
+
if (!debugIssue)
|
|
97
|
+
return null;
|
|
98
|
+
return `Continue original DEBUG issue ${attached.task.id}; retain its existing budget, evidence and hypotheses.`;
|
|
99
|
+
}
|
|
100
|
+
if (!debugIssue && !continuation)
|
|
101
|
+
return null;
|
|
102
|
+
if (continuation) {
|
|
103
|
+
if (attached?.status !== "active" || attached.task.debug === undefined)
|
|
104
|
+
return null;
|
|
105
|
+
try {
|
|
106
|
+
await original.debugService().extend(attached.task.id, session, "latest");
|
|
107
|
+
return `DEBUG original issue ${attached.task.id}: native human time grant applied once; other limits remain authoritative.`;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return `DEBUG continue preserved original issue ${attached.task.id}: ${error instanceof Error ? error.message : "Native user approval unavailable"}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (attached?.status === "active")
|
|
114
|
+
return attached.task.debug === undefined ? null : `Continue original DEBUG issue ${attached.task.id}; retain its existing budget, evidence and hypotheses.`;
|
|
115
|
+
// Another session's worktree may already hold this issue; reusing it keeps
|
|
116
|
+
// its consumed budget and rounds instead of starting a fresh task.
|
|
117
|
+
const holders = await listWorktrees(worktreeDependencies(), root).catch(() => []);
|
|
118
|
+
for (const { record: held } of holders) {
|
|
119
|
+
if (held.sessionId === session)
|
|
120
|
+
continue;
|
|
121
|
+
const heldConfig = (await loadEffectiveConfig(held.path, "project").catch(() => null))?.config;
|
|
122
|
+
if (heldConfig === undefined)
|
|
123
|
+
continue;
|
|
124
|
+
const issue = await new TaskService(new FileTaskStore(join(held.path, ".agent-ops/tasks/state.json"), held.path), { completion: { root: held.path, gitRunner: gitRunner(held.path), loadConfig: async () => heldConfig } }).activeDebugRoot(fields.prompt).catch(() => null);
|
|
125
|
+
if (issue === null)
|
|
126
|
+
continue;
|
|
127
|
+
try {
|
|
128
|
+
await resumeWorktree(worktreeDependencies(), { cwd: root, name: held.name, sessionId: session });
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
return `DEBUG original issue ${issue.task.id} is still active in ${held.path}; a new task would reset its budget. ${error instanceof Error ? error.message : "Resume needs confirmation."} After the user confirms, run agent-ops worktree resume ${held.name} --session ${session}.`;
|
|
132
|
+
}
|
|
133
|
+
return `Continue original DEBUG issue ${issue.task.id} in ${held.path}; its budget, rounds and evidence are preserved.`;
|
|
134
|
+
}
|
|
135
|
+
await original.assertDebugBaseline();
|
|
136
|
+
const record = config.worktree?.mode === "auto" && await readWorktreeRecord(root) === null ?
|
|
137
|
+
await ensureSessionWorktree(worktreeDependencies(), { cwd: root, sessionId: session }) : null;
|
|
138
|
+
const path = record?.path ?? root;
|
|
139
|
+
const effective = (await loadEffectiveConfig(path, "project")).config;
|
|
140
|
+
const tasks = new TaskService(new FileTaskStore(join(path, ".agent-ops/tasks/state.json"), path), { completion: { root: path, gitRunner: gitRunner(path), loadConfig: async () => effective } });
|
|
141
|
+
const existing = await tasks.status({ sessionId: session }).catch(() => null);
|
|
142
|
+
if (existing?.status === "active")
|
|
143
|
+
return existing.task.debug === undefined ? null : `Continue original DEBUG issue ${existing.task.id}.`;
|
|
144
|
+
const held = await tasks.activeDebugRoot(fields.prompt);
|
|
145
|
+
if (held !== null) {
|
|
146
|
+
await tasks.attach(session, held.task.id);
|
|
147
|
+
let message = `Continue original DEBUG issue ${held.task.id}; its budget, rounds and evidence are preserved.`;
|
|
148
|
+
if (foregroundClockSupported(host)) {
|
|
149
|
+
try {
|
|
150
|
+
await tasks.debugService().foregroundStart(held.task.id, session);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
message += ` Foreground clock unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return message;
|
|
157
|
+
}
|
|
158
|
+
const ids = effective.verification.commands.filter(c => c.required).map(c => c.id);
|
|
159
|
+
const baseline = (await gitRunner(path).run(["rev-parse", "--verify", "HEAD"])).stdout.toString().trim();
|
|
160
|
+
const criteria = ["issue-regression", "related-checks"].map(id => ({ id, description: id === "issue-regression" ? "Reproduce and fix the original reported issue with relevant regression evidence." : "Pass necessary related checks and two fresh independent reviews.", verifierIds: ids,
|
|
161
|
+
acceptance: { mode: "review-only", baselineCommit: baseline, bindings: [], reviewOnlyReason: "Provisional investigation intake only. Identify and revise to executable original-issue regression before debug success; this criterion cannot prove reproduction." } }));
|
|
162
|
+
const created = await tasks.create({ title: fields.prompt.split("\n")[0].slice(0, 160), goal: fields.prompt, intent: "Investigate and fix the original issue through authoritative task debug evidence.", debug: { facts: ["User-reported symptom is unverified."], unknowns: ["Baseline reproduction", "Root cause", "Relevant regression and related checks"] }, criteria, sessionId: session, policyConfigHash: calculateConfigHash(effective) });
|
|
163
|
+
let clockNote = "";
|
|
164
|
+
if (foregroundClockSupported(host)) {
|
|
165
|
+
try {
|
|
166
|
+
await tasks.debugService().foregroundStart(created.task.id, session);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return `DEBUG issue ${created.task.id} created in ${path}, but its foreground clock is unavailable: ${error instanceof Error ? error.message : "host lifecycle is unsupported"}. Preserve the issue and do not claim timed completion.`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else
|
|
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, 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}`;
|
|
175
|
+
}
|
|
176
|
+
export async function interruptDebugSession(root, sessionId, nativeInput) {
|
|
177
|
+
const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId)));
|
|
178
|
+
const path = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
|
|
179
|
+
const tasks = new TaskService(new FileTaskStore(join(path, ".agent-ops/tasks/state.json"), path), { completion: { root: path, gitRunner: gitRunner(path), loadConfig: async () => (await loadEffectiveConfig(path, "project")).config } });
|
|
180
|
+
const attached = await tasks.status({ sessionId }).catch(error => {
|
|
181
|
+
if (error instanceof AgentOpsError && error.code === "TASK_SESSION_UNATTACHED")
|
|
182
|
+
return null;
|
|
183
|
+
throw error;
|
|
184
|
+
});
|
|
185
|
+
if (attached?.status === "active" && attached.task.debug !== undefined)
|
|
186
|
+
await tasks.debugService().interrupt(attached.task.id, sessionId, nativeInput);
|
|
187
|
+
}
|
|
188
|
+
/** Close only the task attached to a native host's verified session. */
|
|
189
|
+
export async function closeDebugSession(root, sessionId, host = "claude") {
|
|
190
|
+
if (!foregroundClockSupported(host))
|
|
191
|
+
return;
|
|
192
|
+
if (sessionId.length === 0)
|
|
193
|
+
return;
|
|
194
|
+
const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId)));
|
|
195
|
+
const contextRoot = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
|
|
196
|
+
const tasks = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: gitRunner(contextRoot), loadConfig: async () => (await loadEffectiveConfig(contextRoot, "project")).config } });
|
|
197
|
+
const attached = await tasks.status({ sessionId }).catch(() => null);
|
|
198
|
+
if (attached?.status === "active" && attached.task.debug !== undefined)
|
|
199
|
+
await tasks.debugService().foregroundStop(attached.task.id, sessionId);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Without the completion gate, an active DEBUG issue still cannot end a turn
|
|
203
|
+
* unless a current runtime stop checkpoint exists. Sessions without one pass.
|
|
204
|
+
*/
|
|
205
|
+
export function debugStopGate(root, rootConfig) {
|
|
206
|
+
return { handle: async (event) => {
|
|
207
|
+
if (event.event !== "stop" || event.sessionId === undefined || event.sessionId.length === 0 ||
|
|
208
|
+
event.terminationReason !== "model_stop" || event.fullyIdle !== true)
|
|
209
|
+
return null;
|
|
210
|
+
const sessionId = event.sessionId;
|
|
211
|
+
const evaluate = async (attempt) => {
|
|
212
|
+
const saved = await readWorktreeRecord(join(root, ".worktrees", sessionWorktreeName(sessionId))).catch(() => null);
|
|
213
|
+
const contextRoot = saved?.mainRoot === root && saved.sessionId === sessionId ? saved.path : root;
|
|
214
|
+
const config = contextRoot === root && attempt === 0 ? rootConfig : (await loadEffectiveConfig(contextRoot, "project")).config;
|
|
215
|
+
const runner = gitRunner(contextRoot);
|
|
216
|
+
const tasks = new TaskService(new FileTaskStore(join(contextRoot, ".agent-ops/tasks/state.json"), contextRoot), { completion: { root: contextRoot, gitRunner: runner, loadConfig: async () => config } });
|
|
217
|
+
const attached = await tasks.status({ sessionId }).catch(error => {
|
|
218
|
+
if (error instanceof AgentOpsError && error.code === "TASK_SESSION_UNATTACHED")
|
|
219
|
+
return null;
|
|
220
|
+
throw error;
|
|
221
|
+
});
|
|
222
|
+
if (attached?.status !== "active" || attached.task.debug === undefined)
|
|
223
|
+
return null;
|
|
224
|
+
await tasks.debugService().prepareStop(attached.task.id, sessionId);
|
|
225
|
+
const surface = await collectChangeSurface(runner);
|
|
226
|
+
const fingerprint = await calculateSourceFingerprint(contextRoot, { mode: "worktree", changedFiles: surface.paths }, runner);
|
|
227
|
+
return await tasks.debugService().validCheckpoint(attached.task.id, sessionId, fingerprint)
|
|
228
|
+
? { action: "continue", status: "UNKNOWN", code: "COMPLETION_GATE_TASK_DEBUG_STOPPED", remedy: "Original issue remains active with a runtime-bound non-success checkpoint." }
|
|
229
|
+
: { action: "block", status: "FAIL", code: "COMPLETION_GATE_TASK_DEBUG_ACTIVE", remedy: "Continue the original DEBUG investigation, repair, verification and review loop. Failure to reproduce and an arbitrary checkpoint do not permit stopping. Preserve the task and budgets; record a current runtime stop checkpoint only for a proven blocker or exhausted limits." };
|
|
230
|
+
};
|
|
231
|
+
// Reload the task/config once for a transient gate fault; never rewrite bad evidence.
|
|
232
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
233
|
+
try {
|
|
234
|
+
return await evaluate(attempt);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
if (attempt === 0)
|
|
238
|
+
continue;
|
|
239
|
+
const code = error instanceof AgentOpsError ? error.code : "IO_ERROR";
|
|
240
|
+
return { action: "continue", status: "UNKNOWN", code: "TASK_DEBUG_GATE_UNAVAILABLE", remedy: `DEBUG remains incomplete: gate ${code} failed after one targeted reload. Preserve the original task, evidence and budgets; run agent-ops doctor and restore readable task/config evidence before resuming. No completion is attested.` };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
} };
|
|
245
|
+
}
|
|
@@ -14,12 +14,14 @@ 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";
|
|
17
18
|
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
18
19
|
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
19
20
|
import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
|
|
20
21
|
import { readWorktreeRecord } from "../../../runtime/src/parallel/service.js";
|
|
21
22
|
import { FileRunRepository } from "../../../runtime/src/run/service.js";
|
|
22
23
|
import { validateWorkerStopHandoff } from "../../../runtime/src/run/worker-gate.js";
|
|
24
|
+
import { evaluateGuardrail } from "../../../runtime/src/guardrails/evaluate.js";
|
|
23
25
|
import { STOP_VERIFICATION_ENV, StopVerificationService } from "../../../runtime/src/hooks/stop-service.js";
|
|
24
26
|
import { NodeVerificationProcessRunner } from "../../../runtime/src/verify/spawn.js";
|
|
25
27
|
import { normalizeHookInput, runHookCommand, HOOK_EVENTS } from "./commands/hook.js";
|
|
@@ -53,13 +55,19 @@ function parseInput(source) {
|
|
|
53
55
|
}
|
|
54
56
|
function withInjectedSession(harness, input) {
|
|
55
57
|
const sessionId = process.env.AGENT_OPS_SESSION_ID;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
typeof input
|
|
59
|
-
input
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
const codexSession = sessionId ?? process.env.CODEX_THREAD_ID;
|
|
59
|
+
if ((harness !== "agy" && harness !== "codex") ||
|
|
60
|
+
typeof input !== "object" || input === null || Array.isArray(input))
|
|
61
|
+
return input;
|
|
62
|
+
const fields = input;
|
|
63
|
+
if (harness === "agy") {
|
|
64
|
+
return (typeof fields.conversationId === "string" && fields.conversationId.length > 0) || sessionId === undefined
|
|
65
|
+
? input
|
|
66
|
+
: { ...fields, conversationId: sessionId };
|
|
67
|
+
}
|
|
68
|
+
return (typeof fields.session_id === "string" && fields.session_id.length > 0) || codexSession === undefined
|
|
69
|
+
? input
|
|
70
|
+
: { ...fields, session_id: codexSession };
|
|
63
71
|
}
|
|
64
72
|
function writeHookOutput(io, output) {
|
|
65
73
|
if (output.stdout.length > 0) {
|
|
@@ -225,7 +233,7 @@ function completionGateFor(root, config, gitRunner, harness) {
|
|
|
225
233
|
config,
|
|
226
234
|
gitRunner,
|
|
227
235
|
...(harness === undefined ? {} : { harness }),
|
|
228
|
-
taskService: new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root)),
|
|
236
|
+
taskService: new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root), { completion: { root, gitRunner, loadConfig: async () => config } }),
|
|
229
237
|
evidenceStore: new FileEvidenceStore(root, root),
|
|
230
238
|
forRoot: async (worktree) => {
|
|
231
239
|
const outcome = await loadProjectHookConfig(worktree);
|
|
@@ -246,7 +254,7 @@ function completionGateFor(root, config, gitRunner, harness) {
|
|
|
246
254
|
*/
|
|
247
255
|
async function closeOfficeSession(io, dependencies) {
|
|
248
256
|
try {
|
|
249
|
-
if (process.env.AGENT_OPS_DISABLE === "1"
|
|
257
|
+
if (process.env.AGENT_OPS_DISABLE === "1")
|
|
250
258
|
return 0;
|
|
251
259
|
const input = parseInput(await readStdin(io.stdin));
|
|
252
260
|
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
@@ -254,26 +262,95 @@ async function closeOfficeSession(io, dependencies) {
|
|
|
254
262
|
const fields = input;
|
|
255
263
|
const root = dependencies.root ?? (typeof fields.cwd === "string" ? fields.cwd : process.cwd());
|
|
256
264
|
const outcome = await hookConfigOutcome(root, dependencies.loadConfig);
|
|
257
|
-
if (outcome.kind === "invalid"
|
|
265
|
+
if (outcome.kind === "invalid")
|
|
258
266
|
return 0;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
267
|
+
if (dependencies.office !== undefined && outcome.config.features.office?.enabled === true)
|
|
268
|
+
await dependencies.office({
|
|
269
|
+
root, harness: "claude", event: "SessionEnd", input, validated: true,
|
|
270
|
+
...(typeof fields.session_id === "string" ? { sessionId: fields.session_id } : {})
|
|
271
|
+
});
|
|
272
|
+
if (typeof fields.session_id === "string")
|
|
273
|
+
await closeDebugSession(root, fields.session_id, "claude").catch(() => undefined);
|
|
263
274
|
}
|
|
264
275
|
catch {
|
|
265
276
|
// Office never blocks a session, least of all one that is ending.
|
|
266
277
|
}
|
|
267
278
|
return 0;
|
|
268
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* A prompt submitted outside the project loop: Office records the turn's work
|
|
282
|
+
* starting, and command policy refuses a literal credential the way the loop
|
|
283
|
+
* does. Every runtime failure lets the prompt through.
|
|
284
|
+
*/
|
|
285
|
+
async function promptSubmitted(harness, io, dependencies) {
|
|
286
|
+
try {
|
|
287
|
+
if (process.env.AGENT_OPS_DISABLE === "1")
|
|
288
|
+
return 0;
|
|
289
|
+
const input = parseInput(await readStdin(io.stdin));
|
|
290
|
+
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
291
|
+
return 0;
|
|
292
|
+
const fields = input;
|
|
293
|
+
const root = dependencies.root ?? (typeof fields.cwd === "string" ? fields.cwd : process.cwd());
|
|
294
|
+
const outcome = await hookConfigOutcome(root, dependencies.loadConfig);
|
|
295
|
+
if (outcome.kind === "invalid")
|
|
296
|
+
return 0;
|
|
297
|
+
const config = outcome.config;
|
|
298
|
+
if (dependencies.office !== undefined && config.features.office?.enabled === true) {
|
|
299
|
+
await dependencies.office({
|
|
300
|
+
root, harness, event: "UserPromptSubmit", input, validated: true,
|
|
301
|
+
...(typeof fields.session_id === "string" ? { sessionId: fields.session_id } : {})
|
|
302
|
+
}).catch(() => undefined);
|
|
303
|
+
}
|
|
304
|
+
if (config.profiles.length === 0)
|
|
305
|
+
return 0;
|
|
306
|
+
// ponytail: the loop's 64 KiB prompt bound; a larger prompt goes unchecked.
|
|
307
|
+
const prompt = fields.prompt;
|
|
308
|
+
if (typeof prompt !== "string" || prompt.length === 0 || prompt.length > 64 * 1024 || prompt.includes("\0"))
|
|
309
|
+
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");
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
if (!config.profiles.includes("guardrails") && !config.profiles.includes("loop") && !config.profiles.includes("run"))
|
|
317
|
+
return 0;
|
|
318
|
+
const reason = "agent-ops blocked a suspected secret.";
|
|
319
|
+
if (harness === "claude") {
|
|
320
|
+
io.writeStdout(JSON.stringify({ decision: "block", reason }));
|
|
321
|
+
return 0;
|
|
322
|
+
}
|
|
323
|
+
io.writeStderr(reason + "\n");
|
|
324
|
+
return 2;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
269
330
|
export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
270
331
|
const [harness, event] = argv;
|
|
332
|
+
if (harness === "codex" && event === "Interrupt") {
|
|
333
|
+
try {
|
|
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
|
+
}
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
271
348
|
if (harness === "claude" && event === "SessionEnd")
|
|
272
349
|
return await closeOfficeSession(io, dependencies);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
//
|
|
276
|
-
//
|
|
350
|
+
if ((harness === "claude" || harness === "codex") && event === "UserPromptSubmit")
|
|
351
|
+
return await promptSubmitted(harness, io, dependencies);
|
|
352
|
+
// The generic completion gate requires an approval escape hatch. Codex
|
|
353
|
+
// only enforces DEBUG through runtime checkpoints and native human control.
|
|
277
354
|
const completionGateInstalled = (harness === "agy" || harness === "claude") &&
|
|
278
355
|
event === "Stop" &&
|
|
279
356
|
argv.includes("--completion-gate");
|
|
@@ -322,8 +399,19 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
322
399
|
root = inputRoot;
|
|
323
400
|
}
|
|
324
401
|
}
|
|
325
|
-
|
|
402
|
+
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);
|
|
326
406
|
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
|
+
}
|
|
327
415
|
if (completionGateInstalled) {
|
|
328
416
|
writeHookOutput(io, harnessDescriptor(harness).runtime.formatOutput("Stop", {
|
|
329
417
|
action: "block",
|
|
@@ -346,6 +434,15 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
346
434
|
return 0;
|
|
347
435
|
}
|
|
348
436
|
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
|
+
}
|
|
349
446
|
if (completionGateInstalled && !config.features.completionGate.enabled) {
|
|
350
447
|
writeHookOutput(io, harnessDescriptor(harness).runtime.formatOutput("Stop", {
|
|
351
448
|
action: "block",
|
|
@@ -359,8 +456,10 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
359
456
|
? await repositoryTrust(root, config, cliVersion)
|
|
360
457
|
: await dependencies.trust(root, config, cliVersion);
|
|
361
458
|
const trusted = trustStatus === "TRUSTED";
|
|
459
|
+
const normalized = normalizeHookInput(harnessId, parsedInput);
|
|
460
|
+
if (normalized?.event === "session-start" && normalized.sessionId !== undefined)
|
|
461
|
+
await startDebugSession(root, normalized.sessionId, harnessId).catch(() => undefined);
|
|
362
462
|
if (dependencies.office !== undefined && config.features.office?.enabled === true) {
|
|
363
|
-
const normalized = normalizeHookInput(harnessId, parsedInput);
|
|
364
463
|
if (normalized !== null) {
|
|
365
464
|
const phase = officePhaseHint(normalized);
|
|
366
465
|
void dependencies.office({
|
|
@@ -452,21 +551,43 @@ export async function runHookProcess(argv, io, cliVersion, dependencies = {}) {
|
|
|
452
551
|
await recordSessionOrigin(dependencies.worktree, { cwd: root, sessionId }).catch(() => undefined);
|
|
453
552
|
}
|
|
454
553
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
config
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
554
|
+
let hookResult;
|
|
555
|
+
// Without the gate, DEBUG still refuses a Stop that has no valid stop
|
|
556
|
+
// checkpoint. Its own failure stays advisory, so it never blocks other work.
|
|
557
|
+
const debugStop = completionGate === undefined && hookEvent === "Stop" && normalized !== null &&
|
|
558
|
+
(harnessId === "agy" || harnessId === "claude" || harnessId === "codex") && config.features.debug?.enabled === true
|
|
559
|
+
? await debugStopGate(root, config).handle(normalized)
|
|
560
|
+
: null;
|
|
561
|
+
if (debugStop !== null)
|
|
562
|
+
hookResult = debugStop;
|
|
563
|
+
const output = debugStop !== null
|
|
564
|
+
? harnessDescriptor(harnessId).runtime.formatOutput("Stop", debugStop)
|
|
565
|
+
: await runHookCommand({
|
|
566
|
+
harness: harness,
|
|
567
|
+
event: hookEvent,
|
|
568
|
+
stdin: rawInput,
|
|
569
|
+
input: parsedInput,
|
|
570
|
+
config,
|
|
571
|
+
trusted,
|
|
572
|
+
onResult: result => { hookResult = result; },
|
|
573
|
+
...(worktreeGuard === undefined ? {} : { worktreeGuard }),
|
|
574
|
+
...(worktreeEnter === undefined ? {} : { worktreeEnter }),
|
|
575
|
+
...(dependencies.advisory === undefined
|
|
576
|
+
? {}
|
|
577
|
+
: { advisory: dependencies.advisory }),
|
|
578
|
+
...(stopVerification === undefined ? {} : { stopVerification }),
|
|
579
|
+
...(completionGate === undefined ? {} : { completionGate })
|
|
580
|
+
});
|
|
469
581
|
writeHookOutput(io, output);
|
|
582
|
+
// A Stop can be advisory or gated, but the foreground clock only ends on
|
|
583
|
+
// the same trusted idle model_stop that the native decision accepted.
|
|
584
|
+
if (normalized?.event === "stop" &&
|
|
585
|
+
normalized.sessionId !== undefined &&
|
|
586
|
+
normalized.terminationReason === "model_stop" &&
|
|
587
|
+
normalized.fullyIdle === true &&
|
|
588
|
+
hookResult?.action === "continue") {
|
|
589
|
+
await closeDebugSession(root, normalized.sessionId, harnessId).catch(() => undefined);
|
|
590
|
+
}
|
|
470
591
|
}
|
|
471
592
|
catch {
|
|
472
593
|
if (completionGateInstalled) {
|
|
@@ -7,7 +7,7 @@ import { spawn } from "node:child_process";
|
|
|
7
7
|
import { collectOfficeBuilding } from "../../../runtime/src/office/collect.js";
|
|
8
8
|
import { claimOffice, createOfficeServer, ensureOffice, legacyOfficeHome, officeRecordPath, readLiveOffice, officeUrl, releaseOffice, userOfficeHome } from "../../../runtime/src/office/server.js";
|
|
9
9
|
import { registerOfficeRepo } from "../../../runtime/src/office/repos.js";
|
|
10
|
-
import { recordOfficeSession } from "../../../runtime/src/office/sessions.js";
|
|
10
|
+
import { recordOfficeSession, recordOfficeSessionPhase } from "../../../runtime/src/office/sessions.js";
|
|
11
11
|
import { resolveCheckouts } from "../../../runtime/src/parallel/service.js";
|
|
12
12
|
import { createLaunchdDescriptor, LaunchdController } from "../../../runtime/src/run/macOS.js";
|
|
13
13
|
import { ensurePrivateDirectory, readPrivateFile } from "../../../runtime/src/security/permissions.js";
|
|
@@ -47,8 +47,11 @@ function sessionIdentity(options) {
|
|
|
47
47
|
const nested = recordValue(options.input, "input");
|
|
48
48
|
return firstText(options.sessionId, recordValue(options.input, "session_id"), recordValue(options.input, "sessionId"), recordValue(options.input, "conversationId"), recordValue(options.input, "thread_id"), recordValue(options.input, "threadId"), recordValue(nested, "sessionID"), recordValue(nested, "session_id"), process.env.AGENT_OPS_SESSION_ID, process.env.CODEX_THREAD_ID);
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
/** agy's SessionStart is its native PreInvocation, sent as each invocation's work begins. */
|
|
51
|
+
export function officeEvent(harness, event) {
|
|
52
|
+
if (event === "SessionStart")
|
|
53
|
+
return harness === "agy" ? "activity" : "start";
|
|
54
|
+
return event === "Stop" ? "stop" : event === "SessionEnd" ? "end" : "activity";
|
|
52
55
|
}
|
|
53
56
|
/** Phase hints come only from normalized native event kinds and exact agent-ops argv. */
|
|
54
57
|
export function officePhaseHint(event) {
|
|
@@ -64,13 +67,14 @@ export function officePhaseHint(event) {
|
|
|
64
67
|
if (basename(command.command) !== "agent-ops")
|
|
65
68
|
continue;
|
|
66
69
|
const action = command.args[0];
|
|
67
|
-
|
|
70
|
+
// batch and advance report their later steps themselves; this is their first.
|
|
71
|
+
const next = action === "verify" || action === "batch"
|
|
68
72
|
? "verifying"
|
|
69
73
|
: action === "review"
|
|
70
74
|
? "reviewing"
|
|
71
75
|
: action === "worktree" && command.args[1] === "finish"
|
|
72
76
|
? "integrating"
|
|
73
|
-
: action === "task" && command.args[1] === "complete"
|
|
77
|
+
: action === "task" && (command.args[1] === "complete" || command.args[1] === "advance")
|
|
74
78
|
? "integrating"
|
|
75
79
|
: action === "task"
|
|
76
80
|
? "planning"
|
|
@@ -117,7 +121,7 @@ export async function observeOfficeSession(options) {
|
|
|
117
121
|
harness: options.harness,
|
|
118
122
|
projectRoot: mainRoot,
|
|
119
123
|
commonDir,
|
|
120
|
-
event: officeEvent(options.event),
|
|
124
|
+
event: officeEvent(options.harness, options.event),
|
|
121
125
|
...(options.agentId === undefined ? {} : { agentId: options.agentId }),
|
|
122
126
|
...(process.env.AGENT_OPS_RUN_ID === undefined ? {} : { runId: process.env.AGENT_OPS_RUN_ID }),
|
|
123
127
|
...(process.env.AGENT_OPS_WORKER_ID === undefined ? {} : { workerId: process.env.AGENT_OPS_WORKER_ID }),
|
|
@@ -136,6 +140,30 @@ export async function observeOfficeSession(options) {
|
|
|
136
140
|
// Office is display-only; a missing Git checkout, lock or launcher never blocks a host hook.
|
|
137
141
|
}
|
|
138
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Moves these sessions' people as a batch or advance reaches each step.
|
|
145
|
+
* Display only: a disabled Office, an unknown session or a failed write records nothing.
|
|
146
|
+
*/
|
|
147
|
+
export async function officePhaseReporter(cwd, sessionIds) {
|
|
148
|
+
const none = async () => { };
|
|
149
|
+
try {
|
|
150
|
+
if (sessionIds.length === 0 || !(await officeEnabled(cwd)))
|
|
151
|
+
return none;
|
|
152
|
+
const { commonDir } = await checkouts(cwd);
|
|
153
|
+
// One write at a time, in order, so the latest step is the one that stays.
|
|
154
|
+
let chain = Promise.resolve();
|
|
155
|
+
return async (phase) => {
|
|
156
|
+
chain = chain.then(async () => {
|
|
157
|
+
for (const sessionId of sessionIds)
|
|
158
|
+
await recordOfficeSessionPhase(commonDir, sessionId, phase).catch(() => { });
|
|
159
|
+
});
|
|
160
|
+
await chain;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return none;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
139
167
|
async function checkouts(cwd) {
|
|
140
168
|
return await resolveCheckouts(worktreeDependencies(), cwd);
|
|
141
169
|
}
|
|
@@ -13,6 +13,7 @@ import { FileRunRepository, RunService } from "../../../runtime/src/run/service.
|
|
|
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";
|
|
16
|
+
import { officePhaseReporter } from "./office-entry.js";
|
|
16
17
|
import { NativeRunTransport } from "../../../runtime/src/run/transport.js";
|
|
17
18
|
import { ClaudeGoalHost } from "../../../runtime/src/run/hosts/claude.js";
|
|
18
19
|
import { CodexGoalHost } from "../../../runtime/src/run/hosts/codex.js";
|
|
@@ -145,6 +146,13 @@ export async function runPhaseObserver(cwd, runId, workerId) {
|
|
|
145
146
|
const repository = new FileRunRepository(join(commonDir, "agent-ops", "runs"), commonDir);
|
|
146
147
|
return async (phase, progress) => await recordRunPhase(repository, runId, workerId, phase, progress);
|
|
147
148
|
}
|
|
149
|
+
/** A run worker's advance reports to its run; an ordinary session's moves its own person in Office. */
|
|
150
|
+
export async function advancePhaseObserver(cwd, sessionId, env = process.env) {
|
|
151
|
+
if (env.AGENT_OPS_RUN_ID !== undefined && env.AGENT_OPS_WORKER_ID !== undefined)
|
|
152
|
+
return await runPhaseObserver(cwd, env.AGENT_OPS_RUN_ID, env.AGENT_OPS_WORKER_ID);
|
|
153
|
+
const office = await officePhaseReporter(cwd, sessionId === undefined ? [] : [sessionId]);
|
|
154
|
+
return async (phase) => await office(phase);
|
|
155
|
+
}
|
|
148
156
|
export const runEntry = fileURLToPath(new URL("./run-entry.js", import.meta.url));
|
|
149
157
|
export function runDescriptor(state) {
|
|
150
158
|
return createLaunchdDescriptor({ runId: state.runId, workerId: "supervisor", privateDirectory: join(state.commonDir, "agent-ops", "runs", state.runId),
|