@ionivetech/mugiwara 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.opencode/mugiwara-helpers.mjs +2 -2
- package/README.md +196 -330
- package/content/agents/brook-healing.md +1 -1
- package/content/agents/franky-gates.md +1 -1
- package/content/agents/luffy-orchestrator.md +2 -2
- package/content/agents/memory-keeper.md +5 -0
- package/content/agents/usopp-brainstorm.md +3 -2
- package/content/agents/zoro-execution.md +4 -3
- package/content/skills/mugiwara-backend/SKILL.md +52 -43
- package/content/skills/mugiwara-brainstorm/SKILL.md +5 -3
- package/content/skills/mugiwara-checkpoint/SKILL.md +21 -8
- package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
- package/content/skills/mugiwara-execution/SKILL.md +34 -33
- package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
- 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 +28 -16
- package/content/skills/mugiwara-healing/SKILL.md +30 -25
- package/content/skills/mugiwara-lessons/SKILL.md +3 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +10 -9
- package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
- package/content/skills/mugiwara-planning/SKILL.md +28 -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 +8 -13
- package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
- package/content/skills/mugiwara-resume/SKILL.md +3 -9
- package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
- package/content/skills/mugiwara-review/SKILL.md +17 -24
- package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
- package/content/skills/mugiwara-security/SKILL.md +47 -35
- package/content/skills/mugiwara-ship/SKILL.md +2 -0
- package/content/skills/mugiwara-workflow/SKILL.md +13 -13
- package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +6 -3
- package/dist/mugiwara.js +1802 -316
- package/gemini-extension.json +1 -1
- package/hooks/mugiwara-mode-tracker.js +24 -4
- package/hooks/mugiwara-mode-tracker.ts +36 -7
- package/hooks/pipeline-guard.js +1 -1
- package/hooks/pipeline-guard.ts +2 -1
- package/hooks/session-start.js +6 -1
- package/hooks/session-start.ts +8 -1
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/cost-governor.md +104 -0
- package/references/multi-actor.md +21 -0
- package/references/posture-routing.md +31 -0
- package/references/wave-banners.md +1 -2
- 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 +104 -21
- package/scripts/lib/lane-base.sh +4 -4
- package/scripts/retrieval-eval.ts +9 -3
- package/scripts/savepoint.sh +41 -2
- package/scripts/validate-content.ts +82 -3
- package/scripts/verify-install.ts +20 -0
- package/scripts/write-metrics.ts +73 -0
- package/src/adaptive-budget.ts +178 -0
- package/src/args.ts +3 -2
- package/src/budget.ts +18 -16
- package/src/check-artifacts.ts +45 -0
- package/src/cli.ts +221 -8
- package/src/cognition.ts +234 -0
- package/src/config.ts +113 -0
- package/src/context.ts +72 -0
- package/src/continue.ts +29 -0
- package/src/cost.ts +189 -0
- package/src/evidence.ts +160 -0
- package/src/installer.ts +2 -16
- package/src/integrity.ts +65 -16
- package/src/investigation.ts +72 -0
- package/src/mission.ts +246 -16
- package/src/policy.ts +355 -2
- package/src/posture.ts +86 -0
- package/src/provenance.ts +29 -9
- package/src/reporting.ts +225 -0
- package/src/scope.ts +321 -0
- package/src/sign.ts +234 -18
- 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 existsSync16, readdirSync as readdirSync10, readFileSync as readFileSync15, rmSync as rmSync3, writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, renameSync as renameSync2 } from "node:fs";
|
|
5
5
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
6
6
|
import { homedir as homedir4 } from "node:os";
|
|
7
|
-
import { join as
|
|
7
|
+
import { dirname as dirname6, join as join21, resolve as resolve2 } from "node:path";
|
|
8
8
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
9
9
|
|
|
10
10
|
// src/args.ts
|
|
11
|
-
var VALUE_FLAGS = { "--project": "project", "--target": "target", "--before": "before" };
|
|
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,129 @@ 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
|
+
try {
|
|
662
|
+
const file = join5(projectDir, ".mugiwara", "config");
|
|
663
|
+
let exists = false;
|
|
664
|
+
try {
|
|
665
|
+
exists = lstatSync(file).isFile() || lstatSync(file).isSymbolicLink();
|
|
666
|
+
} catch {
|
|
667
|
+
exists = false;
|
|
668
|
+
}
|
|
669
|
+
if (!exists)
|
|
670
|
+
ensureConfig(projectDir);
|
|
671
|
+
} catch {}
|
|
672
|
+
const out = {};
|
|
673
|
+
for (const file of configPaths(projectDir)) {
|
|
674
|
+
if (!existsSync4(file))
|
|
675
|
+
continue;
|
|
676
|
+
for (const line of readFileSync3(file, "utf8").split(/\r?\n/)) {
|
|
677
|
+
const t = line.trim();
|
|
678
|
+
if (!t || t.startsWith("#"))
|
|
679
|
+
continue;
|
|
680
|
+
const eq = t.indexOf("=");
|
|
681
|
+
if (eq === -1)
|
|
682
|
+
continue;
|
|
683
|
+
const key = t.slice(0, eq).trim();
|
|
684
|
+
if (!key)
|
|
685
|
+
continue;
|
|
686
|
+
if (key in out)
|
|
687
|
+
continue;
|
|
688
|
+
out[key] = t.slice(eq + 1).trim();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return out;
|
|
692
|
+
}
|
|
693
|
+
function ensureConfig(projectDir) {
|
|
694
|
+
const file = join5(projectDir, ".mugiwara", "config");
|
|
695
|
+
let exists = false;
|
|
696
|
+
try {
|
|
697
|
+
exists = lstatSync(file).isFile() || lstatSync(file).isSymbolicLink();
|
|
698
|
+
} catch {
|
|
699
|
+
exists = false;
|
|
700
|
+
}
|
|
701
|
+
if (exists)
|
|
702
|
+
return false;
|
|
703
|
+
mkdirSync3(join5(projectDir, ".mugiwara"), { recursive: true });
|
|
704
|
+
writeFileSync3(file, DEFAULT_CONFIG);
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/installer.ts
|
|
709
|
+
var CONTENT_DIR = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "content");
|
|
710
|
+
var pkg = JSON.parse(readFileSync4(join6(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf8"));
|
|
629
711
|
var VERSION = pkg.version;
|
|
630
712
|
function collectContent() {
|
|
631
|
-
const skillNames = readdirSync3(
|
|
713
|
+
const skillNames = readdirSync3(join6(CONTENT_DIR, "skills"), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
632
714
|
const skills = skillNames.map((name) => {
|
|
633
|
-
const { data, body } = parseFrontmatter(
|
|
634
|
-
return { name, data, body, refs: collectRefs(
|
|
715
|
+
const { data, body } = parseFrontmatter(readFileSync4(join6(CONTENT_DIR, "skills", name, "SKILL.md"), "utf8"));
|
|
716
|
+
return { name, data, body, refs: collectRefs(join6(CONTENT_DIR, "skills", name)) };
|
|
635
717
|
});
|
|
636
|
-
const agents = readdirSync3(
|
|
637
|
-
const { data, body } = parseFrontmatter(
|
|
718
|
+
const agents = readdirSync3(join6(CONTENT_DIR, "agents")).filter((f) => f.endsWith(".md")).map((f) => {
|
|
719
|
+
const { data, body } = parseFrontmatter(readFileSync4(join6(CONTENT_DIR, "agents", f), "utf8"));
|
|
638
720
|
return { name: f.replace(/\.md$/, ""), data, body, refs: [], internal: data.internal === "true" };
|
|
639
721
|
});
|
|
640
|
-
const sharedRefsDir =
|
|
722
|
+
const sharedRefsDir = join6(dirname3(CONTENT_DIR), "references");
|
|
641
723
|
const sharedRefs = [];
|
|
642
|
-
if (
|
|
724
|
+
if (existsSync5(sharedRefsDir)) {
|
|
643
725
|
for (const f of readdirSync3(sharedRefsDir)) {
|
|
644
726
|
if (!f.endsWith(".md"))
|
|
645
727
|
continue;
|
|
646
|
-
sharedRefs.push({ relPath: f, text:
|
|
728
|
+
sharedRefs.push({ relPath: f, text: readFileSync4(join6(sharedRefsDir, f), "utf8") });
|
|
647
729
|
}
|
|
648
730
|
}
|
|
649
731
|
return { skills, agents, sharedRefs };
|
|
650
732
|
}
|
|
651
733
|
function collectRefs(skillDir) {
|
|
652
|
-
const refsDir =
|
|
653
|
-
if (!
|
|
734
|
+
const refsDir = join6(skillDir, "references");
|
|
735
|
+
if (!existsSync5(refsDir))
|
|
654
736
|
return [];
|
|
655
|
-
return readdirSync3(refsDir, { recursive: true }).map((f) => String(f)).filter((f) => f.endsWith(".md")).map((rel) => ({ relPath: rel, text:
|
|
737
|
+
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
738
|
}
|
|
657
739
|
function installTo(target10, opts) {
|
|
658
740
|
const { scope, projectDir, dryRun = false, force = false } = opts;
|
|
659
|
-
const home = opts.home ??
|
|
741
|
+
const home = opts.home ?? homedir2();
|
|
660
742
|
const { skills, agents, sharedRefs } = collectContent();
|
|
661
743
|
const dirs = target10.paths({ scope, projectDir, home });
|
|
662
|
-
const backupRoot =
|
|
744
|
+
const backupRoot = join6(scope === "global" ? home : projectDir, ".mugiwara");
|
|
663
745
|
const result = { written: [], skipped: [], backedUp: [], notes: [] };
|
|
664
746
|
const writeOne = (absPath, text) => {
|
|
665
|
-
if (
|
|
666
|
-
if (
|
|
747
|
+
if (existsSync5(absPath)) {
|
|
748
|
+
if (readFileSync4(absPath, "utf8") === text) {
|
|
667
749
|
result.skipped.push(absPath);
|
|
668
750
|
return;
|
|
669
751
|
}
|
|
@@ -674,17 +756,17 @@ function installTo(target10, opts) {
|
|
|
674
756
|
}
|
|
675
757
|
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
676
758
|
const fileName = absPath.replace(/[^a-zA-Z0-9]+/g, "_");
|
|
677
|
-
const backupDir =
|
|
678
|
-
const backupFile =
|
|
759
|
+
const backupDir = join6(backupRoot, "backup", `${ts}-${target10.id}`);
|
|
760
|
+
const backupFile = join6(backupDir, fileName);
|
|
679
761
|
if (!dryRun) {
|
|
680
|
-
|
|
762
|
+
mkdirSync4(backupDir, { recursive: true });
|
|
681
763
|
copyFileSync3(absPath, backupFile);
|
|
682
764
|
}
|
|
683
765
|
result.backedUp.push(absPath);
|
|
684
766
|
}
|
|
685
767
|
if (!dryRun) {
|
|
686
|
-
|
|
687
|
-
|
|
768
|
+
mkdirSync4(dirname3(absPath), { recursive: true });
|
|
769
|
+
writeFileSync4(absPath, text);
|
|
688
770
|
}
|
|
689
771
|
result.written.push(absPath);
|
|
690
772
|
};
|
|
@@ -695,7 +777,7 @@ function installTo(target10, opts) {
|
|
|
695
777
|
if (target10.refPointerPrefix !== undefined && target10.refPointerPrefix !== "") {
|
|
696
778
|
text = text.replace(/`_shared\/references\//g, "`" + target10.refPointerPrefix + "_shared/references/");
|
|
697
779
|
}
|
|
698
|
-
writeOne(
|
|
780
|
+
writeOne(join6(dirs.skillsDir, out.relPath), text);
|
|
699
781
|
}
|
|
700
782
|
if (target10.transformSkillFull) {
|
|
701
783
|
const full = target10.transformSkillFull(s.data, s.body);
|
|
@@ -704,29 +786,29 @@ function installTo(target10, opts) {
|
|
|
704
786
|
if (target10.refPointerPrefix !== undefined && target10.refPointerPrefix !== "") {
|
|
705
787
|
text = text.replace(/`_shared\/references\//g, "`" + target10.refPointerPrefix + "_shared/references/");
|
|
706
788
|
}
|
|
707
|
-
writeOne(
|
|
789
|
+
writeOne(join6(target10.refsDir({ scope, projectDir, home }, s.name), full.relPath), text);
|
|
708
790
|
}
|
|
709
791
|
}
|
|
710
792
|
if (s.refs.length && target10.refsDir) {
|
|
711
793
|
const refsRoot = target10.refsDir({ scope, projectDir, home }, s.name);
|
|
712
794
|
for (const r of s.refs)
|
|
713
|
-
writeOne(
|
|
795
|
+
writeOne(join6(refsRoot, r.relPath), r.text);
|
|
714
796
|
}
|
|
715
797
|
}
|
|
716
798
|
for (const a of agents) {
|
|
717
799
|
const out = target10.transformAgent({ ...a.data, ...a.internal ? { "internal-agent": "true" } : {} }, a.body);
|
|
718
800
|
if (out)
|
|
719
|
-
writeOne(
|
|
801
|
+
writeOne(join6(dirs.agentsDir, out.relPath), out.text);
|
|
720
802
|
if (target10.transformAgentFull) {
|
|
721
803
|
const full = target10.transformAgentFull(a.data, a.body);
|
|
722
804
|
if (full && target10.refsDir)
|
|
723
|
-
writeOne(
|
|
805
|
+
writeOne(join6(target10.refsDir({ scope, projectDir, home }, a.name), full.relPath), full.text);
|
|
724
806
|
}
|
|
725
807
|
}
|
|
726
808
|
if (sharedRefs.length) {
|
|
727
|
-
const sharedRoot =
|
|
809
|
+
const sharedRoot = join6(dirs.skillsDir, "_shared", "references");
|
|
728
810
|
for (const r of sharedRefs)
|
|
729
|
-
writeOne(
|
|
811
|
+
writeOne(join6(sharedRoot, r.relPath), r.text);
|
|
730
812
|
}
|
|
731
813
|
if (target10.postInstall) {
|
|
732
814
|
const post = target10.postInstall({ scope, projectDir, home, dryRun, files: result.written });
|
|
@@ -734,34 +816,17 @@ function installTo(target10, opts) {
|
|
|
734
816
|
result.notes.push(...post.notes);
|
|
735
817
|
}
|
|
736
818
|
if (scope === "project") {
|
|
737
|
-
const configPath =
|
|
819
|
+
const configPath = join6(projectDir, ".mugiwara", "config");
|
|
738
820
|
let configExists = false;
|
|
739
821
|
try {
|
|
740
|
-
configExists =
|
|
822
|
+
configExists = lstatSync2(configPath).isFile() || lstatSync2(configPath).isSymbolicLink();
|
|
741
823
|
} catch {
|
|
742
824
|
configExists = false;
|
|
743
825
|
}
|
|
744
826
|
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
827
|
if (!dryRun) {
|
|
763
|
-
|
|
764
|
-
|
|
828
|
+
mkdirSync4(dirname3(configPath), { recursive: true });
|
|
829
|
+
writeFileSync4(configPath, DEFAULT_CONFIG);
|
|
765
830
|
}
|
|
766
831
|
result.written.push(configPath);
|
|
767
832
|
result.notes.push(`default config written: ${configPath} (edit it to customise)`);
|
|
@@ -772,7 +837,7 @@ function installTo(target10, opts) {
|
|
|
772
837
|
function removeInstalled(manifest, { dryRun = false } = {}) {
|
|
773
838
|
const removed = [];
|
|
774
839
|
for (const f of manifest.files) {
|
|
775
|
-
if (
|
|
840
|
+
if (existsSync5(f)) {
|
|
776
841
|
if (!dryRun)
|
|
777
842
|
rmSync(f);
|
|
778
843
|
removed.push(f);
|
|
@@ -781,7 +846,7 @@ function removeInstalled(manifest, { dryRun = false } = {}) {
|
|
|
781
846
|
if (!dryRun) {
|
|
782
847
|
for (const f of manifest.files) {
|
|
783
848
|
let d = dirname3(f);
|
|
784
|
-
while (
|
|
849
|
+
while (existsSync5(d) && readdirSync3(d).length === 0) {
|
|
785
850
|
if (d.endsWith("/.mugiwara") || d === dirname3(d))
|
|
786
851
|
break;
|
|
787
852
|
rmSync(d, { recursive: true, force: true });
|
|
@@ -792,10 +857,10 @@ function removeInstalled(manifest, { dryRun = false } = {}) {
|
|
|
792
857
|
return removed;
|
|
793
858
|
}
|
|
794
859
|
function assertNotSymlink(file) {
|
|
795
|
-
if (!
|
|
860
|
+
if (!existsSync5(file))
|
|
796
861
|
return;
|
|
797
862
|
try {
|
|
798
|
-
if (
|
|
863
|
+
if (lstatSync2(file).isSymbolicLink())
|
|
799
864
|
throw new Error(`refusing to follow symlink: ${file}`);
|
|
800
865
|
} catch (e) {
|
|
801
866
|
if (e.code === "ENOENT")
|
|
@@ -816,9 +881,9 @@ var GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit
|
|
|
816
881
|
`;
|
|
817
882
|
function ensureProjectGitignore(projectDir, opts = {}) {
|
|
818
883
|
const { dryRun = false } = opts;
|
|
819
|
-
const path =
|
|
884
|
+
const path = join6(projectDir, ".gitignore");
|
|
820
885
|
assertNotSymlink(path);
|
|
821
|
-
let existing =
|
|
886
|
+
let existing = existsSync5(path) ? readFileSync4(path, "utf8") : "";
|
|
822
887
|
if (existing) {
|
|
823
888
|
if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes(".mugiwara/missions/**/*.json")) {
|
|
824
889
|
return { appended: false, notes: [] };
|
|
@@ -826,24 +891,24 @@ function ensureProjectGitignore(projectDir, opts = {}) {
|
|
|
826
891
|
const hadOld = GITIGNORE_LEGACY.some((m) => existing.includes(m)) || existing.includes(GITIGNORE_BLOCK_START);
|
|
827
892
|
if (hadOld) {
|
|
828
893
|
const clean = removeProjectGitignore(projectDir, { dryRun });
|
|
829
|
-
existing = clean.removed ?
|
|
894
|
+
existing = clean.removed ? existsSync5(path) ? readFileSync4(path, "utf8") : "" : existing;
|
|
830
895
|
}
|
|
831
896
|
}
|
|
832
897
|
const separator = existing.length && !existing.endsWith(`
|
|
833
898
|
`) ? `
|
|
834
899
|
` : "";
|
|
835
900
|
if (!dryRun) {
|
|
836
|
-
|
|
837
|
-
|
|
901
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
902
|
+
writeFileSync4(path, existing + separator + GITIGNORE_BLOCK);
|
|
838
903
|
}
|
|
839
904
|
return { appended: true, notes: [`.gitignore ${dryRun ? "would upgrade+append" : "upgraded"} mugiwara audit-trail block`] };
|
|
840
905
|
}
|
|
841
906
|
function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
|
|
842
|
-
const path =
|
|
907
|
+
const path = join6(projectDir, ".gitignore");
|
|
843
908
|
assertNotSymlink(path);
|
|
844
|
-
if (!
|
|
909
|
+
if (!existsSync5(path))
|
|
845
910
|
return { removed: false, notes: [] };
|
|
846
|
-
const current =
|
|
911
|
+
const current = readFileSync4(path, "utf8");
|
|
847
912
|
const delimited = current.includes(GITIGNORE_BLOCK_START);
|
|
848
913
|
let cleaned;
|
|
849
914
|
if (delimited) {
|
|
@@ -883,62 +948,533 @@ function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
|
|
|
883
948
|
if (cleaned === current)
|
|
884
949
|
return { removed: false, notes: [] };
|
|
885
950
|
if (!dryRun)
|
|
886
|
-
|
|
951
|
+
writeFileSync4(path, cleaned);
|
|
887
952
|
return { removed: true, notes: [`.gitignore ${dryRun ? "would strip" : "stripped"} mugiwara block`] };
|
|
888
953
|
}
|
|
889
954
|
|
|
890
955
|
// src/manifest.ts
|
|
891
|
-
import { existsSync as
|
|
892
|
-
import { dirname as dirname4, join as
|
|
956
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
957
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
893
958
|
function manifestPath({ scope, projectDir, home }) {
|
|
894
|
-
return scope === "global" ?
|
|
959
|
+
return scope === "global" ? join7(home, ".mugiwara", "manifest.json") : join7(projectDir, ".mugiwara", "manifest.json");
|
|
895
960
|
}
|
|
896
961
|
function readManifest(file) {
|
|
897
|
-
return
|
|
962
|
+
return existsSync6(file) ? JSON.parse(readFileSync5(file, "utf8")) : null;
|
|
898
963
|
}
|
|
899
964
|
function writeManifest(file, data) {
|
|
900
|
-
|
|
901
|
-
|
|
965
|
+
mkdirSync5(dirname4(file), { recursive: true });
|
|
966
|
+
writeFileSync5(file, JSON.stringify(data, null, 2) + `
|
|
902
967
|
`);
|
|
903
968
|
}
|
|
904
969
|
|
|
905
970
|
// src/mission.ts
|
|
906
|
-
import { existsSync as
|
|
907
|
-
import { execFileSync as
|
|
908
|
-
import { join as
|
|
971
|
+
import { existsSync as existsSync13, rmSync as rmSync2, readFileSync as readFileSync13, readdirSync as readdirSync7, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9, renameSync, openSync, writeSync, closeSync } from "node:fs";
|
|
972
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
973
|
+
import { join as join18 } from "node:path";
|
|
974
|
+
|
|
975
|
+
// src/integrity.ts
|
|
976
|
+
import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync7 } from "node:fs";
|
|
977
|
+
import { isAbsolute, join as join9, relative } from "node:path";
|
|
978
|
+
|
|
979
|
+
// src/policy.ts
|
|
980
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
981
|
+
import { join as join8 } from "node:path";
|
|
982
|
+
var POLICY_FILES = ["mugiwara.policy.yml", "mugiwara.policy.yaml"];
|
|
983
|
+
var KNOWN_ROOTS = ["lanes", "gates", "evidence", "integrity", "attestation", "harness"];
|
|
984
|
+
function parsePolicyYaml(text) {
|
|
985
|
+
const root = {};
|
|
986
|
+
const stack = [{ keyIndent: -1, obj: root }];
|
|
987
|
+
const lines = text.split(/\r?\n/);
|
|
988
|
+
const process2 = (i) => {
|
|
989
|
+
if (i >= lines.length)
|
|
990
|
+
return;
|
|
991
|
+
const noComment = lines[i].replace(/(^|\s)#.*$/, "");
|
|
992
|
+
if (!noComment.trim())
|
|
993
|
+
return process2(i + 1);
|
|
994
|
+
const indent = noComment.length - noComment.trimStart().length;
|
|
995
|
+
const line = noComment.trim();
|
|
996
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].keyIndent)
|
|
997
|
+
stack.pop();
|
|
998
|
+
const top = stack[stack.length - 1];
|
|
999
|
+
if (top.pending && !line.startsWith("- ") && indent > top.pending.indent && !Array.isArray(top.obj[top.pending.key])) {
|
|
1000
|
+
const { key: key2, indent: pIndent } = top.pending;
|
|
1001
|
+
delete top.pending;
|
|
1002
|
+
const created = {};
|
|
1003
|
+
top.obj[key2] = created;
|
|
1004
|
+
stack.push({ keyIndent: pIndent, obj: created });
|
|
1005
|
+
return process2(i);
|
|
1006
|
+
}
|
|
1007
|
+
if (line.startsWith("- ")) {
|
|
1008
|
+
if (top.pending) {
|
|
1009
|
+
const existing = top.obj[top.pending.key];
|
|
1010
|
+
const arr = Array.isArray(existing) ? existing : [];
|
|
1011
|
+
arr.push(scalar(line.slice(2)));
|
|
1012
|
+
top.obj[top.pending.key] = arr;
|
|
1013
|
+
}
|
|
1014
|
+
return process2(i + 1);
|
|
1015
|
+
}
|
|
1016
|
+
const colon = line.indexOf(":");
|
|
1017
|
+
if (colon === -1)
|
|
1018
|
+
return process2(i + 1);
|
|
1019
|
+
const key = line.slice(0, colon).trim();
|
|
1020
|
+
const rest = line.slice(colon + 1).trim();
|
|
1021
|
+
if (rest === "") {
|
|
1022
|
+
top.pending = { key, indent };
|
|
1023
|
+
} else {
|
|
1024
|
+
top.obj[key] = scalar(rest);
|
|
1025
|
+
delete top.pending;
|
|
1026
|
+
}
|
|
1027
|
+
process2(i + 1);
|
|
1028
|
+
};
|
|
1029
|
+
process2(0);
|
|
1030
|
+
return root;
|
|
1031
|
+
}
|
|
1032
|
+
function scalar(v) {
|
|
1033
|
+
const t = v.trim().replace(/^["']|["']$/g, "");
|
|
1034
|
+
if (/^-?\d+(\.\d+)?$/.test(t))
|
|
1035
|
+
return Number(t);
|
|
1036
|
+
if (t === "true")
|
|
1037
|
+
return true;
|
|
1038
|
+
if (t === "false")
|
|
1039
|
+
return false;
|
|
1040
|
+
return t;
|
|
1041
|
+
}
|
|
1042
|
+
function extractExtraSecretPatterns(text) {
|
|
1043
|
+
const lines = text.split(/\r?\n/);
|
|
1044
|
+
let inBlock = false;
|
|
1045
|
+
let baseIndent = -1;
|
|
1046
|
+
const out = [];
|
|
1047
|
+
let current = null;
|
|
1048
|
+
let currentIndent = -1;
|
|
1049
|
+
for (const rawLine of lines) {
|
|
1050
|
+
const noComment = rawLine.replace(/(^|\s)#.*$/, "");
|
|
1051
|
+
if (!noComment.trim())
|
|
1052
|
+
continue;
|
|
1053
|
+
const indent = noComment.length - noComment.trimStart().length;
|
|
1054
|
+
const trimmed = noComment.trim();
|
|
1055
|
+
if (!inBlock) {
|
|
1056
|
+
if (trimmed.startsWith("extra_secret_patterns:")) {
|
|
1057
|
+
inBlock = true;
|
|
1058
|
+
baseIndent = indent;
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
} else {
|
|
1062
|
+
if (indent <= baseIndent && !trimmed.startsWith("-") && trimmed.includes(":")) {
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
if (trimmed.startsWith("-")) {
|
|
1066
|
+
if (current)
|
|
1067
|
+
out.push(current);
|
|
1068
|
+
current = {};
|
|
1069
|
+
currentIndent = indent;
|
|
1070
|
+
const afterDash = trimmed.slice(1).trim();
|
|
1071
|
+
if (!afterDash)
|
|
1072
|
+
continue;
|
|
1073
|
+
if (afterDash.startsWith("{") && afterDash.endsWith("}")) {
|
|
1074
|
+
const inner = afterDash.slice(1, -1);
|
|
1075
|
+
for (const part of inner.split(",")) {
|
|
1076
|
+
const colon = part.indexOf(":");
|
|
1077
|
+
if (colon === -1)
|
|
1078
|
+
continue;
|
|
1079
|
+
const k = part.slice(0, colon).trim();
|
|
1080
|
+
const v = part.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
|
|
1081
|
+
if (k && v)
|
|
1082
|
+
current[k] = v;
|
|
1083
|
+
}
|
|
1084
|
+
} else if (afterDash.includes(":")) {
|
|
1085
|
+
const colon = afterDash.indexOf(":");
|
|
1086
|
+
const k = afterDash.slice(0, colon).trim();
|
|
1087
|
+
const v = afterDash.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
|
|
1088
|
+
if (k && v)
|
|
1089
|
+
current[k] = v;
|
|
1090
|
+
} else {
|
|
1091
|
+
const v = afterDash.replace(/^["']|["']$/g, "");
|
|
1092
|
+
if (v)
|
|
1093
|
+
current["pattern"] = v;
|
|
1094
|
+
}
|
|
1095
|
+
} else if (current && trimmed.includes(":")) {
|
|
1096
|
+
if (indent > currentIndent) {
|
|
1097
|
+
const colon = trimmed.indexOf(":");
|
|
1098
|
+
const k = trimmed.slice(0, colon).trim();
|
|
1099
|
+
const v = trimmed.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
|
|
1100
|
+
if (k && v)
|
|
1101
|
+
current[k] = v;
|
|
1102
|
+
} else if (indent <= baseIndent) {
|
|
1103
|
+
break;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
if (current)
|
|
1109
|
+
out.push(current);
|
|
1110
|
+
return out.filter((o) => typeof o.pattern === "string" && typeof o.label === "string" && o.pattern.length > 0);
|
|
1111
|
+
}
|
|
1112
|
+
function extractAttestation(text) {
|
|
1113
|
+
const lines = text.split(/\r?\n/);
|
|
1114
|
+
let attBase = -1;
|
|
1115
|
+
let inAtt = false;
|
|
1116
|
+
let required;
|
|
1117
|
+
const trusted = [];
|
|
1118
|
+
const revoked = [];
|
|
1119
|
+
let collecting = null;
|
|
1120
|
+
let collectBase = -1;
|
|
1121
|
+
let current = null;
|
|
1122
|
+
let curIndent = -1;
|
|
1123
|
+
const flush = () => {
|
|
1124
|
+
if (!current)
|
|
1125
|
+
return;
|
|
1126
|
+
if (collecting === "trusted")
|
|
1127
|
+
trusted.push(current);
|
|
1128
|
+
else if (collecting === "revoked")
|
|
1129
|
+
revoked.push(current);
|
|
1130
|
+
current = null;
|
|
1131
|
+
};
|
|
1132
|
+
for (let idx = 0;idx < lines.length; idx++) {
|
|
1133
|
+
const rawLine = lines[idx];
|
|
1134
|
+
const noComment = rawLine.replace(/(^|\s)#.*$/, "");
|
|
1135
|
+
if (!noComment.trim())
|
|
1136
|
+
continue;
|
|
1137
|
+
const indent = noComment.length - noComment.trimStart().length;
|
|
1138
|
+
const trimmed = noComment.trim();
|
|
1139
|
+
if (!inAtt) {
|
|
1140
|
+
if (trimmed === "attestation:" || trimmed.startsWith("attestation:")) {
|
|
1141
|
+
const after = trimmed.slice("attestation:".length).trim();
|
|
1142
|
+
if (after.startsWith("{")) {
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
inAtt = true;
|
|
1146
|
+
attBase = indent;
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
if (indent <= attBase && !trimmed.startsWith("-") && trimmed.includes(":")) {
|
|
1152
|
+
flush();
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
if (collecting) {
|
|
1156
|
+
if (indent <= collectBase && !trimmed.startsWith("-") && trimmed.includes(":")) {
|
|
1157
|
+
flush();
|
|
1158
|
+
collecting = null;
|
|
1159
|
+
} else if (trimmed.startsWith("-")) {
|
|
1160
|
+
flush();
|
|
1161
|
+
current = {};
|
|
1162
|
+
curIndent = indent;
|
|
1163
|
+
const afterDash = trimmed.slice(1).trim();
|
|
1164
|
+
if (!afterDash)
|
|
1165
|
+
continue;
|
|
1166
|
+
if (afterDash.startsWith("{") && afterDash.endsWith("}")) {
|
|
1167
|
+
const inner = afterDash.slice(1, -1);
|
|
1168
|
+
for (const part of inner.split(",")) {
|
|
1169
|
+
const colon = part.indexOf(":");
|
|
1170
|
+
if (colon === -1)
|
|
1171
|
+
continue;
|
|
1172
|
+
const k = part.slice(0, colon).trim();
|
|
1173
|
+
const v = part.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
|
|
1174
|
+
if (k && v)
|
|
1175
|
+
current[k] = v;
|
|
1176
|
+
}
|
|
1177
|
+
} else if (afterDash.includes(":")) {
|
|
1178
|
+
const colon = afterDash.indexOf(":");
|
|
1179
|
+
const k = afterDash.slice(0, colon).trim();
|
|
1180
|
+
const v = afterDash.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
|
|
1181
|
+
if (k && v)
|
|
1182
|
+
current[k] = v;
|
|
1183
|
+
} else {
|
|
1184
|
+
const v = afterDash.replace(/^["']|["']$/g, "");
|
|
1185
|
+
if (v)
|
|
1186
|
+
current["id"] = v;
|
|
1187
|
+
}
|
|
1188
|
+
continue;
|
|
1189
|
+
} else if (current && trimmed.includes(":")) {
|
|
1190
|
+
if (indent > curIndent) {
|
|
1191
|
+
const colon = trimmed.indexOf(":");
|
|
1192
|
+
const k = trimmed.slice(0, colon).trim();
|
|
1193
|
+
const v = trimmed.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
|
|
1194
|
+
if (k && v)
|
|
1195
|
+
current[k] = v;
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (collecting)
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
if (trimmed.startsWith("required:")) {
|
|
1203
|
+
const v = trimmed.slice("required:".length).trim().replace(/^["']|["']$/g, "");
|
|
1204
|
+
if (v === "true")
|
|
1205
|
+
required = true;
|
|
1206
|
+
else if (v === "false")
|
|
1207
|
+
required = false;
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
if (trimmed.startsWith("trusted_keys:")) {
|
|
1211
|
+
const after = trimmed.slice("trusted_keys:".length).trim();
|
|
1212
|
+
if (after === "[]")
|
|
1213
|
+
continue;
|
|
1214
|
+
collecting = "trusted";
|
|
1215
|
+
collectBase = indent;
|
|
1216
|
+
current = null;
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
if (trimmed.startsWith("revoked:")) {
|
|
1220
|
+
const after = trimmed.slice("revoked:".length).trim();
|
|
1221
|
+
if (after === "[]")
|
|
1222
|
+
continue;
|
|
1223
|
+
collecting = "revoked";
|
|
1224
|
+
collectBase = indent;
|
|
1225
|
+
current = null;
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
flush();
|
|
1230
|
+
if (required === undefined && trusted.length === 0 && revoked.length === 0)
|
|
1231
|
+
return null;
|
|
1232
|
+
const out = {};
|
|
1233
|
+
if (required !== undefined)
|
|
1234
|
+
out.required = required;
|
|
1235
|
+
if (trusted.length)
|
|
1236
|
+
out.trusted_keys = trusted;
|
|
1237
|
+
if (revoked.length)
|
|
1238
|
+
out.revoked = revoked;
|
|
1239
|
+
return out;
|
|
1240
|
+
}
|
|
1241
|
+
function loadPolicy(projectDir) {
|
|
1242
|
+
for (const name of POLICY_FILES) {
|
|
1243
|
+
const file = join8(projectDir, name);
|
|
1244
|
+
if (!existsSync7(file))
|
|
1245
|
+
continue;
|
|
1246
|
+
const text = readFileSync6(file, "utf8");
|
|
1247
|
+
const raw = parsePolicyYaml(text);
|
|
1248
|
+
const extra = extractExtraSecretPatterns(text);
|
|
1249
|
+
if (extra.length) {
|
|
1250
|
+
if (!raw.integrity || typeof raw.integrity !== "object" || Array.isArray(raw.integrity))
|
|
1251
|
+
raw.integrity = {};
|
|
1252
|
+
raw.integrity.extra_secret_patterns = extra;
|
|
1253
|
+
}
|
|
1254
|
+
const att = extractAttestation(text);
|
|
1255
|
+
if (att) {
|
|
1256
|
+
if (!raw.attestation || typeof raw.attestation !== "object" || Array.isArray(raw.attestation))
|
|
1257
|
+
raw.attestation = {};
|
|
1258
|
+
const a = raw.attestation;
|
|
1259
|
+
if (att.required !== undefined)
|
|
1260
|
+
a.required = att.required;
|
|
1261
|
+
if (att.trusted_keys)
|
|
1262
|
+
a.trusted_keys = att.trusted_keys;
|
|
1263
|
+
if (att.revoked)
|
|
1264
|
+
a.revoked = att.revoked;
|
|
1265
|
+
}
|
|
1266
|
+
return normalize(raw);
|
|
1267
|
+
}
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
function normalize(raw) {
|
|
1271
|
+
for (const k of Object.keys(raw)) {
|
|
1272
|
+
if (!KNOWN_ROOTS.includes(k))
|
|
1273
|
+
throw new Error(`unknown policy key "${k}" (known: ${KNOWN_ROOTS.join(", ")})`);
|
|
1274
|
+
}
|
|
1275
|
+
const out = {};
|
|
1276
|
+
const lanes = raw.lanes;
|
|
1277
|
+
if (lanes && Array.isArray(lanes.force_full))
|
|
1278
|
+
out.lanes = { force_full: strings(lanes.force_full) };
|
|
1279
|
+
const gates = raw.gates;
|
|
1280
|
+
if (gates) {
|
|
1281
|
+
out.gates = {};
|
|
1282
|
+
const cov = gates.coverage;
|
|
1283
|
+
if (cov) {
|
|
1284
|
+
out.gates.coverage = {};
|
|
1285
|
+
if (typeof cov.new === "number")
|
|
1286
|
+
out.gates.coverage.new = cov.new;
|
|
1287
|
+
if (typeof cov.modified === "number")
|
|
1288
|
+
out.gates.coverage.modified = cov.modified;
|
|
1289
|
+
}
|
|
1290
|
+
if (Array.isArray(gates.require_human_approval))
|
|
1291
|
+
out.gates.require_human_approval = strings(gates.require_human_approval);
|
|
1292
|
+
}
|
|
1293
|
+
const evidence = raw.evidence;
|
|
1294
|
+
if (evidence && Array.isArray(evidence.required))
|
|
1295
|
+
out.evidence = { required: strings(evidence.required) };
|
|
1296
|
+
const integrity = raw.integrity;
|
|
1297
|
+
if (integrity && Array.isArray(integrity.extra_secret_patterns)) {
|
|
1298
|
+
const arr = integrity.extra_secret_patterns;
|
|
1299
|
+
const cleaned = [];
|
|
1300
|
+
for (const e of arr) {
|
|
1301
|
+
if (!e || typeof e !== "object")
|
|
1302
|
+
continue;
|
|
1303
|
+
const rec = e;
|
|
1304
|
+
if (typeof rec.pattern !== "string" || typeof rec.label !== "string")
|
|
1305
|
+
continue;
|
|
1306
|
+
const sev = rec.severity === "warn" ? "warn" : rec.severity === "block" ? "block" : undefined;
|
|
1307
|
+
const entry = { pattern: rec.pattern, label: rec.label };
|
|
1308
|
+
if (sev)
|
|
1309
|
+
entry.severity = sev;
|
|
1310
|
+
cleaned.push(entry);
|
|
1311
|
+
}
|
|
1312
|
+
if (cleaned.length)
|
|
1313
|
+
out.integrity = { extra_secret_patterns: cleaned };
|
|
1314
|
+
}
|
|
1315
|
+
const att = raw.attestation;
|
|
1316
|
+
if (att) {
|
|
1317
|
+
const a = {};
|
|
1318
|
+
if (typeof att.required === "boolean")
|
|
1319
|
+
a.required = att.required;
|
|
1320
|
+
if (Array.isArray(att.trusted_keys)) {
|
|
1321
|
+
const cleanedTk = [];
|
|
1322
|
+
for (const e of att.trusted_keys) {
|
|
1323
|
+
if (!e || typeof e !== "object")
|
|
1324
|
+
continue;
|
|
1325
|
+
const rec = e;
|
|
1326
|
+
if (typeof rec.id !== "string" || typeof rec.pubkey !== "string")
|
|
1327
|
+
continue;
|
|
1328
|
+
if (!rec.id.trim() || !rec.pubkey.trim())
|
|
1329
|
+
continue;
|
|
1330
|
+
const entry = { id: rec.id.trim(), pubkey: rec.pubkey.trim() };
|
|
1331
|
+
if (typeof rec.added === "string" && rec.added.trim())
|
|
1332
|
+
entry.added = rec.added.trim();
|
|
1333
|
+
cleanedTk.push(entry);
|
|
1334
|
+
}
|
|
1335
|
+
if (cleanedTk.length)
|
|
1336
|
+
a.trusted_keys = cleanedTk;
|
|
1337
|
+
}
|
|
1338
|
+
if (Array.isArray(att.revoked)) {
|
|
1339
|
+
const cleanedRv = [];
|
|
1340
|
+
for (const e of att.revoked) {
|
|
1341
|
+
if (!e || typeof e !== "object")
|
|
1342
|
+
continue;
|
|
1343
|
+
const rec = e;
|
|
1344
|
+
if (typeof rec.id !== "string" || !rec.id.trim())
|
|
1345
|
+
continue;
|
|
1346
|
+
const entry = { id: rec.id.trim() };
|
|
1347
|
+
if (typeof rec.revoked === "string" && rec.revoked.trim())
|
|
1348
|
+
entry.revoked = rec.revoked.trim();
|
|
1349
|
+
if (typeof rec.reason === "string" && rec.reason.trim())
|
|
1350
|
+
entry.reason = rec.reason.trim();
|
|
1351
|
+
if (typeof rec.pubkey === "string" && rec.pubkey.trim())
|
|
1352
|
+
entry.pubkey = rec.pubkey.trim();
|
|
1353
|
+
cleanedRv.push(entry);
|
|
1354
|
+
}
|
|
1355
|
+
if (cleanedRv.length)
|
|
1356
|
+
a.revoked = cleanedRv;
|
|
1357
|
+
}
|
|
1358
|
+
if (a.required !== undefined || a.trusted_keys || a.revoked)
|
|
1359
|
+
out.attestation = a;
|
|
1360
|
+
}
|
|
1361
|
+
const harness = raw.harness;
|
|
1362
|
+
if (harness && typeof harness.require_enforcement === "boolean") {
|
|
1363
|
+
out.harness = { require_enforcement: harness.require_enforcement };
|
|
1364
|
+
}
|
|
1365
|
+
return out;
|
|
1366
|
+
}
|
|
1367
|
+
function strings(a) {
|
|
1368
|
+
return a.filter((x) => typeof x === "string" && x.length > 0);
|
|
1369
|
+
}
|
|
1370
|
+
function detectHarness(projectDir) {
|
|
1371
|
+
const e = process.env;
|
|
1372
|
+
const has = (v) => v !== undefined && v !== "";
|
|
1373
|
+
if (has(e.CLAUDECODE) || has(e.CLAUDE_CODE_ENTRYPOINT) || typeof e.ANTHROPIC_MODEL === "string" && /claude/i.test(e.ANTHROPIC_MODEL))
|
|
1374
|
+
return "claude";
|
|
1375
|
+
if (has(e.OPENCODE) || has(e.OPENCODE_TOKENS_FILE))
|
|
1376
|
+
return "opencode";
|
|
1377
|
+
const candidates = [
|
|
1378
|
+
projectDir ? join8(projectDir, ".opencode", "config.json") : null,
|
|
1379
|
+
join8(process.cwd(), ".opencode", "config.json")
|
|
1380
|
+
].filter(Boolean);
|
|
1381
|
+
for (const p of candidates) {
|
|
1382
|
+
try {
|
|
1383
|
+
if (existsSync7(p))
|
|
1384
|
+
return "opencode";
|
|
1385
|
+
} catch {}
|
|
1386
|
+
}
|
|
1387
|
+
if (has(e.CURSOR) || has(e.VSCODE_GIT_ASKPASS_NODE))
|
|
1388
|
+
return "cursor";
|
|
1389
|
+
return "unknown";
|
|
1390
|
+
}
|
|
1391
|
+
function isEnforcedHarness(projectDir) {
|
|
1392
|
+
return detectHarness(projectDir) === "opencode";
|
|
1393
|
+
}
|
|
1394
|
+
function getHarnessEnforcementError(projectDir) {
|
|
1395
|
+
let policy;
|
|
1396
|
+
try {
|
|
1397
|
+
policy = loadPolicy(projectDir);
|
|
1398
|
+
} catch (e) {
|
|
1399
|
+
throw e;
|
|
1400
|
+
}
|
|
1401
|
+
if (!policy?.harness?.require_enforcement)
|
|
1402
|
+
return null;
|
|
1403
|
+
if (isEnforcedHarness(projectDir))
|
|
1404
|
+
return null;
|
|
1405
|
+
const h = detectHarness(projectDir);
|
|
1406
|
+
return `harness enforcement required but current harness is rules-based only — use opencode or set harness.require_enforcement:false (detected: ${h})`;
|
|
1407
|
+
}
|
|
1408
|
+
function enforceHarnessPolicy(projectDir) {
|
|
1409
|
+
const err = getHarnessEnforcementError(projectDir);
|
|
1410
|
+
if (!err)
|
|
1411
|
+
return;
|
|
1412
|
+
console.error(`✗ ${err}`);
|
|
1413
|
+
process.exit(1);
|
|
1414
|
+
}
|
|
909
1415
|
|
|
910
1416
|
// src/integrity.ts
|
|
911
|
-
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
912
|
-
import { isAbsolute, join as join7, relative } from "node:path";
|
|
913
1417
|
var SECRET_PATTERNS = [
|
|
914
|
-
[/AKIA[0-9A-Z]{16}/, "AWS access key id"],
|
|
915
|
-
[/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "private key block"],
|
|
916
|
-
[/gh[pousr]_[A-Za-z0-9]{20,}/, "GitHub token"],
|
|
917
|
-
[/xox[baprs]-[A-Za-z0-9-]{10,}/, "Slack token"],
|
|
918
|
-
[/sk-[A-Za-z0-9]{32,}/, "API key (sk-…)"],
|
|
919
|
-
[/eyJhbGciOi[A-Za-z0-9_.-]{20,}/, "JWT pasted verbatim"],
|
|
920
|
-
[/(api[_-]?key|secret|passwd|password)\s*[=:]\s*["'][^"'\s]{8,}["']/i, "credential assignment"]
|
|
1418
|
+
[/AKIA[0-9A-Z]{16}/, "AWS access key id", "block"],
|
|
1419
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "private key block", "block"],
|
|
1420
|
+
[/gh[pousr]_[A-Za-z0-9]{20,}/, "GitHub token", "block"],
|
|
1421
|
+
[/xox[baprs]-[A-Za-z0-9-]{10,}/, "Slack token", "block"],
|
|
1422
|
+
[/sk-[A-Za-z0-9]{32,}/, "API key (sk-…)", "block"],
|
|
1423
|
+
[/eyJhbGciOi[A-Za-z0-9_.-]{20,}/, "JWT pasted verbatim", "block"],
|
|
1424
|
+
[/(api[_-]?key|secret|passwd|password)\s*[=:]\s*["'][^"'\s]{8,}["']/i, "credential assignment", "block"],
|
|
1425
|
+
[/AIza[0-9A-Za-z_-]{35}/, "Google API key", "block"],
|
|
1426
|
+
[/ya29\.[0-9A-Za-z_-]{20,}/, "Google OAuth token", "block"],
|
|
1427
|
+
[/\b[a-z][a-z0-9+.-]*:\/\/[^\s:@/]+:[^\s@/]{4,}@[^\s/]+/i, "connection string with inline credential", "block"],
|
|
1428
|
+
[/\bAC[a-f0-9]{32}\b/, "Twilio account SID", "block"],
|
|
1429
|
+
[/\bSK[a-f0-9]{32}\b/, "Twilio API key", "block"],
|
|
1430
|
+
[/\bglpat-[A-Za-z0-9_-]{20,}/, "GitLab token", "block"],
|
|
1431
|
+
[/\bnpm_[A-Za-z0-9]{36}\b/, "npm token", "block"],
|
|
1432
|
+
[/\bdop_v1_[a-f0-9]{64}\b/, "DigitalOcean token", "block"],
|
|
1433
|
+
[/\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13})\b/, "card-number shape (verify before committing)", "warn"]
|
|
921
1434
|
];
|
|
922
1435
|
var ALLOW_SECRET = "mugiwara:allow-secret";
|
|
923
|
-
function
|
|
1436
|
+
function loadExtraPatterns(projectRoot) {
|
|
1437
|
+
try {
|
|
1438
|
+
const policy = loadPolicy(projectRoot);
|
|
1439
|
+
const extras = policy?.integrity?.extra_secret_patterns;
|
|
1440
|
+
if (!extras || !Array.isArray(extras))
|
|
1441
|
+
return [];
|
|
1442
|
+
const out = [];
|
|
1443
|
+
for (const e of extras) {
|
|
1444
|
+
if (!e || typeof e.pattern !== "string" || typeof e.label !== "string")
|
|
1445
|
+
continue;
|
|
1446
|
+
const rec = e;
|
|
1447
|
+
const sev = rec.severity === "warn" ? "warn" : "block";
|
|
1448
|
+
try {
|
|
1449
|
+
const re = new RegExp(rec.pattern);
|
|
1450
|
+
out.push([re, rec.label, sev]);
|
|
1451
|
+
} catch {}
|
|
1452
|
+
}
|
|
1453
|
+
return out;
|
|
1454
|
+
} catch {
|
|
1455
|
+
return [];
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
function findSecrets(body, extra) {
|
|
924
1459
|
const out = [];
|
|
1460
|
+
const patterns = extra ? [...SECRET_PATTERNS, ...extra] : SECRET_PATTERNS;
|
|
925
1461
|
for (const line of body.split(/\r?\n/)) {
|
|
926
1462
|
if (line.includes(ALLOW_SECRET))
|
|
927
1463
|
continue;
|
|
928
|
-
for (const [re, label] of
|
|
1464
|
+
for (const [re, label, severity] of patterns) {
|
|
929
1465
|
const hit = line.match(re);
|
|
930
1466
|
if (hit)
|
|
931
|
-
out.push({ label, hit: hit[0] });
|
|
1467
|
+
out.push({ label, hit: hit[0], severity: severity ?? "block" });
|
|
932
1468
|
}
|
|
933
1469
|
}
|
|
934
1470
|
return out;
|
|
935
1471
|
}
|
|
936
|
-
var TRAIL_EXTS = new Set([".md", ".json", ".sh"]);
|
|
1472
|
+
var TRAIL_EXTS = new Set([".md", ".json", ".sh", ".jsonl"]);
|
|
937
1473
|
function trailFiles(dir) {
|
|
938
1474
|
const out = [];
|
|
939
1475
|
const walk = (d) => {
|
|
940
1476
|
for (const e of readdirSync4(d, { withFileTypes: true })) {
|
|
941
|
-
const p =
|
|
1477
|
+
const p = join9(d, e.name);
|
|
942
1478
|
if (e.isDirectory())
|
|
943
1479
|
walk(p);
|
|
944
1480
|
else if (TRAIL_EXTS.has(e.name.slice(e.name.lastIndexOf(".")) || ""))
|
|
@@ -966,7 +1502,7 @@ function collectPassCitedPaths(missionDir) {
|
|
|
966
1502
|
for (const f of trailFiles(missionDir)) {
|
|
967
1503
|
let body;
|
|
968
1504
|
try {
|
|
969
|
-
body =
|
|
1505
|
+
body = readFileSync7(f, "utf8");
|
|
970
1506
|
} catch {
|
|
971
1507
|
continue;
|
|
972
1508
|
}
|
|
@@ -987,44 +1523,47 @@ function collectPassCitedPaths(missionDir) {
|
|
|
987
1523
|
function checkTrail(missionDir, projectRoot) {
|
|
988
1524
|
const issues = [];
|
|
989
1525
|
const files = trailFiles(missionDir);
|
|
1526
|
+
const extraPatterns = loadExtraPatterns(projectRoot);
|
|
990
1527
|
for (const f of files) {
|
|
991
1528
|
let body;
|
|
992
1529
|
try {
|
|
993
|
-
body =
|
|
1530
|
+
body = readFileSync7(f, "utf8");
|
|
994
1531
|
} catch {
|
|
995
1532
|
continue;
|
|
996
1533
|
}
|
|
997
1534
|
for (const target10 of linkedPaths(body)) {
|
|
998
1535
|
if (isAbsolute(target10))
|
|
999
1536
|
continue;
|
|
1000
|
-
const fromMission =
|
|
1001
|
-
const fromRoot =
|
|
1002
|
-
if (!
|
|
1537
|
+
const fromMission = join9(missionDir, target10);
|
|
1538
|
+
const fromRoot = join9(projectRoot, target10);
|
|
1539
|
+
if (!existsSync8(fromMission) && !existsSync8(fromRoot)) {
|
|
1003
1540
|
issues.push({
|
|
1004
1541
|
kind: "dangling-path",
|
|
1005
1542
|
detail: `${relative(projectRoot, f)} links "${target10}" — no such file (mission dir or repo root)`
|
|
1006
1543
|
});
|
|
1007
1544
|
}
|
|
1008
1545
|
}
|
|
1009
|
-
for (const { label, hit } of findSecrets(body)) {
|
|
1546
|
+
for (const { label, hit, severity } of findSecrets(body, extraPatterns.length ? extraPatterns : undefined)) {
|
|
1547
|
+
const isWarn = severity === "warn";
|
|
1010
1548
|
issues.push({
|
|
1011
|
-
kind: "secret",
|
|
1012
|
-
detail: `${relative(projectRoot, f)} matches ${label}: ${hit.slice(0, 12)}
|
|
1549
|
+
kind: isWarn ? "secret-warn" : "secret",
|
|
1550
|
+
detail: `${relative(projectRoot, f)} matches ${label}: ${hit.slice(0, 12)}…`,
|
|
1551
|
+
severity: isWarn ? "warn" : "block"
|
|
1013
1552
|
});
|
|
1014
1553
|
}
|
|
1015
1554
|
}
|
|
1016
1555
|
const evidencePaths = [];
|
|
1017
|
-
const evidenceFile =
|
|
1018
|
-
if (
|
|
1556
|
+
const evidenceFile = join9(missionDir, "state.json");
|
|
1557
|
+
if (existsSync8(evidenceFile)) {
|
|
1019
1558
|
try {
|
|
1020
|
-
const s = JSON.parse(
|
|
1559
|
+
const s = JSON.parse(readFileSync7(evidenceFile, "utf8"));
|
|
1021
1560
|
if (Array.isArray(s.evidence)) {
|
|
1022
1561
|
for (const e of s.evidence) {
|
|
1023
1562
|
if (typeof e !== "string" || !e.trim())
|
|
1024
1563
|
continue;
|
|
1025
1564
|
evidencePaths.push(e);
|
|
1026
|
-
const cand =
|
|
1027
|
-
if (!isAbsolute(e) && !
|
|
1565
|
+
const cand = join9(projectRoot, e);
|
|
1566
|
+
if (!isAbsolute(e) && !existsSync8(cand) && !existsSync8(join9(missionDir, e))) {
|
|
1028
1567
|
issues.push({ kind: "evidence", detail: `state.json evidence "${e}" does not exist` });
|
|
1029
1568
|
}
|
|
1030
1569
|
}
|
|
@@ -1035,14 +1574,16 @@ function checkTrail(missionDir, projectRoot) {
|
|
|
1035
1574
|
for (const e of passCited) {
|
|
1036
1575
|
if (!e.trim() || isAbsolute(e))
|
|
1037
1576
|
continue;
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
const
|
|
1577
|
+
if (/(?:^|\/)(state|continue)(-[^\/]*)?\.json$/.test(e))
|
|
1578
|
+
continue;
|
|
1579
|
+
const candMission = join9(missionDir, e);
|
|
1580
|
+
const candRoot = join9(projectRoot, e);
|
|
1581
|
+
const resolved = existsSync8(candMission) ? candMission : existsSync8(candRoot) ? candRoot : null;
|
|
1041
1582
|
if (!resolved)
|
|
1042
1583
|
continue;
|
|
1043
1584
|
let body;
|
|
1044
1585
|
try {
|
|
1045
|
-
body =
|
|
1586
|
+
body = readFileSync7(resolved, "utf8");
|
|
1046
1587
|
} catch {
|
|
1047
1588
|
continue;
|
|
1048
1589
|
}
|
|
@@ -1057,10 +1598,45 @@ function formatIssues(issues) {
|
|
|
1057
1598
|
`);
|
|
1058
1599
|
}
|
|
1059
1600
|
|
|
1601
|
+
// src/check-artifacts.ts
|
|
1602
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync5 } from "node:fs";
|
|
1603
|
+
import { join as join10 } from "node:path";
|
|
1604
|
+
var LANE_MIN = new Set(["standard", "full", "spike"]);
|
|
1605
|
+
function checkMissionArtifacts(missionDir) {
|
|
1606
|
+
const statePath = join10(missionDir, "state.json");
|
|
1607
|
+
if (!existsSync9(statePath)) {
|
|
1608
|
+
return { ok: true, missing: [], lane: null };
|
|
1609
|
+
}
|
|
1610
|
+
let lane = "unknown";
|
|
1611
|
+
try {
|
|
1612
|
+
const s = JSON.parse(readFileSyncSafe(statePath));
|
|
1613
|
+
if (typeof s.lane === "string")
|
|
1614
|
+
lane = s.lane;
|
|
1615
|
+
} catch {}
|
|
1616
|
+
if (!LANE_MIN.has(lane)) {
|
|
1617
|
+
return { ok: true, missing: [], lane };
|
|
1618
|
+
}
|
|
1619
|
+
const missing = [];
|
|
1620
|
+
if (!existsSync9(join10(missionDir, "plan.md")))
|
|
1621
|
+
missing.push("plan.md");
|
|
1622
|
+
const flowsDir = join10(missionDir, "flows");
|
|
1623
|
+
const hasFlows = existsSync9(flowsDir) && readdirSync5(flowsDir).length > 0;
|
|
1624
|
+
if (!hasFlows)
|
|
1625
|
+
missing.push("flows/ (no execution evidence)");
|
|
1626
|
+
return { ok: missing.length === 0, missing, lane };
|
|
1627
|
+
}
|
|
1628
|
+
function readFileSyncSafe(p) {
|
|
1629
|
+
try {
|
|
1630
|
+
return readFileSync8(p, "utf8");
|
|
1631
|
+
} catch {
|
|
1632
|
+
return "";
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1060
1636
|
// src/rollback.ts
|
|
1061
1637
|
import { execFileSync } from "node:child_process";
|
|
1062
|
-
import { writeFileSync as
|
|
1063
|
-
import { join as
|
|
1638
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
1639
|
+
import { join as join11 } from "node:path";
|
|
1064
1640
|
function git(cwd, args) {
|
|
1065
1641
|
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1066
1642
|
}
|
|
@@ -1099,8 +1675,8 @@ function generateRollback(projectDir, missionDir, input) {
|
|
|
1099
1675
|
const commitsNewestFirst = [...revList].reverse();
|
|
1100
1676
|
const filesTouched = input.baseSha === "unknown" ? [] : git(projectDir, ["diff", "--name-only", input.baseSha, input.branch]).split(/\r?\n/).filter(Boolean);
|
|
1101
1677
|
const body = buildRollback(input, commitsNewestFirst, filesTouched);
|
|
1102
|
-
const file =
|
|
1103
|
-
|
|
1678
|
+
const file = join11(missionDir, "rollback.sh");
|
|
1679
|
+
writeFileSync6(file, body, { mode: 493 });
|
|
1104
1680
|
return { file: "rollback.sh", commits: commitsNewestFirst.length };
|
|
1105
1681
|
} catch {
|
|
1106
1682
|
return null;
|
|
@@ -1109,8 +1685,8 @@ function generateRollback(projectDir, missionDir, input) {
|
|
|
1109
1685
|
|
|
1110
1686
|
// src/provenance.ts
|
|
1111
1687
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1112
|
-
import { writeFileSync as
|
|
1113
|
-
import { join as
|
|
1688
|
+
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
1689
|
+
import { join as join12 } from "node:path";
|
|
1114
1690
|
function git2(cwd, args) {
|
|
1115
1691
|
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1116
1692
|
}
|
|
@@ -1150,16 +1726,33 @@ function renderProvenanceMd(note, sha) {
|
|
|
1150
1726
|
`) + `
|
|
1151
1727
|
`;
|
|
1152
1728
|
}
|
|
1153
|
-
function attachGitNote(projectDir, branch, note) {
|
|
1729
|
+
function attachGitNote(projectDir, branch, note, baseSha) {
|
|
1154
1730
|
try {
|
|
1155
|
-
|
|
1731
|
+
const range = baseSha ? `${baseSha}..${branch}` : branch;
|
|
1732
|
+
let shas = [];
|
|
1156
1733
|
try {
|
|
1157
|
-
|
|
1734
|
+
const raw = git2(projectDir, ["rev-list", range]);
|
|
1735
|
+
shas = raw.split(`
|
|
1736
|
+
`).filter(Boolean);
|
|
1158
1737
|
} catch {
|
|
1159
|
-
|
|
1738
|
+
shas = [];
|
|
1739
|
+
}
|
|
1740
|
+
if (shas.length > 200) {
|
|
1741
|
+
console.warn(`attachGitNote: range ${shas.length} >200, falling back to head-only`);
|
|
1742
|
+
shas = [];
|
|
1160
1743
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1744
|
+
let targets2 = shas;
|
|
1745
|
+
if (!targets2.length) {
|
|
1746
|
+
try {
|
|
1747
|
+
targets2 = [git2(projectDir, ["rev-parse", "--verify", branch])];
|
|
1748
|
+
} catch {
|
|
1749
|
+
targets2 = [git2(projectDir, ["rev-parse", "HEAD"])];
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
for (const sha of targets2) {
|
|
1753
|
+
git2(projectDir, ["notes", "--ref=mugiwara", "add", "-f", "-m", note, sha]);
|
|
1754
|
+
}
|
|
1755
|
+
return { sha: targets2[0], count: targets2.length };
|
|
1163
1756
|
} catch {
|
|
1164
1757
|
return null;
|
|
1165
1758
|
}
|
|
@@ -1179,13 +1772,243 @@ function blamePath(projectDir, path) {
|
|
|
1179
1772
|
${note}`;
|
|
1180
1773
|
} catch {
|
|
1181
1774
|
return `${path} @ ${sha.slice(0, 7)}
|
|
1182
|
-
|
|
1775
|
+
no per-commit note — see .mugiwara/missions/<m>/provenance.md`;
|
|
1183
1776
|
}
|
|
1184
1777
|
}
|
|
1185
|
-
function writeProvenance(projectDir, missionDir, state) {
|
|
1778
|
+
function writeProvenance(projectDir, missionDir, state, baseSha) {
|
|
1186
1779
|
const note = buildNote(state);
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1780
|
+
const resolvedBase = baseSha ?? (typeof state.base_sha === "string" ? state.base_sha : undefined);
|
|
1781
|
+
const cleanBase = resolvedBase && resolvedBase !== "unknown" ? resolvedBase : undefined;
|
|
1782
|
+
const attached = attachGitNote(projectDir, state.branch, note, cleanBase);
|
|
1783
|
+
writeFileSync7(join12(missionDir, "provenance.md"), renderProvenanceMd(note, attached ? attached.sha : null));
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// src/sign.ts
|
|
1787
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1788
|
+
import { chmodSync as chmodSync2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
|
|
1789
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
|
|
1790
|
+
import { homedir as homedir3 } from "node:os";
|
|
1791
|
+
import { join as join13 } from "node:path";
|
|
1792
|
+
function signArgs(reportPath, secretKey) {
|
|
1793
|
+
return ["-Sm", reportPath, "-s", secretKey];
|
|
1794
|
+
}
|
|
1795
|
+
function verifyArgs(reportPath, pubKey) {
|
|
1796
|
+
return pubKey ? ["-Vm", reportPath, "-p", pubKey] : ["-Vm", reportPath];
|
|
1797
|
+
}
|
|
1798
|
+
function hasMinisign() {
|
|
1799
|
+
try {
|
|
1800
|
+
execFileSync3("minisign", ["-v"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
1801
|
+
return true;
|
|
1802
|
+
} catch {
|
|
1803
|
+
return false;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
function defaultKey(flag) {
|
|
1807
|
+
return join13(homedir3(), ".mugiwara", flag === "secret" ? "minisign.key" : "minisign.pub");
|
|
1808
|
+
}
|
|
1809
|
+
function generatePureKey() {
|
|
1810
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
1811
|
+
const privJwk = privateKey.export({ format: "jwk" });
|
|
1812
|
+
const pubJwk = publicKey.export({ format: "jwk" });
|
|
1813
|
+
return {
|
|
1814
|
+
key: Buffer.from(privJwk.d, "base64url").toString("base64"),
|
|
1815
|
+
pub: Buffer.from(pubJwk.x, "base64url").toString("base64")
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
function ensurePureKey(homeDir) {
|
|
1819
|
+
const dir = join13(homeDir, ".mugiwara");
|
|
1820
|
+
const keyPath = join13(dir, "mugiwara.key");
|
|
1821
|
+
const pubPath = join13(dir, "mugiwara.pub");
|
|
1822
|
+
if (!existsSync10(keyPath) || !existsSync10(pubPath)) {
|
|
1823
|
+
mkdirSync6(dir, { recursive: true });
|
|
1824
|
+
const { key, pub } = generatePureKey();
|
|
1825
|
+
if (!existsSync10(keyPath)) {
|
|
1826
|
+
writeFileSync8(keyPath, key + `
|
|
1827
|
+
`, { mode: 384 });
|
|
1828
|
+
}
|
|
1829
|
+
if (!existsSync10(pubPath))
|
|
1830
|
+
writeFileSync8(pubPath, pub + `
|
|
1831
|
+
`);
|
|
1832
|
+
}
|
|
1833
|
+
try {
|
|
1834
|
+
chmodSync2(keyPath, 384);
|
|
1835
|
+
} catch {}
|
|
1836
|
+
return dir;
|
|
1837
|
+
}
|
|
1838
|
+
function pureSign(content, seedBase64, opts) {
|
|
1839
|
+
const seed = Buffer.from(seedBase64.trim(), "base64");
|
|
1840
|
+
if (seed.length !== 32)
|
|
1841
|
+
return { ok: false, message: "invalid seed (want 32B base64)" };
|
|
1842
|
+
const pubBuf = Buffer.from(opts.pub.trim(), "base64");
|
|
1843
|
+
if (pubBuf.length !== 32)
|
|
1844
|
+
return { ok: false, message: "invalid pub (want 32B base64)" };
|
|
1845
|
+
const privateKey = createPrivateKey({
|
|
1846
|
+
key: {
|
|
1847
|
+
kty: "OKP",
|
|
1848
|
+
crv: "Ed25519",
|
|
1849
|
+
d: seed.toString("base64url"),
|
|
1850
|
+
x: pubBuf.toString("base64url")
|
|
1851
|
+
},
|
|
1852
|
+
format: "jwk"
|
|
1853
|
+
});
|
|
1854
|
+
const sig = sign(null, Buffer.from(content, "utf8"), privateKey).toString("base64");
|
|
1855
|
+
const out = { algo: "ed25519-pure", sig, pub: opts.pub, mission: opts.mission, commit: opts.commit, ts: opts.ts };
|
|
1856
|
+
if (opts.outputPath)
|
|
1857
|
+
writeFileSync8(opts.outputPath, JSON.stringify(out, null, 2) + `
|
|
1858
|
+
`);
|
|
1859
|
+
return { ...out, ok: true };
|
|
1860
|
+
}
|
|
1861
|
+
function pureVerify(content, sig) {
|
|
1862
|
+
try {
|
|
1863
|
+
const pub = Buffer.from(sig.pub, "base64");
|
|
1864
|
+
if (pub.length !== 32)
|
|
1865
|
+
return false;
|
|
1866
|
+
const publicKey = createPublicKey({
|
|
1867
|
+
key: { kty: "OKP", crv: "Ed25519", x: pub.toString("base64url") },
|
|
1868
|
+
format: "jwk"
|
|
1869
|
+
});
|
|
1870
|
+
return verify(null, Buffer.from(content, "utf8"), publicKey, Buffer.from(sig.sig, "base64"));
|
|
1871
|
+
} catch {
|
|
1872
|
+
return false;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
function resolveBackend(configured, env) {
|
|
1876
|
+
switch (configured) {
|
|
1877
|
+
case "off":
|
|
1878
|
+
return "off";
|
|
1879
|
+
case "minisign":
|
|
1880
|
+
return env.hasMinisign ? "minisign" : "minisign-fail";
|
|
1881
|
+
case "pure":
|
|
1882
|
+
return "pure";
|
|
1883
|
+
case "auto":
|
|
1884
|
+
default:
|
|
1885
|
+
return env.hasMinisign && env.hasKey ? "minisign" : "pure";
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
function configuredBackend(projectDir) {
|
|
1889
|
+
return readConfig(projectDir).sign;
|
|
1890
|
+
}
|
|
1891
|
+
function missionMeta(projectDir, mission) {
|
|
1892
|
+
let commit = "unknown";
|
|
1893
|
+
try {
|
|
1894
|
+
commit = execFileSync3("git", ["rev-parse", "HEAD"], { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1895
|
+
} catch {}
|
|
1896
|
+
return { commit, ts: new Date().toISOString() };
|
|
1897
|
+
}
|
|
1898
|
+
function signReport(projectDir, missionDir) {
|
|
1899
|
+
const report = join13(missionDir, "report.md");
|
|
1900
|
+
if (!existsSync10(report))
|
|
1901
|
+
return { ok: false, message: "no report.md to sign — archive first" };
|
|
1902
|
+
const mission = missionDir.split(join13(".mugiwara", "missions", "")).pop() ?? "unknown";
|
|
1903
|
+
const backend = resolveBackend(configuredBackend(projectDir), { hasMinisign: hasMinisign(), hasKey: existsSync10(defaultKey("secret")) });
|
|
1904
|
+
if (backend === "off")
|
|
1905
|
+
return { ok: false, message: "signing disabled (sign=off)" };
|
|
1906
|
+
if (backend === "minisign-fail")
|
|
1907
|
+
return { ok: false, message: "sign=minisign but minisign not installed — install it or set sign=pure" };
|
|
1908
|
+
if (backend === "minisign") {
|
|
1909
|
+
const secretKey = process.env.MUGIWARA_SIGN_KEY?.trim() || defaultKey("secret");
|
|
1910
|
+
try {
|
|
1911
|
+
execFileSync3("minisign", signArgs(report, secretKey), { cwd: projectDir, stdio: "pipe", input: process.env.MUGIWARA_SIGN_PASSWORD ?? "" });
|
|
1912
|
+
return { ok: true, message: `signed ${report}.minisig (minisign, key: ${secretKey})` };
|
|
1913
|
+
} catch (e) {
|
|
1914
|
+
return { ok: false, message: `signing failed: ${e.message}` };
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
const dir = ensurePureKey(homedir3());
|
|
1918
|
+
const seed = process.env.MUGIWARA_SIGN_KEY?.trim() || readFileSyncSafe2(join13(dir, "mugiwara.key"));
|
|
1919
|
+
const pub = process.env.MUGIWARA_SIGN_PUB?.trim() || readFileSyncSafe2(join13(dir, "mugiwara.pub"));
|
|
1920
|
+
if (!seed || !pub)
|
|
1921
|
+
return { ok: false, message: "pure keys missing — run `mugiwara sign --gen-key --backend pure`" };
|
|
1922
|
+
const content = readFileSafe(report);
|
|
1923
|
+
if (content === null)
|
|
1924
|
+
return { ok: false, message: `cannot read ${report}` };
|
|
1925
|
+
const { commit, ts } = missionMeta(projectDir, mission);
|
|
1926
|
+
const sig = pureSign(content, seed, { mission, commit, ts, pub, outputPath: `${report}.mugisig` });
|
|
1927
|
+
if (!sig.ok)
|
|
1928
|
+
return { ok: false, message: `signing failed: ${sig.message}` };
|
|
1929
|
+
return { ok: true, message: `signed ${report}.mugisig (pure ed25519, key: ${join13(dir, "mugiwara.key")})` };
|
|
1930
|
+
}
|
|
1931
|
+
function normalizePubkey(pk) {
|
|
1932
|
+
const t = pk.trim();
|
|
1933
|
+
return t.startsWith("ed25519:") ? t.slice("ed25519:".length).trim() : t;
|
|
1934
|
+
}
|
|
1935
|
+
function checkTrust(projectDir, signerPubB64) {
|
|
1936
|
+
try {
|
|
1937
|
+
const policy = loadPolicy(projectDir);
|
|
1938
|
+
const att = policy?.attestation;
|
|
1939
|
+
if (!att)
|
|
1940
|
+
return { ok: true };
|
|
1941
|
+
const trusted = att.trusted_keys ?? [];
|
|
1942
|
+
const revoked = att.revoked ?? [];
|
|
1943
|
+
const signer = signerPubB64.trim();
|
|
1944
|
+
const directlyRevoked = revoked.some((r) => {
|
|
1945
|
+
const rpk = r.pubkey;
|
|
1946
|
+
if (!rpk)
|
|
1947
|
+
return false;
|
|
1948
|
+
return normalizePubkey(rpk) === signer;
|
|
1949
|
+
});
|
|
1950
|
+
if (directlyRevoked)
|
|
1951
|
+
return { ok: false, message: "signature valid but signer revoked (pubkey in revoked list)" };
|
|
1952
|
+
if (trusted.length === 0) {
|
|
1953
|
+
return { ok: true };
|
|
1954
|
+
}
|
|
1955
|
+
const match = trusted.find((e) => normalizePubkey(e.pubkey) === signer);
|
|
1956
|
+
if (!match)
|
|
1957
|
+
return { ok: false, message: "signature valid but signer untrusted (pub not in trusted_keys)" };
|
|
1958
|
+
const revokedById = revoked.some((r) => r.id === match.id);
|
|
1959
|
+
if (revokedById)
|
|
1960
|
+
return { ok: false, message: `signature valid but signer revoked (id: ${match.id})` };
|
|
1961
|
+
return { ok: true };
|
|
1962
|
+
} catch {
|
|
1963
|
+
return { ok: true };
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
function verifyReport(projectDir, missionDir) {
|
|
1967
|
+
const report = join13(missionDir, "report.md");
|
|
1968
|
+
const minisig = `${report}.minisig`;
|
|
1969
|
+
const mugisig = `${report}.mugisig`;
|
|
1970
|
+
if (!existsSync10(minisig) && !existsSync10(mugisig)) {
|
|
1971
|
+
return { ok: false, message: "not signed (no .minisig or .mugisig beside report.md)" };
|
|
1972
|
+
}
|
|
1973
|
+
if (existsSync10(minisig)) {
|
|
1974
|
+
if (!hasMinisign())
|
|
1975
|
+
return { ok: false, message: "minisig present but minisign not installed — cannot verify that signature" };
|
|
1976
|
+
const pubKey = existsSync10(defaultKey("public")) ? defaultKey("public") : null;
|
|
1977
|
+
try {
|
|
1978
|
+
execFileSync3("minisign", verifyArgs(report, pubKey), { cwd: projectDir, stdio: "pipe" });
|
|
1979
|
+
return { ok: true, message: "signature verifies against report.md (minisig)" };
|
|
1980
|
+
} catch {
|
|
1981
|
+
return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (minisig)" };
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
try {
|
|
1985
|
+
const parsed = JSON.parse(readFileSafe(mugisig) ?? "{}");
|
|
1986
|
+
const content = readFileSafe(report);
|
|
1987
|
+
if (content === null || parsed.algo !== "ed25519-pure")
|
|
1988
|
+
return { ok: false, message: "invalid .mugisig file" };
|
|
1989
|
+
if (!pureVerify(content, parsed))
|
|
1990
|
+
return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (mugisig)" };
|
|
1991
|
+
const trust = checkTrust(projectDir, parsed.pub);
|
|
1992
|
+
if (!trust.ok)
|
|
1993
|
+
return { ok: false, message: trust.message };
|
|
1994
|
+
return { ok: true, message: "signature verifies against report.md (mugisig, ed25519-pure)" };
|
|
1995
|
+
} catch {
|
|
1996
|
+
return { ok: false, message: "invalid .mugisig file" };
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
function readFileSafe(p) {
|
|
2000
|
+
try {
|
|
2001
|
+
return readFileSync9(p, "utf8");
|
|
2002
|
+
} catch {
|
|
2003
|
+
return null;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
function readFileSyncSafe2(p) {
|
|
2007
|
+
try {
|
|
2008
|
+
return readFileSync9(p, "utf8").trim();
|
|
2009
|
+
} catch {
|
|
2010
|
+
return "";
|
|
2011
|
+
}
|
|
1189
2012
|
}
|
|
1190
2013
|
|
|
1191
2014
|
// src/routing.ts
|
|
@@ -1242,28 +2065,15 @@ function renderRouting(ranked, mission) {
|
|
|
1242
2065
|
}
|
|
1243
2066
|
|
|
1244
2067
|
// src/budget.ts
|
|
1245
|
-
import { existsSync as
|
|
1246
|
-
import {
|
|
1247
|
-
import { join as join10 } from "node:path";
|
|
2068
|
+
import { existsSync as existsSync11, readdirSync as readdirSync6, statSync } from "node:fs";
|
|
2069
|
+
import { join as join14 } from "node:path";
|
|
1248
2070
|
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;
|
|
2071
|
+
const cfg = readConfig(projectDir);
|
|
2072
|
+
const raw = cfg.context_budget_chars;
|
|
2073
|
+
if (raw === undefined || raw === "")
|
|
2074
|
+
return 0;
|
|
2075
|
+
const n = Number(raw);
|
|
2076
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
1267
2077
|
}
|
|
1268
2078
|
function measureContextChars(missionDir) {
|
|
1269
2079
|
let total = 0;
|
|
@@ -1272,15 +2082,15 @@ function measureContextChars(missionDir) {
|
|
|
1272
2082
|
total += statSync(p).size;
|
|
1273
2083
|
} catch {}
|
|
1274
2084
|
};
|
|
1275
|
-
for (const f of
|
|
2085
|
+
for (const f of readdirSync6(missionDir)) {
|
|
1276
2086
|
if (/\.md$/.test(f))
|
|
1277
|
-
add(
|
|
2087
|
+
add(join14(missionDir, f));
|
|
1278
2088
|
}
|
|
1279
2089
|
for (const sub of ["flows", "waves"]) {
|
|
1280
|
-
const dir =
|
|
1281
|
-
if (
|
|
1282
|
-
for (const f of
|
|
1283
|
-
add(
|
|
2090
|
+
const dir = join14(missionDir, sub);
|
|
2091
|
+
if (existsSync11(dir)) {
|
|
2092
|
+
for (const f of readdirSync6(dir))
|
|
2093
|
+
add(join14(dir, f));
|
|
1284
2094
|
}
|
|
1285
2095
|
}
|
|
1286
2096
|
return total;
|
|
@@ -1291,6 +2101,282 @@ function formatFootprint(chars, budget) {
|
|
|
1291
2101
|
return `${base} (no budget configured)`;
|
|
1292
2102
|
return chars > budget ? `${base} — OVER budget ${budget}` : `${base} (budget ${budget})`;
|
|
1293
2103
|
}
|
|
2104
|
+
var COMPRESS_THRESHOLD_PCT = 0.8;
|
|
2105
|
+
function shouldCompress(budget, chars) {
|
|
2106
|
+
return budget > 0 && chars > Math.floor(budget * COMPRESS_THRESHOLD_PCT);
|
|
2107
|
+
}
|
|
2108
|
+
function compressThreshold(budget) {
|
|
2109
|
+
return Math.floor(budget * COMPRESS_THRESHOLD_PCT);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// src/cost.ts
|
|
2113
|
+
import { appendFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync10 } from "node:fs";
|
|
2114
|
+
import { join as join15 } from "node:path";
|
|
2115
|
+
var LANE_BUDGET = {
|
|
2116
|
+
lean: 12000,
|
|
2117
|
+
standard: 25000,
|
|
2118
|
+
full: 50000,
|
|
2119
|
+
spike: 3000
|
|
2120
|
+
};
|
|
2121
|
+
function budgetForLane(lane) {
|
|
2122
|
+
return LANE_BUDGET[lane] ?? 0;
|
|
2123
|
+
}
|
|
2124
|
+
function warnAt(budget) {
|
|
2125
|
+
return Math.floor(budget * 3 / 2);
|
|
2126
|
+
}
|
|
2127
|
+
function stopAt(budget) {
|
|
2128
|
+
return budget * 3;
|
|
2129
|
+
}
|
|
2130
|
+
function budgetStatus(budget, tokens) {
|
|
2131
|
+
if (budget > 0 && tokens >= stopAt(budget))
|
|
2132
|
+
return "stop";
|
|
2133
|
+
if (budget > 0 && tokens >= warnAt(budget))
|
|
2134
|
+
return "warn";
|
|
2135
|
+
return "ok";
|
|
2136
|
+
}
|
|
2137
|
+
function costEnvelope(state) {
|
|
2138
|
+
const planned = typeof state.budget === "number" && state.budget > 0 ? state.budget : budgetForLane(state.lane ?? "");
|
|
2139
|
+
const used = typeof state.tokens_est === "number" ? state.tokens_est : 0;
|
|
2140
|
+
const remaining = Math.max(planned - used, 0);
|
|
2141
|
+
const pct = planned > 0 ? Math.round(used / planned * 100) : 0;
|
|
2142
|
+
return {
|
|
2143
|
+
planned,
|
|
2144
|
+
used,
|
|
2145
|
+
remaining,
|
|
2146
|
+
pct,
|
|
2147
|
+
warn_at: warnAt(planned),
|
|
2148
|
+
stop_at: stopAt(planned),
|
|
2149
|
+
status: budgetStatus(planned, used)
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
var COST_EVENTS_FILE = "cost-events.jsonl";
|
|
2153
|
+
function isAllowedMissionDir(dir) {
|
|
2154
|
+
if (!dir || dir.includes(".."))
|
|
2155
|
+
return false;
|
|
2156
|
+
if (dir.includes(".mugiwara/missions"))
|
|
2157
|
+
return true;
|
|
2158
|
+
if (dir.includes("mugiwara-"))
|
|
2159
|
+
return true;
|
|
2160
|
+
if (dir.startsWith("/tmp/") && dir.includes("-"))
|
|
2161
|
+
return true;
|
|
2162
|
+
return false;
|
|
2163
|
+
}
|
|
2164
|
+
function assertMissionDir(dir) {
|
|
2165
|
+
if (!isAllowedMissionDir(dir))
|
|
2166
|
+
throw new Error(`Invalid missionDir: ${dir}`);
|
|
2167
|
+
}
|
|
2168
|
+
function appendCostEvent(missionDir, event) {
|
|
2169
|
+
assertMissionDir(missionDir);
|
|
2170
|
+
mkdirSync7(missionDir, { recursive: true });
|
|
2171
|
+
const line = Object.assign({ ts: new Date().toISOString() }, event);
|
|
2172
|
+
appendFileSync(join15(missionDir, COST_EVENTS_FILE), JSON.stringify(line) + `
|
|
2173
|
+
`, "utf8");
|
|
2174
|
+
}
|
|
2175
|
+
var COMPRESSED_KIND = "compressed";
|
|
2176
|
+
|
|
2177
|
+
// src/evidence.ts
|
|
2178
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync11 } from "node:fs";
|
|
2179
|
+
import { join as join16 } from "node:path";
|
|
2180
|
+
var REGISTRY_FILE = "context-registry.jsonl";
|
|
2181
|
+
function isAllowedMissionDir2(dir) {
|
|
2182
|
+
if (!dir || dir.includes(".."))
|
|
2183
|
+
return false;
|
|
2184
|
+
if (dir.includes(".mugiwara/missions"))
|
|
2185
|
+
return true;
|
|
2186
|
+
if (dir.includes("mugiwara-"))
|
|
2187
|
+
return true;
|
|
2188
|
+
if (dir.startsWith("/tmp/") && dir.includes("-"))
|
|
2189
|
+
return true;
|
|
2190
|
+
return false;
|
|
2191
|
+
}
|
|
2192
|
+
function assertMissionDir2(dir) {
|
|
2193
|
+
if (!isAllowedMissionDir2(dir))
|
|
2194
|
+
throw new Error(`Invalid missionDir: ${dir}`);
|
|
2195
|
+
}
|
|
2196
|
+
function loadRegistry(missionDir) {
|
|
2197
|
+
assertMissionDir2(missionDir);
|
|
2198
|
+
const file = join16(missionDir, REGISTRY_FILE);
|
|
2199
|
+
try {
|
|
2200
|
+
const out = [];
|
|
2201
|
+
for (const line of readFileSync11(file, "utf8").split(/\r?\n/)) {
|
|
2202
|
+
if (!line.trim())
|
|
2203
|
+
continue;
|
|
2204
|
+
let e;
|
|
2205
|
+
try {
|
|
2206
|
+
e = JSON.parse(line);
|
|
2207
|
+
} catch {
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2210
|
+
if (e === null || typeof e !== "object")
|
|
2211
|
+
continue;
|
|
2212
|
+
const entry = e;
|
|
2213
|
+
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;
|
|
2214
|
+
if (!ok)
|
|
2215
|
+
continue;
|
|
2216
|
+
entry.reads = Math.floor(entry.reads);
|
|
2217
|
+
out.push(entry);
|
|
2218
|
+
}
|
|
2219
|
+
return out;
|
|
2220
|
+
} catch {
|
|
2221
|
+
return [];
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// src/context.ts
|
|
2226
|
+
function contextStatus(budgetChars, chars) {
|
|
2227
|
+
return budgetChars > 0 && chars > budgetChars ? "over" : "ok";
|
|
2228
|
+
}
|
|
2229
|
+
function computeContextMetrics(stats) {
|
|
2230
|
+
const reuse_rate = stats.reads_total > 0 ? stats.reads_reused / stats.reads_total : 0;
|
|
2231
|
+
const duplicate_chars = stats.total_chars - stats.unique_chars;
|
|
2232
|
+
return {
|
|
2233
|
+
files_loaded: stats.files_loaded,
|
|
2234
|
+
repeated_reads: stats.repeated_reads,
|
|
2235
|
+
duplicate_chars,
|
|
2236
|
+
reuse_rate,
|
|
2237
|
+
read_avoidance_chars: duplicate_chars
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// src/reporting.ts
|
|
2242
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
|
|
2243
|
+
import { join as join17 } from "node:path";
|
|
2244
|
+
function isAllowedMissionDir3(dir) {
|
|
2245
|
+
if (!dir || dir.includes(".."))
|
|
2246
|
+
return false;
|
|
2247
|
+
if (dir.includes(".mugiwara/missions"))
|
|
2248
|
+
return true;
|
|
2249
|
+
if (dir.includes("mugiwara-"))
|
|
2250
|
+
return true;
|
|
2251
|
+
if (dir.startsWith("/tmp/") && dir.includes("-"))
|
|
2252
|
+
return true;
|
|
2253
|
+
return false;
|
|
2254
|
+
}
|
|
2255
|
+
function assertMissionDir3(dir) {
|
|
2256
|
+
if (!isAllowedMissionDir3(dir))
|
|
2257
|
+
throw new Error(`Invalid missionDir: ${dir}`);
|
|
2258
|
+
}
|
|
2259
|
+
function parseDecisionTrail(missionDir) {
|
|
2260
|
+
assertMissionDir3(missionDir);
|
|
2261
|
+
const file = join17(missionDir, "decisions.md");
|
|
2262
|
+
if (!existsSync12(file))
|
|
2263
|
+
return [];
|
|
2264
|
+
const raw = readFileSync12(file, "utf8");
|
|
2265
|
+
const lines = raw.split(/\r?\n/);
|
|
2266
|
+
const headerIdx = lines.findIndex((l) => l.trim() === "## Cost governor decisions" || l.trim() === "## Budget");
|
|
2267
|
+
const scan = headerIdx >= 0 ? lines.slice(headerIdx) : lines;
|
|
2268
|
+
const out = [];
|
|
2269
|
+
const bulletRe = /^-\s+(.+?)\s+—\s+(.+?):\s+(.+?)\s+—\s+reason:\s+(.+?)(?:\s+—\s+evidence:\s+(.+))?\s*$/;
|
|
2270
|
+
for (const line of scan) {
|
|
2271
|
+
const m = bulletRe.exec(line.trim());
|
|
2272
|
+
if (!m)
|
|
2273
|
+
continue;
|
|
2274
|
+
out.push({ ts: m[1], actor: m[2], decision: m[3], reason: m[4], ...m[5] ? { evidence: m[5] } : {} });
|
|
2275
|
+
}
|
|
2276
|
+
return out;
|
|
2277
|
+
}
|
|
2278
|
+
function loadCostEvents(missionDir) {
|
|
2279
|
+
assertMissionDir3(missionDir);
|
|
2280
|
+
const file = join17(missionDir, "cost-events.jsonl");
|
|
2281
|
+
if (!existsSync12(file))
|
|
2282
|
+
return [];
|
|
2283
|
+
const out = [];
|
|
2284
|
+
for (const line of readFileSync12(file, "utf8").split(/\r?\n/)) {
|
|
2285
|
+
if (!line.trim())
|
|
2286
|
+
continue;
|
|
2287
|
+
try {
|
|
2288
|
+
const e = JSON.parse(line);
|
|
2289
|
+
if (e && typeof e === "object" && typeof e.mission === "string")
|
|
2290
|
+
out.push(e);
|
|
2291
|
+
} catch {
|
|
2292
|
+
continue;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
return out;
|
|
2296
|
+
}
|
|
2297
|
+
function computeAvoidedMetrics(input) {
|
|
2298
|
+
const dup = input.registryMetrics?.duplicateCount ?? 0;
|
|
2299
|
+
const rep = input.registryMetrics?.repeatedReads ?? 0;
|
|
2300
|
+
const contexts_avoided = dup + rep;
|
|
2301
|
+
const stages_avoided = input.workMetrics?.stagesAvoided ?? 0;
|
|
2302
|
+
const slop_interventions = input.slopMetrics?.interventions ?? 0;
|
|
2303
|
+
const tokens_avoided_est = contexts_avoided * 150;
|
|
2304
|
+
return { stages_avoided, contexts_avoided, slop_interventions, tokens_avoided_est };
|
|
2305
|
+
}
|
|
2306
|
+
function computeEfficiencyMetrics(input) {
|
|
2307
|
+
const reuse_rate = input.totalReads > 0 ? Math.round(input.reuseHits / input.totalReads * 100) / 100 : 0;
|
|
2308
|
+
const duplicate_avoidance_chars = input.duplicateChars;
|
|
2309
|
+
const budget_efficiency_pct = input.budget > 0 ? Math.round(input.used / input.budget * 100) : 0;
|
|
2310
|
+
return { reuse_rate, duplicate_avoidance_chars, budget_efficiency_pct };
|
|
2311
|
+
}
|
|
2312
|
+
function buildCostLedger(input) {
|
|
2313
|
+
assertMissionDir3(input.missionDir);
|
|
2314
|
+
const events = loadCostEvents(input.missionDir);
|
|
2315
|
+
let registrySize = 0;
|
|
2316
|
+
let totalReads = 0;
|
|
2317
|
+
let reuseHits = 0;
|
|
2318
|
+
let duplicateChars = 0;
|
|
2319
|
+
try {
|
|
2320
|
+
const reg = loadRegistry(input.missionDir);
|
|
2321
|
+
registrySize = reg.length;
|
|
2322
|
+
totalReads = reg.reduce((s, e) => s + e.reads, 0);
|
|
2323
|
+
reuseHits = reg.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
|
|
2324
|
+
const unique = reg.reduce((s, e) => s + (e.chars ?? 0), 0);
|
|
2325
|
+
const total = reg.reduce((s, e) => s + (e.chars ?? 0) * e.reads, 0);
|
|
2326
|
+
duplicateChars = total - unique;
|
|
2327
|
+
if (duplicateChars < 0)
|
|
2328
|
+
duplicateChars = 0;
|
|
2329
|
+
} catch {
|
|
2330
|
+
registrySize = 0;
|
|
2331
|
+
}
|
|
2332
|
+
const decisions = parseDecisionTrail(input.missionDir);
|
|
2333
|
+
const repeatedReads = reuseHits;
|
|
2334
|
+
const duplicateCount = duplicateChars > 0 ? 1 : 0;
|
|
2335
|
+
const avoided = computeAvoidedMetrics({
|
|
2336
|
+
registryMetrics: { duplicateCount: 0, repeatedReads: reuseHits },
|
|
2337
|
+
workMetrics: { stagesAvoided: input.workSummary?.stagesAvoided ?? 0 },
|
|
2338
|
+
slopMetrics: { interventions: input.slopSummary?.interventions ?? 0 }
|
|
2339
|
+
});
|
|
2340
|
+
if (duplicateChars > 0 && avoided.contexts_avoided === 0) {
|
|
2341
|
+
avoided.contexts_avoided = 1;
|
|
2342
|
+
avoided.tokens_avoided_est = 150;
|
|
2343
|
+
}
|
|
2344
|
+
const efficiency = computeEfficiencyMetrics({
|
|
2345
|
+
totalReads,
|
|
2346
|
+
reuseHits,
|
|
2347
|
+
duplicateChars,
|
|
2348
|
+
budget: input.envelope.planned,
|
|
2349
|
+
used: input.envelope.used
|
|
2350
|
+
});
|
|
2351
|
+
return {
|
|
2352
|
+
envelope: input.envelope,
|
|
2353
|
+
ledger: { events, registrySize, decisions },
|
|
2354
|
+
avoided,
|
|
2355
|
+
efficiency,
|
|
2356
|
+
trail: decisions
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
function toCostJSON(ledger) {
|
|
2360
|
+
return JSON.stringify({ envelope: ledger.envelope, ledger: ledger.ledger, avoided: ledger.avoided, efficiency: ledger.efficiency, trail: ledger.trail }, null, 2);
|
|
2361
|
+
}
|
|
2362
|
+
function summarizeAdaptation(missionDir) {
|
|
2363
|
+
const trail = parseDecisionTrail(missionDir);
|
|
2364
|
+
const postureRe = /posture|switch|adapt|pause|parallel-workers|context-relief|phase-isolated|team-scoped/i;
|
|
2365
|
+
const rows = trail.filter((t) => postureRe.test(`${t.decision} ${t.reason}`)).map((t) => ({ ts: t.ts, decision: t.decision, reason: t.reason }));
|
|
2366
|
+
return { count: rows.length, rows };
|
|
2367
|
+
}
|
|
2368
|
+
function renderAdaptationSection(missionDir) {
|
|
2369
|
+
const { count, rows } = summarizeAdaptation(missionDir);
|
|
2370
|
+
if (count === 0)
|
|
2371
|
+
return "";
|
|
2372
|
+
const lines = ["", "## Adaptation", "", "| Time | Decision | Reason |", "|---|---|---|"];
|
|
2373
|
+
for (const r of rows)
|
|
2374
|
+
lines.push(`| ${r.ts} | ${r.decision} | ${r.reason} |`);
|
|
2375
|
+
if (rows.length === 0)
|
|
2376
|
+
lines.push("| — | (posture rows recorded in decisions.md) | |");
|
|
2377
|
+
return lines.join(`
|
|
2378
|
+
`);
|
|
2379
|
+
}
|
|
1294
2380
|
|
|
1295
2381
|
// src/mission.ts
|
|
1296
2382
|
function isStateFile(f) {
|
|
@@ -1302,18 +2388,65 @@ function primaryState(dir, files) {
|
|
|
1302
2388
|
if (!name)
|
|
1303
2389
|
return null;
|
|
1304
2390
|
try {
|
|
1305
|
-
return JSON.parse(
|
|
2391
|
+
return JSON.parse(readFileSync13(join18(dir, name), "utf8"));
|
|
1306
2392
|
} catch {
|
|
1307
2393
|
return null;
|
|
1308
2394
|
}
|
|
1309
2395
|
}
|
|
2396
|
+
function tasksFromState(state) {
|
|
2397
|
+
if (!state)
|
|
2398
|
+
return { done: 0, total: 0 };
|
|
2399
|
+
const t = state.tasks;
|
|
2400
|
+
if (t && typeof t.done === "number" && typeof t.total === "number")
|
|
2401
|
+
return { done: t.done, total: t.total };
|
|
2402
|
+
const done = Number(state.tasks_done);
|
|
2403
|
+
const total = Number(state.tasks_total);
|
|
2404
|
+
return { done: Number.isFinite(done) ? done : 0, total: Number.isFinite(total) ? total : 0 };
|
|
2405
|
+
}
|
|
2406
|
+
function countPlanTasks(missionDir) {
|
|
2407
|
+
let total = 0;
|
|
2408
|
+
let done = 0;
|
|
2409
|
+
const planFile = join18(missionDir, "plan.md");
|
|
2410
|
+
if (existsSync13(planFile)) {
|
|
2411
|
+
try {
|
|
2412
|
+
const text = readFileSync13(planFile, "utf8");
|
|
2413
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2414
|
+
if (/^\s*-\s*\[[ xX]\]/.test(line)) {
|
|
2415
|
+
total++;
|
|
2416
|
+
if (/^\s*-\s*\[[xX]\]/.test(line))
|
|
2417
|
+
done++;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
} catch {}
|
|
2421
|
+
}
|
|
2422
|
+
if (total === 0) {
|
|
2423
|
+
const subPlanDir = join18(missionDir, "sub-plan");
|
|
2424
|
+
if (existsSync13(subPlanDir)) {
|
|
2425
|
+
try {
|
|
2426
|
+
for (const f of readdirSync7(subPlanDir)) {
|
|
2427
|
+
if (!f.endsWith(".md"))
|
|
2428
|
+
continue;
|
|
2429
|
+
const text = readFileSync13(join18(subPlanDir, f), "utf8");
|
|
2430
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2431
|
+
if (/^\s*-\s*\[[ xX]\]/.test(line)) {
|
|
2432
|
+
total++;
|
|
2433
|
+
if (/^\s*-\s*\[[xX]\]/.test(line))
|
|
2434
|
+
done++;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
} catch {}
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
return { done, total };
|
|
2442
|
+
}
|
|
1310
2443
|
function changedFiles(projectDir, state) {
|
|
1311
2444
|
const base = typeof state?.base_sha === "string" ? state.base_sha : "";
|
|
1312
2445
|
const branch = typeof state?.branch === "string" ? state.branch : "";
|
|
1313
2446
|
if (!base || base === "unknown" || !branch)
|
|
1314
2447
|
return [];
|
|
1315
2448
|
try {
|
|
1316
|
-
return
|
|
2449
|
+
return execFileSync4("git", ["diff", "--name-only", base, branch], {
|
|
1317
2450
|
cwd: projectDir,
|
|
1318
2451
|
encoding: "utf8",
|
|
1319
2452
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -1323,16 +2456,16 @@ function changedFiles(projectDir, state) {
|
|
|
1323
2456
|
}
|
|
1324
2457
|
}
|
|
1325
2458
|
function activeActor(projectDir) {
|
|
1326
|
-
const missionsDir =
|
|
1327
|
-
if (!
|
|
2459
|
+
const missionsDir = join18(projectDir, ".mugiwara", "missions");
|
|
2460
|
+
if (!existsSync13(missionsDir))
|
|
1328
2461
|
return null;
|
|
1329
|
-
const missions =
|
|
2462
|
+
const missions = readdirSync7(missionsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1330
2463
|
let latest = null;
|
|
1331
2464
|
for (const mission of missions) {
|
|
1332
|
-
const d =
|
|
1333
|
-
for (const f of
|
|
2465
|
+
const d = join18(missionsDir, mission);
|
|
2466
|
+
for (const f of readdirSync7(d).filter(isStateFile)) {
|
|
1334
2467
|
try {
|
|
1335
|
-
const s = JSON.parse(
|
|
2468
|
+
const s = JSON.parse(readFileSync13(join18(d, f), "utf8"));
|
|
1336
2469
|
const t = Date.parse(s.updated_at || "") || 0;
|
|
1337
2470
|
if (!latest || t > latest.updated)
|
|
1338
2471
|
latest = { actor: s.actor || null, updated: t };
|
|
@@ -1342,8 +2475,8 @@ function activeActor(projectDir) {
|
|
|
1342
2475
|
return latest ? latest.actor : null;
|
|
1343
2476
|
}
|
|
1344
2477
|
function resetMission(projectDir, keepLogs, force) {
|
|
1345
|
-
const root =
|
|
1346
|
-
if (!
|
|
2478
|
+
const root = join18(projectDir, ".mugiwara");
|
|
2479
|
+
if (!existsSync13(root))
|
|
1347
2480
|
return { removed: [], kept: [] };
|
|
1348
2481
|
if (!force) {
|
|
1349
2482
|
const actor = activeActor(projectDir);
|
|
@@ -1354,62 +2487,93 @@ function resetMission(projectDir, keepLogs, force) {
|
|
|
1354
2487
|
const removed = [];
|
|
1355
2488
|
const kept = [];
|
|
1356
2489
|
for (const dir of ["missions", "spec", "plans", "results", "review", "issues", "reports", "state", "continue"]) {
|
|
1357
|
-
const p =
|
|
1358
|
-
if (
|
|
2490
|
+
const p = join18(root, dir);
|
|
2491
|
+
if (existsSync13(p)) {
|
|
1359
2492
|
rmSync2(p, { recursive: true, force: true });
|
|
1360
2493
|
removed.push(dir);
|
|
1361
2494
|
}
|
|
1362
2495
|
}
|
|
1363
|
-
for (const f of
|
|
2496
|
+
for (const f of readdirSync7(root)) {
|
|
1364
2497
|
if (/^state(-.+)?\.json$/.test(f)) {
|
|
1365
|
-
const p =
|
|
1366
|
-
if (
|
|
2498
|
+
const p = join18(root, f);
|
|
2499
|
+
if (existsSync13(p)) {
|
|
1367
2500
|
rmSync2(p);
|
|
1368
2501
|
removed.push(f);
|
|
1369
2502
|
}
|
|
1370
2503
|
}
|
|
1371
2504
|
}
|
|
1372
2505
|
if (!keepLogs) {
|
|
1373
|
-
for (const p of [
|
|
1374
|
-
if (
|
|
2506
|
+
for (const p of [join18(root, "lessons.md"), join18(root, "logs")]) {
|
|
2507
|
+
if (existsSync13(p)) {
|
|
1375
2508
|
rmSync2(p, { recursive: true, force: true });
|
|
1376
|
-
removed.push(p.startsWith(
|
|
2509
|
+
removed.push(p.startsWith(join18(root, "logs")) ? "logs" : "lessons.md");
|
|
1377
2510
|
}
|
|
1378
2511
|
}
|
|
1379
2512
|
} else {
|
|
1380
|
-
if (
|
|
2513
|
+
if (existsSync13(join18(root, "lessons.md")))
|
|
1381
2514
|
kept.push("lessons.md");
|
|
1382
|
-
else if (
|
|
1383
|
-
kept.push(
|
|
2515
|
+
else if (existsSync13(join18(root, join18("logs", "lessons.md"))))
|
|
2516
|
+
kept.push(join18("logs", "lessons.md"));
|
|
2517
|
+
}
|
|
2518
|
+
if (removed.includes("missions") && existsSync13(join18(root, "index.md"))) {
|
|
2519
|
+
rmSync2(join18(root, "index.md"));
|
|
2520
|
+
removed.push("index.md");
|
|
1384
2521
|
}
|
|
1385
2522
|
for (const f of ["config", "manifest.json", "backup"]) {
|
|
1386
|
-
if (
|
|
2523
|
+
if (existsSync13(join18(root, f)))
|
|
1387
2524
|
kept.push(f);
|
|
1388
2525
|
}
|
|
1389
2526
|
return { removed, kept };
|
|
1390
2527
|
}
|
|
1391
2528
|
function archiveMission(projectDir, mission, opts = {}) {
|
|
1392
2529
|
const { dryRun = false } = opts;
|
|
1393
|
-
const root =
|
|
2530
|
+
const root = join18(projectDir, ".mugiwara");
|
|
1394
2531
|
if (!mission || /[^a-zA-Z0-9._-]/.test(mission) || /^\.+$/.test(mission))
|
|
1395
2532
|
throw new Error(`invalid mission name "${mission}" (allowlist: [a-zA-Z0-9._-], not a dot-path)`);
|
|
1396
2533
|
const removed = [];
|
|
1397
2534
|
const kept = [];
|
|
1398
|
-
const dir =
|
|
1399
|
-
if (!
|
|
2535
|
+
const dir = join18(root, "missions", mission);
|
|
2536
|
+
if (!existsSync13(dir))
|
|
1400
2537
|
return { report: null, removed, kept };
|
|
1401
2538
|
if (!dryRun) {
|
|
1402
2539
|
const issues = checkTrail(dir, projectDir);
|
|
1403
|
-
|
|
2540
|
+
const blocking = issues.filter((i) => i.kind !== "secret-warn" && i.severity !== "warn");
|
|
2541
|
+
if (blocking.length) {
|
|
1404
2542
|
throw new Error(`closure integrity gate failed — fix these before archiving:
|
|
1405
|
-
${formatIssues(
|
|
2543
|
+
${formatIssues(blocking)}`);
|
|
2544
|
+
}
|
|
2545
|
+
if (issues.length && blocking.length === 0) {
|
|
2546
|
+
try {
|
|
2547
|
+
const warnText = formatIssues(issues);
|
|
2548
|
+
if (warnText)
|
|
2549
|
+
console.warn(`closure integrity warnings (non-blocking):
|
|
2550
|
+
${warnText}`);
|
|
2551
|
+
} catch {}
|
|
2552
|
+
}
|
|
2553
|
+
const artifacts = checkMissionArtifacts(dir);
|
|
2554
|
+
if (!artifacts.ok) {
|
|
2555
|
+
throw new Error(`archive artifact gate failed — missing: ${artifacts.missing.join(", ")} (lane ${artifacts.lane}). Write the evidence trail before archiving.`);
|
|
2556
|
+
}
|
|
2557
|
+
try {
|
|
2558
|
+
const policy = loadPolicy(projectDir);
|
|
2559
|
+
if (policy?.attestation?.required) {
|
|
2560
|
+
const v = verifyReport(projectDir, dir);
|
|
2561
|
+
if (!v.ok) {
|
|
2562
|
+
throw new Error(`closure integrity gate failed — attestation required but report not signed/trusted: ${v.message}`);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
} catch (e) {
|
|
2566
|
+
if (e.message.startsWith("closure integrity gate failed — attestation required"))
|
|
2567
|
+
throw e;
|
|
2568
|
+
if (e.message.startsWith("unknown policy key"))
|
|
2569
|
+
throw e;
|
|
1406
2570
|
}
|
|
1407
2571
|
}
|
|
1408
|
-
const files =
|
|
2572
|
+
const files = readdirSync7(dir);
|
|
1409
2573
|
const state = primaryState(dir, files);
|
|
1410
2574
|
const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
|
|
1411
2575
|
try {
|
|
1412
|
-
const s = JSON.parse(
|
|
2576
|
+
const s = JSON.parse(readFileSync13(join18(dir, f), "utf8"));
|
|
1413
2577
|
return typeof s.model === "string" ? s.model : "";
|
|
1414
2578
|
} catch {
|
|
1415
2579
|
return "";
|
|
@@ -1420,22 +2584,36 @@ ${formatIssues(issues)}`);
|
|
|
1420
2584
|
const chars = measureContextChars(dir);
|
|
1421
2585
|
const budget = readBudgetConfig(projectDir);
|
|
1422
2586
|
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
2587
|
const est = typeof state.tokens_est === "number" ? state.tokens_est : 0;
|
|
1427
2588
|
const src = typeof state.tokens_source === "string" ? state.tokens_source : "computed";
|
|
1428
2589
|
const lane = typeof state.lane === "string" ? state.lane : "unknown";
|
|
1429
|
-
const laneBudget = lane
|
|
2590
|
+
const laneBudget = budgetForLane(lane);
|
|
2591
|
+
const env = costEnvelope({ lane, budget: laneBudget, tokens_est: est });
|
|
1430
2592
|
const effBudget = budget || laneBudget;
|
|
1431
|
-
const pct = effBudget ? Math.round(est / effBudget * 100) : 0;
|
|
1432
2593
|
const delta = effBudget ? est <= effBudget ? `${(effBudget - est).toLocaleString()} under` : `${(est - effBudget).toLocaleString()} over` : "no budget configured";
|
|
1433
2594
|
const srcLabel = src === "reported" ? "provider-reported" : "estimator";
|
|
2595
|
+
const statusLabel = env.status.toUpperCase();
|
|
2596
|
+
const ctxStatus = contextStatus(budget, chars);
|
|
2597
|
+
const registry = loadRegistry(dir);
|
|
2598
|
+
const reads_total = registry.reduce((s, e) => s + e.reads, 0);
|
|
2599
|
+
const repeated_reads = registry.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
|
|
2600
|
+
const unique_chars = registry.reduce((s, e) => s + (e.chars ?? 0), 0);
|
|
2601
|
+
const total_chars = registry.reduce((s, e) => s + (e.chars ?? 0) * e.reads, 0);
|
|
2602
|
+
const charTracked = registry.length > 0 && total_chars > 0;
|
|
2603
|
+
const metrics = registry.length ? computeContextMetrics({
|
|
2604
|
+
files_loaded: registry.length,
|
|
2605
|
+
reads_total,
|
|
2606
|
+
reads_reused: repeated_reads,
|
|
2607
|
+
unique_chars,
|
|
2608
|
+
total_chars,
|
|
2609
|
+
repeated_reads
|
|
2610
|
+
}) : { files_loaded: 0, repeated_reads: 0, duplicate_chars: 0, reuse_rate: 0, read_avoidance_chars: 0 };
|
|
2611
|
+
const ctxNote = registry.length ? charTracked ? "" : " (char data not tracked)" : " (no registry — reads not tracked)";
|
|
1434
2612
|
let reportedTotal = 0;
|
|
1435
2613
|
let hasReported = false;
|
|
1436
2614
|
for (const f of files.filter(isStateFile)) {
|
|
1437
2615
|
try {
|
|
1438
|
-
const s = JSON.parse(
|
|
2616
|
+
const s = JSON.parse(readFileSync13(join18(dir, f), "utf8"));
|
|
1439
2617
|
if (s.tokens_source === "reported" && typeof s.tokens_est === "number") {
|
|
1440
2618
|
reportedTotal += s.tokens_est;
|
|
1441
2619
|
hasReported = true;
|
|
@@ -1452,17 +2630,92 @@ ${formatIssues(issues)}`);
|
|
|
1452
2630
|
"| Metric | Value |",
|
|
1453
2631
|
"|--------|-------|",
|
|
1454
2632
|
`| **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)"}
|
|
2633
|
+
`| **Lane** | ${lane} (budget ${effBudget ? effBudget.toLocaleString() : "—"} · warn ${effBudget ? env.warn_at.toLocaleString() : "—"} · stop ${effBudget ? env.stop_at.toLocaleString() : "—"}) |`,
|
|
2634
|
+
`| **Budget status** | ${effBudget ? `${env.pct}% of budget · ${delta} · ${statusLabel}` : "no lane budget"} |`,
|
|
2635
|
+
`| **Context footprint** | ${chars.toLocaleString()} chars${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
|
|
2636
|
+
`| **Context budget status** | ${ctxStatus.toUpperCase()}${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
|
|
2637
|
+
`| **Context efficiency** | files_loaded: ${metrics.files_loaded} · repeated_reads: ${metrics.repeated_reads} · duplicate_chars: ${charTracked ? metrics.duplicate_chars : "n/a"} · reuse_rate: ${metrics.reuse_rate} · read_avoidance_chars: ${charTracked ? metrics.read_avoidance_chars : "n/a"}${ctxNote} |`
|
|
1458
2638
|
].join(`
|
|
1459
2639
|
`);
|
|
1460
2640
|
if (hasReported) {
|
|
1461
2641
|
costSection += `
|
|
1462
2642
|
| **Provider total** | ${reportedTotal.toLocaleString()} (provider-reported — sum of reported stages) |`;
|
|
1463
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}% |`;
|
|
2654
|
+
costSection += `
|
|
2655
|
+
| Trail | ${ledger.trail.length} decisions |`;
|
|
2656
|
+
if (ledger.trail.length) {
|
|
2657
|
+
const show = ledger.trail.slice(0, 5);
|
|
2658
|
+
for (const t of show)
|
|
2659
|
+
costSection += `
|
|
2660
|
+
- ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ""}`;
|
|
2661
|
+
if (ledger.trail.length > 5)
|
|
2662
|
+
costSection += `
|
|
2663
|
+
… ${ledger.trail.length - 5} more`;
|
|
2664
|
+
}
|
|
2665
|
+
} catch {}
|
|
1464
2666
|
costSection += `
|
|
1465
2667
|
`;
|
|
2668
|
+
try {
|
|
2669
|
+
costSection += renderAdaptationSection(dir);
|
|
2670
|
+
} catch {}
|
|
2671
|
+
if (shouldCompress(budget, chars)) {
|
|
2672
|
+
try {
|
|
2673
|
+
const flowsDir2 = join18(dir, "flows");
|
|
2674
|
+
const wavesDir = join18(dir, "waves");
|
|
2675
|
+
const targetDir = existsSync13(flowsDir2) ? flowsDir2 : existsSync13(wavesDir) ? wavesDir : null;
|
|
2676
|
+
if (targetDir && existsSync13(targetDir)) {
|
|
2677
|
+
const flowFiles = readdirSync7(targetDir).filter((f) => f.endsWith(".md"));
|
|
2678
|
+
if (flowFiles.length) {
|
|
2679
|
+
const pct = Math.round(chars / budget * 100);
|
|
2680
|
+
const stub = `# Compressed trail
|
|
2681
|
+
|
|
2682
|
+
Trail ${chars} chars exceeds ${pct}% of budget ${budget} (threshold ${compressThreshold(budget)}) — flows archived as stub to preserve budget. Original flows: ${flowFiles.join(", ")}
|
|
2683
|
+
`;
|
|
2684
|
+
for (const f of flowFiles) {
|
|
2685
|
+
try {
|
|
2686
|
+
rmSync2(join18(targetDir, f), { force: true });
|
|
2687
|
+
} catch {}
|
|
2688
|
+
}
|
|
2689
|
+
writeFileSync9(join18(targetDir, "00-compressed.md"), stub);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
appendCostEvent(dir, {
|
|
2693
|
+
kind: COMPRESSED_KIND,
|
|
2694
|
+
mission,
|
|
2695
|
+
tokens_est: est,
|
|
2696
|
+
budget: laneBudget,
|
|
2697
|
+
status: "compressed",
|
|
2698
|
+
context_chars: chars,
|
|
2699
|
+
context_status: ctxStatus,
|
|
2700
|
+
context_metrics: metrics
|
|
2701
|
+
});
|
|
2702
|
+
costSection += `
|
|
2703
|
+
| **Compressed** | yes — ${chars} chars >80% of ${budget} — flows stubbed as 00-compressed.md |`;
|
|
2704
|
+
} catch {}
|
|
2705
|
+
}
|
|
2706
|
+
appendCostEvent(dir, {
|
|
2707
|
+
kind: "closure",
|
|
2708
|
+
mission,
|
|
2709
|
+
tokens_est: est,
|
|
2710
|
+
budget: laneBudget,
|
|
2711
|
+
status: env.status,
|
|
2712
|
+
context_chars: chars,
|
|
2713
|
+
context_status: ctxStatus,
|
|
2714
|
+
context_metrics: metrics
|
|
2715
|
+
});
|
|
2716
|
+
if (budget && chars > budget) {
|
|
2717
|
+
throw new Error(`closure context budget failed — ${footprintLine}. Trim the trail or raise context_budget_chars.`);
|
|
2718
|
+
}
|
|
1466
2719
|
}
|
|
1467
2720
|
const FOLD_TOP = ["decisions.md", "blockers.md", "review.md", "security.md", "spec.md"];
|
|
1468
2721
|
const fold = [];
|
|
@@ -1470,25 +2723,40 @@ ${formatIssues(issues)}`);
|
|
|
1470
2723
|
if (files.includes(f))
|
|
1471
2724
|
fold.push(f);
|
|
1472
2725
|
}
|
|
1473
|
-
const
|
|
1474
|
-
const
|
|
1475
|
-
const
|
|
2726
|
+
const PR_VERDICT = "pr-verdict.md";
|
|
2727
|
+
const PR_VERDICT_SRC = join18("flows", "07-pr-verdict.md");
|
|
2728
|
+
const flowsDir = join18(dir, "flows");
|
|
2729
|
+
const legacyWavesDir = join18(dir, "waves");
|
|
2730
|
+
const artDir = existsSync13(flowsDir) ? flowsDir : existsSync13(legacyWavesDir) ? legacyWavesDir : flowsDir;
|
|
1476
2731
|
const artRel = artDir === legacyWavesDir ? "waves" : "flows";
|
|
1477
|
-
if (
|
|
1478
|
-
for (const f of
|
|
1479
|
-
|
|
2732
|
+
if (existsSync13(artDir)) {
|
|
2733
|
+
for (const f of readdirSync7(artDir).sort()) {
|
|
2734
|
+
if (artRel === "flows" && f === "07-pr-verdict.md")
|
|
2735
|
+
continue;
|
|
2736
|
+
fold.push(join18(artRel, f));
|
|
2737
|
+
}
|
|
1480
2738
|
}
|
|
2739
|
+
if (existsSync13(join18(dir, "cost-events.jsonl")))
|
|
2740
|
+
fold.push("cost-events.jsonl");
|
|
2741
|
+
if (existsSync13(join18(dir, "context-registry.jsonl")))
|
|
2742
|
+
fold.push("context-registry.jsonl");
|
|
1481
2743
|
let report = "";
|
|
1482
|
-
const reportPath =
|
|
2744
|
+
const reportPath = join18(dir, "report.md");
|
|
1483
2745
|
if (files.includes("report.md"))
|
|
1484
|
-
report =
|
|
1485
|
-
else if (
|
|
1486
|
-
report =
|
|
2746
|
+
report = readFileSync13(reportPath, "utf8");
|
|
2747
|
+
else if (existsSync13(join18(artDir, "06-closure.md")))
|
|
2748
|
+
report = readFileSync13(join18(artDir, "06-closure.md"), "utf8");
|
|
1487
2749
|
if (!dryRun) {
|
|
1488
|
-
|
|
2750
|
+
mkdirSync9(dir, { recursive: true });
|
|
2751
|
+
const prVerdictSrc = join18(dir, PR_VERDICT_SRC);
|
|
2752
|
+
if (existsSync13(prVerdictSrc)) {
|
|
2753
|
+
const prVerdictPath = join18(dir, PR_VERDICT);
|
|
2754
|
+
writeFileSync9(prVerdictPath, readFileSync13(prVerdictSrc, "utf8"));
|
|
2755
|
+
kept.push(join18("missions", mission, PR_VERDICT));
|
|
2756
|
+
}
|
|
1489
2757
|
if (fold.length) {
|
|
1490
2758
|
const sections = fold.map((f) => {
|
|
1491
|
-
const body =
|
|
2759
|
+
const body = readFileSync13(join18(dir, f), "utf8").trim();
|
|
1492
2760
|
const name = f.includes("/") ? f.split("/").pop() ?? f : f;
|
|
1493
2761
|
return `
|
|
1494
2762
|
|
|
@@ -1502,22 +2770,26 @@ ${body}`;
|
|
|
1502
2770
|
evidence: Array.isArray(state.evidence) ? state.evidence : [],
|
|
1503
2771
|
sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
|
|
1504
2772
|
}), mission) : "";
|
|
1505
|
-
|
|
2773
|
+
writeFileSync9(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
|
|
1506
2774
|
${costSection}
|
|
1507
2775
|
` : "") + `
|
|
1508
2776
|
`);
|
|
1509
2777
|
renameSync(tmp, reportPath);
|
|
1510
2778
|
}
|
|
1511
2779
|
for (const f of fold) {
|
|
1512
|
-
rmSync2(
|
|
1513
|
-
removed.push(
|
|
2780
|
+
rmSync2(join18(dir, f), { force: true, recursive: true });
|
|
2781
|
+
removed.push(join18("missions", mission, f));
|
|
2782
|
+
}
|
|
2783
|
+
if (existsSync13(prVerdictSrc)) {
|
|
2784
|
+
rmSync2(join18(dir, PR_VERDICT_SRC), { force: true });
|
|
2785
|
+
removed.push(join18("missions", mission, PR_VERDICT_SRC));
|
|
1514
2786
|
}
|
|
1515
2787
|
for (const f of files.filter((f2) => f2.endsWith(".json")))
|
|
1516
|
-
rmSync2(
|
|
1517
|
-
if (
|
|
2788
|
+
rmSync2(join18(dir, f), { force: true });
|
|
2789
|
+
if (existsSync13(artDir) && readdirSync7(artDir).length === 0)
|
|
1518
2790
|
rmSync2(artDir, { recursive: true, force: true });
|
|
1519
|
-
if (!
|
|
1520
|
-
|
|
2791
|
+
if (!existsSync13(reportPath)) {
|
|
2792
|
+
writeFileSync9(reportPath, `# Mission: ${mission}
|
|
1521
2793
|
|
|
1522
2794
|
Archived before closure — no wave artifacts were present.
|
|
1523
2795
|
`);
|
|
@@ -1529,38 +2801,44 @@ Archived before closure — no wave artifacts were present.
|
|
|
1529
2801
|
baseSha: typeof state.base_sha === "string" ? state.base_sha : "unknown"
|
|
1530
2802
|
});
|
|
1531
2803
|
if (rb)
|
|
1532
|
-
kept.push(
|
|
2804
|
+
kept.push(join18("missions", mission, rb.file));
|
|
1533
2805
|
try {
|
|
2806
|
+
const st = tasksFromState(state);
|
|
2807
|
+
const pt = countPlanTasks(dir);
|
|
2808
|
+
const tasks_done = pt.total > 0 ? pt.done : st.done;
|
|
2809
|
+
const tasks_total = pt.total > 0 ? pt.total : st.total;
|
|
2810
|
+
const baseShaForNote = typeof state.base_sha === "string" ? state.base_sha : undefined;
|
|
1534
2811
|
writeProvenance(projectDir, dir, {
|
|
1535
2812
|
mission,
|
|
1536
2813
|
actor: typeof state.actor === "string" ? state.actor : "",
|
|
1537
2814
|
lane: typeof state.lane === "string" ? state.lane : "",
|
|
1538
2815
|
mode: typeof state.mode === "string" ? state.mode : "",
|
|
1539
2816
|
branch: state.branch,
|
|
1540
|
-
tasks_done
|
|
1541
|
-
tasks_total
|
|
2817
|
+
tasks_done,
|
|
2818
|
+
tasks_total,
|
|
1542
2819
|
evidence: Array.isArray(state.evidence) ? state.evidence : [],
|
|
1543
|
-
models: stageModels
|
|
1544
|
-
|
|
1545
|
-
|
|
2820
|
+
models: stageModels,
|
|
2821
|
+
base_sha: baseShaForNote
|
|
2822
|
+
}, baseShaForNote);
|
|
2823
|
+
kept.push(join18("missions", mission, "provenance.md"));
|
|
1546
2824
|
} catch {}
|
|
1547
2825
|
}
|
|
1548
2826
|
} else {
|
|
1549
2827
|
for (const f of fold)
|
|
1550
|
-
removed.push(
|
|
2828
|
+
removed.push(join18("missions", mission, f));
|
|
1551
2829
|
}
|
|
1552
|
-
removed.push(
|
|
2830
|
+
removed.push(join18("missions", mission, "<session state>"));
|
|
1553
2831
|
if (files.includes("plan.md"))
|
|
1554
|
-
kept.push(
|
|
2832
|
+
kept.push(join18("missions", mission, "plan.md"));
|
|
1555
2833
|
if (files.includes("handoff.md"))
|
|
1556
|
-
kept.push(
|
|
1557
|
-
kept.push(
|
|
2834
|
+
kept.push(join18("missions", mission, "handoff.md"));
|
|
2835
|
+
kept.push(join18("missions", mission, "report.md"));
|
|
1558
2836
|
let index;
|
|
1559
|
-
const indexFile =
|
|
2837
|
+
const indexFile = join18(root, "index.md");
|
|
1560
2838
|
const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}
|
|
1561
2839
|
`;
|
|
1562
2840
|
if (!dryRun) {
|
|
1563
|
-
const existing =
|
|
2841
|
+
const existing = existsSync13(indexFile) ? readFileSync13(indexFile, "utf8") : "";
|
|
1564
2842
|
if (!existing.split(/\r?\n/).some((l) => l.startsWith(`- ${mission} —`))) {
|
|
1565
2843
|
const header = existing ? "" : `# Mission index
|
|
1566
2844
|
|
|
@@ -1574,28 +2852,28 @@ Archived before closure — no wave artifacts were present.
|
|
|
1574
2852
|
}
|
|
1575
2853
|
index = "index.md";
|
|
1576
2854
|
}
|
|
1577
|
-
return { report:
|
|
2855
|
+
return { report: join18("missions", mission, "report.md"), removed, kept, index };
|
|
1578
2856
|
}
|
|
1579
2857
|
|
|
1580
2858
|
// src/run.ts
|
|
1581
|
-
import { existsSync as
|
|
2859
|
+
import { existsSync as existsSync14, readdirSync as readdirSync8 } from "node:fs";
|
|
1582
2860
|
import { spawnSync } from "node:child_process";
|
|
1583
|
-
import { dirname as dirname5, join as
|
|
2861
|
+
import { dirname as dirname5, join as join19, resolve } from "node:path";
|
|
1584
2862
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1585
2863
|
var here3 = dirname5(fileURLToPath4(import.meta.url));
|
|
1586
|
-
var SCRIPTS_DIR =
|
|
2864
|
+
var SCRIPTS_DIR = join19(here3, "..", "scripts");
|
|
1587
2865
|
var RUNNABLE = ["savepoint.sh", "lane.sh"];
|
|
1588
2866
|
function findBash() {
|
|
1589
2867
|
const explicit = process.env.MUGIWARA_BASH?.trim();
|
|
1590
2868
|
if (explicit)
|
|
1591
|
-
return
|
|
2869
|
+
return existsSync14(explicit) ? explicit : null;
|
|
1592
2870
|
const candidates = process.platform === "win32" ? [
|
|
1593
2871
|
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
1594
2872
|
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
1595
|
-
|
|
2873
|
+
join19(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
|
|
1596
2874
|
] : ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash", "/opt/homebrew/bin/bash"];
|
|
1597
2875
|
for (const p of candidates) {
|
|
1598
|
-
if (p &&
|
|
2876
|
+
if (p && existsSync14(p))
|
|
1599
2877
|
return p;
|
|
1600
2878
|
}
|
|
1601
2879
|
const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bash"], {
|
|
@@ -1612,9 +2890,9 @@ function runScript(name, args, projectDir) {
|
|
|
1612
2890
|
if (!/^[a-z0-9-]+\.sh$/.test(name)) {
|
|
1613
2891
|
throw new Error(`invalid script name "${name}" (expected one of: ${RUNNABLE.join(", ")})`);
|
|
1614
2892
|
}
|
|
1615
|
-
const script =
|
|
1616
|
-
if (!
|
|
1617
|
-
const have =
|
|
2893
|
+
const script = join19(SCRIPTS_DIR, name);
|
|
2894
|
+
if (!existsSync14(script)) {
|
|
2895
|
+
const have = existsSync14(SCRIPTS_DIR) ? readdirSync8(SCRIPTS_DIR).filter((f) => f.endsWith(".sh")) : [];
|
|
1618
2896
|
throw new Error(`script not found: ${name}${have.length ? ` (available: ${have.join(", ")})` : ""}`);
|
|
1619
2897
|
}
|
|
1620
2898
|
const bash = findBash();
|
|
@@ -1631,9 +2909,30 @@ function runScript(name, args, projectDir) {
|
|
|
1631
2909
|
}
|
|
1632
2910
|
|
|
1633
2911
|
// src/continue.ts
|
|
1634
|
-
import { existsSync as
|
|
1635
|
-
import { execFileSync as
|
|
1636
|
-
import { join as
|
|
2912
|
+
import { existsSync as existsSync15, readFileSync as readFileSync14, readdirSync as readdirSync9 } from "node:fs";
|
|
2913
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
2914
|
+
import { join as join20 } from "node:path";
|
|
2915
|
+
var CURRENT_SCHEMA_VERSION = 2;
|
|
2916
|
+
function hasLegacyLayout(projectDir) {
|
|
2917
|
+
for (const legacy of [join20(projectDir, ".mugiwara", "state"), join20(projectDir, ".mugiwara", "continue")]) {
|
|
2918
|
+
if (!existsSync15(legacy))
|
|
2919
|
+
continue;
|
|
2920
|
+
try {
|
|
2921
|
+
const walk = (dir) => {
|
|
2922
|
+
for (const e of readdirSync9(dir, { withFileTypes: true })) {
|
|
2923
|
+
if (e.isFile() && e.name.endsWith(".json"))
|
|
2924
|
+
return true;
|
|
2925
|
+
if (e.isDirectory() && walk(join20(dir, e.name)))
|
|
2926
|
+
return true;
|
|
2927
|
+
}
|
|
2928
|
+
return false;
|
|
2929
|
+
};
|
|
2930
|
+
if (walk(legacy))
|
|
2931
|
+
return true;
|
|
2932
|
+
} catch {}
|
|
2933
|
+
}
|
|
2934
|
+
return false;
|
|
2935
|
+
}
|
|
1637
2936
|
var SAFE = /^[A-Za-z0-9._-]+$/;
|
|
1638
2937
|
var isSafeKey = (s) => SAFE.test(s) && !/^\.+$/.test(s);
|
|
1639
2938
|
var num = (v) => {
|
|
@@ -1650,7 +2949,7 @@ function gitActor(cwd) {
|
|
|
1650
2949
|
return author;
|
|
1651
2950
|
const git3 = (args) => {
|
|
1652
2951
|
try {
|
|
1653
|
-
return
|
|
2952
|
+
return execFileSync5("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1654
2953
|
} catch {
|
|
1655
2954
|
return "";
|
|
1656
2955
|
}
|
|
@@ -1662,14 +2961,14 @@ function gitActor(cwd) {
|
|
|
1662
2961
|
return name || process.env.USER || process.env.USERNAME || "";
|
|
1663
2962
|
}
|
|
1664
2963
|
function scan(projectDir, kind, map) {
|
|
1665
|
-
const base =
|
|
1666
|
-
if (!
|
|
2964
|
+
const base = join20(projectDir, ".mugiwara", "missions");
|
|
2965
|
+
if (!existsSync15(base))
|
|
1667
2966
|
return [];
|
|
1668
2967
|
const out = [];
|
|
1669
|
-
const missions =
|
|
2968
|
+
const missions = readdirSync9(base, { withFileTypes: true }).filter((e) => e.isDirectory() && isSafeKey(e.name)).map((e) => e.name).sort();
|
|
1670
2969
|
for (const mission of missions) {
|
|
1671
|
-
const dir =
|
|
1672
|
-
for (const f of
|
|
2970
|
+
const dir = join20(base, mission);
|
|
2971
|
+
for (const f of readdirSync9(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
1673
2972
|
const stem = f.slice(0, -".json".length);
|
|
1674
2973
|
let member;
|
|
1675
2974
|
if (kind === "state") {
|
|
@@ -1687,7 +2986,7 @@ function scan(projectDir, kind, map) {
|
|
|
1687
2986
|
if (member !== null && !isSafeKey(member))
|
|
1688
2987
|
continue;
|
|
1689
2988
|
try {
|
|
1690
|
-
const raw = JSON.parse(
|
|
2989
|
+
const raw = JSON.parse(readFileSync14(join20(dir, f), "utf8"));
|
|
1691
2990
|
if (text(raw.mission) !== mission)
|
|
1692
2991
|
continue;
|
|
1693
2992
|
out.push(map(raw, member));
|
|
@@ -1715,6 +3014,7 @@ function readContinue(projectDir) {
|
|
|
1715
3014
|
function readState(projectDir) {
|
|
1716
3015
|
return scan(projectDir, "state", (r, member) => {
|
|
1717
3016
|
const tasks = r.tasks ?? {};
|
|
3017
|
+
const sv = r.schema_version;
|
|
1718
3018
|
return {
|
|
1719
3019
|
mission: text(r.mission),
|
|
1720
3020
|
member,
|
|
@@ -1744,7 +3044,8 @@ function readState(projectDir) {
|
|
|
1744
3044
|
budget: num(r.budget),
|
|
1745
3045
|
budget_status: text(r.budget_status) || "ok",
|
|
1746
3046
|
files_touched: num(r.files_touched),
|
|
1747
|
-
evidence: Array.isArray(r.evidence) ? r.evidence.map(text).filter(Boolean) : []
|
|
3047
|
+
evidence: Array.isArray(r.evidence) ? r.evidence.map(text).filter(Boolean) : [],
|
|
3048
|
+
schema_version: typeof sv === "number" || typeof sv === "string" ? sv : null
|
|
1748
3049
|
};
|
|
1749
3050
|
});
|
|
1750
3051
|
}
|
|
@@ -1802,57 +3103,31 @@ function formatResume(e) {
|
|
|
1802
3103
|
return `Resumed: ${e.mission}${scope}, Flow ${e.flow}, ${e.tasks_done}/${e.tasks_total} tasks — next_action: ${e.next_action} — run: ${next}`;
|
|
1803
3104
|
}
|
|
1804
3105
|
|
|
1805
|
-
// src/
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
return { ok: false, message: "no report.md to sign — archive first" };
|
|
1831
|
-
if (!hasMinisign()) {
|
|
1832
|
-
return { ok: false, message: "minisign not installed — signing skipped (install minisign to enable attestation)" };
|
|
1833
|
-
}
|
|
1834
|
-
const secretKey = process.env.MUGIWARA_SIGN_KEY?.trim() || defaultKey("secret");
|
|
1835
|
-
try {
|
|
1836
|
-
execFileSync5("minisign", signArgs(report, secretKey), { cwd: projectDir, stdio: "pipe", input: process.env.MUGIWARA_SIGN_PASSWORD ?? "" });
|
|
1837
|
-
return { ok: true, message: `signed ${report}.minisig (key: ${secretKey})` };
|
|
1838
|
-
} catch (e) {
|
|
1839
|
-
return { ok: false, message: `signing failed: ${e.message}` };
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
function verifyReport(projectDir, missionDir) {
|
|
1843
|
-
const report = join14(missionDir, "report.md");
|
|
1844
|
-
const sig = `${report}.minisig`;
|
|
1845
|
-
if (!existsSync11(sig))
|
|
1846
|
-
return { ok: false, message: "not signed (no .minisig beside report.md)" };
|
|
1847
|
-
if (!hasMinisign())
|
|
1848
|
-
return { ok: false, message: "minisign not installed — cannot verify" };
|
|
1849
|
-
const pubKey = existsSync11(defaultKey("public")) ? defaultKey("public") : null;
|
|
1850
|
-
try {
|
|
1851
|
-
execFileSync5("minisign", verifyArgs(report, pubKey), { cwd: projectDir, stdio: "pipe" });
|
|
1852
|
-
return { ok: true, message: "signature verifies against report.md" };
|
|
1853
|
-
} catch {
|
|
1854
|
-
return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing" };
|
|
1855
|
-
}
|
|
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 };
|
|
1856
3131
|
}
|
|
1857
3132
|
|
|
1858
3133
|
// src/cli.ts
|
|
@@ -1866,6 +3141,23 @@ async function run(argv) {
|
|
|
1866
3141
|
console.log(`mugiwara ${VERSION}`);
|
|
1867
3142
|
return;
|
|
1868
3143
|
}
|
|
3144
|
+
{
|
|
3145
|
+
const bypass = new Set(["install", "update", "uninstall", "list"]);
|
|
3146
|
+
if (!bypass.has(command)) {
|
|
3147
|
+
const projectDirForHarness = resolve2(str(flags.project) ?? process.cwd());
|
|
3148
|
+
enforceHarnessPolicy(projectDirForHarness);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
const isDryRunInstall = (command === "install" || command === "update") && flag(flags.dryRun);
|
|
3152
|
+
if (!isDryRunInstall) {
|
|
3153
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
3154
|
+
if (ensureConfig(projectDir)) {
|
|
3155
|
+
console.log(`default .mugiwara/config written at ${join21(projectDir, ".mugiwara", "config")} (edit it to customise)`);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
if (command === "continue" || command === "status") {
|
|
3159
|
+
return command === "continue" ? continueCmd(flags, _) : statusCmd(flags);
|
|
3160
|
+
}
|
|
1869
3161
|
switch (command) {
|
|
1870
3162
|
case "install":
|
|
1871
3163
|
return install(flags);
|
|
@@ -1885,6 +3177,8 @@ async function run(argv) {
|
|
|
1885
3177
|
return continueCmd(flags, _);
|
|
1886
3178
|
case "status":
|
|
1887
3179
|
return statusCmd(flags);
|
|
3180
|
+
case "cost":
|
|
3181
|
+
return costCmd(flags, _);
|
|
1888
3182
|
case "run":
|
|
1889
3183
|
return runCmd(flags, _);
|
|
1890
3184
|
case "savepoint":
|
|
@@ -1895,6 +3189,8 @@ async function run(argv) {
|
|
|
1895
3189
|
return handoffCmd(flags, _);
|
|
1896
3190
|
case "sign":
|
|
1897
3191
|
return signCmd(flags, _);
|
|
3192
|
+
case "migrate":
|
|
3193
|
+
return migrateCmd(flags);
|
|
1898
3194
|
default:
|
|
1899
3195
|
throw new Error(`Unknown command: ${command}`);
|
|
1900
3196
|
}
|
|
@@ -1936,8 +3232,8 @@ function archive(flags, positionals) {
|
|
|
1936
3232
|
function cleanCmd(flags) {
|
|
1937
3233
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1938
3234
|
const dryRun = flag(flags.dryRun);
|
|
1939
|
-
const root =
|
|
1940
|
-
if (!
|
|
3235
|
+
const root = join21(projectDir, ".mugiwara", "missions");
|
|
3236
|
+
if (!existsSync16(root)) {
|
|
1941
3237
|
console.log("nothing to clean (.mugiwara/missions/ does not exist).");
|
|
1942
3238
|
return;
|
|
1943
3239
|
}
|
|
@@ -1947,8 +3243,8 @@ function cleanCmd(flags) {
|
|
|
1947
3243
|
console.error(`invalid --before date: ${before}`);
|
|
1948
3244
|
process.exit(1);
|
|
1949
3245
|
}
|
|
1950
|
-
let candidates =
|
|
1951
|
-
const stateFiles = (m) =>
|
|
3246
|
+
let candidates = readdirSync10(root, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Za-z0-9._-]+$/.test(e.name) && !/^\.+$/.test(e.name)).map((e) => e.name);
|
|
3247
|
+
const stateFiles = (m) => readdirSync10(join21(root, m)).filter((f) => {
|
|
1952
3248
|
const stem = f.replace(/\.json$/, "");
|
|
1953
3249
|
return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
|
|
1954
3250
|
});
|
|
@@ -1958,7 +3254,7 @@ function cleanCmd(flags) {
|
|
|
1958
3254
|
return false;
|
|
1959
3255
|
for (const f of stateFiles(m)) {
|
|
1960
3256
|
try {
|
|
1961
|
-
const ts = Date.parse(JSON.parse(
|
|
3257
|
+
const ts = Date.parse(JSON.parse(readFileSync15(join21(root, m, f), "utf8")).updated_at ?? "") || 0;
|
|
1962
3258
|
if (ts === 0 || ts >= beforeMs)
|
|
1963
3259
|
return false;
|
|
1964
3260
|
} catch {
|
|
@@ -1968,7 +3264,7 @@ function cleanCmd(flags) {
|
|
|
1968
3264
|
return true;
|
|
1969
3265
|
};
|
|
1970
3266
|
if (!flag(flags.all)) {
|
|
1971
|
-
candidates = candidates.filter((m) =>
|
|
3267
|
+
candidates = candidates.filter((m) => existsSync16(join21(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
|
|
1972
3268
|
} else if (!flag(flags.force)) {
|
|
1973
3269
|
const live = candidates.filter((m) => hasLiveState(m) && !staleBefore(m));
|
|
1974
3270
|
if (live.length) {
|
|
@@ -2002,7 +3298,7 @@ async function resolveOptions(flags) {
|
|
|
2002
3298
|
scope = await choose(rl, "Install scope?", ["global (user-wide)", "project (this repo)"]) === 0 ? "global" : "project";
|
|
2003
3299
|
}
|
|
2004
3300
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2005
|
-
if (scope === "project" && !
|
|
3301
|
+
if (scope === "project" && !existsSync16(projectDir))
|
|
2006
3302
|
throw new Error(`Project dir not found: ${projectDir}`);
|
|
2007
3303
|
let targetIds = str(flags.target)?.split(",").map((s) => s.trim()) ?? null;
|
|
2008
3304
|
if (targetIds && targetIds.includes("all"))
|
|
@@ -2081,7 +3377,7 @@ async function uninstall(flags) {
|
|
|
2081
3377
|
if (!manifest) {
|
|
2082
3378
|
if (scope === "project") {
|
|
2083
3379
|
const globalFile = manifestPath({ scope: "global", projectDir, home });
|
|
2084
|
-
if (
|
|
3380
|
+
if (existsSync16(globalFile)) {
|
|
2085
3381
|
console.log(`No project manifest found, but a global install exists. Try:
|
|
2086
3382
|
mugiwara uninstall --global`);
|
|
2087
3383
|
return;
|
|
@@ -2118,13 +3414,13 @@ async function uninstall(flags) {
|
|
|
2118
3414
|
console.log(` note: ${n}`);
|
|
2119
3415
|
}
|
|
2120
3416
|
rmSync3(file);
|
|
2121
|
-
const mugiDir =
|
|
2122
|
-
if (
|
|
3417
|
+
const mugiDir = join21(scope === "global" ? home : projectDir, ".mugiwara");
|
|
3418
|
+
if (existsSync16(mugiDir) && readdirSync10(mugiDir).length === 0) {
|
|
2123
3419
|
rmSync3(mugiDir, { recursive: true, force: true });
|
|
2124
3420
|
}
|
|
2125
3421
|
if (manifest.targets.includes("opencode")) {
|
|
2126
|
-
const opencodeCache =
|
|
2127
|
-
if (
|
|
3422
|
+
const opencodeCache = join21(home, ".cache", "opencode", "packages", "@ionivetech");
|
|
3423
|
+
if (existsSync16(opencodeCache)) {
|
|
2128
3424
|
rmSync3(opencodeCache, { recursive: true, force: true });
|
|
2129
3425
|
console.log(" cleared opencode npm cache (stale plugin versions)");
|
|
2130
3426
|
}
|
|
@@ -2132,9 +3428,25 @@ async function uninstall(flags) {
|
|
|
2132
3428
|
}
|
|
2133
3429
|
console.log(`OK removed ${removed.length} files`);
|
|
2134
3430
|
}
|
|
3431
|
+
function legacyWarning(projectDir) {
|
|
3432
|
+
if (hasLegacyLayout(projectDir)) {
|
|
3433
|
+
console.error("⚠ legacy layout detected (v0.6 .mugiwara/state/ — run `mugiwara migrate` to move to missions/)");
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
function schemaWarnings(projectDir) {
|
|
3437
|
+
const states = readState(projectDir);
|
|
3438
|
+
for (const s of states) {
|
|
3439
|
+
const v = s.schema_version;
|
|
3440
|
+
if (v !== CURRENT_SCHEMA_VERSION) {
|
|
3441
|
+
const wrote = v === null || v === undefined || v === "" ? "unknown" : String(v);
|
|
3442
|
+
console.error(`⚠ state written by v${wrote} (mission ${s.mission}${s.member ? `/${s.member}` : ""}) — current expects v${CURRENT_SCHEMA_VERSION} — run \`mugiwara migrate\``);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
2135
3446
|
function list(flags) {
|
|
2136
3447
|
const home = homedir4();
|
|
2137
3448
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
3449
|
+
legacyWarning(projectDir);
|
|
2138
3450
|
let found = false;
|
|
2139
3451
|
for (const [label, file] of [
|
|
2140
3452
|
["project", manifestPath({ scope: "project", projectDir, home })],
|
|
@@ -2145,7 +3457,7 @@ function list(flags) {
|
|
|
2145
3457
|
continue;
|
|
2146
3458
|
found = true;
|
|
2147
3459
|
if (flag(flags.check)) {
|
|
2148
|
-
const missing = m.files.filter((f) => !
|
|
3460
|
+
const missing = m.files.filter((f) => !existsSync16(f));
|
|
2149
3461
|
console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
|
|
2150
3462
|
} else {
|
|
2151
3463
|
console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} installed=${m.installedAt}`);
|
|
@@ -2156,6 +3468,8 @@ function list(flags) {
|
|
|
2156
3468
|
}
|
|
2157
3469
|
function continueCmd(flags, positionals) {
|
|
2158
3470
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
3471
|
+
legacyWarning(projectDir);
|
|
3472
|
+
schemaWarnings(projectDir);
|
|
2159
3473
|
const [mission, member] = positionals.slice(1);
|
|
2160
3474
|
let entries = readContinue(projectDir);
|
|
2161
3475
|
if (!flag(flags.all)) {
|
|
@@ -2196,6 +3510,8 @@ Pick one: mugiwara continue ${r.mission} <member>`);
|
|
|
2196
3510
|
}
|
|
2197
3511
|
function statusCmd(flags) {
|
|
2198
3512
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
3513
|
+
legacyWarning(projectDir);
|
|
3514
|
+
schemaWarnings(projectDir);
|
|
2199
3515
|
const states = readState(projectDir);
|
|
2200
3516
|
if (!states.length) {
|
|
2201
3517
|
console.log("No mission state on disk.");
|
|
@@ -2215,6 +3531,58 @@ function statusCmd(flags) {
|
|
|
2215
3531
|
console.log(` evidence: ${s.evidence.join(", ")}`);
|
|
2216
3532
|
}
|
|
2217
3533
|
}
|
|
3534
|
+
function costCmd(flags, positionals) {
|
|
3535
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
3536
|
+
const mission = str(flags.mission) ?? positionals[1] ?? (() => {
|
|
3537
|
+
const states2 = readState(projectDir);
|
|
3538
|
+
if (states2.length === 1)
|
|
3539
|
+
return states2[0].mission;
|
|
3540
|
+
if (states2.length > 1) {
|
|
3541
|
+
console.error("multiple missions in flight — specify --mission <id>");
|
|
3542
|
+
process.exit(1);
|
|
3543
|
+
}
|
|
3544
|
+
return null;
|
|
3545
|
+
})();
|
|
3546
|
+
if (!mission) {
|
|
3547
|
+
console.error("usage: mugiwara cost [--mission <id>] [--json] [--ledger] [--project <dir>]");
|
|
3548
|
+
process.exit(1);
|
|
3549
|
+
}
|
|
3550
|
+
const missionDir = join21(projectDir, ".mugiwara", "missions", mission);
|
|
3551
|
+
if (!existsSync16(missionDir)) {
|
|
3552
|
+
console.error(`No cost ledger found for mission "${mission}"`);
|
|
3553
|
+
process.exit(1);
|
|
3554
|
+
}
|
|
3555
|
+
const states = readState(projectDir).filter((s) => s.mission === mission);
|
|
3556
|
+
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 });
|
|
3557
|
+
const state0 = states[0];
|
|
3558
|
+
let repeatedReads = 0;
|
|
3559
|
+
try {
|
|
3560
|
+
const reg = loadRegistry(missionDir);
|
|
3561
|
+
repeatedReads = reg.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
|
|
3562
|
+
} catch {
|
|
3563
|
+
repeatedReads = 0;
|
|
3564
|
+
}
|
|
3565
|
+
const liveSlop = computeLiveSlop({ heal_cycle: state0?.heal_cycle ?? 0, repeated_reads: repeatedReads });
|
|
3566
|
+
const ledger = buildCostLedger({ missionDir, envelope, slopSummary: { interventions: liveSlop.interventions } });
|
|
3567
|
+
if (flag(flags.json)) {
|
|
3568
|
+
console.log(toCostJSON(ledger));
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
console.log(`Cost envelope: ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned})`);
|
|
3572
|
+
console.log(`Avoided: ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ~${ledger.avoided.tokens_avoided_est} tokens`);
|
|
3573
|
+
console.log(`Efficiency: reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}%`);
|
|
3574
|
+
if (ledger.avoided.slop_interventions > 0) {
|
|
3575
|
+
const roles = Object.entries(liveSlop.perRole).map(([r, n]) => `${r}:${n}`).join(", ");
|
|
3576
|
+
console.log(`Slop: ${ledger.avoided.slop_interventions} intervention(s) — ${roles}`);
|
|
3577
|
+
}
|
|
3578
|
+
console.log(`Trail: ${ledger.trail.length} decisions`);
|
|
3579
|
+
if (flag(flags.ledger) && ledger.trail.length) {
|
|
3580
|
+
for (const t of ledger.trail.slice(0, 20))
|
|
3581
|
+
console.log(`- ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ""}`);
|
|
3582
|
+
if (ledger.trail.length > 20)
|
|
3583
|
+
console.log(`… ${ledger.trail.length - 20} more`);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
2218
3586
|
function runCmd(flags, positionals) {
|
|
2219
3587
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2220
3588
|
const name = positionals[1];
|
|
@@ -2300,8 +3668,8 @@ function handoffCmd(flags, positionals) {
|
|
|
2300
3668
|
}
|
|
2301
3669
|
lines.push("", "## Resuming", "", `\`mugiwara continue ${mission}\` prints the exact resume point.`);
|
|
2302
3670
|
lines.push("Verify `next_action` against plan.md before executing — the table above is computed state, not judgement.");
|
|
2303
|
-
const out =
|
|
2304
|
-
|
|
3671
|
+
const out = join21(".mugiwara", "missions", mission, "handoff.md");
|
|
3672
|
+
writeFileSync10(resolve2(projectDir, out), lines.join(`
|
|
2305
3673
|
`) + `
|
|
2306
3674
|
`);
|
|
2307
3675
|
console.log(lines.join(`
|
|
@@ -2309,15 +3677,126 @@ function handoffCmd(flags, positionals) {
|
|
|
2309
3677
|
console.log(`
|
|
2310
3678
|
written: ${out}`);
|
|
2311
3679
|
}
|
|
3680
|
+
function migrateCmd(flags) {
|
|
3681
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
3682
|
+
const dryRun = flag(flags.dryRun);
|
|
3683
|
+
const legacyState = join21(projectDir, ".mugiwara", "state");
|
|
3684
|
+
const legacyContinue = join21(projectDir, ".mugiwara", "continue");
|
|
3685
|
+
const missionsRoot = join21(projectDir, ".mugiwara", "missions");
|
|
3686
|
+
const moves = [];
|
|
3687
|
+
const collect = (srcRoot, isContinue) => {
|
|
3688
|
+
if (!existsSync16(srcRoot))
|
|
3689
|
+
return;
|
|
3690
|
+
const walk = (dir) => {
|
|
3691
|
+
for (const e of readdirSync10(dir, { withFileTypes: true })) {
|
|
3692
|
+
const full = join21(dir, e.name);
|
|
3693
|
+
if (e.isDirectory())
|
|
3694
|
+
walk(full);
|
|
3695
|
+
else if (e.isFile() && e.name.endsWith(".json")) {
|
|
3696
|
+
const rel = full.slice(srcRoot.length + 1);
|
|
3697
|
+
let destRel;
|
|
3698
|
+
if (isContinue) {
|
|
3699
|
+
const parts = rel.split("/");
|
|
3700
|
+
const file = parts.pop();
|
|
3701
|
+
const mission = parts.join("/");
|
|
3702
|
+
const stem = file.slice(0, -".json".length);
|
|
3703
|
+
let destFile;
|
|
3704
|
+
if (stem === "state")
|
|
3705
|
+
destFile = "continue.json";
|
|
3706
|
+
else
|
|
3707
|
+
destFile = `continue-${stem}.json`;
|
|
3708
|
+
destRel = mission ? join21(mission, destFile) : destFile;
|
|
3709
|
+
} else {
|
|
3710
|
+
destRel = rel;
|
|
3711
|
+
}
|
|
3712
|
+
moves.push({ src: full, dest: join21(missionsRoot, destRel) });
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
};
|
|
3716
|
+
walk(srcRoot);
|
|
3717
|
+
};
|
|
3718
|
+
collect(legacyState, false);
|
|
3719
|
+
collect(legacyContinue, true);
|
|
3720
|
+
if (!moves.length) {
|
|
3721
|
+
if (!existsSync16(legacyState) && !existsSync16(legacyContinue)) {
|
|
3722
|
+
console.log("no legacy layout found (.mugiwara/state/ does not exist)");
|
|
3723
|
+
} else {
|
|
3724
|
+
console.log("no legacy state files to migrate");
|
|
3725
|
+
}
|
|
3726
|
+
return;
|
|
3727
|
+
}
|
|
3728
|
+
for (const m of moves) {
|
|
3729
|
+
console.log(`${dryRun ? "would migrate" : "migrated"} ${m.src} → ${m.dest}`);
|
|
3730
|
+
if (!dryRun) {
|
|
3731
|
+
mkdirSync10(dirname6(m.dest), { recursive: true });
|
|
3732
|
+
try {
|
|
3733
|
+
const raw = JSON.parse(readFileSync15(m.src, "utf8"));
|
|
3734
|
+
raw.schema_version = CURRENT_SCHEMA_VERSION;
|
|
3735
|
+
writeFileSync10(m.dest, JSON.stringify(raw, null, 2) + `
|
|
3736
|
+
`);
|
|
3737
|
+
rmSync3(m.src, { force: true });
|
|
3738
|
+
} catch {
|
|
3739
|
+
try {
|
|
3740
|
+
renameSync2(m.src, m.dest);
|
|
3741
|
+
} catch {}
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
if (!dryRun) {
|
|
3746
|
+
const prune = (root) => {
|
|
3747
|
+
if (!existsSync16(root))
|
|
3748
|
+
return;
|
|
3749
|
+
const walkPrune = (dir) => {
|
|
3750
|
+
for (const e of readdirSync10(dir, { withFileTypes: true })) {
|
|
3751
|
+
if (e.isDirectory())
|
|
3752
|
+
walkPrune(join21(dir, e.name));
|
|
3753
|
+
}
|
|
3754
|
+
try {
|
|
3755
|
+
if (readdirSync10(dir).length === 0)
|
|
3756
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
3757
|
+
} catch {}
|
|
3758
|
+
};
|
|
3759
|
+
walkPrune(root);
|
|
3760
|
+
try {
|
|
3761
|
+
if (existsSync16(root) && readdirSync10(root).length === 0)
|
|
3762
|
+
rmSync3(root, { recursive: true, force: true });
|
|
3763
|
+
} catch {}
|
|
3764
|
+
};
|
|
3765
|
+
prune(legacyState);
|
|
3766
|
+
prune(legacyContinue);
|
|
3767
|
+
}
|
|
3768
|
+
console.log(`${dryRun ? "would migrate" : "migrated"} ${moves.length} file(s)${dryRun ? " (dry run)" : ""}`);
|
|
3769
|
+
}
|
|
2312
3770
|
function signCmd(flags, _) {
|
|
2313
3771
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
3772
|
+
if (flag(flags.genKey)) {
|
|
3773
|
+
const backend = str(flags.backend) ?? "auto";
|
|
3774
|
+
const home = homedir4();
|
|
3775
|
+
if (backend === "minisign") {
|
|
3776
|
+
if (!hasMinisign()) {
|
|
3777
|
+
console.error("✗ minisign not installed — cannot generate keys with this backend");
|
|
3778
|
+
process.exit(1);
|
|
3779
|
+
}
|
|
3780
|
+
try {
|
|
3781
|
+
execFileSync6("minisign", ["-G"], { stdio: "inherit" });
|
|
3782
|
+
console.log("✓ minisign key pair generated in ~/.mugiwara/");
|
|
3783
|
+
return;
|
|
3784
|
+
} catch {
|
|
3785
|
+
console.error("✗ key generation failed");
|
|
3786
|
+
process.exit(1);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
const dir = ensurePureKey(home);
|
|
3790
|
+
console.log(`✓ pure ed25519 key pair ready: ${join21(dir, "mugiwara.key")} / ${join21(dir, "mugiwara.pub")}`);
|
|
3791
|
+
return;
|
|
3792
|
+
}
|
|
2314
3793
|
const mission = _[1];
|
|
2315
3794
|
if (!mission) {
|
|
2316
|
-
console.error("usage: mugiwara sign <mission> [--verify] [--project <dir>]");
|
|
3795
|
+
console.error("usage: mugiwara sign <mission> [--verify] [--gen-key [--backend pure|minisign]] [--project <dir>]");
|
|
2317
3796
|
process.exit(1);
|
|
2318
3797
|
}
|
|
2319
|
-
const missionDir =
|
|
2320
|
-
if (!
|
|
3798
|
+
const missionDir = join21(projectDir, ".mugiwara", "missions", mission);
|
|
3799
|
+
if (!existsSync16(missionDir)) {
|
|
2321
3800
|
console.error(`no mission dir: ${missionDir}`);
|
|
2322
3801
|
process.exit(1);
|
|
2323
3802
|
}
|
|
@@ -2343,15 +3822,21 @@ Usage:
|
|
|
2343
3822
|
mugiwara continue <m> [member]
|
|
2344
3823
|
print the exact resume point for that mission/member
|
|
2345
3824
|
mugiwara status computed mission state: wave, tasks, lane, blockers, budget
|
|
3825
|
+
mugiwara cost [--mission <id>] [--json] [--ledger]
|
|
3826
|
+
show cost ledger, avoided work, efficiency, trail (human + JSON)
|
|
2346
3827
|
mugiwara blame <path> provenance note on the last commit touching <path>
|
|
2347
3828
|
(fetch notes first: git fetch origin 'refs/notes/mugiwara:refs/notes/mugiwara')
|
|
2348
3829
|
mugiwara handoff <m> write .mugiwara/missions/<m>/handoff.md — a report the next
|
|
2349
3830
|
engineer can act on (computed state + staleness check)
|
|
2350
|
-
mugiwara sign <m>
|
|
3831
|
+
mugiwara sign <m> attestation: sign report.md (auto/minisign/pure/off; --verify to check)
|
|
3832
|
+
mugiwara sign --gen-key [--backend pure|minisign]
|
|
3833
|
+
create signing keys (pure ed25519 default)
|
|
3834
|
+
mugiwara migrate [--dry-run] [--project <dir>]
|
|
3835
|
+
move legacy .mugiwara/state/ layout to .mugiwara/missions/
|
|
2351
3836
|
mugiwara run <script> [args...]
|
|
2352
|
-
|
|
3837
|
+
run a bundled harness script here (${RUNNABLE.join(", ")})
|
|
2353
3838
|
mugiwara savepoint <mission> [member] [flow] [mode]
|
|
2354
|
-
|
|
3839
|
+
shorthand for: mugiwara run savepoint.sh ...
|
|
2355
3840
|
mugiwara --help this help
|
|
2356
3841
|
mugiwara --version print version
|
|
2357
3842
|
|
|
@@ -2376,6 +3861,7 @@ if (isMain) {
|
|
|
2376
3861
|
});
|
|
2377
3862
|
}
|
|
2378
3863
|
export {
|
|
3864
|
+
migrateCmd,
|
|
2379
3865
|
run,
|
|
2380
3866
|
stalenessLine
|
|
2381
3867
|
};
|