@ionivetech/mugiwara 0.6.6 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -7
- package/.codex-plugin/plugin.json +2 -7
- package/.cursor-plugin/plugin.json +2 -7
- package/.kimi-plugin/plugin.json +2 -7
- package/.opencode/mugiwara-helpers.mjs +2 -2
- package/.opencode/plugins/mugiwara.mjs +3 -18
- package/AGENTS.md +5 -4
- package/GEMINI.md +3 -3
- package/README.md +203 -331
- package/content/agents/brook-healing.md +8 -4
- package/content/agents/chopper-checkpoint.md +7 -3
- package/content/agents/eval-runner.md +4 -4
- package/content/agents/franky-gates.md +4 -4
- package/content/agents/jinbe-security.md +6 -7
- package/content/agents/luffy-orchestrator.md +11 -13
- package/content/agents/memory-keeper.md +4 -4
- package/content/agents/nami-planner.md +4 -4
- package/content/agents/resume-coordinator.md +7 -7
- package/content/agents/robin-reviewer.md +7 -3
- package/content/agents/sanji-quality.md +2 -2
- package/content/agents/skeptic-verifier.md +2 -2
- package/content/agents/usopp-brainstorm.md +3 -3
- package/content/agents/zoro-execution.md +6 -6
- package/content/skills/mugiwara-backend/SKILL.md +54 -43
- package/content/skills/mugiwara-backend/references/database.md +61 -0
- package/content/skills/mugiwara-brainstorm/SKILL.md +1 -1
- package/content/skills/mugiwara-checkpoint/SKILL.md +23 -11
- package/content/skills/mugiwara-checkpoint/references/ledger-format.md +1 -1
- package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
- package/content/skills/mugiwara-execution/SKILL.md +37 -37
- 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-execution/references/resume-batching.md +2 -2
- package/content/skills/mugiwara-frontend/SKILL.md +44 -43
- package/content/skills/mugiwara-frontend/references/checklist.md +14 -0
- package/content/skills/mugiwara-gates/SKILL.md +23 -14
- package/content/skills/mugiwara-healing/SKILL.md +26 -25
- package/content/skills/mugiwara-lessons/SKILL.md +2 -2
- package/content/skills/mugiwara-orchestration/SKILL.md +11 -11
- package/content/skills/mugiwara-orchestration/references/check-ins.md +2 -2
- package/content/skills/mugiwara-orchestration/references/closure.md +17 -12
- package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +11 -1
- package/content/skills/mugiwara-planning/SKILL.md +26 -29
- package/content/skills/mugiwara-planning/references/large-campaign-subplan.md +41 -0
- package/content/skills/mugiwara-planning/references/plan-template.md +24 -2
- package/content/skills/mugiwara-quality/SKILL.md +21 -12
- package/content/skills/mugiwara-resume/SKILL.md +8 -3
- package/content/skills/mugiwara-review/SKILL.md +19 -13
- package/content/skills/mugiwara-security/SKILL.md +47 -36
- package/content/skills/mugiwara-ship/SKILL.md +1 -1
- package/content/skills/mugiwara-ship/references/cleanup.md +17 -16
- package/content/skills/mugiwara-workflow/SKILL.md +11 -17
- package/content/skills/mugiwara-workflow/references/adaptive-budget-governor.md +5 -0
- package/content/skills/mugiwara-workflow/references/benchmark-governor.md +53 -0
- package/content/skills/mugiwara-workflow/references/cognitive-output-governor.md +5 -0
- package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
- package/content/skills/mugiwara-workflow/references/scope-code-governor.md +14 -0
- package/content/skills/mugiwara-workflow/references/stop-slop-governor.md +14 -0
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +42 -38
- package/dist/mugiwara.js +1642 -528
- package/gemini-extension.json +1 -1
- package/hooks/auto-savepoint.js +18 -10
- package/hooks/auto-savepoint.ts +23 -23
- package/hooks/engagement-marker.js +1 -1
- package/hooks/engagement-marker.ts +1 -1
- package/hooks/pipeline-guard.js +17 -13
- package/hooks/pipeline-guard.ts +24 -20
- package/hooks/session-start.js +13 -10
- package/hooks/session-start.ts +24 -19
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/complexity.md +27 -4
- package/references/definition-of-done.md +1 -1
- package/references/multi-actor.md +20 -2
- package/references/posture-routing.md +31 -0
- package/references/prose-style.md +54 -0
- package/scripts/benchmark-governor.ts +516 -0
- package/scripts/benchmark-thresholds.json +47 -0
- package/scripts/check-doc-links.ts +46 -0
- package/scripts/conformance.ts +6 -16
- package/scripts/coverage-gate.ts +20 -6
- package/scripts/gate-selftest.ts +138 -7
- package/scripts/lane.sh +54 -8
- package/scripts/lib/lane-base.sh +4 -4
- package/scripts/lib/patterns.sh +5 -0
- package/scripts/policy-force.ts +22 -0
- package/scripts/retrieval-eval.ts +10 -4
- package/scripts/savepoint.sh +155 -58
- package/scripts/validate-content.ts +43 -11
- package/scripts/verify-install.ts +9 -7
- package/src/adaptive-budget.ts +178 -0
- package/src/args.ts +4 -3
- package/src/budget.ts +47 -0
- package/src/check-artifacts.ts +45 -0
- package/src/cli.ts +257 -108
- package/src/cognition.ts +234 -0
- package/src/config.ts +107 -0
- package/src/context.ts +72 -0
- package/src/continue.ts +29 -10
- package/src/cost.ts +186 -0
- package/src/evidence.ts +160 -0
- package/src/installer.ts +25 -34
- package/src/integrity.ts +158 -0
- package/src/investigation.ts +72 -0
- package/src/mission.ts +341 -94
- package/src/policy.ts +156 -0
- package/src/posture.ts +86 -0
- package/src/provenance.ts +116 -0
- package/src/reporting.ts +225 -0
- package/src/rollback.ts +95 -0
- package/src/routing.ts +69 -0
- package/src/run.ts +2 -2
- package/src/scope.ts +321 -0
- package/src/sign.ts +235 -0
- package/src/slop.ts +306 -0
- package/src/targets/claude.ts +2 -2
- package/src/work.ts +273 -0
- package/.opencode/commands/mugiwara-execute.md +0 -13
- package/.opencode/commands/mugiwara-heal.md +0 -13
- package/.opencode/commands/mugiwara-onboard.md +0 -14
- package/.opencode/commands/mugiwara-plan.md +0 -17
- package/.opencode/commands/mugiwara-ship.md +0 -13
- package/.opencode/commands/using-mugiwara.md +0 -20
- package/content/skills/mugiwara-agent-security/SKILL.md +0 -58
- package/content/skills/mugiwara-agent-security/references/checklist.md +0 -20
- package/content/skills/mugiwara-context-budget/SKILL.md +0 -66
- package/content/skills/mugiwara-context-budget/references/context-budget.md +0 -37
- package/content/skills/mugiwara-pr/SKILL.md +0 -69
- package/content/skills/mugiwara-pr/references/verdict-format.md +0 -31
- package/content/skills/mugiwara-sunset/SKILL.md +0 -82
- package/content/skills/using-mugiwara/SKILL.md +0 -51
- package/references/token-budget.md +0 -56
- package/scripts/evidence.sh +0 -81
- package/scripts/initiative.ts +0 -296
- package/scripts/mission-report.sh +0 -293
- package/src/onboard.ts +0 -207
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
|
|
5
|
-
import {
|
|
6
|
-
import { homedir as
|
|
7
|
-
import { join as
|
|
4
|
+
import { existsSync as existsSync15, readdirSync as readdirSync10, readFileSync as readFileSync14, rmSync as rmSync3, writeFileSync as writeFileSync10 } from "node:fs";
|
|
5
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
6
|
+
import { homedir as homedir4 } from "node:os";
|
|
7
|
+
import { join as join20, resolve as resolve2 } from "node:path";
|
|
8
8
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
9
9
|
|
|
10
10
|
// src/args.ts
|
|
11
|
-
var VALUE_FLAGS = { "--project": "project", "--target": "target" };
|
|
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",
|
|
@@ -18,10 +18,14 @@ var BOOL_FLAGS = {
|
|
|
18
18
|
"--keep-logs": "keepLogs",
|
|
19
19
|
"--check": "check",
|
|
20
20
|
"--all": "all",
|
|
21
|
+
"--verify": "verify",
|
|
22
|
+
"--gen-key": "genKey",
|
|
21
23
|
"--help": "help",
|
|
22
24
|
"-h": "help",
|
|
23
25
|
"--version": "version",
|
|
24
|
-
"-v": "version"
|
|
26
|
+
"-v": "version",
|
|
27
|
+
"--json": "json",
|
|
28
|
+
"--ledger": "ledger"
|
|
25
29
|
};
|
|
26
30
|
function parseArgs(argv) {
|
|
27
31
|
const out = { _: [], flags: {}, command: "install" };
|
|
@@ -130,7 +134,7 @@ function copyScripts(dstDir) {
|
|
|
130
134
|
if (!existsSync(src))
|
|
131
135
|
return;
|
|
132
136
|
for (const f of readdirSync(src)) {
|
|
133
|
-
if (!f.endsWith(".sh")
|
|
137
|
+
if (!f.endsWith(".sh") || f === "install.sh")
|
|
134
138
|
continue;
|
|
135
139
|
const to = join(dst, f);
|
|
136
140
|
mkdirSync(dst, { recursive: true });
|
|
@@ -619,50 +623,118 @@ var targets = { claude: target, opencode: target2, copilot: target3, gemini: tar
|
|
|
619
623
|
var TARGET_IDS = Object.keys(targets);
|
|
620
624
|
|
|
621
625
|
// src/installer.ts
|
|
622
|
-
import { existsSync as
|
|
623
|
-
import { dirname as dirname3, join as
|
|
624
|
-
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";
|
|
625
629
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
626
|
-
|
|
627
|
-
|
|
630
|
+
|
|
631
|
+
// src/config.ts
|
|
632
|
+
import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
633
|
+
import { homedir } from "node:os";
|
|
634
|
+
import { join as join5 } from "node:path";
|
|
635
|
+
var DEFAULT_CONFIG = [
|
|
636
|
+
"mode=guided",
|
|
637
|
+
"branch=feature/{type}-{issue}-{slug}",
|
|
638
|
+
"commit=conventional",
|
|
639
|
+
"auto_commit=on",
|
|
640
|
+
"coverage_new=85",
|
|
641
|
+
"coverage_modified=90",
|
|
642
|
+
"review_depth=full",
|
|
643
|
+
"quality_depth=full",
|
|
644
|
+
"verify_merged=off",
|
|
645
|
+
"delegate_threshold=60",
|
|
646
|
+
"heal_max_cycles=3",
|
|
647
|
+
"verbosity=normal",
|
|
648
|
+
"# context_budget_chars=150000 # optional: fail archive if trail exceeds this (measured in report Cost section)",
|
|
649
|
+
"# investigation_max_passes=2 # optional: cap investigation passes (spec §13)",
|
|
650
|
+
"# investigation_max_unrelated_files=5",
|
|
651
|
+
"# investigation_repeated_read_threshold=2",
|
|
652
|
+
"# sign=auto # optional: auto | minisign | pure | off — report attestation",
|
|
653
|
+
"# enforce=block # optional: off | warn | block — pipeline-guard policy"
|
|
654
|
+
].join(`
|
|
655
|
+
`) + `
|
|
656
|
+
`;
|
|
657
|
+
function configPaths(projectDir) {
|
|
658
|
+
return [join5(projectDir, ".mugiwara", "config"), join5(homedir(), ".mugiwara", "config")];
|
|
659
|
+
}
|
|
660
|
+
function readConfig(projectDir) {
|
|
661
|
+
const out = {};
|
|
662
|
+
for (const file of configPaths(projectDir)) {
|
|
663
|
+
if (!existsSync4(file))
|
|
664
|
+
continue;
|
|
665
|
+
for (const line of readFileSync3(file, "utf8").split(/\r?\n/)) {
|
|
666
|
+
const t = line.trim();
|
|
667
|
+
if (!t || t.startsWith("#"))
|
|
668
|
+
continue;
|
|
669
|
+
const eq = t.indexOf("=");
|
|
670
|
+
if (eq === -1)
|
|
671
|
+
continue;
|
|
672
|
+
const key = t.slice(0, eq).trim();
|
|
673
|
+
if (!key)
|
|
674
|
+
continue;
|
|
675
|
+
if (key in out)
|
|
676
|
+
continue;
|
|
677
|
+
out[key] = t.slice(eq + 1).trim();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return out;
|
|
681
|
+
}
|
|
682
|
+
function ensureConfig(projectDir) {
|
|
683
|
+
const file = join5(projectDir, ".mugiwara", "config");
|
|
684
|
+
let exists = false;
|
|
685
|
+
try {
|
|
686
|
+
exists = lstatSync(file).isFile() || lstatSync(file).isSymbolicLink();
|
|
687
|
+
} catch {
|
|
688
|
+
exists = false;
|
|
689
|
+
}
|
|
690
|
+
if (exists)
|
|
691
|
+
return false;
|
|
692
|
+
mkdirSync3(join5(projectDir, ".mugiwara"), { recursive: true });
|
|
693
|
+
writeFileSync3(file, DEFAULT_CONFIG);
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/installer.ts
|
|
698
|
+
var CONTENT_DIR = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "content");
|
|
699
|
+
var pkg = JSON.parse(readFileSync4(join6(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf8"));
|
|
628
700
|
var VERSION = pkg.version;
|
|
629
701
|
function collectContent() {
|
|
630
|
-
const skillNames = readdirSync3(
|
|
702
|
+
const skillNames = readdirSync3(join6(CONTENT_DIR, "skills"), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
631
703
|
const skills = skillNames.map((name) => {
|
|
632
|
-
const { data, body } = parseFrontmatter(
|
|
633
|
-
return { name, data, body, refs: collectRefs(
|
|
704
|
+
const { data, body } = parseFrontmatter(readFileSync4(join6(CONTENT_DIR, "skills", name, "SKILL.md"), "utf8"));
|
|
705
|
+
return { name, data, body, refs: collectRefs(join6(CONTENT_DIR, "skills", name)) };
|
|
634
706
|
});
|
|
635
|
-
const agents = readdirSync3(
|
|
636
|
-
const { data, body } = parseFrontmatter(
|
|
707
|
+
const agents = readdirSync3(join6(CONTENT_DIR, "agents")).filter((f) => f.endsWith(".md")).map((f) => {
|
|
708
|
+
const { data, body } = parseFrontmatter(readFileSync4(join6(CONTENT_DIR, "agents", f), "utf8"));
|
|
637
709
|
return { name: f.replace(/\.md$/, ""), data, body, refs: [], internal: data.internal === "true" };
|
|
638
710
|
});
|
|
639
|
-
const sharedRefsDir =
|
|
711
|
+
const sharedRefsDir = join6(dirname3(CONTENT_DIR), "references");
|
|
640
712
|
const sharedRefs = [];
|
|
641
|
-
if (
|
|
713
|
+
if (existsSync5(sharedRefsDir)) {
|
|
642
714
|
for (const f of readdirSync3(sharedRefsDir)) {
|
|
643
715
|
if (!f.endsWith(".md"))
|
|
644
716
|
continue;
|
|
645
|
-
sharedRefs.push({ relPath: f, text:
|
|
717
|
+
sharedRefs.push({ relPath: f, text: readFileSync4(join6(sharedRefsDir, f), "utf8") });
|
|
646
718
|
}
|
|
647
719
|
}
|
|
648
720
|
return { skills, agents, sharedRefs };
|
|
649
721
|
}
|
|
650
722
|
function collectRefs(skillDir) {
|
|
651
|
-
const refsDir =
|
|
652
|
-
if (!
|
|
723
|
+
const refsDir = join6(skillDir, "references");
|
|
724
|
+
if (!existsSync5(refsDir))
|
|
653
725
|
return [];
|
|
654
|
-
return readdirSync3(refsDir, { recursive: true }).map((f) => String(f)).filter((f) => f.endsWith(".md")).map((rel) => ({ relPath: rel, text:
|
|
726
|
+
return readdirSync3(refsDir, { recursive: true }).map((f) => String(f)).filter((f) => f.endsWith(".md")).map((rel) => ({ relPath: rel, text: readFileSync4(join6(refsDir, rel), "utf8") }));
|
|
655
727
|
}
|
|
656
728
|
function installTo(target10, opts) {
|
|
657
729
|
const { scope, projectDir, dryRun = false, force = false } = opts;
|
|
658
|
-
const home = opts.home ??
|
|
730
|
+
const home = opts.home ?? homedir2();
|
|
659
731
|
const { skills, agents, sharedRefs } = collectContent();
|
|
660
732
|
const dirs = target10.paths({ scope, projectDir, home });
|
|
661
|
-
const backupRoot =
|
|
733
|
+
const backupRoot = join6(scope === "global" ? home : projectDir, ".mugiwara");
|
|
662
734
|
const result = { written: [], skipped: [], backedUp: [], notes: [] };
|
|
663
735
|
const writeOne = (absPath, text) => {
|
|
664
|
-
if (
|
|
665
|
-
if (
|
|
736
|
+
if (existsSync5(absPath)) {
|
|
737
|
+
if (readFileSync4(absPath, "utf8") === text) {
|
|
666
738
|
result.skipped.push(absPath);
|
|
667
739
|
return;
|
|
668
740
|
}
|
|
@@ -673,17 +745,17 @@ function installTo(target10, opts) {
|
|
|
673
745
|
}
|
|
674
746
|
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
675
747
|
const fileName = absPath.replace(/[^a-zA-Z0-9]+/g, "_");
|
|
676
|
-
const backupDir =
|
|
677
|
-
const backupFile =
|
|
748
|
+
const backupDir = join6(backupRoot, "backup", `${ts}-${target10.id}`);
|
|
749
|
+
const backupFile = join6(backupDir, fileName);
|
|
678
750
|
if (!dryRun) {
|
|
679
|
-
|
|
751
|
+
mkdirSync4(backupDir, { recursive: true });
|
|
680
752
|
copyFileSync3(absPath, backupFile);
|
|
681
753
|
}
|
|
682
754
|
result.backedUp.push(absPath);
|
|
683
755
|
}
|
|
684
756
|
if (!dryRun) {
|
|
685
|
-
|
|
686
|
-
|
|
757
|
+
mkdirSync4(dirname3(absPath), { recursive: true });
|
|
758
|
+
writeFileSync4(absPath, text);
|
|
687
759
|
}
|
|
688
760
|
result.written.push(absPath);
|
|
689
761
|
};
|
|
@@ -694,7 +766,7 @@ function installTo(target10, opts) {
|
|
|
694
766
|
if (target10.refPointerPrefix !== undefined && target10.refPointerPrefix !== "") {
|
|
695
767
|
text = text.replace(/`_shared\/references\//g, "`" + target10.refPointerPrefix + "_shared/references/");
|
|
696
768
|
}
|
|
697
|
-
writeOne(
|
|
769
|
+
writeOne(join6(dirs.skillsDir, out.relPath), text);
|
|
698
770
|
}
|
|
699
771
|
if (target10.transformSkillFull) {
|
|
700
772
|
const full = target10.transformSkillFull(s.data, s.body);
|
|
@@ -703,29 +775,29 @@ function installTo(target10, opts) {
|
|
|
703
775
|
if (target10.refPointerPrefix !== undefined && target10.refPointerPrefix !== "") {
|
|
704
776
|
text = text.replace(/`_shared\/references\//g, "`" + target10.refPointerPrefix + "_shared/references/");
|
|
705
777
|
}
|
|
706
|
-
writeOne(
|
|
778
|
+
writeOne(join6(target10.refsDir({ scope, projectDir, home }, s.name), full.relPath), text);
|
|
707
779
|
}
|
|
708
780
|
}
|
|
709
781
|
if (s.refs.length && target10.refsDir) {
|
|
710
782
|
const refsRoot = target10.refsDir({ scope, projectDir, home }, s.name);
|
|
711
783
|
for (const r of s.refs)
|
|
712
|
-
writeOne(
|
|
784
|
+
writeOne(join6(refsRoot, r.relPath), r.text);
|
|
713
785
|
}
|
|
714
786
|
}
|
|
715
787
|
for (const a of agents) {
|
|
716
788
|
const out = target10.transformAgent({ ...a.data, ...a.internal ? { "internal-agent": "true" } : {} }, a.body);
|
|
717
789
|
if (out)
|
|
718
|
-
writeOne(
|
|
790
|
+
writeOne(join6(dirs.agentsDir, out.relPath), out.text);
|
|
719
791
|
if (target10.transformAgentFull) {
|
|
720
792
|
const full = target10.transformAgentFull(a.data, a.body);
|
|
721
793
|
if (full && target10.refsDir)
|
|
722
|
-
writeOne(
|
|
794
|
+
writeOne(join6(target10.refsDir({ scope, projectDir, home }, a.name), full.relPath), full.text);
|
|
723
795
|
}
|
|
724
796
|
}
|
|
725
797
|
if (sharedRefs.length) {
|
|
726
|
-
const sharedRoot =
|
|
798
|
+
const sharedRoot = join6(dirs.skillsDir, "_shared", "references");
|
|
727
799
|
for (const r of sharedRefs)
|
|
728
|
-
writeOne(
|
|
800
|
+
writeOne(join6(sharedRoot, r.relPath), r.text);
|
|
729
801
|
}
|
|
730
802
|
if (target10.postInstall) {
|
|
731
803
|
const post = target10.postInstall({ scope, projectDir, home, dryRun, files: result.written });
|
|
@@ -733,35 +805,20 @@ function installTo(target10, opts) {
|
|
|
733
805
|
result.notes.push(...post.notes);
|
|
734
806
|
}
|
|
735
807
|
if (scope === "project") {
|
|
736
|
-
const configPath =
|
|
808
|
+
const configPath = join6(projectDir, ".mugiwara", "config");
|
|
737
809
|
let configExists = false;
|
|
738
810
|
try {
|
|
739
|
-
configExists =
|
|
811
|
+
configExists = lstatSync2(configPath).isFile() || lstatSync2(configPath).isSymbolicLink();
|
|
740
812
|
} catch {
|
|
741
813
|
configExists = false;
|
|
742
814
|
}
|
|
743
815
|
if (!configExists) {
|
|
744
|
-
const body = [
|
|
745
|
-
"mode=guided",
|
|
746
|
-
"branch=feature/{type}-{issue}-{slug}",
|
|
747
|
-
"commit=conventional",
|
|
748
|
-
"auto_commit=on",
|
|
749
|
-
"coverage_new=90",
|
|
750
|
-
"coverage_modified=80",
|
|
751
|
-
"review_depth=full",
|
|
752
|
-
"quality_depth=full",
|
|
753
|
-
"delegate_threshold=60",
|
|
754
|
-
"heal_max_cycles=3",
|
|
755
|
-
"verbosity=normal"
|
|
756
|
-
].join(`
|
|
757
|
-
`) + `
|
|
758
|
-
`;
|
|
759
816
|
if (!dryRun) {
|
|
760
|
-
|
|
761
|
-
|
|
817
|
+
mkdirSync4(dirname3(configPath), { recursive: true });
|
|
818
|
+
writeFileSync4(configPath, DEFAULT_CONFIG);
|
|
762
819
|
}
|
|
763
820
|
result.written.push(configPath);
|
|
764
|
-
result.notes.push(`default config written: ${configPath} (
|
|
821
|
+
result.notes.push(`default config written: ${configPath} (edit it to customise)`);
|
|
765
822
|
}
|
|
766
823
|
}
|
|
767
824
|
return result;
|
|
@@ -769,7 +826,7 @@ function installTo(target10, opts) {
|
|
|
769
826
|
function removeInstalled(manifest, { dryRun = false } = {}) {
|
|
770
827
|
const removed = [];
|
|
771
828
|
for (const f of manifest.files) {
|
|
772
|
-
if (
|
|
829
|
+
if (existsSync5(f)) {
|
|
773
830
|
if (!dryRun)
|
|
774
831
|
rmSync(f);
|
|
775
832
|
removed.push(f);
|
|
@@ -778,7 +835,7 @@ function removeInstalled(manifest, { dryRun = false } = {}) {
|
|
|
778
835
|
if (!dryRun) {
|
|
779
836
|
for (const f of manifest.files) {
|
|
780
837
|
let d = dirname3(f);
|
|
781
|
-
while (
|
|
838
|
+
while (existsSync5(d) && readdirSync3(d).length === 0) {
|
|
782
839
|
if (d.endsWith("/.mugiwara") || d === dirname3(d))
|
|
783
840
|
break;
|
|
784
841
|
rmSync(d, { recursive: true, force: true });
|
|
@@ -789,10 +846,10 @@ function removeInstalled(manifest, { dryRun = false } = {}) {
|
|
|
789
846
|
return removed;
|
|
790
847
|
}
|
|
791
848
|
function assertNotSymlink(file) {
|
|
792
|
-
if (!
|
|
849
|
+
if (!existsSync5(file))
|
|
793
850
|
return;
|
|
794
851
|
try {
|
|
795
|
-
if (
|
|
852
|
+
if (lstatSync2(file).isSymbolicLink())
|
|
796
853
|
throw new Error(`refusing to follow symlink: ${file}`);
|
|
797
854
|
} catch (e) {
|
|
798
855
|
if (e.code === "ENOENT")
|
|
@@ -802,45 +859,45 @@ function assertNotSymlink(file) {
|
|
|
802
859
|
}
|
|
803
860
|
var GITIGNORE_BLOCK_START = "# >>> mugiwara >>>";
|
|
804
861
|
var GITIGNORE_BLOCK_END = "# <<< mugiwara <<<";
|
|
805
|
-
var
|
|
806
|
-
var GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit
|
|
807
|
-
# Ignore session state
|
|
808
|
-
.mugiwara/
|
|
809
|
-
.mugiwara/
|
|
862
|
+
var GITIGNORE_LEGACY = [".mugiwara/state/", "# mugiwara"];
|
|
863
|
+
var GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit plan.md, flows/, review.md,
|
|
864
|
+
# decisions.md, blockers.md, report.md under .mugiwara/missions/. Ignore session state.
|
|
865
|
+
.mugiwara/missions/**/*.json
|
|
866
|
+
.mugiwara/index.md
|
|
810
867
|
.mugiwara/config
|
|
811
868
|
.mugiwara/refs/
|
|
812
869
|
# <<< mugiwara <<<
|
|
813
870
|
`;
|
|
814
871
|
function ensureProjectGitignore(projectDir, opts = {}) {
|
|
815
872
|
const { dryRun = false } = opts;
|
|
816
|
-
const path =
|
|
873
|
+
const path = join6(projectDir, ".gitignore");
|
|
817
874
|
assertNotSymlink(path);
|
|
818
|
-
let existing =
|
|
875
|
+
let existing = existsSync5(path) ? readFileSync4(path, "utf8") : "";
|
|
819
876
|
if (existing) {
|
|
820
|
-
if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes(".mugiwara/
|
|
877
|
+
if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes(".mugiwara/missions/**/*.json")) {
|
|
821
878
|
return { appended: false, notes: [] };
|
|
822
879
|
}
|
|
823
|
-
const hadOld = existing.includes(
|
|
880
|
+
const hadOld = GITIGNORE_LEGACY.some((m) => existing.includes(m)) || existing.includes(GITIGNORE_BLOCK_START);
|
|
824
881
|
if (hadOld) {
|
|
825
882
|
const clean = removeProjectGitignore(projectDir, { dryRun });
|
|
826
|
-
existing = clean.removed ?
|
|
883
|
+
existing = clean.removed ? existsSync5(path) ? readFileSync4(path, "utf8") : "" : existing;
|
|
827
884
|
}
|
|
828
885
|
}
|
|
829
886
|
const separator = existing.length && !existing.endsWith(`
|
|
830
887
|
`) ? `
|
|
831
888
|
` : "";
|
|
832
889
|
if (!dryRun) {
|
|
833
|
-
|
|
834
|
-
|
|
890
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
891
|
+
writeFileSync4(path, existing + separator + GITIGNORE_BLOCK);
|
|
835
892
|
}
|
|
836
893
|
return { appended: true, notes: [`.gitignore ${dryRun ? "would upgrade+append" : "upgraded"} mugiwara audit-trail block`] };
|
|
837
894
|
}
|
|
838
895
|
function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
|
|
839
|
-
const path =
|
|
896
|
+
const path = join6(projectDir, ".gitignore");
|
|
840
897
|
assertNotSymlink(path);
|
|
841
|
-
if (!
|
|
898
|
+
if (!existsSync5(path))
|
|
842
899
|
return { removed: false, notes: [] };
|
|
843
|
-
const current =
|
|
900
|
+
const current = readFileSync4(path, "utf8");
|
|
844
901
|
const delimited = current.includes(GITIGNORE_BLOCK_START);
|
|
845
902
|
let cleaned;
|
|
846
903
|
if (delimited) {
|
|
@@ -849,18 +906,20 @@ function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
|
|
|
849
906
|
if (end < start)
|
|
850
907
|
return { removed: false, notes: ["delimiter mismatch — .gitignore left untouched"] };
|
|
851
908
|
cleaned = current.slice(0, start) + current.slice(end + GITIGNORE_BLOCK_END.length);
|
|
852
|
-
} else if (current.includes(
|
|
909
|
+
} else if (current.includes("# mugiwara")) {
|
|
853
910
|
const LEGACY_LINES = new Set([
|
|
854
911
|
".mugiwara/state.json",
|
|
855
912
|
".mugiwara/state-*.json",
|
|
856
913
|
".mugiwara/config",
|
|
914
|
+
".mugiwara/state/",
|
|
915
|
+
".mugiwara/continue/",
|
|
857
916
|
".mugiwara/continue.md",
|
|
858
917
|
".mugiwara/refs/"
|
|
859
918
|
]);
|
|
860
919
|
const lines = current.split(`
|
|
861
920
|
`).filter((l) => {
|
|
862
921
|
const t = l.trim();
|
|
863
|
-
if (t.startsWith(
|
|
922
|
+
if (t.startsWith("# mugiwara"))
|
|
864
923
|
return false;
|
|
865
924
|
return !LEGACY_LINES.has(t);
|
|
866
925
|
});
|
|
@@ -878,39 +937,746 @@ function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
|
|
|
878
937
|
if (cleaned === current)
|
|
879
938
|
return { removed: false, notes: [] };
|
|
880
939
|
if (!dryRun)
|
|
881
|
-
|
|
940
|
+
writeFileSync4(path, cleaned);
|
|
882
941
|
return { removed: true, notes: [`.gitignore ${dryRun ? "would strip" : "stripped"} mugiwara block`] };
|
|
883
942
|
}
|
|
884
943
|
|
|
885
944
|
// src/manifest.ts
|
|
886
|
-
import { existsSync as
|
|
887
|
-
import { dirname as dirname4, join as
|
|
945
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
946
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
888
947
|
function manifestPath({ scope, projectDir, home }) {
|
|
889
|
-
return scope === "global" ?
|
|
948
|
+
return scope === "global" ? join7(home, ".mugiwara", "manifest.json") : join7(projectDir, ".mugiwara", "manifest.json");
|
|
890
949
|
}
|
|
891
950
|
function readManifest(file) {
|
|
892
|
-
return
|
|
951
|
+
return existsSync6(file) ? JSON.parse(readFileSync5(file, "utf8")) : null;
|
|
893
952
|
}
|
|
894
953
|
function writeManifest(file, data) {
|
|
895
|
-
|
|
896
|
-
|
|
954
|
+
mkdirSync5(dirname4(file), { recursive: true });
|
|
955
|
+
writeFileSync5(file, JSON.stringify(data, null, 2) + `
|
|
897
956
|
`);
|
|
898
957
|
}
|
|
899
958
|
|
|
900
959
|
// src/mission.ts
|
|
901
|
-
import { existsSync as
|
|
902
|
-
import {
|
|
960
|
+
import { existsSync as existsSync11, rmSync as rmSync2, readFileSync as readFileSync11, readdirSync as readdirSync7, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8, renameSync, openSync, writeSync, closeSync } from "node:fs";
|
|
961
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
962
|
+
import { join as join16 } from "node:path";
|
|
963
|
+
|
|
964
|
+
// src/integrity.ts
|
|
965
|
+
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
|
|
966
|
+
import { isAbsolute, join as join8, relative } from "node:path";
|
|
967
|
+
var SECRET_PATTERNS = [
|
|
968
|
+
[/AKIA[0-9A-Z]{16}/, "AWS access key id"],
|
|
969
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "private key block"],
|
|
970
|
+
[/gh[pousr]_[A-Za-z0-9]{20,}/, "GitHub token"],
|
|
971
|
+
[/xox[baprs]-[A-Za-z0-9-]{10,}/, "Slack token"],
|
|
972
|
+
[/sk-[A-Za-z0-9]{32,}/, "API key (sk-…)"],
|
|
973
|
+
[/eyJhbGciOi[A-Za-z0-9_.-]{20,}/, "JWT pasted verbatim"],
|
|
974
|
+
[/(api[_-]?key|secret|passwd|password)\s*[=:]\s*["'][^"'\s]{8,}["']/i, "credential assignment"]
|
|
975
|
+
];
|
|
976
|
+
var ALLOW_SECRET = "mugiwara:allow-secret";
|
|
977
|
+
function findSecrets(body) {
|
|
978
|
+
const out = [];
|
|
979
|
+
for (const line of body.split(/\r?\n/)) {
|
|
980
|
+
if (line.includes(ALLOW_SECRET))
|
|
981
|
+
continue;
|
|
982
|
+
for (const [re, label] of SECRET_PATTERNS) {
|
|
983
|
+
const hit = line.match(re);
|
|
984
|
+
if (hit)
|
|
985
|
+
out.push({ label, hit: hit[0] });
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return out;
|
|
989
|
+
}
|
|
990
|
+
var TRAIL_EXTS = new Set([".md", ".json", ".sh", ".jsonl"]);
|
|
991
|
+
function trailFiles(dir) {
|
|
992
|
+
const out = [];
|
|
993
|
+
const walk = (d) => {
|
|
994
|
+
for (const e of readdirSync4(d, { withFileTypes: true })) {
|
|
995
|
+
const p = join8(d, e.name);
|
|
996
|
+
if (e.isDirectory())
|
|
997
|
+
walk(p);
|
|
998
|
+
else if (TRAIL_EXTS.has(e.name.slice(e.name.lastIndexOf(".")) || ""))
|
|
999
|
+
out.push(p);
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
walk(dir);
|
|
1003
|
+
return out;
|
|
1004
|
+
}
|
|
1005
|
+
function linkedPaths(md) {
|
|
1006
|
+
const out = [];
|
|
1007
|
+
for (const m of md.matchAll(/\]\(([^)\s]+)\)/g)) {
|
|
1008
|
+
const t = m[1];
|
|
1009
|
+
if (/^(https?:|mailto:|#|\/\/)/.test(t))
|
|
1010
|
+
continue;
|
|
1011
|
+
out.push(t.split("#")[0]);
|
|
1012
|
+
}
|
|
1013
|
+
return out.filter(Boolean);
|
|
1014
|
+
}
|
|
1015
|
+
function hasCommandOutputShape(body) {
|
|
1016
|
+
return /`[^`]+`/.test(body) || /\b(exit\s+[01]|✓|✗|\bPASS\b|\bFAIL\b|passed|failed|\d+\s+(passed|failed))\b/i.test(body);
|
|
1017
|
+
}
|
|
1018
|
+
function collectPassCitedPaths(missionDir) {
|
|
1019
|
+
const out = [];
|
|
1020
|
+
for (const f of trailFiles(missionDir)) {
|
|
1021
|
+
let body;
|
|
1022
|
+
try {
|
|
1023
|
+
body = readFileSync6(f, "utf8");
|
|
1024
|
+
} catch {
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
for (const line of body.split(/\r?\n/)) {
|
|
1028
|
+
if (!/\bPASS\b/.test(line))
|
|
1029
|
+
continue;
|
|
1030
|
+
for (const p of linkedPaths(line))
|
|
1031
|
+
out.push(p);
|
|
1032
|
+
for (const m of line.matchAll(/(?:^|[\s"'(])([a-zA-Z0-9._\/-]+\.(?:md|txt|log|json))\b/g)) {
|
|
1033
|
+
const cand = m[1];
|
|
1034
|
+
if (cand.includes("/"))
|
|
1035
|
+
out.push(cand);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return [...new Set(out)];
|
|
1040
|
+
}
|
|
1041
|
+
function checkTrail(missionDir, projectRoot) {
|
|
1042
|
+
const issues = [];
|
|
1043
|
+
const files = trailFiles(missionDir);
|
|
1044
|
+
for (const f of files) {
|
|
1045
|
+
let body;
|
|
1046
|
+
try {
|
|
1047
|
+
body = readFileSync6(f, "utf8");
|
|
1048
|
+
} catch {
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
for (const target10 of linkedPaths(body)) {
|
|
1052
|
+
if (isAbsolute(target10))
|
|
1053
|
+
continue;
|
|
1054
|
+
const fromMission = join8(missionDir, target10);
|
|
1055
|
+
const fromRoot = join8(projectRoot, target10);
|
|
1056
|
+
if (!existsSync7(fromMission) && !existsSync7(fromRoot)) {
|
|
1057
|
+
issues.push({
|
|
1058
|
+
kind: "dangling-path",
|
|
1059
|
+
detail: `${relative(projectRoot, f)} links "${target10}" — no such file (mission dir or repo root)`
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
for (const { label, hit } of findSecrets(body)) {
|
|
1064
|
+
issues.push({
|
|
1065
|
+
kind: "secret",
|
|
1066
|
+
detail: `${relative(projectRoot, f)} matches ${label}: ${hit.slice(0, 12)}…`
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const evidencePaths = [];
|
|
1071
|
+
const evidenceFile = join8(missionDir, "state.json");
|
|
1072
|
+
if (existsSync7(evidenceFile)) {
|
|
1073
|
+
try {
|
|
1074
|
+
const s = JSON.parse(readFileSync6(evidenceFile, "utf8"));
|
|
1075
|
+
if (Array.isArray(s.evidence)) {
|
|
1076
|
+
for (const e of s.evidence) {
|
|
1077
|
+
if (typeof e !== "string" || !e.trim())
|
|
1078
|
+
continue;
|
|
1079
|
+
evidencePaths.push(e);
|
|
1080
|
+
const cand = join8(projectRoot, e);
|
|
1081
|
+
if (!isAbsolute(e) && !existsSync7(cand) && !existsSync7(join8(missionDir, e))) {
|
|
1082
|
+
issues.push({ kind: "evidence", detail: `state.json evidence "${e}" does not exist` });
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
} catch {}
|
|
1087
|
+
}
|
|
1088
|
+
const passCited = collectPassCitedPaths(missionDir);
|
|
1089
|
+
for (const e of passCited) {
|
|
1090
|
+
if (!e.trim() || isAbsolute(e))
|
|
1091
|
+
continue;
|
|
1092
|
+
const candMission = join8(missionDir, e);
|
|
1093
|
+
const candRoot = join8(projectRoot, e);
|
|
1094
|
+
const resolved = existsSync7(candMission) ? candMission : existsSync7(candRoot) ? candRoot : null;
|
|
1095
|
+
if (!resolved)
|
|
1096
|
+
continue;
|
|
1097
|
+
let body;
|
|
1098
|
+
try {
|
|
1099
|
+
body = readFileSync6(resolved, "utf8");
|
|
1100
|
+
} catch {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
if (!hasCommandOutputShape(body)) {
|
|
1104
|
+
issues.push({ kind: "evidence-thin", detail: `evidence "${e}" exists but lacks command output (no backticked command or exit-status token)` });
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return issues;
|
|
1108
|
+
}
|
|
1109
|
+
function formatIssues(issues) {
|
|
1110
|
+
return issues.map((i) => ` ✗ [${i.kind}] ${i.detail}`).join(`
|
|
1111
|
+
`);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// src/check-artifacts.ts
|
|
1115
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
|
|
1116
|
+
import { join as join9 } from "node:path";
|
|
1117
|
+
var LANE_MIN = new Set(["standard", "full", "spike"]);
|
|
1118
|
+
function checkMissionArtifacts(missionDir) {
|
|
1119
|
+
const statePath = join9(missionDir, "state.json");
|
|
1120
|
+
if (!existsSync8(statePath)) {
|
|
1121
|
+
return { ok: true, missing: [], lane: null };
|
|
1122
|
+
}
|
|
1123
|
+
let lane = "unknown";
|
|
1124
|
+
try {
|
|
1125
|
+
const s = JSON.parse(readFileSyncSafe(statePath));
|
|
1126
|
+
if (typeof s.lane === "string")
|
|
1127
|
+
lane = s.lane;
|
|
1128
|
+
} catch {}
|
|
1129
|
+
if (!LANE_MIN.has(lane)) {
|
|
1130
|
+
return { ok: true, missing: [], lane };
|
|
1131
|
+
}
|
|
1132
|
+
const missing = [];
|
|
1133
|
+
if (!existsSync8(join9(missionDir, "plan.md")))
|
|
1134
|
+
missing.push("plan.md");
|
|
1135
|
+
const flowsDir = join9(missionDir, "flows");
|
|
1136
|
+
const hasFlows = existsSync8(flowsDir) && readdirSync5(flowsDir).length > 0;
|
|
1137
|
+
if (!hasFlows)
|
|
1138
|
+
missing.push("flows/ (no execution evidence)");
|
|
1139
|
+
return { ok: missing.length === 0, missing, lane };
|
|
1140
|
+
}
|
|
1141
|
+
function readFileSyncSafe(p) {
|
|
1142
|
+
try {
|
|
1143
|
+
return readFileSync7(p, "utf8");
|
|
1144
|
+
} catch {
|
|
1145
|
+
return "";
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// src/rollback.ts
|
|
1150
|
+
import { execFileSync } from "node:child_process";
|
|
1151
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
1152
|
+
import { join as join10 } from "node:path";
|
|
1153
|
+
function git(cwd, args) {
|
|
1154
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1155
|
+
}
|
|
1156
|
+
function buildRollback(input, commitsNewestFirst, filesTouched) {
|
|
1157
|
+
const lines = [
|
|
1158
|
+
"#!/usr/bin/env bash",
|
|
1159
|
+
`# Rollback map for mission "${input.mission}" — generated at closure.`,
|
|
1160
|
+
"# Human-executed. Review before running; mugiwara never runs this.",
|
|
1161
|
+
`# Branch: ${input.branch}`,
|
|
1162
|
+
`# Base: ${input.baseSha}`,
|
|
1163
|
+
"",
|
|
1164
|
+
"set -euo pipefail",
|
|
1165
|
+
""
|
|
1166
|
+
];
|
|
1167
|
+
if (!commitsNewestFirst.length && !filesTouched.length) {
|
|
1168
|
+
lines.push("# No commits between base and HEAD on this branch — nothing to revert.", "");
|
|
1169
|
+
return lines.join(`
|
|
1170
|
+
`);
|
|
1171
|
+
}
|
|
1172
|
+
if (!commitsNewestFirst.length) {
|
|
1173
|
+
lines.push("# UNRESOLVED: squash-merged state detected.", `# git rev-list ${input.baseSha}..${input.branch} is empty, but the diff`, `# ${input.baseSha}..${input.branch} touches ${filesTouched.length} file(s):`, "# the mission's changes were collapsed into commit(s) already on the base", "# ref. No per-commit revert list can be derived automatically.", "# Locate the squash commit, review it, then revert it:", `# git log --oneline ${input.baseSha}..HEAD --grep="${input.mission}"`, "# git revert <squash-commit>", "# Files carrying the squashed changes (verify each after reverting):", ...filesTouched.map((f) => `# ${f}`), "", 'echo "ROLLBACK INCOMPLETE: squash-merged state — locate and revert the squash commit manually" >&2', "exit 1", "");
|
|
1174
|
+
return lines.join(`
|
|
1175
|
+
`);
|
|
1176
|
+
}
|
|
1177
|
+
lines.push("# Revert newest-first so earlier reverts never conflict with later ones.", "git revert --no-edit \\", commitsNewestFirst.map((c) => ` ${c}`).join(" \\\n"), "");
|
|
1178
|
+
if (filesTouched.length) {
|
|
1179
|
+
lines.push("# Files this mission touched (verify the working tree is clean afterwards):", ...filesTouched.map((f) => `# ${f}`), "");
|
|
1180
|
+
}
|
|
1181
|
+
return lines.join(`
|
|
1182
|
+
`);
|
|
1183
|
+
}
|
|
1184
|
+
function generateRollback(projectDir, missionDir, input) {
|
|
1185
|
+
try {
|
|
1186
|
+
const range = `${input.baseSha}..${input.branch}`;
|
|
1187
|
+
const revList = git(projectDir, ["rev-list", "--reverse", range]).split(/\r?\n/).filter(Boolean);
|
|
1188
|
+
const commitsNewestFirst = [...revList].reverse();
|
|
1189
|
+
const filesTouched = input.baseSha === "unknown" ? [] : git(projectDir, ["diff", "--name-only", input.baseSha, input.branch]).split(/\r?\n/).filter(Boolean);
|
|
1190
|
+
const body = buildRollback(input, commitsNewestFirst, filesTouched);
|
|
1191
|
+
const file = join10(missionDir, "rollback.sh");
|
|
1192
|
+
writeFileSync6(file, body, { mode: 493 });
|
|
1193
|
+
return { file: "rollback.sh", commits: commitsNewestFirst.length };
|
|
1194
|
+
} catch {
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// src/provenance.ts
|
|
1200
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1201
|
+
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
1202
|
+
import { join as join11 } from "node:path";
|
|
1203
|
+
function git2(cwd, args) {
|
|
1204
|
+
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1205
|
+
}
|
|
1206
|
+
function modelLabel() {
|
|
1207
|
+
return process.env.MUGIWARA_MODEL?.trim() || process.env.ANTHROPIC_MODEL?.trim() || "model-unrecorded (set MUGIWARA_MODEL to attribute)";
|
|
1208
|
+
}
|
|
1209
|
+
function buildNote(s) {
|
|
1210
|
+
const gates = s.evidence.length ? s.evidence.join(" · ") : "no evidence recorded";
|
|
1211
|
+
const uniqModels = [...new Set((s.models ?? []).filter(Boolean))];
|
|
1212
|
+
const modelPart = uniqModels.length ? `model(s): ${uniqModels.join(", ")}` : s.model ?? modelLabel();
|
|
1213
|
+
return [
|
|
1214
|
+
`mission: ${s.mission}`,
|
|
1215
|
+
`agent: ${s.actor || "unknown"} · ${modelPart} · lane ${s.lane} · mode ${s.mode}`,
|
|
1216
|
+
`tasks: ${s.tasks_done}/${s.tasks_total}`,
|
|
1217
|
+
`gates/evidence: ${gates}`,
|
|
1218
|
+
`branch: ${s.branch}`,
|
|
1219
|
+
"human review: pending (PR review is the terminal gate)"
|
|
1220
|
+
].join(`
|
|
1221
|
+
`);
|
|
1222
|
+
}
|
|
1223
|
+
function renderProvenanceMd(note, sha) {
|
|
1224
|
+
const lines = [
|
|
1225
|
+
"# Provenance",
|
|
1226
|
+
"",
|
|
1227
|
+
"<!-- paste below into the PR description or a PR comment -->",
|
|
1228
|
+
"",
|
|
1229
|
+
"```",
|
|
1230
|
+
note,
|
|
1231
|
+
"```",
|
|
1232
|
+
"",
|
|
1233
|
+
sha ? `Commit: ${sha}` : "Commit: not recorded (no git head resolved at closure)",
|
|
1234
|
+
"",
|
|
1235
|
+
"Query locally after pushing notes:",
|
|
1236
|
+
"`git fetch origin refs/notes/mugiwara:refs/notes/mugiwara` then `mugiwara blame <path>`."
|
|
1237
|
+
];
|
|
1238
|
+
return lines.join(`
|
|
1239
|
+
`) + `
|
|
1240
|
+
`;
|
|
1241
|
+
}
|
|
1242
|
+
function attachGitNote(projectDir, branch, note) {
|
|
1243
|
+
try {
|
|
1244
|
+
let sha;
|
|
1245
|
+
try {
|
|
1246
|
+
sha = git2(projectDir, ["rev-parse", "--verify", branch]);
|
|
1247
|
+
} catch {
|
|
1248
|
+
sha = git2(projectDir, ["rev-parse", "HEAD"]);
|
|
1249
|
+
}
|
|
1250
|
+
git2(projectDir, ["notes", "--ref=mugiwara", "add", "-f", "-m", note, sha]);
|
|
1251
|
+
return { sha };
|
|
1252
|
+
} catch {
|
|
1253
|
+
return null;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
function blamePath(projectDir, path) {
|
|
1257
|
+
let sha;
|
|
1258
|
+
try {
|
|
1259
|
+
sha = git2(projectDir, ["log", "-1", "--format=%H", "--", path]);
|
|
1260
|
+
} catch {
|
|
1261
|
+
return `blame: not a git repository (${projectDir})`;
|
|
1262
|
+
}
|
|
1263
|
+
if (!sha)
|
|
1264
|
+
return `blame: no commit touches "${path}"`;
|
|
1265
|
+
try {
|
|
1266
|
+
const note = git2(projectDir, ["notes", "--ref=mugiwara", "show", sha]);
|
|
1267
|
+
return `${path} @ ${sha.slice(0, 7)}
|
|
1268
|
+
${note}`;
|
|
1269
|
+
} catch {
|
|
1270
|
+
return `${path} @ ${sha.slice(0, 7)}
|
|
1271
|
+
(no mugiwara provenance note on this commit)`;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
function writeProvenance(projectDir, missionDir, state) {
|
|
1275
|
+
const note = buildNote(state);
|
|
1276
|
+
const attached = attachGitNote(projectDir, state.branch, note);
|
|
1277
|
+
writeFileSync7(join11(missionDir, "provenance.md"), renderProvenanceMd(note, attached ? attached.sha : null));
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// src/routing.ts
|
|
1281
|
+
var DOC_PAT = /\.(md|txt|rst)$|^docs\/|^(CHANGELOG|LICENSE|README)/;
|
|
1282
|
+
var TEST_PAT = /\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)(tests?|__tests__|specs?)\//;
|
|
1283
|
+
function scorePath(path, sensitivePaths, evidenceJoined) {
|
|
1284
|
+
const reasons = [];
|
|
1285
|
+
let score = 0;
|
|
1286
|
+
const sensitiveHit = sensitivePaths.some((s) => s && (path === s || path.startsWith(s.replace(/\/?$/, "/")) || new RegExp(s.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*")).test(path)));
|
|
1287
|
+
if (sensitiveHit) {
|
|
1288
|
+
score += 100;
|
|
1289
|
+
reasons.push("sensitive path");
|
|
1290
|
+
}
|
|
1291
|
+
if (!DOC_PAT.test(path) && !TEST_PAT.test(path)) {
|
|
1292
|
+
score += 50;
|
|
1293
|
+
reasons.push("production code");
|
|
1294
|
+
} else if (TEST_PAT.test(path)) {
|
|
1295
|
+
score += 10;
|
|
1296
|
+
reasons.push("test scaffolding — skim unless behavior changed");
|
|
1297
|
+
} else {
|
|
1298
|
+
score += 5;
|
|
1299
|
+
reasons.push("docs/config");
|
|
1300
|
+
}
|
|
1301
|
+
if (!evidenceJoined.includes(path)) {
|
|
1302
|
+
score += 20;
|
|
1303
|
+
reasons.push("not covered by recorded evidence");
|
|
1304
|
+
}
|
|
1305
|
+
return { score, reasons };
|
|
1306
|
+
}
|
|
1307
|
+
function rankFiles(paths, state) {
|
|
1308
|
+
const sensitive = state.sensitive_paths ?? [];
|
|
1309
|
+
const ev = state.evidence.join(" ");
|
|
1310
|
+
return paths.map((path) => {
|
|
1311
|
+
const { score, reasons } = scorePath(path, sensitive, ev);
|
|
1312
|
+
return { path, score, reasons };
|
|
1313
|
+
}).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
1314
|
+
}
|
|
1315
|
+
function renderRouting(ranked, mission) {
|
|
1316
|
+
if (!ranked.length)
|
|
1317
|
+
return "";
|
|
1318
|
+
const lines = [
|
|
1319
|
+
"",
|
|
1320
|
+
"## Review routing",
|
|
1321
|
+
"",
|
|
1322
|
+
`Ranked reading order for \`${mission}\` (heuristic ordering — it decides where to look first, never correctness):`,
|
|
1323
|
+
""
|
|
1324
|
+
];
|
|
1325
|
+
ranked.forEach((r, i) => {
|
|
1326
|
+
lines.push(`${i + 1}. \`${r.path}\` — ${r.reasons.join("; ")}`);
|
|
1327
|
+
});
|
|
1328
|
+
lines.push("");
|
|
1329
|
+
return lines.join(`
|
|
1330
|
+
`);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// src/budget.ts
|
|
1334
|
+
import { existsSync as existsSync9, readdirSync as readdirSync6, statSync } from "node:fs";
|
|
1335
|
+
import { join as join12 } from "node:path";
|
|
1336
|
+
function readBudgetConfig(projectDir) {
|
|
1337
|
+
const cfg = readConfig(projectDir);
|
|
1338
|
+
const raw = cfg.context_budget_chars;
|
|
1339
|
+
if (raw === undefined || raw === "")
|
|
1340
|
+
return 0;
|
|
1341
|
+
const n = Number(raw);
|
|
1342
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
1343
|
+
}
|
|
1344
|
+
function measureContextChars(missionDir) {
|
|
1345
|
+
let total = 0;
|
|
1346
|
+
const add = (p) => {
|
|
1347
|
+
try {
|
|
1348
|
+
total += statSync(p).size;
|
|
1349
|
+
} catch {}
|
|
1350
|
+
};
|
|
1351
|
+
for (const f of readdirSync6(missionDir)) {
|
|
1352
|
+
if (/\.md$/.test(f))
|
|
1353
|
+
add(join12(missionDir, f));
|
|
1354
|
+
}
|
|
1355
|
+
for (const sub of ["flows", "waves"]) {
|
|
1356
|
+
const dir = join12(missionDir, sub);
|
|
1357
|
+
if (existsSync9(dir)) {
|
|
1358
|
+
for (const f of readdirSync6(dir))
|
|
1359
|
+
add(join12(dir, f));
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
return total;
|
|
1363
|
+
}
|
|
1364
|
+
function formatFootprint(chars, budget) {
|
|
1365
|
+
const base = `Context footprint: ${chars} chars`;
|
|
1366
|
+
if (!budget)
|
|
1367
|
+
return `${base} (no budget configured)`;
|
|
1368
|
+
return chars > budget ? `${base} — OVER budget ${budget}` : `${base} (budget ${budget})`;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// src/cost.ts
|
|
1372
|
+
import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync8 } from "node:fs";
|
|
1373
|
+
import { join as join13 } from "node:path";
|
|
1374
|
+
var LANE_BUDGET = {
|
|
1375
|
+
lean: 12000,
|
|
1376
|
+
standard: 25000,
|
|
1377
|
+
full: 50000,
|
|
1378
|
+
spike: 3000
|
|
1379
|
+
};
|
|
1380
|
+
function budgetForLane(lane) {
|
|
1381
|
+
return LANE_BUDGET[lane] ?? 0;
|
|
1382
|
+
}
|
|
1383
|
+
function warnAt(budget) {
|
|
1384
|
+
return Math.floor(budget * 3 / 2);
|
|
1385
|
+
}
|
|
1386
|
+
function stopAt(budget) {
|
|
1387
|
+
return budget * 3;
|
|
1388
|
+
}
|
|
1389
|
+
function budgetStatus(budget, tokens) {
|
|
1390
|
+
if (budget > 0 && tokens >= stopAt(budget))
|
|
1391
|
+
return "stop";
|
|
1392
|
+
if (budget > 0 && tokens >= warnAt(budget))
|
|
1393
|
+
return "warn";
|
|
1394
|
+
return "ok";
|
|
1395
|
+
}
|
|
1396
|
+
function costEnvelope(state) {
|
|
1397
|
+
const planned = typeof state.budget === "number" && state.budget > 0 ? state.budget : budgetForLane(state.lane ?? "");
|
|
1398
|
+
const used = typeof state.tokens_est === "number" ? state.tokens_est : 0;
|
|
1399
|
+
const remaining = Math.max(planned - used, 0);
|
|
1400
|
+
const pct = planned > 0 ? Math.round(used / planned * 100) : 0;
|
|
1401
|
+
return {
|
|
1402
|
+
planned,
|
|
1403
|
+
used,
|
|
1404
|
+
remaining,
|
|
1405
|
+
pct,
|
|
1406
|
+
warn_at: warnAt(planned),
|
|
1407
|
+
stop_at: stopAt(planned),
|
|
1408
|
+
status: budgetStatus(planned, used)
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
var COST_EVENTS_FILE = "cost-events.jsonl";
|
|
1412
|
+
function isAllowedMissionDir(dir) {
|
|
1413
|
+
if (!dir || dir.includes(".."))
|
|
1414
|
+
return false;
|
|
1415
|
+
if (dir.includes(".mugiwara/missions"))
|
|
1416
|
+
return true;
|
|
1417
|
+
if (dir.includes("mugiwara-"))
|
|
1418
|
+
return true;
|
|
1419
|
+
if (dir.startsWith("/tmp/") && dir.includes("-"))
|
|
1420
|
+
return true;
|
|
1421
|
+
return false;
|
|
1422
|
+
}
|
|
1423
|
+
function assertMissionDir(dir) {
|
|
1424
|
+
if (!isAllowedMissionDir(dir))
|
|
1425
|
+
throw new Error(`Invalid missionDir: ${dir}`);
|
|
1426
|
+
}
|
|
1427
|
+
function appendCostEvent(missionDir, event) {
|
|
1428
|
+
assertMissionDir(missionDir);
|
|
1429
|
+
mkdirSync6(missionDir, { recursive: true });
|
|
1430
|
+
const line = Object.assign({ ts: new Date().toISOString() }, event);
|
|
1431
|
+
appendFileSync(join13(missionDir, COST_EVENTS_FILE), JSON.stringify(line) + `
|
|
1432
|
+
`, "utf8");
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// src/evidence.ts
|
|
1436
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync9 } from "node:fs";
|
|
1437
|
+
import { join as join14 } from "node:path";
|
|
1438
|
+
var REGISTRY_FILE = "context-registry.jsonl";
|
|
1439
|
+
function isAllowedMissionDir2(dir) {
|
|
1440
|
+
if (!dir || dir.includes(".."))
|
|
1441
|
+
return false;
|
|
1442
|
+
if (dir.includes(".mugiwara/missions"))
|
|
1443
|
+
return true;
|
|
1444
|
+
if (dir.includes("mugiwara-"))
|
|
1445
|
+
return true;
|
|
1446
|
+
if (dir.startsWith("/tmp/") && dir.includes("-"))
|
|
1447
|
+
return true;
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
function assertMissionDir2(dir) {
|
|
1451
|
+
if (!isAllowedMissionDir2(dir))
|
|
1452
|
+
throw new Error(`Invalid missionDir: ${dir}`);
|
|
1453
|
+
}
|
|
1454
|
+
function loadRegistry(missionDir) {
|
|
1455
|
+
assertMissionDir2(missionDir);
|
|
1456
|
+
const file = join14(missionDir, REGISTRY_FILE);
|
|
1457
|
+
try {
|
|
1458
|
+
const out = [];
|
|
1459
|
+
for (const line of readFileSync9(file, "utf8").split(/\r?\n/)) {
|
|
1460
|
+
if (!line.trim())
|
|
1461
|
+
continue;
|
|
1462
|
+
let e;
|
|
1463
|
+
try {
|
|
1464
|
+
e = JSON.parse(line);
|
|
1465
|
+
} catch {
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
if (e === null || typeof e !== "object")
|
|
1469
|
+
continue;
|
|
1470
|
+
const entry = e;
|
|
1471
|
+
const ok = typeof entry.fingerprint === "string" && typeof entry.kind === "string" && typeof entry.file === "string" && typeof entry.id === "string" && typeof entry.ref === "string" && typeof entry.reads === "number" && Number.isFinite(entry.reads) && entry.reads >= 0;
|
|
1472
|
+
if (!ok)
|
|
1473
|
+
continue;
|
|
1474
|
+
entry.reads = Math.floor(entry.reads);
|
|
1475
|
+
out.push(entry);
|
|
1476
|
+
}
|
|
1477
|
+
return out;
|
|
1478
|
+
} catch {
|
|
1479
|
+
return [];
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// src/context.ts
|
|
1484
|
+
function contextStatus(budgetChars, chars) {
|
|
1485
|
+
return budgetChars > 0 && chars > budgetChars ? "over" : "ok";
|
|
1486
|
+
}
|
|
1487
|
+
function computeContextMetrics(stats) {
|
|
1488
|
+
const reuse_rate = stats.reads_total > 0 ? stats.reads_reused / stats.reads_total : 0;
|
|
1489
|
+
const duplicate_chars = stats.total_chars - stats.unique_chars;
|
|
1490
|
+
return {
|
|
1491
|
+
files_loaded: stats.files_loaded,
|
|
1492
|
+
repeated_reads: stats.repeated_reads,
|
|
1493
|
+
duplicate_chars,
|
|
1494
|
+
reuse_rate,
|
|
1495
|
+
read_avoidance_chars: duplicate_chars
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// src/reporting.ts
|
|
1500
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
|
|
1501
|
+
import { join as join15 } from "node:path";
|
|
1502
|
+
function isAllowedMissionDir3(dir) {
|
|
1503
|
+
if (!dir || dir.includes(".."))
|
|
1504
|
+
return false;
|
|
1505
|
+
if (dir.includes(".mugiwara/missions"))
|
|
1506
|
+
return true;
|
|
1507
|
+
if (dir.includes("mugiwara-"))
|
|
1508
|
+
return true;
|
|
1509
|
+
if (dir.startsWith("/tmp/") && dir.includes("-"))
|
|
1510
|
+
return true;
|
|
1511
|
+
return false;
|
|
1512
|
+
}
|
|
1513
|
+
function assertMissionDir3(dir) {
|
|
1514
|
+
if (!isAllowedMissionDir3(dir))
|
|
1515
|
+
throw new Error(`Invalid missionDir: ${dir}`);
|
|
1516
|
+
}
|
|
1517
|
+
function parseDecisionTrail(missionDir) {
|
|
1518
|
+
assertMissionDir3(missionDir);
|
|
1519
|
+
const file = join15(missionDir, "decisions.md");
|
|
1520
|
+
if (!existsSync10(file))
|
|
1521
|
+
return [];
|
|
1522
|
+
const raw = readFileSync10(file, "utf8");
|
|
1523
|
+
const lines = raw.split(/\r?\n/);
|
|
1524
|
+
const headerIdx = lines.findIndex((l) => l.trim() === "## Cost governor decisions" || l.trim() === "## Budget");
|
|
1525
|
+
const scan = headerIdx >= 0 ? lines.slice(headerIdx) : lines;
|
|
1526
|
+
const out = [];
|
|
1527
|
+
const bulletRe = /^-\s+(.+?)\s+—\s+(.+?):\s+(.+?)\s+—\s+reason:\s+(.+?)(?:\s+—\s+evidence:\s+(.+))?\s*$/;
|
|
1528
|
+
for (const line of scan) {
|
|
1529
|
+
const m = bulletRe.exec(line.trim());
|
|
1530
|
+
if (!m)
|
|
1531
|
+
continue;
|
|
1532
|
+
out.push({ ts: m[1], actor: m[2], decision: m[3], reason: m[4], ...m[5] ? { evidence: m[5] } : {} });
|
|
1533
|
+
}
|
|
1534
|
+
return out;
|
|
1535
|
+
}
|
|
1536
|
+
function loadCostEvents(missionDir) {
|
|
1537
|
+
assertMissionDir3(missionDir);
|
|
1538
|
+
const file = join15(missionDir, "cost-events.jsonl");
|
|
1539
|
+
if (!existsSync10(file))
|
|
1540
|
+
return [];
|
|
1541
|
+
const out = [];
|
|
1542
|
+
for (const line of readFileSync10(file, "utf8").split(/\r?\n/)) {
|
|
1543
|
+
if (!line.trim())
|
|
1544
|
+
continue;
|
|
1545
|
+
try {
|
|
1546
|
+
const e = JSON.parse(line);
|
|
1547
|
+
if (e && typeof e === "object" && typeof e.mission === "string")
|
|
1548
|
+
out.push(e);
|
|
1549
|
+
} catch {
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
return out;
|
|
1554
|
+
}
|
|
1555
|
+
function computeAvoidedMetrics(input) {
|
|
1556
|
+
const dup = input.registryMetrics?.duplicateCount ?? 0;
|
|
1557
|
+
const rep = input.registryMetrics?.repeatedReads ?? 0;
|
|
1558
|
+
const contexts_avoided = dup + rep;
|
|
1559
|
+
const stages_avoided = input.workMetrics?.stagesAvoided ?? 0;
|
|
1560
|
+
const slop_interventions = input.slopMetrics?.interventions ?? 0;
|
|
1561
|
+
const tokens_avoided_est = contexts_avoided * 150;
|
|
1562
|
+
return { stages_avoided, contexts_avoided, slop_interventions, tokens_avoided_est };
|
|
1563
|
+
}
|
|
1564
|
+
function computeEfficiencyMetrics(input) {
|
|
1565
|
+
const reuse_rate = input.totalReads > 0 ? Math.round(input.reuseHits / input.totalReads * 100) / 100 : 0;
|
|
1566
|
+
const duplicate_avoidance_chars = input.duplicateChars;
|
|
1567
|
+
const budget_efficiency_pct = input.budget > 0 ? Math.round(input.used / input.budget * 100) : 0;
|
|
1568
|
+
return { reuse_rate, duplicate_avoidance_chars, budget_efficiency_pct };
|
|
1569
|
+
}
|
|
1570
|
+
function buildCostLedger(input) {
|
|
1571
|
+
assertMissionDir3(input.missionDir);
|
|
1572
|
+
const events = loadCostEvents(input.missionDir);
|
|
1573
|
+
let registrySize = 0;
|
|
1574
|
+
let totalReads = 0;
|
|
1575
|
+
let reuseHits = 0;
|
|
1576
|
+
let duplicateChars = 0;
|
|
1577
|
+
try {
|
|
1578
|
+
const reg = loadRegistry(input.missionDir);
|
|
1579
|
+
registrySize = reg.length;
|
|
1580
|
+
totalReads = reg.reduce((s, e) => s + e.reads, 0);
|
|
1581
|
+
reuseHits = reg.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
|
|
1582
|
+
const unique = reg.reduce((s, e) => s + (e.chars ?? 0), 0);
|
|
1583
|
+
const total = reg.reduce((s, e) => s + (e.chars ?? 0) * e.reads, 0);
|
|
1584
|
+
duplicateChars = total - unique;
|
|
1585
|
+
if (duplicateChars < 0)
|
|
1586
|
+
duplicateChars = 0;
|
|
1587
|
+
} catch {
|
|
1588
|
+
registrySize = 0;
|
|
1589
|
+
}
|
|
1590
|
+
const decisions = parseDecisionTrail(input.missionDir);
|
|
1591
|
+
const repeatedReads = reuseHits;
|
|
1592
|
+
const duplicateCount = duplicateChars > 0 ? 1 : 0;
|
|
1593
|
+
const avoided = computeAvoidedMetrics({
|
|
1594
|
+
registryMetrics: { duplicateCount: 0, repeatedReads: reuseHits },
|
|
1595
|
+
workMetrics: { stagesAvoided: input.workSummary?.stagesAvoided ?? 0 },
|
|
1596
|
+
slopMetrics: { interventions: input.slopSummary?.interventions ?? 0 }
|
|
1597
|
+
});
|
|
1598
|
+
if (duplicateChars > 0 && avoided.contexts_avoided === 0) {
|
|
1599
|
+
avoided.contexts_avoided = 1;
|
|
1600
|
+
avoided.tokens_avoided_est = 150;
|
|
1601
|
+
}
|
|
1602
|
+
const efficiency = computeEfficiencyMetrics({
|
|
1603
|
+
totalReads,
|
|
1604
|
+
reuseHits,
|
|
1605
|
+
duplicateChars,
|
|
1606
|
+
budget: input.envelope.planned,
|
|
1607
|
+
used: input.envelope.used
|
|
1608
|
+
});
|
|
1609
|
+
return {
|
|
1610
|
+
envelope: input.envelope,
|
|
1611
|
+
ledger: { events, registrySize, decisions },
|
|
1612
|
+
avoided,
|
|
1613
|
+
efficiency,
|
|
1614
|
+
trail: decisions
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
function toCostJSON(ledger) {
|
|
1618
|
+
return JSON.stringify({ envelope: ledger.envelope, ledger: ledger.ledger, avoided: ledger.avoided, efficiency: ledger.efficiency, trail: ledger.trail }, null, 2);
|
|
1619
|
+
}
|
|
1620
|
+
function summarizeAdaptation(missionDir) {
|
|
1621
|
+
const trail = parseDecisionTrail(missionDir);
|
|
1622
|
+
const postureRe = /posture|switch|adapt|pause|parallel-workers|context-relief|phase-isolated|team-scoped/i;
|
|
1623
|
+
const rows = trail.filter((t) => postureRe.test(`${t.decision} ${t.reason}`)).map((t) => ({ ts: t.ts, decision: t.decision, reason: t.reason }));
|
|
1624
|
+
return { count: rows.length, rows };
|
|
1625
|
+
}
|
|
1626
|
+
function renderAdaptationSection(missionDir) {
|
|
1627
|
+
const { count, rows } = summarizeAdaptation(missionDir);
|
|
1628
|
+
if (count === 0)
|
|
1629
|
+
return "";
|
|
1630
|
+
const lines = ["", "## Adaptation", "", "| Time | Decision | Reason |", "|---|---|---|"];
|
|
1631
|
+
for (const r of rows)
|
|
1632
|
+
lines.push(`| ${r.ts} | ${r.decision} | ${r.reason} |`);
|
|
1633
|
+
if (rows.length === 0)
|
|
1634
|
+
lines.push("| — | (posture rows recorded in decisions.md) | |");
|
|
1635
|
+
return lines.join(`
|
|
1636
|
+
`);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// src/mission.ts
|
|
1640
|
+
function isStateFile(f) {
|
|
1641
|
+
const stem = f.replace(/\.json$/, "");
|
|
1642
|
+
return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
|
|
1643
|
+
}
|
|
1644
|
+
function primaryState(dir, files) {
|
|
1645
|
+
const name = files.includes("state.json") ? "state.json" : files.find((f) => f.endsWith(".json") && f !== "continue.json" && !f.startsWith("continue-"));
|
|
1646
|
+
if (!name)
|
|
1647
|
+
return null;
|
|
1648
|
+
try {
|
|
1649
|
+
return JSON.parse(readFileSync11(join16(dir, name), "utf8"));
|
|
1650
|
+
} catch {
|
|
1651
|
+
return null;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
function changedFiles(projectDir, state) {
|
|
1655
|
+
const base = typeof state?.base_sha === "string" ? state.base_sha : "";
|
|
1656
|
+
const branch = typeof state?.branch === "string" ? state.branch : "";
|
|
1657
|
+
if (!base || base === "unknown" || !branch)
|
|
1658
|
+
return [];
|
|
1659
|
+
try {
|
|
1660
|
+
return execFileSync3("git", ["diff", "--name-only", base, branch], {
|
|
1661
|
+
cwd: projectDir,
|
|
1662
|
+
encoding: "utf8",
|
|
1663
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1664
|
+
}).split(/\r?\n/).filter(Boolean);
|
|
1665
|
+
} catch {
|
|
1666
|
+
return [];
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
903
1669
|
function activeActor(projectDir) {
|
|
904
|
-
const
|
|
905
|
-
if (!
|
|
1670
|
+
const missionsDir = join16(projectDir, ".mugiwara", "missions");
|
|
1671
|
+
if (!existsSync11(missionsDir))
|
|
906
1672
|
return null;
|
|
907
|
-
const missions =
|
|
1673
|
+
const missions = readdirSync7(missionsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
908
1674
|
let latest = null;
|
|
909
1675
|
for (const mission of missions) {
|
|
910
|
-
const d =
|
|
911
|
-
for (const f of
|
|
1676
|
+
const d = join16(missionsDir, mission);
|
|
1677
|
+
for (const f of readdirSync7(d).filter(isStateFile)) {
|
|
912
1678
|
try {
|
|
913
|
-
const s = JSON.parse(
|
|
1679
|
+
const s = JSON.parse(readFileSync11(join16(d, f), "utf8"));
|
|
914
1680
|
const t = Date.parse(s.updated_at || "") || 0;
|
|
915
1681
|
if (!latest || t > latest.updated)
|
|
916
1682
|
latest = { actor: s.actor || null, updated: t };
|
|
@@ -920,8 +1686,8 @@ function activeActor(projectDir) {
|
|
|
920
1686
|
return latest ? latest.actor : null;
|
|
921
1687
|
}
|
|
922
1688
|
function resetMission(projectDir, keepLogs, force) {
|
|
923
|
-
const root =
|
|
924
|
-
if (!
|
|
1689
|
+
const root = join16(projectDir, ".mugiwara");
|
|
1690
|
+
if (!existsSync11(root))
|
|
925
1691
|
return { removed: [], kept: [] };
|
|
926
1692
|
if (!force) {
|
|
927
1693
|
const actor = activeActor(projectDir);
|
|
@@ -931,361 +1697,326 @@ function resetMission(projectDir, keepLogs, force) {
|
|
|
931
1697
|
}
|
|
932
1698
|
const removed = [];
|
|
933
1699
|
const kept = [];
|
|
934
|
-
for (const dir of ["spec", "plans", "results", "review", "issues", "reports"]) {
|
|
935
|
-
const p =
|
|
936
|
-
if (
|
|
937
|
-
rmSync2(p, { recursive: true, force: true });
|
|
938
|
-
removed.push(dir);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
for (const dir of ["state", "continue"]) {
|
|
942
|
-
const p = join7(root, dir);
|
|
943
|
-
if (existsSync6(p)) {
|
|
1700
|
+
for (const dir of ["missions", "spec", "plans", "results", "review", "issues", "reports", "state", "continue"]) {
|
|
1701
|
+
const p = join16(root, dir);
|
|
1702
|
+
if (existsSync11(p)) {
|
|
944
1703
|
rmSync2(p, { recursive: true, force: true });
|
|
945
1704
|
removed.push(dir);
|
|
946
1705
|
}
|
|
947
1706
|
}
|
|
948
|
-
for (const f of
|
|
1707
|
+
for (const f of readdirSync7(root)) {
|
|
949
1708
|
if (/^state(-.+)?\.json$/.test(f)) {
|
|
950
|
-
const p =
|
|
951
|
-
if (
|
|
1709
|
+
const p = join16(root, f);
|
|
1710
|
+
if (existsSync11(p)) {
|
|
952
1711
|
rmSync2(p);
|
|
953
1712
|
removed.push(f);
|
|
954
1713
|
}
|
|
955
1714
|
}
|
|
956
1715
|
}
|
|
957
1716
|
if (!keepLogs) {
|
|
958
|
-
const p
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1717
|
+
for (const p of [join16(root, "lessons.md"), join16(root, "logs")]) {
|
|
1718
|
+
if (existsSync11(p)) {
|
|
1719
|
+
rmSync2(p, { recursive: true, force: true });
|
|
1720
|
+
removed.push(p.startsWith(join16(root, "logs")) ? "logs" : "lessons.md");
|
|
1721
|
+
}
|
|
962
1722
|
}
|
|
963
|
-
} else
|
|
964
|
-
|
|
1723
|
+
} else {
|
|
1724
|
+
if (existsSync11(join16(root, "lessons.md")))
|
|
1725
|
+
kept.push("lessons.md");
|
|
1726
|
+
else if (existsSync11(join16(root, join16("logs", "lessons.md"))))
|
|
1727
|
+
kept.push(join16("logs", "lessons.md"));
|
|
965
1728
|
}
|
|
966
1729
|
for (const f of ["config", "manifest.json", "backup"]) {
|
|
967
|
-
if (
|
|
1730
|
+
if (existsSync11(join16(root, f)))
|
|
968
1731
|
kept.push(f);
|
|
969
1732
|
}
|
|
970
1733
|
return { removed, kept };
|
|
971
1734
|
}
|
|
972
1735
|
function archiveMission(projectDir, mission, opts = {}) {
|
|
973
1736
|
const { dryRun = false } = opts;
|
|
974
|
-
const root =
|
|
1737
|
+
const root = join16(projectDir, ".mugiwara");
|
|
975
1738
|
if (!mission || /[^a-zA-Z0-9._-]/.test(mission) || /^\.+$/.test(mission))
|
|
976
1739
|
throw new Error(`invalid mission name "${mission}" (allowlist: [a-zA-Z0-9._-], not a dot-path)`);
|
|
977
1740
|
const removed = [];
|
|
978
1741
|
const kept = [];
|
|
979
|
-
const
|
|
980
|
-
|
|
981
|
-
return
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const m = n.match(/^(\d{4}-\d{2}-\d{2})-(.+)\.md$/);
|
|
988
|
-
return !!m && m[2] === mission;
|
|
989
|
-
});
|
|
990
|
-
if (f)
|
|
991
|
-
report = join7("reports", f);
|
|
992
|
-
}
|
|
993
|
-
const resultsDir = join7(root, "results", mission);
|
|
994
|
-
if (existsSync6(resultsDir)) {
|
|
995
|
-
for (const f of readdirSync4(resultsDir)) {
|
|
996
|
-
kept.push(join7("results", mission, f));
|
|
1742
|
+
const dir = join16(root, "missions", mission);
|
|
1743
|
+
if (!existsSync11(dir))
|
|
1744
|
+
return { report: null, removed, kept };
|
|
1745
|
+
if (!dryRun) {
|
|
1746
|
+
const issues = checkTrail(dir, projectDir);
|
|
1747
|
+
if (issues.length) {
|
|
1748
|
+
throw new Error(`closure integrity gate failed — fix these before archiving:
|
|
1749
|
+
${formatIssues(issues)}`);
|
|
997
1750
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
for (const f of readdirSync4(specDir)) {
|
|
1002
|
-
if (!belongs(f))
|
|
1003
|
-
continue;
|
|
1004
|
-
const p = join7(specDir, f);
|
|
1005
|
-
if (!dryRun)
|
|
1006
|
-
rmSync2(p);
|
|
1007
|
-
removed.push(join7("spec", f));
|
|
1751
|
+
const artifacts = checkMissionArtifacts(dir);
|
|
1752
|
+
if (!artifacts.ok) {
|
|
1753
|
+
throw new Error(`archive artifact gate failed — missing: ${artifacts.missing.join(", ")} (lane ${artifacts.lane}). Write the evidence trail before archiving.`);
|
|
1008
1754
|
}
|
|
1009
1755
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1756
|
+
const files = readdirSync7(dir);
|
|
1757
|
+
const state = primaryState(dir, files);
|
|
1758
|
+
const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
|
|
1759
|
+
try {
|
|
1760
|
+
const s = JSON.parse(readFileSync11(join16(dir, f), "utf8"));
|
|
1761
|
+
return typeof s.model === "string" ? s.model : "";
|
|
1762
|
+
} catch {
|
|
1763
|
+
return "";
|
|
1764
|
+
}
|
|
1765
|
+
}).filter(Boolean))];
|
|
1766
|
+
let costSection = "";
|
|
1767
|
+
if (!dryRun && state) {
|
|
1768
|
+
const chars = measureContextChars(dir);
|
|
1769
|
+
const budget = readBudgetConfig(projectDir);
|
|
1770
|
+
const footprintLine = formatFootprint(chars, budget);
|
|
1771
|
+
const est = typeof state.tokens_est === "number" ? state.tokens_est : 0;
|
|
1772
|
+
const src = typeof state.tokens_source === "string" ? state.tokens_source : "computed";
|
|
1773
|
+
const lane = typeof state.lane === "string" ? state.lane : "unknown";
|
|
1774
|
+
const laneBudget = budgetForLane(lane);
|
|
1775
|
+
const env = costEnvelope({ lane, budget: laneBudget, tokens_est: est });
|
|
1776
|
+
const effBudget = budget || laneBudget;
|
|
1777
|
+
const delta = effBudget ? est <= effBudget ? `${(effBudget - est).toLocaleString()} under` : `${(est - effBudget).toLocaleString()} over` : "no budget configured";
|
|
1778
|
+
const srcLabel = src === "reported" ? "provider-reported" : "estimator";
|
|
1779
|
+
const statusLabel = env.status.toUpperCase();
|
|
1780
|
+
const ctxStatus = contextStatus(budget, chars);
|
|
1781
|
+
const registry = loadRegistry(dir);
|
|
1782
|
+
const reads_total = registry.reduce((s, e) => s + e.reads, 0);
|
|
1783
|
+
const repeated_reads = registry.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
|
|
1784
|
+
const unique_chars = registry.reduce((s, e) => s + (e.chars ?? 0), 0);
|
|
1785
|
+
const total_chars = registry.reduce((s, e) => s + (e.chars ?? 0) * e.reads, 0);
|
|
1786
|
+
const charTracked = registry.length > 0 && total_chars > 0;
|
|
1787
|
+
const metrics = registry.length ? computeContextMetrics({
|
|
1788
|
+
files_loaded: registry.length,
|
|
1789
|
+
reads_total,
|
|
1790
|
+
reads_reused: repeated_reads,
|
|
1791
|
+
unique_chars,
|
|
1792
|
+
total_chars,
|
|
1793
|
+
repeated_reads
|
|
1794
|
+
}) : { files_loaded: 0, repeated_reads: 0, duplicate_chars: 0, reuse_rate: 0, read_avoidance_chars: 0 };
|
|
1795
|
+
const ctxNote = registry.length ? charTracked ? "" : " (char data not tracked)" : " (no registry — reads not tracked)";
|
|
1796
|
+
let reportedTotal = 0;
|
|
1797
|
+
let hasReported = false;
|
|
1798
|
+
for (const f of files.filter(isStateFile)) {
|
|
1799
|
+
try {
|
|
1800
|
+
const s = JSON.parse(readFileSync11(join16(dir, f), "utf8"));
|
|
1801
|
+
if (s.tokens_source === "reported" && typeof s.tokens_est === "number") {
|
|
1802
|
+
reportedTotal += s.tokens_est;
|
|
1803
|
+
hasReported = true;
|
|
1804
|
+
}
|
|
1805
|
+
} catch {}
|
|
1806
|
+
}
|
|
1807
|
+
if (!hasReported && src === "reported" && est > 0) {
|
|
1808
|
+
reportedTotal = est;
|
|
1809
|
+
hasReported = true;
|
|
1810
|
+
}
|
|
1811
|
+
costSection = [
|
|
1812
|
+
"## Cost",
|
|
1813
|
+
"",
|
|
1814
|
+
"| Metric | Value |",
|
|
1815
|
+
"|--------|-------|",
|
|
1816
|
+
`| **Tokens used** | ${est.toLocaleString()} (${srcLabel}) |`,
|
|
1817
|
+
`| **Lane** | ${lane} (budget ${effBudget ? effBudget.toLocaleString() : "—"} · warn ${effBudget ? env.warn_at.toLocaleString() : "—"} · stop ${effBudget ? env.stop_at.toLocaleString() : "—"}) |`,
|
|
1818
|
+
`| **Budget status** | ${effBudget ? `${env.pct}% of budget · ${delta} · ${statusLabel}` : "no lane budget"} |`,
|
|
1819
|
+
`| **Context footprint** | ${chars.toLocaleString()} chars${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
|
|
1820
|
+
`| **Context budget status** | ${ctxStatus.toUpperCase()}${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
|
|
1821
|
+
`| **Context efficiency** | files_loaded: ${metrics.files_loaded} · repeated_reads: ${metrics.repeated_reads} · duplicate_chars: ${charTracked ? metrics.duplicate_chars : "n/a"} · reuse_rate: ${metrics.reuse_rate} · read_avoidance_chars: ${charTracked ? metrics.read_avoidance_chars : "n/a"}${ctxNote} |`
|
|
1822
|
+
].join(`
|
|
1823
|
+
`);
|
|
1824
|
+
if (hasReported) {
|
|
1825
|
+
costSection += `
|
|
1826
|
+
| **Provider total** | ${reportedTotal.toLocaleString()} (provider-reported — sum of reported stages) |`;
|
|
1827
|
+
}
|
|
1828
|
+
try {
|
|
1829
|
+
const ledger = buildCostLedger({ missionDir: dir, envelope: env });
|
|
1830
|
+
costSection += `
|
|
1831
|
+
| Budget | ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned}) |`;
|
|
1832
|
+
costSection += `
|
|
1833
|
+
| Context | ${chars.toLocaleString()} chars, reuse ${ledger.efficiency.reuse_rate} |`;
|
|
1834
|
+
costSection += `
|
|
1835
|
+
| Avoided | ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ${ledger.avoided.tokens_avoided_est} tokens est |`;
|
|
1836
|
+
costSection += `
|
|
1837
|
+
| Efficiency | reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}% |`;
|
|
1838
|
+
costSection += `
|
|
1839
|
+
| Trail | ${ledger.trail.length} decisions |`;
|
|
1840
|
+
if (ledger.trail.length) {
|
|
1841
|
+
const show = ledger.trail.slice(0, 5);
|
|
1842
|
+
for (const t of show)
|
|
1843
|
+
costSection += `
|
|
1844
|
+
- ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ""}`;
|
|
1845
|
+
if (ledger.trail.length > 5)
|
|
1846
|
+
costSection += `
|
|
1847
|
+
… ${ledger.trail.length - 5} more`;
|
|
1848
|
+
}
|
|
1849
|
+
} catch {}
|
|
1850
|
+
costSection += `
|
|
1851
|
+
`;
|
|
1852
|
+
try {
|
|
1853
|
+
costSection += renderAdaptationSection(dir);
|
|
1854
|
+
} catch {}
|
|
1855
|
+
appendCostEvent(dir, {
|
|
1856
|
+
kind: "closure",
|
|
1857
|
+
mission,
|
|
1858
|
+
tokens_est: est,
|
|
1859
|
+
budget: laneBudget,
|
|
1860
|
+
status: env.status,
|
|
1861
|
+
context_chars: chars,
|
|
1862
|
+
context_status: ctxStatus,
|
|
1863
|
+
context_metrics: metrics
|
|
1864
|
+
});
|
|
1865
|
+
if (budget && chars > budget) {
|
|
1866
|
+
throw new Error(`closure context budget failed — ${footprintLine}. Trim the trail or raise context_budget_chars.`);
|
|
1021
1867
|
}
|
|
1022
1868
|
}
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1869
|
+
const FOLD_TOP = ["decisions.md", "blockers.md", "review.md", "security.md", "spec.md"];
|
|
1870
|
+
const fold = [];
|
|
1871
|
+
for (const f of FOLD_TOP) {
|
|
1872
|
+
if (files.includes(f))
|
|
1873
|
+
fold.push(f);
|
|
1874
|
+
}
|
|
1875
|
+
const PR_VERDICT = "pr-verdict.md";
|
|
1876
|
+
const PR_VERDICT_SRC = join16("flows", "07-pr-verdict.md");
|
|
1877
|
+
const flowsDir = join16(dir, "flows");
|
|
1878
|
+
const legacyWavesDir = join16(dir, "waves");
|
|
1879
|
+
const artDir = existsSync11(flowsDir) ? flowsDir : existsSync11(legacyWavesDir) ? legacyWavesDir : flowsDir;
|
|
1880
|
+
const artRel = artDir === legacyWavesDir ? "waves" : "flows";
|
|
1881
|
+
if (existsSync11(artDir)) {
|
|
1882
|
+
for (const f of readdirSync7(artDir).sort()) {
|
|
1883
|
+
if (artRel === "flows" && f === "07-pr-verdict.md")
|
|
1027
1884
|
continue;
|
|
1028
|
-
|
|
1029
|
-
if (!dryRun)
|
|
1030
|
-
rmSync2(p);
|
|
1031
|
-
removed.push(join7("logs", f));
|
|
1885
|
+
fold.push(join16(artRel, f));
|
|
1032
1886
|
}
|
|
1033
1887
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
kept.push(
|
|
1051
|
-
|
|
1888
|
+
if (existsSync11(join16(dir, "cost-events.jsonl")))
|
|
1889
|
+
fold.push("cost-events.jsonl");
|
|
1890
|
+
if (existsSync11(join16(dir, "context-registry.jsonl")))
|
|
1891
|
+
fold.push("context-registry.jsonl");
|
|
1892
|
+
let report = "";
|
|
1893
|
+
const reportPath = join16(dir, "report.md");
|
|
1894
|
+
if (files.includes("report.md"))
|
|
1895
|
+
report = readFileSync11(reportPath, "utf8");
|
|
1896
|
+
else if (existsSync11(join16(artDir, "06-closure.md")))
|
|
1897
|
+
report = readFileSync11(join16(artDir, "06-closure.md"), "utf8");
|
|
1898
|
+
if (!dryRun) {
|
|
1899
|
+
mkdirSync8(dir, { recursive: true });
|
|
1900
|
+
const prVerdictSrc = join16(dir, PR_VERDICT_SRC);
|
|
1901
|
+
if (existsSync11(prVerdictSrc)) {
|
|
1902
|
+
const prVerdictPath = join16(dir, PR_VERDICT);
|
|
1903
|
+
writeFileSync8(prVerdictPath, readFileSync11(prVerdictSrc, "utf8"));
|
|
1904
|
+
kept.push(join16("missions", mission, PR_VERDICT));
|
|
1905
|
+
}
|
|
1906
|
+
if (fold.length) {
|
|
1907
|
+
const sections = fold.map((f) => {
|
|
1908
|
+
const body = readFileSync11(join16(dir, f), "utf8").trim();
|
|
1909
|
+
const name = f.includes("/") ? f.split("/").pop() ?? f : f;
|
|
1910
|
+
return `
|
|
1911
|
+
|
|
1912
|
+
## Archived: ${name}
|
|
1913
|
+
|
|
1914
|
+
${body}`;
|
|
1915
|
+
}).join("");
|
|
1916
|
+
const tmp = `${reportPath}.tmp`;
|
|
1917
|
+
const routingSection = state ? renderRouting(rankFiles(changedFiles(projectDir, state), {
|
|
1918
|
+
mission,
|
|
1919
|
+
evidence: Array.isArray(state.evidence) ? state.evidence : [],
|
|
1920
|
+
sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
|
|
1921
|
+
}), mission) : "";
|
|
1922
|
+
writeFileSync8(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
|
|
1923
|
+
${costSection}
|
|
1924
|
+
` : "") + `
|
|
1925
|
+
`);
|
|
1926
|
+
renameSync(tmp, reportPath);
|
|
1927
|
+
}
|
|
1928
|
+
for (const f of fold) {
|
|
1929
|
+
rmSync2(join16(dir, f), { force: true, recursive: true });
|
|
1930
|
+
removed.push(join16("missions", mission, f));
|
|
1931
|
+
}
|
|
1932
|
+
if (existsSync11(prVerdictSrc)) {
|
|
1933
|
+
rmSync2(join16(dir, PR_VERDICT_SRC), { force: true });
|
|
1934
|
+
removed.push(join16("missions", mission, PR_VERDICT_SRC));
|
|
1935
|
+
}
|
|
1936
|
+
for (const f of files.filter((f2) => f2.endsWith(".json")))
|
|
1937
|
+
rmSync2(join16(dir, f), { force: true });
|
|
1938
|
+
if (existsSync11(artDir) && readdirSync7(artDir).length === 0)
|
|
1939
|
+
rmSync2(artDir, { recursive: true, force: true });
|
|
1940
|
+
if (!existsSync11(reportPath)) {
|
|
1941
|
+
writeFileSync8(reportPath, `# Mission: ${mission}
|
|
1942
|
+
|
|
1943
|
+
Archived before closure — no wave artifacts were present.
|
|
1944
|
+
`);
|
|
1945
|
+
}
|
|
1946
|
+
if (state && typeof state.branch === "string") {
|
|
1947
|
+
const rb = generateRollback(projectDir, dir, {
|
|
1948
|
+
mission,
|
|
1949
|
+
branch: state.branch,
|
|
1950
|
+
baseSha: typeof state.base_sha === "string" ? state.base_sha : "unknown"
|
|
1951
|
+
});
|
|
1952
|
+
if (rb)
|
|
1953
|
+
kept.push(join16("missions", mission, rb.file));
|
|
1954
|
+
try {
|
|
1955
|
+
writeProvenance(projectDir, dir, {
|
|
1956
|
+
mission,
|
|
1957
|
+
actor: typeof state.actor === "string" ? state.actor : "",
|
|
1958
|
+
lane: typeof state.lane === "string" ? state.lane : "",
|
|
1959
|
+
mode: typeof state.mode === "string" ? state.mode : "",
|
|
1960
|
+
branch: state.branch,
|
|
1961
|
+
tasks_done: Number(state.tasks_done) || 0,
|
|
1962
|
+
tasks_total: Number(state.tasks_total) || 0,
|
|
1963
|
+
evidence: Array.isArray(state.evidence) ? state.evidence : [],
|
|
1964
|
+
models: stageModels
|
|
1965
|
+
});
|
|
1966
|
+
kept.push(join16("missions", mission, "provenance.md"));
|
|
1967
|
+
} catch {}
|
|
1968
|
+
}
|
|
1969
|
+
} else {
|
|
1970
|
+
for (const f of fold)
|
|
1971
|
+
removed.push(join16("missions", mission, f));
|
|
1972
|
+
}
|
|
1973
|
+
removed.push(join16("missions", mission, "<session state>"));
|
|
1974
|
+
if (files.includes("plan.md"))
|
|
1975
|
+
kept.push(join16("missions", mission, "plan.md"));
|
|
1976
|
+
if (files.includes("handoff.md"))
|
|
1977
|
+
kept.push(join16("missions", mission, "handoff.md"));
|
|
1978
|
+
kept.push(join16("missions", mission, "report.md"));
|
|
1052
1979
|
let index;
|
|
1053
|
-
const indexFile =
|
|
1054
|
-
const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}
|
|
1980
|
+
const indexFile = join16(root, "index.md");
|
|
1981
|
+
const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}
|
|
1055
1982
|
`;
|
|
1056
1983
|
if (!dryRun) {
|
|
1057
|
-
|
|
1058
|
-
const existing = existsSync6(indexFile) ? readFileSync5(indexFile, "utf8") : "";
|
|
1984
|
+
const existing = existsSync11(indexFile) ? readFileSync11(indexFile, "utf8") : "";
|
|
1059
1985
|
if (!existing.split(/\r?\n/).some((l) => l.startsWith(`- ${mission} —`))) {
|
|
1060
1986
|
const header = existing ? "" : `# Mission index
|
|
1061
1987
|
|
|
1062
1988
|
`;
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
// src/onboard.ts
|
|
1071
|
-
import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1072
|
-
import { join as join8 } from "node:path";
|
|
1073
|
-
var DEFAULT_ORDER = [
|
|
1074
|
-
"mode",
|
|
1075
|
-
"branch",
|
|
1076
|
-
"commit",
|
|
1077
|
-
"auto_commit",
|
|
1078
|
-
"coverage_new",
|
|
1079
|
-
"coverage_modified",
|
|
1080
|
-
"review_depth",
|
|
1081
|
-
"quality_depth",
|
|
1082
|
-
"delegate_threshold",
|
|
1083
|
-
"heal_max_cycles",
|
|
1084
|
-
"verbosity"
|
|
1085
|
-
];
|
|
1086
|
-
var DEFAULTS = {
|
|
1087
|
-
auto_commit: "on",
|
|
1088
|
-
delegate_threshold: "60",
|
|
1089
|
-
heal_max_cycles: "3",
|
|
1090
|
-
verbosity: "normal"
|
|
1091
|
-
};
|
|
1092
|
-
var BRANCH_PRESETS = {
|
|
1093
|
-
1: "feature/{type}-{issue}-{slug}",
|
|
1094
|
-
2: "feature/{slug}",
|
|
1095
|
-
3: "feat/{slug}"
|
|
1096
|
-
};
|
|
1097
|
-
var COMMIT_PRESETS = {
|
|
1098
|
-
1: "conventional",
|
|
1099
|
-
2: "gitmoji",
|
|
1100
|
-
3: "plain"
|
|
1101
|
-
};
|
|
1102
|
-
function parseConfig(lines) {
|
|
1103
|
-
const map = new Map;
|
|
1104
|
-
for (const line of lines) {
|
|
1105
|
-
const t = line.trim();
|
|
1106
|
-
if (!t || t.startsWith("#"))
|
|
1107
|
-
continue;
|
|
1108
|
-
const eq = t.indexOf("=");
|
|
1109
|
-
if (eq === -1)
|
|
1110
|
-
continue;
|
|
1111
|
-
const k = t.slice(0, eq).trim();
|
|
1112
|
-
const v = t.slice(eq + 1).trim();
|
|
1113
|
-
if (!map.has(k))
|
|
1114
|
-
map.set(k, v);
|
|
1115
|
-
}
|
|
1116
|
-
return map;
|
|
1117
|
-
}
|
|
1118
|
-
function buildConfig(existing, updates) {
|
|
1119
|
-
const merged = new Map(existing);
|
|
1120
|
-
for (const [k, v] of Object.entries(updates))
|
|
1121
|
-
merged.set(k, v);
|
|
1122
|
-
const lines = [];
|
|
1123
|
-
for (const k of DEFAULT_ORDER) {
|
|
1124
|
-
const v = merged.get(k) ?? DEFAULTS[k];
|
|
1125
|
-
if (v === undefined)
|
|
1126
|
-
continue;
|
|
1127
|
-
lines.push(`${k}=${v}`);
|
|
1128
|
-
merged.delete(k);
|
|
1129
|
-
}
|
|
1130
|
-
for (const [k, v] of merged)
|
|
1131
|
-
lines.push(`${k}=${v}`);
|
|
1132
|
-
return lines.join(`
|
|
1133
|
-
`) + `
|
|
1134
|
-
`;
|
|
1135
|
-
}
|
|
1136
|
-
async function askFreeText(rl, prompt) {
|
|
1137
|
-
for (;; ) {
|
|
1138
|
-
const v = (await rl.question(prompt)).trim();
|
|
1139
|
-
if (v)
|
|
1140
|
-
return v;
|
|
1141
|
-
console.log(" Cannot be empty.");
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
function assertNotSymlink2(file) {
|
|
1145
|
-
if (!existsSync7(file))
|
|
1146
|
-
return;
|
|
1147
|
-
try {
|
|
1148
|
-
if (lstatSync2(file).isSymbolicLink())
|
|
1149
|
-
throw new Error(`refusing to follow symlink: ${file}`);
|
|
1150
|
-
} catch (e) {
|
|
1151
|
-
if (e.code === "ENOENT")
|
|
1152
|
-
return;
|
|
1153
|
-
throw e;
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
async function runOnboard(projectDir) {
|
|
1157
|
-
if (!process.stdin.isTTY) {
|
|
1158
|
-
throw new Error("Not a terminal. Run `mugiwara onboard` in a terminal, or edit .mugiwara/config directly.");
|
|
1159
|
-
}
|
|
1160
|
-
const mugiwaraDir = join8(projectDir, ".mugiwara");
|
|
1161
|
-
const configPath = join8(mugiwaraDir, "config");
|
|
1162
|
-
assertNotSymlink2(configPath);
|
|
1163
|
-
const existing = parseConfig(existsSync7(configPath) ? readFileSync6(configPath, "utf8").split(/\r?\n/) : []);
|
|
1164
|
-
const rl = createRl();
|
|
1165
|
-
try {
|
|
1166
|
-
console.log("");
|
|
1167
|
-
console.log("╔══════════════════════════════════════╗");
|
|
1168
|
-
console.log("║ Mugiwara Onboarding Wizard ║");
|
|
1169
|
-
console.log("╚══════════════════════════════════════╝");
|
|
1170
|
-
console.log("");
|
|
1171
|
-
if (existing.size > 0) {
|
|
1172
|
-
const redo = await confirm(rl, "Existing .mugiwara/config found. Re-onboard?");
|
|
1173
|
-
if (!redo) {
|
|
1174
|
-
console.log("Onboarding cancelled. Config preserved.");
|
|
1175
|
-
return;
|
|
1989
|
+
const fd = openSync(indexFile, "a");
|
|
1990
|
+
try {
|
|
1991
|
+
writeSync(fd, header + line);
|
|
1992
|
+
} finally {
|
|
1993
|
+
closeSync(fd);
|
|
1176
1994
|
}
|
|
1177
|
-
console.log("");
|
|
1178
|
-
}
|
|
1179
|
-
const branchChoice = await choose(rl, "Q1 — Branch naming pattern:", [
|
|
1180
|
-
"Trunk-based — feature/{type}-{issue}-{slug}",
|
|
1181
|
-
"GitFlow — feature/{slug}",
|
|
1182
|
-
"GitHub Flow — feat/{slug}",
|
|
1183
|
-
"Custom — enter your own pattern"
|
|
1184
|
-
]);
|
|
1185
|
-
let branch;
|
|
1186
|
-
if (branchChoice === 3) {
|
|
1187
|
-
branch = await askFreeText(rl, " Branch pattern (placeholders: {type} {issue} {slug}, e.g. {issue}-{slug}): ");
|
|
1188
|
-
} else {
|
|
1189
|
-
branch = BRANCH_PRESETS[branchChoice + 1];
|
|
1190
1995
|
}
|
|
1191
|
-
|
|
1192
|
-
"guided — ask before every wave transition",
|
|
1193
|
-
"semi — auto-advance through waves, pause on failures",
|
|
1194
|
-
"auto — full auto-pilot"
|
|
1195
|
-
]);
|
|
1196
|
-
const mode = ["guided", "semi", "auto"][modeIdx];
|
|
1197
|
-
const reviewIdx = await choose(rl, "Q3 — Code review depth:", [
|
|
1198
|
-
"full — breaking-change map, five-axis review, <=3 cycles",
|
|
1199
|
-
"standard — five-axis review, 1 cycle",
|
|
1200
|
-
"quick — diff-only, no caller-map"
|
|
1201
|
-
]);
|
|
1202
|
-
const reviewDepth = ["full", "standard", "quick"][reviewIdx];
|
|
1203
|
-
const qualityIdx = await choose(rl, "Q4 — Quality check depth:", [
|
|
1204
|
-
"full — format, lint, typecheck, test, build",
|
|
1205
|
-
"standard — lint, typecheck, test",
|
|
1206
|
-
"quick — test only"
|
|
1207
|
-
]);
|
|
1208
|
-
const qualityDepth = ["full", "standard", "quick"][qualityIdx];
|
|
1209
|
-
const covIdx = await choose(rl, "Q5 — Test coverage threshold:", [
|
|
1210
|
-
"90/80 — new code 90%, modified 80%",
|
|
1211
|
-
"80/70 — new code 80%, modified 70%",
|
|
1212
|
-
"custom — enter your own values",
|
|
1213
|
-
"none — 0/0, no coverage enforcement"
|
|
1214
|
-
]);
|
|
1215
|
-
let coverageNew = 90;
|
|
1216
|
-
let coverageModified = 80;
|
|
1217
|
-
if (covIdx === 1) {
|
|
1218
|
-
coverageNew = 80;
|
|
1219
|
-
coverageModified = 70;
|
|
1220
|
-
} else if (covIdx === 2) {
|
|
1221
|
-
coverageNew = Number.parseInt(await askFreeText(rl, " Coverage for new code (%): "), 10) || 0;
|
|
1222
|
-
coverageModified = Number.parseInt(await askFreeText(rl, " Coverage for modified code (%): "), 10) || 0;
|
|
1223
|
-
} else if (covIdx === 3) {
|
|
1224
|
-
coverageNew = 0;
|
|
1225
|
-
coverageModified = 0;
|
|
1226
|
-
}
|
|
1227
|
-
const commitChoice = await choose(rl, "Q6 — Commit style:", [
|
|
1228
|
-
"Conventional Commits — feat:, fix:, chore:, docs:",
|
|
1229
|
-
"Gitmoji — emoji-prefixed conventional",
|
|
1230
|
-
"Plain — no prefix, imperative sentence",
|
|
1231
|
-
"Custom template — enter your own (placeholders: {type} {issue} {title})"
|
|
1232
|
-
]);
|
|
1233
|
-
let commit;
|
|
1234
|
-
if (commitChoice === 3) {
|
|
1235
|
-
commit = await askFreeText(rl, " Commit template (e.g. {issue}: {title}): ");
|
|
1236
|
-
} else {
|
|
1237
|
-
commit = COMMIT_PRESETS[commitChoice + 1];
|
|
1238
|
-
}
|
|
1239
|
-
if (!existsSync7(mugiwaraDir))
|
|
1240
|
-
mkdirSync6(mugiwaraDir, { recursive: true });
|
|
1241
|
-
const body = buildConfig(existing, {
|
|
1242
|
-
mode,
|
|
1243
|
-
branch,
|
|
1244
|
-
commit,
|
|
1245
|
-
coverage_new: String(coverageNew),
|
|
1246
|
-
coverage_modified: String(coverageModified),
|
|
1247
|
-
review_depth: reviewDepth,
|
|
1248
|
-
quality_depth: qualityDepth
|
|
1249
|
-
});
|
|
1250
|
-
writeFileSync5(configPath, `# .mugiwara/config — written by mugiwara onboard on ${new Date().toISOString().slice(0, 10)}
|
|
1251
|
-
${body}`);
|
|
1252
|
-
console.log("╔══════════════════════════════════════╗");
|
|
1253
|
-
console.log("║ Onboarding Complete ║");
|
|
1254
|
-
console.log("╚══════════════════════════════════════╝");
|
|
1255
|
-
console.log("");
|
|
1256
|
-
console.log(` Branch pattern: ${branch}`);
|
|
1257
|
-
console.log(` Autonomy mode: ${mode}`);
|
|
1258
|
-
console.log(` Review depth: ${reviewDepth}`);
|
|
1259
|
-
console.log(` Quality depth: ${qualityDepth}`);
|
|
1260
|
-
console.log(` Coverage: ${coverageNew}/${coverageModified}`);
|
|
1261
|
-
console.log(` Commit style: ${commit}`);
|
|
1262
|
-
console.log("");
|
|
1263
|
-
console.log(` Config written: ${configPath}`);
|
|
1264
|
-
console.log("");
|
|
1265
|
-
} finally {
|
|
1266
|
-
rl.close();
|
|
1996
|
+
index = "index.md";
|
|
1267
1997
|
}
|
|
1998
|
+
return { report: join16("missions", mission, "report.md"), removed, kept, index };
|
|
1268
1999
|
}
|
|
1269
2000
|
|
|
1270
2001
|
// src/run.ts
|
|
1271
|
-
import { existsSync as
|
|
2002
|
+
import { existsSync as existsSync12, readdirSync as readdirSync8 } from "node:fs";
|
|
1272
2003
|
import { spawnSync } from "node:child_process";
|
|
1273
|
-
import { dirname as dirname5, join as
|
|
2004
|
+
import { dirname as dirname5, join as join17, resolve } from "node:path";
|
|
1274
2005
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1275
2006
|
var here3 = dirname5(fileURLToPath4(import.meta.url));
|
|
1276
|
-
var SCRIPTS_DIR =
|
|
1277
|
-
var RUNNABLE = ["savepoint.sh", "lane.sh"
|
|
2007
|
+
var SCRIPTS_DIR = join17(here3, "..", "scripts");
|
|
2008
|
+
var RUNNABLE = ["savepoint.sh", "lane.sh"];
|
|
1278
2009
|
function findBash() {
|
|
1279
2010
|
const explicit = process.env.MUGIWARA_BASH?.trim();
|
|
1280
2011
|
if (explicit)
|
|
1281
|
-
return
|
|
2012
|
+
return existsSync12(explicit) ? explicit : null;
|
|
1282
2013
|
const candidates = process.platform === "win32" ? [
|
|
1283
2014
|
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
1284
2015
|
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
1285
|
-
|
|
2016
|
+
join17(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
|
|
1286
2017
|
] : ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash", "/opt/homebrew/bin/bash"];
|
|
1287
2018
|
for (const p of candidates) {
|
|
1288
|
-
if (p &&
|
|
2019
|
+
if (p && existsSync12(p))
|
|
1289
2020
|
return p;
|
|
1290
2021
|
}
|
|
1291
2022
|
const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bash"], {
|
|
@@ -1302,9 +2033,9 @@ function runScript(name, args, projectDir) {
|
|
|
1302
2033
|
if (!/^[a-z0-9-]+\.sh$/.test(name)) {
|
|
1303
2034
|
throw new Error(`invalid script name "${name}" (expected one of: ${RUNNABLE.join(", ")})`);
|
|
1304
2035
|
}
|
|
1305
|
-
const script =
|
|
1306
|
-
if (!
|
|
1307
|
-
const have =
|
|
2036
|
+
const script = join17(SCRIPTS_DIR, name);
|
|
2037
|
+
if (!existsSync12(script)) {
|
|
2038
|
+
const have = existsSync12(SCRIPTS_DIR) ? readdirSync8(SCRIPTS_DIR).filter((f) => f.endsWith(".sh")) : [];
|
|
1308
2039
|
throw new Error(`script not found: ${name}${have.length ? ` (available: ${have.join(", ")})` : ""}`);
|
|
1309
2040
|
}
|
|
1310
2041
|
const bash = findBash();
|
|
@@ -1321,9 +2052,9 @@ function runScript(name, args, projectDir) {
|
|
|
1321
2052
|
}
|
|
1322
2053
|
|
|
1323
2054
|
// src/continue.ts
|
|
1324
|
-
import { existsSync as
|
|
1325
|
-
import { execFileSync } from "node:child_process";
|
|
1326
|
-
import { join as
|
|
2055
|
+
import { existsSync as existsSync13, readFileSync as readFileSync12, readdirSync as readdirSync9 } from "node:fs";
|
|
2056
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
2057
|
+
import { join as join18 } from "node:path";
|
|
1327
2058
|
var SAFE = /^[A-Za-z0-9._-]+$/;
|
|
1328
2059
|
var isSafeKey = (s) => SAFE.test(s) && !/^\.+$/.test(s);
|
|
1329
2060
|
var num = (v) => {
|
|
@@ -1338,34 +2069,46 @@ function gitActor(cwd) {
|
|
|
1338
2069
|
const author = (process.env.GIT_AUTHOR_NAME ?? "").trim();
|
|
1339
2070
|
if (author)
|
|
1340
2071
|
return author;
|
|
1341
|
-
const
|
|
2072
|
+
const git3 = (args) => {
|
|
1342
2073
|
try {
|
|
1343
|
-
return
|
|
2074
|
+
return execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1344
2075
|
} catch {
|
|
1345
2076
|
return "";
|
|
1346
2077
|
}
|
|
1347
2078
|
};
|
|
1348
|
-
const name =
|
|
1349
|
-
const email =
|
|
2079
|
+
const name = git3(["config", "user.name"]);
|
|
2080
|
+
const email = git3(["config", "user.email"]);
|
|
1350
2081
|
if (name && email)
|
|
1351
2082
|
return `${name} <${email}>`;
|
|
1352
2083
|
return name || process.env.USER || process.env.USERNAME || "";
|
|
1353
2084
|
}
|
|
1354
|
-
function scan(projectDir,
|
|
1355
|
-
const base =
|
|
1356
|
-
if (!
|
|
2085
|
+
function scan(projectDir, kind, map) {
|
|
2086
|
+
const base = join18(projectDir, ".mugiwara", "missions");
|
|
2087
|
+
if (!existsSync13(base))
|
|
1357
2088
|
return [];
|
|
1358
2089
|
const out = [];
|
|
1359
|
-
const missions =
|
|
2090
|
+
const missions = readdirSync9(base, { withFileTypes: true }).filter((e) => e.isDirectory() && isSafeKey(e.name)).map((e) => e.name).sort();
|
|
1360
2091
|
for (const mission of missions) {
|
|
1361
|
-
const dir =
|
|
1362
|
-
for (const f of
|
|
2092
|
+
const dir = join18(base, mission);
|
|
2093
|
+
for (const f of readdirSync9(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
1363
2094
|
const stem = f.slice(0, -".json".length);
|
|
1364
|
-
|
|
2095
|
+
let member;
|
|
2096
|
+
if (kind === "state") {
|
|
2097
|
+
if (stem === "continue" || stem.startsWith("continue-"))
|
|
2098
|
+
continue;
|
|
2099
|
+
member = stem === "state" ? null : stem;
|
|
2100
|
+
} else {
|
|
2101
|
+
if (stem === "continue")
|
|
2102
|
+
member = null;
|
|
2103
|
+
else if (stem.startsWith("continue-"))
|
|
2104
|
+
member = stem.slice("continue-".length);
|
|
2105
|
+
else
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
1365
2108
|
if (member !== null && !isSafeKey(member))
|
|
1366
2109
|
continue;
|
|
1367
2110
|
try {
|
|
1368
|
-
const raw = JSON.parse(
|
|
2111
|
+
const raw = JSON.parse(readFileSync12(join18(dir, f), "utf8"));
|
|
1369
2112
|
if (text(raw.mission) !== mission)
|
|
1370
2113
|
continue;
|
|
1371
2114
|
out.push(map(raw, member));
|
|
@@ -1407,8 +2150,10 @@ function readState(projectDir) {
|
|
|
1407
2150
|
lane_rose: r.lane_rose === true,
|
|
1408
2151
|
lane_prev: text(r.lane_prev),
|
|
1409
2152
|
lane_peak: text(r.lane_peak),
|
|
1410
|
-
|
|
1411
|
-
|
|
2153
|
+
base_sha: text(r.base_sha),
|
|
2154
|
+
sensitive_paths: Array.isArray(r.sensitive_paths) ? r.sensitive_paths.map(text).filter(Boolean) : [],
|
|
2155
|
+
next_action: text(r.next_action),
|
|
2156
|
+
next_session_prompt: text(r.next_session_prompt),
|
|
1412
2157
|
updated_at: text(r.updated_at),
|
|
1413
2158
|
blockers_open: num(r.blockers_open),
|
|
1414
2159
|
heal_cycle: num(r.heal_cycle),
|
|
@@ -1478,6 +2223,221 @@ function formatResume(e) {
|
|
|
1478
2223
|
return `Resumed: ${e.mission}${scope}, Flow ${e.flow}, ${e.tasks_done}/${e.tasks_total} tasks — next_action: ${e.next_action} — run: ${next}`;
|
|
1479
2224
|
}
|
|
1480
2225
|
|
|
2226
|
+
// src/sign.ts
|
|
2227
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
2228
|
+
import { chmodSync as chmodSync2, existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
|
|
2229
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
|
|
2230
|
+
import { homedir as homedir3 } from "node:os";
|
|
2231
|
+
import { join as join19 } from "node:path";
|
|
2232
|
+
function signArgs(reportPath, secretKey) {
|
|
2233
|
+
return ["-Sm", reportPath, "-s", secretKey];
|
|
2234
|
+
}
|
|
2235
|
+
function verifyArgs(reportPath, pubKey) {
|
|
2236
|
+
return pubKey ? ["-Vm", reportPath, "-p", pubKey] : ["-Vm", reportPath];
|
|
2237
|
+
}
|
|
2238
|
+
function hasMinisign() {
|
|
2239
|
+
try {
|
|
2240
|
+
execFileSync5("minisign", ["-v"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
2241
|
+
return true;
|
|
2242
|
+
} catch {
|
|
2243
|
+
return false;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
function defaultKey(flag) {
|
|
2247
|
+
return join19(homedir3(), ".mugiwara", flag === "secret" ? "minisign.key" : "minisign.pub");
|
|
2248
|
+
}
|
|
2249
|
+
function generatePureKey() {
|
|
2250
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
2251
|
+
const privJwk = privateKey.export({ format: "jwk" });
|
|
2252
|
+
const pubJwk = publicKey.export({ format: "jwk" });
|
|
2253
|
+
return {
|
|
2254
|
+
key: Buffer.from(privJwk.d, "base64url").toString("base64"),
|
|
2255
|
+
pub: Buffer.from(pubJwk.x, "base64url").toString("base64")
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
function ensurePureKey(homeDir) {
|
|
2259
|
+
const dir = join19(homeDir, ".mugiwara");
|
|
2260
|
+
const keyPath = join19(dir, "mugiwara.key");
|
|
2261
|
+
const pubPath = join19(dir, "mugiwara.pub");
|
|
2262
|
+
if (!existsSync14(keyPath) || !existsSync14(pubPath)) {
|
|
2263
|
+
mkdirSync9(dir, { recursive: true });
|
|
2264
|
+
const { key, pub } = generatePureKey();
|
|
2265
|
+
if (!existsSync14(keyPath)) {
|
|
2266
|
+
writeFileSync9(keyPath, key + `
|
|
2267
|
+
`, { mode: 384 });
|
|
2268
|
+
}
|
|
2269
|
+
if (!existsSync14(pubPath))
|
|
2270
|
+
writeFileSync9(pubPath, pub + `
|
|
2271
|
+
`);
|
|
2272
|
+
}
|
|
2273
|
+
try {
|
|
2274
|
+
chmodSync2(keyPath, 384);
|
|
2275
|
+
} catch {}
|
|
2276
|
+
return dir;
|
|
2277
|
+
}
|
|
2278
|
+
function pureSign(content, seedBase64, opts) {
|
|
2279
|
+
const seed = Buffer.from(seedBase64.trim(), "base64");
|
|
2280
|
+
if (seed.length !== 32)
|
|
2281
|
+
return { ok: false, message: "invalid seed (want 32B base64)" };
|
|
2282
|
+
const pubBuf = Buffer.from(opts.pub.trim(), "base64");
|
|
2283
|
+
if (pubBuf.length !== 32)
|
|
2284
|
+
return { ok: false, message: "invalid pub (want 32B base64)" };
|
|
2285
|
+
const privateKey = createPrivateKey({
|
|
2286
|
+
key: {
|
|
2287
|
+
kty: "OKP",
|
|
2288
|
+
crv: "Ed25519",
|
|
2289
|
+
d: seed.toString("base64url"),
|
|
2290
|
+
x: pubBuf.toString("base64url")
|
|
2291
|
+
},
|
|
2292
|
+
format: "jwk"
|
|
2293
|
+
});
|
|
2294
|
+
const sig = sign(null, Buffer.from(content, "utf8"), privateKey).toString("base64");
|
|
2295
|
+
const out = { algo: "ed25519-pure", sig, pub: opts.pub, mission: opts.mission, commit: opts.commit, ts: opts.ts };
|
|
2296
|
+
if (opts.outputPath)
|
|
2297
|
+
writeFileSync9(opts.outputPath, JSON.stringify(out, null, 2) + `
|
|
2298
|
+
`);
|
|
2299
|
+
return { ...out, ok: true };
|
|
2300
|
+
}
|
|
2301
|
+
function pureVerify(content, sig) {
|
|
2302
|
+
try {
|
|
2303
|
+
const pub = Buffer.from(sig.pub, "base64");
|
|
2304
|
+
if (pub.length !== 32)
|
|
2305
|
+
return false;
|
|
2306
|
+
const publicKey = createPublicKey({
|
|
2307
|
+
key: { kty: "OKP", crv: "Ed25519", x: pub.toString("base64url") },
|
|
2308
|
+
format: "jwk"
|
|
2309
|
+
});
|
|
2310
|
+
return verify(null, Buffer.from(content, "utf8"), publicKey, Buffer.from(sig.sig, "base64"));
|
|
2311
|
+
} catch {
|
|
2312
|
+
return false;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
function resolveBackend(configured, env) {
|
|
2316
|
+
switch (configured) {
|
|
2317
|
+
case "off":
|
|
2318
|
+
return "off";
|
|
2319
|
+
case "minisign":
|
|
2320
|
+
return env.hasMinisign ? "minisign" : "minisign-fail";
|
|
2321
|
+
case "pure":
|
|
2322
|
+
return "pure";
|
|
2323
|
+
case "auto":
|
|
2324
|
+
default:
|
|
2325
|
+
return env.hasMinisign && env.hasKey ? "minisign" : "pure";
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
function configuredBackend(projectDir) {
|
|
2329
|
+
return readConfig(projectDir).sign;
|
|
2330
|
+
}
|
|
2331
|
+
function missionMeta(projectDir, mission) {
|
|
2332
|
+
let commit = "unknown";
|
|
2333
|
+
try {
|
|
2334
|
+
commit = execFileSync5("git", ["rev-parse", "HEAD"], { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
2335
|
+
} catch {}
|
|
2336
|
+
return { commit, ts: new Date().toISOString() };
|
|
2337
|
+
}
|
|
2338
|
+
function signReport(projectDir, missionDir) {
|
|
2339
|
+
const report = join19(missionDir, "report.md");
|
|
2340
|
+
if (!existsSync14(report))
|
|
2341
|
+
return { ok: false, message: "no report.md to sign — archive first" };
|
|
2342
|
+
const mission = missionDir.split(join19(".mugiwara", "missions", "")).pop() ?? "unknown";
|
|
2343
|
+
const backend = resolveBackend(configuredBackend(projectDir), { hasMinisign: hasMinisign(), hasKey: existsSync14(defaultKey("secret")) });
|
|
2344
|
+
if (backend === "off")
|
|
2345
|
+
return { ok: false, message: "signing disabled (sign=off)" };
|
|
2346
|
+
if (backend === "minisign-fail")
|
|
2347
|
+
return { ok: false, message: "sign=minisign but minisign not installed — install it or set sign=pure" };
|
|
2348
|
+
if (backend === "minisign") {
|
|
2349
|
+
const secretKey = process.env.MUGIWARA_SIGN_KEY?.trim() || defaultKey("secret");
|
|
2350
|
+
try {
|
|
2351
|
+
execFileSync5("minisign", signArgs(report, secretKey), { cwd: projectDir, stdio: "pipe", input: process.env.MUGIWARA_SIGN_PASSWORD ?? "" });
|
|
2352
|
+
return { ok: true, message: `signed ${report}.minisig (minisign, key: ${secretKey})` };
|
|
2353
|
+
} catch (e) {
|
|
2354
|
+
return { ok: false, message: `signing failed: ${e.message}` };
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
const dir = ensurePureKey(homedir3());
|
|
2358
|
+
const seed = process.env.MUGIWARA_SIGN_KEY?.trim() || readFileSyncSafe2(join19(dir, "mugiwara.key"));
|
|
2359
|
+
const pub = process.env.MUGIWARA_SIGN_PUB?.trim() || readFileSyncSafe2(join19(dir, "mugiwara.pub"));
|
|
2360
|
+
if (!seed || !pub)
|
|
2361
|
+
return { ok: false, message: "pure keys missing — run `mugiwara sign --gen-key --backend pure`" };
|
|
2362
|
+
const content = readFileSafe(report);
|
|
2363
|
+
if (content === null)
|
|
2364
|
+
return { ok: false, message: `cannot read ${report}` };
|
|
2365
|
+
const { commit, ts } = missionMeta(projectDir, mission);
|
|
2366
|
+
const sig = pureSign(content, seed, { mission, commit, ts, pub, outputPath: `${report}.mugisig` });
|
|
2367
|
+
if (!sig.ok)
|
|
2368
|
+
return { ok: false, message: `signing failed: ${sig.message}` };
|
|
2369
|
+
return { ok: true, message: `signed ${report}.mugisig (pure ed25519, key: ${join19(dir, "mugiwara.key")})` };
|
|
2370
|
+
}
|
|
2371
|
+
function verifyReport(projectDir, missionDir) {
|
|
2372
|
+
const report = join19(missionDir, "report.md");
|
|
2373
|
+
const minisig = `${report}.minisig`;
|
|
2374
|
+
const mugisig = `${report}.mugisig`;
|
|
2375
|
+
if (!existsSync14(minisig) && !existsSync14(mugisig)) {
|
|
2376
|
+
return { ok: false, message: "not signed (no .minisig or .mugisig beside report.md)" };
|
|
2377
|
+
}
|
|
2378
|
+
if (existsSync14(minisig)) {
|
|
2379
|
+
if (!hasMinisign())
|
|
2380
|
+
return { ok: false, message: "minisig present but minisign not installed — cannot verify that signature" };
|
|
2381
|
+
const pubKey = existsSync14(defaultKey("public")) ? defaultKey("public") : null;
|
|
2382
|
+
try {
|
|
2383
|
+
execFileSync5("minisign", verifyArgs(report, pubKey), { cwd: projectDir, stdio: "pipe" });
|
|
2384
|
+
return { ok: true, message: "signature verifies against report.md (minisig)" };
|
|
2385
|
+
} catch {
|
|
2386
|
+
return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (minisig)" };
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
try {
|
|
2390
|
+
const parsed = JSON.parse(readFileSafe(mugisig) ?? "{}");
|
|
2391
|
+
const content = readFileSafe(report);
|
|
2392
|
+
if (content === null || parsed.algo !== "ed25519-pure")
|
|
2393
|
+
return { ok: false, message: "invalid .mugisig file" };
|
|
2394
|
+
return pureVerify(content, parsed) ? { ok: true, message: "signature verifies against report.md (mugisig, ed25519-pure)" } : { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (mugisig)" };
|
|
2395
|
+
} catch {
|
|
2396
|
+
return { ok: false, message: "invalid .mugisig file" };
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
function readFileSafe(p) {
|
|
2400
|
+
try {
|
|
2401
|
+
return readFileSync13(p, "utf8");
|
|
2402
|
+
} catch {
|
|
2403
|
+
return null;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
function readFileSyncSafe2(p) {
|
|
2407
|
+
try {
|
|
2408
|
+
return readFileSync13(p, "utf8").trim();
|
|
2409
|
+
} catch {
|
|
2410
|
+
return "";
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// src/slop.ts
|
|
2415
|
+
function detectHealingSlop(input) {
|
|
2416
|
+
const kind = "healing";
|
|
2417
|
+
const max = input.max_cycles ?? 3;
|
|
2418
|
+
const hasZeroHistory = input.history_fixes.some((n) => n === 0);
|
|
2419
|
+
if (input.fixes_in_cycle === 0 && hasZeroHistory) {
|
|
2420
|
+
return { slop: true, reason: `slop: healing — no fixes in cycle ${input.cycle} with previous zero-fix cycle`, kind };
|
|
2421
|
+
}
|
|
2422
|
+
if (input.cycle >= max && input.fixes_in_cycle === 0) {
|
|
2423
|
+
return { slop: true, reason: `slop: healing — cycle ${input.cycle} ≥ max ${max} with no fixes`, kind };
|
|
2424
|
+
}
|
|
2425
|
+
return { slop: false, reason: "no slop — healing making progress", kind };
|
|
2426
|
+
}
|
|
2427
|
+
function computeLiveSlop(input) {
|
|
2428
|
+
const rows = [];
|
|
2429
|
+
const thr = input.repeated_read_threshold ?? 3;
|
|
2430
|
+
const heal = detectHealingSlop({ cycle: input.heal_cycle, fixes_in_cycle: 0, history_fixes: [], max_cycles: input.max_heal_cycles ?? 3 });
|
|
2431
|
+
if (heal.slop)
|
|
2432
|
+
rows.push({ role: "Brook", kind: "healing", reason: heal.reason });
|
|
2433
|
+
if (input.repeated_reads >= thr)
|
|
2434
|
+
rows.push({ role: "all", kind: "context", reason: `repeated reads ${input.repeated_reads} ≥ ${thr}` });
|
|
2435
|
+
const perRole = {};
|
|
2436
|
+
for (const r of rows)
|
|
2437
|
+
perRole[r.role] = (perRole[r.role] ?? 0) + 1;
|
|
2438
|
+
return { interventions: rows.length, perRole, rows };
|
|
2439
|
+
}
|
|
2440
|
+
|
|
1481
2441
|
// src/cli.ts
|
|
1482
2442
|
var str = (v) => typeof v === "string" ? v : undefined;
|
|
1483
2443
|
var flag = (v) => v === true;
|
|
@@ -1489,6 +2449,16 @@ async function run(argv) {
|
|
|
1489
2449
|
console.log(`mugiwara ${VERSION}`);
|
|
1490
2450
|
return;
|
|
1491
2451
|
}
|
|
2452
|
+
if (command === "continue" || command === "status") {
|
|
2453
|
+
return command === "continue" ? continueCmd(flags, _) : statusCmd(flags);
|
|
2454
|
+
}
|
|
2455
|
+
const isDryRunInstall = (command === "install" || command === "update") && flag(flags.dryRun);
|
|
2456
|
+
if (!isDryRunInstall) {
|
|
2457
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2458
|
+
if (ensureConfig(projectDir)) {
|
|
2459
|
+
console.log(`default .mugiwara/config written at ${join20(projectDir, ".mugiwara", "config")} (edit it to customise)`);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
1492
2462
|
switch (command) {
|
|
1493
2463
|
case "install":
|
|
1494
2464
|
return install(flags);
|
|
@@ -1502,20 +2472,24 @@ async function run(argv) {
|
|
|
1502
2472
|
return resetCmd(flags);
|
|
1503
2473
|
case "archive":
|
|
1504
2474
|
return archive(flags, _);
|
|
1505
|
-
case "
|
|
1506
|
-
return
|
|
2475
|
+
case "clean":
|
|
2476
|
+
return cleanCmd(flags);
|
|
1507
2477
|
case "continue":
|
|
1508
2478
|
return continueCmd(flags, _);
|
|
1509
2479
|
case "status":
|
|
1510
2480
|
return statusCmd(flags);
|
|
2481
|
+
case "cost":
|
|
2482
|
+
return costCmd(flags, _);
|
|
1511
2483
|
case "run":
|
|
1512
2484
|
return runCmd(flags, _);
|
|
1513
2485
|
case "savepoint":
|
|
1514
2486
|
return runCmd(flags, ["run", "savepoint.sh", ..._.slice(1)]);
|
|
1515
|
-
case "
|
|
1516
|
-
return
|
|
1517
|
-
case "
|
|
1518
|
-
return
|
|
2487
|
+
case "blame":
|
|
2488
|
+
return blameCmd(flags, _);
|
|
2489
|
+
case "handoff":
|
|
2490
|
+
return handoffCmd(flags, _);
|
|
2491
|
+
case "sign":
|
|
2492
|
+
return signCmd(flags, _);
|
|
1519
2493
|
default:
|
|
1520
2494
|
throw new Error(`Unknown command: ${command}`);
|
|
1521
2495
|
}
|
|
@@ -1545,6 +2519,8 @@ function archive(flags, positionals) {
|
|
|
1545
2519
|
const result = archiveMission(projectDir, mission, { dryRun: flag(flags.dryRun) });
|
|
1546
2520
|
if (result.report)
|
|
1547
2521
|
console.log(`archive target: ${result.report}`);
|
|
2522
|
+
else
|
|
2523
|
+
console.error(`no mission dir for "${mission}" under .mugiwara/missions/`);
|
|
1548
2524
|
if (result.removed.length)
|
|
1549
2525
|
console.log(`${flag(flags.dryRun) ? "would remove" : "removed"}: ${result.removed.join(", ")}`);
|
|
1550
2526
|
if (result.kept.length)
|
|
@@ -1552,11 +2528,59 @@ function archive(flags, positionals) {
|
|
|
1552
2528
|
if (result.index)
|
|
1553
2529
|
console.log(`index updated: ${result.index}`);
|
|
1554
2530
|
}
|
|
1555
|
-
|
|
2531
|
+
function cleanCmd(flags) {
|
|
1556
2532
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
2533
|
+
const dryRun = flag(flags.dryRun);
|
|
2534
|
+
const root = join20(projectDir, ".mugiwara", "missions");
|
|
2535
|
+
if (!existsSync15(root)) {
|
|
2536
|
+
console.log("nothing to clean (.mugiwara/missions/ does not exist).");
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
const before = str(flags.before);
|
|
2540
|
+
const beforeMs = before ? Date.parse(before) : NaN;
|
|
2541
|
+
if (before && !Number.isFinite(beforeMs)) {
|
|
2542
|
+
console.error(`invalid --before date: ${before}`);
|
|
2543
|
+
process.exit(1);
|
|
2544
|
+
}
|
|
2545
|
+
let candidates = readdirSync10(root, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Za-z0-9._-]+$/.test(e.name) && !/^\.+$/.test(e.name)).map((e) => e.name);
|
|
2546
|
+
const stateFiles = (m) => readdirSync10(join20(root, m)).filter((f) => {
|
|
2547
|
+
const stem = f.replace(/\.json$/, "");
|
|
2548
|
+
return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
|
|
2549
|
+
});
|
|
2550
|
+
const hasLiveState = (m) => stateFiles(m).length > 0;
|
|
2551
|
+
const staleBefore = (m) => {
|
|
2552
|
+
if (!Number.isFinite(beforeMs))
|
|
2553
|
+
return false;
|
|
2554
|
+
for (const f of stateFiles(m)) {
|
|
2555
|
+
try {
|
|
2556
|
+
const ts = Date.parse(JSON.parse(readFileSync14(join20(root, m, f), "utf8")).updated_at ?? "") || 0;
|
|
2557
|
+
if (ts === 0 || ts >= beforeMs)
|
|
2558
|
+
return false;
|
|
2559
|
+
} catch {
|
|
2560
|
+
return false;
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
return true;
|
|
2564
|
+
};
|
|
2565
|
+
if (!flag(flags.all)) {
|
|
2566
|
+
candidates = candidates.filter((m) => existsSync15(join20(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
|
|
2567
|
+
} else if (!flag(flags.force)) {
|
|
2568
|
+
const live = candidates.filter((m) => hasLiveState(m) && !staleBefore(m));
|
|
2569
|
+
if (live.length) {
|
|
2570
|
+
console.error(`✗ in-flight mission(s): ${live.join(", ")}. Use --force to archive them anyway.`);
|
|
2571
|
+
process.exit(1);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
if (!candidates.length) {
|
|
2575
|
+
console.log("nothing to clean.");
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
for (const m of candidates) {
|
|
2579
|
+
const r = archiveMission(projectDir, m, { dryRun });
|
|
2580
|
+
console.log(`${dryRun ? "would clean" : "cleaned"} ${m}${r.report ? ` → ${r.report}` : ""}`);
|
|
2581
|
+
if (r.index)
|
|
2582
|
+
console.log(`index updated: ${r.index}`);
|
|
2583
|
+
}
|
|
1560
2584
|
}
|
|
1561
2585
|
async function resolveOptions(flags) {
|
|
1562
2586
|
const interactive = !flag(flags.yes);
|
|
@@ -1573,7 +2597,7 @@ async function resolveOptions(flags) {
|
|
|
1573
2597
|
scope = await choose(rl, "Install scope?", ["global (user-wide)", "project (this repo)"]) === 0 ? "global" : "project";
|
|
1574
2598
|
}
|
|
1575
2599
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1576
|
-
if (scope === "project" && !
|
|
2600
|
+
if (scope === "project" && !existsSync15(projectDir))
|
|
1577
2601
|
throw new Error(`Project dir not found: ${projectDir}`);
|
|
1578
2602
|
let targetIds = str(flags.target)?.split(",").map((s) => s.trim()) ?? null;
|
|
1579
2603
|
if (targetIds && targetIds.includes("all"))
|
|
@@ -1598,7 +2622,7 @@ async function resolveOptions(flags) {
|
|
|
1598
2622
|
}
|
|
1599
2623
|
async function install(flags) {
|
|
1600
2624
|
const { scope, projectDir, targetIds } = await resolveOptions(flags);
|
|
1601
|
-
const home =
|
|
2625
|
+
const home = homedir4();
|
|
1602
2626
|
const allFiles = [];
|
|
1603
2627
|
const allNotes = [];
|
|
1604
2628
|
const installed = [];
|
|
@@ -1640,34 +2664,19 @@ Dry run — nothing written.`);
|
|
|
1640
2664
|
OK mugiwara ${VERSION} installed (manifest: ${file})`);
|
|
1641
2665
|
if (allNotes.length)
|
|
1642
2666
|
console.log(`${allNotes.length} note(s) above may need attention.`);
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
let mode = "guided", enforce = "block";
|
|
1646
|
-
if (existsSync10(cfg)) {
|
|
1647
|
-
for (const line of readFileSync8(cfg, "utf8").split(/\r?\n/)) {
|
|
1648
|
-
const [k, v] = line.split("=").map((s) => s.trim());
|
|
1649
|
-
if (k === "mode")
|
|
1650
|
-
mode = v;
|
|
1651
|
-
if (k === "enforce")
|
|
1652
|
-
enforce = v;
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
console.log(`
|
|
1656
|
-
Next: run \`mugiwara onboard\` to customise (mode=${mode}, enforce=${enforce} now).`);
|
|
1657
|
-
} else {
|
|
1658
|
-
console.log("\nNext: run `mugiwara onboard` in a project to write its .mugiwara/config.");
|
|
1659
|
-
}
|
|
2667
|
+
console.log(`
|
|
2668
|
+
Next: edit .mugiwara/config to customise (mode, branch, coverage, depths).`);
|
|
1660
2669
|
}
|
|
1661
2670
|
async function uninstall(flags) {
|
|
1662
2671
|
const scope = flag(flags.global) ? "global" : "project";
|
|
1663
2672
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1664
|
-
const home =
|
|
2673
|
+
const home = homedir4();
|
|
1665
2674
|
const file = manifestPath({ scope, projectDir, home });
|
|
1666
2675
|
const manifest = readManifest(file);
|
|
1667
2676
|
if (!manifest) {
|
|
1668
2677
|
if (scope === "project") {
|
|
1669
2678
|
const globalFile = manifestPath({ scope: "global", projectDir, home });
|
|
1670
|
-
if (
|
|
2679
|
+
if (existsSync15(globalFile)) {
|
|
1671
2680
|
console.log(`No project manifest found, but a global install exists. Try:
|
|
1672
2681
|
mugiwara uninstall --global`);
|
|
1673
2682
|
return;
|
|
@@ -1704,13 +2713,13 @@ async function uninstall(flags) {
|
|
|
1704
2713
|
console.log(` note: ${n}`);
|
|
1705
2714
|
}
|
|
1706
2715
|
rmSync3(file);
|
|
1707
|
-
const mugiDir =
|
|
1708
|
-
if (
|
|
2716
|
+
const mugiDir = join20(scope === "global" ? home : projectDir, ".mugiwara");
|
|
2717
|
+
if (existsSync15(mugiDir) && readdirSync10(mugiDir).length === 0) {
|
|
1709
2718
|
rmSync3(mugiDir, { recursive: true, force: true });
|
|
1710
2719
|
}
|
|
1711
2720
|
if (manifest.targets.includes("opencode")) {
|
|
1712
|
-
const opencodeCache =
|
|
1713
|
-
if (
|
|
2721
|
+
const opencodeCache = join20(home, ".cache", "opencode", "packages", "@ionivetech");
|
|
2722
|
+
if (existsSync15(opencodeCache)) {
|
|
1714
2723
|
rmSync3(opencodeCache, { recursive: true, force: true });
|
|
1715
2724
|
console.log(" cleared opencode npm cache (stale plugin versions)");
|
|
1716
2725
|
}
|
|
@@ -1719,7 +2728,7 @@ async function uninstall(flags) {
|
|
|
1719
2728
|
console.log(`OK removed ${removed.length} files`);
|
|
1720
2729
|
}
|
|
1721
2730
|
function list(flags) {
|
|
1722
|
-
const home =
|
|
2731
|
+
const home = homedir4();
|
|
1723
2732
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1724
2733
|
let found = false;
|
|
1725
2734
|
for (const [label, file] of [
|
|
@@ -1731,7 +2740,7 @@ function list(flags) {
|
|
|
1731
2740
|
continue;
|
|
1732
2741
|
found = true;
|
|
1733
2742
|
if (flag(flags.check)) {
|
|
1734
|
-
const missing = m.files.filter((f) => !
|
|
2743
|
+
const missing = m.files.filter((f) => !existsSync15(f));
|
|
1735
2744
|
console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
|
|
1736
2745
|
} else {
|
|
1737
2746
|
console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} installed=${m.installedAt}`);
|
|
@@ -1753,6 +2762,10 @@ function continueCmd(flags, positionals) {
|
|
|
1753
2762
|
const r = resolveContinue(entries, mission, member);
|
|
1754
2763
|
if (r.kind === "resume") {
|
|
1755
2764
|
console.log(formatResume(r.entry));
|
|
2765
|
+
const st = readState(projectDir).find((s) => s.mission === r.entry.mission && s.member === r.entry.member);
|
|
2766
|
+
const stale = st ? stalenessLine(projectDir, st.base_sha) : null;
|
|
2767
|
+
if (stale)
|
|
2768
|
+
console.log(stale);
|
|
1756
2769
|
return;
|
|
1757
2770
|
}
|
|
1758
2771
|
if (r.kind === "none") {
|
|
@@ -1797,6 +2810,58 @@ function statusCmd(flags) {
|
|
|
1797
2810
|
console.log(` evidence: ${s.evidence.join(", ")}`);
|
|
1798
2811
|
}
|
|
1799
2812
|
}
|
|
2813
|
+
function costCmd(flags, positionals) {
|
|
2814
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2815
|
+
const mission = str(flags.mission) ?? positionals[1] ?? (() => {
|
|
2816
|
+
const states2 = readState(projectDir);
|
|
2817
|
+
if (states2.length === 1)
|
|
2818
|
+
return states2[0].mission;
|
|
2819
|
+
if (states2.length > 1) {
|
|
2820
|
+
console.error("multiple missions in flight — specify --mission <id>");
|
|
2821
|
+
process.exit(1);
|
|
2822
|
+
}
|
|
2823
|
+
return null;
|
|
2824
|
+
})();
|
|
2825
|
+
if (!mission) {
|
|
2826
|
+
console.error("usage: mugiwara cost [--mission <id>] [--json] [--ledger] [--project <dir>]");
|
|
2827
|
+
process.exit(1);
|
|
2828
|
+
}
|
|
2829
|
+
const missionDir = join20(projectDir, ".mugiwara", "missions", mission);
|
|
2830
|
+
if (!existsSync15(missionDir)) {
|
|
2831
|
+
console.error(`No cost ledger found for mission "${mission}"`);
|
|
2832
|
+
process.exit(1);
|
|
2833
|
+
}
|
|
2834
|
+
const states = readState(projectDir).filter((s) => s.mission === mission);
|
|
2835
|
+
const envelope = states.length ? costEnvelope({ lane: states[0].lane, budget: states[0].budget, tokens_est: states[0].tokens_est }) : costEnvelope({ lane: "full", tokens_est: 0 });
|
|
2836
|
+
const state0 = states[0];
|
|
2837
|
+
let repeatedReads = 0;
|
|
2838
|
+
try {
|
|
2839
|
+
const reg = loadRegistry(missionDir);
|
|
2840
|
+
repeatedReads = reg.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
|
|
2841
|
+
} catch {
|
|
2842
|
+
repeatedReads = 0;
|
|
2843
|
+
}
|
|
2844
|
+
const liveSlop = computeLiveSlop({ heal_cycle: state0?.heal_cycle ?? 0, repeated_reads: repeatedReads });
|
|
2845
|
+
const ledger = buildCostLedger({ missionDir, envelope, slopSummary: { interventions: liveSlop.interventions } });
|
|
2846
|
+
if (flag(flags.json)) {
|
|
2847
|
+
console.log(toCostJSON(ledger));
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
console.log(`Cost envelope: ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned})`);
|
|
2851
|
+
console.log(`Avoided: ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ~${ledger.avoided.tokens_avoided_est} tokens`);
|
|
2852
|
+
console.log(`Efficiency: reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}%`);
|
|
2853
|
+
if (ledger.avoided.slop_interventions > 0) {
|
|
2854
|
+
const roles = Object.entries(liveSlop.perRole).map(([r, n]) => `${r}:${n}`).join(", ");
|
|
2855
|
+
console.log(`Slop: ${ledger.avoided.slop_interventions} intervention(s) — ${roles}`);
|
|
2856
|
+
}
|
|
2857
|
+
console.log(`Trail: ${ledger.trail.length} decisions`);
|
|
2858
|
+
if (flag(flags.ledger) && ledger.trail.length) {
|
|
2859
|
+
for (const t of ledger.trail.slice(0, 20))
|
|
2860
|
+
console.log(`- ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ""}`);
|
|
2861
|
+
if (ledger.trail.length > 20)
|
|
2862
|
+
console.log(`… ${ledger.trail.length - 20} more`);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
1800
2865
|
function runCmd(flags, positionals) {
|
|
1801
2866
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1802
2867
|
const name = positionals[1];
|
|
@@ -1809,85 +2874,125 @@ function runCmd(flags, positionals) {
|
|
|
1809
2874
|
if (code !== 0)
|
|
1810
2875
|
process.exit(code);
|
|
1811
2876
|
}
|
|
1812
|
-
function
|
|
2877
|
+
function blameCmd(flags, positionals) {
|
|
1813
2878
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1814
|
-
const
|
|
1815
|
-
if (!
|
|
1816
|
-
console.error("usage: mugiwara
|
|
2879
|
+
const path = positionals[1];
|
|
2880
|
+
if (!path) {
|
|
2881
|
+
console.error("usage: mugiwara blame <file-path>");
|
|
1817
2882
|
process.exit(1);
|
|
1818
2883
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
2884
|
+
console.log(blamePath(projectDir, path));
|
|
2885
|
+
}
|
|
2886
|
+
function stalenessLine(projectDir, baseSha) {
|
|
2887
|
+
if (!baseSha || baseSha === "unknown")
|
|
2888
|
+
return null;
|
|
2889
|
+
const git3 = (args) => {
|
|
2890
|
+
try {
|
|
2891
|
+
return execFileSync6("git", args, { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
2892
|
+
} catch {
|
|
2893
|
+
return "";
|
|
2894
|
+
}
|
|
2895
|
+
};
|
|
2896
|
+
let main = "";
|
|
2897
|
+
for (const ref of ["main", "master"]) {
|
|
2898
|
+
main = git3(["rev-parse", "--verify", ref]);
|
|
2899
|
+
if (main)
|
|
2900
|
+
break;
|
|
2901
|
+
}
|
|
2902
|
+
if (!main)
|
|
2903
|
+
return null;
|
|
2904
|
+
try {
|
|
2905
|
+
const behind = Number(git3(["rev-list", "--count", `${baseSha}..${main}`])) || 0;
|
|
2906
|
+
return behind > 0 ? `⚠ stale base: main is ${behind} commit(s) ahead of this mission's base ${baseSha.slice(0, 7)} — rebase check before continuing` : null;
|
|
2907
|
+
} catch {
|
|
2908
|
+
return null;
|
|
2909
|
+
}
|
|
1830
2910
|
}
|
|
1831
|
-
function
|
|
2911
|
+
function handoffCmd(flags, positionals) {
|
|
1832
2912
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1833
2913
|
const mission = positionals[1];
|
|
1834
2914
|
if (!mission) {
|
|
1835
|
-
console.error("usage: mugiwara
|
|
2915
|
+
console.error("usage: mugiwara handoff <mission> [--project <dir>]");
|
|
1836
2916
|
process.exit(1);
|
|
1837
2917
|
}
|
|
1838
|
-
const
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
const bare = join11(plansDir, `${mission}.md`);
|
|
1842
|
-
if (existsSync10(bare))
|
|
1843
|
-
planFile = bare;
|
|
1844
|
-
else {
|
|
1845
|
-
const dated = readdirSync7(plansDir).filter((f) => f.endsWith(`-${mission}.md`)).sort().pop();
|
|
1846
|
-
if (dated)
|
|
1847
|
-
planFile = join11(plansDir, dated);
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
if (!planFile) {
|
|
1851
|
-
console.error(`mugiwara start: no plan found for "${mission}" in ${plansDir}`);
|
|
2918
|
+
const states = readState(projectDir).filter((s) => s.mission === mission);
|
|
2919
|
+
if (!states.length) {
|
|
2920
|
+
console.error(`no in-flight mission "${mission}"`);
|
|
1852
2921
|
process.exit(1);
|
|
1853
2922
|
}
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
2923
|
+
const lines = [
|
|
2924
|
+
`# Handoff: ${mission}`,
|
|
2925
|
+
"",
|
|
2926
|
+
`Generated ${new Date().toISOString()} by \`mugiwara handoff\`.`,
|
|
2927
|
+
"",
|
|
2928
|
+
"| | |",
|
|
2929
|
+
"|---|---|"
|
|
2930
|
+
];
|
|
2931
|
+
for (const s of states) {
|
|
2932
|
+
const scope = s.member ? ` [${s.member}]` : "";
|
|
2933
|
+
lines.push(`| Mission${scope} | flow ${s.flow}, tasks ${s.tasks_done}/${s.tasks_total}, lane ${s.lane}${s.lane_rose ? " (rose)" : ""}, mode ${s.mode} |`);
|
|
2934
|
+
lines.push(`| Branch | \`${s.branch}\` |`);
|
|
2935
|
+
lines.push(`| Actor | ${s.actor || "(unknown)"} |`);
|
|
2936
|
+
if (s.next_action)
|
|
2937
|
+
lines.push(`| Next action | ${s.next_action} |`);
|
|
2938
|
+
if (s.blockers_open)
|
|
2939
|
+
lines.push(`| Open blockers | ${s.blockers_open} |`);
|
|
2940
|
+
if (s.heal_cycle)
|
|
2941
|
+
lines.push(`| Heal cycles | ${s.heal_cycle}/${s.heal_max_cycles}${s.heal_halt ? " — HALTED" : ""} |`);
|
|
2942
|
+
if (s.evidence.length)
|
|
2943
|
+
lines.push(`| Evidence | ${s.evidence.join(", ")} |`);
|
|
2944
|
+
const stale = stalenessLine(projectDir, s.base_sha);
|
|
2945
|
+
if (stale)
|
|
2946
|
+
lines.push(`| Staleness | ${stale.replace("⚠ stale base: ", "")} |`);
|
|
2947
|
+
}
|
|
2948
|
+
lines.push("", "## Resuming", "", `\`mugiwara continue ${mission}\` prints the exact resume point.`);
|
|
2949
|
+
lines.push("Verify `next_action` against plan.md before executing — the table above is computed state, not judgement.");
|
|
2950
|
+
const out = join20(".mugiwara", "missions", mission, "handoff.md");
|
|
2951
|
+
writeFileSync10(resolve2(projectDir, out), lines.join(`
|
|
2952
|
+
`) + `
|
|
2953
|
+
`);
|
|
2954
|
+
console.log(lines.join(`
|
|
2955
|
+
`));
|
|
2956
|
+
console.log(`
|
|
2957
|
+
written: ${out}`);
|
|
2958
|
+
}
|
|
2959
|
+
function signCmd(flags, _) {
|
|
2960
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2961
|
+
if (flag(flags.genKey)) {
|
|
2962
|
+
const backend = str(flags.backend) ?? "auto";
|
|
2963
|
+
const home = homedir4();
|
|
2964
|
+
if (backend === "minisign") {
|
|
2965
|
+
if (!hasMinisign()) {
|
|
2966
|
+
console.error("✗ minisign not installed — cannot generate keys with this backend");
|
|
2967
|
+
process.exit(1);
|
|
2968
|
+
}
|
|
2969
|
+
try {
|
|
2970
|
+
execFileSync6("minisign", ["-G"], { stdio: "inherit" });
|
|
2971
|
+
console.log("✓ minisign key pair generated in ~/.mugiwara/");
|
|
2972
|
+
return;
|
|
2973
|
+
} catch {
|
|
2974
|
+
console.error("✗ key generation failed");
|
|
2975
|
+
process.exit(1);
|
|
1868
2976
|
}
|
|
1869
2977
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
}
|
|
1874
|
-
member = found;
|
|
2978
|
+
const dir = ensurePureKey(home);
|
|
2979
|
+
console.log(`✓ pure ed25519 key pair ready: ${join20(dir, "mugiwara.key")} / ${join20(dir, "mugiwara.pub")}`);
|
|
2980
|
+
return;
|
|
1875
2981
|
}
|
|
1876
|
-
const
|
|
1877
|
-
if (
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
const init = join11(import.meta.dirname, "..", "scripts", "initiative.ts");
|
|
1881
|
-
const bun = process.platform === "win32" ? "bun.exe" : "bun";
|
|
1882
|
-
const r = spawnSync2(bun, [init, "set-status", planFile, "--id", member, "--status", "in-progress"], {
|
|
1883
|
-
cwd: projectDir,
|
|
1884
|
-
stdio: "inherit",
|
|
1885
|
-
env: process.env
|
|
1886
|
-
});
|
|
1887
|
-
if (r.status)
|
|
1888
|
-
process.exit(r.status);
|
|
2982
|
+
const mission = _[1];
|
|
2983
|
+
if (!mission) {
|
|
2984
|
+
console.error("usage: mugiwara sign <mission> [--verify] [--gen-key [--backend pure|minisign]] [--project <dir>]");
|
|
2985
|
+
process.exit(1);
|
|
1889
2986
|
}
|
|
1890
|
-
|
|
2987
|
+
const missionDir = join20(projectDir, ".mugiwara", "missions", mission);
|
|
2988
|
+
if (!existsSync15(missionDir)) {
|
|
2989
|
+
console.error(`no mission dir: ${missionDir}`);
|
|
2990
|
+
process.exit(1);
|
|
2991
|
+
}
|
|
2992
|
+
const r = flag(flags.verify) ? verifyReport(projectDir, missionDir) : signReport(projectDir, missionDir);
|
|
2993
|
+
console.log(`${r.ok ? "✓" : "✗"} ${r.message}`);
|
|
2994
|
+
if (!r.ok)
|
|
2995
|
+
process.exit(1);
|
|
1891
2996
|
}
|
|
1892
2997
|
function help() {
|
|
1893
2998
|
console.log(`mugiwara ${VERSION} — the Straw Hat crew for AI agents
|
|
@@ -1898,21 +3003,27 @@ Usage:
|
|
|
1898
3003
|
mugiwara uninstall remove installed files via manifest
|
|
1899
3004
|
mugiwara list show installations
|
|
1900
3005
|
mugiwara list --check health check: show installations + missing files
|
|
1901
|
-
mugiwara reset wipe mission state (
|
|
1902
|
-
mugiwara archive <m>
|
|
1903
|
-
mugiwara
|
|
3006
|
+
mugiwara reset wipe mission state (missions/ + legacy dirs)
|
|
3007
|
+
mugiwara archive <m> fold a closed mission's waves into its report, then remove loose files
|
|
3008
|
+
mugiwara clean [--all] [--before <date>]
|
|
3009
|
+
batch-archive every closed mission (report.md present, no live state)
|
|
1904
3010
|
mugiwara continue list in-flight missions (exit 2 = pick one, nothing resumed)
|
|
1905
3011
|
mugiwara continue <m> [member]
|
|
1906
3012
|
print the exact resume point for that mission/member
|
|
1907
3013
|
mugiwara status computed mission state: wave, tasks, lane, blockers, budget
|
|
3014
|
+
mugiwara cost [--mission <id>] [--json] [--ledger]
|
|
3015
|
+
show cost ledger, avoided work, efficiency, trail (human + JSON)
|
|
3016
|
+
mugiwara blame <path> provenance note on the last commit touching <path>
|
|
3017
|
+
(fetch notes first: git fetch origin 'refs/notes/mugiwara:refs/notes/mugiwara')
|
|
3018
|
+
mugiwara handoff <m> write .mugiwara/missions/<m>/handoff.md — a report the next
|
|
3019
|
+
engineer can act on (computed state + staleness check)
|
|
3020
|
+
mugiwara sign <m> attestation: sign report.md (auto/minisign/pure/off; --verify to check)
|
|
3021
|
+
mugiwara sign --gen-key [--backend pure|minisign]
|
|
3022
|
+
create signing keys (pure ed25519 default)
|
|
1908
3023
|
mugiwara run <script> [args...]
|
|
1909
3024
|
run a bundled harness script here (${RUNNABLE.join(", ")})
|
|
1910
|
-
mugiwara savepoint <mission> [member] [
|
|
3025
|
+
mugiwara savepoint <mission> [member] [flow] [mode]
|
|
1911
3026
|
shorthand for: mugiwara run savepoint.sh ...
|
|
1912
|
-
mugiwara initiative <status|conflict-check|set-status> <plan> [args]
|
|
1913
|
-
team sub-mission coordination (status / conflict-check / set-status)
|
|
1914
|
-
mugiwara start <mission> [member]
|
|
1915
|
-
begin a mission from its plan (solo, or team sub-mission by git actor)
|
|
1916
3027
|
mugiwara --help this help
|
|
1917
3028
|
mugiwara --version print version
|
|
1918
3029
|
|
|
@@ -1924,8 +3035,10 @@ Flags:
|
|
|
1924
3035
|
--force overwrite differing files (with backup)
|
|
1925
3036
|
--dry-run print actions without writing
|
|
1926
3037
|
--check with list: report missing files (health check)
|
|
1927
|
-
|
|
1928
|
-
|
|
3038
|
+
--all with continue/status: every actor; with clean: include in-flight missions
|
|
3039
|
+
--force with clean --all: archive in-flight missions anyway
|
|
3040
|
+
--before <date> with clean: also archive missions untouched since this date
|
|
3041
|
+
--keep-logs with reset: keep lessons.md (lessons ledger survives)`);
|
|
1929
3042
|
}
|
|
1930
3043
|
var isMain = process.argv[1] !== undefined && fileURLToPath5(import.meta.url) === resolve2(process.argv[1]);
|
|
1931
3044
|
if (isMain) {
|
|
@@ -1935,5 +3048,6 @@ if (isMain) {
|
|
|
1935
3048
|
});
|
|
1936
3049
|
}
|
|
1937
3050
|
export {
|
|
1938
|
-
run
|
|
3051
|
+
run,
|
|
3052
|
+
stalenessLine
|
|
1939
3053
|
};
|