majlis 0.5.2 → 0.6.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.
Files changed (2) hide show
  1. package/dist/cli.js +1249 -50
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -219,6 +219,36 @@ var init_migrations = __esm({
219
219
 
220
220
  ALTER TABLE dead_ends ADD COLUMN category TEXT DEFAULT 'structural'
221
221
  CHECK(category IN ('structural', 'procedural'));
222
+ `);
223
+ },
224
+ // Migration 005: v4 → v5 — Swarm tracking tables
225
+ (db) => {
226
+ db.exec(`
227
+ CREATE TABLE swarm_runs (
228
+ id INTEGER PRIMARY KEY,
229
+ goal TEXT NOT NULL,
230
+ parallel_count INTEGER NOT NULL,
231
+ status TEXT NOT NULL DEFAULT 'running'
232
+ CHECK(status IN ('running', 'completed', 'failed')),
233
+ total_cost_usd REAL DEFAULT 0,
234
+ best_experiment_slug TEXT,
235
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
236
+ completed_at DATETIME
237
+ );
238
+
239
+ CREATE TABLE swarm_members (
240
+ id INTEGER PRIMARY KEY,
241
+ swarm_run_id INTEGER REFERENCES swarm_runs(id),
242
+ experiment_slug TEXT NOT NULL,
243
+ worktree_path TEXT NOT NULL,
244
+ final_status TEXT,
245
+ overall_grade TEXT,
246
+ cost_usd REAL DEFAULT 0,
247
+ error TEXT,
248
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
249
+ );
250
+
251
+ CREATE INDEX idx_swarm_members_run ON swarm_members(swarm_run_id);
222
252
  `);
223
253
  }
224
254
  ];
@@ -273,6 +303,17 @@ function closeDb() {
273
303
  function resetDb() {
274
304
  _db = null;
275
305
  }
306
+ function openDbAt(projectRoot) {
307
+ const majlisDir = path.join(projectRoot, ".majlis");
308
+ if (!fs.existsSync(majlisDir)) {
309
+ fs.mkdirSync(majlisDir, { recursive: true });
310
+ }
311
+ const db = new import_better_sqlite3.default(path.join(majlisDir, "majlis.db"));
312
+ db.pragma("journal_mode = WAL");
313
+ db.pragma("foreign_keys = ON");
314
+ runMigrations(db);
315
+ return db;
316
+ }
276
317
  var import_better_sqlite3, path, fs, _db;
277
318
  var init_connection = __esm({
278
319
  "src/db/connection.ts"() {
@@ -469,7 +510,8 @@ ${cmd.body}
469
510
  "verification",
470
511
  "reframes",
471
512
  "rihla",
472
- "synthesis"
513
+ "synthesis",
514
+ "diagnosis"
473
515
  ];
474
516
  for (const dir of docDirs) {
475
517
  mkdirSafe(path2.join(docsDir, dir));
@@ -923,7 +965,74 @@ gate_decision:
923
965
  "stale_references": ["list of stale references found, if any"],
924
966
  "overlapping_dead_ends": [0]
925
967
  }
926
- -->`
968
+ -->`,
969
+ diagnostician: `---
970
+ name: diagnostician
971
+ model: opus
972
+ tools: [Read, Write, Bash, Glob, Grep, WebSearch]
973
+ ---
974
+ You are the Diagnostician. You perform deep project-wide analysis.
975
+
976
+ You have the highest turn budget of any agent. Use it for depth, not breadth.
977
+ Your job is pure insight \u2014 you do NOT fix code, you do NOT build, you do NOT
978
+ make decisions. You diagnose.
979
+
980
+ ## What You Receive
981
+ - Full database export: every experiment, decision, doubt, challenge, verification,
982
+ dead-end, metric, and compression across the entire project history
983
+ - Current synthesis, fragility map, and dead-end registry
984
+ - Full read access to the entire project codebase
985
+ - Bash access to run tests, profiling, git archaeology, and analysis scripts
986
+
987
+ ## What You Can Do
988
+ 1. **Read everything** \u2014 source code, docs, git history, test output
989
+ 2. **Run analysis** \u2014 execute tests, profilers, git log/blame/bisect, custom scripts
990
+ 3. **Write analysis scripts** \u2014 you may write scripts ONLY to \`.majlis/scripts/\`
991
+ 4. **Search externally** \u2014 WebSearch for patterns, known issues, relevant techniques
992
+
993
+ ## What You CANNOT Do
994
+ - Modify any project files outside \`.majlis/scripts/\`
995
+ - Make code changes, fixes, or patches
996
+ - Create experiments or make decisions
997
+ - Write to docs/, src/, or any other project directory
998
+
999
+ ## Your Approach
1000
+
1001
+ Phase 1: Orientation (turns 1-10)
1002
+ - Read the full database export in your context
1003
+ - Read synthesis, fragility, dead-ends
1004
+ - Identify patterns: recurring failures, unresolved doubts, evidence gaps
1005
+
1006
+ Phase 2: Deep Investigation (turns 11-40)
1007
+ - Read source code at critical points identified in Phase 1
1008
+ - Run targeted tests, profiling, git archaeology
1009
+ - Write and execute analysis scripts in .majlis/scripts/
1010
+ - Cross-reference findings across experiments
1011
+
1012
+ Phase 3: Synthesis (turns 41-60)
1013
+ - Compile findings into a diagnostic report
1014
+ - Identify root causes, not symptoms
1015
+ - Rank issues by structural impact
1016
+ - Suggest investigation directions (not fixes)
1017
+
1018
+ ## Output Format
1019
+ Produce a diagnostic report as markdown. At the end, include:
1020
+
1021
+ <!-- majlis-json
1022
+ {
1023
+ "diagnosis": {
1024
+ "root_causes": ["List of identified root causes"],
1025
+ "patterns": ["Recurring patterns across experiments"],
1026
+ "evidence_gaps": ["What we don't know but should"],
1027
+ "investigation_directions": ["Suggested directions for next experiments"]
1028
+ }
1029
+ }
1030
+ -->
1031
+
1032
+ ## Safety Reminders
1033
+ - You are READ-ONLY for project code. Write ONLY to .majlis/scripts/.
1034
+ - Focus on diagnosis, not fixing. Your value is insight, not implementation.
1035
+ - Trust the database export over docs/ files when they conflict.`
927
1036
  };
928
1037
  SLASH_COMMANDS = {
929
1038
  classify: {
@@ -983,6 +1092,14 @@ Produce a rihla document at docs/rihla/.`
983
1092
  If the CLI is not installed, review: original objective, current classification,
984
1093
  recent failures, dead-ends. Ask: is the classification serving the objective?
985
1094
  Would we decompose differently with what we now know?`
1095
+ },
1096
+ diagnose: {
1097
+ description: "Deep project-wide diagnostic analysis",
1098
+ body: `Run \`majlis diagnose $ARGUMENTS\` for deep diagnosis.
1099
+ If the CLI is not installed, perform a deep diagnostic analysis.
1100
+ Read docs/synthesis/current.md, fragility.md, dead-ends.md, and all experiments.
1101
+ Identify root causes, recurring patterns, evidence gaps, and investigation directions.
1102
+ Do NOT modify project code \u2014 analysis only.`
986
1103
  }
987
1104
  };
988
1105
  HOOKS_CONFIG = {
@@ -1581,6 +1698,29 @@ function insertFinding(db, experimentId, approach, source, relevance, contradict
1581
1698
  VALUES (?, ?, ?, ?, ?)
1582
1699
  `).run(experimentId, approach, source, relevance, contradictsCurrent ? 1 : 0);
1583
1700
  }
1701
+ function createSwarmRun(db, goal, parallelCount) {
1702
+ const result = db.prepare(`
1703
+ INSERT INTO swarm_runs (goal, parallel_count) VALUES (?, ?)
1704
+ `).run(goal, parallelCount);
1705
+ return { id: result.lastInsertRowid };
1706
+ }
1707
+ function updateSwarmRun(db, id, status2, totalCostUsd, bestSlug) {
1708
+ db.prepare(`
1709
+ UPDATE swarm_runs SET status = ?, total_cost_usd = ?, best_experiment_slug = ?,
1710
+ completed_at = CURRENT_TIMESTAMP WHERE id = ?
1711
+ `).run(status2, totalCostUsd, bestSlug, id);
1712
+ }
1713
+ function addSwarmMember(db, swarmRunId, slug, worktreePath) {
1714
+ db.prepare(`
1715
+ INSERT INTO swarm_members (swarm_run_id, experiment_slug, worktree_path) VALUES (?, ?, ?)
1716
+ `).run(swarmRunId, slug, worktreePath);
1717
+ }
1718
+ function updateSwarmMember(db, swarmRunId, slug, finalStatus, overallGrade, costUsd, error) {
1719
+ db.prepare(`
1720
+ UPDATE swarm_members SET final_status = ?, overall_grade = ?, cost_usd = ?, error = ?
1721
+ WHERE swarm_run_id = ? AND experiment_slug = ?
1722
+ `).run(finalStatus, overallGrade, costUsd, error, swarmRunId, slug);
1723
+ }
1584
1724
  function exportForCompressor(db, maxLength = 3e4) {
1585
1725
  const experiments = listAllExperiments(db);
1586
1726
  const sections = ["# Structured Data Export (from SQLite)\n"];
@@ -1643,6 +1783,79 @@ function exportForCompressor(db, maxLength = 3e4) {
1643
1783
  if (full.length > maxLength) {
1644
1784
  return full.slice(0, maxLength) + `
1645
1785
 
1786
+ [TRUNCATED \u2014 full export was ${full.length} chars]`;
1787
+ }
1788
+ return full;
1789
+ }
1790
+ function exportForDiagnostician(db, maxLength = 6e4) {
1791
+ const base = exportForCompressor(db, maxLength);
1792
+ const sections = [base];
1793
+ const metrics = db.prepare(`
1794
+ SELECT m.*, e.slug FROM metrics m
1795
+ JOIN experiments e ON m.experiment_id = e.id
1796
+ ORDER BY m.captured_at
1797
+ `).all();
1798
+ if (metrics.length > 0) {
1799
+ sections.push("\n## Metric History (all experiments)");
1800
+ for (const m of metrics) {
1801
+ sections.push(`- ${m.slug} [${m.phase}] ${m.fixture}/${m.metric_name}: ${m.metric_value}`);
1802
+ }
1803
+ }
1804
+ const sessions = db.prepare("SELECT * FROM sessions ORDER BY started_at").all();
1805
+ if (sessions.length > 0) {
1806
+ sections.push("\n## Session History");
1807
+ for (const s of sessions) {
1808
+ sections.push(`- #${s.id}: "${s.intent}" (${s.ended_at ? "ended" : "active"})`);
1809
+ if (s.accomplished) sections.push(` accomplished: ${s.accomplished}`);
1810
+ if (s.unfinished) sections.push(` unfinished: ${s.unfinished}`);
1811
+ if (s.new_fragility) sections.push(` fragility: ${s.new_fragility}`);
1812
+ }
1813
+ }
1814
+ const compressions = db.prepare("SELECT * FROM compressions ORDER BY created_at").all();
1815
+ if (compressions.length > 0) {
1816
+ sections.push("\n## Compression History");
1817
+ for (const c of compressions) {
1818
+ sections.push(`- #${c.id}: ${c.synthesis_size_before}B \u2192 ${c.synthesis_size_after}B (${c.session_count_since_last} sessions)`);
1819
+ }
1820
+ }
1821
+ try {
1822
+ const swarmRuns = db.prepare("SELECT * FROM swarm_runs ORDER BY created_at").all();
1823
+ if (swarmRuns.length > 0) {
1824
+ sections.push("\n## Swarm History");
1825
+ for (const sr of swarmRuns) {
1826
+ sections.push(`- #${sr.id}: "${sr.goal}" (${sr.status}, best: ${sr.best_experiment_slug ?? "none"})`);
1827
+ }
1828
+ }
1829
+ } catch {
1830
+ }
1831
+ const reframes = db.prepare(`
1832
+ SELECT r.*, e.slug FROM reframes r
1833
+ JOIN experiments e ON r.experiment_id = e.id
1834
+ ORDER BY r.created_at
1835
+ `).all();
1836
+ if (reframes.length > 0) {
1837
+ sections.push("\n## Reframe History");
1838
+ for (const r of reframes) {
1839
+ const decomp = String(r.decomposition ?? "").slice(0, 200);
1840
+ sections.push(`- ${r.slug}: ${decomp}`);
1841
+ if (r.recommendation) sections.push(` recommendation: ${String(r.recommendation).slice(0, 200)}`);
1842
+ }
1843
+ }
1844
+ const findings = db.prepare(`
1845
+ SELECT f.*, e.slug FROM findings f
1846
+ JOIN experiments e ON f.experiment_id = e.id
1847
+ ORDER BY f.created_at
1848
+ `).all();
1849
+ if (findings.length > 0) {
1850
+ sections.push("\n## Scout Findings");
1851
+ for (const f of findings) {
1852
+ sections.push(`- ${f.slug}: ${f.approach} (${f.source}) ${f.contradicts_current ? "[CONTRADICTS CURRENT]" : ""}`);
1853
+ }
1854
+ }
1855
+ const full = sections.join("\n");
1856
+ if (full.length > maxLength) {
1857
+ return full.slice(0, maxLength) + `
1858
+
1646
1859
  [TRUNCATED \u2014 full export was ${full.length} chars]`;
1647
1860
  }
1648
1861
  return full;
@@ -1690,6 +1903,16 @@ function truncateContext(content, limit) {
1690
1903
  if (content.length <= limit) return content;
1691
1904
  return content.slice(0, limit) + "\n[TRUNCATED]";
1692
1905
  }
1906
+ function readLatestDiagnosis(projectRoot) {
1907
+ const dir = path3.join(projectRoot, "docs", "diagnosis");
1908
+ try {
1909
+ const files = fs3.readdirSync(dir).filter((f) => f.startsWith("diagnosis-") && f.endsWith(".md")).sort().reverse();
1910
+ if (files.length === 0) return "";
1911
+ return fs3.readFileSync(path3.join(dir, files[0]), "utf-8");
1912
+ } catch {
1913
+ return "";
1914
+ }
1915
+ }
1693
1916
  var fs3, path3, DEFAULT_CONFIG2, _cachedConfig, _cachedRoot, CONTEXT_LIMITS;
1694
1917
  var init_config = __esm({
1695
1918
  "src/config.ts"() {
@@ -1838,6 +2061,8 @@ function getExtractionSchema(role) {
1838
2061
  return '{"findings": [{"approach": "string", "source": "string", "relevance": "string", "contradicts_current": true}]}';
1839
2062
  case "compressor":
1840
2063
  return '{"compression_report": {"synthesis_delta": "string", "new_dead_ends": ["string"], "fragility_changes": ["string"]}}';
2064
+ case "diagnostician":
2065
+ return '{"diagnosis": {"root_causes": ["string"], "patterns": ["string"], "evidence_gaps": ["string"], "investigation_directions": ["string"]}}';
1841
2066
  default:
1842
2067
  return EXTRACTION_SCHEMA;
1843
2068
  }
@@ -1861,7 +2086,8 @@ var init_types = __esm({
1861
2086
  gatekeeper: ["gate_decision"],
1862
2087
  reframer: ["reframe"],
1863
2088
  scout: ["findings"],
1864
- compressor: ["compression_report"]
2089
+ compressor: ["compression_report"],
2090
+ diagnostician: ["diagnosis"]
1865
2091
  };
1866
2092
  }
1867
2093
  });
@@ -1996,7 +2222,7 @@ ${truncated}`;
1996
2222
  }
1997
2223
  }
1998
2224
  function hasData(output) {
1999
- return !!(output.decisions && output.decisions.length > 0 || output.grades && output.grades.length > 0 || output.doubts && output.doubts.length > 0 || output.challenges && output.challenges.length > 0 || output.findings && output.findings.length > 0 || output.guidance || output.reframe || output.compression_report || output.gate_decision);
2225
+ return !!(output.decisions && output.decisions.length > 0 || output.grades && output.grades.length > 0 || output.doubts && output.doubts.length > 0 || output.challenges && output.challenges.length > 0 || output.findings && output.findings.length > 0 || output.guidance || output.reframe || output.compression_report || output.gate_decision || output.diagnosis);
2000
2226
  }
2001
2227
  function validateForRole(role, output) {
2002
2228
  const required = ROLE_REQUIRED_FIELDS[role];
@@ -2069,6 +2295,12 @@ You may ONLY write to docs/synthesis/.
2069
2295
  - Have you updated current.md, fragility.md, dead-ends.md?
2070
2296
  - If yes \u2192 output compression report JSON.
2071
2297
  - Do NOT write to MEMORY.md or files outside docs/synthesis/.`;
2298
+ case "diagnostician":
2299
+ return `${header2}
2300
+ You are READ-ONLY for project code. Write ONLY to .majlis/scripts/.
2301
+ Focus on diagnosis, not fixing. Your value is insight, not implementation.
2302
+ Phase 1 (1-10): orientation. Phase 2 (11-40): deep investigation. Phase 3 (41-60): synthesis.
2303
+ If you are past turn 40, begin compiling your diagnostic report.`;
2072
2304
  default:
2073
2305
  return `${header2}
2074
2306
  Check: is your core task done? If yes, wrap up and output JSON.`;
@@ -2092,6 +2324,36 @@ function buildPreToolUseGuards(role) {
2092
2324
  { matcher: "Edit", hooks: [guardHook] }
2093
2325
  ];
2094
2326
  }
2327
+ if (role === "diagnostician") {
2328
+ const writeGuard = async (input) => {
2329
+ const toolInput = input.tool_input ?? {};
2330
+ const filePath = toolInput.file_path ?? "";
2331
+ if (filePath && !filePath.includes("/.majlis/scripts/")) {
2332
+ return {
2333
+ decision: "block",
2334
+ reason: `Diagnostician may only write to .majlis/scripts/. Blocked: ${filePath}`
2335
+ };
2336
+ }
2337
+ return {};
2338
+ };
2339
+ const bashGuard = async (input) => {
2340
+ const toolInput = input.tool_input ?? {};
2341
+ const command = toolInput.command ?? "";
2342
+ const destructive = /\b(rm\s+-rf|git\s+(checkout|reset|stash|clean|push)|chmod|chown|mkfs|dd\s+if=)\b/i;
2343
+ if (destructive.test(command)) {
2344
+ return {
2345
+ decision: "block",
2346
+ reason: `Diagnostician blocked destructive command: ${command.slice(0, 100)}`
2347
+ };
2348
+ }
2349
+ return {};
2350
+ };
2351
+ return [
2352
+ { matcher: "Write", hooks: [writeGuard] },
2353
+ { matcher: "Edit", hooks: [writeGuard] },
2354
+ { matcher: "Bash", hooks: [bashGuard] }
2355
+ ];
2356
+ }
2095
2357
  return void 0;
2096
2358
  }
2097
2359
  function buildAgentHooks(role, maxTurns) {
@@ -2165,8 +2427,10 @@ ${taskPrompt}`;
2165
2427
  }
2166
2428
  return { output: markdown, structured, truncated };
2167
2429
  }
2168
- async function spawnSynthesiser(context, projectRoot) {
2430
+ async function spawnSynthesiser(context, projectRoot, opts) {
2169
2431
  const root = projectRoot ?? findProjectRoot() ?? process.cwd();
2432
+ const maxTurns = opts?.maxTurns ?? 5;
2433
+ const tools = opts?.tools ?? ["Read", "Glob", "Grep"];
2170
2434
  const contextJson = JSON.stringify(context);
2171
2435
  const taskPrompt = context.taskPrompt ?? "Synthesise the findings into actionable builder guidance.";
2172
2436
  const prompt = `Here is your context:
@@ -2177,14 +2441,14 @@ ${contextJson}
2177
2441
 
2178
2442
  ${taskPrompt}`;
2179
2443
  const systemPrompt = 'You are a Synthesis Agent. Be concrete: which decisions failed, which assumptions broke, what constraints must the next approach satisfy. CRITICAL: Your LAST line of output MUST be a <!-- majlis-json --> block. The framework parses this programmatically \u2014 if you omit it, the pipeline breaks. Format: <!-- majlis-json {"guidance": "your guidance here"} -->';
2180
- console.log(`[synthesiser] Spawning (maxTurns: 5)...`);
2444
+ console.log(`[synthesiser] Spawning (maxTurns: ${maxTurns})...`);
2181
2445
  const { text: markdown, costUsd, truncated } = await runQuery({
2182
2446
  prompt,
2183
2447
  model: "sonnet",
2184
- tools: ["Read", "Glob", "Grep"],
2448
+ tools,
2185
2449
  systemPrompt,
2186
2450
  cwd: root,
2187
- maxTurns: 5,
2451
+ maxTurns,
2188
2452
  label: "synthesiser",
2189
2453
  role: "synthesiser"
2190
2454
  });
@@ -2365,7 +2629,7 @@ function writeArtifact(role, context, markdown, projectRoot) {
2365
2629
  };
2366
2630
  const dir = dirMap[role];
2367
2631
  if (!dir) return null;
2368
- if (role === "builder" || role === "compressor") return null;
2632
+ if (role === "builder" || role === "compressor" || role === "diagnostician") return null;
2369
2633
  const fullDir = path4.join(projectRoot, dir);
2370
2634
  if (!fs4.existsSync(fullDir)) {
2371
2635
  fs4.mkdirSync(fullDir, { recursive: true });
@@ -2394,14 +2658,16 @@ var init_spawn = __esm({
2394
2658
  compressor: 30,
2395
2659
  reframer: 20,
2396
2660
  scout: 20,
2397
- gatekeeper: 10
2661
+ gatekeeper: 10,
2662
+ diagnostician: 60
2398
2663
  };
2399
2664
  CHECKPOINT_INTERVAL = {
2400
2665
  builder: 15,
2401
2666
  verifier: 12,
2402
2667
  critic: 15,
2403
2668
  adversary: 15,
2404
- compressor: 15
2669
+ compressor: 15,
2670
+ diagnostician: 20
2405
2671
  };
2406
2672
  DIM2 = "\x1B[2m";
2407
2673
  RESET2 = "\x1B[0m";
@@ -2409,6 +2675,38 @@ var init_spawn = __esm({
2409
2675
  }
2410
2676
  });
2411
2677
 
2678
+ // src/git.ts
2679
+ function autoCommit(root, message) {
2680
+ try {
2681
+ (0, import_node_child_process.execSync)("git add docs/ .majlis/scripts/ 2>/dev/null; true", {
2682
+ cwd: root,
2683
+ encoding: "utf-8",
2684
+ stdio: ["pipe", "pipe", "pipe"]
2685
+ });
2686
+ const diff = (0, import_node_child_process.execSync)("git diff --cached --stat", {
2687
+ cwd: root,
2688
+ encoding: "utf-8",
2689
+ stdio: ["pipe", "pipe", "pipe"]
2690
+ }).trim();
2691
+ if (!diff) return;
2692
+ (0, import_node_child_process.execSync)(`git commit -m ${JSON.stringify(`[majlis] ${message}`)}`, {
2693
+ cwd: root,
2694
+ encoding: "utf-8",
2695
+ stdio: ["pipe", "pipe", "pipe"]
2696
+ });
2697
+ info(`Auto-committed: ${message}`);
2698
+ } catch {
2699
+ }
2700
+ }
2701
+ var import_node_child_process;
2702
+ var init_git = __esm({
2703
+ "src/git.ts"() {
2704
+ "use strict";
2705
+ import_node_child_process = require("child_process");
2706
+ init_format();
2707
+ }
2708
+ });
2709
+
2412
2710
  // src/metrics.ts
2413
2711
  function compareMetrics(db, experimentId, config) {
2414
2712
  const before = getMetricsByExperimentAndPhase(db, experimentId, "before");
@@ -2498,7 +2796,7 @@ async function captureMetrics(phase, args) {
2498
2796
  if (config.build.pre_measure) {
2499
2797
  info(`Running pre-measure: ${config.build.pre_measure}`);
2500
2798
  try {
2501
- (0, import_node_child_process.execSync)(config.build.pre_measure, { cwd: root, encoding: "utf-8", stdio: "inherit" });
2799
+ (0, import_node_child_process2.execSync)(config.build.pre_measure, { cwd: root, encoding: "utf-8", stdio: "inherit" });
2502
2800
  } catch {
2503
2801
  warn("Pre-measure command failed \u2014 continuing anyway.");
2504
2802
  }
@@ -2509,7 +2807,7 @@ async function captureMetrics(phase, args) {
2509
2807
  info(`Running metrics: ${config.metrics.command}`);
2510
2808
  let metricsOutput;
2511
2809
  try {
2512
- metricsOutput = (0, import_node_child_process.execSync)(config.metrics.command, {
2810
+ metricsOutput = (0, import_node_child_process2.execSync)(config.metrics.command, {
2513
2811
  cwd: root,
2514
2812
  encoding: "utf-8",
2515
2813
  stdio: ["pipe", "pipe", "pipe"]
@@ -2528,7 +2826,7 @@ async function captureMetrics(phase, args) {
2528
2826
  success(`Captured ${parsed.length} metric(s) for ${exp.slug} (phase: ${phase})`);
2529
2827
  if (config.build.post_measure) {
2530
2828
  try {
2531
- (0, import_node_child_process.execSync)(config.build.post_measure, { cwd: root, encoding: "utf-8", stdio: "inherit" });
2829
+ (0, import_node_child_process2.execSync)(config.build.post_measure, { cwd: root, encoding: "utf-8", stdio: "inherit" });
2532
2830
  } catch {
2533
2831
  warn("Post-measure command failed.");
2534
2832
  }
@@ -2579,11 +2877,11 @@ function formatDelta(delta) {
2579
2877
  const prefix = delta > 0 ? "+" : "";
2580
2878
  return `${prefix}${delta.toFixed(4)}`;
2581
2879
  }
2582
- var import_node_child_process;
2880
+ var import_node_child_process2;
2583
2881
  var init_measure = __esm({
2584
2882
  "src/commands/measure.ts"() {
2585
2883
  "use strict";
2586
- import_node_child_process = require("child_process");
2884
+ import_node_child_process2 = require("child_process");
2587
2885
  init_connection();
2588
2886
  init_queries();
2589
2887
  init_metrics();
@@ -2616,7 +2914,7 @@ async function newExperiment(args) {
2616
2914
  const paddedNum = String(num).padStart(3, "0");
2617
2915
  const branch = `exp/${paddedNum}-${slug}`;
2618
2916
  try {
2619
- (0, import_node_child_process2.execSync)(`git checkout -b ${branch}`, {
2917
+ (0, import_node_child_process3.execSync)(`git checkout -b ${branch}`, {
2620
2918
  cwd: root,
2621
2919
  encoding: "utf-8",
2622
2920
  stdio: ["pipe", "pipe", "pipe"]
@@ -2637,6 +2935,7 @@ async function newExperiment(args) {
2637
2935
  fs5.writeFileSync(logPath, logContent);
2638
2936
  info(`Created experiment log: docs/experiments/${paddedNum}-${slug}.md`);
2639
2937
  }
2938
+ autoCommit(root, `new: ${slug}`);
2640
2939
  if (config.cycle.auto_baseline_on_new_experiment && config.metrics.command) {
2641
2940
  info("Auto-baselining... (run `majlis baseline` to do this manually)");
2642
2941
  try {
@@ -2673,12 +2972,12 @@ async function revert(args) {
2673
2972
  );
2674
2973
  updateExperimentStatus(db, exp.id, "dead_end");
2675
2974
  try {
2676
- const currentBranch = (0, import_node_child_process2.execSync)("git rev-parse --abbrev-ref HEAD", {
2975
+ const currentBranch = (0, import_node_child_process3.execSync)("git rev-parse --abbrev-ref HEAD", {
2677
2976
  cwd: root,
2678
2977
  encoding: "utf-8"
2679
2978
  }).trim();
2680
2979
  if (currentBranch === exp.branch) {
2681
- (0, import_node_child_process2.execSync)("git checkout main 2>/dev/null || git checkout master", {
2980
+ (0, import_node_child_process3.execSync)("git checkout main 2>/dev/null || git checkout master", {
2682
2981
  cwd: root,
2683
2982
  encoding: "utf-8",
2684
2983
  stdio: ["pipe", "pipe", "pipe"]
@@ -2689,17 +2988,18 @@ async function revert(args) {
2689
2988
  }
2690
2989
  info(`Experiment ${exp.slug} reverted to dead-end. Reason: ${reason}`);
2691
2990
  }
2692
- var fs5, path5, import_node_child_process2;
2991
+ var fs5, path5, import_node_child_process3;
2693
2992
  var init_experiment = __esm({
2694
2993
  "src/commands/experiment.ts"() {
2695
2994
  "use strict";
2696
2995
  fs5 = __toESM(require("fs"));
2697
2996
  path5 = __toESM(require("path"));
2698
- import_node_child_process2 = require("child_process");
2997
+ import_node_child_process3 = require("child_process");
2699
2998
  init_connection();
2700
2999
  init_queries();
2701
3000
  init_config();
2702
3001
  init_spawn();
3002
+ init_git();
2703
3003
  init_format();
2704
3004
  }
2705
3005
  });
@@ -3047,6 +3347,7 @@ async function resolve(db, exp, projectRoot) {
3047
3347
  gitMerge(exp.branch, projectRoot);
3048
3348
  const gaps = grades.filter((g) => g.grade === "good").map((g) => `- **${g.component}**: ${g.notes ?? "minor gaps"}`).join("\n");
3049
3349
  appendToFragilityMap(projectRoot, exp.slug, gaps);
3350
+ autoCommit(projectRoot, `resolve: fragility gaps from ${exp.slug}`);
3050
3351
  updateExperimentStatus(db, exp.id, "merged");
3051
3352
  success(`Experiment ${exp.slug} MERGED (good, ${grades.filter((g) => g.grade === "good").length} gaps added to fragility map).`);
3052
3353
  break;
@@ -3101,9 +3402,92 @@ async function resolve(db, exp, projectRoot) {
3101
3402
  }
3102
3403
  }
3103
3404
  }
3405
+ async function resolveDbOnly(db, exp, projectRoot) {
3406
+ let grades = getVerificationsByExperiment(db, exp.id);
3407
+ if (grades.length === 0) {
3408
+ warn(`No verification records for ${exp.slug}. Defaulting to weak.`);
3409
+ insertVerification(
3410
+ db,
3411
+ exp.id,
3412
+ "auto-default",
3413
+ "weak",
3414
+ null,
3415
+ null,
3416
+ "No structured verification output. Auto-defaulted to weak."
3417
+ );
3418
+ grades = getVerificationsByExperiment(db, exp.id);
3419
+ }
3420
+ const overallGrade = worstGrade(grades);
3421
+ switch (overallGrade) {
3422
+ case "sound":
3423
+ updateExperimentStatus(db, exp.id, "merged");
3424
+ success(`Experiment ${exp.slug} RESOLVED (sound) \u2014 git merge deferred.`);
3425
+ break;
3426
+ case "good": {
3427
+ const gaps = grades.filter((g) => g.grade === "good").map((g) => `- **${g.component}**: ${g.notes ?? "minor gaps"}`).join("\n");
3428
+ appendToFragilityMap(projectRoot, exp.slug, gaps);
3429
+ updateExperimentStatus(db, exp.id, "merged");
3430
+ success(`Experiment ${exp.slug} RESOLVED (good) \u2014 git merge deferred.`);
3431
+ break;
3432
+ }
3433
+ case "weak": {
3434
+ const confirmedDoubts = getConfirmedDoubts(db, exp.id);
3435
+ const guidance = await spawnSynthesiser({
3436
+ experiment: {
3437
+ id: exp.id,
3438
+ slug: exp.slug,
3439
+ hypothesis: exp.hypothesis,
3440
+ status: exp.status,
3441
+ sub_type: exp.sub_type,
3442
+ builder_guidance: exp.builder_guidance
3443
+ },
3444
+ verificationReport: grades,
3445
+ confirmedDoubts,
3446
+ taskPrompt: "Synthesise the verification report, confirmed doubts, and adversarial case results into specific, actionable guidance for the builder's next attempt. Be concrete: which specific decisions need revisiting, which assumptions broke, and what constraints must the next approach satisfy."
3447
+ }, projectRoot);
3448
+ const guidanceText = guidance.structured?.guidance ?? guidance.output;
3449
+ db.transaction(() => {
3450
+ storeBuilderGuidance(db, exp.id, guidanceText);
3451
+ updateExperimentStatus(db, exp.id, "building");
3452
+ if (exp.sub_type) {
3453
+ incrementSubTypeFailure(db, exp.sub_type, exp.id, "weak");
3454
+ }
3455
+ })();
3456
+ warn(`Experiment ${exp.slug} CYCLING BACK (weak). Guidance generated.`);
3457
+ break;
3458
+ }
3459
+ case "rejected": {
3460
+ const rejectedComponents = grades.filter((g) => g.grade === "rejected");
3461
+ const whyFailed = rejectedComponents.map((r) => r.notes ?? "rejected").join("; ");
3462
+ db.transaction(() => {
3463
+ insertDeadEnd(
3464
+ db,
3465
+ exp.id,
3466
+ exp.hypothesis ?? exp.slug,
3467
+ whyFailed,
3468
+ `Approach rejected: ${whyFailed}`,
3469
+ exp.sub_type,
3470
+ "structural"
3471
+ );
3472
+ updateExperimentStatus(db, exp.id, "dead_end");
3473
+ if (exp.sub_type) {
3474
+ incrementSubTypeFailure(db, exp.sub_type, exp.id, "rejected");
3475
+ }
3476
+ })();
3477
+ info(`Experiment ${exp.slug} DEAD-ENDED (rejected). Constraint recorded.`);
3478
+ break;
3479
+ }
3480
+ }
3481
+ return overallGrade;
3482
+ }
3104
3483
  function gitMerge(branch, cwd) {
3105
3484
  try {
3106
- (0, import_node_child_process3.execSync)(`git merge ${branch} --no-ff -m "Merge experiment branch ${branch}"`, {
3485
+ (0, import_node_child_process4.execSync)("git checkout main 2>/dev/null || git checkout master", {
3486
+ cwd,
3487
+ encoding: "utf-8",
3488
+ stdio: ["pipe", "pipe", "pipe"]
3489
+ });
3490
+ (0, import_node_child_process4.execSync)(`git merge ${branch} --no-ff -m "Merge experiment branch ${branch}"`, {
3107
3491
  cwd,
3108
3492
  encoding: "utf-8",
3109
3493
  stdio: ["pipe", "pipe", "pipe"]
@@ -3114,16 +3498,16 @@ function gitMerge(branch, cwd) {
3114
3498
  }
3115
3499
  function gitRevert(branch, cwd) {
3116
3500
  try {
3117
- const currentBranch = (0, import_node_child_process3.execSync)("git rev-parse --abbrev-ref HEAD", {
3501
+ const currentBranch = (0, import_node_child_process4.execSync)("git rev-parse --abbrev-ref HEAD", {
3118
3502
  cwd,
3119
3503
  encoding: "utf-8"
3120
3504
  }).trim();
3121
3505
  if (currentBranch === branch) {
3122
3506
  try {
3123
- (0, import_node_child_process3.execSync)("git checkout -- .", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
3507
+ (0, import_node_child_process4.execSync)("git checkout -- .", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
3124
3508
  } catch {
3125
3509
  }
3126
- (0, import_node_child_process3.execSync)("git checkout main 2>/dev/null || git checkout master", {
3510
+ (0, import_node_child_process4.execSync)("git checkout main 2>/dev/null || git checkout master", {
3127
3511
  cwd,
3128
3512
  encoding: "utf-8",
3129
3513
  stdio: ["pipe", "pipe", "pipe"]
@@ -3145,7 +3529,7 @@ ${gaps}
3145
3529
  `;
3146
3530
  fs7.writeFileSync(fragPath, content + entry);
3147
3531
  }
3148
- var fs7, path7, import_node_child_process3;
3532
+ var fs7, path7, import_node_child_process4;
3149
3533
  var init_resolve = __esm({
3150
3534
  "src/resolve.ts"() {
3151
3535
  "use strict";
@@ -3154,7 +3538,8 @@ var init_resolve = __esm({
3154
3538
  init_types2();
3155
3539
  init_queries();
3156
3540
  init_spawn();
3157
- import_node_child_process3 = require("child_process");
3541
+ import_node_child_process4 = require("child_process");
3542
+ init_git();
3158
3543
  init_format();
3159
3544
  }
3160
3545
  });
@@ -3163,7 +3548,9 @@ var init_resolve = __esm({
3163
3548
  var cycle_exports = {};
3164
3549
  __export(cycle_exports, {
3165
3550
  cycle: () => cycle,
3166
- resolveCmd: () => resolveCmd
3551
+ resolveCmd: () => resolveCmd,
3552
+ runResolve: () => runResolve,
3553
+ runStep: () => runStep
3167
3554
  });
3168
3555
  async function cycle(step, args) {
3169
3556
  const root = findProjectRoot();
@@ -3195,6 +3582,28 @@ async function resolveCmd(args) {
3195
3582
  transition(exp.status, "resolved" /* RESOLVED */);
3196
3583
  await resolve(db, exp, root);
3197
3584
  }
3585
+ async function runStep(step, db, exp, root) {
3586
+ switch (step) {
3587
+ case "build":
3588
+ return doBuild(db, exp, root);
3589
+ case "challenge":
3590
+ return doChallenge(db, exp, root);
3591
+ case "doubt":
3592
+ return doDoubt(db, exp, root);
3593
+ case "scout":
3594
+ return doScout(db, exp, root);
3595
+ case "verify":
3596
+ return doVerify(db, exp, root);
3597
+ case "gate":
3598
+ return doGate(db, exp, root);
3599
+ case "compress":
3600
+ return doCompress(db, root);
3601
+ }
3602
+ }
3603
+ async function runResolve(db, exp, root) {
3604
+ transition(exp.status, "resolved" /* RESOLVED */);
3605
+ await resolve(db, exp, root);
3606
+ }
3198
3607
  async function doGate(db, exp, root) {
3199
3608
  transition(exp.status, "gated" /* GATED */);
3200
3609
  const synthesis = truncateContext(readFileOrEmpty(path8.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
@@ -3249,7 +3658,7 @@ async function doBuild(db, exp, root) {
3249
3658
  const existingBaseline = getMetricsByExperimentAndPhase(db, exp.id, "before");
3250
3659
  if (config.metrics?.command && existingBaseline.length === 0) {
3251
3660
  try {
3252
- const output = (0, import_node_child_process4.execSync)(config.metrics.command, {
3661
+ const output = (0, import_node_child_process5.execSync)(config.metrics.command, {
3253
3662
  cwd: root,
3254
3663
  encoding: "utf-8",
3255
3664
  timeout: 6e4,
@@ -3306,7 +3715,7 @@ Build the experiment: ${exp.hypothesis}` : `Build the experiment: ${exp.hypothes
3306
3715
  } else {
3307
3716
  if (config.metrics?.command) {
3308
3717
  try {
3309
- const output = (0, import_node_child_process4.execSync)(config.metrics.command, {
3718
+ const output = (0, import_node_child_process5.execSync)(config.metrics.command, {
3310
3719
  cwd: root,
3311
3720
  encoding: "utf-8",
3312
3721
  timeout: 6e4,
@@ -3330,7 +3739,7 @@ async function doChallenge(db, exp, root) {
3330
3739
  transition(exp.status, "challenged" /* CHALLENGED */);
3331
3740
  let gitDiff = "";
3332
3741
  try {
3333
- gitDiff = (0, import_node_child_process4.execSync)('git diff main -- . ":!.majlis/"', {
3742
+ gitDiff = (0, import_node_child_process5.execSync)('git diff main -- . ":!.majlis/"', {
3334
3743
  cwd: root,
3335
3744
  encoding: "utf-8",
3336
3745
  stdio: ["pipe", "pipe", "pipe"]
@@ -3542,12 +3951,13 @@ async function doCompress(db, root) {
3542
3951
  }, root);
3543
3952
  const sizeAfter = fs8.existsSync(synthesisPath) ? fs8.statSync(synthesisPath).size : 0;
3544
3953
  recordCompression(db, sessionCount, sizeBefore, sizeAfter);
3954
+ autoCommit(root, "compress: update synthesis");
3545
3955
  success(`Compression complete. Synthesis: ${sizeBefore}B \u2192 ${sizeAfter}B`);
3546
3956
  }
3547
3957
  function gitCommitBuild(exp, cwd) {
3548
3958
  try {
3549
- (0, import_node_child_process4.execSync)('git add -A -- ":!.majlis/"', { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
3550
- const diff = (0, import_node_child_process4.execSync)("git diff --cached --stat", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3959
+ (0, import_node_child_process5.execSync)('git add -A -- ":!.majlis/"', { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
3960
+ const diff = (0, import_node_child_process5.execSync)("git diff --cached --stat", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3551
3961
  if (!diff) {
3552
3962
  info("No code changes to commit.");
3553
3963
  return;
@@ -3555,7 +3965,7 @@ function gitCommitBuild(exp, cwd) {
3555
3965
  const msg = `EXP-${String(exp.id).padStart(3, "0")}: ${exp.slug}
3556
3966
 
3557
3967
  ${exp.hypothesis ?? ""}`;
3558
- (0, import_node_child_process4.execSync)(`git commit -m ${JSON.stringify(msg)}`, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
3968
+ (0, import_node_child_process5.execSync)(`git commit -m ${JSON.stringify(msg)}`, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
3559
3969
  info(`Committed builder changes on ${exp.branch}.`);
3560
3970
  } catch {
3561
3971
  warn("Could not auto-commit builder changes \u2014 commit manually before resolving.");
@@ -3631,13 +4041,13 @@ function ingestStructuredOutput(db, experimentId, structured) {
3631
4041
  info(`Ingested ${structured.findings.length} finding(s)`);
3632
4042
  }
3633
4043
  }
3634
- var fs8, path8, import_node_child_process4;
4044
+ var fs8, path8, import_node_child_process5;
3635
4045
  var init_cycle = __esm({
3636
4046
  "src/commands/cycle.ts"() {
3637
4047
  "use strict";
3638
4048
  fs8 = __toESM(require("fs"));
3639
4049
  path8 = __toESM(require("path"));
3640
- import_node_child_process4 = require("child_process");
4050
+ import_node_child_process5 = require("child_process");
3641
4051
  init_connection();
3642
4052
  init_queries();
3643
4053
  init_machine();
@@ -3646,6 +4056,7 @@ var init_cycle = __esm({
3646
4056
  init_resolve();
3647
4057
  init_config();
3648
4058
  init_metrics();
4059
+ init_git();
3649
4060
  init_format();
3650
4061
  }
3651
4062
  });
@@ -3679,6 +4090,7 @@ ${deadEnds}
3679
4090
 
3680
4091
  Write the classification to docs/classification/ following the template.`
3681
4092
  }, root);
4093
+ autoCommit(root, `classify: ${domain.slice(0, 60)}`);
3682
4094
  success("Classification complete. Check docs/classification/ for the output.");
3683
4095
  }
3684
4096
  async function reframe(args) {
@@ -3721,6 +4133,7 @@ ${deadEnds}
3721
4133
  Independently propose a decomposition. Compare with the existing classification. Flag structural divergences \u2014 these are the most valuable signals.
3722
4134
  Write to docs/reframes/.`
3723
4135
  }, root);
4136
+ autoCommit(root, `reframe: ${target.slice(0, 60)}`);
3724
4137
  success("Reframe complete. Check docs/reframes/ for the output.");
3725
4138
  }
3726
4139
  var fs9, path9;
@@ -3731,6 +4144,7 @@ var init_classify = __esm({
3731
4144
  path9 = __toESM(require("path"));
3732
4145
  init_connection();
3733
4146
  init_spawn();
4147
+ init_git();
3734
4148
  init_format();
3735
4149
  }
3736
4150
  });
@@ -4005,13 +4419,13 @@ async function run(args) {
4005
4419
  const db = getDb(root);
4006
4420
  const config = loadConfig(root);
4007
4421
  const MAX_EXPERIMENTS = 10;
4008
- const MAX_STEPS = 200;
4422
+ const MAX_STEPS2 = 200;
4009
4423
  let experimentCount = 0;
4010
4424
  let stepCount = 0;
4011
4425
  let consecutiveFailures = 0;
4012
4426
  const usedHypotheses = /* @__PURE__ */ new Set();
4013
4427
  header(`Autonomous Mode \u2014 ${goal}`);
4014
- while (stepCount < MAX_STEPS && experimentCount < MAX_EXPERIMENTS) {
4428
+ while (stepCount < MAX_STEPS2 && experimentCount < MAX_EXPERIMENTS) {
4015
4429
  if (isShutdownRequested()) {
4016
4430
  warn("Shutdown requested. Stopping autonomous mode.");
4017
4431
  break;
@@ -4042,6 +4456,7 @@ async function run(args) {
4042
4456
  usedHypotheses.add(hypothesis);
4043
4457
  info(`Next hypothesis: ${hypothesis}`);
4044
4458
  exp = await createNewExperiment(db, root, hypothesis);
4459
+ autoCommit(root, `new: ${exp.slug}`);
4045
4460
  success(`Created experiment #${exp.id}: ${exp.slug}`);
4046
4461
  }
4047
4462
  if (isTerminal(exp.status)) {
@@ -4081,8 +4496,8 @@ async function run(args) {
4081
4496
  }
4082
4497
  }
4083
4498
  }
4084
- if (stepCount >= MAX_STEPS) {
4085
- warn(`Reached max steps (${MAX_STEPS}). Stopping autonomous mode.`);
4499
+ if (stepCount >= MAX_STEPS2) {
4500
+ warn(`Reached max steps (${MAX_STEPS2}). Stopping autonomous mode.`);
4086
4501
  }
4087
4502
  header("Autonomous Mode Complete");
4088
4503
  info(`Goal: ${goal}`);
@@ -4093,12 +4508,13 @@ async function deriveNextHypothesis(goal, root, db) {
4093
4508
  const synthesis = truncateContext(readFileOrEmpty(path11.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
4094
4509
  const fragility = truncateContext(readFileOrEmpty(path11.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
4095
4510
  const deadEndsDoc = truncateContext(readFileOrEmpty(path11.join(root, "docs", "synthesis", "dead-ends.md")), CONTEXT_LIMITS.deadEnds);
4511
+ const diagnosis = truncateContext(readLatestDiagnosis(root), CONTEXT_LIMITS.synthesis);
4096
4512
  const deadEnds = listAllDeadEnds(db);
4097
4513
  const config = loadConfig(root);
4098
4514
  let metricsOutput = "";
4099
4515
  if (config.metrics?.command) {
4100
4516
  try {
4101
- metricsOutput = (0, import_node_child_process5.execSync)(config.metrics.command, {
4517
+ metricsOutput = (0, import_node_child_process6.execSync)(config.metrics.command, {
4102
4518
  cwd: root,
4103
4519
  encoding: "utf-8",
4104
4520
  timeout: 6e4,
@@ -4113,7 +4529,10 @@ async function deriveNextHypothesis(goal, root, db) {
4113
4529
 
4114
4530
  ## Goal
4115
4531
  ${goal}
4116
-
4532
+ ${diagnosis ? `
4533
+ ## Latest Diagnosis Report (PRIORITISE \u2014 deep analysis from diagnostician agent)
4534
+ ${diagnosis}
4535
+ ` : ""}
4117
4536
  ## Current Metrics
4118
4537
  ${metricsOutput || "(no metrics configured)"}
4119
4538
 
@@ -4133,6 +4552,8 @@ Note: [structural] dead ends are HARD CONSTRAINTS \u2014 your hypothesis MUST NO
4133
4552
  [procedural] dead ends are process failures \u2014 the approach may still be valid if executed differently.
4134
4553
 
4135
4554
  ## Your Task
4555
+ DO NOT read source code or use tools. All context you need is above. Plan from the synthesis and dead-end registry.
4556
+
4136
4557
  1. Assess: based on the metrics and synthesis, has the goal been met? Be specific.
4137
4558
  2. If YES \u2014 output the JSON block below with goal_met: true.
4138
4559
  3. If NO \u2014 propose the SINGLE most promising next experiment hypothesis.
@@ -4148,7 +4569,7 @@ CRITICAL: Your LAST line of output MUST be EXACTLY this format (on its own line,
4148
4569
 
4149
4570
  If the goal is met:
4150
4571
  <!-- majlis-json {"goal_met": true, "hypothesis": null} -->`
4151
- }, root);
4572
+ }, root, { maxTurns: 2, tools: [] });
4152
4573
  const structured = result.structured;
4153
4574
  if (structured?.goal_met === true) {
4154
4575
  return null;
@@ -4174,7 +4595,7 @@ If the goal is met:
4174
4595
  ${result.output.slice(-2e3)}
4175
4596
 
4176
4597
  <!-- majlis-json {"goal_met": false, "hypothesis": "your hypothesis"} -->`
4177
- }, root);
4598
+ }, root, { maxTurns: 1, tools: [] });
4178
4599
  if (retry.structured?.hypothesis) return retry.structured.hypothesis;
4179
4600
  warn("Could not extract hypothesis. Using goal as fallback.");
4180
4601
  return goal;
@@ -4192,7 +4613,7 @@ async function createNewExperiment(db, root, hypothesis) {
4192
4613
  const paddedNum = String(num).padStart(3, "0");
4193
4614
  const branch = `exp/${paddedNum}-${finalSlug}`;
4194
4615
  try {
4195
- (0, import_node_child_process5.execSync)(`git checkout -b ${branch}`, {
4616
+ (0, import_node_child_process6.execSync)(`git checkout -b ${branch}`, {
4196
4617
  cwd: root,
4197
4618
  encoding: "utf-8",
4198
4619
  stdio: ["pipe", "pipe", "pipe"]
@@ -4215,13 +4636,13 @@ async function createNewExperiment(db, root, hypothesis) {
4215
4636
  }
4216
4637
  return exp;
4217
4638
  }
4218
- var fs11, path11, import_node_child_process5;
4639
+ var fs11, path11, import_node_child_process6;
4219
4640
  var init_run = __esm({
4220
4641
  "src/commands/run.ts"() {
4221
4642
  "use strict";
4222
4643
  fs11 = __toESM(require("fs"));
4223
4644
  path11 = __toESM(require("path"));
4224
- import_node_child_process5 = require("child_process");
4645
+ import_node_child_process6 = require("child_process");
4225
4646
  init_connection();
4226
4647
  init_queries();
4227
4648
  init_machine();
@@ -4230,15 +4651,781 @@ var init_run = __esm({
4230
4651
  init_spawn();
4231
4652
  init_config();
4232
4653
  init_shutdown();
4654
+ init_git();
4655
+ init_format();
4656
+ }
4657
+ });
4658
+
4659
+ // src/swarm/worktree.ts
4660
+ function createWorktree(mainRoot, slug, paddedNum) {
4661
+ const projectName = path12.basename(mainRoot);
4662
+ const worktreeName = `${projectName}-swarm-${paddedNum}-${slug}`;
4663
+ const worktreePath = path12.join(path12.dirname(mainRoot), worktreeName);
4664
+ const branch = `swarm/${paddedNum}-${slug}`;
4665
+ (0, import_node_child_process7.execSync)(`git worktree add ${JSON.stringify(worktreePath)} -b ${branch}`, {
4666
+ cwd: mainRoot,
4667
+ encoding: "utf-8",
4668
+ stdio: ["pipe", "pipe", "pipe"]
4669
+ });
4670
+ return {
4671
+ path: worktreePath,
4672
+ branch,
4673
+ slug,
4674
+ hypothesis: "",
4675
+ // filled in by caller
4676
+ paddedNum
4677
+ };
4678
+ }
4679
+ function initializeWorktree(mainRoot, worktreePath) {
4680
+ const majlisDir = path12.join(worktreePath, ".majlis");
4681
+ fs12.mkdirSync(majlisDir, { recursive: true });
4682
+ const configSrc = path12.join(mainRoot, ".majlis", "config.json");
4683
+ if (fs12.existsSync(configSrc)) {
4684
+ fs12.copyFileSync(configSrc, path12.join(majlisDir, "config.json"));
4685
+ }
4686
+ const agentsSrc = path12.join(mainRoot, ".majlis", "agents");
4687
+ if (fs12.existsSync(agentsSrc)) {
4688
+ const agentsDst = path12.join(majlisDir, "agents");
4689
+ fs12.mkdirSync(agentsDst, { recursive: true });
4690
+ for (const file of fs12.readdirSync(agentsSrc)) {
4691
+ fs12.copyFileSync(path12.join(agentsSrc, file), path12.join(agentsDst, file));
4692
+ }
4693
+ }
4694
+ const synthSrc = path12.join(mainRoot, "docs", "synthesis");
4695
+ if (fs12.existsSync(synthSrc)) {
4696
+ const synthDst = path12.join(worktreePath, "docs", "synthesis");
4697
+ fs12.mkdirSync(synthDst, { recursive: true });
4698
+ for (const file of fs12.readdirSync(synthSrc)) {
4699
+ const srcFile = path12.join(synthSrc, file);
4700
+ if (fs12.statSync(srcFile).isFile()) {
4701
+ fs12.copyFileSync(srcFile, path12.join(synthDst, file));
4702
+ }
4703
+ }
4704
+ }
4705
+ const templateSrc = path12.join(mainRoot, "docs", "experiments", "_TEMPLATE.md");
4706
+ if (fs12.existsSync(templateSrc)) {
4707
+ const expDir = path12.join(worktreePath, "docs", "experiments");
4708
+ fs12.mkdirSync(expDir, { recursive: true });
4709
+ fs12.copyFileSync(templateSrc, path12.join(expDir, "_TEMPLATE.md"));
4710
+ }
4711
+ const db = openDbAt(worktreePath);
4712
+ db.close();
4713
+ }
4714
+ function cleanupWorktree(mainRoot, wt) {
4715
+ try {
4716
+ (0, import_node_child_process7.execSync)(`git worktree remove ${JSON.stringify(wt.path)} --force`, {
4717
+ cwd: mainRoot,
4718
+ encoding: "utf-8",
4719
+ stdio: ["pipe", "pipe", "pipe"]
4720
+ });
4721
+ } catch {
4722
+ warn(`Could not remove worktree ${wt.path} \u2014 remove manually.`);
4723
+ }
4724
+ try {
4725
+ (0, import_node_child_process7.execSync)(`git branch -D ${wt.branch}`, {
4726
+ cwd: mainRoot,
4727
+ encoding: "utf-8",
4728
+ stdio: ["pipe", "pipe", "pipe"]
4729
+ });
4730
+ } catch {
4731
+ }
4732
+ try {
4733
+ (0, import_node_child_process7.execSync)("git worktree prune", {
4734
+ cwd: mainRoot,
4735
+ encoding: "utf-8",
4736
+ stdio: ["pipe", "pipe", "pipe"]
4737
+ });
4738
+ } catch {
4739
+ }
4740
+ }
4741
+ var fs12, path12, import_node_child_process7;
4742
+ var init_worktree = __esm({
4743
+ "src/swarm/worktree.ts"() {
4744
+ "use strict";
4745
+ fs12 = __toESM(require("fs"));
4746
+ path12 = __toESM(require("path"));
4747
+ import_node_child_process7 = require("child_process");
4748
+ init_connection();
4749
+ init_format();
4750
+ }
4751
+ });
4752
+
4753
+ // src/swarm/runner.ts
4754
+ async function runExperimentInWorktree(wt) {
4755
+ const label = `[swarm:${wt.paddedNum}]`;
4756
+ let db;
4757
+ let exp = null;
4758
+ let overallGrade = null;
4759
+ let stepCount = 0;
4760
+ try {
4761
+ db = openDbAt(wt.path);
4762
+ exp = createExperiment(db, wt.slug, wt.branch, wt.hypothesis, null, null);
4763
+ updateExperimentStatus(db, exp.id, "reframed");
4764
+ exp.status = "reframed";
4765
+ const templatePath = path13.join(wt.path, "docs", "experiments", "_TEMPLATE.md");
4766
+ if (fs13.existsSync(templatePath)) {
4767
+ const template = fs13.readFileSync(templatePath, "utf-8");
4768
+ const logContent = template.replace(/\{\{title\}\}/g, wt.hypothesis).replace(/\{\{hypothesis\}\}/g, wt.hypothesis).replace(/\{\{branch\}\}/g, wt.branch).replace(/\{\{status\}\}/g, "classified").replace(/\{\{sub_type\}\}/g, "unclassified").replace(/\{\{date\}\}/g, (/* @__PURE__ */ new Date()).toISOString().split("T")[0]);
4769
+ const logPath = path13.join(wt.path, "docs", "experiments", `${wt.paddedNum}-${wt.slug}.md`);
4770
+ fs13.writeFileSync(logPath, logContent);
4771
+ }
4772
+ info(`${label} Starting: ${wt.hypothesis}`);
4773
+ while (stepCount < MAX_STEPS) {
4774
+ if (isShutdownRequested()) {
4775
+ warn(`${label} Shutdown requested. Stopping.`);
4776
+ break;
4777
+ }
4778
+ stepCount++;
4779
+ const fresh = getExperimentBySlug(db, wt.slug);
4780
+ if (!fresh) break;
4781
+ exp = fresh;
4782
+ if (isTerminal(exp.status)) {
4783
+ success(`${label} Reached terminal: ${exp.status}`);
4784
+ break;
4785
+ }
4786
+ const valid = validNext(exp.status);
4787
+ if (valid.length === 0) break;
4788
+ const nextStep = determineNextStep(
4789
+ exp,
4790
+ valid,
4791
+ hasDoubts(db, exp.id),
4792
+ hasChallenges(db, exp.id)
4793
+ );
4794
+ info(`${label} [${stepCount}/${MAX_STEPS}] ${exp.status} -> ${nextStep}`);
4795
+ if (nextStep === "resolved" /* RESOLVED */) {
4796
+ overallGrade = await resolveDbOnly(db, exp, wt.path);
4797
+ continue;
4798
+ }
4799
+ if (nextStep === "compressed" /* COMPRESSED */) {
4800
+ await runStep("compress", db, exp, wt.path);
4801
+ updateExperimentStatus(db, exp.id, "compressed");
4802
+ continue;
4803
+ }
4804
+ if (nextStep === "merged" /* MERGED */) {
4805
+ updateExperimentStatus(db, exp.id, "merged");
4806
+ success(`${label} Merged.`);
4807
+ break;
4808
+ }
4809
+ if (nextStep === "reframed" /* REFRAMED */) {
4810
+ updateExperimentStatus(db, exp.id, "reframed");
4811
+ continue;
4812
+ }
4813
+ const stepName = statusToStepName(nextStep);
4814
+ if (!stepName) {
4815
+ warn(`${label} Unknown step: ${nextStep}`);
4816
+ break;
4817
+ }
4818
+ try {
4819
+ await runStep(stepName, db, exp, wt.path);
4820
+ } catch (err) {
4821
+ const message = err instanceof Error ? err.message : String(err);
4822
+ warn(`${label} Step failed: ${message}`);
4823
+ try {
4824
+ insertDeadEnd(
4825
+ db,
4826
+ exp.id,
4827
+ exp.hypothesis ?? exp.slug,
4828
+ message,
4829
+ `Process failure: ${message}`,
4830
+ exp.sub_type,
4831
+ "procedural"
4832
+ );
4833
+ updateExperimentStatus(db, exp.id, "dead_end");
4834
+ } catch {
4835
+ }
4836
+ break;
4837
+ }
4838
+ }
4839
+ if (stepCount >= MAX_STEPS) {
4840
+ warn(`${label} Hit max steps (${MAX_STEPS}).`);
4841
+ }
4842
+ const finalExp = getExperimentBySlug(db, wt.slug);
4843
+ if (finalExp) exp = finalExp;
4844
+ const finalStatus = exp?.status ?? "error";
4845
+ return {
4846
+ worktree: wt,
4847
+ experiment: exp,
4848
+ finalStatus,
4849
+ overallGrade,
4850
+ costUsd: 0,
4851
+ // TODO: track via SDK when available
4852
+ stepCount
4853
+ };
4854
+ } catch (err) {
4855
+ const message = err instanceof Error ? err.message : String(err);
4856
+ warn(`${label} Fatal error: ${message}`);
4857
+ return {
4858
+ worktree: wt,
4859
+ experiment: exp,
4860
+ finalStatus: "error",
4861
+ overallGrade: null,
4862
+ costUsd: 0,
4863
+ stepCount,
4864
+ error: message
4865
+ };
4866
+ } finally {
4867
+ if (db) {
4868
+ try {
4869
+ db.close();
4870
+ } catch {
4871
+ }
4872
+ }
4873
+ }
4874
+ }
4875
+ function statusToStepName(status2) {
4876
+ switch (status2) {
4877
+ case "gated" /* GATED */:
4878
+ return "gate";
4879
+ case "building" /* BUILDING */:
4880
+ return "build";
4881
+ case "challenged" /* CHALLENGED */:
4882
+ return "challenge";
4883
+ case "doubted" /* DOUBTED */:
4884
+ return "doubt";
4885
+ case "scouted" /* SCOUTED */:
4886
+ return "scout";
4887
+ case "verifying" /* VERIFYING */:
4888
+ return "verify";
4889
+ default:
4890
+ return null;
4891
+ }
4892
+ }
4893
+ var fs13, path13, MAX_STEPS;
4894
+ var init_runner = __esm({
4895
+ "src/swarm/runner.ts"() {
4896
+ "use strict";
4897
+ fs13 = __toESM(require("fs"));
4898
+ path13 = __toESM(require("path"));
4899
+ init_connection();
4900
+ init_queries();
4901
+ init_machine();
4902
+ init_types2();
4903
+ init_cycle();
4904
+ init_resolve();
4905
+ init_shutdown();
4906
+ init_format();
4907
+ MAX_STEPS = 20;
4908
+ }
4909
+ });
4910
+
4911
+ // src/swarm/aggregate.ts
4912
+ function importExperimentFromWorktree(sourceDb, targetDb, slug) {
4913
+ const sourceExp = sourceDb.prepare(
4914
+ "SELECT * FROM experiments WHERE slug = ?"
4915
+ ).get(slug);
4916
+ if (!sourceExp) {
4917
+ throw new Error(`Experiment ${slug} not found in source DB`);
4918
+ }
4919
+ const sourceId = sourceExp.id;
4920
+ const insertExp = targetDb.prepare(`
4921
+ INSERT INTO experiments (slug, branch, status, classification_ref, sub_type,
4922
+ hypothesis, builder_guidance, created_at, updated_at)
4923
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
4924
+ `);
4925
+ const result = insertExp.run(
4926
+ sourceExp.slug,
4927
+ sourceExp.branch,
4928
+ sourceExp.status,
4929
+ sourceExp.classification_ref,
4930
+ sourceExp.sub_type,
4931
+ sourceExp.hypothesis,
4932
+ sourceExp.builder_guidance,
4933
+ sourceExp.created_at,
4934
+ sourceExp.updated_at
4935
+ );
4936
+ const targetId = result.lastInsertRowid;
4937
+ for (const table2 of CHILD_TABLES) {
4938
+ importChildTable(sourceDb, targetDb, table2, sourceId, targetId);
4939
+ }
4940
+ const stfRows = sourceDb.prepare(
4941
+ "SELECT * FROM sub_type_failures WHERE experiment_id = ?"
4942
+ ).all(sourceId);
4943
+ for (const row of stfRows) {
4944
+ targetDb.prepare(`
4945
+ INSERT INTO sub_type_failures (sub_type, experiment_id, grade, created_at)
4946
+ VALUES (?, ?, ?, ?)
4947
+ `).run(row.sub_type, targetId, row.grade, row.created_at);
4948
+ }
4949
+ return targetId;
4950
+ }
4951
+ function importChildTable(sourceDb, targetDb, table2, sourceExpId, targetExpId) {
4952
+ const rows = sourceDb.prepare(
4953
+ `SELECT * FROM ${table2} WHERE experiment_id = ?`
4954
+ ).all(sourceExpId);
4955
+ if (rows.length === 0) return;
4956
+ const cols = Object.keys(rows[0]).filter((c) => c !== "id");
4957
+ const placeholders = cols.map(() => "?").join(", ");
4958
+ const insert = targetDb.prepare(
4959
+ `INSERT INTO ${table2} (${cols.join(", ")}) VALUES (${placeholders})`
4960
+ );
4961
+ for (const row of rows) {
4962
+ const values = cols.map(
4963
+ (c) => c === "experiment_id" ? targetExpId : row[c]
4964
+ );
4965
+ insert.run(...values);
4966
+ }
4967
+ }
4968
+ function aggregateSwarmResults(mainRoot, mainDb, results) {
4969
+ let mergedCount = 0;
4970
+ let deadEndCount = 0;
4971
+ let errorCount = 0;
4972
+ let totalCostUsd = 0;
4973
+ for (const r of results) {
4974
+ totalCostUsd += r.costUsd;
4975
+ if (r.error || !r.experiment) {
4976
+ errorCount++;
4977
+ continue;
4978
+ }
4979
+ try {
4980
+ const sourceDb = openDbAt(r.worktree.path);
4981
+ mainDb.transaction(() => {
4982
+ importExperimentFromWorktree(sourceDb, mainDb, r.worktree.slug);
4983
+ })();
4984
+ sourceDb.close();
4985
+ if (r.finalStatus === "merged") mergedCount++;
4986
+ else if (r.finalStatus === "dead_end") deadEndCount++;
4987
+ } catch (err) {
4988
+ const msg = err instanceof Error ? err.message : String(err);
4989
+ warn(`Failed to import ${r.worktree.slug}: ${msg}`);
4990
+ errorCount++;
4991
+ }
4992
+ }
4993
+ const ranked = results.filter((r) => r.overallGrade && !r.error).sort((a, b) => {
4994
+ const aRank = GRADE_RANK[a.overallGrade] ?? 99;
4995
+ const bRank = GRADE_RANK[b.overallGrade] ?? 99;
4996
+ return aRank - bRank;
4997
+ });
4998
+ const best = ranked.length > 0 ? ranked[0] : null;
4999
+ return {
5000
+ goal: "",
5001
+ // filled by caller
5002
+ parallelCount: results.length,
5003
+ results,
5004
+ bestExperiment: best,
5005
+ totalCostUsd,
5006
+ mergedCount,
5007
+ deadEndCount,
5008
+ errorCount
5009
+ };
5010
+ }
5011
+ var CHILD_TABLES, GRADE_RANK;
5012
+ var init_aggregate = __esm({
5013
+ "src/swarm/aggregate.ts"() {
5014
+ "use strict";
5015
+ init_connection();
5016
+ init_format();
5017
+ CHILD_TABLES = [
5018
+ "decisions",
5019
+ "doubts",
5020
+ "challenges",
5021
+ "verifications",
5022
+ "metrics",
5023
+ "dead_ends",
5024
+ "reframes",
5025
+ "findings"
5026
+ ];
5027
+ GRADE_RANK = {
5028
+ sound: 0,
5029
+ good: 1,
5030
+ weak: 2,
5031
+ rejected: 3
5032
+ };
5033
+ }
5034
+ });
5035
+
5036
+ // src/commands/swarm.ts
5037
+ var swarm_exports = {};
5038
+ __export(swarm_exports, {
5039
+ swarm: () => swarm
5040
+ });
5041
+ async function swarm(args) {
5042
+ const root = findProjectRoot();
5043
+ if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
5044
+ const goal = args.filter((a) => !a.startsWith("--")).join(" ");
5045
+ if (!goal) throw new Error('Usage: majlis swarm "goal description" [--parallel N]');
5046
+ const parallelStr = getFlagValue(args, "--parallel");
5047
+ const parallelCount = Math.min(
5048
+ Math.max(2, parseInt(parallelStr ?? String(DEFAULT_PARALLEL), 10) || DEFAULT_PARALLEL),
5049
+ MAX_PARALLEL
5050
+ );
5051
+ try {
5052
+ const status2 = (0, import_node_child_process8.execSync)("git status --porcelain", {
5053
+ cwd: root,
5054
+ encoding: "utf-8",
5055
+ stdio: ["pipe", "pipe", "pipe"]
5056
+ }).trim();
5057
+ if (status2) {
5058
+ warn("Working tree has uncommitted changes. Commit or stash before swarming.");
5059
+ throw new Error("Dirty working tree. Commit or stash first.");
5060
+ }
5061
+ } catch (err) {
5062
+ if (err instanceof Error && err.message.includes("Dirty working tree")) throw err;
5063
+ warn("Could not check git status.");
5064
+ }
5065
+ const db = getDb(root);
5066
+ const swarmRun = createSwarmRun(db, goal, parallelCount);
5067
+ header(`Swarm Mode \u2014 ${goal}`);
5068
+ info(`Generating ${parallelCount} diverse hypotheses...`);
5069
+ const hypotheses = await deriveMultipleHypotheses(goal, root, parallelCount);
5070
+ if (hypotheses.length === 0) {
5071
+ success("Planner says the goal has been met. Nothing to swarm.");
5072
+ updateSwarmRun(db, swarmRun.id, "completed", 0, null);
5073
+ return;
5074
+ }
5075
+ info(`Got ${hypotheses.length} hypotheses:`);
5076
+ for (let i = 0; i < hypotheses.length; i++) {
5077
+ info(` ${i + 1}. ${hypotheses[i]}`);
5078
+ }
5079
+ const worktrees = [];
5080
+ for (let i = 0; i < hypotheses.length; i++) {
5081
+ const paddedNum = String(i + 1).padStart(3, "0");
5082
+ const slug = await generateSlug(hypotheses[i], root);
5083
+ try {
5084
+ const wt = createWorktree(root, slug, paddedNum);
5085
+ wt.hypothesis = hypotheses[i];
5086
+ initializeWorktree(root, wt.path);
5087
+ worktrees.push(wt);
5088
+ addSwarmMember(db, swarmRun.id, slug, wt.path);
5089
+ info(`Created worktree ${paddedNum}: ${slug}`);
5090
+ } catch (err) {
5091
+ const msg = err instanceof Error ? err.message : String(err);
5092
+ warn(`Failed to create worktree for hypothesis ${i + 1}: ${msg}`);
5093
+ }
5094
+ }
5095
+ if (worktrees.length === 0) {
5096
+ warn("No worktrees created. Aborting swarm.");
5097
+ updateSwarmRun(db, swarmRun.id, "failed", 0, null);
5098
+ return;
5099
+ }
5100
+ info(`Running ${worktrees.length} experiments in parallel...`);
5101
+ info("");
5102
+ const settled = await Promise.allSettled(
5103
+ worktrees.map((wt) => runExperimentInWorktree(wt))
5104
+ );
5105
+ const results = settled.map((s, i) => {
5106
+ if (s.status === "fulfilled") return s.value;
5107
+ return {
5108
+ worktree: worktrees[i],
5109
+ experiment: null,
5110
+ finalStatus: "error",
5111
+ overallGrade: null,
5112
+ costUsd: 0,
5113
+ stepCount: 0,
5114
+ error: s.reason instanceof Error ? s.reason.message : String(s.reason)
5115
+ };
5116
+ });
5117
+ for (const r of results) {
5118
+ updateSwarmMember(
5119
+ db,
5120
+ swarmRun.id,
5121
+ r.worktree.slug,
5122
+ r.finalStatus,
5123
+ r.overallGrade,
5124
+ r.costUsd,
5125
+ r.error ?? null
5126
+ );
5127
+ }
5128
+ info("");
5129
+ header("Aggregation");
5130
+ const summary = aggregateSwarmResults(root, db, results);
5131
+ summary.goal = goal;
5132
+ if (summary.bestExperiment && isMergeable(summary.bestExperiment.overallGrade)) {
5133
+ const best = summary.bestExperiment;
5134
+ info(`Best experiment: ${best.worktree.slug} (${best.overallGrade})`);
5135
+ try {
5136
+ (0, import_node_child_process8.execSync)(
5137
+ `git merge ${best.worktree.branch} --no-ff -m "Merge swarm winner: ${best.worktree.slug}"`,
5138
+ { cwd: root, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
5139
+ );
5140
+ success(`Merged ${best.worktree.slug} into main.`);
5141
+ } catch {
5142
+ warn(`Git merge of ${best.worktree.slug} failed. Merge manually with:`);
5143
+ info(` git merge ${best.worktree.branch} --no-ff`);
5144
+ }
5145
+ } else {
5146
+ info("No experiment achieved sound/good grade. Nothing merged.");
5147
+ }
5148
+ for (const r of results) {
5149
+ if (r === summary.bestExperiment || r.error || !r.experiment) continue;
5150
+ const mainExp = getExperimentBySlug(db, r.worktree.slug);
5151
+ if (mainExp && mainExp.status !== "dead_end") {
5152
+ updateExperimentStatus(db, mainExp.id, "dead_end");
5153
+ }
5154
+ }
5155
+ updateSwarmRun(
5156
+ db,
5157
+ swarmRun.id,
5158
+ summary.errorCount === results.length ? "failed" : "completed",
5159
+ summary.totalCostUsd,
5160
+ summary.bestExperiment?.worktree.slug ?? null
5161
+ );
5162
+ info("Cleaning up worktrees...");
5163
+ for (const wt of worktrees) {
5164
+ cleanupWorktree(root, wt);
5165
+ }
5166
+ info("");
5167
+ header("Swarm Summary");
5168
+ info(`Goal: ${goal}`);
5169
+ info(`Parallel: ${worktrees.length}`);
5170
+ info(`Results:`);
5171
+ for (const r of results) {
5172
+ const grade = r.overallGrade ?? "n/a";
5173
+ const status2 = r.error ? `ERROR: ${r.error.slice(0, 60)}` : r.finalStatus;
5174
+ const marker = r === summary.bestExperiment ? " <-- BEST" : "";
5175
+ info(` ${r.worktree.paddedNum} ${r.worktree.slug}: ${grade} (${status2})${marker}`);
5176
+ }
5177
+ info(`Merged: ${summary.mergedCount} | Dead-ends: ${summary.deadEndCount} | Errors: ${summary.errorCount}`);
5178
+ }
5179
+ function isMergeable(grade) {
5180
+ return grade === "sound" || grade === "good";
5181
+ }
5182
+ async function deriveMultipleHypotheses(goal, root, count) {
5183
+ const synthesis = truncateContext(
5184
+ readFileOrEmpty(path14.join(root, "docs", "synthesis", "current.md")),
5185
+ CONTEXT_LIMITS.synthesis
5186
+ );
5187
+ const fragility = truncateContext(
5188
+ readFileOrEmpty(path14.join(root, "docs", "synthesis", "fragility.md")),
5189
+ CONTEXT_LIMITS.fragility
5190
+ );
5191
+ const deadEndsDoc = truncateContext(
5192
+ readFileOrEmpty(path14.join(root, "docs", "synthesis", "dead-ends.md")),
5193
+ CONTEXT_LIMITS.deadEnds
5194
+ );
5195
+ const diagnosis = truncateContext(readLatestDiagnosis(root), CONTEXT_LIMITS.synthesis);
5196
+ const db = getDb(root);
5197
+ const deadEnds = listAllDeadEnds(db);
5198
+ const config = loadConfig(root);
5199
+ let metricsOutput = "";
5200
+ if (config.metrics?.command) {
5201
+ try {
5202
+ metricsOutput = (0, import_node_child_process8.execSync)(config.metrics.command, {
5203
+ cwd: root,
5204
+ encoding: "utf-8",
5205
+ timeout: 6e4,
5206
+ stdio: ["pipe", "pipe", "pipe"]
5207
+ }).trim();
5208
+ } catch {
5209
+ metricsOutput = "(metrics command failed)";
5210
+ }
5211
+ }
5212
+ const result = await spawnSynthesiser({
5213
+ taskPrompt: `You are the Planner for a parallel Majlis swarm.
5214
+
5215
+ ## Goal
5216
+ ${goal}
5217
+ ${diagnosis ? `
5218
+ ## Latest Diagnosis Report (PRIORITISE \u2014 deep analysis from diagnostician agent)
5219
+ ${diagnosis}
5220
+ ` : ""}
5221
+ ## Current Metrics
5222
+ ${metricsOutput || "(no metrics configured)"}
5223
+
5224
+ ## Synthesis (what we know so far)
5225
+ ${synthesis || "(empty \u2014 first experiment)"}
5226
+
5227
+ ## Fragility Map (known weak areas)
5228
+ ${fragility || "(none)"}
5229
+
5230
+ ## Dead-End Registry
5231
+ ${deadEndsDoc || "(none)"}
5232
+
5233
+ ## Dead Ends (from DB \u2014 ${deadEnds.length} total)
5234
+ ${deadEnds.map((d) => `- [${d.category ?? "structural"}] ${d.approach}: ${d.why_failed} [constraint: ${d.structural_constraint}]`).join("\n") || "(none)"}
5235
+
5236
+ Note: [structural] dead ends are HARD CONSTRAINTS \u2014 hypotheses MUST NOT repeat these approaches.
5237
+ [procedural] dead ends are process failures \u2014 the approach may still be valid if executed differently.
5238
+
5239
+ ## Your Task
5240
+ DO NOT read source code or use tools. All context you need is above. Plan from the synthesis and dead-end registry.
5241
+
5242
+ 1. Assess: based on the metrics and synthesis, has the goal been met? Be specific.
5243
+ 2. If YES \u2014 output the JSON block below with goal_met: true.
5244
+ 3. If NO \u2014 generate exactly ${count} DIVERSE hypotheses for parallel testing.
5245
+
5246
+ Requirements for hypotheses:
5247
+ - Each must attack the problem from a DIFFERENT angle
5248
+ - They must NOT share the same mechanism, function target, or strategy
5249
+ - At least one should be an unconventional or indirect approach
5250
+ - None may repeat a dead-ended structural approach
5251
+ - Each must be specific and actionable \u2014 name the function or mechanism to change
5252
+ - Do NOT reference specific line numbers \u2014 they shift between experiments
5253
+
5254
+ CRITICAL: Your LAST line of output MUST be EXACTLY this format (on its own line, nothing after it):
5255
+ <!-- majlis-json {"goal_met": false, "hypotheses": ["hypothesis 1", "hypothesis 2", "hypothesis 3"]} -->
5256
+
5257
+ If the goal is met:
5258
+ <!-- majlis-json {"goal_met": true, "hypotheses": []} -->`
5259
+ }, root, { maxTurns: 2, tools: [] });
5260
+ if (result.structured?.goal_met === true) return [];
5261
+ if (result.structured?.hypotheses && Array.isArray(result.structured.hypotheses)) {
5262
+ return result.structured.hypotheses.filter(
5263
+ (h) => typeof h === "string" && h.length > 10
5264
+ );
5265
+ }
5266
+ const blockMatch = result.output.match(/<!--\s*majlis-json\s*(\{[\s\S]*?\})\s*-->/);
5267
+ if (blockMatch) {
5268
+ try {
5269
+ const parsed = JSON.parse(blockMatch[1]);
5270
+ if (parsed.goal_met === true) return [];
5271
+ if (Array.isArray(parsed.hypotheses)) {
5272
+ return parsed.hypotheses.filter(
5273
+ (h) => typeof h === "string" && h.length > 10
5274
+ );
5275
+ }
5276
+ } catch {
5277
+ }
5278
+ }
5279
+ warn("Planner did not return structured hypotheses. Using goal as single hypothesis.");
5280
+ return [goal];
5281
+ }
5282
+ var path14, import_node_child_process8, MAX_PARALLEL, DEFAULT_PARALLEL;
5283
+ var init_swarm = __esm({
5284
+ "src/commands/swarm.ts"() {
5285
+ "use strict";
5286
+ path14 = __toESM(require("path"));
5287
+ import_node_child_process8 = require("child_process");
5288
+ init_connection();
5289
+ init_queries();
5290
+ init_spawn();
5291
+ init_config();
5292
+ init_worktree();
5293
+ init_runner();
5294
+ init_aggregate();
5295
+ init_format();
5296
+ MAX_PARALLEL = 8;
5297
+ DEFAULT_PARALLEL = 3;
5298
+ }
5299
+ });
5300
+
5301
+ // src/commands/diagnose.ts
5302
+ var diagnose_exports = {};
5303
+ __export(diagnose_exports, {
5304
+ diagnose: () => diagnose
5305
+ });
5306
+ async function diagnose(args) {
5307
+ const root = findProjectRoot();
5308
+ if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
5309
+ const db = getDb(root);
5310
+ const focus = args.filter((a) => !a.startsWith("--")).join(" ");
5311
+ const keepScripts = args.includes("--keep-scripts");
5312
+ const scriptsDir = path15.join(root, ".majlis", "scripts");
5313
+ if (!fs14.existsSync(scriptsDir)) {
5314
+ fs14.mkdirSync(scriptsDir, { recursive: true });
5315
+ }
5316
+ header("Deep Diagnosis");
5317
+ if (focus) info(`Focus: ${focus}`);
5318
+ const dbExport = exportForDiagnostician(db);
5319
+ const synthesis = readFileOrEmpty(path15.join(root, "docs", "synthesis", "current.md"));
5320
+ const fragility = readFileOrEmpty(path15.join(root, "docs", "synthesis", "fragility.md"));
5321
+ const deadEndsDoc = readFileOrEmpty(path15.join(root, "docs", "synthesis", "dead-ends.md"));
5322
+ const config = loadConfig(root);
5323
+ let metricsOutput = "";
5324
+ if (config.metrics?.command) {
5325
+ try {
5326
+ metricsOutput = (0, import_node_child_process9.execSync)(config.metrics.command, {
5327
+ cwd: root,
5328
+ encoding: "utf-8",
5329
+ timeout: 6e4,
5330
+ stdio: ["pipe", "pipe", "pipe"]
5331
+ }).trim();
5332
+ } catch {
5333
+ metricsOutput = "(metrics command failed)";
5334
+ }
5335
+ }
5336
+ let taskPrompt = `## Full Database Export (CANONICAL \u2014 source of truth)
5337
+ ${dbExport}
5338
+
5339
+ `;
5340
+ taskPrompt += `## Current Synthesis
5341
+ ${synthesis || "(empty \u2014 no experiments yet)"}
5342
+
5343
+ `;
5344
+ taskPrompt += `## Fragility Map
5345
+ ${fragility || "(none)"}
5346
+
5347
+ `;
5348
+ taskPrompt += `## Dead-End Registry
5349
+ ${deadEndsDoc || "(none)"}
5350
+
5351
+ `;
5352
+ taskPrompt += `## Current Metrics
5353
+ ${metricsOutput || "(no metrics configured)"}
5354
+
5355
+ `;
5356
+ taskPrompt += `## Project Objective
5357
+ ${config.project?.objective || "(not specified)"}
5358
+
5359
+ `;
5360
+ if (focus) {
5361
+ taskPrompt += `## Focus Area
5362
+ The user has asked you to focus your diagnosis on: ${focus}
5363
+
5364
+ `;
5365
+ }
5366
+ taskPrompt += `## Your Task
5367
+ Perform a deep diagnostic analysis of this project. Identify root causes, recurring patterns, evidence gaps, and investigation directions. You have 60 turns \u2014 use them for depth. Write analysis scripts to .majlis/scripts/ as needed.
5368
+
5369
+ Remember: you may write files ONLY to .majlis/scripts/. You cannot modify project code.`;
5370
+ info("Spawning diagnostician (60 turns, full DB access)...");
5371
+ const result = await spawnAgent("diagnostician", { taskPrompt }, root);
5372
+ const diagnosisDir = path15.join(root, "docs", "diagnosis");
5373
+ if (!fs14.existsSync(diagnosisDir)) {
5374
+ fs14.mkdirSync(diagnosisDir, { recursive: true });
5375
+ }
5376
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
5377
+ const artifactPath = path15.join(diagnosisDir, `diagnosis-${timestamp}.md`);
5378
+ fs14.writeFileSync(artifactPath, result.output);
5379
+ info(`Diagnostic report: docs/diagnosis/diagnosis-${timestamp}.md`);
5380
+ if (result.structured?.diagnosis) {
5381
+ const d = result.structured.diagnosis;
5382
+ if (d.root_causes?.length) {
5383
+ info(`Root causes identified: ${d.root_causes.length}`);
5384
+ }
5385
+ if (d.investigation_directions?.length) {
5386
+ info(`Investigation directions: ${d.investigation_directions.length}`);
5387
+ }
5388
+ }
5389
+ if (!keepScripts) {
5390
+ try {
5391
+ const files = fs14.readdirSync(scriptsDir);
5392
+ for (const f of files) {
5393
+ fs14.unlinkSync(path15.join(scriptsDir, f));
5394
+ }
5395
+ fs14.rmdirSync(scriptsDir);
5396
+ info("Cleaned up .majlis/scripts/");
5397
+ } catch {
5398
+ }
5399
+ } else {
5400
+ info("Scripts preserved in .majlis/scripts/ (--keep-scripts)");
5401
+ }
5402
+ if (result.truncated) {
5403
+ warn("Diagnostician was truncated (hit 60 turn limit).");
5404
+ }
5405
+ autoCommit(root, `diagnosis: ${focus || "general"}`);
5406
+ success("Diagnosis complete.");
5407
+ }
5408
+ var fs14, path15, import_node_child_process9;
5409
+ var init_diagnose = __esm({
5410
+ "src/commands/diagnose.ts"() {
5411
+ "use strict";
5412
+ fs14 = __toESM(require("fs"));
5413
+ path15 = __toESM(require("path"));
5414
+ import_node_child_process9 = require("child_process");
5415
+ init_connection();
5416
+ init_queries();
5417
+ init_spawn();
5418
+ init_config();
5419
+ init_git();
4233
5420
  init_format();
4234
5421
  }
4235
5422
  });
4236
5423
 
4237
5424
  // src/cli.ts
4238
- var fs12 = __toESM(require("fs"));
4239
- var path12 = __toESM(require("path"));
5425
+ var fs15 = __toESM(require("fs"));
5426
+ var path16 = __toESM(require("path"));
4240
5427
  var VERSION = JSON.parse(
4241
- fs12.readFileSync(path12.join(__dirname, "..", "package.json"), "utf-8")
5428
+ fs15.readFileSync(path16.join(__dirname, "..", "package.json"), "utf-8")
4242
5429
  ).version;
4243
5430
  async function main() {
4244
5431
  let sigintCount = 0;
@@ -4348,11 +5535,21 @@ async function main() {
4348
5535
  await run2(rest);
4349
5536
  break;
4350
5537
  }
5538
+ case "swarm": {
5539
+ const { swarm: swarm2 } = await Promise.resolve().then(() => (init_swarm(), swarm_exports));
5540
+ await swarm2(rest);
5541
+ break;
5542
+ }
4351
5543
  case "audit": {
4352
5544
  const { audit: audit2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
4353
5545
  await audit2(rest);
4354
5546
  break;
4355
5547
  }
5548
+ case "diagnose": {
5549
+ const { diagnose: diagnose2 } = await Promise.resolve().then(() => (init_diagnose(), diagnose_exports));
5550
+ await diagnose2(rest);
5551
+ break;
5552
+ }
4356
5553
  default:
4357
5554
  console.error(`Unknown command: ${command}`);
4358
5555
  printHelp();
@@ -4405,6 +5602,7 @@ Queries:
4405
5602
 
4406
5603
  Audit:
4407
5604
  audit "objective" Maqasid check \u2014 is the frame right?
5605
+ diagnose ["focus area"] Deep diagnosis \u2014 root causes, patterns, gaps
4408
5606
 
4409
5607
  Sessions:
4410
5608
  session start "intent" Declare session intent
@@ -4412,6 +5610,7 @@ Sessions:
4412
5610
 
4413
5611
  Orchestration:
4414
5612
  run "goal" Autonomous orchestration until goal met
5613
+ swarm "goal" [--parallel N] Run N experiments in parallel worktrees
4415
5614
 
4416
5615
  Flags:
4417
5616
  --json Output as JSON