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/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.1",
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,
@@ -64285,6 +64291,49 @@ var init_synonym_map = __esm(() => {
64285
64291
  MIN_READ_CEILING_BYTES = 64 * 1024;
64286
64292
  });
64287
64293
 
64294
+ // src/hooks/knowledge-reinforcement.ts
64295
+ function isActiveSwarmKnowledgeEntry(entry) {
64296
+ return !INACTIVE_STATUSES.has(entry.status);
64297
+ }
64298
+ function findActiveSwarmNearDuplicate(lesson, entries, threshold) {
64299
+ return findNearDuplicate(lesson, entries.filter(isActiveSwarmKnowledgeEntry), threshold);
64300
+ }
64301
+ function distinctPhaseCount(records) {
64302
+ const phases = new Set;
64303
+ for (const record3 of records ?? []) {
64304
+ if (Number.isInteger(record3.phase_number)) {
64305
+ phases.add(record3.phase_number);
64306
+ }
64307
+ }
64308
+ return phases.size;
64309
+ }
64310
+ function reinforceSwarmKnowledgeEntry(entry, confirmation) {
64311
+ if (!isActiveSwarmKnowledgeEntry(entry)) {
64312
+ return { entryId: entry.id, reinforced: false, reason: "inactive" };
64313
+ }
64314
+ if ((entry.confirmed_by ?? []).some((record3) => record3.phase_number === confirmation.phase_number)) {
64315
+ return {
64316
+ entryId: entry.id,
64317
+ reinforced: false,
64318
+ reason: "already_confirmed_phase"
64319
+ };
64320
+ }
64321
+ entry.confirmed_by = [...entry.confirmed_by ?? [], confirmation];
64322
+ entry.updated_at = confirmation.confirmed_at;
64323
+ entry.phases_alive = 0;
64324
+ entry.confidence = computeConfidence(distinctPhaseCount(entry.confirmed_by), entry.auto_generated ?? false);
64325
+ return { entryId: entry.id, reinforced: true, reason: "reinforced" };
64326
+ }
64327
+ var INACTIVE_STATUSES;
64328
+ var init_knowledge_reinforcement = __esm(() => {
64329
+ init_knowledge_store();
64330
+ INACTIVE_STATUSES = new Set([
64331
+ "archived",
64332
+ "quarantined",
64333
+ "quarantined_unactionable"
64334
+ ]);
64335
+ });
64336
+
64288
64337
  // src/hooks/skill-scoring.ts
64289
64338
  import * as fs21 from "node:fs";
64290
64339
  import * as path41 from "node:path";
@@ -65838,6 +65887,7 @@ async function curateAndStoreSwarm(lessons, projectName, phaseInfo, directory, c
65838
65887
  ]);
65839
65888
  const snapshotPlusNew = [...snapshot];
65840
65889
  const toAdd = [];
65890
+ const pendingReinforcementIds = new Set;
65841
65891
  for (const lesson of lessons) {
65842
65892
  const tags = inferTags(lesson);
65843
65893
  let category = "process";
@@ -65867,8 +65917,9 @@ async function curateAndStoreSwarm(lessons, projectName, phaseInfo, directory, c
65867
65917
  continue;
65868
65918
  }
65869
65919
  }
65870
- const duplicate = findNearDuplicate(lesson, snapshotPlusNew, config3.dedup_threshold);
65920
+ const duplicate = findActiveSwarmNearDuplicate(lesson, snapshotPlusNew, config3.dedup_threshold);
65871
65921
  if (duplicate) {
65922
+ pendingReinforcementIds.add(duplicate.id);
65872
65923
  skipped++;
65873
65924
  continue;
65874
65925
  }
@@ -65949,7 +66000,9 @@ async function curateAndStoreSwarm(lessons, projectName, phaseInfo, directory, c
65949
66000
  } catch {}
65950
66001
  continue;
65951
66002
  }
65952
- if (findNearDuplicate(entry.lesson, snapshotPlusNew, config3.dedup_threshold)) {
66003
+ const duplicate = findActiveSwarmNearDuplicate(entry.lesson, snapshotPlusNew, config3.dedup_threshold);
66004
+ if (duplicate) {
66005
+ pendingReinforcementIds.add(duplicate.id);
65953
66006
  skipped++;
65954
66007
  continue;
65955
66008
  }
@@ -65958,15 +66011,48 @@ async function curateAndStoreSwarm(lessons, projectName, phaseInfo, directory, c
65958
66011
  }
65959
66012
  } catch {}
65960
66013
  let stored = 0;
65961
- if (toAdd.length > 0) {
66014
+ let reinforced = 0;
66015
+ if (toAdd.length > 0 || pendingReinforcementIds.size > 0) {
65962
66016
  await transactKnowledge(knowledgePath, (current) => {
65963
- const trulyNew = toAdd.filter((e) => !findNearDuplicate(e.lesson, current, config3.dedup_threshold));
65964
- const extraDups = toAdd.length - trulyNew.length;
65965
- skipped += extraDups;
65966
- if (trulyNew.length === 0)
65967
- return null;
65968
- stored = trulyNew.length;
65969
- return [...current, ...trulyNew];
66017
+ let changed = false;
66018
+ for (const id of pendingReinforcementIds) {
66019
+ const existing = current.find((entry) => entry.id === id);
66020
+ if (!existing)
66021
+ continue;
66022
+ const result = reinforceSwarmKnowledgeEntry(existing, {
66023
+ phase_number: phaseInfo.phase_number,
66024
+ confirmed_at: new Date().toISOString(),
66025
+ project_name: projectName
66026
+ });
66027
+ if (result.reinforced) {
66028
+ reinforced++;
66029
+ changed = true;
66030
+ }
66031
+ }
66032
+ const trulyNew = [];
66033
+ for (const entry of toAdd) {
66034
+ const duplicate = findActiveSwarmNearDuplicate(entry.lesson, current, config3.dedup_threshold);
66035
+ if (duplicate) {
66036
+ skipped++;
66037
+ const result = reinforceSwarmKnowledgeEntry(duplicate, {
66038
+ phase_number: phaseInfo.phase_number,
66039
+ confirmed_at: new Date().toISOString(),
66040
+ project_name: projectName
66041
+ });
66042
+ if (result.reinforced) {
66043
+ reinforced++;
66044
+ changed = true;
66045
+ }
66046
+ continue;
66047
+ }
66048
+ trulyNew.push(entry);
66049
+ }
66050
+ if (trulyNew.length > 0) {
66051
+ current.push(...trulyNew);
66052
+ stored = trulyNew.length;
66053
+ changed = true;
66054
+ }
66055
+ return changed ? current : null;
65970
66056
  });
65971
66057
  }
65972
66058
  await enforceKnowledgeCap(knowledgePath, config3.swarm_max_entries);
@@ -65984,7 +66070,7 @@ async function curateAndStoreSwarm(lessons, projectName, phaseInfo, directory, c
65984
66070
  if (!options?.skipAutoPromotion) {
65985
66071
  await _internals30.runAutoPromotion(directory, config3);
65986
66072
  }
65987
- return { stored, skipped, rejected, quarantined };
66073
+ return { stored, reinforced, skipped, rejected, quarantined };
65988
66074
  }
65989
66075
  async function runAutoPromotion(directory, config3) {
65990
66076
  const knowledgePath = resolveSwarmKnowledgePath(directory);
@@ -66097,6 +66183,7 @@ var init_knowledge_curator = __esm(() => {
66097
66183
  init_synonym_map();
66098
66184
  init_logger();
66099
66185
  init_knowledge_events();
66186
+ init_knowledge_reinforcement();
66100
66187
  init_knowledge_store();
66101
66188
  init_knowledge_validator();
66102
66189
  init_micro_reflector();
@@ -75112,8 +75199,8 @@ var init_history = __esm(() => {
75112
75199
  init_history_service();
75113
75200
  });
75114
75201
 
75115
- // src/commands/pr-ref.ts
75116
- import { execSync as execSync2 } from "node:child_process";
75202
+ // src/commands/_shared/url-security.ts
75203
+ import { spawnSync as spawnSync8 } from "node:child_process";
75117
75204
  function sanitizeUrl(raw) {
75118
75205
  let urlStr = raw.trim();
75119
75206
  urlStr = urlStr.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
@@ -75131,13 +75218,29 @@ function sanitizeUrl(raw) {
75131
75218
  }
75132
75219
  return urlStr.trim();
75133
75220
  }
75134
- function sanitizeInstructions(raw) {
75135
- const collapsed = raw.replace(/\s+/g, " ").trim();
75136
- const stripped = collapsed.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
75137
- const normalized = stripped.replace(/\s+/g, " ").trim();
75138
- if (normalized.length <= MAX_INSTRUCTIONS_LEN)
75139
- return normalized;
75140
- 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;
75141
75244
  }
75142
75245
  function hasNonAsciiHostname(hostname5) {
75143
75246
  for (const ch of hostname5) {
@@ -75147,31 +75250,38 @@ function hasNonAsciiHostname(hostname5) {
75147
75250
  }
75148
75251
  return false;
75149
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
+ }
75150
75265
  function isPrivateHost(url3) {
75151
- const host = url3.hostname.toLowerCase();
75152
- 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)) {
75153
75268
  return true;
75154
75269
  }
75155
75270
  if (host.startsWith("localhost") || host === "localhost.com") {
75156
75271
  return true;
75157
75272
  }
75158
- const ipv4Private = /^10\./;
75159
- const ipv4172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
75160
- const ipv4192 = /^192\.168\./;
75161
- const ipv6Private = /^fe80:/i;
75162
- const ipv6Unique = /^f[cd][0-9a-f]{2}:/i;
75163
- 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)) {
75164
75274
  return true;
75165
75275
  }
75166
75276
  if (host.startsWith("::ffff:")) {
75167
75277
  const inner = host.slice(7);
75168
- if (ipv4Private.test(inner) || ipv4172.test(inner) || ipv4192.test(inner)) {
75278
+ if (isIpv4MappedPrivateHost(inner)) {
75169
75279
  return true;
75170
75280
  }
75171
75281
  }
75172
75282
  return false;
75173
75283
  }
75174
- function validateAndSanitizeUrl(rawUrl) {
75284
+ function validateAndSanitizeGithubUrl(rawUrl, resource) {
75175
75285
  const sanitized = sanitizeUrl(rawUrl);
75176
75286
  if (!sanitized) {
75177
75287
  return { error: "Empty URL" };
@@ -75187,10 +75297,10 @@ function validateAndSanitizeUrl(rawUrl) {
75187
75297
  if (isPrivateHost(url3)) {
75188
75298
  return { error: "Private or localhost URLs are not allowed" };
75189
75299
  }
75190
- const githubPrPattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/([0-9]+)\/?$/;
75191
- if (!githubPrPattern.test(sanitized)) {
75300
+ const githubPattern = new RegExp(`^https:\\/\\/github\\.com\\/([^/]+)\\/([^/]+)\\/${resource}\\/([0-9]+)\\/?$`);
75301
+ if (!githubPattern.test(sanitized)) {
75192
75302
  return {
75193
- 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)"
75194
75304
  };
75195
75305
  }
75196
75306
  return { sanitized };
@@ -75198,50 +75308,18 @@ function validateAndSanitizeUrl(rawUrl) {
75198
75308
  return { error: "Invalid URL format" };
75199
75309
  }
75200
75310
  }
75201
- function parsePrRef(input, cwd) {
75202
- const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i);
75203
- if (urlMatch) {
75204
- return {
75205
- owner: urlMatch[1],
75206
- repo: urlMatch[2],
75207
- number: parseInt(urlMatch[3], 10)
75208
- };
75209
- }
75210
- const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
75211
- if (shorthandMatch) {
75212
- return {
75213
- owner: shorthandMatch[1],
75214
- repo: shorthandMatch[2],
75215
- number: parseInt(shorthandMatch[3], 10)
75216
- };
75217
- }
75218
- const bareMatch = input.match(/^(\d+)$/);
75219
- if (bareMatch) {
75220
- const prNumber = parseInt(bareMatch[1], 10);
75221
- const remoteUrl = detectGitRemote(cwd);
75222
- if (!remoteUrl) {
75223
- return null;
75224
- }
75225
- const parsed = parseGitRemoteUrl(remoteUrl);
75226
- if (!parsed) {
75227
- return null;
75228
- }
75229
- return {
75230
- owner: parsed.owner,
75231
- repo: parsed.repo,
75232
- number: prNumber
75233
- };
75234
- }
75235
- return null;
75236
- }
75237
75311
  function detectGitRemote(cwd) {
75238
75312
  try {
75239
- const remoteUrl = _internals39.execSync("git remote get-url origin", {
75313
+ const result = _internals39.spawnSync("git", ["remote", "get-url", "origin"], {
75240
75314
  encoding: "utf-8",
75241
- stdio: ["pipe", "pipe", "pipe"],
75315
+ stdio: ["ignore", "pipe", "pipe"],
75242
75316
  timeout: 5000,
75243
75317
  ...cwd ? { cwd } : {}
75244
- }).trim();
75318
+ });
75319
+ if (result.status !== 0 || result.error) {
75320
+ return null;
75321
+ }
75322
+ const remoteUrl = (result.stdout ?? "").trim();
75245
75323
  return remoteUrl || null;
75246
75324
  } catch {
75247
75325
  return null;
@@ -75250,123 +75328,49 @@ function detectGitRemote(cwd) {
75250
75328
  function parseGitRemoteUrl(remoteUrl) {
75251
75329
  const httpsMatch = remoteUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
75252
75330
  if (httpsMatch) {
75253
- return {
75254
- owner: httpsMatch[1],
75255
- repo: httpsMatch[2].replace(/\.git$/, "")
75256
- };
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 };
75257
75337
  }
75258
75338
  const sshMatch = remoteUrl.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
75259
75339
  if (sshMatch) {
75260
- return {
75261
- owner: sshMatch[1],
75262
- repo: sshMatch[2].replace(/\.git$/, "")
75263
- };
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 };
75264
75346
  }
75265
75347
  const pathMatch = remoteUrl.match(/\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
75266
75348
  if (pathMatch) {
75267
- return {
75268
- owner: pathMatch[1],
75269
- repo: pathMatch[2].replace(/\.git$/, "")
75270
- };
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 };
75271
75355
  }
75272
75356
  return null;
75273
75357
  }
75274
- function looksLikePrRef(token) {
75275
- return /^https?:\/\//i.test(token) || /^[^/]+\/[^#]+#\d+$/.test(token) || /^\d+$/.test(token);
75276
- }
75277
- function resolvePrCommandInput(rest, cwd) {
75278
- if (rest.length === 0) {
75279
- return null;
75280
- }
75281
- const refToken = rest[0];
75282
- const instructions = sanitizeInstructions(rest.slice(1).join(" "));
75283
- const isFullUrl = /^https?:\/\//i.test(refToken);
75284
- const prInfo = parsePrRef(isFullUrl ? sanitizeUrl(refToken) : refToken, cwd);
75285
- if (!prInfo) {
75286
- return { error: `Could not parse PR reference from "${refToken}"` };
75287
- }
75288
- const prUrl = `https://github.com/${prInfo.owner}/${prInfo.repo}/pull/${prInfo.number}`;
75289
- const result = validateAndSanitizeUrl(prUrl);
75290
- if ("error" in result) {
75291
- return { error: result.error };
75292
- }
75293
- return { prUrl: result.sanitized, instructions };
75294
- }
75295
- var _internals39, MAX_URL_LEN = 2048, MAX_INSTRUCTIONS_LEN = 1000;
75296
- var init_pr_ref = __esm(() => {
75297
- _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 };
75298
75369
  });
75299
75370
 
75300
75371
  // src/commands/issue.ts
75301
- import { execSync as execSync3 } from "node:child_process";
75302
- function sanitizeUrl2(raw) {
75303
- let urlStr = raw.trim();
75304
- urlStr = urlStr.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
75305
- const fragmentIdx = urlStr.indexOf("#");
75306
- if (fragmentIdx !== -1) {
75307
- urlStr = urlStr.slice(0, fragmentIdx);
75308
- }
75309
- const queryIdx = urlStr.indexOf("?");
75310
- if (queryIdx !== -1) {
75311
- urlStr = urlStr.slice(0, queryIdx);
75312
- }
75313
- urlStr = urlStr.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^@/]+@/, "https://");
75314
- if (urlStr.length > MAX_URL_LEN2) {
75315
- urlStr = urlStr.slice(0, MAX_URL_LEN2);
75316
- }
75317
- return urlStr.trim();
75318
- }
75319
- function isPrivateHost2(url3) {
75320
- const host = url3.hostname.toLowerCase();
75321
- if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0") {
75322
- return true;
75323
- }
75324
- if (host.startsWith("localhost") || host === "localhost.com") {
75325
- return true;
75326
- }
75327
- const ipv4Private = /^10\./;
75328
- const ipv4172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
75329
- const ipv4192 = /^192\.168\./;
75330
- const ipv6Private = /^fe80:/i;
75331
- const ipv6Unique = /^f[cd][0-9a-f]{2}:/i;
75332
- if (ipv4Private.test(host) || ipv4172.test(host) || ipv4192.test(host) || ipv6Private.test(host) || ipv6Unique.test(host)) {
75333
- return true;
75334
- }
75335
- if (host.startsWith("::ffff:")) {
75336
- const inner = host.slice(7);
75337
- if (ipv4Private.test(inner) || ipv4172.test(inner) || ipv4192.test(inner)) {
75338
- return true;
75339
- }
75340
- }
75341
- return false;
75342
- }
75343
- function validateAndSanitizeUrl2(rawUrl) {
75344
- const sanitized = sanitizeUrl2(rawUrl);
75345
- if (!sanitized) {
75346
- return { error: "Empty URL" };
75347
- }
75348
- if (!sanitized.startsWith("https://")) {
75349
- return { error: "URL must use HTTPS scheme" };
75350
- }
75351
- try {
75352
- const url3 = new URL(sanitized);
75353
- const hostname5 = url3.hostname;
75354
- if (/[\u0080-\u{10FFFF}]/u.test(hostname5)) {
75355
- return { error: "Non-ASCII hostnames are not allowed" };
75356
- }
75357
- if (isPrivateHost2(url3)) {
75358
- return { error: "Private or localhost URLs are not allowed" };
75359
- }
75360
- const githubIssuePattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/([0-9]+)\/?$/;
75361
- if (!githubIssuePattern.test(sanitized)) {
75362
- return {
75363
- error: "URL must be a GitHub issue URL (https://github.com/owner/repo/issues/N)"
75364
- };
75365
- }
75366
- return { sanitized };
75367
- } catch {
75368
- return { error: "Invalid URL format" };
75369
- }
75372
+ function validateAndSanitizeUrl(rawUrl) {
75373
+ return validateAndSanitizeGithubUrl(rawUrl, "issues");
75370
75374
  }
75371
75375
  function parseArgs6(args2) {
75372
75376
  const out2 = {
@@ -75393,9 +75397,12 @@ function parseArgs6(args2) {
75393
75397
  }
75394
75398
  return out2;
75395
75399
  }
75396
- function parseIssueRef(input) {
75400
+ function parseIssueRef(input, directory) {
75397
75401
  const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)\/?$/i);
75398
75402
  if (urlMatch) {
75403
+ if (containsControlCharacters(urlMatch[1]) || containsControlCharacters(urlMatch[2])) {
75404
+ return null;
75405
+ }
75399
75406
  return {
75400
75407
  owner: urlMatch[1],
75401
75408
  repo: urlMatch[2],
@@ -75404,6 +75411,9 @@ function parseIssueRef(input) {
75404
75411
  }
75405
75412
  const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
75406
75413
  if (shorthandMatch) {
75414
+ if (containsControlCharacters(shorthandMatch[1]) || containsControlCharacters(shorthandMatch[2])) {
75415
+ return null;
75416
+ }
75407
75417
  return {
75408
75418
  owner: shorthandMatch[1],
75409
75419
  repo: shorthandMatch[2],
@@ -75413,7 +75423,7 @@ function parseIssueRef(input) {
75413
75423
  const bareMatch = input.match(/^(\d+)$/);
75414
75424
  if (bareMatch) {
75415
75425
  const issueNumber = parseInt(bareMatch[1], 10);
75416
- const remoteUrl = detectGitRemote2();
75426
+ const remoteUrl = detectGitRemote(directory);
75417
75427
  if (!remoteUrl) {
75418
75428
  return null;
75419
75429
  }
@@ -75429,33 +75439,21 @@ function parseIssueRef(input) {
75429
75439
  }
75430
75440
  return null;
75431
75441
  }
75432
- function detectGitRemote2() {
75433
- try {
75434
- const remoteUrl = execSync3("git remote get-url origin", {
75435
- encoding: "utf-8",
75436
- stdio: ["pipe", "pipe", "pipe"],
75437
- timeout: 5000
75438
- }).trim();
75439
- return remoteUrl || null;
75440
- } catch {
75441
- return null;
75442
- }
75443
- }
75444
- function handleIssueCommand(_directory, args2) {
75442
+ function handleIssueCommand(directory, args2) {
75445
75443
  const parsed = parseArgs6(args2);
75446
75444
  const rawInput = parsed.rest.join(" ").trim();
75447
75445
  if (!rawInput) {
75448
75446
  return USAGE6;
75449
75447
  }
75450
75448
  const isFullUrl = /^https?:\/\//i.test(rawInput);
75451
- const issueInfo = parseIssueRef(isFullUrl ? sanitizeUrl2(rawInput) : rawInput);
75449
+ const issueInfo = parseIssueRef(isFullUrl ? sanitizeUrl(rawInput) : rawInput, directory);
75452
75450
  if (!issueInfo) {
75453
- return `Error: Could not parse issue reference from "${rawInput}"
75451
+ return `Error: Could not parse issue reference from "${sanitizeErrorEcho(rawInput)}"
75454
75452
 
75455
75453
  ${USAGE6}`;
75456
75454
  }
75457
75455
  const issueUrl = `https://github.com/${issueInfo.owner}/${issueInfo.repo}/issues/${issueInfo.number}`;
75458
- const result = validateAndSanitizeUrl2(issueUrl);
75456
+ const result = validateAndSanitizeUrl(issueUrl);
75459
75457
  if ("error" in result) {
75460
75458
  return `Error: ${result.error}
75461
75459
 
@@ -75471,9 +75469,9 @@ ${USAGE6}`;
75471
75469
  const flagsStr = flags2.length > 0 ? ` ${flags2.join(" ")}` : "";
75472
75470
  return `[MODE: ISSUE_INGEST issue="${result.sanitized}"${flagsStr}]`;
75473
75471
  }
75474
- var MAX_URL_LEN2 = 2048, USAGE6;
75472
+ var USAGE6;
75475
75473
  var init_issue = __esm(() => {
75476
- init_pr_ref();
75474
+ init_url_security();
75477
75475
  USAGE6 = [
75478
75476
  "Usage: /swarm issue <url|owner/repo#N|N> [--plan] [--trace] [--no-repro]",
75479
75477
  "",
@@ -80656,6 +80654,88 @@ var init_post_mortem = __esm(() => {
80656
80654
  init_curator_postmortem();
80657
80655
  });
80658
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
+
80659
80739
  // src/commands/pr-feedback.ts
80660
80740
  function handlePrFeedbackCommand(directory, args2) {
80661
80741
  const rest = args2.filter((t) => t.trim().length > 0);
@@ -124942,10 +125022,10 @@ var knowledge_ack = createSwarmTool({
124942
125022
  // src/tools/knowledge-add.ts
124943
125023
  init_zod();
124944
125024
  init_config();
125025
+ init_knowledge_reinforcement();
124945
125026
  init_knowledge_store();
124946
125027
  init_knowledge_validator();
124947
125028
  init_manager();
124948
- init_utils();
124949
125029
  init_create_tool();
124950
125030
  import { randomUUID as randomUUID15 } from "node:crypto";
124951
125031
  var VALID_CATEGORIES2 = [
@@ -125037,9 +125117,13 @@ var knowledge_add = createSwarmTool({
125037
125117
  });
125038
125118
  }
125039
125119
  let project_name = "";
125120
+ let phase_number = 1;
125040
125121
  try {
125041
125122
  const plan = await loadPlan(directory);
125042
125123
  project_name = plan?.title ?? "";
125124
+ if (typeof plan?.current_phase === "number") {
125125
+ phase_number = plan.current_phase;
125126
+ }
125043
125127
  } catch {}
125044
125128
  const entry = {
125045
125129
  id: randomUUID15(),
@@ -125084,19 +125168,6 @@ var knowledge_add = createSwarmTool({
125084
125168
  }
125085
125169
  }
125086
125170
  } catch {}
125087
- try {
125088
- const existingEntries = await readKnowledge(resolveSwarmKnowledgePath(directory));
125089
- const duplicate = findNearDuplicate(lesson, existingEntries, dedupThreshold);
125090
- if (duplicate) {
125091
- return JSON.stringify({
125092
- success: false,
125093
- id: duplicate.id,
125094
- message: "near-duplicate of existing entry"
125095
- });
125096
- }
125097
- } catch (err2) {
125098
- warn("knowledge_add: dedup check failed — skipping near-duplicate detection", err2);
125099
- }
125100
125171
  const actionability = validateActionability(entry);
125101
125172
  if (!actionability.actionable) {
125102
125173
  try {
@@ -125112,7 +125183,55 @@ var knowledge_add = createSwarmTool({
125112
125183
  }
125113
125184
  try {
125114
125185
  const maxEntries = config3?.knowledge?.swarm_max_entries ?? 100;
125115
- await appendKnowledgeWithCapEnforcement(resolveSwarmKnowledgePath(directory), entry, maxEntries);
125186
+ let duplicateResponse;
125187
+ await transactKnowledge(resolveSwarmKnowledgePath(directory), (existingEntries) => {
125188
+ const activeDuplicate = findActiveSwarmNearDuplicate(lesson, existingEntries, dedupThreshold);
125189
+ if (activeDuplicate) {
125190
+ const result = reinforceSwarmKnowledgeEntry(activeDuplicate, {
125191
+ phase_number,
125192
+ confirmed_at: new Date().toISOString(),
125193
+ project_name
125194
+ });
125195
+ duplicateResponse = {
125196
+ id: activeDuplicate.id,
125197
+ reinforced: result.reinforced,
125198
+ idempotent: result.reason === "already_confirmed_phase",
125199
+ inactive: false
125200
+ };
125201
+ return result.reinforced ? existingEntries : null;
125202
+ }
125203
+ const inactiveDuplicate = findNearDuplicate(lesson, existingEntries, dedupThreshold);
125204
+ if (inactiveDuplicate) {
125205
+ duplicateResponse = {
125206
+ id: inactiveDuplicate.id,
125207
+ reinforced: false,
125208
+ idempotent: false,
125209
+ inactive: true
125210
+ };
125211
+ return null;
125212
+ }
125213
+ const updated = [...existingEntries, entry];
125214
+ if (updated.length > maxEntries) {
125215
+ return updated.slice(updated.length - maxEntries);
125216
+ }
125217
+ return updated;
125218
+ });
125219
+ if (duplicateResponse) {
125220
+ if (duplicateResponse.inactive) {
125221
+ return JSON.stringify({
125222
+ success: false,
125223
+ id: duplicateResponse.id,
125224
+ message: "near-duplicate of inactive existing entry"
125225
+ });
125226
+ }
125227
+ return JSON.stringify({
125228
+ success: true,
125229
+ id: duplicateResponse.id,
125230
+ reinforced: duplicateResponse.reinforced,
125231
+ idempotent: duplicateResponse.idempotent,
125232
+ message: duplicateResponse.reinforced ? "near-duplicate reinforced existing entry" : "near-duplicate already confirmed for this phase"
125233
+ });
125234
+ }
125116
125235
  } catch (err2) {
125117
125236
  const message = err2 instanceof Error ? err2.message : "Unknown error";
125118
125237
  return JSON.stringify({
@@ -127725,7 +127844,7 @@ import * as fs103 from "node:fs";
127725
127844
  import * as path156 from "node:path";
127726
127845
 
127727
127846
  // src/mutation/engine.ts
127728
- import { spawnSync as spawnSync10 } from "node:child_process";
127847
+ import { spawnSync as spawnSync11 } from "node:child_process";
127729
127848
  import { unlinkSync as unlinkSync19, writeFileSync as writeFileSync27 } from "node:fs";
127730
127849
  import * as path155 from "node:path";
127731
127850
 
@@ -127903,7 +128022,7 @@ var _internals88 = {
127903
128022
  executeMutation,
127904
128023
  computeReport,
127905
128024
  executeMutationSuite,
127906
- spawnSync: spawnSync10
128025
+ spawnSync: spawnSync11
127907
128026
  };
127908
128027
  async function executeMutation(patch, testCommand, testFiles, workingDir) {
127909
128028
  const startTime = Date.now();
@@ -130197,7 +130316,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
130197
130316
  const sessionState = swarmState.agentSessions.get(sessionID);
130198
130317
  if (sessionState) {
130199
130318
  sessionState.pendingAdvisoryMessages ??= [];
130200
- sessionState.pendingAdvisoryMessages.push(`[CURATOR] Knowledge curation: ${curationResult.stored} stored, ${curationResult.skipped} skipped, ${curationResult.rejected} rejected, ${curationResult.quarantined} quarantined (unactionable).`);
130319
+ sessionState.pendingAdvisoryMessages.push(`[CURATOR] Knowledge curation: ${curationResult.stored} stored, ${curationResult.reinforced} reinforced, ${curationResult.skipped} skipped, ${curationResult.rejected} rejected, ${curationResult.quarantined} quarantined (unactionable).`);
130201
130320
  }
130202
130321
  }
130203
130322
  await updateRetrievalOutcome(dir, `Phase ${phase}`, true);
@@ -141956,6 +142075,7 @@ var write_architecture_supervisor_evidence = createSwarmTool({
141956
142075
  };
141957
142076
  const evidencePath = writeSupervisorReport(dirResult.directory, report);
141958
142077
  let knowledgeProposed = 0;
142078
+ let knowledgeReinforced = 0;
141959
142079
  let knowledgeQuarantined = 0;
141960
142080
  try {
141961
142081
  const config3 = loadPluginConfig(dirResult.directory);
@@ -141972,6 +142092,7 @@ var write_architecture_supervisor_evidence = createSwarmTool({
141972
142092
  }
141973
142093
  });
141974
142094
  knowledgeProposed = result.stored;
142095
+ knowledgeReinforced = result.reinforced;
141975
142096
  knowledgeQuarantined = result.quarantined;
141976
142097
  }
141977
142098
  } catch {}
@@ -141993,6 +142114,7 @@ var write_architecture_supervisor_evidence = createSwarmTool({
141993
142114
  verdict: args2.verdict,
141994
142115
  findings_count: args2.findings.length,
141995
142116
  knowledge_proposed: knowledgeProposed,
142117
+ knowledge_reinforced: knowledgeReinforced,
141996
142118
  knowledge_quarantined: knowledgeQuarantined,
141997
142119
  skills_proposed: skillsProposed,
141998
142120
  evidence_path: evidencePath