@ordewell/cli 0.7.0 → 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 +392 -254
  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
 
@@ -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)
@@ -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);
@@ -3311,15 +3379,15 @@ function handleAuto(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" },
@@ -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) {
@@ -4161,16 +4230,16 @@ function resultLines(block, room, detailAll) {
4161
4230
  const fit2 = (line) => detailAll ? wrap(line, room) : [truncate(line, room)];
4162
4231
  const outcome = unsettledOutcome(block);
4163
4232
  if (outcome) {
4164
- const [reason, ...rest] = (0, import_core21.outputLines)(block.output).map(sanitize);
4233
+ const [reason, ...rest] = (0, import_core22.outputLines)(block.output).map(sanitize);
4165
4234
  const status = fit2(reason ? `${outcome.label}: ${reason}` : outcome.label).map(outcome.paint);
4166
4235
  return detailAll ? [...status, ...rest.flatMap(fit2).map(style.grey)] : status;
4167
4236
  }
4168
4237
  if (detailAll) {
4169
- const lines2 = (0, import_core21.outputLines)(block.output).map(sanitize);
4238
+ const lines2 = (0, import_core22.outputLines)(block.output).map(sanitize);
4170
4239
  const note = lines2.length > PREVIEW_LINES ? ["\u2026 (ctrl+o to collapse)"] : [];
4171
4240
  return lines2.length > 0 ? [...lines2.flatMap(fit2), ...note].map(style.grey) : [style.grey("(no output)")];
4172
4241
  }
4173
- const { lines, hiddenLineCount } = (0, import_core21.outputPreview)(block.output, PREVIEW_LINES);
4242
+ const { lines, hiddenLineCount } = (0, import_core22.outputPreview)(block.output, PREVIEW_LINES);
4174
4243
  if (lines.length === 0) return [style.grey("(no output)")];
4175
4244
  const more = hiddenLineCount > 0 ? [`\u2026 +${countOf(hiddenLineCount, "line")} (ctrl+o to expand)`] : [];
4176
4245
  return [...lines.map(sanitize), ...more].map((line) => style.grey(truncate(line, room)));
@@ -4184,7 +4253,7 @@ var DIFF_PAINT = {
4184
4253
  };
4185
4254
  var DIFF_SIGN = { added: "+", removed: "-", context: " ", gap: "" };
4186
4255
  function diffLines(block, diff, room, detailAll) {
4187
- const rows = (0, import_core21.diffRows)(block.output);
4256
+ const rows = (0, import_core22.diffRows)(block.output);
4188
4257
  const gutter = Math.max(0, ...rows.map((row) => String(row.line ?? "").length));
4189
4258
  const draw = (row) => {
4190
4259
  const text = row.kind === "gap" ? `${" ".repeat(gutter)} \u22EE` : `${String(row.line ?? "").padStart(gutter)} ${DIFF_SIGN[row.kind]} ${sanitize(row.text)}`;
@@ -4193,7 +4262,7 @@ function diffLines(block, diff, room, detailAll) {
4193
4262
  const shown = detailAll ? rows : rows.slice(0, DIFF_PREVIEW_ROWS);
4194
4263
  const hidden = rows.length - shown.length;
4195
4264
  const more = hidden > 0 ? [style.grey(`\u2026 +${countOf(hidden, "line")} (ctrl+o to expand)`)] : [];
4196
- return [(0, import_core21.diffSummary)(diff), ...shown.flatMap(draw), ...more];
4265
+ return [(0, import_core22.diffSummary)(diff), ...shown.flatMap(draw), ...more];
4197
4266
  }
4198
4267
  function toolLines(block, cols, detailAll) {
4199
4268
  const room = Math.max(1, cols - OUTPUT_INDENT.length);
@@ -4249,6 +4318,19 @@ function approvalLine(block, cols) {
4249
4318
  const line = `${mark} ${status} \xB7 ${APPROVAL_KIND[block.kind]}: ${firstLine(block.subject)}${note}`;
4250
4319
  return `${paint(mark)}${truncate(line, cols).slice(mark.length)}`;
4251
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
+ }
4252
4334
  function planLine(block, cols) {
4253
4335
  const count = block.taskCount === void 0 ? "" : ` (${countOf(block.taskCount, "task")})`;
4254
4336
  const text = block.status === "building" ? "Building plan\u2026" : `Plan ${block.status}${count}`;
@@ -4281,11 +4363,13 @@ function drawn(block, cols, detailAll) {
4281
4363
  drawnBlocks.set(block, { cols, detailAll, lines });
4282
4364
  return lines;
4283
4365
  }
4284
- function conversationLines(blocks, cols, detailAll) {
4366
+ function conversationLines(blocks, cols, detailAll, answering) {
4285
4367
  const lines = [];
4286
4368
  for (const block of blocks) {
4287
4369
  if (block.type === "usage") continue;
4288
- 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("");
4289
4373
  }
4290
4374
  return lines;
4291
4375
  }
@@ -4313,7 +4397,7 @@ function tokenLine(blocks, cols) {
4313
4397
  }
4314
4398
 
4315
4399
  // src/tui/state.ts
4316
- var import_core22 = require("@ordewell/core");
4400
+ var import_core23 = require("@ordewell/core");
4317
4401
  var SKILL_IDS = ["tdd", "verify"];
4318
4402
  function noSkills() {
4319
4403
  return { tdd: false, verify: false };
@@ -4325,8 +4409,7 @@ function visibleItems(picker2) {
4325
4409
  (i) => i.id.toLowerCase().includes(filter) || i.label.toLowerCase().includes(filter) || (i.detail ?? "").toLowerCase().includes(filter)
4326
4410
  );
4327
4411
  }
4328
- var isTaskRunning = (task) => task.status === "in_progress" || task.status === "running";
4329
- var anyTaskRunning = (state) => state.tasks.some(isTaskRunning);
4412
+ var anyTaskRunning = (state) => state.tasks.some(import_core23.isTaskRunning);
4330
4413
  var plannerInFlight = (state) => state.status === "planning" || state.status === "researching";
4331
4414
  function planRows(state) {
4332
4415
  const rows = [];
@@ -4346,6 +4429,9 @@ function selectedPlanRow(state) {
4346
4429
  function waitingApproval(tv) {
4347
4430
  return tv.view.blocks.find((b) => b.type === "approval" && b.status === "pending" && b.approvalId !== void 0);
4348
4431
  }
4432
+ function waitingCheckpoint(task) {
4433
+ return task?.status === "awaiting_user" && task.awaitingReason === "checkpoint" ? task.checkpoint : void 0;
4434
+ }
4349
4435
  function continuesTask(task) {
4350
4436
  return task?.continuable === true && (task.status === "completed" || task.status === "failed");
4351
4437
  }
@@ -4360,9 +4446,9 @@ function findTask(tasks, id) {
4360
4446
  function initialState(overrides = {}) {
4361
4447
  return {
4362
4448
  editor: emptyEditor(),
4363
- conversation: import_core22.EMPTY_CONVERSATION,
4449
+ conversation: import_core23.EMPTY_CONVERSATION,
4364
4450
  taskView: null,
4365
- turnGate: import_core22.NO_TURN,
4451
+ turnGate: import_core23.NO_TURN,
4366
4452
  detailAll: false,
4367
4453
  status: "idle",
4368
4454
  busyLabel: "",
@@ -4387,7 +4473,7 @@ function initialState(overrides = {}) {
4387
4473
  orchestratorModel: "",
4388
4474
  plannerProvider: "",
4389
4475
  plannerEffort: "",
4390
- maxParallel: import_core22.DEFAULT_MAX_PARALLEL,
4476
+ maxParallel: import_core23.DEFAULT_MAX_PARALLEL,
4391
4477
  configuredProviders: [],
4392
4478
  allowlist: {},
4393
4479
  autonomous: true,
@@ -4399,7 +4485,7 @@ function initialState(overrides = {}) {
4399
4485
  gate: null,
4400
4486
  overlay: null,
4401
4487
  pendingApprovals: [],
4402
- queuedPrompts: import_core22.EMPTY_HOLD,
4488
+ queuedPrompts: import_core23.EMPTY_HOLD,
4403
4489
  stopArmed: false,
4404
4490
  stopArmToken: 0,
4405
4491
  stopRequested: false,
@@ -4447,7 +4533,7 @@ function chatEditorRoomFor(state, active) {
4447
4533
  }
4448
4534
 
4449
4535
  // src/tui/layout.ts
4450
- var import_core23 = require("@ordewell/core");
4536
+ var import_core24 = require("@ordewell/core");
4451
4537
  var CHROME_ROWS = 4;
4452
4538
  function chatInputWrap(state) {
4453
4539
  const active = state.focus === "chat" && !state.overlay;
@@ -4456,7 +4542,7 @@ function chatInputWrap(state) {
4456
4542
  }
4457
4543
  function footerHints(state) {
4458
4544
  const selected = selectedPlanRow(state)?.task;
4459
- const markHint = selected?.status === "completed" ? "m undone" : "m done";
4545
+ const markHint = selected && (0, import_core24.markAction)(selected) === "uncomplete" ? "m undone" : "m done";
4460
4546
  const resolveHint = selected?.isolation?.state === "conflict" ? ["x resolve conflict"] : [];
4461
4547
  const planning = plannerInFlight(state) && !state.taskView;
4462
4548
  const escHint = !planning ? null : state.queuedPrompts.length > 0 ? "esc unsend" : "esc \xD72 stop planning";
@@ -4487,7 +4573,7 @@ function footerHints(state) {
4487
4573
  "tab chat"
4488
4574
  ];
4489
4575
  }
4490
- 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"] : [];
4491
4577
  return [
4492
4578
  "/help",
4493
4579
  "tab plan",
@@ -4508,20 +4594,25 @@ function queuedBubble(text, cols) {
4508
4594
  function taskQueuedRows(state, cols) {
4509
4595
  const tv = state.taskView;
4510
4596
  if (!tv) return [];
4511
- 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));
4512
4598
  }
4513
- function queuedTaskBubble(text, selected, cols) {
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";
4603
+ }
4604
+ function queuedTaskBubble(message, selected, cols) {
4605
+ const { text } = message;
4514
4606
  const marker2 = selected ? style.accent("\u276F") : style.grey("\u25C7");
4515
- const tag = selected ? " \xB7 queued \xB7 ctrl-r removes" : " \xB7 queued";
4607
+ const tag = queuedTaskTag(message, selected);
4516
4608
  const room = Math.max(1, cols - width(`\u276F \u25C7 ${tag}`));
4517
4609
  return wrap(text, room).map((line, i) => i === 0 ? truncate(`${marker2} ${selected ? style.bold(line) : line}${style.grey(tag)}`, cols) : truncate(` ${line}`, cols));
4518
4610
  }
4519
4611
  function taskActivity(task, working) {
4520
- if (task?.awaitingApproval) return approvalLabel(task.awaitingApproval);
4612
+ const approvals = (0, import_core24.approvalLabel)(task?.awaitingApproval);
4613
+ if (approvals) return inRow(approvals);
4521
4614
  if (working) return "working";
4522
- if (task?.status === "awaiting_user") {
4523
- return task.awaitingReason ? AWAITING_LABEL[task.awaitingReason] : "waiting for your input";
4524
- }
4615
+ if (task?.status === "awaiting_user") return inRow((0, import_core24.awaitingLabel)(task) ?? "Waiting for your input");
4525
4616
  return task?.status ?? "";
4526
4617
  }
4527
4618
  function taskHeaderLines(state, tv, cols) {
@@ -4533,7 +4624,8 @@ function taskHeaderLines(state, tv, cols) {
4533
4624
  taskActivity(task, tv.view.working)
4534
4625
  ].filter(Boolean);
4535
4626
  const approval = waitingApproval(tv);
4536
- 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");
4537
4629
  const index = tv.attempts.indexOf(tv.attempt);
4538
4630
  if (tv.attempts.length > 1) hint.push(`alt\u2190/\u2192 attempt ${index >= 0 ? index + 1 : 1}/${tv.attempts.length}`);
4539
4631
  hint.push("esc back");
@@ -4566,10 +4658,10 @@ function bodyRows(state) {
4566
4658
  return Math.max(0, state.rows - CHROME_ROWS - input - footer + 2);
4567
4659
  }
4568
4660
  var chatBodyMemo = null;
4569
- function chatBodyLines(blocks, cols, detailAll) {
4661
+ function chatBodyLines(blocks, cols, detailAll, answering) {
4570
4662
  const memo = chatBodyMemo;
4571
4663
  if (memo && memo.blocks === blocks && memo.cols === cols && memo.detailAll === detailAll) return memo.lines;
4572
- const lines = conversationLines(blocks, cols, detailAll);
4664
+ const lines = conversationLines(blocks, cols, detailAll, answering);
4573
4665
  chatBodyMemo = { blocks, cols, detailAll, lines };
4574
4666
  return lines;
4575
4667
  }
@@ -4577,7 +4669,10 @@ function chatLayout(state, rows, cols) {
4577
4669
  const tv = state.taskView;
4578
4670
  const blocks = tv ? tv.view.blocks : state.conversation.blocks;
4579
4671
  const welcome = !tv && state.tasks.length === 0;
4580
- 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;
4581
4676
  const transcript = !welcome ? body : body.length === 0 ? welcomeLines(state, cols) : [...welcomeLines(state, cols), "", ...body];
4582
4677
  const bubbles = tv ? taskQueuedRows(state, cols) : queuedPromptRows(state, cols);
4583
4678
  const lines = [...transcript, ...bubbles.length > 0 ? ["", ...bubbles] : []];
@@ -4592,9 +4687,9 @@ function chatScrollMax(state) {
4592
4687
  }
4593
4688
  function plannerSetup(state) {
4594
4689
  const provider = state.plannerProvider;
4595
- const registration = import_core23.ALL_PROVIDERS[provider];
4690
+ const registration = import_core24.ALL_PROVIDERS[provider];
4596
4691
  if (!registration) return { label: "not set", ready: false };
4597
- const runner = (0, import_core23.runnerForProvider)(provider);
4692
+ const runner = (0, import_core24.runnerForProvider)(provider);
4598
4693
  const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
4599
4694
  return { label: registration.label, ready };
4600
4695
  }
@@ -4681,45 +4776,34 @@ function welcomeLines(state, cols) {
4681
4776
  return lines;
4682
4777
  }
4683
4778
  var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4684
- var STATUS_MARK = {
4685
- completed: style.green,
4779
+ var STATUS_PAINT = {
4780
+ done: style.green,
4686
4781
  running: style.yellow,
4687
- in_progress: style.yellow,
4782
+ quiet: style.cyan,
4688
4783
  failed: style.red,
4689
4784
  blocked: style.red,
4690
- awaiting_user: style.yellow,
4691
- cancelled: style.grey,
4692
- skipped: style.grey
4785
+ awaiting: style.yellow,
4786
+ todo: (text) => text
4693
4787
  };
4694
4788
  var STATUS_ICON = {
4695
- completed: "\u2713",
4789
+ done: "\u2713",
4696
4790
  failed: "\u2717",
4697
4791
  blocked: "!",
4698
- awaiting_user: "?",
4699
- cancelled: "\u2212",
4700
- skipped: "\u2212",
4701
- approved: "\xB7",
4702
- pending: "\xB7"
4703
- };
4704
- var AWAITING_LABEL = {
4705
- input: "waiting for your input",
4706
- checkpoint: "checkpoint",
4707
- conflict: "merge conflict",
4708
- "files-changed": "changed tracked files"
4792
+ awaiting: "?",
4793
+ quiet: "~",
4794
+ todo: "\xB7"
4709
4795
  };
4710
- function approvalLabel(count) {
4711
- return count > 1 ? `waiting for approval (${count})` : "waiting for approval";
4712
- }
4713
- var IDLE_ICON = "~";
4796
+ var inRow = (label) => label.charAt(0).toLowerCase() + label.slice(1);
4714
4797
  var FIRST_ROW_LINE = 2;
4715
4798
  function planLayout(state, rows, cols) {
4716
4799
  const done = state.tasks.filter((t) => t.status === "completed").length;
4717
4800
  const lines = [style.bold(`Plan ${done}/${state.tasks.length}`), ""];
4718
4801
  const rowSpans = [];
4802
+ const orderOf = new Map(state.tasks.map((task) => [task.id, task.order]));
4719
4803
  let editorLine;
4720
4804
  planRows(state).forEach((row, i) => {
4721
4805
  const taskStart = lines.length;
4722
- const renderedTask = taskLines(state, row, i, cols);
4806
+ const renderedTask = taskLines(state, row, i, cols, orderOf);
4723
4807
  if (row.task.id === state.expandedTaskId && renderedTask.editorLine !== void 0) {
4724
4808
  editorLine = taskStart + renderedTask.editorLine;
4725
4809
  }
@@ -4775,18 +4859,16 @@ function planScrollExtent(state) {
4775
4859
  }
4776
4860
  return planLayout(state, bodyRows(state), cols);
4777
4861
  }
4778
- function taskLines(state, row, index, cols) {
4779
- 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) });
4780
4865
  const selected = state.focus === "plan" && index === state.selectedTask;
4781
4866
  const expanded = state.expandedTaskId === task.id;
4782
- const running = isTaskRunning(task);
4783
- const idle = running && task.status === "in_progress" && task.idleSince != null;
4784
- const paint = idle ? style.cyan : STATUS_MARK[task.status] ?? ((t) => t);
4785
- const rawIcon = idle ? IDLE_ICON : running ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[task.status] ?? "\xB7";
4786
- const icon = paint(rawIcon);
4787
- 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");
4788
4870
  const caret = selected ? style.cyan("\u276F") : " ";
4789
- const orderLabel = parent ? (0, import_core23.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
4871
+ const orderLabel = parent ? row.orderLabel : row.orderLabel.padStart(2);
4790
4872
  const indent = parent ? " " : "";
4791
4873
  const head = `${indent}${caret} ${icon} ${orderLabel} ${kind} `;
4792
4874
  const titleRoom = Math.max(1, cols - width(head));
@@ -4803,58 +4885,51 @@ function taskLines(state, row, index, cols) {
4803
4885
  });
4804
4886
  const model = task.assignedModel?.modelLabel ?? (task.type === "ai" ? "default model" : "");
4805
4887
  const effort = task.type === "ai" ? `effort: ${task.assignedModel?.thinkingEffort ?? "default"}` : "";
4806
- const modeInfo = task.type === "ai" ? modesForTask(state.modesByRunner, task).find((m) => m.id === task.taskMode) : void 0;
4807
- 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") : ""}` : "";
4808
4889
  const runner = task.assignedRunner ?? "";
4809
4890
  const bodyPad = parent ? " " : " ";
4810
4891
  const structured = task.transport?.kind === "structured";
4811
- const waiting = task.status === "awaiting_user" && task.awaitingReason ? AWAITING_LABEL[task.awaitingReason] : "";
4812
- 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) : "";
4813
4893
  const meta = [activity, runner, structured ? "structured" : "", model].filter(Boolean).join(" \xB7 ");
4814
4894
  if (meta) lines.push(style.grey(truncate(`${bodyPad}${meta}`, cols)));
4815
4895
  if (task.transport?.fallback) lines.push(style.yellow(truncate(`${bodyPad}terminal: ${task.transport.fallback}`, cols)));
4816
4896
  if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
4817
- if (task.isolation?.state === "conflict") {
4818
- const where = task.isolation.conflictRepo && task.isolation.conflictRepo !== "." ? ` in ${task.isolation.conflictRepo}` : "";
4819
- 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})` : "";
4820
4900
  lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where}${files} \u2014 its work is kept on its own branch`, cols)));
4821
4901
  }
4822
- if (task.isolation?.state === "repairing") {
4823
- const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core23.capConflictFiles)(task.isolation.conflictFiles)}` : "";
4824
- 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})` : "";
4825
4905
  lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
4826
4906
  }
4827
- if (task.mergeGate?.length) {
4828
- const deps = task.mergeGate.map((id) => {
4829
- const dep = state.tasks.find((t) => t.id === id);
4830
- return dep ? `#${dep.order}` : id;
4831
- });
4832
- 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)));
4833
4909
  }
4834
- if (task.status === "awaiting_user" && task.awaitingReason === "files-changed") {
4910
+ if (row.opsChangedFiles) {
4835
4911
  lines.push(style.yellow(truncate(`${bodyPad}\u26A0 an ops task changed tracked files \u2014 check them, then m done or /retry`, cols)));
4836
4912
  }
4837
- if (task.isolation?.state === "integrated" && task.isolation.repairedFiles?.length) {
4838
- 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)));
4839
4915
  }
4840
4916
  let editorLine;
4841
4917
  if (expanded) {
4842
4918
  lines.push(style.grey(`${bodyPad}${task.status.replace(/_/g, " ")}`));
4843
4919
  if (task.isolation && task.isolation.state !== "none" && task.isolation.branch) {
4844
4920
  lines.push(...taskText("Branch", task.isolation.branch, cols, bodyPad));
4845
- const repos = taskRepoNames(task.isolation);
4846
- 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));
4847
4922
  if (task.isolation.state !== "integrated" && task.isolation.worktree) {
4848
4923
  lines.push(...taskText("Worktree", task.isolation.worktree, cols, bodyPad));
4849
4924
  }
4850
4925
  }
4851
- if (task.ops) {
4926
+ if (row.kind === "ops") {
4852
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));
4853
4928
  }
4854
- if (task.forcedPastGate?.length) {
4855
- 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));
4856
4931
  }
4857
- if (modeInfo?.autonomous) {
4932
+ if (row.autonomous) {
4858
4933
  lines.push(...taskText("Autonomy", "Runs without permission prompts (Full). Change the level with /auto.", cols, bodyPad));
4859
4934
  }
4860
4935
  const editableSeed = task.prompt ?? task.description ?? task.title;
@@ -4868,10 +4943,8 @@ function taskLines(state, row, index, cols) {
4868
4943
  editorLine = lines.length + 1 + cp.line;
4869
4944
  lines.push(...taskPromptLines(wrapped, cp, bodyPad));
4870
4945
  }
4871
- if (task.dependencies.length > 0) {
4872
- const orderById = new Map(state.tasks.map((candidate) => [candidate.id, candidate.order]));
4873
- const dependencies = task.dependencies.map((id) => orderById.has(id) ? `#${orderById.get(id)}` : id).join(", ");
4874
- 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));
4875
4948
  }
4876
4949
  lines.push(style.grey(`${bodyPad}enter save \xB7 esc cancel`));
4877
4950
  lines.push(style.grey(truncate(`${bodyPad}then R runner \xB7 o model \xB7 e effort \xB7 M mode \xB7 D deps`, cols)));
@@ -4918,7 +4991,13 @@ function helpLayout(rows, cols) {
4918
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")
4919
4992
  );
4920
4993
  body.push(
4921
- 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")
4922
5001
  );
4923
5002
  const lines = body.map((line) => truncate(line, Math.max(1, cols - 2)));
4924
5003
  const room = Math.max(1, rows - 2);
@@ -4929,7 +5008,7 @@ function helpScrollMax(state) {
4929
5008
  }
4930
5009
 
4931
5010
  // src/tui/render.ts
4932
- var import_core24 = require("@ordewell/core");
5011
+ var import_core25 = require("@ordewell/core");
4933
5012
  function render(state) {
4934
5013
  const { rows, cols } = state;
4935
5014
  const rowsForBody = bodyRows(state);
@@ -5194,8 +5273,8 @@ function renderHandoff(state, overlay, rows, cols) {
5194
5273
  return frame(`Diff \u2014 ${handoffBranch(handoff)}`, [...shown, "", style.grey(`${more}enter or esc goes back`)], rows, cols);
5195
5274
  }
5196
5275
  const landed = handoff.landed.length === 0 ? [style.grey("Nothing landed on it.")] : handoff.landed.map((task) => {
5197
- const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core24.capConflictFiles)(task.repairedFiles)})`) : "";
5198
- 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}`;
5199
5278
  });
5200
5279
  const actions = handoffActions(handoff).map((action, index) => {
5201
5280
  const active = index === overlay.index;
@@ -5284,7 +5363,10 @@ function frame(title, body, rows, cols) {
5284
5363
  }
5285
5364
 
5286
5365
  // src/tui/reducer.ts
5287
- 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");
5288
5370
 
5289
5371
  // src/tui/reducers/shared.ts
5290
5372
  function step(state, effects = []) {
@@ -5333,10 +5415,10 @@ function handleApprovalKey(state, overlay, key) {
5333
5415
  }
5334
5416
 
5335
5417
  // src/tui/reducers/commands.ts
5336
- var import_core28 = require("@ordewell/core");
5418
+ var import_core30 = require("@ordewell/core");
5337
5419
 
5338
5420
  // src/tui/reducers/pickers.ts
5339
- var import_core25 = require("@ordewell/core");
5421
+ var import_core26 = require("@ordewell/core");
5340
5422
  var import_plan_utils2 = require("@ordewell/core/plan-utils");
5341
5423
  var DEFAULT_EFFORT = "__runner_default__";
5342
5424
  function picker(title, items, action, extra = {}) {
@@ -5355,11 +5437,11 @@ function modelDetail(m) {
5355
5437
  function providerErrorHint(state) {
5356
5438
  const failed = Object.keys(state.providerErrors ?? {});
5357
5439
  if (failed.length === 0) return void 0;
5358
- 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);
5359
5441
  return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
5360
5442
  }
5361
5443
  function plannerModelItems(state) {
5362
- const runner = (0, import_core25.runnerForProvider)(state.plannerProvider);
5444
+ const runner = (0, import_core26.runnerForProvider)(state.plannerProvider);
5363
5445
  if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
5364
5446
  const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
5365
5447
  id: m.id,
@@ -5373,7 +5455,7 @@ function plannerModelItems(state) {
5373
5455
  return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
5374
5456
  }
5375
5457
  function plannerEffortItems(state) {
5376
- const runner = (0, import_core25.runnerForProvider)(state.plannerProvider);
5458
+ const runner = (0, import_core26.runnerForProvider)(state.plannerProvider);
5377
5459
  if (!runner) {
5378
5460
  return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
5379
5461
  }
@@ -5437,7 +5519,7 @@ function pickerItemsFor(state, action) {
5437
5519
  if (action.kind === "set-task-deps") {
5438
5520
  return (0, import_plan_utils2.dependencyCandidates)(state.tasks, action.taskId).map((candidate) => ({
5439
5521
  id: candidate.id,
5440
- label: `#${candidate.order} ${candidate.title}`,
5522
+ label: (0, import_core26.titledTaskRef)(candidate),
5441
5523
  detail: candidate.status === "completed" ? "already completed" : void 0
5442
5524
  }));
5443
5525
  }
@@ -5482,26 +5564,29 @@ function pickerItemsFor(state, action) {
5482
5564
  }
5483
5565
  function plannerItems(state) {
5484
5566
  const installed = new Set(state.runners.map((r) => r.id));
5485
- const agents = import_core25.CLI_PROVIDERS.map((id) => {
5486
- const runner = (0, import_core25.runnerForProvider)(id);
5567
+ const agents = import_core26.CLI_PROVIDERS.map((id) => {
5568
+ const runner = (0, import_core26.runnerForProvider)(id);
5487
5569
  const ready = installed.has(runner);
5488
5570
  return {
5489
5571
  id,
5490
- label: import_core25.ALL_PROVIDERS[id].label,
5572
+ label: import_core26.ALL_PROVIDERS[id].label,
5491
5573
  detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
5492
5574
  selected: state.plannerProvider === id,
5493
5575
  disabled: !ready
5494
5576
  };
5495
5577
  });
5496
- 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) => ({
5497
5579
  id,
5498
- label: import_core25.ALL_PROVIDERS[id].label,
5499
- detail: import_core25.ALL_PROVIDERS[id].apiKeyEnvVar,
5580
+ label: import_core26.ALL_PROVIDERS[id].label,
5581
+ detail: import_core26.ALL_PROVIDERS[id].apiKeyEnvVar,
5500
5582
  selected: state.plannerProvider === id
5501
5583
  }));
5502
5584
  return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
5503
5585
  }
5504
5586
 
5587
+ // src/tui/reducers/planPane.ts
5588
+ var import_core28 = require("@ordewell/core");
5589
+
5505
5590
  // src/tui/reducers/pointer.ts
5506
5591
  var WHEEL_NOTCH = 3;
5507
5592
  function pageNotch(viewport) {
@@ -5578,13 +5663,13 @@ function handleMouseSelect(state, key) {
5578
5663
  }
5579
5664
 
5580
5665
  // src/tui/reducers/taskView.ts
5581
- var import_core26 = require("@ordewell/core");
5666
+ var import_core27 = require("@ordewell/core");
5582
5667
  function openTaskView(state, sessionId, taskId) {
5583
5668
  const task = findTask(state.tasks, taskId);
5584
5669
  if (!task) return step(say(state, "system", `No task ${taskId} in this plan.`));
5585
5670
  const taskView = {
5586
5671
  taskId,
5587
- view: import_core26.EMPTY_TASK_LOG,
5672
+ view: import_core27.EMPTY_TASK_LOG,
5588
5673
  attempts: [],
5589
5674
  attempt: 0,
5590
5675
  pending: [],
@@ -5617,13 +5702,13 @@ function applyLive(tv, attempt, events) {
5617
5702
  }
5618
5703
  next = {
5619
5704
  ...next,
5620
- view: import_core26.EMPTY_TASK_LOG,
5705
+ view: import_core27.EMPTY_TASK_LOG,
5621
5706
  attempt,
5622
5707
  attempts: next.attempts.includes(attempt) ? next.attempts : [...next.attempts, attempt]
5623
5708
  };
5624
5709
  }
5625
5710
  if (attempt < next.attempt) return next;
5626
- return clampQueueIndex({ ...next, view: events.reduce(import_core26.reduceTaskLog, next.view) });
5711
+ return clampQueueIndex({ ...next, view: events.reduce(import_core27.reduceTaskLog, next.view) });
5627
5712
  }
5628
5713
  function taskLogAbandoned(state) {
5629
5714
  const tv = state.taskView;
@@ -5643,15 +5728,22 @@ function taskLogArrived(state, action) {
5643
5728
  function announceApprovals(state, action) {
5644
5729
  if (state.taskView?.taskId === action.taskId) return state;
5645
5730
  const task = findTask(state.tasks, action.taskId);
5646
- 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);
5647
5732
  return spoken !== state ? { ...spoken, scroll: state.scroll } : spoken;
5648
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
+ }
5649
5741
  function taskLogLoaded(state, action) {
5650
5742
  const tv = state.taskView;
5651
5743
  if (!tv || tv.taskId !== action.taskId) return state;
5652
5744
  const loaded = action.attempt >= 1 ? {
5653
5745
  ...tv,
5654
- view: (0, import_core26.replayTaskLog)(action.events),
5746
+ view: (0, import_core27.replayTaskLog)(action.events),
5655
5747
  attempts: action.attempts ?? tv.attempts,
5656
5748
  attempt: action.attempt
5657
5749
  } : { ...tv, attempts: action.attempts ?? tv.attempts };
@@ -5673,6 +5765,29 @@ function answerApproval(state, tv, sessionId, decision) {
5673
5765
  const next = note ? { ...state, editor: { ...state.editor, text: "", cursor: 0 } } : state;
5674
5766
  return step(next, [{ type: "answerTaskApproval", sessionId, approvalId: approval.approvalId, answer }]);
5675
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
+ }
5676
5791
  function handleTaskViewKey(state, key) {
5677
5792
  const tv = state.taskView;
5678
5793
  if (!tv || !state.sessionId) return null;
@@ -5692,14 +5807,16 @@ function handleTaskViewKey(state, key) {
5692
5807
  if (key.name === "ctrl-r") {
5693
5808
  const queued = tv.view.queued[tv.queuedIndex];
5694
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."));
5695
5811
  return step(state, [{ type: "removeTaskMessage", sessionId: state.sessionId, taskId: tv.taskId, messageId: queued.id }]);
5696
5812
  }
5813
+ if (key.name === "ctrl-s") return sendNow(state, tv, state.sessionId);
5697
5814
  if (key.name === "ctrl-x") {
5698
5815
  return step(state, [{ type: "interruptTask", sessionId: state.sessionId, taskId: tv.taskId }]);
5699
5816
  }
5700
- 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");
5701
5818
  if (key.name === "ctrl-t") return answerApproval(state, tv, state.sessionId, "allowForTask");
5702
- 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");
5703
5820
  if (key.name === "ctrl-n" || key.name === "ctrl-p") {
5704
5821
  const count = tv.view.queued.length;
5705
5822
  if (count === 0) return null;
@@ -5720,7 +5837,7 @@ function openTaskRunnerPicker(state, task) {
5720
5837
  ...state,
5721
5838
  overlay: {
5722
5839
  kind: "picker",
5723
- 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, {
5724
5841
  hint: "Changing the runner re-picks this task model, effort and mode for it."
5725
5842
  })
5726
5843
  }
@@ -5728,14 +5845,12 @@ function openTaskRunnerPicker(state, task) {
5728
5845
  );
5729
5846
  }
5730
5847
  function confirmRemoveTask(state, task) {
5731
- const dependents = (0, import_plan_utils3.dependentsOf)(state.tasks, task.id);
5732
- const named = dependents.map((t) => `#${t.order} ${t.title}`).join(", ");
5733
5848
  return step({
5734
5849
  ...state,
5735
5850
  overlay: {
5736
5851
  kind: "confirm",
5737
- title: `Remove #${task.order} ${task.title}?`,
5738
- 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.",
5739
5854
  action: { kind: "remove-task", taskId: task.id }
5740
5855
  }
5741
5856
  });
@@ -5744,26 +5859,22 @@ function toggleTaskOps(state, task, subtask, to = !task.ops) {
5744
5859
  if (!state.sessionId) return fail3(state, "No active plan.");
5745
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.");
5746
5861
  if (subtask) return fail3(state, "A subtask runs with its parent; make the parent an ops task instead.");
5747
- 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.`);
5748
5863
  return step(state, [{
5749
5864
  type: "updateTask",
5750
5865
  sessionId: state.sessionId,
5751
5866
  taskId: task.id,
5752
5867
  changes: { ops: to },
5753
- 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.`
5754
5869
  }]);
5755
5870
  }
5756
5871
  function confirmForceStartPastGate(state, task) {
5757
- const named = (task.mergeGate ?? []).map((id) => {
5758
- const dep = findTask(state.tasks, id);
5759
- return dep ? `#${dep.order} ${dep.title}` : id;
5760
- });
5761
5872
  return step({
5762
5873
  ...state,
5763
5874
  overlay: {
5764
5875
  kind: "confirm",
5765
- title: `Force start #${task.order} ${task.title}?`,
5766
- 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"),
5767
5878
  action: { kind: "force-start-gated", taskId: task.id }
5768
5879
  }
5769
5880
  });
@@ -5771,12 +5882,12 @@ function confirmForceStartPastGate(state, task) {
5771
5882
  function openTaskDepsPicker(state, task) {
5772
5883
  const action = { kind: "set-task-deps", taskId: task.id };
5773
5884
  const items = pickerItemsFor(state, action);
5774
- 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.`);
5775
5886
  return step({
5776
5887
  ...state,
5777
5888
  overlay: {
5778
5889
  kind: "picker",
5779
- 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, {
5780
5891
  hint: "Only tasks earlier in the plan can be dependencies.",
5781
5892
  multi: true,
5782
5893
  chosen: task.dependencies.filter((id) => items.some((i) => i.id === id))
@@ -5796,7 +5907,7 @@ function openTaskModePicker(state, task) {
5796
5907
  ...state,
5797
5908
  overlay: {
5798
5909
  kind: "picker",
5799
- 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, {
5800
5911
  hint: `Modes declared by ${task.assignedRunner}.`
5801
5912
  })
5802
5913
  }
@@ -5811,7 +5922,7 @@ function openTaskModelPicker(state, task) {
5811
5922
  ...state,
5812
5923
  overlay: {
5813
5924
  kind: "picker",
5814
- 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, {
5815
5926
  hint: task.assignedRunner ? `Showing models discovered for ${task.assignedRunner}.` : "Choose the model this task will run with."
5816
5927
  })
5817
5928
  }
@@ -5828,7 +5939,7 @@ function openTaskEffortPicker(state, task) {
5828
5939
  ...state,
5829
5940
  overlay: {
5830
5941
  kind: "picker",
5831
- 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, {
5832
5943
  hint: `${task.assignedModel.modelLabel} \xB7 choose runner default or a supported effort`
5833
5944
  })
5834
5945
  }
@@ -5845,7 +5956,7 @@ function assignTaskModel(state, sessionId, task, model) {
5845
5956
  // JSON drops `undefined`; null is intentional here so changing models can
5846
5957
  // also clear a stale legacy top-level effort on the persisted task.
5847
5958
  changes: { assignedModel, thinkingEffort: assignedModel.thinkingEffort ?? null },
5848
- message: `Task #${task.order} model set to ${model.label}.`
5959
+ message: `Task ${(0, import_plan_utils3.taskRef)(task)} model set to ${model.label}.`
5849
5960
  }]);
5850
5961
  }
5851
5962
  function assignTaskRunner(state, sessionId, task, runner, runnerLabel) {
@@ -5854,7 +5965,7 @@ function assignTaskRunner(state, sessionId, task, runner, runnerLabel) {
5854
5965
  sessionId,
5855
5966
  taskId: task.id,
5856
5967
  changes: { assignedRunner: runner },
5857
- message: `Task #${task.order} runner set to ${runnerLabel}.`
5968
+ message: `Task ${(0, import_plan_utils3.taskRef)(task)} runner set to ${runnerLabel}.`
5858
5969
  }]);
5859
5970
  }
5860
5971
  function assignTaskMode(state, sessionId, task, mode, modeLabel) {
@@ -5863,7 +5974,7 @@ function assignTaskMode(state, sessionId, task, mode, modeLabel) {
5863
5974
  sessionId,
5864
5975
  taskId: task.id,
5865
5976
  changes: { taskMode: mode },
5866
- message: `Task #${task.order} mode set to ${modeLabel}.`
5977
+ message: `Task ${(0, import_plan_utils3.taskRef)(task)} mode set to ${modeLabel}.`
5867
5978
  }]);
5868
5979
  }
5869
5980
  function assignTaskEffort(state, sessionId, task, thinkingEffort) {
@@ -5873,7 +5984,7 @@ function assignTaskEffort(state, sessionId, task, thinkingEffort) {
5873
5984
  sessionId,
5874
5985
  taskId: task.id,
5875
5986
  changes: { assignedModel, thinkingEffort: thinkingEffort ?? null },
5876
- 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"}.`
5877
5988
  }]);
5878
5989
  }
5879
5990
  function taskModelCommand(state, args) {
@@ -5975,9 +6086,6 @@ function taskActionEffect(state, sessionId, taskId, action) {
5975
6086
  const watch = SPAWNS_RUNNER.includes(action) && state.status !== "executing";
5976
6087
  return watch ? { type: "taskAction", sessionId, taskId, action, watch: true } : { type: "taskAction", sessionId, taskId, action };
5977
6088
  }
5978
- function markAction(task) {
5979
- return task.status === "completed" ? "uncomplete" : "complete";
5980
- }
5981
6089
  function handlePlanKey(state, key) {
5982
6090
  const editingTask = state.expandedTaskId ? findTask(state.tasks, state.expandedTaskId) : void 0;
5983
6091
  if (editingTask && state.taskEditor) return handleTaskEditKey(state, editingTask, state.taskEditor, key);
@@ -6012,7 +6120,7 @@ function handlePlanKey(state, key) {
6012
6120
  if (!state.sessionId || key.name !== "char") return step(state);
6013
6121
  if (key.char === "E") return step(state, [{ type: "execute", sessionId: state.sessionId }]);
6014
6122
  if (key.char === "S") return step(state, [{ type: "stopExecution", sessionId: state.sessionId }]);
6015
- 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 ?? ""];
6016
6124
  if (action === "force-start" && task.mergeGate?.length) return confirmForceStartPastGate(state, task);
6017
6125
  if (action) {
6018
6126
  return step(state, [taskActionEffect(state, state.sessionId, task.id, action)]);
@@ -6047,14 +6155,14 @@ function commitTaskEdit(state, task, editor) {
6047
6155
  sessionId: state.sessionId,
6048
6156
  taskId: task.id,
6049
6157
  changes: { prompt },
6050
- message: `Task #${task.order} prompt updated.`
6158
+ message: `Task ${(0, import_core28.taskRef)(task)} prompt updated.`
6051
6159
  }]);
6052
6160
  }
6053
6161
 
6054
6162
  // src/tui/reducers/turnQueue.ts
6055
- var import_core27 = require("@ordewell/core");
6163
+ var import_core29 = require("@ordewell/core");
6056
6164
  function drainQueue(state, settled) {
6057
- const next = (0, import_core27.drainNext)(state.queuedPrompts);
6165
+ const next = (0, import_core29.drainNext)(state.queuedPrompts);
6058
6166
  const ended = disarmStop({ ...settled, stopRequested: false });
6059
6167
  if (!next) return step(ended);
6060
6168
  const spoken = say(ended, "user", next.text);
@@ -6073,14 +6181,14 @@ function stopPlanning(state, sessionId) {
6073
6181
  return step(queueToEditor(cutTurn({ ...dismissed, stopArmed: false, stopRequested: true })), [{ type: "cancelPlanning", sessionId }]);
6074
6182
  }
6075
6183
  function queueToEditor(state) {
6076
- const all = (0, import_core27.unsendAll)(state.queuedPrompts);
6184
+ const all = (0, import_core29.unsendAll)(state.queuedPrompts);
6077
6185
  if (!all) return state;
6078
6186
  return { ...state, queuedPrompts: all.rest, editor: { ...state.editor, text: all.text, cursor: all.text.length } };
6079
6187
  }
6080
6188
  function unsendLatest(state) {
6081
- const latest = (0, import_core27.unsendLatest)(state.queuedPrompts);
6189
+ const latest = (0, import_core29.unsendLatest)(state.queuedPrompts);
6082
6190
  if (!latest) return step(state);
6083
- const text = (0, import_core27.aheadOfDraft)(latest.text, state.editor.text);
6191
+ const text = (0, import_core29.aheadOfDraft)(latest.text, state.editor.text);
6084
6192
  return step({
6085
6193
  ...state,
6086
6194
  queuedPrompts: latest.rest,
@@ -6097,7 +6205,7 @@ function plannerEscape(state) {
6097
6205
  }
6098
6206
 
6099
6207
  // src/tui/reducers/commands.ts
6100
- var KNOWN_PROVIDERS2 = Object.keys(import_core28.ALL_PROVIDERS);
6208
+ var KNOWN_PROVIDERS2 = Object.keys(import_core30.ALL_PROVIDERS);
6101
6209
  function runCommand(state, { name, args }) {
6102
6210
  if (!findCommand(name)) {
6103
6211
  return fail3(state, `Unknown command: /${name} \u2014 type /help to see what's available.`);
@@ -6184,6 +6292,14 @@ function runCommand(state, { name, args }) {
6184
6292
  if (!text) return fail3(state, "Usage: /continue <id> <message>");
6185
6293
  return continueTaskStep(state, sessionId, taskId, text);
6186
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
+ });
6187
6303
  case "task-runner":
6188
6304
  return taskRunnerCommand(state, args);
6189
6305
  case "task-model":
@@ -6255,8 +6371,8 @@ function newSession(state) {
6255
6371
  taskView: null,
6256
6372
  tasks: [],
6257
6373
  planApproved: false,
6258
- conversation: import_core28.EMPTY_CONVERSATION,
6259
- turnGate: import_core28.NO_TURN,
6374
+ conversation: import_core30.EMPTY_CONVERSATION,
6375
+ turnGate: import_core30.NO_TURN,
6260
6376
  selectedTask: 0,
6261
6377
  expandedTaskId: null,
6262
6378
  taskEditor: null,
@@ -6267,14 +6383,14 @@ function newSession(state) {
6267
6383
  // Prompts belong to the session that raised them; the old planner is gone
6268
6384
  // and its pending requests deny on their own timeout.
6269
6385
  pendingApprovals: [],
6270
- queuedPrompts: import_core28.EMPTY_HOLD,
6386
+ queuedPrompts: import_core30.EMPTY_HOLD,
6271
6387
  stopArmed: false,
6272
6388
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6273
6389
  }, closeEffects);
6274
6390
  }
6275
6391
  function setModel(state, args) {
6276
6392
  if (args[0] === "set" && args[1]) {
6277
- const runner = (0, import_core28.runnerForProvider)(state.plannerProvider);
6393
+ const runner = (0, import_core30.runnerForProvider)(state.plannerProvider);
6278
6394
  const known = runner ? modelsForRunner(state.models, runner) : [];
6279
6395
  if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
6280
6396
  return fail3(state, `${args[1]} was not discovered for ${runner}.`);
@@ -6347,16 +6463,16 @@ function setKey(state, args) {
6347
6463
  }
6348
6464
  return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
6349
6465
  }
6350
- const items = import_core28.PROVIDER_PRIORITY.map((id) => ({
6466
+ const items = import_core30.PROVIDER_PRIORITY.map((id) => ({
6351
6467
  id,
6352
- label: import_core28.ALL_PROVIDERS[id].label,
6353
- detail: import_core28.ALL_PROVIDERS[id].apiKeyEnvVar,
6468
+ label: import_core30.ALL_PROVIDERS[id].label,
6469
+ detail: import_core30.ALL_PROVIDERS[id].apiKeyEnvVar,
6354
6470
  selected: state.configuredProviders.includes(id)
6355
6471
  }));
6356
6472
  return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
6357
6473
  }
6358
6474
  function keyPrompt(provider) {
6359
- const meta = import_core28.ALL_PROVIDERS[provider];
6475
+ const meta = import_core30.ALL_PROVIDERS[provider];
6360
6476
  return {
6361
6477
  kind: "prompt",
6362
6478
  title: `${meta.label} API key`,
@@ -6433,19 +6549,19 @@ function runners(state, args) {
6433
6549
  }
6434
6550
  function setMaxParallel(state, arg) {
6435
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.`));
6436
- const limit = (0, import_core28.parseMaxParallel)(arg);
6552
+ const limit = (0, import_core30.parseMaxParallel)(arg);
6437
6553
  if (limit === null) return fail3(state, "Usage: /parallel [<number of tasks, 1 or more>]");
6438
6554
  return step({ ...state, maxParallel: limit }, [{ type: "setMaxParallel", limit }]);
6439
6555
  }
6440
6556
  function setAutonomous(state, arg) {
6441
- if (arg === void 0) return step(say(state, "system", `Autonomy level: ${(0, import_core28.autonomyLevelLabel)(state.autonomous)} \u2014 /auto full or /auto guarded changes it for new plans.`));
6442
- const enabled = (0, import_core28.parseAutonomyLevel)(arg);
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);
6443
6559
  if (enabled === null) return fail3(state, "Usage: /auto [full|guarded]");
6444
6560
  return step({ ...state, autonomous: enabled }, [{ type: "setAutonomous", enabled }]);
6445
6561
  }
6446
6562
  function setTransport(state, arg) {
6447
6563
  const value = arg?.toLowerCase() ?? (state.runnerTransport === "structured" ? "terminal" : "structured");
6448
- 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]");
6449
6565
  return step(state, [{ type: "setTransport", transport: value }]);
6450
6566
  }
6451
6567
  function setMouseCapture(state, arg) {
@@ -6622,7 +6738,7 @@ function choose(state, picker2, item) {
6622
6738
  sessionId: state.sessionId,
6623
6739
  taskId: task.id,
6624
6740
  changes: { dependencies: picker2.chosen },
6625
- 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.`
6626
6742
  }]);
6627
6743
  }
6628
6744
  if (!item) return step(state);
@@ -6699,7 +6815,7 @@ function choose(state, picker2, item) {
6699
6815
  }
6700
6816
 
6701
6817
  // src/tui/reducers/incoming.ts
6702
- var import_core29 = require("@ordewell/core");
6818
+ var import_core32 = require("@ordewell/core");
6703
6819
  var planText = (value, fallback = "") => sanitize(String(value ?? fallback));
6704
6820
  var planLabel = (value, fallback = "") => planText(value, fallback).replace(/\n+/g, " ");
6705
6821
  var asRecord = (value) => typeof value === "object" && value !== null ? value : {};
@@ -6730,7 +6846,7 @@ function toTaskView2(t, index) {
6730
6846
  prompt: typeof t.prompt === "string" ? sanitize(t.prompt) : void 0,
6731
6847
  type: t.type === "user" ? "user" : "ai",
6732
6848
  status: planLabel(t.status, "pending"),
6733
- 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,
6734
6850
  dependencies: Array.isArray(t.dependencies) ? t.dependencies.map((id) => planLabel(id)) : [],
6735
6851
  assignedRunner: typeof t.assignedRunner === "string" ? planLabel(t.assignedRunner) : void 0,
6736
6852
  taskMode: typeof t.taskMode === "string" ? planLabel(t.taskMode) : void 0,
@@ -6763,12 +6879,12 @@ function applySettings(state, settings) {
6763
6879
  plannerProvider: typeof settings.aiProvider === "string" ? settings.aiProvider : state.plannerProvider,
6764
6880
  plannerEffort: typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : state.plannerEffort,
6765
6881
  maxParallel: typeof settings.maxParallel === "number" ? settings.maxParallel : state.maxParallel,
6766
- runnerTransport: (0, import_core29.isRunnerTransport)(settings.runnerTransport) ? settings.runnerTransport : state.runnerTransport,
6882
+ runnerTransport: (0, import_core32.isRunnerTransport)(settings.runnerTransport) ? settings.runnerTransport : state.runnerTransport,
6767
6883
  allowlist: settings.modelAllowlist && typeof settings.modelAllowlist === "object" ? settings.modelAllowlist : state.allowlist
6768
6884
  };
6769
6885
  }
6770
6886
  function runLabel(tasks, gate = null) {
6771
- const running = tasks.filter(isTaskRunning);
6887
+ const running = tasks.filter(import_core23.isTaskRunning);
6772
6888
  if (running.length > 0) {
6773
6889
  const [first] = running;
6774
6890
  const name = first.assignedRunner ? `${first.title} \xB7 ${first.assignedRunner}` : first.title;
@@ -6834,13 +6950,13 @@ function reduceAction(state, action) {
6834
6950
  goal: "",
6835
6951
  taskView: null,
6836
6952
  pendingApprovals: [],
6837
- queuedPrompts: import_core30.EMPTY_HOLD,
6953
+ queuedPrompts: import_core33.EMPTY_HOLD,
6838
6954
  stopArmed: false,
6839
6955
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6840
6956
  });
6841
6957
  case "chatRestored":
6842
6958
  if (stale(state, action.sessionId)) return step(state);
6843
- 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 });
6844
6960
  case "planUpdated": {
6845
6961
  if (stale(state, action.sessionId)) return step(state);
6846
6962
  const isolation = isolationOfPlan(action.plan);
@@ -6887,14 +7003,17 @@ function reduceAction(state, action) {
6887
7003
  case "taskStarted": {
6888
7004
  if (stale(state, action.sessionId)) return step(state);
6889
7005
  const existing = findTask(state.tasks, action.taskId);
6890
- if (existing && isTaskRunning(existing)) return step(state);
6891
- const tasks = state.tasks.map((t) => t.id === action.taskId && !isTaskRunning(t) ? { ...t, status: "in_progress" } : t);
6892
- 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));
6893
7009
  return step({ ...spoken, tasks, status: "executing", busyLabel: runLabel(tasks) });
6894
7010
  }
7011
+ case "taskCheckpoint":
7012
+ if (stale(state, action.sessionId)) return step(state);
7013
+ return step(announceCheckpoint(state, action));
6895
7014
  case "taskStatus": {
6896
7015
  if (stale(state, action.sessionId) || !state.tasks.some((t) => t.id === action.taskId)) return step(state);
6897
- 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);
6898
7017
  const status = runStatus(state, tasks);
6899
7018
  return step({ ...state, status, tasks, busyLabel: status === "executing" ? runLabel(tasks, state.gate) : state.busyLabel });
6900
7019
  }
@@ -6909,13 +7028,14 @@ function reduceAction(state, action) {
6909
7028
  const isolation = update.isolation ?? t.isolation;
6910
7029
  const transport = update.transport;
6911
7030
  const awaitingReason = update.awaitingReason;
7031
+ const checkpoint = update.checkpoint;
6912
7032
  const continuable = update.continuable === true;
6913
7033
  const awaitingApproval = update.awaitingApproval;
6914
7034
  const mergeGate = update.mergeGate;
6915
7035
  const forcedPastGate = update.forcedPastGate;
6916
- 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)) {
6917
7037
  changed = true;
6918
- 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 };
6919
7039
  }
6920
7040
  return t;
6921
7041
  });
@@ -6998,7 +7118,7 @@ function reduceAction(state, action) {
6998
7118
  busyLabel: "",
6999
7119
  planApproved: false,
7000
7120
  pendingApprovals: [],
7001
- queuedPrompts: import_core30.EMPTY_HOLD,
7121
+ queuedPrompts: import_core33.EMPTY_HOLD,
7002
7122
  stopArmed: false,
7003
7123
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
7004
7124
  });
@@ -7065,7 +7185,7 @@ function sameGate(a, b) {
7065
7185
  return a.paused === b.paused && JSON.stringify(a.handoff) === JSON.stringify(b.handoff);
7066
7186
  }
7067
7187
  function runStatus(state, tasks) {
7068
- if (tasks.some(isTaskRunning)) return "executing";
7188
+ if (tasks.some(import_core23.isTaskRunning)) return "executing";
7069
7189
  if (tasks.some((t) => t.status === "awaiting_user") || state.gate?.paused) return "executing";
7070
7190
  return plannerInFlight(state) ? state.status : "idle";
7071
7191
  }
@@ -7134,7 +7254,7 @@ function submit(state) {
7134
7254
  return step(cleared, [{ type: "sendTaskMessage", sessionId: state.sessionId, taskId, text }]);
7135
7255
  }
7136
7256
  if (plannerInFlight(state)) {
7137
- return step({ ...cleared, queuedPrompts: (0, import_core30.holdPrompt)(state.queuedPrompts, text) });
7257
+ return step({ ...cleared, queuedPrompts: (0, import_core33.holdPrompt)(state.queuedPrompts, text) });
7138
7258
  }
7139
7259
  const spoken = say(cleared, "user", text);
7140
7260
  const effect = state.sessionId ? { type: "sendMessage", sessionId: state.sessionId, message: text } : { type: "startConversation", goal: text };
@@ -7194,10 +7314,10 @@ function createApp(deps) {
7194
7314
 
7195
7315
  // src/tui/effects.ts
7196
7316
  var import_child_process4 = require("child_process");
7197
- var import_core32 = require("@ordewell/core");
7317
+ var import_core35 = require("@ordewell/core");
7198
7318
 
7199
7319
  // src/tui/inbound.ts
7200
- var import_core31 = require("@ordewell/core");
7320
+ var import_core34 = require("@ordewell/core");
7201
7321
  var CONVERSATION_TYPES = /* @__PURE__ */ new Set([
7202
7322
  "planner_message",
7203
7323
  "planner_turn_started",
@@ -7412,7 +7532,8 @@ function dispatchLifecycle(dispatch, event, sessionId) {
7412
7532
  idleSince: task.idleSince ?? null,
7413
7533
  isolation: task.isolation,
7414
7534
  transport: task.transport,
7415
- 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) } : {},
7416
7537
  ...task.continuable ? { continuable: true } : {},
7417
7538
  awaitingApproval: typeof task.awaitingApproval === "number" && task.awaitingApproval > 0 ? task.awaitingApproval : void 0,
7418
7539
  ...task.mergeGate?.length ? { mergeGate: task.mergeGate.map(String) } : {},
@@ -7426,10 +7547,10 @@ function dispatchLifecycle(dispatch, event, sessionId) {
7426
7547
  case "task_started":
7427
7548
  dispatch({ type: "taskStarted", taskId: String(event.taskId), title: String(event.title ?? event.taskId), runner: event.runner, sessionId });
7428
7549
  return;
7429
- // The extension shows these in its checkpoint panel; here they are
7430
- // 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.
7431
7552
  case "checkpoint":
7432
- 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 });
7433
7554
  return;
7434
7555
  case "review_needed":
7435
7556
  dispatch({ type: "notice", message: "Plan needs your sign-off \u2014 /approve to continue." });
@@ -7587,20 +7708,18 @@ async function perform(effect, deps) {
7587
7708
  return;
7588
7709
  case "taskAction": {
7589
7710
  const { sessionId, taskId, action } = effect;
7590
- 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);
7591
7712
  await (effect.watch ? withExecutionStream(deps, sessionId, request) : request());
7592
7713
  await refreshPlan(deps, sessionId);
7593
7714
  return;
7594
7715
  }
7595
- case "addTask":
7596
- await api.addTask(effect.sessionId, {
7597
- title: effect.title,
7598
- description: effect.title,
7599
- prompt: effect.title,
7600
- type: "ai"
7601
- });
7716
+ case "addTask": {
7717
+ const fields = (0, import_core35.newTaskFields)(effect.title);
7718
+ if (!fields) return;
7719
+ await api.addTask(effect.sessionId, { ...fields });
7602
7720
  await refreshPlan(deps, effect.sessionId);
7603
7721
  return;
7722
+ }
7604
7723
  case "updateTask":
7605
7724
  await api.updateTask(effect.sessionId, effect.taskId, effect.changes);
7606
7725
  await refreshPlan(deps, effect.sessionId);
@@ -7627,6 +7746,13 @@ async function perform(effect, deps) {
7627
7746
  }
7628
7747
  return;
7629
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;
7630
7756
  // A task with a saved log ran structured, whatever its `transport` says
7631
7757
  // after a reload; it has no tmux window, so open the log rather than fail.
7632
7758
  case "openTaskTerminal": {
@@ -7676,6 +7802,16 @@ async function perform(effect, deps) {
7676
7802
  if (!removed) dispatch({ type: "notice", message: "That message was already delivered to the task." });
7677
7803
  return;
7678
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
+ }
7679
7815
  case "interruptTask":
7680
7816
  await api.interruptTask(effect.sessionId, effect.taskId);
7681
7817
  dispatch({ type: "notice", message: "Interrupting the task\u2026" });
@@ -7699,7 +7835,7 @@ async function perform(effect, deps) {
7699
7835
  dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
7700
7836
  return;
7701
7837
  case "setPlanner": {
7702
- const meta = import_core32.ALL_PROVIDERS[effect.provider];
7838
+ const meta = import_core35.ALL_PROVIDERS[effect.provider];
7703
7839
  if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
7704
7840
  const env = { AI_PROVIDER: effect.provider };
7705
7841
  const settings = await api.updateSettings({ env });
@@ -7711,7 +7847,7 @@ async function perform(effect, deps) {
7711
7847
  });
7712
7848
  dispatch({
7713
7849
  type: "notice",
7714
- 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)}`
7715
7851
  });
7716
7852
  await loadModels(deps);
7717
7853
  return;
@@ -7723,7 +7859,7 @@ async function perform(effect, deps) {
7723
7859
  dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
7724
7860
  return;
7725
7861
  case "setApiKey": {
7726
- const meta = import_core32.ALL_PROVIDERS[effect.provider];
7862
+ const meta = import_core35.ALL_PROVIDERS[effect.provider];
7727
7863
  if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
7728
7864
  await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
7729
7865
  deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
@@ -7761,7 +7897,7 @@ async function perform(effect, deps) {
7761
7897
  return;
7762
7898
  case "setAutonomous":
7763
7899
  deps.setEnvVar("ORDEWELL_AUTONOMOUS_MODE", String(effect.enabled));
7764
- 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.` });
7765
7901
  return;
7766
7902
  case "setMouseCapture":
7767
7903
  deps.setMouseCapture(effect.enabled);
@@ -7895,7 +8031,7 @@ function osc52(text) {
7895
8031
  function copySelection(deps, text) {
7896
8032
  if (!text) return;
7897
8033
  const lines = text.split("\n").length;
7898
- const command = (0, import_core32.clipboardCopyCommand)(deps.hasBin);
8034
+ const command = (0, import_core35.clipboardCopyCommand)(deps.hasBin);
7899
8035
  if (command) {
7900
8036
  try {
7901
8037
  (deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
@@ -8012,6 +8148,7 @@ var CONTROL2 = {
8012
8148
  "": "ctrl-o",
8013
8149
  "": "ctrl-p",
8014
8150
  "": "ctrl-r",
8151
+ "": "ctrl-s",
8015
8152
  "": "ctrl-t",
8016
8153
  "": "ctrl-u",
8017
8154
  "": "ctrl-w",
@@ -8285,7 +8422,7 @@ async function handleTui(subArgs) {
8285
8422
  }
8286
8423
  const workspace = flag(subArgs, "--workspace") || process.cwd();
8287
8424
  try {
8288
- (0, import_core33.assertWorkspaceExists)(workspace);
8425
+ (0, import_core36.assertWorkspaceExists)(workspace);
8289
8426
  } catch {
8290
8427
  console.error(
8291
8428
  `Workspace "${workspace}" does not exist.
@@ -8294,7 +8431,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
8294
8431
  );
8295
8432
  process.exit(1);
8296
8433
  }
8297
- registerSkillCommands((0, import_core33.createSkillsService)(workspace).listSkills());
8434
+ registerSkillCommands((0, import_core36.createSkillsService)(workspace).listSkills());
8298
8435
  const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
8299
8436
  const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
8300
8437
  const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
@@ -8311,7 +8448,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
8311
8448
  workspace,
8312
8449
  port,
8313
8450
  dispatch: (action) => app.dispatch(action),
8314
- newSessionId: () => (0, import_core33.mintSessionId)(),
8451
+ newSessionId: () => (0, import_core36.mintSessionId)(),
8315
8452
  setEnvVar: (key, value) => {
8316
8453
  writeEnvVar(findEnvFile(), key, value);
8317
8454
  process.env[key] = value;
@@ -8402,6 +8539,7 @@ var COMMANDS = {
8402
8539
  "retry": handleRetry,
8403
8540
  "cancel": handleCancel,
8404
8541
  continue: handleContinue,
8542
+ checkpoint: handleCheckpoint,
8405
8543
  "add-task": handleAddTask,
8406
8544
  "remove-task": handleRemoveTask,
8407
8545
  "task-runner": handleTaskRunner,
@@ -8435,7 +8573,7 @@ function cliVersion() {
8435
8573
  // src/main.ts
8436
8574
  var TUI_FLAGS = /* @__PURE__ */ new Set(["--workspace", "--port"]);
8437
8575
  async function main() {
8438
- (0, import_core34.migrateOldConfigDir)();
8576
+ (0, import_core37.migrateOldConfigDir)();
8439
8577
  loadEnvFile();
8440
8578
  const argv = process.argv.slice(2);
8441
8579
  if (TUI_FLAGS.has(argv[0] ?? "")) argv.unshift("tui");