majlis 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1140 -15
  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;
@@ -1838,6 +2051,8 @@ function getExtractionSchema(role) {
1838
2051
  return '{"findings": [{"approach": "string", "source": "string", "relevance": "string", "contradicts_current": true}]}';
1839
2052
  case "compressor":
1840
2053
  return '{"compression_report": {"synthesis_delta": "string", "new_dead_ends": ["string"], "fragility_changes": ["string"]}}';
2054
+ case "diagnostician":
2055
+ return '{"diagnosis": {"root_causes": ["string"], "patterns": ["string"], "evidence_gaps": ["string"], "investigation_directions": ["string"]}}';
1841
2056
  default:
1842
2057
  return EXTRACTION_SCHEMA;
1843
2058
  }
@@ -1861,7 +2076,8 @@ var init_types = __esm({
1861
2076
  gatekeeper: ["gate_decision"],
1862
2077
  reframer: ["reframe"],
1863
2078
  scout: ["findings"],
1864
- compressor: ["compression_report"]
2079
+ compressor: ["compression_report"],
2080
+ diagnostician: ["diagnosis"]
1865
2081
  };
1866
2082
  }
1867
2083
  });
@@ -1996,7 +2212,7 @@ ${truncated}`;
1996
2212
  }
1997
2213
  }
1998
2214
  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);
2215
+ 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
2216
  }
2001
2217
  function validateForRole(role, output) {
2002
2218
  const required = ROLE_REQUIRED_FIELDS[role];
@@ -2069,6 +2285,12 @@ You may ONLY write to docs/synthesis/.
2069
2285
  - Have you updated current.md, fragility.md, dead-ends.md?
2070
2286
  - If yes \u2192 output compression report JSON.
2071
2287
  - Do NOT write to MEMORY.md or files outside docs/synthesis/.`;
2288
+ case "diagnostician":
2289
+ return `${header2}
2290
+ You are READ-ONLY for project code. Write ONLY to .majlis/scripts/.
2291
+ Focus on diagnosis, not fixing. Your value is insight, not implementation.
2292
+ Phase 1 (1-10): orientation. Phase 2 (11-40): deep investigation. Phase 3 (41-60): synthesis.
2293
+ If you are past turn 40, begin compiling your diagnostic report.`;
2072
2294
  default:
2073
2295
  return `${header2}
2074
2296
  Check: is your core task done? If yes, wrap up and output JSON.`;
@@ -2092,6 +2314,36 @@ function buildPreToolUseGuards(role) {
2092
2314
  { matcher: "Edit", hooks: [guardHook] }
2093
2315
  ];
2094
2316
  }
2317
+ if (role === "diagnostician") {
2318
+ const writeGuard = async (input) => {
2319
+ const toolInput = input.tool_input ?? {};
2320
+ const filePath = toolInput.file_path ?? "";
2321
+ if (filePath && !filePath.includes("/.majlis/scripts/")) {
2322
+ return {
2323
+ decision: "block",
2324
+ reason: `Diagnostician may only write to .majlis/scripts/. Blocked: ${filePath}`
2325
+ };
2326
+ }
2327
+ return {};
2328
+ };
2329
+ const bashGuard = async (input) => {
2330
+ const toolInput = input.tool_input ?? {};
2331
+ const command = toolInput.command ?? "";
2332
+ const destructive = /\b(rm\s+-rf|git\s+(checkout|reset|stash|clean|push)|chmod|chown|mkfs|dd\s+if=)\b/i;
2333
+ if (destructive.test(command)) {
2334
+ return {
2335
+ decision: "block",
2336
+ reason: `Diagnostician blocked destructive command: ${command.slice(0, 100)}`
2337
+ };
2338
+ }
2339
+ return {};
2340
+ };
2341
+ return [
2342
+ { matcher: "Write", hooks: [writeGuard] },
2343
+ { matcher: "Edit", hooks: [writeGuard] },
2344
+ { matcher: "Bash", hooks: [bashGuard] }
2345
+ ];
2346
+ }
2095
2347
  return void 0;
2096
2348
  }
2097
2349
  function buildAgentHooks(role, maxTurns) {
@@ -2365,7 +2617,7 @@ function writeArtifact(role, context, markdown, projectRoot) {
2365
2617
  };
2366
2618
  const dir = dirMap[role];
2367
2619
  if (!dir) return null;
2368
- if (role === "builder" || role === "compressor") return null;
2620
+ if (role === "builder" || role === "compressor" || role === "diagnostician") return null;
2369
2621
  const fullDir = path4.join(projectRoot, dir);
2370
2622
  if (!fs4.existsSync(fullDir)) {
2371
2623
  fs4.mkdirSync(fullDir, { recursive: true });
@@ -2394,14 +2646,16 @@ var init_spawn = __esm({
2394
2646
  compressor: 30,
2395
2647
  reframer: 20,
2396
2648
  scout: 20,
2397
- gatekeeper: 10
2649
+ gatekeeper: 10,
2650
+ diagnostician: 60
2398
2651
  };
2399
2652
  CHECKPOINT_INTERVAL = {
2400
2653
  builder: 15,
2401
2654
  verifier: 12,
2402
2655
  critic: 15,
2403
2656
  adversary: 15,
2404
- compressor: 15
2657
+ compressor: 15,
2658
+ diagnostician: 20
2405
2659
  };
2406
2660
  DIM2 = "\x1B[2m";
2407
2661
  RESET2 = "\x1B[0m";
@@ -3101,6 +3355,84 @@ async function resolve(db, exp, projectRoot) {
3101
3355
  }
3102
3356
  }
3103
3357
  }
3358
+ async function resolveDbOnly(db, exp, projectRoot) {
3359
+ let grades = getVerificationsByExperiment(db, exp.id);
3360
+ if (grades.length === 0) {
3361
+ warn(`No verification records for ${exp.slug}. Defaulting to weak.`);
3362
+ insertVerification(
3363
+ db,
3364
+ exp.id,
3365
+ "auto-default",
3366
+ "weak",
3367
+ null,
3368
+ null,
3369
+ "No structured verification output. Auto-defaulted to weak."
3370
+ );
3371
+ grades = getVerificationsByExperiment(db, exp.id);
3372
+ }
3373
+ const overallGrade = worstGrade(grades);
3374
+ switch (overallGrade) {
3375
+ case "sound":
3376
+ updateExperimentStatus(db, exp.id, "merged");
3377
+ success(`Experiment ${exp.slug} RESOLVED (sound) \u2014 git merge deferred.`);
3378
+ break;
3379
+ case "good": {
3380
+ const gaps = grades.filter((g) => g.grade === "good").map((g) => `- **${g.component}**: ${g.notes ?? "minor gaps"}`).join("\n");
3381
+ appendToFragilityMap(projectRoot, exp.slug, gaps);
3382
+ updateExperimentStatus(db, exp.id, "merged");
3383
+ success(`Experiment ${exp.slug} RESOLVED (good) \u2014 git merge deferred.`);
3384
+ break;
3385
+ }
3386
+ case "weak": {
3387
+ const confirmedDoubts = getConfirmedDoubts(db, exp.id);
3388
+ const guidance = await spawnSynthesiser({
3389
+ experiment: {
3390
+ id: exp.id,
3391
+ slug: exp.slug,
3392
+ hypothesis: exp.hypothesis,
3393
+ status: exp.status,
3394
+ sub_type: exp.sub_type,
3395
+ builder_guidance: exp.builder_guidance
3396
+ },
3397
+ verificationReport: grades,
3398
+ confirmedDoubts,
3399
+ 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."
3400
+ }, projectRoot);
3401
+ const guidanceText = guidance.structured?.guidance ?? guidance.output;
3402
+ db.transaction(() => {
3403
+ storeBuilderGuidance(db, exp.id, guidanceText);
3404
+ updateExperimentStatus(db, exp.id, "building");
3405
+ if (exp.sub_type) {
3406
+ incrementSubTypeFailure(db, exp.sub_type, exp.id, "weak");
3407
+ }
3408
+ })();
3409
+ warn(`Experiment ${exp.slug} CYCLING BACK (weak). Guidance generated.`);
3410
+ break;
3411
+ }
3412
+ case "rejected": {
3413
+ const rejectedComponents = grades.filter((g) => g.grade === "rejected");
3414
+ const whyFailed = rejectedComponents.map((r) => r.notes ?? "rejected").join("; ");
3415
+ db.transaction(() => {
3416
+ insertDeadEnd(
3417
+ db,
3418
+ exp.id,
3419
+ exp.hypothesis ?? exp.slug,
3420
+ whyFailed,
3421
+ `Approach rejected: ${whyFailed}`,
3422
+ exp.sub_type,
3423
+ "structural"
3424
+ );
3425
+ updateExperimentStatus(db, exp.id, "dead_end");
3426
+ if (exp.sub_type) {
3427
+ incrementSubTypeFailure(db, exp.sub_type, exp.id, "rejected");
3428
+ }
3429
+ })();
3430
+ info(`Experiment ${exp.slug} DEAD-ENDED (rejected). Constraint recorded.`);
3431
+ break;
3432
+ }
3433
+ }
3434
+ return overallGrade;
3435
+ }
3104
3436
  function gitMerge(branch, cwd) {
3105
3437
  try {
3106
3438
  (0, import_node_child_process3.execSync)(`git merge ${branch} --no-ff -m "Merge experiment branch ${branch}"`, {
@@ -3163,7 +3495,9 @@ var init_resolve = __esm({
3163
3495
  var cycle_exports = {};
3164
3496
  __export(cycle_exports, {
3165
3497
  cycle: () => cycle,
3166
- resolveCmd: () => resolveCmd
3498
+ resolveCmd: () => resolveCmd,
3499
+ runResolve: () => runResolve,
3500
+ runStep: () => runStep
3167
3501
  });
3168
3502
  async function cycle(step, args) {
3169
3503
  const root = findProjectRoot();
@@ -3195,6 +3529,28 @@ async function resolveCmd(args) {
3195
3529
  transition(exp.status, "resolved" /* RESOLVED */);
3196
3530
  await resolve(db, exp, root);
3197
3531
  }
3532
+ async function runStep(step, db, exp, root) {
3533
+ switch (step) {
3534
+ case "build":
3535
+ return doBuild(db, exp, root);
3536
+ case "challenge":
3537
+ return doChallenge(db, exp, root);
3538
+ case "doubt":
3539
+ return doDoubt(db, exp, root);
3540
+ case "scout":
3541
+ return doScout(db, exp, root);
3542
+ case "verify":
3543
+ return doVerify(db, exp, root);
3544
+ case "gate":
3545
+ return doGate(db, exp, root);
3546
+ case "compress":
3547
+ return doCompress(db, root);
3548
+ }
3549
+ }
3550
+ async function runResolve(db, exp, root) {
3551
+ transition(exp.status, "resolved" /* RESOLVED */);
3552
+ await resolve(db, exp, root);
3553
+ }
3198
3554
  async function doGate(db, exp, root) {
3199
3555
  transition(exp.status, "gated" /* GATED */);
3200
3556
  const synthesis = truncateContext(readFileOrEmpty(path8.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
@@ -4005,13 +4361,13 @@ async function run(args) {
4005
4361
  const db = getDb(root);
4006
4362
  const config = loadConfig(root);
4007
4363
  const MAX_EXPERIMENTS = 10;
4008
- const MAX_STEPS = 200;
4364
+ const MAX_STEPS2 = 200;
4009
4365
  let experimentCount = 0;
4010
4366
  let stepCount = 0;
4011
4367
  let consecutiveFailures = 0;
4012
4368
  const usedHypotheses = /* @__PURE__ */ new Set();
4013
4369
  header(`Autonomous Mode \u2014 ${goal}`);
4014
- while (stepCount < MAX_STEPS && experimentCount < MAX_EXPERIMENTS) {
4370
+ while (stepCount < MAX_STEPS2 && experimentCount < MAX_EXPERIMENTS) {
4015
4371
  if (isShutdownRequested()) {
4016
4372
  warn("Shutdown requested. Stopping autonomous mode.");
4017
4373
  break;
@@ -4081,8 +4437,8 @@ async function run(args) {
4081
4437
  }
4082
4438
  }
4083
4439
  }
4084
- if (stepCount >= MAX_STEPS) {
4085
- warn(`Reached max steps (${MAX_STEPS}). Stopping autonomous mode.`);
4440
+ if (stepCount >= MAX_STEPS2) {
4441
+ warn(`Reached max steps (${MAX_STEPS2}). Stopping autonomous mode.`);
4086
4442
  }
4087
4443
  header("Autonomous Mode Complete");
4088
4444
  info(`Goal: ${goal}`);
@@ -4234,11 +4590,768 @@ var init_run = __esm({
4234
4590
  }
4235
4591
  });
4236
4592
 
4593
+ // src/swarm/worktree.ts
4594
+ function createWorktree(mainRoot, slug, paddedNum) {
4595
+ const projectName = path12.basename(mainRoot);
4596
+ const worktreeName = `${projectName}-swarm-${paddedNum}-${slug}`;
4597
+ const worktreePath = path12.join(path12.dirname(mainRoot), worktreeName);
4598
+ const branch = `swarm/${paddedNum}-${slug}`;
4599
+ (0, import_node_child_process6.execSync)(`git worktree add ${JSON.stringify(worktreePath)} -b ${branch}`, {
4600
+ cwd: mainRoot,
4601
+ encoding: "utf-8",
4602
+ stdio: ["pipe", "pipe", "pipe"]
4603
+ });
4604
+ return {
4605
+ path: worktreePath,
4606
+ branch,
4607
+ slug,
4608
+ hypothesis: "",
4609
+ // filled in by caller
4610
+ paddedNum
4611
+ };
4612
+ }
4613
+ function initializeWorktree(mainRoot, worktreePath) {
4614
+ const majlisDir = path12.join(worktreePath, ".majlis");
4615
+ fs12.mkdirSync(majlisDir, { recursive: true });
4616
+ const configSrc = path12.join(mainRoot, ".majlis", "config.json");
4617
+ if (fs12.existsSync(configSrc)) {
4618
+ fs12.copyFileSync(configSrc, path12.join(majlisDir, "config.json"));
4619
+ }
4620
+ const agentsSrc = path12.join(mainRoot, ".majlis", "agents");
4621
+ if (fs12.existsSync(agentsSrc)) {
4622
+ const agentsDst = path12.join(majlisDir, "agents");
4623
+ fs12.mkdirSync(agentsDst, { recursive: true });
4624
+ for (const file of fs12.readdirSync(agentsSrc)) {
4625
+ fs12.copyFileSync(path12.join(agentsSrc, file), path12.join(agentsDst, file));
4626
+ }
4627
+ }
4628
+ const synthSrc = path12.join(mainRoot, "docs", "synthesis");
4629
+ if (fs12.existsSync(synthSrc)) {
4630
+ const synthDst = path12.join(worktreePath, "docs", "synthesis");
4631
+ fs12.mkdirSync(synthDst, { recursive: true });
4632
+ for (const file of fs12.readdirSync(synthSrc)) {
4633
+ const srcFile = path12.join(synthSrc, file);
4634
+ if (fs12.statSync(srcFile).isFile()) {
4635
+ fs12.copyFileSync(srcFile, path12.join(synthDst, file));
4636
+ }
4637
+ }
4638
+ }
4639
+ const templateSrc = path12.join(mainRoot, "docs", "experiments", "_TEMPLATE.md");
4640
+ if (fs12.existsSync(templateSrc)) {
4641
+ const expDir = path12.join(worktreePath, "docs", "experiments");
4642
+ fs12.mkdirSync(expDir, { recursive: true });
4643
+ fs12.copyFileSync(templateSrc, path12.join(expDir, "_TEMPLATE.md"));
4644
+ }
4645
+ const db = openDbAt(worktreePath);
4646
+ db.close();
4647
+ }
4648
+ function cleanupWorktree(mainRoot, wt) {
4649
+ try {
4650
+ (0, import_node_child_process6.execSync)(`git worktree remove ${JSON.stringify(wt.path)} --force`, {
4651
+ cwd: mainRoot,
4652
+ encoding: "utf-8",
4653
+ stdio: ["pipe", "pipe", "pipe"]
4654
+ });
4655
+ } catch {
4656
+ warn(`Could not remove worktree ${wt.path} \u2014 remove manually.`);
4657
+ }
4658
+ try {
4659
+ (0, import_node_child_process6.execSync)(`git branch -D ${wt.branch}`, {
4660
+ cwd: mainRoot,
4661
+ encoding: "utf-8",
4662
+ stdio: ["pipe", "pipe", "pipe"]
4663
+ });
4664
+ } catch {
4665
+ }
4666
+ try {
4667
+ (0, import_node_child_process6.execSync)("git worktree prune", {
4668
+ cwd: mainRoot,
4669
+ encoding: "utf-8",
4670
+ stdio: ["pipe", "pipe", "pipe"]
4671
+ });
4672
+ } catch {
4673
+ }
4674
+ }
4675
+ var fs12, path12, import_node_child_process6;
4676
+ var init_worktree = __esm({
4677
+ "src/swarm/worktree.ts"() {
4678
+ "use strict";
4679
+ fs12 = __toESM(require("fs"));
4680
+ path12 = __toESM(require("path"));
4681
+ import_node_child_process6 = require("child_process");
4682
+ init_connection();
4683
+ init_format();
4684
+ }
4685
+ });
4686
+
4687
+ // src/swarm/runner.ts
4688
+ async function runExperimentInWorktree(wt) {
4689
+ const label = `[swarm:${wt.paddedNum}]`;
4690
+ let db;
4691
+ let exp = null;
4692
+ let overallGrade = null;
4693
+ let stepCount = 0;
4694
+ try {
4695
+ db = openDbAt(wt.path);
4696
+ exp = createExperiment(db, wt.slug, wt.branch, wt.hypothesis, null, null);
4697
+ updateExperimentStatus(db, exp.id, "reframed");
4698
+ exp.status = "reframed";
4699
+ const templatePath = path13.join(wt.path, "docs", "experiments", "_TEMPLATE.md");
4700
+ if (fs13.existsSync(templatePath)) {
4701
+ const template = fs13.readFileSync(templatePath, "utf-8");
4702
+ 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]);
4703
+ const logPath = path13.join(wt.path, "docs", "experiments", `${wt.paddedNum}-${wt.slug}.md`);
4704
+ fs13.writeFileSync(logPath, logContent);
4705
+ }
4706
+ info(`${label} Starting: ${wt.hypothesis}`);
4707
+ while (stepCount < MAX_STEPS) {
4708
+ if (isShutdownRequested()) {
4709
+ warn(`${label} Shutdown requested. Stopping.`);
4710
+ break;
4711
+ }
4712
+ stepCount++;
4713
+ const fresh = getExperimentBySlug(db, wt.slug);
4714
+ if (!fresh) break;
4715
+ exp = fresh;
4716
+ if (isTerminal(exp.status)) {
4717
+ success(`${label} Reached terminal: ${exp.status}`);
4718
+ break;
4719
+ }
4720
+ const valid = validNext(exp.status);
4721
+ if (valid.length === 0) break;
4722
+ const nextStep = determineNextStep(
4723
+ exp,
4724
+ valid,
4725
+ hasDoubts(db, exp.id),
4726
+ hasChallenges(db, exp.id)
4727
+ );
4728
+ info(`${label} [${stepCount}/${MAX_STEPS}] ${exp.status} -> ${nextStep}`);
4729
+ if (nextStep === "resolved" /* RESOLVED */) {
4730
+ overallGrade = await resolveDbOnly(db, exp, wt.path);
4731
+ continue;
4732
+ }
4733
+ if (nextStep === "compressed" /* COMPRESSED */) {
4734
+ await runStep("compress", db, exp, wt.path);
4735
+ updateExperimentStatus(db, exp.id, "compressed");
4736
+ continue;
4737
+ }
4738
+ if (nextStep === "merged" /* MERGED */) {
4739
+ updateExperimentStatus(db, exp.id, "merged");
4740
+ success(`${label} Merged.`);
4741
+ break;
4742
+ }
4743
+ if (nextStep === "reframed" /* REFRAMED */) {
4744
+ updateExperimentStatus(db, exp.id, "reframed");
4745
+ continue;
4746
+ }
4747
+ const stepName = statusToStepName(nextStep);
4748
+ if (!stepName) {
4749
+ warn(`${label} Unknown step: ${nextStep}`);
4750
+ break;
4751
+ }
4752
+ try {
4753
+ await runStep(stepName, db, exp, wt.path);
4754
+ } catch (err) {
4755
+ const message = err instanceof Error ? err.message : String(err);
4756
+ warn(`${label} Step failed: ${message}`);
4757
+ try {
4758
+ insertDeadEnd(
4759
+ db,
4760
+ exp.id,
4761
+ exp.hypothesis ?? exp.slug,
4762
+ message,
4763
+ `Process failure: ${message}`,
4764
+ exp.sub_type,
4765
+ "procedural"
4766
+ );
4767
+ updateExperimentStatus(db, exp.id, "dead_end");
4768
+ } catch {
4769
+ }
4770
+ break;
4771
+ }
4772
+ }
4773
+ if (stepCount >= MAX_STEPS) {
4774
+ warn(`${label} Hit max steps (${MAX_STEPS}).`);
4775
+ }
4776
+ const finalExp = getExperimentBySlug(db, wt.slug);
4777
+ if (finalExp) exp = finalExp;
4778
+ const finalStatus = exp?.status ?? "error";
4779
+ return {
4780
+ worktree: wt,
4781
+ experiment: exp,
4782
+ finalStatus,
4783
+ overallGrade,
4784
+ costUsd: 0,
4785
+ // TODO: track via SDK when available
4786
+ stepCount
4787
+ };
4788
+ } catch (err) {
4789
+ const message = err instanceof Error ? err.message : String(err);
4790
+ warn(`${label} Fatal error: ${message}`);
4791
+ return {
4792
+ worktree: wt,
4793
+ experiment: exp,
4794
+ finalStatus: "error",
4795
+ overallGrade: null,
4796
+ costUsd: 0,
4797
+ stepCount,
4798
+ error: message
4799
+ };
4800
+ } finally {
4801
+ if (db) {
4802
+ try {
4803
+ db.close();
4804
+ } catch {
4805
+ }
4806
+ }
4807
+ }
4808
+ }
4809
+ function statusToStepName(status2) {
4810
+ switch (status2) {
4811
+ case "gated" /* GATED */:
4812
+ return "gate";
4813
+ case "building" /* BUILDING */:
4814
+ return "build";
4815
+ case "challenged" /* CHALLENGED */:
4816
+ return "challenge";
4817
+ case "doubted" /* DOUBTED */:
4818
+ return "doubt";
4819
+ case "scouted" /* SCOUTED */:
4820
+ return "scout";
4821
+ case "verifying" /* VERIFYING */:
4822
+ return "verify";
4823
+ default:
4824
+ return null;
4825
+ }
4826
+ }
4827
+ var fs13, path13, MAX_STEPS;
4828
+ var init_runner = __esm({
4829
+ "src/swarm/runner.ts"() {
4830
+ "use strict";
4831
+ fs13 = __toESM(require("fs"));
4832
+ path13 = __toESM(require("path"));
4833
+ init_connection();
4834
+ init_queries();
4835
+ init_machine();
4836
+ init_types2();
4837
+ init_cycle();
4838
+ init_resolve();
4839
+ init_shutdown();
4840
+ init_format();
4841
+ MAX_STEPS = 20;
4842
+ }
4843
+ });
4844
+
4845
+ // src/swarm/aggregate.ts
4846
+ function importExperimentFromWorktree(sourceDb, targetDb, slug) {
4847
+ const sourceExp = sourceDb.prepare(
4848
+ "SELECT * FROM experiments WHERE slug = ?"
4849
+ ).get(slug);
4850
+ if (!sourceExp) {
4851
+ throw new Error(`Experiment ${slug} not found in source DB`);
4852
+ }
4853
+ const sourceId = sourceExp.id;
4854
+ const insertExp = targetDb.prepare(`
4855
+ INSERT INTO experiments (slug, branch, status, classification_ref, sub_type,
4856
+ hypothesis, builder_guidance, created_at, updated_at)
4857
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
4858
+ `);
4859
+ const result = insertExp.run(
4860
+ sourceExp.slug,
4861
+ sourceExp.branch,
4862
+ sourceExp.status,
4863
+ sourceExp.classification_ref,
4864
+ sourceExp.sub_type,
4865
+ sourceExp.hypothesis,
4866
+ sourceExp.builder_guidance,
4867
+ sourceExp.created_at,
4868
+ sourceExp.updated_at
4869
+ );
4870
+ const targetId = result.lastInsertRowid;
4871
+ for (const table2 of CHILD_TABLES) {
4872
+ importChildTable(sourceDb, targetDb, table2, sourceId, targetId);
4873
+ }
4874
+ const stfRows = sourceDb.prepare(
4875
+ "SELECT * FROM sub_type_failures WHERE experiment_id = ?"
4876
+ ).all(sourceId);
4877
+ for (const row of stfRows) {
4878
+ targetDb.prepare(`
4879
+ INSERT INTO sub_type_failures (sub_type, experiment_id, grade, created_at)
4880
+ VALUES (?, ?, ?, ?)
4881
+ `).run(row.sub_type, targetId, row.grade, row.created_at);
4882
+ }
4883
+ return targetId;
4884
+ }
4885
+ function importChildTable(sourceDb, targetDb, table2, sourceExpId, targetExpId) {
4886
+ const rows = sourceDb.prepare(
4887
+ `SELECT * FROM ${table2} WHERE experiment_id = ?`
4888
+ ).all(sourceExpId);
4889
+ if (rows.length === 0) return;
4890
+ const cols = Object.keys(rows[0]).filter((c) => c !== "id");
4891
+ const placeholders = cols.map(() => "?").join(", ");
4892
+ const insert = targetDb.prepare(
4893
+ `INSERT INTO ${table2} (${cols.join(", ")}) VALUES (${placeholders})`
4894
+ );
4895
+ for (const row of rows) {
4896
+ const values = cols.map(
4897
+ (c) => c === "experiment_id" ? targetExpId : row[c]
4898
+ );
4899
+ insert.run(...values);
4900
+ }
4901
+ }
4902
+ function aggregateSwarmResults(mainRoot, mainDb, results) {
4903
+ let mergedCount = 0;
4904
+ let deadEndCount = 0;
4905
+ let errorCount = 0;
4906
+ let totalCostUsd = 0;
4907
+ for (const r of results) {
4908
+ totalCostUsd += r.costUsd;
4909
+ if (r.error || !r.experiment) {
4910
+ errorCount++;
4911
+ continue;
4912
+ }
4913
+ try {
4914
+ const sourceDb = openDbAt(r.worktree.path);
4915
+ mainDb.transaction(() => {
4916
+ importExperimentFromWorktree(sourceDb, mainDb, r.worktree.slug);
4917
+ })();
4918
+ sourceDb.close();
4919
+ if (r.finalStatus === "merged") mergedCount++;
4920
+ else if (r.finalStatus === "dead_end") deadEndCount++;
4921
+ } catch (err) {
4922
+ const msg = err instanceof Error ? err.message : String(err);
4923
+ warn(`Failed to import ${r.worktree.slug}: ${msg}`);
4924
+ errorCount++;
4925
+ }
4926
+ }
4927
+ const ranked = results.filter((r) => r.overallGrade && !r.error).sort((a, b) => {
4928
+ const aRank = GRADE_RANK[a.overallGrade] ?? 99;
4929
+ const bRank = GRADE_RANK[b.overallGrade] ?? 99;
4930
+ return aRank - bRank;
4931
+ });
4932
+ const best = ranked.length > 0 ? ranked[0] : null;
4933
+ return {
4934
+ goal: "",
4935
+ // filled by caller
4936
+ parallelCount: results.length,
4937
+ results,
4938
+ bestExperiment: best,
4939
+ totalCostUsd,
4940
+ mergedCount,
4941
+ deadEndCount,
4942
+ errorCount
4943
+ };
4944
+ }
4945
+ var CHILD_TABLES, GRADE_RANK;
4946
+ var init_aggregate = __esm({
4947
+ "src/swarm/aggregate.ts"() {
4948
+ "use strict";
4949
+ init_connection();
4950
+ init_format();
4951
+ CHILD_TABLES = [
4952
+ "decisions",
4953
+ "doubts",
4954
+ "challenges",
4955
+ "verifications",
4956
+ "metrics",
4957
+ "dead_ends",
4958
+ "reframes",
4959
+ "findings"
4960
+ ];
4961
+ GRADE_RANK = {
4962
+ sound: 0,
4963
+ good: 1,
4964
+ weak: 2,
4965
+ rejected: 3
4966
+ };
4967
+ }
4968
+ });
4969
+
4970
+ // src/commands/swarm.ts
4971
+ var swarm_exports = {};
4972
+ __export(swarm_exports, {
4973
+ swarm: () => swarm
4974
+ });
4975
+ async function swarm(args) {
4976
+ const root = findProjectRoot();
4977
+ if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
4978
+ const goal = args.filter((a) => !a.startsWith("--")).join(" ");
4979
+ if (!goal) throw new Error('Usage: majlis swarm "goal description" [--parallel N]');
4980
+ const parallelStr = getFlagValue(args, "--parallel");
4981
+ const parallelCount = Math.min(
4982
+ Math.max(2, parseInt(parallelStr ?? String(DEFAULT_PARALLEL), 10) || DEFAULT_PARALLEL),
4983
+ MAX_PARALLEL
4984
+ );
4985
+ try {
4986
+ const status2 = (0, import_node_child_process7.execSync)("git status --porcelain", {
4987
+ cwd: root,
4988
+ encoding: "utf-8",
4989
+ stdio: ["pipe", "pipe", "pipe"]
4990
+ }).trim();
4991
+ if (status2) {
4992
+ warn("Working tree has uncommitted changes. Commit or stash before swarming.");
4993
+ throw new Error("Dirty working tree. Commit or stash first.");
4994
+ }
4995
+ } catch (err) {
4996
+ if (err instanceof Error && err.message.includes("Dirty working tree")) throw err;
4997
+ warn("Could not check git status.");
4998
+ }
4999
+ const db = getDb(root);
5000
+ const swarmRun = createSwarmRun(db, goal, parallelCount);
5001
+ header(`Swarm Mode \u2014 ${goal}`);
5002
+ info(`Generating ${parallelCount} diverse hypotheses...`);
5003
+ const hypotheses = await deriveMultipleHypotheses(goal, root, parallelCount);
5004
+ if (hypotheses.length === 0) {
5005
+ success("Planner says the goal has been met. Nothing to swarm.");
5006
+ updateSwarmRun(db, swarmRun.id, "completed", 0, null);
5007
+ return;
5008
+ }
5009
+ info(`Got ${hypotheses.length} hypotheses:`);
5010
+ for (let i = 0; i < hypotheses.length; i++) {
5011
+ info(` ${i + 1}. ${hypotheses[i]}`);
5012
+ }
5013
+ const worktrees = [];
5014
+ for (let i = 0; i < hypotheses.length; i++) {
5015
+ const paddedNum = String(i + 1).padStart(3, "0");
5016
+ const slug = await generateSlug(hypotheses[i], root);
5017
+ try {
5018
+ const wt = createWorktree(root, slug, paddedNum);
5019
+ wt.hypothesis = hypotheses[i];
5020
+ initializeWorktree(root, wt.path);
5021
+ worktrees.push(wt);
5022
+ addSwarmMember(db, swarmRun.id, slug, wt.path);
5023
+ info(`Created worktree ${paddedNum}: ${slug}`);
5024
+ } catch (err) {
5025
+ const msg = err instanceof Error ? err.message : String(err);
5026
+ warn(`Failed to create worktree for hypothesis ${i + 1}: ${msg}`);
5027
+ }
5028
+ }
5029
+ if (worktrees.length === 0) {
5030
+ warn("No worktrees created. Aborting swarm.");
5031
+ updateSwarmRun(db, swarmRun.id, "failed", 0, null);
5032
+ return;
5033
+ }
5034
+ info(`Running ${worktrees.length} experiments in parallel...`);
5035
+ info("");
5036
+ const settled = await Promise.allSettled(
5037
+ worktrees.map((wt) => runExperimentInWorktree(wt))
5038
+ );
5039
+ const results = settled.map((s, i) => {
5040
+ if (s.status === "fulfilled") return s.value;
5041
+ return {
5042
+ worktree: worktrees[i],
5043
+ experiment: null,
5044
+ finalStatus: "error",
5045
+ overallGrade: null,
5046
+ costUsd: 0,
5047
+ stepCount: 0,
5048
+ error: s.reason instanceof Error ? s.reason.message : String(s.reason)
5049
+ };
5050
+ });
5051
+ for (const r of results) {
5052
+ updateSwarmMember(
5053
+ db,
5054
+ swarmRun.id,
5055
+ r.worktree.slug,
5056
+ r.finalStatus,
5057
+ r.overallGrade,
5058
+ r.costUsd,
5059
+ r.error ?? null
5060
+ );
5061
+ }
5062
+ info("");
5063
+ header("Aggregation");
5064
+ const summary = aggregateSwarmResults(root, db, results);
5065
+ summary.goal = goal;
5066
+ if (summary.bestExperiment && isMergeable(summary.bestExperiment.overallGrade)) {
5067
+ const best = summary.bestExperiment;
5068
+ info(`Best experiment: ${best.worktree.slug} (${best.overallGrade})`);
5069
+ try {
5070
+ (0, import_node_child_process7.execSync)(
5071
+ `git merge ${best.worktree.branch} --no-ff -m "Merge swarm winner: ${best.worktree.slug}"`,
5072
+ { cwd: root, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
5073
+ );
5074
+ success(`Merged ${best.worktree.slug} into main.`);
5075
+ } catch {
5076
+ warn(`Git merge of ${best.worktree.slug} failed. Merge manually with:`);
5077
+ info(` git merge ${best.worktree.branch} --no-ff`);
5078
+ }
5079
+ } else {
5080
+ info("No experiment achieved sound/good grade. Nothing merged.");
5081
+ }
5082
+ for (const r of results) {
5083
+ if (r === summary.bestExperiment || r.error || !r.experiment) continue;
5084
+ const mainExp = getExperimentBySlug(db, r.worktree.slug);
5085
+ if (mainExp && mainExp.status !== "dead_end") {
5086
+ updateExperimentStatus(db, mainExp.id, "dead_end");
5087
+ }
5088
+ }
5089
+ updateSwarmRun(
5090
+ db,
5091
+ swarmRun.id,
5092
+ summary.errorCount === results.length ? "failed" : "completed",
5093
+ summary.totalCostUsd,
5094
+ summary.bestExperiment?.worktree.slug ?? null
5095
+ );
5096
+ info("Cleaning up worktrees...");
5097
+ for (const wt of worktrees) {
5098
+ cleanupWorktree(root, wt);
5099
+ }
5100
+ info("");
5101
+ header("Swarm Summary");
5102
+ info(`Goal: ${goal}`);
5103
+ info(`Parallel: ${worktrees.length}`);
5104
+ info(`Results:`);
5105
+ for (const r of results) {
5106
+ const grade = r.overallGrade ?? "n/a";
5107
+ const status2 = r.error ? `ERROR: ${r.error.slice(0, 60)}` : r.finalStatus;
5108
+ const marker = r === summary.bestExperiment ? " <-- BEST" : "";
5109
+ info(` ${r.worktree.paddedNum} ${r.worktree.slug}: ${grade} (${status2})${marker}`);
5110
+ }
5111
+ info(`Merged: ${summary.mergedCount} | Dead-ends: ${summary.deadEndCount} | Errors: ${summary.errorCount}`);
5112
+ }
5113
+ function isMergeable(grade) {
5114
+ return grade === "sound" || grade === "good";
5115
+ }
5116
+ async function deriveMultipleHypotheses(goal, root, count) {
5117
+ const synthesis = truncateContext(
5118
+ readFileOrEmpty(path14.join(root, "docs", "synthesis", "current.md")),
5119
+ CONTEXT_LIMITS.synthesis
5120
+ );
5121
+ const fragility = truncateContext(
5122
+ readFileOrEmpty(path14.join(root, "docs", "synthesis", "fragility.md")),
5123
+ CONTEXT_LIMITS.fragility
5124
+ );
5125
+ const deadEndsDoc = truncateContext(
5126
+ readFileOrEmpty(path14.join(root, "docs", "synthesis", "dead-ends.md")),
5127
+ CONTEXT_LIMITS.deadEnds
5128
+ );
5129
+ const db = getDb(root);
5130
+ const deadEnds = listAllDeadEnds(db);
5131
+ const config = loadConfig(root);
5132
+ let metricsOutput = "";
5133
+ if (config.metrics?.command) {
5134
+ try {
5135
+ metricsOutput = (0, import_node_child_process7.execSync)(config.metrics.command, {
5136
+ cwd: root,
5137
+ encoding: "utf-8",
5138
+ timeout: 6e4,
5139
+ stdio: ["pipe", "pipe", "pipe"]
5140
+ }).trim();
5141
+ } catch {
5142
+ metricsOutput = "(metrics command failed)";
5143
+ }
5144
+ }
5145
+ const result = await spawnSynthesiser({
5146
+ taskPrompt: `You are the Planner for a parallel Majlis swarm.
5147
+
5148
+ ## Goal
5149
+ ${goal}
5150
+
5151
+ ## Current Metrics
5152
+ ${metricsOutput || "(no metrics configured)"}
5153
+
5154
+ ## Synthesis (what we know so far)
5155
+ ${synthesis || "(empty \u2014 first experiment)"}
5156
+
5157
+ ## Fragility Map (known weak areas)
5158
+ ${fragility || "(none)"}
5159
+
5160
+ ## Dead-End Registry
5161
+ ${deadEndsDoc || "(none)"}
5162
+
5163
+ ## Dead Ends (from DB \u2014 ${deadEnds.length} total)
5164
+ ${deadEnds.map((d) => `- [${d.category ?? "structural"}] ${d.approach}: ${d.why_failed} [constraint: ${d.structural_constraint}]`).join("\n") || "(none)"}
5165
+
5166
+ Note: [structural] dead ends are HARD CONSTRAINTS \u2014 hypotheses MUST NOT repeat these approaches.
5167
+ [procedural] dead ends are process failures \u2014 the approach may still be valid if executed differently.
5168
+
5169
+ ## Your Task
5170
+ 1. Assess: based on the metrics and synthesis, has the goal been met? Be specific.
5171
+ 2. If YES \u2014 output the JSON block below with goal_met: true.
5172
+ 3. If NO \u2014 generate exactly ${count} DIVERSE hypotheses for parallel testing.
5173
+
5174
+ Requirements for hypotheses:
5175
+ - Each must attack the problem from a DIFFERENT angle
5176
+ - They must NOT share the same mechanism, function target, or strategy
5177
+ - At least one should be an unconventional or indirect approach
5178
+ - None may repeat a dead-ended structural approach
5179
+ - Each must be specific and actionable \u2014 name the function or mechanism to change
5180
+ - Do NOT reference specific line numbers \u2014 they shift between experiments
5181
+
5182
+ CRITICAL: Your LAST line of output MUST be EXACTLY this format (on its own line, nothing after it):
5183
+ <!-- majlis-json {"goal_met": false, "hypotheses": ["hypothesis 1", "hypothesis 2", "hypothesis 3"]} -->
5184
+
5185
+ If the goal is met:
5186
+ <!-- majlis-json {"goal_met": true, "hypotheses": []} -->`
5187
+ }, root);
5188
+ if (result.structured?.goal_met === true) return [];
5189
+ if (result.structured?.hypotheses && Array.isArray(result.structured.hypotheses)) {
5190
+ return result.structured.hypotheses.filter(
5191
+ (h) => typeof h === "string" && h.length > 10
5192
+ );
5193
+ }
5194
+ const blockMatch = result.output.match(/<!--\s*majlis-json\s*(\{[\s\S]*?\})\s*-->/);
5195
+ if (blockMatch) {
5196
+ try {
5197
+ const parsed = JSON.parse(blockMatch[1]);
5198
+ if (parsed.goal_met === true) return [];
5199
+ if (Array.isArray(parsed.hypotheses)) {
5200
+ return parsed.hypotheses.filter(
5201
+ (h) => typeof h === "string" && h.length > 10
5202
+ );
5203
+ }
5204
+ } catch {
5205
+ }
5206
+ }
5207
+ warn("Planner did not return structured hypotheses. Using goal as single hypothesis.");
5208
+ return [goal];
5209
+ }
5210
+ var path14, import_node_child_process7, MAX_PARALLEL, DEFAULT_PARALLEL;
5211
+ var init_swarm = __esm({
5212
+ "src/commands/swarm.ts"() {
5213
+ "use strict";
5214
+ path14 = __toESM(require("path"));
5215
+ import_node_child_process7 = require("child_process");
5216
+ init_connection();
5217
+ init_queries();
5218
+ init_spawn();
5219
+ init_config();
5220
+ init_worktree();
5221
+ init_runner();
5222
+ init_aggregate();
5223
+ init_format();
5224
+ MAX_PARALLEL = 8;
5225
+ DEFAULT_PARALLEL = 3;
5226
+ }
5227
+ });
5228
+
5229
+ // src/commands/diagnose.ts
5230
+ var diagnose_exports = {};
5231
+ __export(diagnose_exports, {
5232
+ diagnose: () => diagnose
5233
+ });
5234
+ async function diagnose(args) {
5235
+ const root = findProjectRoot();
5236
+ if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
5237
+ const db = getDb(root);
5238
+ const focus = args.filter((a) => !a.startsWith("--")).join(" ");
5239
+ const keepScripts = args.includes("--keep-scripts");
5240
+ const scriptsDir = path15.join(root, ".majlis", "scripts");
5241
+ if (!fs14.existsSync(scriptsDir)) {
5242
+ fs14.mkdirSync(scriptsDir, { recursive: true });
5243
+ }
5244
+ header("Deep Diagnosis");
5245
+ if (focus) info(`Focus: ${focus}`);
5246
+ const dbExport = exportForDiagnostician(db);
5247
+ const synthesis = readFileOrEmpty(path15.join(root, "docs", "synthesis", "current.md"));
5248
+ const fragility = readFileOrEmpty(path15.join(root, "docs", "synthesis", "fragility.md"));
5249
+ const deadEndsDoc = readFileOrEmpty(path15.join(root, "docs", "synthesis", "dead-ends.md"));
5250
+ const config = loadConfig(root);
5251
+ let metricsOutput = "";
5252
+ if (config.metrics?.command) {
5253
+ try {
5254
+ metricsOutput = (0, import_node_child_process8.execSync)(config.metrics.command, {
5255
+ cwd: root,
5256
+ encoding: "utf-8",
5257
+ timeout: 6e4,
5258
+ stdio: ["pipe", "pipe", "pipe"]
5259
+ }).trim();
5260
+ } catch {
5261
+ metricsOutput = "(metrics command failed)";
5262
+ }
5263
+ }
5264
+ let taskPrompt = `## Full Database Export (CANONICAL \u2014 source of truth)
5265
+ ${dbExport}
5266
+
5267
+ `;
5268
+ taskPrompt += `## Current Synthesis
5269
+ ${synthesis || "(empty \u2014 no experiments yet)"}
5270
+
5271
+ `;
5272
+ taskPrompt += `## Fragility Map
5273
+ ${fragility || "(none)"}
5274
+
5275
+ `;
5276
+ taskPrompt += `## Dead-End Registry
5277
+ ${deadEndsDoc || "(none)"}
5278
+
5279
+ `;
5280
+ taskPrompt += `## Current Metrics
5281
+ ${metricsOutput || "(no metrics configured)"}
5282
+
5283
+ `;
5284
+ taskPrompt += `## Project Objective
5285
+ ${config.project?.objective || "(not specified)"}
5286
+
5287
+ `;
5288
+ if (focus) {
5289
+ taskPrompt += `## Focus Area
5290
+ The user has asked you to focus your diagnosis on: ${focus}
5291
+
5292
+ `;
5293
+ }
5294
+ taskPrompt += `## Your Task
5295
+ 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.
5296
+
5297
+ Remember: you may write files ONLY to .majlis/scripts/. You cannot modify project code.`;
5298
+ info("Spawning diagnostician (60 turns, full DB access)...");
5299
+ const result = await spawnAgent("diagnostician", { taskPrompt }, root);
5300
+ const diagnosisDir = path15.join(root, "docs", "diagnosis");
5301
+ if (!fs14.existsSync(diagnosisDir)) {
5302
+ fs14.mkdirSync(diagnosisDir, { recursive: true });
5303
+ }
5304
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
5305
+ const artifactPath = path15.join(diagnosisDir, `diagnosis-${timestamp}.md`);
5306
+ fs14.writeFileSync(artifactPath, result.output);
5307
+ info(`Diagnostic report: docs/diagnosis/diagnosis-${timestamp}.md`);
5308
+ if (result.structured?.diagnosis) {
5309
+ const d = result.structured.diagnosis;
5310
+ if (d.root_causes?.length) {
5311
+ info(`Root causes identified: ${d.root_causes.length}`);
5312
+ }
5313
+ if (d.investigation_directions?.length) {
5314
+ info(`Investigation directions: ${d.investigation_directions.length}`);
5315
+ }
5316
+ }
5317
+ if (!keepScripts) {
5318
+ try {
5319
+ const files = fs14.readdirSync(scriptsDir);
5320
+ for (const f of files) {
5321
+ fs14.unlinkSync(path15.join(scriptsDir, f));
5322
+ }
5323
+ fs14.rmdirSync(scriptsDir);
5324
+ info("Cleaned up .majlis/scripts/");
5325
+ } catch {
5326
+ }
5327
+ } else {
5328
+ info("Scripts preserved in .majlis/scripts/ (--keep-scripts)");
5329
+ }
5330
+ if (result.truncated) {
5331
+ warn("Diagnostician was truncated (hit 60 turn limit).");
5332
+ }
5333
+ success("Diagnosis complete.");
5334
+ }
5335
+ var fs14, path15, import_node_child_process8;
5336
+ var init_diagnose = __esm({
5337
+ "src/commands/diagnose.ts"() {
5338
+ "use strict";
5339
+ fs14 = __toESM(require("fs"));
5340
+ path15 = __toESM(require("path"));
5341
+ import_node_child_process8 = require("child_process");
5342
+ init_connection();
5343
+ init_queries();
5344
+ init_spawn();
5345
+ init_config();
5346
+ init_format();
5347
+ }
5348
+ });
5349
+
4237
5350
  // src/cli.ts
4238
- var fs12 = __toESM(require("fs"));
4239
- var path12 = __toESM(require("path"));
5351
+ var fs15 = __toESM(require("fs"));
5352
+ var path16 = __toESM(require("path"));
4240
5353
  var VERSION = JSON.parse(
4241
- fs12.readFileSync(path12.join(__dirname, "..", "package.json"), "utf-8")
5354
+ fs15.readFileSync(path16.join(__dirname, "..", "package.json"), "utf-8")
4242
5355
  ).version;
4243
5356
  async function main() {
4244
5357
  let sigintCount = 0;
@@ -4348,11 +5461,21 @@ async function main() {
4348
5461
  await run2(rest);
4349
5462
  break;
4350
5463
  }
5464
+ case "swarm": {
5465
+ const { swarm: swarm2 } = await Promise.resolve().then(() => (init_swarm(), swarm_exports));
5466
+ await swarm2(rest);
5467
+ break;
5468
+ }
4351
5469
  case "audit": {
4352
5470
  const { audit: audit2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
4353
5471
  await audit2(rest);
4354
5472
  break;
4355
5473
  }
5474
+ case "diagnose": {
5475
+ const { diagnose: diagnose2 } = await Promise.resolve().then(() => (init_diagnose(), diagnose_exports));
5476
+ await diagnose2(rest);
5477
+ break;
5478
+ }
4356
5479
  default:
4357
5480
  console.error(`Unknown command: ${command}`);
4358
5481
  printHelp();
@@ -4405,6 +5528,7 @@ Queries:
4405
5528
 
4406
5529
  Audit:
4407
5530
  audit "objective" Maqasid check \u2014 is the frame right?
5531
+ diagnose ["focus area"] Deep diagnosis \u2014 root causes, patterns, gaps
4408
5532
 
4409
5533
  Sessions:
4410
5534
  session start "intent" Declare session intent
@@ -4412,6 +5536,7 @@ Sessions:
4412
5536
 
4413
5537
  Orchestration:
4414
5538
  run "goal" Autonomous orchestration until goal met
5539
+ swarm "goal" [--parallel N] Run N experiments in parallel worktrees
4415
5540
 
4416
5541
  Flags:
4417
5542
  --json Output as JSON
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "majlis",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Multi-agent workflow CLI for structured doubt, independent verification, and compressed knowledge",
5
5
  "bin": {
6
6
  "majlis": "./dist/cli.js"