opencode-swarm 6.83.0 → 6.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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();
@@ -15207,13 +15218,37 @@ var init_schema = __esm(() => {
15207
15218
  rules: exports_external.record(exports_external.string(), AgentAuthorityRuleSchema).default({}),
15208
15219
  universal_deny_prefixes: exports_external.array(exports_external.string().min(1)).default([])
15209
15220
  });
15221
+ GeneralCouncilMemberConfigSchema = exports_external.object({
15222
+ memberId: exports_external.string().min(1),
15223
+ model: exports_external.string().min(1),
15224
+ role: exports_external.enum([
15225
+ "generalist",
15226
+ "skeptic",
15227
+ "domain_expert",
15228
+ "devil_advocate",
15229
+ "synthesizer"
15230
+ ]),
15231
+ persona: exports_external.string().optional()
15232
+ }).strict();
15233
+ GeneralCouncilConfigSchema = exports_external.object({
15234
+ enabled: exports_external.boolean().default(false),
15235
+ searchProvider: exports_external.enum(["tavily", "brave"]).default("tavily"),
15236
+ searchApiKey: exports_external.string().optional(),
15237
+ members: exports_external.array(GeneralCouncilMemberConfigSchema).default([]),
15238
+ presets: exports_external.record(exports_external.string(), exports_external.array(GeneralCouncilMemberConfigSchema)).default({}),
15239
+ deliberate: exports_external.boolean().default(true),
15240
+ moderator: exports_external.boolean().default(true),
15241
+ moderatorModel: exports_external.string().optional(),
15242
+ maxSourcesPerMember: exports_external.number().int().min(1).max(20).default(5)
15243
+ }).strict();
15210
15244
  CouncilConfigSchema = exports_external.object({
15211
15245
  enabled: exports_external.boolean().default(false),
15212
15246
  maxRounds: exports_external.number().int().min(1).max(10).default(3),
15213
15247
  parallelTimeoutMs: exports_external.number().int().min(5000).max(120000).default(30000),
15214
15248
  vetoPriority: exports_external.boolean().default(true),
15215
15249
  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.")
15250
+ 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."),
15251
+ general: GeneralCouncilConfigSchema.optional()
15217
15252
  }).strict();
15218
15253
  ParallelizationConfigSchema = exports_external.object({
15219
15254
  enabled: exports_external.boolean().default(false),
@@ -19707,7 +19742,8 @@ var init_qa_gate_profile = __esm(() => {
19707
19742
  critic_pre_plan: true,
19708
19743
  hallucination_guard: false,
19709
19744
  sast_enabled: true,
19710
- mutation_test: false
19745
+ mutation_test: false,
19746
+ council_general_review: false
19711
19747
  };
19712
19748
  });
19713
19749
 
@@ -41864,6 +41900,76 @@ var init_config2 = __esm(() => {
41864
41900
  init_loader();
41865
41901
  });
41866
41902
 
41903
+ // src/commands/council.ts
41904
+ function sanitizeQuestion(raw) {
41905
+ const collapsed = raw.replace(/\s+/g, " ").trim();
41906
+ const stripped = collapsed.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
41907
+ const normalized = stripped.replace(/\s+/g, " ").trim();
41908
+ if (normalized.length <= MAX_QUESTION_LEN)
41909
+ return normalized;
41910
+ return `${normalized.slice(0, MAX_QUESTION_LEN)}\u2026`;
41911
+ }
41912
+ function sanitizePresetName(raw) {
41913
+ const trimmed = raw.trim();
41914
+ if (!trimmed)
41915
+ return null;
41916
+ if (trimmed.length > 64)
41917
+ return null;
41918
+ if (!/^[A-Za-z0-9_-]+$/.test(trimmed))
41919
+ return null;
41920
+ return trimmed;
41921
+ }
41922
+ function parseArgs(args2) {
41923
+ const out2 = { specReview: false, rest: [] };
41924
+ for (let i2 = 0;i2 < args2.length; i2++) {
41925
+ const token = args2[i2];
41926
+ if (token === "--spec-review") {
41927
+ out2.specReview = true;
41928
+ continue;
41929
+ }
41930
+ if (token === "--preset") {
41931
+ const next = args2[i2 + 1];
41932
+ if (next !== undefined) {
41933
+ const sanitized = sanitizePresetName(next);
41934
+ if (sanitized)
41935
+ out2.preset = sanitized;
41936
+ i2++;
41937
+ }
41938
+ continue;
41939
+ }
41940
+ out2.rest.push(token);
41941
+ }
41942
+ return out2;
41943
+ }
41944
+ async function handleCouncilCommand(_directory, args2) {
41945
+ const parsed = parseArgs(args2);
41946
+ const question = sanitizeQuestion(parsed.rest.join(" "));
41947
+ if (!question) {
41948
+ return USAGE;
41949
+ }
41950
+ const tokens = ["MODE: COUNCIL"];
41951
+ if (parsed.preset) {
41952
+ tokens.push(`preset=${parsed.preset}`);
41953
+ }
41954
+ if (parsed.specReview) {
41955
+ tokens.push("spec_review");
41956
+ }
41957
+ return `[${tokens.join(" ")}] ${question}`;
41958
+ }
41959
+ var MAX_QUESTION_LEN = 2000, USAGE;
41960
+ var init_council = __esm(() => {
41961
+ USAGE = [
41962
+ "Usage: /swarm council <question> [--preset <name>] [--spec-review]",
41963
+ "",
41964
+ " question The question to put to the council",
41965
+ " --preset <name> Use a named member preset from council.general.presets",
41966
+ " --spec-review Use spec_review mode (single advisory pass on a draft spec)",
41967
+ "",
41968
+ "Requires council.general.enabled: true and a configured search API key in opencode-swarm.json."
41969
+ ].join(`
41970
+ `);
41971
+ });
41972
+
41867
41973
  // src/agents/explorer.ts
41868
41974
  function createExplorerAgent(model, customPrompt, customAppendPrompt) {
41869
41975
  let prompt = EXPLORER_PROMPT;
@@ -52150,7 +52256,8 @@ var init_qa_gates = __esm(() => {
52150
52256
  "critic_pre_plan",
52151
52257
  "hallucination_guard",
52152
52258
  "sast_enabled",
52153
- "mutation_test"
52259
+ "mutation_test",
52260
+ "council_general_review"
52154
52261
  ];
52155
52262
  });
52156
52263
 
@@ -53776,6 +53883,7 @@ var init_registry = __esm(() => {
53776
53883
  init_checkpoint2();
53777
53884
  init_close();
53778
53885
  init_config2();
53886
+ init_council();
53779
53887
  init_curate();
53780
53888
  init_dark_matter();
53781
53889
  init_diagnose();
@@ -53931,11 +54039,17 @@ var init_registry = __esm(() => {
53931
54039
  args: "[topic-text]",
53932
54040
  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
54041
  },
54042
+ council: {
54043
+ handler: (ctx) => handleCouncilCommand(ctx.directory, ctx.args),
54044
+ description: "Enter architect MODE: COUNCIL \u2014 multi-model deliberation [question] [--preset <name>] [--spec-review]",
54045
+ args: "<question> [--preset <name>] [--spec-review]",
54046
+ 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."
54047
+ },
53934
54048
  "qa-gates": {
53935
54049
  handler: (ctx) => handleQaGatesCommand(ctx.directory, ctx.args, ctx.sessionID),
53936
54050
  description: "View or modify QA gate profile for the current plan [enable|override <gate>...]",
53937
54051
  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."
54052
+ 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
54053
  },
53940
54054
  promote: {
53941
54055
  handler: (ctx) => handlePromoteCommand(ctx.directory, ctx.args),
@@ -54091,14 +54205,24 @@ from different members.`;
54091
54205
  function buildYourToolsList(council) {
54092
54206
  const tools = AGENT_TOOL_MAP.architect ?? [];
54093
54207
  const sorted = [...tools].sort();
54094
- const filtered = council?.enabled === true ? sorted : sorted.filter((t) => t !== "convene_council" && t !== "declare_council_criteria");
54208
+ const qaCouncilEnabled = council?.enabled === true;
54209
+ const generalCouncilEnabled = council?.general?.enabled === true;
54210
+ const filtered = sorted.filter((t) => {
54211
+ if (!qaCouncilEnabled && (t === "convene_council" || t === "declare_council_criteria")) {
54212
+ return false;
54213
+ }
54214
+ if (!generalCouncilEnabled && t === "convene_general_council") {
54215
+ return false;
54216
+ }
54217
+ return true;
54218
+ });
54095
54219
  return `Task (delegation), ${filtered.join(", ")}.`;
54096
54220
  }
54097
54221
  function buildQaGateSelectionDialogue(modeLabel) {
54098
54222
  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
54223
  return `${leadIn}
54100
54224
 
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:
54225
+ 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
54226
  - reviewer (default: ON) \u2014 code review of coder output
54103
54227
  - test_engineer (default: ON) \u2014 test verification of coder output
54104
54228
  - sme_enabled (default: ON) \u2014 SME consultation during planning/clarification
@@ -54107,13 +54231,24 @@ Present the eight gates with their defaults (DEFAULT_QA_GATES) as a single user-
54107
54231
  - council_mode (default: OFF) \u2014 multi-member council gate (recommended for high-impact architecture, public APIs, schema/data mutation, security-sensitive code)
54108
54232
  - 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
54233
  - 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)
54234
+ - 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
54235
 
54111
54236
  One question, one message, defaults pre-stated. Wait for the user's answer.`;
54112
54237
  }
54113
54238
  function buildAvailableToolsList(council) {
54114
54239
  const tools = AGENT_TOOL_MAP.architect ?? [];
54115
54240
  const sorted = [...tools].sort();
54116
- const filtered = council?.enabled === true ? sorted : sorted.filter((t) => t !== "convene_council" && t !== "declare_council_criteria");
54241
+ const qaCouncilEnabled = council?.enabled === true;
54242
+ const generalCouncilEnabled = council?.general?.enabled === true;
54243
+ const filtered = sorted.filter((t) => {
54244
+ if (!qaCouncilEnabled && (t === "convene_council" || t === "declare_council_criteria")) {
54245
+ return false;
54246
+ }
54247
+ if (!generalCouncilEnabled && t === "convene_general_council") {
54248
+ return false;
54249
+ }
54250
+ return true;
54251
+ });
54117
54252
  return filtered.map((t) => {
54118
54253
  const desc = TOOL_DESCRIPTIONS[t];
54119
54254
  return desc ? `${t} (${desc})` : t;
@@ -54154,7 +54289,8 @@ function buildSlashCommandsList() {
54154
54289
  "analyze",
54155
54290
  "plan",
54156
54291
  "sync-plan",
54157
- "acknowledge-spec-drift"
54292
+ "acknowledge-spec-drift",
54293
+ "council"
54158
54294
  ],
54159
54295
  "Execution Modes": ["turbo", "full-auto"],
54160
54296
  Observation: [
@@ -54799,6 +54935,29 @@ MODE: BRAINSTORM runs seven phases in strict order. Do not skip phases. Do not c
54799
54935
  **Phase 6: QA GATE SELECTION (architect, dialogue only).**
54800
54936
  {{QA_GATE_DIALOGUE_BRAINSTORM}}
54801
54937
 
54938
+ <!-- BEHAVIORAL_GUIDANCE_START -->
54939
+ GATE SELECTION IS MANDATORY \u2014 these thoughts are WRONG and must be ignored:
54940
+ \u2717 "I'll use the defaults \u2014 they're probably fine"
54941
+ \u2192 WRONG: defaults are not the user's decision. The user must be asked every time.
54942
+ \u2717 "The user didn't mention gates, so defaults are fine"
54943
+ \u2192 WRONG: silence is not consent. The gate dialogue is not optional.
54944
+ \u2717 "I'll handle it in MODE: PLAN after the spec is done"
54945
+ \u2192 WRONG: ## Pending QA Gate Selection must exist in context.md BEFORE save_plan is called.
54946
+ save_plan will reject with QA_GATE_SELECTION_REQUIRED if this section is absent.
54947
+ \u2717 "This feature is simple \u2014 gates are obvious"
54948
+ \u2192 WRONG: complexity does not exempt this step. Gate selection is mandatory for ALL plans.
54949
+ \u2717 "I already know which gates are right for this project"
54950
+ \u2192 WRONG: the architect does not configure gates. The user configures gates. Always ask.
54951
+ \u2717 "council_general_review is off by default, I don't need to mention it"
54952
+ \u2192 WRONG: every gate is presented with its default stated. The user opts in or accepts the default explicitly.
54953
+
54954
+ MANDATORY PAUSE: Do NOT write the spec summary (step 7). Do NOT suggest next steps.
54955
+ You are BLOCKED until ALL THREE of these conditions are met:
54956
+ (1) The gate selection question has been presented to the user in a single message
54957
+ (2) The user has responded (accept defaults OR customized list)
54958
+ (3) The elected gates have been written to .swarm/context.md under "## Pending QA Gate Selection"
54959
+ <!-- BEHAVIORAL_GUIDANCE_END -->
54960
+
54802
54961
  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
54962
  \`\`\`
54804
54963
  ## Pending QA Gate Selection
@@ -54810,6 +54969,7 @@ Do NOT call \`set_qa_gates\` yet \u2014 \`plan.json\` does not exist at this poi
54810
54969
  - council_mode: <true|false>
54811
54970
  - hallucination_guard: <true|false>
54812
54971
  - mutation_test: <true|false>
54972
+ - council_general_review: <true|false>
54813
54973
  - recorded_at: <ISO timestamp>
54814
54974
  \`\`\`
54815
54975
  MODE: PLAN applies these after \`save_plan\` succeeds via \`set_qa_gates\`.
@@ -54852,6 +55012,29 @@ Activates when: user asks to "specify", "define requirements", "write a spec", o
54852
55012
  5b. **QA GATE SELECTION (dialogue only).**
54853
55013
  {{QA_GATE_DIALOGUE_SPECIFY}}
54854
55014
 
55015
+ <!-- BEHAVIORAL_GUIDANCE_START -->
55016
+ GATE SELECTION IS MANDATORY \u2014 these thoughts are WRONG and must be ignored:
55017
+ \u2717 "I'll use the defaults \u2014 they're probably fine"
55018
+ \u2192 WRONG: defaults are not the user's decision. The user must be asked every time.
55019
+ \u2717 "The user didn't mention gates, so defaults are fine"
55020
+ \u2192 WRONG: silence is not consent. The gate dialogue is not optional.
55021
+ \u2717 "I'll handle it in MODE: PLAN after the spec is done"
55022
+ \u2192 WRONG: ## Pending QA Gate Selection must exist in context.md BEFORE save_plan is called.
55023
+ save_plan will reject with QA_GATE_SELECTION_REQUIRED if this section is absent.
55024
+ \u2717 "This feature is simple \u2014 gates are obvious"
55025
+ \u2192 WRONG: complexity does not exempt this step. Gate selection is mandatory for ALL plans.
55026
+ \u2717 "I already know which gates are right for this project"
55027
+ \u2192 WRONG: the architect does not configure gates. The user configures gates. Always ask.
55028
+ \u2717 "council_general_review is off by default, I don't need to mention it"
55029
+ \u2192 WRONG: every gate is presented with its default stated. The user opts in or accepts the default explicitly.
55030
+
55031
+ MANDATORY PAUSE: Do NOT write the spec summary (step 7). Do NOT suggest next steps.
55032
+ You are BLOCKED until ALL THREE of these conditions are met:
55033
+ (1) The gate selection question has been presented to the user in a single message
55034
+ (2) The user has responded (accept defaults OR customized list)
55035
+ (3) The elected gates have been written to .swarm/context.md under "## Pending QA Gate Selection"
55036
+ <!-- BEHAVIORAL_GUIDANCE_END -->
55037
+
54855
55038
  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
55039
  \`\`\`
54857
55040
  ## Pending QA Gate Selection
@@ -54863,9 +55046,37 @@ Do NOT call \`set_qa_gates\` yet \u2014 \`plan.json\` does not exist at this poi
54863
55046
  - council_mode: <true|false>
54864
55047
  - hallucination_guard: <true|false>
54865
55048
  - mutation_test: <true|false>
55049
+ - council_general_review: <true|false>
54866
55050
  - recorded_at: <ISO timestamp>
54867
55051
  \`\`\`
54868
55052
  MODE: PLAN will read this section after \`save_plan\` succeeds and persist via \`set_qa_gates\`.
55053
+
55054
+ 5c. **SPECIFY-COUNCIL-REVIEW (fires ONLY when council_general_review gate is true).**
55055
+ 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.
55056
+
55057
+ If \`council_general_review\` is true:
55058
+ 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.
55059
+ 2. Determine the council members from \`council.general.members\` (or \`council.general.presets[<name>]\` if you were invoked via \`/swarm council --preset <name>\` originally).
55060
+ 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.
55061
+ 4. Collect all member JSON responses.
55062
+ 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.
55063
+ 6. Read \`consensusPoints\` \u2014 incorporate unambiguous consensus directly into the spec.
55064
+ 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.
55065
+ 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.
55066
+ 9. Revise \`.swarm/spec.md\` to reflect the council input.
55067
+
55068
+ <!-- BEHAVIORAL_GUIDANCE_START -->
55069
+ SPECIFY-COUNCIL-REVIEW RULES:
55070
+ \u2717 "council_general_review is off by default, I'll skip this"
55071
+ \u2192 CORRECT only when the gate is explicitly false or absent. Do NOT assume false. Read the actual gate value before deciding to skip.
55072
+ \u2717 "The spec is already good, no need to ask the council"
55073
+ \u2192 WRONG when gate is true: the user enabled this gate for a reason. Run it regardless.
55074
+ \u2717 "I'll include round2Responses for spec_review \u2014 more is better"
55075
+ \u2192 WRONG: spec review is a single advisory pass. Omit \`round2Responses\` for spec_review mode.
55076
+ \u2717 "I'll skip the moderator pass to save time"
55077
+ \u2192 WRONG when council.general.moderator is true: invoke \`{{AGENT_PREFIX}}council_moderator\` with the moderatorPrompt the tool returns.
55078
+ <!-- BEHAVIORAL_GUIDANCE_END -->
55079
+
54869
55080
  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
55081
 
54871
55082
  SPEC CONTENT RULES \u2014 the spec MUST NOT contain:
@@ -55034,6 +55245,39 @@ This check fires automatically in:
55034
55245
 
55035
55246
  GREENFIELD EXEMPTION: If the work is purely greenfield (new project, no existing codebase references), skip this check.
55036
55247
 
55248
+ ### MODE: COUNCIL
55249
+
55250
+ Activates when: user invokes \`/swarm council <question>\` (optionally with \`--preset <name>\` or \`--spec-review\`).
55251
+
55252
+ 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.
55253
+
55254
+ 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\`).
55255
+
55256
+ #### Pre-flight (always run first)
55257
+ 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.
55258
+
55259
+ #### Round 1 \u2014 Parallel Independent Search
55260
+ 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.
55261
+ 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.
55262
+ 4. Collect all member JSON responses (each member returns a fenced JSON block per the council_member prompt).
55263
+
55264
+ #### Synthesis and Deliberation (when council.general.deliberate is true; default true)
55265
+ 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\`.
55266
+ 6. If \`disagreementsCount > 0\`:
55267
+ a. For each disagreement in the tool's response, identify the disputing members (the members listed in the disagreement's positions).
55268
+ 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.
55269
+ c. Collect the Round 2 responses.
55270
+ d. Call \`convene_general_council\` AGAIN with both \`round1Responses\` AND \`round2Responses\` populated.
55271
+
55272
+ #### Moderator Pass (when council.general.moderator is true; default true)
55273
+ 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.
55274
+
55275
+ #### Output
55276
+ 8. Present the final answer to the user:
55277
+ - If the moderator pass ran: present the moderator's output verbatim, prefaced with the participating models (one line).
55278
+ - If no moderator: present the structural \`synthesis\` markdown from the tool's return.
55279
+ In either case, do NOT present the raw per-member JSON. Do NOT silently pick a winner among persisting disagreements \u2014 surface them honestly.
55280
+
55037
55281
  ### MODE: PLAN
55038
55282
 
55039
55283
  SPEC GATE (soft \u2014 check before planning):
@@ -55112,7 +55356,18 @@ save_plan({
55112
55356
  **POST-SAVE_PLAN: APPLY QA GATE SELECTION.**
55113
55357
  After \`save_plan\` succeeds, read \`.swarm/context.md\`:
55114
55358
  - 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.
55359
+ - If no pending section exists: {{QA_GATE_DIALOGUE_PLAN}}
55360
+ <!-- BEHAVIORAL_GUIDANCE_START -->
55361
+ INLINE GATE SELECTION \u2014 no pending section found in context.md. You MUST ask now.
55362
+ \u2717 "I'll call set_qa_gates with defaults and move on"
55363
+ \u2192 WRONG: set_qa_gates with assumed values is a gate violation. The user must answer first.
55364
+ \u2717 "The user provided a plan \u2014 they know what gates they want"
55365
+ \u2192 WRONG: providing a plan is not the same as configuring gates. Always ask.
55366
+
55367
+ MANDATORY PAUSE: Present the gate question. Wait for the user's answer.
55368
+ Do NOT call \`set_qa_gates\` until the user has responded.
55369
+ <!-- BEHAVIORAL_GUIDANCE_END -->
55370
+ Then call \`set_qa_gates\` with the user's chosen flags.
55116
55371
  Either path must yield a persisted QA gate profile before the first task dispatches.
55117
55372
 
55118
55373
  \u26A0\uFE0F If \`save_plan\` is unavailable, delegate plan writing to {{AGENT_PREFIX}}coder:
@@ -55701,6 +55956,189 @@ META.SUMMARY CONVENTION \u2014 When reporting task completion, include:
55701
55956
 
55702
55957
  `;
55703
55958
 
55959
+ // src/agents/council-member.ts
55960
+ function createCouncilMemberAgent(model, customPrompt, customAppendPrompt) {
55961
+ let prompt = COUNCIL_MEMBER_PROMPT;
55962
+ if (customPrompt) {
55963
+ prompt = customPrompt;
55964
+ } else if (customAppendPrompt) {
55965
+ prompt = `${COUNCIL_MEMBER_PROMPT}
55966
+
55967
+ ${customAppendPrompt}`;
55968
+ }
55969
+ return {
55970
+ name: "council_member",
55971
+ 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.",
55972
+ config: {
55973
+ model,
55974
+ temperature: 0.4,
55975
+ prompt,
55976
+ tools: {
55977
+ write: false,
55978
+ edit: false,
55979
+ patch: false
55980
+ }
55981
+ }
55982
+ };
55983
+ }
55984
+ var COUNCIL_MEMBER_PROMPT = `You are Council Member {{MEMBER_ID}} ({{ROLE}}) on a multi-model General Council.
55985
+
55986
+ {{PERSONA_BLOCK}}
55987
+
55988
+ 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.
55989
+
55990
+ ================================================================
55991
+ ROUND {{ROUND}} PROTOCOL
55992
+ ================================================================
55993
+
55994
+ ROUND 1 \u2014 Independent Research and Answer
55995
+ - Issue 1\u20133 targeted web_search calls to gather evidence relevant to the question.
55996
+ - Cite EVERY factual claim with a source URL from your search results.
55997
+ - State your confidence (0.0\u20131.0) explicitly. Be honest \u2014 overconfident answers hurt the council.
55998
+ - Enumerate areas of uncertainty so the architect knows where you're guessing vs. where you're sure.
55999
+ - Do NOT coordinate with other members. You will not see their responses until Round 2.
56000
+ - Do NOT pad. Be concise. Substance over volume.
56001
+
56002
+ ROUND 2 \u2014 Targeted Deliberation (ONLY when this round is invoked for you)
56003
+ - {{DISAGREEMENT_BLOCK}}
56004
+ - Issue at most 1 additional web_search call.
56005
+ - Declare your stance explicitly using one of these keywords as the FIRST word of a paragraph:
56006
+ MAINTAIN \u2014 your Round 1 position holds; cite the new evidence supporting it
56007
+ CONCEDE \u2014 the opposing position is correct; state specifically what you got wrong
56008
+ NUANCE \u2014 both positions are partially right; state the boundary condition that distinguishes them
56009
+ - Never CONCEDE without evidence. Sycophantic capitulation degrades the council below an individual member's baseline (NSED arXiv:2601.16863).
56010
+ - Never MAINTAIN without engaging the opposing argument on its merits.
56011
+
56012
+ ================================================================
56013
+ RESPONSE FORMAT (always \u2014 both rounds)
56014
+ ================================================================
56015
+
56016
+ Reply with a single fenced JSON block. No prose outside the block.
56017
+
56018
+ \`\`\`json
56019
+ {
56020
+ "memberId": "{{MEMBER_ID}}",
56021
+ "role": "{{ROLE}}",
56022
+ "round": {{ROUND}},
56023
+ "response": "Your full answer (Round 1) or stance + reasoning (Round 2). Markdown OK inside the string.",
56024
+ "searchQueries": ["query 1", "query 2"],
56025
+ "sources": [
56026
+ { "title": "...", "url": "...", "snippet": "...", "query": "..." }
56027
+ ],
56028
+ "confidence": 0.85,
56029
+ "areasOfUncertainty": [
56030
+ "What I'm not sure about, in plain language."
56031
+ ],
56032
+ "disagreementTopics": []
56033
+ }
56034
+ \`\`\`
56035
+
56036
+ For Round 1: leave \`disagreementTopics\` as []. For Round 2: list the specific disagreement topics this response addresses.
56037
+
56038
+ ================================================================
56039
+ HARD RULES
56040
+ ================================================================
56041
+ - web_search is your ONLY tool. You cannot read or write files, run commands, or delegate.
56042
+ - Never invent sources. If a search returns nothing useful, say so in \`areasOfUncertainty\`.
56043
+ - Never echo other members' responses verbatim. Paraphrase or quote with attribution.
56044
+ - Stay within your role and persona. The architect chose you for a specific perspective.
56045
+ `;
56046
+
56047
+ // src/agents/council-moderator.ts
56048
+ function createCouncilModeratorAgent(model, customPrompt, customAppendPrompt) {
56049
+ let prompt = COUNCIL_MODERATOR_PROMPT;
56050
+ if (customPrompt) {
56051
+ prompt = customPrompt;
56052
+ } else if (customAppendPrompt) {
56053
+ prompt = `${COUNCIL_MODERATOR_PROMPT}
56054
+
56055
+ ${customAppendPrompt}`;
56056
+ }
56057
+ return {
56058
+ name: "council_moderator",
56059
+ description: "General Council moderator. Synthesizes a coherent final answer from member " + "responses; no web search (works on already-gathered content).",
56060
+ config: {
56061
+ model,
56062
+ temperature: 0.3,
56063
+ prompt,
56064
+ tools: {
56065
+ write: false,
56066
+ edit: false,
56067
+ patch: false
56068
+ }
56069
+ }
56070
+ };
56071
+ }
56072
+ var COUNCIL_MODERATOR_PROMPT = `You are the General Council Moderator.
56073
+
56074
+ You are receiving the structural synthesis from a multi-model council deliberation:
56075
+ - Question (and mode: general or spec_review)
56076
+ - All member Round 1 responses with sources
56077
+ - Detected disagreements
56078
+ - Round 2 deliberation responses (if any)
56079
+ - Confidence-weighted consensus claims
56080
+ - Persisting disagreements after deliberation
56081
+
56082
+ Your job: produce a coherent, well-structured final answer for the user.
56083
+
56084
+ ================================================================
56085
+ RULES
56086
+ ================================================================
56087
+
56088
+ 1. LEAD WITH CONSENSUS \u2014 open with the strongest consensus position. Use the
56089
+ confidence-weighted ordering (Quadratic Voting): higher-confidence claims
56090
+ from multiple members rank higher, but evidence quality outranks raw
56091
+ confidence. Never elevate a single confident voice over a well-evidenced
56092
+ contrary majority.
56093
+
56094
+ 2. ACKNOWLEDGE DISAGREEMENT HONESTLY \u2014 for each persisting disagreement, write
56095
+ "experts disagree on X because\u2026" and present the strongest version of each
56096
+ side. Do NOT pretend disagreements are resolved when they are not. Do NOT
56097
+ silently pick a winner.
56098
+
56099
+ 3. CITE THE STRONGEST SOURCES \u2014 link key claims with [title](url) format from
56100
+ the deduplicated source list. Pick the most reputable source for each claim;
56101
+ do not cite duplicates.
56102
+
56103
+ 4. BE CONCISE \u2014 the user wants an answer, not a committee report. Default
56104
+ length: a few short paragraphs plus a bulleted summary. Expand only when
56105
+ the question genuinely requires it.
56106
+
56107
+ ================================================================
56108
+ HARD CONSTRAINTS
56109
+ ================================================================
56110
+
56111
+ - You MUST NOT invent claims that are not present in the council's responses.
56112
+ - You MUST NOT add new web research. If something was missed, say so.
56113
+ - You MUST NOT favor a position based on member confidence alone \u2014 evidence
56114
+ quality is the tie-breaker.
56115
+ - You have NO tools. You write the final synthesis from the input given.
56116
+
56117
+ ================================================================
56118
+ OUTPUT FORMAT
56119
+ ================================================================
56120
+
56121
+ Plain markdown. No code fences. No JSON. Suggested structure:
56122
+
56123
+ # Answer
56124
+
56125
+ <lead consensus position with citation(s)>
56126
+
56127
+ <remaining consensus / context paragraphs as needed>
56128
+
56129
+ ## Where Experts Disagree
56130
+
56131
+ - <topic 1>: <position A> vs <position B>, with sources for each
56132
+ - <topic 2>: ...
56133
+
56134
+ ## Sources
56135
+
56136
+ - [title](url)
56137
+ - ...
56138
+
56139
+ (Omit any section that is empty.)
56140
+ `;
56141
+
55704
56142
  // src/agents/critic.ts
55705
56143
  function parseSoundingBoardResponse(raw) {
55706
56144
  if (typeof raw !== "string" || raw.trim().length === 0)
@@ -57345,6 +57783,19 @@ If you call @coder instead of @${swarmId}_coder, the call will FAIL or go to the
57345
57783
  testEngineer.name = prefixName("test_engineer");
57346
57784
  agents.push(applyOverrides(testEngineer, swarmAgents, swarmPrefix));
57347
57785
  }
57786
+ if (pluginConfig?.council?.general?.enabled === true && !isAgentDisabled("council_member", swarmAgents, swarmPrefix)) {
57787
+ const councilMemberPrompts = getPrompts("council_member");
57788
+ const councilMember = createCouncilMemberAgent(getModel("council_member"), councilMemberPrompts.prompt, councilMemberPrompts.appendPrompt);
57789
+ councilMember.name = prefixName("council_member");
57790
+ agents.push(applyOverrides(councilMember, swarmAgents, swarmPrefix));
57791
+ }
57792
+ if (pluginConfig?.council?.general?.enabled === true && pluginConfig?.council?.general?.moderator === true && !isAgentDisabled("council_moderator", swarmAgents, swarmPrefix)) {
57793
+ const moderatorPrompts = getPrompts("council_moderator");
57794
+ const moderatorModel = pluginConfig?.council?.general?.moderatorModel ?? getModel("council_moderator");
57795
+ const councilModerator = createCouncilModeratorAgent(moderatorModel, moderatorPrompts.prompt, moderatorPrompts.appendPrompt);
57796
+ councilModerator.name = prefixName("council_moderator");
57797
+ agents.push(applyOverrides(councilModerator, swarmAgents, swarmPrefix));
57798
+ }
57348
57799
  if (!isAgentDisabled("docs", swarmAgents, swarmPrefix)) {
57349
57800
  const docsPrompts = getPrompts("docs");
57350
57801
  const docs = createDocsAgent(getModel("docs"), docsPrompts.prompt, docsPrompts.appendPrompt);
@@ -62357,7 +62808,7 @@ var init_curator_drift = __esm(() => {
62357
62808
 
62358
62809
  // src/index.ts
62359
62810
  init_agents();
62360
- import * as path102 from "path";
62811
+ import * as path103 from "path";
62361
62812
 
62362
62813
  // src/background/index.ts
62363
62814
  init_event_bus();
@@ -62656,6 +63107,7 @@ init_benchmark();
62656
63107
  init_checkpoint2();
62657
63108
  init_close();
62658
63109
  init_config2();
63110
+ init_council();
62659
63111
  init_curate();
62660
63112
  init_dark_matter();
62661
63113
  init_diagnose();
@@ -74014,6 +74466,496 @@ var convene_council = createSwarmTool({
74014
74466
  }, null, 2);
74015
74467
  }
74016
74468
  });
74469
+ // src/tools/convene-general-council.ts
74470
+ init_dist();
74471
+ init_zod();
74472
+ init_loader();
74473
+ import * as fs59 from "fs";
74474
+ import * as path73 from "path";
74475
+
74476
+ // src/council/general-council-advisory.ts
74477
+ var ADVISORY_HEADER = "[general_council] (advisory; not blocking)";
74478
+ function pushGeneralCouncilAdvisory(session, result) {
74479
+ if (!session)
74480
+ return;
74481
+ const body2 = renderAdvisoryBody(result);
74482
+ if (!body2)
74483
+ return;
74484
+ session.pendingAdvisoryMessages ??= [];
74485
+ session.pendingAdvisoryMessages.push(`${ADVISORY_HEADER}
74486
+ ${body2}`);
74487
+ }
74488
+ function renderAdvisoryBody(result) {
74489
+ const parts2 = [result.synthesis];
74490
+ if (result.moderatorOutput && result.moderatorOutput.trim().length > 0) {
74491
+ parts2.push("", "### Moderator Output", result.moderatorOutput);
74492
+ }
74493
+ return parts2.join(`
74494
+ `).trim();
74495
+ }
74496
+
74497
+ // src/council/disagreement-detector.ts
74498
+ var MAX_DISAGREEMENTS = 10;
74499
+ var EXPLICIT_DISAGREEMENT_MARKERS = [
74500
+ "i disagree with",
74501
+ "i would push back on",
74502
+ "contrary to",
74503
+ "this contradicts",
74504
+ "unlike "
74505
+ ];
74506
+ var STRONG_RECOMMENDATION_MARKERS = [
74507
+ "recommend",
74508
+ "best approach",
74509
+ "should use",
74510
+ "i suggest",
74511
+ "the answer is",
74512
+ "the right choice is"
74513
+ ];
74514
+ function tokenize(text) {
74515
+ return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3);
74516
+ }
74517
+ function termOverlap(a, b) {
74518
+ const tokensA = new Set(tokenize(a));
74519
+ const tokensB = new Set(tokenize(b));
74520
+ if (tokensA.size === 0 || tokensB.size === 0)
74521
+ return 0;
74522
+ let intersection3 = 0;
74523
+ for (const t of tokensA) {
74524
+ if (tokensB.has(t))
74525
+ intersection3++;
74526
+ }
74527
+ const union3 = tokensA.size + tokensB.size - intersection3;
74528
+ return union3 === 0 ? 0 : intersection3 / union3;
74529
+ }
74530
+ function extractMarkerSentence(response, markers) {
74531
+ const lower = response.toLowerCase();
74532
+ const sentences = response.split(/(?<=[.!?])\s+/);
74533
+ for (const sentence of sentences) {
74534
+ const sentLower = sentence.toLowerCase();
74535
+ if (markers.some((m) => sentLower.includes(m))) {
74536
+ return sentence.trim();
74537
+ }
74538
+ }
74539
+ for (const marker of markers) {
74540
+ const idx = lower.indexOf(marker);
74541
+ if (idx !== -1) {
74542
+ const slice = response.slice(idx, idx + 200);
74543
+ return slice.split(/\n/)[0]?.trim() ?? slice.trim();
74544
+ }
74545
+ }
74546
+ return null;
74547
+ }
74548
+ function dedupeByTopic(disagreements) {
74549
+ const result = [];
74550
+ for (const d of disagreements) {
74551
+ const topicLower = d.topic.toLowerCase();
74552
+ const existing = result.find((r) => r.topic.toLowerCase().includes(topicLower) || topicLower.includes(r.topic.toLowerCase()));
74553
+ if (existing) {
74554
+ for (const pos of d.positions) {
74555
+ if (!existing.positions.some((p) => p.memberId === pos.memberId)) {
74556
+ existing.positions.push(pos);
74557
+ }
74558
+ }
74559
+ } else {
74560
+ result.push(d);
74561
+ }
74562
+ }
74563
+ return result;
74564
+ }
74565
+ function detectExplicitMarkers(responses) {
74566
+ const out2 = [];
74567
+ for (const member of responses) {
74568
+ const markerSentence = extractMarkerSentence(member.response, EXPLICIT_DISAGREEMENT_MARKERS);
74569
+ if (!markerSentence)
74570
+ continue;
74571
+ const position = {
74572
+ memberId: member.memberId,
74573
+ claim: markerSentence,
74574
+ evidence: member.sources[0]?.url ?? "(no source cited in marker sentence)"
74575
+ };
74576
+ out2.push({
74577
+ topic: markerSentence.slice(0, 80),
74578
+ positions: [position]
74579
+ });
74580
+ }
74581
+ return out2;
74582
+ }
74583
+ function extractRecommendation(response) {
74584
+ return extractMarkerSentence(response, STRONG_RECOMMENDATION_MARKERS);
74585
+ }
74586
+ function detectClaimDivergence(responses) {
74587
+ const recommendations = [];
74588
+ for (const member of responses) {
74589
+ const rec = extractRecommendation(member.response);
74590
+ if (!rec)
74591
+ continue;
74592
+ recommendations.push({
74593
+ memberId: member.memberId,
74594
+ text: rec,
74595
+ evidence: member.sources[0]?.url ?? "(no source cited)"
74596
+ });
74597
+ }
74598
+ const out2 = [];
74599
+ for (let i2 = 0;i2 < recommendations.length; i2++) {
74600
+ for (let j = i2 + 1;j < recommendations.length; j++) {
74601
+ const a = recommendations[i2];
74602
+ const b = recommendations[j];
74603
+ if (!a || !b)
74604
+ continue;
74605
+ const topicOverlap = termOverlap(a.text, b.text);
74606
+ if (topicOverlap > 0.4)
74607
+ continue;
74608
+ if (topicOverlap > 0 && topicOverlap < 0.3) {
74609
+ const topic = `${a.text.slice(0, 50)} vs ${b.text.slice(0, 50)}`;
74610
+ out2.push({
74611
+ topic,
74612
+ positions: [
74613
+ { memberId: a.memberId, claim: a.text, evidence: a.evidence },
74614
+ { memberId: b.memberId, claim: b.text, evidence: b.evidence }
74615
+ ]
74616
+ });
74617
+ }
74618
+ }
74619
+ }
74620
+ return out2;
74621
+ }
74622
+ function detectDisagreements(responses) {
74623
+ if (!Array.isArray(responses) || responses.length < 2)
74624
+ return [];
74625
+ const safeResponses = responses.filter((r) => typeof r?.memberId === "string" && typeof r?.response === "string");
74626
+ const explicit = detectExplicitMarkers(safeResponses);
74627
+ const divergent = detectClaimDivergence(safeResponses);
74628
+ const combined = [...explicit, ...divergent];
74629
+ const deduped = dedupeByTopic(combined);
74630
+ return deduped.slice(0, MAX_DISAGREEMENTS);
74631
+ }
74632
+
74633
+ // src/council/general-council-service.ts
74634
+ var CONSENSUS_WEIGHT_THRESHOLD = 0.6;
74635
+ function tokenize2(text) {
74636
+ return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 4);
74637
+ }
74638
+ function similarity(a, b) {
74639
+ const tokensA = new Set(tokenize2(a));
74640
+ const tokensB = new Set(tokenize2(b));
74641
+ if (tokensA.size === 0 || tokensB.size === 0)
74642
+ return 0;
74643
+ let intersection3 = 0;
74644
+ for (const t of tokensA)
74645
+ if (tokensB.has(t))
74646
+ intersection3++;
74647
+ const union3 = tokensA.size + tokensB.size - intersection3;
74648
+ return union3 === 0 ? 0 : intersection3 / union3;
74649
+ }
74650
+ function extractClaims(response) {
74651
+ return response.split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter((s) => s.length >= 30 && s.length <= 400);
74652
+ }
74653
+ function buildConsensusClusters(responses) {
74654
+ if (responses.length < 2)
74655
+ return [];
74656
+ const totalMembers = responses.length;
74657
+ const clusters = [];
74658
+ for (const member of responses) {
74659
+ const confidence = clamp01(member.confidence ?? 0.5);
74660
+ const claims = extractClaims(member.response ?? "");
74661
+ for (const claim of claims) {
74662
+ let assigned = false;
74663
+ for (const cluster of clusters) {
74664
+ if (similarity(cluster.representative, claim) >= 0.5) {
74665
+ if (!cluster.memberIds.has(member.memberId)) {
74666
+ cluster.weightedAgreement += confidence;
74667
+ cluster.memberIds.add(member.memberId);
74668
+ }
74669
+ if (claim.length > cluster.representative.length) {
74670
+ cluster.representative = claim;
74671
+ }
74672
+ assigned = true;
74673
+ break;
74674
+ }
74675
+ }
74676
+ if (!assigned) {
74677
+ clusters.push({
74678
+ representative: claim,
74679
+ weightedAgreement: confidence,
74680
+ memberIds: new Set([member.memberId])
74681
+ });
74682
+ }
74683
+ }
74684
+ }
74685
+ 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);
74686
+ }
74687
+ function clamp01(n) {
74688
+ if (typeof n !== "number" || Number.isNaN(n))
74689
+ return 0;
74690
+ if (n < 0)
74691
+ return 0;
74692
+ if (n > 1)
74693
+ return 1;
74694
+ return n;
74695
+ }
74696
+ function computePersistingDisagreements(disagreements, round2) {
74697
+ if (disagreements.length === 0)
74698
+ return [];
74699
+ if (round2.length === 0)
74700
+ return disagreements;
74701
+ return disagreements.filter((d) => {
74702
+ const disputants = new Set(d.positions.map((p) => p.memberId));
74703
+ const conceded = round2.some((r) => {
74704
+ if (!disputants.has(r.memberId))
74705
+ return false;
74706
+ if (!r.disagreementTopics?.includes(d.topic))
74707
+ return false;
74708
+ return /\bconcede\b/i.test(r.response ?? "");
74709
+ });
74710
+ return !conceded;
74711
+ });
74712
+ }
74713
+ function dedupeSources(round1, round2) {
74714
+ const seen = new Set;
74715
+ const out2 = [];
74716
+ const allSources = [...round1, ...round2].flatMap((r) => r.sources ?? []);
74717
+ for (const src of allSources) {
74718
+ if (!src?.url)
74719
+ continue;
74720
+ if (seen.has(src.url))
74721
+ continue;
74722
+ seen.add(src.url);
74723
+ out2.push(src);
74724
+ }
74725
+ return out2;
74726
+ }
74727
+ function renderSynthesisMarkdown(question, mode, roundsCompleted, members, consensusPoints, persistingDisagreements, allSources) {
74728
+ const memberLines = members.map((m) => `- ${m.memberId} (${m.model}, ${m.role})`).join(`
74729
+ `);
74730
+ const consensusBlock = consensusPoints.length > 0 ? consensusPoints.map((c) => `- ${c}`).join(`
74731
+ `) : "_No consensus claims reached the weighted-agreement threshold._";
74732
+ const disagreementsBlock = persistingDisagreements.length > 0 ? persistingDisagreements.map((d) => `- **${d.topic}**
74733
+ ` + d.positions.map((p) => ` - ${p.memberId}: ${p.claim}`).join(`
74734
+ `)).join(`
74735
+ `) : "_No persisting disagreements after deliberation._";
74736
+ const sourcesBlock = allSources.length > 0 ? allSources.map((s) => `- [${s.title || s.url}](${s.url})`).join(`
74737
+ `) : "_No sources cited._";
74738
+ return [
74739
+ "## General Council Synthesis",
74740
+ "",
74741
+ `**Question:** ${question}`,
74742
+ `**Mode:** ${mode}`,
74743
+ `**Members:**
74744
+ ${memberLines}`,
74745
+ `**Rounds:** ${roundsCompleted}`,
74746
+ "",
74747
+ "### Consensus",
74748
+ consensusBlock,
74749
+ "",
74750
+ "### Persistent Disagreements",
74751
+ disagreementsBlock,
74752
+ "",
74753
+ "### Sources",
74754
+ sourcesBlock
74755
+ ].join(`
74756
+ `);
74757
+ }
74758
+ function synthesizeGeneralCouncil(question, mode, round1Responses, round2Responses) {
74759
+ const safeRound1 = Array.isArray(round1Responses) ? round1Responses : [];
74760
+ const safeRound2 = Array.isArray(round2Responses) ? round2Responses : [];
74761
+ const disagreements = detectDisagreements(safeRound1);
74762
+ const consensusPoints = buildConsensusClusters(safeRound1);
74763
+ const persistingDisagreements = computePersistingDisagreements(disagreements, safeRound2);
74764
+ const allSources = dedupeSources(safeRound1, safeRound2);
74765
+ const roundsCompleted = safeRound2.length > 0 ? 2 : 1;
74766
+ const synthesis = renderSynthesisMarkdown(question, mode, roundsCompleted, safeRound1, consensusPoints, persistingDisagreements, allSources);
74767
+ return {
74768
+ question,
74769
+ mode,
74770
+ round1Responses: safeRound1,
74771
+ disagreements,
74772
+ round2Responses: safeRound2,
74773
+ synthesis,
74774
+ consensusPoints,
74775
+ persistingDisagreements: persistingDisagreements.map((d) => d.topic),
74776
+ allSources,
74777
+ timestamp: new Date().toISOString()
74778
+ };
74779
+ }
74780
+
74781
+ // src/tools/convene-general-council.ts
74782
+ init_state();
74783
+ init_create_tool();
74784
+ init_resolve_working_directory();
74785
+ var WebSearchResultSchema = exports_external.object({
74786
+ title: exports_external.string(),
74787
+ url: exports_external.string(),
74788
+ snippet: exports_external.string(),
74789
+ query: exports_external.string()
74790
+ });
74791
+ var MemberRoleEnum = exports_external.enum([
74792
+ "generalist",
74793
+ "skeptic",
74794
+ "domain_expert",
74795
+ "devil_advocate",
74796
+ "synthesizer"
74797
+ ]);
74798
+ var Round1ResponseSchema = exports_external.object({
74799
+ memberId: exports_external.string().min(1),
74800
+ model: exports_external.string().min(1),
74801
+ role: MemberRoleEnum,
74802
+ response: exports_external.string(),
74803
+ sources: exports_external.array(WebSearchResultSchema).default([]),
74804
+ searchQueries: exports_external.array(exports_external.string()).default([]),
74805
+ confidence: exports_external.number().min(0).max(1),
74806
+ areasOfUncertainty: exports_external.array(exports_external.string()).default([]),
74807
+ durationMs: exports_external.number().nonnegative().default(0)
74808
+ });
74809
+ var Round2ResponseSchema = Round1ResponseSchema.extend({
74810
+ disagreementTopics: exports_external.array(exports_external.string()).default([])
74811
+ });
74812
+ var ArgsSchema2 = exports_external.object({
74813
+ question: exports_external.string().min(1).max(8000),
74814
+ mode: exports_external.enum(["general", "spec_review"]).default("general"),
74815
+ members: exports_external.array(exports_external.string()).default([]),
74816
+ round1Responses: exports_external.array(Round1ResponseSchema).min(1),
74817
+ round2Responses: exports_external.array(Round2ResponseSchema).optional(),
74818
+ working_directory: exports_external.string().optional()
74819
+ });
74820
+ function buildModeratorPrompt(question, synthesis) {
74821
+ return [
74822
+ "A multi-model council has deliberated on the following question. Your job is to synthesize",
74823
+ "the council output into a single coherent answer for the user, following the rules in your",
74824
+ "system prompt (lead with consensus, acknowledge persisting disagreement honestly, cite the",
74825
+ "strongest sources, be concise, do not invent claims, do not run new searches).",
74826
+ "",
74827
+ `QUESTION:
74828
+ ${question}`,
74829
+ "",
74830
+ "COUNCIL OUTPUT:",
74831
+ synthesis
74832
+ ].join(`
74833
+ `);
74834
+ }
74835
+ var convene_general_council = createSwarmTool({
74836
+ 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.",
74837
+ args: {
74838
+ question: tool.schema.string().min(1).max(8000).describe("The question put to the council, or the spec text to review."),
74839
+ mode: tool.schema.enum(["general", "spec_review"]).optional().describe('"general" for /swarm council; "spec_review" for SPECIFY-COUNCIL-REVIEW gate.'),
74840
+ members: tool.schema.array(tool.schema.string()).optional().describe("Optional list of member IDs convened (for evidence/audit)."),
74841
+ round1Responses: tool.schema.array(tool.schema.object({
74842
+ memberId: tool.schema.string().min(1),
74843
+ model: tool.schema.string().min(1),
74844
+ role: tool.schema.enum([
74845
+ "generalist",
74846
+ "skeptic",
74847
+ "domain_expert",
74848
+ "devil_advocate",
74849
+ "synthesizer"
74850
+ ]),
74851
+ response: tool.schema.string(),
74852
+ sources: tool.schema.array(tool.schema.object({
74853
+ title: tool.schema.string(),
74854
+ url: tool.schema.string(),
74855
+ snippet: tool.schema.string(),
74856
+ query: tool.schema.string()
74857
+ })).optional(),
74858
+ searchQueries: tool.schema.array(tool.schema.string()).optional(),
74859
+ confidence: tool.schema.number().min(0).max(1),
74860
+ areasOfUncertainty: tool.schema.array(tool.schema.string()).optional(),
74861
+ durationMs: tool.schema.number().nonnegative().optional()
74862
+ })).describe("Round 1 member responses (one per council member)."),
74863
+ round2Responses: tool.schema.array(tool.schema.object({
74864
+ memberId: tool.schema.string().min(1),
74865
+ model: tool.schema.string().min(1),
74866
+ role: tool.schema.enum([
74867
+ "generalist",
74868
+ "skeptic",
74869
+ "domain_expert",
74870
+ "devil_advocate",
74871
+ "synthesizer"
74872
+ ]),
74873
+ response: tool.schema.string(),
74874
+ sources: tool.schema.array(tool.schema.object({
74875
+ title: tool.schema.string(),
74876
+ url: tool.schema.string(),
74877
+ snippet: tool.schema.string(),
74878
+ query: tool.schema.string()
74879
+ })).optional(),
74880
+ searchQueries: tool.schema.array(tool.schema.string()).optional(),
74881
+ confidence: tool.schema.number().min(0).max(1),
74882
+ areasOfUncertainty: tool.schema.array(tool.schema.string()).optional(),
74883
+ durationMs: tool.schema.number().nonnegative().optional(),
74884
+ disagreementTopics: tool.schema.array(tool.schema.string()).optional()
74885
+ })).optional().describe("Round 2 deliberation responses (omit when no deliberation has occurred)."),
74886
+ working_directory: tool.schema.string().optional().describe("Project root for config + evidence path resolution.")
74887
+ },
74888
+ execute: async (args2, directory, ctx) => {
74889
+ const parsed = ArgsSchema2.safeParse(args2);
74890
+ if (!parsed.success) {
74891
+ const fail = {
74892
+ success: false,
74893
+ reason: "invalid_args",
74894
+ message: parsed.error.issues.map((i2) => `${i2.path.join(".")}: ${i2.message}`).join("; ")
74895
+ };
74896
+ return JSON.stringify(fail, null, 2);
74897
+ }
74898
+ const input = parsed.data;
74899
+ const dirResult = resolveWorkingDirectory(input.working_directory, directory);
74900
+ if (!dirResult.success) {
74901
+ const fail = {
74902
+ success: false,
74903
+ reason: "invalid_working_directory",
74904
+ message: dirResult.message
74905
+ };
74906
+ return JSON.stringify(fail, null, 2);
74907
+ }
74908
+ const workingDir = dirResult.directory;
74909
+ const config3 = loadPluginConfig(workingDir);
74910
+ const generalConfig = config3.council?.general;
74911
+ if (!generalConfig || generalConfig.enabled !== true) {
74912
+ const fail = {
74913
+ success: false,
74914
+ reason: "council_general_disabled",
74915
+ message: "convene_general_council requires council.general.enabled: true in opencode-swarm.json."
74916
+ };
74917
+ return JSON.stringify(fail, null, 2);
74918
+ }
74919
+ const round1 = input.round1Responses;
74920
+ const round2 = input.round2Responses ?? [];
74921
+ const result = synthesizeGeneralCouncil(input.question, input.mode, round1, round2);
74922
+ const evidenceDir = path73.join(workingDir, ".swarm", "council", "general");
74923
+ const safeTimestamp = result.timestamp.replace(/[:.]/g, "-");
74924
+ const evidenceFile = `${safeTimestamp}-${input.mode}.json`;
74925
+ const evidencePath = path73.join(evidenceDir, evidenceFile);
74926
+ try {
74927
+ await fs59.promises.mkdir(evidenceDir, { recursive: true });
74928
+ await fs59.promises.writeFile(evidencePath, JSON.stringify(result, null, 2));
74929
+ } catch (err2) {
74930
+ const message = err2 instanceof Error ? err2.message : String(err2);
74931
+ console.warn(`[convene_general_council] Failed to write evidence to ${evidencePath}: ${message}`);
74932
+ }
74933
+ try {
74934
+ const sessionID = ctx?.sessionID;
74935
+ if (sessionID) {
74936
+ const session = getAgentSession(sessionID);
74937
+ if (session) {
74938
+ pushGeneralCouncilAdvisory(session, result);
74939
+ }
74940
+ }
74941
+ } catch {}
74942
+ const moderatorPrompt = generalConfig.moderator === true ? buildModeratorPrompt(input.question, result.synthesis) : undefined;
74943
+ const ok = {
74944
+ success: true,
74945
+ question: input.question,
74946
+ mode: input.mode,
74947
+ roundsCompleted: round2.length > 0 ? 2 : 1,
74948
+ consensusPoints: result.consensusPoints,
74949
+ disagreementsCount: result.disagreements.length,
74950
+ persistingDisagreements: result.persistingDisagreements,
74951
+ allSourcesCount: result.allSources.length,
74952
+ synthesis: result.synthesis,
74953
+ ...moderatorPrompt !== undefined && { moderatorPrompt },
74954
+ evidencePath
74955
+ };
74956
+ return JSON.stringify(ok, null, 2);
74957
+ }
74958
+ });
74017
74959
  // src/tools/curator-analyze.ts
74018
74960
  init_dist();
74019
74961
  init_config();
@@ -74143,7 +75085,7 @@ var CriteriaItemSchema = exports_external.object({
74143
75085
  description: exports_external.string().min(10).max(500),
74144
75086
  mandatory: exports_external.boolean()
74145
75087
  });
74146
- var ArgsSchema2 = exports_external.object({
75088
+ var ArgsSchema3 = exports_external.object({
74147
75089
  taskId: exports_external.string().min(1).regex(/^\d+\.\d+(\.\d+)*$/, "Task ID must be in N.M or N.M.P format"),
74148
75090
  criteria: exports_external.array(CriteriaItemSchema).min(1).max(20),
74149
75091
  working_directory: exports_external.string().optional()
@@ -74160,7 +75102,7 @@ var declare_council_criteria = createSwarmTool({
74160
75102
  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
75103
  },
74162
75104
  async execute(args2, directory) {
74163
- const parsed = ArgsSchema2.safeParse(args2);
75105
+ const parsed = ArgsSchema3.safeParse(args2);
74164
75106
  if (!parsed.success) {
74165
75107
  return JSON.stringify({
74166
75108
  success: false,
@@ -74221,8 +75163,8 @@ init_scope_persistence();
74221
75163
  init_state();
74222
75164
  init_task_id();
74223
75165
  init_create_tool();
74224
- import * as fs59 from "fs";
74225
- import * as path73 from "path";
75166
+ import * as fs60 from "fs";
75167
+ import * as path74 from "path";
74226
75168
  function validateTaskIdFormat2(taskId) {
74227
75169
  return validateTaskIdFormat(taskId);
74228
75170
  }
@@ -74296,8 +75238,8 @@ async function executeDeclareScope(args2, fallbackDir) {
74296
75238
  };
74297
75239
  }
74298
75240
  }
74299
- normalizedDir = path73.normalize(args2.working_directory);
74300
- const pathParts = normalizedDir.split(path73.sep);
75241
+ normalizedDir = path74.normalize(args2.working_directory);
75242
+ const pathParts = normalizedDir.split(path74.sep);
74301
75243
  if (pathParts.includes("..")) {
74302
75244
  return {
74303
75245
  success: false,
@@ -74307,11 +75249,11 @@ async function executeDeclareScope(args2, fallbackDir) {
74307
75249
  ]
74308
75250
  };
74309
75251
  }
74310
- const resolvedDir = path73.resolve(normalizedDir);
75252
+ const resolvedDir = path74.resolve(normalizedDir);
74311
75253
  try {
74312
- const realPath = fs59.realpathSync(resolvedDir);
74313
- const planPath2 = path73.join(realPath, ".swarm", "plan.json");
74314
- if (!fs59.existsSync(planPath2)) {
75254
+ const realPath = fs60.realpathSync(resolvedDir);
75255
+ const planPath2 = path74.join(realPath, ".swarm", "plan.json");
75256
+ if (!fs60.existsSync(planPath2)) {
74315
75257
  return {
74316
75258
  success: false,
74317
75259
  message: `Invalid working_directory: plan not found in "${realPath}"`,
@@ -74334,8 +75276,8 @@ async function executeDeclareScope(args2, fallbackDir) {
74334
75276
  console.warn("[declare-scope] fallbackDir is undefined, falling back to process.cwd()");
74335
75277
  }
74336
75278
  const directory = normalizedDir || fallbackDir;
74337
- const planPath = path73.resolve(directory, ".swarm", "plan.json");
74338
- if (!fs59.existsSync(planPath)) {
75279
+ const planPath = path74.resolve(directory, ".swarm", "plan.json");
75280
+ if (!fs60.existsSync(planPath)) {
74339
75281
  return {
74340
75282
  success: false,
74341
75283
  message: "No plan found",
@@ -74344,7 +75286,7 @@ async function executeDeclareScope(args2, fallbackDir) {
74344
75286
  }
74345
75287
  let planContent;
74346
75288
  try {
74347
- planContent = JSON.parse(fs59.readFileSync(planPath, "utf-8"));
75289
+ planContent = JSON.parse(fs60.readFileSync(planPath, "utf-8"));
74348
75290
  } catch {
74349
75291
  return {
74350
75292
  success: false,
@@ -74374,8 +75316,8 @@ async function executeDeclareScope(args2, fallbackDir) {
74374
75316
  const normalizeErrors = [];
74375
75317
  const dir = normalizedDir || fallbackDir || process.cwd();
74376
75318
  const mergedFiles = rawMergedFiles.map((file3) => {
74377
- if (path73.isAbsolute(file3)) {
74378
- const relativePath = path73.relative(dir, file3).replace(/\\/g, "/");
75319
+ if (path74.isAbsolute(file3)) {
75320
+ const relativePath = path74.relative(dir, file3).replace(/\\/g, "/");
74379
75321
  if (relativePath.startsWith("..")) {
74380
75322
  normalizeErrors.push(`Path '${file3}' resolves outside the project directory`);
74381
75323
  return file3;
@@ -74435,8 +75377,8 @@ var declare_scope = createSwarmTool({
74435
75377
  // src/tools/diff.ts
74436
75378
  init_dist();
74437
75379
  import * as child_process7 from "child_process";
74438
- import * as fs60 from "fs";
74439
- import * as path74 from "path";
75380
+ import * as fs61 from "fs";
75381
+ import * as path75 from "path";
74440
75382
  init_create_tool();
74441
75383
  var MAX_DIFF_LINES = 500;
74442
75384
  var DIFF_TIMEOUT_MS = 30000;
@@ -74465,20 +75407,20 @@ function validateBase(base) {
74465
75407
  function validatePaths(paths) {
74466
75408
  if (!paths)
74467
75409
  return null;
74468
- for (const path75 of paths) {
74469
- if (!path75 || path75.length === 0) {
75410
+ for (const path76 of paths) {
75411
+ if (!path76 || path76.length === 0) {
74470
75412
  return "empty path not allowed";
74471
75413
  }
74472
- if (path75.length > MAX_PATH_LENGTH) {
75414
+ if (path76.length > MAX_PATH_LENGTH) {
74473
75415
  return `path exceeds maximum length of ${MAX_PATH_LENGTH}`;
74474
75416
  }
74475
- if (SHELL_METACHARACTERS2.test(path75)) {
75417
+ if (SHELL_METACHARACTERS2.test(path76)) {
74476
75418
  return "path contains shell metacharacters";
74477
75419
  }
74478
- if (path75.startsWith("-")) {
75420
+ if (path76.startsWith("-")) {
74479
75421
  return 'path cannot start with "-" (option-like arguments not allowed)';
74480
75422
  }
74481
- if (CONTROL_CHAR_PATTERN2.test(path75)) {
75423
+ if (CONTROL_CHAR_PATTERN2.test(path76)) {
74482
75424
  return "path contains control characters";
74483
75425
  }
74484
75426
  }
@@ -74584,8 +75526,8 @@ var diff = createSwarmTool({
74584
75526
  if (parts2.length >= 3) {
74585
75527
  const additions = parseInt(parts2[0], 10) || 0;
74586
75528
  const deletions = parseInt(parts2[1], 10) || 0;
74587
- const path75 = parts2[2];
74588
- files.push({ path: path75, additions, deletions });
75529
+ const path76 = parts2[2];
75530
+ files.push({ path: path76, additions, deletions });
74589
75531
  }
74590
75532
  }
74591
75533
  const contractChanges = [];
@@ -74625,7 +75567,7 @@ var diff = createSwarmTool({
74625
75567
  } else if (base === "unstaged") {
74626
75568
  const oldRef = `:${file3.path}`;
74627
75569
  oldContent = fileExistsInRef(oldRef) ? getContentFromRef(oldRef) : "";
74628
- newContent = fs60.readFileSync(path74.join(directory, file3.path), "utf-8");
75570
+ newContent = fs61.readFileSync(path75.join(directory, file3.path), "utf-8");
74629
75571
  } else {
74630
75572
  const oldRef = `${base}:${file3.path}`;
74631
75573
  oldContent = fileExistsInRef(oldRef) ? getContentFromRef(oldRef) : "";
@@ -74699,8 +75641,8 @@ var diff = createSwarmTool({
74699
75641
  // src/tools/diff-summary.ts
74700
75642
  init_dist();
74701
75643
  import * as child_process8 from "child_process";
74702
- import * as fs61 from "fs";
74703
- import * as path75 from "path";
75644
+ import * as fs62 from "fs";
75645
+ import * as path76 from "path";
74704
75646
  init_create_tool();
74705
75647
  var diff_summary = createSwarmTool({
74706
75648
  description: "Generate a filtered semantic diff summary from AST analysis. Returns SemanticDiffSummary with optional filtering by classification or riskLevel.",
@@ -74748,7 +75690,7 @@ var diff_summary = createSwarmTool({
74748
75690
  }
74749
75691
  try {
74750
75692
  let oldContent;
74751
- const newContent = fs61.readFileSync(path75.join(workingDir, filePath), "utf-8");
75693
+ const newContent = fs62.readFileSync(path76.join(workingDir, filePath), "utf-8");
74752
75694
  if (fileExistsInHead) {
74753
75695
  oldContent = child_process8.execFileSync("git", ["show", `HEAD:${filePath}`], {
74754
75696
  encoding: "utf-8",
@@ -74975,8 +75917,8 @@ Use these as DOMAIN values when delegating to @sme.`;
74975
75917
  init_dist();
74976
75918
  init_create_tool();
74977
75919
  init_path_security();
74978
- import * as fs62 from "fs";
74979
- import * as path76 from "path";
75920
+ import * as fs63 from "fs";
75921
+ import * as path77 from "path";
74980
75922
  var MAX_FILE_SIZE_BYTES6 = 1024 * 1024;
74981
75923
  var MAX_EVIDENCE_FILES = 1000;
74982
75924
  var EVIDENCE_DIR3 = ".swarm/evidence";
@@ -75003,9 +75945,9 @@ function validateRequiredTypes(input) {
75003
75945
  return null;
75004
75946
  }
75005
75947
  function isPathWithinSwarm2(filePath, cwd) {
75006
- const normalizedCwd = path76.resolve(cwd);
75007
- const swarmPath = path76.join(normalizedCwd, ".swarm");
75008
- const normalizedPath = path76.resolve(filePath);
75948
+ const normalizedCwd = path77.resolve(cwd);
75949
+ const swarmPath = path77.join(normalizedCwd, ".swarm");
75950
+ const normalizedPath = path77.resolve(filePath);
75009
75951
  return normalizedPath.startsWith(swarmPath);
75010
75952
  }
75011
75953
  function parseCompletedTasks(planContent) {
@@ -75021,12 +75963,12 @@ function parseCompletedTasks(planContent) {
75021
75963
  }
75022
75964
  function readEvidenceFiles(evidenceDir, _cwd) {
75023
75965
  const evidence = [];
75024
- if (!fs62.existsSync(evidenceDir) || !fs62.statSync(evidenceDir).isDirectory()) {
75966
+ if (!fs63.existsSync(evidenceDir) || !fs63.statSync(evidenceDir).isDirectory()) {
75025
75967
  return evidence;
75026
75968
  }
75027
75969
  let files;
75028
75970
  try {
75029
- files = fs62.readdirSync(evidenceDir);
75971
+ files = fs63.readdirSync(evidenceDir);
75030
75972
  } catch {
75031
75973
  return evidence;
75032
75974
  }
@@ -75035,14 +75977,14 @@ function readEvidenceFiles(evidenceDir, _cwd) {
75035
75977
  if (!VALID_EVIDENCE_FILENAME_REGEX.test(filename)) {
75036
75978
  continue;
75037
75979
  }
75038
- const filePath = path76.join(evidenceDir, filename);
75980
+ const filePath = path77.join(evidenceDir, filename);
75039
75981
  try {
75040
- const resolvedPath = path76.resolve(filePath);
75041
- const evidenceDirResolved = path76.resolve(evidenceDir);
75982
+ const resolvedPath = path77.resolve(filePath);
75983
+ const evidenceDirResolved = path77.resolve(evidenceDir);
75042
75984
  if (!resolvedPath.startsWith(evidenceDirResolved)) {
75043
75985
  continue;
75044
75986
  }
75045
- const stat4 = fs62.lstatSync(filePath);
75987
+ const stat4 = fs63.lstatSync(filePath);
75046
75988
  if (!stat4.isFile()) {
75047
75989
  continue;
75048
75990
  }
@@ -75051,7 +75993,7 @@ function readEvidenceFiles(evidenceDir, _cwd) {
75051
75993
  }
75052
75994
  let fileStat;
75053
75995
  try {
75054
- fileStat = fs62.statSync(filePath);
75996
+ fileStat = fs63.statSync(filePath);
75055
75997
  if (fileStat.size > MAX_FILE_SIZE_BYTES6) {
75056
75998
  continue;
75057
75999
  }
@@ -75060,7 +76002,7 @@ function readEvidenceFiles(evidenceDir, _cwd) {
75060
76002
  }
75061
76003
  let content;
75062
76004
  try {
75063
- content = fs62.readFileSync(filePath, "utf-8");
76005
+ content = fs63.readFileSync(filePath, "utf-8");
75064
76006
  } catch {
75065
76007
  continue;
75066
76008
  }
@@ -75156,7 +76098,7 @@ var evidence_check = createSwarmTool({
75156
76098
  return JSON.stringify(errorResult, null, 2);
75157
76099
  }
75158
76100
  const requiredTypes = requiredTypesValue.split(",").map((t) => t.trim()).filter((t) => t.length > 0).map(normalizeEvidenceType);
75159
- const planPath = path76.join(cwd, PLAN_FILE);
76101
+ const planPath = path77.join(cwd, PLAN_FILE);
75160
76102
  if (!isPathWithinSwarm2(planPath, cwd)) {
75161
76103
  const errorResult = {
75162
76104
  error: "plan file path validation failed",
@@ -75170,7 +76112,7 @@ var evidence_check = createSwarmTool({
75170
76112
  }
75171
76113
  let planContent;
75172
76114
  try {
75173
- planContent = fs62.readFileSync(planPath, "utf-8");
76115
+ planContent = fs63.readFileSync(planPath, "utf-8");
75174
76116
  } catch {
75175
76117
  const result2 = {
75176
76118
  message: "No completed tasks found in plan.",
@@ -75188,7 +76130,7 @@ var evidence_check = createSwarmTool({
75188
76130
  };
75189
76131
  return JSON.stringify(result2, null, 2);
75190
76132
  }
75191
- const evidenceDir = path76.join(cwd, EVIDENCE_DIR3);
76133
+ const evidenceDir = path77.join(cwd, EVIDENCE_DIR3);
75192
76134
  const evidence = readEvidenceFiles(evidenceDir, cwd);
75193
76135
  const { tasksWithFullEvidence, gaps } = analyzeGaps(completedTasks, evidence, requiredTypes);
75194
76136
  const completeness = completedTasks.length > 0 ? Math.round(tasksWithFullEvidence.length / completedTasks.length * 100) / 100 : 1;
@@ -75205,8 +76147,8 @@ var evidence_check = createSwarmTool({
75205
76147
  // src/tools/file-extractor.ts
75206
76148
  init_tool();
75207
76149
  init_create_tool();
75208
- import * as fs63 from "fs";
75209
- import * as path77 from "path";
76150
+ import * as fs64 from "fs";
76151
+ import * as path78 from "path";
75210
76152
  var EXT_MAP = {
75211
76153
  python: ".py",
75212
76154
  py: ".py",
@@ -75268,8 +76210,8 @@ var extract_code_blocks = createSwarmTool({
75268
76210
  execute: async (args2, directory) => {
75269
76211
  const { content, output_dir, prefix } = args2;
75270
76212
  const targetDir = output_dir || directory;
75271
- if (!fs63.existsSync(targetDir)) {
75272
- fs63.mkdirSync(targetDir, { recursive: true });
76213
+ if (!fs64.existsSync(targetDir)) {
76214
+ fs64.mkdirSync(targetDir, { recursive: true });
75273
76215
  }
75274
76216
  if (!content) {
75275
76217
  return "Error: content is required";
@@ -75287,16 +76229,16 @@ var extract_code_blocks = createSwarmTool({
75287
76229
  if (prefix) {
75288
76230
  filename = `${prefix}_${filename}`;
75289
76231
  }
75290
- let filepath = path77.join(targetDir, filename);
75291
- const base = path77.basename(filepath, path77.extname(filepath));
75292
- const ext = path77.extname(filepath);
76232
+ let filepath = path78.join(targetDir, filename);
76233
+ const base = path78.basename(filepath, path78.extname(filepath));
76234
+ const ext = path78.extname(filepath);
75293
76235
  let counter = 1;
75294
- while (fs63.existsSync(filepath)) {
75295
- filepath = path77.join(targetDir, `${base}_${counter}${ext}`);
76236
+ while (fs64.existsSync(filepath)) {
76237
+ filepath = path78.join(targetDir, `${base}_${counter}${ext}`);
75296
76238
  counter++;
75297
76239
  }
75298
76240
  try {
75299
- fs63.writeFileSync(filepath, code.trim(), "utf-8");
76241
+ fs64.writeFileSync(filepath, code.trim(), "utf-8");
75300
76242
  savedFiles.push(filepath);
75301
76243
  } catch (error93) {
75302
76244
  errors5.push(`Failed to save ${filename}: ${error93 instanceof Error ? error93.message : String(error93)}`);
@@ -75555,8 +76497,8 @@ var gitingest = createSwarmTool({
75555
76497
  init_dist();
75556
76498
  init_create_tool();
75557
76499
  init_path_security();
75558
- import * as fs64 from "fs";
75559
- import * as path78 from "path";
76500
+ import * as fs65 from "fs";
76501
+ import * as path79 from "path";
75560
76502
  var MAX_FILE_PATH_LENGTH2 = 500;
75561
76503
  var MAX_SYMBOL_LENGTH = 256;
75562
76504
  var MAX_FILE_SIZE_BYTES7 = 1024 * 1024;
@@ -75604,7 +76546,7 @@ function validateSymbolInput(symbol3) {
75604
76546
  return null;
75605
76547
  }
75606
76548
  function isBinaryFile2(filePath, buffer) {
75607
- const ext = path78.extname(filePath).toLowerCase();
76549
+ const ext = path79.extname(filePath).toLowerCase();
75608
76550
  if (ext === ".json" || ext === ".md" || ext === ".txt") {
75609
76551
  return false;
75610
76552
  }
@@ -75628,15 +76570,15 @@ function parseImports(content, targetFile, targetSymbol) {
75628
76570
  const imports = [];
75629
76571
  let _resolvedTarget;
75630
76572
  try {
75631
- _resolvedTarget = path78.resolve(targetFile);
76573
+ _resolvedTarget = path79.resolve(targetFile);
75632
76574
  } catch {
75633
76575
  _resolvedTarget = targetFile;
75634
76576
  }
75635
- const targetBasename = path78.basename(targetFile, path78.extname(targetFile));
76577
+ const targetBasename = path79.basename(targetFile, path79.extname(targetFile));
75636
76578
  const targetWithExt = targetFile;
75637
76579
  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, "/");
76580
+ const normalizedTargetWithExt = path79.normalize(targetWithExt).replace(/\\/g, "/");
76581
+ const normalizedTargetWithoutExt = path79.normalize(targetWithoutExt).replace(/\\/g, "/");
75640
76582
  const importRegex = /import\s+(?:\{[\s\S]*?\}|(?:\*\s+as\s+\w+)|\w+)\s+from\s+['"`]([^'"`]+)['"`]|import\s+['"`]([^'"`]+)['"`]|require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
75641
76583
  for (let match = importRegex.exec(content);match !== null; match = importRegex.exec(content)) {
75642
76584
  const modulePath = match[1] || match[2] || match[3];
@@ -75659,9 +76601,9 @@ function parseImports(content, targetFile, targetSymbol) {
75659
76601
  }
75660
76602
  const _normalizedModule = modulePath.replace(/^\.\//, "").replace(/^\.\.\\/, "../");
75661
76603
  let isMatch = false;
75662
- const _targetDir = path78.dirname(targetFile);
75663
- const targetExt = path78.extname(targetFile);
75664
- const targetBasenameNoExt = path78.basename(targetFile, targetExt);
76604
+ const _targetDir = path79.dirname(targetFile);
76605
+ const targetExt = path79.extname(targetFile);
76606
+ const targetBasenameNoExt = path79.basename(targetFile, targetExt);
75665
76607
  const moduleNormalized = modulePath.replace(/\\/g, "/").replace(/^\.\//, "");
75666
76608
  const moduleName = modulePath.split(/[/\\]/).pop() || "";
75667
76609
  const moduleNameNoExt = moduleName.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/i, "");
@@ -75718,7 +76660,7 @@ var SKIP_DIRECTORIES4 = new Set([
75718
76660
  function findSourceFiles3(dir, files = [], stats = { skippedDirs: [], skippedFiles: 0, fileErrors: [] }) {
75719
76661
  let entries;
75720
76662
  try {
75721
- entries = fs64.readdirSync(dir);
76663
+ entries = fs65.readdirSync(dir);
75722
76664
  } catch (e) {
75723
76665
  stats.fileErrors.push({
75724
76666
  path: dir,
@@ -75729,13 +76671,13 @@ function findSourceFiles3(dir, files = [], stats = { skippedDirs: [], skippedFil
75729
76671
  entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
75730
76672
  for (const entry of entries) {
75731
76673
  if (SKIP_DIRECTORIES4.has(entry)) {
75732
- stats.skippedDirs.push(path78.join(dir, entry));
76674
+ stats.skippedDirs.push(path79.join(dir, entry));
75733
76675
  continue;
75734
76676
  }
75735
- const fullPath = path78.join(dir, entry);
76677
+ const fullPath = path79.join(dir, entry);
75736
76678
  let stat4;
75737
76679
  try {
75738
- stat4 = fs64.statSync(fullPath);
76680
+ stat4 = fs65.statSync(fullPath);
75739
76681
  } catch (e) {
75740
76682
  stats.fileErrors.push({
75741
76683
  path: fullPath,
@@ -75746,7 +76688,7 @@ function findSourceFiles3(dir, files = [], stats = { skippedDirs: [], skippedFil
75746
76688
  if (stat4.isDirectory()) {
75747
76689
  findSourceFiles3(fullPath, files, stats);
75748
76690
  } else if (stat4.isFile()) {
75749
- const ext = path78.extname(fullPath).toLowerCase();
76691
+ const ext = path79.extname(fullPath).toLowerCase();
75750
76692
  if (SUPPORTED_EXTENSIONS3.includes(ext)) {
75751
76693
  files.push(fullPath);
75752
76694
  }
@@ -75803,8 +76745,8 @@ var imports = createSwarmTool({
75803
76745
  return JSON.stringify(errorResult, null, 2);
75804
76746
  }
75805
76747
  try {
75806
- const targetFile = path78.resolve(file3);
75807
- if (!fs64.existsSync(targetFile)) {
76748
+ const targetFile = path79.resolve(file3);
76749
+ if (!fs65.existsSync(targetFile)) {
75808
76750
  const errorResult = {
75809
76751
  error: `target file not found: ${file3}`,
75810
76752
  target: file3,
@@ -75814,7 +76756,7 @@ var imports = createSwarmTool({
75814
76756
  };
75815
76757
  return JSON.stringify(errorResult, null, 2);
75816
76758
  }
75817
- const targetStat = fs64.statSync(targetFile);
76759
+ const targetStat = fs65.statSync(targetFile);
75818
76760
  if (!targetStat.isFile()) {
75819
76761
  const errorResult = {
75820
76762
  error: "target must be a file, not a directory",
@@ -75825,7 +76767,7 @@ var imports = createSwarmTool({
75825
76767
  };
75826
76768
  return JSON.stringify(errorResult, null, 2);
75827
76769
  }
75828
- const baseDir = path78.dirname(targetFile);
76770
+ const baseDir = path79.dirname(targetFile);
75829
76771
  const scanStats = {
75830
76772
  skippedDirs: [],
75831
76773
  skippedFiles: 0,
@@ -75840,12 +76782,12 @@ var imports = createSwarmTool({
75840
76782
  if (consumers.length >= MAX_CONSUMERS)
75841
76783
  break;
75842
76784
  try {
75843
- const stat4 = fs64.statSync(filePath);
76785
+ const stat4 = fs65.statSync(filePath);
75844
76786
  if (stat4.size > MAX_FILE_SIZE_BYTES7) {
75845
76787
  skippedFileCount++;
75846
76788
  continue;
75847
76789
  }
75848
- const buffer = fs64.readFileSync(filePath);
76790
+ const buffer = fs65.readFileSync(filePath);
75849
76791
  if (isBinaryFile2(filePath, buffer)) {
75850
76792
  skippedFileCount++;
75851
76793
  continue;
@@ -76357,8 +77299,8 @@ init_schema();
76357
77299
  init_qa_gate_profile();
76358
77300
  init_manager2();
76359
77301
  init_curator();
76360
- import * as fs65 from "fs";
76361
- import * as path79 from "path";
77302
+ import * as fs66 from "fs";
77303
+ import * as path80 from "path";
76362
77304
  init_knowledge_curator();
76363
77305
  init_knowledge_reader();
76364
77306
  init_knowledge_store();
@@ -76589,11 +77531,11 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76589
77531
  safeWarn(`[phase_complete] Completion verify error (non-blocking):`, completionError);
76590
77532
  }
76591
77533
  try {
76592
- const driftEvidencePath = path79.join(dir, ".swarm", "evidence", String(phase), "drift-verifier.json");
77534
+ const driftEvidencePath = path80.join(dir, ".swarm", "evidence", String(phase), "drift-verifier.json");
76593
77535
  let driftVerdictFound = false;
76594
77536
  let driftVerdictApproved = false;
76595
77537
  try {
76596
- const driftEvidenceContent = fs65.readFileSync(driftEvidencePath, "utf-8");
77538
+ const driftEvidenceContent = fs66.readFileSync(driftEvidencePath, "utf-8");
76597
77539
  const driftEvidence = JSON.parse(driftEvidenceContent);
76598
77540
  const entries = driftEvidence.entries ?? [];
76599
77541
  for (const entry of entries) {
@@ -76623,14 +77565,14 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76623
77565
  driftVerdictFound = false;
76624
77566
  }
76625
77567
  if (!driftVerdictFound) {
76626
- const specPath = path79.join(dir, ".swarm", "spec.md");
76627
- const specExists = fs65.existsSync(specPath);
77568
+ const specPath = path80.join(dir, ".swarm", "spec.md");
77569
+ const specExists = fs66.existsSync(specPath);
76628
77570
  if (!specExists) {
76629
77571
  let incompleteTaskCount = 0;
76630
77572
  let planPhaseFound = false;
76631
77573
  try {
76632
77574
  const planPath = validateSwarmPath(dir, "plan.json");
76633
- const planRaw = fs65.readFileSync(planPath, "utf-8");
77575
+ const planRaw = fs66.readFileSync(planPath, "utf-8");
76634
77576
  const plan = JSON.parse(planRaw);
76635
77577
  const targetPhase = plan.phases.find((p) => p.id === phase);
76636
77578
  if (targetPhase) {
@@ -76681,11 +77623,11 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76681
77623
  const overrides = session2?.qaGateSessionOverrides ?? {};
76682
77624
  const effective = getEffectiveGates(profile, overrides);
76683
77625
  if (effective.hallucination_guard === true) {
76684
- const hgPath = path79.join(dir, ".swarm", "evidence", String(phase), "hallucination-guard.json");
77626
+ const hgPath = path80.join(dir, ".swarm", "evidence", String(phase), "hallucination-guard.json");
76685
77627
  let hgVerdictFound = false;
76686
77628
  let hgVerdictApproved = false;
76687
77629
  try {
76688
- const hgContent = fs65.readFileSync(hgPath, "utf-8");
77630
+ const hgContent = fs66.readFileSync(hgPath, "utf-8");
76689
77631
  const hgBundle = JSON.parse(hgContent);
76690
77632
  for (const entry of hgBundle.entries ?? []) {
76691
77633
  if (typeof entry.type === "string" && entry.type.includes("hallucination") && typeof entry.verdict === "string") {
@@ -76753,11 +77695,11 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76753
77695
  const overrides = session2?.qaGateSessionOverrides ?? {};
76754
77696
  const effective = getEffectiveGates(profile, overrides);
76755
77697
  if (effective.mutation_test === true) {
76756
- const mgPath = path79.join(dir, ".swarm", "evidence", String(phase), "mutation-gate.json");
77698
+ const mgPath = path80.join(dir, ".swarm", "evidence", String(phase), "mutation-gate.json");
76757
77699
  let mgVerdictFound = false;
76758
77700
  let mgVerdict;
76759
77701
  try {
76760
- const mgContent = fs65.readFileSync(mgPath, "utf-8");
77702
+ const mgContent = fs66.readFileSync(mgPath, "utf-8");
76761
77703
  const mgBundle = JSON.parse(mgContent);
76762
77704
  for (const entry of mgBundle.entries ?? []) {
76763
77705
  if (typeof entry.type === "string" && entry.type === "mutation-gate" && typeof entry.verdict === "string") {
@@ -76825,7 +77767,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76825
77767
  }
76826
77768
  if (retroFound && retroEntry?.lessons_learned && retroEntry.lessons_learned.length > 0) {
76827
77769
  try {
76828
- const projectName = path79.basename(dir);
77770
+ const projectName = path80.basename(dir);
76829
77771
  const curationResult = await curateAndStoreSwarm(retroEntry.lessons_learned, projectName, { phase_number: phase }, dir, knowledgeConfig);
76830
77772
  if (curationResult) {
76831
77773
  const sessionState = swarmState.agentSessions.get(sessionID);
@@ -76905,7 +77847,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76905
77847
  let phaseRequiredAgents;
76906
77848
  try {
76907
77849
  const planPath = validateSwarmPath(dir, "plan.json");
76908
- const planRaw = fs65.readFileSync(planPath, "utf-8");
77850
+ const planRaw = fs66.readFileSync(planPath, "utf-8");
76909
77851
  const plan = JSON.parse(planRaw);
76910
77852
  const phaseObj = plan.phases.find((p) => p.id === phase);
76911
77853
  phaseRequiredAgents = phaseObj?.required_agents;
@@ -76920,7 +77862,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76920
77862
  if (agentsMissing.length > 0) {
76921
77863
  try {
76922
77864
  const planPath = validateSwarmPath(dir, "plan.json");
76923
- const planRaw = fs65.readFileSync(planPath, "utf-8");
77865
+ const planRaw = fs66.readFileSync(planPath, "utf-8");
76924
77866
  const plan = JSON.parse(planRaw);
76925
77867
  const targetPhase = plan.phases.find((p) => p.id === phase);
76926
77868
  if (targetPhase && targetPhase.tasks.length > 0 && targetPhase.tasks.every((t) => t.status === "completed")) {
@@ -76960,7 +77902,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
76960
77902
  if (phaseCompleteConfig.regression_sweep?.enforce) {
76961
77903
  try {
76962
77904
  const planPath = validateSwarmPath(dir, "plan.json");
76963
- const planRaw = fs65.readFileSync(planPath, "utf-8");
77905
+ const planRaw = fs66.readFileSync(planPath, "utf-8");
76964
77906
  const plan = JSON.parse(planRaw);
76965
77907
  const targetPhase = plan.phases.find((p) => p.id === phase);
76966
77908
  if (targetPhase) {
@@ -77014,7 +77956,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
77014
77956
  }
77015
77957
  try {
77016
77958
  const eventsPath = validateSwarmPath(dir, "events.jsonl");
77017
- fs65.appendFileSync(eventsPath, `${JSON.stringify(event)}
77959
+ fs66.appendFileSync(eventsPath, `${JSON.stringify(event)}
77018
77960
  `, "utf-8");
77019
77961
  } catch (writeError) {
77020
77962
  warnings.push(`Warning: failed to write phase complete event: ${writeError instanceof Error ? writeError.message : String(writeError)}`);
@@ -77089,12 +78031,12 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
77089
78031
  warnings.push(`Warning: failed to update plan.json phase status`);
77090
78032
  try {
77091
78033
  const planPath = validateSwarmPath(dir, "plan.json");
77092
- const planRaw = fs65.readFileSync(planPath, "utf-8");
78034
+ const planRaw = fs66.readFileSync(planPath, "utf-8");
77093
78035
  const plan2 = JSON.parse(planRaw);
77094
78036
  const phaseObj = plan2.phases.find((p) => p.id === phase);
77095
78037
  if (phaseObj) {
77096
78038
  phaseObj.status = "complete";
77097
- fs65.writeFileSync(planPath, JSON.stringify(plan2, null, 2), "utf-8");
78039
+ fs66.writeFileSync(planPath, JSON.stringify(plan2, null, 2), "utf-8");
77098
78040
  }
77099
78041
  } catch {}
77100
78042
  } else if (plan) {
@@ -77131,12 +78073,12 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
77131
78073
  warnings.push(`Warning: failed to update plan.json phase status`);
77132
78074
  try {
77133
78075
  const planPath = validateSwarmPath(dir, "plan.json");
77134
- const planRaw = fs65.readFileSync(planPath, "utf-8");
78076
+ const planRaw = fs66.readFileSync(planPath, "utf-8");
77135
78077
  const plan = JSON.parse(planRaw);
77136
78078
  const phaseObj = plan.phases.find((p) => p.id === phase);
77137
78079
  if (phaseObj) {
77138
78080
  phaseObj.status = "complete";
77139
- fs65.writeFileSync(planPath, JSON.stringify(plan, null, 2), "utf-8");
78081
+ fs66.writeFileSync(planPath, JSON.stringify(plan, null, 2), "utf-8");
77140
78082
  }
77141
78083
  } catch {}
77142
78084
  }
@@ -77193,8 +78135,8 @@ init_dist();
77193
78135
  init_discovery();
77194
78136
  init_utils();
77195
78137
  init_create_tool();
77196
- import * as fs66 from "fs";
77197
- import * as path80 from "path";
78138
+ import * as fs67 from "fs";
78139
+ import * as path81 from "path";
77198
78140
  var MAX_OUTPUT_BYTES5 = 52428800;
77199
78141
  var AUDIT_TIMEOUT_MS = 120000;
77200
78142
  function isValidEcosystem(value) {
@@ -77222,31 +78164,31 @@ function validateArgs3(args2) {
77222
78164
  function detectEcosystems(directory) {
77223
78165
  const ecosystems = [];
77224
78166
  const cwd = directory;
77225
- if (fs66.existsSync(path80.join(cwd, "package.json"))) {
78167
+ if (fs67.existsSync(path81.join(cwd, "package.json"))) {
77226
78168
  ecosystems.push("npm");
77227
78169
  }
77228
- if (fs66.existsSync(path80.join(cwd, "pyproject.toml")) || fs66.existsSync(path80.join(cwd, "requirements.txt"))) {
78170
+ if (fs67.existsSync(path81.join(cwd, "pyproject.toml")) || fs67.existsSync(path81.join(cwd, "requirements.txt"))) {
77229
78171
  ecosystems.push("pip");
77230
78172
  }
77231
- if (fs66.existsSync(path80.join(cwd, "Cargo.toml"))) {
78173
+ if (fs67.existsSync(path81.join(cwd, "Cargo.toml"))) {
77232
78174
  ecosystems.push("cargo");
77233
78175
  }
77234
- if (fs66.existsSync(path80.join(cwd, "go.mod"))) {
78176
+ if (fs67.existsSync(path81.join(cwd, "go.mod"))) {
77235
78177
  ecosystems.push("go");
77236
78178
  }
77237
78179
  try {
77238
- const files = fs66.readdirSync(cwd);
78180
+ const files = fs67.readdirSync(cwd);
77239
78181
  if (files.some((f) => f.endsWith(".csproj") || f.endsWith(".sln"))) {
77240
78182
  ecosystems.push("dotnet");
77241
78183
  }
77242
78184
  } catch {}
77243
- if (fs66.existsSync(path80.join(cwd, "Gemfile")) || fs66.existsSync(path80.join(cwd, "Gemfile.lock"))) {
78185
+ if (fs67.existsSync(path81.join(cwd, "Gemfile")) || fs67.existsSync(path81.join(cwd, "Gemfile.lock"))) {
77244
78186
  ecosystems.push("ruby");
77245
78187
  }
77246
- if (fs66.existsSync(path80.join(cwd, "pubspec.yaml"))) {
78188
+ if (fs67.existsSync(path81.join(cwd, "pubspec.yaml"))) {
77247
78189
  ecosystems.push("dart");
77248
78190
  }
77249
- if (fs66.existsSync(path80.join(cwd, "composer.lock"))) {
78191
+ if (fs67.existsSync(path81.join(cwd, "composer.lock"))) {
77250
78192
  ecosystems.push("composer");
77251
78193
  }
77252
78194
  return ecosystems;
@@ -78405,8 +79347,8 @@ var pkg_audit = createSwarmTool({
78405
79347
  // src/tools/placeholder-scan.ts
78406
79348
  init_dist();
78407
79349
  init_manager2();
78408
- import * as fs67 from "fs";
78409
- import * as path81 from "path";
79350
+ import * as fs68 from "fs";
79351
+ import * as path82 from "path";
78410
79352
  init_utils();
78411
79353
  init_create_tool();
78412
79354
  var MAX_FILE_SIZE = 1024 * 1024;
@@ -78529,7 +79471,7 @@ function isScaffoldFile(filePath) {
78529
79471
  if (SCAFFOLD_PATH_PATTERNS.some((pattern) => pattern.test(normalizedPath))) {
78530
79472
  return true;
78531
79473
  }
78532
- const filename = path81.basename(filePath);
79474
+ const filename = path82.basename(filePath);
78533
79475
  if (SCAFFOLD_FILENAME_PATTERNS.some((pattern) => pattern.test(filename))) {
78534
79476
  return true;
78535
79477
  }
@@ -78546,7 +79488,7 @@ function isAllowedByGlobs(filePath, allowGlobs) {
78546
79488
  if (regex.test(normalizedPath)) {
78547
79489
  return true;
78548
79490
  }
78549
- const filename = path81.basename(filePath);
79491
+ const filename = path82.basename(filePath);
78550
79492
  const filenameRegex = new RegExp(`^${regexPattern}$`, "i");
78551
79493
  if (filenameRegex.test(filename)) {
78552
79494
  return true;
@@ -78555,7 +79497,7 @@ function isAllowedByGlobs(filePath, allowGlobs) {
78555
79497
  return false;
78556
79498
  }
78557
79499
  function isParserSupported(filePath) {
78558
- const ext = path81.extname(filePath).toLowerCase();
79500
+ const ext = path82.extname(filePath).toLowerCase();
78559
79501
  return SUPPORTED_PARSER_EXTENSIONS.has(ext);
78560
79502
  }
78561
79503
  function isPlanFile(filePath) {
@@ -78802,28 +79744,28 @@ async function placeholderScan(input, directory) {
78802
79744
  let filesScanned = 0;
78803
79745
  const filesWithFindings = new Set;
78804
79746
  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) {
79747
+ const fullPath = path82.isAbsolute(filePath) ? filePath : path82.resolve(directory, filePath);
79748
+ const resolvedDirectory = path82.resolve(directory);
79749
+ if (!fullPath.startsWith(resolvedDirectory + path82.sep) && fullPath !== resolvedDirectory) {
78808
79750
  continue;
78809
79751
  }
78810
- if (!fs67.existsSync(fullPath)) {
79752
+ if (!fs68.existsSync(fullPath)) {
78811
79753
  continue;
78812
79754
  }
78813
79755
  if (isAllowedByGlobs(filePath, allow_globs)) {
78814
79756
  continue;
78815
79757
  }
78816
- const relativeFilePath = path81.relative(directory, fullPath).replace(/\\/g, "/");
79758
+ const relativeFilePath = path82.relative(directory, fullPath).replace(/\\/g, "/");
78817
79759
  if (FILE_ALLOWLIST.some((allowed) => relativeFilePath.endsWith(allowed))) {
78818
79760
  continue;
78819
79761
  }
78820
79762
  let content;
78821
79763
  try {
78822
- const stat4 = fs67.statSync(fullPath);
79764
+ const stat4 = fs68.statSync(fullPath);
78823
79765
  if (stat4.size > MAX_FILE_SIZE) {
78824
79766
  continue;
78825
79767
  }
78826
- content = fs67.readFileSync(fullPath, "utf-8");
79768
+ content = fs68.readFileSync(fullPath, "utf-8");
78827
79769
  } catch {
78828
79770
  continue;
78829
79771
  }
@@ -78885,8 +79827,8 @@ var placeholder_scan = createSwarmTool({
78885
79827
  });
78886
79828
  // src/tools/pre-check-batch.ts
78887
79829
  init_dist();
78888
- import * as fs70 from "fs";
78889
- import * as path84 from "path";
79830
+ import * as fs71 from "fs";
79831
+ import * as path85 from "path";
78890
79832
  init_manager2();
78891
79833
  init_utils();
78892
79834
  init_create_tool();
@@ -79021,8 +79963,8 @@ var quality_budget = createSwarmTool({
79021
79963
  init_dist();
79022
79964
  init_manager2();
79023
79965
  init_detector();
79024
- import * as fs69 from "fs";
79025
- import * as path83 from "path";
79966
+ import * as fs70 from "fs";
79967
+ import * as path84 from "path";
79026
79968
  import { extname as extname18 } from "path";
79027
79969
 
79028
79970
  // src/sast/rules/c.ts
@@ -79915,25 +80857,25 @@ init_create_tool();
79915
80857
  // src/tools/sast-baseline.ts
79916
80858
  init_utils2();
79917
80859
  import * as crypto8 from "crypto";
79918
- import * as fs68 from "fs";
79919
- import * as path82 from "path";
80860
+ import * as fs69 from "fs";
80861
+ import * as path83 from "path";
79920
80862
  var BASELINE_SCHEMA_VERSION = "1.0.0";
79921
80863
  var MAX_BASELINE_FINDINGS = 2000;
79922
80864
  var MAX_BASELINE_BYTES = 2 * 1048576;
79923
80865
  var LOCK_RETRY_DELAYS_MS = [50, 100, 200, 400, 800];
79924
80866
  function normalizeFindingPath(directory, file3) {
79925
- const resolved = path82.isAbsolute(file3) ? file3 : path82.resolve(directory, file3);
79926
- const rel = path82.relative(path82.resolve(directory), resolved);
80867
+ const resolved = path83.isAbsolute(file3) ? file3 : path83.resolve(directory, file3);
80868
+ const rel = path83.relative(path83.resolve(directory), resolved);
79927
80869
  return rel.replace(/\\/g, "/");
79928
80870
  }
79929
80871
  function baselineRelPath(phase) {
79930
- return path82.join("evidence", String(phase), "sast-baseline.json");
80872
+ return path83.join("evidence", String(phase), "sast-baseline.json");
79931
80873
  }
79932
80874
  function tempRelPath(phase) {
79933
- return path82.join("evidence", String(phase), `sast-baseline.json.tmp.${Date.now()}.${process.pid}`);
80875
+ return path83.join("evidence", String(phase), `sast-baseline.json.tmp.${Date.now()}.${process.pid}`);
79934
80876
  }
79935
80877
  function lockRelPath(phase) {
79936
- return path82.join("evidence", String(phase), "sast-baseline.json.lock");
80878
+ return path83.join("evidence", String(phase), "sast-baseline.json.lock");
79937
80879
  }
79938
80880
  function getLine(lines, idx) {
79939
80881
  if (idx < 0 || idx >= lines.length)
@@ -79950,7 +80892,7 @@ function fingerprintFinding(finding, directory, occurrenceIndex) {
79950
80892
  }
79951
80893
  const lineNum = finding.location.line;
79952
80894
  try {
79953
- const content = fs68.readFileSync(finding.location.file, "utf-8");
80895
+ const content = fs69.readFileSync(finding.location.file, "utf-8");
79954
80896
  const lines = content.split(`
79955
80897
  `);
79956
80898
  const idx = lineNum - 1;
@@ -79981,7 +80923,7 @@ function assignOccurrenceIndices(findings, directory) {
79981
80923
  try {
79982
80924
  if (relFile.startsWith(".."))
79983
80925
  throw new Error("escapes workspace");
79984
- const content = fs68.readFileSync(finding.location.file, "utf-8");
80926
+ const content = fs69.readFileSync(finding.location.file, "utf-8");
79985
80927
  const lines = content.split(`
79986
80928
  `);
79987
80929
  const idx = lineNum - 1;
@@ -80010,11 +80952,11 @@ function assignOccurrenceIndices(findings, directory) {
80010
80952
  async function acquireLock(lockPath) {
80011
80953
  for (let attempt = 0;attempt <= LOCK_RETRY_DELAYS_MS.length; attempt++) {
80012
80954
  try {
80013
- const fd = fs68.openSync(lockPath, "wx");
80014
- fs68.closeSync(fd);
80955
+ const fd = fs69.openSync(lockPath, "wx");
80956
+ fs69.closeSync(fd);
80015
80957
  return () => {
80016
80958
  try {
80017
- fs68.unlinkSync(lockPath);
80959
+ fs69.unlinkSync(lockPath);
80018
80960
  } catch {}
80019
80961
  };
80020
80962
  } catch {
@@ -80054,12 +80996,12 @@ async function captureOrMergeBaseline(directory, phase, findings, engine, scanne
80054
80996
  message: e instanceof Error ? e.message : "Path validation failed"
80055
80997
  };
80056
80998
  }
80057
- fs68.mkdirSync(path82.dirname(baselinePath), { recursive: true });
80999
+ fs69.mkdirSync(path83.dirname(baselinePath), { recursive: true });
80058
81000
  const releaseLock = await acquireLock(lockPath);
80059
81001
  try {
80060
81002
  let existing = null;
80061
81003
  try {
80062
- const raw = fs68.readFileSync(baselinePath, "utf-8");
81004
+ const raw = fs69.readFileSync(baselinePath, "utf-8");
80063
81005
  const parsed = JSON.parse(raw);
80064
81006
  if (parsed.schema_version === BASELINE_SCHEMA_VERSION) {
80065
81007
  existing = parsed;
@@ -80119,8 +81061,8 @@ async function captureOrMergeBaseline(directory, phase, findings, engine, scanne
80119
81061
  message: `Baseline would exceed size cap (${json4.length} bytes > ${MAX_BASELINE_BYTES})`
80120
81062
  };
80121
81063
  }
80122
- fs68.writeFileSync(tempPath, json4, "utf-8");
80123
- fs68.renameSync(tempPath, baselinePath);
81064
+ fs69.writeFileSync(tempPath, json4, "utf-8");
81065
+ fs69.renameSync(tempPath, baselinePath);
80124
81066
  return {
80125
81067
  status: "merged",
80126
81068
  path: baselinePath,
@@ -80151,8 +81093,8 @@ async function captureOrMergeBaseline(directory, phase, findings, engine, scanne
80151
81093
  message: `Baseline would exceed size cap (${json3.length} bytes > ${MAX_BASELINE_BYTES})`
80152
81094
  };
80153
81095
  }
80154
- fs68.writeFileSync(tempPath, json3, "utf-8");
80155
- fs68.renameSync(tempPath, baselinePath);
81096
+ fs69.writeFileSync(tempPath, json3, "utf-8");
81097
+ fs69.renameSync(tempPath, baselinePath);
80156
81098
  return {
80157
81099
  status: "written",
80158
81100
  path: baselinePath,
@@ -80177,7 +81119,7 @@ function loadBaseline(directory, phase) {
80177
81119
  };
80178
81120
  }
80179
81121
  try {
80180
- const raw = fs68.readFileSync(baselinePath, "utf-8");
81122
+ const raw = fs69.readFileSync(baselinePath, "utf-8");
80181
81123
  const parsed = JSON.parse(raw);
80182
81124
  if (parsed.schema_version !== BASELINE_SCHEMA_VERSION) {
80183
81125
  return {
@@ -80219,17 +81161,17 @@ var SEVERITY_ORDER = {
80219
81161
  };
80220
81162
  function shouldSkipFile(filePath) {
80221
81163
  try {
80222
- const stats = fs69.statSync(filePath);
81164
+ const stats = fs70.statSync(filePath);
80223
81165
  if (stats.size > MAX_FILE_SIZE_BYTES8) {
80224
81166
  return { skip: true, reason: "file too large" };
80225
81167
  }
80226
81168
  if (stats.size === 0) {
80227
81169
  return { skip: true, reason: "empty file" };
80228
81170
  }
80229
- const fd = fs69.openSync(filePath, "r");
81171
+ const fd = fs70.openSync(filePath, "r");
80230
81172
  const buffer = Buffer.alloc(8192);
80231
- const bytesRead = fs69.readSync(fd, buffer, 0, 8192, 0);
80232
- fs69.closeSync(fd);
81173
+ const bytesRead = fs70.readSync(fd, buffer, 0, 8192, 0);
81174
+ fs70.closeSync(fd);
80233
81175
  if (bytesRead > 0) {
80234
81176
  let nullCount = 0;
80235
81177
  for (let i2 = 0;i2 < bytesRead; i2++) {
@@ -80268,7 +81210,7 @@ function countBySeverity(findings) {
80268
81210
  }
80269
81211
  function scanFileWithTierA(filePath, language) {
80270
81212
  try {
80271
- const content = fs69.readFileSync(filePath, "utf-8");
81213
+ const content = fs70.readFileSync(filePath, "utf-8");
80272
81214
  const findings = executeRulesSync(filePath, content, language);
80273
81215
  return findings.map((f) => ({
80274
81216
  rule_id: f.rule_id,
@@ -80321,13 +81263,13 @@ async function sastScan(input, directory, config3) {
80321
81263
  _filesSkipped++;
80322
81264
  continue;
80323
81265
  }
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) {
81266
+ const resolvedPath = path84.isAbsolute(filePath) ? filePath : path84.resolve(directory, filePath);
81267
+ const resolvedDirectory = path84.resolve(directory);
81268
+ if (!resolvedPath.startsWith(resolvedDirectory + path84.sep) && resolvedPath !== resolvedDirectory) {
80327
81269
  _filesSkipped++;
80328
81270
  continue;
80329
81271
  }
80330
- if (!fs69.existsSync(resolvedPath)) {
81272
+ if (!fs70.existsSync(resolvedPath)) {
80331
81273
  _filesSkipped++;
80332
81274
  continue;
80333
81275
  }
@@ -80634,18 +81576,18 @@ function validatePath(inputPath, baseDir, workspaceDir) {
80634
81576
  let resolved;
80635
81577
  const isWinAbs = isWindowsAbsolutePath(inputPath);
80636
81578
  if (isWinAbs) {
80637
- resolved = path84.win32.resolve(inputPath);
80638
- } else if (path84.isAbsolute(inputPath)) {
80639
- resolved = path84.resolve(inputPath);
81579
+ resolved = path85.win32.resolve(inputPath);
81580
+ } else if (path85.isAbsolute(inputPath)) {
81581
+ resolved = path85.resolve(inputPath);
80640
81582
  } else {
80641
- resolved = path84.resolve(baseDir, inputPath);
81583
+ resolved = path85.resolve(baseDir, inputPath);
80642
81584
  }
80643
- const workspaceResolved = path84.resolve(workspaceDir);
81585
+ const workspaceResolved = path85.resolve(workspaceDir);
80644
81586
  let relative20;
80645
81587
  if (isWinAbs) {
80646
- relative20 = path84.win32.relative(workspaceResolved, resolved);
81588
+ relative20 = path85.win32.relative(workspaceResolved, resolved);
80647
81589
  } else {
80648
- relative20 = path84.relative(workspaceResolved, resolved);
81590
+ relative20 = path85.relative(workspaceResolved, resolved);
80649
81591
  }
80650
81592
  if (relative20.startsWith("..")) {
80651
81593
  return "path traversal detected";
@@ -80710,7 +81652,7 @@ async function runLintOnFiles(linter, files, workspaceDir) {
80710
81652
  if (typeof file3 !== "string") {
80711
81653
  continue;
80712
81654
  }
80713
- const resolvedPath = path84.resolve(file3);
81655
+ const resolvedPath = path85.resolve(file3);
80714
81656
  const validationError = validatePath(resolvedPath, workspaceDir, workspaceDir);
80715
81657
  if (validationError) {
80716
81658
  continue;
@@ -80867,7 +81809,7 @@ async function runSecretscanWithFiles(files, directory) {
80867
81809
  skippedFiles++;
80868
81810
  continue;
80869
81811
  }
80870
- const resolvedPath = path84.resolve(file3);
81812
+ const resolvedPath = path85.resolve(file3);
80871
81813
  const validationError = validatePath(resolvedPath, directory, directory);
80872
81814
  if (validationError) {
80873
81815
  skippedFiles++;
@@ -80885,14 +81827,14 @@ async function runSecretscanWithFiles(files, directory) {
80885
81827
  };
80886
81828
  }
80887
81829
  for (const file3 of validatedFiles) {
80888
- const ext = path84.extname(file3).toLowerCase();
81830
+ const ext = path85.extname(file3).toLowerCase();
80889
81831
  if (DEFAULT_EXCLUDE_EXTENSIONS2.has(ext)) {
80890
81832
  skippedFiles++;
80891
81833
  continue;
80892
81834
  }
80893
81835
  let stat4;
80894
81836
  try {
80895
- stat4 = fs70.statSync(file3);
81837
+ stat4 = fs71.statSync(file3);
80896
81838
  } catch {
80897
81839
  skippedFiles++;
80898
81840
  continue;
@@ -80903,7 +81845,7 @@ async function runSecretscanWithFiles(files, directory) {
80903
81845
  }
80904
81846
  let content;
80905
81847
  try {
80906
- const buffer = fs70.readFileSync(file3);
81848
+ const buffer = fs71.readFileSync(file3);
80907
81849
  if (buffer.includes(0)) {
80908
81850
  skippedFiles++;
80909
81851
  continue;
@@ -81104,7 +82046,7 @@ function classifySastFindings(findings, changedLineRanges, directory) {
81104
82046
  const preexistingFindings = [];
81105
82047
  for (const finding of findings) {
81106
82048
  const filePath = finding.location.file;
81107
- const normalised = path84.relative(directory, filePath).replace(/\\/g, "/");
82049
+ const normalised = path85.relative(directory, filePath).replace(/\\/g, "/");
81108
82050
  const changedLines = changedLineRanges.get(normalised);
81109
82051
  if (changedLines?.has(finding.location.line)) {
81110
82052
  newFindings.push(finding);
@@ -81155,7 +82097,7 @@ async function runPreCheckBatch(input, workspaceDir, contextDir) {
81155
82097
  warn(`pre_check_batch: Invalid file path: ${file3}`);
81156
82098
  continue;
81157
82099
  }
81158
- changedFiles.push(path84.resolve(directory, file3));
82100
+ changedFiles.push(path85.resolve(directory, file3));
81159
82101
  }
81160
82102
  if (changedFiles.length === 0) {
81161
82103
  warn("pre_check_batch: No valid files after validation, skipping all tools (fail-closed)");
@@ -81356,7 +82298,7 @@ var pre_check_batch = createSwarmTool({
81356
82298
  };
81357
82299
  return JSON.stringify(errorResult, null, 2);
81358
82300
  }
81359
- const resolvedDirectory = path84.resolve(typedArgs.directory);
82301
+ const resolvedDirectory = path85.resolve(typedArgs.directory);
81360
82302
  const workspaceAnchor = resolvedDirectory;
81361
82303
  const dirError = validateDirectory2(resolvedDirectory, workspaceAnchor);
81362
82304
  if (dirError) {
@@ -81397,7 +82339,7 @@ var pre_check_batch = createSwarmTool({
81397
82339
  });
81398
82340
  // src/tools/repo-map.ts
81399
82341
  init_dist();
81400
- import * as path85 from "path";
82342
+ import * as path86 from "path";
81401
82343
  init_path_security();
81402
82344
  init_create_tool();
81403
82345
  var VALID_ACTIONS = [
@@ -81422,7 +82364,7 @@ function validateFile(p) {
81422
82364
  return "file contains control characters";
81423
82365
  if (containsPathTraversal(p))
81424
82366
  return "file contains path traversal";
81425
- if (path85.isAbsolute(p) || /^[a-zA-Z]:[\\/]/.test(p)) {
82367
+ if (path86.isAbsolute(p) || /^[a-zA-Z]:[\\/]/.test(p)) {
81426
82368
  return "file must be a workspace-relative path, not absolute";
81427
82369
  }
81428
82370
  return null;
@@ -81445,8 +82387,8 @@ function ok(action, payload) {
81445
82387
  }
81446
82388
  function toRelativeGraphPath(input, workspaceRoot) {
81447
82389
  const normalized = input.replace(/\\/g, "/");
81448
- if (path85.isAbsolute(normalized)) {
81449
- const rel = path85.relative(workspaceRoot, normalized).replace(/\\/g, "/");
82390
+ if (path86.isAbsolute(normalized)) {
82391
+ const rel = path86.relative(workspaceRoot, normalized).replace(/\\/g, "/");
81450
82392
  return normalizeGraphPath2(rel);
81451
82393
  }
81452
82394
  return normalizeGraphPath2(normalized);
@@ -81590,8 +82532,8 @@ var repo_map = createSwarmTool({
81590
82532
  // src/tools/req-coverage.ts
81591
82533
  init_dist();
81592
82534
  init_create_tool();
81593
- import * as fs71 from "fs";
81594
- import * as path86 from "path";
82535
+ import * as fs72 from "fs";
82536
+ import * as path87 from "path";
81595
82537
  var SPEC_FILE = ".swarm/spec.md";
81596
82538
  var EVIDENCE_DIR4 = ".swarm/evidence";
81597
82539
  var OBLIGATION_KEYWORDS = ["MUST", "SHOULD", "SHALL"];
@@ -81650,19 +82592,19 @@ function extractObligationAndText(id, lineText) {
81650
82592
  var PHASE_TASK_ID_REGEX = /^\d+\.\d+(\.\d+)*$/;
81651
82593
  function readTouchedFiles(evidenceDir, phase, cwd) {
81652
82594
  const touchedFiles = new Set;
81653
- if (!fs71.existsSync(evidenceDir) || !fs71.statSync(evidenceDir).isDirectory()) {
82595
+ if (!fs72.existsSync(evidenceDir) || !fs72.statSync(evidenceDir).isDirectory()) {
81654
82596
  return [];
81655
82597
  }
81656
82598
  let entries;
81657
82599
  try {
81658
- entries = fs71.readdirSync(evidenceDir);
82600
+ entries = fs72.readdirSync(evidenceDir);
81659
82601
  } catch {
81660
82602
  return [];
81661
82603
  }
81662
82604
  for (const entry of entries) {
81663
- const entryPath = path86.join(evidenceDir, entry);
82605
+ const entryPath = path87.join(evidenceDir, entry);
81664
82606
  try {
81665
- const stat4 = fs71.statSync(entryPath);
82607
+ const stat4 = fs72.statSync(entryPath);
81666
82608
  if (!stat4.isDirectory()) {
81667
82609
  continue;
81668
82610
  }
@@ -81676,14 +82618,14 @@ function readTouchedFiles(evidenceDir, phase, cwd) {
81676
82618
  if (entryPhase !== String(phase)) {
81677
82619
  continue;
81678
82620
  }
81679
- const evidenceFilePath = path86.join(entryPath, "evidence.json");
82621
+ const evidenceFilePath = path87.join(entryPath, "evidence.json");
81680
82622
  try {
81681
- const resolvedPath = path86.resolve(evidenceFilePath);
81682
- const evidenceDirResolved = path86.resolve(evidenceDir);
81683
- if (!resolvedPath.startsWith(evidenceDirResolved + path86.sep)) {
82623
+ const resolvedPath = path87.resolve(evidenceFilePath);
82624
+ const evidenceDirResolved = path87.resolve(evidenceDir);
82625
+ if (!resolvedPath.startsWith(evidenceDirResolved + path87.sep)) {
81684
82626
  continue;
81685
82627
  }
81686
- const stat4 = fs71.lstatSync(evidenceFilePath);
82628
+ const stat4 = fs72.lstatSync(evidenceFilePath);
81687
82629
  if (!stat4.isFile()) {
81688
82630
  continue;
81689
82631
  }
@@ -81695,7 +82637,7 @@ function readTouchedFiles(evidenceDir, phase, cwd) {
81695
82637
  }
81696
82638
  let content;
81697
82639
  try {
81698
- content = fs71.readFileSync(evidenceFilePath, "utf-8");
82640
+ content = fs72.readFileSync(evidenceFilePath, "utf-8");
81699
82641
  } catch {
81700
82642
  continue;
81701
82643
  }
@@ -81714,7 +82656,7 @@ function readTouchedFiles(evidenceDir, phase, cwd) {
81714
82656
  if (Array.isArray(diffEntry.files_changed)) {
81715
82657
  for (const file3 of diffEntry.files_changed) {
81716
82658
  if (typeof file3 === "string") {
81717
- touchedFiles.add(path86.resolve(cwd, file3));
82659
+ touchedFiles.add(path87.resolve(cwd, file3));
81718
82660
  }
81719
82661
  }
81720
82662
  }
@@ -81727,12 +82669,12 @@ function readTouchedFiles(evidenceDir, phase, cwd) {
81727
82669
  }
81728
82670
  function searchFileForKeywords(filePath, keywords, cwd) {
81729
82671
  try {
81730
- const resolvedPath = path86.resolve(filePath);
81731
- const cwdResolved = path86.resolve(cwd);
82672
+ const resolvedPath = path87.resolve(filePath);
82673
+ const cwdResolved = path87.resolve(cwd);
81732
82674
  if (!resolvedPath.startsWith(cwdResolved)) {
81733
82675
  return false;
81734
82676
  }
81735
- const content = fs71.readFileSync(resolvedPath, "utf-8");
82677
+ const content = fs72.readFileSync(resolvedPath, "utf-8");
81736
82678
  for (const keyword of keywords) {
81737
82679
  const regex = new RegExp(`\\b${keyword}\\b`, "i");
81738
82680
  if (regex.test(content)) {
@@ -81862,10 +82804,10 @@ var req_coverage = createSwarmTool({
81862
82804
  }, null, 2);
81863
82805
  }
81864
82806
  const cwd = inputDirectory || directory;
81865
- const specPath = path86.join(cwd, SPEC_FILE);
82807
+ const specPath = path87.join(cwd, SPEC_FILE);
81866
82808
  let specContent;
81867
82809
  try {
81868
- specContent = fs71.readFileSync(specPath, "utf-8");
82810
+ specContent = fs72.readFileSync(specPath, "utf-8");
81869
82811
  } catch (readError) {
81870
82812
  return JSON.stringify({
81871
82813
  success: false,
@@ -81889,7 +82831,7 @@ var req_coverage = createSwarmTool({
81889
82831
  message: "No FR requirements found in spec.md"
81890
82832
  }, null, 2);
81891
82833
  }
81892
- const evidenceDir = path86.join(cwd, EVIDENCE_DIR4);
82834
+ const evidenceDir = path87.join(cwd, EVIDENCE_DIR4);
81893
82835
  const touchedFiles = readTouchedFiles(evidenceDir, phase, cwd);
81894
82836
  const analyzedRequirements = [];
81895
82837
  let coveredCount = 0;
@@ -81915,12 +82857,12 @@ var req_coverage = createSwarmTool({
81915
82857
  requirements: analyzedRequirements
81916
82858
  };
81917
82859
  const reportFilename = `req-coverage-phase-${phase}.json`;
81918
- const reportPath = path86.join(evidenceDir, reportFilename);
82860
+ const reportPath = path87.join(evidenceDir, reportFilename);
81919
82861
  try {
81920
- if (!fs71.existsSync(evidenceDir)) {
81921
- fs71.mkdirSync(evidenceDir, { recursive: true });
82862
+ if (!fs72.existsSync(evidenceDir)) {
82863
+ fs72.mkdirSync(evidenceDir, { recursive: true });
81922
82864
  }
81923
- fs71.writeFileSync(reportPath, JSON.stringify(result, null, 2), "utf-8");
82865
+ fs72.writeFileSync(reportPath, JSON.stringify(result, null, 2), "utf-8");
81924
82866
  } catch (writeError) {
81925
82867
  console.warn(`Failed to write coverage report: ${writeError instanceof Error ? writeError.message : String(writeError)}`);
81926
82868
  }
@@ -81999,6 +82941,7 @@ ${paginatedContent}`;
81999
82941
  // src/tools/save-plan.ts
82000
82942
  init_tool();
82001
82943
  init_plan_schema();
82944
+ init_qa_gate_profile();
82002
82945
  init_file_locks();
82003
82946
  init_checkpoint3();
82004
82947
  init_ledger();
@@ -82006,8 +82949,8 @@ init_manager();
82006
82949
  init_state();
82007
82950
  init_create_tool();
82008
82951
  import * as crypto9 from "crypto";
82009
- import * as fs72 from "fs";
82010
- import * as path87 from "path";
82952
+ import * as fs73 from "fs";
82953
+ import * as path88 from "path";
82011
82954
  function detectPlaceholderContent(args2) {
82012
82955
  const issues = [];
82013
82956
  const placeholderPattern = /^\[\w[\w\s]*\]$/;
@@ -82081,17 +83024,17 @@ async function executeSavePlan(args2, fallbackDir) {
82081
83024
  };
82082
83025
  }
82083
83026
  if (args2.working_directory && fallbackDir) {
82084
- const resolvedTarget = path87.resolve(args2.working_directory);
82085
- const resolvedRoot = path87.resolve(fallbackDir);
83027
+ const resolvedTarget = path88.resolve(args2.working_directory);
83028
+ const resolvedRoot = path88.resolve(fallbackDir);
82086
83029
  let fallbackExists = false;
82087
83030
  try {
82088
- fs72.accessSync(resolvedRoot, fs72.constants.F_OK);
83031
+ fs73.accessSync(resolvedRoot, fs73.constants.F_OK);
82089
83032
  fallbackExists = true;
82090
83033
  } catch {
82091
83034
  fallbackExists = false;
82092
83035
  }
82093
83036
  if (fallbackExists) {
82094
- const isSubdirectory = resolvedTarget.startsWith(resolvedRoot + path87.sep);
83037
+ const isSubdirectory = resolvedTarget.startsWith(resolvedRoot + path88.sep);
82095
83038
  if (isSubdirectory) {
82096
83039
  return {
82097
83040
  success: false,
@@ -82107,11 +83050,11 @@ async function executeSavePlan(args2, fallbackDir) {
82107
83050
  let specMtime;
82108
83051
  let specHash;
82109
83052
  if (process.env.SWARM_SKIP_SPEC_GATE !== "1") {
82110
- const specPath = path87.join(targetWorkspace, ".swarm", "spec.md");
83053
+ const specPath = path88.join(targetWorkspace, ".swarm", "spec.md");
82111
83054
  try {
82112
- const stat4 = await fs72.promises.stat(specPath);
83055
+ const stat4 = await fs73.promises.stat(specPath);
82113
83056
  specMtime = stat4.mtime.toISOString();
82114
- const content = await fs72.promises.readFile(specPath, "utf8");
83057
+ const content = await fs73.promises.readFile(specPath, "utf8");
82115
83058
  specHash = crypto9.createHash("sha256").update(content).digest("hex");
82116
83059
  } catch {
82117
83060
  return {
@@ -82122,6 +83065,32 @@ async function executeSavePlan(args2, fallbackDir) {
82122
83065
  };
82123
83066
  }
82124
83067
  }
83068
+ if (process.env.SWARM_SKIP_GATE_SELECTION !== "1") {
83069
+ const contextPath = path88.join(targetWorkspace, ".swarm", "context.md");
83070
+ let contextContent = "";
83071
+ try {
83072
+ contextContent = await fs73.promises.readFile(contextPath, "utf8");
83073
+ } catch {}
83074
+ const hasPendingSection = contextContent.includes("## Pending QA Gate Selection");
83075
+ if (!hasPendingSection) {
83076
+ const candidatePlanId = `${args2.swarm_id}-${args2.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
83077
+ let existingProfile = null;
83078
+ try {
83079
+ existingProfile = getProfile(targetWorkspace, candidatePlanId);
83080
+ } catch {}
83081
+ if (!existingProfile) {
83082
+ return {
83083
+ success: false,
83084
+ 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.',
83085
+ errors: [
83086
+ "Missing ## Pending QA Gate Selection in .swarm/context.md",
83087
+ "No existing QaGateProfile found for this plan"
83088
+ ],
83089
+ 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."
83090
+ };
83091
+ }
83092
+ }
83093
+ }
82125
83094
  const dir = targetWorkspace;
82126
83095
  const existingStatusMap = new Map;
82127
83096
  let preservedExecutionProfile;
@@ -82247,14 +83216,14 @@ async function executeSavePlan(args2, fallbackDir) {
82247
83216
  }
82248
83217
  await writeCheckpoint(dir).catch(() => {});
82249
83218
  try {
82250
- const markerPath = path87.join(dir, ".swarm", ".plan-write-marker");
83219
+ const markerPath = path88.join(dir, ".swarm", ".plan-write-marker");
82251
83220
  const marker = JSON.stringify({
82252
83221
  source: "save_plan",
82253
83222
  timestamp: new Date().toISOString(),
82254
83223
  phases_count: plan.phases.length,
82255
83224
  tasks_count: tasksCount
82256
83225
  });
82257
- await fs72.promises.writeFile(markerPath, marker, "utf8");
83226
+ await fs73.promises.writeFile(markerPath, marker, "utf8");
82258
83227
  } catch {}
82259
83228
  const warnings = [];
82260
83229
  let criticReviewFound = false;
@@ -82270,7 +83239,7 @@ async function executeSavePlan(args2, fallbackDir) {
82270
83239
  return {
82271
83240
  success: true,
82272
83241
  message: "Plan saved successfully",
82273
- plan_path: path87.join(dir, ".swarm", "plan.json"),
83242
+ plan_path: path88.join(dir, ".swarm", "plan.json"),
82274
83243
  phases_count: plan.phases.length,
82275
83244
  tasks_count: tasksCount,
82276
83245
  ...resolvedProfile !== undefined ? { execution_profile: resolvedProfile } : {},
@@ -82322,8 +83291,8 @@ var save_plan = createSwarmTool({
82322
83291
  // src/tools/sbom-generate.ts
82323
83292
  init_dist();
82324
83293
  init_manager2();
82325
- import * as fs73 from "fs";
82326
- import * as path88 from "path";
83294
+ import * as fs74 from "fs";
83295
+ import * as path89 from "path";
82327
83296
 
82328
83297
  // src/sbom/detectors/index.ts
82329
83298
  init_utils();
@@ -83171,9 +84140,9 @@ function findManifestFiles(rootDir) {
83171
84140
  const patterns = [...new Set(allDetectors.flatMap((d) => d.patterns))];
83172
84141
  function searchDir(dir) {
83173
84142
  try {
83174
- const entries = fs73.readdirSync(dir, { withFileTypes: true });
84143
+ const entries = fs74.readdirSync(dir, { withFileTypes: true });
83175
84144
  for (const entry of entries) {
83176
- const fullPath = path88.join(dir, entry.name);
84145
+ const fullPath = path89.join(dir, entry.name);
83177
84146
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === "target") {
83178
84147
  continue;
83179
84148
  }
@@ -83182,7 +84151,7 @@ function findManifestFiles(rootDir) {
83182
84151
  } else if (entry.isFile()) {
83183
84152
  for (const pattern of patterns) {
83184
84153
  if (simpleGlobToRegex(pattern).test(entry.name)) {
83185
- manifestFiles.push(path88.relative(rootDir, fullPath));
84154
+ manifestFiles.push(path89.relative(rootDir, fullPath));
83186
84155
  break;
83187
84156
  }
83188
84157
  }
@@ -83198,13 +84167,13 @@ function findManifestFilesInDirs(directories, workingDir) {
83198
84167
  const patterns = [...new Set(allDetectors.flatMap((d) => d.patterns))];
83199
84168
  for (const dir of directories) {
83200
84169
  try {
83201
- const entries = fs73.readdirSync(dir, { withFileTypes: true });
84170
+ const entries = fs74.readdirSync(dir, { withFileTypes: true });
83202
84171
  for (const entry of entries) {
83203
- const fullPath = path88.join(dir, entry.name);
84172
+ const fullPath = path89.join(dir, entry.name);
83204
84173
  if (entry.isFile()) {
83205
84174
  for (const pattern of patterns) {
83206
84175
  if (simpleGlobToRegex(pattern).test(entry.name)) {
83207
- found.push(path88.relative(workingDir, fullPath));
84176
+ found.push(path89.relative(workingDir, fullPath));
83208
84177
  break;
83209
84178
  }
83210
84179
  }
@@ -83217,11 +84186,11 @@ function findManifestFilesInDirs(directories, workingDir) {
83217
84186
  function getDirectoriesFromChangedFiles(changedFiles, workingDir) {
83218
84187
  const dirs = new Set;
83219
84188
  for (const file3 of changedFiles) {
83220
- let currentDir = path88.dirname(file3);
84189
+ let currentDir = path89.dirname(file3);
83221
84190
  while (true) {
83222
- if (currentDir && currentDir !== "." && currentDir !== path88.sep) {
83223
- dirs.add(path88.join(workingDir, currentDir));
83224
- const parent = path88.dirname(currentDir);
84191
+ if (currentDir && currentDir !== "." && currentDir !== path89.sep) {
84192
+ dirs.add(path89.join(workingDir, currentDir));
84193
+ const parent = path89.dirname(currentDir);
83225
84194
  if (parent === currentDir)
83226
84195
  break;
83227
84196
  currentDir = parent;
@@ -83235,7 +84204,7 @@ function getDirectoriesFromChangedFiles(changedFiles, workingDir) {
83235
84204
  }
83236
84205
  function ensureOutputDir(outputDir) {
83237
84206
  try {
83238
- fs73.mkdirSync(outputDir, { recursive: true });
84207
+ fs74.mkdirSync(outputDir, { recursive: true });
83239
84208
  } catch (error93) {
83240
84209
  if (!error93 || error93.code !== "EEXIST") {
83241
84210
  throw error93;
@@ -83305,7 +84274,7 @@ var sbom_generate = createSwarmTool({
83305
84274
  const changedFiles = obj.changed_files;
83306
84275
  const relativeOutputDir = obj.output_dir || DEFAULT_OUTPUT_DIR;
83307
84276
  const workingDir = directory;
83308
- const outputDir = path88.isAbsolute(relativeOutputDir) ? relativeOutputDir : path88.join(workingDir, relativeOutputDir);
84277
+ const outputDir = path89.isAbsolute(relativeOutputDir) ? relativeOutputDir : path89.join(workingDir, relativeOutputDir);
83309
84278
  let manifestFiles = [];
83310
84279
  if (scope === "all") {
83311
84280
  manifestFiles = findManifestFiles(workingDir);
@@ -83328,11 +84297,11 @@ var sbom_generate = createSwarmTool({
83328
84297
  const processedFiles = [];
83329
84298
  for (const manifestFile of manifestFiles) {
83330
84299
  try {
83331
- const fullPath = path88.isAbsolute(manifestFile) ? manifestFile : path88.join(workingDir, manifestFile);
83332
- if (!fs73.existsSync(fullPath)) {
84300
+ const fullPath = path89.isAbsolute(manifestFile) ? manifestFile : path89.join(workingDir, manifestFile);
84301
+ if (!fs74.existsSync(fullPath)) {
83333
84302
  continue;
83334
84303
  }
83335
- const content = fs73.readFileSync(fullPath, "utf-8");
84304
+ const content = fs74.readFileSync(fullPath, "utf-8");
83336
84305
  const components = detectComponents(manifestFile, content);
83337
84306
  processedFiles.push(manifestFile);
83338
84307
  if (components.length > 0) {
@@ -83345,8 +84314,8 @@ var sbom_generate = createSwarmTool({
83345
84314
  const bom = generateCycloneDX(allComponents);
83346
84315
  const bomJson = serializeCycloneDX(bom);
83347
84316
  const filename = generateSbomFilename();
83348
- const outputPath = path88.join(outputDir, filename);
83349
- fs73.writeFileSync(outputPath, bomJson, "utf-8");
84317
+ const outputPath = path89.join(outputDir, filename);
84318
+ fs74.writeFileSync(outputPath, bomJson, "utf-8");
83350
84319
  const verdict = processedFiles.length > 0 ? "pass" : "pass";
83351
84320
  try {
83352
84321
  const timestamp = new Date().toISOString();
@@ -83388,8 +84357,8 @@ var sbom_generate = createSwarmTool({
83388
84357
  // src/tools/schema-drift.ts
83389
84358
  init_dist();
83390
84359
  init_create_tool();
83391
- import * as fs74 from "fs";
83392
- import * as path89 from "path";
84360
+ import * as fs75 from "fs";
84361
+ import * as path90 from "path";
83393
84362
  var SPEC_CANDIDATES = [
83394
84363
  "openapi.json",
83395
84364
  "openapi.yaml",
@@ -83421,28 +84390,28 @@ function normalizePath3(p) {
83421
84390
  }
83422
84391
  function discoverSpecFile(cwd, specFileArg) {
83423
84392
  if (specFileArg) {
83424
- const resolvedPath = path89.resolve(cwd, specFileArg);
83425
- const normalizedCwd = cwd.endsWith(path89.sep) ? cwd : cwd + path89.sep;
84393
+ const resolvedPath = path90.resolve(cwd, specFileArg);
84394
+ const normalizedCwd = cwd.endsWith(path90.sep) ? cwd : cwd + path90.sep;
83426
84395
  if (!resolvedPath.startsWith(normalizedCwd) && resolvedPath !== cwd) {
83427
84396
  throw new Error("Invalid spec_file: path traversal detected");
83428
84397
  }
83429
- const ext = path89.extname(resolvedPath).toLowerCase();
84398
+ const ext = path90.extname(resolvedPath).toLowerCase();
83430
84399
  if (!ALLOWED_EXTENSIONS.includes(ext)) {
83431
84400
  throw new Error(`Invalid spec_file: must end in .json, .yaml, or .yml, got ${ext}`);
83432
84401
  }
83433
- const stats = fs74.statSync(resolvedPath);
84402
+ const stats = fs75.statSync(resolvedPath);
83434
84403
  if (stats.size > MAX_SPEC_SIZE) {
83435
84404
  throw new Error(`Invalid spec_file: file exceeds ${MAX_SPEC_SIZE / 1024 / 1024}MB limit`);
83436
84405
  }
83437
- if (!fs74.existsSync(resolvedPath)) {
84406
+ if (!fs75.existsSync(resolvedPath)) {
83438
84407
  throw new Error(`Spec file not found: ${resolvedPath}`);
83439
84408
  }
83440
84409
  return resolvedPath;
83441
84410
  }
83442
84411
  for (const candidate of SPEC_CANDIDATES) {
83443
- const candidatePath = path89.resolve(cwd, candidate);
83444
- if (fs74.existsSync(candidatePath)) {
83445
- const stats = fs74.statSync(candidatePath);
84412
+ const candidatePath = path90.resolve(cwd, candidate);
84413
+ if (fs75.existsSync(candidatePath)) {
84414
+ const stats = fs75.statSync(candidatePath);
83446
84415
  if (stats.size <= MAX_SPEC_SIZE) {
83447
84416
  return candidatePath;
83448
84417
  }
@@ -83451,8 +84420,8 @@ function discoverSpecFile(cwd, specFileArg) {
83451
84420
  return null;
83452
84421
  }
83453
84422
  function parseSpec(specFile) {
83454
- const content = fs74.readFileSync(specFile, "utf-8");
83455
- const ext = path89.extname(specFile).toLowerCase();
84423
+ const content = fs75.readFileSync(specFile, "utf-8");
84424
+ const ext = path90.extname(specFile).toLowerCase();
83456
84425
  if (ext === ".json") {
83457
84426
  return parseJsonSpec(content);
83458
84427
  }
@@ -83523,12 +84492,12 @@ function extractRoutes(cwd) {
83523
84492
  function walkDir(dir) {
83524
84493
  let entries;
83525
84494
  try {
83526
- entries = fs74.readdirSync(dir, { withFileTypes: true });
84495
+ entries = fs75.readdirSync(dir, { withFileTypes: true });
83527
84496
  } catch {
83528
84497
  return;
83529
84498
  }
83530
84499
  for (const entry of entries) {
83531
- const fullPath = path89.join(dir, entry.name);
84500
+ const fullPath = path90.join(dir, entry.name);
83532
84501
  if (entry.isSymbolicLink()) {
83533
84502
  continue;
83534
84503
  }
@@ -83538,7 +84507,7 @@ function extractRoutes(cwd) {
83538
84507
  }
83539
84508
  walkDir(fullPath);
83540
84509
  } else if (entry.isFile()) {
83541
- const ext = path89.extname(entry.name).toLowerCase();
84510
+ const ext = path90.extname(entry.name).toLowerCase();
83542
84511
  const baseName = entry.name.toLowerCase();
83543
84512
  if (![".ts", ".js", ".mjs"].includes(ext)) {
83544
84513
  continue;
@@ -83556,7 +84525,7 @@ function extractRoutes(cwd) {
83556
84525
  }
83557
84526
  function extractRoutesFromFile(filePath) {
83558
84527
  const routes = [];
83559
- const content = fs74.readFileSync(filePath, "utf-8");
84528
+ const content = fs75.readFileSync(filePath, "utf-8");
83560
84529
  const lines = content.split(/\r?\n/);
83561
84530
  const expressRegex = /(?:app|router|server|express)\.(get|post|put|patch|delete|options|head)\s*\(\s*['"`]([^'"`]+)['"`]/g;
83562
84531
  const flaskRegex = /@(?:app|blueprint|bp)\.route\s*\(\s*['"]([^'"]+)['"]/g;
@@ -83704,8 +84673,8 @@ var schema_drift = createSwarmTool({
83704
84673
  init_tool();
83705
84674
  init_path_security();
83706
84675
  init_create_tool();
83707
- import * as fs75 from "fs";
83708
- import * as path90 from "path";
84676
+ import * as fs76 from "fs";
84677
+ import * as path91 from "path";
83709
84678
  var DEFAULT_MAX_RESULTS = 100;
83710
84679
  var DEFAULT_MAX_LINES = 200;
83711
84680
  var REGEX_TIMEOUT_MS = 5000;
@@ -83741,11 +84710,11 @@ function containsWindowsAttacks3(str) {
83741
84710
  }
83742
84711
  function isPathInWorkspace3(filePath, workspace) {
83743
84712
  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)) {
84713
+ const resolvedPath = path91.resolve(workspace, filePath);
84714
+ const realWorkspace = fs76.realpathSync(workspace);
84715
+ const realResolvedPath = fs76.realpathSync(resolvedPath);
84716
+ const relativePath = path91.relative(realWorkspace, realResolvedPath);
84717
+ if (relativePath.startsWith("..") || path91.isAbsolute(relativePath)) {
83749
84718
  return false;
83750
84719
  }
83751
84720
  return true;
@@ -83758,12 +84727,12 @@ function validatePathForRead2(filePath, workspace) {
83758
84727
  }
83759
84728
  function findRgInEnvPath() {
83760
84729
  const searchPath = process.env.PATH ?? "";
83761
- for (const dir of searchPath.split(path90.delimiter)) {
84730
+ for (const dir of searchPath.split(path91.delimiter)) {
83762
84731
  if (!dir)
83763
84732
  continue;
83764
84733
  const isWindows = process.platform === "win32";
83765
- const candidate = path90.join(dir, isWindows ? "rg.exe" : "rg");
83766
- if (fs75.existsSync(candidate))
84734
+ const candidate = path91.join(dir, isWindows ? "rg.exe" : "rg");
84735
+ if (fs76.existsSync(candidate))
83767
84736
  return candidate;
83768
84737
  }
83769
84738
  return null;
@@ -83890,10 +84859,10 @@ function collectFiles(dir, workspace, includeGlobs, excludeGlobs) {
83890
84859
  return files;
83891
84860
  }
83892
84861
  try {
83893
- const entries = fs75.readdirSync(dir, { withFileTypes: true });
84862
+ const entries = fs76.readdirSync(dir, { withFileTypes: true });
83894
84863
  for (const entry of entries) {
83895
- const fullPath = path90.join(dir, entry.name);
83896
- const relativePath = path90.relative(workspace, fullPath);
84864
+ const fullPath = path91.join(dir, entry.name);
84865
+ const relativePath = path91.relative(workspace, fullPath);
83897
84866
  if (!validatePathForRead2(fullPath, workspace)) {
83898
84867
  continue;
83899
84868
  }
@@ -83934,13 +84903,13 @@ async function fallbackSearch(opts) {
83934
84903
  const matches = [];
83935
84904
  let total = 0;
83936
84905
  for (const file3 of files) {
83937
- const fullPath = path90.join(opts.workspace, file3);
84906
+ const fullPath = path91.join(opts.workspace, file3);
83938
84907
  if (!validatePathForRead2(fullPath, opts.workspace)) {
83939
84908
  continue;
83940
84909
  }
83941
84910
  let stats;
83942
84911
  try {
83943
- stats = fs75.statSync(fullPath);
84912
+ stats = fs76.statSync(fullPath);
83944
84913
  if (stats.size > MAX_FILE_SIZE_BYTES10) {
83945
84914
  continue;
83946
84915
  }
@@ -83949,7 +84918,7 @@ async function fallbackSearch(opts) {
83949
84918
  }
83950
84919
  let content;
83951
84920
  try {
83952
- content = fs75.readFileSync(fullPath, "utf-8");
84921
+ content = fs76.readFileSync(fullPath, "utf-8");
83953
84922
  } catch {
83954
84923
  continue;
83955
84924
  }
@@ -84061,7 +85030,7 @@ var search = createSwarmTool({
84061
85030
  message: "Exclude pattern contains invalid Windows-specific sequence"
84062
85031
  }, null, 2);
84063
85032
  }
84064
- if (!fs75.existsSync(directory)) {
85033
+ if (!fs76.existsSync(directory)) {
84065
85034
  return JSON.stringify({
84066
85035
  error: true,
84067
85036
  type: "unknown",
@@ -84129,7 +85098,8 @@ async function executeSetQaGates(args2, directory) {
84129
85098
  "critic_pre_plan",
84130
85099
  "hallucination_guard",
84131
85100
  "sast_enabled",
84132
- "mutation_test"
85101
+ "mutation_test",
85102
+ "council_general_review"
84133
85103
  ]) {
84134
85104
  if (args2[key] !== undefined)
84135
85105
  partial3[key] = args2[key];
@@ -84175,6 +85145,7 @@ var set_qa_gates = createSwarmTool({
84175
85145
  hallucination_guard: tool.schema.boolean().optional().describe("Enable hallucination_guard checks on plan and implementation claims."),
84176
85146
  sast_enabled: tool.schema.boolean().optional().describe("Enable SAST scanning as a required QA gate."),
84177
85147
  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."),
85148
+ 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
85149
  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
85150
  },
84180
85151
  execute: async (args2, directory) => {
@@ -84186,8 +85157,8 @@ var set_qa_gates = createSwarmTool({
84186
85157
  init_tool();
84187
85158
  init_path_security();
84188
85159
  init_create_tool();
84189
- import * as fs76 from "fs";
84190
- import * as path91 from "path";
85160
+ import * as fs77 from "fs";
85161
+ import * as path92 from "path";
84191
85162
  var WINDOWS_RESERVED_NAMES4 = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|:|$)/i;
84192
85163
  function containsWindowsAttacks4(str) {
84193
85164
  if (/:[^\\/]/.test(str))
@@ -84201,14 +85172,14 @@ function containsWindowsAttacks4(str) {
84201
85172
  }
84202
85173
  function isPathInWorkspace4(filePath, workspace) {
84203
85174
  try {
84204
- const resolvedPath = path91.resolve(workspace, filePath);
84205
- if (!fs76.existsSync(resolvedPath)) {
85175
+ const resolvedPath = path92.resolve(workspace, filePath);
85176
+ if (!fs77.existsSync(resolvedPath)) {
84206
85177
  return true;
84207
85178
  }
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)) {
85179
+ const realWorkspace = fs77.realpathSync(workspace);
85180
+ const realResolvedPath = fs77.realpathSync(resolvedPath);
85181
+ const relativePath = path92.relative(realWorkspace, realResolvedPath);
85182
+ if (relativePath.startsWith("..") || path92.isAbsolute(relativePath)) {
84212
85183
  return false;
84213
85184
  }
84214
85185
  return true;
@@ -84380,7 +85351,7 @@ var suggestPatch = createSwarmTool({
84380
85351
  message: "changes cannot be empty"
84381
85352
  }, null, 2);
84382
85353
  }
84383
- if (!fs76.existsSync(directory)) {
85354
+ if (!fs77.existsSync(directory)) {
84384
85355
  return JSON.stringify({
84385
85356
  success: false,
84386
85357
  error: true,
@@ -84416,8 +85387,8 @@ var suggestPatch = createSwarmTool({
84416
85387
  });
84417
85388
  continue;
84418
85389
  }
84419
- const fullPath = path91.resolve(directory, change.file);
84420
- if (!fs76.existsSync(fullPath)) {
85390
+ const fullPath = path92.resolve(directory, change.file);
85391
+ if (!fs77.existsSync(fullPath)) {
84421
85392
  errors5.push({
84422
85393
  success: false,
84423
85394
  error: true,
@@ -84431,7 +85402,7 @@ var suggestPatch = createSwarmTool({
84431
85402
  }
84432
85403
  let content;
84433
85404
  try {
84434
- content = fs76.readFileSync(fullPath, "utf-8");
85405
+ content = fs77.readFileSync(fullPath, "utf-8");
84435
85406
  } catch (err3) {
84436
85407
  errors5.push({
84437
85408
  success: false,
@@ -84665,8 +85636,8 @@ var generate_mutants = createSwarmTool({
84665
85636
  // src/tools/lint-spec.ts
84666
85637
  init_spec_schema();
84667
85638
  init_create_tool();
84668
- import * as fs77 from "fs";
84669
- import * as path92 from "path";
85639
+ import * as fs78 from "fs";
85640
+ import * as path93 from "path";
84670
85641
  var SPEC_FILE_NAME = "spec.md";
84671
85642
  var SWARM_DIR2 = ".swarm";
84672
85643
  var OBLIGATION_KEYWORDS2 = ["MUST", "SHALL", "SHOULD", "MAY"];
@@ -84719,8 +85690,8 @@ var lint_spec = createSwarmTool({
84719
85690
  async execute(_args, directory) {
84720
85691
  const errors5 = [];
84721
85692
  const warnings = [];
84722
- const specPath = path92.join(directory, SWARM_DIR2, SPEC_FILE_NAME);
84723
- if (!fs77.existsSync(specPath)) {
85693
+ const specPath = path93.join(directory, SWARM_DIR2, SPEC_FILE_NAME);
85694
+ if (!fs78.existsSync(specPath)) {
84724
85695
  const result2 = {
84725
85696
  valid: false,
84726
85697
  specMtime: null,
@@ -84739,12 +85710,12 @@ var lint_spec = createSwarmTool({
84739
85710
  }
84740
85711
  let specMtime = null;
84741
85712
  try {
84742
- const stats = fs77.statSync(specPath);
85713
+ const stats = fs78.statSync(specPath);
84743
85714
  specMtime = stats.mtime.toISOString();
84744
85715
  } catch {}
84745
85716
  let content;
84746
85717
  try {
84747
- content = fs77.readFileSync(specPath, "utf-8");
85718
+ content = fs78.readFileSync(specPath, "utf-8");
84748
85719
  } catch (e) {
84749
85720
  const result2 = {
84750
85721
  valid: false,
@@ -84789,13 +85760,13 @@ var lint_spec = createSwarmTool({
84789
85760
  });
84790
85761
  // src/tools/mutation-test.ts
84791
85762
  init_dist();
84792
- import * as fs78 from "fs";
84793
- import * as path94 from "path";
85763
+ import * as fs79 from "fs";
85764
+ import * as path95 from "path";
84794
85765
 
84795
85766
  // src/mutation/engine.ts
84796
85767
  import { spawnSync as spawnSync3 } from "child_process";
84797
85768
  import { unlinkSync as unlinkSync12, writeFileSync as writeFileSync18 } from "fs";
84798
- import * as path93 from "path";
85769
+ import * as path94 from "path";
84799
85770
 
84800
85771
  // src/mutation/equivalence.ts
84801
85772
  function isStaticallyEquivalent(originalCode, mutatedCode) {
@@ -84930,7 +85901,7 @@ async function executeMutation(patch, testCommand, _testFiles, workingDir) {
84930
85901
  let patchFile;
84931
85902
  try {
84932
85903
  const safeId2 = patch.id.replace(/[^a-zA-Z0-9_-]/g, "_");
84933
- patchFile = path93.join(workingDir, `.mutation_patch_${safeId2}.diff`);
85904
+ patchFile = path94.join(workingDir, `.mutation_patch_${safeId2}.diff`);
84934
85905
  try {
84935
85906
  writeFileSync18(patchFile, patch.patch);
84936
85907
  } catch (writeErr) {
@@ -85324,8 +86295,8 @@ var mutation_test = createSwarmTool({
85324
86295
  ];
85325
86296
  for (const filePath of uniquePaths) {
85326
86297
  try {
85327
- const resolvedPath = path94.resolve(cwd, filePath);
85328
- sourceFiles.set(filePath, fs78.readFileSync(resolvedPath, "utf-8"));
86298
+ const resolvedPath = path95.resolve(cwd, filePath);
86299
+ sourceFiles.set(filePath, fs79.readFileSync(resolvedPath, "utf-8"));
85329
86300
  } catch {}
85330
86301
  }
85331
86302
  const report = await executeMutationSuite(typedArgs.patches, typedArgs.test_command, typedArgs.files, cwd, undefined, undefined, sourceFiles.size > 0 ? sourceFiles : undefined);
@@ -85343,8 +86314,8 @@ var mutation_test = createSwarmTool({
85343
86314
  init_dist();
85344
86315
  init_manager2();
85345
86316
  init_detector();
85346
- import * as fs79 from "fs";
85347
- import * as path95 from "path";
86317
+ import * as fs80 from "fs";
86318
+ import * as path96 from "path";
85348
86319
  init_create_tool();
85349
86320
  var MAX_FILE_SIZE2 = 2 * 1024 * 1024;
85350
86321
  var BINARY_CHECK_BYTES = 8192;
@@ -85410,7 +86381,7 @@ async function syntaxCheck(input, directory, config3) {
85410
86381
  if (languages?.length) {
85411
86382
  const lowerLangs = languages.map((l) => l.toLowerCase());
85412
86383
  filesToCheck = filesToCheck.filter((file3) => {
85413
- const ext = path95.extname(file3.path).toLowerCase();
86384
+ const ext = path96.extname(file3.path).toLowerCase();
85414
86385
  const langDef = getLanguageForExtension(ext);
85415
86386
  const fileProfile = getProfileForFile(file3.path);
85416
86387
  const langId = fileProfile?.id || langDef?.id;
@@ -85423,7 +86394,7 @@ async function syntaxCheck(input, directory, config3) {
85423
86394
  let skippedCount = 0;
85424
86395
  for (const fileInfo of filesToCheck) {
85425
86396
  const { path: filePath } = fileInfo;
85426
- const fullPath = path95.isAbsolute(filePath) ? filePath : path95.join(directory, filePath);
86397
+ const fullPath = path96.isAbsolute(filePath) ? filePath : path96.join(directory, filePath);
85427
86398
  const result = {
85428
86399
  path: filePath,
85429
86400
  language: "",
@@ -85453,7 +86424,7 @@ async function syntaxCheck(input, directory, config3) {
85453
86424
  }
85454
86425
  let content;
85455
86426
  try {
85456
- content = fs79.readFileSync(fullPath, "utf8");
86427
+ content = fs80.readFileSync(fullPath, "utf8");
85457
86428
  } catch {
85458
86429
  result.skipped_reason = "file_read_error";
85459
86430
  skippedCount++;
@@ -85472,7 +86443,7 @@ async function syntaxCheck(input, directory, config3) {
85472
86443
  results.push(result);
85473
86444
  continue;
85474
86445
  }
85475
- const ext = path95.extname(filePath).toLowerCase();
86446
+ const ext = path96.extname(filePath).toLowerCase();
85476
86447
  const langDef = getLanguageForExtension(ext);
85477
86448
  result.language = profile?.id || langDef?.id || "unknown";
85478
86449
  const errors5 = extractSyntaxErrors(parser, content);
@@ -85564,8 +86535,8 @@ init_dist();
85564
86535
  init_utils();
85565
86536
  init_create_tool();
85566
86537
  init_path_security();
85567
- import * as fs80 from "fs";
85568
- import * as path96 from "path";
86538
+ import * as fs81 from "fs";
86539
+ import * as path97 from "path";
85569
86540
  var MAX_TEXT_LENGTH = 200;
85570
86541
  var MAX_FILE_SIZE_BYTES11 = 1024 * 1024;
85571
86542
  var SUPPORTED_EXTENSIONS4 = new Set([
@@ -85631,9 +86602,9 @@ function validatePathsInput(paths, cwd) {
85631
86602
  return { error: "paths contains path traversal", resolvedPath: null };
85632
86603
  }
85633
86604
  try {
85634
- const resolvedPath = path96.resolve(paths);
85635
- const normalizedCwd = path96.resolve(cwd);
85636
- const normalizedResolved = path96.resolve(resolvedPath);
86605
+ const resolvedPath = path97.resolve(paths);
86606
+ const normalizedCwd = path97.resolve(cwd);
86607
+ const normalizedResolved = path97.resolve(resolvedPath);
85637
86608
  if (!normalizedResolved.startsWith(normalizedCwd)) {
85638
86609
  return {
85639
86610
  error: "paths must be within the current working directory",
@@ -85649,13 +86620,13 @@ function validatePathsInput(paths, cwd) {
85649
86620
  }
85650
86621
  }
85651
86622
  function isSupportedExtension(filePath) {
85652
- const ext = path96.extname(filePath).toLowerCase();
86623
+ const ext = path97.extname(filePath).toLowerCase();
85653
86624
  return SUPPORTED_EXTENSIONS4.has(ext);
85654
86625
  }
85655
86626
  function findSourceFiles4(dir, files = []) {
85656
86627
  let entries;
85657
86628
  try {
85658
- entries = fs80.readdirSync(dir);
86629
+ entries = fs81.readdirSync(dir);
85659
86630
  } catch {
85660
86631
  return files;
85661
86632
  }
@@ -85664,10 +86635,10 @@ function findSourceFiles4(dir, files = []) {
85664
86635
  if (SKIP_DIRECTORIES5.has(entry)) {
85665
86636
  continue;
85666
86637
  }
85667
- const fullPath = path96.join(dir, entry);
86638
+ const fullPath = path97.join(dir, entry);
85668
86639
  let stat4;
85669
86640
  try {
85670
- stat4 = fs80.statSync(fullPath);
86641
+ stat4 = fs81.statSync(fullPath);
85671
86642
  } catch {
85672
86643
  continue;
85673
86644
  }
@@ -85760,7 +86731,7 @@ var todo_extract = createSwarmTool({
85760
86731
  return JSON.stringify(errorResult, null, 2);
85761
86732
  }
85762
86733
  const scanPath = resolvedPath;
85763
- if (!fs80.existsSync(scanPath)) {
86734
+ if (!fs81.existsSync(scanPath)) {
85764
86735
  const errorResult = {
85765
86736
  error: `path not found: ${pathsInput}`,
85766
86737
  total: 0,
@@ -85770,13 +86741,13 @@ var todo_extract = createSwarmTool({
85770
86741
  return JSON.stringify(errorResult, null, 2);
85771
86742
  }
85772
86743
  const filesToScan = [];
85773
- const stat4 = fs80.statSync(scanPath);
86744
+ const stat4 = fs81.statSync(scanPath);
85774
86745
  if (stat4.isFile()) {
85775
86746
  if (isSupportedExtension(scanPath)) {
85776
86747
  filesToScan.push(scanPath);
85777
86748
  } else {
85778
86749
  const errorResult = {
85779
- error: `unsupported file extension: ${path96.extname(scanPath)}`,
86750
+ error: `unsupported file extension: ${path97.extname(scanPath)}`,
85780
86751
  total: 0,
85781
86752
  byPriority: { high: 0, medium: 0, low: 0 },
85782
86753
  entries: []
@@ -85789,11 +86760,11 @@ var todo_extract = createSwarmTool({
85789
86760
  const allEntries = [];
85790
86761
  for (const filePath of filesToScan) {
85791
86762
  try {
85792
- const fileStat = fs80.statSync(filePath);
86763
+ const fileStat = fs81.statSync(filePath);
85793
86764
  if (fileStat.size > MAX_FILE_SIZE_BYTES11) {
85794
86765
  continue;
85795
86766
  }
85796
- const content = fs80.readFileSync(filePath, "utf-8");
86767
+ const content = fs81.readFileSync(filePath, "utf-8");
85797
86768
  const entries = parseTodoComments(content, filePath, tagsSet);
85798
86769
  allEntries.push(...entries);
85799
86770
  } catch {}
@@ -85823,18 +86794,18 @@ init_tool();
85823
86794
  init_loader();
85824
86795
  init_schema();
85825
86796
  init_gate_evidence();
85826
- import * as fs82 from "fs";
85827
- import * as path98 from "path";
86797
+ import * as fs83 from "fs";
86798
+ import * as path99 from "path";
85828
86799
 
85829
86800
  // src/hooks/diff-scope.ts
85830
- import * as fs81 from "fs";
85831
- import * as path97 from "path";
86801
+ import * as fs82 from "fs";
86802
+ import * as path98 from "path";
85832
86803
  function getDeclaredScope(taskId, directory) {
85833
86804
  try {
85834
- const planPath = path97.join(directory, ".swarm", "plan.json");
85835
- if (!fs81.existsSync(planPath))
86805
+ const planPath = path98.join(directory, ".swarm", "plan.json");
86806
+ if (!fs82.existsSync(planPath))
85836
86807
  return null;
85837
- const raw = fs81.readFileSync(planPath, "utf-8");
86808
+ const raw = fs82.readFileSync(planPath, "utf-8");
85838
86809
  const plan = JSON.parse(raw);
85839
86810
  for (const phase of plan.phases ?? []) {
85840
86811
  for (const task of phase.tasks ?? []) {
@@ -85950,7 +86921,7 @@ var TIER_3_PATTERNS = [
85950
86921
  ];
85951
86922
  function matchesTier3Pattern(files) {
85952
86923
  for (const file3 of files) {
85953
- const fileName = path98.basename(file3);
86924
+ const fileName = path99.basename(file3);
85954
86925
  for (const pattern of TIER_3_PATTERNS) {
85955
86926
  if (pattern.test(fileName)) {
85956
86927
  return true;
@@ -85964,8 +86935,8 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
85964
86935
  if (hasActiveTurboMode()) {
85965
86936
  const resolvedDir2 = workingDirectory;
85966
86937
  try {
85967
- const planPath = path98.join(resolvedDir2, ".swarm", "plan.json");
85968
- const planRaw = fs82.readFileSync(planPath, "utf-8");
86938
+ const planPath = path99.join(resolvedDir2, ".swarm", "plan.json");
86939
+ const planRaw = fs83.readFileSync(planPath, "utf-8");
85969
86940
  const plan = JSON.parse(planRaw);
85970
86941
  for (const planPhase of plan.phases ?? []) {
85971
86942
  for (const task of planPhase.tasks ?? []) {
@@ -86034,8 +87005,8 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
86034
87005
  }
86035
87006
  try {
86036
87007
  const resolvedDir2 = workingDirectory;
86037
- const planPath = path98.join(resolvedDir2, ".swarm", "plan.json");
86038
- const planRaw = fs82.readFileSync(planPath, "utf-8");
87008
+ const planPath = path99.join(resolvedDir2, ".swarm", "plan.json");
87009
+ const planRaw = fs83.readFileSync(planPath, "utf-8");
86039
87010
  const plan = JSON.parse(planRaw);
86040
87011
  for (const planPhase of plan.phases ?? []) {
86041
87012
  for (const task of planPhase.tasks ?? []) {
@@ -86259,8 +87230,8 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
86259
87230
  };
86260
87231
  }
86261
87232
  }
86262
- normalizedDir = path98.normalize(args2.working_directory);
86263
- const pathParts = normalizedDir.split(path98.sep);
87233
+ normalizedDir = path99.normalize(args2.working_directory);
87234
+ const pathParts = normalizedDir.split(path99.sep);
86264
87235
  if (pathParts.includes("..")) {
86265
87236
  return {
86266
87237
  success: false,
@@ -86270,11 +87241,11 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
86270
87241
  ]
86271
87242
  };
86272
87243
  }
86273
- const resolvedDir = path98.resolve(normalizedDir);
87244
+ const resolvedDir = path99.resolve(normalizedDir);
86274
87245
  try {
86275
- const realPath = fs82.realpathSync(resolvedDir);
86276
- const planPath = path98.join(realPath, ".swarm", "plan.json");
86277
- if (!fs82.existsSync(planPath)) {
87246
+ const realPath = fs83.realpathSync(resolvedDir);
87247
+ const planPath = path99.join(realPath, ".swarm", "plan.json");
87248
+ if (!fs83.existsSync(planPath)) {
86278
87249
  return {
86279
87250
  success: false,
86280
87251
  message: `Invalid working_directory: plan not found in "${realPath}"`,
@@ -86305,22 +87276,22 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
86305
87276
  }
86306
87277
  if (args2.status === "in_progress") {
86307
87278
  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");
87279
+ const evidencePath = path99.join(directory, ".swarm", "evidence", `${args2.task_id}.json`);
87280
+ fs83.mkdirSync(path99.dirname(evidencePath), { recursive: true });
87281
+ const fd = fs83.openSync(evidencePath, "wx");
86311
87282
  let writeOk = false;
86312
87283
  try {
86313
- fs82.writeSync(fd, JSON.stringify({
87284
+ fs83.writeSync(fd, JSON.stringify({
86314
87285
  taskId: args2.task_id,
86315
87286
  required_gates: ["reviewer", "test_engineer"],
86316
87287
  gates: {}
86317
87288
  }, null, 2));
86318
87289
  writeOk = true;
86319
87290
  } finally {
86320
- fs82.closeSync(fd);
87291
+ fs83.closeSync(fd);
86321
87292
  if (!writeOk) {
86322
87293
  try {
86323
- fs82.unlinkSync(evidencePath);
87294
+ fs83.unlinkSync(evidencePath);
86324
87295
  } catch {}
86325
87296
  }
86326
87297
  }
@@ -86330,8 +87301,8 @@ async function executeUpdateTaskStatus(args2, fallbackDir) {
86330
87301
  recoverTaskStateFromDelegations(args2.task_id);
86331
87302
  let phaseRequiresReviewer = true;
86332
87303
  try {
86333
- const planPath = path98.join(directory, ".swarm", "plan.json");
86334
- const planRaw = fs82.readFileSync(planPath, "utf-8");
87304
+ const planPath = path99.join(directory, ".swarm", "plan.json");
87305
+ const planRaw = fs83.readFileSync(planPath, "utf-8");
86335
87306
  const plan = JSON.parse(planRaw);
86336
87307
  const taskPhase = plan.phases.find((p) => p.tasks.some((t) => t.id === args2.task_id));
86337
87308
  if (taskPhase?.required_agents && !taskPhase.required_agents.includes("reviewer")) {
@@ -86433,6 +87404,216 @@ var update_task_status = createSwarmTool({
86433
87404
  return JSON.stringify(await executeUpdateTaskStatus(args2, _directory), null, 2);
86434
87405
  }
86435
87406
  });
87407
+ // src/tools/web-search.ts
87408
+ init_dist();
87409
+ init_zod();
87410
+ init_loader();
87411
+
87412
+ // src/council/web-search-provider.ts
87413
+ class WebSearchError extends Error {
87414
+ cause;
87415
+ constructor(message, cause) {
87416
+ super(message);
87417
+ this.cause = cause;
87418
+ this.name = "WebSearchError";
87419
+ }
87420
+ }
87421
+
87422
+ class WebSearchConfigError extends Error {
87423
+ constructor(message) {
87424
+ super(message);
87425
+ this.name = "WebSearchConfigError";
87426
+ }
87427
+ }
87428
+
87429
+ class TavilyProvider {
87430
+ apiKey;
87431
+ constructor(apiKey) {
87432
+ this.apiKey = apiKey;
87433
+ }
87434
+ async search(query, maxResults) {
87435
+ let response;
87436
+ try {
87437
+ response = await fetch("https://api.tavily.com/search", {
87438
+ method: "POST",
87439
+ headers: { "Content-Type": "application/json" },
87440
+ body: JSON.stringify({
87441
+ api_key: this.apiKey,
87442
+ query,
87443
+ max_results: maxResults,
87444
+ search_depth: "advanced"
87445
+ })
87446
+ });
87447
+ } catch (err3) {
87448
+ throw new WebSearchError(`Tavily network error for query "${query}"`, err3);
87449
+ }
87450
+ if (!response.ok) {
87451
+ throw new WebSearchError(`Tavily HTTP ${response.status} for query "${query}"`);
87452
+ }
87453
+ let body2;
87454
+ try {
87455
+ body2 = await response.json();
87456
+ } catch (err3) {
87457
+ throw new WebSearchError("Tavily returned non-JSON response", err3);
87458
+ }
87459
+ const results = body2?.results;
87460
+ if (!Array.isArray(results)) {
87461
+ return [];
87462
+ }
87463
+ return results.filter((r) => typeof r?.title === "string" && typeof r?.url === "string" && typeof r?.content === "string").map((r) => ({
87464
+ title: r.title,
87465
+ url: r.url,
87466
+ snippet: r.content,
87467
+ query
87468
+ }));
87469
+ }
87470
+ }
87471
+
87472
+ class BraveProvider {
87473
+ apiKey;
87474
+ constructor(apiKey) {
87475
+ this.apiKey = apiKey;
87476
+ }
87477
+ async search(query, maxResults) {
87478
+ const url3 = new URL("https://api.search.brave.com/res/v1/web/search");
87479
+ url3.searchParams.set("q", query);
87480
+ url3.searchParams.set("count", String(maxResults));
87481
+ let response;
87482
+ try {
87483
+ response = await fetch(url3.toString(), {
87484
+ method: "GET",
87485
+ headers: {
87486
+ "X-Subscription-Token": this.apiKey,
87487
+ Accept: "application/json"
87488
+ }
87489
+ });
87490
+ } catch (err3) {
87491
+ throw new WebSearchError(`Brave network error for query "${query}"`, err3);
87492
+ }
87493
+ if (!response.ok) {
87494
+ throw new WebSearchError(`Brave HTTP ${response.status} for query "${query}"`);
87495
+ }
87496
+ let body2;
87497
+ try {
87498
+ body2 = await response.json();
87499
+ } catch (err3) {
87500
+ throw new WebSearchError("Brave returned non-JSON response", err3);
87501
+ }
87502
+ const results = body2?.web?.results;
87503
+ if (!Array.isArray(results)) {
87504
+ return [];
87505
+ }
87506
+ return results.filter((r) => typeof r?.title === "string" && typeof r?.url === "string" && typeof r?.description === "string").map((r) => ({
87507
+ title: r.title,
87508
+ url: r.url,
87509
+ snippet: r.description,
87510
+ query
87511
+ }));
87512
+ }
87513
+ }
87514
+ function resolveApiKey(provider, configKey) {
87515
+ if (configKey && configKey.length > 0) {
87516
+ return configKey;
87517
+ }
87518
+ const envName = provider === "tavily" ? "TAVILY_API_KEY" : "BRAVE_SEARCH_API_KEY";
87519
+ const fromEnv = process.env[envName];
87520
+ return fromEnv && fromEnv.length > 0 ? fromEnv : undefined;
87521
+ }
87522
+ function createWebSearchProvider(config3) {
87523
+ const apiKey = resolveApiKey(config3.searchProvider, config3.searchApiKey);
87524
+ if (!apiKey) {
87525
+ const envName = config3.searchProvider === "tavily" ? "TAVILY_API_KEY" : "BRAVE_SEARCH_API_KEY";
87526
+ throw new WebSearchConfigError(`No API key for search provider "${config3.searchProvider}". Set ` + `council.general.searchApiKey in opencode-swarm.json or export ${envName}.`);
87527
+ }
87528
+ switch (config3.searchProvider) {
87529
+ case "tavily":
87530
+ return new TavilyProvider(apiKey);
87531
+ case "brave":
87532
+ return new BraveProvider(apiKey);
87533
+ }
87534
+ }
87535
+
87536
+ // src/tools/web-search.ts
87537
+ init_create_tool();
87538
+ init_resolve_working_directory();
87539
+ var MAX_RESULTS_HARD_CAP = 10;
87540
+ var ArgsSchema4 = exports_external.object({
87541
+ query: exports_external.string().min(1).max(500),
87542
+ max_results: exports_external.number().int().min(1).max(20).optional(),
87543
+ working_directory: exports_external.string().optional()
87544
+ });
87545
+ var web_search = createSwarmTool({
87546
+ 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.",
87547
+ args: {
87548
+ query: tool.schema.string().min(1).max(500).describe("Search query string (1\u2013500 characters)."),
87549
+ 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.`),
87550
+ working_directory: tool.schema.string().optional().describe("Project root for config resolution. Optional.")
87551
+ },
87552
+ execute: async (args2, directory) => {
87553
+ const parsed = ArgsSchema4.safeParse(args2);
87554
+ if (!parsed.success) {
87555
+ const fail = {
87556
+ success: false,
87557
+ reason: "invalid_args",
87558
+ message: parsed.error.issues.map((i2) => `${i2.path.join(".")}: ${i2.message}`).join("; ")
87559
+ };
87560
+ return JSON.stringify(fail, null, 2);
87561
+ }
87562
+ const dirResult = resolveWorkingDirectory(parsed.data.working_directory, directory);
87563
+ if (!dirResult.success) {
87564
+ const fail = {
87565
+ success: false,
87566
+ reason: "invalid_working_directory",
87567
+ message: dirResult.message
87568
+ };
87569
+ return JSON.stringify(fail, null, 2);
87570
+ }
87571
+ const config3 = loadPluginConfig(dirResult.directory);
87572
+ const generalConfig = config3.council?.general;
87573
+ if (!generalConfig || generalConfig.enabled !== true) {
87574
+ const fail = {
87575
+ success: false,
87576
+ reason: "council_general_disabled",
87577
+ message: "web_search is disabled \u2014 set council.general.enabled: true in opencode-swarm.json."
87578
+ };
87579
+ return JSON.stringify(fail, null, 2);
87580
+ }
87581
+ const requested = parsed.data.max_results ?? generalConfig.maxSourcesPerMember;
87582
+ const maxResults = Math.min(requested, MAX_RESULTS_HARD_CAP);
87583
+ let provider;
87584
+ try {
87585
+ provider = createWebSearchProvider(generalConfig);
87586
+ } catch (err3) {
87587
+ const fail = {
87588
+ success: false,
87589
+ reason: err3 instanceof WebSearchConfigError ? "missing_api_key" : "provider_init_failed",
87590
+ message: err3 instanceof Error ? err3.message : String(err3)
87591
+ };
87592
+ return JSON.stringify(fail, null, 2);
87593
+ }
87594
+ try {
87595
+ const results = await provider.search(parsed.data.query, maxResults);
87596
+ const ok2 = {
87597
+ success: true,
87598
+ query: parsed.data.query,
87599
+ totalResults: results.length,
87600
+ results: results.map(({ title, url: url3, snippet }) => ({
87601
+ title,
87602
+ url: url3,
87603
+ snippet
87604
+ }))
87605
+ };
87606
+ return JSON.stringify(ok2, null, 2);
87607
+ } catch (err3) {
87608
+ const fail = {
87609
+ success: false,
87610
+ reason: err3 instanceof WebSearchError ? "search_failed" : "unknown",
87611
+ message: err3 instanceof Error ? err3.message : String(err3)
87612
+ };
87613
+ return JSON.stringify(fail, null, 2);
87614
+ }
87615
+ }
87616
+ });
86436
87617
  // src/tools/write-drift-evidence.ts
86437
87618
  init_tool();
86438
87619
  init_qa_gate_profile();
@@ -86440,8 +87621,8 @@ init_utils2();
86440
87621
  init_ledger();
86441
87622
  init_manager();
86442
87623
  init_create_tool();
86443
- import fs83 from "fs";
86444
- import path99 from "path";
87624
+ import fs84 from "fs";
87625
+ import path100 from "path";
86445
87626
  function derivePlanId5(plan) {
86446
87627
  return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
86447
87628
  }
@@ -86492,7 +87673,7 @@ async function executeWriteDriftEvidence(args2, directory) {
86492
87673
  entries: [evidenceEntry]
86493
87674
  };
86494
87675
  const filename = "drift-verifier.json";
86495
- const relativePath = path99.join("evidence", String(phase), filename);
87676
+ const relativePath = path100.join("evidence", String(phase), filename);
86496
87677
  let validatedPath;
86497
87678
  try {
86498
87679
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -86503,12 +87684,12 @@ async function executeWriteDriftEvidence(args2, directory) {
86503
87684
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
86504
87685
  }, null, 2);
86505
87686
  }
86506
- const evidenceDir = path99.dirname(validatedPath);
87687
+ const evidenceDir = path100.dirname(validatedPath);
86507
87688
  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);
87689
+ await fs84.promises.mkdir(evidenceDir, { recursive: true });
87690
+ const tempPath = path100.join(evidenceDir, `.${filename}.tmp`);
87691
+ await fs84.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
87692
+ await fs84.promises.rename(tempPath, validatedPath);
86512
87693
  let snapshotInfo;
86513
87694
  let snapshotError;
86514
87695
  let qaProfileLocked;
@@ -86602,8 +87783,8 @@ var write_drift_evidence = createSwarmTool({
86602
87783
  init_tool();
86603
87784
  init_utils2();
86604
87785
  init_create_tool();
86605
- import fs84 from "fs";
86606
- import path100 from "path";
87786
+ import fs85 from "fs";
87787
+ import path101 from "path";
86607
87788
  function normalizeVerdict2(verdict) {
86608
87789
  switch (verdict) {
86609
87790
  case "APPROVED":
@@ -86651,7 +87832,7 @@ async function executeWriteHallucinationEvidence(args2, directory) {
86651
87832
  entries: [evidenceEntry]
86652
87833
  };
86653
87834
  const filename = "hallucination-guard.json";
86654
- const relativePath = path100.join("evidence", String(phase), filename);
87835
+ const relativePath = path101.join("evidence", String(phase), filename);
86655
87836
  let validatedPath;
86656
87837
  try {
86657
87838
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -86662,12 +87843,12 @@ async function executeWriteHallucinationEvidence(args2, directory) {
86662
87843
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
86663
87844
  }, null, 2);
86664
87845
  }
86665
- const evidenceDir = path100.dirname(validatedPath);
87846
+ const evidenceDir = path101.dirname(validatedPath);
86666
87847
  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);
87848
+ await fs85.promises.mkdir(evidenceDir, { recursive: true });
87849
+ const tempPath = path101.join(evidenceDir, `.${filename}.tmp`);
87850
+ await fs85.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
87851
+ await fs85.promises.rename(tempPath, validatedPath);
86671
87852
  return JSON.stringify({
86672
87853
  success: true,
86673
87854
  phase,
@@ -86713,8 +87894,8 @@ var write_hallucination_evidence = createSwarmTool({
86713
87894
  init_tool();
86714
87895
  init_utils2();
86715
87896
  init_create_tool();
86716
- import fs85 from "fs";
86717
- import path101 from "path";
87897
+ import fs86 from "fs";
87898
+ import path102 from "path";
86718
87899
  function normalizeVerdict3(verdict) {
86719
87900
  switch (verdict) {
86720
87901
  case "PASS":
@@ -86788,7 +87969,7 @@ async function executeWriteMutationEvidence(args2, directory) {
86788
87969
  entries: [evidenceEntry]
86789
87970
  };
86790
87971
  const filename = "mutation-gate.json";
86791
- const relativePath = path101.join("evidence", String(phase), filename);
87972
+ const relativePath = path102.join("evidence", String(phase), filename);
86792
87973
  let validatedPath;
86793
87974
  try {
86794
87975
  validatedPath = validateSwarmPath(directory, relativePath);
@@ -86799,12 +87980,12 @@ async function executeWriteMutationEvidence(args2, directory) {
86799
87980
  message: error93 instanceof Error ? error93.message : "Failed to validate path"
86800
87981
  }, null, 2);
86801
87982
  }
86802
- const evidenceDir = path101.dirname(validatedPath);
87983
+ const evidenceDir = path102.dirname(validatedPath);
86803
87984
  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);
87985
+ await fs86.promises.mkdir(evidenceDir, { recursive: true });
87986
+ const tempPath = path102.join(evidenceDir, `.${filename}.tmp`);
87987
+ await fs86.promises.writeFile(tempPath, JSON.stringify(evidenceContent, null, 2), "utf-8");
87988
+ await fs86.promises.rename(tempPath, validatedPath);
86808
87989
  return JSON.stringify({
86809
87990
  success: true,
86810
87991
  phase,
@@ -87022,7 +88203,7 @@ var OpenCodeSwarm = async (ctx) => {
87022
88203
  const { PreflightTriggerManager: PTM } = await Promise.resolve().then(() => (init_trigger(), exports_trigger));
87023
88204
  preflightTriggerManager = new PTM(automationConfig);
87024
88205
  const { AutomationStatusArtifact: ASA } = await Promise.resolve().then(() => (init_status_artifact(), exports_status_artifact));
87025
- const swarmDir = path102.resolve(ctx.directory, ".swarm");
88206
+ const swarmDir = path103.resolve(ctx.directory, ".swarm");
87026
88207
  statusArtifact = new ASA(swarmDir);
87027
88208
  statusArtifact.updateConfig(automationConfig.mode, automationConfig.capabilities);
87028
88209
  if (automationConfig.capabilities?.evidence_auto_summaries === true) {
@@ -87128,6 +88309,7 @@ var OpenCodeSwarm = async (ctx) => {
87128
88309
  completion_verify,
87129
88310
  complexity_hotspots,
87130
88311
  convene_council,
88312
+ convene_general_council,
87131
88313
  curator_analyze,
87132
88314
  declare_council_criteria,
87133
88315
  knowledge_add,
@@ -87174,6 +88356,7 @@ var OpenCodeSwarm = async (ctx) => {
87174
88356
  build_check,
87175
88357
  suggest_patch: suggestPatch,
87176
88358
  update_task_status,
88359
+ web_search,
87177
88360
  write_retro,
87178
88361
  write_drift_evidence,
87179
88362
  write_hallucination_evidence,