ralphctl 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -11
- package/dist/{add-CIM72NE3.mjs → add-DVPVHENV.mjs} +7 -7
- package/dist/{add-GX7P7XTT.mjs → add-YVXM34RP.mjs} +6 -5
- package/dist/{chunk-GL7MKLLS.mjs → chunk-ACRMBVEE.mjs} +458 -181
- package/dist/{chunk-NUYQK5MN.mjs → chunk-BSB4EDGR.mjs} +2 -2
- package/dist/{chunk-YCDUVPRT.mjs → chunk-CBMFRQ4Y.mjs} +5 -73
- package/dist/{chunk-3QBEBKMZ.mjs → chunk-FNAAA32W.mjs} +7 -7
- package/dist/{chunk-JOQO4HMM.mjs → chunk-GQ2WFKBN.mjs} +11 -11
- package/dist/{chunk-TKPTT2UG.mjs → chunk-OFILN7QL.mjs} +798 -1023
- package/dist/{chunk-7JLZQICD.mjs → chunk-OGEXYSFS.mjs} +7 -7
- package/dist/{chunk-D2YGPLIV.mjs → chunk-PYZEQ2VK.mjs} +214 -9
- package/dist/{chunk-57UWLHRH.mjs → chunk-VAZ3LJBI.mjs} +12 -1
- package/dist/{chunk-CTP2A436.mjs → chunk-WDMLPXOD.mjs} +11 -4
- package/dist/{chunk-FKMKOWLA.mjs → chunk-XN2UIHBY.mjs} +84 -3
- package/dist/chunk-ZLWSPLWI.mjs +1117 -0
- package/dist/cli.mjs +72 -21
- package/dist/create-Z635FQKO.mjs +15 -0
- package/dist/{handle-BBAZJ44Y.mjs → handle-23EFF3BE.mjs} +1 -1
- package/dist/{mount-ISHZM36X.mjs → mount-VEV3TESX.mjs} +1702 -1202
- package/dist/{project-2IE7VWDB.mjs → project-DQHF4ISP.mjs} +3 -3
- package/dist/prompts/check-script-discover.md +69 -0
- package/dist/prompts/repo-onboard.md +111 -0
- package/dist/prompts/sprint-feedback.md +4 -0
- package/dist/prompts/task-evaluation.md +44 -2
- package/dist/prompts/task-execution.md +5 -0
- package/dist/{resolver-EOE5WUMV.mjs → resolver-OVPYVW6Q.mjs} +4 -4
- package/dist/{sprint-OGOFEJJH.mjs → sprint-4E26AB5F.mjs} +4 -4
- package/dist/start-2WH4BTDB.mjs +19 -0
- package/package.json +6 -6
- package/dist/create-7WFSCMP4.mjs +0 -15
- package/dist/start-76JKJQIH.mjs +0 -17
|
@@ -0,0 +1,1117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
EXIT_INTERRUPTED
|
|
4
|
+
} from "./chunk-CFUVE2BP.mjs";
|
|
5
|
+
import {
|
|
6
|
+
getPrompt
|
|
7
|
+
} from "./chunk-747KW2RW.mjs";
|
|
8
|
+
import {
|
|
9
|
+
emoji,
|
|
10
|
+
getAiProvider,
|
|
11
|
+
log,
|
|
12
|
+
setAiProvider
|
|
13
|
+
} from "./chunk-XN2UIHBY.mjs";
|
|
14
|
+
import {
|
|
15
|
+
ensureError,
|
|
16
|
+
wrapAsync
|
|
17
|
+
} from "./chunk-IWXBJD2D.mjs";
|
|
18
|
+
import {
|
|
19
|
+
assertSafeCwd
|
|
20
|
+
} from "./chunk-WDMLPXOD.mjs";
|
|
21
|
+
import {
|
|
22
|
+
IOError,
|
|
23
|
+
SpawnError
|
|
24
|
+
} from "./chunk-VAZ3LJBI.mjs";
|
|
25
|
+
|
|
26
|
+
// src/integration/ui/tui/runtime/screen.ts
|
|
27
|
+
var ENTER_ALT_SCREEN = "\x1B[?1049h";
|
|
28
|
+
var LEAVE_ALT_SCREEN = "\x1B[?1049l";
|
|
29
|
+
var HIDE_CURSOR = "\x1B[?25l";
|
|
30
|
+
var SHOW_CURSOR = "\x1B[?25h";
|
|
31
|
+
var CLEAR_SCREEN = "\x1B[2J\x1B[H";
|
|
32
|
+
var altScreenActive = false;
|
|
33
|
+
var safetyNetsInstalled = false;
|
|
34
|
+
function writeRaw(seq) {
|
|
35
|
+
if (process.stdout.isTTY) process.stdout.write(seq);
|
|
36
|
+
}
|
|
37
|
+
function restore() {
|
|
38
|
+
if (!altScreenActive) return;
|
|
39
|
+
altScreenActive = false;
|
|
40
|
+
writeRaw(SHOW_CURSOR);
|
|
41
|
+
writeRaw(LEAVE_ALT_SCREEN);
|
|
42
|
+
}
|
|
43
|
+
function installSafetyNets() {
|
|
44
|
+
if (safetyNetsInstalled) return;
|
|
45
|
+
safetyNetsInstalled = true;
|
|
46
|
+
process.on("exit", restore);
|
|
47
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
48
|
+
process.on(sig, () => {
|
|
49
|
+
restore();
|
|
50
|
+
process.kill(process.pid, sig);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
process.on("uncaughtException", (err) => {
|
|
54
|
+
restore();
|
|
55
|
+
setImmediate(() => {
|
|
56
|
+
throw err;
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function enterAltScreen() {
|
|
61
|
+
if (altScreenActive) return;
|
|
62
|
+
if (!process.stdout.isTTY) return;
|
|
63
|
+
installSafetyNets();
|
|
64
|
+
altScreenActive = true;
|
|
65
|
+
writeRaw(ENTER_ALT_SCREEN);
|
|
66
|
+
writeRaw(CLEAR_SCREEN);
|
|
67
|
+
writeRaw(HIDE_CURSOR);
|
|
68
|
+
}
|
|
69
|
+
function exitAltScreen() {
|
|
70
|
+
restore();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/integration/ui/tui/runtime/suspend.ts
|
|
74
|
+
var activeInstance = null;
|
|
75
|
+
function registerTuiInstance(instance) {
|
|
76
|
+
activeInstance = instance;
|
|
77
|
+
return () => {
|
|
78
|
+
if (activeInstance === instance) {
|
|
79
|
+
activeInstance = null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function withSuspendedTui(cb) {
|
|
84
|
+
const instance = activeInstance;
|
|
85
|
+
if (instance === null) {
|
|
86
|
+
return cb();
|
|
87
|
+
}
|
|
88
|
+
exitAltScreen();
|
|
89
|
+
try {
|
|
90
|
+
return await cb();
|
|
91
|
+
} finally {
|
|
92
|
+
enterAltScreen();
|
|
93
|
+
instance.clear();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/integration/ai/session/session.ts
|
|
98
|
+
import { spawn, spawnSync } from "child_process";
|
|
99
|
+
|
|
100
|
+
// src/integration/ai/session/process-manager.ts
|
|
101
|
+
var GRACEFUL_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
102
|
+
var FORCE_QUIT_WINDOW_MS = 5e3;
|
|
103
|
+
var ProcessManager = class _ProcessManager {
|
|
104
|
+
static instance = null;
|
|
105
|
+
/** All active AI child processes */
|
|
106
|
+
children = /* @__PURE__ */ new Set();
|
|
107
|
+
/** Cleanup callbacks (for stopping spinners, removing temp files) */
|
|
108
|
+
cleanupCallbacks = /* @__PURE__ */ new Set();
|
|
109
|
+
/** Whether we're currently shutting down */
|
|
110
|
+
exiting = false;
|
|
111
|
+
/** Whether signal handlers have been installed */
|
|
112
|
+
handlersInstalled = false;
|
|
113
|
+
/** Timestamp of first SIGINT (for double-signal detection) */
|
|
114
|
+
firstSigintAt = null;
|
|
115
|
+
/** Stored signal handler references for cleanup */
|
|
116
|
+
sigintHandler = null;
|
|
117
|
+
sigtermHandler = null;
|
|
118
|
+
constructor() {
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get the singleton instance.
|
|
122
|
+
*/
|
|
123
|
+
static getInstance() {
|
|
124
|
+
_ProcessManager.instance ??= new _ProcessManager();
|
|
125
|
+
return _ProcessManager.instance;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Reset the singleton for testing.
|
|
129
|
+
* @internal
|
|
130
|
+
*/
|
|
131
|
+
static resetForTesting() {
|
|
132
|
+
if (_ProcessManager.instance) {
|
|
133
|
+
_ProcessManager.instance.dispose();
|
|
134
|
+
_ProcessManager.instance = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Register a child process for tracking.
|
|
139
|
+
* Automatically installs signal handlers on first registration.
|
|
140
|
+
* Throws an error if called during shutdown.
|
|
141
|
+
*
|
|
142
|
+
* @throws Error if called during shutdown
|
|
143
|
+
*/
|
|
144
|
+
registerChild(child) {
|
|
145
|
+
if (this.exiting) {
|
|
146
|
+
throw new Error("Cannot register child process during shutdown");
|
|
147
|
+
}
|
|
148
|
+
this.children.add(child);
|
|
149
|
+
child.once("close", () => {
|
|
150
|
+
this.children.delete(child);
|
|
151
|
+
});
|
|
152
|
+
if (!this.handlersInstalled) {
|
|
153
|
+
this.installSignalHandlers();
|
|
154
|
+
this.handlersInstalled = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Eagerly install signal handlers without requiring a child registration.
|
|
159
|
+
* Call this at the top of execution loops so Ctrl+C works even before
|
|
160
|
+
* the first AI process is spawned (e.g. while the spinner is visible).
|
|
161
|
+
* Idempotent — safe to call multiple times.
|
|
162
|
+
*/
|
|
163
|
+
ensureHandlers() {
|
|
164
|
+
if (!this.handlersInstalled) {
|
|
165
|
+
this.installSignalHandlers();
|
|
166
|
+
this.handlersInstalled = true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Wire a task-scoped AbortSignal to a terminate callback. When the signal
|
|
171
|
+
* aborts, the callback fires once. Returns a disposer that detaches the
|
|
172
|
+
* listener — call it in a `finally` after the child exits so listeners
|
|
173
|
+
* don't accumulate across repeated runs.
|
|
174
|
+
*
|
|
175
|
+
* If the signal is already aborted, terminate runs on the next microtask
|
|
176
|
+
* so the caller always observes a consistent async flow.
|
|
177
|
+
*/
|
|
178
|
+
registerAbort(signal, terminate) {
|
|
179
|
+
let fired = false;
|
|
180
|
+
const handler = () => {
|
|
181
|
+
if (fired) return;
|
|
182
|
+
fired = true;
|
|
183
|
+
try {
|
|
184
|
+
terminate();
|
|
185
|
+
} catch (err) {
|
|
186
|
+
log.error(`Error in abort-signal handler: ${err instanceof Error ? err.message : String(err)}`);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
if (signal.aborted) {
|
|
190
|
+
queueMicrotask(handler);
|
|
191
|
+
}
|
|
192
|
+
signal.addEventListener("abort", handler, { once: true });
|
|
193
|
+
return () => {
|
|
194
|
+
signal.removeEventListener("abort", handler);
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Check if a shutdown is in progress.
|
|
199
|
+
* Used by execution loops to break immediately on Ctrl+C.
|
|
200
|
+
*/
|
|
201
|
+
isShuttingDown() {
|
|
202
|
+
return this.exiting;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Manually unregister a child process.
|
|
206
|
+
* Normally not needed - children auto-unregister via event listeners.
|
|
207
|
+
*/
|
|
208
|
+
unregisterChild(child) {
|
|
209
|
+
this.children.delete(child);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Register a cleanup callback (for spinners, temp files, etc.).
|
|
213
|
+
* Returns a deregister function.
|
|
214
|
+
*/
|
|
215
|
+
registerCleanup(callback) {
|
|
216
|
+
this.cleanupCallbacks.add(callback);
|
|
217
|
+
return () => {
|
|
218
|
+
this.cleanupCallbacks.delete(callback);
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Kill all tracked child processes with the given signal.
|
|
223
|
+
* Catches errors (ESRCH = already dead, EPERM = permission denied).
|
|
224
|
+
*/
|
|
225
|
+
killAll(signal) {
|
|
226
|
+
for (const child of this.children) {
|
|
227
|
+
try {
|
|
228
|
+
child.kill(signal);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
const error = err;
|
|
231
|
+
if (error.code === "ESRCH") {
|
|
232
|
+
this.children.delete(child);
|
|
233
|
+
} else if (error.code === "EPERM") {
|
|
234
|
+
log.warn(`Permission denied killing process ${String(child.pid)}`);
|
|
235
|
+
} else {
|
|
236
|
+
log.error(`Error killing process ${String(child.pid)}: ${error.message}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Graceful shutdown sequence:
|
|
243
|
+
* 1. Run all cleanup callbacks (stop spinners)
|
|
244
|
+
* 2. Send SIGINT to all children (what AI CLI processes expect)
|
|
245
|
+
* 3. Wait up to 5 seconds for children to exit
|
|
246
|
+
* 4. Send SIGKILL to any remaining children (force)
|
|
247
|
+
* 5. Exit with code 130 (SIGINT) or 1 (force-quit)
|
|
248
|
+
*
|
|
249
|
+
* Double Ctrl+C: immediate SIGKILL + exit(1)
|
|
250
|
+
*/
|
|
251
|
+
async shutdown(signal) {
|
|
252
|
+
if (signal === "SIGINT" && this.firstSigintAt) {
|
|
253
|
+
const now = Date.now();
|
|
254
|
+
if (now - this.firstSigintAt < FORCE_QUIT_WINDOW_MS) {
|
|
255
|
+
log.warn("\n\nForce quit (double signal) \u2014 killing all processes immediately...");
|
|
256
|
+
this.killAll("SIGKILL");
|
|
257
|
+
process.exit(1);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (this.exiting) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
this.exiting = true;
|
|
265
|
+
if (signal === "SIGINT") {
|
|
266
|
+
this.firstSigintAt = Date.now();
|
|
267
|
+
}
|
|
268
|
+
log.dim("\n\nShutting down gracefully... (press Ctrl+C again to force-quit)");
|
|
269
|
+
for (const callback of this.cleanupCallbacks) {
|
|
270
|
+
try {
|
|
271
|
+
callback();
|
|
272
|
+
} catch (err) {
|
|
273
|
+
log.error(`Error in cleanup callback: ${err instanceof Error ? err.message : String(err)}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
this.cleanupCallbacks.clear();
|
|
277
|
+
this.killAll("SIGINT");
|
|
278
|
+
const waitStart = Date.now();
|
|
279
|
+
while (this.children.size > 0 && Date.now() - waitStart < GRACEFUL_SHUTDOWN_TIMEOUT_MS) {
|
|
280
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
281
|
+
}
|
|
282
|
+
if (this.children.size > 0) {
|
|
283
|
+
log.warn(`Force-killing ${String(this.children.size)} remaining process(es)...`);
|
|
284
|
+
this.killAll("SIGKILL");
|
|
285
|
+
}
|
|
286
|
+
process.exit(signal === "SIGINT" ? EXIT_INTERRUPTED : 1);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Clean up all resources (for testing).
|
|
290
|
+
* @internal
|
|
291
|
+
*/
|
|
292
|
+
dispose() {
|
|
293
|
+
if (this.sigintHandler) {
|
|
294
|
+
process.removeListener("SIGINT", this.sigintHandler);
|
|
295
|
+
this.sigintHandler = null;
|
|
296
|
+
}
|
|
297
|
+
if (this.sigtermHandler) {
|
|
298
|
+
process.removeListener("SIGTERM", this.sigtermHandler);
|
|
299
|
+
this.sigtermHandler = null;
|
|
300
|
+
}
|
|
301
|
+
this.children.clear();
|
|
302
|
+
this.cleanupCallbacks.clear();
|
|
303
|
+
this.exiting = false;
|
|
304
|
+
this.handlersInstalled = false;
|
|
305
|
+
this.firstSigintAt = null;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Install signal handlers for SIGINT and SIGTERM.
|
|
309
|
+
* Uses process.on() (persistent) not process.once() (one-shot).
|
|
310
|
+
* Stores handler references so dispose() can remove them.
|
|
311
|
+
*/
|
|
312
|
+
installSignalHandlers() {
|
|
313
|
+
this.sigintHandler = () => {
|
|
314
|
+
void this.shutdown("SIGINT");
|
|
315
|
+
};
|
|
316
|
+
this.sigtermHandler = () => {
|
|
317
|
+
void this.shutdown("SIGTERM");
|
|
318
|
+
};
|
|
319
|
+
process.on("SIGINT", this.sigintHandler);
|
|
320
|
+
process.on("SIGTERM", this.sigtermHandler);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
var processLifecycleAdapter = {
|
|
324
|
+
ensureHandlers: () => {
|
|
325
|
+
ProcessManager.getInstance().ensureHandlers();
|
|
326
|
+
},
|
|
327
|
+
isShuttingDown: () => ProcessManager.getInstance().isShuttingDown(),
|
|
328
|
+
registerAbort: (signal, terminate) => ProcessManager.getInstance().registerAbort(signal, terminate)
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// src/integration/ai/providers/claude.ts
|
|
332
|
+
import { Result } from "typescript-result";
|
|
333
|
+
var claudeAdapter = {
|
|
334
|
+
name: "claude",
|
|
335
|
+
displayName: "Claude",
|
|
336
|
+
binary: "claude",
|
|
337
|
+
baseArgs: ["--permission-mode", "acceptEdits", "--effort", "xhigh"],
|
|
338
|
+
experimental: false,
|
|
339
|
+
buildInteractiveArgs(prompt, extraArgs = []) {
|
|
340
|
+
return [...this.baseArgs, ...extraArgs, "--", prompt];
|
|
341
|
+
},
|
|
342
|
+
buildHeadlessArgs(extraArgs = []) {
|
|
343
|
+
return ["-p", "--output-format", "json", ...this.baseArgs, ...extraArgs];
|
|
344
|
+
},
|
|
345
|
+
parseJsonOutput(stdout) {
|
|
346
|
+
const jsonResult = Result.try(() => JSON.parse(stdout));
|
|
347
|
+
if (!jsonResult.ok) {
|
|
348
|
+
return { result: stdout, sessionId: null, model: null };
|
|
349
|
+
}
|
|
350
|
+
const parsed = jsonResult.value;
|
|
351
|
+
return {
|
|
352
|
+
result: parsed.result ?? stdout,
|
|
353
|
+
sessionId: parsed.session_id ?? null,
|
|
354
|
+
model: parsed.model ?? null
|
|
355
|
+
};
|
|
356
|
+
},
|
|
357
|
+
buildResumeArgs(sessionId) {
|
|
358
|
+
if (!/^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,127}$/.test(sessionId)) {
|
|
359
|
+
throw new Error("Invalid session ID format");
|
|
360
|
+
}
|
|
361
|
+
return ["--resume", sessionId];
|
|
362
|
+
},
|
|
363
|
+
detectRateLimit(stderr) {
|
|
364
|
+
const patterns = [/rate.?limit/i, /\b429\b/, /too many requests/i, /overloaded/i, /\b529\b/];
|
|
365
|
+
const isRateLimited = patterns.some((p) => p.test(stderr));
|
|
366
|
+
if (!isRateLimited) {
|
|
367
|
+
return { rateLimited: false, retryAfterMs: null };
|
|
368
|
+
}
|
|
369
|
+
const retryMatch = /retry.?after:?\s*(\d+)/i.exec(stderr);
|
|
370
|
+
const retryAfterMs = retryMatch?.[1] ? parseInt(retryMatch[1], 10) * 1e3 : null;
|
|
371
|
+
return { rateLimited: true, retryAfterMs };
|
|
372
|
+
},
|
|
373
|
+
getSpawnEnv() {
|
|
374
|
+
return { CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: "1" };
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// src/integration/ai/providers/copilot.ts
|
|
379
|
+
import { lstat, readdir, unlink } from "fs/promises";
|
|
380
|
+
import { join } from "path";
|
|
381
|
+
import { Result as Result2 } from "typescript-result";
|
|
382
|
+
var copilotAdapter = {
|
|
383
|
+
name: "copilot",
|
|
384
|
+
displayName: "Copilot",
|
|
385
|
+
binary: "copilot",
|
|
386
|
+
experimental: true,
|
|
387
|
+
baseArgs: ["--allow-all-tools"],
|
|
388
|
+
buildInteractiveArgs(prompt, extraArgs = []) {
|
|
389
|
+
return [...this.baseArgs, ...extraArgs, "-i", prompt];
|
|
390
|
+
},
|
|
391
|
+
buildHeadlessArgs(extraArgs = []) {
|
|
392
|
+
return ["-p", "--output-format", "json", "--autopilot", "--no-ask-user", "--share", ...this.baseArgs, ...extraArgs];
|
|
393
|
+
},
|
|
394
|
+
parseJsonOutput(stdout) {
|
|
395
|
+
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
396
|
+
if (lines.length === 0) {
|
|
397
|
+
return { result: "", sessionId: null, model: null };
|
|
398
|
+
}
|
|
399
|
+
const lastLine = lines.at(-1) ?? "";
|
|
400
|
+
const jsonResult = Result2.try(() => JSON.parse(lastLine));
|
|
401
|
+
if (jsonResult.ok) {
|
|
402
|
+
const parsed = jsonResult.value;
|
|
403
|
+
return {
|
|
404
|
+
result: parsed.result ?? parsed.result_text ?? lastLine,
|
|
405
|
+
sessionId: parsed.session_id ?? null,
|
|
406
|
+
model: null
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return { result: stdout.trim(), sessionId: null, model: null };
|
|
410
|
+
},
|
|
411
|
+
buildResumeArgs(sessionId) {
|
|
412
|
+
if (!/^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,127}$/.test(sessionId)) {
|
|
413
|
+
throw new Error("Invalid session ID format");
|
|
414
|
+
}
|
|
415
|
+
return [`--resume=${sessionId}`];
|
|
416
|
+
},
|
|
417
|
+
async extractSessionId(cwd) {
|
|
418
|
+
const filesResult = await wrapAsync(
|
|
419
|
+
() => readdir(cwd),
|
|
420
|
+
(err) => new IOError(`Failed to read directory: ${cwd}`, err instanceof Error ? err : void 0)
|
|
421
|
+
);
|
|
422
|
+
if (!filesResult.ok) return null;
|
|
423
|
+
const files = filesResult.value;
|
|
424
|
+
const shareFile = files.find((f) => /^copilot-session-[a-zA-Z0-9_][a-zA-Z0-9_-]*\.md$/.test(f));
|
|
425
|
+
if (!shareFile) return null;
|
|
426
|
+
const match = /^copilot-session-([a-zA-Z0-9_][a-zA-Z0-9_-]{0,127})\.md$/.exec(shareFile);
|
|
427
|
+
if (!match?.[1]) return null;
|
|
428
|
+
const filePath = join(cwd, shareFile);
|
|
429
|
+
const stat = await lstat(filePath).catch(() => null);
|
|
430
|
+
if (stat?.isFile()) {
|
|
431
|
+
await unlink(filePath).catch(() => {
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return match[1];
|
|
435
|
+
},
|
|
436
|
+
detectRateLimit(stderr) {
|
|
437
|
+
const patterns = [/rate.?limit/i, /\b429\b/, /too many requests/i, /overloaded/i, /\b529\b/];
|
|
438
|
+
const isRateLimited = patterns.some((p) => p.test(stderr));
|
|
439
|
+
if (!isRateLimited) {
|
|
440
|
+
return { rateLimited: false, retryAfterMs: null };
|
|
441
|
+
}
|
|
442
|
+
const retryMatch = /retry.?after:?\s*(\d+)/i.exec(stderr);
|
|
443
|
+
const retryAfterMs = retryMatch?.[1] ? parseInt(retryMatch[1], 10) * 1e3 : null;
|
|
444
|
+
return { rateLimited: true, retryAfterMs };
|
|
445
|
+
},
|
|
446
|
+
getSpawnEnv() {
|
|
447
|
+
return {};
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// src/integration/external/provider.ts
|
|
452
|
+
async function resolveProvider() {
|
|
453
|
+
const stored = await getAiProvider();
|
|
454
|
+
if (stored) return stored;
|
|
455
|
+
const choice = await getPrompt().select({
|
|
456
|
+
message: `${emoji.donut} Which AI buddy should help with my homework?`,
|
|
457
|
+
choices: [
|
|
458
|
+
{ label: "Claude Code", value: "claude" },
|
|
459
|
+
{ label: "GitHub Copilot", value: "copilot" }
|
|
460
|
+
]
|
|
461
|
+
});
|
|
462
|
+
await setAiProvider(choice);
|
|
463
|
+
return choice;
|
|
464
|
+
}
|
|
465
|
+
function providerDisplayName(provider) {
|
|
466
|
+
return provider === "claude" ? "Claude" : "Copilot";
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/integration/ai/providers/registry.ts
|
|
470
|
+
function getProvider(provider) {
|
|
471
|
+
switch (provider) {
|
|
472
|
+
case "claude":
|
|
473
|
+
return claudeAdapter;
|
|
474
|
+
case "copilot":
|
|
475
|
+
return copilotAdapter;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async function getActiveProvider() {
|
|
479
|
+
const provider = await resolveProvider();
|
|
480
|
+
return getProvider(provider);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/integration/ai/session/session.ts
|
|
484
|
+
function spawnInteractive(prompt, options, provider) {
|
|
485
|
+
assertSafeCwd(options.cwd);
|
|
486
|
+
const args = prompt ? provider.buildInteractiveArgs(prompt, options.args ?? []) : [...provider.baseArgs, ...options.args ?? []];
|
|
487
|
+
const env = options.env ? { ...process.env, ...options.env } : void 0;
|
|
488
|
+
const result = spawnSync(provider.binary, args, {
|
|
489
|
+
cwd: options.cwd,
|
|
490
|
+
stdio: "inherit",
|
|
491
|
+
env
|
|
492
|
+
});
|
|
493
|
+
if (result.error) {
|
|
494
|
+
return { code: 1, error: `Failed to spawn ${provider.binary} CLI: ${result.error.message}` };
|
|
495
|
+
}
|
|
496
|
+
return { code: result.status ?? 1 };
|
|
497
|
+
}
|
|
498
|
+
async function spawnHeadless(options, provider) {
|
|
499
|
+
assertSafeCwd(options.cwd);
|
|
500
|
+
const p = provider ?? await getActiveProvider();
|
|
501
|
+
return new Promise((resolve, reject) => {
|
|
502
|
+
const allArgs = p.buildHeadlessArgs(options.args ?? []);
|
|
503
|
+
if (options.resumeSessionId) {
|
|
504
|
+
try {
|
|
505
|
+
allArgs.push(...p.buildResumeArgs(options.resumeSessionId));
|
|
506
|
+
} catch {
|
|
507
|
+
reject(new SpawnError("Invalid session ID format", "", 1));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const child = spawn(p.binary, allArgs, {
|
|
512
|
+
cwd: options.cwd,
|
|
513
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
514
|
+
env: options.env ? { ...process.env, ...options.env } : void 0
|
|
515
|
+
});
|
|
516
|
+
const manager = ProcessManager.getInstance();
|
|
517
|
+
try {
|
|
518
|
+
manager.registerChild(child);
|
|
519
|
+
} catch {
|
|
520
|
+
reject(new SpawnError("Cannot spawn during shutdown", "", 1));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
let detachAbort = null;
|
|
524
|
+
if (options.abortSignal) {
|
|
525
|
+
detachAbort = manager.registerAbort(options.abortSignal, () => {
|
|
526
|
+
try {
|
|
527
|
+
child.kill("SIGTERM");
|
|
528
|
+
} catch {
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
const MAX_STDOUT_SIZE = 1e7;
|
|
533
|
+
const MAX_PROMPT_SIZE = 1e6;
|
|
534
|
+
if (options.prompt) {
|
|
535
|
+
if (options.prompt.length > MAX_PROMPT_SIZE) {
|
|
536
|
+
reject(new SpawnError("Prompt exceeds maximum size (1MB)", "", 1));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
child.stdin.write(options.prompt);
|
|
540
|
+
}
|
|
541
|
+
child.stdin.end();
|
|
542
|
+
let rawStdout = "";
|
|
543
|
+
let stderr = "";
|
|
544
|
+
child.stdout.on("data", (data) => {
|
|
545
|
+
if (rawStdout.length < MAX_STDOUT_SIZE) {
|
|
546
|
+
rawStdout += data.toString();
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
child.stderr.on("data", (data) => {
|
|
550
|
+
stderr += data.toString();
|
|
551
|
+
});
|
|
552
|
+
child.on("close", (code) => {
|
|
553
|
+
detachAbort?.();
|
|
554
|
+
void (async () => {
|
|
555
|
+
const exitCode = code ?? 1;
|
|
556
|
+
const { result, sessionId: parsedSessionId, model: parsedModel } = p.parseJsonOutput(rawStdout);
|
|
557
|
+
const sessionId = parsedSessionId ?? await p.extractSessionId?.(options.cwd) ?? null;
|
|
558
|
+
if (exitCode !== 0) {
|
|
559
|
+
reject(
|
|
560
|
+
new SpawnError(
|
|
561
|
+
`${p.displayName} CLI exited with code ${String(exitCode)}: ${stderr}`,
|
|
562
|
+
stderr,
|
|
563
|
+
exitCode,
|
|
564
|
+
sessionId
|
|
565
|
+
)
|
|
566
|
+
);
|
|
567
|
+
} else {
|
|
568
|
+
resolve({ stdout: result, stderr, exitCode: 0, sessionId, model: parsedModel });
|
|
569
|
+
}
|
|
570
|
+
})().catch((err) => {
|
|
571
|
+
reject(new SpawnError(`Unexpected error in close handler: ${String(err)}`, "", 1));
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
child.on("error", (err) => {
|
|
575
|
+
detachAbort?.();
|
|
576
|
+
reject(new SpawnError(`Failed to spawn ${p.binary} CLI: ${err.message}`, "", 1));
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
var DEFAULT_MAX_RETRIES = 5;
|
|
581
|
+
var BASE_DELAY_MS = 2e3;
|
|
582
|
+
var MAX_DELAY_MS = 12e4;
|
|
583
|
+
var DEFAULT_TOTAL_TIMEOUT_MS = 6e5;
|
|
584
|
+
function sleep(ms) {
|
|
585
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
586
|
+
}
|
|
587
|
+
function jitter() {
|
|
588
|
+
return Math.floor(Math.random() * 1e3);
|
|
589
|
+
}
|
|
590
|
+
async function spawnWithRetry(options, retryOptions, provider) {
|
|
591
|
+
const p = provider ?? await getActiveProvider();
|
|
592
|
+
const maxRetries = retryOptions?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
593
|
+
const totalTimeoutMs = retryOptions?.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT_MS;
|
|
594
|
+
const startTime = Date.now();
|
|
595
|
+
let resumeSessionId = options.resumeSessionId;
|
|
596
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
597
|
+
const elapsed = Date.now() - startTime;
|
|
598
|
+
if (attempt > 0 && elapsed >= totalTimeoutMs) {
|
|
599
|
+
throw new SpawnError(`Total retry timeout exceeded (${String(totalTimeoutMs)}ms)`, "", 1, resumeSessionId);
|
|
600
|
+
}
|
|
601
|
+
if (options.abortSignal?.aborted) {
|
|
602
|
+
throw new SpawnError("Aborted by caller", "", 1, resumeSessionId);
|
|
603
|
+
}
|
|
604
|
+
const r = await wrapAsync(async () => spawnHeadless({ ...options, resumeSessionId }, p), ensureError);
|
|
605
|
+
if (r.ok) return r.value;
|
|
606
|
+
const err = r.error;
|
|
607
|
+
if (!(err instanceof SpawnError) || !err.rateLimited) {
|
|
608
|
+
throw err;
|
|
609
|
+
}
|
|
610
|
+
if (err.sessionId) {
|
|
611
|
+
resumeSessionId = err.sessionId;
|
|
612
|
+
}
|
|
613
|
+
if (attempt >= maxRetries) {
|
|
614
|
+
throw err;
|
|
615
|
+
}
|
|
616
|
+
const delay = Math.min(err.retryAfterMs ?? BASE_DELAY_MS * Math.pow(2, attempt), MAX_DELAY_MS) + jitter();
|
|
617
|
+
retryOptions?.onRetry?.(attempt + 1, delay, err);
|
|
618
|
+
await sleep(delay);
|
|
619
|
+
}
|
|
620
|
+
throw new Error("Max retries exceeded");
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/integration/ai/session/session-adapter.ts
|
|
624
|
+
var ProviderAiSessionAdapter = class {
|
|
625
|
+
provider = null;
|
|
626
|
+
/** Lazily resolve and cache the active provider. */
|
|
627
|
+
async getProvider() {
|
|
628
|
+
this.provider ??= await getActiveProvider();
|
|
629
|
+
return this.provider;
|
|
630
|
+
}
|
|
631
|
+
/** Public eager resolver — required before the sync getters can be used safely. */
|
|
632
|
+
async ensureReady() {
|
|
633
|
+
await this.getProvider();
|
|
634
|
+
}
|
|
635
|
+
async spawnInteractive(prompt, options) {
|
|
636
|
+
const provider = await this.getProvider();
|
|
637
|
+
await withSuspendedTui(() => {
|
|
638
|
+
const result = spawnInteractive(
|
|
639
|
+
prompt,
|
|
640
|
+
{
|
|
641
|
+
cwd: options.cwd,
|
|
642
|
+
args: options.args,
|
|
643
|
+
env: options.env
|
|
644
|
+
},
|
|
645
|
+
provider
|
|
646
|
+
);
|
|
647
|
+
if (result.error) {
|
|
648
|
+
throw new Error(result.error);
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
async spawnHeadless(prompt, options) {
|
|
653
|
+
const provider = await this.getProvider();
|
|
654
|
+
const result = await spawnHeadless(
|
|
655
|
+
{
|
|
656
|
+
cwd: options.cwd,
|
|
657
|
+
args: options.args,
|
|
658
|
+
env: options.env,
|
|
659
|
+
prompt,
|
|
660
|
+
resumeSessionId: options.resumeSessionId,
|
|
661
|
+
abortSignal: options.abortSignal
|
|
662
|
+
},
|
|
663
|
+
provider
|
|
664
|
+
);
|
|
665
|
+
return {
|
|
666
|
+
output: result.stdout,
|
|
667
|
+
sessionId: result.sessionId ?? void 0,
|
|
668
|
+
model: result.model ?? void 0
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
async spawnWithRetry(prompt, options) {
|
|
672
|
+
const provider = await this.getProvider();
|
|
673
|
+
const result = await spawnWithRetry(
|
|
674
|
+
{
|
|
675
|
+
cwd: options.cwd,
|
|
676
|
+
args: options.args,
|
|
677
|
+
env: options.env,
|
|
678
|
+
prompt,
|
|
679
|
+
resumeSessionId: options.resumeSessionId,
|
|
680
|
+
abortSignal: options.abortSignal
|
|
681
|
+
},
|
|
682
|
+
{ maxRetries: options.maxRetries },
|
|
683
|
+
provider
|
|
684
|
+
);
|
|
685
|
+
return {
|
|
686
|
+
output: result.stdout,
|
|
687
|
+
sessionId: result.sessionId ?? void 0,
|
|
688
|
+
model: result.model ?? void 0
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
async resumeSession(sessionId, prompt, options) {
|
|
692
|
+
const provider = await this.getProvider();
|
|
693
|
+
const result = await spawnWithRetry(
|
|
694
|
+
{
|
|
695
|
+
cwd: options.cwd,
|
|
696
|
+
args: options.args,
|
|
697
|
+
env: options.env,
|
|
698
|
+
prompt,
|
|
699
|
+
resumeSessionId: sessionId,
|
|
700
|
+
abortSignal: options.abortSignal
|
|
701
|
+
},
|
|
702
|
+
void 0,
|
|
703
|
+
provider
|
|
704
|
+
);
|
|
705
|
+
return {
|
|
706
|
+
output: result.stdout,
|
|
707
|
+
sessionId: result.sessionId ?? void 0,
|
|
708
|
+
model: result.model ?? void 0
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
getProviderName() {
|
|
712
|
+
if (!this.provider) {
|
|
713
|
+
throw new Error("Provider not yet resolved. Call an async method first.");
|
|
714
|
+
}
|
|
715
|
+
return this.provider.name;
|
|
716
|
+
}
|
|
717
|
+
getProviderDisplayName() {
|
|
718
|
+
if (!this.provider) {
|
|
719
|
+
throw new Error("Provider not yet resolved. Call an async method first.");
|
|
720
|
+
}
|
|
721
|
+
return this.provider.displayName;
|
|
722
|
+
}
|
|
723
|
+
getSpawnEnv() {
|
|
724
|
+
if (!this.provider) {
|
|
725
|
+
throw new Error("Provider not yet resolved. Call an async method first.");
|
|
726
|
+
}
|
|
727
|
+
return this.provider.getSpawnEnv();
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// src/integration/signals/parser.ts
|
|
732
|
+
var SIGNAL_PATTERNS = {
|
|
733
|
+
progress: /<progress>([\s\S]*?)<\/progress>/g,
|
|
734
|
+
progressWithFiles: /<progress>([\s\S]*?)<\/progress>/,
|
|
735
|
+
evaluation_passed: /<evaluation-passed>/,
|
|
736
|
+
evaluation_failed: /<evaluation-failed>([\s\S]*?)<\/evaluation-failed>/,
|
|
737
|
+
task_verified: /<task-verified>([\s\S]*?)<\/task-verified>/,
|
|
738
|
+
task_complete: /<task-complete>/,
|
|
739
|
+
task_blocked: /<task-blocked>([\s\S]*?)<\/task-blocked>/,
|
|
740
|
+
note: /<note>([\s\S]*?)<\/note>/g,
|
|
741
|
+
check_script: /<check-script>([\s\S]*?)<\/check-script>/,
|
|
742
|
+
agents_md: /<agents-md>([\s\S]*?)<\/agents-md>/
|
|
743
|
+
};
|
|
744
|
+
var DIMENSION_LINE = /\*\*([A-Za-z][A-Za-z0-9]{2,29})\*\*\s*:\s*(PASS|FAIL)\s*(?:—|-)\s*(.+)/gi;
|
|
745
|
+
function parseDimensionScores(output) {
|
|
746
|
+
const scores = [];
|
|
747
|
+
const seen = /* @__PURE__ */ new Set();
|
|
748
|
+
DIMENSION_LINE.lastIndex = 0;
|
|
749
|
+
let match;
|
|
750
|
+
while ((match = DIMENSION_LINE.exec(output)) !== null) {
|
|
751
|
+
const rawName = match[1];
|
|
752
|
+
const verdict = match[2];
|
|
753
|
+
const finding = match[3];
|
|
754
|
+
if (!rawName || !verdict || !finding) continue;
|
|
755
|
+
const name = rawName.toLowerCase();
|
|
756
|
+
if (seen.has(name)) continue;
|
|
757
|
+
seen.add(name);
|
|
758
|
+
scores.push({
|
|
759
|
+
dimension: name,
|
|
760
|
+
passed: verdict.toUpperCase() === "PASS",
|
|
761
|
+
finding: finding.trim()
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
return scores;
|
|
765
|
+
}
|
|
766
|
+
var DANGEROUS_COMMAND_PATTERNS = [
|
|
767
|
+
/\|\s*(ba)?sh\b/,
|
|
768
|
+
/\bcurl\b[^|;&\n]*\|/,
|
|
769
|
+
/\bwget\b[^|;&\n]*(-O-|--output-document=-)[^|;&\n]*\|/,
|
|
770
|
+
/\beval\b/,
|
|
771
|
+
/\brm\s+-[rf]+\b/
|
|
772
|
+
];
|
|
773
|
+
function isDangerousCommand(command) {
|
|
774
|
+
return DANGEROUS_COMMAND_PATTERNS.some((re) => re.test(command));
|
|
775
|
+
}
|
|
776
|
+
var SignalParser = class {
|
|
777
|
+
parseSignals(output) {
|
|
778
|
+
const signals = [];
|
|
779
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
780
|
+
let progressMatch;
|
|
781
|
+
while ((progressMatch = SIGNAL_PATTERNS.progress.exec(output)) !== null) {
|
|
782
|
+
const summary = progressMatch[1]?.trim();
|
|
783
|
+
if (summary) {
|
|
784
|
+
const progressSignal = {
|
|
785
|
+
type: "progress",
|
|
786
|
+
summary,
|
|
787
|
+
// Note: Phase 1 doesn't parse files attribute; added in Phase 2+
|
|
788
|
+
timestamp
|
|
789
|
+
};
|
|
790
|
+
signals.push(progressSignal);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (output.includes("<evaluation-passed>")) {
|
|
794
|
+
const dimensions = parseDimensionScores(output);
|
|
795
|
+
const evaluationSignal = {
|
|
796
|
+
type: "evaluation",
|
|
797
|
+
status: "passed",
|
|
798
|
+
dimensions,
|
|
799
|
+
timestamp
|
|
800
|
+
};
|
|
801
|
+
signals.push(evaluationSignal);
|
|
802
|
+
} else {
|
|
803
|
+
const failedMatch = SIGNAL_PATTERNS.evaluation_failed.exec(output);
|
|
804
|
+
if (failedMatch?.[1]) {
|
|
805
|
+
const critique = failedMatch[1].trim();
|
|
806
|
+
const dimensions = parseDimensionScores(output);
|
|
807
|
+
const evaluationSignal = {
|
|
808
|
+
type: "evaluation",
|
|
809
|
+
status: dimensions.length > 0 ? "failed" : "malformed",
|
|
810
|
+
dimensions,
|
|
811
|
+
critique: dimensions.length > 0 ? critique : void 0,
|
|
812
|
+
timestamp
|
|
813
|
+
};
|
|
814
|
+
signals.push(evaluationSignal);
|
|
815
|
+
} else if (parseDimensionScores(output).length > 0) {
|
|
816
|
+
const dimensions = parseDimensionScores(output);
|
|
817
|
+
const evaluationSignal = {
|
|
818
|
+
type: "evaluation",
|
|
819
|
+
status: "failed",
|
|
820
|
+
dimensions,
|
|
821
|
+
timestamp
|
|
822
|
+
};
|
|
823
|
+
signals.push(evaluationSignal);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const taskVerifiedMatch = SIGNAL_PATTERNS.task_verified.exec(output);
|
|
827
|
+
if (taskVerifiedMatch?.[1]) {
|
|
828
|
+
const verificationOutput = taskVerifiedMatch[1].trim();
|
|
829
|
+
const verifiedSignal = {
|
|
830
|
+
type: "task-verified",
|
|
831
|
+
output: verificationOutput,
|
|
832
|
+
timestamp
|
|
833
|
+
};
|
|
834
|
+
signals.push(verifiedSignal);
|
|
835
|
+
}
|
|
836
|
+
if (output.includes("<task-complete>")) {
|
|
837
|
+
const completeSignal = {
|
|
838
|
+
type: "task-complete",
|
|
839
|
+
timestamp
|
|
840
|
+
};
|
|
841
|
+
signals.push(completeSignal);
|
|
842
|
+
}
|
|
843
|
+
const taskBlockedMatch = SIGNAL_PATTERNS.task_blocked.exec(output);
|
|
844
|
+
if (taskBlockedMatch?.[1]) {
|
|
845
|
+
const reason = taskBlockedMatch[1].trim();
|
|
846
|
+
const blockedSignal = {
|
|
847
|
+
type: "task-blocked",
|
|
848
|
+
reason,
|
|
849
|
+
timestamp
|
|
850
|
+
};
|
|
851
|
+
signals.push(blockedSignal);
|
|
852
|
+
}
|
|
853
|
+
let noteMatch;
|
|
854
|
+
while ((noteMatch = SIGNAL_PATTERNS.note.exec(output)) !== null) {
|
|
855
|
+
const text = noteMatch[1]?.trim();
|
|
856
|
+
if (text) {
|
|
857
|
+
const noteSignal = {
|
|
858
|
+
type: "note",
|
|
859
|
+
text,
|
|
860
|
+
timestamp
|
|
861
|
+
};
|
|
862
|
+
signals.push(noteSignal);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const checkScriptMatch = SIGNAL_PATTERNS.check_script.exec(output);
|
|
866
|
+
if (checkScriptMatch?.[1]) {
|
|
867
|
+
const command = checkScriptMatch[1].trim();
|
|
868
|
+
if (command.length > 0 && !isDangerousCommand(command)) {
|
|
869
|
+
const checkScriptSignal = {
|
|
870
|
+
type: "check-script-discovery",
|
|
871
|
+
command,
|
|
872
|
+
timestamp
|
|
873
|
+
};
|
|
874
|
+
signals.push(checkScriptSignal);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const agentsMdMatch = SIGNAL_PATTERNS.agents_md.exec(output);
|
|
878
|
+
if (agentsMdMatch?.[1]) {
|
|
879
|
+
const content = agentsMdMatch[1].trim();
|
|
880
|
+
if (content.length > 0) {
|
|
881
|
+
const agentsMdSignal = {
|
|
882
|
+
type: "agents-md-proposal",
|
|
883
|
+
content,
|
|
884
|
+
timestamp
|
|
885
|
+
};
|
|
886
|
+
signals.push(agentsMdSignal);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return signals;
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
// src/integration/ai/prompts/loader.ts
|
|
894
|
+
import { existsSync, readFileSync } from "fs";
|
|
895
|
+
import { dirname, join as join2 } from "path";
|
|
896
|
+
import { fileURLToPath } from "url";
|
|
897
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
898
|
+
function getPromptDir() {
|
|
899
|
+
const bundled = join2(__dirname, "prompts");
|
|
900
|
+
if (existsSync(bundled)) return bundled;
|
|
901
|
+
return __dirname;
|
|
902
|
+
}
|
|
903
|
+
var promptDir = getPromptDir();
|
|
904
|
+
function loadTemplate(name) {
|
|
905
|
+
return readFileSync(join2(promptDir, `${name}.md`), "utf-8");
|
|
906
|
+
}
|
|
907
|
+
function loadPartial(name) {
|
|
908
|
+
return loadTemplate(name).replace(/\s+$/, "");
|
|
909
|
+
}
|
|
910
|
+
var UNREPLACED_TOKEN_RE = /\{\{[A-Z_]+\}\}/g;
|
|
911
|
+
function composePrompt(template, substitutions) {
|
|
912
|
+
let result = template;
|
|
913
|
+
for (const [key, value] of Object.entries(substitutions)) {
|
|
914
|
+
result = result.replaceAll(`{{${key}}}`, value);
|
|
915
|
+
}
|
|
916
|
+
const remaining = result.match(UNREPLACED_TOKEN_RE);
|
|
917
|
+
if (remaining) {
|
|
918
|
+
throw new Error(`composePrompt: unreplaced placeholders: ${[...new Set(remaining)].join(", ")}`);
|
|
919
|
+
}
|
|
920
|
+
return result;
|
|
921
|
+
}
|
|
922
|
+
var CHECK_GATE_EXAMPLE = "Run the project's check gate \u2014 all pass (omit this step when the project has no check script)";
|
|
923
|
+
function buildPlanCommon(projectToolingSection) {
|
|
924
|
+
return composePrompt(loadPartial("plan-common"), {
|
|
925
|
+
PLAN_COMMON_EXAMPLES: loadPartial("plan-common-examples"),
|
|
926
|
+
PROJECT_TOOLING: projectToolingSection,
|
|
927
|
+
CHECK_GATE_EXAMPLE
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
function buildPlannerBase(projectToolingSection) {
|
|
931
|
+
return {
|
|
932
|
+
HARNESS_CONTEXT: loadPartial("harness-context"),
|
|
933
|
+
COMMON: buildPlanCommon(projectToolingSection),
|
|
934
|
+
VALIDATION: loadPartial("validation-checklist"),
|
|
935
|
+
SIGNALS: loadPartial("signals-planning"),
|
|
936
|
+
CHECK_GATE_EXAMPLE
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
function buildInteractivePrompt(context, outputFile, schema, projectToolingSection) {
|
|
940
|
+
return composePrompt(loadTemplate("plan-interactive"), {
|
|
941
|
+
...buildPlannerBase(projectToolingSection),
|
|
942
|
+
CONTEXT: context,
|
|
943
|
+
OUTPUT_FILE: outputFile,
|
|
944
|
+
SCHEMA: schema
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
function buildAutoPrompt(context, schema, projectToolingSection) {
|
|
948
|
+
return composePrompt(loadTemplate("plan-auto"), {
|
|
949
|
+
...buildPlannerBase(projectToolingSection),
|
|
950
|
+
CONTEXT: context,
|
|
951
|
+
SCHEMA: schema
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
function buildTaskExecutionPrompt(progressFilePath, noCommit, contextFileName, projectToolingSection = "") {
|
|
955
|
+
let template = loadTemplate("task-execution");
|
|
956
|
+
if (noCommit) {
|
|
957
|
+
template = template.replace(/^[ \t]*\{\{COMMIT_STEP\}\}\n/m, "\n");
|
|
958
|
+
template = template.replace(/^[ \t]*\{\{COMMIT_CONSTRAINT\}\}\n/m, "");
|
|
959
|
+
}
|
|
960
|
+
const commitStep = noCommit ? "" : " - **Before continuing:** Create a git commit with a descriptive message for the changes made.";
|
|
961
|
+
const commitConstraint = noCommit ? "" : "- **Must commit** \u2014 Create a git commit before signaling completion.";
|
|
962
|
+
return composePrompt(template, {
|
|
963
|
+
HARNESS_CONTEXT: loadPartial("harness-context"),
|
|
964
|
+
SIGNALS: loadPartial("signals-task"),
|
|
965
|
+
PROGRESS_FILE: progressFilePath,
|
|
966
|
+
COMMIT_STEP: commitStep,
|
|
967
|
+
COMMIT_CONSTRAINT: commitConstraint,
|
|
968
|
+
CONTEXT_FILE: contextFileName,
|
|
969
|
+
PROJECT_TOOLING: projectToolingSection
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
function buildTicketRefinePrompt(ticketContent, outputFile, schema, issueContext = "") {
|
|
973
|
+
const template = loadTemplate("ticket-refine");
|
|
974
|
+
const issueContextSection = issueContext ? `<context>
|
|
975
|
+
|
|
976
|
+
${issueContext}
|
|
977
|
+
|
|
978
|
+
</context>` : "";
|
|
979
|
+
return composePrompt(template, {
|
|
980
|
+
TICKET: ticketContent,
|
|
981
|
+
OUTPUT_FILE: outputFile,
|
|
982
|
+
SCHEMA: schema,
|
|
983
|
+
ISSUE_CONTEXT: issueContextSection
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
function buildIdeatePrompt(ideaTitle, ideaDescription, projectName, repositories, outputFile, schema, projectToolingSection) {
|
|
987
|
+
return composePrompt(loadTemplate("ideate"), {
|
|
988
|
+
...buildPlannerBase(projectToolingSection),
|
|
989
|
+
IDEA_TITLE: ideaTitle,
|
|
990
|
+
IDEA_DESCRIPTION: ideaDescription,
|
|
991
|
+
PROJECT_NAME: projectName,
|
|
992
|
+
REPOSITORIES: repositories,
|
|
993
|
+
OUTPUT_FILE: outputFile,
|
|
994
|
+
SCHEMA: schema
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
function buildIdeateAutoPrompt(ideaTitle, ideaDescription, projectName, repositories, schema, projectToolingSection) {
|
|
998
|
+
return composePrompt(loadTemplate("ideate-auto"), {
|
|
999
|
+
...buildPlannerBase(projectToolingSection),
|
|
1000
|
+
IDEA_TITLE: ideaTitle,
|
|
1001
|
+
IDEA_DESCRIPTION: ideaDescription,
|
|
1002
|
+
PROJECT_NAME: projectName,
|
|
1003
|
+
REPOSITORIES: repositories,
|
|
1004
|
+
SCHEMA: schema
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
function renderExtraDimensions(extras) {
|
|
1008
|
+
if (extras.length === 0) {
|
|
1009
|
+
return { section: "", passBar: "", assessment: "" };
|
|
1010
|
+
}
|
|
1011
|
+
const section = extras.map(
|
|
1012
|
+
(name) => `
|
|
1013
|
+
<dimension name="${name}" floor="false">
|
|
1014
|
+
Additional task-specific dimension flagged by the planner. Apply judgment to whether the implementation satisfies this dimension given the task's verification criteria and steps.
|
|
1015
|
+
</dimension>
|
|
1016
|
+
`
|
|
1017
|
+
).join("");
|
|
1018
|
+
const passBar = extras.map((name) => `
|
|
1019
|
+
- **${name}**: Task-specific dimension flagged by the planner`).join("");
|
|
1020
|
+
return {
|
|
1021
|
+
section,
|
|
1022
|
+
passBar,
|
|
1023
|
+
assessment: extras.map((name) => `
|
|
1024
|
+
**${name}**: PASS/FAIL \u2014 [one-line finding]`).join("")
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
function buildEvaluatorPrompt(ctx) {
|
|
1028
|
+
const template = loadTemplate("task-evaluation");
|
|
1029
|
+
const descriptionSection = ctx.taskDescription ? `
|
|
1030
|
+
**Description:** ${ctx.taskDescription}` : "";
|
|
1031
|
+
const stepsSection = ctx.taskSteps.length > 0 ? `
|
|
1032
|
+
**Implementation Steps:**
|
|
1033
|
+
${ctx.taskSteps.map((s) => `- ${s}`).join("\n")}` : "";
|
|
1034
|
+
const criteriaSection = ctx.verificationCriteria.length > 0 ? `
|
|
1035
|
+
**Verification Criteria:**
|
|
1036
|
+
${ctx.verificationCriteria.map((c) => `- ${c}`).join("\n")}` : "";
|
|
1037
|
+
const checkSection = ctx.checkScriptSection ? `
|
|
1038
|
+
|
|
1039
|
+
${ctx.checkScriptSection}` : "";
|
|
1040
|
+
const extras = renderExtraDimensions(ctx.extraDimensions);
|
|
1041
|
+
const extraAssessmentPass = extras.assessment.replace(/PASS\/FAIL/g, "PASS");
|
|
1042
|
+
return composePrompt(template, {
|
|
1043
|
+
HARNESS_CONTEXT: loadPartial("harness-context"),
|
|
1044
|
+
SIGNALS: loadPartial("signals-evaluation"),
|
|
1045
|
+
TASK_NAME: ctx.taskName,
|
|
1046
|
+
TASK_DESCRIPTION_SECTION: descriptionSection,
|
|
1047
|
+
TASK_STEPS_SECTION: stepsSection,
|
|
1048
|
+
VERIFICATION_CRITERIA_SECTION: criteriaSection,
|
|
1049
|
+
PROJECT_PATH: ctx.projectPath,
|
|
1050
|
+
CHECK_SCRIPT_SECTION: checkSection,
|
|
1051
|
+
PROJECT_TOOLING: ctx.projectToolingSection,
|
|
1052
|
+
EXTRA_DIMENSIONS_SECTION: extras.section,
|
|
1053
|
+
EXTRA_DIMENSIONS_PASS_BAR: extras.passBar,
|
|
1054
|
+
EXTRA_DIMENSIONS_ASSESSMENT_PASS: extraAssessmentPass,
|
|
1055
|
+
EXTRA_DIMENSIONS_ASSESSMENT_MIXED: extras.assessment
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
function buildSprintFeedbackPrompt(sprintName, completedTasks, feedback, branch) {
|
|
1059
|
+
const template = loadTemplate("sprint-feedback");
|
|
1060
|
+
const branchSection = branch ? `
|
|
1061
|
+
**Branch:** ${branch}
|
|
1062
|
+
` : "";
|
|
1063
|
+
return composePrompt(template, {
|
|
1064
|
+
HARNESS_CONTEXT: loadPartial("harness-context"),
|
|
1065
|
+
SIGNALS: loadPartial("signals-task"),
|
|
1066
|
+
SPRINT_NAME: sprintName,
|
|
1067
|
+
BRANCH_SECTION: branchSection,
|
|
1068
|
+
COMPLETED_TASKS: completedTasks,
|
|
1069
|
+
FEEDBACK: feedback
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
function buildCheckScriptDiscoverPrompt(repoPath) {
|
|
1073
|
+
return composePrompt(loadTemplate("check-script-discover"), {
|
|
1074
|
+
REPO_PATH: repoPath
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
function buildRepoOnboardPrompt(ctx) {
|
|
1078
|
+
const existingSection = ctx.existingAgentsMd ? `
|
|
1079
|
+
**Existing project context file:**
|
|
1080
|
+
|
|
1081
|
+
\`\`\`
|
|
1082
|
+
${ctx.existingAgentsMd}
|
|
1083
|
+
\`\`\`
|
|
1084
|
+
` : "";
|
|
1085
|
+
return composePrompt(loadTemplate("repo-onboard"), {
|
|
1086
|
+
REPO_PATH: ctx.repoPath,
|
|
1087
|
+
MODE: ctx.mode,
|
|
1088
|
+
EXISTING_AGENTS_MD: existingSection,
|
|
1089
|
+
PROJECT_TYPE: ctx.projectType || "unknown",
|
|
1090
|
+
CHECK_SCRIPT_SUGGESTION: ctx.checkScriptSuggestion,
|
|
1091
|
+
FILE_NAME: ctx.fileName
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
export {
|
|
1096
|
+
buildInteractivePrompt,
|
|
1097
|
+
buildAutoPrompt,
|
|
1098
|
+
buildTaskExecutionPrompt,
|
|
1099
|
+
buildTicketRefinePrompt,
|
|
1100
|
+
buildIdeatePrompt,
|
|
1101
|
+
buildIdeateAutoPrompt,
|
|
1102
|
+
buildEvaluatorPrompt,
|
|
1103
|
+
buildSprintFeedbackPrompt,
|
|
1104
|
+
buildCheckScriptDiscoverPrompt,
|
|
1105
|
+
buildRepoOnboardPrompt,
|
|
1106
|
+
processLifecycleAdapter,
|
|
1107
|
+
resolveProvider,
|
|
1108
|
+
providerDisplayName,
|
|
1109
|
+
getActiveProvider,
|
|
1110
|
+
spawnInteractive,
|
|
1111
|
+
enterAltScreen,
|
|
1112
|
+
exitAltScreen,
|
|
1113
|
+
registerTuiInstance,
|
|
1114
|
+
withSuspendedTui,
|
|
1115
|
+
ProviderAiSessionAdapter,
|
|
1116
|
+
SignalParser
|
|
1117
|
+
};
|