@ordewell/cli 0.10.4 → 0.10.5

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 +174 -72
  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
  };
@@ -644,6 +644,10 @@ var ApiClient = class _ApiClient {
644
644
  cancelResume(sessionId, taskId) {
645
645
  return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/resume/cancel`, "Cancel resume failed");
646
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");
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
653
  return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/resume/now`, "Resume failed");
@@ -1610,10 +1614,21 @@ var STATE_OF = {
1610
1614
  function reposOf(run) {
1611
1615
  if (Array.isArray(run.repos)) {
1612
1616
  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;
1617
+ return repos.length > 0 ? repos.map((r) => ({
1618
+ path: String(r.path),
1619
+ integrationBranch: String(r.integrationBranch),
1620
+ baseRef: String(r.baseRef),
1621
+ ...typeof r.baseBranch === "string" && r.baseBranch ? { baseBranch: r.baseBranch } : {},
1622
+ ...r.startedDetached === true ? { startedDetached: true } : {}
1623
+ })) : null;
1614
1624
  }
1615
1625
  if (typeof run.integrationBranch === "string" && typeof run.baseRef === "string") {
1616
- return [{ path: ".", integrationBranch: run.integrationBranch, baseRef: run.baseRef }];
1626
+ return [{
1627
+ path: ".",
1628
+ integrationBranch: run.integrationBranch,
1629
+ baseRef: run.baseRef,
1630
+ ...typeof run.baseBranch === "string" && run.baseBranch ? { baseBranch: run.baseBranch } : {}
1631
+ }];
1617
1632
  }
1618
1633
  return null;
1619
1634
  }
@@ -1664,15 +1679,11 @@ function handoffBranch(handoff) {
1664
1679
  return [...new Set(handoff.repos.map((r) => r.integrationBranch))].join(", ");
1665
1680
  }
1666
1681
  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);
1682
+ const group = (0, import_core10.isRepoGroup)(handoff);
1683
+ return handoff.repos.map((r) => {
1684
+ const ref = r.baseRef.slice(0, length);
1685
+ return `${group ? `${r.path}: ` : ""}${r.baseBranch ? `${r.baseBranch} @ ${ref}` : `${r.startedDetached ? "detached at" : "base"} ${ref}`}`;
1686
+ }).join(", ");
1676
1687
  }
1677
1688
  function repairedLanded(handoff) {
1678
1689
  return handoff.landed.filter((t) => t.repairedFiles?.length);
@@ -1750,7 +1761,7 @@ async function followExecution(api, sessionId, start, onBlocked) {
1750
1761
  for (const t of entries) {
1751
1762
  const icon = iconFor(t.status);
1752
1763
  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` : "";
1764
+ const waits = t.waitsForApproval ? `: ${inLine(import_core12.WAITS_FOR_APPROVAL_LABEL)}: \`ordewell force-start <id>\` approves it, \`ordewell run\` approves all` : "";
1754
1765
  lines.push(` ${icon} #${t.id.slice(-4)} ${t.status}${verdict}${waits}`);
1755
1766
  }
1756
1767
  const output = lines.join("\n");
@@ -1812,10 +1823,10 @@ Done. ${summary.completed || 0} completed, ${summary.failed || 0} failed, ${summ
1812
1823
  }
1813
1824
  if (event.type === "isolation_handoff") {
1814
1825
  const n = event.landed.length;
1815
- const where = isRepoGroup(event) ? ` in ${event.repos.map((r) => r.path).join(", ")}` : "";
1826
+ const where = (0, import_core10.isRepoGroup)(event) ? ` in ${event.repos.map((r) => r.path).join(", ")}` : "";
1816
1827
  console.log(`
1817
1828
  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}`);
1829
+ if ((0, import_core10.isRepoGroup)(event)) for (const line of repoResultLines(event)) console.log(` ${line}`);
1819
1830
  if (event.deferredTo === "supervisor") console.log(" The supervisor may merge it.");
1820
1831
  console.log(" `ordewell handoff review|merge|discard|cleanup` to land it.");
1821
1832
  }
@@ -2656,27 +2667,28 @@ function formatCost(currency, amount) {
2656
2667
  function tokenLine(blocks2, cols) {
2657
2668
  const usage = blocks2[blocks2.length - 1];
2658
2669
  if (usage?.type !== "usage") return null;
2659
- const parts = usageMeasures(usage.totals, false);
2670
+ const parts = usageMeasures(usage.totals);
2660
2671
  if (usage.model && !usage.contextFill) parts.push("ctx unknown");
2661
2672
  if (usage.contextFill) parts.splice(2, 0, `${Math.round(usage.contextFill.usedTokens / usage.contextFill.windowTokens * 100)}% ctx`);
2662
2673
  return parts.length > 0 ? style.grey(truncate(parts.join(" \xB7 "), cols)) : null;
2663
2674
  }
2664
- function usageMeasures(totals, unknown) {
2675
+ function usageMeasures(totals) {
2665
2676
  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"] : [],
2677
+ ...totals.inputTokens !== void 0 ? [`${compactCount(totals.inputTokens)} in`] : [],
2678
+ ...totals.outputTokens !== void 0 ? [`${compactCount(totals.outputTokens)} out`] : [],
2668
2679
  ...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"] : []
2680
+ ...Object.entries(totals.reportedCost ?? {}).map(([currency, amount]) => formatCost(currency, amount))
2671
2681
  ];
2672
2682
  }
2673
2683
  function taskUsageText(usage) {
2674
2684
  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"
2685
+ ...usageMeasures(usage.totals),
2686
+ ...usage.attempt !== void 0 ? [usage.contextFill ? `ctx ${Math.round(usage.contextFill.usedTokens / usage.contextFill.windowTokens * 100)}%` : "ctx unknown"] : []
2678
2687
  ].join(" \xB7 ");
2679
2688
  }
2689
+ function assignmentFlagText(row) {
2690
+ return `${row.assignmentFlag} \u2014 ${row.assignmentConsequence}`;
2691
+ }
2680
2692
 
2681
2693
  // src/commands/status.ts
2682
2694
  var import_fs4 = require("fs");
@@ -2694,7 +2706,7 @@ async function handleStatus(subArgs, injectedApi) {
2694
2706
  emitJson(session, outputPath);
2695
2707
  return;
2696
2708
  }
2697
- printOneSession(sessionId, session.meta, session.plan);
2709
+ printOneSession(sessionId, session.meta, session.plan, await assignmentScope(api));
2698
2710
  } else {
2699
2711
  const sessions = await api.getSessions(workspace);
2700
2712
  if (asJson) {
@@ -2733,7 +2745,20 @@ function emitJson(payload, outputPath) {
2733
2745
  process.exit(1);
2734
2746
  }
2735
2747
  }
2736
- function printOneSession(sessionId, meta, plan) {
2748
+ async function assignmentScope(api) {
2749
+ try {
2750
+ const [runners2, settings, models] = await Promise.all([api.getRunners(), api.getSettings(), api.getModels()]);
2751
+ if (!Array.isArray(runners2.runners) || runners2.runners.length === 0) return void 0;
2752
+ return {
2753
+ enabledRunners: runners2.runners.filter((r) => r.enabled).map((r) => r.id),
2754
+ allowlist: settings.modelAllowlist ?? {},
2755
+ models: models.modelsByRunner ?? {}
2756
+ };
2757
+ } catch {
2758
+ return void 0;
2759
+ }
2760
+ }
2761
+ function printOneSession(sessionId, meta, plan, runnerScope) {
2737
2762
  const allTasks = allTasksOf(plan);
2738
2763
  const everyTask = (0, import_core15.flattenTasks)(allTasks);
2739
2764
  const blocked = (0, import_core15.blockingFailures)(everyTask.map((t) => ({ ...t, subtasks: [], dependencies: t.dependencies ?? [] })));
@@ -2742,20 +2767,23 @@ function printOneSession(sessionId, meta, plan) {
2742
2767
  console.log(`Goal: ${meta.goal}`);
2743
2768
  console.log(`Runner: ${(meta.runners || []).join(", ")} | Status: ${meta.status}`);
2744
2769
  console.log(`Phase: ${plan?.phase || "unknown"} | Created: ${meta.createdAt}`);
2770
+ const isolation = isolationOfPlan(plan);
2771
+ if (isolation) console.log(`Base: ${handoffBase(isolation.handoff, 8)}`);
2745
2772
  console.log(`Tasks: ${everyTask.length} total | ${count("completed")} done | ${count("failed")} failed | ${count("in_progress")} running | ${count("blocked")} blocked | ${count("skipped")} skipped
2746
2773
  `);
2747
2774
  const usage = (0, import_core15.planUsage)(allTasks);
2748
2775
  if (usage.attempts) console.log(`Tasks total: ${taskUsageText(usage)}`);
2749
2776
  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;
2777
+ const approval = (row, ref) => row.waitsForApproval ? `${row.waitsForApproval}: \`ordewell force-start ${ref}\` approves it, \`ordewell run\` approves all` : null;
2751
2778
  for (const t of allTasks) {
2752
2779
  const icon = iconFor(blocked.has(t.id) ? "blocked" : t.status);
2753
2780
  const model = t.assignedModel ? ` (${t.assignedModel.modelLabel})` : "";
2754
2781
  const shortId = (t.id ?? "").slice(-8);
2755
2782
  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 });
2783
+ const row = (0, import_core15.taskRowView)({ ...t, dependencies: t.dependencies ?? [], waitsForApproval: (0, import_core15.waitsForApproval)(t, record) }, { tasks: allTasks, runnerScope });
2757
2784
  const waiting = approval(row, (0, import_core15.taskOrderLabel)(t));
2758
2785
  if (waiting) console.log(` ${waiting}`);
2786
+ if (row.assignmentFlag) console.log(` \u26A0 ${assignmentFlagText(row)}`);
2759
2787
  if (row.blockedBy) console.log(` ${(0, import_core15.blockedByLabel)(row.blockedBy)}`);
2760
2788
  if (row.status === "skipped") console.log(` ${(0, import_core15.skippedLabel)(row.skipReason)}`);
2761
2789
  if (row.usage) console.log(` ${taskUsageText(row.usage)}`);
@@ -2764,9 +2792,10 @@ function printOneSession(sessionId, meta, plan) {
2764
2792
  const subModel = sub.assignedModel ? ` (${sub.assignedModel.modelLabel})` : "";
2765
2793
  const subShortId = (sub.id ?? "").slice(-8);
2766
2794
  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 });
2795
+ const subRow = (0, import_core15.taskRowView)({ ...sub, dependencies: sub.dependencies ?? [], waitsForApproval: (0, import_core15.waitsForApproval)(sub, record) }, { parent: t, tasks: allTasks, runnerScope });
2768
2796
  const subWaiting = approval(subRow, (0, import_core15.taskOrderLabel)(sub, t));
2769
2797
  if (subWaiting) console.log(` ${subWaiting}`);
2798
+ if (subRow.assignmentFlag) console.log(` \u26A0 ${assignmentFlagText(subRow)}`);
2770
2799
  if (subRow.blockedBy) console.log(` ${(0, import_core15.blockedByLabel)(subRow.blockedBy)}`);
2771
2800
  if (subRow.status === "skipped") console.log(` ${(0, import_core15.skippedLabel)(subRow.skipReason)}`);
2772
2801
  if (subRow.usage) console.log(` ${taskUsageText(subRow.usage)}`);
@@ -3263,8 +3292,8 @@ function makeHandler(action, command = action) {
3263
3292
  async (api, sessionId, taskId) => {
3264
3293
  if (PASSES_GATE.has(action)) await confirmPastGate(subArgs, api, sessionId, taskId, confirm2);
3265
3294
  try {
3266
- await api.taskControl(sessionId, taskId, action);
3267
- console.log(PAST_TENSE[action]);
3295
+ const { queued } = await api.taskControl(sessionId, taskId, action);
3296
+ console.log(queued ?? PAST_TENSE[action]);
3268
3297
  } catch (err) {
3269
3298
  console.error(`Failed to ${action} task: ${err.message}`);
3270
3299
  process.exit(1);
@@ -3561,6 +3590,17 @@ async function handleRewind(subArgs, injectedApi) {
3561
3590
  console.log("\nThe message you rewound to, to resend or edit:\n");
3562
3591
  console.log(fork.rewoundMessage);
3563
3592
  }
3593
+ async function handleResend(subArgs, injectedApi) {
3594
+ if (positionals(subArgs)[0] !== "cancel") fail("Usage: ordewell resend cancel [--session-id <id>] [--workspace /path]");
3595
+ const { api, sessionId } = await adopted(subArgs, injectedApi);
3596
+ let cancelled;
3597
+ try {
3598
+ ({ cancelled } = await api.cancelPlannerResend(sessionId));
3599
+ } catch (err) {
3600
+ fail(`Failed to cancel the resend: ${err.message}`);
3601
+ }
3602
+ 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.");
3603
+ }
3564
3604
 
3565
3605
  // src/tui/transcript.ts
3566
3606
  var import_core22 = require("@ordewell/core");
@@ -3594,7 +3634,7 @@ var HANDOFF_ACTIONS = [
3594
3634
  { id: "cleanup", label: "Clean up", hint: "remove worktrees and task branches, keep this branch" }
3595
3635
  ];
3596
3636
  function handoffActions(handoff) {
3597
- if (!isRepoGroup(handoff)) return HANDOFF_ACTIONS;
3637
+ if (!(0, import_core10.isRepoGroup)(handoff)) return HANDOFF_ACTIONS;
3598
3638
  return [
3599
3639
  { id: "review", label: "Review diff", hint: "each repository's branch against the commit it forked from" },
3600
3640
  { id: "merge", label: "Merge all", hint: "every repository's branch into what you have checked out there (asks first)" },
@@ -3619,7 +3659,7 @@ function handoffCommand(state, arg) {
3619
3659
  }
3620
3660
  function handoffArrived(state, handoff) {
3621
3661
  const landed = handoff.landed.length;
3622
- const where = isRepoGroup(handoff) ? ` in ${reposOf2(handoff)}` : "";
3662
+ const where = (0, import_core10.isRepoGroup)(handoff) ? ` in ${reposOf2(handoff)}` : "";
3623
3663
  const branch = handoffBranch(handoff);
3624
3664
  const tasks = `${landed} task${landed === 1 ? "" : "s"} landed`;
3625
3665
  const received = { ...state, handoff };
@@ -3654,8 +3694,8 @@ function runHandoffAction(state, id) {
3654
3694
  ...state,
3655
3695
  overlay: {
3656
3696
  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)}`,
3697
+ title: (0, import_core10.isRepoGroup)(handoff) ? "Merge what has landed into your branches?" : "Merge what has landed into your branch?",
3698
+ 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
3699
  action: { kind: "merge-run" }
3660
3700
  }
3661
3701
  },
@@ -3665,15 +3705,15 @@ function runHandoffAction(state, id) {
3665
3705
  return {
3666
3706
  state: {
3667
3707
  ...state,
3668
- overlay: isRepoGroup(handoff) ? {
3708
+ overlay: (0, import_core10.isRepoGroup)(handoff) ? {
3669
3709
  kind: "confirm",
3670
3710
  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)}`,
3711
+ 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
3712
  action: { kind: "merge-run" }
3673
3713
  } : {
3674
3714
  kind: "confirm",
3675
3715
  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)}`,
3716
+ 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
3717
  action: { kind: "merge-run" }
3678
3718
  }
3679
3719
  },
@@ -3686,7 +3726,7 @@ function runHandoffAction(state, id) {
3686
3726
  overlay: {
3687
3727
  kind: "confirm",
3688
3728
  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.`,
3729
+ 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
3730
  action: { kind: "discard-run" }
3691
3731
  }
3692
3732
  },
@@ -3704,7 +3744,7 @@ function confirmedHandoff(state, kind) {
3704
3744
  type: "isolationMerge",
3705
3745
  sessionId,
3706
3746
  branch: handoffBranch(handoff),
3707
- ...isRepoGroup(handoff) ? { group: true } : {},
3747
+ ...(0, import_core10.isRepoGroup)(handoff) ? { group: true } : {},
3708
3748
  ...repaired.length > 0 ? { repaired } : {},
3709
3749
  ...midRun(state) ? { midRun: true } : {}
3710
3750
  } : { type: "isolationDiscard", sessionId, branch: handoffBranch(handoff) };
@@ -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);
@@ -4426,6 +4466,7 @@ function initialState(overrides = {}) {
4426
4466
  workspace: process.cwd(),
4427
4467
  handoff: null,
4428
4468
  runMerge: null,
4469
+ plannerResend: null,
4429
4470
  overlay: null,
4430
4471
  pendingApprovals: [],
4431
4472
  queuedPrompts: import_core28.EMPTY_HOLD,
@@ -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" },
@@ -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
  }
@@ -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
  [
@@ -7593,6 +7670,8 @@ function runCommand(state, { name, args }) {
7593
7670
  return withIdlePlanner(state, (sessionId) => step(state, [{ type: "forkConversation", sessionId }]));
7594
7671
  case "rewind":
7595
7672
  return rewind(state, args[0]);
7673
+ case "resend":
7674
+ 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
7675
  case "compact":
7597
7676
  return withIdlePlanner(
7598
7677
  state,
@@ -7866,7 +7945,7 @@ function runners(state, args) {
7866
7945
  multi: true,
7867
7946
  chosen: state.runners.filter((r) => r.enabled).map((r) => r.id)
7868
7947
  })
7869
- } });
7948
+ } }, [{ type: "loadRunners" }]);
7870
7949
  }
7871
7950
  function setMaxParallel(state, arg) {
7872
7951
  if (!arg) return step(say(state, "system", `Up to ${state.maxParallel} AI task${state.maxParallel === 1 ? "" : "s"} run at once: /parallel <n> changes it.`));
@@ -8360,13 +8439,15 @@ function reduceAction(state, action) {
8360
8439
  focus: "chat",
8361
8440
  editor: { ...state.editor, text: action.text, cursor: action.text.length, historyIndex: state.editor.history.length, draft: "" }
8362
8441
  });
8442
+ case "sharedSettingsChanged":
8443
+ return step(state, [{ type: "loadScope" }]);
8363
8444
  case "runnersLoaded":
8364
- return step(refreshStepper(refillPicker({
8445
+ return step(refreshStepper(reseedRunnerChoice(state, refillPicker({
8365
8446
  ...state,
8366
8447
  runners: action.runners,
8367
8448
  runnersKnown: true,
8368
8449
  orchestratorModel: action.orchestratorModel ?? state.orchestratorModel
8369
- }, ["set-task-runner"])));
8450
+ }, ["set-task-runner"]))));
8370
8451
  case "failed": {
8371
8452
  if (!action.turn) return step(say(taskLogAbandoned(state), "error", action.message));
8372
8453
  if (state.forceSendPending) {
@@ -8413,6 +8494,7 @@ function showPlan(state, plan) {
8413
8494
  const expandedTaskId = state.expandedTaskId && findTask(tasks, state.expandedTaskId) ? state.expandedTaskId : null;
8414
8495
  const next = {
8415
8496
  ...folded,
8497
+ plannerResend: plannerResendOf(plan),
8416
8498
  expandedTaskId,
8417
8499
  taskEditor: expandedTaskId !== null ? state.taskEditor : null
8418
8500
  };
@@ -8422,6 +8504,11 @@ function showPlan(state, plan) {
8422
8504
  focus: tasks.length === 0 ? "chat" : state.focus
8423
8505
  });
8424
8506
  }
8507
+ function plannerResendOf(plan) {
8508
+ const saved = plan?.plannerResend;
8509
+ if (typeof saved?.at !== "string") return null;
8510
+ return { ...typeof saved.runner === "string" ? { runner: saved.runner } : {}, at: saved.at };
8511
+ }
8425
8512
  function runStatus(state, tasks) {
8426
8513
  if (tasks.some(import_core28.isTaskRunning)) return "executing";
8427
8514
  if (tasks.some((t) => t.status === "awaiting_user") || state.runMerge?.paused) return "executing";
@@ -9361,6 +9448,12 @@ async function perform(effect, deps) {
9361
9448
  // user gets to read — no separate notice to repeat it. The daemon also
9362
9449
  // broadcast that summary; `redrawn` keeps a late socket copy of it from
9363
9450
  // landing as a second, spoken turn.
9451
+ case "cancelPlannerResend": {
9452
+ const { cancelled } = await api.cancelPlannerResend(effect.sessionId);
9453
+ 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." });
9454
+ await refreshPlan(deps, effect.sessionId);
9455
+ return;
9456
+ }
9364
9457
  case "compactConversation": {
9365
9458
  const { plan } = await api.compactConversation(effect.sessionId);
9366
9459
  const inbound = inboundFor(deps.api, deps.dispatch, effect.sessionId);
@@ -9423,6 +9516,12 @@ async function perform(effect, deps) {
9423
9516
  } catch {
9424
9517
  }
9425
9518
  return;
9519
+ case "loadRunners":
9520
+ await loadRunners(deps);
9521
+ return;
9522
+ case "loadScope":
9523
+ await Promise.all([loadRunners(deps), loadSettings(deps)]);
9524
+ return;
9426
9525
  case "refresh":
9427
9526
  await Promise.all([loadRunners(deps), loadSettings(deps), loadModels(deps)]);
9428
9527
  if (effect.announce) dispatch({ type: "notice", message: "Refreshed runners, settings and models." });
@@ -9986,10 +10085,12 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
9986
10085
  if (terminal) app.dispatch({ type: "resize", ...terminal.size() });
9987
10086
  }
9988
10087
  });
10088
+ const watchingSettings = settings.watch(() => app.dispatch({ type: "sharedSettingsChanged" }));
9989
10089
  const { rows, cols } = terminal.size();
9990
10090
  app.dispatch({ type: "resize", rows, cols });
9991
10091
  app.start();
9992
10092
  function shutdown(code) {
10093
+ watchingSettings.dispose();
9993
10094
  terminal?.close();
9994
10095
  if (owned) stopDaemon(port).catch(() => {
9995
10096
  });
@@ -10059,6 +10160,7 @@ var COMMANDS = {
10059
10160
  fork: handleFork,
10060
10161
  compact: handleCompact,
10061
10162
  rewind: handleRewind,
10163
+ resend: handleResend,
10062
10164
  handoff: handleHandoff,
10063
10165
  tui: handleTui
10064
10166
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ordewell/cli",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "description": "Ordewell CLI and TUI — plan, run and verify coding-agent tasks from the terminal.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Alessandro Costanzo Ciano",
@@ -51,8 +51,8 @@
51
51
  "typecheck": "tsc --noEmit"
52
52
  },
53
53
  "dependencies": {
54
- "@ordewell/core": "^0.10.4",
55
- "@ordewell/web": "^0.10.4",
54
+ "@ordewell/core": "^0.10.5",
55
+ "@ordewell/web": "^0.10.5",
56
56
  "string-width": "^7.2.0",
57
57
  "ws": "^8.18.0"
58
58
  },