opencode-swarm 7.73.2 → 7.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/skills/swarm-pr-feedback/SKILL.md +19 -0
- package/dist/cli/index.js +194 -195
- package/dist/commands/_shared/url-security.d.ts +44 -0
- package/dist/commands/issue.d.ts +1 -1
- package/dist/commands/pr-ref.d.ts +2 -45
- package/dist/hooks/skill-usage-log.d.ts +13 -1
- package/dist/index.js +226 -205
- package/package.json +1 -1
|
@@ -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.
|
|
55
|
+
version: "7.74.0",
|
|
56
56
|
description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
|
|
57
57
|
main: "dist/index.js",
|
|
58
58
|
types: "dist/index.d.ts",
|
|
@@ -14700,7 +14700,7 @@ var init_plan_schema = __esm(() => {
|
|
|
14700
14700
|
init_zod();
|
|
14701
14701
|
ExecutionProfileSchema = exports_external.object({
|
|
14702
14702
|
parallelization_enabled: exports_external.boolean().default(false),
|
|
14703
|
-
max_concurrent_tasks: exports_external.number().int().min(1).max(64).default(
|
|
14703
|
+
max_concurrent_tasks: exports_external.number().int().min(1).max(64).default(10),
|
|
14704
14704
|
council_parallel: exports_external.boolean().default(true),
|
|
14705
14705
|
locked: exports_external.boolean().default(false),
|
|
14706
14706
|
auto_proceed: exports_external.boolean().default(false)
|
|
@@ -40093,6 +40093,9 @@ import * as path19 from "path";
|
|
|
40093
40093
|
function resolveLogPath(directory) {
|
|
40094
40094
|
return validateSwarmPath(directory, "skill-usage.jsonl");
|
|
40095
40095
|
}
|
|
40096
|
+
function normalizeComplianceVerdict(verdict) {
|
|
40097
|
+
return verdict === "violation" ? "violated" : verdict;
|
|
40098
|
+
}
|
|
40096
40099
|
function appendSkillUsageEntry(directory, entry) {
|
|
40097
40100
|
const {
|
|
40098
40101
|
skillPath,
|
|
@@ -40163,7 +40166,9 @@ function readSkillUsageEntries(directory, options) {
|
|
|
40163
40166
|
if (!trimmed)
|
|
40164
40167
|
continue;
|
|
40165
40168
|
try {
|
|
40166
|
-
|
|
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 === "
|
|
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 !== "
|
|
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 === "
|
|
40404
|
+
else if (entry.complianceVerdict === "violated")
|
|
40399
40405
|
violationCount++;
|
|
40400
40406
|
}
|
|
40401
40407
|
if (compliantCount === 0 && violationCount === 0)
|
|
@@ -45184,7 +45190,7 @@ function buildStatusMessage(session, plan) {
|
|
|
45184
45190
|
const overrideActive = session.maxConcurrencyOverride !== undefined;
|
|
45185
45191
|
const configuredOverride = session.maxConcurrencyOverride ?? "absent";
|
|
45186
45192
|
const hasPlan = plan !== null && plan !== undefined;
|
|
45187
|
-
const planBaseline = hasPlan ? plan.execution_profile?.max_concurrent_tasks ??
|
|
45193
|
+
const planBaseline = hasPlan ? plan.execution_profile?.max_concurrent_tasks ?? 10 : 10;
|
|
45188
45194
|
const parallelizationEnabled = hasPlan ? plan.execution_profile?.parallelization_enabled ?? false : false;
|
|
45189
45195
|
const operationalEffective = !parallelizationEnabled ? 1 : session.maxConcurrencyOverride ?? planBaseline;
|
|
45190
45196
|
let description;
|
|
@@ -45213,8 +45219,8 @@ var init_concurrency = __esm(() => {
|
|
|
45213
45219
|
init_state();
|
|
45214
45220
|
PRESETS = {
|
|
45215
45221
|
min: 1,
|
|
45216
|
-
medium:
|
|
45217
|
-
max:
|
|
45222
|
+
medium: 8,
|
|
45223
|
+
max: 16
|
|
45218
45224
|
};
|
|
45219
45225
|
});
|
|
45220
45226
|
|
|
@@ -51472,8 +51478,8 @@ var init_history = __esm(() => {
|
|
|
51472
51478
|
init_history_service();
|
|
51473
51479
|
});
|
|
51474
51480
|
|
|
51475
|
-
// src/commands/
|
|
51476
|
-
import {
|
|
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
|
|
51495
|
-
|
|
51496
|
-
const
|
|
51497
|
-
|
|
51498
|
-
|
|
51499
|
-
|
|
51500
|
-
|
|
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 === "
|
|
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
|
-
|
|
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 (
|
|
51557
|
+
if (isIpv4MappedPrivateHost(inner)) {
|
|
51529
51558
|
return true;
|
|
51530
51559
|
}
|
|
51531
51560
|
}
|
|
51532
51561
|
return false;
|
|
51533
51562
|
}
|
|
51534
|
-
function
|
|
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
|
|
51551
|
-
if (!
|
|
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
|
|
51592
|
+
const result = _internals28.spawnSync("git", ["remote", "get-url", "origin"], {
|
|
51600
51593
|
encoding: "utf-8",
|
|
51601
|
-
stdio: ["
|
|
51594
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
51602
51595
|
timeout: 5000,
|
|
51603
51596
|
...cwd ? { cwd } : {}
|
|
51604
|
-
})
|
|
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
|
-
|
|
51614
|
-
|
|
51615
|
-
|
|
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
|
-
|
|
51621
|
-
|
|
51622
|
-
|
|
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
|
-
|
|
51628
|
-
|
|
51629
|
-
|
|
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
|
-
|
|
51635
|
-
|
|
51636
|
-
|
|
51637
|
-
|
|
51638
|
-
|
|
51639
|
-
|
|
51640
|
-
|
|
51641
|
-
|
|
51642
|
-
|
|
51643
|
-
|
|
51644
|
-
|
|
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
|
-
|
|
51662
|
-
|
|
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 =
|
|
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
|
|
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 ?
|
|
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 =
|
|
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
|
|
51751
|
+
var USAGE6;
|
|
51835
51752
|
var init_issue = __esm(() => {
|
|
51836
|
-
|
|
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;
|
package/dist/commands/issue.d.ts
CHANGED
|
@@ -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(
|
|
13
|
+
export declare function handleIssueCommand(directory: string, args: string[]): string;
|