@wrongstack/core 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 (165) hide show
  1. package/dist/chronicle/index.d.ts +2 -0
  2. package/dist/chronicle/index.js +297 -10
  3. package/dist/chronicle/project-server.js +253 -15
  4. package/dist/chronicle/sqlite-compaction.d.ts +20 -0
  5. package/dist/chronicle/sqlite-journal.d.ts +57 -0
  6. package/dist/coordination/agents/capability-manifest.d.ts +7 -0
  7. package/dist/coordination/agents/index.d.ts +3 -2
  8. package/dist/coordination/agents/index.js +818 -570
  9. package/dist/coordination/agents/project-agent-identity.d.ts +6 -6
  10. package/dist/coordination/agents/role-skills.d.ts +1 -1
  11. package/dist/coordination/agents/types.d.ts +6 -0
  12. package/dist/coordination/director.d.ts +10 -2
  13. package/dist/coordination/fleet-manager.d.ts +48 -3
  14. package/dist/coordination/ifleet-manager.d.ts +2 -0
  15. package/dist/coordination/index.js +6880 -6440
  16. package/dist/coordination/mail-tools.d.ts +3 -3
  17. package/dist/coordination/mailbox-project-server.js +3 -4
  18. package/dist/coordination/mailbox-types.d.ts +6 -0
  19. package/dist/coordination/multi-agent-coordinator.d.ts +1 -0
  20. package/dist/core/agent-response.d.ts +5 -1
  21. package/dist/core/agent-tools.d.ts +3 -0
  22. package/dist/core/context.d.ts +20 -0
  23. package/dist/core/fallback-model.d.ts +48 -0
  24. package/dist/core/index.d.ts +11 -9
  25. package/dist/core/index.js +1119 -310
  26. package/dist/core/instruction-template.d.ts +83 -0
  27. package/dist/core/next-steps-slot.d.ts +88 -0
  28. package/dist/core/system-prompt-blocks.d.ts +10 -1
  29. package/dist/core/system-prompt-builder.d.ts +40 -4
  30. package/dist/core/system-prompt-memory-skills.d.ts +1 -0
  31. package/dist/core/system-prompt-skill-bodies.d.ts +3 -3
  32. package/dist/defaults/index.js +9805 -8767
  33. package/dist/design/index.js +11 -3
  34. package/dist/execution/auto-compaction-middleware.d.ts +17 -0
  35. package/dist/execution/autonomy-brain.d.ts +15 -2
  36. package/dist/execution/brain-runtime.d.ts +3 -1
  37. package/dist/execution/compaction-core.d.ts +41 -2
  38. package/dist/execution/council-brain.d.ts +47 -1
  39. package/dist/execution/council-orchestrator.d.ts +25 -6
  40. package/dist/execution/council-prompts.d.ts +12 -1
  41. package/dist/execution/index.d.ts +11 -10
  42. package/dist/execution/index.js +10507 -9189
  43. package/dist/execution/tool-executor.d.ts +4 -1
  44. package/dist/execution/topic-shift-advisor.d.ts +53 -0
  45. package/dist/extension/index.js +8 -2
  46. package/dist/extension/registry.d.ts +8 -1
  47. package/dist/fleet-notifier.d.ts +9 -2
  48. package/dist/goal/index.js +11 -3
  49. package/dist/hooks/index.js +50 -13
  50. package/dist/hooks/runner.d.ts +12 -1
  51. package/dist/hq/index.js +59 -21
  52. package/dist/hq/protocol/fleet.d.ts +20 -0
  53. package/dist/hq/protocol.js +10 -0
  54. package/dist/hq/publisher.d.ts +21 -3
  55. package/dist/index.d.ts +22 -20
  56. package/dist/index.js +12211 -8572
  57. package/dist/infrastructure/index.js +108 -21
  58. package/dist/kernel/events/brain-events.d.ts +9 -0
  59. package/dist/kernel/events/provider-events.d.ts +42 -1
  60. package/dist/kernel/events/session-events.d.ts +13 -0
  61. package/dist/kernel/events/tool-events.d.ts +3 -3
  62. package/dist/models/index.d.ts +1 -0
  63. package/dist/models/index.js +18 -1
  64. package/dist/models/provider-credentials.d.ts +54 -0
  65. package/dist/plugin/api.d.ts +6 -0
  66. package/dist/plugin/config.d.ts +55 -0
  67. package/dist/plugin/index.d.ts +2 -1
  68. package/dist/plugin/index.js +2179 -544
  69. package/dist/plugins/auto-review-plugin.d.ts +3 -0
  70. package/dist/plugins/cloud-config-sync-plugin.d.ts +22 -0
  71. package/dist/plugins/review-claim-registry.d.ts +23 -8
  72. package/dist/plugins/review-types.d.ts +2 -0
  73. package/dist/registry/index.js +109 -74
  74. package/dist/registry/tool-registry.d.ts +15 -0
  75. package/dist/security/capabilities.d.ts +2 -0
  76. package/dist/security/index.d.ts +2 -2
  77. package/dist/security/index.js +166 -44
  78. package/dist/security/permission-helpers.d.ts +23 -6
  79. package/dist/security/permission-policy.d.ts +16 -0
  80. package/dist/security/readonly-permission-policy.d.ts +20 -0
  81. package/dist/security/totp.d.ts +14 -0
  82. package/dist/session-registry-atomic-file.d.ts +32 -5
  83. package/dist/skills/frontmatter.d.ts +6 -0
  84. package/dist/skills/index.js +22 -5
  85. package/dist/storage/cloud-config-sync/sanitize.d.ts +54 -0
  86. package/dist/storage/cloud-config-sync.d.ts +87 -0
  87. package/dist/storage/director-state.d.ts +7 -0
  88. package/dist/storage/index.d.ts +1 -0
  89. package/dist/storage/index.js +887 -74
  90. package/dist/tools/fallback-system-config-view-tool.d.ts +1 -1
  91. package/dist/tools/index.d.ts +1 -1
  92. package/dist/tools/index.js +898 -335
  93. package/dist/tools/one-shot-llm-tool.d.ts +1 -0
  94. package/dist/tools/plugin-manager.d.ts +27 -0
  95. package/dist/types/config/mcp-features.d.ts +18 -11
  96. package/dist/types/config/root.d.ts +8 -1
  97. package/dist/types/config/runtime.d.ts +20 -6
  98. package/dist/types/config/skills-fleet-brain.d.ts +12 -4
  99. package/dist/types/config/tools.d.ts +16 -0
  100. package/dist/types/context-window.d.ts +1 -0
  101. package/dist/types/council.d.ts +11 -0
  102. package/dist/types/hooks.d.ts +14 -0
  103. package/dist/types/index.d.ts +3 -3
  104. package/dist/types/index.js +1 -0
  105. package/dist/types/multi-agent.d.ts +12 -0
  106. package/dist/types/one-shot-llm.d.ts +25 -0
  107. package/dist/types/plugin.d.ts +28 -0
  108. package/dist/types/provider.d.ts +16 -0
  109. package/dist/types/runtime-capability-manifest.d.ts +168 -0
  110. package/dist/types/skill.d.ts +5 -0
  111. package/dist/types/system-prompt.d.ts +3 -1
  112. package/dist/types/tool-executor.d.ts +8 -1
  113. package/dist/utils/context-breakdown.d.ts +8 -2
  114. package/dist/utils/index.d.ts +1 -1
  115. package/dist/utils/index.js +141 -31
  116. package/dist/utils/regex-guard.d.ts +16 -2
  117. package/dist/utils/sage-output-block.d.ts +7 -10
  118. package/dist/utils/wstack-paths.d.ts +11 -3
  119. package/dist/worktree/index.js +4 -4
  120. package/instructions/agents/browser.md +1 -4
  121. package/instructions/agents/e2e.md +17 -15
  122. package/instructions/agents/ios.md +3 -3
  123. package/instructions/agents/search.md +1 -1
  124. package/instructions/autonomy/goal-preamble.md +4 -4
  125. package/instructions/coordination/director-preamble.md +2 -3
  126. package/instructions/coordination/subagent-baseline.md +3 -1
  127. package/instructions/leader-after-task.md +12 -0
  128. package/instructions/llm/chimera-review.md +1 -1
  129. package/instructions/modes/audit-lite.md +1 -1
  130. package/instructions/sections/tool/common-patterns.md +18 -2
  131. package/instructions/sections/tool/mailbox-compact.md +1 -1
  132. package/instructions/sections/tool/mailbox-full.md +1 -1
  133. package/instructions/system-lite.md +91 -3
  134. package/instructions/system-pro.md +296 -102
  135. package/instructions/system.md +248 -92
  136. package/package.json +3 -3
  137. package/skills/api-design/SKILL.md +3 -0
  138. package/skills/audit-log/SKILL.md +2 -0
  139. package/skills/auto-review/SKILL.md +6 -1
  140. package/skills/bug-hunter/SKILL.md +4 -1
  141. package/skills/chimera/SKILL.md +3 -0
  142. package/skills/data-governance/SKILL.md +3 -0
  143. package/skills/design-system/SKILL.md +5 -2
  144. package/skills/docker-deploy/SKILL.md +2 -0
  145. package/skills/git-flow/SKILL.md +2 -0
  146. package/skills/mailbox-bridge/SKILL.md +3 -0
  147. package/skills/mnemosyne/SKILL.md +2 -0
  148. package/skills/multi-agent/SKILL.md +4 -1
  149. package/skills/node-modern/SKILL.md +4 -1
  150. package/skills/observability/SKILL.md +3 -0
  151. package/skills/output-standards/SKILL.md +2 -0
  152. package/skills/plugin-author/SKILL.md +4 -1
  153. package/skills/prompt-engineering/SKILL.md +3 -1
  154. package/skills/react-modern/SKILL.md +6 -3
  155. package/skills/refactor-planner/SKILL.md +2 -0
  156. package/skills/research-web/SKILL.md +3 -0
  157. package/skills/sdd/SKILL.md +3 -1
  158. package/skills/security-scanner/SKILL.md +3 -0
  159. package/skills/skill-creator/SKILL.md +2 -0
  160. package/skills/tech-stack/SKILL.md +3 -0
  161. package/skills/testing/SKILL.md +4 -1
  162. package/skills/typescript-strict/SKILL.md +4 -1
  163. package/skills/wrongstack-kanban/SKILL.md +6 -3
  164. package/skills/wrongstack-mailbox/SKILL.md +4 -1
  165. package/skills/wrongstack-mailbox-mcp/SKILL.md +10 -8
@@ -35,27 +35,81 @@ var init_pid = __esm({
35
35
  // src/session-registry-atomic-file.ts
36
36
  import { randomUUID } from "node:crypto";
37
37
  import * as fs6 from "node:fs/promises";
38
+ import { hostname } from "node:os";
38
39
  import * as path8 from "node:path";
40
+ function lockOwnerStamp() {
41
+ return `${HOST_NAME}:${process.pid}`;
42
+ }
43
+ async function maybeUnlinkOwnedLock(lockPath) {
44
+ const owner = await fs6.readFile(lockPath, "utf8").catch(() => null);
45
+ if (owner === null || owner.trim() !== lockOwnerStamp()) return;
46
+ for (let attempt = 0; attempt < 3; attempt++) {
47
+ try {
48
+ await fs6.unlink(lockPath);
49
+ return;
50
+ } catch {
51
+ if (attempt < 2) await new Promise((resolve5) => setTimeout(resolve5, 5));
52
+ }
53
+ }
54
+ }
39
55
  async function breakStaleLock(lockPath) {
40
56
  try {
41
- const [stat5, content] = await Promise.all([
42
- fs6.stat(lockPath),
43
- /* v8 ignore start -- best-effort lock-content read; .catch only fires if the lock vanished */
44
- fs6.readFile(lockPath, "utf8").catch(() => "")
45
- /* v8 ignore stop */
46
- ]);
47
- const ageMs = Date.now() - stat5.mtimeMs;
48
- const ownerPid = Number.parseInt(content.trim(), 10);
49
- const ownerDead = Number.isInteger(ownerPid) && ownerPid > 0 && ownerPid !== process.pid && !isPidAlive(ownerPid);
50
- if (ownerDead || ageMs > STALE_LOCK_MS) {
51
- await fs6.unlink(lockPath).catch(() => void 0);
52
- return true;
57
+ const content = await fs6.readFile(lockPath, "utf8").catch(() => "");
58
+ const trimmed = content.trim();
59
+ const colonIdx = trimmed.indexOf(":");
60
+ if (colonIdx === -1) {
61
+ const barePid = Number.parseInt(trimmed, 10);
62
+ if (Number.isInteger(barePid) && barePid > 0 && !isPidAlive(barePid)) {
63
+ return await breakStaleLockVerified(lockPath, async () => {
64
+ const reread = await fs6.readFile(lockPath, "utf8").catch(() => "");
65
+ return reread.trim() === trimmed;
66
+ });
67
+ }
68
+ return await breakStaleLockVerified(lockPath, async () => {
69
+ const st = await fs6.stat(lockPath);
70
+ return Date.now() - st.mtimeMs > STALE_LOCK_MS;
71
+ });
53
72
  }
54
- return false;
73
+ const ownerHost = trimmed.slice(0, colonIdx);
74
+ const ownerPid = Number.parseInt(trimmed.slice(colonIdx + 1), 10);
75
+ if (ownerHost === HOST_NAME && Number.isInteger(ownerPid) && ownerPid > 0) {
76
+ if (isPidAlive(ownerPid)) {
77
+ const stat5 = await fs6.stat(lockPath);
78
+ if (Date.now() - stat5.mtimeMs > SAME_HOST_STALE_MS) {
79
+ return await breakStaleLockVerified(lockPath, async () => {
80
+ const st = await fs6.stat(lockPath);
81
+ return Date.now() - st.mtimeMs > SAME_HOST_STALE_MS;
82
+ });
83
+ }
84
+ return false;
85
+ }
86
+ return await breakStaleLockVerified(lockPath, async () => {
87
+ const reread = await fs6.readFile(lockPath, "utf8").catch(() => "");
88
+ return reread.trim() === trimmed;
89
+ });
90
+ }
91
+ return await breakStaleLockVerified(lockPath, async () => {
92
+ const st = await fs6.stat(lockPath);
93
+ return Date.now() - st.mtimeMs > STALE_LOCK_MS;
94
+ });
55
95
  } catch {
56
96
  return true;
57
97
  }
58
98
  }
99
+ async function breakStaleLockVerified(lockPath, verify) {
100
+ if (!await verify()) return false;
101
+ return await breakLockAtomically(lockPath) === true;
102
+ }
103
+ async function breakLockAtomically(lockPath) {
104
+ const tombstone = `${lockPath}.stale-${randomUUID()}.tmp`;
105
+ try {
106
+ await fs6.rename(lockPath, tombstone);
107
+ } catch {
108
+ return false;
109
+ }
110
+ void fs6.unlink(tombstone).catch(() => void 0);
111
+ return true;
112
+ }
59
113
  async function writeAtomicFile(filePath, registry) {
60
114
  const tmp = path8.join(
61
115
  path8.dirname(filePath),
@@ -120,14 +174,16 @@ async function pruneStaleTempFiles(filePath) {
120
174
  } catch {
121
175
  }
122
176
  }
123
- var STALE_LOCK_MS, STALE_TMP_MS, MAX_STALE_TMP_FILES;
177
+ var STALE_LOCK_MS, SAME_HOST_STALE_MS, STALE_TMP_MS, MAX_STALE_TMP_FILES, HOST_NAME;
124
178
  var init_session_registry_atomic_file = __esm({
125
179
  "src/session-registry-atomic-file.ts"() {
126
180
  "use strict";
127
181
  init_pid();
128
182
  STALE_LOCK_MS = 1e4;
183
+ SAME_HOST_STALE_MS = 2 * STALE_LOCK_MS;
129
184
  STALE_TMP_MS = 6e4;
130
185
  MAX_STALE_TMP_FILES = 20;
186
+ HOST_NAME = hostname();
131
187
  }
132
188
  });
133
189
 
@@ -552,16 +608,33 @@ var init_session_registry = __esm({
552
608
  continue;
553
609
  }
554
610
  }
611
+ let stampFailed = false;
555
612
  try {
556
- await lockHandle.writeFile(String(process.pid)).catch(() => void 0);
613
+ let stamped = false;
614
+ for (let stampAttempt = 0; stampAttempt < 3 && !stamped; stampAttempt++) {
615
+ try {
616
+ await lockHandle.writeFile(lockOwnerStamp());
617
+ stamped = true;
618
+ } catch {
619
+ if (stampAttempt < 2) await new Promise((resolve5) => setTimeout(resolve5, 5));
620
+ }
621
+ }
622
+ if (!stamped) {
623
+ stampFailed = true;
624
+ throw new Error("failed to stamp session-registry lock owner");
625
+ }
557
626
  const raw = await fs7.readFile(this.filePath, "utf8").catch(() => "{}");
558
627
  const registry = parseRegistry(raw);
559
628
  fn(registry);
560
629
  await this.writeAtomicWithRetry(registry);
561
630
  return;
562
631
  } finally {
563
- await lockHandle.close();
564
- await fs7.unlink(lockPath).catch(() => void 0);
632
+ await lockHandle.close().catch(() => void 0);
633
+ if (stampFailed) {
634
+ await fs7.unlink(lockPath).catch(() => void 0);
635
+ } else {
636
+ await maybeUnlinkOwnedLock(lockPath);
637
+ }
565
638
  }
566
639
  } catch (err) {
567
640
  if (err instanceof SessionOwnershipConflictError) throw err;
@@ -2911,6 +2984,9 @@ function canonicalProjectRoot(absRoot) {
2911
2984
  const commonDirFile = path5.join(gitDir, "commondir");
2912
2985
  if (!fs4.statSync(commonDirFile).isFile()) return checkoutRoot;
2913
2986
  const commonDir = path5.resolve(gitDir, fs4.readFileSync(commonDirFile, "utf8").trim());
2987
+ const worktreesDir = path5.dirname(gitDir);
2988
+ if (path5.basename(worktreesDir).toLowerCase() !== "worktrees") return checkoutRoot;
2989
+ if (path5.resolve(worktreesDir, "..") !== commonDir) return checkoutRoot;
2914
2990
  if (path5.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
2915
2991
  return path5.dirname(commonDir);
2916
2992
  } catch {
@@ -2933,7 +3009,7 @@ function safeProfileName(name) {
2933
3009
  const safe = (name ?? "").replace(/[/\\:]/g, "_").replace(/\.\./g, "_").trim();
2934
3010
  return safe || "default";
2935
3011
  }
2936
- function activeProfileName(globalRoot) {
3012
+ function bootstrapProfileName(globalRoot) {
2937
3013
  try {
2938
3014
  const parsed = JSON.parse(fs4.readFileSync(path5.join(globalRoot, "config.json"), "utf8"));
2939
3015
  return safeProfileName(
@@ -2951,7 +3027,7 @@ function wstackGlobalRoot() {
2951
3027
  function resolveWstackPaths(opts) {
2952
3028
  const globalRoot = opts.globalRoot ?? (opts.userHome ? path5.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
2953
3029
  const homeDir = opts.userHome ?? os2.homedir();
2954
- const profileName = safeProfileName(opts.profileName ?? activeProfileName(globalRoot));
3030
+ const profileName = safeProfileName(opts.profileName ?? bootstrapProfileName(globalRoot));
2955
3031
  const profileDir = path5.join(globalRoot, "profiles", profileName);
2956
3032
  const hash = projectHash(opts.projectRoot);
2957
3033
  const slug = projectSlug(opts.projectRoot);
@@ -3021,7 +3097,12 @@ function resolveWstackPaths(opts) {
3021
3097
  projectRequirementIntakes: path5.join(projectDir, "requirement-intakes"),
3022
3098
  syncConfig: path5.join(profileDir, "sync.json"),
3023
3099
  configHistoryDir: path5.join(globalRoot, "config-history"),
3024
- projectStatus: (projectHash2) => path5.join(globalRoot, "projects", projectHash2, "status.json")
3100
+ projectStatus: (statusProjectSlug) => {
3101
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,39})-[a-f0-9]{6}$/.test(statusProjectSlug)) {
3102
+ throw new Error(`Invalid project slug: ${statusProjectSlug}`);
3103
+ }
3104
+ return path5.join(globalRoot, "projects", statusProjectSlug, "status.json");
3105
+ }
3025
3106
  };
3026
3107
  }
3027
3108
 
@@ -3035,6 +3116,7 @@ function isBootstrapOnly(config) {
3035
3116
  function normalizeTokenSavingTier(val) {
3036
3117
  if (val === void 0) return "off";
3037
3118
  if (typeof val === "boolean") return val ? "medium" : "off";
3119
+ if (val === "auto") return "medium";
3038
3120
  const validTiers = /* @__PURE__ */ new Set([
3039
3121
  "off",
3040
3122
  "minimal",
@@ -3176,7 +3258,7 @@ var CONFIG_BEHAVIOR_DEFAULTS = {
3176
3258
  enabled: false
3177
3259
  }
3178
3260
  },
3179
- skills: { readClaudeSkills: true },
3261
+ skills: { readClaudeSkills: true, mode: "progressive" },
3180
3262
  mcpServers: {},
3181
3263
  fallbackAuto: true,
3182
3264
  maxConcurrent: 4,
@@ -3378,6 +3460,10 @@ var KNOWN_DENIED_IN_PROJECT = [
3378
3460
  reason: "Controls which plugin boot states the LLM may mutate; user-owned trust policy."
3379
3461
  },
3380
3462
  { key: "sync", reason: "Carries githubToken credential and target repo." },
3463
+ {
3464
+ key: "cloudSync",
3465
+ reason: "Carries the my.wrongstack.com machine token credential and endpoint URL."
3466
+ },
3381
3467
  { key: "yolo", reason: "Disables all permission confirmation prompts." },
3382
3468
  { key: "extensions", reason: "Per-plugin config can carry command/credential fields." },
3383
3469
  { key: "hq", reason: "Carries HQ client token credential and endpoint URL." },
@@ -3440,6 +3526,7 @@ var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
3440
3526
  "modelRuntime",
3441
3527
  "hq",
3442
3528
  "sync",
3529
+ "cloudSync",
3443
3530
  "extensions",
3444
3531
  "acp",
3445
3532
  "fleet",
@@ -264,6 +264,15 @@ export interface BrainEventMap {
264
264
  completed: number;
265
265
  /** Optional fleet-wide cost total (USD) — set by the host bridge when known. */
266
266
  totalCostUsd?: number | undefined;
267
+ /** Concurrent-subagent ceiling (issue #323). */
268
+ maxConcurrent?: number | undefined;
269
+ /** Lifetime spawn budget snapshot (issue #323). */
270
+ maxSpawns?: number | undefined;
271
+ usedSpawns?: number | undefined;
272
+ remainingSpawns?: number | undefined;
273
+ effectiveSource?: string | undefined;
274
+ checkpointMaxSpawns?: number | undefined;
275
+ ceilingMismatch?: boolean | undefined;
267
276
  subagentStatuses: {
268
277
  subagentId: string;
269
278
  taskId: string;
@@ -160,6 +160,8 @@ export interface ProviderEventMap {
160
160
  };
161
161
  status: number;
162
162
  providerSwitched: boolean;
163
+ /** Gate correlation id — set when a fallback gate mediated the switch. */
164
+ requestId?: string | undefined;
163
165
  contextWindowWarning?: {
164
166
  fromMaxContext: number;
165
167
  toMaxContext: number;
@@ -167,7 +169,33 @@ export interface ProviderEventMap {
167
169
  } | undefined;
168
170
  };
169
171
  /**
170
- * Fired whenever a (providerId, model) pair transitions between
172
+ * Fired by the fallback gate function (supplied to the fallback-model
173
+ * extension via `FallbackModelDeps.fallbackGate`) when the chain is about
174
+ * to engage, BEFORE attempting any fallback entry. Carries the full
175
+ * candidate list so the UI can show a modal with a countdown and manual
176
+ * pick. The gate waits for a `provider.fallback_choice` event bus emission
177
+ * (or the countdown timer) before proceeding with the chosen model or
178
+ * auto-switching to the next candidate.
179
+ */
180
+ 'provider.fallback_pending': {
181
+ sessionId?: string | undefined;
182
+ from: {
183
+ providerId: string;
184
+ model: string;
185
+ };
186
+ status: number;
187
+ candidates: Array<{
188
+ providerId: string;
189
+ model: string;
190
+ }>;
191
+ /** Seconds the UI should count down before auto-switching to the next model. */
192
+ autoSwitchSeconds: number;
193
+ /** Unique request id — the UI echoes this back in the choice message. */
194
+ requestId: string;
195
+ timestamp: number;
196
+ };
197
+ /**
198
+ * Fired when a (providerId, model) pair transitions between
171
199
  * healthy/degraded/blocked states. The tracker emits this so the
172
200
  * CLI/TUI/WebUI can render a live status indicator.
173
201
  */
@@ -180,6 +208,19 @@ export interface ProviderEventMap {
180
208
  timestamp: number;
181
209
  stateExpiresAt?: number | undefined;
182
210
  };
211
+ /**
212
+ * Fired by the UI when the user manually picks a model from the
213
+ * fallback modal. The fallback gate listens for this event (matched
214
+ * by `requestId`) to resolve with the chosen model instead of waiting
215
+ * for the countdown.
216
+ */
217
+ 'provider.fallback_choice': {
218
+ requestId: string;
219
+ providerId?: string | undefined;
220
+ model?: string | undefined;
221
+ /** When true, auto-switch to the next candidate (countdown expired or Esc). */
222
+ autoSwitch?: boolean | undefined;
223
+ };
183
224
  /**
184
225
  * Fired when the agent's actively selected (primary, not fallback)
185
226
  * provider/model is blocked and will be skipped. The CLI/TUI should
@@ -11,6 +11,13 @@ export interface SessionEventMap {
11
11
  id: string;
12
12
  sessionId?: string | undefined;
13
13
  usage: Usage;
14
+ /**
15
+ * Register asynchronous session-end producer work during the synchronous
16
+ * `session.ended` callback. Cleanup waits for every registered promise to
17
+ * settle (fulfilled or rejected) before draining review work and closing.
18
+ * Calls made after the callback returns are outside the join contract.
19
+ */
20
+ waitUntil?: ((work: Promise<void>) => void) | undefined;
14
21
  };
15
22
  'session.damaged': {
16
23
  sessionId: string;
@@ -72,6 +79,12 @@ export interface SessionEventMap {
72
79
  providerId: string;
73
80
  modelId: string;
74
81
  maxContext: number;
82
+ /** Previous positive limit when this event represents a live change. */
83
+ previousMaxContext?: number | undefined;
84
+ /** Whether the value came from static resolution or a live provider probe. */
85
+ source?: 'configured' | 'provider' | 'provider_overflow' | undefined;
86
+ /** True when the positive limit decreased and UI should surface a warning. */
87
+ decreased?: boolean | undefined;
75
88
  };
76
89
  'context.repaired': {
77
90
  sessionId?: string | undefined;
@@ -202,9 +202,9 @@ export interface ToolEventMap {
202
202
  input?: unknown | undefined;
203
203
  output?: string | undefined;
204
204
  /**
205
- * The `--- SAGE: … (Memory Injector) ---` block the SAGE middleware
206
- * appended to the result, split off `output` BEFORE the preview cap and
207
- * carried as whole lines (header first). Absent when nothing was injected.
205
+ * Legacy inline `--- SAGE: … (Memory Injector) ---` content split off
206
+ * `output` before the preview cap. Current retrievals travel through
207
+ * `memory.injector_run` and provider memory-evidence blocks instead.
208
208
  *
209
209
  * Surfaces render it as a memory card, never as tool output: sharing the
210
210
  * `output` budget used to cut a memory line mid-fence on short tool
@@ -8,4 +8,5 @@ export { ALIBABA_TOKEN_PLAN_MODELS, alibabaTokenPlanModelMeta, type AlibabaToken
8
8
  export { MODELS_DEV_MODALITY_VALUES, modelsDevCostSchema, modelsDevLimitSchema, modelsDevModalitiesSchema, modelsDevModelSchema, modelsDevPayloadSchema, modelsDevProviderSchema, type ModelsDevModelParsed, type ModelsDevPayloadParsed, type ModelsDevProviderParsed, type ModelsDevSchemaSyncProof, } from './models-dev-schema.js';
9
9
  export { MODEL_PROFILES, TASK_TO_ROLE, inferTaskType, findModelProfile, scoreModelForTask, type ModelProfile, type TaskType, } from './model-intelligence.js';
10
10
  export { ModelRouter, type RouterConfig, type ModelPick, type RouterCosts, type ModelIntelligenceEntry, } from './model-router.js';
11
+ export { hasProviderCredential, hasProviderKeyInConfig, hasProviderKeyInEnv, type ProviderCredentialConfig, type ProviderCredentialSubject, } from './provider-credentials.js';
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -584,7 +584,7 @@ var DefaultModelsRegistry = class {
584
584
  async load(opts = {}) {
585
585
  if (this.payload && !opts.force) return this.payload;
586
586
  if (this.seed) {
587
- this.payload = this.seed;
587
+ this.payload = this.withExtraOverlay(this.seed);
588
588
  this.fetchedAt = /* @__PURE__ */ new Date();
589
589
  return this.payload;
590
590
  }
@@ -2391,6 +2391,20 @@ var ModelRouter = class {
2391
2391
  return matrix;
2392
2392
  }
2393
2393
  };
2394
+
2395
+ // src/models/provider-credentials.ts
2396
+ function hasProviderKeyInEnv(provider, env = process.env) {
2397
+ return (provider.envVars ?? []).some((name) => !!env[name]);
2398
+ }
2399
+ function hasProviderKeyInConfig(providerId, config) {
2400
+ const entry = config?.providers?.[providerId];
2401
+ if (!entry) return false;
2402
+ if (typeof entry.apiKey === "string" && entry.apiKey.length > 0) return true;
2403
+ return Array.isArray(entry.apiKeys) && entry.apiKeys.some((k) => !!k?.apiKey);
2404
+ }
2405
+ function hasProviderCredential(provider, config, env = process.env) {
2406
+ return hasProviderKeyInEnv(provider, env) || hasProviderKeyInConfig(provider.id, config);
2407
+ }
2394
2408
  export {
2395
2409
  ALIBABA_TOKEN_PLAN_MODELS,
2396
2410
  CODEX_MODELS,
@@ -2406,6 +2420,9 @@ export {
2406
2420
  codexModelMeta,
2407
2421
  describeCatalogModel,
2408
2422
  findModelProfile,
2423
+ hasProviderCredential,
2424
+ hasProviderKeyInConfig,
2425
+ hasProviderKeyInEnv,
2409
2426
  inferTaskType,
2410
2427
  loadProjectModes,
2411
2428
  loadUserModes,
@@ -0,0 +1,54 @@
1
+ /**
2
+ * "Does this provider have a usable credential?" — one answer.
3
+ *
4
+ * There were three, and they disagreed:
5
+ *
6
+ * - `cli/src/provider-helpers.ts` — env var OR `config.providers[id].apiKey`
7
+ * OR any entry in `apiKeys[]`. The complete rule.
8
+ * - `cli/src/slash-commands/modelcaps.ts` — config only. `/modelcaps` printed
9
+ * a hollow `○` next to every provider keyed purely through
10
+ * `ANTHROPIC_API_KEY`, which is the most common way to key one.
11
+ * - `webui-server/src/server/provider-handlers.ts` — `savedIds.has(id) || env`.
12
+ * `savedIds` means "appears in the saved config at all", so a provider saved
13
+ * with a `baseUrl` and no key reported `hasApiKey: true` and the WebUI
14
+ * offered it as ready to use.
15
+ *
16
+ * The rule itself is small; that it lives in three places is the defect. This
17
+ * module is in core because the CLI and the WebUI server both need it and
18
+ * neither may import the other.
19
+ */
20
+ /** The provider fields the check reads. Both call sites can supply these. */
21
+ export interface ProviderCredentialSubject {
22
+ readonly id: string;
23
+ /** Environment variables that carry this provider's key, if any. */
24
+ readonly envVars?: readonly string[] | undefined;
25
+ }
26
+ /** The saved-config shape the check reads. */
27
+ export interface ProviderCredentialConfig {
28
+ readonly providers?: Readonly<Record<string, {
29
+ readonly apiKey?: string | undefined;
30
+ readonly apiKeys?: readonly ({
31
+ readonly apiKey?: string | undefined;
32
+ } | undefined)[] | undefined;
33
+ } | undefined>> | undefined;
34
+ }
35
+ /**
36
+ * Is a key present in the environment for this provider?
37
+ *
38
+ * Split out because the WebUI reports the two sources separately in places, and
39
+ * because a caller with no config still has a meaningful answer.
40
+ */
41
+ export declare function hasProviderKeyInEnv(provider: ProviderCredentialSubject, env?: Readonly<Record<string, string | undefined>>): boolean;
42
+ /** Is a non-empty key stored in the saved config for this provider? */
43
+ export declare function hasProviderKeyInConfig(providerId: string, config: ProviderCredentialConfig | undefined): boolean;
44
+ /**
45
+ * Can the user call this provider right now?
46
+ *
47
+ * Deliberately does NOT cover keyless local gateways (loopback servers that
48
+ * declare no env vars). They need no credential at all, which is a different
49
+ * question with a different answer — see `isKeylessLocalProvider`. Folding the
50
+ * two together here would make "has a key" mean "is usable" and quietly lose
51
+ * the distinction the picker needs.
52
+ */
53
+ export declare function hasProviderCredential(provider: ProviderCredentialSubject, config: ProviderCredentialConfig | undefined, env?: Readonly<Record<string, string | undefined>>): boolean;
54
+ //# sourceMappingURL=provider-credentials.d.ts.map
@@ -7,9 +7,11 @@ import type { ProviderRegistry } from '../registry/provider-registry.js';
7
7
  import type { SlashCommandRegistry } from '../registry/slash-command-registry.js';
8
8
  import type { ToolRegistry } from '../registry/tool-registry.js';
9
9
  import type { Config } from '../types/config.js';
10
+ import type { CouncilQuestion, CouncilResult } from '../types/council.js';
10
11
  import type { HookEvent, HookMatcher, InProcessHook } from '../types/hooks.js';
11
12
  import type { Logger } from '../types/logger.js';
12
13
  import type { ModelsRegistry } from '../types/models-registry.js';
14
+ import type { OneShotLLMInput, OneShotLLMResult } from '../types/one-shot-llm.js';
13
15
  import type { MCPRegistryView, MetricsSinkView, Notifier, PluginAPI, PluginCapabilities, PluginDependency, PluginLLM, PluginPipelines, ProviderRegistryView, SessionWriterView, SlashCommandRegistryView, ToolRegistryView } from '../types/plugin.js';
14
16
  import type { Provider } from '../types/provider.js';
15
17
  import type { SystemPromptContributor } from '../types/system-prompt-contributor.js';
@@ -91,6 +93,10 @@ export interface PluginAPIInit {
91
93
  getProvider?: (() => Provider) | undefined;
92
94
  getModel?: (() => string) | undefined;
93
95
  createProvider?: ((name: string, model?: string) => Provider) | undefined;
96
+ /** Preferred production path: shared One Shot runtime with fallbacks. */
97
+ oneShot?: ((input: OneShotLLMInput) => Promise<OneShotLLMResult>) | undefined;
98
+ /** Optional shared multi-model Council runtime. */
99
+ council?: ((question: CouncilQuestion) => Promise<CouncilResult>) | undefined;
94
100
  } | undefined;
95
101
  config: Config;
96
102
  /**
@@ -28,6 +28,61 @@ export interface PluginConfigChange {
28
28
  * beat aliases, making migrations deterministic regardless of object order.
29
29
  */
30
30
  export declare function resolvePluginConfig(input: ResolvePluginConfigInput): ResolvedPluginConfig;
31
+ /**
32
+ * Default `config.plugins` name matcher: the canonical name, a declared
33
+ * alias, or the `@wrongstack/plugins/<name>` subpath spelling of either.
34
+ *
35
+ * Surfaces that know more pass their own `matches` — the loader has an alias
36
+ * table (`lsp` → `@wrongstack/plug-lsp`), the CLI folds `telegram` and
37
+ * `@wrongstack/telegram` into one row. This is the floor, not the ceiling.
38
+ */
39
+ export declare function pluginEntryMatchesName(configuredName: string, name: string, aliases?: readonly string[]): boolean;
40
+ export type PluginEnablementSource = 'feature-flag' | 'plugin-entry' | 'extension' | 'default';
41
+ export interface ResolvedPluginEnablement {
42
+ enabled: boolean;
43
+ source: PluginEnablementSource;
44
+ }
45
+ export interface ResolvePluginEnablementInput {
46
+ name: string;
47
+ aliases?: readonly string[] | undefined;
48
+ /**
49
+ * `'active'` — runs unless something turns it off.
50
+ * `'inactive'` — runs only when something explicitly turns it on.
51
+ * Omitted behaves like `'inactive'`.
52
+ */
53
+ defaultState?: 'active' | 'inactive' | undefined;
54
+ config?: (Partial<Pick<Config, 'plugins' | 'extensions'>> & {
55
+ features?: Partial<Config['features']> | undefined;
56
+ }) | undefined;
57
+ /**
58
+ * Overrides how a `config.plugins` entry name is matched against this
59
+ * plugin. Surfaces normalize specs differently (the loader maps
60
+ * `@wrongstack/plugins/foo` → `foo`, the CLI treats `telegram` and
61
+ * `@wrongstack/telegram` as one row), so matching is per-surface —
62
+ * only the PRECEDENCE below is shared. Defaults to name/alias equality.
63
+ */
64
+ matches?: ((configuredName: string) => boolean) | undefined;
65
+ }
66
+ /**
67
+ * Resolve whether a plugin boots, under ONE precedence shared by every
68
+ * surface that reports or acts on plugin state:
69
+ *
70
+ * 1. `features.plugins === false` — kills every plugin.
71
+ * 2. a matching `config.plugins` entry — `{ enabled: false }` is off,
72
+ * anything else is on. This is what `wstack plugin enable|disable`
73
+ * writes, so it stays the highest per-plugin authority.
74
+ * 3. `config.extensions[name].enabled`, when it is a boolean — the
75
+ * plugin's own master switch.
76
+ * 4. the catalog `defaultState`.
77
+ *
78
+ * Rule 3 used to be read only by the loader, and only in its `=== true`
79
+ * direction: a plugin switched on via `extensions` ran while every
80
+ * reporting surface — which looked at `config.plugins` alone — called it
81
+ * disabled, and `extensions[name].enabled = false` turned nothing off.
82
+ * Both directions now resolve here, so "what runs" and "what the report
83
+ * says" cannot drift apart again.
84
+ */
85
+ export declare function resolvePluginEnablement(input: ResolvePluginEnablementInput): ResolvedPluginEnablement;
31
86
  export declare function resolvePluginManifestConfig(plugin: Pick<Plugin, 'name' | 'configAliases' | 'defaultConfig'>, config?: ResolvePluginConfigInput['config'], explicitOptions?: Readonly<Record<string, unknown>>): ResolvedPluginConfig;
32
87
  /** Unknown fields are immutable by default so a new option cannot silently hot-reload. */
33
88
  export declare function diffPluginConfig(previous: Readonly<Record<string, unknown>>, next: Readonly<Record<string, unknown>>, fields: Readonly<Record<string, PluginConfigFieldMetadata>>): PluginConfigChange[];
@@ -1,5 +1,5 @@
1
1
  export { DefaultPluginAPI, definePlugin, type PluginAPIInit } from './api.js';
2
- export { diffPluginConfig, type PluginConfigChange, type PluginConfigSource, type ResolvePluginConfigInput, type ResolvedPluginConfig, redactPluginConfig, resolvePluginConfig, resolvePluginManifestConfig, validatePluginConfigMetadata, } from './config.js';
2
+ export { diffPluginConfig, type PluginConfigChange, type PluginConfigSource, type PluginEnablementSource, pluginEntryMatchesName, type ResolvePluginConfigInput, type ResolvePluginEnablementInput, type ResolvedPluginConfig, type ResolvedPluginEnablement, redactPluginConfig, resolvePluginConfig, resolvePluginEnablement, resolvePluginManifestConfig, validatePluginConfigMetadata, } from './config.js';
3
3
  export { KERNEL_API_VERSION, loadPlugins, type LoadPluginsOptions, type PluginHostHandle, type PluginLoadFailure, unloadPlugins, } from './loader.js';
4
4
  export type { PluginAPI } from '../types/plugin.js';
5
5
  export { buildReviewerModelPool, createAutoReviewPlugin, parseReviewSeverity, type ReviewerModelAssignment, selectRoundRobinReviewerAssignment, } from '../plugins/auto-review-plugin.js';
@@ -12,4 +12,5 @@ export { persistReviewReport, type ReportIntegrationResult, } from '../plugins/r
12
12
  export { maybeCompactReviewStores, type ReviewStoreMaintenanceResult, } from '../plugins/review-store-maintenance.js';
13
13
  export { createSkillsPlugin } from '../plugins/skills-plugin.js';
14
14
  export { createSyncPlugin } from '../plugins/sync-plugin.js';
15
+ export { createCloudConfigSyncPlugin } from '../plugins/cloud-config-sync-plugin.js';
15
16
  //# sourceMappingURL=index.d.ts.map