@wrongstack/core 0.298.2 → 0.299.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 (42) hide show
  1. package/dist/chronicle/index.js +4 -1
  2. package/dist/coordination/agents/index.js +4 -1
  3. package/dist/coordination/index.js +7 -4
  4. package/dist/core/index.js +62 -8
  5. package/dist/defaults/index.js +120 -18
  6. package/dist/design/index.js +4 -1
  7. package/dist/execution/council-brain.d.ts +6 -2
  8. package/dist/execution/council-personas.d.ts +10 -0
  9. package/dist/execution/index.d.ts +1 -1
  10. package/dist/execution/index.js +57 -7
  11. package/dist/goal/index.js +4 -1
  12. package/dist/hooks/index.js +132 -6
  13. package/dist/hq/exposure.d.ts +0 -11
  14. package/dist/hq/index.js +16 -4
  15. package/dist/hq/protocol/client.d.ts +14 -1
  16. package/dist/hq/protocol/fleet.d.ts +2 -0
  17. package/dist/hq/protocol.js +2 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +207 -47
  20. package/dist/infrastructure/index.js +50 -2
  21. package/dist/infrastructure/mcp-servers.d.ts +35 -0
  22. package/dist/kernel/events/provider-events.d.ts +7 -1
  23. package/dist/kernel/events/sdd-events.d.ts +2 -0
  24. package/dist/plugin/index.js +4 -1
  25. package/dist/storage/index.js +13 -1
  26. package/dist/tools/council-tool.d.ts +1 -1
  27. package/dist/tools/index.js +61 -10
  28. package/dist/types/config/skills-fleet-brain.d.ts +4 -2
  29. package/dist/types/config/tools.d.ts +99 -0
  30. package/dist/types/index.d.ts +2 -1
  31. package/dist/types/index.js +3 -3
  32. package/dist/types/one-shot-llm.d.ts +22 -3
  33. package/dist/types/session.d.ts +5 -1
  34. package/dist/utils/index.js +4 -1
  35. package/dist/utils/wstack-paths.d.ts +2 -0
  36. package/dist/worktree/index.js +43 -21
  37. package/dist/worktree/worktree-manager.d.ts +16 -10
  38. package/instructions/coordination/subagent-baseline.md +8 -0
  39. package/instructions/system-lite.md +2 -0
  40. package/instructions/system-pro.md +14 -10
  41. package/instructions/system.md +11 -7
  42. package/package.json +3 -3
@@ -2603,7 +2603,9 @@ function safeProfileName(name) {
2603
2603
  function activeProfileName(globalRoot) {
2604
2604
  try {
2605
2605
  const parsed = JSON.parse(fs.readFileSync(path.join(globalRoot, "config.json"), "utf8"));
2606
- return safeProfileName(typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0);
2606
+ return safeProfileName(
2607
+ typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0
2608
+ );
2607
2609
  } catch {
2608
2610
  return "default";
2609
2611
  }
@@ -2683,6 +2685,7 @@ function resolveWstackPaths(opts) {
2683
2685
  projectPlan: path.join(projectDir, "plan.json"),
2684
2686
  projectAutophase: path.join(projectDir, "autophase"),
2685
2687
  projectSddBoards: path.join(projectDir, "sdd-boards"),
2688
+ projectRequirementIntakes: path.join(projectDir, "requirement-intakes"),
2686
2689
  syncConfig: path.join(profileDir, "sync.json"),
2687
2690
  configHistoryDir: path.join(globalRoot, "config-history"),
2688
2691
  projectStatus: (projectHash2) => path.join(globalRoot, "projects", projectHash2, "status.json")
@@ -879,7 +879,9 @@ function safeProfileName(name) {
879
879
  function activeProfileName(globalRoot) {
880
880
  try {
881
881
  const parsed = JSON.parse(fs2.readFileSync(path2.join(globalRoot, "config.json"), "utf8"));
882
- return safeProfileName(typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0);
882
+ return safeProfileName(
883
+ typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0
884
+ );
883
885
  } catch {
884
886
  return "default";
885
887
  }
@@ -959,6 +961,7 @@ function resolveWstackPaths(opts) {
959
961
  projectPlan: path2.join(projectDir, "plan.json"),
960
962
  projectAutophase: path2.join(projectDir, "autophase"),
961
963
  projectSddBoards: path2.join(projectDir, "sdd-boards"),
964
+ projectRequirementIntakes: path2.join(projectDir, "requirement-intakes"),
962
965
  syncConfig: path2.join(profileDir, "sync.json"),
963
966
  configHistoryDir: path2.join(globalRoot, "config-history"),
964
967
  projectStatus: (projectHash2) => path2.join(globalRoot, "projects", projectHash2, "status.json")
@@ -6920,6 +6923,15 @@ var IN_PROJECT_DENIED_PATHS = [
6920
6923
  reason: "Extends the exec allow-list; a repo could authorise its own binaries."
6921
6924
  },
6922
6925
  { path: "tools.exec.danger", reason: "Weakens the destructive-command banner." },
6926
+ {
6927
+ // The whole subtree, not just the dangerous leaves: a persona's
6928
+ // `instruction` is rendered into the voter SYSTEM prompt, a profile seat
6929
+ // may pin providerId/model, and `defaultProfile` selects which of those
6930
+ // runs when the agent names no profile. Denying the parent leaves no leaf
6931
+ // to reclassify wrongly later.
6932
+ path: "tools.council",
6933
+ reason: "Council tool panel definitions: persona instructions are injected into the voter SYSTEM prompt and profile seats can pin an attacker-chosen providerId/model."
6934
+ },
6923
6935
  { path: "skills.extraDirs", reason: "Loads skill definitions from repo-chosen directories." },
6924
6936
  { path: "skills.registryUrl", reason: "Redirects skill installs to a repo-chosen host." },
6925
6937
  // Deliberately NOT denied: skills.mode and skills.eagerMaxChars. The audit
@@ -2,7 +2,7 @@ import type { FallbackProfileManager } from '../core/fallback-profile-manager.js
2
2
  import type { CouncilLLMCaller, CouncilOption, CouncilProfileConfig, CouncilResult } from '../types/council.js';
3
3
  import type { Tool } from '../types/tool.js';
4
4
  import type { CouncilPersonaRegistry } from '../execution/council-personas.js';
5
- import type { CouncilProfileRegistry } from '../execution/council-profiles.js';
5
+ import { type CouncilProfileRegistry } from '../execution/council-profiles.js';
6
6
  export declare const COUNCIL_TOOL_NAME = "council";
7
7
  export declare const MAX_COUNCIL_TOOL_OPTIONS = 12;
8
8
  export declare const MAX_COUNCIL_QUESTION_CHARS = 20000;
@@ -46,6 +46,9 @@ var BUILTIN_COUNCIL_PERSONAS = Object.freeze([
46
46
  tags: ["users", "usability", "accessibility"]
47
47
  })
48
48
  ]);
49
+ var BUILTIN_COUNCIL_PERSONA_IDS = Object.freeze(
50
+ BUILTIN_COUNCIL_PERSONAS.map((persona) => persona.id)
51
+ );
49
52
  var CouncilPersonaRegistry = class _CouncilPersonaRegistry {
50
53
  byId;
51
54
  constructor(personas = []) {
@@ -1238,7 +1241,7 @@ var INPUT_SCHEMA = {
1238
1241
  },
1239
1242
  profile: {
1240
1243
  type: "string",
1241
- description: "Registered Council profile id. Defaults to the host-configured profile."
1244
+ description: 'Registered Council profile id (e.g. "balanced", "fast", "risk-review"). Defaults to the host-configured profile.'
1242
1245
  }
1243
1246
  },
1244
1247
  required: ["question"],
@@ -1249,10 +1252,12 @@ function createCouncilTool(opts) {
1249
1252
  ...opts,
1250
1253
  fallbackProfileManager: opts.fallbackProfileManager
1251
1254
  });
1255
+ const profiles = opts.profiles ?? DEFAULT_COUNCIL_PROFILE_REGISTRY;
1256
+ const profileIds = profiles.list().map((profile) => profile.id);
1252
1257
  return {
1253
1258
  name: COUNCIL_TOOL_NAME,
1254
1259
  description: "Ask an independent, multi-persona Council to evaluate a decision or synthesize an answer. Uses bounded parallel voters, quorum/veto/weighted resolution, optional judging, model routing, fallback chains, and cancellation.",
1255
- usageHint: "Use for consequential or disputed decisions that benefit from independent lenses. Provide `options` for a vote or omit them for an open answer. Keep context evidence-focused; the Council treats it as untrusted data.",
1260
+ usageHint: `Use for consequential or disputed decisions that benefit from independent lenses. Provide \`options\` for a vote or omit them for an open answer. Keep context evidence-focused; the Council treats it as untrusted data. Available profiles: ${profileIds.join(", ")}.`,
1256
1261
  category: "meta",
1257
1262
  inputSchema: INPUT_SCHEMA,
1258
1263
  permission: "auto",
@@ -1270,11 +1275,17 @@ function createCouncilTool(opts) {
1270
1275
  };
1271
1276
  return orchestrator.ask(question);
1272
1277
  },
1273
- validate: validateCouncilToolInput
1278
+ validate: (input) => validateCouncilToolInput(input, profileIds)
1274
1279
  };
1275
1280
  }
1276
- function validateCouncilToolInput(input) {
1281
+ function validateCouncilToolInput(input, profileIds) {
1277
1282
  const errors = [];
1283
+ if (typeof input.profile === "string" && input.profile.trim()) {
1284
+ const id = input.profile.trim();
1285
+ if (!profileIds.includes(id)) {
1286
+ errors.push(`Unknown \`profile\` "${id}". Available: ${profileIds.join(", ")}.`);
1287
+ }
1288
+ }
1278
1289
  const question = input.question?.trim() ?? "";
1279
1290
  if (!question) errors.push("`question` must not be empty.");
1280
1291
  if (question.length > MAX_COUNCIL_QUESTION_CHARS) {
@@ -1418,7 +1429,7 @@ var FsError = class extends WrongStackError {
1418
1429
  var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)(?:\s+(?:has|have))?(?:\s+been)?[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit|usage[-_\s]*limit[-_\s]*(?:reached|exceeded)/i;
1419
1430
 
1420
1431
  // src/types/provider.ts
1421
- var CONTEXT_OVERFLOW_RE = /context.length|context.window|maximum context|max.*tokens?.*exceeded|prompt is too long|too long|exceeds the context|\btokens\b.*exceed|too many tokens|reduce the length|resulted in \d+ tokens|input.{0,12}too (?:large|long)|context_length_exceeded/i;
1432
+ var CONTEXT_OVERFLOW_RE = /context.length|context.window|maximum context|max.*tokens?.*exceeded|(?:prompt|request|input|messages?).{0,12}too (?:large|long)|exceeds the context|\btokens\b.*exceed|too many tokens|reduce the length|resulted in \d+ tokens|context_length_exceeded/i;
1422
1433
  var CONTENT_FILTER_RE = /content.(filter|policy|moderation)|safety (system|filter)/i;
1423
1434
  var RATE_LIMIT_EXCEEDED_RE = /rate[-_\s]*limit[-_\s]*exceeded/i;
1424
1435
  function classifyProviderError(status, body, message) {
@@ -1428,7 +1439,7 @@ function classifyProviderError(status, body, message) {
1428
1439
  if (status === 408) return "timeout";
1429
1440
  if (status === 599) return "stream_hang";
1430
1441
  if (status === 402 || QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
1431
- if (status === 429 && body?.message && RATE_LIMIT_EXCEEDED_RE.test(body.message)) {
1442
+ if (status === 429 && body?.message && body.type !== "rate_limit_exceeded" && RATE_LIMIT_EXCEEDED_RE.test(body.message)) {
1432
1443
  return "quota_exhausted";
1433
1444
  }
1434
1445
  if (type === "rate_limit_error" || status === 429) return "rate_limit";
@@ -1488,7 +1499,7 @@ var ProviderError = class extends WrongStackError {
1488
1499
  const e = err;
1489
1500
  const name = e.name;
1490
1501
  if (typeof name !== "string" || !name.endsWith("Error")) return false;
1491
- return typeof e.status === "number" && typeof e.retryable === "boolean" && typeof e.kind === "string";
1502
+ return typeof e.status === "number" && typeof e.retryable === "boolean" && typeof e.kind === "string" && typeof e.describe === "function";
1492
1503
  }
1493
1504
  constructor(message, status, retryable, providerId, opts = {}) {
1494
1505
  const kind = opts.kind ?? classifyProviderError(status, opts.body, message);
@@ -2216,7 +2227,9 @@ function safeProfileName(name) {
2216
2227
  function activeProfileName(globalRoot) {
2217
2228
  try {
2218
2229
  const parsed = JSON.parse(fs.readFileSync(path2.join(globalRoot, "config.json"), "utf8"));
2219
- return safeProfileName(typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0);
2230
+ return safeProfileName(
2231
+ typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0
2232
+ );
2220
2233
  } catch {
2221
2234
  return "default";
2222
2235
  }
@@ -2296,6 +2309,7 @@ function resolveWstackPaths(opts) {
2296
2309
  projectPlan: path2.join(projectDir, "plan.json"),
2297
2310
  projectAutophase: path2.join(projectDir, "autophase"),
2298
2311
  projectSddBoards: path2.join(projectDir, "sdd-boards"),
2312
+ projectRequirementIntakes: path2.join(projectDir, "requirement-intakes"),
2299
2313
  syncConfig: path2.join(profileDir, "sync.json"),
2300
2314
  configHistoryDir: path2.join(globalRoot, "config-history"),
2301
2315
  projectStatus: (projectHash2) => path2.join(globalRoot, "projects", projectHash2, "status.json")
@@ -7187,6 +7201,38 @@ var sshManagerServer = () => ({
7187
7201
  permission: "confirm",
7188
7202
  requestTimeoutMs: 18e4
7189
7203
  });
7204
+ var requirementIntakeServer = () => ({
7205
+ name: "requirement-intake",
7206
+ description: "WrongStack Requirements Intake \u2014 list intake records and file new ones (project-scoped, --writable)",
7207
+ transport: "stdio",
7208
+ command: "wstack-requirement-intake-mcp",
7209
+ args: ["--project-root", ".", "--writable"],
7210
+ permission: "auto"
7211
+ });
7212
+ var kanbanServer = () => ({
7213
+ name: "kanban",
7214
+ description: "WrongStack Kanban \u2014 inspect and manage project work boards (project-scoped, manage tier, no destructive ops)",
7215
+ transport: "stdio",
7216
+ command: "wstack-kanban-mcp",
7217
+ args: ["--project-root", ".", "--writable"],
7218
+ permission: "confirm"
7219
+ });
7220
+ var mailboxServer = () => ({
7221
+ name: "mailbox",
7222
+ description: "WrongStack Mailbox \u2014 read and send project agent mail (project-scoped, no admin/credentials)",
7223
+ transport: "stdio",
7224
+ command: "wstack-mailbox-mcp",
7225
+ args: ["--project-root", ".", "--actor", "external-agent", "--writable"],
7226
+ permission: "auto"
7227
+ });
7228
+ var codebaseIndexServer = () => ({
7229
+ name: "codebase-index",
7230
+ description: "WrongStack Codebase Index \u2014 symbol search and dependency graphs (project-scoped, --writable)",
7231
+ transport: "stdio",
7232
+ command: "wstack-codebase-index-mcp",
7233
+ args: ["--project-root", ".", "--writable"],
7234
+ permission: "auto"
7235
+ });
7190
7236
  var allServers = () => ({
7191
7237
  filesystem: { ...filesystemServer(), enabled: false },
7192
7238
  github: { ...githubServer(), enabled: false },
@@ -7201,7 +7247,11 @@ var allServers = () => ({
7201
7247
  "zai-vision": { ...zaiVisionServer(), enabled: false },
7202
7248
  "minimax-vision": { ...miniMaxVisionServer(), enabled: false },
7203
7249
  playwright: { ...playwrightServer(), enabled: false },
7204
- ssh: { ...sshManagerServer(), enabled: false }
7250
+ ssh: { ...sshManagerServer(), enabled: false },
7251
+ kanban: { ...kanbanServer(), enabled: false },
7252
+ mailbox: { ...mailboxServer(), enabled: false },
7253
+ "codebase-index": { ...codebaseIndexServer(), enabled: false },
7254
+ "requirement-intake": { ...requirementIntakeServer(), enabled: false }
7205
7255
  });
7206
7256
 
7207
7257
  // src/utils/config-json.ts
@@ -7788,7 +7838,8 @@ var OneShotOrchestrator = class {
7788
7838
  resolveTarget(input, config) {
7789
7839
  if (input.role && this.opts.modelRouter) {
7790
7840
  const pick = this.opts.modelRouter.pickForTask(input.role, "");
7791
- if (pick) {
7841
+ const hasExplicitTarget = Boolean(input.providerId || input.model);
7842
+ if (pick && (pick.fromMatrix === true || !hasExplicitTarget)) {
7792
7843
  return { providerId: pick.provider, model: pick.model };
7793
7844
  }
7794
7845
  }
@@ -170,8 +170,10 @@ export interface BrainModelEntry {
170
170
  export interface BrainCouncilVoterConfig extends BrainModelEntry {
171
171
  /**
172
172
  * Decision lens for this seat. Built-ins: 'executor' (progress-biased),
173
- * 'skeptic' (risk-hunting), 'auditor' (cost/waste-focused). Any other
174
- * string is injected verbatim as the persona description.
173
+ * 'skeptic' (risk-hunting), 'auditor' (cost/waste-focused), 'security'
174
+ * (trust boundaries, abuse cases), 'maintainer' (complexity, compatibility)
175
+ * and 'user-advocate' (usability, recovery). Any other string is registered
176
+ * as an ad-hoc lens whose instruction is the string itself.
175
177
  */
176
178
  persona?: string | undefined;
177
179
  /** Vote weight in the tally. Default 1. */
@@ -50,6 +50,21 @@ export interface ToolsConfig {
50
50
  * it is honored from any source.
51
51
  */
52
52
  exec?: ExecToolConfig | undefined;
53
+ /**
54
+ * Agent-callable `council` tool: which panel profile it runs by default and
55
+ * which extra lenses/profiles are available to it.
56
+ *
57
+ * Distinct from `brain.council`, which is the Brain's own decision tier —
58
+ * that one is convened by the Brain on high-risk questions, this one is
59
+ * invoked by the agent.
60
+ *
61
+ * SECURITY: in the in-project DENY list. A persona's `instruction` is
62
+ * rendered into the voter SYSTEM prompt, and a profile seat may pin a
63
+ * `providerId`/`model` — so a repo-committed config could otherwise inject
64
+ * system-level instructions into every seat and reroute the calls to an
65
+ * attacker-chosen provider. Only honoured from the active-profile config.
66
+ */
67
+ council?: CouncilToolConfig | undefined;
53
68
  /**
54
69
  * Agent-loop repetition detector tuning. The detector watches two signals:
55
70
  * consecutive effectively-identical iterations (same tool-name set + inputs
@@ -86,6 +101,90 @@ export interface LoopDetectionConfig {
86
101
  */
87
102
  callRepeatThreshold?: number | undefined;
88
103
  }
104
+ /**
105
+ * Configuration for the agent-callable `council` tool.
106
+ *
107
+ * The Council orchestrator has always accepted custom persona and profile
108
+ * registries, but no host ever built one — the tool was pinned to the three
109
+ * built-in profiles with no way to add a lens, change the default panel, or
110
+ * tune concurrency. This is that surface.
111
+ */
112
+ export interface CouncilToolConfig {
113
+ /**
114
+ * Profile the tool runs when a call names none. Must be a built-in id
115
+ * (`balanced`, `fast`, `risk-review`) or one defined in `profiles`.
116
+ * Default `balanced`.
117
+ */
118
+ defaultProfile?: string | undefined;
119
+ /** Seats polled concurrently, 1..8. Default 3. */
120
+ maxConcurrency?: number | undefined;
121
+ /**
122
+ * Extra decision lenses, registered alongside the six built-ins. Each needs
123
+ * a kebab-case `id`, a `name`, a `description` and the `instruction` that
124
+ * becomes the seat's system prompt.
125
+ */
126
+ personas?: CouncilPersonaDefinition[] | undefined;
127
+ /**
128
+ * Extra panel profiles, registered alongside the built-ins. A profile whose
129
+ * id matches a built-in replaces it.
130
+ */
131
+ profiles?: CouncilToolProfileDefinition[] | undefined;
132
+ }
133
+ /** A custom Council decision lens declared in configuration. */
134
+ export interface CouncilPersonaDefinition {
135
+ /** Kebab-case identifier, e.g. "latency-hawk". */
136
+ id: string;
137
+ name: string;
138
+ description: string;
139
+ /** Trusted system-level instruction for this lens. */
140
+ instruction: string;
141
+ defaultWeight?: number | undefined;
142
+ defaultVeto?: boolean | undefined;
143
+ tags?: string[] | undefined;
144
+ }
145
+ /**
146
+ * A custom Council panel declared in configuration.
147
+ *
148
+ * Structurally the config-facing half of `CouncilProfileConfig`; kept as its
149
+ * own type so the config surface stays JSON-shaped (no readonly arrays) and
150
+ * documents itself where users read it.
151
+ */
152
+ export interface CouncilToolProfileDefinition {
153
+ id: string;
154
+ name?: string | undefined;
155
+ description?: string | undefined;
156
+ seats: Array<{
157
+ id?: string | undefined;
158
+ label?: string | undefined;
159
+ /** Persona id — a built-in or one declared in `personas`. */
160
+ persona: string;
161
+ target?: {
162
+ providerId?: string | undefined;
163
+ model?: string | undefined;
164
+ /** Model-matrix role, resolved before the explicit target. */
165
+ role?: string | undefined;
166
+ fallbackProfile?: string | undefined;
167
+ fallbackModels?: string[] | undefined;
168
+ } | undefined;
169
+ weight?: number | undefined;
170
+ veto?: boolean | undefined;
171
+ }>;
172
+ /** `false` disables judging; omitted also means no judge. */
173
+ judge?: false | {
174
+ providerId?: string | undefined;
175
+ model?: string | undefined;
176
+ role?: string | undefined;
177
+ fallbackProfile?: string | undefined;
178
+ fallbackModels?: string[] | undefined;
179
+ } | undefined;
180
+ quorumFraction?: number | undefined;
181
+ approvalFraction?: number | undefined;
182
+ distinctness?: 'none' | 'model' | 'provider' | undefined;
183
+ voterMaxTokens?: number | undefined;
184
+ judgeMaxTokens?: number | undefined;
185
+ perCallTimeoutMs?: number | undefined;
186
+ overallTimeoutMs?: number | undefined;
187
+ }
89
188
  /** Allow/deny extension of the `exec` tool's built-in command allowlist. */
90
189
  export interface ExecToolConfig {
91
190
  /**
@@ -14,7 +14,7 @@ export type { CacheTtl, Capabilities, JsonSchemaSpec, Provider, ProviderErrorBod
14
14
  export { StreamHangError } from './provider.js';
15
15
  export type { ProviderRunner, RunProviderOptions } from './provider-runner.js';
16
16
  export { DEFAULT_TUI_THINKING_WORD, FLEET_CHAT_VERBOSITY_VALUES, MAX_TUI_THINKING_WORD_LENGTH, normalizeTokenSavingTier, normalizeTuiThinkingWord, resolveFleetChatVerbosity, resolveTokenSavingTier } from './config.js';
17
- export type { AdaptiveConcurrencyConfig, AutonomyConfig, BrainConfig, BrainCouncilConfig, BrainCouncilVoterConfig, BrainModelEntry, CircuitBreakerRuntimeConfig, ConcreteTokenSavingTier, Config, ConfigLoader, ConfigStore, ContextConfig, CustomModelDefinition, ExecDangerConfig, ExecToolConfig, FeaturesConfig, FleetChatVerbosity, FleetConfig, FleetSupervisorConfig, GitBehaviorConfig, HqClientConfig, IndexingConfig, InputHistoryConfig, LaunchConfig, LaunchMenuChoice, LogConfig, LoopDetectionConfig, MCPHealthConfig, MCPHealthThresholds, MCPServerConfig, ModelMatrixEntry, ModelRuntimeCacheConfig, ModelRuntimeConfig, ModelRuntimeParametersConfig, ModelRuntimeReasoningConfig, PluginConfig, PluginManagerConfig, ProviderApiKey, ProviderConfig, SageConfig, SessionLoggingConfig, SkillsConfig, SyncCategory, SyncConfig, TokenSavingTier, ToolDescriptionMode, ToolDescriptionModeConfig, ToolResultRenderMode, ToolResultRenderModeConfig, ToolsConfig } from './config.js';
17
+ export type { AdaptiveConcurrencyConfig, AutonomyConfig, BrainConfig, BrainCouncilConfig, BrainCouncilVoterConfig, BrainModelEntry, CircuitBreakerRuntimeConfig, ConcreteTokenSavingTier, Config, ConfigLoader, ConfigStore, ContextConfig, CouncilPersonaDefinition, CouncilToolConfig, CouncilToolProfileDefinition, CustomModelDefinition, ExecDangerConfig, ExecToolConfig, FeaturesConfig, FleetChatVerbosity, FleetConfig, FleetSupervisorConfig, GitBehaviorConfig, HqClientConfig, IndexingConfig, InputHistoryConfig, LaunchConfig, LaunchMenuChoice, LogConfig, LoopDetectionConfig, MCPHealthConfig, MCPHealthThresholds, MCPServerConfig, ModelMatrixEntry, ModelRuntimeCacheConfig, ModelRuntimeConfig, ModelRuntimeParametersConfig, ModelRuntimeReasoningConfig, PluginConfig, PluginManagerConfig, ProviderApiKey, ProviderConfig, SageConfig, SessionLoggingConfig, SkillsConfig, SyncCategory, SyncConfig, TokenSavingTier, ToolDescriptionMode, ToolDescriptionModeConfig, ToolResultRenderMode, ToolResultRenderModeConfig, ToolsConfig } from './config.js';
18
18
  export type { AnyHookOutcome, ConfiguredHook, HookEntry, HookEvent, HookFailurePolicy, HookInput, HookInvocationContext, HookMatcher, HookOutcome, HookRegistrationOptions, HttpHook, InProcessHook, PreToolUseOutcome, PreToolUseStage, ShellHook } from './hooks.js';
19
19
  export type { Compactor, CompactReport } from './compactor.js';
20
20
  export type { DirectoryPolicy, DirectoryRule, PermissionDecision, PermissionPolicy, PermissionTrace, PermissionTraceStep, TrustPolicy } from './permission.js';
@@ -50,6 +50,7 @@ export { CONTEXT_WINDOW_MODES, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEPRECATED_CONTEX
50
50
  export type { ContextSnapshot, ContextWindowAggressiveOn, ContextWindowConfigLike, ContextWindowMode, ContextWindowModeId, ContextWindowModeSelectionId, ContextWindowPolicy, ContextWindowThresholds, DeprecatedContextWindowModeId } from './context-window.js';
51
51
  export type { CompletedWorkEvidence, CompletedWorkSource, ContextEvidenceState, ContextFileEvidence, ContextIntentEvidence, ContextRepeatedReadEvidence, ToolEvidenceStatus, ToolOutputMetadata } from './context-evidence.js';
52
52
  export type { CouncilDistinctness, CouncilLLMCaller, CouncilModelTarget, CouncilOption, CouncilPersona, CouncilProfileConfig, CouncilQuestion, CouncilResolutionMethod, CouncilResult, CouncilSeatConfig, CouncilUsage, CouncilVoteResult, CouncilVoteStatus, ResolvedCouncilProfile, ResolvedCouncilSeat } from './council.js';
53
+ export type { OneShotLLMInput, OneShotLLMResult, OneShotModelPick, OneShotModelRouter, OneShotOrchestratorOptions } from './one-shot-llm.js';
53
54
  export type { AwaitAnyResult, CoordinatorEvents, CoordinatorStatus, DoneCondition, MultiAgentConfig, MultiAgentCoordinator, SpawnResult, SubagentConfig, SubagentContext, SubagentError, SubagentErrorKind, SubagentPartialResult, SubagentRunContext, SubagentRunOutcome, SubagentRunner, SubagentSpawnLineage, SubagentStructuredReport, TaskDelegation, TaskResult, TaskSpec } from './multi-agent.js';
54
55
  export { DEFAULT_SPEC_TEMPLATE } from './spec.js';
55
56
  export type { SpecAnalysis, SpecApiEndpoint, SpecRequirement, SpecSection, SpecSectionType, SpecStatus, SpecTemplate, SpecValidationResult, Specification } from './spec.js';
@@ -324,7 +324,7 @@ var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not eno
324
324
  function effectiveInputTokens(usage) {
325
325
  return usage.input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
326
326
  }
327
- var CONTEXT_OVERFLOW_RE = /context.length|context.window|maximum context|max.*tokens?.*exceeded|prompt is too long|too long|exceeds the context|\btokens\b.*exceed|too many tokens|reduce the length|resulted in \d+ tokens|input.{0,12}too (?:large|long)|context_length_exceeded/i;
327
+ var CONTEXT_OVERFLOW_RE = /context.length|context.window|maximum context|max.*tokens?.*exceeded|(?:prompt|request|input|messages?).{0,12}too (?:large|long)|exceeds the context|\btokens\b.*exceed|too many tokens|reduce the length|resulted in \d+ tokens|context_length_exceeded/i;
328
328
  var CONTENT_FILTER_RE = /content.(filter|policy|moderation)|safety (system|filter)/i;
329
329
  var RATE_LIMIT_EXCEEDED_RE = /rate[-_\s]*limit[-_\s]*exceeded/i;
330
330
  function classifyProviderError(status, body, message) {
@@ -334,7 +334,7 @@ function classifyProviderError(status, body, message) {
334
334
  if (status === 408) return "timeout";
335
335
  if (status === 599) return "stream_hang";
336
336
  if (status === 402 || QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
337
- if (status === 429 && body?.message && RATE_LIMIT_EXCEEDED_RE.test(body.message)) {
337
+ if (status === 429 && body?.message && body.type !== "rate_limit_exceeded" && RATE_LIMIT_EXCEEDED_RE.test(body.message)) {
338
338
  return "quota_exhausted";
339
339
  }
340
340
  if (type === "rate_limit_error" || status === 429) return "rate_limit";
@@ -411,7 +411,7 @@ var ProviderError = class extends WrongStackError {
411
411
  const e = err;
412
412
  const name = e.name;
413
413
  if (typeof name !== "string" || !name.endsWith("Error")) return false;
414
- return typeof e.status === "number" && typeof e.retryable === "boolean" && typeof e.kind === "string";
414
+ return typeof e.status === "number" && typeof e.retryable === "boolean" && typeof e.kind === "string" && typeof e.describe === "function";
415
415
  }
416
416
  constructor(message, status, retryable, providerId, opts = {}) {
417
417
  const kind = opts.kind ?? classifyProviderError(status, opts.body, message);
@@ -100,6 +100,21 @@ export interface OneShotLLMResult {
100
100
  */
101
101
  error?: string | undefined;
102
102
  }
103
+ /** One role→model resolution. Mirrors `ModelPick` from the ModelRouter. */
104
+ export interface OneShotModelPick {
105
+ provider: string;
106
+ model: string;
107
+ /**
108
+ * True when the pick came from the user's `/setmodel` matrix (explicit
109
+ * intent) rather than from capability heuristics. Only a matrix pick may
110
+ * override a caller's explicit `providerId`/`model`.
111
+ */
112
+ fromMatrix?: boolean | undefined;
113
+ }
114
+ /** Minimal router contract consumed by role-based routing. */
115
+ export interface OneShotModelRouter {
116
+ pickForTask(role: string, description: string): OneShotModelPick | undefined;
117
+ }
103
118
  /**
104
119
  * Dependencies for OneShotOrchestrator.
105
120
  */
@@ -119,10 +134,14 @@ export interface OneShotOrchestratorOptions {
119
134
  */
120
135
  fallbackProfileManager: import('../core/fallback-profile-manager.js').FallbackProfileManager;
121
136
  /**
122
- * Optional ModelRouter for role-based model selection.
123
- * When absent, role-based routing is skipped.
137
+ * Optional role→model router. When absent, role-based routing is skipped
138
+ * and `role` on an input is inert.
139
+ *
140
+ * Structurally typed rather than pinned to the concrete `ModelRouter` class
141
+ * so a host can supply a router rebuilt from the LIVE config on every call
142
+ * (the `/setmodel` matrix changes mid-session). `ModelRouter` satisfies it.
124
143
  */
125
- modelRouter?: import('../models/model-router.js').ModelRouter | undefined;
144
+ modelRouter?: OneShotModelRouter | undefined;
126
145
  /**
127
146
  * Optional logger for fallback warnings.
128
147
  */
@@ -1,6 +1,6 @@
1
1
  import type { ContentBlock } from './blocks.js';
2
2
  import type { Message } from './messages.js';
3
- import type { Usage } from './provider.js';
3
+ import type { ProviderErrorBody, Usage } from './provider.js';
4
4
  export interface SessionMetadata {
5
5
  id: string;
6
6
  title?: string | undefined;
@@ -295,6 +295,8 @@ export type SessionEvent = {
295
295
  delayMs: number;
296
296
  status?: number | undefined;
297
297
  description: string;
298
+ /** Scrubbed raw provider error envelope/body for post-run diagnosis. */
299
+ errorBody?: ProviderErrorBody | undefined;
298
300
  } | {
299
301
  type: 'provider_error';
300
302
  ts: string;
@@ -302,6 +304,8 @@ export type SessionEvent = {
302
304
  status?: number | undefined;
303
305
  description: string;
304
306
  retryable: boolean;
307
+ /** Scrubbed raw provider error envelope/body for post-run diagnosis. */
308
+ errorBody?: ProviderErrorBody | undefined;
305
309
  } | {
306
310
  type: 'checkpoint';
307
311
  ts: string;
@@ -2478,7 +2478,9 @@ function safeProfileName(name) {
2478
2478
  function activeProfileName(globalRoot) {
2479
2479
  try {
2480
2480
  const parsed = JSON.parse(fs3.readFileSync(path3.join(globalRoot, "config.json"), "utf8"));
2481
- return safeProfileName(typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0);
2481
+ return safeProfileName(
2482
+ typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0
2483
+ );
2482
2484
  } catch {
2483
2485
  return "default";
2484
2486
  }
@@ -2558,6 +2560,7 @@ function resolveWstackPaths(opts) {
2558
2560
  projectPlan: path3.join(projectDir, "plan.json"),
2559
2561
  projectAutophase: path3.join(projectDir, "autophase"),
2560
2562
  projectSddBoards: path3.join(projectDir, "sdd-boards"),
2563
+ projectRequirementIntakes: path3.join(projectDir, "requirement-intakes"),
2561
2564
  syncConfig: path3.join(profileDir, "sync.json"),
2562
2565
  configHistoryDir: path3.join(globalRoot, "config-history"),
2563
2566
  projectStatus: (projectHash2) => path3.join(globalRoot, "projects", projectHash2, "status.json")
@@ -124,6 +124,8 @@ export interface WstackPaths {
124
124
  projectAutophase: string;
125
125
  /** ~/.wrongstack/projects/<hash>/sdd-boards — live SDD board snapshots + JSONL event logs */
126
126
  projectSddBoards: string;
127
+ /** ~/.wrongstack/projects/<hash>/requirement-intakes — requirement intake records */
128
+ projectRequirementIntakes: string;
127
129
  /** ~/.wrongstack/profiles/<activeProfile>/sync.json — CloudSync configuration */
128
130
  syncConfig: string;
129
131
  /** ~/.wrongstack/config-history — timestamped backups on every config write */
@@ -1,5 +1,5 @@
1
1
  // src/worktree/worktree-manager.ts
2
- import { mkdir } from "node:fs/promises";
2
+ import { mkdir, readFile } from "node:fs/promises";
3
3
  import { join, resolve as resolve3, sep as sep3 } from "node:path";
4
4
 
5
5
  // src/utils/error.ts
@@ -523,6 +523,10 @@ ${merged.stderr}`);
523
523
  * Safe because SDD merges are serialized — no other commit lands in between.
524
524
  */
525
525
  async revertBaseTo(handle, sha) {
526
+ const status = await this.runGit(["status", "--porcelain"], this.projectRoot);
527
+ if (status.code !== 0) return false;
528
+ const hasTrackedDirt = status.stdout.split("\n").some((line) => line.trim().length > 0 && !line.startsWith("??"));
529
+ if (hasTrackedDirt) return false;
526
530
  const co = await this.runGit(["checkout", handle.baseBranch], this.projectRoot);
527
531
  if (co.code !== 0) return false;
528
532
  const reset = await this.runGit(["reset", "--hard", sha], this.projectRoot);
@@ -737,29 +741,47 @@ ${merged.stderr}`);
737
741
  /**
738
742
  * True when staged content still carries conflict markers.
739
743
  *
740
- * Primary probe: `git grep --cached` scans the STAGED blobs directly for a
741
- * full marker line (`<<<<<<< `, `=======`, `>>>>>>> `, `||||||| `) exit 0
742
- * means found. This is byte-level and independent of git version, locale,
743
- * and human-output phrasing. When the caller knows which files conflicted,
744
- * the scan is restricted to them so an unrelated `=======` underline in
745
- * some document can't false-positive.
744
+ * Reads the working-tree files directly and matches a full marker line
745
+ * (`<<<<<<< `, `=======`, `>>>>>>> `, `||||||| `) with a lenient regex
746
+ * (`{7,}`) that stays platform- and config-independent unlike the old
747
+ * `git diff --check` (only fires when `core.whitespace` opts in) and
748
+ * `git grep` (assumes the default 7-char markers).
749
+ *
750
+ * Scan set: when the caller supplies the conflicted paths (the normal
751
+ * path from {@link tryResolveConflict}), ONLY those files are scanned so
752
+ * an unrelated `=======` setext-heading underline in a staged document
753
+ * can't false-positive and discard valid resolution work. The staged-file
754
+ * listing (`git diff --cached --name-only`) is used only as a fallback
755
+ * when no paths are given.
746
756
  *
747
- * Fallback probe: `git diff --cached --check` prints a "leftover conflict
748
- * marker" line per survivor. Kept as belt-and-braces — it was the original
749
- * sole probe, but CI runners were observed to miss markers through it
750
- * (output parsing), which let a half-resolved merge commit.
757
+ * CRLF caveat: conflicted files checked out with CRLF (`core.autocrlf=true`,
758
+ * `.gitattributes eol=crlf`) carry bare markers as `=======\r\n`. Because
759
+ * `$` in multiline mode matches only before `\n`, carriage returns are
760
+ * stripped before matching so `\r` never defeats the end-of-line anchor.
751
761
  */
752
762
  async hasConflictMarkers(files) {
753
- const pathspec = files && files.length > 0 ? ["--", ...files] : [];
754
- const grep = await this.runGit(
755
- ["grep", "--cached", "-q", "-E", "^(<{7} |={7}$|>{7} |\\|{7} )", ...pathspec],
756
- this.projectRoot
757
- );
758
- if (grep.code === 0) return true;
759
- const check = await this.runGit(["diff", "--cached", "--check"], this.projectRoot);
760
- if (check.code === 0) return false;
761
- return /conflict marker/i.test(`${check.stdout}
762
- ${check.stderr}`);
763
+ const listed = files ?? [];
764
+ const paths = listed.length > 0 ? listed : (await this.runGit(["diff", "--cached", "--name-only", "-z"], this.projectRoot)).stdout.split("\0").filter(Boolean);
765
+ const marker = /^(?:<{7,}(?: |$)|={7,}$|>{7,}(?: |$)|\|{7,}(?: |$))/m;
766
+ const startMarker = /^(?:<{7,}(?: |$)|\|{7,}(?: |$))/m;
767
+ for (const rel of paths) {
768
+ try {
769
+ const content = (await readFile(join(this.projectRoot, rel), "utf8")).replace(/\r/g, "");
770
+ const lines = content.split("\n");
771
+ let seenStart = false;
772
+ for (const line of lines) {
773
+ if (!marker.test(line)) continue;
774
+ if (line.startsWith("=====")) {
775
+ if (!seenStart) continue;
776
+ } else if (startMarker.test(line)) {
777
+ seenStart = true;
778
+ }
779
+ return true;
780
+ }
781
+ } catch {
782
+ }
783
+ }
784
+ return false;
763
785
  }
764
786
  /**
765
787
  * Remove the worktree + branch. Conflicted/failed handles (or `keep:true`)
@@ -153,17 +153,23 @@ export declare class WorktreeManager {
153
153
  /**
154
154
  * True when staged content still carries conflict markers.
155
155
  *
156
- * Primary probe: `git grep --cached` scans the STAGED blobs directly for a
157
- * full marker line (`<<<<<<< `, `=======`, `>>>>>>> `, `||||||| `) exit 0
158
- * means found. This is byte-level and independent of git version, locale,
159
- * and human-output phrasing. When the caller knows which files conflicted,
160
- * the scan is restricted to them so an unrelated `=======` underline in
161
- * some document can't false-positive.
156
+ * Reads the working-tree files directly and matches a full marker line
157
+ * (`<<<<<<< `, `=======`, `>>>>>>> `, `||||||| `) with a lenient regex
158
+ * (`{7,}`) that stays platform- and config-independent unlike the old
159
+ * `git diff --check` (only fires when `core.whitespace` opts in) and
160
+ * `git grep` (assumes the default 7-char markers).
162
161
  *
163
- * Fallback probe: `git diff --cached --check` prints a "leftover conflict
164
- * marker" line per survivor. Kept as belt-and-braces it was the original
165
- * sole probe, but CI runners were observed to miss markers through it
166
- * (output parsing), which let a half-resolved merge commit.
162
+ * Scan set: when the caller supplies the conflicted paths (the normal
163
+ * path from {@link tryResolveConflict}), ONLY those files are scanned so
164
+ * an unrelated `=======` setext-heading underline in a staged document
165
+ * can't false-positive and discard valid resolution work. The staged-file
166
+ * listing (`git diff --cached --name-only`) is used only as a fallback
167
+ * when no paths are given.
168
+ *
169
+ * CRLF caveat: conflicted files checked out with CRLF (`core.autocrlf=true`,
170
+ * `.gitattributes eol=crlf`) carry bare markers as `=======\r\n`. Because
171
+ * `$` in multiline mode matches only before `\n`, carriage returns are
172
+ * stripped before matching so `\r` never defeats the end-of-line anchor.
167
173
  */
168
174
  private hasConflictMarkers;
169
175
  /**
@@ -40,6 +40,14 @@ deletion, history rewriting, force-push, or database destruction unless the
40
40
  task explicitly requires the action and identifies the exact target. Recheck
41
41
  the target before any destructive step.
42
42
 
43
+ ## Codebase discovery
44
+
45
+ When the index is available, use it before broad `grep`/`glob` exploration:
46
+ - `codebase-search` to locate symbols, definitions, and candidate modules.
47
+ - `codebase-incoming-calls` to find all callers of a symbol before refactoring.
48
+ - `codebase-outgoing-calls` to understand a symbol's dependencies.
49
+ Read source files returned by search before relying on them.
50
+
43
51
  ## No further delegation
44
52
 
45
53
  You MUST NOT call `delegate`, `spawn_subagent`, `assign_task`, or any equivalent.