@a5c-ai/babysitter-sdk 0.0.184 → 0.0.185
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/dist/cli/commands/instructions.d.ts.map +1 -1
- package/dist/cli/commands/instructions.js +41 -0
- package/dist/cli/main.js +2 -2
- package/dist/config/defaults.d.ts +11 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +55 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -1
- package/dist/harness/claudeCode.d.ts +2 -2
- package/dist/harness/claudeCode.d.ts.map +1 -1
- package/dist/harness/claudeCode.js +47 -72
- package/dist/harness/codex.d.ts.map +1 -1
- package/dist/harness/codex.js +10 -49
- package/dist/harness/cursor.d.ts.map +1 -1
- package/dist/harness/cursor.js +23 -34
- package/dist/harness/customAdapter.d.ts.map +1 -1
- package/dist/harness/customAdapter.js +8 -3
- package/dist/harness/discovery.js +7 -7
- package/dist/harness/geminiCli.d.ts.map +1 -1
- package/dist/harness/geminiCli.js +20 -27
- package/dist/harness/githubCopilot.d.ts.map +1 -1
- package/dist/harness/githubCopilot.js +17 -40
- package/dist/harness/index.d.ts +1 -0
- package/dist/harness/index.d.ts.map +1 -1
- package/dist/harness/index.js +3 -1
- package/dist/harness/installSupport.d.ts +1 -0
- package/dist/harness/installSupport.d.ts.map +1 -1
- package/dist/harness/installSupport.js +7 -3
- package/dist/harness/invoker.js +1 -1
- package/dist/harness/ohMyPi.d.ts +0 -14
- package/dist/harness/ohMyPi.d.ts.map +1 -1
- package/dist/harness/ohMyPi.js +163 -24
- package/dist/harness/opencode.d.ts +28 -0
- package/dist/harness/opencode.d.ts.map +1 -0
- package/dist/harness/opencode.js +578 -0
- package/dist/harness/pi.d.ts +1 -11
- package/dist/harness/pi.d.ts.map +1 -1
- package/dist/harness/pi.js +81 -105
- package/dist/harness/registry.d.ts.map +1 -1
- package/dist/harness/registry.js +2 -0
- package/dist/prompts/context.d.ts +9 -0
- package/dist/prompts/context.d.ts.map +1 -1
- package/dist/prompts/context.js +128 -20
- package/dist/prompts/index.d.ts +1 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +2 -1
- package/dist/prompts/templates/completion-proof.md +8 -0
- package/dist/prompts/templates/critical-rules.md +5 -5
- package/dist/prompts/types.d.ts +4 -5
- package/dist/prompts/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OpenCode harness adapter.
|
|
4
|
+
*
|
|
5
|
+
* Centralizes all OpenCode-specific behaviors:
|
|
6
|
+
* - Session ID resolution (BABYSITTER_SESSION_ID, self-injected via shell.env hook)
|
|
7
|
+
* - State directory conventions (global state dir by default)
|
|
8
|
+
* - Session binding (run:create → state file with run association)
|
|
9
|
+
* - Minimal stop hook handler (no native stop hook — in-turn model)
|
|
10
|
+
* - Session-start hook handler (baseline state file creation)
|
|
11
|
+
*
|
|
12
|
+
* OpenCode characteristics:
|
|
13
|
+
* - Go binary, CLI command: `opencode`
|
|
14
|
+
* - Config directory: `.opencode/`
|
|
15
|
+
* - Plugin system: JS/TS modules in `.opencode/plugins/` with hooks for
|
|
16
|
+
* `tool.execute.before/after`, `session.idle`, `session.created`, `shell.env`
|
|
17
|
+
* - NO blocking stop hook — `session.idle` is fire-and-forget
|
|
18
|
+
* - Programmatic API: `@opencode-ai/sdk` with `session.create()`,
|
|
19
|
+
* `session.prompt()`, `event.subscribe()` (SSE)
|
|
20
|
+
* - Env vars: Does NOT inject distinctive env vars into plugins — weak
|
|
21
|
+
* caller detection. The babysitter plugin self-injects
|
|
22
|
+
* `BABYSITTER_SESSION_ID` via the `shell.env` hook.
|
|
23
|
+
* - Loop mechanism: in-turn (no stop-hook, must use SDK loop driver or
|
|
24
|
+
* in-turn orchestration)
|
|
25
|
+
* - Capabilities: [HeadlessPrompt] only
|
|
26
|
+
*/
|
|
27
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
30
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
31
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
32
|
+
}
|
|
33
|
+
Object.defineProperty(o, k2, desc);
|
|
34
|
+
}) : (function(o, m, k, k2) {
|
|
35
|
+
if (k2 === undefined) k2 = k;
|
|
36
|
+
o[k2] = m[k];
|
|
37
|
+
}));
|
|
38
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
39
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
40
|
+
}) : function(o, v) {
|
|
41
|
+
o["default"] = v;
|
|
42
|
+
});
|
|
43
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
44
|
+
var ownKeys = function(o) {
|
|
45
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
46
|
+
var ar = [];
|
|
47
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
48
|
+
return ar;
|
|
49
|
+
};
|
|
50
|
+
return ownKeys(o);
|
|
51
|
+
};
|
|
52
|
+
return function (mod) {
|
|
53
|
+
if (mod && mod.__esModule) return mod;
|
|
54
|
+
var result = {};
|
|
55
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
56
|
+
__setModuleDefault(result, mod);
|
|
57
|
+
return result;
|
|
58
|
+
};
|
|
59
|
+
})();
|
|
60
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
61
|
+
exports.createOpenCodeAdapter = createOpenCodeAdapter;
|
|
62
|
+
const path = __importStar(require("node:path"));
|
|
63
|
+
const node_fs_1 = require("node:fs");
|
|
64
|
+
const journal_1 = require("../storage/journal");
|
|
65
|
+
const runFiles_1 = require("../storage/runFiles");
|
|
66
|
+
const effectIndex_1 = require("../runtime/replay/effectIndex");
|
|
67
|
+
const session_1 = require("../session");
|
|
68
|
+
const types_1 = require("./types");
|
|
69
|
+
const context_1 = require("../prompts/context");
|
|
70
|
+
const config_1 = require("../config");
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Constants
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
const HARNESS_NAME = "opencode";
|
|
75
|
+
function createHookLogger(hookName) {
|
|
76
|
+
const logDir = process.env.BABYSITTER_LOG_DIR || ".a5c/logs";
|
|
77
|
+
const logFile = logDir ? path.join(logDir, `${hookName}.log`) : null;
|
|
78
|
+
const context = {};
|
|
79
|
+
if (logFile) {
|
|
80
|
+
try {
|
|
81
|
+
(0, node_fs_1.mkdirSync)(logDir, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Best-effort
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function write(level, message) {
|
|
88
|
+
if (!logFile)
|
|
89
|
+
return;
|
|
90
|
+
const ts = new Date().toISOString();
|
|
91
|
+
const ctxParts = Object.entries(context).map(([k, v]) => `${k}=${v}`);
|
|
92
|
+
const ctxStr = ctxParts.length > 0 ? ` [${ctxParts.join(" ")}]` : "";
|
|
93
|
+
const line = `[${level}] ${ts}${ctxStr} ${message}\n`;
|
|
94
|
+
try {
|
|
95
|
+
(0, node_fs_1.appendFileSync)(logFile, line);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Best-effort
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
info: (msg) => write("INFO", msg),
|
|
103
|
+
warn: (msg) => write("WARN", msg),
|
|
104
|
+
error: (msg) => write("ERROR", msg),
|
|
105
|
+
setContext: (key, value) => {
|
|
106
|
+
context[key] = value;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Stdin reader
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
function readStdin() {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
let data = "";
|
|
116
|
+
process.stdin.setEncoding("utf8");
|
|
117
|
+
process.stdin.on("data", (chunk) => {
|
|
118
|
+
data += chunk;
|
|
119
|
+
});
|
|
120
|
+
process.stdin.on("end", () => resolve(data));
|
|
121
|
+
process.stdin.on("error", reject);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Hook input parsing
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
function parseHookInput(raw) {
|
|
128
|
+
const trimmed = raw.trim();
|
|
129
|
+
if (!trimmed)
|
|
130
|
+
return {};
|
|
131
|
+
try {
|
|
132
|
+
const parsed = JSON.parse(trimmed);
|
|
133
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
134
|
+
return parsed;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Malformed JSON — treat as empty
|
|
139
|
+
}
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
function safeStr(obj, key) {
|
|
143
|
+
const val = obj[key];
|
|
144
|
+
return typeof val === "string" ? val : "";
|
|
145
|
+
}
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Pending-by-kind helper
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
function countPendingByKind(records) {
|
|
150
|
+
const counts = new Map();
|
|
151
|
+
for (const record of records) {
|
|
152
|
+
const key = record.kind ?? "unknown";
|
|
153
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
154
|
+
}
|
|
155
|
+
return Object.fromEntries(Array.from(counts.entries()).sort(([a], [b]) => a.localeCompare(b)));
|
|
156
|
+
}
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Cleanup helper
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
async function cleanupSession(filePath) {
|
|
161
|
+
try {
|
|
162
|
+
await (0, session_1.deleteSessionFile)(filePath);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Best-effort cleanup
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// State directory resolution
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
function resolveStateDirInternal(args) {
|
|
172
|
+
if (args.stateDir)
|
|
173
|
+
return path.resolve(args.stateDir);
|
|
174
|
+
return (0, config_1.getGlobalStateDir)();
|
|
175
|
+
}
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Session ID resolution
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
function resolveSessionIdInternal(parsed) {
|
|
180
|
+
// 1. Explicit arg (highest priority)
|
|
181
|
+
if (parsed.sessionId)
|
|
182
|
+
return parsed.sessionId;
|
|
183
|
+
// 2. Cross-harness standard env var (self-injected by babysitter plugin's shell.env hook)
|
|
184
|
+
if (process.env.BABYSITTER_SESSION_ID)
|
|
185
|
+
return process.env.BABYSITTER_SESSION_ID;
|
|
186
|
+
// 3. OpenCode-specific env vars (set by babysitter plugin's shell.env hook)
|
|
187
|
+
if (process.env.OPENCODE_SESSION_ID)
|
|
188
|
+
return process.env.OPENCODE_SESSION_ID;
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Stop hook handler (minimal — no native stop hook)
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
/**
|
|
195
|
+
* Handles the stop hook for OpenCode.
|
|
196
|
+
*
|
|
197
|
+
* OpenCode does NOT have a native stop hook that can block/restart the agent.
|
|
198
|
+
* This handler checks run status and returns approve/block as a decision, but
|
|
199
|
+
* the actual loop driving happens externally (in-turn model or SDK loop driver).
|
|
200
|
+
*
|
|
201
|
+
* When a run is still in progress (has pending effects or is not completed),
|
|
202
|
+
* we output a block decision with context for the next iteration. When the
|
|
203
|
+
* run is completed/failed or no run is bound, we allow exit.
|
|
204
|
+
*/
|
|
205
|
+
async function handleStopHookImpl(args) {
|
|
206
|
+
const { verbose } = args;
|
|
207
|
+
const log = createHookLogger("babysitter-opencode-stop-hook");
|
|
208
|
+
log.info("handleStopHook started (opencode)");
|
|
209
|
+
// 1. Read hook input JSON from stdin (if available)
|
|
210
|
+
let rawInput;
|
|
211
|
+
try {
|
|
212
|
+
rawInput = await readStdin();
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
rawInput = "";
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
if (typeof process.stdin.unref === "function") {
|
|
219
|
+
process.stdin.unref();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const hookInput = parseHookInput(rawInput);
|
|
223
|
+
// 2. Resolve session ID
|
|
224
|
+
const sessionId = safeStr(hookInput, "session_id") ||
|
|
225
|
+
process.env.BABYSITTER_SESSION_ID ||
|
|
226
|
+
process.env.OPENCODE_SESSION_ID ||
|
|
227
|
+
"";
|
|
228
|
+
if (!sessionId) {
|
|
229
|
+
log.info("No session ID — allowing exit");
|
|
230
|
+
process.stdout.write("{}\n");
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
log.setContext("session", sessionId);
|
|
234
|
+
// 3. Resolve state directory and read session state
|
|
235
|
+
const stateDir = resolveStateDirInternal(args);
|
|
236
|
+
const runsDir = args.runsDir || ".a5c/runs";
|
|
237
|
+
const filePath = (0, session_1.getSessionFilePath)(stateDir, sessionId);
|
|
238
|
+
let sessionFile;
|
|
239
|
+
try {
|
|
240
|
+
if (!(await (0, session_1.sessionFileExists)(filePath))) {
|
|
241
|
+
log.info("No active session — allowing exit");
|
|
242
|
+
process.stdout.write("{}\n");
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
sessionFile = await (0, session_1.readSessionFile)(filePath);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
log.warn("Session file read error — allowing exit");
|
|
249
|
+
process.stdout.write("{}\n");
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
const { state } = sessionFile;
|
|
253
|
+
const runId = state.runId ?? "";
|
|
254
|
+
// 4. Check max iterations
|
|
255
|
+
if (state.maxIterations > 0 && state.iteration >= state.maxIterations) {
|
|
256
|
+
if (verbose) {
|
|
257
|
+
process.stderr.write(`[hook:run stop] Max iterations (${state.maxIterations}) reached\n`);
|
|
258
|
+
}
|
|
259
|
+
await cleanupSession(filePath);
|
|
260
|
+
process.stdout.write("{}\n");
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
// 5. If no run is bound, allow exit
|
|
264
|
+
if (!runId) {
|
|
265
|
+
log.info("No run associated with session — allowing exit");
|
|
266
|
+
await cleanupSession(filePath);
|
|
267
|
+
process.stdout.write("{}\n");
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
270
|
+
log.setContext("run", runId);
|
|
271
|
+
// 6. Get run state
|
|
272
|
+
let runState = "";
|
|
273
|
+
let pendingKinds = "";
|
|
274
|
+
try {
|
|
275
|
+
const runDir = path.isAbsolute(runId)
|
|
276
|
+
? runId
|
|
277
|
+
: path.join(runsDir, runId);
|
|
278
|
+
void (0, runFiles_1.readRunMetadata)(runDir);
|
|
279
|
+
const journal = await (0, journal_1.loadJournal)(runDir);
|
|
280
|
+
const index = await (0, effectIndex_1.buildEffectIndex)({ runDir, events: journal });
|
|
281
|
+
const hasCompleted = journal.some((e) => e.type === "RUN_COMPLETED");
|
|
282
|
+
const hasFailed = journal.some((e) => e.type === "RUN_FAILED");
|
|
283
|
+
const pendingRecords = index.listPendingEffects();
|
|
284
|
+
const pendingByKind = countPendingByKind(pendingRecords);
|
|
285
|
+
const kindKeys = Object.keys(pendingByKind);
|
|
286
|
+
if (kindKeys.length > 0) {
|
|
287
|
+
pendingKinds = kindKeys.join(", ");
|
|
288
|
+
}
|
|
289
|
+
if (hasCompleted) {
|
|
290
|
+
runState = "completed";
|
|
291
|
+
}
|
|
292
|
+
else if (hasFailed) {
|
|
293
|
+
runState = "failed";
|
|
294
|
+
}
|
|
295
|
+
else if (pendingRecords.length > 0) {
|
|
296
|
+
runState = "waiting";
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
runState = "created";
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
runState = "";
|
|
304
|
+
}
|
|
305
|
+
log.info(`Run state: ${runState || "unknown"}`);
|
|
306
|
+
// 7. Allow exit if run is completed, failed, or state is unknown
|
|
307
|
+
if (runState === "completed" || runState === "failed" || !runState) {
|
|
308
|
+
await cleanupSession(filePath);
|
|
309
|
+
process.stdout.write("{}\n");
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
// 8. Run still in progress — output block decision with context
|
|
313
|
+
const nextIteration = state.iteration + 1;
|
|
314
|
+
const currentTime = (0, session_1.getCurrentTimestamp)();
|
|
315
|
+
const updatedState = {
|
|
316
|
+
...state,
|
|
317
|
+
iteration: nextIteration,
|
|
318
|
+
lastIterationAt: currentTime,
|
|
319
|
+
};
|
|
320
|
+
try {
|
|
321
|
+
await (0, session_1.writeSessionFile)(filePath, updatedState, sessionFile.prompt ?? "");
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
log.warn("Failed to update session state");
|
|
325
|
+
}
|
|
326
|
+
const output = {
|
|
327
|
+
decision: "block",
|
|
328
|
+
reason: pendingKinds
|
|
329
|
+
? `Babysitter iteration ${nextIteration} | Waiting on: ${pendingKinds}. Call 'babysitter run:iterate .a5c/runs/${runId} --json'.`
|
|
330
|
+
: `Babysitter iteration ${nextIteration} | Continue orchestration: call 'babysitter run:iterate .a5c/runs/${runId} --json'.`,
|
|
331
|
+
};
|
|
332
|
+
log.info(`Decision: block (iteration=${nextIteration})`);
|
|
333
|
+
if (verbose) {
|
|
334
|
+
process.stderr.write(`[hook:run stop] Blocking, iteration=${nextIteration}\n`);
|
|
335
|
+
}
|
|
336
|
+
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// SessionStart hook handler
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
async function handleSessionStartHookImpl(args) {
|
|
343
|
+
const { verbose } = args;
|
|
344
|
+
const log = createHookLogger("babysitter-opencode-session-start-hook");
|
|
345
|
+
log.info("handleSessionStartHook started (opencode)");
|
|
346
|
+
// 1. Read hook input JSON from stdin
|
|
347
|
+
let rawInput;
|
|
348
|
+
try {
|
|
349
|
+
rawInput = await readStdin();
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
process.stdout.write("{}\n");
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
if (typeof process.stdin.unref === "function") {
|
|
357
|
+
process.stdin.unref();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const hookInput = parseHookInput(rawInput);
|
|
361
|
+
// 2. Resolve session ID
|
|
362
|
+
// OpenCode does NOT auto-inject env vars — the babysitter plugin's
|
|
363
|
+
// shell.env hook self-injects BABYSITTER_SESSION_ID.
|
|
364
|
+
const sessionId = safeStr(hookInput, "session_id") ||
|
|
365
|
+
process.env.BABYSITTER_SESSION_ID ||
|
|
366
|
+
process.env.OPENCODE_SESSION_ID ||
|
|
367
|
+
"";
|
|
368
|
+
if (!sessionId) {
|
|
369
|
+
log.info("No session ID — skipping state file creation");
|
|
370
|
+
process.stdout.write("{}\n");
|
|
371
|
+
return 0;
|
|
372
|
+
}
|
|
373
|
+
log.setContext("session", sessionId);
|
|
374
|
+
log.info(`Session ID: ${sessionId}`);
|
|
375
|
+
// 3. Resolve state directory and create baseline session file
|
|
376
|
+
const stateDir = resolveStateDirInternal(args);
|
|
377
|
+
log.info(`Resolved stateDir: ${stateDir}`);
|
|
378
|
+
const filePath = (0, session_1.getSessionFilePath)(stateDir, sessionId);
|
|
379
|
+
try {
|
|
380
|
+
if (!(await (0, session_1.sessionFileExists)(filePath))) {
|
|
381
|
+
const nowTs = (0, session_1.getCurrentTimestamp)();
|
|
382
|
+
const state = {
|
|
383
|
+
active: true,
|
|
384
|
+
iteration: 1,
|
|
385
|
+
maxIterations: 256,
|
|
386
|
+
runId: "",
|
|
387
|
+
startedAt: nowTs,
|
|
388
|
+
lastIterationAt: nowTs,
|
|
389
|
+
iterationTimes: [],
|
|
390
|
+
};
|
|
391
|
+
await (0, session_1.writeSessionFile)(filePath, state, "");
|
|
392
|
+
log.info(`Created session state: ${filePath}`);
|
|
393
|
+
if (verbose) {
|
|
394
|
+
process.stderr.write(`[hook:run session-start] Created session state: ${filePath}\n`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
log.info(`Session state already exists: ${filePath}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch (e) {
|
|
402
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
403
|
+
log.warn(`Failed to create session state: ${msg}`);
|
|
404
|
+
if (verbose) {
|
|
405
|
+
process.stderr.write(`[hook:run session-start] Failed to create session state: ${msg}\n`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// 4. Output empty object
|
|
409
|
+
process.stdout.write("{}\n");
|
|
410
|
+
return 0;
|
|
411
|
+
}
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
// Session binding (run:create flow)
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
async function bindSessionImpl(opts) {
|
|
416
|
+
const { sessionId, runId, maxIterations = 256, prompt, verbose } = opts;
|
|
417
|
+
// Resolve state directory
|
|
418
|
+
const stateDir = resolveStateDirInternal({
|
|
419
|
+
stateDir: opts.stateDir,
|
|
420
|
+
pluginRoot: opts.pluginRoot,
|
|
421
|
+
});
|
|
422
|
+
const filePath = (0, session_1.getSessionFilePath)(stateDir, sessionId);
|
|
423
|
+
// Check for existing session (prevent re-entrant runs)
|
|
424
|
+
if (await (0, session_1.sessionFileExists)(filePath)) {
|
|
425
|
+
try {
|
|
426
|
+
const existing = await (0, session_1.readSessionFile)(filePath);
|
|
427
|
+
if (existing.state.runId && existing.state.runId !== runId) {
|
|
428
|
+
return {
|
|
429
|
+
harness: HARNESS_NAME,
|
|
430
|
+
sessionId,
|
|
431
|
+
stateFile: filePath,
|
|
432
|
+
error: `Session already associated with run: ${existing.state.runId}`,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
// Update existing session with run ID
|
|
436
|
+
await (0, session_1.updateSessionState)(filePath, { runId, active: true }, { state: existing.state, prompt: existing.prompt });
|
|
437
|
+
if (verbose) {
|
|
438
|
+
process.stderr.write(`[run:create] Updated existing session ${sessionId} with run ${runId}\n`);
|
|
439
|
+
}
|
|
440
|
+
return { harness: HARNESS_NAME, sessionId, stateFile: filePath };
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
// Corrupted state file — overwrite
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Create new session state with run associated
|
|
447
|
+
const nowTs = (0, session_1.getCurrentTimestamp)();
|
|
448
|
+
const state = {
|
|
449
|
+
active: true,
|
|
450
|
+
iteration: 1,
|
|
451
|
+
maxIterations,
|
|
452
|
+
runId,
|
|
453
|
+
startedAt: nowTs,
|
|
454
|
+
lastIterationAt: nowTs,
|
|
455
|
+
iterationTimes: [],
|
|
456
|
+
};
|
|
457
|
+
try {
|
|
458
|
+
await (0, session_1.writeSessionFile)(filePath, state, prompt);
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
return {
|
|
462
|
+
harness: HARNESS_NAME,
|
|
463
|
+
sessionId,
|
|
464
|
+
error: `Failed to write session state: ${e instanceof Error ? e.message : String(e)}`,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (verbose) {
|
|
468
|
+
process.stderr.write(`[run:create] Session ${sessionId} initialized and bound to run ${runId}\n`);
|
|
469
|
+
}
|
|
470
|
+
return { harness: HARNESS_NAME, sessionId, stateFile: filePath };
|
|
471
|
+
}
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
// Install helpers
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
function installOpenCodePlugin(_options) {
|
|
476
|
+
// OpenCode plugin installation is not yet automated.
|
|
477
|
+
// Plugins are JS/TS modules placed in .opencode/plugins/.
|
|
478
|
+
return {
|
|
479
|
+
harness: HARNESS_NAME,
|
|
480
|
+
summary: "OpenCode plugin installation is not yet automated. " +
|
|
481
|
+
"Place babysitter plugin files in .opencode/plugins/babysitter/ manually.",
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
// Adapter factory
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
function createOpenCodeAdapter() {
|
|
488
|
+
return {
|
|
489
|
+
name: HARNESS_NAME,
|
|
490
|
+
isActive() {
|
|
491
|
+
// OpenCode does NOT inject distinctive env vars into plugin subprocesses.
|
|
492
|
+
// The babysitter plugin's shell.env hook self-injects BABYSITTER_SESSION_ID.
|
|
493
|
+
// OPENCODE_CONFIG is a real OpenCode env var that indicates OpenCode context.
|
|
494
|
+
return !!(process.env.BABYSITTER_SESSION_ID ||
|
|
495
|
+
process.env.OPENCODE_CONFIG);
|
|
496
|
+
},
|
|
497
|
+
autoResolvesSessionId() {
|
|
498
|
+
// OpenCode does not natively inject session IDs — the babysitter
|
|
499
|
+
// plugin's shell.env hook self-injects BABYSITTER_SESSION_ID.
|
|
500
|
+
return false;
|
|
501
|
+
},
|
|
502
|
+
getMissingSessionIdHint() {
|
|
503
|
+
return ("OpenCode does not auto-inject session IDs. Use --session-id explicitly, " +
|
|
504
|
+
"or ensure the babysitter plugin's shell.env hook is configured to set " +
|
|
505
|
+
"BABYSITTER_SESSION_ID.");
|
|
506
|
+
},
|
|
507
|
+
supportsHookType(hookType) {
|
|
508
|
+
// OpenCode plugin hooks: session.created, session.idle,
|
|
509
|
+
// tool.execute.before, tool.execute.after, shell.env
|
|
510
|
+
// Maps to SDK hook types:
|
|
511
|
+
// session-start → session.created
|
|
512
|
+
// pre-tool-use → tool.execute.before
|
|
513
|
+
// post-tool-use → tool.execute.after
|
|
514
|
+
// Note: NO stop hook — session.idle is fire-and-forget
|
|
515
|
+
const supported = [
|
|
516
|
+
"session-start",
|
|
517
|
+
"pre-tool-use",
|
|
518
|
+
"post-tool-use",
|
|
519
|
+
];
|
|
520
|
+
return supported.includes(hookType);
|
|
521
|
+
},
|
|
522
|
+
getUnsupportedHookMessage(hookType) {
|
|
523
|
+
if (hookType === "stop") {
|
|
524
|
+
return ("OpenCode does not support a blocking stop hook. " +
|
|
525
|
+
"The session.idle event is fire-and-forget. " +
|
|
526
|
+
"Use in-turn orchestration or the SDK loop driver instead.");
|
|
527
|
+
}
|
|
528
|
+
return `Hook type "${hookType}" is not supported by the OpenCode adapter.`;
|
|
529
|
+
},
|
|
530
|
+
getCapabilities() {
|
|
531
|
+
// No StopHook, no SessionBinding natively, no MCP.
|
|
532
|
+
// HeadlessPrompt via @opencode-ai/sdk programmatic API.
|
|
533
|
+
return [types_1.HarnessCapability.HeadlessPrompt];
|
|
534
|
+
},
|
|
535
|
+
resolveSessionId(parsed) {
|
|
536
|
+
return resolveSessionIdInternal(parsed);
|
|
537
|
+
},
|
|
538
|
+
resolveStateDir(args) {
|
|
539
|
+
return resolveStateDirInternal(args);
|
|
540
|
+
},
|
|
541
|
+
resolvePluginRoot(args) {
|
|
542
|
+
const root = args.pluginRoot ||
|
|
543
|
+
process.env.OPENCODE_PLUGIN_ROOT;
|
|
544
|
+
return root ? path.resolve(root) : undefined;
|
|
545
|
+
},
|
|
546
|
+
bindSession(opts) {
|
|
547
|
+
return bindSessionImpl(opts);
|
|
548
|
+
},
|
|
549
|
+
handleStopHook(args) {
|
|
550
|
+
return handleStopHookImpl(args);
|
|
551
|
+
},
|
|
552
|
+
handleSessionStartHook(args) {
|
|
553
|
+
return handleSessionStartHookImpl(args);
|
|
554
|
+
},
|
|
555
|
+
findHookDispatcherPath(startCwd) {
|
|
556
|
+
// OpenCode plugins are in .opencode/plugins/, not standalone scripts.
|
|
557
|
+
// Walk up from startCwd looking for .opencode/plugins/babysitter/
|
|
558
|
+
let current = path.resolve(startCwd);
|
|
559
|
+
const root = path.parse(current).root;
|
|
560
|
+
while (current !== root) {
|
|
561
|
+
const candidate = path.join(current, ".opencode", "plugins", "babysitter", "index.js");
|
|
562
|
+
if ((0, node_fs_1.existsSync)(candidate))
|
|
563
|
+
return candidate;
|
|
564
|
+
const tsCandidate = path.join(current, ".opencode", "plugins", "babysitter", "index.ts");
|
|
565
|
+
if ((0, node_fs_1.existsSync)(tsCandidate))
|
|
566
|
+
return tsCandidate;
|
|
567
|
+
current = path.dirname(current);
|
|
568
|
+
}
|
|
569
|
+
return null;
|
|
570
|
+
},
|
|
571
|
+
installPlugin(options) {
|
|
572
|
+
return Promise.resolve(installOpenCodePlugin(options));
|
|
573
|
+
},
|
|
574
|
+
getPromptContext(opts) {
|
|
575
|
+
return (0, context_1.createOpenCodeContext)(opts);
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
}
|
package/dist/harness/pi.d.ts
CHANGED
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Oh-My-Pi harness adapter.
|
|
3
|
-
*
|
|
4
|
-
* Extends the SDK harness layer with "pi" support while reusing the
|
|
5
|
-
* mature Claude stop/session-start hook handlers. The Pi adapter maps
|
|
6
|
-
* Oh-My-Pi-specific environment conventions to the generic adapter interface.
|
|
7
|
-
*/
|
|
8
1
|
import type { HarnessAdapter, HarnessInstallOptions, HarnessInstallResult } from "./types";
|
|
9
|
-
export declare function
|
|
10
|
-
harness: "pi" | "oh-my-pi";
|
|
11
|
-
options: HarnessInstallOptions;
|
|
12
|
-
}): Promise<HarnessInstallResult>;
|
|
2
|
+
export declare function installPiPlugin(options: HarnessInstallOptions): Promise<HarnessInstallResult>;
|
|
13
3
|
export declare function createPiAdapter(): HarnessAdapter;
|
|
14
4
|
//# sourceMappingURL=pi.d.ts.map
|
package/dist/harness/pi.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pi.d.ts","sourceRoot":"","sources":["../../src/harness/pi.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pi.d.ts","sourceRoot":"","sources":["../../src/harness/pi.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EAGd,qBAAqB,EACrB,oBAAoB,EAGrB,MAAM,SAAS,CAAC;AAwHjB,wBAAsB,eAAe,CACnC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAM/B;AAED,wBAAgB,eAAe,IAAI,cAAc,CAmFhD"}
|