@glrs-dev/cli 2.3.0 → 2.4.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/CHANGELOG.md +2 -0
- package/dist/{chunk-EM4MJBOD.js → chunk-2AZKRWC6.js} +4 -4
- package/dist/{chunk-UXBOTMDY.js → chunk-2P3ETOT2.js} +2 -2
- package/dist/chunk-2VMFXAJH.js +795 -0
- package/dist/chunk-5ZVUFNCP.js +140 -0
- package/dist/{chunk-W37UX3U2.js → chunk-6Y27RQQL.js} +2 -2
- package/dist/{chunk-RZWOWTKF.js → chunk-EKNRKZWR.js} +4 -4
- package/dist/{chunk-YGNDPKIW.js → chunk-HQUCVJ4G.js} +3 -1
- package/dist/{chunk-OABVEBWW.js → chunk-MBEVC327.js} +1 -1
- package/dist/{chunk-MIWZLETC.js → chunk-MCM47HH4.js} +1 -1
- package/dist/{chunk-F3AFRUT2.js → chunk-PTIO556V.js} +2 -2
- package/dist/{chunk-E2UNZIZT.js → chunk-R2WXQ54P.js} +1 -1
- package/dist/{chunk-I2KUXY3I.js → chunk-SMDIOB5B.js} +2 -2
- package/dist/{chunk-SPULDN7P.js → chunk-YY7EWHMA.js} +5 -3
- package/dist/cli.js +31 -20
- package/dist/commands/autopilot-interactive.d.ts +89 -0
- package/dist/commands/autopilot-interactive.js +248 -0
- package/dist/commands/autopilot-raw.d.ts +1 -0
- package/dist/commands/autopilot-raw.js +368 -0
- package/dist/commands/autopilot-tui.d.ts +7 -0
- package/dist/commands/autopilot-tui.js +7 -0
- package/dist/commands/autopilot.d.ts +39 -0
- package/dist/commands/autopilot.js +395 -0
- package/dist/commands/cleanup.js +3 -3
- package/dist/commands/create.js +4 -4
- package/dist/commands/dashboard.d.ts +3 -0
- package/dist/commands/dashboard.js +1549 -0
- package/dist/commands/debrief.d.ts +57 -0
- package/dist/commands/debrief.js +9 -0
- package/dist/commands/delete.js +3 -3
- package/dist/commands/go.js +2 -2
- package/dist/commands/list.js +3 -3
- package/dist/commands/loop.d.ts +42 -0
- package/dist/commands/loop.js +133 -0
- package/dist/commands/plan-picker.d.ts +15 -0
- package/dist/commands/plan-picker.js +76 -0
- package/dist/commands/scoper.d.ts +54 -0
- package/dist/{vendor/harness-opencode/dist/scoper-S77SOK7X.js → commands/scoper.js} +30 -15
- package/dist/commands/switch.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/lib/auto-update.js +1 -1
- package/dist/lib/config.d.ts +3 -2
- package/dist/lib/config.js +1 -1
- package/dist/lib/registry.d.ts +2 -0
- package/dist/lib/registry.js +1 -1
- package/dist/lib/worktree.js +3 -3
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +7 -0
- package/dist/vendor/harness-opencode/dist/chunk-GILWWWMB.js +66 -0
- package/dist/vendor/harness-opencode/dist/cli.js +335 -639
- package/dist/vendor/harness-opencode/dist/index.js +35 -8
- package/dist/vendor/harness-opencode/dist/plugin-check-GJRD2OK6.js +14 -0
- package/dist/vendor/harness-opencode/package.json +1 -1
- package/package.json +8 -2
- package/dist/vendor/harness-opencode/dist/autopilot/prompt-template.md +0 -104
- package/dist/vendor/harness-opencode/dist/chunk-GCWHRUOK.js +0 -259
- package/dist/vendor/harness-opencode/dist/chunk-MJSMBY2Y.js +0 -87
- package/dist/vendor/harness-opencode/dist/chunk-NIFAVPNN.js +0 -544
- package/dist/vendor/harness-opencode/dist/loop-session-J35NILUZ.js +0 -30
- package/dist/vendor/harness-opencode/dist/opencode-server-KPCDFYAX.js +0 -22
- package/dist/vendor/harness-opencode/dist/plan-parser-TMHEKT22.js +0 -6
- package/dist/vendor/harness-opencode/dist/plan-session-7VS32P52.js +0 -117
|
@@ -1,544 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
parsePlanState
|
|
3
|
-
} from "./chunk-MJSMBY2Y.js";
|
|
4
|
-
import {
|
|
5
|
-
createSession,
|
|
6
|
-
getLastAssistantMessage,
|
|
7
|
-
getSessionCost,
|
|
8
|
-
sendAndWait,
|
|
9
|
-
startServer
|
|
10
|
-
} from "./chunk-GCWHRUOK.js";
|
|
11
|
-
|
|
12
|
-
// src/autopilot/loop.ts
|
|
13
|
-
import { execFile as execFileCb } from "child_process";
|
|
14
|
-
import { promisify } from "util";
|
|
15
|
-
import { readFileSync } from "fs";
|
|
16
|
-
import { join as join3 } from "path";
|
|
17
|
-
|
|
18
|
-
// src/lib/logger.ts
|
|
19
|
-
import pino from "pino";
|
|
20
|
-
import PinoPretty from "pino-pretty";
|
|
21
|
-
import { existsSync, mkdirSync } from "fs";
|
|
22
|
-
import { dirname, join } from "path";
|
|
23
|
-
function resolveStderrLevel() {
|
|
24
|
-
const env = process.env["GLRS_LOG_LEVEL"];
|
|
25
|
-
if (env && ["fatal", "error", "warn", "info", "debug", "trace", "silent"].includes(env)) {
|
|
26
|
-
return env;
|
|
27
|
-
}
|
|
28
|
-
return "info";
|
|
29
|
-
}
|
|
30
|
-
function shouldPrettyPrint() {
|
|
31
|
-
if (process.env["GLRS_LOG_FORMAT"] === "json") return false;
|
|
32
|
-
if (process.env["GLRS_LOG_FORMAT"] === "pretty") return true;
|
|
33
|
-
return process.stderr.isTTY ?? false;
|
|
34
|
-
}
|
|
35
|
-
function resolveLogFilePath(cwd) {
|
|
36
|
-
const env = process.env["GLRS_AUTOPILOT_LOG_FILE"];
|
|
37
|
-
if (env === "off") return null;
|
|
38
|
-
if (env) return env;
|
|
39
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
40
|
-
return join(cwd, ".agent", "autopilot-logs", `${timestamp}.log`);
|
|
41
|
-
}
|
|
42
|
-
function buildStderrStream(level) {
|
|
43
|
-
if (shouldPrettyPrint()) {
|
|
44
|
-
const pretty = PinoPretty({
|
|
45
|
-
colorize: true,
|
|
46
|
-
translateTime: "HH:MM:ss.l",
|
|
47
|
-
ignore: "pid,hostname,component",
|
|
48
|
-
messageFormat: "[{component}] {msg}",
|
|
49
|
-
destination: 2
|
|
50
|
-
// stderr
|
|
51
|
-
});
|
|
52
|
-
return { level, stream: pretty };
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
level,
|
|
56
|
-
stream: pino.destination({ fd: 2, sync: false })
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
function buildFileStream(cwd) {
|
|
60
|
-
const filePath = resolveLogFilePath(cwd);
|
|
61
|
-
if (!filePath) return null;
|
|
62
|
-
const parent = dirname(filePath);
|
|
63
|
-
if (!existsSync(parent)) {
|
|
64
|
-
mkdirSync(parent, { recursive: true });
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
path: filePath,
|
|
68
|
-
entry: {
|
|
69
|
-
level: "trace",
|
|
70
|
-
// sync: true gives deterministic flushSync semantics so the file
|
|
71
|
-
// log is safe to read immediately after flush() returns (critical
|
|
72
|
-
// for tests and for reliable postmortem after a crash). Autopilot
|
|
73
|
-
// runs are long-lived enough that sync writes aren't a bottleneck.
|
|
74
|
-
stream: pino.destination({ dest: filePath, sync: true, mkdir: true })
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
function createAutopilotLogger(opts) {
|
|
79
|
-
const stderrLevel = resolveStderrLevel();
|
|
80
|
-
const fileSink = buildFileStream(opts.cwd);
|
|
81
|
-
const streams = [buildStderrStream(stderrLevel)];
|
|
82
|
-
if (fileSink) streams.push(fileSink.entry);
|
|
83
|
-
const ms = pino.multistream(streams);
|
|
84
|
-
const root = pino(
|
|
85
|
-
{
|
|
86
|
-
level: "trace",
|
|
87
|
-
// router gate; individual streams apply their own levels
|
|
88
|
-
timestamp: pino.stdTimeFunctions.isoTime
|
|
89
|
-
},
|
|
90
|
-
ms
|
|
91
|
-
);
|
|
92
|
-
const flush = async () => {
|
|
93
|
-
try {
|
|
94
|
-
ms.flushSync();
|
|
95
|
-
} catch {
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
return {
|
|
99
|
-
root,
|
|
100
|
-
logFilePath: fileSink?.path ?? null,
|
|
101
|
-
flush
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
function childLogger(root, component) {
|
|
105
|
-
return root.child({ component });
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// src/autopilot/status.ts
|
|
109
|
-
function formatElapsed(ms) {
|
|
110
|
-
const totalSeconds = Math.floor(ms / 1e3);
|
|
111
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
112
|
-
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
113
|
-
const seconds = totalSeconds % 60;
|
|
114
|
-
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
|
|
115
|
-
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
116
|
-
return `${seconds}s`;
|
|
117
|
-
}
|
|
118
|
-
function formatCost(usd) {
|
|
119
|
-
if (usd === 0) return "$0.00";
|
|
120
|
-
return `$${usd.toFixed(3)}`;
|
|
121
|
-
}
|
|
122
|
-
function composeStatusMessage(state, now) {
|
|
123
|
-
const elapsed = formatElapsed(now - state.startedAt);
|
|
124
|
-
const cost = formatCost(state.cumulativeCostUsd);
|
|
125
|
-
const iterNote = state.iterationsCompleted === 0 ? "iteration 1 in flight" : `${state.iterationsCompleted} iteration${state.iterationsCompleted === 1 ? "" : "s"} complete`;
|
|
126
|
-
let planNote = "";
|
|
127
|
-
if (state.phaseCount !== void 0 && state.phasesCompleted !== void 0 && state.mainCheckboxesTotal !== void 0 && state.mainCheckboxesCompleted !== void 0) {
|
|
128
|
-
planNote = `, phase ${state.phasesCompleted}/${state.phaseCount}, ${state.mainCheckboxesCompleted}/${state.mainCheckboxesTotal} boxes`;
|
|
129
|
-
}
|
|
130
|
-
if (state.lastIterationErrored) {
|
|
131
|
-
return `working (${iterNote}, ${elapsed} elapsed, ${cost} used${planNote}) \u2014 last iteration errored`;
|
|
132
|
-
}
|
|
133
|
-
return `working (${iterNote}, ${elapsed} elapsed, ${cost} used${planNote})`;
|
|
134
|
-
}
|
|
135
|
-
function createStatusHeartbeat(opts) {
|
|
136
|
-
const now = opts._deps?.now ?? (() => Date.now());
|
|
137
|
-
const setIntervalFn = opts._deps?.setInterval ?? setInterval;
|
|
138
|
-
const clearIntervalFn = opts._deps?.clearInterval ?? clearInterval;
|
|
139
|
-
const state = {
|
|
140
|
-
startedAt: now(),
|
|
141
|
-
iterationsCompleted: 0,
|
|
142
|
-
cumulativeCostUsd: 0,
|
|
143
|
-
lastIterationProgress: false,
|
|
144
|
-
lastIterationErrored: false
|
|
145
|
-
};
|
|
146
|
-
let timerId = null;
|
|
147
|
-
const tick = () => {
|
|
148
|
-
if (opts.pollCost) {
|
|
149
|
-
opts.pollCost().then((cost) => {
|
|
150
|
-
if (cost > 0) state.cumulativeCostUsd = cost;
|
|
151
|
-
}).catch(() => {
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
const message = composeStatusMessage(state, now());
|
|
155
|
-
opts.logger.info(
|
|
156
|
-
{
|
|
157
|
-
elapsedMs: now() - state.startedAt,
|
|
158
|
-
iterationsCompleted: state.iterationsCompleted,
|
|
159
|
-
cumulativeCostUsd: state.cumulativeCostUsd,
|
|
160
|
-
lastIterationProgress: state.lastIterationProgress,
|
|
161
|
-
lastIterationErrored: state.lastIterationErrored
|
|
162
|
-
},
|
|
163
|
-
message
|
|
164
|
-
);
|
|
165
|
-
};
|
|
166
|
-
return {
|
|
167
|
-
start() {
|
|
168
|
-
if (timerId !== null) return;
|
|
169
|
-
timerId = setIntervalFn(tick, opts.intervalMs);
|
|
170
|
-
},
|
|
171
|
-
stop() {
|
|
172
|
-
if (timerId === null) return;
|
|
173
|
-
clearIntervalFn(timerId);
|
|
174
|
-
timerId = null;
|
|
175
|
-
},
|
|
176
|
-
update(patch) {
|
|
177
|
-
Object.assign(state, patch);
|
|
178
|
-
},
|
|
179
|
-
getState() {
|
|
180
|
-
return { ...state };
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// src/autopilot/config.ts
|
|
186
|
-
var MAX_ITERATIONS = 50;
|
|
187
|
-
var STRUGGLE_THRESHOLD = 3;
|
|
188
|
-
var TIMEOUT_MS = 4 * 60 * 60 * 1e3;
|
|
189
|
-
var STALL_MS = 60 * 60 * 1e3;
|
|
190
|
-
var KILL_SWITCH_PATH = ".agent/autopilot-disable";
|
|
191
|
-
var SENTINEL_TAG = "<autopilot-done>";
|
|
192
|
-
var STATUS_INTERVAL_MS = (() => {
|
|
193
|
-
const env = process.env["GLRS_AUTOPILOT_STATUS_INTERVAL_MS"];
|
|
194
|
-
if (!env) return 5 * 60 * 1e3;
|
|
195
|
-
const n = Number.parseInt(env, 10);
|
|
196
|
-
if (Number.isNaN(n) || n < 1e3 || n > 60 * 60 * 1e3) return 5 * 60 * 1e3;
|
|
197
|
-
return n;
|
|
198
|
-
})();
|
|
199
|
-
|
|
200
|
-
// src/autopilot/sentinel.ts
|
|
201
|
-
function detectSentinel(text) {
|
|
202
|
-
if (!text.includes(SENTINEL_TAG)) {
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
const withoutFences = text.replace(/```[\s\S]*?```/g, "");
|
|
206
|
-
const withoutInline = withoutFences.replace(/`[^`\n]*`/g, "");
|
|
207
|
-
return withoutInline.includes(SENTINEL_TAG);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// src/autopilot/struggle.ts
|
|
211
|
-
import * as fs from "fs";
|
|
212
|
-
import * as path from "path";
|
|
213
|
-
var StruggleDetector = class {
|
|
214
|
-
_consecutiveStalls = 0;
|
|
215
|
-
_threshold;
|
|
216
|
-
constructor(threshold) {
|
|
217
|
-
this._threshold = threshold;
|
|
218
|
-
}
|
|
219
|
-
/** Number of consecutive stall iterations recorded so far. */
|
|
220
|
-
get consecutiveStalls() {
|
|
221
|
-
return this._consecutiveStalls;
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Record the result of one iteration.
|
|
225
|
-
* @param madeProgress - true if the agent made filesystem changes this iteration.
|
|
226
|
-
*/
|
|
227
|
-
record(madeProgress) {
|
|
228
|
-
if (madeProgress) {
|
|
229
|
-
this._consecutiveStalls = 0;
|
|
230
|
-
} else {
|
|
231
|
-
this._consecutiveStalls++;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Returns true if the agent has stalled for `threshold` consecutive
|
|
236
|
-
* iterations without making progress.
|
|
237
|
-
*/
|
|
238
|
-
isStruggling() {
|
|
239
|
-
return this._consecutiveStalls >= this._threshold;
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
function checkKillSwitch(cwd) {
|
|
243
|
-
const killSwitchFile = path.join(cwd, KILL_SWITCH_PATH);
|
|
244
|
-
return fs.existsSync(killSwitchFile);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// src/autopilot/loop.ts
|
|
248
|
-
var execFile = promisify(execFileCb);
|
|
249
|
-
function buildFullPrompt(userPrompt) {
|
|
250
|
-
const candidates = [
|
|
251
|
-
join3(import.meta.dir, "prompt-template.md"),
|
|
252
|
-
join3(import.meta.dir, "..", "..", "src", "autopilot", "prompt-template.md")
|
|
253
|
-
];
|
|
254
|
-
let template = "";
|
|
255
|
-
for (const candidate of candidates) {
|
|
256
|
-
try {
|
|
257
|
-
const raw = readFileSync(candidate, "utf8");
|
|
258
|
-
template = raw.replace(/^---\n[\s\S]*?\n---\n/, "");
|
|
259
|
-
break;
|
|
260
|
-
} catch {
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
const withArgs = template.replace("$ARGUMENTS", userPrompt);
|
|
264
|
-
return withArgs || userPrompt;
|
|
265
|
-
}
|
|
266
|
-
async function checkProgress(cwd, baseRef) {
|
|
267
|
-
try {
|
|
268
|
-
const { stdout } = await execFile("git", ["diff", "--stat", baseRef], { cwd });
|
|
269
|
-
return stdout.trim().length > 0;
|
|
270
|
-
} catch {
|
|
271
|
-
return true;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
async function getHeadSha(cwd) {
|
|
275
|
-
try {
|
|
276
|
-
const { stdout } = await execFile("git", ["rev-parse", "HEAD"], { cwd });
|
|
277
|
-
return stdout.trim();
|
|
278
|
-
} catch {
|
|
279
|
-
return "HEAD";
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
async function runRalphLoop(opts) {
|
|
283
|
-
const maxIterations = opts.maxIterations ?? MAX_ITERATIONS;
|
|
284
|
-
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
285
|
-
const stallMs = opts.stallMs ?? STALL_MS;
|
|
286
|
-
const struggleThreshold = opts.struggleThreshold ?? STRUGGLE_THRESHOLD;
|
|
287
|
-
const _startServer = opts._deps?.startServer ?? startServer;
|
|
288
|
-
const _createSession = opts._deps?.createSession ?? createSession;
|
|
289
|
-
const _sendAndWait = opts._deps?.sendAndWait ?? sendAndWait;
|
|
290
|
-
const _getLastAssistantMessage = opts._deps?.getLastAssistantMessage ?? getLastAssistantMessage;
|
|
291
|
-
const fullPrompt = buildFullPrompt(opts.prompt);
|
|
292
|
-
const struggle = new StruggleDetector(struggleThreshold);
|
|
293
|
-
const startTime = Date.now();
|
|
294
|
-
const autopilotLog = createAutopilotLogger({ cwd: opts.cwd });
|
|
295
|
-
const log = childLogger(autopilotLog.root, "autopilot.loop");
|
|
296
|
-
const toolLog = childLogger(autopilotLog.root, "autopilot.tool");
|
|
297
|
-
const streamLog = childLogger(autopilotLog.root, "autopilot.stream");
|
|
298
|
-
const statusLog = childLogger(autopilotLog.root, "autopilot.status");
|
|
299
|
-
let heartbeat = null;
|
|
300
|
-
if (autopilotLog.logFilePath) {
|
|
301
|
-
log.info({ file: autopilotLog.logFilePath }, `Logging to ${autopilotLog.logFilePath}`);
|
|
302
|
-
}
|
|
303
|
-
log.info({ cwd: opts.cwd, maxIterations, timeoutMs }, "Starting OpenCode server");
|
|
304
|
-
const server = await _startServer({ cwd: opts.cwd });
|
|
305
|
-
log.info({ url: server.url }, "Server ready");
|
|
306
|
-
const abort = new AbortController();
|
|
307
|
-
const timeoutHandle = setTimeout(() => {
|
|
308
|
-
abort.abort();
|
|
309
|
-
}, timeoutMs);
|
|
310
|
-
let sessionId;
|
|
311
|
-
try {
|
|
312
|
-
sessionId = await _createSession(server.client, {
|
|
313
|
-
cwd: opts.cwd,
|
|
314
|
-
agentName: "autopilot-prime"
|
|
315
|
-
});
|
|
316
|
-
log.info({ sessionId }, "Session created with autopilot-prime");
|
|
317
|
-
heartbeat = createStatusHeartbeat({
|
|
318
|
-
logger: statusLog,
|
|
319
|
-
intervalMs: STATUS_INTERVAL_MS,
|
|
320
|
-
pollCost: async () => getSessionCost(server.client, sessionId)
|
|
321
|
-
});
|
|
322
|
-
heartbeat.start();
|
|
323
|
-
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
324
|
-
if (checkKillSwitch(opts.cwd)) {
|
|
325
|
-
log.warn({ iteration: iteration - 1 }, "Kill switch active \u2014 stopping");
|
|
326
|
-
return {
|
|
327
|
-
exitReason: "kill-switch",
|
|
328
|
-
iterations: iteration - 1,
|
|
329
|
-
message: `Kill switch active (.agent/autopilot-disable exists). Stopping after ${iteration - 1} iteration(s).`,
|
|
330
|
-
sessionId
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
if (Date.now() - startTime >= timeoutMs) {
|
|
334
|
-
log.warn({ iteration: iteration - 1, timeoutMs }, "Total timeout exceeded");
|
|
335
|
-
return {
|
|
336
|
-
exitReason: "timeout",
|
|
337
|
-
iterations: iteration - 1,
|
|
338
|
-
message: `Total timeout (${timeoutMs}ms) exceeded after ${iteration - 1} iteration(s).`,
|
|
339
|
-
sessionId
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
const headBefore = await getHeadSha(opts.cwd);
|
|
343
|
-
const iterationBaseCost = heartbeat.getState().cumulativeCostUsd;
|
|
344
|
-
const iterStart = Date.now();
|
|
345
|
-
log.debug({ iteration, maxIterations }, `Iteration ${iteration}/${maxIterations} \u2014 sending prompt`);
|
|
346
|
-
let streamDeltaCount = 0;
|
|
347
|
-
let streamCharCount = 0;
|
|
348
|
-
let lastStreamLogAt = 0;
|
|
349
|
-
let lastToolOrStreamLogAt = Date.now();
|
|
350
|
-
const DEBUG_STREAM_INTERVAL_MS = 15e3;
|
|
351
|
-
const INFO_STREAM_INTERVAL_MS = 6e4;
|
|
352
|
-
const result = await _sendAndWait(server.client, {
|
|
353
|
-
sessionId,
|
|
354
|
-
message: fullPrompt,
|
|
355
|
-
stallMs,
|
|
356
|
-
abortSignal: abort.signal,
|
|
357
|
-
// Autopilot is lights-out: auto-reject every permission prompt
|
|
358
|
-
// (question tool, edit gates, bash confirmations) so the agent
|
|
359
|
-
// can't deadlock waiting on a human response.
|
|
360
|
-
autoRejectPermissions: true,
|
|
361
|
-
serverUrl: server.url,
|
|
362
|
-
onPermissionRejected: (perm) => {
|
|
363
|
-
log.warn(
|
|
364
|
-
{ iteration, permissionId: perm.id, permissionType: perm.type, title: perm.title },
|
|
365
|
-
`Auto-rejected permission: ${perm.type} \u2014 "${perm.title}"`
|
|
366
|
-
);
|
|
367
|
-
},
|
|
368
|
-
onToolCall: (toolName) => {
|
|
369
|
-
toolLog.debug({ iteration, tool: toolName }, toolName);
|
|
370
|
-
lastToolOrStreamLogAt = Date.now();
|
|
371
|
-
streamDeltaCount = 0;
|
|
372
|
-
streamCharCount = 0;
|
|
373
|
-
lastStreamLogAt = Date.now();
|
|
374
|
-
},
|
|
375
|
-
onCostUpdate: (cost, tokens) => {
|
|
376
|
-
heartbeat.update({
|
|
377
|
-
cumulativeCostUsd: iterationBaseCost + cost
|
|
378
|
-
});
|
|
379
|
-
},
|
|
380
|
-
onTextDelta: (charCount) => {
|
|
381
|
-
streamDeltaCount += 1;
|
|
382
|
-
streamCharCount += charCount;
|
|
383
|
-
const now = Date.now();
|
|
384
|
-
if (now - lastStreamLogAt >= DEBUG_STREAM_INTERVAL_MS) {
|
|
385
|
-
streamLog.debug(
|
|
386
|
-
{ iteration, deltas: streamDeltaCount, chars: streamCharCount },
|
|
387
|
-
`streaming (${streamDeltaCount} deltas, ${streamCharCount} chars)`
|
|
388
|
-
);
|
|
389
|
-
lastStreamLogAt = now;
|
|
390
|
-
}
|
|
391
|
-
const silenceSinceLastTool = now - lastToolOrStreamLogAt;
|
|
392
|
-
if (silenceSinceLastTool >= INFO_STREAM_INTERVAL_MS) {
|
|
393
|
-
streamLog.info(
|
|
394
|
-
{
|
|
395
|
-
iteration,
|
|
396
|
-
deltas: streamDeltaCount,
|
|
397
|
-
chars: streamCharCount,
|
|
398
|
-
silenceMs: silenceSinceLastTool
|
|
399
|
-
},
|
|
400
|
-
`still streaming (${streamDeltaCount} deltas, ${streamCharCount} chars, ${Math.round(silenceSinceLastTool / 1e3)}s since last tool)`
|
|
401
|
-
);
|
|
402
|
-
lastToolOrStreamLogAt = now;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
const iterDurationMs = Date.now() - iterStart;
|
|
407
|
-
if (result.kind === "abort") {
|
|
408
|
-
log.warn({ iteration, iterDurationMs }, "Iteration aborted (total timeout)");
|
|
409
|
-
return {
|
|
410
|
-
exitReason: "timeout",
|
|
411
|
-
iterations: iteration,
|
|
412
|
-
message: `Aborted after ${iteration} iteration(s) (total timeout exceeded).`,
|
|
413
|
-
sessionId
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
if (result.kind === "stall") {
|
|
417
|
-
log.warn({ iteration, stallMs: result.stallMs }, "Iteration stalled");
|
|
418
|
-
return {
|
|
419
|
-
exitReason: "stall",
|
|
420
|
-
iterations: iteration,
|
|
421
|
-
message: `Iteration ${iteration} stalled for ${result.stallMs}ms with no idle signal.`,
|
|
422
|
-
sessionId
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
if (result.kind === "error") {
|
|
426
|
-
log.error({ iteration, err: result.message }, "Iteration errored");
|
|
427
|
-
heartbeat.update({
|
|
428
|
-
iterationsCompleted: iteration,
|
|
429
|
-
lastIterationErrored: true
|
|
430
|
-
});
|
|
431
|
-
return {
|
|
432
|
-
exitReason: "error",
|
|
433
|
-
iterations: iteration,
|
|
434
|
-
message: `Error in iteration ${iteration}: ${result.message}`,
|
|
435
|
-
sessionId
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
if (result.kind === "question_rejected") {
|
|
439
|
-
log.warn(
|
|
440
|
-
{ iteration, questionTitle: result.title },
|
|
441
|
-
`Question rejected \u2014 re-sending prompt with reminder (iteration ${iteration})`
|
|
442
|
-
);
|
|
443
|
-
const reminderResult = await _sendAndWait(server.client, {
|
|
444
|
-
sessionId,
|
|
445
|
-
message: fullPrompt + "\n\nIMPORTANT: Your previous attempt to use the question tool was rejected. The question tool is not available in autopilot mode. You must solve this without asking the user. Pick a sensible default, document the decision in the plan's ## Open questions, and continue working.",
|
|
446
|
-
stallMs,
|
|
447
|
-
abortSignal: abort.signal,
|
|
448
|
-
autoRejectPermissions: true,
|
|
449
|
-
serverUrl: server.url,
|
|
450
|
-
onPermissionRejected: (perm) => {
|
|
451
|
-
log.warn(
|
|
452
|
-
{ iteration, permissionId: perm.id, permissionType: perm.type },
|
|
453
|
-
`Auto-rejected permission on retry: ${perm.type}`
|
|
454
|
-
);
|
|
455
|
-
},
|
|
456
|
-
onToolCall: (toolName) => {
|
|
457
|
-
toolLog.debug({ iteration, tool: toolName }, toolName);
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
if (reminderResult.kind === "question_rejected") {
|
|
461
|
-
log.error(
|
|
462
|
-
{ iteration },
|
|
463
|
-
"Agent invoked question tool twice in same iteration \u2014 giving up on this iteration"
|
|
464
|
-
);
|
|
465
|
-
} else if (reminderResult.kind !== "idle") {
|
|
466
|
-
log.warn(
|
|
467
|
-
{ iteration, kind: reminderResult.kind },
|
|
468
|
-
`Retry after question rejection returned ${reminderResult.kind}`
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
const lastMessage = await _getLastAssistantMessage(server.client, sessionId);
|
|
473
|
-
if (detectSentinel(lastMessage)) {
|
|
474
|
-
log.info({ iteration, iterDurationMs }, "Sentinel detected \u2014 autopilot done");
|
|
475
|
-
return {
|
|
476
|
-
exitReason: "sentinel",
|
|
477
|
-
iterations: iteration,
|
|
478
|
-
message: `Agent emitted <autopilot-done> at iteration ${iteration}.`,
|
|
479
|
-
sessionId
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
const madeProgress = await checkProgress(opts.cwd, headBefore);
|
|
483
|
-
struggle.record(madeProgress);
|
|
484
|
-
const cumulativeCostUsd = await getSessionCost(server.client, sessionId);
|
|
485
|
-
const planPathMatch = opts.prompt.match(/plans\/([^/\s]+(?:\/[^/\s]+)?)/);
|
|
486
|
-
let planProgressPatch = {};
|
|
487
|
-
if (planPathMatch) {
|
|
488
|
-
try {
|
|
489
|
-
const planPath = planPathMatch[0];
|
|
490
|
-
const planState = parsePlanState(planPath);
|
|
491
|
-
if (planState.type === "multi") {
|
|
492
|
-
planProgressPatch = {
|
|
493
|
-
phaseCount: planState.phaseCount,
|
|
494
|
-
phasesCompleted: planState.phasesCompleted,
|
|
495
|
-
mainCheckboxesTotal: planState.totalItems,
|
|
496
|
-
mainCheckboxesCompleted: planState.checkedItems
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
} catch (err) {
|
|
500
|
-
log.debug({ err }, "plan-parser error \u2014 falling back to plan-blind heartbeat");
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
heartbeat.update({
|
|
504
|
-
iterationsCompleted: iteration,
|
|
505
|
-
cumulativeCostUsd,
|
|
506
|
-
lastIterationProgress: madeProgress,
|
|
507
|
-
lastIterationErrored: false,
|
|
508
|
-
...planProgressPatch
|
|
509
|
-
});
|
|
510
|
-
log.debug(
|
|
511
|
-
{ iteration, iterDurationMs, madeProgress, cumulativeCostUsd },
|
|
512
|
-
`Iteration ${iteration} idle (${(iterDurationMs / 1e3).toFixed(1)}s, ${madeProgress ? "progress" : "no progress"})`
|
|
513
|
-
);
|
|
514
|
-
if (struggle.isStruggling()) {
|
|
515
|
-
log.warn({ iteration, struggleThreshold }, "Struggle detected \u2014 stopping");
|
|
516
|
-
return {
|
|
517
|
-
exitReason: "struggle",
|
|
518
|
-
iterations: iteration,
|
|
519
|
-
message: `Agent made no filesystem progress for ${struggleThreshold} consecutive iteration(s). Stopping at iteration ${iteration}.`,
|
|
520
|
-
sessionId
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
log.warn({ maxIterations }, "Reached max iterations");
|
|
525
|
-
return {
|
|
526
|
-
exitReason: "max-iterations",
|
|
527
|
-
iterations: maxIterations,
|
|
528
|
-
message: `Reached maximum iterations (${maxIterations}). Stopping.`,
|
|
529
|
-
sessionId
|
|
530
|
-
};
|
|
531
|
-
} finally {
|
|
532
|
-
clearTimeout(timeoutHandle);
|
|
533
|
-
heartbeat?.stop();
|
|
534
|
-
log.info({}, "Shutting down server");
|
|
535
|
-
await server.shutdown();
|
|
536
|
-
await autopilotLog.flush();
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
export {
|
|
541
|
-
MAX_ITERATIONS,
|
|
542
|
-
TIMEOUT_MS,
|
|
543
|
-
runRalphLoop
|
|
544
|
-
};
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
runRalphLoop
|
|
3
|
-
} from "./chunk-NIFAVPNN.js";
|
|
4
|
-
import "./chunk-MJSMBY2Y.js";
|
|
5
|
-
import "./chunk-GCWHRUOK.js";
|
|
6
|
-
|
|
7
|
-
// src/autopilot/loop-session.ts
|
|
8
|
-
import * as fs from "fs";
|
|
9
|
-
import * as path from "path";
|
|
10
|
-
async function runLoopSession(opts) {
|
|
11
|
-
const _runRalphLoop = opts._deps?.runRalphLoop ?? runRalphLoop;
|
|
12
|
-
const isDirectory = opts._deps?.isDirectory ? opts._deps.isDirectory(opts.planPath) : (() => {
|
|
13
|
-
try {
|
|
14
|
-
return fs.statSync(opts.planPath).isDirectory();
|
|
15
|
-
} catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
})();
|
|
19
|
-
let prompt;
|
|
20
|
-
if (isDirectory) {
|
|
21
|
-
const mainMd = path.join(opts.planPath, "main.md");
|
|
22
|
-
prompt = `Work the plan at ${mainMd}. Find the first unchecked phase in ## Phases and complete all its items. Continue until all phases are checked and all main.md items are checked. Mark items done as they complete.`;
|
|
23
|
-
} else {
|
|
24
|
-
prompt = `Work the plan at ${opts.planPath}. Complete all items in ## Acceptance criteria. Mark items done as they complete.`;
|
|
25
|
-
}
|
|
26
|
-
return _runRalphLoop({ prompt, cwd: opts.cwd });
|
|
27
|
-
}
|
|
28
|
-
export {
|
|
29
|
-
runLoopSession
|
|
30
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_STARTUP_TIMEOUT_MS,
|
|
3
|
-
createSession,
|
|
4
|
-
execFileP,
|
|
5
|
-
getLastAssistantMessage,
|
|
6
|
-
getSessionCost,
|
|
7
|
-
selfTest,
|
|
8
|
-
sendAndWait,
|
|
9
|
-
startServer,
|
|
10
|
-
waitForIdle
|
|
11
|
-
} from "./chunk-GCWHRUOK.js";
|
|
12
|
-
export {
|
|
13
|
-
DEFAULT_STARTUP_TIMEOUT_MS,
|
|
14
|
-
createSession,
|
|
15
|
-
execFileP,
|
|
16
|
-
getLastAssistantMessage,
|
|
17
|
-
getSessionCost,
|
|
18
|
-
selfTest,
|
|
19
|
-
sendAndWait,
|
|
20
|
-
startServer,
|
|
21
|
-
waitForIdle
|
|
22
|
-
};
|