@ordewell/cli 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +154 -58
  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
 
@@ -941,6 +944,25 @@ function readLastSession(workspace) {
941
944
  }
942
945
  return null;
943
946
  }
947
+ var SESSION_PREFIX = "session-";
948
+ function expandSessionId(id, workspace) {
949
+ if (id.startsWith(SESSION_PREFIX)) return id;
950
+ let files;
951
+ try {
952
+ files = (0, import_fs3.readdirSync)((0, import_path3.join)((0, import_core5.getStateDir)(workspace), "sessions")).filter((f) => f.endsWith(".json"));
953
+ } catch {
954
+ return id;
955
+ }
956
+ const matches = /* @__PURE__ */ new Set();
957
+ for (const file of files) {
958
+ try {
959
+ const full = JSON.parse((0, import_fs3.readFileSync)((0, import_path3.join)((0, import_core5.getStateDir)(workspace), "sessions", file), "utf8"))?.meta?.id;
960
+ if (typeof full === "string" && (full.endsWith(id) || full.slice(SESSION_PREFIX.length).startsWith(id))) matches.add(full);
961
+ } catch {
962
+ }
963
+ }
964
+ return matches.size === 1 ? [...matches][0] : id;
965
+ }
944
966
 
945
967
  // src/approvals.ts
946
968
  function describeApproval(event) {
@@ -2961,30 +2983,47 @@ async function handlePlannerEffort(subArgs, injectedApi) {
2961
2983
  console.log(`Planner effort set to ${level}.`);
2962
2984
  }
2963
2985
 
2964
- // src/commands/key.ts
2986
+ // src/commands/parallel.ts
2965
2987
  var import_core14 = require("@ordewell/core");
2966
- var USAGE5 = "Usage: ordewell key [list | set <provider> <key>]";
2988
+ var USAGE5 = "Usage: ordewell parallel [<number of tasks, 1 or more>]";
2989
+ async function handleParallel(subArgs, injectedApi) {
2990
+ const [wanted] = positionals(subArgs);
2991
+ const api = await connect(subArgs, injectedApi);
2992
+ if (!wanted || wanted === "show") {
2993
+ const settings = await api.getSettings();
2994
+ console.log(`Up to ${String(settings.maxParallel ?? "?")} AI tasks run at once. Change with: ordewell parallel <n>`);
2995
+ return;
2996
+ }
2997
+ const limit = (0, import_core14.parseMaxParallel)(wanted);
2998
+ if (limit === null) fail(`"${wanted}" is not a number of tasks (1 or more).`, USAGE5);
2999
+ await persistEnv(api, { ORDEWELL_MAX_PARALLEL: String(limit) });
3000
+ console.log(`Up to ${limit} AI task${limit === 1 ? "" : "s"} now run at once.`);
3001
+ }
3002
+
3003
+ // src/commands/key.ts
3004
+ var import_core15 = require("@ordewell/core");
3005
+ var USAGE6 = "Usage: ordewell key [list | set <provider> <key>]";
2967
3006
  async function handleKey(subArgs, injectedApi) {
2968
3007
  const [action, provider, ...keyParts] = positionals(subArgs);
2969
3008
  if (action && action !== "list" && action !== "set") {
2970
- fail(`Unknown key action: "${action}"`, USAGE5);
3009
+ fail(`Unknown key action: "${action}"`, USAGE6);
2971
3010
  }
2972
3011
  const api = await connect(subArgs, injectedApi);
2973
3012
  if (action !== "set") {
2974
3013
  const catalog = await fetchCatalog(api);
2975
3014
  console.log("\nAPI provider keys\n");
2976
- for (const id2 of import_core14.PROVIDER_PRIORITY) {
2977
- const meta2 = import_core14.ALL_PROVIDERS[id2];
3015
+ for (const id2 of import_core15.PROVIDER_PRIORITY) {
3016
+ const meta2 = import_core15.ALL_PROVIDERS[id2];
2978
3017
  const configured = catalog.providers.includes(id2);
2979
3018
  console.log(` ${configured ? "\u2713" : " "} ${id2.padEnd(14)} ${meta2.apiKeyEnvVar.padEnd(28)} ${meta2.label}`);
2980
3019
  }
2981
3020
  console.log(`
2982
- \u2713 = a working key was found. ${USAGE5}`);
3021
+ \u2713 = a working key was found. ${USAGE6}`);
2983
3022
  return;
2984
3023
  }
2985
- if (!provider) fail(USAGE5);
3024
+ if (!provider) fail(USAGE6);
2986
3025
  const id = provider.toLowerCase();
2987
- const meta = import_core14.ALL_PROVIDERS[id];
3026
+ const meta = import_core15.ALL_PROVIDERS[id];
2988
3027
  if (!meta) {
2989
3028
  fail(`Unknown provider: ${provider}`, "Run `ordewell key` to see the list.");
2990
3029
  }
@@ -2995,7 +3034,7 @@ async function handleKey(subArgs, injectedApi) {
2995
3034
  }
2996
3035
 
2997
3036
  // src/commands/runners.ts
2998
- var USAGE6 = "Usage: ordewell runners [<runner-id> on|off]";
3037
+ var USAGE7 = "Usage: ordewell runners [<runner-id> on|off]";
2999
3038
  async function handleRunners(subArgs, injectedApi) {
3000
3039
  const [runner, toggle] = positionals(subArgs);
3001
3040
  const api = await connect(subArgs, injectedApi);
@@ -3006,7 +3045,7 @@ async function handleRunners(subArgs, injectedApi) {
3006
3045
  console.log(` ${r.enabled ? "\u2713" : " "} ${r.id.padEnd(14)} ${r.name}${r.enabled ? "" : " (disabled)"}`);
3007
3046
  }
3008
3047
  console.log(`
3009
- \u2713 = offered to the planner. ${USAGE6}`);
3048
+ \u2713 = offered to the planner. ${USAGE7}`);
3010
3049
  return;
3011
3050
  }
3012
3051
  const state = await api.getRunners();
@@ -3015,7 +3054,7 @@ async function handleRunners(subArgs, injectedApi) {
3015
3054
  fail(`Unknown runner: ${runner}`, `Enabled or not, this daemon knows: ${state.runners.map((r) => r.id).join(", ")}`);
3016
3055
  }
3017
3056
  const enabled = toggle === void 0 ? !known.enabled : toggle === "on" ? true : toggle === "off" ? false : null;
3018
- if (enabled === null) fail(USAGE6);
3057
+ if (enabled === null) fail(USAGE7);
3019
3058
  await api.setRunnerEnabled(runner, enabled);
3020
3059
  console.log(`${runner} ${enabled ? "enabled" : "disabled"}.`);
3021
3060
  }
@@ -3065,7 +3104,7 @@ async function handleApprove(subArgs, injectedApi) {
3065
3104
  // src/tui/terminalLauncher.ts
3066
3105
  var import_child_process3 = require("child_process");
3067
3106
  var import_util = require("util");
3068
- var import_core15 = require("@ordewell/core");
3107
+ var import_core16 = require("@ordewell/core");
3069
3108
  var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
3070
3109
  var defaultExecFile = async (command, args) => {
3071
3110
  const { stdout, stderr } = await execFileAsync(command, args);
@@ -3085,16 +3124,16 @@ var defaultWhich = (bin) => {
3085
3124
  async function openTaskTerminal(port, planSessionId, taskId, deps = {}) {
3086
3125
  const platform = deps.platform ?? process.platform;
3087
3126
  const env = deps.env ?? process.env;
3088
- const hasTmuxImpl = deps.hasTmuxImpl ?? import_core15.hasTmux;
3127
+ const hasTmuxImpl = deps.hasTmuxImpl ?? import_core16.hasTmux;
3089
3128
  const execFileImpl = deps.execFileImpl ?? defaultExecFile;
3090
3129
  const spawnImpl = deps.spawnImpl ?? defaultSpawn;
3091
3130
  const which = deps.which ?? defaultWhich;
3092
3131
  if (!hasTmuxImpl()) {
3093
3132
  return { ok: false, message: "tmux is not installed \u2014 install it to get a real terminal per task." };
3094
3133
  }
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);
3134
+ const session = (0, import_core16.tmuxSessionName)(port);
3135
+ const socket = (0, import_core16.tmuxSocketName)(port);
3136
+ const base = (0, import_core16.tmuxWindowName)(taskId, planSessionId);
3098
3137
  let windows;
3099
3138
  try {
3100
3139
  const { stdout } = await execFileImpl("tmux", ["-L", socket, "list-windows", "-t", session, "-F", "#{window_name}"]);
@@ -3388,7 +3427,7 @@ Dependencies \xB7 #${task.order} ${task.title}
3388
3427
  }
3389
3428
 
3390
3429
  // src/tui/index.ts
3391
- var import_core20 = require("@ordewell/core");
3430
+ var import_core22 = require("@ordewell/core");
3392
3431
 
3393
3432
  // src/tui/editor.ts
3394
3433
  function emptyEditor() {
@@ -3550,6 +3589,7 @@ var SLASH_COMMANDS = [
3550
3589
  { name: "allowlist", usage: "/allowlist [set <runner> <ids> | clear <runner>]", description: "Limit which models a runner may use", category: "models" },
3551
3590
  { name: "runners", usage: "/runners [<id> on|off]", description: "Enable or disable runners (claude-code, opencode, codex)", category: "models" },
3552
3591
  { name: "auto", usage: "/auto [on|off]", description: "Autonomous permission mode for new plans", category: "models" },
3592
+ { name: "parallel", usage: "/parallel [<n>]", description: "How many AI tasks run at once (applies to a live run)", category: "models" },
3553
3593
  { name: "refresh", usage: "/refresh", description: "Re-discover runners and model catalogs", category: "models" },
3554
3594
  // Skills
3555
3595
  { name: "tdd", usage: "/tdd [on|off]", description: "Test-Driven Development mode", category: "skills" },
@@ -3774,6 +3814,7 @@ function chatEditorRoom(cols, active) {
3774
3814
  }
3775
3815
 
3776
3816
  // src/tui/state.ts
3817
+ var import_core17 = require("@ordewell/core");
3777
3818
  var SKILL_IDS = ["tdd", "verify"];
3778
3819
  function noSkills() {
3779
3820
  return { tdd: false, verify: false };
@@ -3838,6 +3879,7 @@ function initialState(overrides = {}) {
3838
3879
  orchestratorModel: "",
3839
3880
  plannerProvider: "",
3840
3881
  plannerEffort: "",
3882
+ maxParallel: import_core17.DEFAULT_MAX_PARALLEL,
3841
3883
  configuredProviders: [],
3842
3884
  allowlist: {},
3843
3885
  autonomous: true,
@@ -3857,7 +3899,7 @@ function initialState(overrides = {}) {
3857
3899
  }
3858
3900
 
3859
3901
  // src/tui/layout.ts
3860
- var import_core16 = require("@ordewell/core");
3902
+ var import_core18 = require("@ordewell/core");
3861
3903
  var CHROME_ROWS = 4;
3862
3904
  function chatInputWrap(state) {
3863
3905
  const active = state.focus === "chat" && !state.overlay;
@@ -3993,9 +4035,9 @@ function chatScrollMax(state) {
3993
4035
  }
3994
4036
  function plannerSetup(state) {
3995
4037
  const provider = state.plannerProvider;
3996
- const registration = import_core16.ALL_PROVIDERS[provider];
4038
+ const registration = import_core18.ALL_PROVIDERS[provider];
3997
4039
  if (!registration) return { label: "not set", ready: false };
3998
- const runner = (0, import_core16.runnerForProvider)(provider);
4040
+ const runner = (0, import_core18.runnerForProvider)(provider);
3999
4041
  const ready = runner ? state.runners.some((r) => r.id === runner) : state.configuredProviders.includes(provider);
4000
4042
  return { label: registration.label, ready };
4001
4043
  }
@@ -4178,7 +4220,7 @@ function taskLines(state, row, index, cols) {
4178
4220
  const icon = paint(rawIcon);
4179
4221
  const kind = running ? style.yellow("RUN") : task.type === "user" ? style.yellow("MAN") : style.grey(" AI");
4180
4222
  const caret = selected ? style.cyan("\u276F") : " ";
4181
- const orderLabel = parent ? (0, import_core16.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
4223
+ const orderLabel = parent ? (0, import_core18.taskOrderLabel)(task, parent) : String(task.order).padStart(2);
4182
4224
  const indent = parent ? " " : "";
4183
4225
  const head = `${indent}${caret} ${icon} ${orderLabel} ${kind} `;
4184
4226
  const titleRoom = Math.max(1, cols - width(head));
@@ -4199,21 +4241,22 @@ function taskLines(state, row, index, cols) {
4199
4241
  const mode = task.type === "ai" ? `mode: ${task.taskMode ?? "default"}${modeInfo?.autonomous ? style.yellow(" \u26A1") : ""}` : "";
4200
4242
  const runner = task.assignedRunner ?? "";
4201
4243
  const bodyPad = parent ? " " : " ";
4202
- const meta = [running ? "working" : "", runner, model].filter(Boolean).join(" \xB7 ");
4244
+ const activity = idle ? "quiet \u2014 t opens its terminal" : running ? "working" : "";
4245
+ const meta = [activity, runner, model].filter(Boolean).join(" \xB7 ");
4203
4246
  if (meta) lines.push(style.grey(truncate(`${bodyPad}${meta}`, cols)));
4204
4247
  if (effort || mode) lines.push(style.grey(truncate(`${bodyPad}${[effort, mode].filter(Boolean).join(" \xB7 ")}`, cols)));
4205
4248
  if (task.isolation?.state === "conflict") {
4206
4249
  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)})` : "";
4250
+ const files = task.isolation.conflictFiles?.length ? ` (${(0, import_core18.capConflictFiles)(task.isolation.conflictFiles)})` : "";
4208
4251
  lines.push(style.red(truncate(`${bodyPad}\u26A0 merge conflict${where}${files} \u2014 its work is kept on its own branch`, cols)));
4209
4252
  }
4210
4253
  if (task.isolation?.state === "repairing") {
4211
- const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core16.capConflictFiles)(task.isolation.conflictFiles)}` : "";
4254
+ const files = task.isolation.conflictFiles?.length ? ` in ${(0, import_core18.capConflictFiles)(task.isolation.conflictFiles)}` : "";
4212
4255
  const attempt = task.isolation.repair ? ` (attempt ${task.isolation.repair.attempt}/${task.isolation.repair.limit})` : "";
4213
4256
  lines.push(style.yellow(truncate(`${bodyPad}\u21BB repairing conflict${files}${attempt}`, cols)));
4214
4257
  }
4215
4258
  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)));
4259
+ lines.push(style.grey(truncate(`${bodyPad}\u21BB landed after repairing conflict in ${(0, import_core18.capConflictFiles)(task.isolation.repairedFiles)}`, cols)));
4217
4260
  }
4218
4261
  let editorLine;
4219
4262
  if (expanded) {
@@ -4298,7 +4341,7 @@ function helpScrollMax(state) {
4298
4341
  }
4299
4342
 
4300
4343
  // src/tui/render.ts
4301
- var import_core17 = require("@ordewell/core");
4344
+ var import_core19 = require("@ordewell/core");
4302
4345
  function render(state) {
4303
4346
  const { rows, cols } = state;
4304
4347
  const rowsForBody = bodyRows(state);
@@ -4562,7 +4605,7 @@ function renderHandoff(state, overlay, rows, cols) {
4562
4605
  return frame(`Diff \u2014 ${handoffBranch(handoff)}`, [...shown, "", style.grey(`${more}enter or esc goes back`)], rows, cols);
4563
4606
  }
4564
4607
  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)})`) : "";
4608
+ const repaired = task.repairedFiles?.length ? style.yellow(` \u2014 repaired (${(0, import_core19.capConflictFiles)(task.repairedFiles)})`) : "";
4566
4609
  return ` ${style.green("\u2713")} #${task.order} ${task.title}${repaired}`;
4567
4610
  });
4568
4611
  const actions = handoffActions(handoff).map((action, index) => {
@@ -4652,9 +4695,9 @@ function frame(title, body, rows, cols) {
4652
4695
  }
4653
4696
 
4654
4697
  // src/tui/reducer.ts
4655
- var import_core18 = require("@ordewell/core");
4698
+ var import_core20 = require("@ordewell/core");
4656
4699
  var import_plan_utils3 = require("@ordewell/core/plan-utils");
4657
- var KNOWN_PROVIDERS2 = Object.keys(import_core18.ALL_PROVIDERS);
4700
+ var KNOWN_PROVIDERS2 = Object.keys(import_core20.ALL_PROVIDERS);
4658
4701
  var DEFAULT_EFFORT = "__runner_default__";
4659
4702
  function step(state, effects = []) {
4660
4703
  return { state, effects };
@@ -4673,6 +4716,17 @@ function alreadySpoken(messages, content) {
4673
4716
  const redrawn = messages.at(-1)?.role === "system" && messages.at(-1)?.content === content;
4674
4717
  return spoken || redrawn;
4675
4718
  }
4719
+ function runLabel(tasks) {
4720
+ const running = tasks.filter(isTaskRunning);
4721
+ if (running.length > 0) {
4722
+ const [first] = running;
4723
+ const name = first.assignedRunner ? `${first.title} \xB7 ${first.assignedRunner}` : first.title;
4724
+ return running.length > 1 ? `${name} (+${running.length - 1} more)` : name;
4725
+ }
4726
+ const waiting = tasks.filter((t) => t.status === "awaiting_user").length;
4727
+ if (waiting === 0) return "";
4728
+ return waiting === 1 ? "1 task waits for you" : `${waiting} tasks wait for you`;
4729
+ }
4676
4730
  function researchLabel(messages, summary) {
4677
4731
  const others = messages.filter(isPendingResearch).length - 1;
4678
4732
  return others > 0 ? `${summary} (+${others} more)` : summary;
@@ -4776,6 +4830,16 @@ function reduce(state, action) {
4776
4830
  const status = state.status === "planning" ? "researching" : state.status;
4777
4831
  return step({ ...spoken, status, busyLabel: researchLabel(spoken.messages, summary) });
4778
4832
  }
4833
+ // A settled transcript line, not a research step: nothing ever settles a
4834
+ // task's start, so as a step it stayed "⋯" forever and was counted into
4835
+ // the planner's next "(+N more)".
4836
+ case "taskStarted": {
4837
+ if (stale(state, action.sessionId)) return step(state);
4838
+ const tasks = state.tasks.map((t) => t.id === action.taskId && !isTaskRunning(t) ? { ...t, status: "in_progress" } : t);
4839
+ const who = action.runner ? ` \xB7 ${action.runner}` : "";
4840
+ const spoken = say(state, "system", sanitize(`Started "${action.title}"${who}`));
4841
+ return step({ ...spoken, tasks, status: "executing", busyLabel: runLabel(tasks) });
4842
+ }
4779
4843
  case "researchStepDone": {
4780
4844
  if (stale(state, action.sessionId)) return step(state);
4781
4845
  const messages = settleResearchStep(state.messages, action);
@@ -4816,7 +4880,7 @@ function reduce(state, action) {
4816
4880
  return t;
4817
4881
  });
4818
4882
  if (!changed) return step(state);
4819
- return step({ ...state, status: "executing", tasks });
4883
+ return step({ ...state, status: "executing", tasks, busyLabel: runLabel(tasks) });
4820
4884
  }
4821
4885
  case "queueReady": {
4822
4886
  if (stale(state, action.sessionId)) return step(state);
@@ -4986,6 +5050,7 @@ function applySettings(state, settings) {
4986
5050
  orchestratorModel: typeof settings.orchestratorModel === "string" ? settings.orchestratorModel : state.orchestratorModel,
4987
5051
  plannerProvider: typeof settings.aiProvider === "string" ? settings.aiProvider : state.plannerProvider,
4988
5052
  plannerEffort: typeof settings.plannerThinkingEffort === "string" ? settings.plannerThinkingEffort : state.plannerEffort,
5053
+ maxParallel: typeof settings.maxParallel === "number" ? settings.maxParallel : state.maxParallel,
4989
5054
  allowlist: settings.modelAllowlist && typeof settings.modelAllowlist === "object" ? settings.modelAllowlist : state.allowlist
4990
5055
  };
4991
5056
  }
@@ -5002,11 +5067,11 @@ function modelDetail(m) {
5002
5067
  function providerErrorHint(state) {
5003
5068
  const failed = Object.keys(state.providerErrors ?? {});
5004
5069
  if (failed.length === 0) return void 0;
5005
- const names = failed.map((p) => import_core18.ALL_PROVIDERS[p]?.label ?? p);
5070
+ const names = failed.map((p) => import_core20.ALL_PROVIDERS[p]?.label ?? p);
5006
5071
  return `\u26A0 Unavailable (key or fetch failed): ${names.join(", ")} \u2014 showing working providers only.`;
5007
5072
  }
5008
5073
  function plannerModelItems(state) {
5009
- const runner = (0, import_core18.runnerForProvider)(state.plannerProvider);
5074
+ const runner = (0, import_core20.runnerForProvider)(state.plannerProvider);
5010
5075
  if (!runner) return state.orchestratorModels.map((m) => ({ id: m.id, label: m.label, detail: modelDetail(m) }));
5011
5076
  const items = state.models.filter((m) => m.runners?.includes(runner)).map((m) => ({
5012
5077
  id: m.id,
@@ -5020,7 +5085,7 @@ function plannerModelItems(state) {
5020
5085
  return items.length > 0 ? items : [{ id: "", label: `No ${runner} models discovered yet`, detail: "run /refresh", disabled: true }];
5021
5086
  }
5022
5087
  function plannerEffortItems(state) {
5023
- const runner = (0, import_core18.runnerForProvider)(state.plannerProvider);
5088
+ const runner = (0, import_core20.runnerForProvider)(state.plannerProvider);
5024
5089
  if (!runner) {
5025
5090
  return [{ id: "", label: "Thinking effort applies to a coding-agent planner", detail: "switch with /planner", disabled: true }];
5026
5091
  }
@@ -5601,6 +5666,8 @@ function runCommand(state, { name, args }) {
5601
5666
  return runners(state, args);
5602
5667
  case "auto":
5603
5668
  return setAutonomous(state, args[0]);
5669
+ case "parallel":
5670
+ return setMaxParallel(state, args[0]);
5604
5671
  case "mouse":
5605
5672
  return setMouseCapture(state, args[0]);
5606
5673
  case "sessions":
@@ -5769,7 +5836,7 @@ function newSession(state) {
5769
5836
  }
5770
5837
  function setModel(state, args) {
5771
5838
  if (args[0] === "set" && args[1]) {
5772
- const runner = (0, import_core18.runnerForProvider)(state.plannerProvider);
5839
+ const runner = (0, import_core20.runnerForProvider)(state.plannerProvider);
5773
5840
  const known = runner ? modelsForRunner(state.models, runner) : [];
5774
5841
  if (runner && known.length > 0 && !known.some((m) => m.id === args[1])) {
5775
5842
  return fail3(state, `${args[1]} was not discovered for ${runner}.`);
@@ -5791,21 +5858,21 @@ function setModel(state, args) {
5791
5858
  }
5792
5859
  function plannerItems(state) {
5793
5860
  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);
5861
+ const agents = import_core20.CLI_PROVIDERS.map((id) => {
5862
+ const runner = (0, import_core20.runnerForProvider)(id);
5796
5863
  const ready = installed.has(runner);
5797
5864
  return {
5798
5865
  id,
5799
- label: import_core18.ALL_PROVIDERS[id].label,
5866
+ label: import_core20.ALL_PROVIDERS[id].label,
5800
5867
  detail: ready ? "coding agent \xB7 no API key needed" : "CLI not installed or not on PATH",
5801
5868
  selected: state.plannerProvider === id,
5802
5869
  disabled: !ready
5803
5870
  };
5804
5871
  });
5805
- const vendors = import_core18.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
5872
+ const vendors = import_core20.PROVIDER_PRIORITY.filter((id) => state.configuredProviders.includes(id)).map((id) => ({
5806
5873
  id,
5807
- label: import_core18.ALL_PROVIDERS[id].label,
5808
- detail: import_core18.ALL_PROVIDERS[id].apiKeyEnvVar,
5874
+ label: import_core20.ALL_PROVIDERS[id].label,
5875
+ detail: import_core20.ALL_PROVIDERS[id].apiKeyEnvVar,
5809
5876
  selected: state.plannerProvider === id
5810
5877
  }));
5811
5878
  return vendors.length > 0 ? [...agents, ...vendors] : [...agents, { id: "", label: "No API providers configured", detail: "add a key with /key", disabled: true }];
@@ -5863,16 +5930,16 @@ function setKey(state, args) {
5863
5930
  }
5864
5931
  return step(state, [{ type: "setApiKey", provider, key: args.slice(2).join(" ") }]);
5865
5932
  }
5866
- const items = import_core18.PROVIDER_PRIORITY.map((id) => ({
5933
+ const items = import_core20.PROVIDER_PRIORITY.map((id) => ({
5867
5934
  id,
5868
- label: import_core18.ALL_PROVIDERS[id].label,
5869
- detail: import_core18.ALL_PROVIDERS[id].apiKeyEnvVar,
5935
+ label: import_core20.ALL_PROVIDERS[id].label,
5936
+ detail: import_core20.ALL_PROVIDERS[id].apiKeyEnvVar,
5870
5937
  selected: state.configuredProviders.includes(id)
5871
5938
  }));
5872
5939
  return step({ ...state, overlay: { kind: "picker", picker: picker("API provider key", items, { kind: "set-key" }) } });
5873
5940
  }
5874
5941
  function keyPrompt(provider) {
5875
- const meta = import_core18.ALL_PROVIDERS[provider];
5942
+ const meta = import_core20.ALL_PROVIDERS[provider];
5876
5943
  return {
5877
5944
  kind: "prompt",
5878
5945
  title: `${meta.label} API key`,
@@ -5947,6 +6014,12 @@ function runners(state, args) {
5947
6014
  })
5948
6015
  } });
5949
6016
  }
6017
+ function setMaxParallel(state, arg) {
6018
+ 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.`));
6019
+ const limit = (0, import_core20.parseMaxParallel)(arg);
6020
+ if (limit === null) return fail3(state, "Usage: /parallel [<number of tasks, 1 or more>]");
6021
+ return step({ ...state, maxParallel: limit }, [{ type: "setMaxParallel", limit }]);
6022
+ }
5950
6023
  function setAutonomous(state, arg) {
5951
6024
  const enabled = resolveToggle(arg, state.autonomous);
5952
6025
  if (enabled === null) return fail3(state, "Usage: /auto [on|off]");
@@ -6202,7 +6275,9 @@ function createApp(deps) {
6202
6275
  function dispatch(action) {
6203
6276
  if (exited) return;
6204
6277
  const result = reduce(state, action);
6278
+ const previousSession = state.sessionId;
6205
6279
  state = result.state;
6280
+ if (state.sessionId && state.sessionId !== previousSession) deps.onSessionChange?.(state.sessionId, state);
6206
6281
  if (state.exiting) {
6207
6282
  exited = true;
6208
6283
  clearInterval(spinnerInterval);
@@ -6234,7 +6309,7 @@ function createApp(deps) {
6234
6309
 
6235
6310
  // src/tui/effects.ts
6236
6311
  var import_child_process4 = require("child_process");
6237
- var import_core19 = require("@ordewell/core");
6312
+ var import_core21 = require("@ordewell/core");
6238
6313
  var import_plan_utils4 = require("@ordewell/core/plan-utils");
6239
6314
  var ConversationQueue = class {
6240
6315
  tail = Promise.resolve();
@@ -6405,7 +6480,7 @@ async function perform(effect, deps) {
6405
6480
  dispatch({ type: "notice", message: `Orchestrator model set to ${effect.modelId}.` });
6406
6481
  return;
6407
6482
  case "setPlanner": {
6408
- const meta = import_core19.ALL_PROVIDERS[effect.provider];
6483
+ const meta = import_core21.ALL_PROVIDERS[effect.provider];
6409
6484
  if (!meta) throw new Error(`Unknown planner: ${effect.provider}`);
6410
6485
  const env = { AI_PROVIDER: effect.provider };
6411
6486
  const settings = await api.updateSettings({ env });
@@ -6417,7 +6492,7 @@ async function perform(effect, deps) {
6417
6492
  });
6418
6493
  dispatch({
6419
6494
  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)}`
6495
+ message: `${(0, import_core21.isCliProvider)(effect.provider) ? `Planning with ${meta.label} \u2014 no API key needed.` : `Planner set to ${meta.label}.`} ${plannerModelNotice(recall2)}`
6421
6496
  });
6422
6497
  await loadModels(deps);
6423
6498
  return;
@@ -6429,7 +6504,7 @@ async function perform(effect, deps) {
6429
6504
  dispatch({ type: "notice", message: `Planner effort set to ${effect.effort || "the runner default"}.` });
6430
6505
  return;
6431
6506
  case "setApiKey": {
6432
- const meta = import_core19.ALL_PROVIDERS[effect.provider];
6507
+ const meta = import_core21.ALL_PROVIDERS[effect.provider];
6433
6508
  if (!meta) throw new Error(`Unknown provider: ${effect.provider}`);
6434
6509
  await api.updateSettings({ env: { [meta.apiKeyEnvVar]: effect.key } });
6435
6510
  deps.setEnvVar(meta.apiKeyEnvVar, effect.key);
@@ -6460,6 +6535,11 @@ async function perform(effect, deps) {
6460
6535
  dispatch({ type: "notice", message: effect.message });
6461
6536
  return;
6462
6537
  }
6538
+ case "setMaxParallel":
6539
+ await api.updateSettings({ env: { ORDEWELL_MAX_PARALLEL: String(effect.limit) } });
6540
+ deps.setEnvVar("ORDEWELL_MAX_PARALLEL", String(effect.limit));
6541
+ dispatch({ type: "notice", message: `Up to ${effect.limit} AI task${effect.limit === 1 ? "" : "s"} now run at once.` });
6542
+ return;
6463
6543
  case "setAutonomous":
6464
6544
  deps.setEnvVar("ORDEWELL_AUTONOMOUS_MODE", String(effect.enabled));
6465
6545
  dispatch({ type: "notice", message: `Autonomous mode ${effect.enabled ? "on" : "off"}.` });
@@ -6590,7 +6670,7 @@ function osc52(text) {
6590
6670
  function copySelection(deps, text) {
6591
6671
  if (!text) return;
6592
6672
  const lines = text.split("\n").length;
6593
- const command = (0, import_core19.clipboardCopyCommand)(deps.hasBin);
6673
+ const command = (0, import_core21.clipboardCopyCommand)(deps.hasBin);
6594
6674
  if (command) {
6595
6675
  try {
6596
6676
  (deps.pipeToClipboard ?? pipeToClipboardDefault)(command, text);
@@ -6727,12 +6807,12 @@ function onExecutionEvent(dispatch, event, sessionId) {
6727
6807
  return;
6728
6808
  }
6729
6809
  case "task_started":
6730
- dispatch({ type: "researchStep", summary: `${event.title ?? event.taskId} \xB7 ${event.runner ?? ""}`.trim(), sessionId });
6810
+ dispatch({ type: "taskStarted", taskId: String(event.taskId), title: String(event.title ?? event.taskId), runner: event.runner, sessionId });
6731
6811
  return;
6732
6812
  // The extension shows these in its checkpoint panel; here they are
6733
6813
  // transcript lines, which is the TUI's equivalent surface.
6734
6814
  case "checkpoint":
6735
- dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core19.truncateCheckpointSummary)(event.summary)}` });
6815
+ dispatch({ type: "notice", message: `\xB7 Checkpoint \u2014 ${event.taskTitle}: ${(0, import_core21.truncateCheckpointSummary)(event.summary)}` });
6736
6816
  return;
6737
6817
  case "approval_decided":
6738
6818
  dispatch({ type: "notice", message: describeApprovalDecision2(event) });
@@ -7116,7 +7196,7 @@ async function handleTui(subArgs) {
7116
7196
  }
7117
7197
  const workspace = flag(subArgs, "--workspace") || process.cwd();
7118
7198
  try {
7119
- (0, import_core20.assertWorkspaceExists)(workspace);
7199
+ (0, import_core22.assertWorkspaceExists)(workspace);
7120
7200
  } catch {
7121
7201
  console.error(
7122
7202
  `Workspace "${workspace}" does not exist.
@@ -7125,7 +7205,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
7125
7205
  );
7126
7206
  process.exit(1);
7127
7207
  }
7128
- registerSkillCommands((0, import_core20.createSkillsService)(workspace).listSkills());
7208
+ registerSkillCommands((0, import_core22.createSkillsService)(workspace).listSkills());
7129
7209
  const explicitPort = flag(subArgs, "--port") || process.env.ORDEWELL_PORT;
7130
7210
  const targetPort = explicitPort ? resolvePort(subArgs) : await findFreePort();
7131
7211
  const { port, owned: ownedAtLaunch } = await ensureDaemonOwned(targetPort, { detached: false });
@@ -7144,7 +7224,7 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
7144
7224
  conversationQueue,
7145
7225
  port,
7146
7226
  dispatch: (action) => app.dispatch(action),
7147
- newSessionId: () => (0, import_core20.mintSessionId)(),
7227
+ newSessionId: () => (0, import_core22.mintSessionId)(),
7148
7228
  setEnvVar: (key, value) => {
7149
7229
  writeEnvVar(findEnvFile(), key, value);
7150
7230
  process.env[key] = value;
@@ -7161,7 +7241,16 @@ cd into a directory that exists, or pass --workspace <path> naming one that does
7161
7241
  },
7162
7242
  exit: () => shutdown(0)
7163
7243
  }),
7164
- onExit: () => shutdown(0)
7244
+ onExit: () => shutdown(0),
7245
+ // The same pointer `ordewell plan` leaves, so `ordewell handoff`,
7246
+ // `terminal <n>` and the other one-shot commands act on the session this
7247
+ // TUI is showing instead of saying there is none.
7248
+ onSessionChange: (sessionId, state) => {
7249
+ try {
7250
+ saveLastSession(sessionId, state.goal, state.runners.filter((r) => r.enabled).map((r) => r.id), workspace);
7251
+ } catch {
7252
+ }
7253
+ }
7165
7254
  });
7166
7255
  terminal = openTerminal({
7167
7256
  mouse: mouseCapture,
@@ -7207,6 +7296,7 @@ var COMMANDS = {
7207
7296
  model: handleModel,
7208
7297
  planner: handlePlanner,
7209
7298
  "planner-effort": handlePlannerEffort,
7299
+ parallel: handleParallel,
7210
7300
  key: handleKey,
7211
7301
  runners: handleRunners,
7212
7302
  auto: handleAuto,
@@ -7253,10 +7343,12 @@ function cliVersion() {
7253
7343
  }
7254
7344
 
7255
7345
  // src/main.ts
7346
+ var TUI_FLAGS = /* @__PURE__ */ new Set(["--workspace", "--port"]);
7256
7347
  async function main() {
7257
- (0, import_core21.migrateOldConfigDir)();
7348
+ (0, import_core23.migrateOldConfigDir)();
7258
7349
  loadEnvFile();
7259
7350
  const argv = process.argv.slice(2);
7351
+ if (TUI_FLAGS.has(argv[0] ?? "")) argv.unshift("tui");
7260
7352
  const command = argv[0] ?? (process.stdin.isTTY ? "tui" : "--help");
7261
7353
  if (command === "--help" || command === "-h") {
7262
7354
  printHelp();
@@ -7266,6 +7358,10 @@ async function main() {
7266
7358
  console.log(cliVersion());
7267
7359
  return;
7268
7360
  }
7361
+ const sessionAt = argv.indexOf("--session-id");
7362
+ if (sessionAt > 0 && argv[sessionAt + 1]) {
7363
+ argv[sessionAt + 1] = expandSessionId(argv[sessionAt + 1], flag(argv, "--workspace") || process.cwd());
7364
+ }
7269
7365
  const handler = COMMANDS[command];
7270
7366
  if (!handler) {
7271
7367
  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.2",
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.2",
52
+ "@ordewell/web": "^0.5.2",
53
53
  "string-width": "^7.2.0",
54
54
  "ws": "^8.18.0"
55
55
  },