opencode-swarm 6.67.1 → 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/cli/index.js CHANGED
@@ -18507,7 +18507,9 @@ var TOOL_NAMES = [
18507
18507
  "suggest_patch",
18508
18508
  "req_coverage",
18509
18509
  "get_approved_plan",
18510
- "repo_map"
18510
+ "repo_map",
18511
+ "get_qa_gate_profile",
18512
+ "set_qa_gates"
18511
18513
  ];
18512
18514
  var TOOL_NAME_SET = new Set(TOOL_NAMES);
18513
18515
 
@@ -18577,7 +18579,9 @@ var AGENT_TOOL_MAP = {
18577
18579
  "knowledge_remove",
18578
18580
  "co_change_analyzer",
18579
18581
  "suggest_patch",
18580
- "repo_map"
18582
+ "repo_map",
18583
+ "get_qa_gate_profile",
18584
+ "set_qa_gates"
18581
18585
  ],
18582
18586
  explorer: [
18583
18587
  "complexity_hotspots",
@@ -19818,6 +19822,24 @@ async function handleBenchmarkCommand(directory, args) {
19818
19822
  `);
19819
19823
  }
19820
19824
 
19825
+ // src/commands/brainstorm.ts
19826
+ function sanitizeTopic(raw) {
19827
+ const collapsed = raw.replace(/\s+/g, " ").trim();
19828
+ const stripped = collapsed.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
19829
+ const normalized = stripped.replace(/\s+/g, " ").trim();
19830
+ const MAX_TOPIC_LEN = 2000;
19831
+ if (normalized.length <= MAX_TOPIC_LEN)
19832
+ return normalized;
19833
+ return `${normalized.slice(0, MAX_TOPIC_LEN)}\u2026`;
19834
+ }
19835
+ async function handleBrainstormCommand(_directory, args) {
19836
+ const description = sanitizeTopic(args.join(" "));
19837
+ if (description) {
19838
+ return `[MODE: BRAINSTORM] ${description}`;
19839
+ }
19840
+ 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).";
19841
+ }
19842
+
19821
19843
  // src/commands/checkpoint.ts
19822
19844
  init_zod();
19823
19845
 
@@ -41493,6 +41515,324 @@ async function handlePromoteCommand(directory, args) {
41493
41515
  }
41494
41516
  }
41495
41517
 
41518
+ // src/db/qa-gate-profile.ts
41519
+ import { createHash as createHash4 } from "crypto";
41520
+
41521
+ // src/db/project-db.ts
41522
+ import { Database } from "bun:sqlite";
41523
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7 } from "fs";
41524
+ import { join as join22, resolve as resolve11 } from "path";
41525
+ var MIGRATIONS = [
41526
+ {
41527
+ version: 1,
41528
+ name: "create_project_constraints",
41529
+ sql: `CREATE TABLE project_constraints (
41530
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41531
+ constraint_type TEXT NOT NULL,
41532
+ content TEXT NOT NULL,
41533
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
41534
+ )`
41535
+ },
41536
+ {
41537
+ version: 2,
41538
+ name: "create_qa_gate_profile",
41539
+ sql: `CREATE TABLE qa_gate_profile (
41540
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41541
+ plan_id TEXT NOT NULL UNIQUE,
41542
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
41543
+ project_type TEXT,
41544
+ gates TEXT NOT NULL DEFAULT '{}',
41545
+ locked_at TEXT,
41546
+ locked_by_snapshot_seq INTEGER
41547
+ )`
41548
+ },
41549
+ {
41550
+ version: 3,
41551
+ name: "create_qa_gate_profile_immutability_trigger",
41552
+ sql: `CREATE TRIGGER IF NOT EXISTS trg_qa_gate_profile_no_update_after_lock
41553
+ BEFORE UPDATE ON qa_gate_profile
41554
+ WHEN OLD.locked_at IS NOT NULL
41555
+ BEGIN
41556
+ SELECT RAISE(ABORT, 'qa_gate_profile row is locked and cannot be modified after critic approval');
41557
+ END`
41558
+ }
41559
+ ];
41560
+ var _projectDbs = new Map;
41561
+ function runProjectMigrations(db) {
41562
+ db.run(`CREATE TABLE IF NOT EXISTS schema_migrations (
41563
+ version INTEGER PRIMARY KEY,
41564
+ name TEXT NOT NULL,
41565
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
41566
+ )`);
41567
+ const row = db.query("SELECT MAX(version) as version FROM schema_migrations").get();
41568
+ const currentVersion = row?.version ?? 0;
41569
+ for (const migration of MIGRATIONS) {
41570
+ if (migration.version <= currentVersion)
41571
+ continue;
41572
+ const apply = db.transaction(() => {
41573
+ db.run(migration.sql);
41574
+ db.run("INSERT INTO schema_migrations (version, name) VALUES (?, ?)", [
41575
+ migration.version,
41576
+ migration.name
41577
+ ]);
41578
+ });
41579
+ apply();
41580
+ }
41581
+ }
41582
+ function projectDbPath(directory) {
41583
+ return join22(resolve11(directory), ".swarm", "swarm.db");
41584
+ }
41585
+ function projectDbExists(directory) {
41586
+ return existsSync16(projectDbPath(directory));
41587
+ }
41588
+ function getProjectDb(directory) {
41589
+ const key = resolve11(directory);
41590
+ const existing = _projectDbs.get(key);
41591
+ if (existing)
41592
+ return existing;
41593
+ const swarmDir = join22(key, ".swarm");
41594
+ mkdirSync7(swarmDir, { recursive: true });
41595
+ const db = new Database(join22(swarmDir, "swarm.db"));
41596
+ db.run("PRAGMA journal_mode = WAL;");
41597
+ db.run("PRAGMA synchronous = NORMAL;");
41598
+ db.run("PRAGMA busy_timeout = 5000;");
41599
+ db.run("PRAGMA foreign_keys = ON;");
41600
+ runProjectMigrations(db);
41601
+ _projectDbs.set(key, db);
41602
+ return db;
41603
+ }
41604
+
41605
+ // src/db/qa-gate-profile.ts
41606
+ var DEFAULT_QA_GATES = {
41607
+ reviewer: true,
41608
+ test_engineer: true,
41609
+ council_mode: false,
41610
+ sme_enabled: true,
41611
+ critic_pre_plan: true,
41612
+ hallucination_guard: false,
41613
+ sast_enabled: true
41614
+ };
41615
+ function rowToProfile(row) {
41616
+ let parsed = {};
41617
+ try {
41618
+ parsed = JSON.parse(row.gates);
41619
+ } catch {
41620
+ parsed = {};
41621
+ }
41622
+ const gates = { ...DEFAULT_QA_GATES, ...parsed };
41623
+ return {
41624
+ id: row.id,
41625
+ plan_id: row.plan_id,
41626
+ created_at: row.created_at,
41627
+ project_type: row.project_type,
41628
+ gates,
41629
+ locked_at: row.locked_at,
41630
+ locked_by_snapshot_seq: row.locked_by_snapshot_seq
41631
+ };
41632
+ }
41633
+ function getProfile(directory, planId) {
41634
+ if (!projectDbExists(directory))
41635
+ return null;
41636
+ const db = getProjectDb(directory);
41637
+ const row = db.query("SELECT * FROM qa_gate_profile WHERE plan_id = ?").get(planId);
41638
+ return row ? rowToProfile(row) : null;
41639
+ }
41640
+ function getOrCreateProfile(directory, planId, projectType) {
41641
+ const existing = getProfile(directory, planId);
41642
+ if (existing)
41643
+ return existing;
41644
+ const db = getProjectDb(directory);
41645
+ const gatesJson = JSON.stringify(DEFAULT_QA_GATES);
41646
+ const insert = db.transaction(() => {
41647
+ db.run("INSERT INTO qa_gate_profile (plan_id, project_type, gates) VALUES (?, ?, ?)", [planId, projectType ?? null, gatesJson]);
41648
+ });
41649
+ try {
41650
+ insert();
41651
+ } catch (err) {
41652
+ const msg = err instanceof Error ? err.message : String(err);
41653
+ if (!msg.toLowerCase().includes("unique")) {
41654
+ throw err;
41655
+ }
41656
+ }
41657
+ const after = getProfile(directory, planId);
41658
+ if (!after) {
41659
+ throw new Error(`Failed to create or load QA gate profile for plan_id=${planId}`);
41660
+ }
41661
+ return after;
41662
+ }
41663
+ function setGates(directory, planId, gates) {
41664
+ const current = getProfile(directory, planId);
41665
+ if (!current) {
41666
+ throw new Error(`No QA gate profile found for plan_id=${planId} \u2014 call getOrCreateProfile first`);
41667
+ }
41668
+ if (current.locked_at !== null) {
41669
+ throw new Error("Cannot modify gates: QA gate profile is locked after critic approval");
41670
+ }
41671
+ const merged = { ...current.gates };
41672
+ for (const key of Object.keys(gates)) {
41673
+ const incoming = gates[key];
41674
+ if (incoming === undefined)
41675
+ continue;
41676
+ if (incoming === false && current.gates[key] === true) {
41677
+ throw new Error(`Cannot disable gate '${key}': sessions can only ratchet tighter`);
41678
+ }
41679
+ if (incoming === true) {
41680
+ merged[key] = true;
41681
+ }
41682
+ }
41683
+ const db = getProjectDb(directory);
41684
+ db.run("UPDATE qa_gate_profile SET gates = ? WHERE plan_id = ?", [
41685
+ JSON.stringify(merged),
41686
+ planId
41687
+ ]);
41688
+ const updated = getProfile(directory, planId);
41689
+ if (!updated) {
41690
+ throw new Error(`Failed to re-read QA gate profile after update for plan_id=${planId}`);
41691
+ }
41692
+ return updated;
41693
+ }
41694
+ function computeProfileHash(profile) {
41695
+ const payload = JSON.stringify({
41696
+ plan_id: profile.plan_id,
41697
+ gates: profile.gates
41698
+ });
41699
+ return createHash4("sha256").update(payload).digest("hex");
41700
+ }
41701
+ function getEffectiveGates(profile, sessionOverrides) {
41702
+ const merged = { ...profile.gates };
41703
+ for (const key of Object.keys(sessionOverrides)) {
41704
+ if (sessionOverrides[key] === true) {
41705
+ merged[key] = true;
41706
+ }
41707
+ }
41708
+ return merged;
41709
+ }
41710
+
41711
+ // src/commands/qa-gates.ts
41712
+ init_manager();
41713
+ var ALL_GATE_NAMES = [
41714
+ "reviewer",
41715
+ "test_engineer",
41716
+ "council_mode",
41717
+ "sme_enabled",
41718
+ "critic_pre_plan",
41719
+ "hallucination_guard",
41720
+ "sast_enabled"
41721
+ ];
41722
+ function derivePlanId(plan) {
41723
+ return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
41724
+ }
41725
+ function isGateName(name) {
41726
+ return ALL_GATE_NAMES.includes(name);
41727
+ }
41728
+ function formatGates(gates) {
41729
+ return ALL_GATE_NAMES.map((g) => ` - ${g}: ${gates[g] ? "on" : "off"}`).join(`
41730
+ `);
41731
+ }
41732
+ async function handleQaGatesCommand(directory, args, sessionID) {
41733
+ const plan = await loadPlanJsonOnly(directory);
41734
+ if (!plan) {
41735
+ return "Error: plan.json not found or invalid. Create a plan first (e.g. /swarm specify or save_plan).";
41736
+ }
41737
+ const planId = derivePlanId(plan);
41738
+ const subcommand = args[0]?.toLowerCase();
41739
+ const gateArgs = args.slice(1);
41740
+ if (!subcommand || subcommand === "show" || subcommand === "status") {
41741
+ const profile = getProfile(directory, planId);
41742
+ const spec = profile ? profile.gates : DEFAULT_QA_GATES;
41743
+ const session = sessionID ? getAgentSession(sessionID) : null;
41744
+ const overrides = session?.qaGateSessionOverrides ?? {};
41745
+ const effective = profile ? getEffectiveGates(profile, overrides) : { ...DEFAULT_QA_GATES, ...overrides };
41746
+ const lines = [];
41747
+ lines.push(`QA Gate Profile for plan_id=${planId}`);
41748
+ if (!profile) {
41749
+ lines.push(" (no profile persisted yet \u2014 showing defaults)");
41750
+ } else {
41751
+ lines.push(` locked: ${profile.locked_at ? `yes @ ${profile.locked_at} (seq ${profile.locked_by_snapshot_seq ?? "?"})` : "no"}`);
41752
+ lines.push(` profile_hash: ${computeProfileHash(profile)}`);
41753
+ }
41754
+ lines.push("Spec-level gates:");
41755
+ lines.push(formatGates(spec));
41756
+ lines.push("Session overrides (ratchet-tighter only):");
41757
+ if (Object.keys(overrides).length === 0) {
41758
+ lines.push(" (none)");
41759
+ } else {
41760
+ for (const k of ALL_GATE_NAMES) {
41761
+ if (overrides[k] === true)
41762
+ lines.push(` - ${k}: on (override)`);
41763
+ }
41764
+ }
41765
+ lines.push("Effective gates:");
41766
+ lines.push(formatGates(effective));
41767
+ return lines.join(`
41768
+ `);
41769
+ }
41770
+ if (subcommand === "enable") {
41771
+ if (gateArgs.length === 0) {
41772
+ return "Usage: /swarm qa-gates enable <gate> [<gate> ...]";
41773
+ }
41774
+ const invalid = gateArgs.filter((g) => !isGateName(g));
41775
+ if (invalid.length > 0) {
41776
+ return `Error: unknown gate(s): ${invalid.join(", ")}. Valid gates: ${ALL_GATE_NAMES.join(", ")}`;
41777
+ }
41778
+ getOrCreateProfile(directory, planId);
41779
+ const patch = {};
41780
+ for (const g of gateArgs) {
41781
+ if (isGateName(g))
41782
+ patch[g] = true;
41783
+ }
41784
+ try {
41785
+ const updated = setGates(directory, planId, patch);
41786
+ return [
41787
+ `Enabled gates persisted for plan_id=${planId}:`,
41788
+ formatGates(updated.gates),
41789
+ `profile_hash: ${computeProfileHash(updated)}`
41790
+ ].join(`
41791
+ `);
41792
+ } catch (err) {
41793
+ const msg = err instanceof Error ? err.message : String(err);
41794
+ return `Error: ${msg}`;
41795
+ }
41796
+ }
41797
+ if (subcommand === "override") {
41798
+ if (!sessionID) {
41799
+ return "Error: session overrides require an active session context.";
41800
+ }
41801
+ if (gateArgs.length === 0) {
41802
+ return "Usage: /swarm qa-gates override <gate> [<gate> ...]";
41803
+ }
41804
+ const invalid = gateArgs.filter((g) => !isGateName(g));
41805
+ if (invalid.length > 0) {
41806
+ return `Error: unknown gate(s): ${invalid.join(", ")}. Valid gates: ${ALL_GATE_NAMES.join(", ")}`;
41807
+ }
41808
+ const session = getAgentSession(sessionID);
41809
+ if (!session) {
41810
+ return "Error: no active session found for override.";
41811
+ }
41812
+ const current = session.qaGateSessionOverrides ?? {};
41813
+ const next = { ...current };
41814
+ for (const g of gateArgs) {
41815
+ if (isGateName(g))
41816
+ next[g] = true;
41817
+ }
41818
+ session.qaGateSessionOverrides = next;
41819
+ return [
41820
+ `Session overrides updated for plan_id=${planId}:`,
41821
+ Object.keys(next).filter((k) => next[k] === true).map((k) => ` - ${k}: on`).join(`
41822
+ `) || " (none)"
41823
+ ].join(`
41824
+ `);
41825
+ }
41826
+ return [
41827
+ "Usage:",
41828
+ " /swarm qa-gates show current profile + effective gates",
41829
+ " /swarm qa-gates enable <gate>... persist-enable gate(s) (rejected if locked)",
41830
+ " /swarm qa-gates override <gate>... session-only enable (ratchet-tighter)",
41831
+ `Valid gates: ${ALL_GATE_NAMES.join(", ")}`
41832
+ ].join(`
41833
+ `);
41834
+ }
41835
+
41496
41836
  // src/commands/reset.ts
41497
41837
  import * as fs16 from "fs";
41498
41838
 
@@ -41549,13 +41889,13 @@ class CircuitBreaker {
41549
41889
  if (this.config.callTimeoutMs <= 0) {
41550
41890
  return fn();
41551
41891
  }
41552
- return new Promise((resolve11, reject) => {
41892
+ return new Promise((resolve12, reject) => {
41553
41893
  const timeout = setTimeout(() => {
41554
41894
  reject(new Error(`Call timeout after ${this.config.callTimeoutMs}ms`));
41555
41895
  }, this.config.callTimeoutMs);
41556
41896
  fn().then((result) => {
41557
41897
  clearTimeout(timeout);
41558
- resolve11(result);
41898
+ resolve12(result);
41559
41899
  }).catch((error93) => {
41560
41900
  clearTimeout(timeout);
41561
41901
  reject(error93);
@@ -41839,7 +42179,7 @@ class AutomationQueue {
41839
42179
 
41840
42180
  // src/background/worker.ts
41841
42181
  function sleep(ms) {
41842
- return new Promise((resolve11) => setTimeout(resolve11, ms));
42182
+ return new Promise((resolve12) => setTimeout(resolve12, ms));
41843
42183
  }
41844
42184
 
41845
42185
  class WorkerManager {
@@ -42930,6 +43270,18 @@ var COMMAND_REGISTRY = {
42930
43270
  description: "Generate or import a feature specification [description]",
42931
43271
  args: "[description-text]"
42932
43272
  },
43273
+ brainstorm: {
43274
+ handler: (ctx) => handleBrainstormCommand(ctx.directory, ctx.args),
43275
+ description: "Enter architect MODE: BRAINSTORM \u2014 structured seven-phase planning workflow [topic]",
43276
+ args: "[topic-text]",
43277
+ 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."
43278
+ },
43279
+ "qa-gates": {
43280
+ handler: (ctx) => handleQaGatesCommand(ctx.directory, ctx.args, ctx.sessionID),
43281
+ description: "View or modify QA gate profile for the current plan [enable|override <gate>...]",
43282
+ args: "[show|enable|override] <gate>...",
43283
+ 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."
43284
+ },
42933
43285
  promote: {
42934
43286
  handler: (ctx) => handlePromoteCommand(ctx.directory, ctx.args),
42935
43287
  description: "Manually promote lesson to hive knowledge",
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Handle /swarm brainstorm command.
3
+ *
4
+ * Returns a trigger prompt instructing the architect to enter
5
+ * MODE: BRAINSTORM — the seven-phase planning workflow defined in the
6
+ * architect prompt: CONTEXT SCAN → DIALOGUE → APPROACHES → DESIGN SECTIONS
7
+ * → SPEC WRITE + SELF-REVIEW → QA GATE SELECTION → TRANSITION.
8
+ *
9
+ * Any arguments become the initial topic/problem statement for the
10
+ * architect to reason about. The topic is sanitized to prevent prompt
11
+ * injection of rival MODE: headers or newline-based control sequences.
12
+ */
13
+ export declare function handleBrainstormCommand(_directory: string, args: string[]): Promise<string>;
@@ -0,0 +1 @@
1
+ export {};
@@ -4,6 +4,7 @@ export { handleAgentsCommand } from './agents';
4
4
  export { handleAnalyzeCommand } from './analyze';
5
5
  export { handleArchiveCommand } from './archive';
6
6
  export { handleBenchmarkCommand } from './benchmark';
7
+ export { handleBrainstormCommand } from './brainstorm';
7
8
  export { handleCheckpointCommand } from './checkpoint';
8
9
  export { handleClarifyCommand } from './clarify';
9
10
  export { handleCloseCommand } from './close';
@@ -21,6 +22,7 @@ export { handleKnowledgeListCommand, handleKnowledgeMigrateCommand, handleKnowle
21
22
  export { handlePlanCommand } from './plan';
22
23
  export { handlePreflightCommand } from './preflight';
23
24
  export { handlePromoteCommand } from './promote';
25
+ export { handleQaGatesCommand } from './qa-gates';
24
26
  export type { CommandContext, CommandEntry, RegisteredCommand, } from './registry.js';
25
27
  export { COMMAND_REGISTRY, resolveCommand, VALID_COMMANDS, } from './registry.js';
26
28
  export { handleResetCommand } from './reset';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * /swarm qa-gates command.
3
+ *
4
+ * View, enable, or add session overrides for QA gates tied to the current
5
+ * plan's QA gate profile. Read-only display when called without arguments;
6
+ * ratchet-tighter enable/override when called with `enable <gate>...` or
7
+ * `override <gate>...`.
8
+ *
9
+ * /swarm qa-gates -> show profile + effective gates
10
+ * /swarm qa-gates enable <gate>... -> persist into profile (architect)
11
+ * /swarm qa-gates override <gate>... -> session-only override
12
+ *
13
+ * Refuses to persist into a locked profile.
14
+ */
15
+ export declare function handleQaGatesCommand(directory: string, args: string[], sessionID: string): Promise<string>;
@@ -0,0 +1 @@
1
+ export {};
@@ -150,6 +150,18 @@ export declare const COMMAND_REGISTRY: {
150
150
  readonly description: "Generate or import a feature specification [description]";
151
151
  readonly args: "[description-text]";
152
152
  };
153
+ readonly brainstorm: {
154
+ readonly handler: (ctx: CommandContext) => Promise<string>;
155
+ readonly description: "Enter architect MODE: BRAINSTORM — structured seven-phase planning workflow [topic]";
156
+ readonly args: "[topic-text]";
157
+ readonly 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.";
158
+ };
159
+ readonly 'qa-gates': {
160
+ readonly handler: (ctx: CommandContext) => Promise<string>;
161
+ readonly description: "View or modify QA gate profile for the current plan [enable|override <gate>...]";
162
+ readonly args: "[show|enable|override] <gate>...";
163
+ readonly 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.";
164
+ };
153
165
  readonly promote: {
154
166
  readonly handler: (ctx: CommandContext) => Promise<string>;
155
167
  readonly description: "Manually promote lesson to hive knowledge";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Global SQLite database singleton for opencode-swarm.
3
+ *
4
+ * Owns `global-rules.db` in the platform config directory. Stores cross-project
5
+ * rules and agent prompt sections. Per-project QA gate profiles live in the
6
+ * project DB (see `./project-db.ts`), not here.
7
+ */
8
+ import { Database } from 'bun:sqlite';
9
+ /**
10
+ * Run all pending migrations on the provided database.
11
+ * Idempotent: existing migrations are not re-applied.
12
+ */
13
+ export declare function runGlobalMigrations(db: Database): void;
14
+ /**
15
+ * Return the process-wide singleton global database, creating it on first call.
16
+ * Directory is created if it does not exist. WAL mode is enabled immediately.
17
+ */
18
+ export declare function getGlobalDb(): Database;
19
+ /**
20
+ * Close and clear the global database singleton. Test-only.
21
+ */
22
+ export declare function closeGlobalDb(): void;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Tests for src/db/global-db.ts.
3
+ *
4
+ * Uses XDG_CONFIG_HOME override so getPlatformConfigDir() resolves into
5
+ * a temp directory (Linux only — this test suite is gated accordingly).
6
+ */
7
+ export {};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Barrel re-exports for the opencode-swarm SQLite database layer.
3
+ *
4
+ * - `global-db`: process-wide singleton for cross-project rules and
5
+ * agent prompt sections (`global-rules.db` in the platform config dir).
6
+ * - `project-db`: per-project database cache (`.swarm/swarm.db`), keyed by
7
+ * normalized directory path.
8
+ * - `qa-gate-profile`: service layer for per-plan QA gate profiles stored
9
+ * in the project DB.
10
+ */
11
+ export { closeGlobalDb, getGlobalDb, runGlobalMigrations, } from './global-db.js';
12
+ export { closeAllProjectDbs, closeProjectDb, getProjectDb, projectDbExists, projectDbPath, runProjectMigrations, } from './project-db.js';
13
+ export { computeProfileHash, DEFAULT_QA_GATES, getEffectiveGates, getOrCreateProfile, getProfile, lockProfile, type QaGateProfile, type QaGates, setGates, } from './qa-gate-profile.js';
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Per-project SQLite database for opencode-swarm.
3
+ *
4
+ * Owns `.swarm/swarm.db` in each project directory. Stores per-project
5
+ * constraints and QA gate profiles. One cached instance per normalized
6
+ * directory path.
7
+ */
8
+ import { Database } from 'bun:sqlite';
9
+ /**
10
+ * Run all pending migrations on the provided database.
11
+ * Idempotent: existing migrations are not re-applied.
12
+ */
13
+ export declare function runProjectMigrations(db: Database): void;
14
+ /**
15
+ * Return the absolute path to `.swarm/swarm.db` for the given directory.
16
+ * Does not create the file or any parent directory.
17
+ */
18
+ export declare function projectDbPath(directory: string): string;
19
+ /**
20
+ * Return true iff the project DB file already exists on disk. Does not
21
+ * open the DB, create `.swarm/`, or run migrations. Intended for
22
+ * read-only callers (e.g. `getProfile`) that must avoid mutating the
23
+ * workspace just to check for a missing record.
24
+ */
25
+ export declare function projectDbExists(directory: string): boolean;
26
+ /**
27
+ * Return the cached project database for the given directory, opening it
28
+ * if needed. Creates `.swarm/` if absent and enables WAL + foreign keys.
29
+ */
30
+ export declare function getProjectDb(directory: string): Database;
31
+ /**
32
+ * Close and remove the cached project database for the given directory.
33
+ * Test-only.
34
+ */
35
+ export declare function closeProjectDb(directory: string): void;
36
+ /**
37
+ * Close and remove all cached project databases.
38
+ * Test-only.
39
+ */
40
+ export declare function closeAllProjectDbs(): void;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Tests for src/db/project-db.ts.
3
+ */
4
+ export {};
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Service layer for the `qa_gate_profile` table in the per-project database.
3
+ *
4
+ * A QA gate profile is keyed by plan_id and captures which QA gates are
5
+ * enabled for that plan. Profiles are locked after critic approval; once
6
+ * locked, row updates are rejected by a SQLite trigger and by this service.
7
+ * Sessions can only ratchet gates tighter (enable more), never disable them.
8
+ */
9
+ /**
10
+ * QA gate flags. All seven gates are tracked explicitly.
11
+ */
12
+ export interface QaGates {
13
+ reviewer: boolean;
14
+ test_engineer: boolean;
15
+ council_mode: boolean;
16
+ sme_enabled: boolean;
17
+ critic_pre_plan: boolean;
18
+ hallucination_guard: boolean;
19
+ sast_enabled: boolean;
20
+ }
21
+ /**
22
+ * Default QA gate configuration for newly-created profiles.
23
+ */
24
+ export declare const DEFAULT_QA_GATES: QaGates;
25
+ /**
26
+ * Row-level representation of a persisted QA gate profile.
27
+ */
28
+ export interface QaGateProfile {
29
+ id: number;
30
+ plan_id: string;
31
+ created_at: string;
32
+ project_type: string | null;
33
+ gates: QaGates;
34
+ locked_at: string | null;
35
+ locked_by_snapshot_seq: number | null;
36
+ }
37
+ /**
38
+ * Fetch the profile for `planId` or return null if none exists.
39
+ *
40
+ * Read-only: if `.swarm/swarm.db` does not exist yet, returns null
41
+ * without creating the DB file or running migrations. This keeps callers
42
+ * on read-only paths (`get_approved_plan`, `get_qa_gate_profile`, the
43
+ * `qa-gates show` command) from silently mutating the workspace just by
44
+ * looking for a profile. Write paths (`getOrCreateProfile`, `setGates`,
45
+ * `lockProfile`) continue to initialize the DB on demand.
46
+ */
47
+ export declare function getProfile(directory: string, planId: string): QaGateProfile | null;
48
+ /**
49
+ * Return the existing profile for `planId`, or create a new one seeded with
50
+ * `DEFAULT_QA_GATES` if none exists. Tolerates races on the UNIQUE index.
51
+ */
52
+ export declare function getOrCreateProfile(directory: string, planId: string, projectType?: string): QaGateProfile;
53
+ /**
54
+ * Update gates for `planId`. Gates can only be ratcheted tighter —
55
+ * attempting to disable a currently-enabled gate throws. Throws if the
56
+ * profile is locked.
57
+ */
58
+ export declare function setGates(directory: string, planId: string, gates: Partial<QaGates>): QaGateProfile;
59
+ /**
60
+ * Lock the profile for `planId`, recording the snapshot seq that anchors it.
61
+ * Idempotent: locking an already-locked profile returns it unchanged.
62
+ */
63
+ export declare function lockProfile(directory: string, planId: string, snapshotSeq: number): QaGateProfile;
64
+ /**
65
+ * Compute a SHA-256 hex digest over the stable identity of a profile.
66
+ * Used by `get_approved_plan` for drift detection.
67
+ */
68
+ export declare function computeProfileHash(profile: QaGateProfile): string;
69
+ /**
70
+ * Merge session-level gate overrides on top of the spec-level profile.
71
+ * Session overrides can only ratchet gates tighter (set to true); false
72
+ * values in overrides are ignored.
73
+ *
74
+ * IMPORTANT — caller responsibility: this function is the *computation*
75
+ * of effective gates, not an enforcement point. Enforcement consumers
76
+ * (reviewer dispatch, SAST runner, council convene paths, etc.) must
77
+ * call this at their own check sites, passing the current profile from
78
+ * `getProfile` and the agent session's `qaGateSessionOverrides ?? {}`.
79
+ * Reading raw `profile.gates` directly from an enforcement site will
80
+ * silently ignore operator-applied session overrides. Session overrides
81
+ * are currently surfaced via `/swarm qa-gates show`; wiring additional
82
+ * enforcement consumers is tracked as follow-up work and does not affect
83
+ * spec-level gate correctness on the approved-plan path.
84
+ *
85
+ * Session overrides are intentionally ephemeral — they live only in
86
+ * in-memory `AgentSessionState.qaGateSessionOverrides` and are NOT
87
+ * persisted to the session snapshot. Process restart clears them.
88
+ */
89
+ export declare function getEffectiveGates(profile: QaGateProfile, sessionOverrides: Partial<QaGates>): QaGates;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Tests for src/db/qa-gate-profile.ts.
3
+ */
4
+ export {};