@ionivetech/mugiwara 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +2 -2
- package/README.md +194 -328
- package/content/agents/franky-gates.md +1 -1
- package/content/agents/luffy-orchestrator.md +2 -2
- package/content/skills/mugiwara-backend/SKILL.md +52 -43
- package/content/skills/mugiwara-checkpoint/SKILL.md +19 -8
- package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
- package/content/skills/mugiwara-execution/SKILL.md +32 -32
- package/content/skills/mugiwara-execution/references/execution-phase-flows.md +18 -0
- package/content/skills/mugiwara-frontend/SKILL.md +44 -44
- package/content/skills/mugiwara-gates/SKILL.md +22 -16
- package/content/skills/mugiwara-healing/SKILL.md +26 -25
- package/content/skills/mugiwara-orchestration/SKILL.md +6 -6
- package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
- package/content/skills/mugiwara-planning/SKILL.md +26 -14
- package/content/skills/mugiwara-planning/references/large-campaign-subplan.md +41 -0
- package/content/skills/mugiwara-planning/references/plan-template.md +22 -0
- package/content/skills/mugiwara-quality/SKILL.md +19 -13
- package/content/skills/mugiwara-resume/SKILL.md +6 -1
- package/content/skills/mugiwara-review/SKILL.md +17 -12
- package/content/skills/mugiwara-security/SKILL.md +46 -35
- package/content/skills/mugiwara-workflow/SKILL.md +6 -9
- package/content/skills/mugiwara-workflow/references/adaptive-budget-governor.md +5 -0
- package/content/skills/mugiwara-workflow/references/benchmark-governor.md +53 -0
- package/content/skills/mugiwara-workflow/references/cognitive-output-governor.md +5 -0
- package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
- package/content/skills/mugiwara-workflow/references/scope-code-governor.md +14 -0
- package/content/skills/mugiwara-workflow/references/stop-slop-governor.md +14 -0
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +6 -3
- package/dist/mugiwara.js +925 -253
- package/gemini-extension.json +1 -1
- package/hooks/pipeline-guard.js +1 -1
- package/hooks/pipeline-guard.ts +2 -1
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/multi-actor.md +21 -0
- package/references/posture-routing.md +31 -0
- package/scripts/benchmark-governor.ts +516 -0
- package/scripts/benchmark-thresholds.json +47 -0
- package/scripts/check-doc-links.ts +8 -2
- package/scripts/gate-selftest.ts +20 -0
- package/scripts/lib/lane-base.sh +4 -4
- package/scripts/retrieval-eval.ts +9 -3
- package/scripts/savepoint.sh +20 -1
- package/scripts/validate-content.ts +22 -3
- package/src/adaptive-budget.ts +178 -0
- package/src/args.ts +3 -2
- package/src/budget.ts +7 -16
- package/src/check-artifacts.ts +45 -0
- package/src/cli.ts +102 -4
- package/src/cognition.ts +234 -0
- package/src/config.ts +107 -0
- package/src/context.ts +72 -0
- package/src/cost.ts +186 -0
- package/src/evidence.ts +160 -0
- package/src/installer.ts +2 -16
- package/src/integrity.ts +1 -1
- package/src/investigation.ts +72 -0
- package/src/mission.ts +124 -10
- package/src/posture.ts +86 -0
- package/src/reporting.ts +225 -0
- package/src/scope.ts +321 -0
- package/src/sign.ts +194 -20
- package/src/slop.ts +306 -0
- package/src/work.ts +273 -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 existsSync15, readdirSync as readdirSync10, readFileSync as readFileSync14, rmSync as rmSync3, writeFileSync as writeFileSync10 } from "node:fs";
|
|
5
5
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
6
6
|
import { homedir as homedir4 } from "node:os";
|
|
7
|
-
import { join as
|
|
7
|
+
import { join as join20, 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" };
|
|
11
|
+
var VALUE_FLAGS = { "--project": "project", "--target": "target", "--before": "before", "--backend": "backend", "--mission": "mission" };
|
|
12
12
|
var BOOL_FLAGS = {
|
|
13
13
|
"--global": "global",
|
|
14
14
|
"--yes": "yes",
|
|
@@ -19,10 +19,13 @@ var BOOL_FLAGS = {
|
|
|
19
19
|
"--check": "check",
|
|
20
20
|
"--all": "all",
|
|
21
21
|
"--verify": "verify",
|
|
22
|
+
"--gen-key": "genKey",
|
|
22
23
|
"--help": "help",
|
|
23
24
|
"-h": "help",
|
|
24
25
|
"--version": "version",
|
|
25
|
-
"-v": "version"
|
|
26
|
+
"-v": "version",
|
|
27
|
+
"--json": "json",
|
|
28
|
+
"--ledger": "ledger"
|
|
26
29
|
};
|
|
27
30
|
function parseArgs(argv) {
|
|
28
31
|
const out = { _: [], flags: {}, command: "install" };
|
|
@@ -620,50 +623,118 @@ var targets = { claude: target, opencode: target2, copilot: target3, gemini: tar
|
|
|
620
623
|
var TARGET_IDS = Object.keys(targets);
|
|
621
624
|
|
|
622
625
|
// src/installer.ts
|
|
623
|
-
import { existsSync as
|
|
624
|
-
import { dirname as dirname3, join as
|
|
625
|
-
import { homedir } from "node:os";
|
|
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";
|
|
627
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
628
|
+
import { homedir as homedir2 } from "node:os";
|
|
626
629
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
627
|
-
|
|
628
|
-
|
|
630
|
+
|
|
631
|
+
// src/config.ts
|
|
632
|
+
import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
633
|
+
import { homedir } from "node:os";
|
|
634
|
+
import { join as join5 } from "node:path";
|
|
635
|
+
var DEFAULT_CONFIG = [
|
|
636
|
+
"mode=guided",
|
|
637
|
+
"branch=feature/{type}-{issue}-{slug}",
|
|
638
|
+
"commit=conventional",
|
|
639
|
+
"auto_commit=on",
|
|
640
|
+
"coverage_new=85",
|
|
641
|
+
"coverage_modified=90",
|
|
642
|
+
"review_depth=full",
|
|
643
|
+
"quality_depth=full",
|
|
644
|
+
"verify_merged=off",
|
|
645
|
+
"delegate_threshold=60",
|
|
646
|
+
"heal_max_cycles=3",
|
|
647
|
+
"verbosity=normal",
|
|
648
|
+
"# context_budget_chars=150000 # optional: fail archive if trail exceeds this (measured in report Cost section)",
|
|
649
|
+
"# investigation_max_passes=2 # optional: cap investigation passes (spec §13)",
|
|
650
|
+
"# investigation_max_unrelated_files=5",
|
|
651
|
+
"# investigation_repeated_read_threshold=2",
|
|
652
|
+
"# sign=auto # optional: auto | minisign | pure | off — report attestation",
|
|
653
|
+
"# enforce=block # optional: off | warn | block — pipeline-guard policy"
|
|
654
|
+
].join(`
|
|
655
|
+
`) + `
|
|
656
|
+
`;
|
|
657
|
+
function configPaths(projectDir) {
|
|
658
|
+
return [join5(projectDir, ".mugiwara", "config"), join5(homedir(), ".mugiwara", "config")];
|
|
659
|
+
}
|
|
660
|
+
function readConfig(projectDir) {
|
|
661
|
+
const out = {};
|
|
662
|
+
for (const file of configPaths(projectDir)) {
|
|
663
|
+
if (!existsSync4(file))
|
|
664
|
+
continue;
|
|
665
|
+
for (const line of readFileSync3(file, "utf8").split(/\r?\n/)) {
|
|
666
|
+
const t = line.trim();
|
|
667
|
+
if (!t || t.startsWith("#"))
|
|
668
|
+
continue;
|
|
669
|
+
const eq = t.indexOf("=");
|
|
670
|
+
if (eq === -1)
|
|
671
|
+
continue;
|
|
672
|
+
const key = t.slice(0, eq).trim();
|
|
673
|
+
if (!key)
|
|
674
|
+
continue;
|
|
675
|
+
if (key in out)
|
|
676
|
+
continue;
|
|
677
|
+
out[key] = t.slice(eq + 1).trim();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return out;
|
|
681
|
+
}
|
|
682
|
+
function ensureConfig(projectDir) {
|
|
683
|
+
const file = join5(projectDir, ".mugiwara", "config");
|
|
684
|
+
let exists = false;
|
|
685
|
+
try {
|
|
686
|
+
exists = lstatSync(file).isFile() || lstatSync(file).isSymbolicLink();
|
|
687
|
+
} catch {
|
|
688
|
+
exists = false;
|
|
689
|
+
}
|
|
690
|
+
if (exists)
|
|
691
|
+
return false;
|
|
692
|
+
mkdirSync3(join5(projectDir, ".mugiwara"), { recursive: true });
|
|
693
|
+
writeFileSync3(file, DEFAULT_CONFIG);
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/installer.ts
|
|
698
|
+
var CONTENT_DIR = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "content");
|
|
699
|
+
var pkg = JSON.parse(readFileSync4(join6(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf8"));
|
|
629
700
|
var VERSION = pkg.version;
|
|
630
701
|
function collectContent() {
|
|
631
|
-
const skillNames = readdirSync3(
|
|
702
|
+
const skillNames = readdirSync3(join6(CONTENT_DIR, "skills"), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
632
703
|
const skills = skillNames.map((name) => {
|
|
633
|
-
const { data, body } = parseFrontmatter(
|
|
634
|
-
return { name, data, body, refs: collectRefs(
|
|
704
|
+
const { data, body } = parseFrontmatter(readFileSync4(join6(CONTENT_DIR, "skills", name, "SKILL.md"), "utf8"));
|
|
705
|
+
return { name, data, body, refs: collectRefs(join6(CONTENT_DIR, "skills", name)) };
|
|
635
706
|
});
|
|
636
|
-
const agents = readdirSync3(
|
|
637
|
-
const { data, body } = parseFrontmatter(
|
|
707
|
+
const agents = readdirSync3(join6(CONTENT_DIR, "agents")).filter((f) => f.endsWith(".md")).map((f) => {
|
|
708
|
+
const { data, body } = parseFrontmatter(readFileSync4(join6(CONTENT_DIR, "agents", f), "utf8"));
|
|
638
709
|
return { name: f.replace(/\.md$/, ""), data, body, refs: [], internal: data.internal === "true" };
|
|
639
710
|
});
|
|
640
|
-
const sharedRefsDir =
|
|
711
|
+
const sharedRefsDir = join6(dirname3(CONTENT_DIR), "references");
|
|
641
712
|
const sharedRefs = [];
|
|
642
|
-
if (
|
|
713
|
+
if (existsSync5(sharedRefsDir)) {
|
|
643
714
|
for (const f of readdirSync3(sharedRefsDir)) {
|
|
644
715
|
if (!f.endsWith(".md"))
|
|
645
716
|
continue;
|
|
646
|
-
sharedRefs.push({ relPath: f, text:
|
|
717
|
+
sharedRefs.push({ relPath: f, text: readFileSync4(join6(sharedRefsDir, f), "utf8") });
|
|
647
718
|
}
|
|
648
719
|
}
|
|
649
720
|
return { skills, agents, sharedRefs };
|
|
650
721
|
}
|
|
651
722
|
function collectRefs(skillDir) {
|
|
652
|
-
const refsDir =
|
|
653
|
-
if (!
|
|
723
|
+
const refsDir = join6(skillDir, "references");
|
|
724
|
+
if (!existsSync5(refsDir))
|
|
654
725
|
return [];
|
|
655
|
-
return readdirSync3(refsDir, { recursive: true }).map((f) => String(f)).filter((f) => f.endsWith(".md")).map((rel) => ({ relPath: rel, text:
|
|
726
|
+
return readdirSync3(refsDir, { recursive: true }).map((f) => String(f)).filter((f) => f.endsWith(".md")).map((rel) => ({ relPath: rel, text: readFileSync4(join6(refsDir, rel), "utf8") }));
|
|
656
727
|
}
|
|
657
728
|
function installTo(target10, opts) {
|
|
658
729
|
const { scope, projectDir, dryRun = false, force = false } = opts;
|
|
659
|
-
const home = opts.home ??
|
|
730
|
+
const home = opts.home ?? homedir2();
|
|
660
731
|
const { skills, agents, sharedRefs } = collectContent();
|
|
661
732
|
const dirs = target10.paths({ scope, projectDir, home });
|
|
662
|
-
const backupRoot =
|
|
733
|
+
const backupRoot = join6(scope === "global" ? home : projectDir, ".mugiwara");
|
|
663
734
|
const result = { written: [], skipped: [], backedUp: [], notes: [] };
|
|
664
735
|
const writeOne = (absPath, text) => {
|
|
665
|
-
if (
|
|
666
|
-
if (
|
|
736
|
+
if (existsSync5(absPath)) {
|
|
737
|
+
if (readFileSync4(absPath, "utf8") === text) {
|
|
667
738
|
result.skipped.push(absPath);
|
|
668
739
|
return;
|
|
669
740
|
}
|
|
@@ -674,17 +745,17 @@ function installTo(target10, opts) {
|
|
|
674
745
|
}
|
|
675
746
|
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
676
747
|
const fileName = absPath.replace(/[^a-zA-Z0-9]+/g, "_");
|
|
677
|
-
const backupDir =
|
|
678
|
-
const backupFile =
|
|
748
|
+
const backupDir = join6(backupRoot, "backup", `${ts}-${target10.id}`);
|
|
749
|
+
const backupFile = join6(backupDir, fileName);
|
|
679
750
|
if (!dryRun) {
|
|
680
|
-
|
|
751
|
+
mkdirSync4(backupDir, { recursive: true });
|
|
681
752
|
copyFileSync3(absPath, backupFile);
|
|
682
753
|
}
|
|
683
754
|
result.backedUp.push(absPath);
|
|
684
755
|
}
|
|
685
756
|
if (!dryRun) {
|
|
686
|
-
|
|
687
|
-
|
|
757
|
+
mkdirSync4(dirname3(absPath), { recursive: true });
|
|
758
|
+
writeFileSync4(absPath, text);
|
|
688
759
|
}
|
|
689
760
|
result.written.push(absPath);
|
|
690
761
|
};
|
|
@@ -695,7 +766,7 @@ function installTo(target10, opts) {
|
|
|
695
766
|
if (target10.refPointerPrefix !== undefined && target10.refPointerPrefix !== "") {
|
|
696
767
|
text = text.replace(/`_shared\/references\//g, "`" + target10.refPointerPrefix + "_shared/references/");
|
|
697
768
|
}
|
|
698
|
-
writeOne(
|
|
769
|
+
writeOne(join6(dirs.skillsDir, out.relPath), text);
|
|
699
770
|
}
|
|
700
771
|
if (target10.transformSkillFull) {
|
|
701
772
|
const full = target10.transformSkillFull(s.data, s.body);
|
|
@@ -704,29 +775,29 @@ function installTo(target10, opts) {
|
|
|
704
775
|
if (target10.refPointerPrefix !== undefined && target10.refPointerPrefix !== "") {
|
|
705
776
|
text = text.replace(/`_shared\/references\//g, "`" + target10.refPointerPrefix + "_shared/references/");
|
|
706
777
|
}
|
|
707
|
-
writeOne(
|
|
778
|
+
writeOne(join6(target10.refsDir({ scope, projectDir, home }, s.name), full.relPath), text);
|
|
708
779
|
}
|
|
709
780
|
}
|
|
710
781
|
if (s.refs.length && target10.refsDir) {
|
|
711
782
|
const refsRoot = target10.refsDir({ scope, projectDir, home }, s.name);
|
|
712
783
|
for (const r of s.refs)
|
|
713
|
-
writeOne(
|
|
784
|
+
writeOne(join6(refsRoot, r.relPath), r.text);
|
|
714
785
|
}
|
|
715
786
|
}
|
|
716
787
|
for (const a of agents) {
|
|
717
788
|
const out = target10.transformAgent({ ...a.data, ...a.internal ? { "internal-agent": "true" } : {} }, a.body);
|
|
718
789
|
if (out)
|
|
719
|
-
writeOne(
|
|
790
|
+
writeOne(join6(dirs.agentsDir, out.relPath), out.text);
|
|
720
791
|
if (target10.transformAgentFull) {
|
|
721
792
|
const full = target10.transformAgentFull(a.data, a.body);
|
|
722
793
|
if (full && target10.refsDir)
|
|
723
|
-
writeOne(
|
|
794
|
+
writeOne(join6(target10.refsDir({ scope, projectDir, home }, a.name), full.relPath), full.text);
|
|
724
795
|
}
|
|
725
796
|
}
|
|
726
797
|
if (sharedRefs.length) {
|
|
727
|
-
const sharedRoot =
|
|
798
|
+
const sharedRoot = join6(dirs.skillsDir, "_shared", "references");
|
|
728
799
|
for (const r of sharedRefs)
|
|
729
|
-
writeOne(
|
|
800
|
+
writeOne(join6(sharedRoot, r.relPath), r.text);
|
|
730
801
|
}
|
|
731
802
|
if (target10.postInstall) {
|
|
732
803
|
const post = target10.postInstall({ scope, projectDir, home, dryRun, files: result.written });
|
|
@@ -734,34 +805,17 @@ function installTo(target10, opts) {
|
|
|
734
805
|
result.notes.push(...post.notes);
|
|
735
806
|
}
|
|
736
807
|
if (scope === "project") {
|
|
737
|
-
const configPath =
|
|
808
|
+
const configPath = join6(projectDir, ".mugiwara", "config");
|
|
738
809
|
let configExists = false;
|
|
739
810
|
try {
|
|
740
|
-
configExists =
|
|
811
|
+
configExists = lstatSync2(configPath).isFile() || lstatSync2(configPath).isSymbolicLink();
|
|
741
812
|
} catch {
|
|
742
813
|
configExists = false;
|
|
743
814
|
}
|
|
744
815
|
if (!configExists) {
|
|
745
|
-
const body = [
|
|
746
|
-
"mode=guided",
|
|
747
|
-
"branch=feature/{type}-{issue}-{slug}",
|
|
748
|
-
"commit=conventional",
|
|
749
|
-
"auto_commit=on",
|
|
750
|
-
"coverage_new=90",
|
|
751
|
-
"coverage_modified=80",
|
|
752
|
-
"review_depth=full",
|
|
753
|
-
"quality_depth=full",
|
|
754
|
-
"verify_merged=off",
|
|
755
|
-
"delegate_threshold=60",
|
|
756
|
-
"heal_max_cycles=3",
|
|
757
|
-
"verbosity=normal",
|
|
758
|
-
"# context_budget_chars=150000 # optional: fail archive if trail exceeds this (measured in report Cost section)"
|
|
759
|
-
].join(`
|
|
760
|
-
`) + `
|
|
761
|
-
`;
|
|
762
816
|
if (!dryRun) {
|
|
763
|
-
|
|
764
|
-
|
|
817
|
+
mkdirSync4(dirname3(configPath), { recursive: true });
|
|
818
|
+
writeFileSync4(configPath, DEFAULT_CONFIG);
|
|
765
819
|
}
|
|
766
820
|
result.written.push(configPath);
|
|
767
821
|
result.notes.push(`default config written: ${configPath} (edit it to customise)`);
|
|
@@ -772,7 +826,7 @@ function installTo(target10, opts) {
|
|
|
772
826
|
function removeInstalled(manifest, { dryRun = false } = {}) {
|
|
773
827
|
const removed = [];
|
|
774
828
|
for (const f of manifest.files) {
|
|
775
|
-
if (
|
|
829
|
+
if (existsSync5(f)) {
|
|
776
830
|
if (!dryRun)
|
|
777
831
|
rmSync(f);
|
|
778
832
|
removed.push(f);
|
|
@@ -781,7 +835,7 @@ function removeInstalled(manifest, { dryRun = false } = {}) {
|
|
|
781
835
|
if (!dryRun) {
|
|
782
836
|
for (const f of manifest.files) {
|
|
783
837
|
let d = dirname3(f);
|
|
784
|
-
while (
|
|
838
|
+
while (existsSync5(d) && readdirSync3(d).length === 0) {
|
|
785
839
|
if (d.endsWith("/.mugiwara") || d === dirname3(d))
|
|
786
840
|
break;
|
|
787
841
|
rmSync(d, { recursive: true, force: true });
|
|
@@ -792,10 +846,10 @@ function removeInstalled(manifest, { dryRun = false } = {}) {
|
|
|
792
846
|
return removed;
|
|
793
847
|
}
|
|
794
848
|
function assertNotSymlink(file) {
|
|
795
|
-
if (!
|
|
849
|
+
if (!existsSync5(file))
|
|
796
850
|
return;
|
|
797
851
|
try {
|
|
798
|
-
if (
|
|
852
|
+
if (lstatSync2(file).isSymbolicLink())
|
|
799
853
|
throw new Error(`refusing to follow symlink: ${file}`);
|
|
800
854
|
} catch (e) {
|
|
801
855
|
if (e.code === "ENOENT")
|
|
@@ -816,9 +870,9 @@ var GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit
|
|
|
816
870
|
`;
|
|
817
871
|
function ensureProjectGitignore(projectDir, opts = {}) {
|
|
818
872
|
const { dryRun = false } = opts;
|
|
819
|
-
const path =
|
|
873
|
+
const path = join6(projectDir, ".gitignore");
|
|
820
874
|
assertNotSymlink(path);
|
|
821
|
-
let existing =
|
|
875
|
+
let existing = existsSync5(path) ? readFileSync4(path, "utf8") : "";
|
|
822
876
|
if (existing) {
|
|
823
877
|
if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes(".mugiwara/missions/**/*.json")) {
|
|
824
878
|
return { appended: false, notes: [] };
|
|
@@ -826,24 +880,24 @@ function ensureProjectGitignore(projectDir, opts = {}) {
|
|
|
826
880
|
const hadOld = GITIGNORE_LEGACY.some((m) => existing.includes(m)) || existing.includes(GITIGNORE_BLOCK_START);
|
|
827
881
|
if (hadOld) {
|
|
828
882
|
const clean = removeProjectGitignore(projectDir, { dryRun });
|
|
829
|
-
existing = clean.removed ?
|
|
883
|
+
existing = clean.removed ? existsSync5(path) ? readFileSync4(path, "utf8") : "" : existing;
|
|
830
884
|
}
|
|
831
885
|
}
|
|
832
886
|
const separator = existing.length && !existing.endsWith(`
|
|
833
887
|
`) ? `
|
|
834
888
|
` : "";
|
|
835
889
|
if (!dryRun) {
|
|
836
|
-
|
|
837
|
-
|
|
890
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
891
|
+
writeFileSync4(path, existing + separator + GITIGNORE_BLOCK);
|
|
838
892
|
}
|
|
839
893
|
return { appended: true, notes: [`.gitignore ${dryRun ? "would upgrade+append" : "upgraded"} mugiwara audit-trail block`] };
|
|
840
894
|
}
|
|
841
895
|
function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
|
|
842
|
-
const path =
|
|
896
|
+
const path = join6(projectDir, ".gitignore");
|
|
843
897
|
assertNotSymlink(path);
|
|
844
|
-
if (!
|
|
898
|
+
if (!existsSync5(path))
|
|
845
899
|
return { removed: false, notes: [] };
|
|
846
|
-
const current =
|
|
900
|
+
const current = readFileSync4(path, "utf8");
|
|
847
901
|
const delimited = current.includes(GITIGNORE_BLOCK_START);
|
|
848
902
|
let cleaned;
|
|
849
903
|
if (delimited) {
|
|
@@ -883,33 +937,33 @@ function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
|
|
|
883
937
|
if (cleaned === current)
|
|
884
938
|
return { removed: false, notes: [] };
|
|
885
939
|
if (!dryRun)
|
|
886
|
-
|
|
940
|
+
writeFileSync4(path, cleaned);
|
|
887
941
|
return { removed: true, notes: [`.gitignore ${dryRun ? "would strip" : "stripped"} mugiwara block`] };
|
|
888
942
|
}
|
|
889
943
|
|
|
890
944
|
// src/manifest.ts
|
|
891
|
-
import { existsSync as
|
|
892
|
-
import { dirname as dirname4, join as
|
|
945
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
946
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
893
947
|
function manifestPath({ scope, projectDir, home }) {
|
|
894
|
-
return scope === "global" ?
|
|
948
|
+
return scope === "global" ? join7(home, ".mugiwara", "manifest.json") : join7(projectDir, ".mugiwara", "manifest.json");
|
|
895
949
|
}
|
|
896
950
|
function readManifest(file) {
|
|
897
|
-
return
|
|
951
|
+
return existsSync6(file) ? JSON.parse(readFileSync5(file, "utf8")) : null;
|
|
898
952
|
}
|
|
899
953
|
function writeManifest(file, data) {
|
|
900
|
-
|
|
901
|
-
|
|
954
|
+
mkdirSync5(dirname4(file), { recursive: true });
|
|
955
|
+
writeFileSync5(file, JSON.stringify(data, null, 2) + `
|
|
902
956
|
`);
|
|
903
957
|
}
|
|
904
958
|
|
|
905
959
|
// src/mission.ts
|
|
906
|
-
import { existsSync as
|
|
960
|
+
import { existsSync as existsSync11, rmSync as rmSync2, readFileSync as readFileSync11, readdirSync as readdirSync7, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8, renameSync, openSync, writeSync, closeSync } from "node:fs";
|
|
907
961
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
908
|
-
import { join as
|
|
962
|
+
import { join as join16 } from "node:path";
|
|
909
963
|
|
|
910
964
|
// src/integrity.ts
|
|
911
|
-
import { existsSync as
|
|
912
|
-
import { isAbsolute, join as
|
|
965
|
+
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
|
|
966
|
+
import { isAbsolute, join as join8, relative } from "node:path";
|
|
913
967
|
var SECRET_PATTERNS = [
|
|
914
968
|
[/AKIA[0-9A-Z]{16}/, "AWS access key id"],
|
|
915
969
|
[/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "private key block"],
|
|
@@ -933,12 +987,12 @@ function findSecrets(body) {
|
|
|
933
987
|
}
|
|
934
988
|
return out;
|
|
935
989
|
}
|
|
936
|
-
var TRAIL_EXTS = new Set([".md", ".json", ".sh"]);
|
|
990
|
+
var TRAIL_EXTS = new Set([".md", ".json", ".sh", ".jsonl"]);
|
|
937
991
|
function trailFiles(dir) {
|
|
938
992
|
const out = [];
|
|
939
993
|
const walk = (d) => {
|
|
940
994
|
for (const e of readdirSync4(d, { withFileTypes: true })) {
|
|
941
|
-
const p =
|
|
995
|
+
const p = join8(d, e.name);
|
|
942
996
|
if (e.isDirectory())
|
|
943
997
|
walk(p);
|
|
944
998
|
else if (TRAIL_EXTS.has(e.name.slice(e.name.lastIndexOf(".")) || ""))
|
|
@@ -966,7 +1020,7 @@ function collectPassCitedPaths(missionDir) {
|
|
|
966
1020
|
for (const f of trailFiles(missionDir)) {
|
|
967
1021
|
let body;
|
|
968
1022
|
try {
|
|
969
|
-
body =
|
|
1023
|
+
body = readFileSync6(f, "utf8");
|
|
970
1024
|
} catch {
|
|
971
1025
|
continue;
|
|
972
1026
|
}
|
|
@@ -990,16 +1044,16 @@ function checkTrail(missionDir, projectRoot) {
|
|
|
990
1044
|
for (const f of files) {
|
|
991
1045
|
let body;
|
|
992
1046
|
try {
|
|
993
|
-
body =
|
|
1047
|
+
body = readFileSync6(f, "utf8");
|
|
994
1048
|
} catch {
|
|
995
1049
|
continue;
|
|
996
1050
|
}
|
|
997
1051
|
for (const target10 of linkedPaths(body)) {
|
|
998
1052
|
if (isAbsolute(target10))
|
|
999
1053
|
continue;
|
|
1000
|
-
const fromMission =
|
|
1001
|
-
const fromRoot =
|
|
1002
|
-
if (!
|
|
1054
|
+
const fromMission = join8(missionDir, target10);
|
|
1055
|
+
const fromRoot = join8(projectRoot, target10);
|
|
1056
|
+
if (!existsSync7(fromMission) && !existsSync7(fromRoot)) {
|
|
1003
1057
|
issues.push({
|
|
1004
1058
|
kind: "dangling-path",
|
|
1005
1059
|
detail: `${relative(projectRoot, f)} links "${target10}" — no such file (mission dir or repo root)`
|
|
@@ -1014,17 +1068,17 @@ function checkTrail(missionDir, projectRoot) {
|
|
|
1014
1068
|
}
|
|
1015
1069
|
}
|
|
1016
1070
|
const evidencePaths = [];
|
|
1017
|
-
const evidenceFile =
|
|
1018
|
-
if (
|
|
1071
|
+
const evidenceFile = join8(missionDir, "state.json");
|
|
1072
|
+
if (existsSync7(evidenceFile)) {
|
|
1019
1073
|
try {
|
|
1020
|
-
const s = JSON.parse(
|
|
1074
|
+
const s = JSON.parse(readFileSync6(evidenceFile, "utf8"));
|
|
1021
1075
|
if (Array.isArray(s.evidence)) {
|
|
1022
1076
|
for (const e of s.evidence) {
|
|
1023
1077
|
if (typeof e !== "string" || !e.trim())
|
|
1024
1078
|
continue;
|
|
1025
1079
|
evidencePaths.push(e);
|
|
1026
|
-
const cand =
|
|
1027
|
-
if (!isAbsolute(e) && !
|
|
1080
|
+
const cand = join8(projectRoot, e);
|
|
1081
|
+
if (!isAbsolute(e) && !existsSync7(cand) && !existsSync7(join8(missionDir, e))) {
|
|
1028
1082
|
issues.push({ kind: "evidence", detail: `state.json evidence "${e}" does not exist` });
|
|
1029
1083
|
}
|
|
1030
1084
|
}
|
|
@@ -1035,14 +1089,14 @@ function checkTrail(missionDir, projectRoot) {
|
|
|
1035
1089
|
for (const e of passCited) {
|
|
1036
1090
|
if (!e.trim() || isAbsolute(e))
|
|
1037
1091
|
continue;
|
|
1038
|
-
const candMission =
|
|
1039
|
-
const candRoot =
|
|
1040
|
-
const resolved =
|
|
1092
|
+
const candMission = join8(missionDir, e);
|
|
1093
|
+
const candRoot = join8(projectRoot, e);
|
|
1094
|
+
const resolved = existsSync7(candMission) ? candMission : existsSync7(candRoot) ? candRoot : null;
|
|
1041
1095
|
if (!resolved)
|
|
1042
1096
|
continue;
|
|
1043
1097
|
let body;
|
|
1044
1098
|
try {
|
|
1045
|
-
body =
|
|
1099
|
+
body = readFileSync6(resolved, "utf8");
|
|
1046
1100
|
} catch {
|
|
1047
1101
|
continue;
|
|
1048
1102
|
}
|
|
@@ -1057,10 +1111,45 @@ function formatIssues(issues) {
|
|
|
1057
1111
|
`);
|
|
1058
1112
|
}
|
|
1059
1113
|
|
|
1114
|
+
// src/check-artifacts.ts
|
|
1115
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
|
|
1116
|
+
import { join as join9 } from "node:path";
|
|
1117
|
+
var LANE_MIN = new Set(["standard", "full", "spike"]);
|
|
1118
|
+
function checkMissionArtifacts(missionDir) {
|
|
1119
|
+
const statePath = join9(missionDir, "state.json");
|
|
1120
|
+
if (!existsSync8(statePath)) {
|
|
1121
|
+
return { ok: true, missing: [], lane: null };
|
|
1122
|
+
}
|
|
1123
|
+
let lane = "unknown";
|
|
1124
|
+
try {
|
|
1125
|
+
const s = JSON.parse(readFileSyncSafe(statePath));
|
|
1126
|
+
if (typeof s.lane === "string")
|
|
1127
|
+
lane = s.lane;
|
|
1128
|
+
} catch {}
|
|
1129
|
+
if (!LANE_MIN.has(lane)) {
|
|
1130
|
+
return { ok: true, missing: [], lane };
|
|
1131
|
+
}
|
|
1132
|
+
const missing = [];
|
|
1133
|
+
if (!existsSync8(join9(missionDir, "plan.md")))
|
|
1134
|
+
missing.push("plan.md");
|
|
1135
|
+
const flowsDir = join9(missionDir, "flows");
|
|
1136
|
+
const hasFlows = existsSync8(flowsDir) && readdirSync5(flowsDir).length > 0;
|
|
1137
|
+
if (!hasFlows)
|
|
1138
|
+
missing.push("flows/ (no execution evidence)");
|
|
1139
|
+
return { ok: missing.length === 0, missing, lane };
|
|
1140
|
+
}
|
|
1141
|
+
function readFileSyncSafe(p) {
|
|
1142
|
+
try {
|
|
1143
|
+
return readFileSync7(p, "utf8");
|
|
1144
|
+
} catch {
|
|
1145
|
+
return "";
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1060
1149
|
// src/rollback.ts
|
|
1061
1150
|
import { execFileSync } from "node:child_process";
|
|
1062
|
-
import { writeFileSync as
|
|
1063
|
-
import { join as
|
|
1151
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
1152
|
+
import { join as join10 } from "node:path";
|
|
1064
1153
|
function git(cwd, args) {
|
|
1065
1154
|
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1066
1155
|
}
|
|
@@ -1099,8 +1188,8 @@ function generateRollback(projectDir, missionDir, input) {
|
|
|
1099
1188
|
const commitsNewestFirst = [...revList].reverse();
|
|
1100
1189
|
const filesTouched = input.baseSha === "unknown" ? [] : git(projectDir, ["diff", "--name-only", input.baseSha, input.branch]).split(/\r?\n/).filter(Boolean);
|
|
1101
1190
|
const body = buildRollback(input, commitsNewestFirst, filesTouched);
|
|
1102
|
-
const file =
|
|
1103
|
-
|
|
1191
|
+
const file = join10(missionDir, "rollback.sh");
|
|
1192
|
+
writeFileSync6(file, body, { mode: 493 });
|
|
1104
1193
|
return { file: "rollback.sh", commits: commitsNewestFirst.length };
|
|
1105
1194
|
} catch {
|
|
1106
1195
|
return null;
|
|
@@ -1109,8 +1198,8 @@ function generateRollback(projectDir, missionDir, input) {
|
|
|
1109
1198
|
|
|
1110
1199
|
// src/provenance.ts
|
|
1111
1200
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1112
|
-
import { writeFileSync as
|
|
1113
|
-
import { join as
|
|
1201
|
+
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
1202
|
+
import { join as join11 } from "node:path";
|
|
1114
1203
|
function git2(cwd, args) {
|
|
1115
1204
|
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1116
1205
|
}
|
|
@@ -1185,7 +1274,7 @@ ${note}`;
|
|
|
1185
1274
|
function writeProvenance(projectDir, missionDir, state) {
|
|
1186
1275
|
const note = buildNote(state);
|
|
1187
1276
|
const attached = attachGitNote(projectDir, state.branch, note);
|
|
1188
|
-
|
|
1277
|
+
writeFileSync7(join11(missionDir, "provenance.md"), renderProvenanceMd(note, attached ? attached.sha : null));
|
|
1189
1278
|
}
|
|
1190
1279
|
|
|
1191
1280
|
// src/routing.ts
|
|
@@ -1242,28 +1331,15 @@ function renderRouting(ranked, mission) {
|
|
|
1242
1331
|
}
|
|
1243
1332
|
|
|
1244
1333
|
// src/budget.ts
|
|
1245
|
-
import { existsSync as
|
|
1246
|
-
import {
|
|
1247
|
-
import { join as join10 } from "node:path";
|
|
1334
|
+
import { existsSync as existsSync9, readdirSync as readdirSync6, statSync } from "node:fs";
|
|
1335
|
+
import { join as join12 } from "node:path";
|
|
1248
1336
|
function readBudgetConfig(projectDir) {
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
if (!t || t.startsWith("#"))
|
|
1256
|
-
continue;
|
|
1257
|
-
const eq = t.indexOf("=");
|
|
1258
|
-
if (eq === -1)
|
|
1259
|
-
continue;
|
|
1260
|
-
if (t.slice(0, eq).trim() !== "context_budget_chars")
|
|
1261
|
-
continue;
|
|
1262
|
-
const n = Number(t.slice(eq + 1).trim());
|
|
1263
|
-
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
return 0;
|
|
1337
|
+
const cfg = readConfig(projectDir);
|
|
1338
|
+
const raw = cfg.context_budget_chars;
|
|
1339
|
+
if (raw === undefined || raw === "")
|
|
1340
|
+
return 0;
|
|
1341
|
+
const n = Number(raw);
|
|
1342
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
1267
1343
|
}
|
|
1268
1344
|
function measureContextChars(missionDir) {
|
|
1269
1345
|
let total = 0;
|
|
@@ -1272,15 +1348,15 @@ function measureContextChars(missionDir) {
|
|
|
1272
1348
|
total += statSync(p).size;
|
|
1273
1349
|
} catch {}
|
|
1274
1350
|
};
|
|
1275
|
-
for (const f of
|
|
1351
|
+
for (const f of readdirSync6(missionDir)) {
|
|
1276
1352
|
if (/\.md$/.test(f))
|
|
1277
|
-
add(
|
|
1353
|
+
add(join12(missionDir, f));
|
|
1278
1354
|
}
|
|
1279
1355
|
for (const sub of ["flows", "waves"]) {
|
|
1280
|
-
const dir =
|
|
1281
|
-
if (
|
|
1282
|
-
for (const f of
|
|
1283
|
-
add(
|
|
1356
|
+
const dir = join12(missionDir, sub);
|
|
1357
|
+
if (existsSync9(dir)) {
|
|
1358
|
+
for (const f of readdirSync6(dir))
|
|
1359
|
+
add(join12(dir, f));
|
|
1284
1360
|
}
|
|
1285
1361
|
}
|
|
1286
1362
|
return total;
|
|
@@ -1292,6 +1368,274 @@ function formatFootprint(chars, budget) {
|
|
|
1292
1368
|
return chars > budget ? `${base} — OVER budget ${budget}` : `${base} (budget ${budget})`;
|
|
1293
1369
|
}
|
|
1294
1370
|
|
|
1371
|
+
// src/cost.ts
|
|
1372
|
+
import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync8 } from "node:fs";
|
|
1373
|
+
import { join as join13 } from "node:path";
|
|
1374
|
+
var LANE_BUDGET = {
|
|
1375
|
+
lean: 12000,
|
|
1376
|
+
standard: 25000,
|
|
1377
|
+
full: 50000,
|
|
1378
|
+
spike: 3000
|
|
1379
|
+
};
|
|
1380
|
+
function budgetForLane(lane) {
|
|
1381
|
+
return LANE_BUDGET[lane] ?? 0;
|
|
1382
|
+
}
|
|
1383
|
+
function warnAt(budget) {
|
|
1384
|
+
return Math.floor(budget * 3 / 2);
|
|
1385
|
+
}
|
|
1386
|
+
function stopAt(budget) {
|
|
1387
|
+
return budget * 3;
|
|
1388
|
+
}
|
|
1389
|
+
function budgetStatus(budget, tokens) {
|
|
1390
|
+
if (budget > 0 && tokens >= stopAt(budget))
|
|
1391
|
+
return "stop";
|
|
1392
|
+
if (budget > 0 && tokens >= warnAt(budget))
|
|
1393
|
+
return "warn";
|
|
1394
|
+
return "ok";
|
|
1395
|
+
}
|
|
1396
|
+
function costEnvelope(state) {
|
|
1397
|
+
const planned = typeof state.budget === "number" && state.budget > 0 ? state.budget : budgetForLane(state.lane ?? "");
|
|
1398
|
+
const used = typeof state.tokens_est === "number" ? state.tokens_est : 0;
|
|
1399
|
+
const remaining = Math.max(planned - used, 0);
|
|
1400
|
+
const pct = planned > 0 ? Math.round(used / planned * 100) : 0;
|
|
1401
|
+
return {
|
|
1402
|
+
planned,
|
|
1403
|
+
used,
|
|
1404
|
+
remaining,
|
|
1405
|
+
pct,
|
|
1406
|
+
warn_at: warnAt(planned),
|
|
1407
|
+
stop_at: stopAt(planned),
|
|
1408
|
+
status: budgetStatus(planned, used)
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
var COST_EVENTS_FILE = "cost-events.jsonl";
|
|
1412
|
+
function isAllowedMissionDir(dir) {
|
|
1413
|
+
if (!dir || dir.includes(".."))
|
|
1414
|
+
return false;
|
|
1415
|
+
if (dir.includes(".mugiwara/missions"))
|
|
1416
|
+
return true;
|
|
1417
|
+
if (dir.includes("mugiwara-"))
|
|
1418
|
+
return true;
|
|
1419
|
+
if (dir.startsWith("/tmp/") && dir.includes("-"))
|
|
1420
|
+
return true;
|
|
1421
|
+
return false;
|
|
1422
|
+
}
|
|
1423
|
+
function assertMissionDir(dir) {
|
|
1424
|
+
if (!isAllowedMissionDir(dir))
|
|
1425
|
+
throw new Error(`Invalid missionDir: ${dir}`);
|
|
1426
|
+
}
|
|
1427
|
+
function appendCostEvent(missionDir, event) {
|
|
1428
|
+
assertMissionDir(missionDir);
|
|
1429
|
+
mkdirSync6(missionDir, { recursive: true });
|
|
1430
|
+
const line = Object.assign({ ts: new Date().toISOString() }, event);
|
|
1431
|
+
appendFileSync(join13(missionDir, COST_EVENTS_FILE), JSON.stringify(line) + `
|
|
1432
|
+
`, "utf8");
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// src/evidence.ts
|
|
1436
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync9 } from "node:fs";
|
|
1437
|
+
import { join as join14 } from "node:path";
|
|
1438
|
+
var REGISTRY_FILE = "context-registry.jsonl";
|
|
1439
|
+
function isAllowedMissionDir2(dir) {
|
|
1440
|
+
if (!dir || dir.includes(".."))
|
|
1441
|
+
return false;
|
|
1442
|
+
if (dir.includes(".mugiwara/missions"))
|
|
1443
|
+
return true;
|
|
1444
|
+
if (dir.includes("mugiwara-"))
|
|
1445
|
+
return true;
|
|
1446
|
+
if (dir.startsWith("/tmp/") && dir.includes("-"))
|
|
1447
|
+
return true;
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
function assertMissionDir2(dir) {
|
|
1451
|
+
if (!isAllowedMissionDir2(dir))
|
|
1452
|
+
throw new Error(`Invalid missionDir: ${dir}`);
|
|
1453
|
+
}
|
|
1454
|
+
function loadRegistry(missionDir) {
|
|
1455
|
+
assertMissionDir2(missionDir);
|
|
1456
|
+
const file = join14(missionDir, REGISTRY_FILE);
|
|
1457
|
+
try {
|
|
1458
|
+
const out = [];
|
|
1459
|
+
for (const line of readFileSync9(file, "utf8").split(/\r?\n/)) {
|
|
1460
|
+
if (!line.trim())
|
|
1461
|
+
continue;
|
|
1462
|
+
let e;
|
|
1463
|
+
try {
|
|
1464
|
+
e = JSON.parse(line);
|
|
1465
|
+
} catch {
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
if (e === null || typeof e !== "object")
|
|
1469
|
+
continue;
|
|
1470
|
+
const entry = e;
|
|
1471
|
+
const ok = typeof entry.fingerprint === "string" && typeof entry.kind === "string" && typeof entry.file === "string" && typeof entry.id === "string" && typeof entry.ref === "string" && typeof entry.reads === "number" && Number.isFinite(entry.reads) && entry.reads >= 0;
|
|
1472
|
+
if (!ok)
|
|
1473
|
+
continue;
|
|
1474
|
+
entry.reads = Math.floor(entry.reads);
|
|
1475
|
+
out.push(entry);
|
|
1476
|
+
}
|
|
1477
|
+
return out;
|
|
1478
|
+
} catch {
|
|
1479
|
+
return [];
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// src/context.ts
|
|
1484
|
+
function contextStatus(budgetChars, chars) {
|
|
1485
|
+
return budgetChars > 0 && chars > budgetChars ? "over" : "ok";
|
|
1486
|
+
}
|
|
1487
|
+
function computeContextMetrics(stats) {
|
|
1488
|
+
const reuse_rate = stats.reads_total > 0 ? stats.reads_reused / stats.reads_total : 0;
|
|
1489
|
+
const duplicate_chars = stats.total_chars - stats.unique_chars;
|
|
1490
|
+
return {
|
|
1491
|
+
files_loaded: stats.files_loaded,
|
|
1492
|
+
repeated_reads: stats.repeated_reads,
|
|
1493
|
+
duplicate_chars,
|
|
1494
|
+
reuse_rate,
|
|
1495
|
+
read_avoidance_chars: duplicate_chars
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// src/reporting.ts
|
|
1500
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
|
|
1501
|
+
import { join as join15 } from "node:path";
|
|
1502
|
+
function isAllowedMissionDir3(dir) {
|
|
1503
|
+
if (!dir || dir.includes(".."))
|
|
1504
|
+
return false;
|
|
1505
|
+
if (dir.includes(".mugiwara/missions"))
|
|
1506
|
+
return true;
|
|
1507
|
+
if (dir.includes("mugiwara-"))
|
|
1508
|
+
return true;
|
|
1509
|
+
if (dir.startsWith("/tmp/") && dir.includes("-"))
|
|
1510
|
+
return true;
|
|
1511
|
+
return false;
|
|
1512
|
+
}
|
|
1513
|
+
function assertMissionDir3(dir) {
|
|
1514
|
+
if (!isAllowedMissionDir3(dir))
|
|
1515
|
+
throw new Error(`Invalid missionDir: ${dir}`);
|
|
1516
|
+
}
|
|
1517
|
+
function parseDecisionTrail(missionDir) {
|
|
1518
|
+
assertMissionDir3(missionDir);
|
|
1519
|
+
const file = join15(missionDir, "decisions.md");
|
|
1520
|
+
if (!existsSync10(file))
|
|
1521
|
+
return [];
|
|
1522
|
+
const raw = readFileSync10(file, "utf8");
|
|
1523
|
+
const lines = raw.split(/\r?\n/);
|
|
1524
|
+
const headerIdx = lines.findIndex((l) => l.trim() === "## Cost governor decisions" || l.trim() === "## Budget");
|
|
1525
|
+
const scan = headerIdx >= 0 ? lines.slice(headerIdx) : lines;
|
|
1526
|
+
const out = [];
|
|
1527
|
+
const bulletRe = /^-\s+(.+?)\s+—\s+(.+?):\s+(.+?)\s+—\s+reason:\s+(.+?)(?:\s+—\s+evidence:\s+(.+))?\s*$/;
|
|
1528
|
+
for (const line of scan) {
|
|
1529
|
+
const m = bulletRe.exec(line.trim());
|
|
1530
|
+
if (!m)
|
|
1531
|
+
continue;
|
|
1532
|
+
out.push({ ts: m[1], actor: m[2], decision: m[3], reason: m[4], ...m[5] ? { evidence: m[5] } : {} });
|
|
1533
|
+
}
|
|
1534
|
+
return out;
|
|
1535
|
+
}
|
|
1536
|
+
function loadCostEvents(missionDir) {
|
|
1537
|
+
assertMissionDir3(missionDir);
|
|
1538
|
+
const file = join15(missionDir, "cost-events.jsonl");
|
|
1539
|
+
if (!existsSync10(file))
|
|
1540
|
+
return [];
|
|
1541
|
+
const out = [];
|
|
1542
|
+
for (const line of readFileSync10(file, "utf8").split(/\r?\n/)) {
|
|
1543
|
+
if (!line.trim())
|
|
1544
|
+
continue;
|
|
1545
|
+
try {
|
|
1546
|
+
const e = JSON.parse(line);
|
|
1547
|
+
if (e && typeof e === "object" && typeof e.mission === "string")
|
|
1548
|
+
out.push(e);
|
|
1549
|
+
} catch {
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
return out;
|
|
1554
|
+
}
|
|
1555
|
+
function computeAvoidedMetrics(input) {
|
|
1556
|
+
const dup = input.registryMetrics?.duplicateCount ?? 0;
|
|
1557
|
+
const rep = input.registryMetrics?.repeatedReads ?? 0;
|
|
1558
|
+
const contexts_avoided = dup + rep;
|
|
1559
|
+
const stages_avoided = input.workMetrics?.stagesAvoided ?? 0;
|
|
1560
|
+
const slop_interventions = input.slopMetrics?.interventions ?? 0;
|
|
1561
|
+
const tokens_avoided_est = contexts_avoided * 150;
|
|
1562
|
+
return { stages_avoided, contexts_avoided, slop_interventions, tokens_avoided_est };
|
|
1563
|
+
}
|
|
1564
|
+
function computeEfficiencyMetrics(input) {
|
|
1565
|
+
const reuse_rate = input.totalReads > 0 ? Math.round(input.reuseHits / input.totalReads * 100) / 100 : 0;
|
|
1566
|
+
const duplicate_avoidance_chars = input.duplicateChars;
|
|
1567
|
+
const budget_efficiency_pct = input.budget > 0 ? Math.round(input.used / input.budget * 100) : 0;
|
|
1568
|
+
return { reuse_rate, duplicate_avoidance_chars, budget_efficiency_pct };
|
|
1569
|
+
}
|
|
1570
|
+
function buildCostLedger(input) {
|
|
1571
|
+
assertMissionDir3(input.missionDir);
|
|
1572
|
+
const events = loadCostEvents(input.missionDir);
|
|
1573
|
+
let registrySize = 0;
|
|
1574
|
+
let totalReads = 0;
|
|
1575
|
+
let reuseHits = 0;
|
|
1576
|
+
let duplicateChars = 0;
|
|
1577
|
+
try {
|
|
1578
|
+
const reg = loadRegistry(input.missionDir);
|
|
1579
|
+
registrySize = reg.length;
|
|
1580
|
+
totalReads = reg.reduce((s, e) => s + e.reads, 0);
|
|
1581
|
+
reuseHits = reg.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
|
|
1582
|
+
const unique = reg.reduce((s, e) => s + (e.chars ?? 0), 0);
|
|
1583
|
+
const total = reg.reduce((s, e) => s + (e.chars ?? 0) * e.reads, 0);
|
|
1584
|
+
duplicateChars = total - unique;
|
|
1585
|
+
if (duplicateChars < 0)
|
|
1586
|
+
duplicateChars = 0;
|
|
1587
|
+
} catch {
|
|
1588
|
+
registrySize = 0;
|
|
1589
|
+
}
|
|
1590
|
+
const decisions = parseDecisionTrail(input.missionDir);
|
|
1591
|
+
const repeatedReads = reuseHits;
|
|
1592
|
+
const duplicateCount = duplicateChars > 0 ? 1 : 0;
|
|
1593
|
+
const avoided = computeAvoidedMetrics({
|
|
1594
|
+
registryMetrics: { duplicateCount: 0, repeatedReads: reuseHits },
|
|
1595
|
+
workMetrics: { stagesAvoided: input.workSummary?.stagesAvoided ?? 0 },
|
|
1596
|
+
slopMetrics: { interventions: input.slopSummary?.interventions ?? 0 }
|
|
1597
|
+
});
|
|
1598
|
+
if (duplicateChars > 0 && avoided.contexts_avoided === 0) {
|
|
1599
|
+
avoided.contexts_avoided = 1;
|
|
1600
|
+
avoided.tokens_avoided_est = 150;
|
|
1601
|
+
}
|
|
1602
|
+
const efficiency = computeEfficiencyMetrics({
|
|
1603
|
+
totalReads,
|
|
1604
|
+
reuseHits,
|
|
1605
|
+
duplicateChars,
|
|
1606
|
+
budget: input.envelope.planned,
|
|
1607
|
+
used: input.envelope.used
|
|
1608
|
+
});
|
|
1609
|
+
return {
|
|
1610
|
+
envelope: input.envelope,
|
|
1611
|
+
ledger: { events, registrySize, decisions },
|
|
1612
|
+
avoided,
|
|
1613
|
+
efficiency,
|
|
1614
|
+
trail: decisions
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
function toCostJSON(ledger) {
|
|
1618
|
+
return JSON.stringify({ envelope: ledger.envelope, ledger: ledger.ledger, avoided: ledger.avoided, efficiency: ledger.efficiency, trail: ledger.trail }, null, 2);
|
|
1619
|
+
}
|
|
1620
|
+
function summarizeAdaptation(missionDir) {
|
|
1621
|
+
const trail = parseDecisionTrail(missionDir);
|
|
1622
|
+
const postureRe = /posture|switch|adapt|pause|parallel-workers|context-relief|phase-isolated|team-scoped/i;
|
|
1623
|
+
const rows = trail.filter((t) => postureRe.test(`${t.decision} ${t.reason}`)).map((t) => ({ ts: t.ts, decision: t.decision, reason: t.reason }));
|
|
1624
|
+
return { count: rows.length, rows };
|
|
1625
|
+
}
|
|
1626
|
+
function renderAdaptationSection(missionDir) {
|
|
1627
|
+
const { count, rows } = summarizeAdaptation(missionDir);
|
|
1628
|
+
if (count === 0)
|
|
1629
|
+
return "";
|
|
1630
|
+
const lines = ["", "## Adaptation", "", "| Time | Decision | Reason |", "|---|---|---|"];
|
|
1631
|
+
for (const r of rows)
|
|
1632
|
+
lines.push(`| ${r.ts} | ${r.decision} | ${r.reason} |`);
|
|
1633
|
+
if (rows.length === 0)
|
|
1634
|
+
lines.push("| — | (posture rows recorded in decisions.md) | |");
|
|
1635
|
+
return lines.join(`
|
|
1636
|
+
`);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1295
1639
|
// src/mission.ts
|
|
1296
1640
|
function isStateFile(f) {
|
|
1297
1641
|
const stem = f.replace(/\.json$/, "");
|
|
@@ -1302,7 +1646,7 @@ function primaryState(dir, files) {
|
|
|
1302
1646
|
if (!name)
|
|
1303
1647
|
return null;
|
|
1304
1648
|
try {
|
|
1305
|
-
return JSON.parse(
|
|
1649
|
+
return JSON.parse(readFileSync11(join16(dir, name), "utf8"));
|
|
1306
1650
|
} catch {
|
|
1307
1651
|
return null;
|
|
1308
1652
|
}
|
|
@@ -1323,16 +1667,16 @@ function changedFiles(projectDir, state) {
|
|
|
1323
1667
|
}
|
|
1324
1668
|
}
|
|
1325
1669
|
function activeActor(projectDir) {
|
|
1326
|
-
const missionsDir =
|
|
1327
|
-
if (!
|
|
1670
|
+
const missionsDir = join16(projectDir, ".mugiwara", "missions");
|
|
1671
|
+
if (!existsSync11(missionsDir))
|
|
1328
1672
|
return null;
|
|
1329
|
-
const missions =
|
|
1673
|
+
const missions = readdirSync7(missionsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1330
1674
|
let latest = null;
|
|
1331
1675
|
for (const mission of missions) {
|
|
1332
|
-
const d =
|
|
1333
|
-
for (const f of
|
|
1676
|
+
const d = join16(missionsDir, mission);
|
|
1677
|
+
for (const f of readdirSync7(d).filter(isStateFile)) {
|
|
1334
1678
|
try {
|
|
1335
|
-
const s = JSON.parse(
|
|
1679
|
+
const s = JSON.parse(readFileSync11(join16(d, f), "utf8"));
|
|
1336
1680
|
const t = Date.parse(s.updated_at || "") || 0;
|
|
1337
1681
|
if (!latest || t > latest.updated)
|
|
1338
1682
|
latest = { actor: s.actor || null, updated: t };
|
|
@@ -1342,8 +1686,8 @@ function activeActor(projectDir) {
|
|
|
1342
1686
|
return latest ? latest.actor : null;
|
|
1343
1687
|
}
|
|
1344
1688
|
function resetMission(projectDir, keepLogs, force) {
|
|
1345
|
-
const root =
|
|
1346
|
-
if (!
|
|
1689
|
+
const root = join16(projectDir, ".mugiwara");
|
|
1690
|
+
if (!existsSync11(root))
|
|
1347
1691
|
return { removed: [], kept: [] };
|
|
1348
1692
|
if (!force) {
|
|
1349
1693
|
const actor = activeActor(projectDir);
|
|
@@ -1354,49 +1698,49 @@ function resetMission(projectDir, keepLogs, force) {
|
|
|
1354
1698
|
const removed = [];
|
|
1355
1699
|
const kept = [];
|
|
1356
1700
|
for (const dir of ["missions", "spec", "plans", "results", "review", "issues", "reports", "state", "continue"]) {
|
|
1357
|
-
const p =
|
|
1358
|
-
if (
|
|
1701
|
+
const p = join16(root, dir);
|
|
1702
|
+
if (existsSync11(p)) {
|
|
1359
1703
|
rmSync2(p, { recursive: true, force: true });
|
|
1360
1704
|
removed.push(dir);
|
|
1361
1705
|
}
|
|
1362
1706
|
}
|
|
1363
|
-
for (const f of
|
|
1707
|
+
for (const f of readdirSync7(root)) {
|
|
1364
1708
|
if (/^state(-.+)?\.json$/.test(f)) {
|
|
1365
|
-
const p =
|
|
1366
|
-
if (
|
|
1709
|
+
const p = join16(root, f);
|
|
1710
|
+
if (existsSync11(p)) {
|
|
1367
1711
|
rmSync2(p);
|
|
1368
1712
|
removed.push(f);
|
|
1369
1713
|
}
|
|
1370
1714
|
}
|
|
1371
1715
|
}
|
|
1372
1716
|
if (!keepLogs) {
|
|
1373
|
-
for (const p of [
|
|
1374
|
-
if (
|
|
1717
|
+
for (const p of [join16(root, "lessons.md"), join16(root, "logs")]) {
|
|
1718
|
+
if (existsSync11(p)) {
|
|
1375
1719
|
rmSync2(p, { recursive: true, force: true });
|
|
1376
|
-
removed.push(p.startsWith(
|
|
1720
|
+
removed.push(p.startsWith(join16(root, "logs")) ? "logs" : "lessons.md");
|
|
1377
1721
|
}
|
|
1378
1722
|
}
|
|
1379
1723
|
} else {
|
|
1380
|
-
if (
|
|
1724
|
+
if (existsSync11(join16(root, "lessons.md")))
|
|
1381
1725
|
kept.push("lessons.md");
|
|
1382
|
-
else if (
|
|
1383
|
-
kept.push(
|
|
1726
|
+
else if (existsSync11(join16(root, join16("logs", "lessons.md"))))
|
|
1727
|
+
kept.push(join16("logs", "lessons.md"));
|
|
1384
1728
|
}
|
|
1385
1729
|
for (const f of ["config", "manifest.json", "backup"]) {
|
|
1386
|
-
if (
|
|
1730
|
+
if (existsSync11(join16(root, f)))
|
|
1387
1731
|
kept.push(f);
|
|
1388
1732
|
}
|
|
1389
1733
|
return { removed, kept };
|
|
1390
1734
|
}
|
|
1391
1735
|
function archiveMission(projectDir, mission, opts = {}) {
|
|
1392
1736
|
const { dryRun = false } = opts;
|
|
1393
|
-
const root =
|
|
1737
|
+
const root = join16(projectDir, ".mugiwara");
|
|
1394
1738
|
if (!mission || /[^a-zA-Z0-9._-]/.test(mission) || /^\.+$/.test(mission))
|
|
1395
1739
|
throw new Error(`invalid mission name "${mission}" (allowlist: [a-zA-Z0-9._-], not a dot-path)`);
|
|
1396
1740
|
const removed = [];
|
|
1397
1741
|
const kept = [];
|
|
1398
|
-
const dir =
|
|
1399
|
-
if (!
|
|
1742
|
+
const dir = join16(root, "missions", mission);
|
|
1743
|
+
if (!existsSync11(dir))
|
|
1400
1744
|
return { report: null, removed, kept };
|
|
1401
1745
|
if (!dryRun) {
|
|
1402
1746
|
const issues = checkTrail(dir, projectDir);
|
|
@@ -1404,12 +1748,16 @@ function archiveMission(projectDir, mission, opts = {}) {
|
|
|
1404
1748
|
throw new Error(`closure integrity gate failed — fix these before archiving:
|
|
1405
1749
|
${formatIssues(issues)}`);
|
|
1406
1750
|
}
|
|
1751
|
+
const artifacts = checkMissionArtifacts(dir);
|
|
1752
|
+
if (!artifacts.ok) {
|
|
1753
|
+
throw new Error(`archive artifact gate failed — missing: ${artifacts.missing.join(", ")} (lane ${artifacts.lane}). Write the evidence trail before archiving.`);
|
|
1754
|
+
}
|
|
1407
1755
|
}
|
|
1408
|
-
const files =
|
|
1756
|
+
const files = readdirSync7(dir);
|
|
1409
1757
|
const state = primaryState(dir, files);
|
|
1410
1758
|
const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
|
|
1411
1759
|
try {
|
|
1412
|
-
const s = JSON.parse(
|
|
1760
|
+
const s = JSON.parse(readFileSync11(join16(dir, f), "utf8"));
|
|
1413
1761
|
return typeof s.model === "string" ? s.model : "";
|
|
1414
1762
|
} catch {
|
|
1415
1763
|
return "";
|
|
@@ -1420,22 +1768,36 @@ ${formatIssues(issues)}`);
|
|
|
1420
1768
|
const chars = measureContextChars(dir);
|
|
1421
1769
|
const budget = readBudgetConfig(projectDir);
|
|
1422
1770
|
const footprintLine = formatFootprint(chars, budget);
|
|
1423
|
-
if (budget && chars > budget) {
|
|
1424
|
-
throw new Error(`closure context budget failed — ${footprintLine}. Trim the trail or raise context_budget_chars.`);
|
|
1425
|
-
}
|
|
1426
1771
|
const est = typeof state.tokens_est === "number" ? state.tokens_est : 0;
|
|
1427
1772
|
const src = typeof state.tokens_source === "string" ? state.tokens_source : "computed";
|
|
1428
1773
|
const lane = typeof state.lane === "string" ? state.lane : "unknown";
|
|
1429
|
-
const laneBudget = lane
|
|
1774
|
+
const laneBudget = budgetForLane(lane);
|
|
1775
|
+
const env = costEnvelope({ lane, budget: laneBudget, tokens_est: est });
|
|
1430
1776
|
const effBudget = budget || laneBudget;
|
|
1431
|
-
const pct = effBudget ? Math.round(est / effBudget * 100) : 0;
|
|
1432
1777
|
const delta = effBudget ? est <= effBudget ? `${(effBudget - est).toLocaleString()} under` : `${(est - effBudget).toLocaleString()} over` : "no budget configured";
|
|
1433
1778
|
const srcLabel = src === "reported" ? "provider-reported" : "estimator";
|
|
1779
|
+
const statusLabel = env.status.toUpperCase();
|
|
1780
|
+
const ctxStatus = contextStatus(budget, chars);
|
|
1781
|
+
const registry = loadRegistry(dir);
|
|
1782
|
+
const reads_total = registry.reduce((s, e) => s + e.reads, 0);
|
|
1783
|
+
const repeated_reads = registry.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
|
|
1784
|
+
const unique_chars = registry.reduce((s, e) => s + (e.chars ?? 0), 0);
|
|
1785
|
+
const total_chars = registry.reduce((s, e) => s + (e.chars ?? 0) * e.reads, 0);
|
|
1786
|
+
const charTracked = registry.length > 0 && total_chars > 0;
|
|
1787
|
+
const metrics = registry.length ? computeContextMetrics({
|
|
1788
|
+
files_loaded: registry.length,
|
|
1789
|
+
reads_total,
|
|
1790
|
+
reads_reused: repeated_reads,
|
|
1791
|
+
unique_chars,
|
|
1792
|
+
total_chars,
|
|
1793
|
+
repeated_reads
|
|
1794
|
+
}) : { files_loaded: 0, repeated_reads: 0, duplicate_chars: 0, reuse_rate: 0, read_avoidance_chars: 0 };
|
|
1795
|
+
const ctxNote = registry.length ? charTracked ? "" : " (char data not tracked)" : " (no registry — reads not tracked)";
|
|
1434
1796
|
let reportedTotal = 0;
|
|
1435
1797
|
let hasReported = false;
|
|
1436
1798
|
for (const f of files.filter(isStateFile)) {
|
|
1437
1799
|
try {
|
|
1438
|
-
const s = JSON.parse(
|
|
1800
|
+
const s = JSON.parse(readFileSync11(join16(dir, f), "utf8"));
|
|
1439
1801
|
if (s.tokens_source === "reported" && typeof s.tokens_est === "number") {
|
|
1440
1802
|
reportedTotal += s.tokens_est;
|
|
1441
1803
|
hasReported = true;
|
|
@@ -1452,17 +1814,57 @@ ${formatIssues(issues)}`);
|
|
|
1452
1814
|
"| Metric | Value |",
|
|
1453
1815
|
"|--------|-------|",
|
|
1454
1816
|
`| **Tokens used** | ${est.toLocaleString()} (${srcLabel}) |`,
|
|
1455
|
-
`| **Lane** | ${lane} (budget ${effBudget ? effBudget.toLocaleString() : "—"} · warn ${effBudget ?
|
|
1456
|
-
`| **Budget status** | ${effBudget ? `${pct}% of budget · ${delta} · ${
|
|
1457
|
-
`| **Context footprint** | ${chars.toLocaleString()} chars${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"}
|
|
1817
|
+
`| **Lane** | ${lane} (budget ${effBudget ? effBudget.toLocaleString() : "—"} · warn ${effBudget ? env.warn_at.toLocaleString() : "—"} · stop ${effBudget ? env.stop_at.toLocaleString() : "—"}) |`,
|
|
1818
|
+
`| **Budget status** | ${effBudget ? `${env.pct}% of budget · ${delta} · ${statusLabel}` : "no lane budget"} |`,
|
|
1819
|
+
`| **Context footprint** | ${chars.toLocaleString()} chars${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
|
|
1820
|
+
`| **Context budget status** | ${ctxStatus.toUpperCase()}${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
|
|
1821
|
+
`| **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} |`
|
|
1458
1822
|
].join(`
|
|
1459
1823
|
`);
|
|
1460
1824
|
if (hasReported) {
|
|
1461
1825
|
costSection += `
|
|
1462
1826
|
| **Provider total** | ${reportedTotal.toLocaleString()} (provider-reported — sum of reported stages) |`;
|
|
1463
1827
|
}
|
|
1828
|
+
try {
|
|
1829
|
+
const ledger = buildCostLedger({ missionDir: dir, envelope: env });
|
|
1830
|
+
costSection += `
|
|
1831
|
+
| Budget | ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned}) |`;
|
|
1832
|
+
costSection += `
|
|
1833
|
+
| Context | ${chars.toLocaleString()} chars, reuse ${ledger.efficiency.reuse_rate} |`;
|
|
1834
|
+
costSection += `
|
|
1835
|
+
| Avoided | ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ${ledger.avoided.tokens_avoided_est} tokens est |`;
|
|
1836
|
+
costSection += `
|
|
1837
|
+
| Efficiency | reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}% |`;
|
|
1838
|
+
costSection += `
|
|
1839
|
+
| Trail | ${ledger.trail.length} decisions |`;
|
|
1840
|
+
if (ledger.trail.length) {
|
|
1841
|
+
const show = ledger.trail.slice(0, 5);
|
|
1842
|
+
for (const t of show)
|
|
1843
|
+
costSection += `
|
|
1844
|
+
- ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ""}`;
|
|
1845
|
+
if (ledger.trail.length > 5)
|
|
1846
|
+
costSection += `
|
|
1847
|
+
… ${ledger.trail.length - 5} more`;
|
|
1848
|
+
}
|
|
1849
|
+
} catch {}
|
|
1464
1850
|
costSection += `
|
|
1465
1851
|
`;
|
|
1852
|
+
try {
|
|
1853
|
+
costSection += renderAdaptationSection(dir);
|
|
1854
|
+
} catch {}
|
|
1855
|
+
appendCostEvent(dir, {
|
|
1856
|
+
kind: "closure",
|
|
1857
|
+
mission,
|
|
1858
|
+
tokens_est: est,
|
|
1859
|
+
budget: laneBudget,
|
|
1860
|
+
status: env.status,
|
|
1861
|
+
context_chars: chars,
|
|
1862
|
+
context_status: ctxStatus,
|
|
1863
|
+
context_metrics: metrics
|
|
1864
|
+
});
|
|
1865
|
+
if (budget && chars > budget) {
|
|
1866
|
+
throw new Error(`closure context budget failed — ${footprintLine}. Trim the trail or raise context_budget_chars.`);
|
|
1867
|
+
}
|
|
1466
1868
|
}
|
|
1467
1869
|
const FOLD_TOP = ["decisions.md", "blockers.md", "review.md", "security.md", "spec.md"];
|
|
1468
1870
|
const fold = [];
|
|
@@ -1470,25 +1872,40 @@ ${formatIssues(issues)}`);
|
|
|
1470
1872
|
if (files.includes(f))
|
|
1471
1873
|
fold.push(f);
|
|
1472
1874
|
}
|
|
1473
|
-
const
|
|
1474
|
-
const
|
|
1475
|
-
const
|
|
1875
|
+
const PR_VERDICT = "pr-verdict.md";
|
|
1876
|
+
const PR_VERDICT_SRC = join16("flows", "07-pr-verdict.md");
|
|
1877
|
+
const flowsDir = join16(dir, "flows");
|
|
1878
|
+
const legacyWavesDir = join16(dir, "waves");
|
|
1879
|
+
const artDir = existsSync11(flowsDir) ? flowsDir : existsSync11(legacyWavesDir) ? legacyWavesDir : flowsDir;
|
|
1476
1880
|
const artRel = artDir === legacyWavesDir ? "waves" : "flows";
|
|
1477
|
-
if (
|
|
1478
|
-
for (const f of
|
|
1479
|
-
|
|
1881
|
+
if (existsSync11(artDir)) {
|
|
1882
|
+
for (const f of readdirSync7(artDir).sort()) {
|
|
1883
|
+
if (artRel === "flows" && f === "07-pr-verdict.md")
|
|
1884
|
+
continue;
|
|
1885
|
+
fold.push(join16(artRel, f));
|
|
1886
|
+
}
|
|
1480
1887
|
}
|
|
1888
|
+
if (existsSync11(join16(dir, "cost-events.jsonl")))
|
|
1889
|
+
fold.push("cost-events.jsonl");
|
|
1890
|
+
if (existsSync11(join16(dir, "context-registry.jsonl")))
|
|
1891
|
+
fold.push("context-registry.jsonl");
|
|
1481
1892
|
let report = "";
|
|
1482
|
-
const reportPath =
|
|
1893
|
+
const reportPath = join16(dir, "report.md");
|
|
1483
1894
|
if (files.includes("report.md"))
|
|
1484
|
-
report =
|
|
1485
|
-
else if (
|
|
1486
|
-
report =
|
|
1895
|
+
report = readFileSync11(reportPath, "utf8");
|
|
1896
|
+
else if (existsSync11(join16(artDir, "06-closure.md")))
|
|
1897
|
+
report = readFileSync11(join16(artDir, "06-closure.md"), "utf8");
|
|
1487
1898
|
if (!dryRun) {
|
|
1488
|
-
|
|
1899
|
+
mkdirSync8(dir, { recursive: true });
|
|
1900
|
+
const prVerdictSrc = join16(dir, PR_VERDICT_SRC);
|
|
1901
|
+
if (existsSync11(prVerdictSrc)) {
|
|
1902
|
+
const prVerdictPath = join16(dir, PR_VERDICT);
|
|
1903
|
+
writeFileSync8(prVerdictPath, readFileSync11(prVerdictSrc, "utf8"));
|
|
1904
|
+
kept.push(join16("missions", mission, PR_VERDICT));
|
|
1905
|
+
}
|
|
1489
1906
|
if (fold.length) {
|
|
1490
1907
|
const sections = fold.map((f) => {
|
|
1491
|
-
const body =
|
|
1908
|
+
const body = readFileSync11(join16(dir, f), "utf8").trim();
|
|
1492
1909
|
const name = f.includes("/") ? f.split("/").pop() ?? f : f;
|
|
1493
1910
|
return `
|
|
1494
1911
|
|
|
@@ -1502,22 +1919,26 @@ ${body}`;
|
|
|
1502
1919
|
evidence: Array.isArray(state.evidence) ? state.evidence : [],
|
|
1503
1920
|
sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
|
|
1504
1921
|
}), mission) : "";
|
|
1505
|
-
|
|
1922
|
+
writeFileSync8(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
|
|
1506
1923
|
${costSection}
|
|
1507
1924
|
` : "") + `
|
|
1508
1925
|
`);
|
|
1509
1926
|
renameSync(tmp, reportPath);
|
|
1510
1927
|
}
|
|
1511
1928
|
for (const f of fold) {
|
|
1512
|
-
rmSync2(
|
|
1513
|
-
removed.push(
|
|
1929
|
+
rmSync2(join16(dir, f), { force: true, recursive: true });
|
|
1930
|
+
removed.push(join16("missions", mission, f));
|
|
1931
|
+
}
|
|
1932
|
+
if (existsSync11(prVerdictSrc)) {
|
|
1933
|
+
rmSync2(join16(dir, PR_VERDICT_SRC), { force: true });
|
|
1934
|
+
removed.push(join16("missions", mission, PR_VERDICT_SRC));
|
|
1514
1935
|
}
|
|
1515
1936
|
for (const f of files.filter((f2) => f2.endsWith(".json")))
|
|
1516
|
-
rmSync2(
|
|
1517
|
-
if (
|
|
1937
|
+
rmSync2(join16(dir, f), { force: true });
|
|
1938
|
+
if (existsSync11(artDir) && readdirSync7(artDir).length === 0)
|
|
1518
1939
|
rmSync2(artDir, { recursive: true, force: true });
|
|
1519
|
-
if (!
|
|
1520
|
-
|
|
1940
|
+
if (!existsSync11(reportPath)) {
|
|
1941
|
+
writeFileSync8(reportPath, `# Mission: ${mission}
|
|
1521
1942
|
|
|
1522
1943
|
Archived before closure — no wave artifacts were present.
|
|
1523
1944
|
`);
|
|
@@ -1529,7 +1950,7 @@ Archived before closure — no wave artifacts were present.
|
|
|
1529
1950
|
baseSha: typeof state.base_sha === "string" ? state.base_sha : "unknown"
|
|
1530
1951
|
});
|
|
1531
1952
|
if (rb)
|
|
1532
|
-
kept.push(
|
|
1953
|
+
kept.push(join16("missions", mission, rb.file));
|
|
1533
1954
|
try {
|
|
1534
1955
|
writeProvenance(projectDir, dir, {
|
|
1535
1956
|
mission,
|
|
@@ -1542,25 +1963,25 @@ Archived before closure — no wave artifacts were present.
|
|
|
1542
1963
|
evidence: Array.isArray(state.evidence) ? state.evidence : [],
|
|
1543
1964
|
models: stageModels
|
|
1544
1965
|
});
|
|
1545
|
-
kept.push(
|
|
1966
|
+
kept.push(join16("missions", mission, "provenance.md"));
|
|
1546
1967
|
} catch {}
|
|
1547
1968
|
}
|
|
1548
1969
|
} else {
|
|
1549
1970
|
for (const f of fold)
|
|
1550
|
-
removed.push(
|
|
1971
|
+
removed.push(join16("missions", mission, f));
|
|
1551
1972
|
}
|
|
1552
|
-
removed.push(
|
|
1973
|
+
removed.push(join16("missions", mission, "<session state>"));
|
|
1553
1974
|
if (files.includes("plan.md"))
|
|
1554
|
-
kept.push(
|
|
1975
|
+
kept.push(join16("missions", mission, "plan.md"));
|
|
1555
1976
|
if (files.includes("handoff.md"))
|
|
1556
|
-
kept.push(
|
|
1557
|
-
kept.push(
|
|
1977
|
+
kept.push(join16("missions", mission, "handoff.md"));
|
|
1978
|
+
kept.push(join16("missions", mission, "report.md"));
|
|
1558
1979
|
let index;
|
|
1559
|
-
const indexFile =
|
|
1980
|
+
const indexFile = join16(root, "index.md");
|
|
1560
1981
|
const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}
|
|
1561
1982
|
`;
|
|
1562
1983
|
if (!dryRun) {
|
|
1563
|
-
const existing =
|
|
1984
|
+
const existing = existsSync11(indexFile) ? readFileSync11(indexFile, "utf8") : "";
|
|
1564
1985
|
if (!existing.split(/\r?\n/).some((l) => l.startsWith(`- ${mission} —`))) {
|
|
1565
1986
|
const header = existing ? "" : `# Mission index
|
|
1566
1987
|
|
|
@@ -1574,28 +1995,28 @@ Archived before closure — no wave artifacts were present.
|
|
|
1574
1995
|
}
|
|
1575
1996
|
index = "index.md";
|
|
1576
1997
|
}
|
|
1577
|
-
return { report:
|
|
1998
|
+
return { report: join16("missions", mission, "report.md"), removed, kept, index };
|
|
1578
1999
|
}
|
|
1579
2000
|
|
|
1580
2001
|
// src/run.ts
|
|
1581
|
-
import { existsSync as
|
|
2002
|
+
import { existsSync as existsSync12, readdirSync as readdirSync8 } from "node:fs";
|
|
1582
2003
|
import { spawnSync } from "node:child_process";
|
|
1583
|
-
import { dirname as dirname5, join as
|
|
2004
|
+
import { dirname as dirname5, join as join17, resolve } from "node:path";
|
|
1584
2005
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1585
2006
|
var here3 = dirname5(fileURLToPath4(import.meta.url));
|
|
1586
|
-
var SCRIPTS_DIR =
|
|
2007
|
+
var SCRIPTS_DIR = join17(here3, "..", "scripts");
|
|
1587
2008
|
var RUNNABLE = ["savepoint.sh", "lane.sh"];
|
|
1588
2009
|
function findBash() {
|
|
1589
2010
|
const explicit = process.env.MUGIWARA_BASH?.trim();
|
|
1590
2011
|
if (explicit)
|
|
1591
|
-
return
|
|
2012
|
+
return existsSync12(explicit) ? explicit : null;
|
|
1592
2013
|
const candidates = process.platform === "win32" ? [
|
|
1593
2014
|
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
1594
2015
|
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
1595
|
-
|
|
2016
|
+
join17(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
|
|
1596
2017
|
] : ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash", "/opt/homebrew/bin/bash"];
|
|
1597
2018
|
for (const p of candidates) {
|
|
1598
|
-
if (p &&
|
|
2019
|
+
if (p && existsSync12(p))
|
|
1599
2020
|
return p;
|
|
1600
2021
|
}
|
|
1601
2022
|
const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bash"], {
|
|
@@ -1612,9 +2033,9 @@ function runScript(name, args, projectDir) {
|
|
|
1612
2033
|
if (!/^[a-z0-9-]+\.sh$/.test(name)) {
|
|
1613
2034
|
throw new Error(`invalid script name "${name}" (expected one of: ${RUNNABLE.join(", ")})`);
|
|
1614
2035
|
}
|
|
1615
|
-
const script =
|
|
1616
|
-
if (!
|
|
1617
|
-
const have =
|
|
2036
|
+
const script = join17(SCRIPTS_DIR, name);
|
|
2037
|
+
if (!existsSync12(script)) {
|
|
2038
|
+
const have = existsSync12(SCRIPTS_DIR) ? readdirSync8(SCRIPTS_DIR).filter((f) => f.endsWith(".sh")) : [];
|
|
1618
2039
|
throw new Error(`script not found: ${name}${have.length ? ` (available: ${have.join(", ")})` : ""}`);
|
|
1619
2040
|
}
|
|
1620
2041
|
const bash = findBash();
|
|
@@ -1631,9 +2052,9 @@ function runScript(name, args, projectDir) {
|
|
|
1631
2052
|
}
|
|
1632
2053
|
|
|
1633
2054
|
// src/continue.ts
|
|
1634
|
-
import { existsSync as
|
|
2055
|
+
import { existsSync as existsSync13, readFileSync as readFileSync12, readdirSync as readdirSync9 } from "node:fs";
|
|
1635
2056
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
1636
|
-
import { join as
|
|
2057
|
+
import { join as join18 } from "node:path";
|
|
1637
2058
|
var SAFE = /^[A-Za-z0-9._-]+$/;
|
|
1638
2059
|
var isSafeKey = (s) => SAFE.test(s) && !/^\.+$/.test(s);
|
|
1639
2060
|
var num = (v) => {
|
|
@@ -1662,14 +2083,14 @@ function gitActor(cwd) {
|
|
|
1662
2083
|
return name || process.env.USER || process.env.USERNAME || "";
|
|
1663
2084
|
}
|
|
1664
2085
|
function scan(projectDir, kind, map) {
|
|
1665
|
-
const base =
|
|
1666
|
-
if (!
|
|
2086
|
+
const base = join18(projectDir, ".mugiwara", "missions");
|
|
2087
|
+
if (!existsSync13(base))
|
|
1667
2088
|
return [];
|
|
1668
2089
|
const out = [];
|
|
1669
|
-
const missions =
|
|
2090
|
+
const missions = readdirSync9(base, { withFileTypes: true }).filter((e) => e.isDirectory() && isSafeKey(e.name)).map((e) => e.name).sort();
|
|
1670
2091
|
for (const mission of missions) {
|
|
1671
|
-
const dir =
|
|
1672
|
-
for (const f of
|
|
2092
|
+
const dir = join18(base, mission);
|
|
2093
|
+
for (const f of readdirSync9(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
1673
2094
|
const stem = f.slice(0, -".json".length);
|
|
1674
2095
|
let member;
|
|
1675
2096
|
if (kind === "state") {
|
|
@@ -1687,7 +2108,7 @@ function scan(projectDir, kind, map) {
|
|
|
1687
2108
|
if (member !== null && !isSafeKey(member))
|
|
1688
2109
|
continue;
|
|
1689
2110
|
try {
|
|
1690
|
-
const raw = JSON.parse(
|
|
2111
|
+
const raw = JSON.parse(readFileSync12(join18(dir, f), "utf8"));
|
|
1691
2112
|
if (text(raw.mission) !== mission)
|
|
1692
2113
|
continue;
|
|
1693
2114
|
out.push(map(raw, member));
|
|
@@ -1804,9 +2225,10 @@ function formatResume(e) {
|
|
|
1804
2225
|
|
|
1805
2226
|
// src/sign.ts
|
|
1806
2227
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
1807
|
-
import { existsSync as
|
|
2228
|
+
import { chmodSync as chmodSync2, existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
|
|
2229
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
|
|
1808
2230
|
import { homedir as homedir3 } from "node:os";
|
|
1809
|
-
import { join as
|
|
2231
|
+
import { join as join19 } from "node:path";
|
|
1810
2232
|
function signArgs(reportPath, secretKey) {
|
|
1811
2233
|
return ["-Sm", reportPath, "-s", secretKey];
|
|
1812
2234
|
}
|
|
@@ -1822,39 +2244,200 @@ function hasMinisign() {
|
|
|
1822
2244
|
}
|
|
1823
2245
|
}
|
|
1824
2246
|
function defaultKey(flag) {
|
|
1825
|
-
return
|
|
2247
|
+
return join19(homedir3(), ".mugiwara", flag === "secret" ? "minisign.key" : "minisign.pub");
|
|
2248
|
+
}
|
|
2249
|
+
function generatePureKey() {
|
|
2250
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
2251
|
+
const privJwk = privateKey.export({ format: "jwk" });
|
|
2252
|
+
const pubJwk = publicKey.export({ format: "jwk" });
|
|
2253
|
+
return {
|
|
2254
|
+
key: Buffer.from(privJwk.d, "base64url").toString("base64"),
|
|
2255
|
+
pub: Buffer.from(pubJwk.x, "base64url").toString("base64")
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
function ensurePureKey(homeDir) {
|
|
2259
|
+
const dir = join19(homeDir, ".mugiwara");
|
|
2260
|
+
const keyPath = join19(dir, "mugiwara.key");
|
|
2261
|
+
const pubPath = join19(dir, "mugiwara.pub");
|
|
2262
|
+
if (!existsSync14(keyPath) || !existsSync14(pubPath)) {
|
|
2263
|
+
mkdirSync9(dir, { recursive: true });
|
|
2264
|
+
const { key, pub } = generatePureKey();
|
|
2265
|
+
if (!existsSync14(keyPath)) {
|
|
2266
|
+
writeFileSync9(keyPath, key + `
|
|
2267
|
+
`, { mode: 384 });
|
|
2268
|
+
}
|
|
2269
|
+
if (!existsSync14(pubPath))
|
|
2270
|
+
writeFileSync9(pubPath, pub + `
|
|
2271
|
+
`);
|
|
2272
|
+
}
|
|
2273
|
+
try {
|
|
2274
|
+
chmodSync2(keyPath, 384);
|
|
2275
|
+
} catch {}
|
|
2276
|
+
return dir;
|
|
2277
|
+
}
|
|
2278
|
+
function pureSign(content, seedBase64, opts) {
|
|
2279
|
+
const seed = Buffer.from(seedBase64.trim(), "base64");
|
|
2280
|
+
if (seed.length !== 32)
|
|
2281
|
+
return { ok: false, message: "invalid seed (want 32B base64)" };
|
|
2282
|
+
const pubBuf = Buffer.from(opts.pub.trim(), "base64");
|
|
2283
|
+
if (pubBuf.length !== 32)
|
|
2284
|
+
return { ok: false, message: "invalid pub (want 32B base64)" };
|
|
2285
|
+
const privateKey = createPrivateKey({
|
|
2286
|
+
key: {
|
|
2287
|
+
kty: "OKP",
|
|
2288
|
+
crv: "Ed25519",
|
|
2289
|
+
d: seed.toString("base64url"),
|
|
2290
|
+
x: pubBuf.toString("base64url")
|
|
2291
|
+
},
|
|
2292
|
+
format: "jwk"
|
|
2293
|
+
});
|
|
2294
|
+
const sig = sign(null, Buffer.from(content, "utf8"), privateKey).toString("base64");
|
|
2295
|
+
const out = { algo: "ed25519-pure", sig, pub: opts.pub, mission: opts.mission, commit: opts.commit, ts: opts.ts };
|
|
2296
|
+
if (opts.outputPath)
|
|
2297
|
+
writeFileSync9(opts.outputPath, JSON.stringify(out, null, 2) + `
|
|
2298
|
+
`);
|
|
2299
|
+
return { ...out, ok: true };
|
|
2300
|
+
}
|
|
2301
|
+
function pureVerify(content, sig) {
|
|
2302
|
+
try {
|
|
2303
|
+
const pub = Buffer.from(sig.pub, "base64");
|
|
2304
|
+
if (pub.length !== 32)
|
|
2305
|
+
return false;
|
|
2306
|
+
const publicKey = createPublicKey({
|
|
2307
|
+
key: { kty: "OKP", crv: "Ed25519", x: pub.toString("base64url") },
|
|
2308
|
+
format: "jwk"
|
|
2309
|
+
});
|
|
2310
|
+
return verify(null, Buffer.from(content, "utf8"), publicKey, Buffer.from(sig.sig, "base64"));
|
|
2311
|
+
} catch {
|
|
2312
|
+
return false;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
function resolveBackend(configured, env) {
|
|
2316
|
+
switch (configured) {
|
|
2317
|
+
case "off":
|
|
2318
|
+
return "off";
|
|
2319
|
+
case "minisign":
|
|
2320
|
+
return env.hasMinisign ? "minisign" : "minisign-fail";
|
|
2321
|
+
case "pure":
|
|
2322
|
+
return "pure";
|
|
2323
|
+
case "auto":
|
|
2324
|
+
default:
|
|
2325
|
+
return env.hasMinisign && env.hasKey ? "minisign" : "pure";
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
function configuredBackend(projectDir) {
|
|
2329
|
+
return readConfig(projectDir).sign;
|
|
2330
|
+
}
|
|
2331
|
+
function missionMeta(projectDir, mission) {
|
|
2332
|
+
let commit = "unknown";
|
|
2333
|
+
try {
|
|
2334
|
+
commit = execFileSync5("git", ["rev-parse", "HEAD"], { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
2335
|
+
} catch {}
|
|
2336
|
+
return { commit, ts: new Date().toISOString() };
|
|
1826
2337
|
}
|
|
1827
2338
|
function signReport(projectDir, missionDir) {
|
|
1828
|
-
const report =
|
|
1829
|
-
if (!
|
|
2339
|
+
const report = join19(missionDir, "report.md");
|
|
2340
|
+
if (!existsSync14(report))
|
|
1830
2341
|
return { ok: false, message: "no report.md to sign — archive first" };
|
|
1831
|
-
|
|
1832
|
-
|
|
2342
|
+
const mission = missionDir.split(join19(".mugiwara", "missions", "")).pop() ?? "unknown";
|
|
2343
|
+
const backend = resolveBackend(configuredBackend(projectDir), { hasMinisign: hasMinisign(), hasKey: existsSync14(defaultKey("secret")) });
|
|
2344
|
+
if (backend === "off")
|
|
2345
|
+
return { ok: false, message: "signing disabled (sign=off)" };
|
|
2346
|
+
if (backend === "minisign-fail")
|
|
2347
|
+
return { ok: false, message: "sign=minisign but minisign not installed — install it or set sign=pure" };
|
|
2348
|
+
if (backend === "minisign") {
|
|
2349
|
+
const secretKey = process.env.MUGIWARA_SIGN_KEY?.trim() || defaultKey("secret");
|
|
2350
|
+
try {
|
|
2351
|
+
execFileSync5("minisign", signArgs(report, secretKey), { cwd: projectDir, stdio: "pipe", input: process.env.MUGIWARA_SIGN_PASSWORD ?? "" });
|
|
2352
|
+
return { ok: true, message: `signed ${report}.minisig (minisign, key: ${secretKey})` };
|
|
2353
|
+
} catch (e) {
|
|
2354
|
+
return { ok: false, message: `signing failed: ${e.message}` };
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
const dir = ensurePureKey(homedir3());
|
|
2358
|
+
const seed = process.env.MUGIWARA_SIGN_KEY?.trim() || readFileSyncSafe2(join19(dir, "mugiwara.key"));
|
|
2359
|
+
const pub = process.env.MUGIWARA_SIGN_PUB?.trim() || readFileSyncSafe2(join19(dir, "mugiwara.pub"));
|
|
2360
|
+
if (!seed || !pub)
|
|
2361
|
+
return { ok: false, message: "pure keys missing — run `mugiwara sign --gen-key --backend pure`" };
|
|
2362
|
+
const content = readFileSafe(report);
|
|
2363
|
+
if (content === null)
|
|
2364
|
+
return { ok: false, message: `cannot read ${report}` };
|
|
2365
|
+
const { commit, ts } = missionMeta(projectDir, mission);
|
|
2366
|
+
const sig = pureSign(content, seed, { mission, commit, ts, pub, outputPath: `${report}.mugisig` });
|
|
2367
|
+
if (!sig.ok)
|
|
2368
|
+
return { ok: false, message: `signing failed: ${sig.message}` };
|
|
2369
|
+
return { ok: true, message: `signed ${report}.mugisig (pure ed25519, key: ${join19(dir, "mugiwara.key")})` };
|
|
2370
|
+
}
|
|
2371
|
+
function verifyReport(projectDir, missionDir) {
|
|
2372
|
+
const report = join19(missionDir, "report.md");
|
|
2373
|
+
const minisig = `${report}.minisig`;
|
|
2374
|
+
const mugisig = `${report}.mugisig`;
|
|
2375
|
+
if (!existsSync14(minisig) && !existsSync14(mugisig)) {
|
|
2376
|
+
return { ok: false, message: "not signed (no .minisig or .mugisig beside report.md)" };
|
|
2377
|
+
}
|
|
2378
|
+
if (existsSync14(minisig)) {
|
|
2379
|
+
if (!hasMinisign())
|
|
2380
|
+
return { ok: false, message: "minisig present but minisign not installed — cannot verify that signature" };
|
|
2381
|
+
const pubKey = existsSync14(defaultKey("public")) ? defaultKey("public") : null;
|
|
2382
|
+
try {
|
|
2383
|
+
execFileSync5("minisign", verifyArgs(report, pubKey), { cwd: projectDir, stdio: "pipe" });
|
|
2384
|
+
return { ok: true, message: "signature verifies against report.md (minisig)" };
|
|
2385
|
+
} catch {
|
|
2386
|
+
return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (minisig)" };
|
|
2387
|
+
}
|
|
1833
2388
|
}
|
|
1834
|
-
const secretKey = process.env.MUGIWARA_SIGN_KEY?.trim() || defaultKey("secret");
|
|
1835
2389
|
try {
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
2390
|
+
const parsed = JSON.parse(readFileSafe(mugisig) ?? "{}");
|
|
2391
|
+
const content = readFileSafe(report);
|
|
2392
|
+
if (content === null || parsed.algo !== "ed25519-pure")
|
|
2393
|
+
return { ok: false, message: "invalid .mugisig file" };
|
|
2394
|
+
return pureVerify(content, parsed) ? { ok: true, message: "signature verifies against report.md (mugisig, ed25519-pure)" } : { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (mugisig)" };
|
|
2395
|
+
} catch {
|
|
2396
|
+
return { ok: false, message: "invalid .mugisig file" };
|
|
1840
2397
|
}
|
|
1841
2398
|
}
|
|
1842
|
-
function
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
return
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
2399
|
+
function readFileSafe(p) {
|
|
2400
|
+
try {
|
|
2401
|
+
return readFileSync13(p, "utf8");
|
|
2402
|
+
} catch {
|
|
2403
|
+
return null;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
function readFileSyncSafe2(p) {
|
|
1850
2407
|
try {
|
|
1851
|
-
|
|
1852
|
-
return { ok: true, message: "signature verifies against report.md" };
|
|
2408
|
+
return readFileSync13(p, "utf8").trim();
|
|
1853
2409
|
} catch {
|
|
1854
|
-
return
|
|
2410
|
+
return "";
|
|
1855
2411
|
}
|
|
1856
2412
|
}
|
|
1857
2413
|
|
|
2414
|
+
// src/slop.ts
|
|
2415
|
+
function detectHealingSlop(input) {
|
|
2416
|
+
const kind = "healing";
|
|
2417
|
+
const max = input.max_cycles ?? 3;
|
|
2418
|
+
const hasZeroHistory = input.history_fixes.some((n) => n === 0);
|
|
2419
|
+
if (input.fixes_in_cycle === 0 && hasZeroHistory) {
|
|
2420
|
+
return { slop: true, reason: `slop: healing — no fixes in cycle ${input.cycle} with previous zero-fix cycle`, kind };
|
|
2421
|
+
}
|
|
2422
|
+
if (input.cycle >= max && input.fixes_in_cycle === 0) {
|
|
2423
|
+
return { slop: true, reason: `slop: healing — cycle ${input.cycle} ≥ max ${max} with no fixes`, kind };
|
|
2424
|
+
}
|
|
2425
|
+
return { slop: false, reason: "no slop — healing making progress", kind };
|
|
2426
|
+
}
|
|
2427
|
+
function computeLiveSlop(input) {
|
|
2428
|
+
const rows = [];
|
|
2429
|
+
const thr = input.repeated_read_threshold ?? 3;
|
|
2430
|
+
const heal = detectHealingSlop({ cycle: input.heal_cycle, fixes_in_cycle: 0, history_fixes: [], max_cycles: input.max_heal_cycles ?? 3 });
|
|
2431
|
+
if (heal.slop)
|
|
2432
|
+
rows.push({ role: "Brook", kind: "healing", reason: heal.reason });
|
|
2433
|
+
if (input.repeated_reads >= thr)
|
|
2434
|
+
rows.push({ role: "all", kind: "context", reason: `repeated reads ${input.repeated_reads} ≥ ${thr}` });
|
|
2435
|
+
const perRole = {};
|
|
2436
|
+
for (const r of rows)
|
|
2437
|
+
perRole[r.role] = (perRole[r.role] ?? 0) + 1;
|
|
2438
|
+
return { interventions: rows.length, perRole, rows };
|
|
2439
|
+
}
|
|
2440
|
+
|
|
1858
2441
|
// src/cli.ts
|
|
1859
2442
|
var str = (v) => typeof v === "string" ? v : undefined;
|
|
1860
2443
|
var flag = (v) => v === true;
|
|
@@ -1866,6 +2449,16 @@ async function run(argv) {
|
|
|
1866
2449
|
console.log(`mugiwara ${VERSION}`);
|
|
1867
2450
|
return;
|
|
1868
2451
|
}
|
|
2452
|
+
if (command === "continue" || command === "status") {
|
|
2453
|
+
return command === "continue" ? continueCmd(flags, _) : statusCmd(flags);
|
|
2454
|
+
}
|
|
2455
|
+
const isDryRunInstall = (command === "install" || command === "update") && flag(flags.dryRun);
|
|
2456
|
+
if (!isDryRunInstall) {
|
|
2457
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2458
|
+
if (ensureConfig(projectDir)) {
|
|
2459
|
+
console.log(`default .mugiwara/config written at ${join20(projectDir, ".mugiwara", "config")} (edit it to customise)`);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
1869
2462
|
switch (command) {
|
|
1870
2463
|
case "install":
|
|
1871
2464
|
return install(flags);
|
|
@@ -1885,6 +2478,8 @@ async function run(argv) {
|
|
|
1885
2478
|
return continueCmd(flags, _);
|
|
1886
2479
|
case "status":
|
|
1887
2480
|
return statusCmd(flags);
|
|
2481
|
+
case "cost":
|
|
2482
|
+
return costCmd(flags, _);
|
|
1888
2483
|
case "run":
|
|
1889
2484
|
return runCmd(flags, _);
|
|
1890
2485
|
case "savepoint":
|
|
@@ -1936,8 +2531,8 @@ function archive(flags, positionals) {
|
|
|
1936
2531
|
function cleanCmd(flags) {
|
|
1937
2532
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1938
2533
|
const dryRun = flag(flags.dryRun);
|
|
1939
|
-
const root =
|
|
1940
|
-
if (!
|
|
2534
|
+
const root = join20(projectDir, ".mugiwara", "missions");
|
|
2535
|
+
if (!existsSync15(root)) {
|
|
1941
2536
|
console.log("nothing to clean (.mugiwara/missions/ does not exist).");
|
|
1942
2537
|
return;
|
|
1943
2538
|
}
|
|
@@ -1947,8 +2542,8 @@ function cleanCmd(flags) {
|
|
|
1947
2542
|
console.error(`invalid --before date: ${before}`);
|
|
1948
2543
|
process.exit(1);
|
|
1949
2544
|
}
|
|
1950
|
-
let candidates =
|
|
1951
|
-
const stateFiles = (m) =>
|
|
2545
|
+
let candidates = readdirSync10(root, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Za-z0-9._-]+$/.test(e.name) && !/^\.+$/.test(e.name)).map((e) => e.name);
|
|
2546
|
+
const stateFiles = (m) => readdirSync10(join20(root, m)).filter((f) => {
|
|
1952
2547
|
const stem = f.replace(/\.json$/, "");
|
|
1953
2548
|
return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
|
|
1954
2549
|
});
|
|
@@ -1958,7 +2553,7 @@ function cleanCmd(flags) {
|
|
|
1958
2553
|
return false;
|
|
1959
2554
|
for (const f of stateFiles(m)) {
|
|
1960
2555
|
try {
|
|
1961
|
-
const ts = Date.parse(JSON.parse(
|
|
2556
|
+
const ts = Date.parse(JSON.parse(readFileSync14(join20(root, m, f), "utf8")).updated_at ?? "") || 0;
|
|
1962
2557
|
if (ts === 0 || ts >= beforeMs)
|
|
1963
2558
|
return false;
|
|
1964
2559
|
} catch {
|
|
@@ -1968,7 +2563,7 @@ function cleanCmd(flags) {
|
|
|
1968
2563
|
return true;
|
|
1969
2564
|
};
|
|
1970
2565
|
if (!flag(flags.all)) {
|
|
1971
|
-
candidates = candidates.filter((m) =>
|
|
2566
|
+
candidates = candidates.filter((m) => existsSync15(join20(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
|
|
1972
2567
|
} else if (!flag(flags.force)) {
|
|
1973
2568
|
const live = candidates.filter((m) => hasLiveState(m) && !staleBefore(m));
|
|
1974
2569
|
if (live.length) {
|
|
@@ -2002,7 +2597,7 @@ async function resolveOptions(flags) {
|
|
|
2002
2597
|
scope = await choose(rl, "Install scope?", ["global (user-wide)", "project (this repo)"]) === 0 ? "global" : "project";
|
|
2003
2598
|
}
|
|
2004
2599
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2005
|
-
if (scope === "project" && !
|
|
2600
|
+
if (scope === "project" && !existsSync15(projectDir))
|
|
2006
2601
|
throw new Error(`Project dir not found: ${projectDir}`);
|
|
2007
2602
|
let targetIds = str(flags.target)?.split(",").map((s) => s.trim()) ?? null;
|
|
2008
2603
|
if (targetIds && targetIds.includes("all"))
|
|
@@ -2081,7 +2676,7 @@ async function uninstall(flags) {
|
|
|
2081
2676
|
if (!manifest) {
|
|
2082
2677
|
if (scope === "project") {
|
|
2083
2678
|
const globalFile = manifestPath({ scope: "global", projectDir, home });
|
|
2084
|
-
if (
|
|
2679
|
+
if (existsSync15(globalFile)) {
|
|
2085
2680
|
console.log(`No project manifest found, but a global install exists. Try:
|
|
2086
2681
|
mugiwara uninstall --global`);
|
|
2087
2682
|
return;
|
|
@@ -2118,13 +2713,13 @@ async function uninstall(flags) {
|
|
|
2118
2713
|
console.log(` note: ${n}`);
|
|
2119
2714
|
}
|
|
2120
2715
|
rmSync3(file);
|
|
2121
|
-
const mugiDir =
|
|
2122
|
-
if (
|
|
2716
|
+
const mugiDir = join20(scope === "global" ? home : projectDir, ".mugiwara");
|
|
2717
|
+
if (existsSync15(mugiDir) && readdirSync10(mugiDir).length === 0) {
|
|
2123
2718
|
rmSync3(mugiDir, { recursive: true, force: true });
|
|
2124
2719
|
}
|
|
2125
2720
|
if (manifest.targets.includes("opencode")) {
|
|
2126
|
-
const opencodeCache =
|
|
2127
|
-
if (
|
|
2721
|
+
const opencodeCache = join20(home, ".cache", "opencode", "packages", "@ionivetech");
|
|
2722
|
+
if (existsSync15(opencodeCache)) {
|
|
2128
2723
|
rmSync3(opencodeCache, { recursive: true, force: true });
|
|
2129
2724
|
console.log(" cleared opencode npm cache (stale plugin versions)");
|
|
2130
2725
|
}
|
|
@@ -2145,7 +2740,7 @@ function list(flags) {
|
|
|
2145
2740
|
continue;
|
|
2146
2741
|
found = true;
|
|
2147
2742
|
if (flag(flags.check)) {
|
|
2148
|
-
const missing = m.files.filter((f) => !
|
|
2743
|
+
const missing = m.files.filter((f) => !existsSync15(f));
|
|
2149
2744
|
console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
|
|
2150
2745
|
} else {
|
|
2151
2746
|
console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} installed=${m.installedAt}`);
|
|
@@ -2215,6 +2810,58 @@ function statusCmd(flags) {
|
|
|
2215
2810
|
console.log(` evidence: ${s.evidence.join(", ")}`);
|
|
2216
2811
|
}
|
|
2217
2812
|
}
|
|
2813
|
+
function costCmd(flags, positionals) {
|
|
2814
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2815
|
+
const mission = str(flags.mission) ?? positionals[1] ?? (() => {
|
|
2816
|
+
const states2 = readState(projectDir);
|
|
2817
|
+
if (states2.length === 1)
|
|
2818
|
+
return states2[0].mission;
|
|
2819
|
+
if (states2.length > 1) {
|
|
2820
|
+
console.error("multiple missions in flight — specify --mission <id>");
|
|
2821
|
+
process.exit(1);
|
|
2822
|
+
}
|
|
2823
|
+
return null;
|
|
2824
|
+
})();
|
|
2825
|
+
if (!mission) {
|
|
2826
|
+
console.error("usage: mugiwara cost [--mission <id>] [--json] [--ledger] [--project <dir>]");
|
|
2827
|
+
process.exit(1);
|
|
2828
|
+
}
|
|
2829
|
+
const missionDir = join20(projectDir, ".mugiwara", "missions", mission);
|
|
2830
|
+
if (!existsSync15(missionDir)) {
|
|
2831
|
+
console.error(`No cost ledger found for mission "${mission}"`);
|
|
2832
|
+
process.exit(1);
|
|
2833
|
+
}
|
|
2834
|
+
const states = readState(projectDir).filter((s) => s.mission === mission);
|
|
2835
|
+
const envelope = states.length ? costEnvelope({ lane: states[0].lane, budget: states[0].budget, tokens_est: states[0].tokens_est }) : costEnvelope({ lane: "full", tokens_est: 0 });
|
|
2836
|
+
const state0 = states[0];
|
|
2837
|
+
let repeatedReads = 0;
|
|
2838
|
+
try {
|
|
2839
|
+
const reg = loadRegistry(missionDir);
|
|
2840
|
+
repeatedReads = reg.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
|
|
2841
|
+
} catch {
|
|
2842
|
+
repeatedReads = 0;
|
|
2843
|
+
}
|
|
2844
|
+
const liveSlop = computeLiveSlop({ heal_cycle: state0?.heal_cycle ?? 0, repeated_reads: repeatedReads });
|
|
2845
|
+
const ledger = buildCostLedger({ missionDir, envelope, slopSummary: { interventions: liveSlop.interventions } });
|
|
2846
|
+
if (flag(flags.json)) {
|
|
2847
|
+
console.log(toCostJSON(ledger));
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
console.log(`Cost envelope: ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned})`);
|
|
2851
|
+
console.log(`Avoided: ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ~${ledger.avoided.tokens_avoided_est} tokens`);
|
|
2852
|
+
console.log(`Efficiency: reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}%`);
|
|
2853
|
+
if (ledger.avoided.slop_interventions > 0) {
|
|
2854
|
+
const roles = Object.entries(liveSlop.perRole).map(([r, n]) => `${r}:${n}`).join(", ");
|
|
2855
|
+
console.log(`Slop: ${ledger.avoided.slop_interventions} intervention(s) — ${roles}`);
|
|
2856
|
+
}
|
|
2857
|
+
console.log(`Trail: ${ledger.trail.length} decisions`);
|
|
2858
|
+
if (flag(flags.ledger) && ledger.trail.length) {
|
|
2859
|
+
for (const t of ledger.trail.slice(0, 20))
|
|
2860
|
+
console.log(`- ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ""}`);
|
|
2861
|
+
if (ledger.trail.length > 20)
|
|
2862
|
+
console.log(`… ${ledger.trail.length - 20} more`);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2218
2865
|
function runCmd(flags, positionals) {
|
|
2219
2866
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2220
2867
|
const name = positionals[1];
|
|
@@ -2300,8 +2947,8 @@ function handoffCmd(flags, positionals) {
|
|
|
2300
2947
|
}
|
|
2301
2948
|
lines.push("", "## Resuming", "", `\`mugiwara continue ${mission}\` prints the exact resume point.`);
|
|
2302
2949
|
lines.push("Verify `next_action` against plan.md before executing — the table above is computed state, not judgement.");
|
|
2303
|
-
const out =
|
|
2304
|
-
|
|
2950
|
+
const out = join20(".mugiwara", "missions", mission, "handoff.md");
|
|
2951
|
+
writeFileSync10(resolve2(projectDir, out), lines.join(`
|
|
2305
2952
|
`) + `
|
|
2306
2953
|
`);
|
|
2307
2954
|
console.log(lines.join(`
|
|
@@ -2311,13 +2958,34 @@ written: ${out}`);
|
|
|
2311
2958
|
}
|
|
2312
2959
|
function signCmd(flags, _) {
|
|
2313
2960
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2961
|
+
if (flag(flags.genKey)) {
|
|
2962
|
+
const backend = str(flags.backend) ?? "auto";
|
|
2963
|
+
const home = homedir4();
|
|
2964
|
+
if (backend === "minisign") {
|
|
2965
|
+
if (!hasMinisign()) {
|
|
2966
|
+
console.error("✗ minisign not installed — cannot generate keys with this backend");
|
|
2967
|
+
process.exit(1);
|
|
2968
|
+
}
|
|
2969
|
+
try {
|
|
2970
|
+
execFileSync6("minisign", ["-G"], { stdio: "inherit" });
|
|
2971
|
+
console.log("✓ minisign key pair generated in ~/.mugiwara/");
|
|
2972
|
+
return;
|
|
2973
|
+
} catch {
|
|
2974
|
+
console.error("✗ key generation failed");
|
|
2975
|
+
process.exit(1);
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
const dir = ensurePureKey(home);
|
|
2979
|
+
console.log(`✓ pure ed25519 key pair ready: ${join20(dir, "mugiwara.key")} / ${join20(dir, "mugiwara.pub")}`);
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2314
2982
|
const mission = _[1];
|
|
2315
2983
|
if (!mission) {
|
|
2316
|
-
console.error("usage: mugiwara sign <mission> [--verify] [--project <dir>]");
|
|
2984
|
+
console.error("usage: mugiwara sign <mission> [--verify] [--gen-key [--backend pure|minisign]] [--project <dir>]");
|
|
2317
2985
|
process.exit(1);
|
|
2318
2986
|
}
|
|
2319
|
-
const missionDir =
|
|
2320
|
-
if (!
|
|
2987
|
+
const missionDir = join20(projectDir, ".mugiwara", "missions", mission);
|
|
2988
|
+
if (!existsSync15(missionDir)) {
|
|
2321
2989
|
console.error(`no mission dir: ${missionDir}`);
|
|
2322
2990
|
process.exit(1);
|
|
2323
2991
|
}
|
|
@@ -2343,11 +3011,15 @@ Usage:
|
|
|
2343
3011
|
mugiwara continue <m> [member]
|
|
2344
3012
|
print the exact resume point for that mission/member
|
|
2345
3013
|
mugiwara status computed mission state: wave, tasks, lane, blockers, budget
|
|
3014
|
+
mugiwara cost [--mission <id>] [--json] [--ledger]
|
|
3015
|
+
show cost ledger, avoided work, efficiency, trail (human + JSON)
|
|
2346
3016
|
mugiwara blame <path> provenance note on the last commit touching <path>
|
|
2347
3017
|
(fetch notes first: git fetch origin 'refs/notes/mugiwara:refs/notes/mugiwara')
|
|
2348
3018
|
mugiwara handoff <m> write .mugiwara/missions/<m>/handoff.md — a report the next
|
|
2349
3019
|
engineer can act on (computed state + staleness check)
|
|
2350
|
-
mugiwara sign <m>
|
|
3020
|
+
mugiwara sign <m> attestation: sign report.md (auto/minisign/pure/off; --verify to check)
|
|
3021
|
+
mugiwara sign --gen-key [--backend pure|minisign]
|
|
3022
|
+
create signing keys (pure ed25519 default)
|
|
2351
3023
|
mugiwara run <script> [args...]
|
|
2352
3024
|
run a bundled harness script here (${RUNNABLE.join(", ")})
|
|
2353
3025
|
mugiwara savepoint <mission> [member] [flow] [mode]
|