opencode-swarm 7.71.3 → 7.72.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
@@ -69,7 +69,7 @@ var package_default;
69
69
  var init_package = __esm(() => {
70
70
  package_default = {
71
71
  name: "opencode-swarm",
72
- version: "7.71.3",
72
+ version: "7.72.0",
73
73
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
74
74
  main: "dist/index.js",
75
75
  types: "dist/index.d.ts",
@@ -112,6 +112,7 @@ var init_package = __esm(() => {
112
112
  ".opencode/skills/pre-phase-briefing",
113
113
  ".opencode/skills/council",
114
114
  ".opencode/skills/deep-dive",
115
+ ".opencode/skills/deep-research",
115
116
  ".opencode/skills/codebase-review-swarm",
116
117
  ".opencode/skills/design-docs",
117
118
  ".opencode/skills/swarm-pr-review",
@@ -589,6 +590,10 @@ var init_tool_metadata = __esm(() => {
589
590
  description: "External web search (Tavily or Brave) for architect-driven council research, SME domain research, and skill-improver research. Returns titled results with snippets, URLs, normalized query metadata, temporal intent, freshness, and removed stale years. Config-gated on council.general.enabled in the resolved config: global ~/.config/opencode/opencode-swarm.json, then project .opencode/opencode-swarm.json overrides. Requires a search API key. Used by the architect in MODE: COUNCIL to gather a RESEARCH CONTEXT before dispatching council agents and by SME for opt-in external skill/source evaluation.",
590
591
  agents: ["architect", "sme", "skill_improver"]
591
592
  },
593
+ web_fetch: {
594
+ description: "Fetch the readable text of a single http(s) URL (architect-only). Returns decoded page text, document title, final URL after redirects, and an evidence reference. Reads primary sources that web_search only surfaces as snippets. Config-gated on council.general.enabled. Blocks private/loopback/link-local/metadata addresses (re-validated and re-pinned across redirects); enforces timeout and body size cap.",
595
+ agents: ["architect"]
596
+ },
592
597
  convene_general_council: {
593
598
  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, and a structured synthesis. Architect-only. Config-gated on council.general.enabled in the resolved config: global ~/.config/opencode/opencode-swarm.json, then project .opencode/opencode-swarm.json overrides.",
594
599
  agents: ["architect"]
@@ -17101,6 +17106,7 @@ var init_bundled_skills = __esm(() => {
17101
17106
  "pre-phase-briefing",
17102
17107
  "council",
17103
17108
  "deep-dive",
17109
+ "deep-research",
17104
17110
  "codebase-review-swarm",
17105
17111
  "design-docs",
17106
17112
  "swarm-pr-review",
@@ -69353,6 +69359,119 @@ var init_deep_dive = __esm(() => {
69353
69359
  ]);
69354
69360
  });
69355
69361
 
69362
+ // src/commands/deep-research.ts
69363
+ function sanitizeQuestion2(raw) {
69364
+ const collapsed = raw.replace(/\s+/g, " ").trim();
69365
+ const stripped = collapsed.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
69366
+ const normalized = stripped.replace(/\s+/g, " ").trim();
69367
+ if (normalized.length <= MAX_QUESTION_LEN2)
69368
+ return normalized;
69369
+ return `${normalized.slice(0, MAX_QUESTION_LEN2)}…`;
69370
+ }
69371
+ function isBoundedInteger(raw, min, max) {
69372
+ if (!raw || !/^\d+$/.test(raw))
69373
+ return false;
69374
+ const n = Number(raw);
69375
+ return Number.isInteger(n) && n >= min && n <= max;
69376
+ }
69377
+ function parseArgs4(args2) {
69378
+ const result = {
69379
+ depth: DEFAULT_DEPTH,
69380
+ maxResearchers: DEFAULT_MAX_RESEARCHERS,
69381
+ rounds: DEFAULT_ROUNDS,
69382
+ output: "report",
69383
+ rest: []
69384
+ };
69385
+ let i2 = 0;
69386
+ while (i2 < args2.length) {
69387
+ const token = args2[i2];
69388
+ if (token === "--depth") {
69389
+ if (i2 + 1 >= args2.length)
69390
+ return { ...result, error: `Flag "${token}" requires a value` };
69391
+ const value = args2[++i2];
69392
+ if (!DEPTHS.has(value)) {
69393
+ return {
69394
+ ...result,
69395
+ error: `Invalid depth "${value}". Must be one of: standard, exhaustive.`
69396
+ };
69397
+ }
69398
+ result.depth = value;
69399
+ } else if (token === "--max-researchers") {
69400
+ if (i2 + 1 >= args2.length)
69401
+ return { ...result, error: `Flag "${token}" requires a value` };
69402
+ const value = args2[++i2];
69403
+ if (!isBoundedInteger(value, 1, 6)) {
69404
+ return {
69405
+ ...result,
69406
+ error: `Invalid --max-researchers value "${value}". Must be an integer between 1 and 6.`
69407
+ };
69408
+ }
69409
+ result.maxResearchers = Number(value);
69410
+ result.maxResearchersExplicit = true;
69411
+ } else if (token === "--rounds") {
69412
+ if (i2 + 1 >= args2.length)
69413
+ return { ...result, error: `Flag "${token}" requires a value` };
69414
+ const value = args2[++i2];
69415
+ if (!isBoundedInteger(value, 1, 4)) {
69416
+ return {
69417
+ ...result,
69418
+ error: `Invalid --rounds value "${value}". Must be an integer between 1 and 4.`
69419
+ };
69420
+ }
69421
+ result.rounds = Number(value);
69422
+ result.roundsExplicit = true;
69423
+ } else if (token === "--brief") {
69424
+ result.output = "brief";
69425
+ } else if (token.startsWith("--")) {
69426
+ return { ...result, error: `Unknown flag "${token}"` };
69427
+ } else {
69428
+ result.rest.push(token);
69429
+ }
69430
+ i2++;
69431
+ }
69432
+ return result;
69433
+ }
69434
+ async function handleDeepResearchCommand(_directory, args2) {
69435
+ const parsed = parseArgs4(args2);
69436
+ if (parsed.error) {
69437
+ return `Error: ${parsed.error}
69438
+
69439
+ ${USAGE4}`;
69440
+ }
69441
+ const question = sanitizeQuestion2(parsed.rest.join(" "));
69442
+ if (!question) {
69443
+ return USAGE4;
69444
+ }
69445
+ if (parsed.depth === "exhaustive") {
69446
+ if (!parsed.maxResearchersExplicit)
69447
+ parsed.maxResearchers = EXHAUSTIVE_DEFAULT_MAX_RESEARCHERS;
69448
+ if (!parsed.roundsExplicit)
69449
+ parsed.rounds = EXHAUSTIVE_DEFAULT_ROUNDS;
69450
+ }
69451
+ return `[MODE: DEEP_RESEARCH depth=${parsed.depth} max_researchers=${parsed.maxResearchers} rounds=${parsed.rounds} output=${parsed.output}] ${question}`;
69452
+ }
69453
+ var MAX_QUESTION_LEN2 = 2000, DEPTHS, DEFAULT_DEPTH = "standard", DEFAULT_MAX_RESEARCHERS = 3, EXHAUSTIVE_DEFAULT_MAX_RESEARCHERS = 5, DEFAULT_ROUNDS = 2, EXHAUSTIVE_DEFAULT_ROUNDS = 3, USAGE4 = `Usage: /swarm deep-research <question> [--depth standard|exhaustive] [--max-researchers 1..6] [--rounds 1..4] [--brief]
69454
+
69455
+ Run a multi-source, fact-checked deep research pass and synthesize a cited report.
69456
+
69457
+ Examples:
69458
+ /swarm deep-research "What are the tradeoffs of WASM vs native plugins?"
69459
+ /swarm deep research "current state of deep research agents" --depth exhaustive
69460
+ /swarm deep-research "is Tavily or Brave better for our use case" --rounds 3 --brief
69461
+
69462
+ Flags:
69463
+ --depth <name> standard (focused) or exhaustive (broader fan-out)
69464
+ --max-researchers <N> parallel synthesis workers per round, 1..6
69465
+ --rounds <N> max iterative research rounds, 1..4
69466
+ --brief emit a short brief instead of a full cited report
69467
+
69468
+ Requires council.general.enabled: true and a configured search API key (Tavily or Brave)
69469
+ in the resolved config: global ~/.config/opencode/opencode-swarm.json, then project
69470
+ .opencode/opencode-swarm.json overrides.`;
69471
+ var init_deep_research = __esm(() => {
69472
+ DEPTHS = new Set(["standard", "exhaustive"]);
69473
+ });
69474
+
69356
69475
  // src/commands/design-docs.ts
69357
69476
  function sanitizeDescription(raw) {
69358
69477
  const collapsed = raw.replace(/\s+/g, " ").trim();
@@ -69370,7 +69489,7 @@ function cleanFlagValue(raw) {
69370
69489
  return null;
69371
69490
  return raw;
69372
69491
  }
69373
- function parseArgs4(args2) {
69492
+ function parseArgs5(args2) {
69374
69493
  const result = {
69375
69494
  out: "docs",
69376
69495
  lang: "auto",
@@ -69425,30 +69544,30 @@ function parseArgs4(args2) {
69425
69544
  return result;
69426
69545
  }
69427
69546
  async function handleDesignDocsCommand(directory, args2) {
69428
- const parsed = parseArgs4(args2);
69547
+ const parsed = parseArgs5(args2);
69429
69548
  if (parsed.error) {
69430
69549
  return `Error: ${parsed.error}
69431
69550
 
69432
- ${USAGE4}`;
69551
+ ${USAGE5}`;
69433
69552
  }
69434
69553
  try {
69435
69554
  const { config: config3 } = loadPluginConfigWithMeta(directory);
69436
69555
  if (config3.design_docs?.enabled !== true) {
69437
69556
  return "Error: design docs are disabled. Set `design_docs.enabled: true` in " + `opencode-swarm.json to enable the docs_design agent and this command.
69438
69557
 
69439
- ` + USAGE4;
69558
+ ` + USAGE5;
69440
69559
  }
69441
69560
  } catch (configErr) {
69442
69561
  console.warn(`[design-docs] Could not read opencode-swarm.json (${String(configErr)}). ` + "Falling through — the architect will abort if docs_design is not registered.");
69443
69562
  }
69444
69563
  const description = sanitizeDescription(parsed.rest.join(" "));
69445
69564
  if (!description && !parsed.update) {
69446
- return USAGE4;
69565
+ return USAGE5;
69447
69566
  }
69448
69567
  const header = `[MODE: DESIGN_DOCS out=${parsed.out} lang=${parsed.lang} update=${parsed.update}] ${description}`;
69449
69568
  return header.trimEnd();
69450
69569
  }
69451
- var MAX_DESC_LEN = 2000, USAGE4 = `Usage: /swarm design-docs <description> [--out <dir>] [--lang <name>] [--update]
69570
+ var MAX_DESC_LEN = 2000, USAGE5 = `Usage: /swarm design-docs <description> [--out <dir>] [--lang <name>] [--update]
69452
69571
 
69453
69572
  Generate or sync language-agnostic design docs for the project under build:
69454
69573
  <out>/domain.md, <out>/technical-spec.md, <out>/behavior-spec.md,
@@ -75204,7 +75323,7 @@ function validateAndSanitizeUrl2(rawUrl) {
75204
75323
  return { error: "Invalid URL format" };
75205
75324
  }
75206
75325
  }
75207
- function parseArgs5(args2) {
75326
+ function parseArgs6(args2) {
75208
75327
  const out2 = {
75209
75328
  plan: false,
75210
75329
  trace: false,
@@ -75278,24 +75397,24 @@ function detectGitRemote2() {
75278
75397
  }
75279
75398
  }
75280
75399
  function handleIssueCommand(_directory, args2) {
75281
- const parsed = parseArgs5(args2);
75400
+ const parsed = parseArgs6(args2);
75282
75401
  const rawInput = parsed.rest.join(" ").trim();
75283
75402
  if (!rawInput) {
75284
- return USAGE5;
75403
+ return USAGE6;
75285
75404
  }
75286
75405
  const isFullUrl = /^https?:\/\//i.test(rawInput);
75287
75406
  const issueInfo = parseIssueRef(isFullUrl ? sanitizeUrl2(rawInput) : rawInput);
75288
75407
  if (!issueInfo) {
75289
75408
  return `Error: Could not parse issue reference from "${rawInput}"
75290
75409
 
75291
- ${USAGE5}`;
75410
+ ${USAGE6}`;
75292
75411
  }
75293
75412
  const issueUrl = `https://github.com/${issueInfo.owner}/${issueInfo.repo}/issues/${issueInfo.number}`;
75294
75413
  const result = validateAndSanitizeUrl2(issueUrl);
75295
75414
  if ("error" in result) {
75296
75415
  return `Error: ${result.error}
75297
75416
 
75298
- ${USAGE5}`;
75417
+ ${USAGE6}`;
75299
75418
  }
75300
75419
  const flags2 = [];
75301
75420
  if (parsed.plan)
@@ -75307,10 +75426,10 @@ ${USAGE5}`;
75307
75426
  const flagsStr = flags2.length > 0 ? ` ${flags2.join(" ")}` : "";
75308
75427
  return `[MODE: ISSUE_INGEST issue="${result.sanitized}"${flagsStr}]`;
75309
75428
  }
75310
- var MAX_URL_LEN2 = 2048, USAGE5;
75429
+ var MAX_URL_LEN2 = 2048, USAGE6;
75311
75430
  var init_issue = __esm(() => {
75312
75431
  init_pr_ref();
75313
- USAGE5 = [
75432
+ USAGE6 = [
75314
75433
  "Usage: /swarm issue <url|owner/repo#N|N> [--plan] [--trace] [--no-repro]",
75315
75434
  "",
75316
75435
  "Ingest a GitHub issue into the swarm workflow.",
@@ -80579,7 +80698,7 @@ var init_pr_monitor_status = __esm(() => {
80579
80698
  });
80580
80699
 
80581
80700
  // src/commands/pr-review.ts
80582
- function parseArgs6(args2) {
80701
+ function parseArgs7(args2) {
80583
80702
  const out2 = { council: false, rest: [] };
80584
80703
  for (const token of args2) {
80585
80704
  if (token === "--council") {
@@ -80598,29 +80717,29 @@ function parseArgs6(args2) {
80598
80717
  return out2;
80599
80718
  }
80600
80719
  function handlePrReviewCommand(directory, args2) {
80601
- const parsed = parseArgs6(args2);
80720
+ const parsed = parseArgs7(args2);
80602
80721
  if (parsed.unknownFlag) {
80603
80722
  return `Error: Unknown flag "${parsed.unknownFlag}"
80604
80723
 
80605
- ${USAGE6}`;
80724
+ ${USAGE7}`;
80606
80725
  }
80607
80726
  const resolved = resolvePrCommandInput(parsed.rest, directory);
80608
80727
  if (resolved === null) {
80609
- return USAGE6;
80728
+ return USAGE7;
80610
80729
  }
80611
80730
  if ("error" in resolved) {
80612
80731
  return `Error: ${resolved.error}
80613
80732
 
80614
- ${USAGE6}`;
80733
+ ${USAGE7}`;
80615
80734
  }
80616
80735
  const councilFlag = parsed.council ? "council=true" : "council=false";
80617
80736
  const signal = `[MODE: PR_REVIEW pr="${resolved.prUrl}" ${councilFlag}]`;
80618
80737
  return resolved.instructions ? `${signal} ${resolved.instructions}` : signal;
80619
80738
  }
80620
- var USAGE6;
80739
+ var USAGE7;
80621
80740
  var init_pr_review = __esm(() => {
80622
80741
  init_pr_ref();
80623
- USAGE6 = [
80742
+ USAGE7 = [
80624
80743
  "Usage: /swarm pr-review <url|owner/repo#N|N> [--council] [instructions...]",
80625
80744
  "",
80626
80745
  "Run a full swarm PR review on a GitHub pull request.",
@@ -87896,7 +88015,7 @@ var init_rollback = __esm(() => {
87896
88015
  });
87897
88016
 
87898
88017
  // src/commands/sdd.ts
87899
- function parseArgs7(args2) {
88018
+ function parseArgs8(args2) {
87900
88019
  const parsed = { json: false, dryRun: false };
87901
88020
  for (let i2 = 0;i2 < args2.length; i2++) {
87902
88021
  const token = args2[i2];
@@ -87927,11 +88046,11 @@ function formatList(items) {
87927
88046
  `) : "- none";
87928
88047
  }
87929
88048
  async function handleSddStatusCommand(directory, args2) {
87930
- const parsed = parseArgs7(args2);
88049
+ const parsed = parseArgs8(args2);
87931
88050
  if (parsed.error)
87932
88051
  return `Error: ${parsed.error}
87933
88052
 
87934
- ${USAGE7}`;
88053
+ ${USAGE8}`;
87935
88054
  const status = loadSddStatusSync(directory);
87936
88055
  if (parsed.json)
87937
88056
  return JSON.stringify(status, null, 2);
@@ -87965,11 +88084,11 @@ ${formatList([
87965
88084
  `);
87966
88085
  }
87967
88086
  async function handleSddValidateCommand(directory, args2) {
87968
- const parsed = parseArgs7(args2);
88087
+ const parsed = parseArgs8(args2);
87969
88088
  if (parsed.error)
87970
88089
  return `Error: ${parsed.error}
87971
88090
 
87972
- ${USAGE7}`;
88091
+ ${USAGE8}`;
87973
88092
  const status = loadSddStatusSync(directory);
87974
88093
  const projection = buildOpenSpecProjectionSync(directory, {
87975
88094
  changeId: parsed.changeId
@@ -88007,11 +88126,11 @@ ${formatList(result.warnings)}` : ""
88007
88126
  `);
88008
88127
  }
88009
88128
  async function handleSddProjectCommand(directory, args2) {
88010
- const parsed = parseArgs7(args2);
88129
+ const parsed = parseArgs8(args2);
88011
88130
  if (parsed.error)
88012
88131
  return `Error: ${parsed.error}
88013
88132
 
88014
- ${USAGE7}`;
88133
+ ${USAGE8}`;
88015
88134
  const result = writeProjectedSpecSync(directory, {
88016
88135
  changeId: parsed.changeId,
88017
88136
  dryRun: parsed.dryRun
@@ -88031,7 +88150,7 @@ ${USAGE7}`;
88031
88150
  return [
88032
88151
  "SDD projection failed: no valid OpenSpec-compatible projection could be built.",
88033
88152
  "",
88034
- USAGE7
88153
+ USAGE8
88035
88154
  ].join(`
88036
88155
  `);
88037
88156
  }
@@ -88048,9 +88167,9 @@ ${formatList(result.projection.warnings)}` : ""
88048
88167
  `);
88049
88168
  }
88050
88169
  async function handleSddCommand(_directory, _args) {
88051
- return USAGE7;
88170
+ return USAGE8;
88052
88171
  }
88053
- var USAGE7 = `Usage:
88172
+ var USAGE8 = `Usage:
88054
88173
  /swarm sdd status [--json]
88055
88174
  /swarm sdd validate [--json] [--change <id>]
88056
88175
  /swarm sdd project [--dry-run] [--json] [--change <id>]
@@ -89556,6 +89675,7 @@ __export(exports_commands, {
89556
89675
  handleEvidenceCommand: () => handleEvidenceCommand,
89557
89676
  handleDoctorCommand: () => handleDoctorCommand,
89558
89677
  handleDiagnoseCommand: () => handleDiagnoseCommand,
89678
+ handleDeepResearchCommand: () => handleDeepResearchCommand,
89559
89679
  handleDeepDiveCommand: () => handleDeepDiveCommand,
89560
89680
  handleDarkMatterCommand: () => handleDarkMatterCommand,
89561
89681
  handleCurateCommand: () => handleCurateCommand,
@@ -89834,6 +89954,7 @@ var init_commands = __esm(() => {
89834
89954
  init_curate();
89835
89955
  init_dark_matter();
89836
89956
  init_deep_dive();
89957
+ init_deep_research();
89837
89958
  init_diagnose();
89838
89959
  init_doctor();
89839
89960
  init_evidence();
@@ -90066,6 +90187,7 @@ var init_registry = __esm(() => {
90066
90187
  init_curate();
90067
90188
  init_dark_matter();
90068
90189
  init_deep_dive();
90190
+ init_deep_research();
90069
90191
  init_design_docs();
90070
90192
  init_diagnose();
90071
90193
  init_doctor();
@@ -90461,6 +90583,20 @@ Subcommands:
90461
90583
  category: "agent",
90462
90584
  aliasOf: "deep-dive"
90463
90585
  },
90586
+ "deep-research": {
90587
+ handler: (ctx) => handleModeCommandWithBundledSkills(ctx, handleDeepResearchCommand),
90588
+ description: "Launch a multi-source, fact-checked deep research pass and synthesize a cited report [question]",
90589
+ args: "<question> [--depth standard|exhaustive] [--max-researchers 1..6] [--rounds 1..4] [--brief]",
90590
+ details: "Runs the orchestrator-worker deep-research protocol: the architect decomposes the question into subtopics, gathers evidence with web_search and web_fetch across up to N iterative rounds, dispatches parallel sme synthesis workers, verifies every claim against cited sources with dual reviewers, challenges high-stakes claims with the critic, and presents a cited report in chat. Read-only — does not mutate source code, delegate to coder, or call declare_scope. Requires council.general.enabled and a search API key.",
90591
+ category: "agent"
90592
+ },
90593
+ "deep research": {
90594
+ handler: (ctx) => handleModeCommandWithBundledSkills(ctx, handleDeepResearchCommand),
90595
+ description: "Alias for /swarm deep-research — launch a cited deep research pass",
90596
+ args: "<question> [--depth standard|exhaustive] [--max-researchers 1..6] [--rounds 1..4] [--brief]",
90597
+ category: "agent",
90598
+ aliasOf: "deep-research"
90599
+ },
90464
90600
  "codebase-review": {
90465
90601
  handler: (ctx) => handleModeCommandWithBundledSkills(ctx, handleCodebaseReviewCommand),
90466
90602
  description: "Launch codebase-review-swarm for a quote-grounded full-repo or large-subsystem audit",
@@ -91957,6 +92093,22 @@ HARD CONSTRAINTS (apply regardless of skill load success):
91957
92093
  - Explorers generate candidate findings only — reviewers verify or reject
91958
92094
  - Critics challenge only HIGH/CRITICAL findings — do NOT waste cycles on lower severity
91959
92095
 
92096
+ ### MODE: DEEP_RESEARCH
92097
+ Activates when: architect receives \`[MODE: DEEP_RESEARCH depth=X max_researchers=N rounds=N output=report|brief] <question>\` signal from the deep-research command handler.
92098
+
92099
+ Purpose: Orchestrator-worker deep research over external sources. Decompose the question into subtopics, gather evidence with \`web_search\` and \`web_fetch\` across up to \`rounds\` iterative rounds (re-planning gaps between rounds), dispatch parallel sme synthesis workers, verify every claim against cited sources with 2 reviewers, challenge high-stakes claims with the critic, and present a cited report in chat. This mode does NOT mutate source code, does NOT delegate to coder, and does NOT call declare_scope.
92100
+
92101
+ ACTION: Load skill file:.opencode/skills/deep-research/SKILL.md immediately and follow its protocol.
92102
+
92103
+ HARD CONSTRAINTS (apply regardless of skill load success):
92104
+ - Do NOT delegate to coder
92105
+ - Do NOT call declare_scope
92106
+ - Do NOT mutate source code or write any files outside .swarm/
92107
+ - You (architect) own \`web_search\` and \`web_fetch\`; sme workers receive gathered evidence in their dispatch message — do NOT expect sme to fetch
92108
+ - Every claim in the final report MUST cite a source from the gathered evidence; reviewers verify claim↔citation before a claim is reported
92109
+ - Critics challenge only high-stakes / contested claims — do NOT waste cycles on well-supported ones
92110
+ - If council.general.enabled is false or no search API key is configured, surface that and STOP — do not produce ungrounded research
92111
+
91960
92112
  ### MODE: CODEBASE_REVIEW
91961
92113
  Activates when: architect receives \`[MODE: CODEBASE_REVIEW mode=X output=X update_main=X allow_dirty=X tracks="..." continue_run="..."] scope="..."\` signal from the codebase-review command handler.
91962
92114
 
@@ -101144,7 +101296,7 @@ var init_design_doc_drift = __esm(() => {
101144
101296
  var exports_project_context = {};
101145
101297
  __export(exports_project_context, {
101146
101298
  buildProjectContext: () => buildProjectContext,
101147
- _internals: () => _internals103,
101299
+ _internals: () => _internals104,
101148
101300
  LANG_BACKEND_DETECTION_TIMEOUT_MS: () => LANG_BACKEND_DETECTION_TIMEOUT_MS
101149
101301
  });
101150
101302
  import * as fs136 from "node:fs";
@@ -101228,7 +101380,7 @@ function selectLintCommand(backend, directory) {
101228
101380
  return null;
101229
101381
  }
101230
101382
  async function buildProjectContext(directory) {
101231
- const backend = await _internals103.pickBackend(directory);
101383
+ const backend = await _internals104.pickBackend(directory);
101232
101384
  if (!backend)
101233
101385
  return null;
101234
101386
  const ctx = emptyProjectContext();
@@ -101267,17 +101419,17 @@ async function buildProjectContext(directory) {
101267
101419
  if (backend.prompts.reviewerChecklist.length > 0) {
101268
101420
  ctx.REVIEWER_CHECKLIST = bulletList(backend.prompts.reviewerChecklist);
101269
101421
  }
101270
- const profiles = _internals103.pickedProfiles(directory);
101422
+ const profiles = _internals104.pickedProfiles(directory);
101271
101423
  if (profiles.length > 1) {
101272
101424
  ctx.PROJECT_CONTEXT_SECONDARY_LANGUAGES = profiles.slice(1).map((p) => p.id).join(", ");
101273
101425
  }
101274
101426
  return ctx;
101275
101427
  }
101276
- var LANG_BACKEND_DETECTION_TIMEOUT_MS = 300, _internals103;
101428
+ var LANG_BACKEND_DETECTION_TIMEOUT_MS = 300, _internals104;
101277
101429
  var init_project_context = __esm(() => {
101278
101430
  init_dispatch();
101279
101431
  init_framework_detector();
101280
- _internals103 = {
101432
+ _internals104 = {
101281
101433
  pickBackend,
101282
101434
  pickedProfiles
101283
101435
  };
@@ -140503,6 +140655,696 @@ var update_task_status = createSwarmTool({
140503
140655
  }
140504
140656
  });
140505
140657
 
140658
+ // src/tools/web-fetch.ts
140659
+ init_zod();
140660
+ init_loader();
140661
+ import { lookup } from "node:dns/promises";
140662
+ import * as http from "node:http";
140663
+ import * as https from "node:https";
140664
+ import { isIP } from "node:net";
140665
+ import { Readable } from "node:stream";
140666
+ import * as zlib from "node:zlib";
140667
+
140668
+ // src/evidence/documents.ts
140669
+ init_utils2();
140670
+ init_redaction();
140671
+ import { createHash as createHash16 } from "node:crypto";
140672
+ import { appendFile as appendFile17, mkdir as mkdir31 } from "node:fs/promises";
140673
+ import * as path188 from "node:path";
140674
+ var EVIDENCE_CACHE_FILE = "evidence-cache/documents.jsonl";
140675
+ var MAX_EVIDENCE_TEXT_LENGTH = 4000;
140676
+ async function writeEvidenceDocuments(directory, inputs, now = () => new Date) {
140677
+ const filePath = validateSwarmPath(directory, EVIDENCE_CACHE_FILE);
140678
+ const capturedAt = now().toISOString();
140679
+ const records = inputs.map((input) => createEvidenceDocumentRecord(input, capturedAt)).filter((record3) => record3 !== null);
140680
+ if (records.length > 0) {
140681
+ await mkdir31(path188.dirname(filePath), { recursive: true });
140682
+ await appendFile17(filePath, `${records.map((record3) => JSON.stringify(record3)).join(`
140683
+ `)}
140684
+ `, "utf-8");
140685
+ }
140686
+ return {
140687
+ path: ".swarm/evidence-cache/documents.jsonl",
140688
+ records,
140689
+ refs: records.map((record3) => record3.ref)
140690
+ };
140691
+ }
140692
+ function createEvidenceDocumentRecord(input, defaultCapturedAt) {
140693
+ const text = normalizeEvidenceText(input.text ?? input.snippet ?? "");
140694
+ if (!text)
140695
+ return null;
140696
+ const capturedAt = input.capturedAt ?? defaultCapturedAt;
140697
+ const base = {
140698
+ sourceType: input.sourceType,
140699
+ query: normalizeOptional(input.query),
140700
+ title: normalizeOptional(input.title),
140701
+ url: normalizeOptional(input.url),
140702
+ text
140703
+ };
140704
+ const id = createEvidenceDocumentId(base);
140705
+ return {
140706
+ id,
140707
+ ref: `evidence-cache:${id}`,
140708
+ ...base,
140709
+ capturedAt,
140710
+ createdBy: normalizeOptional(input.createdBy),
140711
+ metadata: input.metadata ?? {}
140712
+ };
140713
+ }
140714
+ function createEvidenceDocumentId(input) {
140715
+ const hash4 = createHash16("sha256").update([
140716
+ input.sourceType,
140717
+ input.query ?? "",
140718
+ input.title ?? "",
140719
+ input.url ?? "",
140720
+ input.text
140721
+ ].join(`
140722
+ `)).digest("hex");
140723
+ return `evd_${hash4.slice(0, 16)}`;
140724
+ }
140725
+ function normalizeEvidenceText(text) {
140726
+ const normalized = redactSecrets(text.replace(/\s+/g, " ").trim());
140727
+ return truncateEvidenceText(normalized, MAX_EVIDENCE_TEXT_LENGTH);
140728
+ }
140729
+ function truncateEvidenceText(text, maxLength) {
140730
+ if (text.length <= maxLength)
140731
+ return text;
140732
+ const truncated = text.slice(0, maxLength);
140733
+ const lastPlaceholderStart = truncated.lastIndexOf("[REDACTED:");
140734
+ const lastPlaceholderEnd = truncated.lastIndexOf("]");
140735
+ if (lastPlaceholderStart > lastPlaceholderEnd) {
140736
+ return truncated.slice(0, lastPlaceholderStart).trimEnd();
140737
+ }
140738
+ return truncated;
140739
+ }
140740
+ function normalizeOptional(value) {
140741
+ const normalized = value?.replace(/\s+/g, " ").trim();
140742
+ return normalized ? redactSecrets(normalized) : undefined;
140743
+ }
140744
+
140745
+ // src/tools/web-fetch.ts
140746
+ init_create_tool();
140747
+ init_resolve_working_directory();
140748
+ var DEFAULT_MAX_BYTES = 1e6;
140749
+ var MAX_BYTES_HARD_CAP = 5000000;
140750
+ var DEFAULT_TIMEOUT_MS4 = 15000;
140751
+ var MAX_TIMEOUT_MS2 = 30000;
140752
+ var MAX_REDIRECTS = 5;
140753
+ var MAX_TEXT_LENGTH2 = 50000;
140754
+ var ArgsSchema6 = exports_external.object({
140755
+ url: exports_external.string().min(1).max(2048),
140756
+ max_bytes: exports_external.number().int().min(1024).max(MAX_BYTES_HARD_CAP).optional(),
140757
+ timeout_ms: exports_external.number().int().min(1000).max(MAX_TIMEOUT_MS2).optional(),
140758
+ working_directory: exports_external.string().optional()
140759
+ });
140760
+ function isBlockedAddress(address) {
140761
+ const family = isIP(address);
140762
+ if (family === 4)
140763
+ return isBlockedIPv4(address);
140764
+ if (family === 6)
140765
+ return isBlockedIPv6(address);
140766
+ return true;
140767
+ }
140768
+ function isBlockedIPv4(address) {
140769
+ const parts2 = address.split(".");
140770
+ if (parts2.length !== 4)
140771
+ return true;
140772
+ const octets = parts2.map((p) => Number(p));
140773
+ if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255))
140774
+ return true;
140775
+ const [a, b] = octets;
140776
+ if (a === 0)
140777
+ return true;
140778
+ if (a === 10)
140779
+ return true;
140780
+ if (a === 127)
140781
+ return true;
140782
+ if (a === 169 && b === 254)
140783
+ return true;
140784
+ if (a === 172 && b >= 16 && b <= 31)
140785
+ return true;
140786
+ if (a === 192 && b === 168)
140787
+ return true;
140788
+ if (a === 100 && b >= 64 && b <= 127)
140789
+ return true;
140790
+ if (a === 198 && (b === 18 || b === 19))
140791
+ return true;
140792
+ if (a === 192 && b === 0 && octets[2] === 0)
140793
+ return true;
140794
+ if (a === 192 && b === 0 && octets[2] === 2)
140795
+ return true;
140796
+ if (a === 198 && b === 51 && octets[2] === 100)
140797
+ return true;
140798
+ if (a === 203 && b === 0 && octets[2] === 113)
140799
+ return true;
140800
+ if (a === 192 && b === 88 && octets[2] === 99)
140801
+ return true;
140802
+ if (a >= 224)
140803
+ return true;
140804
+ return false;
140805
+ }
140806
+ function expandIPv6(input) {
140807
+ let s = input.toLowerCase().split("%")[0];
140808
+ if (!s)
140809
+ return null;
140810
+ if (s.includes(".")) {
140811
+ const colon = s.lastIndexOf(":");
140812
+ if (colon === -1)
140813
+ return null;
140814
+ const v4 = s.slice(colon + 1).split(".");
140815
+ if (v4.length !== 4)
140816
+ return null;
140817
+ const o = v4.map((p) => Number(p));
140818
+ if (o.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
140819
+ return null;
140820
+ const h1 = (o[0] << 8 | o[1]).toString(16);
140821
+ const h2 = (o[2] << 8 | o[3]).toString(16);
140822
+ s = `${s.slice(0, colon + 1)}${h1}:${h2}`;
140823
+ }
140824
+ const halves = s.split("::");
140825
+ if (halves.length > 2)
140826
+ return null;
140827
+ const parseGroups = (part) => {
140828
+ if (part === "")
140829
+ return [];
140830
+ const groups = part.split(":");
140831
+ const out2 = [];
140832
+ for (const g of groups) {
140833
+ if (!/^[0-9a-f]{1,4}$/.test(g))
140834
+ return null;
140835
+ out2.push(Number.parseInt(g, 16));
140836
+ }
140837
+ return out2;
140838
+ };
140839
+ const head = parseGroups(halves[0]);
140840
+ if (head === null)
140841
+ return null;
140842
+ if (halves.length === 2) {
140843
+ const tail = parseGroups(halves[1]);
140844
+ if (tail === null)
140845
+ return null;
140846
+ const missing = 8 - head.length - tail.length;
140847
+ if (missing < 1)
140848
+ return null;
140849
+ return [...head, ...new Array(missing).fill(0), ...tail];
140850
+ }
140851
+ return head.length === 8 ? head : null;
140852
+ }
140853
+ function isBlockedIPv6(raw) {
140854
+ const h = expandIPv6(raw);
140855
+ if (!h)
140856
+ return true;
140857
+ if (h.every((x) => x === 0))
140858
+ return true;
140859
+ if (h.slice(0, 7).every((x) => x === 0) && h[7] === 1)
140860
+ return true;
140861
+ const mappedV4 = h.slice(0, 5).every((x) => x === 0) && h[5] === 65535;
140862
+ const compatV4 = h.slice(0, 6).every((x) => x === 0) && !(h[6] === 0 && h[7] <= 1);
140863
+ if (mappedV4 || compatV4) {
140864
+ const v4 = `${h[6] >> 8}.${h[6] & 255}.${h[7] >> 8}.${h[7] & 255}`;
140865
+ return isBlockedIPv4(v4);
140866
+ }
140867
+ const first = h[0];
140868
+ if (first >= 64512 && first <= 65023)
140869
+ return true;
140870
+ if (first >= 65152 && first <= 65215)
140871
+ return true;
140872
+ if (first >= 65280)
140873
+ return true;
140874
+ return false;
140875
+ }
140876
+ async function validateFetchUrl(candidate, dnsLookup) {
140877
+ let url3;
140878
+ try {
140879
+ url3 = new URL(candidate);
140880
+ } catch {
140881
+ return {
140882
+ ok: false,
140883
+ reason: "invalid_url",
140884
+ message: `Not a valid URL: ${candidate}`
140885
+ };
140886
+ }
140887
+ if (url3.protocol !== "http:" && url3.protocol !== "https:") {
140888
+ return {
140889
+ ok: false,
140890
+ reason: "blocked_scheme",
140891
+ message: `Only http and https URLs are allowed (got "${url3.protocol}").`
140892
+ };
140893
+ }
140894
+ const host = url3.hostname.replace(/^\[|\]$/g, "");
140895
+ if (isIP(host)) {
140896
+ if (isBlockedAddress(host)) {
140897
+ return {
140898
+ ok: false,
140899
+ reason: "blocked_host",
140900
+ message: `Refusing to fetch a private, loopback, or reserved address: ${host}`
140901
+ };
140902
+ }
140903
+ return { ok: true, url: url3, address: host };
140904
+ }
140905
+ let resolved;
140906
+ try {
140907
+ resolved = await dnsLookup(host, { all: true });
140908
+ } catch (err3) {
140909
+ return {
140910
+ ok: false,
140911
+ reason: "dns_failure",
140912
+ message: `Could not resolve host "${host}": ${err3 instanceof Error ? err3.message : String(err3)}`
140913
+ };
140914
+ }
140915
+ if (resolved.length === 0) {
140916
+ return {
140917
+ ok: false,
140918
+ reason: "dns_failure",
140919
+ message: `Host "${host}" resolved to no addresses.`
140920
+ };
140921
+ }
140922
+ for (const { address } of resolved) {
140923
+ if (isBlockedAddress(address)) {
140924
+ return {
140925
+ ok: false,
140926
+ reason: "blocked_host",
140927
+ message: `Host "${host}" resolves to a private, loopback, or reserved address (${address}).`
140928
+ };
140929
+ }
140930
+ }
140931
+ return { ok: true, url: url3, address: resolved[0].address };
140932
+ }
140933
+ function isAllowedContentType(contentType) {
140934
+ if (!contentType)
140935
+ return true;
140936
+ const type = contentType.split(";")[0].trim().toLowerCase();
140937
+ if (type.startsWith("text/"))
140938
+ return true;
140939
+ if (type === "application/json" || type === "application/xml")
140940
+ return true;
140941
+ if (type === "application/xhtml+xml" || type.endsWith("+json") || type.endsWith("+xml")) {
140942
+ return true;
140943
+ }
140944
+ return false;
140945
+ }
140946
+ function extractTitle(html) {
140947
+ const lower = html.toLowerCase();
140948
+ const start2 = lower.indexOf("<title");
140949
+ if (start2 === -1)
140950
+ return;
140951
+ const tagEnd = lower.indexOf(">", start2);
140952
+ if (tagEnd === -1)
140953
+ return;
140954
+ const contentStart = tagEnd + 1;
140955
+ const end = lower.indexOf("</title", contentStart);
140956
+ if (end === -1)
140957
+ return;
140958
+ const content = html.slice(contentStart, end);
140959
+ const title = decodeEntities(content.replace(/\s+/g, " ").trim());
140960
+ return title || undefined;
140961
+ }
140962
+ var NAMED_ENTITIES = {
140963
+ amp: "&",
140964
+ lt: "<",
140965
+ gt: ">",
140966
+ quot: '"',
140967
+ apos: "'",
140968
+ nbsp: " ",
140969
+ "#39": "'"
140970
+ };
140971
+ function decodeEntities(text) {
140972
+ return text.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (whole, body2) => {
140973
+ const key = body2.toLowerCase();
140974
+ if (key in NAMED_ENTITIES)
140975
+ return NAMED_ENTITIES[key];
140976
+ if (body2.startsWith("#x") || body2.startsWith("#X")) {
140977
+ const code = Number.parseInt(body2.slice(2), 16);
140978
+ return Number.isFinite(code) ? safeFromCodePoint(code) : whole;
140979
+ }
140980
+ if (body2.startsWith("#")) {
140981
+ const code = Number.parseInt(body2.slice(1), 10);
140982
+ return Number.isFinite(code) ? safeFromCodePoint(code) : whole;
140983
+ }
140984
+ return whole;
140985
+ });
140986
+ }
140987
+ function safeFromCodePoint(code) {
140988
+ try {
140989
+ return String.fromCodePoint(code);
140990
+ } catch {
140991
+ return "";
140992
+ }
140993
+ }
140994
+ function stripSpans(input, open3, close) {
140995
+ const lower = input.toLowerCase();
140996
+ let out2 = "";
140997
+ let i2 = 0;
140998
+ while (i2 < input.length) {
140999
+ const start2 = lower.indexOf(open3, i2);
141000
+ if (start2 === -1) {
141001
+ out2 += input.slice(i2);
141002
+ break;
141003
+ }
141004
+ out2 += input.slice(i2, start2);
141005
+ const end = lower.indexOf(close, start2 + open3.length);
141006
+ if (end === -1)
141007
+ break;
141008
+ i2 = end + close.length;
141009
+ }
141010
+ return out2;
141011
+ }
141012
+ function htmlToText(html) {
141013
+ let withoutScripts = stripSpans(html, "<script", "</script>");
141014
+ withoutScripts = stripSpans(withoutScripts, "<style", "</style>");
141015
+ withoutScripts = stripSpans(withoutScripts, "<noscript", "</noscript>");
141016
+ withoutScripts = stripSpans(withoutScripts, "<!--", "-->");
141017
+ const withBreaks = withoutScripts.replace(/<\/(p|div|li|h[1-6]|tr|section|article|header|footer)>/gi, `
141018
+ `).replace(/<br\s*\/?>/gi, `
141019
+ `);
141020
+ const noTags = withBreaks.replace(/<[^>]+>/g, " ");
141021
+ const decoded = decodeEntities(noTags);
141022
+ return decoded.replace(/[ \t\f\v]+/g, " ").replace(/\s*\n\s*/g, `
141023
+ `).replace(/\n{3,}/g, `
141024
+
141025
+ `).trim();
141026
+ }
141027
+ function makeAbortError() {
141028
+ try {
141029
+ return new DOMException("The operation was aborted", "AbortError");
141030
+ } catch {
141031
+ const err3 = new Error("The operation was aborted");
141032
+ err3.name = "AbortError";
141033
+ return err3;
141034
+ }
141035
+ }
141036
+ function performHttpRequest(args2) {
141037
+ const { url: url3, pinnedAddress, signal, headers } = args2;
141038
+ const isHttps = url3.protocol === "https:";
141039
+ const port = url3.port ? Number(url3.port) : isHttps ? 443 : 80;
141040
+ const hostNoBrackets = url3.hostname.replace(/^\[|\]$/g, "");
141041
+ const useSni = isHttps && isIP(hostNoBrackets) === 0;
141042
+ const options = {
141043
+ host: pinnedAddress,
141044
+ port,
141045
+ path: `${url3.pathname}${url3.search}`,
141046
+ method: "GET",
141047
+ headers: { Host: url3.host, ...headers }
141048
+ };
141049
+ if (useSni)
141050
+ options.servername = url3.hostname;
141051
+ return new Promise((resolve72, reject) => {
141052
+ let req;
141053
+ const onResponse = (res) => {
141054
+ const normHeaders = {};
141055
+ for (const [key, value] of Object.entries(res.headers)) {
141056
+ normHeaders[key] = Array.isArray(value) ? value[0] : value;
141057
+ }
141058
+ res.on("error", () => {});
141059
+ resolve72({
141060
+ status: res.statusCode ?? 0,
141061
+ headers: normHeaders,
141062
+ body: res,
141063
+ cancel: () => req.destroy()
141064
+ });
141065
+ };
141066
+ req = isHttps ? https.request(options, onResponse) : http.request(options, onResponse);
141067
+ const onAbort = () => req.destroy(makeAbortError());
141068
+ if (signal.aborted)
141069
+ req.destroy(makeAbortError());
141070
+ else
141071
+ signal.addEventListener("abort", onAbort, { once: true });
141072
+ req.on("error", reject);
141073
+ req.end();
141074
+ });
141075
+ }
141076
+ async function boundedFetch(start2, maxBytes, timeoutMs, deps) {
141077
+ let current = start2;
141078
+ const controller = new AbortController;
141079
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
141080
+ try {
141081
+ for (let hop = 0;hop <= MAX_REDIRECTS; hop++) {
141082
+ let raw;
141083
+ try {
141084
+ raw = await deps.httpRequest({
141085
+ url: current.url,
141086
+ pinnedAddress: current.address,
141087
+ signal: controller.signal,
141088
+ headers: {
141089
+ Accept: "text/html,application/xhtml+xml,text/plain,application/json;q=0.9,*/*;q=0.5"
141090
+ }
141091
+ });
141092
+ } catch (err3) {
141093
+ if (controller.signal.aborted) {
141094
+ return {
141095
+ ok: false,
141096
+ reason: "timeout",
141097
+ message: `Fetch exceeded ${timeoutMs}ms for ${current.url.toString()}`
141098
+ };
141099
+ }
141100
+ return {
141101
+ ok: false,
141102
+ reason: "network_error",
141103
+ message: err3 instanceof Error ? err3.message : String(err3)
141104
+ };
141105
+ }
141106
+ if (raw.status >= 300 && raw.status < 400) {
141107
+ const location = raw.headers.location;
141108
+ raw.cancel?.();
141109
+ if (!location) {
141110
+ return {
141111
+ ok: false,
141112
+ reason: "bad_redirect",
141113
+ message: `Redirect ${raw.status} with no Location header.`
141114
+ };
141115
+ }
141116
+ let next;
141117
+ try {
141118
+ next = new URL(location, current.url);
141119
+ } catch {
141120
+ return {
141121
+ ok: false,
141122
+ reason: "bad_redirect",
141123
+ message: `Invalid redirect target: ${location}`
141124
+ };
141125
+ }
141126
+ const revalidated = await validateFetchUrl(next.toString(), deps.dnsLookup);
141127
+ if (!revalidated.ok)
141128
+ return revalidated;
141129
+ if (hop === MAX_REDIRECTS) {
141130
+ return {
141131
+ ok: false,
141132
+ reason: "too_many_redirects",
141133
+ message: `Exceeded ${MAX_REDIRECTS} redirect hops.`
141134
+ };
141135
+ }
141136
+ current = { url: revalidated.url, address: revalidated.address };
141137
+ continue;
141138
+ }
141139
+ if (raw.status < 200 || raw.status >= 300) {
141140
+ raw.cancel?.();
141141
+ return {
141142
+ ok: false,
141143
+ reason: "http_error",
141144
+ message: `HTTP ${raw.status} for ${current.url.toString()}`
141145
+ };
141146
+ }
141147
+ const contentType = raw.headers["content-type"] ?? null;
141148
+ if (!isAllowedContentType(contentType)) {
141149
+ raw.cancel?.();
141150
+ return {
141151
+ ok: false,
141152
+ reason: "unsupported_content_type",
141153
+ message: `Refusing to read non-text content type "${contentType}".`
141154
+ };
141155
+ }
141156
+ let activeBody = raw.body;
141157
+ if (raw.body !== null && raw.body instanceof Readable) {
141158
+ const encoding = (raw.headers["content-encoding"] ?? "").toLowerCase();
141159
+ let decoder = null;
141160
+ if (encoding === "gzip" || encoding === "x-gzip")
141161
+ decoder = raw.body.pipe(zlib.createGunzip());
141162
+ else if (encoding === "deflate")
141163
+ decoder = raw.body.pipe(zlib.createInflate());
141164
+ else if (encoding === "br")
141165
+ decoder = raw.body.pipe(zlib.createBrotliDecompress());
141166
+ if (decoder) {
141167
+ decoder.on("error", () => {});
141168
+ activeBody = decoder;
141169
+ }
141170
+ }
141171
+ let body2;
141172
+ try {
141173
+ body2 = await readBounded(activeBody, maxBytes);
141174
+ } catch (err3) {
141175
+ raw.cancel?.();
141176
+ if (controller.signal.aborted) {
141177
+ return {
141178
+ ok: false,
141179
+ reason: "timeout",
141180
+ message: `Fetch exceeded ${timeoutMs}ms while reading the body of ${current.url.toString()}`
141181
+ };
141182
+ }
141183
+ return {
141184
+ ok: false,
141185
+ reason: "network_error",
141186
+ message: err3 instanceof Error ? err3.message : String(err3)
141187
+ };
141188
+ }
141189
+ raw.cancel?.();
141190
+ return {
141191
+ ok: true,
141192
+ outcome: {
141193
+ status: raw.status,
141194
+ finalUrl: current.url.toString(),
141195
+ contentType,
141196
+ bytes: body2.bytes,
141197
+ truncated: body2.truncated
141198
+ }
141199
+ };
141200
+ }
141201
+ return {
141202
+ ok: false,
141203
+ reason: "too_many_redirects",
141204
+ message: `Exceeded ${MAX_REDIRECTS} redirect hops.`
141205
+ };
141206
+ } finally {
141207
+ clearTimeout(timer);
141208
+ }
141209
+ }
141210
+ async function readBounded(body2, maxBytes) {
141211
+ if (!body2)
141212
+ return { bytes: new Uint8Array(0), truncated: false };
141213
+ const chunks = [];
141214
+ let received = 0;
141215
+ let truncated = false;
141216
+ for await (const value of body2) {
141217
+ if (!value || value.byteLength === 0)
141218
+ continue;
141219
+ chunks.push(value);
141220
+ received += value.byteLength;
141221
+ if (received > maxBytes) {
141222
+ truncated = true;
141223
+ break;
141224
+ }
141225
+ }
141226
+ const merged = new Uint8Array(Math.min(received, maxBytes));
141227
+ let offset = 0;
141228
+ for (const chunk of chunks) {
141229
+ if (offset >= merged.length)
141230
+ break;
141231
+ const slice = chunk.subarray(0, merged.length - offset);
141232
+ merged.set(slice, offset);
141233
+ offset += slice.byteLength;
141234
+ }
141235
+ return { bytes: merged, truncated };
141236
+ }
141237
+ var web_fetch = createSwarmTool({
141238
+ description: "Fetch the readable text of a single http(s) URL for architect-driven deep research (MODE: DEEP_RESEARCH). " + "Returns decoded page text (HTML stripped to plain text), the document title, the final URL after redirects, " + "and an evidence reference stored alongside web_search results. Use it to read primary sources that web_search " + "only surfaces as snippets. Config-gated on council.general.enabled; no search API key required. " + "Blocks private/loopback/link-local/metadata addresses (re-validated across redirects), enforces a timeout, " + "and caps the response body size.",
141239
+ args: {
141240
+ url: exports_external.string().min(1).max(2048).describe("Absolute http(s) URL to fetch (1–2048 chars)."),
141241
+ max_bytes: exports_external.number().int().min(1024).max(MAX_BYTES_HARD_CAP).optional().describe(`Max decoded response bytes to read (1024..${MAX_BYTES_HARD_CAP}, default ${DEFAULT_MAX_BYTES}).`),
141242
+ timeout_ms: exports_external.number().int().min(1000).max(MAX_TIMEOUT_MS2).optional().describe(`Request timeout in ms (1000..${MAX_TIMEOUT_MS2}, default ${DEFAULT_TIMEOUT_MS4}).`),
141243
+ working_directory: exports_external.string().optional().describe("Project root for config resolution and evidence storage. Optional.")
141244
+ },
141245
+ execute: async (args2, directory) => {
141246
+ const parsed = ArgsSchema6.safeParse(args2);
141247
+ if (!parsed.success) {
141248
+ const fail = {
141249
+ success: false,
141250
+ reason: "invalid_args",
141251
+ message: parsed.error.issues.map((i2) => `${i2.path.join(".")}: ${i2.message}`).join("; ")
141252
+ };
141253
+ return JSON.stringify(fail, null, 2);
141254
+ }
141255
+ const dirResult = resolveWorkingDirectory(parsed.data.working_directory, directory);
141256
+ if (!dirResult.success) {
141257
+ const fail = {
141258
+ success: false,
141259
+ reason: "invalid_working_directory",
141260
+ message: dirResult.message
141261
+ };
141262
+ return JSON.stringify(fail, null, 2);
141263
+ }
141264
+ const config3 = _internals102.loadPluginConfig(dirResult.directory);
141265
+ const generalConfig = config3.council?.general;
141266
+ if (!generalConfig || generalConfig.enabled !== true) {
141267
+ const fail = {
141268
+ success: false,
141269
+ reason: "council_general_disabled",
141270
+ message: "web_fetch is disabled - set council.general.enabled: true in the resolved config: global ~/.config/opencode/opencode-swarm.json or project .opencode/opencode-swarm.json."
141271
+ };
141272
+ return JSON.stringify(fail, null, 2);
141273
+ }
141274
+ const validated = await validateFetchUrl(parsed.data.url, _internals102.dnsLookup);
141275
+ if (!validated.ok) {
141276
+ const fail = {
141277
+ success: false,
141278
+ reason: validated.reason,
141279
+ message: validated.message
141280
+ };
141281
+ return JSON.stringify(fail, null, 2);
141282
+ }
141283
+ const maxBytes = parsed.data.max_bytes ?? DEFAULT_MAX_BYTES;
141284
+ const timeoutMs = parsed.data.timeout_ms ?? DEFAULT_TIMEOUT_MS4;
141285
+ const result = await boundedFetch({ url: validated.url, address: validated.address }, maxBytes, timeoutMs, _internals102);
141286
+ if (!result.ok) {
141287
+ const fail = {
141288
+ success: false,
141289
+ reason: result.reason,
141290
+ message: result.message
141291
+ };
141292
+ return JSON.stringify(fail, null, 2);
141293
+ }
141294
+ const { outcome } = result;
141295
+ const raw = new TextDecoder("utf-8", { fatal: false }).decode(outcome.bytes);
141296
+ const isHtml = (outcome.contentType ?? "").toLowerCase().includes("html") || /<html|<!doctype html/i.test(raw);
141297
+ const title = isHtml ? extractTitle(raw) : undefined;
141298
+ const bodyText = isHtml ? htmlToText(raw) : raw.replace(/\s*\n\s*/g, `
141299
+ `).trim();
141300
+ const text = bodyText.length > MAX_TEXT_LENGTH2 ? `${bodyText.slice(0, MAX_TEXT_LENGTH2)}…` : bodyText;
141301
+ const textTruncated = outcome.truncated || bodyText.length > MAX_TEXT_LENGTH2;
141302
+ const evidence = await captureFetchEvidence(dirResult.directory, outcome.finalUrl, title, text);
141303
+ const ok2 = {
141304
+ success: true,
141305
+ url: parsed.data.url,
141306
+ finalUrl: outcome.finalUrl,
141307
+ status: outcome.status,
141308
+ contentType: outcome.contentType ?? undefined,
141309
+ title,
141310
+ text,
141311
+ truncated: textTruncated,
141312
+ bytesReturned: outcome.bytes.byteLength,
141313
+ evidence
141314
+ };
141315
+ return JSON.stringify(ok2, null, 2);
141316
+ }
141317
+ });
141318
+ async function captureFetchEvidence(directory, url3, title, text) {
141319
+ try {
141320
+ const written = await _internals102.writeEvidenceDocuments(directory, [
141321
+ {
141322
+ sourceType: "crawl",
141323
+ url: url3,
141324
+ title,
141325
+ text,
141326
+ createdBy: "web_fetch"
141327
+ }
141328
+ ]);
141329
+ return {
141330
+ stored: written.records.length > 0,
141331
+ ref: written.refs[0],
141332
+ path: written.path
141333
+ };
141334
+ } catch (err3) {
141335
+ return {
141336
+ stored: false,
141337
+ error: err3 instanceof Error ? err3.message : String(err3)
141338
+ };
141339
+ }
141340
+ }
141341
+ var _internals102 = {
141342
+ httpRequest: performHttpRequest,
141343
+ dnsLookup: lookup,
141344
+ loadPluginConfig,
141345
+ writeEvidenceDocuments
141346
+ };
141347
+
140506
141348
  // src/tools/web-search.ts
140507
141349
  init_zod();
140508
141350
  init_loader();
@@ -140699,88 +141541,11 @@ function createWebSearchProvider(config3) {
140699
141541
  }
140700
141542
  }
140701
141543
 
140702
- // src/evidence/documents.ts
140703
- init_utils2();
140704
- init_redaction();
140705
- import { createHash as createHash16 } from "node:crypto";
140706
- import { appendFile as appendFile17, mkdir as mkdir31 } from "node:fs/promises";
140707
- import * as path188 from "node:path";
140708
- var EVIDENCE_CACHE_FILE = "evidence-cache/documents.jsonl";
140709
- var MAX_EVIDENCE_TEXT_LENGTH = 4000;
140710
- async function writeEvidenceDocuments(directory, inputs, now = () => new Date) {
140711
- const filePath = validateSwarmPath(directory, EVIDENCE_CACHE_FILE);
140712
- const capturedAt = now().toISOString();
140713
- const records = inputs.map((input) => createEvidenceDocumentRecord(input, capturedAt)).filter((record3) => record3 !== null);
140714
- if (records.length > 0) {
140715
- await mkdir31(path188.dirname(filePath), { recursive: true });
140716
- await appendFile17(filePath, `${records.map((record3) => JSON.stringify(record3)).join(`
140717
- `)}
140718
- `, "utf-8");
140719
- }
140720
- return {
140721
- path: ".swarm/evidence-cache/documents.jsonl",
140722
- records,
140723
- refs: records.map((record3) => record3.ref)
140724
- };
140725
- }
140726
- function createEvidenceDocumentRecord(input, defaultCapturedAt) {
140727
- const text = normalizeEvidenceText(input.text ?? input.snippet ?? "");
140728
- if (!text)
140729
- return null;
140730
- const capturedAt = input.capturedAt ?? defaultCapturedAt;
140731
- const base = {
140732
- sourceType: input.sourceType,
140733
- query: normalizeOptional(input.query),
140734
- title: normalizeOptional(input.title),
140735
- url: normalizeOptional(input.url),
140736
- text
140737
- };
140738
- const id = createEvidenceDocumentId(base);
140739
- return {
140740
- id,
140741
- ref: `evidence-cache:${id}`,
140742
- ...base,
140743
- capturedAt,
140744
- createdBy: normalizeOptional(input.createdBy),
140745
- metadata: input.metadata ?? {}
140746
- };
140747
- }
140748
- function createEvidenceDocumentId(input) {
140749
- const hash4 = createHash16("sha256").update([
140750
- input.sourceType,
140751
- input.query ?? "",
140752
- input.title ?? "",
140753
- input.url ?? "",
140754
- input.text
140755
- ].join(`
140756
- `)).digest("hex");
140757
- return `evd_${hash4.slice(0, 16)}`;
140758
- }
140759
- function normalizeEvidenceText(text) {
140760
- const normalized = redactSecrets(text.replace(/\s+/g, " ").trim());
140761
- return truncateEvidenceText(normalized, MAX_EVIDENCE_TEXT_LENGTH);
140762
- }
140763
- function truncateEvidenceText(text, maxLength) {
140764
- if (text.length <= maxLength)
140765
- return text;
140766
- const truncated = text.slice(0, maxLength);
140767
- const lastPlaceholderStart = truncated.lastIndexOf("[REDACTED:");
140768
- const lastPlaceholderEnd = truncated.lastIndexOf("]");
140769
- if (lastPlaceholderStart > lastPlaceholderEnd) {
140770
- return truncated.slice(0, lastPlaceholderStart).trimEnd();
140771
- }
140772
- return truncated;
140773
- }
140774
- function normalizeOptional(value) {
140775
- const normalized = value?.replace(/\s+/g, " ").trim();
140776
- return normalized ? redactSecrets(normalized) : undefined;
140777
- }
140778
-
140779
141544
  // src/tools/web-search.ts
140780
141545
  init_create_tool();
140781
141546
  init_resolve_working_directory();
140782
141547
  var MAX_RESULTS_HARD_CAP = 10;
140783
- var ArgsSchema6 = exports_external.object({
141548
+ var ArgsSchema7 = exports_external.object({
140784
141549
  query: exports_external.string().min(1).max(500),
140785
141550
  max_results: exports_external.number().int().min(1).max(20).optional(),
140786
141551
  freshness: exports_external.enum(["auto", "none", "day", "week", "month", "year"]).default("auto"),
@@ -140795,7 +141560,7 @@ var web_search = createSwarmTool({
140795
141560
  working_directory: exports_external.string().optional().describe("Project root for config resolution. Optional.")
140796
141561
  },
140797
141562
  execute: async (args2, directory) => {
140798
- const parsed = ArgsSchema6.safeParse(args2);
141563
+ const parsed = ArgsSchema7.safeParse(args2);
140799
141564
  if (!parsed.success) {
140800
141565
  const fail = {
140801
141566
  success: false,
@@ -140878,7 +141643,7 @@ var web_search = createSwarmTool({
140878
141643
  });
140879
141644
  async function captureSearchEvidence(directory, query, results) {
140880
141645
  try {
140881
- const written = await _internals102.writeEvidenceDocuments(directory, results.map((result) => ({
141646
+ const written = await _internals103.writeEvidenceDocuments(directory, results.map((result) => ({
140882
141647
  sourceType: "web_search",
140883
141648
  query,
140884
141649
  title: result.title,
@@ -140906,7 +141671,7 @@ async function captureSearchEvidence(directory, query, results) {
140906
141671
  };
140907
141672
  }
140908
141673
  }
140909
- var _internals102 = {
141674
+ var _internals103 = {
140910
141675
  writeEvidenceDocuments
140911
141676
  };
140912
141677
 
@@ -140937,7 +141702,7 @@ var KnowledgeRecommendationSchema2 = exports_external.object({
140937
141702
  confidence: exports_external.number().min(0).max(1).default(0.5),
140938
141703
  evidence_refs: exports_external.array(exports_external.string().min(1)).default([])
140939
141704
  });
140940
- var ArgsSchema7 = exports_external.object({
141705
+ var ArgsSchema8 = exports_external.object({
140941
141706
  phase: exports_external.number().int().min(0).max(999),
140942
141707
  verdict: exports_external.enum(["APPROVE", "CONCERNS", "REJECT"]),
140943
141708
  findings: exports_external.array(FindingSchema2).default([]),
@@ -140958,7 +141723,7 @@ var write_architecture_supervisor_evidence = createSwarmTool({
140958
141723
  provenance_session_id: exports_external.string().min(1).optional().describe("Session ID of the agent that produced this evidence (optional provenance).")
140959
141724
  },
140960
141725
  execute: async (rawArgs, directory) => {
140961
- const parsed = ArgsSchema7.safeParse(rawArgs);
141726
+ const parsed = ArgsSchema8.safeParse(rawArgs);
140962
141727
  if (!parsed.success) {
140963
141728
  return JSON.stringify({
140964
141729
  success: false,
@@ -141237,7 +142002,7 @@ var VerdictSchema3 = exports_external.object({
141237
142002
  criteriaUnmet: exports_external.array(exports_external.string()),
141238
142003
  durationMs: exports_external.number().nonnegative()
141239
142004
  });
141240
- var ArgsSchema8 = exports_external.object({
142005
+ var ArgsSchema9 = exports_external.object({
141241
142006
  phase: exports_external.number().int().min(1),
141242
142007
  projectSummary: exports_external.string().min(1),
141243
142008
  roundNumber: exports_external.number().int().min(1).max(10).optional(),
@@ -141253,7 +142018,7 @@ function normalizeFinalVerdict(verdict, requiredFixesCount) {
141253
142018
  return requiredFixesCount > 0 ? "rejected" : "concerns";
141254
142019
  }
141255
142020
  async function executeWriteFinalCouncilEvidence(args2, directory) {
141256
- const parsed = ArgsSchema8.safeParse(args2);
142021
+ const parsed = ArgsSchema9.safeParse(args2);
141257
142022
  if (!parsed.success) {
141258
142023
  return JSON.stringify({
141259
142024
  success: false,
@@ -141373,7 +142138,7 @@ var write_final_council_evidence = createSwarmTool({
141373
142138
  verdicts: exports_external.array(VerdictSchema3).min(1).max(5).describe("Collected CouncilMemberVerdict objects from critic, reviewer, sme, test_engineer, and explorer.")
141374
142139
  },
141375
142140
  execute: async (args2, directory) => {
141376
- const parsed = ArgsSchema8.safeParse(args2);
142141
+ const parsed = ArgsSchema9.safeParse(args2);
141377
142142
  if (!parsed.success) {
141378
142143
  return JSON.stringify({
141379
142144
  success: false,
@@ -141707,6 +142472,7 @@ var TOOL_MANIFEST = defineHandlers({
141707
142472
  get_qa_gate_profile: () => get_qa_gate_profile,
141708
142473
  set_qa_gates: () => set_qa_gates,
141709
142474
  web_search: () => web_search,
142475
+ web_fetch: () => web_fetch,
141710
142476
  convene_general_council: () => convene_general_council,
141711
142477
  write_final_council_evidence: () => write_final_council_evidence,
141712
142478
  skill_generate: () => skill_generate,
@@ -142389,6 +143155,10 @@ async function initializeOpenCodeSwarm(ctx) {
142389
143155
  template: "/swarm deep-dive $ARGUMENTS",
142390
143156
  description: "Use /swarm deep-dive to launch a read-only deep audit with parallel explorer waves, dual reviewers, and critic challenge"
142391
143157
  },
143158
+ "swarm-deep-research": {
143159
+ template: "/swarm deep-research $ARGUMENTS",
143160
+ description: "Use /swarm deep-research <question> to run a multi-source, fact-checked deep research pass and synthesize a cited report [--depth standard|exhaustive] [--max-researchers 1..6] [--rounds 1..4] [--brief]"
143161
+ },
142392
143162
  "swarm-codebase-review": {
142393
143163
  template: "/swarm codebase-review $ARGUMENTS",
142394
143164
  description: "Use /swarm codebase-review to launch codebase-review-swarm for a quote-grounded full-repo or large-subsystem audit"