@wrongstack/cli 0.306.3 → 0.306.4

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,6 +1,6 @@
1
1
  import {
2
2
  loadManifest
3
- } from "./chunk-JUHRI6OV.js";
3
+ } from "./chunk-G7337TTK.js";
4
4
 
5
5
  // src/project-picker.ts
6
6
  import { color } from "@wrongstack/core/utils";
@@ -415,4 +415,4 @@ export {
415
415
  skipDivider,
416
416
  runProjectPicker
417
417
  };
418
- //# sourceMappingURL=chunk-5N34EWJR.js.map
418
+ //# sourceMappingURL=chunk-2ZNBRZ4W.js.map
@@ -15,12 +15,31 @@ function projectsDataDir(globalConfigPath) {
15
15
  }
16
16
  async function loadManifest(globalConfigPath) {
17
17
  const file = projectsJsonPath(globalConfigPath);
18
+ let raw;
19
+ try {
20
+ raw = await fs.readFile(file, "utf8");
21
+ } catch (error) {
22
+ if (error.code === "ENOENT") return { projects: [] };
23
+ throw new ConfigError({
24
+ message: `Unable to read projects manifest: ${file}`,
25
+ code: "CONFIG_INVALID",
26
+ context: { file, phase: "manifest-read" },
27
+ cause: error
28
+ });
29
+ }
18
30
  try {
19
- const raw = await fs.readFile(file, "utf8");
20
31
  const parsed = JSON.parse(raw);
21
- return { projects: parsed.projects ?? [] };
22
- } catch {
23
- return { projects: [] };
32
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.projects)) {
33
+ throw new Error("projects must be an array");
34
+ }
35
+ return { projects: parsed.projects };
36
+ } catch (error) {
37
+ throw new ConfigError({
38
+ message: `Unable to parse projects manifest: ${file}`,
39
+ code: "CONFIG_PARSE_FAILED",
40
+ context: { file, phase: "manifest-parse" },
41
+ cause: error
42
+ });
24
43
  }
25
44
  }
26
45
  async function saveManifest(manifest, globalConfigPath) {
@@ -97,4 +116,4 @@ export {
97
116
  ensureProjectDataDir,
98
117
  touchProjectInManifest
99
118
  };
100
- //# sourceMappingURL=chunk-JUHRI6OV.js.map
119
+ //# sourceMappingURL=chunk-G7337TTK.js.map
@@ -3,14 +3,14 @@ import {
3
3
  } from "./chunk-KV6DNQT6.js";
4
4
  import {
5
5
  runProjectPicker
6
- } from "./chunk-5N34EWJR.js";
6
+ } from "./chunk-2ZNBRZ4W.js";
7
7
  import {
8
8
  ensureProjectDataDir,
9
9
  findProject,
10
10
  generateSlug,
11
11
  loadManifest,
12
12
  saveManifest
13
- } from "./chunk-JUHRI6OV.js";
13
+ } from "./chunk-G7337TTK.js";
14
14
  import {
15
15
  createHqCommandDispatcher
16
16
  } from "./chunk-7YLN7YUA.js";
@@ -2977,7 +2977,11 @@ function buildAcpSubagentRunner(subagentId) {
2977
2977
  // src/fleet/host-helpers.ts
2978
2978
  import * as path2 from "node:path";
2979
2979
  import { makeSubagentResultTool } from "@wrongstack/core/coordination";
2980
- import { ToolCapabilities, WIDE_SUBAGENT_CAPABILITIES } from "@wrongstack/core/security";
2980
+ import {
2981
+ ToolCapabilities,
2982
+ WIDE_SUBAGENT_CAPABILITIES,
2983
+ clampSubagentCapabilities
2984
+ } from "@wrongstack/core/security";
2981
2985
  import { makePreferSideConflictResolver } from "@wrongstack/sdd";
2982
2986
  function isInsideDirectory(root, candidate) {
2983
2987
  const relative5 = path2.relative(path2.resolve(root), path2.resolve(candidate));
@@ -3085,9 +3089,10 @@ function selectSubagentTools(allTools, directorToolsByName, allow) {
3085
3089
  }
3086
3090
  function resolveSubagentCapabilities(subCfg, toolsForAllow) {
3087
3091
  if (subCfg.allowedCapabilities) {
3088
- return Array.from(
3089
- /* @__PURE__ */ new Set([...subCfg.allowedCapabilities, ToolCapabilities.COORDINATION_RESULT_SUBMIT])
3090
- );
3092
+ return clampSubagentCapabilities([
3093
+ ...subCfg.allowedCapabilities,
3094
+ ToolCapabilities.COORDINATION_RESULT_SUBMIT
3095
+ ]).granted;
3091
3096
  }
3092
3097
  const allow = subCfg.tools;
3093
3098
  if (!allow || allow.length === 0) return WIDE_SUBAGENT_CAPABILITIES;
@@ -3095,7 +3100,7 @@ function resolveSubagentCapabilities(subCfg, toolsForAllow) {
3095
3100
  for (const tool of toolsForAllow([...allow])) {
3096
3101
  for (const capability of tool.capabilities ?? []) caps.add(capability);
3097
3102
  }
3098
- return [...caps];
3103
+ return clampSubagentCapabilities([...caps]).granted;
3099
3104
  }
3100
3105
  function resolveFleetWorktreePolicy(config) {
3101
3106
  const env = process.env["WRONGSTACK_FLEET_WORKTREES"]?.trim().toLowerCase();
@@ -4589,9 +4594,9 @@ ${audienceMemory.map((text) => `- ${text}`).join("\n")}`
4589
4594
  // so the waiting-room transition never fires along the subagent
4590
4595
  // dispatch path. The tracker is the same singleton the leader uses,
4591
4596
  // populated by brain-and-orchestration.ts into host.opts.statusTracker.
4592
- ...host.opts.statusTracker ? { statusTracker: host.opts.statusTracker } : {},
4593
- ...mergedConfig.fallbackStickiness?.primaryProbeInterval !== void 0 ? { primaryCooldownMs: mergedConfig.fallbackStickiness.primaryProbeInterval } : {},
4594
- ...mergedConfig.fallbackStickiness?.stickyFallbackTurns !== void 0 ? { stickyFallbackTurns: mergedConfig.fallbackStickiness.stickyFallbackTurns } : {}
4597
+ ...host.opts.statusTracker ? { statusTracker: host.opts.statusTracker } : {}
4598
+ // `fallbackStickiness` is read live from `getConfig()` inside the
4599
+ // extension forwarding it here would pin the spawn-time value.
4595
4600
  })
4596
4601
  );
4597
4602
  const disposeBridge = installSubagentEventBridge({
@@ -8516,6 +8521,7 @@ function runGate(events, gateParams) {
8516
8521
  }
8517
8522
  };
8518
8523
  events.on("provider.fallback_choice", handler);
8524
+ timer = setTimeout(() => finish(null), Math.round(autoSwitchSeconds * 1e3));
8519
8525
  deadline = setTimeout(() => finish(null), Math.round(autoSwitchSeconds * 1500));
8520
8526
  events.emit("provider.fallback_pending", {
8521
8527
  ...sessionId ? { sessionId } : {},
@@ -8561,6 +8567,11 @@ function setupProviderRuntime(deps) {
8561
8567
  fallbackProfileManager.reload(next);
8562
8568
  onConfigUpdate(next);
8563
8569
  };
8570
+ teardownHandlers.push(
8571
+ configStore.watch((next) => {
8572
+ fallbackProfileManager.reload(next);
8573
+ })
8574
+ );
8564
8575
  const resolveProviderCfg2 = (providerId) => resolveProviderCfgRuntime(cfg, providerId);
8565
8576
  const buildProviderForId2 = (providerId) => buildProviderForIdRuntime({ config: cfg, providerRegistry }, providerId);
8566
8577
  const buildProviderForModel = async (providerId, modelId) => {
@@ -8599,9 +8610,10 @@ function setupProviderRuntime(deps) {
8599
8610
  // emits provider.fallback_pending and waits for provider.fallback_choice
8600
8611
  // or the auto-switch countdown.
8601
8612
  fallbackGate: createFallbackGate(events),
8602
- fallbackGateSeconds: 7,
8603
- ...cfg.fallbackStickiness?.primaryProbeInterval !== void 0 ? { primaryCooldownMs: cfg.fallbackStickiness.primaryProbeInterval } : {},
8604
- ...cfg.fallbackStickiness?.stickyFallbackTurns !== void 0 ? { stickyFallbackTurns: cfg.fallbackStickiness.stickyFallbackTurns } : {}
8613
+ fallbackGateSeconds: 7
8614
+ // `fallbackStickiness` is NOT forwarded here on purpose: the extension
8615
+ // reads it from `getConfig()` on every use, so an edit takes effect
8616
+ // without a restart. Passing it as a dep would pin the boot-time value.
8605
8617
  })
8606
8618
  );
8607
8619
  if (cfg.features.memory && cfg.features.memoryConsolidation !== false) {
@@ -8637,26 +8649,35 @@ function setupProviderRuntime(deps) {
8637
8649
  "fallbackModels",
8638
8650
  "fallbackBridge",
8639
8651
  "fallbackProfiles",
8652
+ "fallbackProfile",
8640
8653
  "favoriteModels",
8641
8654
  "favoriteModelsOnly",
8642
8655
  "modelAvailabilitySchedule",
8643
8656
  "modelMatrix",
8644
8657
  "fallbackAuto",
8658
+ "fallbackStickiness",
8659
+ "fallbackMaxLastResortCandidates",
8645
8660
  "uiLocale"
8646
8661
  ];
8662
+ const activeProfileOf = (c) => typeof c.activeProfile === "string" && c.activeProfile ? c.activeProfile : "default";
8647
8663
  if (process.env["WRONGSTACK_DISABLE_CONFIG_WATCH"] !== "1") {
8648
- const activeProfile = typeof cfg.activeProfile === "string" && cfg.activeProfile ? cfg.activeProfile : "default";
8649
- const configLayers = [
8650
- { path: wpaths.profileConfig(activeProfile), priority: 1, trusted: true },
8651
- { path: wpaths.projectLocalConfig, priority: 2, trusted: true },
8652
- { path: wpaths.inProjectConfig, priority: 3, trusted: false }
8653
- ];
8664
+ let activeProfile = activeProfileOf(cfg);
8665
+ let configLayers = [];
8666
+ const rebuildLayers = () => {
8667
+ configLayers = [
8668
+ { path: wpaths.profileConfig(activeProfile), priority: 1, trusted: true },
8669
+ { path: wpaths.projectLocalConfig, priority: 2, trusted: true },
8670
+ { path: wpaths.inProjectConfig, priority: 3, trusted: false }
8671
+ ];
8672
+ };
8673
+ rebuildLayers();
8654
8674
  const sanitizeUntrustedSnapshot = (snap, path32) => {
8655
- const { providers, apiKey, baseUrl, ...rest } = snap;
8675
+ const { providers, apiKey, baseUrl, fallbackMaxLastResortCandidates, ...rest } = snap;
8656
8676
  const dropped = [
8657
8677
  ...Object.keys(providers ?? {}).length > 0 ? ["providers"] : [],
8658
8678
  ...apiKey !== void 0 ? ["apiKey"] : [],
8659
- ...baseUrl !== void 0 ? ["baseUrl"] : []
8679
+ ...baseUrl !== void 0 ? ["baseUrl"] : [],
8680
+ ...fallbackMaxLastResortCandidates !== void 0 ? ["fallbackMaxLastResortCandidates"] : []
8660
8681
  ];
8661
8682
  if (dropped.length > 0) {
8662
8683
  logger.warn(
@@ -8682,23 +8703,20 @@ function setupProviderRuntime(deps) {
8682
8703
  snapshotHasProviders: snap.snapshotHasProviders
8683
8704
  };
8684
8705
  } else {
8685
- merged = {
8706
+ const next = {
8686
8707
  providers: { ...merged.providers, ...snap.providers },
8687
- ...snap.apiKey !== void 0 ? { apiKey: snap.apiKey } : merged.apiKey !== void 0 ? { apiKey: merged.apiKey } : {},
8688
- ...snap.baseUrl !== void 0 ? { baseUrl: snap.baseUrl } : merged.baseUrl !== void 0 ? { baseUrl: merged.baseUrl } : {},
8689
- ...snap.fallbackModels !== void 0 ? { fallbackModels: snap.fallbackModels } : merged.fallbackModels !== void 0 ? { fallbackModels: merged.fallbackModels } : {},
8690
- ...snap.fallbackBridge !== void 0 ? { fallbackBridge: snap.fallbackBridge } : merged.fallbackBridge !== void 0 ? { fallbackBridge: merged.fallbackBridge } : {},
8691
- ...snap.fallbackProfiles !== void 0 ? { fallbackProfiles: snap.fallbackProfiles } : merged.fallbackProfiles !== void 0 ? { fallbackProfiles: merged.fallbackProfiles } : {},
8692
- ...snap.favoriteModels !== void 0 ? { favoriteModels: snap.favoriteModels } : merged.favoriteModels !== void 0 ? { favoriteModels: merged.favoriteModels } : {},
8693
- ...snap.favoriteModelsOnly !== void 0 ? { favoriteModelsOnly: snap.favoriteModelsOnly } : merged.favoriteModelsOnly !== void 0 ? { favoriteModelsOnly: merged.favoriteModelsOnly } : {},
8694
- ...snap.modelAvailabilitySchedule !== void 0 ? { modelAvailabilitySchedule: snap.modelAvailabilitySchedule } : merged.modelAvailabilitySchedule !== void 0 ? { modelAvailabilitySchedule: merged.modelAvailabilitySchedule } : {},
8695
- ...snap.modelMatrix !== void 0 ? { modelMatrix: snap.modelMatrix } : merged.modelMatrix !== void 0 ? { modelMatrix: merged.modelMatrix } : {},
8696
- ...snap.fallbackAuto !== void 0 ? { fallbackAuto: snap.fallbackAuto } : merged.fallbackAuto !== void 0 ? { fallbackAuto: merged.fallbackAuto } : {},
8697
- ...snap.uiLocale !== void 0 ? { uiLocale: snap.uiLocale } : merged.uiLocale !== void 0 ? { uiLocale: merged.uiLocale } : {},
8698
8708
  // Carry forward the snapshotHasProviders flag from the higher-priority layer.
8699
8709
  // When a layer has providers, merged knows providers were found somewhere.
8700
8710
  snapshotHasProviders: snap.snapshotHasProviders || merged.snapshotHasProviders
8701
8711
  };
8712
+ const snapRec = snap;
8713
+ const mergedRec = merged;
8714
+ const nextRec = next;
8715
+ for (const key of ["apiKey", "baseUrl", ...ROUTING_FIELDS]) {
8716
+ const winner = snapRec[key] ?? mergedRec[key];
8717
+ if (winner !== void 0) nextRec[key] = winner;
8718
+ }
8719
+ merged = next;
8702
8720
  }
8703
8721
  }
8704
8722
  return merged;
@@ -8717,7 +8735,7 @@ function setupProviderRuntime(deps) {
8717
8735
  if (merged.apiKey !== void 0) mergedPatch.apiKey = merged.apiKey;
8718
8736
  if (merged.baseUrl !== void 0) mergedPatch.baseUrl = merged.baseUrl;
8719
8737
  for (const key of ROUTING_FIELDS) {
8720
- mergedPatch[key] = merged[key] ?? null;
8738
+ mergedPatch[key] = merged[key];
8721
8739
  }
8722
8740
  const before = JSON.stringify(resolveProviderCfg2(activeId).cfg);
8723
8741
  sync(patchConfig(cfg, mergedPatch));
@@ -8733,20 +8751,37 @@ function setupProviderRuntime(deps) {
8733
8751
  );
8734
8752
  }
8735
8753
  };
8736
- const watchers = configLayers.map((layer) => {
8737
- const w = watchProviderConfig(
8738
- layer.path,
8739
- vault,
8740
- () => {
8741
- void onAnyConfigChange();
8742
- },
8743
- { warn: (msg) => logger.warn(`Config watcher (${layer.path}): ${msg}`) }
8754
+ let watchers = [];
8755
+ const openWatchers = () => {
8756
+ watchers = configLayers.map(
8757
+ (layer) => watchProviderConfig(
8758
+ layer.path,
8759
+ vault,
8760
+ () => {
8761
+ void onAnyConfigChange();
8762
+ },
8763
+ { warn: (msg) => logger.warn(`Config watcher (${layer.path}): ${msg}`) }
8764
+ )
8744
8765
  );
8745
- return w;
8746
- });
8747
- teardownHandlers.push(() => {
8766
+ };
8767
+ const closeWatchers = () => {
8748
8768
  for (const w of watchers) w.close();
8749
- });
8769
+ watchers = [];
8770
+ };
8771
+ openWatchers();
8772
+ teardownHandlers.push(
8773
+ configStore.watch((next) => {
8774
+ const nextProfile = activeProfileOf(next);
8775
+ if (nextProfile === activeProfile) return;
8776
+ activeProfile = nextProfile;
8777
+ closeWatchers();
8778
+ rebuildLayers();
8779
+ openWatchers();
8780
+ previousSnapshotSerialized = void 0;
8781
+ void onAnyConfigChange();
8782
+ })
8783
+ );
8784
+ teardownHandlers.push(closeWatchers);
8750
8785
  }
8751
8786
  return {
8752
8787
  resolveProviderCfg: resolveProviderCfg2,
@@ -14728,6 +14763,7 @@ import * as fs9 from "node:fs/promises";
14728
14763
  import {
14729
14764
  normalizeModelRef,
14730
14765
  parseModelRef as parseModelRef2,
14766
+ runtimeFallbackChain,
14731
14767
  smartDefaultFallbackChain
14732
14768
  } from "@wrongstack/core/agent";
14733
14769
  import { decryptConfigSecrets, encryptConfigSecrets, noOpVault as noOpVault2 } from "@wrongstack/core/security";
@@ -14736,11 +14772,6 @@ import { atomicWrite as atomicWrite5, color as color24, toErrorMessage as toErro
14736
14772
  function refInvalidReason(ref, config) {
14737
14773
  const parsed = parseModelRef2(ref);
14738
14774
  if (!parsed.model) return "no model in reference";
14739
- const providerId = parsed.provider ?? config.provider;
14740
- const entry = config.providers?.[providerId];
14741
- if (entry?.models && !entry.models.includes(parsed.model)) {
14742
- return `"${parsed.model}" not in ${providerId} model list`;
14743
- }
14744
14775
  const favorites = config.favoriteModels ?? [];
14745
14776
  if (favorites.length === 0) return void 0;
14746
14777
  const canonical = normalizeModelRef(ref, config.provider);
@@ -14748,13 +14779,13 @@ function refInvalidReason(ref, config) {
14748
14779
  if (!inFavorites) return "not in favorites";
14749
14780
  return void 0;
14750
14781
  }
14751
- function refInactiveReason(ref, config) {
14782
+ function refStaleModelListWarning(ref, config) {
14752
14783
  const parsed = parseModelRef2(ref);
14753
14784
  if (!parsed.model) return "no model in reference";
14754
14785
  const providerId = parsed.provider ?? config.provider;
14755
14786
  const entry = config.providers?.[providerId];
14756
- if (entry?.models && !entry.models.includes(parsed.model)) {
14757
- return `"${parsed.model}" not in ${providerId} model list`;
14787
+ if (Array.isArray(entry?.models) && !entry.models.includes(parsed.model)) {
14788
+ return `not in ${providerId} saved model list \u2014 will still be tried`;
14758
14789
  }
14759
14790
  return void 0;
14760
14791
  }
@@ -14804,7 +14835,8 @@ function buildFallbackCommand(opts) {
14804
14835
  " /fallback bridge clear Disable the continuity route",
14805
14836
  " /fallback auto on|off Toggle the auto-derived smart default",
14806
14837
  " /fallback profile set <name> <ref,ref,...> Create or replace a named chain",
14807
- " /fallback profile use <name> Make a profile the leader fallback chain",
14838
+ " /fallback profile use <name> Select a profile for failover",
14839
+ " /fallback profile none Deselect (explicit chain / smart default apply)",
14808
14840
  " /fallback profile remove <name> Delete a named chain",
14809
14841
  " /fallback fav add <provider/model> Add a favorite model",
14810
14842
  " /fallback fav remove <n|ref> Remove a favorite model",
@@ -14825,12 +14857,14 @@ function buildFallbackCommand(opts) {
14825
14857
  const rawCap = config.fallbackMaxLastResortCandidates;
14826
14858
  const capValue = typeof rawCap === "number" && Number.isFinite(rawCap) && rawCap >= 0 ? Math.floor(rawCap) : 12;
14827
14859
  const capLabel = capValue === 0 ? color24.dim("disabled") : color24.green(String(capValue));
14828
- const filteredReason = (ref) => refInactiveReason(ref, config);
14860
+ const filteredReason = (ref) => refStaleModelListWarning(ref, config);
14861
+ const activeProfile = typeof config.fallbackProfile === "string" && profiles[config.fallbackProfile] ? config.fallbackProfile : void 0;
14829
14862
  const lines = [
14830
14863
  `${color24.bold("WrongStack")} ${color24.dim("\u2014 Fallback chain")}`,
14831
14864
  "",
14832
14865
  ` ${color24.bold("leader")} ${color24.cyan(`${config.provider}/${config.model}`)}`,
14833
14866
  ` ${color24.bold("bridge")} ${bridge ? color24.cyan(bridge) : color24.dim("(disabled)")} ${color24.dim("/fallback bridge set <provider/model>")}`,
14867
+ ` ${color24.bold("profile")} ${activeProfile ? color24.amber(activeProfile) : color24.dim("(none)")} ${color24.dim("/fallback profile use <name> | none")}`,
14834
14868
  ""
14835
14869
  ];
14836
14870
  if (explicit.length > 0) {
@@ -14838,8 +14872,8 @@ function buildFallbackCommand(opts) {
14838
14872
  ` ${color24.bold("explicit chain")} ${color24.dim("(tried in order after the leader)")}`
14839
14873
  );
14840
14874
  explicit.forEach((ref, i) => {
14841
- const inactive = filteredReason(ref);
14842
- const suffix = inactive ? ` ${color24.red(`\u26A0 inactive \u2014 ${inactive}`)}` : "";
14875
+ const note = filteredReason(ref);
14876
+ const suffix = note ? ` ${color24.amber(`\u26A0 ${note}`)}` : "";
14843
14877
  lines.push(` ${color24.amber(String(i + 1).padStart(2))}. ${color24.cyan(ref)}${suffix}`);
14844
14878
  });
14845
14879
  } else {
@@ -14858,6 +14892,17 @@ function buildFallbackCommand(opts) {
14858
14892
  }
14859
14893
  }
14860
14894
  }
14895
+ const runtime = runtimeFallbackChain(config);
14896
+ lines.push("", ` ${color24.bold("effective order")} ${color24.dim("(what will actually be tried)")}`);
14897
+ if (runtime.length === 0) {
14898
+ lines.push(
14899
+ ` ${color24.red("empty")} ${color24.dim("\u2014 a failure on the leader has nowhere to go")}`
14900
+ );
14901
+ } else {
14902
+ runtime.forEach((ref, i) => {
14903
+ lines.push(` ${color24.amber(String(i + 1).padStart(2))}. ${color24.cyan(ref)}`);
14904
+ });
14905
+ }
14861
14906
  lines.push(
14862
14907
  "",
14863
14908
  ` ${color24.bold("auto")} ${auto ? color24.green("on") : color24.dim("off")} ${color24.dim("/fallback auto on|off")}`,
@@ -14872,8 +14917,8 @@ function buildFallbackCommand(opts) {
14872
14917
  return [
14873
14918
  ` ${color24.amber(name)} \u2192`,
14874
14919
  ...chain.map((ref) => {
14875
- const inactive = filteredReason(ref);
14876
- const suffix = inactive ? ` ${color24.red(`\u26A0 inactive \u2014 ${inactive}`)}` : "";
14920
+ const note = filteredReason(ref);
14921
+ const suffix = note ? ` ${color24.amber(`\u26A0 ${note}`)}` : "";
14877
14922
  return ` ${color24.cyan(ref)}${suffix}`;
14878
14923
  })
14879
14924
  ];
@@ -14881,8 +14926,8 @@ function buildFallbackCommand(opts) {
14881
14926
  "",
14882
14927
  ` ${color24.bold("favorites")} ${favorites.length ? "" : color24.dim("(none)")}`,
14883
14928
  ...favorites.map((ref, i) => {
14884
- const inactive = filteredReason(ref);
14885
- const suffix = inactive ? ` ${color24.red(`\u26A0 inactive \u2014 ${inactive}`)}` : "";
14929
+ const note = filteredReason(ref);
14930
+ const suffix = note ? ` ${color24.amber(`\u26A0 ${note}`)}` : "";
14886
14931
  return ` ${color24.amber(String(i + 1).padStart(2))}. ${color24.cyan(ref)}${suffix}`;
14887
14932
  }),
14888
14933
  "",
@@ -14952,18 +14997,18 @@ function buildFallbackCommand(opts) {
14952
14997
  const invalid = refInvalidReason(ref, config);
14953
14998
  if (invalid) {
14954
14999
  return {
14955
- message: `${color24.red("Cannot add")}: "${ref}" \u2014 ${invalid}. ` + (invalid === "not in favorites" ? `${color24.dim("Add it first: /fallback fav add " + ref)}` : invalid.startsWith('"') && invalid.includes("not in") ? color24.dim(
14956
- "Update the provider models list via provider_manage or pick a different model"
14957
- ) : "")
15000
+ message: `${color24.red("Cannot add")}: "${ref}" \u2014 ${invalid}. ` + (invalid === "not in favorites" ? `${color24.dim("Add it first: /fallback fav add " + ref)}` : "")
14958
15001
  };
14959
15002
  }
15003
+ const stale = refStaleModelListWarning(ref, config);
14960
15004
  explicit.push(ref);
14961
15005
  const decrypted = await patchGlobalConfig(globalConfigPath, (cfg) => {
14962
15006
  cfg.fallbackModels = explicit;
14963
15007
  });
14964
15008
  opts.configStore.update({ fallbackModels: decrypted.fallbackModels });
14965
15009
  return {
14966
- message: `${color24.green("\u2713")} added ${color24.cyan(ref)} ${color24.dim(`(chain length ${explicit.length})`)}`
15010
+ message: `${color24.green("\u2713")} added ${color24.cyan(ref)} ${color24.dim(`(chain length ${explicit.length})`)}` + (stale ? `
15011
+ ${color24.amber("\u26A0")} ${color24.dim(stale)}` : "")
14967
15012
  };
14968
15013
  }
14969
15014
  if (sub === "remove") {
@@ -15022,12 +15067,21 @@ function buildFallbackCommand(opts) {
15022
15067
  if (sub === "profile") {
15023
15068
  const action = (parts[1] ?? "").toLowerCase();
15024
15069
  const name = parts[2];
15025
- if (!["set", "use", "remove", "list"].includes(action)) {
15070
+ if (!["set", "use", "remove", "list", "none", "clear"].includes(action)) {
15026
15071
  return {
15027
- message: `${color24.amber("Usage:")} /fallback profile set <name> <ref,ref,...> | use <name> | remove <name>`
15072
+ message: `${color24.amber("Usage:")} /fallback profile set <name> <ref,ref,...> | use <name> | none | remove <name>`
15028
15073
  };
15029
15074
  }
15030
15075
  if (action === "list") return { message: currentView() };
15076
+ if (action === "none" || action === "clear") {
15077
+ await patchGlobalConfig(globalConfigPath, (cfg) => {
15078
+ delete cfg.fallbackProfile;
15079
+ });
15080
+ opts.configStore.update({ fallbackProfile: void 0 });
15081
+ return {
15082
+ message: `${color24.green("\u2713")} no fallback profile selected ${color24.dim("(explicit chain / smart default apply)")}`
15083
+ };
15084
+ }
15031
15085
  if (!name) {
15032
15086
  return { message: `${color24.amber("Usage:")} /fallback profile ${action} <name>` };
15033
15087
  }
@@ -15065,23 +15119,33 @@ ${color24.dim("Add favorites first: /fallback fav add <ref>, or fix provider mod
15065
15119
  return { message: `${color24.red("Profile not found")}: ${color24.amber(name)}` };
15066
15120
  }
15067
15121
  if (action === "use") {
15068
- const chain = profiles[name] ?? [];
15069
15122
  const decrypted = await patchGlobalConfig(globalConfigPath, (cfg) => {
15070
- cfg.fallbackModels = chain;
15123
+ cfg.fallbackProfile = name;
15124
+ });
15125
+ opts.configStore.update({
15126
+ fallbackProfile: decrypted.fallbackProfile
15071
15127
  });
15072
- opts.configStore.update({ fallbackModels: decrypted.fallbackModels });
15073
- return { message: `${color24.green("\u2713")} active chain \u2190 profile ${color24.amber(name)}` };
15128
+ const explicitActive = (config.fallbackModels ?? []).length > 0;
15129
+ return {
15130
+ message: `${color24.green("\u2713")} fallback profile \u2192 ${color24.amber(name)}` + (explicitActive ? `
15131
+ ${color24.dim("Note: your explicit chain still takes precedence. Run /fallback clear to let the profile drive failover.")}` : "")
15132
+ };
15074
15133
  }
15075
15134
  if (action === "remove") {
15135
+ const wasSelected = config.fallbackProfile === name;
15076
15136
  const decrypted = await patchGlobalConfig(globalConfigPath, (cfg) => {
15077
15137
  const next = { ...cfg.fallbackProfiles ?? {} };
15078
15138
  delete next[name];
15079
15139
  cfg.fallbackProfiles = next;
15140
+ if (cfg.fallbackProfile === name) delete cfg.fallbackProfile;
15080
15141
  });
15081
15142
  opts.configStore.update({
15082
- fallbackProfiles: decrypted.fallbackProfiles
15143
+ fallbackProfiles: decrypted.fallbackProfiles,
15144
+ ...wasSelected ? { fallbackProfile: void 0 } : {}
15083
15145
  });
15084
- return { message: `${color24.green("\u2713")} removed profile ${color24.amber(name)}` };
15146
+ return {
15147
+ message: `${color24.green("\u2713")} removed profile ${color24.amber(name)}` + (wasSelected ? color24.dim(" (it was the selected profile \u2014 selection cleared)") : "")
15148
+ };
15085
15149
  }
15086
15150
  }
15087
15151
  if (sub === "fav" || sub === "favorite" || sub === "favorites") {
@@ -32268,7 +32332,7 @@ async function runInteractive(cliCtx) {
32268
32332
  onEvent: evOn
32269
32333
  });
32270
32334
  const savedProviderCfg = config.providers?.[config.provider];
32271
- const { execute } = await import("./execution-4IG5XKOL.js");
32335
+ const { execute } = await import("./execution-547PM3NI.js");
32272
32336
  const stopHeapWatchdog = startSharedHeapWatchdog({
32273
32337
  collectStats: () => {
32274
32338
  const hqQueue = hqPublisherRef.current?.getQueueStats();
@@ -32506,4 +32570,4 @@ export {
32506
32570
  CLI_VERSION,
32507
32571
  runInteractive
32508
32572
  };
32509
- //# sourceMappingURL=cli-main-3PUWFKSL.js.map
32573
+ //# sourceMappingURL=cli-main-4TZCCZR6.js.map
@@ -870,7 +870,7 @@ function onSwitchToSession(ctx, _sessionId, targetRoot, projectName) {
870
870
  import * as path4 from "node:path";
871
871
  import { color as color3 } from "@wrongstack/core/utils";
872
872
  async function getProjectPickerItems(ctx) {
873
- const { buildPickerItems } = await import("./project-picker-BOCWHBXP.js");
873
+ const { buildPickerItems } = await import("./project-picker-R5TYAE3Q.js");
874
874
  return buildPickerItems({
875
875
  globalConfigPath: ctx.state.wpaths.globalConfig,
876
876
  currentProjectRoot: ctx.state.projectRoot
@@ -888,7 +888,7 @@ async function onProjectSelect(ctx, slug, kind) {
888
888
  }
889
889
  return;
890
890
  }
891
- const { loadManifest, saveManifest } = await import("./project-manifest-GE25PW4S.js");
891
+ const { loadManifest, saveManifest } = await import("./project-manifest-YFTJ3D5N.js");
892
892
  const manifest = await loadManifest(state.wpaths.globalConfig);
893
893
  const project = manifest.projects.find((p) => p.slug === slug);
894
894
  if (!project) return;
@@ -5998,4 +5998,4 @@ export {
5998
5998
  execute,
5999
5999
  resolveReviewerFallbackModels
6000
6000
  };
6001
- //# sourceMappingURL=execution-4IG5XKOL.js.map
6001
+ //# sourceMappingURL=execution-547PM3NI.js.map
package/dist/index.js CHANGED
@@ -729,7 +729,7 @@ var loaders = {
729
729
  skills: async () => (await import("./tools-skills-UNKLOB7M.js")).skillsCmd,
730
730
  providers: async () => (await import("./providers-models-HQN3AT3K.js")).providersCmd,
731
731
  models: async () => (await import("./providers-models-HQN3AT3K.js")).modelsCmd,
732
- mcp: async () => (await import("./mcp-JNWEQ6T2.js")).mcpCmd,
732
+ mcp: async () => (await import("./mcp-W7HHN7IA.js")).mcpCmd,
733
733
  plugin: async () => (await import("./plugin-usage-PTEETL3J.js")).pluginCmd,
734
734
  plugins: async () => (await import("./plugin-usage-PTEETL3J.js")).pluginCmd,
735
735
  diag: async () => (await import("./diag-doctor-WTBHPOMB.js")).diagCmd,
@@ -3302,7 +3302,7 @@ async function initializeCli(argv) {
3302
3302
  async function main(argv) {
3303
3303
  const cliCtx = await initializeCli(argv);
3304
3304
  if (typeof cliCtx === "number") return cliCtx;
3305
- const { runInteractive } = await import("./cli-main-3PUWFKSL.js");
3305
+ const { runInteractive } = await import("./cli-main-4TZCCZR6.js");
3306
3306
  return runInteractive(cliCtx);
3307
3307
  }
3308
3308
 
@@ -146,6 +146,9 @@ function mimeTypeFor(file) {
146
146
  function isTextMime(mimeType) {
147
147
  return mimeType.startsWith("text/") || mimeType === "application/json" || mimeType === "application/yaml";
148
148
  }
149
+ function resolveServeFsRestriction(config) {
150
+ return config.tools?.restrictToProjectRoot ?? true;
151
+ }
149
152
  function makeServeContext(cwd, projectRoot, signal, restrictFsToRoot = true) {
150
153
  const provider = {
151
154
  id: "mcp-serve",
@@ -213,7 +216,7 @@ async function serveMcpStdio(deps) {
213
216
  deps.cwd,
214
217
  deps.projectRoot,
215
218
  controller.signal,
216
- deps.config.tools?.restrictToProjectRoot ?? false
219
+ resolveServeFsRestriction(deps.config)
217
220
  );
218
221
  const permissionPolicy = yolo ? new AllowAllPermissionPolicy() : new AutoApprovePermissionPolicy();
219
222
  const executor = new ToolExecutor(registry, {
@@ -253,7 +256,7 @@ async function serveMcpStdio(deps) {
253
256
  deps.cwd,
254
257
  deps.projectRoot,
255
258
  controller.signal,
256
- deps.config.tools?.restrictToProjectRoot ?? false
259
+ resolveServeFsRestriction(deps.config)
257
260
  );
258
261
  const batch = await executor.executeBatch([use], requestCtx, "sequential");
259
262
  const result = batch.outputs[0]?.result;
@@ -429,4 +432,4 @@ function isRecord(value) {
429
432
  export {
430
433
  mcpCmd
431
434
  };
432
- //# sourceMappingURL=mcp-JNWEQ6T2.js.map
435
+ //# sourceMappingURL=mcp-W7HHN7IA.js.map
@@ -9,6 +9,25 @@ export interface SelectedMcpServeContent {
9
9
  }
10
10
  /** Load only files explicitly named by trusted CLI flags; nothing is auto-discovered. */
11
11
  export declare function loadSelectedMcpServeContent(flags: Record<string, string | boolean>, cwd: string): Promise<SelectedMcpServeContent>;
12
+ /**
13
+ * Whether the file tools are confined to the project root for `mcp serve`.
14
+ *
15
+ * `mcp serve` is the one surface where the file tools are driven by a REMOTE
16
+ * caller, so an unset config must inherit containment rather than invert it —
17
+ * an unset value here previously became `false`, which short-circuited
18
+ * `ensureInsideRoot` and let `tools/call read` return `~/.ssh/id_rsa`.
19
+ *
20
+ * This lives in one exported function rather than as a literal repeated at each
21
+ * call site so the invariant is testable and a third call site cannot quietly
22
+ * reintroduce the wrong default. Only a trusted user config can opt out:
23
+ * `tools.restrictToProjectRoot` is on the in-project denylist, so a
24
+ * checked-out repository cannot reach it.
25
+ */
26
+ export declare function resolveServeFsRestriction(config: {
27
+ tools?: {
28
+ restrictToProjectRoot?: boolean | undefined;
29
+ } | undefined;
30
+ }): boolean;
12
31
  /** Minimal run context — tools read cwd/projectRoot/signal; provider/session are stubs. */
13
32
  export declare function makeServeContext(cwd: string, projectRoot: string, signal: AbortSignal, restrictFsToRoot?: boolean): Context;
14
33
  /**
@@ -5,7 +5,7 @@ import {
5
5
  loadManifest,
6
6
  saveManifest,
7
7
  touchProjectInManifest
8
- } from "./chunk-JUHRI6OV.js";
8
+ } from "./chunk-G7337TTK.js";
9
9
  import "./chunk-7OCVIDC7.js";
10
10
  export {
11
11
  ensureProjectDataDir,
@@ -15,4 +15,4 @@ export {
15
15
  saveManifest,
16
16
  touchProjectInManifest
17
17
  };
18
- //# sourceMappingURL=project-manifest-GE25PW4S.js.map
18
+ //# sourceMappingURL=project-manifest-YFTJ3D5N.js.map
@@ -4,8 +4,8 @@ import {
4
4
  filterItems,
5
5
  runProjectPicker,
6
6
  skipDivider
7
- } from "./chunk-5N34EWJR.js";
8
- import "./chunk-JUHRI6OV.js";
7
+ } from "./chunk-2ZNBRZ4W.js";
8
+ import "./chunk-G7337TTK.js";
9
9
  import "./chunk-7OCVIDC7.js";
10
10
  export {
11
11
  buildPickerItems,
@@ -14,4 +14,4 @@ export {
14
14
  runProjectPicker,
15
15
  skipDivider
16
16
  };
17
- //# sourceMappingURL=project-picker-BOCWHBXP.js.map
17
+ //# sourceMappingURL=project-picker-R5TYAE3Q.js.map
@@ -17,6 +17,12 @@ import type { SlashCommandContext } from './command-context.js';
17
17
  * bridge set <ref> Set the single continuity route tried before the chain.
18
18
  * bridge clear Disable the continuity bridge.
19
19
  * auto on|off Toggle the smart default (config.fallbackAuto).
20
+ * profile use <name> Select a named profile (config.fallbackProfile).
21
+ * profile none Deselect it.
22
+ *
23
+ * Resolution order at runtime: explicit `fallbackModels` → selected profile →
24
+ * smart default. The view renders the real `resolveCandidates` order, not just
25
+ * the explicit list.
20
26
  */
21
27
  export declare function buildFallbackCommand(opts: SlashCommandContext): SlashCommand;
22
28
  //# sourceMappingURL=fallback.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/cli",
3
- "version": "0.306.3",
3
+ "version": "0.306.4",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack CLI — terminal AI coding agent with provider catalog from models.dev. Provides `wrongstack` and `wstack` binaries.",
6
6
  "keywords": [
@@ -42,31 +42,31 @@
42
42
  ],
43
43
  "dependencies": {
44
44
  "ws": "^8.21.1",
45
- "@wrongstack/kanban": "0.306.3",
46
- "@wrongstack/acp": "0.306.3",
47
- "@wrongstack/core": "0.306.3",
48
- "@wrongstack/bench": "0.306.3",
49
- "@wrongstack/mcp": "0.306.3",
50
- "@wrongstack/plugins": "0.306.3",
51
- "@wrongstack/providers": "0.306.3",
52
- "@wrongstack/plug-lsp": "0.306.3",
53
- "@wrongstack/requirement-intake": "0.306.3",
54
- "@wrongstack/persistence": "0.306.3",
55
- "@wrongstack/security-scanner": "0.306.3",
56
- "@wrongstack/runtime": "0.306.3",
57
- "@wrongstack/sage": "0.306.3",
58
- "@wrongstack/techstack": "0.306.3",
59
- "@wrongstack/tools": "0.306.3",
60
- "@wrongstack/simpleui": "0.306.3",
61
- "@wrongstack/sdd": "0.306.3",
62
- "@wrongstack/telegram": "0.306.3",
63
- "@wrongstack/tui": "0.306.3",
64
- "@wrongstack/webui": "0.306.3",
65
- "@wrongstack/webui-server": "0.306.3",
66
- "@wrongstack/webui-hq": "0.306.3"
45
+ "@wrongstack/acp": "0.306.4",
46
+ "@wrongstack/mcp": "0.306.4",
47
+ "@wrongstack/kanban": "0.306.4",
48
+ "@wrongstack/persistence": "0.306.4",
49
+ "@wrongstack/core": "0.306.4",
50
+ "@wrongstack/bench": "0.306.4",
51
+ "@wrongstack/plug-lsp": "0.306.4",
52
+ "@wrongstack/requirement-intake": "0.306.4",
53
+ "@wrongstack/plugins": "0.306.4",
54
+ "@wrongstack/providers": "0.306.4",
55
+ "@wrongstack/sage": "0.306.4",
56
+ "@wrongstack/sdd": "0.306.4",
57
+ "@wrongstack/security-scanner": "0.306.4",
58
+ "@wrongstack/runtime": "0.306.4",
59
+ "@wrongstack/simpleui": "0.306.4",
60
+ "@wrongstack/techstack": "0.306.4",
61
+ "@wrongstack/telegram": "0.306.4",
62
+ "@wrongstack/tui": "0.306.4",
63
+ "@wrongstack/webui": "0.306.4",
64
+ "@wrongstack/tools": "0.306.4",
65
+ "@wrongstack/webui-server": "0.306.4",
66
+ "@wrongstack/webui-hq": "0.306.4"
67
67
  },
68
68
  "optionalDependencies": {
69
- "@wrongstack/desktop": "0.306.3"
69
+ "@wrongstack/desktop": "0.306.4"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/node": "^26.1.2",