@wrongstack/core 0.298.3 → 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
package/dist/index.js CHANGED
@@ -3207,7 +3207,9 @@ function safeProfileName(name) {
3207
3207
  function activeProfileName(globalRoot) {
3208
3208
  try {
3209
3209
  const parsed = JSON.parse(fs4.readFileSync(path5.join(globalRoot, "config.json"), "utf8"));
3210
- return safeProfileName(typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0);
3210
+ return safeProfileName(
3211
+ typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0
3212
+ );
3211
3213
  } catch {
3212
3214
  return "default";
3213
3215
  }
@@ -3287,6 +3289,7 @@ function resolveWstackPaths(opts) {
3287
3289
  projectPlan: path5.join(projectDir, "plan.json"),
3288
3290
  projectAutophase: path5.join(projectDir, "autophase"),
3289
3291
  projectSddBoards: path5.join(projectDir, "sdd-boards"),
3292
+ projectRequirementIntakes: path5.join(projectDir, "requirement-intakes"),
3290
3293
  syncConfig: path5.join(profileDir, "sync.json"),
3291
3294
  configHistoryDir: path5.join(globalRoot, "config-history"),
3292
3295
  projectStatus: (projectHash2) => path5.join(globalRoot, "projects", projectHash2, "status.json")
@@ -3795,6 +3798,15 @@ var IN_PROJECT_DENIED_PATHS = [
3795
3798
  reason: "Extends the exec allow-list; a repo could authorise its own binaries."
3796
3799
  },
3797
3800
  { path: "tools.exec.danger", reason: "Weakens the destructive-command banner." },
3801
+ {
3802
+ // The whole subtree, not just the dangerous leaves: a persona's
3803
+ // `instruction` is rendered into the voter SYSTEM prompt, a profile seat
3804
+ // may pin providerId/model, and `defaultProfile` selects which of those
3805
+ // runs when the agent names no profile. Denying the parent leaves no leaf
3806
+ // to reclassify wrongly later.
3807
+ path: "tools.council",
3808
+ reason: "Council tool panel definitions: persona instructions are injected into the voter SYSTEM prompt and profile seats can pin an attacker-chosen providerId/model."
3809
+ },
3798
3810
  { path: "skills.extraDirs", reason: "Loads skill definitions from repo-chosen directories." },
3799
3811
  { path: "skills.registryUrl", reason: "Redirects skill installs to a repo-chosen host." },
3800
3812
  // Deliberately NOT denied: skills.mode and skills.eagerMaxChars. The audit
@@ -29664,7 +29676,7 @@ var ROUTE_SCOPED_QUOTA_RE = /\b(?:for|on)\s+(?:(?:this|the)\s+)?(?:route|model)\
29664
29676
  function effectiveInputTokens(usage) {
29665
29677
  return usage.input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
29666
29678
  }
29667
- 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;
29679
+ 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;
29668
29680
  var CONTENT_FILTER_RE = /content.(filter|policy|moderation)|safety (system|filter)/i;
29669
29681
  var RATE_LIMIT_EXCEEDED_RE = /rate[-_\s]*limit[-_\s]*exceeded/i;
29670
29682
  function classifyProviderError(status, body, message) {
@@ -29674,7 +29686,7 @@ function classifyProviderError(status, body, message) {
29674
29686
  if (status === 408) return "timeout";
29675
29687
  if (status === 599) return "stream_hang";
29676
29688
  if (status === 402 || QUOTA_EXHAUSTED_RE.test(text2)) return "quota_exhausted";
29677
- if (status === 429 && body?.message && RATE_LIMIT_EXCEEDED_RE.test(body.message)) {
29689
+ if (status === 429 && body?.message && body.type !== "rate_limit_exceeded" && RATE_LIMIT_EXCEEDED_RE.test(body.message)) {
29678
29690
  return "quota_exhausted";
29679
29691
  }
29680
29692
  if (type === "rate_limit_error" || status === 429) return "rate_limit";
@@ -29810,7 +29822,7 @@ var ProviderError = class extends WrongStackError {
29810
29822
  const e = err;
29811
29823
  const name = e.name;
29812
29824
  if (typeof name !== "string" || !name.endsWith("Error")) return false;
29813
- return typeof e.status === "number" && typeof e.retryable === "boolean" && typeof e.kind === "string";
29825
+ return typeof e.status === "number" && typeof e.retryable === "boolean" && typeof e.kind === "string" && typeof e.describe === "function";
29814
29826
  }
29815
29827
  constructor(message, status, retryable, providerId, opts = {}) {
29816
29828
  const kind = opts.kind ?? classifyProviderError(status, opts.body, message);
@@ -47391,7 +47403,8 @@ var KNOWN_HQ_CLIENT_FRAME_TYPES = /* @__PURE__ */ new Set([
47391
47403
  "client.event",
47392
47404
  "client.command_poll",
47393
47405
  "client.command_ack",
47394
- "client.resume"
47406
+ "client.resume",
47407
+ "client.event_poll"
47395
47408
  ]);
47396
47409
  var HQ_CLIENT_KINDS = /* @__PURE__ */ new Set([
47397
47410
  "tui",
@@ -49918,6 +49931,16 @@ function hash3(value) {
49918
49931
 
49919
49932
  // src/core/provider-runner.ts
49920
49933
  import { randomUUID as randomUUID31 } from "node:crypto";
49934
+ function scrubProviderBody(body) {
49935
+ if (!body) return void 0;
49936
+ return {
49937
+ ...body,
49938
+ ...body.type !== void 0 ? { type: scrubErrorText(body.type) } : {},
49939
+ ...body.message !== void 0 ? { message: scrubErrorText(body.message) } : {},
49940
+ ...body.raw !== void 0 ? { raw: scrubErrorText(body.raw) } : {},
49941
+ ...body.requestId !== void 0 ? { requestId: scrubErrorText(body.requestId) } : {}
49942
+ };
49943
+ }
49921
49944
  function providerLogCtx(p, r) {
49922
49945
  return {
49923
49946
  providerId: p.id,
@@ -50002,7 +50025,10 @@ async function runProviderWithRetry(opts) {
50002
50025
  const isProviderErr = err instanceof ProviderError || ProviderError.isProviderError(err);
50003
50026
  const errAsErr = err instanceof Error ? err : new Error(String(err));
50004
50027
  const canRetry = retry.shouldRetry(isProviderErr ? err : errAsErr, attempt);
50005
- const description = isProviderErr ? err.describe() : errAsErr.message;
50028
+ const providerErrorBody = isProviderErr ? scrubProviderBody(err.body) : void 0;
50029
+ const description = scrubErrorText(
50030
+ isProviderErr ? err.describe() : errAsErr.message
50031
+ );
50006
50032
  const delay3 = canRetry ? Math.round(retry.delayMs(attempt, isProviderErr ? err : errAsErr)) : void 0;
50007
50033
  events.emit("provider.attempt.failed", {
50008
50034
  ...correlation,
@@ -50015,7 +50041,8 @@ async function runProviderWithRetry(opts) {
50015
50041
  retryable: canRetry,
50016
50042
  retryScheduled: canRetry,
50017
50043
  ...delay3 !== void 0 ? { retryDelayMs: delay3 } : {},
50018
- ...isProviderErr && err.body?.requestId ? { providerRequestId: err.body.requestId } : {}
50044
+ ...providerErrorBody?.requestId ? { providerRequestId: providerErrorBody.requestId } : {},
50045
+ ...providerErrorBody ? { errorBody: providerErrorBody } : {}
50019
50046
  });
50020
50047
  if (!canRetry) {
50021
50048
  events.emit("provider.error", {
@@ -50023,13 +50050,15 @@ async function runProviderWithRetry(opts) {
50023
50050
  providerId: isProviderErr ? err.providerId : provider.id,
50024
50051
  status: isProviderErr ? err.status : 0,
50025
50052
  description,
50026
- retryable: false
50053
+ retryable: false,
50054
+ ...providerErrorBody ? { errorBody: providerErrorBody } : {}
50027
50055
  });
50028
50056
  logger.error(`Provider call failed after ${attempt + 1} attempt(s) \u2014 ${description}`, {
50029
50057
  ...providerLogCtx(provider, request),
50030
50058
  attempts: attempt + 1,
50031
50059
  errorDescription: description,
50032
50060
  status: isProviderErr ? err.status : void 0,
50061
+ errorBody: providerErrorBody,
50033
50062
  errorName: err instanceof Error ? err.name : void 0,
50034
50063
  errorStack: err instanceof Error ? err.stack?.split("\n").slice(0, 3).join("\n") : void 0
50035
50064
  });
@@ -50043,7 +50072,8 @@ async function runProviderWithRetry(opts) {
50043
50072
  maxAttempts,
50044
50073
  delayMs: delay3,
50045
50074
  errorDescription: description,
50046
- status: isProviderErr ? err.status : void 0
50075
+ status: isProviderErr ? err.status : void 0,
50076
+ errorBody: providerErrorBody
50047
50077
  });
50048
50078
  events.emit("provider.retry", {
50049
50079
  sessionId: resolveEventSessionId(ctx),
@@ -50051,7 +50081,8 @@ async function runProviderWithRetry(opts) {
50051
50081
  attempt: attemptNum,
50052
50082
  delayMs: delay3,
50053
50083
  status: isProviderErr ? err.status : 0,
50054
- description
50084
+ description,
50085
+ ...providerErrorBody ? { errorBody: providerErrorBody } : {}
50055
50086
  });
50056
50087
  await new Promise((resolve50, reject) => {
50057
50088
  let settled = false;
@@ -60665,6 +60696,38 @@ var sshManagerServer = () => ({
60665
60696
  permission: "confirm",
60666
60697
  requestTimeoutMs: 18e4
60667
60698
  });
60699
+ var requirementIntakeServer = () => ({
60700
+ name: "requirement-intake",
60701
+ description: "WrongStack Requirements Intake \u2014 list intake records and file new ones (project-scoped, --writable)",
60702
+ transport: "stdio",
60703
+ command: "wstack-requirement-intake-mcp",
60704
+ args: ["--project-root", ".", "--writable"],
60705
+ permission: "auto"
60706
+ });
60707
+ var kanbanServer = () => ({
60708
+ name: "kanban",
60709
+ description: "WrongStack Kanban \u2014 inspect and manage project work boards (project-scoped, manage tier, no destructive ops)",
60710
+ transport: "stdio",
60711
+ command: "wstack-kanban-mcp",
60712
+ args: ["--project-root", ".", "--writable"],
60713
+ permission: "confirm"
60714
+ });
60715
+ var mailboxServer = () => ({
60716
+ name: "mailbox",
60717
+ description: "WrongStack Mailbox \u2014 read and send project agent mail (project-scoped, no admin/credentials)",
60718
+ transport: "stdio",
60719
+ command: "wstack-mailbox-mcp",
60720
+ args: ["--project-root", ".", "--actor", "external-agent", "--writable"],
60721
+ permission: "auto"
60722
+ });
60723
+ var codebaseIndexServer = () => ({
60724
+ name: "codebase-index",
60725
+ description: "WrongStack Codebase Index \u2014 symbol search and dependency graphs (project-scoped, --writable)",
60726
+ transport: "stdio",
60727
+ command: "wstack-codebase-index-mcp",
60728
+ args: ["--project-root", ".", "--writable"],
60729
+ permission: "auto"
60730
+ });
60668
60731
  var allServers = () => ({
60669
60732
  filesystem: { ...filesystemServer(), enabled: false },
60670
60733
  github: { ...githubServer(), enabled: false },
@@ -60679,7 +60742,11 @@ var allServers = () => ({
60679
60742
  "zai-vision": { ...zaiVisionServer(), enabled: false },
60680
60743
  "minimax-vision": { ...miniMaxVisionServer(), enabled: false },
60681
60744
  playwright: { ...playwrightServer(), enabled: false },
60682
- ssh: { ...sshManagerServer(), enabled: false }
60745
+ ssh: { ...sshManagerServer(), enabled: false },
60746
+ kanban: { ...kanbanServer(), enabled: false },
60747
+ mailbox: { ...mailboxServer(), enabled: false },
60748
+ "codebase-index": { ...codebaseIndexServer(), enabled: false },
60749
+ "requirement-intake": { ...requirementIntakeServer(), enabled: false }
60683
60750
  });
60684
60751
 
60685
60752
  // src/models/codex-catalog.ts
@@ -65416,6 +65483,9 @@ var BUILTIN_COUNCIL_PERSONAS = Object.freeze([
65416
65483
  tags: ["users", "usability", "accessibility"]
65417
65484
  })
65418
65485
  ]);
65486
+ var BUILTIN_COUNCIL_PERSONA_IDS = Object.freeze(
65487
+ BUILTIN_COUNCIL_PERSONAS.map((persona) => persona.id)
65488
+ );
65419
65489
  var CouncilPersonaRegistry = class _CouncilPersonaRegistry {
65420
65490
  byId;
65421
65491
  constructor(personas = []) {
@@ -66537,6 +66607,44 @@ function errorMessage3(error2) {
66537
66607
  }
66538
66608
 
66539
66609
  // src/execution/council-brain.ts
66610
+ var MAX_CUSTOM_PERSONA_ID_CHARS = 48;
66611
+ function customPersonaId(raw, index) {
66612
+ const slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, MAX_CUSTOM_PERSONA_ID_CHARS).replace(/-+$/g, "");
66613
+ return slug ? `custom-${slug}` : `custom-${index + 1}`;
66614
+ }
66615
+ function resolvePersonaRegistry(voters) {
66616
+ let registry2 = DEFAULT_COUNCIL_PERSONA_REGISTRY;
66617
+ const personaIds = [];
66618
+ const byRawText = /* @__PURE__ */ new Map();
66619
+ voters.forEach((voter, index) => {
66620
+ const raw = voter.persona?.trim();
66621
+ if (!raw) {
66622
+ personaIds.push("executor");
66623
+ return;
66624
+ }
66625
+ if (registry2.has(raw)) {
66626
+ personaIds.push(raw);
66627
+ return;
66628
+ }
66629
+ const existing = byRawText.get(raw);
66630
+ if (existing) {
66631
+ personaIds.push(existing);
66632
+ return;
66633
+ }
66634
+ let id = customPersonaId(raw, index);
66635
+ let suffix = 2;
66636
+ while (registry2.has(id)) id = `${customPersonaId(raw, index)}-${suffix++}`;
66637
+ registry2 = registry2.with({
66638
+ id,
66639
+ name: raw.length <= 40 ? raw : `${raw.slice(0, 39)}\u2026`,
66640
+ description: "Custom decision lens supplied by the host configuration.",
66641
+ instruction: raw
66642
+ });
66643
+ byRawText.set(raw, id);
66644
+ personaIds.push(id);
66645
+ });
66646
+ return { registry: registry2, personaIds };
66647
+ }
66540
66648
  function createCouncilBrainArbiter(opts) {
66541
66649
  if (opts.voters.length === 0) {
66542
66650
  throw new Error("createCouncilBrainArbiter: at least one voter is required.");
@@ -66602,14 +66710,15 @@ function createCouncilBrainArbiter(opts) {
66602
66710
  }
66603
66711
  return makeSeatCallerForVoter(voter);
66604
66712
  };
66713
+ const { registry: personas, personaIds } = resolvePersonaRegistry(opts.voters);
66605
66714
  const seats = opts.voters.map((voter, i) => ({
66606
66715
  id: `voter-${i}`,
66607
66716
  label: voter.label ?? voter.model,
66608
- persona: voter.persona ?? "executor",
66717
+ persona: personaIds[i] ?? "executor",
66609
66718
  target: {
66610
66719
  providerId: voter.provider.id,
66611
66720
  model: voter.model,
66612
- role: voter.persona ?? "executor"
66721
+ role: personaIds[i] ?? "executor"
66613
66722
  },
66614
66723
  weight: voter.weight,
66615
66724
  veto: voter.veto
@@ -66631,6 +66740,9 @@ function createCouncilBrainArbiter(opts) {
66631
66740
  };
66632
66741
  const orchestrator = new CouncilOrchestrator({
66633
66742
  defaultProfile: "brain-council-adapter",
66743
+ // Carries the ad-hoc lenses registered above; without it the orchestrator
66744
+ // falls back to the built-in registry and rejects every custom persona.
66745
+ personas,
66634
66746
  // Previously never passed, so the panel was pinned at the orchestrator's
66635
66747
  // default of 3 concurrent seats no matter how many voters were configured.
66636
66748
  ...opts.maxConcurrency !== void 0 ? { maxConcurrency: opts.maxConcurrency } : {},
@@ -67984,7 +68096,8 @@ var OneShotOrchestrator = class {
67984
68096
  resolveTarget(input, config) {
67985
68097
  if (input.role && this.opts.modelRouter) {
67986
68098
  const pick = this.opts.modelRouter.pickForTask(input.role, "");
67987
- if (pick) {
68099
+ const hasExplicitTarget = Boolean(input.providerId || input.model);
68100
+ if (pick && (pick.fromMatrix === true || !hasExplicitTarget)) {
67988
68101
  return { providerId: pick.provider, model: pick.model };
67989
68102
  }
67990
68103
  }
@@ -71265,18 +71378,26 @@ function parseHookOutcome(stdout) {
71265
71378
  // src/hooks/http-executor.ts
71266
71379
  var DEFAULT_TIMEOUT_MS6 = 5e3;
71267
71380
  var MAX_OUTPUT_BYTES2 = 64 * 1024;
71268
- function isAllowedUrl(raw) {
71381
+ async function isAllowedUrl(raw) {
71382
+ let url;
71269
71383
  try {
71270
- const url = new URL(raw);
71271
- if (url.protocol === "https:") return true;
71272
- if (url.protocol !== "http:") return false;
71273
- return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
71384
+ url = new URL(raw);
71274
71385
  } catch {
71275
71386
  return false;
71276
71387
  }
71388
+ if (url.protocol === "https:") {
71389
+ try {
71390
+ await assertNotPrivateHost(url.hostname);
71391
+ return true;
71392
+ } catch {
71393
+ return false;
71394
+ }
71395
+ }
71396
+ if (url.protocol !== "http:") return false;
71397
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
71277
71398
  }
71278
71399
  async function runHttpHookDetailed(spec, input, logger, options = {}) {
71279
- if (!isAllowedUrl(spec.url)) {
71400
+ if (!await isAllowedUrl(spec.url)) {
71280
71401
  return {
71281
71402
  outcome: null,
71282
71403
  failure: {
@@ -74070,6 +74191,7 @@ var HqBootstrapCodeStore = class {
74070
74191
  };
74071
74192
 
74072
74193
  // src/hq/exposure.ts
74194
+ import { isIP as isIP2 } from "node:net";
74073
74195
  var HQ_CLI_DEFAULT_HOST = "0.0.0.0";
74074
74196
  var HqInsecureExposureError = class extends Error {
74075
74197
  constructor(message) {
@@ -74079,8 +74201,15 @@ var HqInsecureExposureError = class extends Error {
74079
74201
  };
74080
74202
  function isLoopbackHost(host) {
74081
74203
  const normalized = host.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "");
74082
- if (normalized === "localhost" || normalized === "::1") return true;
74083
- return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(normalized);
74204
+ if (normalized === "localhost") return true;
74205
+ const family = isIP2(normalized);
74206
+ if (family === 4) {
74207
+ return normalized.startsWith("127.");
74208
+ }
74209
+ if (family === 6) {
74210
+ return normalized === "::1" || /^::ffff:127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(normalized);
74211
+ }
74212
+ return false;
74084
74213
  }
74085
74214
  function isOpenMode(input) {
74086
74215
  return !input.hasBrowserTokens && !input.hasPassword;
@@ -85082,7 +85211,7 @@ var INPUT_SCHEMA = {
85082
85211
  },
85083
85212
  profile: {
85084
85213
  type: "string",
85085
- description: "Registered Council profile id. Defaults to the host-configured profile."
85214
+ description: 'Registered Council profile id (e.g. "balanced", "fast", "risk-review"). Defaults to the host-configured profile.'
85086
85215
  }
85087
85216
  },
85088
85217
  required: ["question"],
@@ -85093,10 +85222,12 @@ function createCouncilTool(opts) {
85093
85222
  ...opts,
85094
85223
  fallbackProfileManager: opts.fallbackProfileManager
85095
85224
  });
85225
+ const profiles = opts.profiles ?? DEFAULT_COUNCIL_PROFILE_REGISTRY;
85226
+ const profileIds = profiles.list().map((profile) => profile.id);
85096
85227
  return {
85097
85228
  name: COUNCIL_TOOL_NAME,
85098
85229
  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.",
85099
- 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.",
85230
+ 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(", ")}.`,
85100
85231
  category: "meta",
85101
85232
  inputSchema: INPUT_SCHEMA,
85102
85233
  permission: "auto",
@@ -85114,11 +85245,17 @@ function createCouncilTool(opts) {
85114
85245
  };
85115
85246
  return orchestrator.ask(question);
85116
85247
  },
85117
- validate: validateCouncilToolInput
85248
+ validate: (input) => validateCouncilToolInput(input, profileIds)
85118
85249
  };
85119
85250
  }
85120
- function validateCouncilToolInput(input) {
85251
+ function validateCouncilToolInput(input, profileIds) {
85121
85252
  const errors = [];
85253
+ if (typeof input.profile === "string" && input.profile.trim()) {
85254
+ const id = input.profile.trim();
85255
+ if (!profileIds.includes(id)) {
85256
+ errors.push(`Unknown \`profile\` "${id}". Available: ${profileIds.join(", ")}.`);
85257
+ }
85258
+ }
85122
85259
  const question = input.question?.trim() ?? "";
85123
85260
  if (!question) errors.push("`question` must not be empty.");
85124
85261
  if (question.length > MAX_COUNCIL_QUESTION_CHARS) {
@@ -87075,7 +87212,7 @@ var DEFAULT_SPEC_TEMPLATE = {
87075
87212
 
87076
87213
  // src/worktree/worktree-manager.ts
87077
87214
  init_error();
87078
- import { mkdir as mkdir31 } from "node:fs/promises";
87215
+ import { mkdir as mkdir31, readFile as readFile66 } from "node:fs/promises";
87079
87216
  import { join as join85, resolve as resolve49, sep as sep8 } from "node:path";
87080
87217
 
87081
87218
  // src/worktree/worktree-git.ts
@@ -87456,6 +87593,10 @@ ${merged.stderr}`);
87456
87593
  * Safe because SDD merges are serialized — no other commit lands in between.
87457
87594
  */
87458
87595
  async revertBaseTo(handle, sha) {
87596
+ const status = await this.runGit(["status", "--porcelain"], this.projectRoot);
87597
+ if (status.code !== 0) return false;
87598
+ const hasTrackedDirt = status.stdout.split("\n").some((line) => line.trim().length > 0 && !line.startsWith("??"));
87599
+ if (hasTrackedDirt) return false;
87459
87600
  const co = await this.runGit(["checkout", handle.baseBranch], this.projectRoot);
87460
87601
  if (co.code !== 0) return false;
87461
87602
  const reset = await this.runGit(["reset", "--hard", sha], this.projectRoot);
@@ -87670,29 +87811,47 @@ ${merged.stderr}`);
87670
87811
  /**
87671
87812
  * True when staged content still carries conflict markers.
87672
87813
  *
87673
- * Primary probe: `git grep --cached` scans the STAGED blobs directly for a
87674
- * full marker line (`<<<<<<< `, `=======`, `>>>>>>> `, `||||||| `) exit 0
87675
- * means found. This is byte-level and independent of git version, locale,
87676
- * and human-output phrasing. When the caller knows which files conflicted,
87677
- * the scan is restricted to them so an unrelated `=======` underline in
87678
- * some document can't false-positive.
87814
+ * Reads the working-tree files directly and matches a full marker line
87815
+ * (`<<<<<<< `, `=======`, `>>>>>>> `, `||||||| `) with a lenient regex
87816
+ * (`{7,}`) that stays platform- and config-independent unlike the old
87817
+ * `git diff --check` (only fires when `core.whitespace` opts in) and
87818
+ * `git grep` (assumes the default 7-char markers).
87819
+ *
87820
+ * Scan set: when the caller supplies the conflicted paths (the normal
87821
+ * path from {@link tryResolveConflict}), ONLY those files are scanned so
87822
+ * an unrelated `=======` setext-heading underline in a staged document
87823
+ * can't false-positive and discard valid resolution work. The staged-file
87824
+ * listing (`git diff --cached --name-only`) is used only as a fallback
87825
+ * when no paths are given.
87679
87826
  *
87680
- * Fallback probe: `git diff --cached --check` prints a "leftover conflict
87681
- * marker" line per survivor. Kept as belt-and-braces — it was the original
87682
- * sole probe, but CI runners were observed to miss markers through it
87683
- * (output parsing), which let a half-resolved merge commit.
87827
+ * CRLF caveat: conflicted files checked out with CRLF (`core.autocrlf=true`,
87828
+ * `.gitattributes eol=crlf`) carry bare markers as `=======\r\n`. Because
87829
+ * `$` in multiline mode matches only before `\n`, carriage returns are
87830
+ * stripped before matching so `\r` never defeats the end-of-line anchor.
87684
87831
  */
87685
87832
  async hasConflictMarkers(files) {
87686
- const pathspec = files && files.length > 0 ? ["--", ...files] : [];
87687
- const grep = await this.runGit(
87688
- ["grep", "--cached", "-q", "-E", "^(<{7} |={7}$|>{7} |\\|{7} )", ...pathspec],
87689
- this.projectRoot
87690
- );
87691
- if (grep.code === 0) return true;
87692
- const check = await this.runGit(["diff", "--cached", "--check"], this.projectRoot);
87693
- if (check.code === 0) return false;
87694
- return /conflict marker/i.test(`${check.stdout}
87695
- ${check.stderr}`);
87833
+ const listed = files ?? [];
87834
+ const paths = listed.length > 0 ? listed : (await this.runGit(["diff", "--cached", "--name-only", "-z"], this.projectRoot)).stdout.split("\0").filter(Boolean);
87835
+ const marker = /^(?:<{7,}(?: |$)|={7,}$|>{7,}(?: |$)|\|{7,}(?: |$))/m;
87836
+ const startMarker = /^(?:<{7,}(?: |$)|\|{7,}(?: |$))/m;
87837
+ for (const rel of paths) {
87838
+ try {
87839
+ const content = (await readFile66(join85(this.projectRoot, rel), "utf8")).replace(/\r/g, "");
87840
+ const lines = content.split("\n");
87841
+ let seenStart = false;
87842
+ for (const line of lines) {
87843
+ if (!marker.test(line)) continue;
87844
+ if (line.startsWith("=====")) {
87845
+ if (!seenStart) continue;
87846
+ } else if (startMarker.test(line)) {
87847
+ seenStart = true;
87848
+ }
87849
+ return true;
87850
+ }
87851
+ } catch {
87852
+ }
87853
+ }
87854
+ return false;
87696
87855
  }
87697
87856
  /**
87698
87857
  * Remove the worktree + branch. Conflicted/failed handles (or `keep:true`)
@@ -87826,6 +87985,7 @@ export {
87826
87985
  BUG_HUNTER_AGENT,
87827
87986
  BUILD_AGENTS,
87828
87987
  BUILTIN_COUNCIL_PERSONAS,
87988
+ BUILTIN_COUNCIL_PERSONA_IDS,
87829
87989
  BUILTIN_COUNCIL_PROFILES,
87830
87990
  BUILTIN_PROMPT_CATEGORIES,
87831
87991
  BrainCircuitBreaker,
@@ -1397,6 +1397,38 @@ var sshManagerServer = () => ({
1397
1397
  permission: "confirm",
1398
1398
  requestTimeoutMs: 18e4
1399
1399
  });
1400
+ var requirementIntakeServer = () => ({
1401
+ name: "requirement-intake",
1402
+ description: "WrongStack Requirements Intake \u2014 list intake records and file new ones (project-scoped, --writable)",
1403
+ transport: "stdio",
1404
+ command: "wstack-requirement-intake-mcp",
1405
+ args: ["--project-root", ".", "--writable"],
1406
+ permission: "auto"
1407
+ });
1408
+ var kanbanServer = () => ({
1409
+ name: "kanban",
1410
+ description: "WrongStack Kanban \u2014 inspect and manage project work boards (project-scoped, manage tier, no destructive ops)",
1411
+ transport: "stdio",
1412
+ command: "wstack-kanban-mcp",
1413
+ args: ["--project-root", ".", "--writable"],
1414
+ permission: "confirm"
1415
+ });
1416
+ var mailboxServer = () => ({
1417
+ name: "mailbox",
1418
+ description: "WrongStack Mailbox \u2014 read and send project agent mail (project-scoped, no admin/credentials)",
1419
+ transport: "stdio",
1420
+ command: "wstack-mailbox-mcp",
1421
+ args: ["--project-root", ".", "--actor", "external-agent", "--writable"],
1422
+ permission: "auto"
1423
+ });
1424
+ var codebaseIndexServer = () => ({
1425
+ name: "codebase-index",
1426
+ description: "WrongStack Codebase Index \u2014 symbol search and dependency graphs (project-scoped, --writable)",
1427
+ transport: "stdio",
1428
+ command: "wstack-codebase-index-mcp",
1429
+ args: ["--project-root", ".", "--writable"],
1430
+ permission: "auto"
1431
+ });
1400
1432
  var allServers = () => ({
1401
1433
  filesystem: { ...filesystemServer(), enabled: false },
1402
1434
  github: { ...githubServer(), enabled: false },
@@ -1411,7 +1443,11 @@ var allServers = () => ({
1411
1443
  "zai-vision": { ...zaiVisionServer(), enabled: false },
1412
1444
  "minimax-vision": { ...miniMaxVisionServer(), enabled: false },
1413
1445
  playwright: { ...playwrightServer(), enabled: false },
1414
- ssh: { ...sshManagerServer(), enabled: false }
1446
+ ssh: { ...sshManagerServer(), enabled: false },
1447
+ kanban: { ...kanbanServer(), enabled: false },
1448
+ mailbox: { ...mailboxServer(), enabled: false },
1449
+ "codebase-index": { ...codebaseIndexServer(), enabled: false },
1450
+ "requirement-intake": { ...requirementIntakeServer(), enabled: false }
1415
1451
  });
1416
1452
 
1417
1453
  // src/utils/expect-defined.ts
@@ -2900,7 +2936,9 @@ function safeProfileName(name) {
2900
2936
  function activeProfileName(globalRoot) {
2901
2937
  try {
2902
2938
  const parsed = JSON.parse(fs4.readFileSync(path5.join(globalRoot, "config.json"), "utf8"));
2903
- return safeProfileName(typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0);
2939
+ return safeProfileName(
2940
+ typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0
2941
+ );
2904
2942
  } catch {
2905
2943
  return "default";
2906
2944
  }
@@ -2980,6 +3018,7 @@ function resolveWstackPaths(opts) {
2980
3018
  projectPlan: path5.join(projectDir, "plan.json"),
2981
3019
  projectAutophase: path5.join(projectDir, "autophase"),
2982
3020
  projectSddBoards: path5.join(projectDir, "sdd-boards"),
3021
+ projectRequirementIntakes: path5.join(projectDir, "requirement-intakes"),
2983
3022
  syncConfig: path5.join(profileDir, "sync.json"),
2984
3023
  configHistoryDir: path5.join(globalRoot, "config-history"),
2985
3024
  projectStatus: (projectHash2) => path5.join(globalRoot, "projects", projectHash2, "status.json")
@@ -3413,6 +3452,15 @@ var IN_PROJECT_DENIED_PATHS = [
3413
3452
  reason: "Extends the exec allow-list; a repo could authorise its own binaries."
3414
3453
  },
3415
3454
  { path: "tools.exec.danger", reason: "Weakens the destructive-command banner." },
3455
+ {
3456
+ // The whole subtree, not just the dangerous leaves: a persona's
3457
+ // `instruction` is rendered into the voter SYSTEM prompt, a profile seat
3458
+ // may pin providerId/model, and `defaultProfile` selects which of those
3459
+ // runs when the agent names no profile. Denying the parent leaves no leaf
3460
+ // to reclassify wrongly later.
3461
+ path: "tools.council",
3462
+ reason: "Council tool panel definitions: persona instructions are injected into the voter SYSTEM prompt and profile seats can pin an attacker-chosen providerId/model."
3463
+ },
3416
3464
  { path: "skills.extraDirs", reason: "Loads skill definitions from repo-chosen directories." },
3417
3465
  { path: "skills.registryUrl", reason: "Redirects skill installs to a repo-chosen host." },
3418
3466
  // Deliberately NOT denied: skills.mode and skills.eagerMaxChars. The audit
@@ -78,6 +78,41 @@ export declare const miniMaxVisionServer: () => MCPServerConfig;
78
78
  * env/TOML config (for example SSH_SERVER_<NAME>_HOST, USER, KEYPATH/PASSWORD) or ssh-agent.
79
79
  */
80
80
  export declare const sshManagerServer: () => MCPServerConfig;
81
+ /**
82
+ * WrongStack Requirements Intake — list intake records and file + submit new
83
+ * ones against the project's intake store (`wstack-requirement-intake-mcp`).
84
+ * Project-scoped: resolves the project from the spawn cwd (`--project-root .`);
85
+ * customize the arg for absolute paths. Tools are structured domain tools
86
+ * (list/submit) with no file, shell, or network surface, so the permission
87
+ * defaults to `auto`.
88
+ */
89
+ export declare const requirementIntakeServer: () => MCPServerConfig;
90
+ /**
91
+ * WrongStack Kanban — inspect and manage durable project work through the
92
+ * project-scoped Kanban IPC owner (`wstack-kanban-mcp`). The preset exposes
93
+ * the writable manage tier (create/update/transition tasks, splitting,
94
+ * assignment) but never `--destructive` (delete/merge/transfer). Mutations
95
+ * touch durable project board state, so the permission defaults to `confirm`.
96
+ */
97
+ export declare const kanbanServer: () => MCPServerConfig;
98
+ /**
99
+ * WrongStack Mailbox — coordinate with project agents through the existing
100
+ * project-scoped mailbox IPC owner (`wstack-mailbox-mcp`). The preset exposes
101
+ * the writable tier (send, receipts, self-presence) but never `--admin`
102
+ * (maintenance + credential operations).
103
+ *
104
+ * `--actor` is mandatory: the child CLI exits with code 2 without it. The
105
+ * default `external-agent` identity should be customized per external agent
106
+ * (it is the authoritative identity for sends, receipts, and registration).
107
+ */
108
+ export declare const mailboxServer: () => MCPServerConfig;
109
+ /**
110
+ * WrongStack Codebase Index — symbol search and dependency graphs over the
111
+ * project's codebase index (`wstack-codebase-index-mcp`). The preset exposes
112
+ * `--writable` so `codebase_index` (incremental/full rebuild) is available;
113
+ * index rebuilds are CPU/disk work with no semantic risk.
114
+ */
115
+ export declare const codebaseIndexServer: () => MCPServerConfig;
81
116
  /** Everything bundled — full set of built-in servers. Useful for `wstack mcp add --all`. */
82
117
  export declare const allServers: () => Record<string, MCPServerConfig>;
83
118
  //# sourceMappingURL=mcp-servers.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import type { ChroniclePromptManifest } from '../../chronicle/prompt-manifest.js';
2
2
  import type { Context } from '../../core/context.js';
3
3
  import type { ContentBlock } from '../../types/blocks.js';
4
- import type { Usage } from '../../types/provider.js';
4
+ import type { ProviderErrorBody, Usage } from '../../types/provider.js';
5
5
  export interface ProviderEventMap {
6
6
  /** Fired before every physical provider/model attempt, including attempt zero. */
7
7
  'provider.attempt.started': {
@@ -63,6 +63,8 @@ export interface ProviderEventMap {
63
63
  retryScheduled: boolean;
64
64
  retryDelayMs?: number | undefined;
65
65
  providerRequestId?: string | undefined;
66
+ /** Scrubbed provider failure envelope/body retained for diagnostics. */
67
+ errorBody?: ProviderErrorBody | undefined;
66
68
  /** Kanban task this provider call belongs to, when known. */
67
69
  taskId?: string | undefined;
68
70
  /** Kanban board this provider call belongs to, when known. */
@@ -123,6 +125,8 @@ export interface ProviderEventMap {
123
125
  delayMs: number;
124
126
  status: number;
125
127
  description: string;
128
+ /** Scrubbed provider failure envelope/body retained for diagnostics. */
129
+ errorBody?: ProviderErrorBody | undefined;
126
130
  };
127
131
  /**
128
132
  * Fired once when a provider call ultimately fails (retries exhausted, or
@@ -134,6 +138,8 @@ export interface ProviderEventMap {
134
138
  status: number;
135
139
  description: string;
136
140
  retryable: boolean;
141
+ /** Scrubbed provider failure envelope/body retained for diagnostics. */
142
+ errorBody?: ProviderErrorBody | undefined;
137
143
  };
138
144
  /**
139
145
  * Fired by the fallback-model extension when the primary model is overloaded
@@ -17,6 +17,8 @@ export interface SddEventMap {
17
17
  completed: number;
18
18
  failed: number;
19
19
  stopped: boolean;
20
+ /** Fatal, non-recoverable error that hard-stopped the run (implies `stopped: true`). */
21
+ fatalError?: string | undefined;
20
22
  };
21
23
  /** A task began executing on a worker (carries who + which worktree). */
22
24
  'sdd.task.started': {