@ordewell/cli 0.5.1 → 0.5.3

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 +182 -64
  2. package/package.json +4 -4
package/dist/main.js CHANGED
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/main.ts
27
- var import_core21 = require("@ordewell/core");
27
+ var import_core23 = require("@ordewell/core");
28
28
 
29
29
  // src/utils/env.ts
30
30
  var import_fs = require("fs");
@@ -95,6 +95,7 @@ Planning:
95
95
  ordewell fork Continue in a copy of the conversation and its tasks
96
96
  ordewell compact Condense the conversation into a summary (tasks are kept)
97
97
  ordewell rewind [<n>] Fork the conversation from before message <n> (no <n>: list them)
98
+ ordewell handoff [action] Review, merge, discard or clean up a finished isolated run
98
99
 
99
100
  Tasks (<id> is an order number or a task ID):
100
101
  ordewell add-task --title "..." Add a task to the current plan
@@ -123,6 +124,7 @@ Planner, models and runners:
123
124
  ordewell runners [<id> on|off] Enable or disable runners (claude-code, opencode, codex)
124
125
  ordewell allowlist set|clear|show Limit which models a runner may use
125
126
  ordewell auto [on|off] Autonomous permission mode for new sessions
127
+ ordewell parallel [<n>] How many AI tasks run at once (default 3; applies to a live run)
126
128
  ordewell refresh Re-discover runners and model catalogs
127
129
  ordewell models List every provider's catalog (works without a server)
128
130
 
@@ -196,8 +198,9 @@ Environment:
196
198
  OPENROUTER_BASE_URL Default: https://openrouter.ai/api/v1
197
199
  ORDEWELL_AUTONOMOUS_MODE Approval posture for new sessions (see "ordewell auto")
198
200
  ORDEWELL_PORT Daemon port CLI commands target (default: 3742)
201
+ ORDEWELL_DIRENV false: agents skip the project's direnv .envrc
199
202
  ORDEWELL_RESEARCH_ENABLED true (default) or false
200
- ORDEWELL_MAX_PARALLEL Default: 3
203
+ ORDEWELL_MAX_PARALLEL Default: 3 \u2014 set with "ordewell parallel"
201
204
  `);
202
205
  }
203
206
 
@@ -387,10 +390,15 @@ var WorkspaceInitNeededError = class extends Error {
387
390
  }
388
391
  workspace;
389
392
  };
390
- var ApiClient = class {
393
+ var SESSION_PATH = /^\/api\/(?:plans|sessions)\/([^/?]+)/;
394
+ var ApiClient = class _ApiClient {
395
+ /** Where a CLI invocation's sessions live, when a client is built without one: `--workspace`, else the cwd. */
396
+ static defaultWorkspace;
391
397
  port;
392
- constructor(port) {
398
+ workspace;
399
+ constructor(port, workspace) {
393
400
  this.port = port || DEFAULT_PORT;
401
+ this.workspace = workspace ?? _ApiClient.defaultWorkspace ?? process.cwd();
394
402
  }
395
403
  /**
396
404
  * Read at call time rather than at construction: a daemon restarted mid-run
@@ -400,7 +408,23 @@ var ApiClient = class {
400
408
  token() {
401
409
  return (0, import_core3.readDaemonToken)(this.port);
402
410
  }
403
- httpRequest(method, urlPath, body) {
411
+ /**
412
+ * A daemon holds only the sessions adopted since it started, so after a
413
+ * restart every session-scoped call answered "Session not found" until the
414
+ * user ran `ordewell sessions load`. The saved session is adopted here
415
+ * instead — idempotent for one the daemon still holds — and the call made
416
+ * once more; a 404 means the first attempt never ran, so the retry cannot
417
+ * act twice.
418
+ */
419
+ async httpRequest(method, urlPath, body) {
420
+ const res = await this.rawRequest(method, urlPath, body);
421
+ const sessionId = urlPath.match(SESSION_PATH)?.[1];
422
+ const missing = res.status === 404 && res.data?.error === "Session not found";
423
+ if (!missing || !sessionId || urlPath.split("?")[0].endsWith("/load")) return res;
424
+ const adopted2 = await this.rawRequest("POST", `/api/sessions/${sessionId}/load?workspace=${encodeURIComponent(this.workspace)}`);
425
+ return adopted2.status === 200 ? this.rawRequest(method, urlPath, body) : res;
426
+ }
427
+ rawRequest(method, urlPath, body) {
404
428
  return new Promise((resolve3, reject) => {
405
429
  const url = new URL(urlPath, `http://127.0.0.1:${this.port}`);
406
430
  const configuredTimeout = Number.parseInt(process.env.ORDEWELL_HTTP_TIMEOUT_MS || "", 10);
@@ -941,6 +965,25 @@ function readLastSession(workspace) {
941
965
  }
942
966
  return null;
943
967
  }
968
+ var SESSION_PREFIX = "session-";
969
+ function expandSessionId(id, workspace) {
970
+ if (id.startsWith(SESSION_PREFIX)) return id;
971
+ let files;
972
+ try {
973
+ files = (0, import_fs3.readdirSync)((0, import_path3.join)((0, import_core5.getStateDir)(workspace), "sessions")).filter((f) => f.endsWith(".json"));
974
+ } catch {
975
+ return id;
976
+ }
977
+ const matches = /* @__PURE__ */ new Set();
978
+ for (const file of files) {
979
+ try {
980
+ const full = JSON.parse((0, import_fs3.readFileSync)((0, import_path3.join)((0, import_core5.getStateDir)(workspace), "sessions", file), "utf8"))?.meta?.id;
981
+ if (typeof full === "string" && (full.endsWith(id) || full.slice(SESSION_PREFIX.length).startsWith(id))) matches.add(full);
982
+ } catch {
983
+ }
984
+ }
985
+ return matches.size === 1 ? [...matches][0] : id;
986
+ }
944
987
 
945
988
  // src/approvals.ts
946
989
  function describeApproval(event) {
@@ -1043,7 +1086,7 @@ function isTransient(event) {
1043
1086
  // src/commands/plan.ts
1044
1087
  var import_core6 = require("@ordewell/core");
1045
1088
  function planTasks(plan) {
1046
- return plan?.tasks || [];
1089
+ return plan ? allTasksOf(plan) : [];
1047
1090
  }
1048
1091
  function hasCommittedPlan(plan) {
1049
1092
  return planTasks(plan).length > 0;
@@ -1111,7 +1154,7 @@ function printPlan(plan, sessionId, runners2, models) {
1111
1154
  return rp ? ` (${task.assignedModel.modelLabel} \xB7 ${label})` : ` (${task.assignedModel.modelLabel})`;
1112
1155
  };
1113
1156
  console.log(`
1114
- Plan: ${tasks.length} tasks (${aiCount} AI, ${manCount} Manual) \u2014 ${(plan.runners || runners2).join(", ")}`);
1157
+ Plan: ${tasks.length} task${tasks.length === 1 ? "" : "s"} (${aiCount} AI, ${manCount} Manual) \u2014 ${(plan.runners || runners2).join(", ")}`);
1115
1158
  console.log(`Session: ${sessionId}
1116
1159
  `);
1117
1160
  for (const t of tasks) {
@@ -2961,30 +3004,47 @@ async function handlePlannerEffort(subArgs, injectedApi) {
2961
3004
  console.log(`Planner effort set to ${level}.`);
2962
3005
  }
2963
3006
 
2964
- // src/commands/key.ts
3007
+ // src/commands/parallel.ts
2965
3008
  var import_core14 = require("@ordewell/core");
2966
- var USAGE5 = "Usage: ordewell key [list | set <provider> <key>]";
3009
+ var USAGE5 = "Usage: ordewell parallel [<number of tasks, 1 or more>]";
3010
+ async function handleParallel(subArgs, injectedApi) {
3011
+ const [wanted] = positionals(subArgs);
3012
+ const api = await connect(subArgs, injectedApi);
3013
+ if (!wanted || wanted === "show") {
3014
+ const settings = await api.getSettings();
3015
+ console.log(`Up to ${String(settings.maxParallel ?? "?")} AI tasks run at once. Change with: ordewell parallel <n>`);
3016
+ return;
3017
+ }
3018
+ const limit = (0, import_core14.parseMaxParallel)(wanted);
3019
+ if (limit === null) fail(`"${wanted}" is not a number of tasks (1 or more).`, USAGE5);
3020
+ await persistEnv(api, { ORDEWELL_MAX_PARALLEL: String(limit) });
3021
+ console.log(`Up to ${limit} AI task${limit === 1 ? "" : "s"} now run at once.`);
3022
+ }
3023
+
3024
+ // src/commands/key.ts
3025
+ var import_core15 = require("@ordewell/core");
3026
+ var USAGE6 = "Usage: ordewell key [list | set <provider> <key>]";
2967
3027
  async function handleKey(subArgs, injectedApi) {
2968
3028
  const [action, provider, ...keyParts] = positionals(subArgs);
2969
3029
  if (action && action !== "list" && action !== "set") {
2970
- fail(`Unknown key action: "${action}"`, USAGE5);
3030
+ fail(`Unknown key action: "${action}"`, USAGE6);
2971
3031
  }
2972
3032
  const api = await connect(subArgs, injectedApi);
2973
3033
  if (action !== "set") {
2974
3034
  const catalog = await fetchCatalog(api);
2975
3035
  console.log("\nAPI provider keys\n");
2976
- for (const id2 of import_core14.PROVIDER_PRIORITY) {
2977
- const meta2 = import_core14.ALL_PROVIDERS[id2];
3036
+ for (const id2 of import_core15.PROVIDER_PRIORITY) {
3037
+ const meta2 = import_core15.ALL_PROVIDERS[id2];
2978
3038
  const configured = catalog.providers.includes(id2);
2979
3039
  console.log(` ${configured ? "\u2713" : " "} ${id2.padEnd(14)} ${meta2.apiKeyEnvVar.padEnd(28)} ${meta2.label}`);
2980
3040
  }
2981
3041
  console.log(`
2982
- \u2713 = a working key was found. ${USAGE5}`);
3042
+ \u2713 = a working key was found. ${USAGE6}`);
2983
3043
  return;
2984
3044
  }
2985
- if (!provider) fail(USAGE5);
3045
+ if (!provider) fail(USAGE6);
2986
3046
  const id = provider.toLowerCase();
2987
- const meta = import_core14.ALL_PROVIDERS[id];
3047
+ const meta = import_core15.ALL_PROVIDERS[id];
2988
3048
  if (!meta) {
2989
3049
  fail(`Unknown provider: ${provider}`, "Run `ordewell key` to see the list.");
2990
3050
  }
@@ -2995,7 +3055,7 @@ async function handleKey(subArgs, injectedApi) {
2995
3055
  }
2996
3056
 
2997
3057
  // src/commands/runners.ts
2998
- var USAGE6 = "Usage: ordewell runners [<runner-id> on|off]";
3058
+ var USAGE7 = "Usage: ordewell runners [<runner-id> on|off]";
2999
3059
  async function handleRunners(subArgs, injectedApi) {
3000
3060
  const [runner, toggle] = positionals(subArgs);
3001
3061
  const api = await connect(subArgs, injectedApi);
@@ -3006,7 +3066,7 @@ async function handleRunners(subArgs, injectedApi) {
3006
3066
  console.log(` ${r.enabled ? "\u2713" : " "} ${r.id.padEnd(14)} ${r.name}${r.enabled ? "" : " (disabled)"}`);
3007
3067
  }
3008
3068
  console.log(`
3009
- \u2713 = offered to the planner. ${USAGE6}`);
3069
+ \u2713 = offered to the planner. ${USAGE7}`);
3010
3070
  return;
3011
3071
  }
3012
3072
  const state = await api.getRunners();
@@ -3015,7 +3075,7 @@ async function handleRunners(subArgs, injectedApi) {
3015
3075
  fail(`Unknown runner: ${runner}`, `Enabled or not, this daemon knows: ${state.runners.map((r) => r.id).join(", ")}`);
3016
3076
  }
3017
3077
  const enabled = toggle === void 0 ? !known.enabled : toggle === "on" ? true : toggle === "off" ? false : null;
3018
- if (enabled === null) fail(USAGE6);
3078
+ if (enabled === null) fail(USAGE7);
3019
3079
  await api.setRunnerEnabled(runner, enabled);
3020
3080
  console.log(`${runner} ${enabled ? "enabled" : "disabled"}.`);
3021
3081
  }
@@ -3065,7 +3125,7 @@ async function handleApprove(subArgs, injectedApi) {
3065
3125
  // src/tui/terminalLauncher.ts
3066
3126
  var import_child_process3 = require("child_process");
3067
3127
  var import_util = require("util");
3068
- var import_core15 = require("@ordewell/core");
3128
+ var import_core16 = require("@ordewell/core");
3069
3129
  var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
3070
3130
  var defaultExecFile = async (command, args) => {
3071
3131
  const { stdout, stderr } = await execFileAsync(command, args);
@@ -3085,16 +3145,16 @@ var defaultWhich = (bin) => {
3085
3145
  async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
3086
3146
  const platform = deps.platform ?? process.platform;
3087
3147
  const env = deps.env ?? process.env;
3088
- const hasTmuxImpl = deps.hasTmuxImpl ?? import_core15.hasTmux;
3148
+ const hasTmuxImpl = deps.hasTmuxImpl ?? import_core16.hasTmux;
3089
3149
  const execFileImpl = deps.execFileImpl ?? defaultExecFile;
3090
3150
  const spawnImpl = deps.spawnImpl ?? defaultSpawn;
3091
3151
  const which = deps.which ?? defaultWhich;
3092
3152
  if (!hasTmuxImpl()) {
3093
3153
  return { ok: false, message: "tmux is not installed \u2014 install it to get a real terminal per task." };
3094
3154
  }
3095
- const session = (0, import_core15.tmuxSessionName)(port);
3096
- const socket = (0, import_core15.tmuxSocketName)(port);
3097
- const base = (0, import_core15.tmuxWindowName)(taskId, planSessionId);
3155
+ const session = (0, import_core16.tmuxSessionName)(port);
3156
+ const socket = (0, import_core16.tmuxSocketName)(port);
3157
+ const base = (0, import_core16.tmuxWindowName)(taskId, planSessionId);
3098
3158
  let windows;
3099
3159
  try {
3100
3160
  const { stdout } = await execFileImpl("tmux", ["-L", socket, "list-windows", "-t", session, "-F", "#{window_name}"]);
@@ -3388,7 +3448,7 @@ Dependencies \xB7 #${task.order} ${task.title}
3388
3448
  }
3389
3449
 
3390
3450
  // src/tui/index.ts
3391
- var import_core20 = require("@ordewell/core");
3451
+ var import_core22 = require("@ordewell/core");
3392
3452
 
3393
3453
  // src/tui/editor.ts
3394
3454
  function emptyEditor() {
@@ -3550,6 +3610,7 @@ var SLASH_COMMANDS = [
3550
3610
  { name: "allowlist", usage: "/allowlist [set <runner> <ids> | clear <runner>]", description: "Limit which models a runner may use", category: "models" },
3551
3611
  { name: "runners", usage: "/runners [<id> on|off]", description: "Enable or disable runners (claude-code, opencode, codex)", category: "models" },
3552
3612
  { name: "auto", usage: "/auto [on|off]", description: "Autonomous permission mode for new plans", category: "models" },
3613
+ { name: "parallel", usage: "/parallel [<n>]", description: "How many AI tasks run at once (applies to a live run)", category: "models" },
3553
3614
  { name: "refresh", usage: "/refresh", description: "Re-discover runners and model catalogs", category: "models" },
3554
3615
  // Skills
3555
3616
  { name: "tdd", usage: "/tdd [on|off]", description: "Test-Driven Development mode", category: "skills" },
@@ -3774,6 +3835,7 @@ function chatEditorRoom(cols, active) {
3774
3835
  }
3775
3836
 
3776
3837
  // src/tui/state.ts
3838
+ var import_core17 = require("@ordewell/core");
3777
3839
  var SKILL_IDS = ["tdd", "verify"];
3778
3840
  function noSkills() {
3779
3841
  return { tdd: false, verify: false };
@@ -3838,6 +3900,7 @@ function initialState(overrides = {}) {
3838
3900
  orchestratorModel: "",
3839
3901
  plannerProvider: "",
3840
3902
  plannerEffort: "",
3903
+ maxParallel: import_core17.DEFAULT_MAX_PARALLEL,
3841
3904
  configuredProviders: [],
3842
3905
  allowlist: {},
3843
3906
  autonomous: true,
@@ -3857,7 +3920,7 @@ function initialState(overrides = {}) {
3857
3920
  }
3858
3921
 
3859
3922
  // src/tui/layout.ts
3860
- var import_core16 = require("@ordewell/core");
3923
+ var import_core18 = require("@ordewell/core");
3861
3924
  var CHROME_ROWS = 4;
3862
3925
  function chatInputWrap(state) {
3863
3926
  const active = state.focus === "chat" && !state.overlay;
@@ -3993,9 +4056,9 @@ function chatScrollMax(state) {
3993
4056
  }
3994
4057
  function plannerSetup(state) {
3995
4058
  const provider = state.plannerProvider;
3996
- const registration = import_core16.ALL_PROVIDERS[provider];
4059
+ const registration = import_core18.ALL_PROVIDERS[provider];
3997
4060
  if (!registration) return { label: "not set", ready: false };
3998
- const runner = (0, import_core16.runnerForProvider)(provider);
4061
+ const runner = (0, import_core18.runnerForProvider)(provider);
3999
4062
  const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
4000
4063
  return { label: registration.label, ready };
4001
4064
  }
@@ -4178,7 +4241,7 @@ function taskLines(state, row, index, cols) {
4178
4241
  const icon = paint(rawIcon);
4179
4242
  const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : style.grey(" AI");
4180
4243
  const caret = selected ? style.cyan("\u276F") : " ";
4181
- const orderLabel = parent ? (0, import_core16.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
4244
+ const orderLabel = parent ? (0, import_core18.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
4182
4245
  const indent = parent ? " " : "";
4183
4246
  const head = `${indent}${caret} ${icon} ${orderLabel} ${kind} `;
4184
4247
  const titleRoom = Math.max(1, cols - width(head));
@@ -4199,21 +4262,22 @@ function taskLines(state, row, index, cols) {
4199
4262
  const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}${modeInfo?.autonomous ? style.yellow(" \u26A1") : ""}` : "";
4200
4263
  const runner = task.assignedRunner ?? "";
4201
4264
  const bodyPad = parent ? " " : " ";
4202
- const meta = [running ? "working" : "", runner, model].filter(Boolean).join(" \xB7 ");
4265
+ const activity = idle ? "quiet \u2014 t opens its terminal" : running ? "working" : "";
4266
+ const meta = [activity, runner, model].filter(Boolean).join(" \xB7 ");
4203
4267
  if (meta) lines.push(style.grey(truncate(`${bodyPad}${meta}`, cols)));
4204
4268
  if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
4205
4269
  if (task.isolation?.state === "conflict") {
4206
4270
  const where = task.isolation.conflictRepo && task.isolation.conflictRepo !== "." ? ` in ${task.isolation.conflictRepo}` : "";
4207
- const files = task.isolation.conflictFiles?.length ? ` (${(0, import_core16.capConflictFiles)(task.isolation.conflictFiles)})` : "";
4271
+ const files = task.isolation.conflictFiles?.length ? ` (${(0, import_core18.capConflictFiles)(task.isolation.conflictFiles)})` : "";
4208
4272
  lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where}${files} \u2014 its work is kept on its own branch`, cols)));
4209
4273
  }
4210
4274
  if (task.isolation?.state === "repairing") {
4211
- const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core16.capConflictFiles)(task.isolation.conflictFiles)}` : "";
4275
+ const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core18.capConflictFiles)(task.isolation.conflictFiles)}` : "";
4212
4276
  const attempt = task.isolation.repair ? ` (attempt ${task.isolation.repair.attempt}/${task.isolation.repair.limit})` : "";
4213
4277
  lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
4214
4278
  }
4215
4279
  if (task.isolation?.state === "integrated" && task.isolation.repairedFiles?.length) {
4216
- lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0, import_core16.capConflictFiles)(task.isolation.repairedFiles)}`, cols)));
4280
+ lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0, import_core18.capConflictFiles)(task.isolation.repairedFiles)}`, cols)));
4217
4281
  }
4218
4282
  let editorLine;
4219
4283
  if (expanded) {
@@ -4298,7 +4362,7 @@ function helpScrollMax(state) {
4298
4362
  }
4299
4363
 
4300
4364
  // src/tui/render.ts
4301
- var import_core17 = require("@ordewell/core");
4365
+ var import_core19 = require("@ordewell/core");
4302
4366
  function render(state) {
4303
4367
  const { rows, cols } = state;
4304
4368
  const rowsForBody = bodyRows(state);
@@ -4562,7 +4626,7 @@ function renderHandoff(state, overlay, rows, cols) {
4562
4626
  return frame(`Diff \u2014 ${handoffBranch(handoff)}`, [...shown, "", style.grey(`${more}enter or esc goes back`)], rows, cols);
4563
4627
  }
4564
4628
  const landed = handoff.landed.length === 0 ? [style.grey("Nothing landed on it.")] : handoff.landed.map((task) => {
4565
- const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core17.capConflictFiles)(task.repairedFiles)})`) : "";
4629
+ const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core19.capConflictFiles)(task.repairedFiles)})`) : "";
4566
4630
  return ` ${style.green("\u2713")} #${task.order} ${task.title}${repaired}`;
4567
4631
  });
4568
4632
  const actions = handoffActions(handoff).map((action, index) => {
@@ -4652,9 +4716,9 @@ function frame(title, body, rows, cols) {
4652
4716
  }
4653
4717
 
4654
4718
  // src/tui/reducer.ts
4655
- var import_core18 = require("@ordewell/core");
4719
+ var import_core20 = require("@ordewell/core");
4656
4720
  var import_plan_utils3 = require("@ordewell/core/plan-utils");
4657
- var KNOWN_PROVIDERS2 = Object.keys(import_core18.ALL_PROVIDERS);
4721
+ var KNOWN_PROVIDERS2 = Object.keys(import_core20.ALL_PROVIDERS);
4658
4722
  var DEFAULT_EFFORT = "__runner_default__";
4659
4723
  function step(state, effects = []) {
4660
4724
  return { state, effects };
@@ -4673,6 +4737,17 @@ function alreadySpoken(messages, content) {
4673
4737
  const redrawn = messages.at(-1)?.role === "system" && messages.at(-1)?.content === content;
4674
4738
  return spoken || redrawn;
4675
4739
  }
4740
+ function runLabel(tasks) {
4741
+ const running = tasks.filter(isTaskRunning);
4742
+ if (running.length > 0) {
4743
+ const [first] = running;
4744
+ const name = first.assignedRunner ? `${first.title} \xB7 ${first.assignedRunner}` : first.title;
4745
+ return running.length > 1 ? `${name} (+${running.length - 1} more)` : name;
4746
+ }
4747
+ const waiting = tasks.filter((t) => t.status === "awaiting_user").length;
4748
+ if (waiting === 0) return "";
4749
+ return waiting === 1 ? "1 task waits for you" : `${waiting} tasks wait for you`;
4750
+ }
4676
4751
  function researchLabel(messages, summary) {
4677
4752
  const others = messages.filter(isPendingResearch).length - 1;
4678
4753
  return others > 0 ? `${summary} (+${others} more)` : summary;
@@ -4776,6 +4851,16 @@ function reduce(state, action) {
4776
4851
  const status = state.status === "planning" ? "researching" : state.status;
4777
4852
  return step({ ...spoken, status, busyLabel: researchLabel(spoken.messages, summary) });
4778
4853
  }
4854
+ // A settled transcript line, not a research step: nothing ever settles a
4855
+ // task's start, so as a step it stayed "⋯" forever and was counted into
4856
+ // the planner's next "(+N more)".
4857
+ case "taskStarted": {
4858
+ if (stale(state, action.sessionId)) return step(state);
4859
+ const tasks = state.tasks.map((t) => t.id === action.taskId && !isTaskRunning(t) ? { ...t, status: "in_progress" } : t);
4860
+ const who = action.runner ? ` \xB7 ${action.runner}` : "";
4861
+ const spoken = say(state, "system", sanitize(`Started "${action.title}"${who}`));
4862
+ return step({ ...spoken, tasks, status: "executing", busyLabel: runLabel(tasks) });
4863
+ }
4779
4864
  case "researchStepDone": {
4780
4865
  if (stale(state, action.sessionId)) return step(state);
4781
4866
  const messages = settleResearchStep(state.messages, action);
@@ -4816,7 +4901,7 @@ function reduce(state, action) {
4816
4901
  return t;
4817
4902
  });
4818
4903
  if (!changed) return step(state);
4819
- return step({ ...state, status: "executing", tasks });
4904
+ return step({ ...state, status: "executing", tasks, busyLabel: runLabel(tasks) });
4820
4905
  }
4821
4906
  case "queueReady": {
4822
4907
  if (stale(state, action.sessionId)) return step(state);
@@ -4986,6 +5071,7 @@ function applySettings(state, settings) {
4986
5071
  orchestratorModel: typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : state.orchestratorModel,
4987
5072
  plannerProvider: typeof settings.aiProvider === "string" ? settings.aiProvider : state.plannerProvider,
4988
5073
  plannerEffort: typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : state.plannerEffort,
5074
+ maxParallel: typeof settings.maxParallel === "number" ? settings.maxParallel : state.maxParallel,
4989
5075
  allowlist: settings.modelAllowlist && typeof settings.modelAllowlist === "object" ? settings.modelAllowlist : state.allowlist
4990
5076
  };
4991
5077
  }
@@ -5002,11 +5088,11 @@ function modelDetail(m) {
5002
5088
  function providerErrorHint(state) {
5003
5089
  const failed = Object.keys(state.providerErrors ?? {});
5004
5090
  if (failed.length === 0) return void 0;
5005
- const names = failed.map((p) => import_core18.ALL_PROVIDERS[p]?.label ?? p);
5091
+ const names = failed.map((p) => import_core20.ALL_PROVIDERS[p]?.label ?? p);
5006
5092
  return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
5007
5093
  }
5008
5094
  function plannerModelItems(state) {
5009
- const runner = (0, import_core18.runnerForProvider)(state.plannerProvider);
5095
+ const runner = (0, import_core20.runnerForProvider)(state.plannerProvider);
5010
5096
  if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
5011
5097
  const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
5012
5098
  id: m.id,
@@ -5020,7 +5106,7 @@ function plannerModelItems(state) {
5020
5106
  return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
5021
5107
  }
5022
5108
  function plannerEffortItems(state) {
5023
- const runner = (0, import_core18.runnerForProvider)(state.plannerProvider);
5109
+ const runner = (0, import_core20.runnerForProvider)(state.plannerProvider);
5024
5110
  if (!runner) {
5025
5111
  return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
5026
5112
  }
@@ -5601,6 +5687,8 @@ function runCommand(state, { name, args }) {
5601
5687
  return runners(state, args);
5602
5688
  case "auto":
5603
5689
  return setAutonomous(state, args[0]);
5690
+ case "parallel":
5691
+ return setMaxParallel(state, args[0]);
5604
5692
  case "mouse":
5605
5693
  return setMouseCapture(state, args[0]);
5606
5694
  case "sessions":
@@ -5769,7 +5857,7 @@ function newSession(state) {
5769
5857
  }
5770
5858
  function setModel(state, args) {
5771
5859
  if (args[0] === "set" && args[1]) {
5772
- const runner = (0, import_core18.runnerForProvider)(state.plannerProvider);
5860
+ const runner = (0, import_core20.runnerForProvider)(state.plannerProvider);
5773
5861
  const known = runner ? modelsForRunner(state.models, runner) : [];
5774
5862
  if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
5775
5863
  return fail3(state, `${args[1]} was not discovered for ${runner}.`);
@@ -5791,21 +5879,21 @@ function setModel(state, args) {
5791
5879
  }
5792
5880
  function plannerItems(state) {
5793
5881
  const installed = new Set(state.runners.map((r) => r.id));
5794
- const agents = import_core18.CLI_PROVIDERS.map((id) => {
5795
- const runner = (0, import_core18.runnerForProvider)(id);
5882
+ const agents = import_core20.CLI_PROVIDERS.map((id) => {
5883
+ const runner = (0, import_core20.runnerForProvider)(id);
5796
5884
  const ready = installed.has(runner);
5797
5885
  return {
5798
5886
  id,
5799
- label: import_core18.ALL_PROVIDERS[id].label,
5887
+ label: import_core20.ALL_PROVIDERS[id].label,
5800
5888
  detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
5801
5889
  selected: state.plannerProvider === id,
5802
5890
  disabled: !ready
5803
5891
  };
5804
5892
  });
5805
- const vendors = import_core18.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
5893
+ const vendors = import_core20.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
5806
5894
  id,
5807
- label: import_core18.ALL_PROVIDERS[id].label,
5808
- detail: import_core18.ALL_PROVIDERS[id].apiKeyEnvVar,
5895
+ label: import_core20.ALL_PROVIDERS[id].label,
5896
+ detail: import_core20.ALL_PROVIDERS[id].apiKeyEnvVar,
5809
5897
  selected: state.plannerProvider === id
5810
5898
  }));
5811
5899
  return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
@@ -5863,16 +5951,16 @@ function setKey(state, args) {
5863
5951
  }
5864
5952
  return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
5865
5953
  }
5866
- const items = import_core18.PROVIDER_PRIORITY.map((id) => ({
5954
+ const items = import_core20.PROVIDER_PRIORITY.map((id) => ({
5867
5955
  id,
5868
- label: import_core18.ALL_PROVIDERS[id].label,
5869
- detail: import_core18.ALL_PROVIDERS[id].apiKeyEnvVar,
5956
+ label: import_core20.ALL_PROVIDERS[id].label,
5957
+ detail: import_core20.ALL_PROVIDERS[id].apiKeyEnvVar,
5870
5958
  selected: state.configuredProviders.includes(id)
5871
5959
  }));
5872
5960
  return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
5873
5961
  }
5874
5962
  function keyPrompt(provider) {
5875
- const meta = import_core18.ALL_PROVIDERS[provider];
5963
+ const meta = import_core20.ALL_PROVIDERS[provider];
5876
5964
  return {
5877
5965
  kind: "prompt",
5878
5966
  title: `${meta.label} API key`,
@@ -5947,6 +6035,12 @@ function runners(state, args) {
5947
6035
  })
5948
6036
  } });
5949
6037
  }
6038
+ function setMaxParallel(state, arg) {
6039
+ if (!arg) return step(say(state, "system", `Up to ${state.maxParallel} AI task${state.maxParallel === 1 ? "" : "s"} run at once \u2014 /parallel <n> changes it.`));
6040
+ const limit = (0, import_core20.parseMaxParallel)(arg);
6041
+ if (limit === null) return fail3(state, "Usage: /parallel [<number of tasks, 1 or more>]");
6042
+ return step({ ...state, maxParallel: limit }, [{ type: "setMaxParallel", limit }]);
6043
+ }
5950
6044
  function setAutonomous(state, arg) {
5951
6045
  const enabled = resolveToggle(arg, state.autonomous);
5952
6046
  if (enabled === null) return fail3(state, "Usage: /auto [on|off]");
@@ -6202,7 +6296,9 @@ function createApp(deps) {
6202
6296
  function dispatch(action) {
6203
6297
  if (exited) return;
6204
6298
  const result = reduce(state, action);
6299
+ const previousSession = state.sessionId;
6205
6300
  state = result.state;
6301
+ if (state.sessionId && state.sessionId !== previousSession) deps.onSessionChange?.(state.sessionId, state);
6206
6302
  if (state.exiting) {
6207
6303
  exited = true;
6208
6304
  clearInterval(spinnerInterval);
@@ -6234,7 +6330,7 @@ function createApp(deps) {
6234
6330
 
6235
6331
  // src/tui/effects.ts
6236
6332
  var import_child_process4 = require("child_process");
6237
- var import_core19 = require("@ordewell/core");
6333
+ var import_core21 = require("@ordewell/core");
6238
6334
  var import_plan_utils4 = require("@ordewell/core/plan-utils");
6239
6335
  var ConversationQueue = class {
6240
6336
  tail = Promise.resolve();
@@ -6405,7 +6501,7 @@ async function perform(effect, deps) {
6405
6501
  dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
6406
6502
  return;
6407
6503
  case "setPlanner": {
6408
- const meta = import_core19.ALL_PROVIDERS[effect.provider];
6504
+ const meta = import_core21.ALL_PROVIDERS[effect.provider];
6409
6505
  if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
6410
6506
  const env = { AI_PROVIDER: effect.provider };
6411
6507
  const settings = await api.updateSettings({ env });
@@ -6417,7 +6513,7 @@ async function perform(effect, deps) {
6417
6513
  });
6418
6514
  dispatch({
6419
6515
  type: "notice",
6420
- message: `${(0, import_core19.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
6516
+ message: `${(0, import_core21.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
6421
6517
  });
6422
6518
  await loadModels(deps);
6423
6519
  return;
@@ -6429,7 +6525,7 @@ async function perform(effect, deps) {
6429
6525
  dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
6430
6526
  return;
6431
6527
  case "setApiKey": {
6432
- const meta = import_core19.ALL_PROVIDERS[effect.provider];
6528
+ const meta = import_core21.ALL_PROVIDERS[effect.provider];
6433
6529
  if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
6434
6530
  await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
6435
6531
  deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
@@ -6460,6 +6556,11 @@ async function perform(effect, deps) {
6460
6556
  dispatch({ type: "notice", message: effect.message });
6461
6557
  return;
6462
6558
  }
6559
+ case "setMaxParallel":
6560
+ await api.updateSettings({ env: { ORDEWELL_MAX_PARALLEL: String(effect.limit) } });
6561
+ deps.setEnvVar("ORDEWELL_MAX_PARALLEL", String(effect.limit));
6562
+ dispatch({ type: "notice", message: `Up to ${effect.limit} AI task${effect.limit === 1 ? "" : "s"} now run at once.` });
6563
+ return;
6463
6564
  case "setAutonomous":
6464
6565
  deps.setEnvVar("ORDEWELL_AUTONOMOUS_MODE", String(effect.enabled));
6465
6566
  dispatch({ type: "notice", message: `Autonomous mode ${effect.enabled ? "on" : "off"}.` });
@@ -6590,7 +6691,7 @@ function osc52(text) {
6590
6691
  function copySelection(deps, text) {
6591
6692
  if (!text) return;
6592
6693
  const lines = text.split("\n").length;
6593
- const command = (0, import_core19.clipboardCopyCommand)(deps.hasBin);
6694
+ const command = (0, import_core21.clipboardCopyCommand)(deps.hasBin);
6594
6695
  if (command) {
6595
6696
  try {
6596
6697
  (deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
@@ -6727,12 +6828,12 @@ function onExecutionEvent(dispatch, event, sessionId) {
6727
6828
  return;
6728
6829
  }
6729
6830
  case "task_started":
6730
- dispatch({ type: "researchStep", summary: `${event.title ?? event.taskId} \xB7 ${event.runner ?? ""}`.trim(), sessionId });
6831
+ dispatch({ type: "taskStarted", taskId: String(event.taskId), title: String(event.title ?? event.taskId), runner: event.runner, sessionId });
6731
6832
  return;
6732
6833
  // The extension shows these in its checkpoint panel; here they are
6733
6834
  // transcript lines, which is the TUI's equivalent surface.
6734
6835
  case "checkpoint":
6735
- dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core19.truncateCheckpointSummary)(event.summary)}` });
6836
+ dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core21.truncateCheckpointSummary)(event.summary)}` });
6736
6837
  return;
6737
6838
  case "approval_decided":
6738
6839
  dispatch({ type: "notice", message: describeApprovalDecision2(event) });
@@ -7116,7 +7217,7 @@ async function handleTui(subArgs) {
7116
7217
  }
7117
7218
  const workspace = flag(subArgs, "--workspace") || process.cwd();
7118
7219
  try {
7119
- (0, import_core20.assertWorkspaceExists)(workspace);
7220
+ (0, import_core22.assertWorkspaceExists)(workspace);
7120
7221
  } catch {
7121
7222
  console.error(
7122
7223
  `Workspace "${workspace}" does not exist.
@@ -7125,12 +7226,12 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
7125
7226
  );
7126
7227
  process.exit(1);
7127
7228
  }
7128
- registerSkillCommands((0, import_core20.createSkillsService)(workspace).listSkills());
7229
+ registerSkillCommands((0, import_core22.createSkillsService)(workspace).listSkills());
7129
7230
  const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
7130
7231
  const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
7131
7232
  const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
7132
7233
  let owned = ownedAtLaunch;
7133
- const api = new ApiClient(port);
7234
+ const api = new ApiClient(port, workspace);
7134
7235
  const conversationQueue = new ConversationQueue();
7135
7236
  let terminal;
7136
7237
  const autoMode = process.env.ORDEWELL_AUTONOMOUS_MODE;
@@ -7144,7 +7245,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
7144
7245
  conversationQueue,
7145
7246
  port,
7146
7247
  dispatch: (action) => app.dispatch(action),
7147
- newSessionId: () => (0, import_core20.mintSessionId)(),
7248
+ newSessionId: () => (0, import_core22.mintSessionId)(),
7148
7249
  setEnvVar: (key, value) => {
7149
7250
  writeEnvVar(findEnvFile(), key, value);
7150
7251
  process.env[key] = value;
@@ -7161,7 +7262,16 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
7161
7262
  },
7162
7263
  exit: () => shutdown(0)
7163
7264
  }),
7164
- onExit: () => shutdown(0)
7265
+ onExit: () => shutdown(0),
7266
+ // The same pointer `ordewell plan` leaves, so `ordewell handoff`,
7267
+ // `terminal <n>` and the other one-shot commands act on the session this
7268
+ // TUI is showing instead of saying there is none.
7269
+ onSessionChange: (sessionId, state) => {
7270
+ try {
7271
+ saveLastSession(sessionId, state.goal, state.runners.filter((r) => r.enabled).map((r) => r.id), workspace);
7272
+ } catch {
7273
+ }
7274
+ }
7165
7275
  });
7166
7276
  terminal = openTerminal({
7167
7277
  mouse: mouseCapture,
@@ -7207,6 +7317,7 @@ var COMMANDS = {
7207
7317
  model: handleModel,
7208
7318
  planner: handlePlanner,
7209
7319
  "planner-effort": handlePlannerEffort,
7320
+ parallel: handleParallel,
7210
7321
  key: handleKey,
7211
7322
  runners: handleRunners,
7212
7323
  auto: handleAuto,
@@ -7253,10 +7364,12 @@ function cliVersion() {
7253
7364
  }
7254
7365
 
7255
7366
  // src/main.ts
7367
+ var TUI_FLAGS = /* @__PURE__ */ new Set(["--workspace", "--port"]);
7256
7368
  async function main() {
7257
- (0, import_core21.migrateOldConfigDir)();
7369
+ (0, import_core23.migrateOldConfigDir)();
7258
7370
  loadEnvFile();
7259
7371
  const argv = process.argv.slice(2);
7372
+ if (TUI_FLAGS.has(argv[0] ?? "")) argv.unshift("tui");
7260
7373
  const command = argv[0] ?? (process.stdin.isTTY ? "tui" : "--help");
7261
7374
  if (command === "--help" || command === "-h") {
7262
7375
  printHelp();
@@ -7266,6 +7379,11 @@ async function main() {
7266
7379
  console.log(cliVersion());
7267
7380
  return;
7268
7381
  }
7382
+ ApiClient.defaultWorkspace = flag(argv, "--workspace") || void 0;
7383
+ const sessionAt = argv.indexOf("--session-id");
7384
+ if (sessionAt > 0 && argv[sessionAt + 1]) {
7385
+ argv[sessionAt + 1] = expandSessionId(argv[sessionAt + 1], flag(argv, "--workspace") || process.cwd());
7386
+ }
7269
7387
  const handler = COMMANDS[command];
7270
7388
  if (!handler) {
7271
7389
  console.error(`Unknown command: ${command}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ordewell/cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Ordewell CLI and TUI — plan, run and verify coding-agent tasks from the terminal.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Alessandro Costanzo Ciano",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "main": "./dist/main.js",
34
34
  "bin": {
35
- "ordewell": "./dist/main.js"
35
+ "ordewell": "dist/main.js"
36
36
  },
37
37
  "files": [
38
38
  "dist",
@@ -48,8 +48,8 @@
48
48
  "typecheck": "tsc --noEmit"
49
49
  },
50
50
  "dependencies": {
51
- "@ordewell/core": "^0.5.1",
52
- "@ordewell/web": "^0.5.1",
51
+ "@ordewell/core": "^0.5.3",
52
+ "@ordewell/web": "^0.5.3",
53
53
  "string-width": "^7.2.0",
54
54
  "ws": "^8.18.0"
55
55
  },