opencode-swarm 7.73.2 → 7.74.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.
@@ -61,6 +61,25 @@ final state. Expect N rounds of review for N pushes, and budget for it.
61
61
  each round's work is bounded by new findings + carried-forward items only.
62
62
  This matches how the bot actually behaves and avoids wasted cycles.
63
63
 
64
+ ### Bot Review Verification Traps
65
+
66
+ When a bot or pasted review cites a code fact, verify the fact against the
67
+ current branch before editing:
68
+
69
+ - **Import/export claims:** Check the exact import path used by the changed file.
70
+ A symbol may be missing from an internal submodule but correctly exported by the
71
+ public barrel the tests or runtime actually import.
72
+ - **Line numbers:** Treat bot line references as approximate after any follow-up
73
+ push or local edit. Re-locate the symbol or block with `rg` before patching.
74
+ - **Ordering claims:** If the concern is about rule precedence, add or run a
75
+ direct precedence test that would fail under the wrong ordering; comments alone
76
+ are not enough.
77
+ - **Disproved findings:** Do not change unrelated code to satisfy a false claim.
78
+ Keep the finding in the closure ledger with the source or test evidence that
79
+ disproves it.
80
+ - **Cache/state claims:** Test both relevant state orders when the behavior
81
+ depends on cache priming, singleton state, or prior calls.
82
+
64
83
  ## Operating Stance
65
84
 
66
85
  Treat every review comment, CI failure, bot summary, PR body claim, and pasted note
package/dist/cli/index.js CHANGED
@@ -52,7 +52,7 @@ var package_default;
52
52
  var init_package = __esm(() => {
53
53
  package_default = {
54
54
  name: "opencode-swarm",
55
- version: "7.73.2",
55
+ version: "7.74.0",
56
56
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
57
57
  main: "dist/index.js",
58
58
  types: "dist/index.d.ts",
@@ -14700,7 +14700,7 @@ var init_plan_schema = __esm(() => {
14700
14700
  init_zod();
14701
14701
  ExecutionProfileSchema = exports_external.object({
14702
14702
  parallelization_enabled: exports_external.boolean().default(false),
14703
- max_concurrent_tasks: exports_external.number().int().min(1).max(64).default(1),
14703
+ max_concurrent_tasks: exports_external.number().int().min(1).max(64).default(10),
14704
14704
  council_parallel: exports_external.boolean().default(true),
14705
14705
  locked: exports_external.boolean().default(false),
14706
14706
  auto_proceed: exports_external.boolean().default(false)
@@ -40093,6 +40093,9 @@ import * as path19 from "path";
40093
40093
  function resolveLogPath(directory) {
40094
40094
  return validateSwarmPath(directory, "skill-usage.jsonl");
40095
40095
  }
40096
+ function normalizeComplianceVerdict(verdict) {
40097
+ return verdict === "violation" ? "violated" : verdict;
40098
+ }
40096
40099
  function appendSkillUsageEntry(directory, entry) {
40097
40100
  const {
40098
40101
  skillPath,
@@ -40163,7 +40166,9 @@ function readSkillUsageEntries(directory, options) {
40163
40166
  if (!trimmed)
40164
40167
  continue;
40165
40168
  try {
40166
- entries.push(JSON.parse(trimmed));
40169
+ const entry = JSON.parse(trimmed);
40170
+ entry.complianceVerdict = normalizeComplianceVerdict(entry.complianceVerdict);
40171
+ entries.push(entry);
40167
40172
  } catch {}
40168
40173
  }
40169
40174
  if (!options)
@@ -40222,6 +40227,7 @@ function readSkillUsageEntriesTail(directory, filters, maxBytes = TAIL_BYTES_DEF
40222
40227
  continue;
40223
40228
  try {
40224
40229
  const entry = JSON.parse(line);
40230
+ entry.complianceVerdict = normalizeComplianceVerdict(entry.complianceVerdict);
40225
40231
  if (filters.sessionID !== undefined && entry.sessionID !== filters.sessionID) {
40226
40232
  continue;
40227
40233
  }
@@ -40256,7 +40262,7 @@ function computeComplianceByVersion(entries, skillPath) {
40256
40262
  stats.total += 1;
40257
40263
  if (e.complianceVerdict === "compliant")
40258
40264
  stats.compliant += 1;
40259
- if (e.complianceVerdict === "violation")
40265
+ if (e.complianceVerdict === "violated")
40260
40266
  stats.violation += 1;
40261
40267
  }
40262
40268
  for (const stats of map3.values()) {
@@ -40369,7 +40375,7 @@ async function applySkillUsageFeedback(directory, options) {
40369
40375
  try {
40370
40376
  const allEntries = readSkillUsageEntries(directory);
40371
40377
  const actionable = allEntries.filter((e) => {
40372
- if (e.complianceVerdict !== "compliant" && e.complianceVerdict !== "violation") {
40378
+ if (e.complianceVerdict !== "compliant" && e.complianceVerdict !== "violated") {
40373
40379
  return false;
40374
40380
  }
40375
40381
  if (options?.sinceTimestamp && e.timestamp <= options.sinceTimestamp) {
@@ -40395,7 +40401,7 @@ async function applySkillUsageFeedback(directory, options) {
40395
40401
  for (const entry of entries) {
40396
40402
  if (entry.complianceVerdict === "compliant")
40397
40403
  compliantCount++;
40398
- else if (entry.complianceVerdict === "violation")
40404
+ else if (entry.complianceVerdict === "violated")
40399
40405
  violationCount++;
40400
40406
  }
40401
40407
  if (compliantCount === 0 && violationCount === 0)
@@ -45184,7 +45190,7 @@ function buildStatusMessage(session, plan) {
45184
45190
  const overrideActive = session.maxConcurrencyOverride !== undefined;
45185
45191
  const configuredOverride = session.maxConcurrencyOverride ?? "absent";
45186
45192
  const hasPlan = plan !== null && plan !== undefined;
45187
- const planBaseline = hasPlan ? plan.execution_profile?.max_concurrent_tasks ?? 1 : 1;
45193
+ const planBaseline = hasPlan ? plan.execution_profile?.max_concurrent_tasks ?? 10 : 10;
45188
45194
  const parallelizationEnabled = hasPlan ? plan.execution_profile?.parallelization_enabled ?? false : false;
45189
45195
  const operationalEffective = !parallelizationEnabled ? 1 : session.maxConcurrencyOverride ?? planBaseline;
45190
45196
  let description;
@@ -45213,8 +45219,8 @@ var init_concurrency = __esm(() => {
45213
45219
  init_state();
45214
45220
  PRESETS = {
45215
45221
  min: 1,
45216
- medium: 3,
45217
- max: 8
45222
+ medium: 8,
45223
+ max: 16
45218
45224
  };
45219
45225
  });
45220
45226
 
@@ -51472,8 +51478,8 @@ var init_history = __esm(() => {
51472
51478
  init_history_service();
51473
51479
  });
51474
51480
 
51475
- // src/commands/pr-ref.ts
51476
- import { execSync as execSync2 } from "child_process";
51481
+ // src/commands/_shared/url-security.ts
51482
+ import { spawnSync as spawnSync3 } from "child_process";
51477
51483
  function sanitizeUrl(raw) {
51478
51484
  let urlStr = raw.trim();
51479
51485
  urlStr = urlStr.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
@@ -51491,13 +51497,29 @@ function sanitizeUrl(raw) {
51491
51497
  }
51492
51498
  return urlStr.trim();
51493
51499
  }
51494
- function sanitizeInstructions(raw) {
51495
- const collapsed = raw.replace(/\s+/g, " ").trim();
51496
- const stripped = collapsed.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
51497
- const normalized = stripped.replace(/\s+/g, " ").trim();
51498
- if (normalized.length <= MAX_INSTRUCTIONS_LEN)
51499
- return normalized;
51500
- return `${normalized.slice(0, MAX_INSTRUCTIONS_LEN)}\u2026`;
51500
+ function sanitizeErrorEcho(raw, maxLength = 80) {
51501
+ let stripped = "";
51502
+ for (const ch of raw) {
51503
+ const cp = ch.codePointAt(0);
51504
+ if (cp !== undefined && (cp <= 31 || cp === 127)) {
51505
+ stripped += " ";
51506
+ continue;
51507
+ }
51508
+ stripped += ch;
51509
+ }
51510
+ const collapsed = stripped.replace(/\s+/g, " ").trim();
51511
+ if (collapsed.length <= maxLength)
51512
+ return collapsed;
51513
+ return `${collapsed.slice(0, maxLength)}\u2026`;
51514
+ }
51515
+ function containsControlCharacters(value) {
51516
+ for (const ch of value) {
51517
+ const cp = ch.codePointAt(0);
51518
+ if (cp !== undefined && (cp <= 31 || cp === 127)) {
51519
+ return true;
51520
+ }
51521
+ }
51522
+ return false;
51501
51523
  }
51502
51524
  function hasNonAsciiHostname(hostname5) {
51503
51525
  for (const ch of hostname5) {
@@ -51507,31 +51529,38 @@ function hasNonAsciiHostname(hostname5) {
51507
51529
  }
51508
51530
  return false;
51509
51531
  }
51532
+ function isIpv4MappedPrivateHost(inner) {
51533
+ if (IPV4_PRIVATE.test(inner) || IPV4_LOOPBACK.test(inner) || IPV4_LINK_LOCAL.test(inner) || IPV4_PRIVATE_172.test(inner) || IPV4_PRIVATE_192.test(inner) || IPV4_ZERO_NETWORK.test(inner)) {
51534
+ return true;
51535
+ }
51536
+ const firstSegment = inner.split(":", 1)[0];
51537
+ if (!firstSegment)
51538
+ return false;
51539
+ const firstWord = Number.parseInt(firstSegment, 16);
51540
+ if (!Number.isFinite(firstWord))
51541
+ return false;
51542
+ return firstWord >= 0 && firstWord <= 255 || firstWord >= 2560 && firstWord <= 2815 || firstWord >= 32512 && firstWord <= 32767 || firstWord === 43518 || firstWord >= 44048 && firstWord <= 44063 || firstWord === 49320;
51543
+ }
51510
51544
  function isPrivateHost(url3) {
51511
- const host = url3.hostname.toLowerCase();
51512
- if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0") {
51545
+ const host = url3.hostname.toLowerCase().replace(/^\[|\]$/g, "");
51546
+ if (host === "localhost" || host === "::1" || host === "0.0.0.0" || IPV4_LOOPBACK.test(host) || IPV4_ZERO_NETWORK.test(host)) {
51513
51547
  return true;
51514
51548
  }
51515
51549
  if (host.startsWith("localhost") || host === "localhost.com") {
51516
51550
  return true;
51517
51551
  }
51518
- const ipv4Private = /^10\./;
51519
- const ipv4172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
51520
- const ipv4192 = /^192\.168\./;
51521
- const ipv6Private = /^fe80:/i;
51522
- const ipv6Unique = /^f[cd][0-9a-f]{2}:/i;
51523
- if (ipv4Private.test(host) || ipv4172.test(host) || ipv4192.test(host) || ipv6Private.test(host) || ipv6Unique.test(host)) {
51552
+ if (IPV4_PRIVATE.test(host) || IPV4_LINK_LOCAL.test(host) || IPV4_PRIVATE_172.test(host) || IPV4_PRIVATE_192.test(host) || IPV6_LINK_LOCAL.test(host) || IPV6_UNIQUE_LOCAL.test(host)) {
51524
51553
  return true;
51525
51554
  }
51526
51555
  if (host.startsWith("::ffff:")) {
51527
51556
  const inner = host.slice(7);
51528
- if (ipv4Private.test(inner) || ipv4172.test(inner) || ipv4192.test(inner)) {
51557
+ if (isIpv4MappedPrivateHost(inner)) {
51529
51558
  return true;
51530
51559
  }
51531
51560
  }
51532
51561
  return false;
51533
51562
  }
51534
- function validateAndSanitizeUrl(rawUrl) {
51563
+ function validateAndSanitizeGithubUrl(rawUrl, resource) {
51535
51564
  const sanitized = sanitizeUrl(rawUrl);
51536
51565
  if (!sanitized) {
51537
51566
  return { error: "Empty URL" };
@@ -51547,10 +51576,10 @@ function validateAndSanitizeUrl(rawUrl) {
51547
51576
  if (isPrivateHost(url3)) {
51548
51577
  return { error: "Private or localhost URLs are not allowed" };
51549
51578
  }
51550
- const githubPrPattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/([0-9]+)\/?$/;
51551
- if (!githubPrPattern.test(sanitized)) {
51579
+ const githubPattern = new RegExp(`^https:\\/\\/github\\.com\\/([^/]+)\\/([^/]+)\\/${resource}\\/([0-9]+)\\/?$`);
51580
+ if (!githubPattern.test(sanitized)) {
51552
51581
  return {
51553
- error: "URL must be a GitHub pull request URL (https://github.com/owner/repo/pull/N)"
51582
+ error: resource === "issues" ? "URL must be a GitHub issue URL (https://github.com/owner/repo/issues/N)" : "URL must be a GitHub pull request URL (https://github.com/owner/repo/pull/N)"
51554
51583
  };
51555
51584
  }
51556
51585
  return { sanitized };
@@ -51558,50 +51587,18 @@ function validateAndSanitizeUrl(rawUrl) {
51558
51587
  return { error: "Invalid URL format" };
51559
51588
  }
51560
51589
  }
51561
- function parsePrRef(input, cwd) {
51562
- const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i);
51563
- if (urlMatch) {
51564
- return {
51565
- owner: urlMatch[1],
51566
- repo: urlMatch[2],
51567
- number: parseInt(urlMatch[3], 10)
51568
- };
51569
- }
51570
- const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
51571
- if (shorthandMatch) {
51572
- return {
51573
- owner: shorthandMatch[1],
51574
- repo: shorthandMatch[2],
51575
- number: parseInt(shorthandMatch[3], 10)
51576
- };
51577
- }
51578
- const bareMatch = input.match(/^(\d+)$/);
51579
- if (bareMatch) {
51580
- const prNumber = parseInt(bareMatch[1], 10);
51581
- const remoteUrl = detectGitRemote(cwd);
51582
- if (!remoteUrl) {
51583
- return null;
51584
- }
51585
- const parsed = parseGitRemoteUrl(remoteUrl);
51586
- if (!parsed) {
51587
- return null;
51588
- }
51589
- return {
51590
- owner: parsed.owner,
51591
- repo: parsed.repo,
51592
- number: prNumber
51593
- };
51594
- }
51595
- return null;
51596
- }
51597
51590
  function detectGitRemote(cwd) {
51598
51591
  try {
51599
- const remoteUrl = _internals28.execSync("git remote get-url origin", {
51592
+ const result = _internals28.spawnSync("git", ["remote", "get-url", "origin"], {
51600
51593
  encoding: "utf-8",
51601
- stdio: ["pipe", "pipe", "pipe"],
51594
+ stdio: ["ignore", "pipe", "pipe"],
51602
51595
  timeout: 5000,
51603
51596
  ...cwd ? { cwd } : {}
51604
- }).trim();
51597
+ });
51598
+ if (result.status !== 0 || result.error) {
51599
+ return null;
51600
+ }
51601
+ const remoteUrl = (result.stdout ?? "").trim();
51605
51602
  return remoteUrl || null;
51606
51603
  } catch {
51607
51604
  return null;
@@ -51610,123 +51607,49 @@ function detectGitRemote(cwd) {
51610
51607
  function parseGitRemoteUrl(remoteUrl) {
51611
51608
  const httpsMatch = remoteUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
51612
51609
  if (httpsMatch) {
51613
- return {
51614
- owner: httpsMatch[1],
51615
- repo: httpsMatch[2].replace(/\.git$/, "")
51616
- };
51610
+ const owner = httpsMatch[1];
51611
+ const repo = httpsMatch[2].replace(/\.git$/, "");
51612
+ if (containsControlCharacters(owner) || containsControlCharacters(repo)) {
51613
+ return null;
51614
+ }
51615
+ return { owner, repo };
51617
51616
  }
51618
51617
  const sshMatch = remoteUrl.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
51619
51618
  if (sshMatch) {
51620
- return {
51621
- owner: sshMatch[1],
51622
- repo: sshMatch[2].replace(/\.git$/, "")
51623
- };
51619
+ const owner = sshMatch[1];
51620
+ const repo = sshMatch[2].replace(/\.git$/, "");
51621
+ if (containsControlCharacters(owner) || containsControlCharacters(repo)) {
51622
+ return null;
51623
+ }
51624
+ return { owner, repo };
51624
51625
  }
51625
51626
  const pathMatch = remoteUrl.match(/\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
51626
51627
  if (pathMatch) {
51627
- return {
51628
- owner: pathMatch[1],
51629
- repo: pathMatch[2].replace(/\.git$/, "")
51630
- };
51628
+ const owner = pathMatch[1];
51629
+ const repo = pathMatch[2].replace(/\.git$/, "");
51630
+ if (containsControlCharacters(owner) || containsControlCharacters(repo)) {
51631
+ return null;
51632
+ }
51633
+ return { owner, repo };
51631
51634
  }
51632
51635
  return null;
51633
51636
  }
51634
- function looksLikePrRef(token) {
51635
- return /^https?:\/\//i.test(token) || /^[^/]+\/[^#]+#\d+$/.test(token) || /^\d+$/.test(token);
51636
- }
51637
- function resolvePrCommandInput(rest, cwd) {
51638
- if (rest.length === 0) {
51639
- return null;
51640
- }
51641
- const refToken = rest[0];
51642
- const instructions = sanitizeInstructions(rest.slice(1).join(" "));
51643
- const isFullUrl = /^https?:\/\//i.test(refToken);
51644
- const prInfo = parsePrRef(isFullUrl ? sanitizeUrl(refToken) : refToken, cwd);
51645
- if (!prInfo) {
51646
- return { error: `Could not parse PR reference from "${refToken}"` };
51647
- }
51648
- const prUrl = `https://github.com/${prInfo.owner}/${prInfo.repo}/pull/${prInfo.number}`;
51649
- const result = validateAndSanitizeUrl(prUrl);
51650
- if ("error" in result) {
51651
- return { error: result.error };
51652
- }
51653
- return { prUrl: result.sanitized, instructions };
51654
- }
51655
- var _internals28, MAX_URL_LEN = 2048, MAX_INSTRUCTIONS_LEN = 1000;
51656
- var init_pr_ref = __esm(() => {
51657
- _internals28 = { execSync: execSync2 };
51637
+ var MAX_URL_LEN = 2048, IPV4_PRIVATE, IPV4_LOOPBACK, IPV4_LINK_LOCAL, IPV4_PRIVATE_172, IPV4_PRIVATE_192, IPV4_ZERO_NETWORK, IPV6_LINK_LOCAL, IPV6_UNIQUE_LOCAL, _internals28;
51638
+ var init_url_security = __esm(() => {
51639
+ IPV4_PRIVATE = /^10\./;
51640
+ IPV4_LOOPBACK = /^127\./;
51641
+ IPV4_LINK_LOCAL = /^169\.254\./;
51642
+ IPV4_PRIVATE_172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
51643
+ IPV4_PRIVATE_192 = /^192\.168\./;
51644
+ IPV4_ZERO_NETWORK = /^0\./;
51645
+ IPV6_LINK_LOCAL = /^fe80:/i;
51646
+ IPV6_UNIQUE_LOCAL = /^f[cd][0-9a-f]{2}:/i;
51647
+ _internals28 = { spawnSync: spawnSync3 };
51658
51648
  });
51659
51649
 
51660
51650
  // src/commands/issue.ts
51661
- import { execSync as execSync3 } from "child_process";
51662
- function sanitizeUrl2(raw) {
51663
- let urlStr = raw.trim();
51664
- urlStr = urlStr.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
51665
- const fragmentIdx = urlStr.indexOf("#");
51666
- if (fragmentIdx !== -1) {
51667
- urlStr = urlStr.slice(0, fragmentIdx);
51668
- }
51669
- const queryIdx = urlStr.indexOf("?");
51670
- if (queryIdx !== -1) {
51671
- urlStr = urlStr.slice(0, queryIdx);
51672
- }
51673
- urlStr = urlStr.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^@/]+@/, "https://");
51674
- if (urlStr.length > MAX_URL_LEN2) {
51675
- urlStr = urlStr.slice(0, MAX_URL_LEN2);
51676
- }
51677
- return urlStr.trim();
51678
- }
51679
- function isPrivateHost2(url3) {
51680
- const host = url3.hostname.toLowerCase();
51681
- if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0") {
51682
- return true;
51683
- }
51684
- if (host.startsWith("localhost") || host === "localhost.com") {
51685
- return true;
51686
- }
51687
- const ipv4Private = /^10\./;
51688
- const ipv4172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
51689
- const ipv4192 = /^192\.168\./;
51690
- const ipv6Private = /^fe80:/i;
51691
- const ipv6Unique = /^f[cd][0-9a-f]{2}:/i;
51692
- if (ipv4Private.test(host) || ipv4172.test(host) || ipv4192.test(host) || ipv6Private.test(host) || ipv6Unique.test(host)) {
51693
- return true;
51694
- }
51695
- if (host.startsWith("::ffff:")) {
51696
- const inner = host.slice(7);
51697
- if (ipv4Private.test(inner) || ipv4172.test(inner) || ipv4192.test(inner)) {
51698
- return true;
51699
- }
51700
- }
51701
- return false;
51702
- }
51703
- function validateAndSanitizeUrl2(rawUrl) {
51704
- const sanitized = sanitizeUrl2(rawUrl);
51705
- if (!sanitized) {
51706
- return { error: "Empty URL" };
51707
- }
51708
- if (!sanitized.startsWith("https://")) {
51709
- return { error: "URL must use HTTPS scheme" };
51710
- }
51711
- try {
51712
- const url3 = new URL(sanitized);
51713
- const hostname5 = url3.hostname;
51714
- if (/[\u0080-\u{10FFFF}]/u.test(hostname5)) {
51715
- return { error: "Non-ASCII hostnames are not allowed" };
51716
- }
51717
- if (isPrivateHost2(url3)) {
51718
- return { error: "Private or localhost URLs are not allowed" };
51719
- }
51720
- const githubIssuePattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/([0-9]+)\/?$/;
51721
- if (!githubIssuePattern.test(sanitized)) {
51722
- return {
51723
- error: "URL must be a GitHub issue URL (https://github.com/owner/repo/issues/N)"
51724
- };
51725
- }
51726
- return { sanitized };
51727
- } catch {
51728
- return { error: "Invalid URL format" };
51729
- }
51651
+ function validateAndSanitizeUrl(rawUrl) {
51652
+ return validateAndSanitizeGithubUrl(rawUrl, "issues");
51730
51653
  }
51731
51654
  function parseArgs6(args) {
51732
51655
  const out = {
@@ -51753,9 +51676,12 @@ function parseArgs6(args) {
51753
51676
  }
51754
51677
  return out;
51755
51678
  }
51756
- function parseIssueRef(input) {
51679
+ function parseIssueRef(input, directory) {
51757
51680
  const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)\/?$/i);
51758
51681
  if (urlMatch) {
51682
+ if (containsControlCharacters(urlMatch[1]) || containsControlCharacters(urlMatch[2])) {
51683
+ return null;
51684
+ }
51759
51685
  return {
51760
51686
  owner: urlMatch[1],
51761
51687
  repo: urlMatch[2],
@@ -51764,6 +51690,9 @@ function parseIssueRef(input) {
51764
51690
  }
51765
51691
  const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
51766
51692
  if (shorthandMatch) {
51693
+ if (containsControlCharacters(shorthandMatch[1]) || containsControlCharacters(shorthandMatch[2])) {
51694
+ return null;
51695
+ }
51767
51696
  return {
51768
51697
  owner: shorthandMatch[1],
51769
51698
  repo: shorthandMatch[2],
@@ -51773,7 +51702,7 @@ function parseIssueRef(input) {
51773
51702
  const bareMatch = input.match(/^(\d+)$/);
51774
51703
  if (bareMatch) {
51775
51704
  const issueNumber = parseInt(bareMatch[1], 10);
51776
- const remoteUrl = detectGitRemote2();
51705
+ const remoteUrl = detectGitRemote(directory);
51777
51706
  if (!remoteUrl) {
51778
51707
  return null;
51779
51708
  }
@@ -51789,33 +51718,21 @@ function parseIssueRef(input) {
51789
51718
  }
51790
51719
  return null;
51791
51720
  }
51792
- function detectGitRemote2() {
51793
- try {
51794
- const remoteUrl = execSync3("git remote get-url origin", {
51795
- encoding: "utf-8",
51796
- stdio: ["pipe", "pipe", "pipe"],
51797
- timeout: 5000
51798
- }).trim();
51799
- return remoteUrl || null;
51800
- } catch {
51801
- return null;
51802
- }
51803
- }
51804
- function handleIssueCommand(_directory, args) {
51721
+ function handleIssueCommand(directory, args) {
51805
51722
  const parsed = parseArgs6(args);
51806
51723
  const rawInput = parsed.rest.join(" ").trim();
51807
51724
  if (!rawInput) {
51808
51725
  return USAGE6;
51809
51726
  }
51810
51727
  const isFullUrl = /^https?:\/\//i.test(rawInput);
51811
- const issueInfo = parseIssueRef(isFullUrl ? sanitizeUrl2(rawInput) : rawInput);
51728
+ const issueInfo = parseIssueRef(isFullUrl ? sanitizeUrl(rawInput) : rawInput, directory);
51812
51729
  if (!issueInfo) {
51813
- return `Error: Could not parse issue reference from "${rawInput}"
51730
+ return `Error: Could not parse issue reference from "${sanitizeErrorEcho(rawInput)}"
51814
51731
 
51815
51732
  ${USAGE6}`;
51816
51733
  }
51817
51734
  const issueUrl = `https://github.com/${issueInfo.owner}/${issueInfo.repo}/issues/${issueInfo.number}`;
51818
- const result = validateAndSanitizeUrl2(issueUrl);
51735
+ const result = validateAndSanitizeUrl(issueUrl);
51819
51736
  if ("error" in result) {
51820
51737
  return `Error: ${result.error}
51821
51738
 
@@ -51831,9 +51748,9 @@ ${USAGE6}`;
51831
51748
  const flagsStr = flags.length > 0 ? ` ${flags.join(" ")}` : "";
51832
51749
  return `[MODE: ISSUE_INGEST issue="${result.sanitized}"${flagsStr}]`;
51833
51750
  }
51834
- var MAX_URL_LEN2 = 2048, USAGE6;
51751
+ var USAGE6;
51835
51752
  var init_issue = __esm(() => {
51836
- init_pr_ref();
51753
+ init_url_security();
51837
51754
  USAGE6 = [
51838
51755
  "Usage: /swarm issue <url|owner/repo#N|N> [--plan] [--trace] [--no-repro]",
51839
51756
  "",
@@ -56025,6 +55942,88 @@ var init_post_mortem = __esm(() => {
56025
55942
  init_curator_postmortem();
56026
55943
  });
56027
55944
 
55945
+ // src/commands/pr-ref.ts
55946
+ function sanitizeInstructions(raw) {
55947
+ const collapsed = raw.replace(/\s+/g, " ").trim();
55948
+ const stripped = collapsed.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
55949
+ const normalized = stripped.replace(/\s+/g, " ").trim();
55950
+ if (normalized.length <= MAX_INSTRUCTIONS_LEN)
55951
+ return normalized;
55952
+ return `${normalized.slice(0, MAX_INSTRUCTIONS_LEN)}\u2026`;
55953
+ }
55954
+ function validateAndSanitizeUrl2(rawUrl) {
55955
+ return validateAndSanitizeGithubUrl(rawUrl, "pull");
55956
+ }
55957
+ function parsePrRef(input, cwd) {
55958
+ const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i);
55959
+ if (urlMatch) {
55960
+ if (containsControlCharacters(urlMatch[1]) || containsControlCharacters(urlMatch[2])) {
55961
+ return null;
55962
+ }
55963
+ return {
55964
+ owner: urlMatch[1],
55965
+ repo: urlMatch[2],
55966
+ number: parseInt(urlMatch[3], 10)
55967
+ };
55968
+ }
55969
+ const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
55970
+ if (shorthandMatch) {
55971
+ if (containsControlCharacters(shorthandMatch[1]) || containsControlCharacters(shorthandMatch[2])) {
55972
+ return null;
55973
+ }
55974
+ return {
55975
+ owner: shorthandMatch[1],
55976
+ repo: shorthandMatch[2],
55977
+ number: parseInt(shorthandMatch[3], 10)
55978
+ };
55979
+ }
55980
+ const bareMatch = input.match(/^(\d+)$/);
55981
+ if (bareMatch) {
55982
+ const prNumber = parseInt(bareMatch[1], 10);
55983
+ const remoteUrl = detectGitRemote(cwd);
55984
+ if (!remoteUrl) {
55985
+ return null;
55986
+ }
55987
+ const parsed = parseGitRemoteUrl(remoteUrl);
55988
+ if (!parsed) {
55989
+ return null;
55990
+ }
55991
+ return {
55992
+ owner: parsed.owner,
55993
+ repo: parsed.repo,
55994
+ number: prNumber
55995
+ };
55996
+ }
55997
+ return null;
55998
+ }
55999
+ function looksLikePrRef(token) {
56000
+ return /^https?:\/\//i.test(token) || /^[^/]+\/[^#]+#\d+$/.test(token) || /^\d+$/.test(token);
56001
+ }
56002
+ function resolvePrCommandInput(rest, cwd) {
56003
+ if (rest.length === 0) {
56004
+ return null;
56005
+ }
56006
+ const refToken = rest[0];
56007
+ const instructions = sanitizeInstructions(rest.slice(1).join(" "));
56008
+ const isFullUrl = /^https?:\/\//i.test(refToken);
56009
+ const prInfo = parsePrRef(isFullUrl ? sanitizeUrl(refToken) : refToken, cwd);
56010
+ if (!prInfo) {
56011
+ return {
56012
+ error: `Could not parse PR reference from "${sanitizeErrorEcho(refToken)}"`
56013
+ };
56014
+ }
56015
+ const prUrl = `https://github.com/${prInfo.owner}/${prInfo.repo}/pull/${prInfo.number}`;
56016
+ const result = validateAndSanitizeUrl2(prUrl);
56017
+ if ("error" in result) {
56018
+ return { error: result.error };
56019
+ }
56020
+ return { prUrl: result.sanitized, instructions };
56021
+ }
56022
+ var MAX_INSTRUCTIONS_LEN = 1000;
56023
+ var init_pr_ref = __esm(() => {
56024
+ init_url_security();
56025
+ });
56026
+
56028
56027
  // src/commands/pr-feedback.ts
56029
56028
  function handlePrFeedbackCommand(directory, args) {
56030
56029
  const rest = args.filter((t) => t.trim().length > 0);
@@ -0,0 +1,44 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ export declare const MAX_URL_LEN = 2048;
3
+ export type ValidationResult = {
4
+ sanitized: string;
5
+ } | {
6
+ error: string;
7
+ };
8
+ /**
9
+ * File-scoped indirection seam for git remote lookups.
10
+ */
11
+ export declare const _internals: {
12
+ spawnSync: typeof spawnSync;
13
+ };
14
+ /**
15
+ * Strip query strings, fragments, injected MODE headers, and credentials from
16
+ * a URL string.
17
+ */
18
+ export declare function sanitizeUrl(raw: string): string;
19
+ /**
20
+ * Strip control characters from user-visible error echoes and bound the result.
21
+ */
22
+ export declare function sanitizeErrorEcho(raw: string, maxLength?: number): string;
23
+ export declare function containsControlCharacters(value: string): boolean;
24
+ export declare function isIpv4MappedPrivateHost(inner: string): boolean;
25
+ /**
26
+ * Blocklist of private/localhost hostnames and IP ranges.
27
+ */
28
+ export declare function isPrivateHost(url: URL): boolean;
29
+ /**
30
+ * Validate and sanitize a GitHub URL for a specific resource kind.
31
+ */
32
+ export declare function validateAndSanitizeGithubUrl(rawUrl: string, resource: 'issues' | 'pull'): ValidationResult;
33
+ /**
34
+ * Detect the `origin` remote URL from git config.
35
+ */
36
+ export declare function detectGitRemote(cwd?: string): string | null;
37
+ /**
38
+ * Parse owner/repo from a git remote URL.
39
+ */
40
+ export declare function parseGitRemoteUrl(remoteUrl: string): {
41
+ owner: string;
42
+ repo: string;
43
+ } | null;
44
+ export declare function isIPv4ZeroNetwork(host: string): boolean;
@@ -10,4 +10,4 @@
10
10
  * --no-repro → appends noRepro=true to emitted signal
11
11
  * no args → returns usage string (no throw)
12
12
  */
13
- export declare function handleIssueCommand(_directory: string, args: string[]): string;
13
+ export declare function handleIssueCommand(directory: string, args: string[]): string;