opencode-swarm 7.71.3 → 7.72.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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.1",
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"]
@@ -16948,6 +16953,12 @@ function addDeferredWarning(warning) {
16948
16953
  deferredWarnings.push(warning);
16949
16954
  }
16950
16955
  }
16956
+ function getDeferredWarnings() {
16957
+ return [...deferredWarnings];
16958
+ }
16959
+ function clearDeferredWarnings() {
16960
+ deferredWarnings.length = 0;
16961
+ }
16951
16962
  var deferredWarnings, MAX_DEFERRED_WARNINGS = 50;
16952
16963
  var init_warning_buffer = __esm(() => {
16953
16964
  deferredWarnings = [];
@@ -17101,6 +17112,7 @@ var init_bundled_skills = __esm(() => {
17101
17112
  "pre-phase-briefing",
17102
17113
  "council",
17103
17114
  "deep-dive",
17115
+ "deep-research",
17104
17116
  "codebase-review-swarm",
17105
17117
  "design-docs",
17106
17118
  "swarm-pr-review",
@@ -69353,6 +69365,119 @@ var init_deep_dive = __esm(() => {
69353
69365
  ]);
69354
69366
  });
69355
69367
 
69368
+ // src/commands/deep-research.ts
69369
+ function sanitizeQuestion2(raw) {
69370
+ const collapsed = raw.replace(/\s+/g, " ").trim();
69371
+ const stripped = collapsed.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
69372
+ const normalized = stripped.replace(/\s+/g, " ").trim();
69373
+ if (normalized.length <= MAX_QUESTION_LEN2)
69374
+ return normalized;
69375
+ return `${normalized.slice(0, MAX_QUESTION_LEN2)}…`;
69376
+ }
69377
+ function isBoundedInteger(raw, min, max) {
69378
+ if (!raw || !/^\d+$/.test(raw))
69379
+ return false;
69380
+ const n = Number(raw);
69381
+ return Number.isInteger(n) && n >= min && n <= max;
69382
+ }
69383
+ function parseArgs4(args2) {
69384
+ const result = {
69385
+ depth: DEFAULT_DEPTH,
69386
+ maxResearchers: DEFAULT_MAX_RESEARCHERS,
69387
+ rounds: DEFAULT_ROUNDS,
69388
+ output: "report",
69389
+ rest: []
69390
+ };
69391
+ let i2 = 0;
69392
+ while (i2 < args2.length) {
69393
+ const token = args2[i2];
69394
+ if (token === "--depth") {
69395
+ if (i2 + 1 >= args2.length)
69396
+ return { ...result, error: `Flag "${token}" requires a value` };
69397
+ const value = args2[++i2];
69398
+ if (!DEPTHS.has(value)) {
69399
+ return {
69400
+ ...result,
69401
+ error: `Invalid depth "${value}". Must be one of: standard, exhaustive.`
69402
+ };
69403
+ }
69404
+ result.depth = value;
69405
+ } else if (token === "--max-researchers") {
69406
+ if (i2 + 1 >= args2.length)
69407
+ return { ...result, error: `Flag "${token}" requires a value` };
69408
+ const value = args2[++i2];
69409
+ if (!isBoundedInteger(value, 1, 6)) {
69410
+ return {
69411
+ ...result,
69412
+ error: `Invalid --max-researchers value "${value}". Must be an integer between 1 and 6.`
69413
+ };
69414
+ }
69415
+ result.maxResearchers = Number(value);
69416
+ result.maxResearchersExplicit = true;
69417
+ } else if (token === "--rounds") {
69418
+ if (i2 + 1 >= args2.length)
69419
+ return { ...result, error: `Flag "${token}" requires a value` };
69420
+ const value = args2[++i2];
69421
+ if (!isBoundedInteger(value, 1, 4)) {
69422
+ return {
69423
+ ...result,
69424
+ error: `Invalid --rounds value "${value}". Must be an integer between 1 and 4.`
69425
+ };
69426
+ }
69427
+ result.rounds = Number(value);
69428
+ result.roundsExplicit = true;
69429
+ } else if (token === "--brief") {
69430
+ result.output = "brief";
69431
+ } else if (token.startsWith("--")) {
69432
+ return { ...result, error: `Unknown flag "${token}"` };
69433
+ } else {
69434
+ result.rest.push(token);
69435
+ }
69436
+ i2++;
69437
+ }
69438
+ return result;
69439
+ }
69440
+ async function handleDeepResearchCommand(_directory, args2) {
69441
+ const parsed = parseArgs4(args2);
69442
+ if (parsed.error) {
69443
+ return `Error: ${parsed.error}
69444
+
69445
+ ${USAGE4}`;
69446
+ }
69447
+ const question = sanitizeQuestion2(parsed.rest.join(" "));
69448
+ if (!question) {
69449
+ return USAGE4;
69450
+ }
69451
+ if (parsed.depth === "exhaustive") {
69452
+ if (!parsed.maxResearchersExplicit)
69453
+ parsed.maxResearchers = EXHAUSTIVE_DEFAULT_MAX_RESEARCHERS;
69454
+ if (!parsed.roundsExplicit)
69455
+ parsed.rounds = EXHAUSTIVE_DEFAULT_ROUNDS;
69456
+ }
69457
+ return `[MODE: DEEP_RESEARCH depth=${parsed.depth} max_researchers=${parsed.maxResearchers} rounds=${parsed.rounds} output=${parsed.output}] ${question}`;
69458
+ }
69459
+ 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]
69460
+
69461
+ Run a multi-source, fact-checked deep research pass and synthesize a cited report.
69462
+
69463
+ Examples:
69464
+ /swarm deep-research "What are the tradeoffs of WASM vs native plugins?"
69465
+ /swarm deep research "current state of deep research agents" --depth exhaustive
69466
+ /swarm deep-research "is Tavily or Brave better for our use case" --rounds 3 --brief
69467
+
69468
+ Flags:
69469
+ --depth <name> standard (focused) or exhaustive (broader fan-out)
69470
+ --max-researchers <N> parallel synthesis workers per round, 1..6
69471
+ --rounds <N> max iterative research rounds, 1..4
69472
+ --brief emit a short brief instead of a full cited report
69473
+
69474
+ Requires council.general.enabled: true and a configured search API key (Tavily or Brave)
69475
+ in the resolved config: global ~/.config/opencode/opencode-swarm.json, then project
69476
+ .opencode/opencode-swarm.json overrides.`;
69477
+ var init_deep_research = __esm(() => {
69478
+ DEPTHS = new Set(["standard", "exhaustive"]);
69479
+ });
69480
+
69356
69481
  // src/commands/design-docs.ts
69357
69482
  function sanitizeDescription(raw) {
69358
69483
  const collapsed = raw.replace(/\s+/g, " ").trim();
@@ -69370,7 +69495,7 @@ function cleanFlagValue(raw) {
69370
69495
  return null;
69371
69496
  return raw;
69372
69497
  }
69373
- function parseArgs4(args2) {
69498
+ function parseArgs5(args2) {
69374
69499
  const result = {
69375
69500
  out: "docs",
69376
69501
  lang: "auto",
@@ -69425,30 +69550,30 @@ function parseArgs4(args2) {
69425
69550
  return result;
69426
69551
  }
69427
69552
  async function handleDesignDocsCommand(directory, args2) {
69428
- const parsed = parseArgs4(args2);
69553
+ const parsed = parseArgs5(args2);
69429
69554
  if (parsed.error) {
69430
69555
  return `Error: ${parsed.error}
69431
69556
 
69432
- ${USAGE4}`;
69557
+ ${USAGE5}`;
69433
69558
  }
69434
69559
  try {
69435
69560
  const { config: config3 } = loadPluginConfigWithMeta(directory);
69436
69561
  if (config3.design_docs?.enabled !== true) {
69437
69562
  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
69563
 
69439
- ` + USAGE4;
69564
+ ` + USAGE5;
69440
69565
  }
69441
69566
  } catch (configErr) {
69442
69567
  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
69568
  }
69444
69569
  const description = sanitizeDescription(parsed.rest.join(" "));
69445
69570
  if (!description && !parsed.update) {
69446
- return USAGE4;
69571
+ return USAGE5;
69447
69572
  }
69448
69573
  const header = `[MODE: DESIGN_DOCS out=${parsed.out} lang=${parsed.lang} update=${parsed.update}] ${description}`;
69449
69574
  return header.trimEnd();
69450
69575
  }
69451
- var MAX_DESC_LEN = 2000, USAGE4 = `Usage: /swarm design-docs <description> [--out <dir>] [--lang <name>] [--update]
69576
+ var MAX_DESC_LEN = 2000, USAGE5 = `Usage: /swarm design-docs <description> [--out <dir>] [--lang <name>] [--update]
69452
69577
 
69453
69578
  Generate or sync language-agnostic design docs for the project under build:
69454
69579
  <out>/domain.md, <out>/technical-spec.md, <out>/behavior-spec.md,
@@ -70663,11 +70788,11 @@ async function getDiagnoseData(directory) {
70663
70788
  detail: "No snapshots yet (snapshots written on next session start)"
70664
70789
  });
70665
70790
  }
70666
- if (deferredWarnings.length > 0) {
70791
+ if (getDeferredWarnings().length > 0) {
70667
70792
  checks5.push({
70668
70793
  name: "Deferred Warnings",
70669
70794
  status: "⚠️",
70670
- detail: `${deferredWarnings.length} warning(s) deferred from init (run with verbose logs for details)`
70795
+ detail: `${getDeferredWarnings().length} warning(s) deferred from init (run with verbose logs for details)`
70671
70796
  });
70672
70797
  }
70673
70798
  const cachePaths = getPluginCachePaths();
@@ -70706,7 +70831,8 @@ async function getDiagnoseData(directory) {
70706
70831
  checks: checks5,
70707
70832
  passCount,
70708
70833
  totalCount,
70709
- allPassed
70834
+ allPassed,
70835
+ deferredWarnings: getDeferredWarnings()
70710
70836
  };
70711
70837
  }
70712
70838
  function formatDiagnoseMarkdown(diagnose) {
@@ -70717,11 +70843,11 @@ function formatDiagnoseMarkdown(diagnose) {
70717
70843
  "",
70718
70844
  `**Result**: ${diagnose.allPassed ? "✅ All checks passed" : `⚠️ ${diagnose.passCount}/${diagnose.totalCount} checks passed`}`
70719
70845
  ];
70720
- if (deferredWarnings.length > 0) {
70846
+ if (diagnose.deferredWarnings.length > 0) {
70721
70847
  lines.push("");
70722
70848
  lines.push("## Deferred Warnings");
70723
70849
  lines.push("");
70724
- for (const warning of deferredWarnings) {
70850
+ for (const warning of diagnose.deferredWarnings) {
70725
70851
  lines.push(`- ${warning}`);
70726
70852
  }
70727
70853
  }
@@ -75204,7 +75330,7 @@ function validateAndSanitizeUrl2(rawUrl) {
75204
75330
  return { error: "Invalid URL format" };
75205
75331
  }
75206
75332
  }
75207
- function parseArgs5(args2) {
75333
+ function parseArgs6(args2) {
75208
75334
  const out2 = {
75209
75335
  plan: false,
75210
75336
  trace: false,
@@ -75278,24 +75404,24 @@ function detectGitRemote2() {
75278
75404
  }
75279
75405
  }
75280
75406
  function handleIssueCommand(_directory, args2) {
75281
- const parsed = parseArgs5(args2);
75407
+ const parsed = parseArgs6(args2);
75282
75408
  const rawInput = parsed.rest.join(" ").trim();
75283
75409
  if (!rawInput) {
75284
- return USAGE5;
75410
+ return USAGE6;
75285
75411
  }
75286
75412
  const isFullUrl = /^https?:\/\//i.test(rawInput);
75287
75413
  const issueInfo = parseIssueRef(isFullUrl ? sanitizeUrl2(rawInput) : rawInput);
75288
75414
  if (!issueInfo) {
75289
75415
  return `Error: Could not parse issue reference from "${rawInput}"
75290
75416
 
75291
- ${USAGE5}`;
75417
+ ${USAGE6}`;
75292
75418
  }
75293
75419
  const issueUrl = `https://github.com/${issueInfo.owner}/${issueInfo.repo}/issues/${issueInfo.number}`;
75294
75420
  const result = validateAndSanitizeUrl2(issueUrl);
75295
75421
  if ("error" in result) {
75296
75422
  return `Error: ${result.error}
75297
75423
 
75298
- ${USAGE5}`;
75424
+ ${USAGE6}`;
75299
75425
  }
75300
75426
  const flags2 = [];
75301
75427
  if (parsed.plan)
@@ -75307,10 +75433,10 @@ ${USAGE5}`;
75307
75433
  const flagsStr = flags2.length > 0 ? ` ${flags2.join(" ")}` : "";
75308
75434
  return `[MODE: ISSUE_INGEST issue="${result.sanitized}"${flagsStr}]`;
75309
75435
  }
75310
- var MAX_URL_LEN2 = 2048, USAGE5;
75436
+ var MAX_URL_LEN2 = 2048, USAGE6;
75311
75437
  var init_issue = __esm(() => {
75312
75438
  init_pr_ref();
75313
- USAGE5 = [
75439
+ USAGE6 = [
75314
75440
  "Usage: /swarm issue <url|owner/repo#N|N> [--plan] [--trace] [--no-repro]",
75315
75441
  "",
75316
75442
  "Ingest a GitHub issue into the swarm workflow.",
@@ -80579,7 +80705,7 @@ var init_pr_monitor_status = __esm(() => {
80579
80705
  });
80580
80706
 
80581
80707
  // src/commands/pr-review.ts
80582
- function parseArgs6(args2) {
80708
+ function parseArgs7(args2) {
80583
80709
  const out2 = { council: false, rest: [] };
80584
80710
  for (const token of args2) {
80585
80711
  if (token === "--council") {
@@ -80598,29 +80724,29 @@ function parseArgs6(args2) {
80598
80724
  return out2;
80599
80725
  }
80600
80726
  function handlePrReviewCommand(directory, args2) {
80601
- const parsed = parseArgs6(args2);
80727
+ const parsed = parseArgs7(args2);
80602
80728
  if (parsed.unknownFlag) {
80603
80729
  return `Error: Unknown flag "${parsed.unknownFlag}"
80604
80730
 
80605
- ${USAGE6}`;
80731
+ ${USAGE7}`;
80606
80732
  }
80607
80733
  const resolved = resolvePrCommandInput(parsed.rest, directory);
80608
80734
  if (resolved === null) {
80609
- return USAGE6;
80735
+ return USAGE7;
80610
80736
  }
80611
80737
  if ("error" in resolved) {
80612
80738
  return `Error: ${resolved.error}
80613
80739
 
80614
- ${USAGE6}`;
80740
+ ${USAGE7}`;
80615
80741
  }
80616
80742
  const councilFlag = parsed.council ? "council=true" : "council=false";
80617
80743
  const signal = `[MODE: PR_REVIEW pr="${resolved.prUrl}" ${councilFlag}]`;
80618
80744
  return resolved.instructions ? `${signal} ${resolved.instructions}` : signal;
80619
80745
  }
80620
- var USAGE6;
80746
+ var USAGE7;
80621
80747
  var init_pr_review = __esm(() => {
80622
80748
  init_pr_ref();
80623
- USAGE6 = [
80749
+ USAGE7 = [
80624
80750
  "Usage: /swarm pr-review <url|owner/repo#N|N> [--council] [instructions...]",
80625
80751
  "",
80626
80752
  "Run a full swarm PR review on a GitHub pull request.",
@@ -87896,7 +88022,7 @@ var init_rollback = __esm(() => {
87896
88022
  });
87897
88023
 
87898
88024
  // src/commands/sdd.ts
87899
- function parseArgs7(args2) {
88025
+ function parseArgs8(args2) {
87900
88026
  const parsed = { json: false, dryRun: false };
87901
88027
  for (let i2 = 0;i2 < args2.length; i2++) {
87902
88028
  const token = args2[i2];
@@ -87927,11 +88053,11 @@ function formatList(items) {
87927
88053
  `) : "- none";
87928
88054
  }
87929
88055
  async function handleSddStatusCommand(directory, args2) {
87930
- const parsed = parseArgs7(args2);
88056
+ const parsed = parseArgs8(args2);
87931
88057
  if (parsed.error)
87932
88058
  return `Error: ${parsed.error}
87933
88059
 
87934
- ${USAGE7}`;
88060
+ ${USAGE8}`;
87935
88061
  const status = loadSddStatusSync(directory);
87936
88062
  if (parsed.json)
87937
88063
  return JSON.stringify(status, null, 2);
@@ -87965,11 +88091,11 @@ ${formatList([
87965
88091
  `);
87966
88092
  }
87967
88093
  async function handleSddValidateCommand(directory, args2) {
87968
- const parsed = parseArgs7(args2);
88094
+ const parsed = parseArgs8(args2);
87969
88095
  if (parsed.error)
87970
88096
  return `Error: ${parsed.error}
87971
88097
 
87972
- ${USAGE7}`;
88098
+ ${USAGE8}`;
87973
88099
  const status = loadSddStatusSync(directory);
87974
88100
  const projection = buildOpenSpecProjectionSync(directory, {
87975
88101
  changeId: parsed.changeId
@@ -88007,11 +88133,11 @@ ${formatList(result.warnings)}` : ""
88007
88133
  `);
88008
88134
  }
88009
88135
  async function handleSddProjectCommand(directory, args2) {
88010
- const parsed = parseArgs7(args2);
88136
+ const parsed = parseArgs8(args2);
88011
88137
  if (parsed.error)
88012
88138
  return `Error: ${parsed.error}
88013
88139
 
88014
- ${USAGE7}`;
88140
+ ${USAGE8}`;
88015
88141
  const result = writeProjectedSpecSync(directory, {
88016
88142
  changeId: parsed.changeId,
88017
88143
  dryRun: parsed.dryRun
@@ -88031,7 +88157,7 @@ ${USAGE7}`;
88031
88157
  return [
88032
88158
  "SDD projection failed: no valid OpenSpec-compatible projection could be built.",
88033
88159
  "",
88034
- USAGE7
88160
+ USAGE8
88035
88161
  ].join(`
88036
88162
  `);
88037
88163
  }
@@ -88048,9 +88174,9 @@ ${formatList(result.projection.warnings)}` : ""
88048
88174
  `);
88049
88175
  }
88050
88176
  async function handleSddCommand(_directory, _args) {
88051
- return USAGE7;
88177
+ return USAGE8;
88052
88178
  }
88053
- var USAGE7 = `Usage:
88179
+ var USAGE8 = `Usage:
88054
88180
  /swarm sdd status [--json]
88055
88181
  /swarm sdd validate [--json] [--change <id>]
88056
88182
  /swarm sdd project [--dry-run] [--json] [--change <id>]
@@ -89556,6 +89682,7 @@ __export(exports_commands, {
89556
89682
  handleEvidenceCommand: () => handleEvidenceCommand,
89557
89683
  handleDoctorCommand: () => handleDoctorCommand,
89558
89684
  handleDiagnoseCommand: () => handleDiagnoseCommand,
89685
+ handleDeepResearchCommand: () => handleDeepResearchCommand,
89559
89686
  handleDeepDiveCommand: () => handleDeepDiveCommand,
89560
89687
  handleDarkMatterCommand: () => handleDarkMatterCommand,
89561
89688
  handleCurateCommand: () => handleCurateCommand,
@@ -89834,6 +89961,7 @@ var init_commands = __esm(() => {
89834
89961
  init_curate();
89835
89962
  init_dark_matter();
89836
89963
  init_deep_dive();
89964
+ init_deep_research();
89837
89965
  init_diagnose();
89838
89966
  init_doctor();
89839
89967
  init_evidence();
@@ -90066,6 +90194,7 @@ var init_registry = __esm(() => {
90066
90194
  init_curate();
90067
90195
  init_dark_matter();
90068
90196
  init_deep_dive();
90197
+ init_deep_research();
90069
90198
  init_design_docs();
90070
90199
  init_diagnose();
90071
90200
  init_doctor();
@@ -90461,6 +90590,20 @@ Subcommands:
90461
90590
  category: "agent",
90462
90591
  aliasOf: "deep-dive"
90463
90592
  },
90593
+ "deep-research": {
90594
+ handler: (ctx) => handleModeCommandWithBundledSkills(ctx, handleDeepResearchCommand),
90595
+ description: "Launch a multi-source, fact-checked deep research pass and synthesize a cited report [question]",
90596
+ args: "<question> [--depth standard|exhaustive] [--max-researchers 1..6] [--rounds 1..4] [--brief]",
90597
+ 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.",
90598
+ category: "agent"
90599
+ },
90600
+ "deep research": {
90601
+ handler: (ctx) => handleModeCommandWithBundledSkills(ctx, handleDeepResearchCommand),
90602
+ description: "Alias for /swarm deep-research — launch a cited deep research pass",
90603
+ args: "<question> [--depth standard|exhaustive] [--max-researchers 1..6] [--rounds 1..4] [--brief]",
90604
+ category: "agent",
90605
+ aliasOf: "deep-research"
90606
+ },
90464
90607
  "codebase-review": {
90465
90608
  handler: (ctx) => handleModeCommandWithBundledSkills(ctx, handleCodebaseReviewCommand),
90466
90609
  description: "Launch codebase-review-swarm for a quote-grounded full-repo or large-subsystem audit",
@@ -91957,6 +92100,22 @@ HARD CONSTRAINTS (apply regardless of skill load success):
91957
92100
  - Explorers generate candidate findings only — reviewers verify or reject
91958
92101
  - Critics challenge only HIGH/CRITICAL findings — do NOT waste cycles on lower severity
91959
92102
 
92103
+ ### MODE: DEEP_RESEARCH
92104
+ 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.
92105
+
92106
+ 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.
92107
+
92108
+ ACTION: Load skill file:.opencode/skills/deep-research/SKILL.md immediately and follow its protocol.
92109
+
92110
+ HARD CONSTRAINTS (apply regardless of skill load success):
92111
+ - Do NOT delegate to coder
92112
+ - Do NOT call declare_scope
92113
+ - Do NOT mutate source code or write any files outside .swarm/
92114
+ - You (architect) own \`web_search\` and \`web_fetch\`; sme workers receive gathered evidence in their dispatch message — do NOT expect sme to fetch
92115
+ - Every claim in the final report MUST cite a source from the gathered evidence; reviewers verify claim↔citation before a claim is reported
92116
+ - Critics challenge only high-stakes / contested claims — do NOT waste cycles on well-supported ones
92117
+ - If council.general.enabled is false or no search API key is configured, surface that and STOP — do not produce ungrounded research
92118
+
91960
92119
  ### MODE: CODEBASE_REVIEW
91961
92120
  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
92121
 
@@ -101144,7 +101303,7 @@ var init_design_doc_drift = __esm(() => {
101144
101303
  var exports_project_context = {};
101145
101304
  __export(exports_project_context, {
101146
101305
  buildProjectContext: () => buildProjectContext,
101147
- _internals: () => _internals103,
101306
+ _internals: () => _internals104,
101148
101307
  LANG_BACKEND_DETECTION_TIMEOUT_MS: () => LANG_BACKEND_DETECTION_TIMEOUT_MS
101149
101308
  });
101150
101309
  import * as fs136 from "node:fs";
@@ -101228,7 +101387,7 @@ function selectLintCommand(backend, directory) {
101228
101387
  return null;
101229
101388
  }
101230
101389
  async function buildProjectContext(directory) {
101231
- const backend = await _internals103.pickBackend(directory);
101390
+ const backend = await _internals104.pickBackend(directory);
101232
101391
  if (!backend)
101233
101392
  return null;
101234
101393
  const ctx = emptyProjectContext();
@@ -101267,17 +101426,17 @@ async function buildProjectContext(directory) {
101267
101426
  if (backend.prompts.reviewerChecklist.length > 0) {
101268
101427
  ctx.REVIEWER_CHECKLIST = bulletList(backend.prompts.reviewerChecklist);
101269
101428
  }
101270
- const profiles = _internals103.pickedProfiles(directory);
101429
+ const profiles = _internals104.pickedProfiles(directory);
101271
101430
  if (profiles.length > 1) {
101272
101431
  ctx.PROJECT_CONTEXT_SECONDARY_LANGUAGES = profiles.slice(1).map((p) => p.id).join(", ");
101273
101432
  }
101274
101433
  return ctx;
101275
101434
  }
101276
- var LANG_BACKEND_DETECTION_TIMEOUT_MS = 300, _internals103;
101435
+ var LANG_BACKEND_DETECTION_TIMEOUT_MS = 300, _internals104;
101277
101436
  var init_project_context = __esm(() => {
101278
101437
  init_dispatch();
101279
101438
  init_framework_detector();
101280
- _internals103 = {
101439
+ _internals104 = {
101281
101440
  pickBackend,
101282
101441
  pickedProfiles
101283
101442
  };
@@ -140503,6 +140662,696 @@ var update_task_status = createSwarmTool({
140503
140662
  }
140504
140663
  });
140505
140664
 
140665
+ // src/tools/web-fetch.ts
140666
+ init_zod();
140667
+ init_loader();
140668
+ import { lookup } from "node:dns/promises";
140669
+ import * as http from "node:http";
140670
+ import * as https from "node:https";
140671
+ import { isIP } from "node:net";
140672
+ import { Readable } from "node:stream";
140673
+ import * as zlib from "node:zlib";
140674
+
140675
+ // src/evidence/documents.ts
140676
+ init_utils2();
140677
+ init_redaction();
140678
+ import { createHash as createHash16 } from "node:crypto";
140679
+ import { appendFile as appendFile17, mkdir as mkdir31 } from "node:fs/promises";
140680
+ import * as path188 from "node:path";
140681
+ var EVIDENCE_CACHE_FILE = "evidence-cache/documents.jsonl";
140682
+ var MAX_EVIDENCE_TEXT_LENGTH = 4000;
140683
+ async function writeEvidenceDocuments(directory, inputs, now = () => new Date) {
140684
+ const filePath = validateSwarmPath(directory, EVIDENCE_CACHE_FILE);
140685
+ const capturedAt = now().toISOString();
140686
+ const records = inputs.map((input) => createEvidenceDocumentRecord(input, capturedAt)).filter((record3) => record3 !== null);
140687
+ if (records.length > 0) {
140688
+ await mkdir31(path188.dirname(filePath), { recursive: true });
140689
+ await appendFile17(filePath, `${records.map((record3) => JSON.stringify(record3)).join(`
140690
+ `)}
140691
+ `, "utf-8");
140692
+ }
140693
+ return {
140694
+ path: ".swarm/evidence-cache/documents.jsonl",
140695
+ records,
140696
+ refs: records.map((record3) => record3.ref)
140697
+ };
140698
+ }
140699
+ function createEvidenceDocumentRecord(input, defaultCapturedAt) {
140700
+ const text = normalizeEvidenceText(input.text ?? input.snippet ?? "");
140701
+ if (!text)
140702
+ return null;
140703
+ const capturedAt = input.capturedAt ?? defaultCapturedAt;
140704
+ const base = {
140705
+ sourceType: input.sourceType,
140706
+ query: normalizeOptional(input.query),
140707
+ title: normalizeOptional(input.title),
140708
+ url: normalizeOptional(input.url),
140709
+ text
140710
+ };
140711
+ const id = createEvidenceDocumentId(base);
140712
+ return {
140713
+ id,
140714
+ ref: `evidence-cache:${id}`,
140715
+ ...base,
140716
+ capturedAt,
140717
+ createdBy: normalizeOptional(input.createdBy),
140718
+ metadata: input.metadata ?? {}
140719
+ };
140720
+ }
140721
+ function createEvidenceDocumentId(input) {
140722
+ const hash4 = createHash16("sha256").update([
140723
+ input.sourceType,
140724
+ input.query ?? "",
140725
+ input.title ?? "",
140726
+ input.url ?? "",
140727
+ input.text
140728
+ ].join(`
140729
+ `)).digest("hex");
140730
+ return `evd_${hash4.slice(0, 16)}`;
140731
+ }
140732
+ function normalizeEvidenceText(text) {
140733
+ const normalized = redactSecrets(text.replace(/\s+/g, " ").trim());
140734
+ return truncateEvidenceText(normalized, MAX_EVIDENCE_TEXT_LENGTH);
140735
+ }
140736
+ function truncateEvidenceText(text, maxLength) {
140737
+ if (text.length <= maxLength)
140738
+ return text;
140739
+ const truncated = text.slice(0, maxLength);
140740
+ const lastPlaceholderStart = truncated.lastIndexOf("[REDACTED:");
140741
+ const lastPlaceholderEnd = truncated.lastIndexOf("]");
140742
+ if (lastPlaceholderStart > lastPlaceholderEnd) {
140743
+ return truncated.slice(0, lastPlaceholderStart).trimEnd();
140744
+ }
140745
+ return truncated;
140746
+ }
140747
+ function normalizeOptional(value) {
140748
+ const normalized = value?.replace(/\s+/g, " ").trim();
140749
+ return normalized ? redactSecrets(normalized) : undefined;
140750
+ }
140751
+
140752
+ // src/tools/web-fetch.ts
140753
+ init_create_tool();
140754
+ init_resolve_working_directory();
140755
+ var DEFAULT_MAX_BYTES = 1e6;
140756
+ var MAX_BYTES_HARD_CAP = 5000000;
140757
+ var DEFAULT_TIMEOUT_MS4 = 15000;
140758
+ var MAX_TIMEOUT_MS2 = 30000;
140759
+ var MAX_REDIRECTS = 5;
140760
+ var MAX_TEXT_LENGTH2 = 50000;
140761
+ var ArgsSchema6 = exports_external.object({
140762
+ url: exports_external.string().min(1).max(2048),
140763
+ max_bytes: exports_external.number().int().min(1024).max(MAX_BYTES_HARD_CAP).optional(),
140764
+ timeout_ms: exports_external.number().int().min(1000).max(MAX_TIMEOUT_MS2).optional(),
140765
+ working_directory: exports_external.string().optional()
140766
+ });
140767
+ function isBlockedAddress(address) {
140768
+ const family = isIP(address);
140769
+ if (family === 4)
140770
+ return isBlockedIPv4(address);
140771
+ if (family === 6)
140772
+ return isBlockedIPv6(address);
140773
+ return true;
140774
+ }
140775
+ function isBlockedIPv4(address) {
140776
+ const parts2 = address.split(".");
140777
+ if (parts2.length !== 4)
140778
+ return true;
140779
+ const octets = parts2.map((p) => Number(p));
140780
+ if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255))
140781
+ return true;
140782
+ const [a, b] = octets;
140783
+ if (a === 0)
140784
+ return true;
140785
+ if (a === 10)
140786
+ return true;
140787
+ if (a === 127)
140788
+ return true;
140789
+ if (a === 169 && b === 254)
140790
+ return true;
140791
+ if (a === 172 && b >= 16 && b <= 31)
140792
+ return true;
140793
+ if (a === 192 && b === 168)
140794
+ return true;
140795
+ if (a === 100 && b >= 64 && b <= 127)
140796
+ return true;
140797
+ if (a === 198 && (b === 18 || b === 19))
140798
+ return true;
140799
+ if (a === 192 && b === 0 && octets[2] === 0)
140800
+ return true;
140801
+ if (a === 192 && b === 0 && octets[2] === 2)
140802
+ return true;
140803
+ if (a === 198 && b === 51 && octets[2] === 100)
140804
+ return true;
140805
+ if (a === 203 && b === 0 && octets[2] === 113)
140806
+ return true;
140807
+ if (a === 192 && b === 88 && octets[2] === 99)
140808
+ return true;
140809
+ if (a >= 224)
140810
+ return true;
140811
+ return false;
140812
+ }
140813
+ function expandIPv6(input) {
140814
+ let s = input.toLowerCase().split("%")[0];
140815
+ if (!s)
140816
+ return null;
140817
+ if (s.includes(".")) {
140818
+ const colon = s.lastIndexOf(":");
140819
+ if (colon === -1)
140820
+ return null;
140821
+ const v4 = s.slice(colon + 1).split(".");
140822
+ if (v4.length !== 4)
140823
+ return null;
140824
+ const o = v4.map((p) => Number(p));
140825
+ if (o.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
140826
+ return null;
140827
+ const h1 = (o[0] << 8 | o[1]).toString(16);
140828
+ const h2 = (o[2] << 8 | o[3]).toString(16);
140829
+ s = `${s.slice(0, colon + 1)}${h1}:${h2}`;
140830
+ }
140831
+ const halves = s.split("::");
140832
+ if (halves.length > 2)
140833
+ return null;
140834
+ const parseGroups = (part) => {
140835
+ if (part === "")
140836
+ return [];
140837
+ const groups = part.split(":");
140838
+ const out2 = [];
140839
+ for (const g of groups) {
140840
+ if (!/^[0-9a-f]{1,4}$/.test(g))
140841
+ return null;
140842
+ out2.push(Number.parseInt(g, 16));
140843
+ }
140844
+ return out2;
140845
+ };
140846
+ const head = parseGroups(halves[0]);
140847
+ if (head === null)
140848
+ return null;
140849
+ if (halves.length === 2) {
140850
+ const tail = parseGroups(halves[1]);
140851
+ if (tail === null)
140852
+ return null;
140853
+ const missing = 8 - head.length - tail.length;
140854
+ if (missing < 1)
140855
+ return null;
140856
+ return [...head, ...new Array(missing).fill(0), ...tail];
140857
+ }
140858
+ return head.length === 8 ? head : null;
140859
+ }
140860
+ function isBlockedIPv6(raw) {
140861
+ const h = expandIPv6(raw);
140862
+ if (!h)
140863
+ return true;
140864
+ if (h.every((x) => x === 0))
140865
+ return true;
140866
+ if (h.slice(0, 7).every((x) => x === 0) && h[7] === 1)
140867
+ return true;
140868
+ const mappedV4 = h.slice(0, 5).every((x) => x === 0) && h[5] === 65535;
140869
+ const compatV4 = h.slice(0, 6).every((x) => x === 0) && !(h[6] === 0 && h[7] <= 1);
140870
+ if (mappedV4 || compatV4) {
140871
+ const v4 = `${h[6] >> 8}.${h[6] & 255}.${h[7] >> 8}.${h[7] & 255}`;
140872
+ return isBlockedIPv4(v4);
140873
+ }
140874
+ const first = h[0];
140875
+ if (first >= 64512 && first <= 65023)
140876
+ return true;
140877
+ if (first >= 65152 && first <= 65215)
140878
+ return true;
140879
+ if (first >= 65280)
140880
+ return true;
140881
+ return false;
140882
+ }
140883
+ async function validateFetchUrl(candidate, dnsLookup) {
140884
+ let url3;
140885
+ try {
140886
+ url3 = new URL(candidate);
140887
+ } catch {
140888
+ return {
140889
+ ok: false,
140890
+ reason: "invalid_url",
140891
+ message: `Not a valid URL: ${candidate}`
140892
+ };
140893
+ }
140894
+ if (url3.protocol !== "http:" && url3.protocol !== "https:") {
140895
+ return {
140896
+ ok: false,
140897
+ reason: "blocked_scheme",
140898
+ message: `Only http and https URLs are allowed (got "${url3.protocol}").`
140899
+ };
140900
+ }
140901
+ const host = url3.hostname.replace(/^\[|\]$/g, "");
140902
+ if (isIP(host)) {
140903
+ if (isBlockedAddress(host)) {
140904
+ return {
140905
+ ok: false,
140906
+ reason: "blocked_host",
140907
+ message: `Refusing to fetch a private, loopback, or reserved address: ${host}`
140908
+ };
140909
+ }
140910
+ return { ok: true, url: url3, address: host };
140911
+ }
140912
+ let resolved;
140913
+ try {
140914
+ resolved = await dnsLookup(host, { all: true });
140915
+ } catch (err3) {
140916
+ return {
140917
+ ok: false,
140918
+ reason: "dns_failure",
140919
+ message: `Could not resolve host "${host}": ${err3 instanceof Error ? err3.message : String(err3)}`
140920
+ };
140921
+ }
140922
+ if (resolved.length === 0) {
140923
+ return {
140924
+ ok: false,
140925
+ reason: "dns_failure",
140926
+ message: `Host "${host}" resolved to no addresses.`
140927
+ };
140928
+ }
140929
+ for (const { address } of resolved) {
140930
+ if (isBlockedAddress(address)) {
140931
+ return {
140932
+ ok: false,
140933
+ reason: "blocked_host",
140934
+ message: `Host "${host}" resolves to a private, loopback, or reserved address (${address}).`
140935
+ };
140936
+ }
140937
+ }
140938
+ return { ok: true, url: url3, address: resolved[0].address };
140939
+ }
140940
+ function isAllowedContentType(contentType) {
140941
+ if (!contentType)
140942
+ return true;
140943
+ const type = contentType.split(";")[0].trim().toLowerCase();
140944
+ if (type.startsWith("text/"))
140945
+ return true;
140946
+ if (type === "application/json" || type === "application/xml")
140947
+ return true;
140948
+ if (type === "application/xhtml+xml" || type.endsWith("+json") || type.endsWith("+xml")) {
140949
+ return true;
140950
+ }
140951
+ return false;
140952
+ }
140953
+ function extractTitle(html) {
140954
+ const lower = html.toLowerCase();
140955
+ const start2 = lower.indexOf("<title");
140956
+ if (start2 === -1)
140957
+ return;
140958
+ const tagEnd = lower.indexOf(">", start2);
140959
+ if (tagEnd === -1)
140960
+ return;
140961
+ const contentStart = tagEnd + 1;
140962
+ const end = lower.indexOf("</title", contentStart);
140963
+ if (end === -1)
140964
+ return;
140965
+ const content = html.slice(contentStart, end);
140966
+ const title = decodeEntities(content.replace(/\s+/g, " ").trim());
140967
+ return title || undefined;
140968
+ }
140969
+ var NAMED_ENTITIES = {
140970
+ amp: "&",
140971
+ lt: "<",
140972
+ gt: ">",
140973
+ quot: '"',
140974
+ apos: "'",
140975
+ nbsp: " ",
140976
+ "#39": "'"
140977
+ };
140978
+ function decodeEntities(text) {
140979
+ return text.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (whole, body2) => {
140980
+ const key = body2.toLowerCase();
140981
+ if (key in NAMED_ENTITIES)
140982
+ return NAMED_ENTITIES[key];
140983
+ if (body2.startsWith("#x") || body2.startsWith("#X")) {
140984
+ const code = Number.parseInt(body2.slice(2), 16);
140985
+ return Number.isFinite(code) ? safeFromCodePoint(code) : whole;
140986
+ }
140987
+ if (body2.startsWith("#")) {
140988
+ const code = Number.parseInt(body2.slice(1), 10);
140989
+ return Number.isFinite(code) ? safeFromCodePoint(code) : whole;
140990
+ }
140991
+ return whole;
140992
+ });
140993
+ }
140994
+ function safeFromCodePoint(code) {
140995
+ try {
140996
+ return String.fromCodePoint(code);
140997
+ } catch {
140998
+ return "";
140999
+ }
141000
+ }
141001
+ function stripSpans(input, open3, close) {
141002
+ const lower = input.toLowerCase();
141003
+ let out2 = "";
141004
+ let i2 = 0;
141005
+ while (i2 < input.length) {
141006
+ const start2 = lower.indexOf(open3, i2);
141007
+ if (start2 === -1) {
141008
+ out2 += input.slice(i2);
141009
+ break;
141010
+ }
141011
+ out2 += input.slice(i2, start2);
141012
+ const end = lower.indexOf(close, start2 + open3.length);
141013
+ if (end === -1)
141014
+ break;
141015
+ i2 = end + close.length;
141016
+ }
141017
+ return out2;
141018
+ }
141019
+ function htmlToText(html) {
141020
+ let withoutScripts = stripSpans(html, "<script", "</script>");
141021
+ withoutScripts = stripSpans(withoutScripts, "<style", "</style>");
141022
+ withoutScripts = stripSpans(withoutScripts, "<noscript", "</noscript>");
141023
+ withoutScripts = stripSpans(withoutScripts, "<!--", "-->");
141024
+ const withBreaks = withoutScripts.replace(/<\/(p|div|li|h[1-6]|tr|section|article|header|footer)>/gi, `
141025
+ `).replace(/<br\s*\/?>/gi, `
141026
+ `);
141027
+ const noTags = withBreaks.replace(/<[^>]+>/g, " ");
141028
+ const decoded = decodeEntities(noTags);
141029
+ return decoded.replace(/[ \t\f\v]+/g, " ").replace(/\s*\n\s*/g, `
141030
+ `).replace(/\n{3,}/g, `
141031
+
141032
+ `).trim();
141033
+ }
141034
+ function makeAbortError() {
141035
+ try {
141036
+ return new DOMException("The operation was aborted", "AbortError");
141037
+ } catch {
141038
+ const err3 = new Error("The operation was aborted");
141039
+ err3.name = "AbortError";
141040
+ return err3;
141041
+ }
141042
+ }
141043
+ function performHttpRequest(args2) {
141044
+ const { url: url3, pinnedAddress, signal, headers } = args2;
141045
+ const isHttps = url3.protocol === "https:";
141046
+ const port = url3.port ? Number(url3.port) : isHttps ? 443 : 80;
141047
+ const hostNoBrackets = url3.hostname.replace(/^\[|\]$/g, "");
141048
+ const useSni = isHttps && isIP(hostNoBrackets) === 0;
141049
+ const options = {
141050
+ host: pinnedAddress,
141051
+ port,
141052
+ path: `${url3.pathname}${url3.search}`,
141053
+ method: "GET",
141054
+ headers: { Host: url3.host, ...headers }
141055
+ };
141056
+ if (useSni)
141057
+ options.servername = url3.hostname;
141058
+ return new Promise((resolve72, reject) => {
141059
+ let req;
141060
+ const onResponse = (res) => {
141061
+ const normHeaders = {};
141062
+ for (const [key, value] of Object.entries(res.headers)) {
141063
+ normHeaders[key] = Array.isArray(value) ? value[0] : value;
141064
+ }
141065
+ res.on("error", () => {});
141066
+ resolve72({
141067
+ status: res.statusCode ?? 0,
141068
+ headers: normHeaders,
141069
+ body: res,
141070
+ cancel: () => req.destroy()
141071
+ });
141072
+ };
141073
+ req = isHttps ? https.request(options, onResponse) : http.request(options, onResponse);
141074
+ const onAbort = () => req.destroy(makeAbortError());
141075
+ if (signal.aborted)
141076
+ req.destroy(makeAbortError());
141077
+ else
141078
+ signal.addEventListener("abort", onAbort, { once: true });
141079
+ req.on("error", reject);
141080
+ req.end();
141081
+ });
141082
+ }
141083
+ async function boundedFetch(start2, maxBytes, timeoutMs, deps) {
141084
+ let current = start2;
141085
+ const controller = new AbortController;
141086
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
141087
+ try {
141088
+ for (let hop = 0;hop <= MAX_REDIRECTS; hop++) {
141089
+ let raw;
141090
+ try {
141091
+ raw = await deps.httpRequest({
141092
+ url: current.url,
141093
+ pinnedAddress: current.address,
141094
+ signal: controller.signal,
141095
+ headers: {
141096
+ Accept: "text/html,application/xhtml+xml,text/plain,application/json;q=0.9,*/*;q=0.5"
141097
+ }
141098
+ });
141099
+ } catch (err3) {
141100
+ if (controller.signal.aborted) {
141101
+ return {
141102
+ ok: false,
141103
+ reason: "timeout",
141104
+ message: `Fetch exceeded ${timeoutMs}ms for ${current.url.toString()}`
141105
+ };
141106
+ }
141107
+ return {
141108
+ ok: false,
141109
+ reason: "network_error",
141110
+ message: err3 instanceof Error ? err3.message : String(err3)
141111
+ };
141112
+ }
141113
+ if (raw.status >= 300 && raw.status < 400) {
141114
+ const location = raw.headers.location;
141115
+ raw.cancel?.();
141116
+ if (!location) {
141117
+ return {
141118
+ ok: false,
141119
+ reason: "bad_redirect",
141120
+ message: `Redirect ${raw.status} with no Location header.`
141121
+ };
141122
+ }
141123
+ let next;
141124
+ try {
141125
+ next = new URL(location, current.url);
141126
+ } catch {
141127
+ return {
141128
+ ok: false,
141129
+ reason: "bad_redirect",
141130
+ message: `Invalid redirect target: ${location}`
141131
+ };
141132
+ }
141133
+ const revalidated = await validateFetchUrl(next.toString(), deps.dnsLookup);
141134
+ if (!revalidated.ok)
141135
+ return revalidated;
141136
+ if (hop === MAX_REDIRECTS) {
141137
+ return {
141138
+ ok: false,
141139
+ reason: "too_many_redirects",
141140
+ message: `Exceeded ${MAX_REDIRECTS} redirect hops.`
141141
+ };
141142
+ }
141143
+ current = { url: revalidated.url, address: revalidated.address };
141144
+ continue;
141145
+ }
141146
+ if (raw.status < 200 || raw.status >= 300) {
141147
+ raw.cancel?.();
141148
+ return {
141149
+ ok: false,
141150
+ reason: "http_error",
141151
+ message: `HTTP ${raw.status} for ${current.url.toString()}`
141152
+ };
141153
+ }
141154
+ const contentType = raw.headers["content-type"] ?? null;
141155
+ if (!isAllowedContentType(contentType)) {
141156
+ raw.cancel?.();
141157
+ return {
141158
+ ok: false,
141159
+ reason: "unsupported_content_type",
141160
+ message: `Refusing to read non-text content type "${contentType}".`
141161
+ };
141162
+ }
141163
+ let activeBody = raw.body;
141164
+ if (raw.body !== null && raw.body instanceof Readable) {
141165
+ const encoding = (raw.headers["content-encoding"] ?? "").toLowerCase();
141166
+ let decoder = null;
141167
+ if (encoding === "gzip" || encoding === "x-gzip")
141168
+ decoder = raw.body.pipe(zlib.createGunzip());
141169
+ else if (encoding === "deflate")
141170
+ decoder = raw.body.pipe(zlib.createInflate());
141171
+ else if (encoding === "br")
141172
+ decoder = raw.body.pipe(zlib.createBrotliDecompress());
141173
+ if (decoder) {
141174
+ decoder.on("error", () => {});
141175
+ activeBody = decoder;
141176
+ }
141177
+ }
141178
+ let body2;
141179
+ try {
141180
+ body2 = await readBounded(activeBody, maxBytes);
141181
+ } catch (err3) {
141182
+ raw.cancel?.();
141183
+ if (controller.signal.aborted) {
141184
+ return {
141185
+ ok: false,
141186
+ reason: "timeout",
141187
+ message: `Fetch exceeded ${timeoutMs}ms while reading the body of ${current.url.toString()}`
141188
+ };
141189
+ }
141190
+ return {
141191
+ ok: false,
141192
+ reason: "network_error",
141193
+ message: err3 instanceof Error ? err3.message : String(err3)
141194
+ };
141195
+ }
141196
+ raw.cancel?.();
141197
+ return {
141198
+ ok: true,
141199
+ outcome: {
141200
+ status: raw.status,
141201
+ finalUrl: current.url.toString(),
141202
+ contentType,
141203
+ bytes: body2.bytes,
141204
+ truncated: body2.truncated
141205
+ }
141206
+ };
141207
+ }
141208
+ return {
141209
+ ok: false,
141210
+ reason: "too_many_redirects",
141211
+ message: `Exceeded ${MAX_REDIRECTS} redirect hops.`
141212
+ };
141213
+ } finally {
141214
+ clearTimeout(timer);
141215
+ }
141216
+ }
141217
+ async function readBounded(body2, maxBytes) {
141218
+ if (!body2)
141219
+ return { bytes: new Uint8Array(0), truncated: false };
141220
+ const chunks = [];
141221
+ let received = 0;
141222
+ let truncated = false;
141223
+ for await (const value of body2) {
141224
+ if (!value || value.byteLength === 0)
141225
+ continue;
141226
+ chunks.push(value);
141227
+ received += value.byteLength;
141228
+ if (received > maxBytes) {
141229
+ truncated = true;
141230
+ break;
141231
+ }
141232
+ }
141233
+ const merged = new Uint8Array(Math.min(received, maxBytes));
141234
+ let offset = 0;
141235
+ for (const chunk of chunks) {
141236
+ if (offset >= merged.length)
141237
+ break;
141238
+ const slice = chunk.subarray(0, merged.length - offset);
141239
+ merged.set(slice, offset);
141240
+ offset += slice.byteLength;
141241
+ }
141242
+ return { bytes: merged, truncated };
141243
+ }
141244
+ var web_fetch = createSwarmTool({
141245
+ 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.",
141246
+ args: {
141247
+ url: exports_external.string().min(1).max(2048).describe("Absolute http(s) URL to fetch (1–2048 chars)."),
141248
+ 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}).`),
141249
+ 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}).`),
141250
+ working_directory: exports_external.string().optional().describe("Project root for config resolution and evidence storage. Optional.")
141251
+ },
141252
+ execute: async (args2, directory) => {
141253
+ const parsed = ArgsSchema6.safeParse(args2);
141254
+ if (!parsed.success) {
141255
+ const fail = {
141256
+ success: false,
141257
+ reason: "invalid_args",
141258
+ message: parsed.error.issues.map((i2) => `${i2.path.join(".")}: ${i2.message}`).join("; ")
141259
+ };
141260
+ return JSON.stringify(fail, null, 2);
141261
+ }
141262
+ const dirResult = resolveWorkingDirectory(parsed.data.working_directory, directory);
141263
+ if (!dirResult.success) {
141264
+ const fail = {
141265
+ success: false,
141266
+ reason: "invalid_working_directory",
141267
+ message: dirResult.message
141268
+ };
141269
+ return JSON.stringify(fail, null, 2);
141270
+ }
141271
+ const config3 = _internals102.loadPluginConfig(dirResult.directory);
141272
+ const generalConfig = config3.council?.general;
141273
+ if (!generalConfig || generalConfig.enabled !== true) {
141274
+ const fail = {
141275
+ success: false,
141276
+ reason: "council_general_disabled",
141277
+ 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."
141278
+ };
141279
+ return JSON.stringify(fail, null, 2);
141280
+ }
141281
+ const validated = await validateFetchUrl(parsed.data.url, _internals102.dnsLookup);
141282
+ if (!validated.ok) {
141283
+ const fail = {
141284
+ success: false,
141285
+ reason: validated.reason,
141286
+ message: validated.message
141287
+ };
141288
+ return JSON.stringify(fail, null, 2);
141289
+ }
141290
+ const maxBytes = parsed.data.max_bytes ?? DEFAULT_MAX_BYTES;
141291
+ const timeoutMs = parsed.data.timeout_ms ?? DEFAULT_TIMEOUT_MS4;
141292
+ const result = await boundedFetch({ url: validated.url, address: validated.address }, maxBytes, timeoutMs, _internals102);
141293
+ if (!result.ok) {
141294
+ const fail = {
141295
+ success: false,
141296
+ reason: result.reason,
141297
+ message: result.message
141298
+ };
141299
+ return JSON.stringify(fail, null, 2);
141300
+ }
141301
+ const { outcome } = result;
141302
+ const raw = new TextDecoder("utf-8", { fatal: false }).decode(outcome.bytes);
141303
+ const isHtml = (outcome.contentType ?? "").toLowerCase().includes("html") || /<html|<!doctype html/i.test(raw);
141304
+ const title = isHtml ? extractTitle(raw) : undefined;
141305
+ const bodyText = isHtml ? htmlToText(raw) : raw.replace(/\s*\n\s*/g, `
141306
+ `).trim();
141307
+ const text = bodyText.length > MAX_TEXT_LENGTH2 ? `${bodyText.slice(0, MAX_TEXT_LENGTH2)}…` : bodyText;
141308
+ const textTruncated = outcome.truncated || bodyText.length > MAX_TEXT_LENGTH2;
141309
+ const evidence = await captureFetchEvidence(dirResult.directory, outcome.finalUrl, title, text);
141310
+ const ok2 = {
141311
+ success: true,
141312
+ url: parsed.data.url,
141313
+ finalUrl: outcome.finalUrl,
141314
+ status: outcome.status,
141315
+ contentType: outcome.contentType ?? undefined,
141316
+ title,
141317
+ text,
141318
+ truncated: textTruncated,
141319
+ bytesReturned: outcome.bytes.byteLength,
141320
+ evidence
141321
+ };
141322
+ return JSON.stringify(ok2, null, 2);
141323
+ }
141324
+ });
141325
+ async function captureFetchEvidence(directory, url3, title, text) {
141326
+ try {
141327
+ const written = await _internals102.writeEvidenceDocuments(directory, [
141328
+ {
141329
+ sourceType: "crawl",
141330
+ url: url3,
141331
+ title,
141332
+ text,
141333
+ createdBy: "web_fetch"
141334
+ }
141335
+ ]);
141336
+ return {
141337
+ stored: written.records.length > 0,
141338
+ ref: written.refs[0],
141339
+ path: written.path
141340
+ };
141341
+ } catch (err3) {
141342
+ return {
141343
+ stored: false,
141344
+ error: err3 instanceof Error ? err3.message : String(err3)
141345
+ };
141346
+ }
141347
+ }
141348
+ var _internals102 = {
141349
+ httpRequest: performHttpRequest,
141350
+ dnsLookup: lookup,
141351
+ loadPluginConfig,
141352
+ writeEvidenceDocuments
141353
+ };
141354
+
140506
141355
  // src/tools/web-search.ts
140507
141356
  init_zod();
140508
141357
  init_loader();
@@ -140699,88 +141548,11 @@ function createWebSearchProvider(config3) {
140699
141548
  }
140700
141549
  }
140701
141550
 
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
141551
  // src/tools/web-search.ts
140780
141552
  init_create_tool();
140781
141553
  init_resolve_working_directory();
140782
141554
  var MAX_RESULTS_HARD_CAP = 10;
140783
- var ArgsSchema6 = exports_external.object({
141555
+ var ArgsSchema7 = exports_external.object({
140784
141556
  query: exports_external.string().min(1).max(500),
140785
141557
  max_results: exports_external.number().int().min(1).max(20).optional(),
140786
141558
  freshness: exports_external.enum(["auto", "none", "day", "week", "month", "year"]).default("auto"),
@@ -140795,7 +141567,7 @@ var web_search = createSwarmTool({
140795
141567
  working_directory: exports_external.string().optional().describe("Project root for config resolution. Optional.")
140796
141568
  },
140797
141569
  execute: async (args2, directory) => {
140798
- const parsed = ArgsSchema6.safeParse(args2);
141570
+ const parsed = ArgsSchema7.safeParse(args2);
140799
141571
  if (!parsed.success) {
140800
141572
  const fail = {
140801
141573
  success: false,
@@ -140878,7 +141650,7 @@ var web_search = createSwarmTool({
140878
141650
  });
140879
141651
  async function captureSearchEvidence(directory, query, results) {
140880
141652
  try {
140881
- const written = await _internals102.writeEvidenceDocuments(directory, results.map((result) => ({
141653
+ const written = await _internals103.writeEvidenceDocuments(directory, results.map((result) => ({
140882
141654
  sourceType: "web_search",
140883
141655
  query,
140884
141656
  title: result.title,
@@ -140906,7 +141678,7 @@ async function captureSearchEvidence(directory, query, results) {
140906
141678
  };
140907
141679
  }
140908
141680
  }
140909
- var _internals102 = {
141681
+ var _internals103 = {
140910
141682
  writeEvidenceDocuments
140911
141683
  };
140912
141684
 
@@ -140937,7 +141709,7 @@ var KnowledgeRecommendationSchema2 = exports_external.object({
140937
141709
  confidence: exports_external.number().min(0).max(1).default(0.5),
140938
141710
  evidence_refs: exports_external.array(exports_external.string().min(1)).default([])
140939
141711
  });
140940
- var ArgsSchema7 = exports_external.object({
141712
+ var ArgsSchema8 = exports_external.object({
140941
141713
  phase: exports_external.number().int().min(0).max(999),
140942
141714
  verdict: exports_external.enum(["APPROVE", "CONCERNS", "REJECT"]),
140943
141715
  findings: exports_external.array(FindingSchema2).default([]),
@@ -140958,7 +141730,7 @@ var write_architecture_supervisor_evidence = createSwarmTool({
140958
141730
  provenance_session_id: exports_external.string().min(1).optional().describe("Session ID of the agent that produced this evidence (optional provenance).")
140959
141731
  },
140960
141732
  execute: async (rawArgs, directory) => {
140961
- const parsed = ArgsSchema7.safeParse(rawArgs);
141733
+ const parsed = ArgsSchema8.safeParse(rawArgs);
140962
141734
  if (!parsed.success) {
140963
141735
  return JSON.stringify({
140964
141736
  success: false,
@@ -141237,7 +142009,7 @@ var VerdictSchema3 = exports_external.object({
141237
142009
  criteriaUnmet: exports_external.array(exports_external.string()),
141238
142010
  durationMs: exports_external.number().nonnegative()
141239
142011
  });
141240
- var ArgsSchema8 = exports_external.object({
142012
+ var ArgsSchema9 = exports_external.object({
141241
142013
  phase: exports_external.number().int().min(1),
141242
142014
  projectSummary: exports_external.string().min(1),
141243
142015
  roundNumber: exports_external.number().int().min(1).max(10).optional(),
@@ -141253,7 +142025,7 @@ function normalizeFinalVerdict(verdict, requiredFixesCount) {
141253
142025
  return requiredFixesCount > 0 ? "rejected" : "concerns";
141254
142026
  }
141255
142027
  async function executeWriteFinalCouncilEvidence(args2, directory) {
141256
- const parsed = ArgsSchema8.safeParse(args2);
142028
+ const parsed = ArgsSchema9.safeParse(args2);
141257
142029
  if (!parsed.success) {
141258
142030
  return JSON.stringify({
141259
142031
  success: false,
@@ -141373,7 +142145,7 @@ var write_final_council_evidence = createSwarmTool({
141373
142145
  verdicts: exports_external.array(VerdictSchema3).min(1).max(5).describe("Collected CouncilMemberVerdict objects from critic, reviewer, sme, test_engineer, and explorer.")
141374
142146
  },
141375
142147
  execute: async (args2, directory) => {
141376
- const parsed = ArgsSchema8.safeParse(args2);
142148
+ const parsed = ArgsSchema9.safeParse(args2);
141377
142149
  if (!parsed.success) {
141378
142150
  return JSON.stringify({
141379
142151
  success: false,
@@ -141707,6 +142479,7 @@ var TOOL_MANIFEST = defineHandlers({
141707
142479
  get_qa_gate_profile: () => get_qa_gate_profile,
141708
142480
  set_qa_gates: () => set_qa_gates,
141709
142481
  web_search: () => web_search,
142482
+ web_fetch: () => web_fetch,
141710
142483
  convene_general_council: () => convene_general_council,
141711
142484
  write_final_council_evidence: () => write_final_council_evidence,
141712
142485
  skill_generate: () => skill_generate,
@@ -141834,7 +142607,7 @@ var OpenCodeSwarm = async (ctx) => {
141834
142607
  };
141835
142608
  async function initializeOpenCodeSwarm(ctx) {
141836
142609
  const { config: config3, loadedFromFile } = await loadPluginConfigWithMetaAsync(ctx.directory);
141837
- deferredWarnings.length = 0;
142610
+ clearDeferredWarnings();
141838
142611
  if (config3.full_auto?.enabled === true) {
141839
142612
  const criticModel = config3.full_auto.critic_model ?? config3.agents?.critic?.model ?? DEFAULT_MODELS.critic;
141840
142613
  const architectModel = config3.agents?.architect?.model ?? DEFAULT_MODELS.default;
@@ -142389,6 +143162,10 @@ async function initializeOpenCodeSwarm(ctx) {
142389
143162
  template: "/swarm deep-dive $ARGUMENTS",
142390
143163
  description: "Use /swarm deep-dive to launch a read-only deep audit with parallel explorer waves, dual reviewers, and critic challenge"
142391
143164
  },
143165
+ "swarm-deep-research": {
143166
+ template: "/swarm deep-research $ARGUMENTS",
143167
+ 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]"
143168
+ },
142392
143169
  "swarm-codebase-review": {
142393
143170
  template: "/swarm codebase-review $ARGUMENTS",
142394
143171
  description: "Use /swarm codebase-review to launch codebase-review-swarm for a quote-grounded full-repo or large-subsystem audit"