majlis 0.4.5 → 0.5.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 +855 -278
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -29,6 +29,27 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
29
29
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
30
  mod
31
31
  ));
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
+
34
+ // src/shutdown.ts
35
+ var shutdown_exports = {};
36
+ __export(shutdown_exports, {
37
+ isShutdownRequested: () => isShutdownRequested,
38
+ requestShutdown: () => requestShutdown
39
+ });
40
+ function requestShutdown() {
41
+ _requested = true;
42
+ }
43
+ function isShutdownRequested() {
44
+ return _requested;
45
+ }
46
+ var _requested;
47
+ var init_shutdown = __esm({
48
+ "src/shutdown.ts"() {
49
+ "use strict";
50
+ _requested = false;
51
+ }
52
+ });
32
53
 
33
54
  // src/db/migrations.ts
34
55
  function runMigrations(db) {
@@ -170,6 +191,34 @@ var init_migrations = __esm({
170
191
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
171
192
  );
172
193
  CREATE INDEX idx_challenges_experiment ON challenges(experiment_id);
194
+ `);
195
+ },
196
+ // Migration 004: v3 → v4 — Reframes, findings tables; dead-end classification
197
+ (db) => {
198
+ db.exec(`
199
+ CREATE TABLE reframes (
200
+ id INTEGER PRIMARY KEY,
201
+ experiment_id INTEGER REFERENCES experiments(id),
202
+ decomposition TEXT NOT NULL,
203
+ divergences TEXT NOT NULL,
204
+ recommendation TEXT NOT NULL,
205
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
206
+ );
207
+ CREATE INDEX idx_reframes_experiment ON reframes(experiment_id);
208
+
209
+ CREATE TABLE findings (
210
+ id INTEGER PRIMARY KEY,
211
+ experiment_id INTEGER REFERENCES experiments(id),
212
+ approach TEXT NOT NULL,
213
+ source TEXT NOT NULL,
214
+ relevance TEXT NOT NULL,
215
+ contradicts_current BOOLEAN NOT NULL DEFAULT 0,
216
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
217
+ );
218
+ CREATE INDEX idx_findings_experiment ON findings(experiment_id);
219
+
220
+ ALTER TABLE dead_ends ADD COLUMN category TEXT DEFAULT 'structural'
221
+ CHECK(category IN ('structural', 'procedural'));
173
222
  `);
174
223
  }
175
224
  ];
@@ -552,11 +601,20 @@ and write up what you learned.
552
601
  - \`scripts/benchmark.py\` \u2014 the measurement tool. Never change how you're measured.
553
602
  - \`.majlis/\` \u2014 framework config. Not your concern.
554
603
 
604
+ ## Confirmed Doubts
605
+ If your context includes confirmedDoubts, these are weaknesses that the verifier has
606
+ confirmed from a previous cycle. You MUST address each one. Do not ignore them \u2014
607
+ the verifier will check again.
608
+
609
+ ## Metrics
610
+ The framework captures baseline and post-build metrics automatically. Do NOT claim
611
+ specific metric numbers unless quoting framework output. Do NOT run the benchmark
612
+ yourself unless instructed to. If you need to verify your change works, do a minimal
613
+ targeted test, not a full benchmark run.
614
+
555
615
  ## During building:
556
616
  - Tag EVERY decision: proof / test / strong-consensus / consensus / analogy / judgment
557
617
  - When making judgment-level decisions, state: "This is judgment \u2014 reasoning without precedent"
558
- - Run baseline metrics BEFORE making changes
559
- - Run comparison metrics AFTER making changes (once)
560
618
 
561
619
  You may NOT verify your own work or mark your own decisions as proven.
562
620
  Output your decisions in structured format so they can be recorded in the database.
@@ -579,8 +637,14 @@ tools: [Read, Glob, Grep]
579
637
  ---
580
638
  You are the Critic. You practise constructive doubt.
581
639
 
582
- You receive the builder's OUTPUT only \u2014 never its reasoning chain.
583
- Read the experiment log, related prior experiments, classification, and synthesis.
640
+ You receive:
641
+ - The builder's experiment document (the artifact, not the reasoning chain)
642
+ - The current synthesis (project state)
643
+ - Dead-ends (approaches that have been tried and failed)
644
+ - The hypothesis and experiment metadata
645
+
646
+ You do NOT see the builder's reasoning chain \u2014 only their documented output.
647
+ Use the experiment doc, synthesis, and dead-ends to find weaknesses.
584
648
 
585
649
  For each doubt:
586
650
  - What specific claim, decision, or assumption you doubt
@@ -611,6 +675,13 @@ tools: [Read, Glob, Grep]
611
675
  You are the Adversary. You do NOT review code for bugs.
612
676
  You reason about problem structure to CONSTRUCT pathological cases.
613
677
 
678
+ You receive:
679
+ - The git diff of the builder's code changes (the actual code, not prose)
680
+ - The current synthesis (project state)
681
+ - The hypothesis and experiment metadata
682
+
683
+ Study the CODE DIFF carefully \u2014 that is where the builder's assumptions are exposed.
684
+
614
685
  For each approach the builder takes, ask:
615
686
  - What input would make this fail?
616
687
  - What boundary condition was not tested?
@@ -637,6 +708,12 @@ tools: [Read, Glob, Grep, Bash]
637
708
  ---
638
709
  You are the Verifier. Perform dual verification:
639
710
 
711
+ You receive:
712
+ - All doubts with explicit DOUBT-{id} identifiers (use these in your doubt_resolutions)
713
+ - Challenge documents from the adversary
714
+ - Framework-captured metrics (baseline vs post-build) \u2014 this is GROUND TRUTH
715
+ - The hypothesis and experiment metadata
716
+
640
717
  ## Scope Constraint (CRITICAL)
641
718
 
642
719
  You must produce your structured output (grades + doubt resolutions) within your turn budget.
@@ -646,6 +723,11 @@ Reserve your final turns for writing the structured majlis-json output.
646
723
 
647
724
  The framework saves your output automatically. Do NOT attempt to write files.
648
725
 
726
+ ## Metrics (GROUND TRUTH)
727
+ If framework-captured metrics are in your context, these are the canonical before/after numbers.
728
+ Do NOT trust numbers claimed by the builder \u2014 compare against the framework metrics.
729
+ If the builder claims improvement but the framework metrics show regression, flag this.
730
+
649
731
  ## PROVENANCE CHECK:
650
732
  - Can every piece of code trace to an experiment or decision?
651
733
  - Is the chain unbroken from requirement -> classification -> experiment -> code?
@@ -660,13 +742,17 @@ Grade each component: sound / good / weak / rejected
660
742
  Grade each doubt/challenge: confirmed / dismissed (with evidence) / inconclusive
661
743
 
662
744
  ## Structured Output Format
745
+ IMPORTANT: For doubt_resolutions, use the DOUBT-{id} numbers from your context.
746
+ Example: if your context lists "DOUBT-7: [critical] The algorithm fails on X",
747
+ use doubt_id: 7 in your output.
748
+
663
749
  <!-- majlis-json
664
750
  {
665
751
  "grades": [
666
752
  { "component": "...", "grade": "sound|good|weak|rejected", "provenance_intact": true, "content_correct": true, "notes": "..." }
667
753
  ],
668
754
  "doubt_resolutions": [
669
- { "doubt_id": 0, "resolution": "confirmed|dismissed|inconclusive" }
755
+ { "doubt_id": 7, "resolution": "confirmed|dismissed|inconclusive" }
670
756
  ]
671
757
  }
672
758
  -->`,
@@ -692,7 +778,18 @@ Compare your decomposition with the existing classification.
692
778
  Flag structural divergences \u2014 these are the most valuable signals.
693
779
 
694
780
  Produce your reframe document as output. Do NOT attempt to write files.
695
- The framework saves your output automatically.`,
781
+ The framework saves your output automatically.
782
+
783
+ ## Structured Output Format
784
+ <!-- majlis-json
785
+ {
786
+ "reframe": {
787
+ "decomposition": "How you decomposed the problem",
788
+ "divergences": ["List of structural divergences from current classification"],
789
+ "recommendation": "What should change based on your independent analysis"
790
+ }
791
+ }
792
+ -->`,
696
793
  compressor: `---
697
794
  name: compressor
698
795
  model: opus
@@ -700,23 +797,36 @@ tools: [Read, Write, Edit, Glob, Grep]
700
797
  ---
701
798
  You are the Compressor. Hold the entire project in view and compress it.
702
799
 
703
- 1. Read ALL experiments, decisions, doubts, challenges, verification reports,
704
- reframes, and recent diffs.
705
- 2. Cross-reference: same question in different language? contradicting decisions?
800
+ Your taskPrompt includes a "Structured Data (CANONICAL)" section exported directly
801
+ from the SQLite database. This is the source of truth. docs/ files are agent artifacts
802
+ that may contain stale or incorrect information. Cross-reference everything against
803
+ the database export.
804
+
805
+ 1. Read the database export in your context FIRST \u2014 it has all experiments, decisions,
806
+ doubts (with resolutions), verifications (with grades), challenges, and dead-ends.
807
+ 2. Read docs/ files for narrative context, but trust the database when they conflict.
808
+ 3. Cross-reference: same question in different language? contradicting decisions?
706
809
  workaround masking root cause?
707
- 3. Update fragility map: thin coverage, weak components, untested judgment
810
+ 4. Update fragility map: thin coverage, weak components, untested judgment
708
811
  decisions, broken provenance.
709
- 4. Update dead-end registry: compress rejected experiments into structural constraints.
710
- 5. REWRITE synthesis \u2014 shorter and denser. If it's growing, you're accumulating,
711
- not compressing.
712
- 6. Review classification: new sub-types? resolved sub-types?
812
+ 5. Update dead-end registry: compress rejected experiments into structural constraints.
813
+ Mark each dead-end as [structural] or [procedural].
814
+ 6. REWRITE synthesis using the Write tool \u2014 shorter and denser. If it's growing,
815
+ you're accumulating, not compressing. You MUST use the Write tool to update
816
+ docs/synthesis/current.md, docs/synthesis/fragility.md, and docs/synthesis/dead-ends.md.
817
+ The framework does NOT auto-save your output for these files.
818
+ 7. Review classification: new sub-types? resolved sub-types?
713
819
 
714
820
  You may NOT write code, make decisions, or run experiments.
715
821
 
716
822
  ## Structured Output Format
717
823
  <!-- majlis-json
718
824
  {
719
- "guidance": "Summary of compression findings and updated state"
825
+ "compression_report": {
826
+ "synthesis_delta": "What changed in synthesis and why",
827
+ "new_dead_ends": ["List of newly identified dead-end constraints"],
828
+ "fragility_changes": ["List of changes to the fragility map"]
829
+ }
720
830
  }
721
831
  -->`,
722
832
  scout: `---
@@ -729,6 +839,11 @@ You are the Scout. You practise rihla \u2014 travel in search of knowledge.
729
839
  Your job is to search externally for alternative approaches, contradictory evidence,
730
840
  and perspectives from other fields that could inform the current experiment.
731
841
 
842
+ You receive:
843
+ - The current synthesis and fragility map
844
+ - Dead-ends (approaches that have been tried and failed) \u2014 search for alternatives that circumvent these
845
+ - The hypothesis and experiment metadata
846
+
732
847
  For the given experiment:
733
848
  1. Describe the problem in domain-neutral terms
734
849
  2. Search for alternative approaches in other fields or frameworks
@@ -739,13 +854,60 @@ For the given experiment:
739
854
  Rules:
740
855
  - Present findings neutrally. Report each approach on its own terms.
741
856
  - Note where external approaches contradict the current one \u2014 these are the most valuable signals.
857
+ - Focus on approaches that CIRCUMVENT known dead-ends \u2014 these are the most valuable.
742
858
  - You may NOT modify code or make decisions. Produce your rihla document as output only.
743
859
  - Do NOT attempt to write files. The framework saves your output automatically.
744
860
 
745
861
  ## Structured Output Format
746
862
  <!-- majlis-json
747
863
  {
748
- "decisions": []
864
+ "findings": [
865
+ { "approach": "Name of alternative approach", "source": "Where you found it", "relevance": "How it applies", "contradicts_current": true }
866
+ ]
867
+ }
868
+ -->`,
869
+ gatekeeper: `---
870
+ name: gatekeeper
871
+ model: sonnet
872
+ tools: [Read, Glob, Grep]
873
+ ---
874
+ You are the Gatekeeper. You check hypotheses before expensive build cycles.
875
+
876
+ Your job is a fast quality gate \u2014 prevent wasted Opus builds on hypotheses that
877
+ are stale, redundant with dead-ends, or too vague to produce a focused change.
878
+
879
+ ## Checks (in order)
880
+
881
+ ### 1. Stale References
882
+ Does the hypothesis reference specific functions, line numbers, or structures that
883
+ may not exist in the current code? Read the relevant files to verify.
884
+ - If references are stale, list them in stale_references.
885
+
886
+ ### 2. Dead-End Overlap
887
+ Does this hypothesis repeat an approach already ruled out by structural dead-ends?
888
+ Check each structural dead-end in your context \u2014 if the hypothesis matches the
889
+ approach or violates the structural_constraint, flag it.
890
+ - If overlapping, list the dead-end IDs in overlapping_dead_ends.
891
+
892
+ ### 3. Scope Check
893
+ Is this a single focused change? A good hypothesis names ONE function, mechanism,
894
+ or parameter to change. A bad hypothesis says "improve X and also Y and also Z."
895
+ - Flag if the hypothesis tries to do multiple things.
896
+
897
+ ## Output
898
+
899
+ gate_decision:
900
+ - **approve** \u2014 all checks pass, proceed to build
901
+ - **flag** \u2014 concerns found but not blocking (warnings only)
902
+ - **reject** \u2014 hypothesis must be revised (stale refs, dead-end repeat, or too vague)
903
+
904
+ ## Structured Output Format
905
+ <!-- majlis-json
906
+ {
907
+ "gate_decision": "approve|reject|flag",
908
+ "reason": "Brief explanation of decision",
909
+ "stale_references": ["list of stale references found, if any"],
910
+ "overlapping_dead_ends": [0]
749
911
  }
750
912
  -->`
751
913
  };
@@ -1235,12 +1397,12 @@ function getMetricHistoryByFixture(db, fixture) {
1235
1397
  ORDER BY m.captured_at
1236
1398
  `).all(fixture);
1237
1399
  }
1238
- function insertDeadEnd(db, experimentId, approach, whyFailed, structuralConstraint, subType) {
1400
+ function insertDeadEnd(db, experimentId, approach, whyFailed, structuralConstraint, subType, category = "structural") {
1239
1401
  const stmt = db.prepare(`
1240
- INSERT INTO dead_ends (experiment_id, approach, why_failed, structural_constraint, sub_type)
1241
- VALUES (?, ?, ?, ?, ?)
1402
+ INSERT INTO dead_ends (experiment_id, approach, why_failed, structural_constraint, sub_type, category)
1403
+ VALUES (?, ?, ?, ?, ?, ?)
1242
1404
  `);
1243
- const result = stmt.run(experimentId, approach, whyFailed, structuralConstraint, subType);
1405
+ const result = stmt.run(experimentId, approach, whyFailed, structuralConstraint, subType, category);
1244
1406
  return db.prepare("SELECT * FROM dead_ends WHERE id = ?").get(result.lastInsertRowid);
1245
1407
  }
1246
1408
  function listDeadEndsBySubType(db, subType) {
@@ -1315,6 +1477,9 @@ function insertChallenge(db, experimentId, description, reasoning) {
1315
1477
  const result = stmt.run(experimentId, description, reasoning);
1316
1478
  return db.prepare("SELECT * FROM challenges WHERE id = ?").get(result.lastInsertRowid);
1317
1479
  }
1480
+ function getChallengesByExperiment(db, experimentId) {
1481
+ return db.prepare("SELECT * FROM challenges WHERE experiment_id = ? ORDER BY created_at").all(experimentId);
1482
+ }
1318
1483
  function incrementSubTypeFailure(db, subType, experimentId, grade) {
1319
1484
  db.prepare(`
1320
1485
  INSERT INTO sub_type_failures (sub_type, experiment_id, grade)
@@ -1380,12 +1545,167 @@ function recordCompression(db, sessionCountSinceLast, synthesisSizeBefore, synth
1380
1545
  const result = stmt.run(sessionCountSinceLast, synthesisSizeBefore, synthesisSizeAfter);
1381
1546
  return db.prepare("SELECT * FROM compressions WHERE id = ?").get(result.lastInsertRowid);
1382
1547
  }
1548
+ function listStructuralDeadEnds(db) {
1549
+ return db.prepare(`
1550
+ SELECT * FROM dead_ends WHERE category = 'structural' ORDER BY created_at
1551
+ `).all();
1552
+ }
1553
+ function listStructuralDeadEndsBySubType(db, subType) {
1554
+ return db.prepare(`
1555
+ SELECT * FROM dead_ends WHERE category = 'structural' AND sub_type = ? ORDER BY created_at
1556
+ `).all(subType);
1557
+ }
1558
+ function insertReframe(db, experimentId, decomposition, divergences, recommendation) {
1559
+ db.prepare(`
1560
+ INSERT INTO reframes (experiment_id, decomposition, divergences, recommendation)
1561
+ VALUES (?, ?, ?, ?)
1562
+ `).run(experimentId, decomposition, divergences, recommendation);
1563
+ }
1564
+ function insertFinding(db, experimentId, approach, source, relevance, contradictsCurrent) {
1565
+ db.prepare(`
1566
+ INSERT INTO findings (experiment_id, approach, source, relevance, contradicts_current)
1567
+ VALUES (?, ?, ?, ?, ?)
1568
+ `).run(experimentId, approach, source, relevance, contradictsCurrent ? 1 : 0);
1569
+ }
1570
+ function exportForCompressor(db, maxLength = 3e4) {
1571
+ const experiments = listAllExperiments(db);
1572
+ const sections = ["# Structured Data Export (from SQLite)\n"];
1573
+ sections.push("## Experiments");
1574
+ for (const exp of experiments) {
1575
+ sections.push(`### EXP-${String(exp.id).padStart(3, "0")}: ${exp.slug}`);
1576
+ sections.push(`- Status: ${exp.status} | Sub-type: ${exp.sub_type ?? "(none)"}`);
1577
+ sections.push(`- Hypothesis: ${exp.hypothesis ?? "(none)"}`);
1578
+ const decisions = listDecisionsByExperiment(db, exp.id);
1579
+ if (decisions.length > 0) {
1580
+ sections.push(`#### Decisions (${decisions.length})`);
1581
+ for (const d of decisions) {
1582
+ sections.push(`- [${d.evidence_level}] ${d.description} \u2014 ${d.justification} (${d.status})`);
1583
+ }
1584
+ }
1585
+ const doubts = getDoubtsByExperiment(db, exp.id);
1586
+ if (doubts.length > 0) {
1587
+ sections.push(`#### Doubts (${doubts.length})`);
1588
+ for (const d of doubts) {
1589
+ sections.push(`- [${d.severity}] ${d.claim_doubted} (resolution: ${d.resolution ?? "pending"})`);
1590
+ }
1591
+ }
1592
+ const verifications = getVerificationsByExperiment(db, exp.id);
1593
+ if (verifications.length > 0) {
1594
+ sections.push(`#### Verifications (${verifications.length})`);
1595
+ for (const v of verifications) {
1596
+ sections.push(`- ${v.component}: ${v.grade}${v.notes ? ` \u2014 ${v.notes}` : ""}`);
1597
+ }
1598
+ }
1599
+ const challenges = getChallengesByExperiment(db, exp.id);
1600
+ if (challenges.length > 0) {
1601
+ sections.push(`#### Challenges (${challenges.length})`);
1602
+ for (const c of challenges) {
1603
+ sections.push(`- ${c.description}`);
1604
+ }
1605
+ }
1606
+ sections.push("");
1607
+ }
1608
+ const deadEnds = listAllDeadEnds(db);
1609
+ if (deadEnds.length > 0) {
1610
+ sections.push("## Dead Ends");
1611
+ for (const de of deadEnds) {
1612
+ sections.push(`- [${de.category ?? "structural"}] ${de.approach}: ${de.why_failed} \u2192 ${de.structural_constraint}`);
1613
+ }
1614
+ sections.push("");
1615
+ }
1616
+ const unresolvedDoubts = db.prepare(`
1617
+ SELECT d.*, e.slug as experiment_slug
1618
+ FROM doubts d JOIN experiments e ON d.experiment_id = e.id
1619
+ WHERE d.resolution IS NULL
1620
+ ORDER BY d.severity DESC, d.created_at
1621
+ `).all();
1622
+ if (unresolvedDoubts.length > 0) {
1623
+ sections.push("## Unresolved Doubts");
1624
+ for (const d of unresolvedDoubts) {
1625
+ sections.push(`- [${d.severity}] ${d.claim_doubted} (exp: ${d.experiment_slug})`);
1626
+ }
1627
+ }
1628
+ const full = sections.join("\n");
1629
+ if (full.length > maxLength) {
1630
+ return full.slice(0, maxLength) + `
1631
+
1632
+ [TRUNCATED \u2014 full export was ${full.length} chars]`;
1633
+ }
1634
+ return full;
1635
+ }
1383
1636
  var init_queries = __esm({
1384
1637
  "src/db/queries.ts"() {
1385
1638
  "use strict";
1386
1639
  }
1387
1640
  });
1388
1641
 
1642
+ // src/config.ts
1643
+ function loadConfig(projectRoot) {
1644
+ if (_cachedConfig && _cachedRoot === projectRoot) return _cachedConfig;
1645
+ const configPath = path3.join(projectRoot, ".majlis", "config.json");
1646
+ if (!fs3.existsSync(configPath)) {
1647
+ _cachedConfig = { ...DEFAULT_CONFIG2 };
1648
+ _cachedRoot = projectRoot;
1649
+ return _cachedConfig;
1650
+ }
1651
+ const loaded = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
1652
+ _cachedConfig = {
1653
+ ...DEFAULT_CONFIG2,
1654
+ ...loaded,
1655
+ project: { ...DEFAULT_CONFIG2.project, ...loaded.project },
1656
+ metrics: { ...DEFAULT_CONFIG2.metrics, ...loaded.metrics },
1657
+ build: { ...DEFAULT_CONFIG2.build, ...loaded.build },
1658
+ cycle: { ...DEFAULT_CONFIG2.cycle, ...loaded.cycle }
1659
+ };
1660
+ _cachedRoot = projectRoot;
1661
+ return _cachedConfig;
1662
+ }
1663
+ function readFileOrEmpty(filePath) {
1664
+ try {
1665
+ return fs3.readFileSync(filePath, "utf-8");
1666
+ } catch {
1667
+ return "";
1668
+ }
1669
+ }
1670
+ function getFlagValue(args, flag) {
1671
+ const idx = args.indexOf(flag);
1672
+ if (idx < 0 || idx + 1 >= args.length) return void 0;
1673
+ return args[idx + 1];
1674
+ }
1675
+ function truncateContext(content, limit) {
1676
+ if (content.length <= limit) return content;
1677
+ return content.slice(0, limit) + "\n[TRUNCATED]";
1678
+ }
1679
+ var fs3, path3, DEFAULT_CONFIG2, _cachedConfig, _cachedRoot, CONTEXT_LIMITS;
1680
+ var init_config = __esm({
1681
+ "src/config.ts"() {
1682
+ "use strict";
1683
+ fs3 = __toESM(require("fs"));
1684
+ path3 = __toESM(require("path"));
1685
+ DEFAULT_CONFIG2 = {
1686
+ project: { name: "", description: "", objective: "" },
1687
+ metrics: { command: "", fixtures: [], tracked: {} },
1688
+ build: { pre_measure: null, post_measure: null },
1689
+ cycle: {
1690
+ compression_interval: 5,
1691
+ circuit_breaker_threshold: 3,
1692
+ require_doubt_before_verify: true,
1693
+ require_challenge_before_verify: false,
1694
+ auto_baseline_on_new_experiment: true
1695
+ },
1696
+ models: {}
1697
+ };
1698
+ _cachedConfig = null;
1699
+ _cachedRoot = null;
1700
+ CONTEXT_LIMITS = {
1701
+ synthesis: 3e4,
1702
+ fragility: 15e3,
1703
+ experimentDoc: 15e3,
1704
+ deadEnds: 15e3
1705
+ };
1706
+ }
1707
+ });
1708
+
1389
1709
  // src/commands/status.ts
1390
1710
  var status_exports = {};
1391
1711
  __export(status_exports, {
@@ -1475,21 +1795,12 @@ function buildSummary(expCount, activeSession, sessionsSinceCompression, config)
1475
1795
  }
1476
1796
  return parts.join(". ");
1477
1797
  }
1478
- function loadConfig(projectRoot) {
1479
- const configPath = path3.join(projectRoot, ".majlis", "config.json");
1480
- if (!fs3.existsSync(configPath)) {
1481
- throw new Error("Missing .majlis/config.json. Run `majlis init` first.");
1482
- }
1483
- return JSON.parse(fs3.readFileSync(configPath, "utf-8"));
1484
- }
1485
- var fs3, path3;
1486
1798
  var init_status = __esm({
1487
1799
  "src/commands/status.ts"() {
1488
1800
  "use strict";
1489
- fs3 = __toESM(require("fs"));
1490
- path3 = __toESM(require("path"));
1491
1801
  init_connection();
1492
1802
  init_queries();
1803
+ init_config();
1493
1804
  init_format();
1494
1805
  }
1495
1806
  });
@@ -1571,11 +1882,11 @@ async function captureMetrics(phase, args) {
1571
1882
  const root = findProjectRoot();
1572
1883
  if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
1573
1884
  const db = getDb(root);
1574
- const config = loadConfig2(root);
1575
- const expIdIdx = args.indexOf("--experiment");
1885
+ const config = loadConfig(root);
1886
+ const expIdStr = getFlagValue(args, "--experiment");
1576
1887
  let exp;
1577
- if (expIdIdx >= 0) {
1578
- exp = getExperimentById(db, Number(args[expIdIdx + 1]));
1888
+ if (expIdStr !== void 0) {
1889
+ exp = getExperimentById(db, Number(expIdStr));
1579
1890
  } else {
1580
1891
  exp = getLatestExperiment(db);
1581
1892
  }
@@ -1623,11 +1934,11 @@ async function compare(args, isJson) {
1623
1934
  const root = findProjectRoot();
1624
1935
  if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
1625
1936
  const db = getDb(root);
1626
- const config = loadConfig2(root);
1627
- const expIdIdx = args.indexOf("--experiment");
1937
+ const config = loadConfig(root);
1938
+ const expIdStr = getFlagValue(args, "--experiment");
1628
1939
  let exp;
1629
- if (expIdIdx >= 0) {
1630
- exp = getExperimentById(db, Number(args[expIdIdx + 1]));
1940
+ if (expIdStr !== void 0) {
1941
+ exp = getExperimentById(db, Number(expIdStr));
1631
1942
  } else {
1632
1943
  exp = getLatestExperiment(db);
1633
1944
  }
@@ -1664,23 +1975,15 @@ function formatDelta(delta) {
1664
1975
  const prefix = delta > 0 ? "+" : "";
1665
1976
  return `${prefix}${delta.toFixed(4)}`;
1666
1977
  }
1667
- function loadConfig2(projectRoot) {
1668
- const configPath = path4.join(projectRoot, ".majlis", "config.json");
1669
- if (!fs4.existsSync(configPath)) {
1670
- throw new Error("Missing .majlis/config.json. Run `majlis init` first.");
1671
- }
1672
- return JSON.parse(fs4.readFileSync(configPath, "utf-8"));
1673
- }
1674
- var fs4, path4, import_node_child_process;
1978
+ var import_node_child_process;
1675
1979
  var init_measure = __esm({
1676
1980
  "src/commands/measure.ts"() {
1677
1981
  "use strict";
1678
- fs4 = __toESM(require("fs"));
1679
- path4 = __toESM(require("path"));
1680
1982
  import_node_child_process = require("child_process");
1681
1983
  init_connection();
1682
1984
  init_queries();
1683
1985
  init_metrics();
1986
+ init_config();
1684
1987
  init_format();
1685
1988
  }
1686
1989
  });
@@ -1699,7 +2002,7 @@ async function newExperiment(args) {
1699
2002
  throw new Error('Usage: majlis new "hypothesis"');
1700
2003
  }
1701
2004
  const db = getDb(root);
1702
- const config = loadConfig3(root);
2005
+ const config = loadConfig(root);
1703
2006
  const slug = slugify(hypothesis);
1704
2007
  if (getExperimentBySlug(db, slug)) {
1705
2008
  throw new Error(`Experiment with slug "${slug}" already exists.`);
@@ -1718,17 +2021,16 @@ async function newExperiment(args) {
1718
2021
  } catch (err) {
1719
2022
  warn(`Could not create branch ${branch} \u2014 continuing without git branch.`);
1720
2023
  }
1721
- const subTypeIdx = args.indexOf("--sub-type");
1722
- const subType = subTypeIdx >= 0 ? args[subTypeIdx + 1] : null;
2024
+ const subType = getFlagValue(args, "--sub-type") ?? null;
1723
2025
  const exp = createExperiment(db, slug, branch, hypothesis, subType, null);
1724
2026
  success(`Created experiment #${exp.id}: ${exp.slug}`);
1725
- const docsDir = path5.join(root, "docs", "experiments");
1726
- const templatePath = path5.join(docsDir, "_TEMPLATE.md");
1727
- if (fs5.existsSync(templatePath)) {
1728
- const template = fs5.readFileSync(templatePath, "utf-8");
2027
+ const docsDir = path4.join(root, "docs", "experiments");
2028
+ const templatePath = path4.join(docsDir, "_TEMPLATE.md");
2029
+ if (fs4.existsSync(templatePath)) {
2030
+ const template = fs4.readFileSync(templatePath, "utf-8");
1729
2031
  const logContent = template.replace(/\{\{title\}\}/g, hypothesis).replace(/\{\{hypothesis\}\}/g, hypothesis).replace(/\{\{branch\}\}/g, branch).replace(/\{\{status\}\}/g, "classified").replace(/\{\{sub_type\}\}/g, subType ?? "unclassified").replace(/\{\{date\}\}/g, (/* @__PURE__ */ new Date()).toISOString().split("T")[0]);
1730
- const logPath = path5.join(docsDir, `${paddedNum}-${slug}.md`);
1731
- fs5.writeFileSync(logPath, logContent);
2032
+ const logPath = path4.join(docsDir, `${paddedNum}-${slug}.md`);
2033
+ fs4.writeFileSync(logPath, logContent);
1732
2034
  info(`Created experiment log: docs/experiments/${paddedNum}-${slug}.md`);
1733
2035
  }
1734
2036
  if (config.cycle.auto_baseline_on_new_experiment && config.metrics.command) {
@@ -1754,15 +2056,16 @@ async function revert(args) {
1754
2056
  exp = getLatestExperiment(db);
1755
2057
  if (!exp) throw new Error("No active experiments to revert.");
1756
2058
  }
1757
- const reasonIdx = args.indexOf("--reason");
1758
- const reason = reasonIdx >= 0 ? args[reasonIdx + 1] : "Manually reverted";
2059
+ const reason = getFlagValue(args, "--reason") ?? "Manually reverted";
2060
+ const category = args.includes("--structural") ? "structural" : "procedural";
1759
2061
  insertDeadEnd(
1760
2062
  db,
1761
2063
  exp.id,
1762
2064
  exp.hypothesis ?? exp.slug,
1763
2065
  reason,
1764
2066
  `Reverted: ${reason}`,
1765
- exp.sub_type
2067
+ exp.sub_type,
2068
+ category
1766
2069
  );
1767
2070
  updateExperimentStatus(db, exp.id, "dead_end");
1768
2071
  try {
@@ -1785,22 +2088,16 @@ async function revert(args) {
1785
2088
  function slugify(text) {
1786
2089
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
1787
2090
  }
1788
- function loadConfig3(projectRoot) {
1789
- const configPath = path5.join(projectRoot, ".majlis", "config.json");
1790
- if (!fs5.existsSync(configPath)) {
1791
- return { cycle: { auto_baseline_on_new_experiment: false } };
1792
- }
1793
- return JSON.parse(fs5.readFileSync(configPath, "utf-8"));
1794
- }
1795
- var fs5, path5, import_node_child_process2;
2091
+ var fs4, path4, import_node_child_process2;
1796
2092
  var init_experiment = __esm({
1797
2093
  "src/commands/experiment.ts"() {
1798
2094
  "use strict";
1799
- fs5 = __toESM(require("fs"));
1800
- path5 = __toESM(require("path"));
2095
+ fs4 = __toESM(require("fs"));
2096
+ path4 = __toESM(require("path"));
1801
2097
  import_node_child_process2 = require("child_process");
1802
2098
  init_connection();
1803
2099
  init_queries();
2100
+ init_config();
1804
2101
  init_format();
1805
2102
  }
1806
2103
  });
@@ -1840,12 +2137,9 @@ async function session(args) {
1840
2137
  if (!active) {
1841
2138
  throw new Error("No active session to end.");
1842
2139
  }
1843
- const accomplishedIdx = args.indexOf("--accomplished");
1844
- const accomplished = accomplishedIdx >= 0 ? args[accomplishedIdx + 1] : null;
1845
- const unfinishedIdx = args.indexOf("--unfinished");
1846
- const unfinished = unfinishedIdx >= 0 ? args[unfinishedIdx + 1] : null;
1847
- const fragilityIdx = args.indexOf("--fragility");
1848
- const fragility = fragilityIdx >= 0 ? args[fragilityIdx + 1] : null;
2140
+ const accomplished = getFlagValue(args, "--accomplished") ?? null;
2141
+ const unfinished = getFlagValue(args, "--unfinished") ?? null;
2142
+ const fragility = getFlagValue(args, "--fragility") ?? null;
1849
2143
  endSession(db, active.id, accomplished, unfinished, fragility);
1850
2144
  success(`Session ended: "${active.intent}"`);
1851
2145
  if (accomplished) info(`Accomplished: ${accomplished}`);
@@ -1858,6 +2152,7 @@ var init_session = __esm({
1858
2152
  "use strict";
1859
2153
  init_connection();
1860
2154
  init_queries();
2155
+ init_config();
1861
2156
  init_format();
1862
2157
  }
1863
2158
  });
@@ -1887,10 +2182,9 @@ async function query(command, args, isJson) {
1887
2182
  }
1888
2183
  }
1889
2184
  function queryDecisions(db, args, isJson) {
1890
- const levelIdx = args.indexOf("--level");
1891
- const level = levelIdx >= 0 ? args[levelIdx + 1] : void 0;
1892
- const expIdx = args.indexOf("--experiment");
1893
- const experimentId = expIdx >= 0 ? Number(args[expIdx + 1]) : void 0;
2185
+ const level = getFlagValue(args, "--level");
2186
+ const expIdStr = getFlagValue(args, "--experiment");
2187
+ const experimentId = expIdStr !== void 0 ? Number(expIdStr) : void 0;
1894
2188
  const decisions = listAllDecisions(db, level, experimentId);
1895
2189
  if (isJson) {
1896
2190
  console.log(JSON.stringify(decisions, null, 2));
@@ -1911,10 +2205,8 @@ function queryDecisions(db, args, isJson) {
1911
2205
  console.log(table(["ID", "Exp", "Level", "Description", "Status"], rows));
1912
2206
  }
1913
2207
  function queryDeadEnds(db, args, isJson) {
1914
- const subTypeIdx = args.indexOf("--sub-type");
1915
- const subType = subTypeIdx >= 0 ? args[subTypeIdx + 1] : void 0;
1916
- const searchIdx = args.indexOf("--search");
1917
- const searchTerm = searchIdx >= 0 ? args[searchIdx + 1] : void 0;
2208
+ const subType = getFlagValue(args, "--sub-type");
2209
+ const searchTerm = getFlagValue(args, "--search");
1918
2210
  let deadEnds;
1919
2211
  if (subType) {
1920
2212
  deadEnds = listDeadEndsBySubType(db, subType);
@@ -1941,12 +2233,12 @@ function queryDeadEnds(db, args, isJson) {
1941
2233
  console.log(table(["ID", "Sub-Type", "Approach", "Constraint"], rows));
1942
2234
  }
1943
2235
  function queryFragility(root, isJson) {
1944
- const fragPath = path6.join(root, "docs", "synthesis", "fragility.md");
1945
- if (!fs6.existsSync(fragPath)) {
2236
+ const fragPath = path5.join(root, "docs", "synthesis", "fragility.md");
2237
+ if (!fs5.existsSync(fragPath)) {
1946
2238
  info("No fragility map found.");
1947
2239
  return;
1948
2240
  }
1949
- const content = fs6.readFileSync(fragPath, "utf-8");
2241
+ const content = fs5.readFileSync(fragPath, "utf-8");
1950
2242
  if (isJson) {
1951
2243
  console.log(JSON.stringify({ content }, null, 2));
1952
2244
  return;
@@ -1980,7 +2272,7 @@ function queryHistory(db, args, isJson) {
1980
2272
  console.log(table(["Exp", "Slug", "Phase", "Metric", "Value", "Captured"], rows));
1981
2273
  }
1982
2274
  function queryCircuitBreakers(db, root, isJson) {
1983
- const config = loadConfig4(root);
2275
+ const config = loadConfig(root);
1984
2276
  const states = getAllCircuitBreakerStates(db, config.cycle.circuit_breaker_threshold);
1985
2277
  if (isJson) {
1986
2278
  console.log(JSON.stringify(states, null, 2));
@@ -2002,7 +2294,7 @@ function queryCircuitBreakers(db, root, isJson) {
2002
2294
  function checkCommit(db) {
2003
2295
  let stdinData = "";
2004
2296
  try {
2005
- stdinData = fs6.readFileSync(0, "utf-8");
2297
+ stdinData = fs5.readFileSync(0, "utf-8");
2006
2298
  } catch {
2007
2299
  }
2008
2300
  if (stdinData) {
@@ -2027,21 +2319,15 @@ function checkCommit(db) {
2027
2319
  process.exit(1);
2028
2320
  }
2029
2321
  }
2030
- function loadConfig4(projectRoot) {
2031
- const configPath = path6.join(projectRoot, ".majlis", "config.json");
2032
- if (!fs6.existsSync(configPath)) {
2033
- return { cycle: { circuit_breaker_threshold: 3 } };
2034
- }
2035
- return JSON.parse(fs6.readFileSync(configPath, "utf-8"));
2036
- }
2037
- var fs6, path6;
2322
+ var fs5, path5;
2038
2323
  var init_query = __esm({
2039
2324
  "src/commands/query.ts"() {
2040
2325
  "use strict";
2041
- fs6 = __toESM(require("fs"));
2042
- path6 = __toESM(require("path"));
2326
+ fs5 = __toESM(require("fs"));
2327
+ path5 = __toESM(require("path"));
2043
2328
  init_connection();
2044
2329
  init_queries();
2330
+ init_config();
2045
2331
  init_format();
2046
2332
  }
2047
2333
  });
@@ -2052,8 +2338,10 @@ var init_types = __esm({
2052
2338
  "src/state/types.ts"() {
2053
2339
  "use strict";
2054
2340
  TRANSITIONS = {
2055
- ["classified" /* CLASSIFIED */]: ["reframed" /* REFRAMED */, "building" /* BUILDING */],
2056
- ["reframed" /* REFRAMED */]: ["building" /* BUILDING */],
2341
+ ["classified" /* CLASSIFIED */]: ["reframed" /* REFRAMED */, "gated" /* GATED */],
2342
+ ["reframed" /* REFRAMED */]: ["gated" /* GATED */],
2343
+ ["gated" /* GATED */]: ["building" /* BUILDING */, "gated" /* GATED */],
2344
+ // self-loop for rejected hypotheses
2057
2345
  ["building" /* BUILDING */]: ["built" /* BUILT */, "building" /* BUILDING */],
2058
2346
  // self-loop for retry after truncation
2059
2347
  ["built" /* BUILT */]: ["challenged" /* CHALLENGED */, "doubted" /* DOUBTED */],
@@ -2063,7 +2351,9 @@ var init_types = __esm({
2063
2351
  ["verifying" /* VERIFYING */]: ["verified" /* VERIFIED */],
2064
2352
  ["verified" /* VERIFIED */]: ["resolved" /* RESOLVED */],
2065
2353
  ["resolved" /* RESOLVED */]: ["compressed" /* COMPRESSED */, "building" /* BUILDING */],
2354
+ // cycle-back skips gate
2066
2355
  ["compressed" /* COMPRESSED */]: ["merged" /* MERGED */, "building" /* BUILDING */],
2356
+ // cycle-back skips gate
2067
2357
  ["merged" /* MERGED */]: [],
2068
2358
  ["dead_end" /* DEAD_END */]: []
2069
2359
  };
@@ -2092,7 +2382,10 @@ function determineNextStep(exp, valid, hasDoubts2, hasChallenges2) {
2092
2382
  throw new Error(`Experiment ${exp.slug} is terminal (${exp.status})`);
2093
2383
  }
2094
2384
  const status2 = exp.status;
2095
- if (status2 === "classified" /* CLASSIFIED */) {
2385
+ if (status2 === "classified" /* CLASSIFIED */ || status2 === "reframed" /* REFRAMED */) {
2386
+ return valid.includes("gated" /* GATED */) ? "gated" /* GATED */ : valid[0];
2387
+ }
2388
+ if (status2 === "gated" /* GATED */) {
2096
2389
  return valid.includes("building" /* BUILDING */) ? "building" /* BUILDING */ : valid[0];
2097
2390
  }
2098
2391
  if (status2 === "built" /* BUILT */ && !hasDoubts2) {
@@ -2116,7 +2409,29 @@ var init_machine = __esm({
2116
2409
  });
2117
2410
 
2118
2411
  // src/agents/types.ts
2119
- var EXTRACTION_SCHEMA;
2412
+ function getExtractionSchema(role) {
2413
+ switch (role) {
2414
+ case "builder":
2415
+ return '{"decisions": [{"description": "string", "evidence_level": "proof|test|strong_consensus|consensus|analogy|judgment", "justification": "string"}]}';
2416
+ case "critic":
2417
+ return '{"doubts": [{"claim_doubted": "string", "evidence_level_of_claim": "string", "evidence_for_doubt": "string", "severity": "minor|moderate|critical"}]}';
2418
+ case "adversary":
2419
+ return '{"challenges": [{"description": "string", "reasoning": "string"}]}';
2420
+ case "verifier":
2421
+ return '{"grades": [{"component": "string", "grade": "sound|good|weak|rejected", "provenance_intact": true, "content_correct": true, "notes": "string"}], "doubt_resolutions": [{"doubt_id": 0, "resolution": "confirmed|dismissed|inconclusive"}]}';
2422
+ case "gatekeeper":
2423
+ return '{"gate_decision": "approve|reject|flag", "reason": "string", "stale_references": ["string"], "overlapping_dead_ends": [0]}';
2424
+ case "reframer":
2425
+ return '{"reframe": {"decomposition": "string", "divergences": ["string"], "recommendation": "string"}}';
2426
+ case "scout":
2427
+ return '{"findings": [{"approach": "string", "source": "string", "relevance": "string", "contradicts_current": true}]}';
2428
+ case "compressor":
2429
+ return '{"compression_report": {"synthesis_delta": "string", "new_dead_ends": ["string"], "fragility_changes": ["string"]}}';
2430
+ default:
2431
+ return EXTRACTION_SCHEMA;
2432
+ }
2433
+ }
2434
+ var EXTRACTION_SCHEMA, ROLE_REQUIRED_FIELDS;
2120
2435
  var init_types2 = __esm({
2121
2436
  "src/agents/types.ts"() {
2122
2437
  "use strict";
@@ -2127,6 +2442,16 @@ var init_types2 = __esm({
2127
2442
  "guidance": "string (actionable builder guidance)",
2128
2443
  "doubt_resolutions": [{ "doubt_id": 0, "resolution": "confirmed|dismissed|inconclusive" }]
2129
2444
  }`;
2445
+ ROLE_REQUIRED_FIELDS = {
2446
+ builder: ["decisions"],
2447
+ critic: ["doubts"],
2448
+ adversary: ["challenges"],
2449
+ verifier: ["grades"],
2450
+ gatekeeper: ["gate_decision"],
2451
+ reframer: ["reframe"],
2452
+ scout: ["findings"],
2453
+ compressor: ["compression_report"]
2454
+ };
2130
2455
  }
2131
2456
  });
2132
2457
 
@@ -2214,7 +2539,8 @@ function extractViaPatterns(role, markdown) {
2214
2539
  while ((match = doubtPattern.exec(markdown)) !== null) {
2215
2540
  doubts.push({
2216
2541
  claim_doubted: match[1].trim(),
2217
- evidence_level_of_claim: "judgment",
2542
+ evidence_level_of_claim: "unknown",
2543
+ // Don't fabricate — mark as unknown for review
2218
2544
  evidence_for_doubt: "Extracted via regex \u2014 review original document",
2219
2545
  severity: match[2].toLowerCase().trim()
2220
2546
  });
@@ -2225,7 +2551,8 @@ function extractViaPatterns(role, markdown) {
2225
2551
  async function extractViaHaiku(role, markdown) {
2226
2552
  try {
2227
2553
  const truncated = markdown.length > 8e3 ? markdown.slice(0, 8e3) + "\n[truncated]" : markdown;
2228
- const prompt = `Extract all decisions, evidence levels, grades, doubts, and guidance from this ${role} document as JSON. Follow this schema exactly: ${EXTRACTION_SCHEMA}
2554
+ const schema = getExtractionSchema(role);
2555
+ const prompt = `Extract structured data from this ${role} document as JSON. Follow this schema exactly: ${schema}
2229
2556
 
2230
2557
  Document:
2231
2558
  ${truncated}`;
@@ -2258,7 +2585,18 @@ ${truncated}`;
2258
2585
  }
2259
2586
  }
2260
2587
  function hasData(output) {
2261
- return !!(output.decisions && output.decisions.length > 0 || output.grades && output.grades.length > 0 || output.doubts && output.doubts.length > 0 || output.guidance);
2588
+ 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);
2589
+ }
2590
+ function validateForRole(role, output) {
2591
+ const required = ROLE_REQUIRED_FIELDS[role];
2592
+ if (!required) return { valid: true, missing: [] };
2593
+ const missing = required.filter((field) => {
2594
+ const value = output[field];
2595
+ if (value === void 0 || value === null) return true;
2596
+ if (Array.isArray(value) && value.length === 0) return true;
2597
+ return false;
2598
+ });
2599
+ return { valid: missing.length === 0, missing };
2262
2600
  }
2263
2601
  var import_claude_agent_sdk;
2264
2602
  var init_parse = __esm({
@@ -2272,11 +2610,11 @@ var init_parse = __esm({
2272
2610
  // src/agents/spawn.ts
2273
2611
  function loadAgentDefinition(role, projectRoot) {
2274
2612
  const root = projectRoot ?? findProjectRoot() ?? process.cwd();
2275
- const filePath = path7.join(root, ".majlis", "agents", `${role}.md`);
2276
- if (!fs7.existsSync(filePath)) {
2613
+ const filePath = path6.join(root, ".majlis", "agents", `${role}.md`);
2614
+ if (!fs6.existsSync(filePath)) {
2277
2615
  throw new Error(`Agent definition not found: ${filePath}`);
2278
2616
  }
2279
- const content = fs7.readFileSync(filePath, "utf-8");
2617
+ const content = fs6.readFileSync(filePath, "utf-8");
2280
2618
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
2281
2619
  if (!frontmatterMatch) {
2282
2620
  throw new Error(`Invalid agent definition (missing YAML frontmatter): ${filePath}`);
@@ -2297,7 +2635,7 @@ async function spawnAgent(role, context, projectRoot) {
2297
2635
  const agentDef = loadAgentDefinition(role, projectRoot);
2298
2636
  const root = projectRoot ?? findProjectRoot() ?? process.cwd();
2299
2637
  const taskPrompt = context.taskPrompt ?? `Perform your role as ${agentDef.name}.`;
2300
- const contextJson = JSON.stringify(context, null, 2);
2638
+ const contextJson = JSON.stringify(context);
2301
2639
  const prompt = `Here is your context:
2302
2640
 
2303
2641
  \`\`\`json
@@ -2322,11 +2660,17 @@ ${taskPrompt}`;
2322
2660
  console.log(`[${role}] Artifact written to ${artifactPath}`);
2323
2661
  }
2324
2662
  const structured = await extractStructuredData(role, markdown);
2663
+ if (structured) {
2664
+ const { valid, missing } = validateForRole(role, structured);
2665
+ if (!valid) {
2666
+ console.warn(`[${role}] Output missing expected fields: ${missing.join(", ")}`);
2667
+ }
2668
+ }
2325
2669
  return { output: markdown, structured, truncated };
2326
2670
  }
2327
2671
  async function spawnSynthesiser(context, projectRoot) {
2328
2672
  const root = projectRoot ?? findProjectRoot() ?? process.cwd();
2329
- const contextJson = JSON.stringify(context, null, 2);
2673
+ const contextJson = JSON.stringify(context);
2330
2674
  const taskPrompt = context.taskPrompt ?? "Synthesise the findings into actionable builder guidance.";
2331
2675
  const prompt = `Here is your context:
2332
2676
 
@@ -2339,7 +2683,7 @@ ${taskPrompt}`;
2339
2683
  console.log(`[synthesiser] Spawning (maxTurns: 5)...`);
2340
2684
  const { text: markdown, costUsd, truncated } = await runQuery({
2341
2685
  prompt,
2342
- model: "opus",
2686
+ model: "sonnet",
2343
2687
  tools: ["Read", "Glob", "Grep"],
2344
2688
  systemPrompt,
2345
2689
  cwd: root,
@@ -2353,15 +2697,15 @@ async function spawnRecovery(role, partialOutput, context, projectRoot) {
2353
2697
  const root = projectRoot ?? findProjectRoot() ?? process.cwd();
2354
2698
  const expSlug = context.experiment?.slug ?? "unknown";
2355
2699
  console.log(`[recovery] Cleaning up after truncated ${role} for ${expSlug}...`);
2356
- const expDocPath = path7.join(
2700
+ const expDocPath = path6.join(
2357
2701
  root,
2358
2702
  "docs",
2359
2703
  "experiments",
2360
2704
  `${String(context.experiment?.id ?? 0).padStart(3, "0")}-${expSlug}.md`
2361
2705
  );
2362
- const templatePath = path7.join(root, "docs", "experiments", "_TEMPLATE.md");
2363
- const template = fs7.existsSync(templatePath) ? fs7.readFileSync(templatePath, "utf-8") : "";
2364
- const currentDoc = fs7.existsSync(expDocPath) ? fs7.readFileSync(expDocPath, "utf-8") : "";
2706
+ const templatePath = path6.join(root, "docs", "experiments", "_TEMPLATE.md");
2707
+ const template = fs6.existsSync(templatePath) ? fs6.readFileSync(templatePath, "utf-8") : "";
2708
+ const currentDoc = fs6.existsSync(expDocPath) ? fs6.readFileSync(expDocPath, "utf-8") : "";
2365
2709
  const prompt = `The ${role} agent was truncated (hit max turns) while working on experiment "${expSlug}".
2366
2710
 
2367
2711
  Here is the partial agent output (reasoning + tool calls):
@@ -2498,23 +2842,23 @@ function writeArtifact(role, context, markdown, projectRoot) {
2498
2842
  const dir = dirMap[role];
2499
2843
  if (!dir) return null;
2500
2844
  if (role === "builder" || role === "compressor") return null;
2501
- const fullDir = path7.join(projectRoot, dir);
2502
- if (!fs7.existsSync(fullDir)) {
2503
- fs7.mkdirSync(fullDir, { recursive: true });
2845
+ const fullDir = path6.join(projectRoot, dir);
2846
+ if (!fs6.existsSync(fullDir)) {
2847
+ fs6.mkdirSync(fullDir, { recursive: true });
2504
2848
  }
2505
2849
  const expSlug = context.experiment?.slug ?? "general";
2506
2850
  const nextNum = String(context.experiment?.id ?? 1).padStart(3, "0");
2507
2851
  const filename = `${nextNum}-${role}-${expSlug}.md`;
2508
- const target = path7.join(fullDir, filename);
2509
- fs7.writeFileSync(target, markdown);
2852
+ const target = path6.join(fullDir, filename);
2853
+ fs6.writeFileSync(target, markdown);
2510
2854
  return target;
2511
2855
  }
2512
- var fs7, path7, import_claude_agent_sdk2, ROLE_MAX_TURNS, DIM2, RESET2, CYAN2;
2856
+ var fs6, path6, import_claude_agent_sdk2, ROLE_MAX_TURNS, DIM2, RESET2, CYAN2;
2513
2857
  var init_spawn = __esm({
2514
2858
  "src/agents/spawn.ts"() {
2515
2859
  "use strict";
2516
- fs7 = __toESM(require("fs"));
2517
- path7 = __toESM(require("path"));
2860
+ fs6 = __toESM(require("fs"));
2861
+ path6 = __toESM(require("path"));
2518
2862
  import_claude_agent_sdk2 = require("@anthropic-ai/claude-agent-sdk");
2519
2863
  init_parse();
2520
2864
  init_connection();
@@ -2525,7 +2869,8 @@ var init_spawn = __esm({
2525
2869
  verifier: 50,
2526
2870
  compressor: 30,
2527
2871
  reframer: 20,
2528
- scout: 20
2872
+ scout: 20,
2873
+ gatekeeper: 10
2529
2874
  };
2530
2875
  DIM2 = "\x1B[2m";
2531
2876
  RESET2 = "\x1B[0m";
@@ -2587,11 +2932,13 @@ async function resolve(db, exp, projectRoot) {
2587
2932
  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."
2588
2933
  }, projectRoot);
2589
2934
  const guidanceText = guidance.structured?.guidance ?? guidance.output;
2590
- storeBuilderGuidance(db, exp.id, guidanceText);
2591
- updateExperimentStatus(db, exp.id, "building");
2592
- if (exp.sub_type) {
2593
- incrementSubTypeFailure(db, exp.sub_type, exp.id, "weak");
2594
- }
2935
+ db.transaction(() => {
2936
+ storeBuilderGuidance(db, exp.id, guidanceText);
2937
+ updateExperimentStatus(db, exp.id, "building");
2938
+ if (exp.sub_type) {
2939
+ incrementSubTypeFailure(db, exp.sub_type, exp.id, "weak");
2940
+ }
2941
+ })();
2595
2942
  warn(`Experiment ${exp.slug} CYCLING BACK (weak). Guidance generated for builder.`);
2596
2943
  break;
2597
2944
  }
@@ -2599,18 +2946,21 @@ async function resolve(db, exp, projectRoot) {
2599
2946
  gitRevert(exp.branch, projectRoot);
2600
2947
  const rejectedComponents = grades.filter((g) => g.grade === "rejected");
2601
2948
  const whyFailed = rejectedComponents.map((r) => r.notes ?? "rejected").join("; ");
2602
- insertDeadEnd(
2603
- db,
2604
- exp.id,
2605
- exp.hypothesis ?? exp.slug,
2606
- whyFailed,
2607
- `Approach rejected: ${whyFailed}`,
2608
- exp.sub_type
2609
- );
2610
- updateExperimentStatus(db, exp.id, "dead_end");
2611
- if (exp.sub_type) {
2612
- incrementSubTypeFailure(db, exp.sub_type, exp.id, "rejected");
2613
- }
2949
+ db.transaction(() => {
2950
+ insertDeadEnd(
2951
+ db,
2952
+ exp.id,
2953
+ exp.hypothesis ?? exp.slug,
2954
+ whyFailed,
2955
+ `Approach rejected: ${whyFailed}`,
2956
+ exp.sub_type,
2957
+ "structural"
2958
+ );
2959
+ updateExperimentStatus(db, exp.id, "dead_end");
2960
+ if (exp.sub_type) {
2961
+ incrementSubTypeFailure(db, exp.sub_type, exp.id, "rejected");
2962
+ }
2963
+ })();
2614
2964
  info(`Experiment ${exp.slug} DEAD-ENDED (rejected). Constraint recorded.`);
2615
2965
  break;
2616
2966
  }
@@ -2649,23 +2999,23 @@ function gitRevert(branch, cwd) {
2649
2999
  }
2650
3000
  }
2651
3001
  function appendToFragilityMap(projectRoot, expSlug, gaps) {
2652
- const fragPath = path8.join(projectRoot, "docs", "synthesis", "fragility.md");
3002
+ const fragPath = path7.join(projectRoot, "docs", "synthesis", "fragility.md");
2653
3003
  let content = "";
2654
- if (fs8.existsSync(fragPath)) {
2655
- content = fs8.readFileSync(fragPath, "utf-8");
3004
+ if (fs7.existsSync(fragPath)) {
3005
+ content = fs7.readFileSync(fragPath, "utf-8");
2656
3006
  }
2657
3007
  const entry = `
2658
3008
  ## From experiment: ${expSlug}
2659
3009
  ${gaps}
2660
3010
  `;
2661
- fs8.writeFileSync(fragPath, content + entry);
3011
+ fs7.writeFileSync(fragPath, content + entry);
2662
3012
  }
2663
- var fs8, path8, import_node_child_process3;
3013
+ var fs7, path7, import_node_child_process3;
2664
3014
  var init_resolve = __esm({
2665
3015
  "src/resolve.ts"() {
2666
3016
  "use strict";
2667
- fs8 = __toESM(require("fs"));
2668
- path8 = __toESM(require("path"));
3017
+ fs7 = __toESM(require("fs"));
3018
+ path7 = __toESM(require("path"));
2669
3019
  init_types();
2670
3020
  init_queries();
2671
3021
  init_spawn();
@@ -2696,6 +3046,8 @@ async function cycle(step, args) {
2696
3046
  return doScout(db, exp, root);
2697
3047
  case "verify":
2698
3048
  return doVerify(db, exp, root);
3049
+ case "gate":
3050
+ return doGate(db, exp, root);
2699
3051
  case "compress":
2700
3052
  return doCompress(db, root);
2701
3053
  }
@@ -2709,15 +3061,88 @@ async function resolveCmd(args) {
2709
3061
  await resolve(db, exp, root);
2710
3062
  updateExperimentStatus(db, exp.id, "resolved");
2711
3063
  }
3064
+ async function doGate(db, exp, root) {
3065
+ transition(exp.status, "gated" /* GATED */);
3066
+ const synthesis = truncateContext(readFileOrEmpty(path8.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
3067
+ const fragility = truncateContext(readFileOrEmpty(path8.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
3068
+ const structuralDeadEnds = exp.sub_type ? listStructuralDeadEndsBySubType(db, exp.sub_type) : listStructuralDeadEnds(db);
3069
+ const result = await spawnAgent("gatekeeper", {
3070
+ experiment: {
3071
+ id: exp.id,
3072
+ slug: exp.slug,
3073
+ hypothesis: exp.hypothesis,
3074
+ status: exp.status,
3075
+ sub_type: exp.sub_type,
3076
+ builder_guidance: null
3077
+ },
3078
+ deadEnds: structuralDeadEnds.map((d) => ({
3079
+ approach: d.approach,
3080
+ why_failed: d.why_failed,
3081
+ structural_constraint: d.structural_constraint
3082
+ })),
3083
+ fragility,
3084
+ synthesis,
3085
+ taskPrompt: `Gate-check hypothesis for experiment ${exp.slug}:
3086
+ "${exp.hypothesis}"
3087
+
3088
+ Check: (a) stale references \u2014 does the hypothesis reference specific lines, functions, or structures that may not exist? (b) dead-end overlap \u2014 does this hypothesis repeat an approach already ruled out by structural dead-ends? (c) scope \u2014 is this a single focused change, or does it try to do multiple things?
3089
+
3090
+ Output your gate_decision as "approve", "reject", or "flag" with reasoning.`
3091
+ }, root);
3092
+ ingestStructuredOutput(db, exp.id, result.structured);
3093
+ const decision = result.structured?.gate_decision ?? "approve";
3094
+ const reason = result.structured?.reason ?? "";
3095
+ if (decision === "reject") {
3096
+ updateExperimentStatus(db, exp.id, "gated");
3097
+ warn(`Gate REJECTED for ${exp.slug}: ${reason}`);
3098
+ warn(`Revise the hypothesis or run \`majlis revert\` to abandon.`);
3099
+ } else {
3100
+ if (decision === "flag") {
3101
+ warn(`Gate flagged concerns for ${exp.slug}: ${reason}`);
3102
+ }
3103
+ updateExperimentStatus(db, exp.id, "gated");
3104
+ success(`Gate passed for ${exp.slug}. Run \`majlis build\` next.`);
3105
+ }
3106
+ }
2712
3107
  async function doBuild(db, exp, root) {
2713
3108
  transition(exp.status, "building" /* BUILDING */);
2714
3109
  const deadEnds = exp.sub_type ? listDeadEndsBySubType(db, exp.sub_type) : listAllDeadEnds(db);
2715
3110
  const builderGuidance = getBuilderGuidance(db, exp.id);
2716
- const fragilityPath = path9.join(root, "docs", "synthesis", "fragility.md");
2717
- const fragility = fs9.existsSync(fragilityPath) ? fs9.readFileSync(fragilityPath, "utf-8") : "";
2718
- const synthesisPath = path9.join(root, "docs", "synthesis", "current.md");
2719
- const synthesis = fs9.existsSync(synthesisPath) ? fs9.readFileSync(synthesisPath, "utf-8") : "";
3111
+ const fragility = truncateContext(readFileOrEmpty(path8.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
3112
+ const synthesis = truncateContext(readFileOrEmpty(path8.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
3113
+ const confirmedDoubts = getConfirmedDoubts(db, exp.id);
3114
+ const config = loadConfig(root);
3115
+ const existingBaseline = getMetricsByExperimentAndPhase(db, exp.id, "before");
3116
+ if (config.metrics?.command && existingBaseline.length === 0) {
3117
+ try {
3118
+ const output = (0, import_node_child_process4.execSync)(config.metrics.command, {
3119
+ cwd: root,
3120
+ encoding: "utf-8",
3121
+ timeout: 6e4,
3122
+ stdio: ["pipe", "pipe", "pipe"]
3123
+ }).trim();
3124
+ const parsed = parseMetricsOutput(output);
3125
+ for (const m of parsed) {
3126
+ insertMetric(db, exp.id, "before", m.fixture, m.metric_name, m.metric_value);
3127
+ }
3128
+ if (parsed.length > 0) info(`Captured ${parsed.length} baseline metric(s).`);
3129
+ } catch {
3130
+ warn("Could not capture baseline metrics.");
3131
+ }
3132
+ }
2720
3133
  updateExperimentStatus(db, exp.id, "building");
3134
+ let taskPrompt = builderGuidance ? `Previous attempt was weak. Here is guidance for this attempt:
3135
+ ${builderGuidance}
3136
+
3137
+ Build the experiment: ${exp.hypothesis}` : `Build the experiment: ${exp.hypothesis}`;
3138
+ if (confirmedDoubts.length > 0) {
3139
+ taskPrompt += "\n\n## Confirmed Doubts (MUST address)\nThese weaknesses were confirmed by the verifier. Your build MUST address each one:\n";
3140
+ for (const d of confirmedDoubts) {
3141
+ taskPrompt += `- [${d.severity}] ${d.claim_doubted}: ${d.evidence_for_doubt}
3142
+ `;
3143
+ }
3144
+ }
3145
+ taskPrompt += "\n\nNote: The framework captures metrics automatically. Do NOT claim specific numbers unless quoting framework output.";
2721
3146
  const result = await spawnAgent("builder", {
2722
3147
  experiment: {
2723
3148
  id: exp.id,
@@ -2734,10 +3159,8 @@ async function doBuild(db, exp, root) {
2734
3159
  })),
2735
3160
  fragility,
2736
3161
  synthesis,
2737
- taskPrompt: builderGuidance ? `Previous attempt was weak. Here is guidance for this attempt:
2738
- ${builderGuidance}
2739
-
2740
- Build the experiment: ${exp.hypothesis}` : `Build the experiment: ${exp.hypothesis}`
3162
+ confirmedDoubts,
3163
+ taskPrompt
2741
3164
  }, root);
2742
3165
  ingestStructuredOutput(db, exp.id, result.structured);
2743
3166
  if (result.truncated && !result.structured) {
@@ -2747,6 +3170,23 @@ Build the experiment: ${exp.hypothesis}` : `Build the experiment: ${exp.hypothes
2747
3170
  }, root);
2748
3171
  warn(`Experiment stays at 'building'. Run \`majlis build\` to retry or \`majlis revert\` to abandon.`);
2749
3172
  } else {
3173
+ if (config.metrics?.command) {
3174
+ try {
3175
+ const output = (0, import_node_child_process4.execSync)(config.metrics.command, {
3176
+ cwd: root,
3177
+ encoding: "utf-8",
3178
+ timeout: 6e4,
3179
+ stdio: ["pipe", "pipe", "pipe"]
3180
+ }).trim();
3181
+ const parsed = parseMetricsOutput(output);
3182
+ for (const m of parsed) {
3183
+ insertMetric(db, exp.id, "after", m.fixture, m.metric_name, m.metric_value);
3184
+ }
3185
+ if (parsed.length > 0) info(`Captured ${parsed.length} post-build metric(s).`);
3186
+ } catch {
3187
+ warn("Could not capture post-build metrics.");
3188
+ }
3189
+ }
2750
3190
  gitCommitBuild(exp, root);
2751
3191
  updateExperimentStatus(db, exp.id, "built");
2752
3192
  success(`Build complete for ${exp.slug}. Run \`majlis doubt\` or \`majlis challenge\` next.`);
@@ -2754,6 +3194,26 @@ Build the experiment: ${exp.hypothesis}` : `Build the experiment: ${exp.hypothes
2754
3194
  }
2755
3195
  async function doChallenge(db, exp, root) {
2756
3196
  transition(exp.status, "challenged" /* CHALLENGED */);
3197
+ let gitDiff = "";
3198
+ try {
3199
+ gitDiff = (0, import_node_child_process4.execSync)('git diff main -- . ":!.majlis/"', {
3200
+ cwd: root,
3201
+ encoding: "utf-8",
3202
+ stdio: ["pipe", "pipe", "pipe"]
3203
+ }).trim();
3204
+ } catch {
3205
+ }
3206
+ if (gitDiff.length > 8e3) gitDiff = gitDiff.slice(0, 8e3) + "\n[DIFF TRUNCATED]";
3207
+ const synthesis = truncateContext(readFileOrEmpty(path8.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
3208
+ let taskPrompt = `Construct adversarial test cases for experiment ${exp.slug}: ${exp.hypothesis}`;
3209
+ if (gitDiff) {
3210
+ taskPrompt += `
3211
+
3212
+ ## Code Changes (git diff main)
3213
+ \`\`\`diff
3214
+ ${gitDiff}
3215
+ \`\`\``;
3216
+ }
2757
3217
  const result = await spawnAgent("adversary", {
2758
3218
  experiment: {
2759
3219
  id: exp.id,
@@ -2763,7 +3223,8 @@ async function doChallenge(db, exp, root) {
2763
3223
  sub_type: exp.sub_type,
2764
3224
  builder_guidance: null
2765
3225
  },
2766
- taskPrompt: `Construct adversarial test cases for experiment ${exp.slug}: ${exp.hypothesis}`
3226
+ synthesis,
3227
+ taskPrompt
2767
3228
  }, root);
2768
3229
  ingestStructuredOutput(db, exp.id, result.structured);
2769
3230
  if (result.truncated && !result.structured) {
@@ -2775,6 +3236,20 @@ async function doChallenge(db, exp, root) {
2775
3236
  }
2776
3237
  async function doDoubt(db, exp, root) {
2777
3238
  transition(exp.status, "doubted" /* DOUBTED */);
3239
+ const paddedNum = String(exp.id).padStart(3, "0");
3240
+ const expDocPath = path8.join(root, "docs", "experiments", `${paddedNum}-${exp.slug}.md`);
3241
+ const experimentDoc = truncateContext(readFileOrEmpty(expDocPath), CONTEXT_LIMITS.experimentDoc);
3242
+ const synthesis = truncateContext(readFileOrEmpty(path8.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
3243
+ const deadEnds = exp.sub_type ? listDeadEndsBySubType(db, exp.sub_type) : listAllDeadEnds(db);
3244
+ let taskPrompt = `Doubt the work in experiment ${exp.slug}: ${exp.hypothesis}. Produce a doubt document with evidence for each doubt.`;
3245
+ if (experimentDoc) {
3246
+ taskPrompt += `
3247
+
3248
+ ## Experiment Document (builder's artifact)
3249
+ <experiment_doc>
3250
+ ${experimentDoc}
3251
+ </experiment_doc>`;
3252
+ }
2778
3253
  const result = await spawnAgent("critic", {
2779
3254
  experiment: {
2780
3255
  id: exp.id,
@@ -2785,7 +3260,13 @@ async function doDoubt(db, exp, root) {
2785
3260
  builder_guidance: null
2786
3261
  // Critic does NOT see builder reasoning
2787
3262
  },
2788
- taskPrompt: `Doubt the work in experiment ${exp.slug}: ${exp.hypothesis}. Produce a doubt document with evidence for each doubt.`
3263
+ synthesis,
3264
+ deadEnds: deadEnds.map((d) => ({
3265
+ approach: d.approach,
3266
+ why_failed: d.why_failed,
3267
+ structural_constraint: d.structural_constraint
3268
+ })),
3269
+ taskPrompt
2789
3270
  }, root);
2790
3271
  ingestStructuredOutput(db, exp.id, result.structured);
2791
3272
  if (result.truncated && !result.structured) {
@@ -2797,35 +3278,91 @@ async function doDoubt(db, exp, root) {
2797
3278
  }
2798
3279
  async function doScout(db, exp, root) {
2799
3280
  transition(exp.status, "scouted" /* SCOUTED */);
2800
- const synthesisPath = path9.join(root, "docs", "synthesis", "current.md");
2801
- const synthesis = fs9.existsSync(synthesisPath) ? fs9.readFileSync(synthesisPath, "utf-8") : "";
2802
- updateExperimentStatus(db, exp.id, "scouted");
3281
+ const synthesis = truncateContext(readFileOrEmpty(path8.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
3282
+ const fragility = truncateContext(readFileOrEmpty(path8.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
3283
+ const deadEnds = exp.sub_type ? listDeadEndsBySubType(db, exp.sub_type) : listAllDeadEnds(db);
3284
+ const deadEndsSummary = deadEnds.map(
3285
+ (d) => `- [${d.category ?? "structural"}] ${d.approach}: ${d.why_failed}`
3286
+ ).join("\n");
3287
+ let taskPrompt = `Search for alternative approaches to the problem in experiment ${exp.slug}: ${exp.hypothesis}. Look for contradictory approaches, solutions from other fields, and known limitations of the current approach.`;
3288
+ if (deadEndsSummary) {
3289
+ taskPrompt += `
3290
+
3291
+ ## Known Dead Ends (avoid these approaches)
3292
+ ${deadEndsSummary}`;
3293
+ }
3294
+ if (fragility) {
3295
+ taskPrompt += `
3296
+
3297
+ ## Fragility Map (target these weak areas)
3298
+ ${fragility}`;
3299
+ }
2803
3300
  const result = await spawnAgent("scout", {
2804
3301
  experiment: {
2805
3302
  id: exp.id,
2806
3303
  slug: exp.slug,
2807
3304
  hypothesis: exp.hypothesis,
2808
- status: "scouted",
3305
+ status: exp.status,
2809
3306
  sub_type: exp.sub_type,
2810
3307
  builder_guidance: null
2811
3308
  },
2812
3309
  synthesis,
2813
- taskPrompt: `Search for alternative approaches to the problem in experiment ${exp.slug}: ${exp.hypothesis}. Look for contradictory approaches, solutions from other fields, and known limitations of the current approach.`
3310
+ fragility,
3311
+ deadEnds: deadEnds.map((d) => ({
3312
+ approach: d.approach,
3313
+ why_failed: d.why_failed,
3314
+ structural_constraint: d.structural_constraint
3315
+ })),
3316
+ taskPrompt
2814
3317
  }, root);
2815
3318
  ingestStructuredOutput(db, exp.id, result.structured);
3319
+ if (result.truncated && !result.structured) {
3320
+ warn(`Scout was truncated without structured output. Experiment stays at current status.`);
3321
+ return;
3322
+ }
3323
+ updateExperimentStatus(db, exp.id, "scouted");
2816
3324
  success(`Scout pass complete for ${exp.slug}. Run \`majlis verify\` next.`);
2817
3325
  }
2818
3326
  async function doVerify(db, exp, root) {
2819
3327
  transition(exp.status, "verifying" /* VERIFYING */);
2820
3328
  const doubts = getDoubtsByExperiment(db, exp.id);
2821
- const challengeDir = path9.join(root, "docs", "challenges");
3329
+ const challengeDir = path8.join(root, "docs", "challenges");
2822
3330
  let challenges = "";
2823
- if (fs9.existsSync(challengeDir)) {
2824
- const files = fs9.readdirSync(challengeDir).filter((f) => f.includes(exp.slug) && f.endsWith(".md"));
3331
+ if (fs8.existsSync(challengeDir)) {
3332
+ const files = fs8.readdirSync(challengeDir).filter((f) => f.includes(exp.slug) && f.endsWith(".md"));
2825
3333
  for (const f of files) {
2826
- challenges += fs9.readFileSync(path9.join(challengeDir, f), "utf-8") + "\n\n";
3334
+ challenges += fs8.readFileSync(path8.join(challengeDir, f), "utf-8") + "\n\n";
2827
3335
  }
2828
3336
  }
3337
+ const beforeMetrics = getMetricsByExperimentAndPhase(db, exp.id, "before");
3338
+ const afterMetrics = getMetricsByExperimentAndPhase(db, exp.id, "after");
3339
+ let metricsSection = "";
3340
+ if (beforeMetrics.length > 0 || afterMetrics.length > 0) {
3341
+ metricsSection = "\n\n## Framework-Captured Metrics (GROUND TRUTH \u2014 not self-reported by builder)\n";
3342
+ if (beforeMetrics.length > 0) {
3343
+ metricsSection += "### Before Build\n";
3344
+ for (const m of beforeMetrics) {
3345
+ metricsSection += `- ${m.fixture} / ${m.metric_name}: ${m.metric_value}
3346
+ `;
3347
+ }
3348
+ }
3349
+ if (afterMetrics.length > 0) {
3350
+ metricsSection += "### After Build\n";
3351
+ for (const m of afterMetrics) {
3352
+ metricsSection += `- ${m.fixture} / ${m.metric_name}: ${m.metric_value}
3353
+ `;
3354
+ }
3355
+ }
3356
+ }
3357
+ let doubtReference = "";
3358
+ if (doubts.length > 0) {
3359
+ doubtReference = "\n\n## Doubt Reference (use these IDs in doubt_resolutions)\n";
3360
+ for (const d of doubts) {
3361
+ doubtReference += `- DOUBT-${d.id}: [${d.severity}] ${d.claim_doubted}
3362
+ `;
3363
+ }
3364
+ doubtReference += "\nWhen resolving doubts, use the DOUBT-{id} number as the doubt_id value in your doubt_resolutions output.";
3365
+ }
2829
3366
  updateExperimentStatus(db, exp.id, "verifying");
2830
3367
  const result = await spawnAgent("verifier", {
2831
3368
  experiment: {
@@ -2838,7 +3375,7 @@ async function doVerify(db, exp, root) {
2838
3375
  },
2839
3376
  doubts,
2840
3377
  challenges,
2841
- taskPrompt: `Verify experiment ${exp.slug}: ${exp.hypothesis}. Check provenance and content. Test the ${doubts.length} doubt(s) and any adversarial challenges.`
3378
+ taskPrompt: `Verify experiment ${exp.slug}: ${exp.hypothesis}. Check provenance and content. Test the ${doubts.length} doubt(s) and any adversarial challenges.` + metricsSection + doubtReference
2842
3379
  }, root);
2843
3380
  ingestStructuredOutput(db, exp.id, result.structured);
2844
3381
  if (result.truncated && !result.structured) {
@@ -2846,9 +3383,15 @@ async function doVerify(db, exp, root) {
2846
3383
  return;
2847
3384
  }
2848
3385
  if (result.structured?.doubt_resolutions) {
2849
- for (const dr of result.structured.doubt_resolutions) {
2850
- if (dr.doubt_id && dr.resolution) {
3386
+ const knownDoubtIds = new Set(doubts.map((d) => d.id));
3387
+ for (let i = 0; i < result.structured.doubt_resolutions.length; i++) {
3388
+ const dr = result.structured.doubt_resolutions[i];
3389
+ if (!dr.resolution) continue;
3390
+ if (dr.doubt_id && knownDoubtIds.has(dr.doubt_id)) {
2851
3391
  updateDoubtResolution(db, dr.doubt_id, dr.resolution);
3392
+ } else if (doubts[i]) {
3393
+ warn(`Doubt resolution ID ${dr.doubt_id} not found. Using ordinal fallback \u2192 DOUBT-${doubts[i].id}.`);
3394
+ updateDoubtResolution(db, doubts[i].id, dr.resolution);
2852
3395
  }
2853
3396
  }
2854
3397
  }
@@ -2856,13 +3399,14 @@ async function doVerify(db, exp, root) {
2856
3399
  success(`Verification complete for ${exp.slug}. Run \`majlis resolve\` next.`);
2857
3400
  }
2858
3401
  async function doCompress(db, root) {
2859
- const synthesisPath = path9.join(root, "docs", "synthesis", "current.md");
2860
- const sizeBefore = fs9.existsSync(synthesisPath) ? fs9.statSync(synthesisPath).size : 0;
3402
+ const synthesisPath = path8.join(root, "docs", "synthesis", "current.md");
3403
+ const sizeBefore = fs8.existsSync(synthesisPath) ? fs8.statSync(synthesisPath).size : 0;
2861
3404
  const sessionCount = getSessionsSinceCompression(db);
3405
+ const dbExport = exportForCompressor(db);
2862
3406
  const result = await spawnAgent("compressor", {
2863
- taskPrompt: "Read ALL experiments, decisions, doubts, challenges, verification reports, reframes, and recent diffs. Cross-reference for contradictions, redundancies, and patterns. REWRITE docs/synthesis/current.md \u2014 shorter and denser. Update docs/synthesis/fragility.md with current weak areas. Update docs/synthesis/dead-ends.md with structural constraints from rejected experiments."
3407
+ taskPrompt: "## Structured Data (CANONICAL \u2014 from SQLite database)\nThe database export below is the source of truth. docs/ files are agent artifacts that may contain stale or incorrect information. Cross-reference everything against this data.\n\n" + dbExport + "\n\n## Your Task\nRead ALL experiments, decisions, doubts, challenges, verification reports, reframes, and recent diffs. Cross-reference for contradictions, redundancies, and patterns. REWRITE docs/synthesis/current.md \u2014 shorter and denser. Update docs/synthesis/fragility.md with current weak areas. Update docs/synthesis/dead-ends.md with structural constraints from rejected experiments."
2864
3408
  }, root);
2865
- const sizeAfter = fs9.existsSync(synthesisPath) ? fs9.statSync(synthesisPath).size : 0;
3409
+ const sizeAfter = fs8.existsSync(synthesisPath) ? fs8.statSync(synthesisPath).size : 0;
2866
3410
  recordCompression(db, sessionCount, sizeBefore, sizeAfter);
2867
3411
  success(`Compression complete. Synthesis: ${sizeBefore}B \u2192 ${sizeAfter}B`);
2868
3412
  }
@@ -2936,13 +3480,29 @@ function ingestStructuredOutput(db, experimentId, structured) {
2936
3480
  }
2937
3481
  info(`Ingested ${structured.challenges.length} challenge(s)`);
2938
3482
  }
3483
+ if (structured.reframe) {
3484
+ insertReframe(
3485
+ db,
3486
+ experimentId,
3487
+ structured.reframe.decomposition,
3488
+ JSON.stringify(structured.reframe.divergences),
3489
+ structured.reframe.recommendation
3490
+ );
3491
+ info(`Ingested reframe`);
3492
+ }
3493
+ if (structured.findings) {
3494
+ for (const f of structured.findings) {
3495
+ insertFinding(db, experimentId, f.approach, f.source, f.relevance, f.contradicts_current);
3496
+ }
3497
+ info(`Ingested ${structured.findings.length} finding(s)`);
3498
+ }
2939
3499
  }
2940
- var fs9, path9, import_node_child_process4;
3500
+ var fs8, path8, import_node_child_process4;
2941
3501
  var init_cycle = __esm({
2942
3502
  "src/commands/cycle.ts"() {
2943
3503
  "use strict";
2944
- fs9 = __toESM(require("fs"));
2945
- path9 = __toESM(require("path"));
3504
+ fs8 = __toESM(require("fs"));
3505
+ path8 = __toESM(require("path"));
2946
3506
  import_node_child_process4 = require("child_process");
2947
3507
  init_connection();
2948
3508
  init_queries();
@@ -2950,7 +3510,8 @@ var init_cycle = __esm({
2950
3510
  init_types();
2951
3511
  init_spawn();
2952
3512
  init_resolve();
2953
- init_queries();
3513
+ init_config();
3514
+ init_metrics();
2954
3515
  init_format();
2955
3516
  }
2956
3517
  });
@@ -2968,10 +3529,10 @@ async function classify(args) {
2968
3529
  if (!domain) {
2969
3530
  throw new Error('Usage: majlis classify "domain description"');
2970
3531
  }
2971
- const synthesisPath = path10.join(root, "docs", "synthesis", "current.md");
2972
- const synthesis = fs10.existsSync(synthesisPath) ? fs10.readFileSync(synthesisPath, "utf-8") : "";
2973
- const deadEndsPath = path10.join(root, "docs", "synthesis", "dead-ends.md");
2974
- const deadEnds = fs10.existsSync(deadEndsPath) ? fs10.readFileSync(deadEndsPath, "utf-8") : "";
3532
+ const synthesisPath = path9.join(root, "docs", "synthesis", "current.md");
3533
+ const synthesis = fs9.existsSync(synthesisPath) ? fs9.readFileSync(synthesisPath, "utf-8") : "";
3534
+ const deadEndsPath = path9.join(root, "docs", "synthesis", "dead-ends.md");
3535
+ const deadEnds = fs9.existsSync(deadEndsPath) ? fs9.readFileSync(deadEndsPath, "utf-8") : "";
2975
3536
  info(`Classifying problem domain: ${domain}`);
2976
3537
  const result = await spawnAgent("builder", {
2977
3538
  synthesis,
@@ -2989,22 +3550,22 @@ Write the classification to docs/classification/ following the template.`
2989
3550
  async function reframe(args) {
2990
3551
  const root = findProjectRoot();
2991
3552
  if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
2992
- const classificationDir = path10.join(root, "docs", "classification");
3553
+ const classificationDir = path9.join(root, "docs", "classification");
2993
3554
  let classificationContent = "";
2994
- if (fs10.existsSync(classificationDir)) {
2995
- const files = fs10.readdirSync(classificationDir).filter((f) => f.endsWith(".md") && !f.startsWith("_"));
3555
+ if (fs9.existsSync(classificationDir)) {
3556
+ const files = fs9.readdirSync(classificationDir).filter((f) => f.endsWith(".md") && !f.startsWith("_"));
2996
3557
  for (const f of files) {
2997
- classificationContent += fs10.readFileSync(path10.join(classificationDir, f), "utf-8") + "\n\n";
3558
+ classificationContent += fs9.readFileSync(path9.join(classificationDir, f), "utf-8") + "\n\n";
2998
3559
  }
2999
3560
  }
3000
- const synthesisPath = path10.join(root, "docs", "synthesis", "current.md");
3001
- const synthesis = fs10.existsSync(synthesisPath) ? fs10.readFileSync(synthesisPath, "utf-8") : "";
3002
- const deadEndsPath = path10.join(root, "docs", "synthesis", "dead-ends.md");
3003
- const deadEnds = fs10.existsSync(deadEndsPath) ? fs10.readFileSync(deadEndsPath, "utf-8") : "";
3004
- const configPath = path10.join(root, ".majlis", "config.json");
3561
+ const synthesisPath = path9.join(root, "docs", "synthesis", "current.md");
3562
+ const synthesis = fs9.existsSync(synthesisPath) ? fs9.readFileSync(synthesisPath, "utf-8") : "";
3563
+ const deadEndsPath = path9.join(root, "docs", "synthesis", "dead-ends.md");
3564
+ const deadEnds = fs9.existsSync(deadEndsPath) ? fs9.readFileSync(deadEndsPath, "utf-8") : "";
3565
+ const configPath = path9.join(root, ".majlis", "config.json");
3005
3566
  let problemStatement = "";
3006
- if (fs10.existsSync(configPath)) {
3007
- const config = JSON.parse(fs10.readFileSync(configPath, "utf-8"));
3567
+ if (fs9.existsSync(configPath)) {
3568
+ const config = JSON.parse(fs9.readFileSync(configPath, "utf-8"));
3008
3569
  problemStatement = `${config.project?.description ?? ""}
3009
3570
  Objective: ${config.project?.objective ?? ""}`;
3010
3571
  }
@@ -3028,12 +3589,12 @@ Write to docs/reframes/.`
3028
3589
  }, root);
3029
3590
  success("Reframe complete. Check docs/reframes/ for the output.");
3030
3591
  }
3031
- var fs10, path10;
3592
+ var fs9, path9;
3032
3593
  var init_classify = __esm({
3033
3594
  "src/commands/classify.ts"() {
3034
3595
  "use strict";
3035
- fs10 = __toESM(require("fs"));
3036
- path10 = __toESM(require("path"));
3596
+ fs9 = __toESM(require("fs"));
3597
+ path9 = __toESM(require("path"));
3037
3598
  init_connection();
3038
3599
  init_spawn();
3039
3600
  init_format();
@@ -3050,20 +3611,19 @@ async function audit(args) {
3050
3611
  if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
3051
3612
  const db = getDb(root);
3052
3613
  const objective = args.filter((a) => !a.startsWith("--")).join(" ");
3053
- const config = loadConfig5(root);
3614
+ const config = loadConfig(root);
3054
3615
  const experiments = listAllExperiments(db);
3055
3616
  const deadEnds = listAllDeadEnds(db);
3056
3617
  const circuitBreakers = getAllCircuitBreakerStates(db, config.cycle.circuit_breaker_threshold);
3057
- const classificationDir = path11.join(root, "docs", "classification");
3618
+ const classificationDir = path10.join(root, "docs", "classification");
3058
3619
  let classification = "";
3059
- if (fs11.existsSync(classificationDir)) {
3060
- const files = fs11.readdirSync(classificationDir).filter((f) => f.endsWith(".md") && !f.startsWith("_"));
3620
+ if (fs10.existsSync(classificationDir)) {
3621
+ const files = fs10.readdirSync(classificationDir).filter((f) => f.endsWith(".md") && !f.startsWith("_"));
3061
3622
  for (const f of files) {
3062
- classification += fs11.readFileSync(path11.join(classificationDir, f), "utf-8") + "\n\n";
3623
+ classification += fs10.readFileSync(path10.join(classificationDir, f), "utf-8") + "\n\n";
3063
3624
  }
3064
3625
  }
3065
- const synthesisPath = path11.join(root, "docs", "synthesis", "current.md");
3066
- const synthesis = fs11.existsSync(synthesisPath) ? fs11.readFileSync(synthesisPath, "utf-8") : "";
3626
+ const synthesis = readFileOrEmpty(path10.join(root, "docs", "synthesis", "current.md"));
3067
3627
  header("Maqasid Check \u2014 Purpose Audit");
3068
3628
  const trippedBreakers = circuitBreakers.filter((cb) => cb.tripped);
3069
3629
  if (trippedBreakers.length > 0) {
@@ -3107,22 +3667,16 @@ Output: either "classification confirmed \u2014 continue" or "re-classify from X
3107
3667
  }, root);
3108
3668
  success("Purpose audit complete. Review the output above.");
3109
3669
  }
3110
- function loadConfig5(projectRoot) {
3111
- const configPath = path11.join(projectRoot, ".majlis", "config.json");
3112
- if (!fs11.existsSync(configPath)) {
3113
- return { project: { name: "", description: "", objective: "" }, cycle: { circuit_breaker_threshold: 3 } };
3114
- }
3115
- return JSON.parse(fs11.readFileSync(configPath, "utf-8"));
3116
- }
3117
- var fs11, path11;
3670
+ var fs10, path10;
3118
3671
  var init_audit = __esm({
3119
3672
  "src/commands/audit.ts"() {
3120
3673
  "use strict";
3121
- fs11 = __toESM(require("fs"));
3122
- path11 = __toESM(require("path"));
3674
+ fs10 = __toESM(require("fs"));
3675
+ path10 = __toESM(require("path"));
3123
3676
  init_connection();
3124
3677
  init_queries();
3125
3678
  init_spawn();
3679
+ init_config();
3126
3680
  init_format();
3127
3681
  }
3128
3682
  });
@@ -3136,7 +3690,7 @@ async function next(args, isJson) {
3136
3690
  const root = findProjectRoot();
3137
3691
  if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
3138
3692
  const db = getDb(root);
3139
- const config = loadConfig6(root);
3693
+ const config = loadConfig(root);
3140
3694
  const slugArg = args.filter((a) => !a.startsWith("--"))[0];
3141
3695
  let exp;
3142
3696
  if (slugArg) {
@@ -3168,7 +3722,17 @@ async function runNextStep(db, exp, config, root, isJson) {
3168
3722
  }
3169
3723
  if (exp.sub_type && checkCircuitBreaker(db, exp.sub_type, config.cycle.circuit_breaker_threshold)) {
3170
3724
  warn(`Circuit breaker: ${exp.sub_type} has ${config.cycle.circuit_breaker_threshold}+ failures.`);
3171
- warn("Triggering Maqasid Check (purpose audit).");
3725
+ insertDeadEnd(
3726
+ db,
3727
+ exp.id,
3728
+ exp.hypothesis ?? exp.slug,
3729
+ `Circuit breaker tripped for ${exp.sub_type}`,
3730
+ `Sub-type ${exp.sub_type} exceeded ${config.cycle.circuit_breaker_threshold} failures`,
3731
+ exp.sub_type,
3732
+ "procedural"
3733
+ );
3734
+ updateExperimentStatus(db, exp.id, "dead_end");
3735
+ warn("Experiment dead-ended. Triggering Maqasid Check (purpose audit).");
3172
3736
  await audit([config.project?.objective ?? ""]);
3173
3737
  return;
3174
3738
  }
@@ -3208,6 +3772,16 @@ async function runAutoLoop(db, exp, config, root, isJson) {
3208
3772
  }
3209
3773
  if (exp.sub_type && checkCircuitBreaker(db, exp.sub_type, config.cycle.circuit_breaker_threshold)) {
3210
3774
  warn(`Circuit breaker tripped for ${exp.sub_type}. Stopping auto mode.`);
3775
+ insertDeadEnd(
3776
+ db,
3777
+ exp.id,
3778
+ exp.hypothesis ?? exp.slug,
3779
+ `Circuit breaker tripped for ${exp.sub_type}`,
3780
+ `Sub-type ${exp.sub_type} exceeded ${config.cycle.circuit_breaker_threshold} failures`,
3781
+ exp.sub_type,
3782
+ "procedural"
3783
+ );
3784
+ updateExperimentStatus(db, exp.id, "dead_end");
3211
3785
  await audit([config.project?.objective ?? ""]);
3212
3786
  break;
3213
3787
  }
@@ -3249,41 +3823,26 @@ async function executeStep(step, exp, root) {
3249
3823
  updateExperimentStatus(getDb(root), exp.id, "compressed");
3250
3824
  info(`Experiment ${exp.slug} compressed.`);
3251
3825
  break;
3826
+ case "gated" /* GATED */:
3827
+ await cycle("gate", expArgs);
3828
+ break;
3252
3829
  case "reframed" /* REFRAMED */:
3253
3830
  updateExperimentStatus(getDb(root), exp.id, "reframed");
3254
- info(`Reframe acknowledged for ${exp.slug}. Proceeding to build.`);
3831
+ info(`Reframe acknowledged for ${exp.slug}. Proceeding to gate.`);
3255
3832
  break;
3256
3833
  default:
3257
3834
  warn(`Don't know how to execute step: ${step}`);
3258
3835
  }
3259
3836
  }
3260
- function loadConfig6(projectRoot) {
3261
- const configPath = path12.join(projectRoot, ".majlis", "config.json");
3262
- if (!fs12.existsSync(configPath)) {
3263
- return {
3264
- project: { name: "", description: "", objective: "" },
3265
- cycle: {
3266
- compression_interval: 5,
3267
- circuit_breaker_threshold: 3,
3268
- require_doubt_before_verify: true,
3269
- require_challenge_before_verify: false,
3270
- auto_baseline_on_new_experiment: true
3271
- }
3272
- };
3273
- }
3274
- return JSON.parse(fs12.readFileSync(configPath, "utf-8"));
3275
- }
3276
- var fs12, path12;
3277
3837
  var init_next = __esm({
3278
3838
  "src/commands/next.ts"() {
3279
3839
  "use strict";
3280
- fs12 = __toESM(require("fs"));
3281
- path12 = __toESM(require("path"));
3282
3840
  init_connection();
3283
3841
  init_queries();
3284
3842
  init_machine();
3285
3843
  init_types();
3286
3844
  init_queries();
3845
+ init_config();
3287
3846
  init_cycle();
3288
3847
  init_audit();
3289
3848
  init_format();
@@ -3303,13 +3862,19 @@ async function run(args) {
3303
3862
  throw new Error('Usage: majlis run "goal description"');
3304
3863
  }
3305
3864
  const db = getDb(root);
3306
- const config = loadConfig7(root);
3865
+ const config = loadConfig(root);
3307
3866
  const MAX_EXPERIMENTS = 10;
3308
3867
  const MAX_STEPS = 200;
3309
3868
  let experimentCount = 0;
3310
3869
  let stepCount = 0;
3870
+ let consecutiveFailures = 0;
3871
+ const usedHypotheses = /* @__PURE__ */ new Set();
3311
3872
  header(`Autonomous Mode \u2014 ${goal}`);
3312
3873
  while (stepCount < MAX_STEPS && experimentCount < MAX_EXPERIMENTS) {
3874
+ if (isShutdownRequested()) {
3875
+ warn("Shutdown requested. Stopping autonomous mode.");
3876
+ break;
3877
+ }
3313
3878
  stepCount++;
3314
3879
  let exp = getLatestExperiment(db);
3315
3880
  if (!exp) {
@@ -3329,6 +3894,11 @@ async function run(args) {
3329
3894
  success("Planner says the goal has been met. Stopping.");
3330
3895
  break;
3331
3896
  }
3897
+ if (usedHypotheses.has(hypothesis)) {
3898
+ warn(`Planner returned duplicate hypothesis: "${hypothesis.slice(0, 80)}". Stopping.`);
3899
+ break;
3900
+ }
3901
+ usedHypotheses.add(hypothesis);
3332
3902
  info(`Next hypothesis: ${hypothesis}`);
3333
3903
  exp = createNewExperiment(db, root, hypothesis);
3334
3904
  success(`Created experiment #${exp.id}: ${exp.slug}`);
@@ -3344,12 +3914,29 @@ async function run(args) {
3344
3914
  info(`[Step ${stepCount}] ${exp.slug}: ${exp.status}`);
3345
3915
  try {
3346
3916
  await next([exp.slug], false);
3917
+ consecutiveFailures = 0;
3347
3918
  } catch (err) {
3919
+ consecutiveFailures++;
3348
3920
  const message = err instanceof Error ? err.message : String(err);
3349
3921
  warn(`Step failed for ${exp.slug}: ${message}`);
3350
3922
  try {
3923
+ insertDeadEnd(
3924
+ db,
3925
+ exp.id,
3926
+ exp.hypothesis ?? exp.slug,
3927
+ message,
3928
+ `Process failure: ${message}`,
3929
+ exp.sub_type,
3930
+ "procedural"
3931
+ );
3351
3932
  updateExperimentStatus(db, exp.id, "dead_end");
3352
- } catch {
3933
+ } catch (innerErr) {
3934
+ const innerMsg = innerErr instanceof Error ? innerErr.message : String(innerErr);
3935
+ warn(`Could not record dead-end: ${innerMsg}`);
3936
+ }
3937
+ if (consecutiveFailures >= 3) {
3938
+ warn(`${consecutiveFailures} consecutive failures. Stopping autonomous mode.`);
3939
+ break;
3353
3940
  }
3354
3941
  }
3355
3942
  }
@@ -3362,11 +3949,11 @@ async function run(args) {
3362
3949
  info("Run `majlis status` to see final state.");
3363
3950
  }
3364
3951
  async function deriveNextHypothesis(goal, root, db) {
3365
- const synthesis = readFileOrEmpty(path13.join(root, "docs", "synthesis", "current.md"));
3366
- const fragility = readFileOrEmpty(path13.join(root, "docs", "synthesis", "fragility.md"));
3367
- const deadEndsDoc = readFileOrEmpty(path13.join(root, "docs", "synthesis", "dead-ends.md"));
3952
+ const synthesis = truncateContext(readFileOrEmpty(path11.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
3953
+ const fragility = truncateContext(readFileOrEmpty(path11.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
3954
+ const deadEndsDoc = truncateContext(readFileOrEmpty(path11.join(root, "docs", "synthesis", "dead-ends.md")), CONTEXT_LIMITS.deadEnds);
3368
3955
  const deadEnds = listAllDeadEnds(db);
3369
- const config = loadConfig7(root);
3956
+ const config = loadConfig(root);
3370
3957
  let metricsOutput = "";
3371
3958
  if (config.metrics?.command) {
3372
3959
  try {
@@ -3399,7 +3986,10 @@ ${fragility || "(none)"}
3399
3986
  ${deadEndsDoc || "(none)"}
3400
3987
 
3401
3988
  ## Dead Ends (from DB \u2014 ${deadEnds.length} total)
3402
- ${deadEnds.map((d) => `- ${d.approach}: ${d.why_failed} [constraint: ${d.structural_constraint}]`).join("\n") || "(none)"}
3989
+ ${deadEnds.map((d) => `- [${d.category ?? "structural"}] ${d.approach}: ${d.why_failed} [constraint: ${d.structural_constraint}]`).join("\n") || "(none)"}
3990
+
3991
+ Note: [structural] dead ends are HARD CONSTRAINTS \u2014 your hypothesis MUST NOT repeat these approaches.
3992
+ [procedural] dead ends are process failures \u2014 the approach may still be valid if executed differently.
3403
3993
 
3404
3994
  ## Your Task
3405
3995
  1. Assess: based on the metrics and synthesis, has the goal been met? Be specific.
@@ -3473,49 +4063,26 @@ function createNewExperiment(db, root, hypothesis) {
3473
4063
  const exp = createExperiment(db, finalSlug, branch, hypothesis, null, null);
3474
4064
  updateExperimentStatus(db, exp.id, "reframed");
3475
4065
  exp.status = "reframed";
3476
- const docsDir = path13.join(root, "docs", "experiments");
3477
- const templatePath = path13.join(docsDir, "_TEMPLATE.md");
3478
- if (fs13.existsSync(templatePath)) {
3479
- const template = fs13.readFileSync(templatePath, "utf-8");
4066
+ const docsDir = path11.join(root, "docs", "experiments");
4067
+ const templatePath = path11.join(docsDir, "_TEMPLATE.md");
4068
+ if (fs11.existsSync(templatePath)) {
4069
+ const template = fs11.readFileSync(templatePath, "utf-8");
3480
4070
  const logContent = template.replace(/\{\{title\}\}/g, hypothesis).replace(/\{\{hypothesis\}\}/g, hypothesis).replace(/\{\{branch\}\}/g, branch).replace(/\{\{status\}\}/g, "classified").replace(/\{\{sub_type\}\}/g, "unclassified").replace(/\{\{date\}\}/g, (/* @__PURE__ */ new Date()).toISOString().split("T")[0]);
3481
- const logPath = path13.join(docsDir, `${paddedNum}-${finalSlug}.md`);
3482
- fs13.writeFileSync(logPath, logContent);
4071
+ const logPath = path11.join(docsDir, `${paddedNum}-${finalSlug}.md`);
4072
+ fs11.writeFileSync(logPath, logContent);
3483
4073
  info(`Created experiment log: docs/experiments/${paddedNum}-${finalSlug}.md`);
3484
4074
  }
3485
4075
  return exp;
3486
4076
  }
3487
- function readFileOrEmpty(filePath) {
3488
- try {
3489
- return fs13.readFileSync(filePath, "utf-8");
3490
- } catch {
3491
- return "";
3492
- }
3493
- }
3494
4077
  function slugify2(text) {
3495
4078
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
3496
4079
  }
3497
- function loadConfig7(projectRoot) {
3498
- const configPath = path13.join(projectRoot, ".majlis", "config.json");
3499
- if (!fs13.existsSync(configPath)) {
3500
- return {
3501
- project: { name: "", description: "", objective: "" },
3502
- cycle: {
3503
- compression_interval: 5,
3504
- circuit_breaker_threshold: 3,
3505
- require_doubt_before_verify: true,
3506
- require_challenge_before_verify: false,
3507
- auto_baseline_on_new_experiment: true
3508
- }
3509
- };
3510
- }
3511
- return JSON.parse(fs13.readFileSync(configPath, "utf-8"));
3512
- }
3513
- var fs13, path13, import_node_child_process5;
4080
+ var fs11, path11, import_node_child_process5;
3514
4081
  var init_run = __esm({
3515
4082
  "src/commands/run.ts"() {
3516
4083
  "use strict";
3517
- fs13 = __toESM(require("fs"));
3518
- path13 = __toESM(require("path"));
4084
+ fs11 = __toESM(require("fs"));
4085
+ path11 = __toESM(require("path"));
3519
4086
  import_node_child_process5 = require("child_process");
3520
4087
  init_connection();
3521
4088
  init_queries();
@@ -3523,17 +4090,27 @@ var init_run = __esm({
3523
4090
  init_next();
3524
4091
  init_cycle();
3525
4092
  init_spawn();
4093
+ init_config();
4094
+ init_shutdown();
3526
4095
  init_format();
3527
4096
  }
3528
4097
  });
3529
4098
 
3530
4099
  // src/cli.ts
3531
- var fs14 = __toESM(require("fs"));
3532
- var path14 = __toESM(require("path"));
4100
+ var fs12 = __toESM(require("fs"));
4101
+ var path12 = __toESM(require("path"));
3533
4102
  var VERSION = JSON.parse(
3534
- fs14.readFileSync(path14.join(__dirname, "..", "package.json"), "utf-8")
4103
+ fs12.readFileSync(path12.join(__dirname, "..", "package.json"), "utf-8")
3535
4104
  ).version;
3536
4105
  async function main() {
4106
+ let sigintCount = 0;
4107
+ process.on("SIGINT", () => {
4108
+ sigintCount++;
4109
+ if (sigintCount >= 2) process.exit(130);
4110
+ const { requestShutdown: requestShutdown2 } = (init_shutdown(), __toCommonJS(shutdown_exports));
4111
+ requestShutdown2();
4112
+ console.error("\n\x1B[33m[majlis] Interrupt received. Finishing current step...\x1B[0m");
4113
+ });
3537
4114
  const args = process.argv.slice(2);
3538
4115
  if (args.includes("--version") || args.includes("-v")) {
3539
4116
  console.log(VERSION);