opencode-swarm 7.73.2 → 7.73.3

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.73.3",
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",
@@ -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)
@@ -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;
@@ -10,20 +10,8 @@
10
10
  * fragments, and embedded credentials are stripped before the value is ever
11
11
  * placed back into a signal string.
12
12
  */
13
- import { execSync } from 'node:child_process';
14
- /**
15
- * File-scoped indirection seam for the subprocess call. Tests override
16
- * `_internals.execSync` (no `mock.module`) to assert the working directory is
17
- * threaded through and to simulate a missing `origin` remote.
18
- */
19
- export declare const _internals: {
20
- execSync: typeof execSync;
21
- };
22
- /**
23
- * Strip query strings, fragments, injected MODE headers, and credentials from
24
- * a URL string.
25
- */
26
- export declare function sanitizeUrl(raw: string): string;
13
+ import { _internals, detectGitRemote, parseGitRemoteUrl, sanitizeUrl, type ValidationResult } from './_shared/url-security.js';
14
+ export { _internals, detectGitRemote, parseGitRemoteUrl, sanitizeUrl };
27
15
  /**
28
16
  * Sanitize free-text instructions so they cannot forge a competing MODE
29
17
  * header, inject control sequences, or break out of the signal line.
@@ -31,19 +19,6 @@ export declare function sanitizeUrl(raw: string): string;
31
19
  * headers, and truncates to a bounded length.
32
20
  */
33
21
  export declare function sanitizeInstructions(raw: string): string;
34
- /**
35
- * Blocklist of private/localhost hostnames and IP ranges.
36
- */
37
- export declare function isPrivateHost(url: URL): boolean;
38
- /**
39
- * Validate and sanitize a GitHub PR URL.
40
- * Returns the sanitized URL on success, or an error message on failure.
41
- */
42
- export type ValidationResult = {
43
- sanitized: string;
44
- } | {
45
- error: string;
46
- };
47
22
  export declare function validateAndSanitizeUrl(rawUrl: string): ValidationResult;
48
23
  export interface ParsedPr {
49
24
  owner: string;
@@ -57,24 +32,6 @@ export interface ParsedPr {
57
32
  * 3. Bare number: N (resolved against the `origin` git remote in `cwd`)
58
33
  */
59
34
  export declare function parsePrRef(input: string, cwd?: string): ParsedPr | null;
60
- /**
61
- * Detect the `origin` remote URL from git config.
62
- *
63
- * `cwd` should be the project directory the command was invoked for. Without it
64
- * the lookup runs in `process.cwd()`, which in a plugin host is frequently not
65
- * the repository root — so bare-number PR resolution would silently fail or
66
- * resolve against the wrong repo (invariant #3: subprocesses run in an explicit
67
- * working directory).
68
- */
69
- export declare function detectGitRemote(cwd?: string): string | null;
70
- /**
71
- * Parse owner/repo from a git remote URL.
72
- * Supports HTTPS (https://github.com/owner/repo.git) and SSH (git@github.com:owner/repo.git).
73
- */
74
- export declare function parseGitRemoteUrl(remoteUrl: string): {
75
- owner: string;
76
- repo: string;
77
- } | null;
78
35
  /**
79
36
  * Whether a token is *shaped* like a PR reference — a full `http(s)` URL, an
80
37
  * `owner/repo#N` shorthand, or a bare number. This is intent detection, not
@@ -17,7 +17,9 @@ export interface SkillUsageEntry {
17
17
  taskID: string;
18
18
  /** ISO 8601 timestamp of the event. */
19
19
  timestamp: string;
20
- /** Compliance outcome — 'compliant' | 'violation' | 'partial' | 'not_checked' | custom. */
20
+ /** Compliance outcome — 'compliant' | 'partial' | 'violated' | 'not_checked' | custom.
21
+ * Legacy on-disk entries may carry the pre-fix spelling 'violation'; these are
22
+ * normalized to 'violated' on the read path (see normalizeComplianceVerdict). */
21
23
  complianceVerdict: string;
22
24
  /** Optional free-text notes from the reviewer. */
23
25
  reviewerNotes?: string;
@@ -51,6 +53,16 @@ export interface PruneResult {
51
53
  /** Error message when the write/rename step fails; absent on success. */
52
54
  error?: string;
53
55
  }
56
+ /**
57
+ * Normalize a compliance verdict to the canonical spelling.
58
+ * The sole producer (`skill-propagation-gate.ts`) lowercases the regex
59
+ * capture, yielding 'violated'. Pre-fix on-disk entries may carry the
60
+ * legacy spelling 'violation'; this maps them to the canonical form so
61
+ * that every downstream comparison can use a single string.
62
+ *
63
+ * Exported for unit-testing.
64
+ */
65
+ export declare function normalizeComplianceVerdict(verdict: string): string;
54
66
  /**
55
67
  * Test-only dependency-injection seam. Tests override these without
56
68
  * `mock.module` (which leaks across files in Bun's shared test-runner).
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.73.2",
72
+ version: "7.73.3",
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",
@@ -62433,6 +62433,9 @@ import * as path37 from "node:path";
62433
62433
  function resolveLogPath(directory) {
62434
62434
  return validateSwarmPath(directory, "skill-usage.jsonl");
62435
62435
  }
62436
+ function normalizeComplianceVerdict(verdict) {
62437
+ return verdict === "violation" ? "violated" : verdict;
62438
+ }
62436
62439
  function appendSkillUsageEntry(directory, entry) {
62437
62440
  const {
62438
62441
  skillPath,
@@ -62503,7 +62506,9 @@ function readSkillUsageEntries(directory, options) {
62503
62506
  if (!trimmed)
62504
62507
  continue;
62505
62508
  try {
62506
- entries.push(JSON.parse(trimmed));
62509
+ const entry = JSON.parse(trimmed);
62510
+ entry.complianceVerdict = normalizeComplianceVerdict(entry.complianceVerdict);
62511
+ entries.push(entry);
62507
62512
  } catch {}
62508
62513
  }
62509
62514
  if (!options)
@@ -62562,6 +62567,7 @@ function readSkillUsageEntriesTail(directory, filters, maxBytes = TAIL_BYTES_DEF
62562
62567
  continue;
62563
62568
  try {
62564
62569
  const entry = JSON.parse(line);
62570
+ entry.complianceVerdict = normalizeComplianceVerdict(entry.complianceVerdict);
62565
62571
  if (filters.sessionID !== undefined && entry.sessionID !== filters.sessionID) {
62566
62572
  continue;
62567
62573
  }
@@ -62596,7 +62602,7 @@ function computeComplianceByVersion(entries, skillPath) {
62596
62602
  stats.total += 1;
62597
62603
  if (e.complianceVerdict === "compliant")
62598
62604
  stats.compliant += 1;
62599
- if (e.complianceVerdict === "violation")
62605
+ if (e.complianceVerdict === "violated")
62600
62606
  stats.violation += 1;
62601
62607
  }
62602
62608
  for (const stats of map3.values()) {
@@ -62709,7 +62715,7 @@ async function applySkillUsageFeedback(directory, options) {
62709
62715
  try {
62710
62716
  const allEntries = readSkillUsageEntries(directory);
62711
62717
  const actionable = allEntries.filter((e) => {
62712
- if (e.complianceVerdict !== "compliant" && e.complianceVerdict !== "violation") {
62718
+ if (e.complianceVerdict !== "compliant" && e.complianceVerdict !== "violated") {
62713
62719
  return false;
62714
62720
  }
62715
62721
  if (options?.sinceTimestamp && e.timestamp <= options.sinceTimestamp) {
@@ -62735,7 +62741,7 @@ async function applySkillUsageFeedback(directory, options) {
62735
62741
  for (const entry of entries) {
62736
62742
  if (entry.complianceVerdict === "compliant")
62737
62743
  compliantCount++;
62738
- else if (entry.complianceVerdict === "violation")
62744
+ else if (entry.complianceVerdict === "violated")
62739
62745
  violationCount++;
62740
62746
  }
62741
62747
  if (compliantCount === 0 && violationCount === 0)
@@ -62819,7 +62825,7 @@ async function autoRetireSkills(directory, curatorKnowledgePath, excludeSlugs) {
62819
62825
  return true;
62820
62826
  return false;
62821
62827
  });
62822
- const violations = skillUsage.filter((e) => e.complianceVerdict === "violation").length;
62828
+ const violations = skillUsage.filter((e) => e.complianceVerdict === "violated").length;
62823
62829
  const violationRate = skillUsage.length > 0 ? violations / skillUsage.length : 0;
62824
62830
  let allArchived = false;
62825
62831
  try {
@@ -63484,7 +63490,7 @@ ${phaseDigest.summary}`,
63484
63490
  });
63485
63491
  if (skillUsage.length === 0)
63486
63492
  continue;
63487
- const violations = skillUsage.filter((e) => e.complianceVerdict === "violation").length;
63493
+ const violations = skillUsage.filter((e) => e.complianceVerdict === "violated").length;
63488
63494
  const violationRate = violations / skillUsage.length;
63489
63495
  if (violationRate > REVISION_VIOLATION_THRESHOLD && violationRate <= 0.3) {
63490
63496
  const content = await _internals27.readFileAsync(active.path, "utf-8");
@@ -63492,7 +63498,7 @@ ${phaseDigest.summary}`,
63492
63498
  if (fm && fm.skillOrigin === "promoted_external")
63493
63499
  continue;
63494
63500
  const currentVersion = fm?.version ?? 1;
63495
- const violationContexts = skillUsage.filter((e) => e.complianceVerdict === "violation").slice(-10).map((e) => ({
63501
+ const violationContexts = skillUsage.filter((e) => e.complianceVerdict === "violated").slice(-10).map((e) => ({
63496
63502
  taskId: e.taskID,
63497
63503
  agent: e.agentName,
63498
63504
  verdict: e.complianceVerdict,
@@ -75193,8 +75199,8 @@ var init_history = __esm(() => {
75193
75199
  init_history_service();
75194
75200
  });
75195
75201
 
75196
- // src/commands/pr-ref.ts
75197
- import { execSync as execSync2 } from "node:child_process";
75202
+ // src/commands/_shared/url-security.ts
75203
+ import { spawnSync as spawnSync8 } from "node:child_process";
75198
75204
  function sanitizeUrl(raw) {
75199
75205
  let urlStr = raw.trim();
75200
75206
  urlStr = urlStr.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
@@ -75212,13 +75218,29 @@ function sanitizeUrl(raw) {
75212
75218
  }
75213
75219
  return urlStr.trim();
75214
75220
  }
75215
- function sanitizeInstructions(raw) {
75216
- const collapsed = raw.replace(/\s+/g, " ").trim();
75217
- const stripped = collapsed.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
75218
- const normalized = stripped.replace(/\s+/g, " ").trim();
75219
- if (normalized.length <= MAX_INSTRUCTIONS_LEN)
75220
- return normalized;
75221
- return `${normalized.slice(0, MAX_INSTRUCTIONS_LEN)}…`;
75221
+ function sanitizeErrorEcho(raw, maxLength = 80) {
75222
+ let stripped = "";
75223
+ for (const ch of raw) {
75224
+ const cp = ch.codePointAt(0);
75225
+ if (cp !== undefined && (cp <= 31 || cp === 127)) {
75226
+ stripped += " ";
75227
+ continue;
75228
+ }
75229
+ stripped += ch;
75230
+ }
75231
+ const collapsed = stripped.replace(/\s+/g, " ").trim();
75232
+ if (collapsed.length <= maxLength)
75233
+ return collapsed;
75234
+ return `${collapsed.slice(0, maxLength)}…`;
75235
+ }
75236
+ function containsControlCharacters(value) {
75237
+ for (const ch of value) {
75238
+ const cp = ch.codePointAt(0);
75239
+ if (cp !== undefined && (cp <= 31 || cp === 127)) {
75240
+ return true;
75241
+ }
75242
+ }
75243
+ return false;
75222
75244
  }
75223
75245
  function hasNonAsciiHostname(hostname5) {
75224
75246
  for (const ch of hostname5) {
@@ -75228,31 +75250,38 @@ function hasNonAsciiHostname(hostname5) {
75228
75250
  }
75229
75251
  return false;
75230
75252
  }
75253
+ function isIpv4MappedPrivateHost(inner) {
75254
+ 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)) {
75255
+ return true;
75256
+ }
75257
+ const firstSegment = inner.split(":", 1)[0];
75258
+ if (!firstSegment)
75259
+ return false;
75260
+ const firstWord = Number.parseInt(firstSegment, 16);
75261
+ if (!Number.isFinite(firstWord))
75262
+ return false;
75263
+ return firstWord >= 0 && firstWord <= 255 || firstWord >= 2560 && firstWord <= 2815 || firstWord >= 32512 && firstWord <= 32767 || firstWord === 43518 || firstWord >= 44048 && firstWord <= 44063 || firstWord === 49320;
75264
+ }
75231
75265
  function isPrivateHost(url3) {
75232
- const host = url3.hostname.toLowerCase();
75233
- if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0") {
75266
+ const host = url3.hostname.toLowerCase().replace(/^\[|\]$/g, "");
75267
+ if (host === "localhost" || host === "::1" || host === "0.0.0.0" || IPV4_LOOPBACK.test(host) || IPV4_ZERO_NETWORK.test(host)) {
75234
75268
  return true;
75235
75269
  }
75236
75270
  if (host.startsWith("localhost") || host === "localhost.com") {
75237
75271
  return true;
75238
75272
  }
75239
- const ipv4Private = /^10\./;
75240
- const ipv4172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
75241
- const ipv4192 = /^192\.168\./;
75242
- const ipv6Private = /^fe80:/i;
75243
- const ipv6Unique = /^f[cd][0-9a-f]{2}:/i;
75244
- if (ipv4Private.test(host) || ipv4172.test(host) || ipv4192.test(host) || ipv6Private.test(host) || ipv6Unique.test(host)) {
75273
+ 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)) {
75245
75274
  return true;
75246
75275
  }
75247
75276
  if (host.startsWith("::ffff:")) {
75248
75277
  const inner = host.slice(7);
75249
- if (ipv4Private.test(inner) || ipv4172.test(inner) || ipv4192.test(inner)) {
75278
+ if (isIpv4MappedPrivateHost(inner)) {
75250
75279
  return true;
75251
75280
  }
75252
75281
  }
75253
75282
  return false;
75254
75283
  }
75255
- function validateAndSanitizeUrl(rawUrl) {
75284
+ function validateAndSanitizeGithubUrl(rawUrl, resource) {
75256
75285
  const sanitized = sanitizeUrl(rawUrl);
75257
75286
  if (!sanitized) {
75258
75287
  return { error: "Empty URL" };
@@ -75268,10 +75297,10 @@ function validateAndSanitizeUrl(rawUrl) {
75268
75297
  if (isPrivateHost(url3)) {
75269
75298
  return { error: "Private or localhost URLs are not allowed" };
75270
75299
  }
75271
- const githubPrPattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/([0-9]+)\/?$/;
75272
- if (!githubPrPattern.test(sanitized)) {
75300
+ const githubPattern = new RegExp(`^https:\\/\\/github\\.com\\/([^/]+)\\/([^/]+)\\/${resource}\\/([0-9]+)\\/?$`);
75301
+ if (!githubPattern.test(sanitized)) {
75273
75302
  return {
75274
- error: "URL must be a GitHub pull request URL (https://github.com/owner/repo/pull/N)"
75303
+ 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)"
75275
75304
  };
75276
75305
  }
75277
75306
  return { sanitized };
@@ -75279,50 +75308,18 @@ function validateAndSanitizeUrl(rawUrl) {
75279
75308
  return { error: "Invalid URL format" };
75280
75309
  }
75281
75310
  }
75282
- function parsePrRef(input, cwd) {
75283
- const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i);
75284
- if (urlMatch) {
75285
- return {
75286
- owner: urlMatch[1],
75287
- repo: urlMatch[2],
75288
- number: parseInt(urlMatch[3], 10)
75289
- };
75290
- }
75291
- const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
75292
- if (shorthandMatch) {
75293
- return {
75294
- owner: shorthandMatch[1],
75295
- repo: shorthandMatch[2],
75296
- number: parseInt(shorthandMatch[3], 10)
75297
- };
75298
- }
75299
- const bareMatch = input.match(/^(\d+)$/);
75300
- if (bareMatch) {
75301
- const prNumber = parseInt(bareMatch[1], 10);
75302
- const remoteUrl = detectGitRemote(cwd);
75303
- if (!remoteUrl) {
75304
- return null;
75305
- }
75306
- const parsed = parseGitRemoteUrl(remoteUrl);
75307
- if (!parsed) {
75308
- return null;
75309
- }
75310
- return {
75311
- owner: parsed.owner,
75312
- repo: parsed.repo,
75313
- number: prNumber
75314
- };
75315
- }
75316
- return null;
75317
- }
75318
75311
  function detectGitRemote(cwd) {
75319
75312
  try {
75320
- const remoteUrl = _internals39.execSync("git remote get-url origin", {
75313
+ const result = _internals39.spawnSync("git", ["remote", "get-url", "origin"], {
75321
75314
  encoding: "utf-8",
75322
- stdio: ["pipe", "pipe", "pipe"],
75315
+ stdio: ["ignore", "pipe", "pipe"],
75323
75316
  timeout: 5000,
75324
75317
  ...cwd ? { cwd } : {}
75325
- }).trim();
75318
+ });
75319
+ if (result.status !== 0 || result.error) {
75320
+ return null;
75321
+ }
75322
+ const remoteUrl = (result.stdout ?? "").trim();
75326
75323
  return remoteUrl || null;
75327
75324
  } catch {
75328
75325
  return null;
@@ -75331,123 +75328,49 @@ function detectGitRemote(cwd) {
75331
75328
  function parseGitRemoteUrl(remoteUrl) {
75332
75329
  const httpsMatch = remoteUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
75333
75330
  if (httpsMatch) {
75334
- return {
75335
- owner: httpsMatch[1],
75336
- repo: httpsMatch[2].replace(/\.git$/, "")
75337
- };
75331
+ const owner = httpsMatch[1];
75332
+ const repo = httpsMatch[2].replace(/\.git$/, "");
75333
+ if (containsControlCharacters(owner) || containsControlCharacters(repo)) {
75334
+ return null;
75335
+ }
75336
+ return { owner, repo };
75338
75337
  }
75339
75338
  const sshMatch = remoteUrl.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
75340
75339
  if (sshMatch) {
75341
- return {
75342
- owner: sshMatch[1],
75343
- repo: sshMatch[2].replace(/\.git$/, "")
75344
- };
75340
+ const owner = sshMatch[1];
75341
+ const repo = sshMatch[2].replace(/\.git$/, "");
75342
+ if (containsControlCharacters(owner) || containsControlCharacters(repo)) {
75343
+ return null;
75344
+ }
75345
+ return { owner, repo };
75345
75346
  }
75346
75347
  const pathMatch = remoteUrl.match(/\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
75347
75348
  if (pathMatch) {
75348
- return {
75349
- owner: pathMatch[1],
75350
- repo: pathMatch[2].replace(/\.git$/, "")
75351
- };
75349
+ const owner = pathMatch[1];
75350
+ const repo = pathMatch[2].replace(/\.git$/, "");
75351
+ if (containsControlCharacters(owner) || containsControlCharacters(repo)) {
75352
+ return null;
75353
+ }
75354
+ return { owner, repo };
75352
75355
  }
75353
75356
  return null;
75354
75357
  }
75355
- function looksLikePrRef(token) {
75356
- return /^https?:\/\//i.test(token) || /^[^/]+\/[^#]+#\d+$/.test(token) || /^\d+$/.test(token);
75357
- }
75358
- function resolvePrCommandInput(rest, cwd) {
75359
- if (rest.length === 0) {
75360
- return null;
75361
- }
75362
- const refToken = rest[0];
75363
- const instructions = sanitizeInstructions(rest.slice(1).join(" "));
75364
- const isFullUrl = /^https?:\/\//i.test(refToken);
75365
- const prInfo = parsePrRef(isFullUrl ? sanitizeUrl(refToken) : refToken, cwd);
75366
- if (!prInfo) {
75367
- return { error: `Could not parse PR reference from "${refToken}"` };
75368
- }
75369
- const prUrl = `https://github.com/${prInfo.owner}/${prInfo.repo}/pull/${prInfo.number}`;
75370
- const result = validateAndSanitizeUrl(prUrl);
75371
- if ("error" in result) {
75372
- return { error: result.error };
75373
- }
75374
- return { prUrl: result.sanitized, instructions };
75375
- }
75376
- var _internals39, MAX_URL_LEN = 2048, MAX_INSTRUCTIONS_LEN = 1000;
75377
- var init_pr_ref = __esm(() => {
75378
- _internals39 = { execSync: execSync2 };
75358
+ 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, _internals39;
75359
+ var init_url_security = __esm(() => {
75360
+ IPV4_PRIVATE = /^10\./;
75361
+ IPV4_LOOPBACK = /^127\./;
75362
+ IPV4_LINK_LOCAL = /^169\.254\./;
75363
+ IPV4_PRIVATE_172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
75364
+ IPV4_PRIVATE_192 = /^192\.168\./;
75365
+ IPV4_ZERO_NETWORK = /^0\./;
75366
+ IPV6_LINK_LOCAL = /^fe80:/i;
75367
+ IPV6_UNIQUE_LOCAL = /^f[cd][0-9a-f]{2}:/i;
75368
+ _internals39 = { spawnSync: spawnSync8 };
75379
75369
  });
75380
75370
 
75381
75371
  // src/commands/issue.ts
75382
- import { execSync as execSync3 } from "node:child_process";
75383
- function sanitizeUrl2(raw) {
75384
- let urlStr = raw.trim();
75385
- urlStr = urlStr.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
75386
- const fragmentIdx = urlStr.indexOf("#");
75387
- if (fragmentIdx !== -1) {
75388
- urlStr = urlStr.slice(0, fragmentIdx);
75389
- }
75390
- const queryIdx = urlStr.indexOf("?");
75391
- if (queryIdx !== -1) {
75392
- urlStr = urlStr.slice(0, queryIdx);
75393
- }
75394
- urlStr = urlStr.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^@/]+@/, "https://");
75395
- if (urlStr.length > MAX_URL_LEN2) {
75396
- urlStr = urlStr.slice(0, MAX_URL_LEN2);
75397
- }
75398
- return urlStr.trim();
75399
- }
75400
- function isPrivateHost2(url3) {
75401
- const host = url3.hostname.toLowerCase();
75402
- if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0") {
75403
- return true;
75404
- }
75405
- if (host.startsWith("localhost") || host === "localhost.com") {
75406
- return true;
75407
- }
75408
- const ipv4Private = /^10\./;
75409
- const ipv4172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
75410
- const ipv4192 = /^192\.168\./;
75411
- const ipv6Private = /^fe80:/i;
75412
- const ipv6Unique = /^f[cd][0-9a-f]{2}:/i;
75413
- if (ipv4Private.test(host) || ipv4172.test(host) || ipv4192.test(host) || ipv6Private.test(host) || ipv6Unique.test(host)) {
75414
- return true;
75415
- }
75416
- if (host.startsWith("::ffff:")) {
75417
- const inner = host.slice(7);
75418
- if (ipv4Private.test(inner) || ipv4172.test(inner) || ipv4192.test(inner)) {
75419
- return true;
75420
- }
75421
- }
75422
- return false;
75423
- }
75424
- function validateAndSanitizeUrl2(rawUrl) {
75425
- const sanitized = sanitizeUrl2(rawUrl);
75426
- if (!sanitized) {
75427
- return { error: "Empty URL" };
75428
- }
75429
- if (!sanitized.startsWith("https://")) {
75430
- return { error: "URL must use HTTPS scheme" };
75431
- }
75432
- try {
75433
- const url3 = new URL(sanitized);
75434
- const hostname5 = url3.hostname;
75435
- if (/[\u0080-\u{10FFFF}]/u.test(hostname5)) {
75436
- return { error: "Non-ASCII hostnames are not allowed" };
75437
- }
75438
- if (isPrivateHost2(url3)) {
75439
- return { error: "Private or localhost URLs are not allowed" };
75440
- }
75441
- const githubIssuePattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/([0-9]+)\/?$/;
75442
- if (!githubIssuePattern.test(sanitized)) {
75443
- return {
75444
- error: "URL must be a GitHub issue URL (https://github.com/owner/repo/issues/N)"
75445
- };
75446
- }
75447
- return { sanitized };
75448
- } catch {
75449
- return { error: "Invalid URL format" };
75450
- }
75372
+ function validateAndSanitizeUrl(rawUrl) {
75373
+ return validateAndSanitizeGithubUrl(rawUrl, "issues");
75451
75374
  }
75452
75375
  function parseArgs6(args2) {
75453
75376
  const out2 = {
@@ -75474,9 +75397,12 @@ function parseArgs6(args2) {
75474
75397
  }
75475
75398
  return out2;
75476
75399
  }
75477
- function parseIssueRef(input) {
75400
+ function parseIssueRef(input, directory) {
75478
75401
  const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)\/?$/i);
75479
75402
  if (urlMatch) {
75403
+ if (containsControlCharacters(urlMatch[1]) || containsControlCharacters(urlMatch[2])) {
75404
+ return null;
75405
+ }
75480
75406
  return {
75481
75407
  owner: urlMatch[1],
75482
75408
  repo: urlMatch[2],
@@ -75485,6 +75411,9 @@ function parseIssueRef(input) {
75485
75411
  }
75486
75412
  const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
75487
75413
  if (shorthandMatch) {
75414
+ if (containsControlCharacters(shorthandMatch[1]) || containsControlCharacters(shorthandMatch[2])) {
75415
+ return null;
75416
+ }
75488
75417
  return {
75489
75418
  owner: shorthandMatch[1],
75490
75419
  repo: shorthandMatch[2],
@@ -75494,7 +75423,7 @@ function parseIssueRef(input) {
75494
75423
  const bareMatch = input.match(/^(\d+)$/);
75495
75424
  if (bareMatch) {
75496
75425
  const issueNumber = parseInt(bareMatch[1], 10);
75497
- const remoteUrl = detectGitRemote2();
75426
+ const remoteUrl = detectGitRemote(directory);
75498
75427
  if (!remoteUrl) {
75499
75428
  return null;
75500
75429
  }
@@ -75510,33 +75439,21 @@ function parseIssueRef(input) {
75510
75439
  }
75511
75440
  return null;
75512
75441
  }
75513
- function detectGitRemote2() {
75514
- try {
75515
- const remoteUrl = execSync3("git remote get-url origin", {
75516
- encoding: "utf-8",
75517
- stdio: ["pipe", "pipe", "pipe"],
75518
- timeout: 5000
75519
- }).trim();
75520
- return remoteUrl || null;
75521
- } catch {
75522
- return null;
75523
- }
75524
- }
75525
- function handleIssueCommand(_directory, args2) {
75442
+ function handleIssueCommand(directory, args2) {
75526
75443
  const parsed = parseArgs6(args2);
75527
75444
  const rawInput = parsed.rest.join(" ").trim();
75528
75445
  if (!rawInput) {
75529
75446
  return USAGE6;
75530
75447
  }
75531
75448
  const isFullUrl = /^https?:\/\//i.test(rawInput);
75532
- const issueInfo = parseIssueRef(isFullUrl ? sanitizeUrl2(rawInput) : rawInput);
75449
+ const issueInfo = parseIssueRef(isFullUrl ? sanitizeUrl(rawInput) : rawInput, directory);
75533
75450
  if (!issueInfo) {
75534
- return `Error: Could not parse issue reference from "${rawInput}"
75451
+ return `Error: Could not parse issue reference from "${sanitizeErrorEcho(rawInput)}"
75535
75452
 
75536
75453
  ${USAGE6}`;
75537
75454
  }
75538
75455
  const issueUrl = `https://github.com/${issueInfo.owner}/${issueInfo.repo}/issues/${issueInfo.number}`;
75539
- const result = validateAndSanitizeUrl2(issueUrl);
75456
+ const result = validateAndSanitizeUrl(issueUrl);
75540
75457
  if ("error" in result) {
75541
75458
  return `Error: ${result.error}
75542
75459
 
@@ -75552,9 +75469,9 @@ ${USAGE6}`;
75552
75469
  const flagsStr = flags2.length > 0 ? ` ${flags2.join(" ")}` : "";
75553
75470
  return `[MODE: ISSUE_INGEST issue="${result.sanitized}"${flagsStr}]`;
75554
75471
  }
75555
- var MAX_URL_LEN2 = 2048, USAGE6;
75472
+ var USAGE6;
75556
75473
  var init_issue = __esm(() => {
75557
- init_pr_ref();
75474
+ init_url_security();
75558
75475
  USAGE6 = [
75559
75476
  "Usage: /swarm issue <url|owner/repo#N|N> [--plan] [--trace] [--no-repro]",
75560
75477
  "",
@@ -80737,6 +80654,88 @@ var init_post_mortem = __esm(() => {
80737
80654
  init_curator_postmortem();
80738
80655
  });
80739
80656
 
80657
+ // src/commands/pr-ref.ts
80658
+ function sanitizeInstructions(raw) {
80659
+ const collapsed = raw.replace(/\s+/g, " ").trim();
80660
+ const stripped = collapsed.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
80661
+ const normalized = stripped.replace(/\s+/g, " ").trim();
80662
+ if (normalized.length <= MAX_INSTRUCTIONS_LEN)
80663
+ return normalized;
80664
+ return `${normalized.slice(0, MAX_INSTRUCTIONS_LEN)}…`;
80665
+ }
80666
+ function validateAndSanitizeUrl2(rawUrl) {
80667
+ return validateAndSanitizeGithubUrl(rawUrl, "pull");
80668
+ }
80669
+ function parsePrRef(input, cwd) {
80670
+ const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i);
80671
+ if (urlMatch) {
80672
+ if (containsControlCharacters(urlMatch[1]) || containsControlCharacters(urlMatch[2])) {
80673
+ return null;
80674
+ }
80675
+ return {
80676
+ owner: urlMatch[1],
80677
+ repo: urlMatch[2],
80678
+ number: parseInt(urlMatch[3], 10)
80679
+ };
80680
+ }
80681
+ const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
80682
+ if (shorthandMatch) {
80683
+ if (containsControlCharacters(shorthandMatch[1]) || containsControlCharacters(shorthandMatch[2])) {
80684
+ return null;
80685
+ }
80686
+ return {
80687
+ owner: shorthandMatch[1],
80688
+ repo: shorthandMatch[2],
80689
+ number: parseInt(shorthandMatch[3], 10)
80690
+ };
80691
+ }
80692
+ const bareMatch = input.match(/^(\d+)$/);
80693
+ if (bareMatch) {
80694
+ const prNumber = parseInt(bareMatch[1], 10);
80695
+ const remoteUrl = detectGitRemote(cwd);
80696
+ if (!remoteUrl) {
80697
+ return null;
80698
+ }
80699
+ const parsed = parseGitRemoteUrl(remoteUrl);
80700
+ if (!parsed) {
80701
+ return null;
80702
+ }
80703
+ return {
80704
+ owner: parsed.owner,
80705
+ repo: parsed.repo,
80706
+ number: prNumber
80707
+ };
80708
+ }
80709
+ return null;
80710
+ }
80711
+ function looksLikePrRef(token) {
80712
+ return /^https?:\/\//i.test(token) || /^[^/]+\/[^#]+#\d+$/.test(token) || /^\d+$/.test(token);
80713
+ }
80714
+ function resolvePrCommandInput(rest, cwd) {
80715
+ if (rest.length === 0) {
80716
+ return null;
80717
+ }
80718
+ const refToken = rest[0];
80719
+ const instructions = sanitizeInstructions(rest.slice(1).join(" "));
80720
+ const isFullUrl = /^https?:\/\//i.test(refToken);
80721
+ const prInfo = parsePrRef(isFullUrl ? sanitizeUrl(refToken) : refToken, cwd);
80722
+ if (!prInfo) {
80723
+ return {
80724
+ error: `Could not parse PR reference from "${sanitizeErrorEcho(refToken)}"`
80725
+ };
80726
+ }
80727
+ const prUrl = `https://github.com/${prInfo.owner}/${prInfo.repo}/pull/${prInfo.number}`;
80728
+ const result = validateAndSanitizeUrl2(prUrl);
80729
+ if ("error" in result) {
80730
+ return { error: result.error };
80731
+ }
80732
+ return { prUrl: result.sanitized, instructions };
80733
+ }
80734
+ var MAX_INSTRUCTIONS_LEN = 1000;
80735
+ var init_pr_ref = __esm(() => {
80736
+ init_url_security();
80737
+ });
80738
+
80740
80739
  // src/commands/pr-feedback.ts
80741
80740
  function handlePrFeedbackCommand(directory, args2) {
80742
80741
  const rest = args2.filter((t) => t.trim().length > 0);
@@ -127845,7 +127844,7 @@ import * as fs103 from "node:fs";
127845
127844
  import * as path156 from "node:path";
127846
127845
 
127847
127846
  // src/mutation/engine.ts
127848
- import { spawnSync as spawnSync10 } from "node:child_process";
127847
+ import { spawnSync as spawnSync11 } from "node:child_process";
127849
127848
  import { unlinkSync as unlinkSync19, writeFileSync as writeFileSync27 } from "node:fs";
127850
127849
  import * as path155 from "node:path";
127851
127850
 
@@ -128023,7 +128022,7 @@ var _internals88 = {
128023
128022
  executeMutation,
128024
128023
  computeReport,
128025
128024
  executeMutationSuite,
128026
- spawnSync: spawnSync10
128025
+ spawnSync: spawnSync11
128027
128026
  };
128028
128027
  async function executeMutation(patch, testCommand, testFiles, workingDir) {
128029
128028
  const startTime = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.73.2",
3
+ "version": "7.73.3",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",