@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/.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/README.md +61 -56
- package/content/agents/luffy-orchestrator.md +1 -0
- package/content/skills/mugiwara-lessons/SKILL.md +2 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +11 -20
- 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-workflow/SKILL.md +20 -20
- package/dist/mugiwara.js +863 -133
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/references/multi-actor.md +17 -14
- package/scripts/gate-selftest.ts +294 -1
- package/scripts/lane-base.ts +16 -0
- package/scripts/lane.sh +5 -1
- package/scripts/lib/lane-base.sh +1 -1
- package/scripts/savepoint.sh +151 -15
- package/scripts/validate-content.ts +168 -0
- package/src/args.ts +1 -1
- package/src/cli.ts +203 -21
- package/src/config.ts +33 -11
- package/src/continue.ts +7 -1
- package/src/cost.ts +1 -1
- package/src/installer.ts +27 -4
- package/src/integrity.ts +41 -10
- package/src/mission.ts +137 -52
- package/src/policy.ts +17 -2
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
|
-
"
|
|
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
|
-
"
|
|
646
|
-
"
|
|
647
|
-
"
|
|
648
|
-
"
|
|
649
|
-
"
|
|
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
|
|
653
|
-
"# enforce=block
|
|
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
|
-
|
|
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
|
|
1295
|
-
|
|
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
|
|
1557
|
-
|
|
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
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2740
|
-
|
|
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 (
|
|
2758
|
-
const
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
3681
|
-
const projectDir =
|
|
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 =
|
|
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
|
-
|
|
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]
|