opencode-swarm 7.73.1 → 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.
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.1",
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)
@@ -40921,6 +40927,49 @@ var init_synonym_map = __esm(() => {
40921
40927
  MIN_READ_CEILING_BYTES = 64 * 1024;
40922
40928
  });
40923
40929
 
40930
+ // src/hooks/knowledge-reinforcement.ts
40931
+ function isActiveSwarmKnowledgeEntry(entry) {
40932
+ return !INACTIVE_STATUSES.has(entry.status);
40933
+ }
40934
+ function findActiveSwarmNearDuplicate(lesson, entries, threshold) {
40935
+ return findNearDuplicate(lesson, entries.filter(isActiveSwarmKnowledgeEntry), threshold);
40936
+ }
40937
+ function distinctPhaseCount(records) {
40938
+ const phases = new Set;
40939
+ for (const record3 of records ?? []) {
40940
+ if (Number.isInteger(record3.phase_number)) {
40941
+ phases.add(record3.phase_number);
40942
+ }
40943
+ }
40944
+ return phases.size;
40945
+ }
40946
+ function reinforceSwarmKnowledgeEntry(entry, confirmation) {
40947
+ if (!isActiveSwarmKnowledgeEntry(entry)) {
40948
+ return { entryId: entry.id, reinforced: false, reason: "inactive" };
40949
+ }
40950
+ if ((entry.confirmed_by ?? []).some((record3) => record3.phase_number === confirmation.phase_number)) {
40951
+ return {
40952
+ entryId: entry.id,
40953
+ reinforced: false,
40954
+ reason: "already_confirmed_phase"
40955
+ };
40956
+ }
40957
+ entry.confirmed_by = [...entry.confirmed_by ?? [], confirmation];
40958
+ entry.updated_at = confirmation.confirmed_at;
40959
+ entry.phases_alive = 0;
40960
+ entry.confidence = computeConfidence(distinctPhaseCount(entry.confirmed_by), entry.auto_generated ?? false);
40961
+ return { entryId: entry.id, reinforced: true, reason: "reinforced" };
40962
+ }
40963
+ var INACTIVE_STATUSES;
40964
+ var init_knowledge_reinforcement = __esm(() => {
40965
+ init_knowledge_store();
40966
+ INACTIVE_STATUSES = new Set([
40967
+ "archived",
40968
+ "quarantined",
40969
+ "quarantined_unactionable"
40970
+ ]);
40971
+ });
40972
+
40924
40973
  // src/hooks/skill-scoring.ts
40925
40974
  import * as fs10 from "fs";
40926
40975
  import * as path22 from "path";
@@ -42244,6 +42293,7 @@ async function curateAndStoreSwarm(lessons, projectName, phaseInfo, directory, c
42244
42293
  ]);
42245
42294
  const snapshotPlusNew = [...snapshot];
42246
42295
  const toAdd = [];
42296
+ const pendingReinforcementIds = new Set;
42247
42297
  for (const lesson of lessons) {
42248
42298
  const tags = inferTags(lesson);
42249
42299
  let category = "process";
@@ -42273,8 +42323,9 @@ async function curateAndStoreSwarm(lessons, projectName, phaseInfo, directory, c
42273
42323
  continue;
42274
42324
  }
42275
42325
  }
42276
- const duplicate = findNearDuplicate(lesson, snapshotPlusNew, config3.dedup_threshold);
42326
+ const duplicate = findActiveSwarmNearDuplicate(lesson, snapshotPlusNew, config3.dedup_threshold);
42277
42327
  if (duplicate) {
42328
+ pendingReinforcementIds.add(duplicate.id);
42278
42329
  skipped++;
42279
42330
  continue;
42280
42331
  }
@@ -42355,7 +42406,9 @@ async function curateAndStoreSwarm(lessons, projectName, phaseInfo, directory, c
42355
42406
  } catch {}
42356
42407
  continue;
42357
42408
  }
42358
- if (findNearDuplicate(entry.lesson, snapshotPlusNew, config3.dedup_threshold)) {
42409
+ const duplicate = findActiveSwarmNearDuplicate(entry.lesson, snapshotPlusNew, config3.dedup_threshold);
42410
+ if (duplicate) {
42411
+ pendingReinforcementIds.add(duplicate.id);
42359
42412
  skipped++;
42360
42413
  continue;
42361
42414
  }
@@ -42364,15 +42417,48 @@ async function curateAndStoreSwarm(lessons, projectName, phaseInfo, directory, c
42364
42417
  }
42365
42418
  } catch {}
42366
42419
  let stored = 0;
42367
- if (toAdd.length > 0) {
42420
+ let reinforced = 0;
42421
+ if (toAdd.length > 0 || pendingReinforcementIds.size > 0) {
42368
42422
  await transactKnowledge(knowledgePath, (current) => {
42369
- const trulyNew = toAdd.filter((e) => !findNearDuplicate(e.lesson, current, config3.dedup_threshold));
42370
- const extraDups = toAdd.length - trulyNew.length;
42371
- skipped += extraDups;
42372
- if (trulyNew.length === 0)
42373
- return null;
42374
- stored = trulyNew.length;
42375
- return [...current, ...trulyNew];
42423
+ let changed = false;
42424
+ for (const id of pendingReinforcementIds) {
42425
+ const existing = current.find((entry) => entry.id === id);
42426
+ if (!existing)
42427
+ continue;
42428
+ const result = reinforceSwarmKnowledgeEntry(existing, {
42429
+ phase_number: phaseInfo.phase_number,
42430
+ confirmed_at: new Date().toISOString(),
42431
+ project_name: projectName
42432
+ });
42433
+ if (result.reinforced) {
42434
+ reinforced++;
42435
+ changed = true;
42436
+ }
42437
+ }
42438
+ const trulyNew = [];
42439
+ for (const entry of toAdd) {
42440
+ const duplicate = findActiveSwarmNearDuplicate(entry.lesson, current, config3.dedup_threshold);
42441
+ if (duplicate) {
42442
+ skipped++;
42443
+ const result = reinforceSwarmKnowledgeEntry(duplicate, {
42444
+ phase_number: phaseInfo.phase_number,
42445
+ confirmed_at: new Date().toISOString(),
42446
+ project_name: projectName
42447
+ });
42448
+ if (result.reinforced) {
42449
+ reinforced++;
42450
+ changed = true;
42451
+ }
42452
+ continue;
42453
+ }
42454
+ trulyNew.push(entry);
42455
+ }
42456
+ if (trulyNew.length > 0) {
42457
+ current.push(...trulyNew);
42458
+ stored = trulyNew.length;
42459
+ changed = true;
42460
+ }
42461
+ return changed ? current : null;
42376
42462
  });
42377
42463
  }
42378
42464
  await enforceKnowledgeCap(knowledgePath, config3.swarm_max_entries);
@@ -42390,7 +42476,7 @@ async function curateAndStoreSwarm(lessons, projectName, phaseInfo, directory, c
42390
42476
  if (!options?.skipAutoPromotion) {
42391
42477
  await _internals19.runAutoPromotion(directory, config3);
42392
42478
  }
42393
- return { stored, skipped, rejected, quarantined };
42479
+ return { stored, reinforced, skipped, rejected, quarantined };
42394
42480
  }
42395
42481
  async function runAutoPromotion(directory, config3) {
42396
42482
  const knowledgePath = resolveSwarmKnowledgePath(directory);
@@ -42503,6 +42589,7 @@ var init_knowledge_curator = __esm(() => {
42503
42589
  init_synonym_map();
42504
42590
  init_logger();
42505
42591
  init_knowledge_events();
42592
+ init_knowledge_reinforcement();
42506
42593
  init_knowledge_store();
42507
42594
  init_knowledge_validator();
42508
42595
  init_micro_reflector();
@@ -51391,8 +51478,8 @@ var init_history = __esm(() => {
51391
51478
  init_history_service();
51392
51479
  });
51393
51480
 
51394
- // src/commands/pr-ref.ts
51395
- import { execSync as execSync2 } from "child_process";
51481
+ // src/commands/_shared/url-security.ts
51482
+ import { spawnSync as spawnSync3 } from "child_process";
51396
51483
  function sanitizeUrl(raw) {
51397
51484
  let urlStr = raw.trim();
51398
51485
  urlStr = urlStr.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
@@ -51410,13 +51497,29 @@ function sanitizeUrl(raw) {
51410
51497
  }
51411
51498
  return urlStr.trim();
51412
51499
  }
51413
- function sanitizeInstructions(raw) {
51414
- const collapsed = raw.replace(/\s+/g, " ").trim();
51415
- const stripped = collapsed.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
51416
- const normalized = stripped.replace(/\s+/g, " ").trim();
51417
- if (normalized.length <= MAX_INSTRUCTIONS_LEN)
51418
- return normalized;
51419
- 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;
51420
51523
  }
51421
51524
  function hasNonAsciiHostname(hostname5) {
51422
51525
  for (const ch of hostname5) {
@@ -51426,31 +51529,38 @@ function hasNonAsciiHostname(hostname5) {
51426
51529
  }
51427
51530
  return false;
51428
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
+ }
51429
51544
  function isPrivateHost(url3) {
51430
- const host = url3.hostname.toLowerCase();
51431
- 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)) {
51432
51547
  return true;
51433
51548
  }
51434
51549
  if (host.startsWith("localhost") || host === "localhost.com") {
51435
51550
  return true;
51436
51551
  }
51437
- const ipv4Private = /^10\./;
51438
- const ipv4172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
51439
- const ipv4192 = /^192\.168\./;
51440
- const ipv6Private = /^fe80:/i;
51441
- const ipv6Unique = /^f[cd][0-9a-f]{2}:/i;
51442
- 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)) {
51443
51553
  return true;
51444
51554
  }
51445
51555
  if (host.startsWith("::ffff:")) {
51446
51556
  const inner = host.slice(7);
51447
- if (ipv4Private.test(inner) || ipv4172.test(inner) || ipv4192.test(inner)) {
51557
+ if (isIpv4MappedPrivateHost(inner)) {
51448
51558
  return true;
51449
51559
  }
51450
51560
  }
51451
51561
  return false;
51452
51562
  }
51453
- function validateAndSanitizeUrl(rawUrl) {
51563
+ function validateAndSanitizeGithubUrl(rawUrl, resource) {
51454
51564
  const sanitized = sanitizeUrl(rawUrl);
51455
51565
  if (!sanitized) {
51456
51566
  return { error: "Empty URL" };
@@ -51466,10 +51576,10 @@ function validateAndSanitizeUrl(rawUrl) {
51466
51576
  if (isPrivateHost(url3)) {
51467
51577
  return { error: "Private or localhost URLs are not allowed" };
51468
51578
  }
51469
- const githubPrPattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/([0-9]+)\/?$/;
51470
- if (!githubPrPattern.test(sanitized)) {
51579
+ const githubPattern = new RegExp(`^https:\\/\\/github\\.com\\/([^/]+)\\/([^/]+)\\/${resource}\\/([0-9]+)\\/?$`);
51580
+ if (!githubPattern.test(sanitized)) {
51471
51581
  return {
51472
- 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)"
51473
51583
  };
51474
51584
  }
51475
51585
  return { sanitized };
@@ -51477,50 +51587,18 @@ function validateAndSanitizeUrl(rawUrl) {
51477
51587
  return { error: "Invalid URL format" };
51478
51588
  }
51479
51589
  }
51480
- function parsePrRef(input, cwd) {
51481
- const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i);
51482
- if (urlMatch) {
51483
- return {
51484
- owner: urlMatch[1],
51485
- repo: urlMatch[2],
51486
- number: parseInt(urlMatch[3], 10)
51487
- };
51488
- }
51489
- const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
51490
- if (shorthandMatch) {
51491
- return {
51492
- owner: shorthandMatch[1],
51493
- repo: shorthandMatch[2],
51494
- number: parseInt(shorthandMatch[3], 10)
51495
- };
51496
- }
51497
- const bareMatch = input.match(/^(\d+)$/);
51498
- if (bareMatch) {
51499
- const prNumber = parseInt(bareMatch[1], 10);
51500
- const remoteUrl = detectGitRemote(cwd);
51501
- if (!remoteUrl) {
51502
- return null;
51503
- }
51504
- const parsed = parseGitRemoteUrl(remoteUrl);
51505
- if (!parsed) {
51506
- return null;
51507
- }
51508
- return {
51509
- owner: parsed.owner,
51510
- repo: parsed.repo,
51511
- number: prNumber
51512
- };
51513
- }
51514
- return null;
51515
- }
51516
51590
  function detectGitRemote(cwd) {
51517
51591
  try {
51518
- const remoteUrl = _internals28.execSync("git remote get-url origin", {
51592
+ const result = _internals28.spawnSync("git", ["remote", "get-url", "origin"], {
51519
51593
  encoding: "utf-8",
51520
- stdio: ["pipe", "pipe", "pipe"],
51594
+ stdio: ["ignore", "pipe", "pipe"],
51521
51595
  timeout: 5000,
51522
51596
  ...cwd ? { cwd } : {}
51523
- }).trim();
51597
+ });
51598
+ if (result.status !== 0 || result.error) {
51599
+ return null;
51600
+ }
51601
+ const remoteUrl = (result.stdout ?? "").trim();
51524
51602
  return remoteUrl || null;
51525
51603
  } catch {
51526
51604
  return null;
@@ -51529,123 +51607,49 @@ function detectGitRemote(cwd) {
51529
51607
  function parseGitRemoteUrl(remoteUrl) {
51530
51608
  const httpsMatch = remoteUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
51531
51609
  if (httpsMatch) {
51532
- return {
51533
- owner: httpsMatch[1],
51534
- repo: httpsMatch[2].replace(/\.git$/, "")
51535
- };
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 };
51536
51616
  }
51537
51617
  const sshMatch = remoteUrl.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
51538
51618
  if (sshMatch) {
51539
- return {
51540
- owner: sshMatch[1],
51541
- repo: sshMatch[2].replace(/\.git$/, "")
51542
- };
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 };
51543
51625
  }
51544
51626
  const pathMatch = remoteUrl.match(/\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
51545
51627
  if (pathMatch) {
51546
- return {
51547
- owner: pathMatch[1],
51548
- repo: pathMatch[2].replace(/\.git$/, "")
51549
- };
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 };
51550
51634
  }
51551
51635
  return null;
51552
51636
  }
51553
- function looksLikePrRef(token) {
51554
- return /^https?:\/\//i.test(token) || /^[^/]+\/[^#]+#\d+$/.test(token) || /^\d+$/.test(token);
51555
- }
51556
- function resolvePrCommandInput(rest, cwd) {
51557
- if (rest.length === 0) {
51558
- return null;
51559
- }
51560
- const refToken = rest[0];
51561
- const instructions = sanitizeInstructions(rest.slice(1).join(" "));
51562
- const isFullUrl = /^https?:\/\//i.test(refToken);
51563
- const prInfo = parsePrRef(isFullUrl ? sanitizeUrl(refToken) : refToken, cwd);
51564
- if (!prInfo) {
51565
- return { error: `Could not parse PR reference from "${refToken}"` };
51566
- }
51567
- const prUrl = `https://github.com/${prInfo.owner}/${prInfo.repo}/pull/${prInfo.number}`;
51568
- const result = validateAndSanitizeUrl(prUrl);
51569
- if ("error" in result) {
51570
- return { error: result.error };
51571
- }
51572
- return { prUrl: result.sanitized, instructions };
51573
- }
51574
- var _internals28, MAX_URL_LEN = 2048, MAX_INSTRUCTIONS_LEN = 1000;
51575
- var init_pr_ref = __esm(() => {
51576
- _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 };
51577
51648
  });
51578
51649
 
51579
51650
  // src/commands/issue.ts
51580
- import { execSync as execSync3 } from "child_process";
51581
- function sanitizeUrl2(raw) {
51582
- let urlStr = raw.trim();
51583
- urlStr = urlStr.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
51584
- const fragmentIdx = urlStr.indexOf("#");
51585
- if (fragmentIdx !== -1) {
51586
- urlStr = urlStr.slice(0, fragmentIdx);
51587
- }
51588
- const queryIdx = urlStr.indexOf("?");
51589
- if (queryIdx !== -1) {
51590
- urlStr = urlStr.slice(0, queryIdx);
51591
- }
51592
- urlStr = urlStr.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^@/]+@/, "https://");
51593
- if (urlStr.length > MAX_URL_LEN2) {
51594
- urlStr = urlStr.slice(0, MAX_URL_LEN2);
51595
- }
51596
- return urlStr.trim();
51597
- }
51598
- function isPrivateHost2(url3) {
51599
- const host = url3.hostname.toLowerCase();
51600
- if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0") {
51601
- return true;
51602
- }
51603
- if (host.startsWith("localhost") || host === "localhost.com") {
51604
- return true;
51605
- }
51606
- const ipv4Private = /^10\./;
51607
- const ipv4172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
51608
- const ipv4192 = /^192\.168\./;
51609
- const ipv6Private = /^fe80:/i;
51610
- const ipv6Unique = /^f[cd][0-9a-f]{2}:/i;
51611
- if (ipv4Private.test(host) || ipv4172.test(host) || ipv4192.test(host) || ipv6Private.test(host) || ipv6Unique.test(host)) {
51612
- return true;
51613
- }
51614
- if (host.startsWith("::ffff:")) {
51615
- const inner = host.slice(7);
51616
- if (ipv4Private.test(inner) || ipv4172.test(inner) || ipv4192.test(inner)) {
51617
- return true;
51618
- }
51619
- }
51620
- return false;
51621
- }
51622
- function validateAndSanitizeUrl2(rawUrl) {
51623
- const sanitized = sanitizeUrl2(rawUrl);
51624
- if (!sanitized) {
51625
- return { error: "Empty URL" };
51626
- }
51627
- if (!sanitized.startsWith("https://")) {
51628
- return { error: "URL must use HTTPS scheme" };
51629
- }
51630
- try {
51631
- const url3 = new URL(sanitized);
51632
- const hostname5 = url3.hostname;
51633
- if (/[\u0080-\u{10FFFF}]/u.test(hostname5)) {
51634
- return { error: "Non-ASCII hostnames are not allowed" };
51635
- }
51636
- if (isPrivateHost2(url3)) {
51637
- return { error: "Private or localhost URLs are not allowed" };
51638
- }
51639
- const githubIssuePattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/([0-9]+)\/?$/;
51640
- if (!githubIssuePattern.test(sanitized)) {
51641
- return {
51642
- error: "URL must be a GitHub issue URL (https://github.com/owner/repo/issues/N)"
51643
- };
51644
- }
51645
- return { sanitized };
51646
- } catch {
51647
- return { error: "Invalid URL format" };
51648
- }
51651
+ function validateAndSanitizeUrl(rawUrl) {
51652
+ return validateAndSanitizeGithubUrl(rawUrl, "issues");
51649
51653
  }
51650
51654
  function parseArgs6(args) {
51651
51655
  const out = {
@@ -51672,9 +51676,12 @@ function parseArgs6(args) {
51672
51676
  }
51673
51677
  return out;
51674
51678
  }
51675
- function parseIssueRef(input) {
51679
+ function parseIssueRef(input, directory) {
51676
51680
  const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)\/?$/i);
51677
51681
  if (urlMatch) {
51682
+ if (containsControlCharacters(urlMatch[1]) || containsControlCharacters(urlMatch[2])) {
51683
+ return null;
51684
+ }
51678
51685
  return {
51679
51686
  owner: urlMatch[1],
51680
51687
  repo: urlMatch[2],
@@ -51683,6 +51690,9 @@ function parseIssueRef(input) {
51683
51690
  }
51684
51691
  const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
51685
51692
  if (shorthandMatch) {
51693
+ if (containsControlCharacters(shorthandMatch[1]) || containsControlCharacters(shorthandMatch[2])) {
51694
+ return null;
51695
+ }
51686
51696
  return {
51687
51697
  owner: shorthandMatch[1],
51688
51698
  repo: shorthandMatch[2],
@@ -51692,7 +51702,7 @@ function parseIssueRef(input) {
51692
51702
  const bareMatch = input.match(/^(\d+)$/);
51693
51703
  if (bareMatch) {
51694
51704
  const issueNumber = parseInt(bareMatch[1], 10);
51695
- const remoteUrl = detectGitRemote2();
51705
+ const remoteUrl = detectGitRemote(directory);
51696
51706
  if (!remoteUrl) {
51697
51707
  return null;
51698
51708
  }
@@ -51708,33 +51718,21 @@ function parseIssueRef(input) {
51708
51718
  }
51709
51719
  return null;
51710
51720
  }
51711
- function detectGitRemote2() {
51712
- try {
51713
- const remoteUrl = execSync3("git remote get-url origin", {
51714
- encoding: "utf-8",
51715
- stdio: ["pipe", "pipe", "pipe"],
51716
- timeout: 5000
51717
- }).trim();
51718
- return remoteUrl || null;
51719
- } catch {
51720
- return null;
51721
- }
51722
- }
51723
- function handleIssueCommand(_directory, args) {
51721
+ function handleIssueCommand(directory, args) {
51724
51722
  const parsed = parseArgs6(args);
51725
51723
  const rawInput = parsed.rest.join(" ").trim();
51726
51724
  if (!rawInput) {
51727
51725
  return USAGE6;
51728
51726
  }
51729
51727
  const isFullUrl = /^https?:\/\//i.test(rawInput);
51730
- const issueInfo = parseIssueRef(isFullUrl ? sanitizeUrl2(rawInput) : rawInput);
51728
+ const issueInfo = parseIssueRef(isFullUrl ? sanitizeUrl(rawInput) : rawInput, directory);
51731
51729
  if (!issueInfo) {
51732
- return `Error: Could not parse issue reference from "${rawInput}"
51730
+ return `Error: Could not parse issue reference from "${sanitizeErrorEcho(rawInput)}"
51733
51731
 
51734
51732
  ${USAGE6}`;
51735
51733
  }
51736
51734
  const issueUrl = `https://github.com/${issueInfo.owner}/${issueInfo.repo}/issues/${issueInfo.number}`;
51737
- const result = validateAndSanitizeUrl2(issueUrl);
51735
+ const result = validateAndSanitizeUrl(issueUrl);
51738
51736
  if ("error" in result) {
51739
51737
  return `Error: ${result.error}
51740
51738
 
@@ -51750,9 +51748,9 @@ ${USAGE6}`;
51750
51748
  const flagsStr = flags.length > 0 ? ` ${flags.join(" ")}` : "";
51751
51749
  return `[MODE: ISSUE_INGEST issue="${result.sanitized}"${flagsStr}]`;
51752
51750
  }
51753
- var MAX_URL_LEN2 = 2048, USAGE6;
51751
+ var USAGE6;
51754
51752
  var init_issue = __esm(() => {
51755
- init_pr_ref();
51753
+ init_url_security();
51756
51754
  USAGE6 = [
51757
51755
  "Usage: /swarm issue <url|owner/repo#N|N> [--plan] [--trace] [--no-repro]",
51758
51756
  "",
@@ -55944,6 +55942,88 @@ var init_post_mortem = __esm(() => {
55944
55942
  init_curator_postmortem();
55945
55943
  });
55946
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
+
55947
56027
  // src/commands/pr-feedback.ts
55948
56028
  function handlePrFeedbackCommand(directory, args) {
55949
56029
  const rest = args.filter((t) => t.trim().length > 0);