opencode-swarm 6.72.0 → 6.72.1

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
@@ -17537,6 +17537,220 @@ var init_archive = __esm(() => {
17537
17537
  init_manager2();
17538
17538
  });
17539
17539
 
17540
+ // src/db/project-db.ts
17541
+ import { Database } from "bun:sqlite";
17542
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
17543
+ import { join as join6, resolve as resolve4 } from "path";
17544
+ function runProjectMigrations(db) {
17545
+ db.run(`CREATE TABLE IF NOT EXISTS schema_migrations (
17546
+ version INTEGER PRIMARY KEY,
17547
+ name TEXT NOT NULL,
17548
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
17549
+ )`);
17550
+ const row = db.query("SELECT MAX(version) as version FROM schema_migrations").get();
17551
+ const currentVersion = row?.version ?? 0;
17552
+ for (const migration of MIGRATIONS) {
17553
+ if (migration.version <= currentVersion)
17554
+ continue;
17555
+ const apply = db.transaction(() => {
17556
+ db.run(migration.sql);
17557
+ db.run("INSERT INTO schema_migrations (version, name) VALUES (?, ?)", [
17558
+ migration.version,
17559
+ migration.name
17560
+ ]);
17561
+ });
17562
+ apply();
17563
+ }
17564
+ }
17565
+ function projectDbPath(directory) {
17566
+ return join6(resolve4(directory), ".swarm", "swarm.db");
17567
+ }
17568
+ function projectDbExists(directory) {
17569
+ return existsSync4(projectDbPath(directory));
17570
+ }
17571
+ function getProjectDb(directory) {
17572
+ const key = resolve4(directory);
17573
+ const existing = _projectDbs.get(key);
17574
+ if (existing)
17575
+ return existing;
17576
+ const swarmDir = join6(key, ".swarm");
17577
+ mkdirSync3(swarmDir, { recursive: true });
17578
+ const db = new Database(join6(swarmDir, "swarm.db"));
17579
+ db.run("PRAGMA journal_mode = WAL;");
17580
+ db.run("PRAGMA synchronous = NORMAL;");
17581
+ db.run("PRAGMA busy_timeout = 5000;");
17582
+ db.run("PRAGMA foreign_keys = ON;");
17583
+ runProjectMigrations(db);
17584
+ _projectDbs.set(key, db);
17585
+ return db;
17586
+ }
17587
+ var MIGRATIONS, _projectDbs;
17588
+ var init_project_db = __esm(() => {
17589
+ MIGRATIONS = [
17590
+ {
17591
+ version: 1,
17592
+ name: "create_project_constraints",
17593
+ sql: `CREATE TABLE project_constraints (
17594
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17595
+ constraint_type TEXT NOT NULL,
17596
+ content TEXT NOT NULL,
17597
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
17598
+ )`
17599
+ },
17600
+ {
17601
+ version: 2,
17602
+ name: "create_qa_gate_profile",
17603
+ sql: `CREATE TABLE qa_gate_profile (
17604
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17605
+ plan_id TEXT NOT NULL UNIQUE,
17606
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
17607
+ project_type TEXT,
17608
+ gates TEXT NOT NULL DEFAULT '{}',
17609
+ locked_at TEXT,
17610
+ locked_by_snapshot_seq INTEGER
17611
+ )`
17612
+ },
17613
+ {
17614
+ version: 3,
17615
+ name: "create_qa_gate_profile_immutability_trigger",
17616
+ sql: `CREATE TRIGGER IF NOT EXISTS trg_qa_gate_profile_no_update_after_lock
17617
+ BEFORE UPDATE ON qa_gate_profile
17618
+ WHEN OLD.locked_at IS NOT NULL
17619
+ BEGIN
17620
+ SELECT RAISE(ABORT, 'qa_gate_profile row is locked and cannot be modified after critic approval');
17621
+ END`
17622
+ }
17623
+ ];
17624
+ _projectDbs = new Map;
17625
+ });
17626
+
17627
+ // src/db/qa-gate-profile.ts
17628
+ import { createHash as createHash3 } from "crypto";
17629
+ function rowToProfile(row) {
17630
+ let parsed = {};
17631
+ try {
17632
+ parsed = JSON.parse(row.gates);
17633
+ } catch {
17634
+ parsed = {};
17635
+ }
17636
+ const gates = { ...DEFAULT_QA_GATES, ...parsed };
17637
+ return {
17638
+ id: row.id,
17639
+ plan_id: row.plan_id,
17640
+ created_at: row.created_at,
17641
+ project_type: row.project_type,
17642
+ gates,
17643
+ locked_at: row.locked_at,
17644
+ locked_by_snapshot_seq: row.locked_by_snapshot_seq
17645
+ };
17646
+ }
17647
+ function getProfile(directory, planId) {
17648
+ if (!projectDbExists(directory))
17649
+ return null;
17650
+ const db = getProjectDb(directory);
17651
+ const row = db.query("SELECT * FROM qa_gate_profile WHERE plan_id = ?").get(planId);
17652
+ return row ? rowToProfile(row) : null;
17653
+ }
17654
+ function getOrCreateProfile(directory, planId, projectType) {
17655
+ const existing = getProfile(directory, planId);
17656
+ if (existing)
17657
+ return existing;
17658
+ const db = getProjectDb(directory);
17659
+ const gatesJson = JSON.stringify(DEFAULT_QA_GATES);
17660
+ const insert = db.transaction(() => {
17661
+ db.run("INSERT INTO qa_gate_profile (plan_id, project_type, gates) VALUES (?, ?, ?)", [planId, projectType ?? null, gatesJson]);
17662
+ });
17663
+ try {
17664
+ insert();
17665
+ } catch (err2) {
17666
+ const msg = err2 instanceof Error ? err2.message : String(err2);
17667
+ if (!msg.toLowerCase().includes("unique")) {
17668
+ throw err2;
17669
+ }
17670
+ }
17671
+ const after = getProfile(directory, planId);
17672
+ if (!after) {
17673
+ throw new Error(`Failed to create or load QA gate profile for plan_id=${planId}`);
17674
+ }
17675
+ return after;
17676
+ }
17677
+ function setGates(directory, planId, gates) {
17678
+ const current = getProfile(directory, planId);
17679
+ if (!current) {
17680
+ throw new Error(`No QA gate profile found for plan_id=${planId} \u2014 call getOrCreateProfile first`);
17681
+ }
17682
+ if (current.locked_at !== null) {
17683
+ throw new Error("Cannot modify gates: QA gate profile is locked after critic approval");
17684
+ }
17685
+ const merged = { ...current.gates };
17686
+ for (const key of Object.keys(gates)) {
17687
+ const incoming = gates[key];
17688
+ if (incoming === undefined)
17689
+ continue;
17690
+ if (incoming === false && current.gates[key] === true) {
17691
+ throw new Error(`Cannot disable gate '${key}': sessions can only ratchet tighter`);
17692
+ }
17693
+ if (incoming === true) {
17694
+ merged[key] = true;
17695
+ }
17696
+ }
17697
+ const db = getProjectDb(directory);
17698
+ db.run("UPDATE qa_gate_profile SET gates = ? WHERE plan_id = ?", [
17699
+ JSON.stringify(merged),
17700
+ planId
17701
+ ]);
17702
+ const updated = getProfile(directory, planId);
17703
+ if (!updated) {
17704
+ throw new Error(`Failed to re-read QA gate profile after update for plan_id=${planId}`);
17705
+ }
17706
+ return updated;
17707
+ }
17708
+ function lockProfile(directory, planId, snapshotSeq) {
17709
+ const current = getProfile(directory, planId);
17710
+ if (!current) {
17711
+ throw new Error(`No QA gate profile found for plan_id=${planId} \u2014 cannot lock`);
17712
+ }
17713
+ if (current.locked_at !== null) {
17714
+ return current;
17715
+ }
17716
+ const db = getProjectDb(directory);
17717
+ db.run("UPDATE qa_gate_profile SET locked_at = datetime('now'), locked_by_snapshot_seq = ? WHERE plan_id = ?", [snapshotSeq, planId]);
17718
+ const locked = getProfile(directory, planId);
17719
+ if (!locked) {
17720
+ throw new Error(`Failed to re-read locked QA gate profile for plan_id=${planId}`);
17721
+ }
17722
+ return locked;
17723
+ }
17724
+ function computeProfileHash(profile) {
17725
+ const payload = JSON.stringify({
17726
+ plan_id: profile.plan_id,
17727
+ gates: profile.gates
17728
+ });
17729
+ return createHash3("sha256").update(payload).digest("hex");
17730
+ }
17731
+ function getEffectiveGates(profile, sessionOverrides) {
17732
+ const merged = { ...profile.gates };
17733
+ for (const key of Object.keys(sessionOverrides)) {
17734
+ if (sessionOverrides[key] === true) {
17735
+ merged[key] = true;
17736
+ }
17737
+ }
17738
+ return merged;
17739
+ }
17740
+ var DEFAULT_QA_GATES;
17741
+ var init_qa_gate_profile = __esm(() => {
17742
+ init_project_db();
17743
+ DEFAULT_QA_GATES = {
17744
+ reviewer: true,
17745
+ test_engineer: true,
17746
+ council_mode: false,
17747
+ sme_enabled: true,
17748
+ critic_pre_plan: true,
17749
+ hallucination_guard: false,
17750
+ sast_enabled: true
17751
+ };
17752
+ });
17753
+
17540
17754
  // src/environment/profile.ts
17541
17755
  function detectHostOS() {
17542
17756
  switch (process.platform) {
@@ -21359,12 +21573,12 @@ var require_adapter = __commonJS((exports, module2) => {
21359
21573
  return newFs;
21360
21574
  }
21361
21575
  function toPromise(method) {
21362
- return (...args2) => new Promise((resolve4, reject) => {
21576
+ return (...args2) => new Promise((resolve5, reject) => {
21363
21577
  args2.push((err2, result) => {
21364
21578
  if (err2) {
21365
21579
  reject(err2);
21366
21580
  } else {
21367
- resolve4(result);
21581
+ resolve5(result);
21368
21582
  }
21369
21583
  });
21370
21584
  method(...args2);
@@ -24112,7 +24326,7 @@ __export(exports_gate_evidence, {
24112
24326
  deriveRequiredGates: () => deriveRequiredGates,
24113
24327
  DEFAULT_REQUIRED_GATES: () => DEFAULT_REQUIRED_GATES
24114
24328
  });
24115
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, renameSync as renameSync5, unlinkSync as unlinkSync3 } from "fs";
24329
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, renameSync as renameSync5, unlinkSync as unlinkSync3 } from "fs";
24116
24330
  import * as path9 from "path";
24117
24331
  function isValidTaskId(taskId) {
24118
24332
  return isStrictTaskId(taskId);
@@ -24177,7 +24391,7 @@ async function recordGateEvidence(directory, taskId, gate, sessionId, turbo) {
24177
24391
  assertValidTaskId(taskId);
24178
24392
  const evidenceDir = getEvidenceDir(directory);
24179
24393
  const evidencePath = getEvidencePath(directory, taskId);
24180
- mkdirSync5(evidenceDir, { recursive: true });
24394
+ mkdirSync6(evidenceDir, { recursive: true });
24181
24395
  const existing = readExisting(evidencePath);
24182
24396
  const requiredGates = existing ? expandRequiredGates(existing.required_gates, gate) : deriveRequiredGates(gate);
24183
24397
  const updated = {
@@ -24200,7 +24414,7 @@ async function recordAgentDispatch(directory, taskId, agentType, turbo) {
24200
24414
  assertValidTaskId(taskId);
24201
24415
  const evidenceDir = getEvidenceDir(directory);
24202
24416
  const evidencePath = getEvidencePath(directory, taskId);
24203
- mkdirSync5(evidenceDir, { recursive: true });
24417
+ mkdirSync6(evidenceDir, { recursive: true });
24204
24418
  const existing = readExisting(evidencePath);
24205
24419
  const requiredGates = existing ? expandRequiredGates(existing.required_gates, agentType) : deriveRequiredGates(agentType);
24206
24420
  const updated = {
@@ -24379,6 +24593,37 @@ function createDelegationGateHook(config2, directory) {
24379
24593
  if (!session)
24380
24594
  return;
24381
24595
  const normalized = normalizeToolName(input.tool);
24596
+ const councilActive = await isCouncilGateActive(directory, config2.council);
24597
+ if (normalized === "convene_council") {
24598
+ try {
24599
+ const parsed = typeof _output === "string" ? JSON.parse(_output) : _output;
24600
+ const result = parsed;
24601
+ if (result && typeof result === "object" && result.success === true && typeof result.overallVerdict === "string") {
24602
+ const directArgs = input.args;
24603
+ const storedArgs = getStoredInputArgs(input.callID);
24604
+ const taskIdRaw = directArgs?.taskId ?? storedArgs?.taskId;
24605
+ const taskId = typeof taskIdRaw === "string" ? taskIdRaw : null;
24606
+ if (taskId) {
24607
+ if (!session.taskCouncilApproved)
24608
+ session.taskCouncilApproved = new Map;
24609
+ session.taskCouncilApproved.set(taskId, {
24610
+ verdict: result.overallVerdict,
24611
+ roundNumber: typeof result.roundNumber === "number" ? result.roundNumber : 1
24612
+ });
24613
+ if (councilActive && result.overallVerdict === "APPROVE" && result.allCriteriaMet === true && (result.requiredFixesCount ?? 0) === 0) {
24614
+ try {
24615
+ advanceTaskState(session, taskId, "complete");
24616
+ } catch (err2) {
24617
+ console.warn(`[delegation-gate] toolAfter convene_council: could not advance ${taskId} \u2192 complete: ${err2 instanceof Error ? err2.message : String(err2)}`);
24618
+ }
24619
+ }
24620
+ }
24621
+ }
24622
+ } catch (err2) {
24623
+ console.warn(`[delegation-gate] toolAfter convene_council: failed to parse output: ${err2 instanceof Error ? err2.message : String(err2)}`);
24624
+ }
24625
+ return;
24626
+ }
24382
24627
  if (normalized === "Task" || normalized === "task") {
24383
24628
  const directArgs = input.args;
24384
24629
  const storedArgs = getStoredInputArgs(input.callID);
@@ -24391,60 +24636,62 @@ function createDelegationGateHook(config2, directory) {
24391
24636
  hasReviewer = true;
24392
24637
  if (targetAgent === "test_engineer")
24393
24638
  hasTestEngineer = true;
24394
- if (targetAgent === "reviewer" && session.taskWorkflowStates) {
24395
- for (const [taskId, state] of session.taskWorkflowStates) {
24396
- if (state === "coder_delegated" || state === "pre_check_passed") {
24397
- try {
24398
- advanceTaskState(session, taskId, "reviewer_run");
24399
- } catch (err2) {
24400
- console.warn(`[delegation-gate] toolAfter: could not advance ${taskId} (${state}) \u2192 reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24639
+ if (!councilActive) {
24640
+ if (targetAgent === "reviewer" && session.taskWorkflowStates) {
24641
+ for (const [taskId, state] of session.taskWorkflowStates) {
24642
+ if (state === "coder_delegated" || state === "pre_check_passed") {
24643
+ try {
24644
+ advanceTaskState(session, taskId, "reviewer_run");
24645
+ } catch (err2) {
24646
+ console.warn(`[delegation-gate] toolAfter: could not advance ${taskId} (${state}) \u2192 reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24647
+ }
24401
24648
  }
24402
24649
  }
24403
24650
  }
24404
- }
24405
- if (targetAgent === "test_engineer" && session.taskWorkflowStates) {
24406
- for (const [taskId, state] of session.taskWorkflowStates) {
24407
- if (state === "reviewer_run") {
24408
- try {
24409
- advanceTaskState(session, taskId, "tests_run");
24410
- } catch (err2) {
24411
- console.warn(`[delegation-gate] toolAfter: could not advance ${taskId} (${state}) \u2192 tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24651
+ if (targetAgent === "test_engineer" && session.taskWorkflowStates) {
24652
+ for (const [taskId, state] of session.taskWorkflowStates) {
24653
+ if (state === "reviewer_run") {
24654
+ try {
24655
+ advanceTaskState(session, taskId, "tests_run");
24656
+ } catch (err2) {
24657
+ console.warn(`[delegation-gate] toolAfter: could not advance ${taskId} (${state}) \u2192 tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24658
+ }
24412
24659
  }
24413
24660
  }
24414
24661
  }
24415
- }
24416
- if (targetAgent === "reviewer" || targetAgent === "test_engineer") {
24417
- for (const [, otherSession] of swarmState.agentSessions) {
24418
- if (otherSession === session)
24419
- continue;
24420
- if (!otherSession.taskWorkflowStates)
24421
- continue;
24422
- if (targetAgent === "reviewer") {
24423
- const seedTaskId = getSeedTaskId(session);
24424
- if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
24425
- otherSession.taskWorkflowStates.set(seedTaskId, "coder_delegated");
24426
- }
24427
- for (const [taskId, state] of otherSession.taskWorkflowStates) {
24428
- if (state === "coder_delegated" || state === "pre_check_passed") {
24429
- try {
24430
- advanceTaskState(otherSession, taskId, "reviewer_run");
24431
- } catch (err2) {
24432
- console.warn(`[delegation-gate] toolAfter cross-session: could not advance ${taskId} (${state}) \u2192 reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24662
+ if (targetAgent === "reviewer" || targetAgent === "test_engineer") {
24663
+ for (const [, otherSession] of swarmState.agentSessions) {
24664
+ if (otherSession === session)
24665
+ continue;
24666
+ if (!otherSession.taskWorkflowStates)
24667
+ continue;
24668
+ if (targetAgent === "reviewer") {
24669
+ const seedTaskId = getSeedTaskId(session);
24670
+ if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
24671
+ otherSession.taskWorkflowStates.set(seedTaskId, "coder_delegated");
24672
+ }
24673
+ for (const [taskId, state] of otherSession.taskWorkflowStates) {
24674
+ if (state === "coder_delegated" || state === "pre_check_passed") {
24675
+ try {
24676
+ advanceTaskState(otherSession, taskId, "reviewer_run");
24677
+ } catch (err2) {
24678
+ console.warn(`[delegation-gate] toolAfter cross-session: could not advance ${taskId} (${state}) \u2192 reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24679
+ }
24433
24680
  }
24434
24681
  }
24435
24682
  }
24436
- }
24437
- if (targetAgent === "test_engineer") {
24438
- const seedTaskId = getSeedTaskId(session);
24439
- if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
24440
- otherSession.taskWorkflowStates.set(seedTaskId, "reviewer_run");
24441
- }
24442
- for (const [taskId, state] of otherSession.taskWorkflowStates) {
24443
- if (state === "reviewer_run") {
24444
- try {
24445
- advanceTaskState(otherSession, taskId, "tests_run");
24446
- } catch (err2) {
24447
- console.warn(`[delegation-gate] toolAfter cross-session: could not advance ${taskId} (${state}) \u2192 tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24683
+ if (targetAgent === "test_engineer") {
24684
+ const seedTaskId = getSeedTaskId(session);
24685
+ if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
24686
+ otherSession.taskWorkflowStates.set(seedTaskId, "reviewer_run");
24687
+ }
24688
+ for (const [taskId, state] of otherSession.taskWorkflowStates) {
24689
+ if (state === "reviewer_run") {
24690
+ try {
24691
+ advanceTaskState(otherSession, taskId, "tests_run");
24692
+ } catch (err2) {
24693
+ console.warn(`[delegation-gate] toolAfter cross-session: could not advance ${taskId} (${state}) \u2192 tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24694
+ }
24448
24695
  }
24449
24696
  }
24450
24697
  }
@@ -24503,69 +24750,71 @@ function createDelegationGateHook(config2, directory) {
24503
24750
  if (target === "test_engineer")
24504
24751
  hasTestEngineer = true;
24505
24752
  }
24506
- if (lastCoderIndex !== -1 && hasReviewer && hasTestEngineer) {
24507
- session.qaSkipCount = 0;
24508
- session.qaSkipTaskIds = [];
24509
- }
24510
- if (lastCoderIndex !== -1 && hasReviewer && session.taskWorkflowStates) {
24511
- for (const [taskId, state] of session.taskWorkflowStates) {
24512
- if (state === "coder_delegated" || state === "pre_check_passed") {
24513
- try {
24514
- advanceTaskState(session, taskId, "reviewer_run");
24515
- } catch (err2) {
24516
- console.warn(`[delegation-gate] fallback: could not advance ${taskId} (${state}) \u2192 reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24517
- }
24518
- }
24519
- }
24520
- }
24521
- if (lastCoderIndex !== -1 && hasReviewer && hasTestEngineer && session.taskWorkflowStates) {
24522
- for (const [taskId, state] of session.taskWorkflowStates) {
24523
- if (state === "reviewer_run") {
24524
- try {
24525
- advanceTaskState(session, taskId, "tests_run");
24526
- } catch (err2) {
24527
- console.warn(`[delegation-gate] fallback: could not advance ${taskId} (${state}) \u2192 tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24528
- }
24529
- }
24753
+ if (!councilActive) {
24754
+ if (lastCoderIndex !== -1 && hasReviewer && hasTestEngineer) {
24755
+ session.qaSkipCount = 0;
24756
+ session.qaSkipTaskIds = [];
24530
24757
  }
24531
- }
24532
- if (lastCoderIndex !== -1 && hasReviewer) {
24533
- for (const [, otherSession] of swarmState.agentSessions) {
24534
- if (otherSession === session)
24535
- continue;
24536
- if (!otherSession.taskWorkflowStates)
24537
- continue;
24538
- const seedTaskId = getSeedTaskId(session);
24539
- if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
24540
- otherSession.taskWorkflowStates.set(seedTaskId, "coder_delegated");
24541
- }
24542
- for (const [taskId, state] of otherSession.taskWorkflowStates) {
24758
+ if (lastCoderIndex !== -1 && hasReviewer && session.taskWorkflowStates) {
24759
+ for (const [taskId, state] of session.taskWorkflowStates) {
24543
24760
  if (state === "coder_delegated" || state === "pre_check_passed") {
24544
24761
  try {
24545
- advanceTaskState(otherSession, taskId, "reviewer_run");
24762
+ advanceTaskState(session, taskId, "reviewer_run");
24546
24763
  } catch (err2) {
24547
- console.warn(`[delegation-gate] fallback cross-session: could not advance ${taskId} (${state}) \u2192 reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24764
+ console.warn(`[delegation-gate] fallback: could not advance ${taskId} (${state}) \u2192 reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24548
24765
  }
24549
24766
  }
24550
24767
  }
24551
24768
  }
24552
- }
24553
- if (lastCoderIndex !== -1 && hasReviewer && hasTestEngineer) {
24554
- for (const [, otherSession] of swarmState.agentSessions) {
24555
- if (otherSession === session)
24556
- continue;
24557
- if (!otherSession.taskWorkflowStates)
24558
- continue;
24559
- const seedTaskId = getSeedTaskId(session);
24560
- if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
24561
- otherSession.taskWorkflowStates.set(seedTaskId, "reviewer_run");
24562
- }
24563
- for (const [taskId, state] of otherSession.taskWorkflowStates) {
24769
+ if (lastCoderIndex !== -1 && hasReviewer && hasTestEngineer && session.taskWorkflowStates) {
24770
+ for (const [taskId, state] of session.taskWorkflowStates) {
24564
24771
  if (state === "reviewer_run") {
24565
24772
  try {
24566
- advanceTaskState(otherSession, taskId, "tests_run");
24773
+ advanceTaskState(session, taskId, "tests_run");
24567
24774
  } catch (err2) {
24568
- console.warn(`[delegation-gate] fallback cross-session: could not advance ${taskId} (${state}) \u2192 tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24775
+ console.warn(`[delegation-gate] fallback: could not advance ${taskId} (${state}) \u2192 tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24776
+ }
24777
+ }
24778
+ }
24779
+ }
24780
+ if (lastCoderIndex !== -1 && hasReviewer) {
24781
+ for (const [, otherSession] of swarmState.agentSessions) {
24782
+ if (otherSession === session)
24783
+ continue;
24784
+ if (!otherSession.taskWorkflowStates)
24785
+ continue;
24786
+ const seedTaskId = getSeedTaskId(session);
24787
+ if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
24788
+ otherSession.taskWorkflowStates.set(seedTaskId, "coder_delegated");
24789
+ }
24790
+ for (const [taskId, state] of otherSession.taskWorkflowStates) {
24791
+ if (state === "coder_delegated" || state === "pre_check_passed") {
24792
+ try {
24793
+ advanceTaskState(otherSession, taskId, "reviewer_run");
24794
+ } catch (err2) {
24795
+ console.warn(`[delegation-gate] fallback cross-session: could not advance ${taskId} (${state}) \u2192 reviewer_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24796
+ }
24797
+ }
24798
+ }
24799
+ }
24800
+ }
24801
+ if (lastCoderIndex !== -1 && hasReviewer && hasTestEngineer) {
24802
+ for (const [, otherSession] of swarmState.agentSessions) {
24803
+ if (otherSession === session)
24804
+ continue;
24805
+ if (!otherSession.taskWorkflowStates)
24806
+ continue;
24807
+ const seedTaskId = getSeedTaskId(session);
24808
+ if (seedTaskId && !otherSession.taskWorkflowStates.has(seedTaskId)) {
24809
+ otherSession.taskWorkflowStates.set(seedTaskId, "reviewer_run");
24810
+ }
24811
+ for (const [taskId, state] of otherSession.taskWorkflowStates) {
24812
+ if (state === "reviewer_run") {
24813
+ try {
24814
+ advanceTaskState(otherSession, taskId, "tests_run");
24815
+ } catch (err2) {
24816
+ console.warn(`[delegation-gate] fallback cross-session: could not advance ${taskId} (${state}) \u2192 tests_run: ${err2 instanceof Error ? err2.message : String(err2)}`);
24817
+ }
24569
24818
  }
24570
24819
  }
24571
24820
  }
@@ -24836,6 +25085,7 @@ __export(exports_state, {
24836
25085
  rehydrateSessionFromDisk: () => rehydrateSessionFromDisk,
24837
25086
  recordPhaseAgentDispatch: () => recordPhaseAgentDispatch,
24838
25087
  pruneOldWindows: () => pruneOldWindows,
25088
+ isCouncilGateActive: () => isCouncilGateActive,
24839
25089
  hasActiveTurboMode: () => hasActiveTurboMode,
24840
25090
  hasActiveFullAuto: () => hasActiveFullAuto,
24841
25091
  getTaskState: () => getTaskState,
@@ -24848,7 +25098,8 @@ __export(exports_state, {
24848
25098
  buildRehydrationCache: () => buildRehydrationCache,
24849
25099
  beginInvocation: () => beginInvocation,
24850
25100
  applyRehydrationCache: () => applyRehydrationCache,
24851
- advanceTaskState: () => advanceTaskState
25101
+ advanceTaskState: () => advanceTaskState,
25102
+ _resetCouncilDisagreementWarnings: () => _resetCouncilDisagreementWarnings
24852
25103
  });
24853
25104
  import * as fs9 from "fs/promises";
24854
25105
  import * as path11 from "path";
@@ -24868,6 +25119,7 @@ function resetSwarmState() {
24868
25119
  swarmState.fullAutoEnabledInConfig = false;
24869
25120
  swarmState.environmentProfiles.clear();
24870
25121
  clearPendingCoderScope();
25122
+ _councilDisagreementWarned.clear();
24871
25123
  }
24872
25124
  function startAgentSession(sessionId, agentName, staleDurationMs = 7200000, directory) {
24873
25125
  const now = Date.now();
@@ -24906,6 +25158,7 @@ function startAgentSession(sessionId, agentName, staleDurationMs = 7200000, dire
24906
25158
  qaSkipCount: 0,
24907
25159
  qaSkipTaskIds: [],
24908
25160
  taskWorkflowStates: new Map,
25161
+ taskCouncilApproved: new Map,
24909
25162
  lastGateOutcome: null,
24910
25163
  declaredCoderScope: null,
24911
25164
  lastScopeViolation: null,
@@ -25020,6 +25273,9 @@ function ensureAgentSession(sessionId, agentName, directory) {
25020
25273
  if (!session.taskWorkflowStates) {
25021
25274
  session.taskWorkflowStates = new Map;
25022
25275
  }
25276
+ if (!session.taskCouncilApproved) {
25277
+ session.taskCouncilApproved = new Map;
25278
+ }
25023
25279
  if (session.lastGateOutcome === undefined) {
25024
25280
  session.lastGateOutcome = null;
25025
25281
  }
@@ -25174,7 +25430,12 @@ function advanceTaskState(session, taskId, newState) {
25174
25430
  throw new Error(`INVALID_TASK_STATE_TRANSITION: ${taskId} ${current} \u2192 ${newState}`);
25175
25431
  }
25176
25432
  if (newState === "complete" && current !== "tests_run") {
25177
- throw new Error(`INVALID_TASK_STATE_TRANSITION: ${taskId} cannot reach complete from ${current} \u2014 must pass through tests_run first`);
25433
+ const councilEntry = session.taskCouncilApproved?.get(taskId);
25434
+ const councilApproved = councilEntry?.verdict === "APPROVE";
25435
+ const pastPreCheck = currentIndex >= STATE_ORDER.indexOf("pre_check_passed");
25436
+ if (!councilApproved || !pastPreCheck) {
25437
+ throw new Error(`INVALID_TASK_STATE_TRANSITION: ${taskId} cannot reach complete from ${current} \u2014 must pass through tests_run first (or have council APPROVE after pre_check)`);
25438
+ }
25178
25439
  }
25179
25440
  session.taskWorkflowStates.set(taskId, newState);
25180
25441
  telemetry.taskStateChanged(session.agentName, taskId, newState, current);
@@ -25188,6 +25449,48 @@ function getTaskState(session, taskId) {
25188
25449
  }
25189
25450
  return session.taskWorkflowStates.get(taskId) ?? "idle";
25190
25451
  }
25452
+ function derivePlanIdFromPlan(plan) {
25453
+ return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
25454
+ }
25455
+ async function isCouncilGateActive(directory, council) {
25456
+ const enabled = council?.enabled === true;
25457
+ let plan = null;
25458
+ try {
25459
+ plan = await loadPlanJsonOnly(directory);
25460
+ } catch {
25461
+ plan = null;
25462
+ }
25463
+ if (!plan) {
25464
+ return false;
25465
+ }
25466
+ const planId = derivePlanIdFromPlan(plan);
25467
+ let profile = null;
25468
+ try {
25469
+ profile = getProfile(directory, planId);
25470
+ } catch (err2) {
25471
+ const msg = err2 instanceof Error ? err2.message : String(err2);
25472
+ const isBenign = msg.includes("SQLITE_CANTOPEN") || msg.includes("ENOENT");
25473
+ if (!isBenign) {
25474
+ console.warn(`[isCouncilGateActive] getProfile threw unexpectedly for plan ${planId}: ${msg}. Treating council as inactive.`);
25475
+ }
25476
+ profile = null;
25477
+ }
25478
+ if (!profile) {
25479
+ return false;
25480
+ }
25481
+ const councilMode = profile.gates.council_mode === true;
25482
+ if (enabled && councilMode) {
25483
+ return true;
25484
+ }
25485
+ if (enabled !== councilMode && !_councilDisagreementWarned.has(planId)) {
25486
+ _councilDisagreementWarned.add(planId);
25487
+ console.warn(`[delegation-gate] Council mode mismatch for plan ${planId}: ` + `pluginConfig.council.enabled=${enabled}, QaGates.council_mode=${councilMode}. ` + "Falling back to Stage B (non-council) advancement.");
25488
+ }
25489
+ return false;
25490
+ }
25491
+ function _resetCouncilDisagreementWarnings() {
25492
+ _councilDisagreementWarned.clear();
25493
+ }
25191
25494
  function planStatusToWorkflowState(status) {
25192
25495
  switch (status) {
25193
25496
  case "in_progress":
@@ -25273,6 +25576,9 @@ function applyRehydrationCache(session) {
25273
25576
  if (!session.taskWorkflowStates) {
25274
25577
  session.taskWorkflowStates = new Map;
25275
25578
  }
25579
+ if (!session.taskCouncilApproved) {
25580
+ session.taskCouncilApproved = new Map;
25581
+ }
25276
25582
  const { planTaskStates, evidenceMap } = _rehydrationCache;
25277
25583
  const STATE_ORDER = [
25278
25584
  "idle",
@@ -25346,13 +25652,16 @@ function ensureSessionEnvironment(sessionId) {
25346
25652
  }).catch(() => {});
25347
25653
  return profile;
25348
25654
  }
25349
- var _rehydrationCache = null, swarmState;
25655
+ var _rehydrationCache = null, _councilDisagreementWarned, swarmState;
25350
25656
  var init_state = __esm(() => {
25351
25657
  init_constants();
25352
25658
  init_plan_schema();
25353
25659
  init_schema();
25660
+ init_qa_gate_profile();
25354
25661
  init_delegation_gate();
25662
+ init_manager();
25355
25663
  init_telemetry();
25664
+ _councilDisagreementWarned = new Set;
25356
25665
  swarmState = {
25357
25666
  activeToolCalls: new Map,
25358
25667
  toolAggregates: new Map,
@@ -38879,7 +39188,7 @@ var init_branch = __esm(() => {
38879
39188
  });
38880
39189
 
38881
39190
  // src/hooks/knowledge-store.ts
38882
- import { existsSync as existsSync7 } from "fs";
39191
+ import { existsSync as existsSync8 } from "fs";
38883
39192
  import { appendFile as appendFile3, mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
38884
39193
  import * as os3 from "os";
38885
39194
  import * as path13 from "path";
@@ -38907,7 +39216,7 @@ function resolveHiveRejectedPath() {
38907
39216
  return path13.join(path13.dirname(hivePath), "shared-learnings-rejected.jsonl");
38908
39217
  }
38909
39218
  async function readKnowledge(filePath) {
38910
- if (!existsSync7(filePath))
39219
+ if (!existsSync8(filePath))
38911
39220
  return [];
38912
39221
  const content = await readFile3(filePath, "utf-8");
38913
39222
  const results = [];
@@ -39143,7 +39452,7 @@ var init_knowledge_store = __esm(() => {
39143
39452
  });
39144
39453
 
39145
39454
  // src/hooks/knowledge-reader.ts
39146
- import { existsSync as existsSync8 } from "fs";
39455
+ import { existsSync as existsSync9 } from "fs";
39147
39456
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
39148
39457
  import * as path14 from "path";
39149
39458
  function inferCategoriesFromPhase(phaseDescription) {
@@ -39193,7 +39502,7 @@ async function recordLessonsShown(directory, lessonIds, currentPhase) {
39193
39502
  const shownFile = path14.join(directory, ".swarm", ".knowledge-shown.json");
39194
39503
  try {
39195
39504
  let shownData = {};
39196
- if (existsSync8(shownFile)) {
39505
+ if (existsSync9(shownFile)) {
39197
39506
  const content = await readFile4(shownFile, "utf-8");
39198
39507
  shownData = JSON.parse(content);
39199
39508
  }
@@ -39299,7 +39608,7 @@ async function readMergedKnowledge(directory, config3, context) {
39299
39608
  async function updateRetrievalOutcome(directory, phaseInfo, phaseSucceeded) {
39300
39609
  const shownFile = path14.join(directory, ".swarm", ".knowledge-shown.json");
39301
39610
  try {
39302
- if (!existsSync8(shownFile)) {
39611
+ if (!existsSync9(shownFile)) {
39303
39612
  return;
39304
39613
  }
39305
39614
  const content = await readFile4(shownFile, "utf-8");
@@ -40072,7 +40381,7 @@ var init_checkpoint3 = __esm(() => {
40072
40381
  });
40073
40382
 
40074
40383
  // src/session/snapshot-writer.ts
40075
- import { mkdirSync as mkdirSync7, renameSync as renameSync7 } from "fs";
40384
+ import { mkdirSync as mkdirSync8, renameSync as renameSync7 } from "fs";
40076
40385
  import * as path17 from "path";
40077
40386
  function serializeAgentSession(s) {
40078
40387
  const gateLog = {};
@@ -40163,7 +40472,7 @@ async function writeSnapshot(directory, state) {
40163
40472
  const content = JSON.stringify(snapshot, null, 2);
40164
40473
  const resolvedPath = validateSwarmPath(directory, "session/state.json");
40165
40474
  const dir = path17.dirname(resolvedPath);
40166
- mkdirSync7(dir, { recursive: true });
40475
+ mkdirSync8(dir, { recursive: true });
40167
40476
  const tempPath = `${resolvedPath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
40168
40477
  await Bun.write(tempPath, content);
40169
40478
  renameSync7(tempPath, resolvedPath);
@@ -42699,7 +43008,7 @@ var init_dark_matter = __esm(() => {
42699
43008
 
42700
43009
  // src/services/diagnose-service.ts
42701
43010
  import * as child_process4 from "child_process";
42702
- import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
43011
+ import { existsSync as existsSync10, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
42703
43012
  import path23 from "path";
42704
43013
  import { fileURLToPath } from "url";
42705
43014
  function validateTaskDag(plan) {
@@ -42933,7 +43242,7 @@ async function checkConfigBackups(directory) {
42933
43242
  }
42934
43243
  async function checkGitRepository(directory) {
42935
43244
  try {
42936
- if (!existsSync9(directory) || !statSync5(directory).isDirectory()) {
43245
+ if (!existsSync10(directory) || !statSync5(directory).isDirectory()) {
42937
43246
  return {
42938
43247
  name: "Git Repository",
42939
43248
  status: "\u274C",
@@ -42998,7 +43307,7 @@ async function checkSpecStaleness(directory, plan) {
42998
43307
  }
42999
43308
  async function checkConfigParseability(directory) {
43000
43309
  const configPath = path23.join(directory, ".opencode/opencode-swarm.json");
43001
- if (!existsSync9(configPath)) {
43310
+ if (!existsSync10(configPath)) {
43002
43311
  return {
43003
43312
  name: "Config Parseability",
43004
43313
  status: "\u2705",
@@ -43048,11 +43357,11 @@ async function checkGrammarWasmFiles() {
43048
43357
  const isSource = thisDir.replace(/\\/g, "/").endsWith("/src/services");
43049
43358
  const grammarDir = isSource ? path23.join(thisDir, "..", "lang", "grammars") : path23.join(thisDir, "lang", "grammars");
43050
43359
  const missing = [];
43051
- if (!existsSync9(path23.join(grammarDir, "tree-sitter.wasm"))) {
43360
+ if (!existsSync10(path23.join(grammarDir, "tree-sitter.wasm"))) {
43052
43361
  missing.push("tree-sitter.wasm (core runtime)");
43053
43362
  }
43054
43363
  for (const file3 of grammarFiles) {
43055
- if (!existsSync9(path23.join(grammarDir, file3))) {
43364
+ if (!existsSync10(path23.join(grammarDir, file3))) {
43056
43365
  missing.push(file3);
43057
43366
  }
43058
43367
  }
@@ -43071,7 +43380,7 @@ async function checkGrammarWasmFiles() {
43071
43380
  }
43072
43381
  async function checkCheckpointManifest(directory) {
43073
43382
  const manifestPath = path23.join(directory, ".swarm/checkpoints.json");
43074
- if (!existsSync9(manifestPath)) {
43383
+ if (!existsSync10(manifestPath)) {
43075
43384
  return {
43076
43385
  name: "Checkpoint Manifest",
43077
43386
  status: "\u2705",
@@ -43123,7 +43432,7 @@ async function checkCheckpointManifest(directory) {
43123
43432
  }
43124
43433
  async function checkEventStreamIntegrity(directory) {
43125
43434
  const eventsPath = path23.join(directory, ".swarm/events.jsonl");
43126
- if (!existsSync9(eventsPath)) {
43435
+ if (!existsSync10(eventsPath)) {
43127
43436
  return {
43128
43437
  name: "Event Stream",
43129
43438
  status: "\u2705",
@@ -43164,7 +43473,7 @@ async function checkEventStreamIntegrity(directory) {
43164
43473
  }
43165
43474
  async function checkSteeringDirectives(directory) {
43166
43475
  const eventsPath = path23.join(directory, ".swarm/events.jsonl");
43167
- if (!existsSync9(eventsPath)) {
43476
+ if (!existsSync10(eventsPath)) {
43168
43477
  return {
43169
43478
  name: "Steering Directives",
43170
43479
  status: "\u2705",
@@ -43220,7 +43529,7 @@ async function checkCurator(directory) {
43220
43529
  };
43221
43530
  }
43222
43531
  const summaryPath = path23.join(directory, ".swarm/curator-summary.json");
43223
- if (!existsSync9(summaryPath)) {
43532
+ if (!existsSync10(summaryPath)) {
43224
43533
  return {
43225
43534
  name: "Curator",
43226
43535
  status: "\u2705",
@@ -43368,7 +43677,7 @@ async function getDiagnoseData(directory) {
43368
43677
  checks5.push(await checkCurator(directory));
43369
43678
  try {
43370
43679
  const evidenceDir = path23.join(directory, ".swarm", "evidence");
43371
- const snapshotFiles = existsSync9(evidenceDir) ? readdirSync3(evidenceDir).filter((f) => f.startsWith("agent-tools-") && f.endsWith(".json")) : [];
43680
+ const snapshotFiles = existsSync10(evidenceDir) ? readdirSync3(evidenceDir).filter((f) => f.startsWith("agent-tools-") && f.endsWith(".json")) : [];
43372
43681
  if (snapshotFiles.length > 0) {
43373
43682
  const latest = snapshotFiles.sort().pop();
43374
43683
  checks5.push({
@@ -45033,7 +45342,7 @@ var init_profiles = __esm(() => {
45033
45342
 
45034
45343
  // src/lang/detector.ts
45035
45344
  import { access as access2, readdir as readdir3 } from "fs/promises";
45036
- import { extname as extname2, join as join21 } from "path";
45345
+ import { extname as extname2, join as join22 } from "path";
45037
45346
  function getProfileForFile(filePath) {
45038
45347
  const ext = extname2(filePath);
45039
45348
  if (!ext)
@@ -45055,7 +45364,7 @@ async function detectProjectLanguages(projectDir) {
45055
45364
  if (detectFile.includes("*") || detectFile.includes("?"))
45056
45365
  continue;
45057
45366
  try {
45058
- await access2(join21(dir, detectFile));
45367
+ await access2(join22(dir, detectFile));
45059
45368
  detected.add(profile.id);
45060
45369
  break;
45061
45370
  } catch {}
@@ -45076,7 +45385,7 @@ async function detectProjectLanguages(projectDir) {
45076
45385
  const topEntries = await readdir3(projectDir, { withFileTypes: true });
45077
45386
  for (const entry of topEntries) {
45078
45387
  if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
45079
- await scanDir(join21(projectDir, entry.name));
45388
+ await scanDir(join22(projectDir, entry.name));
45080
45389
  }
45081
45390
  }
45082
45391
  } catch {}
@@ -46763,14 +47072,14 @@ var init_history = __esm(() => {
46763
47072
 
46764
47073
  // src/hooks/knowledge-migrator.ts
46765
47074
  import { randomUUID as randomUUID3 } from "crypto";
46766
- import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
47075
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
46767
47076
  import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
46768
47077
  import * as path27 from "path";
46769
47078
  async function migrateContextToKnowledge(directory, config3) {
46770
47079
  const sentinelPath = path27.join(directory, ".swarm", ".knowledge-migrated");
46771
47080
  const contextPath = path27.join(directory, ".swarm", "context.md");
46772
47081
  const knowledgePath = resolveSwarmKnowledgePath(directory);
46773
- if (existsSync13(sentinelPath)) {
47082
+ if (existsSync14(sentinelPath)) {
46774
47083
  return {
46775
47084
  migrated: false,
46776
47085
  entriesMigrated: 0,
@@ -46779,7 +47088,7 @@ async function migrateContextToKnowledge(directory, config3) {
46779
47088
  skippedReason: "sentinel-exists"
46780
47089
  };
46781
47090
  }
46782
- if (!existsSync13(contextPath)) {
47091
+ if (!existsSync14(contextPath)) {
46783
47092
  return {
46784
47093
  migrated: false,
46785
47094
  entriesMigrated: 0,
@@ -46965,7 +47274,7 @@ function truncateLesson(text) {
46965
47274
  }
46966
47275
  function inferProjectName(directory) {
46967
47276
  const packageJsonPath = path27.join(directory, "package.json");
46968
- if (existsSync13(packageJsonPath)) {
47277
+ if (existsSync14(packageJsonPath)) {
46969
47278
  try {
46970
47279
  const pkg = JSON.parse(readFileSync11(packageJsonPath, "utf-8"));
46971
47280
  if (pkg.name && typeof pkg.name === "string") {
@@ -47533,7 +47842,7 @@ async function _detectAvailableLinter(_projectDir, biomeBin, eslintBin) {
47533
47842
  stderr: "pipe"
47534
47843
  });
47535
47844
  const biomeExit = biomeProc.exited;
47536
- const timeout = new Promise((resolve9) => setTimeout(() => resolve9("timeout"), DETECT_TIMEOUT));
47845
+ const timeout = new Promise((resolve10) => setTimeout(() => resolve10("timeout"), DETECT_TIMEOUT));
47537
47846
  const result = await Promise.race([biomeExit, timeout]);
47538
47847
  if (result === "timeout") {
47539
47848
  biomeProc.kill();
@@ -47547,7 +47856,7 @@ async function _detectAvailableLinter(_projectDir, biomeBin, eslintBin) {
47547
47856
  stderr: "pipe"
47548
47857
  });
47549
47858
  const eslintExit = eslintProc.exited;
47550
- const timeout = new Promise((resolve9) => setTimeout(() => resolve9("timeout"), DETECT_TIMEOUT));
47859
+ const timeout = new Promise((resolve10) => setTimeout(() => resolve10("timeout"), DETECT_TIMEOUT));
47551
47860
  const result = await Promise.race([eslintExit, timeout]);
47552
47861
  if (result === "timeout") {
47553
47862
  eslintProc.kill();
@@ -48930,15 +49239,15 @@ function appendTestRun(record3, workingDir) {
48930
49239
  prunedRecords.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
48931
49240
  try {
48932
49241
  const lines = prunedRecords.map((rec) => JSON.stringify(rec));
48933
- const content = lines.join(`
48934
- `) + `
49242
+ const content = `${lines.join(`
49243
+ `)}
48935
49244
  `;
48936
- const tempPath = historyPath + ".tmp";
49245
+ const tempPath = `${historyPath}.tmp`;
48937
49246
  fs21.writeFileSync(tempPath, content, "utf-8");
48938
49247
  fs21.renameSync(tempPath, historyPath);
48939
49248
  } catch (err2) {
48940
49249
  try {
48941
- const tempPath = historyPath + ".tmp";
49250
+ const tempPath = `${historyPath}.tmp`;
48942
49251
  if (fs21.existsSync(tempPath)) {
48943
49252
  fs21.unlinkSync(tempPath);
48944
49253
  }
@@ -49746,9 +50055,9 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
49746
50055
  stderr: "pipe",
49747
50056
  cwd
49748
50057
  });
49749
- const timeoutPromise = new Promise((resolve12) => setTimeout(() => {
50058
+ const timeoutPromise = new Promise((resolve13) => setTimeout(() => {
49750
50059
  proc.kill();
49751
- resolve12(-1);
50060
+ resolve13(-1);
49752
50061
  }, timeout_ms));
49753
50062
  const [exitCode, stdoutResult, stderrResult] = await Promise.all([
49754
50063
  Promise.race([proc.exited, timeoutPromise]),
@@ -51027,220 +51336,6 @@ var init_promote = __esm(() => {
51027
51336
  init_hive_promoter2();
51028
51337
  });
51029
51338
 
51030
- // src/db/project-db.ts
51031
- import { Database } from "bun:sqlite";
51032
- import { existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
51033
- import { join as join30, resolve as resolve13 } from "path";
51034
- function runProjectMigrations(db) {
51035
- db.run(`CREATE TABLE IF NOT EXISTS schema_migrations (
51036
- version INTEGER PRIMARY KEY,
51037
- name TEXT NOT NULL,
51038
- applied_at TEXT NOT NULL DEFAULT (datetime('now'))
51039
- )`);
51040
- const row = db.query("SELECT MAX(version) as version FROM schema_migrations").get();
51041
- const currentVersion = row?.version ?? 0;
51042
- for (const migration of MIGRATIONS) {
51043
- if (migration.version <= currentVersion)
51044
- continue;
51045
- const apply = db.transaction(() => {
51046
- db.run(migration.sql);
51047
- db.run("INSERT INTO schema_migrations (version, name) VALUES (?, ?)", [
51048
- migration.version,
51049
- migration.name
51050
- ]);
51051
- });
51052
- apply();
51053
- }
51054
- }
51055
- function projectDbPath(directory) {
51056
- return join30(resolve13(directory), ".swarm", "swarm.db");
51057
- }
51058
- function projectDbExists(directory) {
51059
- return existsSync19(projectDbPath(directory));
51060
- }
51061
- function getProjectDb(directory) {
51062
- const key = resolve13(directory);
51063
- const existing = _projectDbs.get(key);
51064
- if (existing)
51065
- return existing;
51066
- const swarmDir = join30(key, ".swarm");
51067
- mkdirSync11(swarmDir, { recursive: true });
51068
- const db = new Database(join30(swarmDir, "swarm.db"));
51069
- db.run("PRAGMA journal_mode = WAL;");
51070
- db.run("PRAGMA synchronous = NORMAL;");
51071
- db.run("PRAGMA busy_timeout = 5000;");
51072
- db.run("PRAGMA foreign_keys = ON;");
51073
- runProjectMigrations(db);
51074
- _projectDbs.set(key, db);
51075
- return db;
51076
- }
51077
- var MIGRATIONS, _projectDbs;
51078
- var init_project_db = __esm(() => {
51079
- MIGRATIONS = [
51080
- {
51081
- version: 1,
51082
- name: "create_project_constraints",
51083
- sql: `CREATE TABLE project_constraints (
51084
- id INTEGER PRIMARY KEY AUTOINCREMENT,
51085
- constraint_type TEXT NOT NULL,
51086
- content TEXT NOT NULL,
51087
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
51088
- )`
51089
- },
51090
- {
51091
- version: 2,
51092
- name: "create_qa_gate_profile",
51093
- sql: `CREATE TABLE qa_gate_profile (
51094
- id INTEGER PRIMARY KEY AUTOINCREMENT,
51095
- plan_id TEXT NOT NULL UNIQUE,
51096
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
51097
- project_type TEXT,
51098
- gates TEXT NOT NULL DEFAULT '{}',
51099
- locked_at TEXT,
51100
- locked_by_snapshot_seq INTEGER
51101
- )`
51102
- },
51103
- {
51104
- version: 3,
51105
- name: "create_qa_gate_profile_immutability_trigger",
51106
- sql: `CREATE TRIGGER IF NOT EXISTS trg_qa_gate_profile_no_update_after_lock
51107
- BEFORE UPDATE ON qa_gate_profile
51108
- WHEN OLD.locked_at IS NOT NULL
51109
- BEGIN
51110
- SELECT RAISE(ABORT, 'qa_gate_profile row is locked and cannot be modified after critic approval');
51111
- END`
51112
- }
51113
- ];
51114
- _projectDbs = new Map;
51115
- });
51116
-
51117
- // src/db/qa-gate-profile.ts
51118
- import { createHash as createHash4 } from "crypto";
51119
- function rowToProfile(row) {
51120
- let parsed = {};
51121
- try {
51122
- parsed = JSON.parse(row.gates);
51123
- } catch {
51124
- parsed = {};
51125
- }
51126
- const gates = { ...DEFAULT_QA_GATES, ...parsed };
51127
- return {
51128
- id: row.id,
51129
- plan_id: row.plan_id,
51130
- created_at: row.created_at,
51131
- project_type: row.project_type,
51132
- gates,
51133
- locked_at: row.locked_at,
51134
- locked_by_snapshot_seq: row.locked_by_snapshot_seq
51135
- };
51136
- }
51137
- function getProfile(directory, planId) {
51138
- if (!projectDbExists(directory))
51139
- return null;
51140
- const db = getProjectDb(directory);
51141
- const row = db.query("SELECT * FROM qa_gate_profile WHERE plan_id = ?").get(planId);
51142
- return row ? rowToProfile(row) : null;
51143
- }
51144
- function getOrCreateProfile(directory, planId, projectType) {
51145
- const existing = getProfile(directory, planId);
51146
- if (existing)
51147
- return existing;
51148
- const db = getProjectDb(directory);
51149
- const gatesJson = JSON.stringify(DEFAULT_QA_GATES);
51150
- const insert = db.transaction(() => {
51151
- db.run("INSERT INTO qa_gate_profile (plan_id, project_type, gates) VALUES (?, ?, ?)", [planId, projectType ?? null, gatesJson]);
51152
- });
51153
- try {
51154
- insert();
51155
- } catch (err2) {
51156
- const msg = err2 instanceof Error ? err2.message : String(err2);
51157
- if (!msg.toLowerCase().includes("unique")) {
51158
- throw err2;
51159
- }
51160
- }
51161
- const after = getProfile(directory, planId);
51162
- if (!after) {
51163
- throw new Error(`Failed to create or load QA gate profile for plan_id=${planId}`);
51164
- }
51165
- return after;
51166
- }
51167
- function setGates(directory, planId, gates) {
51168
- const current = getProfile(directory, planId);
51169
- if (!current) {
51170
- throw new Error(`No QA gate profile found for plan_id=${planId} \u2014 call getOrCreateProfile first`);
51171
- }
51172
- if (current.locked_at !== null) {
51173
- throw new Error("Cannot modify gates: QA gate profile is locked after critic approval");
51174
- }
51175
- const merged = { ...current.gates };
51176
- for (const key of Object.keys(gates)) {
51177
- const incoming = gates[key];
51178
- if (incoming === undefined)
51179
- continue;
51180
- if (incoming === false && current.gates[key] === true) {
51181
- throw new Error(`Cannot disable gate '${key}': sessions can only ratchet tighter`);
51182
- }
51183
- if (incoming === true) {
51184
- merged[key] = true;
51185
- }
51186
- }
51187
- const db = getProjectDb(directory);
51188
- db.run("UPDATE qa_gate_profile SET gates = ? WHERE plan_id = ?", [
51189
- JSON.stringify(merged),
51190
- planId
51191
- ]);
51192
- const updated = getProfile(directory, planId);
51193
- if (!updated) {
51194
- throw new Error(`Failed to re-read QA gate profile after update for plan_id=${planId}`);
51195
- }
51196
- return updated;
51197
- }
51198
- function lockProfile(directory, planId, snapshotSeq) {
51199
- const current = getProfile(directory, planId);
51200
- if (!current) {
51201
- throw new Error(`No QA gate profile found for plan_id=${planId} \u2014 cannot lock`);
51202
- }
51203
- if (current.locked_at !== null) {
51204
- return current;
51205
- }
51206
- const db = getProjectDb(directory);
51207
- db.run("UPDATE qa_gate_profile SET locked_at = datetime('now'), locked_by_snapshot_seq = ? WHERE plan_id = ?", [snapshotSeq, planId]);
51208
- const locked = getProfile(directory, planId);
51209
- if (!locked) {
51210
- throw new Error(`Failed to re-read locked QA gate profile for plan_id=${planId}`);
51211
- }
51212
- return locked;
51213
- }
51214
- function computeProfileHash(profile) {
51215
- const payload = JSON.stringify({
51216
- plan_id: profile.plan_id,
51217
- gates: profile.gates
51218
- });
51219
- return createHash4("sha256").update(payload).digest("hex");
51220
- }
51221
- function getEffectiveGates(profile, sessionOverrides) {
51222
- const merged = { ...profile.gates };
51223
- for (const key of Object.keys(sessionOverrides)) {
51224
- if (sessionOverrides[key] === true) {
51225
- merged[key] = true;
51226
- }
51227
- }
51228
- return merged;
51229
- }
51230
- var DEFAULT_QA_GATES;
51231
- var init_qa_gate_profile = __esm(() => {
51232
- init_project_db();
51233
- DEFAULT_QA_GATES = {
51234
- reviewer: true,
51235
- test_engineer: true,
51236
- council_mode: false,
51237
- sme_enabled: true,
51238
- critic_pre_plan: true,
51239
- hallucination_guard: false,
51240
- sast_enabled: true
51241
- };
51242
- });
51243
-
51244
51339
  // src/commands/qa-gates.ts
51245
51340
  function derivePlanId(plan) {
51246
51341
  return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
@@ -53250,8 +53345,7 @@ function buildCouncilWorkflow(council) {
53250
53345
  return `## Work Complete Council (when enabled)
53251
53346
 
53252
53347
  When \`council.enabled\` is true, every task goes through a four-phase verification
53253
- gate before advancing to \`complete\`. This supplements \u2014 does NOT replace \u2014 the
53254
- existing precheckbatch / reviewer / test_engineer gate sequence.
53348
+ gate before advancing to \`complete\`. When council is authoritative, this REPLACES Stage B (reviewer + test_engineer as standalone delegations). Stage A (precheckbatch) still runs as the pre-review gate; Phase 1 dispatch of reviewer and test_engineer is the sole review pass for this task.
53255
53349
 
53256
53350
  ### Phase 0 \u2014 Pre-declare criteria (at plan time, BEFORE dispatching the coder)
53257
53351
  Call \`declare_council_criteria\` for each task with at least 3 concrete,
@@ -53306,15 +53400,32 @@ architect resolves any \`unresolvedConflicts\` in \`unifiedFeedbackMd\` BEFORE
53306
53400
  sending it to the coder \u2014 the coder never sees contradictory instructions
53307
53401
  from different members.`;
53308
53402
  }
53309
- function buildYourToolsList() {
53403
+ function buildYourToolsList(council) {
53310
53404
  const tools = AGENT_TOOL_MAP.architect ?? [];
53311
53405
  const sorted = [...tools].sort();
53312
- return `Task (delegation), ${sorted.join(", ")}.`;
53406
+ const filtered = council?.enabled === true ? sorted : sorted.filter((t) => t !== "convene_council" && t !== "declare_council_criteria");
53407
+ return `Task (delegation), ${filtered.join(", ")}.`;
53408
+ }
53409
+ function buildQaGateSelectionDialogue(modeLabel) {
53410
+ const leadIn = modeLabel === "BRAINSTORM" ? "Now ask the user which QA gates to enable for this plan \u2014 do not select on their behalf." : modeLabel === "SPECIFY" ? "Ask the user which QA gates to enable for this plan before suggesting the next step." : "No pending gate selection found in `.swarm/context.md`. Ask the user inline now.";
53411
+ return `${leadIn}
53412
+
53413
+ Present the seven gates with their defaults (DEFAULT_QA_GATES) as a single user-facing question. Offer the user a one-shot choice: accept defaults, or customize. The seven gates are:
53414
+ - reviewer (default: ON) \u2014 code review of coder output
53415
+ - test_engineer (default: ON) \u2014 test verification of coder output
53416
+ - sme_enabled (default: ON) \u2014 SME consultation during planning/clarification
53417
+ - critic_pre_plan (default: ON) \u2014 critic review before plan finalization
53418
+ - sast_enabled (default: ON) \u2014 static security scanning
53419
+ - council_mode (default: OFF) \u2014 multi-member council gate (recommended for high-impact architecture, public APIs, schema/data mutation, security-sensitive code)
53420
+ - hallucination_guard (default: OFF) \u2014 claim verification (recommended for claim-heavy or research-heavy work)
53421
+
53422
+ One question, one message, defaults pre-stated. Wait for the user's answer.`;
53313
53423
  }
53314
- function buildAvailableToolsList() {
53424
+ function buildAvailableToolsList(council) {
53315
53425
  const tools = AGENT_TOOL_MAP.architect ?? [];
53316
53426
  const sorted = [...tools].sort();
53317
- return sorted.map((t) => {
53427
+ const filtered = council?.enabled === true ? sorted : sorted.filter((t) => t !== "convene_council" && t !== "declare_council_criteria");
53428
+ return filtered.map((t) => {
53318
53429
  const desc = TOOL_DESCRIPTIONS[t];
53319
53430
  return desc ? `${t} (${desc})` : t;
53320
53431
  }).join(", ");
@@ -53455,7 +53566,8 @@ function createArchitectAgent(model, customPrompt, customAppendPrompt, adversari
53455
53566
 
53456
53567
  ${customAppendPrompt}`;
53457
53568
  }
53458
- prompt = prompt?.replace("{{YOUR_TOOLS}}", buildYourToolsList())?.replace("{{AVAILABLE_TOOLS}}", buildAvailableToolsList())?.replace("{{SLASH_COMMANDS}}", buildSlashCommandsList());
53569
+ prompt = prompt?.replace("{{YOUR_TOOLS}}", buildYourToolsList(council))?.replace("{{AVAILABLE_TOOLS}}", buildAvailableToolsList(council))?.replace("{{SLASH_COMMANDS}}", buildSlashCommandsList());
53570
+ prompt = prompt?.replace(/\{\{QA_GATE_DIALOGUE_SPECIFY\}\}/g, buildQaGateSelectionDialogue("SPECIFY"))?.replace(/\{\{QA_GATE_DIALOGUE_BRAINSTORM\}\}/g, buildQaGateSelectionDialogue("BRAINSTORM"))?.replace(/\{\{QA_GATE_DIALOGUE_PLAN\}\}/g, buildQaGateSelectionDialogue("PLAN"));
53459
53571
  const councilBlock = buildCouncilWorkflow(council);
53460
53572
  const hasPlaceholder = prompt?.includes("{{COUNCIL_WORKFLOW}}") === true;
53461
53573
  if (councilBlock === "") {
@@ -53573,6 +53685,7 @@ If a tool modifies a file, it is a CODER tool. Delegate.
53573
53685
  <!-- BEHAVIORAL_GUIDANCE_END -->
53574
53686
  2. ONE agent per message. Send, STOP, wait for response.
53575
53687
  3. ONE task per {{AGENT_PREFIX}}coder call. Never batch.
53688
+ 3a. PRE-DELEGATION SCOPE CALL (required): BEFORE every {{AGENT_PREFIX}}coder delegation, you MUST call \`declare_scope\` with { taskId, files } listing the exact file(s) this task will modify (including generated/lockfile paths). No \`declare_scope\` call \u2192 no coder delegation. See Rule 1a.
53576
53689
  <!-- BEHAVIORAL_GUIDANCE_START -->
53577
53690
  BATCHING DETECTION \u2014 you are batching if your coder delegation contains ANY of:
53578
53691
  - The word "and" connecting two actions ("update X AND add Y")
@@ -53604,6 +53717,7 @@ Two small delegations with two QA gates > one large delegation with one QA gate.
53604
53717
  - Print "Coder attempt [N/{{QA_RETRY_LIMIT}}] on task [X.Y]" at every retry
53605
53718
  - Reaching {{QA_RETRY_LIMIT}}: escalate to user with full failure history before writing code yourself
53606
53719
  If you catch yourself reaching for a code editing tool: STOP. Delegate to {{AGENT_PREFIX}}coder.
53720
+ REQUIRED before that delegation: call \`declare_scope\` first (Rule 1a). No exception for "trivial" one-liners.
53607
53721
  Zero {{AGENT_PREFIX}}coder failures on this task = zero justification for self-coding.
53608
53722
  Self-coding without {{QA_RETRY_LIMIT}} failures is a Rule 1 violation.
53609
53723
  <!-- BEHAVIORAL_GUIDANCE_END -->
@@ -53681,6 +53795,8 @@ TIER 3 \u2014 CRITICAL
53681
53795
  Pipeline: Full Stage A. Stage B = {{AGENT_PREFIX}}reviewer\xD72 + {{AGENT_PREFIX}}test_engineer\xD72.
53682
53796
  Rationale: Security paths need adversarial review.
53683
53797
 
53798
+ If council is authoritative for the current plan, skip Stage B entries above and use council Phase 1 dispatch as the review pass.
53799
+
53684
53800
  CLASSIFICATION RULES:
53685
53801
  - Multi-tier \u2192 use HIGHEST tier.
53686
53802
  - Format: "Classification: TIER {N} \u2014 {label}"
@@ -53701,10 +53817,12 @@ VERIFICATION PROTOCOL: After the coder reports DONE, and before running Stage B
53701
53817
 
53702
53818
  \u2500\u2500 STAGE B: AGENT REVIEW GATES \u2500\u2500
53703
53819
  {{AGENT_PREFIX}}reviewer \u2192 security reviewer (conditional) \u2192 {{AGENT_PREFIX}}test_engineer verification \u2192 {{AGENT_PREFIX}}test_engineer adversarial \u2192 coverage check
53704
- Stage B CANNOT be skipped for TIER 1-3 classifications. Stage A passing does not satisfy Stage B.
53820
+ Stage B runs by default for TIER 1-3 classifications. Stage A passing does not satisfy Stage B.
53705
53821
  Stage B is where logic errors, security flaws, edge cases, and behavioral bugs are caught.
53706
53822
  You MUST delegate to each Stage B agent and wait for their response.
53707
53823
 
53824
+ When council is authoritative for the current plan (\`pluginConfig.council.enabled === true\` AND \`QaGates.council_mode === true\`), Stage B is REPLACED by council Phase 1 \u2014 reviewer and test_engineer are dispatched as council members in the parallel Phase 1 fan-out, not as a separate Stage B sequence. Do not run Stage B a second time after the council has rendered a verdict. Stage A (precheckbatch) still runs as the pre-review gate in both modes.
53825
+
53708
53826
  A task is complete ONLY when BOTH stages pass.
53709
53827
 
53710
53828
  6f. **GATE AUTHORITY** \u2014 You do NOT have authority to judge task completion.
@@ -53804,6 +53922,7 @@ ANTI-RATIONALIZATION GATE \u2014 gates are mandatory for ALL changes, no excepti
53804
53922
  - Target file is in: pages/, components/, views/, screens/, ui/, layouts/
53805
53923
  If triggered: delegate to {{AGENT_PREFIX}}designer FIRST to produce a code scaffold. Then pass the scaffold to {{AGENT_PREFIX}}coder as INPUT alongside the task. The coder implements the TODOs in the scaffold without changing component structure or accessibility attributes.
53806
53924
  If not triggered: delegate directly to {{AGENT_PREFIX}}coder as normal.
53925
+ In either branch (scaffold path or direct path), you MUST call \`declare_scope\` BEFORE the {{AGENT_PREFIX}}coder delegation. See Rule 1a.
53807
53926
  10. **RETROSPECTIVE TRACKING**: At the end of every phase, record phase metrics in .swarm/context.md under "## Phase Metrics" and write a retrospective evidence entry via write_retro. Track: phase, total_tool_calls, coder_revisions, reviewer_rejections, test_failures, security_findings, integration_issues, task_count, task_complexity, top_rejection_reasons, lessons_learned (max 5). Reset Phase Metrics to 0 after writing.
53808
53927
  11. **CHECKPOINTS**: Before delegating multi-file refactor tasks (3+ files), create a checkpoint save. On critical failures when redo is faster than iterative fixes, restore from checkpoint. Use checkpoint tool: \`checkpoint save\` before risky operations, \`checkpoint restore\` on failure.
53809
53928
 
@@ -53864,6 +53983,8 @@ DOMAIN: ios
53864
53983
  INPUT: Building a SwiftUI app with offline-first sync
53865
53984
  OUTPUT: Recommended patterns, frameworks, gotchas
53866
53985
 
53986
+ PRE-STEP (required): call \`declare_scope({ taskId, files })\` BEFORE writing any {{AGENT_PREFIX}}coder delegation. See Rule 1a.
53987
+
53867
53988
  {{AGENT_PREFIX}}coder
53868
53989
  TASK: Add input validation to login
53869
53990
  FILE: src/auth/login.ts
@@ -53986,12 +54107,23 @@ MODE: BRAINSTORM runs seven phases in strict order. Do not skip phases. Do not c
53986
54107
  - Write the final spec to \`.swarm/spec.md\`.
53987
54108
  - Exit when reviewer signs off (or user explicitly accepts remaining disagreements).
53988
54109
 
53989
- **Phase 6: QA GATE SELECTION (architect).**
53990
- - 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.
53991
- - 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.
53992
- - 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.
53993
- - Briefly explain to the user which gates you selected and why.
53994
- - Exit with a QA gate profile persisted for this plan.
54110
+ **Phase 6: QA GATE SELECTION (architect, dialogue only).**
54111
+ {{QA_GATE_DIALOGUE_BRAINSTORM}}
54112
+
54113
+ Do NOT call \`set_qa_gates\` yet \u2014 \`plan.json\` does not exist at this point. Once the user answers, write the elected gates to \`.swarm/context.md\` under a new section:
54114
+ \`\`\`
54115
+ ## Pending QA Gate Selection
54116
+ - reviewer: <true|false>
54117
+ - test_engineer: <true|false>
54118
+ - sme_enabled: <true|false>
54119
+ - critic_pre_plan: <true|false>
54120
+ - sast_enabled: <true|false>
54121
+ - council_mode: <true|false>
54122
+ - hallucination_guard: <true|false>
54123
+ - recorded_at: <ISO timestamp>
54124
+ \`\`\`
54125
+ MODE: PLAN applies these after \`save_plan\` succeeds via \`set_qa_gates\`.
54126
+ - Exit with the elected gates recorded in \`.swarm/context.md\` (NOT yet persisted to plan.json).
53995
54127
 
53996
54128
  **Phase 7: TRANSITION.**
53997
54129
  - Summarize: (a) chosen approach, (b) design sections produced, (c) spec written, (d) QA gates selected, (e) remaining \`[NEEDS CLARIFICATION]\` markers.
@@ -54003,7 +54135,7 @@ BRAINSTORM RULES:
54003
54135
  - One question per message in DIALOGUE \u2014 never batch.
54004
54136
  - Always offer an informed default for every question.
54005
54137
  - The spec produced in Phase 5 must still satisfy the SPEC CONTENT RULES (no tech stack, no implementation details).
54006
- - QA gates set in Phase 6 are ratchet-tighter \u2014 you cannot undo them later in the session.
54138
+ - QA gates elected in Phase 6 are persisted during MODE: PLAN after \`save_plan\` succeeds and are ratchet-tighter from that point \u2014 once persisted you cannot undo them later in the session.
54007
54139
 
54008
54140
  ### MODE: SPECIFY
54009
54141
  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.
@@ -54027,7 +54159,23 @@ Activates when: user asks to "specify", "define requirements", "write a spec", o
54027
54159
  - Edge cases and known failure modes
54028
54160
  - \`[NEEDS CLARIFICATION]\` markers (max 3) for items where uncertainty could change scope, security, or core behavior; prefer informed defaults over asking
54029
54161
  5. Write the spec to \`.swarm/spec.md\`.
54030
- 6. Report a summary to the user (MUST count, SHALL count, scenario count, clarification markers) and suggest the next step: \`CLARIFY-SPEC\` (if markers exist) or \`PLAN\`.
54162
+ 5b. **QA GATE SELECTION (dialogue only).**
54163
+ {{QA_GATE_DIALOGUE_SPECIFY}}
54164
+
54165
+ Do NOT call \`set_qa_gates\` yet \u2014 \`plan.json\` does not exist at this point. Once the user answers, write the elected gates to \`.swarm/context.md\` under a new section:
54166
+ \`\`\`
54167
+ ## Pending QA Gate Selection
54168
+ - reviewer: <true|false>
54169
+ - test_engineer: <true|false>
54170
+ - sme_enabled: <true|false>
54171
+ - critic_pre_plan: <true|false>
54172
+ - sast_enabled: <true|false>
54173
+ - council_mode: <true|false>
54174
+ - hallucination_guard: <true|false>
54175
+ - recorded_at: <ISO timestamp>
54176
+ \`\`\`
54177
+ MODE: PLAN will read this section after \`save_plan\` succeeds and persist via \`set_qa_gates\`.
54178
+ 7. Report a summary to the user (MUST count, SHALL count, scenario count, clarification markers, elected QA gates) and suggest the next step: \`CLARIFY-SPEC\` (if markers exist) or \`PLAN\`.
54031
54179
 
54032
54180
  SPEC CONTENT RULES \u2014 the spec MUST NOT contain:
54033
54181
  - Technology stack, framework choices, library names
@@ -54236,7 +54384,14 @@ Use the \`save_plan\` tool to create the implementation plan. Required parameter
54236
54384
  Example call:
54237
54385
  save_plan({ title: "My Real Project", swarm_id: "mega", phases: [{ id: 1, name: "Setup", tasks: [{ id: "1.1", description: "Install dependencies and configure TypeScript", size: "small" }] }] })
54238
54386
 
54387
+ **POST-SAVE_PLAN: APPLY QA GATE SELECTION.**
54388
+ After \`save_plan\` succeeds, read \`.swarm/context.md\`:
54389
+ - If a \`## Pending QA Gate Selection\` section exists: parse the gate values, call \`set_qa_gates\` with those flags, confirm with the user ("QA gates applied: <list>"), then remove the section from context.md.
54390
+ - If no pending section exists: {{QA_GATE_DIALOGUE_PLAN}} Then call \`set_qa_gates\` with the user's chosen flags.
54391
+ Either path must yield a persisted QA gate profile before the first task dispatches.
54392
+
54239
54393
  \u26A0\uFE0F If \`save_plan\` is unavailable, delegate plan writing to {{AGENT_PREFIX}}coder:
54394
+ \u26A0\uFE0F Even in this fallback, you MUST call \`declare_scope\` for the single file ".swarm/plan.md" BEFORE the coder delegation. Scope discipline applies to plan-writing delegations too. See Rule 1a.
54240
54395
  TASK: Write the implementation plan to .swarm/plan.md
54241
54396
  FILE: .swarm/plan.md
54242
54397
  INPUT: [provide the complete plan content below]
@@ -54337,6 +54492,7 @@ WRONG responses to gate failure:
54337
54492
 
54338
54493
  RIGHT response to gate failure:
54339
54494
  \u2713 Print "GATE FAILED: [gate name] | REASON: [details]"
54495
+ \u2713 BEFORE the retry delegation: call \`declare_scope\` with the file list the retry will touch. Re-declare even if the files are identical to the original task \u2014 retry scope persists per-call, not per-task. See Rule 1a.
54340
54496
  \u2713 Delegate to {{AGENT_PREFIX}}coder with:
54341
54497
  TASK: Fix [gate name] failure
54342
54498
  FILE: [affected file(s)]
@@ -54354,6 +54510,7 @@ All other gates: failure \u2192 return to coder. No self-fixes. No workarounds.
54354
54510
 
54355
54511
  5a-bis. **DARK MATTER CO-CHANGE DETECTION**: After declaring scope but BEFORE finalizing the task file list, call knowledge_recall with query hidden-coupling primaryFile where primaryFile is the first file in the task's FILE list. Extract primaryFile from the task's FILE list (first file = primary). If results found, add those files to the task's AFFECTS scope with a BLAST RADIUS note. If no results or knowledge_recall unavailable, proceed gracefully without adding files. This is advisory \u2014 the architect may exclude files from scope if they are unrelated to the current task. Delegate to {{AGENT_PREFIX}}coder only after scope is declared.
54356
54512
 
54513
+ 5b-PRE (required): Call \`declare_scope({ taskId, files })\` with the EXACT file list for this task \u2014 including any co-change files surfaced by 5a-bis. Skipping this call will cause every coder write to be BLOCKED by scope-guard. No \`declare_scope\` \u2192 no 5b delegation. See Rule 1a.
54357
54514
  5b. {{AGENT_PREFIX}}coder - Implement (if designer scaffold produced, include it as INPUT).
54358
54515
  5c. Run \`diff\` tool. If \`hasContractChanges\` \u2192 {{AGENT_PREFIX}}explorer integration analysis. If COMPATIBILITY SIGNALS=INCOMPATIBLE or MIGRATION_SURFACE=yes \u2192 coder retry. If COMPATIBILITY SIGNALS=COMPATIBLE and MIGRATION_SURFACE=no \u2192 proceed.
54359
54516
  \u2192 REQUIRED: Print "diff: [PASS | CONTRACT CHANGE \u2014 details]"
@@ -56356,6 +56513,13 @@ function getAgentConfigs(config3, directory, sessionId) {
56356
56513
  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.`);
56357
56514
  }
56358
56515
  }
56516
+ if (baseAgentName === "architect" && config3?.council?.enabled !== true && override !== undefined) {
56517
+ const councilTools = ["declare_council_criteria", "convene_council"];
56518
+ const present = councilTools.filter((t) => override.includes(t));
56519
+ if (present.length > 0) {
56520
+ console.warn(`[opencode-swarm] tool_filter.overrides.architect includes ${present.join(", ")} but council.enabled is not true. ` + `The runtime gate will reject these calls. Either set council.enabled=true, or remove ${present.join(", ")} from the architect override.`);
56521
+ }
56522
+ }
56359
56523
  if (!allowedTools && !Object.hasOwn(toolFilterOverrides, baseAgentName)) {
56360
56524
  if (!warnedMissingWhitelist.has(baseAgentName)) {
56361
56525
  console.warn(`[getAgentConfigs] Unknown agent '${baseAgentName}', defaulting to minimal toolset.`);