@wrongstack/cli 0.299.0 → 0.301.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 (82) hide show
  1. package/dist/{acp-S4LZQB66.js → acp-DATHHPNT.js} +36 -17
  2. package/dist/acp-server-agent.d.ts +1 -1
  3. package/dist/{audit-2ZSFGNIF.js → audit-QRCLEHHW.js} +5 -1
  4. package/dist/{auth-G5YKKSRH.js → auth-JASC4K3F.js} +151 -38
  5. package/dist/auth-menu/loopback-server.d.ts +41 -55
  6. package/dist/auth-menu/openai-codex-oauth.d.ts +36 -123
  7. package/dist/auto-discover-providers-DRW4OZHG.js +8 -0
  8. package/dist/boot/dispatch-webui.d.ts +3 -0
  9. package/dist/boot/launch-menu.d.ts +2 -4
  10. package/dist/boot/tool-registry.d.ts +3 -0
  11. package/dist/chimera-reviewer-policy.d.ts +10 -3
  12. package/dist/chimera-work-registry.d.ts +19 -0
  13. package/dist/{chronicle-QQEQFSSE.js → chronicle-63MFCZN6.js} +26 -5
  14. package/dist/{chunk-YWUPHRGB.js → chunk-263G3FMB.js} +33 -10
  15. package/dist/{chunk-2SLSUDYS.js → chunk-2KDOFTTM.js} +312 -211
  16. package/dist/{chunk-TGDHN4LM.js → chunk-4GVH7V3S.js} +4 -24
  17. package/dist/chunk-CSAPOCBP.js +24 -0
  18. package/dist/{chunk-66T43Q4Y.js → chunk-ETOEGL3V.js} +6 -9
  19. package/dist/{chunk-PEKUKUCH.js → chunk-GUHQQG63.js} +32 -14
  20. package/dist/chunk-KE7E7DPX.js +38 -0
  21. package/dist/{chunk-ZNZJ34RF.js → chunk-MHF2IJDE.js} +21 -4
  22. package/dist/{chunk-4HH3YGG4.js → chunk-MRLYDHQ7.js} +8 -5
  23. package/dist/{chunk-L5CE52XW.js → chunk-O7XNGCRT.js} +317 -309
  24. package/dist/chunk-PYTFS4MC.js +238 -0
  25. package/dist/{chunk-DPJZQCVP.js → chunk-YMXXOOFN.js} +1 -1
  26. package/dist/{chunk-4U3DMA2Y.js → chunk-ZECLGUTF.js} +28 -21
  27. package/dist/{cli-main-3MQDFSYK.js → cli-main-XWC57MGE.js} +849 -242
  28. package/dist/{execution-ANOYMWPO.js → execution-LHMSMDHB.js} +174 -55
  29. package/dist/execution-chimera-cascade.d.ts +3 -4
  30. package/dist/execution-chimera-review.d.ts +2 -3
  31. package/dist/execution-cleanup.d.ts +3 -2
  32. package/dist/execution.d.ts +1 -1
  33. package/dist/{export-TPORYVRZ.js → export-DOBGPY23.js} +12 -1
  34. package/dist/fleet/budget-source.d.ts +18 -0
  35. package/dist/fleet/host-context.d.ts +1 -1
  36. package/dist/fleet/host-helpers.d.ts +14 -0
  37. package/dist/fleet/host-status.d.ts +31 -0
  38. package/dist/fleet/host-types.d.ts +8 -0
  39. package/dist/fleet/host.d.ts +5 -0
  40. package/dist/fleet/status-broadcast.d.ts +1 -1
  41. package/dist/{hq-BDO56CX6.js → hq-LN5CJHCM.js} +22 -3
  42. package/dist/hq-server/auth-state.d.ts +29 -1
  43. package/dist/hq-server/auth.d.ts +16 -0
  44. package/dist/hq-server/client-address.d.ts +56 -0
  45. package/dist/hq-server/login-attempt-store.d.ts +30 -10
  46. package/dist/hq-server/routes/auth-handlers.d.ts +66 -26
  47. package/dist/hq-server/routes.d.ts +23 -1
  48. package/dist/hq-server/types.d.ts +10 -0
  49. package/dist/hq-server/ws.d.ts +17 -6
  50. package/dist/{hq-server-RJLJJZU7.js → hq-server-S7HZG3PA.js} +3 -2
  51. package/dist/hq-server.d.ts +19 -0
  52. package/dist/index.js +69 -104
  53. package/dist/live-settings-input.d.ts +5 -0
  54. package/dist/{mcp-CYLPUEHC.js → mcp-OHPJH73U.js} +6 -2
  55. package/dist/{modeldiag-HENYJ4N7.js → modeldiag-3DOK4BWW.js} +3 -3
  56. package/dist/{plugin-usage-VTL7ZTVC.js → plugin-usage-YEVLOR33.js} +7 -3
  57. package/dist/{plugins-ETY6W3DC.js → plugins-K3BAUP4D.js} +3 -3
  58. package/dist/profile-config-path.d.ts +0 -2
  59. package/dist/provider-helpers.d.ts +0 -5
  60. package/dist/{providers-models-GDELBHZK.js → providers-models-WEZ4EGLL.js} +38 -12
  61. package/dist/{replay-KCTXNMZQ.js → replay-FCBNHW3K.js} +5 -1
  62. package/dist/{rewind-EJMVFVLS.js → rewind-AOD3JIYX.js} +5 -1
  63. package/dist/{sessions-config-NLCMK3BY.js → sessions-config-XXN7267N.js} +8 -3
  64. package/dist/slash-commands/command-context.d.ts +19 -0
  65. package/dist/subcommands/flags.d.ts +23 -0
  66. package/dist/terminal-format.d.ts +15 -0
  67. package/dist/webui-server/setup-events.d.ts +17 -0
  68. package/dist/webui-server/terminal-log-view.d.ts +29 -12
  69. package/dist/{webui-server-PCLSXX6L.js → webui-server-DD35QFNO.js} +113 -50
  70. package/dist/webui-server-options.d.ts +16 -0
  71. package/dist/wiring/brain-and-orchestration.d.ts +4 -1
  72. package/dist/wiring/director-setup.d.ts +4 -0
  73. package/dist/wiring/fallback-gate.d.ts +31 -0
  74. package/dist/wiring/fleet-command-handlers.d.ts +1 -1
  75. package/dist/wiring/mailbox-bridge-bootstrap.d.ts +3 -1
  76. package/dist/wiring/management-tools.d.ts +9 -1
  77. package/dist/wiring/pipeline.d.ts +5 -5
  78. package/dist/wiring/plugins.d.ts +3 -10
  79. package/dist/wiring/provider-utility-tools.d.ts +27 -1
  80. package/dist/wiring/tools.d.ts +4 -5
  81. package/package.json +23 -23
  82. package/dist/chunk-GO3TJICK.js +0 -489
@@ -1,12 +1,15 @@
1
+ import {
2
+ restoreFlags
3
+ } from "./chunk-CSAPOCBP.js";
1
4
  import {
2
5
  getHistoryEntry,
3
6
  listHistory,
4
7
  restoreFromHistory,
5
8
  restoreLast
6
- } from "./chunk-YWUPHRGB.js";
9
+ } from "./chunk-263G3FMB.js";
7
10
  import {
8
11
  activeProfileConfigPath
9
- } from "./chunk-DPJZQCVP.js";
12
+ } from "./chunk-YMXXOOFN.js";
10
13
  import "./chunk-7OCVIDC7.js";
11
14
 
12
15
  // src/subcommands/handlers/sessions-config.ts
@@ -233,6 +236,7 @@ Fleet Run: ${runId}
233
236
 
234
237
  // src/subcommands/handlers/sessions-config.ts
235
238
  var sessionsCmd = async (args, deps) => {
239
+ args = restoreFlags(args, deps, ["to", "id", "latest", "l"]);
236
240
  const sub = args[0];
237
241
  if (sub === "fleet") {
238
242
  return sessionsFleetCmd(args.slice(1), deps);
@@ -302,6 +306,7 @@ var sessionsCmd = async (args, deps) => {
302
306
  return 0;
303
307
  };
304
308
  var configCmd = async (args, deps) => {
309
+ args = restoreFlags(args, deps, ["id", "latest", "l"]);
305
310
  const sub = args[0];
306
311
  if (!sub || sub === "show") {
307
312
  deps.renderer.write(JSON.stringify(redactKeys(deps.config), null, 2) + "\n");
@@ -406,4 +411,4 @@ export {
406
411
  configCmd,
407
412
  sessionsCmd
408
413
  };
409
- //# sourceMappingURL=sessions-config-NLCMK3BY.js.map
414
+ //# sourceMappingURL=sessions-config-XXN7267N.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
  */
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Re-attach the flags `parseArgs` stripped before a subcommand handler ran.
3
+ *
4
+ * The dispatcher (`boot.ts`) hands a handler only `positional.slice(1)`, while
5
+ * `parseArgs` has already pulled every `--flag` into `flags`. A handler that
6
+ * parses its own `args` therefore saw NONE of the flags it documents:
7
+ * `rewind --last 2` printed usage, `export --format json --out r.json` dumped
8
+ * markdown to stdout, `plugin add X --disabled` wrote it enabled, and
9
+ * `chronicle prune --days 7 --dry-run` performed a real 30-day purge.
10
+ *
11
+ * Re-injecting every flag at the dispatcher would be wrong: top-level switches
12
+ * (`--verbose`, `--yolo`, `--tui`) would land in the subcommand's argv too, and
13
+ * handlers with an "Unknown flag" branch — `export.ts` has one — would start
14
+ * rejecting valid invocations. So each handler declares the flags it owns and
15
+ * gets back exactly those.
16
+ *
17
+ * A spelling already present in `args` always wins: a directly-invoked handler
18
+ * (and anything the caller placed after `--`) keeps overriding.
19
+ */
20
+ export declare function restoreFlags(args: string[], deps: {
21
+ flags?: Record<string, string | boolean> | undefined;
22
+ }, names: readonly string[]): string[];
23
+ //# sourceMappingURL=flags.d.ts.map
@@ -0,0 +1,15 @@
1
+ export type TerminalTone = 'blue' | 'cyan' | 'green' | 'magenta' | 'muted' | 'success' | 'yellow';
2
+ export interface TerminalStyleOptions {
3
+ readonly bold?: boolean;
4
+ /** Capability override for deterministic tests; production derives this from env/TTY. */
5
+ readonly enabled?: boolean;
6
+ }
7
+ /** Match the CLI banner's NO_COLOR/FORCE_COLOR/TTY precedence. */
8
+ export declare function terminalFormattingEnabled(env?: NodeJS.ProcessEnv, isTty?: boolean): boolean;
9
+ /** Add semantic color while preserving plain text when formatting is disabled. */
10
+ export declare function terminalText(text: string, tone: TerminalTone, options?: TerminalStyleOptions): string;
11
+ /** Render a colored OSC 8 hyperlink, falling back to the visible URL unchanged. */
12
+ export declare function terminalLink(url: string, options?: TerminalStyleOptions & {
13
+ readonly tone?: TerminalTone;
14
+ }): string;
15
+ //# sourceMappingURL=terminal-format.d.ts.map
@@ -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
@@ -41,11 +41,12 @@ import {
41
41
  resolveAuthToken,
42
42
  SddBoardWebSocketHandler,
43
43
  SddWizardWebSocketHandler,
44
+ sendSerialized,
44
45
  SpecsWebSocketHandler,
45
46
  TerminalWebSocketHandler,
46
47
  WorktreeWebSocketHandler
47
48
  } from "@wrongstack/webui-server";
48
- import { WebSocket, WebSocketServer } from "ws";
49
+ import { WebSocketServer } from "ws";
49
50
 
50
51
  // src/webui-server/client-registration.ts
51
52
  import { createWebuiClientPresence } from "@wrongstack/webui-server";
@@ -679,9 +680,23 @@ function createSetupEvents(deps) {
679
680
  let fleetConcurrency = 0;
680
681
  let fleetConcurrencyMax = 4;
681
682
  const emitConcurrency = () => {
683
+ const budget = deps.getFleetBudget?.() ?? null;
682
684
  deps.broadcast({
683
685
  type: "fleet.concurrency_update",
684
- payload: deps.sessionPayload({ fleetConcurrency, fleetConcurrencyMax })
686
+ payload: deps.sessionPayload({
687
+ fleetConcurrency: budget?.activeAgents ?? fleetConcurrency,
688
+ fleetConcurrencyMax: budget?.maxConcurrent ?? fleetConcurrencyMax,
689
+ ...budget ? {
690
+ maxSpawns: budget.maxSpawns,
691
+ usedSpawns: budget.usedSpawns,
692
+ remainingSpawns: budget.remainingSpawns,
693
+ maxSpawnsSource: budget.maxSpawnsSource,
694
+ maxConcurrentSource: budget.maxConcurrentSource,
695
+ effectiveSource: budget.effectiveSource,
696
+ checkpointMaxSpawns: budget.checkpointMaxSpawns,
697
+ ceilingMismatch: budget.ceilingMismatch
698
+ } : {}
699
+ })
685
700
  });
686
701
  };
687
702
  return function setupEvents() {
@@ -761,24 +776,35 @@ import {
761
776
 
762
777
  // src/webui-server/terminal-log-view.ts
763
778
  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
- } };
779
+ var DEFAULT_BUFFER_LINES = 200;
780
+ var MUTED_LEVELS = ["log", "info", "debug"];
781
+ var FORWARDED_LEVELS = ["warn", "error"];
782
+ var DISABLED = {
783
+ enabled: false,
784
+ mutedCount: 0,
785
+ recent: () => [],
786
+ stop: () => {
787
+ }
788
+ };
789
+ function envVerbose() {
790
+ for (const name of ["WEBUI_VERBOSE", "WRONGSTACK_WEBUI_VERBOSE"]) {
791
+ const value = process.env[name]?.trim().toLowerCase();
792
+ if (value === "1" || value === "true" || value === "yes" || value === "on") return true;
773
793
  }
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));
794
+ return false;
795
+ }
796
+ function startQuietSurfaceConsole(options = {}) {
797
+ const isTTY = options.isTTY ?? process.stdout.isTTY === true;
798
+ const verbose = options.verbose ?? envVerbose();
799
+ if (!isTTY || verbose) return DISABLED;
800
+ const bufferLines = Math.max(1, Math.trunc(options.bufferLines ?? DEFAULT_BUFFER_LINES));
776
801
  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 = [];
802
+ const muted = [];
803
+ let mutedCount = 0;
781
804
  let stopped = false;
805
+ let lastLine = null;
806
+ let lastEmit = null;
807
+ let repeats = 0;
782
808
  const originals = {
783
809
  log: target.log,
784
810
  info: target.info,
@@ -787,35 +813,47 @@ function startBoundedTerminalLogView(options = {}) {
787
813
  debug: target.debug
788
814
  };
789
815
  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
- }
816
+ const flushRepeats = () => {
817
+ if (repeats === 0 || !lastEmit) return;
818
+ const count = repeats;
819
+ repeats = 0;
820
+ lastEmit.call(target, ` \u2191 previous line repeated ${count}\xD7`);
795
821
  };
796
- for (const level of Object.keys(originals)) {
822
+ for (const level of MUTED_LEVELS) {
823
+ const wrapper = (...args) => {
824
+ mutedCount += 1;
825
+ muted.push(format(...args));
826
+ if (muted.length > bufferLines) muted.shift();
827
+ };
828
+ wrappers[level] = wrapper;
829
+ target[level] = wrapper;
830
+ }
831
+ for (const level of FORWARDED_LEVELS) {
797
832
  const original = originals[level];
798
833
  const wrapper = (...args) => {
799
- remember(args);
834
+ const line = format(...args);
835
+ if (line === lastLine) {
836
+ repeats += 1;
837
+ return;
838
+ }
839
+ flushRepeats();
840
+ lastLine = line;
841
+ lastEmit = original;
800
842
  original.apply(target, args);
801
843
  };
802
844
  wrappers[level] = wrapper;
803
845
  target[level] = wrapper;
804
846
  }
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
847
  return {
813
848
  enabled: true,
814
- redraw,
849
+ get mutedCount() {
850
+ return mutedCount;
851
+ },
852
+ recent: () => [...muted],
815
853
  stop: () => {
816
854
  if (stopped) return;
817
855
  stopped = true;
818
- clearIntervalFn(timer);
856
+ flushRepeats();
819
857
  for (const level of Object.keys(originals)) {
820
858
  if (target[level] === wrappers[level]) target[level] = originals[level];
821
859
  }
@@ -825,6 +863,7 @@ function startBoundedTerminalLogView(options = {}) {
825
863
 
826
864
  // src/webui-server.ts
827
865
  async function runWebUI(opts) {
866
+ const terminalLogView = startQuietSurfaceConsole();
828
867
  const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary({ policyId: "cli-webui-trusted-host-compat-v1" });
829
868
  const host = opts.host ?? process.env["WEBUI_HOST"] ?? process.env["WS_HOST"] ?? "127.0.0.1";
830
869
  const publicUrl = opts.publicUrl ?? process.env["WEBUI_PUBLIC_URL"];
@@ -1060,7 +1099,10 @@ async function runWebUI(opts) {
1060
1099
  publicUrl,
1061
1100
  projectRoot: opts.projectRoot,
1062
1101
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1063
- registryBaseDir
1102
+ registryBaseDir,
1103
+ // Lets a same-project TUI/REPL authenticate `POST /api/fleet/ping` now
1104
+ // that the API requires a token on every bind (H3).
1105
+ authToken: wsToken
1064
1106
  });
1065
1107
  }
1066
1108
  const eventUnsubscribers = [];
@@ -1096,7 +1138,8 @@ async function runWebUI(opts) {
1096
1138
  globalConfigPath: path5.join(globalRoot, "config.json"),
1097
1139
  onFleetBroadcaster: (fn) => {
1098
1140
  fleetBroadcastCli = fn;
1099
- }
1141
+ },
1142
+ ...opts.getFleetBudget ? { getFleetBudget: opts.getFleetBudget } : {}
1100
1143
  });
1101
1144
  const wsHandlerCtx = {
1102
1145
  providerStore: createProviderConfigStore(
@@ -1172,7 +1215,10 @@ async function runWebUI(opts) {
1172
1215
  ...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {}
1173
1216
  };
1174
1217
  const oldMax = opts.agent.ctx.provider.capabilities?.maxContext;
1175
- const prov = makeProviderFromConfig(activeId, { ...newCfg, type: activeId });
1218
+ const prov = makeProviderFromConfig(activeId, {
1219
+ ...newCfg,
1220
+ type: newCfg.type ?? activeId
1221
+ });
1176
1222
  if (oldMax != null && prov.capabilities) prov.capabilities.maxContext = oldMax;
1177
1223
  opts.agent.ctx.provider = prov;
1178
1224
  console.log(`[WebUI] Provider credentials reloaded from config.json (${activeId})`);
@@ -1352,7 +1398,6 @@ async function runWebUI(opts) {
1352
1398
  kanbanHostRoutes,
1353
1399
  statusTracker: opts.statusTracker
1354
1400
  });
1355
- const terminalLogView = startBoundedTerminalLogView();
1356
1401
  const stopped = new Promise((resolve) => {
1357
1402
  let listeningAnnounced = false;
1358
1403
  const announceListening = () => {
@@ -1431,6 +1476,28 @@ async function runWebUI(opts) {
1431
1476
  flushAllStreamBuffers();
1432
1477
  for (const unsub of eventUnsubscribers) unsub();
1433
1478
  },
1479
+ // Issue #322: kill session-owned children (MCP, bash/exec, tool-spawned
1480
+ // dev servers) before closing the listen socket so Windows process trees
1481
+ // do not outlive the parent and strand the event loop on open stdio.
1482
+ stopOwnedChildren: async () => {
1483
+ try {
1484
+ const { getProcessRegistry } = await import("@wrongstack/tools");
1485
+ getProcessRegistry().killAll({ force: true, includeProtected: true });
1486
+ } catch (err) {
1487
+ console.debug(
1488
+ `[webui-server] process-registry killAll failed: ${err instanceof Error ? err.message : String(err)}`
1489
+ );
1490
+ }
1491
+ if (opts.mcpRegistry) {
1492
+ try {
1493
+ await opts.mcpRegistry.stopAll();
1494
+ } catch (err) {
1495
+ console.debug(
1496
+ `[webui-server] mcpRegistry.stopAll failed: ${err instanceof Error ? err.message : String(err)}`
1497
+ );
1498
+ }
1499
+ }
1500
+ },
1434
1501
  disposeResources: () => {
1435
1502
  credentialWatcherClose?.();
1436
1503
  credentialWatcherClose = void 0;
@@ -1455,8 +1522,13 @@ async function runWebUI(opts) {
1455
1522
  pid: process.pid,
1456
1523
  registryBaseDir,
1457
1524
  onStopped: () => {
1458
- terminalLogView.redraw();
1525
+ const muted = terminalLogView.mutedCount;
1459
1526
  terminalLogView.stop();
1527
+ if (muted > 0) {
1528
+ console.log(
1529
+ `[WebUI] ${muted} progress line(s) kept out of this terminal \u2014 set WEBUI_VERBOSE=1 to see them.`
1530
+ );
1531
+ }
1460
1532
  opts.onExit?.();
1461
1533
  resolve();
1462
1534
  }
@@ -1464,20 +1536,11 @@ async function runWebUI(opts) {
1464
1536
  registerWebuiSignalHandlers(signalShutdown);
1465
1537
  });
1466
1538
  function send(ws, msg) {
1467
- if (ws.readyState === WebSocket.OPEN) {
1468
- ws.send(JSON.stringify(msg));
1469
- }
1539
+ sendSerialized(ws, JSON.stringify(msg));
1470
1540
  }
1471
1541
  function broadcast(msg) {
1472
1542
  const data = JSON.stringify(msg);
1473
- for (const [ws] of clients) {
1474
- if (ws.readyState === WebSocket.OPEN) {
1475
- try {
1476
- ws.send(data);
1477
- } catch {
1478
- }
1479
- }
1480
- }
1543
+ for (const [ws] of clients) sendSerialized(ws, data);
1481
1544
  }
1482
1545
  function sendResult(ws, success, message) {
1483
1546
  send(ws, { type: "key.operation_result", payload: { success, message } });
@@ -1487,4 +1550,4 @@ async function runWebUI(opts) {
1487
1550
  export {
1488
1551
  runWebUI
1489
1552
  };
1490
- //# sourceMappingURL=webui-server-PCLSXX6L.js.map
1553
+ //# sourceMappingURL=webui-server-DD35QFNO.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. */
@@ -1,7 +1,7 @@
1
1
  import { BrainDecisionQueue, type BrainEscalationMode, ObservableBrainArbiter } from '@wrongstack/core/coordination';
2
2
  import { type BrainAutoRisk, type BrainRuntime } from '@wrongstack/core/execution';
3
3
  import type { ToolRegistry } from '@wrongstack/core/registry';
4
- import type { Config, Provider, SecretVault, SessionWriter } from '@wrongstack/core/types';
4
+ import { type Config, type Provider, type SecretVault, type SessionWriter } from '@wrongstack/core/types';
5
5
  import { MultiAgentHost } from '../multi-agent.js';
6
6
  type AnyObj = any;
7
7
  /**
@@ -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[];
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Factory for the fallback gate function.
3
+ *
4
+ * The gate is injected into `createFallbackModelExtension` as `fallbackGate`.
5
+ * When the fallback chain is about to engage, the gate:
6
+ *
7
+ * 1. Registers a `provider.fallback_choice` listener keyed on `requestId`.
8
+ * 2. Emits `provider.fallback_pending` on the EventBus — carrying the
9
+ * candidate list, countdown duration, and the caller-supplied
10
+ * `requestId` (generated by the fallback-model extension so the
11
+ * `provider.fallback` completion event can share the same id).
12
+ * 3. Waits for the choice event (the user manually picked a model), OR for
13
+ * the countdown timer to expire (auto-switch to the chain head).
14
+ * 4. Resolves with the chosen model reference, or `null` for auto-switch.
15
+ *
16
+ * Registration happens BEFORE the emit so a same-tick choice reply from a
17
+ * synchronous EventBus listener (test harness, automated UI pick) is not
18
+ * dropped.
19
+ *
20
+ * An absolute deadline (autoSwitchSeconds * 1.5) ensures the gate never
21
+ * hangs indefinitely even if neither signal arrives (headless mode, no UI
22
+ * listener, etc.).
23
+ */
24
+ import type { FallbackGateFn } from '@wrongstack/core/agent';
25
+ import type { EventBus } from '@wrongstack/core/kernel';
26
+ /**
27
+ * Create a fallback gate that emits `provider.fallback_pending` and waits
28
+ * for a `provider.fallback_choice` event or the countdown timer.
29
+ */
30
+ export declare function createFallbackGate(_events: EventBus): FallbackGateFn;
31
+ //# sourceMappingURL=fallback-gate.d.ts.map
@@ -3,7 +3,7 @@ import type { EventBus } from '@wrongstack/core/kernel';
3
3
  import type { MultiAgentHost } from '../multi-agent.js';
4
4
  import type { BuiltinSlashCommandDeps } from './slash-commands.js';
5
5
  type SlashCommandDeps = BuiltinSlashCommandDeps;
6
- export type FleetCommandHandlers = Pick<SlashCommandDeps, 'onSpawn' | 'onSpawnAndWait' | 'onAgents' | 'onFleet' | 'onFleetStatus' | 'onFleetUsage' | 'onFleetKill' | 'onFleetTerminate' | 'onFleetSpawn' | 'onFleetLog' | 'onFleetRetry' | 'onDirector'>;
6
+ export type FleetCommandHandlers = Pick<SlashCommandDeps, 'onSpawn' | 'onSpawnAndWait' | 'onAgents' | 'onFleet' | 'onFleetStatus' | 'onFleetBudget' | 'onFleetUsage' | 'onFleetKill' | 'onFleetTerminate' | 'onFleetSpawn' | 'onFleetLog' | 'onFleetRetry' | 'onDirector'>;
7
7
  export interface FleetCommandHandlersInput {
8
8
  multiAgentHost: MultiAgentHost;
9
9
  getDirector: () => Director | null;
@@ -33,7 +33,9 @@ export declare const MAILBOX_BRIDGE_META_KEY = "mailboxBridge";
33
33
  */
34
34
  export declare function bootstrapMailboxBridgeAtStartup(params: {
35
35
  projectRoot: string | undefined;
36
- config: Pick<Config, 'features'> | undefined;
36
+ config: {
37
+ features: Pick<Config['features'], 'mailboxBridge'>;
38
+ } | undefined;
37
39
  logger: Logger;
38
40
  /** Surface label — logged for debugging when something goes wrong. */
39
41
  source: 'cli' | 'webui' | 'eternal';
@@ -1,4 +1,5 @@
1
1
  import type { Config } from '@wrongstack/core/types';
2
+ import { type PluginManagerHookRunner } from '@wrongstack/core/tools';
2
3
  import type { ToolRegistry } from '@wrongstack/core/registry';
3
4
  type ConfigStoreLike = {
4
5
  get(): Config;
@@ -9,7 +10,14 @@ export interface RegisterCliManagementToolsDeps {
9
10
  configStore: ConfigStoreLike;
10
11
  profileConfigPath: string;
11
12
  stdinInteractive: boolean;
13
+ /**
14
+ * Late-bound PreToolUse pipeline for plugin_manager's nested `use` path.
15
+ * The hook runner is created by `setupLifecycleAndPlugins` AFTER the
16
+ * management tools register, so cli-main passes a ref-backed getter that
17
+ * starts null and is filled once wiring completes.
18
+ */
19
+ getHookRunner?: (() => PluginManagerHookRunner | null) | undefined;
12
20
  }
13
- export declare function registerCliManagementTools({ toolRegistry, configStore, profileConfigPath, stdinInteractive, }: RegisterCliManagementToolsDeps): void;
21
+ export declare function registerCliManagementTools({ toolRegistry, configStore, profileConfigPath, stdinInteractive, getHookRunner, }: RegisterCliManagementToolsDeps): void;
14
22
  export {};
15
23
  //# sourceMappingURL=management-tools.d.ts.map
@@ -1,9 +1,9 @@
1
1
  import { Agent, type AgentPipelines, type Context } from '@wrongstack/core/agent';
2
2
  import { AutoCompactionMiddleware } from '@wrongstack/core/execution';
3
- import type { Config, Logger, ModelsRegistry, Provider } from '@wrongstack/core/types';
4
- import type { SessionEventBridge } from '@wrongstack/core/storage';
5
3
  import { type EventBus } from '@wrongstack/core/kernel';
6
4
  import type { ProviderRegistry, ToolRegistry } from '@wrongstack/core/registry';
5
+ import type { SessionEventBridge } from '@wrongstack/core/storage';
6
+ import type { Config, Logger, ModelsRegistry, Provider } from '@wrongstack/core/types';
7
7
  type CompactionDriver = ConstructorParameters<typeof AutoCompactionMiddleware>[0];
8
8
  export declare function setupPipelines(params: {
9
9
  events: EventBus;
@@ -90,9 +90,9 @@ export declare function createAgent(params: {
90
90
  /**
91
91
  * Full Config object, used for `features.mailboxBridge` gating and
92
92
  * forwarded to `bootstrapMailboxBridgeAtStartup`. Optional — when
93
- * omitted, the bootstrap defaults to 'auto' (i.e. always run).
94
- * Tests can pass a stub with just `{ features: { mailboxBridge: 'off' } }`
95
- * to skip the bridge entirely without touching the host config.
93
+ * omitted, the bootstrap defaults to 'off' (i.e. never run), so tests
94
+ * and embedders get no detached bridge process unless they ask for one
95
+ * with `{ features: { mailboxBridge: 'auto' } }`.
96
96
  */
97
97
  fullConfig?: Pick<Config, 'features'> | undefined;
98
98
  /** Surface label for the bootstrap log breadcrumb. */
@@ -1,10 +1,9 @@
1
1
  import type { AgentPipelines } from '@wrongstack/core/agent';
2
- import type { Config, HealthRegistry, Logger, MetricsRuntimeStatus, MetricsSinkView, ModelsRegistry, Plugin, PromptLoader, SkillLoader } from '@wrongstack/core/types';
3
2
  import type { ExtensionRegistry } from '@wrongstack/core/extension';
4
- import type { PluginHostHandle } from '@wrongstack/core/plugin';
5
- import type { ConfigStore, SessionWriter } from '@wrongstack/core/types';
6
3
  import type { Container, EventBus } from '@wrongstack/core/kernel';
4
+ import type { PluginAPIInit, PluginHostHandle } from '@wrongstack/core/plugin';
7
5
  import type { ProviderRegistry, SlashCommandRegistry, ToolRegistry } from '@wrongstack/core/registry';
6
+ import type { Config, ConfigStore, HealthRegistry, Logger, MetricsRuntimeStatus, MetricsSinkView, ModelsRegistry, Plugin, PromptLoader, SessionWriter, SkillLoader } from '@wrongstack/core/types';
8
7
  import type { MCPRegistry } from '@wrongstack/mcp';
9
8
  export declare const DEPRECATED_PLUGIN_NAMES: Record<string, string>;
10
9
  export declare const BUILTIN_PLUGIN_CONFIG_NAMES: Set<string>;
@@ -71,13 +70,7 @@ export interface PluginsWiringDeps {
71
70
  * default model, and a factory for named-provider overrides. Optional —
72
71
  * minimal hosts omit it and plugins see `api.llm === undefined`.
73
72
  */
74
- llm?: {
75
- provider: import('@wrongstack/core/types').Provider;
76
- model: string;
77
- getProvider?: (() => import('@wrongstack/core/types').Provider) | undefined;
78
- getModel?: (() => string) | undefined;
79
- createProvider?: ((name: string, model?: string) => import('@wrongstack/core/types').Provider) | undefined;
80
- } | undefined;
73
+ llm?: PluginAPIInit['llm'];
81
74
  /** Health registry — injected so the observability built-in can run /health. */
82
75
  healthRegistry?: HealthRegistry | undefined;
83
76
  /** Skill loader — injected so the skills built-in can list/read skills. */