@ordewell/cli 0.10.4 → 0.10.6

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 +370 -299
  2. package/package.json +3 -3
package/dist/main.js CHANGED
@@ -145,6 +145,7 @@ Planning:
145
145
  ordewell fork Continue in a copy of the conversation and its tasks
146
146
  ordewell compact Condense the conversation into a summary (tasks are kept)
147
147
  ordewell rewind [<n>] Fork the conversation from before message <n> (no <n>: list them)
148
+ ordewell resend cancel Keep a planner turn waiting on its usage limit from going at the reset
148
149
  ordewell handoff [action] Review, merge, discard or clean up an isolated run
149
150
 
150
151
  Tasks (<id> is an order number or a task ID):
@@ -153,7 +154,7 @@ Tasks (<id> is an order number or a task ID):
153
154
  ordewell complete <id> Mark a task complete (alias: mark-complete)
154
155
  ordewell uncomplete <id> Mark a completed task not done
155
156
  ordewell skip <id> Skip a task: nothing it did lands, dependents go ahead
156
- ordewell force-start <id> Start a task now, ignoring dependencies (asks past a merge gate)
157
+ ordewell force-start <id> Approve a task and start it now, or once its dependencies finish (asks past a merge gate)
157
158
  ordewell run-task <id> Run only one task
158
159
  ordewell retry <id> Re-run a failed task
159
160
  ordewell continue <id> <msg> Continue a finished task in its saved runner session
@@ -548,8 +549,7 @@ var ApiClient = class _ApiClient {
548
549
  ...body ? { "Content-Type": "application/json" } : {},
549
550
  ...token ? { Authorization: (0, import_core4.bearerHeaderValue)(token) } : {}
550
551
  },
551
- // Planner research on a real repository regularly exceeds two minutes.
552
- // The benchmark harness uses the same 15-minute default explicitly;
552
+ // Planner research on a real repository regularly exceeds two minutes;
553
553
  // keep the env override for callers that need a tighter/looser bound.
554
554
  timeout: Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : DEFAULT_HTTP_TIMEOUT_MS
555
555
  };
@@ -631,70 +631,74 @@ var ApiClient = class _ApiClient {
631
631
  return this.call("POST", `/api/plans/${sessionId}/review/approve`, "Approve failed");
632
632
  }
633
633
  markTaskComplete(sessionId, taskId) {
634
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/complete`, "Mark complete failed");
634
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/complete`, "Mark complete failed");
635
635
  }
636
636
  /** Skip: end the task without landing its work, so what depends on it goes ahead once the work it waited on is done; never a mark complete. */
637
637
  skipTask(sessionId, taskId, reason) {
638
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/skip`, "Skip failed", reason ? { reason } : {});
638
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/skip`, "Skip failed", reason ? { reason } : {});
639
639
  }
640
640
  markTaskIncomplete(sessionId, taskId) {
641
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/uncomplete`, "Mark not done failed");
641
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/uncomplete`, "Mark not done failed");
642
642
  }
643
- /** Keep a task paused on a usage limit from resuming on its own; `cancelled` is false when none was scheduled. */
643
+ /** Keep a task paused on a usage limit from resuming on its own; a task with none scheduled is refused with the reason. */
644
644
  cancelResume(sessionId, taskId) {
645
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/resume/cancel`, "Cancel resume failed");
645
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/resume/cancel`, "Cancel resume failed");
646
+ }
647
+ /** Keep what waits on the planner's usage limit from being sent at the reset; `cancelled` is false when nothing waited. */
648
+ cancelPlannerResend(sessionId) {
649
+ return this.call("POST", `/api/plans/${sessionId}/converse/resend/cancel`, "Cancel resend failed");
646
650
  }
647
651
  /** Resume a task paused on a usage limit now, in its saved session, once the user says the limit has reset. */
648
652
  resumeNow(sessionId, taskId) {
649
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/resume/now`, "Resume failed");
653
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/resume/now`, "Resume failed");
650
654
  }
651
655
  /** run | force-start | retry | cancel — real orchestrator work, not a status patch. */
652
656
  taskControl(sessionId, taskId, action) {
653
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/${action}`, `${action} failed`);
657
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/${action}`, `${action} failed`);
654
658
  }
655
659
  /** A user message to a structured task: delivered now if it waits for input, else queued behind its turn. */
656
660
  sendTaskMessage(sessionId, taskId, text) {
657
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages`, "Send message failed", { text });
661
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/messages`, "Send message failed", { text });
658
662
  }
659
663
  /** Force send: interrupt the task's running turn and deliver `text` next, ahead of anything queued (ADR-0023, F1). */
660
664
  forceSendTaskMessage(sessionId, taskId, text) {
661
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages/now`, "Send now failed", { text });
665
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/messages/now`, "Send now failed", { text });
662
666
  }
663
667
  /** Run the task runner's own command `/name args` as a turn of its own (#66). */
664
668
  invokeTaskCommand(sessionId, taskId, name, args) {
665
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/commands`, "Command failed", { name, args });
669
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/commands`, "Command failed", { name, args });
666
670
  }
667
- /** Force send a message still queued; `sent` is false once the runner already has it. */
671
+ /** Force send a message still queued; one the runner already has is refused with the reason. */
668
672
  forceSendQueuedTaskMessage(sessionId, taskId, messageId) {
669
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages/${encodeURIComponent(messageId)}/now`, "Send now failed");
673
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/messages/${encodeURIComponent(messageId)}/now`, "Send now failed");
670
674
  }
671
675
  /** Continue a finished structured task in its saved session, with `text` as its next turn. */
672
676
  continueTask(sessionId, taskId, text) {
673
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/continue`, "Continue failed", { text });
677
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/continue`, "Continue failed", { text });
674
678
  }
675
- /** `removed` is false once the message was already delivered. */
679
+ /** Take back a queued message; one already delivered is refused with the reason. */
676
680
  removeQueuedTaskMessage(sessionId, taskId, messageId) {
677
- return this.call("DELETE", `/api/plans/${sessionId}/tasks/${taskId}/messages/${encodeURIComponent(messageId)}`, "Remove message failed");
681
+ return this.call("DELETE", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/messages/${encodeURIComponent(messageId)}`, "Remove message failed");
678
682
  }
679
683
  interruptTask(sessionId, taskId) {
680
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/interrupt`, "Interrupt failed");
684
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/interrupt`, "Interrupt failed");
681
685
  }
682
686
  /** Let a task go on from the checkpoint it waits at; refused, with the reason, when none waits. */
683
687
  approveTaskCheckpoint(sessionId, taskId) {
684
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/checkpoint/approve`, "Approve checkpoint failed");
688
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/checkpoint/approve`, "Approve checkpoint failed");
685
689
  }
686
690
  /** Turn a task back at its checkpoint; `reason` is what the agent is told. */
687
691
  rejectTaskCheckpoint(sessionId, taskId, reason) {
688
- return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/checkpoint/reject`, "Reject checkpoint failed", reason ? { reason } : {});
692
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/checkpoint/reject`, "Reject checkpoint failed", reason ? { reason } : {});
689
693
  }
690
694
  addTask(sessionId, task) {
691
695
  return this.call("POST", `/api/plans/${sessionId}/tasks`, "Add task failed", task);
692
696
  }
693
697
  updateTask(sessionId, taskId, changes) {
694
- return this.call("PUT", `/api/plans/${sessionId}/tasks/${taskId}`, "Update task failed", changes);
698
+ return this.call("PUT", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}`, "Update task failed", changes);
695
699
  }
696
700
  removeTask(sessionId, taskId) {
697
- return this.call("DELETE", `/api/plans/${sessionId}/tasks/${taskId}`, "Remove task failed");
701
+ return this.call("DELETE", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}`, "Remove task failed");
698
702
  }
699
703
  /**
700
704
  * Make a saved session live on the server. `getSession` only reads the file;
@@ -704,7 +708,7 @@ var ApiClient = class _ApiClient {
704
708
  async adoptSession(sessionId, workspace) {
705
709
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
706
710
  const res = await this.call("POST", `/api/sessions/${sessionId}/load${qs}`, "Failed to load session");
707
- return { plan: res.plan, goal: res.goal ?? "" };
711
+ return { plan: res.plan, goal: res.goal ?? "", ...res.run ? { run: res.run } : {} };
708
712
  }
709
713
  deleteSession(sessionId, workspace) {
710
714
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
@@ -735,7 +739,7 @@ var ApiClient = class _ApiClient {
735
739
  }
736
740
  /** The ancestors a task waits on at its merge gate (ADR-0020); empty when nothing gates it. */
737
741
  async getMergeGate(sessionId, taskId) {
738
- const res = await this.call("GET", `/api/plans/${sessionId}/tasks/${taskId}/merge-gate`, "Could not read the merge gate");
742
+ const res = await this.call("GET", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/merge-gate`, "Could not read the merge gate");
739
743
  return res.mergeGate ?? [];
740
744
  }
741
745
  async reviewRunDiff(sessionId) {
@@ -765,7 +769,7 @@ var ApiClient = class _ApiClient {
765
769
  await this.call("POST", `/api/plans/${sessionId}/isolation/${segment}`, failure);
766
770
  }
767
771
  async resolveConflictAsTask(sessionId, taskId) {
768
- const res = await this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/resolve-conflict`, "Could not add a resolver task");
772
+ const res = await this.call("POST", `/api/plans/${sessionId}/tasks/${encodeURIComponent(taskId)}/resolve-conflict`, "Could not add a resolver task");
769
773
  return res.plan;
770
774
  }
771
775
  async closeSession(sessionId) {
@@ -794,13 +798,13 @@ var ApiClient = class _ApiClient {
794
798
  async getTaskLogAttempts(sessionId, taskId, workspace) {
795
799
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
796
800
  const res = await this.call("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log${qs}`, "Could not read the task log");
797
- return res.attempts;
801
+ return res;
798
802
  }
799
803
  /** One attempt's saved log, for `replayTaskLog`. */
800
804
  async getTaskLog(sessionId, taskId, attempt, workspace) {
801
805
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
802
806
  const res = await this.call("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log/${attempt}${qs}`, "Could not read the task log");
803
- return res.events;
807
+ return res;
804
808
  }
805
809
  /**
806
810
  * The one place a session socket is built, so the token travels on both
@@ -1041,12 +1045,7 @@ function allTasksOf(plan) {
1041
1045
  return (0, import_core5.rejoinPlanTree)(tasks, executionLog);
1042
1046
  }
1043
1047
  function resolveTaskId(plan, identifier) {
1044
- const allTasks = allTasksOf(plan);
1045
- const byRef = (0, import_core5.resolveTaskRef)(allTasks, identifier);
1046
- if (byRef) return byRef.id;
1047
- const prefixMatches = allTasks.filter((t) => typeof t.id === "string" && t.id.startsWith(identifier));
1048
- if (prefixMatches.length === 1) return prefixMatches[0].id;
1049
- return void 0;
1048
+ return (0, import_core5.resolveUserTaskRef)(allTasksOf(plan), identifier)?.id;
1050
1049
  }
1051
1050
 
1052
1051
  // src/utils/session.ts
@@ -1610,10 +1609,21 @@ var STATE_OF = {
1610
1609
  function reposOf(run) {
1611
1610
  if (Array.isArray(run.repos)) {
1612
1611
  const repos = run.repos.filter((r) => r && typeof r.path === "string" && typeof r.integrationBranch === "string" && typeof r.baseRef === "string");
1613
- return repos.length > 0 ? repos.map((r) => ({ path: String(r.path), integrationBranch: String(r.integrationBranch), baseRef: String(r.baseRef) })) : null;
1612
+ return repos.length > 0 ? repos.map((r) => ({
1613
+ path: String(r.path),
1614
+ integrationBranch: String(r.integrationBranch),
1615
+ baseRef: String(r.baseRef),
1616
+ ...typeof r.baseBranch === "string" && r.baseBranch ? { baseBranch: r.baseBranch } : {},
1617
+ ...r.startedDetached === true ? { startedDetached: true } : {}
1618
+ })) : null;
1614
1619
  }
1615
1620
  if (typeof run.integrationBranch === "string" && typeof run.baseRef === "string") {
1616
- return [{ path: ".", integrationBranch: run.integrationBranch, baseRef: run.baseRef }];
1621
+ return [{
1622
+ path: ".",
1623
+ integrationBranch: run.integrationBranch,
1624
+ baseRef: run.baseRef,
1625
+ ...typeof run.baseBranch === "string" && run.baseBranch ? { baseBranch: run.baseBranch } : {}
1626
+ }];
1617
1627
  }
1618
1628
  return null;
1619
1629
  }
@@ -1664,15 +1674,11 @@ function handoffBranch(handoff) {
1664
1674
  return [...new Set(handoff.repos.map((r) => r.integrationBranch))].join(", ");
1665
1675
  }
1666
1676
  function handoffBase(handoff, length) {
1667
- const group = isRepoGroup(handoff);
1668
- return handoff.repos.map((r) => `${group ? `${r.path} ` : ""}${r.baseRef.slice(0, length)}`).join(", ");
1669
- }
1670
- function isRepoGroup(handoff) {
1671
- return handoff.repos.some((r) => r.path !== ".");
1672
- }
1673
- function reposWithWork(handoff) {
1674
- const withWork = handoff.repos.filter((r) => r.landed.length > 0);
1675
- return (withWork.length > 0 ? withWork : handoff.repos).map((r) => r.path);
1677
+ const group = (0, import_core10.isRepoGroup)(handoff);
1678
+ return handoff.repos.map((r) => {
1679
+ const ref = r.baseRef.slice(0, length);
1680
+ return `${group ? `${r.path}: ` : ""}${r.baseBranch ? `${r.baseBranch} @ ${ref}` : `${r.startedDetached ? "detached at" : "base"} ${ref}`}`;
1681
+ }).join(", ");
1676
1682
  }
1677
1683
  function repairedLanded(handoff) {
1678
1684
  return handoff.landed.filter((t) => t.repairedFiles?.length);
@@ -1750,7 +1756,7 @@ async function followExecution(api, sessionId, start, onBlocked) {
1750
1756
  for (const t of entries) {
1751
1757
  const icon = iconFor(t.status);
1752
1758
  const verdict = t.verdict ? `: ${t.verdict.outcome.toUpperCase()}: ${t.verdict.reason.slice(0, 60)}` : "";
1753
- const waits = t.waitsForApproval ? `: ${inLine(import_core12.WAITS_FOR_APPROVAL_LABEL)}: \`ordewell run-task <id>\` starts it` : "";
1759
+ const waits = t.waitsForApproval ? `: ${inLine(import_core12.WAITS_FOR_APPROVAL_LABEL)}: \`ordewell force-start <id>\` approves it, \`ordewell run\` approves all` : "";
1754
1760
  lines.push(` ${icon} #${t.id.slice(-4)} ${t.status}${verdict}${waits}`);
1755
1761
  }
1756
1762
  const output = lines.join("\n");
@@ -1812,10 +1818,10 @@ Done. ${summary.completed || 0} completed, ${summary.failed || 0} failed, ${summ
1812
1818
  }
1813
1819
  if (event.type === "isolation_handoff") {
1814
1820
  const n = event.landed.length;
1815
- const where = isRepoGroup(event) ? ` in ${event.repos.map((r) => r.path).join(", ")}` : "";
1821
+ const where = (0, import_core10.isRepoGroup)(event) ? ` in ${event.repos.map((r) => r.path).join(", ")}` : "";
1816
1822
  console.log(`
1817
1823
  Run finished on ${handoffBranch(event)}${where}: ${n} task${n === 1 ? "" : "s"} landed.`);
1818
- if (isRepoGroup(event)) for (const line of repoResultLines(event)) console.log(` ${line}`);
1824
+ if ((0, import_core10.isRepoGroup)(event)) for (const line of repoResultLines(event)) console.log(` ${line}`);
1819
1825
  if (event.deferredTo === "supervisor") console.log(" The supervisor may merge it.");
1820
1826
  console.log(" `ordewell handoff review|merge|discard|cleanup` to land it.");
1821
1827
  }
@@ -1852,7 +1858,8 @@ async function handleRun(subArgs, injectedApi) {
1852
1858
  const onBlocked = hasFlag(subArgs, "--stash") ? "stash" : hasFlag(subArgs, "--without-isolation") ? "shared" : void 0;
1853
1859
  await followExecution(api, sessionId, async () => {
1854
1860
  try {
1855
- await api.executePlan(sessionId);
1861
+ const started = await api.executePlan(sessionId);
1862
+ if (started.message) console.error(started.message);
1856
1863
  } catch (err) {
1857
1864
  console.error(`Failed to start execution: ${err.message}`);
1858
1865
  process.exit(1);
@@ -2656,27 +2663,28 @@ function formatCost(currency, amount) {
2656
2663
  function tokenLine(blocks2, cols) {
2657
2664
  const usage = blocks2[blocks2.length - 1];
2658
2665
  if (usage?.type !== "usage") return null;
2659
- const parts = usageMeasures(usage.totals, false);
2666
+ const parts = usageMeasures(usage.totals);
2660
2667
  if (usage.model && !usage.contextFill) parts.push("ctx unknown");
2661
2668
  if (usage.contextFill) parts.splice(2, 0, `${Math.round(usage.contextFill.usedTokens / usage.contextFill.windowTokens * 100)}% ctx`);
2662
2669
  return parts.length > 0 ? style.grey(truncate(parts.join(" \xB7 "), cols)) : null;
2663
2670
  }
2664
- function usageMeasures(totals, unknown) {
2671
+ function usageMeasures(totals) {
2665
2672
  return [
2666
- ...totals.inputTokens !== void 0 ? [`${compactCount(totals.inputTokens)} in`] : unknown ? ["unknown in"] : [],
2667
- ...totals.outputTokens !== void 0 ? [`${compactCount(totals.outputTokens)} out`] : unknown ? ["unknown out"] : [],
2673
+ ...totals.inputTokens !== void 0 ? [`${compactCount(totals.inputTokens)} in`] : [],
2674
+ ...totals.outputTokens !== void 0 ? [`${compactCount(totals.outputTokens)} out`] : [],
2668
2675
  ...totals.cachedInputTokens !== void 0 ? [`${compactCount(totals.cachedInputTokens)} cached`] : [],
2669
- ...Object.entries(totals.reportedCost ?? {}).map(([currency, amount]) => formatCost(currency, amount)),
2670
- ...unknown && !Object.keys(totals.reportedCost ?? {}).length ? ["cost unknown"] : []
2676
+ ...Object.entries(totals.reportedCost ?? {}).map(([currency, amount]) => formatCost(currency, amount))
2671
2677
  ];
2672
2678
  }
2673
2679
  function taskUsageText(usage) {
2674
2680
  return [
2675
- ...usageMeasures(usage.totals, true),
2676
- ...usage.attempt !== void 0 ? [usage.contextFill ? `${Math.round(usage.contextFill.usedTokens / usage.contextFill.windowTokens * 100)}% ctx` : "ctx unknown"] : [],
2677
- usage.wallTimeMs !== void 0 ? `${Math.floor(usage.wallTimeMs / 1e3)}s` : "time unknown"
2681
+ ...usageMeasures(usage.totals),
2682
+ ...usage.attempt !== void 0 ? [usage.contextFill ? `ctx ${Math.round(usage.contextFill.usedTokens / usage.contextFill.windowTokens * 100)}%` : "ctx unknown"] : []
2678
2683
  ].join(" \xB7 ");
2679
2684
  }
2685
+ function assignmentFlagText(row) {
2686
+ return `${row.assignmentFlag} \u2014 ${row.assignmentConsequence}`;
2687
+ }
2680
2688
 
2681
2689
  // src/commands/status.ts
2682
2690
  var import_fs4 = require("fs");
@@ -2694,7 +2702,7 @@ async function handleStatus(subArgs, injectedApi) {
2694
2702
  emitJson(session, outputPath);
2695
2703
  return;
2696
2704
  }
2697
- printOneSession(sessionId, session.meta, session.plan);
2705
+ printOneSession(sessionId, session.meta, session.plan, await assignmentScope(api));
2698
2706
  } else {
2699
2707
  const sessions = await api.getSessions(workspace);
2700
2708
  if (asJson) {
@@ -2733,7 +2741,20 @@ function emitJson(payload, outputPath) {
2733
2741
  process.exit(1);
2734
2742
  }
2735
2743
  }
2736
- function printOneSession(sessionId, meta, plan) {
2744
+ async function assignmentScope(api) {
2745
+ try {
2746
+ const [runners2, settings, models] = await Promise.all([api.getRunners(), api.getSettings(), api.getModels()]);
2747
+ if (!Array.isArray(runners2.runners) || runners2.runners.length === 0) return void 0;
2748
+ return {
2749
+ enabledRunners: runners2.runners.filter((r) => r.enabled).map((r) => r.id),
2750
+ allowlist: settings.modelAllowlist ?? {},
2751
+ models: models.modelsByRunner ?? {}
2752
+ };
2753
+ } catch {
2754
+ return void 0;
2755
+ }
2756
+ }
2757
+ function printOneSession(sessionId, meta, plan, runnerScope) {
2737
2758
  const allTasks = allTasksOf(plan);
2738
2759
  const everyTask = (0, import_core15.flattenTasks)(allTasks);
2739
2760
  const blocked = (0, import_core15.blockingFailures)(everyTask.map((t) => ({ ...t, subtasks: [], dependencies: t.dependencies ?? [] })));
@@ -2742,20 +2763,23 @@ function printOneSession(sessionId, meta, plan) {
2742
2763
  console.log(`Goal: ${meta.goal}`);
2743
2764
  console.log(`Runner: ${(meta.runners || []).join(", ")} | Status: ${meta.status}`);
2744
2765
  console.log(`Phase: ${plan?.phase || "unknown"} | Created: ${meta.createdAt}`);
2766
+ const isolation = isolationOfPlan(plan);
2767
+ if (isolation) console.log(`Base: ${handoffBase(isolation.handoff, 8)}`);
2745
2768
  console.log(`Tasks: ${everyTask.length} total | ${count("completed")} done | ${count("failed")} failed | ${count("in_progress")} running | ${count("blocked")} blocked | ${count("skipped")} skipped
2746
2769
  `);
2747
2770
  const usage = (0, import_core15.planUsage)(allTasks);
2748
2771
  if (usage.attempts) console.log(`Tasks total: ${taskUsageText(usage)}`);
2749
2772
  const record = servedApprovalRecord(plan);
2750
- const approval = (row, ref) => row.waitsForApproval ? `${row.waitsForApproval}: \`ordewell run-task ${ref}\` starts it, or \`ordewell run\` runs the plan` : null;
2773
+ const approval = (row, ref) => row.waitsForApproval ? `${row.waitsForApproval}: \`ordewell force-start ${ref}\` approves it, \`ordewell run\` approves all` : null;
2751
2774
  for (const t of allTasks) {
2752
2775
  const icon = iconFor(blocked.has(t.id) ? "blocked" : t.status);
2753
2776
  const model = t.assignedModel ? ` (${t.assignedModel.modelLabel})` : "";
2754
2777
  const shortId = (t.id ?? "").slice(-8);
2755
2778
  console.log(` ${(0, import_core15.taskOrderLabel)(t).padStart(2)}. ${icon} [${shortId}] ${t.title}${model}`);
2756
- const row = (0, import_core15.taskRowView)({ ...t, dependencies: t.dependencies ?? [], waitsForApproval: (0, import_core15.waitsForApproval)(t, record) }, { tasks: allTasks });
2779
+ const row = (0, import_core15.taskRowView)({ ...t, dependencies: t.dependencies ?? [], waitsForApproval: (0, import_core15.waitsForApproval)(t, record) }, { tasks: allTasks, runnerScope });
2757
2780
  const waiting = approval(row, (0, import_core15.taskOrderLabel)(t));
2758
2781
  if (waiting) console.log(` ${waiting}`);
2782
+ if (row.assignmentFlag) console.log(` \u26A0 ${assignmentFlagText(row)}`);
2759
2783
  if (row.blockedBy) console.log(` ${(0, import_core15.blockedByLabel)(row.blockedBy)}`);
2760
2784
  if (row.status === "skipped") console.log(` ${(0, import_core15.skippedLabel)(row.skipReason)}`);
2761
2785
  if (row.usage) console.log(` ${taskUsageText(row.usage)}`);
@@ -2764,9 +2788,10 @@ function printOneSession(sessionId, meta, plan) {
2764
2788
  const subModel = sub.assignedModel ? ` (${sub.assignedModel.modelLabel})` : "";
2765
2789
  const subShortId = (sub.id ?? "").slice(-8);
2766
2790
  console.log(` ${(0, import_core15.taskOrderLabel)(sub, t)}. ${subIcon} [${subShortId}] ${sub.title}${subModel}`);
2767
- const subRow = (0, import_core15.taskRowView)({ ...sub, dependencies: sub.dependencies ?? [], waitsForApproval: (0, import_core15.waitsForApproval)(sub, record) }, { parent: t, tasks: allTasks });
2791
+ const subRow = (0, import_core15.taskRowView)({ ...sub, dependencies: sub.dependencies ?? [], waitsForApproval: (0, import_core15.waitsForApproval)(sub, record) }, { parent: t, tasks: allTasks, runnerScope });
2768
2792
  const subWaiting = approval(subRow, (0, import_core15.taskOrderLabel)(sub, t));
2769
2793
  if (subWaiting) console.log(` ${subWaiting}`);
2794
+ if (subRow.assignmentFlag) console.log(` \u26A0 ${assignmentFlagText(subRow)}`);
2770
2795
  if (subRow.blockedBy) console.log(` ${(0, import_core15.blockedByLabel)(subRow.blockedBy)}`);
2771
2796
  if (subRow.status === "skipped") console.log(` ${(0, import_core15.skippedLabel)(subRow.skipReason)}`);
2772
2797
  if (subRow.usage) console.log(` ${taskUsageText(subRow.usage)}`);
@@ -2779,7 +2804,7 @@ function servedApprovalRecord(plan) {
2779
2804
  }
2780
2805
 
2781
2806
  // src/commands/stop.ts
2782
- async function handleStop(subArgs) {
2807
+ async function handleStop(subArgs, injectedApi) {
2783
2808
  if (subArgs.includes("--server") || subArgs[0] === "server") {
2784
2809
  const stopped = await stopDaemon(resolvePort(subArgs));
2785
2810
  if (!stopped) process.exit(1);
@@ -2796,10 +2821,9 @@ async function handleStop(subArgs) {
2796
2821
  sessionId = last.sessionId;
2797
2822
  console.error(`Stopping last session: ${sessionId}`);
2798
2823
  }
2799
- const port = await ensureDaemon(resolvePort(subArgs));
2800
- const api = new ApiClient(port);
2801
- await api.stopExecution(sessionId);
2802
- console.log("Execution stopped.");
2824
+ const api = await connect(subArgs, injectedApi);
2825
+ const { message } = await api.stopExecution(sessionId);
2826
+ console.log(message ?? "Execution stopped.");
2803
2827
  }
2804
2828
 
2805
2829
  // src/commands/web.ts
@@ -3214,7 +3238,7 @@ var PAST_TENSE = {
3214
3238
  retry: "Task retried.",
3215
3239
  cancel: "Task cancelled."
3216
3240
  };
3217
- async function withResolvedTask(subArgs, usage, injectedApi, run) {
3241
+ async function sessionAndRef(subArgs, usage, injectedApi) {
3218
3242
  const workspace = flag(subArgs, "--workspace") || process.cwd();
3219
3243
  let sessionId = flag(subArgs, "--session-id");
3220
3244
  if (!sessionId) {
@@ -3225,12 +3249,20 @@ async function withResolvedTask(subArgs, usage, injectedApi, run) {
3225
3249
  }
3226
3250
  sessionId = last.sessionId;
3227
3251
  }
3228
- const identifier = positionals(subArgs)[0];
3229
- if (!identifier) {
3252
+ const ref = positionals(subArgs)[0];
3253
+ if (!ref) {
3230
3254
  console.error(usage);
3231
3255
  process.exit(1);
3232
3256
  }
3233
3257
  const api = injectedApi || new ApiClient(await ensureDaemon(resolvePort(subArgs)));
3258
+ return { api, sessionId, ref, workspace };
3259
+ }
3260
+ async function withTaskRef(subArgs, usage, injectedApi, run) {
3261
+ const { api, sessionId, ref } = await sessionAndRef(subArgs, usage, injectedApi);
3262
+ await run(api, sessionId, ref);
3263
+ }
3264
+ async function withResolvedTask(subArgs, usage, injectedApi, run) {
3265
+ const { api, sessionId, ref, workspace } = await sessionAndRef(subArgs, usage, injectedApi);
3234
3266
  let plan;
3235
3267
  try {
3236
3268
  plan = (await api.getSession(sessionId, workspace)).plan;
@@ -3238,35 +3270,37 @@ async function withResolvedTask(subArgs, usage, injectedApi, run) {
3238
3270
  console.error(`Failed to load session: ${err.message}`);
3239
3271
  process.exit(1);
3240
3272
  }
3241
- const taskId = resolveTaskId(plan, identifier);
3273
+ const taskId = resolveTaskId(plan, ref);
3242
3274
  if (!taskId) {
3243
- console.error(`Task not found: "${identifier}"`);
3275
+ console.error(`Task not found: "${ref}"`);
3244
3276
  process.exit(1);
3245
3277
  }
3246
3278
  await run(api, sessionId, taskId, plan);
3247
3279
  }
3248
3280
  var PASSES_GATE = /* @__PURE__ */ new Set(["run", "force-start"]);
3249
- async function confirmPastGate(subArgs, api, sessionId, taskId, confirm2) {
3250
- if (hasFlag(subArgs, "--yes")) return;
3251
- const unmerged = await api.getMergeGate(sessionId, taskId).catch(() => []);
3252
- if (unmerged.length === 0) return;
3281
+ async function confirmPastGate(subArgs, api, sessionId, ref, confirm2) {
3282
+ const unmerged = await api.getMergeGate(sessionId, ref).catch(() => []);
3283
+ if (unmerged.length === 0) return false;
3284
+ if (hasFlag(subArgs, "--yes")) return true;
3253
3285
  const question = `${(0, import_core21.pastGateConfirmation)(unmerged.map((d) => (0, import_core21.titledTaskRef)(d)))} Start it anyway?`;
3254
3286
  if (!await confirm2(question)) fail("Not started: nothing was changed. Pass --yes to start it without a prompt.");
3287
+ return true;
3255
3288
  }
3256
3289
  function makeHandler(action, command = action) {
3257
3290
  const yes = PASSES_GATE.has(action) ? " [--yes]" : "";
3258
3291
  return async function handle(subArgs, injectedApi, confirm2 = askYesNo) {
3259
- await withResolvedTask(
3292
+ await withTaskRef(
3260
3293
  subArgs,
3261
3294
  `Usage: ordewell ${command} <task-id-or-order> [--session-id <id>] [--workspace /path]${yes}`,
3262
3295
  injectedApi,
3263
- async (api, sessionId, taskId) => {
3264
- if (PASSES_GATE.has(action)) await confirmPastGate(subArgs, api, sessionId, taskId, confirm2);
3296
+ async (api, sessionId, ref) => {
3297
+ const pastGate = PASSES_GATE.has(action) ? await confirmPastGate(subArgs, api, sessionId, ref, confirm2) : false;
3298
+ const control = pastGate && action === "run" ? "force-start" : action;
3265
3299
  try {
3266
- await api.taskControl(sessionId, taskId, action);
3267
- console.log(PAST_TENSE[action]);
3300
+ const { message, queued } = await api.taskControl(sessionId, ref, control);
3301
+ console.log(message ?? queued ?? PAST_TENSE[control]);
3268
3302
  } catch (err) {
3269
- console.error(`Failed to ${action} task: ${err.message}`);
3303
+ console.error(`Failed to ${control} task: ${err.message}`);
3270
3304
  process.exit(1);
3271
3305
  }
3272
3306
  }
@@ -3280,10 +3314,10 @@ async function handleContinue(subArgs, injectedApi) {
3280
3314
  console.error(usage);
3281
3315
  process.exit(1);
3282
3316
  }
3283
- await withResolvedTask(subArgs, usage, injectedApi, async (api, sessionId, taskId) => {
3317
+ await withTaskRef(subArgs, usage, injectedApi, async (api, sessionId, ref) => {
3284
3318
  try {
3285
- await api.continueTask(sessionId, taskId, message);
3286
- console.log("Task continued.");
3319
+ const sent = await api.continueTask(sessionId, ref, message);
3320
+ console.log(sent.message ?? "Task continued.");
3287
3321
  } catch (err) {
3288
3322
  console.error(`Failed to continue task: ${err.message}`);
3289
3323
  process.exit(1);
@@ -3303,11 +3337,10 @@ async function handleCheckpoint(subArgs, injectedApi) {
3303
3337
  ${usage}`);
3304
3338
  process.exit(1);
3305
3339
  }
3306
- await withResolvedTask(subArgs, usage, injectedApi, async (api, sessionId, taskId) => {
3340
+ await withTaskRef(subArgs, usage, injectedApi, async (api, sessionId, ref) => {
3307
3341
  try {
3308
- if (answer === "approve") await api.approveTaskCheckpoint(sessionId, taskId);
3309
- else await api.rejectTaskCheckpoint(sessionId, taskId, reason || void 0);
3310
- console.log(answer === "approve" ? "Checkpoint approved." : "Checkpoint rejected.");
3342
+ const sent = answer === "approve" ? await api.approveTaskCheckpoint(sessionId, ref) : await api.rejectTaskCheckpoint(sessionId, ref, reason || void 0);
3343
+ console.log(sent.message ?? (answer === "approve" ? "Checkpoint approved." : "Checkpoint rejected."));
3311
3344
  } catch (err) {
3312
3345
  console.error(`Failed to ${answer} checkpoint: ${err.message}`);
3313
3346
  process.exit(1);
@@ -3321,14 +3354,14 @@ var handleCancel = makeHandler("cancel");
3321
3354
 
3322
3355
  // src/commands/mark-complete.ts
3323
3356
  async function handleMarkComplete(subArgs, api) {
3324
- await withResolvedTask(
3357
+ await withTaskRef(
3325
3358
  subArgs,
3326
3359
  "Usage: ordewell mark-complete <task-id-or-order> [--session-id <id>]",
3327
3360
  api,
3328
- async (client, sessionId, taskId) => {
3361
+ async (client, sessionId, ref) => {
3329
3362
  try {
3330
- await client.markTaskComplete(sessionId, taskId);
3331
- console.log("Task marked complete.");
3363
+ const done = await client.markTaskComplete(sessionId, ref);
3364
+ console.log(done.message ?? "Task marked complete.");
3332
3365
  } catch (err) {
3333
3366
  console.error(`Failed to mark task complete: ${err.message}`);
3334
3367
  process.exit(1);
@@ -3337,14 +3370,14 @@ async function handleMarkComplete(subArgs, api) {
3337
3370
  );
3338
3371
  }
3339
3372
  async function handleUncomplete(subArgs, api) {
3340
- await withResolvedTask(
3373
+ await withTaskRef(
3341
3374
  subArgs,
3342
3375
  "Usage: ordewell uncomplete <task-id-or-order> [--session-id <id>]",
3343
3376
  api,
3344
- async (client, sessionId, taskId) => {
3377
+ async (client, sessionId, ref) => {
3345
3378
  try {
3346
- await client.markTaskIncomplete(sessionId, taskId);
3347
- console.log("Task marked not done.");
3379
+ const done = await client.markTaskIncomplete(sessionId, ref);
3380
+ console.log(done.message ?? "Task marked not done.");
3348
3381
  } catch (err) {
3349
3382
  console.error(`Failed to mark task not done: ${err.message}`);
3350
3383
  process.exit(1);
@@ -3353,14 +3386,14 @@ async function handleUncomplete(subArgs, api) {
3353
3386
  );
3354
3387
  }
3355
3388
  async function handleSkip(subArgs, api) {
3356
- await withResolvedTask(
3389
+ await withTaskRef(
3357
3390
  subArgs,
3358
3391
  "Usage: ordewell skip <task-id-or-order> [--reason <text>] [--session-id <id>]",
3359
3392
  api,
3360
- async (client, sessionId, taskId) => {
3393
+ async (client, sessionId, ref) => {
3361
3394
  try {
3362
- await client.skipTask(sessionId, taskId, flag(subArgs, "--reason"));
3363
- console.log("Task skipped: none of its work lands, and the tasks that depend on it go ahead without it once the work it waited on is done.");
3395
+ const done = await client.skipTask(sessionId, ref, flag(subArgs, "--reason"));
3396
+ console.log(done.message ?? "Task skipped: none of its work lands, and the tasks that depend on it go ahead without it once the work it waited on is done.");
3364
3397
  } catch (err) {
3365
3398
  console.error(`Failed to skip task: ${err.message}`);
3366
3399
  process.exit(1);
@@ -3561,6 +3594,17 @@ async function handleRewind(subArgs, injectedApi) {
3561
3594
  console.log("\nThe message you rewound to, to resend or edit:\n");
3562
3595
  console.log(fork.rewoundMessage);
3563
3596
  }
3597
+ async function handleResend(subArgs, injectedApi) {
3598
+ if (positionals(subArgs)[0] !== "cancel") fail("Usage: ordewell resend cancel [--session-id <id>] [--workspace /path]");
3599
+ const { api, sessionId } = await adopted(subArgs, injectedApi);
3600
+ let cancelled;
3601
+ try {
3602
+ ({ cancelled } = await api.cancelPlannerResend(sessionId));
3603
+ } catch (err) {
3604
+ fail(`Failed to cancel the resend: ${err.message}`);
3605
+ }
3606
+ console.log(cancelled ? "Resend cancelled: what waits goes with your next message." : "Nothing waits on the planner's usage limit to be sent at a reset.");
3607
+ }
3564
3608
 
3565
3609
  // src/tui/transcript.ts
3566
3610
  var import_core22 = require("@ordewell/core");
@@ -3594,7 +3638,7 @@ var HANDOFF_ACTIONS = [
3594
3638
  { id: "cleanup", label: "Clean up", hint: "remove worktrees and task branches, keep this branch" }
3595
3639
  ];
3596
3640
  function handoffActions(handoff) {
3597
- if (!isRepoGroup(handoff)) return HANDOFF_ACTIONS;
3641
+ if (!(0, import_core10.isRepoGroup)(handoff)) return HANDOFF_ACTIONS;
3598
3642
  return [
3599
3643
  { id: "review", label: "Review diff", hint: "each repository's branch against the commit it forked from" },
3600
3644
  { id: "merge", label: "Merge all", hint: "every repository's branch into what you have checked out there (asks first)" },
@@ -3619,7 +3663,7 @@ function handoffCommand(state, arg) {
3619
3663
  }
3620
3664
  function handoffArrived(state, handoff) {
3621
3665
  const landed = handoff.landed.length;
3622
- const where = isRepoGroup(handoff) ? ` in ${reposOf2(handoff)}` : "";
3666
+ const where = (0, import_core10.isRepoGroup)(handoff) ? ` in ${reposOf2(handoff)}` : "";
3623
3667
  const branch = handoffBranch(handoff);
3624
3668
  const tasks = `${landed} task${landed === 1 ? "" : "s"} landed`;
3625
3669
  const received = { ...state, handoff };
@@ -3634,9 +3678,6 @@ function handoffArrived(state, handoff) {
3634
3678
  );
3635
3679
  return state.overlay ? told : { ...told, overlay: { kind: "handoff", index: 0, diff: null } };
3636
3680
  }
3637
- function withDeferral(fresh, known) {
3638
- return known?.deferredTo && handoffBranch(known) === handoffBranch(fresh) ? { ...fresh, deferredTo: known.deferredTo } : fresh;
3639
- }
3640
3681
  function runHandoffAction(state, id) {
3641
3682
  const { sessionId } = state;
3642
3683
  const handoff = currentHandoff(state);
@@ -3654,8 +3695,8 @@ function runHandoffAction(state, id) {
3654
3695
  ...state,
3655
3696
  overlay: {
3656
3697
  kind: "confirm",
3657
- title: isRepoGroup(handoff) ? "Merge what has landed into your branches?" : "Merge what has landed into your branch?",
3658
- message: `Merge ${handoffBranch(handoff)} as it stands now into whatever ${isRepoGroup(handoff) ? `each of ${reposWithWork(handoff).join(", ")} has` : "you have"} checked out. The run goes on: tasks waiting for Merge all start once it has merged, and later tasks land on the same branch for the next Merge all. A merge that conflicts is aborted, and your ${isRepoGroup(handoff) ? "trees stay" : "tree stays"} as ${isRepoGroup(handoff) ? "they were" : "it was"}.${repairedNotice(handoff)}`,
3698
+ title: (0, import_core10.isRepoGroup)(handoff) ? "Merge what has landed into your branches?" : "Merge what has landed into your branch?",
3699
+ message: `Merge ${handoffBranch(handoff)} as it stands now into ${(0, import_core10.mergeTarget)(handoff)}.${(0, import_core10.checkoutRule)(handoff)} The run goes on: tasks waiting for Merge all start once it has merged, and later tasks land on the same branch for the next Merge all. A merge that conflicts is aborted, and your ${(0, import_core10.isRepoGroup)(handoff) ? "trees stay" : "tree stays"} as ${(0, import_core10.isRepoGroup)(handoff) ? "they were" : "it was"}.${repairedNotice(handoff)}`,
3659
3700
  action: { kind: "merge-run" }
3660
3701
  }
3661
3702
  },
@@ -3665,15 +3706,15 @@ function runHandoffAction(state, id) {
3665
3706
  return {
3666
3707
  state: {
3667
3708
  ...state,
3668
- overlay: isRepoGroup(handoff) ? {
3709
+ overlay: (0, import_core10.isRepoGroup)(handoff) ? {
3669
3710
  kind: "confirm",
3670
3711
  title: "Merge all into your branches?",
3671
- message: `Merge ${handoffBranch(handoff)} into whatever each of ${reposWithWork(handoff).join(", ")} has checked out. 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. Once every repository has merged, the run's worktrees and branches are removed unless a task still has work in its worktree that has not landed: then the run stays, and so does that work.${repairedNotice(handoff)}`,
3712
+ message: `Merge ${handoffBranch(handoff)} into ${(0, import_core10.mergeTarget)(handoff)}.${(0, import_core10.checkoutRule)(handoff)} 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. Once every repository has merged, the run's worktrees and branches are removed unless a task still has work in its worktree that has not landed: then the run stays, and so does that work.${repairedNotice(handoff)}`,
3672
3713
  action: { kind: "merge-run" }
3673
3714
  } : {
3674
3715
  kind: "confirm",
3675
3716
  title: "Merge into your branch?",
3676
- message: `Merge ${handoffBranch(handoff)} into whatever you have checked out. If it conflicts the merge is aborted and your tree stays as it was. Once it has merged, the run's worktrees and branches are removed unless a task still has work in its worktree that has not landed: then the run stays, and so does that work.${repairedNotice(handoff)}`,
3717
+ message: `Merge ${handoffBranch(handoff)} into ${(0, import_core10.mergeTarget)(handoff)}.${(0, import_core10.checkoutRule)(handoff)} If it conflicts the merge is aborted and your tree stays as it was. Once it has merged, the run's worktrees and branches are removed unless a task still has work in its worktree that has not landed: then the run stays, and so does that work.${repairedNotice(handoff)}`,
3677
3718
  action: { kind: "merge-run" }
3678
3719
  }
3679
3720
  },
@@ -3686,7 +3727,7 @@ function runHandoffAction(state, id) {
3686
3727
  overlay: {
3687
3728
  kind: "confirm",
3688
3729
  title: "Discard this run?",
3689
- message: `Removes the run's worktrees and task branches and deletes ${handoffBranch(handoff)}${isRepoGroup(handoff) ? ` in ${reposOf2(handoff)}` : ""}. Tasks keep their status: mark one not done if you did not keep its work.`,
3730
+ message: `Removes the run's worktrees and task branches and deletes ${handoffBranch(handoff)}${(0, import_core10.isRepoGroup)(handoff) ? ` in ${reposOf2(handoff)}` : ""}. Tasks keep their status: mark one not done if you did not keep its work.`,
3690
3731
  action: { kind: "discard-run" }
3691
3732
  }
3692
3733
  },
@@ -3704,9 +3745,8 @@ function confirmedHandoff(state, kind) {
3704
3745
  type: "isolationMerge",
3705
3746
  sessionId,
3706
3747
  branch: handoffBranch(handoff),
3707
- ...isRepoGroup(handoff) ? { group: true } : {},
3708
- ...repaired.length > 0 ? { repaired } : {},
3709
- ...midRun(state) ? { midRun: true } : {}
3748
+ ...(0, import_core10.isRepoGroup)(handoff) ? { group: true } : {},
3749
+ ...repaired.length > 0 ? { repaired } : {}
3710
3750
  } : { type: "isolationDiscard", sessionId, branch: handoffBranch(handoff) };
3711
3751
  return { state: closed, effects: [effect] };
3712
3752
  }
@@ -3789,7 +3829,7 @@ async function handleHandoff(subArgs, injectedApi, confirm2 = askYesNo) {
3789
3829
  const handoff = isolationOfPlan(plan)?.handoff;
3790
3830
  if (!handoff) fail("This session has no isolated run to hand off.");
3791
3831
  const branch = handoffBranch(handoff);
3792
- const group = isRepoGroup(handoff);
3832
+ const group = (0, import_core10.isRepoGroup)(handoff);
3793
3833
  if (group) for (const line of repoResultLines(handoff)) console.error(line);
3794
3834
  const asked = hasFlag(subArgs, "--yes");
3795
3835
  const confirmed = async (question) => {
@@ -3810,7 +3850,7 @@ async function handleHandoff(subArgs, injectedApi, confirm2 = askYesNo) {
3810
3850
  return;
3811
3851
  }
3812
3852
  case "merge": {
3813
- 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; once a finished run has merged everything, its worktrees and branches are removed.` : `Merge ${branch} into the branch you have checked out? Once a finished run has merged everything, its worktrees and branches are removed.`) + repairedNotice(handoff));
3853
+ await confirmed((group ? `Merge ${branch} into ${(0, import_core10.mergeTarget)(handoff)}? Nothing is merged unless every repository can take it${(0, import_core10.checkoutRule)(handoff) ? " and is on that branch" : ""}; once a finished run has merged everything, its worktrees and branches are removed.` : `Merge ${branch} into ${(0, import_core10.mergeTarget)(handoff)}?${(0, import_core10.checkoutRule)(handoff)} Once a finished run has merged everything, its worktrees and branches are removed.`) + repairedNotice(handoff));
3814
3854
  const { ok, message } = mergeOutcome(await attempt(() => api.mergeRun(sessionId)), branch, group, repairedLanded(handoff));
3815
3855
  if (ok) console.log(message);
3816
3856
  else fail(message);
@@ -4107,21 +4147,23 @@ async function handleApprove(subArgs, injectedApi) {
4107
4147
  const sessionId = resolveSessionId(subArgs);
4108
4148
  const api = await connect(subArgs, injectedApi);
4109
4149
  await followExecution(api, sessionId, async () => {
4150
+ let approved;
4110
4151
  try {
4111
- await api.approveReview(sessionId);
4152
+ approved = await api.approveReview(sessionId);
4112
4153
  } catch (err) {
4113
4154
  console.error(`Failed to approve: ${err.message}`);
4114
4155
  process.exit(1);
4115
4156
  }
4116
- console.error("Plan approved: continuing execution.");
4157
+ console.error(approved.message ?? "Plan approved: continuing execution.");
4117
4158
  });
4118
4159
  }
4119
4160
 
4120
4161
  // src/commands/task-assign.ts
4121
4162
  var import_core29 = require("@ordewell/core");
4122
- var import_plan_utils = require("@ordewell/core/plan-utils");
4163
+ var import_plan_utils3 = require("@ordewell/core/plan-utils");
4123
4164
 
4124
4165
  // src/tui/taskAssignment.ts
4166
+ var import_plan_utils = require("@ordewell/core/plan-utils");
4125
4167
  function runnerServes(runner, model) {
4126
4168
  return !runner || !model.runners || model.runners.length === 0 || model.runners.includes(runner);
4127
4169
  }
@@ -4144,7 +4186,7 @@ function effortsForTask(models, task) {
4144
4186
  }
4145
4187
  function assignedModelFor(model, currentEffort) {
4146
4188
  const variants = model.variants ?? [];
4147
- const thinkingEffort = variants.some((variant) => variant.id === currentEffort) ? currentEffort : void 0;
4189
+ const thinkingEffort = (0, import_plan_utils.clampThinkingEffort)(currentEffort, variants);
4148
4190
  return {
4149
4191
  modelId: model.id,
4150
4192
  modelLabel: model.label,
@@ -4158,6 +4200,7 @@ function modesForTask(modesByRunner, task) {
4158
4200
  }
4159
4201
 
4160
4202
  // src/tui/state.ts
4203
+ var import_plan_utils2 = require("@ordewell/core/plan-utils");
4161
4204
  var import_core28 = require("@ordewell/core");
4162
4205
 
4163
4206
  // src/tui/editor.ts
@@ -4354,12 +4397,7 @@ function resumesTask(task) {
4354
4397
  return task?.status === "awaiting_user" && task.awaitingReason === "usage-limit";
4355
4398
  }
4356
4399
  function findTask(tasks, id) {
4357
- for (const task of tasks) {
4358
- if (task.id === id) return task;
4359
- const found = findTask(task.subtasks ?? [], id);
4360
- if (found) return found;
4361
- }
4362
- return void 0;
4400
+ return (0, import_plan_utils2.taskPlacement)(tasks, id)?.task;
4363
4401
  }
4364
4402
  function refFor(state, task) {
4365
4403
  return (0, import_core28.taskRefOf)(state.tasks, task.id) ?? (0, import_core28.taskRef)(task);
@@ -4426,6 +4464,7 @@ function initialState(overrides = {}) {
4426
4464
  workspace: process.cwd(),
4427
4465
  handoff: null,
4428
4466
  runMerge: null,
4467
+ plannerResend: null,
4429
4468
  overlay: null,
4430
4469
  pendingApprovals: [],
4431
4470
  queuedPrompts: import_core28.EMPTY_HOLD,
@@ -4444,8 +4483,8 @@ function initialState(overrides = {}) {
4444
4483
  }
4445
4484
 
4446
4485
  // src/commands/task-assign.ts
4447
- var refOf = (tasks, task) => (0, import_plan_utils.taskRefOf)(tasks, task.id) ?? (0, import_plan_utils.taskRef)(task);
4448
- var namedOf = (tasks, task) => (0, import_plan_utils.titledTaskRefOf)(tasks, task.id) ?? (0, import_plan_utils.titledTaskRef)(task);
4486
+ var refOf = (tasks, task) => (0, import_plan_utils3.taskRefOf)(tasks, task.id) ?? (0, import_plan_utils3.taskRef)(task);
4487
+ var namedOf = (tasks, task) => (0, import_plan_utils3.titledTaskRefOf)(tasks, task.id) ?? (0, import_plan_utils3.titledTaskRef)(task);
4449
4488
  async function withTask(subArgs, usage, injectedApi, run, { joinValue = false } = {}) {
4450
4489
  await withResolvedTask(subArgs, usage, injectedApi, async (api, sessionId, taskId, plan) => {
4451
4490
  const tasks = taskViews(plan);
@@ -4507,8 +4546,9 @@ Model \xB7 ${namedOf(tasks, task)}${task.assignedRunner ? ` (${task.assignedRunn
4507
4546
  const assignedModel = assignedModelFor(model, task.assignedModel?.thinkingEffort);
4508
4547
  await api.updateTask(sessionId, task.id, { assignedModel });
4509
4548
  console.log(`Task ${refOf(tasks, task)} model set to ${model.label}.`);
4510
- if (task.assignedModel?.thinkingEffort && !assignedModel.thinkingEffort) {
4511
- console.log(` ${model.label} does not expose "${task.assignedModel.thinkingEffort}", so the effort went back to the runner default.`);
4549
+ const was = task.assignedModel?.thinkingEffort;
4550
+ if (was && assignedModel.thinkingEffort !== was) {
4551
+ console.log(` ${model.label} does not expose "${was}", so the effort is now ${assignedModel.thinkingEffort ? `"${assignedModel.thinkingEffort}", its nearest level` : "the runner default"}.`);
4512
4552
  }
4513
4553
  });
4514
4554
  }
@@ -4584,15 +4624,16 @@ Mode \xB7 ${namedOf(tasks, task)}${task.assignedRunner ? ` (${task.assignedRunne
4584
4624
  var DEPS_USAGE = "Usage: ordewell task-deps <task-id-or-order> [<id,id,\u2026>|none] [--session-id <id>]";
4585
4625
  async function handleTaskDeps(subArgs, injectedApi) {
4586
4626
  await withTask(subArgs, DEPS_USAGE, injectedApi, async (api, sessionId, task, tasks, value) => {
4587
- const candidates = (0, import_plan_utils.dependencyCandidates)(tasks, task.id);
4627
+ const candidates = (0, import_plan_utils3.dependencyCandidates)(tasks, task.id);
4628
+ const held = task.dependencies.filter((id) => !candidates.some((c) => c.id === id)).flatMap((id) => (0, import_plan_utils3.resolveTaskRef)(tasks, id) ?? []);
4588
4629
  if (!value) {
4589
4630
  console.log(`
4590
4631
  Dependencies \xB7 ${namedOf(tasks, task)}
4591
4632
  `);
4592
4633
  if (candidates.length === 0) {
4593
- console.log(` Nothing runs before ${refOf(tasks, task)}, so it has no possible dependencies.`);
4634
+ console.log(` No earlier siblings to offer as dependencies for ${refOf(tasks, task)}.`);
4594
4635
  }
4595
- for (const c of candidates) {
4636
+ for (const c of [...candidates, ...held]) {
4596
4637
  const chosen = task.dependencies.includes(c.id);
4597
4638
  console.log(` ${chosen ? "*" : " "} ${c.id} ${namedOf(tasks, c)}${c.status === "completed" ? " (already completed)" : ""}`);
4598
4639
  }
@@ -4600,8 +4641,8 @@ Dependencies \xB7 ${namedOf(tasks, task)}
4600
4641
  * = current. ${DEPS_USAGE}`);
4601
4642
  return;
4602
4643
  }
4603
- const dependencies = value.toLowerCase() === "none" ? [] : value.split(",").map((s) => s.trim()).filter(Boolean).map((token) => tasks.find((t) => t.id === token || String(t.order) === token)?.id ?? token);
4604
- const check = (0, import_plan_utils.canSetDependencies)(tasks, task.id, dependencies);
4644
+ const dependencies = value.toLowerCase() === "none" ? [] : value.split(",").map((s) => s.trim()).filter(Boolean).map((token) => (0, import_plan_utils3.resolveUserTaskRef)(tasks, token)?.id ?? token);
4645
+ const check = (0, import_plan_utils3.canSetDependencies)(tasks, task.id, dependencies);
4605
4646
  if (!check.ok) fail(check.error ?? "Invalid dependencies.");
4606
4647
  await api.updateTask(sessionId, task.id, { dependencies });
4607
4648
  console.log(dependencies.length > 0 ? `Task ${refOf(tasks, task)} now depends on ${dependencies.length} task${dependencies.length === 1 ? "" : "s"}.` : `Task ${refOf(tasks, task)} no longer depends on anything.`);
@@ -4666,6 +4707,7 @@ var SLASH_COMMANDS = [
4666
4707
  { name: "approve", usage: "/approve", description: "Approve the drafted plan", category: "planning" },
4667
4708
  { name: "run", usage: "/run", description: "Execute the approved plan", category: "planning" },
4668
4709
  { name: "stop", usage: "/stop", description: "Stop the planner or the running execution, whichever is active", category: "planning" },
4710
+ { name: "resend", usage: "/resend cancel", description: "Keep what waits on the planner's usage limit from being sent at the reset: it goes with your next message", category: "planning" },
4669
4711
  { 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" },
4670
4712
  // Tasks
4671
4713
  { name: "add-task", usage: "/add-task <title>", description: "Add a task to the current plan", category: "tasks" },
@@ -4675,7 +4717,7 @@ var SLASH_COMMANDS = [
4675
4717
  { name: "skip", usage: "/skip <id>", description: "Skip a task", category: "tasks" },
4676
4718
  { name: "retry", usage: "/retry <id>", description: "Re-run a failed task", category: "tasks" },
4677
4719
  { name: "cancel", usage: "/cancel <id>", description: "Kill a running task", category: "tasks" },
4678
- { name: "force-start", usage: "/force-start <id>", description: "Start a task now, ignoring dependencies", category: "tasks" },
4720
+ { name: "force-start", usage: "/force-start <id>", description: "Approve a task and start it now, or once its dependencies finish", category: "tasks" },
4679
4721
  { name: "continue", usage: "/continue <id> <message>", description: "Continue a finished task in its saved runner session, with your message", category: "tasks" },
4680
4722
  { name: "open", usage: "/open <id>", description: "Open a task's view: its log, and the keys that answer its checkpoint or tool request", category: "tasks" },
4681
4723
  { name: "checkpoint", usage: "/checkpoint <id> approve|reject [reason]", description: "Answer the checkpoint a task waits at: approve lets it go on, reject tells it why not", category: "tasks" },
@@ -4829,7 +4871,7 @@ var import_core33 = require("@ordewell/core");
4829
4871
 
4830
4872
  // src/tui/reducers/pickers.ts
4831
4873
  var import_core31 = require("@ordewell/core");
4832
- var import_plan_utils2 = require("@ordewell/core/plan-utils");
4874
+ var import_plan_utils4 = require("@ordewell/core/plan-utils");
4833
4875
  var DEFAULT_EFFORT = "__runner_default__";
4834
4876
  function picker(title, items, action, extra = {}) {
4835
4877
  return { title, items, filter: "", index: 0, multi: false, chosen: [], filterable: false, action, ...extra };
@@ -4841,6 +4883,15 @@ function refillPicker(state, kinds) {
4841
4883
  const hint = overlay.picker.action.kind === "set-model" ? providerErrorHint(state) : overlay.picker.hint;
4842
4884
  return { ...state, overlay: { kind: "picker", picker: { ...overlay.picker, items, hint, index: 0 } } };
4843
4885
  }
4886
+ function reseedRunnerChoice(before, after) {
4887
+ const overlay = after.overlay;
4888
+ if (overlay?.kind !== "picker" || overlay.picker.action.kind !== "set-runners") return after;
4889
+ const enabledIds = (state) => state.runners.filter((r) => r.enabled).map((r) => r.id);
4890
+ const shown = enabledIds(before);
4891
+ const untouched = overlay.picker.chosen.length === shown.length && shown.every((id) => overlay.picker.chosen.includes(id));
4892
+ const picker2 = { ...overlay.picker, items: pickerItemsFor(after, overlay.picker.action) };
4893
+ return { ...after, overlay: { kind: "picker", picker: untouched ? { ...picker2, chosen: enabledIds(after) } : picker2 } };
4894
+ }
4844
4895
  function modelDetail(m) {
4845
4896
  return [m.provider, m.pricing].filter(Boolean).join(" \xB7 ") || void 0;
4846
4897
  }
@@ -4929,7 +4980,7 @@ function pickerItemsFor(state, action) {
4929
4980
  }));
4930
4981
  }
4931
4982
  if (action.kind === "set-task-deps") {
4932
- return (0, import_plan_utils2.dependencyCandidates)(state.tasks, action.taskId).map((candidate) => ({
4983
+ return (0, import_plan_utils4.dependencyCandidates)(state.tasks, action.taskId).map((candidate) => ({
4933
4984
  id: candidate.id,
4934
4985
  label: titledRefFor(state, candidate),
4935
4986
  detail: candidate.status === "completed" ? "already completed" : void 0
@@ -5039,7 +5090,13 @@ var PLAN_KEYS = {
5039
5090
  deps: { keys: "D", meaning: "pick the tasks it depends on", group: "Assign" },
5040
5091
  skills: { keys: "K", meaning: "pick the skills it loads", group: "Assign" },
5041
5092
  ops: { keys: "O", meaning: "switch between a change and an ops task", group: "Assign" },
5042
- start: { keys: "f", meaning: "start the task now", group: "Run", hint: "f start" },
5093
+ start: {
5094
+ keys: "f",
5095
+ meaning: "start the task, approving it if it waits for you; it starts once its dependencies finish",
5096
+ group: "Run",
5097
+ // Added after the plan was approved, it waits for this key; the row says the same.
5098
+ hint: (task) => task && (0, import_core32.taskRowView)(task).waitsForApproval ? "f approve" : "f start"
5099
+ },
5043
5100
  runPlan: { keys: "E", meaning: "run the whole plan", group: "Run", hint: "E run plan" },
5044
5101
  stop: { keys: "S", meaning: "stop the run", group: "Run", hint: "S stop" },
5045
5102
  cancel: {
@@ -5295,11 +5352,18 @@ function chatLayout(state, rows, cols) {
5295
5352
  const bubbles = tv ? taskQueuedRows(state, cols) : queuedPromptRows(state, cols);
5296
5353
  const content = [...transcript, ...bubbles.length > 0 ? ["", ...bubbles] : []];
5297
5354
  const taskHeader = tv ? taskHeaderLines(state, tv, cols) : [];
5298
- const usage = rows <= 1 ? [] : tv ? taskUsageFooter(state, tv, blocks2, cols) : tokenRow(blocks2, cols);
5355
+ const usage = rows <= 1 ? [] : tv ? taskUsageFooter(state, tv, blocks2, cols) : [...plannerResendRow(state, cols), ...tokenRow(blocks2, cols)];
5299
5356
  const footer = usage.slice(0, Math.max(1, rows - taskHeader.length - 1));
5300
5357
  const space = Math.max(0, rows - taskHeader.length - footer.length);
5301
5358
  return { lines: content, header: taskHeader, footer, anchor: content.length > space ? "bottom" : "top", maxScroll: Math.max(0, content.length - space) };
5302
5359
  }
5360
+ function plannerResendRow(state, cols) {
5361
+ const { plannerResend } = state;
5362
+ const at = plannerResend ? new Date(plannerResend.at) : null;
5363
+ if (!plannerResend || !at || Number.isNaN(at.getTime())) return [];
5364
+ const whose = plannerResend.runner ? `${plannerResend.runner}'s` : "the planner's";
5365
+ return [style.yellow(truncate(`\u23F8 Resending at ${(0, import_core34.resumeTime)(at)}, once ${whose} usage limit resets \xB7 /resend cancel keeps it for your next message`, cols))];
5366
+ }
5303
5367
  var tokenRow = (blocks2, cols) => {
5304
5368
  const line = tokenLine(blocks2, cols);
5305
5369
  return line ? [line] : [];
@@ -5307,15 +5371,13 @@ var tokenRow = (blocks2, cols) => {
5307
5371
  function taskUsageFooter(state, tv, blocks2, cols) {
5308
5372
  const task = findTask(state.tasks, tv.taskId);
5309
5373
  const reported = task?.attemptUsage ?? [];
5374
+ if (!task || reported.length === 0) return tokenRow(blocks2, cols);
5310
5375
  const onScreen = tv.attempt > 0 ? reported.find((a) => a.attempt === tv.attempt) : reported.at(-1);
5311
- if (!task || !onScreen) return tokenRow(blocks2, cols);
5312
- const usage = (0, import_core34.taskRowView)({ ...task, attemptUsage: [onScreen] }).usage;
5313
- if (!usage) return tokenRow(blocks2, cols);
5314
- const models = usage.byModel ?? [];
5315
- return [
5316
- style.grey(truncate(taskUsageText(usage), cols)),
5317
- ...models.length > 1 ? models.map((m) => style.grey(truncate(`${sanitize(m.model ?? "unattributed")}: ${usageMeasures(m.totals, true).join(" \xB7 ")}`, cols))) : []
5318
- ];
5376
+ const spend = (0, import_core34.taskUsageSummary)(task);
5377
+ const total = taskUsageText({ ...spend, attempt: reported.at(-1)?.attempt ?? 0 });
5378
+ if (onScreen === reported.at(-1)) return [style.grey(truncate(total, cols))];
5379
+ const own = onScreen ? [style.grey(truncate(`attempt ${onScreen.attempt}: ${taskUsageText({ totals: onScreen.totals, contextFill: (0, import_core34.plannerContextFill)(onScreen), attempt: onScreen.attempt })}`, cols))] : tokenRow(blocks2, cols);
5380
+ return [...own, style.grey(truncate(`task total: ${total}`, cols))];
5319
5381
  }
5320
5382
  function chatScrollMax(state) {
5321
5383
  return chatLayout(state, bodyRows(state), chatPaneWidth(state)).maxScroll;
@@ -5558,9 +5620,17 @@ function planScrollExtent(state) {
5558
5620
  }
5559
5621
  return planLayout(state, bodyRows(state), cols);
5560
5622
  }
5623
+ function modelsByRunnerOf(models) {
5624
+ const out = {};
5625
+ for (const m of models) {
5626
+ for (const runner of m.runners ?? []) (out[runner] ??= []).push({ modelId: m.id, modelLabel: m.label, variants: [] });
5627
+ }
5628
+ return out;
5629
+ }
5561
5630
  function taskLines(state, planRow, index, cols) {
5562
5631
  const { task, parent } = planRow;
5563
- const row = (0, import_core34.taskRowView)(task, { parent, tasks: state.tasks, modes: modesForTask(state.modesByRunner, task) });
5632
+ const runnerScope = state.runners.length > 0 ? { enabledRunners: state.runners.filter((r) => r.enabled).map((r) => r.id), allowlist: state.allowlist, models: modelsByRunnerOf(state.models) } : void 0;
5633
+ const row = (0, import_core34.taskRowView)(task, { parent, tasks: state.tasks, modes: modesForTask(state.modesByRunner, task), runnerScope });
5564
5634
  const selected = state.focus === "plan" && index === state.selectedTask;
5565
5635
  const expanded = state.expandedTaskId === task.id;
5566
5636
  const rawIcon = row.status === "running" ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[row.status];
@@ -5582,8 +5652,7 @@ function taskLines(state, planRow, index, cols) {
5582
5652
  const prefix = lineIndex === 0 ? head : continuation;
5583
5653
  return prefix + (selected ? style.bold(line) : line);
5584
5654
  });
5585
- const reported = [...new Set((row.usage?.byModel ?? []).flatMap((m) => m.model ? [sanitize(m.model)] : []))];
5586
- const model = reported.length ? reported.join(", ") : task.assignedModel?.modelLabel ?? (task.type === "ai" ? "default model" : "");
5655
+ const model = task.assignedModel?.modelLabel ?? (task.type === "ai" ? "default model" : "");
5587
5656
  const effort = task.type === "ai" ? `effort: ${task.assignedModel?.thinkingEffort ?? "default"}` : "";
5588
5657
  const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}${row.autonomous ? style.yellow(" \u26A1") : ""}` : "";
5589
5658
  const runner = task.assignedRunner ?? "";
@@ -5599,36 +5668,39 @@ function taskLines(state, planRow, index, cols) {
5599
5668
  if (row.conflict) {
5600
5669
  const where = row.conflict.repo ? ` in ${row.conflict.repo}` : "";
5601
5670
  const files = row.conflict.files ? ` (${row.conflict.files})` : "";
5602
- lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where}${files}: its work is kept on its own branch`, cols)));
5671
+ lines.push(...statusLines(style.red, "\u26A0", `merge conflict${where}${files}: its work is kept on its own branch`, cols, bodyPad));
5603
5672
  }
5604
5673
  if (row.repairing) {
5605
5674
  const files = row.repairing.files ? ` in ${row.repairing.files}` : "";
5606
5675
  const attempt = row.repairing.attempt ? ` (attempt ${row.repairing.attempt.attempt}/${row.repairing.attempt.limit})` : "";
5607
- lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
5676
+ lines.push(...statusLines(style.yellow, "\u21BB", `repairing conflict${files}${attempt}`, cols, bodyPad));
5608
5677
  }
5609
5678
  if (row.mergeGate) {
5610
- lines.push(style.yellow(truncate(`${bodyPad}\u23F8 waits for Merge all: ${row.mergeGate.join(", ")} not merged into your branch yet`, cols)));
5679
+ lines.push(...statusLines(style.yellow, "\u23F8", `waits for Merge all: ${row.mergeGate.join(", ")} not merged into your branch yet`, cols, bodyPad));
5680
+ }
5681
+ if (row.assignmentFlag) {
5682
+ lines.push(...statusLines(style.yellow, "\u26A0", assignmentFlagText(row), cols, bodyPad));
5611
5683
  }
5612
5684
  if (row.waitsForApproval) {
5613
- lines.push(style.yellow(truncate(`${bodyPad}\u23F8 ${inRow(row.waitsForApproval)}: added after you approved the plan; f starts it, or /run runs the plan`, cols)));
5685
+ lines.push(...statusLines(style.yellow, "\u23F8", `${inRow(row.waitsForApproval)} \xB7 f approves it \xB7 /run approves all`, cols, bodyPad));
5614
5686
  }
5615
5687
  if (row.blockedBy) {
5616
- lines.push(style.red(truncate(`${bodyPad}\u2298 ${inRow((0, import_core34.blockedByLabel)(row.blockedBy))}: runs once ${row.blockedBy.length === 1 ? "it is retried and passes" : "they are retried and pass"}`, cols)));
5688
+ lines.push(...statusLines(style.red, "\u2298", `${inRow((0, import_core34.blockedByLabel)(row.blockedBy))}: runs once ${row.blockedBy.length === 1 ? "it is retried and passes" : "they are retried and pass"}`, cols, bodyPad));
5617
5689
  }
5618
5690
  if (row.status === "skipped") {
5619
- lines.push(style.grey(truncate(`${bodyPad}\xBB ${inRow((0, import_core34.skippedLabel)(row.skipReason))}: nothing of it landed; m done lands its kept work`, cols)));
5691
+ lines.push(...statusLines(style.grey, "\xBB", `${inRow((0, import_core34.skippedLabel)(row.skipReason))}: nothing of it landed; m done lands its kept work`, cols, bodyPad));
5620
5692
  }
5621
5693
  const resumable = row.actions.includes("resume-now");
5622
5694
  if (row.resumes) {
5623
- lines.push(style.yellow(truncate(`${bodyPad}\u23F8 ${inRow(row.resumes)}, once the usage limit resets: ${resumable ? "r resume now \xB7 " : ""}c don't resume`, cols)));
5695
+ lines.push(...statusLines(style.yellow, "\u23F8", `${inRow(row.resumes)}, once the usage limit resets: ${resumable ? "r resume now \xB7 " : ""}c don't resume`, cols, bodyPad));
5624
5696
  } else if (resumable) {
5625
- lines.push(style.yellow(truncate(`${bodyPad}\u23F8 waits for the usage limit to reset: r resume now`, cols)));
5697
+ lines.push(...statusLines(style.yellow, "\u23F8", "waits for the usage limit to reset: r resume now", cols, bodyPad));
5626
5698
  }
5627
5699
  if (row.opsChangedFiles) {
5628
- lines.push(style.yellow(truncate(`${bodyPad}\u26A0 an ops task changed tracked files: check them, then m done or /retry`, cols)));
5700
+ lines.push(...statusLines(style.yellow, "\u26A0", "an ops task changed tracked files: check them, then m done or /retry", cols, bodyPad));
5629
5701
  }
5630
5702
  if (row.repaired?.landed) {
5631
- lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${row.repaired.files}`, cols)));
5703
+ lines.push(...statusLines(style.grey, "\u21BB", `landed after repairing conflict in ${row.repaired.files}`, cols, bodyPad));
5632
5704
  }
5633
5705
  let editorLine;
5634
5706
  if (expanded) {
@@ -5670,6 +5742,11 @@ function taskLines(state, planRow, index, cols) {
5670
5742
  }
5671
5743
  return { lines, editorLine };
5672
5744
  }
5745
+ function statusLines(paint, glyph, text, cols, pad2) {
5746
+ const lead = `${pad2}${glyph} `;
5747
+ const room = Math.max(1, cols - width(lead));
5748
+ return wrap(text, room).map((line, i) => paint(`${i === 0 ? lead : " ".repeat(width(lead))}${line}`));
5749
+ }
5673
5750
  function taskText(label, text, cols, pad2 = " ") {
5674
5751
  const room = Math.max(1, cols - width(pad2));
5675
5752
  return [
@@ -6057,7 +6134,7 @@ function renderHandoff(state, overlay, rows, cols) {
6057
6134
  const caret = active ? style.brand("\u276F") : " ";
6058
6135
  return `${caret} ${active ? style.bold(action.label) : action.label}${style.grey(`: ${action.hint}`)}`;
6059
6136
  });
6060
- const perRepo = isRepoGroup(handoff) ? ["Per repository:", ...handoff.repos.map((repo, i) => ` ${repo.landed.length > 0 ? style.green("\u2713") : style.grey("\xB7")} ${repoResultLines(handoff)[i]}`), ""] : [];
6137
+ const perRepo = (0, import_core10.isRepoGroup)(handoff) ? ["Per repository:", ...handoff.repos.map((repo, i) => ` ${repo.landed.length > 0 ? style.green("\u2713") : style.grey("\xB7")} ${repoResultLines(handoff)[i]}`), ""] : [];
6061
6138
  return frame(
6062
6139
  "Run handoff",
6063
6140
  [
@@ -6574,12 +6651,7 @@ function openTaskView(state, sessionId, taskId) {
6574
6651
  if (!task) return step(say(state, "system", `No task ${taskId} in this plan.`));
6575
6652
  const taskView = {
6576
6653
  taskId,
6577
- view: import_core40.EMPTY_TASK_LOG,
6578
- attempts: [],
6579
- attempt: 0,
6580
- pending: [],
6581
- loaded: false,
6582
- followLatest: true,
6654
+ ...import_core40.EMPTY_TASK_LOG_FOLLOW,
6583
6655
  queuedIndex: null
6584
6656
  };
6585
6657
  return step({ ...state, taskView, focus: "chat", scroll: 0 }, [{ type: "openTaskLog", sessionId, taskId }]);
@@ -6595,67 +6667,35 @@ function clampQueueIndex(tv) {
6595
6667
  const max = tv.view.queued.length - 1;
6596
6668
  return tv.queuedIndex <= max ? tv : { ...tv, queuedIndex: max };
6597
6669
  }
6598
- function applyLive(tv, attempt, events) {
6599
- if (attempt <= 0) return tv;
6600
- let next = tv;
6601
- if (attempt > next.attempt) {
6602
- if (!next.followLatest) {
6603
- return next.attempts.includes(attempt) ? next : { ...next, attempts: [...next.attempts, attempt] };
6604
- }
6605
- next = {
6606
- ...next,
6607
- view: import_core40.EMPTY_TASK_LOG,
6608
- attempt,
6609
- attempts: next.attempts.includes(attempt) ? next.attempts : [...next.attempts, attempt]
6610
- };
6611
- }
6612
- if (attempt < next.attempt) return next;
6613
- return clampQueueIndex({ ...next, view: events.reduce(import_core40.reduceTaskLog, next.view) });
6614
- }
6615
6670
  function taskLogAbandoned(state) {
6616
6671
  const tv = state.taskView;
6617
6672
  if (!tv || tv.loaded) return state;
6618
- let next = { ...tv, pending: [], loaded: true };
6619
- for (const batch of tv.pending) next = applyLive(next, batch.attempt, batch.events);
6620
- return { ...state, taskView: clampQueueIndex(next) };
6673
+ return { ...state, taskView: clampQueueIndex({ ...tv, ...(0, import_core40.followTaskLog)(tv, { type: "loaded", attempt: tv.attempt, events: [], abandoned: true }) }) };
6621
6674
  }
6622
6675
  function taskLogArrived(state, action) {
6623
6676
  const tv = state.taskView;
6624
6677
  if (!tv || tv.taskId !== action.taskId) return state;
6625
- if (!tv.loaded) {
6626
- return { ...state, taskView: { ...tv, pending: [...tv.pending, { attempt: action.attempt, events: action.events }] } };
6627
- }
6628
- return { ...state, taskView: applyLive(tv, action.attempt, action.events) };
6678
+ const next = (0, import_core40.followTaskLog)(tv, { ...action, type: "live" });
6679
+ return { ...state, taskView: next === tv ? tv : clampQueueIndex({ ...tv, ...next }) };
6629
6680
  }
6630
6681
  function announceApprovals(state, action) {
6631
6682
  if (state.taskView?.taskId === action.taskId) return state;
6632
6683
  const task = findTask(state.tasks, action.taskId);
6633
- const way = task ? `tab to the plan, select task ${task.order} and press enter (or /open ${task.order})` : "tab to the plan, select it and press enter";
6634
- const spoken = action.events.reduce((s, event) => event.type === "approval_requested" && !event.decided ? say(s, "system", `Task ${task?.order ?? "?"} waits for approval: ${(0, import_core40.runnerToolSubject)(event.tool, event.args)}: ${way}, then ctrl-y allows or ctrl-g denies`) : s, state);
6684
+ const way = task ? `tab to the plan, select task ${refFor(state, task)} and press enter (or /open ${refFor(state, task)})` : "tab to the plan, select it and press enter";
6685
+ const spoken = action.events.reduce((s, event) => event.type === "approval_requested" && !event.decided ? say(s, "system", `Task ${task ? refFor(state, task) : "?"} waits for approval: ${(0, import_core40.runnerToolSubject)(event.tool, event.args)}: ${way}, then ctrl-y allows or ctrl-g denies`) : s, state);
6635
6686
  return spoken !== state ? { ...spoken, scroll: state.scroll } : spoken;
6636
6687
  }
6637
6688
  function announceCheckpoint(state, action) {
6638
6689
  const task = findTask(state.tasks, action.taskId) ?? state.tasks.find((t) => t.title === action.title);
6639
- const label = task ? `Task ${task.order}` : action.title;
6640
- const where = task ? `tab to the plan, select task ${task.order} and press enter (or /open ${task.order}) to answer` : "tab to the plan, select it and press enter to answer";
6690
+ const label = task ? `Task ${refFor(state, task)}` : action.title;
6691
+ const where = task ? `tab to the plan, select task ${refFor(state, task)} and press enter (or /open ${refFor(state, task)}) to answer` : "tab to the plan, select it and press enter to answer";
6641
6692
  const spoken = say(state, "system", `${label} asks: ${(0, import_core40.truncateCheckpointSummary)(action.summary)}: ${where}`);
6642
6693
  return { ...spoken, scroll: state.scroll };
6643
6694
  }
6644
6695
  function taskLogLoaded(state, action) {
6645
6696
  const tv = state.taskView;
6646
6697
  if (!tv || tv.taskId !== action.taskId) return state;
6647
- const loaded = action.attempt >= 1 ? {
6648
- ...tv,
6649
- view: (0, import_core40.replayTaskLog)(action.events),
6650
- attempts: action.attempts ?? tv.attempts,
6651
- attempt: action.attempt
6652
- } : { ...tv, attempts: action.attempts ?? tv.attempts };
6653
- let next = { ...loaded, pending: [], loaded: true };
6654
- for (const batch of tv.pending) {
6655
- if (batch.attempt === action.attempt) continue;
6656
- next = applyLive(next, batch.attempt, batch.events);
6657
- }
6658
- return { ...state, taskView: clampQueueIndex(next) };
6698
+ return { ...state, taskView: clampQueueIndex({ ...tv, ...(0, import_core40.followTaskLog)(tv, { ...action, type: "loaded" }) }) };
6659
6699
  }
6660
6700
  function answerApproval(state, tv, sessionId, decision) {
6661
6701
  const approval = waitingApproval(tv);
@@ -6699,7 +6739,7 @@ function handleTaskViewKey(state, key) {
6699
6739
  const tv = state.taskView;
6700
6740
  if (!tv || !state.sessionId) return null;
6701
6741
  if (key.name === "alt-left" || key.name === "alt-right") {
6702
- const index = tv.attempts.indexOf(tv.attempt);
6742
+ const index = tv.attempts.indexOf(tv.loadingAttempt ?? tv.attempt);
6703
6743
  if (index < 0) return null;
6704
6744
  const target = key.name === "alt-left" ? index - 1 : index + 1;
6705
6745
  const attempt = tv.attempts[target];
@@ -6707,7 +6747,7 @@ function handleTaskViewKey(state, key) {
6707
6747
  return step(say(state, "system", target < 0 ? "This is the first attempt." : "This is the latest attempt."));
6708
6748
  }
6709
6749
  return step(
6710
- { ...state, taskView: { ...tv, followLatest: false } },
6750
+ { ...state, taskView: { ...tv, ...(0, import_core40.followTaskLog)(tv, { type: "picked", attempt }) } },
6711
6751
  [{ type: "loadTaskAttempt", sessionId: state.sessionId, taskId: tv.taskId, attempt }]
6712
6752
  );
6713
6753
  }
@@ -6739,7 +6779,7 @@ function handleTaskViewKey(state, key) {
6739
6779
 
6740
6780
  // src/tui/reducers/taskEdits.ts
6741
6781
  var import_core41 = require("@ordewell/core");
6742
- var import_plan_utils3 = require("@ordewell/core/plan-utils");
6782
+ var import_plan_utils5 = require("@ordewell/core/plan-utils");
6743
6783
  function openTaskRunnerPicker(state, task) {
6744
6784
  if (task.type !== "ai") return fail3(state, "Manual tasks do not run on an executor, so they have no runner.");
6745
6785
  const action = { kind: "set-task-runner", taskId: task.id };
@@ -6761,7 +6801,7 @@ function confirmRemoveTask(state, task) {
6761
6801
  overlay: {
6762
6802
  kind: "confirm",
6763
6803
  title: `Remove ${titledRefFor(state, task)}?`,
6764
- message: (0, import_plan_utils3.dependentsNotice)((0, import_plan_utils3.dependentsOf)(state.tasks, task.id)) ?? "This cannot be undone.",
6804
+ message: (0, import_plan_utils5.dependentsNotice)((0, import_plan_utils5.dependentsOf)(state.tasks, task.id), state.tasks) ?? "This cannot be undone.",
6765
6805
  action: { kind: "remove-task", taskId: task.id }
6766
6806
  }
6767
6807
  });
@@ -6785,7 +6825,7 @@ function confirmForceStartPastGate(state, task) {
6785
6825
  overlay: {
6786
6826
  kind: "confirm",
6787
6827
  title: `Force start ${titledRefFor(state, task)}?`,
6788
- message: (0, import_plan_utils3.pastGateConfirmation)((0, import_plan_utils3.titledRefs)(task.mergeGate ?? [], state.tasks), "It"),
6828
+ message: (0, import_plan_utils5.pastGateConfirmation)((0, import_plan_utils5.titledRefs)(task.mergeGate ?? [], state.tasks), "It"),
6789
6829
  action: { kind: "force-start-gated", taskId: task.id }
6790
6830
  }
6791
6831
  });
@@ -6793,16 +6833,16 @@ function confirmForceStartPastGate(state, task) {
6793
6833
  function openTaskDepsPicker(state, task) {
6794
6834
  const action = { kind: "set-task-deps", taskId: task.id };
6795
6835
  const items = pickerItemsFor(state, action);
6796
- if (items.length === 0) return fail3(state, `Nothing runs before ${refFor(state, task)}, so it has no possible dependencies.`);
6836
+ if (items.length === 0) return fail3(state, `No earlier siblings to offer as dependencies for ${refFor(state, task)}.`);
6797
6837
  return step({
6798
6838
  ...state,
6799
6839
  overlay: {
6800
6840
  kind: "picker",
6801
6841
  picker: picker(`Depends on \xB7 ${titledRefFor(state, task)}`, items, action, {
6802
- hint: "Only tasks earlier in the plan can be dependencies.",
6842
+ hint: "Offers earlier siblings; existing dependencies are kept.",
6803
6843
  multi: true,
6804
6844
  filterable: true,
6805
- chosen: task.dependencies.filter((id) => items.some((i) => i.id === id))
6845
+ chosen: task.dependencies
6806
6846
  })
6807
6847
  }
6808
6848
  });
@@ -7020,14 +7060,7 @@ function taskCommand(state, token, run) {
7020
7060
  });
7021
7061
  }
7022
7062
  function resolveTaskId2(state, token) {
7023
- const byId = findTask(state.tasks, token);
7024
- if (byId) return byId.id;
7025
- const order = Number(token);
7026
- if (Number.isInteger(order)) {
7027
- const byOrder = state.tasks.find((t) => t.order === order);
7028
- if (byOrder) return byOrder.id;
7029
- }
7030
- return null;
7063
+ return (0, import_plan_utils5.resolveUserTaskRef)(state.tasks, token)?.id ?? null;
7031
7064
  }
7032
7065
 
7033
7066
  // src/tui/reducers/planPane.ts
@@ -7272,28 +7305,6 @@ function foldRun(state, input) {
7272
7305
  const run = (0, import_core44.reduceRun)(state.run, input);
7273
7306
  return run === state.run ? state : withRun(state, run);
7274
7307
  }
7275
- function mapRunTasks(tasks, map) {
7276
- return tasks.map((task) => ({ ...map(task), subtasks: mapRunTasks(task.subtasks, map) }));
7277
- }
7278
- function applyIsolationRecord(state, plan) {
7279
- const record = isolationOfPlan(plan);
7280
- if (!record) return state;
7281
- const { run } = state;
7282
- return withRun(state, {
7283
- ...run,
7284
- tasks: mapRunTasks(run.tasks, (task) => ({ ...task, isolation: record.tasks[task.id] ?? { state: "none" } })),
7285
- handoff: withDeferral(record.handoff, run.handoff)
7286
- });
7287
- }
7288
- function clearRunIsolation(run) {
7289
- return {
7290
- ...run,
7291
- handoff: null,
7292
- runMerge: null,
7293
- mergeResult: null,
7294
- tasks: mapRunTasks(run.tasks, ({ isolation: _dropped, ...task }) => task)
7295
- };
7296
- }
7297
7308
  function applySettings(state, settings) {
7298
7309
  return {
7299
7310
  ...state,
@@ -7593,6 +7604,8 @@ function runCommand(state, { name, args }) {
7593
7604
  return withIdlePlanner(state, (sessionId) => step(state, [{ type: "forkConversation", sessionId }]));
7594
7605
  case "rewind":
7595
7606
  return rewind(state, args[0]);
7607
+ case "resend":
7608
+ return args[0] === "cancel" ? withSession(state, (sessionId) => step(state, [{ type: "cancelPlannerResend", sessionId }])) : step(say(state, "system", "A planner turn that hits the planner's usage limit is sent again at the reset: /resend cancel keeps it for your next message instead."));
7596
7609
  case "compact":
7597
7610
  return withIdlePlanner(
7598
7611
  state,
@@ -7866,7 +7879,7 @@ function runners(state, args) {
7866
7879
  multi: true,
7867
7880
  chosen: state.runners.filter((r) => r.enabled).map((r) => r.id)
7868
7881
  })
7869
- } });
7882
+ } }, [{ type: "loadRunners" }]);
7870
7883
  }
7871
7884
  function setMaxParallel(state, arg) {
7872
7885
  if (!arg) return step(say(state, "system", `Up to ${state.maxParallel} AI task${state.maxParallel === 1 ? "" : "s"} run at once: /parallel <n> changes it.`));
@@ -8162,6 +8175,24 @@ function reduceAction(state, action) {
8162
8175
  // tasks — may carry over into this.
8163
8176
  case "sessionStarted":
8164
8177
  return step(withRun({ ...state, sessionId: action.sessionId, goal: action.goal, taskView: null }, import_core49.EMPTY_RUN));
8178
+ // A loaded session is a different one: the turn the old one had out, the
8179
+ // prompts held behind it and a stop armed against it are not this one's.
8180
+ // (`sessionStarted` cannot reset them: `startConversation` dispatches it
8181
+ // as its own turn begins.)
8182
+ case "sessionLoaded":
8183
+ return step(withRun({
8184
+ ...state,
8185
+ sessionId: action.sessionId,
8186
+ goal: action.goal,
8187
+ taskView: null,
8188
+ planner: "idle",
8189
+ turnGate: import_core49.NO_TURN,
8190
+ pendingApprovals: [],
8191
+ queuedPrompts: import_core49.EMPTY_HOLD,
8192
+ stopArmed: false,
8193
+ forceSendPending: null,
8194
+ overlay: state.overlay?.kind === "approval" ? null : state.overlay
8195
+ }, import_core49.EMPTY_RUN));
8165
8196
  // The daemon registers a session only once planning succeeds; when it does
8166
8197
  // not, holding on to the id would send every next message to a session the
8167
8198
  // server never had.
@@ -8171,6 +8202,7 @@ function reduceAction(state, action) {
8171
8202
  sessionId: null,
8172
8203
  goal: "",
8173
8204
  taskView: null,
8205
+ turnGate: import_core49.NO_TURN,
8174
8206
  pendingApprovals: [],
8175
8207
  queuedPrompts: import_core49.EMPTY_HOLD,
8176
8208
  stopArmed: false,
@@ -8184,6 +8216,9 @@ function reduceAction(state, action) {
8184
8216
  case "chatRestored":
8185
8217
  if (stale(state, action.sessionId)) return step(state);
8186
8218
  return step({ ...state, conversation: (0, import_core49.fromTranscript)(action.history, action.researchLog, action.plannerUsage, action.pendingTurns), scroll: 0 });
8219
+ case "planRefreshed":
8220
+ if (stale(state, action.sessionId)) return step(state);
8221
+ return step(showPlan(state, action.plan));
8187
8222
  case "planUpdated": {
8188
8223
  if (stale(state, action.sessionId)) return step(state);
8189
8224
  const settled = { ...showPlan(state, action.plan), planner: "idle" };
@@ -8270,10 +8305,16 @@ function reduceAction(state, action) {
8270
8305
  if (stale(state, action.sessionId)) return step(state);
8271
8306
  return step(showDiff(state, action.diff));
8272
8307
  }
8273
- case "runCleared": {
8308
+ // What Merge all did, folded the way the Session says it: a merge that
8309
+ // leaves the run going keeps the marks (ADR-0020), and only `run_closed`
8310
+ // takes them (below).
8311
+ case "isolationMerged": {
8274
8312
  if (stale(state, action.sessionId)) return step(state);
8275
- const overlay = state.overlay?.kind === "handoff" ? null : state.overlay;
8276
- return step({ ...withRun(state, clearRunIsolation(state.run)), overlay });
8313
+ return step(withoutHandoffOverlay(foldRun(state, { type: "isolation_merge", result: action.result })));
8314
+ }
8315
+ case "runClosed": {
8316
+ if (stale(state, action.sessionId)) return step(state);
8317
+ return step(withoutHandoffOverlay(foldRun(state, { type: "run_closed" })));
8277
8318
  }
8278
8319
  case "executionComplete": {
8279
8320
  if (stale(state, action.sessionId)) return step(state);
@@ -8333,9 +8374,9 @@ function reduceAction(state, action) {
8333
8374
  if (stale(state, action.sessionId)) return step(state);
8334
8375
  if (action.pick !== void 0) return pickRewindTarget({ ...state, rewindTargets: action.targets }, action.pick);
8335
8376
  return step(refillPicker({ ...state, rewindTargets: action.targets }, ["rewind"]));
8336
- // A fork holds no run, whatever the session it came from was doing — the
8337
- // original keeps its run, and its events are stale from here on. Its plan
8338
- // arrives next, onto an empty run view.
8377
+ // A fork holds no run and no turn, whatever the session it came from was
8378
+ // doing — the original keeps both, and its events are stale from here on.
8379
+ // Its plan arrives next, onto an empty run view.
8339
8380
  case "sessionForked":
8340
8381
  return step(withRun({
8341
8382
  ...state,
@@ -8344,6 +8385,7 @@ function reduceAction(state, action) {
8344
8385
  taskView: null,
8345
8386
  status: "idle",
8346
8387
  planner: "idle",
8388
+ turnGate: import_core49.NO_TURN,
8347
8389
  planApproved: false,
8348
8390
  pendingApprovals: [],
8349
8391
  queuedPrompts: import_core49.EMPTY_HOLD,
@@ -8360,13 +8402,15 @@ function reduceAction(state, action) {
8360
8402
  focus: "chat",
8361
8403
  editor: { ...state.editor, text: action.text, cursor: action.text.length, historyIndex: state.editor.history.length, draft: "" }
8362
8404
  });
8405
+ case "sharedSettingsChanged":
8406
+ return step(state, [{ type: "loadScope" }]);
8363
8407
  case "runnersLoaded":
8364
- return step(refreshStepper(refillPicker({
8408
+ return step(refreshStepper(reseedRunnerChoice(state, refillPicker({
8365
8409
  ...state,
8366
8410
  runners: action.runners,
8367
8411
  runnersKnown: true,
8368
8412
  orchestratorModel: action.orchestratorModel ?? state.orchestratorModel
8369
- }, ["set-task-runner"])));
8413
+ }, ["set-task-runner"]))));
8370
8414
  case "failed": {
8371
8415
  if (!action.turn) return step(say(taskLogAbandoned(state), "error", action.message));
8372
8416
  if (state.forceSendPending) {
@@ -8407,12 +8451,16 @@ function reduceAction(state, action) {
8407
8451
  return step(disarmStop(state));
8408
8452
  }
8409
8453
  }
8454
+ function withoutHandoffOverlay(state) {
8455
+ return state.handoff || state.overlay?.kind !== "handoff" ? state : { ...state, overlay: null };
8456
+ }
8410
8457
  function showPlan(state, plan) {
8411
- const folded = applyIsolationRecord(foldRun(state, planSnapshot(plan)), plan);
8458
+ const folded = foldRun(state, planSnapshot(plan));
8412
8459
  const { tasks } = folded;
8413
8460
  const expandedTaskId = state.expandedTaskId && findTask(tasks, state.expandedTaskId) ? state.expandedTaskId : null;
8414
8461
  const next = {
8415
8462
  ...folded,
8463
+ plannerResend: plannerResendOf(plan),
8416
8464
  expandedTaskId,
8417
8465
  taskEditor: expandedTaskId !== null ? state.taskEditor : null
8418
8466
  };
@@ -8422,6 +8470,11 @@ function showPlan(state, plan) {
8422
8470
  focus: tasks.length === 0 ? "chat" : state.focus
8423
8471
  });
8424
8472
  }
8473
+ function plannerResendOf(plan) {
8474
+ const saved = plan?.plannerResend;
8475
+ if (typeof saved?.at !== "string") return null;
8476
+ return { ...typeof saved.runner === "string" ? { runner: saved.runner } : {}, at: saved.at };
8477
+ }
8425
8478
  function runStatus(state, tasks) {
8426
8479
  if (tasks.some(import_core28.isTaskRunning)) return "executing";
8427
8480
  if (tasks.some((t) => t.status === "awaiting_user") || state.runMerge?.paused) return "executing";
@@ -8760,6 +8813,7 @@ function createInbound(dispatch, sessionId) {
8760
8813
  const delivered = new Delivered(256);
8761
8814
  const copies = new Copies(64);
8762
8815
  const shown = new WindowSet(64);
8816
+ const shownTexts = new WindowSet(64);
8763
8817
  const pending = new WindowSet(16);
8764
8818
  const DELTA_DEBOUNCE_MS = 75;
8765
8819
  let held = null;
@@ -8788,7 +8842,8 @@ function createInbound(dispatch, sessionId) {
8788
8842
  timer = setTimeout(flush, DELTA_DEBOUNCE_MS);
8789
8843
  };
8790
8844
  const showReply = (message) => {
8791
- shown.add(message.content);
8845
+ shown.add(replyKey(message.content, message.timestamp));
8846
+ shownTexts.add(message.content);
8792
8847
  dispatch({ type: "sessionMessage", message, sessionId });
8793
8848
  };
8794
8849
  const fresh = (id, event) => {
@@ -8860,14 +8915,15 @@ function createInbound(dispatch, sessionId) {
8860
8915
  const id = `e${++subscription}`;
8861
8916
  return (event) => receive("execution", id, event);
8862
8917
  },
8863
- backfill: (content, timestamp) => {
8864
- if (shown.has(content)) return;
8865
- const message = { type: "planner_message", content, timestamp: timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
8918
+ backfill: (content, timestamp, runs) => {
8919
+ if (timestamp ? shown.has(replyKey(content, timestamp)) : shownTexts.has(content)) return;
8920
+ const message = { type: "planner_message", content, timestamp: timestamp ?? (/* @__PURE__ */ new Date()).toISOString(), ...runs ? { runs } : {} };
8866
8921
  showReply(message);
8867
8922
  if (timestamp) pending.add(replyKey(content, timestamp));
8868
8923
  },
8869
8924
  redrawn: (content, timestamp) => {
8870
- shown.add(content);
8925
+ shown.add(replyKey(content, timestamp));
8926
+ shownTexts.add(content);
8871
8927
  pending.add(replyKey(content, timestamp));
8872
8928
  },
8873
8929
  flush,
@@ -8939,7 +8995,7 @@ function dispatchLifecycle(dispatch, event, sessionId) {
8939
8995
  // its only reader, and the reducer drops it unless that view is open on
8940
8996
  // this task. The run's watcher itself does nothing with it.
8941
8997
  case "task_log":
8942
- dispatch({ type: "taskLog", taskId: String(event.taskId), attempt: event.attempt, events: event.events, sessionId });
8998
+ dispatch({ type: "taskLog", taskId: String(event.taskId), attempt: event.attempt, events: event.events, position: stampOf(event) ?? void 0, sessionId });
8943
8999
  return;
8944
9000
  case "task_updated":
8945
9001
  dispatch({ type: "taskUpdated", taskId: String(event.taskId), changes: event.changes, sessionId });
@@ -8949,10 +9005,13 @@ function dispatchLifecycle(dispatch, event, sessionId) {
8949
9005
  // does with it.
8950
9006
  case "task_output":
8951
9007
  return;
8952
- // Its asker already has the words, from the merge request itself; a
8953
- // viewer that did not ask still has to drop a run that is gone.
9008
+ // Its asker already has the words, from the merge request itself. Whether
9009
+ // the merge ended the run is the Session's to say, in `run_closed`.
8954
9010
  case "isolation_merge":
8955
- if (event.result.outcome === "merged") dispatch({ type: "runCleared", sessionId });
9011
+ dispatch({ type: "isolationMerged", result: event.result, sessionId });
9012
+ return;
9013
+ case "run_closed":
9014
+ dispatch({ type: "runClosed", sessionId });
8956
9015
  return;
8957
9016
  default: {
8958
9017
  const unhandled = event;
@@ -9049,9 +9108,10 @@ async function perform(effect, deps) {
9049
9108
  case "execute":
9050
9109
  await withExecutionStream(deps, effect.sessionId, () => api.executePlan(effect.sessionId));
9051
9110
  return;
9111
+ // The seam keeps its own line of a Stop in the timeline and the stream
9112
+ // announces the run's end, so this adds none (ADR-0029).
9052
9113
  case "stopExecution":
9053
9114
  await api.stopExecution(effect.sessionId);
9054
- dispatch({ type: "notice", message: "Execution stopped." });
9055
9115
  return;
9056
9116
  case "cancelPlanning": {
9057
9117
  const { cancelled } = await api.cancelPlanning(effect.sessionId);
@@ -9069,8 +9129,7 @@ async function perform(effect, deps) {
9069
9129
  case "taskAction": {
9070
9130
  const { sessionId, taskId, action } = effect;
9071
9131
  if (action === "cancel-resume") {
9072
- const { cancelled } = await api.cancelResume(sessionId, taskId);
9073
- dispatch({ type: "notice", message: cancelled ? "Resume cancelled: retry the task once the limit resets." : "That task had no resume scheduled." });
9132
+ await api.cancelResume(sessionId, taskId);
9074
9133
  await refreshPlan(deps, sessionId);
9075
9134
  return;
9076
9135
  }
@@ -9128,19 +9187,19 @@ async function perform(effect, deps) {
9128
9187
  // The saved log is read off disk (ADR-0018, P1) so the view opens with the
9129
9188
  // task's history; the live `task_log` stream then catches it up in place.
9130
9189
  case "openTaskLog": {
9131
- const attempts = await api.getTaskLogAttempts(effect.sessionId, effect.taskId, workspace);
9190
+ const { attempts, position } = await api.getTaskLogAttempts(effect.sessionId, effect.taskId, workspace);
9132
9191
  if (attempts.length === 0) {
9133
- dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts: [], attempt: 0, events: [], sessionId: effect.sessionId });
9192
+ dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts: [], attempt: 0, events: [], position, sessionId: effect.sessionId });
9134
9193
  return;
9135
9194
  }
9136
9195
  const attempt = attempts[attempts.length - 1];
9137
- const events = await api.getTaskLog(effect.sessionId, effect.taskId, attempt, workspace);
9138
- dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts, attempt, events, sessionId: effect.sessionId });
9196
+ const log = await api.getTaskLog(effect.sessionId, effect.taskId, attempt, workspace);
9197
+ dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempts, ...log, sessionId: effect.sessionId });
9139
9198
  return;
9140
9199
  }
9141
9200
  case "loadTaskAttempt": {
9142
- const events = await api.getTaskLog(effect.sessionId, effect.taskId, effect.attempt, workspace);
9143
- dispatch({ type: "taskLogLoaded", taskId: effect.taskId, attempt: effect.attempt, events, sessionId: effect.sessionId });
9201
+ const log = await api.getTaskLog(effect.sessionId, effect.taskId, effect.attempt, workspace);
9202
+ dispatch({ type: "taskLogLoaded", taskId: effect.taskId, ...log, sessionId: effect.sessionId });
9144
9203
  return;
9145
9204
  }
9146
9205
  // The daemon queues the message (or delivers it to a waiting task) and
@@ -9160,24 +9219,20 @@ async function perform(effect, deps) {
9160
9219
  await refreshPlan(deps, sessionId);
9161
9220
  return;
9162
9221
  }
9163
- case "removeTaskMessage": {
9164
- const { removed } = await api.removeQueuedTaskMessage(effect.sessionId, effect.taskId, effect.messageId);
9165
- if (!removed) dispatch({ type: "notice", message: "That message was already delivered to the task." });
9222
+ // One already delivered is refused in the daemon's words.
9223
+ case "removeTaskMessage":
9224
+ await api.removeQueuedTaskMessage(effect.sessionId, effect.taskId, effect.messageId);
9166
9225
  return;
9167
- }
9168
9226
  // The task log shows the forced message at the head of the queue, then
9169
9227
  // the interrupt, then the turn it opens; nothing to dispatch here.
9170
9228
  case "forceSendTaskMessage":
9171
9229
  await api.forceSendTaskMessage(effect.sessionId, effect.taskId, effect.text);
9172
9230
  return;
9173
- case "forceSendQueuedTaskMessage": {
9174
- const { sent } = await api.forceSendQueuedTaskMessage(effect.sessionId, effect.taskId, effect.messageId);
9175
- if (!sent) dispatch({ type: "notice", message: "That message has already reached the runner." });
9231
+ case "forceSendQueuedTaskMessage":
9232
+ await api.forceSendQueuedTaskMessage(effect.sessionId, effect.taskId, effect.messageId);
9176
9233
  return;
9177
- }
9178
9234
  case "interruptTask":
9179
9235
  await api.interruptTask(effect.sessionId, effect.taskId);
9180
- dispatch({ type: "notice", message: "Interrupting the task\u2026" });
9181
9236
  return;
9182
9237
  case "setModel": {
9183
9238
  const settled = await api.updateSettings({ orchestratorModel: effect.modelId });
@@ -9317,11 +9372,12 @@ async function perform(effect, deps) {
9317
9372
  else unfollow(deps, effect.sessionId);
9318
9373
  throw err;
9319
9374
  }
9320
- const { plan, goal } = adopted2;
9321
- dispatch({ type: "sessionStarted", sessionId: effect.sessionId, goal });
9375
+ const { plan, goal, run } = adopted2;
9376
+ dispatch({ type: "sessionLoaded", sessionId: effect.sessionId, goal });
9322
9377
  dispatch(restoredChat(plan, effect.sessionId));
9323
9378
  inbound.releaseSnapshot(true);
9324
9379
  dispatch({ type: "planUpdated", plan, sessionId: effect.sessionId });
9380
+ if (run) dispatch({ type: "runSnapshot", view: run, sessionId: effect.sessionId });
9325
9381
  dispatch({ type: "notice", message: `Loaded "${goal || effect.sessionId}".` });
9326
9382
  return;
9327
9383
  }
@@ -9361,6 +9417,12 @@ async function perform(effect, deps) {
9361
9417
  // user gets to read — no separate notice to repeat it. The daemon also
9362
9418
  // broadcast that summary; `redrawn` keeps a late socket copy of it from
9363
9419
  // landing as a second, spoken turn.
9420
+ case "cancelPlannerResend": {
9421
+ const { cancelled } = await api.cancelPlannerResend(effect.sessionId);
9422
+ dispatch({ type: "notice", message: cancelled ? "Resend cancelled: what waits goes with your next message." : "Nothing waits on the planner's usage limit to be sent at a reset." });
9423
+ await refreshPlan(deps, effect.sessionId);
9424
+ return;
9425
+ }
9364
9426
  case "compactConversation": {
9365
9427
  const { plan } = await api.compactConversation(effect.sessionId);
9366
9428
  const inbound = inboundFor(deps.api, deps.dispatch, effect.sessionId);
@@ -9376,16 +9438,15 @@ async function perform(effect, deps) {
9376
9438
  // A conflict or a refusal is an answer, not a fault: either way the user's
9377
9439
  // tree is exactly as it was, and the words say what to do next. A full
9378
9440
  // merge of a settled run leaves nothing to hand over — the daemon has
9379
- // cleared the run up; one mid-run leaves the run going (ADR-0020).
9441
+ // cleared the run up and says so on the stream (`run_closed`); one
9442
+ // mid-run leaves the run going (ADR-0020).
9380
9443
  case "isolationMerge": {
9381
9444
  const { ok, message } = mergeOutcome(await api.mergeRun(effect.sessionId), effect.branch, effect.group === true, effect.repaired ?? []);
9382
- if (ok && !effect.midRun) dispatch({ type: "runCleared", sessionId: effect.sessionId });
9383
9445
  dispatch({ type: ok ? "notice" : "failed", message });
9384
9446
  return;
9385
9447
  }
9386
9448
  case "isolationDiscard":
9387
9449
  await api.discardRun(effect.sessionId);
9388
- dispatch({ type: "runCleared", sessionId: effect.sessionId });
9389
9450
  dispatch({ type: "notice", message: `Discarded the run and ${effect.branch}.` });
9390
9451
  return;
9391
9452
  case "isolationCleanup":
@@ -9423,6 +9484,12 @@ async function perform(effect, deps) {
9423
9484
  } catch {
9424
9485
  }
9425
9486
  return;
9487
+ case "loadRunners":
9488
+ await loadRunners(deps);
9489
+ return;
9490
+ case "loadScope":
9491
+ await Promise.all([loadRunners(deps), loadSettings(deps)]);
9492
+ return;
9426
9493
  case "refresh":
9427
9494
  await Promise.all([loadRunners(deps), loadSettings(deps), loadModels(deps)]);
9428
9495
  if (effect.announce) dispatch({ type: "notice", message: "Refreshed runners, settings and models." });
@@ -9527,8 +9594,7 @@ async function converse(deps, sessionId, call) {
9527
9594
  await Promise.all([stream.ready, follow(deps, sessionId)]);
9528
9595
  const plan = await call();
9529
9596
  inbound.flush();
9530
- const reply = lastAssistantMessage(plan);
9531
- if (reply) inbound.backfill(reply.content, reply.timestamp);
9597
+ for (const reply of turnReplies(plan)) inbound.backfill(reply.content, reply.timestamp, reply.runs);
9532
9598
  deps.dispatch({ type: "planUpdated", plan, sessionId });
9533
9599
  } finally {
9534
9600
  inbound.flush();
@@ -9546,17 +9612,18 @@ function restoredChat(plan, sessionId) {
9546
9612
  sessionId
9547
9613
  };
9548
9614
  }
9549
- function lastAssistantMessage(plan) {
9615
+ function turnReplies(plan) {
9550
9616
  const history = plan?.conversationHistory ?? [];
9617
+ const replies = [];
9551
9618
  for (let i = history.length - 1; i >= 0; i--) {
9552
9619
  const entry = history[i];
9553
- if ((0, import_core50.isUserMessage)(entry)) return null;
9620
+ if ((0, import_core50.isUserMessage)(entry)) break;
9554
9621
  if (entry.role === "assistant" && entry.kind === void 0) {
9555
- const { content, timestamp } = entry;
9556
- return { content, ...timestamp ? { timestamp } : {} };
9622
+ const { content, timestamp, runs } = entry;
9623
+ replies.unshift({ content, ...timestamp ? { timestamp } : {}, ...runs ? { runs } : {} });
9557
9624
  }
9558
9625
  }
9559
- return null;
9626
+ return replies;
9560
9627
  }
9561
9628
  function compactionSummary(plan) {
9562
9629
  const history = plan?.conversationHistory ?? [];
@@ -9564,8 +9631,9 @@ function compactionSummary(plan) {
9564
9631
  return entry ? { content: entry.content, ...entry.timestamp ? { timestamp: entry.timestamp } : {} } : null;
9565
9632
  }
9566
9633
  async function refreshPlan(deps, sessionId) {
9567
- const { plan } = await deps.api.getSession(sessionId, deps.workspace);
9568
- deps.dispatch({ type: "planUpdated", plan, sessionId });
9634
+ const { plan, run } = await deps.api.getSession(sessionId, deps.workspace);
9635
+ deps.dispatch({ type: "planRefreshed", plan, sessionId });
9636
+ if (run) deps.dispatch({ type: "runSnapshot", view: run, sessionId });
9569
9637
  }
9570
9638
  async function loadModels(deps) {
9571
9639
  deps.dispatch({ type: "modelsLoaded", ...normalizeCatalog(await deps.api.getModels()) });
@@ -9986,10 +10054,12 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
9986
10054
  if (terminal) app.dispatch({ type: "resize", ...terminal.size() });
9987
10055
  }
9988
10056
  });
10057
+ const watchingSettings = settings.watch(() => app.dispatch({ type: "sharedSettingsChanged" }));
9989
10058
  const { rows, cols } = terminal.size();
9990
10059
  app.dispatch({ type: "resize", rows, cols });
9991
10060
  app.start();
9992
10061
  function shutdown(code) {
10062
+ watchingSettings.dispose();
9993
10063
  terminal?.close();
9994
10064
  if (owned) stopDaemon(port).catch(() => {
9995
10065
  });
@@ -10059,6 +10129,7 @@ var COMMANDS = {
10059
10129
  fork: handleFork,
10060
10130
  compact: handleCompact,
10061
10131
  rewind: handleRewind,
10132
+ resend: handleResend,
10062
10133
  handoff: handleHandoff,
10063
10134
  tui: handleTui
10064
10135
  };