@ordewell/cli 0.4.23 → 0.5.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 (2) hide show
  1. package/dist/main.js +1204 -437
  2. package/package.json +3 -3
package/dist/main.js CHANGED
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/main.ts
27
- var import_core19 = require("@ordewell/core");
27
+ var import_core20 = require("@ordewell/core");
28
28
 
29
29
  // src/utils/env.ts
30
30
  var import_fs = require("fs");
@@ -92,6 +92,9 @@ Planning:
92
92
  ordewell stop Stop execution of the last session (or --server)
93
93
  ordewell status List recent sessions (use --session-id for detail)
94
94
  ordewell sessions list|load|delete Manage named sessions
95
+ ordewell fork Continue in a copy of the conversation and its tasks
96
+ ordewell compact Condense the conversation into a summary (tasks are kept)
97
+ ordewell rewind [<n>] Rewind the conversation to before message <n> (no <n>: list them)
95
98
 
96
99
  Tasks (<id> is an order number or a task ID):
97
100
  ordewell add-task --title "..." Add a task to the current plan
@@ -598,6 +601,83 @@ var ApiClient = class {
598
601
  }
599
602
  return res.data;
600
603
  }
604
+ /** Copy the conversation and its tasks into a new session the daemon has already adopted. */
605
+ async forkConversation(sessionId) {
606
+ const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/fork`);
607
+ if (res.status !== 200) {
608
+ throw new Error(res.data?.error || "Fork failed");
609
+ }
610
+ return { sessionId: res.data.sessionId, goal: res.data.goal, plan: res.data.plan };
611
+ }
612
+ async rewindTargets(sessionId) {
613
+ const res = await this.httpRequest("GET", `/api/plans/${sessionId}/conversation/rewind-targets`);
614
+ if (res.status !== 200) {
615
+ throw new Error(res.data?.error || "Failed to list rewind targets");
616
+ }
617
+ return res.data.targets;
618
+ }
619
+ /** Cut the conversation back to just before the user message at `index` (a transcript position). */
620
+ async rewindConversation(sessionId, index) {
621
+ const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/rewind`, { index });
622
+ if (res.status !== 200) {
623
+ throw new Error(res.data?.error || "Rewind failed");
624
+ }
625
+ return res.data.plan;
626
+ }
627
+ /**
628
+ * Condense the conversation into a summary. One planner call, so it can take
629
+ * as long as a reply; a refusal or failure leaves the conversation as it was.
630
+ */
631
+ async compactConversation(sessionId) {
632
+ const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/compact`);
633
+ if (res.status !== 200) {
634
+ throw new Error(res.data?.error || "Compaction failed");
635
+ }
636
+ return { plan: res.data.plan, summary: res.data.summary, keptMessages: res.data.keptMessages };
637
+ }
638
+ async reviewRunDiff(sessionId) {
639
+ const res = await this.httpRequest("GET", `/api/plans/${sessionId}/isolation/diff`);
640
+ if (res.status !== 200) {
641
+ throw new Error(res.data?.error || "Could not read the diff");
642
+ }
643
+ return res.data.diff;
644
+ }
645
+ /**
646
+ * A conflict or a block is an outcome, not an error. Passed on whole: which
647
+ * repos blocked the merge, or landed before it stopped, is the answer.
648
+ */
649
+ async mergeRun(sessionId) {
650
+ const res = await this.httpRequest("POST", `/api/plans/${sessionId}/isolation/merge`);
651
+ if (res.status !== 200) {
652
+ throw new Error(res.data?.error || "Merge failed");
653
+ }
654
+ return res.data;
655
+ }
656
+ discardRun(sessionId) {
657
+ return this.isolationAction(sessionId, "discard", "Discard failed");
658
+ }
659
+ cleanupRun(sessionId) {
660
+ return this.isolationAction(sessionId, "cleanup", "Cleanup failed");
661
+ }
662
+ continueWithStash(sessionId) {
663
+ return this.isolationAction(sessionId, "stash-and-continue", "Could not stash and continue");
664
+ }
665
+ continueWithoutIsolation(sessionId) {
666
+ return this.isolationAction(sessionId, "run-without", "Could not continue without isolation");
667
+ }
668
+ async isolationAction(sessionId, segment, failure) {
669
+ const res = await this.httpRequest("POST", `/api/plans/${sessionId}/isolation/${segment}`);
670
+ if (res.status !== 200) {
671
+ throw new Error(res.data?.error || failure);
672
+ }
673
+ }
674
+ async resolveConflictAsTask(sessionId, taskId) {
675
+ const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/resolve-conflict`);
676
+ if (res.status !== 200) {
677
+ throw new Error(res.data?.error || "Could not add a resolver task");
678
+ }
679
+ return res.data.plan;
680
+ }
601
681
  async closeSession(sessionId) {
602
682
  const res = await this.httpRequest("POST", `/api/sessions/${sessionId}/close`);
603
683
  return res.data;
@@ -676,7 +756,7 @@ var ApiClient = class {
676
756
  try {
677
757
  const event = JSON.parse(data.toString());
678
758
  onEvent(event);
679
- if ((event.type === "execution_complete" || event.type === "execution_stopped") && !resolved) {
759
+ if ((event.type === "execution_complete" || event.type === "execution_stopped" || event.type === "isolation_blocked") && !resolved) {
680
760
  resolved = true;
681
761
  socket.close();
682
762
  resolve3();
@@ -1176,6 +1256,96 @@ ${lastPlannerMessage(plan)}
1176
1256
  }
1177
1257
  }
1178
1258
 
1259
+ // src/isolation.ts
1260
+ var import_core7 = require("@ordewell/core");
1261
+ var STATE_OF = {
1262
+ active: "active",
1263
+ merged: "integrated",
1264
+ conflict: "conflict",
1265
+ // A failed integration keeps its refs exactly as a failed verdict does; the
1266
+ // user sees one thing: work that did not land and can be looked at.
1267
+ kept: "kept",
1268
+ failed: "kept"
1269
+ };
1270
+ function reposOf(run) {
1271
+ if (Array.isArray(run.repos)) {
1272
+ const repos = run.repos.filter((r) => r && typeof r.path === "string" && typeof r.integrationBranch === "string" && typeof r.baseRef === "string");
1273
+ return repos.length > 0 ? repos.map((r) => ({ path: String(r.path), integrationBranch: String(r.integrationBranch), baseRef: String(r.baseRef) })) : null;
1274
+ }
1275
+ if (typeof run.integrationBranch === "string" && typeof run.baseRef === "string") {
1276
+ return [{ path: ".", integrationBranch: run.integrationBranch, baseRef: run.baseRef }];
1277
+ }
1278
+ return null;
1279
+ }
1280
+ function changedRepos(record, legacy, state) {
1281
+ if (legacy) return state === "integrated" ? ["."] : [];
1282
+ return Object.entries(record.repos ?? {}).filter(([, r]) => r?.changed === true).map(([path]) => path);
1283
+ }
1284
+ function isolationOfPlan(plan) {
1285
+ const run = plan?.isolation?.run;
1286
+ const repos = run ? reposOf(run) : null;
1287
+ if (!run || !repos) return null;
1288
+ const legacy = !Array.isArray(run.repos);
1289
+ const records = Object.entries(run.tasks ?? {});
1290
+ const tasks = {};
1291
+ const landed = [];
1292
+ for (const [taskId, record] of records) {
1293
+ const state = STATE_OF[String(record.status)];
1294
+ if (!state) continue;
1295
+ const changed = changedRepos(record, legacy, state);
1296
+ tasks[taskId] = {
1297
+ state,
1298
+ branch: String(record.branch ?? ""),
1299
+ worktree: String(record.workspace ?? record.worktree ?? ""),
1300
+ repos: changed,
1301
+ ...typeof record.conflictRepo === "string" ? { conflictRepo: record.conflictRepo } : legacy && state === "conflict" ? { conflictRepo: "." } : {}
1302
+ };
1303
+ if (state === "integrated") {
1304
+ landed.push({ taskId, order: Number(record.order ?? 0), title: String(record.title ?? taskId), changed });
1305
+ }
1306
+ }
1307
+ landed.sort((a, b) => a.order - b.order);
1308
+ const entry = ({ taskId, order, title }) => ({ taskId, order, title });
1309
+ return {
1310
+ handoff: {
1311
+ repos: repos.map((repo) => ({ ...repo, landed: landed.filter((t) => t.changed.includes(repo.path)).map(entry) })),
1312
+ landed: landed.map(entry)
1313
+ },
1314
+ tasks
1315
+ };
1316
+ }
1317
+ function handoffBranch(handoff) {
1318
+ return [...new Set(handoff.repos.map((r) => r.integrationBranch))].join(", ");
1319
+ }
1320
+ function handoffBase(handoff, length) {
1321
+ const group = isRepoGroup(handoff);
1322
+ return handoff.repos.map((r) => `${group ? `${r.path} ` : ""}${r.baseRef.slice(0, length)}`).join(", ");
1323
+ }
1324
+ function isRepoGroup(handoff) {
1325
+ return handoff.repos.some((r) => r.path !== ".");
1326
+ }
1327
+ function reposWithWork(handoff) {
1328
+ const withWork = handoff.repos.filter((r) => r.landed.length > 0);
1329
+ return (withWork.length > 0 ? withWork : handoff.repos).map((r) => r.path);
1330
+ }
1331
+ function repoResultLines(handoff) {
1332
+ return handoff.repos.map(({ path, landed }) => {
1333
+ const n = landed.length;
1334
+ return `${path}: ${n === 0 ? "nothing to merge" : `${n} task${n === 1 ? "" : "s"} landed`}`;
1335
+ });
1336
+ }
1337
+ function taskRepoNames(isolation) {
1338
+ return (isolation?.repos ?? []).filter((r) => r !== ".");
1339
+ }
1340
+ function mergeOutcome(result, branch, group) {
1341
+ const ok = result.outcome === "merged";
1342
+ if (!group && result.outcome === "conflict") {
1343
+ return { ok, message: `Merging ${branch} conflicted, so it was aborted \u2014 your tree is as it was. Merge it with git and resolve the conflict there.` };
1344
+ }
1345
+ const { message } = (0, import_core7.describeMergeResult)(result, branch, group);
1346
+ return { ok, message: ok || !group ? message : `${message} Each repository's ${branch} is a plain branch you can merge by hand.` };
1347
+ }
1348
+
1179
1349
  // src/utils/output.ts
1180
1350
  function iconFor(status) {
1181
1351
  switch (status) {
@@ -1193,7 +1363,98 @@ function iconFor(status) {
1193
1363
  }
1194
1364
 
1195
1365
  // src/commands/run.ts
1196
- var import_core7 = require("@ordewell/core");
1366
+ var import_core8 = require("@ordewell/core");
1367
+
1368
+ // src/catalog.ts
1369
+ function normalizeCatalog(result) {
1370
+ const runnersByModel = /* @__PURE__ */ new Map();
1371
+ for (const [runner, entries] of Object.entries(result.modelsByRunner ?? {})) {
1372
+ for (const entry of entries ?? []) {
1373
+ const id = String(entry.modelId ?? entry.id);
1374
+ runnersByModel.set(id, [.../* @__PURE__ */ new Set([...runnersByModel.get(id) ?? [], runner])]);
1375
+ }
1376
+ }
1377
+ const models = (result.models ?? []).map((m) => {
1378
+ const id = String(m.modelId ?? m.id);
1379
+ return {
1380
+ id,
1381
+ label: String(m.modelLabel ?? m.label ?? id),
1382
+ provider: String(m.runnerProviderLabel ?? m.runnerProvider ?? m.provider ?? ""),
1383
+ pricing: m.pricing ? `$${m.pricing}/MTok` : void 0,
1384
+ variants: Array.isArray(m.variants) ? m.variants.map((v) => ({ id: String(v.id), label: String(v.label ?? v.id) })) : [],
1385
+ runners: runnersByModel.get(id) ?? []
1386
+ };
1387
+ });
1388
+ const orchestratorModels = (result.orchestratorModels ?? []).map((m) => ({
1389
+ id: String(m.id),
1390
+ label: String(m.label ?? m.id),
1391
+ provider: String(m.provider ?? ""),
1392
+ pricing: m.pricing ? `$${m.pricing}/MTok` : void 0
1393
+ }));
1394
+ return {
1395
+ models,
1396
+ orchestratorModels,
1397
+ providers: result.providers ?? [],
1398
+ providerErrors: result.providerErrors ?? {},
1399
+ modesByRunner: Object.fromEntries(
1400
+ Object.entries(result.modesByRunner ?? {}).map(([runner, modes]) => [
1401
+ runner,
1402
+ (modes ?? []).map((m) => ({
1403
+ id: String(m.id),
1404
+ label: String(m.label ?? m.id),
1405
+ description: m.description ? String(m.description) : void 0,
1406
+ autonomous: m.autonomous ? true : void 0
1407
+ }))
1408
+ ])
1409
+ )
1410
+ };
1411
+ }
1412
+
1413
+ // src/commands/shared.ts
1414
+ async function connect(subArgs, injected) {
1415
+ return injected ?? new ApiClient(await ensureDaemon(resolvePort(subArgs)));
1416
+ }
1417
+ async function fetchCatalog(api) {
1418
+ return normalizeCatalog(await api.getModels());
1419
+ }
1420
+ function fail(...lines) {
1421
+ for (const line of lines) console.error(line);
1422
+ process.exit(1);
1423
+ }
1424
+ function writeEnv(env) {
1425
+ const envFile = findEnvFile();
1426
+ for (const [key, value] of Object.entries(env)) {
1427
+ writeEnvVar(envFile, key, value);
1428
+ process.env[key] = value;
1429
+ }
1430
+ }
1431
+ async function persistEnv(api, env) {
1432
+ const settings = await api.updateSettings({ env });
1433
+ writeEnv(env);
1434
+ return settings;
1435
+ }
1436
+ function toTaskView(task) {
1437
+ return {
1438
+ id: task.id,
1439
+ order: task.order,
1440
+ title: task.title,
1441
+ description: task.description,
1442
+ prompt: task.prompt ?? void 0,
1443
+ type: task.type === "user" ? "user" : "ai",
1444
+ status: task.status ?? "pending",
1445
+ dependencies: task.dependencies ?? [],
1446
+ assignedRunner: task.assignedRunner || void 0,
1447
+ taskMode: task.taskMode || void 0,
1448
+ assignedModel: task.assignedModel ?? void 0,
1449
+ subtasks: [...task.subtasks ?? []].sort((a, b) => a.order - b.order).map(toTaskView)
1450
+ };
1451
+ }
1452
+ function taskViews(plan) {
1453
+ const tasks = allTasksOf(plan);
1454
+ return [...tasks].sort((a, b) => a.order - b.order).map(toTaskView);
1455
+ }
1456
+
1457
+ // src/commands/run.ts
1197
1458
  function resolveSessionId(subArgs) {
1198
1459
  const explicit = flag(subArgs, "--session-id");
1199
1460
  if (explicit) return explicit;
@@ -1206,7 +1467,9 @@ function resolveSessionId(subArgs) {
1206
1467
  console.error(`Using last session: ${last.sessionId} ("${last.goal}")`);
1207
1468
  return last.sessionId;
1208
1469
  }
1209
- async function followExecution(api, sessionId) {
1470
+ async function followExecution(api, sessionId, start, onBlocked) {
1471
+ let blocked = null;
1472
+ let dirtyRepos = [];
1210
1473
  const taskStates = /* @__PURE__ */ new Map();
1211
1474
  let lastPrinted = "";
1212
1475
  function printStatus(tasks) {
@@ -1230,55 +1493,87 @@ async function followExecution(api, sessionId) {
1230
1493
  lastPrinted = output;
1231
1494
  }
1232
1495
  }
1233
- try {
1234
- await api.streamExecution(sessionId, (event) => {
1235
- if (event.type === "task_started") {
1236
- process.stderr.write(`[${event.order}/${event.title}] Started: ${event.runner} / ${event.modelId}
1496
+ let settleReady = () => {
1497
+ };
1498
+ const streamReady = new Promise((resolve3, reject) => {
1499
+ settleReady = (error) => error ? reject(error) : resolve3();
1500
+ });
1501
+ const stream = api.streamExecution(sessionId, (event) => {
1502
+ if (event.type === "task_started") {
1503
+ process.stderr.write(`[${event.order}/${event.title}] Started: ${event.runner} / ${event.modelId}
1237
1504
  `);
1238
- }
1239
- if (event.type === "checkpoint") {
1240
- process.stderr.write(`\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core7.truncateCheckpointSummary)(event.summary)}
1505
+ }
1506
+ if (event.type === "checkpoint") {
1507
+ process.stderr.write(`\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core8.truncateCheckpointSummary)(event.summary)}
1241
1508
  `);
1242
- }
1243
- if (event.type === "status_update" && event.tasks) {
1244
- printStatus(event.tasks);
1245
- }
1246
- if (event.type === "review_needed") {
1247
- console.log("\nPlan needs your sign-off \u2014 run `ordewell approve` to continue.");
1248
- }
1249
- if (event.type === "execution_complete") {
1250
- const s = event;
1251
- const summary = s.summary || { total: 0 };
1252
- console.log(
1253
- `
1509
+ }
1510
+ if (event.type === "status_update" && event.tasks) {
1511
+ printStatus(event.tasks);
1512
+ }
1513
+ if (event.type === "review_needed") {
1514
+ console.log("\nPlan needs your sign-off \u2014 run `ordewell approve` to continue.");
1515
+ }
1516
+ if (event.type === "execution_complete") {
1517
+ const s = event;
1518
+ const summary = s.summary || { total: 0 };
1519
+ console.log(
1520
+ `
1254
1521
  Done. ${summary.completed || 0} completed, ${summary.failed || 0} failed, ${summary.blocked ?? summary.total - (summary.completed || 0) - (summary.failed || 0)} blocked.`
1255
- );
1256
- }
1257
- if (event.type === "execution_stopped") {
1258
- console.log("\nExecution stopped.");
1259
- }
1260
- });
1261
- } catch (err) {
1522
+ );
1523
+ }
1524
+ if (event.type === "execution_stopped") {
1525
+ console.log("\nExecution stopped.");
1526
+ }
1527
+ if (event.type === "notice") console.error(`\xB7 ${event.message}`);
1528
+ if (event.type === "isolation_blocked") {
1529
+ blocked = event.message;
1530
+ dirtyRepos = event.repos ?? [];
1531
+ }
1532
+ if (event.type === "isolation_handoff") {
1533
+ const n = event.landed.length;
1534
+ const where = isRepoGroup(event) ? ` in ${event.repos.map((r) => r.path).join(", ")}` : "";
1535
+ console.log(`
1536
+ Run finished on ${handoffBranch(event)}${where} \u2014 ${n} task${n === 1 ? "" : "s"} landed.`);
1537
+ if (isRepoGroup(event)) for (const line of repoResultLines(event)) console.log(` ${line}`);
1538
+ console.log(" `ordewell handoff review|merge|discard|cleanup` to land it.");
1539
+ }
1540
+ }, settleReady);
1541
+ const streamFailed = (err) => {
1262
1542
  console.error(`Execution stream error: ${err.message}`);
1263
1543
  process.exit(1);
1544
+ };
1545
+ void stream.catch(settleReady);
1546
+ await streamReady.catch(streamFailed);
1547
+ await start();
1548
+ await stream.catch(streamFailed);
1549
+ if (blocked === null) return;
1550
+ if (!onBlocked) {
1551
+ await api.stopExecution(sessionId);
1552
+ console.error(blocked);
1553
+ const where = dirtyRepos.length > 0 ? ` in ${dirtyRepos.join(", ")}` : "";
1554
+ console.error(`Nothing was started. Re-run with \`--stash\` to stash your tracked changes${where} first, or \`--without-isolation\` to run in your working tree this once.`);
1555
+ process.exit(1);
1264
1556
  }
1557
+ await followExecution(api, sessionId, () => onBlocked === "stash" ? api.continueWithStash(sessionId) : api.continueWithoutIsolation(sessionId));
1265
1558
  }
1266
- async function handleRun(subArgs) {
1559
+ async function handleRun(subArgs, injectedApi) {
1267
1560
  const sessionId = resolveSessionId(subArgs);
1268
- const api = new ApiClient(await ensureDaemon(resolvePort(subArgs)));
1561
+ const api = await connect(subArgs, injectedApi);
1269
1562
  console.error(`Executing plan...`);
1270
- try {
1271
- await api.executePlan(sessionId);
1272
- } catch (err) {
1273
- console.error(`Failed to start execution: ${err.message}`);
1274
- process.exit(1);
1275
- }
1276
- await followExecution(api, sessionId);
1563
+ const onBlocked = hasFlag(subArgs, "--stash") ? "stash" : hasFlag(subArgs, "--without-isolation") ? "shared" : void 0;
1564
+ await followExecution(api, sessionId, async () => {
1565
+ try {
1566
+ await api.executePlan(sessionId);
1567
+ } catch (err) {
1568
+ console.error(`Failed to start execution: ${err.message}`);
1569
+ process.exit(1);
1570
+ }
1571
+ }, onBlocked);
1277
1572
  }
1278
1573
 
1279
1574
  // src/commands/status.ts
1280
1575
  var import_fs4 = require("fs");
1281
- var import_core8 = require("@ordewell/core");
1576
+ var import_core9 = require("@ordewell/core");
1282
1577
  async function handleStatus(subArgs, injectedApi) {
1283
1578
  const sessionId = flag(subArgs, "--session-id");
1284
1579
  const workspace = flag(subArgs, "--workspace") || process.cwd();
@@ -1349,12 +1644,12 @@ function printOneSession(sessionId, meta, plan) {
1349
1644
  const icon = iconFor(t.status);
1350
1645
  const model = t.assignedModel ? ` (${t.assignedModel.modelLabel})` : "";
1351
1646
  const shortId = (t.id ?? "").slice(-8);
1352
- console.log(` ${(0, import_core8.taskOrderLabel)(t).padStart(2)}. ${icon} [${shortId}] ${t.title}${model}`);
1647
+ console.log(` ${(0, import_core9.taskOrderLabel)(t).padStart(2)}. ${icon} [${shortId}] ${t.title}${model}`);
1353
1648
  for (const sub of t.subtasks || []) {
1354
1649
  const subIcon = iconFor(sub.status);
1355
1650
  const subModel = sub.assignedModel ? ` (${sub.assignedModel.modelLabel})` : "";
1356
1651
  const subShortId = (sub.id ?? "").slice(-8);
1357
- console.log(` ${(0, import_core8.taskOrderLabel)(sub, t)}. ${subIcon} [${subShortId}] ${sub.title}${subModel}`);
1652
+ console.log(` ${(0, import_core9.taskOrderLabel)(sub, t)}. ${subIcon} [${subShortId}] ${sub.title}${subModel}`);
1358
1653
  }
1359
1654
  }
1360
1655
  }
@@ -1406,123 +1701,32 @@ async function handleWeb(subArgs) {
1406
1701
  }
1407
1702
 
1408
1703
  // src/commands/models.ts
1409
- var import_core10 = require("@ordewell/core");
1704
+ var import_core11 = require("@ordewell/core");
1410
1705
 
1411
1706
  // src/commands/model.ts
1412
- var import_core9 = require("@ordewell/core");
1413
-
1414
- // src/catalog.ts
1415
- function normalizeCatalog(result) {
1416
- const runnersByModel = /* @__PURE__ */ new Map();
1417
- for (const [runner, entries] of Object.entries(result.modelsByRunner ?? {})) {
1418
- for (const entry of entries ?? []) {
1419
- const id = String(entry.modelId ?? entry.id);
1420
- runnersByModel.set(id, [.../* @__PURE__ */ new Set([...runnersByModel.get(id) ?? [], runner])]);
1421
- }
1422
- }
1423
- const models = (result.models ?? []).map((m) => {
1424
- const id = String(m.modelId ?? m.id);
1425
- return {
1426
- id,
1427
- label: String(m.modelLabel ?? m.label ?? id),
1428
- provider: String(m.runnerProviderLabel ?? m.runnerProvider ?? m.provider ?? ""),
1429
- pricing: m.pricing ? `$${m.pricing}/MTok` : void 0,
1430
- variants: Array.isArray(m.variants) ? m.variants.map((v) => ({ id: String(v.id), label: String(v.label ?? v.id) })) : [],
1431
- runners: runnersByModel.get(id) ?? []
1432
- };
1433
- });
1434
- const orchestratorModels = (result.orchestratorModels ?? []).map((m) => ({
1435
- id: String(m.id),
1436
- label: String(m.label ?? m.id),
1437
- provider: String(m.provider ?? ""),
1438
- pricing: m.pricing ? `$${m.pricing}/MTok` : void 0
1439
- }));
1440
- return {
1441
- models,
1442
- orchestratorModels,
1443
- providers: result.providers ?? [],
1444
- providerErrors: result.providerErrors ?? {},
1445
- modesByRunner: Object.fromEntries(
1446
- Object.entries(result.modesByRunner ?? {}).map(([runner, modes]) => [
1447
- runner,
1448
- (modes ?? []).map((m) => ({
1449
- id: String(m.id),
1450
- label: String(m.label ?? m.id),
1451
- description: m.description ? String(m.description) : void 0,
1452
- autonomous: m.autonomous ? true : void 0
1453
- }))
1454
- ])
1455
- )
1456
- };
1457
- }
1458
-
1459
- // src/commands/shared.ts
1460
- async function connect(subArgs, injected) {
1461
- return injected ?? new ApiClient(await ensureDaemon(resolvePort(subArgs)));
1462
- }
1463
- async function fetchCatalog(api) {
1464
- return normalizeCatalog(await api.getModels());
1465
- }
1466
- function fail(...lines) {
1467
- for (const line of lines) console.error(line);
1468
- process.exit(1);
1469
- }
1470
- function writeEnv(env) {
1471
- const envFile = findEnvFile();
1472
- for (const [key, value] of Object.entries(env)) {
1473
- writeEnvVar(envFile, key, value);
1474
- process.env[key] = value;
1475
- }
1476
- }
1477
- async function persistEnv(api, env) {
1478
- const settings = await api.updateSettings({ env });
1479
- writeEnv(env);
1480
- return settings;
1481
- }
1482
- function toTaskView(task) {
1483
- return {
1484
- id: task.id,
1485
- order: task.order,
1486
- title: task.title,
1487
- description: task.description,
1488
- prompt: task.prompt ?? void 0,
1489
- type: task.type === "user" ? "user" : "ai",
1490
- status: task.status ?? "pending",
1491
- dependencies: task.dependencies ?? [],
1492
- assignedRunner: task.assignedRunner || void 0,
1493
- taskMode: task.taskMode || void 0,
1494
- assignedModel: task.assignedModel ?? void 0,
1495
- subtasks: [...task.subtasks ?? []].sort((a, b) => a.order - b.order).map(toTaskView)
1496
- };
1497
- }
1498
- function taskViews(plan) {
1499
- const tasks = allTasksOf(plan);
1500
- return [...tasks].sort((a, b) => a.order - b.order).map(toTaskView);
1501
- }
1502
-
1503
- // src/commands/model.ts
1504
- var USAGE = "Usage: ordewell model [show | set <model-id>]";
1505
- async function handleModel(subArgs, injectedApi) {
1506
- const [action, modelId] = positionals(subArgs);
1507
- if (action && action !== "show" && action !== "set") {
1508
- fail(`Unknown model action: "${action}"`, USAGE);
1509
- }
1510
- const api = await connect(subArgs, injectedApi);
1511
- if (action !== "set") {
1512
- await showModels(api);
1513
- return;
1514
- }
1515
- if (!modelId) fail(USAGE);
1516
- const settings = await api.getSettings();
1517
- const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
1518
- const runner = (0, import_core9.runnerForProvider)(provider);
1519
- if (runner) {
1520
- const known = (await fetchCatalog(api)).models.filter((m) => m.runners?.includes(runner));
1521
- if (known.length > 0 && !known.some((m) => m.id === modelId)) {
1522
- fail(
1523
- `${modelId} was not discovered for ${runner}.`,
1524
- `Run \`ordewell model\` to see what ${runner} offers.`
1525
- );
1707
+ var import_core10 = require("@ordewell/core");
1708
+ var USAGE = "Usage: ordewell model [show | set <model-id>]";
1709
+ async function handleModel(subArgs, injectedApi) {
1710
+ const [action, modelId] = positionals(subArgs);
1711
+ if (action && action !== "show" && action !== "set") {
1712
+ fail(`Unknown model action: "${action}"`, USAGE);
1713
+ }
1714
+ const api = await connect(subArgs, injectedApi);
1715
+ if (action !== "set") {
1716
+ await showModels(api);
1717
+ return;
1718
+ }
1719
+ if (!modelId) fail(USAGE);
1720
+ const settings = await api.getSettings();
1721
+ const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
1722
+ const runner = (0, import_core10.runnerForProvider)(provider);
1723
+ if (runner) {
1724
+ const known = (await fetchCatalog(api)).models.filter((m) => m.runners?.includes(runner));
1725
+ if (known.length > 0 && !known.some((m) => m.id === modelId)) {
1726
+ fail(
1727
+ `${modelId} was not discovered for ${runner}.`,
1728
+ `Run \`ordewell model\` to see what ${runner} offers.`
1729
+ );
1526
1730
  }
1527
1731
  }
1528
1732
  await api.updateSettings({ orchestratorModel: modelId });
@@ -1534,7 +1738,7 @@ async function showModels(api) {
1534
1738
  const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
1535
1739
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
1536
1740
  const current = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
1537
- const runner = (0, import_core9.runnerForProvider)(provider);
1741
+ const runner = (0, import_core10.runnerForProvider)(provider);
1538
1742
  const options = runner ? catalog.models.filter((m) => m.runners?.includes(runner)) : catalog.orchestratorModels;
1539
1743
  console.log(`
1540
1744
  Planner model: ${current || "(backend default)"}`);
@@ -1568,8 +1772,8 @@ async function handleModels(subArgs) {
1568
1772
  const rest = subArgs.filter((_, i) => i !== setIdx && i !== setIdx + 1);
1569
1773
  return handleModel(["set", subArgs[setIdx + 1], ...rest]);
1570
1774
  }
1571
- const config = new import_core10.EnvConfig();
1572
- const providers = (0, import_core10.configuredProviders)(config);
1775
+ const config = new import_core11.EnvConfig();
1776
+ const providers = (0, import_core11.configuredProviders)(config);
1573
1777
  if (providers.length === 0) {
1574
1778
  console.error("No providers configured. Set an API key for a provider, e.g.:");
1575
1779
  console.error(" OPENROUTER_API_KEY=... (OpenRouter)");
@@ -1578,19 +1782,19 @@ async function handleModels(subArgs) {
1578
1782
  console.error(" OPENAI_COMPATIBLE_BASE_URL=... (self-hosted / OpenAI-compatible)");
1579
1783
  process.exit(1);
1580
1784
  }
1581
- const { apiKeys, baseUrls } = (0, import_core10.collectProviderCredentials)(config);
1785
+ const { apiKeys, baseUrls } = (0, import_core11.collectProviderCredentials)(config);
1582
1786
  let models;
1583
1787
  let errors;
1584
1788
  try {
1585
- const result = await (0, import_core10.fetchAllProviderModels)({ apiKeys, baseUrls });
1586
- models = (0, import_core10.toOrchestratorOptions)(result.models, import_core10.ORCHESTRATOR_SHORTCUTS);
1789
+ const result = await (0, import_core11.fetchAllProviderModels)({ apiKeys, baseUrls });
1790
+ models = (0, import_core11.toOrchestratorOptions)(result.models, import_core11.ORCHESTRATOR_SHORTCUTS);
1587
1791
  errors = result.errors;
1588
1792
  } catch (err) {
1589
1793
  console.error(`Failed to fetch models: ${err instanceof Error ? err.message : String(err)}`);
1590
1794
  process.exit(1);
1591
1795
  return;
1592
1796
  }
1593
- const providerLabel = (p) => (0, import_core10.getProviderMeta)(p)?.label ?? p;
1797
+ const providerLabel = (p) => (0, import_core11.getProviderMeta)(p)?.label ?? p;
1594
1798
  console.log(
1595
1799
  `
1596
1800
  Available models (${models.length}) across ${providers.length} configured provider(s):`
@@ -1707,9 +1911,9 @@ function handleSetup(subArgs) {
1707
1911
  }
1708
1912
 
1709
1913
  // src/commands/plugins.ts
1710
- var import_core11 = require("@ordewell/core");
1914
+ var import_core12 = require("@ordewell/core");
1711
1915
  function handlePlugins(subArgs) {
1712
- const registry = new import_core11.RunnerRegistry();
1916
+ const registry = new import_core12.RunnerRegistry();
1713
1917
  registry.loadUserPlugins();
1714
1918
  const sub = subArgs[0];
1715
1919
  if (sub === "list" || !sub) {
@@ -1745,7 +1949,7 @@ ${plugins.length} plugin(s):
1745
1949
  process.exit(1);
1746
1950
  }
1747
1951
  try {
1748
- const resolved = (0, import_core11.classifyPluginSource)(source);
1952
+ const resolved = (0, import_core12.classifyPluginSource)(source);
1749
1953
  const manifest = resolved.kind === "git" ? registry.installFromGit(resolved.url) : registry.installFromPath(resolved.path);
1750
1954
  console.log(`Installed plugin: ${manifest.name} v${manifest.version}`);
1751
1955
  } catch (err) {
@@ -2132,15 +2336,477 @@ async function handleSessions(subArgs, injectedApi) {
2132
2336
  console.error(`Failed to delete session: ${err.message}`);
2133
2337
  process.exit(1);
2134
2338
  }
2135
- return;
2339
+ return;
2340
+ }
2341
+ console.error(`Unknown sessions action: "${action}"`);
2342
+ console.error(USAGE3);
2343
+ process.exit(1);
2344
+ }
2345
+
2346
+ // src/commands/conversation.ts
2347
+ var REWIND_USAGE = "Usage: ordewell rewind [<n>] [--session-id <id>] [--workspace /path] \u2014 with no <n>, lists the messages you can rewind to.";
2348
+ async function adopted(subArgs, injectedApi) {
2349
+ const workspace = flag(subArgs, "--workspace") || process.cwd();
2350
+ const sessionId = resolveSessionId(subArgs);
2351
+ const api = await connect(subArgs, injectedApi);
2352
+ let plan;
2353
+ try {
2354
+ ({ plan } = await api.adoptSession(sessionId, workspace));
2355
+ } catch (err) {
2356
+ fail(`Failed to load session: ${err.message}`);
2357
+ }
2358
+ return { api, sessionId, workspace, plan };
2359
+ }
2360
+ async function handleFork(subArgs, injectedApi) {
2361
+ const { api, sessionId, workspace } = await adopted(subArgs, injectedApi);
2362
+ let fork;
2363
+ try {
2364
+ fork = await api.forkConversation(sessionId);
2365
+ } catch (err) {
2366
+ fail(`Failed to fork: ${err.message}`);
2367
+ }
2368
+ saveLastSession(fork.sessionId, fork.goal, fork.plan.runners ?? [], workspace);
2369
+ console.log(`Forked ${sessionId} into ${fork.sessionId} \u2014 the fork is now the current session.`);
2370
+ console.log(` \`ordewell sessions load ${sessionId}\` to go back to the original.`);
2371
+ }
2372
+ async function handleCompact(subArgs, injectedApi) {
2373
+ const { api, sessionId } = await adopted(subArgs, injectedApi);
2374
+ let result;
2375
+ try {
2376
+ result = await api.compactConversation(sessionId);
2377
+ } catch (err) {
2378
+ fail(`Failed to condense: ${err.message}`);
2379
+ }
2380
+ console.log(`Condensed ${sessionId}. The last two exchanges were kept as they were; the tasks are unchanged.
2381
+ `);
2382
+ console.log(result.summary);
2383
+ }
2384
+ async function handleRewind(subArgs, injectedApi) {
2385
+ const arg = positionals(subArgs)[0];
2386
+ if (arg !== void 0 && !/^\d+$/.test(arg)) fail(REWIND_USAGE);
2387
+ const { api, sessionId } = await adopted(subArgs, injectedApi);
2388
+ if (arg === void 0) {
2389
+ let targets;
2390
+ try {
2391
+ targets = await api.rewindTargets(sessionId);
2392
+ } catch (err) {
2393
+ fail(`Failed to list messages: ${err.message}`);
2394
+ }
2395
+ if (targets.length === 0) {
2396
+ console.log("Nothing to rewind to yet \u2014 the only message so far is the goal.");
2397
+ return;
2398
+ }
2399
+ console.log("Your messages, most recent first:\n");
2400
+ for (const t of [...targets].reverse()) console.log(` ${String(t.index).padStart(4)} ${t.preview}`);
2401
+ console.log("\n `ordewell rewind <n>` discards message <n> and everything after it. The tasks stay as they are.");
2402
+ return;
2403
+ }
2404
+ try {
2405
+ await api.rewindConversation(sessionId, Number(arg));
2406
+ } catch (err) {
2407
+ fail(`Failed to rewind: ${err.message}`);
2408
+ }
2409
+ console.log(`Rewound ${sessionId} to just before message ${arg}. The tasks are unchanged; the next message continues from there.`);
2410
+ }
2411
+
2412
+ // src/commands/handoff.ts
2413
+ var import_readline2 = require("readline");
2414
+
2415
+ // src/tui/ansi.ts
2416
+ var import_string_width = __toESM(require("string-width"));
2417
+ var ESCAPE = /\x1b(?:\][\s\S]*?(?:\x07|\x1b\\|$)|[P^_][\s\S]*?(?:\x1b\\|$)|\[[0-?]*[ -/]*[@-~]?|[ -/]*[0-~]?)/g;
2418
+ var ESCAPE_SPLIT = new RegExp(`(${ESCAPE.source})`);
2419
+ var CONTROL = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
2420
+ var CONTROL_IN_ROW = /[\x00-\x1f\x7f-\x9f]/g;
2421
+ var HAS_CONTROL = /[\x00-\x1f\x7f-\x9f]/;
2422
+ var PAINT_ESCAPE = /^\x1b\[[0-9;]*[mGK]$/;
2423
+ function stripAnsi(text) {
2424
+ return text.replace(ESCAPE, "");
2425
+ }
2426
+ function sanitize(text) {
2427
+ return text.replace(ESCAPE, "").replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(CONTROL, "");
2428
+ }
2429
+ function paintOnly(line) {
2430
+ if (!HAS_CONTROL.test(line)) return line;
2431
+ return line.split(ESCAPE_SPLIT).map((piece) => piece.startsWith("\x1B") ? PAINT_ESCAPE.test(piece) ? piece : "" : piece.replace(CONTROL_IN_ROW, "")).join("");
2432
+ }
2433
+ function width(text) {
2434
+ return (0, import_string_width.default)(stripAnsi(text));
2435
+ }
2436
+ function truncate(text, max) {
2437
+ if (max <= 0) return "";
2438
+ if (width(text) <= max) return text;
2439
+ let out = "";
2440
+ let used = 0;
2441
+ for (const char of stripAnsi(text)) {
2442
+ const w = (0, import_string_width.default)(char);
2443
+ if (used + w > max - 1) break;
2444
+ out += char;
2445
+ used += w;
2446
+ }
2447
+ return out + "\u2026";
2448
+ }
2449
+ function pad(text, target) {
2450
+ const current = width(text);
2451
+ if (current > target) return truncate(text, target);
2452
+ return text + " ".repeat(target - current);
2453
+ }
2454
+ function chopWord(piece, max) {
2455
+ const chunks = [];
2456
+ let cur = "";
2457
+ let curWidth = 0;
2458
+ for (const ch of piece) {
2459
+ const w = (0, import_string_width.default)(ch);
2460
+ if (cur && curWidth + w > max) {
2461
+ chunks.push(cur);
2462
+ cur = ch;
2463
+ curWidth = w;
2464
+ } else {
2465
+ cur += ch;
2466
+ curWidth += w;
2467
+ }
2468
+ }
2469
+ chunks.push(cur);
2470
+ return chunks;
2471
+ }
2472
+ var wrapMemo = /* @__PURE__ */ new Map();
2473
+ var WRAP_MEMO_MAX = 64;
2474
+ function wrapLines(text, max) {
2475
+ const key = `${max}\0${text}`;
2476
+ const cached = wrapMemo.get(key);
2477
+ if (cached) {
2478
+ wrapMemo.delete(key);
2479
+ wrapMemo.set(key, cached);
2480
+ return cached;
2481
+ }
2482
+ const result = computeWrapLines(text, max);
2483
+ if (wrapMemo.size >= WRAP_MEMO_MAX) {
2484
+ const oldest = wrapMemo.keys().next().value;
2485
+ if (oldest !== void 0) wrapMemo.delete(oldest);
2486
+ }
2487
+ wrapMemo.set(key, result);
2488
+ return result;
2489
+ }
2490
+ function computeWrapLines(text, max) {
2491
+ if (max <= 0) return [{ line: "", start: 0 }];
2492
+ const out = [];
2493
+ let pos = 0;
2494
+ for (const paragraph of text.split("\n")) {
2495
+ let current = "";
2496
+ let currentStart = pos;
2497
+ let first = true;
2498
+ let wordPos = pos;
2499
+ for (const word of paragraph.split(" ")) {
2500
+ let piece = word;
2501
+ let pieceStart = wordPos;
2502
+ if (width(piece) > max) {
2503
+ const chunks = chopWord(piece, max);
2504
+ for (let ci = 0; ci < chunks.length - 1; ci++) {
2505
+ const head = chunks[ci];
2506
+ if (!first) {
2507
+ out.push({ line: current, start: currentStart });
2508
+ current = "";
2509
+ }
2510
+ out.push({ line: head, start: pieceStart });
2511
+ pieceStart += head.length;
2512
+ first = true;
2513
+ }
2514
+ piece = chunks[chunks.length - 1];
2515
+ }
2516
+ const candidate = first ? piece : `${current} ${piece}`;
2517
+ const candidateStart = first ? pieceStart : currentStart;
2518
+ first = false;
2519
+ if (width(candidate) > max) {
2520
+ out.push({ line: current, start: currentStart });
2521
+ current = piece;
2522
+ currentStart = pieceStart;
2523
+ } else {
2524
+ current = candidate;
2525
+ currentStart = candidateStart;
2526
+ }
2527
+ wordPos += word.length + 1;
2528
+ }
2529
+ out.push({ line: current, start: currentStart });
2530
+ pos += paragraph.length + 1;
2531
+ }
2532
+ return out.length > 0 ? out : [{ line: "", start: 0 }];
2533
+ }
2534
+ function wrap(text, max) {
2535
+ return wrapLines(text, max).map((l) => l.line);
2536
+ }
2537
+ function colour(open) {
2538
+ return (text) => style.enabled ? `\x1B[${open}m${text}\x1B[0m` : text;
2539
+ }
2540
+ var style = {
2541
+ /** Set false when the output is piped or the terminal has no colour. */
2542
+ enabled: true,
2543
+ bold: colour("1"),
2544
+ dim: colour("2"),
2545
+ italic: colour("3"),
2546
+ underline: colour("4"),
2547
+ inverse: colour("7"),
2548
+ red: colour("31"),
2549
+ green: colour("32"),
2550
+ yellow: colour("33"),
2551
+ blue: colour("34"),
2552
+ magenta: colour("35"),
2553
+ cyan: colour("36"),
2554
+ grey: colour("90")
2555
+ };
2556
+
2557
+ // src/tui/transcript.ts
2558
+ function say(state, role, content, research, extra) {
2559
+ const message = {
2560
+ role,
2561
+ content: sanitize(content),
2562
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2563
+ ...research ? { research } : {},
2564
+ ...extra
2565
+ };
2566
+ return { ...state, messages: [...state.messages, message], scroll: 0 };
2567
+ }
2568
+
2569
+ // src/tui/handoff.ts
2570
+ var HANDOFF_ACTIONS = [
2571
+ { id: "review", label: "Review diff", hint: "the branch against the commit it forked from" },
2572
+ { id: "merge", label: "Merge", hint: "into the branch you have checked out \u2014 asks first" },
2573
+ { id: "discard", label: "Discard", hint: "the run, its worktrees and this branch \u2014 asks first" },
2574
+ { id: "cleanup", label: "Clean up", hint: "remove worktrees and task branches, keep this branch" }
2575
+ ];
2576
+ function handoffActions(handoff) {
2577
+ if (!isRepoGroup(handoff)) return HANDOFF_ACTIONS;
2578
+ return [
2579
+ { id: "review", label: "Review diff", hint: "each repository's branch against the commit it forked from" },
2580
+ { id: "merge", label: "Merge all", hint: "every repository's branch into what you have checked out there \u2014 asks first" },
2581
+ { id: "discard", label: "Discard", hint: "the run, its worktrees and every repository's branch \u2014 asks first" },
2582
+ { id: "cleanup", label: "Clean up", hint: "remove worktrees and task branches everywhere, keep the integration branches" }
2583
+ ];
2584
+ }
2585
+ var reposOf2 = (handoff) => handoff.repos.map((r) => r.path).join(", ");
2586
+ var HANDOFF_USAGE = `/handoff [${HANDOFF_ACTIONS.map((a) => a.id).join("|")}]`;
2587
+ var diffRoom = (rows) => Math.max(1, rows - 3);
2588
+ var fail2 = (state, content) => ({ state: say(state, "error", content), effects: [] });
2589
+ function handoffCommand(state, arg) {
2590
+ if (!state.sessionId || !state.handoff) return fail2(state, "No isolated run to hand off \u2014 a plan that ran in worktrees leaves one here.");
2591
+ if (arg === void 0) return { state: { ...state, overlay: { kind: "handoff", index: 0, diff: null } }, effects: [] };
2592
+ const action = HANDOFF_ACTIONS.find((a) => a.id === arg.toLowerCase());
2593
+ if (!action) return fail2(state, `Usage: ${HANDOFF_USAGE}`);
2594
+ return runHandoffAction(state, action.id);
2595
+ }
2596
+ function handoffArrived(state, handoff) {
2597
+ const landed = handoff.landed.length;
2598
+ const where = isRepoGroup(handoff) ? ` in ${reposOf2(handoff)}` : "";
2599
+ const told = say(
2600
+ { ...state, handoff },
2601
+ "system",
2602
+ `Run finished on ${handoffBranch(handoff)}${where} \u2014 ${landed} task${landed === 1 ? "" : "s"} landed. /handoff to review and land it.`
2603
+ );
2604
+ return state.overlay ? told : { ...told, overlay: { kind: "handoff", index: 0, diff: null } };
2605
+ }
2606
+ function runHandoffAction(state, id) {
2607
+ const { sessionId, handoff } = state;
2608
+ if (!sessionId || !handoff) return fail2(state, "No isolated run to hand off.");
2609
+ const closed = { ...state, overlay: null };
2610
+ switch (id) {
2611
+ case "review":
2612
+ return { state, effects: [{ type: "isolationReviewDiff", sessionId }] };
2613
+ case "cleanup":
2614
+ return { state: closed, effects: [{ type: "isolationCleanup", sessionId, branch: handoffBranch(handoff) }] };
2615
+ case "merge":
2616
+ return {
2617
+ state: {
2618
+ ...state,
2619
+ overlay: isRepoGroup(handoff) ? {
2620
+ kind: "confirm",
2621
+ title: "Merge all into your branches?",
2622
+ message: `Merge ${handoffBranch(handoff)} into whatever each of ${reposWithWork(handoff).join(", ")} has checked out. Ordewell never does this on its own. It checks every repository first and merges none unless every repository can take it; on git older than 2.38 it goes repository by repository and stops at the first that fails. A merge that conflicts is aborted, and your trees stay as they were.`,
2623
+ action: { kind: "merge-run" }
2624
+ } : {
2625
+ kind: "confirm",
2626
+ title: "Merge into your branch?",
2627
+ message: `Merge ${handoffBranch(handoff)} into whatever you have checked out. Ordewell never does this on its own. If it conflicts the merge is aborted and your tree stays as it was.`,
2628
+ action: { kind: "merge-run" }
2629
+ }
2630
+ },
2631
+ effects: []
2632
+ };
2633
+ case "discard":
2634
+ return {
2635
+ state: {
2636
+ ...state,
2637
+ overlay: {
2638
+ kind: "confirm",
2639
+ title: "Discard this run?",
2640
+ message: `Removes the run's worktrees and task branches and deletes ${handoffBranch(handoff)}${isRepoGroup(handoff) ? ` in ${reposOf2(handoff)}` : ""}. Tasks keep their status \u2014 mark one not done if you did not keep its work.`,
2641
+ action: { kind: "discard-run" }
2642
+ }
2643
+ },
2644
+ effects: []
2645
+ };
2646
+ }
2647
+ }
2648
+ function confirmedHandoff(state, kind) {
2649
+ const closed = { ...state, overlay: null };
2650
+ if (!state.sessionId || !state.handoff) return { state: closed, effects: [] };
2651
+ const { sessionId, handoff } = state;
2652
+ const effect = kind === "merge-run" ? { type: "isolationMerge", sessionId, branch: handoffBranch(handoff), ...isRepoGroup(handoff) ? { group: true } : {} } : { type: "isolationDiscard", sessionId, branch: handoffBranch(handoff) };
2653
+ return { state: closed, effects: [effect] };
2654
+ }
2655
+ function showDiff(state, diff) {
2656
+ if (diff.trim() === "") return say(state, "system", `Nothing differs from ${state.handoff ? handoffBase(state.handoff, 8) : "the base commit"}.`);
2657
+ const index = state.overlay?.kind === "handoff" ? state.overlay.index : 0;
2658
+ const lines = sanitize(diff.replace(/\t/g, " ")).split("\n");
2659
+ return { ...state, overlay: { kind: "handoff", index, diff: { lines, scroll: 0 } } };
2660
+ }
2661
+ function handleHandoffKey(state, overlay, key, rows) {
2662
+ const hold = { state, effects: [] };
2663
+ const up = key.name === "up" || key.name === "scrollup";
2664
+ const down = key.name === "down" || key.name === "scrolldown";
2665
+ if (overlay.diff) {
2666
+ const { lines, scroll } = overlay.diff;
2667
+ const room = diffRoom(rows);
2668
+ const max = Math.max(0, lines.length - room);
2669
+ const delta = up ? -1 : down ? 1 : key.name === "pageup" ? -room : key.name === "pagedown" ? room : null;
2670
+ if (delta !== null) {
2671
+ const next = Math.min(max, Math.max(0, scroll + delta));
2672
+ return { state: { ...state, overlay: { ...overlay, diff: { lines, scroll: next } } }, effects: [] };
2673
+ }
2674
+ if (key.name === "escape" || key.name === "enter") return { state: { ...state, overlay: { ...overlay, diff: null } }, effects: [] };
2675
+ return hold;
2676
+ }
2677
+ if (key.name === "escape") return { state: { ...state, overlay: null }, effects: [] };
2678
+ if (up || down) {
2679
+ const index = Math.min(HANDOFF_ACTIONS.length - 1, Math.max(0, overlay.index + (up ? -1 : 1)));
2680
+ return { state: { ...state, overlay: { ...overlay, index } }, effects: [] };
2681
+ }
2682
+ if (key.name === "enter") return runHandoffAction(state, HANDOFF_ACTIONS[overlay.index].id);
2683
+ return hold;
2684
+ }
2685
+ var BLOCKED_ITEMS = [
2686
+ { id: "stash", label: "Stash and continue", detail: "git stash your tracked changes, then run in worktrees" },
2687
+ { id: "shared", label: "Run without isolation", detail: "this run only, in your working tree" },
2688
+ { id: "cancel", label: "Cancel", detail: "start nothing" }
2689
+ ];
2690
+ function blockedPicker(state, message, repos = []) {
2691
+ const items = repos.length === 0 ? BLOCKED_ITEMS : BLOCKED_ITEMS.map((item) => item.id === "stash" ? { ...item, detail: `git stash the tracked changes in ${repos.join(", ")}, then run in worktrees` } : item);
2692
+ return {
2693
+ ...state,
2694
+ overlay: {
2695
+ kind: "picker",
2696
+ picker: {
2697
+ title: "Run blocked by uncommitted changes",
2698
+ hint: message,
2699
+ items,
2700
+ filter: "",
2701
+ index: 0,
2702
+ multi: false,
2703
+ chosen: [],
2704
+ action: { kind: "isolation-blocked" }
2705
+ }
2706
+ }
2707
+ };
2708
+ }
2709
+ function chooseBlocked(state, choice) {
2710
+ const closed = { ...state, overlay: null };
2711
+ if (!state.sessionId) return { state: closed, effects: [] };
2712
+ const { sessionId } = state;
2713
+ if (choice === "cancel") return { state: closed, effects: [{ type: "stopExecution", sessionId }] };
2714
+ return { state: closed, effects: [{ type: "isolationContinue", sessionId, mode: choice }] };
2715
+ }
2716
+ function sameIsolation(a, b) {
2717
+ return a?.state === b?.state && a?.branch === b?.branch && a?.worktree === b?.worktree && a?.conflictRepo === b?.conflictRepo && (a?.repos ?? []).join("\0") === (b?.repos ?? []).join("\0");
2718
+ }
2719
+ function mapTasks(tasks, map) {
2720
+ return tasks.map((task) => {
2721
+ const mapped = map(task);
2722
+ return task.subtasks ? { ...mapped, subtasks: mapTasks(task.subtasks, map) } : mapped;
2723
+ });
2724
+ }
2725
+ function clearIsolation(state) {
2726
+ return {
2727
+ ...state,
2728
+ handoff: null,
2729
+ tasks: mapTasks(state.tasks, ({ isolation: _dropped, ...task }) => task)
2730
+ };
2731
+ }
2732
+ function isolationForPlan(tasks, record, previous) {
2733
+ const known = /* @__PURE__ */ new Map();
2734
+ mapTasks(previous, (task) => {
2735
+ if (task.isolation) known.set(task.id, task.isolation);
2736
+ return task;
2737
+ });
2738
+ return mapTasks(tasks, (task) => {
2739
+ const isolation = record ? record.tasks[task.id] ?? { state: "none" } : known.get(task.id);
2740
+ return isolation ? { ...task, isolation } : task;
2741
+ });
2742
+ }
2743
+
2744
+ // src/commands/handoff.ts
2745
+ var USAGE4 = "Usage: ordewell handoff [review|merge|discard|cleanup] [--session-id <id>] [--workspace /path] [--yes]";
2746
+ async function askYesNo(question) {
2747
+ if (!process.stdin.isTTY) return false;
2748
+ const rl = (0, import_readline2.createInterface)({ input: process.stdin, output: process.stderr });
2749
+ try {
2750
+ const answer = await new Promise((resolve3) => rl.question(`${question} [y/N] `, resolve3));
2751
+ return /^y(es)?$/i.test(answer.trim());
2752
+ } finally {
2753
+ rl.close();
2754
+ }
2755
+ }
2756
+ async function handleHandoff(subArgs, injectedApi, confirm = askYesNo) {
2757
+ const action = positionals(subArgs)[0]?.toLowerCase();
2758
+ if (action === void 0) {
2759
+ console.log(`${USAGE4}
2760
+ `);
2761
+ for (const a of HANDOFF_ACTIONS) console.log(` ${a.id.padEnd(8)} ${a.label} \u2014 ${a.hint}`);
2762
+ return;
2763
+ }
2764
+ if (!HANDOFF_ACTIONS.some((a) => a.id === action)) fail(`Unknown handoff action "${action}".`, USAGE4);
2765
+ const { api, sessionId, plan } = await adopted(subArgs, injectedApi);
2766
+ const handoff = isolationOfPlan(plan)?.handoff;
2767
+ if (!handoff) fail("This session has no isolated run to hand off.");
2768
+ const branch = handoffBranch(handoff);
2769
+ const group = isRepoGroup(handoff);
2770
+ if (group) for (const line of repoResultLines(handoff)) console.error(line);
2771
+ const asked = hasFlag(subArgs, "--yes");
2772
+ const confirmed = async (question) => {
2773
+ if (asked || await confirm(question)) return;
2774
+ fail("Not confirmed \u2014 nothing was changed. Pass --yes to confirm without a prompt.");
2775
+ };
2776
+ const attempt = async (work) => {
2777
+ try {
2778
+ return await work();
2779
+ } catch (err) {
2780
+ return fail(`Handoff ${action} failed: ${err.message}`);
2781
+ }
2782
+ };
2783
+ switch (action) {
2784
+ case "review": {
2785
+ const diff = await attempt(() => api.reviewRunDiff(sessionId));
2786
+ console.log(diff.trim() === "" ? `Nothing differs from ${handoffBase(handoff, 8)}.` : diff);
2787
+ return;
2788
+ }
2789
+ case "merge": {
2790
+ await confirmed(group ? `Merge ${branch} into the branch checked out in each of ${reposWithWork(handoff).join(", ")}? Nothing is merged unless every repository can take it.` : `Merge ${branch} into the branch you have checked out?`);
2791
+ const { ok, message } = mergeOutcome(await attempt(() => api.mergeRun(sessionId)), branch, group);
2792
+ if (ok) console.log(message);
2793
+ else fail(message);
2794
+ return;
2795
+ }
2796
+ case "discard":
2797
+ await confirmed(group ? `Discard this run's worktrees and task branches, and delete ${branch} in every repository?` : `Discard this run's worktrees and task branches, and delete ${branch}?`);
2798
+ await attempt(() => api.discardRun(sessionId));
2799
+ console.log(`Discarded the run and ${branch}${group ? " in every repository" : ""}.`);
2800
+ return;
2801
+ case "cleanup":
2802
+ await attempt(() => api.cleanupRun(sessionId));
2803
+ console.log(`Removed the run's worktrees and task branches; ${branch} is kept${group ? " in every repository" : ""}.`);
2804
+ return;
2136
2805
  }
2137
- console.error(`Unknown sessions action: "${action}"`);
2138
- console.error(USAGE3);
2139
- process.exit(1);
2140
2806
  }
2141
2807
 
2142
2808
  // src/commands/planner.ts
2143
- var import_core12 = require("@ordewell/core");
2809
+ var import_core13 = require("@ordewell/core");
2144
2810
 
2145
2811
  // src/plannerModelSwitch.ts
2146
2812
  function describePlannerSwitch(settings, provider) {
@@ -2159,7 +2825,7 @@ function rememberedModelFor(settings, provider) {
2159
2825
  }
2160
2826
 
2161
2827
  // src/commands/planner.ts
2162
- var KNOWN_PROVIDERS = Object.keys(import_core12.ALL_PROVIDERS);
2828
+ var KNOWN_PROVIDERS = Object.keys(import_core13.ALL_PROVIDERS);
2163
2829
  async function handlePlanner(subArgs, injectedApi) {
2164
2830
  const api = await connect(subArgs, injectedApi);
2165
2831
  const [wanted] = positionals(subArgs);
@@ -2174,13 +2840,13 @@ async function handlePlanner(subArgs, injectedApi) {
2174
2840
  const settings = await persistEnv(api, { AI_PROVIDER: provider });
2175
2841
  const recall2 = describePlannerSwitch(settings, provider);
2176
2842
  writeEnv({ ORCHESTRATOR_MODEL: recall2.model, ORDEWELL_PLANNER_EFFORT: recall2.effort });
2177
- const meta = import_core12.ALL_PROVIDERS[provider];
2843
+ const meta = import_core13.ALL_PROVIDERS[provider];
2178
2844
  const catalog = await fetchCatalog(api);
2179
2845
  console.log(
2180
- (0, import_core12.isCliProvider)(provider) ? `Planning with ${meta.label} \u2014 no API key needed, it uses that agent's own subscription.` : `Planner set to ${meta.label}.`
2846
+ (0, import_core13.isCliProvider)(provider) ? `Planning with ${meta.label} \u2014 no API key needed, it uses that agent's own subscription.` : `Planner set to ${meta.label}.`
2181
2847
  );
2182
2848
  console.log(plannerModelLine(recall2));
2183
- if (!(0, import_core12.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
2849
+ if (!(0, import_core13.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
2184
2850
  console.log(` No ${meta.apiKeyEnvVar} configured yet \u2014 set one with \`ordewell key set ${provider} <key>\`.`);
2185
2851
  }
2186
2852
  }
@@ -2198,15 +2864,15 @@ async function showPlanners(api) {
2198
2864
  const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
2199
2865
  const current = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
2200
2866
  console.log("\nCoding agents (no API key \u2014 they use their own subscription)");
2201
- for (const id of import_core12.CLI_PROVIDERS) {
2202
- const runner = (0, import_core12.runnerForProvider)(id);
2867
+ for (const id of import_core13.CLI_PROVIDERS) {
2868
+ const runner = (0, import_core13.runnerForProvider)(id);
2203
2869
  const discovered = runner ? catalog.models.some((m) => m.runners?.includes(runner)) : false;
2204
- console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core12.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
2870
+ console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core13.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
2205
2871
  }
2206
2872
  console.log("\nAPI providers");
2207
- for (const id of import_core12.PROVIDER_PRIORITY) {
2873
+ for (const id of import_core13.PROVIDER_PRIORITY) {
2208
2874
  const configured = catalog.providers.includes(id);
2209
- const meta = import_core12.ALL_PROVIDERS[id];
2875
+ const meta = import_core13.ALL_PROVIDERS[id];
2210
2876
  console.log(` ${marker(id === current)} ${id.padEnd(14)} ${meta.label}${configured ? "" : ` (no ${meta.apiKeyEnvVar})`}`);
2211
2877
  }
2212
2878
  console.log(`
@@ -2223,7 +2889,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
2223
2889
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
2224
2890
  const modelId = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
2225
2891
  const currentEffort = typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : "";
2226
- const runner = (0, import_core12.runnerForProvider)(provider);
2892
+ const runner = (0, import_core13.runnerForProvider)(provider);
2227
2893
  if (!runner) {
2228
2894
  fail(
2229
2895
  "Thinking effort applies to a coding-agent planner.",
@@ -2263,29 +2929,29 @@ async function handlePlannerEffort(subArgs, injectedApi) {
2263
2929
  }
2264
2930
 
2265
2931
  // src/commands/key.ts
2266
- var import_core13 = require("@ordewell/core");
2267
- var USAGE4 = "Usage: ordewell key [list | set <provider> <key>]";
2932
+ var import_core14 = require("@ordewell/core");
2933
+ var USAGE5 = "Usage: ordewell key [list | set <provider> <key>]";
2268
2934
  async function handleKey(subArgs, injectedApi) {
2269
2935
  const [action, provider, ...keyParts] = positionals(subArgs);
2270
2936
  if (action && action !== "list" && action !== "set") {
2271
- fail(`Unknown key action: "${action}"`, USAGE4);
2937
+ fail(`Unknown key action: "${action}"`, USAGE5);
2272
2938
  }
2273
2939
  const api = await connect(subArgs, injectedApi);
2274
2940
  if (action !== "set") {
2275
2941
  const catalog = await fetchCatalog(api);
2276
2942
  console.log("\nAPI provider keys\n");
2277
- for (const id2 of import_core13.PROVIDER_PRIORITY) {
2278
- const meta2 = import_core13.ALL_PROVIDERS[id2];
2943
+ for (const id2 of import_core14.PROVIDER_PRIORITY) {
2944
+ const meta2 = import_core14.ALL_PROVIDERS[id2];
2279
2945
  const configured = catalog.providers.includes(id2);
2280
2946
  console.log(` ${configured ? "\u2713" : " "} ${id2.padEnd(14)} ${meta2.apiKeyEnvVar.padEnd(28)} ${meta2.label}`);
2281
2947
  }
2282
2948
  console.log(`
2283
- \u2713 = a working key was found. ${USAGE4}`);
2949
+ \u2713 = a working key was found. ${USAGE5}`);
2284
2950
  return;
2285
2951
  }
2286
- if (!provider) fail(USAGE4);
2952
+ if (!provider) fail(USAGE5);
2287
2953
  const id = provider.toLowerCase();
2288
- const meta = import_core13.ALL_PROVIDERS[id];
2954
+ const meta = import_core14.ALL_PROVIDERS[id];
2289
2955
  if (!meta) {
2290
2956
  fail(`Unknown provider: ${provider}`, "Run `ordewell key` to see the list.");
2291
2957
  }
@@ -2296,7 +2962,7 @@ async function handleKey(subArgs, injectedApi) {
2296
2962
  }
2297
2963
 
2298
2964
  // src/commands/runners.ts
2299
- var USAGE5 = "Usage: ordewell runners [<runner-id> on|off]";
2965
+ var USAGE6 = "Usage: ordewell runners [<runner-id> on|off]";
2300
2966
  async function handleRunners(subArgs, injectedApi) {
2301
2967
  const [runner, toggle] = positionals(subArgs);
2302
2968
  const api = await connect(subArgs, injectedApi);
@@ -2307,7 +2973,7 @@ async function handleRunners(subArgs, injectedApi) {
2307
2973
  console.log(` ${r.enabled ? "\u2713" : " "} ${r.id.padEnd(14)} ${r.name}${r.enabled ? "" : " (disabled)"}`);
2308
2974
  }
2309
2975
  console.log(`
2310
- \u2713 = offered to the planner. ${USAGE5}`);
2976
+ \u2713 = offered to the planner. ${USAGE6}`);
2311
2977
  return;
2312
2978
  }
2313
2979
  const state = await api.getRunners();
@@ -2316,7 +2982,7 @@ async function handleRunners(subArgs, injectedApi) {
2316
2982
  fail(`Unknown runner: ${runner}`, `Enabled or not, this daemon knows: ${state.runners.map((r) => r.id).join(", ")}`);
2317
2983
  }
2318
2984
  const enabled = toggle === void 0 ? !known.enabled : toggle === "on" ? true : toggle === "off" ? false : null;
2319
- if (enabled === null) fail(USAGE5);
2985
+ if (enabled === null) fail(USAGE6);
2320
2986
  await api.setRunnerEnabled(runner, enabled);
2321
2987
  console.log(`${runner} ${enabled ? "enabled" : "disabled"}.`);
2322
2988
  }
@@ -2352,20 +3018,21 @@ async function handleRefresh(subArgs, injectedApi) {
2352
3018
  async function handleApprove(subArgs, injectedApi) {
2353
3019
  const sessionId = resolveSessionId(subArgs);
2354
3020
  const api = await connect(subArgs, injectedApi);
2355
- try {
2356
- await api.approveReview(sessionId);
2357
- } catch (err) {
2358
- console.error(`Failed to approve: ${err.message}`);
2359
- process.exit(1);
2360
- }
2361
- console.error("Plan approved \u2014 continuing execution.");
2362
- await followExecution(api, sessionId);
3021
+ await followExecution(api, sessionId, async () => {
3022
+ try {
3023
+ await api.approveReview(sessionId);
3024
+ } catch (err) {
3025
+ console.error(`Failed to approve: ${err.message}`);
3026
+ process.exit(1);
3027
+ }
3028
+ console.error("Plan approved \u2014 continuing execution.");
3029
+ });
2363
3030
  }
2364
3031
 
2365
3032
  // src/tui/terminalLauncher.ts
2366
3033
  var import_child_process3 = require("child_process");
2367
3034
  var import_util = require("util");
2368
- var import_core14 = require("@ordewell/core");
3035
+ var import_core15 = require("@ordewell/core");
2369
3036
  var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
2370
3037
  var defaultExecFile = async (command, args) => {
2371
3038
  const { stdout, stderr } = await execFileAsync(command, args);
@@ -2385,16 +3052,16 @@ var defaultWhich = (bin) => {
2385
3052
  async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
2386
3053
  const platform = deps.platform ?? process.platform;
2387
3054
  const env = deps.env ?? process.env;
2388
- const hasTmuxImpl = deps.hasTmuxImpl ?? import_core14.hasTmux;
3055
+ const hasTmuxImpl = deps.hasTmuxImpl ?? import_core15.hasTmux;
2389
3056
  const execFileImpl = deps.execFileImpl ?? defaultExecFile;
2390
3057
  const spawnImpl = deps.spawnImpl ?? defaultSpawn;
2391
3058
  const which = deps.which ?? defaultWhich;
2392
3059
  if (!hasTmuxImpl()) {
2393
3060
  return { ok: false, message: "tmux is not installed \u2014 install it to get a real terminal per task." };
2394
3061
  }
2395
- const session = (0, import_core14.tmuxSessionName)(port);
2396
- const socket = (0, import_core14.tmuxSocketName)(port);
2397
- const base = (0, import_core14.tmuxWindowName)(taskId, planSessionId);
3062
+ const session = (0, import_core15.tmuxSessionName)(port);
3063
+ const socket = (0, import_core15.tmuxSocketName)(port);
3064
+ const base = (0, import_core15.tmuxWindowName)(taskId, planSessionId);
2398
3065
  let windows;
2399
3066
  try {
2400
3067
  const { stdout } = await execFileImpl("tmux", ["-L", socket, "list-windows", "-t", session, "-F", "#{window_name}"]);
@@ -2688,149 +3355,7 @@ Dependencies \xB7 #${task.order} ${task.title}
2688
3355
  }
2689
3356
 
2690
3357
  // src/tui/index.ts
2691
- var import_core18 = require("@ordewell/core");
2692
-
2693
- // src/tui/ansi.ts
2694
- var import_string_width = __toESM(require("string-width"));
2695
- var ESCAPE = /\x1b(?:\][\s\S]*?(?:\x07|\x1b\\|$)|[P^_][\s\S]*?(?:\x1b\\|$)|\[[0-?]*[ -/]*[@-~]?|[ -/]*[0-~]?)/g;
2696
- var ESCAPE_SPLIT = new RegExp(`(${ESCAPE.source})`);
2697
- var CONTROL = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
2698
- var CONTROL_IN_ROW = /[\x00-\x1f\x7f-\x9f]/g;
2699
- var HAS_CONTROL = /[\x00-\x1f\x7f-\x9f]/;
2700
- var PAINT_ESCAPE = /^\x1b\[[0-9;]*[mGK]$/;
2701
- function stripAnsi(text) {
2702
- return text.replace(ESCAPE, "");
2703
- }
2704
- function sanitize(text) {
2705
- return text.replace(ESCAPE, "").replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(CONTROL, "");
2706
- }
2707
- function paintOnly(line) {
2708
- if (!HAS_CONTROL.test(line)) return line;
2709
- return line.split(ESCAPE_SPLIT).map((piece) => piece.startsWith("\x1B") ? PAINT_ESCAPE.test(piece) ? piece : "" : piece.replace(CONTROL_IN_ROW, "")).join("");
2710
- }
2711
- function width(text) {
2712
- return (0, import_string_width.default)(stripAnsi(text));
2713
- }
2714
- function truncate(text, max) {
2715
- if (max <= 0) return "";
2716
- if (width(text) <= max) return text;
2717
- let out = "";
2718
- let used = 0;
2719
- for (const char of stripAnsi(text)) {
2720
- const w = (0, import_string_width.default)(char);
2721
- if (used + w > max - 1) break;
2722
- out += char;
2723
- used += w;
2724
- }
2725
- return out + "\u2026";
2726
- }
2727
- function pad(text, target) {
2728
- const current = width(text);
2729
- if (current > target) return truncate(text, target);
2730
- return text + " ".repeat(target - current);
2731
- }
2732
- function chopWord(piece, max) {
2733
- const chunks = [];
2734
- let cur = "";
2735
- let curWidth = 0;
2736
- for (const ch of piece) {
2737
- const w = (0, import_string_width.default)(ch);
2738
- if (cur && curWidth + w > max) {
2739
- chunks.push(cur);
2740
- cur = ch;
2741
- curWidth = w;
2742
- } else {
2743
- cur += ch;
2744
- curWidth += w;
2745
- }
2746
- }
2747
- chunks.push(cur);
2748
- return chunks;
2749
- }
2750
- var wrapMemo = /* @__PURE__ */ new Map();
2751
- var WRAP_MEMO_MAX = 64;
2752
- function wrapLines(text, max) {
2753
- const key = `${max}\0${text}`;
2754
- const cached = wrapMemo.get(key);
2755
- if (cached) {
2756
- wrapMemo.delete(key);
2757
- wrapMemo.set(key, cached);
2758
- return cached;
2759
- }
2760
- const result = computeWrapLines(text, max);
2761
- if (wrapMemo.size >= WRAP_MEMO_MAX) {
2762
- const oldest = wrapMemo.keys().next().value;
2763
- if (oldest !== void 0) wrapMemo.delete(oldest);
2764
- }
2765
- wrapMemo.set(key, result);
2766
- return result;
2767
- }
2768
- function computeWrapLines(text, max) {
2769
- if (max <= 0) return [{ line: "", start: 0 }];
2770
- const out = [];
2771
- let pos = 0;
2772
- for (const paragraph of text.split("\n")) {
2773
- let current = "";
2774
- let currentStart = pos;
2775
- let first = true;
2776
- let wordPos = pos;
2777
- for (const word of paragraph.split(" ")) {
2778
- let piece = word;
2779
- let pieceStart = wordPos;
2780
- if (width(piece) > max) {
2781
- const chunks = chopWord(piece, max);
2782
- for (let ci = 0; ci < chunks.length - 1; ci++) {
2783
- const head = chunks[ci];
2784
- if (!first) {
2785
- out.push({ line: current, start: currentStart });
2786
- current = "";
2787
- }
2788
- out.push({ line: head, start: pieceStart });
2789
- pieceStart += head.length;
2790
- first = true;
2791
- }
2792
- piece = chunks[chunks.length - 1];
2793
- }
2794
- const candidate = first ? piece : `${current} ${piece}`;
2795
- const candidateStart = first ? pieceStart : currentStart;
2796
- first = false;
2797
- if (width(candidate) > max) {
2798
- out.push({ line: current, start: currentStart });
2799
- current = piece;
2800
- currentStart = pieceStart;
2801
- } else {
2802
- current = candidate;
2803
- currentStart = candidateStart;
2804
- }
2805
- wordPos += word.length + 1;
2806
- }
2807
- out.push({ line: current, start: currentStart });
2808
- pos += paragraph.length + 1;
2809
- }
2810
- return out.length > 0 ? out : [{ line: "", start: 0 }];
2811
- }
2812
- function wrap(text, max) {
2813
- return wrapLines(text, max).map((l) => l.line);
2814
- }
2815
- function colour(open) {
2816
- return (text) => style.enabled ? `\x1B[${open}m${text}\x1B[0m` : text;
2817
- }
2818
- var style = {
2819
- /** Set false when the output is piped or the terminal has no colour. */
2820
- enabled: true,
2821
- bold: colour("1"),
2822
- dim: colour("2"),
2823
- italic: colour("3"),
2824
- underline: colour("4"),
2825
- inverse: colour("7"),
2826
- red: colour("31"),
2827
- green: colour("32"),
2828
- yellow: colour("33"),
2829
- blue: colour("34"),
2830
- magenta: colour("35"),
2831
- cyan: colour("36"),
2832
- grey: colour("90")
2833
- };
3358
+ var import_core19 = require("@ordewell/core");
2834
3359
 
2835
3360
  // src/tui/editor.ts
2836
3361
  function emptyEditor() {
@@ -2968,6 +3493,7 @@ var SLASH_COMMANDS = [
2968
3493
  { name: "approve", usage: "/approve", description: "Approve the drafted plan", category: "planning" },
2969
3494
  { name: "run", usage: "/run", description: "Execute the approved plan", category: "planning" },
2970
3495
  { name: "stop", usage: "/stop", description: "Stop the planner or the running execution, whichever is active", category: "planning" },
3496
+ { name: "handoff", usage: "/handoff [review|merge|discard|cleanup]", description: "Land an isolated run's branch: review it, merge it into yours, or discard it", category: "planning" },
2971
3497
  // Tasks
2972
3498
  { name: "add-task", usage: "/add-task <title>", description: "Add a task to the current plan", category: "tasks" },
2973
3499
  { name: "remove-task", usage: "/remove-task <id>", description: "Remove a task from the plan", category: "tasks" },
@@ -2997,6 +3523,9 @@ var SLASH_COMMANDS = [
2997
3523
  { name: "verify", usage: "/verify [on|off]", description: "Verification mode \u2014 final evidence-based task that runs the suite", category: "skills" },
2998
3524
  // Sessions
2999
3525
  { name: "sessions", usage: "/sessions", description: "List saved sessions", category: "session" },
3526
+ { name: "fork", usage: "/fork", description: "Continue in a copy of this conversation and its tasks; the original stays as it is", category: "session" },
3527
+ { name: "compact", usage: "/compact", description: "Condense this conversation into a summary; the last two exchanges and all tasks are kept", category: "session" },
3528
+ { name: "rewind", usage: "/rewind [<message>]", description: "Rewind the conversation to just before one of your messages (tasks are kept)", category: "session" },
3000
3529
  { name: "new", usage: "/new", description: "Start a new session", category: "session" },
3001
3530
  { name: "save", usage: "/save", description: "Save the current session", category: "session" },
3002
3531
  { name: "load", usage: "/load <id>", description: "Load a saved session", category: "session" },
@@ -3268,6 +3797,7 @@ function initialState(overrides = {}) {
3268
3797
  skills: noSkills(),
3269
3798
  runners: [],
3270
3799
  sessions: [],
3800
+ rewindTargets: null,
3271
3801
  models: [],
3272
3802
  modesByRunner: {},
3273
3803
  orchestratorModels: [],
@@ -3281,6 +3811,7 @@ function initialState(overrides = {}) {
3281
3811
  mouseCapture: true,
3282
3812
  selection: null,
3283
3813
  workspace: process.cwd(),
3814
+ handoff: null,
3284
3815
  overlay: null,
3285
3816
  pendingApprovals: [],
3286
3817
  toast: "",
@@ -3293,7 +3824,7 @@ function initialState(overrides = {}) {
3293
3824
  }
3294
3825
 
3295
3826
  // src/tui/layout.ts
3296
- var import_core15 = require("@ordewell/core");
3827
+ var import_core16 = require("@ordewell/core");
3297
3828
  var CHROME_ROWS = 4;
3298
3829
  function chatInputWrap(state) {
3299
3830
  const active = state.focus === "chat" && !state.overlay;
@@ -3301,7 +3832,9 @@ function chatInputWrap(state) {
3301
3832
  return wrapLines(state.editor.text, chatEditorRoom(state.cols, true));
3302
3833
  }
3303
3834
  function footerHints(state) {
3304
- const markHint = selectedPlanRow(state)?.task.status === "completed" ? "m undone" : "m done";
3835
+ const selected = selectedPlanRow(state)?.task;
3836
+ const markHint = selected?.status === "completed" ? "m undone" : "m done";
3837
+ const resolveHint = selected?.isolation?.state === "conflict" ? ["x resolve conflict"] : [];
3305
3838
  const planning = state.status === "planning" || state.status === "researching";
3306
3839
  const escHint = planning ? "esc stop planning" : null;
3307
3840
  if (state.focus === "plan") {
@@ -3324,6 +3857,7 @@ function footerHints(state) {
3324
3857
  "a add",
3325
3858
  "d remove",
3326
3859
  "t terminal",
3860
+ ...resolveHint,
3327
3861
  "pgup/pgdn scroll",
3328
3862
  "tab chat"
3329
3863
  ];
@@ -3429,9 +3963,9 @@ function chatScrollMax(state) {
3429
3963
  }
3430
3964
  function plannerSetup(state) {
3431
3965
  const provider = state.plannerProvider;
3432
- const registration = import_core15.ALL_PROVIDERS[provider];
3966
+ const registration = import_core16.ALL_PROVIDERS[provider];
3433
3967
  if (!registration) return { label: "not set", ready: false };
3434
- const runner = (0, import_core15.runnerForProvider)(provider);
3968
+ const runner = (0, import_core16.runnerForProvider)(provider);
3435
3969
  const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
3436
3970
  return { label: registration.label, ready };
3437
3971
  }
@@ -3578,7 +4112,7 @@ function taskLines(state, row, index, cols) {
3578
4112
  const icon = paint(rawIcon);
3579
4113
  const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : style.grey(" AI");
3580
4114
  const caret = selected ? style.cyan("\u276F") : " ";
3581
- const orderLabel = parent ? (0, import_core15.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
4115
+ const orderLabel = parent ? (0, import_core16.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
3582
4116
  const indent = parent ? " " : "";
3583
4117
  const head = `${indent}${caret} ${icon} ${orderLabel} ${kind} `;
3584
4118
  const titleRoom = Math.max(1, cols - width(head));
@@ -3602,9 +4136,21 @@ function taskLines(state, row, index, cols) {
3602
4136
  const meta = [running ? "working" : "", runner, model].filter(Boolean).join(" \xB7 ");
3603
4137
  if (meta) lines.push(style.grey(truncate(`${bodyPad}${meta}`, cols)));
3604
4138
  if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
4139
+ if (task.isolation?.state === "conflict") {
4140
+ const where = task.isolation.conflictRepo && task.isolation.conflictRepo !== "." ? ` in ${task.isolation.conflictRepo}` : "";
4141
+ lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where} \u2014 its work is kept on its own branch`, cols)));
4142
+ }
3605
4143
  let editorLine;
3606
4144
  if (expanded) {
3607
4145
  lines.push(style.grey(`${bodyPad}${task.status.replace(/_/g, " ")}`));
4146
+ if (task.isolation && task.isolation.state !== "none" && task.isolation.branch) {
4147
+ lines.push(...taskText("Branch", task.isolation.branch, cols, bodyPad));
4148
+ const repos = taskRepoNames(task.isolation);
4149
+ if (repos.length > 0) lines.push(...taskText("Repos", repos.join(", "), cols, bodyPad));
4150
+ if (task.isolation.state !== "integrated" && task.isolation.worktree) {
4151
+ lines.push(...taskText("Worktree", task.isolation.worktree, cols, bodyPad));
4152
+ }
4153
+ }
3608
4154
  if (modeInfo?.autonomous) {
3609
4155
  lines.push(...taskText("Autonomy", "Runs without permission prompts. Toggle with /auto.", cols, bodyPad));
3610
4156
  }
@@ -3934,8 +4480,51 @@ function renderOverlay(state, rows, cols) {
3934
4480
  cols
3935
4481
  );
3936
4482
  }
4483
+ if (overlay.kind === "handoff") return renderHandoff(state, overlay, rows, cols);
3937
4484
  return renderPicker(overlay.picker, rows, cols);
3938
4485
  }
4486
+ function renderHandoff(state, overlay, rows, cols) {
4487
+ const handoff = state.handoff;
4488
+ if (!handoff) return frame("Handoff", [style.grey("Nothing to hand off.")], rows, cols);
4489
+ if (overlay.diff) {
4490
+ const room = diffRoom(rows);
4491
+ const { lines, scroll } = overlay.diff;
4492
+ const shown = lines.slice(scroll, scroll + room).map((line) => paintDiffLine(truncate(line, Math.max(1, cols - 2))));
4493
+ const more = scroll + room < lines.length ? "\u2191\u2193 pgup/pgdn scroll \xB7 " : "";
4494
+ return frame(`Diff \u2014 ${handoffBranch(handoff)}`, [...shown, "", style.grey(`${more}enter or esc goes back`)], rows, cols);
4495
+ }
4496
+ const landed = handoff.landed.length === 0 ? [style.grey("Nothing landed on it.")] : handoff.landed.map((task) => ` ${style.green("\u2713")} #${task.order} ${task.title}`);
4497
+ const actions = handoffActions(handoff).map((action, index) => {
4498
+ const active = index === overlay.index;
4499
+ const caret = active ? style.cyan("\u276F") : " ";
4500
+ return `${caret} ${active ? style.bold(action.label) : action.label}${style.grey(` \u2014 ${action.hint}`)}`;
4501
+ });
4502
+ const perRepo = isRepoGroup(handoff) ? ["Per repository:", ...handoff.repos.map((repo, i) => ` ${repo.landed.length > 0 ? style.green("\u2713") : style.grey("\xB7")} ${repoResultLines(handoff)[i]}`), ""] : [];
4503
+ return frame(
4504
+ "Run handoff",
4505
+ [
4506
+ `Branch ${style.cyan(handoffBranch(handoff))}`,
4507
+ style.grey(`Forked from ${handoffBase(handoff, 12)}`),
4508
+ "",
4509
+ ...perRepo,
4510
+ "Landed on it, in plan order:",
4511
+ ...landed,
4512
+ "",
4513
+ ...actions,
4514
+ "",
4515
+ style.grey("\u2191\u2193 choose \xB7 enter selects \xB7 esc closes")
4516
+ ],
4517
+ rows,
4518
+ cols
4519
+ );
4520
+ }
4521
+ function paintDiffLine(line) {
4522
+ if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("diff ")) return style.bold(line);
4523
+ if (line.startsWith("+")) return style.green(line);
4524
+ if (line.startsWith("-")) return style.red(line);
4525
+ if (line.startsWith("@@")) return style.cyan(line);
4526
+ return line;
4527
+ }
3939
4528
  function renderPicker(picker2, rows, cols) {
3940
4529
  const items = visibleItems(picker2);
3941
4530
  const body = [];
@@ -3974,26 +4563,16 @@ function frame(title, body, rows, cols) {
3974
4563
  }
3975
4564
 
3976
4565
  // src/tui/reducer.ts
3977
- var import_core16 = require("@ordewell/core");
4566
+ var import_core17 = require("@ordewell/core");
3978
4567
  var import_plan_utils3 = require("@ordewell/core/plan-utils");
3979
- var KNOWN_PROVIDERS2 = Object.keys(import_core16.ALL_PROVIDERS);
4568
+ var KNOWN_PROVIDERS2 = Object.keys(import_core17.ALL_PROVIDERS);
3980
4569
  var DEFAULT_EFFORT = "__runner_default__";
3981
4570
  function step(state, effects = []) {
3982
4571
  return { state, effects };
3983
4572
  }
3984
- function say(state, role, content, research, extra) {
3985
- const message = {
3986
- role,
3987
- content: sanitize(content),
3988
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3989
- ...research ? { research } : {},
3990
- ...extra
3991
- };
3992
- return { ...state, messages: [...state.messages, message], scroll: 0 };
3993
- }
3994
4573
  function restoredMessages(history) {
3995
4574
  return history.map((entry) => ({
3996
- role: entry.kind === "plan_generated" ? "system" : entry.kind === "system" ? "system" : entry.role,
4575
+ role: entry.kind === "plan_generated" || entry.kind === "system" || entry.kind === "compaction" ? "system" : entry.role,
3997
4576
  content: sanitize(entry.kind === "plan_generated" ? "Plan generated." : entry.content),
3998
4577
  timestamp: entry.timestamp
3999
4578
  }));
@@ -4001,7 +4580,9 @@ function restoredMessages(history) {
4001
4580
  var isPendingResearch = (m) => m.role === "research" && !m.research?.outcome;
4002
4581
  function alreadySpoken(messages, content) {
4003
4582
  const last = findLastIndex(messages, (m) => m.role === "assistant" || m.role === "user");
4004
- return last >= 0 && messages[last].role === "assistant" && messages[last].content === content;
4583
+ const spoken = last >= 0 && messages[last].role === "assistant" && messages[last].content === content;
4584
+ const redrawn = messages.at(-1)?.role === "system" && messages.at(-1)?.content === content;
4585
+ return spoken || redrawn;
4005
4586
  }
4006
4587
  function researchLabel(messages, summary) {
4007
4588
  const others = messages.filter(isPendingResearch).length - 1;
@@ -4025,20 +4606,22 @@ function findLastIndex(items, match) {
4025
4606
  return -1;
4026
4607
  }
4027
4608
  var THINKING_TAIL = 400;
4028
- var fail2 = (state, message) => step(say(state, "error", message));
4609
+ var fail3 = (state, message) => step(say(state, "error", message));
4029
4610
  var stale = (state, sessionId) => sessionId !== void 0 && sessionId !== state.sessionId;
4030
4611
  function reduce(state, action) {
4031
4612
  switch (action.type) {
4032
4613
  case "key":
4033
4614
  return handleKey2(state, action.key);
4615
+ // Isolation belongs to one session's plan, and task ids repeat across
4616
+ // sessions, so nothing of the last one's may carry over into this.
4034
4617
  case "sessionStarted":
4035
- return step({ ...state, sessionId: action.sessionId, goal: action.goal });
4618
+ return step(clearIsolation({ ...state, sessionId: action.sessionId, goal: action.goal }));
4036
4619
  // The daemon registers a session only once planning succeeds; when it does
4037
4620
  // not, holding on to the id would send every next message to a session the
4038
4621
  // server never had.
4039
4622
  case "sessionCleared":
4040
4623
  return step({
4041
- ...state,
4624
+ ...clearIsolation(state),
4042
4625
  sessionId: null,
4043
4626
  goal: "",
4044
4627
  pendingApprovals: [],
@@ -4057,11 +4640,13 @@ function reduce(state, action) {
4057
4640
  return step({ ...state, messages: restoredMessages(action.history), scroll: 0 });
4058
4641
  case "planUpdated": {
4059
4642
  if (stale(state, action.sessionId)) return step(state);
4060
- const tasks = normalizeTasks(action.plan);
4643
+ const isolation = isolationOfPlan(action.plan);
4644
+ const tasks = isolationForPlan(normalizeTasks(action.plan), isolation, state.tasks);
4061
4645
  const expandedTaskId = state.expandedTaskId && findTask(tasks, state.expandedTaskId) ? state.expandedTaskId : null;
4062
4646
  const next = {
4063
4647
  ...state,
4064
4648
  tasks,
4649
+ handoff: isolation ? isolation.handoff : state.handoff,
4065
4650
  status: state.status === "planning" || state.status === "researching" ? "idle" : state.status,
4066
4651
  busyLabel: "",
4067
4652
  thinkingLine: "",
@@ -4133,9 +4718,10 @@ function reduce(state, action) {
4133
4718
  const update = updates[t.id];
4134
4719
  if (update === void 0) return t;
4135
4720
  const idleSince = update.idleSince ?? null;
4136
- if (update.status !== t.status || idleSince !== (t.idleSince ?? null)) {
4721
+ const isolation = update.isolation ?? t.isolation;
4722
+ if (update.status !== t.status || idleSince !== (t.idleSince ?? null) || !sameIsolation(isolation, t.isolation)) {
4137
4723
  changed = true;
4138
- return { ...t, status: update.status, idleSince };
4724
+ return { ...t, status: update.status, idleSince, isolation };
4139
4725
  }
4140
4726
  return t;
4141
4727
  });
@@ -4148,6 +4734,23 @@ function reduce(state, action) {
4148
4734
  if (sessionId === null) return step(state);
4149
4735
  return step(state, [{ type: "processQueued", sessionId }]);
4150
4736
  }
4737
+ case "isolationBlocked": {
4738
+ if (stale(state, action.sessionId)) return step(state);
4739
+ return step(blockedPicker(state, action.message, action.repos));
4740
+ }
4741
+ case "isolationHandoff": {
4742
+ if (stale(state, action.sessionId)) return step(state);
4743
+ return step(handoffArrived(state, action.handoff));
4744
+ }
4745
+ case "handoffDiff": {
4746
+ if (stale(state, action.sessionId)) return step(state);
4747
+ return step(showDiff(state, action.diff));
4748
+ }
4749
+ case "handoffDiscarded": {
4750
+ if (stale(state, action.sessionId)) return step(state);
4751
+ const overlay = state.overlay?.kind === "handoff" ? null : state.overlay;
4752
+ return step({ ...clearIsolation(state), overlay });
4753
+ }
4151
4754
  case "executionComplete": {
4152
4755
  if (stale(state, action.sessionId)) return step(state);
4153
4756
  const { completed, total, failed } = action.summary ?? {
@@ -4182,6 +4785,23 @@ function reduce(state, action) {
4182
4785
  );
4183
4786
  case "sessionsLoaded":
4184
4787
  return step(refillPicker({ ...state, sessions: action.sessions }, ["load-session", "delete-session"]));
4788
+ case "rewindTargetsLoaded":
4789
+ if (stale(state, action.sessionId)) return step(state);
4790
+ return step(refillPicker({ ...state, rewindTargets: action.targets }, ["rewind"]));
4791
+ // A fork holds no run, whatever the session it came from was doing — the
4792
+ // original keeps its run, and its events are stale from here on.
4793
+ case "sessionForked":
4794
+ return step({
4795
+ ...clearIsolation(state),
4796
+ sessionId: action.sessionId,
4797
+ goal: action.goal,
4798
+ status: "idle",
4799
+ busyLabel: "",
4800
+ thinkingLine: "",
4801
+ planApproved: false,
4802
+ pendingApprovals: [],
4803
+ overlay: state.overlay?.kind === "approval" ? null : state.overlay
4804
+ });
4185
4805
  case "runnersLoaded":
4186
4806
  return step(refillPicker({
4187
4807
  ...state,
@@ -4286,11 +4906,11 @@ function modelDetail(m) {
4286
4906
  function providerErrorHint(state) {
4287
4907
  const failed = Object.keys(state.providerErrors ?? {});
4288
4908
  if (failed.length === 0) return void 0;
4289
- const names = failed.map((p) => import_core16.ALL_PROVIDERS[p]?.label ?? p);
4909
+ const names = failed.map((p) => import_core17.ALL_PROVIDERS[p]?.label ?? p);
4290
4910
  return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
4291
4911
  }
4292
4912
  function plannerModelItems(state) {
4293
- const runner = (0, import_core16.runnerForProvider)(state.plannerProvider);
4913
+ const runner = (0, import_core17.runnerForProvider)(state.plannerProvider);
4294
4914
  if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
4295
4915
  const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
4296
4916
  id: m.id,
@@ -4304,7 +4924,7 @@ function plannerModelItems(state) {
4304
4924
  return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
4305
4925
  }
4306
4926
  function plannerEffortItems(state) {
4307
- const runner = (0, import_core16.runnerForProvider)(state.plannerProvider);
4927
+ const runner = (0, import_core17.runnerForProvider)(state.plannerProvider);
4308
4928
  if (!runner) {
4309
4929
  return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
4310
4930
  }
@@ -4331,6 +4951,13 @@ function pickerItemsFor(state, action) {
4331
4951
  if (action.kind === "set-planner-effort") {
4332
4952
  return plannerEffortItems(state);
4333
4953
  }
4954
+ if (action.kind === "rewind") {
4955
+ if (state.rewindTargets === null) return [];
4956
+ if (state.rewindTargets.length === 0) {
4957
+ return [{ id: "", label: "Nothing to rewind to yet", detail: "the only message so far is the goal", disabled: true }];
4958
+ }
4959
+ return [...state.rewindTargets].reverse().map((t) => ({ id: String(t.index), label: t.preview, detail: `message ${t.index}` }));
4960
+ }
4334
4961
  if (action.kind === "load-session" || action.kind === "delete-session") {
4335
4962
  return state.sessions.map((s) => ({
4336
4963
  id: s.id,
@@ -4561,6 +5188,9 @@ function handlePlanKey(state, key) {
4561
5188
  if (key.char === "t") {
4562
5189
  return step(state, [{ type: "openTaskTerminal", sessionId: state.sessionId, taskId: task.id }]);
4563
5190
  }
5191
+ if (key.char === "x" && task.isolation?.state === "conflict") {
5192
+ return step(state, [{ type: "resolveConflict", sessionId: state.sessionId, taskId: task.id }]);
5193
+ }
4564
5194
  if (key.char === "R") return openTaskRunnerPicker(state, task);
4565
5195
  if (key.char === "o") return openTaskModelPicker(state, task);
4566
5196
  if (key.char === "e") return openTaskEffortPicker(state, task);
@@ -4618,9 +5248,13 @@ function handleApprovalKey(state, overlay, key) {
4618
5248
  function handleOverlayKey(state, overlay, key) {
4619
5249
  if (overlay.kind === "help") return handleHelpKey(state, overlay.scroll ?? 0, key);
4620
5250
  if (key.name === "wheelignored") return step(state);
5251
+ if (overlay.kind === "handoff") return handleHandoffKey(state, overlay, key, bodyRows(state));
4621
5252
  if (isWheel(key) && overlay.kind !== "picker") return scrollPointed(state, key);
4622
5253
  if (overlay.kind === "approval") return handleApprovalKey(state, overlay, key);
4623
5254
  if (overlay.kind === "confirm") return handleConfirmKey(state, overlay, key);
5255
+ if (key.name === "escape" && overlay.kind === "picker" && overlay.picker.action.kind === "isolation-blocked") {
5256
+ return chooseBlocked(state, "cancel");
5257
+ }
4624
5258
  if (key.name === "escape") return step({ ...state, overlay: null });
4625
5259
  if (overlay.kind === "prompt") return handlePromptKey(state, overlay, key);
4626
5260
  return handlePickerKey(state, overlay.picker, key);
@@ -4634,6 +5268,9 @@ function handleConfirmKey(state, overlay, key) {
4634
5268
  if (!state.sessionId) return step(closed);
4635
5269
  return step(closed, [{ type: "removeTask", sessionId: state.sessionId, taskId: overlay.action.taskId }]);
4636
5270
  }
5271
+ if (overlay.action.kind === "merge-run" || overlay.action.kind === "discard-run") {
5272
+ return confirmedHandoff(state, overlay.action.kind);
5273
+ }
4637
5274
  if (overlay.action.kind === "init-workspace") {
4638
5275
  return step({ ...closed, status: "planning" }, [{ type: "startConversation", goal: overlay.action.goal, allowInit: true }]);
4639
5276
  }
@@ -4661,7 +5298,7 @@ function handlePromptKey(state, overlay, key) {
4661
5298
  if (overlay.action.kind === "api-key") {
4662
5299
  return step(closed, [{ type: "setApiKey", provider: overlay.action.provider, key: value2 }]);
4663
5300
  }
4664
- if (!state.sessionId) return fail2(closed, "No active plan to add a task to.");
5301
+ if (!state.sessionId) return fail3(closed, "No active plan to add a task to.");
4665
5302
  return step(closed, [{ type: "addTask", sessionId: state.sessionId, title: value2 }]);
4666
5303
  }
4667
5304
  const value = editText(overlay.value, key);
@@ -4694,6 +5331,7 @@ function handlePickerKey(state, picker2, key) {
4694
5331
  }
4695
5332
  function choose(state, picker2, item) {
4696
5333
  const closed = { ...state, overlay: null };
5334
+ if (picker2.action.kind === "isolation-blocked") return chooseBlocked(state, item?.id ?? "cancel");
4697
5335
  if (picker2.action.kind === "set-allowlist") {
4698
5336
  return step(closed, [{ type: "setAllowlist", runner: picker2.action.runner, modelIds: picker2.chosen }]);
4699
5337
  }
@@ -4732,6 +5370,11 @@ function choose(state, picker2, item) {
4732
5370
  return step(closed, [{ type: "setPlannerEffort", effort: item.id === DEFAULT_EFFORT ? "" : item.id }]);
4733
5371
  case "load-session":
4734
5372
  return step(closed, [{ type: "loadSession", sessionId: item.id }]);
5373
+ case "rewind":
5374
+ return withSession(
5375
+ closed,
5376
+ (sessionId) => step(closed, [{ type: "rewindConversation", sessionId, index: Number(item.id) }])
5377
+ );
4735
5378
  case "delete-session":
4736
5379
  return step(closed, [{ type: "deleteSession", sessionId: item.id }]);
4737
5380
  case "set-key":
@@ -4801,7 +5444,7 @@ function submit(state) {
4801
5444
  }
4802
5445
  function runCommand(state, { name, args }) {
4803
5446
  if (!findCommand(name)) {
4804
- return fail2(state, `Unknown command: /${name} \u2014 type /help to see what's available.`);
5447
+ return fail3(state, `Unknown command: /${name} \u2014 type /help to see what's available.`);
4805
5448
  }
4806
5449
  if (SKILL_IDS.includes(name)) {
4807
5450
  return toggleSkill(state, name, args[0]);
@@ -4848,14 +5491,25 @@ function runCommand(state, { name, args }) {
4848
5491
  { ...state, overlay: { kind: "picker", picker: picker("Sessions", [], { kind: "load-session" }) } },
4849
5492
  [{ type: "loadSessions" }]
4850
5493
  );
5494
+ case "handoff":
5495
+ return handoffCommand(state, args[0]);
5496
+ case "fork":
5497
+ return withIdlePlanner(state, (sessionId) => step(state, [{ type: "forkConversation", sessionId }]));
5498
+ case "rewind":
5499
+ return rewind(state, args[0]);
5500
+ case "compact":
5501
+ return withIdlePlanner(
5502
+ state,
5503
+ (sessionId) => step({ ...state, status: "planning", busyLabel: "Condensing the conversation\u2026" }, [{ type: "compactConversation", sessionId }])
5504
+ );
4851
5505
  case "new":
4852
5506
  return requestNewSession(state);
4853
5507
  case "save":
4854
5508
  return withSession(state, (sessionId) => step(state, [{ type: "saveSession", sessionId }]));
4855
5509
  case "load":
4856
- return args[0] ? step(state, [{ type: "loadSession", sessionId: args[0] }]) : fail2(state, "Usage: /load <session-id> \u2014 or run /sessions to pick one.");
5510
+ return args[0] ? step(state, [{ type: "loadSession", sessionId: args[0] }]) : fail3(state, "Usage: /load <session-id> \u2014 or run /sessions to pick one.");
4857
5511
  case "delete":
4858
- return args[0] ? step(state, [{ type: "deleteSession", sessionId: args[0] }]) : fail2(state, "Usage: /delete <session-id>");
5512
+ return args[0] ? step(state, [{ type: "deleteSession", sessionId: args[0] }]) : fail3(state, "Usage: /delete <session-id>");
4859
5513
  case "add-task":
4860
5514
  return addTask(state, args.join(" "));
4861
5515
  case "remove-task":
@@ -4896,17 +5550,35 @@ function runCommand(state, { name, args }) {
4896
5550
  (sessionId, taskId) => step(state, [taskActionEffect(state, sessionId, taskId, name)])
4897
5551
  );
4898
5552
  }
4899
- return fail2(state, `/${name} is not wired up yet.`);
5553
+ return fail3(state, `/${name} is not wired up yet.`);
4900
5554
  }
4901
5555
  function picker(title, items, action, extra = {}) {
4902
5556
  return { title, items, filter: "", index: 0, multi: false, chosen: [], action, ...extra };
4903
5557
  }
4904
5558
  function withSession(state, run) {
4905
5559
  if (!state.sessionId) {
4906
- return fail2(state, "No active plan \u2014 describe a goal first, or load a session with /sessions.");
5560
+ return fail3(state, "No active plan \u2014 describe a goal first, or load a session with /sessions.");
4907
5561
  }
4908
5562
  return run(state.sessionId);
4909
5563
  }
5564
+ function withIdlePlanner(state, run) {
5565
+ return withSession(
5566
+ state,
5567
+ (sessionId) => state.status === "planning" || state.status === "researching" ? fail3(state, "The planner is still answering \u2014 wait for its reply, or /stop it first.") : run(sessionId)
5568
+ );
5569
+ }
5570
+ function rewind(state, arg) {
5571
+ return withIdlePlanner(state, (sessionId) => {
5572
+ if (arg === void 0) {
5573
+ return step(
5574
+ { ...state, rewindTargets: null, overlay: { kind: "picker", picker: picker("Rewind to before\u2026", [], { kind: "rewind" }, { hint: "The chosen message and everything after it are discarded; the tasks stay as they are." }) } },
5575
+ [{ type: "loadRewindTargets", sessionId }]
5576
+ );
5577
+ }
5578
+ if (!/^\d+$/.test(arg)) return fail3(state, "Usage: /rewind [<message>] \u2014 or /rewind alone to pick one.");
5579
+ return step(state, [{ type: "rewindConversation", sessionId, index: Number(arg) }]);
5580
+ });
5581
+ }
4910
5582
  function resolveToggle(arg, current) {
4911
5583
  const value = arg?.toLowerCase();
4912
5584
  if (value === "on") return true;
@@ -4916,7 +5588,7 @@ function resolveToggle(arg, current) {
4916
5588
  }
4917
5589
  function toggleSkill(state, skill, arg) {
4918
5590
  const enabled = resolveToggle(arg, state.skills[skill]);
4919
- if (enabled === null) return fail2(state, `Usage: /${skill} [on|off]`);
5591
+ if (enabled === null) return fail3(state, `Usage: /${skill} [on|off]`);
4920
5592
  return step(state, [{ type: "command", name: skill, action: enabled ? "on" : "off" }]);
4921
5593
  }
4922
5594
  function requestNewSession(state) {
@@ -4936,6 +5608,7 @@ function newSession(state) {
4936
5608
  const closeEffects = state.sessionId ? [{ type: "closeSession", sessionId: state.sessionId }] : [];
4937
5609
  return step({
4938
5610
  ...state,
5611
+ handoff: null,
4939
5612
  sessionId: null,
4940
5613
  goal: "",
4941
5614
  tasks: [],
@@ -4956,14 +5629,14 @@ function newSession(state) {
4956
5629
  }
4957
5630
  function setModel(state, args) {
4958
5631
  if (args[0] === "set" && args[1]) {
4959
- const runner = (0, import_core16.runnerForProvider)(state.plannerProvider);
5632
+ const runner = (0, import_core17.runnerForProvider)(state.plannerProvider);
4960
5633
  const known = runner ? modelsForRunner(state.models, runner) : [];
4961
5634
  if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
4962
- return fail2(state, `${args[1]} was not discovered for ${runner}.`);
5635
+ return fail3(state, `${args[1]} was not discovered for ${runner}.`);
4963
5636
  }
4964
5637
  return step(state, [{ type: "setModel", modelId: args[1] }]);
4965
5638
  }
4966
- if (args.length > 0 && args[0] !== "set") return fail2(state, "Usage: /model [set <model-id>]");
5639
+ if (args.length > 0 && args[0] !== "set") return fail3(state, "Usage: /model [set <model-id>]");
4967
5640
  const items = pickerItemsFor(state, { kind: "set-model" });
4968
5641
  return step(
4969
5642
  {
@@ -4978,21 +5651,21 @@ function setModel(state, args) {
4978
5651
  }
4979
5652
  function plannerItems(state) {
4980
5653
  const installed = new Set(state.runners.map((r) => r.id));
4981
- const agents = import_core16.CLI_PROVIDERS.map((id) => {
4982
- const runner = (0, import_core16.runnerForProvider)(id);
5654
+ const agents = import_core17.CLI_PROVIDERS.map((id) => {
5655
+ const runner = (0, import_core17.runnerForProvider)(id);
4983
5656
  const ready = installed.has(runner);
4984
5657
  return {
4985
5658
  id,
4986
- label: import_core16.ALL_PROVIDERS[id].label,
5659
+ label: import_core17.ALL_PROVIDERS[id].label,
4987
5660
  detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
4988
5661
  selected: state.plannerProvider === id,
4989
5662
  disabled: !ready
4990
5663
  };
4991
5664
  });
4992
- const vendors = import_core16.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
5665
+ const vendors = import_core17.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
4993
5666
  id,
4994
- label: import_core16.ALL_PROVIDERS[id].label,
4995
- detail: import_core16.ALL_PROVIDERS[id].apiKeyEnvVar,
5667
+ label: import_core17.ALL_PROVIDERS[id].label,
5668
+ detail: import_core17.ALL_PROVIDERS[id].apiKeyEnvVar,
4996
5669
  selected: state.plannerProvider === id
4997
5670
  }));
4998
5671
  return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
@@ -5001,7 +5674,7 @@ function setPlanner(state, args) {
5001
5674
  if (args[0]) {
5002
5675
  const id = args[0].toLowerCase();
5003
5676
  if (!KNOWN_PROVIDERS2.includes(id)) {
5004
- return fail2(state, `Unknown planner: ${id}. Run /planner with no arguments to see the list.`);
5677
+ return fail3(state, `Unknown planner: ${id}. Run /planner with no arguments to see the list.`);
5005
5678
  }
5006
5679
  return step(state, [{ type: "setPlanner", provider: id }]);
5007
5680
  }
@@ -5022,7 +5695,7 @@ function setPlannerEffort(state, args) {
5022
5695
  const match = items.find((i) => !i.disabled && (i.id === wanted || wanted === "default" && i.id === DEFAULT_EFFORT));
5023
5696
  if (!match) {
5024
5697
  const available = items.filter((i) => !i.disabled && i.id !== DEFAULT_EFFORT).map((i) => i.id);
5025
- return fail2(state, available.length > 0 ? `Unknown effort: ${args[0]}. Available: ${available.join(", ")}, default.` : items[0]?.label ?? "No effort levels available.");
5698
+ return fail3(state, available.length > 0 ? `Unknown effort: ${args[0]}. Available: ${available.join(", ")}, default.` : items[0]?.label ?? "No effort levels available.");
5026
5699
  }
5027
5700
  return step(state, [{ type: "setPlannerEffort", effort: match.id === DEFAULT_EFFORT ? "" : match.id }]);
5028
5701
  }
@@ -5040,7 +5713,7 @@ function setKey(state, args) {
5040
5713
  if (args[0] === "set" && args[1]) {
5041
5714
  const provider = args[1].toLowerCase();
5042
5715
  if (!KNOWN_PROVIDERS2.includes(provider)) {
5043
- return fail2(state, `Unknown provider: ${provider}. Run /key with no arguments to see the list.`);
5716
+ return fail3(state, `Unknown provider: ${provider}. Run /key with no arguments to see the list.`);
5044
5717
  }
5045
5718
  if (!args[2]) {
5046
5719
  return step({
@@ -5050,16 +5723,16 @@ function setKey(state, args) {
5050
5723
  }
5051
5724
  return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
5052
5725
  }
5053
- const items = import_core16.PROVIDER_PRIORITY.map((id) => ({
5726
+ const items = import_core17.PROVIDER_PRIORITY.map((id) => ({
5054
5727
  id,
5055
- label: import_core16.ALL_PROVIDERS[id].label,
5056
- detail: import_core16.ALL_PROVIDERS[id].apiKeyEnvVar,
5728
+ label: import_core17.ALL_PROVIDERS[id].label,
5729
+ detail: import_core17.ALL_PROVIDERS[id].apiKeyEnvVar,
5057
5730
  selected: state.configuredProviders.includes(id)
5058
5731
  }));
5059
5732
  return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
5060
5733
  }
5061
5734
  function keyPrompt(provider) {
5062
- const meta = import_core16.ALL_PROVIDERS[provider];
5735
+ const meta = import_core17.ALL_PROVIDERS[provider];
5063
5736
  return {
5064
5737
  kind: "prompt",
5065
5738
  title: `${meta.label} API key`,
@@ -5071,23 +5744,23 @@ function keyPrompt(provider) {
5071
5744
  function allowlist(state, args) {
5072
5745
  const [sub, runner, ...rest] = args;
5073
5746
  if (sub === "set") {
5074
- if (!runner || rest.length === 0) return fail2(state, "Usage: /allowlist set <runner> <id1,id2,\u2026>");
5747
+ if (!runner || rest.length === 0) return fail3(state, "Usage: /allowlist set <runner> <id1,id2,\u2026>");
5075
5748
  if (state.runners.length > 0 && !state.runners.some((r) => r.id === runner)) {
5076
- return fail2(state, `Unknown runner "${runner}".`);
5749
+ return fail3(state, `Unknown runner "${runner}".`);
5077
5750
  }
5078
5751
  const modelIds = rest.join(" ").split(",").map((s) => s.trim()).filter(Boolean);
5079
- if (modelIds.length === 0) return fail2(state, "Usage: /allowlist set <runner> <id1,id2,\u2026>");
5752
+ if (modelIds.length === 0) return fail3(state, "Usage: /allowlist set <runner> <id1,id2,\u2026>");
5080
5753
  const known = modelsForRunner(state.models, runner);
5081
5754
  if (state.models.length > 0 && known.length > 0) {
5082
5755
  const stray = modelIds.filter((id) => !known.some((m) => m.id === id));
5083
5756
  if (stray.length > 0) {
5084
- return fail2(state, `Not discovered for ${runner}: ${stray.join(", ")}.`);
5757
+ return fail3(state, `Not discovered for ${runner}: ${stray.join(", ")}.`);
5085
5758
  }
5086
5759
  }
5087
5760
  return step(state, [{ type: "setAllowlist", runner, modelIds }]);
5088
5761
  }
5089
5762
  if (sub === "clear") {
5090
- if (!runner) return fail2(state, "Usage: /allowlist clear <runner>");
5763
+ if (!runner) return fail3(state, "Usage: /allowlist clear <runner>");
5091
5764
  return step(state, [{ type: "setAllowlist", runner, modelIds: [] }]);
5092
5765
  }
5093
5766
  if (sub === "show" || sub === void 0) {
@@ -5109,7 +5782,7 @@ function allowlist(state, args) {
5109
5782
  [{ type: "loadModels" }]
5110
5783
  );
5111
5784
  }
5112
- return fail2(state, "Usage: /allowlist [set <runner> <ids> | clear <runner>]");
5785
+ return fail3(state, "Usage: /allowlist [set <runner> <ids> | clear <runner>]");
5113
5786
  }
5114
5787
  function describeAllowlist(ids) {
5115
5788
  if (!ids || ids.length === 0) return "no restriction";
@@ -5120,7 +5793,7 @@ function runners(state, args) {
5120
5793
  if (runner) {
5121
5794
  const known = state.runners.find((r) => r.id === runner);
5122
5795
  const enabled = resolveToggle(arg, known?.enabled ?? true);
5123
- if (enabled === null) return fail2(state, "Usage: /runners [<runner-id> on|off]");
5796
+ if (enabled === null) return fail3(state, "Usage: /runners [<runner-id> on|off]");
5124
5797
  return step(state, [{ type: "setRunnerEnabled", runner, enabled }]);
5125
5798
  }
5126
5799
  const action = { kind: "set-runners" };
@@ -5136,12 +5809,12 @@ function runners(state, args) {
5136
5809
  }
5137
5810
  function setAutonomous(state, arg) {
5138
5811
  const enabled = resolveToggle(arg, state.autonomous);
5139
- if (enabled === null) return fail2(state, "Usage: /auto [on|off]");
5812
+ if (enabled === null) return fail3(state, "Usage: /auto [on|off]");
5140
5813
  return step({ ...state, autonomous: enabled }, [{ type: "setAutonomous", enabled }]);
5141
5814
  }
5142
5815
  function setMouseCapture(state, arg) {
5143
5816
  const enabled = resolveToggle(arg, state.mouseCapture);
5144
- if (enabled === null) return fail2(state, "Usage: /mouse [on|off]");
5817
+ if (enabled === null) return fail3(state, "Usage: /mouse [on|off]");
5145
5818
  const noted = say(
5146
5819
  // A selection made under capture is meaningless once the terminal owns the
5147
5820
  // mouse again, and its highlight would sit on screen with nothing able to
@@ -5153,7 +5826,7 @@ function setMouseCapture(state, arg) {
5153
5826
  return step(noted, [{ type: "setMouseCapture", enabled }]);
5154
5827
  }
5155
5828
  function openTaskRunnerPicker(state, task) {
5156
- if (task.type !== "ai") return fail2(state, "Manual tasks do not run on an executor, so they have no runner.");
5829
+ if (task.type !== "ai") return fail3(state, "Manual tasks do not run on an executor, so they have no runner.");
5157
5830
  const action = { kind: "set-task-runner", taskId: task.id };
5158
5831
  return step(
5159
5832
  {
@@ -5183,7 +5856,7 @@ function confirmRemoveTask(state, task) {
5183
5856
  function openTaskDepsPicker(state, task) {
5184
5857
  const action = { kind: "set-task-deps", taskId: task.id };
5185
5858
  const items = pickerItemsFor(state, action);
5186
- if (items.length === 0) return fail2(state, `Nothing runs before #${task.order}, so it has no possible dependencies.`);
5859
+ if (items.length === 0) return fail3(state, `Nothing runs before #${task.order}, so it has no possible dependencies.`);
5187
5860
  return step({
5188
5861
  ...state,
5189
5862
  overlay: {
@@ -5197,10 +5870,10 @@ function openTaskDepsPicker(state, task) {
5197
5870
  });
5198
5871
  }
5199
5872
  function openTaskModePicker(state, task) {
5200
- if (task.type !== "ai") return fail2(state, "Manual tasks do not have an executor mode.");
5873
+ if (task.type !== "ai") return fail3(state, "Manual tasks do not have an executor mode.");
5201
5874
  const modes = modesForTask(state.modesByRunner, task);
5202
5875
  if (modes.length === 0) {
5203
- return fail2(state, `${task.assignedRunner ?? "This runner"} declares no modes.`);
5876
+ return fail3(state, `${task.assignedRunner ?? "This runner"} declares no modes.`);
5204
5877
  }
5205
5878
  const action = { kind: "set-task-mode", taskId: task.id };
5206
5879
  return step(
@@ -5216,7 +5889,7 @@ function openTaskModePicker(state, task) {
5216
5889
  );
5217
5890
  }
5218
5891
  function openTaskModelPicker(state, task) {
5219
- if (task.type !== "ai") return fail2(state, "Manual tasks do not have an executor model.");
5892
+ if (task.type !== "ai") return fail3(state, "Manual tasks do not have an executor model.");
5220
5893
  const action = { kind: "set-task-model", taskId: task.id };
5221
5894
  return step(
5222
5895
  {
@@ -5232,8 +5905,8 @@ function openTaskModelPicker(state, task) {
5232
5905
  );
5233
5906
  }
5234
5907
  function openTaskEffortPicker(state, task) {
5235
- if (task.type !== "ai") return fail2(state, "Manual tasks do not have a thinking effort.");
5236
- if (!task.assignedModel) return fail2(state, "Choose a model for this task before setting its thinking effort.");
5908
+ if (task.type !== "ai") return fail3(state, "Manual tasks do not have a thinking effort.");
5909
+ if (!task.assignedModel) return fail3(state, "Choose a model for this task before setting its thinking effort.");
5237
5910
  const action = { kind: "set-task-effort", taskId: task.id };
5238
5911
  return step(
5239
5912
  {
@@ -5299,7 +5972,7 @@ function taskModelCommand(state, args) {
5299
5972
  variants: []
5300
5973
  };
5301
5974
  if (!runnerAccepts(task, model)) {
5302
- return fail2(state, `${model.label} was not discovered for ${task.assignedRunner}.`);
5975
+ return fail3(state, `${model.label} was not discovered for ${task.assignedRunner}.`);
5303
5976
  }
5304
5977
  return assignTaskModel(state, sessionId, task, model);
5305
5978
  });
@@ -5309,7 +5982,7 @@ function taskRunnerCommand(state, args) {
5309
5982
  const task = findTask(state.tasks, taskId);
5310
5983
  if (!args[1]) return openTaskRunnerPicker(state, task);
5311
5984
  const runner = state.runners.find((candidate) => candidate.id === args[1]);
5312
- if (!runner) return fail2(state, `Unknown runner "${args[1]}".`);
5985
+ if (!runner) return fail3(state, `Unknown runner "${args[1]}".`);
5313
5986
  return assignTaskRunner(state, sessionId, task, runner.id, runner.name);
5314
5987
  });
5315
5988
  }
@@ -5318,7 +5991,7 @@ function taskModeCommand(state, args) {
5318
5991
  const task = findTask(state.tasks, taskId);
5319
5992
  if (!args[1]) return openTaskModePicker(state, task);
5320
5993
  const mode = modesForTask(state.modesByRunner, task).find((candidate) => candidate.id === args[1]);
5321
- if (!mode) return fail2(state, `Unsupported mode "${args[1]}" for ${task.assignedRunner}.`);
5994
+ if (!mode) return fail3(state, `Unsupported mode "${args[1]}" for ${task.assignedRunner}.`);
5322
5995
  return assignTaskMode(state, sessionId, task, mode.id, mode.label);
5323
5996
  });
5324
5997
  }
@@ -5326,12 +5999,12 @@ function taskEffortCommand(state, args) {
5326
5999
  return taskCommand(state, args[0], (sessionId, taskId) => {
5327
6000
  const task = findTask(state.tasks, taskId);
5328
6001
  if (!args[1]) return openTaskEffortPicker(state, task);
5329
- if (!task.assignedModel) return fail2(state, "Choose a model for this task before setting its thinking effort.");
6002
+ if (!task.assignedModel) return fail3(state, "Choose a model for this task before setting its thinking effort.");
5330
6003
  const effort = args[1].toLowerCase();
5331
6004
  const value = effort === "default" || effort === "none" ? void 0 : args[1];
5332
6005
  const supported = effortsForTask(state.models, task);
5333
6006
  if (value && supported.length > 0 && !supported.some((variant) => variant.id === value)) {
5334
- return fail2(state, `Unsupported effort "${value}" for ${task.assignedModel.modelLabel}.`);
6007
+ return fail3(state, `Unsupported effort "${value}" for ${task.assignedModel.modelLabel}.`);
5335
6008
  }
5336
6009
  return assignTaskEffort(state, sessionId, task, value);
5337
6010
  });
@@ -5349,9 +6022,9 @@ function addTask(state, title) {
5349
6022
  }
5350
6023
  function taskCommand(state, token, run) {
5351
6024
  return withSession(state, (sessionId) => {
5352
- if (!token) return fail2(state, "Which task? Pass a task id or its number in the plan.");
6025
+ if (!token) return fail3(state, "Which task? Pass a task id or its number in the plan.");
5353
6026
  const taskId = resolveTaskId2(state, token);
5354
- if (!taskId) return fail2(state, `No task matching "${token}" in the current plan.`);
6027
+ if (!taskId) return fail3(state, `No task matching "${token}" in the current plan.`);
5355
6028
  return run(sessionId, taskId);
5356
6029
  });
5357
6030
  }
@@ -5417,7 +6090,7 @@ function createApp(deps) {
5417
6090
 
5418
6091
  // src/tui/effects.ts
5419
6092
  var import_child_process4 = require("child_process");
5420
- var import_core17 = require("@ordewell/core");
6093
+ var import_core18 = require("@ordewell/core");
5421
6094
  var import_plan_utils4 = require("@ordewell/core/plan-utils");
5422
6095
  var ConversationQueue = class {
5423
6096
  tail = Promise.resolve();
@@ -5588,7 +6261,7 @@ async function perform(effect, deps) {
5588
6261
  dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
5589
6262
  return;
5590
6263
  case "setPlanner": {
5591
- const meta = import_core17.ALL_PROVIDERS[effect.provider];
6264
+ const meta = import_core18.ALL_PROVIDERS[effect.provider];
5592
6265
  if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
5593
6266
  const env = { AI_PROVIDER: effect.provider };
5594
6267
  const settings = await api.updateSettings({ env });
@@ -5600,7 +6273,7 @@ async function perform(effect, deps) {
5600
6273
  });
5601
6274
  dispatch({
5602
6275
  type: "notice",
5603
- message: `${(0, import_core17.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
6276
+ message: `${(0, import_core18.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
5604
6277
  });
5605
6278
  await loadModels(deps);
5606
6279
  return;
@@ -5612,7 +6285,7 @@ async function perform(effect, deps) {
5612
6285
  dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
5613
6286
  return;
5614
6287
  case "setApiKey": {
5615
- const meta = import_core17.ALL_PROVIDERS[effect.provider];
6288
+ const meta = import_core18.ALL_PROVIDERS[effect.provider];
5616
6289
  if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
5617
6290
  await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
5618
6291
  deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
@@ -5668,6 +6341,67 @@ async function perform(effect, deps) {
5668
6341
  dispatch({ type: "notice", message: `Loaded "${goal || effect.sessionId}".` });
5669
6342
  return;
5670
6343
  }
6344
+ // The daemon has already adopted the fork, so switching is only the TUI
6345
+ // catching up — the original session is not closed: it may be running.
6346
+ case "forkConversation": {
6347
+ const fork = await api.forkConversation(effect.sessionId);
6348
+ dispatch({ type: "sessionForked", sessionId: fork.sessionId, goal: fork.goal });
6349
+ dispatch({ type: "chatRestored", history: fork.plan.conversationHistory ?? [], sessionId: fork.sessionId });
6350
+ dispatch({ type: "planUpdated", plan: fork.plan, sessionId: fork.sessionId });
6351
+ dispatch({ type: "notice", message: `Forked ${effect.sessionId} into ${fork.sessionId} \u2014 you are in the fork now. /sessions to go back.` });
6352
+ return;
6353
+ }
6354
+ case "loadRewindTargets":
6355
+ dispatch({ type: "rewindTargetsLoaded", targets: await api.rewindTargets(effect.sessionId), sessionId: effect.sessionId });
6356
+ return;
6357
+ case "rewindConversation": {
6358
+ const plan = await api.rewindConversation(effect.sessionId, effect.index);
6359
+ dispatch({ type: "chatRestored", history: plan.conversationHistory ?? [], sessionId: effect.sessionId });
6360
+ dispatch({ type: "planUpdated", plan, sessionId: effect.sessionId });
6361
+ dispatch({ type: "notice", message: "Rewound the conversation. The tasks are unchanged; your next message continues from here." });
6362
+ return;
6363
+ }
6364
+ // The redrawn transcript opens with the summary entry, which is what the
6365
+ // user gets to read — no separate notice to repeat it.
6366
+ case "compactConversation": {
6367
+ const { plan } = await api.compactConversation(effect.sessionId);
6368
+ dispatch({ type: "chatRestored", history: plan.conversationHistory ?? [], sessionId: effect.sessionId });
6369
+ dispatch({ type: "planUpdated", plan, sessionId: effect.sessionId });
6370
+ return;
6371
+ }
6372
+ case "isolationReviewDiff":
6373
+ dispatch({ type: "handoffDiff", diff: await api.reviewRunDiff(effect.sessionId), sessionId: effect.sessionId });
6374
+ return;
6375
+ // A conflict or a refusal is an answer, not a fault: either way the user's
6376
+ // tree is exactly as it was, and the words say what to do next.
6377
+ case "isolationMerge": {
6378
+ const { ok, message } = mergeOutcome(await api.mergeRun(effect.sessionId), effect.branch, effect.group === true);
6379
+ dispatch({ type: ok ? "notice" : "failed", message });
6380
+ return;
6381
+ }
6382
+ case "isolationDiscard":
6383
+ await api.discardRun(effect.sessionId);
6384
+ dispatch({ type: "handoffDiscarded", sessionId: effect.sessionId });
6385
+ dispatch({ type: "notice", message: `Discarded the run and ${effect.branch}.` });
6386
+ return;
6387
+ case "isolationCleanup":
6388
+ await api.cleanupRun(effect.sessionId);
6389
+ await refreshPlan(deps, effect.sessionId);
6390
+ dispatch({ type: "notice", message: `Removed the run's worktrees and task branches; ${effect.branch} is kept.` });
6391
+ return;
6392
+ // Spawns runners again, so it needs the stream a run needs — the blocked
6393
+ // one closed itself when nothing started.
6394
+ case "isolationContinue": {
6395
+ const { sessionId } = effect;
6396
+ await withExecutionStream(deps, sessionId, () => effect.mode === "stash" ? api.continueWithStash(sessionId) : api.continueWithoutIsolation(sessionId));
6397
+ await refreshPlan(deps, sessionId);
6398
+ return;
6399
+ }
6400
+ case "resolveConflict":
6401
+ await api.resolveConflictAsTask(effect.sessionId, effect.taskId);
6402
+ await refreshPlan(deps, effect.sessionId);
6403
+ dispatch({ type: "notice", message: "Added a task that merges the conflicted branch by hand. Run it with E (run plan); when it lands, the conflicted task lands through it." });
6404
+ return;
5671
6405
  case "deleteSession":
5672
6406
  await api.deleteSession(effect.sessionId, workspace);
5673
6407
  await loadSessions(deps);
@@ -5700,7 +6434,7 @@ function osc52(text) {
5700
6434
  function copySelection(deps, text) {
5701
6435
  if (!text) return;
5702
6436
  const lines = text.split("\n").length;
5703
- const command = (0, import_core17.clipboardCopyCommand)(deps.hasBin);
6437
+ const command = (0, import_core18.clipboardCopyCommand)(deps.hasBin);
5704
6438
  if (command) {
5705
6439
  try {
5706
6440
  (deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
@@ -5831,7 +6565,7 @@ function onExecutionEvent(dispatch, event, sessionId) {
5831
6565
  case "status_update": {
5832
6566
  const updates = {};
5833
6567
  for (const task of event.tasks ?? []) {
5834
- updates[String(task.id)] = { status: String(task.status), idleSince: task.idleSince ?? null };
6568
+ updates[String(task.id)] = { status: String(task.status), idleSince: task.idleSince ?? null, isolation: task.isolation };
5835
6569
  }
5836
6570
  dispatch({ type: "tasksStatus", updates, sessionId });
5837
6571
  return;
@@ -5842,7 +6576,7 @@ function onExecutionEvent(dispatch, event, sessionId) {
5842
6576
  // The extension shows these in its checkpoint panel; here they are
5843
6577
  // transcript lines, which is the TUI's equivalent surface.
5844
6578
  case "checkpoint":
5845
- dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core17.truncateCheckpointSummary)(event.summary)}` });
6579
+ dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core18.truncateCheckpointSummary)(event.summary)}` });
5846
6580
  return;
5847
6581
  case "approval_decided":
5848
6582
  dispatch({ type: "notice", message: describeApprovalDecision2(event) });
@@ -5873,10 +6607,39 @@ function onExecutionEvent(dispatch, event, sessionId) {
5873
6607
  case "execution_stopped":
5874
6608
  dispatch({ type: "executionComplete", stopped: true, sessionId });
5875
6609
  return;
5876
- // task_output / plan_token / plan_thinking are raw runner chatter; the
5877
- // status line and the plan pane already say everything the user needs.
5878
- default:
6610
+ // Nothing started; the user chooses how to go on.
6611
+ case "isolation_blocked":
6612
+ dispatch({ type: "isolationBlocked", message: event.message, ...event.repos ? { repos: event.repos } : {}, sessionId });
6613
+ return;
6614
+ // How the run isolates. The daemon has no toast channel, so this is the only place the user hears it.
6615
+ case "notice":
6616
+ dispatch({ type: "notice", message: event.message });
6617
+ return;
6618
+ case "isolation_handoff":
6619
+ dispatch({ type: "isolationHandoff", handoff: { repos: event.repos, landed: event.landed }, sessionId });
5879
6620
  return;
6621
+ // Raw runner chatter, and the planner's own turn: the status line and the
6622
+ // plan pane already say everything the user needs during a run. Listed
6623
+ // rather than defaulted, so a new SessionMessage variant fails to compile
6624
+ // here until someone decides what a run's watcher does with it.
6625
+ case "task_updated":
6626
+ case "task_output":
6627
+ case "plan_thinking":
6628
+ case "planner_liveness":
6629
+ case "research_step":
6630
+ case "plan_token":
6631
+ case "research_step_done":
6632
+ case "approval_request":
6633
+ case "approval_settled":
6634
+ return;
6635
+ // Its asker already has the result, from the merge request itself.
6636
+ case "isolation_merge":
6637
+ return;
6638
+ default: {
6639
+ const unhandled = event;
6640
+ void unhandled;
6641
+ return;
6642
+ }
5880
6643
  }
5881
6644
  }
5882
6645
  async function refreshPlan(deps, sessionId) {
@@ -6195,7 +6958,7 @@ async function handleTui(subArgs) {
6195
6958
  }
6196
6959
  const workspace = flag(subArgs, "--workspace") || process.cwd();
6197
6960
  try {
6198
- (0, import_core18.assertWorkspaceExists)(workspace);
6961
+ (0, import_core19.assertWorkspaceExists)(workspace);
6199
6962
  } catch {
6200
6963
  console.error(
6201
6964
  `Workspace "${workspace}" does not exist.
@@ -6204,7 +6967,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
6204
6967
  );
6205
6968
  process.exit(1);
6206
6969
  }
6207
- registerSkillCommands((0, import_core18.createSkillsService)(workspace).listSkills());
6970
+ registerSkillCommands((0, import_core19.createSkillsService)(workspace).listSkills());
6208
6971
  const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
6209
6972
  const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
6210
6973
  const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
@@ -6223,7 +6986,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
6223
6986
  conversationQueue,
6224
6987
  port,
6225
6988
  dispatch: (action) => app.dispatch(action),
6226
- newSessionId: () => (0, import_core18.mintSessionId)(),
6989
+ newSessionId: () => (0, import_core19.mintSessionId)(),
6227
6990
  setEnvVar: (key, value) => {
6228
6991
  writeEnvVar(findEnvFile(), key, value);
6229
6992
  process.env[key] = value;
@@ -6311,6 +7074,10 @@ var COMMANDS = {
6311
7074
  "task-deps": handleTaskDeps,
6312
7075
  terminal: handleTerminal,
6313
7076
  "sessions": handleSessions,
7077
+ fork: handleFork,
7078
+ compact: handleCompact,
7079
+ rewind: handleRewind,
7080
+ handoff: handleHandoff,
6314
7081
  tui: handleTui
6315
7082
  };
6316
7083
 
@@ -6329,7 +7096,7 @@ function cliVersion() {
6329
7096
 
6330
7097
  // src/main.ts
6331
7098
  async function main() {
6332
- (0, import_core19.migrateOldConfigDir)();
7099
+ (0, import_core20.migrateOldConfigDir)();
6333
7100
  loadEnvFile();
6334
7101
  const argv = process.argv.slice(2);
6335
7102
  const command = argv[0] ?? (process.stdin.isTTY ? "tui" : "--help");