majlis 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1662 -1307
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -369,6 +369,12 @@ var init_migrations = __esm({
369
369
  db.exec(`
370
370
  ALTER TABLE experiments ADD COLUMN depends_on TEXT;
371
371
  ALTER TABLE experiments ADD COLUMN context_files TEXT;
372
+ `);
373
+ },
374
+ // Migration 007: v6 → v7 — Gate rejection reason (pause instead of auto-kill)
375
+ (db) => {
376
+ db.exec(`
377
+ ALTER TABLE experiments ADD COLUMN gate_rejection_reason TEXT;
372
378
  `);
373
379
  }
374
380
  ];
@@ -512,7 +518,9 @@ You are the Builder. You write code, run experiments, and make technical decisio
512
518
  Before building:
513
519
  1. Read docs/synthesis/current.md for project state \u2014 this IS ground truth. Trust it.
514
520
  2. Read the dead-ends provided in your context \u2014 these are structural constraints.
515
- 3. Read the experiment doc for this experiment \u2014 it has your hypothesis.
521
+ 3. Read your experiment doc \u2014 its path is in your taskPrompt. It already exists
522
+ (the framework created it from a template). Read it, then fill in the Approach
523
+ section before you start coding. Do NOT search for it with glob or ls.
516
524
 
517
525
  The synthesis already contains the diagnosis. Do NOT re-diagnose. Do NOT run
518
526
  exploratory scripts to "understand the problem." The classify/doubt/challenge
@@ -527,6 +535,17 @@ Do NOT read raw data files (fixtures/, ground truth JSON/STL). The synthesis
527
535
  has the relevant facts. Reading raw data wastes turns re-deriving what the
528
536
  doubt/challenge/verify cycle already established.
529
537
 
538
+ ## Anti-patterns (DO NOT \u2014 these waste turns and produce zero value)
539
+ - Do NOT query SQLite or explore \`.majlis/\`. The framework manages its own state.
540
+ - Do NOT use \`ls\`, \`find\`, or broad globs (\`**/*\`) to discover project structure.
541
+ The synthesis has the architecture. Read the specific files named in your hypothesis.
542
+ - Do NOT pipe commands through \`head\`, \`tail\`, or \`| grep\`. The tools handle
543
+ output truncation automatically. Run the command directly.
544
+ - Do NOT create or run exploratory/diagnostic scripts (Python, shell, etc.).
545
+ Diagnosis is the diagnostician's job, not yours.
546
+ - Do NOT spend your reading turns on framework internals, CI config, or build
547
+ system files unless your hypothesis specifically targets them.
548
+
530
549
  ## The Rule: ONE Change, Then Document
531
550
 
532
551
  You make ONE code change per cycle. Not two, not "one more quick fix." ONE.
@@ -584,8 +603,8 @@ If you are running low on turns, STOP coding and immediately:
584
603
  The framework CANNOT recover your work if you get truncated without structured output.
585
604
  An incomplete experiment doc with honest "did not finish" notes is infinitely better
586
605
  than a truncated run with no output. Budget your turns: ~8 turns for reading,
587
- ~10 turns for coding + benchmark, ~5 turns for documentation. If you've used 35+
588
- turns, wrap up NOW regardless of where you are.
606
+ ~20 turns for coding + build verification, ~10 turns for benchmark + documentation.
607
+ If you've used 40+ turns, wrap up NOW regardless of where you are.
589
608
 
590
609
  You may NOT verify your own work or mark your own decisions as proven.
591
610
  Output your decisions in structured format so they can be recorded in the database.
@@ -1096,7 +1115,80 @@ Produce a diagnostic report as markdown. At the end, include:
1096
1115
  ## Safety Reminders
1097
1116
  - You are READ-ONLY for project code. Write ONLY to .majlis/scripts/.
1098
1117
  - Focus on diagnosis, not fixing. Your value is insight, not implementation.
1099
- - Trust the database export over docs/ files when they conflict.`
1118
+ - Trust the database export over docs/ files when they conflict.`,
1119
+ postmortem: `---
1120
+ name: postmortem
1121
+ model: opus
1122
+ tools: [Read, Glob, Grep]
1123
+ ---
1124
+ You are the Post-Mortem Analyst. You analyze reverted or failed experiments and extract
1125
+ structural learnings that prevent future experiments from repeating the same mistakes.
1126
+
1127
+ You run automatically when an experiment is reverted. Your job is to produce a specific,
1128
+ falsifiable structural constraint that blocks future experiments from repeating the approach.
1129
+
1130
+ ## What You Receive
1131
+
1132
+ - The experiment's hypothesis and metadata
1133
+ - Git diff of the experiment branch vs main (what was changed or attempted)
1134
+ - The user's reason for reverting (if provided) \u2014 use as a starting point, not the final answer
1135
+ - Related dead-ends from the registry
1136
+ - Synthesis and fragility docs
1137
+ - Optionally: artifact files (sweep results, build logs, etc.) pointed to by --context
1138
+
1139
+ ## Your Process
1140
+
1141
+ 1. **Read the context** \u2014 understand what was attempted and why it's being reverted.
1142
+ 2. **Examine artifacts** \u2014 if --context files are provided, read them. If sweep results,
1143
+ build logs, or metric outputs exist in the working directory, find and read them.
1144
+ 3. **Analyze the failure** \u2014 determine whether this is structural (approach provably wrong)
1145
+ or procedural (approach might work but was executed poorly or abandoned for other reasons).
1146
+ 4. **Produce the constraint** \u2014 write a specific, falsifiable structural constraint.
1147
+
1148
+ ## Constraint Quality
1149
+
1150
+ Good constraints are specific and block future repetition:
1151
+ - "L6 config space is null \u2014 13-eval Bayesian sweep found all 12 params insensitive (ls=1.27), score ceiling 0.67"
1152
+ - "Relaxing curvature split threshold in recursive_curvature_split causes false splits on pure-surface thin strips (seg_pct 95->72.5)"
1153
+ - "Torus topology prevents genus-0 assumption for manifold extraction"
1154
+
1155
+ Bad constraints are vague and useless:
1156
+ - "Didn't work"
1157
+ - "Manually reverted"
1158
+ - "Needs more investigation"
1159
+
1160
+ ## Scope
1161
+
1162
+ The constraint should clearly state what class of approaches it applies to and what it
1163
+ does NOT apply to. For example:
1164
+ - "SCOPE: Applies to split threshold changes in Pass 2. Does NOT apply to post-Pass-1 merge operations."
1165
+
1166
+ ## Output Format
1167
+
1168
+ Write a brief analysis (2-5 paragraphs), then output:
1169
+
1170
+ <!-- majlis-json
1171
+ {
1172
+ "postmortem": {
1173
+ "why_failed": "What was tried and why it failed \u2014 specific, evidence-based",
1174
+ "structural_constraint": "What this proves about the solution space \u2014 blocks future repeats. Include scope.",
1175
+ "category": "structural or procedural"
1176
+ }
1177
+ }
1178
+ -->
1179
+
1180
+ Categories:
1181
+ - **structural** \u2014 the approach is provably wrong or the solution space is null. Future experiments
1182
+ that repeat this approach should be rejected by the gatekeeper.
1183
+ - **procedural** \u2014 the approach was abandoned for process reasons (e.g., time, priority change,
1184
+ execution error). The approach might still be valid if executed differently.
1185
+
1186
+ ## Safety Reminders
1187
+ - You are READ-ONLY. Do not modify any files.
1188
+ - Focus on extracting the constraint, not on suggesting fixes.
1189
+ - Trust the evidence in the context over speculation.
1190
+ - If you cannot determine the structural constraint from the available context, say so explicitly
1191
+ and categorize as procedural.`
1100
1192
  };
1101
1193
  var SLASH_COMMANDS2 = {
1102
1194
  classify: {
@@ -1771,6 +1863,8 @@ function getExtractionSchema(role) {
1771
1863
  return '{"architecture": {"modules": ["string"], "entry_points": ["string"], "key_abstractions": ["string"], "dependency_graph": "string"}}';
1772
1864
  case "toolsmith":
1773
1865
  return '{"toolsmith": {"metrics_command": "string|null", "build_command": "string|null", "test_command": "string|null", "test_framework": "string|null", "pre_measure": "string|null", "post_measure": "string|null", "fixtures": {}, "tracked": {}, "verification_output": "string", "issues": ["string"]}}';
1866
+ case "postmortem":
1867
+ return '{"postmortem": {"why_failed": "string", "structural_constraint": "string", "category": "structural|procedural"}}';
1774
1868
  default:
1775
1869
  return EXTRACTION_SCHEMA;
1776
1870
  }
@@ -1797,7 +1891,8 @@ var init_types = __esm({
1797
1891
  compressor: ["compression_report"],
1798
1892
  diagnostician: ["diagnosis"],
1799
1893
  cartographer: ["architecture"],
1800
- toolsmith: ["toolsmith"]
1894
+ toolsmith: ["toolsmith"],
1895
+ postmortem: ["postmortem"]
1801
1896
  };
1802
1897
  }
1803
1898
  });
@@ -1896,6 +1991,18 @@ function extractViaPatterns(role, markdown) {
1896
1991
  });
1897
1992
  }
1898
1993
  if (doubts.length > 0) result.doubts = doubts;
1994
+ if (role === "postmortem") {
1995
+ const pmPattern = /(?:WHY\s*FAILED|Why\s*Failed|Failure)\s*[:=]\s*(.+?)(?:\n|$)[\s\S]*?(?:STRUCTURAL\s*CONSTRAINT|Structural\s*Constraint|Constraint)\s*[:=]\s*(.+?)(?:\n|$)/im;
1996
+ const pmMatch = markdown.match(pmPattern);
1997
+ if (pmMatch) {
1998
+ const categoryMatch = markdown.match(/(?:CATEGORY|Category)\s*[:=]\s*(structural|procedural)/im);
1999
+ result.postmortem = {
2000
+ why_failed: pmMatch[1].trim(),
2001
+ structural_constraint: pmMatch[2].trim(),
2002
+ category: categoryMatch?.[1]?.toLowerCase() ?? "procedural"
2003
+ };
2004
+ }
2005
+ }
1899
2006
  if (role === "builder") {
1900
2007
  const abandonPattern = /\[ABANDON\]\s*(.+?)(?:\n|$)[\s\S]*?(?:structural.?constraint|Constraint|CONSTRAINT)\s*[:=]\s*(.+?)(?:\n|$)/im;
1901
2008
  const abandonMatch = markdown.match(abandonPattern);
@@ -1952,7 +2059,7 @@ ${truncated}`;
1952
2059
  }
1953
2060
  }
1954
2061
  function hasData(output) {
1955
- return !!(output.decisions && output.decisions.length > 0 || output.grades && output.grades.length > 0 || output.doubts && output.doubts.length > 0 || output.challenges && output.challenges.length > 0 || output.findings && output.findings.length > 0 || output.guidance || output.reframe || output.compression_report || output.gate_decision || output.diagnosis || output.abandon);
2062
+ return !!(output.decisions && output.decisions.length > 0 || output.grades && output.grades.length > 0 || output.doubts && output.doubts.length > 0 || output.challenges && output.challenges.length > 0 || output.findings && output.findings.length > 0 || output.guidance || output.reframe || output.compression_report || output.gate_decision || output.diagnosis || output.abandon || output.postmortem);
1956
2063
  }
1957
2064
  function validateForRole(role, output) {
1958
2065
  const required = ROLE_REQUIRED_FIELDS[role];
@@ -1998,12 +2105,27 @@ function buildCheckpointMessage(role, toolUseCount, maxTurns) {
1998
2105
  const approxTurn = Math.round(toolUseCount / 2);
1999
2106
  const header2 = `[MAJLIS CHECKPOINT \u2014 ~${approxTurn} of ${maxTurns} turns used]`;
2000
2107
  switch (role) {
2001
- case "builder":
2002
- return `${header2}
2003
- Reminder: ONE code change per cycle.
2004
- - Have you run the benchmark? YES \u2192 document results + output JSON + STOP.
2005
- - If NO \u2192 run it now, then wrap up.
2108
+ case "builder": {
2109
+ if (toolUseCount <= 15) {
2110
+ return `${header2}
2111
+ You should be done reading by now.
2112
+ - Your experiment doc path is in the taskPrompt. Do NOT search for it.
2113
+ - Do NOT query SQLite, explore .majlis/, or glob broadly.
2114
+ - If you haven't started coding, START NOW.`;
2115
+ } else if (toolUseCount <= 40) {
2116
+ return `${header2}
2117
+ ONE code change per cycle.
2118
+ - Have you run the build/benchmark? YES \u2192 document results + output JSON + STOP.
2119
+ - If NO \u2192 finish your change and run it now.
2006
2120
  Do NOT start a second change or investigate unrelated failures.`;
2121
+ } else {
2122
+ return `${header2}
2123
+ URGENT: You are running out of turns.
2124
+ 1. Update the experiment doc with whatever results you have.
2125
+ 2. Output the <!-- majlis-json --> block NOW.
2126
+ The framework CANNOT recover your work without structured output.`;
2127
+ }
2128
+ }
2007
2129
  case "verifier":
2008
2130
  return `${header2}
2009
2131
  AT MOST 3 diagnostic scripts total.
@@ -2046,6 +2168,11 @@ If you are past turn 30, begin writing current.md and fragility.md NOW.`;
2046
2168
  You write ONLY to .majlis/scripts/. Verify toolchain, create metrics wrapper.
2047
2169
  Phase 1 (1-10): verify toolchain. Phase 2 (11-25): create metrics.sh. Phase 3 (26-30): output config JSON.
2048
2170
  If you are past turn 25, output your structured JSON NOW.`;
2171
+ case "postmortem":
2172
+ return `${header2}
2173
+ You are READ-ONLY. Focus on the structural constraint.
2174
+ What does this experiment prove about the solution space?
2175
+ If you have enough context, output your postmortem JSON NOW.`;
2049
2176
  default:
2050
2177
  return `${header2}
2051
2178
  Check: is your core task done? If yes, wrap up and output JSON.`;
@@ -2154,6 +2281,18 @@ function buildPreToolUseGuards(role, cwd) {
2154
2281
  { matcher: "Bash", hooks: [bashGuard] }
2155
2282
  ];
2156
2283
  }
2284
+ if (role === "postmortem") {
2285
+ const blockWrite = async () => {
2286
+ return {
2287
+ decision: "block",
2288
+ reason: "Post-mortem agent is read-only. No file modifications allowed."
2289
+ };
2290
+ };
2291
+ return [
2292
+ { matcher: "Write", hooks: [blockWrite] },
2293
+ { matcher: "Edit", hooks: [blockWrite] }
2294
+ ];
2295
+ }
2157
2296
  if (role === "builder") {
2158
2297
  const bashGuard = async (input) => {
2159
2298
  const toolInput = input.tool_input ?? {};
@@ -2476,10 +2615,11 @@ var init_spawn = __esm({
2476
2615
  gatekeeper: 10,
2477
2616
  diagnostician: 60,
2478
2617
  cartographer: 40,
2479
- toolsmith: 30
2618
+ toolsmith: 30,
2619
+ postmortem: 20
2480
2620
  };
2481
2621
  CHECKPOINT_INTERVAL = {
2482
- builder: 15,
2622
+ builder: 12,
2483
2623
  verifier: 12,
2484
2624
  critic: 15,
2485
2625
  adversary: 15,
@@ -2487,7 +2627,8 @@ var init_spawn = __esm({
2487
2627
  gatekeeper: 2,
2488
2628
  diagnostician: 20,
2489
2629
  cartographer: 12,
2490
- toolsmith: 10
2630
+ toolsmith: 10,
2631
+ postmortem: 8
2491
2632
  };
2492
2633
  }
2493
2634
  });
@@ -3323,6 +3464,56 @@ function autoCommit(root, message) {
3323
3464
  } catch {
3324
3465
  }
3325
3466
  }
3467
+ function handleDeadEndGit(exp, root) {
3468
+ try {
3469
+ const currentBranch = (0, import_node_child_process2.execFileSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
3470
+ cwd: root,
3471
+ encoding: "utf-8"
3472
+ }).trim();
3473
+ if (currentBranch !== exp.branch) return;
3474
+ } catch {
3475
+ return;
3476
+ }
3477
+ try {
3478
+ (0, import_node_child_process2.execSync)('git add -A -- ":!.majlis/"', {
3479
+ cwd: root,
3480
+ encoding: "utf-8",
3481
+ stdio: ["pipe", "pipe", "pipe"]
3482
+ });
3483
+ const diff = (0, import_node_child_process2.execSync)("git diff --cached --stat", {
3484
+ cwd: root,
3485
+ encoding: "utf-8",
3486
+ stdio: ["pipe", "pipe", "pipe"]
3487
+ }).trim();
3488
+ if (diff) {
3489
+ const msg = `EXP-${String(exp.id).padStart(3, "0")}: ${exp.slug} [dead-end]`;
3490
+ (0, import_node_child_process2.execFileSync)("git", ["commit", "-m", msg], {
3491
+ cwd: root,
3492
+ encoding: "utf-8",
3493
+ stdio: ["pipe", "pipe", "pipe"]
3494
+ });
3495
+ info(`Committed builder changes on ${exp.branch} before dead-end.`);
3496
+ }
3497
+ } catch {
3498
+ }
3499
+ try {
3500
+ (0, import_node_child_process2.execFileSync)("git", ["checkout", "main"], {
3501
+ cwd: root,
3502
+ encoding: "utf-8",
3503
+ stdio: ["pipe", "pipe", "pipe"]
3504
+ });
3505
+ } catch {
3506
+ try {
3507
+ (0, import_node_child_process2.execFileSync)("git", ["checkout", "master"], {
3508
+ cwd: root,
3509
+ encoding: "utf-8",
3510
+ stdio: ["pipe", "pipe", "pipe"]
3511
+ });
3512
+ } catch {
3513
+ warn(`Could not switch away from ${exp.branch} \u2014 do this manually.`);
3514
+ }
3515
+ }
3516
+ }
3326
3517
  var import_node_child_process2;
3327
3518
  var init_git = __esm({
3328
3519
  "src/git.ts"() {
@@ -3532,6 +3723,16 @@ function getBuilderGuidance(db, experimentId) {
3532
3723
  const row = db.prepare("SELECT builder_guidance FROM experiments WHERE id = ?").get(experimentId);
3533
3724
  return row?.builder_guidance ?? null;
3534
3725
  }
3726
+ function storeGateRejection(db, experimentId, reason) {
3727
+ db.prepare(`
3728
+ UPDATE experiments SET gate_rejection_reason = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
3729
+ `).run(reason, experimentId);
3730
+ }
3731
+ function clearGateRejection(db, experimentId) {
3732
+ db.prepare(`
3733
+ UPDATE experiments SET gate_rejection_reason = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?
3734
+ `).run(experimentId);
3735
+ }
3535
3736
  function insertDecision(db, experimentId, description, evidenceLevel, justification) {
3536
3737
  const stmt = db.prepare(`
3537
3738
  INSERT INTO decisions (experiment_id, description, evidence_level, justification)
@@ -4215,7 +4416,7 @@ function determineNextStep(exp, valid, hasDoubts2, hasChallenges2) {
4215
4416
  }
4216
4417
  }
4217
4418
  if (status2 === "building" /* BUILDING */) {
4218
- return valid.includes("built" /* BUILT */) ? "built" /* BUILT */ : valid[0];
4419
+ return "building" /* BUILDING */;
4219
4420
  }
4220
4421
  if (status2 === "scouted" /* SCOUTED */) {
4221
4422
  return valid.includes("verifying" /* VERIFYING */) ? "verifying" /* VERIFYING */ : valid[0];
@@ -4223,6 +4424,9 @@ function determineNextStep(exp, valid, hasDoubts2, hasChallenges2) {
4223
4424
  if (status2 === "verified" /* VERIFIED */) {
4224
4425
  return valid.includes("resolved" /* RESOLVED */) ? "resolved" /* RESOLVED */ : valid[0];
4225
4426
  }
4427
+ if (status2 === "resolved" /* RESOLVED */) {
4428
+ return valid.includes("compressed" /* COMPRESSED */) ? "compressed" /* COMPRESSED */ : valid[0];
4429
+ }
4226
4430
  if (status2 === "compressed" /* COMPRESSED */) {
4227
4431
  return valid.includes("merged" /* MERGED */) ? "merged" /* MERGED */ : valid[0];
4228
4432
  }
@@ -4307,869 +4511,701 @@ var init_metrics = __esm({
4307
4511
  }
4308
4512
  });
4309
4513
 
4310
- // src/commands/measure.ts
4311
- var measure_exports = {};
4312
- __export(measure_exports, {
4313
- baseline: () => baseline,
4314
- compare: () => compare,
4315
- measure: () => measure
4316
- });
4317
- async function baseline(args) {
4318
- await captureMetrics("before", args);
4319
- }
4320
- async function measure(args) {
4321
- await captureMetrics("after", args);
4322
- }
4323
- async function captureMetrics(phase, args) {
4324
- const root = findProjectRoot();
4325
- if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
4326
- const db = getDb(root);
4327
- const config = loadConfig(root);
4328
- const expIdStr = getFlagValue(args, "--experiment");
4329
- let exp;
4330
- if (expIdStr !== void 0) {
4331
- exp = getExperimentById(db, Number(expIdStr));
4332
- } else {
4333
- exp = getLatestExperiment(db);
4334
- }
4335
- if (!exp) throw new Error('No active experiment. Run `majlis new "hypothesis"` first.');
4336
- if (config.build.pre_measure) {
4337
- info(`Running pre-measure: ${config.build.pre_measure}`);
4338
- try {
4339
- (0, import_node_child_process4.execSync)(config.build.pre_measure, { cwd: root, encoding: "utf-8", stdio: "inherit" });
4340
- } catch {
4341
- warn("Pre-measure command failed \u2014 continuing anyway.");
4342
- }
4514
+ // src/resolve.ts
4515
+ function worstGrade(grades) {
4516
+ if (grades.length === 0) {
4517
+ throw new Error("Cannot determine grade from empty verification set \u2014 this indicates a data integrity issue");
4343
4518
  }
4344
- if (!config.metrics.command) {
4345
- throw new Error("No metrics.command configured in .majlis/config.json");
4519
+ for (const grade of GRADE_ORDER) {
4520
+ if (grades.some((g) => g.grade === grade)) return grade;
4346
4521
  }
4347
- info(`Running metrics: ${config.metrics.command}`);
4348
- let metricsOutput;
4349
- try {
4350
- metricsOutput = (0, import_node_child_process4.execSync)(config.metrics.command, {
4351
- cwd: root,
4352
- encoding: "utf-8",
4353
- stdio: ["pipe", "pipe", "pipe"]
4354
- });
4355
- } catch (err) {
4356
- throw new Error(`Metrics command failed: ${err instanceof Error ? err.message : String(err)}`);
4522
+ return "sound";
4523
+ }
4524
+ async function resolve2(db, exp, projectRoot) {
4525
+ let grades = getVerificationsByExperiment(db, exp.id);
4526
+ if (grades.length === 0) {
4527
+ warn(`No verification records for ${exp.slug}. Defaulting to weak.`);
4528
+ insertVerification(
4529
+ db,
4530
+ exp.id,
4531
+ "auto-default",
4532
+ "weak",
4533
+ null,
4534
+ null,
4535
+ "No structured verification output. Auto-defaulted to weak."
4536
+ );
4537
+ grades = getVerificationsByExperiment(db, exp.id);
4357
4538
  }
4358
- const parsed = parseMetricsOutput(metricsOutput);
4359
- if (parsed.length === 0) {
4360
- warn("Metrics command returned no data.");
4539
+ const overallGrade = worstGrade(grades);
4540
+ const config = loadConfig(projectRoot);
4541
+ const metricComparisons = compareMetrics(db, exp.id, config);
4542
+ const gateViolations = checkGateViolations(metricComparisons);
4543
+ if (gateViolations.length > 0 && (overallGrade === "sound" || overallGrade === "good")) {
4544
+ warn("Gate fixture regression detected \u2014 blocking merge:");
4545
+ for (const v of gateViolations) {
4546
+ warn(` ${v.fixture} / ${v.metric}: ${v.before} \u2192 ${v.after} (${v.delta > 0 ? "+" : ""}${v.delta})`);
4547
+ }
4548
+ updateExperimentStatus(db, exp.id, "resolved");
4549
+ const guidanceText = `Gate fixture regression blocks merge. Fix these regressions before re-attempting:
4550
+ ` + gateViolations.map((v) => `- ${v.fixture} / ${v.metric}: was ${v.before}, now ${v.after}`).join("\n");
4551
+ transition("resolved" /* RESOLVED */, "building" /* BUILDING */);
4552
+ db.transaction(() => {
4553
+ storeBuilderGuidance(db, exp.id, guidanceText);
4554
+ updateExperimentStatus(db, exp.id, "building");
4555
+ if (exp.sub_type) {
4556
+ incrementSubTypeFailure(db, exp.sub_type, exp.id, "weak");
4557
+ }
4558
+ })();
4559
+ warn(`Experiment ${exp.slug} CYCLING BACK \u2014 gate fixture(s) regressed.`);
4361
4560
  return;
4362
4561
  }
4363
- for (const m of parsed) {
4364
- insertMetric(db, exp.id, phase, m.fixture, m.metric_name, m.metric_value);
4365
- }
4366
- success(`Captured ${parsed.length} metric(s) for ${exp.slug} (phase: ${phase})`);
4367
- if (config.build.post_measure) {
4368
- try {
4369
- (0, import_node_child_process4.execSync)(config.build.post_measure, { cwd: root, encoding: "utf-8", stdio: "inherit" });
4370
- } catch {
4371
- warn("Post-measure command failed.");
4562
+ updateExperimentStatus(db, exp.id, "resolved");
4563
+ switch (overallGrade) {
4564
+ case "sound": {
4565
+ gitMerge(exp.branch, projectRoot);
4566
+ transition("resolved" /* RESOLVED */, "merged" /* MERGED */);
4567
+ updateExperimentStatus(db, exp.id, "merged");
4568
+ success(`Experiment ${exp.slug} MERGED (all sound).`);
4569
+ break;
4570
+ }
4571
+ case "good": {
4572
+ gitMerge(exp.branch, projectRoot);
4573
+ const gaps = grades.filter((g) => g.grade === "good").map((g) => `- **${g.component}**: ${g.notes ?? "minor gaps"}`).join("\n");
4574
+ appendToFragilityMap(projectRoot, exp.slug, gaps);
4575
+ autoCommit(projectRoot, `resolve: fragility gaps from ${exp.slug}`);
4576
+ transition("resolved" /* RESOLVED */, "merged" /* MERGED */);
4577
+ updateExperimentStatus(db, exp.id, "merged");
4578
+ success(`Experiment ${exp.slug} MERGED (good, ${grades.filter((g) => g.grade === "good").length} gaps added to fragility map).`);
4579
+ break;
4580
+ }
4581
+ case "weak": {
4582
+ const confirmedDoubts = getConfirmedDoubts(db, exp.id);
4583
+ const guidance = await spawnSynthesiser({
4584
+ experiment: {
4585
+ id: exp.id,
4586
+ slug: exp.slug,
4587
+ hypothesis: exp.hypothesis,
4588
+ status: exp.status,
4589
+ sub_type: exp.sub_type,
4590
+ builder_guidance: exp.builder_guidance
4591
+ },
4592
+ verificationReport: grades,
4593
+ confirmedDoubts,
4594
+ 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."
4595
+ }, projectRoot);
4596
+ const guidanceText = guidance.structured?.guidance ?? guidance.output;
4597
+ transition("resolved" /* RESOLVED */, "building" /* BUILDING */);
4598
+ db.transaction(() => {
4599
+ storeBuilderGuidance(db, exp.id, guidanceText);
4600
+ updateExperimentStatus(db, exp.id, "building");
4601
+ if (exp.sub_type) {
4602
+ incrementSubTypeFailure(db, exp.sub_type, exp.id, "weak");
4603
+ }
4604
+ })();
4605
+ warn(`Experiment ${exp.slug} CYCLING BACK (weak). Guidance generated for builder.`);
4606
+ break;
4607
+ }
4608
+ case "rejected": {
4609
+ gitRevert(exp.branch, projectRoot);
4610
+ const rejectedComponents = grades.filter((g) => g.grade === "rejected");
4611
+ const whyFailed = rejectedComponents.map((r) => r.notes ?? "rejected").join("; ");
4612
+ transition("resolved" /* RESOLVED */, "dead_end" /* DEAD_END */);
4613
+ db.transaction(() => {
4614
+ insertDeadEnd(
4615
+ db,
4616
+ exp.id,
4617
+ exp.hypothesis ?? exp.slug,
4618
+ whyFailed,
4619
+ `Approach rejected: ${whyFailed}`,
4620
+ exp.sub_type,
4621
+ "structural"
4622
+ );
4623
+ updateExperimentStatus(db, exp.id, "dead_end");
4624
+ if (exp.sub_type) {
4625
+ incrementSubTypeFailure(db, exp.sub_type, exp.id, "rejected");
4626
+ }
4627
+ })();
4628
+ info(`Experiment ${exp.slug} DEAD-ENDED (rejected). Constraint recorded.`);
4629
+ break;
4372
4630
  }
4373
4631
  }
4374
4632
  }
4375
- async function compare(args, isJson) {
4376
- const root = findProjectRoot();
4377
- if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
4378
- const db = getDb(root);
4379
- const config = loadConfig(root);
4380
- const expIdStr = getFlagValue(args, "--experiment");
4381
- let exp;
4382
- if (expIdStr !== void 0) {
4383
- exp = getExperimentById(db, Number(expIdStr));
4384
- } else {
4385
- exp = getLatestExperiment(db);
4386
- }
4387
- if (!exp) throw new Error("No active experiment.");
4388
- const comparisons = compareMetrics(db, exp.id, config);
4389
- if (comparisons.length === 0) {
4390
- warn(`No before/after metrics to compare for ${exp.slug}. Run baseline and measure first.`);
4391
- return;
4633
+ async function resolveDbOnly(db, exp, projectRoot) {
4634
+ let grades = getVerificationsByExperiment(db, exp.id);
4635
+ if (grades.length === 0) {
4636
+ warn(`No verification records for ${exp.slug}. Defaulting to weak.`);
4637
+ insertVerification(
4638
+ db,
4639
+ exp.id,
4640
+ "auto-default",
4641
+ "weak",
4642
+ null,
4643
+ null,
4644
+ "No structured verification output. Auto-defaulted to weak."
4645
+ );
4646
+ grades = getVerificationsByExperiment(db, exp.id);
4392
4647
  }
4393
- if (isJson) {
4394
- console.log(JSON.stringify({ experiment: exp.slug, comparisons }, null, 2));
4395
- return;
4648
+ const overallGrade = worstGrade(grades);
4649
+ const config = loadConfig(projectRoot);
4650
+ const metricComparisons = compareMetrics(db, exp.id, config);
4651
+ const gateViolations = checkGateViolations(metricComparisons);
4652
+ if (gateViolations.length > 0 && (overallGrade === "sound" || overallGrade === "good")) {
4653
+ warn("Gate fixture regression detected \u2014 blocking merge:");
4654
+ for (const v of gateViolations) {
4655
+ warn(` ${v.fixture} / ${v.metric}: ${v.before} \u2192 ${v.after} (${v.delta > 0 ? "+" : ""}${v.delta})`);
4656
+ }
4657
+ updateExperimentStatus(db, exp.id, "resolved");
4658
+ const guidanceText = `Gate fixture regression blocks merge. Fix these regressions before re-attempting:
4659
+ ` + gateViolations.map((v) => `- ${v.fixture} / ${v.metric}: was ${v.before}, now ${v.after}`).join("\n");
4660
+ transition("resolved" /* RESOLVED */, "building" /* BUILDING */);
4661
+ db.transaction(() => {
4662
+ storeBuilderGuidance(db, exp.id, guidanceText);
4663
+ updateExperimentStatus(db, exp.id, "building");
4664
+ if (exp.sub_type) {
4665
+ incrementSubTypeFailure(db, exp.sub_type, exp.id, "weak");
4666
+ }
4667
+ })();
4668
+ warn(`Experiment ${exp.slug} CYCLING BACK \u2014 gate fixture(s) regressed.`);
4669
+ return "weak";
4396
4670
  }
4397
- header(`Metric Comparison \u2014 ${exp.slug}`);
4398
- const regressions = comparisons.filter((c) => c.regression);
4399
- const rows = comparisons.map((c) => [
4400
- c.fixture,
4401
- c.metric,
4402
- String(c.before),
4403
- String(c.after),
4404
- formatDelta(c.delta),
4405
- c.regression ? red("REGRESSION") : green("OK")
4406
- ]);
4407
- console.log(table(["Fixture", "Metric", "Before", "After", "Delta", "Status"], rows));
4408
- if (regressions.length > 0) {
4409
- console.log();
4410
- warn(`${regressions.length} regression(s) detected!`);
4411
- } else {
4412
- console.log();
4413
- success("No regressions detected.");
4671
+ updateExperimentStatus(db, exp.id, "resolved");
4672
+ switch (overallGrade) {
4673
+ case "sound":
4674
+ transition("resolved" /* RESOLVED */, "merged" /* MERGED */);
4675
+ updateExperimentStatus(db, exp.id, "merged");
4676
+ success(`Experiment ${exp.slug} RESOLVED (sound) \u2014 git merge deferred.`);
4677
+ break;
4678
+ case "good": {
4679
+ const gaps = grades.filter((g) => g.grade === "good").map((g) => `- **${g.component}**: ${g.notes ?? "minor gaps"}`).join("\n");
4680
+ appendToFragilityMap(projectRoot, exp.slug, gaps);
4681
+ transition("resolved" /* RESOLVED */, "merged" /* MERGED */);
4682
+ updateExperimentStatus(db, exp.id, "merged");
4683
+ success(`Experiment ${exp.slug} RESOLVED (good) \u2014 git merge deferred.`);
4684
+ break;
4685
+ }
4686
+ case "weak": {
4687
+ const confirmedDoubts = getConfirmedDoubts(db, exp.id);
4688
+ const guidance = await spawnSynthesiser({
4689
+ experiment: {
4690
+ id: exp.id,
4691
+ slug: exp.slug,
4692
+ hypothesis: exp.hypothesis,
4693
+ status: exp.status,
4694
+ sub_type: exp.sub_type,
4695
+ builder_guidance: exp.builder_guidance
4696
+ },
4697
+ verificationReport: grades,
4698
+ confirmedDoubts,
4699
+ 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."
4700
+ }, projectRoot);
4701
+ const guidanceText = guidance.structured?.guidance ?? guidance.output;
4702
+ transition("resolved" /* RESOLVED */, "building" /* BUILDING */);
4703
+ db.transaction(() => {
4704
+ storeBuilderGuidance(db, exp.id, guidanceText);
4705
+ updateExperimentStatus(db, exp.id, "building");
4706
+ if (exp.sub_type) {
4707
+ incrementSubTypeFailure(db, exp.sub_type, exp.id, "weak");
4708
+ }
4709
+ })();
4710
+ warn(`Experiment ${exp.slug} CYCLING BACK (weak). Guidance generated.`);
4711
+ break;
4712
+ }
4713
+ case "rejected": {
4714
+ const rejectedComponents = grades.filter((g) => g.grade === "rejected");
4715
+ const whyFailed = rejectedComponents.map((r) => r.notes ?? "rejected").join("; ");
4716
+ transition("resolved" /* RESOLVED */, "dead_end" /* DEAD_END */);
4717
+ db.transaction(() => {
4718
+ insertDeadEnd(
4719
+ db,
4720
+ exp.id,
4721
+ exp.hypothesis ?? exp.slug,
4722
+ whyFailed,
4723
+ `Approach rejected: ${whyFailed}`,
4724
+ exp.sub_type,
4725
+ "structural"
4726
+ );
4727
+ updateExperimentStatus(db, exp.id, "dead_end");
4728
+ if (exp.sub_type) {
4729
+ incrementSubTypeFailure(db, exp.sub_type, exp.id, "rejected");
4730
+ }
4731
+ })();
4732
+ info(`Experiment ${exp.slug} DEAD-ENDED (rejected). Constraint recorded.`);
4733
+ break;
4734
+ }
4414
4735
  }
4736
+ return overallGrade;
4415
4737
  }
4416
- function formatDelta(delta) {
4417
- const prefix = delta > 0 ? "+" : "";
4418
- return `${prefix}${delta.toFixed(4)}`;
4419
- }
4420
- var import_node_child_process4;
4421
- var init_measure = __esm({
4422
- "src/commands/measure.ts"() {
4423
- "use strict";
4424
- import_node_child_process4 = require("child_process");
4425
- init_connection();
4426
- init_queries();
4427
- init_metrics();
4428
- init_config();
4429
- init_format();
4430
- }
4431
- });
4432
-
4433
- // src/commands/experiment.ts
4434
- var experiment_exports = {};
4435
- __export(experiment_exports, {
4436
- newExperiment: () => newExperiment,
4437
- revert: () => revert
4438
- });
4439
- async function newExperiment(args) {
4440
- const root = findProjectRoot();
4441
- if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
4442
- const hypothesis = args.filter((a) => !a.startsWith("--")).join(" ");
4443
- if (!hypothesis) {
4444
- throw new Error('Usage: majlis new "hypothesis"');
4445
- }
4446
- const db = getDb(root);
4447
- const config = loadConfig(root);
4448
- const slug = getFlagValue(args, "--slug") ?? await generateSlug(hypothesis, root);
4449
- if (getExperimentBySlug(db, slug)) {
4450
- throw new Error(`Experiment with slug "${slug}" already exists.`);
4451
- }
4452
- const allExps = db.prepare("SELECT COUNT(*) as count FROM experiments").get();
4453
- const num = allExps.count + 1;
4454
- const paddedNum = String(num).padStart(3, "0");
4455
- const branch = `exp/${paddedNum}-${slug}`;
4738
+ function gitMerge(branch, cwd) {
4456
4739
  try {
4457
- (0, import_node_child_process5.execFileSync)("git", ["checkout", "-b", branch], {
4458
- cwd: root,
4740
+ try {
4741
+ (0, import_node_child_process4.execFileSync)("git", ["checkout", "main"], {
4742
+ cwd,
4743
+ encoding: "utf-8",
4744
+ stdio: ["pipe", "pipe", "pipe"]
4745
+ });
4746
+ } catch {
4747
+ (0, import_node_child_process4.execFileSync)("git", ["checkout", "master"], {
4748
+ cwd,
4749
+ encoding: "utf-8",
4750
+ stdio: ["pipe", "pipe", "pipe"]
4751
+ });
4752
+ }
4753
+ (0, import_node_child_process4.execFileSync)("git", ["merge", branch, "--no-ff", "-m", `Merge experiment branch ${branch}`], {
4754
+ cwd,
4459
4755
  encoding: "utf-8",
4460
4756
  stdio: ["pipe", "pipe", "pipe"]
4461
4757
  });
4462
- info(`Created branch: ${branch}`);
4463
4758
  } catch (err) {
4464
- warn(`Could not create branch ${branch} \u2014 continuing without git branch.`);
4465
- }
4466
- const subType = getFlagValue(args, "--sub-type") ?? null;
4467
- const dependsOn = getFlagValue(args, "--depends-on") ?? null;
4468
- const contextArg = getFlagValue(args, "--context") ?? null;
4469
- const contextFiles = contextArg ? contextArg.split(",").map((f) => f.trim()) : null;
4470
- if (dependsOn) {
4471
- const depExp = getExperimentBySlug(db, dependsOn);
4472
- if (!depExp) {
4473
- throw new Error(`Dependency experiment not found: ${dependsOn}`);
4474
- }
4475
- info(`Depends on: ${dependsOn} (status: ${depExp.status})`);
4476
- }
4477
- const exp = createExperiment(db, slug, branch, hypothesis, subType, null, dependsOn, contextFiles);
4478
- if (contextFiles) {
4479
- info(`Context files: ${contextFiles.join(", ")}`);
4480
- }
4481
- success(`Created experiment #${exp.id}: ${exp.slug}`);
4482
- const docsDir = path10.join(root, "docs", "experiments");
4483
- const templatePath = path10.join(docsDir, "_TEMPLATE.md");
4484
- if (fs10.existsSync(templatePath)) {
4485
- const template = fs10.readFileSync(templatePath, "utf-8");
4486
- 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]);
4487
- const logPath = path10.join(docsDir, `${paddedNum}-${slug}.md`);
4488
- fs10.writeFileSync(logPath, logContent);
4489
- info(`Created experiment log: docs/experiments/${paddedNum}-${slug}.md`);
4490
- }
4491
- autoCommit(root, `new: ${slug}`);
4492
- if (config.cycle.auto_baseline_on_new_experiment && config.metrics.command) {
4493
- info("Auto-baselining... (run `majlis baseline` to do this manually)");
4494
- try {
4495
- const { baseline: baseline2 } = await Promise.resolve().then(() => (init_measure(), measure_exports));
4496
- await baseline2(["--experiment", String(exp.id)]);
4497
- } catch (err) {
4498
- warn("Auto-baseline failed \u2014 run `majlis baseline` manually.");
4499
- }
4759
+ warn(`Git merge of ${branch} failed \u2014 you may need to merge manually.`);
4500
4760
  }
4501
4761
  }
4502
- async function revert(args) {
4503
- const root = findProjectRoot();
4504
- if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
4505
- const db = getDb(root);
4506
- let exp;
4507
- const slugArg = args.filter((a) => !a.startsWith("--"))[0];
4508
- if (slugArg) {
4509
- exp = getExperimentBySlug(db, slugArg);
4510
- if (!exp) throw new Error(`Experiment not found: ${slugArg}`);
4511
- } else {
4512
- exp = getLatestExperiment(db);
4513
- if (!exp) throw new Error("No active experiments to revert.");
4514
- }
4515
- const reason = getFlagValue(args, "--reason") ?? "Manually reverted";
4516
- const category = args.includes("--structural") ? "structural" : "procedural";
4517
- insertDeadEnd(
4518
- db,
4519
- exp.id,
4520
- exp.hypothesis ?? exp.slug,
4521
- reason,
4522
- `Reverted: ${reason}`,
4523
- exp.sub_type,
4524
- category
4525
- );
4526
- adminTransitionAndPersist(db, exp.id, exp.status, "dead_end" /* DEAD_END */, "revert");
4762
+ function gitRevert(branch, cwd) {
4527
4763
  try {
4528
- const currentBranch = (0, import_node_child_process5.execFileSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
4529
- cwd: root,
4764
+ const currentBranch = (0, import_node_child_process4.execFileSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
4765
+ cwd,
4530
4766
  encoding: "utf-8"
4531
4767
  }).trim();
4532
- if (currentBranch === exp.branch) {
4768
+ if (currentBranch === branch) {
4533
4769
  try {
4534
- (0, import_node_child_process5.execFileSync)("git", ["checkout", "main"], {
4535
- cwd: root,
4770
+ (0, import_node_child_process4.execFileSync)("git", ["checkout", "--", "."], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
4771
+ } catch {
4772
+ }
4773
+ try {
4774
+ (0, import_node_child_process4.execFileSync)("git", ["checkout", "main"], {
4775
+ cwd,
4536
4776
  encoding: "utf-8",
4537
4777
  stdio: ["pipe", "pipe", "pipe"]
4538
4778
  });
4539
4779
  } catch {
4540
- (0, import_node_child_process5.execFileSync)("git", ["checkout", "master"], {
4541
- cwd: root,
4780
+ (0, import_node_child_process4.execFileSync)("git", ["checkout", "master"], {
4781
+ cwd,
4542
4782
  encoding: "utf-8",
4543
4783
  stdio: ["pipe", "pipe", "pipe"]
4544
4784
  });
4545
4785
  }
4546
4786
  }
4547
4787
  } catch {
4548
- warn("Could not switch git branches \u2014 do this manually.");
4788
+ warn(`Could not switch away from ${branch} \u2014 you may need to do this manually.`);
4549
4789
  }
4550
- info(`Experiment ${exp.slug} reverted to dead-end. Reason: ${reason}`);
4551
4790
  }
4552
- var fs10, path10, import_node_child_process5;
4553
- var init_experiment = __esm({
4554
- "src/commands/experiment.ts"() {
4555
- "use strict";
4556
- fs10 = __toESM(require("fs"));
4791
+ function appendToFragilityMap(projectRoot, expSlug, gaps) {
4792
+ const fragPath = path10.join(projectRoot, "docs", "synthesis", "fragility.md");
4793
+ let content = "";
4794
+ if (fs10.existsSync(fragPath)) {
4795
+ content = fs10.readFileSync(fragPath, "utf-8");
4796
+ }
4797
+ const entry = `
4798
+ ## From experiment: ${expSlug}
4799
+ ${gaps}
4800
+ `;
4801
+ fs10.writeFileSync(fragPath, content + entry);
4802
+ }
4803
+ var fs10, path10, import_node_child_process4;
4804
+ var init_resolve = __esm({
4805
+ "src/resolve.ts"() {
4806
+ "use strict";
4807
+ fs10 = __toESM(require("fs"));
4557
4808
  path10 = __toESM(require("path"));
4558
- import_node_child_process5 = require("child_process");
4559
- init_connection();
4560
- init_queries();
4561
- init_machine();
4562
4809
  init_types2();
4810
+ init_machine();
4811
+ init_queries();
4812
+ init_metrics();
4563
4813
  init_config();
4564
4814
  init_spawn();
4815
+ import_node_child_process4 = require("child_process");
4565
4816
  init_git();
4566
4817
  init_format();
4567
4818
  }
4568
4819
  });
4569
4820
 
4570
- // src/commands/session.ts
4571
- var session_exports = {};
4572
- __export(session_exports, {
4573
- session: () => session
4821
+ // src/commands/cycle.ts
4822
+ var cycle_exports = {};
4823
+ __export(cycle_exports, {
4824
+ cycle: () => cycle,
4825
+ expDocRelPath: () => expDocRelPath,
4826
+ resolveCmd: () => resolveCmd,
4827
+ runResolve: () => runResolve,
4828
+ runStep: () => runStep
4574
4829
  });
4575
- async function session(args) {
4576
- const subcommand = args[0];
4577
- if (!subcommand || subcommand !== "start" && subcommand !== "end") {
4578
- throw new Error('Usage: majlis session start "intent" | majlis session end');
4579
- }
4830
+ async function cycle(step, args) {
4580
4831
  const root = findProjectRoot();
4581
4832
  if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
4582
4833
  const db = getDb(root);
4583
- if (subcommand === "start") {
4584
- const intent = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
4585
- if (!intent) {
4586
- throw new Error('Usage: majlis session start "intent"');
4587
- }
4588
- const existing = getActiveSession(db);
4589
- if (existing) {
4590
- warn(`Session already active: "${existing.intent}" (started ${existing.started_at})`);
4591
- warn("End it first with `majlis session end`.");
4592
- return;
4593
- }
4594
- const latestExp = getLatestExperiment(db);
4595
- const sess = startSession(db, intent, latestExp?.id ?? null);
4596
- success(`Session started: "${intent}" (id: ${sess.id})`);
4597
- if (latestExp) {
4598
- info(`Linked to experiment: ${latestExp.slug} (${latestExp.status})`);
4599
- }
4600
- } else {
4601
- const active = getActiveSession(db);
4602
- if (!active) {
4603
- throw new Error("No active session to end.");
4604
- }
4605
- const accomplished = getFlagValue(args, "--accomplished") ?? null;
4606
- const unfinished = getFlagValue(args, "--unfinished") ?? null;
4607
- const fragility = getFlagValue(args, "--fragility") ?? null;
4608
- endSession(db, active.id, accomplished, unfinished, fragility);
4609
- success(`Session ended: "${active.intent}"`);
4610
- if (accomplished) info(`Accomplished: ${accomplished}`);
4611
- if (unfinished) info(`Unfinished: ${unfinished}`);
4612
- if (fragility) warn(`New fragility: ${fragility}`);
4834
+ if (step === "compress") return doCompress(db, root);
4835
+ const exp = resolveExperimentArg(db, args);
4836
+ switch (step) {
4837
+ case "build":
4838
+ return doBuild(db, exp, root);
4839
+ case "challenge":
4840
+ return doChallenge(db, exp, root);
4841
+ case "doubt":
4842
+ return doDoubt(db, exp, root);
4843
+ case "scout":
4844
+ return doScout(db, exp, root);
4845
+ case "verify":
4846
+ return doVerify(db, exp, root);
4847
+ case "gate":
4848
+ return doGate(db, exp, root);
4613
4849
  }
4614
4850
  }
4615
- var init_session = __esm({
4616
- "src/commands/session.ts"() {
4617
- "use strict";
4618
- init_connection();
4619
- init_queries();
4620
- init_config();
4621
- init_format();
4622
- }
4623
- });
4624
-
4625
- // src/commands/query.ts
4626
- var query_exports = {};
4627
- __export(query_exports, {
4628
- query: () => query3
4629
- });
4630
- async function query3(command, args, isJson) {
4851
+ async function resolveCmd(args) {
4631
4852
  const root = findProjectRoot();
4632
4853
  if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
4633
4854
  const db = getDb(root);
4634
- switch (command) {
4635
- case "decisions":
4636
- return queryDecisions(db, args, isJson);
4637
- case "dead-ends":
4638
- return queryDeadEnds(db, args, isJson);
4639
- case "fragility":
4640
- return queryFragility(root, isJson);
4641
- case "history":
4642
- return queryHistory(db, args, isJson);
4643
- case "circuit-breakers":
4644
- return queryCircuitBreakers(db, root, isJson);
4645
- case "check-commit":
4646
- return checkCommit(db);
4647
- }
4855
+ const exp = resolveExperimentArg(db, args);
4856
+ transition(exp.status, "resolved" /* RESOLVED */);
4857
+ await resolve2(db, exp, root);
4648
4858
  }
4649
- function queryDecisions(db, args, isJson) {
4650
- const level = getFlagValue(args, "--level");
4651
- const expIdStr = getFlagValue(args, "--experiment");
4652
- const experimentId = expIdStr !== void 0 ? Number(expIdStr) : void 0;
4653
- const decisions = listAllDecisions(db, level, experimentId);
4654
- if (isJson) {
4655
- console.log(JSON.stringify(decisions, null, 2));
4656
- return;
4657
- }
4658
- if (decisions.length === 0) {
4659
- info("No decisions found.");
4660
- return;
4859
+ async function runStep(step, db, exp, root) {
4860
+ switch (step) {
4861
+ case "build":
4862
+ return doBuild(db, exp, root);
4863
+ case "challenge":
4864
+ return doChallenge(db, exp, root);
4865
+ case "doubt":
4866
+ return doDoubt(db, exp, root);
4867
+ case "scout":
4868
+ return doScout(db, exp, root);
4869
+ case "verify":
4870
+ return doVerify(db, exp, root);
4871
+ case "gate":
4872
+ return doGate(db, exp, root);
4873
+ case "compress":
4874
+ return doCompress(db, root);
4661
4875
  }
4662
- header("Decisions");
4663
- const rows = decisions.map((d) => [
4664
- String(d.id),
4665
- String(d.experiment_id),
4666
- evidenceColor(d.evidence_level),
4667
- d.description.slice(0, 60) + (d.description.length > 60 ? "..." : ""),
4668
- d.status
4669
- ]);
4670
- console.log(table(["ID", "Exp", "Level", "Description", "Status"], rows));
4671
4876
  }
4672
- function queryDeadEnds(db, args, isJson) {
4673
- const subType = getFlagValue(args, "--sub-type");
4674
- const searchTerm = getFlagValue(args, "--search");
4675
- let deadEnds;
4676
- if (subType) {
4677
- deadEnds = listDeadEndsBySubType(db, subType);
4678
- } else if (searchTerm) {
4679
- deadEnds = searchDeadEnds(db, searchTerm);
4680
- } else {
4681
- deadEnds = listAllDeadEnds(db);
4682
- }
4683
- if (isJson) {
4684
- console.log(JSON.stringify(deadEnds, null, 2));
4685
- return;
4686
- }
4687
- if (deadEnds.length === 0) {
4688
- info("No dead-ends recorded.");
4689
- return;
4690
- }
4691
- header("Dead-End Registry");
4692
- const rows = deadEnds.map((d) => [
4693
- String(d.id),
4694
- d.sub_type ?? "\u2014",
4695
- d.approach.slice(0, 40) + (d.approach.length > 40 ? "..." : ""),
4696
- d.structural_constraint.slice(0, 40) + (d.structural_constraint.length > 40 ? "..." : "")
4697
- ]);
4698
- console.log(table(["ID", "Sub-Type", "Approach", "Constraint"], rows));
4877
+ async function runResolve(db, exp, root) {
4878
+ transition(exp.status, "resolved" /* RESOLVED */);
4879
+ await resolve2(db, exp, root);
4699
4880
  }
4700
- function queryFragility(root, isJson) {
4701
- const fragPath = path11.join(root, "docs", "synthesis", "fragility.md");
4702
- if (!fs11.existsSync(fragPath)) {
4703
- info("No fragility map found.");
4704
- return;
4705
- }
4706
- const content = fs11.readFileSync(fragPath, "utf-8");
4707
- if (isJson) {
4708
- console.log(JSON.stringify({ content }, null, 2));
4881
+ async function doGate(db, exp, root) {
4882
+ transition(exp.status, "gated" /* GATED */);
4883
+ const synthesis = truncateContext(readFileOrEmpty(path11.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
4884
+ const fragility = truncateContext(readFileOrEmpty(path11.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
4885
+ const structuralDeadEnds = exp.sub_type ? listStructuralDeadEndsBySubType(db, exp.sub_type) : listStructuralDeadEnds(db);
4886
+ const result = await spawnAgent("gatekeeper", {
4887
+ experiment: {
4888
+ id: exp.id,
4889
+ slug: exp.slug,
4890
+ hypothesis: exp.hypothesis,
4891
+ status: exp.status,
4892
+ sub_type: exp.sub_type,
4893
+ builder_guidance: null
4894
+ },
4895
+ deadEnds: structuralDeadEnds.map((d) => ({
4896
+ approach: d.approach,
4897
+ why_failed: d.why_failed,
4898
+ structural_constraint: d.structural_constraint
4899
+ })),
4900
+ fragility,
4901
+ synthesis,
4902
+ taskPrompt: `Gate-check hypothesis for experiment ${exp.slug}:
4903
+ "${exp.hypothesis}"
4904
+
4905
+ This is a FAST gate \u2014 decide in 1-2 turns. Do NOT read source code or large files. Use the synthesis, dead-ends, and fragility provided in your context. At most, do one targeted grep to verify a function name exists.
4906
+
4907
+ Check: (a) stale references \u2014 does the hypothesis reference specific lines, functions, or structures that may not exist? (b) dead-end overlap \u2014 does this hypothesis repeat an approach already ruled out by structural dead-ends? (c) scope \u2014 is this a single focused change, or does it try to do multiple things?
4908
+
4909
+ Output your gate_decision as "approve", "reject", or "flag" with reasoning.`
4910
+ }, root);
4911
+ ingestStructuredOutput(db, exp.id, result.structured);
4912
+ const decision = result.structured?.gate_decision ?? "approve";
4913
+ const reason = result.structured?.reason ?? "";
4914
+ if (decision === "reject") {
4915
+ updateExperimentStatus(db, exp.id, "gated");
4916
+ storeGateRejection(db, exp.id, reason);
4917
+ warn(`Gate REJECTED for ${exp.slug}: ${reason}`);
4918
+ info("Run `majlis next --override-gate` to proceed anyway, or `majlis revert` to abandon.");
4709
4919
  return;
4920
+ } else {
4921
+ if (decision === "flag") {
4922
+ warn(`Gate flagged concerns for ${exp.slug}: ${reason}`);
4923
+ }
4924
+ updateExperimentStatus(db, exp.id, "gated");
4925
+ success(`Gate passed for ${exp.slug}. Run \`majlis build\` next.`);
4710
4926
  }
4711
- header("Fragility Map");
4712
- console.log(content);
4713
4927
  }
4714
- function queryHistory(db, args, isJson) {
4715
- const fixture = args.filter((a) => !a.startsWith("--"))[0];
4716
- if (!fixture) {
4717
- throw new Error("Usage: majlis history <fixture>");
4718
- }
4719
- const history = getMetricHistoryByFixture(db, fixture);
4720
- if (isJson) {
4721
- console.log(JSON.stringify(history, null, 2));
4722
- return;
4723
- }
4724
- if (history.length === 0) {
4725
- info(`No metric history for fixture: ${fixture}`);
4726
- return;
4928
+ async function doBuild(db, exp, root) {
4929
+ if (exp.depends_on) {
4930
+ const dep = getExperimentBySlug(db, exp.depends_on);
4931
+ if (!dep || dep.status !== "merged") {
4932
+ throw new Error(
4933
+ `Experiment "${exp.slug}" depends on "${exp.depends_on}" which is ${dep ? dep.status : "not found"}. Dependency must be merged before building.`
4934
+ );
4935
+ }
4727
4936
  }
4728
- header(`Metric History \u2014 ${fixture}`);
4729
- const rows = history.map((h) => [
4730
- String(h.experiment_id),
4731
- h.experiment_slug ?? "\u2014",
4732
- h.phase,
4733
- h.metric_name,
4734
- String(h.metric_value),
4735
- h.captured_at
4736
- ]);
4737
- console.log(table(["Exp", "Slug", "Phase", "Metric", "Value", "Captured"], rows));
4738
- }
4739
- function queryCircuitBreakers(db, root, isJson) {
4937
+ transition(exp.status, "building" /* BUILDING */);
4938
+ const deadEnds = exp.sub_type ? listDeadEndsBySubType(db, exp.sub_type) : listAllDeadEnds(db);
4939
+ const builderGuidance = getBuilderGuidance(db, exp.id);
4940
+ const fragility = truncateContext(readFileOrEmpty(path11.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
4941
+ const synthesis = truncateContext(readFileOrEmpty(path11.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
4942
+ const confirmedDoubts = getConfirmedDoubts(db, exp.id);
4740
4943
  const config = loadConfig(root);
4741
- const states = getAllCircuitBreakerStates(db, config.cycle.circuit_breaker_threshold);
4742
- if (isJson) {
4743
- console.log(JSON.stringify(states, null, 2));
4744
- return;
4745
- }
4746
- if (states.length === 0) {
4747
- info("No circuit breaker data.");
4748
- return;
4749
- }
4750
- header("Circuit Breakers");
4751
- const rows = states.map((s) => [
4752
- s.sub_type,
4753
- String(s.failure_count),
4754
- String(config.cycle.circuit_breaker_threshold),
4755
- s.tripped ? red("TRIPPED") : green("OK")
4756
- ]);
4757
- console.log(table(["Sub-Type", "Failures", "Threshold", "Status"], rows));
4758
- }
4759
- function checkCommit(db) {
4760
- let stdinData = "";
4761
- try {
4762
- stdinData = fs11.readFileSync(0, "utf-8");
4763
- } catch {
4764
- }
4765
- if (stdinData) {
4944
+ const existingBaseline = getMetricsByExperimentAndPhase(db, exp.id, "before");
4945
+ if (config.metrics?.command && existingBaseline.length === 0) {
4766
4946
  try {
4767
- const hookInput = JSON.parse(stdinData);
4768
- const command = hookInput?.tool_input?.command ?? "";
4769
- if (!command.includes("git commit")) {
4770
- return;
4947
+ const output = (0, import_node_child_process5.execSync)(config.metrics.command, {
4948
+ cwd: root,
4949
+ encoding: "utf-8",
4950
+ timeout: 6e4,
4951
+ stdio: ["pipe", "pipe", "pipe"]
4952
+ }).trim();
4953
+ const parsed = parseMetricsOutput(output);
4954
+ for (const m of parsed) {
4955
+ insertMetric(db, exp.id, "before", m.fixture, m.metric_name, m.metric_value);
4771
4956
  }
4957
+ if (parsed.length > 0) info(`Captured ${parsed.length} baseline metric(s).`);
4772
4958
  } catch {
4959
+ warn("Could not capture baseline metrics.");
4773
4960
  }
4774
4961
  }
4775
- const active = listActiveExperiments(db);
4776
- const unverified = active.filter(
4777
- (e) => !["merged", "dead_end", "verified", "resolved", "compressed"].includes(e.status)
4778
- );
4779
- if (unverified.length > 0) {
4780
- console.error(`[majlis] ${unverified.length} unverified experiment(s):`);
4781
- for (const e of unverified) {
4782
- console.error(` - ${e.slug} (${e.status})`);
4962
+ updateExperimentStatus(db, exp.id, "building");
4963
+ let taskPrompt = builderGuidance ? `Previous attempt was weak. Here is guidance for this attempt:
4964
+ ${builderGuidance}
4965
+
4966
+ Build the experiment: ${exp.hypothesis}` : `Build the experiment: ${exp.hypothesis}`;
4967
+ if (confirmedDoubts.length > 0) {
4968
+ taskPrompt += "\n\n## Confirmed Doubts (MUST address)\nThese weaknesses were confirmed by the verifier. Your build MUST address each one:\n";
4969
+ for (const d of confirmedDoubts) {
4970
+ taskPrompt += `- [${d.severity}] ${d.claim_doubted}: ${d.evidence_for_doubt}
4971
+ `;
4783
4972
  }
4784
- process.exit(1);
4785
- }
4786
- }
4787
- var fs11, path11;
4788
- var init_query = __esm({
4789
- "src/commands/query.ts"() {
4790
- "use strict";
4791
- fs11 = __toESM(require("fs"));
4792
- path11 = __toESM(require("path"));
4793
- init_connection();
4794
- init_queries();
4795
- init_config();
4796
- init_format();
4797
4973
  }
4798
- });
4974
+ taskPrompt += `
4799
4975
 
4800
- // src/resolve.ts
4801
- function worstGrade(grades) {
4802
- if (grades.length === 0) {
4803
- throw new Error("Cannot determine grade from empty verification set \u2014 this indicates a data integrity issue");
4804
- }
4805
- for (const grade of GRADE_ORDER) {
4806
- if (grades.some((g) => g.grade === grade)) return grade;
4976
+ Your experiment doc: ${expDocRelPath(exp)}`;
4977
+ taskPrompt += "\n\nNote: The framework captures metrics automatically. Do NOT claim specific numbers unless quoting framework output.";
4978
+ const supplementaryContext = loadExperimentContext(exp, root);
4979
+ const lineage = exportExperimentLineage(db, exp.sub_type);
4980
+ if (lineage) {
4981
+ taskPrompt += "\n\n" + lineage;
4807
4982
  }
4808
- return "sound";
4809
- }
4810
- async function resolve2(db, exp, projectRoot) {
4811
- let grades = getVerificationsByExperiment(db, exp.id);
4812
- if (grades.length === 0) {
4813
- warn(`No verification records for ${exp.slug}. Defaulting to weak.`);
4814
- insertVerification(
4983
+ const result = await spawnAgent("builder", {
4984
+ experiment: {
4985
+ id: exp.id,
4986
+ slug: exp.slug,
4987
+ hypothesis: exp.hypothesis,
4988
+ status: "building",
4989
+ sub_type: exp.sub_type,
4990
+ builder_guidance: builderGuidance
4991
+ },
4992
+ deadEnds: deadEnds.map((d) => ({
4993
+ approach: d.approach,
4994
+ why_failed: d.why_failed,
4995
+ structural_constraint: d.structural_constraint
4996
+ })),
4997
+ fragility,
4998
+ synthesis,
4999
+ confirmedDoubts,
5000
+ supplementaryContext: supplementaryContext || void 0,
5001
+ experimentLineage: lineage || void 0,
5002
+ taskPrompt
5003
+ }, root);
5004
+ ingestStructuredOutput(db, exp.id, result.structured);
5005
+ if (result.structured?.abandon) {
5006
+ insertDeadEnd(
4815
5007
  db,
4816
5008
  exp.id,
4817
- "auto-default",
4818
- "weak",
4819
- null,
4820
- null,
4821
- "No structured verification output. Auto-defaulted to weak."
5009
+ exp.hypothesis ?? exp.slug,
5010
+ result.structured.abandon.reason,
5011
+ result.structured.abandon.structural_constraint,
5012
+ exp.sub_type,
5013
+ "structural"
4822
5014
  );
4823
- grades = getVerificationsByExperiment(db, exp.id);
4824
- }
4825
- const overallGrade = worstGrade(grades);
4826
- const config = loadConfig(projectRoot);
4827
- const metricComparisons = compareMetrics(db, exp.id, config);
4828
- const gateViolations = checkGateViolations(metricComparisons);
4829
- if (gateViolations.length > 0 && (overallGrade === "sound" || overallGrade === "good")) {
4830
- warn("Gate fixture regression detected \u2014 blocking merge:");
4831
- for (const v of gateViolations) {
4832
- warn(` ${v.fixture} / ${v.metric}: ${v.before} \u2192 ${v.after} (${v.delta > 0 ? "+" : ""}${v.delta})`);
4833
- }
4834
- updateExperimentStatus(db, exp.id, "resolved");
4835
- const guidanceText = `Gate fixture regression blocks merge. Fix these regressions before re-attempting:
4836
- ` + gateViolations.map((v) => `- ${v.fixture} / ${v.metric}: was ${v.before}, now ${v.after}`).join("\n");
4837
- transition("resolved" /* RESOLVED */, "building" /* BUILDING */);
4838
- db.transaction(() => {
4839
- storeBuilderGuidance(db, exp.id, guidanceText);
4840
- updateExperimentStatus(db, exp.id, "building");
4841
- if (exp.sub_type) {
4842
- incrementSubTypeFailure(db, exp.sub_type, exp.id, "weak");
4843
- }
4844
- })();
4845
- warn(`Experiment ${exp.slug} CYCLING BACK \u2014 gate fixture(s) regressed.`);
5015
+ adminTransitionAndPersist(db, exp.id, "building", "dead_end" /* DEAD_END */, "revert");
5016
+ handleDeadEndGit(exp, root);
5017
+ info(`Builder abandoned ${exp.slug}: ${result.structured.abandon.reason}`);
4846
5018
  return;
4847
5019
  }
4848
- updateExperimentStatus(db, exp.id, "resolved");
4849
- switch (overallGrade) {
4850
- case "sound": {
4851
- gitMerge(exp.branch, projectRoot);
4852
- transition("resolved" /* RESOLVED */, "merged" /* MERGED */);
4853
- updateExperimentStatus(db, exp.id, "merged");
4854
- success(`Experiment ${exp.slug} MERGED (all sound).`);
4855
- break;
4856
- }
4857
- case "good": {
4858
- gitMerge(exp.branch, projectRoot);
4859
- const gaps = grades.filter((g) => g.grade === "good").map((g) => `- **${g.component}**: ${g.notes ?? "minor gaps"}`).join("\n");
4860
- appendToFragilityMap(projectRoot, exp.slug, gaps);
4861
- autoCommit(projectRoot, `resolve: fragility gaps from ${exp.slug}`);
4862
- transition("resolved" /* RESOLVED */, "merged" /* MERGED */);
4863
- updateExperimentStatus(db, exp.id, "merged");
4864
- success(`Experiment ${exp.slug} MERGED (good, ${grades.filter((g) => g.grade === "good").length} gaps added to fragility map).`);
4865
- break;
4866
- }
4867
- case "weak": {
4868
- const confirmedDoubts = getConfirmedDoubts(db, exp.id);
4869
- const guidance = await spawnSynthesiser({
4870
- experiment: {
4871
- id: exp.id,
4872
- slug: exp.slug,
4873
- hypothesis: exp.hypothesis,
4874
- status: exp.status,
4875
- sub_type: exp.sub_type,
4876
- builder_guidance: exp.builder_guidance
4877
- },
4878
- verificationReport: grades,
4879
- confirmedDoubts,
4880
- 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."
4881
- }, projectRoot);
4882
- const guidanceText = guidance.structured?.guidance ?? guidance.output;
4883
- transition("resolved" /* RESOLVED */, "building" /* BUILDING */);
4884
- db.transaction(() => {
4885
- storeBuilderGuidance(db, exp.id, guidanceText);
4886
- updateExperimentStatus(db, exp.id, "building");
4887
- if (exp.sub_type) {
4888
- incrementSubTypeFailure(db, exp.sub_type, exp.id, "weak");
5020
+ if (result.truncated && !result.structured) {
5021
+ warn(`Builder was truncated (hit max turns) without producing structured output.`);
5022
+ const recovery = await extractStructuredData("builder", result.output);
5023
+ if (recovery.data && !recovery.data.abandon) {
5024
+ info(`Recovered structured output from truncated builder (tier ${recovery.tier}).`);
5025
+ ingestStructuredOutput(db, exp.id, recovery.data);
5026
+ if (config.build?.pre_measure) {
5027
+ try {
5028
+ const [cmd, ...cmdArgs] = config.build.pre_measure.split(/\s+/);
5029
+ (0, import_node_child_process5.execFileSync)(cmd, cmdArgs, {
5030
+ cwd: root,
5031
+ encoding: "utf-8",
5032
+ timeout: 3e4,
5033
+ stdio: ["pipe", "pipe", "pipe"]
5034
+ });
5035
+ } catch (err) {
5036
+ const errMsg = err instanceof Error ? err.message : String(err);
5037
+ storeBuilderGuidance(
5038
+ db,
5039
+ exp.id,
5040
+ `Build verification failed after truncated recovery.
5041
+ Error: ${errMsg.slice(0, 500)}`
5042
+ );
5043
+ warn(`Build verification failed for ${exp.slug}. Staying at 'building'.`);
5044
+ return;
4889
5045
  }
4890
- })();
4891
- warn(`Experiment ${exp.slug} CYCLING BACK (weak). Guidance generated for builder.`);
4892
- break;
4893
- }
4894
- case "rejected": {
4895
- gitRevert(exp.branch, projectRoot);
4896
- const rejectedComponents = grades.filter((g) => g.grade === "rejected");
4897
- const whyFailed = rejectedComponents.map((r) => r.notes ?? "rejected").join("; ");
4898
- transition("resolved" /* RESOLVED */, "dead_end" /* DEAD_END */);
4899
- db.transaction(() => {
4900
- insertDeadEnd(
5046
+ }
5047
+ if (config.metrics?.command) {
5048
+ try {
5049
+ const output = (0, import_node_child_process5.execSync)(config.metrics.command, {
5050
+ cwd: root,
5051
+ encoding: "utf-8",
5052
+ timeout: 6e4,
5053
+ stdio: ["pipe", "pipe", "pipe"]
5054
+ }).trim();
5055
+ const parsed = parseMetricsOutput(output);
5056
+ for (const m of parsed) {
5057
+ insertMetric(db, exp.id, "after", m.fixture, m.metric_name, m.metric_value);
5058
+ }
5059
+ if (parsed.length > 0) info(`Captured ${parsed.length} post-build metric(s).`);
5060
+ } catch {
5061
+ }
5062
+ }
5063
+ gitCommitBuild(exp, root);
5064
+ if (recovery.tier === 3) {
5065
+ warn(`Builder output extracted via Haiku (tier 3). Data provenance degraded.`);
5066
+ const existing = getBuilderGuidance(db, exp.id) ?? "";
5067
+ storeBuilderGuidance(
4901
5068
  db,
4902
5069
  exp.id,
4903
- exp.hypothesis ?? exp.slug,
4904
- whyFailed,
4905
- `Approach rejected: ${whyFailed}`,
4906
- exp.sub_type,
4907
- "structural"
5070
+ existing + "\n[PROVENANCE WARNING] Builder structured output was reconstructed by a secondary model (tier 3). Treat reported decisions with additional scrutiny."
4908
5071
  );
4909
- updateExperimentStatus(db, exp.id, "dead_end");
4910
- if (exp.sub_type) {
4911
- incrementSubTypeFailure(db, exp.sub_type, exp.id, "rejected");
4912
- }
4913
- })();
4914
- info(`Experiment ${exp.slug} DEAD-ENDED (rejected). Constraint recorded.`);
4915
- break;
4916
- }
4917
- }
4918
- }
4919
- async function resolveDbOnly(db, exp, projectRoot) {
4920
- let grades = getVerificationsByExperiment(db, exp.id);
4921
- if (grades.length === 0) {
4922
- warn(`No verification records for ${exp.slug}. Defaulting to weak.`);
4923
- insertVerification(
4924
- db,
4925
- exp.id,
4926
- "auto-default",
4927
- "weak",
4928
- null,
4929
- null,
4930
- "No structured verification output. Auto-defaulted to weak."
4931
- );
4932
- grades = getVerificationsByExperiment(db, exp.id);
4933
- }
4934
- const overallGrade = worstGrade(grades);
4935
- const config = loadConfig(projectRoot);
4936
- const metricComparisons = compareMetrics(db, exp.id, config);
4937
- const gateViolations = checkGateViolations(metricComparisons);
4938
- if (gateViolations.length > 0 && (overallGrade === "sound" || overallGrade === "good")) {
4939
- warn("Gate fixture regression detected \u2014 blocking merge:");
4940
- for (const v of gateViolations) {
4941
- warn(` ${v.fixture} / ${v.metric}: ${v.before} \u2192 ${v.after} (${v.delta > 0 ? "+" : ""}${v.delta})`);
4942
- }
4943
- updateExperimentStatus(db, exp.id, "resolved");
4944
- const guidanceText = `Gate fixture regression blocks merge. Fix these regressions before re-attempting:
4945
- ` + gateViolations.map((v) => `- ${v.fixture} / ${v.metric}: was ${v.before}, now ${v.after}`).join("\n");
4946
- transition("resolved" /* RESOLVED */, "building" /* BUILDING */);
4947
- db.transaction(() => {
4948
- storeBuilderGuidance(db, exp.id, guidanceText);
4949
- updateExperimentStatus(db, exp.id, "building");
4950
- if (exp.sub_type) {
4951
- incrementSubTypeFailure(db, exp.sub_type, exp.id, "weak");
4952
5072
  }
4953
- })();
4954
- warn(`Experiment ${exp.slug} CYCLING BACK \u2014 gate fixture(s) regressed.`);
4955
- return "weak";
4956
- }
4957
- updateExperimentStatus(db, exp.id, "resolved");
4958
- switch (overallGrade) {
4959
- case "sound":
4960
- transition("resolved" /* RESOLVED */, "merged" /* MERGED */);
4961
- updateExperimentStatus(db, exp.id, "merged");
4962
- success(`Experiment ${exp.slug} RESOLVED (sound) \u2014 git merge deferred.`);
4963
- break;
4964
- case "good": {
4965
- const gaps = grades.filter((g) => g.grade === "good").map((g) => `- **${g.component}**: ${g.notes ?? "minor gaps"}`).join("\n");
4966
- appendToFragilityMap(projectRoot, exp.slug, gaps);
4967
- transition("resolved" /* RESOLVED */, "merged" /* MERGED */);
4968
- updateExperimentStatus(db, exp.id, "merged");
4969
- success(`Experiment ${exp.slug} RESOLVED (good) \u2014 git merge deferred.`);
4970
- break;
4971
- }
4972
- case "weak": {
4973
- const confirmedDoubts = getConfirmedDoubts(db, exp.id);
4974
- const guidance = await spawnSynthesiser({
4975
- experiment: {
4976
- id: exp.id,
4977
- slug: exp.slug,
4978
- hypothesis: exp.hypothesis,
4979
- status: exp.status,
4980
- sub_type: exp.sub_type,
4981
- builder_guidance: exp.builder_guidance
4982
- },
4983
- verificationReport: grades,
4984
- confirmedDoubts,
4985
- 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."
4986
- }, projectRoot);
4987
- const guidanceText = guidance.structured?.guidance ?? guidance.output;
4988
- transition("resolved" /* RESOLVED */, "building" /* BUILDING */);
4989
- db.transaction(() => {
4990
- storeBuilderGuidance(db, exp.id, guidanceText);
4991
- updateExperimentStatus(db, exp.id, "building");
4992
- if (exp.sub_type) {
4993
- incrementSubTypeFailure(db, exp.sub_type, exp.id, "weak");
4994
- }
4995
- })();
4996
- warn(`Experiment ${exp.slug} CYCLING BACK (weak). Guidance generated.`);
4997
- break;
4998
- }
4999
- case "rejected": {
5000
- const rejectedComponents = grades.filter((g) => g.grade === "rejected");
5001
- const whyFailed = rejectedComponents.map((r) => r.notes ?? "rejected").join("; ");
5002
- transition("resolved" /* RESOLVED */, "dead_end" /* DEAD_END */);
5003
- db.transaction(() => {
5004
- insertDeadEnd(
5073
+ updateExperimentStatus(db, exp.id, "built");
5074
+ success(`Build complete for ${exp.slug} (recovered from truncation). Run \`majlis doubt\` or \`majlis challenge\` next.`);
5075
+ } else if (recovery.data?.abandon) {
5076
+ insertDeadEnd(
5077
+ db,
5078
+ exp.id,
5079
+ exp.hypothesis ?? exp.slug,
5080
+ recovery.data.abandon.reason,
5081
+ recovery.data.abandon.structural_constraint,
5082
+ exp.sub_type,
5083
+ "structural"
5084
+ );
5085
+ adminTransitionAndPersist(db, exp.id, "building", "dead_end" /* DEAD_END */, "revert");
5086
+ handleDeadEndGit(exp, root);
5087
+ info(`Builder abandoned ${exp.slug} (recovered from truncation): ${recovery.data.abandon.reason}`);
5088
+ } else {
5089
+ const tail = result.output.slice(-2e3).trim();
5090
+ if (tail) {
5091
+ storeBuilderGuidance(
5005
5092
  db,
5006
5093
  exp.id,
5007
- exp.hypothesis ?? exp.slug,
5008
- whyFailed,
5009
- `Approach rejected: ${whyFailed}`,
5010
- exp.sub_type,
5011
- "structural"
5094
+ `Builder was truncated. Last ~2000 chars of output:
5095
+ ${tail}`
5012
5096
  );
5013
- updateExperimentStatus(db, exp.id, "dead_end");
5014
- if (exp.sub_type) {
5015
- incrementSubTypeFailure(db, exp.sub_type, exp.id, "rejected");
5016
- }
5017
- })();
5018
- info(`Experiment ${exp.slug} DEAD-ENDED (rejected). Constraint recorded.`);
5019
- break;
5020
- }
5021
- }
5022
- return overallGrade;
5023
- }
5024
- function gitMerge(branch, cwd) {
5025
- try {
5026
- try {
5027
- (0, import_node_child_process6.execFileSync)("git", ["checkout", "main"], {
5028
- cwd,
5029
- encoding: "utf-8",
5030
- stdio: ["pipe", "pipe", "pipe"]
5031
- });
5032
- } catch {
5033
- (0, import_node_child_process6.execFileSync)("git", ["checkout", "master"], {
5034
- cwd,
5035
- encoding: "utf-8",
5036
- stdio: ["pipe", "pipe", "pipe"]
5037
- });
5038
- }
5039
- (0, import_node_child_process6.execFileSync)("git", ["merge", branch, "--no-ff", "-m", `Merge experiment branch ${branch}`], {
5040
- cwd,
5041
- encoding: "utf-8",
5042
- stdio: ["pipe", "pipe", "pipe"]
5043
- });
5044
- } catch (err) {
5045
- warn(`Git merge of ${branch} failed \u2014 you may need to merge manually.`);
5046
- }
5047
- }
5048
- function gitRevert(branch, cwd) {
5049
- try {
5050
- const currentBranch = (0, import_node_child_process6.execFileSync)("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
5051
- cwd,
5052
- encoding: "utf-8"
5053
- }).trim();
5054
- if (currentBranch === branch) {
5055
- try {
5056
- (0, import_node_child_process6.execFileSync)("git", ["checkout", "--", "."], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5057
- } catch {
5058
5097
  }
5098
+ warn(`Experiment stays at 'building'. Run \`majlis build\` to retry or \`majlis revert\` to abandon.`);
5099
+ }
5100
+ } else {
5101
+ if (config.build?.pre_measure) {
5059
5102
  try {
5060
- (0, import_node_child_process6.execFileSync)("git", ["checkout", "main"], {
5061
- cwd,
5103
+ const [cmd, ...cmdArgs] = config.build.pre_measure.split(/\s+/);
5104
+ (0, import_node_child_process5.execFileSync)(cmd, cmdArgs, {
5105
+ cwd: root,
5062
5106
  encoding: "utf-8",
5107
+ timeout: 3e4,
5063
5108
  stdio: ["pipe", "pipe", "pipe"]
5064
5109
  });
5065
- } catch {
5066
- (0, import_node_child_process6.execFileSync)("git", ["checkout", "master"], {
5067
- cwd,
5110
+ } catch (err) {
5111
+ const errMsg = err instanceof Error ? err.message : String(err);
5112
+ const guidance = `Build verification failed after builder completion. Code may be syntactically broken or incomplete.
5113
+ Error: ${errMsg.slice(0, 500)}`;
5114
+ storeBuilderGuidance(db, exp.id, guidance);
5115
+ warn(`Build verification failed for ${exp.slug}. Staying at 'building'.`);
5116
+ warn(`Guidance stored for retry. Run \`majlis build\` to retry.`);
5117
+ return;
5118
+ }
5119
+ }
5120
+ if (config.metrics?.command) {
5121
+ try {
5122
+ const output = (0, import_node_child_process5.execSync)(config.metrics.command, {
5123
+ cwd: root,
5068
5124
  encoding: "utf-8",
5125
+ timeout: 6e4,
5069
5126
  stdio: ["pipe", "pipe", "pipe"]
5070
- });
5127
+ }).trim();
5128
+ const parsed = parseMetricsOutput(output);
5129
+ for (const m of parsed) {
5130
+ insertMetric(db, exp.id, "after", m.fixture, m.metric_name, m.metric_value);
5131
+ }
5132
+ if (parsed.length > 0) info(`Captured ${parsed.length} post-build metric(s).`);
5133
+ } catch {
5134
+ warn("Could not capture post-build metrics.");
5071
5135
  }
5072
5136
  }
5073
- } catch {
5074
- warn(`Could not switch away from ${branch} \u2014 you may need to do this manually.`);
5075
- }
5076
- }
5077
- function appendToFragilityMap(projectRoot, expSlug, gaps) {
5078
- const fragPath = path12.join(projectRoot, "docs", "synthesis", "fragility.md");
5079
- let content = "";
5080
- if (fs12.existsSync(fragPath)) {
5081
- content = fs12.readFileSync(fragPath, "utf-8");
5137
+ gitCommitBuild(exp, root);
5138
+ if (result.extractionTier === 3) {
5139
+ warn(`Builder output extracted via Haiku (tier 3). Data provenance degraded.`);
5140
+ const existing = getBuilderGuidance(db, exp.id) ?? "";
5141
+ storeBuilderGuidance(
5142
+ db,
5143
+ exp.id,
5144
+ existing + "\n[PROVENANCE WARNING] Builder structured output was reconstructed by a secondary model (tier 3). Treat reported decisions with additional scrutiny."
5145
+ );
5146
+ }
5147
+ updateExperimentStatus(db, exp.id, "built");
5148
+ success(`Build complete for ${exp.slug}. Run \`majlis doubt\` or \`majlis challenge\` next.`);
5082
5149
  }
5083
- const entry = `
5084
- ## From experiment: ${expSlug}
5085
- ${gaps}
5086
- `;
5087
- fs12.writeFileSync(fragPath, content + entry);
5088
5150
  }
5089
- var fs12, path12, import_node_child_process6;
5090
- var init_resolve = __esm({
5091
- "src/resolve.ts"() {
5092
- "use strict";
5093
- fs12 = __toESM(require("fs"));
5094
- path12 = __toESM(require("path"));
5095
- init_types2();
5096
- init_machine();
5097
- init_queries();
5098
- init_metrics();
5099
- init_config();
5100
- init_spawn();
5101
- import_node_child_process6 = require("child_process");
5102
- init_git();
5103
- init_format();
5151
+ async function doChallenge(db, exp, root) {
5152
+ transition(exp.status, "challenged" /* CHALLENGED */);
5153
+ let gitDiff = "";
5154
+ try {
5155
+ gitDiff = (0, import_node_child_process5.execSync)('git diff main -- . ":!.majlis/"', {
5156
+ cwd: root,
5157
+ encoding: "utf-8",
5158
+ stdio: ["pipe", "pipe", "pipe"]
5159
+ }).trim();
5160
+ } catch {
5104
5161
  }
5105
- });
5162
+ if (gitDiff.length > 8e3) gitDiff = gitDiff.slice(0, 8e3) + "\n[DIFF TRUNCATED]";
5163
+ const synthesis = truncateContext(readFileOrEmpty(path11.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
5164
+ let taskPrompt = `Construct adversarial test cases for experiment ${exp.slug}: ${exp.hypothesis}`;
5165
+ if (gitDiff) {
5166
+ taskPrompt += `
5106
5167
 
5107
- // src/commands/cycle.ts
5108
- var cycle_exports = {};
5109
- __export(cycle_exports, {
5110
- cycle: () => cycle,
5111
- resolveCmd: () => resolveCmd,
5112
- runResolve: () => runResolve,
5113
- runStep: () => runStep
5114
- });
5115
- async function cycle(step, args) {
5116
- const root = findProjectRoot();
5117
- if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
5118
- const db = getDb(root);
5119
- const exp = resolveExperimentArg(db, args);
5120
- switch (step) {
5121
- case "build":
5122
- return doBuild(db, exp, root);
5123
- case "challenge":
5124
- return doChallenge(db, exp, root);
5125
- case "doubt":
5126
- return doDoubt(db, exp, root);
5127
- case "scout":
5128
- return doScout(db, exp, root);
5129
- case "verify":
5130
- return doVerify(db, exp, root);
5131
- case "gate":
5132
- return doGate(db, exp, root);
5133
- case "compress":
5134
- return doCompress(db, root);
5168
+ ## Code Changes (git diff main)
5169
+ \`\`\`diff
5170
+ ${gitDiff}
5171
+ \`\`\``;
5135
5172
  }
5136
- }
5137
- async function resolveCmd(args) {
5138
- const root = findProjectRoot();
5139
- if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
5140
- const db = getDb(root);
5141
- const exp = resolveExperimentArg(db, args);
5142
- transition(exp.status, "resolved" /* RESOLVED */);
5143
- await resolve2(db, exp, root);
5144
- }
5145
- async function runStep(step, db, exp, root) {
5146
- switch (step) {
5147
- case "build":
5148
- return doBuild(db, exp, root);
5149
- case "challenge":
5150
- return doChallenge(db, exp, root);
5151
- case "doubt":
5152
- return doDoubt(db, exp, root);
5153
- case "scout":
5154
- return doScout(db, exp, root);
5155
- case "verify":
5156
- return doVerify(db, exp, root);
5157
- case "gate":
5158
- return doGate(db, exp, root);
5159
- case "compress":
5160
- return doCompress(db, root);
5173
+ const result = await spawnAgent("adversary", {
5174
+ experiment: {
5175
+ id: exp.id,
5176
+ slug: exp.slug,
5177
+ hypothesis: exp.hypothesis,
5178
+ status: exp.status,
5179
+ sub_type: exp.sub_type,
5180
+ builder_guidance: null
5181
+ },
5182
+ synthesis,
5183
+ taskPrompt
5184
+ }, root);
5185
+ ingestStructuredOutput(db, exp.id, result.structured);
5186
+ if (result.truncated && !result.structured) {
5187
+ warn(`Adversary was truncated without structured output. Experiment stays at current status.`);
5188
+ } else {
5189
+ updateExperimentStatus(db, exp.id, "challenged");
5190
+ success(`Challenge complete for ${exp.slug}. Run \`majlis doubt\` or \`majlis verify\` next.`);
5161
5191
  }
5162
5192
  }
5163
- async function runResolve(db, exp, root) {
5164
- transition(exp.status, "resolved" /* RESOLVED */);
5165
- await resolve2(db, exp, root);
5166
- }
5167
- async function doGate(db, exp, root) {
5168
- transition(exp.status, "gated" /* GATED */);
5169
- const synthesis = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
5170
- const fragility = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
5171
- const structuralDeadEnds = exp.sub_type ? listStructuralDeadEndsBySubType(db, exp.sub_type) : listStructuralDeadEnds(db);
5172
- const result = await spawnAgent("gatekeeper", {
5193
+ async function doDoubt(db, exp, root) {
5194
+ transition(exp.status, "doubted" /* DOUBTED */);
5195
+ const expDocPath = path11.join(root, expDocRelPath(exp));
5196
+ const experimentDoc = truncateContext(readFileOrEmpty(expDocPath), CONTEXT_LIMITS.experimentDoc);
5197
+ const synthesis = truncateContext(readFileOrEmpty(path11.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
5198
+ const deadEnds = exp.sub_type ? listDeadEndsBySubType(db, exp.sub_type) : listAllDeadEnds(db);
5199
+ let taskPrompt = `Doubt the work in experiment ${exp.slug}: ${exp.hypothesis}. Produce a doubt document with evidence for each doubt.`;
5200
+ if (experimentDoc) {
5201
+ taskPrompt += `
5202
+
5203
+ ## Experiment Document (builder's artifact)
5204
+ <experiment_doc>
5205
+ ${experimentDoc}
5206
+ </experiment_doc>`;
5207
+ }
5208
+ const result = await spawnAgent("critic", {
5173
5209
  experiment: {
5174
5210
  id: exp.id,
5175
5211
  slug: exp.slug,
@@ -5177,391 +5213,66 @@ async function doGate(db, exp, root) {
5177
5213
  status: exp.status,
5178
5214
  sub_type: exp.sub_type,
5179
5215
  builder_guidance: null
5216
+ // Critic does NOT see builder reasoning
5180
5217
  },
5181
- deadEnds: structuralDeadEnds.map((d) => ({
5218
+ synthesis,
5219
+ deadEnds: deadEnds.map((d) => ({
5182
5220
  approach: d.approach,
5183
5221
  why_failed: d.why_failed,
5184
5222
  structural_constraint: d.structural_constraint
5185
5223
  })),
5186
- fragility,
5187
- synthesis,
5188
- taskPrompt: `Gate-check hypothesis for experiment ${exp.slug}:
5189
- "${exp.hypothesis}"
5190
-
5191
- This is a FAST gate \u2014 decide in 1-2 turns. Do NOT read source code or large files. Use the synthesis, dead-ends, and fragility provided in your context. At most, do one targeted grep to verify a function name exists.
5192
-
5193
- Check: (a) stale references \u2014 does the hypothesis reference specific lines, functions, or structures that may not exist? (b) dead-end overlap \u2014 does this hypothesis repeat an approach already ruled out by structural dead-ends? (c) scope \u2014 is this a single focused change, or does it try to do multiple things?
5194
-
5195
- Output your gate_decision as "approve", "reject", or "flag" with reasoning.`
5224
+ taskPrompt
5196
5225
  }, root);
5197
5226
  ingestStructuredOutput(db, exp.id, result.structured);
5198
- const decision = result.structured?.gate_decision ?? "approve";
5199
- const reason = result.structured?.reason ?? "";
5200
- if (decision === "reject") {
5201
- insertDeadEnd(
5202
- db,
5203
- exp.id,
5204
- exp.hypothesis ?? exp.slug,
5205
- reason,
5206
- `Gate rejected: ${reason}`,
5207
- exp.sub_type,
5208
- "procedural"
5209
- );
5210
- adminTransitionAndPersist(db, exp.id, "gated", "dead_end" /* DEAD_END */, "revert");
5211
- warn(`Gate REJECTED for ${exp.slug}: ${reason}. Dead-ended.`);
5212
- return;
5227
+ if (result.truncated && !result.structured) {
5228
+ warn(`Critic was truncated without structured output. Experiment stays at current status.`);
5213
5229
  } else {
5214
- if (decision === "flag") {
5215
- warn(`Gate flagged concerns for ${exp.slug}: ${reason}`);
5216
- }
5217
- updateExperimentStatus(db, exp.id, "gated");
5218
- success(`Gate passed for ${exp.slug}. Run \`majlis build\` next.`);
5230
+ updateExperimentStatus(db, exp.id, "doubted");
5231
+ success(`Doubt pass complete for ${exp.slug}. Run \`majlis challenge\` or \`majlis verify\` next.`);
5219
5232
  }
5220
5233
  }
5221
- async function doBuild(db, exp, root) {
5222
- if (exp.depends_on) {
5223
- const dep = getExperimentBySlug(db, exp.depends_on);
5224
- if (!dep || dep.status !== "merged") {
5225
- throw new Error(
5226
- `Experiment "${exp.slug}" depends on "${exp.depends_on}" which is ${dep ? dep.status : "not found"}. Dependency must be merged before building.`
5227
- );
5228
- }
5229
- }
5230
- transition(exp.status, "building" /* BUILDING */);
5234
+ async function doScout(db, exp, root) {
5235
+ transition(exp.status, "scouted" /* SCOUTED */);
5236
+ const synthesis = truncateContext(readFileOrEmpty(path11.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
5237
+ const fragility = truncateContext(readFileOrEmpty(path11.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
5231
5238
  const deadEnds = exp.sub_type ? listDeadEndsBySubType(db, exp.sub_type) : listAllDeadEnds(db);
5232
- const builderGuidance = getBuilderGuidance(db, exp.id);
5233
- const fragility = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
5234
- const synthesis = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
5235
- const confirmedDoubts = getConfirmedDoubts(db, exp.id);
5236
- const config = loadConfig(root);
5237
- const existingBaseline = getMetricsByExperimentAndPhase(db, exp.id, "before");
5238
- if (config.metrics?.command && existingBaseline.length === 0) {
5239
- try {
5240
- const output = (0, import_node_child_process7.execSync)(config.metrics.command, {
5241
- cwd: root,
5242
- encoding: "utf-8",
5243
- timeout: 6e4,
5244
- stdio: ["pipe", "pipe", "pipe"]
5245
- }).trim();
5246
- const parsed = parseMetricsOutput(output);
5247
- for (const m of parsed) {
5248
- insertMetric(db, exp.id, "before", m.fixture, m.metric_name, m.metric_value);
5249
- }
5250
- if (parsed.length > 0) info(`Captured ${parsed.length} baseline metric(s).`);
5251
- } catch {
5252
- warn("Could not capture baseline metrics.");
5253
- }
5254
- }
5255
- updateExperimentStatus(db, exp.id, "building");
5256
- let taskPrompt = builderGuidance ? `Previous attempt was weak. Here is guidance for this attempt:
5257
- ${builderGuidance}
5239
+ const deadEndsSummary = deadEnds.map(
5240
+ (d) => `- [${d.category ?? "structural"}] ${d.approach}: ${d.why_failed}`
5241
+ ).join("\n");
5242
+ let taskPrompt = `Search for alternative approaches to the problem in experiment ${exp.slug}: ${exp.hypothesis}. Look for contradictory approaches, solutions from other fields, and known limitations of the current approach.`;
5243
+ if (deadEndsSummary) {
5244
+ taskPrompt += `
5258
5245
 
5259
- Build the experiment: ${exp.hypothesis}` : `Build the experiment: ${exp.hypothesis}`;
5260
- if (confirmedDoubts.length > 0) {
5261
- taskPrompt += "\n\n## Confirmed Doubts (MUST address)\nThese weaknesses were confirmed by the verifier. Your build MUST address each one:\n";
5262
- for (const d of confirmedDoubts) {
5263
- taskPrompt += `- [${d.severity}] ${d.claim_doubted}: ${d.evidence_for_doubt}
5264
- `;
5265
- }
5246
+ ## Known Dead Ends (avoid these approaches)
5247
+ ${deadEndsSummary}`;
5266
5248
  }
5267
- taskPrompt += "\n\nNote: The framework captures metrics automatically. Do NOT claim specific numbers unless quoting framework output.";
5268
- const supplementaryContext = loadExperimentContext(exp, root);
5269
- const lineage = exportExperimentLineage(db, exp.sub_type);
5270
- if (lineage) {
5271
- taskPrompt += "\n\n" + lineage;
5249
+ if (fragility) {
5250
+ taskPrompt += `
5251
+
5252
+ ## Fragility Map (target these weak areas)
5253
+ ${fragility}`;
5272
5254
  }
5273
- const result = await spawnAgent("builder", {
5255
+ const result = await spawnAgent("scout", {
5274
5256
  experiment: {
5275
5257
  id: exp.id,
5276
5258
  slug: exp.slug,
5277
5259
  hypothesis: exp.hypothesis,
5278
- status: "building",
5260
+ status: exp.status,
5279
5261
  sub_type: exp.sub_type,
5280
- builder_guidance: builderGuidance
5262
+ builder_guidance: null
5281
5263
  },
5264
+ synthesis,
5265
+ fragility,
5282
5266
  deadEnds: deadEnds.map((d) => ({
5283
5267
  approach: d.approach,
5284
5268
  why_failed: d.why_failed,
5285
5269
  structural_constraint: d.structural_constraint
5286
5270
  })),
5287
- fragility,
5288
- synthesis,
5289
- confirmedDoubts,
5290
- supplementaryContext: supplementaryContext || void 0,
5291
- experimentLineage: lineage || void 0,
5292
5271
  taskPrompt
5293
5272
  }, root);
5294
5273
  ingestStructuredOutput(db, exp.id, result.structured);
5295
- if (result.structured?.abandon) {
5296
- insertDeadEnd(
5297
- db,
5298
- exp.id,
5299
- exp.hypothesis ?? exp.slug,
5300
- result.structured.abandon.reason,
5301
- result.structured.abandon.structural_constraint,
5302
- exp.sub_type,
5303
- "structural"
5304
- );
5305
- adminTransitionAndPersist(db, exp.id, "building", "dead_end" /* DEAD_END */, "revert");
5306
- info(`Builder abandoned ${exp.slug}: ${result.structured.abandon.reason}`);
5307
- return;
5308
- }
5309
- if (result.truncated && !result.structured) {
5310
- warn(`Builder was truncated (hit max turns) without producing structured output.`);
5311
- const recovery = await extractStructuredData("builder", result.output);
5312
- if (recovery.data && !recovery.data.abandon) {
5313
- info(`Recovered structured output from truncated builder (tier ${recovery.tier}).`);
5314
- ingestStructuredOutput(db, exp.id, recovery.data);
5315
- if (config.build?.pre_measure) {
5316
- try {
5317
- const [cmd, ...cmdArgs] = config.build.pre_measure.split(/\s+/);
5318
- (0, import_node_child_process7.execFileSync)(cmd, cmdArgs, {
5319
- cwd: root,
5320
- encoding: "utf-8",
5321
- timeout: 3e4,
5322
- stdio: ["pipe", "pipe", "pipe"]
5323
- });
5324
- } catch (err) {
5325
- const errMsg = err instanceof Error ? err.message : String(err);
5326
- storeBuilderGuidance(
5327
- db,
5328
- exp.id,
5329
- `Build verification failed after truncated recovery.
5330
- Error: ${errMsg.slice(0, 500)}`
5331
- );
5332
- warn(`Build verification failed for ${exp.slug}. Staying at 'building'.`);
5333
- return;
5334
- }
5335
- }
5336
- if (config.metrics?.command) {
5337
- try {
5338
- const output = (0, import_node_child_process7.execSync)(config.metrics.command, {
5339
- cwd: root,
5340
- encoding: "utf-8",
5341
- timeout: 6e4,
5342
- stdio: ["pipe", "pipe", "pipe"]
5343
- }).trim();
5344
- const parsed = parseMetricsOutput(output);
5345
- for (const m of parsed) {
5346
- insertMetric(db, exp.id, "after", m.fixture, m.metric_name, m.metric_value);
5347
- }
5348
- if (parsed.length > 0) info(`Captured ${parsed.length} post-build metric(s).`);
5349
- } catch {
5350
- }
5351
- }
5352
- gitCommitBuild(exp, root);
5353
- if (recovery.tier === 3) {
5354
- warn(`Builder output extracted via Haiku (tier 3). Data provenance degraded.`);
5355
- const existing = getBuilderGuidance(db, exp.id) ?? "";
5356
- storeBuilderGuidance(
5357
- db,
5358
- exp.id,
5359
- existing + "\n[PROVENANCE WARNING] Builder structured output was reconstructed by a secondary model (tier 3). Treat reported decisions with additional scrutiny."
5360
- );
5361
- }
5362
- updateExperimentStatus(db, exp.id, "built");
5363
- success(`Build complete for ${exp.slug} (recovered from truncation). Run \`majlis doubt\` or \`majlis challenge\` next.`);
5364
- } else if (recovery.data?.abandon) {
5365
- insertDeadEnd(
5366
- db,
5367
- exp.id,
5368
- exp.hypothesis ?? exp.slug,
5369
- recovery.data.abandon.reason,
5370
- recovery.data.abandon.structural_constraint,
5371
- exp.sub_type,
5372
- "structural"
5373
- );
5374
- adminTransitionAndPersist(db, exp.id, "building", "dead_end" /* DEAD_END */, "revert");
5375
- info(`Builder abandoned ${exp.slug} (recovered from truncation): ${recovery.data.abandon.reason}`);
5376
- } else {
5377
- const tail = result.output.slice(-2e3).trim();
5378
- if (tail) {
5379
- storeBuilderGuidance(
5380
- db,
5381
- exp.id,
5382
- `Builder was truncated. Last ~2000 chars of output:
5383
- ${tail}`
5384
- );
5385
- }
5386
- warn(`Experiment stays at 'building'. Run \`majlis build\` to retry or \`majlis revert\` to abandon.`);
5387
- }
5388
- } else {
5389
- if (config.build?.pre_measure) {
5390
- try {
5391
- const [cmd, ...cmdArgs] = config.build.pre_measure.split(/\s+/);
5392
- (0, import_node_child_process7.execFileSync)(cmd, cmdArgs, {
5393
- cwd: root,
5394
- encoding: "utf-8",
5395
- timeout: 3e4,
5396
- stdio: ["pipe", "pipe", "pipe"]
5397
- });
5398
- } catch (err) {
5399
- const errMsg = err instanceof Error ? err.message : String(err);
5400
- const guidance = `Build verification failed after builder completion. Code may be syntactically broken or incomplete.
5401
- Error: ${errMsg.slice(0, 500)}`;
5402
- storeBuilderGuidance(db, exp.id, guidance);
5403
- warn(`Build verification failed for ${exp.slug}. Staying at 'building'.`);
5404
- warn(`Guidance stored for retry. Run \`majlis build\` to retry.`);
5405
- return;
5406
- }
5407
- }
5408
- if (config.metrics?.command) {
5409
- try {
5410
- const output = (0, import_node_child_process7.execSync)(config.metrics.command, {
5411
- cwd: root,
5412
- encoding: "utf-8",
5413
- timeout: 6e4,
5414
- stdio: ["pipe", "pipe", "pipe"]
5415
- }).trim();
5416
- const parsed = parseMetricsOutput(output);
5417
- for (const m of parsed) {
5418
- insertMetric(db, exp.id, "after", m.fixture, m.metric_name, m.metric_value);
5419
- }
5420
- if (parsed.length > 0) info(`Captured ${parsed.length} post-build metric(s).`);
5421
- } catch {
5422
- warn("Could not capture post-build metrics.");
5423
- }
5424
- }
5425
- gitCommitBuild(exp, root);
5426
- if (result.extractionTier === 3) {
5427
- warn(`Builder output extracted via Haiku (tier 3). Data provenance degraded.`);
5428
- const existing = getBuilderGuidance(db, exp.id) ?? "";
5429
- storeBuilderGuidance(
5430
- db,
5431
- exp.id,
5432
- existing + "\n[PROVENANCE WARNING] Builder structured output was reconstructed by a secondary model (tier 3). Treat reported decisions with additional scrutiny."
5433
- );
5434
- }
5435
- updateExperimentStatus(db, exp.id, "built");
5436
- success(`Build complete for ${exp.slug}. Run \`majlis doubt\` or \`majlis challenge\` next.`);
5437
- }
5438
- }
5439
- async function doChallenge(db, exp, root) {
5440
- transition(exp.status, "challenged" /* CHALLENGED */);
5441
- let gitDiff = "";
5442
- try {
5443
- gitDiff = (0, import_node_child_process7.execSync)('git diff main -- . ":!.majlis/"', {
5444
- cwd: root,
5445
- encoding: "utf-8",
5446
- stdio: ["pipe", "pipe", "pipe"]
5447
- }).trim();
5448
- } catch {
5449
- }
5450
- if (gitDiff.length > 8e3) gitDiff = gitDiff.slice(0, 8e3) + "\n[DIFF TRUNCATED]";
5451
- const synthesis = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
5452
- let taskPrompt = `Construct adversarial test cases for experiment ${exp.slug}: ${exp.hypothesis}`;
5453
- if (gitDiff) {
5454
- taskPrompt += `
5455
-
5456
- ## Code Changes (git diff main)
5457
- \`\`\`diff
5458
- ${gitDiff}
5459
- \`\`\``;
5460
- }
5461
- const result = await spawnAgent("adversary", {
5462
- experiment: {
5463
- id: exp.id,
5464
- slug: exp.slug,
5465
- hypothesis: exp.hypothesis,
5466
- status: exp.status,
5467
- sub_type: exp.sub_type,
5468
- builder_guidance: null
5469
- },
5470
- synthesis,
5471
- taskPrompt
5472
- }, root);
5473
- ingestStructuredOutput(db, exp.id, result.structured);
5474
- if (result.truncated && !result.structured) {
5475
- warn(`Adversary was truncated without structured output. Experiment stays at current status.`);
5476
- } else {
5477
- updateExperimentStatus(db, exp.id, "challenged");
5478
- success(`Challenge complete for ${exp.slug}. Run \`majlis doubt\` or \`majlis verify\` next.`);
5479
- }
5480
- }
5481
- async function doDoubt(db, exp, root) {
5482
- transition(exp.status, "doubted" /* DOUBTED */);
5483
- const paddedNum = String(exp.id).padStart(3, "0");
5484
- const expDocPath = path13.join(root, "docs", "experiments", `${paddedNum}-${exp.slug}.md`);
5485
- const experimentDoc = truncateContext(readFileOrEmpty(expDocPath), CONTEXT_LIMITS.experimentDoc);
5486
- const synthesis = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
5487
- const deadEnds = exp.sub_type ? listDeadEndsBySubType(db, exp.sub_type) : listAllDeadEnds(db);
5488
- let taskPrompt = `Doubt the work in experiment ${exp.slug}: ${exp.hypothesis}. Produce a doubt document with evidence for each doubt.`;
5489
- if (experimentDoc) {
5490
- taskPrompt += `
5491
-
5492
- ## Experiment Document (builder's artifact)
5493
- <experiment_doc>
5494
- ${experimentDoc}
5495
- </experiment_doc>`;
5496
- }
5497
- const result = await spawnAgent("critic", {
5498
- experiment: {
5499
- id: exp.id,
5500
- slug: exp.slug,
5501
- hypothesis: exp.hypothesis,
5502
- status: exp.status,
5503
- sub_type: exp.sub_type,
5504
- builder_guidance: null
5505
- // Critic does NOT see builder reasoning
5506
- },
5507
- synthesis,
5508
- deadEnds: deadEnds.map((d) => ({
5509
- approach: d.approach,
5510
- why_failed: d.why_failed,
5511
- structural_constraint: d.structural_constraint
5512
- })),
5513
- taskPrompt
5514
- }, root);
5515
- ingestStructuredOutput(db, exp.id, result.structured);
5516
- if (result.truncated && !result.structured) {
5517
- warn(`Critic was truncated without structured output. Experiment stays at current status.`);
5518
- } else {
5519
- updateExperimentStatus(db, exp.id, "doubted");
5520
- success(`Doubt pass complete for ${exp.slug}. Run \`majlis challenge\` or \`majlis verify\` next.`);
5521
- }
5522
- }
5523
- async function doScout(db, exp, root) {
5524
- transition(exp.status, "scouted" /* SCOUTED */);
5525
- const synthesis = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "current.md")), CONTEXT_LIMITS.synthesis);
5526
- const fragility = truncateContext(readFileOrEmpty(path13.join(root, "docs", "synthesis", "fragility.md")), CONTEXT_LIMITS.fragility);
5527
- const deadEnds = exp.sub_type ? listDeadEndsBySubType(db, exp.sub_type) : listAllDeadEnds(db);
5528
- const deadEndsSummary = deadEnds.map(
5529
- (d) => `- [${d.category ?? "structural"}] ${d.approach}: ${d.why_failed}`
5530
- ).join("\n");
5531
- let taskPrompt = `Search for alternative approaches to the problem in experiment ${exp.slug}: ${exp.hypothesis}. Look for contradictory approaches, solutions from other fields, and known limitations of the current approach.`;
5532
- if (deadEndsSummary) {
5533
- taskPrompt += `
5534
-
5535
- ## Known Dead Ends (avoid these approaches)
5536
- ${deadEndsSummary}`;
5537
- }
5538
- if (fragility) {
5539
- taskPrompt += `
5540
-
5541
- ## Fragility Map (target these weak areas)
5542
- ${fragility}`;
5543
- }
5544
- const result = await spawnAgent("scout", {
5545
- experiment: {
5546
- id: exp.id,
5547
- slug: exp.slug,
5548
- hypothesis: exp.hypothesis,
5549
- status: exp.status,
5550
- sub_type: exp.sub_type,
5551
- builder_guidance: null
5552
- },
5553
- synthesis,
5554
- fragility,
5555
- deadEnds: deadEnds.map((d) => ({
5556
- approach: d.approach,
5557
- why_failed: d.why_failed,
5558
- structural_constraint: d.structural_constraint
5559
- })),
5560
- taskPrompt
5561
- }, root);
5562
- ingestStructuredOutput(db, exp.id, result.structured);
5563
- if (result.truncated && !result.structured) {
5564
- warn(`Scout was truncated without structured output. Experiment stays at current status.`);
5274
+ if (result.truncated && !result.structured) {
5275
+ warn(`Scout was truncated without structured output. Experiment stays at current status.`);
5565
5276
  return;
5566
5277
  }
5567
5278
  updateExperimentStatus(db, exp.id, "scouted");
@@ -5570,12 +5281,12 @@ ${fragility}`;
5570
5281
  async function doVerify(db, exp, root) {
5571
5282
  transition(exp.status, "verifying" /* VERIFYING */);
5572
5283
  const doubts = getDoubtsByExperiment(db, exp.id);
5573
- const challengeDir = path13.join(root, "docs", "challenges");
5284
+ const challengeDir = path11.join(root, "docs", "challenges");
5574
5285
  let challenges = "";
5575
- if (fs13.existsSync(challengeDir)) {
5576
- const files = fs13.readdirSync(challengeDir).filter((f) => f.includes(exp.slug) && f.endsWith(".md"));
5286
+ if (fs11.existsSync(challengeDir)) {
5287
+ const files = fs11.readdirSync(challengeDir).filter((f) => f.includes(exp.slug) && f.endsWith(".md"));
5577
5288
  for (const f of files) {
5578
- challenges += fs13.readFileSync(path13.join(challengeDir, f), "utf-8") + "\n\n";
5289
+ challenges += fs11.readFileSync(path11.join(challengeDir, f), "utf-8") + "\n\n";
5579
5290
  }
5580
5291
  }
5581
5292
  const config = loadConfig(root);
@@ -5632,191 +5343,788 @@ async function doVerify(db, exp, root) {
5632
5343
  if (verifierLineage) {
5633
5344
  verifierTaskPrompt += "\n\n" + verifierLineage;
5634
5345
  }
5635
- const builderGuidanceForVerifier = getBuilderGuidance(db, exp.id);
5636
- if (builderGuidanceForVerifier?.includes("[PROVENANCE WARNING]")) {
5637
- verifierTaskPrompt += "\n\nNote: The builder's structured output was reconstructed by a secondary model (tier 3). Treat reported decisions with additional scrutiny.";
5346
+ const builderGuidanceForVerifier = getBuilderGuidance(db, exp.id);
5347
+ if (builderGuidanceForVerifier?.includes("[PROVENANCE WARNING]")) {
5348
+ verifierTaskPrompt += "\n\nNote: The builder's structured output was reconstructed by a secondary model (tier 3). Treat reported decisions with additional scrutiny.";
5349
+ }
5350
+ const result = await spawnAgent("verifier", {
5351
+ experiment: {
5352
+ id: exp.id,
5353
+ slug: exp.slug,
5354
+ hypothesis: exp.hypothesis,
5355
+ status: "verifying",
5356
+ sub_type: exp.sub_type,
5357
+ builder_guidance: null
5358
+ },
5359
+ doubts,
5360
+ challenges,
5361
+ metricComparisons: metricComparisons.length > 0 ? metricComparisons : void 0,
5362
+ supplementaryContext: verifierSupplementaryContext || void 0,
5363
+ experimentLineage: verifierLineage || void 0,
5364
+ taskPrompt: verifierTaskPrompt
5365
+ }, root);
5366
+ ingestStructuredOutput(db, exp.id, result.structured);
5367
+ if (result.truncated && !result.structured) {
5368
+ warn(`Verifier was truncated without structured output. Experiment stays at 'verifying'.`);
5369
+ return;
5370
+ }
5371
+ if (result.structured?.doubt_resolutions) {
5372
+ const knownDoubtIds = new Set(doubts.map((d) => d.id));
5373
+ for (let i = 0; i < result.structured.doubt_resolutions.length; i++) {
5374
+ const dr = result.structured.doubt_resolutions[i];
5375
+ if (!dr.resolution) continue;
5376
+ if (dr.doubt_id && knownDoubtIds.has(dr.doubt_id)) {
5377
+ updateDoubtResolution(db, dr.doubt_id, dr.resolution);
5378
+ } else if (doubts[i]) {
5379
+ warn(`Doubt resolution ID ${dr.doubt_id} not found. Using ordinal fallback \u2192 DOUBT-${doubts[i].id}.`);
5380
+ updateDoubtResolution(db, doubts[i].id, dr.resolution);
5381
+ }
5382
+ }
5383
+ }
5384
+ updateExperimentStatus(db, exp.id, "verified");
5385
+ success(`Verification complete for ${exp.slug}. Run \`majlis resolve\` next.`);
5386
+ }
5387
+ async function doCompress(db, root) {
5388
+ const synthesisPath = path11.join(root, "docs", "synthesis", "current.md");
5389
+ const sizeBefore = fs11.existsSync(synthesisPath) ? fs11.statSync(synthesisPath).size : 0;
5390
+ const sessionCount = getSessionsSinceCompression(db);
5391
+ const dbExport = exportForCompressor(db);
5392
+ const result = await spawnAgent("compressor", {
5393
+ 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."
5394
+ }, root);
5395
+ const sizeAfter = fs11.existsSync(synthesisPath) ? fs11.statSync(synthesisPath).size : 0;
5396
+ recordCompression(db, sessionCount, sizeBefore, sizeAfter);
5397
+ autoCommit(root, "compress: update synthesis");
5398
+ success(`Compression complete. Synthesis: ${sizeBefore}B \u2192 ${sizeAfter}B`);
5399
+ }
5400
+ function gitCommitBuild(exp, cwd) {
5401
+ try {
5402
+ (0, import_node_child_process5.execSync)('git add -A -- ":!.majlis/"', { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5403
+ const diff = (0, import_node_child_process5.execSync)("git diff --cached --stat", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
5404
+ if (!diff) {
5405
+ info("No code changes to commit.");
5406
+ return;
5407
+ }
5408
+ const msg = `EXP-${String(exp.id).padStart(3, "0")}: ${exp.slug}
5409
+
5410
+ ${exp.hypothesis ?? ""}`;
5411
+ (0, import_node_child_process5.execFileSync)("git", ["commit", "-m", msg], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5412
+ info(`Committed builder changes on ${exp.branch}.`);
5413
+ } catch {
5414
+ warn("Could not auto-commit builder changes \u2014 commit manually before resolving.");
5415
+ }
5416
+ }
5417
+ function loadExperimentContext(exp, root) {
5418
+ if (!exp.context_files) return "";
5419
+ let files;
5420
+ try {
5421
+ files = JSON.parse(exp.context_files);
5422
+ } catch {
5423
+ return "";
5424
+ }
5425
+ if (!Array.isArray(files) || files.length === 0) return "";
5426
+ const sections = ["## Experiment-Scoped Reference Material"];
5427
+ for (const relPath of files) {
5428
+ const absPath = path11.join(root, relPath);
5429
+ try {
5430
+ const content = fs11.readFileSync(absPath, "utf-8");
5431
+ sections.push(`### ${relPath}
5432
+ \`\`\`
5433
+ ${content.slice(0, 8e3)}
5434
+ \`\`\``);
5435
+ } catch {
5436
+ sections.push(`### ${relPath}
5437
+ *(file not found)*`);
5438
+ }
5439
+ }
5440
+ return sections.join("\n\n");
5441
+ }
5442
+ function expDocRelPath(exp) {
5443
+ return `docs/experiments/${String(exp.id).padStart(3, "0")}-${exp.slug}.md`;
5444
+ }
5445
+ function resolveExperimentArg(db, args) {
5446
+ const slugArg = args.filter((a) => !a.startsWith("--"))[0];
5447
+ let exp;
5448
+ if (slugArg) {
5449
+ exp = getExperimentBySlug(db, slugArg);
5450
+ if (!exp) throw new Error(`Experiment not found: ${slugArg}`);
5451
+ } else {
5452
+ exp = getLatestExperiment(db);
5453
+ if (!exp) throw new Error('No active experiments. Run `majlis new "hypothesis"` first.');
5454
+ }
5455
+ return exp;
5456
+ }
5457
+ function ingestStructuredOutput(db, experimentId, structured) {
5458
+ if (!structured) return;
5459
+ db.transaction(() => {
5460
+ if (structured.decisions) {
5461
+ for (const d of structured.decisions) {
5462
+ insertDecision(db, experimentId, d.description, d.evidence_level, d.justification);
5463
+ }
5464
+ info(`Ingested ${structured.decisions.length} decision(s)`);
5465
+ }
5466
+ if (structured.grades) {
5467
+ for (const g of structured.grades) {
5468
+ insertVerification(
5469
+ db,
5470
+ experimentId,
5471
+ g.component,
5472
+ g.grade,
5473
+ g.provenance_intact ?? null,
5474
+ g.content_correct ?? null,
5475
+ g.notes ?? null
5476
+ );
5477
+ }
5478
+ info(`Ingested ${structured.grades.length} verification grade(s)`);
5479
+ }
5480
+ if (structured.doubts) {
5481
+ for (const d of structured.doubts) {
5482
+ insertDoubt(
5483
+ db,
5484
+ experimentId,
5485
+ d.claim_doubted,
5486
+ d.evidence_level_of_claim,
5487
+ d.evidence_for_doubt,
5488
+ d.severity
5489
+ );
5490
+ }
5491
+ info(`Ingested ${structured.doubts.length} doubt(s)`);
5492
+ }
5493
+ if (structured.challenges) {
5494
+ for (const c of structured.challenges) {
5495
+ insertChallenge(db, experimentId, c.description, c.reasoning);
5496
+ }
5497
+ info(`Ingested ${structured.challenges.length} challenge(s)`);
5498
+ }
5499
+ if (structured.reframe) {
5500
+ insertReframe(
5501
+ db,
5502
+ experimentId,
5503
+ structured.reframe.decomposition,
5504
+ JSON.stringify(structured.reframe.divergences),
5505
+ structured.reframe.recommendation
5506
+ );
5507
+ info(`Ingested reframe`);
5508
+ }
5509
+ if (structured.findings) {
5510
+ for (const f of structured.findings) {
5511
+ insertFinding(db, experimentId, f.approach, f.source, f.relevance, f.contradicts_current);
5512
+ }
5513
+ info(`Ingested ${structured.findings.length} finding(s)`);
5514
+ }
5515
+ })();
5516
+ }
5517
+ var fs11, path11, import_node_child_process5;
5518
+ var init_cycle = __esm({
5519
+ "src/commands/cycle.ts"() {
5520
+ "use strict";
5521
+ fs11 = __toESM(require("fs"));
5522
+ path11 = __toESM(require("path"));
5523
+ import_node_child_process5 = require("child_process");
5524
+ init_connection();
5525
+ init_queries();
5526
+ init_machine();
5527
+ init_types2();
5528
+ init_spawn();
5529
+ init_parse();
5530
+ init_resolve();
5531
+ init_config();
5532
+ init_metrics();
5533
+ init_git();
5534
+ init_format();
5535
+ }
5536
+ });
5537
+
5538
+ // src/commands/measure.ts
5539
+ var measure_exports = {};
5540
+ __export(measure_exports, {
5541
+ baseline: () => baseline,
5542
+ compare: () => compare,
5543
+ measure: () => measure
5544
+ });
5545
+ async function baseline(args) {
5546
+ await captureMetrics("before", args);
5547
+ }
5548
+ async function measure(args) {
5549
+ await captureMetrics("after", args);
5550
+ }
5551
+ async function captureMetrics(phase, args) {
5552
+ const root = findProjectRoot();
5553
+ if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
5554
+ const db = getDb(root);
5555
+ const config = loadConfig(root);
5556
+ const expIdStr = getFlagValue(args, "--experiment");
5557
+ let exp;
5558
+ if (expIdStr !== void 0) {
5559
+ exp = getExperimentById(db, Number(expIdStr));
5560
+ } else {
5561
+ exp = getLatestExperiment(db);
5562
+ }
5563
+ if (!exp) throw new Error('No active experiment. Run `majlis new "hypothesis"` first.');
5564
+ if (config.build.pre_measure) {
5565
+ info(`Running pre-measure: ${config.build.pre_measure}`);
5566
+ try {
5567
+ (0, import_node_child_process6.execSync)(config.build.pre_measure, { cwd: root, encoding: "utf-8", stdio: "inherit" });
5568
+ } catch {
5569
+ warn("Pre-measure command failed \u2014 continuing anyway.");
5570
+ }
5571
+ }
5572
+ if (!config.metrics.command) {
5573
+ throw new Error("No metrics.command configured in .majlis/config.json");
5574
+ }
5575
+ info(`Running metrics: ${config.metrics.command}`);
5576
+ let metricsOutput;
5577
+ try {
5578
+ metricsOutput = (0, import_node_child_process6.execSync)(config.metrics.command, {
5579
+ cwd: root,
5580
+ encoding: "utf-8",
5581
+ stdio: ["pipe", "pipe", "pipe"]
5582
+ });
5583
+ } catch (err) {
5584
+ throw new Error(`Metrics command failed: ${err instanceof Error ? err.message : String(err)}`);
5585
+ }
5586
+ const parsed = parseMetricsOutput(metricsOutput);
5587
+ if (parsed.length === 0) {
5588
+ warn("Metrics command returned no data.");
5589
+ return;
5590
+ }
5591
+ for (const m of parsed) {
5592
+ insertMetric(db, exp.id, phase, m.fixture, m.metric_name, m.metric_value);
5593
+ }
5594
+ success(`Captured ${parsed.length} metric(s) for ${exp.slug} (phase: ${phase})`);
5595
+ if (config.build.post_measure) {
5596
+ try {
5597
+ (0, import_node_child_process6.execSync)(config.build.post_measure, { cwd: root, encoding: "utf-8", stdio: "inherit" });
5598
+ } catch {
5599
+ warn("Post-measure command failed.");
5600
+ }
5601
+ }
5602
+ }
5603
+ async function compare(args, isJson) {
5604
+ const root = findProjectRoot();
5605
+ if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
5606
+ const db = getDb(root);
5607
+ const config = loadConfig(root);
5608
+ const expIdStr = getFlagValue(args, "--experiment");
5609
+ let exp;
5610
+ if (expIdStr !== void 0) {
5611
+ exp = getExperimentById(db, Number(expIdStr));
5612
+ } else {
5613
+ exp = getLatestExperiment(db);
5614
+ }
5615
+ if (!exp) throw new Error("No active experiment.");
5616
+ const comparisons = compareMetrics(db, exp.id, config);
5617
+ if (comparisons.length === 0) {
5618
+ warn(`No before/after metrics to compare for ${exp.slug}. Run baseline and measure first.`);
5619
+ return;
5620
+ }
5621
+ if (isJson) {
5622
+ console.log(JSON.stringify({ experiment: exp.slug, comparisons }, null, 2));
5623
+ return;
5624
+ }
5625
+ header(`Metric Comparison \u2014 ${exp.slug}`);
5626
+ const regressions = comparisons.filter((c) => c.regression);
5627
+ const rows = comparisons.map((c) => [
5628
+ c.fixture,
5629
+ c.metric,
5630
+ String(c.before),
5631
+ String(c.after),
5632
+ formatDelta(c.delta),
5633
+ c.regression ? red("REGRESSION") : green("OK")
5634
+ ]);
5635
+ console.log(table(["Fixture", "Metric", "Before", "After", "Delta", "Status"], rows));
5636
+ if (regressions.length > 0) {
5637
+ console.log();
5638
+ warn(`${regressions.length} regression(s) detected!`);
5639
+ } else {
5640
+ console.log();
5641
+ success("No regressions detected.");
5642
+ }
5643
+ }
5644
+ function formatDelta(delta) {
5645
+ const prefix = delta > 0 ? "+" : "";
5646
+ return `${prefix}${delta.toFixed(4)}`;
5647
+ }
5648
+ var import_node_child_process6;
5649
+ var init_measure = __esm({
5650
+ "src/commands/measure.ts"() {
5651
+ "use strict";
5652
+ import_node_child_process6 = require("child_process");
5653
+ init_connection();
5654
+ init_queries();
5655
+ init_metrics();
5656
+ init_config();
5657
+ init_format();
5658
+ }
5659
+ });
5660
+
5661
+ // src/commands/experiment.ts
5662
+ var experiment_exports = {};
5663
+ __export(experiment_exports, {
5664
+ newExperiment: () => newExperiment,
5665
+ revert: () => revert
5666
+ });
5667
+ async function newExperiment(args) {
5668
+ const root = findProjectRoot();
5669
+ if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
5670
+ const hypothesis = args.filter((a) => !a.startsWith("--")).join(" ");
5671
+ if (!hypothesis) {
5672
+ throw new Error('Usage: majlis new "hypothesis"');
5673
+ }
5674
+ const db = getDb(root);
5675
+ const config = loadConfig(root);
5676
+ let slug = getFlagValue(args, "--slug") ?? await generateSlug(hypothesis, root);
5677
+ let attempt = 0;
5678
+ while (getExperimentBySlug(db, slug + (attempt ? `-${attempt}` : ""))) {
5679
+ attempt++;
5680
+ }
5681
+ if (attempt > 0) {
5682
+ const original = slug;
5683
+ slug = `${slug}-${attempt}`;
5684
+ info(`Slug "${original}" already exists, using "${slug}"`);
5685
+ }
5686
+ const allExps = db.prepare("SELECT COUNT(*) as count FROM experiments").get();
5687
+ const num = allExps.count + 1;
5688
+ const paddedNum = String(num).padStart(3, "0");
5689
+ const branch = `exp/${paddedNum}-${slug}`;
5690
+ try {
5691
+ (0, import_node_child_process7.execFileSync)("git", ["checkout", "-b", branch], {
5692
+ cwd: root,
5693
+ encoding: "utf-8",
5694
+ stdio: ["pipe", "pipe", "pipe"]
5695
+ });
5696
+ info(`Created branch: ${branch}`);
5697
+ } catch (err) {
5698
+ warn(`Could not create branch ${branch} \u2014 continuing without git branch.`);
5699
+ }
5700
+ const subType = getFlagValue(args, "--sub-type") ?? null;
5701
+ const dependsOn = getFlagValue(args, "--depends-on") ?? null;
5702
+ const contextArg = getFlagValue(args, "--context") ?? null;
5703
+ const contextFiles = contextArg ? contextArg.split(",").map((f) => f.trim()) : null;
5704
+ if (dependsOn) {
5705
+ const depExp = getExperimentBySlug(db, dependsOn);
5706
+ if (!depExp) {
5707
+ throw new Error(`Dependency experiment not found: ${dependsOn}`);
5708
+ }
5709
+ info(`Depends on: ${dependsOn} (status: ${depExp.status})`);
5710
+ }
5711
+ const exp = createExperiment(db, slug, branch, hypothesis, subType, null, dependsOn, contextFiles);
5712
+ if (contextFiles) {
5713
+ info(`Context files: ${contextFiles.join(", ")}`);
5714
+ }
5715
+ success(`Created experiment #${exp.id}: ${exp.slug}`);
5716
+ const docRelPath = expDocRelPath(exp);
5717
+ const docsDir = path12.join(root, "docs", "experiments");
5718
+ const templatePath = path12.join(docsDir, "_TEMPLATE.md");
5719
+ if (fs12.existsSync(templatePath)) {
5720
+ const template = fs12.readFileSync(templatePath, "utf-8");
5721
+ 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]);
5722
+ const logPath = path12.join(root, docRelPath);
5723
+ fs12.writeFileSync(logPath, logContent);
5724
+ info(`Created experiment log: ${docRelPath}`);
5725
+ }
5726
+ autoCommit(root, `new: ${slug}`);
5727
+ if (config.cycle.auto_baseline_on_new_experiment && config.metrics.command) {
5728
+ info("Auto-baselining... (run `majlis baseline` to do this manually)");
5729
+ try {
5730
+ const { baseline: baseline2 } = await Promise.resolve().then(() => (init_measure(), measure_exports));
5731
+ await baseline2(["--experiment", String(exp.id)]);
5732
+ } catch (err) {
5733
+ warn("Auto-baseline failed \u2014 run `majlis baseline` manually.");
5734
+ }
5735
+ }
5736
+ }
5737
+ async function revert(args) {
5738
+ const root = findProjectRoot();
5739
+ if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
5740
+ const db = getDb(root);
5741
+ let exp;
5742
+ const slugArg = args.filter((a) => !a.startsWith("--"))[0];
5743
+ if (slugArg) {
5744
+ exp = getExperimentBySlug(db, slugArg);
5745
+ if (!exp) throw new Error(`Experiment not found: ${slugArg}`);
5746
+ } else {
5747
+ exp = getLatestExperiment(db);
5748
+ if (!exp) throw new Error("No active experiments to revert.");
5749
+ }
5750
+ const reason = getFlagValue(args, "--reason") ?? "Manually reverted";
5751
+ const contextArg = getFlagValue(args, "--context") ?? null;
5752
+ const contextFiles = contextArg ? contextArg.split(",").map((f) => f.trim()) : [];
5753
+ let whyFailed = reason;
5754
+ let structuralConstraint = `Reverted: ${reason}`;
5755
+ let category = args.includes("--structural") ? "structural" : "procedural";
5756
+ try {
5757
+ const gitDiff = getGitDiff(root, exp.branch);
5758
+ const synthesis = truncateContext(
5759
+ readFileOrEmpty(path12.join(root, "docs", "synthesis", "current.md")),
5760
+ CONTEXT_LIMITS.synthesis
5761
+ );
5762
+ const fragility = truncateContext(
5763
+ readFileOrEmpty(path12.join(root, "docs", "synthesis", "fragility.md")),
5764
+ CONTEXT_LIMITS.fragility
5765
+ );
5766
+ const deadEnds = exp.sub_type ? listStructuralDeadEndsBySubType(db, exp.sub_type) : listStructuralDeadEnds(db);
5767
+ let supplementary = "";
5768
+ if (contextFiles.length > 0) {
5769
+ const sections = ["## Artifact Files (pointed to by --context)"];
5770
+ for (const relPath of contextFiles) {
5771
+ const absPath = path12.join(root, relPath);
5772
+ try {
5773
+ const content = fs12.readFileSync(absPath, "utf-8");
5774
+ sections.push(`### ${relPath}
5775
+ \`\`\`
5776
+ ${content.slice(0, 12e3)}
5777
+ \`\`\``);
5778
+ } catch {
5779
+ sections.push(`### ${relPath}
5780
+ *(file not found)*`);
5781
+ }
5782
+ }
5783
+ supplementary = sections.join("\n\n");
5784
+ }
5785
+ let taskPrompt = `Analyze this reverted experiment and produce a structured dead-end record.
5786
+
5787
+ `;
5788
+ taskPrompt += `## Experiment
5789
+ - Slug: ${exp.slug}
5790
+ - Hypothesis: ${exp.hypothesis ?? "(none)"}
5791
+ `;
5792
+ taskPrompt += `- Status at revert: ${exp.status}
5793
+ - Sub-type: ${exp.sub_type ?? "(none)"}
5794
+
5795
+ `;
5796
+ taskPrompt += `## User's Reason for Reverting
5797
+ ${reason}
5798
+
5799
+ `;
5800
+ if (gitDiff) {
5801
+ taskPrompt += `## Git Diff (branch vs main)
5802
+ \`\`\`diff
5803
+ ${gitDiff.slice(0, 15e3)}
5804
+ \`\`\`
5805
+
5806
+ `;
5807
+ }
5808
+ if (supplementary) {
5809
+ taskPrompt += `${supplementary}
5810
+
5811
+ `;
5812
+ }
5813
+ taskPrompt += "Produce a specific structural constraint. Include scope (what this applies to and does NOT apply to).";
5814
+ info("Running post-mortem analysis...");
5815
+ const result = await spawnAgent("postmortem", {
5816
+ experiment: {
5817
+ id: exp.id,
5818
+ slug: exp.slug,
5819
+ hypothesis: exp.hypothesis,
5820
+ status: exp.status,
5821
+ sub_type: exp.sub_type,
5822
+ builder_guidance: null
5823
+ },
5824
+ deadEnds: deadEnds.map((d) => ({
5825
+ approach: d.approach,
5826
+ why_failed: d.why_failed,
5827
+ structural_constraint: d.structural_constraint
5828
+ })),
5829
+ fragility,
5830
+ synthesis,
5831
+ supplementaryContext: supplementary || void 0,
5832
+ taskPrompt
5833
+ }, root);
5834
+ if (result.structured?.postmortem) {
5835
+ const pm = result.structured.postmortem;
5836
+ whyFailed = pm.why_failed;
5837
+ structuralConstraint = pm.structural_constraint;
5838
+ category = pm.category;
5839
+ success("Post-mortem analysis complete.");
5840
+ } else {
5841
+ warn("Post-mortem agent did not produce structured output. Using --reason text.");
5842
+ }
5843
+ } catch (err) {
5844
+ const msg = err instanceof Error ? err.message : String(err);
5845
+ warn(`Post-mortem agent failed: ${msg}. Using --reason text.`);
5846
+ }
5847
+ insertDeadEnd(
5848
+ db,
5849
+ exp.id,
5850
+ exp.hypothesis ?? exp.slug,
5851
+ whyFailed,
5852
+ structuralConstraint,
5853
+ exp.sub_type,
5854
+ category
5855
+ );
5856
+ if (exp.gate_rejection_reason) clearGateRejection(db, exp.id);
5857
+ adminTransitionAndPersist(db, exp.id, exp.status, "dead_end" /* DEAD_END */, "revert");
5858
+ handleDeadEndGit(exp, root);
5859
+ info(`Experiment ${exp.slug} reverted to dead-end.`);
5860
+ info(`Constraint: ${structuralConstraint.slice(0, 120)}${structuralConstraint.length > 120 ? "..." : ""}`);
5861
+ }
5862
+ function getGitDiff(root, branch) {
5863
+ try {
5864
+ return (0, import_node_child_process7.execFileSync)("git", ["diff", `main...${branch}`, "--stat", "--patch"], {
5865
+ cwd: root,
5866
+ encoding: "utf-8",
5867
+ maxBuffer: 1024 * 1024,
5868
+ stdio: ["pipe", "pipe", "pipe"]
5869
+ }).trim();
5870
+ } catch {
5871
+ try {
5872
+ return (0, import_node_child_process7.execFileSync)("git", ["diff", `master...${branch}`, "--stat", "--patch"], {
5873
+ cwd: root,
5874
+ encoding: "utf-8",
5875
+ maxBuffer: 1024 * 1024,
5876
+ stdio: ["pipe", "pipe", "pipe"]
5877
+ }).trim();
5878
+ } catch {
5879
+ return null;
5880
+ }
5881
+ }
5882
+ }
5883
+ var fs12, path12, import_node_child_process7;
5884
+ var init_experiment = __esm({
5885
+ "src/commands/experiment.ts"() {
5886
+ "use strict";
5887
+ fs12 = __toESM(require("fs"));
5888
+ path12 = __toESM(require("path"));
5889
+ import_node_child_process7 = require("child_process");
5890
+ init_connection();
5891
+ init_queries();
5892
+ init_machine();
5893
+ init_types2();
5894
+ init_config();
5895
+ init_spawn();
5896
+ init_git();
5897
+ init_cycle();
5898
+ init_format();
5899
+ }
5900
+ });
5901
+
5902
+ // src/commands/session.ts
5903
+ var session_exports = {};
5904
+ __export(session_exports, {
5905
+ session: () => session
5906
+ });
5907
+ async function session(args) {
5908
+ const subcommand = args[0];
5909
+ if (!subcommand || subcommand !== "start" && subcommand !== "end") {
5910
+ throw new Error('Usage: majlis session start "intent" | majlis session end');
5911
+ }
5912
+ const root = findProjectRoot();
5913
+ if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
5914
+ const db = getDb(root);
5915
+ if (subcommand === "start") {
5916
+ const intent = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
5917
+ if (!intent) {
5918
+ throw new Error('Usage: majlis session start "intent"');
5919
+ }
5920
+ const existing = getActiveSession(db);
5921
+ if (existing) {
5922
+ warn(`Session already active: "${existing.intent}" (started ${existing.started_at})`);
5923
+ warn("End it first with `majlis session end`.");
5924
+ return;
5925
+ }
5926
+ const latestExp = getLatestExperiment(db);
5927
+ const sess = startSession(db, intent, latestExp?.id ?? null);
5928
+ success(`Session started: "${intent}" (id: ${sess.id})`);
5929
+ if (latestExp) {
5930
+ info(`Linked to experiment: ${latestExp.slug} (${latestExp.status})`);
5931
+ }
5932
+ } else {
5933
+ const active = getActiveSession(db);
5934
+ if (!active) {
5935
+ throw new Error("No active session to end.");
5936
+ }
5937
+ const accomplished = getFlagValue(args, "--accomplished") ?? null;
5938
+ const unfinished = getFlagValue(args, "--unfinished") ?? null;
5939
+ const fragility = getFlagValue(args, "--fragility") ?? null;
5940
+ endSession(db, active.id, accomplished, unfinished, fragility);
5941
+ success(`Session ended: "${active.intent}"`);
5942
+ if (accomplished) info(`Accomplished: ${accomplished}`);
5943
+ if (unfinished) info(`Unfinished: ${unfinished}`);
5944
+ if (fragility) warn(`New fragility: ${fragility}`);
5945
+ }
5946
+ }
5947
+ var init_session = __esm({
5948
+ "src/commands/session.ts"() {
5949
+ "use strict";
5950
+ init_connection();
5951
+ init_queries();
5952
+ init_config();
5953
+ init_format();
5954
+ }
5955
+ });
5956
+
5957
+ // src/commands/query.ts
5958
+ var query_exports = {};
5959
+ __export(query_exports, {
5960
+ query: () => query3
5961
+ });
5962
+ async function query3(command, args, isJson) {
5963
+ const root = findProjectRoot();
5964
+ if (!root) throw new Error("Not in a Majlis project. Run `majlis init` first.");
5965
+ const db = getDb(root);
5966
+ switch (command) {
5967
+ case "decisions":
5968
+ return queryDecisions(db, args, isJson);
5969
+ case "dead-ends":
5970
+ return queryDeadEnds(db, args, isJson);
5971
+ case "fragility":
5972
+ return queryFragility(root, isJson);
5973
+ case "history":
5974
+ return queryHistory(db, args, isJson);
5975
+ case "circuit-breakers":
5976
+ return queryCircuitBreakers(db, root, isJson);
5977
+ case "check-commit":
5978
+ return checkCommit(db);
5979
+ }
5980
+ }
5981
+ function queryDecisions(db, args, isJson) {
5982
+ const level = getFlagValue(args, "--level");
5983
+ const expIdStr = getFlagValue(args, "--experiment");
5984
+ const experimentId = expIdStr !== void 0 ? Number(expIdStr) : void 0;
5985
+ const decisions = listAllDecisions(db, level, experimentId);
5986
+ if (isJson) {
5987
+ console.log(JSON.stringify(decisions, null, 2));
5988
+ return;
5989
+ }
5990
+ if (decisions.length === 0) {
5991
+ info("No decisions found.");
5992
+ return;
5993
+ }
5994
+ header("Decisions");
5995
+ const rows = decisions.map((d) => [
5996
+ String(d.id),
5997
+ String(d.experiment_id),
5998
+ evidenceColor(d.evidence_level),
5999
+ d.description.slice(0, 60) + (d.description.length > 60 ? "..." : ""),
6000
+ d.status
6001
+ ]);
6002
+ console.log(table(["ID", "Exp", "Level", "Description", "Status"], rows));
6003
+ }
6004
+ function queryDeadEnds(db, args, isJson) {
6005
+ const subType = getFlagValue(args, "--sub-type");
6006
+ const searchTerm = getFlagValue(args, "--search");
6007
+ let deadEnds;
6008
+ if (subType) {
6009
+ deadEnds = listDeadEndsBySubType(db, subType);
6010
+ } else if (searchTerm) {
6011
+ deadEnds = searchDeadEnds(db, searchTerm);
6012
+ } else {
6013
+ deadEnds = listAllDeadEnds(db);
6014
+ }
6015
+ if (isJson) {
6016
+ console.log(JSON.stringify(deadEnds, null, 2));
6017
+ return;
5638
6018
  }
5639
- const result = await spawnAgent("verifier", {
5640
- experiment: {
5641
- id: exp.id,
5642
- slug: exp.slug,
5643
- hypothesis: exp.hypothesis,
5644
- status: "verifying",
5645
- sub_type: exp.sub_type,
5646
- builder_guidance: null
5647
- },
5648
- doubts,
5649
- challenges,
5650
- metricComparisons: metricComparisons.length > 0 ? metricComparisons : void 0,
5651
- supplementaryContext: verifierSupplementaryContext || void 0,
5652
- experimentLineage: verifierLineage || void 0,
5653
- taskPrompt: verifierTaskPrompt
5654
- }, root);
5655
- ingestStructuredOutput(db, exp.id, result.structured);
5656
- if (result.truncated && !result.structured) {
5657
- warn(`Verifier was truncated without structured output. Experiment stays at 'verifying'.`);
6019
+ if (deadEnds.length === 0) {
6020
+ info("No dead-ends recorded.");
5658
6021
  return;
5659
6022
  }
5660
- if (result.structured?.doubt_resolutions) {
5661
- const knownDoubtIds = new Set(doubts.map((d) => d.id));
5662
- for (let i = 0; i < result.structured.doubt_resolutions.length; i++) {
5663
- const dr = result.structured.doubt_resolutions[i];
5664
- if (!dr.resolution) continue;
5665
- if (dr.doubt_id && knownDoubtIds.has(dr.doubt_id)) {
5666
- updateDoubtResolution(db, dr.doubt_id, dr.resolution);
5667
- } else if (doubts[i]) {
5668
- warn(`Doubt resolution ID ${dr.doubt_id} not found. Using ordinal fallback \u2192 DOUBT-${doubts[i].id}.`);
5669
- updateDoubtResolution(db, doubts[i].id, dr.resolution);
5670
- }
5671
- }
6023
+ header("Dead-End Registry");
6024
+ const rows = deadEnds.map((d) => [
6025
+ String(d.id),
6026
+ d.sub_type ?? "\u2014",
6027
+ d.approach.slice(0, 40) + (d.approach.length > 40 ? "..." : ""),
6028
+ d.structural_constraint.slice(0, 40) + (d.structural_constraint.length > 40 ? "..." : "")
6029
+ ]);
6030
+ console.log(table(["ID", "Sub-Type", "Approach", "Constraint"], rows));
6031
+ }
6032
+ function queryFragility(root, isJson) {
6033
+ const fragPath = path13.join(root, "docs", "synthesis", "fragility.md");
6034
+ if (!fs13.existsSync(fragPath)) {
6035
+ info("No fragility map found.");
6036
+ return;
5672
6037
  }
5673
- updateExperimentStatus(db, exp.id, "verified");
5674
- success(`Verification complete for ${exp.slug}. Run \`majlis resolve\` next.`);
6038
+ const content = fs13.readFileSync(fragPath, "utf-8");
6039
+ if (isJson) {
6040
+ console.log(JSON.stringify({ content }, null, 2));
6041
+ return;
6042
+ }
6043
+ header("Fragility Map");
6044
+ console.log(content);
5675
6045
  }
5676
- async function doCompress(db, root) {
5677
- const synthesisPath = path13.join(root, "docs", "synthesis", "current.md");
5678
- const sizeBefore = fs13.existsSync(synthesisPath) ? fs13.statSync(synthesisPath).size : 0;
5679
- const sessionCount = getSessionsSinceCompression(db);
5680
- const dbExport = exportForCompressor(db);
5681
- const result = await spawnAgent("compressor", {
5682
- 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."
5683
- }, root);
5684
- const sizeAfter = fs13.existsSync(synthesisPath) ? fs13.statSync(synthesisPath).size : 0;
5685
- recordCompression(db, sessionCount, sizeBefore, sizeAfter);
5686
- autoCommit(root, "compress: update synthesis");
5687
- success(`Compression complete. Synthesis: ${sizeBefore}B \u2192 ${sizeAfter}B`);
6046
+ function queryHistory(db, args, isJson) {
6047
+ const fixture = args.filter((a) => !a.startsWith("--"))[0];
6048
+ if (!fixture) {
6049
+ throw new Error("Usage: majlis history <fixture>");
6050
+ }
6051
+ const history = getMetricHistoryByFixture(db, fixture);
6052
+ if (isJson) {
6053
+ console.log(JSON.stringify(history, null, 2));
6054
+ return;
6055
+ }
6056
+ if (history.length === 0) {
6057
+ info(`No metric history for fixture: ${fixture}`);
6058
+ return;
6059
+ }
6060
+ header(`Metric History \u2014 ${fixture}`);
6061
+ const rows = history.map((h) => [
6062
+ String(h.experiment_id),
6063
+ h.experiment_slug ?? "\u2014",
6064
+ h.phase,
6065
+ h.metric_name,
6066
+ String(h.metric_value),
6067
+ h.captured_at
6068
+ ]);
6069
+ console.log(table(["Exp", "Slug", "Phase", "Metric", "Value", "Captured"], rows));
5688
6070
  }
5689
- function gitCommitBuild(exp, cwd) {
5690
- try {
5691
- (0, import_node_child_process7.execSync)('git add -A -- ":!.majlis/"', { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5692
- const diff = (0, import_node_child_process7.execSync)("git diff --cached --stat", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
5693
- if (!diff) {
5694
- info("No code changes to commit.");
5695
- return;
5696
- }
5697
- const msg = `EXP-${String(exp.id).padStart(3, "0")}: ${exp.slug}
5698
-
5699
- ${exp.hypothesis ?? ""}`;
5700
- (0, import_node_child_process7.execFileSync)("git", ["commit", "-m", msg], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5701
- info(`Committed builder changes on ${exp.branch}.`);
5702
- } catch {
5703
- warn("Could not auto-commit builder changes \u2014 commit manually before resolving.");
6071
+ function queryCircuitBreakers(db, root, isJson) {
6072
+ const config = loadConfig(root);
6073
+ const states = getAllCircuitBreakerStates(db, config.cycle.circuit_breaker_threshold);
6074
+ if (isJson) {
6075
+ console.log(JSON.stringify(states, null, 2));
6076
+ return;
6077
+ }
6078
+ if (states.length === 0) {
6079
+ info("No circuit breaker data.");
6080
+ return;
5704
6081
  }
6082
+ header("Circuit Breakers");
6083
+ const rows = states.map((s) => [
6084
+ s.sub_type,
6085
+ String(s.failure_count),
6086
+ String(config.cycle.circuit_breaker_threshold),
6087
+ s.tripped ? red("TRIPPED") : green("OK")
6088
+ ]);
6089
+ console.log(table(["Sub-Type", "Failures", "Threshold", "Status"], rows));
5705
6090
  }
5706
- function loadExperimentContext(exp, root) {
5707
- if (!exp.context_files) return "";
5708
- let files;
6091
+ function checkCommit(db) {
6092
+ let stdinData = "";
5709
6093
  try {
5710
- files = JSON.parse(exp.context_files);
6094
+ stdinData = fs13.readFileSync(0, "utf-8");
5711
6095
  } catch {
5712
- return "";
5713
6096
  }
5714
- if (!Array.isArray(files) || files.length === 0) return "";
5715
- const sections = ["## Experiment-Scoped Reference Material"];
5716
- for (const relPath of files) {
5717
- const absPath = path13.join(root, relPath);
6097
+ if (stdinData) {
5718
6098
  try {
5719
- const content = fs13.readFileSync(absPath, "utf-8");
5720
- sections.push(`### ${relPath}
5721
- \`\`\`
5722
- ${content.slice(0, 8e3)}
5723
- \`\`\``);
6099
+ const hookInput = JSON.parse(stdinData);
6100
+ const command = hookInput?.tool_input?.command ?? "";
6101
+ if (!command.includes("git commit")) {
6102
+ return;
6103
+ }
5724
6104
  } catch {
5725
- sections.push(`### ${relPath}
5726
- *(file not found)*`);
5727
6105
  }
5728
6106
  }
5729
- return sections.join("\n\n");
5730
- }
5731
- function resolveExperimentArg(db, args) {
5732
- const slugArg = args.filter((a) => !a.startsWith("--"))[0];
5733
- let exp;
5734
- if (slugArg) {
5735
- exp = getExperimentBySlug(db, slugArg);
5736
- if (!exp) throw new Error(`Experiment not found: ${slugArg}`);
5737
- } else {
5738
- exp = getLatestExperiment(db);
5739
- if (!exp) throw new Error('No active experiments. Run `majlis new "hypothesis"` first.');
5740
- }
5741
- return exp;
5742
- }
5743
- function ingestStructuredOutput(db, experimentId, structured) {
5744
- if (!structured) return;
5745
- db.transaction(() => {
5746
- if (structured.decisions) {
5747
- for (const d of structured.decisions) {
5748
- insertDecision(db, experimentId, d.description, d.evidence_level, d.justification);
5749
- }
5750
- info(`Ingested ${structured.decisions.length} decision(s)`);
5751
- }
5752
- if (structured.grades) {
5753
- for (const g of structured.grades) {
5754
- insertVerification(
5755
- db,
5756
- experimentId,
5757
- g.component,
5758
- g.grade,
5759
- g.provenance_intact ?? null,
5760
- g.content_correct ?? null,
5761
- g.notes ?? null
5762
- );
5763
- }
5764
- info(`Ingested ${structured.grades.length} verification grade(s)`);
5765
- }
5766
- if (structured.doubts) {
5767
- for (const d of structured.doubts) {
5768
- insertDoubt(
5769
- db,
5770
- experimentId,
5771
- d.claim_doubted,
5772
- d.evidence_level_of_claim,
5773
- d.evidence_for_doubt,
5774
- d.severity
5775
- );
5776
- }
5777
- info(`Ingested ${structured.doubts.length} doubt(s)`);
5778
- }
5779
- if (structured.challenges) {
5780
- for (const c of structured.challenges) {
5781
- insertChallenge(db, experimentId, c.description, c.reasoning);
5782
- }
5783
- info(`Ingested ${structured.challenges.length} challenge(s)`);
5784
- }
5785
- if (structured.reframe) {
5786
- insertReframe(
5787
- db,
5788
- experimentId,
5789
- structured.reframe.decomposition,
5790
- JSON.stringify(structured.reframe.divergences),
5791
- structured.reframe.recommendation
5792
- );
5793
- info(`Ingested reframe`);
5794
- }
5795
- if (structured.findings) {
5796
- for (const f of structured.findings) {
5797
- insertFinding(db, experimentId, f.approach, f.source, f.relevance, f.contradicts_current);
5798
- }
5799
- info(`Ingested ${structured.findings.length} finding(s)`);
6107
+ const active = listActiveExperiments(db);
6108
+ const unverified = active.filter(
6109
+ (e) => !["merged", "dead_end", "verified", "resolved", "compressed"].includes(e.status)
6110
+ );
6111
+ if (unverified.length > 0) {
6112
+ console.error(`[majlis] ${unverified.length} unverified experiment(s):`);
6113
+ for (const e of unverified) {
6114
+ console.error(` - ${e.slug} (${e.status})`);
5800
6115
  }
5801
- })();
6116
+ process.exit(1);
6117
+ }
5802
6118
  }
5803
- var fs13, path13, import_node_child_process7;
5804
- var init_cycle = __esm({
5805
- "src/commands/cycle.ts"() {
6119
+ var fs13, path13;
6120
+ var init_query = __esm({
6121
+ "src/commands/query.ts"() {
5806
6122
  "use strict";
5807
6123
  fs13 = __toESM(require("fs"));
5808
6124
  path13 = __toESM(require("path"));
5809
- import_node_child_process7 = require("child_process");
5810
6125
  init_connection();
5811
6126
  init_queries();
5812
- init_machine();
5813
- init_types2();
5814
- init_spawn();
5815
- init_parse();
5816
- init_resolve();
5817
6127
  init_config();
5818
- init_metrics();
5819
- init_git();
5820
6128
  init_format();
5821
6129
  }
5822
6130
  });
@@ -6011,13 +6319,14 @@ async function next(args, isJson) {
6011
6319
  exp = found;
6012
6320
  }
6013
6321
  const auto = args.includes("--auto");
6322
+ const overrideGate = args.includes("--override-gate");
6014
6323
  if (auto) {
6015
6324
  await runAutoLoop(db, exp, config, root, isJson);
6016
6325
  } else {
6017
- await runNextStep(db, exp, config, root, isJson);
6326
+ await runNextStep(db, exp, config, root, isJson, overrideGate);
6018
6327
  }
6019
6328
  }
6020
- async function runNextStep(db, exp, config, root, isJson) {
6329
+ async function runNextStep(db, exp, config, root, isJson, overrideGate = false) {
6021
6330
  const currentStatus = exp.status;
6022
6331
  const valid = validNext(currentStatus);
6023
6332
  if (valid.length === 0) {
@@ -6040,10 +6349,21 @@ async function runNextStep(db, exp, config, root, isJson) {
6040
6349
  "procedural"
6041
6350
  );
6042
6351
  adminTransitionAndPersist(db, exp.id, exp.status, "dead_end" /* DEAD_END */, "circuit_breaker");
6352
+ handleDeadEndGit(exp, root);
6043
6353
  warn("Experiment dead-ended. Triggering Maqasid Check (purpose audit).");
6044
6354
  await audit([config.project?.objective ?? ""]);
6045
6355
  return;
6046
6356
  }
6357
+ if (exp.status === "gated" && exp.gate_rejection_reason) {
6358
+ if (overrideGate) {
6359
+ clearGateRejection(db, exp.id);
6360
+ info(`Gate override accepted for ${exp.slug}. Proceeding to build.`);
6361
+ } else {
6362
+ warn(`Gate rejected: ${exp.gate_rejection_reason}`);
6363
+ info("Run `majlis next --override-gate` to proceed anyway, or `majlis revert` to abandon.");
6364
+ return;
6365
+ }
6366
+ }
6047
6367
  const sessionsSinceCompression = getSessionsSinceCompression(db);
6048
6368
  if (sessionsSinceCompression >= config.cycle.compression_interval) {
6049
6369
  warn(
@@ -6074,6 +6394,11 @@ async function runAutoLoop(db, exp, config, root, isJson) {
6074
6394
  const freshExp = getExperimentBySlug(db, exp.slug);
6075
6395
  if (!freshExp) break;
6076
6396
  exp = freshExp;
6397
+ if (exp.gate_rejection_reason) {
6398
+ warn(`Gate rejected: ${exp.gate_rejection_reason}`);
6399
+ info("Stopping auto mode. Use `majlis next --override-gate` or `majlis revert`.");
6400
+ break;
6401
+ }
6077
6402
  if (isTerminal(exp.status)) {
6078
6403
  success(`Experiment ${exp.slug} reached terminal state: ${exp.status}`);
6079
6404
  break;
@@ -6090,6 +6415,7 @@ async function runAutoLoop(db, exp, config, root, isJson) {
6090
6415
  "procedural"
6091
6416
  );
6092
6417
  adminTransitionAndPersist(db, exp.id, exp.status, "dead_end" /* DEAD_END */, "circuit_breaker");
6418
+ handleDeadEndGit(exp, root);
6093
6419
  await audit([config.project?.objective ?? ""]);
6094
6420
  break;
6095
6421
  }
@@ -6166,6 +6492,7 @@ var init_next = __esm({
6166
6492
  init_config();
6167
6493
  init_cycle();
6168
6494
  init_audit();
6495
+ init_git();
6169
6496
  init_format();
6170
6497
  }
6171
6498
  });
@@ -6237,6 +6564,28 @@ async function run(args) {
6237
6564
  try {
6238
6565
  await next([exp.slug], false);
6239
6566
  consecutiveFailures = 0;
6567
+ const afterStep = getExperimentBySlug(db, exp.slug);
6568
+ if (afterStep?.gate_rejection_reason) {
6569
+ warn(`Gate rejected in autonomous mode: ${afterStep.gate_rejection_reason}. Dead-ending.`);
6570
+ insertDeadEnd(
6571
+ db,
6572
+ afterStep.id,
6573
+ afterStep.hypothesis ?? afterStep.slug,
6574
+ afterStep.gate_rejection_reason,
6575
+ `Gate rejected: ${afterStep.gate_rejection_reason}`,
6576
+ afterStep.sub_type,
6577
+ "procedural"
6578
+ );
6579
+ adminTransitionAndPersist(
6580
+ db,
6581
+ afterStep.id,
6582
+ afterStep.status,
6583
+ "dead_end" /* DEAD_END */,
6584
+ "revert"
6585
+ );
6586
+ handleDeadEndGit(afterStep, root);
6587
+ continue;
6588
+ }
6240
6589
  } catch (err) {
6241
6590
  consecutiveFailures++;
6242
6591
  const message = err instanceof Error ? err.message : String(err);
@@ -6252,6 +6601,7 @@ async function run(args) {
6252
6601
  "procedural"
6253
6602
  );
6254
6603
  adminTransitionAndPersist(db, exp.id, exp.status, "dead_end" /* DEAD_END */, "error_recovery");
6604
+ handleDeadEndGit(exp, root);
6255
6605
  } catch (innerErr) {
6256
6606
  const innerMsg = innerErr instanceof Error ? innerErr.message : String(innerErr);
6257
6607
  warn(`Could not record dead-end: ${innerMsg}`);
@@ -6391,14 +6741,15 @@ async function createNewExperiment(db, root, hypothesis) {
6391
6741
  const exp = createExperiment(db, finalSlug, branch, hypothesis, null, null);
6392
6742
  adminTransitionAndPersist(db, exp.id, exp.status, "reframed" /* REFRAMED */, "bootstrap");
6393
6743
  exp.status = "reframed";
6744
+ const docRelPath = expDocRelPath(exp);
6394
6745
  const docsDir = path16.join(root, "docs", "experiments");
6395
6746
  const templatePath = path16.join(docsDir, "_TEMPLATE.md");
6396
6747
  if (fs16.existsSync(templatePath)) {
6397
6748
  const template = fs16.readFileSync(templatePath, "utf-8");
6398
6749
  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]);
6399
- const logPath = path16.join(docsDir, `${paddedNum}-${finalSlug}.md`);
6750
+ const logPath = path16.join(root, docRelPath);
6400
6751
  fs16.writeFileSync(logPath, logContent);
6401
- info(`Created experiment log: docs/experiments/${paddedNum}-${finalSlug}.md`);
6752
+ info(`Created experiment log: ${docRelPath}`);
6402
6753
  }
6403
6754
  return exp;
6404
6755
  }
@@ -6686,8 +7037,9 @@ function importExperimentFromWorktree(sourceDb, targetDb, slug) {
6686
7037
  const sourceId = sourceExp.id;
6687
7038
  const insertExp = targetDb.prepare(`
6688
7039
  INSERT INTO experiments (slug, branch, status, classification_ref, sub_type,
6689
- hypothesis, builder_guidance, created_at, updated_at)
6690
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
7040
+ hypothesis, builder_guidance, depends_on, context_files,
7041
+ gate_rejection_reason, created_at, updated_at)
7042
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6691
7043
  `);
6692
7044
  const result = insertExp.run(
6693
7045
  sourceExp.slug,
@@ -6697,6 +7049,9 @@ function importExperimentFromWorktree(sourceDb, targetDb, slug) {
6697
7049
  sourceExp.sub_type,
6698
7050
  sourceExp.hypothesis,
6699
7051
  sourceExp.builder_guidance,
7052
+ sourceExp.depends_on ?? null,
7053
+ sourceExp.context_files ?? null,
7054
+ sourceExp.gate_rejection_reason ?? null,
6700
7055
  sourceExp.created_at,
6701
7056
  sourceExp.updated_at
6702
7057
  );