opencode-swarm 6.86.14 → 7.0.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/dist/agents/architect.d.ts +1 -1
- package/dist/cli/index.js +726 -145
- package/dist/commands/close.d.ts +1 -0
- package/dist/commands/issue.d.ts +13 -0
- package/dist/commands/pr-review.d.ts +11 -0
- package/dist/commands/registry.d.ts +13 -1
- package/dist/config/plan-schema.d.ts +8 -0
- package/dist/config/schema.d.ts +6 -0
- package/dist/council/council-service.d.ts +10 -1
- package/dist/council/types.d.ts +36 -0
- package/dist/db/qa-gate-profile.d.ts +4 -1
- package/dist/git/branch.d.ts +20 -0
- package/dist/index.js +1113 -211
- package/dist/tools/set-qa-gates.d.ts +1 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -14055,6 +14055,7 @@ var init_plan_schema = __esm(() => {
|
|
|
14055
14055
|
name: exports_external.string().min(1),
|
|
14056
14056
|
status: PhaseStatusSchema.default("pending"),
|
|
14057
14057
|
tasks: exports_external.array(TaskSchema).default([]),
|
|
14058
|
+
type: exports_external.enum(["code", "non-code"]).optional(),
|
|
14058
14059
|
required_agents: exports_external.array(exports_external.string()).optional()
|
|
14059
14060
|
});
|
|
14060
14061
|
PlanSchema = exports_external.object({
|
|
@@ -18580,7 +18581,7 @@ import * as path33 from "path";
|
|
|
18580
18581
|
// package.json
|
|
18581
18582
|
var package_default = {
|
|
18582
18583
|
name: "opencode-swarm",
|
|
18583
|
-
version: "
|
|
18584
|
+
version: "7.0.0",
|
|
18584
18585
|
description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
|
|
18585
18586
|
main: "dist/index.js",
|
|
18586
18587
|
types: "dist/index.d.ts",
|
|
@@ -19623,12 +19624,15 @@ var CouncilConfigSchema = exports_external.object({
|
|
|
19623
19624
|
requireAllMembers: exports_external.boolean().default(false).describe("When true, submit_council_verdicts rejects if fewer than 5 member verdicts are provided. Equivalent to minimumMembers: 5."),
|
|
19624
19625
|
minimumMembers: exports_external.number().int().min(1).max(5).default(3).describe("Minimum distinct council member verdicts required for synthesis. Default 3. Set to 1 to disable quorum enforcement. requireAllMembers: true overrides this to 5 (stricter constraint wins)."),
|
|
19625
19626
|
escalateOnMaxRounds: exports_external.string().optional().describe("Optional webhook URL or handler name invoked when maxRounds is reached without APPROVE. Declared for forward compatibility; no behavior is implemented yet."),
|
|
19627
|
+
phaseConcernsAllowComplete: exports_external.boolean().default(true).describe("When true, a phase-level council CONCERNS verdict does NOT block phase completion \u2014 the advisory notes are logged as warnings and the phase proceeds. When false, CONCERNS blocks like REJECT. Default: true (CONCERNS is advisory)."),
|
|
19626
19628
|
general: GeneralCouncilConfigSchema.optional()
|
|
19627
19629
|
}).strict();
|
|
19628
19630
|
var ParallelizationConfigSchema = exports_external.object({
|
|
19629
19631
|
enabled: exports_external.boolean().default(false),
|
|
19630
19632
|
maxConcurrentTasks: exports_external.number().int().min(1).max(64).default(1),
|
|
19631
19633
|
evidenceLockTimeoutMs: exports_external.number().int().min(1000).max(300000).default(60000),
|
|
19634
|
+
max_coders: exports_external.number().int().min(1).max(16).default(3),
|
|
19635
|
+
max_reviewers: exports_external.number().int().min(1).max(16).default(2),
|
|
19632
19636
|
stageB: exports_external.object({
|
|
19633
19637
|
parallel: exports_external.object({
|
|
19634
19638
|
enabled: exports_external.boolean().default(false)
|
|
@@ -20066,7 +20070,8 @@ var DEFAULT_QA_GATES = {
|
|
|
20066
20070
|
hallucination_guard: false,
|
|
20067
20071
|
sast_enabled: true,
|
|
20068
20072
|
mutation_test: false,
|
|
20069
|
-
council_general_review: false
|
|
20073
|
+
council_general_review: false,
|
|
20074
|
+
drift_check: true
|
|
20070
20075
|
};
|
|
20071
20076
|
function rowToProfile(row) {
|
|
20072
20077
|
let parsed = {};
|
|
@@ -33822,7 +33827,6 @@ async function handleClarifyCommand(_directory, args) {
|
|
|
33822
33827
|
}
|
|
33823
33828
|
|
|
33824
33829
|
// src/commands/close.ts
|
|
33825
|
-
import { execFileSync } from "child_process";
|
|
33826
33830
|
import { promises as fs7 } from "fs";
|
|
33827
33831
|
import path12 from "path";
|
|
33828
33832
|
init_manager2();
|
|
@@ -33855,23 +33859,220 @@ function getCurrentBranch(cwd) {
|
|
|
33855
33859
|
const output = gitExec2(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
33856
33860
|
return output.trim();
|
|
33857
33861
|
}
|
|
33858
|
-
function
|
|
33862
|
+
function hasUncommittedChanges(cwd) {
|
|
33863
|
+
const status = gitExec2(["status", "--porcelain"], cwd);
|
|
33864
|
+
return status.trim().length > 0;
|
|
33865
|
+
}
|
|
33866
|
+
function detectDefaultRemoteBranch(cwd) {
|
|
33867
|
+
try {
|
|
33868
|
+
const output = gitExec2(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd);
|
|
33869
|
+
const trimmed = output.trim();
|
|
33870
|
+
if (trimmed.startsWith("refs/remotes/origin/")) {
|
|
33871
|
+
return trimmed.slice("refs/remotes/origin/".length);
|
|
33872
|
+
}
|
|
33873
|
+
} catch {}
|
|
33874
|
+
try {
|
|
33875
|
+
const output = gitExec2(["config", "init.defaultBranch"], cwd);
|
|
33876
|
+
const branch = output.trim();
|
|
33877
|
+
if (branch) {
|
|
33878
|
+
return branch;
|
|
33879
|
+
}
|
|
33880
|
+
} catch {}
|
|
33859
33881
|
try {
|
|
33860
33882
|
gitExec2(["rev-parse", "--verify", "origin/main"], cwd);
|
|
33861
|
-
return "
|
|
33883
|
+
return "main";
|
|
33884
|
+
} catch {}
|
|
33885
|
+
try {
|
|
33886
|
+
gitExec2(["rev-parse", "--verify", "origin/master"], cwd);
|
|
33887
|
+
return "master";
|
|
33862
33888
|
} catch {
|
|
33889
|
+
return null;
|
|
33890
|
+
}
|
|
33891
|
+
}
|
|
33892
|
+
function resetToRemoteBranch(cwd, options) {
|
|
33893
|
+
const warnings = [];
|
|
33894
|
+
const prunedBranches = [];
|
|
33895
|
+
try {
|
|
33896
|
+
const currentBranch = getCurrentBranch(cwd);
|
|
33897
|
+
const defaultRemoteBranch = detectDefaultRemoteBranch(cwd);
|
|
33898
|
+
if (!defaultRemoteBranch) {
|
|
33899
|
+
return {
|
|
33900
|
+
success: false,
|
|
33901
|
+
targetBranch: "",
|
|
33902
|
+
localBranch: currentBranch,
|
|
33903
|
+
message: "Could not detect default remote branch",
|
|
33904
|
+
alreadyAligned: false,
|
|
33905
|
+
prunedBranches: [],
|
|
33906
|
+
warnings: []
|
|
33907
|
+
};
|
|
33908
|
+
}
|
|
33909
|
+
const targetBranch = `origin/${defaultRemoteBranch}`;
|
|
33910
|
+
if (currentBranch === "HEAD") {
|
|
33911
|
+
return {
|
|
33912
|
+
success: false,
|
|
33913
|
+
targetBranch,
|
|
33914
|
+
localBranch: "HEAD",
|
|
33915
|
+
message: "Cannot reset: detached HEAD state",
|
|
33916
|
+
alreadyAligned: false,
|
|
33917
|
+
prunedBranches: [],
|
|
33918
|
+
warnings: []
|
|
33919
|
+
};
|
|
33920
|
+
}
|
|
33921
|
+
if (hasUncommittedChanges(cwd)) {
|
|
33922
|
+
return {
|
|
33923
|
+
success: false,
|
|
33924
|
+
targetBranch,
|
|
33925
|
+
localBranch: currentBranch,
|
|
33926
|
+
message: "Cannot reset: uncommitted changes in working tree",
|
|
33927
|
+
alreadyAligned: false,
|
|
33928
|
+
prunedBranches: [],
|
|
33929
|
+
warnings: []
|
|
33930
|
+
};
|
|
33931
|
+
}
|
|
33863
33932
|
try {
|
|
33864
|
-
gitExec2(["
|
|
33865
|
-
|
|
33866
|
-
|
|
33867
|
-
|
|
33933
|
+
const logOutput = gitExec2(["log", `${targetBranch}..HEAD`, "--oneline"], cwd);
|
|
33934
|
+
if (logOutput.trim().length > 0) {
|
|
33935
|
+
return {
|
|
33936
|
+
success: false,
|
|
33937
|
+
targetBranch,
|
|
33938
|
+
localBranch: currentBranch,
|
|
33939
|
+
message: "Cannot reset: unpushed commits",
|
|
33940
|
+
alreadyAligned: false,
|
|
33941
|
+
prunedBranches: [],
|
|
33942
|
+
warnings: []
|
|
33943
|
+
};
|
|
33944
|
+
}
|
|
33945
|
+
} catch {}
|
|
33946
|
+
try {
|
|
33947
|
+
gitExec2(["fetch", "--prune", "origin"], cwd);
|
|
33948
|
+
} catch (err) {
|
|
33949
|
+
return {
|
|
33950
|
+
success: false,
|
|
33951
|
+
targetBranch,
|
|
33952
|
+
localBranch: currentBranch,
|
|
33953
|
+
message: `Fetch failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
33954
|
+
alreadyAligned: false,
|
|
33955
|
+
prunedBranches: [],
|
|
33956
|
+
warnings: []
|
|
33957
|
+
};
|
|
33958
|
+
}
|
|
33959
|
+
const headSha = gitExec2(["rev-parse", "HEAD"], cwd).trim();
|
|
33960
|
+
const remoteSha = gitExec2(["rev-parse", `${targetBranch}`], cwd).trim();
|
|
33961
|
+
if (headSha === remoteSha) {
|
|
33962
|
+
return {
|
|
33963
|
+
success: true,
|
|
33964
|
+
targetBranch,
|
|
33965
|
+
localBranch: currentBranch,
|
|
33966
|
+
message: "Already aligned with remote",
|
|
33967
|
+
alreadyAligned: true,
|
|
33968
|
+
prunedBranches: [],
|
|
33969
|
+
warnings: []
|
|
33970
|
+
};
|
|
33971
|
+
}
|
|
33972
|
+
try {
|
|
33973
|
+
gitExec2(["checkout", currentBranch], cwd);
|
|
33974
|
+
} catch (err) {
|
|
33975
|
+
return {
|
|
33976
|
+
success: false,
|
|
33977
|
+
targetBranch,
|
|
33978
|
+
localBranch: currentBranch,
|
|
33979
|
+
message: `Checkout failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
33980
|
+
alreadyAligned: false,
|
|
33981
|
+
prunedBranches: [],
|
|
33982
|
+
warnings: []
|
|
33983
|
+
};
|
|
33984
|
+
}
|
|
33985
|
+
let resetSucceeded = false;
|
|
33986
|
+
let lastError;
|
|
33987
|
+
for (let retry = 0;retry < 4; retry++) {
|
|
33988
|
+
if (retry > 0 && process.platform === "win32") {
|
|
33989
|
+
const endTime = Date.now() + 500;
|
|
33990
|
+
while (Date.now() < endTime) {}
|
|
33991
|
+
}
|
|
33992
|
+
try {
|
|
33993
|
+
gitExec2(["reset", "--hard", targetBranch], cwd);
|
|
33994
|
+
resetSucceeded = true;
|
|
33995
|
+
break;
|
|
33996
|
+
} catch (err) {
|
|
33997
|
+
lastError = err;
|
|
33998
|
+
}
|
|
33999
|
+
}
|
|
34000
|
+
if (!resetSucceeded) {
|
|
34001
|
+
return {
|
|
34002
|
+
success: false,
|
|
34003
|
+
targetBranch,
|
|
34004
|
+
localBranch: currentBranch,
|
|
34005
|
+
message: `Reset failed: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
|
|
34006
|
+
alreadyAligned: false,
|
|
34007
|
+
prunedBranches: [],
|
|
34008
|
+
warnings: []
|
|
34009
|
+
};
|
|
34010
|
+
}
|
|
34011
|
+
if (options?.pruneBranches) {
|
|
34012
|
+
try {
|
|
34013
|
+
const mergedOutput = gitExec2(["branch", "--merged", targetBranch], cwd);
|
|
34014
|
+
const mergedLines = mergedOutput.split(`
|
|
34015
|
+
`);
|
|
34016
|
+
for (const line of mergedLines) {
|
|
34017
|
+
const trimmedLine = line.trim();
|
|
34018
|
+
if (!trimmedLine || trimmedLine.startsWith("*")) {
|
|
34019
|
+
continue;
|
|
34020
|
+
}
|
|
34021
|
+
try {
|
|
34022
|
+
gitExec2(["branch", "-d", trimmedLine], cwd);
|
|
34023
|
+
prunedBranches.push(trimmedLine);
|
|
34024
|
+
} catch {
|
|
34025
|
+
warnings.push(`Could not safely delete branch: ${trimmedLine}`);
|
|
34026
|
+
}
|
|
34027
|
+
}
|
|
34028
|
+
} catch (err) {
|
|
34029
|
+
warnings.push(`Failed to get merged branches: ${err instanceof Error ? err.message : String(err)}`);
|
|
34030
|
+
}
|
|
34031
|
+
try {
|
|
34032
|
+
const branchVvOutput = gitExec2(["branch", "-vv"], cwd);
|
|
34033
|
+
const vvLines = branchVvOutput.split(`
|
|
34034
|
+
`);
|
|
34035
|
+
for (const line of vvLines) {
|
|
34036
|
+
const trimmedLine = line.trim();
|
|
34037
|
+
if (!trimmedLine || trimmedLine.startsWith("*")) {
|
|
34038
|
+
continue;
|
|
34039
|
+
}
|
|
34040
|
+
if (trimmedLine.includes(": gone]")) {
|
|
34041
|
+
const parts = trimmedLine.split(/\s+/);
|
|
34042
|
+
const branchName = parts[0];
|
|
34043
|
+
try {
|
|
34044
|
+
gitExec2(["branch", "-d", branchName], cwd);
|
|
34045
|
+
prunedBranches.push(branchName);
|
|
34046
|
+
} catch {
|
|
34047
|
+
warnings.push(`Could not delete gone branch: ${branchName}`);
|
|
34048
|
+
}
|
|
34049
|
+
}
|
|
34050
|
+
}
|
|
34051
|
+
} catch (err) {
|
|
34052
|
+
warnings.push(`Failed to prune gone branches: ${err instanceof Error ? err.message : String(err)}`);
|
|
34053
|
+
}
|
|
33868
34054
|
}
|
|
34055
|
+
return {
|
|
34056
|
+
success: true,
|
|
34057
|
+
targetBranch,
|
|
34058
|
+
localBranch: currentBranch,
|
|
34059
|
+
message: "Successfully reset to remote branch",
|
|
34060
|
+
alreadyAligned: false,
|
|
34061
|
+
prunedBranches,
|
|
34062
|
+
warnings
|
|
34063
|
+
};
|
|
34064
|
+
} catch (err) {
|
|
34065
|
+
return {
|
|
34066
|
+
success: false,
|
|
34067
|
+
targetBranch: "",
|
|
34068
|
+
localBranch: "",
|
|
34069
|
+
message: `Unexpected error: ${err instanceof Error ? err.message : String(err)}`,
|
|
34070
|
+
alreadyAligned: false,
|
|
34071
|
+
prunedBranches: [],
|
|
34072
|
+
warnings: []
|
|
34073
|
+
};
|
|
33869
34074
|
}
|
|
33870
34075
|
}
|
|
33871
|
-
function hasUncommittedChanges(cwd) {
|
|
33872
|
-
const status = gitExec2(["status", "--porcelain"], cwd);
|
|
33873
|
-
return status.trim().length > 0;
|
|
33874
|
-
}
|
|
33875
34076
|
|
|
33876
34077
|
// src/hooks/knowledge-store.ts
|
|
33877
34078
|
var import_proper_lockfile3 = __toESM(require_proper_lockfile(), 1);
|
|
@@ -34999,6 +35200,26 @@ var ACTIVE_STATE_TO_CLEAN = [
|
|
|
34999
35200
|
"handoff-consumed.md",
|
|
35000
35201
|
"escalation-report.md"
|
|
35001
35202
|
];
|
|
35203
|
+
function guaranteeAllPlansComplete(planData) {
|
|
35204
|
+
const closedPhaseIds = [];
|
|
35205
|
+
const closedTaskIds = [];
|
|
35206
|
+
for (const phase of planData.phases ?? []) {
|
|
35207
|
+
const wasComplete = phase.status === "complete" || phase.status === "completed" || phase.status === "closed";
|
|
35208
|
+
if (!wasComplete) {
|
|
35209
|
+
phase.status = "closed";
|
|
35210
|
+
closedPhaseIds.push(phase.id);
|
|
35211
|
+
}
|
|
35212
|
+
for (const task of phase.tasks ?? []) {
|
|
35213
|
+
const wasTaskDone = task.status === "completed" || task.status === "complete" || task.status === "closed";
|
|
35214
|
+
if (!wasTaskDone) {
|
|
35215
|
+
task.status = "closed";
|
|
35216
|
+
task.close_reason = "session_terminated";
|
|
35217
|
+
closedTaskIds.push(task.id);
|
|
35218
|
+
}
|
|
35219
|
+
}
|
|
35220
|
+
}
|
|
35221
|
+
return { closedPhaseIds, closedTaskIds };
|
|
35222
|
+
}
|
|
35002
35223
|
async function handleCloseCommand(directory, args) {
|
|
35003
35224
|
const planPath = validateSwarmPath(directory, "plan.json");
|
|
35004
35225
|
const swarmDir = path12.join(directory, ".swarm");
|
|
@@ -35116,41 +35337,62 @@ async function handleCloseCommand(directory, args) {
|
|
|
35116
35337
|
explicitLessons = lessonsText.split(`
|
|
35117
35338
|
`).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
35118
35339
|
} catch {}
|
|
35340
|
+
const retroLessons = [];
|
|
35341
|
+
try {
|
|
35342
|
+
const evidenceDir = path12.join(swarmDir, "evidence");
|
|
35343
|
+
const evidenceEntries = await fs7.readdir(evidenceDir);
|
|
35344
|
+
const retroDirs = evidenceEntries.filter((e) => e.startsWith("retro-"));
|
|
35345
|
+
for (const retroDir of retroDirs) {
|
|
35346
|
+
const evidencePath = path12.join(evidenceDir, retroDir, "evidence.json");
|
|
35347
|
+
try {
|
|
35348
|
+
const content = await fs7.readFile(evidencePath, "utf-8");
|
|
35349
|
+
const parsed = JSON.parse(content);
|
|
35350
|
+
const entries = parsed.entries ?? [parsed];
|
|
35351
|
+
for (const entry of entries) {
|
|
35352
|
+
if (Array.isArray(entry.lessons_learned)) {
|
|
35353
|
+
for (const lesson of entry.lessons_learned) {
|
|
35354
|
+
if (typeof lesson === "string" && lesson.trim().length > 0) {
|
|
35355
|
+
retroLessons.push(lesson.trim());
|
|
35356
|
+
}
|
|
35357
|
+
}
|
|
35358
|
+
}
|
|
35359
|
+
}
|
|
35360
|
+
} catch {}
|
|
35361
|
+
}
|
|
35362
|
+
} catch {}
|
|
35363
|
+
const allLessons = [...new Set([...explicitLessons, ...retroLessons])];
|
|
35119
35364
|
let curationSucceeded = false;
|
|
35120
35365
|
try {
|
|
35121
|
-
await curateAndStoreSwarm(
|
|
35366
|
+
await curateAndStoreSwarm(allLessons, projectName, { phase_number: 0 }, directory, config3);
|
|
35122
35367
|
curationSucceeded = true;
|
|
35123
35368
|
} catch (error93) {
|
|
35124
35369
|
const msg = error93 instanceof Error ? error93.message : String(error93);
|
|
35125
35370
|
warnings.push(`Lessons curation failed: ${msg}`);
|
|
35126
35371
|
console.warn("[close-command] curateAndStoreSwarm error:", error93);
|
|
35127
35372
|
}
|
|
35128
|
-
if (curationSucceeded &&
|
|
35373
|
+
if (curationSucceeded && allLessons.length > 0) {
|
|
35129
35374
|
await fs7.unlink(lessonsFilePath).catch(() => {});
|
|
35130
35375
|
}
|
|
35131
|
-
if (planExists
|
|
35132
|
-
|
|
35133
|
-
|
|
35134
|
-
|
|
35135
|
-
|
|
35136
|
-
closedPhases.push(phase.id);
|
|
35137
|
-
}
|
|
35376
|
+
if (planExists) {
|
|
35377
|
+
const guaranteeResult = guaranteeAllPlansComplete(planData);
|
|
35378
|
+
for (const phaseId of guaranteeResult.closedPhaseIds) {
|
|
35379
|
+
if (!closedPhases.includes(phaseId)) {
|
|
35380
|
+
closedPhases.push(phaseId);
|
|
35138
35381
|
}
|
|
35139
|
-
|
|
35140
|
-
|
|
35141
|
-
|
|
35142
|
-
|
|
35143
|
-
closedTasks.push(task.id);
|
|
35144
|
-
}
|
|
35145
|
-
}
|
|
35382
|
+
}
|
|
35383
|
+
for (const taskId of guaranteeResult.closedTaskIds) {
|
|
35384
|
+
if (!closedTasks.includes(taskId)) {
|
|
35385
|
+
closedTasks.push(taskId);
|
|
35146
35386
|
}
|
|
35147
35387
|
}
|
|
35148
|
-
|
|
35149
|
-
|
|
35150
|
-
|
|
35151
|
-
|
|
35152
|
-
|
|
35153
|
-
|
|
35388
|
+
if (!planAlreadyDone || guaranteeResult.closedPhaseIds.length > 0 || guaranteeResult.closedTaskIds.length > 0) {
|
|
35389
|
+
try {
|
|
35390
|
+
await fs7.writeFile(planPath, JSON.stringify(planData, null, 2), "utf-8");
|
|
35391
|
+
} catch (error93) {
|
|
35392
|
+
const msg = error93 instanceof Error ? error93.message : String(error93);
|
|
35393
|
+
warnings.push(`Failed to persist terminal plan.json state: ${msg}`);
|
|
35394
|
+
console.warn("[close-command] Failed to write plan.json:", error93);
|
|
35395
|
+
}
|
|
35154
35396
|
}
|
|
35155
35397
|
}
|
|
35156
35398
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
@@ -35289,111 +35531,27 @@ async function handleCloseCommand(directory, args) {
|
|
|
35289
35531
|
console.warn("[close-command] Failed to write context.md:", error93);
|
|
35290
35532
|
}
|
|
35291
35533
|
const pruneBranches = args.includes("--prune-branches");
|
|
35292
|
-
const prunedBranches = [];
|
|
35293
|
-
const pruneErrors = [];
|
|
35294
35534
|
let gitAlignResult = "";
|
|
35535
|
+
const prunedBranches = [];
|
|
35295
35536
|
const isGit = isGitRepo2(directory);
|
|
35296
35537
|
if (isGit) {
|
|
35297
|
-
|
|
35298
|
-
|
|
35299
|
-
|
|
35300
|
-
|
|
35301
|
-
|
|
35302
|
-
} else if (hasUncommittedChanges(directory)) {
|
|
35303
|
-
gitAlignResult = "Skipped git alignment: uncommitted changes in worktree";
|
|
35304
|
-
warnings.push("Uncommitted changes detected. Commit or stash before aligning to main.");
|
|
35305
|
-
} else {
|
|
35306
|
-
const baseBranch = getDefaultBaseBranch(directory);
|
|
35307
|
-
const localBase = baseBranch.replace(/^origin\//, "");
|
|
35308
|
-
if (currentBranch === localBase) {
|
|
35309
|
-
try {
|
|
35310
|
-
execFileSync("git", ["fetch", "origin", localBase], {
|
|
35311
|
-
cwd: directory,
|
|
35312
|
-
encoding: "utf-8",
|
|
35313
|
-
timeout: 30000,
|
|
35314
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
35315
|
-
});
|
|
35316
|
-
const mergeBase = execFileSync("git", ["merge-base", "HEAD", baseBranch], {
|
|
35317
|
-
cwd: directory,
|
|
35318
|
-
encoding: "utf-8",
|
|
35319
|
-
timeout: 1e4,
|
|
35320
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
35321
|
-
}).trim();
|
|
35322
|
-
const headSha = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
35323
|
-
cwd: directory,
|
|
35324
|
-
encoding: "utf-8",
|
|
35325
|
-
timeout: 1e4,
|
|
35326
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
35327
|
-
}).trim();
|
|
35328
|
-
if (mergeBase === headSha) {
|
|
35329
|
-
execFileSync("git", ["merge", "--ff-only", baseBranch], {
|
|
35330
|
-
cwd: directory,
|
|
35331
|
-
encoding: "utf-8",
|
|
35332
|
-
timeout: 30000,
|
|
35333
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
35334
|
-
});
|
|
35335
|
-
gitAlignResult = `Aligned to ${baseBranch} (fast-forward)`;
|
|
35336
|
-
} else {
|
|
35337
|
-
gitAlignResult = `On ${localBase} but cannot fast-forward to ${baseBranch} (diverged)`;
|
|
35338
|
-
warnings.push(`Local ${localBase} has diverged from ${baseBranch}. Manual merge/rebase needed.`);
|
|
35339
|
-
}
|
|
35340
|
-
} catch (fetchErr) {
|
|
35341
|
-
gitAlignResult = `Fetch from origin/${localBase} failed \u2014 remote may be unavailable`;
|
|
35342
|
-
warnings.push(`Git fetch failed: ${fetchErr instanceof Error ? fetchErr.message : String(fetchErr)}`);
|
|
35343
|
-
}
|
|
35344
|
-
} else {
|
|
35345
|
-
gitAlignResult = `On branch ${currentBranch}. Switch to ${localBase} manually when ready for a new swarm.`;
|
|
35346
|
-
}
|
|
35347
|
-
}
|
|
35348
|
-
} catch (gitError) {
|
|
35349
|
-
gitAlignResult = `Git alignment error: ${gitError instanceof Error ? gitError.message : String(gitError)}`;
|
|
35538
|
+
const alignResult = resetToRemoteBranch(directory, { pruneBranches });
|
|
35539
|
+
gitAlignResult = alignResult.message;
|
|
35540
|
+
prunedBranches.push(...alignResult.prunedBranches);
|
|
35541
|
+
if (!alignResult.success) {
|
|
35542
|
+
warnings.push(`Git alignment: ${alignResult.message}`);
|
|
35350
35543
|
}
|
|
35351
|
-
if (
|
|
35352
|
-
|
|
35353
|
-
|
|
35354
|
-
|
|
35355
|
-
|
|
35356
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
35357
|
-
});
|
|
35358
|
-
const goneBranches = branchOutput.split(`
|
|
35359
|
-
`).filter((line) => line.includes(": gone]")).map((line) => line.trim().replace(/^[*+]\s+/, "").split(/\s+/)[0]).filter(Boolean);
|
|
35360
|
-
for (const branch of goneBranches) {
|
|
35361
|
-
try {
|
|
35362
|
-
execFileSync("git", ["branch", "-d", branch], {
|
|
35363
|
-
cwd: directory,
|
|
35364
|
-
encoding: "utf-8",
|
|
35365
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
35366
|
-
});
|
|
35367
|
-
prunedBranches.push(branch);
|
|
35368
|
-
} catch {
|
|
35369
|
-
pruneErrors.push(branch);
|
|
35370
|
-
}
|
|
35371
|
-
}
|
|
35372
|
-
} catch {}
|
|
35544
|
+
if (alignResult.alreadyAligned) {
|
|
35545
|
+
gitAlignResult = `Already aligned with ${alignResult.targetBranch}`;
|
|
35546
|
+
}
|
|
35547
|
+
for (const w of alignResult.warnings) {
|
|
35548
|
+
warnings.push(w);
|
|
35373
35549
|
}
|
|
35374
35550
|
} else {
|
|
35375
35551
|
gitAlignResult = "Not a git repository \u2014 skipped git alignment";
|
|
35376
35552
|
}
|
|
35377
35553
|
const closeSummaryPath = validateSwarmPath(directory, "close-summary.md");
|
|
35378
35554
|
const finalizationType = isForced ? "Forced closure" : planAlreadyDone ? "Plan already terminal \u2014 cleanup only" : "Normal finalization";
|
|
35379
|
-
const actionsPerformed = [
|
|
35380
|
-
...!planAlreadyDone && inProgressPhases.length > 0 ? ["- Wrote retrospectives for in-progress phases"] : [],
|
|
35381
|
-
`- ${archiveResult}`,
|
|
35382
|
-
...cleanedFiles.length > 0 ? [
|
|
35383
|
-
`- Cleaned ${cleanedFiles.length} active-state file(s): ${cleanedFiles.join(", ")}`
|
|
35384
|
-
] : [],
|
|
35385
|
-
"- Reset context.md for next session",
|
|
35386
|
-
...configBackupsRemoved > 0 ? [`- Removed ${configBackupsRemoved} stale config backup file(s)`] : [],
|
|
35387
|
-
...swarmPlanFilesRemoved > 0 ? [
|
|
35388
|
-
`- Removed ${swarmPlanFilesRemoved} root-level SWARM_PLAN checkpoint artifact(s)`
|
|
35389
|
-
] : [],
|
|
35390
|
-
...prunedBranches.length > 0 ? [
|
|
35391
|
-
`- Pruned ${prunedBranches.length} stale local git branch(es): ${prunedBranches.join(", ")}`
|
|
35392
|
-
] : [],
|
|
35393
|
-
"- Cleared agent sessions and delegation chains",
|
|
35394
|
-
...planExists && !planAlreadyDone ? ["- Set non-completed phases/tasks to closed status"] : [],
|
|
35395
|
-
...gitAlignResult ? [`- Git: ${gitAlignResult}`] : []
|
|
35396
|
-
];
|
|
35397
35555
|
const summaryContent = [
|
|
35398
35556
|
"# Swarm Close Summary",
|
|
35399
35557
|
"",
|
|
@@ -35401,16 +35559,34 @@ async function handleCloseCommand(directory, args) {
|
|
|
35401
35559
|
`**Closed:** ${new Date().toISOString()}`,
|
|
35402
35560
|
`**Finalization:** ${finalizationType}`,
|
|
35403
35561
|
"",
|
|
35404
|
-
|
|
35405
|
-
!planExists ? "_No plan \u2014 ad-hoc session_" : closedPhases.length > 0 ? closedPhases.map((id) => `- Phase ${id}`).join(`
|
|
35406
|
-
`) : "_No phases
|
|
35562
|
+
"## Retrospective",
|
|
35563
|
+
!planExists ? "_No plan \u2014 ad-hoc session_" : closedPhases.length > 0 ? closedPhases.map((id) => `- Phase ${id} closed`).join(`
|
|
35564
|
+
`) : "_No phases closed this run_",
|
|
35565
|
+
...closedTasks.length > 0 ? [
|
|
35566
|
+
"",
|
|
35567
|
+
`**Tasks marked closed:** ${closedTasks.length}`,
|
|
35568
|
+
...closedTasks.map((id) => `- ${id}`)
|
|
35569
|
+
] : [],
|
|
35570
|
+
"",
|
|
35571
|
+
"## Lessons Committed",
|
|
35572
|
+
allLessons.length > 0 ? `| # | Lesson |` : "_No lessons committed_",
|
|
35573
|
+
...allLessons.length > 0 ? ["| --- | --- |", ...allLessons.map((l, i) => `| ${i + 1} | ${l} |`)] : [],
|
|
35407
35574
|
"",
|
|
35408
|
-
|
|
35409
|
-
|
|
35410
|
-
|
|
35575
|
+
"## Local Repo State",
|
|
35576
|
+
...gitAlignResult ? [`- **Git:** ${gitAlignResult}`] : ["- Git alignment skipped"],
|
|
35577
|
+
...prunedBranches.length > 0 ? [`- **Pruned branches:** ${prunedBranches.join(", ")}`] : [],
|
|
35578
|
+
`- **Archive:** ${archiveResult}`,
|
|
35579
|
+
...cleanedFiles.length > 0 ? [`- **Cleaned:** ${cleanedFiles.length} file(s)`] : [],
|
|
35411
35580
|
"",
|
|
35412
|
-
"##
|
|
35413
|
-
|
|
35581
|
+
"## Context",
|
|
35582
|
+
"- Reset context.md for next session",
|
|
35583
|
+
"- Cleared agent sessions and delegation chains",
|
|
35584
|
+
...configBackupsRemoved > 0 ? [`- Removed ${configBackupsRemoved} stale config backup file(s)`] : [],
|
|
35585
|
+
...swarmPlanFilesRemoved > 0 ? [
|
|
35586
|
+
`- Removed ${swarmPlanFilesRemoved} root-level SWARM_PLAN checkpoint artifact(s)`
|
|
35587
|
+
] : [],
|
|
35588
|
+
...planExists && !planAlreadyDone ? ["- Set non-completed phases/tasks to closed status"] : [],
|
|
35589
|
+
...curationSucceeded && allLessons.length > 0 ? [`- Committed ${allLessons.length} lesson(s) to knowledge store`] : [],
|
|
35414
35590
|
"",
|
|
35415
35591
|
...warnings.length > 0 ? ["## Warnings", ...warnings.map((w) => `- ${w}`), ""] : []
|
|
35416
35592
|
].join(`
|
|
@@ -35438,9 +35614,6 @@ async function handleCloseCommand(directory, args) {
|
|
|
35438
35614
|
swarmState.fullAutoEnabledInConfig = preservedFullAutoFlag;
|
|
35439
35615
|
swarmState.curatorInitAgentNames = preservedCuratorInitNames;
|
|
35440
35616
|
swarmState.curatorPhaseAgentNames = preservedCuratorPhaseNames;
|
|
35441
|
-
if (pruneErrors.length > 0) {
|
|
35442
|
-
warnings.push(`Could not prune ${pruneErrors.length} branch(es) (unmerged or checked out): ${pruneErrors.join(", ")}`);
|
|
35443
|
-
}
|
|
35444
35617
|
const retroWarnings = warnings.filter((w) => w.includes("Retrospective write") || w.includes("retrospective write") || w.includes("Session retrospective"));
|
|
35445
35618
|
const otherWarnings = warnings.filter((w) => !w.includes("Retrospective write") && !w.includes("retrospective write") && !w.includes("Session retrospective"));
|
|
35446
35619
|
let warningMsg = "";
|
|
@@ -35458,16 +35631,19 @@ ${retroWarnings.map((w) => `- ${w}`).join(`
|
|
|
35458
35631
|
${otherWarnings.map((w) => `- ${w}`).join(`
|
|
35459
35632
|
`)}`;
|
|
35460
35633
|
}
|
|
35634
|
+
const lessonSummary = curationSucceeded && allLessons.length > 0 ? `
|
|
35635
|
+
|
|
35636
|
+
**Lessons Committed:** ${allLessons.length} lesson(s) committed to knowledge store` : "";
|
|
35461
35637
|
if (planAlreadyDone) {
|
|
35462
35638
|
return `\u2705 Session finalized. Plan was already in a terminal state \u2014 cleanup and archive applied.
|
|
35463
35639
|
|
|
35464
35640
|
**Archive:** ${archiveResult}
|
|
35465
|
-
**Git:** ${gitAlignResult}${warningMsg}`;
|
|
35641
|
+
**Git:** ${gitAlignResult}${lessonSummary}${warningMsg}`;
|
|
35466
35642
|
}
|
|
35467
35643
|
return `\u2705 Swarm finalized. ${closedPhases.length} phase(s) closed, ${closedTasks.length} incomplete task(s) marked closed.
|
|
35468
35644
|
|
|
35469
35645
|
**Archive:** ${archiveResult}
|
|
35470
|
-
**Git:** ${gitAlignResult}${warningMsg}`;
|
|
35646
|
+
**Git:** ${gitAlignResult}${lessonSummary}${warningMsg}`;
|
|
35471
35647
|
}
|
|
35472
35648
|
|
|
35473
35649
|
// src/commands/config.ts
|
|
@@ -39498,6 +39674,214 @@ async function handleHistoryCommand(directory, _args) {
|
|
|
39498
39674
|
const historyData = await getHistoryData(directory);
|
|
39499
39675
|
return formatHistoryMarkdown(historyData);
|
|
39500
39676
|
}
|
|
39677
|
+
// src/commands/issue.ts
|
|
39678
|
+
import { execSync as execSync2 } from "child_process";
|
|
39679
|
+
var MAX_URL_LEN = 2048;
|
|
39680
|
+
var USAGE2 = [
|
|
39681
|
+
"Usage: /swarm issue <url|owner/repo#N|N> [--plan] [--trace] [--no-repro]",
|
|
39682
|
+
"",
|
|
39683
|
+
"Ingest a GitHub issue into the swarm workflow.",
|
|
39684
|
+
" /swarm issue https://github.com/owner/repo/issues/42",
|
|
39685
|
+
" /swarm issue owner/repo#42",
|
|
39686
|
+
" /swarm issue 42 --plan",
|
|
39687
|
+
" /swarm issue 42 --trace --no-repro",
|
|
39688
|
+
"",
|
|
39689
|
+
"Flags:",
|
|
39690
|
+
" --plan Transition to plan creation after spec generation",
|
|
39691
|
+
" --trace Run full fix-and-PR workflow (implies --plan)",
|
|
39692
|
+
" --no-repro Skip reproduction step"
|
|
39693
|
+
].join(`
|
|
39694
|
+
`);
|
|
39695
|
+
function sanitizeUrl(raw) {
|
|
39696
|
+
let urlStr = raw.trim();
|
|
39697
|
+
urlStr = urlStr.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
|
|
39698
|
+
const fragmentIdx = urlStr.indexOf("#");
|
|
39699
|
+
if (fragmentIdx !== -1) {
|
|
39700
|
+
urlStr = urlStr.slice(0, fragmentIdx);
|
|
39701
|
+
}
|
|
39702
|
+
const queryIdx = urlStr.indexOf("?");
|
|
39703
|
+
if (queryIdx !== -1) {
|
|
39704
|
+
urlStr = urlStr.slice(0, queryIdx);
|
|
39705
|
+
}
|
|
39706
|
+
urlStr = urlStr.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^@/]+@/, "https://");
|
|
39707
|
+
if (urlStr.length > MAX_URL_LEN) {
|
|
39708
|
+
urlStr = urlStr.slice(0, MAX_URL_LEN);
|
|
39709
|
+
}
|
|
39710
|
+
return urlStr.trim();
|
|
39711
|
+
}
|
|
39712
|
+
function isPrivateHost(url3) {
|
|
39713
|
+
const host = url3.hostname.toLowerCase();
|
|
39714
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0") {
|
|
39715
|
+
return true;
|
|
39716
|
+
}
|
|
39717
|
+
if (host.startsWith("localhost") || host === "localhost.com") {
|
|
39718
|
+
return true;
|
|
39719
|
+
}
|
|
39720
|
+
const ipv4Private = /^10\./;
|
|
39721
|
+
const ipv4172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
|
|
39722
|
+
const ipv4192 = /^192\.168\./;
|
|
39723
|
+
const ipv6Private = /^fe80:/i;
|
|
39724
|
+
const ipv6Unique = /^f[cd][0-9a-f]{2}:/i;
|
|
39725
|
+
if (ipv4Private.test(host) || ipv4172.test(host) || ipv4192.test(host) || ipv6Private.test(host) || ipv6Unique.test(host)) {
|
|
39726
|
+
return true;
|
|
39727
|
+
}
|
|
39728
|
+
if (host.startsWith("::ffff:")) {
|
|
39729
|
+
const inner = host.slice(7);
|
|
39730
|
+
if (ipv4Private.test(inner) || ipv4172.test(inner) || ipv4192.test(inner)) {
|
|
39731
|
+
return true;
|
|
39732
|
+
}
|
|
39733
|
+
}
|
|
39734
|
+
return false;
|
|
39735
|
+
}
|
|
39736
|
+
function validateAndSanitizeUrl(rawUrl) {
|
|
39737
|
+
const sanitized = sanitizeUrl(rawUrl);
|
|
39738
|
+
if (!sanitized) {
|
|
39739
|
+
return { error: "Empty URL" };
|
|
39740
|
+
}
|
|
39741
|
+
if (!sanitized.startsWith("https://")) {
|
|
39742
|
+
return { error: "URL must use HTTPS scheme" };
|
|
39743
|
+
}
|
|
39744
|
+
try {
|
|
39745
|
+
const url3 = new URL(sanitized);
|
|
39746
|
+
const hostname5 = url3.hostname;
|
|
39747
|
+
if (/[\u0080-\u{10FFFF}]/u.test(hostname5)) {
|
|
39748
|
+
return { error: "Non-ASCII hostnames are not allowed" };
|
|
39749
|
+
}
|
|
39750
|
+
if (isPrivateHost(url3)) {
|
|
39751
|
+
return { error: "Private or localhost URLs are not allowed" };
|
|
39752
|
+
}
|
|
39753
|
+
const githubIssuePattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/([0-9]+)\/?$/;
|
|
39754
|
+
if (!githubIssuePattern.test(sanitized)) {
|
|
39755
|
+
return {
|
|
39756
|
+
error: "URL must be a GitHub issue URL (https://github.com/owner/repo/issues/N)"
|
|
39757
|
+
};
|
|
39758
|
+
}
|
|
39759
|
+
return { sanitized };
|
|
39760
|
+
} catch {
|
|
39761
|
+
return { error: "Invalid URL format" };
|
|
39762
|
+
}
|
|
39763
|
+
}
|
|
39764
|
+
function parseArgs2(args) {
|
|
39765
|
+
const out = {
|
|
39766
|
+
plan: false,
|
|
39767
|
+
trace: false,
|
|
39768
|
+
noRepro: false,
|
|
39769
|
+
rest: []
|
|
39770
|
+
};
|
|
39771
|
+
for (const token of args) {
|
|
39772
|
+
if (token === "--plan") {
|
|
39773
|
+
out.plan = true;
|
|
39774
|
+
continue;
|
|
39775
|
+
}
|
|
39776
|
+
if (token === "--trace") {
|
|
39777
|
+
out.trace = true;
|
|
39778
|
+
out.plan = true;
|
|
39779
|
+
continue;
|
|
39780
|
+
}
|
|
39781
|
+
if (token === "--no-repro") {
|
|
39782
|
+
out.noRepro = true;
|
|
39783
|
+
continue;
|
|
39784
|
+
}
|
|
39785
|
+
out.rest.push(token);
|
|
39786
|
+
}
|
|
39787
|
+
return out;
|
|
39788
|
+
}
|
|
39789
|
+
function parseIssueRef(input) {
|
|
39790
|
+
const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)\/?$/i);
|
|
39791
|
+
if (urlMatch) {
|
|
39792
|
+
return {
|
|
39793
|
+
owner: urlMatch[1],
|
|
39794
|
+
repo: urlMatch[2],
|
|
39795
|
+
number: parseInt(urlMatch[3], 10)
|
|
39796
|
+
};
|
|
39797
|
+
}
|
|
39798
|
+
const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
39799
|
+
if (shorthandMatch) {
|
|
39800
|
+
return {
|
|
39801
|
+
owner: shorthandMatch[1],
|
|
39802
|
+
repo: shorthandMatch[2],
|
|
39803
|
+
number: parseInt(shorthandMatch[3], 10)
|
|
39804
|
+
};
|
|
39805
|
+
}
|
|
39806
|
+
const bareMatch = input.match(/^(\d+)$/);
|
|
39807
|
+
if (bareMatch) {
|
|
39808
|
+
const issueNumber = parseInt(bareMatch[1], 10);
|
|
39809
|
+
const remoteUrl = detectGitRemote();
|
|
39810
|
+
if (!remoteUrl) {
|
|
39811
|
+
return null;
|
|
39812
|
+
}
|
|
39813
|
+
const parsed = parseGitRemoteUrl(remoteUrl);
|
|
39814
|
+
if (!parsed) {
|
|
39815
|
+
return null;
|
|
39816
|
+
}
|
|
39817
|
+
return {
|
|
39818
|
+
owner: parsed.owner,
|
|
39819
|
+
repo: parsed.repo,
|
|
39820
|
+
number: issueNumber
|
|
39821
|
+
};
|
|
39822
|
+
}
|
|
39823
|
+
return null;
|
|
39824
|
+
}
|
|
39825
|
+
function detectGitRemote() {
|
|
39826
|
+
try {
|
|
39827
|
+
const remoteUrl = execSync2("git remote get-url origin", {
|
|
39828
|
+
encoding: "utf-8",
|
|
39829
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
39830
|
+
timeout: 5000
|
|
39831
|
+
}).trim();
|
|
39832
|
+
return remoteUrl || null;
|
|
39833
|
+
} catch {
|
|
39834
|
+
return null;
|
|
39835
|
+
}
|
|
39836
|
+
}
|
|
39837
|
+
function parseGitRemoteUrl(remoteUrl) {
|
|
39838
|
+
const httpsMatch = remoteUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
|
|
39839
|
+
if (httpsMatch) {
|
|
39840
|
+
return {
|
|
39841
|
+
owner: httpsMatch[1],
|
|
39842
|
+
repo: httpsMatch[2].replace(/\.git$/, "")
|
|
39843
|
+
};
|
|
39844
|
+
}
|
|
39845
|
+
const sshMatch = remoteUrl.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
|
|
39846
|
+
if (sshMatch) {
|
|
39847
|
+
return {
|
|
39848
|
+
owner: sshMatch[1],
|
|
39849
|
+
repo: sshMatch[2].replace(/\.git$/, "")
|
|
39850
|
+
};
|
|
39851
|
+
}
|
|
39852
|
+
return null;
|
|
39853
|
+
}
|
|
39854
|
+
function handleIssueCommand(_directory, args) {
|
|
39855
|
+
const parsed = parseArgs2(args);
|
|
39856
|
+
const rawInput = parsed.rest.join(" ").trim();
|
|
39857
|
+
if (!rawInput) {
|
|
39858
|
+
return USAGE2;
|
|
39859
|
+
}
|
|
39860
|
+
const isFullUrl = /^https?:\/\//i.test(rawInput);
|
|
39861
|
+
const issueInfo = parseIssueRef(isFullUrl ? sanitizeUrl(rawInput) : rawInput);
|
|
39862
|
+
if (!issueInfo) {
|
|
39863
|
+
return `Error: Could not parse issue reference from "${rawInput}"
|
|
39864
|
+
|
|
39865
|
+
${USAGE2}`;
|
|
39866
|
+
}
|
|
39867
|
+
const issueUrl = `https://github.com/${issueInfo.owner}/${issueInfo.repo}/issues/${issueInfo.number}`;
|
|
39868
|
+
const result = validateAndSanitizeUrl(issueUrl);
|
|
39869
|
+
if ("error" in result) {
|
|
39870
|
+
return `Error: ${result.error}
|
|
39871
|
+
|
|
39872
|
+
${USAGE2}`;
|
|
39873
|
+
}
|
|
39874
|
+
const flags = [];
|
|
39875
|
+
if (parsed.plan)
|
|
39876
|
+
flags.push("plan=true");
|
|
39877
|
+
if (parsed.trace)
|
|
39878
|
+
flags.push("trace=true");
|
|
39879
|
+
if (parsed.noRepro)
|
|
39880
|
+
flags.push("noRepro=true");
|
|
39881
|
+
const flagsStr = flags.length > 0 ? ` ${flags.join(" ")}` : "";
|
|
39882
|
+
return `[MODE: ISSUE_INGEST issue="${result.sanitized}"${flagsStr}]`;
|
|
39883
|
+
}
|
|
39884
|
+
|
|
39501
39885
|
// src/commands/knowledge.ts
|
|
39502
39886
|
import { join as join21 } from "path";
|
|
39503
39887
|
|
|
@@ -39990,6 +40374,190 @@ async function handlePlanCommand(directory, args) {
|
|
|
39990
40374
|
const planData = await getPlanData(directory, phaseArg);
|
|
39991
40375
|
return formatPlanMarkdown(planData);
|
|
39992
40376
|
}
|
|
40377
|
+
// src/commands/pr-review.ts
|
|
40378
|
+
import { execSync as execSync3 } from "child_process";
|
|
40379
|
+
var MAX_URL_LEN2 = 2048;
|
|
40380
|
+
var USAGE3 = [
|
|
40381
|
+
"Usage: /swarm pr-review <url|owner/repo#N|N> [--council]",
|
|
40382
|
+
"",
|
|
40383
|
+
"Run a full swarm PR review on a GitHub pull request.",
|
|
40384
|
+
" /swarm pr-review https://github.com/owner/repo/pull/42",
|
|
40385
|
+
" /swarm pr-review owner/repo#42",
|
|
40386
|
+
" /swarm pr-review 42 --council",
|
|
40387
|
+
"",
|
|
40388
|
+
"Flags:",
|
|
40389
|
+
" --council Run adversarial council variant (all lanes assume work is wrong)"
|
|
40390
|
+
].join(`
|
|
40391
|
+
`);
|
|
40392
|
+
function sanitizeUrl2(raw) {
|
|
40393
|
+
let urlStr = raw.trim();
|
|
40394
|
+
urlStr = urlStr.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
|
|
40395
|
+
const fragmentIdx = urlStr.indexOf("#");
|
|
40396
|
+
if (fragmentIdx !== -1) {
|
|
40397
|
+
urlStr = urlStr.slice(0, fragmentIdx);
|
|
40398
|
+
}
|
|
40399
|
+
const queryIdx = urlStr.indexOf("?");
|
|
40400
|
+
if (queryIdx !== -1) {
|
|
40401
|
+
urlStr = urlStr.slice(0, queryIdx);
|
|
40402
|
+
}
|
|
40403
|
+
urlStr = urlStr.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^@/]+@/, "https://");
|
|
40404
|
+
if (urlStr.length > MAX_URL_LEN2) {
|
|
40405
|
+
urlStr = urlStr.slice(0, MAX_URL_LEN2);
|
|
40406
|
+
}
|
|
40407
|
+
return urlStr.trim();
|
|
40408
|
+
}
|
|
40409
|
+
function isPrivateHost2(url3) {
|
|
40410
|
+
const host = url3.hostname.toLowerCase();
|
|
40411
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0") {
|
|
40412
|
+
return true;
|
|
40413
|
+
}
|
|
40414
|
+
if (host.startsWith("localhost") || host === "localhost.com") {
|
|
40415
|
+
return true;
|
|
40416
|
+
}
|
|
40417
|
+
const ipv4Private = /^10\./;
|
|
40418
|
+
const ipv4172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
|
|
40419
|
+
const ipv4192 = /^192\.168\./;
|
|
40420
|
+
const ipv6Private = /^fe80:/i;
|
|
40421
|
+
const ipv6Unique = /^f[cd][0-9a-f]{2}:/i;
|
|
40422
|
+
if (ipv4Private.test(host) || ipv4172.test(host) || ipv4192.test(host) || ipv6Private.test(host) || ipv6Unique.test(host)) {
|
|
40423
|
+
return true;
|
|
40424
|
+
}
|
|
40425
|
+
if (host.startsWith("::ffff:")) {
|
|
40426
|
+
const inner = host.slice(7);
|
|
40427
|
+
if (ipv4Private.test(inner) || ipv4172.test(inner) || ipv4192.test(inner)) {
|
|
40428
|
+
return true;
|
|
40429
|
+
}
|
|
40430
|
+
}
|
|
40431
|
+
return false;
|
|
40432
|
+
}
|
|
40433
|
+
function validateAndSanitizeUrl2(rawUrl) {
|
|
40434
|
+
const sanitized = sanitizeUrl2(rawUrl);
|
|
40435
|
+
if (!sanitized) {
|
|
40436
|
+
return { error: "Empty URL" };
|
|
40437
|
+
}
|
|
40438
|
+
if (!sanitized.startsWith("https://")) {
|
|
40439
|
+
return { error: "URL must use HTTPS scheme" };
|
|
40440
|
+
}
|
|
40441
|
+
try {
|
|
40442
|
+
const url3 = new URL(sanitized);
|
|
40443
|
+
const hostname5 = url3.hostname;
|
|
40444
|
+
if (/[\u0080-\u{10FFFF}]/u.test(hostname5)) {
|
|
40445
|
+
return { error: "Non-ASCII hostnames are not allowed" };
|
|
40446
|
+
}
|
|
40447
|
+
if (isPrivateHost2(url3)) {
|
|
40448
|
+
return { error: "Private or localhost URLs are not allowed" };
|
|
40449
|
+
}
|
|
40450
|
+
const githubPrPattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/([0-9]+)\/?$/;
|
|
40451
|
+
if (!githubPrPattern.test(sanitized)) {
|
|
40452
|
+
return {
|
|
40453
|
+
error: "URL must be a GitHub pull request URL (https://github.com/owner/repo/pull/N)"
|
|
40454
|
+
};
|
|
40455
|
+
}
|
|
40456
|
+
return { sanitized };
|
|
40457
|
+
} catch {
|
|
40458
|
+
return { error: "Invalid URL format" };
|
|
40459
|
+
}
|
|
40460
|
+
}
|
|
40461
|
+
function parseArgs3(args) {
|
|
40462
|
+
const out = { council: false, rest: [] };
|
|
40463
|
+
for (const token of args) {
|
|
40464
|
+
if (token === "--council") {
|
|
40465
|
+
out.council = true;
|
|
40466
|
+
continue;
|
|
40467
|
+
}
|
|
40468
|
+
out.rest.push(token);
|
|
40469
|
+
}
|
|
40470
|
+
return out;
|
|
40471
|
+
}
|
|
40472
|
+
function parsePrRef(input) {
|
|
40473
|
+
const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i);
|
|
40474
|
+
if (urlMatch) {
|
|
40475
|
+
return {
|
|
40476
|
+
owner: urlMatch[1],
|
|
40477
|
+
repo: urlMatch[2],
|
|
40478
|
+
number: parseInt(urlMatch[3], 10)
|
|
40479
|
+
};
|
|
40480
|
+
}
|
|
40481
|
+
const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
40482
|
+
if (shorthandMatch) {
|
|
40483
|
+
return {
|
|
40484
|
+
owner: shorthandMatch[1],
|
|
40485
|
+
repo: shorthandMatch[2],
|
|
40486
|
+
number: parseInt(shorthandMatch[3], 10)
|
|
40487
|
+
};
|
|
40488
|
+
}
|
|
40489
|
+
const bareMatch = input.match(/^(\d+)$/);
|
|
40490
|
+
if (bareMatch) {
|
|
40491
|
+
const prNumber = parseInt(bareMatch[1], 10);
|
|
40492
|
+
const remoteUrl = detectGitRemote2();
|
|
40493
|
+
if (!remoteUrl) {
|
|
40494
|
+
return null;
|
|
40495
|
+
}
|
|
40496
|
+
const parsed = parseGitRemoteUrl2(remoteUrl);
|
|
40497
|
+
if (!parsed) {
|
|
40498
|
+
return null;
|
|
40499
|
+
}
|
|
40500
|
+
return {
|
|
40501
|
+
owner: parsed.owner,
|
|
40502
|
+
repo: parsed.repo,
|
|
40503
|
+
number: prNumber
|
|
40504
|
+
};
|
|
40505
|
+
}
|
|
40506
|
+
return null;
|
|
40507
|
+
}
|
|
40508
|
+
function detectGitRemote2() {
|
|
40509
|
+
try {
|
|
40510
|
+
const remoteUrl = execSync3("git remote get-url origin", {
|
|
40511
|
+
encoding: "utf-8",
|
|
40512
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
40513
|
+
timeout: 5000
|
|
40514
|
+
}).trim();
|
|
40515
|
+
return remoteUrl || null;
|
|
40516
|
+
} catch {
|
|
40517
|
+
return null;
|
|
40518
|
+
}
|
|
40519
|
+
}
|
|
40520
|
+
function parseGitRemoteUrl2(remoteUrl) {
|
|
40521
|
+
const httpsMatch = remoteUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
|
|
40522
|
+
if (httpsMatch) {
|
|
40523
|
+
return {
|
|
40524
|
+
owner: httpsMatch[1],
|
|
40525
|
+
repo: httpsMatch[2].replace(/\.git$/, "")
|
|
40526
|
+
};
|
|
40527
|
+
}
|
|
40528
|
+
const sshMatch = remoteUrl.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
|
|
40529
|
+
if (sshMatch) {
|
|
40530
|
+
return {
|
|
40531
|
+
owner: sshMatch[1],
|
|
40532
|
+
repo: sshMatch[2].replace(/\.git$/, "")
|
|
40533
|
+
};
|
|
40534
|
+
}
|
|
40535
|
+
return null;
|
|
40536
|
+
}
|
|
40537
|
+
function handlePrReviewCommand(_directory, args) {
|
|
40538
|
+
const parsed = parseArgs3(args);
|
|
40539
|
+
const rawInput = parsed.rest.join(" ").trim();
|
|
40540
|
+
if (!rawInput) {
|
|
40541
|
+
return USAGE3;
|
|
40542
|
+
}
|
|
40543
|
+
const isFullUrl = /^https?:\/\//i.test(rawInput);
|
|
40544
|
+
const prInfo = parsePrRef(isFullUrl ? sanitizeUrl2(rawInput) : rawInput);
|
|
40545
|
+
if (!prInfo) {
|
|
40546
|
+
return `Error: Could not parse PR reference from "${rawInput}"
|
|
40547
|
+
|
|
40548
|
+
${USAGE3}`;
|
|
40549
|
+
}
|
|
40550
|
+
const prUrl = `https://github.com/${prInfo.owner}/${prInfo.repo}/pull/${prInfo.number}`;
|
|
40551
|
+
const result = validateAndSanitizeUrl2(prUrl);
|
|
40552
|
+
if ("error" in result) {
|
|
40553
|
+
return `Error: ${result.error}
|
|
40554
|
+
|
|
40555
|
+
${USAGE3}`;
|
|
40556
|
+
}
|
|
40557
|
+
const councilFlag = parsed.council ? "council=true" : "council=false";
|
|
40558
|
+
return `[MODE: PR_REVIEW pr="${result.sanitized}" ${councilFlag}]`;
|
|
40559
|
+
}
|
|
40560
|
+
|
|
39993
40561
|
// src/services/preflight-service.ts
|
|
39994
40562
|
init_manager2();
|
|
39995
40563
|
init_manager();
|
|
@@ -43767,7 +44335,8 @@ var ALL_GATE_NAMES = [
|
|
|
43767
44335
|
"hallucination_guard",
|
|
43768
44336
|
"sast_enabled",
|
|
43769
44337
|
"mutation_test",
|
|
43770
|
-
"council_general_review"
|
|
44338
|
+
"council_general_review",
|
|
44339
|
+
"drift_check"
|
|
43771
44340
|
];
|
|
43772
44341
|
function derivePlanId(plan) {
|
|
43773
44342
|
return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
@@ -45339,11 +45908,23 @@ var COMMAND_REGISTRY = {
|
|
|
45339
45908
|
args: "<question> [--preset <name>] [--spec-review]",
|
|
45340
45909
|
details: "Triggers the architect to convene a configurable General Council: each member independently web-searches, answers, and engages in one structured deliberation round on disagreements; an optional moderator pass synthesizes the final answer. --preset <name> selects a member group from council.general.presets. --spec-review switches to single-pass advisory mode for spec review. Requires council.general.enabled: true and a search API key in opencode-swarm.json."
|
|
45341
45910
|
},
|
|
45911
|
+
"pr-review": {
|
|
45912
|
+
handler: async (ctx) => handlePrReviewCommand(ctx.directory, ctx.args),
|
|
45913
|
+
description: "Launch deep PR review with multi-lane analysis [url] [--council]",
|
|
45914
|
+
args: "<pr-url|owner/repo#N|N> [--council]",
|
|
45915
|
+
details: "Launches a structured PR review: reconstructs PR intent via obligation extraction cascade, runs 6 parallel explorer lanes (correctness, security, dependencies, docs-intent-vs-actual, tests, performance-architecture), validates findings through independent reviewer confirmation, applies critic challenge to HIGH/CRITICAL findings, synthesizes structured report. --council variant fires adversarial multi-model review. Supports full GitHub URL, owner/repo#N shorthand, or bare PR number (resolves against origin remote)."
|
|
45916
|
+
},
|
|
45917
|
+
issue: {
|
|
45918
|
+
handler: async (ctx) => handleIssueCommand(ctx.directory, ctx.args),
|
|
45919
|
+
description: "Ingest a GitHub issue into the swarm workflow [url] [--plan] [--trace] [--no-repro]",
|
|
45920
|
+
args: "<issue-url|owner/repo#N|N> [--plan] [--trace] [--no-repro]",
|
|
45921
|
+
details: "Triggers the architect to enter MODE: ISSUE_INGEST \u2014 ingests a GitHub issue, restructures it into a normalized intake note, localizes root cause through hypothesis-driven tracing, and outputs a resolution spec. --plan transitions to plan creation after spec generation. --trace runs the full fix-and-PR workflow (implies --plan). --no-repro skips the reproduction step. Supports full GitHub URL, owner/repo#N shorthand, or bare issue number (resolves against origin remote)."
|
|
45922
|
+
},
|
|
45342
45923
|
"qa-gates": {
|
|
45343
45924
|
handler: (ctx) => handleQaGatesCommand(ctx.directory, ctx.args, ctx.sessionID),
|
|
45344
45925
|
description: "View or modify QA gate profile for the current plan [enable|override <gate>...]",
|
|
45345
45926
|
args: "[show|enable|override] <gate>...",
|
|
45346
|
-
details: "show: display spec-level, session-override, and effective QA gates for the current plan. enable: persist gate(s) into the locked-once profile (architect; rejected after critic approval lock). override: session-only ratchet-tighter enable. Valid gates: reviewer, test_engineer, council_mode, sme_enabled, critic_pre_plan, hallucination_guard, sast_enabled, mutation_test, council_general_review."
|
|
45927
|
+
details: "show: display spec-level, session-override, and effective QA gates for the current plan. enable: persist gate(s) into the locked-once profile (architect; rejected after critic approval lock). override: session-only ratchet-tighter enable. Valid gates: reviewer, test_engineer, council_mode, sme_enabled, critic_pre_plan, hallucination_guard, sast_enabled, mutation_test, council_general_review, drift_check."
|
|
45347
45928
|
},
|
|
45348
45929
|
promote: {
|
|
45349
45930
|
handler: (ctx) => handlePromoteCommand(ctx.directory, ctx.args),
|