@ordewell/cli 0.4.10 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +334 -292
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -23,12 +23,15 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  mod
24
24
  ));
25
25
 
26
+ // src/main.ts
27
+ var import_core18 = require("@ordewell/core");
28
+
26
29
  // src/utils/env.ts
27
30
  var import_fs = require("fs");
28
31
  var import_path = require("path");
29
- var import_os = require("os");
32
+ var import_core = require("@ordewell/core");
30
33
  function findEnvFile() {
31
- return (0, import_path.join)((0, import_os.homedir)(), ".config", "ordewell", ".env");
34
+ return (0, import_path.join)((0, import_core.globalDataDir)(), ".env");
32
35
  }
33
36
  function loadEnvFile() {
34
37
  const filePath = findEnvFile();
@@ -83,7 +86,7 @@ an id.
83
86
  Planning:
84
87
  ordewell Full-screen terminal UI (everything the VS Code extension does)
85
88
  ordewell tui The same thing, named \u2014 for scripts and aliases
86
- ordewell plan --goal "text" Plan a goal conversationally (honours grill-me/PRD/review)
89
+ ordewell plan --goal "text" Plan a goal conversationally (honours verify)
87
90
  ordewell run Execute the last generated plan (streams status)
88
91
  ordewell approve Sign off a plan paused for review, and continue it
89
92
  ordewell stop Stop execution of the last session (or --server)
@@ -121,12 +124,8 @@ Planner, models and runners:
121
124
  ordewell models List every provider's catalog (works without a server)
122
125
 
123
126
  Modes:
124
- ordewell grill-me [on|off] Toggle Grill-Me challenge mode (or show status)
125
127
  ordewell tdd [on|off] Toggle Test-Driven Development mode (or show status)
126
- ordewell prd [on|off] Toggle PRD mode (planner writes a PRD before the plan)
127
- ordewell review [on|off] Toggle review mode \u2014 adds a final opinion-review task, human sign-off (or show status)
128
128
  ordewell verify [on|off] Toggle verification mode \u2014 adds a final evidence-based task that runs the full suite (or show status)
129
- ordewell research-subagents [on|off] Toggle parallel read-only research subagents during planning (or show status)
130
129
 
131
130
  Other:
132
131
  ordewell web Start the API server (foreground; --daemon for background)
@@ -202,16 +201,21 @@ Environment:
202
201
  // src/daemon.ts
203
202
  var import_child_process = require("child_process");
204
203
  var import_fs2 = require("fs");
205
- var import_os2 = require("os");
204
+ var import_os = require("os");
206
205
  var import_path2 = require("path");
207
206
  var import_http = __toESM(require("http"));
208
- var import_core = require("@ordewell/core");
207
+ var import_net = require("net");
208
+ var import_core2 = require("@ordewell/core");
209
209
  var DEFAULT_PORT = 3742;
210
- var CONFIG_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".config", "ordewell");
210
+ var CONFIG_DIR = (0, import_core2.globalDataDir)();
211
211
  var LEGACY_PID_FILE = (0, import_path2.join)(CONFIG_DIR, "server.pid");
212
+ var OLD_CONFIG_DIR = (0, import_path2.join)((0, import_os.homedir)(), ".config", "ordewell");
212
213
  function pidFileFor(port) {
213
214
  return port === DEFAULT_PORT ? LEGACY_PID_FILE : (0, import_path2.join)(CONFIG_DIR, `server-${port}.pid`);
214
215
  }
216
+ function oldPidFileFor(port) {
217
+ return port === DEFAULT_PORT ? (0, import_path2.join)(OLD_CONFIG_DIR, "server.pid") : (0, import_path2.join)(OLD_CONFIG_DIR, `server-${port}.pid`);
218
+ }
215
219
  function httpGet(url, timeoutMs = 2e3, headers = {}) {
216
220
  return new Promise((resolve3, reject) => {
217
221
  const req = import_http.default.get(url, { timeout: timeoutMs, headers }, (res) => {
@@ -243,12 +247,12 @@ function getWebServerPath() {
243
247
  );
244
248
  }
245
249
  async function isDaemonRunning(port = DEFAULT_PORT) {
246
- const token = (0, import_core.readDaemonToken)(port);
250
+ const token = (0, import_core2.readDaemonToken)(port);
247
251
  try {
248
252
  const res = await httpGet(
249
253
  `http://127.0.0.1:${port}/api/workspaces`,
250
254
  2e3,
251
- token ? { Authorization: (0, import_core.bearerHeaderValue)(token) } : {}
255
+ token ? { Authorization: (0, import_core2.bearerHeaderValue)(token) } : {}
252
256
  );
253
257
  return res.status === 200 || res.status === 401;
254
258
  } catch {
@@ -268,7 +272,12 @@ async function startDaemon(port = DEFAULT_PORT, opts) {
268
272
  }
269
273
  const logFd = (0, import_fs2.openSync)((0, import_path2.join)(CONFIG_DIR, "server.log"), "a");
270
274
  const detached = opts?.detached ?? true;
271
- const child = (0, import_child_process.spawn)(process.execPath, [serverPath, "--port", String(port)], {
275
+ const child = (0, import_child_process.spawn)(process.execPath, [
276
+ serverPath,
277
+ "--port",
278
+ String(port),
279
+ ...detached ? [] : ["--watch-parent", String(process.pid)]
280
+ ], {
272
281
  detached,
273
282
  stdio: ["ignore", logFd, logFd],
274
283
  env: { ...process.env }
@@ -288,7 +297,8 @@ async function startDaemon(port = DEFAULT_PORT, opts) {
288
297
  process.exit(1);
289
298
  }
290
299
  async function stopDaemon(port = DEFAULT_PORT) {
291
- const pidFile = pidFileFor(port);
300
+ let pidFile = pidFileFor(port);
301
+ if (!(0, import_fs2.existsSync)(pidFile) && (0, import_fs2.existsSync)(oldPidFileFor(port))) pidFile = oldPidFileFor(port);
292
302
  if (!(0, import_fs2.existsSync)(pidFile)) {
293
303
  console.error(`No server PID file found for port ${port}. Is the server running?`);
294
304
  return false;
@@ -298,7 +308,7 @@ async function stopDaemon(port = DEFAULT_PORT) {
298
308
  const pid = parseInt(pidStr, 10);
299
309
  if (!pid || isNaN(pid)) {
300
310
  (0, import_fs2.unlinkSync)(pidFile);
301
- (0, import_core.clearDaemonToken)(port);
311
+ (0, import_core2.clearDaemonToken)(port);
302
312
  return false;
303
313
  }
304
314
  process.kill(pid, "SIGTERM");
@@ -311,7 +321,7 @@ async function stopDaemon(port = DEFAULT_PORT) {
311
321
  (0, import_fs2.unlinkSync)(pidFile);
312
322
  } catch {
313
323
  }
314
- (0, import_core.clearDaemonToken)(port);
324
+ (0, import_core2.clearDaemonToken)(port);
315
325
  console.log("Server was not running (stale PID removed).");
316
326
  return true;
317
327
  }
@@ -338,6 +348,18 @@ function isConnectionRefused(err) {
338
348
  function describeConnectionRefused(port) {
339
349
  return `No Ordewell server is listening on port ${port}. It may have been stopped, or it may have crashed \u2014 check ${(0, import_path2.join)(CONFIG_DIR, "server.log")}. Start one with \`ordewell web --daemon\`.`;
340
350
  }
351
+ function findFreePort() {
352
+ return new Promise((resolvePromise, reject) => {
353
+ const srv = (0, import_net.createServer)();
354
+ srv.unref();
355
+ srv.on("error", reject);
356
+ srv.listen(0, "127.0.0.1", () => {
357
+ const address = srv.address();
358
+ const port = typeof address === "object" && address ? address.port : 0;
359
+ srv.close(() => resolvePromise(port));
360
+ });
361
+ });
362
+ }
341
363
  function resolvePort(args = []) {
342
364
  const idx = args.indexOf("--port");
343
365
  if (idx !== -1) {
@@ -352,7 +374,7 @@ function resolvePort(args = []) {
352
374
  // src/apiClient.ts
353
375
  var import_http2 = __toESM(require("http"));
354
376
  var import_ws = __toESM(require("ws"));
355
- var import_core2 = require("@ordewell/core");
377
+ var import_core3 = require("@ordewell/core");
356
378
  var DEFAULT_HTTP_TIMEOUT_MS = 15 * 60 * 1e3;
357
379
  var ApiClient = class {
358
380
  port;
@@ -365,7 +387,7 @@ var ApiClient = class {
365
387
  * A missing file is not an error here — the daemon's own 401 names the path.
366
388
  */
367
389
  token() {
368
- return (0, import_core2.readDaemonToken)(this.port);
390
+ return (0, import_core3.readDaemonToken)(this.port);
369
391
  }
370
392
  httpRequest(method, urlPath, body) {
371
393
  return new Promise((resolve3, reject) => {
@@ -379,7 +401,7 @@ var ApiClient = class {
379
401
  method,
380
402
  headers: {
381
403
  ...body ? { "Content-Type": "application/json" } : {},
382
- ...token ? { Authorization: (0, import_core2.bearerHeaderValue)(token) } : {}
404
+ ...token ? { Authorization: (0, import_core3.bearerHeaderValue)(token) } : {}
383
405
  },
384
406
  // Planner research on a real repository regularly exceeds two minutes.
385
407
  // The benchmark harness uses the same 15-minute default explicitly;
@@ -408,7 +430,7 @@ var ApiClient = class {
408
430
  req.end();
409
431
  });
410
432
  }
411
- async generatePlan(goal, runners2, workspace, sessionId = (0, import_core2.mintSessionId)()) {
433
+ async generatePlan(goal, runners2, workspace, sessionId = (0, import_core3.mintSessionId)()) {
412
434
  const res = await this.httpRequest("POST", `/api/plans/${sessionId}/generate`, {
413
435
  goal,
414
436
  runners: runners2 || void 0,
@@ -421,7 +443,7 @@ var ApiClient = class {
421
443
  }
422
444
  /**
423
445
  * Open the ADR-0002 planner dialogue. Unlike `generatePlan`, this path honours
424
- * the grill-me / PRD / review toggles, so the returned plan may be a question
446
+ * the verify toggle, so the returned plan may be a question
425
447
  * (empty `tasks`, last word in `conversationHistory`) rather than a committed plan.
426
448
  */
427
449
  async startConversation(sessionId, goal, runners2, workspace) {
@@ -609,7 +631,7 @@ var ApiClient = class {
609
631
  const token = this.token();
610
632
  const socket = new import_ws.default(
611
633
  `ws://127.0.0.1:${this.port}/ws/session/${sessionId}`,
612
- token ? (0, import_core2.tokenSubprotocols)(token) : void 0
634
+ token ? (0, import_core3.tokenSubprotocols)(token) : void 0
613
635
  );
614
636
  socket.on("unexpected-response", (_req, res) => {
615
637
  let body = "";
@@ -773,6 +795,7 @@ function positionals(args) {
773
795
  }
774
796
 
775
797
  // src/utils/tasks.ts
798
+ var import_core4 = require("@ordewell/core");
776
799
  function allTasksOf(plan) {
777
800
  const tasks = plan?.pendingTasks || plan?.tasks || [];
778
801
  const executionLog = plan?.executionLog || [];
@@ -780,6 +803,10 @@ function allTasksOf(plan) {
780
803
  }
781
804
  function resolveTaskId(plan, identifier) {
782
805
  const allTasks = allTasksOf(plan);
806
+ if (identifier.includes(".")) {
807
+ const subtask = (0, import_core4.resolveOrderLabel)(allTasks, identifier);
808
+ return subtask?.id;
809
+ }
783
810
  const order = parseInt(identifier, 10);
784
811
  if (!isNaN(order)) {
785
812
  const task = allTasks.find((t) => t.order === order);
@@ -795,20 +822,24 @@ function resolveTaskId(plan, identifier) {
795
822
  // src/utils/session.ts
796
823
  var import_fs3 = require("fs");
797
824
  var import_path3 = require("path");
798
- var import_os3 = require("os");
799
- var LAST_SESSION_FILE = (0, import_path3.join)((0, import_os3.homedir)(), ".config", "ordewell", "last-session.json");
825
+ var import_core5 = require("@ordewell/core");
826
+ var LAST_SESSION_FILENAME = "last-session.json";
827
+ function lastSessionFile(workspace) {
828
+ return (0, import_path3.join)((0, import_core5.getStateDir)(workspace), LAST_SESSION_FILENAME);
829
+ }
800
830
  function saveLastSession(sessionId, goal, runners2, workspace) {
801
- const dir = (0, import_path3.join)((0, import_os3.homedir)(), ".config", "ordewell");
831
+ const dir = (0, import_core5.getStateDir)(workspace);
802
832
  if (!(0, import_fs3.existsSync)(dir)) (0, import_fs3.mkdirSync)(dir, { recursive: true });
803
833
  (0, import_fs3.writeFileSync)(
804
- LAST_SESSION_FILE,
834
+ lastSessionFile(workspace),
805
835
  JSON.stringify({ sessionId, goal, runners: runners2, workspace, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
806
836
  );
807
837
  }
808
- function readLastSession() {
838
+ function readLastSession(workspace) {
809
839
  try {
810
- if ((0, import_fs3.existsSync)(LAST_SESSION_FILE)) {
811
- return JSON.parse((0, import_fs3.readFileSync)(LAST_SESSION_FILE, "utf8"));
840
+ const file = lastSessionFile(workspace);
841
+ if ((0, import_fs3.existsSync)(file)) {
842
+ return JSON.parse((0, import_fs3.readFileSync)(file, "utf8"));
812
843
  }
813
844
  } catch {
814
845
  }
@@ -914,7 +945,7 @@ function isTransient(event) {
914
945
  }
915
946
 
916
947
  // src/commands/plan.ts
917
- var import_core3 = require("@ordewell/core");
948
+ var import_core6 = require("@ordewell/core");
918
949
  function planTasks(plan) {
919
950
  return plan?.tasks || [];
920
951
  }
@@ -976,21 +1007,26 @@ function printPlan(plan, sessionId, runners2, models) {
976
1007
  const tasks = planTasks(plan);
977
1008
  const aiCount = tasks.filter((t) => t.type !== "user").length;
978
1009
  const manCount = tasks.filter((t) => t.type === "user").length;
1010
+ const modelSuffix = (task) => {
1011
+ if (!task.assignedModel) return "";
1012
+ const found = models.find((m) => m.modelId === task.assignedModel?.modelId);
1013
+ const rp = found?.runnerProvider;
1014
+ const label = rp ? rp.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ") : "";
1015
+ return rp ? ` (${task.assignedModel.modelLabel} \xB7 ${label})` : ` (${task.assignedModel.modelLabel})`;
1016
+ };
979
1017
  console.log(`
980
1018
  Plan: ${tasks.length} tasks (${aiCount} AI, ${manCount} Manual) \u2014 ${(plan.runners || runners2).join(", ")}`);
981
1019
  console.log(`Session: ${sessionId}
982
1020
  `);
983
1021
  for (const t of tasks) {
984
1022
  const typeIcon = t.type === "user" ? "[MAN]" : "[ AI]";
985
- let model = "";
986
- if (t.assignedModel) {
987
- const found = models.find((m) => m.modelId === t.assignedModel?.modelId);
988
- const rp = found?.runnerProvider;
989
- const label = rp ? rp.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ") : "";
990
- model = rp ? ` (${t.assignedModel.modelLabel} \xB7 ${label})` : ` (${t.assignedModel.modelLabel})`;
991
- }
992
1023
  const deps = t.dependencies?.length ? ` \u2190 ${t.dependencies.join(", ")}` : "";
993
- console.log(` ${String(t.order).padStart(2)}. ${typeIcon} ${t.title}${model}${deps}`);
1024
+ console.log(` ${(0, import_core6.taskOrderLabel)(t).padStart(2)}. ${typeIcon} ${t.title}${modelSuffix(t)}${deps}`);
1025
+ for (const sub of t.subtasks || []) {
1026
+ const subTypeIcon = sub.type === "user" ? "[MAN]" : "[ AI]";
1027
+ const subDeps = sub.dependencies?.length ? ` \u2190 ${sub.dependencies.join(", ")}` : "";
1028
+ console.log(` ${(0, import_core6.taskOrderLabel)(sub, t)}. ${subTypeIcon} ${sub.title}${modelSuffix(sub)}${subDeps}`);
1029
+ }
994
1030
  }
995
1031
  if (manCount > 0) {
996
1032
  console.log("\n [MAN] = manual step \u2014 run `ordewell tui` to work through it");
@@ -1036,7 +1072,7 @@ async function handlePlan(subArgs, deps = {}) {
1036
1072
  runners2 = runnerFlags;
1037
1073
  }
1038
1074
  console.error(`Generating plan for: "${goal}"...`);
1039
- const sessionId = (0, import_core3.mintSessionId)();
1075
+ const sessionId = (0, import_core6.mintSessionId)();
1040
1076
  const isTty = !!process.stderr.isTTY;
1041
1077
  let lastLineLength = 0;
1042
1078
  function renderStep(event) {
@@ -1148,9 +1184,10 @@ function iconFor(status) {
1148
1184
  function resolveSessionId(subArgs) {
1149
1185
  const explicit = flag(subArgs, "--session-id");
1150
1186
  if (explicit) return explicit;
1151
- const last = readLastSession();
1187
+ const workspace = flag(subArgs, "--workspace") || process.cwd();
1188
+ const last = readLastSession(workspace);
1152
1189
  if (!last) {
1153
- console.error("No session specified. Use --session-id <id> or run `ordewell plan` first.");
1190
+ console.error(`No session specified. Use --session-id <id> or run \`ordewell plan\` in ${workspace} first.`);
1154
1191
  process.exit(1);
1155
1192
  }
1156
1193
  console.error(`Using last session: ${last.sessionId} ("${last.goal}")`);
@@ -1224,51 +1261,29 @@ async function handleRun(subArgs) {
1224
1261
 
1225
1262
  // src/commands/status.ts
1226
1263
  var import_fs4 = require("fs");
1264
+ var import_core7 = require("@ordewell/core");
1227
1265
  async function handleStatus(subArgs, injectedApi) {
1228
1266
  const sessionId = flag(subArgs, "--session-id");
1229
- const explicitWorkspace = flag(subArgs, "--workspace");
1230
- let workspace = explicitWorkspace || process.cwd();
1267
+ const workspace = flag(subArgs, "--workspace") || process.cwd();
1231
1268
  const outputPath = flag(subArgs, "--output");
1232
1269
  const asJson = hasFlag(subArgs, "--json") || Boolean(outputPath);
1233
1270
  const port = injectedApi ? resolvePort(subArgs) : await ensureDaemon(resolvePort(subArgs));
1234
1271
  const api = injectedApi || new ApiClient(port);
1235
1272
  if (sessionId) {
1236
- let session;
1237
- try {
1238
- session = await api.getSession(sessionId, workspace);
1239
- } catch (err) {
1240
- const last = readLastSession();
1241
- if (!explicitWorkspace && last?.workspace && last.workspace !== workspace) {
1242
- session = await api.getSession(sessionId, last.workspace);
1243
- } else {
1244
- throw err;
1245
- }
1246
- }
1273
+ const session = await api.getSession(sessionId, workspace);
1247
1274
  if (asJson) {
1248
1275
  emitJson(session, outputPath);
1249
1276
  return;
1250
1277
  }
1251
1278
  printOneSession(sessionId, session.meta, session.plan);
1252
1279
  } else {
1253
- let sessions = await api.getSessions(workspace);
1254
- if (sessions.length === 0 && !explicitWorkspace) {
1255
- const last2 = readLastSession();
1256
- if (last2?.workspace && last2.workspace !== workspace) {
1257
- const fallback = await api.getSessions(last2.workspace);
1258
- if (fallback.length > 0) {
1259
- workspace = last2.workspace;
1260
- sessions = fallback;
1261
- console.log(`(no sessions in ${process.cwd()} \u2014 showing ${workspace})
1262
- `);
1263
- }
1264
- }
1265
- }
1280
+ const sessions = await api.getSessions(workspace);
1266
1281
  if (asJson) {
1267
1282
  emitJson(sessions, outputPath);
1268
1283
  return;
1269
1284
  }
1270
1285
  if (sessions.length === 0) {
1271
- console.log("No sessions found.");
1286
+ console.log(`No sessions found in ${workspace}.`);
1272
1287
  return;
1273
1288
  }
1274
1289
  console.log(`${sessions.length} session(s):
@@ -1276,7 +1291,7 @@ async function handleStatus(subArgs, injectedApi) {
1276
1291
  for (const s of sessions) {
1277
1292
  console.log(` ${s.id.slice(-8)} ${s.status.padEnd(10)} ${s.taskCount} tasks ${(s.runners || []).join(",").padEnd(12)} ${s.goal.slice(0, 60)}`);
1278
1293
  }
1279
- const last = readLastSession();
1294
+ const last = readLastSession(workspace);
1280
1295
  if (last) {
1281
1296
  console.log(`
1282
1297
  Last active: ${last.sessionId.slice(-8)} ("${last.goal.slice(0, 50)}")`);
@@ -1317,7 +1332,13 @@ function printOneSession(sessionId, meta, plan) {
1317
1332
  const icon = iconFor(t.status);
1318
1333
  const model = t.assignedModel ? ` (${t.assignedModel.modelLabel})` : "";
1319
1334
  const shortId = (t.id ?? "").slice(-8);
1320
- console.log(` ${String(t.order).padStart(2)}. ${icon} [${shortId}] ${t.title}${model}`);
1335
+ console.log(` ${(0, import_core7.taskOrderLabel)(t).padStart(2)}. ${icon} [${shortId}] ${t.title}${model}`);
1336
+ for (const sub of t.subtasks || []) {
1337
+ const subIcon = iconFor(sub.status);
1338
+ const subModel = sub.assignedModel ? ` (${sub.assignedModel.modelLabel})` : "";
1339
+ const subShortId = (sub.id ?? "").slice(-8);
1340
+ console.log(` ${(0, import_core7.taskOrderLabel)(sub, t)}. ${subIcon} [${subShortId}] ${sub.title}${subModel}`);
1341
+ }
1321
1342
  }
1322
1343
  }
1323
1344
 
@@ -1330,7 +1351,8 @@ async function handleStop(subArgs) {
1330
1351
  }
1331
1352
  let sessionId = flag(subArgs, "--session-id");
1332
1353
  if (!sessionId) {
1333
- const last = readLastSession();
1354
+ const workspace = flag(subArgs, "--workspace") || process.cwd();
1355
+ const last = readLastSession(workspace);
1334
1356
  if (!last) {
1335
1357
  console.error("No session specified. Use --session-id <id> or --server to stop the server.");
1336
1358
  process.exit(1);
@@ -1367,10 +1389,10 @@ async function handleWeb(subArgs) {
1367
1389
  }
1368
1390
 
1369
1391
  // src/commands/models.ts
1370
- var import_core5 = require("@ordewell/core");
1392
+ var import_core9 = require("@ordewell/core");
1371
1393
 
1372
1394
  // src/commands/model.ts
1373
- var import_core4 = require("@ordewell/core");
1395
+ var import_core8 = require("@ordewell/core");
1374
1396
 
1375
1397
  // src/catalog.ts
1376
1398
  function normalizeCatalog(result) {
@@ -1452,7 +1474,8 @@ function toTaskView(task) {
1452
1474
  dependencies: task.dependencies ?? [],
1453
1475
  assignedRunner: task.assignedRunner || void 0,
1454
1476
  taskMode: task.taskMode || void 0,
1455
- assignedModel: task.assignedModel ?? void 0
1477
+ assignedModel: task.assignedModel ?? void 0,
1478
+ subtasks: [...task.subtasks ?? []].sort((a, b) => a.order - b.order).map(toTaskView)
1456
1479
  };
1457
1480
  }
1458
1481
  function taskViews(plan) {
@@ -1475,7 +1498,7 @@ async function handleModel(subArgs, injectedApi) {
1475
1498
  if (!modelId) fail(USAGE);
1476
1499
  const settings = await api.getSettings();
1477
1500
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
1478
- const runner = (0, import_core4.runnerForProvider)(provider);
1501
+ const runner = (0, import_core8.runnerForProvider)(provider);
1479
1502
  if (runner) {
1480
1503
  const known = (await fetchCatalog(api)).models.filter((m) => m.runners?.includes(runner));
1481
1504
  if (known.length > 0 && !known.some((m) => m.id === modelId)) {
@@ -1494,7 +1517,7 @@ async function showModels(api) {
1494
1517
  const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
1495
1518
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
1496
1519
  const current = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
1497
- const runner = (0, import_core4.runnerForProvider)(provider);
1520
+ const runner = (0, import_core8.runnerForProvider)(provider);
1498
1521
  const options = runner ? catalog.models.filter((m) => m.runners?.includes(runner)) : catalog.orchestratorModels;
1499
1522
  console.log(`
1500
1523
  Planner model: ${current || "(backend default)"}`);
@@ -1528,8 +1551,8 @@ async function handleModels(subArgs) {
1528
1551
  const rest = subArgs.filter((_, i) => i !== setIdx && i !== setIdx + 1);
1529
1552
  return handleModel(["set", subArgs[setIdx + 1], ...rest]);
1530
1553
  }
1531
- const config = new import_core5.EnvConfig();
1532
- const providers = (0, import_core5.configuredProviders)(config);
1554
+ const config = new import_core9.EnvConfig();
1555
+ const providers = (0, import_core9.configuredProviders)(config);
1533
1556
  if (providers.length === 0) {
1534
1557
  console.error("No providers configured. Set an API key for a provider, e.g.:");
1535
1558
  console.error(" OPENROUTER_API_KEY=... (OpenRouter)");
@@ -1538,19 +1561,19 @@ async function handleModels(subArgs) {
1538
1561
  console.error(" OPENAI_COMPATIBLE_BASE_URL=... (self-hosted / OpenAI-compatible)");
1539
1562
  process.exit(1);
1540
1563
  }
1541
- const { apiKeys, baseUrls } = (0, import_core5.collectProviderCredentials)(config);
1564
+ const { apiKeys, baseUrls } = (0, import_core9.collectProviderCredentials)(config);
1542
1565
  let models;
1543
1566
  let errors;
1544
1567
  try {
1545
- const result = await (0, import_core5.fetchAllProviderModels)({ apiKeys, baseUrls });
1546
- models = (0, import_core5.toOrchestratorOptions)(result.models, import_core5.ORCHESTRATOR_SHORTCUTS);
1568
+ const result = await (0, import_core9.fetchAllProviderModels)({ apiKeys, baseUrls });
1569
+ models = (0, import_core9.toOrchestratorOptions)(result.models, import_core9.ORCHESTRATOR_SHORTCUTS);
1547
1570
  errors = result.errors;
1548
1571
  } catch (err) {
1549
1572
  console.error(`Failed to fetch models: ${err instanceof Error ? err.message : String(err)}`);
1550
1573
  process.exit(1);
1551
1574
  return;
1552
1575
  }
1553
- const providerLabel = (p) => (0, import_core5.getProviderMeta)(p)?.label ?? p;
1576
+ const providerLabel = (p) => (0, import_core9.getProviderMeta)(p)?.label ?? p;
1554
1577
  console.log(
1555
1578
  `
1556
1579
  Available models (${models.length}) across ${providers.length} configured provider(s):`
@@ -1667,9 +1690,9 @@ function handleSetup(subArgs) {
1667
1690
  }
1668
1691
 
1669
1692
  // src/commands/plugins.ts
1670
- var import_core6 = require("@ordewell/core");
1693
+ var import_core10 = require("@ordewell/core");
1671
1694
  function handlePlugins(subArgs) {
1672
- const registry = new import_core6.RunnerRegistry();
1695
+ const registry = new import_core10.RunnerRegistry();
1673
1696
  registry.loadUserPlugins();
1674
1697
  const sub = subArgs[0];
1675
1698
  if (sub === "list" || !sub) {
@@ -1705,7 +1728,7 @@ ${plugins.length} plugin(s):
1705
1728
  process.exit(1);
1706
1729
  }
1707
1730
  try {
1708
- const resolved = (0, import_core6.classifyPluginSource)(source);
1731
+ const resolved = (0, import_core10.classifyPluginSource)(source);
1709
1732
  const manifest = resolved.kind === "git" ? registry.installFromGit(resolved.url) : registry.installFromPath(resolved.path);
1710
1733
  console.log(`Installed plugin: ${manifest.name} v${manifest.version}`);
1711
1734
  } catch (err) {
@@ -1746,23 +1769,6 @@ ${plugins.length} plugin(s):
1746
1769
  process.exit(1);
1747
1770
  }
1748
1771
 
1749
- // src/commands/grillMe.ts
1750
- async function handleGrillMe(subArgs) {
1751
- const action = subArgs[0];
1752
- const port = await ensureDaemon(resolvePort(subArgs));
1753
- const api = new ApiClient(port);
1754
- if (action === "on" || action === "off") {
1755
- const result = await api.sendCommand("grill-me", { action });
1756
- const state = result.settings?.grillMe;
1757
- console.log(`Grill-Me: ${state?.enabled ? "ON" : "OFF"}`);
1758
- } else {
1759
- const settings = await api.getSettings();
1760
- const grillMe = settings.grillMe;
1761
- console.log(`Grill-Me: ${grillMe?.enabled ? "ON" : "OFF"}`);
1762
- console.log("Usage: ordewell grill-me [on|off]");
1763
- }
1764
- }
1765
-
1766
1772
  // src/commands/tdd.ts
1767
1773
  async function handleTdd(subArgs) {
1768
1774
  const action = subArgs[0];
@@ -1780,24 +1786,6 @@ async function handleTdd(subArgs) {
1780
1786
  }
1781
1787
  }
1782
1788
 
1783
- // src/commands/prd.ts
1784
- async function handlePrd(subArgs) {
1785
- const action = subArgs[0];
1786
- const port = await ensureDaemon(resolvePort(subArgs));
1787
- const api = new ApiClient(port);
1788
- if (action === "on" || action === "off") {
1789
- const result = await api.sendCommand("prd", { action });
1790
- const state = result.settings?.prd;
1791
- console.log(`PRD mode: ${state?.enabled ? "ON" : "OFF"}`);
1792
- } else {
1793
- const settings = await api.getSettings();
1794
- const prd = settings.prd;
1795
- console.log(`PRD mode: ${prd?.enabled ? "ON" : "OFF"}`);
1796
- console.log("Usage: ordewell prd [on|off]");
1797
- console.log("When ON, the planner previews and writes a markdown PRD (saved to .scratch/<slug>/PRD.md) before the task plan.");
1798
- }
1799
- }
1800
-
1801
1789
  // src/commands/allowlist.ts
1802
1790
  var USAGE2 = `Usage:
1803
1791
  ordewell allowlist set <runner> <id1,id2,\u2026> Set allowlist for a runner
@@ -1884,23 +1872,6 @@ async function handleShow(args, api) {
1884
1872
  }
1885
1873
  }
1886
1874
 
1887
- // src/commands/review.ts
1888
- async function handleReview(subArgs, api) {
1889
- const action = subArgs[0];
1890
- const port = api ? void 0 : await ensureDaemon(resolvePort(subArgs));
1891
- const client = api || new ApiClient(port);
1892
- if (action === "on" || action === "off") {
1893
- const result = await client.sendCommand("review", { action });
1894
- const state = result.settings?.review;
1895
- console.log(`Review mode: ${state?.enabled ? "ON" : "OFF"}`);
1896
- } else {
1897
- const settings = await client.getSettings();
1898
- const review = settings.review;
1899
- console.log(`Review mode: ${review?.enabled ? "ON" : "OFF"}`);
1900
- console.log("Usage: ordewell review [on|off]");
1901
- }
1902
- }
1903
-
1904
1875
  // src/commands/verify.ts
1905
1876
  async function handleVerify(subArgs, api) {
1906
1877
  const action = subArgs[0];
@@ -1918,23 +1889,6 @@ async function handleVerify(subArgs, api) {
1918
1889
  }
1919
1890
  }
1920
1891
 
1921
- // src/commands/researchSubagents.ts
1922
- async function handleResearchSubagents(subArgs, api) {
1923
- const action = subArgs[0];
1924
- const port = api ? void 0 : await ensureDaemon();
1925
- const client = api || new ApiClient(port);
1926
- if (action === "on" || action === "off") {
1927
- const result = await client.sendCommand("research-subagents", { action });
1928
- const state = result.settings?.researchSubagents;
1929
- console.log(`Research subagents: ${state?.enabled ? "ON" : "OFF"}`);
1930
- } else {
1931
- const settings = await client.getSettings();
1932
- const researchSubagents = settings.researchSubagents;
1933
- console.log(`Research subagents: ${researchSubagents?.enabled ? "ON" : "OFF"}`);
1934
- console.log("Usage: ordewell research-subagents [on|off]");
1935
- }
1936
- }
1937
-
1938
1892
  // src/commands/task-control.ts
1939
1893
  var PAST_TENSE = {
1940
1894
  run: "Task started.",
@@ -1943,11 +1897,12 @@ var PAST_TENSE = {
1943
1897
  cancel: "Task cancelled."
1944
1898
  };
1945
1899
  async function withResolvedTask(subArgs, usage, injectedApi, run) {
1946
- const last = readLastSession();
1900
+ const workspace = flag(subArgs, "--workspace") || process.cwd();
1947
1901
  let sessionId = flag(subArgs, "--session-id");
1948
1902
  if (!sessionId) {
1903
+ const last = readLastSession(workspace);
1949
1904
  if (!last) {
1950
- console.error("No session specified. Use --session-id <id> or run `ordewell plan` first.");
1905
+ console.error(`No session specified. Use --session-id <id> or run \`ordewell plan\` in ${workspace} first.`);
1951
1906
  process.exit(1);
1952
1907
  }
1953
1908
  sessionId = last.sessionId;
@@ -1958,22 +1913,12 @@ async function withResolvedTask(subArgs, usage, injectedApi, run) {
1958
1913
  process.exit(1);
1959
1914
  }
1960
1915
  const api = injectedApi || new ApiClient(await ensureDaemon(resolvePort(subArgs)));
1961
- const explicitWorkspace = flag(subArgs, "--workspace");
1962
1916
  let plan;
1963
1917
  try {
1964
- plan = (await api.getSession(sessionId, explicitWorkspace || process.cwd())).plan;
1918
+ plan = (await api.getSession(sessionId, workspace)).plan;
1965
1919
  } catch (err) {
1966
- if (!explicitWorkspace && last?.workspace && last.workspace !== process.cwd()) {
1967
- try {
1968
- plan = (await api.getSession(sessionId, last.workspace)).plan;
1969
- } catch {
1970
- console.error(`Failed to load session: ${err.message}`);
1971
- process.exit(1);
1972
- }
1973
- } else {
1974
- console.error(`Failed to load session: ${err.message}`);
1975
- process.exit(1);
1976
- }
1920
+ console.error(`Failed to load session: ${err.message}`);
1921
+ process.exit(1);
1977
1922
  }
1978
1923
  const taskId = resolveTaskId(plan, identifier);
1979
1924
  if (!taskId) {
@@ -1986,7 +1931,7 @@ function makeHandler(action, command = action) {
1986
1931
  return async function handle(subArgs, injectedApi) {
1987
1932
  await withResolvedTask(
1988
1933
  subArgs,
1989
- `Usage: ordewell ${command} <task-id-or-order> [--session-id <id>]`,
1934
+ `Usage: ordewell ${command} <task-id-or-order> [--session-id <id>] [--workspace /path]`,
1990
1935
  injectedApi,
1991
1936
  async (api, sessionId, taskId) => {
1992
1937
  try {
@@ -2059,14 +2004,15 @@ async function handleSkip(subArgs, api) {
2059
2004
  async function handleAddTask(subArgs, injectedApi) {
2060
2005
  const title = flag(subArgs, "--title");
2061
2006
  if (!title) {
2062
- console.error('Usage: ordewell add-task --title "..." [--description "..."] [--prompt "..."] [--type ai|user] [--depends-on <task-id> ...] [--session-id <id>]');
2007
+ console.error('Usage: ordewell add-task --title "..." [--description "..."] [--prompt "..."] [--type ai|user] [--depends-on <task-id> ...] [--session-id <id>] [--workspace /path]');
2063
2008
  process.exit(1);
2064
2009
  }
2065
2010
  let sessionId = flag(subArgs, "--session-id");
2066
2011
  if (!sessionId) {
2067
- const last = readLastSession();
2012
+ const workspace = flag(subArgs, "--workspace") || process.cwd();
2013
+ const last = readLastSession(workspace);
2068
2014
  if (!last) {
2069
- console.error("No session specified. Use --session-id <id> or run `ordewell plan` first.");
2015
+ console.error(`No session specified. Use --session-id <id> or run \`ordewell plan\` in ${workspace} first.`);
2070
2016
  process.exit(1);
2071
2017
  }
2072
2018
  sessionId = last.sessionId;
@@ -2096,7 +2042,7 @@ async function handleAddTask(subArgs, injectedApi) {
2096
2042
  async function handleRemoveTask(subArgs, api) {
2097
2043
  await withResolvedTask(
2098
2044
  subArgs,
2099
- "Usage: ordewell remove-task <task-id-or-order> [--session-id <id>]",
2045
+ "Usage: ordewell remove-task <task-id-or-order> [--session-id <id>] [--workspace /path]",
2100
2046
  api,
2101
2047
  async (client, sessionId, taskId) => {
2102
2048
  try {
@@ -2127,7 +2073,7 @@ async function handleSessions(subArgs, injectedApi) {
2127
2073
  console.log(`No sessions found in ${workspace}.`);
2128
2074
  return;
2129
2075
  }
2130
- const last = readLastSession();
2076
+ const last = readLastSession(workspace);
2131
2077
  console.log(`${sessions.length} session(s) in ${workspace}:
2132
2078
  `);
2133
2079
  for (const s of sessions) {
@@ -2177,7 +2123,7 @@ async function handleSessions(subArgs, injectedApi) {
2177
2123
  }
2178
2124
 
2179
2125
  // src/commands/planner.ts
2180
- var import_core7 = require("@ordewell/core");
2126
+ var import_core11 = require("@ordewell/core");
2181
2127
 
2182
2128
  // src/plannerModelSwitch.ts
2183
2129
  function describePlannerSwitch(settings, provider) {
@@ -2196,7 +2142,7 @@ function rememberedModelFor(settings, provider) {
2196
2142
  }
2197
2143
 
2198
2144
  // src/commands/planner.ts
2199
- var KNOWN_PROVIDERS = Object.keys(import_core7.ALL_PROVIDERS);
2145
+ var KNOWN_PROVIDERS = Object.keys(import_core11.ALL_PROVIDERS);
2200
2146
  async function handlePlanner(subArgs, injectedApi) {
2201
2147
  const api = await connect(subArgs, injectedApi);
2202
2148
  const [wanted] = positionals(subArgs);
@@ -2211,13 +2157,13 @@ async function handlePlanner(subArgs, injectedApi) {
2211
2157
  const settings = await persistEnv(api, { AI_PROVIDER: provider });
2212
2158
  const recall2 = describePlannerSwitch(settings, provider);
2213
2159
  writeEnv({ ORCHESTRATOR_MODEL: recall2.model, ORDEWELL_PLANNER_EFFORT: recall2.effort });
2214
- const meta = import_core7.ALL_PROVIDERS[provider];
2160
+ const meta = import_core11.ALL_PROVIDERS[provider];
2215
2161
  const catalog = await fetchCatalog(api);
2216
2162
  console.log(
2217
- (0, import_core7.isCliProvider)(provider) ? `Planning with ${meta.label} \u2014 no API key needed, it uses that agent's own subscription.` : `Planner set to ${meta.label}.`
2163
+ (0, import_core11.isCliProvider)(provider) ? `Planning with ${meta.label} \u2014 no API key needed, it uses that agent's own subscription.` : `Planner set to ${meta.label}.`
2218
2164
  );
2219
2165
  console.log(plannerModelLine(recall2));
2220
- if (!(0, import_core7.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
2166
+ if (!(0, import_core11.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
2221
2167
  console.log(` No ${meta.apiKeyEnvVar} configured yet \u2014 set one with \`ordewell key set ${provider} <key>\`.`);
2222
2168
  }
2223
2169
  }
@@ -2235,15 +2181,15 @@ async function showPlanners(api) {
2235
2181
  const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
2236
2182
  const current = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
2237
2183
  console.log("\nCoding agents (no API key \u2014 they use their own subscription)");
2238
- for (const id of import_core7.CLI_PROVIDERS) {
2239
- const runner = (0, import_core7.runnerForProvider)(id);
2184
+ for (const id of import_core11.CLI_PROVIDERS) {
2185
+ const runner = (0, import_core11.runnerForProvider)(id);
2240
2186
  const discovered = runner ? catalog.models.some((m) => m.runners?.includes(runner)) : false;
2241
- console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core7.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
2187
+ console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core11.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
2242
2188
  }
2243
2189
  console.log("\nAPI providers");
2244
- for (const id of import_core7.PROVIDER_PRIORITY) {
2190
+ for (const id of import_core11.PROVIDER_PRIORITY) {
2245
2191
  const configured = catalog.providers.includes(id);
2246
- const meta = import_core7.ALL_PROVIDERS[id];
2192
+ const meta = import_core11.ALL_PROVIDERS[id];
2247
2193
  console.log(` ${marker(id === current)} ${id.padEnd(14)} ${meta.label}${configured ? "" : ` (no ${meta.apiKeyEnvVar})`}`);
2248
2194
  }
2249
2195
  console.log(`
@@ -2260,7 +2206,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
2260
2206
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
2261
2207
  const modelId = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
2262
2208
  const currentEffort = typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : "";
2263
- const runner = (0, import_core7.runnerForProvider)(provider);
2209
+ const runner = (0, import_core11.runnerForProvider)(provider);
2264
2210
  if (!runner) {
2265
2211
  fail(
2266
2212
  "Thinking effort applies to a coding-agent planner.",
@@ -2300,7 +2246,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
2300
2246
  }
2301
2247
 
2302
2248
  // src/commands/key.ts
2303
- var import_core8 = require("@ordewell/core");
2249
+ var import_core12 = require("@ordewell/core");
2304
2250
  var USAGE4 = "Usage: ordewell key [list | set <provider> <key>]";
2305
2251
  async function handleKey(subArgs, injectedApi) {
2306
2252
  const [action, provider, ...keyParts] = positionals(subArgs);
@@ -2311,8 +2257,8 @@ async function handleKey(subArgs, injectedApi) {
2311
2257
  if (action !== "set") {
2312
2258
  const catalog = await fetchCatalog(api);
2313
2259
  console.log("\nAPI provider keys\n");
2314
- for (const id2 of import_core8.PROVIDER_PRIORITY) {
2315
- const meta2 = import_core8.ALL_PROVIDERS[id2];
2260
+ for (const id2 of import_core12.PROVIDER_PRIORITY) {
2261
+ const meta2 = import_core12.ALL_PROVIDERS[id2];
2316
2262
  const configured = catalog.providers.includes(id2);
2317
2263
  console.log(` ${configured ? "\u2713" : " "} ${id2.padEnd(14)} ${meta2.apiKeyEnvVar.padEnd(28)} ${meta2.label}`);
2318
2264
  }
@@ -2322,7 +2268,7 @@ async function handleKey(subArgs, injectedApi) {
2322
2268
  }
2323
2269
  if (!provider) fail(USAGE4);
2324
2270
  const id = provider.toLowerCase();
2325
- const meta = import_core8.ALL_PROVIDERS[id];
2271
+ const meta = import_core12.ALL_PROVIDERS[id];
2326
2272
  if (!meta) {
2327
2273
  fail(`Unknown provider: ${provider}`, "Run `ordewell key` to see the list.");
2328
2274
  }
@@ -2402,7 +2348,7 @@ async function handleApprove(subArgs, injectedApi) {
2402
2348
  // src/tui/terminalLauncher.ts
2403
2349
  var import_child_process3 = require("child_process");
2404
2350
  var import_util = require("util");
2405
- var import_core9 = require("@ordewell/core");
2351
+ var import_core13 = require("@ordewell/core");
2406
2352
  var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
2407
2353
  var defaultExecFile = async (command, args) => {
2408
2354
  const { stdout, stderr } = await execFileAsync(command, args);
@@ -2422,16 +2368,16 @@ var defaultWhich = (bin) => {
2422
2368
  async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
2423
2369
  const platform = deps.platform ?? process.platform;
2424
2370
  const env = deps.env ?? process.env;
2425
- const hasTmuxImpl = deps.hasTmuxImpl ?? import_core9.hasTmux;
2371
+ const hasTmuxImpl = deps.hasTmuxImpl ?? import_core13.hasTmux;
2426
2372
  const execFileImpl = deps.execFileImpl ?? defaultExecFile;
2427
2373
  const spawnImpl = deps.spawnImpl ?? defaultSpawn;
2428
2374
  const which = deps.which ?? defaultWhich;
2429
2375
  if (!hasTmuxImpl()) {
2430
2376
  return { ok: false, message: "tmux is not installed \u2014 install it to get a real terminal per task." };
2431
2377
  }
2432
- const session = (0, import_core9.tmuxSessionName)(port);
2433
- const socket = (0, import_core9.tmuxSocketName)(port);
2434
- const base = (0, import_core9.tmuxWindowName)(taskId, planSessionId);
2378
+ const session = (0, import_core13.tmuxSessionName)(port);
2379
+ const socket = (0, import_core13.tmuxSocketName)(port);
2380
+ const base = (0, import_core13.tmuxWindowName)(taskId, planSessionId);
2435
2381
  let windows;
2436
2382
  try {
2437
2383
  const { stdout } = await execFileImpl("tmux", ["-L", socket, "list-windows", "-t", session, "-F", "#{window_name}"]);
@@ -2725,7 +2671,7 @@ Dependencies \xB7 #${task.order} ${task.title}
2725
2671
  }
2726
2672
 
2727
2673
  // src/tui/index.ts
2728
- var import_core13 = require("@ordewell/core");
2674
+ var import_core17 = require("@ordewell/core");
2729
2675
 
2730
2676
  // src/tui/ansi.ts
2731
2677
  var import_string_width = __toESM(require("string-width"));
@@ -3030,12 +2976,8 @@ var SLASH_COMMANDS = [
3030
2976
  { name: "auto", usage: "/auto [on|off]", description: "Autonomous permission mode for new plans", category: "models" },
3031
2977
  { name: "refresh", usage: "/refresh", description: "Re-discover runners and model catalogs", category: "models" },
3032
2978
  // Skills
3033
- { name: "grill-me", usage: "/grill-me [on|off]", description: "Grill-Me challenge mode \u2014 the planner interrogates the goal", category: "skills" },
3034
2979
  { name: "tdd", usage: "/tdd [on|off]", description: "Test-Driven Development mode", category: "skills" },
3035
- { name: "prd", usage: "/prd [on|off]", description: "PRD mode \u2014 write a PRD before the plan", category: "skills" },
3036
- { name: "review", usage: "/review [on|off]", description: "Review mode \u2014 final opinion-review task with human sign-off", category: "skills" },
3037
2980
  { name: "verify", usage: "/verify [on|off]", description: "Verification mode \u2014 final evidence-based task that runs the suite", category: "skills" },
3038
- { name: "research-subagents", usage: "/research-subagents [on|off]", description: "Parallel read-only research subagents during planning", category: "skills" },
3039
2981
  // Sessions
3040
2982
  { name: "sessions", usage: "/sessions", description: "List saved sessions", category: "session" },
3041
2983
  { name: "new", usage: "/new", description: "Start a new session", category: "session" },
@@ -3048,6 +2990,23 @@ var SLASH_COMMANDS = [
3048
2990
  { name: "quit", usage: "/quit", description: "Exit the TUI", category: "system" }
3049
2991
  ];
3050
2992
  var BY_NAME = new Map(SLASH_COMMANDS.map((c) => [c.name, c]));
2993
+ var skillCommands = [];
2994
+ var skillByName = /* @__PURE__ */ new Map();
2995
+ function registerSkillCommands(skills) {
2996
+ const map = /* @__PURE__ */ new Map();
2997
+ for (const skill of skills) {
2998
+ if (BY_NAME.has(skill.name)) continue;
2999
+ map.set(skill.name, {
3000
+ name: skill.name,
3001
+ usage: `/${skill.name}`,
3002
+ description: skill.description,
3003
+ category: "skills",
3004
+ source: "skill"
3005
+ });
3006
+ }
3007
+ skillByName = map;
3008
+ skillCommands = [...map.values()];
3009
+ }
3051
3010
  function parseSlash(text) {
3052
3011
  const trimmed = text.trim();
3053
3012
  if (!trimmed.startsWith("/")) return null;
@@ -3056,14 +3015,15 @@ function parseSlash(text) {
3056
3015
  return { name: parts[0].toLowerCase(), args: parts.slice(1) };
3057
3016
  }
3058
3017
  function findCommand(name) {
3059
- return BY_NAME.get(name.replace(/^\//, "").toLowerCase());
3018
+ const key = name.replace(/^\//, "").toLowerCase();
3019
+ return BY_NAME.get(key) ?? skillByName.get(key);
3060
3020
  }
3061
3021
  function completions(text) {
3062
3022
  if (!text.startsWith("/")) return [];
3063
3023
  const typed = text.slice(1);
3064
3024
  if (/\s/.test(typed)) return [];
3065
3025
  const prefix = typed.toLowerCase();
3066
- return SLASH_COMMANDS.filter((c) => c.name.startsWith(prefix));
3026
+ return [...SLASH_COMMANDS, ...skillCommands].filter((c) => c.name.startsWith(prefix));
3067
3027
  }
3068
3028
 
3069
3029
  // src/tui/markdown.ts
@@ -3208,9 +3168,9 @@ function chatEditorRoom(cols, active) {
3208
3168
  }
3209
3169
 
3210
3170
  // src/tui/state.ts
3211
- var SKILL_IDS = ["grill-me", "tdd", "prd", "review", "verify", "research-subagents"];
3171
+ var SKILL_IDS = ["tdd", "verify"];
3212
3172
  function noSkills() {
3213
- return { "grill-me": false, tdd: false, prd: false, review: false, verify: false, "research-subagents": false };
3173
+ return { tdd: false, verify: false };
3214
3174
  }
3215
3175
  function visibleItems(picker2) {
3216
3176
  const filter = picker2.filter.trim().toLowerCase();
@@ -3221,6 +3181,21 @@ function visibleItems(picker2) {
3221
3181
  }
3222
3182
  var isTaskRunning = (task) => task.status === "in_progress" || task.status === "running";
3223
3183
  var anyTaskRunning = (state) => state.tasks.some(isTaskRunning);
3184
+ function planRows(state) {
3185
+ const rows = [];
3186
+ for (const task of state.tasks) {
3187
+ rows.push({ task, parent: null });
3188
+ const subtasks = task.subtasks ?? [];
3189
+ const open = state.expandedTaskId === task.id || subtasks.some((s) => s.id === state.expandedTaskId);
3190
+ if (open) {
3191
+ for (const subtask of subtasks) rows.push({ task: subtask, parent: task });
3192
+ }
3193
+ }
3194
+ return rows;
3195
+ }
3196
+ function selectedPlanRow(state) {
3197
+ return planRows(state)[state.selectedTask] ?? null;
3198
+ }
3224
3199
  function initialState(overrides = {}) {
3225
3200
  return {
3226
3201
  editor: emptyEditor(),
@@ -3266,7 +3241,7 @@ function initialState(overrides = {}) {
3266
3241
  }
3267
3242
 
3268
3243
  // src/tui/layout.ts
3269
- var import_core10 = require("@ordewell/core");
3244
+ var import_core14 = require("@ordewell/core");
3270
3245
  var CHROME_ROWS = 4;
3271
3246
  function chatInputWrap(state) {
3272
3247
  const active = state.focus === "chat" && !state.overlay;
@@ -3274,7 +3249,7 @@ function chatInputWrap(state) {
3274
3249
  return wrapLines(state.editor.text, chatEditorRoom(state.cols, true));
3275
3250
  }
3276
3251
  function footerHints(state) {
3277
- const markHint = state.tasks[state.selectedTask]?.status === "completed" ? "m undone" : "m done";
3252
+ const markHint = selectedPlanRow(state)?.task.status === "completed" ? "m undone" : "m done";
3278
3253
  const planning = state.status === "planning" || state.status === "researching";
3279
3254
  const escHint = planning ? "esc stop planning" : null;
3280
3255
  if (state.focus === "plan") {
@@ -3402,9 +3377,9 @@ function chatScrollMax(state) {
3402
3377
  }
3403
3378
  function plannerSetup(state) {
3404
3379
  const provider = state.plannerProvider;
3405
- const registration = import_core10.ALL_PROVIDERS[provider];
3380
+ const registration = import_core14.ALL_PROVIDERS[provider];
3406
3381
  if (!registration) return { label: "not set", ready: false };
3407
- const runner = (0, import_core10.runnerForProvider)(provider);
3382
+ const runner = (0, import_core14.runnerForProvider)(provider);
3408
3383
  const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
3409
3384
  return { label: registration.label, ready };
3410
3385
  }
@@ -3511,16 +3486,17 @@ var STATUS_ICON = {
3511
3486
  approved: "\xB7",
3512
3487
  pending: "\xB7"
3513
3488
  };
3489
+ var IDLE_ICON = "~";
3514
3490
  function planLayout(state, rows, cols) {
3515
3491
  const done = state.tasks.filter((t) => t.status === "completed").length;
3516
3492
  const lines = [style.bold(`Plan ${done}/${state.tasks.length}`), ""];
3517
3493
  let selectedLine = 2;
3518
3494
  let editorLine;
3519
- state.tasks.forEach((task, i) => {
3495
+ planRows(state).forEach((row, i) => {
3520
3496
  const taskStart = lines.length;
3521
3497
  if (i === state.selectedTask) selectedLine = taskStart;
3522
- const renderedTask = taskLines(state, task, i, cols);
3523
- if (task.id === state.expandedTaskId && renderedTask.editorLine !== void 0) {
3498
+ const renderedTask = taskLines(state, row, i, cols);
3499
+ if (row.task.id === state.expandedTaskId && renderedTask.editorLine !== void 0) {
3524
3500
  editorLine = taskStart + renderedTask.editorLine;
3525
3501
  }
3526
3502
  lines.push(...renderedTask.lines);
@@ -3539,16 +3515,20 @@ function planScrollExtent(state) {
3539
3515
  const { followOffset, maxScroll } = planLayout(state, bodyRows(state), cols);
3540
3516
  return { followOffset, maxScroll };
3541
3517
  }
3542
- function taskLines(state, task, index, cols) {
3518
+ function taskLines(state, row, index, cols) {
3519
+ const { task, parent } = row;
3543
3520
  const selected = state.focus === "plan" && index === state.selectedTask;
3544
3521
  const expanded = state.expandedTaskId === task.id;
3545
3522
  const running = isTaskRunning(task);
3546
- const paint = STATUS_MARK[task.status] ?? ((t) => t);
3547
- const rawIcon = running ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[task.status] ?? "\xB7";
3523
+ const idle = running && task.status === "in_progress" && task.idleSince != null;
3524
+ const paint = idle ? style.cyan : STATUS_MARK[task.status] ?? ((t) => t);
3525
+ const rawIcon = idle ? IDLE_ICON : running ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[task.status] ?? "\xB7";
3548
3526
  const icon = paint(rawIcon);
3549
3527
  const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : style.grey(" AI");
3550
3528
  const caret = selected ? style.cyan("\u276F") : " ";
3551
- const head = `${caret} ${icon} ${String(task.order).padStart(2)} ${kind} `;
3529
+ const orderLabel = parent ? (0, import_core14.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
3530
+ const indent = parent ? " " : "";
3531
+ const head = `${indent}${caret} ${icon} ${orderLabel} ${kind} `;
3552
3532
  const titleRoom = Math.max(1, cols - width(head));
3553
3533
  const wrappedTitle = wrap(task.title, titleRoom);
3554
3534
  const shownTitle = expanded ? wrappedTitle : wrappedTitle.slice(0, 2);
@@ -3566,50 +3546,51 @@ function taskLines(state, task, index, cols) {
3566
3546
  const modeInfo = task.type === "ai" ? modesForTask(state.modesByRunner, task).find((m) => m.id === task.taskMode) : void 0;
3567
3547
  const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}${modeInfo?.autonomous ? style.yellow(" \u26A1") : ""}` : "";
3568
3548
  const runner = task.assignedRunner ?? "";
3549
+ const bodyPad = parent ? " " : " ";
3569
3550
  const meta = [running ? "working" : "", runner, model].filter(Boolean).join(" \xB7 ");
3570
- if (meta) lines.push(style.grey(truncate(` ${meta}`, cols)));
3571
- if (effort || mode) lines.push(style.grey(truncate(` ${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
3551
+ if (meta) lines.push(style.grey(truncate(`${bodyPad}${meta}`, cols)));
3552
+ if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
3572
3553
  let editorLine;
3573
3554
  if (expanded) {
3574
- lines.push(style.grey(` ${task.status.replace(/_/g, " ")}`));
3555
+ lines.push(style.grey(`${bodyPad}${task.status.replace(/_/g, " ")}`));
3575
3556
  if (modeInfo?.autonomous) {
3576
- lines.push(...taskText("Autonomy", "Runs without permission prompts. Toggle with /auto.", cols));
3557
+ lines.push(...taskText("Autonomy", "Runs without permission prompts. Toggle with /auto.", cols, bodyPad));
3577
3558
  }
3578
3559
  const editableSeed = task.prompt ?? task.description ?? task.title;
3579
3560
  if (task.description && task.description !== task.title && task.description !== editableSeed) {
3580
- lines.push(...taskText("Description", task.description, cols));
3561
+ lines.push(...taskText("Description", task.description, cols, bodyPad));
3581
3562
  }
3582
3563
  if (state.taskEditor) {
3583
- const room = paneTextRoom(cols);
3564
+ const room = Math.max(1, cols - width(bodyPad));
3584
3565
  const wrapped = wrapLines(state.taskEditor.text, room);
3585
3566
  const cp = cursorInLines(wrapped, state.taskEditor.cursor, state.taskEditor.text.length);
3586
3567
  editorLine = lines.length + 1 + cp.line;
3587
- lines.push(...taskPromptLines(wrapped, cp));
3568
+ lines.push(...taskPromptLines(wrapped, cp, bodyPad));
3588
3569
  }
3589
3570
  if (task.dependencies.length > 0) {
3590
3571
  const orderById = new Map(state.tasks.map((candidate) => [candidate.id, candidate.order]));
3591
3572
  const dependencies = task.dependencies.map((id) => orderById.has(id) ? `#${orderById.get(id)}` : id).join(", ");
3592
- lines.push(...taskText("Depends on", dependencies, cols));
3573
+ lines.push(...taskText("Depends on", dependencies, cols, bodyPad));
3593
3574
  }
3594
- lines.push(style.grey(" enter save \xB7 esc cancel"));
3595
- lines.push(style.grey(truncate(" then R runner \xB7 o model \xB7 e effort \xB7 M mode \xB7 D deps", cols)));
3575
+ lines.push(style.grey(`${bodyPad}enter save \xB7 esc cancel`));
3576
+ lines.push(style.grey(truncate(`${bodyPad}then R runner \xB7 o model \xB7 e effort \xB7 M mode \xB7 D deps`, cols)));
3596
3577
  }
3597
3578
  return { lines, editorLine };
3598
3579
  }
3599
- function taskText(label, text, cols) {
3600
- const room = paneTextRoom(cols);
3580
+ function taskText(label, text, cols, pad2 = " ") {
3581
+ const room = Math.max(1, cols - width(pad2));
3601
3582
  return [
3602
- style.grey(` ${label}`),
3603
- ...wrap(text, room).map((line) => ` ${line}`)
3583
+ style.grey(`${pad2}${label}`),
3584
+ ...wrap(text, room).map((line) => `${pad2}${line}`)
3604
3585
  ];
3605
3586
  }
3606
- function taskPromptLines(wrapped, cursor) {
3587
+ function taskPromptLines(wrapped, cursor, pad2 = " ") {
3607
3588
  return [
3608
- style.grey(" Prompt"),
3589
+ style.grey(`${pad2}Prompt`),
3609
3590
  ...wrapped.map(({ line }, i) => {
3610
- if (i !== cursor.line) return ` ${line}`;
3591
+ if (i !== cursor.line) return `${pad2}${line}`;
3611
3592
  const col = cursor.col;
3612
- return ` ${line.slice(0, col)}${style.inverse(line.slice(col, col + 1) || " ")}${line.slice(col + 1)}`;
3593
+ return `${pad2}${line.slice(0, col)}${style.inverse(line.slice(col, col + 1) || " ")}${line.slice(col + 1)}`;
3613
3594
  })
3614
3595
  ];
3615
3596
  }
@@ -3762,15 +3743,29 @@ function renderStatus(state, cols) {
3762
3743
  const tail = thinking.length > room ? `\u2026${thinking.slice(-(room - 1)).trimStart()}` : thinking;
3763
3744
  return truncate(`${head}${style.grey(` \xB7 ${tail}`)}`, cols);
3764
3745
  }
3746
+ var LEADING_TOKEN = /^\/\S*/;
3747
+ function skillTokenLength(text) {
3748
+ const match = LEADING_TOKEN.exec(text);
3749
+ if (!match) return 0;
3750
+ return findCommand(match[0])?.source === "skill" ? match[0].length : 0;
3751
+ }
3752
+ function paintRange(text, base, tokenEnd) {
3753
+ const localEnd = Math.max(0, Math.min(text.length, tokenEnd - base));
3754
+ if (localEnd <= 0) return text;
3755
+ if (localEnd >= text.length) return style.cyan(text);
3756
+ return style.cyan(text.slice(0, localEnd)) + text.slice(localEnd);
3757
+ }
3765
3758
  function renderInput(state, cols) {
3766
3759
  const prompt = state.focus === "plan" ? style.grey("\u276F ") : style.cyan("\u276F ");
3767
3760
  const lines = chatInputWrap(state);
3768
3761
  const room = chatEditorRoom(cols, lines !== null);
3769
3762
  const { text, cursor } = state.editor;
3770
3763
  const continuation = " ".repeat(width(prompt));
3764
+ const tokenEnd = skillTokenLength(text);
3771
3765
  if (!lines) {
3772
3766
  const visible = text.replace(/\n/g, "\xB6").replace(/\t/g, " ");
3773
- return [prompt + visible + suggest(state, room - width(visible))];
3767
+ const painted = tokenEnd > 0 ? paintRange(visible, 0, tokenEnd) : visible;
3768
+ return [prompt + painted + suggest(state, room - width(visible))];
3774
3769
  }
3775
3770
  const { line: lineIndex, col } = cursorInLines(lines, cursor, text.length);
3776
3771
  const rawLines = lines.map((l) => l.line);
@@ -3778,7 +3773,8 @@ function renderInput(state, cols) {
3778
3773
  const prefix = i === 0 ? prompt : continuation;
3779
3774
  const line = raw.replace(/\t/g, " ");
3780
3775
  const isCursorLine = i === lineIndex;
3781
- const rendered = isCursorLine ? line.slice(0, col) + style.inverse(line.slice(col, col + 1) || " ") + line.slice(col + 1) : line;
3776
+ const lineTokenEnd = i === 0 ? tokenEnd : 0;
3777
+ const rendered = isCursorLine ? paintRange(line.slice(0, col), 0, lineTokenEnd) + style.inverse(line.slice(col, col + 1) || " ") + paintRange(line.slice(col + 1), col + 1, lineTokenEnd) : paintRange(line, 0, lineTokenEnd);
3782
3778
  const used = width(line) + (isCursorLine && col >= line.length ? 1 : 0);
3783
3779
  const hint = i === rawLines.length - 1 ? suggest(state, room - used) : "";
3784
3780
  return prefix + rendered + hint;
@@ -3908,15 +3904,21 @@ function frame(title, body, rows, cols) {
3908
3904
  }
3909
3905
 
3910
3906
  // src/tui/reducer.ts
3911
- var import_core11 = require("@ordewell/core");
3907
+ var import_core15 = require("@ordewell/core");
3912
3908
  var import_plan_utils3 = require("@ordewell/core/plan-utils");
3913
- var KNOWN_PROVIDERS2 = Object.keys(import_core11.ALL_PROVIDERS);
3909
+ var KNOWN_PROVIDERS2 = Object.keys(import_core15.ALL_PROVIDERS);
3914
3910
  var DEFAULT_EFFORT = "__runner_default__";
3915
3911
  function step(state, effects = []) {
3916
3912
  return { state, effects };
3917
3913
  }
3918
- function say(state, role, content, research) {
3919
- const message = { role, content: sanitize(content), timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...research ? { research } : {} };
3914
+ function say(state, role, content, research, extra) {
3915
+ const message = {
3916
+ role,
3917
+ content: sanitize(content),
3918
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3919
+ ...research ? { research } : {},
3920
+ ...extra
3921
+ };
3920
3922
  return { ...state, messages: [...state.messages, message], scroll: 0 };
3921
3923
  }
3922
3924
  function restoredMessages(history) {
@@ -3986,24 +3988,42 @@ function reduce(state, action) {
3986
3988
  case "planUpdated": {
3987
3989
  if (stale(state, action.sessionId)) return step(state);
3988
3990
  const tasks = normalizeTasks(action.plan);
3989
- return step({
3991
+ const expandedTaskId = tasks.some((task) => task.id === state.expandedTaskId) ? state.expandedTaskId : null;
3992
+ const next = {
3990
3993
  ...state,
3991
3994
  tasks,
3992
3995
  status: state.status === "planning" || state.status === "researching" ? "idle" : state.status,
3993
3996
  busyLabel: "",
3994
3997
  thinkingLine: "",
3995
- selectedTask: Math.min(state.selectedTask, Math.max(0, tasks.length - 1)),
3996
- expandedTaskId: tasks.some((task) => task.id === state.expandedTaskId) ? state.expandedTaskId : null,
3997
- taskEditor: tasks.some((task) => task.id === state.expandedTaskId) ? state.taskEditor : null
3998
- });
3998
+ expandedTaskId,
3999
+ taskEditor: expandedTaskId !== null ? state.taskEditor : null
4000
+ };
4001
+ return step({ ...next, selectedTask: Math.min(state.selectedTask, Math.max(0, planRows(next).length - 1)) });
3999
4002
  }
4000
4003
  case "plannerMessage": {
4001
4004
  if (stale(state, action.sessionId)) return step(state);
4002
4005
  const settled = { ...state, status: "idle", busyLabel: "", thinkingLine: "" };
4003
4006
  const content = sanitize(action.content);
4007
+ const last = state.messages.at(-1);
4008
+ if (last?.role === "assistant" && last.streaming) {
4009
+ const messages = [...state.messages];
4010
+ messages[messages.length - 1] = { ...last, content, streaming: void 0 };
4011
+ return step({ ...settled, messages });
4012
+ }
4004
4013
  if (alreadySpoken(state.messages, content)) return step(settled);
4005
4014
  return step(say(settled, "assistant", content));
4006
4015
  }
4016
+ case "plannerToken": {
4017
+ if (stale(state, action.sessionId)) return step(state);
4018
+ const text = sanitize(action.text);
4019
+ const last = state.messages.at(-1);
4020
+ if (last?.role === "assistant" && last.streaming) {
4021
+ const messages = [...state.messages];
4022
+ messages[messages.length - 1] = { ...last, content: last.content + text };
4023
+ return step({ ...state, messages });
4024
+ }
4025
+ return step(say(state, "assistant", text, void 0, { streaming: true }));
4026
+ }
4007
4027
  case "researchStep": {
4008
4028
  if (stale(state, action.sessionId)) return step(state);
4009
4029
  const summary = sanitize(action.summary);
@@ -4040,10 +4060,12 @@ function reduce(state, action) {
4040
4060
  const updates = action.updates;
4041
4061
  let changed = false;
4042
4062
  const tasks = state.tasks.map((t) => {
4043
- const status = updates[t.id];
4044
- if (status !== void 0 && status !== t.status) {
4063
+ const update = updates[t.id];
4064
+ if (update === void 0) return t;
4065
+ const idleSince = update.idleSince ?? null;
4066
+ if (update.status !== t.status || idleSince !== (t.idleSince ?? null)) {
4045
4067
  changed = true;
4046
- return { ...t, status };
4068
+ return { ...t, status: update.status, idleSince };
4047
4069
  }
4048
4070
  return t;
4049
4071
  });
@@ -4125,9 +4147,12 @@ function normalizeTasks(plan) {
4125
4147
  }
4126
4148
  }
4127
4149
  const raw = byId.size > 0 ? [...byId.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) : source?.tasks ?? [];
4128
- return raw.map((t, i) => ({
4129
- id: String(t.id ?? `task-${i + 1}`),
4130
- order: typeof t.order === "number" ? t.order : i + 1,
4150
+ return raw.map((t, i) => toTaskView2(t, i));
4151
+ }
4152
+ function toTaskView2(t, index) {
4153
+ return {
4154
+ id: String(t.id ?? `task-${index + 1}`),
4155
+ order: typeof t.order === "number" ? t.order : index + 1,
4131
4156
  title: planLabel(t.title, "Untitled task"),
4132
4157
  description: planText(t.description ?? t.title),
4133
4158
  prompt: typeof t.prompt === "string" ? sanitize(t.prompt) : void 0,
@@ -4141,18 +4166,15 @@ function normalizeTasks(plan) {
4141
4166
  modelLabel: planLabel(t.assignedModel.modelLabel ?? t.assignedModel.modelId),
4142
4167
  thinkingEffort: typeof t.assignedModel.thinkingEffort === "string" ? planLabel(t.assignedModel.thinkingEffort) : typeof t.thinkingEffort === "string" ? planLabel(t.thinkingEffort) : void 0,
4143
4168
  availableVariants: Array.isArray(t.assignedModel.availableVariants) ? t.assignedModel.availableVariants.map((variant) => planLabel(variant)) : void 0
4144
- } : void 0
4145
- }));
4169
+ } : void 0,
4170
+ subtasks: Array.isArray(t.subtasks) ? [...t.subtasks].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).map((s, i) => toTaskView2(s, i)) : void 0
4171
+ };
4146
4172
  }
4147
4173
  var enabledFlag = (value) => typeof value?.enabled === "boolean" ? value.enabled : void 0;
4148
4174
  function applySettings(state, settings) {
4149
4175
  const sources = {
4150
- "grill-me": settings.grillMe,
4151
4176
  tdd: settings.tdd,
4152
- prd: settings.prd,
4153
- review: settings.review,
4154
- verify: settings.verification,
4155
- "research-subagents": settings.researchSubagents
4177
+ verify: settings.verification
4156
4178
  };
4157
4179
  const skills = { ...state.skills };
4158
4180
  for (const id of SKILL_IDS) {
@@ -4181,11 +4203,11 @@ function modelDetail(m) {
4181
4203
  function providerErrorHint(state) {
4182
4204
  const failed = Object.keys(state.providerErrors ?? {});
4183
4205
  if (failed.length === 0) return void 0;
4184
- const names = failed.map((p) => import_core11.ALL_PROVIDERS[p]?.label ?? p);
4206
+ const names = failed.map((p) => import_core15.ALL_PROVIDERS[p]?.label ?? p);
4185
4207
  return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
4186
4208
  }
4187
4209
  function plannerModelItems(state) {
4188
- const runner = (0, import_core11.runnerForProvider)(state.plannerProvider);
4210
+ const runner = (0, import_core15.runnerForProvider)(state.plannerProvider);
4189
4211
  if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
4190
4212
  const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
4191
4213
  id: m.id,
@@ -4196,7 +4218,7 @@ function plannerModelItems(state) {
4196
4218
  return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
4197
4219
  }
4198
4220
  function plannerEffortItems(state) {
4199
- const runner = (0, import_core11.runnerForProvider)(state.plannerProvider);
4221
+ const runner = (0, import_core15.runnerForProvider)(state.plannerProvider);
4200
4222
  if (!runner) {
4201
4223
  return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
4202
4224
  }
@@ -4409,20 +4431,20 @@ function handlePlanKey(state, key) {
4409
4431
  if (editingTask && state.taskEditor) return handleTaskEditKey(state, editingTask, state.taskEditor, key);
4410
4432
  if (key.name === "escape") return step({ ...state, focus: "chat" });
4411
4433
  if (key.name === "up") {
4412
- return step({ ...state, selectedTask: Math.max(0, state.selectedTask - 1), expandedTaskId: null, planScroll: null });
4434
+ return step({ ...state, selectedTask: Math.max(0, state.selectedTask - 1), planScroll: null });
4413
4435
  }
4414
4436
  if (key.name === "down") {
4415
4437
  return step({
4416
4438
  ...state,
4417
- selectedTask: Math.min(state.tasks.length - 1, state.selectedTask + 1),
4418
- expandedTaskId: null,
4439
+ selectedTask: Math.min(planRows(state).length - 1, state.selectedTask + 1),
4419
4440
  planScroll: null
4420
4441
  });
4421
4442
  }
4422
4443
  const scroll = scrollDelta(key, state);
4423
4444
  if (scroll !== null) return scrollPlan(state, scroll);
4424
- const task = state.tasks[state.selectedTask];
4425
- if (!task) return step(state);
4445
+ const row = selectedPlanRow(state);
4446
+ if (!row) return step(state);
4447
+ const task = row.task;
4426
4448
  if (key.name === "enter" || key.name === "right") {
4427
4449
  const text = task.prompt ?? task.description ?? task.title;
4428
4450
  return step({
@@ -4672,7 +4694,7 @@ function submit(state) {
4672
4694
  if (!text) return step(state);
4673
4695
  const cleared = { ...state, editor: commit(state.editor) };
4674
4696
  const command = parseSlash(text);
4675
- if (command) return runCommand(cleared, command);
4697
+ if (command && findCommand(command.name)?.source !== "skill") return runCommand(cleared, command);
4676
4698
  const spoken = say(cleared, "user", text);
4677
4699
  const effect = state.sessionId ? { type: "sendMessage", sessionId: state.sessionId, message: text } : { type: "startConversation", goal: text };
4678
4700
  return step({ ...spoken, status: "planning" }, [effect]);
@@ -4834,7 +4856,7 @@ function newSession(state) {
4834
4856
  }
4835
4857
  function setModel(state, args) {
4836
4858
  if (args[0] === "set" && args[1]) {
4837
- const runner = (0, import_core11.runnerForProvider)(state.plannerProvider);
4859
+ const runner = (0, import_core15.runnerForProvider)(state.plannerProvider);
4838
4860
  const known = runner ? modelsForRunner(state.models, runner) : [];
4839
4861
  if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
4840
4862
  return fail2(state, `${args[1]} was not discovered for ${runner}.`);
@@ -4856,21 +4878,21 @@ function setModel(state, args) {
4856
4878
  }
4857
4879
  function plannerItems(state) {
4858
4880
  const installed = new Set(state.runners.map((r) => r.id));
4859
- const agents = import_core11.CLI_PROVIDERS.map((id) => {
4860
- const runner = (0, import_core11.runnerForProvider)(id);
4881
+ const agents = import_core15.CLI_PROVIDERS.map((id) => {
4882
+ const runner = (0, import_core15.runnerForProvider)(id);
4861
4883
  const ready = installed.has(runner);
4862
4884
  return {
4863
4885
  id,
4864
- label: import_core11.ALL_PROVIDERS[id].label,
4886
+ label: import_core15.ALL_PROVIDERS[id].label,
4865
4887
  detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
4866
4888
  selected: state.plannerProvider === id,
4867
4889
  disabled: !ready
4868
4890
  };
4869
4891
  });
4870
- const vendors = import_core11.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
4892
+ const vendors = import_core15.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
4871
4893
  id,
4872
- label: import_core11.ALL_PROVIDERS[id].label,
4873
- detail: import_core11.ALL_PROVIDERS[id].apiKeyEnvVar,
4894
+ label: import_core15.ALL_PROVIDERS[id].label,
4895
+ detail: import_core15.ALL_PROVIDERS[id].apiKeyEnvVar,
4874
4896
  selected: state.plannerProvider === id
4875
4897
  }));
4876
4898
  return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
@@ -4928,16 +4950,16 @@ function setKey(state, args) {
4928
4950
  }
4929
4951
  return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
4930
4952
  }
4931
- const items = import_core11.PROVIDER_PRIORITY.map((id) => ({
4953
+ const items = import_core15.PROVIDER_PRIORITY.map((id) => ({
4932
4954
  id,
4933
- label: import_core11.ALL_PROVIDERS[id].label,
4934
- detail: import_core11.ALL_PROVIDERS[id].apiKeyEnvVar,
4955
+ label: import_core15.ALL_PROVIDERS[id].label,
4956
+ detail: import_core15.ALL_PROVIDERS[id].apiKeyEnvVar,
4935
4957
  selected: state.configuredProviders.includes(id)
4936
4958
  }));
4937
4959
  return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
4938
4960
  }
4939
4961
  function keyPrompt(provider) {
4940
- const meta = import_core11.ALL_PROVIDERS[provider];
4962
+ const meta = import_core15.ALL_PROVIDERS[provider];
4941
4963
  return {
4942
4964
  kind: "prompt",
4943
4965
  title: `${meta.label} API key`,
@@ -5295,7 +5317,7 @@ function createApp(deps) {
5295
5317
 
5296
5318
  // src/tui/effects.ts
5297
5319
  var import_child_process4 = require("child_process");
5298
- var import_core12 = require("@ordewell/core");
5320
+ var import_core16 = require("@ordewell/core");
5299
5321
  var import_plan_utils4 = require("@ordewell/core/plan-utils");
5300
5322
  var ConversationQueue = class {
5301
5323
  tail = Promise.resolve();
@@ -5462,7 +5484,7 @@ async function perform(effect, deps) {
5462
5484
  dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
5463
5485
  return;
5464
5486
  case "setPlanner": {
5465
- const meta = import_core12.ALL_PROVIDERS[effect.provider];
5487
+ const meta = import_core16.ALL_PROVIDERS[effect.provider];
5466
5488
  if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
5467
5489
  const env = { AI_PROVIDER: effect.provider };
5468
5490
  const settings = await api.updateSettings({ env });
@@ -5474,7 +5496,7 @@ async function perform(effect, deps) {
5474
5496
  });
5475
5497
  dispatch({
5476
5498
  type: "notice",
5477
- message: `${(0, import_core12.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
5499
+ message: `${(0, import_core16.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
5478
5500
  });
5479
5501
  await loadModels(deps);
5480
5502
  return;
@@ -5486,7 +5508,7 @@ async function perform(effect, deps) {
5486
5508
  dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
5487
5509
  return;
5488
5510
  case "setApiKey": {
5489
- const meta = import_core12.ALL_PROVIDERS[effect.provider];
5511
+ const meta = import_core16.ALL_PROVIDERS[effect.provider];
5490
5512
  if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
5491
5513
  await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
5492
5514
  deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
@@ -5574,7 +5596,7 @@ function osc52(text) {
5574
5596
  function copySelection(deps, text) {
5575
5597
  if (!text) return;
5576
5598
  const lines = text.split("\n").length;
5577
- const command = (0, import_core12.clipboardCopyCommand)(deps.hasBin);
5599
+ const command = (0, import_core16.clipboardCopyCommand)(deps.hasBin);
5578
5600
  if (command) {
5579
5601
  try {
5580
5602
  (deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
@@ -5607,7 +5629,28 @@ async function withExecutionStream(deps, sessionId, request) {
5607
5629
  }
5608
5630
  async function converse(deps, sessionId, call) {
5609
5631
  let streamed = null;
5632
+ const TOKEN_DEBOUNCE_MS = 75;
5633
+ let tokenBuffer = "";
5634
+ let tokenTimer = null;
5635
+ const flushTokens = () => {
5636
+ if (tokenTimer !== null) {
5637
+ clearTimeout(tokenTimer);
5638
+ tokenTimer = null;
5639
+ }
5640
+ if (!tokenBuffer) return;
5641
+ const text = tokenBuffer;
5642
+ tokenBuffer = "";
5643
+ deps.dispatch({ type: "plannerToken", text, sessionId });
5644
+ };
5610
5645
  const stream = deps.api.streamPlanning(sessionId, (event) => {
5646
+ if (event?.type === "plan_token" && event.token) {
5647
+ tokenBuffer += event.token;
5648
+ if (tokenTimer === null) {
5649
+ tokenTimer = setTimeout(flushTokens, TOKEN_DEBOUNCE_MS);
5650
+ }
5651
+ return;
5652
+ }
5653
+ flushTokens();
5611
5654
  if (event?.type === "approval_request") {
5612
5655
  deps.dispatch({
5613
5656
  type: "approvalRequested",
@@ -5664,6 +5707,7 @@ async function converse(deps, sessionId, call) {
5664
5707
  deps.dispatch({ type: "plannerMessage", content: question, sessionId });
5665
5708
  }
5666
5709
  } finally {
5710
+ if (tokenTimer !== null) clearTimeout(tokenTimer);
5667
5711
  stream.close();
5668
5712
  }
5669
5713
  }
@@ -5683,7 +5727,7 @@ function onExecutionEvent(dispatch, event, sessionId) {
5683
5727
  case "status_update": {
5684
5728
  const updates = {};
5685
5729
  for (const task of event.tasks ?? []) {
5686
- updates[String(task.id)] = String(task.status);
5730
+ updates[String(task.id)] = { status: String(task.status), idleSince: task.idleSince ?? null };
5687
5731
  }
5688
5732
  dispatch({ type: "tasksStatus", updates, sessionId });
5689
5733
  return;
@@ -6047,7 +6091,7 @@ async function handleTui(subArgs) {
6047
6091
  }
6048
6092
  const workspace = flag(subArgs, "--workspace") || process.cwd();
6049
6093
  try {
6050
- (0, import_core13.assertWorkspaceExists)(workspace);
6094
+ (0, import_core17.assertWorkspaceExists)(workspace);
6051
6095
  } catch {
6052
6096
  console.error(
6053
6097
  `Workspace "${workspace}" does not exist.
@@ -6056,7 +6100,10 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
6056
6100
  );
6057
6101
  process.exit(1);
6058
6102
  }
6059
- const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(resolvePort(subArgs), { detached: false });
6103
+ registerSkillCommands((0, import_core17.createSkillsService)(workspace).listSkills());
6104
+ const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
6105
+ const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
6106
+ const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
6060
6107
  let owned = ownedAtLaunch;
6061
6108
  const api = new ApiClient(port);
6062
6109
  const conversationQueue = new ConversationQueue();
@@ -6072,7 +6119,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
6072
6119
  conversationQueue,
6073
6120
  port,
6074
6121
  dispatch: (action) => app.dispatch(action),
6075
- newSessionId: () => (0, import_core13.mintSessionId)(),
6122
+ newSessionId: () => (0, import_core17.mintSessionId)(),
6076
6123
  setEnvVar: (key, value) => {
6077
6124
  writeEnvVar(findEnvFile(), key, value);
6078
6125
  process.env[key] = value;
@@ -6141,14 +6188,8 @@ var COMMANDS = {
6141
6188
  refresh: handleRefresh,
6142
6189
  setup: handleSetup,
6143
6190
  plugins: handlePlugins,
6144
- "grill-me": handleGrillMe,
6145
6191
  "tdd": handleTdd,
6146
- "prd": handlePrd,
6147
- "review": handleReview,
6148
- // 'verify' was once a legacy alias for review; it now toggles the
6149
- // evidence-based verification mode.
6150
6192
  "verify": handleVerify,
6151
- "research-subagents": handleResearchSubagents,
6152
6193
  "mark-complete": handleMarkComplete,
6153
6194
  complete: handleMarkComplete,
6154
6195
  uncomplete: handleUncomplete,
@@ -6184,6 +6225,7 @@ function cliVersion() {
6184
6225
 
6185
6226
  // src/main.ts
6186
6227
  async function main() {
6228
+ (0, import_core18.migrateOldConfigDir)();
6187
6229
  loadEnvFile();
6188
6230
  const argv = process.argv.slice(2);
6189
6231
  const command = argv[0] ?? (process.stdin.isTTY ? "tui" : "--help");