@ordewell/cli 0.6.4 → 0.7.1

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 +434 -262
  2. package/package.json +3 -3
package/dist/main.js CHANGED
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/main.ts
27
- var import_core34 = require("@ordewell/core");
27
+ var import_core37 = require("@ordewell/core");
28
28
 
29
29
  // src/utils/env.ts
30
30
  var import_fs = require("fs");
@@ -121,6 +121,7 @@ Tasks (<id> is an order number or a task ID):
121
121
  ordewell run-task <id> Run only one task
122
122
  ordewell retry <id> Re-run a failed task
123
123
  ordewell continue <id> <msg> Continue a finished task in its saved runner session
124
+ ordewell checkpoint <id> approve|reject [reason] Answer the checkpoint a task waits at
124
125
  ordewell cancel <id> Kill a running task
125
126
  ordewell terminal <id> Open a real terminal attached to a task's runner
126
127
 
@@ -139,7 +140,7 @@ Planner, models and runners:
139
140
  ordewell key [set <p> <key>] Show which providers have a key, or store one
140
141
  ordewell runners [<id> on|off] Enable or disable runners (claude-code, opencode, codex)
141
142
  ordewell allowlist set|clear|show Limit which models a runner may use
142
- ordewell auto [full|auto] Autonomy level for new sessions: Full auto or Auto
143
+ ordewell auto [full|guarded] Autonomy level for new sessions: Full or Guarded
143
144
  ordewell transport [terminal|structured] Structured (default) or terminal task transport, from the next run
144
145
  ordewell parallel [<n>] How many AI tasks run at once (default 3; applies to a live run)
145
146
  ordewell refresh Re-discover runners and model catalogs
@@ -160,6 +161,12 @@ TUI options:
160
161
  --workspace /path Workspace directory (default: cwd)
161
162
  --port N Target a daemon on port N
162
163
 
164
+ TUI task view keys (t on a structured task; /help in the TUI lists them all):
165
+ ctrl-s Send now: interrupt the running step and deliver the composer text next
166
+ (with the composer empty, the selected queued message)
167
+ ctrl-n / ctrl-p Select a queued message; ctrl-r removes it
168
+ ctrl-x Interrupt the running turn
169
+
163
170
  Plan options:
164
171
  --goal "text" Task description (required)
165
172
  --runner <id> Runner to use (repeatable; default: all enabled runners)
@@ -213,7 +220,7 @@ Environment:
213
220
  ORDEWELL_PLANNER_EFFORT Thinking effort for a coding-agent planner
214
221
  ORCHESTRATOR_MODEL Default: deepseek/deepseek-v4-flash
215
222
  OPENROUTER_BASE_URL Default: https://openrouter.ai/api/v1
216
- ORDEWELL_AUTONOMOUS_MODE true: Full auto, false: Auto, for new sessions (see "ordewell auto")
223
+ ORDEWELL_AUTONOMOUS_MODE true: Full, false: Guarded, for new sessions (see "ordewell auto")
217
224
  ORDEWELL_PORT Daemon port CLI commands target (default: 3742)
218
225
  ORDEWELL_DIRENV false: agents skip the project's direnv .envrc
219
226
  ORDEWELL_RESEARCH_ENABLED true (default) or false
@@ -610,6 +617,25 @@ var ApiClient = class _ApiClient {
610
617
  }
611
618
  return res.data;
612
619
  }
620
+ /** Force send: interrupt the task's running turn and deliver `text` next, ahead of anything queued (ADR-0023, F1). */
621
+ async forceSendTaskMessage(sessionId, taskId, text) {
622
+ const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages/now`, { text });
623
+ if (res.status !== 200) {
624
+ throw new Error(res.data?.error || "Send now failed");
625
+ }
626
+ return res.data;
627
+ }
628
+ /** Force send a message still queued; `sent` is false once the runner already has it. */
629
+ async forceSendQueuedTaskMessage(sessionId, taskId, messageId) {
630
+ const res = await this.httpRequest(
631
+ "POST",
632
+ `/api/plans/${sessionId}/tasks/${taskId}/messages/${encodeURIComponent(messageId)}/now`
633
+ );
634
+ if (res.status !== 200) {
635
+ throw new Error(res.data?.error || "Send now failed");
636
+ }
637
+ return res.data;
638
+ }
613
639
  /** Continue a finished structured task in its saved session, with `text` as its next turn. */
614
640
  async continueTask(sessionId, taskId, text) {
615
641
  const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/continue`, { text });
@@ -636,6 +662,26 @@ var ApiClient = class _ApiClient {
636
662
  }
637
663
  return res.data;
638
664
  }
665
+ /** Let a task go on from the checkpoint it waits at; refused, with the reason, when none waits. */
666
+ async approveTaskCheckpoint(sessionId, taskId) {
667
+ const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/checkpoint/approve`);
668
+ if (res.status !== 200) {
669
+ throw new Error(res.data?.error || "Approve checkpoint failed");
670
+ }
671
+ return res.data;
672
+ }
673
+ /** Turn a task back at its checkpoint; `reason` is what the agent is told. */
674
+ async rejectTaskCheckpoint(sessionId, taskId, reason) {
675
+ const res = await this.httpRequest(
676
+ "POST",
677
+ `/api/plans/${sessionId}/tasks/${taskId}/checkpoint/reject`,
678
+ reason ? { reason } : {}
679
+ );
680
+ if (res.status !== 200) {
681
+ throw new Error(res.data?.error || "Reject checkpoint failed");
682
+ }
683
+ return res.data;
684
+ }
639
685
  async addTask(sessionId, task) {
640
686
  const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks`, task);
641
687
  if (res.status !== 200) {
@@ -1528,9 +1574,6 @@ function repoResultLines(handoff) {
1528
1574
  return `${path}: ${n === 0 ? "nothing to merge" : `${n} task${n === 1 ? "" : "s"} landed`}`;
1529
1575
  });
1530
1576
  }
1531
- function taskRepoNames(isolation) {
1532
- return (isolation?.repos ?? []).filter((r) => r !== ".");
1533
- }
1534
1577
  function mergeOutcome(result, branch, group, repaired = []) {
1535
1578
  const ok = result.outcome === "merged";
1536
1579
  if (!group && result.outcome === "conflict") {
@@ -1717,7 +1760,7 @@ async function followExecution(api, sessionId, start, onBlocked) {
1717
1760
  `);
1718
1761
  }
1719
1762
  if (event.type === "checkpoint") {
1720
- process.stderr.write(`\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core9.truncateCheckpointSummary)(event.summary)}
1763
+ process.stderr.write(`\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core9.truncateCheckpointSummary)(event.summary)} \u2014 \`ordewell checkpoint <id> approve|reject [reason]\` answers it
1721
1764
  `);
1722
1765
  }
1723
1766
  if (event.type === "status_update" && event.tasks) {
@@ -2345,6 +2388,7 @@ async function handleTransport(subArgs, api) {
2345
2388
  }
2346
2389
 
2347
2390
  // src/commands/task-control.ts
2391
+ var import_core15 = require("@ordewell/core");
2348
2392
  var PAST_TENSE = {
2349
2393
  run: "Task started.",
2350
2394
  "force-start": "Task force-started.",
@@ -2387,7 +2431,7 @@ async function confirmPastGate(subArgs, api, sessionId, taskId, confirm) {
2387
2431
  if (hasFlag(subArgs, "--yes")) return;
2388
2432
  const unmerged = await api.getMergeGate(sessionId, taskId).catch(() => []);
2389
2433
  if (unmerged.length === 0) return;
2390
- const question = `This task waits for Merge all: the work of ${unmerged.map((d) => `#${d.order} ${d.title}`).join(", ")} is not merged into your branch yet, so it would act without it. Starting it now is kept on the task. Start it anyway?`;
2434
+ const question = `${(0, import_core15.pastGateConfirmation)(unmerged.map((d) => (0, import_core15.titledTaskRef)(d)))} Start it anyway?`;
2391
2435
  if (!await confirm(question)) fail("Not started \u2014 nothing was changed. Pass --yes to start it without a prompt.");
2392
2436
  }
2393
2437
  function makeHandler(action, command = action) {
@@ -2427,6 +2471,30 @@ async function handleContinue(subArgs, injectedApi) {
2427
2471
  }
2428
2472
  });
2429
2473
  }
2474
+ async function handleCheckpoint(subArgs, injectedApi) {
2475
+ const usage = "Usage: ordewell checkpoint <task-id-or-order> approve|reject [reason] [--session-id <id>] [--workspace /path]";
2476
+ const [, answer, ...words] = positionals(subArgs);
2477
+ const reason = words.join(" ").trim();
2478
+ if (answer !== "approve" && answer !== "reject") {
2479
+ console.error(usage);
2480
+ process.exit(1);
2481
+ }
2482
+ if (answer === "approve" && reason) {
2483
+ console.error(`Approving takes no note \u2014 reject with a reason to tell the task something.
2484
+ ${usage}`);
2485
+ process.exit(1);
2486
+ }
2487
+ await withResolvedTask(subArgs, usage, injectedApi, async (api, sessionId, taskId) => {
2488
+ try {
2489
+ if (answer === "approve") await api.approveTaskCheckpoint(sessionId, taskId);
2490
+ else await api.rejectTaskCheckpoint(sessionId, taskId, reason || void 0);
2491
+ console.log(answer === "approve" ? "Checkpoint approved." : "Checkpoint rejected.");
2492
+ } catch (err) {
2493
+ console.error(`Failed to ${answer} checkpoint: ${err.message}`);
2494
+ process.exit(1);
2495
+ }
2496
+ });
2497
+ }
2430
2498
  var handleRunTask = makeHandler("run", "run-task");
2431
2499
  var handleForceStart = makeHandler("force-start");
2432
2500
  var handleRetry = makeHandler("retry");
@@ -2676,24 +2744,24 @@ async function handleRewind(subArgs, injectedApi) {
2676
2744
  }
2677
2745
 
2678
2746
  // src/tui/transcript.ts
2679
- var import_core15 = require("@ordewell/core");
2747
+ var import_core16 = require("@ordewell/core");
2680
2748
  function show(state, { view: conversation, gate: turnGate }) {
2681
2749
  const gated = turnGate === state.turnGate ? state : { ...state, turnGate };
2682
2750
  if (conversation === state.conversation) return gated;
2683
2751
  return { ...gated, conversation };
2684
2752
  }
2685
2753
  function fold(state, input) {
2686
- return show(state, { view: (0, import_core15.reduceConversation)(state.conversation, input), gate: state.turnGate });
2754
+ return show(state, { view: (0, import_core16.reduceConversation)(state.conversation, input), gate: state.turnGate });
2687
2755
  }
2688
2756
  function say(state, role, text) {
2689
2757
  const said = fold(state, { type: "local_entry", role, text });
2690
2758
  return said.conversation.nextId > state.conversation.nextId ? { ...said, scroll: 0 } : said;
2691
2759
  }
2692
2760
  function hear(state, message) {
2693
- return show(state, (0, import_core15.followTurn)(state.conversation, state.turnGate, message));
2761
+ return show(state, (0, import_core16.followTurn)(state.conversation, state.turnGate, message));
2694
2762
  }
2695
2763
  function cutTurn(state) {
2696
- return show(state, (0, import_core15.stopTurn)(state.conversation, state.turnGate));
2764
+ return show(state, (0, import_core16.stopTurn)(state.conversation, state.turnGate));
2697
2765
  }
2698
2766
  function wiped(view) {
2699
2767
  return { ...view, blocks: view.blocks.filter((b) => b.type === "usage") };
@@ -2919,12 +2987,12 @@ function runHandoffAction(state, id) {
2919
2987
  overlay: isRepoGroup(handoff) ? {
2920
2988
  kind: "confirm",
2921
2989
  title: "Merge all into your branches?",
2922
- message: `Merge ${handoffBranch(handoff)} into whatever each of ${reposWithWork(handoff).join(", ")} has checked out. Ordewell never does this on its own. It checks every repository first and merges none unless every repository can take it; on git older than 2.38 it goes repository by repository and stops at the first that fails. A merge that conflicts is aborted, and your trees stay as they were. Once every repository has merged, the run's worktrees and branches are removed.${repairedNotice(handoff)}`,
2990
+ message: `Merge ${handoffBranch(handoff)} into whatever each of ${reposWithWork(handoff).join(", ")} has checked out. Ordewell never does this on its own. It checks every repository first and merges none unless every repository can take it; on git older than 2.38 it goes repository by repository and stops at the first that fails. A merge that conflicts is aborted, and your trees stay as they were. Once every repository has merged, the run's worktrees and branches are removed \u2014 unless a task still has work in its worktree that has not landed: then the run stays, and so does that work.${repairedNotice(handoff)}`,
2923
2991
  action: { kind: "merge-run" }
2924
2992
  } : {
2925
2993
  kind: "confirm",
2926
2994
  title: "Merge into your branch?",
2927
- message: `Merge ${handoffBranch(handoff)} into whatever you have checked out. Ordewell never does this on its own. If it conflicts the merge is aborted and your tree stays as it was. Once it has merged, the run's worktrees and branches are removed.${repairedNotice(handoff)}`,
2995
+ message: `Merge ${handoffBranch(handoff)} into whatever you have checked out. Ordewell never does this on its own. If it conflicts the merge is aborted and your tree stays as it was. Once it has merged, the run's worktrees and branches are removed \u2014 unless a task still has work in its worktree that has not landed: then the run stays, and so does that work.${repairedNotice(handoff)}`,
2928
2996
  action: { kind: "merge-run" }
2929
2997
  }
2930
2998
  },
@@ -3107,7 +3175,7 @@ async function handleHandoff(subArgs, injectedApi, confirm = askYesNo) {
3107
3175
  }
3108
3176
 
3109
3177
  // src/commands/planner.ts
3110
- var import_core16 = require("@ordewell/core");
3178
+ var import_core17 = require("@ordewell/core");
3111
3179
 
3112
3180
  // src/plannerModelSwitch.ts
3113
3181
  function describePlannerSwitch(settings, provider) {
@@ -3126,7 +3194,7 @@ function rememberedModelFor(settings, provider) {
3126
3194
  }
3127
3195
 
3128
3196
  // src/commands/planner.ts
3129
- var KNOWN_PROVIDERS = Object.keys(import_core16.ALL_PROVIDERS);
3197
+ var KNOWN_PROVIDERS = Object.keys(import_core17.ALL_PROVIDERS);
3130
3198
  async function handlePlanner(subArgs, injectedApi) {
3131
3199
  const api = await connect(subArgs, injectedApi);
3132
3200
  const [wanted] = positionals(subArgs);
@@ -3141,13 +3209,13 @@ async function handlePlanner(subArgs, injectedApi) {
3141
3209
  const settings = await persistEnv(api, { AI_PROVIDER: provider });
3142
3210
  const recall2 = describePlannerSwitch(settings, provider);
3143
3211
  writeEnv({ ORCHESTRATOR_MODEL: recall2.model, ORDEWELL_PLANNER_EFFORT: recall2.effort });
3144
- const meta = import_core16.ALL_PROVIDERS[provider];
3212
+ const meta = import_core17.ALL_PROVIDERS[provider];
3145
3213
  const catalog = await fetchCatalog(api);
3146
3214
  console.log(
3147
- (0, import_core16.isCliProvider)(provider) ? `Planning with ${meta.label} \u2014 no API key needed, it uses that agent's own subscription.` : `Planner set to ${meta.label}.`
3215
+ (0, import_core17.isCliProvider)(provider) ? `Planning with ${meta.label} \u2014 no API key needed, it uses that agent's own subscription.` : `Planner set to ${meta.label}.`
3148
3216
  );
3149
3217
  console.log(plannerModelLine(recall2));
3150
- if (!(0, import_core16.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
3218
+ if (!(0, import_core17.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
3151
3219
  console.log(` No ${meta.apiKeyEnvVar} configured yet \u2014 set one with \`ordewell key set ${provider} <key>\`.`);
3152
3220
  }
3153
3221
  }
@@ -3165,15 +3233,15 @@ async function showPlanners(api) {
3165
3233
  const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
3166
3234
  const current = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
3167
3235
  console.log("\nCoding agents (no API key \u2014 they use their own subscription)");
3168
- for (const id of import_core16.CLI_PROVIDERS) {
3169
- const runner = (0, import_core16.runnerForProvider)(id);
3236
+ for (const id of import_core17.CLI_PROVIDERS) {
3237
+ const runner = (0, import_core17.runnerForProvider)(id);
3170
3238
  const discovered = runner ? catalog.models.some((m) => m.runners?.includes(runner)) : false;
3171
- console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core16.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
3239
+ console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core17.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
3172
3240
  }
3173
3241
  console.log("\nAPI providers");
3174
- for (const id of import_core16.PROVIDER_PRIORITY) {
3242
+ for (const id of import_core17.PROVIDER_PRIORITY) {
3175
3243
  const configured = catalog.providers.includes(id);
3176
- const meta = import_core16.ALL_PROVIDERS[id];
3244
+ const meta = import_core17.ALL_PROVIDERS[id];
3177
3245
  console.log(` ${marker(id === current)} ${id.padEnd(14)} ${meta.label}${configured ? "" : ` (no ${meta.apiKeyEnvVar})`}`);
3178
3246
  }
3179
3247
  console.log(`
@@ -3190,7 +3258,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
3190
3258
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
3191
3259
  const modelId = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
3192
3260
  const currentEffort = typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : "";
3193
- const runner = (0, import_core16.runnerForProvider)(provider);
3261
+ const runner = (0, import_core17.runnerForProvider)(provider);
3194
3262
  if (!runner) {
3195
3263
  fail(
3196
3264
  "Thinking effort applies to a coding-agent planner.",
@@ -3230,7 +3298,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
3230
3298
  }
3231
3299
 
3232
3300
  // src/commands/parallel.ts
3233
- var import_core17 = require("@ordewell/core");
3301
+ var import_core18 = require("@ordewell/core");
3234
3302
  var USAGE5 = "Usage: ordewell parallel [<number of tasks, 1 or more>]";
3235
3303
  async function handleParallel(subArgs, injectedApi) {
3236
3304
  const [wanted] = positionals(subArgs);
@@ -3240,14 +3308,14 @@ async function handleParallel(subArgs, injectedApi) {
3240
3308
  console.log(`Up to ${String(settings.maxParallel ?? "?")} AI tasks run at once. Change with: ordewell parallel <n>`);
3241
3309
  return;
3242
3310
  }
3243
- const limit = (0, import_core17.parseMaxParallel)(wanted);
3311
+ const limit = (0, import_core18.parseMaxParallel)(wanted);
3244
3312
  if (limit === null) fail(`"${wanted}" is not a number of tasks (1 or more).`, USAGE5);
3245
3313
  await persistEnv(api, { ORDEWELL_MAX_PARALLEL: String(limit) });
3246
3314
  console.log(`Up to ${limit} AI task${limit === 1 ? "" : "s"} now run at once.`);
3247
3315
  }
3248
3316
 
3249
3317
  // src/commands/key.ts
3250
- var import_core18 = require("@ordewell/core");
3318
+ var import_core19 = require("@ordewell/core");
3251
3319
  var USAGE6 = "Usage: ordewell key [list | set <provider> <key>]";
3252
3320
  async function handleKey(subArgs, injectedApi) {
3253
3321
  const [action, provider, ...keyParts] = positionals(subArgs);
@@ -3258,8 +3326,8 @@ async function handleKey(subArgs, injectedApi) {
3258
3326
  if (action !== "set") {
3259
3327
  const catalog = await fetchCatalog(api);
3260
3328
  console.log("\nAPI provider keys\n");
3261
- for (const id2 of import_core18.PROVIDER_PRIORITY) {
3262
- const meta2 = import_core18.ALL_PROVIDERS[id2];
3329
+ for (const id2 of import_core19.PROVIDER_PRIORITY) {
3330
+ const meta2 = import_core19.ALL_PROVIDERS[id2];
3263
3331
  const configured = catalog.providers.includes(id2);
3264
3332
  console.log(` ${configured ? "\u2713" : " "} ${id2.padEnd(14)} ${meta2.apiKeyEnvVar.padEnd(28)} ${meta2.label}`);
3265
3333
  }
@@ -3269,7 +3337,7 @@ async function handleKey(subArgs, injectedApi) {
3269
3337
  }
3270
3338
  if (!provider) fail(USAGE6);
3271
3339
  const id = provider.toLowerCase();
3272
- const meta = import_core18.ALL_PROVIDERS[id];
3340
+ const meta = import_core19.ALL_PROVIDERS[id];
3273
3341
  if (!meta) {
3274
3342
  fail(`Unknown provider: ${provider}`, "Run `ordewell key` to see the list.");
3275
3343
  }
@@ -3280,7 +3348,7 @@ async function handleKey(subArgs, injectedApi) {
3280
3348
  }
3281
3349
 
3282
3350
  // src/commands/runners.ts
3283
- var import_core19 = require("@ordewell/core");
3351
+ var import_core20 = require("@ordewell/core");
3284
3352
  var USAGE7 = "Usage: ordewell runners [<runner-id> on|off]";
3285
3353
  async function handleRunners(subArgs, injectedApi) {
3286
3354
  const [runner, toggle] = positionals(subArgs);
@@ -3305,21 +3373,21 @@ async function handleRunners(subArgs, injectedApi) {
3305
3373
  await api.setRunnerEnabled(runner, enabled);
3306
3374
  console.log(`${runner} ${enabled ? "enabled" : "disabled"}.`);
3307
3375
  }
3308
- var AUTO_USAGE = "Usage: ordewell auto [full|auto]";
3376
+ var AUTO_USAGE = "Usage: ordewell auto [full|guarded]";
3309
3377
  function handleAuto(subArgs) {
3310
3378
  const [arg] = positionals(subArgs);
3311
3379
  const currentRaw = process.env.ORDEWELL_AUTONOMOUS_MODE;
3312
3380
  const current = currentRaw !== "false" && currentRaw !== "0";
3313
3381
  if (!arg) {
3314
- console.log(`Autonomy level: ${(0, import_core19.autonomyLevelLabel)(current)}`);
3382
+ console.log(`Autonomy level: ${(0, import_core20.autonomyLevelLabel)(current)}`);
3315
3383
  console.log(` ${AUTO_USAGE}`);
3316
3384
  return;
3317
3385
  }
3318
- const enabled = (0, import_core19.parseAutonomyLevel)(arg);
3386
+ const enabled = (0, import_core20.parseAutonomyLevel)(arg);
3319
3387
  if (enabled === null) fail(AUTO_USAGE);
3320
3388
  writeEnvVar(findEnvFile(), "ORDEWELL_AUTONOMOUS_MODE", String(enabled));
3321
3389
  process.env.ORDEWELL_AUTONOMOUS_MODE = String(enabled);
3322
- console.log(`Autonomy level: ${(0, import_core19.autonomyLevelLabel)(enabled)} for new sessions.`);
3390
+ console.log(`Autonomy level: ${(0, import_core20.autonomyLevelLabel)(enabled)} for new sessions.`);
3323
3391
  }
3324
3392
  async function handleRefresh(subArgs, injectedApi) {
3325
3393
  const api = await connect(subArgs, injectedApi);
@@ -3351,7 +3419,7 @@ async function handleApprove(subArgs, injectedApi) {
3351
3419
  // src/tui/terminalLauncher.ts
3352
3420
  var import_child_process3 = require("child_process");
3353
3421
  var import_util = require("util");
3354
- var import_core20 = require("@ordewell/core");
3422
+ var import_core21 = require("@ordewell/core");
3355
3423
  var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
3356
3424
  var defaultExecFile = async (command, args) => {
3357
3425
  const { stdout, stderr } = await execFileAsync(command, args);
@@ -3371,16 +3439,16 @@ var defaultWhich = (bin) => {
3371
3439
  async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
3372
3440
  const platform = deps.platform ?? process.platform;
3373
3441
  const env = deps.env ?? process.env;
3374
- const hasTmuxImpl = deps.hasTmuxImpl ?? import_core20.hasTmux;
3442
+ const hasTmuxImpl = deps.hasTmuxImpl ?? import_core21.hasTmux;
3375
3443
  const execFileImpl = deps.execFileImpl ?? defaultExecFile;
3376
3444
  const spawnImpl = deps.spawnImpl ?? defaultSpawn;
3377
3445
  const which = deps.which ?? defaultWhich;
3378
3446
  if (!hasTmuxImpl()) {
3379
3447
  return { ok: false, message: "No terminal window for this task: tmux is not installed. Install tmux to get one for terminal-transport tasks; a structured task has its log view instead." };
3380
3448
  }
3381
- const session = (0, import_core20.tmuxSessionName)(port);
3382
- const socket = (0, import_core20.tmuxSocketName)(port);
3383
- const base = (0, import_core20.tmuxWindowName)(taskId, planSessionId);
3449
+ const session = (0, import_core21.tmuxSessionName)(port);
3450
+ const socket = (0, import_core21.tmuxSocketName)(port);
3451
+ const base = (0, import_core21.tmuxWindowName)(taskId, planSessionId);
3384
3452
  let windows;
3385
3453
  try {
3386
3454
  const { stdout } = await execFileImpl("tmux", ["-L", socket, "list-windows", "-t", session, "-F", "#{window_name}"]);
@@ -3529,7 +3597,7 @@ async function handleTaskRunner(subArgs, injectedApi) {
3529
3597
  const state = await api.getRunners();
3530
3598
  if (!runner) {
3531
3599
  console.log(`
3532
- Runner \xB7 #${task.order} ${task.title}
3600
+ Runner \xB7 ${(0, import_plan_utils.titledTaskRef)(task)}
3533
3601
  `);
3534
3602
  for (const r of state.runners) {
3535
3603
  console.log(` ${r.id === task.assignedRunner ? "*" : " "} ${r.id.padEnd(14)} ${r.name}${r.enabled ? "" : " (not enabled for planning)"}`);
@@ -3542,7 +3610,7 @@ Runner \xB7 #${task.order} ${task.title}
3542
3610
  fail(`Unknown runner: ${runner}`, `This daemon knows: ${state.runners.map((r) => r.id).join(", ")}`);
3543
3611
  }
3544
3612
  await api.updateTask(sessionId, task.id, { assignedRunner: runner });
3545
- console.log(`Task #${task.order} runner set to ${runner} \u2014 its model, effort and mode were re-picked for it.`);
3613
+ console.log(`Task ${(0, import_plan_utils.taskRef)(task)} runner set to ${runner} \u2014 its model, effort and mode were re-picked for it.`);
3546
3614
  });
3547
3615
  }
3548
3616
  var MODEL_USAGE = "Usage: ordewell task-model <task-id-or-order> [<model-id>] [--session-id <id>]";
@@ -3552,7 +3620,7 @@ async function handleTaskModel(subArgs, injectedApi) {
3552
3620
  const options = modelsForTask(catalog.models, task);
3553
3621
  if (!modelId) {
3554
3622
  console.log(`
3555
- Model \xB7 #${task.order} ${task.title}${task.assignedRunner ? ` (${task.assignedRunner})` : ""}
3623
+ Model \xB7 ${(0, import_plan_utils.titledTaskRef)(task)}${task.assignedRunner ? ` (${task.assignedRunner})` : ""}
3556
3624
  `);
3557
3625
  if (options.length === 0) console.log(" No models discovered for this runner \u2014 try `ordewell refresh`.");
3558
3626
  for (const m of options) {
@@ -3575,7 +3643,7 @@ Model \xB7 #${task.order} ${task.title}${task.assignedRunner ? ` (${task.assigne
3575
3643
  assignedModel,
3576
3644
  thinkingEffort: assignedModel.thinkingEffort ?? null
3577
3645
  });
3578
- console.log(`Task #${task.order} model set to ${model.label}.`);
3646
+ console.log(`Task ${(0, import_plan_utils.taskRef)(task)} model set to ${model.label}.`);
3579
3647
  if (task.assignedModel?.thinkingEffort && !assignedModel.thinkingEffort) {
3580
3648
  console.log(` ${model.label} does not expose "${task.assignedModel.thinkingEffort}", so the effort went back to the runner default.`);
3581
3649
  }
@@ -3589,7 +3657,7 @@ async function handleTaskEffort(subArgs, injectedApi) {
3589
3657
  const current = task.assignedModel?.thinkingEffort;
3590
3658
  if (!level) {
3591
3659
  console.log(`
3592
- Thinking effort \xB7 #${task.order} ${task.title}
3660
+ Thinking effort \xB7 ${(0, import_plan_utils.titledTaskRef)(task)}
3593
3661
  `);
3594
3662
  console.log(` ${current ? " " : "*"} default Let the executor choose`);
3595
3663
  for (const v of variants) {
@@ -3614,7 +3682,7 @@ Thinking effort \xB7 #${task.order} ${task.title}
3614
3682
  assignedModel: task.assignedModel ? { ...task.assignedModel, thinkingEffort } : void 0,
3615
3683
  thinkingEffort: thinkingEffort ?? null
3616
3684
  });
3617
- console.log(`Task #${task.order} thinking effort set to ${thinkingEffort ?? "runner default"}.`);
3685
+ console.log(`Task ${(0, import_plan_utils.taskRef)(task)} thinking effort set to ${thinkingEffort ?? "runner default"}.`);
3618
3686
  });
3619
3687
  }
3620
3688
  var MODE_USAGE = "Usage: ordewell task-mode <task-id-or-order> [<mode>] [--session-id <id>]";
@@ -3624,7 +3692,7 @@ async function handleTaskMode(subArgs, injectedApi) {
3624
3692
  const modes = modesForTask(catalog.modesByRunner, task);
3625
3693
  if (!mode) {
3626
3694
  console.log(`
3627
- Mode \xB7 #${task.order} ${task.title}${task.assignedRunner ? ` (${task.assignedRunner})` : ""}
3695
+ Mode \xB7 ${(0, import_plan_utils.titledTaskRef)(task)}${task.assignedRunner ? ` (${task.assignedRunner})` : ""}
3628
3696
  `);
3629
3697
  if (modes.length === 0) {
3630
3698
  console.log(" This task has no runner, or its runner declares no modes.");
@@ -3643,7 +3711,7 @@ Mode \xB7 #${task.order} ${task.title}${task.assignedRunner ? ` (${task.assigned
3643
3711
  );
3644
3712
  }
3645
3713
  await api.updateTask(sessionId, task.id, { taskMode: chosen.id });
3646
- console.log(`Task #${task.order} mode set to ${chosen.label}.`);
3714
+ console.log(`Task ${(0, import_plan_utils.taskRef)(task)} mode set to ${chosen.label}.`);
3647
3715
  });
3648
3716
  }
3649
3717
  var DEPS_USAGE = "Usage: ordewell task-deps <task-id-or-order> [<id,id,\u2026>|none] [--session-id <id>]";
@@ -3652,14 +3720,14 @@ async function handleTaskDeps(subArgs, injectedApi) {
3652
3720
  const candidates = (0, import_plan_utils.dependencyCandidates)(tasks, task.id);
3653
3721
  if (!value) {
3654
3722
  console.log(`
3655
- Dependencies \xB7 #${task.order} ${task.title}
3723
+ Dependencies \xB7 ${(0, import_plan_utils.titledTaskRef)(task)}
3656
3724
  `);
3657
3725
  if (candidates.length === 0) {
3658
- console.log(` Nothing runs before #${task.order}, so it has no possible dependencies.`);
3726
+ console.log(` Nothing runs before ${(0, import_plan_utils.taskRef)(task)}, so it has no possible dependencies.`);
3659
3727
  }
3660
3728
  for (const c of candidates) {
3661
3729
  const chosen = task.dependencies.includes(c.id);
3662
- console.log(` ${chosen ? "*" : " "} ${c.id} #${c.order} ${c.title}${c.status === "completed" ? " (already completed)" : ""}`);
3730
+ console.log(` ${chosen ? "*" : " "} ${c.id} ${(0, import_plan_utils.titledTaskRef)(c)}${c.status === "completed" ? " (already completed)" : ""}`);
3663
3731
  }
3664
3732
  console.log(`
3665
3733
  * = current. ${DEPS_USAGE}`);
@@ -3669,7 +3737,7 @@ Dependencies \xB7 #${task.order} ${task.title}
3669
3737
  const check = (0, import_plan_utils.canSetDependencies)(tasks, task.id, dependencies);
3670
3738
  if (!check.ok) fail(check.error ?? "Invalid dependencies.");
3671
3739
  await api.updateTask(sessionId, task.id, { dependencies });
3672
- console.log(dependencies.length > 0 ? `Task #${task.order} now depends on ${dependencies.length} task${dependencies.length === 1 ? "" : "s"}.` : `Task #${task.order} no longer depends on anything.`);
3740
+ console.log(dependencies.length > 0 ? `Task ${(0, import_plan_utils.taskRef)(task)} now depends on ${dependencies.length} task${dependencies.length === 1 ? "" : "s"}.` : `Task ${(0, import_plan_utils.taskRef)(task)} no longer depends on anything.`);
3673
3741
  });
3674
3742
  }
3675
3743
  var OPS_USAGE = "Usage: ordewell task-ops <task-id-or-order> [on|off] [--session-id <id>]";
@@ -3677,7 +3745,7 @@ async function handleTaskOps(subArgs, injectedApi) {
3677
3745
  await withTask(subArgs, OPS_USAGE, injectedApi, async (api, sessionId, task, tasks, value) => {
3678
3746
  if (!value) {
3679
3747
  console.log(`
3680
- #${task.order} ${task.title} is ${task.ops ? "an ops task: it runs in your checkout once the work it depends on is merged" : "a change task: it runs in its own worktree"}.`);
3748
+ ${(0, import_plan_utils.titledTaskRef)(task)} is ${task.ops ? "an ops task: it runs in your checkout once the work it depends on is merged" : "a change task: it runs in its own worktree"}.`);
3681
3749
  console.log(`
3682
3750
  ${OPS_USAGE}`);
3683
3751
  return;
@@ -3687,12 +3755,12 @@ async function handleTaskOps(subArgs, injectedApi) {
3687
3755
  if (task.type !== "ai") fail("Only an AI task can be an ops task \u2014 a manual task already runs outside any worktree.");
3688
3756
  if (!tasks.some((t) => t.id === task.id)) fail("A subtask runs with its parent; make the parent an ops task instead.");
3689
3757
  await api.updateTask(sessionId, task.id, { ops: to === "on" });
3690
- console.log(to === "on" ? `Task #${task.order} is an ops task: it runs in your checkout once the work it depends on is merged.` : `Task #${task.order} is a change task: it runs in its own worktree.`);
3758
+ console.log(to === "on" ? `Task ${(0, import_plan_utils.taskRef)(task)} is an ops task: it runs in your checkout once the work it depends on is merged.` : `Task ${(0, import_plan_utils.taskRef)(task)} is a change task: it runs in its own worktree.`);
3691
3759
  });
3692
3760
  }
3693
3761
 
3694
3762
  // src/tui/index.ts
3695
- var import_core33 = require("@ordewell/core");
3763
+ var import_core36 = require("@ordewell/core");
3696
3764
 
3697
3765
  // src/tui/editor.ts
3698
3766
  function emptyEditor() {
@@ -3841,6 +3909,7 @@ var SLASH_COMMANDS = [
3841
3909
  { name: "cancel", usage: "/cancel <id>", description: "Kill a running task", category: "tasks" },
3842
3910
  { name: "force-start", usage: "/force-start <id>", description: "Start a task now, ignoring dependencies", category: "tasks" },
3843
3911
  { name: "continue", usage: "/continue <id> <message>", description: "Continue a finished task in its saved runner session, with your message", category: "tasks" },
3912
+ { 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" },
3844
3913
  { name: "terminal", usage: "/terminal <id>", description: "Open a task: its OS terminal, or a structured task's live log", category: "tasks" },
3845
3914
  { name: "task-runner", usage: "/task-runner <id> [runner]", description: "Choose or set a task's executor (re-picks its model and mode)", category: "tasks" },
3846
3915
  { name: "task-model", usage: "/task-model <id> [model]", description: "Choose or set a task's executor model", category: "tasks" },
@@ -3855,7 +3924,7 @@ var SLASH_COMMANDS = [
3855
3924
  { name: "key", usage: "/key [set <provider> <key>]", description: "Configure API provider keys", category: "models" },
3856
3925
  { name: "allowlist", usage: "/allowlist [set <runner> <ids> | clear <runner>]", description: "Limit which models a runner may use", category: "models" },
3857
3926
  { name: "runners", usage: "/runners [<id> on|off]", description: "Enable or disable runners (claude-code, opencode, codex)", category: "models" },
3858
- { name: "auto", usage: "/auto [full|auto]", description: "Autonomy level for new plans: Full auto or Auto", category: "models" },
3927
+ { name: "auto", usage: "/auto [full|guarded]", description: "Autonomy level for new plans: Full or Guarded", category: "models" },
3859
3928
  { name: "transport", usage: "/transport [terminal|structured]", description: "Drive tasks through each runner's protocol (structured) or a terminal, from the next run", category: "models" },
3860
3929
  { name: "parallel", usage: "/parallel [<n>]", description: "How many AI tasks run at once (applies to a live run)", category: "models" },
3861
3930
  { name: "refresh", usage: "/refresh", description: "Re-discover runners and model catalogs", category: "models" },
@@ -3941,7 +4010,7 @@ function tokenCompletions(token) {
3941
4010
  }
3942
4011
 
3943
4012
  // src/tui/blocks.ts
3944
- var import_core21 = require("@ordewell/core");
4013
+ var import_core22 = require("@ordewell/core");
3945
4014
 
3946
4015
  // src/tui/markdown.ts
3947
4016
  function renderMarkdown(source, cols) {
@@ -4119,6 +4188,17 @@ function commandMark(block) {
4119
4188
  if (block.status === "pending") return style.grey("\u25CB");
4120
4189
  return (unsettledOutcome(block)?.paint ?? style.green)("\u25CF");
4121
4190
  }
4191
+ var PATH_LIKE = /^(?![a-z][a-z0-9+.-]*:\/\/)\S*\/\S*$/i;
4192
+ function truncatePath(path, max) {
4193
+ if (width(path) <= max) return path;
4194
+ let tail = "";
4195
+ for (const char of Array.from(path).reverse()) {
4196
+ if (width(char + tail) > max - 1) break;
4197
+ tail = char + tail;
4198
+ }
4199
+ const slash = tail.indexOf("/");
4200
+ return `\u2026${slash > 0 ? tail.slice(slash) : tail}`;
4201
+ }
4122
4202
  function commandHeader(block, cols, detailAll) {
4123
4203
  const name = sanitize(block.headline.name);
4124
4204
  const keyArg = sanitize(block.headline.keyArg);
@@ -4127,7 +4207,8 @@ function commandHeader(block, cols, detailAll) {
4127
4207
  }
4128
4208
  const open = `\u25CF ${name}(`;
4129
4209
  const room = cols - width(open) - 1;
4130
- const text = room > 1 ? `${open}${truncate(keyArg, room)})` : truncate(`${open}${keyArg})`, cols);
4210
+ const cut = PATH_LIKE.test(keyArg) ? truncatePath(keyArg, room) : truncate(keyArg, room);
4211
+ const text = room > 1 ? `${open}${cut})` : truncate(`${open}${keyArg})`, cols);
4131
4212
  return [`${commandMark(block)}${text.slice(1)}`];
4132
4213
  }
4133
4214
  function argumentLines(args) {
@@ -4149,24 +4230,46 @@ function resultLines(block, room, detailAll) {
4149
4230
  const fit2 = (line) => detailAll ? wrap(line, room) : [truncate(line, room)];
4150
4231
  const outcome = unsettledOutcome(block);
4151
4232
  if (outcome) {
4152
- const [reason, ...rest] = (0, import_core21.outputLines)(block.output).map(sanitize);
4233
+ const [reason, ...rest] = (0, import_core22.outputLines)(block.output).map(sanitize);
4153
4234
  const status = fit2(reason ? `${outcome.label}: ${reason}` : outcome.label).map(outcome.paint);
4154
4235
  return detailAll ? [...status, ...rest.flatMap(fit2).map(style.grey)] : status;
4155
4236
  }
4156
4237
  if (detailAll) {
4157
- const lines2 = (0, import_core21.outputLines)(block.output).map(sanitize);
4238
+ const lines2 = (0, import_core22.outputLines)(block.output).map(sanitize);
4158
4239
  const note = lines2.length > PREVIEW_LINES ? ["\u2026 (ctrl+o to collapse)"] : [];
4159
4240
  return lines2.length > 0 ? [...lines2.flatMap(fit2), ...note].map(style.grey) : [style.grey("(no output)")];
4160
4241
  }
4161
- const { lines, hiddenLineCount } = (0, import_core21.outputPreview)(block.output, PREVIEW_LINES);
4242
+ const { lines, hiddenLineCount } = (0, import_core22.outputPreview)(block.output, PREVIEW_LINES);
4162
4243
  if (lines.length === 0) return [style.grey("(no output)")];
4163
4244
  const more = hiddenLineCount > 0 ? [`\u2026 +${countOf(hiddenLineCount, "line")} (ctrl+o to expand)`] : [];
4164
4245
  return [...lines.map(sanitize), ...more].map((line) => style.grey(truncate(line, room)));
4165
4246
  }
4247
+ var DIFF_PREVIEW_ROWS = 10;
4248
+ var DIFF_PAINT = {
4249
+ added: style.green,
4250
+ removed: style.red,
4251
+ context: style.grey,
4252
+ gap: style.grey
4253
+ };
4254
+ var DIFF_SIGN = { added: "+", removed: "-", context: " ", gap: "" };
4255
+ function diffLines(block, diff, room, detailAll) {
4256
+ const rows = (0, import_core22.diffRows)(block.output);
4257
+ const gutter = Math.max(0, ...rows.map((row) => String(row.line ?? "").length));
4258
+ const draw = (row) => {
4259
+ const text = row.kind === "gap" ? `${" ".repeat(gutter)} \u22EE` : `${String(row.line ?? "").padStart(gutter)} ${DIFF_SIGN[row.kind]} ${sanitize(row.text)}`;
4260
+ return (detailAll ? wrap(text, room) : [truncate(text, room)]).map(DIFF_PAINT[row.kind]);
4261
+ };
4262
+ const shown = detailAll ? rows : rows.slice(0, DIFF_PREVIEW_ROWS);
4263
+ const hidden = rows.length - shown.length;
4264
+ const more = hidden > 0 ? [style.grey(`\u2026 +${countOf(hidden, "line")} (ctrl+o to expand)`)] : [];
4265
+ return [(0, import_core22.diffSummary)(diff), ...shown.flatMap(draw), ...more];
4266
+ }
4166
4267
  function toolLines(block, cols, detailAll) {
4167
4268
  const room = Math.max(1, cols - OUTPUT_INDENT.length);
4168
- const args = detailAll ? argumentLines(block.args).flatMap((line) => wrap(line, room)).map((line) => OUTPUT_INDENT + style.grey(line)) : [];
4169
- return [...commandHeader(block, cols, detailAll), ...args, ...underHeader(resultLines(block, room, detailAll))];
4269
+ const diff = block.diff && block.status === "ok" ? block.diff : null;
4270
+ const args = detailAll && !diff ? argumentLines(block.args).flatMap((line) => wrap(line, room)).map((line) => OUTPUT_INDENT + style.grey(line)) : [];
4271
+ const result = diff ? diffLines(block, diff, room, detailAll) : resultLines(block, room, detailAll);
4272
+ return [...commandHeader(block, cols, detailAll), ...args, ...underHeader(result)];
4170
4273
  }
4171
4274
  var SUBAGENT_PAINT = {
4172
4275
  running: style.yellow,
@@ -4215,6 +4318,19 @@ function approvalLine(block, cols) {
4215
4318
  const line = `${mark} ${status} \xB7 ${APPROVAL_KIND[block.kind]}: ${firstLine(block.subject)}${note}`;
4216
4319
  return `${paint(mark)}${truncate(line, cols).slice(mark.length)}`;
4217
4320
  }
4321
+ function approvalKeysLine(block, cols) {
4322
+ const keys = ["ctrl-y allow", ...block.allowForTask ? ["ctrl-t allow for task"] : [], "ctrl-g deny (composer text is the note)"];
4323
+ return style.grey(truncate(` ${keys.join(" \xB7 ")}`, cols));
4324
+ }
4325
+ function checkpointCardLines(order, question, cols, approvalFirst) {
4326
+ const room = Math.max(1, cols - 2);
4327
+ const keys = approvalFirst ? "answer the tool request above first (ctrl-y / ctrl-g), then ctrl-y approves \xB7 ctrl-g rejects (composer text is the reason)" : "ctrl-y approve \xB7 ctrl-g reject (composer text is the reason)";
4328
+ return [
4329
+ truncate(`${style.accent("\u25C6")} ${style.bold(`Task ${order} asks`)}`, cols),
4330
+ ...wrap(sanitize(question).trim(), room).map((line) => ` ${line}`),
4331
+ ...wrap(keys, room).map((line) => style.grey(` ${line}`))
4332
+ ];
4333
+ }
4218
4334
  function planLine(block, cols) {
4219
4335
  const count = block.taskCount === void 0 ? "" : ` (${countOf(block.taskCount, "task")})`;
4220
4336
  const text = block.status === "building" ? "Building plan\u2026" : `Plan ${block.status}${count}`;
@@ -4247,11 +4363,13 @@ function drawn(block, cols, detailAll) {
4247
4363
  drawnBlocks.set(block, { cols, detailAll, lines });
4248
4364
  return lines;
4249
4365
  }
4250
- function conversationLines(blocks, cols, detailAll) {
4366
+ function conversationLines(blocks, cols, detailAll, answering) {
4251
4367
  const lines = [];
4252
4368
  for (const block of blocks) {
4253
4369
  if (block.type === "usage") continue;
4254
- lines.push(...drawn(block, cols, detailAll), "");
4370
+ lines.push(...drawn(block, cols, detailAll));
4371
+ if (block === answering) lines.push(approvalKeysLine(block, cols));
4372
+ lines.push("");
4255
4373
  }
4256
4374
  return lines;
4257
4375
  }
@@ -4279,7 +4397,7 @@ function tokenLine(blocks, cols) {
4279
4397
  }
4280
4398
 
4281
4399
  // src/tui/state.ts
4282
- var import_core22 = require("@ordewell/core");
4400
+ var import_core23 = require("@ordewell/core");
4283
4401
  var SKILL_IDS = ["tdd", "verify"];
4284
4402
  function noSkills() {
4285
4403
  return { tdd: false, verify: false };
@@ -4291,8 +4409,7 @@ function visibleItems(picker2) {
4291
4409
  (i) => i.id.toLowerCase().includes(filter) || i.label.toLowerCase().includes(filter) || (i.detail ?? "").toLowerCase().includes(filter)
4292
4410
  );
4293
4411
  }
4294
- var isTaskRunning = (task) => task.status === "in_progress" || task.status === "running";
4295
- var anyTaskRunning = (state) => state.tasks.some(isTaskRunning);
4412
+ var anyTaskRunning = (state) => state.tasks.some(import_core23.isTaskRunning);
4296
4413
  var plannerInFlight = (state) => state.status === "planning" || state.status === "researching";
4297
4414
  function planRows(state) {
4298
4415
  const rows = [];
@@ -4312,6 +4429,9 @@ function selectedPlanRow(state) {
4312
4429
  function waitingApproval(tv) {
4313
4430
  return tv.view.blocks.find((b) => b.type === "approval" && b.status === "pending" && b.approvalId !== void 0);
4314
4431
  }
4432
+ function waitingCheckpoint(task) {
4433
+ return task?.status === "awaiting_user" && task.awaitingReason === "checkpoint" ? task.checkpoint : void 0;
4434
+ }
4315
4435
  function continuesTask(task) {
4316
4436
  return task?.continuable === true && (task.status === "completed" || task.status === "failed");
4317
4437
  }
@@ -4326,9 +4446,9 @@ function findTask(tasks, id) {
4326
4446
  function initialState(overrides = {}) {
4327
4447
  return {
4328
4448
  editor: emptyEditor(),
4329
- conversation: import_core22.EMPTY_CONVERSATION,
4449
+ conversation: import_core23.EMPTY_CONVERSATION,
4330
4450
  taskView: null,
4331
- turnGate: import_core22.NO_TURN,
4451
+ turnGate: import_core23.NO_TURN,
4332
4452
  detailAll: false,
4333
4453
  status: "idle",
4334
4454
  busyLabel: "",
@@ -4353,7 +4473,7 @@ function initialState(overrides = {}) {
4353
4473
  orchestratorModel: "",
4354
4474
  plannerProvider: "",
4355
4475
  plannerEffort: "",
4356
- maxParallel: import_core22.DEFAULT_MAX_PARALLEL,
4476
+ maxParallel: import_core23.DEFAULT_MAX_PARALLEL,
4357
4477
  configuredProviders: [],
4358
4478
  allowlist: {},
4359
4479
  autonomous: true,
@@ -4365,7 +4485,7 @@ function initialState(overrides = {}) {
4365
4485
  gate: null,
4366
4486
  overlay: null,
4367
4487
  pendingApprovals: [],
4368
- queuedPrompts: import_core22.EMPTY_HOLD,
4488
+ queuedPrompts: import_core23.EMPTY_HOLD,
4369
4489
  stopArmed: false,
4370
4490
  stopArmToken: 0,
4371
4491
  stopRequested: false,
@@ -4413,7 +4533,7 @@ function chatEditorRoomFor(state, active) {
4413
4533
  }
4414
4534
 
4415
4535
  // src/tui/layout.ts
4416
- var import_core23 = require("@ordewell/core");
4536
+ var import_core24 = require("@ordewell/core");
4417
4537
  var CHROME_ROWS = 4;
4418
4538
  function chatInputWrap(state) {
4419
4539
  const active = state.focus === "chat" && !state.overlay;
@@ -4422,7 +4542,7 @@ function chatInputWrap(state) {
4422
4542
  }
4423
4543
  function footerHints(state) {
4424
4544
  const selected = selectedPlanRow(state)?.task;
4425
- const markHint = selected?.status === "completed" ? "m undone" : "m done";
4545
+ const markHint = selected && (0, import_core24.markAction)(selected) === "uncomplete" ? "m undone" : "m done";
4426
4546
  const resolveHint = selected?.isolation?.state === "conflict" ? ["x resolve conflict"] : [];
4427
4547
  const planning = plannerInFlight(state) && !state.taskView;
4428
4548
  const escHint = !planning ? null : state.queuedPrompts.length > 0 ? "esc unsend" : "esc \xD72 stop planning";
@@ -4453,7 +4573,7 @@ function footerHints(state) {
4453
4573
  "tab chat"
4454
4574
  ];
4455
4575
  }
4456
- const detailHint = (0, import_core23.hasHiddenDetail)(state.taskView ? state.taskView.view.blocks : state.conversation.blocks) ? [state.detailAll ? "ctrl-o collapse all" : "ctrl-o expand all"] : [];
4576
+ const detailHint = (0, import_core24.hasHiddenDetail)(state.taskView ? state.taskView.view.blocks : state.conversation.blocks) ? [state.detailAll ? "ctrl-o collapse all" : "ctrl-o expand all"] : [];
4457
4577
  return [
4458
4578
  "/help",
4459
4579
  "tab plan",
@@ -4474,20 +4594,25 @@ function queuedBubble(text, cols) {
4474
4594
  function taskQueuedRows(state, cols) {
4475
4595
  const tv = state.taskView;
4476
4596
  if (!tv) return [];
4477
- return tv.view.queued.flatMap((message, i) => queuedTaskBubble(message.text, i === tv.queuedIndex, cols));
4597
+ return tv.view.queued.flatMap((message, i) => queuedTaskBubble(message, i === tv.queuedIndex, cols));
4598
+ }
4599
+ function queuedTaskTag({ handedOver, forced }, selected) {
4600
+ if (forced) return " \xB7 sending now";
4601
+ if (handedOver) return " \xB7 handed over";
4602
+ return selected ? " \xB7 queued \xB7 ctrl-r removes" : " \xB7 queued";
4478
4603
  }
4479
- function queuedTaskBubble(text, selected, cols) {
4604
+ function queuedTaskBubble(message, selected, cols) {
4605
+ const { text } = message;
4480
4606
  const marker2 = selected ? style.accent("\u276F") : style.grey("\u25C7");
4481
- const tag = selected ? " \xB7 queued \xB7 ctrl-r removes" : " \xB7 queued";
4607
+ const tag = queuedTaskTag(message, selected);
4482
4608
  const room = Math.max(1, cols - width(`\u276F \u25C7 ${tag}`));
4483
4609
  return wrap(text, room).map((line, i) => i === 0 ? truncate(`${marker2} ${selected ? style.bold(line) : line}${style.grey(tag)}`, cols) : truncate(` ${line}`, cols));
4484
4610
  }
4485
4611
  function taskActivity(task, working) {
4486
- if (task?.awaitingApproval) return approvalLabel(task.awaitingApproval);
4612
+ const approvals = (0, import_core24.approvalLabel)(task?.awaitingApproval);
4613
+ if (approvals) return inRow(approvals);
4487
4614
  if (working) return "working";
4488
- if (task?.status === "awaiting_user") {
4489
- return task.awaitingReason ? AWAITING_LABEL[task.awaitingReason] : "waiting for your input";
4490
- }
4615
+ if (task?.status === "awaiting_user") return inRow((0, import_core24.awaitingLabel)(task) ?? "Waiting for your input");
4491
4616
  return task?.status ?? "";
4492
4617
  }
4493
4618
  function taskHeaderLines(state, tv, cols) {
@@ -4499,7 +4624,8 @@ function taskHeaderLines(state, tv, cols) {
4499
4624
  taskActivity(task, tv.view.working)
4500
4625
  ].filter(Boolean);
4501
4626
  const approval = waitingApproval(tv);
4502
- const hint = approval ? ["ctrl-y allow", ...approval.allowForTask ? ["ctrl-t allow for task"] : [], "ctrl-g deny (composer text is the note)"] : ["ctrl-r remove queued", "ctrl-x interrupt"];
4627
+ const hint = approval ? ["ctrl-y allow", ...approval.allowForTask ? ["ctrl-t allow for task"] : [], "ctrl-g deny (composer text is the note)"] : waitingCheckpoint(task) ? ["ctrl-y approve", "ctrl-g reject (composer text is the reason)", "ctrl-x interrupt"] : ["ctrl-r remove queued", "ctrl-x interrupt", "ctrl-s send now"];
4628
+ if (approval && waitingCheckpoint(task)) hint.push("then the checkpoint");
4503
4629
  const index = tv.attempts.indexOf(tv.attempt);
4504
4630
  if (tv.attempts.length > 1) hint.push(`alt\u2190/\u2192 attempt ${index >= 0 ? index + 1 : 1}/${tv.attempts.length}`);
4505
4631
  hint.push("esc back");
@@ -4532,10 +4658,10 @@ function bodyRows(state) {
4532
4658
  return Math.max(0, state.rows - CHROME_ROWS - input - footer + 2);
4533
4659
  }
4534
4660
  var chatBodyMemo = null;
4535
- function chatBodyLines(blocks, cols, detailAll) {
4661
+ function chatBodyLines(blocks, cols, detailAll, answering) {
4536
4662
  const memo = chatBodyMemo;
4537
4663
  if (memo && memo.blocks === blocks && memo.cols === cols && memo.detailAll === detailAll) return memo.lines;
4538
- const lines = conversationLines(blocks, cols, detailAll);
4664
+ const lines = conversationLines(blocks, cols, detailAll, answering);
4539
4665
  chatBodyMemo = { blocks, cols, detailAll, lines };
4540
4666
  return lines;
4541
4667
  }
@@ -4543,7 +4669,10 @@ function chatLayout(state, rows, cols) {
4543
4669
  const tv = state.taskView;
4544
4670
  const blocks = tv ? tv.view.blocks : state.conversation.blocks;
4545
4671
  const welcome = !tv && state.tasks.length === 0;
4546
- const body = chatBodyLines(blocks, cols, state.detailAll);
4672
+ const checkpoint = tv ? waitingCheckpoint(findTask(state.tasks, tv.taskId)) : void 0;
4673
+ const card = tv && checkpoint ? checkpointCardLines(findTask(state.tasks, tv.taskId)?.order ?? 0, checkpoint, cols, waitingApproval(tv) !== void 0) : [];
4674
+ const answering = chatBodyLines(blocks, cols, state.detailAll, tv ? waitingApproval(tv) : void 0);
4675
+ const body = card.length > 0 ? [...answering, ...card] : answering;
4547
4676
  const transcript = !welcome ? body : body.length === 0 ? welcomeLines(state, cols) : [...welcomeLines(state, cols), "", ...body];
4548
4677
  const bubbles = tv ? taskQueuedRows(state, cols) : queuedPromptRows(state, cols);
4549
4678
  const lines = [...transcript, ...bubbles.length > 0 ? ["", ...bubbles] : []];
@@ -4558,9 +4687,9 @@ function chatScrollMax(state) {
4558
4687
  }
4559
4688
  function plannerSetup(state) {
4560
4689
  const provider = state.plannerProvider;
4561
- const registration = import_core23.ALL_PROVIDERS[provider];
4690
+ const registration = import_core24.ALL_PROVIDERS[provider];
4562
4691
  if (!registration) return { label: "not set", ready: false };
4563
- const runner = (0, import_core23.runnerForProvider)(provider);
4692
+ const runner = (0, import_core24.runnerForProvider)(provider);
4564
4693
  const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
4565
4694
  return { label: registration.label, ready };
4566
4695
  }
@@ -4647,45 +4776,34 @@ function welcomeLines(state, cols) {
4647
4776
  return lines;
4648
4777
  }
4649
4778
  var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4650
- var STATUS_MARK = {
4651
- completed: style.green,
4779
+ var STATUS_PAINT = {
4780
+ done: style.green,
4652
4781
  running: style.yellow,
4653
- in_progress: style.yellow,
4782
+ quiet: style.cyan,
4654
4783
  failed: style.red,
4655
4784
  blocked: style.red,
4656
- awaiting_user: style.yellow,
4657
- cancelled: style.grey,
4658
- skipped: style.grey
4785
+ awaiting: style.yellow,
4786
+ todo: (text) => text
4659
4787
  };
4660
4788
  var STATUS_ICON = {
4661
- completed: "\u2713",
4789
+ done: "\u2713",
4662
4790
  failed: "\u2717",
4663
4791
  blocked: "!",
4664
- awaiting_user: "?",
4665
- cancelled: "\u2212",
4666
- skipped: "\u2212",
4667
- approved: "\xB7",
4668
- pending: "\xB7"
4792
+ awaiting: "?",
4793
+ quiet: "~",
4794
+ todo: "\xB7"
4669
4795
  };
4670
- var AWAITING_LABEL = {
4671
- input: "waiting for your input",
4672
- checkpoint: "checkpoint",
4673
- conflict: "merge conflict",
4674
- "files-changed": "changed tracked files"
4675
- };
4676
- function approvalLabel(count) {
4677
- return count > 1 ? `waiting for approval (${count})` : "waiting for approval";
4678
- }
4679
- var IDLE_ICON = "~";
4796
+ var inRow = (label) => label.charAt(0).toLowerCase() + label.slice(1);
4680
4797
  var FIRST_ROW_LINE = 2;
4681
4798
  function planLayout(state, rows, cols) {
4682
4799
  const done = state.tasks.filter((t) => t.status === "completed").length;
4683
4800
  const lines = [style.bold(`Plan ${done}/${state.tasks.length}`), ""];
4684
4801
  const rowSpans = [];
4802
+ const orderOf = new Map(state.tasks.map((task) => [task.id, task.order]));
4685
4803
  let editorLine;
4686
4804
  planRows(state).forEach((row, i) => {
4687
4805
  const taskStart = lines.length;
4688
- const renderedTask = taskLines(state, row, i, cols);
4806
+ const renderedTask = taskLines(state, row, i, cols, orderOf);
4689
4807
  if (row.task.id === state.expandedTaskId && renderedTask.editorLine !== void 0) {
4690
4808
  editorLine = taskStart + renderedTask.editorLine;
4691
4809
  }
@@ -4741,18 +4859,16 @@ function planScrollExtent(state) {
4741
4859
  }
4742
4860
  return planLayout(state, bodyRows(state), cols);
4743
4861
  }
4744
- function taskLines(state, row, index, cols) {
4745
- const { task, parent } = row;
4862
+ function taskLines(state, planRow, index, cols, orderOf) {
4863
+ const { task, parent } = planRow;
4864
+ const row = (0, import_core24.taskRowView)(task, { parent, orderOf, modes: modesForTask(state.modesByRunner, task) });
4746
4865
  const selected = state.focus === "plan" && index === state.selectedTask;
4747
4866
  const expanded = state.expandedTaskId === task.id;
4748
- const running = isTaskRunning(task);
4749
- const idle = running && task.status === "in_progress" && task.idleSince != null;
4750
- const paint = idle ? style.cyan : STATUS_MARK[task.status] ?? ((t) => t);
4751
- const rawIcon = idle ? IDLE_ICON : running ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[task.status] ?? "\xB7";
4752
- const icon = paint(rawIcon);
4753
- const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : task.ops ? style.magenta("OPS") : style.grey(" AI");
4867
+ const rawIcon = row.status === "running" ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[row.status];
4868
+ const icon = STATUS_PAINT[row.status](rawIcon);
4869
+ const kind = row.running ? style.yellow("RUN") : row.kind === "user" ? style.yellow("MAN") : row.kind === "ops" ? style.magenta("OPS") : style.grey(" AI");
4754
4870
  const caret = selected ? style.cyan("\u276F") : " ";
4755
- const orderLabel = parent ? (0, import_core23.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
4871
+ const orderLabel = parent ? row.orderLabel : row.orderLabel.padStart(2);
4756
4872
  const indent = parent ? " " : "";
4757
4873
  const head = `${indent}${caret} ${icon} ${orderLabel} ${kind} `;
4758
4874
  const titleRoom = Math.max(1, cols - width(head));
@@ -4769,59 +4885,52 @@ function taskLines(state, row, index, cols) {
4769
4885
  });
4770
4886
  const model = task.assignedModel?.modelLabel ?? (task.type === "ai" ? "default model" : "");
4771
4887
  const effort = task.type === "ai" ? `effort: ${task.assignedModel?.thinkingEffort ?? "default"}` : "";
4772
- const modeInfo = task.type === "ai" ? modesForTask(state.modesByRunner, task).find((m) => m.id === task.taskMode) : void 0;
4773
- const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}${modeInfo?.autonomous ? style.yellow(" \u26A1") : ""}` : "";
4888
+ const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}${row.autonomous ? style.yellow(" \u26A1") : ""}` : "";
4774
4889
  const runner = task.assignedRunner ?? "";
4775
4890
  const bodyPad = parent ? " " : " ";
4776
4891
  const structured = task.transport?.kind === "structured";
4777
- const waiting = task.status === "awaiting_user" && task.awaitingReason ? AWAITING_LABEL[task.awaitingReason] : "";
4778
- const activity = task.awaitingApproval ? `${approvalLabel(task.awaitingApproval)} \u2014 t opens it` : idle ? structured ? "quiet" : "quiet \u2014 t opens its terminal" : running ? "working" : waiting;
4892
+ const activity = row.approvals ? `${inRow(row.approvals)} \u2014 t opens it` : row.status === "quiet" ? structured ? "quiet" : "quiet \u2014 t opens its terminal" : row.running ? "working" : row.awaiting ? inRow(row.awaiting) : "";
4779
4893
  const meta = [activity, runner, structured ? "structured" : "", model].filter(Boolean).join(" \xB7 ");
4780
4894
  if (meta) lines.push(style.grey(truncate(`${bodyPad}${meta}`, cols)));
4781
4895
  if (task.transport?.fallback) lines.push(style.yellow(truncate(`${bodyPad}terminal: ${task.transport.fallback}`, cols)));
4782
4896
  if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
4783
- if (task.isolation?.state === "conflict") {
4784
- const where = task.isolation.conflictRepo && task.isolation.conflictRepo !== "." ? ` in ${task.isolation.conflictRepo}` : "";
4785
- const files = task.isolation.conflictFiles?.length ? ` (${(0, import_core23.capConflictFiles)(task.isolation.conflictFiles)})` : "";
4897
+ if (row.conflict) {
4898
+ const where = row.conflict.repo ? ` in ${row.conflict.repo}` : "";
4899
+ const files = row.conflict.files ? ` (${row.conflict.files})` : "";
4786
4900
  lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where}${files} \u2014 its work is kept on its own branch`, cols)));
4787
4901
  }
4788
- if (task.isolation?.state === "repairing") {
4789
- const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core23.capConflictFiles)(task.isolation.conflictFiles)}` : "";
4790
- const attempt = task.isolation.repair ? ` (attempt ${task.isolation.repair.attempt}/${task.isolation.repair.limit})` : "";
4902
+ if (row.repairing) {
4903
+ const files = row.repairing.files ? ` in ${row.repairing.files}` : "";
4904
+ const attempt = row.repairing.attempt ? ` (attempt ${row.repairing.attempt.attempt}/${row.repairing.attempt.limit})` : "";
4791
4905
  lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
4792
4906
  }
4793
- if (task.mergeGate?.length) {
4794
- const deps = task.mergeGate.map((id) => {
4795
- const dep = state.tasks.find((t) => t.id === id);
4796
- return dep ? `#${dep.order}` : id;
4797
- });
4798
- lines.push(style.yellow(truncate(`${bodyPad}\u23F8 waits for Merge all \u2014 ${deps.join(", ")} not merged into your branch yet`, cols)));
4907
+ if (row.mergeGate) {
4908
+ lines.push(style.yellow(truncate(`${bodyPad}\u23F8 waits for Merge all \u2014 ${row.mergeGate.join(", ")} not merged into your branch yet`, cols)));
4799
4909
  }
4800
- if (task.status === "awaiting_user" && task.awaitingReason === "files-changed") {
4910
+ if (row.opsChangedFiles) {
4801
4911
  lines.push(style.yellow(truncate(`${bodyPad}\u26A0 an ops task changed tracked files \u2014 check them, then m done or /retry`, cols)));
4802
4912
  }
4803
- if (task.isolation?.state === "integrated" && task.isolation.repairedFiles?.length) {
4804
- lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0, import_core23.capConflictFiles)(task.isolation.repairedFiles)}`, cols)));
4913
+ if (row.repaired?.landed) {
4914
+ lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${row.repaired.files}`, cols)));
4805
4915
  }
4806
4916
  let editorLine;
4807
4917
  if (expanded) {
4808
4918
  lines.push(style.grey(`${bodyPad}${task.status.replace(/_/g, " ")}`));
4809
4919
  if (task.isolation && task.isolation.state !== "none" && task.isolation.branch) {
4810
4920
  lines.push(...taskText("Branch", task.isolation.branch, cols, bodyPad));
4811
- const repos = taskRepoNames(task.isolation);
4812
- if (repos.length > 0) lines.push(...taskText("Repos", repos.join(", "), cols, bodyPad));
4921
+ if (row.repos.length > 0) lines.push(...taskText("Repos", row.repos.join(", "), cols, bodyPad));
4813
4922
  if (task.isolation.state !== "integrated" && task.isolation.worktree) {
4814
4923
  lines.push(...taskText("Worktree", task.isolation.worktree, cols, bodyPad));
4815
4924
  }
4816
4925
  }
4817
- if (task.ops) {
4926
+ if (row.kind === "ops") {
4818
4927
  lines.push(...taskText("Ops", "Runs in your checkout, not a worktree, once the work it depends on is merged. O makes it a change task.", cols, bodyPad));
4819
4928
  }
4820
- if (task.forcedPastGate?.length) {
4821
- lines.push(...taskText("Forced", `Started before the work of ${task.forcedPastGate.join(", ")} was merged into your branch.`, cols, bodyPad));
4929
+ if (row.forcedPastGate) {
4930
+ lines.push(...taskText("Forced", `Started before the work of ${row.forcedPastGate.join(", ")} was merged into your branch.`, cols, bodyPad));
4822
4931
  }
4823
- if (modeInfo?.autonomous) {
4824
- lines.push(...taskText("Autonomy", "Runs without permission prompts (Full auto). Change the level with /auto.", cols, bodyPad));
4932
+ if (row.autonomous) {
4933
+ lines.push(...taskText("Autonomy", "Runs without permission prompts (Full). Change the level with /auto.", cols, bodyPad));
4825
4934
  }
4826
4935
  const editableSeed = task.prompt ?? task.description ?? task.title;
4827
4936
  if (task.description && task.description !== task.title && task.description !== editableSeed) {
@@ -4834,10 +4943,8 @@ function taskLines(state, row, index, cols) {
4834
4943
  editorLine = lines.length + 1 + cp.line;
4835
4944
  lines.push(...taskPromptLines(wrapped, cp, bodyPad));
4836
4945
  }
4837
- if (task.dependencies.length > 0) {
4838
- const orderById = new Map(state.tasks.map((candidate) => [candidate.id, candidate.order]));
4839
- const dependencies = task.dependencies.map((id) => orderById.has(id) ? `#${orderById.get(id)}` : id).join(", ");
4840
- lines.push(...taskText("Depends on", dependencies, cols, bodyPad));
4946
+ if (row.dependencies.length > 0) {
4947
+ lines.push(...taskText("Depends on", row.dependencies.join(", "), cols, bodyPad));
4841
4948
  }
4842
4949
  lines.push(style.grey(`${bodyPad}enter save \xB7 esc cancel`));
4843
4950
  lines.push(style.grey(truncate(`${bodyPad}then R runner \xB7 o model \xB7 e effort \xB7 M mode \xB7 D deps`, cols)));
@@ -4884,7 +4991,13 @@ function helpLayout(rows, cols) {
4884
4991
  style.grey("tab switches panes \xB7 pgup/pgdn scroll \xB7 ctrl-o toggles full detail \xB7 esc takes back a queued prompt, otherwise esc twice stops \xB7 ctrl-l clears \xB7 ctrl-c quits")
4885
4992
  );
4886
4993
  body.push(
4887
- style.grey("in a task view (t or /terminal on a structured task): ctrl-r removes the selected queued message \xB7 ctrl-x interrupts \xB7 ctrl-y allows a tool request \xB7 ctrl-t allows it for the task \xB7 ctrl-g denies it, with the composer text as the note \xB7 alt\u2190/\u2192 changes attempt \xB7 esc returns")
4994
+ "",
4995
+ style.bold("In a task view (t or /terminal on a structured task)"),
4996
+ style.grey(" ctrl-s sends now: interrupts the running step, then delivers the composer text \u2014 or, with it empty, the selected queued message"),
4997
+ style.grey(" ctrl-n/ctrl-p select a queued message \xB7 ctrl-r removes it \xB7 ctrl-x interrupts"),
4998
+ style.grey(" ctrl-y allows a tool request (its keys sit under it) \xB7 ctrl-t allows it for the task \xB7 ctrl-g denies it, with the composer text as the note"),
4999
+ style.grey(" at a checkpoint, ctrl-y approves and ctrl-g rejects, with the composer text as the reason \xB7 /checkpoint <id> approve|reject [reason] from anywhere"),
5000
+ style.grey(" alt\u2190/\u2192 changes attempt \xB7 esc returns")
4888
5001
  );
4889
5002
  const lines = body.map((line) => truncate(line, Math.max(1, cols - 2)));
4890
5003
  const room = Math.max(1, rows - 2);
@@ -4895,7 +5008,7 @@ function helpScrollMax(state) {
4895
5008
  }
4896
5009
 
4897
5010
  // src/tui/render.ts
4898
- var import_core24 = require("@ordewell/core");
5011
+ var import_core25 = require("@ordewell/core");
4899
5012
  function render(state) {
4900
5013
  const { rows, cols } = state;
4901
5014
  const rowsForBody = bodyRows(state);
@@ -4990,7 +5103,7 @@ function renderSkills(state, cols) {
4990
5103
  const badges = SKILL_IDS.map(
4991
5104
  (id) => state.skills[id] ? style.green(`\u25CF ${id}`) : style.grey(`\u25CB ${id}`)
4992
5105
  );
4993
- const auto = state.autonomous ? style.yellow("\u25CF Full auto") : style.green("\u25CF Auto");
5106
+ const auto = state.autonomous ? style.yellow("\u25CF Full") : style.green("\u25CF Guarded");
4994
5107
  const terminal = state.runnerTransport === "terminal" ? [style.yellow("\u25CF terminal")] : [];
4995
5108
  return truncate([...badges, auto, ...terminal].join(" "), cols);
4996
5109
  }
@@ -5160,8 +5273,8 @@ function renderHandoff(state, overlay, rows, cols) {
5160
5273
  return frame(`Diff \u2014 ${handoffBranch(handoff)}`, [...shown, "", style.grey(`${more}enter or esc goes back`)], rows, cols);
5161
5274
  }
5162
5275
  const landed = handoff.landed.length === 0 ? [style.grey("Nothing landed on it.")] : handoff.landed.map((task) => {
5163
- const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core24.capConflictFiles)(task.repairedFiles)})`) : "";
5164
- return ` ${style.green("\u2713")} #${task.order} ${task.title}${repaired}`;
5276
+ const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core25.capConflictFiles)(task.repairedFiles)})`) : "";
5277
+ return ` ${style.green("\u2713")} ${(0, import_core25.titledTaskRef)(task)}${repaired}`;
5165
5278
  });
5166
5279
  const actions = handoffActions(handoff).map((action, index) => {
5167
5280
  const active = index === overlay.index;
@@ -5250,7 +5363,10 @@ function frame(title, body, rows, cols) {
5250
5363
  }
5251
5364
 
5252
5365
  // src/tui/reducer.ts
5253
- var import_core30 = require("@ordewell/core");
5366
+ var import_core33 = require("@ordewell/core");
5367
+
5368
+ // src/tui/reducers/overlays.ts
5369
+ var import_core31 = require("@ordewell/core");
5254
5370
 
5255
5371
  // src/tui/reducers/shared.ts
5256
5372
  function step(state, effects = []) {
@@ -5299,10 +5415,10 @@ function handleApprovalKey(state, overlay, key) {
5299
5415
  }
5300
5416
 
5301
5417
  // src/tui/reducers/commands.ts
5302
- var import_core28 = require("@ordewell/core");
5418
+ var import_core30 = require("@ordewell/core");
5303
5419
 
5304
5420
  // src/tui/reducers/pickers.ts
5305
- var import_core25 = require("@ordewell/core");
5421
+ var import_core26 = require("@ordewell/core");
5306
5422
  var import_plan_utils2 = require("@ordewell/core/plan-utils");
5307
5423
  var DEFAULT_EFFORT = "__runner_default__";
5308
5424
  function picker(title, items, action, extra = {}) {
@@ -5321,11 +5437,11 @@ function modelDetail(m) {
5321
5437
  function providerErrorHint(state) {
5322
5438
  const failed = Object.keys(state.providerErrors ?? {});
5323
5439
  if (failed.length === 0) return void 0;
5324
- const names = failed.map((p) => import_core25.ALL_PROVIDERS[p]?.label ?? p);
5440
+ const names = failed.map((p) => import_core26.ALL_PROVIDERS[p]?.label ?? p);
5325
5441
  return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
5326
5442
  }
5327
5443
  function plannerModelItems(state) {
5328
- const runner = (0, import_core25.runnerForProvider)(state.plannerProvider);
5444
+ const runner = (0, import_core26.runnerForProvider)(state.plannerProvider);
5329
5445
  if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
5330
5446
  const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
5331
5447
  id: m.id,
@@ -5339,7 +5455,7 @@ function plannerModelItems(state) {
5339
5455
  return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
5340
5456
  }
5341
5457
  function plannerEffortItems(state) {
5342
- const runner = (0, import_core25.runnerForProvider)(state.plannerProvider);
5458
+ const runner = (0, import_core26.runnerForProvider)(state.plannerProvider);
5343
5459
  if (!runner) {
5344
5460
  return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
5345
5461
  }
@@ -5403,7 +5519,7 @@ function pickerItemsFor(state, action) {
5403
5519
  if (action.kind === "set-task-deps") {
5404
5520
  return (0, import_plan_utils2.dependencyCandidates)(state.tasks, action.taskId).map((candidate) => ({
5405
5521
  id: candidate.id,
5406
- label: `#${candidate.order} ${candidate.title}`,
5522
+ label: (0, import_core26.titledTaskRef)(candidate),
5407
5523
  detail: candidate.status === "completed" ? "already completed" : void 0
5408
5524
  }));
5409
5525
  }
@@ -5448,26 +5564,29 @@ function pickerItemsFor(state, action) {
5448
5564
  }
5449
5565
  function plannerItems(state) {
5450
5566
  const installed = new Set(state.runners.map((r) => r.id));
5451
- const agents = import_core25.CLI_PROVIDERS.map((id) => {
5452
- const runner = (0, import_core25.runnerForProvider)(id);
5567
+ const agents = import_core26.CLI_PROVIDERS.map((id) => {
5568
+ const runner = (0, import_core26.runnerForProvider)(id);
5453
5569
  const ready = installed.has(runner);
5454
5570
  return {
5455
5571
  id,
5456
- label: import_core25.ALL_PROVIDERS[id].label,
5572
+ label: import_core26.ALL_PROVIDERS[id].label,
5457
5573
  detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
5458
5574
  selected: state.plannerProvider === id,
5459
5575
  disabled: !ready
5460
5576
  };
5461
5577
  });
5462
- const vendors = import_core25.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
5578
+ const vendors = import_core26.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
5463
5579
  id,
5464
- label: import_core25.ALL_PROVIDERS[id].label,
5465
- detail: import_core25.ALL_PROVIDERS[id].apiKeyEnvVar,
5580
+ label: import_core26.ALL_PROVIDERS[id].label,
5581
+ detail: import_core26.ALL_PROVIDERS[id].apiKeyEnvVar,
5466
5582
  selected: state.plannerProvider === id
5467
5583
  }));
5468
5584
  return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
5469
5585
  }
5470
5586
 
5587
+ // src/tui/reducers/planPane.ts
5588
+ var import_core28 = require("@ordewell/core");
5589
+
5471
5590
  // src/tui/reducers/pointer.ts
5472
5591
  var WHEEL_NOTCH = 3;
5473
5592
  function pageNotch(viewport) {
@@ -5544,13 +5663,13 @@ function handleMouseSelect(state, key) {
5544
5663
  }
5545
5664
 
5546
5665
  // src/tui/reducers/taskView.ts
5547
- var import_core26 = require("@ordewell/core");
5666
+ var import_core27 = require("@ordewell/core");
5548
5667
  function openTaskView(state, sessionId, taskId) {
5549
5668
  const task = findTask(state.tasks, taskId);
5550
5669
  if (!task) return step(say(state, "system", `No task ${taskId} in this plan.`));
5551
5670
  const taskView = {
5552
5671
  taskId,
5553
- view: import_core26.EMPTY_TASK_LOG,
5672
+ view: import_core27.EMPTY_TASK_LOG,
5554
5673
  attempts: [],
5555
5674
  attempt: 0,
5556
5675
  pending: [],
@@ -5583,13 +5702,13 @@ function applyLive(tv, attempt, events) {
5583
5702
  }
5584
5703
  next = {
5585
5704
  ...next,
5586
- view: import_core26.EMPTY_TASK_LOG,
5705
+ view: import_core27.EMPTY_TASK_LOG,
5587
5706
  attempt,
5588
5707
  attempts: next.attempts.includes(attempt) ? next.attempts : [...next.attempts, attempt]
5589
5708
  };
5590
5709
  }
5591
5710
  if (attempt < next.attempt) return next;
5592
- return clampQueueIndex({ ...next, view: events.reduce(import_core26.reduceTaskLog, next.view) });
5711
+ return clampQueueIndex({ ...next, view: events.reduce(import_core27.reduceTaskLog, next.view) });
5593
5712
  }
5594
5713
  function taskLogAbandoned(state) {
5595
5714
  const tv = state.taskView;
@@ -5609,15 +5728,22 @@ function taskLogArrived(state, action) {
5609
5728
  function announceApprovals(state, action) {
5610
5729
  if (state.taskView?.taskId === action.taskId) return state;
5611
5730
  const task = findTask(state.tasks, action.taskId);
5612
- const spoken = action.events.reduce((s, event) => event.type === "approval_requested" ? say(s, "system", `\xB7 Task ${task?.order ?? "?"} waits for approval: ${(0, import_core26.runnerToolSubject)(event.tool, event.args)} \u2014 t on it to answer`) : s, state);
5731
+ const spoken = action.events.reduce((s, event) => event.type === "approval_requested" ? say(s, "system", `\xB7 Task ${task?.order ?? "?"} waits for approval: ${(0, import_core27.runnerToolSubject)(event.tool, event.args)} \u2014 t on it, then ctrl-y to allow or ctrl-g to deny`) : s, state);
5613
5732
  return spoken !== state ? { ...spoken, scroll: state.scroll } : spoken;
5614
5733
  }
5734
+ function announceCheckpoint(state, action) {
5735
+ const task = findTask(state.tasks, action.taskId) ?? state.tasks.find((t) => t.title === action.title);
5736
+ const label = task ? `Task ${task.order}` : action.title;
5737
+ const where = task?.transport?.kind === "structured" ? "t on it to answer" : `/checkpoint ${task?.order ?? "<id>"} approve|reject to answer`;
5738
+ const spoken = say(state, "system", `\xB7 ${label} asks: ${(0, import_core27.truncateCheckpointSummary)(action.summary)} \u2014 ${where}`);
5739
+ return { ...spoken, scroll: state.scroll };
5740
+ }
5615
5741
  function taskLogLoaded(state, action) {
5616
5742
  const tv = state.taskView;
5617
5743
  if (!tv || tv.taskId !== action.taskId) return state;
5618
5744
  const loaded = action.attempt >= 1 ? {
5619
5745
  ...tv,
5620
- view: (0, import_core26.replayTaskLog)(action.events),
5746
+ view: (0, import_core27.replayTaskLog)(action.events),
5621
5747
  attempts: action.attempts ?? tv.attempts,
5622
5748
  attempt: action.attempt
5623
5749
  } : { ...tv, attempts: action.attempts ?? tv.attempts };
@@ -5639,6 +5765,29 @@ function answerApproval(state, tv, sessionId, decision) {
5639
5765
  const next = note ? { ...state, editor: { ...state.editor, text: "", cursor: 0 } } : state;
5640
5766
  return step(next, [{ type: "answerTaskApproval", sessionId, approvalId: approval.approvalId, answer }]);
5641
5767
  }
5768
+ function answerCheckpoint(state, sessionId, taskId, answer, reason = "") {
5769
+ const task = findTask(state.tasks, taskId);
5770
+ if (!waitingCheckpoint(task)) return step(say(state, "system", `Task ${task?.order ?? taskId} is not waiting at a checkpoint.`));
5771
+ const note = answer === "reject" ? reason.trim() : "";
5772
+ return step(state, [{ type: "answerTaskCheckpoint", sessionId, taskId, answer, ...note ? { reason: note } : {} }]);
5773
+ }
5774
+ function answerWaiting(state, tv, sessionId, decision) {
5775
+ if (waitingApproval(tv) || !waitingCheckpoint(findTask(state.tasks, tv.taskId))) return answerApproval(state, tv, sessionId, decision);
5776
+ if (decision === "allow") return answerCheckpoint(state, sessionId, tv.taskId, "approve");
5777
+ const answered = answerCheckpoint(state, sessionId, tv.taskId, "reject", state.editor.text);
5778
+ return state.editor.text.trim() ? { ...answered, state: { ...answered.state, editor: { ...answered.state.editor, text: "", cursor: 0 } } } : answered;
5779
+ }
5780
+ function sendNow(state, tv, sessionId) {
5781
+ const text = state.editor.text.trim();
5782
+ if (text) {
5783
+ if (continuesTask(findTask(state.tasks, tv.taskId))) return step(say(state, "system", "The task has finished, so there is no turn to interrupt \u2014 enter continues it."));
5784
+ return step({ ...state, editor: commit(state.editor) }, [{ type: "forceSendTaskMessage", sessionId, taskId: tv.taskId, text }]);
5785
+ }
5786
+ const queued = tv.view.queued[tv.queuedIndex];
5787
+ if (!queued) return step(say(state, "system", "Nothing to send now: type a message, or pick a queued one with ctrl-n/ctrl-p."));
5788
+ if (queued.handedOver) return step(say(state, "system", "The runner already has that message; it reads it after its current step."));
5789
+ return step(state, [{ type: "forceSendQueuedTaskMessage", sessionId, taskId: tv.taskId, messageId: queued.id }]);
5790
+ }
5642
5791
  function handleTaskViewKey(state, key) {
5643
5792
  const tv = state.taskView;
5644
5793
  if (!tv || !state.sessionId) return null;
@@ -5658,14 +5807,16 @@ function handleTaskViewKey(state, key) {
5658
5807
  if (key.name === "ctrl-r") {
5659
5808
  const queued = tv.view.queued[tv.queuedIndex];
5660
5809
  if (!queued) return step(say(state, "system", "No queued message to remove."));
5810
+ if (queued.handedOver) return step(say(state, "system", "The runner already has that message; it can no longer be taken back."));
5661
5811
  return step(state, [{ type: "removeTaskMessage", sessionId: state.sessionId, taskId: tv.taskId, messageId: queued.id }]);
5662
5812
  }
5813
+ if (key.name === "ctrl-s") return sendNow(state, tv, state.sessionId);
5663
5814
  if (key.name === "ctrl-x") {
5664
5815
  return step(state, [{ type: "interruptTask", sessionId: state.sessionId, taskId: tv.taskId }]);
5665
5816
  }
5666
- if (key.name === "ctrl-y") return answerApproval(state, tv, state.sessionId, "allow");
5817
+ if (key.name === "ctrl-y") return answerWaiting(state, tv, state.sessionId, "allow");
5667
5818
  if (key.name === "ctrl-t") return answerApproval(state, tv, state.sessionId, "allowForTask");
5668
- if (key.name === "ctrl-g") return answerApproval(state, tv, state.sessionId, "deny");
5819
+ if (key.name === "ctrl-g") return answerWaiting(state, tv, state.sessionId, "deny");
5669
5820
  if (key.name === "ctrl-n" || key.name === "ctrl-p") {
5670
5821
  const count = tv.view.queued.length;
5671
5822
  if (count === 0) return null;
@@ -5686,7 +5837,7 @@ function openTaskRunnerPicker(state, task) {
5686
5837
  ...state,
5687
5838
  overlay: {
5688
5839
  kind: "picker",
5689
- picker: picker(`Runner \xB7 #${task.order} ${task.title}`, pickerItemsFor(state, action), action, {
5840
+ picker: picker(`Runner \xB7 ${(0, import_plan_utils3.titledTaskRef)(task)}`, pickerItemsFor(state, action), action, {
5690
5841
  hint: "Changing the runner re-picks this task model, effort and mode for it."
5691
5842
  })
5692
5843
  }
@@ -5694,14 +5845,12 @@ function openTaskRunnerPicker(state, task) {
5694
5845
  );
5695
5846
  }
5696
5847
  function confirmRemoveTask(state, task) {
5697
- const dependents = (0, import_plan_utils3.dependentsOf)(state.tasks, task.id);
5698
- const named = dependents.map((t) => `#${t.order} ${t.title}`).join(", ");
5699
5848
  return step({
5700
5849
  ...state,
5701
5850
  overlay: {
5702
5851
  kind: "confirm",
5703
- title: `Remove #${task.order} ${task.title}?`,
5704
- message: dependents.length > 0 ? `${dependents.length === 1 ? "1 task depends" : `${dependents.length} tasks depend`} on it and will lose that dependency: ${named}.` : "This cannot be undone.",
5852
+ title: `Remove ${(0, import_plan_utils3.titledTaskRef)(task)}?`,
5853
+ message: (0, import_plan_utils3.dependentsNotice)((0, import_plan_utils3.dependentsOf)(state.tasks, task.id)) ?? "This cannot be undone.",
5705
5854
  action: { kind: "remove-task", taskId: task.id }
5706
5855
  }
5707
5856
  });
@@ -5710,26 +5859,22 @@ function toggleTaskOps(state, task, subtask, to = !task.ops) {
5710
5859
  if (!state.sessionId) return fail3(state, "No active plan.");
5711
5860
  if (task.type !== "ai") return fail3(state, "Only an AI task can be an ops task \u2014 a manual task already runs outside any worktree.");
5712
5861
  if (subtask) return fail3(state, "A subtask runs with its parent; make the parent an ops task instead.");
5713
- if (!!task.ops === to) return fail3(state, `#${task.order} is already ${to ? "an ops" : "a change"} task.`);
5862
+ if (!!task.ops === to) return fail3(state, `${(0, import_plan_utils3.taskRef)(task)} is already ${to ? "an ops" : "a change"} task.`);
5714
5863
  return step(state, [{
5715
5864
  type: "updateTask",
5716
5865
  sessionId: state.sessionId,
5717
5866
  taskId: task.id,
5718
5867
  changes: { ops: to },
5719
- message: to ? `Task #${task.order} is an ops task: it runs in your checkout once the work it depends on is merged.` : `Task #${task.order} is a change task: it runs in its own worktree.`
5868
+ message: to ? `Task ${(0, import_plan_utils3.taskRef)(task)} is an ops task: it runs in your checkout once the work it depends on is merged.` : `Task ${(0, import_plan_utils3.taskRef)(task)} is a change task: it runs in its own worktree.`
5720
5869
  }]);
5721
5870
  }
5722
5871
  function confirmForceStartPastGate(state, task) {
5723
- const named = (task.mergeGate ?? []).map((id) => {
5724
- const dep = findTask(state.tasks, id);
5725
- return dep ? `#${dep.order} ${dep.title}` : id;
5726
- });
5727
5872
  return step({
5728
5873
  ...state,
5729
5874
  overlay: {
5730
5875
  kind: "confirm",
5731
- title: `Force start #${task.order} ${task.title}?`,
5732
- message: `It waits for Merge all: the work of ${named.join(", ")} is not merged into your branch yet, so it would act without it. Starting it now is kept on the task.`,
5876
+ title: `Force start ${(0, import_plan_utils3.titledTaskRef)(task)}?`,
5877
+ message: (0, import_plan_utils3.pastGateConfirmation)((0, import_plan_utils3.titledRefs)(task.mergeGate ?? [], state.tasks), "It"),
5733
5878
  action: { kind: "force-start-gated", taskId: task.id }
5734
5879
  }
5735
5880
  });
@@ -5737,12 +5882,12 @@ function confirmForceStartPastGate(state, task) {
5737
5882
  function openTaskDepsPicker(state, task) {
5738
5883
  const action = { kind: "set-task-deps", taskId: task.id };
5739
5884
  const items = pickerItemsFor(state, action);
5740
- if (items.length === 0) return fail3(state, `Nothing runs before #${task.order}, so it has no possible dependencies.`);
5885
+ if (items.length === 0) return fail3(state, `Nothing runs before ${(0, import_plan_utils3.taskRef)(task)}, so it has no possible dependencies.`);
5741
5886
  return step({
5742
5887
  ...state,
5743
5888
  overlay: {
5744
5889
  kind: "picker",
5745
- picker: picker(`Depends on \xB7 #${task.order} ${task.title}`, items, action, {
5890
+ picker: picker(`Depends on \xB7 ${(0, import_plan_utils3.titledTaskRef)(task)}`, items, action, {
5746
5891
  hint: "Only tasks earlier in the plan can be dependencies.",
5747
5892
  multi: true,
5748
5893
  chosen: task.dependencies.filter((id) => items.some((i) => i.id === id))
@@ -5762,7 +5907,7 @@ function openTaskModePicker(state, task) {
5762
5907
  ...state,
5763
5908
  overlay: {
5764
5909
  kind: "picker",
5765
- picker: picker(`Mode \xB7 #${task.order}`, pickerItemsFor(state, action), action, {
5910
+ picker: picker(`Mode \xB7 ${(0, import_plan_utils3.taskRef)(task)}`, pickerItemsFor(state, action), action, {
5766
5911
  hint: `Modes declared by ${task.assignedRunner}.`
5767
5912
  })
5768
5913
  }
@@ -5777,7 +5922,7 @@ function openTaskModelPicker(state, task) {
5777
5922
  ...state,
5778
5923
  overlay: {
5779
5924
  kind: "picker",
5780
- picker: picker(`Model \xB7 #${task.order} ${task.title}`, pickerItemsFor(state, action), action, {
5925
+ picker: picker(`Model \xB7 ${(0, import_plan_utils3.titledTaskRef)(task)}`, pickerItemsFor(state, action), action, {
5781
5926
  hint: task.assignedRunner ? `Showing models discovered for ${task.assignedRunner}.` : "Choose the model this task will run with."
5782
5927
  })
5783
5928
  }
@@ -5794,7 +5939,7 @@ function openTaskEffortPicker(state, task) {
5794
5939
  ...state,
5795
5940
  overlay: {
5796
5941
  kind: "picker",
5797
- picker: picker(`Thinking effort \xB7 #${task.order}`, pickerItemsFor(state, action), action, {
5942
+ picker: picker(`Thinking effort \xB7 ${(0, import_plan_utils3.taskRef)(task)}`, pickerItemsFor(state, action), action, {
5798
5943
  hint: `${task.assignedModel.modelLabel} \xB7 choose runner default or a supported effort`
5799
5944
  })
5800
5945
  }
@@ -5811,7 +5956,7 @@ function assignTaskModel(state, sessionId, task, model) {
5811
5956
  // JSON drops `undefined`; null is intentional here so changing models can
5812
5957
  // also clear a stale legacy top-level effort on the persisted task.
5813
5958
  changes: { assignedModel, thinkingEffort: assignedModel.thinkingEffort ?? null },
5814
- message: `Task #${task.order} model set to ${model.label}.`
5959
+ message: `Task ${(0, import_plan_utils3.taskRef)(task)} model set to ${model.label}.`
5815
5960
  }]);
5816
5961
  }
5817
5962
  function assignTaskRunner(state, sessionId, task, runner, runnerLabel) {
@@ -5820,7 +5965,7 @@ function assignTaskRunner(state, sessionId, task, runner, runnerLabel) {
5820
5965
  sessionId,
5821
5966
  taskId: task.id,
5822
5967
  changes: { assignedRunner: runner },
5823
- message: `Task #${task.order} runner set to ${runnerLabel}.`
5968
+ message: `Task ${(0, import_plan_utils3.taskRef)(task)} runner set to ${runnerLabel}.`
5824
5969
  }]);
5825
5970
  }
5826
5971
  function assignTaskMode(state, sessionId, task, mode, modeLabel) {
@@ -5829,7 +5974,7 @@ function assignTaskMode(state, sessionId, task, mode, modeLabel) {
5829
5974
  sessionId,
5830
5975
  taskId: task.id,
5831
5976
  changes: { taskMode: mode },
5832
- message: `Task #${task.order} mode set to ${modeLabel}.`
5977
+ message: `Task ${(0, import_plan_utils3.taskRef)(task)} mode set to ${modeLabel}.`
5833
5978
  }]);
5834
5979
  }
5835
5980
  function assignTaskEffort(state, sessionId, task, thinkingEffort) {
@@ -5839,7 +5984,7 @@ function assignTaskEffort(state, sessionId, task, thinkingEffort) {
5839
5984
  sessionId,
5840
5985
  taskId: task.id,
5841
5986
  changes: { assignedModel, thinkingEffort: thinkingEffort ?? null },
5842
- message: `Task #${task.order} thinking effort set to ${thinkingEffort ?? "runner default"}.`
5987
+ message: `Task ${(0, import_plan_utils3.taskRef)(task)} thinking effort set to ${thinkingEffort ?? "runner default"}.`
5843
5988
  }]);
5844
5989
  }
5845
5990
  function taskModelCommand(state, args) {
@@ -5941,9 +6086,6 @@ function taskActionEffect(state, sessionId, taskId, action) {
5941
6086
  const watch = SPAWNS_RUNNER.includes(action) && state.status !== "executing";
5942
6087
  return watch ? { type: "taskAction", sessionId, taskId, action, watch: true } : { type: "taskAction", sessionId, taskId, action };
5943
6088
  }
5944
- function markAction(task) {
5945
- return task.status === "completed" ? "uncomplete" : "complete";
5946
- }
5947
6089
  function handlePlanKey(state, key) {
5948
6090
  const editingTask = state.expandedTaskId ? findTask(state.tasks, state.expandedTaskId) : void 0;
5949
6091
  if (editingTask && state.taskEditor) return handleTaskEditKey(state, editingTask, state.taskEditor, key);
@@ -5978,7 +6120,7 @@ function handlePlanKey(state, key) {
5978
6120
  if (!state.sessionId || key.name !== "char") return step(state);
5979
6121
  if (key.char === "E") return step(state, [{ type: "execute", sessionId: state.sessionId }]);
5980
6122
  if (key.char === "S") return step(state, [{ type: "stopExecution", sessionId: state.sessionId }]);
5981
- const action = key.char === "m" ? markAction(task) : PLAN_SHORTCUTS[key.char ?? ""];
6123
+ const action = key.char === "m" ? (0, import_core28.markAction)(task) : PLAN_SHORTCUTS[key.char ?? ""];
5982
6124
  if (action === "force-start" && task.mergeGate?.length) return confirmForceStartPastGate(state, task);
5983
6125
  if (action) {
5984
6126
  return step(state, [taskActionEffect(state, state.sessionId, task.id, action)]);
@@ -6013,14 +6155,14 @@ function commitTaskEdit(state, task, editor) {
6013
6155
  sessionId: state.sessionId,
6014
6156
  taskId: task.id,
6015
6157
  changes: { prompt },
6016
- message: `Task #${task.order} prompt updated.`
6158
+ message: `Task ${(0, import_core28.taskRef)(task)} prompt updated.`
6017
6159
  }]);
6018
6160
  }
6019
6161
 
6020
6162
  // src/tui/reducers/turnQueue.ts
6021
- var import_core27 = require("@ordewell/core");
6163
+ var import_core29 = require("@ordewell/core");
6022
6164
  function drainQueue(state, settled) {
6023
- const next = (0, import_core27.drainNext)(state.queuedPrompts);
6165
+ const next = (0, import_core29.drainNext)(state.queuedPrompts);
6024
6166
  const ended = disarmStop({ ...settled, stopRequested: false });
6025
6167
  if (!next) return step(ended);
6026
6168
  const spoken = say(ended, "user", next.text);
@@ -6039,14 +6181,14 @@ function stopPlanning(state, sessionId) {
6039
6181
  return step(queueToEditor(cutTurn({ ...dismissed, stopArmed: false, stopRequested: true })), [{ type: "cancelPlanning", sessionId }]);
6040
6182
  }
6041
6183
  function queueToEditor(state) {
6042
- const all = (0, import_core27.unsendAll)(state.queuedPrompts);
6184
+ const all = (0, import_core29.unsendAll)(state.queuedPrompts);
6043
6185
  if (!all) return state;
6044
6186
  return { ...state, queuedPrompts: all.rest, editor: { ...state.editor, text: all.text, cursor: all.text.length } };
6045
6187
  }
6046
6188
  function unsendLatest(state) {
6047
- const latest = (0, import_core27.unsendLatest)(state.queuedPrompts);
6189
+ const latest = (0, import_core29.unsendLatest)(state.queuedPrompts);
6048
6190
  if (!latest) return step(state);
6049
- const text = (0, import_core27.aheadOfDraft)(latest.text, state.editor.text);
6191
+ const text = (0, import_core29.aheadOfDraft)(latest.text, state.editor.text);
6050
6192
  return step({
6051
6193
  ...state,
6052
6194
  queuedPrompts: latest.rest,
@@ -6063,7 +6205,7 @@ function plannerEscape(state) {
6063
6205
  }
6064
6206
 
6065
6207
  // src/tui/reducers/commands.ts
6066
- var KNOWN_PROVIDERS2 = Object.keys(import_core28.ALL_PROVIDERS);
6208
+ var KNOWN_PROVIDERS2 = Object.keys(import_core30.ALL_PROVIDERS);
6067
6209
  function runCommand(state, { name, args }) {
6068
6210
  if (!findCommand(name)) {
6069
6211
  return fail3(state, `Unknown command: /${name} \u2014 type /help to see what's available.`);
@@ -6150,6 +6292,14 @@ function runCommand(state, { name, args }) {
6150
6292
  if (!text) return fail3(state, "Usage: /continue <id> <message>");
6151
6293
  return continueTaskStep(state, sessionId, taskId, text);
6152
6294
  });
6295
+ case "checkpoint":
6296
+ return taskCommand(state, args[0], (sessionId, taskId) => {
6297
+ const answer = args[1]?.toLowerCase();
6298
+ const reason = args.slice(2).join(" ").trim();
6299
+ if (answer !== "approve" && answer !== "reject") return fail3(state, "Usage: /checkpoint <id> approve|reject [reason]");
6300
+ if (answer === "approve" && reason) return fail3(state, "Approving takes no note \u2014 reject with a reason to tell the task something.");
6301
+ return answerCheckpoint(state, sessionId, taskId, answer, reason);
6302
+ });
6153
6303
  case "task-runner":
6154
6304
  return taskRunnerCommand(state, args);
6155
6305
  case "task-model":
@@ -6221,8 +6371,8 @@ function newSession(state) {
6221
6371
  taskView: null,
6222
6372
  tasks: [],
6223
6373
  planApproved: false,
6224
- conversation: import_core28.EMPTY_CONVERSATION,
6225
- turnGate: import_core28.NO_TURN,
6374
+ conversation: import_core30.EMPTY_CONVERSATION,
6375
+ turnGate: import_core30.NO_TURN,
6226
6376
  selectedTask: 0,
6227
6377
  expandedTaskId: null,
6228
6378
  taskEditor: null,
@@ -6233,14 +6383,14 @@ function newSession(state) {
6233
6383
  // Prompts belong to the session that raised them; the old planner is gone
6234
6384
  // and its pending requests deny on their own timeout.
6235
6385
  pendingApprovals: [],
6236
- queuedPrompts: import_core28.EMPTY_HOLD,
6386
+ queuedPrompts: import_core30.EMPTY_HOLD,
6237
6387
  stopArmed: false,
6238
6388
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6239
6389
  }, closeEffects);
6240
6390
  }
6241
6391
  function setModel(state, args) {
6242
6392
  if (args[0] === "set" && args[1]) {
6243
- const runner = (0, import_core28.runnerForProvider)(state.plannerProvider);
6393
+ const runner = (0, import_core30.runnerForProvider)(state.plannerProvider);
6244
6394
  const known = runner ? modelsForRunner(state.models, runner) : [];
6245
6395
  if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
6246
6396
  return fail3(state, `${args[1]} was not discovered for ${runner}.`);
@@ -6313,16 +6463,16 @@ function setKey(state, args) {
6313
6463
  }
6314
6464
  return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
6315
6465
  }
6316
- const items = import_core28.PROVIDER_PRIORITY.map((id) => ({
6466
+ const items = import_core30.PROVIDER_PRIORITY.map((id) => ({
6317
6467
  id,
6318
- label: import_core28.ALL_PROVIDERS[id].label,
6319
- detail: import_core28.ALL_PROVIDERS[id].apiKeyEnvVar,
6468
+ label: import_core30.ALL_PROVIDERS[id].label,
6469
+ detail: import_core30.ALL_PROVIDERS[id].apiKeyEnvVar,
6320
6470
  selected: state.configuredProviders.includes(id)
6321
6471
  }));
6322
6472
  return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
6323
6473
  }
6324
6474
  function keyPrompt(provider) {
6325
- const meta = import_core28.ALL_PROVIDERS[provider];
6475
+ const meta = import_core30.ALL_PROVIDERS[provider];
6326
6476
  return {
6327
6477
  kind: "prompt",
6328
6478
  title: `${meta.label} API key`,
@@ -6399,19 +6549,19 @@ function runners(state, args) {
6399
6549
  }
6400
6550
  function setMaxParallel(state, arg) {
6401
6551
  if (!arg) return step(say(state, "system", `Up to ${state.maxParallel} AI task${state.maxParallel === 1 ? "" : "s"} run at once \u2014 /parallel <n> changes it.`));
6402
- const limit = (0, import_core28.parseMaxParallel)(arg);
6552
+ const limit = (0, import_core30.parseMaxParallel)(arg);
6403
6553
  if (limit === null) return fail3(state, "Usage: /parallel [<number of tasks, 1 or more>]");
6404
6554
  return step({ ...state, maxParallel: limit }, [{ type: "setMaxParallel", limit }]);
6405
6555
  }
6406
6556
  function setAutonomous(state, arg) {
6407
- if (arg === void 0) return step(say(state, "system", `Autonomy level: ${(0, import_core28.autonomyLevelLabel)(state.autonomous)} \u2014 /auto full or /auto auto changes it for new plans.`));
6408
- const enabled = (0, import_core28.parseAutonomyLevel)(arg);
6409
- if (enabled === null) return fail3(state, "Usage: /auto [full|auto]");
6557
+ if (arg === void 0) return step(say(state, "system", `Autonomy level: ${(0, import_core30.autonomyLevelLabel)(state.autonomous)} \u2014 /auto full or /auto guarded changes it for new plans.`));
6558
+ const enabled = (0, import_core30.parseAutonomyLevel)(arg);
6559
+ if (enabled === null) return fail3(state, "Usage: /auto [full|guarded]");
6410
6560
  return step({ ...state, autonomous: enabled }, [{ type: "setAutonomous", enabled }]);
6411
6561
  }
6412
6562
  function setTransport(state, arg) {
6413
6563
  const value = arg?.toLowerCase() ?? (state.runnerTransport === "structured" ? "terminal" : "structured");
6414
- if (!(0, import_core28.isRunnerTransport)(value)) return fail3(state, "Usage: /transport [terminal|structured]");
6564
+ if (!(0, import_core30.isRunnerTransport)(value)) return fail3(state, "Usage: /transport [terminal|structured]");
6415
6565
  return step(state, [{ type: "setTransport", transport: value }]);
6416
6566
  }
6417
6567
  function setMouseCapture(state, arg) {
@@ -6588,7 +6738,7 @@ function choose(state, picker2, item) {
6588
6738
  sessionId: state.sessionId,
6589
6739
  taskId: task.id,
6590
6740
  changes: { dependencies: picker2.chosen },
6591
- message: picker2.chosen.length > 0 ? `Task #${task.order} now depends on ${picker2.chosen.length} task${picker2.chosen.length === 1 ? "" : "s"}.` : `Task #${task.order} no longer depends on anything.`
6741
+ message: picker2.chosen.length > 0 ? `Task ${(0, import_core31.taskRef)(task)} now depends on ${picker2.chosen.length} task${picker2.chosen.length === 1 ? "" : "s"}.` : `Task ${(0, import_core31.taskRef)(task)} no longer depends on anything.`
6592
6742
  }]);
6593
6743
  }
6594
6744
  if (!item) return step(state);
@@ -6665,7 +6815,7 @@ function choose(state, picker2, item) {
6665
6815
  }
6666
6816
 
6667
6817
  // src/tui/reducers/incoming.ts
6668
- var import_core29 = require("@ordewell/core");
6818
+ var import_core32 = require("@ordewell/core");
6669
6819
  var planText = (value, fallback = "") => sanitize(String(value ?? fallback));
6670
6820
  var planLabel = (value, fallback = "") => planText(value, fallback).replace(/\n+/g, " ");
6671
6821
  var asRecord = (value) => typeof value === "object" && value !== null ? value : {};
@@ -6696,7 +6846,7 @@ function toTaskView2(t, index) {
6696
6846
  prompt: typeof t.prompt === "string" ? sanitize(t.prompt) : void 0,
6697
6847
  type: t.type === "user" ? "user" : "ai",
6698
6848
  status: planLabel(t.status, "pending"),
6699
- awaitingReason: t.status === "awaiting_user" && (0, import_core29.isAwaitingReason)(t.awaitingReason) ? t.awaitingReason : void 0,
6849
+ awaitingReason: t.status === "awaiting_user" && (0, import_core32.isAwaitingReason)(t.awaitingReason) ? t.awaitingReason : void 0,
6700
6850
  dependencies: Array.isArray(t.dependencies) ? t.dependencies.map((id) => planLabel(id)) : [],
6701
6851
  assignedRunner: typeof t.assignedRunner === "string" ? planLabel(t.assignedRunner) : void 0,
6702
6852
  taskMode: typeof t.taskMode === "string" ? planLabel(t.taskMode) : void 0,
@@ -6729,12 +6879,12 @@ function applySettings(state, settings) {
6729
6879
  plannerProvider: typeof settings.aiProvider === "string" ? settings.aiProvider : state.plannerProvider,
6730
6880
  plannerEffort: typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : state.plannerEffort,
6731
6881
  maxParallel: typeof settings.maxParallel === "number" ? settings.maxParallel : state.maxParallel,
6732
- runnerTransport: (0, import_core29.isRunnerTransport)(settings.runnerTransport) ? settings.runnerTransport : state.runnerTransport,
6882
+ runnerTransport: (0, import_core32.isRunnerTransport)(settings.runnerTransport) ? settings.runnerTransport : state.runnerTransport,
6733
6883
  allowlist: settings.modelAllowlist && typeof settings.modelAllowlist === "object" ? settings.modelAllowlist : state.allowlist
6734
6884
  };
6735
6885
  }
6736
6886
  function runLabel(tasks, gate = null) {
6737
- const running = tasks.filter(isTaskRunning);
6887
+ const running = tasks.filter(import_core23.isTaskRunning);
6738
6888
  if (running.length > 0) {
6739
6889
  const [first] = running;
6740
6890
  const name = first.assignedRunner ? `${first.title} \xB7 ${first.assignedRunner}` : first.title;
@@ -6800,13 +6950,13 @@ function reduceAction(state, action) {
6800
6950
  goal: "",
6801
6951
  taskView: null,
6802
6952
  pendingApprovals: [],
6803
- queuedPrompts: import_core30.EMPTY_HOLD,
6953
+ queuedPrompts: import_core33.EMPTY_HOLD,
6804
6954
  stopArmed: false,
6805
6955
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6806
6956
  });
6807
6957
  case "chatRestored":
6808
6958
  if (stale(state, action.sessionId)) return step(state);
6809
- return step({ ...state, conversation: (0, import_core30.fromTranscript)(action.history, action.researchLog, action.plannerUsage), scroll: 0 });
6959
+ return step({ ...state, conversation: (0, import_core33.fromTranscript)(action.history, action.researchLog, action.plannerUsage), scroll: 0 });
6810
6960
  case "planUpdated": {
6811
6961
  if (stale(state, action.sessionId)) return step(state);
6812
6962
  const isolation = isolationOfPlan(action.plan);
@@ -6853,14 +7003,17 @@ function reduceAction(state, action) {
6853
7003
  case "taskStarted": {
6854
7004
  if (stale(state, action.sessionId)) return step(state);
6855
7005
  const existing = findTask(state.tasks, action.taskId);
6856
- if (existing && isTaskRunning(existing)) return step(state);
6857
- const tasks = state.tasks.map((t) => t.id === action.taskId && !isTaskRunning(t) ? { ...t, status: "in_progress" } : t);
6858
- const spoken = say(state, "system", (0, import_core30.taskStartedNotice)(action.title, action.runner));
7006
+ if (existing && (0, import_core23.isTaskRunning)(existing)) return step(state);
7007
+ const tasks = state.tasks.map((t) => t.id === action.taskId && !(0, import_core23.isTaskRunning)(t) ? { ...t, status: "in_progress" } : t);
7008
+ const spoken = say(state, "system", (0, import_core33.taskStartedNotice)(action.title, action.runner));
6859
7009
  return step({ ...spoken, tasks, status: "executing", busyLabel: runLabel(tasks) });
6860
7010
  }
7011
+ case "taskCheckpoint":
7012
+ if (stale(state, action.sessionId)) return step(state);
7013
+ return step(announceCheckpoint(state, action));
6861
7014
  case "taskStatus": {
6862
7015
  if (stale(state, action.sessionId) || !state.tasks.some((t) => t.id === action.taskId)) return step(state);
6863
- const tasks = state.tasks.map((t) => t.id === action.taskId ? { ...t, status: action.status, awaitingReason: void 0 } : t);
7016
+ const tasks = state.tasks.map((t) => t.id === action.taskId ? { ...t, status: action.status, awaitingReason: void 0, checkpoint: void 0 } : t);
6864
7017
  const status = runStatus(state, tasks);
6865
7018
  return step({ ...state, status, tasks, busyLabel: status === "executing" ? runLabel(tasks, state.gate) : state.busyLabel });
6866
7019
  }
@@ -6875,13 +7028,14 @@ function reduceAction(state, action) {
6875
7028
  const isolation = update.isolation ?? t.isolation;
6876
7029
  const transport = update.transport;
6877
7030
  const awaitingReason = update.awaitingReason;
7031
+ const checkpoint = update.checkpoint;
6878
7032
  const continuable = update.continuable === true;
6879
7033
  const awaitingApproval = update.awaitingApproval;
6880
7034
  const mergeGate = update.mergeGate;
6881
7035
  const forcedPastGate = update.forcedPastGate;
6882
- if (update.status !== t.status || idleSince !== (t.idleSince ?? null) || !sameIsolation(isolation, t.isolation) || !sameTransport(transport, t.transport) || awaitingReason !== t.awaitingReason || continuable !== (t.continuable ?? false) || awaitingApproval !== t.awaitingApproval || !sameList(mergeGate, t.mergeGate) || !sameList(forcedPastGate, t.forcedPastGate)) {
7036
+ if (update.status !== t.status || idleSince !== (t.idleSince ?? null) || !sameIsolation(isolation, t.isolation) || !sameTransport(transport, t.transport) || awaitingReason !== t.awaitingReason || checkpoint !== t.checkpoint || continuable !== (t.continuable ?? false) || awaitingApproval !== t.awaitingApproval || !sameList(mergeGate, t.mergeGate) || !sameList(forcedPastGate, t.forcedPastGate)) {
6883
7037
  changed = true;
6884
- return { ...t, status: update.status, idleSince, isolation, transport, awaitingReason, continuable, awaitingApproval, mergeGate, forcedPastGate };
7038
+ return { ...t, status: update.status, idleSince, isolation, transport, awaitingReason, checkpoint, continuable, awaitingApproval, mergeGate, forcedPastGate };
6885
7039
  }
6886
7040
  return t;
6887
7041
  });
@@ -6964,7 +7118,7 @@ function reduceAction(state, action) {
6964
7118
  busyLabel: "",
6965
7119
  planApproved: false,
6966
7120
  pendingApprovals: [],
6967
- queuedPrompts: import_core30.EMPTY_HOLD,
7121
+ queuedPrompts: import_core33.EMPTY_HOLD,
6968
7122
  stopArmed: false,
6969
7123
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6970
7124
  });
@@ -7031,7 +7185,7 @@ function sameGate(a, b) {
7031
7185
  return a.paused === b.paused && JSON.stringify(a.handoff) === JSON.stringify(b.handoff);
7032
7186
  }
7033
7187
  function runStatus(state, tasks) {
7034
- if (tasks.some(isTaskRunning)) return "executing";
7188
+ if (tasks.some(import_core23.isTaskRunning)) return "executing";
7035
7189
  if (tasks.some((t) => t.status === "awaiting_user") || state.gate?.paused) return "executing";
7036
7190
  return plannerInFlight(state) ? state.status : "idle";
7037
7191
  }
@@ -7100,7 +7254,7 @@ function submit(state) {
7100
7254
  return step(cleared, [{ type: "sendTaskMessage", sessionId: state.sessionId, taskId, text }]);
7101
7255
  }
7102
7256
  if (plannerInFlight(state)) {
7103
- return step({ ...cleared, queuedPrompts: (0, import_core30.holdPrompt)(state.queuedPrompts, text) });
7257
+ return step({ ...cleared, queuedPrompts: (0, import_core33.holdPrompt)(state.queuedPrompts, text) });
7104
7258
  }
7105
7259
  const spoken = say(cleared, "user", text);
7106
7260
  const effect = state.sessionId ? { type: "sendMessage", sessionId: state.sessionId, message: text } : { type: "startConversation", goal: text };
@@ -7160,10 +7314,10 @@ function createApp(deps) {
7160
7314
 
7161
7315
  // src/tui/effects.ts
7162
7316
  var import_child_process4 = require("child_process");
7163
- var import_core32 = require("@ordewell/core");
7317
+ var import_core35 = require("@ordewell/core");
7164
7318
 
7165
7319
  // src/tui/inbound.ts
7166
- var import_core31 = require("@ordewell/core");
7320
+ var import_core34 = require("@ordewell/core");
7167
7321
  var CONVERSATION_TYPES = /* @__PURE__ */ new Set([
7168
7322
  "planner_message",
7169
7323
  "planner_turn_started",
@@ -7378,7 +7532,8 @@ function dispatchLifecycle(dispatch, event, sessionId) {
7378
7532
  idleSince: task.idleSince ?? null,
7379
7533
  isolation: task.isolation,
7380
7534
  transport: task.transport,
7381
- awaitingReason: (0, import_core31.isAwaitingReason)(task.awaitingReason) ? task.awaitingReason : void 0,
7535
+ awaitingReason: (0, import_core34.isAwaitingReason)(task.awaitingReason) ? task.awaitingReason : void 0,
7536
+ ...typeof task.checkpoint === "string" && task.checkpoint ? { checkpoint: sanitize(task.checkpoint) } : {},
7382
7537
  ...task.continuable ? { continuable: true } : {},
7383
7538
  awaitingApproval: typeof task.awaitingApproval === "number" && task.awaitingApproval > 0 ? task.awaitingApproval : void 0,
7384
7539
  ...task.mergeGate?.length ? { mergeGate: task.mergeGate.map(String) } : {},
@@ -7392,10 +7547,10 @@ function dispatchLifecycle(dispatch, event, sessionId) {
7392
7547
  case "task_started":
7393
7548
  dispatch({ type: "taskStarted", taskId: String(event.taskId), title: String(event.title ?? event.taskId), runner: event.runner, sessionId });
7394
7549
  return;
7395
- // The extension shows these in its checkpoint panel; here they are
7396
- // transcript lines, which is the TUI's equivalent surface.
7550
+ // The extension shows these in its checkpoint panel; here they are a
7551
+ // transcript line, and the task view carries the card that answers them.
7397
7552
  case "checkpoint":
7398
- dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core31.truncateCheckpointSummary)(event.summary)}` });
7553
+ dispatch({ type: "taskCheckpoint", taskId: String(event.taskId), title: String(event.taskTitle), summary: event.summary, sessionId });
7399
7554
  return;
7400
7555
  case "review_needed":
7401
7556
  dispatch({ type: "notice", message: "Plan needs your sign-off \u2014 /approve to continue." });
@@ -7553,20 +7708,18 @@ async function perform(effect, deps) {
7553
7708
  return;
7554
7709
  case "taskAction": {
7555
7710
  const { sessionId, taskId, action } = effect;
7556
- const request = action === "complete" || action === "skip" ? () => api.markTaskComplete(sessionId, taskId) : action === "uncomplete" ? () => api.markTaskIncomplete(sessionId, taskId) : () => api.taskControl(sessionId, taskId, action);
7711
+ const request = action === "cancel" || action === "retry" || action === "force-start" ? () => api.taskControl(sessionId, taskId, action) : (0, import_core35.markRequestFor)(action) === "uncomplete" ? () => api.markTaskIncomplete(sessionId, taskId) : () => api.markTaskComplete(sessionId, taskId);
7557
7712
  await (effect.watch ? withExecutionStream(deps, sessionId, request) : request());
7558
7713
  await refreshPlan(deps, sessionId);
7559
7714
  return;
7560
7715
  }
7561
- case "addTask":
7562
- await api.addTask(effect.sessionId, {
7563
- title: effect.title,
7564
- description: effect.title,
7565
- prompt: effect.title,
7566
- type: "ai"
7567
- });
7716
+ case "addTask": {
7717
+ const fields = (0, import_core35.newTaskFields)(effect.title);
7718
+ if (!fields) return;
7719
+ await api.addTask(effect.sessionId, { ...fields });
7568
7720
  await refreshPlan(deps, effect.sessionId);
7569
7721
  return;
7722
+ }
7570
7723
  case "updateTask":
7571
7724
  await api.updateTask(effect.sessionId, effect.taskId, effect.changes);
7572
7725
  await refreshPlan(deps, effect.sessionId);
@@ -7593,6 +7746,13 @@ async function perform(effect, deps) {
7593
7746
  }
7594
7747
  return;
7595
7748
  }
7749
+ // The card goes when the daemon's next status says the task left the
7750
+ // checkpoint; a refusal (settled elsewhere, withdrawn) surfaces as the
7751
+ // failure the daemon words.
7752
+ case "answerTaskCheckpoint":
7753
+ if (effect.answer === "approve") await api.approveTaskCheckpoint(effect.sessionId, effect.taskId);
7754
+ else await api.rejectTaskCheckpoint(effect.sessionId, effect.taskId, effect.reason);
7755
+ return;
7596
7756
  // A task with a saved log ran structured, whatever its `transport` says
7597
7757
  // after a reload; it has no tmux window, so open the log rather than fail.
7598
7758
  case "openTaskTerminal": {
@@ -7642,6 +7802,16 @@ async function perform(effect, deps) {
7642
7802
  if (!removed) dispatch({ type: "notice", message: "That message was already delivered to the task." });
7643
7803
  return;
7644
7804
  }
7805
+ // The task log shows the forced message at the head of the queue, then
7806
+ // the interrupt, then the turn it opens; nothing to dispatch here.
7807
+ case "forceSendTaskMessage":
7808
+ await api.forceSendTaskMessage(effect.sessionId, effect.taskId, effect.text);
7809
+ return;
7810
+ case "forceSendQueuedTaskMessage": {
7811
+ const { sent } = await api.forceSendQueuedTaskMessage(effect.sessionId, effect.taskId, effect.messageId);
7812
+ if (!sent) dispatch({ type: "notice", message: "The runner already has that message; it reads it after its current step." });
7813
+ return;
7814
+ }
7645
7815
  case "interruptTask":
7646
7816
  await api.interruptTask(effect.sessionId, effect.taskId);
7647
7817
  dispatch({ type: "notice", message: "Interrupting the task\u2026" });
@@ -7665,7 +7835,7 @@ async function perform(effect, deps) {
7665
7835
  dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
7666
7836
  return;
7667
7837
  case "setPlanner": {
7668
- const meta = import_core32.ALL_PROVIDERS[effect.provider];
7838
+ const meta = import_core35.ALL_PROVIDERS[effect.provider];
7669
7839
  if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
7670
7840
  const env = { AI_PROVIDER: effect.provider };
7671
7841
  const settings = await api.updateSettings({ env });
@@ -7677,7 +7847,7 @@ async function perform(effect, deps) {
7677
7847
  });
7678
7848
  dispatch({
7679
7849
  type: "notice",
7680
- message: `${(0, import_core32.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
7850
+ message: `${(0, import_core35.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
7681
7851
  });
7682
7852
  await loadModels(deps);
7683
7853
  return;
@@ -7689,7 +7859,7 @@ async function perform(effect, deps) {
7689
7859
  dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
7690
7860
  return;
7691
7861
  case "setApiKey": {
7692
- const meta = import_core32.ALL_PROVIDERS[effect.provider];
7862
+ const meta = import_core35.ALL_PROVIDERS[effect.provider];
7693
7863
  if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
7694
7864
  await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
7695
7865
  deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
@@ -7727,7 +7897,7 @@ async function perform(effect, deps) {
7727
7897
  return;
7728
7898
  case "setAutonomous":
7729
7899
  deps.setEnvVar("ORDEWELL_AUTONOMOUS_MODE", String(effect.enabled));
7730
- dispatch({ type: "notice", message: `Autonomy level: ${(0, import_core32.autonomyLevelLabel)(effect.enabled)} for new plans.` });
7900
+ dispatch({ type: "notice", message: `Autonomy level: ${(0, import_core35.autonomyLevelLabel)(effect.enabled)} for new plans.` });
7731
7901
  return;
7732
7902
  case "setMouseCapture":
7733
7903
  deps.setMouseCapture(effect.enabled);
@@ -7861,7 +8031,7 @@ function osc52(text) {
7861
8031
  function copySelection(deps, text) {
7862
8032
  if (!text) return;
7863
8033
  const lines = text.split("\n").length;
7864
- const command = (0, import_core32.clipboardCopyCommand)(deps.hasBin);
8034
+ const command = (0, import_core35.clipboardCopyCommand)(deps.hasBin);
7865
8035
  if (command) {
7866
8036
  try {
7867
8037
  (deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
@@ -7978,6 +8148,7 @@ var CONTROL2 = {
7978
8148
  "": "ctrl-o",
7979
8149
  "": "ctrl-p",
7980
8150
  "": "ctrl-r",
8151
+ "": "ctrl-s",
7981
8152
  "": "ctrl-t",
7982
8153
  "": "ctrl-u",
7983
8154
  "": "ctrl-w",
@@ -8251,7 +8422,7 @@ async function handleTui(subArgs) {
8251
8422
  }
8252
8423
  const workspace = flag(subArgs, "--workspace") || process.cwd();
8253
8424
  try {
8254
- (0, import_core33.assertWorkspaceExists)(workspace);
8425
+ (0, import_core36.assertWorkspaceExists)(workspace);
8255
8426
  } catch {
8256
8427
  console.error(
8257
8428
  `Workspace "${workspace}" does not exist.
@@ -8260,7 +8431,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
8260
8431
  );
8261
8432
  process.exit(1);
8262
8433
  }
8263
- registerSkillCommands((0, import_core33.createSkillsService)(workspace).listSkills());
8434
+ registerSkillCommands((0, import_core36.createSkillsService)(workspace).listSkills());
8264
8435
  const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
8265
8436
  const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
8266
8437
  const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
@@ -8277,7 +8448,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
8277
8448
  workspace,
8278
8449
  port,
8279
8450
  dispatch: (action) => app.dispatch(action),
8280
- newSessionId: () => (0, import_core33.mintSessionId)(),
8451
+ newSessionId: () => (0, import_core36.mintSessionId)(),
8281
8452
  setEnvVar: (key, value) => {
8282
8453
  writeEnvVar(findEnvFile(), key, value);
8283
8454
  process.env[key] = value;
@@ -8368,6 +8539,7 @@ var COMMANDS = {
8368
8539
  "retry": handleRetry,
8369
8540
  "cancel": handleCancel,
8370
8541
  continue: handleContinue,
8542
+ checkpoint: handleCheckpoint,
8371
8543
  "add-task": handleAddTask,
8372
8544
  "remove-task": handleRemoveTask,
8373
8545
  "task-runner": handleTaskRunner,
@@ -8401,7 +8573,7 @@ function cliVersion() {
8401
8573
  // src/main.ts
8402
8574
  var TUI_FLAGS = /* @__PURE__ */ new Set(["--workspace", "--port"]);
8403
8575
  async function main() {
8404
- (0, import_core34.migrateOldConfigDir)();
8576
+ (0, import_core37.migrateOldConfigDir)();
8405
8577
  loadEnvFile();
8406
8578
  const argv = process.argv.slice(2);
8407
8579
  if (TUI_FLAGS.has(argv[0] ?? "")) argv.unshift("tui");