@ordewell/cli 0.4.10 → 0.4.12

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 +358 -296
  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_core19 = 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) {
@@ -1145,12 +1181,14 @@ function iconFor(status) {
1145
1181
  }
1146
1182
 
1147
1183
  // src/commands/run.ts
1184
+ var import_core7 = require("@ordewell/core");
1148
1185
  function resolveSessionId(subArgs) {
1149
1186
  const explicit = flag(subArgs, "--session-id");
1150
1187
  if (explicit) return explicit;
1151
- const last = readLastSession();
1188
+ const workspace = flag(subArgs, "--workspace") || process.cwd();
1189
+ const last = readLastSession(workspace);
1152
1190
  if (!last) {
1153
- console.error("No session specified. Use --session-id <id> or run `ordewell plan` first.");
1191
+ console.error(`No session specified. Use --session-id <id> or run \`ordewell plan\` in ${workspace} first.`);
1154
1192
  process.exit(1);
1155
1193
  }
1156
1194
  console.error(`Using last session: ${last.sessionId} ("${last.goal}")`);
@@ -1184,6 +1222,10 @@ async function followExecution(api, sessionId) {
1184
1222
  await api.streamExecution(sessionId, (event) => {
1185
1223
  if (event.type === "task_started") {
1186
1224
  process.stderr.write(`[${event.order}/${event.title}] Started: ${event.runner} / ${event.modelId}
1225
+ `);
1226
+ }
1227
+ if (event.type === "checkpoint") {
1228
+ process.stderr.write(`\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core7.truncateCheckpointSummary)(event.summary)}
1187
1229
  `);
1188
1230
  }
1189
1231
  if (event.type === "status_update" && event.tasks) {
@@ -1224,51 +1266,29 @@ async function handleRun(subArgs) {
1224
1266
 
1225
1267
  // src/commands/status.ts
1226
1268
  var import_fs4 = require("fs");
1269
+ var import_core8 = require("@ordewell/core");
1227
1270
  async function handleStatus(subArgs, injectedApi) {
1228
1271
  const sessionId = flag(subArgs, "--session-id");
1229
- const explicitWorkspace = flag(subArgs, "--workspace");
1230
- let workspace = explicitWorkspace || process.cwd();
1272
+ const workspace = flag(subArgs, "--workspace") || process.cwd();
1231
1273
  const outputPath = flag(subArgs, "--output");
1232
1274
  const asJson = hasFlag(subArgs, "--json") || Boolean(outputPath);
1233
1275
  const port = injectedApi ? resolvePort(subArgs) : await ensureDaemon(resolvePort(subArgs));
1234
1276
  const api = injectedApi || new ApiClient(port);
1235
1277
  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
- }
1278
+ const session = await api.getSession(sessionId, workspace);
1247
1279
  if (asJson) {
1248
1280
  emitJson(session, outputPath);
1249
1281
  return;
1250
1282
  }
1251
1283
  printOneSession(sessionId, session.meta, session.plan);
1252
1284
  } 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
- }
1285
+ const sessions = await api.getSessions(workspace);
1266
1286
  if (asJson) {
1267
1287
  emitJson(sessions, outputPath);
1268
1288
  return;
1269
1289
  }
1270
1290
  if (sessions.length === 0) {
1271
- console.log("No sessions found.");
1291
+ console.log(`No sessions found in ${workspace}.`);
1272
1292
  return;
1273
1293
  }
1274
1294
  console.log(`${sessions.length} session(s):
@@ -1276,7 +1296,7 @@ async function handleStatus(subArgs, injectedApi) {
1276
1296
  for (const s of sessions) {
1277
1297
  console.log(` ${s.id.slice(-8)} ${s.status.padEnd(10)} ${s.taskCount} tasks ${(s.runners || []).join(",").padEnd(12)} ${s.goal.slice(0, 60)}`);
1278
1298
  }
1279
- const last = readLastSession();
1299
+ const last = readLastSession(workspace);
1280
1300
  if (last) {
1281
1301
  console.log(`
1282
1302
  Last active: ${last.sessionId.slice(-8)} ("${last.goal.slice(0, 50)}")`);
@@ -1317,7 +1337,13 @@ function printOneSession(sessionId, meta, plan) {
1317
1337
  const icon = iconFor(t.status);
1318
1338
  const model = t.assignedModel ? ` (${t.assignedModel.modelLabel})` : "";
1319
1339
  const shortId = (t.id ?? "").slice(-8);
1320
- console.log(` ${String(t.order).padStart(2)}. ${icon} [${shortId}] ${t.title}${model}`);
1340
+ console.log(` ${(0, import_core8.taskOrderLabel)(t).padStart(2)}. ${icon} [${shortId}] ${t.title}${model}`);
1341
+ for (const sub of t.subtasks || []) {
1342
+ const subIcon = iconFor(sub.status);
1343
+ const subModel = sub.assignedModel ? ` (${sub.assignedModel.modelLabel})` : "";
1344
+ const subShortId = (sub.id ?? "").slice(-8);
1345
+ console.log(` ${(0, import_core8.taskOrderLabel)(sub, t)}. ${subIcon} [${subShortId}] ${sub.title}${subModel}`);
1346
+ }
1321
1347
  }
1322
1348
  }
1323
1349
 
@@ -1330,7 +1356,8 @@ async function handleStop(subArgs) {
1330
1356
  }
1331
1357
  let sessionId = flag(subArgs, "--session-id");
1332
1358
  if (!sessionId) {
1333
- const last = readLastSession();
1359
+ const workspace = flag(subArgs, "--workspace") || process.cwd();
1360
+ const last = readLastSession(workspace);
1334
1361
  if (!last) {
1335
1362
  console.error("No session specified. Use --session-id <id> or --server to stop the server.");
1336
1363
  process.exit(1);
@@ -1367,10 +1394,10 @@ async function handleWeb(subArgs) {
1367
1394
  }
1368
1395
 
1369
1396
  // src/commands/models.ts
1370
- var import_core5 = require("@ordewell/core");
1397
+ var import_core10 = require("@ordewell/core");
1371
1398
 
1372
1399
  // src/commands/model.ts
1373
- var import_core4 = require("@ordewell/core");
1400
+ var import_core9 = require("@ordewell/core");
1374
1401
 
1375
1402
  // src/catalog.ts
1376
1403
  function normalizeCatalog(result) {
@@ -1452,7 +1479,8 @@ function toTaskView(task) {
1452
1479
  dependencies: task.dependencies ?? [],
1453
1480
  assignedRunner: task.assignedRunner || void 0,
1454
1481
  taskMode: task.taskMode || void 0,
1455
- assignedModel: task.assignedModel ?? void 0
1482
+ assignedModel: task.assignedModel ?? void 0,
1483
+ subtasks: [...task.subtasks ?? []].sort((a, b) => a.order - b.order).map(toTaskView)
1456
1484
  };
1457
1485
  }
1458
1486
  function taskViews(plan) {
@@ -1475,7 +1503,7 @@ async function handleModel(subArgs, injectedApi) {
1475
1503
  if (!modelId) fail(USAGE);
1476
1504
  const settings = await api.getSettings();
1477
1505
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
1478
- const runner = (0, import_core4.runnerForProvider)(provider);
1506
+ const runner = (0, import_core9.runnerForProvider)(provider);
1479
1507
  if (runner) {
1480
1508
  const known = (await fetchCatalog(api)).models.filter((m) => m.runners?.includes(runner));
1481
1509
  if (known.length > 0 && !known.some((m) => m.id === modelId)) {
@@ -1494,7 +1522,7 @@ async function showModels(api) {
1494
1522
  const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
1495
1523
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
1496
1524
  const current = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
1497
- const runner = (0, import_core4.runnerForProvider)(provider);
1525
+ const runner = (0, import_core9.runnerForProvider)(provider);
1498
1526
  const options = runner ? catalog.models.filter((m) => m.runners?.includes(runner)) : catalog.orchestratorModels;
1499
1527
  console.log(`
1500
1528
  Planner model: ${current || "(backend default)"}`);
@@ -1528,8 +1556,8 @@ async function handleModels(subArgs) {
1528
1556
  const rest = subArgs.filter((_, i) => i !== setIdx && i !== setIdx + 1);
1529
1557
  return handleModel(["set", subArgs[setIdx + 1], ...rest]);
1530
1558
  }
1531
- const config = new import_core5.EnvConfig();
1532
- const providers = (0, import_core5.configuredProviders)(config);
1559
+ const config = new import_core10.EnvConfig();
1560
+ const providers = (0, import_core10.configuredProviders)(config);
1533
1561
  if (providers.length === 0) {
1534
1562
  console.error("No providers configured. Set an API key for a provider, e.g.:");
1535
1563
  console.error(" OPENROUTER_API_KEY=... (OpenRouter)");
@@ -1538,19 +1566,19 @@ async function handleModels(subArgs) {
1538
1566
  console.error(" OPENAI_COMPATIBLE_BASE_URL=... (self-hosted / OpenAI-compatible)");
1539
1567
  process.exit(1);
1540
1568
  }
1541
- const { apiKeys, baseUrls } = (0, import_core5.collectProviderCredentials)(config);
1569
+ const { apiKeys, baseUrls } = (0, import_core10.collectProviderCredentials)(config);
1542
1570
  let models;
1543
1571
  let errors;
1544
1572
  try {
1545
- const result = await (0, import_core5.fetchAllProviderModels)({ apiKeys, baseUrls });
1546
- models = (0, import_core5.toOrchestratorOptions)(result.models, import_core5.ORCHESTRATOR_SHORTCUTS);
1573
+ const result = await (0, import_core10.fetchAllProviderModels)({ apiKeys, baseUrls });
1574
+ models = (0, import_core10.toOrchestratorOptions)(result.models, import_core10.ORCHESTRATOR_SHORTCUTS);
1547
1575
  errors = result.errors;
1548
1576
  } catch (err) {
1549
1577
  console.error(`Failed to fetch models: ${err instanceof Error ? err.message : String(err)}`);
1550
1578
  process.exit(1);
1551
1579
  return;
1552
1580
  }
1553
- const providerLabel = (p) => (0, import_core5.getProviderMeta)(p)?.label ?? p;
1581
+ const providerLabel = (p) => (0, import_core10.getProviderMeta)(p)?.label ?? p;
1554
1582
  console.log(
1555
1583
  `
1556
1584
  Available models (${models.length}) across ${providers.length} configured provider(s):`
@@ -1667,9 +1695,9 @@ function handleSetup(subArgs) {
1667
1695
  }
1668
1696
 
1669
1697
  // src/commands/plugins.ts
1670
- var import_core6 = require("@ordewell/core");
1698
+ var import_core11 = require("@ordewell/core");
1671
1699
  function handlePlugins(subArgs) {
1672
- const registry = new import_core6.RunnerRegistry();
1700
+ const registry = new import_core11.RunnerRegistry();
1673
1701
  registry.loadUserPlugins();
1674
1702
  const sub = subArgs[0];
1675
1703
  if (sub === "list" || !sub) {
@@ -1705,7 +1733,7 @@ ${plugins.length} plugin(s):
1705
1733
  process.exit(1);
1706
1734
  }
1707
1735
  try {
1708
- const resolved = (0, import_core6.classifyPluginSource)(source);
1736
+ const resolved = (0, import_core11.classifyPluginSource)(source);
1709
1737
  const manifest = resolved.kind === "git" ? registry.installFromGit(resolved.url) : registry.installFromPath(resolved.path);
1710
1738
  console.log(`Installed plugin: ${manifest.name} v${manifest.version}`);
1711
1739
  } catch (err) {
@@ -1746,23 +1774,6 @@ ${plugins.length} plugin(s):
1746
1774
  process.exit(1);
1747
1775
  }
1748
1776
 
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
1777
  // src/commands/tdd.ts
1767
1778
  async function handleTdd(subArgs) {
1768
1779
  const action = subArgs[0];
@@ -1780,24 +1791,6 @@ async function handleTdd(subArgs) {
1780
1791
  }
1781
1792
  }
1782
1793
 
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
1794
  // src/commands/allowlist.ts
1802
1795
  var USAGE2 = `Usage:
1803
1796
  ordewell allowlist set <runner> <id1,id2,\u2026> Set allowlist for a runner
@@ -1884,23 +1877,6 @@ async function handleShow(args, api) {
1884
1877
  }
1885
1878
  }
1886
1879
 
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
1880
  // src/commands/verify.ts
1905
1881
  async function handleVerify(subArgs, api) {
1906
1882
  const action = subArgs[0];
@@ -1918,23 +1894,6 @@ async function handleVerify(subArgs, api) {
1918
1894
  }
1919
1895
  }
1920
1896
 
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
1897
  // src/commands/task-control.ts
1939
1898
  var PAST_TENSE = {
1940
1899
  run: "Task started.",
@@ -1943,11 +1902,12 @@ var PAST_TENSE = {
1943
1902
  cancel: "Task cancelled."
1944
1903
  };
1945
1904
  async function withResolvedTask(subArgs, usage, injectedApi, run) {
1946
- const last = readLastSession();
1905
+ const workspace = flag(subArgs, "--workspace") || process.cwd();
1947
1906
  let sessionId = flag(subArgs, "--session-id");
1948
1907
  if (!sessionId) {
1908
+ const last = readLastSession(workspace);
1949
1909
  if (!last) {
1950
- console.error("No session specified. Use --session-id <id> or run `ordewell plan` first.");
1910
+ console.error(`No session specified. Use --session-id <id> or run \`ordewell plan\` in ${workspace} first.`);
1951
1911
  process.exit(1);
1952
1912
  }
1953
1913
  sessionId = last.sessionId;
@@ -1958,22 +1918,12 @@ async function withResolvedTask(subArgs, usage, injectedApi, run) {
1958
1918
  process.exit(1);
1959
1919
  }
1960
1920
  const api = injectedApi || new ApiClient(await ensureDaemon(resolvePort(subArgs)));
1961
- const explicitWorkspace = flag(subArgs, "--workspace");
1962
1921
  let plan;
1963
1922
  try {
1964
- plan = (await api.getSession(sessionId, explicitWorkspace || process.cwd())).plan;
1923
+ plan = (await api.getSession(sessionId, workspace)).plan;
1965
1924
  } 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
- }
1925
+ console.error(`Failed to load session: ${err.message}`);
1926
+ process.exit(1);
1977
1927
  }
1978
1928
  const taskId = resolveTaskId(plan, identifier);
1979
1929
  if (!taskId) {
@@ -1986,7 +1936,7 @@ function makeHandler(action, command = action) {
1986
1936
  return async function handle(subArgs, injectedApi) {
1987
1937
  await withResolvedTask(
1988
1938
  subArgs,
1989
- `Usage: ordewell ${command} <task-id-or-order> [--session-id <id>]`,
1939
+ `Usage: ordewell ${command} <task-id-or-order> [--session-id <id>] [--workspace /path]`,
1990
1940
  injectedApi,
1991
1941
  async (api, sessionId, taskId) => {
1992
1942
  try {
@@ -2059,14 +2009,15 @@ async function handleSkip(subArgs, api) {
2059
2009
  async function handleAddTask(subArgs, injectedApi) {
2060
2010
  const title = flag(subArgs, "--title");
2061
2011
  if (!title) {
2062
- console.error('Usage: ordewell add-task --title "..." [--description "..."] [--prompt "..."] [--type ai|user] [--depends-on <task-id> ...] [--session-id <id>]');
2012
+ console.error('Usage: ordewell add-task --title "..." [--description "..."] [--prompt "..."] [--type ai|user] [--depends-on <task-id> ...] [--session-id <id>] [--workspace /path]');
2063
2013
  process.exit(1);
2064
2014
  }
2065
2015
  let sessionId = flag(subArgs, "--session-id");
2066
2016
  if (!sessionId) {
2067
- const last = readLastSession();
2017
+ const workspace = flag(subArgs, "--workspace") || process.cwd();
2018
+ const last = readLastSession(workspace);
2068
2019
  if (!last) {
2069
- console.error("No session specified. Use --session-id <id> or run `ordewell plan` first.");
2020
+ console.error(`No session specified. Use --session-id <id> or run \`ordewell plan\` in ${workspace} first.`);
2070
2021
  process.exit(1);
2071
2022
  }
2072
2023
  sessionId = last.sessionId;
@@ -2096,7 +2047,7 @@ async function handleAddTask(subArgs, injectedApi) {
2096
2047
  async function handleRemoveTask(subArgs, api) {
2097
2048
  await withResolvedTask(
2098
2049
  subArgs,
2099
- "Usage: ordewell remove-task <task-id-or-order> [--session-id <id>]",
2050
+ "Usage: ordewell remove-task <task-id-or-order> [--session-id <id>] [--workspace /path]",
2100
2051
  api,
2101
2052
  async (client, sessionId, taskId) => {
2102
2053
  try {
@@ -2127,7 +2078,7 @@ async function handleSessions(subArgs, injectedApi) {
2127
2078
  console.log(`No sessions found in ${workspace}.`);
2128
2079
  return;
2129
2080
  }
2130
- const last = readLastSession();
2081
+ const last = readLastSession(workspace);
2131
2082
  console.log(`${sessions.length} session(s) in ${workspace}:
2132
2083
  `);
2133
2084
  for (const s of sessions) {
@@ -2177,7 +2128,7 @@ async function handleSessions(subArgs, injectedApi) {
2177
2128
  }
2178
2129
 
2179
2130
  // src/commands/planner.ts
2180
- var import_core7 = require("@ordewell/core");
2131
+ var import_core12 = require("@ordewell/core");
2181
2132
 
2182
2133
  // src/plannerModelSwitch.ts
2183
2134
  function describePlannerSwitch(settings, provider) {
@@ -2196,7 +2147,7 @@ function rememberedModelFor(settings, provider) {
2196
2147
  }
2197
2148
 
2198
2149
  // src/commands/planner.ts
2199
- var KNOWN_PROVIDERS = Object.keys(import_core7.ALL_PROVIDERS);
2150
+ var KNOWN_PROVIDERS = Object.keys(import_core12.ALL_PROVIDERS);
2200
2151
  async function handlePlanner(subArgs, injectedApi) {
2201
2152
  const api = await connect(subArgs, injectedApi);
2202
2153
  const [wanted] = positionals(subArgs);
@@ -2211,13 +2162,13 @@ async function handlePlanner(subArgs, injectedApi) {
2211
2162
  const settings = await persistEnv(api, { AI_PROVIDER: provider });
2212
2163
  const recall2 = describePlannerSwitch(settings, provider);
2213
2164
  writeEnv({ ORCHESTRATOR_MODEL: recall2.model, ORDEWELL_PLANNER_EFFORT: recall2.effort });
2214
- const meta = import_core7.ALL_PROVIDERS[provider];
2165
+ const meta = import_core12.ALL_PROVIDERS[provider];
2215
2166
  const catalog = await fetchCatalog(api);
2216
2167
  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}.`
2168
+ (0, import_core12.isCliProvider)(provider) ? `Planning with ${meta.label} \u2014 no API key needed, it uses that agent's own subscription.` : `Planner set to ${meta.label}.`
2218
2169
  );
2219
2170
  console.log(plannerModelLine(recall2));
2220
- if (!(0, import_core7.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
2171
+ if (!(0, import_core12.isCliProvider)(provider) && !catalog.providers.includes(provider)) {
2221
2172
  console.log(` No ${meta.apiKeyEnvVar} configured yet \u2014 set one with \`ordewell key set ${provider} <key>\`.`);
2222
2173
  }
2223
2174
  }
@@ -2235,15 +2186,15 @@ async function showPlanners(api) {
2235
2186
  const [settings, catalog] = await Promise.all([api.getSettings(), fetchCatalog(api)]);
2236
2187
  const current = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
2237
2188
  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);
2189
+ for (const id of import_core12.CLI_PROVIDERS) {
2190
+ const runner = (0, import_core12.runnerForProvider)(id);
2240
2191
  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)"}`);
2192
+ console.log(` ${marker(id === current)} ${id.padEnd(14)} ${import_core12.ALL_PROVIDERS[id].label}${discovered ? "" : " (not detected)"}`);
2242
2193
  }
2243
2194
  console.log("\nAPI providers");
2244
- for (const id of import_core7.PROVIDER_PRIORITY) {
2195
+ for (const id of import_core12.PROVIDER_PRIORITY) {
2245
2196
  const configured = catalog.providers.includes(id);
2246
- const meta = import_core7.ALL_PROVIDERS[id];
2197
+ const meta = import_core12.ALL_PROVIDERS[id];
2247
2198
  console.log(` ${marker(id === current)} ${id.padEnd(14)} ${meta.label}${configured ? "" : ` (no ${meta.apiKeyEnvVar})`}`);
2248
2199
  }
2249
2200
  console.log(`
@@ -2260,7 +2211,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
2260
2211
  const provider = typeof settings.aiProvider === "string" ? settings.aiProvider : "";
2261
2212
  const modelId = typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : "";
2262
2213
  const currentEffort = typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : "";
2263
- const runner = (0, import_core7.runnerForProvider)(provider);
2214
+ const runner = (0, import_core12.runnerForProvider)(provider);
2264
2215
  if (!runner) {
2265
2216
  fail(
2266
2217
  "Thinking effort applies to a coding-agent planner.",
@@ -2300,7 +2251,7 @@ async function handlePlannerEffort(subArgs, injectedApi) {
2300
2251
  }
2301
2252
 
2302
2253
  // src/commands/key.ts
2303
- var import_core8 = require("@ordewell/core");
2254
+ var import_core13 = require("@ordewell/core");
2304
2255
  var USAGE4 = "Usage: ordewell key [list | set <provider> <key>]";
2305
2256
  async function handleKey(subArgs, injectedApi) {
2306
2257
  const [action, provider, ...keyParts] = positionals(subArgs);
@@ -2311,8 +2262,8 @@ async function handleKey(subArgs, injectedApi) {
2311
2262
  if (action !== "set") {
2312
2263
  const catalog = await fetchCatalog(api);
2313
2264
  console.log("\nAPI provider keys\n");
2314
- for (const id2 of import_core8.PROVIDER_PRIORITY) {
2315
- const meta2 = import_core8.ALL_PROVIDERS[id2];
2265
+ for (const id2 of import_core13.PROVIDER_PRIORITY) {
2266
+ const meta2 = import_core13.ALL_PROVIDERS[id2];
2316
2267
  const configured = catalog.providers.includes(id2);
2317
2268
  console.log(` ${configured ? "\u2713" : " "} ${id2.padEnd(14)} ${meta2.apiKeyEnvVar.padEnd(28)} ${meta2.label}`);
2318
2269
  }
@@ -2322,7 +2273,7 @@ async function handleKey(subArgs, injectedApi) {
2322
2273
  }
2323
2274
  if (!provider) fail(USAGE4);
2324
2275
  const id = provider.toLowerCase();
2325
- const meta = import_core8.ALL_PROVIDERS[id];
2276
+ const meta = import_core13.ALL_PROVIDERS[id];
2326
2277
  if (!meta) {
2327
2278
  fail(`Unknown provider: ${provider}`, "Run `ordewell key` to see the list.");
2328
2279
  }
@@ -2402,7 +2353,7 @@ async function handleApprove(subArgs, injectedApi) {
2402
2353
  // src/tui/terminalLauncher.ts
2403
2354
  var import_child_process3 = require("child_process");
2404
2355
  var import_util = require("util");
2405
- var import_core9 = require("@ordewell/core");
2356
+ var import_core14 = require("@ordewell/core");
2406
2357
  var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
2407
2358
  var defaultExecFile = async (command, args) => {
2408
2359
  const { stdout, stderr } = await execFileAsync(command, args);
@@ -2422,16 +2373,16 @@ var defaultWhich = (bin) => {
2422
2373
  async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
2423
2374
  const platform = deps.platform ?? process.platform;
2424
2375
  const env = deps.env ?? process.env;
2425
- const hasTmuxImpl = deps.hasTmuxImpl ?? import_core9.hasTmux;
2376
+ const hasTmuxImpl = deps.hasTmuxImpl ?? import_core14.hasTmux;
2426
2377
  const execFileImpl = deps.execFileImpl ?? defaultExecFile;
2427
2378
  const spawnImpl = deps.spawnImpl ?? defaultSpawn;
2428
2379
  const which = deps.which ?? defaultWhich;
2429
2380
  if (!hasTmuxImpl()) {
2430
2381
  return { ok: false, message: "tmux is not installed \u2014 install it to get a real terminal per task." };
2431
2382
  }
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);
2383
+ const session = (0, import_core14.tmuxSessionName)(port);
2384
+ const socket = (0, import_core14.tmuxSocketName)(port);
2385
+ const base = (0, import_core14.tmuxWindowName)(taskId, planSessionId);
2435
2386
  let windows;
2436
2387
  try {
2437
2388
  const { stdout } = await execFileImpl("tmux", ["-L", socket, "list-windows", "-t", session, "-F", "#{window_name}"]);
@@ -2725,7 +2676,7 @@ Dependencies \xB7 #${task.order} ${task.title}
2725
2676
  }
2726
2677
 
2727
2678
  // src/tui/index.ts
2728
- var import_core13 = require("@ordewell/core");
2679
+ var import_core18 = require("@ordewell/core");
2729
2680
 
2730
2681
  // src/tui/ansi.ts
2731
2682
  var import_string_width = __toESM(require("string-width"));
@@ -3030,12 +2981,8 @@ var SLASH_COMMANDS = [
3030
2981
  { name: "auto", usage: "/auto [on|off]", description: "Autonomous permission mode for new plans", category: "models" },
3031
2982
  { name: "refresh", usage: "/refresh", description: "Re-discover runners and model catalogs", category: "models" },
3032
2983
  // Skills
3033
- { name: "grill-me", usage: "/grill-me [on|off]", description: "Grill-Me challenge mode \u2014 the planner interrogates the goal", category: "skills" },
3034
2984
  { 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
2985
  { 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
2986
  // Sessions
3040
2987
  { name: "sessions", usage: "/sessions", description: "List saved sessions", category: "session" },
3041
2988
  { name: "new", usage: "/new", description: "Start a new session", category: "session" },
@@ -3048,6 +2995,23 @@ var SLASH_COMMANDS = [
3048
2995
  { name: "quit", usage: "/quit", description: "Exit the TUI", category: "system" }
3049
2996
  ];
3050
2997
  var BY_NAME = new Map(SLASH_COMMANDS.map((c) => [c.name, c]));
2998
+ var skillCommands = [];
2999
+ var skillByName = /* @__PURE__ */ new Map();
3000
+ function registerSkillCommands(skills) {
3001
+ const map = /* @__PURE__ */ new Map();
3002
+ for (const skill of skills) {
3003
+ if (BY_NAME.has(skill.name)) continue;
3004
+ map.set(skill.name, {
3005
+ name: skill.name,
3006
+ usage: `/${skill.name}`,
3007
+ description: skill.description,
3008
+ category: "skills",
3009
+ source: "skill"
3010
+ });
3011
+ }
3012
+ skillByName = map;
3013
+ skillCommands = [...map.values()];
3014
+ }
3051
3015
  function parseSlash(text) {
3052
3016
  const trimmed = text.trim();
3053
3017
  if (!trimmed.startsWith("/")) return null;
@@ -3056,14 +3020,15 @@ function parseSlash(text) {
3056
3020
  return { name: parts[0].toLowerCase(), args: parts.slice(1) };
3057
3021
  }
3058
3022
  function findCommand(name) {
3059
- return BY_NAME.get(name.replace(/^\//, "").toLowerCase());
3023
+ const key = name.replace(/^\//, "").toLowerCase();
3024
+ return BY_NAME.get(key) ?? skillByName.get(key);
3060
3025
  }
3061
3026
  function completions(text) {
3062
3027
  if (!text.startsWith("/")) return [];
3063
3028
  const typed = text.slice(1);
3064
3029
  if (/\s/.test(typed)) return [];
3065
3030
  const prefix = typed.toLowerCase();
3066
- return SLASH_COMMANDS.filter((c) => c.name.startsWith(prefix));
3031
+ return [...SLASH_COMMANDS, ...skillCommands].filter((c) => c.name.startsWith(prefix));
3067
3032
  }
3068
3033
 
3069
3034
  // src/tui/markdown.ts
@@ -3208,9 +3173,9 @@ function chatEditorRoom(cols, active) {
3208
3173
  }
3209
3174
 
3210
3175
  // src/tui/state.ts
3211
- var SKILL_IDS = ["grill-me", "tdd", "prd", "review", "verify", "research-subagents"];
3176
+ var SKILL_IDS = ["tdd", "verify"];
3212
3177
  function noSkills() {
3213
- return { "grill-me": false, tdd: false, prd: false, review: false, verify: false, "research-subagents": false };
3178
+ return { tdd: false, verify: false };
3214
3179
  }
3215
3180
  function visibleItems(picker2) {
3216
3181
  const filter = picker2.filter.trim().toLowerCase();
@@ -3221,6 +3186,29 @@ function visibleItems(picker2) {
3221
3186
  }
3222
3187
  var isTaskRunning = (task) => task.status === "in_progress" || task.status === "running";
3223
3188
  var anyTaskRunning = (state) => state.tasks.some(isTaskRunning);
3189
+ function planRows(state) {
3190
+ const rows = [];
3191
+ for (const task of state.tasks) {
3192
+ rows.push({ task, parent: null });
3193
+ const subtasks = task.subtasks ?? [];
3194
+ const open = state.expandedTaskId === task.id || subtasks.some((s) => s.id === state.expandedTaskId);
3195
+ if (open) {
3196
+ for (const subtask of subtasks) rows.push({ task: subtask, parent: task });
3197
+ }
3198
+ }
3199
+ return rows;
3200
+ }
3201
+ function selectedPlanRow(state) {
3202
+ return planRows(state)[state.selectedTask] ?? null;
3203
+ }
3204
+ function findTask(tasks, id) {
3205
+ for (const task of tasks) {
3206
+ if (task.id === id) return task;
3207
+ const found = findTask(task.subtasks ?? [], id);
3208
+ if (found) return found;
3209
+ }
3210
+ return void 0;
3211
+ }
3224
3212
  function initialState(overrides = {}) {
3225
3213
  return {
3226
3214
  editor: emptyEditor(),
@@ -3266,7 +3254,7 @@ function initialState(overrides = {}) {
3266
3254
  }
3267
3255
 
3268
3256
  // src/tui/layout.ts
3269
- var import_core10 = require("@ordewell/core");
3257
+ var import_core15 = require("@ordewell/core");
3270
3258
  var CHROME_ROWS = 4;
3271
3259
  function chatInputWrap(state) {
3272
3260
  const active = state.focus === "chat" && !state.overlay;
@@ -3274,11 +3262,11 @@ function chatInputWrap(state) {
3274
3262
  return wrapLines(state.editor.text, chatEditorRoom(state.cols, true));
3275
3263
  }
3276
3264
  function footerHints(state) {
3277
- const markHint = state.tasks[state.selectedTask]?.status === "completed" ? "m undone" : "m done";
3265
+ const markHint = selectedPlanRow(state)?.task.status === "completed" ? "m undone" : "m done";
3278
3266
  const planning = state.status === "planning" || state.status === "researching";
3279
3267
  const escHint = planning ? "esc stop planning" : null;
3280
3268
  if (state.focus === "plan") {
3281
- if (state.expandedTaskId) {
3269
+ if (state.taskEditor) {
3282
3270
  return ["type to edit prompt", "pgup/pgdn scroll", "alt-enter newline", "enter save", "esc cancel"];
3283
3271
  }
3284
3272
  return [
@@ -3402,9 +3390,9 @@ function chatScrollMax(state) {
3402
3390
  }
3403
3391
  function plannerSetup(state) {
3404
3392
  const provider = state.plannerProvider;
3405
- const registration = import_core10.ALL_PROVIDERS[provider];
3393
+ const registration = import_core15.ALL_PROVIDERS[provider];
3406
3394
  if (!registration) return { label: "not set", ready: false };
3407
- const runner = (0, import_core10.runnerForProvider)(provider);
3395
+ const runner = (0, import_core15.runnerForProvider)(provider);
3408
3396
  const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
3409
3397
  return { label: registration.label, ready };
3410
3398
  }
@@ -3511,16 +3499,17 @@ var STATUS_ICON = {
3511
3499
  approved: "\xB7",
3512
3500
  pending: "\xB7"
3513
3501
  };
3502
+ var IDLE_ICON = "~";
3514
3503
  function planLayout(state, rows, cols) {
3515
3504
  const done = state.tasks.filter((t) => t.status === "completed").length;
3516
3505
  const lines = [style.bold(`Plan ${done}/${state.tasks.length}`), ""];
3517
3506
  let selectedLine = 2;
3518
3507
  let editorLine;
3519
- state.tasks.forEach((task, i) => {
3508
+ planRows(state).forEach((row, i) => {
3520
3509
  const taskStart = lines.length;
3521
3510
  if (i === state.selectedTask) selectedLine = taskStart;
3522
- const renderedTask = taskLines(state, task, i, cols);
3523
- if (task.id === state.expandedTaskId && renderedTask.editorLine !== void 0) {
3511
+ const renderedTask = taskLines(state, row, i, cols);
3512
+ if (row.task.id === state.expandedTaskId && renderedTask.editorLine !== void 0) {
3524
3513
  editorLine = taskStart + renderedTask.editorLine;
3525
3514
  }
3526
3515
  lines.push(...renderedTask.lines);
@@ -3539,16 +3528,20 @@ function planScrollExtent(state) {
3539
3528
  const { followOffset, maxScroll } = planLayout(state, bodyRows(state), cols);
3540
3529
  return { followOffset, maxScroll };
3541
3530
  }
3542
- function taskLines(state, task, index, cols) {
3531
+ function taskLines(state, row, index, cols) {
3532
+ const { task, parent } = row;
3543
3533
  const selected = state.focus === "plan" && index === state.selectedTask;
3544
3534
  const expanded = state.expandedTaskId === task.id;
3545
3535
  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";
3536
+ const idle = running && task.status === "in_progress" && task.idleSince != null;
3537
+ const paint = idle ? style.cyan : STATUS_MARK[task.status] ?? ((t) => t);
3538
+ const rawIcon = idle ? IDLE_ICON : running ? SPINNER[state.spinnerFrame % SPINNER.length] : STATUS_ICON[task.status] ?? "\xB7";
3548
3539
  const icon = paint(rawIcon);
3549
3540
  const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : style.grey(" AI");
3550
3541
  const caret = selected ? style.cyan("\u276F") : " ";
3551
- const head = `${caret} ${icon} ${String(task.order).padStart(2)} ${kind} `;
3542
+ const orderLabel = parent ? (0, import_core15.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
3543
+ const indent = parent ? " " : "";
3544
+ const head = `${indent}${caret} ${icon} ${orderLabel} ${kind} `;
3552
3545
  const titleRoom = Math.max(1, cols - width(head));
3553
3546
  const wrappedTitle = wrap(task.title, titleRoom);
3554
3547
  const shownTitle = expanded ? wrappedTitle : wrappedTitle.slice(0, 2);
@@ -3566,50 +3559,51 @@ function taskLines(state, task, index, cols) {
3566
3559
  const modeInfo = task.type === "ai" ? modesForTask(state.modesByRunner, task).find((m) => m.id === task.taskMode) : void 0;
3567
3560
  const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}${modeInfo?.autonomous ? style.yellow(" \u26A1") : ""}` : "";
3568
3561
  const runner = task.assignedRunner ?? "";
3562
+ const bodyPad = parent ? " " : " ";
3569
3563
  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)));
3564
+ if (meta) lines.push(style.grey(truncate(`${bodyPad}${meta}`, cols)));
3565
+ if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
3572
3566
  let editorLine;
3573
3567
  if (expanded) {
3574
- lines.push(style.grey(` ${task.status.replace(/_/g, " ")}`));
3568
+ lines.push(style.grey(`${bodyPad}${task.status.replace(/_/g, " ")}`));
3575
3569
  if (modeInfo?.autonomous) {
3576
- lines.push(...taskText("Autonomy", "Runs without permission prompts. Toggle with /auto.", cols));
3570
+ lines.push(...taskText("Autonomy", "Runs without permission prompts. Toggle with /auto.", cols, bodyPad));
3577
3571
  }
3578
3572
  const editableSeed = task.prompt ?? task.description ?? task.title;
3579
3573
  if (task.description && task.description !== task.title && task.description !== editableSeed) {
3580
- lines.push(...taskText("Description", task.description, cols));
3574
+ lines.push(...taskText("Description", task.description, cols, bodyPad));
3581
3575
  }
3582
3576
  if (state.taskEditor) {
3583
- const room = paneTextRoom(cols);
3577
+ const room = Math.max(1, cols - width(bodyPad));
3584
3578
  const wrapped = wrapLines(state.taskEditor.text, room);
3585
3579
  const cp = cursorInLines(wrapped, state.taskEditor.cursor, state.taskEditor.text.length);
3586
3580
  editorLine = lines.length + 1 + cp.line;
3587
- lines.push(...taskPromptLines(wrapped, cp));
3581
+ lines.push(...taskPromptLines(wrapped, cp, bodyPad));
3588
3582
  }
3589
3583
  if (task.dependencies.length > 0) {
3590
3584
  const orderById = new Map(state.tasks.map((candidate) => [candidate.id, candidate.order]));
3591
3585
  const dependencies = task.dependencies.map((id) => orderById.has(id) ? `#${orderById.get(id)}` : id).join(", ");
3592
- lines.push(...taskText("Depends on", dependencies, cols));
3586
+ lines.push(...taskText("Depends on", dependencies, cols, bodyPad));
3593
3587
  }
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)));
3588
+ lines.push(style.grey(`${bodyPad}enter save \xB7 esc cancel`));
3589
+ lines.push(style.grey(truncate(`${bodyPad}then R runner \xB7 o model \xB7 e effort \xB7 M mode \xB7 D deps`, cols)));
3596
3590
  }
3597
3591
  return { lines, editorLine };
3598
3592
  }
3599
- function taskText(label, text, cols) {
3600
- const room = paneTextRoom(cols);
3593
+ function taskText(label, text, cols, pad2 = " ") {
3594
+ const room = Math.max(1, cols - width(pad2));
3601
3595
  return [
3602
- style.grey(` ${label}`),
3603
- ...wrap(text, room).map((line) => ` ${line}`)
3596
+ style.grey(`${pad2}${label}`),
3597
+ ...wrap(text, room).map((line) => `${pad2}${line}`)
3604
3598
  ];
3605
3599
  }
3606
- function taskPromptLines(wrapped, cursor) {
3600
+ function taskPromptLines(wrapped, cursor, pad2 = " ") {
3607
3601
  return [
3608
- style.grey(" Prompt"),
3602
+ style.grey(`${pad2}Prompt`),
3609
3603
  ...wrapped.map(({ line }, i) => {
3610
- if (i !== cursor.line) return ` ${line}`;
3604
+ if (i !== cursor.line) return `${pad2}${line}`;
3611
3605
  const col = cursor.col;
3612
- return ` ${line.slice(0, col)}${style.inverse(line.slice(col, col + 1) || " ")}${line.slice(col + 1)}`;
3606
+ return `${pad2}${line.slice(0, col)}${style.inverse(line.slice(col, col + 1) || " ")}${line.slice(col + 1)}`;
3613
3607
  })
3614
3608
  ];
3615
3609
  }
@@ -3762,15 +3756,29 @@ function renderStatus(state, cols) {
3762
3756
  const tail = thinking.length > room ? `\u2026${thinking.slice(-(room - 1)).trimStart()}` : thinking;
3763
3757
  return truncate(`${head}${style.grey(` \xB7 ${tail}`)}`, cols);
3764
3758
  }
3759
+ var LEADING_TOKEN = /^\/\S*/;
3760
+ function skillTokenLength(text) {
3761
+ const match = LEADING_TOKEN.exec(text);
3762
+ if (!match) return 0;
3763
+ return findCommand(match[0])?.source === "skill" ? match[0].length : 0;
3764
+ }
3765
+ function paintRange(text, base, tokenEnd) {
3766
+ const localEnd = Math.max(0, Math.min(text.length, tokenEnd - base));
3767
+ if (localEnd <= 0) return text;
3768
+ if (localEnd >= text.length) return style.cyan(text);
3769
+ return style.cyan(text.slice(0, localEnd)) + text.slice(localEnd);
3770
+ }
3765
3771
  function renderInput(state, cols) {
3766
3772
  const prompt = state.focus === "plan" ? style.grey("\u276F ") : style.cyan("\u276F ");
3767
3773
  const lines = chatInputWrap(state);
3768
3774
  const room = chatEditorRoom(cols, lines !== null);
3769
3775
  const { text, cursor } = state.editor;
3770
3776
  const continuation = " ".repeat(width(prompt));
3777
+ const tokenEnd = skillTokenLength(text);
3771
3778
  if (!lines) {
3772
3779
  const visible = text.replace(/\n/g, "\xB6").replace(/\t/g, " ");
3773
- return [prompt + visible + suggest(state, room - width(visible))];
3780
+ const painted = tokenEnd > 0 ? paintRange(visible, 0, tokenEnd) : visible;
3781
+ return [prompt + painted + suggest(state, room - width(visible))];
3774
3782
  }
3775
3783
  const { line: lineIndex, col } = cursorInLines(lines, cursor, text.length);
3776
3784
  const rawLines = lines.map((l) => l.line);
@@ -3778,7 +3786,8 @@ function renderInput(state, cols) {
3778
3786
  const prefix = i === 0 ? prompt : continuation;
3779
3787
  const line = raw.replace(/\t/g, " ");
3780
3788
  const isCursorLine = i === lineIndex;
3781
- const rendered = isCursorLine ? line.slice(0, col) + style.inverse(line.slice(col, col + 1) || " ") + line.slice(col + 1) : line;
3789
+ const lineTokenEnd = i === 0 ? tokenEnd : 0;
3790
+ 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
3791
  const used = width(line) + (isCursorLine && col >= line.length ? 1 : 0);
3783
3792
  const hint = i === rawLines.length - 1 ? suggest(state, room - used) : "";
3784
3793
  return prefix + rendered + hint;
@@ -3908,15 +3917,21 @@ function frame(title, body, rows, cols) {
3908
3917
  }
3909
3918
 
3910
3919
  // src/tui/reducer.ts
3911
- var import_core11 = require("@ordewell/core");
3920
+ var import_core16 = require("@ordewell/core");
3912
3921
  var import_plan_utils3 = require("@ordewell/core/plan-utils");
3913
- var KNOWN_PROVIDERS2 = Object.keys(import_core11.ALL_PROVIDERS);
3922
+ var KNOWN_PROVIDERS2 = Object.keys(import_core16.ALL_PROVIDERS);
3914
3923
  var DEFAULT_EFFORT = "__runner_default__";
3915
3924
  function step(state, effects = []) {
3916
3925
  return { state, effects };
3917
3926
  }
3918
- function say(state, role, content, research) {
3919
- const message = { role, content: sanitize(content), timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...research ? { research } : {} };
3927
+ function say(state, role, content, research, extra) {
3928
+ const message = {
3929
+ role,
3930
+ content: sanitize(content),
3931
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3932
+ ...research ? { research } : {},
3933
+ ...extra
3934
+ };
3920
3935
  return { ...state, messages: [...state.messages, message], scroll: 0 };
3921
3936
  }
3922
3937
  function restoredMessages(history) {
@@ -3986,24 +4001,42 @@ function reduce(state, action) {
3986
4001
  case "planUpdated": {
3987
4002
  if (stale(state, action.sessionId)) return step(state);
3988
4003
  const tasks = normalizeTasks(action.plan);
3989
- return step({
4004
+ const expandedTaskId = state.expandedTaskId && findTask(tasks, state.expandedTaskId) ? state.expandedTaskId : null;
4005
+ const next = {
3990
4006
  ...state,
3991
4007
  tasks,
3992
4008
  status: state.status === "planning" || state.status === "researching" ? "idle" : state.status,
3993
4009
  busyLabel: "",
3994
4010
  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
- });
4011
+ expandedTaskId,
4012
+ taskEditor: expandedTaskId !== null ? state.taskEditor : null
4013
+ };
4014
+ return step({ ...next, selectedTask: Math.min(state.selectedTask, Math.max(0, planRows(next).length - 1)) });
3999
4015
  }
4000
4016
  case "plannerMessage": {
4001
4017
  if (stale(state, action.sessionId)) return step(state);
4002
4018
  const settled = { ...state, status: "idle", busyLabel: "", thinkingLine: "" };
4003
4019
  const content = sanitize(action.content);
4020
+ const last = state.messages.at(-1);
4021
+ if (last?.role === "assistant" && last.streaming) {
4022
+ const messages = [...state.messages];
4023
+ messages[messages.length - 1] = { ...last, content, streaming: void 0 };
4024
+ return step({ ...settled, messages });
4025
+ }
4004
4026
  if (alreadySpoken(state.messages, content)) return step(settled);
4005
4027
  return step(say(settled, "assistant", content));
4006
4028
  }
4029
+ case "plannerToken": {
4030
+ if (stale(state, action.sessionId)) return step(state);
4031
+ const text = sanitize(action.text);
4032
+ const last = state.messages.at(-1);
4033
+ if (last?.role === "assistant" && last.streaming) {
4034
+ const messages = [...state.messages];
4035
+ messages[messages.length - 1] = { ...last, content: last.content + text };
4036
+ return step({ ...state, messages });
4037
+ }
4038
+ return step(say(state, "assistant", text, void 0, { streaming: true }));
4039
+ }
4007
4040
  case "researchStep": {
4008
4041
  if (stale(state, action.sessionId)) return step(state);
4009
4042
  const summary = sanitize(action.summary);
@@ -4040,10 +4073,12 @@ function reduce(state, action) {
4040
4073
  const updates = action.updates;
4041
4074
  let changed = false;
4042
4075
  const tasks = state.tasks.map((t) => {
4043
- const status = updates[t.id];
4044
- if (status !== void 0 && status !== t.status) {
4076
+ const update = updates[t.id];
4077
+ if (update === void 0) return t;
4078
+ const idleSince = update.idleSince ?? null;
4079
+ if (update.status !== t.status || idleSince !== (t.idleSince ?? null)) {
4045
4080
  changed = true;
4046
- return { ...t, status };
4081
+ return { ...t, status: update.status, idleSince };
4047
4082
  }
4048
4083
  return t;
4049
4084
  });
@@ -4125,9 +4160,12 @@ function normalizeTasks(plan) {
4125
4160
  }
4126
4161
  }
4127
4162
  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,
4163
+ return raw.map((t, i) => toTaskView2(t, i));
4164
+ }
4165
+ function toTaskView2(t, index) {
4166
+ return {
4167
+ id: String(t.id ?? `task-${index + 1}`),
4168
+ order: typeof t.order === "number" ? t.order : index + 1,
4131
4169
  title: planLabel(t.title, "Untitled task"),
4132
4170
  description: planText(t.description ?? t.title),
4133
4171
  prompt: typeof t.prompt === "string" ? sanitize(t.prompt) : void 0,
@@ -4141,18 +4179,15 @@ function normalizeTasks(plan) {
4141
4179
  modelLabel: planLabel(t.assignedModel.modelLabel ?? t.assignedModel.modelId),
4142
4180
  thinkingEffort: typeof t.assignedModel.thinkingEffort === "string" ? planLabel(t.assignedModel.thinkingEffort) : typeof t.thinkingEffort === "string" ? planLabel(t.thinkingEffort) : void 0,
4143
4181
  availableVariants: Array.isArray(t.assignedModel.availableVariants) ? t.assignedModel.availableVariants.map((variant) => planLabel(variant)) : void 0
4144
- } : void 0
4145
- }));
4182
+ } : void 0,
4183
+ subtasks: Array.isArray(t.subtasks) ? [...t.subtasks].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).map((s, i) => toTaskView2(s, i)) : void 0
4184
+ };
4146
4185
  }
4147
4186
  var enabledFlag = (value) => typeof value?.enabled === "boolean" ? value.enabled : void 0;
4148
4187
  function applySettings(state, settings) {
4149
4188
  const sources = {
4150
- "grill-me": settings.grillMe,
4151
4189
  tdd: settings.tdd,
4152
- prd: settings.prd,
4153
- review: settings.review,
4154
- verify: settings.verification,
4155
- "research-subagents": settings.researchSubagents
4190
+ verify: settings.verification
4156
4191
  };
4157
4192
  const skills = { ...state.skills };
4158
4193
  for (const id of SKILL_IDS) {
@@ -4181,11 +4216,11 @@ function modelDetail(m) {
4181
4216
  function providerErrorHint(state) {
4182
4217
  const failed = Object.keys(state.providerErrors ?? {});
4183
4218
  if (failed.length === 0) return void 0;
4184
- const names = failed.map((p) => import_core11.ALL_PROVIDERS[p]?.label ?? p);
4219
+ const names = failed.map((p) => import_core16.ALL_PROVIDERS[p]?.label ?? p);
4185
4220
  return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
4186
4221
  }
4187
4222
  function plannerModelItems(state) {
4188
- const runner = (0, import_core11.runnerForProvider)(state.plannerProvider);
4223
+ const runner = (0, import_core16.runnerForProvider)(state.plannerProvider);
4189
4224
  if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
4190
4225
  const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
4191
4226
  id: m.id,
@@ -4196,7 +4231,7 @@ function plannerModelItems(state) {
4196
4231
  return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
4197
4232
  }
4198
4233
  function plannerEffortItems(state) {
4199
- const runner = (0, import_core11.runnerForProvider)(state.plannerProvider);
4234
+ const runner = (0, import_core16.runnerForProvider)(state.plannerProvider);
4200
4235
  if (!runner) {
4201
4236
  return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
4202
4237
  }
@@ -4405,25 +4440,32 @@ function markAction(task) {
4405
4440
  return task.status === "completed" ? "uncomplete" : "complete";
4406
4441
  }
4407
4442
  function handlePlanKey(state, key) {
4408
- const editingTask = state.expandedTaskId ? state.tasks.find((t) => t.id === state.expandedTaskId) : void 0;
4443
+ const editingTask = state.expandedTaskId ? findTask(state.tasks, state.expandedTaskId) : void 0;
4409
4444
  if (editingTask && state.taskEditor) return handleTaskEditKey(state, editingTask, state.taskEditor, key);
4410
- if (key.name === "escape") return step({ ...state, focus: "chat" });
4445
+ if (key.name === "escape") {
4446
+ if (state.expandedTaskId) return step({ ...state, expandedTaskId: null, planScroll: null });
4447
+ return step({ ...state, focus: "chat" });
4448
+ }
4411
4449
  if (key.name === "up") {
4412
- return step({ ...state, selectedTask: Math.max(0, state.selectedTask - 1), expandedTaskId: null, planScroll: null });
4450
+ return step({ ...state, selectedTask: Math.max(0, state.selectedTask - 1), planScroll: null });
4413
4451
  }
4414
4452
  if (key.name === "down") {
4415
4453
  return step({
4416
4454
  ...state,
4417
- selectedTask: Math.min(state.tasks.length - 1, state.selectedTask + 1),
4418
- expandedTaskId: null,
4455
+ selectedTask: Math.min(planRows(state).length - 1, state.selectedTask + 1),
4419
4456
  planScroll: null
4420
4457
  });
4421
4458
  }
4422
4459
  const scroll = scrollDelta(key, state);
4423
4460
  if (scroll !== null) return scrollPlan(state, scroll);
4424
- const task = state.tasks[state.selectedTask];
4425
- if (!task) return step(state);
4461
+ const row = selectedPlanRow(state);
4462
+ if (!row) return step(state);
4463
+ const task = row.task;
4426
4464
  if (key.name === "enter" || key.name === "right") {
4465
+ const hasSubtasks = (task.subtasks?.length ?? 0) > 0;
4466
+ if (hasSubtasks && state.expandedTaskId !== task.id) {
4467
+ return step({ ...state, expandedTaskId: task.id, taskEditor: null, planScroll: null });
4468
+ }
4427
4469
  const text = task.prompt ?? task.description ?? task.title;
4428
4470
  return step({
4429
4471
  ...state,
@@ -4672,7 +4714,7 @@ function submit(state) {
4672
4714
  if (!text) return step(state);
4673
4715
  const cleared = { ...state, editor: commit(state.editor) };
4674
4716
  const command = parseSlash(text);
4675
- if (command) return runCommand(cleared, command);
4717
+ if (command && findCommand(command.name)?.source !== "skill") return runCommand(cleared, command);
4676
4718
  const spoken = say(cleared, "user", text);
4677
4719
  const effect = state.sessionId ? { type: "sendMessage", sessionId: state.sessionId, message: text } : { type: "startConversation", goal: text };
4678
4720
  return step({ ...spoken, status: "planning" }, [effect]);
@@ -4834,7 +4876,7 @@ function newSession(state) {
4834
4876
  }
4835
4877
  function setModel(state, args) {
4836
4878
  if (args[0] === "set" && args[1]) {
4837
- const runner = (0, import_core11.runnerForProvider)(state.plannerProvider);
4879
+ const runner = (0, import_core16.runnerForProvider)(state.plannerProvider);
4838
4880
  const known = runner ? modelsForRunner(state.models, runner) : [];
4839
4881
  if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
4840
4882
  return fail2(state, `${args[1]} was not discovered for ${runner}.`);
@@ -4856,21 +4898,21 @@ function setModel(state, args) {
4856
4898
  }
4857
4899
  function plannerItems(state) {
4858
4900
  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);
4901
+ const agents = import_core16.CLI_PROVIDERS.map((id) => {
4902
+ const runner = (0, import_core16.runnerForProvider)(id);
4861
4903
  const ready = installed.has(runner);
4862
4904
  return {
4863
4905
  id,
4864
- label: import_core11.ALL_PROVIDERS[id].label,
4906
+ label: import_core16.ALL_PROVIDERS[id].label,
4865
4907
  detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
4866
4908
  selected: state.plannerProvider === id,
4867
4909
  disabled: !ready
4868
4910
  };
4869
4911
  });
4870
- const vendors = import_core11.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
4912
+ const vendors = import_core16.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
4871
4913
  id,
4872
- label: import_core11.ALL_PROVIDERS[id].label,
4873
- detail: import_core11.ALL_PROVIDERS[id].apiKeyEnvVar,
4914
+ label: import_core16.ALL_PROVIDERS[id].label,
4915
+ detail: import_core16.ALL_PROVIDERS[id].apiKeyEnvVar,
4874
4916
  selected: state.plannerProvider === id
4875
4917
  }));
4876
4918
  return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
@@ -4928,16 +4970,16 @@ function setKey(state, args) {
4928
4970
  }
4929
4971
  return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
4930
4972
  }
4931
- const items = import_core11.PROVIDER_PRIORITY.map((id) => ({
4973
+ const items = import_core16.PROVIDER_PRIORITY.map((id) => ({
4932
4974
  id,
4933
- label: import_core11.ALL_PROVIDERS[id].label,
4934
- detail: import_core11.ALL_PROVIDERS[id].apiKeyEnvVar,
4975
+ label: import_core16.ALL_PROVIDERS[id].label,
4976
+ detail: import_core16.ALL_PROVIDERS[id].apiKeyEnvVar,
4935
4977
  selected: state.configuredProviders.includes(id)
4936
4978
  }));
4937
4979
  return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
4938
4980
  }
4939
4981
  function keyPrompt(provider) {
4940
- const meta = import_core11.ALL_PROVIDERS[provider];
4982
+ const meta = import_core16.ALL_PROVIDERS[provider];
4941
4983
  return {
4942
4984
  kind: "prompt",
4943
4985
  title: `${meta.label} API key`,
@@ -5295,7 +5337,7 @@ function createApp(deps) {
5295
5337
 
5296
5338
  // src/tui/effects.ts
5297
5339
  var import_child_process4 = require("child_process");
5298
- var import_core12 = require("@ordewell/core");
5340
+ var import_core17 = require("@ordewell/core");
5299
5341
  var import_plan_utils4 = require("@ordewell/core/plan-utils");
5300
5342
  var ConversationQueue = class {
5301
5343
  tail = Promise.resolve();
@@ -5462,7 +5504,7 @@ async function perform(effect, deps) {
5462
5504
  dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
5463
5505
  return;
5464
5506
  case "setPlanner": {
5465
- const meta = import_core12.ALL_PROVIDERS[effect.provider];
5507
+ const meta = import_core17.ALL_PROVIDERS[effect.provider];
5466
5508
  if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
5467
5509
  const env = { AI_PROVIDER: effect.provider };
5468
5510
  const settings = await api.updateSettings({ env });
@@ -5474,7 +5516,7 @@ async function perform(effect, deps) {
5474
5516
  });
5475
5517
  dispatch({
5476
5518
  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)}`
5519
+ message: `${(0, import_core17.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
5478
5520
  });
5479
5521
  await loadModels(deps);
5480
5522
  return;
@@ -5486,7 +5528,7 @@ async function perform(effect, deps) {
5486
5528
  dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
5487
5529
  return;
5488
5530
  case "setApiKey": {
5489
- const meta = import_core12.ALL_PROVIDERS[effect.provider];
5531
+ const meta = import_core17.ALL_PROVIDERS[effect.provider];
5490
5532
  if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
5491
5533
  await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
5492
5534
  deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
@@ -5574,7 +5616,7 @@ function osc52(text) {
5574
5616
  function copySelection(deps, text) {
5575
5617
  if (!text) return;
5576
5618
  const lines = text.split("\n").length;
5577
- const command = (0, import_core12.clipboardCopyCommand)(deps.hasBin);
5619
+ const command = (0, import_core17.clipboardCopyCommand)(deps.hasBin);
5578
5620
  if (command) {
5579
5621
  try {
5580
5622
  (deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
@@ -5607,7 +5649,28 @@ async function withExecutionStream(deps, sessionId, request) {
5607
5649
  }
5608
5650
  async function converse(deps, sessionId, call) {
5609
5651
  let streamed = null;
5652
+ const TOKEN_DEBOUNCE_MS = 75;
5653
+ let tokenBuffer = "";
5654
+ let tokenTimer = null;
5655
+ const flushTokens = () => {
5656
+ if (tokenTimer !== null) {
5657
+ clearTimeout(tokenTimer);
5658
+ tokenTimer = null;
5659
+ }
5660
+ if (!tokenBuffer) return;
5661
+ const text = tokenBuffer;
5662
+ tokenBuffer = "";
5663
+ deps.dispatch({ type: "plannerToken", text, sessionId });
5664
+ };
5610
5665
  const stream = deps.api.streamPlanning(sessionId, (event) => {
5666
+ if (event?.type === "plan_token" && event.token) {
5667
+ tokenBuffer += event.token;
5668
+ if (tokenTimer === null) {
5669
+ tokenTimer = setTimeout(flushTokens, TOKEN_DEBOUNCE_MS);
5670
+ }
5671
+ return;
5672
+ }
5673
+ flushTokens();
5611
5674
  if (event?.type === "approval_request") {
5612
5675
  deps.dispatch({
5613
5676
  type: "approvalRequested",
@@ -5664,6 +5727,7 @@ async function converse(deps, sessionId, call) {
5664
5727
  deps.dispatch({ type: "plannerMessage", content: question, sessionId });
5665
5728
  }
5666
5729
  } finally {
5730
+ if (tokenTimer !== null) clearTimeout(tokenTimer);
5667
5731
  stream.close();
5668
5732
  }
5669
5733
  }
@@ -5683,7 +5747,7 @@ function onExecutionEvent(dispatch, event, sessionId) {
5683
5747
  case "status_update": {
5684
5748
  const updates = {};
5685
5749
  for (const task of event.tasks ?? []) {
5686
- updates[String(task.id)] = String(task.status);
5750
+ updates[String(task.id)] = { status: String(task.status), idleSince: task.idleSince ?? null };
5687
5751
  }
5688
5752
  dispatch({ type: "tasksStatus", updates, sessionId });
5689
5753
  return;
@@ -5694,7 +5758,7 @@ function onExecutionEvent(dispatch, event, sessionId) {
5694
5758
  // The extension shows these in its checkpoint panel; here they are
5695
5759
  // transcript lines, which is the TUI's equivalent surface.
5696
5760
  case "checkpoint":
5697
- dispatch({ type: "notice", message: `Checkpoint \u2014 ${event.taskTitle}: ${event.summary}` });
5761
+ dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core17.truncateCheckpointSummary)(event.summary)}` });
5698
5762
  return;
5699
5763
  case "approval_decided":
5700
5764
  dispatch({ type: "notice", message: describeApprovalDecision2(event) });
@@ -6047,7 +6111,7 @@ async function handleTui(subArgs) {
6047
6111
  }
6048
6112
  const workspace = flag(subArgs, "--workspace") || process.cwd();
6049
6113
  try {
6050
- (0, import_core13.assertWorkspaceExists)(workspace);
6114
+ (0, import_core18.assertWorkspaceExists)(workspace);
6051
6115
  } catch {
6052
6116
  console.error(
6053
6117
  `Workspace "${workspace}" does not exist.
@@ -6056,7 +6120,10 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
6056
6120
  );
6057
6121
  process.exit(1);
6058
6122
  }
6059
- const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(resolvePort(subArgs), { detached: false });
6123
+ registerSkillCommands((0, import_core18.createSkillsService)(workspace).listSkills());
6124
+ const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
6125
+ const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
6126
+ const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
6060
6127
  let owned = ownedAtLaunch;
6061
6128
  const api = new ApiClient(port);
6062
6129
  const conversationQueue = new ConversationQueue();
@@ -6072,7 +6139,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
6072
6139
  conversationQueue,
6073
6140
  port,
6074
6141
  dispatch: (action) => app.dispatch(action),
6075
- newSessionId: () => (0, import_core13.mintSessionId)(),
6142
+ newSessionId: () => (0, import_core18.mintSessionId)(),
6076
6143
  setEnvVar: (key, value) => {
6077
6144
  writeEnvVar(findEnvFile(), key, value);
6078
6145
  process.env[key] = value;
@@ -6141,14 +6208,8 @@ var COMMANDS = {
6141
6208
  refresh: handleRefresh,
6142
6209
  setup: handleSetup,
6143
6210
  plugins: handlePlugins,
6144
- "grill-me": handleGrillMe,
6145
6211
  "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
6212
  "verify": handleVerify,
6151
- "research-subagents": handleResearchSubagents,
6152
6213
  "mark-complete": handleMarkComplete,
6153
6214
  complete: handleMarkComplete,
6154
6215
  uncomplete: handleUncomplete,
@@ -6184,6 +6245,7 @@ function cliVersion() {
6184
6245
 
6185
6246
  // src/main.ts
6186
6247
  async function main() {
6248
+ (0, import_core19.migrateOldConfigDir)();
6187
6249
  loadEnvFile();
6188
6250
  const argv = process.argv.slice(2);
6189
6251
  const command = argv[0] ?? (process.stdin.isTTY ? "tui" : "--help");