@ordewell/cli 0.7.1 → 0.8.0

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 (3) hide show
  1. package/README.md +8 -4
  2. package/dist/main.js +571 -878
  3. 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_core37 = 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");
@@ -100,7 +100,7 @@ an id.
100
100
  Planning:
101
101
  ordewell Full-screen terminal UI (everything the VS Code extension does)
102
102
  ordewell tui The same thing, named \u2014 for scripts and aliases
103
- ordewell plan --goal "text" Plan a goal conversationally (honours verify)
103
+ ordewell plan --goal "text" Plan a goal conversationally
104
104
  ordewell run Execute the last generated plan (streams status)
105
105
  ordewell approve Sign off a plan paused for review, and continue it
106
106
  ordewell stop Stop execution of the last session (or --server)
@@ -123,7 +123,6 @@ Tasks (<id> is an order number or a task ID):
123
123
  ordewell continue <id> <msg> Continue a finished task in its saved runner session
124
124
  ordewell checkpoint <id> approve|reject [reason] Answer the checkpoint a task waits at
125
125
  ordewell cancel <id> Kill a running task
126
- ordewell terminal <id> Open a real terminal attached to a task's runner
127
126
 
128
127
  Per-task assignment (omit the value to list the options):
129
128
  ordewell task-runner <id> [runner] Set a task's executor (re-picks its model, effort and mode)
@@ -131,6 +130,7 @@ Per-task assignment (omit the value to list the options):
131
130
  ordewell task-effort <id> [level] Set a task's thinking effort ("default" to clear)
132
131
  ordewell task-mode <id> [mode] Set a task's runner mode
133
132
  ordewell task-deps <id> [a,b|none] Set which earlier tasks a task waits for
133
+ ordewell task-skills <id> [a,b|none] Set the skills attached to a task (applies-to: task); none clears them
134
134
  ordewell task-ops <id> [on|off] Make a task an ops task (runs in your checkout once its dependencies are merged) or a change task
135
135
 
136
136
  Planner, models and runners:
@@ -141,19 +141,14 @@ Planner, models and runners:
141
141
  ordewell runners [<id> on|off] Enable or disable runners (claude-code, opencode, codex)
142
142
  ordewell allowlist set|clear|show Limit which models a runner may use
143
143
  ordewell auto [full|guarded] Autonomy level for new sessions: Full or Guarded
144
- ordewell transport [terminal|structured] Structured (default) or terminal task transport, from the next run
145
144
  ordewell parallel [<n>] How many AI tasks run at once (default 3; applies to a live run)
146
145
  ordewell refresh Re-discover runners and model catalogs
147
146
  ordewell models List every provider's catalog (works without a server)
148
147
 
149
- Modes:
150
- ordewell tdd [on|off] Toggle Test-Driven Development mode (or show status)
151
- ordewell verify [on|off] Toggle verification mode \u2014 adds a final evidence-based task that runs the full suite (or show status)
152
-
153
148
  Other:
154
149
  ordewell web Start the API server (foreground; --daemon for background)
155
150
  ordewell setup Interactive first-run setup wizard
156
- ordewell plugins list|install|remove|create Manage runner plugins
151
+ ordewell skills List installed skills and shadowed workspace duplicates
157
152
  ordewell --help Show this help
158
153
  ordewell --version Print the installed version (alias: version, -v)
159
154
 
@@ -171,7 +166,7 @@ Plan options:
171
166
  --goal "text" Task description (required)
172
167
  --runner <id> Runner to use (repeatable; default: all enabled runners)
173
168
  --workspace /path Workspace directory (default: cwd)
174
- --no-chat One-shot plan; skips the planner dialogue (and its toggles)
169
+ --no-chat One-shot plan; skips the planner dialogue
175
170
 
176
171
  Run options:
177
172
  --session-id <id> Execute a specific session (default: last session)
@@ -185,6 +180,10 @@ Sessions options:
185
180
  --workspace /path Workspace to list sessions from (default: cwd)
186
181
  --json Output the list as JSON
187
182
 
183
+ Skills options:
184
+ --workspace /path Workspace to read skills from (default: cwd)
185
+ --json Output resolved skills and shadowed duplicates as JSON
186
+
188
187
  Task options (add-task):
189
188
  --title "text" Task title (required)
190
189
  --description "..." Task description (default: the title)
@@ -247,13 +246,13 @@ function oldPidFileFor(port) {
247
246
  return port === DEFAULT_PORT ? (0, import_path2.join)(OLD_CONFIG_DIR, "server.pid") : (0, import_path2.join)(OLD_CONFIG_DIR, `server-${port}.pid`);
248
247
  }
249
248
  function httpGet(url, timeoutMs = 2e3, headers = {}) {
250
- return new Promise((resolve3, reject) => {
249
+ return new Promise((resolve4, reject) => {
251
250
  const req = import_http.default.get(url, { timeout: timeoutMs, headers }, (res) => {
252
251
  let data = "";
253
252
  res.on("data", (chunk) => {
254
253
  data += chunk.toString();
255
254
  });
256
- res.on("end", () => resolve3({ status: res.statusCode || 0, data }));
255
+ res.on("end", () => resolve4({ status: res.statusCode || 0, data }));
257
256
  });
258
257
  req.on("error", reject);
259
258
  req.on("timeout", () => {
@@ -406,15 +405,30 @@ var import_http2 = __toESM(require("http"));
406
405
  var import_ws = __toESM(require("ws"));
407
406
  var import_core3 = require("@ordewell/core");
408
407
  var DEFAULT_HTTP_TIMEOUT_MS = 15 * 60 * 1e3;
409
- var WorkspaceInitNeededError = class extends Error {
410
- constructor(message, workspace) {
408
+ var DaemonError = class extends Error {
409
+ constructor(message, status, code) {
411
410
  super(message);
411
+ this.status = status;
412
+ this.code = code;
413
+ this.name = "DaemonError";
414
+ }
415
+ status;
416
+ code;
417
+ };
418
+ var WorkspaceInitNeededError = class extends DaemonError {
419
+ constructor(message, workspace) {
420
+ super(message, 400, "workspace_not_a_project");
412
421
  this.workspace = workspace;
413
422
  this.name = "WorkspaceInitNeededError";
414
423
  }
415
424
  workspace;
416
425
  };
417
426
  var SESSION_PATH = /^\/api\/(?:plans|sessions)\/([^/?]+)/;
427
+ function daemonError(status, data, fallback) {
428
+ const { error, code, workspace } = typeof data === "object" && data !== null ? data : {};
429
+ if (code === "workspace_not_a_project") return new WorkspaceInitNeededError(error || "Not a project directory", workspace ?? "");
430
+ return new DaemonError(error || fallback, status, code);
431
+ }
418
432
  var ApiClient = class _ApiClient {
419
433
  /** Where a CLI invocation's sessions live, when a client is built without one: `--workspace`, else the cwd. */
420
434
  static defaultWorkspace;
@@ -422,6 +436,7 @@ var ApiClient = class _ApiClient {
422
436
  workspace;
423
437
  /** A session's open execution stream, so a new one can end it: one run is reported once. */
424
438
  executionStreams = /* @__PURE__ */ new Map();
439
+ executionStreamEnds = /* @__PURE__ */ new WeakMap();
425
440
  constructor(port, workspace) {
426
441
  this.port = port || DEFAULT_PORT;
427
442
  this.workspace = workspace ?? _ApiClient.defaultWorkspace ?? process.cwd();
@@ -436,7 +451,7 @@ var ApiClient = class _ApiClient {
436
451
  }
437
452
  /**
438
453
  * A daemon holds only the sessions adopted since it started, so after a
439
- * restart every session-scoped call answered "Session not found" until the
454
+ * restart every session-scoped call answered "session not found" until the
440
455
  * user ran `ordewell sessions load`. The saved session is adopted here
441
456
  * instead — idempotent for one the daemon still holds — and the call made
442
457
  * once more; a 404 means the first attempt never ran, so the retry cannot
@@ -445,13 +460,19 @@ var ApiClient = class _ApiClient {
445
460
  async httpRequest(method, urlPath, body) {
446
461
  const res = await this.rawRequest(method, urlPath, body);
447
462
  const sessionId = urlPath.match(SESSION_PATH)?.[1];
448
- const missing = res.status === 404 && res.data?.error === "Session not found";
463
+ const missing = res.status === 404 && res.data?.code === "session_not_found";
449
464
  if (!missing || !sessionId || urlPath.split("?")[0].endsWith("/load")) return res;
450
465
  const adopted2 = await this.rawRequest("POST", `/api/sessions/${sessionId}/load?workspace=${encodeURIComponent(this.workspace)}`);
451
466
  return adopted2.status === 200 ? this.rawRequest(method, urlPath, body) : res;
452
467
  }
468
+ /** One request whose only good answer is a 200: anything else becomes a {@link DaemonError} carrying the daemon's code. */
469
+ async call(method, urlPath, fallback, body) {
470
+ const res = await this.httpRequest(method, urlPath, body);
471
+ if (res.status !== 200) throw daemonError(res.status, res.data, fallback);
472
+ return res.data;
473
+ }
453
474
  rawRequest(method, urlPath, body) {
454
- return new Promise((resolve3, reject) => {
475
+ return new Promise((resolve4, reject) => {
455
476
  const url = new URL(urlPath, `http://127.0.0.1:${this.port}`);
456
477
  const configuredTimeout = Number.parseInt(process.env.ORDEWELL_HTTP_TIMEOUT_MS || "", 10);
457
478
  const token = this.token();
@@ -476,9 +497,9 @@ var ApiClient = class _ApiClient {
476
497
  });
477
498
  res.on("end", () => {
478
499
  try {
479
- resolve3({ status: res.statusCode || 0, data: JSON.parse(data) });
500
+ resolve4({ status: res.statusCode || 0, data: JSON.parse(data) });
480
501
  } catch {
481
- resolve3({ status: res.statusCode || 0, data });
502
+ resolve4({ status: res.statusCode || 0, data });
482
503
  }
483
504
  });
484
505
  });
@@ -492,74 +513,43 @@ var ApiClient = class _ApiClient {
492
513
  });
493
514
  }
494
515
  async generatePlan(goal, runners2, workspace, sessionId = (0, import_core3.mintSessionId)()) {
495
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/generate`, {
516
+ const { plan, models, modelsByRunner, notes } = await this.call("POST", `/api/plans/${sessionId}/generate`, "Plan generation failed", {
496
517
  goal,
497
518
  runners: runners2 || void 0,
498
519
  workspace
499
520
  });
500
- if (res.status !== 200) {
501
- throw new Error(res.data?.error || "Plan generation failed");
502
- }
503
- return { sessionId, plan: res.data.plan, models: res.data.models, modelsByRunner: res.data.modelsByRunner };
521
+ return { sessionId, plan, models, modelsByRunner, ...notes ? { notes } : {} };
504
522
  }
505
523
  /**
506
- * Open the ADR-0002 planner dialogue. Unlike `generatePlan`, this path honours
507
- * the verify toggle, so the returned plan may be a question
508
- * (empty `tasks`, last word in `conversationHistory`) rather than a committed plan.
524
+ * Open the ADR-0002 planner dialogue. Unlike `generatePlan`, the returned plan
525
+ * may be a question (empty `tasks`, last word in `conversationHistory`) rather
526
+ * than a committed plan.
509
527
  */
510
528
  async startConversation(sessionId, goal, runners2, workspace, allowInit) {
511
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/converse/start`, {
529
+ const res = await this.call("POST", `/api/plans/${sessionId}/converse/start`, "Planning failed", {
512
530
  goal,
513
531
  runners: runners2 || void 0,
514
532
  workspace,
515
533
  allowInit
516
534
  });
517
- if (res.status !== 200) {
518
- if (res.data?.code === "workspace_not_a_project") {
519
- throw new WorkspaceInitNeededError(res.data.error || "Not a project directory", res.data.workspace || workspace || "");
520
- }
521
- throw new Error(res.data?.error || "Planning failed");
522
- }
523
- return res.data.plan;
535
+ return res.plan;
524
536
  }
525
537
  async sendConversationMessage(sessionId, message) {
526
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/converse/message`, { message });
527
- if (res.status !== 200) {
528
- throw new Error(res.data?.error || "Planner message failed");
529
- }
530
- return res.data.plan;
538
+ const res = await this.call("POST", `/api/plans/${sessionId}/converse/message`, "Planner message failed", { message });
539
+ return res.plan;
531
540
  }
532
- async getRunners() {
533
- const res = await this.httpRequest("GET", "/api/runners");
534
- if (res.status !== 200) {
535
- throw new Error(res.data?.error || "Failed to fetch runners");
536
- }
537
- return res.data;
541
+ getRunners() {
542
+ return this.call("GET", "/api/runners", "Failed to fetch runners");
538
543
  }
539
544
  /** The full provider catalog the daemon has discovered (used by the TUI model picker). */
540
- async getModels() {
541
- const res = await this.httpRequest("GET", "/api/models");
542
- if (res.status !== 200) {
543
- throw new Error(res.data?.error || "Failed to fetch models");
544
- }
545
- return res.data;
545
+ getModels() {
546
+ return this.call("GET", "/api/models", "Failed to fetch models");
546
547
  }
547
- async setRunnerEnabled(runner, enabled) {
548
- const res = await this.httpRequest("PUT", `/api/runners/${runner}`, { enabled });
549
- if (res.status !== 200) {
550
- throw new Error(res.data?.error || `Failed to ${enabled ? "enable" : "disable"} ${runner}`);
551
- }
552
- return res.data;
548
+ setRunnerEnabled(runner, enabled) {
549
+ return this.call("PUT", `/api/runners/${runner}`, `Failed to ${enabled ? "enable" : "disable"} ${runner}`, { enabled });
553
550
  }
554
- async executePlan(sessionId) {
555
- const res = await this.httpRequest(
556
- "POST",
557
- `/api/plans/${sessionId}/execute`
558
- );
559
- if (res.status !== 200) {
560
- throw new Error(res.data?.error || "Execute failed");
561
- }
562
- return res.data;
551
+ executePlan(sessionId) {
552
+ return this.call("POST", `/api/plans/${sessionId}/execute`, "Execute failed");
563
553
  }
564
554
  /**
565
555
  * Sign off a plan waiting on `review_needed` and let it continue.
@@ -568,228 +558,111 @@ var ApiClient = class _ApiClient {
568
558
  * `resetForRun`) before approving, which is right for starting a plan and
569
559
  * wrong for releasing one that is already part-way through a review pause.
570
560
  */
571
- async approveReview(sessionId) {
572
- const res = await this.httpRequest(
573
- "POST",
574
- `/api/plans/${sessionId}/review/approve`
575
- );
576
- if (res.status !== 200) {
577
- throw new Error(res.data?.error || "Approve failed");
578
- }
579
- return res.data;
561
+ approveReview(sessionId) {
562
+ return this.call("POST", `/api/plans/${sessionId}/review/approve`, "Approve failed");
580
563
  }
581
- async markTaskComplete(sessionId, taskId) {
582
- const res = await this.httpRequest(
583
- "POST",
584
- `/api/plans/${sessionId}/tasks/${taskId}/complete`
585
- );
586
- if (res.status !== 200) {
587
- throw new Error(res.data?.error || "Mark complete failed");
588
- }
589
- return res.data;
564
+ markTaskComplete(sessionId, taskId) {
565
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/complete`, "Mark complete failed");
590
566
  }
591
- async markTaskIncomplete(sessionId, taskId) {
592
- const res = await this.httpRequest(
593
- "POST",
594
- `/api/plans/${sessionId}/tasks/${taskId}/uncomplete`
595
- );
596
- if (res.status !== 200) {
597
- throw new Error(res.data?.error || "Mark not done failed");
598
- }
599
- return res.data;
567
+ markTaskIncomplete(sessionId, taskId) {
568
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/uncomplete`, "Mark not done failed");
600
569
  }
601
570
  /** run | force-start | retry | cancel — real orchestrator work, not a status patch. */
602
- async taskControl(sessionId, taskId, action) {
603
- const res = await this.httpRequest(
604
- "POST",
605
- `/api/plans/${sessionId}/tasks/${taskId}/${action}`
606
- );
607
- if (res.status !== 200) {
608
- throw new Error(res.data?.error || `${action} failed`);
609
- }
610
- return res.data;
571
+ taskControl(sessionId, taskId, action) {
572
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/${action}`, `${action} failed`);
611
573
  }
612
574
  /** A user message to a structured task: delivered now if it waits for input, else queued behind its turn. */
613
- async sendTaskMessage(sessionId, taskId, text) {
614
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages`, { text });
615
- if (res.status !== 200) {
616
- throw new Error(res.data?.error || "Send message failed");
617
- }
618
- return res.data;
575
+ sendTaskMessage(sessionId, taskId, text) {
576
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages`, "Send message failed", { text });
619
577
  }
620
578
  /** Force send: interrupt the task's running turn and deliver `text` next, ahead of anything queued (ADR-0023, F1). */
621
- async forceSendTaskMessage(sessionId, taskId, text) {
622
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages/now`, { text });
623
- if (res.status !== 200) {
624
- throw new Error(res.data?.error || "Send now failed");
625
- }
626
- return res.data;
579
+ forceSendTaskMessage(sessionId, taskId, text) {
580
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages/now`, "Send now failed", { text });
627
581
  }
628
582
  /** Force send a message still queued; `sent` is false once the runner already has it. */
629
- async forceSendQueuedTaskMessage(sessionId, taskId, messageId) {
630
- const res = await this.httpRequest(
631
- "POST",
632
- `/api/plans/${sessionId}/tasks/${taskId}/messages/${encodeURIComponent(messageId)}/now`
633
- );
634
- if (res.status !== 200) {
635
- throw new Error(res.data?.error || "Send now failed");
636
- }
637
- return res.data;
583
+ forceSendQueuedTaskMessage(sessionId, taskId, messageId) {
584
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/messages/${encodeURIComponent(messageId)}/now`, "Send now failed");
638
585
  }
639
586
  /** Continue a finished structured task in its saved session, with `text` as its next turn. */
640
- async continueTask(sessionId, taskId, text) {
641
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/continue`, { text });
642
- if (res.status !== 200) {
643
- throw new Error(res.data?.error || "Continue failed");
644
- }
645
- return res.data;
587
+ continueTask(sessionId, taskId, text) {
588
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/continue`, "Continue failed", { text });
646
589
  }
647
590
  /** `removed` is false once the message was already delivered. */
648
- async removeQueuedTaskMessage(sessionId, taskId, messageId) {
649
- const res = await this.httpRequest(
650
- "DELETE",
651
- `/api/plans/${sessionId}/tasks/${taskId}/messages/${encodeURIComponent(messageId)}`
652
- );
653
- if (res.status !== 200) {
654
- throw new Error(res.data?.error || "Remove message failed");
655
- }
656
- return res.data;
591
+ removeQueuedTaskMessage(sessionId, taskId, messageId) {
592
+ return this.call("DELETE", `/api/plans/${sessionId}/tasks/${taskId}/messages/${encodeURIComponent(messageId)}`, "Remove message failed");
657
593
  }
658
- async interruptTask(sessionId, taskId) {
659
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/interrupt`);
660
- if (res.status !== 200) {
661
- throw new Error(res.data?.error || "Interrupt failed");
662
- }
663
- return res.data;
594
+ interruptTask(sessionId, taskId) {
595
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/interrupt`, "Interrupt failed");
664
596
  }
665
597
  /** Let a task go on from the checkpoint it waits at; refused, with the reason, when none waits. */
666
- async approveTaskCheckpoint(sessionId, taskId) {
667
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/checkpoint/approve`);
668
- if (res.status !== 200) {
669
- throw new Error(res.data?.error || "Approve checkpoint failed");
670
- }
671
- return res.data;
598
+ approveTaskCheckpoint(sessionId, taskId) {
599
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/checkpoint/approve`, "Approve checkpoint failed");
672
600
  }
673
601
  /** Turn a task back at its checkpoint; `reason` is what the agent is told. */
674
- async rejectTaskCheckpoint(sessionId, taskId, reason) {
675
- const res = await this.httpRequest(
676
- "POST",
677
- `/api/plans/${sessionId}/tasks/${taskId}/checkpoint/reject`,
678
- reason ? { reason } : {}
679
- );
680
- if (res.status !== 200) {
681
- throw new Error(res.data?.error || "Reject checkpoint failed");
682
- }
683
- return res.data;
602
+ rejectTaskCheckpoint(sessionId, taskId, reason) {
603
+ return this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/checkpoint/reject`, "Reject checkpoint failed", reason ? { reason } : {});
684
604
  }
685
- async addTask(sessionId, task) {
686
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks`, task);
687
- if (res.status !== 200) {
688
- throw new Error(res.data?.error || "Add task failed");
689
- }
690
- return res.data;
605
+ addTask(sessionId, task) {
606
+ return this.call("POST", `/api/plans/${sessionId}/tasks`, "Add task failed", task);
691
607
  }
692
- async updateTask(sessionId, taskId, changes) {
693
- const res = await this.httpRequest("PUT", `/api/plans/${sessionId}/tasks/${taskId}`, changes);
694
- if (res.status !== 200) {
695
- throw new Error(res.data?.error || "Update task failed");
696
- }
697
- return res.data;
608
+ updateTask(sessionId, taskId, changes) {
609
+ return this.call("PUT", `/api/plans/${sessionId}/tasks/${taskId}`, "Update task failed", changes);
698
610
  }
699
- async removeTask(sessionId, taskId) {
700
- const res = await this.httpRequest("DELETE", `/api/plans/${sessionId}/tasks/${taskId}`);
701
- if (res.status !== 200) {
702
- throw new Error(res.data?.error || "Remove task failed");
703
- }
704
- return res.data;
611
+ removeTask(sessionId, taskId) {
612
+ return this.call("DELETE", `/api/plans/${sessionId}/tasks/${taskId}`, "Remove task failed");
705
613
  }
706
614
  /**
707
615
  * Make a saved session live on the server. `getSession` only reads the file;
708
616
  * until the session is adopted there is no orchestrator behind it, so
709
- * execute/retry/cancel answer "Session not found".
617
+ * execute/retry/cancel answer `session_not_found`.
710
618
  */
711
619
  async adoptSession(sessionId, workspace) {
712
620
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
713
- const res = await this.httpRequest("POST", `/api/sessions/${sessionId}/load${qs}`);
714
- if (res.status !== 200) {
715
- throw new Error(res.data?.error || "Failed to load session");
716
- }
717
- return { plan: res.data.plan, goal: res.data.goal ?? "" };
621
+ const res = await this.call("POST", `/api/sessions/${sessionId}/load${qs}`, "Failed to load session");
622
+ return { plan: res.plan, goal: res.goal ?? "" };
718
623
  }
719
- async deleteSession(sessionId, workspace) {
624
+ deleteSession(sessionId, workspace) {
720
625
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
721
- const res = await this.httpRequest("DELETE", `/api/sessions/${sessionId}${qs}`);
722
- if (res.status !== 200) {
723
- throw new Error(res.data?.error || "Delete session failed");
724
- }
725
- return res.data;
626
+ return this.call("DELETE", `/api/sessions/${sessionId}${qs}`, "Delete session failed");
726
627
  }
727
628
  /** Copy the conversation and its tasks into a new session the daemon has already adopted. */
728
- async forkConversation(sessionId) {
729
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/fork`);
730
- if (res.status !== 200) {
731
- throw new Error(res.data?.error || "Fork failed");
732
- }
733
- return { sessionId: res.data.sessionId, goal: res.data.goal, plan: res.data.plan };
629
+ forkConversation(sessionId) {
630
+ return this.call("POST", `/api/plans/${sessionId}/conversation/fork`, "Fork failed");
734
631
  }
735
632
  async rewindTargets(sessionId) {
736
- const res = await this.httpRequest("GET", `/api/plans/${sessionId}/conversation/rewind-targets`);
737
- if (res.status !== 200) {
738
- throw new Error(res.data?.error || "Failed to list rewind targets");
739
- }
740
- return res.data.targets;
633
+ const res = await this.call("GET", `/api/plans/${sessionId}/conversation/rewind-targets`, "Failed to list rewind targets");
634
+ return res.targets;
741
635
  }
742
636
  /**
743
637
  * Fork the conversation from just before the user message at `index` (a
744
638
  * transcript position) into a session the daemon has already adopted. The
745
639
  * original is left as it was; `rewoundMessage` is that message in full.
746
640
  */
747
- async rewindConversation(sessionId, index) {
748
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/rewind`, { index });
749
- if (res.status !== 200) {
750
- throw new Error(res.data?.error || "Rewind failed");
751
- }
752
- return { sessionId: res.data.sessionId, goal: res.data.goal, plan: res.data.plan, rewoundMessage: res.data.rewoundMessage };
641
+ rewindConversation(sessionId, index) {
642
+ return this.call("POST", `/api/plans/${sessionId}/conversation/rewind`, "Rewind failed", { index });
753
643
  }
754
644
  /**
755
645
  * Condense the conversation into a summary. One planner call, so it can take
756
646
  * as long as a reply; a refusal or failure leaves the conversation as it was.
757
647
  */
758
- async compactConversation(sessionId) {
759
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/conversation/compact`);
760
- if (res.status !== 200) {
761
- throw new Error(res.data?.error || "Compaction failed");
762
- }
763
- return { plan: res.data.plan, summary: res.data.summary, keptMessages: res.data.keptMessages };
648
+ compactConversation(sessionId) {
649
+ return this.call("POST", `/api/plans/${sessionId}/conversation/compact`, "Compaction failed");
764
650
  }
765
651
  /** The dependencies a task waits on at its merge gate (ADR-0020); empty when nothing gates it. */
766
652
  async getMergeGate(sessionId, taskId) {
767
- const res = await this.httpRequest(
768
- "GET",
769
- `/api/plans/${sessionId}/tasks/${taskId}/merge-gate`
770
- );
771
- if (res.status !== 200) {
772
- throw new Error(res.data?.error || "Could not read the merge gate");
773
- }
774
- return res.data.mergeGate ?? [];
653
+ const res = await this.call("GET", `/api/plans/${sessionId}/tasks/${taskId}/merge-gate`, "Could not read the merge gate");
654
+ return res.mergeGate ?? [];
775
655
  }
776
656
  async reviewRunDiff(sessionId) {
777
- const res = await this.httpRequest("GET", `/api/plans/${sessionId}/isolation/diff`);
778
- if (res.status !== 200) {
779
- throw new Error(res.data?.error || "Could not read the diff");
780
- }
781
- return res.data.diff;
657
+ const res = await this.call("GET", `/api/plans/${sessionId}/isolation/diff`, "Could not read the diff");
658
+ return res.diff;
782
659
  }
783
660
  /**
784
661
  * A conflict or a block is an outcome, not an error. Passed on whole: which
785
662
  * repos blocked the merge, or landed before it stopped, is the answer.
786
663
  */
787
- async mergeRun(sessionId) {
788
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/isolation/merge`);
789
- if (res.status !== 200) {
790
- throw new Error(res.data?.error || "Merge failed");
791
- }
792
- return res.data;
664
+ mergeRun(sessionId) {
665
+ return this.call("POST", `/api/plans/${sessionId}/isolation/merge`, "Merge failed");
793
666
  }
794
667
  discardRun(sessionId) {
795
668
  return this.isolationAction(sessionId, "discard", "Discard failed");
@@ -804,17 +677,11 @@ var ApiClient = class _ApiClient {
804
677
  return this.isolationAction(sessionId, "run-without", "Could not continue without isolation");
805
678
  }
806
679
  async isolationAction(sessionId, segment, failure) {
807
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/isolation/${segment}`);
808
- if (res.status !== 200) {
809
- throw new Error(res.data?.error || failure);
810
- }
680
+ await this.call("POST", `/api/plans/${sessionId}/isolation/${segment}`, failure);
811
681
  }
812
682
  async resolveConflictAsTask(sessionId, taskId) {
813
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/tasks/${taskId}/resolve-conflict`);
814
- if (res.status !== 200) {
815
- throw new Error(res.data?.error || "Could not add a resolver task");
816
- }
817
- return res.data.plan;
683
+ const res = await this.call("POST", `/api/plans/${sessionId}/tasks/${taskId}/resolve-conflict`, "Could not add a resolver task");
684
+ return res.plan;
818
685
  }
819
686
  async closeSession(sessionId) {
820
687
  const res = await this.httpRequest("POST", `/api/sessions/${sessionId}/close`);
@@ -829,42 +696,26 @@ var ApiClient = class _ApiClient {
829
696
  const res = await this.httpRequest("POST", `/api/plans/${sessionId}/planning/stop`);
830
697
  return res.data;
831
698
  }
832
- async processQueued(sessionId) {
833
- const res = await this.httpRequest("POST", `/api/plans/${sessionId}/process-queued`);
834
- if (res.status !== 200) {
835
- throw new Error(res.data?.error || "Process queued failed");
836
- }
837
- return res.data;
838
- }
839
699
  async getSessions(workspace) {
840
700
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
841
701
  const res = await this.httpRequest("GET", `/api/sessions${qs}`);
842
702
  return res.data || [];
843
703
  }
844
- async getSession(sessionId, workspace) {
704
+ getSession(sessionId, workspace) {
845
705
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
846
- const res = await this.httpRequest(
847
- "GET",
848
- `/api/sessions/${sessionId}${qs}`
849
- );
850
- if (res.status !== 200) {
851
- throw new Error(res.data?.error || "Session not found");
852
- }
853
- return res.data;
706
+ return this.call("GET", `/api/sessions/${sessionId}${qs}`, "Session not found");
854
707
  }
855
708
  /** The attempts of a structured task that have a saved log, oldest first. */
856
709
  async getTaskLogAttempts(sessionId, taskId, workspace) {
857
710
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
858
- const res = await this.httpRequest("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log${qs}`);
859
- if (res.status !== 200) throw new Error(res.data?.error || "Could not read the task log");
860
- return res.data.attempts;
711
+ const res = await this.call("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log${qs}`, "Could not read the task log");
712
+ return res.attempts;
861
713
  }
862
714
  /** One attempt's saved log, for `replayTaskLog`. */
863
715
  async getTaskLog(sessionId, taskId, attempt, workspace) {
864
716
  const qs = workspace ? `?workspace=${encodeURIComponent(workspace)}` : "";
865
- const res = await this.httpRequest("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log/${attempt}${qs}`);
866
- if (res.status !== 200) throw new Error(res.data?.error || "Could not read the task log");
867
- return res.data.events;
717
+ const res = await this.call("GET", `/api/sessions/${sessionId}/tasks/${encodeURIComponent(taskId)}/log/${attempt}${qs}`, "Could not read the task log");
718
+ return res.events;
868
719
  }
869
720
  /**
870
721
  * The one place a session socket is built, so the token travels on both
@@ -895,8 +746,14 @@ var ApiClient = class _ApiClient {
895
746
  });
896
747
  return socket;
897
748
  }
749
+ /** Ends one stream `streamExecution` returned, leaving any newer stream the session has opened since alone. */
750
+ closeExecutionStream(stream) {
751
+ this.executionStreamEnds.get(stream)?.();
752
+ }
898
753
  streamExecution(sessionId, onEvent, onReady) {
899
- return new Promise((resolve3, reject) => {
754
+ let endStream = () => {
755
+ };
756
+ const stream = new Promise((resolve4, reject) => {
900
757
  this.executionStreams.get(sessionId)?.();
901
758
  const socket = this.openSessionSocket(sessionId);
902
759
  let resolved = false;
@@ -909,9 +766,10 @@ var ApiClient = class _ApiClient {
909
766
  if (resolved) return;
910
767
  resolved = true;
911
768
  socket.close();
912
- resolve3();
769
+ resolve4();
913
770
  };
914
771
  this.executionStreams.set(sessionId, end);
772
+ endStream = end;
915
773
  socket.on("open", () => {
916
774
  opened = true;
917
775
  onReady?.();
@@ -928,8 +786,12 @@ var ApiClient = class _ApiClient {
928
786
  socket.on("error", (err) => {
929
787
  forget();
930
788
  if (!opened) onReady?.(err);
931
- if (!resolved) {
932
- resolved = true;
789
+ if (resolved) return;
790
+ resolved = true;
791
+ if (opened) {
792
+ socket.close();
793
+ resolve4("lost");
794
+ } else {
933
795
  reject(err);
934
796
  }
935
797
  });
@@ -938,10 +800,12 @@ var ApiClient = class _ApiClient {
938
800
  if (!opened) onReady?.(new Error("Execution stream closed before connecting"));
939
801
  if (!resolved) {
940
802
  resolved = true;
941
- resolve3();
803
+ resolve4("lost");
942
804
  }
943
805
  });
944
806
  });
807
+ this.executionStreamEnds.set(stream, endStream);
808
+ return stream;
945
809
  }
946
810
  /**
947
811
  * Answer an approval prompt: the planner's with a yes/no, a task runner's
@@ -954,7 +818,7 @@ var ApiClient = class _ApiClient {
954
818
  `/api/approvals/${encodeURIComponent(sessionId)}/${encodeURIComponent(approvalId)}`,
955
819
  typeof answer === "boolean" ? { granted: answer } : answer
956
820
  );
957
- if (status !== 200) throw new Error(`Failed to answer approval ${approvalId} (HTTP ${status})`);
821
+ if (status !== 200) throw new DaemonError(`Failed to answer approval ${approvalId} (HTTP ${status})`, status, data.code);
958
822
  return data;
959
823
  }
960
824
  /**
@@ -964,10 +828,10 @@ var ApiClient = class _ApiClient {
964
828
  */
965
829
  streamPlanning(sessionId, onEvent) {
966
830
  const socket = this.openSessionSocket(sessionId);
967
- const ready = new Promise((resolve3) => {
968
- socket.once("open", () => resolve3());
969
- socket.once("error", () => resolve3());
970
- socket.once("close", () => resolve3());
831
+ const ready = new Promise((resolve4) => {
832
+ socket.once("open", () => resolve4());
833
+ socket.once("error", () => resolve4());
834
+ socket.once("close", () => resolve4());
971
835
  });
972
836
  socket.on("message", (data) => {
973
837
  try {
@@ -979,33 +843,11 @@ var ApiClient = class _ApiClient {
979
843
  });
980
844
  return { ready, close: () => socket.close() };
981
845
  }
982
- async getCommands() {
983
- const res = await this.httpRequest("GET", "/api/commands");
984
- if (res.status !== 200) {
985
- throw new Error(res.data?.error || "Failed to fetch commands");
986
- }
987
- return res.data;
846
+ getSettings() {
847
+ return this.call("GET", "/api/settings", "Failed to fetch settings");
988
848
  }
989
- async sendCommand(name, args = {}) {
990
- const res = await this.httpRequest("POST", `/api/commands/${name}`, { args });
991
- if (res.status !== 200) {
992
- throw new Error(res.data?.error || `Command ${name} failed`);
993
- }
994
- return res.data;
995
- }
996
- async getSettings() {
997
- const res = await this.httpRequest("GET", "/api/settings");
998
- if (res.status !== 200) {
999
- throw new Error(res.data?.error || "Failed to fetch settings");
1000
- }
1001
- return res.data;
1002
- }
1003
- async updateSettings(changes) {
1004
- const res = await this.httpRequest("PATCH", "/api/settings", changes);
1005
- if (res.status !== 200) {
1006
- throw new Error(res.data?.error || "Failed to update settings");
1007
- }
1008
- return res.data;
849
+ updateSettings(changes) {
850
+ return this.call("PATCH", "/api/settings", "Failed to update settings", changes);
1009
851
  }
1010
852
  };
1011
853
 
@@ -1253,16 +1095,24 @@ var import_core7 = require("@ordewell/core");
1253
1095
  function planTasks(plan) {
1254
1096
  return plan ? allTasksOf(plan) : [];
1255
1097
  }
1098
+ function runnersOf(plan, requested) {
1099
+ return "runners" in plan && plan.runners || requested;
1100
+ }
1256
1101
  function hasCommittedPlan(plan) {
1257
1102
  return planTasks(plan).length > 0;
1258
1103
  }
1259
1104
  function lastPlannerMessage(plan) {
1260
- const history = plan?.conversationHistory || [];
1105
+ const history = "conversationHistory" in plan && plan.conversationHistory || [];
1261
1106
  for (let i = history.length - 1; i >= 0; i--) {
1262
- if (history[i].role === "assistant") return history[i].content;
1107
+ if (history[i].role === "assistant" && history[i].kind !== "skill_load") return history[i].content;
1263
1108
  }
1264
1109
  return "(the planner returned no plan and no message)";
1265
1110
  }
1111
+ function commitNotes(plan) {
1112
+ const history = "conversationHistory" in plan && plan.conversationHistory || [];
1113
+ const marker2 = history.map((e) => e.kind).lastIndexOf("plan_generated");
1114
+ return marker2 < 0 ? [] : history.slice(marker2 + 1).filter((e) => e.kind === "system").map((e) => e.content);
1115
+ }
1266
1116
  function stdinReader(input = process.stdin) {
1267
1117
  const rl = (0, import_readline.createInterface)({ input, output: process.stderr });
1268
1118
  const buffered = [];
@@ -1270,9 +1120,9 @@ function stdinReader(input = process.stdin) {
1270
1120
  let ended = false;
1271
1121
  const settle = (answer) => {
1272
1122
  if (!waiting) return false;
1273
- const resolve3 = waiting;
1123
+ const resolve4 = waiting;
1274
1124
  waiting = null;
1275
- resolve3(answer);
1125
+ resolve4(answer);
1276
1126
  return true;
1277
1127
  };
1278
1128
  rl.on("line", (line) => {
@@ -1288,8 +1138,8 @@ function stdinReader(input = process.stdin) {
1288
1138
  if (ended) return Promise.resolve(null);
1289
1139
  rl.setPrompt(question);
1290
1140
  rl.prompt();
1291
- return new Promise((resolve3) => {
1292
- waiting = resolve3;
1141
+ return new Promise((resolve4) => {
1142
+ waiting = resolve4;
1293
1143
  });
1294
1144
  },
1295
1145
  close() {
@@ -1319,7 +1169,7 @@ function printPlan(plan, sessionId, runners2, models) {
1319
1169
  return rp ? ` (${task.assignedModel.modelLabel} \xB7 ${label})` : ` (${task.assignedModel.modelLabel})`;
1320
1170
  };
1321
1171
  console.log(`
1322
- Plan: ${tasks.length} task${tasks.length === 1 ? "" : "s"} (${aiCount} AI, ${manCount} Manual) \u2014 ${(plan.runners || runners2).join(", ")}`);
1172
+ Plan: ${tasks.length} task${tasks.length === 1 ? "" : "s"} (${aiCount} AI, ${manCount} Manual) \u2014 ${runnersOf(plan, runners2).join(", ")}`);
1323
1173
  console.log(`Session: ${sessionId}
1324
1174
  `);
1325
1175
  for (const t of tasks) {
@@ -1423,6 +1273,7 @@ async function handlePlan(subArgs, deps = {}) {
1423
1273
  await stream.ready;
1424
1274
  let plan;
1425
1275
  let models = [];
1276
+ let notes = [];
1426
1277
  if (oneShot) {
1427
1278
  const result = await withSpinner(
1428
1279
  "Researching codebase and building task plan",
@@ -1430,6 +1281,7 @@ async function handlePlan(subArgs, deps = {}) {
1430
1281
  );
1431
1282
  plan = result.plan;
1432
1283
  models = result.models ?? [];
1284
+ notes = result.notes ?? [];
1433
1285
  } else {
1434
1286
  plan = await withSpinner(
1435
1287
  "Researching codebase and building task plan",
@@ -1461,12 +1313,15 @@ ${lastPlannerMessage(plan)}
1461
1313
  plan = await withSpinner("Thinking", () => api.sendConversationMessage(sessionId, reply));
1462
1314
  }
1463
1315
  reader?.close();
1316
+ notes = commitNotes(plan);
1464
1317
  }
1465
1318
  closeStream();
1466
1319
  if (isTty && lastLineLength > 0) process.stderr.write("\r\x1B[2K");
1467
1320
  process.stderr.write("\n");
1468
1321
  printPlan(plan, sessionId, runners2, models);
1469
- saveLastSession(sessionId, goal, plan.runners || runners2, workspace);
1322
+ for (const note of notes) console.log(`
1323
+ ${note}`);
1324
+ saveLastSession(sessionId, goal, runnersOf(plan, runners2), workspace);
1470
1325
  console.log(`
1471
1326
  Run 'ordewell run' to execute, 'ordewell status' to inspect, or 'ordewell tui' for the full UI.`);
1472
1327
  } catch (err) {
@@ -1584,23 +1439,27 @@ function mergeOutcome(result, branch, group, repaired = []) {
1584
1439
  }
1585
1440
 
1586
1441
  // src/utils/output.ts
1442
+ var import_core9 = require("@ordewell/core");
1587
1443
  function iconFor(status) {
1588
- switch (status) {
1589
- case "completed":
1444
+ switch ((0, import_core9.taskStatusKind)({ status })) {
1445
+ case "done":
1590
1446
  return "\x1B[32m\u2713\x1B[0m";
1591
- case "in_progress":
1447
+ case "running":
1448
+ case "quiet":
1592
1449
  return "\x1B[34m\u27F3\x1B[0m";
1593
1450
  case "failed":
1594
1451
  return "\x1B[31m\u2717\x1B[0m";
1595
1452
  case "blocked":
1596
1453
  return "\x1B[33m\u2298\x1B[0m";
1597
- default:
1454
+ case "awaiting":
1455
+ return "\x1B[33m?\x1B[0m";
1456
+ case "todo":
1598
1457
  return "\x1B[90m\u25CB\x1B[0m";
1599
1458
  }
1600
1459
  }
1601
1460
 
1602
1461
  // src/commands/run.ts
1603
- var import_core9 = require("@ordewell/core");
1462
+ var import_core10 = require("@ordewell/core");
1604
1463
 
1605
1464
  // src/commands/shared.ts
1606
1465
  var import_readline2 = require("readline");
@@ -1664,7 +1523,7 @@ async function askYesNo(question) {
1664
1523
  if (!process.stdin.isTTY) return false;
1665
1524
  const rl = (0, import_readline2.createInterface)({ input: process.stdin, output: process.stderr });
1666
1525
  try {
1667
- const answer = await new Promise((resolve3) => rl.question(`${question} [y/N] `, resolve3));
1526
+ const answer = await new Promise((resolve4) => rl.question(`${question} [y/N] `, resolve4));
1668
1527
  return /^y(es)?$/i.test(answer.trim());
1669
1528
  } finally {
1670
1529
  rl.close();
@@ -1696,7 +1555,8 @@ function toTaskView(task) {
1696
1555
  taskMode: task.taskMode || void 0,
1697
1556
  assignedModel: task.assignedModel ?? void 0,
1698
1557
  subtasks: [...task.subtasks ?? []].sort((a, b) => a.order - b.order).map(toTaskView),
1699
- ...task.ops ? { ops: true } : {}
1558
+ ...task.ops ? { ops: true } : {},
1559
+ ...task.skills?.length ? { skills: [...task.skills] } : {}
1700
1560
  };
1701
1561
  }
1702
1562
  function taskViews(plan) {
@@ -1746,21 +1606,21 @@ async function followExecution(api, sessionId, start, onBlocked) {
1746
1606
  let pausedAtGate = false;
1747
1607
  let noteBlocked = () => {
1748
1608
  };
1749
- const blockedSeen = new Promise((resolve3) => {
1750
- noteBlocked = resolve3;
1609
+ const blockedSeen = new Promise((resolve4) => {
1610
+ noteBlocked = resolve4;
1751
1611
  });
1752
1612
  let settleReady = () => {
1753
1613
  };
1754
- const streamReady = new Promise((resolve3, reject) => {
1755
- settleReady = (error) => error ? reject(error) : resolve3();
1614
+ const streamReady = new Promise((resolve4, reject) => {
1615
+ settleReady = (error) => error ? reject(error) : resolve4();
1756
1616
  });
1757
1617
  const stream = api.streamExecution(sessionId, (event) => {
1758
1618
  if (event.type === "task_started") {
1759
- process.stderr.write(`\xB7 ${(0, import_core9.taskStartedNotice)(event.title, event.runner)}
1619
+ process.stderr.write(`\xB7 ${(0, import_core10.taskStartedNotice)(event.title, event.runner)}
1760
1620
  `);
1761
1621
  }
1762
1622
  if (event.type === "checkpoint") {
1763
- process.stderr.write(`\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core9.truncateCheckpointSummary)(event.summary)} \u2014 \`ordewell checkpoint <id> approve|reject [reason]\` answers it
1623
+ process.stderr.write(`\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core10.truncateCheckpointSummary)(event.summary)} \u2014 \`ordewell checkpoint <id> approve|reject [reason]\` answers it
1764
1624
  `);
1765
1625
  }
1766
1626
  if (event.type === "status_update" && event.tasks) {
@@ -1805,7 +1665,11 @@ Run finished on ${handoffBranch(event)}${where} \u2014 ${n} task${n === 1 ? "" :
1805
1665
  void stream.catch(settleReady);
1806
1666
  await streamReady.catch(streamFailed);
1807
1667
  await start();
1808
- await Promise.race([stream, blockedSeen]).catch(streamFailed);
1668
+ const outcome = await Promise.race([stream, blockedSeen]).catch(streamFailed);
1669
+ if (outcome === "lost") {
1670
+ 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.");
1671
+ process.exit(1);
1672
+ }
1809
1673
  if (blocked === null) return;
1810
1674
  if (!onBlocked) {
1811
1675
  await api.stopExecution(sessionId);
@@ -1833,7 +1697,7 @@ async function handleRun(subArgs, injectedApi) {
1833
1697
 
1834
1698
  // src/commands/status.ts
1835
1699
  var import_fs4 = require("fs");
1836
- var import_core10 = require("@ordewell/core");
1700
+ var import_core11 = require("@ordewell/core");
1837
1701
  async function handleStatus(subArgs, injectedApi) {
1838
1702
  const sessionId = flag(subArgs, "--session-id");
1839
1703
  const workspace = flag(subArgs, "--workspace") || process.cwd();
@@ -1904,12 +1768,12 @@ function printOneSession(sessionId, meta, plan) {
1904
1768
  const icon = iconFor(t.status);
1905
1769
  const model = t.assignedModel ? ` (${t.assignedModel.modelLabel})` : "";
1906
1770
  const shortId = (t.id ?? "").slice(-8);
1907
- console.log(` ${(0, import_core10.taskOrderLabel)(t).padStart(2)}. ${icon} [${shortId}] ${t.title}${model}`);
1771
+ console.log(` ${(0, import_core11.taskOrderLabel)(t).padStart(2)}. ${icon} [${shortId}] ${t.title}${model}`);
1908
1772
  for (const sub of t.subtasks || []) {
1909
1773
  const subIcon = iconFor(sub.status);
1910
1774
  const subModel = sub.assignedModel ? ` (${sub.assignedModel.modelLabel})` : "";
1911
1775
  const subShortId = (sub.id ?? "").slice(-8);
1912
- console.log(` ${(0, import_core10.taskOrderLabel)(sub, t)}. ${subIcon} [${subShortId}] ${sub.title}${subModel}`);
1776
+ console.log(` ${(0, import_core11.taskOrderLabel)(sub, t)}. ${subIcon} [${subShortId}] ${sub.title}${subModel}`);
1913
1777
  }
1914
1778
  }
1915
1779
  }
@@ -1961,10 +1825,10 @@ async function handleWeb(subArgs) {
1961
1825
  }
1962
1826
 
1963
1827
  // src/commands/models.ts
1964
- var import_core12 = require("@ordewell/core");
1828
+ var import_core13 = require("@ordewell/core");
1965
1829
 
1966
1830
  // src/commands/model.ts
1967
- var import_core11 = require("@ordewell/core");
1831
+ var import_core12 = require("@ordewell/core");
1968
1832
  var USAGE = "Usage: ordewell model [show | set <model-id>]";
1969
1833
  async function handleModel(subArgs, injectedApi) {
1970
1834
  const [action, modelId] = positionals(subArgs);
@@ -1979,7 +1843,7 @@ async function handleModel(subArgs, injectedApi) {
1979
1843
  if (!modelId) fail(USAGE);
1980
1844
  const settings = await api.getSettings();
1981
1845
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
1982
- const runner = (0, import_core11.runnerForProvider)(provider);
1846
+ const runner = (0, import_core12.runnerForProvider)(provider);
1983
1847
  if (runner) {
1984
1848
  const known = (await fetchCatalog(api)).models.filter((m) => m.runners?.includes(runner));
1985
1849
  if (known.length > 0 && !known.some((m) => m.id === modelId)) {
@@ -1998,7 +1862,7 @@ async function showModels(api) {
1998
1862
  const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
1999
1863
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
2000
1864
  const current = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
2001
- const runner = (0, import_core11.runnerForProvider)(provider);
1865
+ const runner = (0, import_core12.runnerForProvider)(provider);
2002
1866
  const options = runner ? catalog.models.filter((m) => m.runners?.includes(runner)) : catalog.orchestratorModels;
2003
1867
  console.log(`
2004
1868
  Planner model: ${current || "(backend default)"}`);
@@ -2032,8 +1896,8 @@ async function handleModels(subArgs) {
2032
1896
  const rest = subArgs.filter((_, i) => i !== setIdx && i !== setIdx + 1);
2033
1897
  return handleModel(["set", subArgs[setIdx + 1], ...rest]);
2034
1898
  }
2035
- const config = new import_core12.EnvConfig();
2036
- const providers = (0, import_core12.configuredProviders)(config);
1899
+ const config = new import_core13.EnvConfig();
1900
+ const providers = (0, import_core13.configuredProviders)(config);
2037
1901
  if (providers.length === 0) {
2038
1902
  console.error("No providers configured. Set an API key for a provider, e.g.:");
2039
1903
  console.error(" OPENROUTER_API_KEY=... (OpenRouter)");
@@ -2042,19 +1906,19 @@ async function handleModels(subArgs) {
2042
1906
  console.error(" OPENAI_COMPATIBLE_BASE_URL=... (self-hosted / OpenAI-compatible)");
2043
1907
  process.exit(1);
2044
1908
  }
2045
- const { apiKeys, baseUrls } = (0, import_core12.collectProviderCredentials)(config);
1909
+ const { apiKeys, baseUrls } = (0, import_core13.collectProviderCredentials)(config);
2046
1910
  let models;
2047
1911
  let errors;
2048
1912
  try {
2049
- const result = await (0, import_core12.fetchAllProviderModels)({ apiKeys, baseUrls });
2050
- models = (0, import_core12.toOrchestratorOptions)(result.models, import_core12.ORCHESTRATOR_SHORTCUTS);
1913
+ const result = await (0, import_core13.fetchAllProviderModels)({ apiKeys, baseUrls });
1914
+ models = (0, import_core13.toOrchestratorOptions)(result.models, import_core13.ORCHESTRATOR_SHORTCUTS);
2051
1915
  errors = result.errors;
2052
1916
  } catch (err) {
2053
1917
  console.error(`Failed to fetch models: ${err instanceof Error ? err.message : String(err)}`);
2054
1918
  process.exit(1);
2055
1919
  return;
2056
1920
  }
2057
- const providerLabel = (p) => (0, import_core12.getProviderMeta)(p)?.label ?? p;
1921
+ const providerLabel = (p) => (0, import_core13.getProviderMeta)(p)?.label ?? p;
2058
1922
  console.log(
2059
1923
  `
2060
1924
  Available models (${models.length}) across ${providers.length} configured provider(s):`
@@ -2094,7 +1958,7 @@ ${providerLabel(provider)}`);
2094
1958
 
2095
1959
  // src/commands/setup.ts
2096
1960
  var import_fs6 = require("fs");
2097
- var import_core13 = require("@ordewell/core");
1961
+ var import_core14 = require("@ordewell/core");
2098
1962
  function handleSetup(subArgs) {
2099
1963
  const modelIdx = subArgs.indexOf("--model");
2100
1964
  const chosenModel = modelIdx !== -1 ? subArgs[modelIdx + 1] : null;
@@ -2166,105 +2030,65 @@ function handleSetup(subArgs) {
2166
2030
  # STEPFUN_API_KEY=...
2167
2031
  # SILICONFLOW_API_KEY=...
2168
2032
  `;
2169
- (0, import_core13.writePrivateFile)(envFile, content);
2033
+ (0, import_core14.writePrivateFile)(envFile, content);
2170
2034
  console.log(`Wrote: ${envFile}`);
2171
2035
  console.log("Review and edit the file, then run: ordewell tui");
2172
2036
  }
2173
2037
 
2174
- // src/commands/plugins.ts
2175
- var import_core14 = require("@ordewell/core");
2176
- function handlePlugins(subArgs) {
2177
- const registry = new import_core14.RunnerRegistry();
2178
- registry.loadUserPlugins();
2179
- const sub = subArgs[0];
2180
- if (sub === "list" || !sub) {
2181
- const plugins = registry.list();
2182
- if (plugins.length === 0) {
2183
- console.log("No plugins installed.");
2184
- return;
2185
- }
2186
- const nameWidth = Math.max(...plugins.map((p) => p.manifest.name.length), 4);
2187
- const versionWidth = Math.max(...plugins.map((p) => p.manifest.version.length), 7);
2188
- const sourceWidth = Math.max(...plugins.map((p) => p.source.length), 6);
2189
- console.log(`
2190
- ${plugins.length} plugin(s):
2191
- `);
2192
- console.log(
2193
- `${"Name".padEnd(nameWidth)} ${"Version".padEnd(versionWidth)} ${"Source".padEnd(sourceWidth)} Description`
2194
- );
2195
- console.log(
2196
- `${"-".repeat(nameWidth)} ${"-".repeat(versionWidth)} ${"-".repeat(sourceWidth)} ${"-".repeat(20)}`
2197
- );
2198
- for (const p of plugins) {
2199
- const status = p.source === "builtin" ? "(built-in)" : "user";
2200
- console.log(
2201
- `${p.manifest.name.padEnd(nameWidth)} ${p.manifest.version.padEnd(versionWidth)} ${status.padEnd(sourceWidth)} ${p.manifest.description}`
2202
- );
2203
- }
2204
- return;
2205
- }
2206
- if (sub === "install") {
2207
- const source = subArgs[1];
2208
- if (!source) {
2209
- console.error("Usage: ordewell plugins install <path|github:user/repo|name>");
2210
- process.exit(1);
2211
- }
2212
- try {
2213
- const resolved = (0, import_core14.classifyPluginSource)(source);
2214
- const manifest = resolved.kind === "git" ? registry.installFromGit(resolved.url) : registry.installFromPath(resolved.path);
2215
- console.log(`Installed plugin: ${manifest.name} v${manifest.version}`);
2216
- } catch (err) {
2217
- console.error(`Install failed: ${err.message}`);
2218
- process.exit(1);
2219
- }
2220
- return;
2221
- }
2222
- if (sub === "remove") {
2223
- const name = subArgs[1];
2224
- if (!name) {
2225
- console.error("Usage: ordewell plugins remove <name>");
2226
- process.exit(1);
2227
- }
2228
- try {
2229
- registry.remove(name);
2230
- console.log(`Removed plugin: ${name}`);
2231
- } catch (err) {
2232
- console.error(`Remove failed: ${err.message}`);
2233
- process.exit(1);
2234
- }
2038
+ // src/commands/skills.ts
2039
+ var import_core15 = require("@ordewell/core");
2040
+ var import_path5 = require("path");
2041
+ function invocation(skill) {
2042
+ if (skill.userInvocable && skill.modelInvocable) return "both";
2043
+ if (skill.userInvocable) return "user";
2044
+ if (skill.modelInvocable) return "model";
2045
+ return "none";
2046
+ }
2047
+ function handleSkills(subArgs) {
2048
+ if (positionals(subArgs).length > 0) {
2049
+ fail("Usage: ordewell skills [--workspace /path] [--json]");
2050
+ }
2051
+ const workspace = (0, import_path5.resolve)(flag(subArgs, "--workspace") || process.cwd());
2052
+ const catalog = (0, import_core15.createSkillsService)(workspace, new import_core15.EnvConfig().workspaceRepos).readCatalog();
2053
+ const skills = catalog.skills.map((skill) => ({
2054
+ name: skill.name,
2055
+ scope: skill.source,
2056
+ appliesTo: skill.appliesTo,
2057
+ invocation: invocation(skill),
2058
+ path: (0, import_core15.abbreviateHome)(skill.path)
2059
+ }));
2060
+ const shadowed = catalog.shadowed.map(({ skill, shadowedBy }) => ({
2061
+ name: skill.name,
2062
+ path: (0, import_core15.abbreviateHome)(skill.path),
2063
+ shadowedBy: { scope: shadowedBy.source, path: (0, import_core15.abbreviateHome)(shadowedBy.path) }
2064
+ }));
2065
+ const invalid = catalog.invalid.map((skill) => ({
2066
+ name: skill.name,
2067
+ path: (0, import_core15.abbreviateHome)(skill.path),
2068
+ reason: skill.reason
2069
+ }));
2070
+ if (hasFlag(subArgs, "--json")) {
2071
+ console.log(JSON.stringify({ skills, shadowed, invalid }, null, 2));
2235
2072
  return;
2236
2073
  }
2237
- if (sub === "create") {
2238
- const name = subArgs[1];
2239
- if (!name) {
2240
- console.error("Usage: ordewell plugins create <name>");
2241
- process.exit(1);
2242
- }
2243
- const dir = registry.createSkeleton(name, process.cwd());
2244
- console.log(`Created plugin skeleton: ${dir}`);
2245
- console.log(` Edit ${dir}/manifest.json to configure your runner`);
2246
- console.log(` Then install with: ordewell plugins install ${dir}`);
2074
+ const skipped = [
2075
+ ...shadowed.map((skill) => `workspace skill "${skill.name}" shadowed by the ${skill.shadowedBy.scope} copy at ${skill.shadowedBy.path} \xB7 ${skill.path}`),
2076
+ ...invalid.map((skill) => `skill folder "${skill.name}" skipped: ${skill.reason} \xB7 ${skill.path}`)
2077
+ ];
2078
+ if (skills.length === 0) {
2079
+ console.log("No skills installed.");
2080
+ for (const line of skipped) console.log(line);
2247
2081
  return;
2248
2082
  }
2249
- console.error(`Unknown plugins subcommand: ${sub}`);
2250
- console.error("Available: list, install <source>, remove <name>, create <name>");
2251
- process.exit(1);
2252
- }
2253
-
2254
- // src/commands/tdd.ts
2255
- async function handleTdd(subArgs) {
2256
- const action = subArgs[0];
2257
- const port = await ensureDaemon(resolvePort(subArgs));
2258
- const api = new ApiClient(port);
2259
- if (action === "on" || action === "off") {
2260
- const result = await api.sendCommand("tdd", { action });
2261
- const state = result.settings?.tdd;
2262
- console.log(`TDD mode: ${state?.enabled ? "ON" : "OFF"}`);
2263
- } else {
2264
- const settings = await api.getSettings();
2265
- const tdd = settings.tdd;
2266
- console.log(`TDD mode: ${tdd?.enabled ? "ON" : "OFF"}`);
2267
- console.log("Usage: ordewell tdd [on|off]");
2083
+ const headers = ["Name", "Scope", "Applies-to", "Invocation", "SKILL.md"];
2084
+ const rows = skills.map((skill) => [skill.name, skill.scope, skill.appliesTo, skill.invocation, skill.path]);
2085
+ const widths = headers.map((header, i) => Math.max(header.length, ...rows.map((row) => row[i].length)));
2086
+ const format = (row) => row.map((cell, i) => i === row.length - 1 ? cell : cell.padEnd(widths[i])).join(" ");
2087
+ console.log(format(headers));
2088
+ for (const row of rows) console.log(format(row));
2089
+ if (skipped.length > 0) {
2090
+ console.log("");
2091
+ for (const line of skipped) console.log(line);
2268
2092
  }
2269
2093
  }
2270
2094
 
@@ -2354,41 +2178,8 @@ async function handleShow(args, api) {
2354
2178
  }
2355
2179
  }
2356
2180
 
2357
- // src/commands/verify.ts
2358
- async function handleVerify(subArgs, api) {
2359
- const action = subArgs[0];
2360
- const port = api ? void 0 : await ensureDaemon(resolvePort(subArgs));
2361
- const client = api || new ApiClient(port);
2362
- if (action === "on" || action === "off") {
2363
- const result = await client.sendCommand("verify", { action });
2364
- const state = result.settings?.verification;
2365
- console.log(`Verification mode: ${state?.enabled ? "ON" : "OFF"}`);
2366
- } else {
2367
- const settings = await client.getSettings();
2368
- const verification = settings.verification;
2369
- console.log(`Verification mode: ${verification?.enabled ? "ON" : "OFF"}`);
2370
- console.log("Usage: ordewell verify [on|off]");
2371
- }
2372
- }
2373
-
2374
- // src/commands/transport.ts
2375
- var describe = (transport) => `Runner transport: ${transport === "terminal" ? "terminal" : "structured"} \u2014 applies from the next run`;
2376
- async function handleTransport(subArgs, api) {
2377
- const action = subArgs[0];
2378
- const port = api ? void 0 : await ensureDaemon(resolvePort(subArgs));
2379
- const client = api || new ApiClient(port);
2380
- if (action === "terminal" || action === "structured") {
2381
- const result = await client.sendCommand("transport", { action });
2382
- console.log(describe(result.settings?.runnerTransport));
2383
- } else {
2384
- const settings = await client.getSettings();
2385
- console.log(describe(settings.runnerTransport));
2386
- console.log("Usage: ordewell transport [terminal|structured]");
2387
- }
2388
- }
2389
-
2390
2181
  // src/commands/task-control.ts
2391
- var import_core15 = require("@ordewell/core");
2182
+ var import_core16 = require("@ordewell/core");
2392
2183
  var PAST_TENSE = {
2393
2184
  run: "Task started.",
2394
2185
  "force-start": "Task force-started.",
@@ -2431,7 +2222,7 @@ async function confirmPastGate(subArgs, api, sessionId, taskId, confirm) {
2431
2222
  if (hasFlag(subArgs, "--yes")) return;
2432
2223
  const unmerged = await api.getMergeGate(sessionId, taskId).catch(() => []);
2433
2224
  if (unmerged.length === 0) return;
2434
- const question = `${(0, import_core15.pastGateConfirmation)(unmerged.map((d) => (0, import_core15.titledTaskRef)(d)))} Start it anyway?`;
2225
+ const question = `${(0, import_core16.pastGateConfirmation)(unmerged.map((d) => (0, import_core16.titledTaskRef)(d)))} Start it anyway?`;
2435
2226
  if (!await confirm(question)) fail("Not started \u2014 nothing was changed. Pass --yes to start it without a prompt.");
2436
2227
  }
2437
2228
  function makeHandler(action, command = action) {
@@ -2744,24 +2535,24 @@ async function handleRewind(subArgs, injectedApi) {
2744
2535
  }
2745
2536
 
2746
2537
  // src/tui/transcript.ts
2747
- var import_core16 = require("@ordewell/core");
2538
+ var import_core17 = require("@ordewell/core");
2748
2539
  function show(state, { view: conversation, gate: turnGate }) {
2749
2540
  const gated = turnGate === state.turnGate ? state : { ...state, turnGate };
2750
2541
  if (conversation === state.conversation) return gated;
2751
2542
  return { ...gated, conversation };
2752
2543
  }
2753
2544
  function fold(state, input) {
2754
- return show(state, { view: (0, import_core16.reduceConversation)(state.conversation, input), gate: state.turnGate });
2545
+ return show(state, { view: (0, import_core17.reduceConversation)(state.conversation, input), gate: state.turnGate });
2755
2546
  }
2756
2547
  function say(state, role, text) {
2757
2548
  const said = fold(state, { type: "local_entry", role, text });
2758
2549
  return said.conversation.nextId > state.conversation.nextId ? { ...said, scroll: 0 } : said;
2759
2550
  }
2760
2551
  function hear(state, message) {
2761
- return show(state, (0, import_core16.followTurn)(state.conversation, state.turnGate, message));
2552
+ return show(state, (0, import_core17.followTurn)(state.conversation, state.turnGate, message));
2762
2553
  }
2763
2554
  function cutTurn(state) {
2764
- return show(state, (0, import_core16.stopTurn)(state.conversation, state.turnGate));
2555
+ return show(state, (0, import_core17.stopTurn)(state.conversation, state.turnGate));
2765
2556
  }
2766
2557
  function wiped(view) {
2767
2558
  return { ...view, blocks: view.blocks.filter((b) => b.type === "usage") };
@@ -3175,26 +2966,17 @@ async function handleHandoff(subArgs, injectedApi, confirm = askYesNo) {
3175
2966
  }
3176
2967
 
3177
2968
  // src/commands/planner.ts
3178
- var import_core17 = require("@ordewell/core");
2969
+ var import_core18 = require("@ordewell/core");
3179
2970
 
3180
2971
  // src/plannerModelSwitch.ts
3181
- function describePlannerSwitch(settings, provider) {
3182
- const model = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
3183
- const effort = typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : "";
3184
- if (!model) return { model: "", effort: "", source: "none" };
3185
- return rememberedModelFor(settings, provider) === model ? { model, effort, source: "remembered" } : { model, effort, source: "catalog-default" };
3186
- }
3187
- function rememberedModelFor(settings, provider) {
3188
- const map = settings.plannerModels;
3189
- if (!map || typeof map !== "object") return void 0;
3190
- const entry = map[provider];
3191
- if (!entry || typeof entry !== "object") return void 0;
3192
- const { model } = entry;
3193
- return typeof model === "string" ? model : void 0;
2972
+ function plannerSwitchRecall(settings) {
2973
+ if (settings.switchRecall) return settings.switchRecall;
2974
+ const model = settings.orchestratorModel;
2975
+ return model ? { model, effort: settings.plannerThinkingEffort, source: "remembered" } : { model: "", effort: "", source: "none" };
3194
2976
  }
3195
2977
 
3196
2978
  // src/commands/planner.ts
3197
- var KNOWN_PROVIDERS = Object.keys(import_core17.ALL_PROVIDERS);
2979
+ var isAiProvider = (id) => Object.hasOwn(import_core18.ALL_PROVIDERS, id);
3198
2980
  async function handlePlanner(subArgs, injectedApi) {
3199
2981
  const api = await connect(subArgs, injectedApi);
3200
2982
  const [wanted] = positionals(subArgs);
@@ -3203,19 +2985,19 @@ async function handlePlanner(subArgs, injectedApi) {
3203
2985
  return;
3204
2986
  }
3205
2987
  const provider = wanted.toLowerCase();
3206
- if (!KNOWN_PROVIDERS.includes(provider)) {
2988
+ if (!isAiProvider(provider)) {
3207
2989
  fail(`Unknown planner: ${provider}`, "Run `ordewell planner` with no arguments to see the list.");
3208
2990
  }
3209
2991
  const settings = await persistEnv(api, { AI_PROVIDER: provider });
3210
- const recall2 = describePlannerSwitch(settings, provider);
2992
+ const recall2 = plannerSwitchRecall(settings);
3211
2993
  writeEnv({ ORCHESTRATOR_MODEL: recall2.model, ORDEWELL_PLANNER_EFFORT: recall2.effort });
3212
- const meta = import_core17.ALL_PROVIDERS[provider];
2994
+ const meta = import_core18.ALL_PROVIDERS[provider];
3213
2995
  const catalog = await fetchCatalog(api);
3214
2996
  console.log(
3215
- (0, import_core17.isCliProvider)(provider) ? `Planning with ${meta.label} \u2014 no API key needed, it uses that agent's own subscription.` : `Planner set to ${meta.label}.`
2997
+ (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}.`
3216
2998
  );
3217
2999
  console.log(plannerModelLine(recall2));
3218
- if (!(0, import_core17.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
3000
+ if (!(0, import_core18.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
3219
3001
  console.log(` No ${meta.apiKeyEnvVar} configured yet \u2014 set one with \`ordewell key set ${provider} <key>\`.`);
3220
3002
  }
3221
3003
  }
@@ -3233,15 +3015,15 @@ async function showPlanners(api) {
3233
3015
  const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
3234
3016
  const current = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
3235
3017
  console.log("\nCoding agents (no API key \u2014 they use their own subscription)");
3236
- for (const id of import_core17.CLI_PROVIDERS) {
3237
- const runner = (0, import_core17.runnerForProvider)(id);
3018
+ for (const id of import_core18.CLI_PROVIDERS) {
3019
+ const runner = (0, import_core18.runnerForProvider)(id);
3238
3020
  const discovered = runner ? catalog.models.some((m) => m.runners?.includes(runner)) : false;
3239
- console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core17.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
3021
+ console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core18.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
3240
3022
  }
3241
3023
  console.log("\nAPI providers");
3242
- for (const id of import_core17.PROVIDER_PRIORITY) {
3024
+ for (const id of import_core18.PROVIDER_PRIORITY) {
3243
3025
  const configured = catalog.providers.includes(id);
3244
- const meta = import_core17.ALL_PROVIDERS[id];
3026
+ const meta = import_core18.ALL_PROVIDERS[id];
3245
3027
  console.log(` ${marker(id === current)} ${id.padEnd(14)} ${meta.label}${configured ? "" : ` (no ${meta.apiKeyEnvVar})`}`);
3246
3028
  }
3247
3029
  console.log(`
@@ -3258,7 +3040,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
3258
3040
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
3259
3041
  const modelId = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
3260
3042
  const currentEffort = typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : "";
3261
- const runner = (0, import_core17.runnerForProvider)(provider);
3043
+ const runner = isAiProvider(provider) ? (0, import_core18.runnerForProvider)(provider) : null;
3262
3044
  if (!runner) {
3263
3045
  fail(
3264
3046
  "Thinking effort applies to a coding-agent planner.",
@@ -3298,7 +3080,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
3298
3080
  }
3299
3081
 
3300
3082
  // src/commands/parallel.ts
3301
- var import_core18 = require("@ordewell/core");
3083
+ var import_core19 = require("@ordewell/core");
3302
3084
  var USAGE5 = "Usage: ordewell parallel [<number of tasks, 1 or more>]";
3303
3085
  async function handleParallel(subArgs, injectedApi) {
3304
3086
  const [wanted] = positionals(subArgs);
@@ -3308,14 +3090,14 @@ async function handleParallel(subArgs, injectedApi) {
3308
3090
  console.log(`Up to ${String(settings.maxParallel ?? "?")} AI tasks run at once. Change with: ordewell parallel <n>`);
3309
3091
  return;
3310
3092
  }
3311
- const limit = (0, import_core18.parseMaxParallel)(wanted);
3093
+ const limit = (0, import_core19.parseMaxParallel)(wanted);
3312
3094
  if (limit === null) fail(`"${wanted}" is not a number of tasks (1 or more).`, USAGE5);
3313
3095
  await persistEnv(api, { ORDEWELL_MAX_PARALLEL: String(limit) });
3314
3096
  console.log(`Up to ${limit} AI task${limit === 1 ? "" : "s"} now run at once.`);
3315
3097
  }
3316
3098
 
3317
3099
  // src/commands/key.ts
3318
- var import_core19 = require("@ordewell/core");
3100
+ var import_core20 = require("@ordewell/core");
3319
3101
  var USAGE6 = "Usage: ordewell key [list | set <provider> <key>]";
3320
3102
  async function handleKey(subArgs, injectedApi) {
3321
3103
  const [action, provider, ...keyParts] = positionals(subArgs);
@@ -3326,8 +3108,8 @@ async function handleKey(subArgs, injectedApi) {
3326
3108
  if (action !== "set") {
3327
3109
  const catalog = await fetchCatalog(api);
3328
3110
  console.log("\nAPI provider keys\n");
3329
- for (const id2 of import_core19.PROVIDER_PRIORITY) {
3330
- const meta2 = import_core19.ALL_PROVIDERS[id2];
3111
+ for (const id2 of import_core20.PROVIDER_PRIORITY) {
3112
+ const meta2 = import_core20.ALL_PROVIDERS[id2];
3331
3113
  const configured = catalog.providers.includes(id2);
3332
3114
  console.log(` ${configured ? "\u2713" : " "} ${id2.padEnd(14)} ${meta2.apiKeyEnvVar.padEnd(28)} ${meta2.label}`);
3333
3115
  }
@@ -3337,7 +3119,7 @@ async function handleKey(subArgs, injectedApi) {
3337
3119
  }
3338
3120
  if (!provider) fail(USAGE6);
3339
3121
  const id = provider.toLowerCase();
3340
- const meta = import_core19.ALL_PROVIDERS[id];
3122
+ const meta = import_core20.ALL_PROVIDERS[id];
3341
3123
  if (!meta) {
3342
3124
  fail(`Unknown provider: ${provider}`, "Run `ordewell key` to see the list.");
3343
3125
  }
@@ -3348,7 +3130,7 @@ async function handleKey(subArgs, injectedApi) {
3348
3130
  }
3349
3131
 
3350
3132
  // src/commands/runners.ts
3351
- var import_core20 = require("@ordewell/core");
3133
+ var import_core21 = require("@ordewell/core");
3352
3134
  var USAGE7 = "Usage: ordewell runners [<runner-id> on|off]";
3353
3135
  async function handleRunners(subArgs, injectedApi) {
3354
3136
  const [runner, toggle] = positionals(subArgs);
@@ -3379,15 +3161,15 @@ function handleAuto(subArgs) {
3379
3161
  const currentRaw = process.env.ORDEWELL_AUTONOMOUS_MODE;
3380
3162
  const current = currentRaw !== "false" && currentRaw !== "0";
3381
3163
  if (!arg) {
3382
- console.log(`Autonomy level: ${(0, import_core20.autonomyLevelLabel)(current)}`);
3164
+ console.log(`Autonomy level: ${(0, import_core21.autonomyLevelLabel)(current)}`);
3383
3165
  console.log(` ${AUTO_USAGE}`);
3384
3166
  return;
3385
3167
  }
3386
- const enabled = (0, import_core20.parseAutonomyLevel)(arg);
3168
+ const enabled = (0, import_core21.parseAutonomyLevel)(arg);
3387
3169
  if (enabled === null) fail(AUTO_USAGE);
3388
3170
  writeEnvVar(findEnvFile(), "ORDEWELL_AUTONOMOUS_MODE", String(enabled));
3389
3171
  process.env.ORDEWELL_AUTONOMOUS_MODE = String(enabled);
3390
- console.log(`Autonomy level: ${(0, import_core20.autonomyLevelLabel)(enabled)} for new sessions.`);
3172
+ console.log(`Autonomy level: ${(0, import_core21.autonomyLevelLabel)(enabled)} for new sessions.`);
3391
3173
  }
3392
3174
  async function handleRefresh(subArgs, injectedApi) {
3393
3175
  const api = await connect(subArgs, injectedApi);
@@ -3416,130 +3198,8 @@ async function handleApprove(subArgs, injectedApi) {
3416
3198
  });
3417
3199
  }
3418
3200
 
3419
- // src/tui/terminalLauncher.ts
3420
- var import_child_process3 = require("child_process");
3421
- var import_util = require("util");
3422
- var import_core21 = require("@ordewell/core");
3423
- var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
3424
- var defaultExecFile = async (command, args) => {
3425
- const { stdout, stderr } = await execFileAsync(command, args);
3426
- return { stdout: String(stdout), stderr: String(stderr) };
3427
- };
3428
- var defaultSpawn = (command, args) => {
3429
- (0, import_child_process3.spawn)(command, args, { detached: true, stdio: "ignore" }).unref();
3430
- };
3431
- var defaultWhich = (bin) => {
3432
- try {
3433
- (0, import_child_process3.execSync)(`${process.platform === "win32" ? "where" : "which"} ${bin}`, { stdio: "ignore" });
3434
- return true;
3435
- } catch {
3436
- return false;
3437
- }
3438
- };
3439
- async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
3440
- const platform = deps.platform ?? process.platform;
3441
- const env = deps.env ?? process.env;
3442
- const hasTmuxImpl = deps.hasTmuxImpl ?? import_core21.hasTmux;
3443
- const execFileImpl = deps.execFileImpl ?? defaultExecFile;
3444
- const spawnImpl = deps.spawnImpl ?? defaultSpawn;
3445
- const which = deps.which ?? defaultWhich;
3446
- if (!hasTmuxImpl()) {
3447
- return { ok: false, message: "No terminal window for this task: tmux is not installed. Install tmux to get one for terminal-transport tasks; a structured task has its log view instead." };
3448
- }
3449
- const session = (0, import_core21.tmuxSessionName)(port);
3450
- const socket = (0, import_core21.tmuxSocketName)(port);
3451
- const base = (0, import_core21.tmuxWindowName)(taskId, planSessionId);
3452
- let windows;
3453
- try {
3454
- const { stdout } = await execFileImpl("tmux", ["-L", socket, "list-windows", "-t", session, "-F", "#{window_name}"]);
3455
- windows = stdout.split("\n").map((line) => line.trim()).filter(Boolean);
3456
- } catch {
3457
- return { ok: false, message: "No terminal session found for this daemon \u2014 is it still running?" };
3458
- }
3459
- const window = latestAttempt(windows, base);
3460
- if (!window) {
3461
- return { ok: false, message: "This task has no live terminal and no saved log \u2014 it may still be pending." };
3462
- }
3463
- const manualHint = `tmux -L ${socket} attach -t ${session}:${window}`;
3464
- const viewer = `${session}-view-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3465
- const attachCmd = `tmux -L ${socket} new-session -d -s ${viewer} -t ${session} \\; set-option -t ${viewer} destroy-unattached on \\; select-window -t ${viewer}:${window} \\; attach-session -t ${viewer}`;
3466
- const opened = launchTerminal(platform, env, which, spawnImpl, attachCmd);
3467
- if (!opened) {
3468
- return { ok: false, message: `Couldn't open a terminal window automatically \u2014 attach manually: ${manualHint}` };
3469
- }
3470
- return { ok: true, message: "Opened a terminal for this task." };
3471
- }
3472
- function latestAttempt(windows, base) {
3473
- let best = null;
3474
- let bestAttempt = 0;
3475
- for (const name of windows) {
3476
- if (name === base && bestAttempt < 1) {
3477
- best = name;
3478
- bestAttempt = 1;
3479
- continue;
3480
- }
3481
- const suffix = name.startsWith(`${base}-`) ? Number(name.slice(base.length + 1)) : NaN;
3482
- if (Number.isInteger(suffix) && suffix > bestAttempt) {
3483
- best = name;
3484
- bestAttempt = suffix;
3485
- }
3486
- }
3487
- return best;
3488
- }
3489
- function launchTerminal(platform, env, which, spawnImpl, attachCmd) {
3490
- if (platform === "darwin") {
3491
- const script = `tell application "Terminal" to do script "${attachCmd.replace(/"/g, '\\"')}"`;
3492
- spawnImpl("osascript", ["-e", script]);
3493
- return true;
3494
- }
3495
- if (platform === "win32") {
3496
- if (which("wt.exe")) {
3497
- spawnImpl("wt.exe", ["-w", "0", "nt", "bash", "-lc", attachCmd]);
3498
- return true;
3499
- }
3500
- if (which("cmd.exe")) {
3501
- spawnImpl("cmd.exe", ["/c", "start", '""', "bash", "-lc", attachCmd]);
3502
- return true;
3503
- }
3504
- return false;
3505
- }
3506
- if (!env.DISPLAY && !env.WAYLAND_DISPLAY) return false;
3507
- const candidates = [];
3508
- if (env.TERMINAL) candidates.push([env.TERMINAL, ["-e", "bash", "-lc", attachCmd]]);
3509
- candidates.push(
3510
- ["x-terminal-emulator", ["-e", "bash", "-lc", attachCmd]],
3511
- ["gnome-terminal", ["--", "bash", "-lc", attachCmd]],
3512
- ["konsole", ["-e", "bash", "-lc", attachCmd]],
3513
- ["xfce4-terminal", ["-e", `bash -lc ${JSON.stringify(attachCmd)}`]],
3514
- ["xterm", ["-e", "bash", "-lc", attachCmd]]
3515
- );
3516
- for (const [bin, args] of candidates) {
3517
- if (which(bin)) {
3518
- spawnImpl(bin, args);
3519
- return true;
3520
- }
3521
- }
3522
- return false;
3523
- }
3524
-
3525
- // src/commands/terminal.ts
3526
- async function handleTerminal(subArgs, injectedApi) {
3527
- await withResolvedTask(
3528
- subArgs,
3529
- "Usage: ordewell terminal <task-id-or-order> [--session-id <id>]",
3530
- injectedApi,
3531
- async (_api, sessionId, taskId) => {
3532
- const result = await openTaskTerminal(resolvePort(subArgs), sessionId, taskId);
3533
- if (!result.ok) {
3534
- console.error(result.message);
3535
- process.exit(1);
3536
- }
3537
- console.log(result.message);
3538
- }
3539
- );
3540
- }
3541
-
3542
3201
  // src/commands/task-assign.ts
3202
+ var import_core22 = require("@ordewell/core");
3543
3203
  var import_plan_utils = require("@ordewell/core/plan-utils");
3544
3204
 
3545
3205
  // src/tui/taskAssignment.ts
@@ -3579,12 +3239,13 @@ function modesForTask(modesByRunner, task) {
3579
3239
  }
3580
3240
 
3581
3241
  // src/commands/task-assign.ts
3582
- async function withTask(subArgs, usage, injectedApi, run) {
3242
+ async function withTask(subArgs, usage, injectedApi, run, { joinValue = false } = {}) {
3583
3243
  await withResolvedTask(subArgs, usage, injectedApi, async (api, sessionId, taskId, plan) => {
3584
3244
  const tasks = taskViews(plan);
3585
3245
  const task = tasks.find((t) => t.id === taskId);
3586
3246
  if (!task) fail(`Task not found in the plan: ${taskId}`);
3587
- const value = positionals(subArgs)[1];
3247
+ const [, ...rest] = positionals(subArgs);
3248
+ const value = joinValue ? rest.join(" ") || void 0 : rest[0];
3588
3249
  await run(api, sessionId, task, tasks, value);
3589
3250
  });
3590
3251
  }
@@ -3740,6 +3401,35 @@ Dependencies \xB7 ${(0, import_plan_utils.titledTaskRef)(task)}
3740
3401
  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.`);
3741
3402
  });
3742
3403
  }
3404
+ var SKILLS_USAGE = "Usage: ordewell task-skills <task-id-or-order> [<name> [<name> \u2026]|<name,name,\u2026>|none] [--session-id <id>] [--workspace <path>]";
3405
+ async function handleTaskSkills(subArgs, injectedApi) {
3406
+ await withTask(subArgs, SKILLS_USAGE, injectedApi, async (api, sessionId, task, _tasks, value) => {
3407
+ if (task.type !== "ai") fail("Manual tasks do not run an executor, so they take no skills.");
3408
+ const catalog = (0, import_core22.createSkillsService)(flag(subArgs, "--workspace") || process.cwd(), new import_core22.EnvConfig().workspaceRepos).listSkills().filter((s) => s.appliesTo === "task");
3409
+ if (!value) {
3410
+ console.log(`
3411
+ Skills \xB7 ${(0, import_plan_utils.titledTaskRef)(task)}
3412
+ `);
3413
+ if (catalog.length === 0) console.log(" No task skills found (applies-to: task in .ordewell/skills/ or ~/.ordewell/skills/).");
3414
+ for (const s of catalog) {
3415
+ console.log(` ${task.skills?.includes(s.name) ? "*" : " "} ${s.name}${s.description ? ` ${s.description}` : ""}`);
3416
+ }
3417
+ for (const name of task.skills ?? []) {
3418
+ if (!catalog.some((s) => s.name === name)) console.log(` * ${name} (not found)`);
3419
+ }
3420
+ console.log(`
3421
+ * = attached. ${SKILLS_USAGE}`);
3422
+ return;
3423
+ }
3424
+ const skills = value.toLowerCase() === "none" ? [] : [...new Set(value.toLowerCase().split(/[,\s]+/).filter(Boolean))];
3425
+ const unknown = skills.filter((name) => !catalog.some((s) => s.name === name));
3426
+ if (unknown.length > 0) {
3427
+ fail(`No task skill named ${unknown.map((n) => `"${n}"`).join(", ")}.`, `Task skills: ${catalog.map((s) => s.name).join(", ") || "none"}`);
3428
+ }
3429
+ await api.updateTask(sessionId, task.id, { skills });
3430
+ console.log(skills.length > 0 ? `Task ${(0, import_plan_utils.taskRef)(task)} skills set to ${skills.join(", ")}.` : `Task ${(0, import_plan_utils.taskRef)(task)} has no skills attached.`);
3431
+ }, { joinValue: true });
3432
+ }
3743
3433
  var OPS_USAGE = "Usage: ordewell task-ops <task-id-or-order> [on|off] [--session-id <id>]";
3744
3434
  async function handleTaskOps(subArgs, injectedApi) {
3745
3435
  await withTask(subArgs, OPS_USAGE, injectedApi, async (api, sessionId, task, tasks, value) => {
@@ -3760,7 +3450,7 @@ ${(0, import_plan_utils.titledTaskRef)(task)} is ${task.ops ? "an ops task: it r
3760
3450
  }
3761
3451
 
3762
3452
  // src/tui/index.ts
3763
- var import_core36 = require("@ordewell/core");
3453
+ var import_core37 = require("@ordewell/core");
3764
3454
 
3765
3455
  // src/tui/editor.ts
3766
3456
  function emptyEditor() {
@@ -3910,12 +3600,12 @@ var SLASH_COMMANDS = [
3910
3600
  { name: "force-start", usage: "/force-start <id>", description: "Start a task now, ignoring dependencies", category: "tasks" },
3911
3601
  { name: "continue", usage: "/continue <id> <message>", description: "Continue a finished task in its saved runner session, with your message", category: "tasks" },
3912
3602
  { 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" },
3913
- { name: "terminal", usage: "/terminal <id>", description: "Open a task: its OS terminal, or a structured task's live log", category: "tasks" },
3914
3603
  { name: "task-runner", usage: "/task-runner <id> [runner]", description: "Choose or set a task's executor (re-picks its model and mode)", category: "tasks" },
3915
3604
  { name: "task-model", usage: "/task-model <id> [model]", description: "Choose or set a task's executor model", category: "tasks" },
3916
3605
  { name: "task-effort", usage: "/task-effort <id> [level]", description: "Choose or set a task's thinking effort", category: "tasks" },
3917
3606
  { name: "task-mode", usage: "/task-mode <id> [mode]", description: "Choose or set a task's runner mode", category: "tasks" },
3918
3607
  { name: "task-deps", usage: "/task-deps <id>", description: "Edit which earlier tasks a task waits for", category: "tasks" },
3608
+ { name: "task-skills", usage: "/task-skills <id> [a,b|none]", description: "Choose or set the skills attached to a task; none clears them", category: "tasks" },
3919
3609
  { name: "task-ops", usage: "/task-ops <id> [on|off]", description: "Make a task an ops task \u2014 run in your checkout once its dependencies are merged \u2014 or a change task", category: "tasks" },
3920
3610
  // Models & providers
3921
3611
  { name: "planner", usage: "/planner [<provider>]", description: "Choose who plans \u2014 an API provider or a coding agent (no API key)", category: "models" },
@@ -3925,12 +3615,8 @@ var SLASH_COMMANDS = [
3925
3615
  { name: "allowlist", usage: "/allowlist [set <runner> <ids> | clear <runner>]", description: "Limit which models a runner may use", category: "models" },
3926
3616
  { name: "runners", usage: "/runners [<id> on|off]", description: "Enable or disable runners (claude-code, opencode, codex)", category: "models" },
3927
3617
  { name: "auto", usage: "/auto [full|guarded]", description: "Autonomy level for new plans: Full or Guarded", category: "models" },
3928
- { name: "transport", usage: "/transport [terminal|structured]", description: "Drive tasks through each runner's protocol (structured) or a terminal, from the next run", category: "models" },
3929
3618
  { name: "parallel", usage: "/parallel [<n>]", description: "How many AI tasks run at once (applies to a live run)", category: "models" },
3930
3619
  { name: "refresh", usage: "/refresh", description: "Re-discover runners and model catalogs", category: "models" },
3931
- // Skills
3932
- { name: "tdd", usage: "/tdd [on|off]", description: "Test-Driven Development mode", category: "skills" },
3933
- { name: "verify", usage: "/verify [on|off]", description: "Verification mode \u2014 final evidence-based task that runs the suite", category: "skills" },
3934
3620
  // Sessions
3935
3621
  { name: "sessions", usage: "/sessions", description: "List saved sessions", category: "session" },
3936
3622
  { name: "fork", usage: "/fork", description: "Continue in a copy of this conversation and its tasks; the original stays as it is", category: "session" },
@@ -3951,11 +3637,11 @@ var skillByName = /* @__PURE__ */ new Map();
3951
3637
  function registerSkillCommands(skills) {
3952
3638
  const map = /* @__PURE__ */ new Map();
3953
3639
  for (const skill of skills) {
3954
- if (BY_NAME.has(skill.name)) continue;
3640
+ if (BY_NAME.has(skill.name) || skill.userInvocable === false) continue;
3955
3641
  map.set(skill.name, {
3956
3642
  name: skill.name,
3957
3643
  usage: `/${skill.name}`,
3958
- description: skill.description,
3644
+ description: skill.appliesTo === "task" ? `task skill \xB7 ${skill.description}` : skill.description,
3959
3645
  category: "skills",
3960
3646
  source: "skill"
3961
3647
  });
@@ -4010,7 +3696,7 @@ function tokenCompletions(token) {
4010
3696
  }
4011
3697
 
4012
3698
  // src/tui/blocks.ts
4013
- var import_core22 = require("@ordewell/core");
3699
+ var import_core23 = require("@ordewell/core");
4014
3700
 
4015
3701
  // src/tui/markdown.ts
4016
3702
  function renderMarkdown(source, cols) {
@@ -4150,9 +3836,29 @@ var ROLE_PREFIX = {
4150
3836
  function messageLines(block, cols) {
4151
3837
  const text = sanitize(block.text);
4152
3838
  const room = Math.max(1, cols - 2);
4153
- const wrapped = block.role === "planner" || block.role === "agent" ? renderMarkdown(text, room) : wrap(text, room);
3839
+ const wrapped = block.role === "planner" || block.role === "agent" ? renderMarkdown(text, room) : block.skills ? skillPaintedLines(text, block.skills, room) : wrap(text, room);
4154
3840
  return wrapped.map((line, i) => i === 0 ? ROLE_PREFIX[block.role](line) : ` ${line}`);
4155
3841
  }
3842
+ function skillPaintedLines(text, skills, room) {
3843
+ const tokens = (0, import_core23.loadedSkillTokens)(text, skills);
3844
+ return wrapLines(text, room).map(({ line, start }) => {
3845
+ let painted = "";
3846
+ let at = 0;
3847
+ for (const token of tokens) {
3848
+ const from = Math.max(token.start - start, at);
3849
+ const to = Math.min(token.end - start, line.length);
3850
+ if (to <= from) continue;
3851
+ painted += line.slice(at, from) + style.cyan(line.slice(from, to));
3852
+ at = to;
3853
+ }
3854
+ return painted + line.slice(at);
3855
+ });
3856
+ }
3857
+ function skillLoadLine(block, cols) {
3858
+ const head = block.attaches ? `\u25CF /${sanitize(block.name)} will be attached to fitting tasks \xB7 ` : block.invokedBy === "planner" ? `\u25CF ${sanitize(block.name)} skill loaded by planner \xB7 ` : `\u25CF /${sanitize(block.name)} skill loaded \xB7 `;
3859
+ const line = truncate(`${head}${truncatePath(sanitize(block.path), Math.max(1, cols - width(head)))}`, cols);
3860
+ return `${style.green("\u25CF")}${style.grey(line.slice(1))}`;
3861
+ }
4156
3862
  function thinkingLines(block, cols, detailAll) {
4157
3863
  const text = sanitize(block.text).trim();
4158
3864
  const head = `\u2234 Thinking (${countOf(text ? text.split(/\s+/).length : 0, "word")})`;
@@ -4196,7 +3902,7 @@ function truncatePath(path, max) {
4196
3902
  if (width(char + tail) > max - 1) break;
4197
3903
  tail = char + tail;
4198
3904
  }
4199
- const slash = tail.indexOf("/");
3905
+ const slash = tail.search(/[\\/]/);
4200
3906
  return `\u2026${slash > 0 ? tail.slice(slash) : tail}`;
4201
3907
  }
4202
3908
  function commandHeader(block, cols, detailAll) {
@@ -4230,16 +3936,16 @@ function resultLines(block, room, detailAll) {
4230
3936
  const fit2 = (line) => detailAll ? wrap(line, room) : [truncate(line, room)];
4231
3937
  const outcome = unsettledOutcome(block);
4232
3938
  if (outcome) {
4233
- const [reason, ...rest] = (0, import_core22.outputLines)(block.output).map(sanitize);
3939
+ const [reason, ...rest] = (0, import_core23.outputLines)(block.output).map(sanitize);
4234
3940
  const status = fit2(reason ? `${outcome.label}: ${reason}` : outcome.label).map(outcome.paint);
4235
3941
  return detailAll ? [...status, ...rest.flatMap(fit2).map(style.grey)] : status;
4236
3942
  }
4237
3943
  if (detailAll) {
4238
- const lines2 = (0, import_core22.outputLines)(block.output).map(sanitize);
3944
+ const lines2 = (0, import_core23.outputLines)(block.output).map(sanitize);
4239
3945
  const note = lines2.length > PREVIEW_LINES ? ["\u2026 (ctrl+o to collapse)"] : [];
4240
3946
  return lines2.length > 0 ? [...lines2.flatMap(fit2), ...note].map(style.grey) : [style.grey("(no output)")];
4241
3947
  }
4242
- const { lines, hiddenLineCount } = (0, import_core22.outputPreview)(block.output, PREVIEW_LINES);
3948
+ const { lines, hiddenLineCount } = (0, import_core23.outputPreview)(block.output, PREVIEW_LINES);
4243
3949
  if (lines.length === 0) return [style.grey("(no output)")];
4244
3950
  const more = hiddenLineCount > 0 ? [`\u2026 +${countOf(hiddenLineCount, "line")} (ctrl+o to expand)`] : [];
4245
3951
  return [...lines.map(sanitize), ...more].map((line) => style.grey(truncate(line, room)));
@@ -4253,7 +3959,7 @@ var DIFF_PAINT = {
4253
3959
  };
4254
3960
  var DIFF_SIGN = { added: "+", removed: "-", context: " ", gap: "" };
4255
3961
  function diffLines(block, diff, room, detailAll) {
4256
- const rows = (0, import_core22.diffRows)(block.output);
3962
+ const rows = (0, import_core23.diffRows)(block.output);
4257
3963
  const gutter = Math.max(0, ...rows.map((row) => String(row.line ?? "").length));
4258
3964
  const draw = (row) => {
4259
3965
  const text = row.kind === "gap" ? `${" ".repeat(gutter)} \u22EE` : `${String(row.line ?? "").padStart(gutter)} ${DIFF_SIGN[row.kind]} ${sanitize(row.text)}`;
@@ -4262,7 +3968,7 @@ function diffLines(block, diff, room, detailAll) {
4262
3968
  const shown = detailAll ? rows : rows.slice(0, DIFF_PREVIEW_ROWS);
4263
3969
  const hidden = rows.length - shown.length;
4264
3970
  const more = hidden > 0 ? [style.grey(`\u2026 +${countOf(hidden, "line")} (ctrl+o to expand)`)] : [];
4265
- return [(0, import_core22.diffSummary)(diff), ...shown.flatMap(draw), ...more];
3971
+ return [(0, import_core23.diffSummary)(diff), ...shown.flatMap(draw), ...more];
4266
3972
  }
4267
3973
  function toolLines(block, cols, detailAll) {
4268
3974
  const room = Math.max(1, cols - OUTPUT_INDENT.length);
@@ -4350,6 +4056,8 @@ function blockLines(block, cols, detailAll) {
4350
4056
  return [approvalLine(block, cols)];
4351
4057
  case "plan":
4352
4058
  return [planLine(block, cols)];
4059
+ case "skill_load":
4060
+ return [skillLoadLine(block, cols)];
4353
4061
  // Pinned under the pane rather than drawn in it — see `tokenLine`.
4354
4062
  case "usage":
4355
4063
  return [];
@@ -4365,11 +4073,11 @@ function drawn(block, cols, detailAll) {
4365
4073
  }
4366
4074
  function conversationLines(blocks, cols, detailAll, answering) {
4367
4075
  const lines = [];
4368
- for (const block of blocks) {
4076
+ for (const [i, block] of blocks.entries()) {
4369
4077
  if (block.type === "usage") continue;
4370
4078
  lines.push(...drawn(block, cols, detailAll));
4371
4079
  if (block === answering) lines.push(approvalKeysLine(block, cols));
4372
- lines.push("");
4080
+ if (blocks[i + 1]?.type !== "skill_load") lines.push("");
4373
4081
  }
4374
4082
  return lines;
4375
4083
  }
@@ -4397,11 +4105,7 @@ function tokenLine(blocks, cols) {
4397
4105
  }
4398
4106
 
4399
4107
  // src/tui/state.ts
4400
- var import_core23 = require("@ordewell/core");
4401
- var SKILL_IDS = ["tdd", "verify"];
4402
- function noSkills() {
4403
- return { tdd: false, verify: false };
4404
- }
4108
+ var import_core24 = require("@ordewell/core");
4405
4109
  function visibleItems(picker2) {
4406
4110
  const filter = picker2.filter.trim().toLowerCase();
4407
4111
  if (!filter) return picker2.items;
@@ -4409,7 +4113,7 @@ function visibleItems(picker2) {
4409
4113
  (i) => i.id.toLowerCase().includes(filter) || i.label.toLowerCase().includes(filter) || (i.detail ?? "").toLowerCase().includes(filter)
4410
4114
  );
4411
4115
  }
4412
- var anyTaskRunning = (state) => state.tasks.some(import_core23.isTaskRunning);
4116
+ var anyTaskRunning = (state) => state.tasks.some(import_core24.isTaskRunning);
4413
4117
  var plannerInFlight = (state) => state.status === "planning" || state.status === "researching";
4414
4118
  function planRows(state) {
4415
4119
  const rows = [];
@@ -4446,9 +4150,9 @@ function findTask(tasks, id) {
4446
4150
  function initialState(overrides = {}) {
4447
4151
  return {
4448
4152
  editor: emptyEditor(),
4449
- conversation: import_core23.EMPTY_CONVERSATION,
4153
+ conversation: import_core24.EMPTY_CONVERSATION,
4450
4154
  taskView: null,
4451
- turnGate: import_core23.NO_TURN,
4155
+ turnGate: import_core24.NO_TURN,
4452
4156
  detailAll: false,
4453
4157
  status: "idle",
4454
4158
  busyLabel: "",
@@ -4462,22 +4166,21 @@ function initialState(overrides = {}) {
4462
4166
  taskEditor: null,
4463
4167
  scroll: 0,
4464
4168
  planScroll: null,
4465
- skills: noSkills(),
4466
4169
  runners: [],
4467
4170
  sessions: [],
4468
4171
  rewindTargets: null,
4469
4172
  models: [],
4470
4173
  modesByRunner: {},
4174
+ taskSkills: [],
4471
4175
  orchestratorModels: [],
4472
4176
  providerErrors: {},
4473
4177
  orchestratorModel: "",
4474
4178
  plannerProvider: "",
4475
4179
  plannerEffort: "",
4476
- maxParallel: import_core23.DEFAULT_MAX_PARALLEL,
4180
+ maxParallel: import_core24.DEFAULT_MAX_PARALLEL,
4477
4181
  configuredProviders: [],
4478
4182
  allowlist: {},
4479
4183
  autonomous: true,
4480
- runnerTransport: "structured",
4481
4184
  mouseCapture: true,
4482
4185
  selection: null,
4483
4186
  workspace: process.cwd(),
@@ -4485,7 +4188,7 @@ function initialState(overrides = {}) {
4485
4188
  gate: null,
4486
4189
  overlay: null,
4487
4190
  pendingApprovals: [],
4488
- queuedPrompts: import_core23.EMPTY_HOLD,
4191
+ queuedPrompts: import_core24.EMPTY_HOLD,
4489
4192
  stopArmed: false,
4490
4193
  stopArmToken: 0,
4491
4194
  stopRequested: false,
@@ -4533,7 +4236,7 @@ function chatEditorRoomFor(state, active) {
4533
4236
  }
4534
4237
 
4535
4238
  // src/tui/layout.ts
4536
- var import_core24 = require("@ordewell/core");
4239
+ var import_core25 = require("@ordewell/core");
4537
4240
  var CHROME_ROWS = 4;
4538
4241
  function chatInputWrap(state) {
4539
4242
  const active = state.focus === "chat" && !state.overlay;
@@ -4542,7 +4245,7 @@ function chatInputWrap(state) {
4542
4245
  }
4543
4246
  function footerHints(state) {
4544
4247
  const selected = selectedPlanRow(state)?.task;
4545
- const markHint = selected && (0, import_core24.markAction)(selected) === "uncomplete" ? "m undone" : "m done";
4248
+ const markHint = selected && (0, import_core25.markAction)(selected) === "uncomplete" ? "m undone" : "m done";
4546
4249
  const resolveHint = selected?.isolation?.state === "conflict" ? ["x resolve conflict"] : [];
4547
4250
  const planning = plannerInFlight(state) && !state.taskView;
4548
4251
  const escHint = !planning ? null : state.queuedPrompts.length > 0 ? "esc unsend" : "esc \xD72 stop planning";
@@ -4552,12 +4255,13 @@ function footerHints(state) {
4552
4255
  }
4553
4256
  return [
4554
4257
  ...escHint ? [escHint] : [],
4555
- "enter expand",
4258
+ "\u2192 expand/edit",
4556
4259
  "R runner",
4557
4260
  "o model",
4558
4261
  "e effort",
4559
4262
  "M mode",
4560
4263
  "D deps",
4264
+ "K skills",
4561
4265
  "O ops",
4562
4266
  "f start",
4563
4267
  "E run plan",
@@ -4567,13 +4271,13 @@ function footerHints(state) {
4567
4271
  "s skip",
4568
4272
  "a add",
4569
4273
  "d remove",
4570
- "t terminal",
4274
+ "enter log",
4571
4275
  ...resolveHint,
4572
4276
  "pgup/pgdn scroll",
4573
4277
  "tab chat"
4574
4278
  ];
4575
4279
  }
4576
- const detailHint = (0, import_core24.hasHiddenDetail)(state.taskView ? state.taskView.view.blocks : state.conversation.blocks) ? [state.detailAll ? "ctrl-o collapse all" : "ctrl-o expand all"] : [];
4280
+ 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"] : [];
4577
4281
  return [
4578
4282
  "/help",
4579
4283
  "tab plan",
@@ -4609,10 +4313,10 @@ function queuedTaskBubble(message, selected, cols) {
4609
4313
  return wrap(text, room).map((line, i) => i === 0 ? truncate(`${marker2} ${selected ? style.bold(line) : line}${style.grey(tag)}`, cols) : truncate(` ${line}`, cols));
4610
4314
  }
4611
4315
  function taskActivity(task, working) {
4612
- const approvals = (0, import_core24.approvalLabel)(task?.awaitingApproval);
4316
+ const approvals = (0, import_core25.approvalLabel)(task?.awaitingApproval);
4613
4317
  if (approvals) return inRow(approvals);
4614
4318
  if (working) return "working";
4615
- if (task?.status === "awaiting_user") return inRow((0, import_core24.awaitingLabel)(task) ?? "Waiting for your input");
4319
+ if (task?.status === "awaiting_user") return inRow((0, import_core25.awaitingLabel)(task) ?? "Waiting for your input");
4616
4320
  return task?.status ?? "";
4617
4321
  }
4618
4322
  function taskHeaderLines(state, tv, cols) {
@@ -4687,9 +4391,9 @@ function chatScrollMax(state) {
4687
4391
  }
4688
4392
  function plannerSetup(state) {
4689
4393
  const provider = state.plannerProvider;
4690
- const registration = import_core24.ALL_PROVIDERS[provider];
4394
+ const registration = import_core25.ALL_PROVIDERS[provider];
4691
4395
  if (!registration) return { label: "not set", ready: false };
4692
- const runner = (0, import_core24.runnerForProvider)(provider);
4396
+ const runner = (0, import_core25.runnerForProvider)(provider);
4693
4397
  const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
4694
4398
  return { label: registration.label, ready };
4695
4399
  }
@@ -4861,7 +4565,7 @@ function planScrollExtent(state) {
4861
4565
  }
4862
4566
  function taskLines(state, planRow, index, cols, orderOf) {
4863
4567
  const { task, parent } = planRow;
4864
- const row = (0, import_core24.taskRowView)(task, { parent, orderOf, modes: modesForTask(state.modesByRunner, task) });
4568
+ const row = (0, import_core25.taskRowView)(task, { parent, orderOf, modes: modesForTask(state.modesByRunner, task) });
4865
4569
  const selected = state.focus === "plan" && index === state.selectedTask;
4866
4570
  const expanded = state.expandedTaskId === task.id;
4867
4571
  const rawIcon = row.status === "running" ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[row.status];
@@ -4888,11 +4592,11 @@ function taskLines(state, planRow, index, cols, orderOf) {
4888
4592
  const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}${row.autonomous ? style.yellow(" \u26A1") : ""}` : "";
4889
4593
  const runner = task.assignedRunner ?? "";
4890
4594
  const bodyPad = parent ? " " : " ";
4891
- const structured = task.transport?.kind === "structured";
4892
- const activity = row.approvals ? `${inRow(row.approvals)} \u2014 t opens it` : row.status === "quiet" ? structured ? "quiet" : "quiet \u2014 t opens its terminal" : row.running ? "working" : row.awaiting ? inRow(row.awaiting) : "";
4893
- const meta = [activity, runner, structured ? "structured" : "", model].filter(Boolean).join(" \xB7 ");
4595
+ const activity = row.approvals ? `${inRow(row.approvals)} \u2014 enter opens it` : row.status === "quiet" ? "quiet" : row.running ? "working" : row.awaiting ? inRow(row.awaiting) : "";
4596
+ const skills = (task.skills ?? []).join(" \xB7 ");
4597
+ const meta = [activity, runner, model].filter(Boolean).join(" \xB7 ");
4894
4598
  if (meta) lines.push(style.grey(truncate(`${bodyPad}${meta}`, cols)));
4895
- if (task.transport?.fallback) lines.push(style.yellow(truncate(`${bodyPad}terminal: ${task.transport.fallback}`, cols)));
4599
+ if (skills) lines.push(style.grey(truncate(`${bodyPad}skills: ${skills}`, cols)));
4896
4600
  if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
4897
4601
  if (row.conflict) {
4898
4602
  const where = row.conflict.repo ? ` in ${row.conflict.repo}` : "";
@@ -4947,7 +4651,7 @@ function taskLines(state, planRow, index, cols, orderOf) {
4947
4651
  lines.push(...taskText("Depends on", row.dependencies.join(", "), cols, bodyPad));
4948
4652
  }
4949
4653
  lines.push(style.grey(`${bodyPad}enter save \xB7 esc cancel`));
4950
- lines.push(style.grey(truncate(`${bodyPad}then R runner \xB7 o model \xB7 e effort \xB7 M mode \xB7 D deps`, cols)));
4654
+ lines.push(style.grey(truncate(`${bodyPad}then R runner \xB7 o model \xB7 e effort \xB7 M mode \xB7 D deps \xB7 K skills`, cols)));
4951
4655
  }
4952
4656
  return { lines, editorLine };
4953
4657
  }
@@ -4992,7 +4696,7 @@ function helpLayout(rows, cols) {
4992
4696
  );
4993
4697
  body.push(
4994
4698
  "",
4995
- style.bold("In a task view (t or /terminal on a structured task)"),
4699
+ style.bold("In a task view (enter on a task)"),
4996
4700
  style.grey(" ctrl-s sends now: interrupts the running step, then delivers the composer text \u2014 or, with it empty, the selected queued message"),
4997
4701
  style.grey(" ctrl-n/ctrl-p select a queued message \xB7 ctrl-r removes it \xB7 ctrl-x interrupts"),
4998
4702
  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"),
@@ -5008,13 +4712,13 @@ function helpScrollMax(state) {
5008
4712
  }
5009
4713
 
5010
4714
  // src/tui/render.ts
5011
- var import_core25 = require("@ordewell/core");
4715
+ var import_core26 = require("@ordewell/core");
5012
4716
  function render(state) {
5013
4717
  const { rows, cols } = state;
5014
4718
  const rowsForBody = bodyRows(state);
5015
4719
  const body = state.overlay ? renderOverlay(state, rowsForBody, cols) : renderBody(state, rowsForBody, cols);
5016
4720
  const lines = [
5017
- renderSkills(state, cols),
4721
+ renderAutonomy(state, cols),
5018
4722
  ...body,
5019
4723
  renderStatus(state, cols),
5020
4724
  ...renderInput(state),
@@ -5099,13 +4803,9 @@ function fit(lines, rows, anchor) {
5099
4803
  const blanks = Array(rows - lines.length).fill("");
5100
4804
  return anchor === "bottom" ? [...blanks, ...lines] : [...lines, ...blanks];
5101
4805
  }
5102
- function renderSkills(state, cols) {
5103
- const badges = SKILL_IDS.map(
5104
- (id) => state.skills[id] ? style.green(`\u25CF ${id}`) : style.grey(`\u25CB ${id}`)
5105
- );
4806
+ function renderAutonomy(state, cols) {
5106
4807
  const auto = state.autonomous ? style.yellow("\u25CF Full") : style.green("\u25CF Guarded");
5107
- const terminal = state.runnerTransport === "terminal" ? [style.yellow("\u25CF terminal")] : [];
5108
- return truncate([...badges, auto, ...terminal].join(" "), cols);
4808
+ return truncate(auto, cols);
5109
4809
  }
5110
4810
  function scrolledBackMark(state) {
5111
4811
  return state.focus !== "plan" && state.scroll > 0 ? "\u2191 scrolled back \xB7 pgdn for live" : "";
@@ -5273,8 +4973,8 @@ function renderHandoff(state, overlay, rows, cols) {
5273
4973
  return frame(`Diff \u2014 ${handoffBranch(handoff)}`, [...shown, "", style.grey(`${more}enter or esc goes back`)], rows, cols);
5274
4974
  }
5275
4975
  const landed = handoff.landed.length === 0 ? [style.grey("Nothing landed on it.")] : handoff.landed.map((task) => {
5276
- const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core25.capConflictFiles)(task.repairedFiles)})`) : "";
5277
- return ` ${style.green("\u2713")} ${(0, import_core25.titledTaskRef)(task)}${repaired}`;
4976
+ const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core26.capConflictFiles)(task.repairedFiles)})`) : "";
4977
+ return ` ${style.green("\u2713")} ${(0, import_core26.titledTaskRef)(task)}${repaired}`;
5278
4978
  });
5279
4979
  const actions = handoffActions(handoff).map((action, index) => {
5280
4980
  const active = index === overlay.index;
@@ -5363,10 +5063,10 @@ function frame(title, body, rows, cols) {
5363
5063
  }
5364
5064
 
5365
5065
  // src/tui/reducer.ts
5366
- var import_core33 = require("@ordewell/core");
5066
+ var import_core34 = require("@ordewell/core");
5367
5067
 
5368
5068
  // src/tui/reducers/overlays.ts
5369
- var import_core31 = require("@ordewell/core");
5069
+ var import_core32 = require("@ordewell/core");
5370
5070
 
5371
5071
  // src/tui/reducers/shared.ts
5372
5072
  function step(state, effects = []) {
@@ -5415,10 +5115,10 @@ function handleApprovalKey(state, overlay, key) {
5415
5115
  }
5416
5116
 
5417
5117
  // src/tui/reducers/commands.ts
5418
- var import_core30 = require("@ordewell/core");
5118
+ var import_core31 = require("@ordewell/core");
5419
5119
 
5420
5120
  // src/tui/reducers/pickers.ts
5421
- var import_core26 = require("@ordewell/core");
5121
+ var import_core27 = require("@ordewell/core");
5422
5122
  var import_plan_utils2 = require("@ordewell/core/plan-utils");
5423
5123
  var DEFAULT_EFFORT = "__runner_default__";
5424
5124
  function picker(title, items, action, extra = {}) {
@@ -5437,11 +5137,11 @@ function modelDetail(m) {
5437
5137
  function providerErrorHint(state) {
5438
5138
  const failed = Object.keys(state.providerErrors ?? {});
5439
5139
  if (failed.length === 0) return void 0;
5440
- const names = failed.map((p) => import_core26.ALL_PROVIDERS[p]?.label ?? p);
5140
+ const names = failed.map((p) => import_core27.ALL_PROVIDERS[p]?.label ?? p);
5441
5141
  return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
5442
5142
  }
5443
5143
  function plannerModelItems(state) {
5444
- const runner = (0, import_core26.runnerForProvider)(state.plannerProvider);
5144
+ const runner = (0, import_core27.runnerForProvider)(state.plannerProvider);
5445
5145
  if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
5446
5146
  const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
5447
5147
  id: m.id,
@@ -5455,7 +5155,7 @@ function plannerModelItems(state) {
5455
5155
  return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
5456
5156
  }
5457
5157
  function plannerEffortItems(state) {
5458
- const runner = (0, import_core26.runnerForProvider)(state.plannerProvider);
5158
+ const runner = (0, import_core27.runnerForProvider)(state.plannerProvider);
5459
5159
  if (!runner) {
5460
5160
  return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
5461
5161
  }
@@ -5519,10 +5219,21 @@ function pickerItemsFor(state, action) {
5519
5219
  if (action.kind === "set-task-deps") {
5520
5220
  return (0, import_plan_utils2.dependencyCandidates)(state.tasks, action.taskId).map((candidate) => ({
5521
5221
  id: candidate.id,
5522
- label: (0, import_core26.titledTaskRef)(candidate),
5222
+ label: (0, import_core27.titledTaskRef)(candidate),
5523
5223
  detail: candidate.status === "completed" ? "already completed" : void 0
5524
5224
  }));
5525
5225
  }
5226
+ if (action.kind === "set-task-skills") {
5227
+ const task = findTask(state.tasks, action.taskId);
5228
+ if (!task) return [];
5229
+ const known = new Set(state.taskSkills.map((s) => s.name));
5230
+ return [
5231
+ ...state.taskSkills.map((s) => ({ id: s.name, label: s.name, detail: s.description || void 0 })),
5232
+ // Attached but not in the catalog (created by an earlier task's worktree): keep it
5233
+ // selectable so confirming the picker does not silently detach it.
5234
+ ...(task.skills ?? []).filter((name) => !known.has(name)).map((name) => ({ id: name, label: `${name} (not found)`, detail: "not found in the catalog" }))
5235
+ ];
5236
+ }
5526
5237
  if (action.kind === "set-task-mode") {
5527
5238
  const task = findTask(state.tasks, action.taskId);
5528
5239
  if (!task) return [];
@@ -5564,28 +5275,24 @@ function pickerItemsFor(state, action) {
5564
5275
  }
5565
5276
  function plannerItems(state) {
5566
5277
  const installed = new Set(state.runners.map((r) => r.id));
5567
- const agents = import_core26.CLI_PROVIDERS.map((id) => {
5568
- const runner = (0, import_core26.runnerForProvider)(id);
5569
- const ready = installed.has(runner);
5570
- return {
5571
- id,
5572
- label: import_core26.ALL_PROVIDERS[id].label,
5573
- detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
5574
- selected: state.plannerProvider === id,
5575
- disabled: !ready
5576
- };
5577
- });
5578
- const vendors = import_core26.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
5579
- id,
5580
- label: import_core26.ALL_PROVIDERS[id].label,
5581
- detail: import_core26.ALL_PROVIDERS[id].apiKeyEnvVar,
5582
- selected: state.plannerProvider === id
5278
+ const usability = {};
5279
+ for (const id of import_core27.CLI_PROVIDERS) {
5280
+ const runner = (0, import_core27.runnerForProvider)(id);
5281
+ usability[runner] = installed.has(runner) ? { usable: true } : { usable: false, reason: "CLI not installed or not on PATH" };
5282
+ }
5283
+ const entries = (0, import_core27.plannerBackendEntries)(usability, state.configuredProviders);
5284
+ const items = entries.map((entry) => ({
5285
+ id: entry.id,
5286
+ label: entry.label,
5287
+ detail: entry.reason,
5288
+ selected: state.plannerProvider === entry.id,
5289
+ disabled: !entry.usable
5583
5290
  }));
5584
- return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
5291
+ return entries.some((entry) => entry.kind === "vendor") ? items : [...items, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
5585
5292
  }
5586
5293
 
5587
5294
  // src/tui/reducers/planPane.ts
5588
- var import_core28 = require("@ordewell/core");
5295
+ var import_core29 = require("@ordewell/core");
5589
5296
 
5590
5297
  // src/tui/reducers/pointer.ts
5591
5298
  var WHEEL_NOTCH = 3;
@@ -5663,13 +5370,13 @@ function handleMouseSelect(state, key) {
5663
5370
  }
5664
5371
 
5665
5372
  // src/tui/reducers/taskView.ts
5666
- var import_core27 = require("@ordewell/core");
5373
+ var import_core28 = require("@ordewell/core");
5667
5374
  function openTaskView(state, sessionId, taskId) {
5668
5375
  const task = findTask(state.tasks, taskId);
5669
5376
  if (!task) return step(say(state, "system", `No task ${taskId} in this plan.`));
5670
5377
  const taskView = {
5671
5378
  taskId,
5672
- view: import_core27.EMPTY_TASK_LOG,
5379
+ view: import_core28.EMPTY_TASK_LOG,
5673
5380
  attempts: [],
5674
5381
  attempt: 0,
5675
5382
  pending: [],
@@ -5679,11 +5386,6 @@ function openTaskView(state, sessionId, taskId) {
5679
5386
  };
5680
5387
  return step({ ...state, taskView, focus: "chat", scroll: 0 }, [{ type: "openTaskLog", sessionId, taskId }]);
5681
5388
  }
5682
- function openTaskTerminalOrView(state, sessionId, taskId) {
5683
- const task = findTask(state.tasks, taskId);
5684
- if (task?.transport?.kind === "structured") return openTaskView(state, sessionId, taskId);
5685
- return step(state, [{ type: "openTaskTerminal", sessionId, taskId }]);
5686
- }
5687
5389
  function continueTaskStep(state, sessionId, taskId, text) {
5688
5390
  const opened = state.taskView?.taskId === taskId ? step({ ...state, taskView: { ...state.taskView, followLatest: true } }) : openTaskView(state, sessionId, taskId);
5689
5391
  const watch = state.status !== "executing";
@@ -5702,13 +5404,13 @@ function applyLive(tv, attempt, events) {
5702
5404
  }
5703
5405
  next = {
5704
5406
  ...next,
5705
- view: import_core27.EMPTY_TASK_LOG,
5407
+ view: import_core28.EMPTY_TASK_LOG,
5706
5408
  attempt,
5707
5409
  attempts: next.attempts.includes(attempt) ? next.attempts : [...next.attempts, attempt]
5708
5410
  };
5709
5411
  }
5710
5412
  if (attempt < next.attempt) return next;
5711
- return clampQueueIndex({ ...next, view: events.reduce(import_core27.reduceTaskLog, next.view) });
5413
+ return clampQueueIndex({ ...next, view: events.reduce(import_core28.reduceTaskLog, next.view) });
5712
5414
  }
5713
5415
  function taskLogAbandoned(state) {
5714
5416
  const tv = state.taskView;
@@ -5728,14 +5430,14 @@ function taskLogArrived(state, action) {
5728
5430
  function announceApprovals(state, action) {
5729
5431
  if (state.taskView?.taskId === action.taskId) return state;
5730
5432
  const task = findTask(state.tasks, action.taskId);
5731
- const spoken = action.events.reduce((s, event) => event.type === "approval_requested" ? say(s, "system", `\xB7 Task ${task?.order ?? "?"} waits for approval: ${(0, import_core27.runnerToolSubject)(event.tool, event.args)} \u2014 t on it, then ctrl-y to allow or ctrl-g to deny`) : s, state);
5433
+ 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 enter on it, then ctrl-y to allow or ctrl-g to deny`) : s, state);
5732
5434
  return spoken !== state ? { ...spoken, scroll: state.scroll } : spoken;
5733
5435
  }
5734
5436
  function announceCheckpoint(state, action) {
5735
5437
  const task = findTask(state.tasks, action.taskId) ?? state.tasks.find((t) => t.title === action.title);
5736
5438
  const label = task ? `Task ${task.order}` : action.title;
5737
- const where = task?.transport?.kind === "structured" ? "t on it to answer" : `/checkpoint ${task?.order ?? "<id>"} approve|reject to answer`;
5738
- const spoken = say(state, "system", `\xB7 ${label} asks: ${(0, import_core27.truncateCheckpointSummary)(action.summary)} \u2014 ${where}`);
5439
+ const where = "enter on it to answer";
5440
+ const spoken = say(state, "system", `\xB7 ${label} asks: ${(0, import_core28.truncateCheckpointSummary)(action.summary)} \u2014 ${where}`);
5739
5441
  return { ...spoken, scroll: state.scroll };
5740
5442
  }
5741
5443
  function taskLogLoaded(state, action) {
@@ -5743,7 +5445,7 @@ function taskLogLoaded(state, action) {
5743
5445
  if (!tv || tv.taskId !== action.taskId) return state;
5744
5446
  const loaded = action.attempt >= 1 ? {
5745
5447
  ...tv,
5746
- view: (0, import_core27.replayTaskLog)(action.events),
5448
+ view: (0, import_core28.replayTaskLog)(action.events),
5747
5449
  attempts: action.attempts ?? tv.attempts,
5748
5450
  attempt: action.attempt
5749
5451
  } : { ...tv, attempts: action.attempts ?? tv.attempts };
@@ -5895,6 +5597,34 @@ function openTaskDepsPicker(state, task) {
5895
5597
  }
5896
5598
  });
5897
5599
  }
5600
+ function openTaskSkillsPicker(state, task) {
5601
+ if (task.type !== "ai") return fail3(state, "Manual tasks do not run an executor, so they take no skills.");
5602
+ const action = { kind: "set-task-skills", taskId: task.id };
5603
+ const items = pickerItemsFor(state, action);
5604
+ if (items.length === 0) {
5605
+ return fail3(state, "No task skills found. A skill with applies-to: task in .ordewell/skills/ or ~/.ordewell/skills/ shows up here.");
5606
+ }
5607
+ return step({
5608
+ ...state,
5609
+ overlay: {
5610
+ kind: "picker",
5611
+ picker: picker(`Skills \xB7 ${(0, import_plan_utils3.titledTaskRef)(task)}`, items, action, {
5612
+ hint: "Attached skills go into this task's prompt when it starts.",
5613
+ multi: true,
5614
+ chosen: task.skills ?? []
5615
+ })
5616
+ }
5617
+ });
5618
+ }
5619
+ function assignTaskSkills(state, sessionId, task, skills) {
5620
+ return step(state, [{
5621
+ type: "updateTask",
5622
+ sessionId,
5623
+ taskId: task.id,
5624
+ changes: { skills },
5625
+ message: skills.length > 0 ? `Task ${(0, import_plan_utils3.taskRef)(task)} skills set to ${skills.join(", ")}.` : `Task ${(0, import_plan_utils3.taskRef)(task)} has no skills attached.`
5626
+ }]);
5627
+ }
5898
5628
  function openTaskModePicker(state, task) {
5899
5629
  if (task.type !== "ai") return fail3(state, "Manual tasks do not have an executor mode.");
5900
5630
  const modes = modesForTask(state.modesByRunner, task);
@@ -6035,6 +5765,20 @@ function taskEffortCommand(state, args) {
6035
5765
  return assignTaskEffort(state, sessionId, task, value);
6036
5766
  });
6037
5767
  }
5768
+ function taskSkillsCommand(state, args) {
5769
+ return taskCommand(state, args[0], (sessionId, taskId) => {
5770
+ const task = findTask(state.tasks, taskId);
5771
+ if (args.length < 2) return openTaskSkillsPicker(state, task);
5772
+ if (task.type !== "ai") return fail3(state, "Manual tasks do not run an executor, so they take no skills.");
5773
+ const value = args.slice(1).join(" ");
5774
+ const names = value.toLowerCase() === "none" ? [] : [...new Set(value.toLowerCase().split(/[,\s]+/).filter(Boolean))];
5775
+ const unknown = names.filter((name) => !state.taskSkills.some((s) => s.name === name));
5776
+ if (unknown.length > 0) {
5777
+ return fail3(state, `No task skill named ${unknown.map((n) => `"${n}"`).join(", ")}. Task skills: ${state.taskSkills.map((s) => s.name).join(", ") || "none"}.`);
5778
+ }
5779
+ return assignTaskSkills(state, sessionId, task, names);
5780
+ });
5781
+ }
6038
5782
  function taskOpsCommand(state, args) {
6039
5783
  return taskCommand(state, args[0], (_sessionId, taskId) => {
6040
5784
  const task = findTask(state.tasks, taskId);
@@ -6105,7 +5849,8 @@ function handlePlanKey(state, key) {
6105
5849
  const row = selectedPlanRow(state);
6106
5850
  if (!row) return step(state);
6107
5851
  const task = row.task;
6108
- if (key.name === "enter" || key.name === "right") {
5852
+ if (key.name === "enter" && state.sessionId) return openTaskView(state, state.sessionId, task.id);
5853
+ if (key.name === "right") {
6109
5854
  const hasSubtasks = (task.subtasks?.length ?? 0) > 0;
6110
5855
  if (hasSubtasks && state.expandedTaskId !== task.id) {
6111
5856
  return step(settlePlan({ ...state, expandedTaskId: task.id, taskEditor: null }));
@@ -6120,14 +5865,13 @@ function handlePlanKey(state, key) {
6120
5865
  if (!state.sessionId || key.name !== "char") return step(state);
6121
5866
  if (key.char === "E") return step(state, [{ type: "execute", sessionId: state.sessionId }]);
6122
5867
  if (key.char === "S") return step(state, [{ type: "stopExecution", sessionId: state.sessionId }]);
6123
- const action = key.char === "m" ? (0, import_core28.markAction)(task) : PLAN_SHORTCUTS[key.char ?? ""];
5868
+ const action = key.char === "m" ? (0, import_core29.markAction)(task) : PLAN_SHORTCUTS[key.char ?? ""];
6124
5869
  if (action === "force-start" && task.mergeGate?.length) return confirmForceStartPastGate(state, task);
6125
5870
  if (action) {
6126
5871
  return step(state, [taskActionEffect(state, state.sessionId, task.id, action)]);
6127
5872
  }
6128
5873
  if (key.char === "O") return toggleTaskOps(state, task, row.parent !== null);
6129
5874
  if (key.char === "d") return confirmRemoveTask(state, task);
6130
- if (key.char === "t") return openTaskTerminalOrView(state, state.sessionId, task.id);
6131
5875
  if (key.char === "x" && task.isolation?.state === "conflict") {
6132
5876
  return step(state, [{ type: "resolveConflict", sessionId: state.sessionId, taskId: task.id }]);
6133
5877
  }
@@ -6136,6 +5880,7 @@ function handlePlanKey(state, key) {
6136
5880
  if (key.char === "e") return openTaskEffortPicker(state, task);
6137
5881
  if (key.char === "M") return openTaskModePicker(state, task);
6138
5882
  if (key.char === "D") return openTaskDepsPicker(state, task);
5883
+ if (key.char === "K") return openTaskSkillsPicker(state, task);
6139
5884
  return step(state);
6140
5885
  }
6141
5886
  function handleTaskEditKey(state, task, editor, key) {
@@ -6155,14 +5900,14 @@ function commitTaskEdit(state, task, editor) {
6155
5900
  sessionId: state.sessionId,
6156
5901
  taskId: task.id,
6157
5902
  changes: { prompt },
6158
- message: `Task ${(0, import_core28.taskRef)(task)} prompt updated.`
5903
+ message: `Task ${(0, import_core29.taskRef)(task)} prompt updated.`
6159
5904
  }]);
6160
5905
  }
6161
5906
 
6162
5907
  // src/tui/reducers/turnQueue.ts
6163
- var import_core29 = require("@ordewell/core");
5908
+ var import_core30 = require("@ordewell/core");
6164
5909
  function drainQueue(state, settled) {
6165
- const next = (0, import_core29.drainNext)(state.queuedPrompts);
5910
+ const next = (0, import_core30.drainNext)(state.queuedPrompts);
6166
5911
  const ended = disarmStop({ ...settled, stopRequested: false });
6167
5912
  if (!next) return step(ended);
6168
5913
  const spoken = say(ended, "user", next.text);
@@ -6181,14 +5926,14 @@ function stopPlanning(state, sessionId) {
6181
5926
  return step(queueToEditor(cutTurn({ ...dismissed, stopArmed: false, stopRequested: true })), [{ type: "cancelPlanning", sessionId }]);
6182
5927
  }
6183
5928
  function queueToEditor(state) {
6184
- const all = (0, import_core29.unsendAll)(state.queuedPrompts);
5929
+ const all = (0, import_core30.unsendAll)(state.queuedPrompts);
6185
5930
  if (!all) return state;
6186
5931
  return { ...state, queuedPrompts: all.rest, editor: { ...state.editor, text: all.text, cursor: all.text.length } };
6187
5932
  }
6188
5933
  function unsendLatest(state) {
6189
- const latest = (0, import_core29.unsendLatest)(state.queuedPrompts);
5934
+ const latest = (0, import_core30.unsendLatest)(state.queuedPrompts);
6190
5935
  if (!latest) return step(state);
6191
- const text = (0, import_core29.aheadOfDraft)(latest.text, state.editor.text);
5936
+ const text = (0, import_core30.aheadOfDraft)(latest.text, state.editor.text);
6192
5937
  return step({
6193
5938
  ...state,
6194
5939
  queuedPrompts: latest.rest,
@@ -6205,14 +5950,11 @@ function plannerEscape(state) {
6205
5950
  }
6206
5951
 
6207
5952
  // src/tui/reducers/commands.ts
6208
- var KNOWN_PROVIDERS2 = Object.keys(import_core30.ALL_PROVIDERS);
5953
+ var KNOWN_PROVIDERS = Object.keys(import_core31.ALL_PROVIDERS);
5954
+ var unknownCommand = (state, name) => fail3(state, `Unknown command: /${name} \u2014 type /help to see what's available.`);
5955
+ var skillInTaskView = (state, name) => fail3(state, `/${name} is a skill, and skills aren't loaded in a task's chat. To give a task a skill, use /task-skills <id> <name>.`);
6209
5956
  function runCommand(state, { name, args }) {
6210
- if (!findCommand(name)) {
6211
- return fail3(state, `Unknown command: /${name} \u2014 type /help to see what's available.`);
6212
- }
6213
- if (SKILL_IDS.includes(name)) {
6214
- return toggleSkill(state, name, args[0]);
6215
- }
5957
+ if (!findCommand(name)) return unknownCommand(state, name);
6216
5958
  switch (name) {
6217
5959
  case "help":
6218
5960
  return step({ ...state, overlay: { kind: "help", scroll: 0 } });
@@ -6246,8 +5988,6 @@ function runCommand(state, { name, args }) {
6246
5988
  return runners(state, args);
6247
5989
  case "auto":
6248
5990
  return setAutonomous(state, args[0]);
6249
- case "transport":
6250
- return setTransport(state, args[0]);
6251
5991
  case "parallel":
6252
5992
  return setMaxParallel(state, args[0]);
6253
5993
  case "mouse":
@@ -6284,8 +6024,6 @@ function runCommand(state, { name, args }) {
6284
6024
  args[0],
6285
6025
  (sessionId, taskId) => step(state, [{ type: "removeTask", sessionId, taskId }])
6286
6026
  );
6287
- case "terminal":
6288
- return taskCommand(state, args[0], (sessionId, taskId) => openTaskTerminalOrView(state, sessionId, taskId));
6289
6027
  case "continue":
6290
6028
  return taskCommand(state, args[0], (sessionId, taskId) => {
6291
6029
  const text = args.slice(1).join(" ").trim();
@@ -6310,6 +6048,8 @@ function runCommand(state, { name, args }) {
6310
6048
  return taskModeCommand(state, args);
6311
6049
  case "task-ops":
6312
6050
  return taskOpsCommand(state, args);
6051
+ case "task-skills":
6052
+ return taskSkillsCommand(state, args);
6313
6053
  case "task-deps":
6314
6054
  return taskCommand(
6315
6055
  state,
@@ -6343,11 +6083,6 @@ function resolveToggle(arg, current) {
6343
6083
  if (value === void 0) return !current;
6344
6084
  return null;
6345
6085
  }
6346
- function toggleSkill(state, skill, arg) {
6347
- const enabled = resolveToggle(arg, state.skills[skill]);
6348
- if (enabled === null) return fail3(state, `Usage: /${skill} [on|off]`);
6349
- return step(state, [{ type: "command", name: skill, action: enabled ? "on" : "off" }]);
6350
- }
6351
6086
  function requestNewSession(state) {
6352
6087
  const hasContent = state.tasks.length > 0 || state.conversation.blocks.length > 0 || state.goal !== "" || state.status !== "idle";
6353
6088
  if (!state.sessionId || !hasContent) return newSession(state);
@@ -6371,8 +6106,8 @@ function newSession(state) {
6371
6106
  taskView: null,
6372
6107
  tasks: [],
6373
6108
  planApproved: false,
6374
- conversation: import_core30.EMPTY_CONVERSATION,
6375
- turnGate: import_core30.NO_TURN,
6109
+ conversation: import_core31.EMPTY_CONVERSATION,
6110
+ turnGate: import_core31.NO_TURN,
6376
6111
  selectedTask: 0,
6377
6112
  expandedTaskId: null,
6378
6113
  taskEditor: null,
@@ -6383,14 +6118,14 @@ function newSession(state) {
6383
6118
  // Prompts belong to the session that raised them; the old planner is gone
6384
6119
  // and its pending requests deny on their own timeout.
6385
6120
  pendingApprovals: [],
6386
- queuedPrompts: import_core30.EMPTY_HOLD,
6121
+ queuedPrompts: import_core31.EMPTY_HOLD,
6387
6122
  stopArmed: false,
6388
6123
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6389
6124
  }, closeEffects);
6390
6125
  }
6391
6126
  function setModel(state, args) {
6392
6127
  if (args[0] === "set" && args[1]) {
6393
- const runner = (0, import_core30.runnerForProvider)(state.plannerProvider);
6128
+ const runner = (0, import_core31.runnerForProvider)(state.plannerProvider);
6394
6129
  const known = runner ? modelsForRunner(state.models, runner) : [];
6395
6130
  if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
6396
6131
  return fail3(state, `${args[1]} was not discovered for ${runner}.`);
@@ -6413,7 +6148,7 @@ function setModel(state, args) {
6413
6148
  function setPlanner(state, args) {
6414
6149
  if (args[0]) {
6415
6150
  const id = args[0].toLowerCase();
6416
- if (!KNOWN_PROVIDERS2.includes(id)) {
6151
+ if (!KNOWN_PROVIDERS.includes(id)) {
6417
6152
  return fail3(state, `Unknown planner: ${id}. Run /planner with no arguments to see the list.`);
6418
6153
  }
6419
6154
  return step(state, [{ type: "setPlanner", provider: id }]);
@@ -6452,7 +6187,7 @@ function setPlannerEffort(state, args) {
6452
6187
  function setKey(state, args) {
6453
6188
  if (args[0] === "set" && args[1]) {
6454
6189
  const provider = args[1].toLowerCase();
6455
- if (!KNOWN_PROVIDERS2.includes(provider)) {
6190
+ if (!KNOWN_PROVIDERS.includes(provider)) {
6456
6191
  return fail3(state, `Unknown provider: ${provider}. Run /key with no arguments to see the list.`);
6457
6192
  }
6458
6193
  if (!args[2]) {
@@ -6463,16 +6198,16 @@ function setKey(state, args) {
6463
6198
  }
6464
6199
  return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
6465
6200
  }
6466
- const items = import_core30.PROVIDER_PRIORITY.map((id) => ({
6201
+ const items = import_core31.PROVIDER_PRIORITY.map((id) => ({
6467
6202
  id,
6468
- label: import_core30.ALL_PROVIDERS[id].label,
6469
- detail: import_core30.ALL_PROVIDERS[id].apiKeyEnvVar,
6203
+ label: import_core31.ALL_PROVIDERS[id].label,
6204
+ detail: import_core31.ALL_PROVIDERS[id].apiKeyEnvVar,
6470
6205
  selected: state.configuredProviders.includes(id)
6471
6206
  }));
6472
6207
  return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
6473
6208
  }
6474
6209
  function keyPrompt(provider) {
6475
- const meta = import_core30.ALL_PROVIDERS[provider];
6210
+ const meta = import_core31.ALL_PROVIDERS[provider];
6476
6211
  return {
6477
6212
  kind: "prompt",
6478
6213
  title: `${meta.label} API key`,
@@ -6549,21 +6284,16 @@ function runners(state, args) {
6549
6284
  }
6550
6285
  function setMaxParallel(state, arg) {
6551
6286
  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.`));
6552
- const limit = (0, import_core30.parseMaxParallel)(arg);
6287
+ const limit = (0, import_core31.parseMaxParallel)(arg);
6553
6288
  if (limit === null) return fail3(state, "Usage: /parallel [<number of tasks, 1 or more>]");
6554
6289
  return step({ ...state, maxParallel: limit }, [{ type: "setMaxParallel", limit }]);
6555
6290
  }
6556
6291
  function setAutonomous(state, arg) {
6557
- if (arg === void 0) return step(say(state, "system", `Autonomy level: ${(0, import_core30.autonomyLevelLabel)(state.autonomous)} \u2014 /auto full or /auto guarded changes it for new plans.`));
6558
- const enabled = (0, import_core30.parseAutonomyLevel)(arg);
6292
+ 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.`));
6293
+ const enabled = (0, import_core31.parseAutonomyLevel)(arg);
6559
6294
  if (enabled === null) return fail3(state, "Usage: /auto [full|guarded]");
6560
6295
  return step({ ...state, autonomous: enabled }, [{ type: "setAutonomous", enabled }]);
6561
6296
  }
6562
- function setTransport(state, arg) {
6563
- const value = arg?.toLowerCase() ?? (state.runnerTransport === "structured" ? "terminal" : "structured");
6564
- if (!(0, import_core30.isRunnerTransport)(value)) return fail3(state, "Usage: /transport [terminal|structured]");
6565
- return step(state, [{ type: "setTransport", transport: value }]);
6566
- }
6567
6297
  function setMouseCapture(state, arg) {
6568
6298
  const enabled = resolveToggle(arg, state.mouseCapture);
6569
6299
  if (enabled === null) return fail3(state, "Usage: /mouse [on|off]");
@@ -6738,9 +6468,14 @@ function choose(state, picker2, item) {
6738
6468
  sessionId: state.sessionId,
6739
6469
  taskId: task.id,
6740
6470
  changes: { dependencies: picker2.chosen },
6741
- message: picker2.chosen.length > 0 ? `Task ${(0, import_core31.taskRef)(task)} now depends on ${picker2.chosen.length} task${picker2.chosen.length === 1 ? "" : "s"}.` : `Task ${(0, import_core31.taskRef)(task)} no longer depends on anything.`
6471
+ 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.`
6742
6472
  }]);
6743
6473
  }
6474
+ if (picker2.action.kind === "set-task-skills") {
6475
+ const task = findTask(state.tasks, picker2.action.taskId);
6476
+ if (!task || !state.sessionId) return step(closed);
6477
+ return assignTaskSkills(closed, state.sessionId, task, picker2.chosen);
6478
+ }
6744
6479
  if (!item) return step(state);
6745
6480
  if (item.disabled) {
6746
6481
  return step(say(state, "error", item.detail ? `${item.label} \u2014 ${item.detail}` : `${item.label} is not available.`));
@@ -6815,7 +6550,7 @@ function choose(state, picker2, item) {
6815
6550
  }
6816
6551
 
6817
6552
  // src/tui/reducers/incoming.ts
6818
- var import_core32 = require("@ordewell/core");
6553
+ var import_core33 = require("@ordewell/core");
6819
6554
  var planText = (value, fallback = "") => sanitize(String(value ?? fallback));
6820
6555
  var planLabel = (value, fallback = "") => planText(value, fallback).replace(/\n+/g, " ");
6821
6556
  var asRecord = (value) => typeof value === "object" && value !== null ? value : {};
@@ -6846,7 +6581,7 @@ function toTaskView2(t, index) {
6846
6581
  prompt: typeof t.prompt === "string" ? sanitize(t.prompt) : void 0,
6847
6582
  type: t.type === "user" ? "user" : "ai",
6848
6583
  status: planLabel(t.status, "pending"),
6849
- awaitingReason: t.status === "awaiting_user" && (0, import_core32.isAwaitingReason)(t.awaitingReason) ? t.awaitingReason : void 0,
6584
+ awaitingReason: t.status === "awaiting_user" && (0, import_core33.isAwaitingReason)(t.awaitingReason) ? t.awaitingReason : void 0,
6850
6585
  dependencies: Array.isArray(t.dependencies) ? t.dependencies.map((id) => planLabel(id)) : [],
6851
6586
  assignedRunner: typeof t.assignedRunner === "string" ? planLabel(t.assignedRunner) : void 0,
6852
6587
  taskMode: typeof t.taskMode === "string" ? planLabel(t.taskMode) : void 0,
@@ -6858,33 +6593,22 @@ function toTaskView2(t, index) {
6858
6593
  } : void 0,
6859
6594
  subtasks: Array.isArray(t.subtasks) ? [...t.subtasks].sort((a, b) => wireOrder(a) - wireOrder(b)).map((s, i) => toTaskView2(asRecord(s), i)) : void 0,
6860
6595
  ...t.ops === true ? { ops: true } : {},
6596
+ ...Array.isArray(t.skills) && t.skills.length > 0 ? { skills: t.skills.map((name) => planLabel(name)) } : {},
6861
6597
  ...Array.isArray(t.forcedPastGate) && t.forcedPastGate.length > 0 ? { forcedPastGate: t.forcedPastGate.map((title) => planLabel(title)) } : {}
6862
6598
  };
6863
6599
  }
6864
- var enabledFlag = (value) => typeof value?.enabled === "boolean" ? value.enabled : void 0;
6865
6600
  function applySettings(state, settings) {
6866
- const sources = {
6867
- tdd: settings.tdd,
6868
- verify: settings.verification
6869
- };
6870
- const skills = { ...state.skills };
6871
- for (const id of SKILL_IDS) {
6872
- const flag2 = enabledFlag(sources[id]);
6873
- if (flag2 !== void 0) skills[id] = flag2;
6874
- }
6875
6601
  return {
6876
6602
  ...state,
6877
- skills,
6878
6603
  orchestratorModel: typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : state.orchestratorModel,
6879
6604
  plannerProvider: typeof settings.aiProvider === "string" ? settings.aiProvider : state.plannerProvider,
6880
6605
  plannerEffort: typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : state.plannerEffort,
6881
6606
  maxParallel: typeof settings.maxParallel === "number" ? settings.maxParallel : state.maxParallel,
6882
- runnerTransport: (0, import_core32.isRunnerTransport)(settings.runnerTransport) ? settings.runnerTransport : state.runnerTransport,
6883
6607
  allowlist: settings.modelAllowlist && typeof settings.modelAllowlist === "object" ? settings.modelAllowlist : state.allowlist
6884
6608
  };
6885
6609
  }
6886
6610
  function runLabel(tasks, gate = null) {
6887
- const running = tasks.filter(import_core23.isTaskRunning);
6611
+ const running = tasks.filter(import_core24.isTaskRunning);
6888
6612
  if (running.length > 0) {
6889
6613
  const [first] = running;
6890
6614
  const name = first.assignedRunner ? `${first.title} \xB7 ${first.assignedRunner}` : first.title;
@@ -6950,13 +6674,13 @@ function reduceAction(state, action) {
6950
6674
  goal: "",
6951
6675
  taskView: null,
6952
6676
  pendingApprovals: [],
6953
- queuedPrompts: import_core33.EMPTY_HOLD,
6677
+ queuedPrompts: import_core34.EMPTY_HOLD,
6954
6678
  stopArmed: false,
6955
6679
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
6956
6680
  });
6957
6681
  case "chatRestored":
6958
6682
  if (stale(state, action.sessionId)) return step(state);
6959
- return step({ ...state, conversation: (0, import_core33.fromTranscript)(action.history, action.researchLog, action.plannerUsage), scroll: 0 });
6683
+ return step({ ...state, conversation: (0, import_core34.fromTranscript)(action.history, action.researchLog, action.plannerUsage), scroll: 0 });
6960
6684
  case "planUpdated": {
6961
6685
  if (stale(state, action.sessionId)) return step(state);
6962
6686
  const isolation = isolationOfPlan(action.plan);
@@ -7003,9 +6727,9 @@ function reduceAction(state, action) {
7003
6727
  case "taskStarted": {
7004
6728
  if (stale(state, action.sessionId)) return step(state);
7005
6729
  const existing = findTask(state.tasks, action.taskId);
7006
- if (existing && (0, import_core23.isTaskRunning)(existing)) return step(state);
7007
- const tasks = state.tasks.map((t) => t.id === action.taskId && !(0, import_core23.isTaskRunning)(t) ? { ...t, status: "in_progress" } : t);
7008
- const spoken = say(state, "system", (0, import_core33.taskStartedNotice)(action.title, action.runner));
6730
+ if (existing && (0, import_core24.isTaskRunning)(existing)) return step(state);
6731
+ const tasks = state.tasks.map((t) => t.id === action.taskId && !(0, import_core24.isTaskRunning)(t) ? { ...t, status: "in_progress" } : t);
6732
+ const spoken = say(state, "system", (0, import_core34.taskStartedNotice)(action.title, action.runner));
7009
6733
  return step({ ...spoken, tasks, status: "executing", busyLabel: runLabel(tasks) });
7010
6734
  }
7011
6735
  case "taskCheckpoint":
@@ -7026,16 +6750,15 @@ function reduceAction(state, action) {
7026
6750
  if (update === void 0) return t;
7027
6751
  const idleSince = update.idleSince ?? null;
7028
6752
  const isolation = update.isolation ?? t.isolation;
7029
- const transport = update.transport;
7030
6753
  const awaitingReason = update.awaitingReason;
7031
6754
  const checkpoint = update.checkpoint;
7032
6755
  const continuable = update.continuable === true;
7033
6756
  const awaitingApproval = update.awaitingApproval;
7034
6757
  const mergeGate = update.mergeGate;
7035
6758
  const forcedPastGate = update.forcedPastGate;
7036
- if (update.status !== t.status || idleSince !== (t.idleSince ?? null) || !sameIsolation(isolation, t.isolation) || !sameTransport(transport, t.transport) || awaitingReason !== t.awaitingReason || checkpoint !== t.checkpoint || continuable !== (t.continuable ?? false) || awaitingApproval !== t.awaitingApproval || !sameList(mergeGate, t.mergeGate) || !sameList(forcedPastGate, t.forcedPastGate)) {
6759
+ if (update.status !== t.status || idleSince !== (t.idleSince ?? null) || !sameIsolation(isolation, t.isolation) || awaitingReason !== t.awaitingReason || checkpoint !== t.checkpoint || continuable !== (t.continuable ?? false) || awaitingApproval !== t.awaitingApproval || !sameList(mergeGate, t.mergeGate) || !sameList(forcedPastGate, t.forcedPastGate)) {
7037
6760
  changed = true;
7038
- return { ...t, status: update.status, idleSince, isolation, transport, awaitingReason, checkpoint, continuable, awaitingApproval, mergeGate, forcedPastGate };
6761
+ return { ...t, status: update.status, idleSince, isolation, awaitingReason, checkpoint, continuable, awaitingApproval, mergeGate, forcedPastGate };
7039
6762
  }
7040
6763
  return t;
7041
6764
  });
@@ -7046,12 +6769,6 @@ function reduceAction(state, action) {
7046
6769
  const status = runStatus(next, tasks);
7047
6770
  return step({ ...next, status, busyLabel: status === "executing" ? runLabel(tasks, gate) : state.busyLabel });
7048
6771
  }
7049
- case "queueReady": {
7050
- if (stale(state, action.sessionId)) return step(state);
7051
- const sessionId = action.sessionId ?? state.sessionId;
7052
- if (sessionId === null) return step(state);
7053
- return step(state, [{ type: "processQueued", sessionId }]);
7054
- }
7055
6772
  case "isolationBlocked": {
7056
6773
  if (stale(state, action.sessionId)) return step(state);
7057
6774
  return step(blockedPicker(state, action.message, action.repos));
@@ -7084,6 +6801,14 @@ function reduceAction(state, action) {
7084
6801
  busyLabel: ""
7085
6802
  });
7086
6803
  }
6804
+ case "executionLost": {
6805
+ if (stale(state, action.sessionId)) return step(state);
6806
+ return step({
6807
+ ...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."),
6808
+ status: "idle",
6809
+ busyLabel: ""
6810
+ });
6811
+ }
7087
6812
  case "settingsLoaded":
7088
6813
  return step(applySettings(state, action.settings));
7089
6814
  case "modelsLoaded":
@@ -7118,7 +6843,7 @@ function reduceAction(state, action) {
7118
6843
  busyLabel: "",
7119
6844
  planApproved: false,
7120
6845
  pendingApprovals: [],
7121
- queuedPrompts: import_core33.EMPTY_HOLD,
6846
+ queuedPrompts: import_core34.EMPTY_HOLD,
7122
6847
  stopArmed: false,
7123
6848
  overlay: state.overlay?.kind === "approval" ? null : state.overlay
7124
6849
  });
@@ -7174,9 +6899,6 @@ function reduceAction(state, action) {
7174
6899
  return step(disarmStop(state));
7175
6900
  }
7176
6901
  }
7177
- function sameTransport(a, b) {
7178
- return a?.kind === b?.kind && a?.fallback === b?.fallback;
7179
- }
7180
6902
  function sameList(a, b) {
7181
6903
  return (a ?? []).join("\0") === (b ?? []).join("\0");
7182
6904
  }
@@ -7185,7 +6907,7 @@ function sameGate(a, b) {
7185
6907
  return a.paused === b.paused && JSON.stringify(a.handoff) === JSON.stringify(b.handoff);
7186
6908
  }
7187
6909
  function runStatus(state, tasks) {
7188
- if (tasks.some(import_core23.isTaskRunning)) return "executing";
6910
+ if (tasks.some(import_core24.isTaskRunning)) return "executing";
7189
6911
  if (tasks.some((t) => t.status === "awaiting_user") || state.gate?.paused) return "executing";
7190
6912
  return plannerInFlight(state) ? state.status : "idle";
7191
6913
  }
@@ -7246,7 +6968,11 @@ function submit(state) {
7246
6968
  if (!text) return step(state);
7247
6969
  const cleared = { ...state, editor: commit(state.editor) };
7248
6970
  const command = parseSlash(text);
7249
- if (command && findCommand(command.name)?.source !== "skill") return runCommand(cleared, command);
6971
+ if (command) {
6972
+ const known = findCommand(command.name);
6973
+ if (known && known.source !== "skill") return runCommand(cleared, command);
6974
+ if (state.taskView) return known ? skillInTaskView(cleared, command.name) : unknownCommand(cleared, command.name);
6975
+ }
7250
6976
  if (state.taskView) {
7251
6977
  if (!state.sessionId) return step(state);
7252
6978
  const taskId = state.taskView.taskId;
@@ -7254,7 +6980,7 @@ function submit(state) {
7254
6980
  return step(cleared, [{ type: "sendTaskMessage", sessionId: state.sessionId, taskId, text }]);
7255
6981
  }
7256
6982
  if (plannerInFlight(state)) {
7257
- return step({ ...cleared, queuedPrompts: (0, import_core33.holdPrompt)(state.queuedPrompts, text) });
6983
+ return step({ ...cleared, queuedPrompts: (0, import_core34.holdPrompt)(state.queuedPrompts, text) });
7258
6984
  }
7259
6985
  const spoken = say(cleared, "user", text);
7260
6986
  const effect = state.sessionId ? { type: "sendMessage", sessionId: state.sessionId, message: text } : { type: "startConversation", goal: text };
@@ -7313,17 +7039,18 @@ function createApp(deps) {
7313
7039
  }
7314
7040
 
7315
7041
  // src/tui/effects.ts
7316
- var import_child_process4 = require("child_process");
7317
- var import_core35 = require("@ordewell/core");
7042
+ var import_child_process3 = require("child_process");
7043
+ var import_core36 = require("@ordewell/core");
7318
7044
 
7319
7045
  // src/tui/inbound.ts
7320
- var import_core34 = require("@ordewell/core");
7046
+ var import_core35 = require("@ordewell/core");
7321
7047
  var CONVERSATION_TYPES = /* @__PURE__ */ new Set([
7322
7048
  "planner_message",
7323
7049
  "planner_turn_started",
7324
7050
  "planner_turn_ended",
7325
7051
  "planner_text_delta",
7326
7052
  "planner_thinking_delta",
7053
+ "planner_skill_loaded",
7327
7054
  "planner_text_retracted",
7328
7055
  "plan_token",
7329
7056
  "plan_generated",
@@ -7531,8 +7258,7 @@ function dispatchLifecycle(dispatch, event, sessionId) {
7531
7258
  status: String(task.status),
7532
7259
  idleSince: task.idleSince ?? null,
7533
7260
  isolation: task.isolation,
7534
- transport: task.transport,
7535
- awaitingReason: (0, import_core34.isAwaitingReason)(task.awaitingReason) ? task.awaitingReason : void 0,
7261
+ awaitingReason: (0, import_core35.isAwaitingReason)(task.awaitingReason) ? task.awaitingReason : void 0,
7536
7262
  ...typeof task.checkpoint === "string" && task.checkpoint ? { checkpoint: sanitize(task.checkpoint) } : {},
7537
7263
  ...task.continuable ? { continuable: true } : {},
7538
7264
  awaitingApproval: typeof task.awaitingApproval === "number" && task.awaitingApproval > 0 ? task.awaitingApproval : void 0,
@@ -7558,13 +7284,6 @@ function dispatchLifecycle(dispatch, event, sessionId) {
7558
7284
  case "review_approved":
7559
7285
  dispatch({ type: "notice", message: "Plan approved." });
7560
7286
  return;
7561
- // The orchestrator paused fan-out because a structural edit is queued; drain
7562
- // it so the planner reconciles the plan and dependents resume spawning.
7563
- // Dropped on the floor, the queue suppresses every later tick() — which is
7564
- // exactly "tasks did not fan out when the dependent ones finished."
7565
- case "queue_ready":
7566
- dispatch({ type: "queueReady", sessionId });
7567
- return;
7568
7287
  case "execution_complete":
7569
7288
  dispatch({ type: "executionComplete", summary: event.summary, sessionId });
7570
7289
  return;
@@ -7614,8 +7333,7 @@ async function runEffect(effect, deps) {
7614
7333
  await perform(effect, deps);
7615
7334
  } catch (err) {
7616
7335
  if (!isConnectionRefused(err)) {
7617
- const message = err instanceof Error ? err.message : String(err);
7618
- deps.dispatch({ type: "failed", message: explain(message) });
7336
+ deps.dispatch({ type: "failed", message: explain(err) });
7619
7337
  return;
7620
7338
  }
7621
7339
  let revived = false;
@@ -7632,21 +7350,20 @@ async function runEffect(effect, deps) {
7632
7350
  try {
7633
7351
  await perform(effect, deps);
7634
7352
  } catch (retryErr) {
7635
- const message = retryErr instanceof Error ? retryErr.message : String(retryErr);
7636
7353
  deps.dispatch({
7637
7354
  type: "failed",
7638
7355
  // A fresh daemon holds no sessions, so the retry of anything
7639
7356
  // session-scoped legitimately 404s. Say which of the two happened.
7640
- message: isConnectionRefused(retryErr) ? describeConnectionRefused(deps.port) : explain(message)
7357
+ message: isConnectionRefused(retryErr) ? describeConnectionRefused(deps.port) : explain(retryErr)
7641
7358
  });
7642
7359
  }
7643
7360
  }
7644
7361
  }
7645
- function explain(message) {
7646
- if (message === "Session not found") {
7362
+ function explain(err) {
7363
+ if (err instanceof DaemonError && err.code === "session_not_found") {
7647
7364
  return "This server is no longer holding that session \u2014 it was probably restarted. Reload it with /sessions.";
7648
7365
  }
7649
- return message;
7366
+ return err instanceof Error ? err.message : String(err);
7650
7367
  }
7651
7368
  function persistAfterDaemon(deps, env) {
7652
7369
  for (const [key, value] of Object.entries(env)) deps.setEnvVar(key, value);
@@ -7702,19 +7419,15 @@ async function perform(effect, deps) {
7702
7419
  timer.unref?.();
7703
7420
  return;
7704
7421
  }
7705
- case "processQueued":
7706
- await api.processQueued(effect.sessionId);
7707
- await refreshPlan(deps, effect.sessionId);
7708
- return;
7709
7422
  case "taskAction": {
7710
7423
  const { sessionId, taskId, action } = effect;
7711
- const request = action === "cancel" || action === "retry" || action === "force-start" ? () => api.taskControl(sessionId, taskId, action) : (0, import_core35.markRequestFor)(action) === "uncomplete" ? () => api.markTaskIncomplete(sessionId, taskId) : () => api.markTaskComplete(sessionId, taskId);
7424
+ 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);
7712
7425
  await (effect.watch ? withExecutionStream(deps, sessionId, request) : request());
7713
7426
  await refreshPlan(deps, sessionId);
7714
7427
  return;
7715
7428
  }
7716
7429
  case "addTask": {
7717
- const fields = (0, import_core35.newTaskFields)(effect.title);
7430
+ const fields = (0, import_core36.newTaskFields)(effect.title);
7718
7431
  if (!fields) return;
7719
7432
  await api.addTask(effect.sessionId, { ...fields });
7720
7433
  await refreshPlan(deps, effect.sessionId);
@@ -7753,18 +7466,6 @@ async function perform(effect, deps) {
7753
7466
  if (effect.answer === "approve") await api.approveTaskCheckpoint(effect.sessionId, effect.taskId);
7754
7467
  else await api.rejectTaskCheckpoint(effect.sessionId, effect.taskId, effect.reason);
7755
7468
  return;
7756
- // A task with a saved log ran structured, whatever its `transport` says
7757
- // after a reload; it has no tmux window, so open the log rather than fail.
7758
- case "openTaskTerminal": {
7759
- const saved = await api.getTaskLogAttempts(effect.sessionId, effect.taskId, workspace).catch(() => []);
7760
- if (saved.length > 0) {
7761
- dispatch({ type: "taskViewRequested", sessionId: effect.sessionId, taskId: effect.taskId });
7762
- return;
7763
- }
7764
- const result = await deps.openTerminal(effect.sessionId, effect.taskId);
7765
- dispatch(result.ok ? { type: "notice", message: result.message } : { type: "failed", message: result.message });
7766
- return;
7767
- }
7768
7469
  // The saved log is read off disk (ADR-0018, P1) so the view opens with the
7769
7470
  // task's history; the live `task_log` stream then catches it up in place.
7770
7471
  case "openTaskLog": {
@@ -7816,18 +7517,6 @@ async function perform(effect, deps) {
7816
7517
  await api.interruptTask(effect.sessionId, effect.taskId);
7817
7518
  dispatch({ type: "notice", message: "Interrupting the task\u2026" });
7818
7519
  return;
7819
- case "command": {
7820
- const result = await api.sendCommand(effect.name, { action: effect.action });
7821
- if (result.settings) dispatch({ type: "settingsLoaded", settings: result.settings });
7822
- dispatch({ type: "notice", message: `${effect.name} is ${effect.action}.` });
7823
- return;
7824
- }
7825
- case "setTransport": {
7826
- const result = await api.sendCommand("transport", { action: effect.transport });
7827
- if (result.settings) dispatch({ type: "settingsLoaded", settings: result.settings });
7828
- dispatch({ type: "notice", message: `Runner transport is ${effect.transport} \u2014 it applies from the next run.` });
7829
- return;
7830
- }
7831
7520
  case "setModel":
7832
7521
  await api.updateSettings({ orchestratorModel: effect.modelId });
7833
7522
  deps.setEnvVar("ORCHESTRATOR_MODEL", effect.modelId);
@@ -7835,11 +7524,11 @@ async function perform(effect, deps) {
7835
7524
  dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
7836
7525
  return;
7837
7526
  case "setPlanner": {
7838
- const meta = import_core35.ALL_PROVIDERS[effect.provider];
7527
+ const meta = import_core36.ALL_PROVIDERS[effect.provider];
7839
7528
  if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
7840
7529
  const env = { AI_PROVIDER: effect.provider };
7841
7530
  const settings = await api.updateSettings({ env });
7842
- const recall2 = describePlannerSwitch(settings, effect.provider);
7531
+ const recall2 = plannerSwitchRecall(settings);
7843
7532
  persistAfterDaemon(deps, { ...env, ORCHESTRATOR_MODEL: recall2.model, ORDEWELL_PLANNER_EFFORT: recall2.effort });
7844
7533
  dispatch({
7845
7534
  type: "settingsLoaded",
@@ -7847,7 +7536,7 @@ async function perform(effect, deps) {
7847
7536
  });
7848
7537
  dispatch({
7849
7538
  type: "notice",
7850
- message: `${(0, import_core35.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
7539
+ message: `${(0, import_core36.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
7851
7540
  });
7852
7541
  await loadModels(deps);
7853
7542
  return;
@@ -7859,7 +7548,7 @@ async function perform(effect, deps) {
7859
7548
  dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
7860
7549
  return;
7861
7550
  case "setApiKey": {
7862
- const meta = import_core35.ALL_PROVIDERS[effect.provider];
7551
+ const meta = import_core36.ALL_PROVIDERS[effect.provider];
7863
7552
  if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
7864
7553
  await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
7865
7554
  deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
@@ -7897,7 +7586,7 @@ async function perform(effect, deps) {
7897
7586
  return;
7898
7587
  case "setAutonomous":
7899
7588
  deps.setEnvVar("ORDEWELL_AUTONOMOUS_MODE", String(effect.enabled));
7900
- dispatch({ type: "notice", message: `Autonomy level: ${(0, import_core35.autonomyLevelLabel)(effect.enabled)} for new plans.` });
7589
+ dispatch({ type: "notice", message: `Autonomy level: ${(0, import_core36.autonomyLevelLabel)(effect.enabled)} for new plans.` });
7901
7590
  return;
7902
7591
  case "setMouseCapture":
7903
7592
  deps.setMouseCapture(effect.enabled);
@@ -8023,7 +7712,7 @@ async function perform(effect, deps) {
8023
7712
  }
8024
7713
  }
8025
7714
  var pipeToClipboardDefault = (command, text) => {
8026
- (0, import_child_process4.execSync)(command, { input: text, stdio: ["pipe", "ignore", "ignore"] });
7715
+ (0, import_child_process3.execSync)(command, { input: text, stdio: ["pipe", "ignore", "ignore"] });
8027
7716
  };
8028
7717
  function osc52(text) {
8029
7718
  return `\x1B]52;c;${Buffer.from(text, "utf8").toString("base64")}\x07`;
@@ -8031,7 +7720,7 @@ function osc52(text) {
8031
7720
  function copySelection(deps, text) {
8032
7721
  if (!text) return;
8033
7722
  const lines = text.split("\n").length;
8034
- const command = (0, import_core35.clipboardCopyCommand)(deps.hasBin);
7723
+ const command = (0, import_core36.clipboardCopyCommand)(deps.hasBin);
8035
7724
  if (command) {
8036
7725
  try {
8037
7726
  (deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
@@ -8049,15 +7738,20 @@ function copySelection(deps, text) {
8049
7738
  async function withExecutionStream(deps, sessionId, request) {
8050
7739
  let settleReady = () => {
8051
7740
  };
8052
- const streamReady = new Promise((resolve3, reject) => {
8053
- settleReady = (error) => error ? reject(error) : resolve3();
7741
+ const streamReady = new Promise((resolve4, reject) => {
7742
+ settleReady = (error) => error ? reject(error) : resolve4();
8054
7743
  });
8055
7744
  const inbound = inboundFor(deps.api, deps.dispatch, sessionId);
8056
7745
  const stream = deps.api.streamExecution(sessionId, inbound.execution(), settleReady);
8057
7746
  void stream.catch(settleReady);
8058
- await streamReady;
8059
- await request();
8060
- await stream;
7747
+ try {
7748
+ await streamReady;
7749
+ await request();
7750
+ } catch (err) {
7751
+ deps.api.closeExecutionStream(stream);
7752
+ throw err;
7753
+ }
7754
+ if (await stream === "lost") deps.dispatch({ type: "executionLost", sessionId });
8061
7755
  }
8062
7756
  async function converse(deps, sessionId, call) {
8063
7757
  const inbound = inboundFor(deps.api, deps.dispatch, sessionId);
@@ -8087,7 +7781,7 @@ function restoredChat(plan, sessionId) {
8087
7781
  function lastAssistantMessage(plan) {
8088
7782
  const history = plan?.conversationHistory ?? [];
8089
7783
  for (let i = history.length - 1; i >= 0; i--) {
8090
- if (history[i].role === "assistant") {
7784
+ if (history[i].role === "assistant" && history[i].kind !== "skill_load") {
8091
7785
  const { content, timestamp } = history[i];
8092
7786
  return { content, ...timestamp ? { timestamp } : {} };
8093
7787
  }
@@ -8422,7 +8116,7 @@ async function handleTui(subArgs) {
8422
8116
  }
8423
8117
  const workspace = flag(subArgs, "--workspace") || process.cwd();
8424
8118
  try {
8425
- (0, import_core36.assertWorkspaceExists)(workspace);
8119
+ (0, import_core37.assertWorkspaceExists)(workspace);
8426
8120
  } catch {
8427
8121
  console.error(
8428
8122
  `Workspace "${workspace}" does not exist.
@@ -8431,7 +8125,9 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
8431
8125
  );
8432
8126
  process.exit(1);
8433
8127
  }
8434
- registerSkillCommands((0, import_core36.createSkillsService)(workspace).listSkills());
8128
+ const discoveredSkills = (0, import_core37.createSkillsService)(workspace, new import_core37.EnvConfig().workspaceRepos).listSkills();
8129
+ registerSkillCommands(discoveredSkills);
8130
+ const taskSkills = discoveredSkills.filter((s) => s.appliesTo === "task").map((s) => ({ name: s.name, description: s.description }));
8435
8131
  const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
8436
8132
  const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
8437
8133
  const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
@@ -8441,19 +8137,18 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
8441
8137
  const autoMode = process.env.ORDEWELL_AUTONOMOUS_MODE;
8442
8138
  const mouseCapture = process.env.ORDEWELL_TUI_MOUSE !== "false" && process.env.ORDEWELL_TUI_MOUSE !== "0";
8443
8139
  const app = createApp({
8444
- initial: { workspace, autonomous: autoMode !== "false" && autoMode !== "0", mouseCapture },
8140
+ initial: { workspace, autonomous: autoMode !== "false" && autoMode !== "0", mouseCapture, taskSkills },
8445
8141
  draw: (frame2) => terminal?.draw(frame2),
8446
8142
  perform: (effect) => runEffect(effect, {
8447
8143
  api,
8448
8144
  workspace,
8449
8145
  port,
8450
8146
  dispatch: (action) => app.dispatch(action),
8451
- newSessionId: () => (0, import_core36.mintSessionId)(),
8147
+ newSessionId: () => (0, import_core37.mintSessionId)(),
8452
8148
  setEnvVar: (key, value) => {
8453
8149
  writeEnvVar(findEnvFile(), key, value);
8454
8150
  process.env[key] = value;
8455
8151
  },
8456
- openTerminal: (sessionId, taskId) => openTaskTerminal(port, sessionId, taskId),
8457
8152
  setMouseCapture: (enabled) => terminal?.setMouse(enabled),
8458
8153
  // The alt screen is up with the cursor hidden, so an OSC 52 write shows
8459
8154
  // nothing — the terminal reads it and answers by taking the clipboard.
@@ -8467,7 +8162,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
8467
8162
  }),
8468
8163
  onExit: () => shutdown(0),
8469
8164
  // The same pointer `ordewell plan` leaves, so `ordewell handoff`,
8470
- // `terminal <n>` and the other one-shot commands act on the session this
8165
+ // The one-shot commands act on the session this
8471
8166
  // TUI is showing instead of saying there is none.
8472
8167
  onSessionChange: (sessionId, state) => {
8473
8168
  try {
@@ -8499,6 +8194,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
8499
8194
  app.dispatch({ type: "resize", rows: r, cols: c });
8500
8195
  });
8501
8196
  process.on("uncaughtException", (err) => {
8197
+ terminal?.close();
8502
8198
  console.error(`Fatal: ${err.message}`);
8503
8199
  shutdown(1);
8504
8200
  });
@@ -8524,12 +8220,9 @@ var COMMANDS = {
8524
8220
  key: handleKey,
8525
8221
  runners: handleRunners,
8526
8222
  auto: handleAuto,
8527
- transport: handleTransport,
8528
8223
  refresh: handleRefresh,
8529
8224
  setup: handleSetup,
8530
- plugins: handlePlugins,
8531
- "tdd": handleTdd,
8532
- "verify": handleVerify,
8225
+ skills: handleSkills,
8533
8226
  "mark-complete": handleMarkComplete,
8534
8227
  complete: handleMarkComplete,
8535
8228
  uncomplete: handleUncomplete,
@@ -8547,8 +8240,8 @@ var COMMANDS = {
8547
8240
  "task-effort": handleTaskEffort,
8548
8241
  "task-mode": handleTaskMode,
8549
8242
  "task-deps": handleTaskDeps,
8243
+ "task-skills": handleTaskSkills,
8550
8244
  "task-ops": handleTaskOps,
8551
- terminal: handleTerminal,
8552
8245
  "sessions": handleSessions,
8553
8246
  fork: handleFork,
8554
8247
  compact: handleCompact,
@@ -8559,10 +8252,10 @@ var COMMANDS = {
8559
8252
 
8560
8253
  // src/version.ts
8561
8254
  var import_fs7 = require("fs");
8562
- var import_path5 = require("path");
8255
+ var import_path6 = require("path");
8563
8256
  function cliVersion() {
8564
8257
  try {
8565
- const pkg = JSON.parse((0, import_fs7.readFileSync)((0, import_path5.join)(__dirname, "..", "package.json"), "utf8"));
8258
+ const pkg = JSON.parse((0, import_fs7.readFileSync)((0, import_path6.join)(__dirname, "..", "package.json"), "utf8"));
8566
8259
  const version = pkg.version;
8567
8260
  return typeof version === "string" && version ? version : "unknown";
8568
8261
  } catch {
@@ -8573,7 +8266,7 @@ function cliVersion() {
8573
8266
  // src/main.ts
8574
8267
  var TUI_FLAGS = /* @__PURE__ */ new Set(["--workspace", "--port"]);
8575
8268
  async function main() {
8576
- (0, import_core37.migrateOldConfigDir)();
8269
+ (0, import_core38.migrateOldConfigDir)();
8577
8270
  loadEnvFile();
8578
8271
  const argv = process.argv.slice(2);
8579
8272
  if (TUI_FLAGS.has(argv[0] ?? "")) argv.unshift("tui");