@a5c-ai/babysitter-omp 0.1.0
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 +80 -0
- package/bin/cli.cjs +78 -0
- package/bin/install.cjs +144 -0
- package/bin/uninstall.cjs +40 -0
- package/commands/babysitter-call.md +12 -0
- package/commands/babysitter-doctor.md +10 -0
- package/commands/babysitter-resume.md +16 -0
- package/commands/babysitter-status.md +15 -0
- package/extensions/babysitter/cli-wrapper.ts +95 -0
- package/extensions/babysitter/constants.ts +77 -0
- package/extensions/babysitter/custom-tools.ts +208 -0
- package/extensions/babysitter/effect-executor.ts +362 -0
- package/extensions/babysitter/guards.ts +257 -0
- package/extensions/babysitter/index.ts +554 -0
- package/extensions/babysitter/loop-driver.ts +256 -0
- package/extensions/babysitter/result-poster.ts +115 -0
- package/extensions/babysitter/sdk-bridge.ts +243 -0
- package/extensions/babysitter/session-binder.ts +284 -0
- package/extensions/babysitter/status-line.ts +54 -0
- package/extensions/babysitter/task-interceptor.ts +82 -0
- package/extensions/babysitter/todo-replacement.ts +125 -0
- package/extensions/babysitter/tool-renderer.ts +263 -0
- package/extensions/babysitter/tui-widgets.ts +164 -0
- package/extensions/babysitter/types.ts +222 -0
- package/package.json +57 -0
- package/scripts/setup.sh +74 -0
- package/scripts/sync-command-docs.cjs +115 -0
- package/skills/babysitter/SKILL.md +45 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main entry point for the babysitter oh-my-pi extension.
|
|
3
|
+
*
|
|
4
|
+
* This module is the default export consumed by the oh-my-pi extension
|
|
5
|
+
* loader. It wires every sub-module into the extension API by
|
|
6
|
+
* subscribing to lifecycle events, registering renderers, and setting
|
|
7
|
+
* up the orchestration loop.
|
|
8
|
+
*
|
|
9
|
+
* @module index
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtensionAPI } from './types';
|
|
13
|
+
import { EXTENSION_NAME, EXTENSION_VERSION, ENV_RUNS_DIR } from './constants';
|
|
14
|
+
import { initSession, getActiveRun, setActiveRun, clearActiveRun, bindRun } from './session-binder';
|
|
15
|
+
import { onAgentEnd, buildContinuationPrompt } from './loop-driver';
|
|
16
|
+
import { interceptToolCall } from './task-interceptor';
|
|
17
|
+
import { syncTodoState } from './todo-replacement';
|
|
18
|
+
import { renderRunWidget } from './tui-widgets';
|
|
19
|
+
import { updateStatusLine, clearStatusLine } from './status-line';
|
|
20
|
+
import { createToolRenderer } from './tool-renderer';
|
|
21
|
+
import { resetDigests, checkGuards } from './guards';
|
|
22
|
+
import { registerCustomTools } from './custom-tools';
|
|
23
|
+
import { iterate, getRunStatus } from './sdk-bridge';
|
|
24
|
+
import * as path from 'path';
|
|
25
|
+
import * as fs from 'fs';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Activate the babysitter extension.
|
|
29
|
+
*
|
|
30
|
+
* Called by oh-my-pi when the extension is loaded. Registers all event
|
|
31
|
+
* handlers, message renderers, and slash commands needed to drive
|
|
32
|
+
* babysitter orchestration from within an oh-my-pi session.
|
|
33
|
+
*
|
|
34
|
+
* @param pi - The oh-my-pi {@link ExtensionAPI} handle.
|
|
35
|
+
*/
|
|
36
|
+
export default function activate(pi: ExtensionAPI): void {
|
|
37
|
+
// Guard appendEntry calls during activation — some harnesses (e.g. oh-my-pi)
|
|
38
|
+
// don't allow action methods until extension loading completes.
|
|
39
|
+
const safeAppend = (entry: { type: string; content: string }) => {
|
|
40
|
+
try { pi.appendEntry(entry); } catch { /* deferred — not yet initialized */ }
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
safeAppend({
|
|
44
|
+
type: 'info',
|
|
45
|
+
content: `[${EXTENSION_NAME}] v${EXTENSION_VERSION} activating...`,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// -- session_start: auto-bind to babysitter --------------------------------
|
|
49
|
+
pi.on('session_start', async (...args: unknown[]) => {
|
|
50
|
+
const sessionId = (args[0] as { sessionId?: string })?.sessionId ?? 'default';
|
|
51
|
+
const runState = initSession(sessionId);
|
|
52
|
+
|
|
53
|
+
if (runState) {
|
|
54
|
+
renderRunWidget(runState, pi);
|
|
55
|
+
updateStatusLine(runState, pi);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// -- agent_end: drive the orchestration loop --------------------------------
|
|
60
|
+
pi.on('agent_end', async (...args: unknown[]) => {
|
|
61
|
+
const event = args[0] as {
|
|
62
|
+
sessionId?: string;
|
|
63
|
+
output?: string;
|
|
64
|
+
text?: string;
|
|
65
|
+
};
|
|
66
|
+
await onAgentEnd(event ?? {}, pi);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// -- tool_call: intercept task/todo tools during active runs ----------------
|
|
70
|
+
pi.on('tool_call', (...args: unknown[]) => {
|
|
71
|
+
const event = args[0] as {
|
|
72
|
+
toolName?: string;
|
|
73
|
+
params?: Record<string, unknown>;
|
|
74
|
+
sessionId?: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const toolName = event?.toolName ?? '';
|
|
78
|
+
const params = event?.params ?? {};
|
|
79
|
+
const _sessionId = event?.sessionId ?? 'default';
|
|
80
|
+
const result = interceptToolCall(toolName, params, pi);
|
|
81
|
+
|
|
82
|
+
if (result?.block) {
|
|
83
|
+
return result; // oh-my-pi will block the tool call
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return undefined; // allow the call to proceed
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// -- context: inject babysitter state into the agent context ----------------
|
|
90
|
+
pi.on('context', (...args: unknown[]) => {
|
|
91
|
+
const event = args[0] as { sessionId?: string };
|
|
92
|
+
const sessionId = event?.sessionId ?? 'default';
|
|
93
|
+
const runState = getActiveRun(sessionId);
|
|
94
|
+
|
|
95
|
+
if (!runState) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Provide babysitter run state as additional context
|
|
100
|
+
return {
|
|
101
|
+
babysitter: {
|
|
102
|
+
runId: runState.runId,
|
|
103
|
+
status: runState.status,
|
|
104
|
+
iteration: runState.iteration,
|
|
105
|
+
processId: runState.processId,
|
|
106
|
+
maxIterations: runState.maxIterations,
|
|
107
|
+
startedAt: runState.startedAt,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// -- session_shutdown: clean up resources ----------------------------------
|
|
113
|
+
pi.on('session_shutdown', (...args: unknown[]) => {
|
|
114
|
+
const sessionId = (args[0] as { sessionId?: string })?.sessionId ?? 'default';
|
|
115
|
+
|
|
116
|
+
clearActiveRun(sessionId);
|
|
117
|
+
resetDigests();
|
|
118
|
+
clearStatusLine(pi);
|
|
119
|
+
|
|
120
|
+
pi.appendEntry({
|
|
121
|
+
type: 'info',
|
|
122
|
+
content: `[${EXTENSION_NAME}] Session ${sessionId} cleaned up.`,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// -- Register custom tools for run inspection and control -------------------
|
|
127
|
+
registerCustomTools(pi);
|
|
128
|
+
|
|
129
|
+
// -- Register custom message renderer for babysitter tool results -----------
|
|
130
|
+
pi.registerMessageRenderer('babysitter:tool-result', createToolRenderer());
|
|
131
|
+
|
|
132
|
+
// -- Register slash command for manual todo sync ----------------------------
|
|
133
|
+
pi.registerCommand('babysitter:sync', { handler: (...args: unknown[]) => {
|
|
134
|
+
const sessionId = (args[0] as string) ?? 'default';
|
|
135
|
+
const runState = getActiveRun(sessionId);
|
|
136
|
+
|
|
137
|
+
if (runState) {
|
|
138
|
+
syncTodoState(runState.runDir, pi);
|
|
139
|
+
renderRunWidget(runState, pi);
|
|
140
|
+
updateStatusLine(runState, pi);
|
|
141
|
+
}
|
|
142
|
+
}});
|
|
143
|
+
|
|
144
|
+
// -- babysitter:call handler (shared by aliases) ----------------------------
|
|
145
|
+
const handleBabysitterCall = async (...args: unknown[]) => {
|
|
146
|
+
const prompt = (args[0] as string) ?? '';
|
|
147
|
+
if (!prompt) {
|
|
148
|
+
pi.appendEntry({
|
|
149
|
+
type: 'error',
|
|
150
|
+
content: `[${EXTENSION_NAME}] /babysitter:call requires a prompt argument. Usage: /babysitter:call "build feature X"`,
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sessionId = (args[1] as string) ?? 'default';
|
|
156
|
+
const runsDir = process.env[ENV_RUNS_DIR] ?? path.resolve('.a5c', 'runs');
|
|
157
|
+
|
|
158
|
+
// If a run is already active, warn and replace it
|
|
159
|
+
const existingRun = getActiveRun(sessionId);
|
|
160
|
+
if (existingRun) {
|
|
161
|
+
pi.appendEntry({
|
|
162
|
+
type: 'warning',
|
|
163
|
+
content: `[${EXTENSION_NAME}] Replacing active run ${existingRun.runId} for session ${sessionId}.`,
|
|
164
|
+
});
|
|
165
|
+
clearActiveRun(sessionId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const run = await bindRun(sessionId, {
|
|
170
|
+
processId: 'babysitter:call',
|
|
171
|
+
importPath: 'babysitter/process',
|
|
172
|
+
prompt,
|
|
173
|
+
runsDir,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
pi.appendEntry({
|
|
177
|
+
type: 'info',
|
|
178
|
+
content: `[${EXTENSION_NAME}] Run ${run.runId} created and bound to session ${sessionId}. Starting iteration...`,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Kick off the first iteration
|
|
182
|
+
const iterResult = await iterate(run.runDir);
|
|
183
|
+
run.iteration += 1;
|
|
184
|
+
run.iterationTimes.push(0);
|
|
185
|
+
setActiveRun(run);
|
|
186
|
+
|
|
187
|
+
if (iterResult.status === 'waiting') {
|
|
188
|
+
const continuationPrompt = buildContinuationPrompt(iterResult, {
|
|
189
|
+
runId: run.runId,
|
|
190
|
+
iteration: run.iteration,
|
|
191
|
+
});
|
|
192
|
+
pi.sendUserMessage({ role: 'user', content: continuationPrompt });
|
|
193
|
+
} else if (iterResult.status === 'completed') {
|
|
194
|
+
pi.appendEntry({
|
|
195
|
+
type: 'info',
|
|
196
|
+
content: `[${EXTENSION_NAME}] Run ${run.runId} completed immediately.`,
|
|
197
|
+
});
|
|
198
|
+
run.status = 'completed';
|
|
199
|
+
setActiveRun(run);
|
|
200
|
+
} else if (iterResult.status === 'failed') {
|
|
201
|
+
const errMsg = iterResult.error instanceof Error
|
|
202
|
+
? iterResult.error.message
|
|
203
|
+
: String(iterResult.error ?? 'unknown error');
|
|
204
|
+
pi.appendEntry({
|
|
205
|
+
type: 'error',
|
|
206
|
+
content: `[${EXTENSION_NAME}] Run ${run.runId} failed: ${errMsg}`,
|
|
207
|
+
});
|
|
208
|
+
run.status = 'failed';
|
|
209
|
+
setActiveRun(run);
|
|
210
|
+
}
|
|
211
|
+
} catch (err: unknown) {
|
|
212
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
213
|
+
pi.appendEntry({
|
|
214
|
+
type: 'error',
|
|
215
|
+
content: `[${EXTENSION_NAME}] Failed to start run: ${errMsg}`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// -- Register slash command: babysitter:call + aliases ----------------------
|
|
221
|
+
pi.registerCommand('babysitter:call', { description: 'Start a babysitter orchestration run', handler: handleBabysitterCall });
|
|
222
|
+
pi.registerCommand('call', { description: 'Start a babysitter orchestration run (alias)', handler: handleBabysitterCall });
|
|
223
|
+
pi.registerCommand('babysitter', { description: 'Start a babysitter orchestration run (alias)', handler: handleBabysitterCall });
|
|
224
|
+
|
|
225
|
+
// -- Register slash command: babysitter:status ------------------------------
|
|
226
|
+
pi.registerCommand('babysitter:status', { description: 'Show babysitter run status', handler: async (...args: unknown[]) => {
|
|
227
|
+
const runIdArg = args[0] as string | undefined;
|
|
228
|
+
const sessionId = (args[1] as string) ?? 'default';
|
|
229
|
+
|
|
230
|
+
// Determine the run to inspect
|
|
231
|
+
const activeRun = getActiveRun(sessionId);
|
|
232
|
+
let runDir: string | null = null;
|
|
233
|
+
let runId: string | null = null;
|
|
234
|
+
|
|
235
|
+
if (runIdArg) {
|
|
236
|
+
// Explicit run ID — resolve the run directory
|
|
237
|
+
const runsDir = process.env[ENV_RUNS_DIR] ?? path.resolve('.a5c', 'runs');
|
|
238
|
+
const candidateDir = path.join(runsDir, runIdArg);
|
|
239
|
+
if (fs.existsSync(path.join(candidateDir, 'run.json'))) {
|
|
240
|
+
runDir = candidateDir;
|
|
241
|
+
runId = runIdArg;
|
|
242
|
+
} else {
|
|
243
|
+
pi.appendEntry({
|
|
244
|
+
type: 'error',
|
|
245
|
+
content: `[${EXTENSION_NAME}] Run ${runIdArg} not found in ${runsDir}.`,
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
} else if (activeRun) {
|
|
250
|
+
runDir = activeRun.runDir;
|
|
251
|
+
runId = activeRun.runId;
|
|
252
|
+
} else {
|
|
253
|
+
pi.appendEntry({
|
|
254
|
+
type: 'warning',
|
|
255
|
+
content: `[${EXTENSION_NAME}] No active run for this session. Pass a run ID or start a run with /babysitter:call.`,
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const status = await getRunStatus(runDir);
|
|
262
|
+
const elapsed = activeRun
|
|
263
|
+
? Date.now() - new Date(activeRun.startedAt).getTime()
|
|
264
|
+
: 0;
|
|
265
|
+
const elapsedSec = Math.round(elapsed / 1000);
|
|
266
|
+
|
|
267
|
+
const lines = [
|
|
268
|
+
`[babysitter:status] Run ${runId}`,
|
|
269
|
+
` Process: ${status.processId}`,
|
|
270
|
+
` Status: ${status.status}`,
|
|
271
|
+
` Iteration: ${activeRun?.iteration ?? 'N/A'}`,
|
|
272
|
+
` Elapsed: ${elapsedSec}s`,
|
|
273
|
+
` Pending: ${status.pendingEffects.length} effect(s)`,
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
if (status.pendingEffects.length > 0) {
|
|
277
|
+
lines.push(' Pending effects:');
|
|
278
|
+
for (const effect of status.pendingEffects) {
|
|
279
|
+
const title = effect.taskDef?.title ?? effect.label ?? effect.effectId;
|
|
280
|
+
lines.push(` - [${effect.kind}] ${title} (${effect.effectId})`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
pi.appendEntry({ type: 'info', content: lines.join('\n') });
|
|
285
|
+
} catch (err: unknown) {
|
|
286
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
287
|
+
pi.appendEntry({
|
|
288
|
+
type: 'error',
|
|
289
|
+
content: `[${EXTENSION_NAME}] Failed to read run status: ${errMsg}`,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}});
|
|
293
|
+
|
|
294
|
+
// -- Register slash command: babysitter:resume ------------------------------
|
|
295
|
+
pi.registerCommand('babysitter:resume', { description: 'Resume an existing babysitter run', handler: async (...args: unknown[]) => {
|
|
296
|
+
const runIdArg = args[0] as string | undefined;
|
|
297
|
+
const sessionId = (args[1] as string) ?? 'default';
|
|
298
|
+
|
|
299
|
+
if (!runIdArg) {
|
|
300
|
+
pi.appendEntry({
|
|
301
|
+
type: 'error',
|
|
302
|
+
content: `[${EXTENSION_NAME}] /babysitter:resume requires a run ID. Usage: /babysitter:resume <runId>`,
|
|
303
|
+
});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const runsDir = process.env[ENV_RUNS_DIR] ?? path.resolve('.a5c', 'runs');
|
|
308
|
+
const runDir = path.join(runsDir, runIdArg);
|
|
309
|
+
|
|
310
|
+
if (!fs.existsSync(path.join(runDir, 'run.json'))) {
|
|
311
|
+
pi.appendEntry({
|
|
312
|
+
type: 'error',
|
|
313
|
+
content: `[${EXTENSION_NAME}] Run ${runIdArg} not found in ${runsDir}.`,
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check that the run is actually resumable
|
|
319
|
+
try {
|
|
320
|
+
const status = await getRunStatus(runDir);
|
|
321
|
+
|
|
322
|
+
if (status.status === 'completed') {
|
|
323
|
+
pi.appendEntry({
|
|
324
|
+
type: 'warning',
|
|
325
|
+
content: `[${EXTENSION_NAME}] Run ${runIdArg} has already completed. Nothing to resume.`,
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (status.status === 'failed') {
|
|
330
|
+
pi.appendEntry({
|
|
331
|
+
type: 'warning',
|
|
332
|
+
content: `[${EXTENSION_NAME}] Run ${runIdArg} has failed. Cannot resume a failed run.`,
|
|
333
|
+
});
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Clear any existing active run for this session
|
|
338
|
+
const existingRun = getActiveRun(sessionId);
|
|
339
|
+
if (existingRun) {
|
|
340
|
+
pi.appendEntry({
|
|
341
|
+
type: 'warning',
|
|
342
|
+
content: `[${EXTENSION_NAME}] Replacing active run ${existingRun.runId} for session ${sessionId}.`,
|
|
343
|
+
});
|
|
344
|
+
clearActiveRun(sessionId);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Re-bind the run to this session
|
|
348
|
+
setActiveRun({
|
|
349
|
+
sessionId,
|
|
350
|
+
runId: runIdArg,
|
|
351
|
+
runDir,
|
|
352
|
+
iteration: 0,
|
|
353
|
+
maxIterations: 256,
|
|
354
|
+
iterationTimes: [],
|
|
355
|
+
startedAt: new Date().toISOString(),
|
|
356
|
+
processId: status.processId,
|
|
357
|
+
status: 'running',
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
pi.appendEntry({
|
|
361
|
+
type: 'info',
|
|
362
|
+
content: `[${EXTENSION_NAME}] Resuming run ${runIdArg}. Starting next iteration...`,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Run the next iteration
|
|
366
|
+
const iterResult = await iterate(runDir);
|
|
367
|
+
const run = getActiveRun(sessionId);
|
|
368
|
+
|
|
369
|
+
if (run) {
|
|
370
|
+
run.iteration += 1;
|
|
371
|
+
run.iterationTimes.push(0);
|
|
372
|
+
setActiveRun(run);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (iterResult.status === 'waiting' && run) {
|
|
376
|
+
const continuationPrompt = buildContinuationPrompt(iterResult, {
|
|
377
|
+
runId: run.runId,
|
|
378
|
+
iteration: run.iteration,
|
|
379
|
+
});
|
|
380
|
+
pi.sendUserMessage({ role: 'user', content: continuationPrompt });
|
|
381
|
+
} else if (iterResult.status === 'completed') {
|
|
382
|
+
pi.appendEntry({
|
|
383
|
+
type: 'info',
|
|
384
|
+
content: `[${EXTENSION_NAME}] Run ${runIdArg} completed on resume.`,
|
|
385
|
+
});
|
|
386
|
+
clearActiveRun(sessionId);
|
|
387
|
+
} else if (iterResult.status === 'failed') {
|
|
388
|
+
const errMsg = iterResult.error instanceof Error
|
|
389
|
+
? iterResult.error.message
|
|
390
|
+
: String(iterResult.error ?? 'unknown error');
|
|
391
|
+
pi.appendEntry({
|
|
392
|
+
type: 'error',
|
|
393
|
+
content: `[${EXTENSION_NAME}] Run ${runIdArg} failed on resume: ${errMsg}`,
|
|
394
|
+
});
|
|
395
|
+
clearActiveRun(sessionId);
|
|
396
|
+
}
|
|
397
|
+
} catch (err: unknown) {
|
|
398
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
399
|
+
pi.appendEntry({
|
|
400
|
+
type: 'error',
|
|
401
|
+
content: `[${EXTENSION_NAME}] Failed to resume run ${runIdArg}: ${errMsg}`,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}});
|
|
405
|
+
|
|
406
|
+
// -- Register slash command: babysitter:doctor ------------------------------
|
|
407
|
+
pi.registerCommand('babysitter:doctor', { description: 'Diagnose babysitter run health', handler: async (...args: unknown[]) => {
|
|
408
|
+
const runIdArg = args[0] as string | undefined;
|
|
409
|
+
const sessionId = (args[1] as string) ?? 'default';
|
|
410
|
+
|
|
411
|
+
// Determine the run to diagnose
|
|
412
|
+
const activeRun = getActiveRun(sessionId);
|
|
413
|
+
let runDir: string | null = null;
|
|
414
|
+
let runId: string | null = null;
|
|
415
|
+
|
|
416
|
+
if (runIdArg) {
|
|
417
|
+
const runsDir = process.env[ENV_RUNS_DIR] ?? path.resolve('.a5c', 'runs');
|
|
418
|
+
const candidateDir = path.join(runsDir, runIdArg);
|
|
419
|
+
if (fs.existsSync(path.join(candidateDir, 'run.json'))) {
|
|
420
|
+
runDir = candidateDir;
|
|
421
|
+
runId = runIdArg;
|
|
422
|
+
} else {
|
|
423
|
+
pi.appendEntry({
|
|
424
|
+
type: 'error',
|
|
425
|
+
content: `[${EXTENSION_NAME}] Run ${runIdArg} not found.`,
|
|
426
|
+
});
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
} else if (activeRun) {
|
|
430
|
+
runDir = activeRun.runDir;
|
|
431
|
+
runId = activeRun.runId;
|
|
432
|
+
} else {
|
|
433
|
+
pi.appendEntry({
|
|
434
|
+
type: 'warning',
|
|
435
|
+
content: `[${EXTENSION_NAME}] No active run. Pass a run ID or start a run with /babysitter:call.`,
|
|
436
|
+
});
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const checks: string[] = [`[babysitter:doctor] Diagnosing run ${runId}`, ''];
|
|
441
|
+
|
|
442
|
+
// Check 1: Run directory structure
|
|
443
|
+
const requiredPaths = ['run.json', 'inputs.json', 'journal'];
|
|
444
|
+
const optionalPaths = ['state', 'tasks', 'blobs', 'process'];
|
|
445
|
+
let structureOk = true;
|
|
446
|
+
|
|
447
|
+
for (const p of requiredPaths) {
|
|
448
|
+
const fullPath = path.join(runDir, p);
|
|
449
|
+
if (fs.existsSync(fullPath)) {
|
|
450
|
+
checks.push(` [OK] ${p} exists`);
|
|
451
|
+
} else {
|
|
452
|
+
checks.push(` [FAIL] ${p} is missing`);
|
|
453
|
+
structureOk = false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const p of optionalPaths) {
|
|
457
|
+
const fullPath = path.join(runDir, p);
|
|
458
|
+
if (fs.existsSync(fullPath)) {
|
|
459
|
+
checks.push(` [OK] ${p} exists`);
|
|
460
|
+
} else {
|
|
461
|
+
checks.push(` [WARN] ${p} not found (optional)`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Check 2: Lock file
|
|
466
|
+
const lockPath = path.join(runDir, 'run.lock');
|
|
467
|
+
if (fs.existsSync(lockPath)) {
|
|
468
|
+
try {
|
|
469
|
+
const lockData = JSON.parse(fs.readFileSync(lockPath, 'utf-8')) as {
|
|
470
|
+
pid?: number;
|
|
471
|
+
acquiredAt?: string;
|
|
472
|
+
};
|
|
473
|
+
const lockAge = lockData.acquiredAt
|
|
474
|
+
? Date.now() - new Date(lockData.acquiredAt).getTime()
|
|
475
|
+
: 0;
|
|
476
|
+
const lockAgeSec = Math.round(lockAge / 1000);
|
|
477
|
+
if (lockAgeSec > 300) {
|
|
478
|
+
checks.push(` [WARN] Stale lock file detected (age: ${lockAgeSec}s, pid: ${lockData.pid ?? 'unknown'}). Consider deleting run.lock.`);
|
|
479
|
+
} else {
|
|
480
|
+
checks.push(` [OK] Lock file present (age: ${lockAgeSec}s, pid: ${lockData.pid ?? 'unknown'})`);
|
|
481
|
+
}
|
|
482
|
+
} catch {
|
|
483
|
+
checks.push(` [WARN] Lock file exists but could not be parsed.`);
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
checks.push(` [OK] No lock file (run is not locked)`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Check 3: Journal integrity (basic)
|
|
490
|
+
const journalDir = path.join(runDir, 'journal');
|
|
491
|
+
if (fs.existsSync(journalDir)) {
|
|
492
|
+
try {
|
|
493
|
+
const entries = fs.readdirSync(journalDir).filter((f: string) => f.endsWith('.json')).sort();
|
|
494
|
+
checks.push(` [OK] Journal has ${entries.length} event(s)`);
|
|
495
|
+
|
|
496
|
+
if (entries.length === 0) {
|
|
497
|
+
checks.push(` [WARN] Journal is empty. The run may not have started.`);
|
|
498
|
+
}
|
|
499
|
+
} catch {
|
|
500
|
+
checks.push(` [FAIL] Could not read journal directory.`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Check 4: State cache
|
|
505
|
+
const statePath = path.join(runDir, 'state', 'state.json');
|
|
506
|
+
if (fs.existsSync(statePath)) {
|
|
507
|
+
checks.push(` [OK] State cache exists`);
|
|
508
|
+
} else {
|
|
509
|
+
checks.push(` [WARN] State cache missing. Will be rebuilt on next iteration. Run: babysitter run:rebuild-state`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Check 5: Run status via SDK
|
|
513
|
+
try {
|
|
514
|
+
const status = await getRunStatus(runDir);
|
|
515
|
+
checks.push(` [OK] SDK status: ${status.status}`);
|
|
516
|
+
checks.push(` [OK] Pending effects: ${status.pendingEffects.length}`);
|
|
517
|
+
|
|
518
|
+
if (status.pendingEffects.length > 0) {
|
|
519
|
+
for (const effect of status.pendingEffects) {
|
|
520
|
+
const title = effect.taskDef?.title ?? effect.label ?? effect.effectId;
|
|
521
|
+
checks.push(` - [${effect.kind}] ${title}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
} catch (err: unknown) {
|
|
525
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
526
|
+
checks.push(` [FAIL] Could not read run status via SDK: ${errMsg}`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Check 6: Guard status (only for active runs)
|
|
530
|
+
if (activeRun) {
|
|
531
|
+
const guardResult = checkGuards(activeRun);
|
|
532
|
+
if (guardResult.passed) {
|
|
533
|
+
checks.push(` [OK] Guards passing (iteration ${activeRun.iteration}/${activeRun.maxIterations})`);
|
|
534
|
+
} else {
|
|
535
|
+
checks.push(` [WARN] Guard tripped: ${guardResult.reason}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Summary
|
|
540
|
+
checks.push('');
|
|
541
|
+
if (!structureOk) {
|
|
542
|
+
checks.push('Remediation: run `babysitter run:repair-journal` to fix structural issues.');
|
|
543
|
+
} else {
|
|
544
|
+
checks.push('No critical issues detected.');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
pi.appendEntry({ type: 'info', content: checks.join('\n') });
|
|
548
|
+
}});
|
|
549
|
+
|
|
550
|
+
safeAppend({
|
|
551
|
+
type: 'info',
|
|
552
|
+
content: `[${EXTENSION_NAME}] v${EXTENSION_VERSION} activated.`,
|
|
553
|
+
});
|
|
554
|
+
}
|