majlis 0.6.3 → 0.7.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 +918 -1561
  2. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -249,6 +249,13 @@ var init_migrations = __esm({
249
249
  );
250
250
 
251
251
  CREATE INDEX idx_swarm_members_run ON swarm_members(swarm_run_id);
252
+ `);
253
+ },
254
+ // Migration 006: v5 → v6 — Experiment dependencies and scoped context
255
+ (db) => {
256
+ db.exec(`
257
+ ALTER TABLE experiments ADD COLUMN depends_on TEXT;
258
+ ALTER TABLE experiments ADD COLUMN context_files TEXT;
252
259
  `);
253
260
  }
254
261
  ];
@@ -301,6 +308,12 @@ function closeDb() {
301
308
  }
302
309
  }
303
310
  function resetDb() {
311
+ if (_db) {
312
+ try {
313
+ _db.close();
314
+ } catch {
315
+ }
316
+ }
304
317
  _db = null;
305
318
  }
306
319
  function openDbAt(projectRoot) {
@@ -512,7 +525,7 @@ async function extractStructuredData(role, markdown) {
512
525
  return null;
513
526
  }
514
527
  function extractMajlisJsonBlock(markdown) {
515
- const match = markdown.match(/<!--\s*majlis-json\s*\n([\s\S]*?)-->/);
528
+ const match = markdown.match(/<!--\s*majlis-json\s*\n?([\s\S]*?)-->/);
516
529
  if (!match) return null;
517
530
  return match[1].trim();
518
531
  }
@@ -717,12 +730,17 @@ If you are past turn 25, output your structured JSON NOW.`;
717
730
  Check: is your core task done? If yes, wrap up and output JSON.`;
718
731
  }
719
732
  }
720
- function buildPreToolUseGuards(role) {
733
+ function isInsideDir(filePath, allowedDir) {
734
+ const resolved = path2.resolve(filePath);
735
+ return resolved.startsWith(allowedDir + path2.sep) || resolved === allowedDir;
736
+ }
737
+ function buildPreToolUseGuards(role, cwd) {
721
738
  if (role === "compressor") {
739
+ const allowedDir = path2.resolve(cwd, "docs", "synthesis");
722
740
  const guardHook = async (input) => {
723
741
  const toolInput = input.tool_input ?? {};
724
742
  const filePath = toolInput.file_path ?? "";
725
- if (filePath && !filePath.includes("/docs/synthesis/")) {
743
+ if (filePath && !isInsideDir(filePath, allowedDir)) {
726
744
  return {
727
745
  decision: "block",
728
746
  reason: `Compressor may only write to docs/synthesis/. Blocked: ${filePath}`
@@ -736,10 +754,11 @@ function buildPreToolUseGuards(role) {
736
754
  ];
737
755
  }
738
756
  if (role === "diagnostician") {
757
+ const allowedDir = path2.resolve(cwd, ".majlis", "scripts");
739
758
  const writeGuard = async (input) => {
740
759
  const toolInput = input.tool_input ?? {};
741
760
  const filePath = toolInput.file_path ?? "";
742
- if (filePath && !filePath.includes("/.majlis/scripts/")) {
761
+ if (filePath && !isInsideDir(filePath, allowedDir)) {
743
762
  return {
744
763
  decision: "block",
745
764
  reason: `Diagnostician may only write to .majlis/scripts/. Blocked: ${filePath}`
@@ -750,7 +769,7 @@ function buildPreToolUseGuards(role) {
750
769
  const bashGuard = async (input) => {
751
770
  const toolInput = input.tool_input ?? {};
752
771
  const command = toolInput.command ?? "";
753
- const destructive = /\b(rm\s+-rf|git\s+(checkout|reset|stash|clean|push)|chmod|chown|mkfs|dd\s+if=)\b/i;
772
+ const destructive = /\b(rm\s+(-\w*[rf]|--recursive|--force)|git\s+(checkout|reset|stash|clean|push\s+--force)|chmod|chown|mkfs|dd\s+if=|sudo\s)/i;
754
773
  if (destructive.test(command)) {
755
774
  return {
756
775
  decision: "block",
@@ -766,10 +785,11 @@ function buildPreToolUseGuards(role) {
766
785
  ];
767
786
  }
768
787
  if (role === "cartographer") {
788
+ const allowedDir = path2.resolve(cwd, "docs", "synthesis");
769
789
  const guardHook = async (input) => {
770
790
  const toolInput = input.tool_input ?? {};
771
791
  const filePath = toolInput.file_path ?? "";
772
- if (filePath && !filePath.includes("/docs/synthesis/")) {
792
+ if (filePath && !isInsideDir(filePath, allowedDir)) {
773
793
  return {
774
794
  decision: "block",
775
795
  reason: `Cartographer may only write to docs/synthesis/. Blocked: ${filePath}`
@@ -783,10 +803,11 @@ function buildPreToolUseGuards(role) {
783
803
  ];
784
804
  }
785
805
  if (role === "toolsmith") {
806
+ const allowedDir = path2.resolve(cwd, ".majlis", "scripts");
786
807
  const writeGuard = async (input) => {
787
808
  const toolInput = input.tool_input ?? {};
788
809
  const filePath = toolInput.file_path ?? "";
789
- if (filePath && !filePath.includes("/.majlis/scripts/")) {
810
+ if (filePath && !isInsideDir(filePath, allowedDir)) {
790
811
  return {
791
812
  decision: "block",
792
813
  reason: `Toolsmith may only write to .majlis/scripts/. Blocked: ${filePath}`
@@ -797,7 +818,7 @@ function buildPreToolUseGuards(role) {
797
818
  const bashGuard = async (input) => {
798
819
  const toolInput = input.tool_input ?? {};
799
820
  const command = toolInput.command ?? "";
800
- const destructive = /\b(rm\s+-rf|git\s+(checkout|reset|stash|clean|push)|chmod|chown|mkfs|dd\s+if=)\b/i;
821
+ const destructive = /\b(rm\s+(-\w*[rf]|--recursive|--force)|git\s+(checkout|reset|stash|clean|push\s+--force)|chmod|chown|mkfs|dd\s+if=|sudo\s)/i;
801
822
  if (destructive.test(command)) {
802
823
  return {
803
824
  decision: "block",
@@ -812,9 +833,59 @@ function buildPreToolUseGuards(role) {
812
833
  { matcher: "Bash", hooks: [bashGuard] }
813
834
  ];
814
835
  }
836
+ if (role === "builder") {
837
+ const bashGuard = async (input) => {
838
+ const toolInput = input.tool_input ?? {};
839
+ const command = toolInput.command ?? "";
840
+ const destructive = /\b(rm\s+(-\w*[rf]|--recursive|--force)|git\s+(checkout|reset|stash|clean|push\s+--force)|chmod|chown|mkfs|dd\s+if=|sudo\s)/i;
841
+ if (destructive.test(command)) {
842
+ return { decision: "block", reason: `Builder blocked destructive command: ${command.slice(0, 100)}` };
843
+ }
844
+ return {};
845
+ };
846
+ const configFile = path2.resolve(cwd, ".majlis", "config.json");
847
+ const dbFile = path2.resolve(cwd, ".majlis", "majlis.db");
848
+ const settingsFile = path2.resolve(cwd, ".claude", "settings.json");
849
+ const configGuard = async (input) => {
850
+ const toolInput = input.tool_input ?? {};
851
+ const filePath = toolInput.file_path ?? "";
852
+ if (filePath) {
853
+ const resolved = path2.resolve(filePath);
854
+ if (resolved === configFile || resolved === dbFile || resolved === settingsFile) {
855
+ return { decision: "block", reason: `Builder may not modify framework files: ${filePath}` };
856
+ }
857
+ }
858
+ return {};
859
+ };
860
+ return [
861
+ { matcher: "Bash", hooks: [bashGuard] },
862
+ { matcher: "Write", hooks: [configGuard] },
863
+ { matcher: "Edit", hooks: [configGuard] }
864
+ ];
865
+ }
866
+ if (role === "verifier") {
867
+ const configFile = path2.resolve(cwd, ".majlis", "config.json");
868
+ const dbFile = path2.resolve(cwd, ".majlis", "majlis.db");
869
+ const settingsFile = path2.resolve(cwd, ".claude", "settings.json");
870
+ const configGuard = async (input) => {
871
+ const toolInput = input.tool_input ?? {};
872
+ const filePath = toolInput.file_path ?? "";
873
+ if (filePath) {
874
+ const resolved = path2.resolve(filePath);
875
+ if (resolved === configFile || resolved === dbFile || resolved === settingsFile) {
876
+ return { decision: "block", reason: `Verifier may not modify framework files: ${filePath}` };
877
+ }
878
+ }
879
+ return {};
880
+ };
881
+ return [
882
+ { matcher: "Write", hooks: [configGuard] },
883
+ { matcher: "Edit", hooks: [configGuard] }
884
+ ];
885
+ }
815
886
  return void 0;
816
887
  }
817
- function buildAgentHooks(role, maxTurns) {
888
+ function buildAgentHooks(role, maxTurns, cwd) {
818
889
  const result = {};
819
890
  let hasHooks = false;
820
891
  const interval = CHECKPOINT_INTERVAL[role];
@@ -836,7 +907,7 @@ function buildAgentHooks(role, maxTurns) {
836
907
  result.PostToolUse = [{ hooks: [checkpointHook] }];
837
908
  hasHooks = true;
838
909
  }
839
- const guards = buildPreToolUseGuards(role);
910
+ const guards = buildPreToolUseGuards(role, cwd);
840
911
  if (guards) {
841
912
  result.PreToolUse = guards;
842
913
  hasHooks = true;
@@ -965,7 +1036,7 @@ Your job: Write a CLEAN experiment doc to ${expDocPath} using the Write tool.
965
1036
  async function runQuery(opts) {
966
1037
  let truncated = false;
967
1038
  const tag = opts.label ?? "majlis";
968
- const hooks = opts.role ? buildAgentHooks(opts.role, opts.maxTurns ?? 15) : void 0;
1039
+ const hooks = opts.role ? buildAgentHooks(opts.role, opts.maxTurns ?? 15, opts.cwd) : void 0;
969
1040
  const conversation = (0, import_claude_agent_sdk2.query)({
970
1041
  prompt: opts.prompt,
971
1042
  options: {
@@ -1197,7 +1268,7 @@ var init_config = __esm({
1197
1268
  path3 = __toESM(require("path"));
1198
1269
  DEFAULT_CONFIG = {
1199
1270
  project: { name: "", description: "", objective: "" },
1200
- metrics: { command: "", fixtures: [], tracked: {} },
1271
+ metrics: { command: "", fixtures: {}, tracked: {} },
1201
1272
  build: { pre_measure: null, post_measure: null },
1202
1273
  cycle: {
1203
1274
  compression_interval: 5,
@@ -1811,18 +1882,23 @@ var init_scan = __esm({
1811
1882
  // src/commands/init.ts
1812
1883
  var init_exports = {};
1813
1884
  __export(init_exports, {
1814
- AGENT_DEFINITIONS: () => AGENT_DEFINITIONS,
1815
- CLAUDE_MD_SECTION: () => CLAUDE_MD_SECTION,
1816
- HOOKS_CONFIG: () => HOOKS_CONFIG,
1817
- SLASH_COMMANDS: () => SLASH_COMMANDS,
1885
+ AGENT_DEFINITIONS: () => import_shared.AGENT_DEFINITIONS,
1886
+ CLAUDE_MD_SECTION: () => import_shared.CLAUDE_MD_SECTION,
1887
+ HOOKS_CONFIG: () => import_shared.HOOKS_CONFIG,
1888
+ SLASH_COMMANDS: () => import_shared.SLASH_COMMANDS,
1818
1889
  init: () => init
1819
1890
  });
1891
+ function writeIfMissing(filePath, content) {
1892
+ if (!fs7.existsSync(filePath)) {
1893
+ fs7.writeFileSync(filePath, content);
1894
+ }
1895
+ }
1820
1896
  async function init(_args) {
1821
1897
  const runScan = _args.includes("--scan");
1822
1898
  const projectRoot = process.cwd();
1823
1899
  header("Initializing Majlis");
1824
1900
  const majlisDir = path7.join(projectRoot, ".majlis");
1825
- mkdirSafe(majlisDir);
1901
+ (0, import_shared.mkdirSafe)(majlisDir);
1826
1902
  info("Created .majlis/");
1827
1903
  resetDb();
1828
1904
  const db = getDb(projectRoot);
@@ -1831,7 +1907,7 @@ async function init(_args) {
1831
1907
  resetDb();
1832
1908
  const configPath = path7.join(majlisDir, "config.json");
1833
1909
  if (!fs7.existsSync(configPath)) {
1834
- const config = { ...DEFAULT_CONFIG2 };
1910
+ const config = { ...import_shared.DEFAULT_CONFIG };
1835
1911
  const pkgPath = path7.join(projectRoot, "package.json");
1836
1912
  if (fs7.existsSync(pkgPath)) {
1837
1913
  try {
@@ -1845,20 +1921,20 @@ async function init(_args) {
1845
1921
  info("Created .majlis/config.json");
1846
1922
  }
1847
1923
  const agentsDir = path7.join(majlisDir, "agents");
1848
- mkdirSafe(agentsDir);
1849
- for (const [name, content] of Object.entries(AGENT_DEFINITIONS)) {
1850
- fs7.writeFileSync(path7.join(agentsDir, `${name}.md`), content);
1924
+ (0, import_shared.mkdirSafe)(agentsDir);
1925
+ for (const [name, content] of Object.entries(import_shared.AGENT_DEFINITIONS)) {
1926
+ writeIfMissing(path7.join(agentsDir, `${name}.md`), content);
1851
1927
  }
1852
1928
  info("Created agent definitions in .majlis/agents/");
1853
1929
  const claudeAgentsDir = path7.join(projectRoot, ".claude", "agents");
1854
- mkdirSafe(claudeAgentsDir);
1855
- for (const [name, content] of Object.entries(AGENT_DEFINITIONS)) {
1856
- fs7.writeFileSync(path7.join(claudeAgentsDir, `${name}.md`), content);
1930
+ (0, import_shared.mkdirSafe)(claudeAgentsDir);
1931
+ for (const [name, content] of Object.entries(import_shared.AGENT_DEFINITIONS)) {
1932
+ writeIfMissing(path7.join(claudeAgentsDir, `${name}.md`), content);
1857
1933
  }
1858
1934
  info("Copied agent definitions to .claude/agents/");
1859
1935
  const commandsDir = path7.join(projectRoot, ".claude", "commands");
1860
- mkdirSafe(commandsDir);
1861
- for (const [name, cmd] of Object.entries(SLASH_COMMANDS)) {
1936
+ (0, import_shared.mkdirSafe)(commandsDir);
1937
+ for (const [name, cmd] of Object.entries(import_shared.SLASH_COMMANDS)) {
1862
1938
  const content = `---
1863
1939
  description: ${cmd.description}
1864
1940
  ---
@@ -1871,69 +1947,53 @@ ${cmd.body}
1871
1947
  if (fs7.existsSync(settingsPath)) {
1872
1948
  try {
1873
1949
  const existing = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
1874
- existing.hooks = { ...existing.hooks, ...HOOKS_CONFIG.hooks };
1950
+ existing.hooks = { ...existing.hooks, ...import_shared.HOOKS_CONFIG.hooks };
1875
1951
  fs7.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
1876
1952
  } catch {
1877
- fs7.writeFileSync(settingsPath, JSON.stringify(HOOKS_CONFIG, null, 2));
1953
+ fs7.writeFileSync(settingsPath, JSON.stringify(import_shared.HOOKS_CONFIG, null, 2));
1878
1954
  }
1879
1955
  } else {
1880
- fs7.writeFileSync(settingsPath, JSON.stringify(HOOKS_CONFIG, null, 2));
1956
+ fs7.writeFileSync(settingsPath, JSON.stringify(import_shared.HOOKS_CONFIG, null, 2));
1881
1957
  }
1882
1958
  info("Created hooks in .claude/settings.json");
1883
1959
  const docsDir = path7.join(projectRoot, "docs");
1884
- const docDirs = [
1885
- "inbox",
1886
- "experiments",
1887
- "decisions",
1888
- "classification",
1889
- "doubts",
1890
- "challenges",
1891
- "verification",
1892
- "reframes",
1893
- "rihla",
1894
- "synthesis",
1895
- "diagnosis"
1896
- ];
1897
- for (const dir of docDirs) {
1898
- mkdirSafe(path7.join(docsDir, dir));
1960
+ for (const dir of import_shared.DOC_DIRS) {
1961
+ (0, import_shared.mkdirSafe)(path7.join(docsDir, dir));
1899
1962
  }
1900
- for (const [relativePath, content] of Object.entries(DOC_TEMPLATES)) {
1963
+ for (const [relativePath, content] of Object.entries(import_shared.DOC_TEMPLATES)) {
1901
1964
  const fullPath = path7.join(docsDir, relativePath);
1902
- if (!fs7.existsSync(fullPath)) {
1903
- fs7.writeFileSync(fullPath, content);
1904
- }
1965
+ writeIfMissing(fullPath, content);
1905
1966
  }
1906
1967
  info("Created docs/ tree with templates");
1907
1968
  const synthesisDir = path7.join(docsDir, "synthesis");
1908
- const currentPath = path7.join(synthesisDir, "current.md");
1909
- if (!fs7.existsSync(currentPath)) {
1910
- fs7.writeFileSync(currentPath, '# Project Synthesis\n\n*No experiments yet. Run `majlis new "hypothesis"` to begin.*\n');
1911
- }
1912
- const fragPath = path7.join(synthesisDir, "fragility.md");
1913
- if (!fs7.existsSync(fragPath)) {
1914
- fs7.writeFileSync(fragPath, "# Fragility Map\n\n*No fragility recorded yet.*\n");
1915
- }
1916
- const deadEndsPath = path7.join(synthesisDir, "dead-ends.md");
1917
- if (!fs7.existsSync(deadEndsPath)) {
1918
- fs7.writeFileSync(deadEndsPath, "# Dead-End Registry\n\n*No dead-ends recorded yet.*\n");
1969
+ for (const [filename, content] of Object.entries(import_shared.SYNTHESIS_STARTERS)) {
1970
+ writeIfMissing(path7.join(synthesisDir, filename), content);
1919
1971
  }
1920
1972
  const workflowPath = path7.join(docsDir, "workflow.md");
1921
- if (!fs7.existsSync(workflowPath)) {
1922
- fs7.writeFileSync(workflowPath, WORKFLOW_MD);
1923
- }
1973
+ writeIfMissing(workflowPath, import_shared.WORKFLOW_MD);
1924
1974
  info("Created docs/workflow.md");
1925
1975
  const claudeMdPath = path7.join(projectRoot, "CLAUDE.md");
1926
1976
  if (fs7.existsSync(claudeMdPath)) {
1927
1977
  const existing = fs7.readFileSync(claudeMdPath, "utf-8");
1928
1978
  if (!existing.includes("## Majlis Protocol")) {
1929
- fs7.writeFileSync(claudeMdPath, existing + "\n" + CLAUDE_MD_SECTION);
1979
+ fs7.writeFileSync(claudeMdPath, existing + "\n" + import_shared.CLAUDE_MD_SECTION);
1930
1980
  info("Appended Majlis Protocol to existing CLAUDE.md");
1931
1981
  }
1932
1982
  } else {
1933
1983
  fs7.writeFileSync(claudeMdPath, `# ${path7.basename(projectRoot)}
1934
- ${CLAUDE_MD_SECTION}`);
1984
+ ${import_shared.CLAUDE_MD_SECTION}`);
1935
1985
  info("Created CLAUDE.md with Majlis Protocol");
1936
1986
  }
1987
+ const gitignorePath = path7.join(projectRoot, ".gitignore");
1988
+ const dbEntries = [".majlis/majlis.db", ".majlis/majlis.db-wal", ".majlis/majlis.db-shm"];
1989
+ if (fs7.existsSync(gitignorePath)) {
1990
+ const existing = fs7.readFileSync(gitignorePath, "utf-8");
1991
+ if (!existing.includes(".majlis/majlis.db")) {
1992
+ const suffix = existing.endsWith("\n") ? "" : "\n";
1993
+ fs7.writeFileSync(gitignorePath, existing + suffix + dbEntries.join("\n") + "\n");
1994
+ info("Added .majlis/majlis.db to .gitignore");
1995
+ }
1996
+ }
1937
1997
  success("Majlis initialized. Run `majlis status` to see project state.");
1938
1998
  if (runScan) {
1939
1999
  info("Running project scan...");
@@ -1941,12 +2001,7 @@ ${CLAUDE_MD_SECTION}`);
1941
2001
  await scan2([]);
1942
2002
  }
1943
2003
  }
1944
- function mkdirSafe(dir) {
1945
- if (!fs7.existsSync(dir)) {
1946
- fs7.mkdirSync(dir, { recursive: true });
1947
- }
1948
- }
1949
- var fs7, path7, DEFAULT_CONFIG2, AGENT_DEFINITIONS, SLASH_COMMANDS, HOOKS_CONFIG, CLAUDE_MD_SECTION, WORKFLOW_MD, DOC_TEMPLATES;
2004
+ var fs7, path7, import_shared;
1950
2005
  var init_init = __esm({
1951
2006
  "src/commands/init.ts"() {
1952
2007
  "use strict";
@@ -1954,1006 +2009,28 @@ var init_init = __esm({
1954
2009
  path7 = __toESM(require("path"));
1955
2010
  init_connection();
1956
2011
  init_format();
1957
- DEFAULT_CONFIG2 = {
1958
- project: {
1959
- name: "",
1960
- description: "",
1961
- objective: ""
1962
- },
1963
- metrics: {
1964
- command: `echo '{"fixtures":{}}'`,
1965
- fixtures: [],
1966
- tracked: {}
1967
- },
1968
- build: {
1969
- pre_measure: null,
1970
- post_measure: null
1971
- },
1972
- cycle: {
1973
- compression_interval: 5,
1974
- circuit_breaker_threshold: 3,
1975
- require_doubt_before_verify: true,
1976
- require_challenge_before_verify: false,
1977
- auto_baseline_on_new_experiment: true
1978
- },
1979
- models: {
1980
- builder: "opus",
1981
- critic: "opus",
1982
- adversary: "opus",
1983
- verifier: "opus",
1984
- reframer: "opus",
1985
- compressor: "opus"
1986
- }
1987
- };
1988
- AGENT_DEFINITIONS = {
1989
- builder: `---
1990
- name: builder
1991
- model: opus
1992
- tools: [Read, Write, Edit, Bash, Glob, Grep]
1993
- ---
1994
- You are the Builder. You write code, run experiments, and make technical decisions.
1995
-
1996
- Before building:
1997
- 1. Read docs/synthesis/current.md for project state
1998
- 2. Read the dead-ends provided in your context \u2014 these are structural constraints
1999
- 3. Check docs/classification/ for problem taxonomy
2000
- 4. Check docs/experiments/ for prior work
2001
-
2002
- Read as much code as you need to understand the problem. Reading is free \u2014 spend
2003
- as many turns as necessary on Read, Grep, and Glob to build full context before
2004
- you touch anything.
2005
-
2006
- Do NOT read raw data files (fixtures/, ground truth JSON/STL). The synthesis
2007
- has the relevant facts. Reading raw data wastes turns re-deriving what the
2008
- doubt/challenge/verify cycle already established.
2009
-
2010
- ## The Rule: ONE Change, Then Document
2011
-
2012
- You make ONE code change per cycle. Not two, not "one more quick fix." ONE.
2013
-
2014
- The sequence:
2015
- 1. **Read and understand** \u2014 read synthesis, dead-ends, source code. Take your time.
2016
- 2. **Write the experiment doc FIRST** \u2014 before coding, fill in the Approach section
2017
- with what you plan to do and why. This ensures there is always a record.
2018
- 3. **Implement ONE focused change** \u2014 a single coherent edit to the codebase.
2019
- 4. **Run the benchmark ONCE** \u2014 observe the result.
2020
- 5. **Update the experiment doc** \u2014 fill in Results and Metrics with what happened.
2021
- 6. **Output the majlis-json block** \u2014 your structured decisions.
2022
- 7. **STOP.**
2023
-
2024
- If your change doesn't work, document what happened and STOP. Do NOT try to fix it.
2025
- Do NOT iterate. Do NOT "try one more thing." The adversary, critic, and verifier
2026
- exist to diagnose what went wrong. The cycle comes back to you with their insights.
2027
-
2028
- If you find yourself wanting to debug your own fix, that's the signal to stop
2029
- and write up what you learned.
2030
-
2031
- ## Off-limits (DO NOT modify)
2032
- - \`fixtures/\` \u2014 test data, ground truth, STL files. Read-only.
2033
- - \`scripts/benchmark.py\` \u2014 the measurement tool. Never change how you're measured.
2034
- - \`.majlis/\` \u2014 framework config. Not your concern.
2035
-
2036
- ## Confirmed Doubts
2037
- If your context includes confirmedDoubts, these are weaknesses that the verifier has
2038
- confirmed from a previous cycle. You MUST address each one. Do not ignore them \u2014
2039
- the verifier will check again.
2040
-
2041
- ## Metrics
2042
- The framework captures baseline and post-build metrics automatically. Do NOT claim
2043
- specific metric numbers unless quoting framework output. Do NOT run the benchmark
2044
- yourself unless instructed to. If you need to verify your change works, do a minimal
2045
- targeted test, not a full benchmark run.
2046
-
2047
- ## During building:
2048
- - Tag EVERY decision: proof / test / strong-consensus / consensus / analogy / judgment
2049
- - When making judgment-level decisions, state: "This is judgment \u2014 reasoning without precedent"
2050
-
2051
- You may NOT verify your own work or mark your own decisions as proven.
2052
- Output your decisions in structured format so they can be recorded in the database.
2053
-
2054
- ## Structured Output Format
2055
- At the end of your work, include a <!-- majlis-json --> block with your decisions:
2056
- \`\`\`
2057
- <!-- majlis-json
2058
- {
2059
- "decisions": [
2060
- { "description": "...", "evidence_level": "judgment|test|proof|analogy|consensus|strong_consensus", "justification": "..." }
2061
- ]
2062
- }
2063
- -->
2064
- \`\`\``,
2065
- critic: `---
2066
- name: critic
2067
- model: opus
2068
- tools: [Read, Glob, Grep]
2069
- ---
2070
- You are the Critic. You practise constructive doubt.
2071
-
2072
- You receive:
2073
- - The builder's experiment document (the artifact, not the reasoning chain)
2074
- - The current synthesis (project state)
2075
- - Dead-ends (approaches that have been tried and failed)
2076
- - The hypothesis and experiment metadata
2077
-
2078
- You do NOT see the builder's reasoning chain \u2014 only their documented output.
2079
- Use the experiment doc, synthesis, and dead-ends to find weaknesses.
2080
-
2081
- For each doubt:
2082
- - What specific claim, decision, or assumption you doubt
2083
- - WHY: reference a prior experiment, inconsistency, untested case, or false analogy
2084
- - Evidence level of the doubted decision
2085
- - Severity: minor / moderate / critical
2086
-
2087
- Rules:
2088
- - Every doubt MUST reference evidence. "This feels wrong" is not a doubt.
2089
- - You may NOT suggest fixes. Identify problems only.
2090
- - Focus on judgment and analogy-level decisions first.
2091
- - You may NOT modify any files. Produce your doubt document as output only.
2092
- - Do NOT attempt to write files. The framework saves your output automatically.
2093
-
2094
- ## Structured Output Format
2095
- <!-- majlis-json
2096
- {
2097
- "doubts": [
2098
- { "claim_doubted": "...", "evidence_level_of_claim": "judgment", "evidence_for_doubt": "...", "severity": "critical|moderate|minor" }
2099
- ]
2100
- }
2101
- -->`,
2102
- adversary: `---
2103
- name: adversary
2104
- model: opus
2105
- tools: [Read, Glob, Grep]
2106
- ---
2107
- You are the Adversary. You do NOT review code for bugs.
2108
- You reason about problem structure to CONSTRUCT pathological cases.
2109
-
2110
- You receive:
2111
- - The git diff of the builder's code changes (the actual code, not prose)
2112
- - The current synthesis (project state)
2113
- - The hypothesis and experiment metadata
2114
-
2115
- Study the CODE DIFF carefully \u2014 that is where the builder's assumptions are exposed.
2116
-
2117
- For each approach the builder takes, ask:
2118
- - What input would make this fail?
2119
- - What boundary condition was not tested?
2120
- - What degenerate case collapses a distinction the algorithm relies on?
2121
- - What distribution shift invalidates the assumptions?
2122
- - Under what conditions do two things the builder treats as distinct become identical?
2123
-
2124
- Produce constructed counterexamples with reasoning.
2125
- Do NOT suggest fixes. Do NOT modify files. Do NOT attempt to write files.
2126
- The framework saves your output automatically.
2127
-
2128
- ## Structured Output Format
2129
- <!-- majlis-json
2130
- {
2131
- "challenges": [
2132
- { "description": "...", "reasoning": "..." }
2133
- ]
2134
- }
2135
- -->`,
2136
- verifier: `---
2137
- name: verifier
2138
- model: opus
2139
- tools: [Read, Glob, Grep, Bash]
2140
- ---
2141
- You are the Verifier. Perform dual verification:
2142
-
2143
- You receive:
2144
- - All doubts with explicit DOUBT-{id} identifiers (use these in your doubt_resolutions)
2145
- - Challenge documents from the adversary
2146
- - Framework-captured metrics (baseline vs post-build) \u2014 this is GROUND TRUTH
2147
- - The hypothesis and experiment metadata
2148
-
2149
- ## Scope Constraint (CRITICAL)
2150
-
2151
- You must produce your structured output (grades + doubt resolutions) within your turn budget.
2152
- Do NOT exhaustively test every doubt and challenge \u2014 prioritize the critical ones.
2153
- For each doubt/challenge: one targeted check is enough. Confirm, dismiss, or mark inconclusive.
2154
- Reserve your final turns for writing the structured majlis-json output.
2155
-
2156
- The framework saves your output automatically. Do NOT attempt to write files.
2157
-
2158
- ## Metrics (GROUND TRUTH)
2159
- If framework-captured metrics are in your context, these are the canonical before/after numbers.
2160
- Do NOT trust numbers claimed by the builder \u2014 compare against the framework metrics.
2161
- If the builder claims improvement but the framework metrics show regression, flag this.
2162
-
2163
- ## PROVENANCE CHECK:
2164
- - Can every piece of code trace to an experiment or decision?
2165
- - Is the chain unbroken from requirement -> classification -> experiment -> code?
2166
- - Flag any broken chains.
2167
-
2168
- ## CONTENT CHECK:
2169
- - Does the code do what the experiment log says?
2170
- - Run at most 3-5 targeted diagnostic scripts, focused on the critical doubts/challenges.
2171
- - Do NOT run exhaustive diagnostics on every claim.
2172
-
2173
- Framework-captured metrics are ground truth \u2014 if they show regression, that
2174
- alone justifies a "rejected" grade. Do not re-derive from raw fixture data.
2175
-
2176
- Grade each component: sound / good / weak / rejected
2177
- Grade each doubt/challenge: confirmed / dismissed (with evidence) / inconclusive
2178
-
2179
- ## Structured Output Format
2180
- IMPORTANT: For doubt_resolutions, use the DOUBT-{id} numbers from your context.
2181
- Example: if your context lists "DOUBT-7: [critical] The algorithm fails on X",
2182
- use doubt_id: 7 in your output.
2183
-
2184
- <!-- majlis-json
2185
- {
2186
- "grades": [
2187
- { "component": "...", "grade": "sound|good|weak|rejected", "provenance_intact": true, "content_correct": true, "notes": "..." }
2188
- ],
2189
- "doubt_resolutions": [
2190
- { "doubt_id": 7, "resolution": "confirmed|dismissed|inconclusive" }
2191
- ]
2192
- }
2193
- -->`,
2194
- reframer: `---
2195
- name: reframer
2196
- model: opus
2197
- tools: [Read, Glob, Grep]
2198
- ---
2199
- You are the Reframer. You receive ONLY:
2200
- - The original problem statement
2201
- - The current classification document
2202
- - The synthesis and dead-end registry
2203
-
2204
- You do NOT read builder code, experiments, or solutions.
2205
-
2206
- Independently propose:
2207
- - How should this problem be decomposed?
2208
- - What are the natural joints?
2209
- - What analogies from other domains apply?
2210
- - What framework would a different field use?
2211
-
2212
- Compare your decomposition with the existing classification.
2213
- Flag structural divergences \u2014 these are the most valuable signals.
2214
-
2215
- Produce your reframe document as output. Do NOT attempt to write files.
2216
- The framework saves your output automatically.
2217
-
2218
- ## Structured Output Format
2219
- <!-- majlis-json
2220
- {
2221
- "reframe": {
2222
- "decomposition": "How you decomposed the problem",
2223
- "divergences": ["List of structural divergences from current classification"],
2224
- "recommendation": "What should change based on your independent analysis"
2225
- }
2226
- }
2227
- -->`,
2228
- compressor: `---
2229
- name: compressor
2230
- model: opus
2231
- tools: [Read, Write, Edit, Glob, Grep]
2232
- ---
2233
- You are the Compressor. Hold the entire project in view and compress it.
2234
-
2235
- Your taskPrompt includes a "Structured Data (CANONICAL)" section exported directly
2236
- from the SQLite database. This is the source of truth. docs/ files are agent artifacts
2237
- that may contain stale or incorrect information. Cross-reference everything against
2238
- the database export.
2239
-
2240
- 1. Read the database export in your context FIRST \u2014 it has all experiments, decisions,
2241
- doubts (with resolutions), verifications (with grades), challenges, and dead-ends.
2242
- 2. Read docs/ files for narrative context, but trust the database when they conflict.
2243
- 3. Cross-reference: same question in different language? contradicting decisions?
2244
- workaround masking root cause?
2245
- 4. Update fragility map: thin coverage, weak components, untested judgment
2246
- decisions, broken provenance.
2247
- 5. Update dead-end registry: compress rejected experiments into structural constraints.
2248
- Mark each dead-end as [structural] or [procedural].
2249
- 6. REWRITE synthesis using the Write tool \u2014 shorter and denser. If it's growing,
2250
- you're accumulating, not compressing. You MUST use the Write tool to update
2251
- docs/synthesis/current.md, docs/synthesis/fragility.md, and docs/synthesis/dead-ends.md.
2252
- The framework does NOT auto-save your output for these files.
2253
- 7. Review classification: new sub-types? resolved sub-types?
2254
-
2255
- You may ONLY write to these three files:
2256
- - docs/synthesis/current.md
2257
- - docs/synthesis/fragility.md
2258
- - docs/synthesis/dead-ends.md
2259
-
2260
- Do NOT modify MEMORY.md, .claude/, classification/, experiments/, or any other paths.
2261
-
2262
- You may NOT write code, make decisions, or run experiments.
2263
-
2264
- ## Structured Output Format
2265
- <!-- majlis-json
2266
- {
2267
- "compression_report": {
2268
- "synthesis_delta": "What changed in synthesis and why",
2269
- "new_dead_ends": ["List of newly identified dead-end constraints"],
2270
- "fragility_changes": ["List of changes to the fragility map"]
2271
- }
2272
- }
2273
- -->`,
2274
- scout: `---
2275
- name: scout
2276
- model: opus
2277
- tools: [Read, Glob, Grep, WebSearch]
2278
- ---
2279
- You are the Scout. You practise rihla \u2014 travel in search of knowledge.
2280
-
2281
- Your job is to search externally for alternative approaches, contradictory evidence,
2282
- and perspectives from other fields that could inform the current experiment.
2283
-
2284
- You receive:
2285
- - The current synthesis and fragility map
2286
- - Dead-ends (approaches that have been tried and failed) \u2014 search for alternatives that circumvent these
2287
- - The hypothesis and experiment metadata
2288
-
2289
- For the given experiment:
2290
- 1. Describe the problem in domain-neutral terms
2291
- 2. Search for alternative approaches in other fields or frameworks
2292
- 3. Identify known limitations of the current approach from external sources
2293
- 4. Find structurally similar problems in unrelated domains
2294
- 5. Report what you find on its own terms \u2014 do not judge or filter
2295
-
2296
- Rules:
2297
- - Present findings neutrally. Report each approach on its own terms.
2298
- - Note where external approaches contradict the current one \u2014 these are the most valuable signals.
2299
- - Focus on approaches that CIRCUMVENT known dead-ends \u2014 these are the most valuable.
2300
- - You may NOT modify code or make decisions. Produce your rihla document as output only.
2301
- - Do NOT attempt to write files. The framework saves your output automatically.
2302
-
2303
- ## Structured Output Format
2304
- <!-- majlis-json
2305
- {
2306
- "findings": [
2307
- { "approach": "Name of alternative approach", "source": "Where you found it", "relevance": "How it applies", "contradicts_current": true }
2308
- ]
2309
- }
2310
- -->`,
2311
- gatekeeper: `---
2312
- name: gatekeeper
2313
- model: sonnet
2314
- tools: [Read, Glob, Grep]
2315
- ---
2316
- You are the Gatekeeper. You check hypotheses before expensive build cycles.
2317
-
2318
- Your job is a fast quality gate \u2014 prevent wasted Opus builds on hypotheses that
2319
- are stale, redundant with dead-ends, or too vague to produce a focused change.
2320
-
2321
- ## Checks (in order)
2322
-
2323
- ### 1. Stale References
2324
- Does the hypothesis reference specific functions, line numbers, or structures that
2325
- may not exist in the current code? Read the relevant files to verify.
2326
- - If references are stale, list them in stale_references.
2327
-
2328
- ### 2. Dead-End Overlap
2329
- Does this hypothesis repeat an approach already ruled out by structural dead-ends?
2330
- Check each structural dead-end in your context \u2014 if the hypothesis matches the
2331
- approach or violates the structural_constraint, flag it.
2332
- - If overlapping, list the dead-end IDs in overlapping_dead_ends.
2333
-
2334
- ### 3. Scope Check
2335
- Is this a single focused change? A good hypothesis names ONE function, mechanism,
2336
- or parameter to change. A bad hypothesis says "improve X and also Y and also Z."
2337
- - Flag if the hypothesis tries to do multiple things.
2338
-
2339
- ## Output
2340
-
2341
- gate_decision:
2342
- - **approve** \u2014 all checks pass, proceed to build
2343
- - **flag** \u2014 concerns found but not blocking (warnings only)
2344
- - **reject** \u2014 hypothesis must be revised (stale refs, dead-end repeat, or too vague)
2345
-
2346
- ## Structured Output Format
2347
- <!-- majlis-json
2348
- {
2349
- "gate_decision": "approve|reject|flag",
2350
- "reason": "Brief explanation of decision",
2351
- "stale_references": ["list of stale references found, if any"],
2352
- "overlapping_dead_ends": [0]
2353
- }
2354
- -->`,
2355
- cartographer: `---
2356
- name: cartographer
2357
- model: opus
2358
- tools: [Read, Write, Edit, Glob, Grep, Bash]
2359
- ---
2360
- You are the Cartographer. You map the architecture of an existing codebase.
2361
-
2362
- You receive a ProjectProfile JSON (deterministic surface scan) as context.
2363
- Your job is to deeply explore the codebase and produce two synthesis documents:
2364
- - docs/synthesis/current.md \u2014 project identity, architecture, key abstractions,
2365
- entry points, test coverage, build pipeline
2366
- - docs/synthesis/fragility.md \u2014 untested areas, single points of failure,
2367
- dependency risk, tech debt
2368
-
2369
- ## Your Approach
2370
-
2371
- Phase 1: Orientation (turns 1-10)
2372
- - Read README, main entry point, 2-3 key imports
2373
- - Understand the project's purpose and structure
2374
-
2375
- Phase 2: Architecture Mapping (turns 11-30)
2376
- - Trace module boundaries and dependency graph
2377
- - Identify data flow patterns, config patterns
2378
- - For huge codebases: focus on entry points and top 5 most-imported modules
2379
- - Map test coverage and build pipeline
2380
-
2381
- Phase 3: Write Synthesis (turns 31-40)
2382
- - Write docs/synthesis/current.md with dense, actionable content
2383
- - Write docs/synthesis/fragility.md with identified weak spots
2384
-
2385
- You may ONLY write to docs/synthesis/. Do NOT modify source code.
2386
-
2387
- ## Structured Output Format
2388
- <!-- majlis-json
2389
- {
2390
- "architecture": {
2391
- "modules": ["list of key modules"],
2392
- "entry_points": ["main entry points"],
2393
- "key_abstractions": ["core abstractions and patterns"],
2394
- "dependency_graph": "brief description of dependency structure"
2395
- }
2396
- }
2397
- -->`,
2398
- toolsmith: `---
2399
- name: toolsmith
2400
- model: opus
2401
- tools: [Read, Write, Edit, Bash, Glob, Grep]
2402
- ---
2403
- You are the Toolsmith. You verify toolchain and create a working metrics pipeline.
2404
-
2405
- You receive a ProjectProfile JSON as context with detected test/build commands.
2406
- Your job is to verify these commands actually work, then create a metrics wrapper
2407
- script that translates test output into Majlis fixtures JSON format.
2408
-
2409
- ## Your Approach
2410
-
2411
- Phase 1: Verify Toolchain (turns 1-10)
2412
- - Try running the detected test command
2413
- - Try the build command
2414
- - Read CI config for hints if commands fail
2415
- - Determine what actually works
2416
-
2417
- Phase 2: Create Metrics Wrapper (turns 11-25)
2418
- - Create .majlis/scripts/metrics.sh
2419
- - The wrapper MUST output valid JSON to stdout: {"fixtures":{"test_suite":{"total":N,"passed":N,"failed":N,"duration_ms":N}}}
2420
- - Redirect all non-JSON output to stderr
2421
- - Strategy per framework:
2422
- - jest/vitest: --json flag \u2192 parse JSON
2423
- - pytest: --tb=no -q \u2192 parse summary line
2424
- - go test: -json \u2192 aggregate
2425
- - cargo test: parse "test result:" line
2426
- - no tests: stub with {"fixtures":{"project":{"has_tests":0}}}
2427
-
2428
- Phase 3: Output Config (turns 26-30)
2429
- - Output structured JSON with verified commands and config
2430
-
2431
- ## Edge Cases
2432
- - Build fails \u2192 set build_command: null, note issue, metrics wrapper still works
2433
- - Tests fail \u2192 wrapper still outputs valid JSON with fail counts
2434
- - No tests \u2192 stub wrapper
2435
- - Huge monorepo \u2192 focus on primary workspace
2436
-
2437
- You may ONLY write to .majlis/scripts/. Do NOT modify source code.
2438
-
2439
- ## Structured Output Format
2440
- <!-- majlis-json
2441
- {
2442
- "toolsmith": {
2443
- "metrics_command": ".majlis/scripts/metrics.sh",
2444
- "build_command": "npm run build",
2445
- "test_command": "npm test",
2446
- "test_framework": "jest",
2447
- "pre_measure": null,
2448
- "post_measure": null,
2449
- "fixtures": {},
2450
- "tracked": {},
2451
- "verification_output": "brief summary of what worked",
2452
- "issues": ["list of issues encountered"]
2453
- }
2454
- }
2455
- -->`,
2456
- diagnostician: `---
2457
- name: diagnostician
2458
- model: opus
2459
- tools: [Read, Write, Bash, Glob, Grep, WebSearch]
2460
- ---
2461
- You are the Diagnostician. You perform deep project-wide analysis.
2462
-
2463
- You have the highest turn budget of any agent. Use it for depth, not breadth.
2464
- Your job is pure insight \u2014 you do NOT fix code, you do NOT build, you do NOT
2465
- make decisions. You diagnose.
2466
-
2467
- ## What You Receive
2468
- - Full database export: every experiment, decision, doubt, challenge, verification,
2469
- dead-end, metric, and compression across the entire project history
2470
- - Current synthesis, fragility map, and dead-end registry
2471
- - Full read access to the entire project codebase
2472
- - Bash access to run tests, profiling, git archaeology, and analysis scripts
2473
-
2474
- ## What You Can Do
2475
- 1. **Read everything** \u2014 source code, docs, git history, test output
2476
- 2. **Run analysis** \u2014 execute tests, profilers, git log/blame/bisect, custom scripts
2477
- 3. **Write analysis scripts** \u2014 you may write scripts ONLY to \`.majlis/scripts/\`
2478
- 4. **Search externally** \u2014 WebSearch for patterns, known issues, relevant techniques
2479
-
2480
- ## What You CANNOT Do
2481
- - Modify any project files outside \`.majlis/scripts/\`
2482
- - Make code changes, fixes, or patches
2483
- - Create experiments or make decisions
2484
- - Write to docs/, src/, or any other project directory
2485
-
2486
- ## Your Approach
2487
-
2488
- Phase 1: Orientation (turns 1-10)
2489
- - Read the full database export in your context
2490
- - Read synthesis, fragility, dead-ends
2491
- - Identify patterns: recurring failures, unresolved doubts, evidence gaps
2492
-
2493
- Phase 2: Deep Investigation (turns 11-40)
2494
- - Read source code at critical points identified in Phase 1
2495
- - Run targeted tests, profiling, git archaeology
2496
- - Write and execute analysis scripts in .majlis/scripts/
2497
- - Cross-reference findings across experiments
2498
-
2499
- Phase 3: Synthesis (turns 41-60)
2500
- - Compile findings into a diagnostic report
2501
- - Identify root causes, not symptoms
2502
- - Rank issues by structural impact
2503
- - Suggest investigation directions (not fixes)
2504
-
2505
- ## Output Format
2506
- Produce a diagnostic report as markdown. At the end, include:
2507
-
2508
- <!-- majlis-json
2509
- {
2510
- "diagnosis": {
2511
- "root_causes": ["List of identified root causes"],
2512
- "patterns": ["Recurring patterns across experiments"],
2513
- "evidence_gaps": ["What we don't know but should"],
2514
- "investigation_directions": ["Suggested directions for next experiments"]
2515
- }
2516
- }
2517
- -->
2518
-
2519
- ## Safety Reminders
2520
- - You are READ-ONLY for project code. Write ONLY to .majlis/scripts/.
2521
- - Focus on diagnosis, not fixing. Your value is insight, not implementation.
2522
- - Trust the database export over docs/ files when they conflict.`
2523
- };
2524
- SLASH_COMMANDS = {
2525
- classify: {
2526
- description: "Classify a problem domain into canonical sub-types before building",
2527
- body: `Run \`majlis classify "$ARGUMENTS"\` and follow its output.
2528
- If the CLI is not installed, act as the Builder in classification mode.
2529
- Read docs/synthesis/current.md and docs/synthesis/dead-ends.md for context.
2530
- Enumerate and classify all canonical sub-types of: $ARGUMENTS
2531
- Produce a classification document following docs/classification/_TEMPLATE.md.`
2532
- },
2533
- doubt: {
2534
- description: "Run a constructive doubt pass on an experiment",
2535
- body: `Run \`majlis doubt $ARGUMENTS\` to spawn the critic agent.
2536
- If the CLI is not installed, act as the Critic directly.
2537
- Doubt the experiment at $ARGUMENTS. Produce a doubt document
2538
- following docs/doubts/_TEMPLATE.md.`
2539
- },
2540
- challenge: {
2541
- description: "Construct adversarial test cases for an experiment",
2542
- body: `Run \`majlis challenge $ARGUMENTS\` to spawn the adversary agent.
2543
- If the CLI is not installed, act as the Adversary directly.
2544
- Construct pathological inputs designed to break the approach in $ARGUMENTS.
2545
- Produce a challenge document following docs/challenges/_TEMPLATE.md.`
2546
- },
2547
- verify: {
2548
- description: "Verify correctness and provenance of an experiment",
2549
- body: `Run \`majlis verify $ARGUMENTS\` to spawn the verifier agent.
2550
- If the CLI is not installed, act as the Verifier directly.
2551
- Perform dual verification (provenance + content) on $ARGUMENTS.
2552
- Produce a verification report following docs/verification/_TEMPLATE.md.`
2553
- },
2554
- reframe: {
2555
- description: "Independently reframe a problem from scratch",
2556
- body: `Run \`majlis reframe $ARGUMENTS\` to spawn the reframer agent.
2557
- If the CLI is not installed, act as the Reframer directly.
2558
- You receive ONLY the problem statement and classification \u2014 NOT builder code.
2559
- Independently decompose $ARGUMENTS and compare with existing classification.`
2560
- },
2561
- compress: {
2562
- description: "Compress project state into dense synthesis",
2563
- body: `Run \`majlis compress\` to spawn the compressor agent.
2564
- If the CLI is not installed, act as the Compressor directly.
2565
- Read everything. Rewrite docs/synthesis/current.md shorter and denser.
2566
- Update fragility map and dead-end registry.`
2567
- },
2568
- scout: {
2569
- description: "Search externally for alternative approaches",
2570
- body: `Run \`majlis scout $ARGUMENTS\` to spawn the scout agent.
2571
- If the CLI is not installed, search for alternative approaches to $ARGUMENTS.
2572
- Look for: limitations of current approach, alternative formulations from other fields,
2573
- structurally similar problems in unrelated domains.
2574
- Produce a rihla document at docs/rihla/.`
2575
- },
2576
- audit: {
2577
- description: "Maqasid check \u2014 is the frame right?",
2578
- body: `Run \`majlis audit "$ARGUMENTS"\` for a purpose audit.
2579
- If the CLI is not installed, review: original objective, current classification,
2580
- recent failures, dead-ends. Ask: is the classification serving the objective?
2581
- Would we decompose differently with what we now know?`
2582
- },
2583
- diagnose: {
2584
- description: "Deep project-wide diagnostic analysis",
2585
- body: `Run \`majlis diagnose $ARGUMENTS\` for deep diagnosis.
2586
- If the CLI is not installed, perform a deep diagnostic analysis.
2587
- Read docs/synthesis/current.md, fragility.md, dead-ends.md, and all experiments.
2588
- Identify root causes, recurring patterns, evidence gaps, and investigation directions.
2589
- Do NOT modify project code \u2014 analysis only.`
2590
- },
2591
- scan: {
2592
- description: "Scan existing project to auto-detect config and write synthesis",
2593
- body: `Run \`majlis scan\` to analyze the existing codebase.
2594
- This spawns two agents in parallel:
2595
- - Cartographer: maps architecture \u2192 docs/synthesis/current.md + fragility.md
2596
- - Toolsmith: verifies toolchain \u2192 .majlis/scripts/metrics.sh + config.json
2597
- Use --force to overwrite existing synthesis files.`
2598
- },
2599
- resync: {
2600
- description: "Update stale synthesis after project evolved without Majlis",
2601
- body: `Run \`majlis resync\` to bring Majlis back up to speed.
2602
- Unlike scan (which starts from zero), resync starts from existing knowledge.
2603
- It assesses staleness, then re-runs cartographer (always) and toolsmith (if needed)
2604
- with the old synthesis and DB history as context.
2605
- Use --check to see the staleness report without making changes.
2606
- Use --force to skip active experiment checks.`
2607
- }
2608
- };
2609
- HOOKS_CONFIG = {
2610
- hooks: {
2611
- SessionStart: [
2612
- {
2613
- hooks: [
2614
- {
2615
- type: "command",
2616
- command: "majlis status --json 2>/dev/null || true"
2617
- }
2618
- ]
2619
- }
2620
- ],
2621
- PreToolUse: [
2622
- {
2623
- matcher: "Bash",
2624
- hooks: [
2625
- {
2626
- type: "command",
2627
- command: "majlis check-commit 2>/dev/null || true",
2628
- timeout: 10
2629
- }
2630
- ]
2631
- }
2632
- ],
2633
- SubagentStop: [
2634
- {
2635
- hooks: [
2636
- {
2637
- type: "command",
2638
- command: "echo 'Subagent completed. Run majlis next to continue the cycle.'",
2639
- timeout: 5
2640
- }
2641
- ]
2642
- }
2643
- ]
2644
- }
2645
- };
2646
- CLAUDE_MD_SECTION = `
2647
- ## Majlis Protocol
2648
-
2649
- This project uses the Majlis Framework for structured multi-agent problem solving.
2650
- See \`docs/workflow.md\` for the full cycle. See \`.claude/agents/\` for role definitions (source of truth in \`.majlis/agents/\`).
2651
-
2652
- ### Evidence Hierarchy (tag every decision)
2653
- 1. **Proof** \u2014 mathematical proof. Overturn requires error in proof.
2654
- 2. **Test** \u2014 empirical test. Overturn requires showing test insufficiency.
2655
- 3a. **Strong Consensus** \u2014 convergence across independent approaches.
2656
- 3b. **Consensus** \u2014 agreement from same-model experiments.
2657
- 4. **Analogy** \u2014 justified by similarity to prior work.
2658
- 5. **Judgment** \u2014 independent reasoning without precedent.
2659
-
2660
- ### Session Discipline
2661
- - One intent per session. Declare it with \`majlis session start "intent"\`.
2662
- - Stray thoughts \u2192 Telegram (Scribe) or docs/inbox/.
2663
- - Every session ends with \`majlis session end\`.
2664
-
2665
- ### Before Building
2666
- - Read \`docs/synthesis/current.md\` for compressed project state.
2667
- - Run \`majlis dead-ends --sub-type <relevant>\` for structural constraints.
2668
- - Run \`majlis decisions --level judgment\` for provisional decisions to challenge.
2669
-
2670
- ### Compression Trigger
2671
- - Run \`majlis status\` \u2014 it will warn when compression is due.
2672
-
2673
- ### Current State
2674
- Run \`majlis status\` for live experiment state and cycle position.
2675
- `;
2676
- WORKFLOW_MD = `# Majlis Workflow \u2014 Quick Reference
2677
-
2678
- ## The Cycle
2679
-
2680
- \`\`\`
2681
- 1. CLASSIFY \u2192 Taxonomy before solution (Al-Khwarizmi)
2682
- 2. REFRAME \u2192 Independent decomposition (Al-Biruni)
2683
- 3. BUILD \u2192 Write code with tagged decisions (Ijtihad)
2684
- 4. CHALLENGE \u2192 Construct breaking inputs (Ibn al-Haytham)
2685
- 5. DOUBT \u2192 Systematic challenge with evidence (Shukuk)
2686
- 6. SCOUT \u2192 External search for alternatives (Rihla)
2687
- 7. VERIFY \u2192 Provenance + content checks (Isnad + Matn)
2688
- 8. RESOLVE \u2192 Route based on grades
2689
- 9. COMPRESS \u2192 Shorter and denser (Hifz)
2690
- \`\`\`
2691
-
2692
- ## Resolution
2693
- - **Sound** \u2192 Merge
2694
- - **Good** \u2192 Merge + add gaps to fragility map
2695
- - **Weak** \u2192 Cycle back with synthesised guidance
2696
- - **Rejected** \u2192 Dead-end with structural constraint
2697
-
2698
- ## Circuit Breaker
2699
- 3+ weak/rejected on same sub-type \u2192 Maqasid Check (purpose audit)
2700
-
2701
- ## Evidence Hierarchy
2702
- 1. Proof \u2192 2. Test \u2192 3a. Strong Consensus \u2192 3b. Consensus \u2192 4. Analogy \u2192 5. Judgment
2703
-
2704
- ## Commands
2705
- | Action | Command |
2706
- |--------|---------|
2707
- | Initialize | \`majlis init\` |
2708
- | Status | \`majlis status\` |
2709
- | New experiment | \`majlis new "hypothesis"\` |
2710
- | Baseline metrics | \`majlis baseline\` |
2711
- | Measure metrics | \`majlis measure\` |
2712
- | Compare metrics | \`majlis compare\` |
2713
- | Next step | \`majlis next\` |
2714
- | Auto cycle | \`majlis next --auto\` |
2715
- | Autonomous | \`majlis run "goal"\` |
2716
- | Session start | \`majlis session start "intent"\` |
2717
- | Session end | \`majlis session end\` |
2718
- | Compress | \`majlis compress\` |
2719
- | Audit | \`majlis audit "objective"\` |
2720
- `;
2721
- DOC_TEMPLATES = {
2722
- "experiments/_TEMPLATE.md": `# Experiment: {{title}}
2723
-
2724
- **Hypothesis:** {{hypothesis}}
2725
- **Branch:** {{branch}}
2726
- **Status:** {{status}}
2727
- **Sub-type:** {{sub_type}}
2728
- **Created:** {{date}}
2729
-
2730
- ## Approach
2731
-
2732
- [Describe the approach]
2733
-
2734
- ## Decisions
2735
-
2736
- - [evidence_level] Decision description \u2014 justification
2737
-
2738
- ## Results
2739
-
2740
- [Describe the results]
2741
-
2742
- ## Metrics
2743
-
2744
- | Fixture | Metric | Before | After | Delta |
2745
- |---------|--------|--------|-------|-------|
2746
- | | | | | |
2747
-
2748
- <!-- majlis-json
2749
- {
2750
- "decisions": [],
2751
- "grades": []
2752
- }
2753
- -->
2754
- `,
2755
- "decisions/_TEMPLATE.md": `# Decision: {{title}}
2756
-
2757
- **Evidence Level:** {{evidence_level}}
2758
- **Experiment:** {{experiment}}
2759
- **Date:** {{date}}
2760
-
2761
- ## Description
2762
-
2763
- [What was decided]
2764
-
2765
- ## Justification
2766
-
2767
- [Why this decision was made, referencing evidence]
2768
-
2769
- ## Alternatives Considered
2770
-
2771
- [What else was considered and why it was rejected]
2772
-
2773
- <!-- majlis-json
2774
- {
2775
- "decisions": [
2776
- { "description": "", "evidence_level": "", "justification": "" }
2777
- ]
2778
- }
2779
- -->
2780
- `,
2781
- "classification/_TEMPLATE.md": `# Classification: {{domain}}
2782
-
2783
- **Date:** {{date}}
2784
-
2785
- ## Problem Domain
2786
-
2787
- [Describe the problem domain]
2788
-
2789
- ## Sub-Types
2790
-
2791
- ### 1. {{sub_type_1}}
2792
- - **Description:**
2793
- - **Canonical form:**
2794
- - **Known constraints:**
2795
-
2796
- ### 2. {{sub_type_2}}
2797
- - **Description:**
2798
- - **Canonical form:**
2799
- - **Known constraints:**
2800
-
2801
- ## Relationships
2802
-
2803
- [How sub-types relate to each other]
2804
- `,
2805
- "doubts/_TEMPLATE.md": `# Doubt Document \u2014 Against Experiment {{experiment}}
2806
-
2807
- **Critic:** {{agent}}
2808
- **Date:** {{date}}
2809
-
2810
- ## Doubt 1: {{title}}
2811
-
2812
- **Claim doubted:** {{claim}}
2813
- **Evidence level of claim:** {{evidence_level}}
2814
- **Severity:** {{severity}}
2815
-
2816
- **Evidence for doubt:**
2817
- [Specific evidence \u2014 a prior experiment, inconsistency, untested case, or false analogy]
2818
-
2819
- <!-- majlis-json
2820
- {
2821
- "doubts": [
2822
- { "claim_doubted": "", "evidence_level_of_claim": "", "evidence_for_doubt": "", "severity": "critical" }
2823
- ]
2824
- }
2825
- -->
2826
- `,
2827
- "challenges/_TEMPLATE.md": `# Challenge Document \u2014 Against Experiment {{experiment}}
2828
-
2829
- **Adversary:** {{agent}}
2830
- **Date:** {{date}}
2831
-
2832
- ## Challenge 1: {{title}}
2833
-
2834
- **Constructed case:**
2835
- [Specific input or condition designed to break the approach]
2836
-
2837
- **Reasoning:**
2838
- [Why this case should break the approach \u2014 what assumption does it violate?]
2839
-
2840
- ## Challenge 2: {{title}}
2841
-
2842
- **Constructed case:**
2843
- [Specific input or condition]
2844
-
2845
- **Reasoning:**
2846
- [Why this should break]
2847
-
2848
- <!-- majlis-json
2849
- {
2850
- "challenges": [
2851
- { "description": "", "reasoning": "" }
2852
- ]
2853
- }
2854
- -->
2855
- `,
2856
- "verification/_TEMPLATE.md": `# Verification Report \u2014 Experiment {{experiment}}
2857
-
2858
- **Verifier:** {{agent}}
2859
- **Date:** {{date}}
2860
-
2861
- ## Provenance Check (Isnad)
2862
-
2863
- | Component | Traceable | Chain intact | Notes |
2864
- |-----------|-----------|--------------|-------|
2865
- | | yes/no | yes/no | |
2866
-
2867
- ## Content Check (Matn)
2868
-
2869
- | Component | Tests pass | Consistent | Grade | Notes |
2870
- |-----------|-----------|------------|-------|-------|
2871
- | | yes/no | yes/no | sound/good/weak/rejected | |
2872
-
2873
- ## Doubt Resolution
2874
-
2875
- | Doubt | Resolution | Evidence |
2876
- |-------|------------|----------|
2877
- | | confirmed/dismissed/inconclusive | |
2878
-
2879
- <!-- majlis-json
2880
- {
2881
- "grades": [
2882
- { "component": "", "grade": "sound", "provenance_intact": true, "content_correct": true, "notes": "" }
2883
- ],
2884
- "doubt_resolutions": [
2885
- { "doubt_id": 0, "resolution": "confirmed" }
2886
- ]
2887
- }
2888
- -->
2889
- `,
2890
- "reframes/_TEMPLATE.md": `# Reframe: {{domain}}
2891
-
2892
- **Reframer:** {{agent}}
2893
- **Date:** {{date}}
2894
-
2895
- ## Independent Decomposition
2896
-
2897
- [How this problem should be decomposed \u2014 without seeing the builder's approach]
2898
-
2899
- ## Natural Joints
2900
-
2901
- [Where does this problem naturally divide?]
2902
-
2903
- ## Cross-Domain Analogies
2904
-
2905
- [What analogies from other domains apply?]
2906
-
2907
- ## Comparison with Existing Classification
2908
-
2909
- [Structural divergences from the current classification]
2910
-
2911
- ## Divergences (Most Valuable Signals)
2912
-
2913
- [Where the independent decomposition differs from the builder's classification]
2914
- `,
2915
- "rihla/_TEMPLATE.md": `# Rihla (Scout Report): {{topic}}
2916
-
2917
- **Date:** {{date}}
2918
-
2919
- ## Problem (Domain-Neutral)
2920
-
2921
- [Describe the problem in domain-neutral terms]
2922
-
2923
- ## Alternative Approaches Found
2924
-
2925
- ### 1. {{approach}}
2926
- - **Source:**
2927
- - **Description:**
2928
- - **Applicability:**
2929
-
2930
- ## Known Limitations of Current Approach
2931
-
2932
- [What external sources say about where this approach fails]
2933
-
2934
- ## Cross-Domain Analogues
2935
-
2936
- [Structurally similar problems in unrelated domains]
2937
- `
2938
- };
2012
+ import_shared = require("@majlis/shared");
2939
2013
  }
2940
2014
  });
2941
2015
 
2942
2016
  // src/git.ts
2943
2017
  function autoCommit(root, message) {
2944
2018
  try {
2945
- (0, import_node_child_process2.execSync)("git add docs/ .majlis/scripts/ 2>/dev/null; true", {
2946
- cwd: root,
2947
- encoding: "utf-8",
2948
- stdio: ["pipe", "pipe", "pipe"]
2949
- });
2019
+ try {
2020
+ (0, import_node_child_process2.execFileSync)("git", ["add", "docs/", ".majlis/scripts/"], {
2021
+ cwd: root,
2022
+ encoding: "utf-8",
2023
+ stdio: ["pipe", "pipe", "pipe"]
2024
+ });
2025
+ } catch {
2026
+ }
2950
2027
  const diff = (0, import_node_child_process2.execSync)("git diff --cached --stat", {
2951
2028
  cwd: root,
2952
2029
  encoding: "utf-8",
2953
2030
  stdio: ["pipe", "pipe", "pipe"]
2954
2031
  }).trim();
2955
2032
  if (!diff) return;
2956
- (0, import_node_child_process2.execSync)(`git commit -m ${JSON.stringify(`[majlis] ${message}`)}`, {
2033
+ (0, import_node_child_process2.execFileSync)("git", ["commit", "-m", `[majlis] ${message}`], {
2957
2034
  cwd: root,
2958
2035
  encoding: "utf-8",
2959
2036
  stdio: ["pipe", "pipe", "pipe"]
@@ -2986,7 +2063,7 @@ async function upgrade(_args) {
2986
2063
  const claudeAgentsDir = path8.join(root, ".claude", "agents");
2987
2064
  mkdirSafe2(majlisAgentsDir);
2988
2065
  mkdirSafe2(claudeAgentsDir);
2989
- for (const [name, content] of Object.entries(AGENT_DEFINITIONS)) {
2066
+ for (const [name, content] of Object.entries(import_shared.AGENT_DEFINITIONS)) {
2990
2067
  const majlisPath = path8.join(majlisAgentsDir, `${name}.md`);
2991
2068
  const claudePath = path8.join(claudeAgentsDir, `${name}.md`);
2992
2069
  const existed = fs8.existsSync(majlisPath);
@@ -3006,7 +2083,7 @@ async function upgrade(_args) {
3006
2083
  try {
3007
2084
  for (const file of fs8.readdirSync(majlisAgentsDir)) {
3008
2085
  const name = file.replace(".md", "");
3009
- if (!AGENT_DEFINITIONS[name]) {
2086
+ if (!import_shared.AGENT_DEFINITIONS[name]) {
3010
2087
  fs8.unlinkSync(path8.join(majlisAgentsDir, file));
3011
2088
  try {
3012
2089
  fs8.unlinkSync(path8.join(claudeAgentsDir, file));
@@ -3020,7 +2097,7 @@ async function upgrade(_args) {
3020
2097
  }
3021
2098
  const commandsDir = path8.join(root, ".claude", "commands");
3022
2099
  mkdirSafe2(commandsDir);
3023
- for (const [name, cmd] of Object.entries(SLASH_COMMANDS)) {
2100
+ for (const [name, cmd] of Object.entries(import_shared.SLASH_COMMANDS)) {
3024
2101
  const cmdPath = path8.join(commandsDir, `${name}.md`);
3025
2102
  const content = `---
3026
2103
  description: ${cmd.description}
@@ -3044,14 +2121,14 @@ ${cmd.body}
3044
2121
  if (fs8.existsSync(settingsPath)) {
3045
2122
  const existing = JSON.parse(fs8.readFileSync(settingsPath, "utf-8"));
3046
2123
  const before = JSON.stringify(existing.hooks);
3047
- existing.hooks = { ...existing.hooks, ...HOOKS_CONFIG.hooks };
2124
+ existing.hooks = { ...existing.hooks, ...import_shared.HOOKS_CONFIG.hooks };
3048
2125
  if (JSON.stringify(existing.hooks) !== before) {
3049
2126
  fs8.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
3050
2127
  info(" Updated hooks in .claude/settings.json");
3051
2128
  updated++;
3052
2129
  }
3053
2130
  } else {
3054
- fs8.writeFileSync(settingsPath, JSON.stringify(HOOKS_CONFIG, null, 2));
2131
+ fs8.writeFileSync(settingsPath, JSON.stringify(import_shared.HOOKS_CONFIG, null, 2));
3055
2132
  info(" Created .claude/settings.json");
3056
2133
  added++;
3057
2134
  }
@@ -3085,7 +2162,7 @@ ${cmd.body}
3085
2162
  if (existing.includes("## Majlis Protocol")) {
3086
2163
  const replaced = existing.replace(
3087
2164
  /## Majlis Protocol[\s\S]*?(?=\n## [^M]|\n## $|$)/,
3088
- CLAUDE_MD_SECTION.trim()
2165
+ import_shared.CLAUDE_MD_SECTION.trim()
3089
2166
  );
3090
2167
  if (replaced !== existing) {
3091
2168
  fs8.writeFileSync(claudeMdPath, replaced);
@@ -3093,7 +2170,7 @@ ${cmd.body}
3093
2170
  updated++;
3094
2171
  }
3095
2172
  } else {
3096
- fs8.writeFileSync(claudeMdPath, existing + "\n" + CLAUDE_MD_SECTION);
2173
+ fs8.writeFileSync(claudeMdPath, existing + "\n" + import_shared.CLAUDE_MD_SECTION);
3097
2174
  info(" Appended Majlis Protocol to CLAUDE.md");
3098
2175
  added++;
3099
2176
  }
@@ -3127,12 +2204,13 @@ var init_upgrade = __esm({
3127
2204
  });
3128
2205
 
3129
2206
  // src/db/queries.ts
3130
- function createExperiment(db, slug, branch, hypothesis, subType, classificationRef) {
2207
+ function createExperiment(db, slug, branch, hypothesis, subType, classificationRef, dependsOn = null, contextFiles = null) {
3131
2208
  const stmt = db.prepare(`
3132
- INSERT INTO experiments (slug, branch, hypothesis, sub_type, classification_ref, status)
3133
- VALUES (?, ?, ?, ?, ?, 'classified')
2209
+ INSERT INTO experiments (slug, branch, hypothesis, sub_type, classification_ref, status, depends_on, context_files)
2210
+ VALUES (?, ?, ?, ?, ?, 'classified', ?, ?)
3134
2211
  `);
3135
- const result = stmt.run(slug, branch, hypothesis, subType, classificationRef);
2212
+ const contextJson = contextFiles && contextFiles.length > 0 ? JSON.stringify(contextFiles) : null;
2213
+ const result = stmt.run(slug, branch, hypothesis, subType, classificationRef, dependsOn, contextJson);
3136
2214
  return getExperimentById(db, result.lastInsertRowid);
3137
2215
  }
3138
2216
  function getExperimentById(db, id) {
@@ -3641,6 +2719,34 @@ async function status(isJson) {
3641
2719
  console.log(`
3642
2720
  ${yellow(`${judgmentDecisions.length} judgment-level decisions`)} (provisional targets for doubt)`);
3643
2721
  }
2722
+ console.log();
2723
+ header("Project Readiness");
2724
+ const validation = (0, import_shared2.validateProject)({
2725
+ hasGitRepo: fs9.existsSync(path9.join(root, ".git")),
2726
+ hasClaudeMd: fs9.existsSync(path9.join(root, "CLAUDE.md")),
2727
+ metricsCommand: config.metrics.command,
2728
+ metricsCommandRunnable: checkCommandRunnable(config.metrics.command, root),
2729
+ fixtures: config.metrics.fixtures,
2730
+ tracked: config.metrics.tracked,
2731
+ preMeasure: config.build.pre_measure,
2732
+ hasObjective: !!(config.project.objective && config.project.objective.length > 0),
2733
+ hasSynthesis: (() => {
2734
+ const sp = path9.join(root, "docs", "synthesis", "current.md");
2735
+ if (!fs9.existsSync(sp)) return false;
2736
+ const content = fs9.readFileSync(sp, "utf-8");
2737
+ return content.length > 100 && !content.includes("No experiments yet");
2738
+ })()
2739
+ });
2740
+ console.log((0, import_shared2.formatValidation)(validation));
2741
+ }
2742
+ function checkCommandRunnable(command, cwd) {
2743
+ if (!command || command.includes(`echo '{"fixtures":{}}'`)) return false;
2744
+ try {
2745
+ (0, import_node_child_process3.execSync)(command, { cwd, encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"] });
2746
+ return true;
2747
+ } catch {
2748
+ return false;
2749
+ }
3644
2750
  }
3645
2751
  function buildSummary(expCount, activeSession, sessionsSinceCompression, config) {
3646
2752
  const parts = [];
@@ -3651,73 +2757,205 @@ function buildSummary(expCount, activeSession, sessionsSinceCompression, config)
3651
2757
  }
3652
2758
  return parts.join(". ");
3653
2759
  }
2760
+ var fs9, path9, import_node_child_process3, import_shared2;
3654
2761
  var init_status = __esm({
3655
2762
  "src/commands/status.ts"() {
3656
2763
  "use strict";
2764
+ fs9 = __toESM(require("fs"));
2765
+ path9 = __toESM(require("path"));
2766
+ import_node_child_process3 = require("child_process");
3657
2767
  init_connection();
3658
2768
  init_queries();
3659
2769
  init_config();
2770
+ import_shared2 = require("@majlis/shared");
3660
2771
  init_format();
3661
2772
  }
3662
2773
  });
3663
2774
 
3664
- // src/metrics.ts
3665
- function compareMetrics(db, experimentId, config) {
3666
- const before = getMetricsByExperimentAndPhase(db, experimentId, "before");
3667
- const after = getMetricsByExperimentAndPhase(db, experimentId, "after");
3668
- const fixtures = new Set([...before, ...after].map((m) => m.fixture));
3669
- const trackedMetrics = Object.keys(config.metrics.tracked);
3670
- const comparisons = [];
3671
- for (const fixture of fixtures) {
3672
- for (const metric of trackedMetrics) {
3673
- const b = before.find((m) => m.fixture === fixture && m.metric_name === metric);
3674
- const a = after.find((m) => m.fixture === fixture && m.metric_name === metric);
3675
- if (b && a) {
3676
- const direction = config.metrics.tracked[metric]?.direction ?? "lower_is_better";
3677
- const regression = isRegression(b.metric_value, a.metric_value, direction);
3678
- comparisons.push({
3679
- fixture,
3680
- metric,
3681
- before: b.metric_value,
3682
- after: a.metric_value,
3683
- delta: a.metric_value - b.metric_value,
3684
- regression
3685
- });
3686
- }
3687
- }
3688
- }
3689
- return comparisons;
3690
- }
3691
- function isRegression(before, after, direction) {
3692
- switch (direction) {
3693
- case "lower_is_better":
3694
- return after > before;
3695
- case "higher_is_better":
3696
- return after < before;
3697
- case "closer_to_gt":
3698
- return false;
3699
- default:
3700
- return false;
3701
- }
3702
- }
3703
- function parseMetricsOutput(jsonStr) {
3704
- const data = JSON.parse(jsonStr);
3705
- const results = [];
3706
- if (data.fixtures && typeof data.fixtures === "object") {
3707
- for (const [fixture, metrics] of Object.entries(data.fixtures)) {
3708
- for (const [metricName, metricValue] of Object.entries(metrics)) {
3709
- if (typeof metricValue === "number") {
3710
- results.push({ fixture, metric_name: metricName, metric_value: metricValue });
3711
- }
3712
- }
3713
- }
3714
- }
3715
- return results;
2775
+ // src/state/types.ts
2776
+ function isTerminalStatus(status2) {
2777
+ return TRANSITIONS[status2].length === 0;
3716
2778
  }
3717
- var init_metrics = __esm({
3718
- "src/metrics.ts"() {
2779
+ var TRANSITIONS, GRADE_ORDER, ADMIN_TRANSITIONS;
2780
+ var init_types2 = __esm({
2781
+ "src/state/types.ts"() {
3719
2782
  "use strict";
3720
- init_queries();
2783
+ TRANSITIONS = {
2784
+ ["classified" /* CLASSIFIED */]: ["reframed" /* REFRAMED */, "gated" /* GATED */],
2785
+ ["reframed" /* REFRAMED */]: ["gated" /* GATED */],
2786
+ ["gated" /* GATED */]: ["building" /* BUILDING */, "gated" /* GATED */],
2787
+ // self-loop for rejected hypotheses
2788
+ ["building" /* BUILDING */]: ["built" /* BUILT */, "building" /* BUILDING */],
2789
+ // self-loop for retry after truncation
2790
+ ["built" /* BUILT */]: ["challenged" /* CHALLENGED */, "doubted" /* DOUBTED */],
2791
+ ["challenged" /* CHALLENGED */]: ["doubted" /* DOUBTED */, "verifying" /* VERIFYING */],
2792
+ ["doubted" /* DOUBTED */]: ["challenged" /* CHALLENGED */, "scouted" /* SCOUTED */, "verifying" /* VERIFYING */],
2793
+ ["scouted" /* SCOUTED */]: ["verifying" /* VERIFYING */],
2794
+ ["verifying" /* VERIFYING */]: ["verified" /* VERIFIED */],
2795
+ ["verified" /* VERIFIED */]: ["resolved" /* RESOLVED */],
2796
+ ["resolved" /* RESOLVED */]: ["compressed" /* COMPRESSED */, "building" /* BUILDING */, "merged" /* MERGED */, "dead_end" /* DEAD_END */],
2797
+ ["compressed" /* COMPRESSED */]: ["merged" /* MERGED */, "building" /* BUILDING */],
2798
+ // cycle-back skips gate
2799
+ ["merged" /* MERGED */]: [],
2800
+ ["dead_end" /* DEAD_END */]: []
2801
+ };
2802
+ GRADE_ORDER = ["rejected", "weak", "good", "sound"];
2803
+ ADMIN_TRANSITIONS = {
2804
+ revert: (current, target) => target === "dead_end" /* DEAD_END */ && !isTerminalStatus(current),
2805
+ circuit_breaker: (current, target) => target === "dead_end" /* DEAD_END */ && !isTerminalStatus(current),
2806
+ error_recovery: (current, target) => target === "dead_end" /* DEAD_END */ && !isTerminalStatus(current),
2807
+ bootstrap: (current, target) => current === "classified" /* CLASSIFIED */ && target === "reframed" /* REFRAMED */
2808
+ };
2809
+ }
2810
+ });
2811
+
2812
+ // src/state/machine.ts
2813
+ function transition(current, target) {
2814
+ const valid = TRANSITIONS[current];
2815
+ if (!valid.includes(target)) {
2816
+ throw new Error(
2817
+ `Invalid transition: ${current} \u2192 ${target}. Valid: [${valid.join(", ")}]`
2818
+ );
2819
+ }
2820
+ return target;
2821
+ }
2822
+ function validNext(current) {
2823
+ return TRANSITIONS[current];
2824
+ }
2825
+ function isTerminal(status2) {
2826
+ return TRANSITIONS[status2].length === 0;
2827
+ }
2828
+ function adminTransition(current, target, reason) {
2829
+ const allowed = ADMIN_TRANSITIONS[reason];
2830
+ if (!allowed(current, target)) {
2831
+ throw new Error(
2832
+ `Invalid admin transition (${reason}): ${current} \u2192 ${target}`
2833
+ );
2834
+ }
2835
+ return target;
2836
+ }
2837
+ function transitionAndPersist(db, experimentId, current, target) {
2838
+ const result = transition(current, target);
2839
+ updateExperimentStatus(db, experimentId, result);
2840
+ return result;
2841
+ }
2842
+ function adminTransitionAndPersist(db, experimentId, current, target, reason) {
2843
+ const result = adminTransition(current, target, reason);
2844
+ updateExperimentStatus(db, experimentId, result);
2845
+ return result;
2846
+ }
2847
+ function determineNextStep(exp, valid, hasDoubts2, hasChallenges2) {
2848
+ if (valid.length === 0) {
2849
+ throw new Error(`Experiment ${exp.slug} is terminal (${exp.status})`);
2850
+ }
2851
+ const status2 = exp.status;
2852
+ if (status2 === "classified" /* CLASSIFIED */ || status2 === "reframed" /* REFRAMED */) {
2853
+ return valid.includes("gated" /* GATED */) ? "gated" /* GATED */ : valid[0];
2854
+ }
2855
+ if (status2 === "gated" /* GATED */) {
2856
+ return valid.includes("building" /* BUILDING */) ? "building" /* BUILDING */ : valid[0];
2857
+ }
2858
+ if (status2 === "built" /* BUILT */ && !hasDoubts2) {
2859
+ return valid.includes("doubted" /* DOUBTED */) ? "doubted" /* DOUBTED */ : valid[0];
2860
+ }
2861
+ if (status2 === "doubted" /* DOUBTED */ && !hasChallenges2) {
2862
+ return valid.includes("challenged" /* CHALLENGED */) ? "challenged" /* CHALLENGED */ : valid[0];
2863
+ }
2864
+ if (status2 === "doubted" /* DOUBTED */ || status2 === "challenged" /* CHALLENGED */) {
2865
+ if (valid.includes("verifying" /* VERIFYING */)) {
2866
+ return "verifying" /* VERIFYING */;
2867
+ }
2868
+ }
2869
+ if (status2 === "building" /* BUILDING */) {
2870
+ return valid.includes("built" /* BUILT */) ? "built" /* BUILT */ : valid[0];
2871
+ }
2872
+ if (status2 === "scouted" /* SCOUTED */) {
2873
+ return valid.includes("verifying" /* VERIFYING */) ? "verifying" /* VERIFYING */ : valid[0];
2874
+ }
2875
+ if (status2 === "verified" /* VERIFIED */) {
2876
+ return valid.includes("resolved" /* RESOLVED */) ? "resolved" /* RESOLVED */ : valid[0];
2877
+ }
2878
+ if (status2 === "compressed" /* COMPRESSED */) {
2879
+ return valid.includes("merged" /* MERGED */) ? "merged" /* MERGED */ : valid[0];
2880
+ }
2881
+ return valid[0];
2882
+ }
2883
+ var init_machine = __esm({
2884
+ "src/state/machine.ts"() {
2885
+ "use strict";
2886
+ init_types2();
2887
+ init_queries();
2888
+ }
2889
+ });
2890
+
2891
+ // src/metrics.ts
2892
+ function isGateFixture(fixtures, fixtureName) {
2893
+ if (Array.isArray(fixtures)) return false;
2894
+ return fixtures[fixtureName]?.gate === true;
2895
+ }
2896
+ function compareMetrics(db, experimentId, config) {
2897
+ const before = getMetricsByExperimentAndPhase(db, experimentId, "before");
2898
+ const after = getMetricsByExperimentAndPhase(db, experimentId, "after");
2899
+ const fixtures = new Set([...before, ...after].map((m) => m.fixture));
2900
+ const trackedMetrics = Object.keys(config.metrics.tracked);
2901
+ const comparisons = [];
2902
+ for (const fixture of fixtures) {
2903
+ for (const metric of trackedMetrics) {
2904
+ const b = before.find((m) => m.fixture === fixture && m.metric_name === metric);
2905
+ const a = after.find((m) => m.fixture === fixture && m.metric_name === metric);
2906
+ if (b && a) {
2907
+ const tracked = config.metrics.tracked[metric];
2908
+ const direction = tracked?.direction ?? "lower_is_better";
2909
+ const target = tracked?.target;
2910
+ const regression = isRegression(b.metric_value, a.metric_value, direction, target);
2911
+ comparisons.push({
2912
+ fixture,
2913
+ metric,
2914
+ before: b.metric_value,
2915
+ after: a.metric_value,
2916
+ delta: a.metric_value - b.metric_value,
2917
+ regression,
2918
+ gate: isGateFixture(config.metrics.fixtures, fixture)
2919
+ });
2920
+ }
2921
+ }
2922
+ }
2923
+ return comparisons;
2924
+ }
2925
+ function checkGateViolations(comparisons) {
2926
+ return comparisons.filter((c) => c.gate && c.regression);
2927
+ }
2928
+ function isRegression(before, after, direction, target) {
2929
+ switch (direction) {
2930
+ case "lower_is_better":
2931
+ return after > before;
2932
+ case "higher_is_better":
2933
+ return after < before;
2934
+ case "closer_to_gt":
2935
+ if (target === void 0) return false;
2936
+ return Math.abs(after - target) > Math.abs(before - target);
2937
+ default:
2938
+ return false;
2939
+ }
2940
+ }
2941
+ function parseMetricsOutput(jsonStr) {
2942
+ const data = JSON.parse(jsonStr);
2943
+ const results = [];
2944
+ if (data.fixtures && typeof data.fixtures === "object") {
2945
+ for (const [fixture, metrics] of Object.entries(data.fixtures)) {
2946
+ for (const [metricName, metricValue] of Object.entries(metrics)) {
2947
+ if (typeof metricValue === "number") {
2948
+ results.push({ fixture, metric_name: metricName, metric_value: metricValue });
2949
+ }
2950
+ }
2951
+ }
2952
+ }
2953
+ return results;
2954
+ }
2955
+ var init_metrics = __esm({
2956
+ "src/metrics.ts"() {
2957
+ "use strict";
2958
+ init_queries();
3721
2959
  }
3722
2960
  });
3723
2961
 
@@ -3750,7 +2988,7 @@ async function captureMetrics(phase, args) {
3750
2988
  if (config.build.pre_measure) {
3751
2989
  info(`Running pre-measure: ${config.build.pre_measure}`);
3752
2990
  try {
3753
- (0, import_node_child_process3.execSync)(config.build.pre_measure, { cwd: root, encoding: "utf-8", stdio: "inherit" });
2991
+ (0, import_node_child_process4.execSync)(config.build.pre_measure, { cwd: root, encoding: "utf-8", stdio: "inherit" });
3754
2992
  } catch {
3755
2993
  warn("Pre-measure command failed \u2014 continuing anyway.");
3756
2994
  }
@@ -3761,7 +2999,7 @@ async function captureMetrics(phase, args) {
3761
2999
  info(`Running metrics: ${config.metrics.command}`);
3762
3000
  let metricsOutput;
3763
3001
  try {
3764
- metricsOutput = (0, import_node_child_process3.execSync)(config.metrics.command, {
3002
+ metricsOutput = (0, import_node_child_process4.execSync)(config.metrics.command, {
3765
3003
  cwd: root,
3766
3004
  encoding: "utf-8",
3767
3005
  stdio: ["pipe", "pipe", "pipe"]
@@ -3780,7 +3018,7 @@ async function captureMetrics(phase, args) {
3780
3018
  success(`Captured ${parsed.length} metric(s) for ${exp.slug} (phase: ${phase})`);
3781
3019
  if (config.build.post_measure) {
3782
3020
  try {
3783
- (0, import_node_child_process3.execSync)(config.build.post_measure, { cwd: root, encoding: "utf-8", stdio: "inherit" });
3021
+ (0, import_node_child_process4.execSync)(config.build.post_measure, { cwd: root, encoding: "utf-8", stdio: "inherit" });
3784
3022
  } catch {
3785
3023
  warn("Post-measure command failed.");
3786
3024
  }
@@ -3831,11 +3069,11 @@ function formatDelta(delta) {
3831
3069
  const prefix = delta > 0 ? "+" : "";
3832
3070
  return `${prefix}${delta.toFixed(4)}`;
3833
3071
  }
3834
- var import_node_child_process3;
3072
+ var import_node_child_process4;
3835
3073
  var init_measure = __esm({
3836
3074
  "src/commands/measure.ts"() {
3837
3075
  "use strict";
3838
- import_node_child_process3 = require("child_process");
3076
+ import_node_child_process4 = require("child_process");
3839
3077
  init_connection();
3840
3078
  init_queries();
3841
3079
  init_metrics();
@@ -3868,7 +3106,7 @@ async function newExperiment(args) {
3868
3106
  const paddedNum = String(num).padStart(3, "0");
3869
3107
  const branch = `exp/${paddedNum}-${slug}`;
3870
3108
  try {
3871
- (0, import_node_child_process4.execSync)(`git checkout -b ${branch}`, {
3109
+ (0, import_node_child_process5.execFileSync)("git", ["checkout", "-b", branch], {
3872
3110
  cwd: root,
3873
3111
  encoding: "utf-8",
3874
3112
  stdio: ["pipe", "pipe", "pipe"]
@@ -3878,15 +3116,28 @@ async function newExperiment(args) {
3878
3116
  warn(`Could not create branch ${branch} \u2014 continuing without git branch.`);
3879
3117
  }
3880
3118
  const subType = getFlagValue(args, "--sub-type") ?? null;
3881
- const exp = createExperiment(db, slug, branch, hypothesis, subType, null);
3119
+ const dependsOn = getFlagValue(args, "--depends-on") ?? null;
3120
+ const contextArg = getFlagValue(args, "--context") ?? null;
3121
+ const contextFiles = contextArg ? contextArg.split(",").map((f) => f.trim()) : null;
3122
+ if (dependsOn) {
3123
+ const depExp = getExperimentBySlug(db, dependsOn);
3124
+ if (!depExp) {
3125
+ throw new Error(`Dependency experiment not found: ${dependsOn}`);
3126
+ }
3127
+ info(`Depends on: ${dependsOn} (status: ${depExp.status})`);
3128
+ }
3129
+ const exp = createExperiment(db, slug, branch, hypothesis, subType, null, dependsOn, contextFiles);
3130
+ if (contextFiles) {
3131
+ info(`Context files: ${contextFiles.join(", ")}`);
3132
+ }
3882
3133
  success(`Created experiment #${exp.id}: ${exp.slug}`);
3883
- const docsDir = path9.join(root, "docs", "experiments");
3884
- const templatePath = path9.join(docsDir, "_TEMPLATE.md");
3885
- if (fs9.existsSync(templatePath)) {
3886
- const template = fs9.readFileSync(templatePath, "utf-8");
3134
+ const docsDir = path10.join(root, "docs", "experiments");
3135
+ const templatePath = path10.join(docsDir, "_TEMPLATE.md");
3136
+ if (fs10.existsSync(templatePath)) {
3137
+ const template = fs10.readFileSync(templatePath, "utf-8");
3887
3138
  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]);
3888
- const logPath = path9.join(docsDir, `${paddedNum}-${slug}.md`);
3889
- fs9.writeFileSync(logPath, logContent);
3139
+ const logPath = path10.join(docsDir, `${paddedNum}-${slug}.md`);
3140
+ fs10.writeFileSync(logPath, logContent);
3890
3141
  info(`Created experiment log: docs/experiments/${paddedNum}-${slug}.md`);
3891
3142
  }
3892
3143
  autoCommit(root, `new: ${slug}`);
@@ -3924,33 +3175,43 @@ async function revert(args) {
3924
3175
  exp.sub_type,
3925
3176
  category
3926
3177
  );
3927
- updateExperimentStatus(db, exp.id, "dead_end");
3178
+ adminTransitionAndPersist(db, exp.id, exp.status, "dead_end" /* DEAD_END */, "revert");
3928
3179
  try {
3929
- const currentBranch = (0, import_node_child_process4.execSync)("git rev-parse --abbrev-ref HEAD", {
3180
+ const currentBranch = (0, import_node_child_process5.execFileSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
3930
3181
  cwd: root,
3931
3182
  encoding: "utf-8"
3932
3183
  }).trim();
3933
3184
  if (currentBranch === exp.branch) {
3934
- (0, import_node_child_process4.execSync)("git checkout main 2>/dev/null || git checkout master", {
3935
- cwd: root,
3936
- encoding: "utf-8",
3937
- stdio: ["pipe", "pipe", "pipe"]
3938
- });
3185
+ try {
3186
+ (0, import_node_child_process5.execFileSync)("git", ["checkout", "main"], {
3187
+ cwd: root,
3188
+ encoding: "utf-8",
3189
+ stdio: ["pipe", "pipe", "pipe"]
3190
+ });
3191
+ } catch {
3192
+ (0, import_node_child_process5.execFileSync)("git", ["checkout", "master"], {
3193
+ cwd: root,
3194
+ encoding: "utf-8",
3195
+ stdio: ["pipe", "pipe", "pipe"]
3196
+ });
3197
+ }
3939
3198
  }
3940
3199
  } catch {
3941
3200
  warn("Could not switch git branches \u2014 do this manually.");
3942
3201
  }
3943
3202
  info(`Experiment ${exp.slug} reverted to dead-end. Reason: ${reason}`);
3944
3203
  }
3945
- var fs9, path9, import_node_child_process4;
3204
+ var fs10, path10, import_node_child_process5;
3946
3205
  var init_experiment = __esm({
3947
3206
  "src/commands/experiment.ts"() {
3948
3207
  "use strict";
3949
- fs9 = __toESM(require("fs"));
3950
- path9 = __toESM(require("path"));
3951
- import_node_child_process4 = require("child_process");
3208
+ fs10 = __toESM(require("fs"));
3209
+ path10 = __toESM(require("path"));
3210
+ import_node_child_process5 = require("child_process");
3952
3211
  init_connection();
3953
3212
  init_queries();
3213
+ init_machine();
3214
+ init_types2();
3954
3215
  init_config();
3955
3216
  init_spawn();
3956
3217
  init_git();
@@ -4089,12 +3350,12 @@ function queryDeadEnds(db, args, isJson) {
4089
3350
  console.log(table(["ID", "Sub-Type", "Approach", "Constraint"], rows));
4090
3351
  }
4091
3352
  function queryFragility(root, isJson) {
4092
- const fragPath = path10.join(root, "docs", "synthesis", "fragility.md");
4093
- if (!fs10.existsSync(fragPath)) {
3353
+ const fragPath = path11.join(root, "docs", "synthesis", "fragility.md");
3354
+ if (!fs11.existsSync(fragPath)) {
4094
3355
  info("No fragility map found.");
4095
3356
  return;
4096
3357
  }
4097
- const content = fs10.readFileSync(fragPath, "utf-8");
3358
+ const content = fs11.readFileSync(fragPath, "utf-8");
4098
3359
  if (isJson) {
4099
3360
  console.log(JSON.stringify({ content }, null, 2));
4100
3361
  return;
@@ -4150,7 +3411,7 @@ function queryCircuitBreakers(db, root, isJson) {
4150
3411
  function checkCommit(db) {
4151
3412
  let stdinData = "";
4152
3413
  try {
4153
- stdinData = fs10.readFileSync(0, "utf-8");
3414
+ stdinData = fs11.readFileSync(0, "utf-8");
4154
3415
  } catch {
4155
3416
  }
4156
3417
  if (stdinData) {
@@ -4175,12 +3436,12 @@ function checkCommit(db) {
4175
3436
  process.exit(1);
4176
3437
  }
4177
3438
  }
4178
- var fs10, path10;
3439
+ var fs11, path11;
4179
3440
  var init_query = __esm({
4180
3441
  "src/commands/query.ts"() {
4181
3442
  "use strict";
4182
- fs10 = __toESM(require("fs"));
4183
- path10 = __toESM(require("path"));
3443
+ fs11 = __toESM(require("fs"));
3444
+ path11 = __toESM(require("path"));
4184
3445
  init_connection();
4185
3446
  init_queries();
4186
3447
  init_config();
@@ -4188,93 +3449,17 @@ var init_query = __esm({
4188
3449
  }
4189
3450
  });
4190
3451
 
4191
- // src/state/types.ts
4192
- var TRANSITIONS, GRADE_ORDER;
4193
- var init_types2 = __esm({
4194
- "src/state/types.ts"() {
4195
- "use strict";
4196
- TRANSITIONS = {
4197
- ["classified" /* CLASSIFIED */]: ["reframed" /* REFRAMED */, "gated" /* GATED */],
4198
- ["reframed" /* REFRAMED */]: ["gated" /* GATED */],
4199
- ["gated" /* GATED */]: ["building" /* BUILDING */, "gated" /* GATED */],
4200
- // self-loop for rejected hypotheses
4201
- ["building" /* BUILDING */]: ["built" /* BUILT */, "building" /* BUILDING */],
4202
- // self-loop for retry after truncation
4203
- ["built" /* BUILT */]: ["challenged" /* CHALLENGED */, "doubted" /* DOUBTED */],
4204
- ["challenged" /* CHALLENGED */]: ["doubted" /* DOUBTED */, "verifying" /* VERIFYING */],
4205
- ["doubted" /* DOUBTED */]: ["challenged" /* CHALLENGED */, "scouted" /* SCOUTED */, "verifying" /* VERIFYING */],
4206
- ["scouted" /* SCOUTED */]: ["verifying" /* VERIFYING */],
4207
- ["verifying" /* VERIFYING */]: ["verified" /* VERIFIED */],
4208
- ["verified" /* VERIFIED */]: ["resolved" /* RESOLVED */],
4209
- ["resolved" /* RESOLVED */]: ["compressed" /* COMPRESSED */, "building" /* BUILDING */],
4210
- // cycle-back skips gate
4211
- ["compressed" /* COMPRESSED */]: ["merged" /* MERGED */, "building" /* BUILDING */],
4212
- // cycle-back skips gate
4213
- ["merged" /* MERGED */]: [],
4214
- ["dead_end" /* DEAD_END */]: []
4215
- };
4216
- GRADE_ORDER = ["rejected", "weak", "good", "sound"];
4217
- }
4218
- });
4219
-
4220
- // src/state/machine.ts
4221
- function transition(current, target) {
4222
- const valid = TRANSITIONS[current];
4223
- if (!valid.includes(target)) {
4224
- throw new Error(
4225
- `Invalid transition: ${current} \u2192 ${target}. Valid: [${valid.join(", ")}]`
4226
- );
4227
- }
4228
- return target;
4229
- }
4230
- function validNext(current) {
4231
- return TRANSITIONS[current];
4232
- }
4233
- function isTerminal(status2) {
4234
- return TRANSITIONS[status2].length === 0;
4235
- }
4236
- function determineNextStep(exp, valid, hasDoubts2, hasChallenges2) {
4237
- if (valid.length === 0) {
4238
- throw new Error(`Experiment ${exp.slug} is terminal (${exp.status})`);
4239
- }
4240
- const status2 = exp.status;
4241
- if (status2 === "classified" /* CLASSIFIED */ || status2 === "reframed" /* REFRAMED */) {
4242
- return valid.includes("gated" /* GATED */) ? "gated" /* GATED */ : valid[0];
4243
- }
4244
- if (status2 === "gated" /* GATED */) {
4245
- return valid.includes("building" /* BUILDING */) ? "building" /* BUILDING */ : valid[0];
4246
- }
4247
- if (status2 === "built" /* BUILT */ && !hasDoubts2) {
4248
- return valid.includes("doubted" /* DOUBTED */) ? "doubted" /* DOUBTED */ : valid[0];
4249
- }
4250
- if (status2 === "doubted" /* DOUBTED */ && !hasChallenges2) {
4251
- return valid.includes("challenged" /* CHALLENGED */) ? "challenged" /* CHALLENGED */ : valid[0];
4252
- }
4253
- if (status2 === "doubted" /* DOUBTED */ || status2 === "challenged" /* CHALLENGED */) {
4254
- if (valid.includes("verifying" /* VERIFYING */)) {
4255
- return "verifying" /* VERIFYING */;
4256
- }
4257
- }
4258
- if (status2 === "compressed" /* COMPRESSED */) {
4259
- return valid.includes("merged" /* MERGED */) ? "merged" /* MERGED */ : valid[0];
4260
- }
4261
- return valid[0];
4262
- }
4263
- var init_machine = __esm({
4264
- "src/state/machine.ts"() {
4265
- "use strict";
4266
- init_types2();
4267
- }
4268
- });
4269
-
4270
3452
  // src/resolve.ts
4271
3453
  function worstGrade(grades) {
3454
+ if (grades.length === 0) {
3455
+ throw new Error("Cannot determine grade from empty verification set \u2014 this indicates a data integrity issue");
3456
+ }
4272
3457
  for (const grade of GRADE_ORDER) {
4273
3458
  if (grades.some((g) => g.grade === grade)) return grade;
4274
3459
  }
4275
3460
  return "sound";
4276
3461
  }
4277
- async function resolve(db, exp, projectRoot) {
3462
+ async function resolve2(db, exp, projectRoot) {
4278
3463
  let grades = getVerificationsByExperiment(db, exp.id);
4279
3464
  if (grades.length === 0) {
4280
3465
  warn(`No verification records for ${exp.slug}. Defaulting to weak.`);
@@ -4290,9 +3475,33 @@ async function resolve(db, exp, projectRoot) {
4290
3475
  grades = getVerificationsByExperiment(db, exp.id);
4291
3476
  }
4292
3477
  const overallGrade = worstGrade(grades);
3478
+ const config = loadConfig(projectRoot);
3479
+ const metricComparisons = compareMetrics(db, exp.id, config);
3480
+ const gateViolations = checkGateViolations(metricComparisons);
3481
+ if (gateViolations.length > 0 && (overallGrade === "sound" || overallGrade === "good")) {
3482
+ warn("Gate fixture regression detected \u2014 blocking merge:");
3483
+ for (const v of gateViolations) {
3484
+ warn(` ${v.fixture} / ${v.metric}: ${v.before} \u2192 ${v.after} (${v.delta > 0 ? "+" : ""}${v.delta})`);
3485
+ }
3486
+ updateExperimentStatus(db, exp.id, "resolved");
3487
+ const guidanceText = `Gate fixture regression blocks merge. Fix these regressions before re-attempting:
3488
+ ` + gateViolations.map((v) => `- ${v.fixture} / ${v.metric}: was ${v.before}, now ${v.after}`).join("\n");
3489
+ transition("resolved" /* RESOLVED */, "building" /* BUILDING */);
3490
+ db.transaction(() => {
3491
+ storeBuilderGuidance(db, exp.id, guidanceText);
3492
+ updateExperimentStatus(db, exp.id, "building");
3493
+ if (exp.sub_type) {
3494
+ incrementSubTypeFailure(db, exp.sub_type, exp.id, "weak");
3495
+ }
3496
+ })();
3497
+ warn(`Experiment ${exp.slug} CYCLING BACK \u2014 gate fixture(s) regressed.`);
3498
+ return;
3499
+ }
3500
+ updateExperimentStatus(db, exp.id, "resolved");
4293
3501
  switch (overallGrade) {
4294
3502
  case "sound": {
4295
3503
  gitMerge(exp.branch, projectRoot);
3504
+ transition("resolved" /* RESOLVED */, "merged" /* MERGED */);
4296
3505
  updateExperimentStatus(db, exp.id, "merged");
4297
3506
  success(`Experiment ${exp.slug} MERGED (all sound).`);
4298
3507
  break;
@@ -4302,6 +3511,7 @@ async function resolve(db, exp, projectRoot) {
4302
3511
  const gaps = grades.filter((g) => g.grade === "good").map((g) => `- **${g.component}**: ${g.notes ?? "minor gaps"}`).join("\n");
4303
3512
  appendToFragilityMap(projectRoot, exp.slug, gaps);
4304
3513
  autoCommit(projectRoot, `resolve: fragility gaps from ${exp.slug}`);
3514
+ transition("resolved" /* RESOLVED */, "merged" /* MERGED */);
4305
3515
  updateExperimentStatus(db, exp.id, "merged");
4306
3516
  success(`Experiment ${exp.slug} MERGED (good, ${grades.filter((g) => g.grade === "good").length} gaps added to fragility map).`);
4307
3517
  break;
@@ -4322,6 +3532,7 @@ async function resolve(db, exp, projectRoot) {
4322
3532
  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."
4323
3533
  }, projectRoot);
4324
3534
  const guidanceText = guidance.structured?.guidance ?? guidance.output;
3535
+ transition("resolved" /* RESOLVED */, "building" /* BUILDING */);
4325
3536
  db.transaction(() => {
4326
3537
  storeBuilderGuidance(db, exp.id, guidanceText);
4327
3538
  updateExperimentStatus(db, exp.id, "building");
@@ -4336,6 +3547,7 @@ async function resolve(db, exp, projectRoot) {
4336
3547
  gitRevert(exp.branch, projectRoot);
4337
3548
  const rejectedComponents = grades.filter((g) => g.grade === "rejected");
4338
3549
  const whyFailed = rejectedComponents.map((r) => r.notes ?? "rejected").join("; ");
3550
+ transition("resolved" /* RESOLVED */, "dead_end" /* DEAD_END */);
4339
3551
  db.transaction(() => {
4340
3552
  insertDeadEnd(
4341
3553
  db,
@@ -4372,14 +3584,39 @@ async function resolveDbOnly(db, exp, projectRoot) {
4372
3584
  grades = getVerificationsByExperiment(db, exp.id);
4373
3585
  }
4374
3586
  const overallGrade = worstGrade(grades);
3587
+ const config = loadConfig(projectRoot);
3588
+ const metricComparisons = compareMetrics(db, exp.id, config);
3589
+ const gateViolations = checkGateViolations(metricComparisons);
3590
+ if (gateViolations.length > 0 && (overallGrade === "sound" || overallGrade === "good")) {
3591
+ warn("Gate fixture regression detected \u2014 blocking merge:");
3592
+ for (const v of gateViolations) {
3593
+ warn(` ${v.fixture} / ${v.metric}: ${v.before} \u2192 ${v.after} (${v.delta > 0 ? "+" : ""}${v.delta})`);
3594
+ }
3595
+ updateExperimentStatus(db, exp.id, "resolved");
3596
+ const guidanceText = `Gate fixture regression blocks merge. Fix these regressions before re-attempting:
3597
+ ` + gateViolations.map((v) => `- ${v.fixture} / ${v.metric}: was ${v.before}, now ${v.after}`).join("\n");
3598
+ transition("resolved" /* RESOLVED */, "building" /* BUILDING */);
3599
+ db.transaction(() => {
3600
+ storeBuilderGuidance(db, exp.id, guidanceText);
3601
+ updateExperimentStatus(db, exp.id, "building");
3602
+ if (exp.sub_type) {
3603
+ incrementSubTypeFailure(db, exp.sub_type, exp.id, "weak");
3604
+ }
3605
+ })();
3606
+ warn(`Experiment ${exp.slug} CYCLING BACK \u2014 gate fixture(s) regressed.`);
3607
+ return "weak";
3608
+ }
3609
+ updateExperimentStatus(db, exp.id, "resolved");
4375
3610
  switch (overallGrade) {
4376
3611
  case "sound":
3612
+ transition("resolved" /* RESOLVED */, "merged" /* MERGED */);
4377
3613
  updateExperimentStatus(db, exp.id, "merged");
4378
3614
  success(`Experiment ${exp.slug} RESOLVED (sound) \u2014 git merge deferred.`);
4379
3615
  break;
4380
3616
  case "good": {
4381
3617
  const gaps = grades.filter((g) => g.grade === "good").map((g) => `- **${g.component}**: ${g.notes ?? "minor gaps"}`).join("\n");
4382
3618
  appendToFragilityMap(projectRoot, exp.slug, gaps);
3619
+ transition("resolved" /* RESOLVED */, "merged" /* MERGED */);
4383
3620
  updateExperimentStatus(db, exp.id, "merged");
4384
3621
  success(`Experiment ${exp.slug} RESOLVED (good) \u2014 git merge deferred.`);
4385
3622
  break;
@@ -4400,6 +3637,7 @@ async function resolveDbOnly(db, exp, projectRoot) {
4400
3637
  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."
4401
3638
  }, projectRoot);
4402
3639
  const guidanceText = guidance.structured?.guidance ?? guidance.output;
3640
+ transition("resolved" /* RESOLVED */, "building" /* BUILDING */);
4403
3641
  db.transaction(() => {
4404
3642
  storeBuilderGuidance(db, exp.id, guidanceText);
4405
3643
  updateExperimentStatus(db, exp.id, "building");
@@ -4413,6 +3651,7 @@ async function resolveDbOnly(db, exp, projectRoot) {
4413
3651
  case "rejected": {
4414
3652
  const rejectedComponents = grades.filter((g) => g.grade === "rejected");
4415
3653
  const whyFailed = rejectedComponents.map((r) => r.notes ?? "rejected").join("; ");
3654
+ transition("resolved" /* RESOLVED */, "dead_end" /* DEAD_END */);
4416
3655
  db.transaction(() => {
4417
3656
  insertDeadEnd(
4418
3657
  db,
@@ -4436,12 +3675,20 @@ async function resolveDbOnly(db, exp, projectRoot) {
4436
3675
  }
4437
3676
  function gitMerge(branch, cwd) {
4438
3677
  try {
4439
- (0, import_node_child_process5.execSync)("git checkout main 2>/dev/null || git checkout master", {
4440
- cwd,
4441
- encoding: "utf-8",
4442
- stdio: ["pipe", "pipe", "pipe"]
4443
- });
4444
- (0, import_node_child_process5.execSync)(`git merge ${branch} --no-ff -m "Merge experiment branch ${branch}"`, {
3678
+ try {
3679
+ (0, import_node_child_process6.execFileSync)("git", ["checkout", "main"], {
3680
+ cwd,
3681
+ encoding: "utf-8",
3682
+ stdio: ["pipe", "pipe", "pipe"]
3683
+ });
3684
+ } catch {
3685
+ (0, import_node_child_process6.execFileSync)("git", ["checkout", "master"], {
3686
+ cwd,
3687
+ encoding: "utf-8",
3688
+ stdio: ["pipe", "pipe", "pipe"]
3689
+ });
3690
+ }
3691
+ (0, import_node_child_process6.execFileSync)("git", ["merge", branch, "--no-ff", "-m", `Merge experiment branch ${branch}`], {
4445
3692
  cwd,
4446
3693
  encoding: "utf-8",
4447
3694
  stdio: ["pipe", "pipe", "pipe"]
@@ -4452,47 +3699,58 @@ function gitMerge(branch, cwd) {
4452
3699
  }
4453
3700
  function gitRevert(branch, cwd) {
4454
3701
  try {
4455
- const currentBranch = (0, import_node_child_process5.execSync)("git rev-parse --abbrev-ref HEAD", {
3702
+ const currentBranch = (0, import_node_child_process6.execFileSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
4456
3703
  cwd,
4457
3704
  encoding: "utf-8"
4458
3705
  }).trim();
4459
3706
  if (currentBranch === branch) {
4460
3707
  try {
4461
- (0, import_node_child_process5.execSync)("git checkout -- .", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
3708
+ (0, import_node_child_process6.execFileSync)("git", ["checkout", "--", "."], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
4462
3709
  } catch {
4463
3710
  }
4464
- (0, import_node_child_process5.execSync)("git checkout main 2>/dev/null || git checkout master", {
4465
- cwd,
4466
- encoding: "utf-8",
4467
- stdio: ["pipe", "pipe", "pipe"]
4468
- });
3711
+ try {
3712
+ (0, import_node_child_process6.execFileSync)("git", ["checkout", "main"], {
3713
+ cwd,
3714
+ encoding: "utf-8",
3715
+ stdio: ["pipe", "pipe", "pipe"]
3716
+ });
3717
+ } catch {
3718
+ (0, import_node_child_process6.execFileSync)("git", ["checkout", "master"], {
3719
+ cwd,
3720
+ encoding: "utf-8",
3721
+ stdio: ["pipe", "pipe", "pipe"]
3722
+ });
3723
+ }
4469
3724
  }
4470
3725
  } catch {
4471
3726
  console.warn(`[majlis] Could not switch away from ${branch} \u2014 you may need to do this manually.`);
4472
3727
  }
4473
3728
  }
4474
3729
  function appendToFragilityMap(projectRoot, expSlug, gaps) {
4475
- const fragPath = path11.join(projectRoot, "docs", "synthesis", "fragility.md");
3730
+ const fragPath = path12.join(projectRoot, "docs", "synthesis", "fragility.md");
4476
3731
  let content = "";
4477
- if (fs11.existsSync(fragPath)) {
4478
- content = fs11.readFileSync(fragPath, "utf-8");
3732
+ if (fs12.existsSync(fragPath)) {
3733
+ content = fs12.readFileSync(fragPath, "utf-8");
4479
3734
  }
4480
3735
  const entry = `
4481
3736
  ## From experiment: ${expSlug}
4482
3737
  ${gaps}
4483
3738
  `;
4484
- fs11.writeFileSync(fragPath, content + entry);
3739
+ fs12.writeFileSync(fragPath, content + entry);
4485
3740
  }
4486
- var fs11, path11, import_node_child_process5;
3741
+ var fs12, path12, import_node_child_process6;
4487
3742
  var init_resolve = __esm({
4488
3743
  "src/resolve.ts"() {
4489
3744
  "use strict";
4490
- fs11 = __toESM(require("fs"));
4491
- path11 = __toESM(require("path"));
3745
+ fs12 = __toESM(require("fs"));
3746
+ path12 = __toESM(require("path"));
4492
3747
  init_types2();
3748
+ init_machine();
4493
3749
  init_queries();
3750
+ init_metrics();
3751
+ init_config();
4494
3752
  init_spawn();
4495
- import_node_child_process5 = require("child_process");
3753
+ import_node_child_process6 = require("child_process");
4496
3754
  init_git();
4497
3755
  init_format();
4498
3756
  }
@@ -4534,7 +3792,7 @@ async function resolveCmd(args) {
4534
3792
  const db = getDb(root);
4535
3793
  const exp = resolveExperimentArg(db, args);
4536
3794
  transition(exp.status, "resolved" /* RESOLVED */);
4537
- await resolve(db, exp, root);
3795
+ await resolve2(db, exp, root);
4538
3796
  }
4539
3797
  async function runStep(step, db, exp, root) {
4540
3798
  switch (step) {
@@ -4556,12 +3814,12 @@ async function runStep(step, db, exp, root) {
4556
3814
  }
4557
3815
  async function runResolve(db, exp, root) {
4558
3816
  transition(exp.status, "resolved" /* RESOLVED */);
4559
- await resolve(db, exp, root);
3817
+ await resolve2(db, exp, root);
4560
3818
  }
4561
3819
  async function doGate(db, exp, root) {
4562
3820
  transition(exp.status, "gated" /* GATED */);
4563
- const synthesis = truncateContext(readFileOrEmpty(path12.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
4564
- const fragility = truncateContext(readFileOrEmpty(path12.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
3821
+ const synthesis = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
3822
+ const fragility = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
4565
3823
  const structuralDeadEnds = exp.sub_type ? listStructuralDeadEndsBySubType(db, exp.sub_type) : listStructuralDeadEnds(db);
4566
3824
  const result = await spawnAgent("gatekeeper", {
4567
3825
  experiment: {
@@ -4604,17 +3862,25 @@ Output your gate_decision as "approve", "reject", or "flag" with reasoning.`
4604
3862
  }
4605
3863
  }
4606
3864
  async function doBuild(db, exp, root) {
3865
+ if (exp.depends_on) {
3866
+ const dep = getExperimentBySlug(db, exp.depends_on);
3867
+ if (!dep || dep.status !== "merged") {
3868
+ throw new Error(
3869
+ `Experiment "${exp.slug}" depends on "${exp.depends_on}" which is ${dep ? dep.status : "not found"}. Dependency must be merged before building.`
3870
+ );
3871
+ }
3872
+ }
4607
3873
  transition(exp.status, "building" /* BUILDING */);
4608
3874
  const deadEnds = exp.sub_type ? listDeadEndsBySubType(db, exp.sub_type) : listAllDeadEnds(db);
4609
3875
  const builderGuidance = getBuilderGuidance(db, exp.id);
4610
- const fragility = truncateContext(readFileOrEmpty(path12.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
4611
- const synthesis = truncateContext(readFileOrEmpty(path12.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
3876
+ const fragility = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
3877
+ const synthesis = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
4612
3878
  const confirmedDoubts = getConfirmedDoubts(db, exp.id);
4613
3879
  const config = loadConfig(root);
4614
3880
  const existingBaseline = getMetricsByExperimentAndPhase(db, exp.id, "before");
4615
3881
  if (config.metrics?.command && existingBaseline.length === 0) {
4616
3882
  try {
4617
- const output = (0, import_node_child_process6.execSync)(config.metrics.command, {
3883
+ const output = (0, import_node_child_process7.execSync)(config.metrics.command, {
4618
3884
  cwd: root,
4619
3885
  encoding: "utf-8",
4620
3886
  timeout: 6e4,
@@ -4642,6 +3908,7 @@ Build the experiment: ${exp.hypothesis}` : `Build the experiment: ${exp.hypothes
4642
3908
  }
4643
3909
  }
4644
3910
  taskPrompt += "\n\nNote: The framework captures metrics automatically. Do NOT claim specific numbers unless quoting framework output.";
3911
+ const supplementaryContext = loadExperimentContext(exp, root);
4645
3912
  const result = await spawnAgent("builder", {
4646
3913
  experiment: {
4647
3914
  id: exp.id,
@@ -4659,6 +3926,7 @@ Build the experiment: ${exp.hypothesis}` : `Build the experiment: ${exp.hypothes
4659
3926
  fragility,
4660
3927
  synthesis,
4661
3928
  confirmedDoubts,
3929
+ supplementaryContext: supplementaryContext || void 0,
4662
3930
  taskPrompt
4663
3931
  }, root);
4664
3932
  ingestStructuredOutput(db, exp.id, result.structured);
@@ -4671,7 +3939,7 @@ Build the experiment: ${exp.hypothesis}` : `Build the experiment: ${exp.hypothes
4671
3939
  } else {
4672
3940
  if (config.metrics?.command) {
4673
3941
  try {
4674
- const output = (0, import_node_child_process6.execSync)(config.metrics.command, {
3942
+ const output = (0, import_node_child_process7.execSync)(config.metrics.command, {
4675
3943
  cwd: root,
4676
3944
  encoding: "utf-8",
4677
3945
  timeout: 6e4,
@@ -4695,7 +3963,7 @@ async function doChallenge(db, exp, root) {
4695
3963
  transition(exp.status, "challenged" /* CHALLENGED */);
4696
3964
  let gitDiff = "";
4697
3965
  try {
4698
- gitDiff = (0, import_node_child_process6.execSync)('git diff main -- . ":!.majlis/"', {
3966
+ gitDiff = (0, import_node_child_process7.execSync)('git diff main -- . ":!.majlis/"', {
4699
3967
  cwd: root,
4700
3968
  encoding: "utf-8",
4701
3969
  stdio: ["pipe", "pipe", "pipe"]
@@ -4703,7 +3971,7 @@ async function doChallenge(db, exp, root) {
4703
3971
  } catch {
4704
3972
  }
4705
3973
  if (gitDiff.length > 8e3) gitDiff = gitDiff.slice(0, 8e3) + "\n[DIFF TRUNCATED]";
4706
- const synthesis = truncateContext(readFileOrEmpty(path12.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
3974
+ const synthesis = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
4707
3975
  let taskPrompt = `Construct adversarial test cases for experiment ${exp.slug}: ${exp.hypothesis}`;
4708
3976
  if (gitDiff) {
4709
3977
  taskPrompt += `
@@ -4736,9 +4004,9 @@ ${gitDiff}
4736
4004
  async function doDoubt(db, exp, root) {
4737
4005
  transition(exp.status, "doubted" /* DOUBTED */);
4738
4006
  const paddedNum = String(exp.id).padStart(3, "0");
4739
- const expDocPath = path12.join(root, "docs", "experiments", `${paddedNum}-${exp.slug}.md`);
4007
+ const expDocPath = path13.join(root, "docs", "experiments", `${paddedNum}-${exp.slug}.md`);
4740
4008
  const experimentDoc = truncateContext(readFileOrEmpty(expDocPath), CONTEXT_LIMITS.experimentDoc);
4741
- const synthesis = truncateContext(readFileOrEmpty(path12.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
4009
+ const synthesis = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
4742
4010
  const deadEnds = exp.sub_type ? listDeadEndsBySubType(db, exp.sub_type) : listAllDeadEnds(db);
4743
4011
  let taskPrompt = `Doubt the work in experiment ${exp.slug}: ${exp.hypothesis}. Produce a doubt document with evidence for each doubt.`;
4744
4012
  if (experimentDoc) {
@@ -4777,8 +4045,8 @@ ${experimentDoc}
4777
4045
  }
4778
4046
  async function doScout(db, exp, root) {
4779
4047
  transition(exp.status, "scouted" /* SCOUTED */);
4780
- const synthesis = truncateContext(readFileOrEmpty(path12.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
4781
- const fragility = truncateContext(readFileOrEmpty(path12.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
4048
+ const synthesis = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
4049
+ const fragility = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
4782
4050
  const deadEnds = exp.sub_type ? listDeadEndsBySubType(db, exp.sub_type) : listAllDeadEnds(db);
4783
4051
  const deadEndsSummary = deadEnds.map(
4784
4052
  (d) => `- [${d.category ?? "structural"}] ${d.approach}: ${d.why_failed}`
@@ -4825,31 +4093,49 @@ ${fragility}`;
4825
4093
  async function doVerify(db, exp, root) {
4826
4094
  transition(exp.status, "verifying" /* VERIFYING */);
4827
4095
  const doubts = getDoubtsByExperiment(db, exp.id);
4828
- const challengeDir = path12.join(root, "docs", "challenges");
4096
+ const challengeDir = path13.join(root, "docs", "challenges");
4829
4097
  let challenges = "";
4830
- if (fs12.existsSync(challengeDir)) {
4831
- const files = fs12.readdirSync(challengeDir).filter((f) => f.includes(exp.slug) && f.endsWith(".md"));
4098
+ if (fs13.existsSync(challengeDir)) {
4099
+ const files = fs13.readdirSync(challengeDir).filter((f) => f.includes(exp.slug) && f.endsWith(".md"));
4832
4100
  for (const f of files) {
4833
- challenges += fs12.readFileSync(path12.join(challengeDir, f), "utf-8") + "\n\n";
4101
+ challenges += fs13.readFileSync(path13.join(challengeDir, f), "utf-8") + "\n\n";
4834
4102
  }
4835
4103
  }
4836
- const beforeMetrics = getMetricsByExperimentAndPhase(db, exp.id, "before");
4837
- const afterMetrics = getMetricsByExperimentAndPhase(db, exp.id, "after");
4104
+ const config = loadConfig(root);
4105
+ const metricComparisons = compareMetrics(db, exp.id, config);
4838
4106
  let metricsSection = "";
4839
- if (beforeMetrics.length > 0 || afterMetrics.length > 0) {
4107
+ if (metricComparisons.length > 0) {
4840
4108
  metricsSection = "\n\n## Framework-Captured Metrics (GROUND TRUTH \u2014 not self-reported by builder)\n";
4841
- if (beforeMetrics.length > 0) {
4842
- metricsSection += "### Before Build\n";
4843
- for (const m of beforeMetrics) {
4844
- metricsSection += `- ${m.fixture} / ${m.metric_name}: ${m.metric_value}
4109
+ metricsSection += "| Fixture | Metric | Before | After | Delta | Regression | Gate |\n";
4110
+ metricsSection += "|---------|--------|--------|-------|-------|------------|------|\n";
4111
+ for (const c of metricComparisons) {
4112
+ metricsSection += `| ${c.fixture} | ${c.metric} | ${c.before} | ${c.after} | ${c.delta > 0 ? "+" : ""}${c.delta} | ${c.regression ? "YES" : "no"} | ${c.gate ? "GATE" : "-"} |
4845
4113
  `;
4846
- }
4847
4114
  }
4848
- if (afterMetrics.length > 0) {
4849
- metricsSection += "### After Build\n";
4850
- for (const m of afterMetrics) {
4851
- metricsSection += `- ${m.fixture} / ${m.metric_name}: ${m.metric_value}
4115
+ const gateViolations = metricComparisons.filter((c) => c.gate && c.regression);
4116
+ if (gateViolations.length > 0) {
4117
+ metricsSection += `
4118
+ **GATE VIOLATION**: ${gateViolations.length} gate fixture(s) regressed. This MUST be addressed \u2014 gate regressions block merge.
4852
4119
  `;
4120
+ }
4121
+ } else {
4122
+ const beforeMetrics = getMetricsByExperimentAndPhase(db, exp.id, "before");
4123
+ const afterMetrics = getMetricsByExperimentAndPhase(db, exp.id, "after");
4124
+ if (beforeMetrics.length > 0 || afterMetrics.length > 0) {
4125
+ metricsSection = "\n\n## Framework-Captured Metrics (GROUND TRUTH \u2014 not self-reported by builder)\n";
4126
+ if (beforeMetrics.length > 0) {
4127
+ metricsSection += "### Before Build\n";
4128
+ for (const m of beforeMetrics) {
4129
+ metricsSection += `- ${m.fixture} / ${m.metric_name}: ${m.metric_value}
4130
+ `;
4131
+ }
4132
+ }
4133
+ if (afterMetrics.length > 0) {
4134
+ metricsSection += "### After Build\n";
4135
+ for (const m of afterMetrics) {
4136
+ metricsSection += `- ${m.fixture} / ${m.metric_name}: ${m.metric_value}
4137
+ `;
4138
+ }
4853
4139
  }
4854
4140
  }
4855
4141
  }
@@ -4863,6 +4149,7 @@ async function doVerify(db, exp, root) {
4863
4149
  doubtReference += "\nWhen resolving doubts, use the DOUBT-{id} number as the doubt_id value in your doubt_resolutions output.";
4864
4150
  }
4865
4151
  updateExperimentStatus(db, exp.id, "verifying");
4152
+ const verifierSupplementaryContext = loadExperimentContext(exp, root);
4866
4153
  const result = await spawnAgent("verifier", {
4867
4154
  experiment: {
4868
4155
  id: exp.id,
@@ -4874,6 +4161,8 @@ async function doVerify(db, exp, root) {
4874
4161
  },
4875
4162
  doubts,
4876
4163
  challenges,
4164
+ metricComparisons: metricComparisons.length > 0 ? metricComparisons : void 0,
4165
+ supplementaryContext: verifierSupplementaryContext || void 0,
4877
4166
  taskPrompt: `Verify experiment ${exp.slug}: ${exp.hypothesis}. Check provenance and content. Test the ${doubts.length} doubt(s) and any adversarial challenges.` + metricsSection + doubtReference
4878
4167
  }, root);
4879
4168
  ingestStructuredOutput(db, exp.id, result.structured);
@@ -4898,22 +4187,22 @@ async function doVerify(db, exp, root) {
4898
4187
  success(`Verification complete for ${exp.slug}. Run \`majlis resolve\` next.`);
4899
4188
  }
4900
4189
  async function doCompress(db, root) {
4901
- const synthesisPath = path12.join(root, "docs", "synthesis", "current.md");
4902
- const sizeBefore = fs12.existsSync(synthesisPath) ? fs12.statSync(synthesisPath).size : 0;
4190
+ const synthesisPath = path13.join(root, "docs", "synthesis", "current.md");
4191
+ const sizeBefore = fs13.existsSync(synthesisPath) ? fs13.statSync(synthesisPath).size : 0;
4903
4192
  const sessionCount = getSessionsSinceCompression(db);
4904
4193
  const dbExport = exportForCompressor(db);
4905
4194
  const result = await spawnAgent("compressor", {
4906
4195
  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."
4907
4196
  }, root);
4908
- const sizeAfter = fs12.existsSync(synthesisPath) ? fs12.statSync(synthesisPath).size : 0;
4197
+ const sizeAfter = fs13.existsSync(synthesisPath) ? fs13.statSync(synthesisPath).size : 0;
4909
4198
  recordCompression(db, sessionCount, sizeBefore, sizeAfter);
4910
4199
  autoCommit(root, "compress: update synthesis");
4911
4200
  success(`Compression complete. Synthesis: ${sizeBefore}B \u2192 ${sizeAfter}B`);
4912
4201
  }
4913
4202
  function gitCommitBuild(exp, cwd) {
4914
4203
  try {
4915
- (0, import_node_child_process6.execSync)('git add -A -- ":!.majlis/"', { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
4916
- const diff = (0, import_node_child_process6.execSync)("git diff --cached --stat", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
4204
+ (0, import_node_child_process7.execSync)('git add -A -- ":!.majlis/"', { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
4205
+ const diff = (0, import_node_child_process7.execSync)("git diff --cached --stat", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
4917
4206
  if (!diff) {
4918
4207
  info("No code changes to commit.");
4919
4208
  return;
@@ -4921,12 +4210,37 @@ function gitCommitBuild(exp, cwd) {
4921
4210
  const msg = `EXP-${String(exp.id).padStart(3, "0")}: ${exp.slug}
4922
4211
 
4923
4212
  ${exp.hypothesis ?? ""}`;
4924
- (0, import_node_child_process6.execSync)(`git commit -m ${JSON.stringify(msg)}`, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
4213
+ (0, import_node_child_process7.execFileSync)("git", ["commit", "-m", msg], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
4925
4214
  info(`Committed builder changes on ${exp.branch}.`);
4926
4215
  } catch {
4927
4216
  warn("Could not auto-commit builder changes \u2014 commit manually before resolving.");
4928
4217
  }
4929
4218
  }
4219
+ function loadExperimentContext(exp, root) {
4220
+ if (!exp.context_files) return "";
4221
+ let files;
4222
+ try {
4223
+ files = JSON.parse(exp.context_files);
4224
+ } catch {
4225
+ return "";
4226
+ }
4227
+ if (!Array.isArray(files) || files.length === 0) return "";
4228
+ const sections = ["## Experiment-Scoped Reference Material"];
4229
+ for (const relPath of files) {
4230
+ const absPath = path13.join(root, relPath);
4231
+ try {
4232
+ const content = fs13.readFileSync(absPath, "utf-8");
4233
+ sections.push(`### ${relPath}
4234
+ \`\`\`
4235
+ ${content.slice(0, 8e3)}
4236
+ \`\`\``);
4237
+ } catch {
4238
+ sections.push(`### ${relPath}
4239
+ *(file not found)*`);
4240
+ }
4241
+ }
4242
+ return sections.join("\n\n");
4243
+ }
4930
4244
  function resolveExperimentArg(db, args) {
4931
4245
  const slugArg = args.filter((a) => !a.startsWith("--"))[0];
4932
4246
  let exp;
@@ -4941,69 +4255,71 @@ function resolveExperimentArg(db, args) {
4941
4255
  }
4942
4256
  function ingestStructuredOutput(db, experimentId, structured) {
4943
4257
  if (!structured) return;
4944
- if (structured.decisions) {
4945
- for (const d of structured.decisions) {
4946
- insertDecision(db, experimentId, d.description, d.evidence_level, d.justification);
4258
+ db.transaction(() => {
4259
+ if (structured.decisions) {
4260
+ for (const d of structured.decisions) {
4261
+ insertDecision(db, experimentId, d.description, d.evidence_level, d.justification);
4262
+ }
4263
+ info(`Ingested ${structured.decisions.length} decision(s)`);
4947
4264
  }
4948
- info(`Ingested ${structured.decisions.length} decision(s)`);
4949
- }
4950
- if (structured.grades) {
4951
- for (const g of structured.grades) {
4952
- insertVerification(
4953
- db,
4954
- experimentId,
4955
- g.component,
4956
- g.grade,
4957
- g.provenance_intact ?? null,
4958
- g.content_correct ?? null,
4959
- g.notes ?? null
4960
- );
4265
+ if (structured.grades) {
4266
+ for (const g of structured.grades) {
4267
+ insertVerification(
4268
+ db,
4269
+ experimentId,
4270
+ g.component,
4271
+ g.grade,
4272
+ g.provenance_intact ?? null,
4273
+ g.content_correct ?? null,
4274
+ g.notes ?? null
4275
+ );
4276
+ }
4277
+ info(`Ingested ${structured.grades.length} verification grade(s)`);
4961
4278
  }
4962
- info(`Ingested ${structured.grades.length} verification grade(s)`);
4963
- }
4964
- if (structured.doubts) {
4965
- for (const d of structured.doubts) {
4966
- insertDoubt(
4279
+ if (structured.doubts) {
4280
+ for (const d of structured.doubts) {
4281
+ insertDoubt(
4282
+ db,
4283
+ experimentId,
4284
+ d.claim_doubted,
4285
+ d.evidence_level_of_claim,
4286
+ d.evidence_for_doubt,
4287
+ d.severity
4288
+ );
4289
+ }
4290
+ info(`Ingested ${structured.doubts.length} doubt(s)`);
4291
+ }
4292
+ if (structured.challenges) {
4293
+ for (const c of structured.challenges) {
4294
+ insertChallenge(db, experimentId, c.description, c.reasoning);
4295
+ }
4296
+ info(`Ingested ${structured.challenges.length} challenge(s)`);
4297
+ }
4298
+ if (structured.reframe) {
4299
+ insertReframe(
4967
4300
  db,
4968
4301
  experimentId,
4969
- d.claim_doubted,
4970
- d.evidence_level_of_claim,
4971
- d.evidence_for_doubt,
4972
- d.severity
4302
+ structured.reframe.decomposition,
4303
+ JSON.stringify(structured.reframe.divergences),
4304
+ structured.reframe.recommendation
4973
4305
  );
4306
+ info(`Ingested reframe`);
4974
4307
  }
4975
- info(`Ingested ${structured.doubts.length} doubt(s)`);
4976
- }
4977
- if (structured.challenges) {
4978
- for (const c of structured.challenges) {
4979
- insertChallenge(db, experimentId, c.description, c.reasoning);
4980
- }
4981
- info(`Ingested ${structured.challenges.length} challenge(s)`);
4982
- }
4983
- if (structured.reframe) {
4984
- insertReframe(
4985
- db,
4986
- experimentId,
4987
- structured.reframe.decomposition,
4988
- JSON.stringify(structured.reframe.divergences),
4989
- structured.reframe.recommendation
4990
- );
4991
- info(`Ingested reframe`);
4992
- }
4993
- if (structured.findings) {
4994
- for (const f of structured.findings) {
4995
- insertFinding(db, experimentId, f.approach, f.source, f.relevance, f.contradicts_current);
4308
+ if (structured.findings) {
4309
+ for (const f of structured.findings) {
4310
+ insertFinding(db, experimentId, f.approach, f.source, f.relevance, f.contradicts_current);
4311
+ }
4312
+ info(`Ingested ${structured.findings.length} finding(s)`);
4996
4313
  }
4997
- info(`Ingested ${structured.findings.length} finding(s)`);
4998
- }
4314
+ })();
4999
4315
  }
5000
- var fs12, path12, import_node_child_process6;
4316
+ var fs13, path13, import_node_child_process7;
5001
4317
  var init_cycle = __esm({
5002
4318
  "src/commands/cycle.ts"() {
5003
4319
  "use strict";
5004
- fs12 = __toESM(require("fs"));
5005
- path12 = __toESM(require("path"));
5006
- import_node_child_process6 = require("child_process");
4320
+ fs13 = __toESM(require("fs"));
4321
+ path13 = __toESM(require("path"));
4322
+ import_node_child_process7 = require("child_process");
5007
4323
  init_connection();
5008
4324
  init_queries();
5009
4325
  init_machine();
@@ -5030,10 +4346,10 @@ async function classify(args) {
5030
4346
  if (!domain) {
5031
4347
  throw new Error('Usage: majlis classify "domain description"');
5032
4348
  }
5033
- const synthesisPath = path13.join(root, "docs", "synthesis", "current.md");
5034
- const synthesis = fs13.existsSync(synthesisPath) ? fs13.readFileSync(synthesisPath, "utf-8") : "";
5035
- const deadEndsPath = path13.join(root, "docs", "synthesis", "dead-ends.md");
5036
- const deadEnds = fs13.existsSync(deadEndsPath) ? fs13.readFileSync(deadEndsPath, "utf-8") : "";
4349
+ const synthesisPath = path14.join(root, "docs", "synthesis", "current.md");
4350
+ const synthesis = fs14.existsSync(synthesisPath) ? fs14.readFileSync(synthesisPath, "utf-8") : "";
4351
+ const deadEndsPath = path14.join(root, "docs", "synthesis", "dead-ends.md");
4352
+ const deadEnds = fs14.existsSync(deadEndsPath) ? fs14.readFileSync(deadEndsPath, "utf-8") : "";
5037
4353
  info(`Classifying problem domain: ${domain}`);
5038
4354
  const result = await spawnAgent("builder", {
5039
4355
  synthesis,
@@ -5052,22 +4368,22 @@ Write the classification to docs/classification/ following the template.`
5052
4368
  async function reframe(args) {
5053
4369
  const root = findProjectRoot();
5054
4370
  if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
5055
- const classificationDir = path13.join(root, "docs", "classification");
4371
+ const classificationDir = path14.join(root, "docs", "classification");
5056
4372
  let classificationContent = "";
5057
- if (fs13.existsSync(classificationDir)) {
5058
- const files = fs13.readdirSync(classificationDir).filter((f) => f.endsWith(".md") && !f.startsWith("_"));
4373
+ if (fs14.existsSync(classificationDir)) {
4374
+ const files = fs14.readdirSync(classificationDir).filter((f) => f.endsWith(".md") && !f.startsWith("_"));
5059
4375
  for (const f of files) {
5060
- classificationContent += fs13.readFileSync(path13.join(classificationDir, f), "utf-8") + "\n\n";
4376
+ classificationContent += fs14.readFileSync(path14.join(classificationDir, f), "utf-8") + "\n\n";
5061
4377
  }
5062
4378
  }
5063
- const synthesisPath = path13.join(root, "docs", "synthesis", "current.md");
5064
- const synthesis = fs13.existsSync(synthesisPath) ? fs13.readFileSync(synthesisPath, "utf-8") : "";
5065
- const deadEndsPath = path13.join(root, "docs", "synthesis", "dead-ends.md");
5066
- const deadEnds = fs13.existsSync(deadEndsPath) ? fs13.readFileSync(deadEndsPath, "utf-8") : "";
5067
- const configPath = path13.join(root, ".majlis", "config.json");
4379
+ const synthesisPath = path14.join(root, "docs", "synthesis", "current.md");
4380
+ const synthesis = fs14.existsSync(synthesisPath) ? fs14.readFileSync(synthesisPath, "utf-8") : "";
4381
+ const deadEndsPath = path14.join(root, "docs", "synthesis", "dead-ends.md");
4382
+ const deadEnds = fs14.existsSync(deadEndsPath) ? fs14.readFileSync(deadEndsPath, "utf-8") : "";
4383
+ const configPath = path14.join(root, ".majlis", "config.json");
5068
4384
  let problemStatement = "";
5069
- if (fs13.existsSync(configPath)) {
5070
- const config = JSON.parse(fs13.readFileSync(configPath, "utf-8"));
4385
+ if (fs14.existsSync(configPath)) {
4386
+ const config = JSON.parse(fs14.readFileSync(configPath, "utf-8"));
5071
4387
  problemStatement = `${config.project?.description ?? ""}
5072
4388
  Objective: ${config.project?.objective ?? ""}`;
5073
4389
  }
@@ -5092,12 +4408,12 @@ Write to docs/reframes/.`
5092
4408
  autoCommit(root, `reframe: ${target.slice(0, 60)}`);
5093
4409
  success("Reframe complete. Check docs/reframes/ for the output.");
5094
4410
  }
5095
- var fs13, path13;
4411
+ var fs14, path14;
5096
4412
  var init_classify = __esm({
5097
4413
  "src/commands/classify.ts"() {
5098
4414
  "use strict";
5099
- fs13 = __toESM(require("fs"));
5100
- path13 = __toESM(require("path"));
4415
+ fs14 = __toESM(require("fs"));
4416
+ path14 = __toESM(require("path"));
5101
4417
  init_connection();
5102
4418
  init_spawn();
5103
4419
  init_git();
@@ -5119,15 +4435,15 @@ async function audit(args) {
5119
4435
  const experiments = listAllExperiments(db);
5120
4436
  const deadEnds = listAllDeadEnds(db);
5121
4437
  const circuitBreakers = getAllCircuitBreakerStates(db, config.cycle.circuit_breaker_threshold);
5122
- const classificationDir = path14.join(root, "docs", "classification");
4438
+ const classificationDir = path15.join(root, "docs", "classification");
5123
4439
  let classification = "";
5124
- if (fs14.existsSync(classificationDir)) {
5125
- const files = fs14.readdirSync(classificationDir).filter((f) => f.endsWith(".md") && !f.startsWith("_"));
4440
+ if (fs15.existsSync(classificationDir)) {
4441
+ const files = fs15.readdirSync(classificationDir).filter((f) => f.endsWith(".md") && !f.startsWith("_"));
5126
4442
  for (const f of files) {
5127
- classification += fs14.readFileSync(path14.join(classificationDir, f), "utf-8") + "\n\n";
4443
+ classification += fs15.readFileSync(path15.join(classificationDir, f), "utf-8") + "\n\n";
5128
4444
  }
5129
4445
  }
5130
- const synthesis = readFileOrEmpty(path14.join(root, "docs", "synthesis", "current.md"));
4446
+ const synthesis = readFileOrEmpty(path15.join(root, "docs", "synthesis", "current.md"));
5131
4447
  header("Maqasid Check \u2014 Purpose Audit");
5132
4448
  const trippedBreakers = circuitBreakers.filter((cb) => cb.tripped);
5133
4449
  if (trippedBreakers.length > 0) {
@@ -5171,12 +4487,12 @@ Output: either "classification confirmed \u2014 continue" or "re-classify from X
5171
4487
  }, root);
5172
4488
  success("Purpose audit complete. Review the output above.");
5173
4489
  }
5174
- var fs14, path14;
4490
+ var fs15, path15;
5175
4491
  var init_audit = __esm({
5176
4492
  "src/commands/audit.ts"() {
5177
4493
  "use strict";
5178
- fs14 = __toESM(require("fs"));
5179
- path14 = __toESM(require("path"));
4494
+ fs15 = __toESM(require("fs"));
4495
+ path15 = __toESM(require("path"));
5180
4496
  init_connection();
5181
4497
  init_queries();
5182
4498
  init_spawn();
@@ -5235,7 +4551,7 @@ async function runNextStep(db, exp, config, root, isJson) {
5235
4551
  exp.sub_type,
5236
4552
  "procedural"
5237
4553
  );
5238
- updateExperimentStatus(db, exp.id, "dead_end");
4554
+ adminTransitionAndPersist(db, exp.id, exp.status, "dead_end" /* DEAD_END */, "circuit_breaker");
5239
4555
  warn("Experiment dead-ended. Triggering Maqasid Check (purpose audit).");
5240
4556
  await audit([config.project?.objective ?? ""]);
5241
4557
  return;
@@ -5285,7 +4601,7 @@ async function runAutoLoop(db, exp, config, root, isJson) {
5285
4601
  exp.sub_type,
5286
4602
  "procedural"
5287
4603
  );
5288
- updateExperimentStatus(db, exp.id, "dead_end");
4604
+ adminTransitionAndPersist(db, exp.id, exp.status, "dead_end" /* DEAD_END */, "circuit_breaker");
5289
4605
  await audit([config.project?.objective ?? ""]);
5290
4606
  break;
5291
4607
  }
@@ -5324,18 +4640,24 @@ async function executeStep(step, exp, root) {
5324
4640
  break;
5325
4641
  case "compressed" /* COMPRESSED */:
5326
4642
  await cycle("compress", []);
5327
- updateExperimentStatus(getDb(root), exp.id, "compressed");
4643
+ transitionAndPersist(getDb(root), exp.id, exp.status, "compressed" /* COMPRESSED */);
5328
4644
  info(`Experiment ${exp.slug} compressed.`);
5329
4645
  break;
5330
4646
  case "gated" /* GATED */:
5331
4647
  await cycle("gate", expArgs);
5332
4648
  break;
5333
- case "reframed" /* REFRAMED */:
5334
- updateExperimentStatus(getDb(root), exp.id, "reframed");
4649
+ case "reframed" /* REFRAMED */: {
4650
+ const currentStatus = exp.status;
4651
+ if (currentStatus === "classified" /* CLASSIFIED */) {
4652
+ adminTransitionAndPersist(getDb(root), exp.id, currentStatus, "reframed" /* REFRAMED */, "bootstrap");
4653
+ } else {
4654
+ transitionAndPersist(getDb(root), exp.id, currentStatus, "reframed" /* REFRAMED */);
4655
+ }
5335
4656
  info(`Reframe acknowledged for ${exp.slug}. Proceeding to gate.`);
5336
4657
  break;
4658
+ }
5337
4659
  case "merged" /* MERGED */:
5338
- updateExperimentStatus(getDb(root), exp.id, "merged");
4660
+ transitionAndPersist(getDb(root), exp.id, exp.status, "merged" /* MERGED */);
5339
4661
  success(`Experiment ${exp.slug} merged.`);
5340
4662
  break;
5341
4663
  case "dead_end" /* DEAD_END */:
@@ -5441,7 +4763,7 @@ async function run(args) {
5441
4763
  exp.sub_type,
5442
4764
  "procedural"
5443
4765
  );
5444
- updateExperimentStatus(db, exp.id, "dead_end");
4766
+ adminTransitionAndPersist(db, exp.id, exp.status, "dead_end" /* DEAD_END */, "error_recovery");
5445
4767
  } catch (innerErr) {
5446
4768
  const innerMsg = innerErr instanceof Error ? innerErr.message : String(innerErr);
5447
4769
  warn(`Could not record dead-end: ${innerMsg}`);
@@ -5461,16 +4783,16 @@ async function run(args) {
5461
4783
  info("Run `majlis status` to see final state.");
5462
4784
  }
5463
4785
  async function deriveNextHypothesis(goal, root, db) {
5464
- const synthesis = truncateContext(readFileOrEmpty(path15.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
5465
- const fragility = truncateContext(readFileOrEmpty(path15.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
5466
- const deadEndsDoc = truncateContext(readFileOrEmpty(path15.join(root, "docs", "synthesis", "dead-ends.md")), CONTEXT_LIMITS.deadEnds);
4786
+ const synthesis = truncateContext(readFileOrEmpty(path16.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
4787
+ const fragility = truncateContext(readFileOrEmpty(path16.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
4788
+ const deadEndsDoc = truncateContext(readFileOrEmpty(path16.join(root, "docs", "synthesis", "dead-ends.md")), CONTEXT_LIMITS.deadEnds);
5467
4789
  const diagnosis = truncateContext(readLatestDiagnosis(root), CONTEXT_LIMITS.synthesis);
5468
4790
  const deadEnds = listAllDeadEnds(db);
5469
4791
  const config = loadConfig(root);
5470
4792
  let metricsOutput = "";
5471
4793
  if (config.metrics?.command) {
5472
4794
  try {
5473
- metricsOutput = (0, import_node_child_process7.execSync)(config.metrics.command, {
4795
+ metricsOutput = (0, import_node_child_process8.execSync)(config.metrics.command, {
5474
4796
  cwd: root,
5475
4797
  encoding: "utf-8",
5476
4798
  timeout: 6e4,
@@ -5569,7 +4891,7 @@ async function createNewExperiment(db, root, hypothesis) {
5569
4891
  const paddedNum = String(num).padStart(3, "0");
5570
4892
  const branch = `exp/${paddedNum}-${finalSlug}`;
5571
4893
  try {
5572
- (0, import_node_child_process7.execSync)(`git checkout -b ${branch}`, {
4894
+ (0, import_node_child_process8.execFileSync)("git", ["checkout", "-b", branch], {
5573
4895
  cwd: root,
5574
4896
  encoding: "utf-8",
5575
4897
  stdio: ["pipe", "pipe", "pipe"]
@@ -5579,29 +4901,30 @@ async function createNewExperiment(db, root, hypothesis) {
5579
4901
  warn(`Could not create branch ${branch} \u2014 continuing without git branch.`);
5580
4902
  }
5581
4903
  const exp = createExperiment(db, finalSlug, branch, hypothesis, null, null);
5582
- updateExperimentStatus(db, exp.id, "reframed");
4904
+ adminTransitionAndPersist(db, exp.id, exp.status, "reframed" /* REFRAMED */, "bootstrap");
5583
4905
  exp.status = "reframed";
5584
- const docsDir = path15.join(root, "docs", "experiments");
5585
- const templatePath = path15.join(docsDir, "_TEMPLATE.md");
5586
- if (fs15.existsSync(templatePath)) {
5587
- const template = fs15.readFileSync(templatePath, "utf-8");
4906
+ const docsDir = path16.join(root, "docs", "experiments");
4907
+ const templatePath = path16.join(docsDir, "_TEMPLATE.md");
4908
+ if (fs16.existsSync(templatePath)) {
4909
+ const template = fs16.readFileSync(templatePath, "utf-8");
5588
4910
  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]);
5589
- const logPath = path15.join(docsDir, `${paddedNum}-${finalSlug}.md`);
5590
- fs15.writeFileSync(logPath, logContent);
4911
+ const logPath = path16.join(docsDir, `${paddedNum}-${finalSlug}.md`);
4912
+ fs16.writeFileSync(logPath, logContent);
5591
4913
  info(`Created experiment log: docs/experiments/${paddedNum}-${finalSlug}.md`);
5592
4914
  }
5593
4915
  return exp;
5594
4916
  }
5595
- var fs15, path15, import_node_child_process7;
4917
+ var fs16, path16, import_node_child_process8;
5596
4918
  var init_run = __esm({
5597
4919
  "src/commands/run.ts"() {
5598
4920
  "use strict";
5599
- fs15 = __toESM(require("fs"));
5600
- path15 = __toESM(require("path"));
5601
- import_node_child_process7 = require("child_process");
4921
+ fs16 = __toESM(require("fs"));
4922
+ path16 = __toESM(require("path"));
4923
+ import_node_child_process8 = require("child_process");
5602
4924
  init_connection();
5603
4925
  init_queries();
5604
4926
  init_machine();
4927
+ init_types2();
5605
4928
  init_next();
5606
4929
  init_cycle();
5607
4930
  init_spawn();
@@ -5614,11 +4937,11 @@ var init_run = __esm({
5614
4937
 
5615
4938
  // src/swarm/worktree.ts
5616
4939
  function createWorktree(mainRoot, slug, paddedNum) {
5617
- const projectName = path16.basename(mainRoot);
4940
+ const projectName = path17.basename(mainRoot);
5618
4941
  const worktreeName = `${projectName}-swarm-${paddedNum}-${slug}`;
5619
- const worktreePath = path16.join(path16.dirname(mainRoot), worktreeName);
4942
+ const worktreePath = path17.join(path17.dirname(mainRoot), worktreeName);
5620
4943
  const branch = `swarm/${paddedNum}-${slug}`;
5621
- (0, import_node_child_process8.execSync)(`git worktree add ${JSON.stringify(worktreePath)} -b ${branch}`, {
4944
+ (0, import_node_child_process9.execFileSync)("git", ["worktree", "add", worktreePath, "-b", branch], {
5622
4945
  cwd: mainRoot,
5623
4946
  encoding: "utf-8",
5624
4947
  stdio: ["pipe", "pipe", "pipe"]
@@ -5633,43 +4956,43 @@ function createWorktree(mainRoot, slug, paddedNum) {
5633
4956
  };
5634
4957
  }
5635
4958
  function initializeWorktree(mainRoot, worktreePath) {
5636
- const majlisDir = path16.join(worktreePath, ".majlis");
5637
- fs16.mkdirSync(majlisDir, { recursive: true });
5638
- const configSrc = path16.join(mainRoot, ".majlis", "config.json");
5639
- if (fs16.existsSync(configSrc)) {
5640
- fs16.copyFileSync(configSrc, path16.join(majlisDir, "config.json"));
5641
- }
5642
- const agentsSrc = path16.join(mainRoot, ".majlis", "agents");
5643
- if (fs16.existsSync(agentsSrc)) {
5644
- const agentsDst = path16.join(majlisDir, "agents");
5645
- fs16.mkdirSync(agentsDst, { recursive: true });
5646
- for (const file of fs16.readdirSync(agentsSrc)) {
5647
- fs16.copyFileSync(path16.join(agentsSrc, file), path16.join(agentsDst, file));
5648
- }
5649
- }
5650
- const synthSrc = path16.join(mainRoot, "docs", "synthesis");
5651
- if (fs16.existsSync(synthSrc)) {
5652
- const synthDst = path16.join(worktreePath, "docs", "synthesis");
5653
- fs16.mkdirSync(synthDst, { recursive: true });
5654
- for (const file of fs16.readdirSync(synthSrc)) {
5655
- const srcFile = path16.join(synthSrc, file);
5656
- if (fs16.statSync(srcFile).isFile()) {
5657
- fs16.copyFileSync(srcFile, path16.join(synthDst, file));
4959
+ const majlisDir = path17.join(worktreePath, ".majlis");
4960
+ fs17.mkdirSync(majlisDir, { recursive: true });
4961
+ const configSrc = path17.join(mainRoot, ".majlis", "config.json");
4962
+ if (fs17.existsSync(configSrc)) {
4963
+ fs17.copyFileSync(configSrc, path17.join(majlisDir, "config.json"));
4964
+ }
4965
+ const agentsSrc = path17.join(mainRoot, ".majlis", "agents");
4966
+ if (fs17.existsSync(agentsSrc)) {
4967
+ const agentsDst = path17.join(majlisDir, "agents");
4968
+ fs17.mkdirSync(agentsDst, { recursive: true });
4969
+ for (const file of fs17.readdirSync(agentsSrc)) {
4970
+ fs17.copyFileSync(path17.join(agentsSrc, file), path17.join(agentsDst, file));
4971
+ }
4972
+ }
4973
+ const synthSrc = path17.join(mainRoot, "docs", "synthesis");
4974
+ if (fs17.existsSync(synthSrc)) {
4975
+ const synthDst = path17.join(worktreePath, "docs", "synthesis");
4976
+ fs17.mkdirSync(synthDst, { recursive: true });
4977
+ for (const file of fs17.readdirSync(synthSrc)) {
4978
+ const srcFile = path17.join(synthSrc, file);
4979
+ if (fs17.statSync(srcFile).isFile()) {
4980
+ fs17.copyFileSync(srcFile, path17.join(synthDst, file));
5658
4981
  }
5659
4982
  }
5660
4983
  }
5661
- const templateSrc = path16.join(mainRoot, "docs", "experiments", "_TEMPLATE.md");
5662
- if (fs16.existsSync(templateSrc)) {
5663
- const expDir = path16.join(worktreePath, "docs", "experiments");
5664
- fs16.mkdirSync(expDir, { recursive: true });
5665
- fs16.copyFileSync(templateSrc, path16.join(expDir, "_TEMPLATE.md"));
4984
+ const templateSrc = path17.join(mainRoot, "docs", "experiments", "_TEMPLATE.md");
4985
+ if (fs17.existsSync(templateSrc)) {
4986
+ const expDir = path17.join(worktreePath, "docs", "experiments");
4987
+ fs17.mkdirSync(expDir, { recursive: true });
4988
+ fs17.copyFileSync(templateSrc, path17.join(expDir, "_TEMPLATE.md"));
5666
4989
  }
5667
4990
  const db = openDbAt(worktreePath);
5668
4991
  db.close();
5669
4992
  }
5670
4993
  function cleanupWorktree(mainRoot, wt) {
5671
4994
  try {
5672
- (0, import_node_child_process8.execSync)(`git worktree remove ${JSON.stringify(wt.path)} --force`, {
4995
+ (0, import_node_child_process9.execFileSync)("git", ["worktree", "remove", wt.path, "--force"], {
5673
4996
  cwd: mainRoot,
5674
4997
  encoding: "utf-8",
5675
4998
  stdio: ["pipe", "pipe", "pipe"]
@@ -5678,7 +5001,7 @@ function cleanupWorktree(mainRoot, wt) {
5678
5001
  warn(`Could not remove worktree ${wt.path} \u2014 remove manually.`);
5679
5002
  }
5680
5003
  try {
5681
- (0, import_node_child_process8.execSync)(`git branch -D ${wt.branch}`, {
5004
+ (0, import_node_child_process9.execFileSync)("git", ["branch", "-D", wt.branch], {
5682
5005
  cwd: mainRoot,
5683
5006
  encoding: "utf-8",
5684
5007
  stdio: ["pipe", "pipe", "pipe"]
@@ -5686,7 +5009,7 @@ function cleanupWorktree(mainRoot, wt) {
5686
5009
  } catch {
5687
5010
  }
5688
5011
  try {
5689
- (0, import_node_child_process8.execSync)("git worktree prune", {
5012
+ (0, import_node_child_process9.execSync)("git worktree prune", {
5690
5013
  cwd: mainRoot,
5691
5014
  encoding: "utf-8",
5692
5015
  stdio: ["pipe", "pipe", "pipe"]
@@ -5694,13 +5017,13 @@ function cleanupWorktree(mainRoot, wt) {
5694
5017
  } catch {
5695
5018
  }
5696
5019
  }
5697
- var fs16, path16, import_node_child_process8;
5020
+ var fs17, path17, import_node_child_process9;
5698
5021
  var init_worktree = __esm({
5699
5022
  "src/swarm/worktree.ts"() {
5700
5023
  "use strict";
5701
- fs16 = __toESM(require("fs"));
5702
- path16 = __toESM(require("path"));
5703
- import_node_child_process8 = require("child_process");
5024
+ fs17 = __toESM(require("fs"));
5025
+ path17 = __toESM(require("path"));
5026
+ import_node_child_process9 = require("child_process");
5704
5027
  init_connection();
5705
5028
  init_format();
5706
5029
  }
@@ -5716,14 +5039,14 @@ async function runExperimentInWorktree(wt) {
5716
5039
  try {
5717
5040
  db = openDbAt(wt.path);
5718
5041
  exp = createExperiment(db, wt.slug, wt.branch, wt.hypothesis, null, null);
5719
- updateExperimentStatus(db, exp.id, "reframed");
5042
+ adminTransitionAndPersist(db, exp.id, exp.status, "reframed" /* REFRAMED */, "bootstrap");
5720
5043
  exp.status = "reframed";
5721
- const templatePath = path17.join(wt.path, "docs", "experiments", "_TEMPLATE.md");
5722
- if (fs17.existsSync(templatePath)) {
5723
- const template = fs17.readFileSync(templatePath, "utf-8");
5044
+ const templatePath = path18.join(wt.path, "docs", "experiments", "_TEMPLATE.md");
5045
+ if (fs18.existsSync(templatePath)) {
5046
+ const template = fs18.readFileSync(templatePath, "utf-8");
5724
5047
  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]);
5725
- const logPath = path17.join(wt.path, "docs", "experiments", `${wt.paddedNum}-${wt.slug}.md`);
5726
- fs17.writeFileSync(logPath, logContent);
5048
+ const logPath = path18.join(wt.path, "docs", "experiments", `${wt.paddedNum}-${wt.slug}.md`);
5049
+ fs18.writeFileSync(logPath, logContent);
5727
5050
  }
5728
5051
  info(`${label} Starting: ${wt.hypothesis}`);
5729
5052
  while (stepCount < MAX_STEPS) {
@@ -5754,16 +5077,16 @@ async function runExperimentInWorktree(wt) {
5754
5077
  }
5755
5078
  if (nextStep === "compressed" /* COMPRESSED */) {
5756
5079
  await runStep("compress", db, exp, wt.path);
5757
- updateExperimentStatus(db, exp.id, "compressed");
5080
+ transitionAndPersist(db, exp.id, exp.status, "compressed" /* COMPRESSED */);
5758
5081
  continue;
5759
5082
  }
5760
5083
  if (nextStep === "merged" /* MERGED */) {
5761
- updateExperimentStatus(db, exp.id, "merged");
5084
+ transitionAndPersist(db, exp.id, exp.status, "merged" /* MERGED */);
5762
5085
  success(`${label} Merged.`);
5763
5086
  break;
5764
5087
  }
5765
5088
  if (nextStep === "reframed" /* REFRAMED */) {
5766
- updateExperimentStatus(db, exp.id, "reframed");
5089
+ adminTransitionAndPersist(db, exp.id, exp.status, "reframed" /* REFRAMED */, "bootstrap");
5767
5090
  continue;
5768
5091
  }
5769
5092
  const stepName = statusToStepName(nextStep);
@@ -5786,7 +5109,7 @@ async function runExperimentInWorktree(wt) {
5786
5109
  exp.sub_type,
5787
5110
  "procedural"
5788
5111
  );
5789
- updateExperimentStatus(db, exp.id, "dead_end");
5112
+ adminTransitionAndPersist(db, exp.id, exp.status, "dead_end" /* DEAD_END */, "error_recovery");
5790
5113
  } catch {
5791
5114
  }
5792
5115
  break;
@@ -5846,12 +5169,12 @@ function statusToStepName(status2) {
5846
5169
  return null;
5847
5170
  }
5848
5171
  }
5849
- var fs17, path17, MAX_STEPS;
5172
+ var fs18, path18, MAX_STEPS;
5850
5173
  var init_runner = __esm({
5851
5174
  "src/swarm/runner.ts"() {
5852
5175
  "use strict";
5853
- fs17 = __toESM(require("fs"));
5854
- path17 = __toESM(require("path"));
5176
+ fs18 = __toESM(require("fs"));
5177
+ path18 = __toESM(require("path"));
5855
5178
  init_connection();
5856
5179
  init_queries();
5857
5180
  init_machine();
@@ -6005,7 +5328,7 @@ async function swarm(args) {
6005
5328
  MAX_PARALLEL
6006
5329
  );
6007
5330
  try {
6008
- const status2 = (0, import_node_child_process9.execSync)("git status --porcelain", {
5331
+ const status2 = (0, import_node_child_process10.execSync)("git status --porcelain", {
6009
5332
  cwd: root,
6010
5333
  encoding: "utf-8",
6011
5334
  stdio: ["pipe", "pipe", "pipe"]
@@ -6032,6 +5355,24 @@ async function swarm(args) {
6032
5355
  for (let i = 0; i < hypotheses.length; i++) {
6033
5356
  info(` ${i + 1}. ${hypotheses[i]}`);
6034
5357
  }
5358
+ try {
5359
+ const worktreeList = (0, import_node_child_process10.execFileSync)("git", ["worktree", "list", "--porcelain"], {
5360
+ cwd: root,
5361
+ encoding: "utf-8"
5362
+ });
5363
+ const orphaned = worktreeList.split("\n").filter((line) => line.startsWith("worktree ")).map((line) => line.replace("worktree ", "")).filter((p) => p.includes("-swarm-"));
5364
+ for (const orphanPath of orphaned) {
5365
+ try {
5366
+ (0, import_node_child_process10.execFileSync)("git", ["worktree", "remove", orphanPath, "--force"], { cwd: root, encoding: "utf-8" });
5367
+ info(`Cleaned up orphaned worktree: ${path19.basename(orphanPath)}`);
5368
+ } catch {
5369
+ }
5370
+ }
5371
+ if (orphaned.length > 0) {
5372
+ (0, import_node_child_process10.execFileSync)("git", ["worktree", "prune"], { cwd: root, encoding: "utf-8" });
5373
+ }
5374
+ } catch {
5375
+ }
6035
5376
  const worktrees = [];
6036
5377
  for (let i = 0; i < hypotheses.length; i++) {
6037
5378
  const paddedNum = String(i + 1).padStart(3, "0");
@@ -6055,69 +5396,75 @@ async function swarm(args) {
6055
5396
  }
6056
5397
  info(`Running ${worktrees.length} experiments in parallel...`);
6057
5398
  info("");
6058
- const settled = await Promise.allSettled(
6059
- worktrees.map((wt) => runExperimentInWorktree(wt))
6060
- );
6061
- const results = settled.map((s, i) => {
6062
- if (s.status === "fulfilled") return s.value;
6063
- return {
6064
- worktree: worktrees[i],
6065
- experiment: null,
6066
- finalStatus: "error",
6067
- overallGrade: null,
6068
- costUsd: 0,
6069
- stepCount: 0,
6070
- error: s.reason instanceof Error ? s.reason.message : String(s.reason)
6071
- };
6072
- });
6073
- for (const r of results) {
6074
- updateSwarmMember(
6075
- db,
6076
- swarmRun.id,
6077
- r.worktree.slug,
6078
- r.finalStatus,
6079
- r.overallGrade,
6080
- r.costUsd,
6081
- r.error ?? null
5399
+ let results;
5400
+ let summary;
5401
+ try {
5402
+ const settled = await Promise.allSettled(
5403
+ worktrees.map((wt) => runExperimentInWorktree(wt))
6082
5404
  );
6083
- }
6084
- info("");
6085
- header("Aggregation");
6086
- const summary = aggregateSwarmResults(root, db, results);
6087
- summary.goal = goal;
6088
- if (summary.bestExperiment && isMergeable(summary.bestExperiment.overallGrade)) {
6089
- const best = summary.bestExperiment;
6090
- info(`Best experiment: ${best.worktree.slug} (${best.overallGrade})`);
6091
- try {
6092
- (0, import_node_child_process9.execSync)(
6093
- `git merge ${best.worktree.branch} --no-ff -m "Merge swarm winner: ${best.worktree.slug}"`,
6094
- { cwd: root, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
5405
+ results = settled.map((s, i) => {
5406
+ if (s.status === "fulfilled") return s.value;
5407
+ return {
5408
+ worktree: worktrees[i],
5409
+ experiment: null,
5410
+ finalStatus: "error",
5411
+ overallGrade: null,
5412
+ costUsd: 0,
5413
+ stepCount: 0,
5414
+ error: s.reason instanceof Error ? s.reason.message : String(s.reason)
5415
+ };
5416
+ });
5417
+ for (const r of results) {
5418
+ updateSwarmMember(
5419
+ db,
5420
+ swarmRun.id,
5421
+ r.worktree.slug,
5422
+ r.finalStatus,
5423
+ r.overallGrade,
5424
+ r.costUsd,
5425
+ r.error ?? null
6095
5426
  );
6096
- success(`Merged ${best.worktree.slug} into main.`);
6097
- } catch {
6098
- warn(`Git merge of ${best.worktree.slug} failed. Merge manually with:`);
6099
- info(` git merge ${best.worktree.branch} --no-ff`);
6100
5427
  }
6101
- } else {
6102
- info("No experiment achieved sound/good grade. Nothing merged.");
6103
- }
6104
- for (const r of results) {
6105
- if (r === summary.bestExperiment || r.error || !r.experiment) continue;
6106
- const mainExp = getExperimentBySlug(db, r.worktree.slug);
6107
- if (mainExp && mainExp.status !== "dead_end") {
6108
- updateExperimentStatus(db, mainExp.id, "dead_end");
5428
+ info("");
5429
+ header("Aggregation");
5430
+ summary = aggregateSwarmResults(root, db, results);
5431
+ summary.goal = goal;
5432
+ if (summary.bestExperiment && isMergeable(summary.bestExperiment.overallGrade)) {
5433
+ const best = summary.bestExperiment;
5434
+ info(`Best experiment: ${best.worktree.slug} (${best.overallGrade})`);
5435
+ try {
5436
+ (0, import_node_child_process10.execFileSync)(
5437
+ "git",
5438
+ ["merge", best.worktree.branch, "--no-ff", "-m", `Merge swarm winner: ${best.worktree.slug}`],
5439
+ { cwd: root, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
5440
+ );
5441
+ success(`Merged ${best.worktree.slug} into main.`);
5442
+ } catch {
5443
+ warn(`Git merge of ${best.worktree.slug} failed. Merge manually with:`);
5444
+ info(` git merge ${best.worktree.branch} --no-ff`);
5445
+ }
5446
+ } else {
5447
+ info("No experiment achieved sound/good grade. Nothing merged.");
5448
+ }
5449
+ for (const r of results) {
5450
+ if (r === summary.bestExperiment || r.error || !r.experiment) continue;
5451
+ const mainExp = getExperimentBySlug(db, r.worktree.slug);
5452
+ if (mainExp && mainExp.status !== "dead_end") {
5453
+ adminTransitionAndPersist(db, mainExp.id, mainExp.status, "dead_end" /* DEAD_END */, "error_recovery");
5454
+ }
5455
+ }
5456
+ updateSwarmRun(
5457
+ db,
5458
+ swarmRun.id,
5459
+ summary.errorCount === results.length ? "failed" : "completed",
5460
+ summary.totalCostUsd,
5461
+ summary.bestExperiment?.worktree.slug ?? null
5462
+ );
5463
+ } finally {
5464
+ info("Cleaning up worktrees...");
5465
+ for (const wt of worktrees) {
5466
+ cleanupWorktree(root, wt);
6109
5467
  }
6110
- }
6111
- updateSwarmRun(
6112
- db,
6113
- swarmRun.id,
6114
- summary.errorCount === results.length ? "failed" : "completed",
6115
- summary.totalCostUsd,
6116
- summary.bestExperiment?.worktree.slug ?? null
6117
- );
6118
- info("Cleaning up worktrees...");
6119
- for (const wt of worktrees) {
6120
- cleanupWorktree(root, wt);
6121
5468
  }
6122
5469
  info("");
6123
5470
  header("Swarm Summary");
@@ -6137,15 +5484,15 @@ function isMergeable(grade) {
6137
5484
  }
6138
5485
  async function deriveMultipleHypotheses(goal, root, count) {
6139
5486
  const synthesis = truncateContext(
6140
- readFileOrEmpty(path18.join(root, "docs", "synthesis", "current.md")),
5487
+ readFileOrEmpty(path19.join(root, "docs", "synthesis", "current.md")),
6141
5488
  CONTEXT_LIMITS.synthesis
6142
5489
  );
6143
5490
  const fragility = truncateContext(
6144
- readFileOrEmpty(path18.join(root, "docs", "synthesis", "fragility.md")),
5491
+ readFileOrEmpty(path19.join(root, "docs", "synthesis", "fragility.md")),
6145
5492
  CONTEXT_LIMITS.fragility
6146
5493
  );
6147
5494
  const deadEndsDoc = truncateContext(
6148
- readFileOrEmpty(path18.join(root, "docs", "synthesis", "dead-ends.md")),
5495
+ readFileOrEmpty(path19.join(root, "docs", "synthesis", "dead-ends.md")),
6149
5496
  CONTEXT_LIMITS.deadEnds
6150
5497
  );
6151
5498
  const diagnosis = truncateContext(readLatestDiagnosis(root), CONTEXT_LIMITS.synthesis);
@@ -6155,7 +5502,7 @@ async function deriveMultipleHypotheses(goal, root, count) {
6155
5502
  let metricsOutput = "";
6156
5503
  if (config.metrics?.command) {
6157
5504
  try {
6158
- metricsOutput = (0, import_node_child_process9.execSync)(config.metrics.command, {
5505
+ metricsOutput = (0, import_node_child_process10.execSync)(config.metrics.command, {
6159
5506
  cwd: root,
6160
5507
  encoding: "utf-8",
6161
5508
  timeout: 6e4,
@@ -6235,14 +5582,16 @@ If the goal is met:
6235
5582
  warn("Planner did not return structured hypotheses. Using goal as single hypothesis.");
6236
5583
  return [goal];
6237
5584
  }
6238
- var path18, import_node_child_process9, MAX_PARALLEL, DEFAULT_PARALLEL;
5585
+ var path19, import_node_child_process10, MAX_PARALLEL, DEFAULT_PARALLEL;
6239
5586
  var init_swarm = __esm({
6240
5587
  "src/commands/swarm.ts"() {
6241
5588
  "use strict";
6242
- path18 = __toESM(require("path"));
6243
- import_node_child_process9 = require("child_process");
5589
+ path19 = __toESM(require("path"));
5590
+ import_node_child_process10 = require("child_process");
6244
5591
  init_connection();
6245
5592
  init_queries();
5593
+ init_machine();
5594
+ init_types2();
6246
5595
  init_spawn();
6247
5596
  init_config();
6248
5597
  init_worktree();
@@ -6265,21 +5614,21 @@ async function diagnose(args) {
6265
5614
  const db = getDb(root);
6266
5615
  const focus = args.filter((a) => !a.startsWith("--")).join(" ");
6267
5616
  const keepScripts = args.includes("--keep-scripts");
6268
- const scriptsDir = path19.join(root, ".majlis", "scripts");
6269
- if (!fs18.existsSync(scriptsDir)) {
6270
- fs18.mkdirSync(scriptsDir, { recursive: true });
5617
+ const scriptsDir = path20.join(root, ".majlis", "scripts");
5618
+ if (!fs19.existsSync(scriptsDir)) {
5619
+ fs19.mkdirSync(scriptsDir, { recursive: true });
6271
5620
  }
6272
5621
  header("Deep Diagnosis");
6273
5622
  if (focus) info(`Focus: ${focus}`);
6274
5623
  const dbExport = exportForDiagnostician(db);
6275
- const synthesis = readFileOrEmpty(path19.join(root, "docs", "synthesis", "current.md"));
6276
- const fragility = readFileOrEmpty(path19.join(root, "docs", "synthesis", "fragility.md"));
6277
- const deadEndsDoc = readFileOrEmpty(path19.join(root, "docs", "synthesis", "dead-ends.md"));
5624
+ const synthesis = readFileOrEmpty(path20.join(root, "docs", "synthesis", "current.md"));
5625
+ const fragility = readFileOrEmpty(path20.join(root, "docs", "synthesis", "fragility.md"));
5626
+ const deadEndsDoc = readFileOrEmpty(path20.join(root, "docs", "synthesis", "dead-ends.md"));
6278
5627
  const config = loadConfig(root);
6279
5628
  let metricsOutput = "";
6280
5629
  if (config.metrics?.command) {
6281
5630
  try {
6282
- metricsOutput = (0, import_node_child_process10.execSync)(config.metrics.command, {
5631
+ metricsOutput = (0, import_node_child_process11.execSync)(config.metrics.command, {
6283
5632
  cwd: root,
6284
5633
  encoding: "utf-8",
6285
5634
  timeout: 6e4,
@@ -6325,13 +5674,13 @@ Perform a deep diagnostic analysis of this project. Identify root causes, recurr
6325
5674
  Remember: you may write files ONLY to .majlis/scripts/. You cannot modify project code.`;
6326
5675
  info("Spawning diagnostician (60 turns, full DB access)...");
6327
5676
  const result = await spawnAgent("diagnostician", { taskPrompt }, root);
6328
- const diagnosisDir = path19.join(root, "docs", "diagnosis");
6329
- if (!fs18.existsSync(diagnosisDir)) {
6330
- fs18.mkdirSync(diagnosisDir, { recursive: true });
5677
+ const diagnosisDir = path20.join(root, "docs", "diagnosis");
5678
+ if (!fs19.existsSync(diagnosisDir)) {
5679
+ fs19.mkdirSync(diagnosisDir, { recursive: true });
6331
5680
  }
6332
5681
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
6333
- const artifactPath = path19.join(diagnosisDir, `diagnosis-${timestamp}.md`);
6334
- fs18.writeFileSync(artifactPath, result.output);
5682
+ const artifactPath = path20.join(diagnosisDir, `diagnosis-${timestamp}.md`);
5683
+ fs19.writeFileSync(artifactPath, result.output);
6335
5684
  info(`Diagnostic report: docs/diagnosis/diagnosis-${timestamp}.md`);
6336
5685
  if (result.structured?.diagnosis) {
6337
5686
  const d = result.structured.diagnosis;
@@ -6344,11 +5693,11 @@ Remember: you may write files ONLY to .majlis/scripts/. You cannot modify projec
6344
5693
  }
6345
5694
  if (!keepScripts) {
6346
5695
  try {
6347
- const files = fs18.readdirSync(scriptsDir);
5696
+ const files = fs19.readdirSync(scriptsDir);
6348
5697
  for (const f of files) {
6349
- fs18.unlinkSync(path19.join(scriptsDir, f));
5698
+ fs19.unlinkSync(path20.join(scriptsDir, f));
6350
5699
  }
6351
- fs18.rmdirSync(scriptsDir);
5700
+ fs19.rmdirSync(scriptsDir);
6352
5701
  info("Cleaned up .majlis/scripts/");
6353
5702
  } catch {
6354
5703
  }
@@ -6361,13 +5710,13 @@ Remember: you may write files ONLY to .majlis/scripts/. You cannot modify projec
6361
5710
  autoCommit(root, `diagnosis: ${focus || "general"}`);
6362
5711
  success("Diagnosis complete.");
6363
5712
  }
6364
- var fs18, path19, import_node_child_process10;
5713
+ var fs19, path20, import_node_child_process11;
6365
5714
  var init_diagnose = __esm({
6366
5715
  "src/commands/diagnose.ts"() {
6367
5716
  "use strict";
6368
- fs18 = __toESM(require("fs"));
6369
- path19 = __toESM(require("path"));
6370
- import_node_child_process10 = require("child_process");
5717
+ fs19 = __toESM(require("fs"));
5718
+ path20 = __toESM(require("path"));
5719
+ import_node_child_process11 = require("child_process");
6371
5720
  init_connection();
6372
5721
  init_queries();
6373
5722
  init_spawn();
@@ -6406,8 +5755,9 @@ function getLastActivityTimestamp(db) {
6406
5755
  }
6407
5756
  function getCommitsSince(root, timestamp) {
6408
5757
  try {
6409
- const output = (0, import_node_child_process11.execSync)(
6410
- `git log --since="${timestamp}" --oneline -- . ":!.majlis/" ":!docs/"`,
5758
+ const output = (0, import_node_child_process12.execFileSync)(
5759
+ "git",
5760
+ ["log", `--since=${timestamp}`, "--oneline", "--", ".", ":!.majlis/", ":!docs/"],
6411
5761
  { cwd: root, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
6412
5762
  ).trim();
6413
5763
  if (!output) return 0;
@@ -6418,13 +5768,15 @@ function getCommitsSince(root, timestamp) {
6418
5768
  }
6419
5769
  function getGitDiffStat(root, timestamp) {
6420
5770
  try {
6421
- const baseRef = (0, import_node_child_process11.execSync)(
6422
- `git rev-list -1 --before="${timestamp}" HEAD`,
5771
+ const baseRef = (0, import_node_child_process12.execFileSync)(
5772
+ "git",
5773
+ ["rev-list", "-1", `--before=${timestamp}`, "HEAD"],
6423
5774
  { cwd: root, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
6424
5775
  ).trim();
6425
5776
  if (!baseRef) return { stat: "", filesChanged: 0 };
6426
- const stat = (0, import_node_child_process11.execSync)(
6427
- `git diff --stat ${baseRef} -- . ":!.majlis/" ":!docs/"`,
5777
+ const stat = (0, import_node_child_process12.execFileSync)(
5778
+ "git",
5779
+ ["diff", "--stat", baseRef, "--", ".", ":!.majlis/", ":!docs/"],
6428
5780
  { cwd: root, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
6429
5781
  ).trim();
6430
5782
  const lines = stat.split("\n");
@@ -6459,11 +5811,11 @@ function checkMetrics(root, config) {
6459
5811
  const command = config.metrics?.command;
6460
5812
  if (!command) return { working: false, error: "No metrics command configured" };
6461
5813
  try {
6462
- const scriptPath = path20.join(root, command);
6463
- if (command.includes("/") && !fs19.existsSync(scriptPath)) {
5814
+ const scriptPath = path21.join(root, command);
5815
+ if (command.includes("/") && !fs20.existsSync(scriptPath)) {
6464
5816
  return { working: false, error: `Script not found: ${command}` };
6465
5817
  }
6466
- const output = (0, import_node_child_process11.execSync)(command, {
5818
+ const output = (0, import_node_child_process12.execSync)(command, {
6467
5819
  cwd: root,
6468
5820
  encoding: "utf-8",
6469
5821
  timeout: 6e4,
@@ -6491,10 +5843,10 @@ function assessStaleness(db, root, profile, config) {
6491
5843
  filesChanged = diffResult.filesChanged;
6492
5844
  }
6493
5845
  const configDrift = detectConfigDrift(config, profile);
6494
- const synthesisPath = path20.join(root, "docs", "synthesis", "current.md");
5846
+ const synthesisPath = path21.join(root, "docs", "synthesis", "current.md");
6495
5847
  let synthesisSize = 0;
6496
5848
  try {
6497
- synthesisSize = fs19.statSync(synthesisPath).size;
5849
+ synthesisSize = fs20.statSync(synthesisPath).size;
6498
5850
  } catch {
6499
5851
  }
6500
5852
  const unresolvedDoubts = db.prepare(`
@@ -6552,13 +5904,13 @@ function printStalenessReport(report) {
6552
5904
  success(" Already up to date.");
6553
5905
  }
6554
5906
  }
6555
- var fs19, path20, import_node_child_process11;
5907
+ var fs20, path21, import_node_child_process12;
6556
5908
  var init_staleness = __esm({
6557
5909
  "src/scan/staleness.ts"() {
6558
5910
  "use strict";
6559
- fs19 = __toESM(require("fs"));
6560
- path20 = __toESM(require("path"));
6561
- import_node_child_process11 = require("child_process");
5911
+ fs20 = __toESM(require("fs"));
5912
+ path21 = __toESM(require("path"));
5913
+ import_node_child_process12 = require("child_process");
6562
5914
  init_queries();
6563
5915
  init_format();
6564
5916
  }
@@ -6608,13 +5960,13 @@ async function resync(args) {
6608
5960
  return;
6609
5961
  }
6610
5962
  info("Phase 1: Deep re-scan...");
6611
- const synthesisDir = path21.join(root, "docs", "synthesis");
6612
- const scriptsDir = path21.join(root, ".majlis", "scripts");
6613
- if (!fs20.existsSync(synthesisDir)) fs20.mkdirSync(synthesisDir, { recursive: true });
6614
- if (!fs20.existsSync(scriptsDir)) fs20.mkdirSync(scriptsDir, { recursive: true });
5963
+ const synthesisDir = path22.join(root, "docs", "synthesis");
5964
+ const scriptsDir = path22.join(root, ".majlis", "scripts");
5965
+ if (!fs21.existsSync(synthesisDir)) fs21.mkdirSync(synthesisDir, { recursive: true });
5966
+ if (!fs21.existsSync(scriptsDir)) fs21.mkdirSync(scriptsDir, { recursive: true });
6615
5967
  const profileJson = JSON.stringify(profile, null, 2);
6616
- const oldSynthesis = readFileOrEmpty(path21.join(root, "docs", "synthesis", "current.md"));
6617
- const oldFragility = readFileOrEmpty(path21.join(root, "docs", "synthesis", "fragility.md"));
5968
+ const oldSynthesis = readFileOrEmpty(path22.join(root, "docs", "synthesis", "current.md"));
5969
+ const oldFragility = readFileOrEmpty(path22.join(root, "docs", "synthesis", "fragility.md"));
6618
5970
  const dbExport = exportForCompressor(db);
6619
5971
  const stalenessSummary = `Last Majlis activity: ${report.daysSinceActivity} days ago (${report.lastActivitySource}).
6620
5972
  Commits since: ${report.commitsSinceActivity}. Files changed: ${report.filesChanged}.
@@ -6740,10 +6092,10 @@ You may ONLY write to .majlis/scripts/. Output your structured JSON when done.`;
6740
6092
  info("Updated .majlis/config.json with resync results.");
6741
6093
  if (toolsmithOutput.metrics_command) {
6742
6094
  try {
6743
- const metricsPath = path21.join(root, toolsmithOutput.metrics_command);
6744
- if (fs20.existsSync(metricsPath)) {
6095
+ const metricsPath = path22.join(root, toolsmithOutput.metrics_command);
6096
+ if (fs21.existsSync(metricsPath)) {
6745
6097
  try {
6746
- fs20.chmodSync(metricsPath, 493);
6098
+ fs21.chmodSync(metricsPath, 493);
6747
6099
  } catch {
6748
6100
  }
6749
6101
  }
@@ -6780,8 +6132,8 @@ You may ONLY write to .majlis/scripts/. Output your structured JSON when done.`;
6780
6132
  }
6781
6133
  if (cartographerOk) {
6782
6134
  const sessionCount = getSessionsSinceCompression(db);
6783
- const newSynthesisPath = path21.join(root, "docs", "synthesis", "current.md");
6784
- const newSynthesisSize = fs20.existsSync(newSynthesisPath) ? fs20.statSync(newSynthesisPath).size : 0;
6135
+ const newSynthesisPath = path22.join(root, "docs", "synthesis", "current.md");
6136
+ const newSynthesisSize = fs21.existsSync(newSynthesisPath) ? fs21.statSync(newSynthesisPath).size : 0;
6785
6137
  recordCompression(db, sessionCount, report.synthesisSize, newSynthesisSize);
6786
6138
  info("Recorded compression in DB.");
6787
6139
  }
@@ -6791,12 +6143,12 @@ You may ONLY write to .majlis/scripts/. Output your structured JSON when done.`;
6791
6143
  if (toolsmithOk) info(" \u2192 .majlis/scripts/metrics.sh + .majlis/config.json");
6792
6144
  info("Run `majlis status` to see project state.");
6793
6145
  }
6794
- var fs20, path21;
6146
+ var fs21, path22;
6795
6147
  var init_resync = __esm({
6796
6148
  "src/commands/resync.ts"() {
6797
6149
  "use strict";
6798
- fs20 = __toESM(require("fs"));
6799
- path21 = __toESM(require("path"));
6150
+ fs21 = __toESM(require("fs"));
6151
+ path22 = __toESM(require("path"));
6800
6152
  init_connection();
6801
6153
  init_connection();
6802
6154
  init_queries();
@@ -6811,10 +6163,10 @@ var init_resync = __esm({
6811
6163
  });
6812
6164
 
6813
6165
  // src/cli.ts
6814
- var fs21 = __toESM(require("fs"));
6815
- var path22 = __toESM(require("path"));
6166
+ var fs22 = __toESM(require("fs"));
6167
+ var path23 = __toESM(require("path"));
6816
6168
  var VERSION2 = JSON.parse(
6817
- fs21.readFileSync(path22.join(__dirname, "..", "package.json"), "utf-8")
6169
+ fs22.readFileSync(path23.join(__dirname, "..", "package.json"), "utf-8")
6818
6170
  ).version;
6819
6171
  async function main() {
6820
6172
  let sigintCount = 0;
@@ -6899,6 +6251,7 @@ async function main() {
6899
6251
  case "doubt":
6900
6252
  case "scout":
6901
6253
  case "verify":
6254
+ case "gate":
6902
6255
  case "compress": {
6903
6256
  const { cycle: cycle2 } = await Promise.resolve().then(() => (init_cycle(), cycle_exports));
6904
6257
  await cycle2(command, rest);
@@ -6980,6 +6333,9 @@ Lifecycle:
6980
6333
 
6981
6334
  Experiments:
6982
6335
  new "hypothesis" Create experiment, branch, log, DB entry
6336
+ --sub-type TYPE Classify by problem sub-type
6337
+ --depends-on SLUG Block building until dependency is merged
6338
+ --context FILE,FILE Inject domain-specific docs into agent context
6983
6339
  baseline Capture metrics snapshot (before)
6984
6340
  measure Capture metrics snapshot (after)
6985
6341
  compare [--json] Compare before/after, detect regressions
@@ -6992,6 +6348,7 @@ Cycle:
6992
6348
  doubt [experiment] Spawn critic agent
6993
6349
  scout [experiment] Spawn scout agent
6994
6350
  verify [experiment] Spawn verifier agent
6351
+ gate [experiment] Spawn gatekeeper agent
6995
6352
  resolve [experiment] Route based on verification grades
6996
6353
  compress Spawn compressor agent
6997
6354