@glrs-dev/cli 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.
Files changed (28) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/{chunk-SB3MLROC.js → chunk-MIWZLETC.js} +7 -2
  3. package/dist/cli.js +1 -1
  4. package/dist/lib/auto-update.js +1 -1
  5. package/dist/vendor/harness-opencode/dist/agents/prompts/build.md +16 -0
  6. package/dist/vendor/harness-opencode/dist/agents/prompts/code-reviewer-thorough.md +6 -7
  7. package/dist/vendor/harness-opencode/dist/agents/prompts/debriefer.md +55 -0
  8. package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +2 -1
  9. package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +97 -7
  10. package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +4 -2
  11. package/dist/vendor/harness-opencode/dist/agents/prompts/scoper.md +129 -0
  12. package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.md +0 -1
  13. package/dist/vendor/harness-opencode/dist/agents/prompts/spec-reviewer.open.md +0 -1
  14. package/dist/vendor/harness-opencode/dist/autopilot/prompt-template.md +69 -45
  15. package/dist/vendor/harness-opencode/dist/chunk-GCWHRUOK.js +259 -0
  16. package/dist/vendor/harness-opencode/dist/chunk-MJSMBY2Y.js +87 -0
  17. package/dist/vendor/harness-opencode/dist/chunk-NIFAVPNN.js +544 -0
  18. package/dist/vendor/harness-opencode/dist/cli.js +448 -503
  19. package/dist/vendor/harness-opencode/dist/index.js +90 -14
  20. package/dist/vendor/harness-opencode/dist/loop-session-J35NILUZ.js +30 -0
  21. package/dist/vendor/harness-opencode/dist/opencode-server-KPCDFYAX.js +22 -0
  22. package/dist/vendor/harness-opencode/dist/plan-parser-TMHEKT22.js +6 -0
  23. package/dist/vendor/harness-opencode/dist/plan-session-7VS32P52.js +117 -0
  24. package/dist/vendor/harness-opencode/dist/scoper-S77SOK7X.js +326 -0
  25. package/dist/vendor/harness-opencode/dist/skills/spear-protocol/SKILL.md +2 -1
  26. package/dist/vendor/harness-opencode/package.json +1 -1
  27. package/package.json +3 -1
  28. package/dist/vendor/harness-opencode/dist/bin/plan-check.sh +0 -255
@@ -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 command2,
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(command3) {
1103
+ function cmd(command4) {
1097
1104
  try {
1098
- return execSync(command3, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
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/bin/plan-check.ts
1236
- import { execFileSync } from "child_process";
1237
- import { fileURLToPath as fileURLToPath2 } from "url";
1238
- import { dirname as dirname3, join as join5 } from "path";
1239
- function planCheck(args) {
1240
- const here = dirname3(fileURLToPath2(import.meta.url));
1241
- const candidates = [
1242
- join5(here, "plan-check.sh"),
1243
- // dev: src/bin/plan-check.sh
1244
- join5(here, "bin", "plan-check.sh")
1245
- // dist: dist/ → dist/bin/plan-check.sh
1246
- ];
1247
- let scriptPath;
1248
- for (const p of candidates) {
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
- execFileSync("test", ["-f", p]);
1251
- scriptPath = p;
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
- if (!scriptPath) {
1257
- console.error("plan-check: could not find plan-check.sh");
1258
- process.exit(2);
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
- execFileSync("bash", [scriptPath, ...args], {
1262
- stdio: "inherit",
1263
- encoding: "utf8"
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
- } catch (e) {
1266
- process.exit(e.status ?? 1);
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/cli.ts
1386
- import { command, option, positional, string as stringType, optional, number as numberType } from "cmd-ts";
1387
-
1388
- // src/autopilot/loop.ts
1389
- import { execFile as execFileCb } from "child_process";
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 createSession(client, opts) {
1433
- const session = await client.session.create({
1434
- body: {
1435
- directory: opts.cwd,
1436
- ...opts.agentName ? { agentID: opts.agentName } : {}
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
- return session.id;
1440
- }
1441
- async function sendAndWait(client, opts) {
1442
- const stallMs = opts.stallMs ?? 60 * 60 * 1e3;
1443
- await client.session.chat({
1444
- sessionID: opts.sessionId,
1445
- body: { content: [{ type: "text", text: opts.message }] }
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
- return waitForIdle(client, {
1448
- sessionId: opts.sessionId,
1449
- stallMs,
1450
- abortSignal: opts.abortSignal
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
- async function waitForIdle(client, opts) {
1454
- const stallMs = opts.stallMs ?? 60 * 60 * 1e3;
1455
- return new Promise((resolve2) => {
1456
- let stallTimer = null;
1457
- let unsubscribe = null;
1458
- let settled = false;
1459
- const settle = (result) => {
1460
- if (settled) return;
1461
- settled = true;
1462
- if (stallTimer) clearTimeout(stallTimer);
1463
- if (unsubscribe) unsubscribe();
1464
- resolve2(result);
1465
- };
1466
- const resetStall = () => {
1467
- if (stallTimer) clearTimeout(stallTimer);
1468
- stallTimer = setTimeout(() => settle({ kind: "stall", stallMs }), stallMs);
1469
- };
1470
- if (opts.abortSignal) {
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
- resetStall();
1478
- const stream = client.event.subscribe();
1479
- let streamDone = false;
1480
- (async () => {
1481
- try {
1482
- for await (const event of stream) {
1483
- if (settled) break;
1484
- const props = event.properties ?? {};
1485
- const eventSessionId = props["sessionID"];
1486
- if (eventSessionId !== opts.sessionId) continue;
1487
- resetStall();
1488
- const type = event.type ?? "";
1489
- if (type === "session.idle") {
1490
- settle({ kind: "idle" });
1491
- break;
1492
- }
1493
- if (type === "session.error") {
1494
- const msg = props["message"] ?? "session error";
1495
- settle({ kind: "error", message: msg });
1496
- break;
1497
- }
1498
- }
1499
- } catch (err) {
1500
- if (!settled) {
1501
- settle({ kind: "error", message: err instanceof Error ? err.message : String(err) });
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
- } finally {
1504
- streamDone = true;
1564
+ continue;
1505
1565
  }
1506
- })();
1507
- unsubscribe = () => {
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 checkProgress(cwd, baseRef) {
1598
- try {
1599
- const { stdout } = await execFile3("git", ["diff", "--stat", baseRef], { cwd });
1600
- return stdout.trim().length > 0;
1601
- } catch {
1602
- return true;
1603
- }
1604
- }
1605
- async function getHeadSha(cwd) {
1606
- try {
1607
- const { stdout } = await execFile3("git", ["rev-parse", "HEAD"], { cwd });
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
- for (let iteration = 1; iteration <= maxIterations; iteration++) {
1636
- if (checkKillSwitch(opts.cwd)) {
1637
- return {
1638
- exitReason: "kill-switch",
1639
- iterations: iteration - 1,
1640
- message: `Kill switch active (.agent/autopilot-disable exists). Stopping after ${iteration - 1} iteration(s).`
1641
- };
1642
- }
1643
- if (Date.now() - startTime >= timeoutMs) {
1644
- return {
1645
- exitReason: "timeout",
1646
- iterations: iteration - 1,
1647
- message: `Total timeout (${timeoutMs}ms) exceeded after ${iteration - 1} iteration(s).`
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
- if (result.kind === "abort") {
1658
- return {
1659
- exitReason: "timeout",
1660
- iterations: iteration,
1661
- message: `Aborted after ${iteration} iteration(s) (total timeout exceeded).`
1662
- };
1663
- }
1664
- if (result.kind === "stall") {
1665
- return {
1666
- exitReason: "stall",
1667
- iterations: iteration,
1668
- message: `Iteration ${iteration} stalled for ${result.stallMs}ms with no idle signal.`
1669
- };
1670
- }
1671
- if (result.kind === "error") {
1672
- return {
1673
- exitReason: "error",
1674
- iterations: iteration,
1675
- message: `Error in iteration ${iteration}: ${result.message}`
1676
- };
1677
- }
1678
- const lastMessage = await _getLastAssistantMessage(server.client, sessionId);
1679
- if (detectSentinel(lastMessage)) {
1680
- return {
1681
- exitReason: "sentinel",
1682
- iterations: iteration,
1683
- message: `Agent emitted <autopilot-done> at iteration ${iteration}.`
1684
- };
1685
- }
1686
- const madeProgress = await checkProgress(opts.cwd, headBefore);
1687
- struggle.record(madeProgress);
1688
- if (struggle.isStruggling()) {
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
- exitReason: "struggle",
1691
- iterations: iteration,
1692
- message: `Agent made no filesystem progress for ${struggleThreshold} consecutive iteration(s). Stopping at iteration ${iteration}.`
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/cli.ts
1708
- var autopilotCmd = command({
1728
+ // src/autopilot/autopilot-cmd.ts
1729
+ var autopilotInteractiveCmd = command2({
1709
1730
  name: "autopilot",
1710
- description: "Run the Ralph loop: send a prompt to PRIME repeatedly until it emits <autopilot-done> or a budget is exhausted.",
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
- prompt: positional({
1713
- type: stringType,
1714
- displayName: "prompt",
1715
- description: "The prompt to send to PRIME each iteration (e.g. a Linear issue ref or free-form task)."
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 ({ prompt, maxIterations, timeout }) => {
1729
- const cwd = process.cwd();
1730
- process.stdout.write("\n\x1B[1mAutopilot \u2014 Ralph loop\x1B[0m\n");
1731
- process.stdout.write(`Prompt: ${prompt.slice(0, 80)}${prompt.length > 80 ? "..." : ""}
1732
- `);
1733
- process.stdout.write(`Max iterations: ${maxIterations ?? MAX_ITERATIONS}
1734
- `);
1735
- process.stdout.write(`Timeout: ${((timeout ?? TIMEOUT_MS) / 36e5).toFixed(1)}h
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 fileURLToPath3 } from "url";
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(fileURLToPath3(import.meta.url));
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 = command2({
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: flag({
1940
+ dryRun: flag2({
1947
1941
  long: "dry-run",
1948
1942
  description: "Preview changes without writing."
1949
1943
  }),
1950
- pin: flag({
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 = command2({
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: flag({
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 = command2({
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 planCheckCmd = command2({
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: flag({
1978
+ dryRun: flag2({
2036
1979
  long: "dry-run",
2037
1980
  description: "Preview changes without writing."
2038
1981
  }),
2039
- pin: flag({
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
- "plan-check": planCheckCmd,
2058
- "plan-dir": planDirCmd,
2059
- autopilot: autopilotCmd
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();