@wrongstack/cli 0.299.0 → 0.300.0

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 (49) hide show
  1. package/dist/{acp-S4LZQB66.js → acp-IDNS2YVE.js} +3 -3
  2. package/dist/{auth-G5YKKSRH.js → auth-6T6ZOT2R.js} +5 -5
  3. package/dist/auth-menu/loopback-server.d.ts +41 -55
  4. package/dist/auth-menu/openai-codex-oauth.d.ts +36 -123
  5. package/dist/auto-discover-providers-DRW4OZHG.js +8 -0
  6. package/dist/boot/dispatch-webui.d.ts +3 -0
  7. package/dist/{chunk-PEKUKUCH.js → chunk-3MWUZMOL.js} +19 -12
  8. package/dist/{chunk-TGDHN4LM.js → chunk-4GVH7V3S.js} +4 -24
  9. package/dist/{chunk-4U3DMA2Y.js → chunk-4WJKVQJY.js} +15 -20
  10. package/dist/{chunk-2SLSUDYS.js → chunk-GYDQABMA.js} +269 -180
  11. package/dist/{chunk-4HH3YGG4.js → chunk-MRLYDHQ7.js} +8 -5
  12. package/dist/{chunk-66T43Q4Y.js → chunk-N7ULWBO5.js} +4 -3
  13. package/dist/{chunk-ZNZJ34RF.js → chunk-PEMN4T5O.js} +2 -2
  14. package/dist/chunk-PYTFS4MC.js +238 -0
  15. package/dist/{chunk-L5CE52XW.js → chunk-V76R7DC5.js} +25 -19
  16. package/dist/{cli-main-3MQDFSYK.js → cli-main-7B3G2YSG.js} +528 -156
  17. package/dist/{execution-ANOYMWPO.js → execution-SHGM7BVC.js} +50 -5
  18. package/dist/fleet/budget-source.d.ts +18 -0
  19. package/dist/fleet/host-status.d.ts +31 -0
  20. package/dist/fleet/host-types.d.ts +8 -0
  21. package/dist/fleet/host.d.ts +5 -0
  22. package/dist/{hq-BDO56CX6.js → hq-DVDD4XPV.js} +22 -3
  23. package/dist/hq-server/auth-state.d.ts +29 -1
  24. package/dist/hq-server/auth.d.ts +16 -0
  25. package/dist/hq-server/client-address.d.ts +56 -0
  26. package/dist/hq-server/login-attempt-store.d.ts +30 -10
  27. package/dist/hq-server/routes/auth-handlers.d.ts +66 -26
  28. package/dist/hq-server/routes.d.ts +13 -0
  29. package/dist/hq-server/types.d.ts +10 -0
  30. package/dist/{hq-server-RJLJJZU7.js → hq-server-DQHLNHDL.js} +2 -2
  31. package/dist/hq-server.d.ts +19 -0
  32. package/dist/index.js +20 -20
  33. package/dist/{modeldiag-HENYJ4N7.js → modeldiag-YOQZHBQV.js} +2 -2
  34. package/dist/{plugin-usage-VTL7ZTVC.js → plugin-usage-EOG4ET4S.js} +2 -2
  35. package/dist/{plugins-ETY6W3DC.js → plugins-56CMWMPR.js} +3 -3
  36. package/dist/{providers-models-GDELBHZK.js → providers-models-K6RT7ABI.js} +34 -9
  37. package/dist/slash-commands/command-context.d.ts +19 -0
  38. package/dist/webui-server/setup-events.d.ts +17 -0
  39. package/dist/webui-server/terminal-log-view.d.ts +29 -12
  40. package/dist/{webui-server-PCLSXX6L.js → webui-server-G3EKFXLL.js} +109 -38
  41. package/dist/webui-server-options.d.ts +16 -0
  42. package/dist/wiring/brain-and-orchestration.d.ts +3 -0
  43. package/dist/wiring/director-setup.d.ts +4 -0
  44. package/dist/wiring/fallback-gate.d.ts +31 -0
  45. package/dist/wiring/fleet-command-handlers.d.ts +1 -1
  46. package/dist/wiring/plugins.d.ts +2 -8
  47. package/dist/wiring/provider-utility-tools.d.ts +26 -1
  48. package/package.json +23 -23
  49. package/dist/chunk-GO3TJICK.js +0 -489
@@ -60,6 +60,25 @@ export interface HqServerOptions {
60
60
  * `--hq-token-ttl-ms` CLI flag.
61
61
  */
62
62
  tokenTtlMs?: number;
63
+ /**
64
+ * How many trusted reverse proxies sit between a browser and this server.
65
+ * Controls which address the login backoff keys on (WS-106).
66
+ *
67
+ * Default `0`: `X-Forwarded-For` is ignored entirely and the socket peer is
68
+ * used — correct for a direct bind, and the only safe default, because the
69
+ * header is client-supplied and honouring it unasked would let an attacker
70
+ * mint a fresh rate-limit identity per request.
71
+ *
72
+ * Set it to the real hop count when HQ runs behind a tunnel or relay
73
+ * (`--hq-public-url`, `--hq-require-browser-auth`). Without it every request
74
+ * arrives from the tunnel's own address, so all users share one backoff
75
+ * bucket: one attacker locks out everyone, and a distributed attacker is not
76
+ * limited per-source at all. `1` covers the common single-tunnel case.
77
+ *
78
+ * Only the rightmost `n` forwarded entries are ever read — see
79
+ * `hq-server/client-address.ts`. Set via `--hq-trusted-proxy-hops`.
80
+ */
81
+ trustedProxyHops?: number;
63
82
  }
64
83
  export interface HqStartupConnectionInfo {
65
84
  dataDir: string;
package/dist/index.js CHANGED
@@ -13,20 +13,20 @@ import {
13
13
  import {
14
14
  patchConfig
15
15
  } from "./chunk-TYO2OVAD.js";
16
+ import {
17
+ DEFAULT_PORT
18
+ } from "./chunk-GYDQABMA.js";
19
+ import "./chunk-Q5GTM25S.js";
16
20
  import {
17
21
  helpCmd,
18
22
  versionCmd
19
23
  } from "./chunk-ARWGQUB3.js";
20
- import {
21
- discoverAndMergeProviders
22
- } from "./chunk-TGDHN4LM.js";
23
- import {
24
- DEFAULT_PORT
25
- } from "./chunk-2SLSUDYS.js";
26
- import "./chunk-Q5GTM25S.js";
27
24
  import {
28
25
  CLI_VERSION
29
26
  } from "./chunk-XJXDOF63.js";
27
+ import {
28
+ discoverAndMergeProviders
29
+ } from "./chunk-4GVH7V3S.js";
30
30
  import {
31
31
  runUpdateCommand
32
32
  } from "./chunk-I5MEUFDI.js";
@@ -36,11 +36,11 @@ import {
36
36
  parseArgs,
37
37
  runPicker,
38
38
  saveToGlobalConfig
39
- } from "./chunk-ZNZJ34RF.js";
39
+ } from "./chunk-PEMN4T5O.js";
40
40
  import {
41
41
  isKeylessLocalProvider,
42
42
  visibleModelIds
43
- } from "./chunk-66T43Q4Y.js";
43
+ } from "./chunk-N7ULWBO5.js";
44
44
  import {
45
45
  mutateConfigProviders,
46
46
  normalizeKeys,
@@ -1495,9 +1495,9 @@ function compactSingleLine(text) {
1495
1495
 
1496
1496
  // src/subcommands/index.ts
1497
1497
  var loaders = {
1498
- acp: async () => (await import("./acp-S4LZQB66.js")).acpCmd,
1498
+ acp: async () => (await import("./acp-IDNS2YVE.js")).acpCmd,
1499
1499
  init: async () => (await import("./init-E2NDDOHI.js")).initCmd,
1500
- auth: async () => (await import("./auth-G5YKKSRH.js")).authCmd,
1500
+ auth: async () => (await import("./auth-6T6ZOT2R.js")).authCmd,
1501
1501
  update: async () => (await import("./update-MV6TUACD.js")).updateCmd,
1502
1502
  sessions: async () => (await import("./sessions-config-NLCMK3BY.js")).sessionsCmd,
1503
1503
  config: async () => (await import("./sessions-config-NLCMK3BY.js")).configCmd,
@@ -1506,23 +1506,23 @@ var loaders = {
1506
1506
  audit: async () => (await import("./audit-2ZSFGNIF.js")).auditCmd,
1507
1507
  tools: async () => (await import("./tools-skills-UNKLOB7M.js")).toolsCmd,
1508
1508
  skills: async () => (await import("./tools-skills-UNKLOB7M.js")).skillsCmd,
1509
- providers: async () => (await import("./providers-models-GDELBHZK.js")).providersCmd,
1510
- models: async () => (await import("./providers-models-GDELBHZK.js")).modelsCmd,
1509
+ providers: async () => (await import("./providers-models-K6RT7ABI.js")).providersCmd,
1510
+ models: async () => (await import("./providers-models-K6RT7ABI.js")).modelsCmd,
1511
1511
  mcp: async () => (await import("./mcp-CYLPUEHC.js")).mcpCmd,
1512
- plugin: async () => (await import("./plugin-usage-VTL7ZTVC.js")).pluginCmd,
1513
- plugins: async () => (await import("./plugin-usage-VTL7ZTVC.js")).pluginCmd,
1512
+ plugin: async () => (await import("./plugin-usage-EOG4ET4S.js")).pluginCmd,
1513
+ plugins: async () => (await import("./plugin-usage-EOG4ET4S.js")).pluginCmd,
1514
1514
  diag: async () => (await import("./diag-doctor-OQ3AA63O.js")).diagCmd,
1515
1515
  doctor: async () => (await import("./diag-doctor-OQ3AA63O.js")).doctorCmd,
1516
1516
  export: async () => (await import("./export-TPORYVRZ.js")).exportCmd,
1517
- usage: async () => (await import("./plugin-usage-VTL7ZTVC.js")).usageCmd,
1517
+ usage: async () => (await import("./plugin-usage-EOG4ET4S.js")).usageCmd,
1518
1518
  version: async () => (await import("./version-help-ASKQYXDJ.js")).versionCmd,
1519
1519
  help: async () => (await import("./version-help-ASKQYXDJ.js")).helpCmd,
1520
1520
  projects: async () => (await import("./projects-XE76NFNZ.js")).projectsCmd,
1521
- modeldiag: async () => (await import("./modeldiag-HENYJ4N7.js")).modeldiagCmd,
1521
+ modeldiag: async () => (await import("./modeldiag-YOQZHBQV.js")).modeldiagCmd,
1522
1522
  quick: async () => (await import("./quick-VIO2BEHS.js")).quickCmd,
1523
1523
  bench: async () => (await import("./bench-7ZDWSNEF.js")).benchCmd,
1524
1524
  chronicle: async () => (await import("./chronicle-QQEQFSSE.js")).chronicleCmd,
1525
- hq: async () => (await import("./hq-BDO56CX6.js")).hqCmd,
1525
+ hq: async () => (await import("./hq-DVDD4XPV.js")).hqCmd,
1526
1526
  mailbox: async () => (await import("./mailbox-serve-DBOOOOWZ.js")).mailboxServeCmd,
1527
1527
  permissions: async () => (await import("./permissions-FNYLZIL4.js")).permissionsCmd,
1528
1528
  project: async () => (await import("./project-BLQ3ALVL.js")).projectCmd,
@@ -2159,7 +2159,7 @@ async function isPortInUse(host, port) {
2159
2159
  }
2160
2160
  async function handleHqShortCircuit(flags) {
2161
2161
  if (flags["hq"] !== true) return null;
2162
- const { startHqServer } = await import("./hq-server-RJLJJZU7.js");
2162
+ const { startHqServer } = await import("./hq-server-DQHLNHDL.js");
2163
2163
  const tunnelRequested = flags["tunnel"] === true;
2164
2164
  const host = typeof flags["host"] === "string" ? flags["host"] : tunnelRequested ? "127.0.0.1" : HQ_CLI_DEFAULT_HOST;
2165
2165
  if (tunnelRequested && !isLoopbackHost(host)) {
@@ -3149,7 +3149,7 @@ async function initializeCli(argv) {
3149
3149
  async function main(argv) {
3150
3150
  const cliCtx = await initializeCli(argv);
3151
3151
  if (typeof cliCtx === "number") return cliCtx;
3152
- const { runInteractive } = await import("./cli-main-3MQDFSYK.js");
3152
+ const { runInteractive } = await import("./cli-main-7B3G2YSG.js");
3153
3153
  return runInteractive(cliCtx);
3154
3154
  }
3155
3155
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  discoverAndMergeProviders
3
- } from "./chunk-TGDHN4LM.js";
3
+ } from "./chunk-4GVH7V3S.js";
4
4
  import {
5
5
  mutateConfigProviders,
6
6
  normalizeKeys,
@@ -1287,4 +1287,4 @@ ${color2.dim('Enter numbers or provider IDs (comma-separated, or "all"):')}
1287
1287
  export {
1288
1288
  modeldiagCmd
1289
1289
  };
1290
- //# sourceMappingURL=modeldiag-HENYJ4N7.js.map
1290
+ //# sourceMappingURL=modeldiag-YOQZHBQV.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runPluginManagementCommand
3
- } from "./chunk-PEKUKUCH.js";
3
+ } from "./chunk-3MWUZMOL.js";
4
4
  import {
5
5
  activeProfileConfigPath
6
6
  } from "./chunk-DPJZQCVP.js";
@@ -37,4 +37,4 @@ export {
37
37
  pluginCmd,
38
38
  usageCmd
39
39
  };
40
- //# sourceMappingURL=plugin-usage-VTL7ZTVC.js.map
40
+ //# sourceMappingURL=plugin-usage-EOG4ET4S.js.map
@@ -7,8 +7,8 @@ import {
7
7
  pluginNameFromSpec,
8
8
  setupPlugins,
9
9
  warnIfDeprecatedPluginName
10
- } from "./chunk-4U3DMA2Y.js";
11
- import "./chunk-PEKUKUCH.js";
10
+ } from "./chunk-4WJKVQJY.js";
11
+ import "./chunk-3MWUZMOL.js";
12
12
  import "./chunk-TYO2OVAD.js";
13
13
  import "./chunk-7OCVIDC7.js";
14
14
  export {
@@ -21,4 +21,4 @@ export {
21
21
  setupPlugins,
22
22
  warnIfDeprecatedPluginName
23
23
  };
24
- //# sourceMappingURL=plugins-ETY6W3DC.js.map
24
+ //# sourceMappingURL=plugins-56CMWMPR.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  visibleModelIds
3
- } from "./chunk-66T43Q4Y.js";
3
+ } from "./chunk-N7ULWBO5.js";
4
4
  import {
5
5
  mutateConfigProviders
6
6
  } from "./chunk-PJHET45A.js";
@@ -127,7 +127,7 @@ var modelsCmd = async (args, deps) => {
127
127
  return 1;
128
128
  }
129
129
  }
130
- const flags = parseFlags(args);
130
+ const flags = subcommandFlags(args, deps);
131
131
  const search = typeof flags["search"] === "string" ? flags["search"].toLowerCase() : "";
132
132
  const perPage = Number(flags["per-page"]) > 0 ? Number(flags["per-page"]) : DEFAULT_PER_PAGE;
133
133
  const page = Math.max(1, Number(flags["page"]) || 1);
@@ -138,10 +138,10 @@ var modelsCmd = async (args, deps) => {
138
138
  );
139
139
  return 1;
140
140
  }
141
- let lookupId = providerId;
142
- const savedAlias = deps.config.providers?.[providerId];
143
- if (savedAlias?.type && savedAlias.type !== providerId) lookupId = savedAlias.type;
144
- const provider = await deps.modelsRegistry.getProvider(lookupId);
141
+ const { providerId: lookupId, provider } = await getCatalogProviderForConfigProvider(
142
+ providerId,
143
+ deps
144
+ );
145
145
  if (!provider) {
146
146
  deps.renderer.writeError(
147
147
  lookupId !== providerId ? `Alias "${providerId}" points at catalog id "${lookupId}" which is not in the cache.` : `Provider "${providerId}" not in catalog.`
@@ -234,11 +234,33 @@ ${navLines.join(" \xB7 ")}
234
234
  function aliasProviderId(configProviderId, cfg) {
235
235
  return cfg?.type && cfg.type !== configProviderId ? cfg.type : configProviderId;
236
236
  }
237
+ async function discoverProviderModels(deps, ids) {
238
+ if (!("mergeOverlay" in deps.modelsRegistry)) return void 0;
239
+ try {
240
+ const { discoverAndMergeProviders } = await import("./auto-discover-providers-DRW4OZHG.js");
241
+ await discoverAndMergeProviders({
242
+ config: deps.config,
243
+ registry: deps.modelsRegistry,
244
+ cacheDir: deps.paths.cacheDir
245
+ });
246
+ } catch {
247
+ return void 0;
248
+ }
249
+ for (const id of ids) {
250
+ const found = await deps.modelsRegistry.getProvider(id);
251
+ if (found) return found;
252
+ }
253
+ return void 0;
254
+ }
237
255
  async function getCatalogProviderForConfigProvider(providerId, deps) {
238
256
  const cfg = deps.config.providers?.[providerId];
239
257
  const lookupId = aliasProviderId(providerId, cfg);
240
258
  const provider = await deps.modelsRegistry.getProvider(lookupId);
241
- return { providerId: lookupId, provider };
259
+ if (provider) return { providerId: lookupId, provider };
260
+ return {
261
+ providerId: lookupId,
262
+ provider: await discoverProviderModels(deps, [lookupId, providerId])
263
+ };
242
264
  }
243
265
  function uniqueStrings(values) {
244
266
  return [...new Set(values.map((v) => v.trim()).filter(Boolean))];
@@ -478,13 +500,16 @@ function parseSizeFlag(raw) {
478
500
  if (unit === "k") return Math.round(num * 1e3);
479
501
  return Math.round(num);
480
502
  }
503
+ function subcommandFlags(args, deps) {
504
+ return { ...deps.flags ?? {}, ...parseFlags(args) };
505
+ }
481
506
  function parseBoolFlag(flags, key) {
482
507
  if (flags[key] === true || flags[key] === "true") return true;
483
508
  if (flags[`no-${key}`] !== void 0) return false;
484
509
  return void 0;
485
510
  }
486
511
  async function modelsAdd(args, deps) {
487
- const flags = parseFlags(args);
512
+ const flags = subcommandFlags(args, deps);
488
513
  const pos = positionals(args);
489
514
  const modelId = pos[0];
490
515
  if (!modelId) {
@@ -586,4 +611,4 @@ export {
586
611
  modelsCmd,
587
612
  providersCmd
588
613
  };
589
- //# sourceMappingURL=providers-models-GDELBHZK.js.map
614
+ //# sourceMappingURL=providers-models-K6RT7ABI.js.map
@@ -130,6 +130,25 @@ export interface SlashCommandContext {
130
130
  * Get live coordinator status for /fleet. Returns null when no fleet is active.
131
131
  */
132
132
  onFleetStatus?: (() => import('@wrongstack/core/types').CoordinatorStatus | null) | undefined;
133
+ /**
134
+ * Read-only concurrency + lifetime spawn budget for `/fleet status`.
135
+ * Returns null when no fleet host is available.
136
+ */
137
+ onFleetBudget?: (() => {
138
+ maxConcurrent: number;
139
+ activeAgents: number;
140
+ maxSpawns: number;
141
+ usedSpawns: number;
142
+ remainingSpawns: number;
143
+ maxTokens?: number | undefined;
144
+ usedTokens?: number | undefined;
145
+ remainingTokens?: number | undefined;
146
+ maxCostUsd?: number | undefined;
147
+ usedCostUsd?: number | undefined;
148
+ remainingCostUsd?: number | undefined;
149
+ checkpointMaxSpawns?: number | undefined;
150
+ ceilingMismatch?: boolean | undefined;
151
+ } | null) | undefined;
133
152
  /**
134
153
  * Get fleet usage summary for /fleet usage.
135
154
  */
@@ -29,6 +29,23 @@ export interface SetupEventsDeps {
29
29
  eventUnsubscribers: Array<() => void>;
30
30
  globalConfigPath?: string | undefined;
31
31
  onFleetBroadcaster?: ((fn: () => Promise<void>) => void) | undefined;
32
+ /**
33
+ * Live fleet budget snapshot (issue #323). Merged into
34
+ * `fleet.concurrency_update` so the WebUI can show used/remaining spawns
35
+ * without a probe spawn.
36
+ */
37
+ getFleetBudget?: (() => {
38
+ maxSpawns?: number | undefined;
39
+ usedSpawns?: number | undefined;
40
+ remainingSpawns?: number | undefined;
41
+ maxConcurrent?: number | undefined;
42
+ activeAgents?: number | undefined;
43
+ maxSpawnsSource?: string | undefined;
44
+ maxConcurrentSource?: string | undefined;
45
+ effectiveSource?: string | undefined;
46
+ checkpointMaxSpawns?: number | undefined;
47
+ ceilingMismatch?: boolean | undefined;
48
+ } | null) | undefined;
32
49
  }
33
50
  /** CLI adapter around the canonical EventBus→WebSocket subscription graph. */
34
51
  export declare function createSetupEvents(deps: SetupEventsDeps): () => void;
@@ -6,25 +6,42 @@ interface ConsoleTarget {
6
6
  error: ConsoleMethod;
7
7
  debug: ConsoleMethod;
8
8
  }
9
- export interface BoundedTerminalLogViewOptions {
10
- maxLines?: number | undefined;
11
- refreshMs?: number | undefined;
9
+ export interface QuietSurfaceConsoleOptions {
12
10
  isTTY?: boolean | undefined;
11
+ /** Pass everything through untouched (the `WEBUI_VERBOSE=1` escape hatch). */
12
+ verbose?: boolean | undefined;
13
13
  consoleTarget?: ConsoleTarget | undefined;
14
- write?: ((chunk: string) => void) | undefined;
15
- setIntervalFn?: ((callback: () => void, delay: number) => NodeJS.Timeout) | undefined;
16
- clearIntervalFn?: ((timer: NodeJS.Timeout) => void) | undefined;
14
+ /** Ring-buffer size for {@link QuietSurfaceConsole.recent}. */
15
+ bufferLines?: number | undefined;
17
16
  }
18
- export interface BoundedTerminalLogView {
17
+ export interface QuietSurfaceConsole {
18
+ /** `false` when the console was left untouched (non-TTY or verbose). */
19
19
  readonly enabled: boolean;
20
- redraw(): void;
20
+ /** Chatter lines dropped from the terminal so far. */
21
+ readonly mutedCount: number;
22
+ /** Newest muted lines, oldest first — for a diagnostic dump on failure. */
23
+ recent(): readonly string[];
21
24
  stop(): void;
22
25
  }
23
26
  /**
24
- * Periodically redraw an interactive WebUI/SimpleUI terminal with only its
25
- * newest physical log lines. Non-TTY output is deliberately untouched so
26
- * redirected output and log collectors retain the complete append-only log.
27
+ * Keep an interactive WebUI/SimpleUI terminal quiet while the browser owns
28
+ * the output surface.
29
+ *
30
+ * Two things fill that terminal, and this handles both:
31
+ *
32
+ * - **Chatter.** Every `console.log`/`info`/`debug` from the server is
33
+ * progress the browser already shows. Those are dropped from the
34
+ * terminal (still counted, and the newest are kept in memory for a
35
+ * failure dump); `console.warn`/`error` are forwarded untouched.
36
+ *
37
+ * - **Repetition.** A warning that fires on a timer used to append an
38
+ * identical line every tick until the terminal was pages deep in the
39
+ * same text. Consecutive identical lines now collapse into a single
40
+ * `↑ previous line repeated N×` notice emitted when the streak ends.
41
+ *
42
+ * Non-TTY output is deliberately untouched so redirected output and log
43
+ * collectors retain the complete append-only log, as is `WEBUI_VERBOSE=1`.
27
44
  */
28
- export declare function startBoundedTerminalLogView(options?: BoundedTerminalLogViewOptions): BoundedTerminalLogView;
45
+ export declare function startQuietSurfaceConsole(options?: QuietSurfaceConsoleOptions): QuietSurfaceConsole;
29
46
  export {};
30
47
  //# sourceMappingURL=terminal-log-view.d.ts.map
@@ -679,9 +679,23 @@ function createSetupEvents(deps) {
679
679
  let fleetConcurrency = 0;
680
680
  let fleetConcurrencyMax = 4;
681
681
  const emitConcurrency = () => {
682
+ const budget = deps.getFleetBudget?.() ?? null;
682
683
  deps.broadcast({
683
684
  type: "fleet.concurrency_update",
684
- payload: deps.sessionPayload({ fleetConcurrency, fleetConcurrencyMax })
685
+ payload: deps.sessionPayload({
686
+ fleetConcurrency: budget?.activeAgents ?? fleetConcurrency,
687
+ fleetConcurrencyMax: budget?.maxConcurrent ?? fleetConcurrencyMax,
688
+ ...budget ? {
689
+ maxSpawns: budget.maxSpawns,
690
+ usedSpawns: budget.usedSpawns,
691
+ remainingSpawns: budget.remainingSpawns,
692
+ maxSpawnsSource: budget.maxSpawnsSource,
693
+ maxConcurrentSource: budget.maxConcurrentSource,
694
+ effectiveSource: budget.effectiveSource,
695
+ checkpointMaxSpawns: budget.checkpointMaxSpawns,
696
+ ceilingMismatch: budget.ceilingMismatch
697
+ } : {}
698
+ })
685
699
  });
686
700
  };
687
701
  return function setupEvents() {
@@ -761,24 +775,35 @@ import {
761
775
 
762
776
  // src/webui-server/terminal-log-view.ts
763
777
  import { format } from "node:util";
764
- var DEFAULT_MAX_LINES = 5;
765
- var DEFAULT_REFRESH_MS = 5e3;
766
- var CLEAR_VISIBLE_TERMINAL = "\x1B[2J\x1B[H";
767
- function startBoundedTerminalLogView(options = {}) {
768
- const isTTY = options.isTTY ?? process.stdout.isTTY === true;
769
- if (!isTTY) {
770
- return { enabled: false, redraw: () => {
771
- }, stop: () => {
772
- } };
778
+ var DEFAULT_BUFFER_LINES = 200;
779
+ var MUTED_LEVELS = ["log", "info", "debug"];
780
+ var FORWARDED_LEVELS = ["warn", "error"];
781
+ var DISABLED = {
782
+ enabled: false,
783
+ mutedCount: 0,
784
+ recent: () => [],
785
+ stop: () => {
786
+ }
787
+ };
788
+ function envVerbose() {
789
+ for (const name of ["WEBUI_VERBOSE", "WRONGSTACK_WEBUI_VERBOSE"]) {
790
+ const value = process.env[name]?.trim().toLowerCase();
791
+ if (value === "1" || value === "true" || value === "yes" || value === "on") return true;
773
792
  }
774
- const maxLines = Math.max(1, Math.trunc(options.maxLines ?? DEFAULT_MAX_LINES));
775
- const refreshMs = Math.max(100, Math.trunc(options.refreshMs ?? DEFAULT_REFRESH_MS));
793
+ return false;
794
+ }
795
+ function startQuietSurfaceConsole(options = {}) {
796
+ const isTTY = options.isTTY ?? process.stdout.isTTY === true;
797
+ const verbose = options.verbose ?? envVerbose();
798
+ if (!isTTY || verbose) return DISABLED;
799
+ const bufferLines = Math.max(1, Math.trunc(options.bufferLines ?? DEFAULT_BUFFER_LINES));
776
800
  const target = options.consoleTarget ?? console;
777
- const write = options.write ?? ((chunk) => process.stdout.write(chunk));
778
- const setIntervalFn = options.setIntervalFn ?? setInterval;
779
- const clearIntervalFn = options.clearIntervalFn ?? clearInterval;
780
- const lines = [];
801
+ const muted = [];
802
+ let mutedCount = 0;
781
803
  let stopped = false;
804
+ let lastLine = null;
805
+ let lastEmit = null;
806
+ let repeats = 0;
782
807
  const originals = {
783
808
  log: target.log,
784
809
  info: target.info,
@@ -787,35 +812,47 @@ function startBoundedTerminalLogView(options = {}) {
787
812
  debug: target.debug
788
813
  };
789
814
  const wrappers = {};
790
- const remember = (args) => {
791
- for (const line of format(...args).split(/\r?\n/u)) {
792
- lines.push(line);
793
- if (lines.length > maxLines) lines.shift();
794
- }
815
+ const flushRepeats = () => {
816
+ if (repeats === 0 || !lastEmit) return;
817
+ const count = repeats;
818
+ repeats = 0;
819
+ lastEmit.call(target, ` \u2191 previous line repeated ${count}\xD7`);
795
820
  };
796
- for (const level of Object.keys(originals)) {
821
+ for (const level of MUTED_LEVELS) {
822
+ const wrapper = (...args) => {
823
+ mutedCount += 1;
824
+ muted.push(format(...args));
825
+ if (muted.length > bufferLines) muted.shift();
826
+ };
827
+ wrappers[level] = wrapper;
828
+ target[level] = wrapper;
829
+ }
830
+ for (const level of FORWARDED_LEVELS) {
797
831
  const original = originals[level];
798
832
  const wrapper = (...args) => {
799
- remember(args);
833
+ const line = format(...args);
834
+ if (line === lastLine) {
835
+ repeats += 1;
836
+ return;
837
+ }
838
+ flushRepeats();
839
+ lastLine = line;
840
+ lastEmit = original;
800
841
  original.apply(target, args);
801
842
  };
802
843
  wrappers[level] = wrapper;
803
844
  target[level] = wrapper;
804
845
  }
805
- const redraw = () => {
806
- if (stopped || lines.length === 0) return;
807
- write(`${CLEAR_VISIBLE_TERMINAL}${lines.join("\n")}
808
- `);
809
- };
810
- const timer = setIntervalFn(redraw, refreshMs);
811
- timer.unref?.();
812
846
  return {
813
847
  enabled: true,
814
- redraw,
848
+ get mutedCount() {
849
+ return mutedCount;
850
+ },
851
+ recent: () => [...muted],
815
852
  stop: () => {
816
853
  if (stopped) return;
817
854
  stopped = true;
818
- clearIntervalFn(timer);
855
+ flushRepeats();
819
856
  for (const level of Object.keys(originals)) {
820
857
  if (target[level] === wrappers[level]) target[level] = originals[level];
821
858
  }
@@ -825,6 +862,7 @@ function startBoundedTerminalLogView(options = {}) {
825
862
 
826
863
  // src/webui-server.ts
827
864
  async function runWebUI(opts) {
865
+ const terminalLogView = startQuietSurfaceConsole();
828
866
  const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary({ policyId: "cli-webui-trusted-host-compat-v1" });
829
867
  const host = opts.host ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
830
868
  const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
@@ -1060,7 +1098,10 @@ async function runWebUI(opts) {
1060
1098
  publicUrl,
1061
1099
  projectRoot: opts.projectRoot,
1062
1100
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1063
- registryBaseDir
1101
+ registryBaseDir,
1102
+ // Lets a same-project TUI/REPL authenticate `POST /api/fleet/ping` now
1103
+ // that the API requires a token on every bind (H3).
1104
+ authToken: wsToken
1064
1105
  });
1065
1106
  }
1066
1107
  const eventUnsubscribers = [];
@@ -1096,7 +1137,8 @@ async function runWebUI(opts) {
1096
1137
  globalConfigPath: path5.join(globalRoot, "config.json"),
1097
1138
  onFleetBroadcaster: (fn) => {
1098
1139
  fleetBroadcastCli = fn;
1099
- }
1140
+ },
1141
+ ...opts.getFleetBudget ? { getFleetBudget: opts.getFleetBudget } : {}
1100
1142
  });
1101
1143
  const wsHandlerCtx = {
1102
1144
  providerStore: createProviderConfigStore(
@@ -1172,7 +1214,10 @@ async function runWebUI(opts) {
1172
1214
  ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {}
1173
1215
  };
1174
1216
  const oldMax = opts.agent.ctx.provider.capabilities?.maxContext;
1175
- const prov = makeProviderFromConfig(activeId, { ...newCfg, type: activeId });
1217
+ const prov = makeProviderFromConfig(activeId, {
1218
+ ...newCfg,
1219
+ type: newCfg.type ?? activeId
1220
+ });
1176
1221
  if (oldMax != null && prov.capabilities) prov.capabilities.maxContext = oldMax;
1177
1222
  opts.agent.ctx.provider = prov;
1178
1223
  console.log(`[WebUI] Provider credentials reloaded from config.json (${activeId})`);
@@ -1352,7 +1397,6 @@ async function runWebUI(opts) {
1352
1397
  kanbanHostRoutes,
1353
1398
  statusTracker: opts.statusTracker
1354
1399
  });
1355
- const terminalLogView = startBoundedTerminalLogView();
1356
1400
  const stopped = new Promise((resolve) => {
1357
1401
  let listeningAnnounced = false;
1358
1402
  const announceListening = () => {
@@ -1431,6 +1475,28 @@ async function runWebUI(opts) {
1431
1475
  flushAllStreamBuffers();
1432
1476
  for (const unsub of eventUnsubscribers) unsub();
1433
1477
  },
1478
+ // Issue #322: kill session-owned children (MCP, bash/exec, tool-spawned
1479
+ // dev servers) before closing the listen socket so Windows process trees
1480
+ // do not outlive the parent and strand the event loop on open stdio.
1481
+ stopOwnedChildren: async () => {
1482
+ try {
1483
+ const { getProcessRegistry } = await import("@wrongstack/tools");
1484
+ getProcessRegistry().killAll({ force: true, includeProtected: true });
1485
+ } catch (err) {
1486
+ console.debug(
1487
+ `[webui-server] process-registry killAll failed: ${err instanceof Error ? err.message : String(err)}`
1488
+ );
1489
+ }
1490
+ if (opts.mcpRegistry) {
1491
+ try {
1492
+ await opts.mcpRegistry.stopAll();
1493
+ } catch (err) {
1494
+ console.debug(
1495
+ `[webui-server] mcpRegistry.stopAll failed: ${err instanceof Error ? err.message : String(err)}`
1496
+ );
1497
+ }
1498
+ }
1499
+ },
1434
1500
  disposeResources: () => {
1435
1501
  credentialWatcherClose?.();
1436
1502
  credentialWatcherClose = void 0;
@@ -1455,8 +1521,13 @@ async function runWebUI(opts) {
1455
1521
  pid: process.pid,
1456
1522
  registryBaseDir,
1457
1523
  onStopped: () => {
1458
- terminalLogView.redraw();
1524
+ const muted = terminalLogView.mutedCount;
1459
1525
  terminalLogView.stop();
1526
+ if (muted > 0) {
1527
+ console.log(
1528
+ `[WebUI] ${muted} progress line(s) kept out of this terminal \u2014 set WEBUI_VERBOSE=1 to see them.`
1529
+ );
1530
+ }
1460
1531
  opts.onExit?.();
1461
1532
  resolve();
1462
1533
  }
@@ -1487,4 +1558,4 @@ async function runWebUI(opts) {
1487
1558
  export {
1488
1559
  runWebUI
1489
1560
  };
1490
- //# sourceMappingURL=webui-server-PCLSXX6L.js.map
1561
+ //# sourceMappingURL=webui-server-G3EKFXLL.js.map
@@ -36,6 +36,22 @@ export interface CliWebUIOptions {
36
36
  surface?: 'webui' | 'simpleui' | undefined;
37
37
  /** Fixed access token/password. Defaults to WEBUI_TOKEN or random per process. */
38
38
  accessToken?: string | undefined;
39
+ /**
40
+ * Live fleet concurrency + lifetime spawn budget for WebUI (issue #323).
41
+ * Merged into `fleet.concurrency_update` broadcasts.
42
+ */
43
+ getFleetBudget?: (() => {
44
+ maxSpawns?: number | undefined;
45
+ usedSpawns?: number | undefined;
46
+ remainingSpawns?: number | undefined;
47
+ maxConcurrent?: number | undefined;
48
+ activeAgents?: number | undefined;
49
+ maxSpawnsSource?: string | undefined;
50
+ maxConcurrentSource?: string | undefined;
51
+ effectiveSource?: string | undefined;
52
+ checkpointMaxSpawns?: number | undefined;
53
+ ceilingMismatch?: boolean | undefined;
54
+ } | null) | undefined;
39
55
  /** Browser-facing HTTP URL, used when WebUI is exposed behind a tunnel/proxy. */
40
56
  publicUrl?: string | undefined;
41
57
  /** Browser-facing WebSocket URL injected into the frontend. */
@@ -49,6 +49,9 @@ export interface BrainOrchestrationDeps {
49
49
  stateCheckpointPath: string | undefined;
50
50
  fleetRootForPromotion: string;
51
51
  maxConcurrent: number | undefined;
52
+ maxSpawns?: number | undefined;
53
+ maxConcurrentSource?: import('../fleet/budget-source.js').FleetBudgetSource | undefined;
54
+ maxSpawnsSource?: import('../fleet/budget-source.js').FleetBudgetSource | undefined;
52
55
  effectiveMaxContextRef: {
53
56
  current: number;
54
57
  };
@@ -23,6 +23,10 @@ export interface DirectorAutonomyResult {
23
23
  director: Director | null;
24
24
  directorMode: boolean;
25
25
  maxConcurrent: number | undefined;
26
+ /** Lifetime spawn ceiling resolved from flag/env/profile/default. */
27
+ maxSpawns: number;
28
+ maxConcurrentSource: import('../fleet/budget-source.js').FleetBudgetSource;
29
+ maxSpawnsSource: import('../fleet/budget-source.js').FleetBudgetSource;
26
30
  autonomyMode: import('../services/autonomy-mode.js').AutonomyMode;
27
31
  nextPredictEnabled: boolean;
28
32
  currentSuggestions: string[];