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/index.js CHANGED
@@ -33,7 +33,7 @@ var package_default;
33
33
  var init_package = __esm(() => {
34
34
  package_default = {
35
35
  name: "opencode-swarm",
36
- version: "6.86.14",
36
+ version: "7.0.0",
37
37
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
38
38
  main: "dist/index.js",
39
39
  types: "dist/index.d.ts",
@@ -540,8 +540,8 @@ var init_constants = __esm(() => {
540
540
  lint_spec: "validate .swarm/spec.md format and required fields",
541
541
  get_approved_plan: "retrieve the last critic-approved immutable plan snapshot for baseline drift comparison",
542
542
  repo_map: "query the repo code graph: importers, dependencies, blast radius, and localization context for structural awareness before refactoring",
543
- get_qa_gate_profile: "retrieve the QA gate profile for the current plan: gates (reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test, council_general_review), lock state, and profile hash. Read-only.",
544
- set_qa_gates: "configure the QA gate profile for the current plan. Architect-only. Ratchet-tighter only — rejected once the profile is locked after critic approval. Supports: reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test, council_general_review.",
543
+ get_qa_gate_profile: "retrieve the QA gate profile for the current plan: gates (reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test, council_general_review, drift_check), lock state, and profile hash. Read-only.",
544
+ set_qa_gates: "configure the QA gate profile for the current plan. Architect-only. Ratchet-tighter only — rejected once the profile is locked after critic approval. Supports: reviewer, test_engineer, sme_enabled, critic_pre_plan, sast_enabled, council_mode, hallucination_guard, mutation_test, council_general_review, drift_check.",
545
545
  req_coverage: "query requirement coverage status for tracked functional requirements"
546
546
  };
547
547
  for (const [agentName, tools] of Object.entries(AGENT_TOOL_MAP)) {
@@ -15330,12 +15330,15 @@ var init_schema = __esm(() => {
15330
15330
  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."),
15331
15331
  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)."),
15332
15332
  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."),
15333
+ phaseConcernsAllowComplete: exports_external.boolean().default(true).describe("When true, a phase-level council CONCERNS verdict does NOT block phase completion — the advisory notes are logged as warnings and the phase proceeds. When false, CONCERNS blocks like REJECT. Default: true (CONCERNS is advisory)."),
15333
15334
  general: GeneralCouncilConfigSchema.optional()
15334
15335
  }).strict();
15335
15336
  ParallelizationConfigSchema = exports_external.object({
15336
15337
  enabled: exports_external.boolean().default(false),
15337
15338
  maxConcurrentTasks: exports_external.number().int().min(1).max(64).default(1),
15338
15339
  evidenceLockTimeoutMs: exports_external.number().int().min(1000).max(300000).default(60000),
15340
+ max_coders: exports_external.number().int().min(1).max(16).default(3),
15341
+ max_reviewers: exports_external.number().int().min(1).max(16).default(2),
15339
15342
  stageB: exports_external.object({
15340
15343
  parallel: exports_external.object({
15341
15344
  enabled: exports_external.boolean().default(false)
@@ -15594,6 +15597,7 @@ var init_plan_schema = __esm(() => {
15594
15597
  name: exports_external.string().min(1),
15595
15598
  status: PhaseStatusSchema.default("pending"),
15596
15599
  tasks: exports_external.array(TaskSchema).default([]),
15600
+ type: exports_external.enum(["code", "non-code"]).optional(),
15597
15601
  required_agents: exports_external.array(exports_external.string()).optional()
15598
15602
  });
15599
15603
  PlanSchema = exports_external.object({
@@ -19928,7 +19932,8 @@ var init_qa_gate_profile = __esm(() => {
19928
19932
  hallucination_guard: false,
19929
19933
  sast_enabled: true,
19930
19934
  mutation_test: false,
19931
- council_general_review: false
19935
+ council_general_review: false,
19936
+ drift_check: true
19932
19937
  };
19933
19938
  });
19934
19939
 
@@ -39918,23 +39923,220 @@ function getCurrentBranch(cwd) {
39918
39923
  const output = gitExec2(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
39919
39924
  return output.trim();
39920
39925
  }
39921
- function getDefaultBaseBranch(cwd) {
39926
+ function hasUncommittedChanges(cwd) {
39927
+ const status = gitExec2(["status", "--porcelain"], cwd);
39928
+ return status.trim().length > 0;
39929
+ }
39930
+ function detectDefaultRemoteBranch(cwd) {
39931
+ try {
39932
+ const output = gitExec2(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd);
39933
+ const trimmed = output.trim();
39934
+ if (trimmed.startsWith("refs/remotes/origin/")) {
39935
+ return trimmed.slice("refs/remotes/origin/".length);
39936
+ }
39937
+ } catch {}
39938
+ try {
39939
+ const output = gitExec2(["config", "init.defaultBranch"], cwd);
39940
+ const branch = output.trim();
39941
+ if (branch) {
39942
+ return branch;
39943
+ }
39944
+ } catch {}
39922
39945
  try {
39923
39946
  gitExec2(["rev-parse", "--verify", "origin/main"], cwd);
39924
- return "origin/main";
39947
+ return "main";
39948
+ } catch {}
39949
+ try {
39950
+ gitExec2(["rev-parse", "--verify", "origin/master"], cwd);
39951
+ return "master";
39925
39952
  } catch {
39953
+ return null;
39954
+ }
39955
+ }
39956
+ function resetToRemoteBranch(cwd, options) {
39957
+ const warnings = [];
39958
+ const prunedBranches = [];
39959
+ try {
39960
+ const currentBranch = getCurrentBranch(cwd);
39961
+ const defaultRemoteBranch = detectDefaultRemoteBranch(cwd);
39962
+ if (!defaultRemoteBranch) {
39963
+ return {
39964
+ success: false,
39965
+ targetBranch: "",
39966
+ localBranch: currentBranch,
39967
+ message: "Could not detect default remote branch",
39968
+ alreadyAligned: false,
39969
+ prunedBranches: [],
39970
+ warnings: []
39971
+ };
39972
+ }
39973
+ const targetBranch = `origin/${defaultRemoteBranch}`;
39974
+ if (currentBranch === "HEAD") {
39975
+ return {
39976
+ success: false,
39977
+ targetBranch,
39978
+ localBranch: "HEAD",
39979
+ message: "Cannot reset: detached HEAD state",
39980
+ alreadyAligned: false,
39981
+ prunedBranches: [],
39982
+ warnings: []
39983
+ };
39984
+ }
39985
+ if (hasUncommittedChanges(cwd)) {
39986
+ return {
39987
+ success: false,
39988
+ targetBranch,
39989
+ localBranch: currentBranch,
39990
+ message: "Cannot reset: uncommitted changes in working tree",
39991
+ alreadyAligned: false,
39992
+ prunedBranches: [],
39993
+ warnings: []
39994
+ };
39995
+ }
39926
39996
  try {
39927
- gitExec2(["rev-parse", "--verify", "origin/master"], cwd);
39928
- return "origin/master";
39929
- } catch {
39930
- return "origin/main";
39997
+ const logOutput = gitExec2(["log", `${targetBranch}..HEAD`, "--oneline"], cwd);
39998
+ if (logOutput.trim().length > 0) {
39999
+ return {
40000
+ success: false,
40001
+ targetBranch,
40002
+ localBranch: currentBranch,
40003
+ message: "Cannot reset: unpushed commits",
40004
+ alreadyAligned: false,
40005
+ prunedBranches: [],
40006
+ warnings: []
40007
+ };
40008
+ }
40009
+ } catch {}
40010
+ try {
40011
+ gitExec2(["fetch", "--prune", "origin"], cwd);
40012
+ } catch (err2) {
40013
+ return {
40014
+ success: false,
40015
+ targetBranch,
40016
+ localBranch: currentBranch,
40017
+ message: `Fetch failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
40018
+ alreadyAligned: false,
40019
+ prunedBranches: [],
40020
+ warnings: []
40021
+ };
39931
40022
  }
40023
+ const headSha = gitExec2(["rev-parse", "HEAD"], cwd).trim();
40024
+ const remoteSha = gitExec2(["rev-parse", `${targetBranch}`], cwd).trim();
40025
+ if (headSha === remoteSha) {
40026
+ return {
40027
+ success: true,
40028
+ targetBranch,
40029
+ localBranch: currentBranch,
40030
+ message: "Already aligned with remote",
40031
+ alreadyAligned: true,
40032
+ prunedBranches: [],
40033
+ warnings: []
40034
+ };
40035
+ }
40036
+ try {
40037
+ gitExec2(["checkout", currentBranch], cwd);
40038
+ } catch (err2) {
40039
+ return {
40040
+ success: false,
40041
+ targetBranch,
40042
+ localBranch: currentBranch,
40043
+ message: `Checkout failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
40044
+ alreadyAligned: false,
40045
+ prunedBranches: [],
40046
+ warnings: []
40047
+ };
40048
+ }
40049
+ let resetSucceeded = false;
40050
+ let lastError;
40051
+ for (let retry = 0;retry < 4; retry++) {
40052
+ if (retry > 0 && process.platform === "win32") {
40053
+ const endTime = Date.now() + 500;
40054
+ while (Date.now() < endTime) {}
40055
+ }
40056
+ try {
40057
+ gitExec2(["reset", "--hard", targetBranch], cwd);
40058
+ resetSucceeded = true;
40059
+ break;
40060
+ } catch (err2) {
40061
+ lastError = err2;
40062
+ }
40063
+ }
40064
+ if (!resetSucceeded) {
40065
+ return {
40066
+ success: false,
40067
+ targetBranch,
40068
+ localBranch: currentBranch,
40069
+ message: `Reset failed: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
40070
+ alreadyAligned: false,
40071
+ prunedBranches: [],
40072
+ warnings: []
40073
+ };
40074
+ }
40075
+ if (options?.pruneBranches) {
40076
+ try {
40077
+ const mergedOutput = gitExec2(["branch", "--merged", targetBranch], cwd);
40078
+ const mergedLines = mergedOutput.split(`
40079
+ `);
40080
+ for (const line of mergedLines) {
40081
+ const trimmedLine = line.trim();
40082
+ if (!trimmedLine || trimmedLine.startsWith("*")) {
40083
+ continue;
40084
+ }
40085
+ try {
40086
+ gitExec2(["branch", "-d", trimmedLine], cwd);
40087
+ prunedBranches.push(trimmedLine);
40088
+ } catch {
40089
+ warnings.push(`Could not safely delete branch: ${trimmedLine}`);
40090
+ }
40091
+ }
40092
+ } catch (err2) {
40093
+ warnings.push(`Failed to get merged branches: ${err2 instanceof Error ? err2.message : String(err2)}`);
40094
+ }
40095
+ try {
40096
+ const branchVvOutput = gitExec2(["branch", "-vv"], cwd);
40097
+ const vvLines = branchVvOutput.split(`
40098
+ `);
40099
+ for (const line of vvLines) {
40100
+ const trimmedLine = line.trim();
40101
+ if (!trimmedLine || trimmedLine.startsWith("*")) {
40102
+ continue;
40103
+ }
40104
+ if (trimmedLine.includes(": gone]")) {
40105
+ const parts2 = trimmedLine.split(/\s+/);
40106
+ const branchName = parts2[0];
40107
+ try {
40108
+ gitExec2(["branch", "-d", branchName], cwd);
40109
+ prunedBranches.push(branchName);
40110
+ } catch {
40111
+ warnings.push(`Could not delete gone branch: ${branchName}`);
40112
+ }
40113
+ }
40114
+ }
40115
+ } catch (err2) {
40116
+ warnings.push(`Failed to prune gone branches: ${err2 instanceof Error ? err2.message : String(err2)}`);
40117
+ }
40118
+ }
40119
+ return {
40120
+ success: true,
40121
+ targetBranch,
40122
+ localBranch: currentBranch,
40123
+ message: "Successfully reset to remote branch",
40124
+ alreadyAligned: false,
40125
+ prunedBranches,
40126
+ warnings
40127
+ };
40128
+ } catch (err2) {
40129
+ return {
40130
+ success: false,
40131
+ targetBranch: "",
40132
+ localBranch: "",
40133
+ message: `Unexpected error: ${err2 instanceof Error ? err2.message : String(err2)}`,
40134
+ alreadyAligned: false,
40135
+ prunedBranches: [],
40136
+ warnings: []
40137
+ };
39932
40138
  }
39933
40139
  }
39934
- function hasUncommittedChanges(cwd) {
39935
- const status = gitExec2(["status", "--porcelain"], cwd);
39936
- return status.trim().length > 0;
39937
- }
39938
40140
  var GIT_TIMEOUT_MS2 = 30000;
39939
40141
  var init_branch = __esm(() => {
39940
40142
  init_logger();
@@ -41588,9 +41790,28 @@ var init_write_retro = __esm(() => {
41588
41790
  });
41589
41791
 
41590
41792
  // src/commands/close.ts
41591
- import { execFileSync } from "node:child_process";
41592
41793
  import { promises as fs12 } from "node:fs";
41593
41794
  import path18 from "node:path";
41795
+ function guaranteeAllPlansComplete(planData) {
41796
+ const closedPhaseIds = [];
41797
+ const closedTaskIds = [];
41798
+ for (const phase of planData.phases ?? []) {
41799
+ const wasComplete = phase.status === "complete" || phase.status === "completed" || phase.status === "closed";
41800
+ if (!wasComplete) {
41801
+ phase.status = "closed";
41802
+ closedPhaseIds.push(phase.id);
41803
+ }
41804
+ for (const task of phase.tasks ?? []) {
41805
+ const wasTaskDone = task.status === "completed" || task.status === "complete" || task.status === "closed";
41806
+ if (!wasTaskDone) {
41807
+ task.status = "closed";
41808
+ task.close_reason = "session_terminated";
41809
+ closedTaskIds.push(task.id);
41810
+ }
41811
+ }
41812
+ }
41813
+ return { closedPhaseIds, closedTaskIds };
41814
+ }
41594
41815
  async function handleCloseCommand(directory, args2) {
41595
41816
  const planPath = validateSwarmPath(directory, "plan.json");
41596
41817
  const swarmDir = path18.join(directory, ".swarm");
@@ -41708,41 +41929,62 @@ async function handleCloseCommand(directory, args2) {
41708
41929
  explicitLessons = lessonsText.split(`
41709
41930
  `).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
41710
41931
  } catch {}
41932
+ const retroLessons = [];
41933
+ try {
41934
+ const evidenceDir = path18.join(swarmDir, "evidence");
41935
+ const evidenceEntries = await fs12.readdir(evidenceDir);
41936
+ const retroDirs = evidenceEntries.filter((e) => e.startsWith("retro-"));
41937
+ for (const retroDir of retroDirs) {
41938
+ const evidencePath = path18.join(evidenceDir, retroDir, "evidence.json");
41939
+ try {
41940
+ const content = await fs12.readFile(evidencePath, "utf-8");
41941
+ const parsed = JSON.parse(content);
41942
+ const entries = parsed.entries ?? [parsed];
41943
+ for (const entry of entries) {
41944
+ if (Array.isArray(entry.lessons_learned)) {
41945
+ for (const lesson of entry.lessons_learned) {
41946
+ if (typeof lesson === "string" && lesson.trim().length > 0) {
41947
+ retroLessons.push(lesson.trim());
41948
+ }
41949
+ }
41950
+ }
41951
+ }
41952
+ } catch {}
41953
+ }
41954
+ } catch {}
41955
+ const allLessons = [...new Set([...explicitLessons, ...retroLessons])];
41711
41956
  let curationSucceeded = false;
41712
41957
  try {
41713
- await curateAndStoreSwarm(explicitLessons, projectName, { phase_number: 0 }, directory, config3);
41958
+ await curateAndStoreSwarm(allLessons, projectName, { phase_number: 0 }, directory, config3);
41714
41959
  curationSucceeded = true;
41715
41960
  } catch (error93) {
41716
41961
  const msg = error93 instanceof Error ? error93.message : String(error93);
41717
41962
  warnings.push(`Lessons curation failed: ${msg}`);
41718
41963
  console.warn("[close-command] curateAndStoreSwarm error:", error93);
41719
41964
  }
41720
- if (curationSucceeded && explicitLessons.length > 0) {
41965
+ if (curationSucceeded && allLessons.length > 0) {
41721
41966
  await fs12.unlink(lessonsFilePath).catch(() => {});
41722
41967
  }
41723
- if (planExists && !planAlreadyDone) {
41724
- for (const phase of phases) {
41725
- if (phase.status !== "complete" && phase.status !== "completed") {
41726
- phase.status = "closed";
41727
- if (!closedPhases.includes(phase.id)) {
41728
- closedPhases.push(phase.id);
41729
- }
41968
+ if (planExists) {
41969
+ const guaranteeResult = guaranteeAllPlansComplete(planData);
41970
+ for (const phaseId of guaranteeResult.closedPhaseIds) {
41971
+ if (!closedPhases.includes(phaseId)) {
41972
+ closedPhases.push(phaseId);
41730
41973
  }
41731
- for (const task of phase.tasks ?? []) {
41732
- if (task.status !== "completed" && task.status !== "complete") {
41733
- task.status = "closed";
41734
- if (!closedTasks.includes(task.id)) {
41735
- closedTasks.push(task.id);
41736
- }
41737
- }
41974
+ }
41975
+ for (const taskId of guaranteeResult.closedTaskIds) {
41976
+ if (!closedTasks.includes(taskId)) {
41977
+ closedTasks.push(taskId);
41738
41978
  }
41739
41979
  }
41740
- try {
41741
- await fs12.writeFile(planPath, JSON.stringify(planData, null, 2), "utf-8");
41742
- } catch (error93) {
41743
- const msg = error93 instanceof Error ? error93.message : String(error93);
41744
- warnings.push(`Failed to persist terminal plan.json state: ${msg}`);
41745
- console.warn("[close-command] Failed to write plan.json:", error93);
41980
+ if (!planAlreadyDone || guaranteeResult.closedPhaseIds.length > 0 || guaranteeResult.closedTaskIds.length > 0) {
41981
+ try {
41982
+ await fs12.writeFile(planPath, JSON.stringify(planData, null, 2), "utf-8");
41983
+ } catch (error93) {
41984
+ const msg = error93 instanceof Error ? error93.message : String(error93);
41985
+ warnings.push(`Failed to persist terminal plan.json state: ${msg}`);
41986
+ console.warn("[close-command] Failed to write plan.json:", error93);
41987
+ }
41746
41988
  }
41747
41989
  }
41748
41990
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
@@ -41881,111 +42123,27 @@ async function handleCloseCommand(directory, args2) {
41881
42123
  console.warn("[close-command] Failed to write context.md:", error93);
41882
42124
  }
41883
42125
  const pruneBranches = args2.includes("--prune-branches");
41884
- const prunedBranches = [];
41885
- const pruneErrors = [];
41886
42126
  let gitAlignResult = "";
42127
+ const prunedBranches = [];
41887
42128
  const isGit = isGitRepo2(directory);
41888
42129
  if (isGit) {
41889
- try {
41890
- const currentBranch = getCurrentBranch(directory);
41891
- if (currentBranch === "HEAD") {
41892
- gitAlignResult = "Skipped git alignment: detached HEAD state";
41893
- warnings.push("Repo is in detached HEAD state. Checkout a branch before starting a new swarm.");
41894
- } else if (hasUncommittedChanges(directory)) {
41895
- gitAlignResult = "Skipped git alignment: uncommitted changes in worktree";
41896
- warnings.push("Uncommitted changes detected. Commit or stash before aligning to main.");
41897
- } else {
41898
- const baseBranch = getDefaultBaseBranch(directory);
41899
- const localBase = baseBranch.replace(/^origin\//, "");
41900
- if (currentBranch === localBase) {
41901
- try {
41902
- execFileSync("git", ["fetch", "origin", localBase], {
41903
- cwd: directory,
41904
- encoding: "utf-8",
41905
- timeout: 30000,
41906
- stdio: ["pipe", "pipe", "pipe"]
41907
- });
41908
- const mergeBase = execFileSync("git", ["merge-base", "HEAD", baseBranch], {
41909
- cwd: directory,
41910
- encoding: "utf-8",
41911
- timeout: 1e4,
41912
- stdio: ["pipe", "pipe", "pipe"]
41913
- }).trim();
41914
- const headSha = execFileSync("git", ["rev-parse", "HEAD"], {
41915
- cwd: directory,
41916
- encoding: "utf-8",
41917
- timeout: 1e4,
41918
- stdio: ["pipe", "pipe", "pipe"]
41919
- }).trim();
41920
- if (mergeBase === headSha) {
41921
- execFileSync("git", ["merge", "--ff-only", baseBranch], {
41922
- cwd: directory,
41923
- encoding: "utf-8",
41924
- timeout: 30000,
41925
- stdio: ["pipe", "pipe", "pipe"]
41926
- });
41927
- gitAlignResult = `Aligned to ${baseBranch} (fast-forward)`;
41928
- } else {
41929
- gitAlignResult = `On ${localBase} but cannot fast-forward to ${baseBranch} (diverged)`;
41930
- warnings.push(`Local ${localBase} has diverged from ${baseBranch}. Manual merge/rebase needed.`);
41931
- }
41932
- } catch (fetchErr) {
41933
- gitAlignResult = `Fetch from origin/${localBase} failed — remote may be unavailable`;
41934
- warnings.push(`Git fetch failed: ${fetchErr instanceof Error ? fetchErr.message : String(fetchErr)}`);
41935
- }
41936
- } else {
41937
- gitAlignResult = `On branch ${currentBranch}. Switch to ${localBase} manually when ready for a new swarm.`;
41938
- }
41939
- }
41940
- } catch (gitError) {
41941
- gitAlignResult = `Git alignment error: ${gitError instanceof Error ? gitError.message : String(gitError)}`;
42130
+ const alignResult = resetToRemoteBranch(directory, { pruneBranches });
42131
+ gitAlignResult = alignResult.message;
42132
+ prunedBranches.push(...alignResult.prunedBranches);
42133
+ if (!alignResult.success) {
42134
+ warnings.push(`Git alignment: ${alignResult.message}`);
41942
42135
  }
41943
- if (pruneBranches) {
41944
- try {
41945
- const branchOutput = execFileSync("git", ["branch", "-vv"], {
41946
- cwd: directory,
41947
- encoding: "utf-8",
41948
- stdio: ["pipe", "pipe", "pipe"]
41949
- });
41950
- const goneBranches = branchOutput.split(`
41951
- `).filter((line) => line.includes(": gone]")).map((line) => line.trim().replace(/^[*+]\s+/, "").split(/\s+/)[0]).filter(Boolean);
41952
- for (const branch of goneBranches) {
41953
- try {
41954
- execFileSync("git", ["branch", "-d", branch], {
41955
- cwd: directory,
41956
- encoding: "utf-8",
41957
- stdio: ["pipe", "pipe", "pipe"]
41958
- });
41959
- prunedBranches.push(branch);
41960
- } catch {
41961
- pruneErrors.push(branch);
41962
- }
41963
- }
41964
- } catch {}
42136
+ if (alignResult.alreadyAligned) {
42137
+ gitAlignResult = `Already aligned with ${alignResult.targetBranch}`;
42138
+ }
42139
+ for (const w of alignResult.warnings) {
42140
+ warnings.push(w);
41965
42141
  }
41966
42142
  } else {
41967
42143
  gitAlignResult = "Not a git repository — skipped git alignment";
41968
42144
  }
41969
42145
  const closeSummaryPath = validateSwarmPath(directory, "close-summary.md");
41970
42146
  const finalizationType = isForced ? "Forced closure" : planAlreadyDone ? "Plan already terminal — cleanup only" : "Normal finalization";
41971
- const actionsPerformed = [
41972
- ...!planAlreadyDone && inProgressPhases.length > 0 ? ["- Wrote retrospectives for in-progress phases"] : [],
41973
- `- ${archiveResult}`,
41974
- ...cleanedFiles.length > 0 ? [
41975
- `- Cleaned ${cleanedFiles.length} active-state file(s): ${cleanedFiles.join(", ")}`
41976
- ] : [],
41977
- "- Reset context.md for next session",
41978
- ...configBackupsRemoved > 0 ? [`- Removed ${configBackupsRemoved} stale config backup file(s)`] : [],
41979
- ...swarmPlanFilesRemoved > 0 ? [
41980
- `- Removed ${swarmPlanFilesRemoved} root-level SWARM_PLAN checkpoint artifact(s)`
41981
- ] : [],
41982
- ...prunedBranches.length > 0 ? [
41983
- `- Pruned ${prunedBranches.length} stale local git branch(es): ${prunedBranches.join(", ")}`
41984
- ] : [],
41985
- "- Cleared agent sessions and delegation chains",
41986
- ...planExists && !planAlreadyDone ? ["- Set non-completed phases/tasks to closed status"] : [],
41987
- ...gitAlignResult ? [`- Git: ${gitAlignResult}`] : []
41988
- ];
41989
42147
  const summaryContent = [
41990
42148
  "# Swarm Close Summary",
41991
42149
  "",
@@ -41993,16 +42151,34 @@ async function handleCloseCommand(directory, args2) {
41993
42151
  `**Closed:** ${new Date().toISOString()}`,
41994
42152
  `**Finalization:** ${finalizationType}`,
41995
42153
  "",
41996
- `## Phases Closed: ${closedPhases.length}`,
41997
- !planExists ? "_No plan — ad-hoc session_" : closedPhases.length > 0 ? closedPhases.map((id) => `- Phase ${id}`).join(`
41998
- `) : "_No phases to close_",
42154
+ "## Retrospective",
42155
+ !planExists ? "_No plan — ad-hoc session_" : closedPhases.length > 0 ? closedPhases.map((id) => `- Phase ${id} closed`).join(`
42156
+ `) : "_No phases closed this run_",
42157
+ ...closedTasks.length > 0 ? [
42158
+ "",
42159
+ `**Tasks marked closed:** ${closedTasks.length}`,
42160
+ ...closedTasks.map((id) => `- ${id}`)
42161
+ ] : [],
41999
42162
  "",
42000
- `## Tasks Closed: ${closedTasks.length}`,
42001
- closedTasks.length > 0 ? closedTasks.map((id) => `- ${id}`).join(`
42002
- `) : "_No incomplete tasks_",
42163
+ "## Lessons Committed",
42164
+ allLessons.length > 0 ? `| # | Lesson |` : "_No lessons committed_",
42165
+ ...allLessons.length > 0 ? ["| --- | --- |", ...allLessons.map((l, i2) => `| ${i2 + 1} | ${l} |`)] : [],
42003
42166
  "",
42004
- "## Actions Performed",
42005
- ...actionsPerformed,
42167
+ "## Local Repo State",
42168
+ ...gitAlignResult ? [`- **Git:** ${gitAlignResult}`] : ["- Git alignment skipped"],
42169
+ ...prunedBranches.length > 0 ? [`- **Pruned branches:** ${prunedBranches.join(", ")}`] : [],
42170
+ `- **Archive:** ${archiveResult}`,
42171
+ ...cleanedFiles.length > 0 ? [`- **Cleaned:** ${cleanedFiles.length} file(s)`] : [],
42172
+ "",
42173
+ "## Context",
42174
+ "- Reset context.md for next session",
42175
+ "- Cleared agent sessions and delegation chains",
42176
+ ...configBackupsRemoved > 0 ? [`- Removed ${configBackupsRemoved} stale config backup file(s)`] : [],
42177
+ ...swarmPlanFilesRemoved > 0 ? [
42178
+ `- Removed ${swarmPlanFilesRemoved} root-level SWARM_PLAN checkpoint artifact(s)`
42179
+ ] : [],
42180
+ ...planExists && !planAlreadyDone ? ["- Set non-completed phases/tasks to closed status"] : [],
42181
+ ...curationSucceeded && allLessons.length > 0 ? [`- Committed ${allLessons.length} lesson(s) to knowledge store`] : [],
42006
42182
  "",
42007
42183
  ...warnings.length > 0 ? ["## Warnings", ...warnings.map((w) => `- ${w}`), ""] : []
42008
42184
  ].join(`
@@ -42030,9 +42206,6 @@ async function handleCloseCommand(directory, args2) {
42030
42206
  swarmState.fullAutoEnabledInConfig = preservedFullAutoFlag;
42031
42207
  swarmState.curatorInitAgentNames = preservedCuratorInitNames;
42032
42208
  swarmState.curatorPhaseAgentNames = preservedCuratorPhaseNames;
42033
- if (pruneErrors.length > 0) {
42034
- warnings.push(`Could not prune ${pruneErrors.length} branch(es) (unmerged or checked out): ${pruneErrors.join(", ")}`);
42035
- }
42036
42209
  const retroWarnings = warnings.filter((w) => w.includes("Retrospective write") || w.includes("retrospective write") || w.includes("Session retrospective"));
42037
42210
  const otherWarnings = warnings.filter((w) => !w.includes("Retrospective write") && !w.includes("retrospective write") && !w.includes("Session retrospective"));
42038
42211
  let warningMsg = "";
@@ -42050,16 +42223,19 @@ ${retroWarnings.map((w) => `- ${w}`).join(`
42050
42223
  ${otherWarnings.map((w) => `- ${w}`).join(`
42051
42224
  `)}`;
42052
42225
  }
42226
+ const lessonSummary = curationSucceeded && allLessons.length > 0 ? `
42227
+
42228
+ **Lessons Committed:** ${allLessons.length} lesson(s) committed to knowledge store` : "";
42053
42229
  if (planAlreadyDone) {
42054
42230
  return `✅ Session finalized. Plan was already in a terminal state — cleanup and archive applied.
42055
42231
 
42056
42232
  **Archive:** ${archiveResult}
42057
- **Git:** ${gitAlignResult}${warningMsg}`;
42233
+ **Git:** ${gitAlignResult}${lessonSummary}${warningMsg}`;
42058
42234
  }
42059
42235
  return `✅ Swarm finalized. ${closedPhases.length} phase(s) closed, ${closedTasks.length} incomplete task(s) marked closed.
42060
42236
 
42061
42237
  **Archive:** ${archiveResult}
42062
- **Git:** ${gitAlignResult}${warningMsg}`;
42238
+ **Git:** ${gitAlignResult}${lessonSummary}${warningMsg}`;
42063
42239
  }
42064
42240
  var ARCHIVE_ARTIFACTS, ACTIVE_STATE_TO_CLEAN;
42065
42241
  var init_close = __esm(() => {
@@ -48185,6 +48361,216 @@ var init_history = __esm(() => {
48185
48361
  init_history_service();
48186
48362
  });
48187
48363
 
48364
+ // src/commands/issue.ts
48365
+ import { execSync as execSync2 } from "node:child_process";
48366
+ function sanitizeUrl(raw) {
48367
+ let urlStr = raw.trim();
48368
+ urlStr = urlStr.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
48369
+ const fragmentIdx = urlStr.indexOf("#");
48370
+ if (fragmentIdx !== -1) {
48371
+ urlStr = urlStr.slice(0, fragmentIdx);
48372
+ }
48373
+ const queryIdx = urlStr.indexOf("?");
48374
+ if (queryIdx !== -1) {
48375
+ urlStr = urlStr.slice(0, queryIdx);
48376
+ }
48377
+ urlStr = urlStr.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^@/]+@/, "https://");
48378
+ if (urlStr.length > MAX_URL_LEN) {
48379
+ urlStr = urlStr.slice(0, MAX_URL_LEN);
48380
+ }
48381
+ return urlStr.trim();
48382
+ }
48383
+ function isPrivateHost(url3) {
48384
+ const host = url3.hostname.toLowerCase();
48385
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0") {
48386
+ return true;
48387
+ }
48388
+ if (host.startsWith("localhost") || host === "localhost.com") {
48389
+ return true;
48390
+ }
48391
+ const ipv4Private = /^10\./;
48392
+ const ipv4172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
48393
+ const ipv4192 = /^192\.168\./;
48394
+ const ipv6Private = /^fe80:/i;
48395
+ const ipv6Unique = /^f[cd][0-9a-f]{2}:/i;
48396
+ if (ipv4Private.test(host) || ipv4172.test(host) || ipv4192.test(host) || ipv6Private.test(host) || ipv6Unique.test(host)) {
48397
+ return true;
48398
+ }
48399
+ if (host.startsWith("::ffff:")) {
48400
+ const inner = host.slice(7);
48401
+ if (ipv4Private.test(inner) || ipv4172.test(inner) || ipv4192.test(inner)) {
48402
+ return true;
48403
+ }
48404
+ }
48405
+ return false;
48406
+ }
48407
+ function validateAndSanitizeUrl(rawUrl) {
48408
+ const sanitized = sanitizeUrl(rawUrl);
48409
+ if (!sanitized) {
48410
+ return { error: "Empty URL" };
48411
+ }
48412
+ if (!sanitized.startsWith("https://")) {
48413
+ return { error: "URL must use HTTPS scheme" };
48414
+ }
48415
+ try {
48416
+ const url3 = new URL(sanitized);
48417
+ const hostname5 = url3.hostname;
48418
+ if (/[\u0080-\u{10FFFF}]/u.test(hostname5)) {
48419
+ return { error: "Non-ASCII hostnames are not allowed" };
48420
+ }
48421
+ if (isPrivateHost(url3)) {
48422
+ return { error: "Private or localhost URLs are not allowed" };
48423
+ }
48424
+ const githubIssuePattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/([0-9]+)\/?$/;
48425
+ if (!githubIssuePattern.test(sanitized)) {
48426
+ return {
48427
+ error: "URL must be a GitHub issue URL (https://github.com/owner/repo/issues/N)"
48428
+ };
48429
+ }
48430
+ return { sanitized };
48431
+ } catch {
48432
+ return { error: "Invalid URL format" };
48433
+ }
48434
+ }
48435
+ function parseArgs2(args2) {
48436
+ const out2 = {
48437
+ plan: false,
48438
+ trace: false,
48439
+ noRepro: false,
48440
+ rest: []
48441
+ };
48442
+ for (const token of args2) {
48443
+ if (token === "--plan") {
48444
+ out2.plan = true;
48445
+ continue;
48446
+ }
48447
+ if (token === "--trace") {
48448
+ out2.trace = true;
48449
+ out2.plan = true;
48450
+ continue;
48451
+ }
48452
+ if (token === "--no-repro") {
48453
+ out2.noRepro = true;
48454
+ continue;
48455
+ }
48456
+ out2.rest.push(token);
48457
+ }
48458
+ return out2;
48459
+ }
48460
+ function parseIssueRef(input) {
48461
+ const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)\/?$/i);
48462
+ if (urlMatch) {
48463
+ return {
48464
+ owner: urlMatch[1],
48465
+ repo: urlMatch[2],
48466
+ number: parseInt(urlMatch[3], 10)
48467
+ };
48468
+ }
48469
+ const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
48470
+ if (shorthandMatch) {
48471
+ return {
48472
+ owner: shorthandMatch[1],
48473
+ repo: shorthandMatch[2],
48474
+ number: parseInt(shorthandMatch[3], 10)
48475
+ };
48476
+ }
48477
+ const bareMatch = input.match(/^(\d+)$/);
48478
+ if (bareMatch) {
48479
+ const issueNumber = parseInt(bareMatch[1], 10);
48480
+ const remoteUrl = detectGitRemote();
48481
+ if (!remoteUrl) {
48482
+ return null;
48483
+ }
48484
+ const parsed = parseGitRemoteUrl(remoteUrl);
48485
+ if (!parsed) {
48486
+ return null;
48487
+ }
48488
+ return {
48489
+ owner: parsed.owner,
48490
+ repo: parsed.repo,
48491
+ number: issueNumber
48492
+ };
48493
+ }
48494
+ return null;
48495
+ }
48496
+ function detectGitRemote() {
48497
+ try {
48498
+ const remoteUrl = execSync2("git remote get-url origin", {
48499
+ encoding: "utf-8",
48500
+ stdio: ["pipe", "pipe", "pipe"],
48501
+ timeout: 5000
48502
+ }).trim();
48503
+ return remoteUrl || null;
48504
+ } catch {
48505
+ return null;
48506
+ }
48507
+ }
48508
+ function parseGitRemoteUrl(remoteUrl) {
48509
+ const httpsMatch = remoteUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
48510
+ if (httpsMatch) {
48511
+ return {
48512
+ owner: httpsMatch[1],
48513
+ repo: httpsMatch[2].replace(/\.git$/, "")
48514
+ };
48515
+ }
48516
+ const sshMatch = remoteUrl.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
48517
+ if (sshMatch) {
48518
+ return {
48519
+ owner: sshMatch[1],
48520
+ repo: sshMatch[2].replace(/\.git$/, "")
48521
+ };
48522
+ }
48523
+ return null;
48524
+ }
48525
+ function handleIssueCommand(_directory, args2) {
48526
+ const parsed = parseArgs2(args2);
48527
+ const rawInput = parsed.rest.join(" ").trim();
48528
+ if (!rawInput) {
48529
+ return USAGE2;
48530
+ }
48531
+ const isFullUrl = /^https?:\/\//i.test(rawInput);
48532
+ const issueInfo = parseIssueRef(isFullUrl ? sanitizeUrl(rawInput) : rawInput);
48533
+ if (!issueInfo) {
48534
+ return `Error: Could not parse issue reference from "${rawInput}"
48535
+
48536
+ ${USAGE2}`;
48537
+ }
48538
+ const issueUrl = `https://github.com/${issueInfo.owner}/${issueInfo.repo}/issues/${issueInfo.number}`;
48539
+ const result = validateAndSanitizeUrl(issueUrl);
48540
+ if ("error" in result) {
48541
+ return `Error: ${result.error}
48542
+
48543
+ ${USAGE2}`;
48544
+ }
48545
+ const flags2 = [];
48546
+ if (parsed.plan)
48547
+ flags2.push("plan=true");
48548
+ if (parsed.trace)
48549
+ flags2.push("trace=true");
48550
+ if (parsed.noRepro)
48551
+ flags2.push("noRepro=true");
48552
+ const flagsStr = flags2.length > 0 ? ` ${flags2.join(" ")}` : "";
48553
+ return `[MODE: ISSUE_INGEST issue="${result.sanitized}"${flagsStr}]`;
48554
+ }
48555
+ var MAX_URL_LEN = 2048, USAGE2;
48556
+ var init_issue = __esm(() => {
48557
+ USAGE2 = [
48558
+ "Usage: /swarm issue <url|owner/repo#N|N> [--plan] [--trace] [--no-repro]",
48559
+ "",
48560
+ "Ingest a GitHub issue into the swarm workflow.",
48561
+ " /swarm issue https://github.com/owner/repo/issues/42",
48562
+ " /swarm issue owner/repo#42",
48563
+ " /swarm issue 42 --plan",
48564
+ " /swarm issue 42 --trace --no-repro",
48565
+ "",
48566
+ "Flags:",
48567
+ " --plan Transition to plan creation after spec generation",
48568
+ " --trace Run full fix-and-PR workflow (implies --plan)",
48569
+ " --no-repro Skip reproduction step"
48570
+ ].join(`
48571
+ `);
48572
+ });
48573
+
48188
48574
  // src/hooks/knowledge-migrator.ts
48189
48575
  import { randomUUID as randomUUID3 } from "node:crypto";
48190
48576
  import { existsSync as existsSync16, readFileSync as readFileSync11 } from "node:fs";
@@ -48693,6 +49079,192 @@ var init_plan = __esm(() => {
48693
49079
  init_plan_service();
48694
49080
  });
48695
49081
 
49082
+ // src/commands/pr-review.ts
49083
+ import { execSync as execSync3 } from "node:child_process";
49084
+ function sanitizeUrl2(raw) {
49085
+ let urlStr = raw.trim();
49086
+ urlStr = urlStr.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
49087
+ const fragmentIdx = urlStr.indexOf("#");
49088
+ if (fragmentIdx !== -1) {
49089
+ urlStr = urlStr.slice(0, fragmentIdx);
49090
+ }
49091
+ const queryIdx = urlStr.indexOf("?");
49092
+ if (queryIdx !== -1) {
49093
+ urlStr = urlStr.slice(0, queryIdx);
49094
+ }
49095
+ urlStr = urlStr.replace(/^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^@/]+@/, "https://");
49096
+ if (urlStr.length > MAX_URL_LEN2) {
49097
+ urlStr = urlStr.slice(0, MAX_URL_LEN2);
49098
+ }
49099
+ return urlStr.trim();
49100
+ }
49101
+ function isPrivateHost2(url3) {
49102
+ const host = url3.hostname.toLowerCase();
49103
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0") {
49104
+ return true;
49105
+ }
49106
+ if (host.startsWith("localhost") || host === "localhost.com") {
49107
+ return true;
49108
+ }
49109
+ const ipv4Private = /^10\./;
49110
+ const ipv4172 = /^172\.(1[6-9]|2\d|3[0-1])\./;
49111
+ const ipv4192 = /^192\.168\./;
49112
+ const ipv6Private = /^fe80:/i;
49113
+ const ipv6Unique = /^f[cd][0-9a-f]{2}:/i;
49114
+ if (ipv4Private.test(host) || ipv4172.test(host) || ipv4192.test(host) || ipv6Private.test(host) || ipv6Unique.test(host)) {
49115
+ return true;
49116
+ }
49117
+ if (host.startsWith("::ffff:")) {
49118
+ const inner = host.slice(7);
49119
+ if (ipv4Private.test(inner) || ipv4172.test(inner) || ipv4192.test(inner)) {
49120
+ return true;
49121
+ }
49122
+ }
49123
+ return false;
49124
+ }
49125
+ function validateAndSanitizeUrl2(rawUrl) {
49126
+ const sanitized = sanitizeUrl2(rawUrl);
49127
+ if (!sanitized) {
49128
+ return { error: "Empty URL" };
49129
+ }
49130
+ if (!sanitized.startsWith("https://")) {
49131
+ return { error: "URL must use HTTPS scheme" };
49132
+ }
49133
+ try {
49134
+ const url3 = new URL(sanitized);
49135
+ const hostname5 = url3.hostname;
49136
+ if (/[\u0080-\u{10FFFF}]/u.test(hostname5)) {
49137
+ return { error: "Non-ASCII hostnames are not allowed" };
49138
+ }
49139
+ if (isPrivateHost2(url3)) {
49140
+ return { error: "Private or localhost URLs are not allowed" };
49141
+ }
49142
+ const githubPrPattern = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/([0-9]+)\/?$/;
49143
+ if (!githubPrPattern.test(sanitized)) {
49144
+ return {
49145
+ error: "URL must be a GitHub pull request URL (https://github.com/owner/repo/pull/N)"
49146
+ };
49147
+ }
49148
+ return { sanitized };
49149
+ } catch {
49150
+ return { error: "Invalid URL format" };
49151
+ }
49152
+ }
49153
+ function parseArgs3(args2) {
49154
+ const out2 = { council: false, rest: [] };
49155
+ for (const token of args2) {
49156
+ if (token === "--council") {
49157
+ out2.council = true;
49158
+ continue;
49159
+ }
49160
+ out2.rest.push(token);
49161
+ }
49162
+ return out2;
49163
+ }
49164
+ function parsePrRef(input) {
49165
+ const urlMatch = input.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i);
49166
+ if (urlMatch) {
49167
+ return {
49168
+ owner: urlMatch[1],
49169
+ repo: urlMatch[2],
49170
+ number: parseInt(urlMatch[3], 10)
49171
+ };
49172
+ }
49173
+ const shorthandMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
49174
+ if (shorthandMatch) {
49175
+ return {
49176
+ owner: shorthandMatch[1],
49177
+ repo: shorthandMatch[2],
49178
+ number: parseInt(shorthandMatch[3], 10)
49179
+ };
49180
+ }
49181
+ const bareMatch = input.match(/^(\d+)$/);
49182
+ if (bareMatch) {
49183
+ const prNumber = parseInt(bareMatch[1], 10);
49184
+ const remoteUrl = detectGitRemote2();
49185
+ if (!remoteUrl) {
49186
+ return null;
49187
+ }
49188
+ const parsed = parseGitRemoteUrl2(remoteUrl);
49189
+ if (!parsed) {
49190
+ return null;
49191
+ }
49192
+ return {
49193
+ owner: parsed.owner,
49194
+ repo: parsed.repo,
49195
+ number: prNumber
49196
+ };
49197
+ }
49198
+ return null;
49199
+ }
49200
+ function detectGitRemote2() {
49201
+ try {
49202
+ const remoteUrl = execSync3("git remote get-url origin", {
49203
+ encoding: "utf-8",
49204
+ stdio: ["pipe", "pipe", "pipe"],
49205
+ timeout: 5000
49206
+ }).trim();
49207
+ return remoteUrl || null;
49208
+ } catch {
49209
+ return null;
49210
+ }
49211
+ }
49212
+ function parseGitRemoteUrl2(remoteUrl) {
49213
+ const httpsMatch = remoteUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i);
49214
+ if (httpsMatch) {
49215
+ return {
49216
+ owner: httpsMatch[1],
49217
+ repo: httpsMatch[2].replace(/\.git$/, "")
49218
+ };
49219
+ }
49220
+ const sshMatch = remoteUrl.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
49221
+ if (sshMatch) {
49222
+ return {
49223
+ owner: sshMatch[1],
49224
+ repo: sshMatch[2].replace(/\.git$/, "")
49225
+ };
49226
+ }
49227
+ return null;
49228
+ }
49229
+ function handlePrReviewCommand(_directory, args2) {
49230
+ const parsed = parseArgs3(args2);
49231
+ const rawInput = parsed.rest.join(" ").trim();
49232
+ if (!rawInput) {
49233
+ return USAGE3;
49234
+ }
49235
+ const isFullUrl = /^https?:\/\//i.test(rawInput);
49236
+ const prInfo = parsePrRef(isFullUrl ? sanitizeUrl2(rawInput) : rawInput);
49237
+ if (!prInfo) {
49238
+ return `Error: Could not parse PR reference from "${rawInput}"
49239
+
49240
+ ${USAGE3}`;
49241
+ }
49242
+ const prUrl = `https://github.com/${prInfo.owner}/${prInfo.repo}/pull/${prInfo.number}`;
49243
+ const result = validateAndSanitizeUrl2(prUrl);
49244
+ if ("error" in result) {
49245
+ return `Error: ${result.error}
49246
+
49247
+ ${USAGE3}`;
49248
+ }
49249
+ const councilFlag = parsed.council ? "council=true" : "council=false";
49250
+ return `[MODE: PR_REVIEW pr="${result.sanitized}" ${councilFlag}]`;
49251
+ }
49252
+ var MAX_URL_LEN2 = 2048, USAGE3;
49253
+ var init_pr_review = __esm(() => {
49254
+ USAGE3 = [
49255
+ "Usage: /swarm pr-review <url|owner/repo#N|N> [--council]",
49256
+ "",
49257
+ "Run a full swarm PR review on a GitHub pull request.",
49258
+ " /swarm pr-review https://github.com/owner/repo/pull/42",
49259
+ " /swarm pr-review owner/repo#42",
49260
+ " /swarm pr-review 42 --council",
49261
+ "",
49262
+ "Flags:",
49263
+ " --council Run adversarial council variant (all lanes assume work is wrong)"
49264
+ ].join(`
49265
+ `);
49266
+ });
49267
+
48696
49268
  // src/utils/path-security.ts
48697
49269
  import * as fs17 from "node:fs";
48698
49270
  import * as path30 from "node:path";
@@ -52653,7 +53225,8 @@ var init_qa_gates = __esm(() => {
52653
53225
  "hallucination_guard",
52654
53226
  "sast_enabled",
52655
53227
  "mutation_test",
52656
- "council_general_review"
53228
+ "council_general_review",
53229
+ "drift_check"
52657
53230
  ];
52658
53231
  });
52659
53232
 
@@ -54333,8 +54906,10 @@ var init_registry = __esm(() => {
54333
54906
  init_full_auto();
54334
54907
  init_handoff();
54335
54908
  init_history();
54909
+ init_issue();
54336
54910
  init_knowledge();
54337
54911
  init_plan();
54912
+ init_pr_review();
54338
54913
  init_preflight();
54339
54914
  init_promote();
54340
54915
  init_qa_gates();
@@ -54489,11 +55064,23 @@ var init_registry = __esm(() => {
54489
55064
  args: "<question> [--preset <name>] [--spec-review]",
54490
55065
  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."
54491
55066
  },
55067
+ "pr-review": {
55068
+ handler: async (ctx) => handlePrReviewCommand(ctx.directory, ctx.args),
55069
+ description: "Launch deep PR review with multi-lane analysis [url] [--council]",
55070
+ args: "<pr-url|owner/repo#N|N> [--council]",
55071
+ 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)."
55072
+ },
55073
+ issue: {
55074
+ handler: async (ctx) => handleIssueCommand(ctx.directory, ctx.args),
55075
+ description: "Ingest a GitHub issue into the swarm workflow [url] [--plan] [--trace] [--no-repro]",
55076
+ args: "<issue-url|owner/repo#N|N> [--plan] [--trace] [--no-repro]",
55077
+ details: "Triggers the architect to enter MODE: ISSUE_INGEST — 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)."
55078
+ },
54492
55079
  "qa-gates": {
54493
55080
  handler: (ctx) => handleQaGatesCommand(ctx.directory, ctx.args, ctx.sessionID),
54494
55081
  description: "View or modify QA gate profile for the current plan [enable|override <gate>...]",
54495
55082
  args: "[show|enable|override] <gate>...",
54496
- 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."
55083
+ 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."
54497
55084
  },
54498
55085
  promote: {
54499
55086
  handler: (ctx) => handlePromoteCommand(ctx.directory, ctx.args),
@@ -54709,7 +55296,7 @@ function buildQaGateSelectionDialogue(modeLabel) {
54709
55296
  const leadIn = modeLabel === "BRAINSTORM" ? "Now ask the user which QA gates to enable for this plan — do not select on their behalf." : modeLabel === "SPECIFY" ? "Ask the user which QA gates to enable for this plan before suggesting the next step." : "No pending gate selection found in `.swarm/context.md`. Ask the user inline now.";
54710
55297
  return `${leadIn}
54711
55298
 
54712
- Present the nine gates with their defaults (DEFAULT_QA_GATES) as a single user-facing question. Offer the user a one-shot choice: accept defaults, or customize. The nine gates are:
55299
+ Present the ten gates with their defaults (DEFAULT_QA_GATES) as a single user-facing question. Offer the user a one-shot choice: accept defaults, or customize. The ten gates are:
54713
55300
  - reviewer (default: ON) — code review of coder output
54714
55301
  - test_engineer (default: ON) — test verification of coder output
54715
55302
  - sme_enabled (default: ON) — SME consultation during planning/clarification
@@ -54719,6 +55306,7 @@ Present the nine gates with their defaults (DEFAULT_QA_GATES) as a single user-f
54719
55306
  - hallucination_guard (default: OFF) — when enabled, mandatory per-phase API/signature/claim/citation verification via critic_hallucination_verifier at PHASE-WRAP; phase_complete will REJECT phase completion unless .swarm/evidence/{phase}/hallucination-guard.json exists with an APPROVED verdict (recommended for claim-heavy or research-heavy work)
54720
55307
  - mutation_test (default: OFF) — when enabled, runs mutation testing on source files touched this phase via generate_mutants + mutation_test + write_mutation_evidence at PHASE-WRAP; FAIL verdict blocks phase_complete; WARN is non-blocking (recommended for projects with coverage gaps or safety-critical code)
54721
55308
  - council_general_review (default: OFF) — when enabled, MODE: SPECIFY runs convene_general_council on the draft spec before the critic-gate; multiple models each independently search the web, deliberate on disagreements, and a moderator synthesizes a final answer that the architect folds into the spec (recommended for novel architecture, unclear best practices, or high-risk design decisions). Requires council.general.enabled: true and a configured search API key.
55309
+ - drift_check (default: ON) — when enabled, mandatory per-phase drift verification via critic_drift_verifier at PHASE-WRAP; compares implemented changes against spec.md intent; hard-blocks phase_complete when spec.md exists and drift evidence is missing or REJECTED; advisory-only when no spec.md exists (recommended for all projects with a specification)
54722
55310
 
54723
55311
  One question, one message, defaults pre-stated. Wait for the user's answer.`;
54724
55312
  }
@@ -55464,6 +56052,7 @@ Do NOT call \`set_qa_gates\` yet — \`plan.json\` does not exist at this point.
55464
56052
  - hallucination_guard: <true|false>
55465
56053
  - mutation_test: <true|false>
55466
56054
  - council_general_review: <true|false>
56055
+ - drift_check: <true|false>
55467
56056
  - recorded_at: <ISO timestamp>
55468
56057
  \`\`\`
55469
56058
  MODE: PLAN applies these after \`save_plan\` succeeds via \`set_qa_gates\`.
@@ -55541,6 +56130,7 @@ Do NOT call \`set_qa_gates\` yet — \`plan.json\` does not exist at this point.
55541
56130
  - hallucination_guard: <true|false>
55542
56131
  - mutation_test: <true|false>
55543
56132
  - council_general_review: <true|false>
56133
+ - drift_check: <true|false>
55544
56134
  - recorded_at: <ISO timestamp>
55545
56135
  \`\`\`
55546
56136
  MODE: PLAN will read this section after \`save_plan\` succeeds and persist via \`set_qa_gates\`.
@@ -55772,6 +56362,61 @@ This mode is ADVISORY — it does NOT block any other workflow and does NOT modi
55772
56362
  - If no moderator: present the structural \`synthesis\` markdown from the tool's return.
55773
56363
  In either case, do NOT present the raw per-member JSON. Do NOT silently pick a winner among persisting disagreements — surface them honestly.
55774
56364
 
56365
+ ### MODE: ISSUE_INGEST
56366
+ Activates when: user invokes \`/swarm issue <url>\`; OR architect receives \`[MODE: ISSUE_INGEST issue="<url>"]\` signal.
56367
+
56368
+ Purpose: ingest a GitHub issue, localize root cause, and produce a resolution spec. The issue URL points to a GitHub issue that describes a bug, feature request, or task to be resolved.
56369
+
56370
+ Flags parsed from signal:
56371
+ - \`plan=true\` → after spec generation, transition to MODE: PLAN (create implementation plan)
56372
+ - \`trace=true\` → after plan, delegate to swarm-implement skill for full fix-and-PR workflow (implies plan=true)
56373
+ - \`noRepro=true\` → skip reproduction verification step
56374
+
56375
+ #### Phase 1: INTAKE
56376
+ 1. Fetch the issue body using the GitHub CLI (\`gh issue view <N> --repo <owner>/<repo> --json title,body,labels,assignees,comments\`) or web fetch.
56377
+ 2. Parse the issue into a normalized **Intake Note** with four required fields:
56378
+ - **Observed behavior**: what the issue reports
56379
+ - **Expected behavior**: what should happen instead
56380
+ - **Reproduction steps**: how to trigger the issue (may be absent; flag with \`[NEEDS REPRO]\` if missing)
56381
+ - **Environment**: platform, version, configuration context
56382
+ 3. If any required field is missing and cannot be inferred from context, flag as \`[NEEDS REPRO]\`.
56383
+ 4. If \`--no-repro\` flag is set, skip reproduction verification and proceed with available information.
56384
+ 5. Exit when the Intake Note is complete or all missing fields are flagged.
56385
+
56386
+ #### Phase 2: LOCALIZATION
56387
+ 1. Delegate to \`mega_explorer\` to scan the codebase for code areas related to the issue's observed behavior.
56388
+ 2. Build 2–5 candidate hypotheses for root cause, each with:
56389
+ - **Location**: file(s) and function(s) most likely responsible
56390
+ - **Confidence**: composite score (stack-trace match 0.4, recency 0.25, call-graph proximity 0.2, test-failure correlation 0.15)
56391
+ - **Falsifiability**: a specific test or observation that would disprove this hypothesis
56392
+ 3. Validate top-3 hypotheses in parallel using targeted \`mega_sme\` consultations.
56393
+ 4. Prune to a single root cause hypothesis with supporting evidence.
56394
+ 5. Exit when a root cause is identified with ≥70% confidence, or when all hypotheses are exhausted (report ambiguity).
56395
+
56396
+ #### Phase 3: SPEC GENERATION
56397
+ 0. Include a **Root Cause** section derived from Phase 2 localization results: concise statement of the identified root cause, location, and confidence score. Include a **Fix Strategy** section at product/behavior level (what the fix must accomplish, not how to implement it).
56398
+ 1. Generate \`.swarm/spec.md\` using the same SPEC CONTENT RULES as MODE: SPECIFY:
56399
+ - WHAT users need and WHY — never HOW to implement
56400
+ - FR-### / SC-### numbering, Given/When/Then scenarios
56401
+ - No technology stack, APIs, or code structure
56402
+ - \`[NEEDS CLARIFICATION]\` markers (max 3)
56403
+ 2. Cross-reference the spec against the issue's expected behavior to ensure alignment.
56404
+ 3. If the issue is a bug: spec must describe the correct behavior, not the broken behavior.
56405
+ 4. If the issue is a feature: spec must describe the user-facing outcome, not the implementation.
56406
+ 5. QA GATE SELECTION: Ask user which QA gates to enable (same dialogue as MODE: SPECIFY). Write to \`.swarm/context.md\` under \`## Pending QA Gate Selection\`.
56407
+
56408
+ #### Phase 4: TRANSITION
56409
+ Based on flags:
56410
+ - No flags → report spec summary and suggest \`PLAN\` or \`CLARIFY-SPEC\`
56411
+ - \`plan=true\` → transition to MODE: PLAN using the generated spec
56412
+ - \`trace=true\` → transition to MODE: PLAN, then delegate to swarm-implement skill for full fix workflow
56413
+
56414
+ RULES:
56415
+ - One question per message in INTAKE dialogue (max 6 questions)
56416
+ - Hypotheses must be falsifiable — no unfalsifiable hypotheses
56417
+ - Spec must be independently testable — each FR must have a verification path
56418
+ - The issue URL is already sanitized by the issue command — do not re-sanitize
56419
+
55775
56420
  ### MODE: PLAN
55776
56421
 
55777
56422
  SPEC GATE (soft — check before planning):
@@ -72786,7 +73431,7 @@ function deserializeAgentSession(s) {
72786
73431
  for (const [key, win] of Object.entries(s.windows ?? {})) {
72787
73432
  windows[key] = {
72788
73433
  ...win,
72789
- transientRetryCount: win.transientRetryCount ?? 0
73434
+ transientRetryCount: "transientRetryCount" in win ? win.transientRetryCount ?? 0 : 0
72790
73435
  };
72791
73436
  }
72792
73437
  return {
@@ -74773,7 +75418,8 @@ var COUNCIL_DEFAULTS = {
74773
75418
  parallelTimeoutMs: 30000,
74774
75419
  vetoPriority: true,
74775
75420
  requireAllMembers: false,
74776
- minimumMembers: 3
75421
+ minimumMembers: 3,
75422
+ phaseConcernsAllowComplete: true
74777
75423
  };
74778
75424
 
74779
75425
  // src/council/council-service.ts
@@ -78126,7 +78772,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
78126
78772
  }, null, 2);
78127
78773
  }
78128
78774
  if (hasActiveTurboMode(sessionID)) {
78129
- console.warn(`[phase_complete] Turbo mode active — skipping completion-verify, drift-verifier, hallucination-guard, and mutation-gate gates for phase ${phase}`);
78775
+ console.warn(`[phase_complete] Turbo mode active — skipping completion-verify, drift-verifier, hallucination-guard, mutation-gate, and phase-council gates for phase ${phase}`);
78130
78776
  } else {
78131
78777
  try {
78132
78778
  const completionResultRaw = await executeCompletionVerify({ phase }, dir);
@@ -78148,88 +78794,138 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
78148
78794
  } catch (completionError) {
78149
78795
  safeWarn(`[phase_complete] Completion verify error (non-blocking):`, completionError);
78150
78796
  }
78797
+ let driftCheckEnabled = true;
78798
+ let driftHasSpecMd = false;
78151
78799
  try {
78152
- const driftEvidencePath = path81.join(dir, ".swarm", "evidence", String(phase), "drift-verifier.json");
78153
- let driftVerdictFound = false;
78154
- let driftVerdictApproved = false;
78800
+ const specMdPath = path81.join(dir, ".swarm", "spec.md");
78801
+ driftHasSpecMd = fs65.existsSync(specMdPath);
78802
+ const gatePlan = await loadPlan(dir);
78803
+ if (gatePlan) {
78804
+ const gatePlanId = `${gatePlan.swarm}-${gatePlan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
78805
+ const gateProfile = getProfile(dir, gatePlanId);
78806
+ if (gateProfile) {
78807
+ const gateSession = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
78808
+ const gateOverrides = gateSession?.qaGateSessionOverrides ?? {};
78809
+ const gateEffective = getEffectiveGates(gateProfile, gateOverrides);
78810
+ driftCheckEnabled = gateEffective.drift_check === true;
78811
+ }
78812
+ }
78813
+ } catch (gateLoadError) {
78814
+ safeWarn(`[phase_complete] QA gate profile load error, drift_check defaults to enabled:`, gateLoadError);
78815
+ }
78816
+ if (!driftCheckEnabled) {
78817
+ console.info(`[phase_complete] drift_check disabled — skipping drift verification gate for phase ${phase}`);
78818
+ warnings.push(`drift_check gate is disabled. Drift verification was skipped for phase ${phase}.`);
78819
+ } else {
78820
+ let phaseType;
78155
78821
  try {
78156
- const driftEvidenceContent = fs65.readFileSync(driftEvidencePath, "utf-8");
78157
- const driftEvidence = JSON.parse(driftEvidenceContent);
78158
- const entries = driftEvidence.entries ?? [];
78159
- for (const entry of entries) {
78160
- if (typeof entry.type === "string" && entry.type.includes("drift") && typeof entry.verdict === "string") {
78161
- driftVerdictFound = true;
78162
- if (entry.verdict === "approved") {
78163
- driftVerdictApproved = true;
78822
+ const planPath = path81.join(dir, ".swarm", "plan.json");
78823
+ if (fs65.existsSync(planPath)) {
78824
+ const planRaw = fs65.readFileSync(planPath, "utf-8");
78825
+ const plan = JSON.parse(planRaw);
78826
+ const targetPhase = plan.phases?.find((p) => p.id === phase);
78827
+ phaseType = targetPhase?.type;
78828
+ }
78829
+ } catch {}
78830
+ if (phaseType === "non-code") {
78831
+ console.info(`[phase_complete] Phase ${phase} annotated as 'non-code' — drift verification skipped.`);
78832
+ warnings.push(`Phase ${phase} is annotated as 'non-code'. Drift verification was skipped per phase type annotation.`);
78833
+ } else {
78834
+ try {
78835
+ const driftEvidencePath = path81.join(dir, ".swarm", "evidence", String(phase), "drift-verifier.json");
78836
+ let driftVerdictFound = false;
78837
+ let driftVerdictApproved = false;
78838
+ try {
78839
+ const driftEvidenceContent = fs65.readFileSync(driftEvidencePath, "utf-8");
78840
+ const driftEvidence = JSON.parse(driftEvidenceContent);
78841
+ const entries = driftEvidence.entries ?? [];
78842
+ for (const entry of entries) {
78843
+ if (typeof entry.type === "string" && entry.type.includes("drift") && typeof entry.verdict === "string") {
78844
+ driftVerdictFound = true;
78845
+ if (entry.verdict === "approved") {
78846
+ driftVerdictApproved = true;
78847
+ }
78848
+ if (entry.verdict === "rejected" || typeof entry.summary === "string" && entry.summary.includes("NEEDS_REVISION")) {
78849
+ return JSON.stringify({
78850
+ success: false,
78851
+ phase,
78852
+ status: "blocked",
78853
+ reason: "DRIFT_VERIFICATION_REJECTED",
78854
+ message: `Phase ${phase} cannot be completed: drift verifier returned verdict '${entry.verdict}'. Address the drift issues before completing the phase.`,
78855
+ agentsDispatched,
78856
+ agentsMissing: [],
78857
+ warnings: []
78858
+ }, null, 2);
78859
+ }
78860
+ }
78861
+ }
78862
+ } catch (readError) {
78863
+ if (readError.code !== "ENOENT") {
78864
+ safeWarn(`[phase_complete] Drift verifier evidence unreadable:`, readError);
78164
78865
  }
78165
- if (entry.verdict === "rejected" || typeof entry.summary === "string" && entry.summary.includes("NEEDS_REVISION")) {
78866
+ driftVerdictFound = false;
78867
+ }
78868
+ if (!driftVerdictFound) {
78869
+ if (!driftHasSpecMd) {
78870
+ let incompleteTaskCount = 0;
78871
+ let planParseable = false;
78872
+ try {
78873
+ const planPath = path81.join(dir, ".swarm", "plan.json");
78874
+ if (fs65.existsSync(planPath)) {
78875
+ const planRaw = fs65.readFileSync(planPath, "utf-8");
78876
+ const plan = JSON.parse(planRaw);
78877
+ planParseable = true;
78878
+ const planPhase = plan.phases?.find((p) => p.id === phase);
78879
+ if (planPhase?.tasks) {
78880
+ incompleteTaskCount = planPhase.tasks.filter((t) => t.status !== "completed" && t.status !== "closed").length;
78881
+ }
78882
+ }
78883
+ } catch {}
78884
+ if (!planParseable) {
78885
+ warnings.push(`No spec.md found and drift verification evidence missing — consider running critic_drift_verifier before phase completion.`);
78886
+ } else if (incompleteTaskCount > 0) {
78887
+ warnings.push(`No spec.md found and drift verification evidence missing. Phase ${phase} has ${incompleteTaskCount} incomplete task(s) in plan.json — consider running critic_drift_verifier before phase completion.`);
78888
+ } else {
78889
+ warnings.push(`No spec.md found. Phase ${phase} tasks are all completed in plan.json. Drift verification was skipped.`);
78890
+ }
78891
+ } else {
78166
78892
  return JSON.stringify({
78167
78893
  success: false,
78168
78894
  phase,
78169
78895
  status: "blocked",
78170
- reason: "DRIFT_VERIFICATION_REJECTED",
78171
- message: `Phase ${phase} cannot be completed: drift verifier returned verdict '${entry.verdict}'. Address the drift issues before completing the phase.`,
78896
+ reason: "DRIFT_VERIFICATION_MISSING",
78897
+ message: `Phase ${phase} cannot be completed: drift_check is enabled and drift verifier evidence not found at .swarm/evidence/${phase}/drift-verifier.json. Run drift verification before completing the phase.`,
78172
78898
  agentsDispatched,
78173
78899
  agentsMissing: [],
78174
78900
  warnings: []
78175
78901
  }, null, 2);
78176
78902
  }
78177
78903
  }
78178
- }
78179
- } catch (readError) {
78180
- if (readError.code !== "ENOENT") {
78181
- safeWarn(`[phase_complete] Drift verifier evidence unreadable:`, readError);
78182
- }
78183
- driftVerdictFound = false;
78184
- }
78185
- if (!driftVerdictFound) {
78186
- const specPath = path81.join(dir, ".swarm", "spec.md");
78187
- const specExists = fs65.existsSync(specPath);
78188
- if (!specExists) {
78189
- let incompleteTaskCount = 0;
78190
- let planPhaseFound = false;
78191
- try {
78192
- const planPath = validateSwarmPath(dir, "plan.json");
78193
- const planRaw = fs65.readFileSync(planPath, "utf-8");
78194
- const plan = JSON.parse(planRaw);
78195
- const targetPhase = plan.phases.find((p) => p.id === phase);
78196
- if (targetPhase) {
78197
- planPhaseFound = true;
78198
- incompleteTaskCount = targetPhase.tasks.filter((t) => t.status !== "completed").length;
78199
- }
78200
- } catch {}
78201
- if (incompleteTaskCount > 0 || !planPhaseFound) {
78202
- warnings.push(`No spec.md found and drift verification evidence missing. Phase ${phase} has ${incompleteTaskCount} incomplete task(s) in plan.json — consider running critic_drift_verifier before phase completion.`);
78203
- } else {
78204
- warnings.push(`No spec.md found. Phase ${phase} tasks are all completed in plan.json. Drift verification was skipped.`);
78904
+ if (!driftVerdictApproved && driftVerdictFound) {
78905
+ return JSON.stringify({
78906
+ success: false,
78907
+ phase,
78908
+ status: "blocked",
78909
+ reason: "DRIFT_VERIFICATION_REJECTED",
78910
+ message: `Phase ${phase} cannot be completed: drift verifier verdict is not approved.`,
78911
+ agentsDispatched,
78912
+ agentsMissing: [],
78913
+ warnings: []
78914
+ }, null, 2);
78205
78915
  }
78206
- } else {
78916
+ } catch (driftError) {
78207
78917
  return JSON.stringify({
78208
78918
  success: false,
78209
78919
  phase,
78210
78920
  status: "blocked",
78211
- reason: "DRIFT_VERIFICATION_MISSING",
78212
- message: `Phase ${phase} cannot be completed: drift verifier evidence not found at .swarm/evidence/${phase}/drift-verifier.json. Run drift verification before completing the phase.`,
78921
+ reason: "DRIFT_VERIFICATION_ERROR",
78922
+ message: `Phase ${phase} cannot be completed: drift verification encountered an error: ${driftError instanceof Error ? driftError.message : String(driftError)}. This is a hard block — resolve the error before completing the phase.`,
78213
78923
  agentsDispatched,
78214
78924
  agentsMissing: [],
78215
78925
  warnings: []
78216
78926
  }, null, 2);
78217
78927
  }
78218
78928
  }
78219
- if (!driftVerdictApproved && driftVerdictFound) {
78220
- return JSON.stringify({
78221
- success: false,
78222
- phase,
78223
- status: "blocked",
78224
- reason: "DRIFT_VERIFICATION_REJECTED",
78225
- message: `Phase ${phase} cannot be completed: drift verifier verdict is not approved.`,
78226
- agentsDispatched,
78227
- agentsMissing: [],
78228
- warnings: []
78229
- }, null, 2);
78230
- }
78231
- } catch (driftError) {
78232
- safeWarn(`[phase_complete] Drift verifier error (non-blocking):`, driftError);
78233
78929
  }
78234
78930
  try {
78235
78931
  const plan = await loadPlan(dir);
@@ -78375,6 +79071,210 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
78375
79071
  } catch (mgError) {
78376
79072
  safeWarn(`[phase_complete] Mutation gate error (non-blocking):`, mgError);
78377
79073
  }
79074
+ let councilModeEnabled = false;
79075
+ try {
79076
+ const plan = await loadPlan(dir);
79077
+ if (plan) {
79078
+ const planId = `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
79079
+ const profile = getProfile(dir, planId);
79080
+ if (profile) {
79081
+ const session2 = sessionID ? swarmState.agentSessions.get(sessionID) : undefined;
79082
+ const overrides = session2?.qaGateSessionOverrides ?? {};
79083
+ const effective = getEffectiveGates(profile, overrides);
79084
+ if (effective.council_mode === true) {
79085
+ councilModeEnabled = true;
79086
+ const pcPath = path81.join(dir, ".swarm", "evidence", String(phase), "phase-council.json");
79087
+ let pcVerdictFound = false;
79088
+ let _pcVerdict;
79089
+ let pcQuorumSize;
79090
+ let pcTimestamp;
79091
+ let pcPhaseNumber;
79092
+ try {
79093
+ const pcContent = fs65.readFileSync(pcPath, "utf-8");
79094
+ const pcBundle = JSON.parse(pcContent);
79095
+ for (const entry of pcBundle.entries ?? []) {
79096
+ if (typeof entry.type === "string" && entry.type === "phase-council" && typeof entry.verdict === "string") {
79097
+ pcVerdictFound = true;
79098
+ _pcVerdict = entry.verdict;
79099
+ pcQuorumSize = typeof entry.quorumSize === "number" ? entry.quorumSize : undefined;
79100
+ pcTimestamp = typeof entry.timestamp === "string" ? entry.timestamp : undefined;
79101
+ pcPhaseNumber = typeof entry.phase_number === "number" ? entry.phase_number : typeof entry.phase === "number" ? entry.phase : undefined;
79102
+ const now2 = new Date;
79103
+ const pcTime = pcTimestamp ? new Date(pcTimestamp) : null;
79104
+ if (!pcTime || Number.isNaN(pcTime.getTime())) {
79105
+ return JSON.stringify({
79106
+ success: false,
79107
+ phase,
79108
+ status: "blocked",
79109
+ reason: "PHASE_COUNCIL_INVALID_TIMESTAMP",
79110
+ message: `Phase ${phase} cannot be completed: phase council evidence has missing or invalid timestamp.`,
79111
+ agentsDispatched,
79112
+ agentsMissing: [],
79113
+ warnings: []
79114
+ }, null, 2);
79115
+ }
79116
+ const maxAge = 24 * 60 * 60 * 1000;
79117
+ if (pcTime.getTime() > now2.getTime()) {
79118
+ return JSON.stringify({
79119
+ success: false,
79120
+ phase,
79121
+ status: "blocked",
79122
+ reason: "PHASE_COUNCIL_FUTURE_TIMESTAMP",
79123
+ message: `Phase ${phase} cannot be completed: phase council evidence timestamp is in the future.`,
79124
+ agentsDispatched,
79125
+ agentsMissing: [],
79126
+ warnings: []
79127
+ }, null, 2);
79128
+ }
79129
+ if (now2.getTime() - pcTime.getTime() > maxAge) {
79130
+ return JSON.stringify({
79131
+ success: false,
79132
+ phase,
79133
+ status: "blocked",
79134
+ reason: "PHASE_COUNCIL_STALE_EVIDENCE",
79135
+ message: `Phase ${phase} cannot be completed: phase council evidence is older than 24 hours. Re-convene council for fresh review.`,
79136
+ agentsDispatched,
79137
+ agentsMissing: [],
79138
+ warnings: []
79139
+ }, null, 2);
79140
+ }
79141
+ if (entry.verdict === "REJECT" || entry.verdict === "reject") {
79142
+ const requiredFixes = entry.requiredFixes ?? entry.required_fixes ?? [];
79143
+ const fixesDetail = Array.isArray(requiredFixes) && requiredFixes.length > 0 ? `
79144
+ Required fixes: ${requiredFixes.map((f) => f.detail ?? JSON.stringify(f)).join("; ")}` : "";
79145
+ return JSON.stringify({
79146
+ success: false,
79147
+ phase,
79148
+ status: "blocked",
79149
+ reason: "PHASE_COUNCIL_REJECTED",
79150
+ message: `Phase ${phase} cannot be completed: phase council returned verdict 'REJECT'. Address the required fixes before completing the phase.${fixesDetail}`,
79151
+ agentsDispatched,
79152
+ agentsMissing: [],
79153
+ warnings: []
79154
+ }, null, 2);
79155
+ }
79156
+ if (entry.verdict === "CONCERNS" || entry.verdict === "concerns") {
79157
+ const phaseConcernsAllow = config3.council?.phaseConcernsAllowComplete ?? true;
79158
+ if (!phaseConcernsAllow) {
79159
+ const advisoryNotes = entry.advisoryNotes ?? entry.advisory_notes ?? [];
79160
+ const notesDetail = Array.isArray(advisoryNotes) && advisoryNotes.length > 0 ? `
79161
+ Advisory notes: ${advisoryNotes.join("; ")}` : "";
79162
+ return JSON.stringify({
79163
+ success: false,
79164
+ phase,
79165
+ status: "blocked",
79166
+ reason: "PHASE_COUNCIL_CONCERNS",
79167
+ message: `Phase ${phase} cannot be completed: phase council returned verdict 'CONCERNS'.${notesDetail}`,
79168
+ agentsDispatched,
79169
+ agentsMissing: [],
79170
+ warnings: []
79171
+ }, null, 2);
79172
+ }
79173
+ safeWarn(`[phase_complete] Phase council returned CONCERNS for phase ${phase} — proceeding (phaseConcernsAllowComplete is enabled)`, undefined);
79174
+ }
79175
+ if (entry.verdict !== "APPROVE" && entry.verdict !== "approve" && entry.verdict !== "CONCERNS" && entry.verdict !== "concerns") {
79176
+ return JSON.stringify({
79177
+ success: false,
79178
+ phase,
79179
+ status: "blocked",
79180
+ reason: "PHASE_COUNCIL_INVALID",
79181
+ message: `Phase ${phase} cannot be completed: phase council evidence contains unrecognized verdict '${entry.verdict}'. Expected one of: APPROVE, CONCERNS, REJECT.`,
79182
+ agentsDispatched,
79183
+ agentsMissing: [],
79184
+ warnings: []
79185
+ }, null, 2);
79186
+ }
79187
+ }
79188
+ }
79189
+ } catch (readErr) {
79190
+ if (readErr.code !== "ENOENT") {
79191
+ safeWarn(`[phase_complete] Phase council evidence unreadable:`, readErr);
79192
+ }
79193
+ pcVerdictFound = false;
79194
+ }
79195
+ if (!pcVerdictFound) {
79196
+ return JSON.stringify({
79197
+ success: false,
79198
+ phase,
79199
+ status: "blocked",
79200
+ reason: "PHASE_COUNCIL_REQUIRED",
79201
+ phase_council_required: true,
79202
+ message: `Phase ${phase} cannot be completed: council_mode is enabled and phase council evidence not found at .swarm/evidence/${phase}/phase-council.json. Convene a phase-level council (dispatch 5 members, collect verdicts, call submit_council_verdicts) before completing the phase.`,
79203
+ agentsDispatched,
79204
+ agentsMissing: [],
79205
+ warnings: [
79206
+ `Phase council required — convene 5 council members (critic, reviewer, sme, test_engineer, explorer) for holistic phase review. Call submit_council_verdicts to synthesize verdicts and write phase-council.json evidence.`
79207
+ ]
79208
+ }, null, 2);
79209
+ }
79210
+ if (pcQuorumSize === undefined || typeof pcQuorumSize !== "number") {
79211
+ return JSON.stringify({
79212
+ success: false,
79213
+ phase,
79214
+ status: "blocked",
79215
+ reason: "PHASE_COUNCIL_MISSING_QUORUM",
79216
+ message: `Phase ${phase} cannot be completed: phase council evidence is missing quorumSize field.`,
79217
+ agentsDispatched,
79218
+ agentsMissing: [],
79219
+ warnings: []
79220
+ }, null, 2);
79221
+ }
79222
+ if (pcQuorumSize < 3) {
79223
+ return JSON.stringify({
79224
+ success: false,
79225
+ phase,
79226
+ status: "blocked",
79227
+ reason: "PHASE_COUNCIL_INSUFFICIENT_QUORUM",
79228
+ message: `Phase ${phase} cannot be completed: phase council quorum (${pcQuorumSize}) is below minimum (3). Re-convene council with sufficient members.`,
79229
+ agentsDispatched,
79230
+ agentsMissing: [],
79231
+ warnings: []
79232
+ }, null, 2);
79233
+ }
79234
+ if (pcPhaseNumber === undefined || typeof pcPhaseNumber !== "number") {
79235
+ return JSON.stringify({
79236
+ success: false,
79237
+ phase,
79238
+ status: "blocked",
79239
+ reason: "PHASE_COUNCIL_MISSING_PHASE",
79240
+ message: `Phase ${phase} cannot be completed: phase council evidence is missing phase_number field.`,
79241
+ agentsDispatched,
79242
+ agentsMissing: [],
79243
+ warnings: []
79244
+ }, null, 2);
79245
+ }
79246
+ if (pcPhaseNumber !== phase) {
79247
+ return JSON.stringify({
79248
+ success: false,
79249
+ phase,
79250
+ status: "blocked",
79251
+ reason: "PHASE_COUNCIL_PHASE_MISMATCH",
79252
+ message: `Phase ${phase} cannot be completed: phase council evidence is for phase ${pcPhaseNumber}, not phase ${phase}. Run council for the correct phase.`,
79253
+ agentsDispatched,
79254
+ agentsMissing: [],
79255
+ warnings: []
79256
+ }, null, 2);
79257
+ }
79258
+ }
79259
+ }
79260
+ }
79261
+ } catch (pcError) {
79262
+ if (councilModeEnabled) {
79263
+ warnings.push(`PHASE_COUNCIL_ERROR: ${String(pcError)}`);
79264
+ return JSON.stringify({
79265
+ success: false,
79266
+ phase,
79267
+ status: "blocked",
79268
+ reason: "PHASE_COUNCIL_ERROR",
79269
+ message: `Phase ${phase} cannot be completed: phase council gate encountered an error when council_mode was enabled. Error: ${String(pcError)}`,
79270
+ agentsDispatched,
79271
+ agentsMissing: [],
79272
+ warnings: [`PHASE_COUNCIL_ERROR: ${String(pcError)}`]
79273
+ }, null, 2);
79274
+ } else {
79275
+ safeWarn(`[phase_complete] Phase council gate error (non-blocking):`, pcError);
79276
+ }
79277
+ }
78378
79278
  }
78379
79279
  let knowledgeConfig;
78380
79280
  try {
@@ -78559,7 +79459,7 @@ async function executePhaseComplete(args2, workingDirectory, directory) {
78559
79459
  const lockTaskId = `phase-complete-${Date.now()}`;
78560
79460
  const eventsFilePath = "events.jsonl";
78561
79461
  let agentName = "phase-complete";
78562
- for (const [, agent] of swarmState.activeAgent) {
79462
+ for (const [, agent] of swarmState?.activeAgent ?? []) {
78563
79463
  agentName = agent;
78564
79464
  break;
78565
79465
  }
@@ -85717,7 +86617,8 @@ async function executeSetQaGates(args2, directory) {
85717
86617
  "hallucination_guard",
85718
86618
  "sast_enabled",
85719
86619
  "mutation_test",
85720
- "council_general_review"
86620
+ "council_general_review",
86621
+ "drift_check"
85721
86622
  ]) {
85722
86623
  if (args2[key] !== undefined)
85723
86624
  partial3[key] = args2[key];
@@ -85764,6 +86665,7 @@ var set_qa_gates = createSwarmTool({
85764
86665
  sast_enabled: exports_external.boolean().optional().describe("Enable SAST scanning as a required QA gate."),
85765
86666
  mutation_test: exports_external.boolean().optional().describe("Enable the mutation-testing gate (default: off). Requires mutation " + "tests to achieve a passing kill rate before phase completion; " + "WARN verdict allows advancement, FAIL blocks."),
85766
86667
  council_general_review: exports_external.boolean().optional().describe("Enable the council_general_review gate (default: off). When on, " + "MODE: SPECIFY runs convene_general_council on the draft spec " + "before the critic-gate, folding multi-model deliberation into " + "the spec. Requires council.general.enabled and a search API key."),
86668
+ drift_check: exports_external.boolean().optional().describe("Enable drift verification gate (default: on). Blocks phase_complete " + "until drift-verifier.json has an approved verdict. When disabled, " + "drift verification is skipped entirely."),
85767
86669
  project_type: exports_external.string().optional().describe('Project type label (e.g. "ts", "python"). Only applied when the profile is being created for the first time.')
85768
86670
  },
85769
86671
  execute: async (args2, directory) => {