@ordewell/cli 0.7.0 → 0.7.2

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 +562 -582
  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_core38 = 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)
@@ -399,15 +406,30 @@ var import_http2 = __toESM(require("http"));
399
406
  var import_ws = __toESM(require("ws"));
400
407
  var import_core3 = require("@ordewell/core");
401
408
  var DEFAULT_HTTP_TIMEOUT_MS = 15 * 60 * 1e3;
402
- var WorkspaceInitNeededError = class extends Error {
403
- constructor(message, workspace) {
409
+ var DaemonError = class extends Error {
410
+ constructor(message, status, code) {
404
411
  super(message);
412
+ this.status = status;
413
+ this.code = code;
414
+ this.name = "DaemonError";
415
+ }
416
+ status;
417
+ code;
418
+ };
419
+ var WorkspaceInitNeededError = class extends DaemonError {
420
+ constructor(message, workspace) {
421
+ super(message, 400, "workspace_not_a_project");
405
422
  this.workspace = workspace;
406
423
  this.name = "WorkspaceInitNeededError";
407
424
  }
408
425
  workspace;
409
426
  };
410
427
  var SESSION_PATH = /^\/api\/(?:plans|sessions)\/([^/?]+)/;
428
+ function daemonError(status, data, fallback) {
429
+ const { error, code, workspace } = typeof data === "object" && data !== null ? data : {};
430
+ if (code === "workspace_not_a_project") return new WorkspaceInitNeededError(error || "Not a project directory", workspace ?? "");
431
+ return new DaemonError(error || fallback, status, code);
432
+ }
411
433
  var ApiClient = class _ApiClient {
412
434
  /** Where a CLI invocation's sessions live, when a client is built without one: `--workspace`, else the cwd. */
413
435
  static defaultWorkspace;
@@ -415,6 +437,7 @@ var ApiClient = class _ApiClient {
415
437
  workspace;
416
438
  /** A session's open execution stream, so a new one can end it: one run is reported once. */
417
439
  executionStreams = /* @__PURE__ */ new Map();
440
+ executionStreamEnds = /* @__PURE__ */ new WeakMap();
418
441
  constructor(port, workspace) {
419
442
  this.port = port || DEFAULT_PORT;
420
443
  this.workspace = workspace ?? _ApiClient.defaultWorkspace ?? process.cwd();
@@ -429,7 +452,7 @@ var ApiClient = class _ApiClient {
429
452
  }
430
453
  /**
431
454
  * A daemon holds only the sessions adopted since it started, so after a
432
- * restart every session-scoped call answered "Session not found" until the
455
+ * restart every session-scoped call answered "session not found" until the
433
456
  * user ran `ordewell sessions load`. The saved session is adopted here
434
457
  * instead — idempotent for one the daemon still holds — and the call made
435
458
  * once more; a 404 means the first attempt never ran, so the retry cannot
@@ -438,11 +461,17 @@ var ApiClient = class _ApiClient {
438
461
  async httpRequest(method, urlPath, body) {
439
462
  const res = await this.rawRequest(method, urlPath, body);
440
463
  const sessionId = urlPath.match(SESSION_PATH)?.[1];
441
- const missing = res.status === 404 && res.data?.error === "Session not found";
464
+ const missing = res.status === 404 && res.data?.code === "session_not_found";
442
465
  if (!missing || !sessionId || urlPath.split("?")[0].endsWith("/load")) return res;
443
466
  const adopted2 = await this.rawRequest("POST", `/api/sessions/${sessionId}/load?workspace=${encodeURIComponent(this.workspace)}`);
444
467
  return adopted2.status === 200 ? this.rawRequest(method, urlPath, body) : res;
445
468
  }
469
+ /** One request whose only good answer is a 200: anything else becomes a {@link DaemonError} carrying the daemon's code. */
470
+ async call(method, urlPath, fallback, body) {
471
+ const res = await this.httpRequest(method, urlPath, body);
472
+ if (res.status !== 200) throw daemonError(res.status, res.data, fallback);
473
+ return res.data;
474
+ }
446
475
  rawRequest(method, urlPath, body) {
447
476
  return new Promise((resolve3, reject) => {
448
477
  const url = new URL(urlPath, `http://127.0.0.1:${this.port}`);
@@ -485,15 +514,12 @@ var ApiClient = class _ApiClient {
485
514
  });
486
515
  }
487
516
  async generatePlan(goal, runners2, workspace, sessionId = (0, import_core3.mintSessionId)()) {
488
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/generate`, {
517
+ const { plan, models, modelsByRunner } = await this.call("POST", `/api/plans/${sessionId}/generate`, "Plan generation failed", {
489
518
  goal,
490
519
  runners: runners2 || void 0,
491
520
  workspace
492
521
  });
493
- if (res.status !== 200) {
494
- throw new Error(res.data?.error || "Plan generation failed");
495
- }
496
- return { sessionId, plan: res.data.plan, models: res.data.models, modelsByRunner: res.data.modelsByRunner };
522
+ return { sessionId, plan, models, modelsByRunner };
497
523
  }
498
524
  /**
499
525
  * Open the ADR-0002 planner dialogue. Unlike `generatePlan`, this path honours
@@ -501,58 +527,30 @@ var ApiClient = class _ApiClient {
501
527
  * (empty `tasks`, last word in `conversationHistory`) rather than a committed plan.
502
528
  */
503
529
  async startConversation(sessionId, goal, runners2, workspace, allowInit) {
504
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/converse/start`, {
530
+ const res = await this.call("POST", `/api/plans/${sessionId}/converse/start`, "Planning failed", {
505
531
  goal,
506
532
  runners: runners2 || void 0,
507
533
  workspace,
508
534
  allowInit
509
535
  });
510
- if (res.status !== 200) {
511
- if (res.data?.code === "workspace_not_a_project") {
512
- throw new WorkspaceInitNeededError(res.data.error || "Not a project directory", res.data.workspace || workspace || "");
513
- }
514
- throw new Error(res.data?.error || "Planning failed");
515
- }
516
- return res.data.plan;
536
+ return res.plan;
517
537
  }
518
538
  async sendConversationMessage(sessionId, message) {
519
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/converse/message`, { message });
520
- if (res.status !== 200) {
521
- throw new Error(res.data?.error || "Planner message failed");
522
- }
523
- return res.data.plan;
539
+ const res = await this.call("POST", `/api/plans/${sessionId}/converse/message`, "Planner message failed", { message });
540
+ return res.plan;
524
541
  }
525
- async getRunners() {
526
- const res = await this.httpRequest("GET", "/api/runners");
527
- if (res.status !== 200) {
528
- throw new Error(res.data?.error || "Failed to fetch runners");
529
- }
530
- return res.data;
542
+ getRunners() {
543
+ return this.call("GET", "/api/runners", "Failed to fetch runners");
531
544
  }
532
545
  /** The full provider catalog the daemon has discovered (used by the TUI model picker). */
533
- async getModels() {
534
- const res = await this.httpRequest("GET", "/api/models");
535
- if (res.status !== 200) {
536
- throw new Error(res.data?.error || "Failed to fetch models");
537
- }
538
- return res.data;
546
+ getModels() {
547
+ return this.call("GET", "/api/models", "Failed to fetch models");
539
548
  }
540
- async setRunnerEnabled(runner, enabled) {
541
- const res = await this.httpRequest("PUT", `/api/runners/${runner}`, { enabled });
542
- if (res.status !== 200) {
543
- throw new Error(res.data?.error || `Failed to ${enabled ? "enable" : "disable"} ${runner}`);
544
- }
545
- return res.data;
549
+ setRunnerEnabled(runner, enabled) {
550
+ return this.call("PUT", `/api/runners/${runner}`, `Failed to ${enabled ? "enable" : "disable"} ${runner}`, { enabled });
546
551
  }
547
- async executePlan(sessionId) {
548
- const res = await this.httpRequest(
549
- "POST",
550
- `/api/plans/${sessionId}/execute`
551
- );
552
- if (res.status !== 200) {
553
- throw new Error(res.data?.error || "Execute failed");
554
- }
555
- return res.data;
552
+ executePlan(sessionId) {
553
+ return this.call("POST", `/api/plans/${sessionId}/execute`, "Execute failed");
556
554
  }
557
555
  /**
558
556
  * Sign off a plan waiting on `review_needed` and let it continue.
@@ -561,189 +559,111 @@ var ApiClient = class _ApiClient {
561
559
  * `resetForRun`) before approving, which is right for starting a plan and
562
560
  * wrong for releasing one that is already part-way through a review pause.
563
561
  */
564
- async approveReview(sessionId) {
565
- const res = await this.httpRequest(
566
- "POST",
567
- `/api/plans/${sessionId}/review/approve`
568
- );
569
- if (res.status !== 200) {
570
- throw new Error(res.data?.error || "Approve failed");
571
- }
572
- return res.data;
562
+ approveReview(sessionId) {
563
+ return this.call("POST", `/api/plans/${sessionId}/review/approve`, "Approve failed");
573
564
  }
574
- async markTaskComplete(sessionId, taskId) {
575
- const res = await this.httpRequest(
576
- "POST",
577
- `/api/plans/${sessionId}/tasks/${taskId}/complete`
578
- );
579
- if (res.status !== 200) {
580
- throw new Error(res.data?.error || "Mark complete failed");
581
- }
582
- return res.data;
565
+ markTaskComplete(sessionId, taskId) {
566
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/complete`, "Mark complete failed");
583
567
  }
584
- async markTaskIncomplete(sessionId, taskId) {
585
- const res = await this.httpRequest(
586
- "POST",
587
- `/api/plans/${sessionId}/tasks/${taskId}/uncomplete`
588
- );
589
- if (res.status !== 200) {
590
- throw new Error(res.data?.error || "Mark not done failed");
591
- }
592
- return res.data;
568
+ markTaskIncomplete(sessionId, taskId) {
569
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/uncomplete`, "Mark not done failed");
593
570
  }
594
571
  /** run | force-start | retry | cancel — real orchestrator work, not a status patch. */
595
- async taskControl(sessionId, taskId, action) {
596
- const res = await this.httpRequest(
597
- "POST",
598
- `/api/plans/${sessionId}/tasks/${taskId}/${action}`
599
- );
600
- if (res.status !== 200) {
601
- throw new Error(res.data?.error || `${action} failed`);
602
- }
603
- return res.data;
572
+ taskControl(sessionId, taskId, action) {
573
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/${action}`, `${action} failed`);
604
574
  }
605
575
  /** A user message to a structured task: delivered now if it waits for input, else queued behind its turn. */
606
- async sendTaskMessage(sessionId, taskId, text) {
607
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages`, { text });
608
- if (res.status !== 200) {
609
- throw new Error(res.data?.error || "Send message failed");
610
- }
611
- return res.data;
576
+ sendTaskMessage(sessionId, taskId, text) {
577
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages`, "Send message failed", { text });
578
+ }
579
+ /** Force send: interrupt the task's running turn and deliver `text` next, ahead of anything queued (ADR-0023, F1). */
580
+ forceSendTaskMessage(sessionId, taskId, text) {
581
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages/now`, "Send now failed", { text });
582
+ }
583
+ /** Force send a message still queued; `sent` is false once the runner already has it. */
584
+ forceSendQueuedTaskMessage(sessionId, taskId, messageId) {
585
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages/${encodeURIComponent(messageId)}/now`, "Send now failed");
612
586
  }
613
587
  /** Continue a finished structured task in its saved session, with `text` as its next turn. */
614
- async continueTask(sessionId, taskId, text) {
615
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/continue`, { text });
616
- if (res.status !== 200) {
617
- throw new Error(res.data?.error || "Continue failed");
618
- }
619
- return res.data;
588
+ continueTask(sessionId, taskId, text) {
589
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/continue`, "Continue failed", { text });
620
590
  }
621
591
  /** `removed` is false once the message was already delivered. */
622
- async removeQueuedTaskMessage(sessionId, taskId, messageId) {
623
- const res = await this.httpRequest(
624
- "DELETE",
625
- `/api/plans/${sessionId}/tasks/${taskId}/messages/${encodeURIComponent(messageId)}`
626
- );
627
- if (res.status !== 200) {
628
- throw new Error(res.data?.error || "Remove message failed");
629
- }
630
- return res.data;
592
+ removeQueuedTaskMessage(sessionId, taskId, messageId) {
593
+ return this.call("DELETE", `/api/plans/${sessionId}/tasks/${taskId}/messages/${encodeURIComponent(messageId)}`, "Remove message failed");
631
594
  }
632
- async interruptTask(sessionId, taskId) {
633
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/interrupt`);
634
- if (res.status !== 200) {
635
- throw new Error(res.data?.error || "Interrupt failed");
636
- }
637
- return res.data;
595
+ interruptTask(sessionId, taskId) {
596
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/interrupt`, "Interrupt failed");
638
597
  }
639
- async addTask(sessionId, task) {
640
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks`, task);
641
- if (res.status !== 200) {
642
- throw new Error(res.data?.error || "Add task failed");
643
- }
644
- return res.data;
598
+ /** Let a task go on from the checkpoint it waits at; refused, with the reason, when none waits. */
599
+ approveTaskCheckpoint(sessionId, taskId) {
600
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/checkpoint/approve`, "Approve checkpoint failed");
645
601
  }
646
- async updateTask(sessionId, taskId, changes) {
647
- const res = await this.httpRequest("PUT", `/api/plans/${sessionId}/tasks/${taskId}`, changes);
648
- if (res.status !== 200) {
649
- throw new Error(res.data?.error || "Update task failed");
650
- }
651
- return res.data;
602
+ /** Turn a task back at its checkpoint; `reason` is what the agent is told. */
603
+ rejectTaskCheckpoint(sessionId, taskId, reason) {
604
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/checkpoint/reject`, "Reject checkpoint failed", reason ? { reason } : {});
652
605
  }
653
- async removeTask(sessionId, taskId) {
654
- const res = await this.httpRequest("DELETE", `/api/plans/${sessionId}/tasks/${taskId}`);
655
- if (res.status !== 200) {
656
- throw new Error(res.data?.error || "Remove task failed");
657
- }
658
- return res.data;
606
+ addTask(sessionId, task) {
607
+ return this.call("POST", `/api/plans/${sessionId}/tasks`, "Add task failed", task);
608
+ }
609
+ updateTask(sessionId, taskId, changes) {
610
+ return this.call("PUT", `/api/plans/${sessionId}/tasks/${taskId}`, "Update task failed", changes);
611
+ }
612
+ removeTask(sessionId, taskId) {
613
+ return this.call("DELETE", `/api/plans/${sessionId}/tasks/${taskId}`, "Remove task failed");
659
614
  }
660
615
  /**
661
616
  * Make a saved session live on the server. `getSession` only reads the file;
662
617
  * until the session is adopted there is no orchestrator behind it, so
663
- * execute/retry/cancel answer "Session not found".
618
+ * execute/retry/cancel answer `session_not_found`.
664
619
  */
665
620
  async adoptSession(sessionId, workspace) {
666
621
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
667
- const res = await this.httpRequest("POST", `/api/sessions/${sessionId}/load${qs}`);
668
- if (res.status !== 200) {
669
- throw new Error(res.data?.error || "Failed to load session");
670
- }
671
- return { plan: res.data.plan, goal: res.data.goal ?? "" };
622
+ const res = await this.call("POST", `/api/sessions/${sessionId}/load${qs}`, "Failed to load session");
623
+ return { plan: res.plan, goal: res.goal ?? "" };
672
624
  }
673
- async deleteSession(sessionId, workspace) {
625
+ deleteSession(sessionId, workspace) {
674
626
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
675
- const res = await this.httpRequest("DELETE", `/api/sessions/${sessionId}${qs}`);
676
- if (res.status !== 200) {
677
- throw new Error(res.data?.error || "Delete session failed");
678
- }
679
- return res.data;
627
+ return this.call("DELETE", `/api/sessions/${sessionId}${qs}`, "Delete session failed");
680
628
  }
681
629
  /** Copy the conversation and its tasks into a new session the daemon has already adopted. */
682
- async forkConversation(sessionId) {
683
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/fork`);
684
- if (res.status !== 200) {
685
- throw new Error(res.data?.error || "Fork failed");
686
- }
687
- return { sessionId: res.data.sessionId, goal: res.data.goal, plan: res.data.plan };
630
+ forkConversation(sessionId) {
631
+ return this.call("POST", `/api/plans/${sessionId}/conversation/fork`, "Fork failed");
688
632
  }
689
633
  async rewindTargets(sessionId) {
690
- const res = await this.httpRequest("GET", `/api/plans/${sessionId}/conversation/rewind-targets`);
691
- if (res.status !== 200) {
692
- throw new Error(res.data?.error || "Failed to list rewind targets");
693
- }
694
- return res.data.targets;
634
+ const res = await this.call("GET", `/api/plans/${sessionId}/conversation/rewind-targets`, "Failed to list rewind targets");
635
+ return res.targets;
695
636
  }
696
637
  /**
697
638
  * Fork the conversation from just before the user message at `index` (a
698
639
  * transcript position) into a session the daemon has already adopted. The
699
640
  * original is left as it was; `rewoundMessage` is that message in full.
700
641
  */
701
- async rewindConversation(sessionId, index) {
702
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/rewind`, { index });
703
- if (res.status !== 200) {
704
- throw new Error(res.data?.error || "Rewind failed");
705
- }
706
- return { sessionId: res.data.sessionId, goal: res.data.goal, plan: res.data.plan, rewoundMessage: res.data.rewoundMessage };
642
+ rewindConversation(sessionId, index) {
643
+ return this.call("POST", `/api/plans/${sessionId}/conversation/rewind`, "Rewind failed", { index });
707
644
  }
708
645
  /**
709
646
  * Condense the conversation into a summary. One planner call, so it can take
710
647
  * as long as a reply; a refusal or failure leaves the conversation as it was.
711
648
  */
712
- async compactConversation(sessionId) {
713
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/compact`);
714
- if (res.status !== 200) {
715
- throw new Error(res.data?.error || "Compaction failed");
716
- }
717
- return { plan: res.data.plan, summary: res.data.summary, keptMessages: res.data.keptMessages };
649
+ compactConversation(sessionId) {
650
+ return this.call("POST", `/api/plans/${sessionId}/conversation/compact`, "Compaction failed");
718
651
  }
719
652
  /** The dependencies a task waits on at its merge gate (ADR-0020); empty when nothing gates it. */
720
653
  async getMergeGate(sessionId, taskId) {
721
- const res = await this.httpRequest(
722
- "GET",
723
- `/api/plans/${sessionId}/tasks/${taskId}/merge-gate`
724
- );
725
- if (res.status !== 200) {
726
- throw new Error(res.data?.error || "Could not read the merge gate");
727
- }
728
- return res.data.mergeGate ?? [];
654
+ const res = await this.call("GET", `/api/plans/${sessionId}/tasks/${taskId}/merge-gate`, "Could not read the merge gate");
655
+ return res.mergeGate ?? [];
729
656
  }
730
657
  async reviewRunDiff(sessionId) {
731
- const res = await this.httpRequest("GET", `/api/plans/${sessionId}/isolation/diff`);
732
- if (res.status !== 200) {
733
- throw new Error(res.data?.error || "Could not read the diff");
734
- }
735
- return res.data.diff;
658
+ const res = await this.call("GET", `/api/plans/${sessionId}/isolation/diff`, "Could not read the diff");
659
+ return res.diff;
736
660
  }
737
661
  /**
738
662
  * A conflict or a block is an outcome, not an error. Passed on whole: which
739
663
  * repos blocked the merge, or landed before it stopped, is the answer.
740
664
  */
741
- async mergeRun(sessionId) {
742
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/isolation/merge`);
743
- if (res.status !== 200) {
744
- throw new Error(res.data?.error || "Merge failed");
745
- }
746
- return res.data;
665
+ mergeRun(sessionId) {
666
+ return this.call("POST", `/api/plans/${sessionId}/isolation/merge`, "Merge failed");
747
667
  }
748
668
  discardRun(sessionId) {
749
669
  return this.isolationAction(sessionId, "discard", "Discard failed");
@@ -758,17 +678,11 @@ var ApiClient = class _ApiClient {
758
678
  return this.isolationAction(sessionId, "run-without", "Could not continue without isolation");
759
679
  }
760
680
  async isolationAction(sessionId, segment, failure) {
761
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/isolation/${segment}`);
762
- if (res.status !== 200) {
763
- throw new Error(res.data?.error || failure);
764
- }
681
+ await this.call("POST", `/api/plans/${sessionId}/isolation/${segment}`, failure);
765
682
  }
766
683
  async resolveConflictAsTask(sessionId, taskId) {
767
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/resolve-conflict`);
768
- if (res.status !== 200) {
769
- throw new Error(res.data?.error || "Could not add a resolver task");
770
- }
771
- return res.data.plan;
684
+ const res = await this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/resolve-conflict`, "Could not add a resolver task");
685
+ return res.plan;
772
686
  }
773
687
  async closeSession(sessionId) {
774
688
  const res = await this.httpRequest("POST", `/api/sessions/${sessionId}/close`);
@@ -783,42 +697,26 @@ var ApiClient = class _ApiClient {
783
697
  const res = await this.httpRequest("POST", `/api/plans/${sessionId}/planning/stop`);
784
698
  return res.data;
785
699
  }
786
- async processQueued(sessionId) {
787
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/process-queued`);
788
- if (res.status !== 200) {
789
- throw new Error(res.data?.error || "Process queued failed");
790
- }
791
- return res.data;
792
- }
793
700
  async getSessions(workspace) {
794
701
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
795
702
  const res = await this.httpRequest("GET", `/api/sessions${qs}`);
796
703
  return res.data || [];
797
704
  }
798
- async getSession(sessionId, workspace) {
705
+ getSession(sessionId, workspace) {
799
706
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
800
- const res = await this.httpRequest(
801
- "GET",
802
- `/api/sessions/${sessionId}${qs}`
803
- );
804
- if (res.status !== 200) {
805
- throw new Error(res.data?.error || "Session not found");
806
- }
807
- return res.data;
707
+ return this.call("GET", `/api/sessions/${sessionId}${qs}`, "Session not found");
808
708
  }
809
709
  /** The attempts of a structured task that have a saved log, oldest first. */
810
710
  async getTaskLogAttempts(sessionId, taskId, workspace) {
811
711
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
812
- const res = await this.httpRequest("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log${qs}`);
813
- if (res.status !== 200) throw new Error(res.data?.error || "Could not read the task log");
814
- return res.data.attempts;
712
+ const res = await this.call("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log${qs}`, "Could not read the task log");
713
+ return res.attempts;
815
714
  }
816
715
  /** One attempt's saved log, for `replayTaskLog`. */
817
716
  async getTaskLog(sessionId, taskId, attempt, workspace) {
818
717
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
819
- const res = await this.httpRequest("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log/${attempt}${qs}`);
820
- if (res.status !== 200) throw new Error(res.data?.error || "Could not read the task log");
821
- return res.data.events;
718
+ const res = await this.call("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log/${attempt}${qs}`, "Could not read the task log");
719
+ return res.events;
822
720
  }
823
721
  /**
824
722
  * The one place a session socket is built, so the token travels on both
@@ -849,8 +747,14 @@ var ApiClient = class _ApiClient {
849
747
  });
850
748
  return socket;
851
749
  }
750
+ /** Ends one stream `streamExecution` returned, leaving any newer stream the session has opened since alone. */
751
+ closeExecutionStream(stream) {
752
+ this.executionStreamEnds.get(stream)?.();
753
+ }
852
754
  streamExecution(sessionId, onEvent, onReady) {
853
- return new Promise((resolve3, reject) => {
755
+ let endStream = () => {
756
+ };
757
+ const stream = new Promise((resolve3, reject) => {
854
758
  this.executionStreams.get(sessionId)?.();
855
759
  const socket = this.openSessionSocket(sessionId);
856
760
  let resolved = false;
@@ -866,6 +770,7 @@ var ApiClient = class _ApiClient {
866
770
  resolve3();
867
771
  };
868
772
  this.executionStreams.set(sessionId, end);
773
+ endStream = end;
869
774
  socket.on("open", () => {
870
775
  opened = true;
871
776
  onReady?.();
@@ -882,8 +787,12 @@ var ApiClient = class _ApiClient {
882
787
  socket.on("error", (err) => {
883
788
  forget();
884
789
  if (!opened) onReady?.(err);
885
- if (!resolved) {
886
- resolved = true;
790
+ if (resolved) return;
791
+ resolved = true;
792
+ if (opened) {
793
+ socket.close();
794
+ resolve3("lost");
795
+ } else {
887
796
  reject(err);
888
797
  }
889
798
  });
@@ -892,10 +801,12 @@ var ApiClient = class _ApiClient {
892
801
  if (!opened) onReady?.(new Error("Execution stream closed before connecting"));
893
802
  if (!resolved) {
894
803
  resolved = true;
895
- resolve3();
804
+ resolve3("lost");
896
805
  }
897
806
  });
898
807
  });
808
+ this.executionStreamEnds.set(stream, endStream);
809
+ return stream;
899
810
  }
900
811
  /**
901
812
  * Answer an approval prompt: the planner's with a yes/no, a task runner's
@@ -908,7 +819,7 @@ var ApiClient = class _ApiClient {
908
819
  `/api/approvals/${encodeURIComponent(sessionId)}/${encodeURIComponent(approvalId)}`,
909
820
  typeof answer === "boolean" ? { granted: answer } : answer
910
821
  );
911
- if (status !== 200) throw new Error(`Failed to answer approval ${approvalId} (HTTP ${status})`);
822
+ if (status !== 200) throw new DaemonError(`Failed to answer approval ${approvalId} (HTTP ${status})`, status, data.code);
912
823
  return data;
913
824
  }
914
825
  /**
@@ -933,33 +844,17 @@ var ApiClient = class _ApiClient {
933
844
  });
934
845
  return { ready, close: () => socket.close() };
935
846
  }
936
- async getCommands() {
937
- const res = await this.httpRequest("GET", "/api/commands");
938
- if (res.status !== 200) {
939
- throw new Error(res.data?.error || "Failed to fetch commands");
940
- }
941
- return res.data;
847
+ getCommands() {
848
+ return this.call("GET", "/api/commands", "Failed to fetch commands");
942
849
  }
943
- async sendCommand(name, args = {}) {
944
- const res = await this.httpRequest("POST", `/api/commands/${name}`, { args });
945
- if (res.status !== 200) {
946
- throw new Error(res.data?.error || `Command ${name} failed`);
947
- }
948
- return res.data;
850
+ sendCommand(name, args = {}) {
851
+ return this.call("POST", `/api/commands/${name}`, `Command ${name} failed`, { args });
949
852
  }
950
- async getSettings() {
951
- const res = await this.httpRequest("GET", "/api/settings");
952
- if (res.status !== 200) {
953
- throw new Error(res.data?.error || "Failed to fetch settings");
954
- }
955
- return res.data;
853
+ getSettings() {
854
+ return this.call("GET", "/api/settings", "Failed to fetch settings");
956
855
  }
957
- async updateSettings(changes) {
958
- const res = await this.httpRequest("PATCH", "/api/settings", changes);
959
- if (res.status !== 200) {
960
- throw new Error(res.data?.error || "Failed to update settings");
961
- }
962
- return res.data;
856
+ updateSettings(changes) {
857
+ return this.call("PATCH", "/api/settings", "Failed to update settings", changes);
963
858
  }
964
859
  };
965
860
 
@@ -1207,11 +1102,14 @@ var import_core7 = require("@ordewell/core");
1207
1102
  function planTasks(plan) {
1208
1103
  return plan ? allTasksOf(plan) : [];
1209
1104
  }
1105
+ function runnersOf(plan, requested) {
1106
+ return "runners" in plan && plan.runners || requested;
1107
+ }
1210
1108
  function hasCommittedPlan(plan) {
1211
1109
  return planTasks(plan).length > 0;
1212
1110
  }
1213
1111
  function lastPlannerMessage(plan) {
1214
- const history = plan?.conversationHistory || [];
1112
+ const history = "conversationHistory" in plan && plan.conversationHistory || [];
1215
1113
  for (let i = history.length - 1; i >= 0; i--) {
1216
1114
  if (history[i].role === "assistant") return history[i].content;
1217
1115
  }
@@ -1273,7 +1171,7 @@ function printPlan(plan, sessionId, runners2, models) {
1273
1171
  return rp ? ` (${task.assignedModel.modelLabel} \xB7 ${label})` : ` (${task.assignedModel.modelLabel})`;
1274
1172
  };
1275
1173
  console.log(`
1276
- Plan: ${tasks.length} task${tasks.length === 1 ? "" : "s"} (${aiCount} AI, ${manCount} Manual) \u2014 ${(plan.runners || runners2).join(", ")}`);
1174
+ Plan: ${tasks.length} task${tasks.length === 1 ? "" : "s"} (${aiCount} AI, ${manCount} Manual) \u2014 ${runnersOf(plan, runners2).join(", ")}`);
1277
1175
  console.log(`Session: ${sessionId}
1278
1176
  `);
1279
1177
  for (const t of tasks) {
@@ -1420,7 +1318,7 @@ ${lastPlannerMessage(plan)}
1420
1318
  if (isTty && lastLineLength > 0) process.stderr.write("\r\x1B[2K");
1421
1319
  process.stderr.write("\n");
1422
1320
  printPlan(plan, sessionId, runners2, models);
1423
- saveLastSession(sessionId, goal, plan.runners || runners2, workspace);
1321
+ saveLastSession(sessionId, goal, runnersOf(plan, runners2), workspace);
1424
1322
  console.log(`
1425
1323
  Run 'ordewell run' to execute, 'ordewell status' to inspect, or 'ordewell tui' for the full UI.`);
1426
1324
  } catch (err) {
@@ -1528,9 +1426,6 @@ function repoResultLines(handoff) {
1528
1426
  return `${path}: ${n === 0 ? "nothing to merge" : `${n} task${n === 1 ? "" : "s"} landed`}`;
1529
1427
  });
1530
1428
  }
1531
- function taskRepoNames(isolation) {
1532
- return (isolation?.repos ?? []).filter((r) => r !== ".");
1533
- }
1534
1429
  function mergeOutcome(result, branch, group, repaired = []) {
1535
1430
  const ok = result.outcome === "merged";
1536
1431
  if (!group && result.outcome === "conflict") {
@@ -1541,23 +1436,27 @@ function mergeOutcome(result, branch, group, repaired = []) {
1541
1436
  }
1542
1437
 
1543
1438
  // src/utils/output.ts
1439
+ var import_core9 = require("@ordewell/core");
1544
1440
  function iconFor(status) {
1545
- switch (status) {
1546
- case "completed":
1441
+ switch ((0, import_core9.taskStatusKind)({ status })) {
1442
+ case "done":
1547
1443
  return "\x1B[32m\u2713\x1B[0m";
1548
- case "in_progress":
1444
+ case "running":
1445
+ case "quiet":
1549
1446
  return "\x1B[34m\u27F3\x1B[0m";
1550
1447
  case "failed":
1551
1448
  return "\x1B[31m\u2717\x1B[0m";
1552
1449
  case "blocked":
1553
1450
  return "\x1B[33m\u2298\x1B[0m";
1554
- default:
1451
+ case "awaiting":
1452
+ return "\x1B[33m?\x1B[0m";
1453
+ case "todo":
1555
1454
  return "\x1B[90m\u25CB\x1B[0m";
1556
1455
  }
1557
1456
  }
1558
1457
 
1559
1458
  // src/commands/run.ts
1560
- var import_core9 = require("@ordewell/core");
1459
+ var import_core10 = require("@ordewell/core");
1561
1460
 
1562
1461
  // src/commands/shared.ts
1563
1462
  var import_readline2 = require("readline");
@@ -1713,11 +1612,11 @@ async function followExecution(api, sessionId, start, onBlocked) {
1713
1612
  });
1714
1613
  const stream = api.streamExecution(sessionId, (event) => {
1715
1614
  if (event.type === "task_started") {
1716
- process.stderr.write(`\xB7 ${(0, import_core9.taskStartedNotice)(event.title, event.runner)}
1615
+ process.stderr.write(`\xB7 ${(0, import_core10.taskStartedNotice)(event.title, event.runner)}
1717
1616
  `);
1718
1617
  }
1719
1618
  if (event.type === "checkpoint") {
1720
- process.stderr.write(`\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core9.truncateCheckpointSummary)(event.summary)}
1619
+ process.stderr.write(`\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core10.truncateCheckpointSummary)(event.summary)} \u2014 \`ordewell checkpoint <id> approve|reject [reason]\` answers it
1721
1620
  `);
1722
1621
  }
1723
1622
  if (event.type === "status_update" && event.tasks) {
@@ -1762,7 +1661,11 @@ Run finished on ${handoffBranch(event)}${where} \u2014 ${n} task${n === 1 ? "" :
1762
1661
  void stream.catch(settleReady);
1763
1662
  await streamReady.catch(streamFailed);
1764
1663
  await start();
1765
- await Promise.race([stream, blockedSeen]).catch(streamFailed);
1664
+ const outcome = await Promise.race([stream, blockedSeen]).catch(streamFailed);
1665
+ if (outcome === "lost") {
1666
+ console.error("Lost the connection to the daemon \u2014 the run is no longer being followed, but the daemon may still be running it. `ordewell status` shows where it stands.");
1667
+ process.exit(1);
1668
+ }
1766
1669
  if (blocked === null) return;
1767
1670
  if (!onBlocked) {
1768
1671
  await api.stopExecution(sessionId);
@@ -1790,7 +1693,7 @@ async function handleRun(subArgs, injectedApi) {
1790
1693
 
1791
1694
  // src/commands/status.ts
1792
1695
  var import_fs4 = require("fs");
1793
- var import_core10 = require("@ordewell/core");
1696
+ var import_core11 = require("@ordewell/core");
1794
1697
  async function handleStatus(subArgs, injectedApi) {
1795
1698
  const sessionId = flag(subArgs, "--session-id");
1796
1699
  const workspace = flag(subArgs, "--workspace") || process.cwd();
@@ -1861,12 +1764,12 @@ function printOneSession(sessionId, meta, plan) {
1861
1764
  const icon = iconFor(t.status);
1862
1765
  const model = t.assignedModel ? ` (${t.assignedModel.modelLabel})` : "";
1863
1766
  const shortId = (t.id ?? "").slice(-8);
1864
- console.log(` ${(0, import_core10.taskOrderLabel)(t).padStart(2)}. ${icon} [${shortId}] ${t.title}${model}`);
1767
+ console.log(` ${(0, import_core11.taskOrderLabel)(t).padStart(2)}. ${icon} [${shortId}] ${t.title}${model}`);
1865
1768
  for (const sub of t.subtasks || []) {
1866
1769
  const subIcon = iconFor(sub.status);
1867
1770
  const subModel = sub.assignedModel ? ` (${sub.assignedModel.modelLabel})` : "";
1868
1771
  const subShortId = (sub.id ?? "").slice(-8);
1869
- console.log(` ${(0, import_core10.taskOrderLabel)(sub, t)}. ${subIcon} [${subShortId}] ${sub.title}${subModel}`);
1772
+ console.log(` ${(0, import_core11.taskOrderLabel)(sub, t)}. ${subIcon} [${subShortId}] ${sub.title}${subModel}`);
1870
1773
  }
1871
1774
  }
1872
1775
  }
@@ -1918,10 +1821,10 @@ async function handleWeb(subArgs) {
1918
1821
  }
1919
1822
 
1920
1823
  // src/commands/models.ts
1921
- var import_core12 = require("@ordewell/core");
1824
+ var import_core13 = require("@ordewell/core");
1922
1825
 
1923
1826
  // src/commands/model.ts
1924
- var import_core11 = require("@ordewell/core");
1827
+ var import_core12 = require("@ordewell/core");
1925
1828
  var USAGE = "Usage: ordewell model [show | set <model-id>]";
1926
1829
  async function handleModel(subArgs, injectedApi) {
1927
1830
  const [action, modelId] = positionals(subArgs);
@@ -1936,7 +1839,7 @@ async function handleModel(subArgs, injectedApi) {
1936
1839
  if (!modelId) fail(USAGE);
1937
1840
  const settings = await api.getSettings();
1938
1841
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
1939
- const runner = (0, import_core11.runnerForProvider)(provider);
1842
+ const runner = (0, import_core12.runnerForProvider)(provider);
1940
1843
  if (runner) {
1941
1844
  const known = (await fetchCatalog(api)).models.filter((m) => m.runners?.includes(runner));
1942
1845
  if (known.length > 0 && !known.some((m) => m.id === modelId)) {
@@ -1955,7 +1858,7 @@ async function showModels(api) {
1955
1858
  const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
1956
1859
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
1957
1860
  const current = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
1958
- const runner = (0, import_core11.runnerForProvider)(provider);
1861
+ const runner = (0, import_core12.runnerForProvider)(provider);
1959
1862
  const options = runner ? catalog.models.filter((m) => m.runners?.includes(runner)) : catalog.orchestratorModels;
1960
1863
  console.log(`
1961
1864
  Planner model: ${current || "(backend default)"}`);
@@ -1989,8 +1892,8 @@ async function handleModels(subArgs) {
1989
1892
  const rest = subArgs.filter((_, i) => i !== setIdx && i !== setIdx + 1);
1990
1893
  return handleModel(["set", subArgs[setIdx + 1], ...rest]);
1991
1894
  }
1992
- const config = new import_core12.EnvConfig();
1993
- const providers = (0, import_core12.configuredProviders)(config);
1895
+ const config = new import_core13.EnvConfig();
1896
+ const providers = (0, import_core13.configuredProviders)(config);
1994
1897
  if (providers.length === 0) {
1995
1898
  console.error("No providers configured. Set an API key for a provider, e.g.:");
1996
1899
  console.error(" OPENROUTER_API_KEY=... (OpenRouter)");
@@ -1999,19 +1902,19 @@ async function handleModels(subArgs) {
1999
1902
  console.error(" OPENAI_COMPATIBLE_BASE_URL=... (self-hosted / OpenAI-compatible)");
2000
1903
  process.exit(1);
2001
1904
  }
2002
- const { apiKeys, baseUrls } = (0, import_core12.collectProviderCredentials)(config);
1905
+ const { apiKeys, baseUrls } = (0, import_core13.collectProviderCredentials)(config);
2003
1906
  let models;
2004
1907
  let errors;
2005
1908
  try {
2006
- const result = await (0, import_core12.fetchAllProviderModels)({ apiKeys, baseUrls });
2007
- models = (0, import_core12.toOrchestratorOptions)(result.models, import_core12.ORCHESTRATOR_SHORTCUTS);
1909
+ const result = await (0, import_core13.fetchAllProviderModels)({ apiKeys, baseUrls });
1910
+ models = (0, import_core13.toOrchestratorOptions)(result.models, import_core13.ORCHESTRATOR_SHORTCUTS);
2008
1911
  errors = result.errors;
2009
1912
  } catch (err) {
2010
1913
  console.error(`Failed to fetch models: ${err instanceof Error ? err.message : String(err)}`);
2011
1914
  process.exit(1);
2012
1915
  return;
2013
1916
  }
2014
- const providerLabel = (p) => (0, import_core12.getProviderMeta)(p)?.label ?? p;
1917
+ const providerLabel = (p) => (0, import_core13.getProviderMeta)(p)?.label ?? p;
2015
1918
  console.log(
2016
1919
  `
2017
1920
  Available models (${models.length}) across ${providers.length} configured provider(s):`
@@ -2051,7 +1954,7 @@ ${providerLabel(provider)}`);
2051
1954
 
2052
1955
  // src/commands/setup.ts
2053
1956
  var import_fs6 = require("fs");
2054
- var import_core13 = require("@ordewell/core");
1957
+ var import_core14 = require("@ordewell/core");
2055
1958
  function handleSetup(subArgs) {
2056
1959
  const modelIdx = subArgs.indexOf("--model");
2057
1960
  const chosenModel = modelIdx !== -1 ? subArgs[modelIdx + 1] : null;
@@ -2123,15 +2026,15 @@ function handleSetup(subArgs) {
2123
2026
  # STEPFUN_API_KEY=...
2124
2027
  # SILICONFLOW_API_KEY=...
2125
2028
  `;
2126
- (0, import_core13.writePrivateFile)(envFile, content);
2029
+ (0, import_core14.writePrivateFile)(envFile, content);
2127
2030
  console.log(`Wrote: ${envFile}`);
2128
2031
  console.log("Review and edit the file, then run: ordewell tui");
2129
2032
  }
2130
2033
 
2131
2034
  // src/commands/plugins.ts
2132
- var import_core14 = require("@ordewell/core");
2035
+ var import_core15 = require("@ordewell/core");
2133
2036
  function handlePlugins(subArgs) {
2134
- const registry = new import_core14.RunnerRegistry();
2037
+ const registry = new import_core15.RunnerRegistry();
2135
2038
  registry.loadUserPlugins();
2136
2039
  const sub = subArgs[0];
2137
2040
  if (sub === "list" || !sub) {
@@ -2167,7 +2070,7 @@ ${plugins.length} plugin(s):
2167
2070
  process.exit(1);
2168
2071
  }
2169
2072
  try {
2170
- const resolved = (0, import_core14.classifyPluginSource)(source);
2073
+ const resolved = (0, import_core15.classifyPluginSource)(source);
2171
2074
  const manifest = resolved.kind === "git" ? registry.installFromGit(resolved.url) : registry.installFromPath(resolved.path);
2172
2075
  console.log(`Installed plugin: ${manifest.name} v${manifest.version}`);
2173
2076
  } catch (err) {
@@ -2345,6 +2248,7 @@ async function handleTransport(subArgs, api) {
2345
2248
  }
2346
2249
 
2347
2250
  // src/commands/task-control.ts
2251
+ var import_core16 = require("@ordewell/core");
2348
2252
  var PAST_TENSE = {
2349
2253
  run: "Task started.",
2350
2254
  "force-start": "Task force-started.",
@@ -2387,7 +2291,7 @@ async function confirmPastGate(subArgs, api, sessionId, taskId, confirm) {
2387
2291
  if (hasFlag(subArgs, "--yes")) return;
2388
2292
  const unmerged = await api.getMergeGate(sessionId, taskId).catch(() => []);
2389
2293
  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?`;
2294
+ const question = `${(0, import_core16.pastGateConfirmation)(unmerged.map((d) => (0, import_core16.titledTaskRef)(d)))} Start it anyway?`;
2391
2295
  if (!await confirm(question)) fail("Not started \u2014 nothing was changed. Pass --yes to start it without a prompt.");
2392
2296
  }
2393
2297
  function makeHandler(action, command = action) {
@@ -2427,6 +2331,30 @@ async function handleContinue(subArgs, injectedApi) {
2427
2331
  }
2428
2332
  });
2429
2333
  }
2334
+ async function handleCheckpoint(subArgs, injectedApi) {
2335
+ const usage = "Usage: ordewell checkpoint <task-id-or-order> approve|reject [reason] [--session-id <id>] [--workspace /path]";
2336
+ const [, answer, ...words] = positionals(subArgs);
2337
+ const reason = words.join(" ").trim();
2338
+ if (answer !== "approve" && answer !== "reject") {
2339
+ console.error(usage);
2340
+ process.exit(1);
2341
+ }
2342
+ if (answer === "approve" && reason) {
2343
+ console.error(`Approving takes no note \u2014 reject with a reason to tell the task something.
2344
+ ${usage}`);
2345
+ process.exit(1);
2346
+ }
2347
+ await withResolvedTask(subArgs, usage, injectedApi, async (api, sessionId, taskId) => {
2348
+ try {
2349
+ if (answer === "approve") await api.approveTaskCheckpoint(sessionId, taskId);
2350
+ else await api.rejectTaskCheckpoint(sessionId, taskId, reason || void 0);
2351
+ console.log(answer === "approve" ? "Checkpoint approved." : "Checkpoint rejected.");
2352
+ } catch (err) {
2353
+ console.error(`Failed to ${answer} checkpoint: ${err.message}`);
2354
+ process.exit(1);
2355
+ }
2356
+ });
2357
+ }
2430
2358
  var handleRunTask = makeHandler("run", "run-task");
2431
2359
  var handleForceStart = makeHandler("force-start");
2432
2360
  var handleRetry = makeHandler("retry");
@@ -2676,24 +2604,24 @@ async function handleRewind(subArgs, injectedApi) {
2676
2604
  }
2677
2605
 
2678
2606
  // src/tui/transcript.ts
2679
- var import_core15 = require("@ordewell/core");
2607
+ var import_core17 = require("@ordewell/core");
2680
2608
  function show(state, { view: conversation, gate: turnGate }) {
2681
2609
  const gated = turnGate === state.turnGate ? state : { ...state, turnGate };
2682
2610
  if (conversation === state.conversation) return gated;
2683
2611
  return { ...gated, conversation };
2684
2612
  }
2685
2613
  function fold(state, input) {
2686
- return show(state, { view: (0, import_core15.reduceConversation)(state.conversation, input), gate: state.turnGate });
2614
+ return show(state, { view: (0, import_core17.reduceConversation)(state.conversation, input), gate: state.turnGate });
2687
2615
  }
2688
2616
  function say(state, role, text) {
2689
2617
  const said = fold(state, { type: "local_entry", role, text });
2690
2618
  return said.conversation.nextId > state.conversation.nextId ? { ...said, scroll: 0 } : said;
2691
2619
  }
2692
2620
  function hear(state, message) {
2693
- return show(state, (0, import_core15.followTurn)(state.conversation, state.turnGate, message));
2621
+ return show(state, (0, import_core17.followTurn)(state.conversation, state.turnGate, message));
2694
2622
  }
2695
2623
  function cutTurn(state) {
2696
- return show(state, (0, import_core15.stopTurn)(state.conversation, state.turnGate));
2624
+ return show(state, (0, import_core17.stopTurn)(state.conversation, state.turnGate));
2697
2625
  }
2698
2626
  function wiped(view) {
2699
2627
  return { ...view, blocks: view.blocks.filter((b) => b.type === "usage") };
@@ -2919,12 +2847,12 @@ function runHandoffAction(state, id) {
2919
2847
  overlay: isRepoGroup(handoff) ? {
2920
2848
  kind: "confirm",
2921
2849
  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)}`,
2850
+ 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
2851
  action: { kind: "merge-run" }
2924
2852
  } : {
2925
2853
  kind: "confirm",
2926
2854
  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)}`,
2855
+ 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
2856
  action: { kind: "merge-run" }
2929
2857
  }
2930
2858
  },
@@ -3107,26 +3035,17 @@ async function handleHandoff(subArgs, injectedApi, confirm = askYesNo) {
3107
3035
  }
3108
3036
 
3109
3037
  // src/commands/planner.ts
3110
- var import_core16 = require("@ordewell/core");
3038
+ var import_core18 = require("@ordewell/core");
3111
3039
 
3112
3040
  // src/plannerModelSwitch.ts
3113
- function describePlannerSwitch(settings, provider) {
3114
- const model = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
3115
- const effort = typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : "";
3116
- if (!model) return { model: "", effort: "", source: "none" };
3117
- return rememberedModelFor(settings, provider) === model ? { model, effort, source: "remembered" } : { model, effort, source: "catalog-default" };
3118
- }
3119
- function rememberedModelFor(settings, provider) {
3120
- const map = settings.plannerModels;
3121
- if (!map || typeof map !== "object") return void 0;
3122
- const entry = map[provider];
3123
- if (!entry || typeof entry !== "object") return void 0;
3124
- const { model } = entry;
3125
- return typeof model === "string" ? model : void 0;
3041
+ function plannerSwitchRecall(settings) {
3042
+ if (settings.switchRecall) return settings.switchRecall;
3043
+ const model = settings.orchestratorModel;
3044
+ return model ? { model, effort: settings.plannerThinkingEffort, source: "remembered" } : { model: "", effort: "", source: "none" };
3126
3045
  }
3127
3046
 
3128
3047
  // src/commands/planner.ts
3129
- var KNOWN_PROVIDERS = Object.keys(import_core16.ALL_PROVIDERS);
3048
+ var isAiProvider = (id) => Object.hasOwn(import_core18.ALL_PROVIDERS, id);
3130
3049
  async function handlePlanner(subArgs, injectedApi) {
3131
3050
  const api = await connect(subArgs, injectedApi);
3132
3051
  const [wanted] = positionals(subArgs);
@@ -3135,19 +3054,19 @@ async function handlePlanner(subArgs, injectedApi) {
3135
3054
  return;
3136
3055
  }
3137
3056
  const provider = wanted.toLowerCase();
3138
- if (!KNOWN_PROVIDERS.includes(provider)) {
3057
+ if (!isAiProvider(provider)) {
3139
3058
  fail(`Unknown planner: ${provider}`, "Run `ordewell planner` with no arguments to see the list.");
3140
3059
  }
3141
3060
  const settings = await persistEnv(api, { AI_PROVIDER: provider });
3142
- const recall2 = describePlannerSwitch(settings, provider);
3061
+ const recall2 = plannerSwitchRecall(settings);
3143
3062
  writeEnv({ ORCHESTRATOR_MODEL: recall2.model, ORDEWELL_PLANNER_EFFORT: recall2.effort });
3144
- const meta = import_core16.ALL_PROVIDERS[provider];
3063
+ const meta = import_core18.ALL_PROVIDERS[provider];
3145
3064
  const catalog = await fetchCatalog(api);
3146
3065
  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}.`
3066
+ (0, import_core18.isCliProvider)(provider) ? `Planning with ${meta.label} \u2014 no API key needed, it uses that agent's own subscription.` : `Planner set to ${meta.label}.`
3148
3067
  );
3149
3068
  console.log(plannerModelLine(recall2));
3150
- if (!(0, import_core16.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
3069
+ if (!(0, import_core18.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
3151
3070
  console.log(` No ${meta.apiKeyEnvVar} configured yet \u2014 set one with \`ordewell key set ${provider} <key>\`.`);
3152
3071
  }
3153
3072
  }
@@ -3165,15 +3084,15 @@ async function showPlanners(api) {
3165
3084
  const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
3166
3085
  const current = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
3167
3086
  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);
3087
+ for (const id of import_core18.CLI_PROVIDERS) {
3088
+ const runner = (0, import_core18.runnerForProvider)(id);
3170
3089
  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)"}`);
3090
+ console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core18.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
3172
3091
  }
3173
3092
  console.log("\nAPI providers");
3174
- for (const id of import_core16.PROVIDER_PRIORITY) {
3093
+ for (const id of import_core18.PROVIDER_PRIORITY) {
3175
3094
  const configured = catalog.providers.includes(id);
3176
- const meta = import_core16.ALL_PROVIDERS[id];
3095
+ const meta = import_core18.ALL_PROVIDERS[id];
3177
3096
  console.log(` ${marker(id === current)} ${id.padEnd(14)} ${meta.label}${configured ? "" : ` (no ${meta.apiKeyEnvVar})`}`);
3178
3097
  }
3179
3098
  console.log(`
@@ -3190,7 +3109,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
3190
3109
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
3191
3110
  const modelId = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
3192
3111
  const currentEffort = typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : "";
3193
- const runner = (0, import_core16.runnerForProvider)(provider);
3112
+ const runner = isAiProvider(provider) ? (0, import_core18.runnerForProvider)(provider) : null;
3194
3113
  if (!runner) {
3195
3114
  fail(
3196
3115
  "Thinking effort applies to a coding-agent planner.",
@@ -3230,7 +3149,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
3230
3149
  }
3231
3150
 
3232
3151
  // src/commands/parallel.ts
3233
- var import_core17 = require("@ordewell/core");
3152
+ var import_core19 = require("@ordewell/core");
3234
3153
  var USAGE5 = "Usage: ordewell parallel [<number of tasks, 1 or more>]";
3235
3154
  async function handleParallel(subArgs, injectedApi) {
3236
3155
  const [wanted] = positionals(subArgs);
@@ -3240,14 +3159,14 @@ async function handleParallel(subArgs, injectedApi) {
3240
3159
  console.log(`Up to ${String(settings.maxParallel ?? "?")} AI tasks run at once. Change with: ordewell parallel <n>`);
3241
3160
  return;
3242
3161
  }
3243
- const limit = (0, import_core17.parseMaxParallel)(wanted);
3162
+ const limit = (0, import_core19.parseMaxParallel)(wanted);
3244
3163
  if (limit === null) fail(`"${wanted}" is not a number of tasks (1 or more).`, USAGE5);
3245
3164
  await persistEnv(api, { ORDEWELL_MAX_PARALLEL: String(limit) });
3246
3165
  console.log(`Up to ${limit} AI task${limit === 1 ? "" : "s"} now run at once.`);
3247
3166
  }
3248
3167
 
3249
3168
  // src/commands/key.ts
3250
- var import_core18 = require("@ordewell/core");
3169
+ var import_core20 = require("@ordewell/core");
3251
3170
  var USAGE6 = "Usage: ordewell key [list | set <provider> <key>]";
3252
3171
  async function handleKey(subArgs, injectedApi) {
3253
3172
  const [action, provider, ...keyParts] = positionals(subArgs);
@@ -3258,8 +3177,8 @@ async function handleKey(subArgs, injectedApi) {
3258
3177
  if (action !== "set") {
3259
3178
  const catalog = await fetchCatalog(api);
3260
3179
  console.log("\nAPI provider keys\n");
3261
- for (const id2 of import_core18.PROVIDER_PRIORITY) {
3262
- const meta2 = import_core18.ALL_PROVIDERS[id2];
3180
+ for (const id2 of import_core20.PROVIDER_PRIORITY) {
3181
+ const meta2 = import_core20.ALL_PROVIDERS[id2];
3263
3182
  const configured = catalog.providers.includes(id2);
3264
3183
  console.log(` ${configured ? "\u2713" : " "} ${id2.padEnd(14)} ${meta2.apiKeyEnvVar.padEnd(28)} ${meta2.label}`);
3265
3184
  }
@@ -3269,7 +3188,7 @@ async function handleKey(subArgs, injectedApi) {
3269
3188
  }
3270
3189
  if (!provider) fail(USAGE6);
3271
3190
  const id = provider.toLowerCase();
3272
- const meta = import_core18.ALL_PROVIDERS[id];
3191
+ const meta = import_core20.ALL_PROVIDERS[id];
3273
3192
  if (!meta) {
3274
3193
  fail(`Unknown provider: ${provider}`, "Run `ordewell key` to see the list.");
3275
3194
  }
@@ -3280,7 +3199,7 @@ async function handleKey(subArgs, injectedApi) {
3280
3199
  }
3281
3200
 
3282
3201
  // src/commands/runners.ts
3283
- var import_core19 = require("@ordewell/core");
3202
+ var import_core21 = require("@ordewell/core");
3284
3203
  var USAGE7 = "Usage: ordewell runners [<runner-id> on|off]";
3285
3204
  async function handleRunners(subArgs, injectedApi) {
3286
3205
  const [runner, toggle] = positionals(subArgs);
@@ -3311,15 +3230,15 @@ function handleAuto(subArgs) {
3311
3230
  const currentRaw = process.env.ORDEWELL_AUTONOMOUS_MODE;
3312
3231
  const current = currentRaw !== "false" && currentRaw !== "0";
3313
3232
  if (!arg) {
3314
- console.log(`Autonomy level: ${(0, import_core19.autonomyLevelLabel)(current)}`);
3233
+ console.log(`Autonomy level: ${(0, import_core21.autonomyLevelLabel)(current)}`);
3315
3234
  console.log(` ${AUTO_USAGE}`);
3316
3235
  return;
3317
3236
  }
3318
- const enabled = (0, import_core19.parseAutonomyLevel)(arg);
3237
+ const enabled = (0, import_core21.parseAutonomyLevel)(arg);
3319
3238
  if (enabled === null) fail(AUTO_USAGE);
3320
3239
  writeEnvVar(findEnvFile(), "ORDEWELL_AUTONOMOUS_MODE", String(enabled));
3321
3240
  process.env.ORDEWELL_AUTONOMOUS_MODE = String(enabled);
3322
- console.log(`Autonomy level: ${(0, import_core19.autonomyLevelLabel)(enabled)} for new sessions.`);
3241
+ console.log(`Autonomy level: ${(0, import_core21.autonomyLevelLabel)(enabled)} for new sessions.`);
3323
3242
  }
3324
3243
  async function handleRefresh(subArgs, injectedApi) {
3325
3244
  const api = await connect(subArgs, injectedApi);
@@ -3351,7 +3270,7 @@ async function handleApprove(subArgs, injectedApi) {
3351
3270
  // src/tui/terminalLauncher.ts
3352
3271
  var import_child_process3 = require("child_process");
3353
3272
  var import_util = require("util");
3354
- var import_core20 = require("@ordewell/core");
3273
+ var import_core22 = require("@ordewell/core");
3355
3274
  var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
3356
3275
  var defaultExecFile = async (command, args) => {
3357
3276
  const { stdout, stderr } = await execFileAsync(command, args);
@@ -3371,16 +3290,16 @@ var defaultWhich = (bin) => {
3371
3290
  async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
3372
3291
  const platform = deps.platform ?? process.platform;
3373
3292
  const env = deps.env ?? process.env;
3374
- const hasTmuxImpl = deps.hasTmuxImpl ?? import_core20.hasTmux;
3293
+ const hasTmuxImpl = deps.hasTmuxImpl ?? import_core22.hasTmux;
3375
3294
  const execFileImpl = deps.execFileImpl ?? defaultExecFile;
3376
3295
  const spawnImpl = deps.spawnImpl ?? defaultSpawn;
3377
3296
  const which = deps.which ?? defaultWhich;
3378
3297
  if (!hasTmuxImpl()) {
3379
3298
  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
3299
  }
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);
3300
+ const session = (0, import_core22.tmuxSessionName)(port);
3301
+ const socket = (0, import_core22.tmuxSocketName)(port);
3302
+ const base = (0, import_core22.tmuxWindowName)(taskId, planSessionId);
3384
3303
  let windows;
3385
3304
  try {
3386
3305
  const { stdout } = await execFileImpl("tmux", ["-L", socket, "list-windows", "-t", session, "-F", "#{window_name}"]);
@@ -3529,7 +3448,7 @@ async function handleTaskRunner(subArgs, injectedApi) {
3529
3448
  const state = await api.getRunners();
3530
3449
  if (!runner) {
3531
3450
  console.log(`
3532
- Runner \xB7 #${task.order} ${task.title}
3451
+ Runner \xB7 ${(0, import_plan_utils.titledTaskRef)(task)}
3533
3452
  `);
3534
3453
  for (const r of state.runners) {
3535
3454
  console.log(` ${r.id === task.assignedRunner ? "*" : " "} ${r.id.padEnd(14)} ${r.name}${r.enabled ? "" : " (not enabled for planning)"}`);
@@ -3542,7 +3461,7 @@ Runner \xB7 #${task.order} ${task.title}
3542
3461
  fail(`Unknown runner: ${runner}`, `This daemon knows: ${state.runners.map((r) => r.id).join(", ")}`);
3543
3462
  }
3544
3463
  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.`);
3464
+ 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
3465
  });
3547
3466
  }
3548
3467
  var MODEL_USAGE = "Usage: ordewell task-model <task-id-or-order> [<model-id>] [--session-id <id>]";
@@ -3552,7 +3471,7 @@ async function handleTaskModel(subArgs, injectedApi) {
3552
3471
  const options = modelsForTask(catalog.models, task);
3553
3472
  if (!modelId) {
3554
3473
  console.log(`
3555
- Model \xB7 #${task.order} ${task.title}${task.assignedRunner ? ` (${task.assignedRunner})` : ""}
3474
+ Model \xB7 ${(0, import_plan_utils.titledTaskRef)(task)}${task.assignedRunner ? ` (${task.assignedRunner})` : ""}
3556
3475
  `);
3557
3476
  if (options.length === 0) console.log(" No models discovered for this runner \u2014 try `ordewell refresh`.");
3558
3477
  for (const m of options) {
@@ -3575,7 +3494,7 @@ Model \xB7 #${task.order} ${task.title}${task.assignedRunner ? ` (${task.assigne
3575
3494
  assignedModel,
3576
3495
  thinkingEffort: assignedModel.thinkingEffort ?? null
3577
3496
  });
3578
- console.log(`Task #${task.order} model set to ${model.label}.`);
3497
+ console.log(`Task ${(0, import_plan_utils.taskRef)(task)} model set to ${model.label}.`);
3579
3498
  if (task.assignedModel?.thinkingEffort && !assignedModel.thinkingEffort) {
3580
3499
  console.log(` ${model.label} does not expose "${task.assignedModel.thinkingEffort}", so the effort went back to the runner default.`);
3581
3500
  }
@@ -3589,7 +3508,7 @@ async function handleTaskEffort(subArgs, injectedApi) {
3589
3508
  const current = task.assignedModel?.thinkingEffort;
3590
3509
  if (!level) {
3591
3510
  console.log(`
3592
- Thinking effort \xB7 #${task.order} ${task.title}
3511
+ Thinking effort \xB7 ${(0, import_plan_utils.titledTaskRef)(task)}
3593
3512
  `);
3594
3513
  console.log(` ${current ? " " : "*"} default Let the executor choose`);
3595
3514
  for (const v of variants) {
@@ -3614,7 +3533,7 @@ Thinking effort \xB7 #${task.order} ${task.title}
3614
3533
  assignedModel: task.assignedModel ? { ...task.assignedModel, thinkingEffort } : void 0,
3615
3534
  thinkingEffort: thinkingEffort ?? null
3616
3535
  });
3617
- console.log(`Task #${task.order} thinking effort set to ${thinkingEffort ?? "runner default"}.`);
3536
+ console.log(`Task ${(0, import_plan_utils.taskRef)(task)} thinking effort set to ${thinkingEffort ?? "runner default"}.`);
3618
3537
  });
3619
3538
  }
3620
3539
  var MODE_USAGE = "Usage: ordewell task-mode <task-id-or-order> [<mode>] [--session-id <id>]";
@@ -3624,7 +3543,7 @@ async function handleTaskMode(subArgs, injectedApi) {
3624
3543
  const modes = modesForTask(catalog.modesByRunner, task);
3625
3544
  if (!mode) {
3626
3545
  console.log(`
3627
- Mode \xB7 #${task.order} ${task.title}${task.assignedRunner ? ` (${task.assignedRunner})` : ""}
3546
+ Mode \xB7 ${(0, import_plan_utils.titledTaskRef)(task)}${task.assignedRunner ? ` (${task.assignedRunner})` : ""}
3628
3547
  `);
3629
3548
  if (modes.length === 0) {
3630
3549
  console.log(" This task has no runner, or its runner declares no modes.");
@@ -3643,7 +3562,7 @@ Mode \xB7 #${task.order} ${task.title}${task.assignedRunner ? ` (${task.assigned
3643
3562
  );
3644
3563
  }
3645
3564
  await api.updateTask(sessionId, task.id, { taskMode: chosen.id });
3646
- console.log(`Task #${task.order} mode set to ${chosen.label}.`);
3565
+ console.log(`Task ${(0, import_plan_utils.taskRef)(task)} mode set to ${chosen.label}.`);
3647
3566
  });
3648
3567
  }
3649
3568
  var DEPS_USAGE = "Usage: ordewell task-deps <task-id-or-order> [<id,id,\u2026>|none] [--session-id <id>]";
@@ -3652,14 +3571,14 @@ async function handleTaskDeps(subArgs, injectedApi) {
3652
3571
  const candidates = (0, import_plan_utils.dependencyCandidates)(tasks, task.id);
3653
3572
  if (!value) {
3654
3573
  console.log(`
3655
- Dependencies \xB7 #${task.order} ${task.title}
3574
+ Dependencies \xB7 ${(0, import_plan_utils.titledTaskRef)(task)}
3656
3575
  `);
3657
3576
  if (candidates.length === 0) {
3658
- console.log(` Nothing runs before #${task.order}, so it has no possible dependencies.`);
3577
+ console.log(` Nothing runs before ${(0, import_plan_utils.taskRef)(task)}, so it has no possible dependencies.`);
3659
3578
  }
3660
3579
  for (const c of candidates) {
3661
3580
  const chosen = task.dependencies.includes(c.id);
3662
- console.log(` ${chosen ? "*" : " "} ${c.id} #${c.order} ${c.title}${c.status === "completed" ? " (already completed)" : ""}`);
3581
+ console.log(` ${chosen ? "*" : " "} ${c.id} ${(0, import_plan_utils.titledTaskRef)(c)}${c.status === "completed" ? " (already completed)" : ""}`);
3663
3582
  }
3664
3583
  console.log(`
3665
3584
  * = current. ${DEPS_USAGE}`);
@@ -3669,7 +3588,7 @@ Dependencies \xB7 #${task.order} ${task.title}
3669
3588
  const check = (0, import_plan_utils.canSetDependencies)(tasks, task.id, dependencies);
3670
3589
  if (!check.ok) fail(check.error ?? "Invalid dependencies.");
3671
3590
  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.`);
3591
+ 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
3592
  });
3674
3593
  }
3675
3594
  var OPS_USAGE = "Usage: ordewell task-ops <task-id-or-order> [on|off] [--session-id <id>]";
@@ -3677,7 +3596,7 @@ async function handleTaskOps(subArgs, injectedApi) {
3677
3596
  await withTask(subArgs, OPS_USAGE, injectedApi, async (api, sessionId, task, tasks, value) => {
3678
3597
  if (!value) {
3679
3598
  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"}.`);
3599
+ ${(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
3600
  console.log(`
3682
3601
  ${OPS_USAGE}`);
3683
3602
  return;
@@ -3687,12 +3606,12 @@ async function handleTaskOps(subArgs, injectedApi) {
3687
3606
  if (task.type !== "ai") fail("Only an AI task can be an ops task \u2014 a manual task already runs outside any worktree.");
3688
3607
  if (!tasks.some((t) => t.id === task.id)) fail("A subtask runs with its parent; make the parent an ops task instead.");
3689
3608
  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.`);
3609
+ 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
3610
  });
3692
3611
  }
3693
3612
 
3694
3613
  // src/tui/index.ts
3695
- var import_core33 = require("@ordewell/core");
3614
+ var import_core37 = require("@ordewell/core");
3696
3615
 
3697
3616
  // src/tui/editor.ts
3698
3617
  function emptyEditor() {
@@ -3841,6 +3760,7 @@ var SLASH_COMMANDS = [
3841
3760
  { name: "cancel", usage: "/cancel <id>", description: "Kill a running task", category: "tasks" },
3842
3761
  { name: "force-start", usage: "/force-start <id>", description: "Start a task now, ignoring dependencies", category: "tasks" },
3843
3762
  { name: "continue", usage: "/continue <id> <message>", description: "Continue a finished task in its saved runner session, with your message", category: "tasks" },
3763
+ { 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
3764
  { name: "terminal", usage: "/terminal <id>", description: "Open a task: its OS terminal, or a structured task's live log", category: "tasks" },
3845
3765
  { 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
3766
  { name: "task-model", usage: "/task-model <id> [model]", description: "Choose or set a task's executor model", category: "tasks" },
@@ -3941,7 +3861,7 @@ function tokenCompletions(token) {
3941
3861
  }
3942
3862
 
3943
3863
  // src/tui/blocks.ts
3944
- var import_core21 = require("@ordewell/core");
3864
+ var import_core23 = require("@ordewell/core");
3945
3865
 
3946
3866
  // src/tui/markdown.ts
3947
3867
  function renderMarkdown(source, cols) {
@@ -4161,16 +4081,16 @@ function resultLines(block, room, detailAll) {
4161
4081
  const fit2 = (line) => detailAll ? wrap(line, room) : [truncate(line, room)];
4162
4082
  const outcome = unsettledOutcome(block);
4163
4083
  if (outcome) {
4164
- const [reason, ...rest] = (0, import_core21.outputLines)(block.output).map(sanitize);
4084
+ const [reason, ...rest] = (0, import_core23.outputLines)(block.output).map(sanitize);
4165
4085
  const status = fit2(reason ? `${outcome.label}: ${reason}` : outcome.label).map(outcome.paint);
4166
4086
  return detailAll ? [...status, ...rest.flatMap(fit2).map(style.grey)] : status;
4167
4087
  }
4168
4088
  if (detailAll) {
4169
- const lines2 = (0, import_core21.outputLines)(block.output).map(sanitize);
4089
+ const lines2 = (0, import_core23.outputLines)(block.output).map(sanitize);
4170
4090
  const note = lines2.length > PREVIEW_LINES ? ["\u2026 (ctrl+o to collapse)"] : [];
4171
4091
  return lines2.length > 0 ? [...lines2.flatMap(fit2), ...note].map(style.grey) : [style.grey("(no output)")];
4172
4092
  }
4173
- const { lines, hiddenLineCount } = (0, import_core21.outputPreview)(block.output, PREVIEW_LINES);
4093
+ const { lines, hiddenLineCount } = (0, import_core23.outputPreview)(block.output, PREVIEW_LINES);
4174
4094
  if (lines.length === 0) return [style.grey("(no output)")];
4175
4095
  const more = hiddenLineCount > 0 ? [`\u2026 +${countOf(hiddenLineCount, "line")} (ctrl+o to expand)`] : [];
4176
4096
  return [...lines.map(sanitize), ...more].map((line) => style.grey(truncate(line, room)));
@@ -4184,7 +4104,7 @@ var DIFF_PAINT = {
4184
4104
  };
4185
4105
  var DIFF_SIGN = { added: "+", removed: "-", context: " ", gap: "" };
4186
4106
  function diffLines(block, diff, room, detailAll) {
4187
- const rows = (0, import_core21.diffRows)(block.output);
4107
+ const rows = (0, import_core23.diffRows)(block.output);
4188
4108
  const gutter = Math.max(0, ...rows.map((row) => String(row.line ?? "").length));
4189
4109
  const draw = (row) => {
4190
4110
  const text = row.kind === "gap" ? `${" ".repeat(gutter)} \u22EE` : `${String(row.line ?? "").padStart(gutter)} ${DIFF_SIGN[row.kind]} ${sanitize(row.text)}`;
@@ -4193,7 +4113,7 @@ function diffLines(block, diff, room, detailAll) {
4193
4113
  const shown = detailAll ? rows : rows.slice(0, DIFF_PREVIEW_ROWS);
4194
4114
  const hidden = rows.length - shown.length;
4195
4115
  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];
4116
+ return [(0, import_core23.diffSummary)(diff), ...shown.flatMap(draw), ...more];
4197
4117
  }
4198
4118
  function toolLines(block, cols, detailAll) {
4199
4119
  const room = Math.max(1, cols - OUTPUT_INDENT.length);
@@ -4249,6 +4169,19 @@ function approvalLine(block, cols) {
4249
4169
  const line = `${mark} ${status} \xB7 ${APPROVAL_KIND[block.kind]}: ${firstLine(block.subject)}${note}`;
4250
4170
  return `${paint(mark)}${truncate(line, cols).slice(mark.length)}`;
4251
4171
  }
4172
+ function approvalKeysLine(block, cols) {
4173
+ const keys = ["ctrl-y allow", ...block.allowForTask ? ["ctrl-t allow for task"] : [], "ctrl-g deny (composer text is the note)"];
4174
+ return style.grey(truncate(` ${keys.join(" \xB7 ")}`, cols));
4175
+ }
4176
+ function checkpointCardLines(order, question, cols, approvalFirst) {
4177
+ const room = Math.max(1, cols - 2);
4178
+ 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)";
4179
+ return [
4180
+ truncate(`${style.accent("\u25C6")} ${style.bold(`Task ${order} asks`)}`, cols),
4181
+ ...wrap(sanitize(question).trim(), room).map((line) => ` ${line}`),
4182
+ ...wrap(keys, room).map((line) => style.grey(` ${line}`))
4183
+ ];
4184
+ }
4252
4185
  function planLine(block, cols) {
4253
4186
  const count = block.taskCount === void 0 ? "" : ` (${countOf(block.taskCount, "task")})`;
4254
4187
  const text = block.status === "building" ? "Building plan\u2026" : `Plan ${block.status}${count}`;
@@ -4281,11 +4214,13 @@ function drawn(block, cols, detailAll) {
4281
4214
  drawnBlocks.set(block, { cols, detailAll, lines });
4282
4215
  return lines;
4283
4216
  }
4284
- function conversationLines(blocks, cols, detailAll) {
4217
+ function conversationLines(blocks, cols, detailAll, answering) {
4285
4218
  const lines = [];
4286
4219
  for (const block of blocks) {
4287
4220
  if (block.type === "usage") continue;
4288
- lines.push(...drawn(block, cols, detailAll), "");
4221
+ lines.push(...drawn(block, cols, detailAll));
4222
+ if (block === answering) lines.push(approvalKeysLine(block, cols));
4223
+ lines.push("");
4289
4224
  }
4290
4225
  return lines;
4291
4226
  }
@@ -4313,7 +4248,7 @@ function tokenLine(blocks, cols) {
4313
4248
  }
4314
4249
 
4315
4250
  // src/tui/state.ts
4316
- var import_core22 = require("@ordewell/core");
4251
+ var import_core24 = require("@ordewell/core");
4317
4252
  var SKILL_IDS = ["tdd", "verify"];
4318
4253
  function noSkills() {
4319
4254
  return { tdd: false, verify: false };
@@ -4325,8 +4260,7 @@ function visibleItems(picker2) {
4325
4260
  (i) => i.id.toLowerCase().includes(filter) || i.label.toLowerCase().includes(filter) || (i.detail ?? "").toLowerCase().includes(filter)
4326
4261
  );
4327
4262
  }
4328
- var isTaskRunning = (task) => task.status === "in_progress" || task.status === "running";
4329
- var anyTaskRunning = (state) => state.tasks.some(isTaskRunning);
4263
+ var anyTaskRunning = (state) => state.tasks.some(import_core24.isTaskRunning);
4330
4264
  var plannerInFlight = (state) => state.status === "planning" || state.status === "researching";
4331
4265
  function planRows(state) {
4332
4266
  const rows = [];
@@ -4346,6 +4280,9 @@ function selectedPlanRow(state) {
4346
4280
  function waitingApproval(tv) {
4347
4281
  return tv.view.blocks.find((b) => b.type === "approval" && b.status === "pending" && b.approvalId !== void 0);
4348
4282
  }
4283
+ function waitingCheckpoint(task) {
4284
+ return task?.status === "awaiting_user" && task.awaitingReason === "checkpoint" ? task.checkpoint : void 0;
4285
+ }
4349
4286
  function continuesTask(task) {
4350
4287
  return task?.continuable === true && (task.status === "completed" || task.status === "failed");
4351
4288
  }
@@ -4360,9 +4297,9 @@ function findTask(tasks, id) {
4360
4297
  function initialState(overrides = {}) {
4361
4298
  return {
4362
4299
  editor: emptyEditor(),
4363
- conversation: import_core22.EMPTY_CONVERSATION,
4300
+ conversation: import_core24.EMPTY_CONVERSATION,
4364
4301
  taskView: null,
4365
- turnGate: import_core22.NO_TURN,
4302
+ turnGate: import_core24.NO_TURN,
4366
4303
  detailAll: false,
4367
4304
  status: "idle",
4368
4305
  busyLabel: "",
@@ -4387,7 +4324,7 @@ function initialState(overrides = {}) {
4387
4324
  orchestratorModel: "",
4388
4325
  plannerProvider: "",
4389
4326
  plannerEffort: "",
4390
- maxParallel: import_core22.DEFAULT_MAX_PARALLEL,
4327
+ maxParallel: import_core24.DEFAULT_MAX_PARALLEL,
4391
4328
  configuredProviders: [],
4392
4329
  allowlist: {},
4393
4330
  autonomous: true,
@@ -4399,7 +4336,7 @@ function initialState(overrides = {}) {
4399
4336
  gate: null,
4400
4337
  overlay: null,
4401
4338
  pendingApprovals: [],
4402
- queuedPrompts: import_core22.EMPTY_HOLD,
4339
+ queuedPrompts: import_core24.EMPTY_HOLD,
4403
4340
  stopArmed: false,
4404
4341
  stopArmToken: 0,
4405
4342
  stopRequested: false,
@@ -4447,7 +4384,7 @@ function chatEditorRoomFor(state, active) {
4447
4384
  }
4448
4385
 
4449
4386
  // src/tui/layout.ts
4450
- var import_core23 = require("@ordewell/core");
4387
+ var import_core25 = require("@ordewell/core");
4451
4388
  var CHROME_ROWS = 4;
4452
4389
  function chatInputWrap(state) {
4453
4390
  const active = state.focus === "chat" && !state.overlay;
@@ -4456,7 +4393,7 @@ function chatInputWrap(state) {
4456
4393
  }
4457
4394
  function footerHints(state) {
4458
4395
  const selected = selectedPlanRow(state)?.task;
4459
- const markHint = selected?.status === "completed" ? "m undone" : "m done";
4396
+ const markHint = selected && (0, import_core25.markAction)(selected) === "uncomplete" ? "m undone" : "m done";
4460
4397
  const resolveHint = selected?.isolation?.state === "conflict" ? ["x resolve conflict"] : [];
4461
4398
  const planning = plannerInFlight(state) && !state.taskView;
4462
4399
  const escHint = !planning ? null : state.queuedPrompts.length > 0 ? "esc unsend" : "esc \xD72 stop planning";
@@ -4487,7 +4424,7 @@ function footerHints(state) {
4487
4424
  "tab chat"
4488
4425
  ];
4489
4426
  }
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"] : [];
4427
+ const detailHint = (0, import_core25.hasHiddenDetail)(state.taskView ? state.taskView.view.blocks : state.conversation.blocks) ? [state.detailAll ? "ctrl-o collapse all" : "ctrl-o expand all"] : [];
4491
4428
  return [
4492
4429
  "/help",
4493
4430
  "tab plan",
@@ -4508,20 +4445,25 @@ function queuedBubble(text, cols) {
4508
4445
  function taskQueuedRows(state, cols) {
4509
4446
  const tv = state.taskView;
4510
4447
  if (!tv) return [];
4511
- return tv.view.queued.flatMap((message, i) => queuedTaskBubble(message.text, i === tv.queuedIndex, cols));
4448
+ return tv.view.queued.flatMap((message, i) => queuedTaskBubble(message, i === tv.queuedIndex, cols));
4449
+ }
4450
+ function queuedTaskTag({ handedOver, forced }, selected) {
4451
+ if (forced) return " \xB7 sending now";
4452
+ if (handedOver) return " \xB7 handed over";
4453
+ return selected ? " \xB7 queued \xB7 ctrl-r removes" : " \xB7 queued";
4512
4454
  }
4513
- function queuedTaskBubble(text, selected, cols) {
4455
+ function queuedTaskBubble(message, selected, cols) {
4456
+ const { text } = message;
4514
4457
  const marker2 = selected ? style.accent("\u276F") : style.grey("\u25C7");
4515
- const tag = selected ? " \xB7 queued \xB7 ctrl-r removes" : " \xB7 queued";
4458
+ const tag = queuedTaskTag(message, selected);
4516
4459
  const room = Math.max(1, cols - width(`\u276F \u25C7 ${tag}`));
4517
4460
  return wrap(text, room).map((line, i) => i === 0 ? truncate(`${marker2} ${selected ? style.bold(line) : line}${style.grey(tag)}`, cols) : truncate(` ${line}`, cols));
4518
4461
  }
4519
4462
  function taskActivity(task, working) {
4520
- if (task?.awaitingApproval) return approvalLabel(task.awaitingApproval);
4463
+ const approvals = (0, import_core25.approvalLabel)(task?.awaitingApproval);
4464
+ if (approvals) return inRow(approvals);
4521
4465
  if (working) return "working";
4522
- if (task?.status === "awaiting_user") {
4523
- return task.awaitingReason ? AWAITING_LABEL[task.awaitingReason] : "waiting for your input";
4524
- }
4466
+ if (task?.status === "awaiting_user") return inRow((0, import_core25.awaitingLabel)(task) ?? "Waiting for your input");
4525
4467
  return task?.status ?? "";
4526
4468
  }
4527
4469
  function taskHeaderLines(state, tv, cols) {
@@ -4533,7 +4475,8 @@ function taskHeaderLines(state, tv, cols) {
4533
4475
  taskActivity(task, tv.view.working)
4534
4476
  ].filter(Boolean);
4535
4477
  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"];
4478
+ 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"];
4479
+ if (approval && waitingCheckpoint(task)) hint.push("then the checkpoint");
4537
4480
  const index = tv.attempts.indexOf(tv.attempt);
4538
4481
  if (tv.attempts.length > 1) hint.push(`alt\u2190/\u2192 attempt ${index >= 0 ? index + 1 : 1}/${tv.attempts.length}`);
4539
4482
  hint.push("esc back");
@@ -4566,10 +4509,10 @@ function bodyRows(state) {
4566
4509
  return Math.max(0, state.rows - CHROME_ROWS - input - footer + 2);
4567
4510
  }
4568
4511
  var chatBodyMemo = null;
4569
- function chatBodyLines(blocks, cols, detailAll) {
4512
+ function chatBodyLines(blocks, cols, detailAll, answering) {
4570
4513
  const memo = chatBodyMemo;
4571
4514
  if (memo && memo.blocks === blocks && memo.cols === cols && memo.detailAll === detailAll) return memo.lines;
4572
- const lines = conversationLines(blocks, cols, detailAll);
4515
+ const lines = conversationLines(blocks, cols, detailAll, answering);
4573
4516
  chatBodyMemo = { blocks, cols, detailAll, lines };
4574
4517
  return lines;
4575
4518
  }
@@ -4577,7 +4520,10 @@ function chatLayout(state, rows, cols) {
4577
4520
  const tv = state.taskView;
4578
4521
  const blocks = tv ? tv.view.blocks : state.conversation.blocks;
4579
4522
  const welcome = !tv && state.tasks.length === 0;
4580
- const body = chatBodyLines(blocks, cols, state.detailAll);
4523
+ const checkpoint = tv ? waitingCheckpoint(findTask(state.tasks, tv.taskId)) : void 0;
4524
+ const card = tv && checkpoint ? checkpointCardLines(findTask(state.tasks, tv.taskId)?.order ?? 0, checkpoint, cols, waitingApproval(tv) !== void 0) : [];
4525
+ const answering = chatBodyLines(blocks, cols, state.detailAll, tv ? waitingApproval(tv) : void 0);
4526
+ const body = card.length > 0 ? [...answering, ...card] : answering;
4581
4527
  const transcript = !welcome ? body : body.length === 0 ? welcomeLines(state, cols) : [...welcomeLines(state, cols), "", ...body];
4582
4528
  const bubbles = tv ? taskQueuedRows(state, cols) : queuedPromptRows(state, cols);
4583
4529
  const lines = [...transcript, ...bubbles.length > 0 ? ["", ...bubbles] : []];
@@ -4592,9 +4538,9 @@ function chatScrollMax(state) {
4592
4538
  }
4593
4539
  function plannerSetup(state) {
4594
4540
  const provider = state.plannerProvider;
4595
- const registration = import_core23.ALL_PROVIDERS[provider];
4541
+ const registration = import_core25.ALL_PROVIDERS[provider];
4596
4542
  if (!registration) return { label: "not set", ready: false };
4597
- const runner = (0, import_core23.runnerForProvider)(provider);
4543
+ const runner = (0, import_core25.runnerForProvider)(provider);
4598
4544
  const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
4599
4545
  return { label: registration.label, ready };
4600
4546
  }
@@ -4681,45 +4627,34 @@ function welcomeLines(state, cols) {
4681
4627
  return lines;
4682
4628
  }
4683
4629
  var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4684
- var STATUS_MARK = {
4685
- completed: style.green,
4630
+ var STATUS_PAINT = {
4631
+ done: style.green,
4686
4632
  running: style.yellow,
4687
- in_progress: style.yellow,
4633
+ quiet: style.cyan,
4688
4634
  failed: style.red,
4689
4635
  blocked: style.red,
4690
- awaiting_user: style.yellow,
4691
- cancelled: style.grey,
4692
- skipped: style.grey
4636
+ awaiting: style.yellow,
4637
+ todo: (text) => text
4693
4638
  };
4694
4639
  var STATUS_ICON = {
4695
- completed: "\u2713",
4640
+ done: "\u2713",
4696
4641
  failed: "\u2717",
4697
4642
  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"
4643
+ awaiting: "?",
4644
+ quiet: "~",
4645
+ todo: "\xB7"
4709
4646
  };
4710
- function approvalLabel(count) {
4711
- return count > 1 ? `waiting for approval (${count})` : "waiting for approval";
4712
- }
4713
- var IDLE_ICON = "~";
4647
+ var inRow = (label) => label.charAt(0).toLowerCase() + label.slice(1);
4714
4648
  var FIRST_ROW_LINE = 2;
4715
4649
  function planLayout(state, rows, cols) {
4716
4650
  const done = state.tasks.filter((t) => t.status === "completed").length;
4717
4651
  const lines = [style.bold(`Plan ${done}/${state.tasks.length}`), ""];
4718
4652
  const rowSpans = [];
4653
+ const orderOf = new Map(state.tasks.map((task) => [task.id, task.order]));
4719
4654
  let editorLine;
4720
4655
  planRows(state).forEach((row, i) => {
4721
4656
  const taskStart = lines.length;
4722
- const renderedTask = taskLines(state, row, i, cols);
4657
+ const renderedTask = taskLines(state, row, i, cols, orderOf);
4723
4658
  if (row.task.id === state.expandedTaskId && renderedTask.editorLine !== void 0) {
4724
4659
  editorLine = taskStart + renderedTask.editorLine;
4725
4660
  }
@@ -4775,18 +4710,16 @@ function planScrollExtent(state) {
4775
4710
  }
4776
4711
  return planLayout(state, bodyRows(state), cols);
4777
4712
  }
4778
- function taskLines(state, row, index, cols) {
4779
- const { task, parent } = row;
4713
+ function taskLines(state, planRow, index, cols, orderOf) {
4714
+ const { task, parent } = planRow;
4715
+ const row = (0, import_core25.taskRowView)(task, { parent, orderOf, modes: modesForTask(state.modesByRunner, task) });
4780
4716
  const selected = state.focus === "plan" && index === state.selectedTask;
4781
4717
  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");
4718
+ const rawIcon = row.status === "running" ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[row.status];
4719
+ const icon = STATUS_PAINT[row.status](rawIcon);
4720
+ const kind = row.running ? style.yellow("RUN") : row.kind === "user" ? style.yellow("MAN") : row.kind === "ops" ? style.magenta("OPS") : style.grey(" AI");
4788
4721
  const caret = selected ? style.cyan("\u276F") : " ";
4789
- const orderLabel = parent ? (0, import_core23.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
4722
+ const orderLabel = parent ? row.orderLabel : row.orderLabel.padStart(2);
4790
4723
  const indent = parent ? " " : "";
4791
4724
  const head = `${indent}${caret} ${icon} ${orderLabel} ${kind} `;
4792
4725
  const titleRoom = Math.max(1, cols - width(head));
@@ -4803,58 +4736,51 @@ function taskLines(state, row, index, cols) {
4803
4736
  });
4804
4737
  const model = task.assignedModel?.modelLabel ?? (task.type === "ai" ? "default model" : "");
4805
4738
  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") : ""}` : "";
4739
+ const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}${row.autonomous ? style.yellow(" \u26A1") : ""}` : "";
4808
4740
  const runner = task.assignedRunner ?? "";
4809
4741
  const bodyPad = parent ? " " : " ";
4810
4742
  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;
4743
+ 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
4744
  const meta = [activity, runner, structured ? "structured" : "", model].filter(Boolean).join(" \xB7 ");
4814
4745
  if (meta) lines.push(style.grey(truncate(`${bodyPad}${meta}`, cols)));
4815
4746
  if (task.transport?.fallback) lines.push(style.yellow(truncate(`${bodyPad}terminal: ${task.transport.fallback}`, cols)));
4816
4747
  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)})` : "";
4748
+ if (row.conflict) {
4749
+ const where = row.conflict.repo ? ` in ${row.conflict.repo}` : "";
4750
+ const files = row.conflict.files ? ` (${row.conflict.files})` : "";
4820
4751
  lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where}${files} \u2014 its work is kept on its own branch`, cols)));
4821
4752
  }
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})` : "";
4753
+ if (row.repairing) {
4754
+ const files = row.repairing.files ? ` in ${row.repairing.files}` : "";
4755
+ const attempt = row.repairing.attempt ? ` (attempt ${row.repairing.attempt.attempt}/${row.repairing.attempt.limit})` : "";
4825
4756
  lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
4826
4757
  }
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)));
4758
+ if (row.mergeGate) {
4759
+ lines.push(style.yellow(truncate(`${bodyPad}\u23F8 waits for Merge all \u2014 ${row.mergeGate.join(", ")} not merged into your branch yet`, cols)));
4833
4760
  }
4834
- if (task.status === "awaiting_user" && task.awaitingReason === "files-changed") {
4761
+ if (row.opsChangedFiles) {
4835
4762
  lines.push(style.yellow(truncate(`${bodyPad}\u26A0 an ops task changed tracked files \u2014 check them, then m done or /retry`, cols)));
4836
4763
  }
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)));
4764
+ if (row.repaired?.landed) {
4765
+ lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${row.repaired.files}`, cols)));
4839
4766
  }
4840
4767
  let editorLine;
4841
4768
  if (expanded) {
4842
4769
  lines.push(style.grey(`${bodyPad}${task.status.replace(/_/g, " ")}`));
4843
4770
  if (task.isolation && task.isolation.state !== "none" && task.isolation.branch) {
4844
4771
  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));
4772
+ if (row.repos.length > 0) lines.push(...taskText("Repos", row.repos.join(", "), cols, bodyPad));
4847
4773
  if (task.isolation.state !== "integrated" && task.isolation.worktree) {
4848
4774
  lines.push(...taskText("Worktree", task.isolation.worktree, cols, bodyPad));
4849
4775
  }
4850
4776
  }
4851
- if (task.ops) {
4777
+ if (row.kind === "ops") {
4852
4778
  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
4779
  }
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));
4780
+ if (row.forcedPastGate) {
4781
+ lines.push(...taskText("Forced", `Started before the work of ${row.forcedPastGate.join(", ")} was merged into your branch.`, cols, bodyPad));
4856
4782
  }
4857
- if (modeInfo?.autonomous) {
4783
+ if (row.autonomous) {
4858
4784
  lines.push(...taskText("Autonomy", "Runs without permission prompts (Full). Change the level with /auto.", cols, bodyPad));
4859
4785
  }
4860
4786
  const editableSeed = task.prompt ?? task.description ?? task.title;
@@ -4868,10 +4794,8 @@ function taskLines(state, row, index, cols) {
4868
4794
  editorLine = lines.length + 1 + cp.line;
4869
4795
  lines.push(...taskPromptLines(wrapped, cp, bodyPad));
4870
4796
  }
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));
4797
+ if (row.dependencies.length > 0) {
4798
+ lines.push(...taskText("Depends on", row.dependencies.join(", "), cols, bodyPad));
4875
4799
  }
4876
4800
  lines.push(style.grey(`${bodyPad}enter save \xB7 esc cancel`));
4877
4801
  lines.push(style.grey(truncate(`${bodyPad}then R runner \xB7 o model \xB7 e effort \xB7 M mode \xB7 D deps`, cols)));
@@ -4918,7 +4842,13 @@ function helpLayout(rows, cols) {
4918
4842
  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
4843
  );
4920
4844
  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")
4845
+ "",
4846
+ style.bold("In a task view (t or /terminal on a structured task)"),
4847
+ style.grey(" ctrl-s sends now: interrupts the running step, then delivers the composer text \u2014 or, with it empty, the selected queued message"),
4848
+ style.grey(" ctrl-n/ctrl-p select a queued message \xB7 ctrl-r removes it \xB7 ctrl-x interrupts"),
4849
+ 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"),
4850
+ 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"),
4851
+ style.grey(" alt\u2190/\u2192 changes attempt \xB7 esc returns")
4922
4852
  );
4923
4853
  const lines = body.map((line) => truncate(line, Math.max(1, cols - 2)));
4924
4854
  const room = Math.max(1, rows - 2);
@@ -4929,7 +4859,7 @@ function helpScrollMax(state) {
4929
4859
  }
4930
4860
 
4931
4861
  // src/tui/render.ts
4932
- var import_core24 = require("@ordewell/core");
4862
+ var import_core26 = require("@ordewell/core");
4933
4863
  function render(state) {
4934
4864
  const { rows, cols } = state;
4935
4865
  const rowsForBody = bodyRows(state);
@@ -5194,8 +5124,8 @@ function renderHandoff(state, overlay, rows, cols) {
5194
5124
  return frame(`Diff \u2014 ${handoffBranch(handoff)}`, [...shown, "", style.grey(`${more}enter or esc goes back`)], rows, cols);
5195
5125
  }
5196
5126
  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}`;
5127
+ const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core26.capConflictFiles)(task.repairedFiles)})`) : "";
5128
+ return ` ${style.green("\u2713")} ${(0, import_core26.titledTaskRef)(task)}${repaired}`;
5199
5129
  });
5200
5130
  const actions = handoffActions(handoff).map((action, index) => {
5201
5131
  const active = index === overlay.index;
@@ -5284,7 +5214,10 @@ function frame(title, body, rows, cols) {
5284
5214
  }
5285
5215
 
5286
5216
  // src/tui/reducer.ts
5287
- var import_core30 = require("@ordewell/core");
5217
+ var import_core34 = require("@ordewell/core");
5218
+
5219
+ // src/tui/reducers/overlays.ts
5220
+ var import_core32 = require("@ordewell/core");
5288
5221
 
5289
5222
  // src/tui/reducers/shared.ts
5290
5223
  function step(state, effects = []) {
@@ -5333,10 +5266,10 @@ function handleApprovalKey(state, overlay, key) {
5333
5266
  }
5334
5267
 
5335
5268
  // src/tui/reducers/commands.ts
5336
- var import_core28 = require("@ordewell/core");
5269
+ var import_core31 = require("@ordewell/core");
5337
5270
 
5338
5271
  // src/tui/reducers/pickers.ts
5339
- var import_core25 = require("@ordewell/core");
5272
+ var import_core27 = require("@ordewell/core");
5340
5273
  var import_plan_utils2 = require("@ordewell/core/plan-utils");
5341
5274
  var DEFAULT_EFFORT = "__runner_default__";
5342
5275
  function picker(title, items, action, extra = {}) {
@@ -5355,11 +5288,11 @@ function modelDetail(m) {
5355
5288
  function providerErrorHint(state) {
5356
5289
  const failed = Object.keys(state.providerErrors ?? {});
5357
5290
  if (failed.length === 0) return void 0;
5358
- const names = failed.map((p) => import_core25.ALL_PROVIDERS[p]?.label ?? p);
5291
+ const names = failed.map((p) => import_core27.ALL_PROVIDERS[p]?.label ?? p);
5359
5292
  return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
5360
5293
  }
5361
5294
  function plannerModelItems(state) {
5362
- const runner = (0, import_core25.runnerForProvider)(state.plannerProvider);
5295
+ const runner = (0, import_core27.runnerForProvider)(state.plannerProvider);
5363
5296
  if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
5364
5297
  const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
5365
5298
  id: m.id,
@@ -5373,7 +5306,7 @@ function plannerModelItems(state) {
5373
5306
  return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
5374
5307
  }
5375
5308
  function plannerEffortItems(state) {
5376
- const runner = (0, import_core25.runnerForProvider)(state.plannerProvider);
5309
+ const runner = (0, import_core27.runnerForProvider)(state.plannerProvider);
5377
5310
  if (!runner) {
5378
5311
  return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
5379
5312
  }
@@ -5437,7 +5370,7 @@ function pickerItemsFor(state, action) {
5437
5370
  if (action.kind === "set-task-deps") {
5438
5371
  return (0, import_plan_utils2.dependencyCandidates)(state.tasks, action.taskId).map((candidate) => ({
5439
5372
  id: candidate.id,
5440
- label: `#${candidate.order} ${candidate.title}`,
5373
+ label: (0, import_core27.titledTaskRef)(candidate),
5441
5374
  detail: candidate.status === "completed" ? "already completed" : void 0
5442
5375
  }));
5443
5376
  }
@@ -5482,26 +5415,25 @@ function pickerItemsFor(state, action) {
5482
5415
  }
5483
5416
  function plannerItems(state) {
5484
5417
  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);
5487
- const ready = installed.has(runner);
5488
- return {
5489
- id,
5490
- label: import_core25.ALL_PROVIDERS[id].label,
5491
- detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
5492
- selected: state.plannerProvider === id,
5493
- disabled: !ready
5494
- };
5495
- });
5496
- const vendors = import_core25.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
5497
- id,
5498
- label: import_core25.ALL_PROVIDERS[id].label,
5499
- detail: import_core25.ALL_PROVIDERS[id].apiKeyEnvVar,
5500
- selected: state.plannerProvider === id
5418
+ const usability = {};
5419
+ for (const id of import_core27.CLI_PROVIDERS) {
5420
+ const runner = (0, import_core27.runnerForProvider)(id);
5421
+ usability[runner] = installed.has(runner) ? { usable: true } : { usable: false, reason: "CLI not installed or not on PATH" };
5422
+ }
5423
+ const entries = (0, import_core27.plannerBackendEntries)(usability, state.configuredProviders);
5424
+ const items = entries.map((entry) => ({
5425
+ id: entry.id,
5426
+ label: entry.label,
5427
+ detail: entry.reason,
5428
+ selected: state.plannerProvider === entry.id,
5429
+ disabled: !entry.usable
5501
5430
  }));
5502
- return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
5431
+ return entries.some((entry) => entry.kind === "vendor") ? items : [...items, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
5503
5432
  }
5504
5433
 
5434
+ // src/tui/reducers/planPane.ts
5435
+ var import_core29 = require("@ordewell/core");
5436
+
5505
5437
  // src/tui/reducers/pointer.ts
5506
5438
  var WHEEL_NOTCH = 3;
5507
5439
  function pageNotch(viewport) {
@@ -5578,13 +5510,13 @@ function handleMouseSelect(state, key) {
5578
5510
  }
5579
5511
 
5580
5512
  // src/tui/reducers/taskView.ts
5581
- var import_core26 = require("@ordewell/core");
5513
+ var import_core28 = require("@ordewell/core");
5582
5514
  function openTaskView(state, sessionId, taskId) {
5583
5515
  const task = findTask(state.tasks, taskId);
5584
5516
  if (!task) return step(say(state, "system", `No task ${taskId} in this plan.`));
5585
5517
  const taskView = {
5586
5518
  taskId,
5587
- view: import_core26.EMPTY_TASK_LOG,
5519
+ view: import_core28.EMPTY_TASK_LOG,
5588
5520
  attempts: [],
5589
5521
  attempt: 0,
5590
5522
  pending: [],
@@ -5617,13 +5549,13 @@ function applyLive(tv, attempt, events) {
5617
5549
  }
5618
5550
  next = {
5619
5551
  ...next,
5620
- view: import_core26.EMPTY_TASK_LOG,
5552
+ view: import_core28.EMPTY_TASK_LOG,
5621
5553
  attempt,
5622
5554
  attempts: next.attempts.includes(attempt) ? next.attempts : [...next.attempts, attempt]
5623
5555
  };
5624
5556
  }
5625
5557
  if (attempt < next.attempt) return next;
5626
- return clampQueueIndex({ ...next, view: events.reduce(import_core26.reduceTaskLog, next.view) });
5558
+ return clampQueueIndex({ ...next, view: events.reduce(import_core28.reduceTaskLog, next.view) });
5627
5559
  }
5628
5560
  function taskLogAbandoned(state) {
5629
5561
  const tv = state.taskView;
@@ -5643,15 +5575,22 @@ function taskLogArrived(state, action) {
5643
5575
  function announceApprovals(state, action) {
5644
5576
  if (state.taskView?.taskId === action.taskId) return state;
5645
5577
  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);
5578
+ const spoken = action.events.reduce((s, event) => event.type === "approval_requested" ? say(s, "system", `\xB7 Task ${task?.order ?? "?"} waits for approval: ${(0, import_core28.runnerToolSubject)(event.tool, event.args)} \u2014 t on it, then ctrl-y to allow or ctrl-g to deny`) : s, state);
5647
5579
  return spoken !== state ? { ...spoken, scroll: state.scroll } : spoken;
5648
5580
  }
5581
+ function announceCheckpoint(state, action) {
5582
+ const task = findTask(state.tasks, action.taskId) ?? state.tasks.find((t) => t.title === action.title);
5583
+ const label = task ? `Task ${task.order}` : action.title;
5584
+ const where = task?.transport?.kind === "structured" ? "t on it to answer" : `/checkpoint ${task?.order ?? "<id>"} approve|reject to answer`;
5585
+ const spoken = say(state, "system", `\xB7 ${label} asks: ${(0, import_core28.truncateCheckpointSummary)(action.summary)} \u2014 ${where}`);
5586
+ return { ...spoken, scroll: state.scroll };
5587
+ }
5649
5588
  function taskLogLoaded(state, action) {
5650
5589
  const tv = state.taskView;
5651
5590
  if (!tv || tv.taskId !== action.taskId) return state;
5652
5591
  const loaded = action.attempt >= 1 ? {
5653
5592
  ...tv,
5654
- view: (0, import_core26.replayTaskLog)(action.events),
5593
+ view: (0, import_core28.replayTaskLog)(action.events),
5655
5594
  attempts: action.attempts ?? tv.attempts,
5656
5595
  attempt: action.attempt
5657
5596
  } : { ...tv, attempts: action.attempts ?? tv.attempts };
@@ -5673,6 +5612,29 @@ function answerApproval(state, tv, sessionId, decision) {
5673
5612
  const next = note ? { ...state, editor: { ...state.editor, text: "", cursor: 0 } } : state;
5674
5613
  return step(next, [{ type: "answerTaskApproval", sessionId, approvalId: approval.approvalId, answer }]);
5675
5614
  }
5615
+ function answerCheckpoint(state, sessionId, taskId, answer, reason = "") {
5616
+ const task = findTask(state.tasks, taskId);
5617
+ if (!waitingCheckpoint(task)) return step(say(state, "system", `Task ${task?.order ?? taskId} is not waiting at a checkpoint.`));
5618
+ const note = answer === "reject" ? reason.trim() : "";
5619
+ return step(state, [{ type: "answerTaskCheckpoint", sessionId, taskId, answer, ...note ? { reason: note } : {} }]);
5620
+ }
5621
+ function answerWaiting(state, tv, sessionId, decision) {
5622
+ if (waitingApproval(tv) || !waitingCheckpoint(findTask(state.tasks, tv.taskId))) return answerApproval(state, tv, sessionId, decision);
5623
+ if (decision === "allow") return answerCheckpoint(state, sessionId, tv.taskId, "approve");
5624
+ const answered = answerCheckpoint(state, sessionId, tv.taskId, "reject", state.editor.text);
5625
+ return state.editor.text.trim() ? { ...answered, state: { ...answered.state, editor: { ...answered.state.editor, text: "", cursor: 0 } } } : answered;
5626
+ }
5627
+ function sendNow(state, tv, sessionId) {
5628
+ const text = state.editor.text.trim();
5629
+ if (text) {
5630
+ 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."));
5631
+ return step({ ...state, editor: commit(state.editor) }, [{ type: "forceSendTaskMessage", sessionId, taskId: tv.taskId, text }]);
5632
+ }
5633
+ const queued = tv.view.queued[tv.queuedIndex];
5634
+ if (!queued) return step(say(state, "system", "Nothing to send now: type a message, or pick a queued one with ctrl-n/ctrl-p."));
5635
+ if (queued.handedOver) return step(say(state, "system", "The runner already has that message; it reads it after its current step."));
5636
+ return step(state, [{ type: "forceSendQueuedTaskMessage", sessionId, taskId: tv.taskId, messageId: queued.id }]);
5637
+ }
5676
5638
  function handleTaskViewKey(state, key) {
5677
5639
  const tv = state.taskView;
5678
5640
  if (!tv || !state.sessionId) return null;
@@ -5692,14 +5654,16 @@ function handleTaskViewKey(state, key) {
5692
5654
  if (key.name === "ctrl-r") {
5693
5655
  const queued = tv.view.queued[tv.queuedIndex];
5694
5656
  if (!queued) return step(say(state, "system", "No queued message to remove."));
5657
+ if (queued.handedOver) return step(say(state, "system", "The runner already has that message; it can no longer be taken back."));
5695
5658
  return step(state, [{ type: "removeTaskMessage", sessionId: state.sessionId, taskId: tv.taskId, messageId: queued.id }]);
5696
5659
  }
5660
+ if (key.name === "ctrl-s") return sendNow(state, tv, state.sessionId);
5697
5661
  if (key.name === "ctrl-x") {
5698
5662
  return step(state, [{ type: "interruptTask", sessionId: state.sessionId, taskId: tv.taskId }]);
5699
5663
  }
5700
- if (key.name === "ctrl-y") return answerApproval(state, tv, state.sessionId, "allow");
5664
+ if (key.name === "ctrl-y") return answerWaiting(state, tv, state.sessionId, "allow");
5701
5665
  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");
5666
+ if (key.name === "ctrl-g") return answerWaiting(state, tv, state.sessionId, "deny");
5703
5667
  if (key.name === "ctrl-n" || key.name === "ctrl-p") {
5704
5668
  const count = tv.view.queued.length;
5705
5669
  if (count === 0) return null;
@@ -5720,7 +5684,7 @@ function openTaskRunnerPicker(state, task) {
5720
5684
  ...state,
5721
5685
  overlay: {
5722
5686
  kind: "picker",
5723
- picker: picker(`Runner \xB7 #${task.order} ${task.title}`, pickerItemsFor(state, action), action, {
5687
+ picker: picker(`Runner \xB7 ${(0, import_plan_utils3.titledTaskRef)(task)}`, pickerItemsFor(state, action), action, {
5724
5688
  hint: "Changing the runner re-picks this task model, effort and mode for it."
5725
5689
  })
5726
5690
  }
@@ -5728,14 +5692,12 @@ function openTaskRunnerPicker(state, task) {
5728
5692
  );
5729
5693
  }
5730
5694
  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
5695
  return step({
5734
5696
  ...state,
5735
5697
  overlay: {
5736
5698
  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.",
5699
+ title: `Remove ${(0, import_plan_utils3.titledTaskRef)(task)}?`,
5700
+ message: (0, import_plan_utils3.dependentsNotice)((0, import_plan_utils3.dependentsOf)(state.tasks, task.id)) ?? "This cannot be undone.",
5739
5701
  action: { kind: "remove-task", taskId: task.id }
5740
5702
  }
5741
5703
  });
@@ -5744,26 +5706,22 @@ function toggleTaskOps(state, task, subtask, to = !task.ops) {
5744
5706
  if (!state.sessionId) return fail3(state, "No active plan.");
5745
5707
  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
5708
  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.`);
5709
+ if (!!task.ops === to) return fail3(state, `${(0, import_plan_utils3.taskRef)(task)} is already ${to ? "an ops" : "a change"} task.`);
5748
5710
  return step(state, [{
5749
5711
  type: "updateTask",
5750
5712
  sessionId: state.sessionId,
5751
5713
  taskId: task.id,
5752
5714
  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.`
5715
+ 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
5716
  }]);
5755
5717
  }
5756
5718
  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
5719
  return step({
5762
5720
  ...state,
5763
5721
  overlay: {
5764
5722
  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.`,
5723
+ title: `Force start ${(0, import_plan_utils3.titledTaskRef)(task)}?`,
5724
+ message: (0, import_plan_utils3.pastGateConfirmation)((0, import_plan_utils3.titledRefs)(task.mergeGate ?? [], state.tasks), "It"),
5767
5725
  action: { kind: "force-start-gated", taskId: task.id }
5768
5726
  }
5769
5727
  });
@@ -5771,12 +5729,12 @@ function confirmForceStartPastGate(state, task) {
5771
5729
  function openTaskDepsPicker(state, task) {
5772
5730
  const action = { kind: "set-task-deps", taskId: task.id };
5773
5731
  const items = pickerItemsFor(state, action);
5774
- if (items.length === 0) return fail3(state, `Nothing runs before #${task.order}, so it has no possible dependencies.`);
5732
+ if (items.length === 0) return fail3(state, `Nothing runs before ${(0, import_plan_utils3.taskRef)(task)}, so it has no possible dependencies.`);
5775
5733
  return step({
5776
5734
  ...state,
5777
5735
  overlay: {
5778
5736
  kind: "picker",
5779
- picker: picker(`Depends on \xB7 #${task.order} ${task.title}`, items, action, {
5737
+ picker: picker(`Depends on \xB7 ${(0, import_plan_utils3.titledTaskRef)(task)}`, items, action, {
5780
5738
  hint: "Only tasks earlier in the plan can be dependencies.",
5781
5739
  multi: true,
5782
5740
  chosen: task.dependencies.filter((id) => items.some((i) => i.id === id))
@@ -5796,7 +5754,7 @@ function openTaskModePicker(state, task) {
5796
5754
  ...state,
5797
5755
  overlay: {
5798
5756
  kind: "picker",
5799
- picker: picker(`Mode \xB7 #${task.order}`, pickerItemsFor(state, action), action, {
5757
+ picker: picker(`Mode \xB7 ${(0, import_plan_utils3.taskRef)(task)}`, pickerItemsFor(state, action), action, {
5800
5758
  hint: `Modes declared by ${task.assignedRunner}.`
5801
5759
  })
5802
5760
  }
@@ -5811,7 +5769,7 @@ function openTaskModelPicker(state, task) {
5811
5769
  ...state,
5812
5770
  overlay: {
5813
5771
  kind: "picker",
5814
- picker: picker(`Model \xB7 #${task.order} ${task.title}`, pickerItemsFor(state, action), action, {
5772
+ picker: picker(`Model \xB7 ${(0, import_plan_utils3.titledTaskRef)(task)}`, pickerItemsFor(state, action), action, {
5815
5773
  hint: task.assignedRunner ? `Showing models discovered for ${task.assignedRunner}.` : "Choose the model this task will run with."
5816
5774
  })
5817
5775
  }
@@ -5828,7 +5786,7 @@ function openTaskEffortPicker(state, task) {
5828
5786
  ...state,
5829
5787
  overlay: {
5830
5788
  kind: "picker",
5831
- picker: picker(`Thinking effort \xB7 #${task.order}`, pickerItemsFor(state, action), action, {
5789
+ picker: picker(`Thinking effort \xB7 ${(0, import_plan_utils3.taskRef)(task)}`, pickerItemsFor(state, action), action, {
5832
5790
  hint: `${task.assignedModel.modelLabel} \xB7 choose runner default or a supported effort`
5833
5791
  })
5834
5792
  }
@@ -5845,7 +5803,7 @@ function assignTaskModel(state, sessionId, task, model) {
5845
5803
  // JSON drops `undefined`; null is intentional here so changing models can
5846
5804
  // also clear a stale legacy top-level effort on the persisted task.
5847
5805
  changes: { assignedModel, thinkingEffort: assignedModel.thinkingEffort ?? null },
5848
- message: `Task #${task.order} model set to ${model.label}.`
5806
+ message: `Task ${(0, import_plan_utils3.taskRef)(task)} model set to ${model.label}.`
5849
5807
  }]);
5850
5808
  }
5851
5809
  function assignTaskRunner(state, sessionId, task, runner, runnerLabel) {
@@ -5854,7 +5812,7 @@ function assignTaskRunner(state, sessionId, task, runner, runnerLabel) {
5854
5812
  sessionId,
5855
5813
  taskId: task.id,
5856
5814
  changes: { assignedRunner: runner },
5857
- message: `Task #${task.order} runner set to ${runnerLabel}.`
5815
+ message: `Task ${(0, import_plan_utils3.taskRef)(task)} runner set to ${runnerLabel}.`
5858
5816
  }]);
5859
5817
  }
5860
5818
  function assignTaskMode(state, sessionId, task, mode, modeLabel) {
@@ -5863,7 +5821,7 @@ function assignTaskMode(state, sessionId, task, mode, modeLabel) {
5863
5821
  sessionId,
5864
5822
  taskId: task.id,
5865
5823
  changes: { taskMode: mode },
5866
- message: `Task #${task.order} mode set to ${modeLabel}.`
5824
+ message: `Task ${(0, import_plan_utils3.taskRef)(task)} mode set to ${modeLabel}.`
5867
5825
  }]);
5868
5826
  }
5869
5827
  function assignTaskEffort(state, sessionId, task, thinkingEffort) {
@@ -5873,7 +5831,7 @@ function assignTaskEffort(state, sessionId, task, thinkingEffort) {
5873
5831
  sessionId,
5874
5832
  taskId: task.id,
5875
5833
  changes: { assignedModel, thinkingEffort: thinkingEffort ?? null },
5876
- message: `Task #${task.order} thinking effort set to ${thinkingEffort ?? "runner default"}.`
5834
+ message: `Task ${(0, import_plan_utils3.taskRef)(task)} thinking effort set to ${thinkingEffort ?? "runner default"}.`
5877
5835
  }]);
5878
5836
  }
5879
5837
  function taskModelCommand(state, args) {
@@ -5975,9 +5933,6 @@ function taskActionEffect(state, sessionId, taskId, action) {
5975
5933
  const watch = SPAWNS_RUNNER.includes(action) && state.status !== "executing";
5976
5934
  return watch ? { type: "taskAction", sessionId, taskId, action, watch: true } : { type: "taskAction", sessionId, taskId, action };
5977
5935
  }
5978
- function markAction(task) {
5979
- return task.status === "completed" ? "uncomplete" : "complete";
5980
- }
5981
5936
  function handlePlanKey(state, key) {
5982
5937
  const editingTask = state.expandedTaskId ? findTask(state.tasks, state.expandedTaskId) : void 0;
5983
5938
  if (editingTask && state.taskEditor) return handleTaskEditKey(state, editingTask, state.taskEditor, key);
@@ -6012,7 +5967,7 @@ function handlePlanKey(state, key) {
6012
5967
  if (!state.sessionId || key.name !== "char") return step(state);
6013
5968
  if (key.char === "E") return step(state, [{ type: "execute", sessionId: state.sessionId }]);
6014
5969
  if (key.char === "S") return step(state, [{ type: "stopExecution", sessionId: state.sessionId }]);
6015
- const action = key.char === "m" ? markAction(task) : PLAN_SHORTCUTS[key.char ?? ""];
5970
+ const action = key.char === "m" ? (0, import_core29.markAction)(task) : PLAN_SHORTCUTS[key.char ?? ""];
6016
5971
  if (action === "force-start" && task.mergeGate?.length) return confirmForceStartPastGate(state, task);
6017
5972
  if (action) {
6018
5973
  return step(state, [taskActionEffect(state, state.sessionId, task.id, action)]);
@@ -6047,14 +6002,14 @@ function commitTaskEdit(state, task, editor) {
6047
6002
  sessionId: state.sessionId,
6048
6003
  taskId: task.id,
6049
6004
  changes: { prompt },
6050
- message: `Task #${task.order} prompt updated.`
6005
+ message: `Task ${(0, import_core29.taskRef)(task)} prompt updated.`
6051
6006
  }]);
6052
6007
  }
6053
6008
 
6054
6009
  // src/tui/reducers/turnQueue.ts
6055
- var import_core27 = require("@ordewell/core");
6010
+ var import_core30 = require("@ordewell/core");
6056
6011
  function drainQueue(state, settled) {
6057
- const next = (0, import_core27.drainNext)(state.queuedPrompts);
6012
+ const next = (0, import_core30.drainNext)(state.queuedPrompts);
6058
6013
  const ended = disarmStop({ ...settled, stopRequested: false });
6059
6014
  if (!next) return step(ended);
6060
6015
  const spoken = say(ended, "user", next.text);
@@ -6073,14 +6028,14 @@ function stopPlanning(state, sessionId) {
6073
6028
  return step(queueToEditor(cutTurn({ ...dismissed, stopArmed: false, stopRequested: true })), [{ type: "cancelPlanning", sessionId }]);
6074
6029
  }
6075
6030
  function queueToEditor(state) {
6076
- const all = (0, import_core27.unsendAll)(state.queuedPrompts);
6031
+ const all = (0, import_core30.unsendAll)(state.queuedPrompts);
6077
6032
  if (!all) return state;
6078
6033
  return { ...state, queuedPrompts: all.rest, editor: { ...state.editor, text: all.text, cursor: all.text.length } };
6079
6034
  }
6080
6035
  function unsendLatest(state) {
6081
- const latest = (0, import_core27.unsendLatest)(state.queuedPrompts);
6036
+ const latest = (0, import_core30.unsendLatest)(state.queuedPrompts);
6082
6037
  if (!latest) return step(state);
6083
- const text = (0, import_core27.aheadOfDraft)(latest.text, state.editor.text);
6038
+ const text = (0, import_core30.aheadOfDraft)(latest.text, state.editor.text);
6084
6039
  return step({
6085
6040
  ...state,
6086
6041
  queuedPrompts: latest.rest,
@@ -6097,7 +6052,7 @@ function plannerEscape(state) {
6097
6052
  }
6098
6053
 
6099
6054
  // src/tui/reducers/commands.ts
6100
- var KNOWN_PROVIDERS2 = Object.keys(import_core28.ALL_PROVIDERS);
6055
+ var KNOWN_PROVIDERS = Object.keys(import_core31.ALL_PROVIDERS);
6101
6056
  function runCommand(state, { name, args }) {
6102
6057
  if (!findCommand(name)) {
6103
6058
  return fail3(state, `Unknown command: /${name} \u2014 type /help to see what's available.`);
@@ -6184,6 +6139,14 @@ function runCommand(state, { name, args }) {
6184
6139
  if (!text) return fail3(state, "Usage: /continue <id> <message>");
6185
6140
  return continueTaskStep(state, sessionId, taskId, text);
6186
6141
  });
6142
+ case "checkpoint":
6143
+ return taskCommand(state, args[0], (sessionId, taskId) => {
6144
+ const answer = args[1]?.toLowerCase();
6145
+ const reason = args.slice(2).join(" ").trim();
6146
+ if (answer !== "approve" && answer !== "reject") return fail3(state, "Usage: /checkpoint <id> approve|reject [reason]");
6147
+ if (answer === "approve" && reason) return fail3(state, "Approving takes no note \u2014 reject with a reason to tell the task something.");
6148
+ return answerCheckpoint(state, sessionId, taskId, answer, reason);
6149
+ });
6187
6150
  case "task-runner":
6188
6151
  return taskRunnerCommand(state, args);
6189
6152
  case "task-model":
@@ -6255,8 +6218,8 @@ function newSession(state) {
6255
6218
  taskView: null,
6256
6219
  tasks: [],
6257
6220
  planApproved: false,
6258
- conversation: import_core28.EMPTY_CONVERSATION,
6259
- turnGate: import_core28.NO_TURN,
6221
+ conversation: import_core31.EMPTY_CONVERSATION,
6222
+ turnGate: import_core31.NO_TURN,
6260
6223
  selectedTask: 0,
6261
6224
  expandedTaskId: null,
6262
6225
  taskEditor: null,
@@ -6267,14 +6230,14 @@ function newSession(state) {
6267
6230
  // Prompts belong to the session that raised them; the old planner is gone
6268
6231
  // and its pending requests deny on their own timeout.
6269
6232
  pendingApprovals: [],
6270
- queuedPrompts: import_core28.EMPTY_HOLD,
6233
+ queuedPrompts: import_core31.EMPTY_HOLD,
6271
6234
  stopArmed: false,
6272
6235
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6273
6236
  }, closeEffects);
6274
6237
  }
6275
6238
  function setModel(state, args) {
6276
6239
  if (args[0] === "set" && args[1]) {
6277
- const runner = (0, import_core28.runnerForProvider)(state.plannerProvider);
6240
+ const runner = (0, import_core31.runnerForProvider)(state.plannerProvider);
6278
6241
  const known = runner ? modelsForRunner(state.models, runner) : [];
6279
6242
  if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
6280
6243
  return fail3(state, `${args[1]} was not discovered for ${runner}.`);
@@ -6297,7 +6260,7 @@ function setModel(state, args) {
6297
6260
  function setPlanner(state, args) {
6298
6261
  if (args[0]) {
6299
6262
  const id = args[0].toLowerCase();
6300
- if (!KNOWN_PROVIDERS2.includes(id)) {
6263
+ if (!KNOWN_PROVIDERS.includes(id)) {
6301
6264
  return fail3(state, `Unknown planner: ${id}. Run /planner with no arguments to see the list.`);
6302
6265
  }
6303
6266
  return step(state, [{ type: "setPlanner", provider: id }]);
@@ -6336,7 +6299,7 @@ function setPlannerEffort(state, args) {
6336
6299
  function setKey(state, args) {
6337
6300
  if (args[0] === "set" && args[1]) {
6338
6301
  const provider = args[1].toLowerCase();
6339
- if (!KNOWN_PROVIDERS2.includes(provider)) {
6302
+ if (!KNOWN_PROVIDERS.includes(provider)) {
6340
6303
  return fail3(state, `Unknown provider: ${provider}. Run /key with no arguments to see the list.`);
6341
6304
  }
6342
6305
  if (!args[2]) {
@@ -6347,16 +6310,16 @@ function setKey(state, args) {
6347
6310
  }
6348
6311
  return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
6349
6312
  }
6350
- const items = import_core28.PROVIDER_PRIORITY.map((id) => ({
6313
+ const items = import_core31.PROVIDER_PRIORITY.map((id) => ({
6351
6314
  id,
6352
- label: import_core28.ALL_PROVIDERS[id].label,
6353
- detail: import_core28.ALL_PROVIDERS[id].apiKeyEnvVar,
6315
+ label: import_core31.ALL_PROVIDERS[id].label,
6316
+ detail: import_core31.ALL_PROVIDERS[id].apiKeyEnvVar,
6354
6317
  selected: state.configuredProviders.includes(id)
6355
6318
  }));
6356
6319
  return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
6357
6320
  }
6358
6321
  function keyPrompt(provider) {
6359
- const meta = import_core28.ALL_PROVIDERS[provider];
6322
+ const meta = import_core31.ALL_PROVIDERS[provider];
6360
6323
  return {
6361
6324
  kind: "prompt",
6362
6325
  title: `${meta.label} API key`,
@@ -6433,19 +6396,19 @@ function runners(state, args) {
6433
6396
  }
6434
6397
  function setMaxParallel(state, arg) {
6435
6398
  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);
6399
+ const limit = (0, import_core31.parseMaxParallel)(arg);
6437
6400
  if (limit === null) return fail3(state, "Usage: /parallel [<number of tasks, 1 or more>]");
6438
6401
  return step({ ...state, maxParallel: limit }, [{ type: "setMaxParallel", limit }]);
6439
6402
  }
6440
6403
  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);
6404
+ if (arg === void 0) return step(say(state, "system", `Autonomy level: ${(0, import_core31.autonomyLevelLabel)(state.autonomous)} \u2014 /auto full or /auto guarded changes it for new plans.`));
6405
+ const enabled = (0, import_core31.parseAutonomyLevel)(arg);
6443
6406
  if (enabled === null) return fail3(state, "Usage: /auto [full|guarded]");
6444
6407
  return step({ ...state, autonomous: enabled }, [{ type: "setAutonomous", enabled }]);
6445
6408
  }
6446
6409
  function setTransport(state, arg) {
6447
6410
  const value = arg?.toLowerCase() ?? (state.runnerTransport === "structured" ? "terminal" : "structured");
6448
- if (!(0, import_core28.isRunnerTransport)(value)) return fail3(state, "Usage: /transport [terminal|structured]");
6411
+ if (!(0, import_core31.isRunnerTransport)(value)) return fail3(state, "Usage: /transport [terminal|structured]");
6449
6412
  return step(state, [{ type: "setTransport", transport: value }]);
6450
6413
  }
6451
6414
  function setMouseCapture(state, arg) {
@@ -6622,7 +6585,7 @@ function choose(state, picker2, item) {
6622
6585
  sessionId: state.sessionId,
6623
6586
  taskId: task.id,
6624
6587
  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.`
6588
+ message: picker2.chosen.length > 0 ? `Task ${(0, import_core32.taskRef)(task)} now depends on ${picker2.chosen.length} task${picker2.chosen.length === 1 ? "" : "s"}.` : `Task ${(0, import_core32.taskRef)(task)} no longer depends on anything.`
6626
6589
  }]);
6627
6590
  }
6628
6591
  if (!item) return step(state);
@@ -6699,7 +6662,7 @@ function choose(state, picker2, item) {
6699
6662
  }
6700
6663
 
6701
6664
  // src/tui/reducers/incoming.ts
6702
- var import_core29 = require("@ordewell/core");
6665
+ var import_core33 = require("@ordewell/core");
6703
6666
  var planText = (value, fallback = "") => sanitize(String(value ?? fallback));
6704
6667
  var planLabel = (value, fallback = "") => planText(value, fallback).replace(/\n+/g, " ");
6705
6668
  var asRecord = (value) => typeof value === "object" && value !== null ? value : {};
@@ -6730,7 +6693,7 @@ function toTaskView2(t, index) {
6730
6693
  prompt: typeof t.prompt === "string" ? sanitize(t.prompt) : void 0,
6731
6694
  type: t.type === "user" ? "user" : "ai",
6732
6695
  status: planLabel(t.status, "pending"),
6733
- awaitingReason: t.status === "awaiting_user" && (0, import_core29.isAwaitingReason)(t.awaitingReason) ? t.awaitingReason : void 0,
6696
+ awaitingReason: t.status === "awaiting_user" && (0, import_core33.isAwaitingReason)(t.awaitingReason) ? t.awaitingReason : void 0,
6734
6697
  dependencies: Array.isArray(t.dependencies) ? t.dependencies.map((id) => planLabel(id)) : [],
6735
6698
  assignedRunner: typeof t.assignedRunner === "string" ? planLabel(t.assignedRunner) : void 0,
6736
6699
  taskMode: typeof t.taskMode === "string" ? planLabel(t.taskMode) : void 0,
@@ -6763,12 +6726,12 @@ function applySettings(state, settings) {
6763
6726
  plannerProvider: typeof settings.aiProvider === "string" ? settings.aiProvider : state.plannerProvider,
6764
6727
  plannerEffort: typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : state.plannerEffort,
6765
6728
  maxParallel: typeof settings.maxParallel === "number" ? settings.maxParallel : state.maxParallel,
6766
- runnerTransport: (0, import_core29.isRunnerTransport)(settings.runnerTransport) ? settings.runnerTransport : state.runnerTransport,
6729
+ runnerTransport: (0, import_core33.isRunnerTransport)(settings.runnerTransport) ? settings.runnerTransport : state.runnerTransport,
6767
6730
  allowlist: settings.modelAllowlist && typeof settings.modelAllowlist === "object" ? settings.modelAllowlist : state.allowlist
6768
6731
  };
6769
6732
  }
6770
6733
  function runLabel(tasks, gate = null) {
6771
- const running = tasks.filter(isTaskRunning);
6734
+ const running = tasks.filter(import_core24.isTaskRunning);
6772
6735
  if (running.length > 0) {
6773
6736
  const [first] = running;
6774
6737
  const name = first.assignedRunner ? `${first.title} \xB7 ${first.assignedRunner}` : first.title;
@@ -6834,13 +6797,13 @@ function reduceAction(state, action) {
6834
6797
  goal: "",
6835
6798
  taskView: null,
6836
6799
  pendingApprovals: [],
6837
- queuedPrompts: import_core30.EMPTY_HOLD,
6800
+ queuedPrompts: import_core34.EMPTY_HOLD,
6838
6801
  stopArmed: false,
6839
6802
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6840
6803
  });
6841
6804
  case "chatRestored":
6842
6805
  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 });
6806
+ return step({ ...state, conversation: (0, import_core34.fromTranscript)(action.history, action.researchLog, action.plannerUsage), scroll: 0 });
6844
6807
  case "planUpdated": {
6845
6808
  if (stale(state, action.sessionId)) return step(state);
6846
6809
  const isolation = isolationOfPlan(action.plan);
@@ -6887,14 +6850,17 @@ function reduceAction(state, action) {
6887
6850
  case "taskStarted": {
6888
6851
  if (stale(state, action.sessionId)) return step(state);
6889
6852
  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));
6853
+ if (existing && (0, import_core24.isTaskRunning)(existing)) return step(state);
6854
+ const tasks = state.tasks.map((t) => t.id === action.taskId && !(0, import_core24.isTaskRunning)(t) ? { ...t, status: "in_progress" } : t);
6855
+ const spoken = say(state, "system", (0, import_core34.taskStartedNotice)(action.title, action.runner));
6893
6856
  return step({ ...spoken, tasks, status: "executing", busyLabel: runLabel(tasks) });
6894
6857
  }
6858
+ case "taskCheckpoint":
6859
+ if (stale(state, action.sessionId)) return step(state);
6860
+ return step(announceCheckpoint(state, action));
6895
6861
  case "taskStatus": {
6896
6862
  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);
6863
+ const tasks = state.tasks.map((t) => t.id === action.taskId ? { ...t, status: action.status, awaitingReason: void 0, checkpoint: void 0 } : t);
6898
6864
  const status = runStatus(state, tasks);
6899
6865
  return step({ ...state, status, tasks, busyLabel: status === "executing" ? runLabel(tasks, state.gate) : state.busyLabel });
6900
6866
  }
@@ -6909,13 +6875,14 @@ function reduceAction(state, action) {
6909
6875
  const isolation = update.isolation ?? t.isolation;
6910
6876
  const transport = update.transport;
6911
6877
  const awaitingReason = update.awaitingReason;
6878
+ const checkpoint = update.checkpoint;
6912
6879
  const continuable = update.continuable === true;
6913
6880
  const awaitingApproval = update.awaitingApproval;
6914
6881
  const mergeGate = update.mergeGate;
6915
6882
  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)) {
6883
+ 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
6884
  changed = true;
6918
- return { ...t, status: update.status, idleSince, isolation, transport, awaitingReason, continuable, awaitingApproval, mergeGate, forcedPastGate };
6885
+ return { ...t, status: update.status, idleSince, isolation, transport, awaitingReason, checkpoint, continuable, awaitingApproval, mergeGate, forcedPastGate };
6919
6886
  }
6920
6887
  return t;
6921
6888
  });
@@ -6926,12 +6893,6 @@ function reduceAction(state, action) {
6926
6893
  const status = runStatus(next, tasks);
6927
6894
  return step({ ...next, status, busyLabel: status === "executing" ? runLabel(tasks, gate) : state.busyLabel });
6928
6895
  }
6929
- case "queueReady": {
6930
- if (stale(state, action.sessionId)) return step(state);
6931
- const sessionId = action.sessionId ?? state.sessionId;
6932
- if (sessionId === null) return step(state);
6933
- return step(state, [{ type: "processQueued", sessionId }]);
6934
- }
6935
6896
  case "isolationBlocked": {
6936
6897
  if (stale(state, action.sessionId)) return step(state);
6937
6898
  return step(blockedPicker(state, action.message, action.repos));
@@ -6964,6 +6925,14 @@ function reduceAction(state, action) {
6964
6925
  busyLabel: ""
6965
6926
  });
6966
6927
  }
6928
+ case "executionLost": {
6929
+ if (stale(state, action.sessionId)) return step(state);
6930
+ return step({
6931
+ ...say(state, "error", "Lost the connection to the daemon \u2014 this view is no longer following the run, but the daemon may still be running it. /load this session again to see where it stands."),
6932
+ status: "idle",
6933
+ busyLabel: ""
6934
+ });
6935
+ }
6967
6936
  case "settingsLoaded":
6968
6937
  return step(applySettings(state, action.settings));
6969
6938
  case "modelsLoaded":
@@ -6998,7 +6967,7 @@ function reduceAction(state, action) {
6998
6967
  busyLabel: "",
6999
6968
  planApproved: false,
7000
6969
  pendingApprovals: [],
7001
- queuedPrompts: import_core30.EMPTY_HOLD,
6970
+ queuedPrompts: import_core34.EMPTY_HOLD,
7002
6971
  stopArmed: false,
7003
6972
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
7004
6973
  });
@@ -7065,7 +7034,7 @@ function sameGate(a, b) {
7065
7034
  return a.paused === b.paused && JSON.stringify(a.handoff) === JSON.stringify(b.handoff);
7066
7035
  }
7067
7036
  function runStatus(state, tasks) {
7068
- if (tasks.some(isTaskRunning)) return "executing";
7037
+ if (tasks.some(import_core24.isTaskRunning)) return "executing";
7069
7038
  if (tasks.some((t) => t.status === "awaiting_user") || state.gate?.paused) return "executing";
7070
7039
  return plannerInFlight(state) ? state.status : "idle";
7071
7040
  }
@@ -7134,7 +7103,7 @@ function submit(state) {
7134
7103
  return step(cleared, [{ type: "sendTaskMessage", sessionId: state.sessionId, taskId, text }]);
7135
7104
  }
7136
7105
  if (plannerInFlight(state)) {
7137
- return step({ ...cleared, queuedPrompts: (0, import_core30.holdPrompt)(state.queuedPrompts, text) });
7106
+ return step({ ...cleared, queuedPrompts: (0, import_core34.holdPrompt)(state.queuedPrompts, text) });
7138
7107
  }
7139
7108
  const spoken = say(cleared, "user", text);
7140
7109
  const effect = state.sessionId ? { type: "sendMessage", sessionId: state.sessionId, message: text } : { type: "startConversation", goal: text };
@@ -7194,10 +7163,10 @@ function createApp(deps) {
7194
7163
 
7195
7164
  // src/tui/effects.ts
7196
7165
  var import_child_process4 = require("child_process");
7197
- var import_core32 = require("@ordewell/core");
7166
+ var import_core36 = require("@ordewell/core");
7198
7167
 
7199
7168
  // src/tui/inbound.ts
7200
- var import_core31 = require("@ordewell/core");
7169
+ var import_core35 = require("@ordewell/core");
7201
7170
  var CONVERSATION_TYPES = /* @__PURE__ */ new Set([
7202
7171
  "planner_message",
7203
7172
  "planner_turn_started",
@@ -7412,7 +7381,8 @@ function dispatchLifecycle(dispatch, event, sessionId) {
7412
7381
  idleSince: task.idleSince ?? null,
7413
7382
  isolation: task.isolation,
7414
7383
  transport: task.transport,
7415
- awaitingReason: (0, import_core31.isAwaitingReason)(task.awaitingReason) ? task.awaitingReason : void 0,
7384
+ awaitingReason: (0, import_core35.isAwaitingReason)(task.awaitingReason) ? task.awaitingReason : void 0,
7385
+ ...typeof task.checkpoint === "string" && task.checkpoint ? { checkpoint: sanitize(task.checkpoint) } : {},
7416
7386
  ...task.continuable ? { continuable: true } : {},
7417
7387
  awaitingApproval: typeof task.awaitingApproval === "number" && task.awaitingApproval > 0 ? task.awaitingApproval : void 0,
7418
7388
  ...task.mergeGate?.length ? { mergeGate: task.mergeGate.map(String) } : {},
@@ -7426,10 +7396,10 @@ function dispatchLifecycle(dispatch, event, sessionId) {
7426
7396
  case "task_started":
7427
7397
  dispatch({ type: "taskStarted", taskId: String(event.taskId), title: String(event.title ?? event.taskId), runner: event.runner, sessionId });
7428
7398
  return;
7429
- // The extension shows these in its checkpoint panel; here they are
7430
- // transcript lines, which is the TUI's equivalent surface.
7399
+ // The extension shows these in its checkpoint panel; here they are a
7400
+ // transcript line, and the task view carries the card that answers them.
7431
7401
  case "checkpoint":
7432
- dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core31.truncateCheckpointSummary)(event.summary)}` });
7402
+ dispatch({ type: "taskCheckpoint", taskId: String(event.taskId), title: String(event.taskTitle), summary: event.summary, sessionId });
7433
7403
  return;
7434
7404
  case "review_needed":
7435
7405
  dispatch({ type: "notice", message: "Plan needs your sign-off \u2014 /approve to continue." });
@@ -7437,13 +7407,6 @@ function dispatchLifecycle(dispatch, event, sessionId) {
7437
7407
  case "review_approved":
7438
7408
  dispatch({ type: "notice", message: "Plan approved." });
7439
7409
  return;
7440
- // The orchestrator paused fan-out because a structural edit is queued; drain
7441
- // it so the planner reconciles the plan and dependents resume spawning.
7442
- // Dropped on the floor, the queue suppresses every later tick() — which is
7443
- // exactly "tasks did not fan out when the dependent ones finished."
7444
- case "queue_ready":
7445
- dispatch({ type: "queueReady", sessionId });
7446
- return;
7447
7410
  case "execution_complete":
7448
7411
  dispatch({ type: "executionComplete", summary: event.summary, sessionId });
7449
7412
  return;
@@ -7493,8 +7456,7 @@ async function runEffect(effect, deps) {
7493
7456
  await perform(effect, deps);
7494
7457
  } catch (err) {
7495
7458
  if (!isConnectionRefused(err)) {
7496
- const message = err instanceof Error ? err.message : String(err);
7497
- deps.dispatch({ type: "failed", message: explain(message) });
7459
+ deps.dispatch({ type: "failed", message: explain(err) });
7498
7460
  return;
7499
7461
  }
7500
7462
  let revived = false;
@@ -7511,21 +7473,20 @@ async function runEffect(effect, deps) {
7511
7473
  try {
7512
7474
  await perform(effect, deps);
7513
7475
  } catch (retryErr) {
7514
- const message = retryErr instanceof Error ? retryErr.message : String(retryErr);
7515
7476
  deps.dispatch({
7516
7477
  type: "failed",
7517
7478
  // A fresh daemon holds no sessions, so the retry of anything
7518
7479
  // session-scoped legitimately 404s. Say which of the two happened.
7519
- message: isConnectionRefused(retryErr) ? describeConnectionRefused(deps.port) : explain(message)
7480
+ message: isConnectionRefused(retryErr) ? describeConnectionRefused(deps.port) : explain(retryErr)
7520
7481
  });
7521
7482
  }
7522
7483
  }
7523
7484
  }
7524
- function explain(message) {
7525
- if (message === "Session not found") {
7485
+ function explain(err) {
7486
+ if (err instanceof DaemonError && err.code === "session_not_found") {
7526
7487
  return "This server is no longer holding that session \u2014 it was probably restarted. Reload it with /sessions.";
7527
7488
  }
7528
- return message;
7489
+ return err instanceof Error ? err.message : String(err);
7529
7490
  }
7530
7491
  function persistAfterDaemon(deps, env) {
7531
7492
  for (const [key, value] of Object.entries(env)) deps.setEnvVar(key, value);
@@ -7581,26 +7542,20 @@ async function perform(effect, deps) {
7581
7542
  timer.unref?.();
7582
7543
  return;
7583
7544
  }
7584
- case "processQueued":
7585
- await api.processQueued(effect.sessionId);
7586
- await refreshPlan(deps, effect.sessionId);
7587
- return;
7588
7545
  case "taskAction": {
7589
7546
  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);
7547
+ const request = action === "cancel" || action === "retry" || action === "force-start" ? () => api.taskControl(sessionId, taskId, action) : (0, import_core36.markRequestFor)(action) === "uncomplete" ? () => api.markTaskIncomplete(sessionId, taskId) : () => api.markTaskComplete(sessionId, taskId);
7591
7548
  await (effect.watch ? withExecutionStream(deps, sessionId, request) : request());
7592
7549
  await refreshPlan(deps, sessionId);
7593
7550
  return;
7594
7551
  }
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
- });
7552
+ case "addTask": {
7553
+ const fields = (0, import_core36.newTaskFields)(effect.title);
7554
+ if (!fields) return;
7555
+ await api.addTask(effect.sessionId, { ...fields });
7602
7556
  await refreshPlan(deps, effect.sessionId);
7603
7557
  return;
7558
+ }
7604
7559
  case "updateTask":
7605
7560
  await api.updateTask(effect.sessionId, effect.taskId, effect.changes);
7606
7561
  await refreshPlan(deps, effect.sessionId);
@@ -7627,6 +7582,13 @@ async function perform(effect, deps) {
7627
7582
  }
7628
7583
  return;
7629
7584
  }
7585
+ // The card goes when the daemon's next status says the task left the
7586
+ // checkpoint; a refusal (settled elsewhere, withdrawn) surfaces as the
7587
+ // failure the daemon words.
7588
+ case "answerTaskCheckpoint":
7589
+ if (effect.answer === "approve") await api.approveTaskCheckpoint(effect.sessionId, effect.taskId);
7590
+ else await api.rejectTaskCheckpoint(effect.sessionId, effect.taskId, effect.reason);
7591
+ return;
7630
7592
  // A task with a saved log ran structured, whatever its `transport` says
7631
7593
  // after a reload; it has no tmux window, so open the log rather than fail.
7632
7594
  case "openTaskTerminal": {
@@ -7676,6 +7638,16 @@ async function perform(effect, deps) {
7676
7638
  if (!removed) dispatch({ type: "notice", message: "That message was already delivered to the task." });
7677
7639
  return;
7678
7640
  }
7641
+ // The task log shows the forced message at the head of the queue, then
7642
+ // the interrupt, then the turn it opens; nothing to dispatch here.
7643
+ case "forceSendTaskMessage":
7644
+ await api.forceSendTaskMessage(effect.sessionId, effect.taskId, effect.text);
7645
+ return;
7646
+ case "forceSendQueuedTaskMessage": {
7647
+ const { sent } = await api.forceSendQueuedTaskMessage(effect.sessionId, effect.taskId, effect.messageId);
7648
+ if (!sent) dispatch({ type: "notice", message: "The runner already has that message; it reads it after its current step." });
7649
+ return;
7650
+ }
7679
7651
  case "interruptTask":
7680
7652
  await api.interruptTask(effect.sessionId, effect.taskId);
7681
7653
  dispatch({ type: "notice", message: "Interrupting the task\u2026" });
@@ -7699,11 +7671,11 @@ async function perform(effect, deps) {
7699
7671
  dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
7700
7672
  return;
7701
7673
  case "setPlanner": {
7702
- const meta = import_core32.ALL_PROVIDERS[effect.provider];
7674
+ const meta = import_core36.ALL_PROVIDERS[effect.provider];
7703
7675
  if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
7704
7676
  const env = { AI_PROVIDER: effect.provider };
7705
7677
  const settings = await api.updateSettings({ env });
7706
- const recall2 = describePlannerSwitch(settings, effect.provider);
7678
+ const recall2 = plannerSwitchRecall(settings);
7707
7679
  persistAfterDaemon(deps, { ...env, ORCHESTRATOR_MODEL: recall2.model, ORDEWELL_PLANNER_EFFORT: recall2.effort });
7708
7680
  dispatch({
7709
7681
  type: "settingsLoaded",
@@ -7711,7 +7683,7 @@ async function perform(effect, deps) {
7711
7683
  });
7712
7684
  dispatch({
7713
7685
  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)}`
7686
+ message: `${(0, import_core36.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
7715
7687
  });
7716
7688
  await loadModels(deps);
7717
7689
  return;
@@ -7723,7 +7695,7 @@ async function perform(effect, deps) {
7723
7695
  dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
7724
7696
  return;
7725
7697
  case "setApiKey": {
7726
- const meta = import_core32.ALL_PROVIDERS[effect.provider];
7698
+ const meta = import_core36.ALL_PROVIDERS[effect.provider];
7727
7699
  if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
7728
7700
  await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
7729
7701
  deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
@@ -7761,7 +7733,7 @@ async function perform(effect, deps) {
7761
7733
  return;
7762
7734
  case "setAutonomous":
7763
7735
  deps.setEnvVar("ORDEWELL_AUTONOMOUS_MODE", String(effect.enabled));
7764
- dispatch({ type: "notice", message: `Autonomy level: ${(0, import_core32.autonomyLevelLabel)(effect.enabled)} for new plans.` });
7736
+ dispatch({ type: "notice", message: `Autonomy level: ${(0, import_core36.autonomyLevelLabel)(effect.enabled)} for new plans.` });
7765
7737
  return;
7766
7738
  case "setMouseCapture":
7767
7739
  deps.setMouseCapture(effect.enabled);
@@ -7895,7 +7867,7 @@ function osc52(text) {
7895
7867
  function copySelection(deps, text) {
7896
7868
  if (!text) return;
7897
7869
  const lines = text.split("\n").length;
7898
- const command = (0, import_core32.clipboardCopyCommand)(deps.hasBin);
7870
+ const command = (0, import_core36.clipboardCopyCommand)(deps.hasBin);
7899
7871
  if (command) {
7900
7872
  try {
7901
7873
  (deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
@@ -7919,9 +7891,14 @@ async function withExecutionStream(deps, sessionId, request) {
7919
7891
  const inbound = inboundFor(deps.api, deps.dispatch, sessionId);
7920
7892
  const stream = deps.api.streamExecution(sessionId, inbound.execution(), settleReady);
7921
7893
  void stream.catch(settleReady);
7922
- await streamReady;
7923
- await request();
7924
- await stream;
7894
+ try {
7895
+ await streamReady;
7896
+ await request();
7897
+ } catch (err) {
7898
+ deps.api.closeExecutionStream(stream);
7899
+ throw err;
7900
+ }
7901
+ if (await stream === "lost") deps.dispatch({ type: "executionLost", sessionId });
7925
7902
  }
7926
7903
  async function converse(deps, sessionId, call) {
7927
7904
  const inbound = inboundFor(deps.api, deps.dispatch, sessionId);
@@ -8012,6 +7989,7 @@ var CONTROL2 = {
8012
7989
  "": "ctrl-o",
8013
7990
  "": "ctrl-p",
8014
7991
  "": "ctrl-r",
7992
+ "": "ctrl-s",
8015
7993
  "": "ctrl-t",
8016
7994
  "": "ctrl-u",
8017
7995
  "": "ctrl-w",
@@ -8285,7 +8263,7 @@ async function handleTui(subArgs) {
8285
8263
  }
8286
8264
  const workspace = flag(subArgs, "--workspace") || process.cwd();
8287
8265
  try {
8288
- (0, import_core33.assertWorkspaceExists)(workspace);
8266
+ (0, import_core37.assertWorkspaceExists)(workspace);
8289
8267
  } catch {
8290
8268
  console.error(
8291
8269
  `Workspace "${workspace}" does not exist.
@@ -8294,7 +8272,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
8294
8272
  );
8295
8273
  process.exit(1);
8296
8274
  }
8297
- registerSkillCommands((0, import_core33.createSkillsService)(workspace).listSkills());
8275
+ registerSkillCommands((0, import_core37.createSkillsService)(workspace).listSkills());
8298
8276
  const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
8299
8277
  const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
8300
8278
  const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
@@ -8311,7 +8289,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
8311
8289
  workspace,
8312
8290
  port,
8313
8291
  dispatch: (action) => app.dispatch(action),
8314
- newSessionId: () => (0, import_core33.mintSessionId)(),
8292
+ newSessionId: () => (0, import_core37.mintSessionId)(),
8315
8293
  setEnvVar: (key, value) => {
8316
8294
  writeEnvVar(findEnvFile(), key, value);
8317
8295
  process.env[key] = value;
@@ -8362,6 +8340,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
8362
8340
  app.dispatch({ type: "resize", rows: r, cols: c });
8363
8341
  });
8364
8342
  process.on("uncaughtException", (err) => {
8343
+ terminal?.close();
8365
8344
  console.error(`Fatal: ${err.message}`);
8366
8345
  shutdown(1);
8367
8346
  });
@@ -8402,6 +8381,7 @@ var COMMANDS = {
8402
8381
  "retry": handleRetry,
8403
8382
  "cancel": handleCancel,
8404
8383
  continue: handleContinue,
8384
+ checkpoint: handleCheckpoint,
8405
8385
  "add-task": handleAddTask,
8406
8386
  "remove-task": handleRemoveTask,
8407
8387
  "task-runner": handleTaskRunner,
@@ -8435,7 +8415,7 @@ function cliVersion() {
8435
8415
  // src/main.ts
8436
8416
  var TUI_FLAGS = /* @__PURE__ */ new Set(["--workspace", "--port"]);
8437
8417
  async function main() {
8438
- (0, import_core34.migrateOldConfigDir)();
8418
+ (0, import_core38.migrateOldConfigDir)();
8439
8419
  loadEnvFile();
8440
8420
  const argv = process.argv.slice(2);
8441
8421
  if (TUI_FLAGS.has(argv[0] ?? "")) argv.unshift("tui");