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/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: "6.86.14",
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 getDefaultBaseBranch(cwd) {
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 "origin/main";
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(["rev-parse", "--verify", "origin/master"], cwd);
33865
- return "origin/master";
33866
- } catch {
33867
- return "origin/main";
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(explicitLessons, projectName, { phase_number: 0 }, directory, config3);
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 && explicitLessons.length > 0) {
35373
+ if (curationSucceeded && allLessons.length > 0) {
35129
35374
  await fs7.unlink(lessonsFilePath).catch(() => {});
35130
35375
  }
35131
- if (planExists && !planAlreadyDone) {
35132
- for (const phase of phases) {
35133
- if (phase.status !== "complete" && phase.status !== "completed") {
35134
- phase.status = "closed";
35135
- if (!closedPhases.includes(phase.id)) {
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
- for (const task of phase.tasks ?? []) {
35140
- if (task.status !== "completed" && task.status !== "complete") {
35141
- task.status = "closed";
35142
- if (!closedTasks.includes(task.id)) {
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
- try {
35149
- await fs7.writeFile(planPath, JSON.stringify(planData, null, 2), "utf-8");
35150
- } catch (error93) {
35151
- const msg = error93 instanceof Error ? error93.message : String(error93);
35152
- warnings.push(`Failed to persist terminal plan.json state: ${msg}`);
35153
- console.warn("[close-command] Failed to write plan.json:", error93);
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
- try {
35298
- const currentBranch = getCurrentBranch(directory);
35299
- if (currentBranch === "HEAD") {
35300
- gitAlignResult = "Skipped git alignment: detached HEAD state";
35301
- warnings.push("Repo is in detached HEAD state. Checkout a branch before starting a new swarm.");
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 (pruneBranches) {
35352
- try {
35353
- const branchOutput = execFileSync("git", ["branch", "-vv"], {
35354
- cwd: directory,
35355
- encoding: "utf-8",
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
- `## Phases Closed: ${closedPhases.length}`,
35405
- !planExists ? "_No plan \u2014 ad-hoc session_" : closedPhases.length > 0 ? closedPhases.map((id) => `- Phase ${id}`).join(`
35406
- `) : "_No phases to close_",
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
- `## Tasks Closed: ${closedTasks.length}`,
35409
- closedTasks.length > 0 ? closedTasks.map((id) => `- ${id}`).join(`
35410
- `) : "_No incomplete tasks_",
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
- "## Actions Performed",
35413
- ...actionsPerformed,
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),