claude-overnight 1.25.30 → 1.25.33

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.
@@ -1 +1 @@
1
- export declare const VERSION = "1.25.30";
1
+ export declare const VERSION = "1.25.33";
package/dist/_version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by build — do not edit manually.
2
- export const VERSION = "1.25.30";
2
+ export const VERSION = "1.25.33";
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { VERSION } from "./_version.js";
7
7
  import { query } from "@anthropic-ai/claude-agent-sdk";
8
8
  import { Swarm } from "./swarm.js";
9
9
  import { planTasks, refinePlan, identifyThemes, buildThinkingTasks, orchestrate, salvageFromFile } from "./planner.js";
10
- import { modelDisplayName, formatContextWindow, DEFAULT_MODEL } from "./models.js";
10
+ import { formatContextWindow, DEFAULT_MODEL } from "./models.js";
11
11
  import { setPlannerEnvResolver } from "./planner-query.js";
12
12
  import { setTranscriptRunDir } from "./transcripts.js";
13
13
  import { getProxyPort, buildProxyUrl } from "./proxy-port.js";
@@ -15,9 +15,10 @@ import { pickModel, loadProviders, preflightProvider, buildEnvResolver, healthCh
15
15
  import { RunDisplay } from "./ui.js";
16
16
  import { renderSummary, wrap } from "./render.js";
17
17
  import { executeRun } from "./run.js";
18
- import { parseCliFlags, isAuthError, fetchModels, ask, select, selectKey, loadTaskFile, validateConcurrency, isGitRepo, validateGitRepo, showPlan, BRAILLE, makeProgressLog, } from "./cli.js";
18
+ import { parseCliFlags, isAuthError, fetchModels, ask, select, selectKey, loadTaskFile, validateConcurrency, isGitRepo, validateGitRepo, showPlan, makeProgressLog, } from "./cli.js";
19
19
  import { loadRunState, findIncompleteRuns, findOrphanedDesigns, backfillOrphanedPlans, formatTimeAgo, showRunHistory, readPreviousRunKnowledge, createRunDir, updateLatestSymlink, readMdDir, saveRunState, autoMergeBranches, } from "./state.js";
20
20
  import { runSetupCoach, loadUserSettings, saveUserSettings, COACH_MODEL } from "./coach.js";
21
+ import { editRunSettings, formatSettingsSummary } from "./settings.js";
21
22
  function countTasksInFile(path) {
22
23
  try {
23
24
  const parsed = JSON.parse(readFileSync(path, "utf-8"));
@@ -66,8 +67,6 @@ async function promptResumeOverrides(state, cliFlags, argv, noTTY, runDir) {
66
67
  catch { }
67
68
  return;
68
69
  }
69
- // Kick off model fetch in the background so it's ready if the user picks Edit.
70
- const modelsPromise = fetchModels(20_000).catch(() => []);
71
70
  // ── Interactive review ──
72
71
  const fmtSummary = () => {
73
72
  const remaining = Math.max(1, state.remaining);
@@ -90,6 +89,7 @@ async function promptResumeOverrides(state, cliFlags, argv, noTTY, runDir) {
90
89
  console.log(` ${chalk.dim("concur ")}${chalk.white(String(state.concurrency))}`);
91
90
  console.log(` ${chalk.dim("usage cap ")}${chalk.white(capStr)}`);
92
91
  console.log(` ${chalk.dim("extra ")}${chalk.white(extraStr)}`);
92
+ console.log(` ${chalk.dim("perms ")}${chalk.white(state.permissionMode === "bypassPermissions" ? "yolo" : state.permissionMode)}`);
93
93
  };
94
94
  fmtSummary();
95
95
  const action = await selectKey("", [
@@ -101,64 +101,24 @@ async function promptResumeOverrides(state, cliFlags, argv, noTTY, runDir) {
101
101
  process.exit(0);
102
102
  if (action === "r")
103
103
  return;
104
- // ── Edit walk ──
105
- let modelFrame = 0;
106
- const modelSpinner = setInterval(() => {
107
- process.stdout.write(`\x1B[2K\r ${chalk.cyan(BRAILLE[modelFrame++ % BRAILLE.length])} ${chalk.dim("loading models...")}`);
108
- }, 120);
109
- let models;
110
- try {
111
- models = await modelsPromise;
112
- }
113
- finally {
114
- clearInterval(modelSpinner);
115
- process.stdout.write(`\x1B[2K\r`);
116
- }
117
- const pick = await pickModel(`${chalk.cyan("①")} Worker model:`, models, state.workerProviderId ?? state.workerModel);
118
- state.workerModel = pick.model;
119
- state.workerProviderId = pick.providerId;
120
- const remAns = await ask(`\n ${chalk.cyan("②")} Remaining sessions ${chalk.dim(`[${state.remaining}]:`)} `);
121
- const parsedRem = parseInt(remAns);
122
- if (!isNaN(parsedRem) && parsedRem > 0) {
123
- state.remaining = parsedRem;
124
- state.budget = state.accCompleted + state.accFailed + parsedRem;
125
- }
126
- const concAns = await ask(`\n ${chalk.cyan("③")} Concurrency ${chalk.dim(`[${state.concurrency}]:`)} `);
127
- const parsedConc = parseInt(concAns);
128
- if (!isNaN(parsedConc) && parsedConc >= 1)
129
- state.concurrency = parsedConc;
130
- const currentCap = state.usageCap != null ? String(Math.round(state.usageCap * 100)) : "off";
131
- const capAns = await ask(`\n ${chalk.cyan("④")} Usage cap % ${chalk.dim(`[${currentCap}]`)} ${chalk.dim("(0 = off):")} `);
132
- if (capAns.trim()) {
133
- const v = parseFloat(capAns);
134
- if (!isNaN(v) && v >= 0 && v <= 100)
135
- state.usageCap = v > 0 ? v / 100 : undefined;
136
- }
137
- const currentExtra = state.allowExtraUsage
138
- ? (state.extraUsageBudget ? `$${state.extraUsageBudget}` : "unlimited")
139
- : "off";
140
- const extraChoice = await select(`${chalk.cyan("⑤")} Extra usage ${chalk.dim(`[current: ${currentExtra}]`)}:`, [
141
- { name: "Keep current", value: "keep" },
142
- { name: "Off", value: "off", hint: "stop at plan limit" },
143
- { name: "With $ cap", value: "budget", hint: "set a spending cap" },
144
- { name: "Unlimited", value: "unlimited", hint: "no cap, billed as overage" },
145
- ]);
146
- if (extraChoice === "off") {
147
- state.allowExtraUsage = false;
148
- state.extraUsageBudget = undefined;
149
- }
150
- else if (extraChoice === "budget") {
151
- const bAns = await ask(` ${chalk.dim("Max extra $:")} `);
152
- const bVal = parseFloat(bAns);
153
- if (!isNaN(bVal) && bVal > 0) {
154
- state.extraUsageBudget = bVal;
155
- state.allowExtraUsage = true;
156
- }
157
- }
158
- else if (extraChoice === "unlimited") {
159
- state.allowExtraUsage = true;
160
- state.extraUsageBudget = undefined;
161
- }
104
+ const settings = {
105
+ workerModel: state.workerModel,
106
+ plannerModel: state.plannerModel,
107
+ fastModel: state.fastModel,
108
+ workerProviderId: state.workerProviderId,
109
+ plannerProviderId: state.plannerProviderId,
110
+ fastProviderId: state.fastProviderId,
111
+ concurrency: state.concurrency,
112
+ usageCap: state.usageCap,
113
+ allowExtraUsage: state.allowExtraUsage ?? false,
114
+ extraUsageBudget: state.extraUsageBudget,
115
+ permissionMode: state.permissionMode,
116
+ };
117
+ await editRunSettings({
118
+ current: settings,
119
+ cliConcurrencySet: !!cliFlags.concurrency,
120
+ });
121
+ Object.assign(state, settings);
162
122
  try {
163
123
  saveRunState(runDir, state);
164
124
  }
@@ -595,7 +555,7 @@ async function main() {
595
555
  const qwenProviders = providers.filter(p => p.model?.toLowerCase().includes("qwen"));
596
556
  const options = [];
597
557
  if (hasAnthropicKey)
598
- options.push({ key: "1", desc: ` — ${COACH_MODEL} (default, cheapest)` });
558
+ options.push({ key: "1", desc: ` — ${COACH_MODEL} (cheapest)` });
599
559
  if (qwenProviders.length > 0)
600
560
  options.push({ key: "2", desc: ` — ${qwenProviders[0].displayName} (${qwenProviders[0].model})` });
601
561
  if (cursorProviders.length > 0)
@@ -637,7 +597,6 @@ async function main() {
637
597
  objective = coachResult.improvedObjective;
638
598
  }
639
599
  }
640
- const modelsPromise = fetchModels();
641
600
  const defaultBudget = coachResult?.recommended.budget ?? 10;
642
601
  const budgetAns = await ask(`\n ${chalk.cyan("②")} ${chalk.dim("Budget")} ${chalk.dim("[")}${chalk.white(String(defaultBudget))}${chalk.dim("]:")} `);
643
602
  budget = parseInt(budgetAns) || defaultBudget;
@@ -645,91 +604,42 @@ async function main() {
645
604
  console.error(chalk.red(` Budget must be a positive number`));
646
605
  process.exit(1);
647
606
  }
648
- // ③ Max concurrency (skip if --concurrency set)
649
- if (cliFlags.concurrency) {
650
- concurrency = parseInt(cliFlags.concurrency);
651
- }
652
- else {
653
- const defaultC = Math.min(coachResult?.recommended.concurrency ?? 5, budget);
654
- const concAns = await ask(`\n ${chalk.cyan("③")} ${chalk.dim("Max concurrency")} ${chalk.dim("[")}${chalk.white(String(defaultC))}${chalk.dim("]:")} `);
655
- concurrency = parseInt(concAns) || defaultC;
656
- if (concurrency < 1)
657
- concurrency = 1;
658
- }
659
- let modelFrame = 0;
660
- const modelSpinner = setInterval(() => {
661
- process.stdout.write(`\x1B[2K\r ${chalk.cyan(BRAILLE[modelFrame++ % BRAILLE.length])} ${chalk.dim("loading models...")}`);
662
- }, 120);
663
- let models;
664
- try {
665
- models = await modelsPromise;
666
- }
667
- finally {
668
- clearInterval(modelSpinner);
669
- process.stdout.write(`\x1B[2K\r`);
670
- }
671
- const plannerPick = await pickModel(`${chalk.cyan("④")} Planner model ${chalk.dim("(thinking, steering -- use your strongest)")}:`, models, coachResult?.recommended.plannerModel);
672
- plannerModel = plannerPick.model;
673
- plannerProvider = plannerPick.provider;
674
- const workerPick = await pickModel(`${chalk.cyan("⑤")} Worker model ${chalk.dim("(what runs the tasks -- Qwen 3.6 Plus / OpenRouter / etc via Other…)")}:`, models, coachResult?.recommended.workerModel);
675
- workerModel = workerPick.model;
676
- workerProvider = workerPick.provider;
677
- // ⑤b Optional fast model for quick tasks that will be verified
678
- const suggestFast = !!coachResult?.recommended.fastModel;
679
- const fastChoice = await select(`${chalk.cyan("⑤b")} Fast model ${chalk.dim("(optional -- Haiku/Qwen for quick tasks, checked by worker)")}:`, [
680
- { name: "Skip", value: "skip", hint: "two-tier mode only (current setup)" },
681
- { name: "Pick a fast model", value: "pick", hint: "Haiku, Qwen, or any provider -- for well-scoped tasks" },
682
- ], suggestFast ? 1 : 0);
683
- if (fastChoice === "pick") {
684
- const fastPick = await pickModel(`${chalk.cyan("⑤c")} Fast model:`, models, coachResult?.recommended.fastModel ?? undefined);
685
- fastModel = fastPick.model;
686
- fastProvider = fastPick.provider;
687
- }
688
- const usageCapItems = [
689
- { name: "Unlimited", value: undefined, hint: "full capacity, wait through rate limits" },
690
- { name: "90%", value: 0.9, hint: "leave 10% for other work" },
691
- { name: "75%", value: 0.75, hint: "conservative, plenty of headroom" },
692
- { name: "50%", value: 0.5, hint: "use half, keep the rest" },
693
- ];
694
- const coachCap = coachResult?.recommended.usageCap;
695
- const usageCapDefault = coachCap == null ? 0
696
- : coachCap >= 0.85 ? 1
697
- : coachCap >= 0.6 ? 2
698
- : 3;
699
- usageCap = await select(`${chalk.cyan("⑥")} Usage cap:`, usageCapItems, usageCapDefault);
700
- const extraChoice = await select(`${chalk.cyan("⑦")} Allow extra usage ${chalk.dim("(billed separately)")}:`, [
701
- { name: "No", value: "no", hint: "stop when plan limits are reached" },
702
- { name: "Yes, with $ limit", value: "budget", hint: "set a spending cap" },
703
- { name: "Yes, unlimited", value: "unlimited", hint: "keep going no matter what" },
704
- ]);
705
- if (extraChoice === "budget") {
706
- const budgetAns2 = await ask(` ${chalk.dim("Max extra usage $:")} `);
707
- extraUsageBudget = parseFloat(budgetAns2);
708
- if (!extraUsageBudget || extraUsageBudget <= 0)
709
- extraUsageBudget = 5;
710
- allowExtraUsage = true;
711
- }
712
- else if (extraChoice === "unlimited")
713
- allowExtraUsage = true;
714
- // ⑧ Permission mode (skip if --yolo or --perm set)
715
607
  const cliYolo = argv.includes("--yolo");
716
- if (cliFlags.perm) {
717
- permissionMode = cliFlags.perm;
718
- }
719
- else if (cliYolo) {
720
- permissionMode = "bypassPermissions";
721
- }
722
- else {
723
- const permItems = [
724
- { name: "Auto", value: "auto", hint: "accept low-risk, reject high-risk" },
725
- { name: "Bypass all", value: "bypassPermissions", hint: "agents can run anything (yolo)" },
726
- { name: "Prompt each", value: "default", hint: "ask for every dangerous op" },
727
- ];
728
- const permDefault = coachResult?.recommended.permissionMode === "bypassPermissions" ? 1
729
- : coachResult?.recommended.permissionMode === "default" ? 2 : 0;
730
- permissionMode = await select(`${chalk.cyan("⑧")} Permissions:`, permItems, permDefault);
731
- }
732
- // Worktrees + merge (skip if --yolo, --worktrees, --no-worktrees, or --merge set)
608
+ const coach = coachResult?.recommended;
609
+ const settingsDefaults = {
610
+ workerModel: coach?.workerModel ?? DEFAULT_MODEL,
611
+ plannerModel: coach?.plannerModel ?? DEFAULT_MODEL,
612
+ fastModel: coach?.fastModel ?? undefined,
613
+ concurrency: Math.min(coach?.concurrency ?? 5, budget),
614
+ usageCap: coach?.usageCap ?? undefined,
615
+ allowExtraUsage: false,
616
+ permissionMode: cliYolo ? "bypassPermissions" : coach?.permissionMode ?? "auto",
617
+ };
618
+ const settings = await editRunSettings({
619
+ current: settingsDefaults,
620
+ cliConcurrencySet: !!cliFlags.concurrency,
621
+ defaults: coach ? {
622
+ plannerModel: coach.plannerModel,
623
+ workerModel: coach.workerModel,
624
+ fastModel: coach.fastModel ?? undefined,
625
+ concurrency: Math.min(coach.concurrency, budget),
626
+ usageCap: coach.usageCap,
627
+ permissionMode: cliYolo ? "bypassPermissions" : coach.permissionMode,
628
+ } : undefined,
629
+ });
630
+ plannerModel = settings.plannerModel;
631
+ workerModel = settings.workerModel;
632
+ fastModel = settings.fastModel;
633
+ concurrency = settings.concurrency;
634
+ usageCap = settings.usageCap;
635
+ allowExtraUsage = settings.allowExtraUsage;
636
+ extraUsageBudget = settings.extraUsageBudget;
637
+ permissionMode = cliFlags.perm ?? (cliYolo ? "bypassPermissions" : settings.permissionMode);
638
+ const savedProviders = loadProviders();
639
+ plannerProvider = settings.plannerProviderId ? savedProviders.find(p => p.id === settings.plannerProviderId) : undefined;
640
+ workerProvider = settings.workerProviderId ? savedProviders.find(p => p.id === settings.workerProviderId) : undefined;
641
+ fastProvider = settings.fastProviderId ? savedProviders.find(p => p.id === settings.fastProviderId) : undefined;
642
+ // ④ Worktrees + merge (skip if --yolo, --worktrees, --no-worktrees, or --merge set)
733
643
  const gitRepo = isGitRepo(cwd);
734
644
  if (cliYolo || argv.includes("--no-worktrees")) {
735
645
  useWorktrees = false;
@@ -740,7 +650,7 @@ async function main() {
740
650
  mergeStrategy = cliFlags.merge || "yolo";
741
651
  }
742
652
  else if (gitRepo) {
743
- const wtChoice = await select(`${chalk.cyan("")} Git isolation:`, [
653
+ const wtChoice = await select(`${chalk.cyan("④b")} Git isolation:`, [
744
654
  { name: "Worktrees + yolo merge", value: "wt-yolo", hint: "isolate agents, merge into current branch" },
745
655
  { name: "Worktrees + new branch", value: "wt-branch", hint: "isolate agents, merge into a new branch" },
746
656
  { name: "No worktrees", value: "no-wt", hint: "all agents share the working directory" },
@@ -752,31 +662,20 @@ async function main() {
752
662
  useWorktrees = false;
753
663
  mergeStrategy = "yolo";
754
664
  }
755
- const parts = [];
756
- if (fastModel)
757
- parts.push(`${modelDisplayName(plannerModel)} → ${modelDisplayName(workerModel)} + ${modelDisplayName(fastModel)}`);
758
- else if (workerModel !== plannerModel)
759
- parts.push(`${modelDisplayName(workerModel)} → ${modelDisplayName(plannerModel)}`);
760
- else
761
- parts.push(modelDisplayName(workerModel));
762
- parts.push(`budget ${budget}`, `${concurrency}×`);
665
+ const inner = formatSettingsSummary({ ...settings, permissionMode });
666
+ const parts2 = [`budget ${budget}`, `${concurrency}×`];
763
667
  if (budget > 2)
764
- parts.push("flex");
765
- if (usageCap != null)
766
- parts.push(`cap ${Math.round(usageCap * 100)}%`);
767
- parts.push(allowExtraUsage ? (extraUsageBudget ? `extra $${extraUsageBudget}` : "extra ∞") : "no extra");
768
- if (permissionMode !== "auto")
769
- parts.push(permissionMode === "bypassPermissions" ? "yolo" : "prompt");
668
+ parts2.push("flex");
770
669
  if (useWorktrees)
771
- parts.push(mergeStrategy === "branch" ? "wt→branch" : "wt→yolo");
670
+ parts2.push(mergeStrategy === "branch" ? "wt→branch" : "wt→yolo");
772
671
  else
773
- parts.push("no wt");
672
+ parts2.push("no wt");
774
673
  if (completedRuns.length > 0)
775
- parts.push(`${completedRuns.length} prior`);
776
- const inner = parts.join(chalk.dim(" · "));
777
- const innerLen = parts.join(" · ").length;
674
+ parts2.push(`${completedRuns.length} prior`);
675
+ const fullLine = inner + chalk.dim(" · ") + parts2.join(chalk.dim(" · "));
676
+ const innerLen = fullLine.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").length;
778
677
  console.log(chalk.dim(`\n ╭${"─".repeat(innerLen + 4)}╮`));
779
- console.log(chalk.dim(" │") + ` ${inner} ` + chalk.dim("│"));
678
+ console.log(chalk.dim(" │") + ` ${fullLine} ` + chalk.dim("│"));
780
679
  console.log(chalk.dim(` ╰${"─".repeat(innerLen + 4)}╯`));
781
680
  }
782
681
  else {
package/dist/models.js CHANGED
@@ -124,7 +124,7 @@ export const FALLBACK_MODEL = "claude-opus-4-7"; // used for planner + worker re
124
124
  * exact → substring match. Falls back to "unknown" entry.
125
125
  */
126
126
  export function getModelCapability(model) {
127
- const m = (model === "default" ? DEFAULT_MODEL : model).toLowerCase();
127
+ const m = model.toLowerCase();
128
128
  if (MODEL_CAPABILITIES[m])
129
129
  return MODEL_CAPABILITIES[m];
130
130
  for (const [key, cap] of Object.entries(MODEL_CAPABILITIES)) {
@@ -135,7 +135,7 @@ export function getModelCapability(model) {
135
135
  }
136
136
  /** Human-readable model name for display (e.g. in run labels). */
137
137
  export function modelDisplayName(model) {
138
- const resolved = model === "default" ? DEFAULT_MODEL : model;
138
+ const resolved = model;
139
139
  const m = resolved.toLowerCase();
140
140
  if (MODEL_CAPABILITIES[m]?.displayName)
141
141
  return MODEL_CAPABILITIES[m].displayName;
package/dist/providers.js CHANGED
@@ -196,8 +196,8 @@ export async function pickModel(label, anthropicModels, currentModelId) {
196
196
  if (anthropicModels.length === 0) {
197
197
  items.push({
198
198
  name: DEFAULT_MODEL,
199
- value: { kind: "anthropic", model: { value: DEFAULT_MODEL, displayName: DEFAULT_MODEL, description: "default (model list unavailable)" } },
200
- hint: "default -- Anthropic model list unavailable",
199
+ value: { kind: "anthropic", model: { value: DEFAULT_MODEL, displayName: DEFAULT_MODEL, description: DEFAULT_MODEL + " (model list unavailable)" } },
200
+ hint: DEFAULT_MODEL + " -- Anthropic model list unavailable",
201
201
  });
202
202
  }
203
203
  for (const p of saved) {
package/dist/render.js CHANGED
@@ -451,7 +451,7 @@ export function renderFrame(swarm, showHotkeys, runInfo, selectedAgentId, maxRow
451
451
  const detailChip = swarm.active > 0 ? chalk.dim(" [d] detail") : "";
452
452
  const selectChip = swarm.active > 0 && running.length <= 10 ? chalk.dim(" [0-9] select") : "";
453
453
  const panelChip = panel?.visible ? chalk.green(` [Ctrl-O] ${panel.state.expanded ? "collapse" : "expand"}`) : "";
454
- hotkeyRow = chalk.dim(` [b] budget [t] cap [c] conc [e] extra ${pauseLabel} [s] steer [?] ask [q] stop`) + fixChip + retryChip + chip + detailChip + selectChip + panelChip;
454
+ hotkeyRow = chalk.dim(` [s] settings ${pauseLabel} [i] inject [?] ask [q] stop`) + fixChip + retryChip + chip + detailChip + selectChip + panelChip;
455
455
  if (swarm.blocked > 0 && swarm.blocked === swarm.active) {
456
456
  extraFooterRows.push(chalk.yellow(` all workers rate-limited -- [r] retry-now, [c] reduce concurrency, [p] pause, [q] quit`));
457
457
  }
@@ -641,7 +641,7 @@ export function renderSteeringFrame(runInfo, data, showHotkeys, rlGetter, maxRow
641
641
  const pending = runInfo?.pendingSteer ?? 0;
642
642
  const chip = pending > 0 ? chalk.cyan(` \u270E ${pending} steer queued`) : "";
643
643
  const panelChip = panel?.visible ? chalk.green(` [Ctrl-O] ${panel.state.expanded ? "collapse" : "expand"}`) : "";
644
- hotkeyRow = chalk.dim(" [b] budget [s] steer [q] stop") + chip + panelChip;
644
+ hotkeyRow = chalk.dim(" [s] settings [i] inject [q] stop") + chip + panelChip;
645
645
  }
646
646
  return renderUnifiedFrame({
647
647
  model: runInfo.model,
package/dist/run.js CHANGED
@@ -19,7 +19,8 @@ export async function executeRun(cfg) {
19
19
  process.stdout.write("\x1B[?25h\n");
20
20
  }
21
21
  catch { } };
22
- const { objective, cwd, workerModel, plannerModel, fastModel, concurrency, permissionMode, allowedTools, beforeWave: beforeWaveCmds, afterWave: afterWaveCmds, afterRun: afterRunCmds, runDir, previousKnowledge, } = cfg;
22
+ const { objective, cwd, allowedTools, beforeWave: beforeWaveCmds, afterWave: afterWaveCmds, afterRun: afterRunCmds, runDir, previousKnowledge } = cfg;
23
+ let { workerModel, plannerModel, fastModel, concurrency, permissionMode } = cfg;
23
24
  const envForModel = buildEnvResolver({
24
25
  plannerModel, plannerProvider: cfg.plannerProvider,
25
26
  workerModel, workerProvider: cfg.workerProvider,
@@ -44,6 +45,7 @@ export async function executeRun(cfg) {
44
45
  const liveConfig = {
45
46
  remaining: 0, usageCap, concurrency, paused: false, dirty: false,
46
47
  extraUsageBudget: cfg.extraUsageBudget,
48
+ workerModel, plannerModel, fastModel, permissionMode,
47
49
  };
48
50
  let waveNum;
49
51
  const waveHistory = [];
@@ -530,6 +532,15 @@ export async function executeRun(cfg) {
530
532
  remaining = liveConfig.remaining;
531
533
  usageCap = liveConfig.usageCap;
532
534
  cfg.extraUsageBudget = liveConfig.extraUsageBudget;
535
+ if (liveConfig.workerModel)
536
+ workerModel = liveConfig.workerModel;
537
+ if (liveConfig.plannerModel)
538
+ plannerModel = liveConfig.plannerModel;
539
+ if (liveConfig.fastModel !== undefined)
540
+ fastModel = liveConfig.fastModel;
541
+ if (liveConfig.permissionMode)
542
+ permissionMode = liveConfig.permissionMode;
543
+ concurrency = liveConfig.concurrency;
533
544
  liveConfig.dirty = false;
534
545
  }
535
546
  liveConfig.remaining = remaining;
@@ -0,0 +1,21 @@
1
+ import type { MutableRunSettings, PermMode } from "./types.js";
2
+ interface EditSettingsOptions {
3
+ /** Existing settings to show as current values (resume) or blank defaults. */
4
+ current: MutableRunSettings;
5
+ /** CLI flags that already override concurrency (skip prompt if set). */
6
+ cliConcurrencySet?: boolean;
7
+ /** Coach-recommended defaults (initial setup only). */
8
+ defaults?: {
9
+ plannerModel?: string;
10
+ workerModel?: string;
11
+ fastModel?: string;
12
+ concurrency?: number;
13
+ usageCap?: number | null;
14
+ permissionMode?: PermMode;
15
+ };
16
+ }
17
+ /** Interactively edit all mutable run settings. Mutates `options.current` in place. */
18
+ export declare function editRunSettings(options: EditSettingsOptions): Promise<MutableRunSettings>;
19
+ /** Format a MutableRunSettings as a compact summary line for the terminal. */
20
+ export declare function formatSettingsSummary(s: MutableRunSettings): string;
21
+ export {};
@@ -0,0 +1,120 @@
1
+ import chalk from "chalk";
2
+ import { modelDisplayName, formatContextWindow } from "./models.js";
3
+ import { fetchModels, ask, select, BRAILLE } from "./cli.js";
4
+ import { pickModel } from "./providers.js";
5
+ /** Interactively edit all mutable run settings. Mutates `options.current` in place. */
6
+ export async function editRunSettings(options) {
7
+ const s = options.current;
8
+ const modelsPromise = fetchModels(20_000).catch(() => []);
9
+ let modelFrame = 0;
10
+ const modelSpinner = setInterval(() => {
11
+ process.stdout.write(`\x1B[2K\r ${chalk.cyan(BRAILLE[modelFrame++ % BRAILLE.length])} ${chalk.dim("loading models...")}`);
12
+ }, 120);
13
+ let models;
14
+ try {
15
+ models = await modelsPromise;
16
+ }
17
+ finally {
18
+ clearInterval(modelSpinner);
19
+ process.stdout.write(`\x1B[2K\r`);
20
+ }
21
+ const plannerPick = await pickModel(`${chalk.cyan("①")} Planner model ${chalk.dim("(thinking, steering -- use your strongest)")}:`, models, options.defaults?.plannerModel ?? s.plannerModel);
22
+ s.plannerModel = plannerPick.model;
23
+ s.plannerProviderId = plannerPick.providerId;
24
+ const workerPick = await pickModel(`${chalk.cyan("②")} Worker model ${chalk.dim("(what runs the tasks -- Qwen 3.6 Plus / OpenRouter / etc via Other…)")}:`, models, options.defaults?.workerModel ?? s.workerModel);
25
+ s.workerModel = workerPick.model;
26
+ s.workerProviderId = workerPick.providerId;
27
+ const suggestFast = !!(options.defaults?.fastModel);
28
+ const fastChoice = await select(`${chalk.cyan("③")} Fast model ${chalk.dim("(optional -- Haiku/Qwen for quick tasks, checked by worker)")}:`, [
29
+ { name: "Skip", value: "skip", hint: "two-tier mode only (current setup)" },
30
+ { name: "Pick a fast model", value: "pick", hint: "Haiku, Qwen, or any provider -- for well-scoped tasks" },
31
+ ], suggestFast ? 1 : 0);
32
+ if (fastChoice === "pick") {
33
+ const fastPick = await pickModel(`${chalk.cyan("③b")} Fast model:`, models, options.defaults?.fastModel ?? s.fastModel);
34
+ s.fastModel = fastPick.model;
35
+ s.fastProviderId = fastPick.providerId;
36
+ }
37
+ else {
38
+ s.fastModel = undefined;
39
+ s.fastProviderId = undefined;
40
+ }
41
+ if (!options.cliConcurrencySet) {
42
+ const defaultC = options.defaults?.concurrency ?? s.concurrency;
43
+ const concAns = await ask(`\n ${chalk.cyan("④")} ${chalk.dim("Max concurrency")} ${chalk.dim("[")}${chalk.white(String(defaultC))}${chalk.dim("]:")} `);
44
+ const parsed = parseInt(concAns);
45
+ if (!isNaN(parsed) && parsed >= 1)
46
+ s.concurrency = parsed;
47
+ }
48
+ const coachCap = options.defaults?.usageCap;
49
+ const usageCapItems = [
50
+ { name: "Unlimited", value: undefined, hint: "full capacity, wait through rate limits" },
51
+ { name: "90%", value: 0.9, hint: "leave 10% for other work" },
52
+ { name: "75%", value: 0.75, hint: "conservative, plenty of headroom" },
53
+ { name: "50%", value: 0.5, hint: "use half, keep the rest" },
54
+ ];
55
+ const usageCapDefault = coachCap == null ? 0
56
+ : coachCap >= 0.85 ? 1
57
+ : coachCap >= 0.6 ? 2
58
+ : 3;
59
+ s.usageCap = await select(`${chalk.cyan("⑤")} Usage cap:`, usageCapItems, usageCapDefault);
60
+ const extraChoice = await select(`${chalk.cyan("⑥")} Allow extra usage ${chalk.dim("(billed separately)")}:`, [
61
+ { name: "No", value: "no", hint: "stop when plan limits are reached" },
62
+ { name: "Yes, with $ limit", value: "budget", hint: "set a spending cap" },
63
+ { name: "Yes, unlimited", value: "unlimited", hint: "keep going no matter what" },
64
+ ]);
65
+ if (extraChoice === "budget") {
66
+ const bAns = await ask(` ${chalk.dim("Max extra usage $:")} `);
67
+ const bVal = parseFloat(bAns);
68
+ s.allowExtraUsage = true;
69
+ s.extraUsageBudget = (!isNaN(bVal) && bVal > 0) ? bVal : 5;
70
+ }
71
+ else if (extraChoice === "unlimited") {
72
+ s.allowExtraUsage = true;
73
+ s.extraUsageBudget = undefined;
74
+ }
75
+ else {
76
+ s.allowExtraUsage = false;
77
+ s.extraUsageBudget = undefined;
78
+ }
79
+ const permItems = [
80
+ { name: "Auto", value: "auto", hint: "accept low-risk, reject high-risk" },
81
+ { name: "Bypass all", value: "bypassPermissions", hint: "agents can run anything (yolo)" },
82
+ { name: "Prompt each", value: "default", hint: "ask for every dangerous op" },
83
+ ];
84
+ const permDefault = options.defaults?.permissionMode === "bypassPermissions" ? 1
85
+ : options.defaults?.permissionMode === "default" ? 2 : 0;
86
+ s.permissionMode = await select(`${chalk.cyan("⑦")} Permissions:`, permItems, permDefault);
87
+ const modelLine = (label, m) => m ? `${chalk.dim(label.padEnd(11))}${chalk.white(m)} ${chalk.dim(`(${formatContextWindow(m)} context)`)}` : null;
88
+ const lines = [
89
+ modelLine("planner", s.plannerModel),
90
+ modelLine("worker", s.workerModel),
91
+ modelLine("fast", s.fastModel),
92
+ ].filter(Boolean);
93
+ console.log();
94
+ for (const l of lines)
95
+ console.log(l);
96
+ const capStr = s.usageCap != null ? `${Math.round(s.usageCap * 100)}%` : "unlimited";
97
+ const extraStr = s.allowExtraUsage ? (s.extraUsageBudget ? `$${s.extraUsageBudget}` : "unlimited") : "off";
98
+ console.log(` ${chalk.dim("concur ")}${chalk.white(String(s.concurrency))}`);
99
+ console.log(` ${chalk.dim("usage cap ")}${chalk.white(capStr)}`);
100
+ console.log(` ${chalk.dim("extra ")}${chalk.white(extraStr)}`);
101
+ console.log(` ${chalk.dim("perms ")}${chalk.white(s.permissionMode === "bypassPermissions" ? "yolo" : s.permissionMode)}`);
102
+ console.log();
103
+ return s;
104
+ }
105
+ /** Format a MutableRunSettings as a compact summary line for the terminal. */
106
+ export function formatSettingsSummary(s) {
107
+ const parts = [];
108
+ if (s.fastModel)
109
+ parts.push(`${modelDisplayName(s.plannerModel)} → ${modelDisplayName(s.workerModel)} + ${modelDisplayName(s.fastModel)}`);
110
+ else if (s.workerModel !== s.plannerModel)
111
+ parts.push(`${modelDisplayName(s.workerModel)} → ${modelDisplayName(s.plannerModel)}`);
112
+ else
113
+ parts.push(modelDisplayName(s.workerModel));
114
+ if (s.usageCap != null)
115
+ parts.push(`cap ${Math.round(s.usageCap * 100)}%`);
116
+ parts.push(s.allowExtraUsage ? (s.extraUsageBudget ? `extra $${s.extraUsageBudget}` : "extra ∞") : "no extra");
117
+ if (s.permissionMode !== "auto")
118
+ parts.push(s.permissionMode === "bypassPermissions" ? "yolo" : "prompt");
119
+ return parts.join(chalk.dim(" · "));
120
+ }
package/dist/steering.js CHANGED
@@ -136,10 +136,19 @@ If done: {"done": true, "reasoning": "...", "statusUpdate": "...", "estimatedSes
136
136
  const statusUpdate = parsed.statusUpdate || undefined;
137
137
  const estRaw = parsed.estimatedSessionsRemaining;
138
138
  const estimatedSessionsRemaining = typeof estRaw === "number" && estRaw >= 0 ? Math.round(estRaw) : undefined;
139
+ // Resolve steering role strings ("worker"/"fast"/"planner") to actual model IDs.
140
+ const resolveModel = (role) => {
141
+ switch (role.toLowerCase()) {
142
+ case "worker": return workerModel;
143
+ case "planner": return plannerModel;
144
+ case "fast": return fastModel ?? workerModel;
145
+ default: return role; // already a real model ID
146
+ }
147
+ };
139
148
  let tasks = (parsed.tasks || []).map((t, i) => ({
140
149
  id: String(i),
141
150
  prompt: typeof t === "string" ? t : t.prompt,
142
- ...(t.model && { model: t.model }),
151
+ ...(t.model && { model: resolveModel(t.model) }),
143
152
  ...(t.noWorktree && { noWorktree: true }),
144
153
  ...(t.type && { type: t.type }),
145
154
  }));
package/dist/swarm.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { Task, AgentState, SwarmPhase, PermMode, MergeStrategy, RateLimitWindow } from "./types.js";
1
+ import { type PermMode } from "./types.js";
2
+ import type { Task, AgentState, SwarmPhase, MergeStrategy, RateLimitWindow } from "./types.js";
2
3
  import type { MergeResult } from "./merge.js";
3
4
  export interface SwarmConfig {
4
5
  tasks: Task[];
@@ -77,12 +78,14 @@ export declare class Swarm {
77
78
  private pendingTools;
78
79
  private ctxWarned;
79
80
  logFile?: string;
80
- readonly model: string | undefined;
81
+ model: string | undefined;
81
82
  usageCap: number | undefined;
82
83
  readonly allowExtraUsage: boolean;
83
84
  extraUsageBudget: number | undefined;
84
85
  readonly baseCostUsd: number;
85
86
  mergeBranch?: string;
87
+ /** Permission mode read from config on each agent dispatch. Writable for mid-run changes. */
88
+ private _permMode;
86
89
  constructor(config: SwarmConfig);
87
90
  get active(): number;
88
91
  get blocked(): number;
@@ -100,6 +103,10 @@ export declare class Swarm {
100
103
  retryRateLimitNow(): void;
101
104
  /** Live-adjust the overage spend cap. `undefined` = unlimited. If already over the new cap, stop dispatch. */
102
105
  setExtraUsageBudget(n: number | undefined): void;
106
+ /** Live-adjust the worker model. Picked up by next agent dispatch. */
107
+ setModel(m: string): void;
108
+ /** Live-adjust the SDK permission mode. Picked up by next agent dispatch. */
109
+ setPermissionMode(m: PermMode): void;
103
110
  run(): Promise<void>;
104
111
  abort(): void;
105
112
  /** Re-queue all errored agents' tasks for retry within this wave. */
package/dist/swarm.js CHANGED
@@ -93,6 +93,8 @@ export class Swarm {
93
93
  extraUsageBudget;
94
94
  baseCostUsd;
95
95
  mergeBranch;
96
+ /** Permission mode read from config on each agent dispatch. Writable for mid-run changes. */
97
+ _permMode;
96
98
  constructor(config) {
97
99
  if (!config.tasks.length)
98
100
  throw new Error("SwarmConfig: tasks array must not be empty");
@@ -115,6 +117,7 @@ export class Swarm {
115
117
  this.queue = [...config.tasks];
116
118
  this.total = config.tasks.length;
117
119
  this.targetConcurrency = config.concurrency;
120
+ this._permMode = config.permissionMode;
118
121
  }
119
122
  get active() { return this.agents.filter(a => a.status === "running").length; }
120
123
  get blocked() { return this.agents.filter(a => a.status === "running" && a.blockedAt != null).length; }
@@ -210,6 +213,23 @@ export class Swarm {
210
213
  this.capForOverage(`Extra usage budget $${n} exceeded ($${this.overageCostUsd.toFixed(2)} spent) -- stopping dispatch`);
211
214
  }
212
215
  }
216
+ /** Live-adjust the worker model. Picked up by next agent dispatch. */
217
+ setModel(m) {
218
+ if (this.model === m)
219
+ return;
220
+ const prev = this.model;
221
+ this.model = m;
222
+ this.log(-1, `Worker model: ${prev} → ${m}`);
223
+ }
224
+ /** Live-adjust the SDK permission mode. Picked up by next agent dispatch. */
225
+ setPermissionMode(m) {
226
+ if (this._permMode === m)
227
+ return;
228
+ const prev = this._permMode ?? "auto";
229
+ this._permMode = m;
230
+ const label = m === "bypassPermissions" ? "yolo" : m;
231
+ this.log(-1, `Permission mode: ${prev === "bypassPermissions" ? "yolo" : prev} → ${label}`);
232
+ }
213
233
  async run() {
214
234
  try {
215
235
  if (this.config.useWorktrees) {
@@ -528,7 +548,7 @@ export class Swarm {
528
548
  agent.finishedAt = undefined;
529
549
  }
530
550
  try {
531
- const perm = this.config.permissionMode ?? "auto";
551
+ const perm = this._permMode ?? "auto";
532
552
  let resumeSessionId = task.resumeSessionId;
533
553
  let resumePrompt = "Continue. Complete the task.";
534
554
  const runOnce = async (isResume) => {
package/dist/types.d.ts CHANGED
@@ -203,6 +203,23 @@ export interface RunMemory {
203
203
  /** Pending user directives from the steer inbox, consumed by the next successful steering call. */
204
204
  userGuidance?: string;
205
205
  }
206
+ /** Mutable subset of RunConfigBase — settings that can be changed at any point, including mid-run. */
207
+ export interface MutableRunSettings {
208
+ workerModel: string;
209
+ plannerModel: string;
210
+ fastModel?: string;
211
+ workerProviderId?: string;
212
+ plannerProviderId?: string;
213
+ fastProviderId?: string;
214
+ concurrency: number;
215
+ usageCap?: number;
216
+ allowExtraUsage: boolean;
217
+ extraUsageBudget?: number;
218
+ permissionMode: PermMode;
219
+ beforeWave?: string | string[];
220
+ afterWave?: string | string[];
221
+ afterRun?: string | string[];
222
+ }
206
223
  /** Shared configuration for a run -- both live (RunConfig) and persisted (RunState). */
207
224
  export interface RunConfigBase {
208
225
  /** Total session budget. */
package/dist/ui.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Swarm } from "./swarm.js";
2
- import type { RLGetter, WaveSummary } from "./types.js";
2
+ import type { RLGetter, WaveSummary, PermMode } from "./types.js";
3
3
  import { InteractivePanel } from "./interactive-panel.js";
4
4
  /** Short-lived context the steering view renders around its live log. */
5
5
  export interface SteeringContext {
@@ -34,8 +34,16 @@ export interface LiveConfig {
34
34
  concurrency: number;
35
35
  paused: boolean;
36
36
  dirty: boolean;
37
- /** Overage spend cap ($) -- undefined = unlimited. Synced from the [e] hotkey. */
37
+ /** Overage spend cap ($) -- undefined = unlimited. Synced from the [s] hotkey. */
38
38
  extraUsageBudget?: number;
39
+ /** Worker model for agent tasks. Changed mid-run, picked up by next wave/agent dispatch. */
40
+ workerModel?: string;
41
+ /** Planner/steering model. Changed mid-run, picked up by next steer/planner call. */
42
+ plannerModel?: string;
43
+ /** Fast model for quick verification tasks. */
44
+ fastModel?: string;
45
+ /** SDK permission mode. Changed mid-run, picked up by next agent dispatch. */
46
+ permissionMode?: PermMode;
39
47
  }
40
48
  /** State of an in-flight or recently-completed ask side query. */
41
49
  export interface AskState {
@@ -57,6 +65,8 @@ export declare class RunDisplay {
57
65
  private interval?;
58
66
  private keyHandler?;
59
67
  private inputMode;
68
+ /** Which field the settings editor is currently editing. Order: budget, cap, conc, extra, worker, planner, fast, perms, pause. */
69
+ private settingsField;
60
70
  private inputSegs;
61
71
  private started;
62
72
  private readonly isTTY;
@@ -120,6 +130,8 @@ export declare class RunDisplay {
120
130
  * The content area shrinks so input prompts are never clipped. */
121
131
  private flush;
122
132
  private render;
133
+ /** Read the current value for a settings field from liveConfig/swarm. */
134
+ private currentFieldValue;
123
135
  private renderInputPrompt;
124
136
  private hasHotkeys;
125
137
  private setupHotkeys;
@@ -127,7 +139,7 @@ export declare class RunDisplay {
127
139
  private handlePaste;
128
140
  /** Keyboard handler used only while the panel is expanded fullscreen.
129
141
  * Handles scroll + close. Swallows everything else so the normal hotkeys
130
- * (b/t/c/p/s/?/d/0-9) do not fire while the user is reading. */
142
+ * (s/p/i/?/d/0-9) do not fire while the user is reading. */
131
143
  private handlePanelKey;
132
144
  /** Handle a typed (non-pasted) chunk. Returns true if the frame needs a redraw.
133
145
  *
@@ -141,7 +153,7 @@ export declare class RunDisplay {
141
153
  * 3. ESC alone → cancel input / close detail / dismiss panel
142
154
  * 4. numeric input → digits, Enter, Backspace
143
155
  * 5. text input → printable chars, Enter, Backspace, ESC (with lookahead)
144
- * 6. hotkey mode → b, t, c, e, p, s, q, ?, d, 0-9
156
+ * 6. hotkey mode → s (settings), i (inject), q, ?, d, 0-9, f, r, p
145
157
  */
146
158
  private handleTyped;
147
159
  private plainTick;
package/dist/ui.js CHANGED
@@ -10,6 +10,8 @@ import { allTurns, cycleFocused } from "./turns.js";
10
10
  const MAX_STEERING_EVENTS = 60;
11
11
  const MAX_INPUT_LEN = 600;
12
12
  const MAX_ASK_LINES = 40;
13
+ const SETTINGS_FIELDS = ["budget", "cap", "conc", "extra", "worker", "planner", "fast", "perms", "pause"];
14
+ const NUMERIC_SETTINGS_FIELDS = new Set(["budget", "cap", "conc", "extra"]);
13
15
  /** Visible lines for the ask panel, clamped to leave room for header/content/footer/input. */
14
16
  function askDisplayCap() {
15
17
  return Math.max(3, Math.min(MAX_ASK_LINES, (process.stdout.rows || 40) - 20));
@@ -28,6 +30,8 @@ export class RunDisplay {
28
30
  interval;
29
31
  keyHandler;
30
32
  inputMode = "none";
33
+ /** Which field the settings editor is currently editing. Order: budget, cap, conc, extra, worker, planner, fast, perms, pause. */
34
+ settingsField = 0;
31
35
  inputSegs = [];
32
36
  started = false;
33
37
  isTTY;
@@ -442,24 +446,54 @@ export class RunDisplay {
442
446
  }
443
447
  return frame + bottom;
444
448
  }
449
+ /** Read the current value for a settings field from liveConfig/swarm. */
450
+ currentFieldValue(field) {
451
+ const lc = this.liveConfig;
452
+ const s = this.swarm;
453
+ switch (field) {
454
+ case "budget": return String(lc?.remaining ?? "—");
455
+ case "cap": return lc?.usageCap != null ? `${Math.round(lc.usageCap * 100)}%` : "unlimited";
456
+ case "conc": return String(lc?.concurrency ?? "—");
457
+ case "extra": return lc?.extraUsageBudget != null ? `$${lc.extraUsageBudget}` : "unlimited";
458
+ case "worker": return lc?.workerModel ?? s?.model ?? "—";
459
+ case "planner": return lc?.plannerModel ?? "—";
460
+ case "fast": return lc?.fastModel ?? "(none)";
461
+ case "perms": {
462
+ const p = lc?.permissionMode ?? "auto";
463
+ return p === "bypassPermissions" ? "yolo" : p;
464
+ }
465
+ case "pause": return s?.paused ? "paused" : "running";
466
+ default: return "";
467
+ }
468
+ }
445
469
  renderInputPrompt() {
446
470
  if (this.inputMode === "none")
447
471
  return "";
448
472
  const rendered = renderSegments(this.inputSegs);
449
- if (this.inputMode === "budget") {
450
- return `\n ${chalk.cyan(">")} New budget (remaining sessions): ${rendered}\u2588`;
451
- }
452
- if (this.inputMode === "threshold") {
453
- return `\n ${chalk.cyan(">")} New usage cap (0-100%): ${rendered}\u2588`;
454
- }
455
- if (this.inputMode === "concurrency") {
456
- return `\n ${chalk.cyan(">")} New concurrency (min 1): ${rendered}\u2588`;
457
- }
458
- if (this.inputMode === "extra") {
459
- return `\n ${chalk.cyan(">")} Extra usage $ cap (0 = stop on overage): ${rendered}\u2588`;
473
+ if (this.inputMode === "settings") {
474
+ const labels = [
475
+ "New budget (remaining sessions)",
476
+ "New usage cap (0-100%, 0=unlimited)",
477
+ "New concurrency (min 1)",
478
+ "Extra usage $ cap (0=stop on overage)",
479
+ "Worker model (for agent tasks)",
480
+ "Planner model (steering/thinking)",
481
+ "Fast model (optional, empty=skip)",
482
+ "Permission mode (auto/yolo/prompt)",
483
+ "Pause/resume workers",
484
+ ];
485
+ const total = SETTINGS_FIELDS.length;
486
+ const field = SETTINGS_FIELDS[this.settingsField % total];
487
+ const label = labels[this.settingsField % total];
488
+ const idx = this.settingsField + 1;
489
+ const currentValue = this.currentFieldValue(field);
490
+ const hint = field === "pause"
491
+ ? chalk.dim(` (Enter to toggle, Tab to skip, Esc to exit)`)
492
+ : chalk.dim(` [${idx}/${total}] Tab=next Esc=exit current: ${chalk.white(currentValue)}`);
493
+ return `\n ${chalk.cyan("◆")} ${chalk.bold(label)}${hint}\n ${rendered}\u2588`;
460
494
  }
461
495
  if (this.inputMode === "steer") {
462
- return `\n ${chalk.cyan(">")} ${chalk.bold("Steer next wave")} ${chalk.dim("(Enter to queue, Esc to cancel)")}\n ${rendered}\u2588`;
496
+ return `\n ${chalk.cyan(">")} ${chalk.bold("Inject next wave")} ${chalk.dim("(Enter to queue, Esc to cancel)")}\n ${rendered}\u2588`;
463
497
  }
464
498
  if (this.inputMode === "ask") {
465
499
  return `\n ${chalk.cyan(">")} ${chalk.bold("Ask the planner")} ${chalk.dim("(Enter to send, Esc to cancel)")}\n ${rendered}\u2588`;
@@ -503,11 +537,18 @@ export class RunDisplay {
503
537
  }
504
538
  /** Handle a pasted block. Returns true if the frame needs a redraw. */
505
539
  handlePaste(text) {
506
- if (this.inputMode === "budget" || this.inputMode === "threshold" || this.inputMode === "concurrency" || this.inputMode === "extra") {
507
- const clean = text.replace(/[^0-9.]/g, "");
508
- if (clean)
509
- appendCharToSegments(this.inputSegs, clean);
510
- return !!clean;
540
+ if (this.inputMode === "settings") {
541
+ const field = SETTINGS_FIELDS[this.settingsField % SETTINGS_FIELDS.length];
542
+ if (NUMERIC_SETTINGS_FIELDS.has(field)) {
543
+ const clean = text.replace(/[^0-9.]/g, "");
544
+ if (clean)
545
+ appendCharToSegments(this.inputSegs, clean);
546
+ return !!clean;
547
+ }
548
+ if (field !== "pause" && text.length + segmentsToString(this.inputSegs).length <= MAX_INPUT_LEN) {
549
+ appendPasteToSegments(this.inputSegs, text);
550
+ return true;
551
+ }
511
552
  }
512
553
  if (this.inputMode === "steer" || this.inputMode === "ask") {
513
554
  if (segmentsToString(this.inputSegs).length + text.length > MAX_INPUT_LEN)
@@ -519,7 +560,7 @@ export class RunDisplay {
519
560
  }
520
561
  /** Keyboard handler used only while the panel is expanded fullscreen.
521
562
  * Handles scroll + close. Swallows everything else so the normal hotkeys
522
- * (b/t/c/p/s/?/d/0-9) do not fire while the user is reading. */
563
+ * (s/p/i/?/d/0-9) do not fire while the user is reading. */
523
564
  handlePanelKey(s) {
524
565
  const bodyRows = Math.max(3, (process.stdout.rows || 40) - 7);
525
566
  // CSI sequences: arrows, PgUp/PgDn, Home/End
@@ -606,14 +647,14 @@ export class RunDisplay {
606
647
  * 3. ESC alone → cancel input / close detail / dismiss panel
607
648
  * 4. numeric input → digits, Enter, Backspace
608
649
  * 5. text input → printable chars, Enter, Backspace, ESC (with lookahead)
609
- * 6. hotkey mode → b, t, c, e, p, s, q, ?, d, 0-9
650
+ * 6. hotkey mode → s (settings), i (inject), q, ?, d, 0-9, f, r, p
610
651
  */
611
652
  handleTyped(s) {
612
653
  const lc = this.liveConfig;
613
654
  // ── 0. Fullscreen panel owns the keyboard ──
614
655
  // While the interactive panel is expanded it takes over the screen and
615
656
  // steals every key except Esc, Ctrl-O (close), Ctrl-C (abort), and scroll.
616
- // Hotkeys like b/t/c/p/s/? are intentionally swallowed so the user can
657
+ // Hotkeys like s/i/p are intentionally swallowed so the user can
617
658
  // read without triggering side effects.
618
659
  if (this.panel.state.expanded) {
619
660
  return this.handlePanelKey(s);
@@ -662,37 +703,93 @@ export class RunDisplay {
662
703
  }
663
704
  return false;
664
705
  }
665
- // ── 4. Input mode: budget / threshold / concurrency / extra ──
666
- if (this.inputMode === "budget" || this.inputMode === "threshold" || this.inputMode === "concurrency" || this.inputMode === "extra") {
706
+ if (s === "\t" && this.inputMode === "settings") {
707
+ const field = SETTINGS_FIELDS[this.settingsField % SETTINGS_FIELDS.length];
708
+ if (field === "pause" && this.swarm) {
709
+ const next = !this.swarm.paused;
710
+ this.swarm.setPaused(next);
711
+ this.liveConfig.paused = next;
712
+ this.liveConfig.dirty = true;
713
+ }
714
+ this.settingsField++;
715
+ this.inputSegs = [];
716
+ if (this.settingsField >= SETTINGS_FIELDS.length)
717
+ this.inputMode = "none";
718
+ return true;
719
+ }
720
+ // ── 4. Settings mode: all mutable fields ──
721
+ if (this.inputMode === "settings") {
667
722
  let dirty = false;
668
723
  for (const ch of s) {
669
724
  if (ch === "\r" || ch === "\n") {
670
- const val = parseFloat(segmentsToString(this.inputSegs));
671
- if (this.inputMode === "budget" && !isNaN(val) && val > 0) {
672
- lc.remaining = Math.round(val);
725
+ const field = SETTINGS_FIELDS[this.settingsField % SETTINGS_FIELDS.length];
726
+ const raw = segmentsToString(this.inputSegs).trim();
727
+ if (field === "budget") {
728
+ const val = parseFloat(raw);
729
+ if (!isNaN(val) && val > 0) {
730
+ lc.remaining = Math.round(val);
731
+ lc.dirty = true;
732
+ this.swarm?.log(-1, `Budget changed to ${lc.remaining} remaining`);
733
+ }
734
+ }
735
+ else if (field === "cap") {
736
+ const val = parseFloat(raw);
737
+ if (!isNaN(val) && val >= 0 && val <= 100) {
738
+ const frac = val / 100;
739
+ lc.usageCap = frac > 0 ? frac : undefined;
740
+ lc.dirty = true;
741
+ if (this.swarm)
742
+ this.swarm.usageCap = lc.usageCap;
743
+ this.swarm?.log(-1, `Usage cap changed to ${val > 0 ? val + "%" : "unlimited"}`);
744
+ }
745
+ }
746
+ else if (field === "conc") {
747
+ const val = parseFloat(raw);
748
+ if (!isNaN(val) && val >= 1) {
749
+ const n = Math.round(val);
750
+ lc.concurrency = n;
751
+ lc.dirty = true;
752
+ this.swarm?.setConcurrency(n);
753
+ }
754
+ }
755
+ else if (field === "extra") {
756
+ const val = parseFloat(raw);
757
+ if (!isNaN(val) && val >= 0) {
758
+ lc.extraUsageBudget = val;
759
+ lc.dirty = true;
760
+ this.swarm?.setExtraUsageBudget(val);
761
+ }
762
+ }
763
+ else if (field === "worker" && raw) {
764
+ lc.workerModel = raw;
673
765
  lc.dirty = true;
674
- this.swarm?.log(-1, `Budget changed to ${lc.remaining} remaining`);
766
+ this.swarm?.setModel(raw);
675
767
  }
676
- else if (this.inputMode === "threshold" && !isNaN(val) && val >= 0 && val <= 100) {
677
- const frac = val / 100;
678
- lc.usageCap = frac > 0 ? frac : undefined;
768
+ else if (field === "planner" && raw) {
769
+ lc.plannerModel = raw;
679
770
  lc.dirty = true;
680
- if (this.swarm)
681
- this.swarm.usageCap = lc.usageCap;
682
- this.swarm?.log(-1, `Usage cap changed to ${val > 0 ? val + "%" : "unlimited"}`);
683
771
  }
684
- else if (this.inputMode === "concurrency" && !isNaN(val) && val >= 1) {
685
- const n = Math.round(val);
686
- lc.concurrency = n;
772
+ else if (field === "fast") {
773
+ lc.fastModel = raw || undefined;
687
774
  lc.dirty = true;
688
- this.swarm?.setConcurrency(n);
689
775
  }
690
- else if (this.inputMode === "extra" && !isNaN(val) && val >= 0) {
691
- lc.extraUsageBudget = val;
776
+ else if (field === "perms" && raw) {
777
+ const m = raw.toLowerCase();
778
+ const mode = m.startsWith("yolo") || m.startsWith("bypass") ? "bypassPermissions"
779
+ : m.startsWith("prompt") || m === "default" ? "default" : "auto";
780
+ lc.permissionMode = mode;
692
781
  lc.dirty = true;
693
- this.swarm?.setExtraUsageBudget(val);
782
+ this.swarm?.setPermissionMode(mode);
694
783
  }
695
- this.inputMode = "none";
784
+ else if (field === "pause" && this.swarm) {
785
+ const next = !this.swarm.paused;
786
+ this.swarm.setPaused(next);
787
+ lc.paused = next;
788
+ lc.dirty = true;
789
+ }
790
+ this.settingsField++;
791
+ if (this.settingsField >= SETTINGS_FIELDS.length)
792
+ this.inputMode = "none";
696
793
  this.inputSegs = [];
697
794
  return true;
698
795
  }
@@ -706,9 +803,19 @@ export class RunDisplay {
706
803
  dirty = true;
707
804
  continue;
708
805
  }
709
- if (/^[0-9.]$/.test(ch)) {
710
- appendCharToSegments(this.inputSegs, ch);
711
- dirty = true;
806
+ const field = SETTINGS_FIELDS[this.settingsField % SETTINGS_FIELDS.length];
807
+ if (NUMERIC_SETTINGS_FIELDS.has(field)) {
808
+ if (/^[0-9.]$/.test(ch)) {
809
+ appendCharToSegments(this.inputSegs, ch);
810
+ dirty = true;
811
+ }
812
+ }
813
+ else if (field !== "pause") {
814
+ const code = ch.charCodeAt(0);
815
+ if (code >= 0x20 && code <= 0x7E) {
816
+ appendCharToSegments(this.inputSegs, ch);
817
+ dirty = true;
818
+ }
712
819
  }
713
820
  }
714
821
  return dirty;
@@ -803,35 +910,14 @@ export class RunDisplay {
803
910
  const code = key.charCodeAt(0);
804
911
  if (code < 0x20 || code > 0x7E)
805
912
  return false;
806
- if (key === "b" || key === "B") {
807
- this.inputMode = "budget";
913
+ if (key === "s" || key === "S") {
914
+ if (!this.swarm)
915
+ return false;
916
+ this.inputMode = "settings";
917
+ this.settingsField = 0;
808
918
  this.inputSegs = [];
809
919
  return true;
810
920
  }
811
- if (key === "t" || key === "T") {
812
- if (this.swarm) {
813
- this.inputMode = "threshold";
814
- this.inputSegs = [];
815
- return true;
816
- }
817
- return false;
818
- }
819
- if (key === "c" || key === "C") {
820
- if (this.swarm) {
821
- this.inputMode = "concurrency";
822
- this.inputSegs = [];
823
- return true;
824
- }
825
- return false;
826
- }
827
- if (key === "e" || key === "E") {
828
- if (this.swarm) {
829
- this.inputMode = "extra";
830
- this.inputSegs = [];
831
- return true;
832
- }
833
- return false;
834
- }
835
921
  if (key === "p" || key === "P") {
836
922
  if (this.swarm) {
837
923
  const next = !this.swarm.paused;
@@ -850,7 +936,7 @@ export class RunDisplay {
850
936
  this.swarm.retryRateLimitNow();
851
937
  return true;
852
938
  }
853
- if ((key === "s" || key === "S") && this.onSteer) {
939
+ if ((key === "i" || key === "I") && this.onSteer) {
854
940
  this.inputMode = "steer";
855
941
  this.inputSegs = [];
856
942
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.25.30",
3
+ "version": "1.25.33",
4
4
  "description": "Parallel Claude agents in git worktrees with a usage cap that reserves headroom for your interactive Claude Code. Crash-safe resume. Provider-agnostic model catalog (Anthropic, Cursor, OpenAI, Gemini, DeepSeek, Llama, Qwen) with capability-based task scoping.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.25.30",
3
+ "version": "1.25.33",
4
4
  "description": "Claude Code skill for understanding, installing, and inspecting claude-overnight runs -- parallel Claude agents in git worktrees with thinking waves, multi-wave steering, and crash-safe resume. Supports Cursor API Proxy, Qwen, OpenRouter.",
5
5
  "author": {
6
6
  "name": "Francesco Fornace"