opencode-swarm 6.83.0 → 6.84.1

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.
package/dist/index.js CHANGED
@@ -87,7 +87,9 @@ var init_tool_names = __esm(() => {
87
87
  "get_approved_plan",
88
88
  "repo_map",
89
89
  "get_qa_gate_profile",
90
- "set_qa_gates"
90
+ "set_qa_gates",
91
+ "web_search",
92
+ "convene_general_council"
91
93
  ];
92
94
  TOOL_NAME_SET = new Set(TOOL_NAMES);
93
95
  });
@@ -170,6 +172,8 @@ var init_constants = __esm(() => {
170
172
  "critic_hallucination_verifier",
171
173
  "curator_init",
172
174
  "curator_phase",
175
+ "council_member",
176
+ "council_moderator",
173
177
  ...QA_AGENTS,
174
178
  ...PIPELINE_AGENTS
175
179
  ];
@@ -241,7 +245,8 @@ var init_constants = __esm(() => {
241
245
  "suggest_patch",
242
246
  "repo_map",
243
247
  "get_qa_gate_profile",
244
- "set_qa_gates"
248
+ "set_qa_gates",
249
+ "convene_general_council"
245
250
  ],
246
251
  explorer: [
247
252
  "complexity_hotspots",
@@ -391,7 +396,9 @@ var init_constants = __esm(() => {
391
396
  "knowledge_recall"
392
397
  ],
393
398
  curator_init: ["knowledge_recall"],
394
- curator_phase: ["knowledge_recall"]
399
+ curator_phase: ["knowledge_recall"],
400
+ council_member: ["web_search"],
401
+ council_moderator: []
395
402
  };
396
403
  WRITE_TOOL_NAMES = [
397
404
  "write",
@@ -453,13 +460,15 @@ var init_constants = __esm(() => {
453
460
  gitingest: "fetch a GitHub repository full content via gitingest.com",
454
461
  retrieve_summary: "retrieve the full content of a stored tool output summary",
455
462
  search: "Workspace-scoped ripgrep-style text search with structured JSON output. Supports literal and regex modes, glob filtering, and result limits. NOTE: This is text search, not structural AST search \u2014 use symbols and imports tools for structural queries.",
463
+ web_search: "External web search (Tavily or Brave) for General Council member agents. Returns titled results with snippets and URLs. Restricted to council_member agents via AGENT_TOOL_MAP. Config-gated on council.general.enabled; requires a search API key.",
464
+ convene_general_council: "Synthesize responses from a multi-model General Council. Accepts parallel member responses (Round 1, optionally Round 2), detects disagreements, and returns consensus points, persisting disagreements, a structured synthesis, and an optional moderator prompt. Architect-only. Config-gated on council.general.enabled.",
456
465
  batch_symbols: "Batched symbol extraction across multiple files. Returns per-file symbol summaries with isolated error handling.",
457
466
  suggest_patch: "Reviewer-safe structured patch suggestion tool. Produces context-anchored patch artifacts without file modification. Returns structured diagnostics on context mismatch.",
458
467
  lint_spec: "validate .swarm/spec.md format and required fields",
459
468
  get_approved_plan: "retrieve the last critic-approved immutable plan snapshot for baseline drift comparison",
460
469
  repo_map: "query the repo code graph: importers, dependencies, blast radius, and localization context for structural awareness before refactoring",
461
- get_qa_gate_profile: "retrieve the QA gate profile for the current plan: gates (reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test), lock state, and profile hash. Read-only.",
462
- set_qa_gates: "configure the QA gate profile for the current plan. Architect-only. Ratchet-tighter only \u2014 rejected once the profile is locked after critic approval. Supports: reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test.",
470
+ get_qa_gate_profile: "retrieve the QA gate profile for the current plan: gates (reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test, council_general_review), lock state, and profile hash. Read-only.",
471
+ set_qa_gates: "configure the QA gate profile for the current plan. Architect-only. Ratchet-tighter only \u2014 rejected once the profile is locked after critic approval. Supports: reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test, council_general_review.",
463
472
  req_coverage: "query requirement coverage status for tracked functional requirements"
464
473
  };
465
474
  for (const [agentName, tools] of Object.entries(AGENT_TOOL_MAP)) {
@@ -483,6 +492,8 @@ var init_constants = __esm(() => {
483
492
  designer: "opencode/trinity-large-preview-free",
484
493
  curator_init: "opencode/trinity-large-preview-free",
485
494
  curator_phase: "opencode/trinity-large-preview-free",
495
+ council_member: "opencode/trinity-large-preview-free",
496
+ council_moderator: "opencode/trinity-large-preview-free",
486
497
  default: "opencode/trinity-large-preview-free"
487
498
  };
488
499
  DEFAULT_SCORING_CONFIG = {
@@ -14690,7 +14701,7 @@ function resolveGuardrailsConfig(config2, agentName) {
14690
14701
  };
14691
14702
  return resolved;
14692
14703
  }
14693
- var KNOWN_SWARM_PREFIXES, SEPARATORS, AgentOverrideConfigSchema, SwarmConfigSchema, HooksConfigSchema, ScoringWeightsSchema, DecisionDecaySchema, TokenRatiosSchema, ScoringConfigSchema, ContextBudgetConfigSchema, EvidenceConfigSchema, GateFeatureSchema, PlaceholderScanConfigSchema, QualityBudgetConfigSchema, GateConfigSchema, PipelineConfigSchema, PhaseCompleteConfigSchema, SummaryConfigSchema, ReviewPassesConfigSchema, AdversarialDetectionConfigSchema, AdversarialTestingConfigSchemaBase, AdversarialTestingConfigSchema, IntegrationAnalysisConfigSchema, DocsConfigSchema, UIReviewConfigSchema, CompactionAdvisoryConfigSchema, LintConfigSchema, SecretscanConfigSchema, GuardrailsProfileSchema, DEFAULT_AGENT_PROFILES, DEFAULT_ARCHITECT_PROFILE, GuardrailsConfigSchema, WatchdogConfigSchema, SelfReviewConfigSchema, ToolFilterConfigSchema, PlanCursorConfigSchema, CheckpointConfigSchema, AutomationModeSchema, AutomationCapabilitiesSchema, AutomationConfigSchemaBase, AutomationConfigSchema, KnowledgeConfigSchema, CuratorConfigSchema, SlopDetectorConfigSchema, IncrementalVerifyConfigSchema, CompactionConfigSchema, PrmConfigSchema, AgentAuthorityRuleSchema, AuthorityConfigSchema, CouncilConfigSchema, ParallelizationConfigSchema, PluginConfigSchema;
14704
+ var KNOWN_SWARM_PREFIXES, SEPARATORS, AgentOverrideConfigSchema, SwarmConfigSchema, HooksConfigSchema, ScoringWeightsSchema, DecisionDecaySchema, TokenRatiosSchema, ScoringConfigSchema, ContextBudgetConfigSchema, EvidenceConfigSchema, GateFeatureSchema, PlaceholderScanConfigSchema, QualityBudgetConfigSchema, GateConfigSchema, PipelineConfigSchema, PhaseCompleteConfigSchema, SummaryConfigSchema, ReviewPassesConfigSchema, AdversarialDetectionConfigSchema, AdversarialTestingConfigSchemaBase, AdversarialTestingConfigSchema, IntegrationAnalysisConfigSchema, DocsConfigSchema, UIReviewConfigSchema, CompactionAdvisoryConfigSchema, LintConfigSchema, SecretscanConfigSchema, GuardrailsProfileSchema, DEFAULT_AGENT_PROFILES, DEFAULT_ARCHITECT_PROFILE, GuardrailsConfigSchema, WatchdogConfigSchema, SelfReviewConfigSchema, ToolFilterConfigSchema, PlanCursorConfigSchema, CheckpointConfigSchema, AutomationModeSchema, AutomationCapabilitiesSchema, AutomationConfigSchemaBase, AutomationConfigSchema, KnowledgeConfigSchema, CuratorConfigSchema, SlopDetectorConfigSchema, IncrementalVerifyConfigSchema, CompactionConfigSchema, PrmConfigSchema, AgentAuthorityRuleSchema, AuthorityConfigSchema, GeneralCouncilMemberConfigSchema, GeneralCouncilConfigSchema, CouncilConfigSchema, ParallelizationConfigSchema, PluginConfigSchema;
14694
14705
  var init_schema = __esm(() => {
14695
14706
  init_zod();
14696
14707
  init_constants();
@@ -15005,21 +15016,25 @@ var init_schema = __esm(() => {
15005
15016
  coder: {
15006
15017
  max_tool_calls: 400,
15007
15018
  max_duration_minutes: 45,
15019
+ max_consecutive_errors: 8,
15008
15020
  warning_threshold: 0.85
15009
15021
  },
15010
15022
  test_engineer: {
15011
15023
  max_tool_calls: 400,
15012
15024
  max_duration_minutes: 45,
15025
+ max_consecutive_errors: 8,
15013
15026
  warning_threshold: 0.85
15014
15027
  },
15015
15028
  explorer: {
15016
15029
  max_tool_calls: 150,
15017
15030
  max_duration_minutes: 20,
15031
+ max_consecutive_errors: 8,
15018
15032
  warning_threshold: 0.75
15019
15033
  },
15020
15034
  reviewer: {
15021
15035
  max_tool_calls: 200,
15022
15036
  max_duration_minutes: 30,
15037
+ max_consecutive_errors: 8,
15023
15038
  warning_threshold: 0.65
15024
15039
  },
15025
15040
  critic: {
@@ -15207,13 +15222,37 @@ var init_schema = __esm(() => {
15207
15222
  rules: exports_external.record(exports_external.string(), AgentAuthorityRuleSchema).default({}),
15208
15223
  universal_deny_prefixes: exports_external.array(exports_external.string().min(1)).default([])
15209
15224
  });
15225
+ GeneralCouncilMemberConfigSchema = exports_external.object({
15226
+ memberId: exports_external.string().min(1),
15227
+ model: exports_external.string().min(1),
15228
+ role: exports_external.enum([
15229
+ "generalist",
15230
+ "skeptic",
15231
+ "domain_expert",
15232
+ "devil_advocate",
15233
+ "synthesizer"
15234
+ ]),
15235
+ persona: exports_external.string().optional()
15236
+ }).strict();
15237
+ GeneralCouncilConfigSchema = exports_external.object({
15238
+ enabled: exports_external.boolean().default(false),
15239
+ searchProvider: exports_external.enum(["tavily", "brave"]).default("tavily"),
15240
+ searchApiKey: exports_external.string().optional(),
15241
+ members: exports_external.array(GeneralCouncilMemberConfigSchema).default([]),
15242
+ presets: exports_external.record(exports_external.string(), exports_external.array(GeneralCouncilMemberConfigSchema)).default({}),
15243
+ deliberate: exports_external.boolean().default(true),
15244
+ moderator: exports_external.boolean().default(true),
15245
+ moderatorModel: exports_external.string().optional(),
15246
+ maxSourcesPerMember: exports_external.number().int().min(1).max(20).default(5)
15247
+ }).strict();
15210
15248
  CouncilConfigSchema = exports_external.object({
15211
15249
  enabled: exports_external.boolean().default(false),
15212
15250
  maxRounds: exports_external.number().int().min(1).max(10).default(3),
15213
15251
  parallelTimeoutMs: exports_external.number().int().min(5000).max(120000).default(30000),
15214
15252
  vetoPriority: exports_external.boolean().default(true),
15215
15253
  requireAllMembers: exports_external.boolean().default(false).describe("When true, convene_council rejects if fewer than 5 member verdicts are provided."),
15216
- escalateOnMaxRounds: exports_external.string().optional().describe("Optional webhook URL or handler name invoked when maxRounds is reached without APPROVE. Declared for forward compatibility; no behavior is implemented yet.")
15254
+ escalateOnMaxRounds: exports_external.string().optional().describe("Optional webhook URL or handler name invoked when maxRounds is reached without APPROVE. Declared for forward compatibility; no behavior is implemented yet."),
15255
+ general: GeneralCouncilConfigSchema.optional()
15217
15256
  }).strict();
15218
15257
  ParallelizationConfigSchema = exports_external.object({
15219
15258
  enabled: exports_external.boolean().default(false),
@@ -19707,7 +19746,8 @@ var init_qa_gate_profile = __esm(() => {
19707
19746
  critic_pre_plan: true,
19708
19747
  hallucination_guard: false,
19709
19748
  sast_enabled: true,
19710
- mutation_test: false
19749
+ mutation_test: false,
19750
+ council_general_review: false
19711
19751
  };
19712
19752
  });
19713
19753
 
@@ -23344,7 +23384,7 @@ function createGuardrailsHooks(directory, directoryOrConfig, config2, authorityC
23344
23384
  if (window2.consecutiveErrors >= agentConfig.max_consecutive_errors) {
23345
23385
  window2.hardLimitHit = true;
23346
23386
  telemetry.hardLimitHit(sessionID, window2.agentName, "consecutive_errors", window2.consecutiveErrors);
23347
- throw new Error(`\uD83D\uDED1 LIMIT REACHED: ${window2.consecutiveErrors} consecutive tool errors detected. Return your progress summary with details of what went wrong.`);
23387
+ throw new Error(`\uD83D\uDED1 LIMIT REACHED: ${window2.consecutiveErrors} consecutive tool errors detected. Return your progress summary with details of what went wrong. Run /swarm reset-session to clear the circuit breaker without restarting your session.`);
23348
23388
  }
23349
23389
  const idleMinutes = (Date.now() - window2.lastSuccessTimeMs) / 60000;
23350
23390
  if (idleMinutes >= agentConfig.idle_timeout_minutes) {
@@ -23885,31 +23925,32 @@ function createGuardrailsHooks(directory, directoryOrConfig, config2, authorityC
23885
23925
  return;
23886
23926
  const hasError = output.output === null || output.output === undefined;
23887
23927
  if (hasError) {
23888
- window2.consecutiveErrors++;
23889
- if (session) {
23890
- const outputStr = typeof output.output === "string" ? output.output : "";
23891
- const errorContent = output.error ?? outputStr;
23892
- if (typeof errorContent === "string" && TRANSIENT_MODEL_ERROR_PATTERN.test(errorContent) && !session.modelFallbackExhausted) {
23893
- session.model_fallback_index++;
23894
- const baseAgentName = session.agentName ? session.agentName.replace(/^[^_]+[_]/, "") : "";
23895
- const swarmAgents = getSwarmAgents();
23896
- const fallbackModels = swarmAgents?.[baseAgentName]?.fallback_models;
23897
- session.modelFallbackExhausted = !fallbackModels || session.model_fallback_index > fallbackModels.length;
23898
- const fallbackModel = resolveFallbackModel(baseAgentName, session.model_fallback_index, swarmAgents);
23899
- const primaryModel = swarmAgents?.[baseAgentName]?.model ?? "default";
23900
- if (fallbackModel) {
23901
- if (swarmAgents?.[baseAgentName]) {
23902
- swarmAgents[baseAgentName].model = fallbackModel;
23903
- }
23904
- session.pendingAdvisoryMessages ??= [];
23905
- session.pendingAdvisoryMessages.push(`MODEL FALLBACK: Applied fallback model "${fallbackModel}" (attempt ${session.model_fallback_index}). ` + `Using /swarm handoff to reset to primary model.`);
23906
- } else {
23907
- session.pendingAdvisoryMessages ??= [];
23908
- session.pendingAdvisoryMessages.push(`MODEL FALLBACK: Transient model error detected (attempt ${session.model_fallback_index}). ` + `No fallback models configured for this agent. Add "fallback_models": ["model-a", "model-b"] ` + `to the agent's config in opencode-swarm.json.`);
23928
+ const outputStr = typeof output.output === "string" ? output.output : "";
23929
+ const errorContent = output.error ?? outputStr;
23930
+ const isTransient = !!session && !session.modelFallbackExhausted && typeof errorContent === "string" && TRANSIENT_MODEL_ERROR_PATTERN.test(errorContent);
23931
+ if (!isTransient) {
23932
+ window2.consecutiveErrors++;
23933
+ }
23934
+ if (session && isTransient) {
23935
+ session.model_fallback_index++;
23936
+ const baseAgentName = session.agentName ? session.agentName.replace(/^[^_]+[_]/, "") : "";
23937
+ const swarmAgents = getSwarmAgents();
23938
+ const fallbackModels = swarmAgents?.[baseAgentName]?.fallback_models;
23939
+ session.modelFallbackExhausted = !fallbackModels || session.model_fallback_index > fallbackModels.length;
23940
+ const fallbackModel = resolveFallbackModel(baseAgentName, session.model_fallback_index, swarmAgents);
23941
+ const primaryModel = swarmAgents?.[baseAgentName]?.model ?? "default";
23942
+ if (fallbackModel) {
23943
+ if (swarmAgents?.[baseAgentName]) {
23944
+ swarmAgents[baseAgentName].model = fallbackModel;
23909
23945
  }
23910
- telemetry.modelFallback(input.sessionID, session.agentName, primaryModel, fallbackModel ?? "none", "transient_model_error");
23911
- swarmState.pendingEvents++;
23946
+ session.pendingAdvisoryMessages ??= [];
23947
+ session.pendingAdvisoryMessages.push(`MODEL FALLBACK: Applied fallback model "${fallbackModel}" (attempt ${session.model_fallback_index}). ` + `Using /swarm handoff to reset to primary model.`);
23948
+ } else {
23949
+ session.pendingAdvisoryMessages ??= [];
23950
+ session.pendingAdvisoryMessages.push(`MODEL FALLBACK: Transient model error detected (attempt ${session.model_fallback_index}). ` + `No fallback models configured for this agent. Add "fallback_models": ["model-a", "model-b"] ` + `to the agent's config in opencode-swarm.json.`);
23912
23951
  }
23952
+ telemetry.modelFallback(input.sessionID, session.agentName, primaryModel, fallbackModel ?? "none", "transient_model_error");
23953
+ swarmState.pendingEvents++;
23913
23954
  }
23914
23955
  } else {
23915
23956
  window2.consecutiveErrors = 0;
@@ -41864,6 +41905,76 @@ var init_config2 = __esm(() => {
41864
41905
  init_loader();
41865
41906
  });
41866
41907
 
41908
+ // src/commands/council.ts
41909
+ function sanitizeQuestion(raw) {
41910
+ const collapsed = raw.replace(/\s+/g, " ").trim();
41911
+ const stripped = collapsed.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
41912
+ const normalized = stripped.replace(/\s+/g, " ").trim();
41913
+ if (normalized.length <= MAX_QUESTION_LEN)
41914
+ return normalized;
41915
+ return `${normalized.slice(0, MAX_QUESTION_LEN)}\u2026`;
41916
+ }
41917
+ function sanitizePresetName(raw) {
41918
+ const trimmed = raw.trim();
41919
+ if (!trimmed)
41920
+ return null;
41921
+ if (trimmed.length > 64)
41922
+ return null;
41923
+ if (!/^[A-Za-z0-9_-]+$/.test(trimmed))
41924
+ return null;
41925
+ return trimmed;
41926
+ }
41927
+ function parseArgs(args2) {
41928
+ const out2 = { specReview: false, rest: [] };
41929
+ for (let i2 = 0;i2 < args2.length; i2++) {
41930
+ const token = args2[i2];
41931
+ if (token === "--spec-review") {
41932
+ out2.specReview = true;
41933
+ continue;
41934
+ }
41935
+ if (token === "--preset") {
41936
+ const next = args2[i2 + 1];
41937
+ if (next !== undefined) {
41938
+ const sanitized = sanitizePresetName(next);
41939
+ if (sanitized)
41940
+ out2.preset = sanitized;
41941
+ i2++;
41942
+ }
41943
+ continue;
41944
+ }
41945
+ out2.rest.push(token);
41946
+ }
41947
+ return out2;
41948
+ }
41949
+ async function handleCouncilCommand(_directory, args2) {
41950
+ const parsed = parseArgs(args2);
41951
+ const question = sanitizeQuestion(parsed.rest.join(" "));
41952
+ if (!question) {
41953
+ return USAGE;
41954
+ }
41955
+ const tokens = ["MODE: COUNCIL"];
41956
+ if (parsed.preset) {
41957
+ tokens.push(`preset=${parsed.preset}`);
41958
+ }
41959
+ if (parsed.specReview) {
41960
+ tokens.push("spec_review");
41961
+ }
41962
+ return `[${tokens.join(" ")}] ${question}`;
41963
+ }
41964
+ var MAX_QUESTION_LEN = 2000, USAGE;
41965
+ var init_council = __esm(() => {
41966
+ USAGE = [
41967
+ "Usage: /swarm council <question> [--preset <name>] [--spec-review]",
41968
+ "",
41969
+ " question The question to put to the council",
41970
+ " --preset <name> Use a named member preset from council.general.presets",
41971
+ " --spec-review Use spec_review mode (single advisory pass on a draft spec)",
41972
+ "",
41973
+ "Requires council.general.enabled: true and a configured search API key in opencode-swarm.json."
41974
+ ].join(`
41975
+ `);
41976
+ });
41977
+
41867
41978
  // src/agents/explorer.ts
41868
41979
  function createExplorerAgent(model, customPrompt, customAppendPrompt) {
41869
41980
  let prompt = EXPLORER_PROMPT;
@@ -52150,7 +52261,8 @@ var init_qa_gates = __esm(() => {
52150
52261
  "critic_pre_plan",
52151
52262
  "hallucination_guard",
52152
52263
  "sast_enabled",
52153
- "mutation_test"
52264
+ "mutation_test",
52265
+ "council_general_review"
52154
52266
  ];
52155
52267
  });
52156
52268
 
@@ -52969,7 +53081,7 @@ async function handleResetSessionCommand(directory, _args) {
52969
53081
  "",
52970
53082
  "Session state cleared. Plan, evidence, and knowledge preserved.",
52971
53083
  "",
52972
- "**Next step:** Start a new OpenCode session. The plugin will initialize fresh session state on startup."
53084
+ "**All circuit breakers and revision limits have been cleared.** You can continue in this session \u2014 fresh state will be initialized automatically on the next tool call."
52973
53085
  ].join(`
52974
53086
  `);
52975
53087
  }
@@ -53776,6 +53888,7 @@ var init_registry = __esm(() => {
53776
53888
  init_checkpoint2();
53777
53889
  init_close();
53778
53890
  init_config2();
53891
+ init_council();
53779
53892
  init_curate();
53780
53893
  init_dark_matter();
53781
53894
  init_diagnose();
@@ -53931,11 +54044,17 @@ var init_registry = __esm(() => {
53931
54044
  args: "[topic-text]",
53932
54045
  details: "Triggers the architect to run the brainstorm workflow: CONTEXT SCAN, single-question DIALOGUE, APPROACHES, DESIGN SECTIONS, SPEC WRITE + SELF-REVIEW, QA GATE SELECTION, TRANSITION. Use for new plans where requirements need to be drawn out before writing spec.md / plan.md."
53933
54046
  },
54047
+ council: {
54048
+ handler: (ctx) => handleCouncilCommand(ctx.directory, ctx.args),
54049
+ description: "Enter architect MODE: COUNCIL \u2014 multi-model deliberation [question] [--preset <name>] [--spec-review]",
54050
+ args: "<question> [--preset <name>] [--spec-review]",
54051
+ details: "Triggers the architect to convene a configurable General Council: each member independently web-searches, answers, and engages in one structured deliberation round on disagreements; an optional moderator pass synthesizes the final answer. --preset <name> selects a member group from council.general.presets. --spec-review switches to single-pass advisory mode for spec review. Requires council.general.enabled: true and a search API key in opencode-swarm.json."
54052
+ },
53934
54053
  "qa-gates": {
53935
54054
  handler: (ctx) => handleQaGatesCommand(ctx.directory, ctx.args, ctx.sessionID),
53936
54055
  description: "View or modify QA gate profile for the current plan [enable|override <gate>...]",
53937
54056
  args: "[show|enable|override] <gate>...",
53938
- details: "show: display spec-level, session-override, and effective QA gates for the current plan. enable: persist gate(s) into the locked-once profile (architect; rejected after critic approval lock). override: session-only ratchet-tighter enable. Valid gates: reviewer, test_engineer, council_mode, sme_enabled, critic_pre_plan, hallucination_guard, sast_enabled, mutation_test."
54057
+ details: "show: display spec-level, session-override, and effective QA gates for the current plan. enable: persist gate(s) into the locked-once profile (architect; rejected after critic approval lock). override: session-only ratchet-tighter enable. Valid gates: reviewer, test_engineer, council_mode, sme_enabled, critic_pre_plan, hallucination_guard, sast_enabled, mutation_test, council_general_review."
53939
54058
  },
53940
54059
  promote: {
53941
54060
  handler: (ctx) => handlePromoteCommand(ctx.directory, ctx.args),
@@ -54091,14 +54210,24 @@ from different members.`;
54091
54210
  function buildYourToolsList(council) {
54092
54211
  const tools = AGENT_TOOL_MAP.architect ?? [];
54093
54212
  const sorted = [...tools].sort();
54094
- const filtered = council?.enabled === true ? sorted : sorted.filter((t) => t !== "convene_council" && t !== "declare_council_criteria");
54213
+ const qaCouncilEnabled = council?.enabled === true;
54214
+ const generalCouncilEnabled = council?.general?.enabled === true;
54215
+ const filtered = sorted.filter((t) => {
54216
+ if (!qaCouncilEnabled && (t === "convene_council" || t === "declare_council_criteria")) {
54217
+ return false;
54218
+ }
54219
+ if (!generalCouncilEnabled && t === "convene_general_council") {
54220
+ return false;
54221
+ }
54222
+ return true;
54223
+ });
54095
54224
  return `Task (delegation), ${filtered.join(", ")}.`;
54096
54225
  }
54097
54226
  function buildQaGateSelectionDialogue(modeLabel) {
54098
54227
  const leadIn = modeLabel === "BRAINSTORM" ? "Now ask the user which QA gates to enable for this plan \u2014 do not select on their behalf." : modeLabel === "SPECIFY" ? "Ask the user which QA gates to enable for this plan before suggesting the next step." : "No pending gate selection found in `.swarm/context.md`. Ask the user inline now.";
54099
54228
  return `${leadIn}
54100
54229
 
54101
- Present the eight gates with their defaults (DEFAULT_QA_GATES) as a single user-facing question. Offer the user a one-shot choice: accept defaults, or customize. The eight gates are:
54230
+ Present the nine gates with their defaults (DEFAULT_QA_GATES) as a single user-facing question. Offer the user a one-shot choice: accept defaults, or customize. The nine gates are:
54102
54231
  - reviewer (default: ON) \u2014 code review of coder output
54103
54232
  - test_engineer (default: ON) \u2014 test verification of coder output
54104
54233
  - sme_enabled (default: ON) \u2014 SME consultation during planning/clarification
@@ -54107,13 +54236,24 @@ Present the eight gates with their defaults (DEFAULT_QA_GATES) as a single user-
54107
54236
  - council_mode (default: OFF) \u2014 multi-member council gate (recommended for high-impact architecture, public APIs, schema/data mutation, security-sensitive code)
54108
54237
  - hallucination_guard (default: OFF) \u2014 when enabled, mandatory per-phase API/signature/claim/citation verification via critic_hallucination_verifier at PHASE-WRAP; phase_complete will REJECT phase completion unless .swarm/evidence/{phase}/hallucination-guard.json exists with an APPROVED verdict (recommended for claim-heavy or research-heavy work)
54109
54238
  - mutation_test (default: OFF) \u2014 when enabled, runs mutation testing on source files touched this phase via generate_mutants + mutation_test + write_mutation_evidence at PHASE-WRAP; FAIL verdict blocks phase_complete; WARN is non-blocking (recommended for projects with coverage gaps or safety-critical code)
54239
+ - council_general_review (default: OFF) \u2014 when enabled, MODE: SPECIFY runs convene_general_council on the draft spec before the critic-gate; multiple models each independently search the web, deliberate on disagreements, and a moderator synthesizes a final answer that the architect folds into the spec (recommended for novel architecture, unclear best practices, or high-risk design decisions). Requires council.general.enabled: true and a configured search API key.
54110
54240
 
54111
54241
  One question, one message, defaults pre-stated. Wait for the user's answer.`;
54112
54242
  }
54113
54243
  function buildAvailableToolsList(council) {
54114
54244
  const tools = AGENT_TOOL_MAP.architect ?? [];
54115
54245
  const sorted = [...tools].sort();
54116
- const filtered = council?.enabled === true ? sorted : sorted.filter((t) => t !== "convene_council" && t !== "declare_council_criteria");
54246
+ const qaCouncilEnabled = council?.enabled === true;
54247
+ const generalCouncilEnabled = council?.general?.enabled === true;
54248
+ const filtered = sorted.filter((t) => {
54249
+ if (!qaCouncilEnabled && (t === "convene_council" || t === "declare_council_criteria")) {
54250
+ return false;
54251
+ }
54252
+ if (!generalCouncilEnabled && t === "convene_general_council") {
54253
+ return false;
54254
+ }
54255
+ return true;
54256
+ });
54117
54257
  return filtered.map((t) => {
54118
54258
  const desc = TOOL_DESCRIPTIONS[t];
54119
54259
  return desc ? `${t} (${desc})` : t;
@@ -54154,7 +54294,8 @@ function buildSlashCommandsList() {
54154
54294
  "analyze",
54155
54295
  "plan",
54156
54296
  "sync-plan",
54157
- "acknowledge-spec-drift"
54297
+ "acknowledge-spec-drift",
54298
+ "council"
54158
54299
  ],
54159
54300
  "Execution Modes": ["turbo", "full-auto"],
54160
54301
  Observation: [
@@ -54799,6 +54940,29 @@ MODE: BRAINSTORM runs seven phases in strict order. Do not skip phases. Do not c
54799
54940
  **Phase 6: QA GATE SELECTION (architect, dialogue only).**
54800
54941
  {{QA_GATE_DIALOGUE_BRAINSTORM}}
54801
54942
 
54943
+ <!-- BEHAVIORAL_GUIDANCE_START -->
54944
+ GATE SELECTION IS MANDATORY \u2014 these thoughts are WRONG and must be ignored:
54945
+ \u2717 "I'll use the defaults \u2014 they're probably fine"
54946
+ \u2192 WRONG: defaults are not the user's decision. The user must be asked every time.
54947
+ \u2717 "The user didn't mention gates, so defaults are fine"
54948
+ \u2192 WRONG: silence is not consent. The gate dialogue is not optional.
54949
+ \u2717 "I'll handle it in MODE: PLAN after the spec is done"
54950
+ \u2192 WRONG: ## Pending QA Gate Selection must exist in context.md BEFORE save_plan is called.
54951
+ save_plan will reject with QA_GATE_SELECTION_REQUIRED if this section is absent.
54952
+ \u2717 "This feature is simple \u2014 gates are obvious"
54953
+ \u2192 WRONG: complexity does not exempt this step. Gate selection is mandatory for ALL plans.
54954
+ \u2717 "I already know which gates are right for this project"
54955
+ \u2192 WRONG: the architect does not configure gates. The user configures gates. Always ask.
54956
+ \u2717 "council_general_review is off by default, I don't need to mention it"
54957
+ \u2192 WRONG: every gate is presented with its default stated. The user opts in or accepts the default explicitly.
54958
+
54959
+ MANDATORY PAUSE: Do NOT write the spec summary (step 7). Do NOT suggest next steps.
54960
+ You are BLOCKED until ALL THREE of these conditions are met:
54961
+ (1) The gate selection question has been presented to the user in a single message
54962
+ (2) The user has responded (accept defaults OR customized list)
54963
+ (3) The elected gates have been written to .swarm/context.md under "## Pending QA Gate Selection"
54964
+ <!-- BEHAVIORAL_GUIDANCE_END -->
54965
+
54802
54966
  Do NOT call \`set_qa_gates\` yet \u2014 \`plan.json\` does not exist at this point. Once the user answers, write the elected gates to \`.swarm/context.md\` under a new section:
54803
54967
  \`\`\`
54804
54968
  ## Pending QA Gate Selection
@@ -54810,6 +54974,7 @@ Do NOT call \`set_qa_gates\` yet \u2014 \`plan.json\` does not exist at this poi
54810
54974
  - council_mode: <true|false>
54811
54975
  - hallucination_guard: <true|false>
54812
54976
  - mutation_test: <true|false>
54977
+ - council_general_review: <true|false>
54813
54978
  - recorded_at: <ISO timestamp>
54814
54979
  \`\`\`
54815
54980
  MODE: PLAN applies these after \`save_plan\` succeeds via \`set_qa_gates\`.
@@ -54852,6 +55017,29 @@ Activates when: user asks to "specify", "define requirements", "write a spec", o
54852
55017
  5b. **QA GATE SELECTION (dialogue only).**
54853
55018
  {{QA_GATE_DIALOGUE_SPECIFY}}
54854
55019
 
55020
+ <!-- BEHAVIORAL_GUIDANCE_START -->
55021
+ GATE SELECTION IS MANDATORY \u2014 these thoughts are WRONG and must be ignored:
55022
+ \u2717 "I'll use the defaults \u2014 they're probably fine"
55023
+ \u2192 WRONG: defaults are not the user's decision. The user must be asked every time.
55024
+ \u2717 "The user didn't mention gates, so defaults are fine"
55025
+ \u2192 WRONG: silence is not consent. The gate dialogue is not optional.
55026
+ \u2717 "I'll handle it in MODE: PLAN after the spec is done"
55027
+ \u2192 WRONG: ## Pending QA Gate Selection must exist in context.md BEFORE save_plan is called.
55028
+ save_plan will reject with QA_GATE_SELECTION_REQUIRED if this section is absent.
55029
+ \u2717 "This feature is simple \u2014 gates are obvious"
55030
+ \u2192 WRONG: complexity does not exempt this step. Gate selection is mandatory for ALL plans.
55031
+ \u2717 "I already know which gates are right for this project"
55032
+ \u2192 WRONG: the architect does not configure gates. The user configures gates. Always ask.
55033
+ \u2717 "council_general_review is off by default, I don't need to mention it"
55034
+ \u2192 WRONG: every gate is presented with its default stated. The user opts in or accepts the default explicitly.
55035
+
55036
+ MANDATORY PAUSE: Do NOT write the spec summary (step 7). Do NOT suggest next steps.
55037
+ You are BLOCKED until ALL THREE of these conditions are met:
55038
+ (1) The gate selection question has been presented to the user in a single message
55039
+ (2) The user has responded (accept defaults OR customized list)
55040
+ (3) The elected gates have been written to .swarm/context.md under "## Pending QA Gate Selection"
55041
+ <!-- BEHAVIORAL_GUIDANCE_END -->
55042
+
54855
55043
  Do NOT call \`set_qa_gates\` yet \u2014 \`plan.json\` does not exist at this point. Once the user answers, write the elected gates to \`.swarm/context.md\` under a new section:
54856
55044
  \`\`\`
54857
55045
  ## Pending QA Gate Selection
@@ -54863,9 +55051,37 @@ Do NOT call \`set_qa_gates\` yet \u2014 \`plan.json\` does not exist at this poi
54863
55051
  - council_mode: <true|false>
54864
55052
  - hallucination_guard: <true|false>
54865
55053
  - mutation_test: <true|false>
55054
+ - council_general_review: <true|false>
54866
55055
  - recorded_at: <ISO timestamp>
54867
55056
  \`\`\`
54868
55057
  MODE: PLAN will read this section after \`save_plan\` succeeds and persist via \`set_qa_gates\`.
55058
+
55059
+ 5c. **SPECIFY-COUNCIL-REVIEW (fires ONLY when council_general_review gate is true).**
55060
+ Read the elected QA gates (parse the \`## Pending QA Gate Selection\` section from \`.swarm/context.md\` you just wrote, OR call \`get_qa_gate_profile\` if a profile already exists). If \`council_general_review\` is false or absent, skip directly to step 7.
55061
+
55062
+ If \`council_general_review\` is true:
55063
+ 1. Read \`council.general\` config. If \`council.general.enabled\` is not true OR no search API key is configured, surface to the user: "council_general_review gate is enabled but the General Council is not configured. Set council.general.enabled: true and configure a search API key in opencode-swarm.json, or unset council_general_review and re-run." Then stop.
55064
+ 2. Determine the council members from \`council.general.members\` (or \`council.general.presets[<name>]\` if you were invoked via \`/swarm council --preset <name>\` originally).
55065
+ 3. Delegate to each council member in PARALLEL \u2014 one message per member, then STOP and wait. Pass: the spec text as the question, the member's role/persona, round number 1. Do NOT share other members' perspectives at this stage.
55066
+ 4. Collect all member JSON responses.
55067
+ 5. Call \`convene_general_council\` with mode: 'spec_review', the spec as question, and the collected \`round1Responses\`. Omit \`round2Responses\` \u2014 spec review is a single-pass advisory, not a full deliberation.
55068
+ 6. Read \`consensusPoints\` \u2014 incorporate unambiguous consensus directly into the spec.
55069
+ 7. Read \`disagreements\` \u2014 for each: (a) accept one position with rationale, (b) mark as \`[NEEDS CLARIFICATION]\` in the spec, or (c) schedule an SME consultation.
55070
+ 8. If \`council.general.moderator\` is true, the tool returned a \`moderatorPrompt\` field. Delegate this prompt to \`{{AGENT_PREFIX}}council_moderator\`. Use the moderator's output to refine the spec further.
55071
+ 9. Revise \`.swarm/spec.md\` to reflect the council input.
55072
+
55073
+ <!-- BEHAVIORAL_GUIDANCE_START -->
55074
+ SPECIFY-COUNCIL-REVIEW RULES:
55075
+ \u2717 "council_general_review is off by default, I'll skip this"
55076
+ \u2192 CORRECT only when the gate is explicitly false or absent. Do NOT assume false. Read the actual gate value before deciding to skip.
55077
+ \u2717 "The spec is already good, no need to ask the council"
55078
+ \u2192 WRONG when gate is true: the user enabled this gate for a reason. Run it regardless.
55079
+ \u2717 "I'll include round2Responses for spec_review \u2014 more is better"
55080
+ \u2192 WRONG: spec review is a single advisory pass. Omit \`round2Responses\` for spec_review mode.
55081
+ \u2717 "I'll skip the moderator pass to save time"
55082
+ \u2192 WRONG when council.general.moderator is true: invoke \`{{AGENT_PREFIX}}council_moderator\` with the moderatorPrompt the tool returns.
55083
+ <!-- BEHAVIORAL_GUIDANCE_END -->
55084
+
54869
55085
  7. Report a summary to the user (MUST count, SHALL count, scenario count, clarification markers, elected QA gates) and suggest the next step: \`CLARIFY-SPEC\` (if markers exist) or \`PLAN\`.
54870
55086
 
54871
55087
  SPEC CONTENT RULES \u2014 the spec MUST NOT contain:
@@ -55034,6 +55250,39 @@ This check fires automatically in:
55034
55250
 
55035
55251
  GREENFIELD EXEMPTION: If the work is purely greenfield (new project, no existing codebase references), skip this check.
55036
55252
 
55253
+ ### MODE: COUNCIL
55254
+
55255
+ Activates when: user invokes \`/swarm council <question>\` (optionally with \`--preset <name>\` or \`--spec-review\`).
55256
+
55257
+ Purpose: convene a configurable multi-model General Council for an advisory deliberation. Each member independently web-searches and answers; the architect routes any disagreements back for one targeted reconciliation round; an optional moderator pass synthesizes the final user-facing answer.
55258
+
55259
+ This mode is ADVISORY \u2014 it does NOT block any other workflow and does NOT modify code, plans, or specs. The output is for the user (general mode) or for the spec being drafted in MODE: SPECIFY (spec_review mode, gated by \`council_general_review\`).
55260
+
55261
+ #### Pre-flight (always run first)
55262
+ 1. Read \`council.general\` config. If \`council.general.enabled\` is not true OR no search API key is configured (neither \`council.general.searchApiKey\` nor the corresponding env var \`TAVILY_API_KEY\` / \`BRAVE_SEARCH_API_KEY\`), surface to the user: "General Council is not enabled. Set council.general.enabled: true and configure a search API key in opencode-swarm.json." Then STOP.
55263
+
55264
+ #### Round 1 \u2014 Parallel Independent Search
55265
+ 2. Determine council members. Default: \`council.general.members\`. If invoked with \`--preset <name>\`: \`council.general.presets[<name>]\`. If a named preset is missing, surface a clear error and stop.
55266
+ 3. Delegate to each council member in PARALLEL \u2014 one message per member, then STOP and wait for all responses to come back. Pass: the question, the member's role/persona, round number 1. Do NOT share other members' responses at this stage.
55267
+ 4. Collect all member JSON responses (each member returns a fenced JSON block per the council_member prompt).
55268
+
55269
+ #### Synthesis and Deliberation (when council.general.deliberate is true; default true)
55270
+ 5. Call \`convene_general_council\` with mode set from the command (\`general\` or \`spec_review\`), \`question\`, and the collected \`round1Responses\` only (omit \`round2Responses\`). Inspect the returned \`disagreementsCount\`.
55271
+ 6. If \`disagreementsCount > 0\`:
55272
+ a. For each disagreement in the tool's response, identify the disputing members (the members listed in the disagreement's positions).
55273
+ b. Re-delegate ONLY to the disputing members \u2014 one message per member \u2014 passing: their Round 1 response, the disagreement topic, the opposing position(s), round number 2.
55274
+ c. Collect the Round 2 responses.
55275
+ d. Call \`convene_general_council\` AGAIN with both \`round1Responses\` AND \`round2Responses\` populated.
55276
+
55277
+ #### Moderator Pass (when council.general.moderator is true; default true)
55278
+ 7. The most recent \`convene_general_council\` call returned a \`moderatorPrompt\` field. Delegate this prompt to \`{{AGENT_PREFIX}}council_moderator\`. The moderator agent has no tools and no web access \u2014 it synthesizes a final user-facing answer from the council output you give it. Collect the moderator's markdown output.
55279
+
55280
+ #### Output
55281
+ 8. Present the final answer to the user:
55282
+ - If the moderator pass ran: present the moderator's output verbatim, prefaced with the participating models (one line).
55283
+ - If no moderator: present the structural \`synthesis\` markdown from the tool's return.
55284
+ In either case, do NOT present the raw per-member JSON. Do NOT silently pick a winner among persisting disagreements \u2014 surface them honestly.
55285
+
55037
55286
  ### MODE: PLAN
55038
55287
 
55039
55288
  SPEC GATE (soft \u2014 check before planning):
@@ -55112,7 +55361,18 @@ save_plan({
55112
55361
  **POST-SAVE_PLAN: APPLY QA GATE SELECTION.**
55113
55362
  After \`save_plan\` succeeds, read \`.swarm/context.md\`:
55114
55363
  - If a \`## Pending QA Gate Selection\` section exists: parse the gate values, call \`set_qa_gates\` with those flags, confirm with the user ("QA gates applied: <list>"), then remove the section from context.md.
55115
- - If no pending section exists: {{QA_GATE_DIALOGUE_PLAN}} Then call \`set_qa_gates\` with the user's chosen flags.
55364
+ - If no pending section exists: {{QA_GATE_DIALOGUE_PLAN}}
55365
+ <!-- BEHAVIORAL_GUIDANCE_START -->
55366
+ INLINE GATE SELECTION \u2014 no pending section found in context.md. You MUST ask now.
55367
+ \u2717 "I'll call set_qa_gates with defaults and move on"
55368
+ \u2192 WRONG: set_qa_gates with assumed values is a gate violation. The user must answer first.
55369
+ \u2717 "The user provided a plan \u2014 they know what gates they want"
55370
+ \u2192 WRONG: providing a plan is not the same as configuring gates. Always ask.
55371
+
55372
+ MANDATORY PAUSE: Present the gate question. Wait for the user's answer.
55373
+ Do NOT call \`set_qa_gates\` until the user has responded.
55374
+ <!-- BEHAVIORAL_GUIDANCE_END -->
55375
+ Then call \`set_qa_gates\` with the user's chosen flags.
55116
55376
  Either path must yield a persisted QA gate profile before the first task dispatches.
55117
55377
 
55118
55378
  \u26A0\uFE0F If \`save_plan\` is unavailable, delegate plan writing to {{AGENT_PREFIX}}coder:
@@ -55701,6 +55961,189 @@ META.SUMMARY CONVENTION \u2014 When reporting task completion, include:
55701
55961
 
55702
55962
  `;
55703
55963
 
55964
+ // src/agents/council-member.ts
55965
+ function createCouncilMemberAgent(model, customPrompt, customAppendPrompt) {
55966
+ let prompt = COUNCIL_MEMBER_PROMPT;
55967
+ if (customPrompt) {
55968
+ prompt = customPrompt;
55969
+ } else if (customAppendPrompt) {
55970
+ prompt = `${COUNCIL_MEMBER_PROMPT}
55971
+
55972
+ ${customAppendPrompt}`;
55973
+ }
55974
+ return {
55975
+ name: "council_member",
55976
+ description: "General Council deliberation member. Independently web-searches and answers in Round 1; " + "targeted MAINTAIN/CONCEDE/NUANCE deliberation in Round 2. Tool-restricted to web_search only.",
55977
+ config: {
55978
+ model,
55979
+ temperature: 0.4,
55980
+ prompt,
55981
+ tools: {
55982
+ write: false,
55983
+ edit: false,
55984
+ patch: false
55985
+ }
55986
+ }
55987
+ };
55988
+ }
55989
+ var COUNCIL_MEMBER_PROMPT = `You are Council Member {{MEMBER_ID}} ({{ROLE}}) on a multi-model General Council.
55990
+
55991
+ {{PERSONA_BLOCK}}
55992
+
55993
+ You are participating in Round {{ROUND}} of a structured deliberation. Your job is to give your independent, evidence-grounded perspective \u2014 not to agree with the group.
55994
+
55995
+ ================================================================
55996
+ ROUND {{ROUND}} PROTOCOL
55997
+ ================================================================
55998
+
55999
+ ROUND 1 \u2014 Independent Research and Answer
56000
+ - Issue 1\u20133 targeted web_search calls to gather evidence relevant to the question.
56001
+ - Cite EVERY factual claim with a source URL from your search results.
56002
+ - State your confidence (0.0\u20131.0) explicitly. Be honest \u2014 overconfident answers hurt the council.
56003
+ - Enumerate areas of uncertainty so the architect knows where you're guessing vs. where you're sure.
56004
+ - Do NOT coordinate with other members. You will not see their responses until Round 2.
56005
+ - Do NOT pad. Be concise. Substance over volume.
56006
+
56007
+ ROUND 2 \u2014 Targeted Deliberation (ONLY when this round is invoked for you)
56008
+ - {{DISAGREEMENT_BLOCK}}
56009
+ - Issue at most 1 additional web_search call.
56010
+ - Declare your stance explicitly using one of these keywords as the FIRST word of a paragraph:
56011
+ MAINTAIN \u2014 your Round 1 position holds; cite the new evidence supporting it
56012
+ CONCEDE \u2014 the opposing position is correct; state specifically what you got wrong
56013
+ NUANCE \u2014 both positions are partially right; state the boundary condition that distinguishes them
56014
+ - Never CONCEDE without evidence. Sycophantic capitulation degrades the council below an individual member's baseline (NSED arXiv:2601.16863).
56015
+ - Never MAINTAIN without engaging the opposing argument on its merits.
56016
+
56017
+ ================================================================
56018
+ RESPONSE FORMAT (always \u2014 both rounds)
56019
+ ================================================================
56020
+
56021
+ Reply with a single fenced JSON block. No prose outside the block.
56022
+
56023
+ \`\`\`json
56024
+ {
56025
+ "memberId": "{{MEMBER_ID}}",
56026
+ "role": "{{ROLE}}",
56027
+ "round": {{ROUND}},
56028
+ "response": "Your full answer (Round 1) or stance + reasoning (Round 2). Markdown OK inside the string.",
56029
+ "searchQueries": ["query 1", "query 2"],
56030
+ "sources": [
56031
+ { "title": "...", "url": "...", "snippet": "...", "query": "..." }
56032
+ ],
56033
+ "confidence": 0.85,
56034
+ "areasOfUncertainty": [
56035
+ "What I'm not sure about, in plain language."
56036
+ ],
56037
+ "disagreementTopics": []
56038
+ }
56039
+ \`\`\`
56040
+
56041
+ For Round 1: leave \`disagreementTopics\` as []. For Round 2: list the specific disagreement topics this response addresses.
56042
+
56043
+ ================================================================
56044
+ HARD RULES
56045
+ ================================================================
56046
+ - web_search is your ONLY tool. You cannot read or write files, run commands, or delegate.
56047
+ - Never invent sources. If a search returns nothing useful, say so in \`areasOfUncertainty\`.
56048
+ - Never echo other members' responses verbatim. Paraphrase or quote with attribution.
56049
+ - Stay within your role and persona. The architect chose you for a specific perspective.
56050
+ `;
56051
+
56052
+ // src/agents/council-moderator.ts
56053
+ function createCouncilModeratorAgent(model, customPrompt, customAppendPrompt) {
56054
+ let prompt = COUNCIL_MODERATOR_PROMPT;
56055
+ if (customPrompt) {
56056
+ prompt = customPrompt;
56057
+ } else if (customAppendPrompt) {
56058
+ prompt = `${COUNCIL_MODERATOR_PROMPT}
56059
+
56060
+ ${customAppendPrompt}`;
56061
+ }
56062
+ return {
56063
+ name: "council_moderator",
56064
+ description: "General Council moderator. Synthesizes a coherent final answer from member " + "responses; no web search (works on already-gathered content).",
56065
+ config: {
56066
+ model,
56067
+ temperature: 0.3,
56068
+ prompt,
56069
+ tools: {
56070
+ write: false,
56071
+ edit: false,
56072
+ patch: false
56073
+ }
56074
+ }
56075
+ };
56076
+ }
56077
+ var COUNCIL_MODERATOR_PROMPT = `You are the General Council Moderator.
56078
+
56079
+ You are receiving the structural synthesis from a multi-model council deliberation:
56080
+ - Question (and mode: general or spec_review)
56081
+ - All member Round 1 responses with sources
56082
+ - Detected disagreements
56083
+ - Round 2 deliberation responses (if any)
56084
+ - Confidence-weighted consensus claims
56085
+ - Persisting disagreements after deliberation
56086
+
56087
+ Your job: produce a coherent, well-structured final answer for the user.
56088
+
56089
+ ================================================================
56090
+ RULES
56091
+ ================================================================
56092
+
56093
+ 1. LEAD WITH CONSENSUS \u2014 open with the strongest consensus position. Use the
56094
+ confidence-weighted ordering (Quadratic Voting): higher-confidence claims
56095
+ from multiple members rank higher, but evidence quality outranks raw
56096
+ confidence. Never elevate a single confident voice over a well-evidenced
56097
+ contrary majority.
56098
+
56099
+ 2. ACKNOWLEDGE DISAGREEMENT HONESTLY \u2014 for each persisting disagreement, write
56100
+ "experts disagree on X because\u2026" and present the strongest version of each
56101
+ side. Do NOT pretend disagreements are resolved when they are not. Do NOT
56102
+ silently pick a winner.
56103
+
56104
+ 3. CITE THE STRONGEST SOURCES \u2014 link key claims with [title](url) format from
56105
+ the deduplicated source list. Pick the most reputable source for each claim;
56106
+ do not cite duplicates.
56107
+
56108
+ 4. BE CONCISE \u2014 the user wants an answer, not a committee report. Default
56109
+ length: a few short paragraphs plus a bulleted summary. Expand only when
56110
+ the question genuinely requires it.
56111
+
56112
+ ================================================================
56113
+ HARD CONSTRAINTS
56114
+ ================================================================
56115
+
56116
+ - You MUST NOT invent claims that are not present in the council's responses.
56117
+ - You MUST NOT add new web research. If something was missed, say so.
56118
+ - You MUST NOT favor a position based on member confidence alone \u2014 evidence
56119
+ quality is the tie-breaker.
56120
+ - You have NO tools. You write the final synthesis from the input given.
56121
+
56122
+ ================================================================
56123
+ OUTPUT FORMAT
56124
+ ================================================================
56125
+
56126
+ Plain markdown. No code fences. No JSON. Suggested structure:
56127
+
56128
+ # Answer
56129
+
56130
+ <lead consensus position with citation(s)>
56131
+
56132
+ <remaining consensus / context paragraphs as needed>
56133
+
56134
+ ## Where Experts Disagree
56135
+
56136
+ - <topic 1>: <position A> vs <position B>, with sources for each
56137
+ - <topic 2>: ...
56138
+
56139
+ ## Sources
56140
+
56141
+ - [title](url)
56142
+ - ...
56143
+
56144
+ (Omit any section that is empty.)
56145
+ `;
56146
+
55704
56147
  // src/agents/critic.ts
55705
56148
  function parseSoundingBoardResponse(raw) {
55706
56149
  if (typeof raw !== "string" || raw.trim().length === 0)
@@ -57345,6 +57788,19 @@ If you call @coder instead of @${swarmId}_coder, the call will FAIL or go to the
57345
57788
  testEngineer.name = prefixName("test_engineer");
57346
57789
  agents.push(applyOverrides(testEngineer, swarmAgents, swarmPrefix));
57347
57790
  }
57791
+ if (pluginConfig?.council?.general?.enabled === true && !isAgentDisabled("council_member", swarmAgents, swarmPrefix)) {
57792
+ const councilMemberPrompts = getPrompts("council_member");
57793
+ const councilMember = createCouncilMemberAgent(getModel("council_member"), councilMemberPrompts.prompt, councilMemberPrompts.appendPrompt);
57794
+ councilMember.name = prefixName("council_member");
57795
+ agents.push(applyOverrides(councilMember, swarmAgents, swarmPrefix));
57796
+ }
57797
+ if (pluginConfig?.council?.general?.enabled === true && pluginConfig?.council?.general?.moderator === true && !isAgentDisabled("council_moderator", swarmAgents, swarmPrefix)) {
57798
+ const moderatorPrompts = getPrompts("council_moderator");
57799
+ const moderatorModel = pluginConfig?.council?.general?.moderatorModel ?? getModel("council_moderator");
57800
+ const councilModerator = createCouncilModeratorAgent(moderatorModel, moderatorPrompts.prompt, moderatorPrompts.appendPrompt);
57801
+ councilModerator.name = prefixName("council_moderator");
57802
+ agents.push(applyOverrides(councilModerator, swarmAgents, swarmPrefix));
57803
+ }
57348
57804
  if (!isAgentDisabled("docs", swarmAgents, swarmPrefix)) {
57349
57805
  const docsPrompts = getPrompts("docs");
57350
57806
  const docs = createDocsAgent(getModel("docs"), docsPrompts.prompt, docsPrompts.appendPrompt);
@@ -62357,7 +62813,7 @@ var init_curator_drift = __esm(() => {
62357
62813
 
62358
62814
  // src/index.ts
62359
62815
  init_agents();
62360
- import * as path102 from "path";
62816
+ import * as path103 from "path";
62361
62817
 
62362
62818
  // src/background/index.ts
62363
62819
  init_event_bus();
@@ -62656,6 +63112,7 @@ init_benchmark();
62656
63112
  init_checkpoint2();
62657
63113
  init_close();
62658
63114
  init_config2();
63115
+ init_council();
62659
63116
  init_curate();
62660
63117
  init_dark_matter();
62661
63118
  init_diagnose();
@@ -74014,6 +74471,496 @@ var convene_council = createSwarmTool({
74014
74471
  }, null, 2);
74015
74472
  }
74016
74473
  });
74474
+ // src/tools/convene-general-council.ts
74475
+ init_dist();
74476
+ init_zod();
74477
+ init_loader();
74478
+ import * as fs59 from "fs";
74479
+ import * as path73 from "path";
74480
+
74481
+ // src/council/general-council-advisory.ts
74482
+ var ADVISORY_HEADER = "[general_council] (advisory; not blocking)";
74483
+ function pushGeneralCouncilAdvisory(session, result) {
74484
+ if (!session)
74485
+ return;
74486
+ const body2 = renderAdvisoryBody(result);
74487
+ if (!body2)
74488
+ return;
74489
+ session.pendingAdvisoryMessages ??= [];
74490
+ session.pendingAdvisoryMessages.push(`${ADVISORY_HEADER}
74491
+ ${body2}`);
74492
+ }
74493
+ function renderAdvisoryBody(result) {
74494
+ const parts2 = [result.synthesis];
74495
+ if (result.moderatorOutput && result.moderatorOutput.trim().length > 0) {
74496
+ parts2.push("", "### Moderator Output", result.moderatorOutput);
74497
+ }
74498
+ return parts2.join(`
74499
+ `).trim();
74500
+ }
74501
+
74502
+ // src/council/disagreement-detector.ts
74503
+ var MAX_DISAGREEMENTS = 10;
74504
+ var EXPLICIT_DISAGREEMENT_MARKERS = [
74505
+ "i disagree with",
74506
+ "i would push back on",
74507
+ "contrary to",
74508
+ "this contradicts",
74509
+ "unlike "
74510
+ ];
74511
+ var STRONG_RECOMMENDATION_MARKERS = [
74512
+ "recommend",
74513
+ "best approach",
74514
+ "should use",
74515
+ "i suggest",
74516
+ "the answer is",
74517
+ "the right choice is"
74518
+ ];
74519
+ function tokenize(text) {
74520
+ return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3);
74521
+ }
74522
+ function termOverlap(a, b) {
74523
+ const tokensA = new Set(tokenize(a));
74524
+ const tokensB = new Set(tokenize(b));
74525
+ if (tokensA.size === 0 || tokensB.size === 0)
74526
+ return 0;
74527
+ let intersection3 = 0;
74528
+ for (const t of tokensA) {
74529
+ if (tokensB.has(t))
74530
+ intersection3++;
74531
+ }
74532
+ const union3 = tokensA.size + tokensB.size - intersection3;
74533
+ return union3 === 0 ? 0 : intersection3 / union3;
74534
+ }
74535
+ function extractMarkerSentence(response, markers) {
74536
+ const lower = response.toLowerCase();
74537
+ const sentences = response.split(/(?<=[.!?])\s+/);
74538
+ for (const sentence of sentences) {
74539
+ const sentLower = sentence.toLowerCase();
74540
+ if (markers.some((m) => sentLower.includes(m))) {
74541
+ return sentence.trim();
74542
+ }
74543
+ }
74544
+ for (const marker of markers) {
74545
+ const idx = lower.indexOf(marker);
74546
+ if (idx !== -1) {
74547
+ const slice = response.slice(idx, idx + 200);
74548
+ return slice.split(/\n/)[0]?.trim() ?? slice.trim();
74549
+ }
74550
+ }
74551
+ return null;
74552
+ }
74553
+ function dedupeByTopic(disagreements) {
74554
+ const result = [];
74555
+ for (const d of disagreements) {
74556
+ const topicLower = d.topic.toLowerCase();
74557
+ const existing = result.find((r) => r.topic.toLowerCase().includes(topicLower) || topicLower.includes(r.topic.toLowerCase()));
74558
+ if (existing) {
74559
+ for (const pos of d.positions) {
74560
+ if (!existing.positions.some((p) => p.memberId === pos.memberId)) {
74561
+ existing.positions.push(pos);
74562
+ }
74563
+ }
74564
+ } else {
74565
+ result.push(d);
74566
+ }
74567
+ }
74568
+ return result;
74569
+ }
74570
+ function detectExplicitMarkers(responses) {
74571
+ const out2 = [];
74572
+ for (const member of responses) {
74573
+ const markerSentence = extractMarkerSentence(member.response, EXPLICIT_DISAGREEMENT_MARKERS);
74574
+ if (!markerSentence)
74575
+ continue;
74576
+ const position = {
74577
+ memberId: member.memberId,
74578
+ claim: markerSentence,
74579
+ evidence: member.sources[0]?.url ?? "(no source cited in marker sentence)"
74580
+ };
74581
+ out2.push({
74582
+ topic: markerSentence.slice(0, 80),
74583
+ positions: [position]
74584
+ });
74585
+ }
74586
+ return out2;
74587
+ }
74588
+ function extractRecommendation(response) {
74589
+ return extractMarkerSentence(response, STRONG_RECOMMENDATION_MARKERS);
74590
+ }
74591
+ function detectClaimDivergence(responses) {
74592
+ const recommendations = [];
74593
+ for (const member of responses) {
74594
+ const rec = extractRecommendation(member.response);
74595
+ if (!rec)
74596
+ continue;
74597
+ recommendations.push({
74598
+ memberId: member.memberId,
74599
+ text: rec,
74600
+ evidence: member.sources[0]?.url ?? "(no source cited)"
74601
+ });
74602
+ }
74603
+ const out2 = [];
74604
+ for (let i2 = 0;i2 < recommendations.length; i2++) {
74605
+ for (let j = i2 + 1;j < recommendations.length; j++) {
74606
+ const a = recommendations[i2];
74607
+ const b = recommendations[j];
74608
+ if (!a || !b)
74609
+ continue;
74610
+ const topicOverlap = termOverlap(a.text, b.text);
74611
+ if (topicOverlap > 0.4)
74612
+ continue;
74613
+ if (topicOverlap > 0 && topicOverlap < 0.3) {
74614
+ const topic = `${a.text.slice(0, 50)} vs ${b.text.slice(0, 50)}`;
74615
+ out2.push({
74616
+ topic,
74617
+ positions: [
74618
+ { memberId: a.memberId, claim: a.text, evidence: a.evidence },
74619
+ { memberId: b.memberId, claim: b.text, evidence: b.evidence }
74620
+ ]
74621
+ });
74622
+ }
74623
+ }
74624
+ }
74625
+ return out2;
74626
+ }
74627
+ function detectDisagreements(responses) {
74628
+ if (!Array.isArray(responses) || responses.length < 2)
74629
+ return [];
74630
+ const safeResponses = responses.filter((r) => typeof r?.memberId === "string" && typeof r?.response === "string");
74631
+ const explicit = detectExplicitMarkers(safeResponses);
74632
+ const divergent = detectClaimDivergence(safeResponses);
74633
+ const combined = [...explicit, ...divergent];
74634
+ const deduped = dedupeByTopic(combined);
74635
+ return deduped.slice(0, MAX_DISAGREEMENTS);
74636
+ }
74637
+
74638
+ // src/council/general-council-service.ts
74639
+ var CONSENSUS_WEIGHT_THRESHOLD = 0.6;
74640
+ function tokenize2(text) {
74641
+ return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 4);
74642
+ }
74643
+ function similarity(a, b) {
74644
+ const tokensA = new Set(tokenize2(a));
74645
+ const tokensB = new Set(tokenize2(b));
74646
+ if (tokensA.size === 0 || tokensB.size === 0)
74647
+ return 0;
74648
+ let intersection3 = 0;
74649
+ for (const t of tokensA)
74650
+ if (tokensB.has(t))
74651
+ intersection3++;
74652
+ const union3 = tokensA.size + tokensB.size - intersection3;
74653
+ return union3 === 0 ? 0 : intersection3 / union3;
74654
+ }
74655
+ function extractClaims(response) {
74656
+ return response.split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter((s) => s.length >= 30 && s.length <= 400);
74657
+ }
74658
+ function buildConsensusClusters(responses) {
74659
+ if (responses.length < 2)
74660
+ return [];
74661
+ const totalMembers = responses.length;
74662
+ const clusters = [];
74663
+ for (const member of responses) {
74664
+ const confidence = clamp01(member.confidence ?? 0.5);
74665
+ const claims = extractClaims(member.response ?? "");
74666
+ for (const claim of claims) {
74667
+ let assigned = false;
74668
+ for (const cluster of clusters) {
74669
+ if (similarity(cluster.representative, claim) >= 0.5) {
74670
+ if (!cluster.memberIds.has(member.memberId)) {
74671
+ cluster.weightedAgreement += confidence;
74672
+ cluster.memberIds.add(member.memberId);
74673
+ }
74674
+ if (claim.length > cluster.representative.length) {
74675
+ cluster.representative = claim;
74676
+ }
74677
+ assigned = true;
74678
+ break;
74679
+ }
74680
+ }
74681
+ if (!assigned) {
74682
+ clusters.push({
74683
+ representative: claim,
74684
+ weightedAgreement: confidence,
74685
+ memberIds: new Set([member.memberId])
74686
+ });
74687
+ }
74688
+ }
74689
+ }
74690
+ return clusters.filter((c) => c.memberIds.size >= 2 && c.weightedAgreement / totalMembers >= CONSENSUS_WEIGHT_THRESHOLD).sort((a, b) => b.weightedAgreement - a.weightedAgreement || b.memberIds.size - a.memberIds.size).map((c) => c.representative);
74691
+ }
74692
+ function clamp01(n) {
74693
+ if (typeof n !== "number" || Number.isNaN(n))
74694
+ return 0;
74695
+ if (n < 0)
74696
+ return 0;
74697
+ if (n > 1)
74698
+ return 1;
74699
+ return n;
74700
+ }
74701
+ function computePersistingDisagreements(disagreements, round2) {
74702
+ if (disagreements.length === 0)
74703
+ return [];
74704
+ if (round2.length === 0)
74705
+ return disagreements;
74706
+ return disagreements.filter((d) => {
74707
+ const disputants = new Set(d.positions.map((p) => p.memberId));
74708
+ const conceded = round2.some((r) => {
74709
+ if (!disputants.has(r.memberId))
74710
+ return false;
74711
+ if (!r.disagreementTopics?.includes(d.topic))
74712
+ return false;
74713
+ return /\bconcede\b/i.test(r.response ?? "");
74714
+ });
74715
+ return !conceded;
74716
+ });
74717
+ }
74718
+ function dedupeSources(round1, round2) {
74719
+ const seen = new Set;
74720
+ const out2 = [];
74721
+ const allSources = [...round1, ...round2].flatMap((r) => r.sources ?? []);
74722
+ for (const src of allSources) {
74723
+ if (!src?.url)
74724
+ continue;
74725
+ if (seen.has(src.url))
74726
+ continue;
74727
+ seen.add(src.url);
74728
+ out2.push(src);
74729
+ }
74730
+ return out2;
74731
+ }
74732
+ function renderSynthesisMarkdown(question, mode, roundsCompleted, members, consensusPoints, persistingDisagreements, allSources) {
74733
+ const memberLines = members.map((m) => `- ${m.memberId} (${m.model}, ${m.role})`).join(`
74734
+ `);
74735
+ const consensusBlock = consensusPoints.length > 0 ? consensusPoints.map((c) => `- ${c}`).join(`
74736
+ `) : "_No consensus claims reached the weighted-agreement threshold._";
74737
+ const disagreementsBlock = persistingDisagreements.length > 0 ? persistingDisagreements.map((d) => `- **${d.topic}**
74738
+ ` + d.positions.map((p) => ` - ${p.memberId}: ${p.claim}`).join(`
74739
+ `)).join(`
74740
+ `) : "_No persisting disagreements after deliberation._";
74741
+ const sourcesBlock = allSources.length > 0 ? allSources.map((s) => `- [${s.title || s.url}](${s.url})`).join(`
74742
+ `) : "_No sources cited._";
74743
+ return [
74744
+ "## General Council Synthesis",
74745
+ "",
74746
+ `**Question:** ${question}`,
74747
+ `**Mode:** ${mode}`,
74748
+ `**Members:**
74749
+ ${memberLines}`,
74750
+ `**Rounds:** ${roundsCompleted}`,
74751
+ "",
74752
+ "### Consensus",
74753
+ consensusBlock,
74754
+ "",
74755
+ "### Persistent Disagreements",
74756
+ disagreementsBlock,
74757
+ "",
74758
+ "### Sources",
74759
+ sourcesBlock
74760
+ ].join(`
74761
+ `);
74762
+ }
74763
+ function synthesizeGeneralCouncil(question, mode, round1Responses, round2Responses) {
74764
+ const safeRound1 = Array.isArray(round1Responses) ? round1Responses : [];
74765
+ const safeRound2 = Array.isArray(round2Responses) ? round2Responses : [];
74766
+ const disagreements = detectDisagreements(safeRound1);
74767
+ const consensusPoints = buildConsensusClusters(safeRound1);
74768
+ const persistingDisagreements = computePersistingDisagreements(disagreements, safeRound2);
74769
+ const allSources = dedupeSources(safeRound1, safeRound2);
74770
+ const roundsCompleted = safeRound2.length > 0 ? 2 : 1;
74771
+ const synthesis = renderSynthesisMarkdown(question, mode, roundsCompleted, safeRound1, consensusPoints, persistingDisagreements, allSources);
74772
+ return {
74773
+ question,
74774
+ mode,
74775
+ round1Responses: safeRound1,
74776
+ disagreements,
74777
+ round2Responses: safeRound2,
74778
+ synthesis,
74779
+ consensusPoints,
74780
+ persistingDisagreements: persistingDisagreements.map((d) => d.topic),
74781
+ allSources,
74782
+ timestamp: new Date().toISOString()
74783
+ };
74784
+ }
74785
+
74786
+ // src/tools/convene-general-council.ts
74787
+ init_state();
74788
+ init_create_tool();
74789
+ init_resolve_working_directory();
74790
+ var WebSearchResultSchema = exports_external.object({
74791
+ title: exports_external.string(),
74792
+ url: exports_external.string(),
74793
+ snippet: exports_external.string(),
74794
+ query: exports_external.string()
74795
+ });
74796
+ var MemberRoleEnum = exports_external.enum([
74797
+ "generalist",
74798
+ "skeptic",
74799
+ "domain_expert",
74800
+ "devil_advocate",
74801
+ "synthesizer"
74802
+ ]);
74803
+ var Round1ResponseSchema = exports_external.object({
74804
+ memberId: exports_external.string().min(1),
74805
+ model: exports_external.string().min(1),
74806
+ role: MemberRoleEnum,
74807
+ response: exports_external.string(),
74808
+ sources: exports_external.array(WebSearchResultSchema).default([]),
74809
+ searchQueries: exports_external.array(exports_external.string()).default([]),
74810
+ confidence: exports_external.number().min(0).max(1),
74811
+ areasOfUncertainty: exports_external.array(exports_external.string()).default([]),
74812
+ durationMs: exports_external.number().nonnegative().default(0)
74813
+ });
74814
+ var Round2ResponseSchema = Round1ResponseSchema.extend({
74815
+ disagreementTopics: exports_external.array(exports_external.string()).default([])
74816
+ });
74817
+ var ArgsSchema2 = exports_external.object({
74818
+ question: exports_external.string().min(1).max(8000),
74819
+ mode: exports_external.enum(["general", "spec_review"]).default("general"),
74820
+ members: exports_external.array(exports_external.string()).default([]),
74821
+ round1Responses: exports_external.array(Round1ResponseSchema).min(1),
74822
+ round2Responses: exports_external.array(Round2ResponseSchema).optional(),
74823
+ working_directory: exports_external.string().optional()
74824
+ });
74825
+ function buildModeratorPrompt(question, synthesis) {
74826
+ return [
74827
+ "A multi-model council has deliberated on the following question. Your job is to synthesize",
74828
+ "the council output into a single coherent answer for the user, following the rules in your",
74829
+ "system prompt (lead with consensus, acknowledge persisting disagreement honestly, cite the",
74830
+ "strongest sources, be concise, do not invent claims, do not run new searches).",
74831
+ "",
74832
+ `QUESTION:
74833
+ ${question}`,
74834
+ "",
74835
+ "COUNCIL OUTPUT:",
74836
+ synthesis
74837
+ ].join(`
74838
+ `);
74839
+ }
74840
+ var convene_general_council = createSwarmTool({
74841
+ description: "Synthesize responses from a multi-model General Council. Accepts parallel member " + "responses (Round 1, optionally Round 2), detects disagreements, and returns " + "consensus points, persisting disagreements, a structured synthesis, and an optional " + "moderator prompt. Architect-only. Config-gated on council.general.enabled.",
74842
+ args: {
74843
+ question: tool.schema.string().min(1).max(8000).describe("The question put to the council, or the spec text to review."),
74844
+ mode: tool.schema.enum(["general", "spec_review"]).optional().describe('"general" for /swarm council; "spec_review" for SPECIFY-COUNCIL-REVIEW gate.'),
74845
+ members: tool.schema.array(tool.schema.string()).optional().describe("Optional list of member IDs convened (for evidence/audit)."),
74846
+ round1Responses: tool.schema.array(tool.schema.object({
74847
+ memberId: tool.schema.string().min(1),
74848
+ model: tool.schema.string().min(1),
74849
+ role: tool.schema.enum([
74850
+ "generalist",
74851
+ "skeptic",
74852
+ "domain_expert",
74853
+ "devil_advocate",
74854
+ "synthesizer"
74855
+ ]),
74856
+ response: tool.schema.string(),
74857
+ sources: tool.schema.array(tool.schema.object({
74858
+ title: tool.schema.string(),
74859
+ url: tool.schema.string(),
74860
+ snippet: tool.schema.string(),
74861
+ query: tool.schema.string()
74862
+ })).optional(),
74863
+ searchQueries: tool.schema.array(tool.schema.string()).optional(),
74864
+ confidence: tool.schema.number().min(0).max(1),
74865
+ areasOfUncertainty: tool.schema.array(tool.schema.string()).optional(),
74866
+ durationMs: tool.schema.number().nonnegative().optional()
74867
+ })).describe("Round 1 member responses (one per council member)."),
74868
+ round2Responses: tool.schema.array(tool.schema.object({
74869
+ memberId: tool.schema.string().min(1),
74870
+ model: tool.schema.string().min(1),
74871
+ role: tool.schema.enum([
74872
+ "generalist",
74873
+ "skeptic",
74874
+ "domain_expert",
74875
+ "devil_advocate",
74876
+ "synthesizer"
74877
+ ]),
74878
+ response: tool.schema.string(),
74879
+ sources: tool.schema.array(tool.schema.object({
74880
+ title: tool.schema.string(),
74881
+ url: tool.schema.string(),
74882
+ snippet: tool.schema.string(),
74883
+ query: tool.schema.string()
74884
+ })).optional(),
74885
+ searchQueries: tool.schema.array(tool.schema.string()).optional(),
74886
+ confidence: tool.schema.number().min(0).max(1),
74887
+ areasOfUncertainty: tool.schema.array(tool.schema.string()).optional(),
74888
+ durationMs: tool.schema.number().nonnegative().optional(),
74889
+ disagreementTopics: tool.schema.array(tool.schema.string()).optional()
74890
+ })).optional().describe("Round 2 deliberation responses (omit when no deliberation has occurred)."),
74891
+ working_directory: tool.schema.string().optional().describe("Project root for config + evidence path resolution.")
74892
+ },
74893
+ execute: async (args2, directory, ctx) => {
74894
+ const parsed = ArgsSchema2.safeParse(args2);
74895
+ if (!parsed.success) {
74896
+ const fail = {
74897
+ success: false,
74898
+ reason: "invalid_args",
74899
+ message: parsed.error.issues.map((i2) => `${i2.path.join(".")}: ${i2.message}`).join("; ")
74900
+ };
74901
+ return JSON.stringify(fail, null, 2);
74902
+ }
74903
+ const input = parsed.data;
74904
+ const dirResult = resolveWorkingDirectory(input.working_directory, directory);
74905
+ if (!dirResult.success) {
74906
+ const fail = {
74907
+ success: false,
74908
+ reason: "invalid_working_directory",
74909
+ message: dirResult.message
74910
+ };
74911
+ return JSON.stringify(fail, null, 2);
74912
+ }
74913
+ const workingDir = dirResult.directory;
74914
+ const config3 = loadPluginConfig(workingDir);
74915
+ const generalConfig = config3.council?.general;
74916
+ if (!generalConfig || generalConfig.enabled !== true) {
74917
+ const fail = {
74918
+ success: false,
74919
+ reason: "council_general_disabled",
74920
+ message: "convene_general_council requires council.general.enabled: true in opencode-swarm.json."
74921
+ };
74922
+ return JSON.stringify(fail, null, 2);
74923
+ }
74924
+ const round1 = input.round1Responses;
74925
+ const round2 = input.round2Responses ?? [];
74926
+ const result = synthesizeGeneralCouncil(input.question, input.mode, round1, round2);
74927
+ const evidenceDir = path73.join(workingDir, ".swarm", "council", "general");
74928
+ const safeTimestamp = result.timestamp.replace(/[:.]/g, "-");
74929
+ const evidenceFile = `${safeTimestamp}-${input.mode}.json`;
74930
+ const evidencePath = path73.join(evidenceDir, evidenceFile);
74931
+ try {
74932
+ await fs59.promises.mkdir(evidenceDir, { recursive: true });
74933
+ await fs59.promises.writeFile(evidencePath, JSON.stringify(result, null, 2));
74934
+ } catch (err2) {
74935
+ const message = err2 instanceof Error ? err2.message : String(err2);
74936
+ console.warn(`[convene_general_council] Failed to write evidence to ${evidencePath}: ${message}`);
74937
+ }
74938
+ try {
74939
+ const sessionID = ctx?.sessionID;
74940
+ if (sessionID) {
74941
+ const session = getAgentSession(sessionID);
74942
+ if (session) {
74943
+ pushGeneralCouncilAdvisory(session, result);
74944
+ }
74945
+ }
74946
+ } catch {}
74947
+ const moderatorPrompt = generalConfig.moderator === true ? buildModeratorPrompt(input.question, result.synthesis) : undefined;
74948
+ const ok = {
74949
+ success: true,
74950
+ question: input.question,
74951
+ mode: input.mode,
74952
+ roundsCompleted: round2.length > 0 ? 2 : 1,
74953
+ consensusPoints: result.consensusPoints,
74954
+ disagreementsCount: result.disagreements.length,
74955
+ persistingDisagreements: result.persistingDisagreements,
74956
+ allSourcesCount: result.allSources.length,
74957
+ synthesis: result.synthesis,
74958
+ ...moderatorPrompt !== undefined && { moderatorPrompt },
74959
+ evidencePath
74960
+ };
74961
+ return JSON.stringify(ok, null, 2);
74962
+ }
74963
+ });
74017
74964
  // src/tools/curator-analyze.ts
74018
74965
  init_dist();
74019
74966
  init_config();
@@ -74143,7 +75090,7 @@ var CriteriaItemSchema = exports_external.object({
74143
75090
  description: exports_external.string().min(10).max(500),
74144
75091
  mandatory: exports_external.boolean()
74145
75092
  });
74146
- var ArgsSchema2 = exports_external.object({
75093
+ var ArgsSchema3 = exports_external.object({
74147
75094
  taskId: exports_external.string().min(1).regex(/^\d+\.\d+(\.\d+)*$/, "Task ID must be in N.M or N.M.P format"),
74148
75095
  criteria: exports_external.array(CriteriaItemSchema).min(1).max(20),
74149
75096
  working_directory: exports_external.string().optional()
@@ -74160,7 +75107,7 @@ var declare_council_criteria = createSwarmTool({
74160
75107
  working_directory: tool.schema.string().optional().describe("Explicit project root directory. When provided, .swarm/council/ is resolved relative to this path instead of the plugin context directory.")
74161
75108
  },
74162
75109
  async execute(args2, directory) {
74163
- const parsed = ArgsSchema2.safeParse(args2);
75110
+ const parsed = ArgsSchema3.safeParse(args2);
74164
75111
  if (!parsed.success) {
74165
75112
  return JSON.stringify({
74166
75113
  success: false,
@@ -74221,8 +75168,8 @@ init_scope_persistence();
74221
75168
  init_state();
74222
75169
  init_task_id();
74223
75170
  init_create_tool();
74224
- import * as fs59 from "fs";
74225
- import * as path73 from "path";
75171
+ import * as fs60 from "fs";
75172
+ import * as path74 from "path";
74226
75173
  function validateTaskIdFormat2(taskId) {
74227
75174
  return validateTaskIdFormat(taskId);
74228
75175
  }
@@ -74296,8 +75243,8 @@ async function executeDeclareScope(args2, fallbackDir) {
74296
75243
  };
74297
75244
  }
74298
75245
  }
74299
- normalizedDir = path73.normalize(args2.working_directory);
74300
- const pathParts = normalizedDir.split(path73.sep);
75246
+ normalizedDir = path74.normalize(args2.working_directory);
75247
+ const pathParts = normalizedDir.split(path74.sep);
74301
75248
  if (pathParts.includes("..")) {
74302
75249
  return {
74303
75250
  success: false,
@@ -74307,11 +75254,11 @@ async function executeDeclareScope(args2, fallbackDir) {
74307
75254
  ]
74308
75255
  };
74309
75256
  }
74310
- const resolvedDir = path73.resolve(normalizedDir);
75257
+ const resolvedDir = path74.resolve(normalizedDir);
74311
75258
  try {
74312
- const realPath = fs59.realpathSync(resolvedDir);
74313
- const planPath2 = path73.join(realPath, ".swarm", "plan.json");
74314
- if (!fs59.existsSync(planPath2)) {
75259
+ const realPath = fs60.realpathSync(resolvedDir);
75260
+ const planPath2 = path74.join(realPath, ".swarm", "plan.json");
75261
+ if (!fs60.existsSync(planPath2)) {
74315
75262
  return {
74316
75263
  success: false,
74317
75264
  message: `Invalid working_directory: plan not found in "${realPath}"`,
@@ -74334,8 +75281,8 @@ async function executeDeclareScope(args2, fallbackDir) {
74334
75281
  console.warn("[declare-scope] fallbackDir is undefined, falling back to process.cwd()");
74335
75282
  }
74336
75283
  const directory = normalizedDir || fallbackDir;
74337
- const planPath = path73.resolve(directory, ".swarm", "plan.json");
74338
- if (!fs59.existsSync(planPath)) {
75284
+ const planPath = path74.resolve(directory, ".swarm", "plan.json");
75285
+ if (!fs60.existsSync(planPath)) {
74339
75286
  return {
74340
75287
  success: false,
74341
75288
  message: "No plan found",
@@ -74344,7 +75291,7 @@ async function executeDeclareScope(args2, fallbackDir) {
74344
75291
  }
74345
75292
  let planContent;
74346
75293
  try {
74347
- planContent = JSON.parse(fs59.readFileSync(planPath, "utf-8"));
75294
+ planContent = JSON.parse(fs60.readFileSync(planPath, "utf-8"));
74348
75295
  } catch {
74349
75296
  return {
74350
75297
  success: false,
@@ -74374,8 +75321,8 @@ async function executeDeclareScope(args2, fallbackDir) {
74374
75321
  const normalizeErrors = [];
74375
75322
  const dir = normalizedDir || fallbackDir || process.cwd();
74376
75323
  const mergedFiles = rawMergedFiles.map((file3) => {
74377
- if (path73.isAbsolute(file3)) {
74378
- const relativePath = path73.relative(dir, file3).replace(/\\/g, "/");
75324
+ if (path74.isAbsolute(file3)) {
75325
+ const relativePath = path74.relative(dir, file3).replace(/\\/g, "/");
74379
75326
  if (relativePath.startsWith("..")) {
74380
75327
  normalizeErrors.push(`Path '${file3}' resolves outside the project directory`);
74381
75328
  return file3;
@@ -74435,8 +75382,8 @@ var declare_scope = createSwarmTool({
74435
75382
  // src/tools/diff.ts
74436
75383
  init_dist();
74437
75384
  import * as child_process7 from "child_process";
74438
- import * as fs60 from "fs";
74439
- import * as path74 from "path";
75385
+ import * as fs61 from "fs";
75386
+ import * as path75 from "path";
74440
75387
  init_create_tool();
74441
75388
  var MAX_DIFF_LINES = 500;
74442
75389
  var DIFF_TIMEOUT_MS = 30000;
@@ -74465,20 +75412,20 @@ function validateBase(base) {
74465
75412
  function validatePaths(paths) {
74466
75413
  if (!paths)
74467
75414
  return null;
74468
- for (const path75 of paths) {
74469
- if (!path75 || path75.length === 0) {
75415
+ for (const path76 of paths) {
75416
+ if (!path76 || path76.length === 0) {
74470
75417
  return "empty path not allowed";
74471
75418
  }
74472
- if (path75.length > MAX_PATH_LENGTH) {
75419
+ if (path76.length > MAX_PATH_LENGTH) {
74473
75420
  return `path exceeds maximum length of ${MAX_PATH_LENGTH}`;
74474
75421
  }
74475
- if (SHELL_METACHARACTERS2.test(path75)) {
75422
+ if (SHELL_METACHARACTERS2.test(path76)) {
74476
75423
  return "path contains shell metacharacters";
74477
75424
  }
74478
- if (path75.startsWith("-")) {
75425
+ if (path76.startsWith("-")) {
74479
75426
  return 'path cannot start with "-" (option-like arguments not allowed)';
74480
75427
  }
74481
- if (CONTROL_CHAR_PATTERN2.test(path75)) {
75428
+ if (CONTROL_CHAR_PATTERN2.test(path76)) {
74482
75429
  return "path contains control characters";
74483
75430
  }
74484
75431
  }
@@ -74584,8 +75531,8 @@ var diff = createSwarmTool({
74584
75531
  if (parts2.length >= 3) {
74585
75532
  const additions = parseInt(parts2[0], 10) || 0;
74586
75533
  const deletions = parseInt(parts2[1], 10) || 0;
74587
- const path75 = parts2[2];
74588
- files.push({ path: path75, additions, deletions });
75534
+ const path76 = parts2[2];
75535
+ files.push({ path: path76, additions, deletions });
74589
75536
  }
74590
75537
  }
74591
75538
  const contractChanges = [];
@@ -74625,7 +75572,7 @@ var diff = createSwarmTool({
74625
75572
  } else if (base === "unstaged") {
74626
75573
  const oldRef = `:${file3.path}`;
74627
75574
  oldContent = fileExistsInRef(oldRef) ? getContentFromRef(oldRef) : "";
74628
- newContent = fs60.readFileSync(path74.join(directory, file3.path), "utf-8");
75575
+ newContent = fs61.readFileSync(path75.join(directory, file3.path), "utf-8");
74629
75576
  } else {
74630
75577
  const oldRef = `${base}:${file3.path}`;
74631
75578
  oldContent = fileExistsInRef(oldRef) ? getContentFromRef(oldRef) : "";
@@ -74699,8 +75646,8 @@ var diff = createSwarmTool({
74699
75646
  // src/tools/diff-summary.ts
74700
75647
  init_dist();
74701
75648
  import * as child_process8 from "child_process";
74702
- import * as fs61 from "fs";
74703
- import * as path75 from "path";
75649
+ import * as fs62 from "fs";
75650
+ import * as path76 from "path";
74704
75651
  init_create_tool();
74705
75652
  var diff_summary = createSwarmTool({
74706
75653
  description: "Generate a filtered semantic diff summary from AST analysis. Returns SemanticDiffSummary with optional filtering by classification or riskLevel.",
@@ -74748,7 +75695,7 @@ var diff_summary = createSwarmTool({
74748
75695
  }
74749
75696
  try {
74750
75697
  let oldContent;
74751
- const newContent = fs61.readFileSync(path75.join(workingDir, filePath), "utf-8");
75698
+ const newContent = fs62.readFileSync(path76.join(workingDir, filePath), "utf-8");
74752
75699
  if (fileExistsInHead) {
74753
75700
  oldContent = child_process8.execFileSync("git", ["show", `HEAD:${filePath}`], {
74754
75701
  encoding: "utf-8",
@@ -74975,8 +75922,8 @@ Use these as DOMAIN values when delegating to @sme.`;
74975
75922
  init_dist();
74976
75923
  init_create_tool();
74977
75924
  init_path_security();
74978
- import * as fs62 from "fs";
74979
- import * as path76 from "path";
75925
+ import * as fs63 from "fs";
75926
+ import * as path77 from "path";
74980
75927
  var MAX_FILE_SIZE_BYTES6 = 1024 * 1024;
74981
75928
  var MAX_EVIDENCE_FILES = 1000;
74982
75929
  var EVIDENCE_DIR3 = ".swarm/evidence";
@@ -75003,9 +75950,9 @@ function validateRequiredTypes(input) {
75003
75950
  return null;
75004
75951
  }
75005
75952
  function isPathWithinSwarm2(filePath, cwd) {
75006
- const normalizedCwd = path76.resolve(cwd);
75007
- const swarmPath = path76.join(normalizedCwd, ".swarm");
75008
- const normalizedPath = path76.resolve(filePath);
75953
+ const normalizedCwd = path77.resolve(cwd);
75954
+ const swarmPath = path77.join(normalizedCwd, ".swarm");
75955
+ const normalizedPath = path77.resolve(filePath);
75009
75956
  return normalizedPath.startsWith(swarmPath);
75010
75957
  }
75011
75958
  function parseCompletedTasks(planContent) {
@@ -75021,12 +75968,12 @@ function parseCompletedTasks(planContent) {
75021
75968
  }
75022
75969
  function readEvidenceFiles(evidenceDir, _cwd) {
75023
75970
  const evidence = [];
75024
- if (!fs62.existsSync(evidenceDir) || !fs62.statSync(evidenceDir).isDirectory()) {
75971
+ if (!fs63.existsSync(evidenceDir) || !fs63.statSync(evidenceDir).isDirectory()) {
75025
75972
  return evidence;
75026
75973
  }
75027
75974
  let files;
75028
75975
  try {
75029
- files = fs62.readdirSync(evidenceDir);
75976
+ files = fs63.readdirSync(evidenceDir);
75030
75977
  } catch {
75031
75978
  return evidence;
75032
75979
  }
@@ -75035,14 +75982,14 @@ function readEvidenceFiles(evidenceDir, _cwd) {
75035
75982
  if (!VALID_EVIDENCE_FILENAME_REGEX.test(filename)) {
75036
75983
  continue;
75037
75984
  }
75038
- const filePath = path76.join(evidenceDir, filename);
75985
+ const filePath = path77.join(evidenceDir, filename);
75039
75986
  try {
75040
- const resolvedPath = path76.resolve(filePath);
75041
- const evidenceDirResolved = path76.resolve(evidenceDir);
75987
+ const resolvedPath = path77.resolve(filePath);
75988
+ const evidenceDirResolved = path77.resolve(evidenceDir);
75042
75989
  if (!resolvedPath.startsWith(evidenceDirResolved)) {
75043
75990
  continue;
75044
75991
  }
75045
- const stat4 = fs62.lstatSync(filePath);
75992
+ const stat4 = fs63.lstatSync(filePath);
75046
75993
  if (!stat4.isFile()) {
75047
75994
  continue;
75048
75995
  }
@@ -75051,7 +75998,7 @@ function readEvidenceFiles(evidenceDir, _cwd) {
75051
75998
  }
75052
75999
  let fileStat;
75053
76000
  try {
75054
- fileStat = fs62.statSync(filePath);
76001
+ fileStat = fs63.statSync(filePath);
75055
76002
  if (fileStat.size > MAX_FILE_SIZE_BYTES6) {
75056
76003
  continue;
75057
76004
  }
@@ -75060,7 +76007,7 @@ function readEvidenceFiles(evidenceDir, _cwd) {
75060
76007
  }
75061
76008
  let content;
75062
76009
  try {
75063
- content = fs62.readFileSync(filePath, "utf-8");
76010
+ content = fs63.readFileSync(filePath, "utf-8");
75064
76011
  } catch {
75065
76012
  continue;
75066
76013
  }
@@ -75156,7 +76103,7 @@ var evidence_check = createSwarmTool({
75156
76103
  return JSON.stringify(errorResult, null, 2);
75157
76104
  }
75158
76105
  const requiredTypes = requiredTypesValue.split(",").map((t) => t.trim()).filter((t) => t.length > 0).map(normalizeEvidenceType);
75159
- const planPath = path76.join(cwd, PLAN_FILE);
76106
+ const planPath = path77.join(cwd, PLAN_FILE);
75160
76107
  if (!isPathWithinSwarm2(planPath, cwd)) {
75161
76108
  const errorResult = {
75162
76109
  error: "plan file path validation failed",
@@ -75170,7 +76117,7 @@ var evidence_check = createSwarmTool({
75170
76117
  }
75171
76118
  let planContent;
75172
76119
  try {
75173
- planContent = fs62.readFileSync(planPath, "utf-8");
76120
+ planContent = fs63.readFileSync(planPath, "utf-8");
75174
76121
  } catch {
75175
76122
  const result2 = {
75176
76123
  message: "No completed tasks found in plan.",
@@ -75188,7 +76135,7 @@ var evidence_check = createSwarmTool({
75188
76135
  };
75189
76136
  return JSON.stringify(result2, null, 2);
75190
76137
  }
75191
- const evidenceDir = path76.join(cwd, EVIDENCE_DIR3);
76138
+ const evidenceDir = path77.join(cwd, EVIDENCE_DIR3);
75192
76139
  const evidence = readEvidenceFiles(evidenceDir, cwd);
75193
76140
  const { tasksWithFullEvidence, gaps } = analyzeGaps(completedTasks, evidence, requiredTypes);
75194
76141
  const completeness = completedTasks.length > 0 ? Math.round(tasksWithFullEvidence.length / completedTasks.length * 100) / 100 : 1;
@@ -75205,8 +76152,8 @@ var evidence_check = createSwarmTool({
75205
76152
  // src/tools/file-extractor.ts
75206
76153
  init_tool();
75207
76154
  init_create_tool();
75208
- import * as fs63 from "fs";
75209
- import * as path77 from "path";
76155
+ import * as fs64 from "fs";
76156
+ import * as path78 from "path";
75210
76157
  var EXT_MAP = {
75211
76158
  python: ".py",
75212
76159
  py: ".py",
@@ -75268,8 +76215,8 @@ var extract_code_blocks = createSwarmTool({
75268
76215
  execute: async (args2, directory) => {
75269
76216
  const { content, output_dir, prefix } = args2;
75270
76217
  const targetDir = output_dir || directory;
75271
- if (!fs63.existsSync(targetDir)) {
75272
- fs63.mkdirSync(targetDir, { recursive: true });
76218
+ if (!fs64.existsSync(targetDir)) {
76219
+ fs64.mkdirSync(targetDir, { recursive: true });
75273
76220
  }
75274
76221
  if (!content) {
75275
76222
  return "Error: content is required";
@@ -75287,16 +76234,16 @@ var extract_code_blocks = createSwarmTool({
75287
76234
  if (prefix) {
75288
76235
  filename = `${prefix}_${filename}`;
75289
76236
  }
75290
- let filepath = path77.join(targetDir, filename);
75291
- const base = path77.basename(filepath, path77.extname(filepath));
75292
- const ext = path77.extname(filepath);
76237
+ let filepath = path78.join(targetDir, filename);
76238
+ const base = path78.basename(filepath, path78.extname(filepath));
76239
+ const ext = path78.extname(filepath);
75293
76240
  let counter = 1;
75294
- while (fs63.existsSync(filepath)) {
75295
- filepath = path77.join(targetDir, `${base}_${counter}${ext}`);
76241
+ while (fs64.existsSync(filepath)) {
76242
+ filepath = path78.join(targetDir, `${base}_${counter}${ext}`);
75296
76243
  counter++;
75297
76244
  }
75298
76245
  try {
75299
- fs63.writeFileSync(filepath, code.trim(), "utf-8");
76246
+ fs64.writeFileSync(filepath, code.trim(), "utf-8");
75300
76247
  savedFiles.push(filepath);
75301
76248
  } catch (error93) {
75302
76249
  errors5.push(`Failed to save ${filename}: ${error93 instanceof Error ? error93.message : String(error93)}`);
@@ -75555,8 +76502,8 @@ var gitingest = createSwarmTool({
75555
76502
  init_dist();
75556
76503
  init_create_tool();
75557
76504
  init_path_security();
75558
- import * as fs64 from "fs";
75559
- import * as path78 from "path";
76505
+ import * as fs65 from "fs";
76506
+ import * as path79 from "path";
75560
76507
  var MAX_FILE_PATH_LENGTH2 = 500;
75561
76508
  var MAX_SYMBOL_LENGTH = 256;
75562
76509
  var MAX_FILE_SIZE_BYTES7 = 1024 * 1024;
@@ -75604,7 +76551,7 @@ function validateSymbolInput(symbol3) {
75604
76551
  return null;
75605
76552
  }
75606
76553
  function isBinaryFile2(filePath, buffer) {
75607
- const ext = path78.extname(filePath).toLowerCase();
76554
+ const ext = path79.extname(filePath).toLowerCase();
75608
76555
  if (ext === ".json" || ext === ".md" || ext === ".txt") {
75609
76556
  return false;
75610
76557
  }
@@ -75628,15 +76575,15 @@ function parseImports(content, targetFile, targetSymbol) {
75628
76575
  const imports = [];
75629
76576
  let _resolvedTarget;
75630
76577
  try {
75631
- _resolvedTarget = path78.resolve(targetFile);
76578
+ _resolvedTarget = path79.resolve(targetFile);
75632
76579
  } catch {
75633
76580
  _resolvedTarget = targetFile;
75634
76581
  }
75635
- const targetBasename = path78.basename(targetFile, path78.extname(targetFile));
76582
+ const targetBasename = path79.basename(targetFile, path79.extname(targetFile));
75636
76583
  const targetWithExt = targetFile;
75637
76584
  const targetWithoutExt = targetFile.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/i, "");
75638
- const normalizedTargetWithExt = path78.normalize(targetWithExt).replace(/\\/g, "/");
75639
- const normalizedTargetWithoutExt = path78.normalize(targetWithoutExt).replace(/\\/g, "/");
76585
+ const normalizedTargetWithExt = path79.normalize(targetWithExt).replace(/\\/g, "/");
76586
+ const normalizedTargetWithoutExt = path79.normalize(targetWithoutExt).replace(/\\/g, "/");
75640
76587
  const importRegex = /import\s+(?:\{[\s\S]*?\}|(?:\*\s+as\s+\w+)|\w+)\s+from\s+['"`]([^'"`]+)['"`]|import\s+['"`]([^'"`]+)['"`]|require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
75641
76588
  for (let match = importRegex.exec(content);match !== null; match = importRegex.exec(content)) {
75642
76589
  const modulePath = match[1] || match[2] || match[3];
@@ -75659,9 +76606,9 @@ function parseImports(content, targetFile, targetSymbol) {
75659
76606
  }
75660
76607
  const _normalizedModule = modulePath.replace(/^\.\//, "").replace(/^\.\.\\/, "../");
75661
76608
  let isMatch = false;
75662
- const _targetDir = path78.dirname(targetFile);
75663
- const targetExt = path78.extname(targetFile);
75664
- const targetBasenameNoExt = path78.basename(targetFile, targetExt);
76609
+ const _targetDir = path79.dirname(targetFile);
76610
+ const targetExt = path79.extname(targetFile);
76611
+ const targetBasenameNoExt = path79.basename(targetFile, targetExt);
75665
76612
  const moduleNormalized = modulePath.replace(/\\/g, "/").replace(/^\.\//, "");
75666
76613
  const moduleName = modulePath.split(/[/\\]/).pop() || "";
75667
76614
  const moduleNameNoExt = moduleName.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/i, "");
@@ -75718,7 +76665,7 @@ var SKIP_DIRECTORIES4 = new Set([
75718
76665
  function findSourceFiles3(dir, files = [], stats = { skippedDirs: [], skippedFiles: 0, fileErrors: [] }) {
75719
76666
  let entries;
75720
76667
  try {
75721
- entries = fs64.readdirSync(dir);
76668
+ entries = fs65.readdirSync(dir);
75722
76669
  } catch (e) {
75723
76670
  stats.fileErrors.push({
75724
76671
  path: dir,
@@ -75729,13 +76676,13 @@ function findSourceFiles3(dir, files = [], stats = { skippedDirs: [], skippedFil
75729
76676
  entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
75730
76677
  for (const entry of entries) {
75731
76678
  if (SKIP_DIRECTORIES4.has(entry)) {
75732
- stats.skippedDirs.push(path78.join(dir, entry));
76679
+ stats.skippedDirs.push(path79.join(dir, entry));
75733
76680
  continue;
75734
76681
  }
75735
- const fullPath = path78.join(dir, entry);
76682
+ const fullPath = path79.join(dir, entry);
75736
76683
  let stat4;
75737
76684
  try {
75738
- stat4 = fs64.statSync(fullPath);
76685
+ stat4 = fs65.statSync(fullPath);
75739
76686
  } catch (e) {
75740
76687
  stats.fileErrors.push({
75741
76688
  path: fullPath,
@@ -75746,7 +76693,7 @@ function findSourceFiles3(dir, files = [], stats = { skippedDirs: [], skippedFil
75746
76693
  if (stat4.isDirectory()) {
75747
76694
  findSourceFiles3(fullPath, files, stats);
75748
76695
  } else if (stat4.isFile()) {
75749
- const ext = path78.extname(fullPath).toLowerCase();
76696
+ const ext = path79.extname(fullPath).toLowerCase();
75750
76697
  if (SUPPORTED_EXTENSIONS3.includes(ext)) {
75751
76698
  files.push(fullPath);
75752
76699
  }
@@ -75803,8 +76750,8 @@ var imports = createSwarmTool({
75803
76750
  return JSON.stringify(errorResult, null, 2);
75804
76751
  }
75805
76752
  try {
75806
- const targetFile = path78.resolve(file3);
75807
- if (!fs64.existsSync(targetFile)) {
76753
+ const targetFile = path79.resolve(file3);
76754
+ if (!fs65.existsSync(targetFile)) {
75808
76755
  const errorResult = {
75809
76756
  error: `target file not found: ${file3}`,
75810
76757
  target: file3,
@@ -75814,7 +76761,7 @@ var imports = createSwarmTool({
75814
76761
  };
75815
76762
  return JSON.stringify(errorResult, null, 2);
75816
76763
  }
75817
- const targetStat = fs64.statSync(targetFile);
76764
+ const targetStat = fs65.statSync(targetFile);
75818
76765
  if (!targetStat.isFile()) {
75819
76766
  const errorResult = {
75820
76767
  error: "target must be a file, not a directory",
@@ -75825,7 +76772,7 @@ var imports = createSwarmTool({
75825
76772
  };
75826
76773
  return JSON.stringify(errorResult, null, 2);
75827
76774
  }
75828
- const baseDir = path78.dirname(targetFile);
76775
+ const baseDir = path79.dirname(targetFile);
75829
76776
  const scanStats = {
75830
76777
  skippedDirs: [],
75831
76778
  skippedFiles: 0,
@@ -75840,12 +76787,12 @@ var imports = createSwarmTool({
75840
76787
  if (consumers.length >= MAX_CONSUMERS)
75841
76788
  break;
75842
76789
  try {
75843
- const stat4 = fs64.statSync(filePath);
76790
+ const stat4 = fs65.statSync(filePath);
75844
76791
  if (stat4.size > MAX_FILE_SIZE_BYTES7) {
75845
76792
  skippedFileCount++;
75846
76793
  continue;
75847
76794
  }
75848
- const buffer = fs64.readFileSync(filePath);
76795
+ const buffer = fs65.readFileSync(filePath);
75849
76796
  if (isBinaryFile2(filePath, buffer)) {
75850
76797
  skippedFileCount++;
75851
76798
  continue;
@@ -76357,8 +77304,8 @@ init_schema();
76357
77304
  init_qa_gate_profile();
76358
77305
  init_manager2();
76359
77306
  init_curator();
76360
- import * as fs65 from "fs";
76361
- import * as path79 from "path";
77307
+ import * as fs66 from "fs";
77308
+ import * as path80 from "path";
76362
77309
  init_knowledge_curator();
76363
77310
  init_knowledge_reader();
76364
77311
  init_knowledge_store();
@@ -76589,11 +77536,11 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76589
77536
  safeWarn(`[phase_complete] Completion verify error (non-blocking):`, completionError);
76590
77537
  }
76591
77538
  try {
76592
- const driftEvidencePath = path79.join(dir, ".swarm", "evidence", String(phase), "drift-verifier.json");
77539
+ const driftEvidencePath = path80.join(dir, ".swarm", "evidence", String(phase), "drift-verifier.json");
76593
77540
  let driftVerdictFound = false;
76594
77541
  let driftVerdictApproved = false;
76595
77542
  try {
76596
- const driftEvidenceContent = fs65.readFileSync(driftEvidencePath, "utf-8");
77543
+ const driftEvidenceContent = fs66.readFileSync(driftEvidencePath, "utf-8");
76597
77544
  const driftEvidence = JSON.parse(driftEvidenceContent);
76598
77545
  const entries = driftEvidence.entries ?? [];
76599
77546
  for (const entry of entries) {
@@ -76623,14 +77570,14 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76623
77570
  driftVerdictFound = false;
76624
77571
  }
76625
77572
  if (!driftVerdictFound) {
76626
- const specPath = path79.join(dir, ".swarm", "spec.md");
76627
- const specExists = fs65.existsSync(specPath);
77573
+ const specPath = path80.join(dir, ".swarm", "spec.md");
77574
+ const specExists = fs66.existsSync(specPath);
76628
77575
  if (!specExists) {
76629
77576
  let incompleteTaskCount = 0;
76630
77577
  let planPhaseFound = false;
76631
77578
  try {
76632
77579
  const planPath = validateSwarmPath(dir, "plan.json");
76633
- const planRaw = fs65.readFileSync(planPath, "utf-8");
77580
+ const planRaw = fs66.readFileSync(planPath, "utf-8");
76634
77581
  const plan = JSON.parse(planRaw);
76635
77582
  const targetPhase = plan.phases.find((p) => p.id === phase);
76636
77583
  if (targetPhase) {
@@ -76681,11 +77628,11 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76681
77628
  const overrides = session2?.qaGateSessionOverrides ?? {};
76682
77629
  const effective = getEffectiveGates(profile, overrides);
76683
77630
  if (effective.hallucination_guard === true) {
76684
- const hgPath = path79.join(dir, ".swarm", "evidence", String(phase), "hallucination-guard.json");
77631
+ const hgPath = path80.join(dir, ".swarm", "evidence", String(phase), "hallucination-guard.json");
76685
77632
  let hgVerdictFound = false;
76686
77633
  let hgVerdictApproved = false;
76687
77634
  try {
76688
- const hgContent = fs65.readFileSync(hgPath, "utf-8");
77635
+ const hgContent = fs66.readFileSync(hgPath, "utf-8");
76689
77636
  const hgBundle = JSON.parse(hgContent);
76690
77637
  for (const entry of hgBundle.entries ?? []) {
76691
77638
  if (typeof entry.type === "string" && entry.type.includes("hallucination") && typeof entry.verdict === "string") {
@@ -76753,11 +77700,11 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76753
77700
  const overrides = session2?.qaGateSessionOverrides ?? {};
76754
77701
  const effective = getEffectiveGates(profile, overrides);
76755
77702
  if (effective.mutation_test === true) {
76756
- const mgPath = path79.join(dir, ".swarm", "evidence", String(phase), "mutation-gate.json");
77703
+ const mgPath = path80.join(dir, ".swarm", "evidence", String(phase), "mutation-gate.json");
76757
77704
  let mgVerdictFound = false;
76758
77705
  let mgVerdict;
76759
77706
  try {
76760
- const mgContent = fs65.readFileSync(mgPath, "utf-8");
77707
+ const mgContent = fs66.readFileSync(mgPath, "utf-8");
76761
77708
  const mgBundle = JSON.parse(mgContent);
76762
77709
  for (const entry of mgBundle.entries ?? []) {
76763
77710
  if (typeof entry.type === "string" && entry.type === "mutation-gate" && typeof entry.verdict === "string") {
@@ -76825,7 +77772,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76825
77772
  }
76826
77773
  if (retroFound && retroEntry?.lessons_learned && retroEntry.lessons_learned.length > 0) {
76827
77774
  try {
76828
- const projectName = path79.basename(dir);
77775
+ const projectName = path80.basename(dir);
76829
77776
  const curationResult = await curateAndStoreSwarm(retroEntry.lessons_learned, projectName, { phase_number: phase }, dir, knowledgeConfig);
76830
77777
  if (curationResult) {
76831
77778
  const sessionState = swarmState.agentSessions.get(sessionID);
@@ -76905,7 +77852,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76905
77852
  let phaseRequiredAgents;
76906
77853
  try {
76907
77854
  const planPath = validateSwarmPath(dir, "plan.json");
76908
- const planRaw = fs65.readFileSync(planPath, "utf-8");
77855
+ const planRaw = fs66.readFileSync(planPath, "utf-8");
76909
77856
  const plan = JSON.parse(planRaw);
76910
77857
  const phaseObj = plan.phases.find((p) => p.id === phase);
76911
77858
  phaseRequiredAgents = phaseObj?.required_agents;
@@ -76920,7 +77867,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76920
77867
  if (agentsMissing.length > 0) {
76921
77868
  try {
76922
77869
  const planPath = validateSwarmPath(dir, "plan.json");
76923
- const planRaw = fs65.readFileSync(planPath, "utf-8");
77870
+ const planRaw = fs66.readFileSync(planPath, "utf-8");
76924
77871
  const plan = JSON.parse(planRaw);
76925
77872
  const targetPhase = plan.phases.find((p) => p.id === phase);
76926
77873
  if (targetPhase && targetPhase.tasks.length > 0 && targetPhase.tasks.every((t) => t.status === "completed")) {
@@ -76960,7 +77907,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76960
77907
  if (phaseCompleteConfig.regression_sweep?.enforce) {
76961
77908
  try {
76962
77909
  const planPath = validateSwarmPath(dir, "plan.json");
76963
- const planRaw = fs65.readFileSync(planPath, "utf-8");
77910
+ const planRaw = fs66.readFileSync(planPath, "utf-8");
76964
77911
  const plan = JSON.parse(planRaw);
76965
77912
  const targetPhase = plan.phases.find((p) => p.id === phase);
76966
77913
  if (targetPhase) {
@@ -77014,7 +77961,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
77014
77961
  }
77015
77962
  try {
77016
77963
  const eventsPath = validateSwarmPath(dir, "events.jsonl");
77017
- fs65.appendFileSync(eventsPath, `${JSON.stringify(event)}
77964
+ fs66.appendFileSync(eventsPath, `${JSON.stringify(event)}
77018
77965
  `, "utf-8");
77019
77966
  } catch (writeError) {
77020
77967
  warnings.push(`Warning: failed to write phase complete event: ${writeError instanceof Error ? writeError.message : String(writeError)}`);
@@ -77089,12 +78036,12 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
77089
78036
  warnings.push(`Warning: failed to update plan.json phase status`);
77090
78037
  try {
77091
78038
  const planPath = validateSwarmPath(dir, "plan.json");
77092
- const planRaw = fs65.readFileSync(planPath, "utf-8");
78039
+ const planRaw = fs66.readFileSync(planPath, "utf-8");
77093
78040
  const plan2 = JSON.parse(planRaw);
77094
78041
  const phaseObj = plan2.phases.find((p) => p.id === phase);
77095
78042
  if (phaseObj) {
77096
78043
  phaseObj.status = "complete";
77097
- fs65.writeFileSync(planPath, JSON.stringify(plan2, null, 2), "utf-8");
78044
+ fs66.writeFileSync(planPath, JSON.stringify(plan2, null, 2), "utf-8");
77098
78045
  }
77099
78046
  } catch {}
77100
78047
  } else if (plan) {
@@ -77131,12 +78078,12 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
77131
78078
  warnings.push(`Warning: failed to update plan.json phase status`);
77132
78079
  try {
77133
78080
  const planPath = validateSwarmPath(dir, "plan.json");
77134
- const planRaw = fs65.readFileSync(planPath, "utf-8");
78081
+ const planRaw = fs66.readFileSync(planPath, "utf-8");
77135
78082
  const plan = JSON.parse(planRaw);
77136
78083
  const phaseObj = plan.phases.find((p) => p.id === phase);
77137
78084
  if (phaseObj) {
77138
78085
  phaseObj.status = "complete";
77139
- fs65.writeFileSync(planPath, JSON.stringify(plan, null, 2), "utf-8");
78086
+ fs66.writeFileSync(planPath, JSON.stringify(plan, null, 2), "utf-8");
77140
78087
  }
77141
78088
  } catch {}
77142
78089
  }
@@ -77193,8 +78140,8 @@ init_dist();
77193
78140
  init_discovery();
77194
78141
  init_utils();
77195
78142
  init_create_tool();
77196
- import * as fs66 from "fs";
77197
- import * as path80 from "path";
78143
+ import * as fs67 from "fs";
78144
+ import * as path81 from "path";
77198
78145
  var MAX_OUTPUT_BYTES5 = 52428800;
77199
78146
  var AUDIT_TIMEOUT_MS = 120000;
77200
78147
  function isValidEcosystem(value) {
@@ -77222,31 +78169,31 @@ function validateArgs3(args2) {
77222
78169
  function detectEcosystems(directory) {
77223
78170
  const ecosystems = [];
77224
78171
  const cwd = directory;
77225
- if (fs66.existsSync(path80.join(cwd, "package.json"))) {
78172
+ if (fs67.existsSync(path81.join(cwd, "package.json"))) {
77226
78173
  ecosystems.push("npm");
77227
78174
  }
77228
- if (fs66.existsSync(path80.join(cwd, "pyproject.toml")) || fs66.existsSync(path80.join(cwd, "requirements.txt"))) {
78175
+ if (fs67.existsSync(path81.join(cwd, "pyproject.toml")) || fs67.existsSync(path81.join(cwd, "requirements.txt"))) {
77229
78176
  ecosystems.push("pip");
77230
78177
  }
77231
- if (fs66.existsSync(path80.join(cwd, "Cargo.toml"))) {
78178
+ if (fs67.existsSync(path81.join(cwd, "Cargo.toml"))) {
77232
78179
  ecosystems.push("cargo");
77233
78180
  }
77234
- if (fs66.existsSync(path80.join(cwd, "go.mod"))) {
78181
+ if (fs67.existsSync(path81.join(cwd, "go.mod"))) {
77235
78182
  ecosystems.push("go");
77236
78183
  }
77237
78184
  try {
77238
- const files = fs66.readdirSync(cwd);
78185
+ const files = fs67.readdirSync(cwd);
77239
78186
  if (files.some((f) => f.endsWith(".csproj") || f.endsWith(".sln"))) {
77240
78187
  ecosystems.push("dotnet");
77241
78188
  }
77242
78189
  } catch {}
77243
- if (fs66.existsSync(path80.join(cwd, "Gemfile")) || fs66.existsSync(path80.join(cwd, "Gemfile.lock"))) {
78190
+ if (fs67.existsSync(path81.join(cwd, "Gemfile")) || fs67.existsSync(path81.join(cwd, "Gemfile.lock"))) {
77244
78191
  ecosystems.push("ruby");
77245
78192
  }
77246
- if (fs66.existsSync(path80.join(cwd, "pubspec.yaml"))) {
78193
+ if (fs67.existsSync(path81.join(cwd, "pubspec.yaml"))) {
77247
78194
  ecosystems.push("dart");
77248
78195
  }
77249
- if (fs66.existsSync(path80.join(cwd, "composer.lock"))) {
78196
+ if (fs67.existsSync(path81.join(cwd, "composer.lock"))) {
77250
78197
  ecosystems.push("composer");
77251
78198
  }
77252
78199
  return ecosystems;
@@ -78405,8 +79352,8 @@ var pkg_audit = createSwarmTool({
78405
79352
  // src/tools/placeholder-scan.ts
78406
79353
  init_dist();
78407
79354
  init_manager2();
78408
- import * as fs67 from "fs";
78409
- import * as path81 from "path";
79355
+ import * as fs68 from "fs";
79356
+ import * as path82 from "path";
78410
79357
  init_utils();
78411
79358
  init_create_tool();
78412
79359
  var MAX_FILE_SIZE = 1024 * 1024;
@@ -78529,7 +79476,7 @@ function isScaffoldFile(filePath) {
78529
79476
  if (SCAFFOLD_PATH_PATTERNS.some((pattern) => pattern.test(normalizedPath))) {
78530
79477
  return true;
78531
79478
  }
78532
- const filename = path81.basename(filePath);
79479
+ const filename = path82.basename(filePath);
78533
79480
  if (SCAFFOLD_FILENAME_PATTERNS.some((pattern) => pattern.test(filename))) {
78534
79481
  return true;
78535
79482
  }
@@ -78546,7 +79493,7 @@ function isAllowedByGlobs(filePath, allowGlobs) {
78546
79493
  if (regex.test(normalizedPath)) {
78547
79494
  return true;
78548
79495
  }
78549
- const filename = path81.basename(filePath);
79496
+ const filename = path82.basename(filePath);
78550
79497
  const filenameRegex = new RegExp(`^${regexPattern}$`, "i");
78551
79498
  if (filenameRegex.test(filename)) {
78552
79499
  return true;
@@ -78555,7 +79502,7 @@ function isAllowedByGlobs(filePath, allowGlobs) {
78555
79502
  return false;
78556
79503
  }
78557
79504
  function isParserSupported(filePath) {
78558
- const ext = path81.extname(filePath).toLowerCase();
79505
+ const ext = path82.extname(filePath).toLowerCase();
78559
79506
  return SUPPORTED_PARSER_EXTENSIONS.has(ext);
78560
79507
  }
78561
79508
  function isPlanFile(filePath) {
@@ -78802,28 +79749,28 @@ async function placeholderScan(input, directory) {
78802
79749
  let filesScanned = 0;
78803
79750
  const filesWithFindings = new Set;
78804
79751
  for (const filePath of changed_files) {
78805
- const fullPath = path81.isAbsolute(filePath) ? filePath : path81.resolve(directory, filePath);
78806
- const resolvedDirectory = path81.resolve(directory);
78807
- if (!fullPath.startsWith(resolvedDirectory + path81.sep) && fullPath !== resolvedDirectory) {
79752
+ const fullPath = path82.isAbsolute(filePath) ? filePath : path82.resolve(directory, filePath);
79753
+ const resolvedDirectory = path82.resolve(directory);
79754
+ if (!fullPath.startsWith(resolvedDirectory + path82.sep) && fullPath !== resolvedDirectory) {
78808
79755
  continue;
78809
79756
  }
78810
- if (!fs67.existsSync(fullPath)) {
79757
+ if (!fs68.existsSync(fullPath)) {
78811
79758
  continue;
78812
79759
  }
78813
79760
  if (isAllowedByGlobs(filePath, allow_globs)) {
78814
79761
  continue;
78815
79762
  }
78816
- const relativeFilePath = path81.relative(directory, fullPath).replace(/\\/g, "/");
79763
+ const relativeFilePath = path82.relative(directory, fullPath).replace(/\\/g, "/");
78817
79764
  if (FILE_ALLOWLIST.some((allowed) => relativeFilePath.endsWith(allowed))) {
78818
79765
  continue;
78819
79766
  }
78820
79767
  let content;
78821
79768
  try {
78822
- const stat4 = fs67.statSync(fullPath);
79769
+ const stat4 = fs68.statSync(fullPath);
78823
79770
  if (stat4.size > MAX_FILE_SIZE) {
78824
79771
  continue;
78825
79772
  }
78826
- content = fs67.readFileSync(fullPath, "utf-8");
79773
+ content = fs68.readFileSync(fullPath, "utf-8");
78827
79774
  } catch {
78828
79775
  continue;
78829
79776
  }
@@ -78885,8 +79832,8 @@ var placeholder_scan = createSwarmTool({
78885
79832
  });
78886
79833
  // src/tools/pre-check-batch.ts
78887
79834
  init_dist();
78888
- import * as fs70 from "fs";
78889
- import * as path84 from "path";
79835
+ import * as fs71 from "fs";
79836
+ import * as path85 from "path";
78890
79837
  init_manager2();
78891
79838
  init_utils();
78892
79839
  init_create_tool();
@@ -79021,8 +79968,8 @@ var quality_budget = createSwarmTool({
79021
79968
  init_dist();
79022
79969
  init_manager2();
79023
79970
  init_detector();
79024
- import * as fs69 from "fs";
79025
- import * as path83 from "path";
79971
+ import * as fs70 from "fs";
79972
+ import * as path84 from "path";
79026
79973
  import { extname as extname18 } from "path";
79027
79974
 
79028
79975
  // src/sast/rules/c.ts
@@ -79915,25 +80862,25 @@ init_create_tool();
79915
80862
  // src/tools/sast-baseline.ts
79916
80863
  init_utils2();
79917
80864
  import * as crypto8 from "crypto";
79918
- import * as fs68 from "fs";
79919
- import * as path82 from "path";
80865
+ import * as fs69 from "fs";
80866
+ import * as path83 from "path";
79920
80867
  var BASELINE_SCHEMA_VERSION = "1.0.0";
79921
80868
  var MAX_BASELINE_FINDINGS = 2000;
79922
80869
  var MAX_BASELINE_BYTES = 2 * 1048576;
79923
80870
  var LOCK_RETRY_DELAYS_MS = [50, 100, 200, 400, 800];
79924
80871
  function normalizeFindingPath(directory, file3) {
79925
- const resolved = path82.isAbsolute(file3) ? file3 : path82.resolve(directory, file3);
79926
- const rel = path82.relative(path82.resolve(directory), resolved);
80872
+ const resolved = path83.isAbsolute(file3) ? file3 : path83.resolve(directory, file3);
80873
+ const rel = path83.relative(path83.resolve(directory), resolved);
79927
80874
  return rel.replace(/\\/g, "/");
79928
80875
  }
79929
80876
  function baselineRelPath(phase) {
79930
- return path82.join("evidence", String(phase), "sast-baseline.json");
80877
+ return path83.join("evidence", String(phase), "sast-baseline.json");
79931
80878
  }
79932
80879
  function tempRelPath(phase) {
79933
- return path82.join("evidence", String(phase), `sast-baseline.json.tmp.${Date.now()}.${process.pid}`);
80880
+ return path83.join("evidence", String(phase), `sast-baseline.json.tmp.${Date.now()}.${process.pid}`);
79934
80881
  }
79935
80882
  function lockRelPath(phase) {
79936
- return path82.join("evidence", String(phase), "sast-baseline.json.lock");
80883
+ return path83.join("evidence", String(phase), "sast-baseline.json.lock");
79937
80884
  }
79938
80885
  function getLine(lines, idx) {
79939
80886
  if (idx < 0 || idx >= lines.length)
@@ -79950,7 +80897,7 @@ function fingerprintFinding(finding, directory, occurrenceIndex) {
79950
80897
  }
79951
80898
  const lineNum = finding.location.line;
79952
80899
  try {
79953
- const content = fs68.readFileSync(finding.location.file, "utf-8");
80900
+ const content = fs69.readFileSync(finding.location.file, "utf-8");
79954
80901
  const lines = content.split(`
79955
80902
  `);
79956
80903
  const idx = lineNum - 1;
@@ -79981,7 +80928,7 @@ function assignOccurrenceIndices(findings, directory) {
79981
80928
  try {
79982
80929
  if (relFile.startsWith(".."))
79983
80930
  throw new Error("escapes workspace");
79984
- const content = fs68.readFileSync(finding.location.file, "utf-8");
80931
+ const content = fs69.readFileSync(finding.location.file, "utf-8");
79985
80932
  const lines = content.split(`
79986
80933
  `);
79987
80934
  const idx = lineNum - 1;
@@ -80010,11 +80957,11 @@ function assignOccurrenceIndices(findings, directory) {
80010
80957
  async function acquireLock(lockPath) {
80011
80958
  for (let attempt = 0;attempt <= LOCK_RETRY_DELAYS_MS.length; attempt++) {
80012
80959
  try {
80013
- const fd = fs68.openSync(lockPath, "wx");
80014
- fs68.closeSync(fd);
80960
+ const fd = fs69.openSync(lockPath, "wx");
80961
+ fs69.closeSync(fd);
80015
80962
  return () => {
80016
80963
  try {
80017
- fs68.unlinkSync(lockPath);
80964
+ fs69.unlinkSync(lockPath);
80018
80965
  } catch {}
80019
80966
  };
80020
80967
  } catch {
@@ -80054,12 +81001,12 @@ async function captureOrMergeBaseline(directory, phase, findings, engine, scanne
80054
81001
  message: e instanceof Error ? e.message : "Path validation failed"
80055
81002
  };
80056
81003
  }
80057
- fs68.mkdirSync(path82.dirname(baselinePath), { recursive: true });
81004
+ fs69.mkdirSync(path83.dirname(baselinePath), { recursive: true });
80058
81005
  const releaseLock = await acquireLock(lockPath);
80059
81006
  try {
80060
81007
  let existing = null;
80061
81008
  try {
80062
- const raw = fs68.readFileSync(baselinePath, "utf-8");
81009
+ const raw = fs69.readFileSync(baselinePath, "utf-8");
80063
81010
  const parsed = JSON.parse(raw);
80064
81011
  if (parsed.schema_version === BASELINE_SCHEMA_VERSION) {
80065
81012
  existing = parsed;
@@ -80119,8 +81066,8 @@ async function captureOrMergeBaseline(directory, phase, findings, engine, scanne
80119
81066
  message: `Baseline would exceed size cap (${json4.length} bytes > ${MAX_BASELINE_BYTES})`
80120
81067
  };
80121
81068
  }
80122
- fs68.writeFileSync(tempPath, json4, "utf-8");
80123
- fs68.renameSync(tempPath, baselinePath);
81069
+ fs69.writeFileSync(tempPath, json4, "utf-8");
81070
+ fs69.renameSync(tempPath, baselinePath);
80124
81071
  return {
80125
81072
  status: "merged",
80126
81073
  path: baselinePath,
@@ -80151,8 +81098,8 @@ async function captureOrMergeBaseline(directory, phase, findings, engine, scanne
80151
81098
  message: `Baseline would exceed size cap (${json3.length} bytes > ${MAX_BASELINE_BYTES})`
80152
81099
  };
80153
81100
  }
80154
- fs68.writeFileSync(tempPath, json3, "utf-8");
80155
- fs68.renameSync(tempPath, baselinePath);
81101
+ fs69.writeFileSync(tempPath, json3, "utf-8");
81102
+ fs69.renameSync(tempPath, baselinePath);
80156
81103
  return {
80157
81104
  status: "written",
80158
81105
  path: baselinePath,
@@ -80177,7 +81124,7 @@ function loadBaseline(directory, phase) {
80177
81124
  };
80178
81125
  }
80179
81126
  try {
80180
- const raw = fs68.readFileSync(baselinePath, "utf-8");
81127
+ const raw = fs69.readFileSync(baselinePath, "utf-8");
80181
81128
  const parsed = JSON.parse(raw);
80182
81129
  if (parsed.schema_version !== BASELINE_SCHEMA_VERSION) {
80183
81130
  return {
@@ -80219,17 +81166,17 @@ var SEVERITY_ORDER = {
80219
81166
  };
80220
81167
  function shouldSkipFile(filePath) {
80221
81168
  try {
80222
- const stats = fs69.statSync(filePath);
81169
+ const stats = fs70.statSync(filePath);
80223
81170
  if (stats.size > MAX_FILE_SIZE_BYTES8) {
80224
81171
  return { skip: true, reason: "file too large" };
80225
81172
  }
80226
81173
  if (stats.size === 0) {
80227
81174
  return { skip: true, reason: "empty file" };
80228
81175
  }
80229
- const fd = fs69.openSync(filePath, "r");
81176
+ const fd = fs70.openSync(filePath, "r");
80230
81177
  const buffer = Buffer.alloc(8192);
80231
- const bytesRead = fs69.readSync(fd, buffer, 0, 8192, 0);
80232
- fs69.closeSync(fd);
81178
+ const bytesRead = fs70.readSync(fd, buffer, 0, 8192, 0);
81179
+ fs70.closeSync(fd);
80233
81180
  if (bytesRead > 0) {
80234
81181
  let nullCount = 0;
80235
81182
  for (let i2 = 0;i2 < bytesRead; i2++) {
@@ -80268,7 +81215,7 @@ function countBySeverity(findings) {
80268
81215
  }
80269
81216
  function scanFileWithTierA(filePath, language) {
80270
81217
  try {
80271
- const content = fs69.readFileSync(filePath, "utf-8");
81218
+ const content = fs70.readFileSync(filePath, "utf-8");
80272
81219
  const findings = executeRulesSync(filePath, content, language);
80273
81220
  return findings.map((f) => ({
80274
81221
  rule_id: f.rule_id,
@@ -80321,13 +81268,13 @@ async function sastScan(input, directory, config3) {
80321
81268
  _filesSkipped++;
80322
81269
  continue;
80323
81270
  }
80324
- const resolvedPath = path83.isAbsolute(filePath) ? filePath : path83.resolve(directory, filePath);
80325
- const resolvedDirectory = path83.resolve(directory);
80326
- if (!resolvedPath.startsWith(resolvedDirectory + path83.sep) && resolvedPath !== resolvedDirectory) {
81271
+ const resolvedPath = path84.isAbsolute(filePath) ? filePath : path84.resolve(directory, filePath);
81272
+ const resolvedDirectory = path84.resolve(directory);
81273
+ if (!resolvedPath.startsWith(resolvedDirectory + path84.sep) && resolvedPath !== resolvedDirectory) {
80327
81274
  _filesSkipped++;
80328
81275
  continue;
80329
81276
  }
80330
- if (!fs69.existsSync(resolvedPath)) {
81277
+ if (!fs70.existsSync(resolvedPath)) {
80331
81278
  _filesSkipped++;
80332
81279
  continue;
80333
81280
  }
@@ -80634,18 +81581,18 @@ function validatePath(inputPath, baseDir, workspaceDir) {
80634
81581
  let resolved;
80635
81582
  const isWinAbs = isWindowsAbsolutePath(inputPath);
80636
81583
  if (isWinAbs) {
80637
- resolved = path84.win32.resolve(inputPath);
80638
- } else if (path84.isAbsolute(inputPath)) {
80639
- resolved = path84.resolve(inputPath);
81584
+ resolved = path85.win32.resolve(inputPath);
81585
+ } else if (path85.isAbsolute(inputPath)) {
81586
+ resolved = path85.resolve(inputPath);
80640
81587
  } else {
80641
- resolved = path84.resolve(baseDir, inputPath);
81588
+ resolved = path85.resolve(baseDir, inputPath);
80642
81589
  }
80643
- const workspaceResolved = path84.resolve(workspaceDir);
81590
+ const workspaceResolved = path85.resolve(workspaceDir);
80644
81591
  let relative20;
80645
81592
  if (isWinAbs) {
80646
- relative20 = path84.win32.relative(workspaceResolved, resolved);
81593
+ relative20 = path85.win32.relative(workspaceResolved, resolved);
80647
81594
  } else {
80648
- relative20 = path84.relative(workspaceResolved, resolved);
81595
+ relative20 = path85.relative(workspaceResolved, resolved);
80649
81596
  }
80650
81597
  if (relative20.startsWith("..")) {
80651
81598
  return "path traversal detected";
@@ -80710,7 +81657,7 @@ async function runLintOnFiles(linter, files, workspaceDir) {
80710
81657
  if (typeof file3 !== "string") {
80711
81658
  continue;
80712
81659
  }
80713
- const resolvedPath = path84.resolve(file3);
81660
+ const resolvedPath = path85.resolve(file3);
80714
81661
  const validationError = validatePath(resolvedPath, workspaceDir, workspaceDir);
80715
81662
  if (validationError) {
80716
81663
  continue;
@@ -80867,7 +81814,7 @@ async function runSecretscanWithFiles(files, directory) {
80867
81814
  skippedFiles++;
80868
81815
  continue;
80869
81816
  }
80870
- const resolvedPath = path84.resolve(file3);
81817
+ const resolvedPath = path85.resolve(file3);
80871
81818
  const validationError = validatePath(resolvedPath, directory, directory);
80872
81819
  if (validationError) {
80873
81820
  skippedFiles++;
@@ -80885,14 +81832,14 @@ async function runSecretscanWithFiles(files, directory) {
80885
81832
  };
80886
81833
  }
80887
81834
  for (const file3 of validatedFiles) {
80888
- const ext = path84.extname(file3).toLowerCase();
81835
+ const ext = path85.extname(file3).toLowerCase();
80889
81836
  if (DEFAULT_EXCLUDE_EXTENSIONS2.has(ext)) {
80890
81837
  skippedFiles++;
80891
81838
  continue;
80892
81839
  }
80893
81840
  let stat4;
80894
81841
  try {
80895
- stat4 = fs70.statSync(file3);
81842
+ stat4 = fs71.statSync(file3);
80896
81843
  } catch {
80897
81844
  skippedFiles++;
80898
81845
  continue;
@@ -80903,7 +81850,7 @@ async function runSecretscanWithFiles(files, directory) {
80903
81850
  }
80904
81851
  let content;
80905
81852
  try {
80906
- const buffer = fs70.readFileSync(file3);
81853
+ const buffer = fs71.readFileSync(file3);
80907
81854
  if (buffer.includes(0)) {
80908
81855
  skippedFiles++;
80909
81856
  continue;
@@ -81104,7 +82051,7 @@ function classifySastFindings(findings, changedLineRanges, directory) {
81104
82051
  const preexistingFindings = [];
81105
82052
  for (const finding of findings) {
81106
82053
  const filePath = finding.location.file;
81107
- const normalised = path84.relative(directory, filePath).replace(/\\/g, "/");
82054
+ const normalised = path85.relative(directory, filePath).replace(/\\/g, "/");
81108
82055
  const changedLines = changedLineRanges.get(normalised);
81109
82056
  if (changedLines?.has(finding.location.line)) {
81110
82057
  newFindings.push(finding);
@@ -81155,7 +82102,7 @@ async function runPreCheckBatch(input, workspaceDir, contextDir) {
81155
82102
  warn(`pre_check_batch: Invalid file path: ${file3}`);
81156
82103
  continue;
81157
82104
  }
81158
- changedFiles.push(path84.resolve(directory, file3));
82105
+ changedFiles.push(path85.resolve(directory, file3));
81159
82106
  }
81160
82107
  if (changedFiles.length === 0) {
81161
82108
  warn("pre_check_batch: No valid files after validation, skipping all tools (fail-closed)");
@@ -81356,7 +82303,7 @@ var pre_check_batch = createSwarmTool({
81356
82303
  };
81357
82304
  return JSON.stringify(errorResult, null, 2);
81358
82305
  }
81359
- const resolvedDirectory = path84.resolve(typedArgs.directory);
82306
+ const resolvedDirectory = path85.resolve(typedArgs.directory);
81360
82307
  const workspaceAnchor = resolvedDirectory;
81361
82308
  const dirError = validateDirectory2(resolvedDirectory, workspaceAnchor);
81362
82309
  if (dirError) {
@@ -81397,7 +82344,7 @@ var pre_check_batch = createSwarmTool({
81397
82344
  });
81398
82345
  // src/tools/repo-map.ts
81399
82346
  init_dist();
81400
- import * as path85 from "path";
82347
+ import * as path86 from "path";
81401
82348
  init_path_security();
81402
82349
  init_create_tool();
81403
82350
  var VALID_ACTIONS = [
@@ -81422,7 +82369,7 @@ function validateFile(p) {
81422
82369
  return "file contains control characters";
81423
82370
  if (containsPathTraversal(p))
81424
82371
  return "file contains path traversal";
81425
- if (path85.isAbsolute(p) || /^[a-zA-Z]:[\\/]/.test(p)) {
82372
+ if (path86.isAbsolute(p) || /^[a-zA-Z]:[\\/]/.test(p)) {
81426
82373
  return "file must be a workspace-relative path, not absolute";
81427
82374
  }
81428
82375
  return null;
@@ -81445,8 +82392,8 @@ function ok(action, payload) {
81445
82392
  }
81446
82393
  function toRelativeGraphPath(input, workspaceRoot) {
81447
82394
  const normalized = input.replace(/\\/g, "/");
81448
- if (path85.isAbsolute(normalized)) {
81449
- const rel = path85.relative(workspaceRoot, normalized).replace(/\\/g, "/");
82395
+ if (path86.isAbsolute(normalized)) {
82396
+ const rel = path86.relative(workspaceRoot, normalized).replace(/\\/g, "/");
81450
82397
  return normalizeGraphPath2(rel);
81451
82398
  }
81452
82399
  return normalizeGraphPath2(normalized);
@@ -81590,8 +82537,8 @@ var repo_map = createSwarmTool({
81590
82537
  // src/tools/req-coverage.ts
81591
82538
  init_dist();
81592
82539
  init_create_tool();
81593
- import * as fs71 from "fs";
81594
- import * as path86 from "path";
82540
+ import * as fs72 from "fs";
82541
+ import * as path87 from "path";
81595
82542
  var SPEC_FILE = ".swarm/spec.md";
81596
82543
  var EVIDENCE_DIR4 = ".swarm/evidence";
81597
82544
  var OBLIGATION_KEYWORDS = ["MUST", "SHOULD", "SHALL"];
@@ -81650,19 +82597,19 @@ function extractObligationAndText(id, lineText) {
81650
82597
  var PHASE_TASK_ID_REGEX = /^\d+\.\d+(\.\d+)*$/;
81651
82598
  function readTouchedFiles(evidenceDir, phase, cwd) {
81652
82599
  const touchedFiles = new Set;
81653
- if (!fs71.existsSync(evidenceDir) || !fs71.statSync(evidenceDir).isDirectory()) {
82600
+ if (!fs72.existsSync(evidenceDir) || !fs72.statSync(evidenceDir).isDirectory()) {
81654
82601
  return [];
81655
82602
  }
81656
82603
  let entries;
81657
82604
  try {
81658
- entries = fs71.readdirSync(evidenceDir);
82605
+ entries = fs72.readdirSync(evidenceDir);
81659
82606
  } catch {
81660
82607
  return [];
81661
82608
  }
81662
82609
  for (const entry of entries) {
81663
- const entryPath = path86.join(evidenceDir, entry);
82610
+ const entryPath = path87.join(evidenceDir, entry);
81664
82611
  try {
81665
- const stat4 = fs71.statSync(entryPath);
82612
+ const stat4 = fs72.statSync(entryPath);
81666
82613
  if (!stat4.isDirectory()) {
81667
82614
  continue;
81668
82615
  }
@@ -81676,14 +82623,14 @@ function readTouchedFiles(evidenceDir, phase, cwd) {
81676
82623
  if (entryPhase !== String(phase)) {
81677
82624
  continue;
81678
82625
  }
81679
- const evidenceFilePath = path86.join(entryPath, "evidence.json");
82626
+ const evidenceFilePath = path87.join(entryPath, "evidence.json");
81680
82627
  try {
81681
- const resolvedPath = path86.resolve(evidenceFilePath);
81682
- const evidenceDirResolved = path86.resolve(evidenceDir);
81683
- if (!resolvedPath.startsWith(evidenceDirResolved + path86.sep)) {
82628
+ const resolvedPath = path87.resolve(evidenceFilePath);
82629
+ const evidenceDirResolved = path87.resolve(evidenceDir);
82630
+ if (!resolvedPath.startsWith(evidenceDirResolved + path87.sep)) {
81684
82631
  continue;
81685
82632
  }
81686
- const stat4 = fs71.lstatSync(evidenceFilePath);
82633
+ const stat4 = fs72.lstatSync(evidenceFilePath);
81687
82634
  if (!stat4.isFile()) {
81688
82635
  continue;
81689
82636
  }
@@ -81695,7 +82642,7 @@ function readTouchedFiles(evidenceDir, phase, cwd) {
81695
82642
  }
81696
82643
  let content;
81697
82644
  try {
81698
- content = fs71.readFileSync(evidenceFilePath, "utf-8");
82645
+ content = fs72.readFileSync(evidenceFilePath, "utf-8");
81699
82646
  } catch {
81700
82647
  continue;
81701
82648
  }
@@ -81714,7 +82661,7 @@ function readTouchedFiles(evidenceDir, phase, cwd) {
81714
82661
  if (Array.isArray(diffEntry.files_changed)) {
81715
82662
  for (const file3 of diffEntry.files_changed) {
81716
82663
  if (typeof file3 === "string") {
81717
- touchedFiles.add(path86.resolve(cwd, file3));
82664
+ touchedFiles.add(path87.resolve(cwd, file3));
81718
82665
  }
81719
82666
  }
81720
82667
  }
@@ -81727,12 +82674,12 @@ function readTouchedFiles(evidenceDir, phase, cwd) {
81727
82674
  }
81728
82675
  function searchFileForKeywords(filePath, keywords, cwd) {
81729
82676
  try {
81730
- const resolvedPath = path86.resolve(filePath);
81731
- const cwdResolved = path86.resolve(cwd);
82677
+ const resolvedPath = path87.resolve(filePath);
82678
+ const cwdResolved = path87.resolve(cwd);
81732
82679
  if (!resolvedPath.startsWith(cwdResolved)) {
81733
82680
  return false;
81734
82681
  }
81735
- const content = fs71.readFileSync(resolvedPath, "utf-8");
82682
+ const content = fs72.readFileSync(resolvedPath, "utf-8");
81736
82683
  for (const keyword of keywords) {
81737
82684
  const regex = new RegExp(`\\b${keyword}\\b`, "i");
81738
82685
  if (regex.test(content)) {
@@ -81862,10 +82809,10 @@ var req_coverage = createSwarmTool({
81862
82809
  }, null, 2);
81863
82810
  }
81864
82811
  const cwd = inputDirectory || directory;
81865
- const specPath = path86.join(cwd, SPEC_FILE);
82812
+ const specPath = path87.join(cwd, SPEC_FILE);
81866
82813
  let specContent;
81867
82814
  try {
81868
- specContent = fs71.readFileSync(specPath, "utf-8");
82815
+ specContent = fs72.readFileSync(specPath, "utf-8");
81869
82816
  } catch (readError) {
81870
82817
  return JSON.stringify({
81871
82818
  success: false,
@@ -81889,7 +82836,7 @@ var req_coverage = createSwarmTool({
81889
82836
  message: "No FR requirements found in spec.md"
81890
82837
  }, null, 2);
81891
82838
  }
81892
- const evidenceDir = path86.join(cwd, EVIDENCE_DIR4);
82839
+ const evidenceDir = path87.join(cwd, EVIDENCE_DIR4);
81893
82840
  const touchedFiles = readTouchedFiles(evidenceDir, phase, cwd);
81894
82841
  const analyzedRequirements = [];
81895
82842
  let coveredCount = 0;
@@ -81915,12 +82862,12 @@ var req_coverage = createSwarmTool({
81915
82862
  requirements: analyzedRequirements
81916
82863
  };
81917
82864
  const reportFilename = `req-coverage-phase-${phase}.json`;
81918
- const reportPath = path86.join(evidenceDir, reportFilename);
82865
+ const reportPath = path87.join(evidenceDir, reportFilename);
81919
82866
  try {
81920
- if (!fs71.existsSync(evidenceDir)) {
81921
- fs71.mkdirSync(evidenceDir, { recursive: true });
82867
+ if (!fs72.existsSync(evidenceDir)) {
82868
+ fs72.mkdirSync(evidenceDir, { recursive: true });
81922
82869
  }
81923
- fs71.writeFileSync(reportPath, JSON.stringify(result, null, 2), "utf-8");
82870
+ fs72.writeFileSync(reportPath, JSON.stringify(result, null, 2), "utf-8");
81924
82871
  } catch (writeError) {
81925
82872
  console.warn(`Failed to write coverage report: ${writeError instanceof Error ? writeError.message : String(writeError)}`);
81926
82873
  }
@@ -81999,6 +82946,7 @@ ${paginatedContent}`;
81999
82946
  // src/tools/save-plan.ts
82000
82947
  init_tool();
82001
82948
  init_plan_schema();
82949
+ init_qa_gate_profile();
82002
82950
  init_file_locks();
82003
82951
  init_checkpoint3();
82004
82952
  init_ledger();
@@ -82006,8 +82954,8 @@ init_manager();
82006
82954
  init_state();
82007
82955
  init_create_tool();
82008
82956
  import * as crypto9 from "crypto";
82009
- import * as fs72 from "fs";
82010
- import * as path87 from "path";
82957
+ import * as fs73 from "fs";
82958
+ import * as path88 from "path";
82011
82959
  function detectPlaceholderContent(args2) {
82012
82960
  const issues = [];
82013
82961
  const placeholderPattern = /^\[\w[\w\s]*\]$/;
@@ -82081,17 +83029,17 @@ async function executeSavePlan(args2, fallbackDir) {
82081
83029
  };
82082
83030
  }
82083
83031
  if (args2.working_directory && fallbackDir) {
82084
- const resolvedTarget = path87.resolve(args2.working_directory);
82085
- const resolvedRoot = path87.resolve(fallbackDir);
83032
+ const resolvedTarget = path88.resolve(args2.working_directory);
83033
+ const resolvedRoot = path88.resolve(fallbackDir);
82086
83034
  let fallbackExists = false;
82087
83035
  try {
82088
- fs72.accessSync(resolvedRoot, fs72.constants.F_OK);
83036
+ fs73.accessSync(resolvedRoot, fs73.constants.F_OK);
82089
83037
  fallbackExists = true;
82090
83038
  } catch {
82091
83039
  fallbackExists = false;
82092
83040
  }
82093
83041
  if (fallbackExists) {
82094
- const isSubdirectory = resolvedTarget.startsWith(resolvedRoot + path87.sep);
83042
+ const isSubdirectory = resolvedTarget.startsWith(resolvedRoot + path88.sep);
82095
83043
  if (isSubdirectory) {
82096
83044
  return {
82097
83045
  success: false,
@@ -82107,11 +83055,11 @@ async function executeSavePlan(args2, fallbackDir) {
82107
83055
  let specMtime;
82108
83056
  let specHash;
82109
83057
  if (process.env.SWARM_SKIP_SPEC_GATE !== "1") {
82110
- const specPath = path87.join(targetWorkspace, ".swarm", "spec.md");
83058
+ const specPath = path88.join(targetWorkspace, ".swarm", "spec.md");
82111
83059
  try {
82112
- const stat4 = await fs72.promises.stat(specPath);
83060
+ const stat4 = await fs73.promises.stat(specPath);
82113
83061
  specMtime = stat4.mtime.toISOString();
82114
- const content = await fs72.promises.readFile(specPath, "utf8");
83062
+ const content = await fs73.promises.readFile(specPath, "utf8");
82115
83063
  specHash = crypto9.createHash("sha256").update(content).digest("hex");
82116
83064
  } catch {
82117
83065
  return {
@@ -82122,6 +83070,32 @@ async function executeSavePlan(args2, fallbackDir) {
82122
83070
  };
82123
83071
  }
82124
83072
  }
83073
+ if (process.env.SWARM_SKIP_GATE_SELECTION !== "1") {
83074
+ const contextPath = path88.join(targetWorkspace, ".swarm", "context.md");
83075
+ let contextContent = "";
83076
+ try {
83077
+ contextContent = await fs73.promises.readFile(contextPath, "utf8");
83078
+ } catch {}
83079
+ const hasPendingSection = contextContent.includes("## Pending QA Gate Selection");
83080
+ if (!hasPendingSection) {
83081
+ const candidatePlanId = `${args2.swarm_id}-${args2.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
83082
+ let existingProfile = null;
83083
+ try {
83084
+ existingProfile = getProfile(targetWorkspace, candidatePlanId);
83085
+ } catch {}
83086
+ if (!existingProfile) {
83087
+ return {
83088
+ success: false,
83089
+ message: "QA_GATE_SELECTION_REQUIRED: QA gate selection has not been completed. " + "Present the gate selection question to the user (step 5b of MODE: SPECIFY " + "or Phase 6 of MODE: BRAINSTORM), write their response to .swarm/context.md " + 'under "## Pending QA Gate Selection", then retry save_plan.',
83090
+ errors: [
83091
+ "Missing ## Pending QA Gate Selection in .swarm/context.md",
83092
+ "No existing QaGateProfile found for this plan"
83093
+ ],
83094
+ recovery_guidance: "Do not call set_qa_gates with assumed values. Present the gate dialogue, " + "wait for the user's answer, write it to context.md, then retry save_plan."
83095
+ };
83096
+ }
83097
+ }
83098
+ }
82125
83099
  const dir = targetWorkspace;
82126
83100
  const existingStatusMap = new Map;
82127
83101
  let preservedExecutionProfile;
@@ -82247,14 +83221,14 @@ async function executeSavePlan(args2, fallbackDir) {
82247
83221
  }
82248
83222
  await writeCheckpoint(dir).catch(() => {});
82249
83223
  try {
82250
- const markerPath = path87.join(dir, ".swarm", ".plan-write-marker");
83224
+ const markerPath = path88.join(dir, ".swarm", ".plan-write-marker");
82251
83225
  const marker = JSON.stringify({
82252
83226
  source: "save_plan",
82253
83227
  timestamp: new Date().toISOString(),
82254
83228
  phases_count: plan.phases.length,
82255
83229
  tasks_count: tasksCount
82256
83230
  });
82257
- await fs72.promises.writeFile(markerPath, marker, "utf8");
83231
+ await fs73.promises.writeFile(markerPath, marker, "utf8");
82258
83232
  } catch {}
82259
83233
  const warnings = [];
82260
83234
  let criticReviewFound = false;
@@ -82270,7 +83244,7 @@ async function executeSavePlan(args2, fallbackDir) {
82270
83244
  return {
82271
83245
  success: true,
82272
83246
  message: "Plan saved successfully",
82273
- plan_path: path87.join(dir, ".swarm", "plan.json"),
83247
+ plan_path: path88.join(dir, ".swarm", "plan.json"),
82274
83248
  phases_count: plan.phases.length,
82275
83249
  tasks_count: tasksCount,
82276
83250
  ...resolvedProfile !== undefined ? { execution_profile: resolvedProfile } : {},
@@ -82322,8 +83296,8 @@ var save_plan = createSwarmTool({
82322
83296
  // src/tools/sbom-generate.ts
82323
83297
  init_dist();
82324
83298
  init_manager2();
82325
- import * as fs73 from "fs";
82326
- import * as path88 from "path";
83299
+ import * as fs74 from "fs";
83300
+ import * as path89 from "path";
82327
83301
 
82328
83302
  // src/sbom/detectors/index.ts
82329
83303
  init_utils();
@@ -83171,9 +84145,9 @@ function findManifestFiles(rootDir) {
83171
84145
  const patterns = [...new Set(allDetectors.flatMap((d) => d.patterns))];
83172
84146
  function searchDir(dir) {
83173
84147
  try {
83174
- const entries = fs73.readdirSync(dir, { withFileTypes: true });
84148
+ const entries = fs74.readdirSync(dir, { withFileTypes: true });
83175
84149
  for (const entry of entries) {
83176
- const fullPath = path88.join(dir, entry.name);
84150
+ const fullPath = path89.join(dir, entry.name);
83177
84151
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === "target") {
83178
84152
  continue;
83179
84153
  }
@@ -83182,7 +84156,7 @@ function findManifestFiles(rootDir) {
83182
84156
  } else if (entry.isFile()) {
83183
84157
  for (const pattern of patterns) {
83184
84158
  if (simpleGlobToRegex(pattern).test(entry.name)) {
83185
- manifestFiles.push(path88.relative(rootDir, fullPath));
84159
+ manifestFiles.push(path89.relative(rootDir, fullPath));
83186
84160
  break;
83187
84161
  }
83188
84162
  }
@@ -83198,13 +84172,13 @@ function findManifestFilesInDirs(directories, workingDir) {
83198
84172
  const patterns = [...new Set(allDetectors.flatMap((d) => d.patterns))];
83199
84173
  for (const dir of directories) {
83200
84174
  try {
83201
- const entries = fs73.readdirSync(dir, { withFileTypes: true });
84175
+ const entries = fs74.readdirSync(dir, { withFileTypes: true });
83202
84176
  for (const entry of entries) {
83203
- const fullPath = path88.join(dir, entry.name);
84177
+ const fullPath = path89.join(dir, entry.name);
83204
84178
  if (entry.isFile()) {
83205
84179
  for (const pattern of patterns) {
83206
84180
  if (simpleGlobToRegex(pattern).test(entry.name)) {
83207
- found.push(path88.relative(workingDir, fullPath));
84181
+ found.push(path89.relative(workingDir, fullPath));
83208
84182
  break;
83209
84183
  }
83210
84184
  }
@@ -83217,11 +84191,11 @@ function findManifestFilesInDirs(directories, workingDir) {
83217
84191
  function getDirectoriesFromChangedFiles(changedFiles, workingDir) {
83218
84192
  const dirs = new Set;
83219
84193
  for (const file3 of changedFiles) {
83220
- let currentDir = path88.dirname(file3);
84194
+ let currentDir = path89.dirname(file3);
83221
84195
  while (true) {
83222
- if (currentDir && currentDir !== "." && currentDir !== path88.sep) {
83223
- dirs.add(path88.join(workingDir, currentDir));
83224
- const parent = path88.dirname(currentDir);
84196
+ if (currentDir && currentDir !== "." && currentDir !== path89.sep) {
84197
+ dirs.add(path89.join(workingDir, currentDir));
84198
+ const parent = path89.dirname(currentDir);
83225
84199
  if (parent === currentDir)
83226
84200
  break;
83227
84201
  currentDir = parent;
@@ -83235,7 +84209,7 @@ function getDirectoriesFromChangedFiles(changedFiles, workingDir) {
83235
84209
  }
83236
84210
  function ensureOutputDir(outputDir) {
83237
84211
  try {
83238
- fs73.mkdirSync(outputDir, { recursive: true });
84212
+ fs74.mkdirSync(outputDir, { recursive: true });
83239
84213
  } catch (error93) {
83240
84214
  if (!error93 || error93.code !== "EEXIST") {
83241
84215
  throw error93;
@@ -83305,7 +84279,7 @@ var sbom_generate = createSwarmTool({
83305
84279
  const changedFiles = obj.changed_files;
83306
84280
  const relativeOutputDir = obj.output_dir || DEFAULT_OUTPUT_DIR;
83307
84281
  const workingDir = directory;
83308
- const outputDir = path88.isAbsolute(relativeOutputDir) ? relativeOutputDir : path88.join(workingDir, relativeOutputDir);
84282
+ const outputDir = path89.isAbsolute(relativeOutputDir) ? relativeOutputDir : path89.join(workingDir, relativeOutputDir);
83309
84283
  let manifestFiles = [];
83310
84284
  if (scope === "all") {
83311
84285
  manifestFiles = findManifestFiles(workingDir);
@@ -83328,11 +84302,11 @@ var sbom_generate = createSwarmTool({
83328
84302
  const processedFiles = [];
83329
84303
  for (const manifestFile of manifestFiles) {
83330
84304
  try {
83331
- const fullPath = path88.isAbsolute(manifestFile) ? manifestFile : path88.join(workingDir, manifestFile);
83332
- if (!fs73.existsSync(fullPath)) {
84305
+ const fullPath = path89.isAbsolute(manifestFile) ? manifestFile : path89.join(workingDir, manifestFile);
84306
+ if (!fs74.existsSync(fullPath)) {
83333
84307
  continue;
83334
84308
  }
83335
- const content = fs73.readFileSync(fullPath, "utf-8");
84309
+ const content = fs74.readFileSync(fullPath, "utf-8");
83336
84310
  const components = detectComponents(manifestFile, content);
83337
84311
  processedFiles.push(manifestFile);
83338
84312
  if (components.length > 0) {
@@ -83345,8 +84319,8 @@ var sbom_generate = createSwarmTool({
83345
84319
  const bom = generateCycloneDX(allComponents);
83346
84320
  const bomJson = serializeCycloneDX(bom);
83347
84321
  const filename = generateSbomFilename();
83348
- const outputPath = path88.join(outputDir, filename);
83349
- fs73.writeFileSync(outputPath, bomJson, "utf-8");
84322
+ const outputPath = path89.join(outputDir, filename);
84323
+ fs74.writeFileSync(outputPath, bomJson, "utf-8");
83350
84324
  const verdict = processedFiles.length > 0 ? "pass" : "pass";
83351
84325
  try {
83352
84326
  const timestamp = new Date().toISOString();
@@ -83388,8 +84362,8 @@ var sbom_generate = createSwarmTool({
83388
84362
  // src/tools/schema-drift.ts
83389
84363
  init_dist();
83390
84364
  init_create_tool();
83391
- import * as fs74 from "fs";
83392
- import * as path89 from "path";
84365
+ import * as fs75 from "fs";
84366
+ import * as path90 from "path";
83393
84367
  var SPEC_CANDIDATES = [
83394
84368
  "openapi.json",
83395
84369
  "openapi.yaml",
@@ -83421,28 +84395,28 @@ function normalizePath3(p) {
83421
84395
  }
83422
84396
  function discoverSpecFile(cwd, specFileArg) {
83423
84397
  if (specFileArg) {
83424
- const resolvedPath = path89.resolve(cwd, specFileArg);
83425
- const normalizedCwd = cwd.endsWith(path89.sep) ? cwd : cwd + path89.sep;
84398
+ const resolvedPath = path90.resolve(cwd, specFileArg);
84399
+ const normalizedCwd = cwd.endsWith(path90.sep) ? cwd : cwd + path90.sep;
83426
84400
  if (!resolvedPath.startsWith(normalizedCwd) && resolvedPath !== cwd) {
83427
84401
  throw new Error("Invalid spec_file: path traversal detected");
83428
84402
  }
83429
- const ext = path89.extname(resolvedPath).toLowerCase();
84403
+ const ext = path90.extname(resolvedPath).toLowerCase();
83430
84404
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
83431
84405
  throw new Error(`Invalid spec_file: must end in .json, .yaml, or .yml, got ${ext}`);
83432
84406
  }
83433
- const stats = fs74.statSync(resolvedPath);
84407
+ const stats = fs75.statSync(resolvedPath);
83434
84408
  if (stats.size > MAX_SPEC_SIZE) {
83435
84409
  throw new Error(`Invalid spec_file: file exceeds ${MAX_SPEC_SIZE / 1024 / 1024}MB limit`);
83436
84410
  }
83437
- if (!fs74.existsSync(resolvedPath)) {
84411
+ if (!fs75.existsSync(resolvedPath)) {
83438
84412
  throw new Error(`Spec file not found: ${resolvedPath}`);
83439
84413
  }
83440
84414
  return resolvedPath;
83441
84415
  }
83442
84416
  for (const candidate of SPEC_CANDIDATES) {
83443
- const candidatePath = path89.resolve(cwd, candidate);
83444
- if (fs74.existsSync(candidatePath)) {
83445
- const stats = fs74.statSync(candidatePath);
84417
+ const candidatePath = path90.resolve(cwd, candidate);
84418
+ if (fs75.existsSync(candidatePath)) {
84419
+ const stats = fs75.statSync(candidatePath);
83446
84420
  if (stats.size <= MAX_SPEC_SIZE) {
83447
84421
  return candidatePath;
83448
84422
  }
@@ -83451,8 +84425,8 @@ function discoverSpecFile(cwd, specFileArg) {
83451
84425
  return null;
83452
84426
  }
83453
84427
  function parseSpec(specFile) {
83454
- const content = fs74.readFileSync(specFile, "utf-8");
83455
- const ext = path89.extname(specFile).toLowerCase();
84428
+ const content = fs75.readFileSync(specFile, "utf-8");
84429
+ const ext = path90.extname(specFile).toLowerCase();
83456
84430
  if (ext === ".json") {
83457
84431
  return parseJsonSpec(content);
83458
84432
  }
@@ -83523,12 +84497,12 @@ function extractRoutes(cwd) {
83523
84497
  function walkDir(dir) {
83524
84498
  let entries;
83525
84499
  try {
83526
- entries = fs74.readdirSync(dir, { withFileTypes: true });
84500
+ entries = fs75.readdirSync(dir, { withFileTypes: true });
83527
84501
  } catch {
83528
84502
  return;
83529
84503
  }
83530
84504
  for (const entry of entries) {
83531
- const fullPath = path89.join(dir, entry.name);
84505
+ const fullPath = path90.join(dir, entry.name);
83532
84506
  if (entry.isSymbolicLink()) {
83533
84507
  continue;
83534
84508
  }
@@ -83538,7 +84512,7 @@ function extractRoutes(cwd) {
83538
84512
  }
83539
84513
  walkDir(fullPath);
83540
84514
  } else if (entry.isFile()) {
83541
- const ext = path89.extname(entry.name).toLowerCase();
84515
+ const ext = path90.extname(entry.name).toLowerCase();
83542
84516
  const baseName = entry.name.toLowerCase();
83543
84517
  if (![".ts", ".js", ".mjs"].includes(ext)) {
83544
84518
  continue;
@@ -83556,7 +84530,7 @@ function extractRoutes(cwd) {
83556
84530
  }
83557
84531
  function extractRoutesFromFile(filePath) {
83558
84532
  const routes = [];
83559
- const content = fs74.readFileSync(filePath, "utf-8");
84533
+ const content = fs75.readFileSync(filePath, "utf-8");
83560
84534
  const lines = content.split(/\r?\n/);
83561
84535
  const expressRegex = /(?:app|router|server|express)\.(get|post|put|patch|delete|options|head)\s*\(\s*['"`]([^'"`]+)['"`]/g;
83562
84536
  const flaskRegex = /@(?:app|blueprint|bp)\.route\s*\(\s*['"]([^'"]+)['"]/g;
@@ -83704,8 +84678,8 @@ var schema_drift = createSwarmTool({
83704
84678
  init_tool();
83705
84679
  init_path_security();
83706
84680
  init_create_tool();
83707
- import * as fs75 from "fs";
83708
- import * as path90 from "path";
84681
+ import * as fs76 from "fs";
84682
+ import * as path91 from "path";
83709
84683
  var DEFAULT_MAX_RESULTS = 100;
83710
84684
  var DEFAULT_MAX_LINES = 200;
83711
84685
  var REGEX_TIMEOUT_MS = 5000;
@@ -83741,11 +84715,11 @@ function containsWindowsAttacks3(str) {
83741
84715
  }
83742
84716
  function isPathInWorkspace3(filePath, workspace) {
83743
84717
  try {
83744
- const resolvedPath = path90.resolve(workspace, filePath);
83745
- const realWorkspace = fs75.realpathSync(workspace);
83746
- const realResolvedPath = fs75.realpathSync(resolvedPath);
83747
- const relativePath = path90.relative(realWorkspace, realResolvedPath);
83748
- if (relativePath.startsWith("..") || path90.isAbsolute(relativePath)) {
84718
+ const resolvedPath = path91.resolve(workspace, filePath);
84719
+ const realWorkspace = fs76.realpathSync(workspace);
84720
+ const realResolvedPath = fs76.realpathSync(resolvedPath);
84721
+ const relativePath = path91.relative(realWorkspace, realResolvedPath);
84722
+ if (relativePath.startsWith("..") || path91.isAbsolute(relativePath)) {
83749
84723
  return false;
83750
84724
  }
83751
84725
  return true;
@@ -83758,12 +84732,12 @@ function validatePathForRead2(filePath, workspace) {
83758
84732
  }
83759
84733
  function findRgInEnvPath() {
83760
84734
  const searchPath = process.env.PATH ?? "";
83761
- for (const dir of searchPath.split(path90.delimiter)) {
84735
+ for (const dir of searchPath.split(path91.delimiter)) {
83762
84736
  if (!dir)
83763
84737
  continue;
83764
84738
  const isWindows = process.platform === "win32";
83765
- const candidate = path90.join(dir, isWindows ? "rg.exe" : "rg");
83766
- if (fs75.existsSync(candidate))
84739
+ const candidate = path91.join(dir, isWindows ? "rg.exe" : "rg");
84740
+ if (fs76.existsSync(candidate))
83767
84741
  return candidate;
83768
84742
  }
83769
84743
  return null;
@@ -83890,10 +84864,10 @@ function collectFiles(dir, workspace, includeGlobs, excludeGlobs) {
83890
84864
  return files;
83891
84865
  }
83892
84866
  try {
83893
- const entries = fs75.readdirSync(dir, { withFileTypes: true });
84867
+ const entries = fs76.readdirSync(dir, { withFileTypes: true });
83894
84868
  for (const entry of entries) {
83895
- const fullPath = path90.join(dir, entry.name);
83896
- const relativePath = path90.relative(workspace, fullPath);
84869
+ const fullPath = path91.join(dir, entry.name);
84870
+ const relativePath = path91.relative(workspace, fullPath);
83897
84871
  if (!validatePathForRead2(fullPath, workspace)) {
83898
84872
  continue;
83899
84873
  }
@@ -83934,13 +84908,13 @@ async function fallbackSearch(opts) {
83934
84908
  const matches = [];
83935
84909
  let total = 0;
83936
84910
  for (const file3 of files) {
83937
- const fullPath = path90.join(opts.workspace, file3);
84911
+ const fullPath = path91.join(opts.workspace, file3);
83938
84912
  if (!validatePathForRead2(fullPath, opts.workspace)) {
83939
84913
  continue;
83940
84914
  }
83941
84915
  let stats;
83942
84916
  try {
83943
- stats = fs75.statSync(fullPath);
84917
+ stats = fs76.statSync(fullPath);
83944
84918
  if (stats.size > MAX_FILE_SIZE_BYTES10) {
83945
84919
  continue;
83946
84920
  }
@@ -83949,7 +84923,7 @@ async function fallbackSearch(opts) {
83949
84923
  }
83950
84924
  let content;
83951
84925
  try {
83952
- content = fs75.readFileSync(fullPath, "utf-8");
84926
+ content = fs76.readFileSync(fullPath, "utf-8");
83953
84927
  } catch {
83954
84928
  continue;
83955
84929
  }
@@ -84061,7 +85035,7 @@ var search = createSwarmTool({
84061
85035
  message: "Exclude pattern contains invalid Windows-specific sequence"
84062
85036
  }, null, 2);
84063
85037
  }
84064
- if (!fs75.existsSync(directory)) {
85038
+ if (!fs76.existsSync(directory)) {
84065
85039
  return JSON.stringify({
84066
85040
  error: true,
84067
85041
  type: "unknown",
@@ -84129,7 +85103,8 @@ async function executeSetQaGates(args2, directory) {
84129
85103
  "critic_pre_plan",
84130
85104
  "hallucination_guard",
84131
85105
  "sast_enabled",
84132
- "mutation_test"
85106
+ "mutation_test",
85107
+ "council_general_review"
84133
85108
  ]) {
84134
85109
  if (args2[key] !== undefined)
84135
85110
  partial3[key] = args2[key];
@@ -84175,6 +85150,7 @@ var set_qa_gates = createSwarmTool({
84175
85150
  hallucination_guard: tool.schema.boolean().optional().describe("Enable hallucination_guard checks on plan and implementation claims."),
84176
85151
  sast_enabled: tool.schema.boolean().optional().describe("Enable SAST scanning as a required QA gate."),
84177
85152
  mutation_test: tool.schema.boolean().optional().describe("Enable the mutation-testing gate (default: off). Requires mutation " + "tests to achieve a passing kill rate before phase completion; " + "WARN verdict allows advancement, FAIL blocks."),
85153
+ council_general_review: tool.schema.boolean().optional().describe("Enable the council_general_review gate (default: off). When on, " + "MODE: SPECIFY runs convene_general_council on the draft spec " + "before the critic-gate, folding multi-model deliberation into " + "the spec. Requires council.general.enabled and a search API key."),
84178
85154
  project_type: tool.schema.string().optional().describe('Project type label (e.g. "ts", "python"). Only applied when the profile is being created for the first time.')
84179
85155
  },
84180
85156
  execute: async (args2, directory) => {
@@ -84186,8 +85162,8 @@ var set_qa_gates = createSwarmTool({
84186
85162
  init_tool();
84187
85163
  init_path_security();
84188
85164
  init_create_tool();
84189
- import * as fs76 from "fs";
84190
- import * as path91 from "path";
85165
+ import * as fs77 from "fs";
85166
+ import * as path92 from "path";
84191
85167
  var WINDOWS_RESERVED_NAMES4 = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|:|$)/i;
84192
85168
  function containsWindowsAttacks4(str) {
84193
85169
  if (/:[^\\/]/.test(str))
@@ -84201,14 +85177,14 @@ function containsWindowsAttacks4(str) {
84201
85177
  }
84202
85178
  function isPathInWorkspace4(filePath, workspace) {
84203
85179
  try {
84204
- const resolvedPath = path91.resolve(workspace, filePath);
84205
- if (!fs76.existsSync(resolvedPath)) {
85180
+ const resolvedPath = path92.resolve(workspace, filePath);
85181
+ if (!fs77.existsSync(resolvedPath)) {
84206
85182
  return true;
84207
85183
  }
84208
- const realWorkspace = fs76.realpathSync(workspace);
84209
- const realResolvedPath = fs76.realpathSync(resolvedPath);
84210
- const relativePath = path91.relative(realWorkspace, realResolvedPath);
84211
- if (relativePath.startsWith("..") || path91.isAbsolute(relativePath)) {
85184
+ const realWorkspace = fs77.realpathSync(workspace);
85185
+ const realResolvedPath = fs77.realpathSync(resolvedPath);
85186
+ const relativePath = path92.relative(realWorkspace, realResolvedPath);
85187
+ if (relativePath.startsWith("..") || path92.isAbsolute(relativePath)) {
84212
85188
  return false;
84213
85189
  }
84214
85190
  return true;
@@ -84380,7 +85356,7 @@ var suggestPatch = createSwarmTool({
84380
85356
  message: "changes cannot be empty"
84381
85357
  }, null, 2);
84382
85358
  }
84383
- if (!fs76.existsSync(directory)) {
85359
+ if (!fs77.existsSync(directory)) {
84384
85360
  return JSON.stringify({
84385
85361
  success: false,
84386
85362
  error: true,
@@ -84416,8 +85392,8 @@ var suggestPatch = createSwarmTool({
84416
85392
  });
84417
85393
  continue;
84418
85394
  }
84419
- const fullPath = path91.resolve(directory, change.file);
84420
- if (!fs76.existsSync(fullPath)) {
85395
+ const fullPath = path92.resolve(directory, change.file);
85396
+ if (!fs77.existsSync(fullPath)) {
84421
85397
  errors5.push({
84422
85398
  success: false,
84423
85399
  error: true,
@@ -84431,7 +85407,7 @@ var suggestPatch = createSwarmTool({
84431
85407
  }
84432
85408
  let content;
84433
85409
  try {
84434
- content = fs76.readFileSync(fullPath, "utf-8");
85410
+ content = fs77.readFileSync(fullPath, "utf-8");
84435
85411
  } catch (err3) {
84436
85412
  errors5.push({
84437
85413
  success: false,
@@ -84665,8 +85641,8 @@ var generate_mutants = createSwarmTool({
84665
85641
  // src/tools/lint-spec.ts
84666
85642
  init_spec_schema();
84667
85643
  init_create_tool();
84668
- import * as fs77 from "fs";
84669
- import * as path92 from "path";
85644
+ import * as fs78 from "fs";
85645
+ import * as path93 from "path";
84670
85646
  var SPEC_FILE_NAME = "spec.md";
84671
85647
  var SWARM_DIR2 = ".swarm";
84672
85648
  var OBLIGATION_KEYWORDS2 = ["MUST", "SHALL", "SHOULD", "MAY"];
@@ -84719,8 +85695,8 @@ var lint_spec = createSwarmTool({
84719
85695
  async execute(_args, directory) {
84720
85696
  const errors5 = [];
84721
85697
  const warnings = [];
84722
- const specPath = path92.join(directory, SWARM_DIR2, SPEC_FILE_NAME);
84723
- if (!fs77.existsSync(specPath)) {
85698
+ const specPath = path93.join(directory, SWARM_DIR2, SPEC_FILE_NAME);
85699
+ if (!fs78.existsSync(specPath)) {
84724
85700
  const result2 = {
84725
85701
  valid: false,
84726
85702
  specMtime: null,
@@ -84739,12 +85715,12 @@ var lint_spec = createSwarmTool({
84739
85715
  }
84740
85716
  let specMtime = null;
84741
85717
  try {
84742
- const stats = fs77.statSync(specPath);
85718
+ const stats = fs78.statSync(specPath);
84743
85719
  specMtime = stats.mtime.toISOString();
84744
85720
  } catch {}
84745
85721
  let content;
84746
85722
  try {
84747
- content = fs77.readFileSync(specPath, "utf-8");
85723
+ content = fs78.readFileSync(specPath, "utf-8");
84748
85724
  } catch (e) {
84749
85725
  const result2 = {
84750
85726
  valid: false,
@@ -84789,13 +85765,13 @@ var lint_spec = createSwarmTool({
84789
85765
  });
84790
85766
  // src/tools/mutation-test.ts
84791
85767
  init_dist();
84792
- import * as fs78 from "fs";
84793
- import * as path94 from "path";
85768
+ import * as fs79 from "fs";
85769
+ import * as path95 from "path";
84794
85770
 
84795
85771
  // src/mutation/engine.ts
84796
85772
  import { spawnSync as spawnSync3 } from "child_process";
84797
85773
  import { unlinkSync as unlinkSync12, writeFileSync as writeFileSync18 } from "fs";
84798
- import * as path93 from "path";
85774
+ import * as path94 from "path";
84799
85775
 
84800
85776
  // src/mutation/equivalence.ts
84801
85777
  function isStaticallyEquivalent(originalCode, mutatedCode) {
@@ -84930,7 +85906,7 @@ async function executeMutation(patch, testCommand, _testFiles, workingDir) {
84930
85906
  let patchFile;
84931
85907
  try {
84932
85908
  const safeId2 = patch.id.replace(/[^a-zA-Z0-9_-]/g, "_");
84933
- patchFile = path93.join(workingDir, `.mutation_patch_${safeId2}.diff`);
85909
+ patchFile = path94.join(workingDir, `.mutation_patch_${safeId2}.diff`);
84934
85910
  try {
84935
85911
  writeFileSync18(patchFile, patch.patch);
84936
85912
  } catch (writeErr) {
@@ -85324,8 +86300,8 @@ var mutation_test = createSwarmTool({
85324
86300
  ];
85325
86301
  for (const filePath of uniquePaths) {
85326
86302
  try {
85327
- const resolvedPath = path94.resolve(cwd, filePath);
85328
- sourceFiles.set(filePath, fs78.readFileSync(resolvedPath, "utf-8"));
86303
+ const resolvedPath = path95.resolve(cwd, filePath);
86304
+ sourceFiles.set(filePath, fs79.readFileSync(resolvedPath, "utf-8"));
85329
86305
  } catch {}
85330
86306
  }
85331
86307
  const report = await executeMutationSuite(typedArgs.patches, typedArgs.test_command, typedArgs.files, cwd, undefined, undefined, sourceFiles.size > 0 ? sourceFiles : undefined);
@@ -85343,8 +86319,8 @@ var mutation_test = createSwarmTool({
85343
86319
  init_dist();
85344
86320
  init_manager2();
85345
86321
  init_detector();
85346
- import * as fs79 from "fs";
85347
- import * as path95 from "path";
86322
+ import * as fs80 from "fs";
86323
+ import * as path96 from "path";
85348
86324
  init_create_tool();
85349
86325
  var MAX_FILE_SIZE2 = 2 * 1024 * 1024;
85350
86326
  var BINARY_CHECK_BYTES = 8192;
@@ -85410,7 +86386,7 @@ async function syntaxCheck(input, directory, config3) {
85410
86386
  if (languages?.length) {
85411
86387
  const lowerLangs = languages.map((l) => l.toLowerCase());
85412
86388
  filesToCheck = filesToCheck.filter((file3) => {
85413
- const ext = path95.extname(file3.path).toLowerCase();
86389
+ const ext = path96.extname(file3.path).toLowerCase();
85414
86390
  const langDef = getLanguageForExtension(ext);
85415
86391
  const fileProfile = getProfileForFile(file3.path);
85416
86392
  const langId = fileProfile?.id || langDef?.id;
@@ -85423,7 +86399,7 @@ async function syntaxCheck(input, directory, config3) {
85423
86399
  let skippedCount = 0;
85424
86400
  for (const fileInfo of filesToCheck) {
85425
86401
  const { path: filePath } = fileInfo;
85426
- const fullPath = path95.isAbsolute(filePath) ? filePath : path95.join(directory, filePath);
86402
+ const fullPath = path96.isAbsolute(filePath) ? filePath : path96.join(directory, filePath);
85427
86403
  const result = {
85428
86404
  path: filePath,
85429
86405
  language: "",
@@ -85453,7 +86429,7 @@ async function syntaxCheck(input, directory, config3) {
85453
86429
  }
85454
86430
  let content;
85455
86431
  try {
85456
- content = fs79.readFileSync(fullPath, "utf8");
86432
+ content = fs80.readFileSync(fullPath, "utf8");
85457
86433
  } catch {
85458
86434
  result.skipped_reason = "file_read_error";
85459
86435
  skippedCount++;
@@ -85472,7 +86448,7 @@ async function syntaxCheck(input, directory, config3) {
85472
86448
  results.push(result);
85473
86449
  continue;
85474
86450
  }
85475
- const ext = path95.extname(filePath).toLowerCase();
86451
+ const ext = path96.extname(filePath).toLowerCase();
85476
86452
  const langDef = getLanguageForExtension(ext);
85477
86453
  result.language = profile?.id || langDef?.id || "unknown";
85478
86454
  const errors5 = extractSyntaxErrors(parser, content);
@@ -85564,8 +86540,8 @@ init_dist();
85564
86540
  init_utils();
85565
86541
  init_create_tool();
85566
86542
  init_path_security();
85567
- import * as fs80 from "fs";
85568
- import * as path96 from "path";
86543
+ import * as fs81 from "fs";
86544
+ import * as path97 from "path";
85569
86545
  var MAX_TEXT_LENGTH = 200;
85570
86546
  var MAX_FILE_SIZE_BYTES11 = 1024 * 1024;
85571
86547
  var SUPPORTED_EXTENSIONS4 = new Set([
@@ -85631,9 +86607,9 @@ function validatePathsInput(paths, cwd) {
85631
86607
  return { error: "paths contains path traversal", resolvedPath: null };
85632
86608
  }
85633
86609
  try {
85634
- const resolvedPath = path96.resolve(paths);
85635
- const normalizedCwd = path96.resolve(cwd);
85636
- const normalizedResolved = path96.resolve(resolvedPath);
86610
+ const resolvedPath = path97.resolve(paths);
86611
+ const normalizedCwd = path97.resolve(cwd);
86612
+ const normalizedResolved = path97.resolve(resolvedPath);
85637
86613
  if (!normalizedResolved.startsWith(normalizedCwd)) {
85638
86614
  return {
85639
86615
  error: "paths must be within the current working directory",
@@ -85649,13 +86625,13 @@ function validatePathsInput(paths, cwd) {
85649
86625
  }
85650
86626
  }
85651
86627
  function isSupportedExtension(filePath) {
85652
- const ext = path96.extname(filePath).toLowerCase();
86628
+ const ext = path97.extname(filePath).toLowerCase();
85653
86629
  return SUPPORTED_EXTENSIONS4.has(ext);
85654
86630
  }
85655
86631
  function findSourceFiles4(dir, files = []) {
85656
86632
  let entries;
85657
86633
  try {
85658
- entries = fs80.readdirSync(dir);
86634
+ entries = fs81.readdirSync(dir);
85659
86635
  } catch {
85660
86636
  return files;
85661
86637
  }
@@ -85664,10 +86640,10 @@ function findSourceFiles4(dir, files = []) {
85664
86640
  if (SKIP_DIRECTORIES5.has(entry)) {
85665
86641
  continue;
85666
86642
  }
85667
- const fullPath = path96.join(dir, entry);
86643
+ const fullPath = path97.join(dir, entry);
85668
86644
  let stat4;
85669
86645
  try {
85670
- stat4 = fs80.statSync(fullPath);
86646
+ stat4 = fs81.statSync(fullPath);
85671
86647
  } catch {
85672
86648
  continue;
85673
86649
  }
@@ -85760,7 +86736,7 @@ var todo_extract = createSwarmTool({
85760
86736
  return JSON.stringify(errorResult, null, 2);
85761
86737
  }
85762
86738
  const scanPath = resolvedPath;
85763
- if (!fs80.existsSync(scanPath)) {
86739
+ if (!fs81.existsSync(scanPath)) {
85764
86740
  const errorResult = {
85765
86741
  error: `path not found: ${pathsInput}`,
85766
86742
  total: 0,
@@ -85770,13 +86746,13 @@ var todo_extract = createSwarmTool({
85770
86746
  return JSON.stringify(errorResult, null, 2);
85771
86747
  }
85772
86748
  const filesToScan = [];
85773
- const stat4 = fs80.statSync(scanPath);
86749
+ const stat4 = fs81.statSync(scanPath);
85774
86750
  if (stat4.isFile()) {
85775
86751
  if (isSupportedExtension(scanPath)) {
85776
86752
  filesToScan.push(scanPath);
85777
86753
  } else {
85778
86754
  const errorResult = {
85779
- error: `unsupported file extension: ${path96.extname(scanPath)}`,
86755
+ error: `unsupported file extension: ${path97.extname(scanPath)}`,
85780
86756
  total: 0,
85781
86757
  byPriority: { high: 0, medium: 0, low: 0 },
85782
86758
  entries: []
@@ -85789,11 +86765,11 @@ var todo_extract = createSwarmTool({
85789
86765
  const allEntries = [];
85790
86766
  for (const filePath of filesToScan) {
85791
86767
  try {
85792
- const fileStat = fs80.statSync(filePath);
86768
+ const fileStat = fs81.statSync(filePath);
85793
86769
  if (fileStat.size > MAX_FILE_SIZE_BYTES11) {
85794
86770
  continue;
85795
86771
  }
85796
- const content = fs80.readFileSync(filePath, "utf-8");
86772
+ const content = fs81.readFileSync(filePath, "utf-8");
85797
86773
  const entries = parseTodoComments(content, filePath, tagsSet);
85798
86774
  allEntries.push(...entries);
85799
86775
  } catch {}
@@ -85823,18 +86799,18 @@ init_tool();
85823
86799
  init_loader();
85824
86800
  init_schema();
85825
86801
  init_gate_evidence();
85826
- import * as fs82 from "fs";
85827
- import * as path98 from "path";
86802
+ import * as fs83 from "fs";
86803
+ import * as path99 from "path";
85828
86804
 
85829
86805
  // src/hooks/diff-scope.ts
85830
- import * as fs81 from "fs";
85831
- import * as path97 from "path";
86806
+ import * as fs82 from "fs";
86807
+ import * as path98 from "path";
85832
86808
  function getDeclaredScope(taskId, directory) {
85833
86809
  try {
85834
- const planPath = path97.join(directory, ".swarm", "plan.json");
85835
- if (!fs81.existsSync(planPath))
86810
+ const planPath = path98.join(directory, ".swarm", "plan.json");
86811
+ if (!fs82.existsSync(planPath))
85836
86812
  return null;
85837
- const raw = fs81.readFileSync(planPath, "utf-8");
86813
+ const raw = fs82.readFileSync(planPath, "utf-8");
85838
86814
  const plan = JSON.parse(raw);
85839
86815
  for (const phase of plan.phases ?? []) {
85840
86816
  for (const task of phase.tasks ?? []) {
@@ -85950,7 +86926,7 @@ var TIER_3_PATTERNS = [
85950
86926
  ];
85951
86927
  function matchesTier3Pattern(files) {
85952
86928
  for (const file3 of files) {
85953
- const fileName = path98.basename(file3);
86929
+ const fileName = path99.basename(file3);
85954
86930
  for (const pattern of TIER_3_PATTERNS) {
85955
86931
  if (pattern.test(fileName)) {
85956
86932
  return true;
@@ -85964,8 +86940,8 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
85964
86940
  if (hasActiveTurboMode()) {
85965
86941
  const resolvedDir2 = workingDirectory;
85966
86942
  try {
85967
- const planPath = path98.join(resolvedDir2, ".swarm", "plan.json");
85968
- const planRaw = fs82.readFileSync(planPath, "utf-8");
86943
+ const planPath = path99.join(resolvedDir2, ".swarm", "plan.json");
86944
+ const planRaw = fs83.readFileSync(planPath, "utf-8");
85969
86945
  const plan = JSON.parse(planRaw);
85970
86946
  for (const planPhase of plan.phases ?? []) {
85971
86947
  for (const task of planPhase.tasks ?? []) {
@@ -86034,8 +87010,8 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
86034
87010
  }
86035
87011
  try {
86036
87012
  const resolvedDir2 = workingDirectory;
86037
- const planPath = path98.join(resolvedDir2, ".swarm", "plan.json");
86038
- const planRaw = fs82.readFileSync(planPath, "utf-8");
87013
+ const planPath = path99.join(resolvedDir2, ".swarm", "plan.json");
87014
+ const planRaw = fs83.readFileSync(planPath, "utf-8");
86039
87015
  const plan = JSON.parse(planRaw);
86040
87016
  for (const planPhase of plan.phases ?? []) {
86041
87017
  for (const task of planPhase.tasks ?? []) {
@@ -86259,8 +87235,8 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
86259
87235
  };
86260
87236
  }
86261
87237
  }
86262
- normalizedDir = path98.normalize(args2.working_directory);
86263
- const pathParts = normalizedDir.split(path98.sep);
87238
+ normalizedDir = path99.normalize(args2.working_directory);
87239
+ const pathParts = normalizedDir.split(path99.sep);
86264
87240
  if (pathParts.includes("..")) {
86265
87241
  return {
86266
87242
  success: false,
@@ -86270,11 +87246,11 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
86270
87246
  ]
86271
87247
  };
86272
87248
  }
86273
- const resolvedDir = path98.resolve(normalizedDir);
87249
+ const resolvedDir = path99.resolve(normalizedDir);
86274
87250
  try {
86275
- const realPath = fs82.realpathSync(resolvedDir);
86276
- const planPath = path98.join(realPath, ".swarm", "plan.json");
86277
- if (!fs82.existsSync(planPath)) {
87251
+ const realPath = fs83.realpathSync(resolvedDir);
87252
+ const planPath = path99.join(realPath, ".swarm", "plan.json");
87253
+ if (!fs83.existsSync(planPath)) {
86278
87254
  return {
86279
87255
  success: false,
86280
87256
  message: `Invalid working_directory: plan not found in "${realPath}"`,
@@ -86305,22 +87281,22 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
86305
87281
  }
86306
87282
  if (args2.status === "in_progress") {
86307
87283
  try {
86308
- const evidencePath = path98.join(directory, ".swarm", "evidence", `${args2.task_id}.json`);
86309
- fs82.mkdirSync(path98.dirname(evidencePath), { recursive: true });
86310
- const fd = fs82.openSync(evidencePath, "wx");
87284
+ const evidencePath = path99.join(directory, ".swarm", "evidence", `${args2.task_id}.json`);
87285
+ fs83.mkdirSync(path99.dirname(evidencePath), { recursive: true });
87286
+ const fd = fs83.openSync(evidencePath, "wx");
86311
87287
  let writeOk = false;
86312
87288
  try {
86313
- fs82.writeSync(fd, JSON.stringify({
87289
+ fs83.writeSync(fd, JSON.stringify({
86314
87290
  taskId: args2.task_id,
86315
87291
  required_gates: ["reviewer", "test_engineer"],
86316
87292
  gates: {}
86317
87293
  }, null, 2));
86318
87294
  writeOk = true;
86319
87295
  } finally {
86320
- fs82.closeSync(fd);
87296
+ fs83.closeSync(fd);
86321
87297
  if (!writeOk) {
86322
87298
  try {
86323
- fs82.unlinkSync(evidencePath);
87299
+ fs83.unlinkSync(evidencePath);
86324
87300
  } catch {}
86325
87301
  }
86326
87302
  }
@@ -86330,8 +87306,8 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
86330
87306
  recoverTaskStateFromDelegations(args2.task_id);
86331
87307
  let phaseRequiresReviewer = true;
86332
87308
  try {
86333
- const planPath = path98.join(directory, ".swarm", "plan.json");
86334
- const planRaw = fs82.readFileSync(planPath, "utf-8");
87309
+ const planPath = path99.join(directory, ".swarm", "plan.json");
87310
+ const planRaw = fs83.readFileSync(planPath, "utf-8");
86335
87311
  const plan = JSON.parse(planRaw);
86336
87312
  const taskPhase = plan.phases.find((p) => p.tasks.some((t) => t.id === args2.task_id));
86337
87313
  if (taskPhase?.required_agents && !taskPhase.required_agents.includes("reviewer")) {
@@ -86433,6 +87409,216 @@ var update_task_status = createSwarmTool({
86433
87409
  return JSON.stringify(await executeUpdateTaskStatus(args2, _directory), null, 2);
86434
87410
  }
86435
87411
  });
87412
+ // src/tools/web-search.ts
87413
+ init_dist();
87414
+ init_zod();
87415
+ init_loader();
87416
+
87417
+ // src/council/web-search-provider.ts
87418
+ class WebSearchError extends Error {
87419
+ cause;
87420
+ constructor(message, cause) {
87421
+ super(message);
87422
+ this.cause = cause;
87423
+ this.name = "WebSearchError";
87424
+ }
87425
+ }
87426
+
87427
+ class WebSearchConfigError extends Error {
87428
+ constructor(message) {
87429
+ super(message);
87430
+ this.name = "WebSearchConfigError";
87431
+ }
87432
+ }
87433
+
87434
+ class TavilyProvider {
87435
+ apiKey;
87436
+ constructor(apiKey) {
87437
+ this.apiKey = apiKey;
87438
+ }
87439
+ async search(query, maxResults) {
87440
+ let response;
87441
+ try {
87442
+ response = await fetch("https://api.tavily.com/search", {
87443
+ method: "POST",
87444
+ headers: { "Content-Type": "application/json" },
87445
+ body: JSON.stringify({
87446
+ api_key: this.apiKey,
87447
+ query,
87448
+ max_results: maxResults,
87449
+ search_depth: "advanced"
87450
+ })
87451
+ });
87452
+ } catch (err3) {
87453
+ throw new WebSearchError(`Tavily network error for query "${query}"`, err3);
87454
+ }
87455
+ if (!response.ok) {
87456
+ throw new WebSearchError(`Tavily HTTP ${response.status} for query "${query}"`);
87457
+ }
87458
+ let body2;
87459
+ try {
87460
+ body2 = await response.json();
87461
+ } catch (err3) {
87462
+ throw new WebSearchError("Tavily returned non-JSON response", err3);
87463
+ }
87464
+ const results = body2?.results;
87465
+ if (!Array.isArray(results)) {
87466
+ return [];
87467
+ }
87468
+ return results.filter((r) => typeof r?.title === "string" && typeof r?.url === "string" && typeof r?.content === "string").map((r) => ({
87469
+ title: r.title,
87470
+ url: r.url,
87471
+ snippet: r.content,
87472
+ query
87473
+ }));
87474
+ }
87475
+ }
87476
+
87477
+ class BraveProvider {
87478
+ apiKey;
87479
+ constructor(apiKey) {
87480
+ this.apiKey = apiKey;
87481
+ }
87482
+ async search(query, maxResults) {
87483
+ const url3 = new URL("https://api.search.brave.com/res/v1/web/search");
87484
+ url3.searchParams.set("q", query);
87485
+ url3.searchParams.set("count", String(maxResults));
87486
+ let response;
87487
+ try {
87488
+ response = await fetch(url3.toString(), {
87489
+ method: "GET",
87490
+ headers: {
87491
+ "X-Subscription-Token": this.apiKey,
87492
+ Accept: "application/json"
87493
+ }
87494
+ });
87495
+ } catch (err3) {
87496
+ throw new WebSearchError(`Brave network error for query "${query}"`, err3);
87497
+ }
87498
+ if (!response.ok) {
87499
+ throw new WebSearchError(`Brave HTTP ${response.status} for query "${query}"`);
87500
+ }
87501
+ let body2;
87502
+ try {
87503
+ body2 = await response.json();
87504
+ } catch (err3) {
87505
+ throw new WebSearchError("Brave returned non-JSON response", err3);
87506
+ }
87507
+ const results = body2?.web?.results;
87508
+ if (!Array.isArray(results)) {
87509
+ return [];
87510
+ }
87511
+ return results.filter((r) => typeof r?.title === "string" && typeof r?.url === "string" && typeof r?.description === "string").map((r) => ({
87512
+ title: r.title,
87513
+ url: r.url,
87514
+ snippet: r.description,
87515
+ query
87516
+ }));
87517
+ }
87518
+ }
87519
+ function resolveApiKey(provider, configKey) {
87520
+ if (configKey && configKey.length > 0) {
87521
+ return configKey;
87522
+ }
87523
+ const envName = provider === "tavily" ? "TAVILY_API_KEY" : "BRAVE_SEARCH_API_KEY";
87524
+ const fromEnv = process.env[envName];
87525
+ return fromEnv && fromEnv.length > 0 ? fromEnv : undefined;
87526
+ }
87527
+ function createWebSearchProvider(config3) {
87528
+ const apiKey = resolveApiKey(config3.searchProvider, config3.searchApiKey);
87529
+ if (!apiKey) {
87530
+ const envName = config3.searchProvider === "tavily" ? "TAVILY_API_KEY" : "BRAVE_SEARCH_API_KEY";
87531
+ throw new WebSearchConfigError(`No API key for search provider "${config3.searchProvider}". Set ` + `council.general.searchApiKey in opencode-swarm.json or export ${envName}.`);
87532
+ }
87533
+ switch (config3.searchProvider) {
87534
+ case "tavily":
87535
+ return new TavilyProvider(apiKey);
87536
+ case "brave":
87537
+ return new BraveProvider(apiKey);
87538
+ }
87539
+ }
87540
+
87541
+ // src/tools/web-search.ts
87542
+ init_create_tool();
87543
+ init_resolve_working_directory();
87544
+ var MAX_RESULTS_HARD_CAP = 10;
87545
+ var ArgsSchema4 = exports_external.object({
87546
+ query: exports_external.string().min(1).max(500),
87547
+ max_results: exports_external.number().int().min(1).max(20).optional(),
87548
+ working_directory: exports_external.string().optional()
87549
+ });
87550
+ var web_search = createSwarmTool({
87551
+ description: "External web search for council member agents. Returns titled results with snippets and URLs. " + "Restricted to council_member agents via AGENT_TOOL_MAP. Requires council.general.enabled and a " + "configured search API key (Tavily or Brave). max_results is capped at 10 with default from council.general.maxSourcesPerMember.",
87552
+ args: {
87553
+ query: tool.schema.string().min(1).max(500).describe("Search query string (1\u2013500 characters)."),
87554
+ max_results: tool.schema.number().int().min(1).max(20).optional().describe(`Number of results to request (1\u201320). Hard-capped at ${MAX_RESULTS_HARD_CAP}. Defaults to council.general.maxSourcesPerMember.`),
87555
+ working_directory: tool.schema.string().optional().describe("Project root for config resolution. Optional.")
87556
+ },
87557
+ execute: async (args2, directory) => {
87558
+ const parsed = ArgsSchema4.safeParse(args2);
87559
+ if (!parsed.success) {
87560
+ const fail = {
87561
+ success: false,
87562
+ reason: "invalid_args",
87563
+ message: parsed.error.issues.map((i2) => `${i2.path.join(".")}: ${i2.message}`).join("; ")
87564
+ };
87565
+ return JSON.stringify(fail, null, 2);
87566
+ }
87567
+ const dirResult = resolveWorkingDirectory(parsed.data.working_directory, directory);
87568
+ if (!dirResult.success) {
87569
+ const fail = {
87570
+ success: false,
87571
+ reason: "invalid_working_directory",
87572
+ message: dirResult.message
87573
+ };
87574
+ return JSON.stringify(fail, null, 2);
87575
+ }
87576
+ const config3 = loadPluginConfig(dirResult.directory);
87577
+ const generalConfig = config3.council?.general;
87578
+ if (!generalConfig || generalConfig.enabled !== true) {
87579
+ const fail = {
87580
+ success: false,
87581
+ reason: "council_general_disabled",
87582
+ message: "web_search is disabled \u2014 set council.general.enabled: true in opencode-swarm.json."
87583
+ };
87584
+ return JSON.stringify(fail, null, 2);
87585
+ }
87586
+ const requested = parsed.data.max_results ?? generalConfig.maxSourcesPerMember;
87587
+ const maxResults = Math.min(requested, MAX_RESULTS_HARD_CAP);
87588
+ let provider;
87589
+ try {
87590
+ provider = createWebSearchProvider(generalConfig);
87591
+ } catch (err3) {
87592
+ const fail = {
87593
+ success: false,
87594
+ reason: err3 instanceof WebSearchConfigError ? "missing_api_key" : "provider_init_failed",
87595
+ message: err3 instanceof Error ? err3.message : String(err3)
87596
+ };
87597
+ return JSON.stringify(fail, null, 2);
87598
+ }
87599
+ try {
87600
+ const results = await provider.search(parsed.data.query, maxResults);
87601
+ const ok2 = {
87602
+ success: true,
87603
+ query: parsed.data.query,
87604
+ totalResults: results.length,
87605
+ results: results.map(({ title, url: url3, snippet }) => ({
87606
+ title,
87607
+ url: url3,
87608
+ snippet
87609
+ }))
87610
+ };
87611
+ return JSON.stringify(ok2, null, 2);
87612
+ } catch (err3) {
87613
+ const fail = {
87614
+ success: false,
87615
+ reason: err3 instanceof WebSearchError ? "search_failed" : "unknown",
87616
+ message: err3 instanceof Error ? err3.message : String(err3)
87617
+ };
87618
+ return JSON.stringify(fail, null, 2);
87619
+ }
87620
+ }
87621
+ });
86436
87622
  // src/tools/write-drift-evidence.ts
86437
87623
  init_tool();
86438
87624
  init_qa_gate_profile();
@@ -86440,8 +87626,8 @@ init_utils2();
86440
87626
  init_ledger();
86441
87627
  init_manager();
86442
87628
  init_create_tool();
86443
- import fs83 from "fs";
86444
- import path99 from "path";
87629
+ import fs84 from "fs";
87630
+ import path100 from "path";
86445
87631
  function derivePlanId5(plan) {
86446
87632
  return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
86447
87633
  }
@@ -86492,7 +87678,7 @@ async function executeWriteDriftEvidence(args2, directory) {
86492
87678
  entries: [evidenceEntry]
86493
87679
  };
86494
87680
  const filename = "drift-verifier.json";
86495
- const relativePath = path99.join("evidence", String(phase), filename);
87681
+ const relativePath = path100.join("evidence", String(phase), filename);
86496
87682
  let validatedPath;
86497
87683
  try {
86498
87684
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -86503,12 +87689,12 @@ async function executeWriteDriftEvidence(args2, directory) {
86503
87689
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
86504
87690
  }, null, 2);
86505
87691
  }
86506
- const evidenceDir = path99.dirname(validatedPath);
87692
+ const evidenceDir = path100.dirname(validatedPath);
86507
87693
  try {
86508
- await fs83.promises.mkdir(evidenceDir, { recursive: true });
86509
- const tempPath = path99.join(evidenceDir, `.${filename}.tmp`);
86510
- await fs83.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
86511
- await fs83.promises.rename(tempPath, validatedPath);
87694
+ await fs84.promises.mkdir(evidenceDir, { recursive: true });
87695
+ const tempPath = path100.join(evidenceDir, `.${filename}.tmp`);
87696
+ await fs84.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
87697
+ await fs84.promises.rename(tempPath, validatedPath);
86512
87698
  let snapshotInfo;
86513
87699
  let snapshotError;
86514
87700
  let qaProfileLocked;
@@ -86602,8 +87788,8 @@ var write_drift_evidence = createSwarmTool({
86602
87788
  init_tool();
86603
87789
  init_utils2();
86604
87790
  init_create_tool();
86605
- import fs84 from "fs";
86606
- import path100 from "path";
87791
+ import fs85 from "fs";
87792
+ import path101 from "path";
86607
87793
  function normalizeVerdict2(verdict) {
86608
87794
  switch (verdict) {
86609
87795
  case "APPROVED":
@@ -86651,7 +87837,7 @@ async function executeWriteHallucinationEvidence(args2, directory) {
86651
87837
  entries: [evidenceEntry]
86652
87838
  };
86653
87839
  const filename = "hallucination-guard.json";
86654
- const relativePath = path100.join("evidence", String(phase), filename);
87840
+ const relativePath = path101.join("evidence", String(phase), filename);
86655
87841
  let validatedPath;
86656
87842
  try {
86657
87843
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -86662,12 +87848,12 @@ async function executeWriteHallucinationEvidence(args2, directory) {
86662
87848
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
86663
87849
  }, null, 2);
86664
87850
  }
86665
- const evidenceDir = path100.dirname(validatedPath);
87851
+ const evidenceDir = path101.dirname(validatedPath);
86666
87852
  try {
86667
- await fs84.promises.mkdir(evidenceDir, { recursive: true });
86668
- const tempPath = path100.join(evidenceDir, `.${filename}.tmp`);
86669
- await fs84.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
86670
- await fs84.promises.rename(tempPath, validatedPath);
87853
+ await fs85.promises.mkdir(evidenceDir, { recursive: true });
87854
+ const tempPath = path101.join(evidenceDir, `.${filename}.tmp`);
87855
+ await fs85.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
87856
+ await fs85.promises.rename(tempPath, validatedPath);
86671
87857
  return JSON.stringify({
86672
87858
  success: true,
86673
87859
  phase,
@@ -86713,8 +87899,8 @@ var write_hallucination_evidence = createSwarmTool({
86713
87899
  init_tool();
86714
87900
  init_utils2();
86715
87901
  init_create_tool();
86716
- import fs85 from "fs";
86717
- import path101 from "path";
87902
+ import fs86 from "fs";
87903
+ import path102 from "path";
86718
87904
  function normalizeVerdict3(verdict) {
86719
87905
  switch (verdict) {
86720
87906
  case "PASS":
@@ -86788,7 +87974,7 @@ async function executeWriteMutationEvidence(args2, directory) {
86788
87974
  entries: [evidenceEntry]
86789
87975
  };
86790
87976
  const filename = "mutation-gate.json";
86791
- const relativePath = path101.join("evidence", String(phase), filename);
87977
+ const relativePath = path102.join("evidence", String(phase), filename);
86792
87978
  let validatedPath;
86793
87979
  try {
86794
87980
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -86799,12 +87985,12 @@ async function executeWriteMutationEvidence(args2, directory) {
86799
87985
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
86800
87986
  }, null, 2);
86801
87987
  }
86802
- const evidenceDir = path101.dirname(validatedPath);
87988
+ const evidenceDir = path102.dirname(validatedPath);
86803
87989
  try {
86804
- await fs85.promises.mkdir(evidenceDir, { recursive: true });
86805
- const tempPath = path101.join(evidenceDir, `.${filename}.tmp`);
86806
- await fs85.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
86807
- await fs85.promises.rename(tempPath, validatedPath);
87990
+ await fs86.promises.mkdir(evidenceDir, { recursive: true });
87991
+ const tempPath = path102.join(evidenceDir, `.${filename}.tmp`);
87992
+ await fs86.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
87993
+ await fs86.promises.rename(tempPath, validatedPath);
86808
87994
  return JSON.stringify({
86809
87995
  success: true,
86810
87996
  phase,
@@ -87022,7 +88208,7 @@ var OpenCodeSwarm = async (ctx) => {
87022
88208
  const { PreflightTriggerManager: PTM } = await Promise.resolve().then(() => (init_trigger(), exports_trigger));
87023
88209
  preflightTriggerManager = new PTM(automationConfig);
87024
88210
  const { AutomationStatusArtifact: ASA } = await Promise.resolve().then(() => (init_status_artifact(), exports_status_artifact));
87025
- const swarmDir = path102.resolve(ctx.directory, ".swarm");
88211
+ const swarmDir = path103.resolve(ctx.directory, ".swarm");
87026
88212
  statusArtifact = new ASA(swarmDir);
87027
88213
  statusArtifact.updateConfig(automationConfig.mode, automationConfig.capabilities);
87028
88214
  if (automationConfig.capabilities?.evidence_auto_summaries === true) {
@@ -87128,6 +88314,7 @@ var OpenCodeSwarm = async (ctx) => {
87128
88314
  completion_verify,
87129
88315
  complexity_hotspots,
87130
88316
  convene_council,
88317
+ convene_general_council,
87131
88318
  curator_analyze,
87132
88319
  declare_council_criteria,
87133
88320
  knowledge_add,
@@ -87174,6 +88361,7 @@ var OpenCodeSwarm = async (ctx) => {
87174
88361
  build_check,
87175
88362
  suggest_patch: suggestPatch,
87176
88363
  update_task_status,
88364
+ web_search,
87177
88365
  write_retro,
87178
88366
  write_drift_evidence,
87179
88367
  write_hallucination_evidence,