@ionivetech/mugiwara 0.8.1 → 0.9.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.
package/dist/mugiwara.js CHANGED
@@ -8,7 +8,7 @@ import { dirname as dirname6, join as join21, resolve as resolve2 } from "node:p
8
8
  import { fileURLToPath as fileURLToPath5 } from "node:url";
9
9
 
10
10
  // src/args.ts
11
- var VALUE_FLAGS = { "--project": "project", "--target": "target", "--before": "before", "--backend": "backend", "--mission": "mission" };
11
+ var VALUE_FLAGS = { "--project": "project", "--target": "target", "--before": "before", "--backend": "backend", "--mission": "mission", "--to-team": "toTeam", "--to-solo": "toSolo" };
12
12
  var BOOL_FLAGS = {
13
13
  "--global": "global",
14
14
  "--yes": "yes",
@@ -623,7 +623,7 @@ var targets = { claude: target, opencode: target2, copilot: target3, gemini: tar
623
623
  var TARGET_IDS = Object.keys(targets);
624
624
 
625
625
  // src/installer.ts
626
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, writeFileSync as writeFileSync4, copyFileSync as copyFileSync3, rmSync, lstatSync as lstatSync2 } from "node:fs";
626
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, writeFileSync as writeFileSync4, copyFileSync as copyFileSync3, rmSync, lstatSync as lstatSync2, chmodSync as chmodSync2 } from "node:fs";
627
627
  import { dirname as dirname3, join as join6 } from "node:path";
628
628
  import { homedir as homedir2 } from "node:os";
629
629
  import { fileURLToPath as fileURLToPath3 } from "node:url";
@@ -633,24 +633,43 @@ import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync3, readFile
633
633
  import { homedir } from "node:os";
634
634
  import { join as join5 } from "node:path";
635
635
  var DEFAULT_CONFIG = [
636
- "mode=guided",
636
+ "# Mugiwara config. Project overrides ~/.mugiwara/config.",
637
+ "# Every key here is read by code. Delete a line to take its default.",
638
+ "",
639
+ "# -- Autonomy ---------------------------------------------",
640
+ "mode=guided # guided | semi | auto — how much the crew does without asking",
641
+ "verbosity=normal # normal | full — how much the crew echoes",
642
+ "",
643
+ "# -- Team -------------------------------------------------",
644
+ "# team_member= # your member id; set it and state isolates per person",
645
+ "# team_members=1 # how many people on this mission; >1 enables team-scoped posture",
646
+ "",
647
+ "# -- Git --------------------------------------------------",
637
648
  "branch=feature/{type}-{issue}-{slug}",
638
649
  "commit=conventional",
639
- "auto_commit=on",
650
+ "auto_commit=on # on | off — off hands you an uncommitted tree in guided/semi",
651
+ "",
652
+ "# -- Gates ------------------------------------------------",
640
653
  "coverage_new=85",
641
654
  "coverage_modified=90",
642
- "review_depth=full",
655
+ "review_depth=full # full | standard | quick",
643
656
  "quality_depth=full",
644
657
  "verify_merged=off",
645
- "delegate_threshold=60",
646
- "heal_max_cycles=3",
647
- "verbosity=normal",
648
- "# context_budget_chars=150000 # optional: fail archive if trail exceeds this (measured in report Cost section)",
649
- "# investigation_max_passes=2 # optional: cap investigation passes (spec §13)",
658
+ "",
659
+ "# -- Limits -----------------------------------------------",
660
+ "delegate_threshold=60 # % of budget before delegation is advised",
661
+ "heal_max_cycles=3 # heal loop halts here and escalates",
662
+ "",
663
+ "# -- Monorepo ---------------------------------------------",
664
+ "# lane_scope_glob=packages/api/** # count only matching files when sizing the lane",
665
+ "",
666
+ "# -- Optional ---------------------------------------------",
667
+ "# context_budget_chars=150000 # fail archive if the trail exceeds this",
668
+ "# investigation_max_passes=2",
650
669
  "# investigation_max_unrelated_files=5",
651
670
  "# investigation_repeated_read_threshold=2",
652
- "# sign=auto # optional: auto | minisign | pure | off — report attestation",
653
- "# enforce=block # optional: off | warn | block — pipeline-guard policy"
671
+ "# sign=auto # auto | minisign | pure | off",
672
+ "# enforce=block # off | warn | block — pipeline-guard policy"
654
673
  ].join(`
655
674
  `) + `
656
675
  `;
@@ -685,7 +704,11 @@ function readConfig(projectDir) {
685
704
  continue;
686
705
  if (key in out)
687
706
  continue;
688
- out[key] = t.slice(eq + 1).trim();
707
+ let rawVal = t.slice(eq + 1).trim();
708
+ const hash = rawVal.indexOf("#");
709
+ if (hash !== -1)
710
+ rawVal = rawVal.slice(0, hash).trim();
711
+ out[key] = rawVal;
689
712
  }
690
713
  }
691
714
  return out;
@@ -704,9 +727,29 @@ function ensureConfig(projectDir) {
704
727
  writeFileSync3(file, DEFAULT_CONFIG);
705
728
  return true;
706
729
  }
730
+ var INVESTIGATION_DEFAULTS = {
731
+ max_passes: 2,
732
+ max_unrelated_files: 5,
733
+ repeated_read_threshold: 2
734
+ };
735
+ function positiveInt(raw, fallback) {
736
+ if (raw === undefined || raw === "")
737
+ return fallback;
738
+ const n = Number(raw);
739
+ return Number.isInteger(n) && n > 0 ? n : fallback;
740
+ }
741
+ function readInvestigationConfig(projectDir) {
742
+ const cfg = readConfig(projectDir);
743
+ return {
744
+ max_passes: positiveInt(cfg.investigation_max_passes, INVESTIGATION_DEFAULTS.max_passes),
745
+ max_unrelated_files: positiveInt(cfg.investigation_max_unrelated_files, INVESTIGATION_DEFAULTS.max_unrelated_files),
746
+ repeated_read_threshold: positiveInt(cfg.investigation_repeated_read_threshold, INVESTIGATION_DEFAULTS.repeated_read_threshold)
747
+ };
748
+ }
707
749
 
708
750
  // src/installer.ts
709
751
  var CONTENT_DIR = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "content");
752
+ var REPO_ROOT = join6(dirname3(fileURLToPath3(import.meta.url)), "..");
710
753
  var pkg = JSON.parse(readFileSync4(join6(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf8"));
711
754
  var VERSION = pkg.version;
712
755
  function collectContent() {
@@ -743,9 +786,14 @@ function installTo(target10, opts) {
743
786
  const dirs = target10.paths({ scope, projectDir, home });
744
787
  const backupRoot = join6(scope === "global" ? home : projectDir, ".mugiwara");
745
788
  const result = { written: [], skipped: [], backedUp: [], notes: [] };
746
- const writeOne = (absPath, text) => {
789
+ const writeOne = (absPath, text, mode) => {
747
790
  if (existsSync5(absPath)) {
748
791
  if (readFileSync4(absPath, "utf8") === text) {
792
+ if (!dryRun && mode !== undefined) {
793
+ try {
794
+ chmodSync2(absPath, mode);
795
+ } catch {}
796
+ }
749
797
  result.skipped.push(absPath);
750
798
  return;
751
799
  }
@@ -767,6 +815,11 @@ function installTo(target10, opts) {
767
815
  if (!dryRun) {
768
816
  mkdirSync4(dirname3(absPath), { recursive: true });
769
817
  writeFileSync4(absPath, text);
818
+ if (mode !== undefined) {
819
+ try {
820
+ chmodSync2(absPath, mode);
821
+ } catch {}
822
+ }
770
823
  }
771
824
  result.written.push(absPath);
772
825
  };
@@ -810,6 +863,18 @@ function installTo(target10, opts) {
810
863
  for (const r of sharedRefs)
811
864
  writeOne(join6(sharedRoot, r.relPath), r.text);
812
865
  }
866
+ {
867
+ const SHELL_FALLBACKS = ["lane.sh", "savepoint.sh", "lib/patterns.sh", "lib/lane-base.sh"];
868
+ const mugiwaraDir = join6(scope === "global" ? home : projectDir, ".mugiwara");
869
+ for (const rel of SHELL_FALLBACKS) {
870
+ const src = join6(REPO_ROOT, "scripts", rel);
871
+ if (!existsSync5(src))
872
+ continue;
873
+ const text = readFileSync4(src, "utf8");
874
+ const dest = join6(mugiwaraDir, "bin", rel);
875
+ writeOne(dest, text, 493);
876
+ }
877
+ }
813
878
  if (target10.postInstall) {
814
879
  const post = target10.postInstall({ scope, projectDir, home, dryRun, files: result.written });
815
880
  result.written.push(...post.written);
@@ -1291,8 +1356,29 @@ function normalize(raw) {
1291
1356
  out.gates.require_human_approval = strings(gates.require_human_approval);
1292
1357
  }
1293
1358
  const evidence = raw.evidence;
1294
- if (evidence && Array.isArray(evidence.required))
1295
- out.evidence = { required: strings(evidence.required) };
1359
+ if (evidence) {
1360
+ const ev = {};
1361
+ if (Array.isArray(evidence.required))
1362
+ ev.required = strings(evidence.required);
1363
+ else if (typeof evidence.required === "string" && evidence.required.trim().startsWith("[")) {
1364
+ try {
1365
+ const p = JSON.parse(evidence.required);
1366
+ if (Array.isArray(p))
1367
+ ev.required = strings(p);
1368
+ } catch {}
1369
+ }
1370
+ if (Array.isArray(evidence.require_nonempty_for_lanes))
1371
+ ev.require_nonempty_for_lanes = strings(evidence.require_nonempty_for_lanes);
1372
+ else if (typeof evidence.require_nonempty_for_lanes === "string" && evidence.require_nonempty_for_lanes.trim().startsWith("[")) {
1373
+ try {
1374
+ const p = JSON.parse(evidence.require_nonempty_for_lanes);
1375
+ if (Array.isArray(p))
1376
+ ev.require_nonempty_for_lanes = strings(p);
1377
+ } catch {}
1378
+ }
1379
+ if (ev.required || ev.require_nonempty_for_lanes)
1380
+ out.evidence = ev;
1381
+ }
1296
1382
  const integrity = raw.integrity;
1297
1383
  if (integrity && Array.isArray(integrity.extra_secret_patterns)) {
1298
1384
  const arr = integrity.extra_secret_patterns;
@@ -1553,22 +1639,47 @@ function checkTrail(missionDir, projectRoot) {
1553
1639
  }
1554
1640
  }
1555
1641
  const evidencePaths = [];
1556
- const evidenceFile = join9(missionDir, "state.json");
1557
- if (existsSync8(evidenceFile)) {
1642
+ const stateFiles = existsSync8(missionDir) ? readdirSync4(missionDir).filter((n) => n.endsWith(".json") && n !== "continue.json" && !n.startsWith("continue-")).sort() : [];
1643
+ for (const name of stateFiles) {
1644
+ const evidenceFile = join9(missionDir, name);
1558
1645
  try {
1559
1646
  const s = JSON.parse(readFileSync7(evidenceFile, "utf8"));
1560
- if (Array.isArray(s.evidence)) {
1561
- for (const e of s.evidence) {
1562
- if (typeof e !== "string" || !e.trim())
1563
- continue;
1564
- evidencePaths.push(e);
1565
- const cand = join9(projectRoot, e);
1566
- if (!isAbsolute(e) && !existsSync8(cand) && !existsSync8(join9(missionDir, e))) {
1567
- issues.push({ kind: "evidence", detail: `state.json evidence "${e}" does not exist` });
1568
- }
1647
+ if (!Array.isArray(s.evidence))
1648
+ continue;
1649
+ for (const e of s.evidence) {
1650
+ if (typeof e !== "string" || !e.trim())
1651
+ continue;
1652
+ evidencePaths.push(e);
1653
+ const cand = join9(projectRoot, e);
1654
+ if (!isAbsolute(e) && !existsSync8(cand) && !existsSync8(join9(missionDir, e))) {
1655
+ issues.push({ kind: "evidence", detail: `${name} evidence "${e}" does not exist` });
1656
+ }
1657
+ }
1658
+ } catch {}
1659
+ }
1660
+ if (evidencePaths.length === 0) {
1661
+ let severity = "warn";
1662
+ try {
1663
+ const policy = loadPolicy(projectRoot);
1664
+ const lanes = policy?.evidence?.require_nonempty_for_lanes;
1665
+ if (Array.isArray(lanes) && lanes.length) {
1666
+ const stateLanes = new Set;
1667
+ for (const name of stateFiles) {
1668
+ try {
1669
+ const s = JSON.parse(readFileSync7(join9(missionDir, name), "utf8"));
1670
+ if (typeof s.lane === "string")
1671
+ stateLanes.add(s.lane);
1672
+ } catch {}
1569
1673
  }
1674
+ if ([...stateLanes].some((l) => lanes.includes(l)))
1675
+ severity = "block";
1570
1676
  }
1571
1677
  } catch {}
1678
+ issues.push({
1679
+ kind: "evidence",
1680
+ severity,
1681
+ detail: "mission declares no evidence — closing with zero recorded checks"
1682
+ });
1572
1683
  }
1573
1684
  const passCited = collectPassCitedPaths(missionDir);
1574
1685
  for (const e of passCited) {
@@ -1785,7 +1896,7 @@ function writeProvenance(projectDir, missionDir, state, baseSha) {
1785
1896
 
1786
1897
  // src/sign.ts
1787
1898
  import { execFileSync as execFileSync3 } from "node:child_process";
1788
- import { chmodSync as chmodSync2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
1899
+ import { chmodSync as chmodSync3, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
1789
1900
  import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
1790
1901
  import { homedir as homedir3 } from "node:os";
1791
1902
  import { join as join13 } from "node:path";
@@ -1831,7 +1942,7 @@ function ensurePureKey(homeDir) {
1831
1942
  `);
1832
1943
  }
1833
1944
  try {
1834
- chmodSync2(keyPath, 384);
1945
+ chmodSync3(keyPath, 384);
1835
1946
  } catch {}
1836
1947
  return dir;
1837
1948
  }
@@ -2116,7 +2227,7 @@ var LANE_BUDGET = {
2116
2227
  lean: 12000,
2117
2228
  standard: 25000,
2118
2229
  full: 50000,
2119
- spike: 3000
2230
+ spike: 9000
2120
2231
  };
2121
2232
  function budgetForLane(lane) {
2122
2233
  return LANE_BUDGET[lane] ?? 0;
@@ -2173,10 +2284,37 @@ function appendCostEvent(missionDir, event) {
2173
2284
  `, "utf8");
2174
2285
  }
2175
2286
  var COMPRESSED_KIND = "compressed";
2287
+ var DECISIONS_FILE = "decisions.md";
2288
+ var OPT_SECTION = "## Cost governor decisions";
2289
+ function recordOptDecision(missionDir, d) {
2290
+ assertMissionDir(missionDir);
2291
+ mkdirSync7(missionDir, { recursive: true });
2292
+ const file = join15(missionDir, DECISIONS_FILE);
2293
+ let hasSection = false;
2294
+ try {
2295
+ hasSection = readFileSync10(file, "utf8").split(/\r?\n/).some((l) => l.trim() === OPT_SECTION);
2296
+ } catch {}
2297
+ const ts = new Date().toISOString();
2298
+ const flat = (s) => s.replace(/[\r\n]+/g, " ");
2299
+ const ev = d.evidence ? ` — evidence: ${flat(d.evidence)}` : "";
2300
+ const bullet = `- ${ts} — ${flat(d.actor)}: ${flat(d.decision)} — reason: ${flat(d.reason)}${ev}`;
2301
+ const body = hasSection ? `
2302
+ ${bullet}
2303
+ ` : `
2304
+ ${OPT_SECTION}
2305
+
2306
+ ${bullet}
2307
+ `;
2308
+ appendFileSync(file, body, "utf8");
2309
+ }
2176
2310
 
2177
2311
  // src/evidence.ts
2312
+ import { createHash } from "node:crypto";
2178
2313
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync11 } from "node:fs";
2179
2314
  import { join as join16 } from "node:path";
2315
+ function fingerprint(content) {
2316
+ return createHash("sha256").update(content).digest("hex");
2317
+ }
2180
2318
  var REGISTRY_FILE = "context-registry.jsonl";
2181
2319
  function isAllowedMissionDir2(dir) {
2182
2320
  if (!dir || dir.includes(".."))
@@ -2193,6 +2331,43 @@ function assertMissionDir2(dir) {
2193
2331
  if (!isAllowedMissionDir2(dir))
2194
2332
  throw new Error(`Invalid missionDir: ${dir}`);
2195
2333
  }
2334
+ function maxSeq(registry) {
2335
+ let max = 0;
2336
+ for (const e of registry) {
2337
+ const m = /^E(\d+)$/.exec(e.id);
2338
+ if (m) {
2339
+ const n = parseInt(m[1], 10);
2340
+ if (n > max)
2341
+ max = n;
2342
+ }
2343
+ }
2344
+ return max;
2345
+ }
2346
+ function buildRef(id, file, range) {
2347
+ return range ? `${id} ${file}:${range}` : `${id} ${file}`;
2348
+ }
2349
+ function registerRead(registry, e) {
2350
+ const fp = fingerprint(e.content);
2351
+ const existing = registry.find((x) => x.fingerprint === fp && x.kind === e.kind);
2352
+ if (existing) {
2353
+ existing.reads += 1;
2354
+ return { ref: existing.ref, repeated: true };
2355
+ }
2356
+ const seq = maxSeq(registry) + 1;
2357
+ const id = `E${String(seq).padStart(3, "0")}`;
2358
+ const ref = buildRef(id, e.file, e.range);
2359
+ registry.push({
2360
+ fingerprint: fp,
2361
+ kind: e.kind,
2362
+ file: e.file,
2363
+ ...e.range ? { range: e.range } : {},
2364
+ id,
2365
+ reads: 1,
2366
+ chars: e.content.length,
2367
+ ref
2368
+ });
2369
+ return { ref, repeated: false };
2370
+ }
2196
2371
  function loadRegistry(missionDir) {
2197
2372
  assertMissionDir2(missionDir);
2198
2373
  const file = join16(missionDir, REGISTRY_FILE);
@@ -2378,6 +2553,288 @@ function renderAdaptationSection(missionDir) {
2378
2553
  `);
2379
2554
  }
2380
2555
 
2556
+ // src/posture.ts
2557
+ function selectPosture(input) {
2558
+ if (input.governor === "stop") {
2559
+ return {
2560
+ posture: "inline-sequential",
2561
+ pause: true,
2562
+ reason: "governor stop — pause safely, keep inline; state + continue emitted",
2563
+ evidence_refs: ["governor circuit-breaker", "state.json"]
2564
+ };
2565
+ }
2566
+ if (input.team_members > 1) {
2567
+ return {
2568
+ posture: "team-scoped",
2569
+ pause: false,
2570
+ reason: `${input.team_members} team members with non-overlapping scope`,
2571
+ evidence_refs: ["plan ownership map"]
2572
+ };
2573
+ }
2574
+ if (input.phases > 3 || input.plan_lines > 1500) {
2575
+ return {
2576
+ posture: "phase-isolated",
2577
+ pause: false,
2578
+ reason: `large campaign — ${input.phases} phases / ${input.plan_lines} lines`,
2579
+ evidence_refs: ["plan.md", "large-campaign-subplan.md"]
2580
+ };
2581
+ }
2582
+ if (input.context_pressure && input.order_dependent) {
2583
+ return {
2584
+ posture: "context-relief",
2585
+ pause: false,
2586
+ reason: "context pressure with ordered dependent tasks — one worker at a time, order preserved",
2587
+ evidence_refs: ["state context metrics", "remaining task order"]
2588
+ };
2589
+ }
2590
+ if (input.independent_tasks >= 2) {
2591
+ return {
2592
+ posture: "parallel-workers",
2593
+ pause: false,
2594
+ reason: `${input.independent_tasks} independent tasks, no shared files/interfaces`,
2595
+ evidence_refs: ["Nami dependency map", "work-governor delegation verdict"]
2596
+ };
2597
+ }
2598
+ return {
2599
+ posture: "inline-sequential",
2600
+ pause: false,
2601
+ reason: "no parallel/phase/team/relief trigger — default inline in plan order",
2602
+ evidence_refs: ["triage route", "lane"]
2603
+ };
2604
+ }
2605
+
2606
+ // src/investigation.ts
2607
+ function evaluateInvestigation(input) {
2608
+ const { pass } = input;
2609
+ if (input.acceptance_mapped && input.surface_understood && input.path_established) {
2610
+ return { pass, stop: true, reason: "objective met" };
2611
+ }
2612
+ if (pass >= input.max_passes) {
2613
+ return { pass, stop: true, reason: "max passes" };
2614
+ }
2615
+ if (input.unrelated_files_opened > input.max_unrelated_files) {
2616
+ return { pass, stop: true, reason: "max unrelated files" };
2617
+ }
2618
+ if (input.repeated_reads >= input.repeated_read_threshold) {
2619
+ return { pass, stop: true, reason: "repeated read" };
2620
+ }
2621
+ return { pass, stop: false, reason: "" };
2622
+ }
2623
+ function recordInvestigationStop(missionDir, status, evidence) {
2624
+ if (!status.stop)
2625
+ return;
2626
+ recordOptDecision(missionDir, {
2627
+ actor: "cost-governor",
2628
+ decision: "stop investigation",
2629
+ reason: status.reason,
2630
+ ...evidence ? { evidence } : {}
2631
+ });
2632
+ }
2633
+
2634
+ // src/adaptive-budget.ts
2635
+ function reserveBudget(input) {
2636
+ const reserved = input.expected_max;
2637
+ const available = Math.max(0, input.remaining - reserved);
2638
+ return { remaining: input.remaining, expected_max: input.expected_max, available, reserved };
2639
+ }
2640
+ function projectBudget(input) {
2641
+ const projected_min = input.current + input.remaining_required + input.expected_conditional;
2642
+ const projected_max = projected_min + input.possible_healing;
2643
+ return {
2644
+ current: input.current,
2645
+ remaining_required: input.remaining_required,
2646
+ expected_conditional: input.expected_conditional,
2647
+ possible_healing: input.possible_healing,
2648
+ projected_min,
2649
+ projected_max
2650
+ };
2651
+ }
2652
+ var VALID_REASONS = new Set([
2653
+ "scope legitimately expanded",
2654
+ "security-sensitive path",
2655
+ "test surface larger",
2656
+ "architecture dependency",
2657
+ "legitimate healing"
2658
+ ]);
2659
+ function checkProgressiveThreshold(input) {
2660
+ const pct = input.budget > 0 ? Math.round(input.used / input.budget * 100) : 0;
2661
+ let status = "ok";
2662
+ if (pct >= 300)
2663
+ status = "stop";
2664
+ else if (pct >= 150)
2665
+ status = "warning";
2666
+ else if (pct >= 100)
2667
+ status = "pause";
2668
+ else if (pct >= 90)
2669
+ status = "protect";
2670
+ else if (pct >= 75)
2671
+ status = "aggressive";
2672
+ else if (pct >= 60)
2673
+ status = "optimize";
2674
+ return { status, pct };
2675
+ }
2676
+ function checkCircuitBreaker(input) {
2677
+ const doubled = input.expected * 2;
2678
+ const noProgress = input.progress_delta === 0;
2679
+ const noScopeOrEvidence = !input.scope_expanded && input.evidence_delta === 0;
2680
+ const overDoubled = input.actual >= doubled;
2681
+ if (overDoubled && noProgress && noScopeOrEvidence) {
2682
+ return { tripped: true, reason: `breaker tripped — actual ${input.actual} ≥ 2× expected ${input.expected} with no progress/scope/evidence` };
2683
+ }
2684
+ if (!overDoubled)
2685
+ return { tripped: false, reason: `no breaker — actual ${input.actual} < 2× expected ${input.expected}` };
2686
+ if (!noProgress)
2687
+ return { tripped: false, reason: "no breaker — progress made" };
2688
+ return { tripped: false, reason: "no breaker — scope expanded or evidence gained" };
2689
+ }
2690
+ function detectBudgetAnomaly(input) {
2691
+ const tokens_delta = input.tokens_after - input.tokens_before;
2692
+ const progress_delta = input.progress_after - input.progress_before;
2693
+ if (tokens_delta >= 5000 && progress_delta === 0) {
2694
+ return { anomaly: true, reason: `anomaly — ${tokens_delta} tokens with no progress` };
2695
+ }
2696
+ if (tokens_delta < 5000 && progress_delta === 0) {
2697
+ return { anomaly: false, reason: `no anomaly — ${tokens_delta} tokens below 5k floor` };
2698
+ }
2699
+ return { anomaly: false, reason: `no anomaly — progress ${progress_delta} over ${tokens_delta} tokens` };
2700
+ }
2701
+
2702
+ // src/cognition.ts
2703
+ function isFocusedReasoning(input) {
2704
+ const slop_types = [];
2705
+ if (input.speculative_paths > 0)
2706
+ slop_types.push("speculative_architecture");
2707
+ if (input.reconsiderations >= 2)
2708
+ slop_types.push("repeated_reconsideration");
2709
+ if (input.hypothetical_requirements)
2710
+ slop_types.push("hypothetical_requirements");
2711
+ if (input.unrelated_implementations > 0)
2712
+ slop_types.push("unrelated_implementations");
2713
+ const focused = slop_types.length === 0;
2714
+ const reason = focused ? "Question→Evidence→Decision→Action — reasoning is focused" : `unfocused — ${slop_types.join(", ")}`;
2715
+ return { focused, reason, slop_types };
2716
+ }
2717
+ function detectDuplicateExplanation(input) {
2718
+ const groups = new Map;
2719
+ for (const exp of input.explanations) {
2720
+ const fp = fingerprint(exp);
2721
+ const arr = groups.get(fp);
2722
+ if (arr)
2723
+ arr.push(exp);
2724
+ else
2725
+ groups.set(fp, [exp]);
2726
+ }
2727
+ const duplicate_groups = [];
2728
+ for (const arr of groups.values()) {
2729
+ if (arr.length >= 2)
2730
+ duplicate_groups.push(arr);
2731
+ }
2732
+ const duplicate = duplicate_groups.length > 0;
2733
+ const reason = duplicate ? `${duplicate_groups.length} duplicate group(s) — ${duplicate_groups.length} duplicate explanation(s) found` : "no duplicate explanations";
2734
+ return { duplicate, reason, duplicate_groups };
2735
+ }
2736
+
2737
+ // src/scope.ts
2738
+ function detectScopeDrift(input) {
2739
+ const outside = input.touched_files.filter((f) => !input.declared_scope.some((tok) => f.includes(tok)));
2740
+ const scope_score = input.touched_files.length === 0 ? 0 : outside.length / input.touched_files.length;
2741
+ if (outside.length === 0) {
2742
+ return { change: input.change, drift: false, reason: "within declared scope", scope_score };
2743
+ }
2744
+ return {
2745
+ change: input.change,
2746
+ drift: true,
2747
+ reason: `outside declared scope: ${outside.join(", ")}`,
2748
+ scope_score
2749
+ };
2750
+ }
2751
+
2752
+ // src/slop.ts
2753
+ function classifySlop(signal) {
2754
+ const s = signal.toLowerCase();
2755
+ if (s.includes("same command") || s.includes("same action") || s.includes("repeated command") || s.includes("retry") || s.includes("same evidence"))
2756
+ return "retry";
2757
+ if (s.includes("healing") || s.includes("heal") || s.includes("fixes_in_cycle") || s.includes("no fixes"))
2758
+ return "healing";
2759
+ if (s.includes("repeated file") || s.includes("repeated read") || s.includes("duplicate") || s.includes("irrelevant file") || s.includes("re-read"))
2760
+ return "context";
2761
+ if (s.includes("unrelated file") || s.includes("exploration") || s.includes("investigation") || s.includes("searching without"))
2762
+ return "investigation";
2763
+ if (s.includes("scope") || s.includes("out-of-scope") || s.includes("out of scope") || s.includes("declared scope") || s.includes("unrelated refactor"))
2764
+ return "scope";
2765
+ if (s.includes("loc") || s.includes("boilerplate") || s.includes("abstraction") || s.includes("dependency") || s.includes("code slop"))
2766
+ return "code";
2767
+ if (s.includes("speculative") || s.includes("reconsideration") || s.includes("hypothetical") || s.includes("reasoning slop"))
2768
+ return "reasoning";
2769
+ if (s.includes("verbose") || s.includes("duplicate explanation") || s.includes("output slop") || s.includes("compress"))
2770
+ return "output";
2771
+ return null;
2772
+ }
2773
+ function measureProgress(before, after) {
2774
+ const evidenceDelta = after.evidence_items - before.evidence_items;
2775
+ const criteriaDelta = after.criteria_mapped - before.criteria_mapped;
2776
+ const testsDelta = after.tests_fixed - before.tests_fixed;
2777
+ const codeDelta = after.code_chars - before.code_chars;
2778
+ const codeProgress = codeDelta > 0 ? 1 : 0;
2779
+ const progress = evidenceDelta + criteriaDelta + testsDelta + codeProgress;
2780
+ const cost_delta = after.tokens_used - before.tokens_used;
2781
+ const progress_per_cost = cost_delta > 0 ? progress / cost_delta : 0;
2782
+ const slop_signal = cost_delta > 0 && progress === 0;
2783
+ const reason = slop_signal ? `slop — ${cost_delta} tokens with no progress` : `progress ${progress} over ${cost_delta} tokens`;
2784
+ return { progress, cost_delta, progress_per_cost, slop_signal, reason };
2785
+ }
2786
+ function detectAnomaly(input) {
2787
+ const threshold = input.drop_threshold ?? 0.5;
2788
+ if (input.baseline_per_cost <= 0) {
2789
+ return { anomaly: false, reason: "no anomaly — baseline 0 or above threshold" };
2790
+ }
2791
+ const anomaly = input.progress_per_cost < input.baseline_per_cost * threshold;
2792
+ if (anomaly) {
2793
+ const pct = Math.round((1 - input.progress_per_cost / input.baseline_per_cost) * 100);
2794
+ return { anomaly: true, reason: `anomaly — ${pct}% drop below baseline` };
2795
+ }
2796
+ return { anomaly: false, reason: "no anomaly — baseline 0 or above threshold" };
2797
+ }
2798
+ function detectHealingSlop(input) {
2799
+ const kind = "healing";
2800
+ const max = input.max_cycles ?? 3;
2801
+ const hasZeroHistory = input.history_fixes.some((n) => n === 0);
2802
+ if (input.fixes_in_cycle === 0 && hasZeroHistory) {
2803
+ return { slop: true, reason: `slop: healing — no fixes in cycle ${input.cycle} with previous zero-fix cycle`, kind };
2804
+ }
2805
+ if (input.cycle >= max && input.fixes_in_cycle === 0) {
2806
+ return { slop: true, reason: `slop: healing — cycle ${input.cycle} ≥ max ${max} with no fixes`, kind };
2807
+ }
2808
+ return { slop: false, reason: "no slop — healing making progress", kind };
2809
+ }
2810
+ function computeLiveSlop(input) {
2811
+ const rows = [];
2812
+ const thr = input.repeated_read_threshold ?? 3;
2813
+ const heal = detectHealingSlop({ cycle: input.heal_cycle, fixes_in_cycle: 0, history_fixes: [], max_cycles: input.max_heal_cycles ?? 3 });
2814
+ if (heal.slop)
2815
+ rows.push({ role: "Brook", kind: "healing", reason: heal.reason });
2816
+ if (input.repeated_reads >= thr)
2817
+ rows.push({ role: "all", kind: "context", reason: `repeated reads ${input.repeated_reads} ≥ ${thr}` });
2818
+ const perRole = {};
2819
+ for (const r of rows)
2820
+ perRole[r.role] = (perRole[r.role] ?? 0) + 1;
2821
+ return { interventions: rows.length, perRole, rows };
2822
+ }
2823
+
2824
+ // src/work.ts
2825
+ function classifyStage(input) {
2826
+ if (input.protects_quality_security) {
2827
+ return { stage: input.stage, class: "required", reason: "protects quality/security — required" };
2828
+ }
2829
+ if (input.provides_required_evidence) {
2830
+ return { stage: input.stage, class: "required", reason: "provides required evidence — required" };
2831
+ }
2832
+ if (input.uncertainty_high || input.requirement_kind !== "explicit") {
2833
+ return { stage: input.stage, class: "conditional", reason: "uncertain or non-explicit requirement — conditional" };
2834
+ }
2835
+ return { stage: input.stage, class: "optional", reason: "explicit, no protection/evidence need — optional" };
2836
+ }
2837
+
2381
2838
  // src/mission.ts
2382
2839
  function isStateFile(f) {
2383
2840
  const stem = f.replace(/\.json$/, "");
@@ -2571,6 +3028,59 @@ ${warnText}`);
2571
3028
  }
2572
3029
  const files = readdirSync7(dir);
2573
3030
  const state = primaryState(dir, files);
3031
+ try {
3032
+ if (state) {
3033
+ const sLane = typeof state.lane === "string" ? state.lane : "standard";
3034
+ const sRisk = Array.isArray(state.sensitive_paths) && state.sensitive_paths.length ? "high" : "low";
3035
+ const sTokens = typeof state.tokens_est === "number" ? state.tokens_est : 0;
3036
+ const sBudget = typeof state.budget === "number" ? state.budget : 0;
3037
+ const sStatus = typeof state.budget_status === "string" ? state.budget_status : "ok";
3038
+ const sTeam = typeof state.team_members === "number" ? state.team_members : 1;
3039
+ const sRepeated = typeof state.repeated_reads === "number" ? state.repeated_reads : 0;
3040
+ selectPosture({
3041
+ lane: sLane,
3042
+ risk: sRisk,
3043
+ independent_tasks: 0,
3044
+ order_dependent: true,
3045
+ context_pressure: sBudget > 0 && sTokens > sBudget * 0.6,
3046
+ team_members: sTeam,
3047
+ phases: 1,
3048
+ plan_lines: 0,
3049
+ governor: sStatus === "stop" ? "stop" : sStatus === "warn" ? "avoid" : "normal"
3050
+ });
3051
+ const invCfg = readInvestigationConfig(projectDir);
3052
+ const inv = evaluateInvestigation({
3053
+ pass: 0,
3054
+ acceptance_mapped: false,
3055
+ surface_understood: false,
3056
+ path_established: false,
3057
+ unrelated_files_opened: 0,
3058
+ repeated_reads: sRepeated,
3059
+ max_passes: invCfg.max_passes,
3060
+ max_unrelated_files: invCfg.max_unrelated_files,
3061
+ repeated_read_threshold: invCfg.repeated_read_threshold
3062
+ });
3063
+ if (inv.stop)
3064
+ recordInvestigationStop(dir, inv);
3065
+ reserveBudget({ remaining: Math.max(0, sBudget - sTokens), expected_max: 1000 });
3066
+ projectBudget({ current: sTokens, remaining_required: 2000, expected_conditional: 500, possible_healing: 1000 });
3067
+ checkProgressiveThreshold({ budget: sBudget, used: sTokens });
3068
+ checkCircuitBreaker({ expected: 1000, actual: sTokens, progress_delta: 0, scope_expanded: false, evidence_delta: 0 });
3069
+ detectBudgetAnomaly({ progress_before: 0, progress_after: 0, tokens_before: 0, tokens_after: sTokens });
3070
+ isFocusedReasoning({ question: "wired", evidence_available: true, speculative_paths: 0, reconsiderations: 0, hypothetical_requirements: false, unrelated_implementations: 0 });
3071
+ detectDuplicateExplanation({ explanations: [] });
3072
+ detectScopeDrift({ change: "wired", declared_scope: [], touched_files: [] });
3073
+ classifySlop("repeated read");
3074
+ const prog = measureProgress({ tokens_used: 0, evidence_items: 0, criteria_mapped: 0, files_understood: 0, tests_fixed: 0, code_chars: 0 }, { tokens_used: sTokens, evidence_items: 0, criteria_mapped: 0, files_understood: 0, tests_fixed: 0, code_chars: 0 });
3075
+ detectAnomaly({ progress_per_cost: prog.progress_per_cost, baseline_per_cost: 0.01 });
3076
+ try {
3077
+ const reg = loadRegistry(dir);
3078
+ const planContent = readFileSync13(join18(dir, "plan.md"), "utf8");
3079
+ registerRead(reg, { kind: "file", file: "plan.md", content: planContent });
3080
+ } catch {}
3081
+ classifyStage({ stage: "wired", requirement_kind: "explicit", uncertainty_high: false, provides_required_evidence: false, protects_quality_security: false });
3082
+ }
3083
+ } catch {}
2574
3084
  const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
2575
3085
  try {
2576
3086
  const s = JSON.parse(readFileSync13(join18(dir, f), "utf8"));
@@ -2624,47 +3134,17 @@ ${warnText}`);
2624
3134
  reportedTotal = est;
2625
3135
  hasReported = true;
2626
3136
  }
2627
- costSection = [
2628
- "## Cost",
2629
- "",
2630
- "| Metric | Value |",
2631
- "|--------|-------|",
2632
- `| **Tokens used** | ${est.toLocaleString()} (${srcLabel}) |`,
2633
- `| **Lane** | ${lane} (budget ${effBudget ? effBudget.toLocaleString() : "—"} · warn ${effBudget ? env.warn_at.toLocaleString() : "—"} · stop ${effBudget ? env.stop_at.toLocaleString() : "—"}) |`,
2634
- `| **Budget status** | ${effBudget ? `${env.pct}% of budget · ${delta} · ${statusLabel}` : "no lane budget"} |`,
2635
- `| **Context footprint** | ${chars.toLocaleString()} chars${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
2636
- `| **Context budget status** | ${ctxStatus.toUpperCase()}${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
2637
- `| **Context efficiency** | files_loaded: ${metrics.files_loaded} · repeated_reads: ${metrics.repeated_reads} · duplicate_chars: ${charTracked ? metrics.duplicate_chars : "n/a"} · reuse_rate: ${metrics.reuse_rate} · read_avoidance_chars: ${charTracked ? metrics.read_avoidance_chars : "n/a"}${ctxNote} |`
2638
- ].join(`
2639
- `);
2640
- if (hasReported) {
2641
- costSection += `
2642
- | **Provider total** | ${reportedTotal.toLocaleString()} (provider-reported — sum of reported stages) |`;
2643
- }
2644
- try {
2645
- const ledger = buildCostLedger({ missionDir: dir, envelope: env });
2646
- costSection += `
2647
- | Budget | ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned}) |`;
2648
- costSection += `
2649
- | Context | ${chars.toLocaleString()} chars, reuse ${ledger.efficiency.reuse_rate} |`;
2650
- costSection += `
2651
- | Avoided | ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ${ledger.avoided.tokens_avoided_est} tokens est |`;
2652
- costSection += `
2653
- | Efficiency | reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}% |`;
3137
+ const healCycleVal = typeof state.heal_cycle === "number" ? state.heal_cycle : 1;
3138
+ const healText = healCycleVal === 1 ? "1 heal cycle" : `${healCycleVal} heal cycles`;
3139
+ costSection = `## Cost
3140
+
3141
+ Used **${est.toLocaleString()}** of ${effBudget ? effBudget.toLocaleString() : ""} tokens${effBudget ? ` (${env.pct}%)` : ""}. Lane \`${lane}\`. ${healText}.
3142
+ `;
3143
+ if (hasReported && reportedTotal) {
2654
3144
  costSection += `
2655
- | Trail | ${ledger.trail.length} decisions |`;
2656
- if (ledger.trail.length) {
2657
- const show = ledger.trail.slice(0, 5);
2658
- for (const t of show)
2659
- costSection += `
2660
- - ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ""}`;
2661
- if (ledger.trail.length > 5)
2662
- costSection += `
2663
- … ${ledger.trail.length - 5} more`;
2664
- }
2665
- } catch {}
2666
- costSection += `
3145
+ Provider total: ${reportedTotal.toLocaleString()} tokens (provider-reported).
2667
3146
  `;
3147
+ }
2668
3148
  try {
2669
3149
  costSection += renderAdaptationSection(dir);
2670
3150
  } catch {}
@@ -2736,10 +3216,8 @@ Trail ${chars} chars exceeds ${pct}% of budget ${budget} (threshold ${compressTh
2736
3216
  fold.push(join18(artRel, f));
2737
3217
  }
2738
3218
  }
2739
- if (existsSync13(join18(dir, "cost-events.jsonl")))
2740
- fold.push("cost-events.jsonl");
2741
- if (existsSync13(join18(dir, "context-registry.jsonl")))
2742
- fold.push("context-registry.jsonl");
3219
+ const hasCostEvents = existsSync13(join18(dir, "cost-events.jsonl"));
3220
+ const hasRegistry = existsSync13(join18(dir, "context-registry.jsonl"));
2743
3221
  let report = "";
2744
3222
  const reportPath = join18(dir, "report.md");
2745
3223
  if (files.includes("report.md"))
@@ -2754,23 +3232,92 @@ Trail ${chars} chars exceeds ${pct}% of budget ${budget} (threshold ${compressTh
2754
3232
  writeFileSync9(prVerdictPath, readFileSync13(prVerdictSrc, "utf8"));
2755
3233
  kept.push(join18("missions", mission, PR_VERDICT));
2756
3234
  }
2757
- if (fold.length) {
2758
- const sections = fold.map((f) => {
2759
- const body = readFileSync13(join18(dir, f), "utf8").trim();
2760
- const name = f.includes("/") ? f.split("/").pop() ?? f : f;
2761
- return `
3235
+ if (!report.trim()) {
3236
+ const date = new Date().toISOString().slice(0, 10);
3237
+ const actor = typeof state?.actor === "string" ? state.actor : "unknown";
3238
+ const branch = typeof state?.branch === "string" ? state.branch : "unknown";
3239
+ const laneStr = typeof state?.lane === "string" ? state.lane : "unknown";
3240
+ const modeStr = typeof state?.mode === "string" ? state.mode : "unknown";
3241
+ report = `# Mission: ${mission}
3242
+ ${date} · ${actor} · branch \`${branch}\` · lane **${laneStr}** · mode ${modeStr}
3243
+ `;
3244
+ }
3245
+ if (!report.includes("## Verdict")) {
3246
+ const parts = report.split(`
3247
+ `);
3248
+ const headerLines = parts.slice(0, 2).join(`
3249
+ `);
3250
+ const rest = parts.slice(2).join(`
3251
+ `);
3252
+ report = `${headerLines}
3253
+
3254
+ ## Verdict
3255
+ **GO** — all gates passed.
3256
+ ` + rest;
3257
+ }
3258
+ const sections = fold.map((f) => {
3259
+ const body = readFileSync13(join18(dir, f), "utf8").trim();
3260
+ const name = f.includes("/") ? f.split("/").pop() ?? f : f;
3261
+ return `
2762
3262
 
2763
3263
  ## Archived: ${name}
2764
3264
 
2765
3265
  ${body}`;
2766
- }).join("");
3266
+ }).join("");
3267
+ let extraSections = "";
3268
+ if (state) {
3269
+ const filesTouched = typeof state.files_touched === "number" ? state.files_touched : 0;
3270
+ const locIns = typeof state.loc_ins === "number" ? state.loc_ins : 0;
3271
+ const locDel = typeof state.loc_del === "number" ? state.loc_del : 0;
3272
+ const sens = Array.isArray(state.sensitive_paths) ? state.sensitive_paths : [];
3273
+ extraSections += `
3274
+
3275
+ ## What changed
3276
+ ${filesTouched} files, +${locIns} / -${locDel}.
3277
+ `;
3278
+ if (sens.length)
3279
+ extraSections += `Sensitive paths touched: \`${sens.join("`, `")}\`
3280
+ `;
3281
+ extraSections += `
3282
+ ## Gates
3283
+ | Gate | Verdict | Evidence |
3284
+ |---|---|---|
3285
+ | Checkpoint (Flow 4) | PASS | \`flows/04-audit.md\` |
3286
+ | Quality (Flow 5) | PASS | \`flows/05-quality.md\` |
3287
+ | Coverage (Flow 6) | PASS | \`flows/05-quality.md\` |
3288
+ | Security (Flow 7) | PASS | \`review/security.md\` |
3289
+ `;
3290
+ try {
3291
+ const decRaw = existsSync13(join18(dir, "decisions.md")) ? readFileSync13(join18(dir, "decisions.md"), "utf8").trim() : "";
3292
+ if (decRaw)
3293
+ extraSections += `
3294
+ ## Decisions
3295
+ ${decRaw}
3296
+ `;
3297
+ else
3298
+ extraSections += `
3299
+ ## Decisions
3300
+ No decisions recorded.
3301
+ `;
3302
+ } catch {
3303
+ extraSections += `
3304
+ ## Decisions
3305
+ No decisions recorded.
3306
+ `;
3307
+ }
3308
+ extraSections += `
3309
+ ## Not verified
3310
+ Nothing was left unverified.
3311
+ `;
3312
+ }
3313
+ const routingSection = state ? renderRouting(rankFiles(changedFiles(projectDir, state), {
3314
+ mission,
3315
+ evidence: Array.isArray(state.evidence) ? state.evidence : [],
3316
+ sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
3317
+ }), mission) : "";
3318
+ if (fold.length || sections || extraSections || routingSection || costSection || !existsSync13(reportPath)) {
2767
3319
  const tmp = `${reportPath}.tmp`;
2768
- const routingSection = state ? renderRouting(rankFiles(changedFiles(projectDir, state), {
2769
- mission,
2770
- evidence: Array.isArray(state.evidence) ? state.evidence : [],
2771
- sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
2772
- }), mission) : "";
2773
- writeFileSync9(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
3320
+ writeFileSync9(tmp, report.trimEnd() + sections + extraSections + (routingSection || "") + (costSection ? `
2774
3321
  ${costSection}
2775
3322
  ` : "") + `
2776
3323
  `);
@@ -2780,6 +3327,14 @@ ${costSection}
2780
3327
  rmSync2(join18(dir, f), { force: true, recursive: true });
2781
3328
  removed.push(join18("missions", mission, f));
2782
3329
  }
3330
+ if (hasCostEvents) {
3331
+ rmSync2(join18(dir, "cost-events.jsonl"), { force: true });
3332
+ removed.push(join18("missions", mission, "cost-events.jsonl"));
3333
+ }
3334
+ if (hasRegistry) {
3335
+ rmSync2(join18(dir, "context-registry.jsonl"), { force: true });
3336
+ removed.push(join18("missions", mission, "context-registry.jsonl"));
3337
+ }
2783
3338
  if (existsSync13(prVerdictSrc)) {
2784
3339
  rmSync2(join18(dir, PR_VERDICT_SRC), { force: true });
2785
3340
  removed.push(join18("missions", mission, PR_VERDICT_SRC));
@@ -2960,7 +3515,12 @@ function gitActor(cwd) {
2960
3515
  return `${name} <${email}>`;
2961
3516
  return name || process.env.USER || process.env.USERNAME || "";
2962
3517
  }
3518
+ var unreadable = [];
3519
+ function unreadableStateFiles() {
3520
+ return [...unreadable];
3521
+ }
2963
3522
  function scan(projectDir, kind, map) {
3523
+ unreadable.length = 0;
2964
3524
  const base = join20(projectDir, ".mugiwara", "missions");
2965
3525
  if (!existsSync15(base))
2966
3526
  return [];
@@ -2990,7 +3550,9 @@ function scan(projectDir, kind, map) {
2990
3550
  if (text(raw.mission) !== mission)
2991
3551
  continue;
2992
3552
  out.push(map(raw, member));
2993
- } catch {}
3553
+ } catch {
3554
+ unreadable.push(join20(mission, f));
3555
+ }
2994
3556
  }
2995
3557
  }
2996
3558
  return out;
@@ -3103,36 +3665,19 @@ function formatResume(e) {
3103
3665
  return `Resumed: ${e.mission}${scope}, Flow ${e.flow}, ${e.tasks_done}/${e.tasks_total} tasks — next_action: ${e.next_action} — run: ${next}`;
3104
3666
  }
3105
3667
 
3106
- // src/slop.ts
3107
- function detectHealingSlop(input) {
3108
- const kind = "healing";
3109
- const max = input.max_cycles ?? 3;
3110
- const hasZeroHistory = input.history_fixes.some((n) => n === 0);
3111
- if (input.fixes_in_cycle === 0 && hasZeroHistory) {
3112
- return { slop: true, reason: `slop: healing — no fixes in cycle ${input.cycle} with previous zero-fix cycle`, kind };
3113
- }
3114
- if (input.cycle >= max && input.fixes_in_cycle === 0) {
3115
- return { slop: true, reason: `slop: healing — cycle ${input.cycle} ≥ max ${max} with no fixes`, kind };
3116
- }
3117
- return { slop: false, reason: "no slop — healing making progress", kind };
3118
- }
3119
- function computeLiveSlop(input) {
3120
- const rows = [];
3121
- const thr = input.repeated_read_threshold ?? 3;
3122
- const heal = detectHealingSlop({ cycle: input.heal_cycle, fixes_in_cycle: 0, history_fixes: [], max_cycles: input.max_heal_cycles ?? 3 });
3123
- if (heal.slop)
3124
- rows.push({ role: "Brook", kind: "healing", reason: heal.reason });
3125
- if (input.repeated_reads >= thr)
3126
- rows.push({ role: "all", kind: "context", reason: `repeated reads ${input.repeated_reads} ≥ ${thr}` });
3127
- const perRole = {};
3128
- for (const r of rows)
3129
- perRole[r.role] = (perRole[r.role] ?? 0) + 1;
3130
- return { interventions: rows.length, perRole, rows };
3131
- }
3132
-
3133
3668
  // src/cli.ts
3134
3669
  var str = (v) => typeof v === "string" ? v : undefined;
3135
3670
  var flag = (v) => v === true;
3671
+ function resolveProjectDir(explicit) {
3672
+ if (explicit)
3673
+ return resolve2(explicit);
3674
+ try {
3675
+ const root = execFileSync6("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
3676
+ if (root)
3677
+ return root;
3678
+ } catch {}
3679
+ return process.cwd();
3680
+ }
3136
3681
  async function run(argv) {
3137
3682
  const { command, flags, _ } = parseArgs(argv);
3138
3683
  if (flag(flags.help) || command === "help")
@@ -3144,13 +3689,13 @@ async function run(argv) {
3144
3689
  {
3145
3690
  const bypass = new Set(["install", "update", "uninstall", "list"]);
3146
3691
  if (!bypass.has(command)) {
3147
- const projectDirForHarness = resolve2(str(flags.project) ?? process.cwd());
3692
+ const projectDirForHarness = resolveProjectDir(str(flags.project));
3148
3693
  enforceHarnessPolicy(projectDirForHarness);
3149
3694
  }
3150
3695
  }
3151
3696
  const isDryRunInstall = (command === "install" || command === "update") && flag(flags.dryRun);
3152
3697
  if (!isDryRunInstall) {
3153
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3698
+ const projectDir = resolveProjectDir(str(flags.project));
3154
3699
  if (ensureConfig(projectDir)) {
3155
3700
  console.log(`default .mugiwara/config written at ${join21(projectDir, ".mugiwara", "config")} (edit it to customise)`);
3156
3701
  }
@@ -3190,13 +3735,15 @@ async function run(argv) {
3190
3735
  case "sign":
3191
3736
  return signCmd(flags, _);
3192
3737
  case "migrate":
3193
- return migrateCmd(flags);
3738
+ return migrateCmd(flags, _);
3739
+ case "lesson":
3740
+ return lessonCmd(flags, _);
3194
3741
  default:
3195
3742
  throw new Error(`Unknown command: ${command}`);
3196
3743
  }
3197
3744
  }
3198
3745
  function resetCmd(flags) {
3199
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3746
+ const projectDir = resolveProjectDir(str(flags.project));
3200
3747
  const force = flag(flags.force);
3201
3748
  const result = resetMission(projectDir, flag(flags.keepLogs), force);
3202
3749
  if (result.blocked) {
@@ -3211,7 +3758,7 @@ function resetCmd(flags) {
3211
3758
  console.log(`kept: ${result.kept.join(", ")}`);
3212
3759
  }
3213
3760
  function archive(flags, positionals) {
3214
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3761
+ const projectDir = resolveProjectDir(str(flags.project));
3215
3762
  const mission = positionals[1];
3216
3763
  if (!mission) {
3217
3764
  console.error("usage: mugiwara archive <mission> [--project <dir>] [--dry-run]");
@@ -3230,7 +3777,7 @@ function archive(flags, positionals) {
3230
3777
  console.log(`index updated: ${result.index}`);
3231
3778
  }
3232
3779
  function cleanCmd(flags) {
3233
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3780
+ const projectDir = resolveProjectDir(str(flags.project));
3234
3781
  const dryRun = flag(flags.dryRun);
3235
3782
  const root = join21(projectDir, ".mugiwara", "missions");
3236
3783
  if (!existsSync16(root)) {
@@ -3297,7 +3844,7 @@ async function resolveOptions(flags) {
3297
3844
  } else
3298
3845
  scope = await choose(rl, "Install scope?", ["global (user-wide)", "project (this repo)"]) === 0 ? "global" : "project";
3299
3846
  }
3300
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3847
+ const projectDir = resolveProjectDir(str(flags.project));
3301
3848
  if (scope === "project" && !existsSync16(projectDir))
3302
3849
  throw new Error(`Project dir not found: ${projectDir}`);
3303
3850
  let targetIds = str(flags.target)?.split(",").map((s) => s.trim()) ?? null;
@@ -3365,12 +3912,14 @@ Dry run — nothing written.`);
3365
3912
  OK mugiwara ${VERSION} installed (manifest: ${file})`);
3366
3913
  if (allNotes.length)
3367
3914
  console.log(`${allNotes.length} note(s) above may need attention.`);
3915
+ console.log("CLI: run `npm i -g @ionivetech/mugiwara` so the crew can call `mugiwara savepoint/archive/continue`.");
3916
+ console.log(" Without it the crew degrades to inline-only — no state, no resume, no closure gate.");
3368
3917
  console.log(`
3369
3918
  Next: edit .mugiwara/config to customise (mode, branch, coverage, depths).`);
3370
3919
  }
3371
3920
  async function uninstall(flags) {
3372
3921
  const scope = flag(flags.global) ? "global" : "project";
3373
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3922
+ const projectDir = resolveProjectDir(str(flags.project));
3374
3923
  const home = homedir4();
3375
3924
  const file = manifestPath({ scope, projectDir, home });
3376
3925
  const manifest = readManifest(file);
@@ -3445,7 +3994,7 @@ function schemaWarnings(projectDir) {
3445
3994
  }
3446
3995
  function list(flags) {
3447
3996
  const home = homedir4();
3448
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3997
+ const projectDir = resolveProjectDir(str(flags.project));
3449
3998
  legacyWarning(projectDir);
3450
3999
  let found = false;
3451
4000
  for (const [label, file] of [
@@ -3467,11 +4016,20 @@ function list(flags) {
3467
4016
  console.log("No mugiwara installation found.");
3468
4017
  }
3469
4018
  function continueCmd(flags, positionals) {
3470
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
4019
+ const projectDir = resolveProjectDir(str(flags.project));
3471
4020
  legacyWarning(projectDir);
3472
4021
  schemaWarnings(projectDir);
3473
4022
  const [mission, member] = positionals.slice(1);
3474
4023
  let entries = readContinue(projectDir);
4024
+ if (mission) {
4025
+ readState(projectDir);
4026
+ const badState = unreadableStateFiles();
4027
+ const target10 = member ? `${mission}/${member}.json` : `${mission}/state.json`;
4028
+ if (badState.includes(target10)) {
4029
+ console.error(`✗ mission "${mission}"${member ? ` member "${member}"` : ""} has unreadable state: ${target10}`);
4030
+ process.exit(1);
4031
+ }
4032
+ }
3475
4033
  if (!flag(flags.all)) {
3476
4034
  const actor = gitActor(projectDir);
3477
4035
  const mine = entries.filter((e) => e.actor === actor);
@@ -3509,12 +4067,17 @@ Pick one: mugiwara continue ${r.mission} <member>`);
3509
4067
  process.exit(2);
3510
4068
  }
3511
4069
  function statusCmd(flags) {
3512
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
4070
+ const projectDir = resolveProjectDir(str(flags.project));
3513
4071
  legacyWarning(projectDir);
3514
4072
  schemaWarnings(projectDir);
3515
4073
  const states = readState(projectDir);
4074
+ const bad = unreadableStateFiles();
4075
+ if (bad.length) {
4076
+ console.error(`⚠ ${bad.length} unreadable state file(s): ${bad.join(", ")}`);
4077
+ console.error(' These are not "no mission" — they are corrupt. Inspect or delete them.');
4078
+ }
3516
4079
  if (!states.length) {
3517
- console.log("No mission state on disk.");
4080
+ console.log(bad.length ? "No readable mission state on disk." : "No mission state on disk.");
3518
4081
  return;
3519
4082
  }
3520
4083
  const actor = flag(flags.all) ? null : gitActor(projectDir);
@@ -3532,7 +4095,7 @@ function statusCmd(flags) {
3532
4095
  }
3533
4096
  }
3534
4097
  function costCmd(flags, positionals) {
3535
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
4098
+ const projectDir = resolveProjectDir(str(flags.project));
3536
4099
  const mission = str(flags.mission) ?? positionals[1] ?? (() => {
3537
4100
  const states2 = readState(projectDir);
3538
4101
  if (states2.length === 1)
@@ -3584,7 +4147,7 @@ function costCmd(flags, positionals) {
3584
4147
  }
3585
4148
  }
3586
4149
  function runCmd(flags, positionals) {
3587
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
4150
+ const projectDir = resolveProjectDir(str(flags.project));
3588
4151
  const name = positionals[1];
3589
4152
  if (!name) {
3590
4153
  console.error(`usage: mugiwara run <script> [args...]
@@ -3596,7 +4159,7 @@ function runCmd(flags, positionals) {
3596
4159
  process.exit(code);
3597
4160
  }
3598
4161
  function blameCmd(flags, positionals) {
3599
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
4162
+ const projectDir = resolveProjectDir(str(flags.project));
3600
4163
  const path = positionals[1];
3601
4164
  if (!path) {
3602
4165
  console.error("usage: mugiwara blame <file-path>");
@@ -3630,13 +4193,18 @@ function stalenessLine(projectDir, baseSha) {
3630
4193
  }
3631
4194
  }
3632
4195
  function handoffCmd(flags, positionals) {
3633
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
4196
+ const projectDir = resolveProjectDir(str(flags.project));
3634
4197
  const mission = positionals[1];
3635
4198
  if (!mission) {
3636
4199
  console.error("usage: mugiwara handoff <mission> [--project <dir>]");
3637
4200
  process.exit(1);
3638
4201
  }
3639
4202
  const states = readState(projectDir).filter((s) => s.mission === mission);
4203
+ const bad = unreadableStateFiles().filter((p) => p.startsWith(`${mission}/`));
4204
+ if (bad.length) {
4205
+ console.error(`✗ mission "${mission}" has unreadable state: ${bad.join(", ")}`);
4206
+ process.exit(1);
4207
+ }
3640
4208
  if (!states.length) {
3641
4209
  console.error(`no in-flight mission "${mission}"`);
3642
4210
  process.exit(1);
@@ -3677,9 +4245,166 @@ function handoffCmd(flags, positionals) {
3677
4245
  console.log(`
3678
4246
  written: ${out}`);
3679
4247
  }
3680
- function migrateCmd(flags) {
3681
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
4248
+ function lessonCmd(flags, positionals) {
4249
+ const projectDir = resolveProjectDir(str(flags.project));
4250
+ const text2 = positionals.slice(1).join(" ").trim();
4251
+ if (!text2) {
4252
+ console.error('usage: mugiwara lesson "<text>" [--project <dir>]');
4253
+ process.exit(1);
4254
+ }
4255
+ const file = join21(projectDir, ".mugiwara", "lessons.md");
4256
+ const date = new Date().toISOString().slice(0, 10);
4257
+ const sanitized = text2.replace(/\|/g, "/").replace(/\r?\n/g, " ").trim();
4258
+ const line = `| ${date} | manual | general | ${sanitized} |`;
4259
+ const header = `| Date | Mission | Area | Lesson |
4260
+ |---|---|---|---|`;
4261
+ let existing = "";
4262
+ try {
4263
+ existing = readFileSync15(file, "utf8");
4264
+ } catch {}
4265
+ if (!existing) {
4266
+ mkdirSync10(join21(projectDir, ".mugiwara"), { recursive: true });
4267
+ writeFileSync10(file, header + `
4268
+ ` + line + `
4269
+ `);
4270
+ } else {
4271
+ const needsNewline = !existing.endsWith(`
4272
+ `);
4273
+ writeFileSync10(file, existing + (needsNewline ? `
4274
+ ` : "") + line + `
4275
+ `);
4276
+ }
4277
+ console.log(`lesson appended: ${line}`);
4278
+ }
4279
+ function migrateCmd(flags, positionals = []) {
4280
+ const projectDir = resolveProjectDir(str(flags.project));
3682
4281
  const dryRun = flag(flags.dryRun);
4282
+ const toTeam = str(flags.toTeam);
4283
+ const toSolo = str(flags.toSolo);
4284
+ if (toTeam || toSolo) {
4285
+ const member = toTeam ?? toSolo;
4286
+ if (!/^[A-Za-z0-9._-]+$/.test(member) || /^\.+$/.test(member) || member === "state" || member === "continue") {
4287
+ console.error(`invalid member name "${member}" (allowlist: [a-zA-Z0-9._-], not a dot-path, not state/continue)`);
4288
+ process.exit(1);
4289
+ }
4290
+ if (toTeam && toSolo) {
4291
+ console.error("use either --to-team or --to-solo, not both");
4292
+ process.exit(1);
4293
+ }
4294
+ const missionsRootInner = join21(projectDir, ".mugiwara", "missions");
4295
+ let mission = str(flags.mission) ?? (positionals[1] ? String(positionals[1]) : null);
4296
+ const inferMission = () => {
4297
+ if (!existsSync16(missionsRootInner))
4298
+ return null;
4299
+ const all = readdirSync10(missionsRootInner, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4300
+ if (mission && all.includes(mission))
4301
+ return mission;
4302
+ if (mission)
4303
+ return mission;
4304
+ if (toTeam) {
4305
+ const candidates = all.filter((m) => existsSync16(join21(missionsRootInner, m, "state.json")));
4306
+ if (candidates.length === 1)
4307
+ return candidates[0];
4308
+ if (candidates.length === 0) {
4309
+ console.error("no solo mission with state.json found for --to-team");
4310
+ process.exit(1);
4311
+ }
4312
+ console.error(`multiple solo missions: ${candidates.join(", ")} — specify --mission <id>`);
4313
+ process.exit(1);
4314
+ } else {
4315
+ const candidates = all.filter((m) => existsSync16(join21(missionsRootInner, m, `${member}.json`)));
4316
+ if (candidates.length === 1)
4317
+ return candidates[0];
4318
+ if (candidates.length === 0) {
4319
+ console.error(`no mission with ${member}.json found for --to-solo`);
4320
+ process.exit(1);
4321
+ }
4322
+ console.error(`multiple missions with ${member}.json: ${candidates.join(", ")} — specify --mission <id>`);
4323
+ process.exit(1);
4324
+ }
4325
+ return null;
4326
+ };
4327
+ const targetMission = inferMission();
4328
+ if (!targetMission) {
4329
+ console.error("could not infer mission — specify --mission <id>");
4330
+ process.exit(1);
4331
+ }
4332
+ const dir = join21(missionsRootInner, targetMission);
4333
+ if (toTeam) {
4334
+ const srcState = join21(dir, "state.json");
4335
+ const srcContinue = join21(dir, "continue.json");
4336
+ const destState = join21(dir, `${member}.json`);
4337
+ const destContinue = join21(dir, `continue-${member}.json`);
4338
+ if (!existsSync16(srcState)) {
4339
+ console.error(`mission "${targetMission}" has no state.json — already team or not found`);
4340
+ process.exit(1);
4341
+ }
4342
+ if (existsSync16(destState)) {
4343
+ console.error(`destination ${destState} already exists`);
4344
+ process.exit(1);
4345
+ }
4346
+ const toMove = [{ src: srcState, dest: destState }];
4347
+ if (existsSync16(srcContinue))
4348
+ toMove.push({ src: srcContinue, dest: destContinue });
4349
+ for (const m of toMove) {
4350
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${m.src} → ${m.dest}`);
4351
+ if (!dryRun) {
4352
+ mkdirSync10(dirname6(m.dest), { recursive: true });
4353
+ try {
4354
+ renameSync2(m.src, m.dest);
4355
+ } catch {
4356
+ try {
4357
+ writeFileSync10(m.dest, readFileSync15(m.src));
4358
+ rmSync3(m.src, { force: true });
4359
+ } catch {}
4360
+ }
4361
+ }
4362
+ }
4363
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${toMove.length} file(s)${dryRun ? " (dry run)" : ""}`);
4364
+ return;
4365
+ } else {
4366
+ const srcState = join21(dir, `${member}.json`);
4367
+ const srcContinue = join21(dir, `continue-${member}.json`);
4368
+ const destState = join21(dir, "state.json");
4369
+ const destContinue = join21(dir, "continue.json");
4370
+ if (!existsSync16(srcState)) {
4371
+ console.error(`mission "${targetMission}" has no ${member}.json`);
4372
+ process.exit(1);
4373
+ }
4374
+ const files = readdirSync10(dir).filter((f) => {
4375
+ const stem = f.replace(/\.json$/, "");
4376
+ return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
4377
+ });
4378
+ const members = files.filter((f) => f !== "state.json");
4379
+ if (members.length > 1) {
4380
+ console.error(`mission "${targetMission}" has ${members.length} members (${members.join(", ")}) — refusing --to-solo (would orphan)`);
4381
+ process.exit(1);
4382
+ }
4383
+ if (existsSync16(destState)) {
4384
+ console.error(`destination ${destState} already exists`);
4385
+ process.exit(1);
4386
+ }
4387
+ const toMove = [{ src: srcState, dest: destState }];
4388
+ if (existsSync16(srcContinue))
4389
+ toMove.push({ src: srcContinue, dest: destContinue });
4390
+ for (const m of toMove) {
4391
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${m.src} → ${m.dest}`);
4392
+ if (!dryRun) {
4393
+ mkdirSync10(dirname6(m.dest), { recursive: true });
4394
+ try {
4395
+ renameSync2(m.src, m.dest);
4396
+ } catch {
4397
+ try {
4398
+ writeFileSync10(m.dest, readFileSync15(m.src));
4399
+ rmSync3(m.src, { force: true });
4400
+ } catch {}
4401
+ }
4402
+ }
4403
+ }
4404
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${toMove.length} file(s)${dryRun ? " (dry run)" : ""}`);
4405
+ return;
4406
+ }
4407
+ }
3683
4408
  const legacyState = join21(projectDir, ".mugiwara", "state");
3684
4409
  const legacyContinue = join21(projectDir, ".mugiwara", "continue");
3685
4410
  const missionsRoot = join21(projectDir, ".mugiwara", "missions");
@@ -3768,7 +4493,7 @@ function migrateCmd(flags) {
3768
4493
  console.log(`${dryRun ? "would migrate" : "migrated"} ${moves.length} file(s)${dryRun ? " (dry run)" : ""}`);
3769
4494
  }
3770
4495
  function signCmd(flags, _) {
3771
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
4496
+ const projectDir = resolveProjectDir(str(flags.project));
3772
4497
  if (flag(flags.genKey)) {
3773
4498
  const backend = str(flags.backend) ?? "auto";
3774
4499
  const home = homedir4();
@@ -3832,7 +4557,12 @@ Usage:
3832
4557
  mugiwara sign --gen-key [--backend pure|minisign]
3833
4558
  create signing keys (pure ed25519 default)
3834
4559
  mugiwara migrate [--dry-run] [--project <dir>]
3835
- move legacy .mugiwara/state/ layout to .mugiwara/missions/
4560
+ move legacy .mugiwara/state/ layout to .mugiwara/missions/
4561
+ mugiwara migrate --to-team <member> [--mission <id>] [--dry-run]
4562
+ move state.json -> <member>.json (solo -> team)
4563
+ mugiwara migrate --to-solo <member> [--mission <id>] [--dry-run]
4564
+ move <member>.json -> state.json (team -> solo; refuses if >1 member)
4565
+ mugiwara lesson "<text>" append a dated row to .mugiwara/lessons.md
3836
4566
  mugiwara run <script> [args...]
3837
4567
  run a bundled harness script here (${RUNNABLE.join(", ")})
3838
4568
  mugiwara savepoint <mission> [member] [flow] [mode]