@glrs-dev/harness-plugin-opencode 2.2.0 → 2.3.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 +63 -0
- package/README.md +7 -6
- package/SECURITY.md +1 -1
- package/dist/agents/prompts/build.md +16 -0
- package/dist/agents/prompts/code-reviewer-thorough.md +6 -7
- package/dist/agents/prompts/debriefer.md +55 -0
- package/dist/agents/prompts/plan-reviewer.md +2 -1
- package/dist/agents/prompts/plan.md +97 -7
- package/dist/agents/prompts/prime.md +4 -2
- package/dist/agents/prompts/scoper.md +129 -0
- package/dist/agents/prompts/spec-reviewer.md +0 -1
- package/dist/agents/prompts/spec-reviewer.open.md +0 -1
- package/dist/autopilot/prompt-template.md +69 -45
- package/dist/chunk-GCWHRUOK.js +259 -0
- package/dist/chunk-MJSMBY2Y.js +87 -0
- package/dist/chunk-NIFAVPNN.js +544 -0
- package/dist/cli.js +448 -503
- package/dist/index.js +90 -14
- package/dist/loop-session-J35NILUZ.js +30 -0
- package/dist/opencode-server-KPCDFYAX.js +22 -0
- package/dist/plan-parser-TMHEKT22.js +6 -0
- package/dist/plan-session-7VS32P52.js +117 -0
- package/dist/scoper-S77SOK7X.js +326 -0
- package/dist/skills/spear-protocol/SKILL.md +2 -1
- package/package.json +3 -1
- package/dist/bin/plan-check.sh +0 -255
package/dist/cli.js
CHANGED
|
@@ -6,17 +6,24 @@ import {
|
|
|
6
6
|
refreshPluginCache,
|
|
7
7
|
validateModelOverride
|
|
8
8
|
} from "./chunk-PDMXYZM4.js";
|
|
9
|
+
import {
|
|
10
|
+
MAX_ITERATIONS,
|
|
11
|
+
TIMEOUT_MS,
|
|
12
|
+
runRalphLoop
|
|
13
|
+
} from "./chunk-NIFAVPNN.js";
|
|
14
|
+
import "./chunk-MJSMBY2Y.js";
|
|
15
|
+
import {
|
|
16
|
+
createSession,
|
|
17
|
+
getLastAssistantMessage,
|
|
18
|
+
sendAndWait
|
|
19
|
+
} from "./chunk-GCWHRUOK.js";
|
|
9
20
|
|
|
10
21
|
// src/cli.ts
|
|
11
22
|
import {
|
|
12
23
|
binary,
|
|
13
|
-
command as
|
|
14
|
-
flag,
|
|
15
|
-
option as option2,
|
|
16
|
-
optional as optional2,
|
|
24
|
+
command as command3,
|
|
25
|
+
flag as flag2,
|
|
17
26
|
positional as positional2,
|
|
18
|
-
restPositionals,
|
|
19
|
-
string,
|
|
20
27
|
subcommands,
|
|
21
28
|
run
|
|
22
29
|
} from "cmd-ts";
|
|
@@ -1093,9 +1100,9 @@ function getOpencodeConfigPath3() {
|
|
|
1093
1100
|
const configHome = process.env["XDG_CONFIG_HOME"] ?? path5.join(os4.homedir(), ".config");
|
|
1094
1101
|
return path5.join(configHome, "opencode", "opencode.json");
|
|
1095
1102
|
}
|
|
1096
|
-
function cmd(
|
|
1103
|
+
function cmd(command4) {
|
|
1097
1104
|
try {
|
|
1098
|
-
return execSync(
|
|
1105
|
+
return execSync(command4, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1099
1106
|
} catch {
|
|
1100
1107
|
return null;
|
|
1101
1108
|
}
|
|
@@ -1216,12 +1223,6 @@ ${c3.bold}Doctor \u2014 ${PLUGIN_NAME3}${c3.reset}
|
|
|
1216
1223
|
} else {
|
|
1217
1224
|
warn2("node/npx not found \u2014 memory MCP won't work");
|
|
1218
1225
|
}
|
|
1219
|
-
const planCheckResult = cmd(`bunx ${PLUGIN_NAME3} plan-check --help 2>/dev/null`);
|
|
1220
|
-
if (planCheckResult !== null) {
|
|
1221
|
-
ok2("plan-check CLI invokable");
|
|
1222
|
-
} else {
|
|
1223
|
-
warn2("plan-check CLI not invokable \u2014 try: bun install");
|
|
1224
|
-
}
|
|
1225
1226
|
if (which("bun")) {
|
|
1226
1227
|
ok2(`bun ${cmd("bun --version") ?? ""}`);
|
|
1227
1228
|
} else if (which("npm")) {
|
|
@@ -1232,41 +1233,168 @@ ${c3.bold}Doctor \u2014 ${PLUGIN_NAME3}${c3.reset}
|
|
|
1232
1233
|
console.log();
|
|
1233
1234
|
}
|
|
1234
1235
|
|
|
1235
|
-
// src/
|
|
1236
|
-
import {
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
function
|
|
1240
|
-
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1236
|
+
// src/autopilot/cli.ts
|
|
1237
|
+
import { command, option, positional, string as stringType, optional, number as numberType, flag } from "cmd-ts";
|
|
1238
|
+
|
|
1239
|
+
// src/autopilot/debrief.ts
|
|
1240
|
+
function shouldRunDebrief(opts) {
|
|
1241
|
+
if (opts.noDebrief) return false;
|
|
1242
|
+
const envVal = opts.env["GLRS_AUTOPILOT_DEBRIEF"];
|
|
1243
|
+
if (envVal !== void 0 && envVal.toLowerCase() === "off") return false;
|
|
1244
|
+
return true;
|
|
1245
|
+
}
|
|
1246
|
+
async function defaultExecGitDiffStat(cwd) {
|
|
1247
|
+
const { execFile: execFileCb } = await import("child_process");
|
|
1248
|
+
const { promisify } = await import("util");
|
|
1249
|
+
const execFile2 = promisify(execFileCb);
|
|
1250
|
+
try {
|
|
1251
|
+
const { stdout } = await execFile2("git", ["diff", "--stat", "HEAD~1", "HEAD"], { cwd });
|
|
1252
|
+
return stdout.trim();
|
|
1253
|
+
} catch {
|
|
1249
1254
|
try {
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
break;
|
|
1255
|
+
const { stdout } = await execFile2("git", ["diff", "--stat"], { cwd });
|
|
1256
|
+
return stdout.trim() || "(no uncommitted changes)";
|
|
1253
1257
|
} catch {
|
|
1258
|
+
return "(git diff unavailable)";
|
|
1254
1259
|
}
|
|
1255
1260
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1261
|
+
}
|
|
1262
|
+
function buildContextMessage(loopResult, prompt, gitDiffStat) {
|
|
1263
|
+
const cost = loopResult.cumulativeCostUsd !== void 0 ? `$${loopResult.cumulativeCostUsd.toFixed(4)}` : "not available";
|
|
1264
|
+
const sessionId = loopResult.sessionId ?? "not available";
|
|
1265
|
+
return [
|
|
1266
|
+
"## Autopilot session context",
|
|
1267
|
+
"",
|
|
1268
|
+
`**Exit reason:** ${loopResult.exitReason}`,
|
|
1269
|
+
`**Iterations completed:** ${loopResult.iterations}`,
|
|
1270
|
+
`**Exit message:** ${loopResult.message}`,
|
|
1271
|
+
`**Cumulative cost:** ${cost}`,
|
|
1272
|
+
`**Session ID:** ${sessionId}`,
|
|
1273
|
+
"",
|
|
1274
|
+
"## Original prompt",
|
|
1275
|
+
"",
|
|
1276
|
+
prompt,
|
|
1277
|
+
"",
|
|
1278
|
+
"## Git diff stat (last commit vs HEAD~1)",
|
|
1279
|
+
"",
|
|
1280
|
+
gitDiffStat || "(no changes)",
|
|
1281
|
+
"",
|
|
1282
|
+
"---",
|
|
1283
|
+
"",
|
|
1284
|
+
"Please produce the five-section debrief as instructed in your system prompt."
|
|
1285
|
+
].join("\n");
|
|
1286
|
+
}
|
|
1287
|
+
async function runDebrief(opts) {
|
|
1288
|
+
const _createSession = opts._deps?.createSession ?? createSession;
|
|
1289
|
+
const _sendAndWait = opts._deps?.sendAndWait ?? sendAndWait;
|
|
1290
|
+
const _getLastAssistantMessage = opts._deps?.getLastAssistantMessage ?? getLastAssistantMessage;
|
|
1291
|
+
const _execGitDiffStat = opts._deps?.execGitDiffStat ?? defaultExecGitDiffStat;
|
|
1260
1292
|
try {
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1293
|
+
const gitDiffStat = await _execGitDiffStat(opts.cwd).catch(() => "(git diff unavailable)");
|
|
1294
|
+
const contextMessage = buildContextMessage(opts.loopResult, opts.prompt, gitDiffStat);
|
|
1295
|
+
const sessionId = await _createSession(opts.server.client, {
|
|
1296
|
+
cwd: opts.cwd,
|
|
1297
|
+
agentName: "debriefer"
|
|
1264
1298
|
});
|
|
1265
|
-
|
|
1266
|
-
|
|
1299
|
+
await _sendAndWait(opts.server.client, {
|
|
1300
|
+
sessionId,
|
|
1301
|
+
message: contextMessage,
|
|
1302
|
+
stallMs: 5 * 60 * 1e3
|
|
1303
|
+
// 5 min stall timeout for debrief
|
|
1304
|
+
});
|
|
1305
|
+
const debriefOutput = await _getLastAssistantMessage(opts.server.client, sessionId);
|
|
1306
|
+
if (debriefOutput) {
|
|
1307
|
+
process.stdout.write("\n\x1B[1m\u2500\u2500\u2500 Autopilot Debrief \u2500\u2500\u2500\x1B[0m\n\n");
|
|
1308
|
+
process.stdout.write(debriefOutput);
|
|
1309
|
+
process.stdout.write("\n\n");
|
|
1310
|
+
}
|
|
1311
|
+
} catch (err) {
|
|
1312
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1313
|
+
process.stderr.write(`\x1B[33m\u26A0 Debrief failed (non-fatal): ${msg}\x1B[0m
|
|
1314
|
+
`);
|
|
1267
1315
|
}
|
|
1268
1316
|
}
|
|
1269
1317
|
|
|
1318
|
+
// src/autopilot/cli.ts
|
|
1319
|
+
var loopCmd = command({
|
|
1320
|
+
name: "loop",
|
|
1321
|
+
description: "Run the Ralph loop: send a prompt to PRIME repeatedly until it emits <autopilot-done> or a budget is exhausted.",
|
|
1322
|
+
args: {
|
|
1323
|
+
prompt: positional({
|
|
1324
|
+
type: stringType,
|
|
1325
|
+
displayName: "prompt",
|
|
1326
|
+
description: "The prompt to send to PRIME each iteration (e.g. a Linear issue ref or free-form task)."
|
|
1327
|
+
}),
|
|
1328
|
+
maxIterations: option({
|
|
1329
|
+
long: "max-iterations",
|
|
1330
|
+
type: optional(numberType),
|
|
1331
|
+
description: `Maximum number of loop iterations (default: ${MAX_ITERATIONS}).`
|
|
1332
|
+
}),
|
|
1333
|
+
timeout: option({
|
|
1334
|
+
long: "timeout",
|
|
1335
|
+
type: optional(numberType),
|
|
1336
|
+
description: `Total wall-clock timeout in milliseconds (default: ${TIMEOUT_MS} = 4 hours).`
|
|
1337
|
+
}),
|
|
1338
|
+
noDebrief: flag({
|
|
1339
|
+
long: "no-debrief",
|
|
1340
|
+
description: "Skip the post-run debrief session."
|
|
1341
|
+
})
|
|
1342
|
+
},
|
|
1343
|
+
handler: async ({ prompt, maxIterations, timeout, noDebrief }) => {
|
|
1344
|
+
const cwd = process.cwd();
|
|
1345
|
+
process.stdout.write("\n\x1B[1mAutopilot \u2014 Ralph loop\x1B[0m\n");
|
|
1346
|
+
process.stdout.write(`Prompt: ${prompt.slice(0, 80)}${prompt.length > 80 ? "..." : ""}
|
|
1347
|
+
`);
|
|
1348
|
+
process.stdout.write(`Max iterations: ${maxIterations ?? MAX_ITERATIONS}
|
|
1349
|
+
`);
|
|
1350
|
+
process.stdout.write(`Timeout: ${((timeout ?? TIMEOUT_MS) / 36e5).toFixed(1)}h
|
|
1351
|
+
|
|
1352
|
+
`);
|
|
1353
|
+
const result = await runRalphLoop({
|
|
1354
|
+
prompt,
|
|
1355
|
+
cwd,
|
|
1356
|
+
maxIterations: maxIterations ?? void 0,
|
|
1357
|
+
timeoutMs: timeout ?? void 0
|
|
1358
|
+
});
|
|
1359
|
+
const icon = result.exitReason === "sentinel" ? "\x1B[32m\u2713\x1B[0m" : result.exitReason === "kill-switch" ? "\x1B[33m\u2298\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
|
|
1360
|
+
process.stdout.write(`
|
|
1361
|
+
${icon} ${result.message}
|
|
1362
|
+
`);
|
|
1363
|
+
process.stdout.write(` Iterations: ${result.iterations}
|
|
1364
|
+
|
|
1365
|
+
`);
|
|
1366
|
+
if (shouldRunDebrief({ noDebrief, env: process.env })) {
|
|
1367
|
+
const { startServer } = await import("./opencode-server-KPCDFYAX.js");
|
|
1368
|
+
let debriefServer;
|
|
1369
|
+
try {
|
|
1370
|
+
debriefServer = await startServer({ cwd });
|
|
1371
|
+
await runDebrief({
|
|
1372
|
+
server: debriefServer,
|
|
1373
|
+
loopResult: result,
|
|
1374
|
+
prompt,
|
|
1375
|
+
cwd
|
|
1376
|
+
});
|
|
1377
|
+
} catch {
|
|
1378
|
+
process.stderr.write("\x1B[33m\u26A0 Debrief server failed to start (non-fatal)\x1B[0m\n");
|
|
1379
|
+
} finally {
|
|
1380
|
+
await debriefServer?.shutdown().catch(() => {
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
if (result.exitReason !== "sentinel" && result.exitReason !== "kill-switch") {
|
|
1385
|
+
process.exit(1);
|
|
1386
|
+
}
|
|
1387
|
+
process.exit(0);
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
// src/autopilot/autopilot-cmd.ts
|
|
1392
|
+
import { command as command2, option as option2, optional as optional2, string as stringType2 } from "cmd-ts";
|
|
1393
|
+
|
|
1394
|
+
// src/autopilot/interactive.ts
|
|
1395
|
+
import * as fs7 from "fs";
|
|
1396
|
+
import * as path7 from "path";
|
|
1397
|
+
|
|
1270
1398
|
// src/plan-paths.ts
|
|
1271
1399
|
import { execFile } from "child_process";
|
|
1272
1400
|
import * as fs6 from "fs/promises";
|
|
@@ -1329,429 +1457,295 @@ async function getPlanDir(worktreeDir) {
|
|
|
1329
1457
|
await fs6.mkdir(planDir, { recursive: true });
|
|
1330
1458
|
return planDir;
|
|
1331
1459
|
}
|
|
1332
|
-
async function migratePlans(worktreeDir, planDir) {
|
|
1333
|
-
const oldDir = path6.join(worktreeDir, ".agent", "plans");
|
|
1334
|
-
const marker = path6.join(oldDir, ".migrated");
|
|
1335
|
-
try {
|
|
1336
|
-
await fs6.stat(oldDir);
|
|
1337
|
-
} catch {
|
|
1338
|
-
return;
|
|
1339
|
-
}
|
|
1340
|
-
try {
|
|
1341
|
-
await fs6.stat(marker);
|
|
1342
|
-
return;
|
|
1343
|
-
} catch {
|
|
1344
|
-
}
|
|
1345
|
-
let entries;
|
|
1346
|
-
try {
|
|
1347
|
-
entries = await fs6.readdir(oldDir);
|
|
1348
|
-
} catch {
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
const planFiles = entries.filter(
|
|
1352
|
-
(name) => name.endsWith(".md") && !name.startsWith(".")
|
|
1353
|
-
);
|
|
1354
|
-
await fs6.mkdir(planDir, { recursive: true });
|
|
1355
|
-
for (const name of planFiles) {
|
|
1356
|
-
const src = path6.join(oldDir, name);
|
|
1357
|
-
const dst = path6.join(planDir, name);
|
|
1358
|
-
let dstExists = false;
|
|
1359
|
-
try {
|
|
1360
|
-
await fs6.stat(dst);
|
|
1361
|
-
dstExists = true;
|
|
1362
|
-
} catch {
|
|
1363
|
-
dstExists = false;
|
|
1364
|
-
}
|
|
1365
|
-
if (!dstExists) {
|
|
1366
|
-
await fs6.rename(src, dst);
|
|
1367
|
-
continue;
|
|
1368
|
-
}
|
|
1369
|
-
const [srcBuf, dstBuf] = await Promise.all([
|
|
1370
|
-
fs6.readFile(src),
|
|
1371
|
-
fs6.readFile(dst)
|
|
1372
|
-
]);
|
|
1373
|
-
if (srcBuf.equals(dstBuf)) {
|
|
1374
|
-
await fs6.unlink(src);
|
|
1375
|
-
continue;
|
|
1376
|
-
}
|
|
1377
|
-
process.stderr.write(
|
|
1378
|
-
`[harness-opencode] migratePlans: conflict on ${name} \u2014 destination ${dst} exists with different content; leaving source ${src} in place. Resolve manually.
|
|
1379
|
-
`
|
|
1380
|
-
);
|
|
1381
|
-
}
|
|
1382
|
-
await fs6.writeFile(marker, "");
|
|
1383
|
-
}
|
|
1384
1460
|
|
|
1385
|
-
// src/autopilot/
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
import { promisify as promisify2 } from "util";
|
|
1391
|
-
import { readFileSync as readFileSync6 } from "fs";
|
|
1392
|
-
import { join as join8 } from "path";
|
|
1393
|
-
|
|
1394
|
-
// src/lib/opencode-server.ts
|
|
1395
|
-
import { execFile as execFile2 } from "child_process";
|
|
1396
|
-
import { promisify } from "util";
|
|
1397
|
-
import {
|
|
1398
|
-
createOpencodeServer,
|
|
1399
|
-
createOpencodeClient
|
|
1400
|
-
} from "@opencode-ai/sdk";
|
|
1401
|
-
var execFileP2 = promisify(execFile2);
|
|
1402
|
-
var DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
|
|
1403
|
-
async function ensureOpencodeOnPath() {
|
|
1404
|
-
try {
|
|
1405
|
-
await execFileP2("opencode", ["--version"]);
|
|
1406
|
-
} catch {
|
|
1407
|
-
throw new Error(
|
|
1408
|
-
"opencode CLI not found on PATH.\n Install: https://opencode.ai\n Or: bunx opencode upgrade"
|
|
1409
|
-
);
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
async function startServer(opts) {
|
|
1413
|
-
await ensureOpencodeOnPath();
|
|
1414
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
|
|
1415
|
-
const port = opts.port ?? 0;
|
|
1416
|
-
const server = await createOpencodeServer({
|
|
1417
|
-
port,
|
|
1418
|
-
timeout: timeoutMs
|
|
1419
|
-
});
|
|
1420
|
-
const client = createOpencodeClient({ url: server.url });
|
|
1421
|
-
let shutdownCalled = false;
|
|
1422
|
-
const shutdown = async () => {
|
|
1423
|
-
if (shutdownCalled) return;
|
|
1424
|
-
shutdownCalled = true;
|
|
1425
|
-
try {
|
|
1426
|
-
await server.close();
|
|
1427
|
-
} catch {
|
|
1428
|
-
}
|
|
1429
|
-
};
|
|
1430
|
-
return { url: server.url, client, shutdown };
|
|
1461
|
+
// src/autopilot/interactive.ts
|
|
1462
|
+
function defaultBanner(message) {
|
|
1463
|
+
process.stdout.write(`
|
|
1464
|
+
${message}
|
|
1465
|
+
`);
|
|
1431
1466
|
}
|
|
1432
|
-
async function
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1467
|
+
async function orchestrateAutopilot(opts, deps) {
|
|
1468
|
+
const banner = deps.onBanner ?? defaultBanner;
|
|
1469
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1470
|
+
banner("\u2192 Phase 1/3: Scoping (interactive)...");
|
|
1471
|
+
const scoperResult = await deps.runScoper({
|
|
1472
|
+
planDir: opts.planDir,
|
|
1473
|
+
slug: opts.slug,
|
|
1474
|
+
initialGoal: opts.initialGoal
|
|
1438
1475
|
});
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1476
|
+
banner(`\u2713 Scope captured at ${scoperResult.scopePath}`);
|
|
1477
|
+
const actualSlug = path7.basename(path7.dirname(scoperResult.scopePath));
|
|
1478
|
+
banner("\u2192 Phase 2/3: Planning (headless)...");
|
|
1479
|
+
const planResult = await deps.runPlan({
|
|
1480
|
+
scopePath: scoperResult.scopePath,
|
|
1481
|
+
planDir: opts.planDir,
|
|
1482
|
+
slug: actualSlug || opts.slug
|
|
1446
1483
|
});
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1484
|
+
banner(`\u2713 Plan written at ${planResult.planPath}`);
|
|
1485
|
+
banner("\u2192 Phase 3/3: Executing (headless loop)...");
|
|
1486
|
+
const loopResult = await deps.runLoop({
|
|
1487
|
+
planPath: planResult.planPath,
|
|
1488
|
+
cwd
|
|
1451
1489
|
});
|
|
1490
|
+
return {
|
|
1491
|
+
scopePath: scoperResult.scopePath,
|
|
1492
|
+
planPath: planResult.planPath,
|
|
1493
|
+
loopResult
|
|
1494
|
+
};
|
|
1452
1495
|
}
|
|
1453
|
-
|
|
1454
|
-
const
|
|
1455
|
-
return
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
if (opts.abortSignal.aborted) {
|
|
1472
|
-
settle({ kind: "abort" });
|
|
1473
|
-
return;
|
|
1474
|
-
}
|
|
1475
|
-
opts.abortSignal.addEventListener("abort", () => settle({ kind: "abort" }), { once: true });
|
|
1496
|
+
function deriveSlug(goal) {
|
|
1497
|
+
const slug = goal.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
1498
|
+
return slug.length > 0 ? slug : `feature-${Date.now()}`;
|
|
1499
|
+
}
|
|
1500
|
+
async function browsePlansDir(planDir, _readdirSync) {
|
|
1501
|
+
const { select: select2 } = await import("@inquirer/prompts");
|
|
1502
|
+
const readdir2 = _readdirSync ?? ((p, o) => fs7.readdirSync(p, o));
|
|
1503
|
+
let currentDir = planDir;
|
|
1504
|
+
while (true) {
|
|
1505
|
+
const entries = readdir2(currentDir, { withFileTypes: true });
|
|
1506
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
1507
|
+
const files = entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name).sort();
|
|
1508
|
+
if (dirs.length === 0 && files.length === 0) {
|
|
1509
|
+
process.stderr.write(`
|
|
1510
|
+
No plans found in ${currentDir}
|
|
1511
|
+
|
|
1512
|
+
`);
|
|
1513
|
+
return null;
|
|
1476
1514
|
}
|
|
1477
|
-
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1515
|
+
const choices = [];
|
|
1516
|
+
for (const d of dirs) {
|
|
1517
|
+
const dirPath = path7.join(currentDir, d);
|
|
1518
|
+
const hasMain = fs7.existsSync(path7.join(dirPath, "main.md"));
|
|
1519
|
+
const fileCount = readdir2(dirPath, { withFileTypes: true }).filter((e) => e.isFile()).length;
|
|
1520
|
+
choices.push({
|
|
1521
|
+
name: hasMain ? `${d}/ (multi-file plan \u2014 ${fileCount} files)` : `${d}/ (${fileCount} files)`,
|
|
1522
|
+
value: `dir:${dirPath}`
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
for (const f of files) {
|
|
1526
|
+
choices.push({
|
|
1527
|
+
name: `${f}`,
|
|
1528
|
+
value: `file:${path7.join(currentDir, f)}`
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
if (currentDir !== planDir) {
|
|
1532
|
+
choices.push({ name: "\u21A9 Back", value: "back" });
|
|
1533
|
+
}
|
|
1534
|
+
choices.push({ name: "\u2715 Cancel (scope a new feature instead)", value: "cancel" });
|
|
1535
|
+
const answer = await select2({
|
|
1536
|
+
message: "Select a plan:",
|
|
1537
|
+
choices
|
|
1538
|
+
});
|
|
1539
|
+
if (answer === "cancel") return null;
|
|
1540
|
+
if (answer === "back") {
|
|
1541
|
+
currentDir = path7.dirname(currentDir);
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
if (answer.startsWith("file:")) {
|
|
1545
|
+
return answer.slice("file:".length);
|
|
1546
|
+
}
|
|
1547
|
+
if (answer.startsWith("dir:")) {
|
|
1548
|
+
const dirPath = answer.slice("dir:".length);
|
|
1549
|
+
const hasMain = fs7.existsSync(path7.join(dirPath, "main.md"));
|
|
1550
|
+
if (hasMain) {
|
|
1551
|
+
const dirAction = await select2({
|
|
1552
|
+
message: `${path7.basename(dirPath)}/ has a main.md. What do you want?`,
|
|
1553
|
+
choices: [
|
|
1554
|
+
{ name: "Select this as a multi-file plan", value: "select" },
|
|
1555
|
+
{ name: "Browse files inside", value: "browse" },
|
|
1556
|
+
{ name: "\u21A9 Back", value: "back" }
|
|
1557
|
+
]
|
|
1558
|
+
});
|
|
1559
|
+
if (dirAction === "select") return dirPath;
|
|
1560
|
+
if (dirAction === "browse") {
|
|
1561
|
+
currentDir = dirPath;
|
|
1562
|
+
continue;
|
|
1502
1563
|
}
|
|
1503
|
-
|
|
1504
|
-
streamDone = true;
|
|
1564
|
+
continue;
|
|
1505
1565
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
};
|
|
1509
|
-
});
|
|
1510
|
-
}
|
|
1511
|
-
async function getLastAssistantMessage(client, sessionId) {
|
|
1512
|
-
try {
|
|
1513
|
-
const messages = await client.session.messages({ path: { id: sessionId } });
|
|
1514
|
-
const assistantMessages = messages.filter((m) => m.info.role === "assistant");
|
|
1515
|
-
if (assistantMessages.length === 0) return "";
|
|
1516
|
-
const last = assistantMessages[assistantMessages.length - 1];
|
|
1517
|
-
return last.parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
1518
|
-
} catch {
|
|
1519
|
-
return "";
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
// src/autopilot/config.ts
|
|
1524
|
-
var MAX_ITERATIONS = 50;
|
|
1525
|
-
var STRUGGLE_THRESHOLD = 3;
|
|
1526
|
-
var TIMEOUT_MS = 4 * 60 * 60 * 1e3;
|
|
1527
|
-
var STALL_MS = 60 * 60 * 1e3;
|
|
1528
|
-
var KILL_SWITCH_PATH = ".agent/autopilot-disable";
|
|
1529
|
-
var SENTINEL_TAG = "<autopilot-done>";
|
|
1530
|
-
|
|
1531
|
-
// src/autopilot/sentinel.ts
|
|
1532
|
-
function detectSentinel(text) {
|
|
1533
|
-
if (!text.includes(SENTINEL_TAG)) {
|
|
1534
|
-
return false;
|
|
1535
|
-
}
|
|
1536
|
-
const withoutFences = text.replace(/```[\s\S]*?```/g, "");
|
|
1537
|
-
const withoutInline = withoutFences.replace(/`[^`\n]*`/g, "");
|
|
1538
|
-
return withoutInline.includes(SENTINEL_TAG);
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
// src/autopilot/struggle.ts
|
|
1542
|
-
import * as fs7 from "fs";
|
|
1543
|
-
import * as path7 from "path";
|
|
1544
|
-
var StruggleDetector = class {
|
|
1545
|
-
_consecutiveStalls = 0;
|
|
1546
|
-
_threshold;
|
|
1547
|
-
constructor(threshold) {
|
|
1548
|
-
this._threshold = threshold;
|
|
1549
|
-
}
|
|
1550
|
-
/** Number of consecutive stall iterations recorded so far. */
|
|
1551
|
-
get consecutiveStalls() {
|
|
1552
|
-
return this._consecutiveStalls;
|
|
1553
|
-
}
|
|
1554
|
-
/**
|
|
1555
|
-
* Record the result of one iteration.
|
|
1556
|
-
* @param madeProgress - true if the agent made filesystem changes this iteration.
|
|
1557
|
-
*/
|
|
1558
|
-
record(madeProgress) {
|
|
1559
|
-
if (madeProgress) {
|
|
1560
|
-
this._consecutiveStalls = 0;
|
|
1561
|
-
} else {
|
|
1562
|
-
this._consecutiveStalls++;
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
/**
|
|
1566
|
-
* Returns true if the agent has stalled for `threshold` consecutive
|
|
1567
|
-
* iterations without making progress.
|
|
1568
|
-
*/
|
|
1569
|
-
isStruggling() {
|
|
1570
|
-
return this._consecutiveStalls >= this._threshold;
|
|
1571
|
-
}
|
|
1572
|
-
};
|
|
1573
|
-
function checkKillSwitch(cwd) {
|
|
1574
|
-
const killSwitchFile = path7.join(cwd, KILL_SWITCH_PATH);
|
|
1575
|
-
return fs7.existsSync(killSwitchFile);
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
// src/autopilot/loop.ts
|
|
1579
|
-
var execFile3 = promisify2(execFileCb);
|
|
1580
|
-
function buildFullPrompt(userPrompt) {
|
|
1581
|
-
const candidates = [
|
|
1582
|
-
join8(import.meta.dir, "prompt-template.md"),
|
|
1583
|
-
join8(import.meta.dir, "..", "..", "src", "autopilot", "prompt-template.md")
|
|
1584
|
-
];
|
|
1585
|
-
let template = "";
|
|
1586
|
-
for (const candidate of candidates) {
|
|
1587
|
-
try {
|
|
1588
|
-
const raw = readFileSync6(candidate, "utf8");
|
|
1589
|
-
template = raw.replace(/^---\n[\s\S]*?\n---\n/, "");
|
|
1590
|
-
break;
|
|
1591
|
-
} catch {
|
|
1566
|
+
currentDir = dirPath;
|
|
1567
|
+
continue;
|
|
1592
1568
|
}
|
|
1593
1569
|
}
|
|
1594
|
-
const withArgs = template.replace("$ARGUMENTS", userPrompt);
|
|
1595
|
-
return withArgs || userPrompt;
|
|
1596
1570
|
}
|
|
1597
|
-
async function
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
return stdout.trim();
|
|
1609
|
-
} catch {
|
|
1610
|
-
return "HEAD";
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
async function runRalphLoop(opts) {
|
|
1614
|
-
const maxIterations = opts.maxIterations ?? MAX_ITERATIONS;
|
|
1615
|
-
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
1616
|
-
const stallMs = opts.stallMs ?? STALL_MS;
|
|
1617
|
-
const struggleThreshold = opts.struggleThreshold ?? STRUGGLE_THRESHOLD;
|
|
1618
|
-
const _startServer = opts._deps?.startServer ?? startServer;
|
|
1619
|
-
const _createSession = opts._deps?.createSession ?? createSession;
|
|
1620
|
-
const _sendAndWait = opts._deps?.sendAndWait ?? sendAndWait;
|
|
1621
|
-
const _getLastAssistantMessage = opts._deps?.getLastAssistantMessage ?? getLastAssistantMessage;
|
|
1622
|
-
const fullPrompt = buildFullPrompt(opts.prompt);
|
|
1623
|
-
const struggle = new StruggleDetector(struggleThreshold);
|
|
1624
|
-
const startTime = Date.now();
|
|
1625
|
-
const server = await _startServer({ cwd: opts.cwd });
|
|
1626
|
-
const abort = new AbortController();
|
|
1627
|
-
const timeoutHandle = setTimeout(() => {
|
|
1628
|
-
abort.abort();
|
|
1629
|
-
}, timeoutMs);
|
|
1630
|
-
try {
|
|
1631
|
-
const sessionId = await _createSession(server.client, {
|
|
1632
|
-
cwd: opts.cwd,
|
|
1633
|
-
agentName: "prime"
|
|
1571
|
+
async function runInteractiveAutopilot(cwd, _deps) {
|
|
1572
|
+
const _getPlanDir = _deps?.getPlanDir ?? getPlanDir;
|
|
1573
|
+
const planDir = await _getPlanDir(cwd);
|
|
1574
|
+
let hasExistingPlan;
|
|
1575
|
+
if (_deps?.promptExistingPlan) {
|
|
1576
|
+
hasExistingPlan = await _deps.promptExistingPlan();
|
|
1577
|
+
} else {
|
|
1578
|
+
const { confirm: confirm2 } = await import("@inquirer/prompts");
|
|
1579
|
+
hasExistingPlan = await confirm2({
|
|
1580
|
+
message: "Do you have an existing plan?",
|
|
1581
|
+
default: false
|
|
1634
1582
|
});
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
}
|
|
1650
|
-
const headBefore = await getHeadSha(opts.cwd);
|
|
1651
|
-
const result = await _sendAndWait(server.client, {
|
|
1652
|
-
sessionId,
|
|
1653
|
-
message: fullPrompt,
|
|
1654
|
-
stallMs,
|
|
1655
|
-
abortSignal: abort.signal
|
|
1583
|
+
}
|
|
1584
|
+
if (hasExistingPlan) {
|
|
1585
|
+
const repoLocalPlansDir = path7.join(cwd, "plans");
|
|
1586
|
+
const hasRepoLocal = fs7.existsSync(repoLocalPlansDir) && fs7.statSync(repoLocalPlansDir).isDirectory();
|
|
1587
|
+
const hasShared = fs7.existsSync(planDir) && fs7.statSync(planDir).isDirectory();
|
|
1588
|
+
let browseRoot;
|
|
1589
|
+
if (hasRepoLocal && hasShared) {
|
|
1590
|
+
const { select: select2 } = await import("@inquirer/prompts");
|
|
1591
|
+
const which2 = await select2({
|
|
1592
|
+
message: "Where are your plans?",
|
|
1593
|
+
choices: [
|
|
1594
|
+
{ name: `./plans/ (repo-local)`, value: repoLocalPlansDir },
|
|
1595
|
+
{ name: `${planDir} (harness-shared)`, value: planDir }
|
|
1596
|
+
]
|
|
1656
1597
|
});
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1598
|
+
browseRoot = which2;
|
|
1599
|
+
} else if (hasRepoLocal) {
|
|
1600
|
+
browseRoot = repoLocalPlansDir;
|
|
1601
|
+
} else {
|
|
1602
|
+
browseRoot = planDir;
|
|
1603
|
+
}
|
|
1604
|
+
let selectedPlan;
|
|
1605
|
+
if (_deps?.browsePlans) {
|
|
1606
|
+
selectedPlan = await _deps.browsePlans(browseRoot);
|
|
1607
|
+
} else {
|
|
1608
|
+
selectedPlan = await browsePlansDir(browseRoot, _deps?.readdirSync);
|
|
1609
|
+
}
|
|
1610
|
+
if (!selectedPlan) {
|
|
1611
|
+
process.stderr.write("\n No plan selected. Starting new feature scoping.\n\n");
|
|
1612
|
+
} else {
|
|
1613
|
+
const isDir = fs7.statSync(selectedPlan).isDirectory();
|
|
1614
|
+
const planPath = isDir ? selectedPlan : selectedPlan;
|
|
1615
|
+
const { parsePlanState } = await import("./plan-parser-TMHEKT22.js");
|
|
1616
|
+
const planState = parsePlanState(planPath);
|
|
1617
|
+
if (planState.totalItems > 0 && planState.checkedItems === planState.totalItems) {
|
|
1618
|
+
const { select: selectAction } = await import("@inquirer/prompts");
|
|
1619
|
+
const action = await selectAction({
|
|
1620
|
+
message: `All ${planState.totalItems} items in this plan are already checked. What do you want to do?`,
|
|
1621
|
+
choices: [
|
|
1622
|
+
{ name: "Uncheck all items and run from scratch", value: "uncheck" },
|
|
1623
|
+
{ name: "Run anyway (agent will verify/audit the checked items)", value: "run" },
|
|
1624
|
+
{ name: "Cancel and pick a different plan", value: "cancel" }
|
|
1625
|
+
]
|
|
1626
|
+
});
|
|
1627
|
+
if (action === "cancel") {
|
|
1628
|
+
process.stderr.write("\n Cancelled. Starting new feature scoping.\n\n");
|
|
1629
|
+
} else {
|
|
1630
|
+
if (action === "uncheck") {
|
|
1631
|
+
const uncheckFiles = isDir ? fs7.readdirSync(planPath).filter((f) => f.endsWith(".md")).map((f) => path7.join(planPath, f)) : [planPath];
|
|
1632
|
+
for (const file of uncheckFiles) {
|
|
1633
|
+
const content = fs7.readFileSync(file, "utf-8");
|
|
1634
|
+
const unchecked = content.replace(/- \[x\]/g, "- [ ]");
|
|
1635
|
+
fs7.writeFileSync(file, unchecked);
|
|
1636
|
+
}
|
|
1637
|
+
process.stderr.write(`
|
|
1638
|
+
\u2713 Unchecked all items in ${uncheckFiles.length} file(s).
|
|
1639
|
+
|
|
1640
|
+
`);
|
|
1641
|
+
}
|
|
1642
|
+
const banner = _deps?.onBanner ?? ((msg) => process.stdout.write(`
|
|
1643
|
+
${msg}
|
|
1644
|
+
`));
|
|
1645
|
+
banner(`\u2192 Running loop against plan: ${planPath}`);
|
|
1646
|
+
const { runLoopSession: runLoopSession2 } = await import("./loop-session-J35NILUZ.js");
|
|
1647
|
+
const _runLoop = _deps?.runLoop ?? runLoopSession2;
|
|
1648
|
+
const loopResult = await _runLoop({ planPath, cwd });
|
|
1649
|
+
return {
|
|
1650
|
+
scopePath: "",
|
|
1651
|
+
planPath,
|
|
1652
|
+
loopResult
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
} else {
|
|
1656
|
+
const unchecked = planState.totalItems - planState.checkedItems;
|
|
1657
|
+
process.stderr.write(
|
|
1658
|
+
`
|
|
1659
|
+
Plan: ${planState.totalItems} items, ${unchecked} remaining.
|
|
1660
|
+
|
|
1661
|
+
`
|
|
1662
|
+
);
|
|
1663
|
+
const banner = _deps?.onBanner ?? ((msg) => process.stdout.write(`
|
|
1664
|
+
${msg}
|
|
1665
|
+
`));
|
|
1666
|
+
banner(`\u2192 Running loop against plan: ${planPath}`);
|
|
1667
|
+
const { runLoopSession: runLoopSession2 } = await import("./loop-session-J35NILUZ.js");
|
|
1668
|
+
const _runLoop = _deps?.runLoop ?? runLoopSession2;
|
|
1669
|
+
const loopResult = await _runLoop({ planPath, cwd });
|
|
1689
1670
|
return {
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1671
|
+
scopePath: "",
|
|
1672
|
+
planPath,
|
|
1673
|
+
loopResult
|
|
1693
1674
|
};
|
|
1694
1675
|
}
|
|
1695
1676
|
}
|
|
1696
|
-
return {
|
|
1697
|
-
exitReason: "max-iterations",
|
|
1698
|
-
iterations: maxIterations,
|
|
1699
|
-
message: `Reached maximum iterations (${maxIterations}). Stopping.`
|
|
1700
|
-
};
|
|
1701
|
-
} finally {
|
|
1702
|
-
clearTimeout(timeoutHandle);
|
|
1703
|
-
await server.shutdown();
|
|
1704
1677
|
}
|
|
1678
|
+
let goal;
|
|
1679
|
+
let ticketRef;
|
|
1680
|
+
if (_deps?.promptGoal) {
|
|
1681
|
+
goal = await _deps.promptGoal();
|
|
1682
|
+
} else {
|
|
1683
|
+
const { input } = await import("@inquirer/prompts");
|
|
1684
|
+
goal = await input({
|
|
1685
|
+
message: "What do you want to build? (one sentence, free-form)",
|
|
1686
|
+
validate: (v) => v.trim().length > 0 ? true : "Please describe what you want to build."
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
if (_deps?.promptTicketRef) {
|
|
1690
|
+
ticketRef = await _deps.promptTicketRef();
|
|
1691
|
+
} else {
|
|
1692
|
+
const { input } = await import("@inquirer/prompts");
|
|
1693
|
+
ticketRef = await input({
|
|
1694
|
+
message: "Optional ticket or issue ref (Linear ID, GitHub issue URL, etc.)",
|
|
1695
|
+
default: ""
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
const slug = deriveSlug(goal);
|
|
1699
|
+
const seedDir = path7.join(planDir, slug);
|
|
1700
|
+
const seedPath = path7.join(seedDir, "scope-seed.md");
|
|
1701
|
+
const _mkdirSync = _deps?.mkdirSync ?? ((p, o) => fs7.mkdirSync(p, o));
|
|
1702
|
+
const _writeFileSync = _deps?.writeFileSync ?? fs7.writeFileSync;
|
|
1703
|
+
_mkdirSync(seedDir, { recursive: true });
|
|
1704
|
+
const seedContent = [
|
|
1705
|
+
`# Scope Seed: ${slug}`,
|
|
1706
|
+
"",
|
|
1707
|
+
`## Goal`,
|
|
1708
|
+
"",
|
|
1709
|
+
goal,
|
|
1710
|
+
"",
|
|
1711
|
+
...ticketRef.trim() ? [`## Ticket / Issue Ref`, "", ticketRef.trim(), ""] : []
|
|
1712
|
+
].join("\n");
|
|
1713
|
+
_writeFileSync(seedPath, seedContent);
|
|
1714
|
+
const { runScoperSession } = await import("./scoper-S77SOK7X.js");
|
|
1715
|
+
const { runPlanSession } = await import("./plan-session-7VS32P52.js");
|
|
1716
|
+
const { runLoopSession } = await import("./loop-session-J35NILUZ.js");
|
|
1717
|
+
return orchestrateAutopilot(
|
|
1718
|
+
{ slug, planDir, cwd, initialGoal: goal },
|
|
1719
|
+
{
|
|
1720
|
+
runScoper: _deps?.runScoper ?? runScoperSession,
|
|
1721
|
+
runPlan: _deps?.runPlan ?? runPlanSession,
|
|
1722
|
+
runLoop: _deps?.runLoop ?? runLoopSession,
|
|
1723
|
+
onBanner: _deps?.onBanner
|
|
1724
|
+
}
|
|
1725
|
+
);
|
|
1705
1726
|
}
|
|
1706
1727
|
|
|
1707
|
-
// src/autopilot/
|
|
1708
|
-
var
|
|
1728
|
+
// src/autopilot/autopilot-cmd.ts
|
|
1729
|
+
var autopilotInteractiveCmd = command2({
|
|
1709
1730
|
name: "autopilot",
|
|
1710
|
-
description: "
|
|
1731
|
+
description: "Interactive three-phase autopilot: scope with @scoper, plan with @plan, then execute with the Ralph loop. Produces a structured plan before running.",
|
|
1711
1732
|
args: {
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
description: "
|
|
1716
|
-
}),
|
|
1717
|
-
maxIterations: option({
|
|
1718
|
-
long: "max-iterations",
|
|
1719
|
-
type: optional(numberType),
|
|
1720
|
-
description: `Maximum number of loop iterations (default: ${MAX_ITERATIONS}).`
|
|
1721
|
-
}),
|
|
1722
|
-
timeout: option({
|
|
1723
|
-
long: "timeout",
|
|
1724
|
-
type: optional(numberType),
|
|
1725
|
-
description: `Total wall-clock timeout in milliseconds (default: ${TIMEOUT_MS} = 4 hours).`
|
|
1733
|
+
slug: option2({
|
|
1734
|
+
long: "slug",
|
|
1735
|
+
type: optional2(stringType2),
|
|
1736
|
+
description: "Plan slug (kebab-case, \u22645 words). If omitted, you will be prompted during the scoping session."
|
|
1726
1737
|
})
|
|
1727
1738
|
},
|
|
1728
|
-
handler: async ({
|
|
1729
|
-
const
|
|
1730
|
-
process.stdout.write(
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
const result = await runRalphLoop({
|
|
1739
|
-
prompt,
|
|
1740
|
-
cwd,
|
|
1741
|
-
maxIterations: maxIterations ?? void 0,
|
|
1742
|
-
timeoutMs: timeout ?? void 0
|
|
1743
|
-
});
|
|
1744
|
-
const icon = result.exitReason === "sentinel" ? "\x1B[32m\u2713\x1B[0m" : result.exitReason === "kill-switch" ? "\x1B[33m\u2298\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
|
|
1745
|
-
process.stdout.write(`
|
|
1746
|
-
${icon} ${result.message}
|
|
1747
|
-
`);
|
|
1748
|
-
process.stdout.write(` Iterations: ${result.iterations}
|
|
1749
|
-
|
|
1750
|
-
`);
|
|
1751
|
-
if (result.exitReason !== "sentinel" && result.exitReason !== "kill-switch") {
|
|
1752
|
-
process.exit(1);
|
|
1753
|
-
}
|
|
1754
|
-
process.exit(0);
|
|
1739
|
+
handler: async ({ slug: _slug }) => {
|
|
1740
|
+
const result = await runInteractiveAutopilot(process.cwd());
|
|
1741
|
+
process.stdout.write(
|
|
1742
|
+
`
|
|
1743
|
+
\x1B[1m\u2713 Autopilot complete\x1B[0m
|
|
1744
|
+
Scope: ${result.scopePath}
|
|
1745
|
+
Plan: ${result.planPath}
|
|
1746
|
+
Loop: ${result.loopResult.exitReason} after ${result.loopResult.iterations} iteration(s)
|
|
1747
|
+
`
|
|
1748
|
+
);
|
|
1755
1749
|
}
|
|
1756
1750
|
});
|
|
1757
1751
|
|
|
@@ -1760,7 +1754,7 @@ import * as fs8 from "fs";
|
|
|
1760
1754
|
import * as path8 from "path";
|
|
1761
1755
|
import * as os6 from "os";
|
|
1762
1756
|
import { spawn } from "child_process";
|
|
1763
|
-
import { fileURLToPath as
|
|
1757
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1764
1758
|
var PACKAGE_NAME = "@glrs-dev/harness-plugin-opencode";
|
|
1765
1759
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1766
1760
|
var c2 = {
|
|
@@ -1805,7 +1799,7 @@ function writeState(state) {
|
|
|
1805
1799
|
}
|
|
1806
1800
|
}
|
|
1807
1801
|
function readInstalledVersion() {
|
|
1808
|
-
const here = path8.dirname(
|
|
1802
|
+
const here = path8.dirname(fileURLToPath2(import.meta.url));
|
|
1809
1803
|
const candidates = [
|
|
1810
1804
|
path8.join(here, "..", "package.json"),
|
|
1811
1805
|
path8.join(here, "..", "..", "package.json"),
|
|
@@ -1939,15 +1933,15 @@ Upgrade Node or run via a compatible Bun runtime. See the "engines" field in pac
|
|
|
1939
1933
|
}
|
|
1940
1934
|
}
|
|
1941
1935
|
var VERSION = "0.1.0";
|
|
1942
|
-
var installCmd =
|
|
1936
|
+
var installCmd = command3({
|
|
1943
1937
|
name: "install",
|
|
1944
1938
|
description: 'Add "@glrs-dev/harness-plugin-opencode" to your opencode.json plugin array.',
|
|
1945
1939
|
args: {
|
|
1946
|
-
dryRun:
|
|
1940
|
+
dryRun: flag2({
|
|
1947
1941
|
long: "dry-run",
|
|
1948
1942
|
description: "Preview changes without writing."
|
|
1949
1943
|
}),
|
|
1950
|
-
pin:
|
|
1944
|
+
pin: flag2({
|
|
1951
1945
|
long: "pin",
|
|
1952
1946
|
description: "Pin to the current exact version (e.g. @0.1.0)."
|
|
1953
1947
|
})
|
|
@@ -1956,11 +1950,11 @@ var installCmd = command2({
|
|
|
1956
1950
|
await install({ dryRun, pin });
|
|
1957
1951
|
}
|
|
1958
1952
|
});
|
|
1959
|
-
var uninstallCmd =
|
|
1953
|
+
var uninstallCmd = command3({
|
|
1960
1954
|
name: "uninstall",
|
|
1961
1955
|
description: 'Remove "@glrs-dev/harness-plugin-opencode" from your opencode.json plugin array.',
|
|
1962
1956
|
args: {
|
|
1963
|
-
dryRun:
|
|
1957
|
+
dryRun: flag2({
|
|
1964
1958
|
long: "dry-run",
|
|
1965
1959
|
description: "Preview changes without writing."
|
|
1966
1960
|
})
|
|
@@ -1969,7 +1963,7 @@ var uninstallCmd = command2({
|
|
|
1969
1963
|
uninstall({ dryRun });
|
|
1970
1964
|
}
|
|
1971
1965
|
});
|
|
1972
|
-
var doctorCmd =
|
|
1966
|
+
var doctorCmd = command3({
|
|
1973
1967
|
name: "doctor",
|
|
1974
1968
|
description: "Check installation health (OpenCode CLI, plugin registration, MCP backends).",
|
|
1975
1969
|
args: {},
|
|
@@ -1977,66 +1971,15 @@ var doctorCmd = command2({
|
|
|
1977
1971
|
doctor();
|
|
1978
1972
|
}
|
|
1979
1973
|
});
|
|
1980
|
-
var
|
|
1981
|
-
name: "plan-check",
|
|
1982
|
-
description: "Parse a plan file's plan-state fence (legacy markdown plans).",
|
|
1983
|
-
args: {
|
|
1984
|
-
run: option2({
|
|
1985
|
-
long: "run",
|
|
1986
|
-
type: optional2(string),
|
|
1987
|
-
description: "Print verify commands for pending items, one per line."
|
|
1988
|
-
}),
|
|
1989
|
-
check: option2({
|
|
1990
|
-
long: "check",
|
|
1991
|
-
type: optional2(string),
|
|
1992
|
-
description: "Structural validation; exits 1 if any item is invalid."
|
|
1993
|
-
}),
|
|
1994
|
-
rest: restPositionals({
|
|
1995
|
-
type: string,
|
|
1996
|
-
displayName: "plan-path",
|
|
1997
|
-
description: "Path to a plan markdown file. Required unless --run / --check is given."
|
|
1998
|
-
})
|
|
1999
|
-
},
|
|
2000
|
-
handler: ({ run: run2, check, rest }) => {
|
|
2001
|
-
const legacy = [];
|
|
2002
|
-
if (run2 !== void 0) {
|
|
2003
|
-
legacy.push("--run", run2);
|
|
2004
|
-
} else if (check !== void 0) {
|
|
2005
|
-
legacy.push("--check", check);
|
|
2006
|
-
} else {
|
|
2007
|
-
legacy.push(...rest);
|
|
2008
|
-
}
|
|
2009
|
-
planCheck(legacy);
|
|
2010
|
-
}
|
|
2011
|
-
});
|
|
2012
|
-
var planDirCmd = command2({
|
|
2013
|
-
name: "plan-dir",
|
|
2014
|
-
description: "Print the repo-shared plan directory for the current worktree (resolves + creates + migrates legacy).",
|
|
2015
|
-
args: {},
|
|
2016
|
-
handler: async () => {
|
|
2017
|
-
try {
|
|
2018
|
-
const cwd = process.cwd();
|
|
2019
|
-
const planDir = await getPlanDir(cwd);
|
|
2020
|
-
await migratePlans(cwd, planDir);
|
|
2021
|
-
process.stdout.write(planDir + "\n");
|
|
2022
|
-
process.exit(0);
|
|
2023
|
-
} catch (err) {
|
|
2024
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2025
|
-
process.stderr.write(`plan-dir: ${msg}
|
|
2026
|
-
`);
|
|
2027
|
-
process.exit(1);
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
});
|
|
2031
|
-
var installPluginCmd = command2({
|
|
1974
|
+
var installPluginCmd = command3({
|
|
2032
1975
|
name: "install-plugin",
|
|
2033
1976
|
description: 'Add "@glrs-dev/harness-plugin-opencode" to your opencode.json plugin array.',
|
|
2034
1977
|
args: {
|
|
2035
|
-
dryRun:
|
|
1978
|
+
dryRun: flag2({
|
|
2036
1979
|
long: "dry-run",
|
|
2037
1980
|
description: "Preview changes without writing."
|
|
2038
1981
|
}),
|
|
2039
|
-
pin:
|
|
1982
|
+
pin: flag2({
|
|
2040
1983
|
long: "pin",
|
|
2041
1984
|
description: "Pin to the current exact version (e.g. @0.1.0)."
|
|
2042
1985
|
})
|
|
@@ -2054,9 +1997,11 @@ var cli = subcommands({
|
|
|
2054
1997
|
install: installCmd,
|
|
2055
1998
|
uninstall: uninstallCmd,
|
|
2056
1999
|
doctor: doctorCmd,
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2000
|
+
// `loop` is the raw-prompt Ralph loop runner.
|
|
2001
|
+
// `autopilot` is the interactive three-phase orchestrator (scope → plan → loop).
|
|
2002
|
+
// PR 3 diverged them: they are now separate subcommands.
|
|
2003
|
+
loop: loopCmd,
|
|
2004
|
+
autopilot: autopilotInteractiveCmd
|
|
2060
2005
|
}
|
|
2061
2006
|
});
|
|
2062
2007
|
var printUpdate = startUpdateCheck();
|