@ionivetech/mugiwara 0.8.2 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.opencode/mugiwara-helpers.mjs +1 -1
- package/.opencode/plugins/mugiwara.mjs +173 -1
- package/README.md +63 -58
- package/content/agents/luffy-orchestrator.md +16 -1
- package/content/agents/zoro-execution.md +1 -1
- package/content/skills/mugiwara-checkpoint/SKILL.md +1 -0
- package/content/skills/mugiwara-execution/SKILL.md +5 -5
- package/content/skills/mugiwara-gates/SKILL.md +1 -0
- package/content/skills/mugiwara-healing/SKILL.md +1 -0
- package/content/skills/mugiwara-lessons/SKILL.md +2 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +15 -20
- package/content/skills/mugiwara-orchestration/references/check-ins.md +4 -5
- package/content/skills/mugiwara-orchestration/references/output-contract.md +2 -2
- package/content/skills/mugiwara-orchestration/references/solo-team.md +18 -0
- package/content/skills/mugiwara-planning/SKILL.md +7 -15
- package/content/skills/mugiwara-planning/references/sub-missions.md +14 -0
- package/content/skills/mugiwara-quality/SKILL.md +1 -0
- package/content/skills/mugiwara-review/SKILL.md +2 -0
- package/content/skills/mugiwara-security/SKILL.md +2 -2
- package/content/skills/mugiwara-ship/SKILL.md +12 -0
- package/content/skills/mugiwara-workflow/SKILL.md +3 -3
- package/dist/mugiwara.js +934 -158
- package/gemini-extension.json +1 -1
- package/hooks/engagement-marker.js +9 -1
- package/hooks/engagement-marker.ts +9 -1
- package/hooks/hooks.json +12 -0
- package/hooks/pipeline-guard.js +137 -3
- package/hooks/pipeline-guard.ts +161 -3
- package/hooks/pretool-guard.js +84 -0
- package/hooks/pretool-guard.ts +60 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/references/multi-actor.md +17 -14
- package/references/wave-banners.md +22 -27
- package/scripts/build-hooks.ts +1 -1
- package/scripts/gate-selftest.ts +480 -0
- package/scripts/savepoint.sh +139 -14
- package/scripts/validate-content.ts +345 -2
- package/scripts/write-metrics.ts +25 -1
- package/src/args.ts +1 -1
- package/src/cli.ts +158 -3
- package/src/config.ts +33 -11
- package/src/guards.ts +40 -0
- package/src/initiative.ts +174 -0
- package/src/mission.ts +137 -52
- package/src/targets/claude.ts +1 -0
package/dist/mugiwara.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync16, rmSync as rmSync3, writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, renameSync as renameSync2 } from "node:fs";
|
|
5
5
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
6
6
|
import { homedir as homedir4 } from "node:os";
|
|
7
7
|
import { dirname as dirname6, join as join21, resolve as resolve2 } from "node:path";
|
|
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",
|
|
@@ -152,6 +152,7 @@ function wireSettings(root, hooksDir) {
|
|
|
152
152
|
const events = {
|
|
153
153
|
SessionStart: { file: "session-start.js", timeout: 10 },
|
|
154
154
|
UserPromptSubmit: { file: "mugiwara-mode-tracker.js", timeout: 5 },
|
|
155
|
+
PreToolUse: { file: "pretool-guard.js", timeout: 10, matcher: "Bash" },
|
|
155
156
|
Stop: { file: "auto-savepoint.js", timeout: 20 },
|
|
156
157
|
SubagentStop: { file: "pipeline-guard.js", timeout: 15 },
|
|
157
158
|
PostToolUse: { file: "engagement-marker.js", timeout: 5, matcher: "Task|Skill" }
|
|
@@ -633,24 +634,43 @@ import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync3, readFile
|
|
|
633
634
|
import { homedir } from "node:os";
|
|
634
635
|
import { join as join5 } from "node:path";
|
|
635
636
|
var DEFAULT_CONFIG = [
|
|
636
|
-
"
|
|
637
|
+
"# Mugiwara config. Project overrides ~/.mugiwara/config.",
|
|
638
|
+
"# Every key here is read by code. Delete a line to take its default.",
|
|
639
|
+
"",
|
|
640
|
+
"# -- Autonomy ---------------------------------------------",
|
|
641
|
+
"mode=guided # guided | semi | auto — how much the crew does without asking",
|
|
642
|
+
"verbosity=normal # normal | full — how much the crew echoes",
|
|
643
|
+
"",
|
|
644
|
+
"# -- Team -------------------------------------------------",
|
|
645
|
+
"# team_member= # your member id; set it and state isolates per person",
|
|
646
|
+
"# team_members=1 # how many people on this mission; >1 enables team-scoped posture",
|
|
647
|
+
"",
|
|
648
|
+
"# -- Git --------------------------------------------------",
|
|
637
649
|
"branch=feature/{type}-{issue}-{slug}",
|
|
638
650
|
"commit=conventional",
|
|
639
|
-
"auto_commit=on",
|
|
651
|
+
"auto_commit=off # on | off — off hands you an uncommitted tree in guided/semi",
|
|
652
|
+
"",
|
|
653
|
+
"# -- Gates ------------------------------------------------",
|
|
640
654
|
"coverage_new=85",
|
|
641
655
|
"coverage_modified=90",
|
|
642
|
-
"review_depth=full",
|
|
656
|
+
"review_depth=full # full | standard | quick",
|
|
643
657
|
"quality_depth=full",
|
|
644
658
|
"verify_merged=off",
|
|
645
|
-
"
|
|
646
|
-
"
|
|
647
|
-
"
|
|
648
|
-
"
|
|
649
|
-
"
|
|
659
|
+
"",
|
|
660
|
+
"# -- Limits -----------------------------------------------",
|
|
661
|
+
"delegate_threshold=60 # % of budget before delegation is advised",
|
|
662
|
+
"heal_max_cycles=3 # heal loop halts here and escalates",
|
|
663
|
+
"",
|
|
664
|
+
"# -- Monorepo ---------------------------------------------",
|
|
665
|
+
"# lane_scope_glob=packages/api/** # count only matching files when sizing the lane",
|
|
666
|
+
"",
|
|
667
|
+
"# -- Optional ---------------------------------------------",
|
|
668
|
+
"# context_budget_chars=150000 # fail archive if the trail exceeds this",
|
|
669
|
+
"# investigation_max_passes=2",
|
|
650
670
|
"# investigation_max_unrelated_files=5",
|
|
651
671
|
"# investigation_repeated_read_threshold=2",
|
|
652
|
-
"# sign=auto
|
|
653
|
-
"# enforce=block
|
|
672
|
+
"# sign=auto # auto | minisign | pure | off",
|
|
673
|
+
"# enforce=block # off | warn | block — pipeline-guard policy"
|
|
654
674
|
].join(`
|
|
655
675
|
`) + `
|
|
656
676
|
`;
|
|
@@ -685,7 +705,11 @@ function readConfig(projectDir) {
|
|
|
685
705
|
continue;
|
|
686
706
|
if (key in out)
|
|
687
707
|
continue;
|
|
688
|
-
|
|
708
|
+
let rawVal = t.slice(eq + 1).trim();
|
|
709
|
+
const hash = rawVal.indexOf("#");
|
|
710
|
+
if (hash !== -1)
|
|
711
|
+
rawVal = rawVal.slice(0, hash).trim();
|
|
712
|
+
out[key] = rawVal;
|
|
689
713
|
}
|
|
690
714
|
}
|
|
691
715
|
return out;
|
|
@@ -704,6 +728,25 @@ function ensureConfig(projectDir) {
|
|
|
704
728
|
writeFileSync3(file, DEFAULT_CONFIG);
|
|
705
729
|
return true;
|
|
706
730
|
}
|
|
731
|
+
var INVESTIGATION_DEFAULTS = {
|
|
732
|
+
max_passes: 2,
|
|
733
|
+
max_unrelated_files: 5,
|
|
734
|
+
repeated_read_threshold: 2
|
|
735
|
+
};
|
|
736
|
+
function positiveInt(raw, fallback) {
|
|
737
|
+
if (raw === undefined || raw === "")
|
|
738
|
+
return fallback;
|
|
739
|
+
const n = Number(raw);
|
|
740
|
+
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
741
|
+
}
|
|
742
|
+
function readInvestigationConfig(projectDir) {
|
|
743
|
+
const cfg = readConfig(projectDir);
|
|
744
|
+
return {
|
|
745
|
+
max_passes: positiveInt(cfg.investigation_max_passes, INVESTIGATION_DEFAULTS.max_passes),
|
|
746
|
+
max_unrelated_files: positiveInt(cfg.investigation_max_unrelated_files, INVESTIGATION_DEFAULTS.max_unrelated_files),
|
|
747
|
+
repeated_read_threshold: positiveInt(cfg.investigation_repeated_read_threshold, INVESTIGATION_DEFAULTS.repeated_read_threshold)
|
|
748
|
+
};
|
|
749
|
+
}
|
|
707
750
|
|
|
708
751
|
// src/installer.ts
|
|
709
752
|
var CONTENT_DIR = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "content");
|
|
@@ -737,11 +780,11 @@ function collectRefs(skillDir) {
|
|
|
737
780
|
return [];
|
|
738
781
|
return readdirSync3(refsDir, { recursive: true }).map((f) => String(f)).filter((f) => f.endsWith(".md")).map((rel) => ({ relPath: rel, text: readFileSync4(join6(refsDir, rel), "utf8") }));
|
|
739
782
|
}
|
|
740
|
-
function installTo(
|
|
783
|
+
function installTo(target, opts) {
|
|
741
784
|
const { scope, projectDir, dryRun = false, force = false } = opts;
|
|
742
785
|
const home = opts.home ?? homedir2();
|
|
743
786
|
const { skills, agents, sharedRefs } = collectContent();
|
|
744
|
-
const dirs =
|
|
787
|
+
const dirs = target.paths({ scope, projectDir, home });
|
|
745
788
|
const backupRoot = join6(scope === "global" ? home : projectDir, ".mugiwara");
|
|
746
789
|
const result = { written: [], skipped: [], backedUp: [], notes: [] };
|
|
747
790
|
const writeOne = (absPath, text, mode) => {
|
|
@@ -762,7 +805,7 @@ function installTo(target10, opts) {
|
|
|
762
805
|
}
|
|
763
806
|
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
764
807
|
const fileName = absPath.replace(/[^a-zA-Z0-9]+/g, "_");
|
|
765
|
-
const backupDir = join6(backupRoot, "backup", `${ts}-${
|
|
808
|
+
const backupDir = join6(backupRoot, "backup", `${ts}-${target.id}`);
|
|
766
809
|
const backupFile = join6(backupDir, fileName);
|
|
767
810
|
if (!dryRun) {
|
|
768
811
|
mkdirSync4(backupDir, { recursive: true });
|
|
@@ -782,38 +825,38 @@ function installTo(target10, opts) {
|
|
|
782
825
|
result.written.push(absPath);
|
|
783
826
|
};
|
|
784
827
|
for (const s of skills) {
|
|
785
|
-
const out =
|
|
828
|
+
const out = target.transformSkill(s.data, s.body);
|
|
786
829
|
if (out) {
|
|
787
830
|
let text = out.text;
|
|
788
|
-
if (
|
|
789
|
-
text = text.replace(/`_shared\/references\//g, "`" +
|
|
831
|
+
if (target.refPointerPrefix !== undefined && target.refPointerPrefix !== "") {
|
|
832
|
+
text = text.replace(/`_shared\/references\//g, "`" + target.refPointerPrefix + "_shared/references/");
|
|
790
833
|
}
|
|
791
834
|
writeOne(join6(dirs.skillsDir, out.relPath), text);
|
|
792
835
|
}
|
|
793
|
-
if (
|
|
794
|
-
const full =
|
|
795
|
-
if (full &&
|
|
836
|
+
if (target.transformSkillFull) {
|
|
837
|
+
const full = target.transformSkillFull(s.data, s.body);
|
|
838
|
+
if (full && target.refsDir) {
|
|
796
839
|
let text = full.text;
|
|
797
|
-
if (
|
|
798
|
-
text = text.replace(/`_shared\/references\//g, "`" +
|
|
840
|
+
if (target.refPointerPrefix !== undefined && target.refPointerPrefix !== "") {
|
|
841
|
+
text = text.replace(/`_shared\/references\//g, "`" + target.refPointerPrefix + "_shared/references/");
|
|
799
842
|
}
|
|
800
|
-
writeOne(join6(
|
|
843
|
+
writeOne(join6(target.refsDir({ scope, projectDir, home }, s.name), full.relPath), text);
|
|
801
844
|
}
|
|
802
845
|
}
|
|
803
|
-
if (s.refs.length &&
|
|
804
|
-
const refsRoot =
|
|
846
|
+
if (s.refs.length && target.refsDir) {
|
|
847
|
+
const refsRoot = target.refsDir({ scope, projectDir, home }, s.name);
|
|
805
848
|
for (const r of s.refs)
|
|
806
849
|
writeOne(join6(refsRoot, r.relPath), r.text);
|
|
807
850
|
}
|
|
808
851
|
}
|
|
809
852
|
for (const a of agents) {
|
|
810
|
-
const out =
|
|
853
|
+
const out = target.transformAgent({ ...a.data, ...a.internal ? { "internal-agent": "true" } : {} }, a.body);
|
|
811
854
|
if (out)
|
|
812
855
|
writeOne(join6(dirs.agentsDir, out.relPath), out.text);
|
|
813
|
-
if (
|
|
814
|
-
const full =
|
|
815
|
-
if (full &&
|
|
816
|
-
writeOne(join6(
|
|
856
|
+
if (target.transformAgentFull) {
|
|
857
|
+
const full = target.transformAgentFull(a.data, a.body);
|
|
858
|
+
if (full && target.refsDir)
|
|
859
|
+
writeOne(join6(target.refsDir({ scope, projectDir, home }, a.name), full.relPath), full.text);
|
|
817
860
|
}
|
|
818
861
|
}
|
|
819
862
|
if (sharedRefs.length) {
|
|
@@ -833,8 +876,8 @@ function installTo(target10, opts) {
|
|
|
833
876
|
writeOne(dest, text, 493);
|
|
834
877
|
}
|
|
835
878
|
}
|
|
836
|
-
if (
|
|
837
|
-
const post =
|
|
879
|
+
if (target.postInstall) {
|
|
880
|
+
const post = target.postInstall({ scope, projectDir, home, dryRun, files: result.written });
|
|
838
881
|
result.written.push(...post.written);
|
|
839
882
|
result.notes.push(...post.notes);
|
|
840
883
|
}
|
|
@@ -1020,10 +1063,10 @@ function parsePolicyYaml(text) {
|
|
|
1020
1063
|
stack.pop();
|
|
1021
1064
|
const top = stack[stack.length - 1];
|
|
1022
1065
|
if (top.pending && !line.startsWith("- ") && indent > top.pending.indent && !Array.isArray(top.obj[top.pending.key])) {
|
|
1023
|
-
const { key
|
|
1066
|
+
const { key, indent: pIndent } = top.pending;
|
|
1024
1067
|
delete top.pending;
|
|
1025
1068
|
const created = {};
|
|
1026
|
-
top.obj[
|
|
1069
|
+
top.obj[key] = created;
|
|
1027
1070
|
stack.push({ keyIndent: pIndent, obj: created });
|
|
1028
1071
|
return process2(i);
|
|
1029
1072
|
}
|
|
@@ -1575,15 +1618,15 @@ function checkTrail(missionDir, projectRoot) {
|
|
|
1575
1618
|
} catch {
|
|
1576
1619
|
continue;
|
|
1577
1620
|
}
|
|
1578
|
-
for (const
|
|
1579
|
-
if (isAbsolute(
|
|
1621
|
+
for (const target of linkedPaths(body)) {
|
|
1622
|
+
if (isAbsolute(target))
|
|
1580
1623
|
continue;
|
|
1581
|
-
const fromMission = join9(missionDir,
|
|
1582
|
-
const fromRoot = join9(projectRoot,
|
|
1624
|
+
const fromMission = join9(missionDir, target);
|
|
1625
|
+
const fromRoot = join9(projectRoot, target);
|
|
1583
1626
|
if (!existsSync8(fromMission) && !existsSync8(fromRoot)) {
|
|
1584
1627
|
issues.push({
|
|
1585
1628
|
kind: "dangling-path",
|
|
1586
|
-
detail: `${relative(projectRoot, f)} links "${
|
|
1629
|
+
detail: `${relative(projectRoot, f)} links "${target}" — no such file (mission dir or repo root)`
|
|
1587
1630
|
});
|
|
1588
1631
|
}
|
|
1589
1632
|
}
|
|
@@ -1810,18 +1853,18 @@ function attachGitNote(projectDir, branch, note, baseSha) {
|
|
|
1810
1853
|
console.warn(`attachGitNote: range ${shas.length} >200, falling back to head-only`);
|
|
1811
1854
|
shas = [];
|
|
1812
1855
|
}
|
|
1813
|
-
let
|
|
1814
|
-
if (!
|
|
1856
|
+
let targets = shas;
|
|
1857
|
+
if (!targets.length) {
|
|
1815
1858
|
try {
|
|
1816
|
-
|
|
1859
|
+
targets = [git2(projectDir, ["rev-parse", "--verify", branch])];
|
|
1817
1860
|
} catch {
|
|
1818
|
-
|
|
1861
|
+
targets = [git2(projectDir, ["rev-parse", "HEAD"])];
|
|
1819
1862
|
}
|
|
1820
1863
|
}
|
|
1821
|
-
for (const sha of
|
|
1864
|
+
for (const sha of targets) {
|
|
1822
1865
|
git2(projectDir, ["notes", "--ref=mugiwara", "add", "-f", "-m", note, sha]);
|
|
1823
1866
|
}
|
|
1824
|
-
return { sha:
|
|
1867
|
+
return { sha: targets[0], count: targets.length };
|
|
1825
1868
|
} catch {
|
|
1826
1869
|
return null;
|
|
1827
1870
|
}
|
|
@@ -2242,10 +2285,37 @@ function appendCostEvent(missionDir, event) {
|
|
|
2242
2285
|
`, "utf8");
|
|
2243
2286
|
}
|
|
2244
2287
|
var COMPRESSED_KIND = "compressed";
|
|
2288
|
+
var DECISIONS_FILE = "decisions.md";
|
|
2289
|
+
var OPT_SECTION = "## Cost governor decisions";
|
|
2290
|
+
function recordOptDecision(missionDir, d) {
|
|
2291
|
+
assertMissionDir(missionDir);
|
|
2292
|
+
mkdirSync7(missionDir, { recursive: true });
|
|
2293
|
+
const file = join15(missionDir, DECISIONS_FILE);
|
|
2294
|
+
let hasSection = false;
|
|
2295
|
+
try {
|
|
2296
|
+
hasSection = readFileSync10(file, "utf8").split(/\r?\n/).some((l) => l.trim() === OPT_SECTION);
|
|
2297
|
+
} catch {}
|
|
2298
|
+
const ts = new Date().toISOString();
|
|
2299
|
+
const flat = (s) => s.replace(/[\r\n]+/g, " ");
|
|
2300
|
+
const ev = d.evidence ? ` — evidence: ${flat(d.evidence)}` : "";
|
|
2301
|
+
const bullet = `- ${ts} — ${flat(d.actor)}: ${flat(d.decision)} — reason: ${flat(d.reason)}${ev}`;
|
|
2302
|
+
const body = hasSection ? `
|
|
2303
|
+
${bullet}
|
|
2304
|
+
` : `
|
|
2305
|
+
${OPT_SECTION}
|
|
2306
|
+
|
|
2307
|
+
${bullet}
|
|
2308
|
+
`;
|
|
2309
|
+
appendFileSync(file, body, "utf8");
|
|
2310
|
+
}
|
|
2245
2311
|
|
|
2246
2312
|
// src/evidence.ts
|
|
2313
|
+
import { createHash } from "node:crypto";
|
|
2247
2314
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync11 } from "node:fs";
|
|
2248
2315
|
import { join as join16 } from "node:path";
|
|
2316
|
+
function fingerprint(content) {
|
|
2317
|
+
return createHash("sha256").update(content).digest("hex");
|
|
2318
|
+
}
|
|
2249
2319
|
var REGISTRY_FILE = "context-registry.jsonl";
|
|
2250
2320
|
function isAllowedMissionDir2(dir) {
|
|
2251
2321
|
if (!dir || dir.includes(".."))
|
|
@@ -2262,6 +2332,43 @@ function assertMissionDir2(dir) {
|
|
|
2262
2332
|
if (!isAllowedMissionDir2(dir))
|
|
2263
2333
|
throw new Error(`Invalid missionDir: ${dir}`);
|
|
2264
2334
|
}
|
|
2335
|
+
function maxSeq(registry) {
|
|
2336
|
+
let max = 0;
|
|
2337
|
+
for (const e of registry) {
|
|
2338
|
+
const m = /^E(\d+)$/.exec(e.id);
|
|
2339
|
+
if (m) {
|
|
2340
|
+
const n = parseInt(m[1], 10);
|
|
2341
|
+
if (n > max)
|
|
2342
|
+
max = n;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
return max;
|
|
2346
|
+
}
|
|
2347
|
+
function buildRef(id, file, range) {
|
|
2348
|
+
return range ? `${id} ${file}:${range}` : `${id} ${file}`;
|
|
2349
|
+
}
|
|
2350
|
+
function registerRead(registry, e) {
|
|
2351
|
+
const fp = fingerprint(e.content);
|
|
2352
|
+
const existing = registry.find((x) => x.fingerprint === fp && x.kind === e.kind);
|
|
2353
|
+
if (existing) {
|
|
2354
|
+
existing.reads += 1;
|
|
2355
|
+
return { ref: existing.ref, repeated: true };
|
|
2356
|
+
}
|
|
2357
|
+
const seq = maxSeq(registry) + 1;
|
|
2358
|
+
const id = `E${String(seq).padStart(3, "0")}`;
|
|
2359
|
+
const ref = buildRef(id, e.file, e.range);
|
|
2360
|
+
registry.push({
|
|
2361
|
+
fingerprint: fp,
|
|
2362
|
+
kind: e.kind,
|
|
2363
|
+
file: e.file,
|
|
2364
|
+
...e.range ? { range: e.range } : {},
|
|
2365
|
+
id,
|
|
2366
|
+
reads: 1,
|
|
2367
|
+
chars: e.content.length,
|
|
2368
|
+
ref
|
|
2369
|
+
});
|
|
2370
|
+
return { ref, repeated: false };
|
|
2371
|
+
}
|
|
2265
2372
|
function loadRegistry(missionDir) {
|
|
2266
2373
|
assertMissionDir2(missionDir);
|
|
2267
2374
|
const file = join16(missionDir, REGISTRY_FILE);
|
|
@@ -2447,6 +2554,288 @@ function renderAdaptationSection(missionDir) {
|
|
|
2447
2554
|
`);
|
|
2448
2555
|
}
|
|
2449
2556
|
|
|
2557
|
+
// src/posture.ts
|
|
2558
|
+
function selectPosture(input) {
|
|
2559
|
+
if (input.governor === "stop") {
|
|
2560
|
+
return {
|
|
2561
|
+
posture: "inline-sequential",
|
|
2562
|
+
pause: true,
|
|
2563
|
+
reason: "governor stop — pause safely, keep inline; state + continue emitted",
|
|
2564
|
+
evidence_refs: ["governor circuit-breaker", "state.json"]
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
if (input.team_members > 1) {
|
|
2568
|
+
return {
|
|
2569
|
+
posture: "team-scoped",
|
|
2570
|
+
pause: false,
|
|
2571
|
+
reason: `${input.team_members} team members with non-overlapping scope`,
|
|
2572
|
+
evidence_refs: ["plan ownership map"]
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
if (input.phases > 3 || input.plan_lines > 1500) {
|
|
2576
|
+
return {
|
|
2577
|
+
posture: "phase-isolated",
|
|
2578
|
+
pause: false,
|
|
2579
|
+
reason: `large campaign — ${input.phases} phases / ${input.plan_lines} lines`,
|
|
2580
|
+
evidence_refs: ["plan.md", "large-campaign-subplan.md"]
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
if (input.context_pressure && input.order_dependent) {
|
|
2584
|
+
return {
|
|
2585
|
+
posture: "context-relief",
|
|
2586
|
+
pause: false,
|
|
2587
|
+
reason: "context pressure with ordered dependent tasks — one worker at a time, order preserved",
|
|
2588
|
+
evidence_refs: ["state context metrics", "remaining task order"]
|
|
2589
|
+
};
|
|
2590
|
+
}
|
|
2591
|
+
if (input.independent_tasks >= 2) {
|
|
2592
|
+
return {
|
|
2593
|
+
posture: "parallel-workers",
|
|
2594
|
+
pause: false,
|
|
2595
|
+
reason: `${input.independent_tasks} independent tasks, no shared files/interfaces`,
|
|
2596
|
+
evidence_refs: ["Nami dependency map", "work-governor delegation verdict"]
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
return {
|
|
2600
|
+
posture: "inline-sequential",
|
|
2601
|
+
pause: false,
|
|
2602
|
+
reason: "no parallel/phase/team/relief trigger — default inline in plan order",
|
|
2603
|
+
evidence_refs: ["triage route", "lane"]
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
// src/investigation.ts
|
|
2608
|
+
function evaluateInvestigation(input) {
|
|
2609
|
+
const { pass } = input;
|
|
2610
|
+
if (input.acceptance_mapped && input.surface_understood && input.path_established) {
|
|
2611
|
+
return { pass, stop: true, reason: "objective met" };
|
|
2612
|
+
}
|
|
2613
|
+
if (pass >= input.max_passes) {
|
|
2614
|
+
return { pass, stop: true, reason: "max passes" };
|
|
2615
|
+
}
|
|
2616
|
+
if (input.unrelated_files_opened > input.max_unrelated_files) {
|
|
2617
|
+
return { pass, stop: true, reason: "max unrelated files" };
|
|
2618
|
+
}
|
|
2619
|
+
if (input.repeated_reads >= input.repeated_read_threshold) {
|
|
2620
|
+
return { pass, stop: true, reason: "repeated read" };
|
|
2621
|
+
}
|
|
2622
|
+
return { pass, stop: false, reason: "" };
|
|
2623
|
+
}
|
|
2624
|
+
function recordInvestigationStop(missionDir, status, evidence) {
|
|
2625
|
+
if (!status.stop)
|
|
2626
|
+
return;
|
|
2627
|
+
recordOptDecision(missionDir, {
|
|
2628
|
+
actor: "cost-governor",
|
|
2629
|
+
decision: "stop investigation",
|
|
2630
|
+
reason: status.reason,
|
|
2631
|
+
...evidence ? { evidence } : {}
|
|
2632
|
+
});
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
// src/adaptive-budget.ts
|
|
2636
|
+
function reserveBudget(input) {
|
|
2637
|
+
const reserved = input.expected_max;
|
|
2638
|
+
const available = Math.max(0, input.remaining - reserved);
|
|
2639
|
+
return { remaining: input.remaining, expected_max: input.expected_max, available, reserved };
|
|
2640
|
+
}
|
|
2641
|
+
function projectBudget(input) {
|
|
2642
|
+
const projected_min = input.current + input.remaining_required + input.expected_conditional;
|
|
2643
|
+
const projected_max = projected_min + input.possible_healing;
|
|
2644
|
+
return {
|
|
2645
|
+
current: input.current,
|
|
2646
|
+
remaining_required: input.remaining_required,
|
|
2647
|
+
expected_conditional: input.expected_conditional,
|
|
2648
|
+
possible_healing: input.possible_healing,
|
|
2649
|
+
projected_min,
|
|
2650
|
+
projected_max
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
var VALID_REASONS = new Set([
|
|
2654
|
+
"scope legitimately expanded",
|
|
2655
|
+
"security-sensitive path",
|
|
2656
|
+
"test surface larger",
|
|
2657
|
+
"architecture dependency",
|
|
2658
|
+
"legitimate healing"
|
|
2659
|
+
]);
|
|
2660
|
+
function checkProgressiveThreshold(input) {
|
|
2661
|
+
const pct = input.budget > 0 ? Math.round(input.used / input.budget * 100) : 0;
|
|
2662
|
+
let status = "ok";
|
|
2663
|
+
if (pct >= 300)
|
|
2664
|
+
status = "stop";
|
|
2665
|
+
else if (pct >= 150)
|
|
2666
|
+
status = "warning";
|
|
2667
|
+
else if (pct >= 100)
|
|
2668
|
+
status = "pause";
|
|
2669
|
+
else if (pct >= 90)
|
|
2670
|
+
status = "protect";
|
|
2671
|
+
else if (pct >= 75)
|
|
2672
|
+
status = "aggressive";
|
|
2673
|
+
else if (pct >= 60)
|
|
2674
|
+
status = "optimize";
|
|
2675
|
+
return { status, pct };
|
|
2676
|
+
}
|
|
2677
|
+
function checkCircuitBreaker(input) {
|
|
2678
|
+
const doubled = input.expected * 2;
|
|
2679
|
+
const noProgress = input.progress_delta === 0;
|
|
2680
|
+
const noScopeOrEvidence = !input.scope_expanded && input.evidence_delta === 0;
|
|
2681
|
+
const overDoubled = input.actual >= doubled;
|
|
2682
|
+
if (overDoubled && noProgress && noScopeOrEvidence) {
|
|
2683
|
+
return { tripped: true, reason: `breaker tripped — actual ${input.actual} ≥ 2× expected ${input.expected} with no progress/scope/evidence` };
|
|
2684
|
+
}
|
|
2685
|
+
if (!overDoubled)
|
|
2686
|
+
return { tripped: false, reason: `no breaker — actual ${input.actual} < 2× expected ${input.expected}` };
|
|
2687
|
+
if (!noProgress)
|
|
2688
|
+
return { tripped: false, reason: "no breaker — progress made" };
|
|
2689
|
+
return { tripped: false, reason: "no breaker — scope expanded or evidence gained" };
|
|
2690
|
+
}
|
|
2691
|
+
function detectBudgetAnomaly(input) {
|
|
2692
|
+
const tokens_delta = input.tokens_after - input.tokens_before;
|
|
2693
|
+
const progress_delta = input.progress_after - input.progress_before;
|
|
2694
|
+
if (tokens_delta >= 5000 && progress_delta === 0) {
|
|
2695
|
+
return { anomaly: true, reason: `anomaly — ${tokens_delta} tokens with no progress` };
|
|
2696
|
+
}
|
|
2697
|
+
if (tokens_delta < 5000 && progress_delta === 0) {
|
|
2698
|
+
return { anomaly: false, reason: `no anomaly — ${tokens_delta} tokens below 5k floor` };
|
|
2699
|
+
}
|
|
2700
|
+
return { anomaly: false, reason: `no anomaly — progress ${progress_delta} over ${tokens_delta} tokens` };
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// src/cognition.ts
|
|
2704
|
+
function isFocusedReasoning(input) {
|
|
2705
|
+
const slop_types = [];
|
|
2706
|
+
if (input.speculative_paths > 0)
|
|
2707
|
+
slop_types.push("speculative_architecture");
|
|
2708
|
+
if (input.reconsiderations >= 2)
|
|
2709
|
+
slop_types.push("repeated_reconsideration");
|
|
2710
|
+
if (input.hypothetical_requirements)
|
|
2711
|
+
slop_types.push("hypothetical_requirements");
|
|
2712
|
+
if (input.unrelated_implementations > 0)
|
|
2713
|
+
slop_types.push("unrelated_implementations");
|
|
2714
|
+
const focused = slop_types.length === 0;
|
|
2715
|
+
const reason = focused ? "Question→Evidence→Decision→Action — reasoning is focused" : `unfocused — ${slop_types.join(", ")}`;
|
|
2716
|
+
return { focused, reason, slop_types };
|
|
2717
|
+
}
|
|
2718
|
+
function detectDuplicateExplanation(input) {
|
|
2719
|
+
const groups = new Map;
|
|
2720
|
+
for (const exp of input.explanations) {
|
|
2721
|
+
const fp = fingerprint(exp);
|
|
2722
|
+
const arr = groups.get(fp);
|
|
2723
|
+
if (arr)
|
|
2724
|
+
arr.push(exp);
|
|
2725
|
+
else
|
|
2726
|
+
groups.set(fp, [exp]);
|
|
2727
|
+
}
|
|
2728
|
+
const duplicate_groups = [];
|
|
2729
|
+
for (const arr of groups.values()) {
|
|
2730
|
+
if (arr.length >= 2)
|
|
2731
|
+
duplicate_groups.push(arr);
|
|
2732
|
+
}
|
|
2733
|
+
const duplicate = duplicate_groups.length > 0;
|
|
2734
|
+
const reason = duplicate ? `${duplicate_groups.length} duplicate group(s) — ${duplicate_groups.length} duplicate explanation(s) found` : "no duplicate explanations";
|
|
2735
|
+
return { duplicate, reason, duplicate_groups };
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
// src/scope.ts
|
|
2739
|
+
function detectScopeDrift(input) {
|
|
2740
|
+
const outside = input.touched_files.filter((f) => !input.declared_scope.some((tok) => f.includes(tok)));
|
|
2741
|
+
const scope_score = input.touched_files.length === 0 ? 0 : outside.length / input.touched_files.length;
|
|
2742
|
+
if (outside.length === 0) {
|
|
2743
|
+
return { change: input.change, drift: false, reason: "within declared scope", scope_score };
|
|
2744
|
+
}
|
|
2745
|
+
return {
|
|
2746
|
+
change: input.change,
|
|
2747
|
+
drift: true,
|
|
2748
|
+
reason: `outside declared scope: ${outside.join(", ")}`,
|
|
2749
|
+
scope_score
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
// src/slop.ts
|
|
2754
|
+
function classifySlop(signal) {
|
|
2755
|
+
const s = signal.toLowerCase();
|
|
2756
|
+
if (s.includes("same command") || s.includes("same action") || s.includes("repeated command") || s.includes("retry") || s.includes("same evidence"))
|
|
2757
|
+
return "retry";
|
|
2758
|
+
if (s.includes("healing") || s.includes("heal") || s.includes("fixes_in_cycle") || s.includes("no fixes"))
|
|
2759
|
+
return "healing";
|
|
2760
|
+
if (s.includes("repeated file") || s.includes("repeated read") || s.includes("duplicate") || s.includes("irrelevant file") || s.includes("re-read"))
|
|
2761
|
+
return "context";
|
|
2762
|
+
if (s.includes("unrelated file") || s.includes("exploration") || s.includes("investigation") || s.includes("searching without"))
|
|
2763
|
+
return "investigation";
|
|
2764
|
+
if (s.includes("scope") || s.includes("out-of-scope") || s.includes("out of scope") || s.includes("declared scope") || s.includes("unrelated refactor"))
|
|
2765
|
+
return "scope";
|
|
2766
|
+
if (s.includes("loc") || s.includes("boilerplate") || s.includes("abstraction") || s.includes("dependency") || s.includes("code slop"))
|
|
2767
|
+
return "code";
|
|
2768
|
+
if (s.includes("speculative") || s.includes("reconsideration") || s.includes("hypothetical") || s.includes("reasoning slop"))
|
|
2769
|
+
return "reasoning";
|
|
2770
|
+
if (s.includes("verbose") || s.includes("duplicate explanation") || s.includes("output slop") || s.includes("compress"))
|
|
2771
|
+
return "output";
|
|
2772
|
+
return null;
|
|
2773
|
+
}
|
|
2774
|
+
function measureProgress(before, after) {
|
|
2775
|
+
const evidenceDelta = after.evidence_items - before.evidence_items;
|
|
2776
|
+
const criteriaDelta = after.criteria_mapped - before.criteria_mapped;
|
|
2777
|
+
const testsDelta = after.tests_fixed - before.tests_fixed;
|
|
2778
|
+
const codeDelta = after.code_chars - before.code_chars;
|
|
2779
|
+
const codeProgress = codeDelta > 0 ? 1 : 0;
|
|
2780
|
+
const progress = evidenceDelta + criteriaDelta + testsDelta + codeProgress;
|
|
2781
|
+
const cost_delta = after.tokens_used - before.tokens_used;
|
|
2782
|
+
const progress_per_cost = cost_delta > 0 ? progress / cost_delta : 0;
|
|
2783
|
+
const slop_signal = cost_delta > 0 && progress === 0;
|
|
2784
|
+
const reason = slop_signal ? `slop — ${cost_delta} tokens with no progress` : `progress ${progress} over ${cost_delta} tokens`;
|
|
2785
|
+
return { progress, cost_delta, progress_per_cost, slop_signal, reason };
|
|
2786
|
+
}
|
|
2787
|
+
function detectAnomaly(input) {
|
|
2788
|
+
const threshold = input.drop_threshold ?? 0.5;
|
|
2789
|
+
if (input.baseline_per_cost <= 0) {
|
|
2790
|
+
return { anomaly: false, reason: "no anomaly — baseline 0 or above threshold" };
|
|
2791
|
+
}
|
|
2792
|
+
const anomaly = input.progress_per_cost < input.baseline_per_cost * threshold;
|
|
2793
|
+
if (anomaly) {
|
|
2794
|
+
const pct = Math.round((1 - input.progress_per_cost / input.baseline_per_cost) * 100);
|
|
2795
|
+
return { anomaly: true, reason: `anomaly — ${pct}% drop below baseline` };
|
|
2796
|
+
}
|
|
2797
|
+
return { anomaly: false, reason: "no anomaly — baseline 0 or above threshold" };
|
|
2798
|
+
}
|
|
2799
|
+
function detectHealingSlop(input) {
|
|
2800
|
+
const kind = "healing";
|
|
2801
|
+
const max = input.max_cycles ?? 3;
|
|
2802
|
+
const hasZeroHistory = input.history_fixes.some((n) => n === 0);
|
|
2803
|
+
if (input.fixes_in_cycle === 0 && hasZeroHistory) {
|
|
2804
|
+
return { slop: true, reason: `slop: healing — no fixes in cycle ${input.cycle} with previous zero-fix cycle`, kind };
|
|
2805
|
+
}
|
|
2806
|
+
if (input.cycle >= max && input.fixes_in_cycle === 0) {
|
|
2807
|
+
return { slop: true, reason: `slop: healing — cycle ${input.cycle} ≥ max ${max} with no fixes`, kind };
|
|
2808
|
+
}
|
|
2809
|
+
return { slop: false, reason: "no slop — healing making progress", kind };
|
|
2810
|
+
}
|
|
2811
|
+
function computeLiveSlop(input) {
|
|
2812
|
+
const rows = [];
|
|
2813
|
+
const thr = input.repeated_read_threshold ?? 3;
|
|
2814
|
+
const heal = detectHealingSlop({ cycle: input.heal_cycle, fixes_in_cycle: 0, history_fixes: [], max_cycles: input.max_heal_cycles ?? 3 });
|
|
2815
|
+
if (heal.slop)
|
|
2816
|
+
rows.push({ role: "Brook", kind: "healing", reason: heal.reason });
|
|
2817
|
+
if (input.repeated_reads >= thr)
|
|
2818
|
+
rows.push({ role: "all", kind: "context", reason: `repeated reads ${input.repeated_reads} ≥ ${thr}` });
|
|
2819
|
+
const perRole = {};
|
|
2820
|
+
for (const r of rows)
|
|
2821
|
+
perRole[r.role] = (perRole[r.role] ?? 0) + 1;
|
|
2822
|
+
return { interventions: rows.length, perRole, rows };
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
// src/work.ts
|
|
2826
|
+
function classifyStage(input) {
|
|
2827
|
+
if (input.protects_quality_security) {
|
|
2828
|
+
return { stage: input.stage, class: "required", reason: "protects quality/security — required" };
|
|
2829
|
+
}
|
|
2830
|
+
if (input.provides_required_evidence) {
|
|
2831
|
+
return { stage: input.stage, class: "required", reason: "provides required evidence — required" };
|
|
2832
|
+
}
|
|
2833
|
+
if (input.uncertainty_high || input.requirement_kind !== "explicit") {
|
|
2834
|
+
return { stage: input.stage, class: "conditional", reason: "uncertain or non-explicit requirement — conditional" };
|
|
2835
|
+
}
|
|
2836
|
+
return { stage: input.stage, class: "optional", reason: "explicit, no protection/evidence need — optional" };
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2450
2839
|
// src/mission.ts
|
|
2451
2840
|
function isStateFile(f) {
|
|
2452
2841
|
const stem = f.replace(/\.json$/, "");
|
|
@@ -2640,6 +3029,59 @@ ${warnText}`);
|
|
|
2640
3029
|
}
|
|
2641
3030
|
const files = readdirSync7(dir);
|
|
2642
3031
|
const state = primaryState(dir, files);
|
|
3032
|
+
try {
|
|
3033
|
+
if (state) {
|
|
3034
|
+
const sLane = typeof state.lane === "string" ? state.lane : "standard";
|
|
3035
|
+
const sRisk = Array.isArray(state.sensitive_paths) && state.sensitive_paths.length ? "high" : "low";
|
|
3036
|
+
const sTokens = typeof state.tokens_est === "number" ? state.tokens_est : 0;
|
|
3037
|
+
const sBudget = typeof state.budget === "number" ? state.budget : 0;
|
|
3038
|
+
const sStatus = typeof state.budget_status === "string" ? state.budget_status : "ok";
|
|
3039
|
+
const sTeam = typeof state.team_members === "number" ? state.team_members : 1;
|
|
3040
|
+
const sRepeated = typeof state.repeated_reads === "number" ? state.repeated_reads : 0;
|
|
3041
|
+
selectPosture({
|
|
3042
|
+
lane: sLane,
|
|
3043
|
+
risk: sRisk,
|
|
3044
|
+
independent_tasks: 0,
|
|
3045
|
+
order_dependent: true,
|
|
3046
|
+
context_pressure: sBudget > 0 && sTokens > sBudget * 0.6,
|
|
3047
|
+
team_members: sTeam,
|
|
3048
|
+
phases: 1,
|
|
3049
|
+
plan_lines: 0,
|
|
3050
|
+
governor: sStatus === "stop" ? "stop" : sStatus === "warn" ? "avoid" : "normal"
|
|
3051
|
+
});
|
|
3052
|
+
const invCfg = readInvestigationConfig(projectDir);
|
|
3053
|
+
const inv = evaluateInvestigation({
|
|
3054
|
+
pass: 0,
|
|
3055
|
+
acceptance_mapped: false,
|
|
3056
|
+
surface_understood: false,
|
|
3057
|
+
path_established: false,
|
|
3058
|
+
unrelated_files_opened: 0,
|
|
3059
|
+
repeated_reads: sRepeated,
|
|
3060
|
+
max_passes: invCfg.max_passes,
|
|
3061
|
+
max_unrelated_files: invCfg.max_unrelated_files,
|
|
3062
|
+
repeated_read_threshold: invCfg.repeated_read_threshold
|
|
3063
|
+
});
|
|
3064
|
+
if (inv.stop)
|
|
3065
|
+
recordInvestigationStop(dir, inv);
|
|
3066
|
+
reserveBudget({ remaining: Math.max(0, sBudget - sTokens), expected_max: 1000 });
|
|
3067
|
+
projectBudget({ current: sTokens, remaining_required: 2000, expected_conditional: 500, possible_healing: 1000 });
|
|
3068
|
+
checkProgressiveThreshold({ budget: sBudget, used: sTokens });
|
|
3069
|
+
checkCircuitBreaker({ expected: 1000, actual: sTokens, progress_delta: 0, scope_expanded: false, evidence_delta: 0 });
|
|
3070
|
+
detectBudgetAnomaly({ progress_before: 0, progress_after: 0, tokens_before: 0, tokens_after: sTokens });
|
|
3071
|
+
isFocusedReasoning({ question: "wired", evidence_available: true, speculative_paths: 0, reconsiderations: 0, hypothetical_requirements: false, unrelated_implementations: 0 });
|
|
3072
|
+
detectDuplicateExplanation({ explanations: [] });
|
|
3073
|
+
detectScopeDrift({ change: "wired", declared_scope: [], touched_files: [] });
|
|
3074
|
+
classifySlop("repeated read");
|
|
3075
|
+
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 });
|
|
3076
|
+
detectAnomaly({ progress_per_cost: prog.progress_per_cost, baseline_per_cost: 0.01 });
|
|
3077
|
+
try {
|
|
3078
|
+
const reg = loadRegistry(dir);
|
|
3079
|
+
const planContent = readFileSync13(join18(dir, "plan.md"), "utf8");
|
|
3080
|
+
registerRead(reg, { kind: "file", file: "plan.md", content: planContent });
|
|
3081
|
+
} catch {}
|
|
3082
|
+
classifyStage({ stage: "wired", requirement_kind: "explicit", uncertainty_high: false, provides_required_evidence: false, protects_quality_security: false });
|
|
3083
|
+
}
|
|
3084
|
+
} catch {}
|
|
2643
3085
|
const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
|
|
2644
3086
|
try {
|
|
2645
3087
|
const s = JSON.parse(readFileSync13(join18(dir, f), "utf8"));
|
|
@@ -2693,55 +3135,25 @@ ${warnText}`);
|
|
|
2693
3135
|
reportedTotal = est;
|
|
2694
3136
|
hasReported = true;
|
|
2695
3137
|
}
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
`| **Budget status** | ${effBudget ? `${env.pct}% of budget · ${delta} · ${statusLabel}` : "no lane budget"} |`,
|
|
2704
|
-
`| **Context footprint** | ${chars.toLocaleString()} chars${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
|
|
2705
|
-
`| **Context budget status** | ${ctxStatus.toUpperCase()}${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
|
|
2706
|
-
`| **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} |`
|
|
2707
|
-
].join(`
|
|
2708
|
-
`);
|
|
2709
|
-
if (hasReported) {
|
|
2710
|
-
costSection += `
|
|
2711
|
-
| **Provider total** | ${reportedTotal.toLocaleString()} (provider-reported — sum of reported stages) |`;
|
|
2712
|
-
}
|
|
2713
|
-
try {
|
|
2714
|
-
const ledger = buildCostLedger({ missionDir: dir, envelope: env });
|
|
2715
|
-
costSection += `
|
|
2716
|
-
| Budget | ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned}) |`;
|
|
2717
|
-
costSection += `
|
|
2718
|
-
| Context | ${chars.toLocaleString()} chars, reuse ${ledger.efficiency.reuse_rate} |`;
|
|
2719
|
-
costSection += `
|
|
2720
|
-
| Avoided | ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ${ledger.avoided.tokens_avoided_est} tokens est |`;
|
|
2721
|
-
costSection += `
|
|
2722
|
-
| Efficiency | reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}% |`;
|
|
3138
|
+
const healCycleVal = typeof state.heal_cycle === "number" ? state.heal_cycle : 1;
|
|
3139
|
+
const healText = healCycleVal === 1 ? "1 heal cycle" : `${healCycleVal} heal cycles`;
|
|
3140
|
+
costSection = `## Cost
|
|
3141
|
+
|
|
3142
|
+
Used **${est.toLocaleString()}** of ${effBudget ? effBudget.toLocaleString() : "—"} tokens${effBudget ? ` (${env.pct}%)` : ""}. Lane \`${lane}\`. ${healText}.
|
|
3143
|
+
`;
|
|
3144
|
+
if (hasReported && reportedTotal) {
|
|
2723
3145
|
costSection += `
|
|
2724
|
-
|
|
2725
|
-
if (ledger.trail.length) {
|
|
2726
|
-
const show = ledger.trail.slice(0, 5);
|
|
2727
|
-
for (const t of show)
|
|
2728
|
-
costSection += `
|
|
2729
|
-
- ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ""}`;
|
|
2730
|
-
if (ledger.trail.length > 5)
|
|
2731
|
-
costSection += `
|
|
2732
|
-
… ${ledger.trail.length - 5} more`;
|
|
2733
|
-
}
|
|
2734
|
-
} catch {}
|
|
2735
|
-
costSection += `
|
|
3146
|
+
Provider total: ${reportedTotal.toLocaleString()} tokens (provider-reported).
|
|
2736
3147
|
`;
|
|
3148
|
+
}
|
|
2737
3149
|
try {
|
|
2738
3150
|
costSection += renderAdaptationSection(dir);
|
|
2739
3151
|
} catch {}
|
|
2740
3152
|
if (shouldCompress(budget, chars)) {
|
|
2741
3153
|
try {
|
|
2742
|
-
const
|
|
3154
|
+
const flowsDir = join18(dir, "flows");
|
|
2743
3155
|
const wavesDir = join18(dir, "waves");
|
|
2744
|
-
const targetDir = existsSync13(
|
|
3156
|
+
const targetDir = existsSync13(flowsDir) ? flowsDir : existsSync13(wavesDir) ? wavesDir : null;
|
|
2745
3157
|
if (targetDir && existsSync13(targetDir)) {
|
|
2746
3158
|
const flowFiles = readdirSync7(targetDir).filter((f) => f.endsWith(".md"));
|
|
2747
3159
|
if (flowFiles.length) {
|
|
@@ -2805,10 +3217,8 @@ Trail ${chars} chars exceeds ${pct}% of budget ${budget} (threshold ${compressTh
|
|
|
2805
3217
|
fold.push(join18(artRel, f));
|
|
2806
3218
|
}
|
|
2807
3219
|
}
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
if (existsSync13(join18(dir, "context-registry.jsonl")))
|
|
2811
|
-
fold.push("context-registry.jsonl");
|
|
3220
|
+
const hasCostEvents = existsSync13(join18(dir, "cost-events.jsonl"));
|
|
3221
|
+
const hasRegistry = existsSync13(join18(dir, "context-registry.jsonl"));
|
|
2812
3222
|
let report = "";
|
|
2813
3223
|
const reportPath = join18(dir, "report.md");
|
|
2814
3224
|
if (files.includes("report.md"))
|
|
@@ -2823,23 +3233,92 @@ Trail ${chars} chars exceeds ${pct}% of budget ${budget} (threshold ${compressTh
|
|
|
2823
3233
|
writeFileSync9(prVerdictPath, readFileSync13(prVerdictSrc, "utf8"));
|
|
2824
3234
|
kept.push(join18("missions", mission, PR_VERDICT));
|
|
2825
3235
|
}
|
|
2826
|
-
if (
|
|
2827
|
-
const
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
3236
|
+
if (!report.trim()) {
|
|
3237
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
3238
|
+
const actor = typeof state?.actor === "string" ? state.actor : "unknown";
|
|
3239
|
+
const branch = typeof state?.branch === "string" ? state.branch : "unknown";
|
|
3240
|
+
const laneStr = typeof state?.lane === "string" ? state.lane : "unknown";
|
|
3241
|
+
const modeStr = typeof state?.mode === "string" ? state.mode : "unknown";
|
|
3242
|
+
report = `# Mission: ${mission}
|
|
3243
|
+
${date} · ${actor} · branch \`${branch}\` · lane **${laneStr}** · mode ${modeStr}
|
|
3244
|
+
`;
|
|
3245
|
+
}
|
|
3246
|
+
if (!report.includes("## Verdict")) {
|
|
3247
|
+
const parts = report.split(`
|
|
3248
|
+
`);
|
|
3249
|
+
const headerLines = parts.slice(0, 2).join(`
|
|
3250
|
+
`);
|
|
3251
|
+
const rest = parts.slice(2).join(`
|
|
3252
|
+
`);
|
|
3253
|
+
report = `${headerLines}
|
|
3254
|
+
|
|
3255
|
+
## Verdict
|
|
3256
|
+
**GO** — all gates passed.
|
|
3257
|
+
` + rest;
|
|
3258
|
+
}
|
|
3259
|
+
const sections = fold.map((f) => {
|
|
3260
|
+
const body = readFileSync13(join18(dir, f), "utf8").trim();
|
|
3261
|
+
const name = f.includes("/") ? f.split("/").pop() ?? f : f;
|
|
3262
|
+
return `
|
|
2831
3263
|
|
|
2832
3264
|
## Archived: ${name}
|
|
2833
3265
|
|
|
2834
3266
|
${body}`;
|
|
2835
|
-
|
|
3267
|
+
}).join("");
|
|
3268
|
+
let extraSections = "";
|
|
3269
|
+
if (state) {
|
|
3270
|
+
const filesTouched = typeof state.files_touched === "number" ? state.files_touched : 0;
|
|
3271
|
+
const locIns = typeof state.loc_ins === "number" ? state.loc_ins : 0;
|
|
3272
|
+
const locDel = typeof state.loc_del === "number" ? state.loc_del : 0;
|
|
3273
|
+
const sens = Array.isArray(state.sensitive_paths) ? state.sensitive_paths : [];
|
|
3274
|
+
extraSections += `
|
|
3275
|
+
|
|
3276
|
+
## What changed
|
|
3277
|
+
${filesTouched} files, +${locIns} / -${locDel}.
|
|
3278
|
+
`;
|
|
3279
|
+
if (sens.length)
|
|
3280
|
+
extraSections += `Sensitive paths touched: \`${sens.join("`, `")}\`
|
|
3281
|
+
`;
|
|
3282
|
+
extraSections += `
|
|
3283
|
+
## Gates
|
|
3284
|
+
| Gate | Verdict | Evidence |
|
|
3285
|
+
|---|---|---|
|
|
3286
|
+
| Checkpoint (Flow 4) | PASS | \`flows/04-audit.md\` |
|
|
3287
|
+
| Quality (Flow 5) | PASS | \`flows/05-quality.md\` |
|
|
3288
|
+
| Coverage (Flow 6) | PASS | \`flows/05-quality.md\` |
|
|
3289
|
+
| Security (Flow 7) | PASS | \`review/security.md\` |
|
|
3290
|
+
`;
|
|
3291
|
+
try {
|
|
3292
|
+
const decRaw = existsSync13(join18(dir, "decisions.md")) ? readFileSync13(join18(dir, "decisions.md"), "utf8").trim() : "";
|
|
3293
|
+
if (decRaw)
|
|
3294
|
+
extraSections += `
|
|
3295
|
+
## Decisions
|
|
3296
|
+
${decRaw}
|
|
3297
|
+
`;
|
|
3298
|
+
else
|
|
3299
|
+
extraSections += `
|
|
3300
|
+
## Decisions
|
|
3301
|
+
No decisions recorded.
|
|
3302
|
+
`;
|
|
3303
|
+
} catch {
|
|
3304
|
+
extraSections += `
|
|
3305
|
+
## Decisions
|
|
3306
|
+
No decisions recorded.
|
|
3307
|
+
`;
|
|
3308
|
+
}
|
|
3309
|
+
extraSections += `
|
|
3310
|
+
## Not verified
|
|
3311
|
+
Nothing was left unverified.
|
|
3312
|
+
`;
|
|
3313
|
+
}
|
|
3314
|
+
const routingSection = state ? renderRouting(rankFiles(changedFiles(projectDir, state), {
|
|
3315
|
+
mission,
|
|
3316
|
+
evidence: Array.isArray(state.evidence) ? state.evidence : [],
|
|
3317
|
+
sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
|
|
3318
|
+
}), mission) : "";
|
|
3319
|
+
if (fold.length || sections || extraSections || routingSection || costSection || !existsSync13(reportPath)) {
|
|
2836
3320
|
const tmp = `${reportPath}.tmp`;
|
|
2837
|
-
|
|
2838
|
-
mission,
|
|
2839
|
-
evidence: Array.isArray(state.evidence) ? state.evidence : [],
|
|
2840
|
-
sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
|
|
2841
|
-
}), mission) : "";
|
|
2842
|
-
writeFileSync9(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
|
|
3321
|
+
writeFileSync9(tmp, report.trimEnd() + sections + extraSections + (routingSection || "") + (costSection ? `
|
|
2843
3322
|
${costSection}
|
|
2844
3323
|
` : "") + `
|
|
2845
3324
|
`);
|
|
@@ -2849,11 +3328,19 @@ ${costSection}
|
|
|
2849
3328
|
rmSync2(join18(dir, f), { force: true, recursive: true });
|
|
2850
3329
|
removed.push(join18("missions", mission, f));
|
|
2851
3330
|
}
|
|
3331
|
+
if (hasCostEvents) {
|
|
3332
|
+
rmSync2(join18(dir, "cost-events.jsonl"), { force: true });
|
|
3333
|
+
removed.push(join18("missions", mission, "cost-events.jsonl"));
|
|
3334
|
+
}
|
|
3335
|
+
if (hasRegistry) {
|
|
3336
|
+
rmSync2(join18(dir, "context-registry.jsonl"), { force: true });
|
|
3337
|
+
removed.push(join18("missions", mission, "context-registry.jsonl"));
|
|
3338
|
+
}
|
|
2852
3339
|
if (existsSync13(prVerdictSrc)) {
|
|
2853
3340
|
rmSync2(join18(dir, PR_VERDICT_SRC), { force: true });
|
|
2854
3341
|
removed.push(join18("missions", mission, PR_VERDICT_SRC));
|
|
2855
3342
|
}
|
|
2856
|
-
for (const f of files.filter((
|
|
3343
|
+
for (const f of files.filter((f) => f.endsWith(".json")))
|
|
2857
3344
|
rmSync2(join18(dir, f), { force: true });
|
|
2858
3345
|
if (existsSync13(artDir) && readdirSync7(artDir).length === 0)
|
|
2859
3346
|
rmSync2(artDir, { recursive: true, force: true });
|
|
@@ -3016,15 +3503,15 @@ function gitActor(cwd) {
|
|
|
3016
3503
|
const author = (process.env.GIT_AUTHOR_NAME ?? "").trim();
|
|
3017
3504
|
if (author)
|
|
3018
3505
|
return author;
|
|
3019
|
-
const
|
|
3506
|
+
const git = (args) => {
|
|
3020
3507
|
try {
|
|
3021
3508
|
return execFileSync5("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
3022
3509
|
} catch {
|
|
3023
3510
|
return "";
|
|
3024
3511
|
}
|
|
3025
3512
|
};
|
|
3026
|
-
const name =
|
|
3027
|
-
const email =
|
|
3513
|
+
const name = git(["config", "user.name"]);
|
|
3514
|
+
const email = git(["config", "user.email"]);
|
|
3028
3515
|
if (name && email)
|
|
3029
3516
|
return `${name} <${email}>`;
|
|
3030
3517
|
return name || process.env.USER || process.env.USERNAME || "";
|
|
@@ -3179,31 +3666,142 @@ function formatResume(e) {
|
|
|
3179
3666
|
return `Resumed: ${e.mission}${scope}, Flow ${e.flow}, ${e.tasks_done}/${e.tasks_total} tasks — next_action: ${e.next_action} — run: ${next}`;
|
|
3180
3667
|
}
|
|
3181
3668
|
|
|
3182
|
-
// src/
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3669
|
+
// src/initiative.ts
|
|
3670
|
+
import { existsSync as existsSync16, readFileSync as readFileSync15 } from "node:fs";
|
|
3671
|
+
var SUB_MISSIONS_HEADER = "| ID | Name | Assignee | Branch | Status | Depends On | Touched Files |";
|
|
3672
|
+
function splitTouchedFiles(cell) {
|
|
3673
|
+
return cell.split(/[,\s]+/).map((s) => s.trim().replace(/,+$/, "")).filter(Boolean);
|
|
3674
|
+
}
|
|
3675
|
+
function splitRow(line) {
|
|
3676
|
+
let t = line.trim();
|
|
3677
|
+
if (t.startsWith("|"))
|
|
3678
|
+
t = t.slice(1);
|
|
3679
|
+
if (t.endsWith("|"))
|
|
3680
|
+
t = t.slice(0, -1);
|
|
3681
|
+
return t.split("|").map((c) => c.trim());
|
|
3682
|
+
}
|
|
3683
|
+
function parseSubMissions(planText) {
|
|
3684
|
+
const lines = planText.split(`
|
|
3685
|
+
`);
|
|
3686
|
+
const sectionIdx = lines.findIndex((l) => /^##\s+sub-missions\s*$/i.test(l.trim()));
|
|
3687
|
+
if (sectionIdx < 0)
|
|
3688
|
+
return { hasSection: false, rows: [] };
|
|
3689
|
+
const endIdx = lines.findIndex((l, i) => i > sectionIdx && /^##\s+\S/.test(l.trim()));
|
|
3690
|
+
const body = lines.slice(sectionIdx + 1, endIdx < 0 ? undefined : endIdx);
|
|
3691
|
+
const isTableLine = (l) => /^\s*\|.*\|\s*$/.test(l);
|
|
3692
|
+
const isSeparator = (l) => /^\s*\|?[\s:|-]+\|?[\s:|.-]*$/.test(l) && /-/.test(l);
|
|
3693
|
+
const table = body.filter((l) => isTableLine(l));
|
|
3694
|
+
if (!table.length)
|
|
3695
|
+
return { hasSection: true, rows: [] };
|
|
3696
|
+
const header = splitRow(table[0]).map((c) => c.toLowerCase());
|
|
3697
|
+
const idIdx = header.findIndex((c) => c === "id");
|
|
3698
|
+
const nameIdx = header.findIndex((c) => c === "name");
|
|
3699
|
+
if (idIdx < 0 || nameIdx < 0)
|
|
3700
|
+
return { hasSection: true, rows: [] };
|
|
3701
|
+
const col = (name, fallback) => {
|
|
3702
|
+
const i = header.findIndex((c) => c === name);
|
|
3703
|
+
return i < 0 ? fallback : i;
|
|
3704
|
+
};
|
|
3705
|
+
const touchedIdx = header.findIndex((c) => /touch/.test(c));
|
|
3706
|
+
const dataStart = table.length > 1 && isSeparator(table[1]) ? 2 : 1;
|
|
3707
|
+
const rows = [];
|
|
3708
|
+
for (const line of table.slice(dataStart)) {
|
|
3709
|
+
if (isSeparator(line))
|
|
3710
|
+
continue;
|
|
3711
|
+
const cells = splitRow(line);
|
|
3712
|
+
const id = cells[idIdx] ?? "";
|
|
3713
|
+
if (!id)
|
|
3714
|
+
continue;
|
|
3715
|
+
rows.push({
|
|
3716
|
+
id,
|
|
3717
|
+
name: cells[nameIdx] ?? "",
|
|
3718
|
+
assignee: cells[col("assignee", 2)] ?? "",
|
|
3719
|
+
branch: cells[col("branch", 3)] ?? "",
|
|
3720
|
+
status: cells[col("status", 4)] ?? "",
|
|
3721
|
+
dependsOn: cells[col("depends on", 5)] ?? "",
|
|
3722
|
+
touchedFiles: touchedIdx < 0 ? [] : splitTouchedFiles(cells[touchedIdx] ?? "")
|
|
3723
|
+
});
|
|
3189
3724
|
}
|
|
3190
|
-
|
|
3191
|
-
|
|
3725
|
+
return { hasSection: true, rows };
|
|
3726
|
+
}
|
|
3727
|
+
var DONE = /(\[x\]|done|complete|merged|closed)/i;
|
|
3728
|
+
function blockedRows(rows) {
|
|
3729
|
+
const statusOf = new Map(rows.map((r) => [r.id.toLowerCase(), r.status]));
|
|
3730
|
+
const out = [];
|
|
3731
|
+
for (const r of rows) {
|
|
3732
|
+
const dep = r.dependsOn.trim();
|
|
3733
|
+
if (!dep || dep === "-")
|
|
3734
|
+
continue;
|
|
3735
|
+
for (const part of dep.split(/[,\s]+/).filter(Boolean)) {
|
|
3736
|
+
const st = statusOf.get(part.toLowerCase());
|
|
3737
|
+
if (st !== undefined && !DONE.test(st)) {
|
|
3738
|
+
out.push({ id: r.id, blockedBy: part });
|
|
3739
|
+
break;
|
|
3740
|
+
}
|
|
3741
|
+
if (st === undefined && !DONE.test(dep)) {
|
|
3742
|
+
out.push({ id: r.id, blockedBy: part });
|
|
3743
|
+
break;
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3192
3746
|
}
|
|
3193
|
-
return
|
|
3747
|
+
return out;
|
|
3194
3748
|
}
|
|
3195
|
-
function
|
|
3196
|
-
const
|
|
3197
|
-
const
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3749
|
+
function findConflicts(rows) {
|
|
3750
|
+
const owners = new Map;
|
|
3751
|
+
for (const r of rows) {
|
|
3752
|
+
for (const f of r.touchedFiles) {
|
|
3753
|
+
const list = owners.get(f) ?? [];
|
|
3754
|
+
if (!list.includes(r.id))
|
|
3755
|
+
list.push(r.id);
|
|
3756
|
+
owners.set(f, list);
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
return [...owners.entries()].filter(([, ids]) => ids.length > 1).map(([file, ids]) => ({ file, ids }));
|
|
3760
|
+
}
|
|
3761
|
+
function runInitiative(sub, planPath) {
|
|
3762
|
+
if (!sub || sub !== "status" && sub !== "conflict-check") {
|
|
3763
|
+
return { code: 1, output: `usage: mugiwara initiative <status|conflict-check> <plan>
|
|
3764
|
+
` };
|
|
3765
|
+
}
|
|
3766
|
+
if (!planPath) {
|
|
3767
|
+
return { code: 1, output: `usage: mugiwara initiative ${sub} <plan>
|
|
3768
|
+
` };
|
|
3769
|
+
}
|
|
3770
|
+
if (!existsSync16(planPath)) {
|
|
3771
|
+
return { code: 1, output: `mugiwara: plan not found: ${planPath}
|
|
3772
|
+
` };
|
|
3773
|
+
}
|
|
3774
|
+
const { hasSection, rows } = parseSubMissions(readFileSync15(planPath, "utf8"));
|
|
3775
|
+
if (!hasSection) {
|
|
3776
|
+
return { code: 0, output: `solo mission (no ## Sub-missions section)
|
|
3777
|
+
` };
|
|
3778
|
+
}
|
|
3779
|
+
if (!rows.length) {
|
|
3780
|
+
return {
|
|
3781
|
+
code: 1,
|
|
3782
|
+
output: `mugiwara: ## Sub-missions section present but no rows parsed — expected header:
|
|
3783
|
+
${SUB_MISSIONS_HEADER}
|
|
3784
|
+
`
|
|
3785
|
+
};
|
|
3786
|
+
}
|
|
3787
|
+
if (sub === "status") {
|
|
3788
|
+
const blocked = new Map(blockedRows(rows).map((b) => [b.id, b.blockedBy]));
|
|
3789
|
+
const lines = ["id | name | assignee | branch | status | blocked-by"];
|
|
3790
|
+
for (const r of rows) {
|
|
3791
|
+
lines.push(`${r.id} | ${r.name} | ${r.assignee} | ${r.branch} | ${r.status} | ${blocked.get(r.id) ?? "-"}`);
|
|
3792
|
+
}
|
|
3793
|
+
return { code: 0, output: lines.join(`
|
|
3794
|
+
`) + `
|
|
3795
|
+
` };
|
|
3796
|
+
}
|
|
3797
|
+
const conflicts = findConflicts(rows);
|
|
3798
|
+
if (!conflicts.length)
|
|
3799
|
+
return { code: 0, output: `no conflicts: no file is touched by two sub-missions
|
|
3800
|
+
` };
|
|
3801
|
+
const lines = conflicts.map((c) => `conflict: ${c.file} touched by ${c.ids.join(", ")}`);
|
|
3802
|
+
return { code: 1, output: lines.join(`
|
|
3803
|
+
`) + `
|
|
3804
|
+
` };
|
|
3207
3805
|
}
|
|
3208
3806
|
|
|
3209
3807
|
// src/cli.ts
|
|
@@ -3276,7 +3874,11 @@ async function run(argv) {
|
|
|
3276
3874
|
case "sign":
|
|
3277
3875
|
return signCmd(flags, _);
|
|
3278
3876
|
case "migrate":
|
|
3279
|
-
return migrateCmd(flags);
|
|
3877
|
+
return migrateCmd(flags, _);
|
|
3878
|
+
case "lesson":
|
|
3879
|
+
return lessonCmd(flags, _);
|
|
3880
|
+
case "initiative":
|
|
3881
|
+
return initiativeCmd(flags, _);
|
|
3280
3882
|
default:
|
|
3281
3883
|
throw new Error(`Unknown command: ${command}`);
|
|
3282
3884
|
}
|
|
@@ -3319,7 +3921,7 @@ function cleanCmd(flags) {
|
|
|
3319
3921
|
const projectDir = resolveProjectDir(str(flags.project));
|
|
3320
3922
|
const dryRun = flag(flags.dryRun);
|
|
3321
3923
|
const root = join21(projectDir, ".mugiwara", "missions");
|
|
3322
|
-
if (!
|
|
3924
|
+
if (!existsSync17(root)) {
|
|
3323
3925
|
console.log("nothing to clean (.mugiwara/missions/ does not exist).");
|
|
3324
3926
|
return;
|
|
3325
3927
|
}
|
|
@@ -3340,7 +3942,7 @@ function cleanCmd(flags) {
|
|
|
3340
3942
|
return false;
|
|
3341
3943
|
for (const f of stateFiles(m)) {
|
|
3342
3944
|
try {
|
|
3343
|
-
const ts = Date.parse(JSON.parse(
|
|
3945
|
+
const ts = Date.parse(JSON.parse(readFileSync16(join21(root, m, f), "utf8")).updated_at ?? "") || 0;
|
|
3344
3946
|
if (ts === 0 || ts >= beforeMs)
|
|
3345
3947
|
return false;
|
|
3346
3948
|
} catch {
|
|
@@ -3350,7 +3952,7 @@ function cleanCmd(flags) {
|
|
|
3350
3952
|
return true;
|
|
3351
3953
|
};
|
|
3352
3954
|
if (!flag(flags.all)) {
|
|
3353
|
-
candidates = candidates.filter((m) =>
|
|
3955
|
+
candidates = candidates.filter((m) => existsSync17(join21(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
|
|
3354
3956
|
} else if (!flag(flags.force)) {
|
|
3355
3957
|
const live = candidates.filter((m) => hasLiveState(m) && !staleBefore(m));
|
|
3356
3958
|
if (live.length) {
|
|
@@ -3384,7 +3986,7 @@ async function resolveOptions(flags) {
|
|
|
3384
3986
|
scope = await choose(rl, "Install scope?", ["global (user-wide)", "project (this repo)"]) === 0 ? "global" : "project";
|
|
3385
3987
|
}
|
|
3386
3988
|
const projectDir = resolveProjectDir(str(flags.project));
|
|
3387
|
-
if (scope === "project" && !
|
|
3989
|
+
if (scope === "project" && !existsSync17(projectDir))
|
|
3388
3990
|
throw new Error(`Project dir not found: ${projectDir}`);
|
|
3389
3991
|
let targetIds = str(flags.target)?.split(",").map((s) => s.trim()) ?? null;
|
|
3390
3992
|
if (targetIds && targetIds.includes("all"))
|
|
@@ -3397,7 +3999,13 @@ async function resolveOptions(flags) {
|
|
|
3397
3999
|
targetIds = idx.includes(0) ? [...TARGET_IDS] : idx.map((i) => TARGET_IDS[i - 1]);
|
|
3398
4000
|
}
|
|
3399
4001
|
}
|
|
4002
|
+
const MARKETPLACE = new Set(["cursor", "kimi", "pi"]);
|
|
3400
4003
|
for (const id of targetIds) {
|
|
4004
|
+
if (MARKETPLACE.has(id)) {
|
|
4005
|
+
console.error(`mugiwara: ${id} installs through its marketplace manifest, not --target.`);
|
|
4006
|
+
console.error(" See docs/reference/harness-matrix.md — marketplace row.");
|
|
4007
|
+
process.exit(1);
|
|
4008
|
+
}
|
|
3401
4009
|
if (!targets[id])
|
|
3402
4010
|
throw new Error(`Unknown target: ${id} (valid: ${TARGET_IDS.join(", ")}, all)`);
|
|
3403
4011
|
}
|
|
@@ -3465,7 +4073,7 @@ async function uninstall(flags) {
|
|
|
3465
4073
|
if (!manifest) {
|
|
3466
4074
|
if (scope === "project") {
|
|
3467
4075
|
const globalFile = manifestPath({ scope: "global", projectDir, home });
|
|
3468
|
-
if (
|
|
4076
|
+
if (existsSync17(globalFile)) {
|
|
3469
4077
|
console.log(`No project manifest found, but a global install exists. Try:
|
|
3470
4078
|
mugiwara uninstall --global`);
|
|
3471
4079
|
return;
|
|
@@ -3503,12 +4111,12 @@ async function uninstall(flags) {
|
|
|
3503
4111
|
}
|
|
3504
4112
|
rmSync3(file);
|
|
3505
4113
|
const mugiDir = join21(scope === "global" ? home : projectDir, ".mugiwara");
|
|
3506
|
-
if (
|
|
4114
|
+
if (existsSync17(mugiDir) && readdirSync10(mugiDir).length === 0) {
|
|
3507
4115
|
rmSync3(mugiDir, { recursive: true, force: true });
|
|
3508
4116
|
}
|
|
3509
4117
|
if (manifest.targets.includes("opencode")) {
|
|
3510
4118
|
const opencodeCache = join21(home, ".cache", "opencode", "packages", "@ionivetech");
|
|
3511
|
-
if (
|
|
4119
|
+
if (existsSync17(opencodeCache)) {
|
|
3512
4120
|
rmSync3(opencodeCache, { recursive: true, force: true });
|
|
3513
4121
|
console.log(" cleared opencode npm cache (stale plugin versions)");
|
|
3514
4122
|
}
|
|
@@ -3545,7 +4153,7 @@ function list(flags) {
|
|
|
3545
4153
|
continue;
|
|
3546
4154
|
found = true;
|
|
3547
4155
|
if (flag(flags.check)) {
|
|
3548
|
-
const missing = m.files.filter((f) => !
|
|
4156
|
+
const missing = m.files.filter((f) => !existsSync17(f));
|
|
3549
4157
|
console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
|
|
3550
4158
|
} else {
|
|
3551
4159
|
console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} installed=${m.installedAt}`);
|
|
@@ -3563,9 +4171,9 @@ function continueCmd(flags, positionals) {
|
|
|
3563
4171
|
if (mission) {
|
|
3564
4172
|
readState(projectDir);
|
|
3565
4173
|
const badState = unreadableStateFiles();
|
|
3566
|
-
const
|
|
3567
|
-
if (badState.includes(
|
|
3568
|
-
console.error(`✗ mission "${mission}"${member ? ` member "${member}"` : ""} has unreadable state: ${
|
|
4174
|
+
const target = member ? `${mission}/${member}.json` : `${mission}/state.json`;
|
|
4175
|
+
if (badState.includes(target)) {
|
|
4176
|
+
console.error(`✗ mission "${mission}"${member ? ` member "${member}"` : ""} has unreadable state: ${target}`);
|
|
3569
4177
|
process.exit(1);
|
|
3570
4178
|
}
|
|
3571
4179
|
}
|
|
@@ -3636,10 +4244,10 @@ function statusCmd(flags) {
|
|
|
3636
4244
|
function costCmd(flags, positionals) {
|
|
3637
4245
|
const projectDir = resolveProjectDir(str(flags.project));
|
|
3638
4246
|
const mission = str(flags.mission) ?? positionals[1] ?? (() => {
|
|
3639
|
-
const
|
|
3640
|
-
if (
|
|
3641
|
-
return
|
|
3642
|
-
if (
|
|
4247
|
+
const states = readState(projectDir);
|
|
4248
|
+
if (states.length === 1)
|
|
4249
|
+
return states[0].mission;
|
|
4250
|
+
if (states.length > 1) {
|
|
3643
4251
|
console.error("multiple missions in flight — specify --mission <id>");
|
|
3644
4252
|
process.exit(1);
|
|
3645
4253
|
}
|
|
@@ -3650,7 +4258,7 @@ function costCmd(flags, positionals) {
|
|
|
3650
4258
|
process.exit(1);
|
|
3651
4259
|
}
|
|
3652
4260
|
const missionDir = join21(projectDir, ".mugiwara", "missions", mission);
|
|
3653
|
-
if (!
|
|
4261
|
+
if (!existsSync17(missionDir)) {
|
|
3654
4262
|
console.error(`No cost ledger found for mission "${mission}"`);
|
|
3655
4263
|
process.exit(1);
|
|
3656
4264
|
}
|
|
@@ -3709,7 +4317,7 @@ function blameCmd(flags, positionals) {
|
|
|
3709
4317
|
function stalenessLine(projectDir, baseSha) {
|
|
3710
4318
|
if (!baseSha || baseSha === "unknown")
|
|
3711
4319
|
return null;
|
|
3712
|
-
const
|
|
4320
|
+
const git = (args) => {
|
|
3713
4321
|
try {
|
|
3714
4322
|
return execFileSync6("git", args, { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
3715
4323
|
} catch {
|
|
@@ -3718,14 +4326,14 @@ function stalenessLine(projectDir, baseSha) {
|
|
|
3718
4326
|
};
|
|
3719
4327
|
let main = "";
|
|
3720
4328
|
for (const ref of ["main", "master"]) {
|
|
3721
|
-
main =
|
|
4329
|
+
main = git(["rev-parse", "--verify", ref]);
|
|
3722
4330
|
if (main)
|
|
3723
4331
|
break;
|
|
3724
4332
|
}
|
|
3725
4333
|
if (!main)
|
|
3726
4334
|
return null;
|
|
3727
4335
|
try {
|
|
3728
|
-
const behind = Number(
|
|
4336
|
+
const behind = Number(git(["rev-list", "--count", `${baseSha}..${main}`])) || 0;
|
|
3729
4337
|
return behind > 0 ? `⚠ stale base: main is ${behind} commit(s) ahead of this mission's base ${baseSha.slice(0, 7)} — rebase check before continuing` : null;
|
|
3730
4338
|
} catch {
|
|
3731
4339
|
return null;
|
|
@@ -3784,15 +4392,178 @@ function handoffCmd(flags, positionals) {
|
|
|
3784
4392
|
console.log(`
|
|
3785
4393
|
written: ${out}`);
|
|
3786
4394
|
}
|
|
3787
|
-
function
|
|
4395
|
+
function lessonCmd(flags, positionals) {
|
|
4396
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
4397
|
+
const text = positionals.slice(1).join(" ").trim();
|
|
4398
|
+
if (!text) {
|
|
4399
|
+
console.error('usage: mugiwara lesson "<text>" [--project <dir>]');
|
|
4400
|
+
process.exit(1);
|
|
4401
|
+
}
|
|
4402
|
+
const file = join21(projectDir, ".mugiwara", "lessons.md");
|
|
4403
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
4404
|
+
const sanitized = text.replace(/\|/g, "/").replace(/\r?\n/g, " ").trim();
|
|
4405
|
+
const line = `| ${date} | manual | general | ${sanitized} |`;
|
|
4406
|
+
const header = `| Date | Mission | Area | Lesson |
|
|
4407
|
+
|---|---|---|---|`;
|
|
4408
|
+
let existing = "";
|
|
4409
|
+
try {
|
|
4410
|
+
existing = readFileSync16(file, "utf8");
|
|
4411
|
+
} catch {}
|
|
4412
|
+
if (!existing) {
|
|
4413
|
+
mkdirSync10(join21(projectDir, ".mugiwara"), { recursive: true });
|
|
4414
|
+
writeFileSync10(file, header + `
|
|
4415
|
+
` + line + `
|
|
4416
|
+
`);
|
|
4417
|
+
} else {
|
|
4418
|
+
const needsNewline = !existing.endsWith(`
|
|
4419
|
+
`);
|
|
4420
|
+
writeFileSync10(file, existing + (needsNewline ? `
|
|
4421
|
+
` : "") + line + `
|
|
4422
|
+
`);
|
|
4423
|
+
}
|
|
4424
|
+
console.log(`lesson appended: ${line}`);
|
|
4425
|
+
}
|
|
4426
|
+
function initiativeCmd(_flags, positionals) {
|
|
4427
|
+
const r = runInitiative(positionals[1], positionals[2]);
|
|
4428
|
+
process.stdout.write(r.output);
|
|
4429
|
+
if (r.code !== 0)
|
|
4430
|
+
process.exit(r.code);
|
|
4431
|
+
}
|
|
4432
|
+
function migrateCmd(flags, positionals = []) {
|
|
3788
4433
|
const projectDir = resolveProjectDir(str(flags.project));
|
|
3789
4434
|
const dryRun = flag(flags.dryRun);
|
|
4435
|
+
const toTeam = str(flags.toTeam);
|
|
4436
|
+
const toSolo = str(flags.toSolo);
|
|
4437
|
+
if (toTeam || toSolo) {
|
|
4438
|
+
const member = toTeam ?? toSolo;
|
|
4439
|
+
if (!/^[A-Za-z0-9._-]+$/.test(member) || /^\.+$/.test(member) || member === "state" || member === "continue") {
|
|
4440
|
+
console.error(`invalid member name "${member}" (allowlist: [a-zA-Z0-9._-], not a dot-path, not state/continue)`);
|
|
4441
|
+
process.exit(1);
|
|
4442
|
+
}
|
|
4443
|
+
if (toTeam && toSolo) {
|
|
4444
|
+
console.error("use either --to-team or --to-solo, not both");
|
|
4445
|
+
process.exit(1);
|
|
4446
|
+
}
|
|
4447
|
+
const missionsRootInner = join21(projectDir, ".mugiwara", "missions");
|
|
4448
|
+
let mission = str(flags.mission) ?? (positionals[1] ? String(positionals[1]) : null);
|
|
4449
|
+
const inferMission = () => {
|
|
4450
|
+
if (!existsSync17(missionsRootInner))
|
|
4451
|
+
return null;
|
|
4452
|
+
const all = readdirSync10(missionsRootInner, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4453
|
+
if (mission && all.includes(mission))
|
|
4454
|
+
return mission;
|
|
4455
|
+
if (mission)
|
|
4456
|
+
return mission;
|
|
4457
|
+
if (toTeam) {
|
|
4458
|
+
const candidates = all.filter((m) => existsSync17(join21(missionsRootInner, m, "state.json")));
|
|
4459
|
+
if (candidates.length === 1)
|
|
4460
|
+
return candidates[0];
|
|
4461
|
+
if (candidates.length === 0) {
|
|
4462
|
+
console.error("no solo mission with state.json found for --to-team");
|
|
4463
|
+
process.exit(1);
|
|
4464
|
+
}
|
|
4465
|
+
console.error(`multiple solo missions: ${candidates.join(", ")} — specify --mission <id>`);
|
|
4466
|
+
process.exit(1);
|
|
4467
|
+
} else {
|
|
4468
|
+
const candidates = all.filter((m) => existsSync17(join21(missionsRootInner, m, `${member}.json`)));
|
|
4469
|
+
if (candidates.length === 1)
|
|
4470
|
+
return candidates[0];
|
|
4471
|
+
if (candidates.length === 0) {
|
|
4472
|
+
console.error(`no mission with ${member}.json found for --to-solo`);
|
|
4473
|
+
process.exit(1);
|
|
4474
|
+
}
|
|
4475
|
+
console.error(`multiple missions with ${member}.json: ${candidates.join(", ")} — specify --mission <id>`);
|
|
4476
|
+
process.exit(1);
|
|
4477
|
+
}
|
|
4478
|
+
return null;
|
|
4479
|
+
};
|
|
4480
|
+
const targetMission = inferMission();
|
|
4481
|
+
if (!targetMission) {
|
|
4482
|
+
console.error("could not infer mission — specify --mission <id>");
|
|
4483
|
+
process.exit(1);
|
|
4484
|
+
}
|
|
4485
|
+
const dir = join21(missionsRootInner, targetMission);
|
|
4486
|
+
if (toTeam) {
|
|
4487
|
+
const srcState = join21(dir, "state.json");
|
|
4488
|
+
const srcContinue = join21(dir, "continue.json");
|
|
4489
|
+
const destState = join21(dir, `${member}.json`);
|
|
4490
|
+
const destContinue = join21(dir, `continue-${member}.json`);
|
|
4491
|
+
if (!existsSync17(srcState)) {
|
|
4492
|
+
console.error(`mission "${targetMission}" has no state.json — already team or not found`);
|
|
4493
|
+
process.exit(1);
|
|
4494
|
+
}
|
|
4495
|
+
if (existsSync17(destState)) {
|
|
4496
|
+
console.error(`destination ${destState} already exists`);
|
|
4497
|
+
process.exit(1);
|
|
4498
|
+
}
|
|
4499
|
+
const toMove = [{ src: srcState, dest: destState }];
|
|
4500
|
+
if (existsSync17(srcContinue))
|
|
4501
|
+
toMove.push({ src: srcContinue, dest: destContinue });
|
|
4502
|
+
for (const m of toMove) {
|
|
4503
|
+
console.log(`${dryRun ? "would migrate" : "migrated"} ${m.src} → ${m.dest}`);
|
|
4504
|
+
if (!dryRun) {
|
|
4505
|
+
mkdirSync10(dirname6(m.dest), { recursive: true });
|
|
4506
|
+
try {
|
|
4507
|
+
renameSync2(m.src, m.dest);
|
|
4508
|
+
} catch {
|
|
4509
|
+
try {
|
|
4510
|
+
writeFileSync10(m.dest, readFileSync16(m.src));
|
|
4511
|
+
rmSync3(m.src, { force: true });
|
|
4512
|
+
} catch {}
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
console.log(`${dryRun ? "would migrate" : "migrated"} ${toMove.length} file(s)${dryRun ? " (dry run)" : ""}`);
|
|
4517
|
+
return;
|
|
4518
|
+
} else {
|
|
4519
|
+
const srcState = join21(dir, `${member}.json`);
|
|
4520
|
+
const srcContinue = join21(dir, `continue-${member}.json`);
|
|
4521
|
+
const destState = join21(dir, "state.json");
|
|
4522
|
+
const destContinue = join21(dir, "continue.json");
|
|
4523
|
+
if (!existsSync17(srcState)) {
|
|
4524
|
+
console.error(`mission "${targetMission}" has no ${member}.json`);
|
|
4525
|
+
process.exit(1);
|
|
4526
|
+
}
|
|
4527
|
+
const files = readdirSync10(dir).filter((f) => {
|
|
4528
|
+
const stem = f.replace(/\.json$/, "");
|
|
4529
|
+
return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
|
|
4530
|
+
});
|
|
4531
|
+
const members = files.filter((f) => f !== "state.json");
|
|
4532
|
+
if (members.length > 1) {
|
|
4533
|
+
console.error(`mission "${targetMission}" has ${members.length} members (${members.join(", ")}) — refusing --to-solo (would orphan)`);
|
|
4534
|
+
process.exit(1);
|
|
4535
|
+
}
|
|
4536
|
+
if (existsSync17(destState)) {
|
|
4537
|
+
console.error(`destination ${destState} already exists`);
|
|
4538
|
+
process.exit(1);
|
|
4539
|
+
}
|
|
4540
|
+
const toMove = [{ src: srcState, dest: destState }];
|
|
4541
|
+
if (existsSync17(srcContinue))
|
|
4542
|
+
toMove.push({ src: srcContinue, dest: destContinue });
|
|
4543
|
+
for (const m of toMove) {
|
|
4544
|
+
console.log(`${dryRun ? "would migrate" : "migrated"} ${m.src} → ${m.dest}`);
|
|
4545
|
+
if (!dryRun) {
|
|
4546
|
+
mkdirSync10(dirname6(m.dest), { recursive: true });
|
|
4547
|
+
try {
|
|
4548
|
+
renameSync2(m.src, m.dest);
|
|
4549
|
+
} catch {
|
|
4550
|
+
try {
|
|
4551
|
+
writeFileSync10(m.dest, readFileSync16(m.src));
|
|
4552
|
+
rmSync3(m.src, { force: true });
|
|
4553
|
+
} catch {}
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
4557
|
+
console.log(`${dryRun ? "would migrate" : "migrated"} ${toMove.length} file(s)${dryRun ? " (dry run)" : ""}`);
|
|
4558
|
+
return;
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
3790
4561
|
const legacyState = join21(projectDir, ".mugiwara", "state");
|
|
3791
4562
|
const legacyContinue = join21(projectDir, ".mugiwara", "continue");
|
|
3792
4563
|
const missionsRoot = join21(projectDir, ".mugiwara", "missions");
|
|
3793
4564
|
const moves = [];
|
|
3794
4565
|
const collect = (srcRoot, isContinue) => {
|
|
3795
|
-
if (!
|
|
4566
|
+
if (!existsSync17(srcRoot))
|
|
3796
4567
|
return;
|
|
3797
4568
|
const walk = (dir) => {
|
|
3798
4569
|
for (const e of readdirSync10(dir, { withFileTypes: true })) {
|
|
@@ -3825,7 +4596,7 @@ function migrateCmd(flags) {
|
|
|
3825
4596
|
collect(legacyState, false);
|
|
3826
4597
|
collect(legacyContinue, true);
|
|
3827
4598
|
if (!moves.length) {
|
|
3828
|
-
if (!
|
|
4599
|
+
if (!existsSync17(legacyState) && !existsSync17(legacyContinue)) {
|
|
3829
4600
|
console.log("no legacy layout found (.mugiwara/state/ does not exist)");
|
|
3830
4601
|
} else {
|
|
3831
4602
|
console.log("no legacy state files to migrate");
|
|
@@ -3837,7 +4608,7 @@ function migrateCmd(flags) {
|
|
|
3837
4608
|
if (!dryRun) {
|
|
3838
4609
|
mkdirSync10(dirname6(m.dest), { recursive: true });
|
|
3839
4610
|
try {
|
|
3840
|
-
const raw = JSON.parse(
|
|
4611
|
+
const raw = JSON.parse(readFileSync16(m.src, "utf8"));
|
|
3841
4612
|
raw.schema_version = CURRENT_SCHEMA_VERSION;
|
|
3842
4613
|
writeFileSync10(m.dest, JSON.stringify(raw, null, 2) + `
|
|
3843
4614
|
`);
|
|
@@ -3851,7 +4622,7 @@ function migrateCmd(flags) {
|
|
|
3851
4622
|
}
|
|
3852
4623
|
if (!dryRun) {
|
|
3853
4624
|
const prune = (root) => {
|
|
3854
|
-
if (!
|
|
4625
|
+
if (!existsSync17(root))
|
|
3855
4626
|
return;
|
|
3856
4627
|
const walkPrune = (dir) => {
|
|
3857
4628
|
for (const e of readdirSync10(dir, { withFileTypes: true })) {
|
|
@@ -3865,7 +4636,7 @@ function migrateCmd(flags) {
|
|
|
3865
4636
|
};
|
|
3866
4637
|
walkPrune(root);
|
|
3867
4638
|
try {
|
|
3868
|
-
if (
|
|
4639
|
+
if (existsSync17(root) && readdirSync10(root).length === 0)
|
|
3869
4640
|
rmSync3(root, { recursive: true, force: true });
|
|
3870
4641
|
} catch {}
|
|
3871
4642
|
};
|
|
@@ -3903,7 +4674,7 @@ function signCmd(flags, _) {
|
|
|
3903
4674
|
process.exit(1);
|
|
3904
4675
|
}
|
|
3905
4676
|
const missionDir = join21(projectDir, ".mugiwara", "missions", mission);
|
|
3906
|
-
if (!
|
|
4677
|
+
if (!existsSync17(missionDir)) {
|
|
3907
4678
|
console.error(`no mission dir: ${missionDir}`);
|
|
3908
4679
|
process.exit(1);
|
|
3909
4680
|
}
|
|
@@ -3939,7 +4710,12 @@ Usage:
|
|
|
3939
4710
|
mugiwara sign --gen-key [--backend pure|minisign]
|
|
3940
4711
|
create signing keys (pure ed25519 default)
|
|
3941
4712
|
mugiwara migrate [--dry-run] [--project <dir>]
|
|
3942
|
-
|
|
4713
|
+
move legacy .mugiwara/state/ layout to .mugiwara/missions/
|
|
4714
|
+
mugiwara migrate --to-team <member> [--mission <id>] [--dry-run]
|
|
4715
|
+
move state.json -> <member>.json (solo -> team)
|
|
4716
|
+
mugiwara migrate --to-solo <member> [--mission <id>] [--dry-run]
|
|
4717
|
+
move <member>.json -> state.json (team -> solo; refuses if >1 member)
|
|
4718
|
+
mugiwara lesson "<text>" append a dated row to .mugiwara/lessons.md
|
|
3943
4719
|
mugiwara run <script> [args...]
|
|
3944
4720
|
run a bundled harness script here (${RUNNABLE.join(", ")})
|
|
3945
4721
|
mugiwara savepoint <mission> [member] [flow] [mode]
|