opencode-swarm 6.67.0 → 6.68.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
@@ -79,7 +79,9 @@ var init_tool_names = __esm(() => {
79
79
  "suggest_patch",
80
80
  "req_coverage",
81
81
  "get_approved_plan",
82
- "repo_map"
82
+ "repo_map",
83
+ "get_qa_gate_profile",
84
+ "set_qa_gates"
83
85
  ];
84
86
  TOOL_NAME_SET = new Set(TOOL_NAMES);
85
87
  });
@@ -217,7 +219,9 @@ var init_constants = __esm(() => {
217
219
  "knowledge_remove",
218
220
  "co_change_analyzer",
219
221
  "suggest_patch",
220
- "repo_map"
222
+ "repo_map",
223
+ "get_qa_gate_profile",
224
+ "set_qa_gates"
221
225
  ],
222
226
  explorer: [
223
227
  "complexity_hotspots",
@@ -409,7 +413,9 @@ var init_constants = __esm(() => {
409
413
  suggest_patch: "Reviewer-safe structured patch suggestion tool. Produces context-anchored patch artifacts without file modification. Returns structured diagnostics on context mismatch.",
410
414
  lint_spec: "validate .swarm/spec.md format and required fields",
411
415
  get_approved_plan: "retrieve the last critic-approved immutable plan snapshot for baseline drift comparison",
412
- repo_map: "query the repo code graph: importers, dependencies, blast radius, and localization context for structural awareness before refactoring"
416
+ repo_map: "query the repo code graph: importers, dependencies, blast radius, and localization context for structural awareness before refactoring",
417
+ get_qa_gate_profile: "retrieve the QA gate profile for the current plan: gates, lock state, and profile hash. Read-only.",
418
+ set_qa_gates: "configure the QA gate profile for the current plan. Architect-only. Ratchet-tighter only \u2014 rejected once the profile is locked after critic approval."
413
419
  };
414
420
  for (const [agentName, tools] of Object.entries(AGENT_TOOL_MAP)) {
415
421
  const invalidTools = tools.filter((tool) => !TOOL_NAME_SET.has(tool));
@@ -17692,6 +17698,7 @@ function startAgentSession(sessionId, agentName, staleDurationMs = 7200000, dire
17692
17698
  scopeViolationDetected: false,
17693
17699
  modifiedFilesThisCoderTask: [],
17694
17700
  turboMode: false,
17701
+ qaGateSessionOverrides: {},
17695
17702
  fullAutoMode: false,
17696
17703
  fullAutoInteractionCount: 0,
17697
17704
  fullAutoDeadlockCount: 0,
@@ -17817,6 +17824,9 @@ function ensureAgentSession(sessionId, agentName, directory) {
17817
17824
  if (session.turboMode === undefined) {
17818
17825
  session.turboMode = false;
17819
17826
  }
17827
+ if (session.qaGateSessionOverrides === undefined) {
17828
+ session.qaGateSessionOverrides = {};
17829
+ }
17820
17830
  if (session.model_fallback_index === undefined) {
17821
17831
  session.model_fallback_index = 0;
17822
17832
  }
@@ -35284,7 +35294,7 @@ function checkAgentToolMapAlignment(registeredKeys) {
35284
35294
  id: `agent-tool-map-mismatch-${agentName}-${toolName}`,
35285
35295
  title: "AGENT_TOOL_MAP alignment gap",
35286
35296
  description: `Tool "${toolName}" is assigned to agent "${agentName}" in AGENT_TOOL_MAP but is not registered in the plugin's tool: {} block. The agent will not be able to use this tool.`,
35287
- severity: "warn",
35297
+ severity: "error",
35288
35298
  path: `AGENT_TOOL_MAP.${agentName}`,
35289
35299
  currentValue: toolName,
35290
35300
  autoFixable: false
@@ -38750,12 +38760,12 @@ __export(exports_evidence_summary_integration, {
38750
38760
  createEvidenceSummaryIntegration: () => createEvidenceSummaryIntegration,
38751
38761
  EvidenceSummaryIntegration: () => EvidenceSummaryIntegration
38752
38762
  });
38753
- import { existsSync as existsSync21, mkdirSync as mkdirSync10, writeFileSync as writeFileSync5 } from "fs";
38763
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, writeFileSync as writeFileSync5 } from "fs";
38754
38764
  import * as path36 from "path";
38755
38765
  function persistSummary(swarmDir, artifact, filename) {
38756
38766
  const swarmPath = path36.join(swarmDir, ".swarm");
38757
- if (!existsSync21(swarmPath)) {
38758
- mkdirSync10(swarmPath, { recursive: true });
38767
+ if (!existsSync22(swarmPath)) {
38768
+ mkdirSync11(swarmPath, { recursive: true });
38759
38769
  }
38760
38770
  const artifactPath = path36.join(swarmPath, filename);
38761
38771
  const content = JSON.stringify(artifact, null, 2);
@@ -41023,7 +41033,7 @@ __export(exports_gate_evidence, {
41023
41033
  deriveRequiredGates: () => deriveRequiredGates,
41024
41034
  DEFAULT_REQUIRED_GATES: () => DEFAULT_REQUIRED_GATES
41025
41035
  });
41026
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync18, renameSync as renameSync10, unlinkSync as unlinkSync5 } from "fs";
41036
+ import { mkdirSync as mkdirSync13, readFileSync as readFileSync18, renameSync as renameSync10, unlinkSync as unlinkSync5 } from "fs";
41027
41037
  import * as path40 from "path";
41028
41038
  function isValidTaskId2(taskId) {
41029
41039
  return isStrictTaskId(taskId);
@@ -41088,7 +41098,7 @@ async function recordGateEvidence(directory, taskId, gate, sessionId, turbo) {
41088
41098
  assertValidTaskId(taskId);
41089
41099
  const evidenceDir = getEvidenceDir(directory);
41090
41100
  const evidencePath = getEvidencePath(directory, taskId);
41091
- mkdirSync12(evidenceDir, { recursive: true });
41101
+ mkdirSync13(evidenceDir, { recursive: true });
41092
41102
  const existing = readExisting(evidencePath);
41093
41103
  const requiredGates = existing ? expandRequiredGates(existing.required_gates, gate) : deriveRequiredGates(gate);
41094
41104
  const updated = {
@@ -41111,7 +41121,7 @@ async function recordAgentDispatch(directory, taskId, agentType, turbo) {
41111
41121
  assertValidTaskId(taskId);
41112
41122
  const evidenceDir = getEvidenceDir(directory);
41113
41123
  const evidencePath = getEvidencePath(directory, taskId);
41114
- mkdirSync12(evidenceDir, { recursive: true });
41124
+ mkdirSync13(evidenceDir, { recursive: true });
41115
41125
  const existing = readExisting(evidencePath);
41116
41126
  const requiredGates = existing ? expandRequiredGates(existing.required_gates, agentType) : deriveRequiredGates(agentType);
41117
41127
  const updated = {
@@ -43534,8 +43544,8 @@ ${JSON.stringify(symbolNames, null, 2)}`);
43534
43544
  var moduleRtn;
43535
43545
  var Module = moduleArg;
43536
43546
  var readyPromiseResolve, readyPromiseReject;
43537
- var readyPromise = new Promise((resolve26, reject) => {
43538
- readyPromiseResolve = resolve26;
43547
+ var readyPromise = new Promise((resolve27, reject) => {
43548
+ readyPromiseResolve = resolve27;
43539
43549
  readyPromiseReject = reject;
43540
43550
  });
43541
43551
  var ENVIRONMENT_IS_WEB = typeof window == "object";
@@ -43615,13 +43625,13 @@ ${JSON.stringify(symbolNames, null, 2)}`);
43615
43625
  }
43616
43626
  readAsync = /* @__PURE__ */ __name(async (url3) => {
43617
43627
  if (isFileURI(url3)) {
43618
- return new Promise((resolve26, reject) => {
43628
+ return new Promise((resolve27, reject) => {
43619
43629
  var xhr = new XMLHttpRequest;
43620
43630
  xhr.open("GET", url3, true);
43621
43631
  xhr.responseType = "arraybuffer";
43622
43632
  xhr.onload = () => {
43623
43633
  if (xhr.status == 200 || xhr.status == 0 && xhr.response) {
43624
- resolve26(xhr.response);
43634
+ resolve27(xhr.response);
43625
43635
  return;
43626
43636
  }
43627
43637
  reject(xhr.status);
@@ -43841,10 +43851,10 @@ ${JSON.stringify(symbolNames, null, 2)}`);
43841
43851
  __name(receiveInstantiationResult, "receiveInstantiationResult");
43842
43852
  var info2 = getWasmImports();
43843
43853
  if (Module["instantiateWasm"]) {
43844
- return new Promise((resolve26, reject) => {
43854
+ return new Promise((resolve27, reject) => {
43845
43855
  Module["instantiateWasm"](info2, (mod, inst) => {
43846
43856
  receiveInstance(mod, inst);
43847
- resolve26(mod.exports);
43857
+ resolve27(mod.exports);
43848
43858
  });
43849
43859
  });
43850
43860
  }
@@ -45364,8 +45374,8 @@ async function loadGrammar(languageId) {
45364
45374
  const parser = new Parser;
45365
45375
  const wasmFileName = getWasmFileName(normalizedId);
45366
45376
  const wasmPath = path66.join(getGrammarsDirAbsolute(), wasmFileName);
45367
- const { existsSync: existsSync39 } = await import("fs");
45368
- if (!existsSync39(wasmPath)) {
45377
+ const { existsSync: existsSync40 } = await import("fs");
45378
+ if (!existsSync40(wasmPath)) {
45369
45379
  throw new Error(`Grammar file not found for ${languageId}: ${wasmPath}
45370
45380
  Make sure to run 'bun run build' to copy grammar files to dist/lang/grammars/`);
45371
45381
  }
@@ -45981,6 +45991,24 @@ async function handleBenchmarkCommand(directory, args2) {
45981
45991
  `);
45982
45992
  }
45983
45993
 
45994
+ // src/commands/brainstorm.ts
45995
+ function sanitizeTopic(raw) {
45996
+ const collapsed = raw.replace(/\s+/g, " ").trim();
45997
+ const stripped = collapsed.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
45998
+ const normalized = stripped.replace(/\s+/g, " ").trim();
45999
+ const MAX_TOPIC_LEN = 2000;
46000
+ if (normalized.length <= MAX_TOPIC_LEN)
46001
+ return normalized;
46002
+ return `${normalized.slice(0, MAX_TOPIC_LEN)}\u2026`;
46003
+ }
46004
+ async function handleBrainstormCommand(_directory, args2) {
46005
+ const description = sanitizeTopic(args2.join(" "));
46006
+ if (description) {
46007
+ return `[MODE: BRAINSTORM] ${description}`;
46008
+ }
46009
+ return "[MODE: BRAINSTORM] Please enter MODE: BRAINSTORM and begin the structured brainstorm workflow (CONTEXT SCAN \u2192 DIALOGUE \u2192 APPROACHES \u2192 DESIGN SECTIONS \u2192 SPEC WRITE + SELF-REVIEW \u2192 QA GATE SELECTION \u2192 TRANSITION).";
46010
+ }
46011
+
45984
46012
  // src/commands/checkpoint.ts
45985
46013
  init_zod();
45986
46014
  init_checkpoint();
@@ -50015,6 +50043,11 @@ function formatToolDoctorMarkdown(result) {
50015
50043
  }
50016
50044
  lines.push("");
50017
50045
  }
50046
+ if (result.summary.error > 0) {
50047
+ lines.push("---", "");
50048
+ lines.push(`**BLOCKING**: ${result.summary.error} error-severity finding(s) must be resolved before release. ` + `AGENT_TOOL_MAP alignment errors mean an agent's system prompt instructs the model to call a tool that opencode has not registered \u2014 the agent's workflow will silently fail at runtime.`);
50049
+ lines.push("");
50050
+ }
50018
50051
  }
50019
50052
  return lines.join(`
50020
50053
  `);
@@ -51492,6 +51525,341 @@ async function handlePromoteCommand(directory, args2) {
51492
51525
  }
51493
51526
  }
51494
51527
 
51528
+ // src/db/qa-gate-profile.ts
51529
+ import { createHash as createHash4 } from "crypto";
51530
+
51531
+ // src/db/project-db.ts
51532
+ import { Database } from "bun:sqlite";
51533
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9 } from "fs";
51534
+ import { join as join26, resolve as resolve11 } from "path";
51535
+ var MIGRATIONS = [
51536
+ {
51537
+ version: 1,
51538
+ name: "create_project_constraints",
51539
+ sql: `CREATE TABLE project_constraints (
51540
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
51541
+ constraint_type TEXT NOT NULL,
51542
+ content TEXT NOT NULL,
51543
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
51544
+ )`
51545
+ },
51546
+ {
51547
+ version: 2,
51548
+ name: "create_qa_gate_profile",
51549
+ sql: `CREATE TABLE qa_gate_profile (
51550
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
51551
+ plan_id TEXT NOT NULL UNIQUE,
51552
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
51553
+ project_type TEXT,
51554
+ gates TEXT NOT NULL DEFAULT '{}',
51555
+ locked_at TEXT,
51556
+ locked_by_snapshot_seq INTEGER
51557
+ )`
51558
+ },
51559
+ {
51560
+ version: 3,
51561
+ name: "create_qa_gate_profile_immutability_trigger",
51562
+ sql: `CREATE TRIGGER IF NOT EXISTS trg_qa_gate_profile_no_update_after_lock
51563
+ BEFORE UPDATE ON qa_gate_profile
51564
+ WHEN OLD.locked_at IS NOT NULL
51565
+ BEGIN
51566
+ SELECT RAISE(ABORT, 'qa_gate_profile row is locked and cannot be modified after critic approval');
51567
+ END`
51568
+ }
51569
+ ];
51570
+ var _projectDbs = new Map;
51571
+ function runProjectMigrations(db) {
51572
+ db.run(`CREATE TABLE IF NOT EXISTS schema_migrations (
51573
+ version INTEGER PRIMARY KEY,
51574
+ name TEXT NOT NULL,
51575
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
51576
+ )`);
51577
+ const row = db.query("SELECT MAX(version) as version FROM schema_migrations").get();
51578
+ const currentVersion = row?.version ?? 0;
51579
+ for (const migration of MIGRATIONS) {
51580
+ if (migration.version <= currentVersion)
51581
+ continue;
51582
+ const apply = db.transaction(() => {
51583
+ db.run(migration.sql);
51584
+ db.run("INSERT INTO schema_migrations (version, name) VALUES (?, ?)", [
51585
+ migration.version,
51586
+ migration.name
51587
+ ]);
51588
+ });
51589
+ apply();
51590
+ }
51591
+ }
51592
+ function projectDbPath(directory) {
51593
+ return join26(resolve11(directory), ".swarm", "swarm.db");
51594
+ }
51595
+ function projectDbExists(directory) {
51596
+ return existsSync18(projectDbPath(directory));
51597
+ }
51598
+ function getProjectDb(directory) {
51599
+ const key = resolve11(directory);
51600
+ const existing = _projectDbs.get(key);
51601
+ if (existing)
51602
+ return existing;
51603
+ const swarmDir = join26(key, ".swarm");
51604
+ mkdirSync9(swarmDir, { recursive: true });
51605
+ const db = new Database(join26(swarmDir, "swarm.db"));
51606
+ db.run("PRAGMA journal_mode = WAL;");
51607
+ db.run("PRAGMA synchronous = NORMAL;");
51608
+ db.run("PRAGMA busy_timeout = 5000;");
51609
+ db.run("PRAGMA foreign_keys = ON;");
51610
+ runProjectMigrations(db);
51611
+ _projectDbs.set(key, db);
51612
+ return db;
51613
+ }
51614
+
51615
+ // src/db/qa-gate-profile.ts
51616
+ var DEFAULT_QA_GATES = {
51617
+ reviewer: true,
51618
+ test_engineer: true,
51619
+ council_mode: false,
51620
+ sme_enabled: true,
51621
+ critic_pre_plan: true,
51622
+ hallucination_guard: false,
51623
+ sast_enabled: true
51624
+ };
51625
+ function rowToProfile(row) {
51626
+ let parsed = {};
51627
+ try {
51628
+ parsed = JSON.parse(row.gates);
51629
+ } catch {
51630
+ parsed = {};
51631
+ }
51632
+ const gates = { ...DEFAULT_QA_GATES, ...parsed };
51633
+ return {
51634
+ id: row.id,
51635
+ plan_id: row.plan_id,
51636
+ created_at: row.created_at,
51637
+ project_type: row.project_type,
51638
+ gates,
51639
+ locked_at: row.locked_at,
51640
+ locked_by_snapshot_seq: row.locked_by_snapshot_seq
51641
+ };
51642
+ }
51643
+ function getProfile(directory, planId) {
51644
+ if (!projectDbExists(directory))
51645
+ return null;
51646
+ const db = getProjectDb(directory);
51647
+ const row = db.query("SELECT * FROM qa_gate_profile WHERE plan_id = ?").get(planId);
51648
+ return row ? rowToProfile(row) : null;
51649
+ }
51650
+ function getOrCreateProfile(directory, planId, projectType) {
51651
+ const existing = getProfile(directory, planId);
51652
+ if (existing)
51653
+ return existing;
51654
+ const db = getProjectDb(directory);
51655
+ const gatesJson = JSON.stringify(DEFAULT_QA_GATES);
51656
+ const insert = db.transaction(() => {
51657
+ db.run("INSERT INTO qa_gate_profile (plan_id, project_type, gates) VALUES (?, ?, ?)", [planId, projectType ?? null, gatesJson]);
51658
+ });
51659
+ try {
51660
+ insert();
51661
+ } catch (err2) {
51662
+ const msg = err2 instanceof Error ? err2.message : String(err2);
51663
+ if (!msg.toLowerCase().includes("unique")) {
51664
+ throw err2;
51665
+ }
51666
+ }
51667
+ const after = getProfile(directory, planId);
51668
+ if (!after) {
51669
+ throw new Error(`Failed to create or load QA gate profile for plan_id=${planId}`);
51670
+ }
51671
+ return after;
51672
+ }
51673
+ function setGates(directory, planId, gates) {
51674
+ const current = getProfile(directory, planId);
51675
+ if (!current) {
51676
+ throw new Error(`No QA gate profile found for plan_id=${planId} \u2014 call getOrCreateProfile first`);
51677
+ }
51678
+ if (current.locked_at !== null) {
51679
+ throw new Error("Cannot modify gates: QA gate profile is locked after critic approval");
51680
+ }
51681
+ const merged = { ...current.gates };
51682
+ for (const key of Object.keys(gates)) {
51683
+ const incoming = gates[key];
51684
+ if (incoming === undefined)
51685
+ continue;
51686
+ if (incoming === false && current.gates[key] === true) {
51687
+ throw new Error(`Cannot disable gate '${key}': sessions can only ratchet tighter`);
51688
+ }
51689
+ if (incoming === true) {
51690
+ merged[key] = true;
51691
+ }
51692
+ }
51693
+ const db = getProjectDb(directory);
51694
+ db.run("UPDATE qa_gate_profile SET gates = ? WHERE plan_id = ?", [
51695
+ JSON.stringify(merged),
51696
+ planId
51697
+ ]);
51698
+ const updated = getProfile(directory, planId);
51699
+ if (!updated) {
51700
+ throw new Error(`Failed to re-read QA gate profile after update for plan_id=${planId}`);
51701
+ }
51702
+ return updated;
51703
+ }
51704
+ function lockProfile(directory, planId, snapshotSeq) {
51705
+ const current = getProfile(directory, planId);
51706
+ if (!current) {
51707
+ throw new Error(`No QA gate profile found for plan_id=${planId} \u2014 cannot lock`);
51708
+ }
51709
+ if (current.locked_at !== null) {
51710
+ return current;
51711
+ }
51712
+ const db = getProjectDb(directory);
51713
+ db.run("UPDATE qa_gate_profile SET locked_at = datetime('now'), locked_by_snapshot_seq = ? WHERE plan_id = ?", [snapshotSeq, planId]);
51714
+ const locked = getProfile(directory, planId);
51715
+ if (!locked) {
51716
+ throw new Error(`Failed to re-read locked QA gate profile for plan_id=${planId}`);
51717
+ }
51718
+ return locked;
51719
+ }
51720
+ function computeProfileHash(profile) {
51721
+ const payload = JSON.stringify({
51722
+ plan_id: profile.plan_id,
51723
+ gates: profile.gates
51724
+ });
51725
+ return createHash4("sha256").update(payload).digest("hex");
51726
+ }
51727
+ function getEffectiveGates(profile, sessionOverrides) {
51728
+ const merged = { ...profile.gates };
51729
+ for (const key of Object.keys(sessionOverrides)) {
51730
+ if (sessionOverrides[key] === true) {
51731
+ merged[key] = true;
51732
+ }
51733
+ }
51734
+ return merged;
51735
+ }
51736
+
51737
+ // src/commands/qa-gates.ts
51738
+ init_manager();
51739
+ init_state();
51740
+ var ALL_GATE_NAMES = [
51741
+ "reviewer",
51742
+ "test_engineer",
51743
+ "council_mode",
51744
+ "sme_enabled",
51745
+ "critic_pre_plan",
51746
+ "hallucination_guard",
51747
+ "sast_enabled"
51748
+ ];
51749
+ function derivePlanId(plan) {
51750
+ return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
51751
+ }
51752
+ function isGateName(name2) {
51753
+ return ALL_GATE_NAMES.includes(name2);
51754
+ }
51755
+ function formatGates(gates) {
51756
+ return ALL_GATE_NAMES.map((g) => ` - ${g}: ${gates[g] ? "on" : "off"}`).join(`
51757
+ `);
51758
+ }
51759
+ async function handleQaGatesCommand(directory, args2, sessionID) {
51760
+ const plan = await loadPlanJsonOnly(directory);
51761
+ if (!plan) {
51762
+ return "Error: plan.json not found or invalid. Create a plan first (e.g. /swarm specify or save_plan).";
51763
+ }
51764
+ const planId = derivePlanId(plan);
51765
+ const subcommand = args2[0]?.toLowerCase();
51766
+ const gateArgs = args2.slice(1);
51767
+ if (!subcommand || subcommand === "show" || subcommand === "status") {
51768
+ const profile = getProfile(directory, planId);
51769
+ const spec = profile ? profile.gates : DEFAULT_QA_GATES;
51770
+ const session = sessionID ? getAgentSession(sessionID) : null;
51771
+ const overrides = session?.qaGateSessionOverrides ?? {};
51772
+ const effective = profile ? getEffectiveGates(profile, overrides) : { ...DEFAULT_QA_GATES, ...overrides };
51773
+ const lines = [];
51774
+ lines.push(`QA Gate Profile for plan_id=${planId}`);
51775
+ if (!profile) {
51776
+ lines.push(" (no profile persisted yet \u2014 showing defaults)");
51777
+ } else {
51778
+ lines.push(` locked: ${profile.locked_at ? `yes @ ${profile.locked_at} (seq ${profile.locked_by_snapshot_seq ?? "?"})` : "no"}`);
51779
+ lines.push(` profile_hash: ${computeProfileHash(profile)}`);
51780
+ }
51781
+ lines.push("Spec-level gates:");
51782
+ lines.push(formatGates(spec));
51783
+ lines.push("Session overrides (ratchet-tighter only):");
51784
+ if (Object.keys(overrides).length === 0) {
51785
+ lines.push(" (none)");
51786
+ } else {
51787
+ for (const k of ALL_GATE_NAMES) {
51788
+ if (overrides[k] === true)
51789
+ lines.push(` - ${k}: on (override)`);
51790
+ }
51791
+ }
51792
+ lines.push("Effective gates:");
51793
+ lines.push(formatGates(effective));
51794
+ return lines.join(`
51795
+ `);
51796
+ }
51797
+ if (subcommand === "enable") {
51798
+ if (gateArgs.length === 0) {
51799
+ return "Usage: /swarm qa-gates enable <gate> [<gate> ...]";
51800
+ }
51801
+ const invalid = gateArgs.filter((g) => !isGateName(g));
51802
+ if (invalid.length > 0) {
51803
+ return `Error: unknown gate(s): ${invalid.join(", ")}. Valid gates: ${ALL_GATE_NAMES.join(", ")}`;
51804
+ }
51805
+ getOrCreateProfile(directory, planId);
51806
+ const patch = {};
51807
+ for (const g of gateArgs) {
51808
+ if (isGateName(g))
51809
+ patch[g] = true;
51810
+ }
51811
+ try {
51812
+ const updated = setGates(directory, planId, patch);
51813
+ return [
51814
+ `Enabled gates persisted for plan_id=${planId}:`,
51815
+ formatGates(updated.gates),
51816
+ `profile_hash: ${computeProfileHash(updated)}`
51817
+ ].join(`
51818
+ `);
51819
+ } catch (err2) {
51820
+ const msg = err2 instanceof Error ? err2.message : String(err2);
51821
+ return `Error: ${msg}`;
51822
+ }
51823
+ }
51824
+ if (subcommand === "override") {
51825
+ if (!sessionID) {
51826
+ return "Error: session overrides require an active session context.";
51827
+ }
51828
+ if (gateArgs.length === 0) {
51829
+ return "Usage: /swarm qa-gates override <gate> [<gate> ...]";
51830
+ }
51831
+ const invalid = gateArgs.filter((g) => !isGateName(g));
51832
+ if (invalid.length > 0) {
51833
+ return `Error: unknown gate(s): ${invalid.join(", ")}. Valid gates: ${ALL_GATE_NAMES.join(", ")}`;
51834
+ }
51835
+ const session = getAgentSession(sessionID);
51836
+ if (!session) {
51837
+ return "Error: no active session found for override.";
51838
+ }
51839
+ const current = session.qaGateSessionOverrides ?? {};
51840
+ const next = { ...current };
51841
+ for (const g of gateArgs) {
51842
+ if (isGateName(g))
51843
+ next[g] = true;
51844
+ }
51845
+ session.qaGateSessionOverrides = next;
51846
+ return [
51847
+ `Session overrides updated for plan_id=${planId}:`,
51848
+ Object.keys(next).filter((k) => next[k] === true).map((k) => ` - ${k}: on`).join(`
51849
+ `) || " (none)"
51850
+ ].join(`
51851
+ `);
51852
+ }
51853
+ return [
51854
+ "Usage:",
51855
+ " /swarm qa-gates show current profile + effective gates",
51856
+ " /swarm qa-gates enable <gate>... persist-enable gate(s) (rejected if locked)",
51857
+ " /swarm qa-gates override <gate>... session-only enable (ratchet-tighter)",
51858
+ `Valid gates: ${ALL_GATE_NAMES.join(", ")}`
51859
+ ].join(`
51860
+ `);
51861
+ }
51862
+
51495
51863
  // src/commands/reset.ts
51496
51864
  import * as fs21 from "fs";
51497
51865
 
@@ -51548,13 +51916,13 @@ class CircuitBreaker {
51548
51916
  if (this.config.callTimeoutMs <= 0) {
51549
51917
  return fn();
51550
51918
  }
51551
- return new Promise((resolve11, reject) => {
51919
+ return new Promise((resolve12, reject) => {
51552
51920
  const timeout = setTimeout(() => {
51553
51921
  reject(new Error(`Call timeout after ${this.config.callTimeoutMs}ms`));
51554
51922
  }, this.config.callTimeoutMs);
51555
51923
  fn().then((result) => {
51556
51924
  clearTimeout(timeout);
51557
- resolve11(result);
51925
+ resolve12(result);
51558
51926
  }).catch((error93) => {
51559
51927
  clearTimeout(timeout);
51560
51928
  reject(error93);
@@ -51700,7 +52068,7 @@ init_queue();
51700
52068
  // src/background/worker.ts
51701
52069
  init_event_bus();
51702
52070
  function sleep(ms) {
51703
- return new Promise((resolve11) => setTimeout(resolve11, ms));
52071
+ return new Promise((resolve12) => setTimeout(resolve12, ms));
51704
52072
  }
51705
52073
 
51706
52074
  class WorkerManager {
@@ -52169,7 +52537,7 @@ async function handleResetSessionCommand(directory, _args) {
52169
52537
  // src/summaries/manager.ts
52170
52538
  init_utils2();
52171
52539
  init_utils();
52172
- import { mkdirSync as mkdirSync9, readdirSync as readdirSync9, renameSync as renameSync8, rmSync as rmSync3, statSync as statSync7 } from "fs";
52540
+ import { mkdirSync as mkdirSync10, readdirSync as readdirSync9, renameSync as renameSync8, rmSync as rmSync3, statSync as statSync7 } from "fs";
52173
52541
  import * as path32 from "path";
52174
52542
  var SUMMARY_ID_REGEX = /^S\d+$/;
52175
52543
  function sanitizeSummaryId(id) {
@@ -52216,7 +52584,7 @@ async function storeSummary(directory, id, fullOutput, summaryText, maxStoredByt
52216
52584
  originalBytes: Buffer.byteLength(fullOutput, "utf8")
52217
52585
  };
52218
52586
  const entryJson = JSON.stringify(entry);
52219
- mkdirSync9(summaryDir, { recursive: true });
52587
+ mkdirSync10(summaryDir, { recursive: true });
52220
52588
  const tempPath = path32.join(summaryDir, `${sanitizedId}.json.tmp.${Date.now()}.${process.pid}`);
52221
52589
  try {
52222
52590
  await Bun.write(tempPath, entryJson);
@@ -53370,6 +53738,18 @@ var COMMAND_REGISTRY = {
53370
53738
  description: "Generate or import a feature specification [description]",
53371
53739
  args: "[description-text]"
53372
53740
  },
53741
+ brainstorm: {
53742
+ handler: (ctx) => handleBrainstormCommand(ctx.directory, ctx.args),
53743
+ description: "Enter architect MODE: BRAINSTORM \u2014 structured seven-phase planning workflow [topic]",
53744
+ args: "[topic-text]",
53745
+ details: "Triggers the architect to run the brainstorm workflow: CONTEXT SCAN, single-question DIALOGUE, APPROACHES, DESIGN SECTIONS, SPEC WRITE + SELF-REVIEW, QA GATE SELECTION, TRANSITION. Use for new plans where requirements need to be drawn out before writing spec.md / plan.md."
53746
+ },
53747
+ "qa-gates": {
53748
+ handler: (ctx) => handleQaGatesCommand(ctx.directory, ctx.args, ctx.sessionID),
53749
+ description: "View or modify QA gate profile for the current plan [enable|override <gate>...]",
53750
+ args: "[show|enable|override] <gate>...",
53751
+ 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."
53752
+ },
53373
53753
  promote: {
53374
53754
  handler: (ctx) => handlePromoteCommand(ctx.directory, ctx.args),
53375
53755
  description: "Manually promote lesson to hive knowledge",
@@ -53913,7 +54293,7 @@ OUTPUT: Code scaffold for src/pages/Settings.tsx with component tree, typed prop
53913
54293
  ### MODE DETECTION (Priority Order)
53914
54294
  Evaluate the user's request and context in this exact order \u2014 the FIRST matching rule wins:
53915
54295
 
53916
- 0. **EXPLICIT COMMAND OVERRIDE** \u2014 User explicitly invokes \`/swarm specify\`, \`/swarm clarify\`, or uses the phrases "specify [something about spec/requirements]", "write a spec", "create a spec", "define requirements", "list requirements", "define a feature", "I have requirements" \u2192 Enter MODE: SPECIFY (or MODE: CLARIFY-SPEC if spec.md exists and user says "clarify"). This override fires BEFORE RESUME \u2014 an explicit spec command always wins, even if plan.md has incomplete tasks. Note: bare "specify" in an ambiguous context (e.g., "specify what this does") should resolve via CLARIFY (priority 4) rather than this override \u2014 use context to determine intent.
54296
+ 0. **EXPLICIT COMMAND OVERRIDE** \u2014 User explicitly invokes \`/swarm specify\`, \`/swarm clarify\`, \`/swarm brainstorm\`, or uses the phrases "specify [something about spec/requirements]", "write a spec", "create a spec", "define requirements", "list requirements", "define a feature", "I have requirements", "brainstorm", "let's think through", "think this through with me", "workshop this idea" \u2192 Enter MODE: SPECIFY, MODE: CLARIFY-SPEC, or MODE: BRAINSTORM as appropriate. This override fires BEFORE RESUME \u2014 an explicit spec command always wins, even if plan.md has incomplete tasks. \`/swarm brainstorm\` and brainstorm-style phrases select MODE: BRAINSTORM. Note: bare "specify" in an ambiguous context (e.g., "specify what this does") should resolve via CLARIFY (priority 4) rather than this override \u2014 use context to determine intent.
53917
54297
  1. **RESUME** \u2014 \`.swarm/plan.md\` exists and contains incomplete (unchecked) tasks AND the user has NOT issued an explicit spec command (see priority 0) \u2192 Resume at current task.
53918
54298
  2. **SPECIFY** \u2014 No \`.swarm/spec.md\` exists AND no \`.swarm/plan.md\` exists \u2192 Enter MODE: SPECIFY.
53919
54299
  3. **CLARIFY-SPEC** \u2014 \`.swarm/spec.md\` exists AND contains \`[NEEDS CLARIFICATION]\` markers; OR user explicitly asks to clarify or refine the spec; OR \`/swarm clarify\` is invoked \u2192 Enter MODE: CLARIFY-SPEC.
@@ -53922,12 +54302,72 @@ Evaluate the user's request and context in this exact order \u2014 the FIRST mat
53922
54302
  6. All other modes (CONSULT, PLAN, CRITIC-GATE, EXECUTE, PHASE-WRAP) \u2014 Follow their respective sections below.
53923
54303
 
53924
54304
  PRIORITY RULES:
53925
- - EXPLICIT COMMAND OVERRIDE (priority 0) wins over everything \u2014 an explicit \`/swarm specify\` or \`/swarm clarify\` command, or explicit spec-creation language ("specify", "write a spec", "create a spec", "define requirements", "define a feature") always overrides RESUME.
54305
+ - EXPLICIT COMMAND OVERRIDE (priority 0) wins over everything \u2014 an explicit \`/swarm specify\`, \`/swarm clarify\`, or \`/swarm brainstorm\` command, or explicit spec-creation / brainstorming language ("specify", "write a spec", "create a spec", "define requirements", "define a feature", "brainstorm", "think through with me") always overrides RESUME.
54306
+ - BRAINSTORM is selected via the EXPLICIT COMMAND OVERRIDE when \`/swarm brainstorm\` is invoked or the user asks to "brainstorm" / "think through" / "workshop" a problem before committing to a spec. Use BRAINSTORM when the problem is still fuzzy \u2014 it produces both spec.md and a QA gate profile. Use SPECIFY when requirements are clear enough to write directly.
53926
54307
  - RESUME wins over SPECIFY (priority 2) and all other modes when no explicit spec command is present \u2014 a user continuing existing work is never accidentally routed to SPECIFY.
53927
54308
  - SPECIFY (priority 2) fires only for new projects with no spec and no plan.
53928
54309
  - CLARIFY-SPEC fires between SPECIFY and CLARIFY; it only activates when no explicit spec command is present and no incomplete (unchecked) tasks exist in plan.md \u2014 RESUME takes priority if they do.
53929
54310
  - CLARIFY fires only when user input is genuinely needed (not as a substitute for informed defaults).
53930
54311
 
54312
+ ### MODE: BRAINSTORM
54313
+ Activates when: user invokes \`/swarm brainstorm\`; OR uses phrases like "brainstorm", "let's think through", "think this through with me", "workshop this idea"; OR the problem is fuzzy/exploratory and the user has not yet written (or does not want to directly dictate) a spec.
54314
+
54315
+ Use BRAINSTORM when requirements need to be drawn out through structured dialogue before committing to a spec. Use SPECIFY when the user has already articulated clear requirements.
54316
+
54317
+ MODE: BRAINSTORM runs seven phases in strict order. Do not skip phases. Do not collapse phases. Each phase has a clear entry signal and a clear exit signal.
54318
+
54319
+ **Phase 1: CONTEXT SCAN (architect + explorer, parallel).**
54320
+ - Delegate to \`{{AGENT_PREFIX}}explorer\` to map the relevant portion of the codebase. Scope the explorer to the area most likely affected by the topic.
54321
+ - In parallel, read any existing \`.swarm/spec.md\`, \`.swarm/plan.md\`, and \`.swarm/knowledge.jsonl\` entries that are relevant.
54322
+ - Run CODEBASE REALITY CHECK on any claims the user made in their topic statement. Surface discrepancies before moving forward.
54323
+ - Exit when you have a confident map of: (a) existing code and patterns, (b) relevant prior decisions, (c) what is actually unknown.
54324
+
54325
+ **Phase 2: DIALOGUE (architect \u2194 user).**
54326
+ - Ask EXACTLY ONE focused question per message. Wait for the user's answer before asking the next.
54327
+ - Prioritize questions that materially change scope, risk, or architecture. Skip questions whose answers can be responsibly defaulted \u2014 use informed defaults and say so.
54328
+ - Hard cap: no more than SIX questions total in this phase. Stop sooner if uncertainty has collapsed.
54329
+ - Each question must include: (a) why it matters, (b) the default you will use if the user doesn't answer, (c) the concrete options you're weighing.
54330
+ - Exit when: remaining ambiguity can be defaulted safely, or the user explicitly says "good, move on" or equivalent.
54331
+
54332
+ **Phase 3: APPROACHES (architect, optionally with SME).**
54333
+ - Produce 2-4 distinct candidate approaches. Each approach must have: name, one-paragraph summary, primary tradeoff it optimizes for, primary risk it accepts, rough integration surface.
54334
+ - For high-risk domains (auth, payments, data mutation, public API, schema, concurrency, security-sensitive parsing), delegate to \`{{AGENT_PREFIX}}sme\` for domain research first.
54335
+ - Present the approaches to the user and recommend one with explicit reasoning. The user can pick, modify, or reject.
54336
+ - Exit when the user has chosen (or agreed to your recommended) approach.
54337
+
54338
+ **Phase 4: DESIGN SECTIONS (architect).**
54339
+ - Draft the structural design of the chosen approach. Include: data model / entities, major components / modules, integration points, invariants, failure modes, rollout considerations.
54340
+ - Keep design technology-aware (this is NOT the spec \u2014 BRAINSTORM design notes can reference frameworks and patterns).
54341
+ - Name the design sections explicitly so you can reference them in the spec without duplicating.
54342
+ - Exit with a design outline the user can skim in under two minutes.
54343
+
54344
+ **Phase 5: SPEC WRITE + SELF-REVIEW (architect + reviewer).**
54345
+ - Generate \`.swarm/spec.md\` following the same SPEC CONTENT RULES that MODE: SPECIFY uses: WHAT/WHY only, no tech stack, no implementation details, FR-### / SC-### numbering, Given/When/Then scenarios, \`[NEEDS CLARIFICATION]\` markers (max 3).
54346
+ - Cross-reference design sections by name where relevant context helps (but keep HOW out of the spec).
54347
+ - Delegate to \`{{AGENT_PREFIX}}reviewer\` for an independent review of the draft spec. Reviewer must flag: requirements that encode HOW, untestable requirements, missing edge cases, silent assumptions.
54348
+ - Apply reviewer feedback. If reviewer rejects, iterate once and re-review. After two rounds, surface remaining disagreements to the user.
54349
+ - Write the final spec to \`.swarm/spec.md\`.
54350
+ - Exit when reviewer signs off (or user explicitly accepts remaining disagreements).
54351
+
54352
+ **Phase 6: QA GATE SELECTION (architect).**
54353
+ - Read the current QA gate profile for this plan via \`get_qa_gate_profile\`. If none exists, the tool returns \`success: false, reason: 'no_profile'\` \u2014 this is expected for a new plan.
54354
+ - Based on risk tier of the work (see "High-risk work" list in the quality policy), choose which gates to enable. Default profile enables reviewer, test_engineer, sme_enabled, critic_pre_plan, and sast_enabled. Consider enabling council_mode for high-impact architecture and hallucination_guard for claim-heavy work.
54355
+ - Apply the chosen gates via \`set_qa_gates\`. The tool ratchets tighter only \u2014 it cannot disable gates that are already on. It rejects writes once the profile is locked by critic approval.
54356
+ - Briefly explain to the user which gates you selected and why.
54357
+ - Exit with a QA gate profile persisted for this plan.
54358
+
54359
+ **Phase 7: TRANSITION.**
54360
+ - Summarize: (a) chosen approach, (b) design sections produced, (c) spec written, (d) QA gates selected, (e) remaining \`[NEEDS CLARIFICATION]\` markers.
54361
+ - Offer the user two next steps: \`PLAN\` (go to MODE: PLAN and write plan.md) or \`CLARIFY-SPEC\` (resolve remaining markers first).
54362
+ - Do NOT proceed to PLAN or CLARIFY-SPEC automatically \u2014 wait for user direction.
54363
+
54364
+ BRAINSTORM RULES:
54365
+ - No skipping phases. Each phase's exit condition must be met before moving on.
54366
+ - One question per message in DIALOGUE \u2014 never batch.
54367
+ - Always offer an informed default for every question.
54368
+ - The spec produced in Phase 5 must still satisfy the SPEC CONTENT RULES (no tech stack, no implementation details).
54369
+ - QA gates set in Phase 6 are ratchet-tighter \u2014 you cannot undo them later in the session.
54370
+
53931
54371
  ### MODE: SPECIFY
53932
54372
  Activates when: user asks to "specify", "define requirements", "write a spec", or "define a feature"; OR \`/swarm specify\` is invoked; OR no \`.swarm/spec.md\` exists and no \`.swarm/plan.md\` exists.
53933
54373
 
@@ -54733,16 +55173,17 @@ ${customAppendPrompt}`;
54733
55173
  }
54734
55174
  prompt = prompt?.replace("{{YOUR_TOOLS}}", buildYourToolsList())?.replace("{{AVAILABLE_TOOLS}}", buildAvailableToolsList())?.replace("{{SLASH_COMMANDS}}", buildSlashCommandsList());
54735
55175
  const councilBlock = buildCouncilWorkflow(council);
55176
+ const hasPlaceholder = prompt?.includes("{{COUNCIL_WORKFLOW}}") === true;
54736
55177
  if (councilBlock === "") {
54737
- prompt = prompt?.replace(`
54738
-
54739
- {{COUNCIL_WORKFLOW}}
54740
-
54741
- `, `
55178
+ prompt = prompt?.replace(/\n\n\{\{COUNCIL_WORKFLOW\}\}\n\n/g, `
54742
55179
 
54743
55180
  `);
55181
+ } else if (hasPlaceholder) {
55182
+ prompt = prompt?.replace(/\{\{COUNCIL_WORKFLOW\}\}/g, councilBlock);
54744
55183
  } else {
54745
- prompt = prompt?.replace("{{COUNCIL_WORKFLOW}}", councilBlock);
55184
+ prompt = `${prompt ?? ""}
55185
+
55186
+ ${councilBlock}`;
54746
55187
  }
54747
55188
  const advEnabled = adversarialTesting?.enabled ?? true;
54748
55189
  const advScope = adversarialTesting?.scope ?? "all";
@@ -56494,6 +56935,13 @@ function getAgentConfigs(config3, directory, sessionId) {
56494
56935
  } else {
56495
56936
  allowedTools = AGENT_TOOL_MAP[baseAgentName];
56496
56937
  }
56938
+ if (baseAgentName === "architect" && config3?.council?.enabled === true && override !== undefined) {
56939
+ const required3 = ["declare_council_criteria", "convene_council"];
56940
+ const missing = required3.filter((t) => !override.includes(t));
56941
+ if (missing.length > 0) {
56942
+ throw new Error(`[opencode-swarm] Conflicting config: council.enabled=true but tool_filter.overrides.architect omits ${missing.join(", ")}. ` + `Either set council.enabled=false, remove the architect override entirely to fall back on AGENT_TOOL_MAP, or add the missing council tools to the override. ` + `Refusing to silently override your explicit tool_filter.overrides.architect.`);
56943
+ }
56944
+ }
56497
56945
  if (!allowedTools && !Object.hasOwn(toolFilterOverrides, baseAgentName)) {
56498
56946
  if (!warnedMissingWhitelist.has(baseAgentName)) {
56499
56947
  console.warn(`[getAgentConfigs] Unknown agent '${baseAgentName}', defaulting to minimal toolset.`);
@@ -56801,13 +57249,13 @@ class PlanSyncWorker {
56801
57249
  } catch {}
56802
57250
  }
56803
57251
  withTimeout(promise3, ms, timeoutMessage) {
56804
- return new Promise((resolve12, reject) => {
57252
+ return new Promise((resolve13, reject) => {
56805
57253
  const timer = setTimeout(() => {
56806
57254
  reject(new Error(`${timeoutMessage} (${ms}ms)`));
56807
57255
  }, ms);
56808
57256
  promise3.then((result) => {
56809
57257
  clearTimeout(timer);
56810
- resolve12(result);
57258
+ resolve13(result);
56811
57259
  }).catch((error93) => {
56812
57260
  clearTimeout(timer);
56813
57261
  reject(error93);
@@ -57034,7 +57482,7 @@ ${content.substring(endIndex + 1)}`;
57034
57482
  // src/hooks/compaction-customizer.ts
57035
57483
  init_manager();
57036
57484
  import * as fs27 from "fs";
57037
- import { join as join35 } from "path";
57485
+ import { join as join36 } from "path";
57038
57486
  init_utils2();
57039
57487
  function createCompactionCustomizerHook(config3, directory) {
57040
57488
  const enabled = config3.hooks?.compaction !== false;
@@ -57080,7 +57528,7 @@ function createCompactionCustomizerHook(config3, directory) {
57080
57528
  }
57081
57529
  }
57082
57530
  try {
57083
- const summariesDir = join35(directory, ".swarm", "summaries");
57531
+ const summariesDir = join36(directory, ".swarm", "summaries");
57084
57532
  const files = await fs27.promises.readdir(summariesDir);
57085
57533
  if (files.length > 0) {
57086
57534
  const count = files.length;
@@ -60993,7 +61441,7 @@ import * as path47 from "path";
60993
61441
  init_utils2();
60994
61442
  init_path_security();
60995
61443
  import * as fsSync2 from "fs";
60996
- import { constants as constants2, existsSync as existsSync27, realpathSync as realpathSync6 } from "fs";
61444
+ import { constants as constants2, existsSync as existsSync28, realpathSync as realpathSync6 } from "fs";
60997
61445
  import * as fsPromises3 from "fs/promises";
60998
61446
  import * as path46 from "path";
60999
61447
 
@@ -61455,7 +61903,7 @@ function resolveModuleSpecifier(workspaceRoot, sourceFile, specifier) {
61455
61903
  } catch {
61456
61904
  realRoot = path46.normalize(workspaceRoot);
61457
61905
  }
61458
- if (!existsSync27(resolved)) {
61906
+ if (!existsSync28(resolved)) {
61459
61907
  const EXTENSIONS = [
61460
61908
  ".ts",
61461
61909
  ".tsx",
@@ -61469,7 +61917,7 @@ function resolveModuleSpecifier(workspaceRoot, sourceFile, specifier) {
61469
61917
  let found = null;
61470
61918
  for (const ext of EXTENSIONS) {
61471
61919
  const candidate = resolved + ext;
61472
- if (existsSync27(candidate)) {
61920
+ if (existsSync28(candidate)) {
61473
61921
  found = candidate;
61474
61922
  break;
61475
61923
  }
@@ -61566,7 +62014,7 @@ async function loadGraph(workspace) {
61566
62014
  if (cached3 && !isDirty(normalized)) {
61567
62015
  try {
61568
62016
  const graphPath = getGraphPath(workspace);
61569
- if (existsSync27(graphPath)) {
62017
+ if (existsSync28(graphPath)) {
61570
62018
  const stats = await fsPromises3.stat(graphPath);
61571
62019
  const cachedMtime = mtimeCache.get(normalized);
61572
62020
  if (cachedMtime !== undefined && stats.mtimeMs !== cachedMtime) {
@@ -61583,7 +62031,7 @@ async function loadGraph(workspace) {
61583
62031
  }
61584
62032
  try {
61585
62033
  const graphPath = getGraphPath(workspace);
61586
- if (!existsSync27(graphPath)) {
62034
+ if (!existsSync28(graphPath)) {
61587
62035
  return null;
61588
62036
  }
61589
62037
  const stats = await fsPromises3.stat(graphPath);
@@ -61709,7 +62157,7 @@ async function saveGraph(workspace, graph, options) {
61709
62157
  lastError = error93 instanceof Error ? error93 : new Error(String(error93));
61710
62158
  if (lastError instanceof Error && "code" in lastError && lastError.code === "EEXIST" && retries < WINDOWS_RENAME_MAX_RETRIES - 1) {
61711
62159
  retries++;
61712
- await new Promise((resolve17) => setTimeout(resolve17, WINDOWS_RENAME_RETRY_DELAY_MS));
62160
+ await new Promise((resolve18) => setTimeout(resolve18, WINDOWS_RENAME_RETRY_DELAY_MS));
61713
62161
  continue;
61714
62162
  }
61715
62163
  throw lastError;
@@ -61842,7 +62290,7 @@ function buildWorkspaceGraph(workspaceRoot, options) {
61842
62290
  const maxFileSize = options?.maxFileSizeBytes ?? 1024 * 1024;
61843
62291
  const maxFiles = options?.maxFiles ?? 1e4;
61844
62292
  const absoluteRoot = path46.resolve(workspaceRoot);
61845
- if (!existsSync27(absoluteRoot)) {
62293
+ if (!existsSync28(absoluteRoot)) {
61846
62294
  throw new Error(`Workspace directory does not exist: ${workspaceRoot}`);
61847
62295
  }
61848
62296
  const graph = createEmptyGraph(workspaceRoot);
@@ -61996,7 +62444,7 @@ async function updateGraphForFiles(workspaceRoot, filePaths, options) {
61996
62444
  const updatedPaths = new Set;
61997
62445
  for (const rawFilePath of filePaths) {
61998
62446
  const normalizedPath = normalizeGraphPath(rawFilePath);
61999
- const fileExists = existsSync27(rawFilePath);
62447
+ const fileExists = existsSync28(rawFilePath);
62000
62448
  if (fileExists) {
62001
62449
  graph.edges = graph.edges.filter((e) => normalizeGraphPath(e.source) !== normalizedPath);
62002
62450
  const result = scanFile(rawFilePath, absoluteRoot, maxFileSize);
@@ -62849,26 +63297,26 @@ function pLimit(concurrency) {
62849
63297
  activeCount--;
62850
63298
  resumeNext();
62851
63299
  };
62852
- const run2 = async (function_, resolve18, arguments_2) => {
63300
+ const run2 = async (function_, resolve19, arguments_2) => {
62853
63301
  const result = (async () => function_(...arguments_2))();
62854
- resolve18(result);
63302
+ resolve19(result);
62855
63303
  try {
62856
63304
  await result;
62857
63305
  } catch {}
62858
63306
  next();
62859
63307
  };
62860
- const enqueue = (function_, resolve18, reject, arguments_2) => {
63308
+ const enqueue = (function_, resolve19, reject, arguments_2) => {
62861
63309
  const queueItem = { reject };
62862
63310
  new Promise((internalResolve) => {
62863
63311
  queueItem.run = internalResolve;
62864
63312
  queue.enqueue(queueItem);
62865
- }).then(run2.bind(undefined, function_, resolve18, arguments_2));
63313
+ }).then(run2.bind(undefined, function_, resolve19, arguments_2));
62866
63314
  if (activeCount < concurrency) {
62867
63315
  resumeNext();
62868
63316
  }
62869
63317
  };
62870
- const generator = (function_, ...arguments_2) => new Promise((resolve18, reject) => {
62871
- enqueue(function_, resolve18, reject, arguments_2);
63318
+ const generator = (function_, ...arguments_2) => new Promise((resolve19, reject) => {
63319
+ enqueue(function_, resolve19, reject, arguments_2);
62872
63320
  });
62873
63321
  Object.defineProperties(generator, {
62874
63322
  activeCount: {
@@ -65597,7 +66045,7 @@ import * as path56 from "path";
65597
66045
  import * as child_process5 from "child_process";
65598
66046
  var WIN32_CMD_BINARIES = new Set(["npm", "npx", "pnpm", "yarn"]);
65599
66047
  function spawnAsync(command, cwd, timeoutMs) {
65600
- return new Promise((resolve21) => {
66048
+ return new Promise((resolve22) => {
65601
66049
  try {
65602
66050
  const [rawCmd, ...args2] = command;
65603
66051
  const cmd = process.platform === "win32" && WIN32_CMD_BINARIES.has(rawCmd) && !rawCmd.includes(".") ? `${rawCmd}.cmd` : rawCmd;
@@ -65644,24 +66092,24 @@ function spawnAsync(command, cwd, timeoutMs) {
65644
66092
  try {
65645
66093
  proc.kill();
65646
66094
  } catch {}
65647
- resolve21(null);
66095
+ resolve22(null);
65648
66096
  }, timeoutMs);
65649
66097
  proc.on("close", (code) => {
65650
66098
  if (done)
65651
66099
  return;
65652
66100
  done = true;
65653
66101
  clearTimeout(timer);
65654
- resolve21({ exitCode: code ?? 1, stdout, stderr });
66102
+ resolve22({ exitCode: code ?? 1, stdout, stderr });
65655
66103
  });
65656
66104
  proc.on("error", () => {
65657
66105
  if (done)
65658
66106
  return;
65659
66107
  done = true;
65660
66108
  clearTimeout(timer);
65661
- resolve21(null);
66109
+ resolve22(null);
65662
66110
  });
65663
66111
  } catch {
65664
- resolve21(null);
66112
+ resolve22(null);
65665
66113
  }
65666
66114
  });
65667
66115
  }
@@ -68373,12 +68821,12 @@ ${body2}`);
68373
68821
  // src/council/council-evidence-writer.ts
68374
68822
  import {
68375
68823
  appendFileSync as appendFileSync7,
68376
- existsSync as existsSync36,
68377
- mkdirSync as mkdirSync16,
68824
+ existsSync as existsSync37,
68825
+ mkdirSync as mkdirSync17,
68378
68826
  readFileSync as readFileSync35,
68379
68827
  writeFileSync as writeFileSync11
68380
68828
  } from "fs";
68381
- import { join as join59 } from "path";
68829
+ import { join as join60 } from "path";
68382
68830
  var EVIDENCE_DIR2 = ".swarm/evidence";
68383
68831
  var VALID_TASK_ID = /^\d+\.\d+(\.\d+)*$/;
68384
68832
  var COUNCIL_GATE_NAME = "council";
@@ -68412,11 +68860,11 @@ function writeCouncilEvidence(workingDir, synthesis) {
68412
68860
  if (!VALID_TASK_ID.test(synthesis.taskId)) {
68413
68861
  throw new Error(`writeCouncilEvidence: invalid taskId "${synthesis.taskId}" \u2014 must match N.M or N.M.P format`);
68414
68862
  }
68415
- const dir = join59(workingDir, EVIDENCE_DIR2);
68416
- mkdirSync16(dir, { recursive: true });
68417
- const filePath = join59(dir, `${synthesis.taskId}.json`);
68863
+ const dir = join60(workingDir, EVIDENCE_DIR2);
68864
+ mkdirSync17(dir, { recursive: true });
68865
+ const filePath = join60(dir, `${synthesis.taskId}.json`);
68418
68866
  const existingRoot = Object.create(null);
68419
- if (existsSync36(filePath)) {
68867
+ if (existsSync37(filePath)) {
68420
68868
  try {
68421
68869
  const parsed = JSON.parse(readFileSync35(filePath, "utf-8"));
68422
68870
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
@@ -68443,15 +68891,15 @@ function writeCouncilEvidence(workingDir, synthesis) {
68443
68891
  updated.gates = mergedGates;
68444
68892
  writeFileSync11(filePath, JSON.stringify(updated, null, 2));
68445
68893
  try {
68446
- const councilDir = join59(workingDir, ".swarm", "council");
68447
- mkdirSync16(councilDir, { recursive: true });
68894
+ const councilDir = join60(workingDir, ".swarm", "council");
68895
+ mkdirSync17(councilDir, { recursive: true });
68448
68896
  const auditLine = JSON.stringify({
68449
68897
  round: synthesis.roundNumber,
68450
68898
  verdict: synthesis.overallVerdict,
68451
68899
  timestamp: synthesis.timestamp,
68452
68900
  vetoedBy: synthesis.vetoedBy
68453
68901
  });
68454
- appendFileSync7(join59(councilDir, `${synthesis.taskId}.rounds.jsonl`), `${auditLine}
68902
+ appendFileSync7(join60(councilDir, `${synthesis.taskId}.rounds.jsonl`), `${auditLine}
68455
68903
  `);
68456
68904
  } catch (auditError) {
68457
68905
  console.warn(`writeCouncilEvidence: failed to append round-history audit log: ${auditError instanceof Error ? auditError.message : String(auditError)}`);
@@ -68580,22 +69028,22 @@ function buildUnifiedFeedback(taskId, verdict, vetoedBy, requiredFixes, advisory
68580
69028
  }
68581
69029
 
68582
69030
  // src/council/criteria-store.ts
68583
- import { existsSync as existsSync37, mkdirSync as mkdirSync17, readFileSync as readFileSync36, writeFileSync as writeFileSync12 } from "fs";
68584
- import { join as join60 } from "path";
69031
+ import { existsSync as existsSync38, mkdirSync as mkdirSync18, readFileSync as readFileSync36, writeFileSync as writeFileSync12 } from "fs";
69032
+ import { join as join61 } from "path";
68585
69033
  var COUNCIL_DIR = ".swarm/council";
68586
69034
  function writeCriteria(workingDir, taskId, criteria) {
68587
- const dir = join60(workingDir, COUNCIL_DIR);
68588
- mkdirSync17(dir, { recursive: true });
69035
+ const dir = join61(workingDir, COUNCIL_DIR);
69036
+ mkdirSync18(dir, { recursive: true });
68589
69037
  const payload = {
68590
69038
  taskId,
68591
69039
  criteria,
68592
69040
  declaredAt: new Date().toISOString()
68593
69041
  };
68594
- writeFileSync12(join60(dir, `${safeId(taskId)}.json`), JSON.stringify(payload, null, 2));
69042
+ writeFileSync12(join61(dir, `${safeId(taskId)}.json`), JSON.stringify(payload, null, 2));
68595
69043
  }
68596
69044
  function readCriteria(workingDir, taskId) {
68597
- const filePath = join60(workingDir, COUNCIL_DIR, `${safeId(taskId)}.json`);
68598
- if (!existsSync37(filePath))
69045
+ const filePath = join61(workingDir, COUNCIL_DIR, `${safeId(taskId)}.json`);
69046
+ if (!existsSync38(filePath))
68599
69047
  return null;
68600
69048
  try {
68601
69049
  const parsed = JSON.parse(readFileSync36(filePath, "utf-8"));
@@ -70163,7 +70611,7 @@ function summarizePlan(plan) {
70163
70611
  }))
70164
70612
  };
70165
70613
  }
70166
- function derivePlanId(plan) {
70614
+ function derivePlanId2(plan) {
70167
70615
  return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
70168
70616
  }
70169
70617
  async function executeGetApprovedPlan(args2, directory) {
@@ -70184,7 +70632,9 @@ async function executeGetApprovedPlan(args2, directory) {
70184
70632
  reason: "no_approved_snapshot"
70185
70633
  };
70186
70634
  }
70187
- const expectedPlanId = derivePlanId(currentPlan);
70635
+ const expectedPlanId = derivePlanId2(currentPlan);
70636
+ const profile = getProfile(directory, expectedPlanId);
70637
+ const qaProfileHash = profile ? computeProfileHash(profile) : null;
70188
70638
  const approved = await loadLastApprovedPlan(directory, expectedPlanId);
70189
70639
  if (!approved) {
70190
70640
  const unscopedSnapshot = await loadLastApprovedPlan(directory);
@@ -70194,12 +70644,14 @@ async function executeGetApprovedPlan(args2, directory) {
70194
70644
  approved_plan: undefined,
70195
70645
  current_plan: null,
70196
70646
  drift_detected: true,
70197
- current_plan_error: "Plan identity (swarm/title) was mutated after approval \u2014 " + `expected plan_id '${expectedPlanId}' but approved snapshot has a different identity. ` + "This is a form of plan tampering."
70647
+ current_plan_error: "Plan identity (swarm/title) was mutated after approval \u2014 " + `expected plan_id '${expectedPlanId}' but approved snapshot has a different identity. ` + "This is a form of plan tampering.",
70648
+ qa_profile_hash: qaProfileHash
70198
70649
  };
70199
70650
  }
70200
70651
  return {
70201
70652
  success: false,
70202
- reason: "no_approved_snapshot"
70653
+ reason: "no_approved_snapshot",
70654
+ qa_profile_hash: qaProfileHash
70203
70655
  };
70204
70656
  }
70205
70657
  const summaryOnly = args2.summary_only === true;
@@ -70220,7 +70672,8 @@ async function executeGetApprovedPlan(args2, directory) {
70220
70672
  success: true,
70221
70673
  approved_plan: approvedPayload,
70222
70674
  current_plan: currentPayload,
70223
- drift_detected: driftDetected
70675
+ drift_detected: driftDetected,
70676
+ qa_profile_hash: qaProfileHash
70224
70677
  };
70225
70678
  }
70226
70679
  var get_approved_plan = createSwarmTool({
@@ -70233,13 +70686,58 @@ var get_approved_plan = createSwarmTool({
70233
70686
  return JSON.stringify(await executeGetApprovedPlan(typedArgs, directory), null, 2);
70234
70687
  }
70235
70688
  });
70689
+ // src/tools/get-qa-gate-profile.ts
70690
+ init_manager();
70691
+ init_create_tool();
70692
+ function derivePlanId3(plan) {
70693
+ return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
70694
+ }
70695
+ async function executeGetQaGateProfile(_args, directory) {
70696
+ const plan = await loadPlanJsonOnly(directory);
70697
+ if (!plan) {
70698
+ return {
70699
+ success: false,
70700
+ reason: "plan_json_unavailable"
70701
+ };
70702
+ }
70703
+ const planId = derivePlanId3(plan);
70704
+ const profile = getProfile(directory, planId);
70705
+ if (!profile) {
70706
+ return {
70707
+ success: false,
70708
+ reason: "no_profile",
70709
+ plan_id: planId
70710
+ };
70711
+ }
70712
+ return {
70713
+ success: true,
70714
+ plan_id: planId,
70715
+ profile: {
70716
+ plan_id: profile.plan_id,
70717
+ project_type: profile.project_type,
70718
+ gates: { ...profile.gates },
70719
+ locked_at: profile.locked_at,
70720
+ locked_by_snapshot_seq: profile.locked_by_snapshot_seq,
70721
+ created_at: profile.created_at,
70722
+ profile_hash: computeProfileHash(profile)
70723
+ }
70724
+ };
70725
+ }
70726
+ var get_qa_gate_profile = createSwarmTool({
70727
+ description: "Retrieve the QA gate profile for the current plan. Returns the spec-level " + "gates, lock state, and a SHA-256 profile hash. Read-only \u2014 does not " + "create a profile if none exists. plan_id is derived automatically from " + "plan.json (swarm + title).",
70728
+ args: {},
70729
+ execute: async (args2, directory) => {
70730
+ const typedArgs = args2 ?? {};
70731
+ return JSON.stringify(await executeGetQaGateProfile(typedArgs, directory), null, 2);
70732
+ }
70733
+ });
70236
70734
  // src/tools/gitingest.ts
70237
70735
  init_dist();
70238
70736
  init_create_tool();
70239
70737
  var GITINGEST_TIMEOUT_MS = 1e4;
70240
70738
  var GITINGEST_MAX_RESPONSE_BYTES = 5242880;
70241
70739
  var GITINGEST_MAX_RETRIES = 2;
70242
- var delay = (ms) => new Promise((resolve27) => setTimeout(resolve27, ms));
70740
+ var delay = (ms) => new Promise((resolve28) => setTimeout(resolve28, ms));
70243
70741
  async function fetchGitingest(args2) {
70244
70742
  for (let attempt = 0;attempt <= GITINGEST_MAX_RETRIES; attempt++) {
70245
70743
  try {
@@ -70816,7 +71314,7 @@ init_dist();
70816
71314
  init_config();
70817
71315
  init_knowledge_store();
70818
71316
  init_create_tool();
70819
- import { existsSync as existsSync42 } from "fs";
71317
+ import { existsSync as existsSync43 } from "fs";
70820
71318
  var DEFAULT_LIMIT = 10;
70821
71319
  var MAX_LESSON_LENGTH = 200;
70822
71320
  var VALID_CATEGORIES3 = [
@@ -70885,14 +71383,14 @@ function validateLimit(limit) {
70885
71383
  }
70886
71384
  async function readSwarmKnowledge(directory) {
70887
71385
  const swarmPath = resolveSwarmKnowledgePath(directory);
70888
- if (!existsSync42(swarmPath)) {
71386
+ if (!existsSync43(swarmPath)) {
70889
71387
  return [];
70890
71388
  }
70891
71389
  return readKnowledge(swarmPath);
70892
71390
  }
70893
71391
  async function readHiveKnowledge() {
70894
71392
  const hivePath = resolveHiveKnowledgePath();
70895
- if (!existsSync42(hivePath)) {
71393
+ if (!existsSync43(hivePath)) {
70896
71394
  return [];
70897
71395
  }
70898
71396
  return readKnowledge(hivePath);
@@ -71849,7 +72347,7 @@ async function runNpmAudit(directory) {
71849
72347
  stderr: "pipe",
71850
72348
  cwd: directory
71851
72349
  });
71852
- const timeoutPromise = new Promise((resolve28) => setTimeout(() => resolve28("timeout"), AUDIT_TIMEOUT_MS));
72350
+ const timeoutPromise = new Promise((resolve29) => setTimeout(() => resolve29("timeout"), AUDIT_TIMEOUT_MS));
71853
72351
  const result = await Promise.race([
71854
72352
  Promise.all([
71855
72353
  new Response(proc.stdout).text(),
@@ -71972,7 +72470,7 @@ async function runPipAudit(directory) {
71972
72470
  stderr: "pipe",
71973
72471
  cwd: directory
71974
72472
  });
71975
- const timeoutPromise = new Promise((resolve28) => setTimeout(() => resolve28("timeout"), AUDIT_TIMEOUT_MS));
72473
+ const timeoutPromise = new Promise((resolve29) => setTimeout(() => resolve29("timeout"), AUDIT_TIMEOUT_MS));
71976
72474
  const result = await Promise.race([
71977
72475
  Promise.all([
71978
72476
  new Response(proc.stdout).text(),
@@ -72103,7 +72601,7 @@ async function runCargoAudit(directory) {
72103
72601
  stderr: "pipe",
72104
72602
  cwd: directory
72105
72603
  });
72106
- const timeoutPromise = new Promise((resolve28) => setTimeout(() => resolve28("timeout"), AUDIT_TIMEOUT_MS));
72604
+ const timeoutPromise = new Promise((resolve29) => setTimeout(() => resolve29("timeout"), AUDIT_TIMEOUT_MS));
72107
72605
  const result = await Promise.race([
72108
72606
  Promise.all([
72109
72607
  new Response(proc.stdout).text(),
@@ -72230,7 +72728,7 @@ async function runGoAudit(directory) {
72230
72728
  stderr: "pipe",
72231
72729
  cwd: directory
72232
72730
  });
72233
- const timeoutPromise = new Promise((resolve28) => setTimeout(() => resolve28("timeout"), AUDIT_TIMEOUT_MS));
72731
+ const timeoutPromise = new Promise((resolve29) => setTimeout(() => resolve29("timeout"), AUDIT_TIMEOUT_MS));
72234
72732
  const result = await Promise.race([
72235
72733
  Promise.all([
72236
72734
  new Response(proc.stdout).text(),
@@ -72366,7 +72864,7 @@ async function runDotnetAudit(directory) {
72366
72864
  stderr: "pipe",
72367
72865
  cwd: directory
72368
72866
  });
72369
- const timeoutPromise = new Promise((resolve28) => setTimeout(() => resolve28("timeout"), AUDIT_TIMEOUT_MS));
72867
+ const timeoutPromise = new Promise((resolve29) => setTimeout(() => resolve29("timeout"), AUDIT_TIMEOUT_MS));
72370
72868
  const result = await Promise.race([
72371
72869
  Promise.all([
72372
72870
  new Response(proc.stdout).text(),
@@ -72485,7 +72983,7 @@ async function runBundleAudit(directory) {
72485
72983
  stderr: "pipe",
72486
72984
  cwd: directory
72487
72985
  });
72488
- const timeoutPromise = new Promise((resolve28) => setTimeout(() => resolve28("timeout"), AUDIT_TIMEOUT_MS));
72986
+ const timeoutPromise = new Promise((resolve29) => setTimeout(() => resolve29("timeout"), AUDIT_TIMEOUT_MS));
72489
72987
  const result = await Promise.race([
72490
72988
  Promise.all([
72491
72989
  new Response(proc.stdout).text(),
@@ -72633,7 +73131,7 @@ async function runDartAudit(directory) {
72633
73131
  stderr: "pipe",
72634
73132
  cwd: directory
72635
73133
  });
72636
- const timeoutPromise = new Promise((resolve28) => setTimeout(() => resolve28("timeout"), AUDIT_TIMEOUT_MS));
73134
+ const timeoutPromise = new Promise((resolve29) => setTimeout(() => resolve29("timeout"), AUDIT_TIMEOUT_MS));
72637
73135
  const result = await Promise.race([
72638
73136
  Promise.all([
72639
73137
  new Response(proc.stdout).text(),
@@ -72751,7 +73249,7 @@ async function runComposerAudit(directory) {
72751
73249
  stderr: "pipe",
72752
73250
  cwd: directory
72753
73251
  });
72754
- const timeoutPromise = new Promise((resolve28) => setTimeout(() => resolve28("timeout"), AUDIT_TIMEOUT_MS));
73252
+ const timeoutPromise = new Promise((resolve29) => setTimeout(() => resolve29("timeout"), AUDIT_TIMEOUT_MS));
72755
73253
  const result = await Promise.race([
72756
73254
  Promise.all([
72757
73255
  new Response(proc.stdout).text(),
@@ -74393,7 +74891,7 @@ function mapSemgrepSeverity(severity) {
74393
74891
  }
74394
74892
  }
74395
74893
  async function executeWithTimeout(command, args2, options) {
74396
- return new Promise((resolve29) => {
74894
+ return new Promise((resolve30) => {
74397
74895
  const child = child_process7.spawn(command, args2, {
74398
74896
  shell: false,
74399
74897
  cwd: options.cwd
@@ -74402,7 +74900,7 @@ async function executeWithTimeout(command, args2, options) {
74402
74900
  let stderr = "";
74403
74901
  const timeout = setTimeout(() => {
74404
74902
  child.kill("SIGTERM");
74405
- resolve29({
74903
+ resolve30({
74406
74904
  stdout,
74407
74905
  stderr: "Process timed out",
74408
74906
  exitCode: 124
@@ -74416,7 +74914,7 @@ async function executeWithTimeout(command, args2, options) {
74416
74914
  });
74417
74915
  child.on("close", (code) => {
74418
74916
  clearTimeout(timeout);
74419
- resolve29({
74917
+ resolve30({
74420
74918
  stdout,
74421
74919
  stderr,
74422
74920
  exitCode: code ?? 0
@@ -74424,7 +74922,7 @@ async function executeWithTimeout(command, args2, options) {
74424
74922
  });
74425
74923
  child.on("error", (err2) => {
74426
74924
  clearTimeout(timeout);
74427
- resolve29({
74925
+ resolve30({
74428
74926
  stdout,
74429
74927
  stderr: err2.message,
74430
74928
  exitCode: 1
@@ -77868,7 +78366,7 @@ async function ripgrepSearch(opts) {
77868
78366
  stderr: "pipe",
77869
78367
  cwd: opts.workspace
77870
78368
  });
77871
- const timeout = new Promise((resolve34) => setTimeout(() => resolve34("timeout"), REGEX_TIMEOUT_MS));
78369
+ const timeout = new Promise((resolve35) => setTimeout(() => resolve35("timeout"), REGEX_TIMEOUT_MS));
77872
78370
  const exitPromise = proc.exited;
77873
78371
  const result = await Promise.race([exitPromise, timeout]);
77874
78372
  if (result === "timeout") {
@@ -78152,6 +78650,84 @@ var search = createSwarmTool({
78152
78650
  // src/tools/index.ts
78153
78651
  init_secretscan();
78154
78652
 
78653
+ // src/tools/set-qa-gates.ts
78654
+ init_dist();
78655
+ init_manager();
78656
+ init_create_tool();
78657
+ function derivePlanId4(plan) {
78658
+ return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
78659
+ }
78660
+ async function executeSetQaGates(args2, directory) {
78661
+ const plan = await loadPlanJsonOnly(directory);
78662
+ if (!plan) {
78663
+ return {
78664
+ success: false,
78665
+ reason: "plan_json_unavailable",
78666
+ message: "Cannot configure QA gates: plan.json is missing or invalid. " + "Create a plan first (e.g. via /swarm specify or save_plan)."
78667
+ };
78668
+ }
78669
+ const planId = derivePlanId4(plan);
78670
+ getOrCreateProfile(directory, planId, args2.project_type);
78671
+ const partial3 = {};
78672
+ for (const key of [
78673
+ "reviewer",
78674
+ "test_engineer",
78675
+ "council_mode",
78676
+ "sme_enabled",
78677
+ "critic_pre_plan",
78678
+ "hallucination_guard",
78679
+ "sast_enabled"
78680
+ ]) {
78681
+ if (args2[key] !== undefined)
78682
+ partial3[key] = args2[key];
78683
+ }
78684
+ try {
78685
+ const updated = setGates(directory, planId, partial3);
78686
+ return {
78687
+ success: true,
78688
+ plan_id: planId,
78689
+ message: `QA gates updated for plan_id=${planId}`,
78690
+ profile: {
78691
+ plan_id: updated.plan_id,
78692
+ gates: { ...updated.gates },
78693
+ locked_at: updated.locked_at,
78694
+ locked_by_snapshot_seq: updated.locked_by_snapshot_seq,
78695
+ profile_hash: computeProfileHash(updated)
78696
+ }
78697
+ };
78698
+ } catch (err3) {
78699
+ const msg = err3 instanceof Error ? err3.message : String(err3);
78700
+ const lower = msg.toLowerCase();
78701
+ let reason = "set_gates_failed";
78702
+ if (lower.includes("locked"))
78703
+ reason = "profile_locked";
78704
+ else if (lower.includes("ratchet"))
78705
+ reason = "ratchet_violation";
78706
+ return {
78707
+ success: false,
78708
+ reason,
78709
+ message: msg,
78710
+ plan_id: planId
78711
+ };
78712
+ }
78713
+ }
78714
+ var set_qa_gates = createSwarmTool({
78715
+ description: "Configure the QA gate profile for the current plan. Architect-only. " + "Ratchet-tighter: can enable additional gates but cannot disable gates " + "that are already enabled. Rejects all writes once the profile is " + "locked (after critic approval). Creates the profile with defaults if " + "none exists. plan_id is derived automatically from plan.json.",
78716
+ args: {
78717
+ reviewer: tool.schema.boolean().optional().describe("Enable the reviewer gate (true) \u2014 cannot be disabled."),
78718
+ test_engineer: tool.schema.boolean().optional().describe("Enable the test_engineer gate (true) \u2014 cannot be disabled once on."),
78719
+ council_mode: tool.schema.boolean().optional().describe("Enable council mode (multi-SME consensus on high-risk phases)."),
78720
+ sme_enabled: tool.schema.boolean().optional().describe("Enable SME consultation."),
78721
+ critic_pre_plan: tool.schema.boolean().optional().describe("Enable critic_pre_plan review before plan approval."),
78722
+ hallucination_guard: tool.schema.boolean().optional().describe("Enable hallucination_guard checks on plan and implementation claims."),
78723
+ sast_enabled: tool.schema.boolean().optional().describe("Enable SAST scanning as a required QA gate."),
78724
+ project_type: tool.schema.string().optional().describe('Project type label (e.g. "ts", "python"). Only applied when the profile is being created for the first time.')
78725
+ },
78726
+ execute: async (args2, directory) => {
78727
+ const typedArgs = args2 ?? {};
78728
+ return JSON.stringify(await executeSetQaGates(typedArgs, directory), null, 2);
78729
+ }
78730
+ });
78155
78731
  // src/tools/suggest-patch.ts
78156
78732
  init_tool();
78157
78733
  init_path_security();
@@ -79659,12 +80235,15 @@ var update_task_status = createSwarmTool({
79659
80235
  });
79660
80236
  // src/tools/write-drift-evidence.ts
79661
80237
  init_tool();
80238
+ import fs71 from "fs";
80239
+ import path87 from "path";
79662
80240
  init_utils2();
79663
80241
  init_ledger();
79664
80242
  init_manager();
79665
80243
  init_create_tool();
79666
- import fs71 from "fs";
79667
- import path87 from "path";
80244
+ function derivePlanId5(plan) {
80245
+ return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
80246
+ }
79668
80247
  function normalizeVerdict(verdict) {
79669
80248
  switch (verdict) {
79670
80249
  case "APPROVED":
@@ -79731,6 +80310,8 @@ async function executeWriteDriftEvidence(args2, directory) {
79731
80310
  await fs71.promises.rename(tempPath, validatedPath);
79732
80311
  let snapshotInfo;
79733
80312
  let snapshotError;
80313
+ let qaProfileLocked;
80314
+ let qaProfileLockError;
79734
80315
  if (normalizedVerdict === "approved") {
79735
80316
  try {
79736
80317
  const currentPlan = await loadPlanJsonOnly(directory);
@@ -79748,6 +80329,21 @@ async function executeWriteDriftEvidence(args2, directory) {
79748
80329
  seq: snapshotEvent.seq,
79749
80330
  timestamp: snapshotEvent.timestamp
79750
80331
  };
80332
+ try {
80333
+ const planId = derivePlanId5(currentPlan);
80334
+ const locked = lockProfile(directory, planId, snapshotEvent.seq);
80335
+ qaProfileLocked = {
80336
+ plan_id: planId,
80337
+ locked_at: locked.locked_at ?? "",
80338
+ locked_by_snapshot_seq: locked.locked_by_snapshot_seq ?? -1
80339
+ };
80340
+ } catch (lockErr) {
80341
+ const msg = lockErr instanceof Error ? lockErr.message : String(lockErr);
80342
+ if (!/No QA gate profile/i.test(msg)) {
80343
+ qaProfileLockError = msg;
80344
+ console.warn("[write_drift_evidence] QA gate profile lock failed:", msg);
80345
+ }
80346
+ }
79751
80347
  } else {
79752
80348
  snapshotError = "plan.json not available for snapshot";
79753
80349
  }
@@ -79762,7 +80358,9 @@ async function executeWriteDriftEvidence(args2, directory) {
79762
80358
  verdict: normalizedVerdict,
79763
80359
  message: `Drift evidence written to .swarm/evidence/${phase}/drift-verifier.json`,
79764
80360
  approvedSnapshot: snapshotInfo,
79765
- snapshotError
80361
+ snapshotError,
80362
+ qaProfileLocked,
80363
+ qaProfileLockError
79766
80364
  }, null, 2);
79767
80365
  } catch (error93) {
79768
80366
  return JSON.stringify({
@@ -80067,7 +80665,9 @@ var OpenCodeSwarm = async (ctx) => {
80067
80665
  checkpoint,
80068
80666
  completion_verify,
80069
80667
  complexity_hotspots,
80668
+ convene_council,
80070
80669
  curator_analyze,
80670
+ declare_council_criteria,
80071
80671
  knowledge_add,
80072
80672
  knowledge_recall,
80073
80673
  knowledge_remove,
@@ -80078,10 +80678,13 @@ var OpenCodeSwarm = async (ctx) => {
80078
80678
  evidence_check,
80079
80679
  extract_code_blocks,
80080
80680
  get_approved_plan,
80681
+ get_qa_gate_profile,
80682
+ set_qa_gates,
80081
80683
  gitingest,
80082
80684
  imports,
80083
80685
  knowledge_query,
80084
80686
  lint,
80687
+ lint_spec,
80085
80688
  diff,
80086
80689
  pkg_audit,
80087
80690
  placeholder_scan,
@@ -80089,6 +80692,7 @@ var OpenCodeSwarm = async (ctx) => {
80089
80692
  pre_check_batch,
80090
80693
  quality_budget,
80091
80694
  repo_map,
80695
+ req_coverage,
80092
80696
  retrieve_summary,
80093
80697
  save_plan,
80094
80698
  sast_scan,
@@ -80118,7 +80722,7 @@ var OpenCodeSwarm = async (ctx) => {
80118
80722
  ...opencodeConfig.command || {},
80119
80723
  swarm: {
80120
80724
  template: "/swarm $ARGUMENTS",
80121
- description: "Swarm management commands: /swarm [status|plan|agents|history|config|evidence|handoff|archive|diagnose|preflight|sync-plan|benchmark|export|reset|rollback|retrieve|clarify|analyze|specify|dark-matter|knowledge|curate|turbo|full-auto|write-retro|reset-session|simulate|promote|checkpoint|close]"
80725
+ description: "Swarm management commands: /swarm [status|plan|agents|history|config|evidence|handoff|archive|diagnose|preflight|sync-plan|benchmark|export|reset|rollback|retrieve|clarify|analyze|specify|brainstorm|qa-gates|dark-matter|knowledge|curate|turbo|full-auto|write-retro|reset-session|simulate|promote|checkpoint|close]"
80122
80726
  },
80123
80727
  "swarm-status": {
80124
80728
  template: "/swarm status",
@@ -80196,6 +80800,14 @@ var OpenCodeSwarm = async (ctx) => {
80196
80800
  template: "/swarm specify $ARGUMENTS",
80197
80801
  description: "Use /swarm specify to generate or import a feature specification"
80198
80802
  },
80803
+ "swarm-brainstorm": {
80804
+ template: "/swarm brainstorm $ARGUMENTS",
80805
+ description: "Use /swarm brainstorm to enter the architect MODE: BRAINSTORM planning workflow"
80806
+ },
80807
+ "swarm-qa-gates": {
80808
+ template: "/swarm qa-gates $ARGUMENTS",
80809
+ description: "Use /swarm qa-gates to view or modify QA gate profile for the current plan"
80810
+ },
80199
80811
  "swarm-dark-matter": {
80200
80812
  template: "/swarm dark-matter",
80201
80813
  description: "Use /swarm dark-matter to detect hidden file couplings"