@ionivetech/mugiwara 0.6.6 → 0.7.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/plugins/mugiwara.mjs +3 -18
- package/AGENTS.md +5 -4
- package/GEMINI.md +3 -3
- package/README.md +69 -63
- 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 +3 -3
- 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 +2 -0
- 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 +6 -5
- package/content/skills/mugiwara-checkpoint/references/ledger-format.md +1 -1
- package/content/skills/mugiwara-execution/SKILL.md +8 -8
- package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
- package/content/skills/mugiwara-execution/references/resume-batching.md +2 -2
- package/content/skills/mugiwara-frontend/SKILL.md +3 -2
- package/content/skills/mugiwara-frontend/references/checklist.md +14 -0
- package/content/skills/mugiwara-gates/SKILL.md +8 -5
- package/content/skills/mugiwara-healing/SKILL.md +2 -2
- package/content/skills/mugiwara-lessons/SKILL.md +2 -2
- package/content/skills/mugiwara-orchestration/SKILL.md +7 -7
- 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/triage-escalation.md +11 -1
- package/content/skills/mugiwara-planning/SKILL.md +3 -18
- package/content/skills/mugiwara-planning/references/plan-template.md +2 -2
- package/content/skills/mugiwara-quality/SKILL.md +5 -2
- package/content/skills/mugiwara-resume/SKILL.md +2 -2
- package/content/skills/mugiwara-review/SKILL.md +3 -2
- package/content/skills/mugiwara-security/SKILL.md +1 -1
- 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 +9 -12
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +39 -38
- package/dist/mugiwara.js +904 -462
- 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 +16 -12
- package/hooks/pipeline-guard.ts +22 -19
- 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 +0 -3
- package/references/prose-style.md +54 -0
- package/scripts/check-doc-links.ts +40 -0
- package/scripts/conformance.ts +6 -16
- package/scripts/coverage-gate.ts +20 -6
- package/scripts/gate-selftest.ts +118 -7
- package/scripts/lane.sh +54 -8
- package/scripts/lib/patterns.sh +5 -0
- package/scripts/policy-force.ts +22 -0
- package/scripts/retrieval-eval.ts +1 -1
- package/scripts/savepoint.sh +136 -58
- package/scripts/validate-content.ts +22 -9
- package/scripts/verify-install.ts +9 -7
- package/src/args.ts +2 -2
- package/src/budget.ts +56 -0
- package/src/cli.ts +161 -110
- package/src/continue.ts +29 -10
- package/src/installer.ts +25 -20
- package/src/integrity.ts +158 -0
- package/src/mission.ts +228 -95
- package/src/policy.ts +156 -0
- package/src/provenance.ts +116 -0
- package/src/rollback.ts +95 -0
- package/src/routing.ts +69 -0
- package/src/run.ts +2 -2
- package/src/sign.ts +61 -0
- package/src/targets/claude.ts +2 -2
- 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 existsSync12, readdirSync as readdirSync9, readFileSync as readFileSync9, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "node:fs";
|
|
5
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
6
|
+
import { homedir as homedir4 } from "node:os";
|
|
7
|
+
import { join as join15, 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" };
|
|
12
12
|
var BOOL_FLAGS = {
|
|
13
13
|
"--global": "global",
|
|
14
14
|
"--yes": "yes",
|
|
@@ -18,6 +18,7 @@ var BOOL_FLAGS = {
|
|
|
18
18
|
"--keep-logs": "keepLogs",
|
|
19
19
|
"--check": "check",
|
|
20
20
|
"--all": "all",
|
|
21
|
+
"--verify": "verify",
|
|
21
22
|
"--help": "help",
|
|
22
23
|
"-h": "help",
|
|
23
24
|
"--version": "version",
|
|
@@ -130,7 +131,7 @@ function copyScripts(dstDir) {
|
|
|
130
131
|
if (!existsSync(src))
|
|
131
132
|
return;
|
|
132
133
|
for (const f of readdirSync(src)) {
|
|
133
|
-
if (!f.endsWith(".sh")
|
|
134
|
+
if (!f.endsWith(".sh") || f === "install.sh")
|
|
134
135
|
continue;
|
|
135
136
|
const to = join(dst, f);
|
|
136
137
|
mkdirSync(dst, { recursive: true });
|
|
@@ -750,9 +751,11 @@ function installTo(target10, opts) {
|
|
|
750
751
|
"coverage_modified=80",
|
|
751
752
|
"review_depth=full",
|
|
752
753
|
"quality_depth=full",
|
|
754
|
+
"verify_merged=off",
|
|
753
755
|
"delegate_threshold=60",
|
|
754
756
|
"heal_max_cycles=3",
|
|
755
|
-
"verbosity=normal"
|
|
757
|
+
"verbosity=normal",
|
|
758
|
+
"# context_budget_chars=150000 # optional: fail archive if trail exceeds this (measured in report Cost section)"
|
|
756
759
|
].join(`
|
|
757
760
|
`) + `
|
|
758
761
|
`;
|
|
@@ -761,7 +764,7 @@ function installTo(target10, opts) {
|
|
|
761
764
|
writeFileSync3(configPath, body);
|
|
762
765
|
}
|
|
763
766
|
result.written.push(configPath);
|
|
764
|
-
result.notes.push(`default config written: ${configPath} (
|
|
767
|
+
result.notes.push(`default config written: ${configPath} (edit it to customise)`);
|
|
765
768
|
}
|
|
766
769
|
}
|
|
767
770
|
return result;
|
|
@@ -802,11 +805,11 @@ function assertNotSymlink(file) {
|
|
|
802
805
|
}
|
|
803
806
|
var GITIGNORE_BLOCK_START = "# >>> mugiwara >>>";
|
|
804
807
|
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/
|
|
808
|
+
var GITIGNORE_LEGACY = [".mugiwara/state/", "# mugiwara"];
|
|
809
|
+
var GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit plan.md, flows/, review.md,
|
|
810
|
+
# decisions.md, blockers.md, report.md under .mugiwara/missions/. Ignore session state.
|
|
811
|
+
.mugiwara/missions/**/*.json
|
|
812
|
+
.mugiwara/index.md
|
|
810
813
|
.mugiwara/config
|
|
811
814
|
.mugiwara/refs/
|
|
812
815
|
# <<< mugiwara <<<
|
|
@@ -817,10 +820,10 @@ function ensureProjectGitignore(projectDir, opts = {}) {
|
|
|
817
820
|
assertNotSymlink(path);
|
|
818
821
|
let existing = existsSync4(path) ? readFileSync3(path, "utf8") : "";
|
|
819
822
|
if (existing) {
|
|
820
|
-
if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes(".mugiwara/
|
|
823
|
+
if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes(".mugiwara/missions/**/*.json")) {
|
|
821
824
|
return { appended: false, notes: [] };
|
|
822
825
|
}
|
|
823
|
-
const hadOld = existing.includes(
|
|
826
|
+
const hadOld = GITIGNORE_LEGACY.some((m) => existing.includes(m)) || existing.includes(GITIGNORE_BLOCK_START);
|
|
824
827
|
if (hadOld) {
|
|
825
828
|
const clean = removeProjectGitignore(projectDir, { dryRun });
|
|
826
829
|
existing = clean.removed ? existsSync4(path) ? readFileSync3(path, "utf8") : "" : existing;
|
|
@@ -849,18 +852,20 @@ function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
|
|
|
849
852
|
if (end < start)
|
|
850
853
|
return { removed: false, notes: ["delimiter mismatch — .gitignore left untouched"] };
|
|
851
854
|
cleaned = current.slice(0, start) + current.slice(end + GITIGNORE_BLOCK_END.length);
|
|
852
|
-
} else if (current.includes(
|
|
855
|
+
} else if (current.includes("# mugiwara")) {
|
|
853
856
|
const LEGACY_LINES = new Set([
|
|
854
857
|
".mugiwara/state.json",
|
|
855
858
|
".mugiwara/state-*.json",
|
|
856
859
|
".mugiwara/config",
|
|
860
|
+
".mugiwara/state/",
|
|
861
|
+
".mugiwara/continue/",
|
|
857
862
|
".mugiwara/continue.md",
|
|
858
863
|
".mugiwara/refs/"
|
|
859
864
|
]);
|
|
860
865
|
const lines = current.split(`
|
|
861
866
|
`).filter((l) => {
|
|
862
867
|
const t = l.trim();
|
|
863
|
-
if (t.startsWith(
|
|
868
|
+
if (t.startsWith("# mugiwara"))
|
|
864
869
|
return false;
|
|
865
870
|
return !LEGACY_LINES.has(t);
|
|
866
871
|
});
|
|
@@ -898,19 +903,436 @@ function writeManifest(file, data) {
|
|
|
898
903
|
}
|
|
899
904
|
|
|
900
905
|
// src/mission.ts
|
|
901
|
-
import { existsSync as
|
|
902
|
-
import {
|
|
906
|
+
import { existsSync as existsSync8, rmSync as rmSync2, readFileSync as readFileSync7, readdirSync as readdirSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync7, renameSync, openSync, writeSync, closeSync } from "node:fs";
|
|
907
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
908
|
+
import { join as join11 } from "node:path";
|
|
909
|
+
|
|
910
|
+
// src/integrity.ts
|
|
911
|
+
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
912
|
+
import { isAbsolute, join as join7, relative } from "node:path";
|
|
913
|
+
var SECRET_PATTERNS = [
|
|
914
|
+
[/AKIA[0-9A-Z]{16}/, "AWS access key id"],
|
|
915
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "private key block"],
|
|
916
|
+
[/gh[pousr]_[A-Za-z0-9]{20,}/, "GitHub token"],
|
|
917
|
+
[/xox[baprs]-[A-Za-z0-9-]{10,}/, "Slack token"],
|
|
918
|
+
[/sk-[A-Za-z0-9]{32,}/, "API key (sk-…)"],
|
|
919
|
+
[/eyJhbGciOi[A-Za-z0-9_.-]{20,}/, "JWT pasted verbatim"],
|
|
920
|
+
[/(api[_-]?key|secret|passwd|password)\s*[=:]\s*["'][^"'\s]{8,}["']/i, "credential assignment"]
|
|
921
|
+
];
|
|
922
|
+
var ALLOW_SECRET = "mugiwara:allow-secret";
|
|
923
|
+
function findSecrets(body) {
|
|
924
|
+
const out = [];
|
|
925
|
+
for (const line of body.split(/\r?\n/)) {
|
|
926
|
+
if (line.includes(ALLOW_SECRET))
|
|
927
|
+
continue;
|
|
928
|
+
for (const [re, label] of SECRET_PATTERNS) {
|
|
929
|
+
const hit = line.match(re);
|
|
930
|
+
if (hit)
|
|
931
|
+
out.push({ label, hit: hit[0] });
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return out;
|
|
935
|
+
}
|
|
936
|
+
var TRAIL_EXTS = new Set([".md", ".json", ".sh"]);
|
|
937
|
+
function trailFiles(dir) {
|
|
938
|
+
const out = [];
|
|
939
|
+
const walk = (d) => {
|
|
940
|
+
for (const e of readdirSync4(d, { withFileTypes: true })) {
|
|
941
|
+
const p = join7(d, e.name);
|
|
942
|
+
if (e.isDirectory())
|
|
943
|
+
walk(p);
|
|
944
|
+
else if (TRAIL_EXTS.has(e.name.slice(e.name.lastIndexOf(".")) || ""))
|
|
945
|
+
out.push(p);
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
walk(dir);
|
|
949
|
+
return out;
|
|
950
|
+
}
|
|
951
|
+
function linkedPaths(md) {
|
|
952
|
+
const out = [];
|
|
953
|
+
for (const m of md.matchAll(/\]\(([^)\s]+)\)/g)) {
|
|
954
|
+
const t = m[1];
|
|
955
|
+
if (/^(https?:|mailto:|#|\/\/)/.test(t))
|
|
956
|
+
continue;
|
|
957
|
+
out.push(t.split("#")[0]);
|
|
958
|
+
}
|
|
959
|
+
return out.filter(Boolean);
|
|
960
|
+
}
|
|
961
|
+
function hasCommandOutputShape(body) {
|
|
962
|
+
return /`[^`]+`/.test(body) || /\b(exit\s+[01]|✓|✗|\bPASS\b|\bFAIL\b|passed|failed|\d+\s+(passed|failed))\b/i.test(body);
|
|
963
|
+
}
|
|
964
|
+
function collectPassCitedPaths(missionDir) {
|
|
965
|
+
const out = [];
|
|
966
|
+
for (const f of trailFiles(missionDir)) {
|
|
967
|
+
let body;
|
|
968
|
+
try {
|
|
969
|
+
body = readFileSync5(f, "utf8");
|
|
970
|
+
} catch {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
for (const line of body.split(/\r?\n/)) {
|
|
974
|
+
if (!/\bPASS\b/.test(line))
|
|
975
|
+
continue;
|
|
976
|
+
for (const p of linkedPaths(line))
|
|
977
|
+
out.push(p);
|
|
978
|
+
for (const m of line.matchAll(/(?:^|[\s"'(])([a-zA-Z0-9._\/-]+\.(?:md|txt|log|json))\b/g)) {
|
|
979
|
+
const cand = m[1];
|
|
980
|
+
if (cand.includes("/"))
|
|
981
|
+
out.push(cand);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return [...new Set(out)];
|
|
986
|
+
}
|
|
987
|
+
function checkTrail(missionDir, projectRoot) {
|
|
988
|
+
const issues = [];
|
|
989
|
+
const files = trailFiles(missionDir);
|
|
990
|
+
for (const f of files) {
|
|
991
|
+
let body;
|
|
992
|
+
try {
|
|
993
|
+
body = readFileSync5(f, "utf8");
|
|
994
|
+
} catch {
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
for (const target10 of linkedPaths(body)) {
|
|
998
|
+
if (isAbsolute(target10))
|
|
999
|
+
continue;
|
|
1000
|
+
const fromMission = join7(missionDir, target10);
|
|
1001
|
+
const fromRoot = join7(projectRoot, target10);
|
|
1002
|
+
if (!existsSync6(fromMission) && !existsSync6(fromRoot)) {
|
|
1003
|
+
issues.push({
|
|
1004
|
+
kind: "dangling-path",
|
|
1005
|
+
detail: `${relative(projectRoot, f)} links "${target10}" — no such file (mission dir or repo root)`
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
for (const { label, hit } of findSecrets(body)) {
|
|
1010
|
+
issues.push({
|
|
1011
|
+
kind: "secret",
|
|
1012
|
+
detail: `${relative(projectRoot, f)} matches ${label}: ${hit.slice(0, 12)}…`
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
const evidencePaths = [];
|
|
1017
|
+
const evidenceFile = join7(missionDir, "state.json");
|
|
1018
|
+
if (existsSync6(evidenceFile)) {
|
|
1019
|
+
try {
|
|
1020
|
+
const s = JSON.parse(readFileSync5(evidenceFile, "utf8"));
|
|
1021
|
+
if (Array.isArray(s.evidence)) {
|
|
1022
|
+
for (const e of s.evidence) {
|
|
1023
|
+
if (typeof e !== "string" || !e.trim())
|
|
1024
|
+
continue;
|
|
1025
|
+
evidencePaths.push(e);
|
|
1026
|
+
const cand = join7(projectRoot, e);
|
|
1027
|
+
if (!isAbsolute(e) && !existsSync6(cand) && !existsSync6(join7(missionDir, e))) {
|
|
1028
|
+
issues.push({ kind: "evidence", detail: `state.json evidence "${e}" does not exist` });
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
} catch {}
|
|
1033
|
+
}
|
|
1034
|
+
const passCited = collectPassCitedPaths(missionDir);
|
|
1035
|
+
for (const e of passCited) {
|
|
1036
|
+
if (!e.trim() || isAbsolute(e))
|
|
1037
|
+
continue;
|
|
1038
|
+
const candMission = join7(missionDir, e);
|
|
1039
|
+
const candRoot = join7(projectRoot, e);
|
|
1040
|
+
const resolved = existsSync6(candMission) ? candMission : existsSync6(candRoot) ? candRoot : null;
|
|
1041
|
+
if (!resolved)
|
|
1042
|
+
continue;
|
|
1043
|
+
let body;
|
|
1044
|
+
try {
|
|
1045
|
+
body = readFileSync5(resolved, "utf8");
|
|
1046
|
+
} catch {
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (!hasCommandOutputShape(body)) {
|
|
1050
|
+
issues.push({ kind: "evidence-thin", detail: `evidence "${e}" exists but lacks command output (no backticked command or exit-status token)` });
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return issues;
|
|
1054
|
+
}
|
|
1055
|
+
function formatIssues(issues) {
|
|
1056
|
+
return issues.map((i) => ` ✗ [${i.kind}] ${i.detail}`).join(`
|
|
1057
|
+
`);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/rollback.ts
|
|
1061
|
+
import { execFileSync } from "node:child_process";
|
|
1062
|
+
import { writeFileSync as writeFileSync5 } from "node:fs";
|
|
1063
|
+
import { join as join8 } from "node:path";
|
|
1064
|
+
function git(cwd, args) {
|
|
1065
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1066
|
+
}
|
|
1067
|
+
function buildRollback(input, commitsNewestFirst, filesTouched) {
|
|
1068
|
+
const lines = [
|
|
1069
|
+
"#!/usr/bin/env bash",
|
|
1070
|
+
`# Rollback map for mission "${input.mission}" — generated at closure.`,
|
|
1071
|
+
"# Human-executed. Review before running; mugiwara never runs this.",
|
|
1072
|
+
`# Branch: ${input.branch}`,
|
|
1073
|
+
`# Base: ${input.baseSha}`,
|
|
1074
|
+
"",
|
|
1075
|
+
"set -euo pipefail",
|
|
1076
|
+
""
|
|
1077
|
+
];
|
|
1078
|
+
if (!commitsNewestFirst.length && !filesTouched.length) {
|
|
1079
|
+
lines.push("# No commits between base and HEAD on this branch — nothing to revert.", "");
|
|
1080
|
+
return lines.join(`
|
|
1081
|
+
`);
|
|
1082
|
+
}
|
|
1083
|
+
if (!commitsNewestFirst.length) {
|
|
1084
|
+
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", "");
|
|
1085
|
+
return lines.join(`
|
|
1086
|
+
`);
|
|
1087
|
+
}
|
|
1088
|
+
lines.push("# Revert newest-first so earlier reverts never conflict with later ones.", "git revert --no-edit \\", commitsNewestFirst.map((c) => ` ${c}`).join(" \\\n"), "");
|
|
1089
|
+
if (filesTouched.length) {
|
|
1090
|
+
lines.push("# Files this mission touched (verify the working tree is clean afterwards):", ...filesTouched.map((f) => `# ${f}`), "");
|
|
1091
|
+
}
|
|
1092
|
+
return lines.join(`
|
|
1093
|
+
`);
|
|
1094
|
+
}
|
|
1095
|
+
function generateRollback(projectDir, missionDir, input) {
|
|
1096
|
+
try {
|
|
1097
|
+
const range = `${input.baseSha}..${input.branch}`;
|
|
1098
|
+
const revList = git(projectDir, ["rev-list", "--reverse", range]).split(/\r?\n/).filter(Boolean);
|
|
1099
|
+
const commitsNewestFirst = [...revList].reverse();
|
|
1100
|
+
const filesTouched = input.baseSha === "unknown" ? [] : git(projectDir, ["diff", "--name-only", input.baseSha, input.branch]).split(/\r?\n/).filter(Boolean);
|
|
1101
|
+
const body = buildRollback(input, commitsNewestFirst, filesTouched);
|
|
1102
|
+
const file = join8(missionDir, "rollback.sh");
|
|
1103
|
+
writeFileSync5(file, body, { mode: 493 });
|
|
1104
|
+
return { file: "rollback.sh", commits: commitsNewestFirst.length };
|
|
1105
|
+
} catch {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/provenance.ts
|
|
1111
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1112
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
1113
|
+
import { join as join9 } from "node:path";
|
|
1114
|
+
function git2(cwd, args) {
|
|
1115
|
+
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1116
|
+
}
|
|
1117
|
+
function modelLabel() {
|
|
1118
|
+
return process.env.MUGIWARA_MODEL?.trim() || process.env.ANTHROPIC_MODEL?.trim() || "model-unrecorded (set MUGIWARA_MODEL to attribute)";
|
|
1119
|
+
}
|
|
1120
|
+
function buildNote(s) {
|
|
1121
|
+
const gates = s.evidence.length ? s.evidence.join(" · ") : "no evidence recorded";
|
|
1122
|
+
const uniqModels = [...new Set((s.models ?? []).filter(Boolean))];
|
|
1123
|
+
const modelPart = uniqModels.length ? `model(s): ${uniqModels.join(", ")}` : s.model ?? modelLabel();
|
|
1124
|
+
return [
|
|
1125
|
+
`mission: ${s.mission}`,
|
|
1126
|
+
`agent: ${s.actor || "unknown"} · ${modelPart} · lane ${s.lane} · mode ${s.mode}`,
|
|
1127
|
+
`tasks: ${s.tasks_done}/${s.tasks_total}`,
|
|
1128
|
+
`gates/evidence: ${gates}`,
|
|
1129
|
+
`branch: ${s.branch}`,
|
|
1130
|
+
"human review: pending (PR review is the terminal gate)"
|
|
1131
|
+
].join(`
|
|
1132
|
+
`);
|
|
1133
|
+
}
|
|
1134
|
+
function renderProvenanceMd(note, sha) {
|
|
1135
|
+
const lines = [
|
|
1136
|
+
"# Provenance",
|
|
1137
|
+
"",
|
|
1138
|
+
"<!-- paste below into the PR description or a PR comment -->",
|
|
1139
|
+
"",
|
|
1140
|
+
"```",
|
|
1141
|
+
note,
|
|
1142
|
+
"```",
|
|
1143
|
+
"",
|
|
1144
|
+
sha ? `Commit: ${sha}` : "Commit: not recorded (no git head resolved at closure)",
|
|
1145
|
+
"",
|
|
1146
|
+
"Query locally after pushing notes:",
|
|
1147
|
+
"`git fetch origin refs/notes/mugiwara:refs/notes/mugiwara` then `mugiwara blame <path>`."
|
|
1148
|
+
];
|
|
1149
|
+
return lines.join(`
|
|
1150
|
+
`) + `
|
|
1151
|
+
`;
|
|
1152
|
+
}
|
|
1153
|
+
function attachGitNote(projectDir, branch, note) {
|
|
1154
|
+
try {
|
|
1155
|
+
let sha;
|
|
1156
|
+
try {
|
|
1157
|
+
sha = git2(projectDir, ["rev-parse", "--verify", branch]);
|
|
1158
|
+
} catch {
|
|
1159
|
+
sha = git2(projectDir, ["rev-parse", "HEAD"]);
|
|
1160
|
+
}
|
|
1161
|
+
git2(projectDir, ["notes", "--ref=mugiwara", "add", "-f", "-m", note, sha]);
|
|
1162
|
+
return { sha };
|
|
1163
|
+
} catch {
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function blamePath(projectDir, path) {
|
|
1168
|
+
let sha;
|
|
1169
|
+
try {
|
|
1170
|
+
sha = git2(projectDir, ["log", "-1", "--format=%H", "--", path]);
|
|
1171
|
+
} catch {
|
|
1172
|
+
return `blame: not a git repository (${projectDir})`;
|
|
1173
|
+
}
|
|
1174
|
+
if (!sha)
|
|
1175
|
+
return `blame: no commit touches "${path}"`;
|
|
1176
|
+
try {
|
|
1177
|
+
const note = git2(projectDir, ["notes", "--ref=mugiwara", "show", sha]);
|
|
1178
|
+
return `${path} @ ${sha.slice(0, 7)}
|
|
1179
|
+
${note}`;
|
|
1180
|
+
} catch {
|
|
1181
|
+
return `${path} @ ${sha.slice(0, 7)}
|
|
1182
|
+
(no mugiwara provenance note on this commit)`;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
function writeProvenance(projectDir, missionDir, state) {
|
|
1186
|
+
const note = buildNote(state);
|
|
1187
|
+
const attached = attachGitNote(projectDir, state.branch, note);
|
|
1188
|
+
writeFileSync6(join9(missionDir, "provenance.md"), renderProvenanceMd(note, attached ? attached.sha : null));
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// src/routing.ts
|
|
1192
|
+
var DOC_PAT = /\.(md|txt|rst)$|^docs\/|^(CHANGELOG|LICENSE|README)/;
|
|
1193
|
+
var TEST_PAT = /\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)(tests?|__tests__|specs?)\//;
|
|
1194
|
+
function scorePath(path, sensitivePaths, evidenceJoined) {
|
|
1195
|
+
const reasons = [];
|
|
1196
|
+
let score = 0;
|
|
1197
|
+
const sensitiveHit = sensitivePaths.some((s) => s && (path === s || path.startsWith(s.replace(/\/?$/, "/")) || new RegExp(s.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*")).test(path)));
|
|
1198
|
+
if (sensitiveHit) {
|
|
1199
|
+
score += 100;
|
|
1200
|
+
reasons.push("sensitive path");
|
|
1201
|
+
}
|
|
1202
|
+
if (!DOC_PAT.test(path) && !TEST_PAT.test(path)) {
|
|
1203
|
+
score += 50;
|
|
1204
|
+
reasons.push("production code");
|
|
1205
|
+
} else if (TEST_PAT.test(path)) {
|
|
1206
|
+
score += 10;
|
|
1207
|
+
reasons.push("test scaffolding — skim unless behavior changed");
|
|
1208
|
+
} else {
|
|
1209
|
+
score += 5;
|
|
1210
|
+
reasons.push("docs/config");
|
|
1211
|
+
}
|
|
1212
|
+
if (!evidenceJoined.includes(path)) {
|
|
1213
|
+
score += 20;
|
|
1214
|
+
reasons.push("not covered by recorded evidence");
|
|
1215
|
+
}
|
|
1216
|
+
return { score, reasons };
|
|
1217
|
+
}
|
|
1218
|
+
function rankFiles(paths, state) {
|
|
1219
|
+
const sensitive = state.sensitive_paths ?? [];
|
|
1220
|
+
const ev = state.evidence.join(" ");
|
|
1221
|
+
return paths.map((path) => {
|
|
1222
|
+
const { score, reasons } = scorePath(path, sensitive, ev);
|
|
1223
|
+
return { path, score, reasons };
|
|
1224
|
+
}).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
1225
|
+
}
|
|
1226
|
+
function renderRouting(ranked, mission) {
|
|
1227
|
+
if (!ranked.length)
|
|
1228
|
+
return "";
|
|
1229
|
+
const lines = [
|
|
1230
|
+
"",
|
|
1231
|
+
"## Review routing",
|
|
1232
|
+
"",
|
|
1233
|
+
`Ranked reading order for \`${mission}\` (heuristic ordering — it decides where to look first, never correctness):`,
|
|
1234
|
+
""
|
|
1235
|
+
];
|
|
1236
|
+
ranked.forEach((r, i) => {
|
|
1237
|
+
lines.push(`${i + 1}. \`${r.path}\` — ${r.reasons.join("; ")}`);
|
|
1238
|
+
});
|
|
1239
|
+
lines.push("");
|
|
1240
|
+
return lines.join(`
|
|
1241
|
+
`);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// src/budget.ts
|
|
1245
|
+
import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync } from "node:fs";
|
|
1246
|
+
import { homedir as homedir2 } from "node:os";
|
|
1247
|
+
import { join as join10 } from "node:path";
|
|
1248
|
+
function readBudgetConfig(projectDir) {
|
|
1249
|
+
for (const base of [projectDir, homedir2()]) {
|
|
1250
|
+
const file = join10(base, ".mugiwara", "config");
|
|
1251
|
+
if (!existsSync7(file))
|
|
1252
|
+
continue;
|
|
1253
|
+
for (const line of readFileSync6(file, "utf8").split(/\r?\n/)) {
|
|
1254
|
+
const t = line.trim();
|
|
1255
|
+
if (!t || t.startsWith("#"))
|
|
1256
|
+
continue;
|
|
1257
|
+
const eq = t.indexOf("=");
|
|
1258
|
+
if (eq === -1)
|
|
1259
|
+
continue;
|
|
1260
|
+
if (t.slice(0, eq).trim() !== "context_budget_chars")
|
|
1261
|
+
continue;
|
|
1262
|
+
const n = Number(t.slice(eq + 1).trim());
|
|
1263
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return 0;
|
|
1267
|
+
}
|
|
1268
|
+
function measureContextChars(missionDir) {
|
|
1269
|
+
let total = 0;
|
|
1270
|
+
const add = (p) => {
|
|
1271
|
+
try {
|
|
1272
|
+
total += statSync(p).size;
|
|
1273
|
+
} catch {}
|
|
1274
|
+
};
|
|
1275
|
+
for (const f of readdirSync5(missionDir)) {
|
|
1276
|
+
if (/\.md$/.test(f))
|
|
1277
|
+
add(join10(missionDir, f));
|
|
1278
|
+
}
|
|
1279
|
+
for (const sub of ["flows", "waves"]) {
|
|
1280
|
+
const dir = join10(missionDir, sub);
|
|
1281
|
+
if (existsSync7(dir)) {
|
|
1282
|
+
for (const f of readdirSync5(dir))
|
|
1283
|
+
add(join10(dir, f));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return total;
|
|
1287
|
+
}
|
|
1288
|
+
function formatFootprint(chars, budget) {
|
|
1289
|
+
const base = `Context footprint: ${chars} chars`;
|
|
1290
|
+
if (!budget)
|
|
1291
|
+
return `${base} (no budget configured)`;
|
|
1292
|
+
return chars > budget ? `${base} — OVER budget ${budget}` : `${base} (budget ${budget})`;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/mission.ts
|
|
1296
|
+
function isStateFile(f) {
|
|
1297
|
+
const stem = f.replace(/\.json$/, "");
|
|
1298
|
+
return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
|
|
1299
|
+
}
|
|
1300
|
+
function primaryState(dir, files) {
|
|
1301
|
+
const name = files.includes("state.json") ? "state.json" : files.find((f) => f.endsWith(".json") && f !== "continue.json" && !f.startsWith("continue-"));
|
|
1302
|
+
if (!name)
|
|
1303
|
+
return null;
|
|
1304
|
+
try {
|
|
1305
|
+
return JSON.parse(readFileSync7(join11(dir, name), "utf8"));
|
|
1306
|
+
} catch {
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
function changedFiles(projectDir, state) {
|
|
1311
|
+
const base = typeof state?.base_sha === "string" ? state.base_sha : "";
|
|
1312
|
+
const branch = typeof state?.branch === "string" ? state.branch : "";
|
|
1313
|
+
if (!base || base === "unknown" || !branch)
|
|
1314
|
+
return [];
|
|
1315
|
+
try {
|
|
1316
|
+
return execFileSync3("git", ["diff", "--name-only", base, branch], {
|
|
1317
|
+
cwd: projectDir,
|
|
1318
|
+
encoding: "utf8",
|
|
1319
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1320
|
+
}).split(/\r?\n/).filter(Boolean);
|
|
1321
|
+
} catch {
|
|
1322
|
+
return [];
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
903
1325
|
function activeActor(projectDir) {
|
|
904
|
-
const
|
|
905
|
-
if (!
|
|
1326
|
+
const missionsDir = join11(projectDir, ".mugiwara", "missions");
|
|
1327
|
+
if (!existsSync8(missionsDir))
|
|
906
1328
|
return null;
|
|
907
|
-
const missions =
|
|
1329
|
+
const missions = readdirSync6(missionsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
908
1330
|
let latest = null;
|
|
909
1331
|
for (const mission of missions) {
|
|
910
|
-
const d =
|
|
911
|
-
for (const f of
|
|
1332
|
+
const d = join11(missionsDir, mission);
|
|
1333
|
+
for (const f of readdirSync6(d).filter(isStateFile)) {
|
|
912
1334
|
try {
|
|
913
|
-
const s = JSON.parse(
|
|
1335
|
+
const s = JSON.parse(readFileSync7(join11(d, f), "utf8"));
|
|
914
1336
|
const t = Date.parse(s.updated_at || "") || 0;
|
|
915
1337
|
if (!latest || t > latest.updated)
|
|
916
1338
|
latest = { actor: s.actor || null, updated: t };
|
|
@@ -920,8 +1342,8 @@ function activeActor(projectDir) {
|
|
|
920
1342
|
return latest ? latest.actor : null;
|
|
921
1343
|
}
|
|
922
1344
|
function resetMission(projectDir, keepLogs, force) {
|
|
923
|
-
const root =
|
|
924
|
-
if (!
|
|
1345
|
+
const root = join11(projectDir, ".mugiwara");
|
|
1346
|
+
if (!existsSync8(root))
|
|
925
1347
|
return { removed: [], kept: [] };
|
|
926
1348
|
if (!force) {
|
|
927
1349
|
const actor = activeActor(projectDir);
|
|
@@ -931,361 +1353,249 @@ function resetMission(projectDir, keepLogs, force) {
|
|
|
931
1353
|
}
|
|
932
1354
|
const removed = [];
|
|
933
1355
|
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)) {
|
|
1356
|
+
for (const dir of ["missions", "spec", "plans", "results", "review", "issues", "reports", "state", "continue"]) {
|
|
1357
|
+
const p = join11(root, dir);
|
|
1358
|
+
if (existsSync8(p)) {
|
|
944
1359
|
rmSync2(p, { recursive: true, force: true });
|
|
945
1360
|
removed.push(dir);
|
|
946
1361
|
}
|
|
947
1362
|
}
|
|
948
|
-
for (const f of
|
|
1363
|
+
for (const f of readdirSync6(root)) {
|
|
949
1364
|
if (/^state(-.+)?\.json$/.test(f)) {
|
|
950
|
-
const p =
|
|
951
|
-
if (
|
|
1365
|
+
const p = join11(root, f);
|
|
1366
|
+
if (existsSync8(p)) {
|
|
952
1367
|
rmSync2(p);
|
|
953
1368
|
removed.push(f);
|
|
954
1369
|
}
|
|
955
1370
|
}
|
|
956
1371
|
}
|
|
957
1372
|
if (!keepLogs) {
|
|
958
|
-
const p
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1373
|
+
for (const p of [join11(root, "lessons.md"), join11(root, "logs")]) {
|
|
1374
|
+
if (existsSync8(p)) {
|
|
1375
|
+
rmSync2(p, { recursive: true, force: true });
|
|
1376
|
+
removed.push(p.startsWith(join11(root, "logs")) ? "logs" : "lessons.md");
|
|
1377
|
+
}
|
|
962
1378
|
}
|
|
963
|
-
} else
|
|
964
|
-
|
|
1379
|
+
} else {
|
|
1380
|
+
if (existsSync8(join11(root, "lessons.md")))
|
|
1381
|
+
kept.push("lessons.md");
|
|
1382
|
+
else if (existsSync8(join11(root, join11("logs", "lessons.md"))))
|
|
1383
|
+
kept.push(join11("logs", "lessons.md"));
|
|
965
1384
|
}
|
|
966
1385
|
for (const f of ["config", "manifest.json", "backup"]) {
|
|
967
|
-
if (
|
|
1386
|
+
if (existsSync8(join11(root, f)))
|
|
968
1387
|
kept.push(f);
|
|
969
1388
|
}
|
|
970
1389
|
return { removed, kept };
|
|
971
1390
|
}
|
|
972
1391
|
function archiveMission(projectDir, mission, opts = {}) {
|
|
973
1392
|
const { dryRun = false } = opts;
|
|
974
|
-
const root =
|
|
1393
|
+
const root = join11(projectDir, ".mugiwara");
|
|
975
1394
|
if (!mission || /[^a-zA-Z0-9._-]/.test(mission) || /^\.+$/.test(mission))
|
|
976
1395
|
throw new Error(`invalid mission name "${mission}" (allowlist: [a-zA-Z0-9._-], not a dot-path)`);
|
|
977
1396
|
const removed = [];
|
|
978
1397
|
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));
|
|
1398
|
+
const dir = join11(root, "missions", mission);
|
|
1399
|
+
if (!existsSync8(dir))
|
|
1400
|
+
return { report: null, removed, kept };
|
|
1401
|
+
if (!dryRun) {
|
|
1402
|
+
const issues = checkTrail(dir, projectDir);
|
|
1403
|
+
if (issues.length) {
|
|
1404
|
+
throw new Error(`closure integrity gate failed — fix these before archiving:
|
|
1405
|
+
${formatIssues(issues)}`);
|
|
997
1406
|
}
|
|
998
1407
|
}
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
removed.push(join7("spec", f));
|
|
1408
|
+
const files = readdirSync6(dir);
|
|
1409
|
+
const state = primaryState(dir, files);
|
|
1410
|
+
const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
|
|
1411
|
+
try {
|
|
1412
|
+
const s = JSON.parse(readFileSync7(join11(dir, f), "utf8"));
|
|
1413
|
+
return typeof s.model === "string" ? s.model : "";
|
|
1414
|
+
} catch {
|
|
1415
|
+
return "";
|
|
1008
1416
|
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
const p = join7(d, f);
|
|
1018
|
-
if (!dryRun)
|
|
1019
|
-
rmSync2(p, { force: true });
|
|
1020
|
-
removed.push(join7(dir, f));
|
|
1417
|
+
}).filter(Boolean))];
|
|
1418
|
+
let costSection = "";
|
|
1419
|
+
if (!dryRun && state) {
|
|
1420
|
+
const chars = measureContextChars(dir);
|
|
1421
|
+
const budget = readBudgetConfig(projectDir);
|
|
1422
|
+
const footprintLine = formatFootprint(chars, budget);
|
|
1423
|
+
if (budget && chars > budget) {
|
|
1424
|
+
throw new Error(`closure context budget failed — ${footprintLine}. Trim the trail or raise context_budget_chars.`);
|
|
1021
1425
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1426
|
+
const est = typeof state.tokens_est === "number" ? state.tokens_est : 0;
|
|
1427
|
+
const src = typeof state.tokens_source === "string" ? state.tokens_source : "computed";
|
|
1428
|
+
const lane = typeof state.lane === "string" ? state.lane : "unknown";
|
|
1429
|
+
const laneBudget = lane === "lean" ? 12000 : lane === "standard" ? 25000 : lane === "full" ? 50000 : lane === "spike" ? 3000 : 0;
|
|
1430
|
+
const effBudget = budget || laneBudget;
|
|
1431
|
+
const pct = effBudget ? Math.round(est / effBudget * 100) : 0;
|
|
1432
|
+
const delta = effBudget ? est <= effBudget ? `${(effBudget - est).toLocaleString()} under` : `${(est - effBudget).toLocaleString()} over` : "no budget configured";
|
|
1433
|
+
const srcLabel = src === "reported" ? "provider-reported" : "estimator";
|
|
1434
|
+
let reportedTotal = 0;
|
|
1435
|
+
let hasReported = false;
|
|
1436
|
+
for (const f of files.filter(isStateFile)) {
|
|
1437
|
+
try {
|
|
1438
|
+
const s = JSON.parse(readFileSync7(join11(dir, f), "utf8"));
|
|
1439
|
+
if (s.tokens_source === "reported" && typeof s.tokens_est === "number") {
|
|
1440
|
+
reportedTotal += s.tokens_est;
|
|
1441
|
+
hasReported = true;
|
|
1442
|
+
}
|
|
1443
|
+
} catch {}
|
|
1032
1444
|
}
|
|
1445
|
+
if (!hasReported && src === "reported" && est > 0) {
|
|
1446
|
+
reportedTotal = est;
|
|
1447
|
+
hasReported = true;
|
|
1448
|
+
}
|
|
1449
|
+
costSection = [
|
|
1450
|
+
"## Cost",
|
|
1451
|
+
"",
|
|
1452
|
+
"| Metric | Value |",
|
|
1453
|
+
"|--------|-------|",
|
|
1454
|
+
`| **Tokens used** | ${est.toLocaleString()} (${srcLabel}) |`,
|
|
1455
|
+
`| **Lane** | ${lane} (budget ${effBudget ? effBudget.toLocaleString() : "—"} · warn ${effBudget ? Math.round(effBudget * 1.5).toLocaleString() : "—"} · stop ${effBudget ? (effBudget * 3).toLocaleString() : "—"}) |`,
|
|
1456
|
+
`| **Budget status** | ${effBudget ? `${pct}% of budget · ${delta} · ${est >= effBudget * 3 ? "STOP" : est >= effBudget * 1.5 ? "WARN" : "OK"}` : "no lane budget"} |`,
|
|
1457
|
+
`| **Context footprint** | ${chars.toLocaleString()} chars${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`
|
|
1458
|
+
].join(`
|
|
1459
|
+
`);
|
|
1460
|
+
if (hasReported) {
|
|
1461
|
+
costSection += `
|
|
1462
|
+
| **Provider total** | ${reportedTotal.toLocaleString()} (provider-reported — sum of reported stages) |`;
|
|
1463
|
+
}
|
|
1464
|
+
costSection += `
|
|
1465
|
+
`;
|
|
1033
1466
|
}
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
}
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1467
|
+
const FOLD_TOP = ["decisions.md", "blockers.md", "review.md", "security.md", "spec.md"];
|
|
1468
|
+
const fold = [];
|
|
1469
|
+
for (const f of FOLD_TOP) {
|
|
1470
|
+
if (files.includes(f))
|
|
1471
|
+
fold.push(f);
|
|
1472
|
+
}
|
|
1473
|
+
const flowsDir = join11(dir, "flows");
|
|
1474
|
+
const legacyWavesDir = join11(dir, "waves");
|
|
1475
|
+
const artDir = existsSync8(flowsDir) ? flowsDir : existsSync8(legacyWavesDir) ? legacyWavesDir : flowsDir;
|
|
1476
|
+
const artRel = artDir === legacyWavesDir ? "waves" : "flows";
|
|
1477
|
+
if (existsSync8(artDir)) {
|
|
1478
|
+
for (const f of readdirSync6(artDir).sort())
|
|
1479
|
+
fold.push(join11(artRel, f));
|
|
1480
|
+
}
|
|
1481
|
+
let report = "";
|
|
1482
|
+
const reportPath = join11(dir, "report.md");
|
|
1483
|
+
if (files.includes("report.md"))
|
|
1484
|
+
report = readFileSync7(reportPath, "utf8");
|
|
1485
|
+
else if (existsSync8(join11(artDir, "06-closure.md")))
|
|
1486
|
+
report = readFileSync7(join11(artDir, "06-closure.md"), "utf8");
|
|
1487
|
+
if (!dryRun) {
|
|
1488
|
+
mkdirSync5(dir, { recursive: true });
|
|
1489
|
+
if (fold.length) {
|
|
1490
|
+
const sections = fold.map((f) => {
|
|
1491
|
+
const body = readFileSync7(join11(dir, f), "utf8").trim();
|
|
1492
|
+
const name = f.includes("/") ? f.split("/").pop() ?? f : f;
|
|
1493
|
+
return `
|
|
1494
|
+
|
|
1495
|
+
## Archived: ${name}
|
|
1496
|
+
|
|
1497
|
+
${body}`;
|
|
1498
|
+
}).join("");
|
|
1499
|
+
const tmp = `${reportPath}.tmp`;
|
|
1500
|
+
const routingSection = state ? renderRouting(rankFiles(changedFiles(projectDir, state), {
|
|
1501
|
+
mission,
|
|
1502
|
+
evidence: Array.isArray(state.evidence) ? state.evidence : [],
|
|
1503
|
+
sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
|
|
1504
|
+
}), mission) : "";
|
|
1505
|
+
writeFileSync7(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
|
|
1506
|
+
${costSection}
|
|
1507
|
+
` : "") + `
|
|
1508
|
+
`);
|
|
1509
|
+
renameSync(tmp, reportPath);
|
|
1510
|
+
}
|
|
1511
|
+
for (const f of fold) {
|
|
1512
|
+
rmSync2(join11(dir, f), { force: true, recursive: true });
|
|
1513
|
+
removed.push(join11("missions", mission, f));
|
|
1514
|
+
}
|
|
1515
|
+
for (const f of files.filter((f2) => f2.endsWith(".json")))
|
|
1516
|
+
rmSync2(join11(dir, f), { force: true });
|
|
1517
|
+
if (existsSync8(artDir) && readdirSync6(artDir).length === 0)
|
|
1518
|
+
rmSync2(artDir, { recursive: true, force: true });
|
|
1519
|
+
if (!existsSync8(reportPath)) {
|
|
1520
|
+
writeFileSync7(reportPath, `# Mission: ${mission}
|
|
1521
|
+
|
|
1522
|
+
Archived before closure — no wave artifacts were present.
|
|
1523
|
+
`);
|
|
1524
|
+
}
|
|
1525
|
+
if (state && typeof state.branch === "string") {
|
|
1526
|
+
const rb = generateRollback(projectDir, dir, {
|
|
1527
|
+
mission,
|
|
1528
|
+
branch: state.branch,
|
|
1529
|
+
baseSha: typeof state.base_sha === "string" ? state.base_sha : "unknown"
|
|
1530
|
+
});
|
|
1531
|
+
if (rb)
|
|
1532
|
+
kept.push(join11("missions", mission, rb.file));
|
|
1533
|
+
try {
|
|
1534
|
+
writeProvenance(projectDir, dir, {
|
|
1535
|
+
mission,
|
|
1536
|
+
actor: typeof state.actor === "string" ? state.actor : "",
|
|
1537
|
+
lane: typeof state.lane === "string" ? state.lane : "",
|
|
1538
|
+
mode: typeof state.mode === "string" ? state.mode : "",
|
|
1539
|
+
branch: state.branch,
|
|
1540
|
+
tasks_done: Number(state.tasks_done) || 0,
|
|
1541
|
+
tasks_total: Number(state.tasks_total) || 0,
|
|
1542
|
+
evidence: Array.isArray(state.evidence) ? state.evidence : [],
|
|
1543
|
+
models: stageModels
|
|
1544
|
+
});
|
|
1545
|
+
kept.push(join11("missions", mission, "provenance.md"));
|
|
1546
|
+
} catch {}
|
|
1547
|
+
}
|
|
1548
|
+
} else {
|
|
1549
|
+
for (const f of fold)
|
|
1550
|
+
removed.push(join11("missions", mission, f));
|
|
1551
|
+
}
|
|
1552
|
+
removed.push(join11("missions", mission, "<session state>"));
|
|
1553
|
+
if (files.includes("plan.md"))
|
|
1554
|
+
kept.push(join11("missions", mission, "plan.md"));
|
|
1555
|
+
if (files.includes("handoff.md"))
|
|
1556
|
+
kept.push(join11("missions", mission, "handoff.md"));
|
|
1557
|
+
kept.push(join11("missions", mission, "report.md"));
|
|
1052
1558
|
let index;
|
|
1053
|
-
const indexFile =
|
|
1054
|
-
const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}
|
|
1559
|
+
const indexFile = join11(root, "index.md");
|
|
1560
|
+
const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}
|
|
1055
1561
|
`;
|
|
1056
1562
|
if (!dryRun) {
|
|
1057
|
-
|
|
1058
|
-
const existing = existsSync6(indexFile) ? readFileSync5(indexFile, "utf8") : "";
|
|
1563
|
+
const existing = existsSync8(indexFile) ? readFileSync7(indexFile, "utf8") : "";
|
|
1059
1564
|
if (!existing.split(/\r?\n/).some((l) => l.startsWith(`- ${mission} —`))) {
|
|
1060
1565
|
const header = existing ? "" : `# Mission index
|
|
1061
1566
|
|
|
1062
1567
|
`;
|
|
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;
|
|
1568
|
+
const fd = openSync(indexFile, "a");
|
|
1569
|
+
try {
|
|
1570
|
+
writeSync(fd, header + line);
|
|
1571
|
+
} finally {
|
|
1572
|
+
closeSync(fd);
|
|
1176
1573
|
}
|
|
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
1574
|
}
|
|
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();
|
|
1575
|
+
index = "index.md";
|
|
1267
1576
|
}
|
|
1577
|
+
return { report: join11("missions", mission, "report.md"), removed, kept, index };
|
|
1268
1578
|
}
|
|
1269
1579
|
|
|
1270
1580
|
// src/run.ts
|
|
1271
|
-
import { existsSync as
|
|
1581
|
+
import { existsSync as existsSync9, readdirSync as readdirSync7 } from "node:fs";
|
|
1272
1582
|
import { spawnSync } from "node:child_process";
|
|
1273
|
-
import { dirname as dirname5, join as
|
|
1583
|
+
import { dirname as dirname5, join as join12, resolve } from "node:path";
|
|
1274
1584
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1275
1585
|
var here3 = dirname5(fileURLToPath4(import.meta.url));
|
|
1276
|
-
var SCRIPTS_DIR =
|
|
1277
|
-
var RUNNABLE = ["savepoint.sh", "lane.sh"
|
|
1586
|
+
var SCRIPTS_DIR = join12(here3, "..", "scripts");
|
|
1587
|
+
var RUNNABLE = ["savepoint.sh", "lane.sh"];
|
|
1278
1588
|
function findBash() {
|
|
1279
1589
|
const explicit = process.env.MUGIWARA_BASH?.trim();
|
|
1280
1590
|
if (explicit)
|
|
1281
|
-
return
|
|
1591
|
+
return existsSync9(explicit) ? explicit : null;
|
|
1282
1592
|
const candidates = process.platform === "win32" ? [
|
|
1283
1593
|
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
1284
1594
|
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
1285
|
-
|
|
1595
|
+
join12(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
|
|
1286
1596
|
] : ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash", "/opt/homebrew/bin/bash"];
|
|
1287
1597
|
for (const p of candidates) {
|
|
1288
|
-
if (p &&
|
|
1598
|
+
if (p && existsSync9(p))
|
|
1289
1599
|
return p;
|
|
1290
1600
|
}
|
|
1291
1601
|
const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bash"], {
|
|
@@ -1302,9 +1612,9 @@ function runScript(name, args, projectDir) {
|
|
|
1302
1612
|
if (!/^[a-z0-9-]+\.sh$/.test(name)) {
|
|
1303
1613
|
throw new Error(`invalid script name "${name}" (expected one of: ${RUNNABLE.join(", ")})`);
|
|
1304
1614
|
}
|
|
1305
|
-
const script =
|
|
1306
|
-
if (!
|
|
1307
|
-
const have =
|
|
1615
|
+
const script = join12(SCRIPTS_DIR, name);
|
|
1616
|
+
if (!existsSync9(script)) {
|
|
1617
|
+
const have = existsSync9(SCRIPTS_DIR) ? readdirSync7(SCRIPTS_DIR).filter((f) => f.endsWith(".sh")) : [];
|
|
1308
1618
|
throw new Error(`script not found: ${name}${have.length ? ` (available: ${have.join(", ")})` : ""}`);
|
|
1309
1619
|
}
|
|
1310
1620
|
const bash = findBash();
|
|
@@ -1321,9 +1631,9 @@ function runScript(name, args, projectDir) {
|
|
|
1321
1631
|
}
|
|
1322
1632
|
|
|
1323
1633
|
// src/continue.ts
|
|
1324
|
-
import { existsSync as
|
|
1325
|
-
import { execFileSync } from "node:child_process";
|
|
1326
|
-
import { join as
|
|
1634
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync8 } from "node:fs";
|
|
1635
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
1636
|
+
import { join as join13 } from "node:path";
|
|
1327
1637
|
var SAFE = /^[A-Za-z0-9._-]+$/;
|
|
1328
1638
|
var isSafeKey = (s) => SAFE.test(s) && !/^\.+$/.test(s);
|
|
1329
1639
|
var num = (v) => {
|
|
@@ -1338,34 +1648,46 @@ function gitActor(cwd) {
|
|
|
1338
1648
|
const author = (process.env.GIT_AUTHOR_NAME ?? "").trim();
|
|
1339
1649
|
if (author)
|
|
1340
1650
|
return author;
|
|
1341
|
-
const
|
|
1651
|
+
const git3 = (args) => {
|
|
1342
1652
|
try {
|
|
1343
|
-
return
|
|
1653
|
+
return execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1344
1654
|
} catch {
|
|
1345
1655
|
return "";
|
|
1346
1656
|
}
|
|
1347
1657
|
};
|
|
1348
|
-
const name =
|
|
1349
|
-
const email =
|
|
1658
|
+
const name = git3(["config", "user.name"]);
|
|
1659
|
+
const email = git3(["config", "user.email"]);
|
|
1350
1660
|
if (name && email)
|
|
1351
1661
|
return `${name} <${email}>`;
|
|
1352
1662
|
return name || process.env.USER || process.env.USERNAME || "";
|
|
1353
1663
|
}
|
|
1354
|
-
function scan(projectDir,
|
|
1355
|
-
const base =
|
|
1356
|
-
if (!
|
|
1664
|
+
function scan(projectDir, kind, map) {
|
|
1665
|
+
const base = join13(projectDir, ".mugiwara", "missions");
|
|
1666
|
+
if (!existsSync10(base))
|
|
1357
1667
|
return [];
|
|
1358
1668
|
const out = [];
|
|
1359
|
-
const missions =
|
|
1669
|
+
const missions = readdirSync8(base, { withFileTypes: true }).filter((e) => e.isDirectory() && isSafeKey(e.name)).map((e) => e.name).sort();
|
|
1360
1670
|
for (const mission of missions) {
|
|
1361
|
-
const dir =
|
|
1362
|
-
for (const f of
|
|
1671
|
+
const dir = join13(base, mission);
|
|
1672
|
+
for (const f of readdirSync8(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
1363
1673
|
const stem = f.slice(0, -".json".length);
|
|
1364
|
-
|
|
1674
|
+
let member;
|
|
1675
|
+
if (kind === "state") {
|
|
1676
|
+
if (stem === "continue" || stem.startsWith("continue-"))
|
|
1677
|
+
continue;
|
|
1678
|
+
member = stem === "state" ? null : stem;
|
|
1679
|
+
} else {
|
|
1680
|
+
if (stem === "continue")
|
|
1681
|
+
member = null;
|
|
1682
|
+
else if (stem.startsWith("continue-"))
|
|
1683
|
+
member = stem.slice("continue-".length);
|
|
1684
|
+
else
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1365
1687
|
if (member !== null && !isSafeKey(member))
|
|
1366
1688
|
continue;
|
|
1367
1689
|
try {
|
|
1368
|
-
const raw = JSON.parse(
|
|
1690
|
+
const raw = JSON.parse(readFileSync8(join13(dir, f), "utf8"));
|
|
1369
1691
|
if (text(raw.mission) !== mission)
|
|
1370
1692
|
continue;
|
|
1371
1693
|
out.push(map(raw, member));
|
|
@@ -1407,8 +1729,10 @@ function readState(projectDir) {
|
|
|
1407
1729
|
lane_rose: r.lane_rose === true,
|
|
1408
1730
|
lane_prev: text(r.lane_prev),
|
|
1409
1731
|
lane_peak: text(r.lane_peak),
|
|
1410
|
-
|
|
1411
|
-
|
|
1732
|
+
base_sha: text(r.base_sha),
|
|
1733
|
+
sensitive_paths: Array.isArray(r.sensitive_paths) ? r.sensitive_paths.map(text).filter(Boolean) : [],
|
|
1734
|
+
next_action: text(r.next_action),
|
|
1735
|
+
next_session_prompt: text(r.next_session_prompt),
|
|
1412
1736
|
updated_at: text(r.updated_at),
|
|
1413
1737
|
blockers_open: num(r.blockers_open),
|
|
1414
1738
|
heal_cycle: num(r.heal_cycle),
|
|
@@ -1478,6 +1802,59 @@ function formatResume(e) {
|
|
|
1478
1802
|
return `Resumed: ${e.mission}${scope}, Flow ${e.flow}, ${e.tasks_done}/${e.tasks_total} tasks — next_action: ${e.next_action} — run: ${next}`;
|
|
1479
1803
|
}
|
|
1480
1804
|
|
|
1805
|
+
// src/sign.ts
|
|
1806
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
1807
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
1808
|
+
import { homedir as homedir3 } from "node:os";
|
|
1809
|
+
import { join as join14 } from "node:path";
|
|
1810
|
+
function signArgs(reportPath, secretKey) {
|
|
1811
|
+
return ["-Sm", reportPath, "-s", secretKey];
|
|
1812
|
+
}
|
|
1813
|
+
function verifyArgs(reportPath, pubKey) {
|
|
1814
|
+
return pubKey ? ["-Vm", reportPath, "-p", pubKey] : ["-Vm", reportPath];
|
|
1815
|
+
}
|
|
1816
|
+
function hasMinisign() {
|
|
1817
|
+
try {
|
|
1818
|
+
execFileSync5("minisign", ["-v"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
1819
|
+
return true;
|
|
1820
|
+
} catch {
|
|
1821
|
+
return false;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
function defaultKey(flag) {
|
|
1825
|
+
return join14(homedir3(), ".mugiwara", flag === "secret" ? "minisign.key" : "minisign.pub");
|
|
1826
|
+
}
|
|
1827
|
+
function signReport(projectDir, missionDir) {
|
|
1828
|
+
const report = join14(missionDir, "report.md");
|
|
1829
|
+
if (!existsSync11(report))
|
|
1830
|
+
return { ok: false, message: "no report.md to sign — archive first" };
|
|
1831
|
+
if (!hasMinisign()) {
|
|
1832
|
+
return { ok: false, message: "minisign not installed — signing skipped (install minisign to enable attestation)" };
|
|
1833
|
+
}
|
|
1834
|
+
const secretKey = process.env.MUGIWARA_SIGN_KEY?.trim() || defaultKey("secret");
|
|
1835
|
+
try {
|
|
1836
|
+
execFileSync5("minisign", signArgs(report, secretKey), { cwd: projectDir, stdio: "pipe", input: process.env.MUGIWARA_SIGN_PASSWORD ?? "" });
|
|
1837
|
+
return { ok: true, message: `signed ${report}.minisig (key: ${secretKey})` };
|
|
1838
|
+
} catch (e) {
|
|
1839
|
+
return { ok: false, message: `signing failed: ${e.message}` };
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
function verifyReport(projectDir, missionDir) {
|
|
1843
|
+
const report = join14(missionDir, "report.md");
|
|
1844
|
+
const sig = `${report}.minisig`;
|
|
1845
|
+
if (!existsSync11(sig))
|
|
1846
|
+
return { ok: false, message: "not signed (no .minisig beside report.md)" };
|
|
1847
|
+
if (!hasMinisign())
|
|
1848
|
+
return { ok: false, message: "minisign not installed — cannot verify" };
|
|
1849
|
+
const pubKey = existsSync11(defaultKey("public")) ? defaultKey("public") : null;
|
|
1850
|
+
try {
|
|
1851
|
+
execFileSync5("minisign", verifyArgs(report, pubKey), { cwd: projectDir, stdio: "pipe" });
|
|
1852
|
+
return { ok: true, message: "signature verifies against report.md" };
|
|
1853
|
+
} catch {
|
|
1854
|
+
return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing" };
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1481
1858
|
// src/cli.ts
|
|
1482
1859
|
var str = (v) => typeof v === "string" ? v : undefined;
|
|
1483
1860
|
var flag = (v) => v === true;
|
|
@@ -1502,8 +1879,8 @@ async function run(argv) {
|
|
|
1502
1879
|
return resetCmd(flags);
|
|
1503
1880
|
case "archive":
|
|
1504
1881
|
return archive(flags, _);
|
|
1505
|
-
case "
|
|
1506
|
-
return
|
|
1882
|
+
case "clean":
|
|
1883
|
+
return cleanCmd(flags);
|
|
1507
1884
|
case "continue":
|
|
1508
1885
|
return continueCmd(flags, _);
|
|
1509
1886
|
case "status":
|
|
@@ -1512,10 +1889,12 @@ async function run(argv) {
|
|
|
1512
1889
|
return runCmd(flags, _);
|
|
1513
1890
|
case "savepoint":
|
|
1514
1891
|
return runCmd(flags, ["run", "savepoint.sh", ..._.slice(1)]);
|
|
1515
|
-
case "
|
|
1516
|
-
return
|
|
1517
|
-
case "
|
|
1518
|
-
return
|
|
1892
|
+
case "blame":
|
|
1893
|
+
return blameCmd(flags, _);
|
|
1894
|
+
case "handoff":
|
|
1895
|
+
return handoffCmd(flags, _);
|
|
1896
|
+
case "sign":
|
|
1897
|
+
return signCmd(flags, _);
|
|
1519
1898
|
default:
|
|
1520
1899
|
throw new Error(`Unknown command: ${command}`);
|
|
1521
1900
|
}
|
|
@@ -1545,6 +1924,8 @@ function archive(flags, positionals) {
|
|
|
1545
1924
|
const result = archiveMission(projectDir, mission, { dryRun: flag(flags.dryRun) });
|
|
1546
1925
|
if (result.report)
|
|
1547
1926
|
console.log(`archive target: ${result.report}`);
|
|
1927
|
+
else
|
|
1928
|
+
console.error(`no mission dir for "${mission}" under .mugiwara/missions/`);
|
|
1548
1929
|
if (result.removed.length)
|
|
1549
1930
|
console.log(`${flag(flags.dryRun) ? "would remove" : "removed"}: ${result.removed.join(", ")}`);
|
|
1550
1931
|
if (result.kept.length)
|
|
@@ -1552,11 +1933,59 @@ function archive(flags, positionals) {
|
|
|
1552
1933
|
if (result.index)
|
|
1553
1934
|
console.log(`index updated: ${result.index}`);
|
|
1554
1935
|
}
|
|
1555
|
-
|
|
1936
|
+
function cleanCmd(flags) {
|
|
1556
1937
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1938
|
+
const dryRun = flag(flags.dryRun);
|
|
1939
|
+
const root = join15(projectDir, ".mugiwara", "missions");
|
|
1940
|
+
if (!existsSync12(root)) {
|
|
1941
|
+
console.log("nothing to clean (.mugiwara/missions/ does not exist).");
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
const before = str(flags.before);
|
|
1945
|
+
const beforeMs = before ? Date.parse(before) : NaN;
|
|
1946
|
+
if (before && !Number.isFinite(beforeMs)) {
|
|
1947
|
+
console.error(`invalid --before date: ${before}`);
|
|
1948
|
+
process.exit(1);
|
|
1949
|
+
}
|
|
1950
|
+
let candidates = readdirSync9(root, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Za-z0-9._-]+$/.test(e.name) && !/^\.+$/.test(e.name)).map((e) => e.name);
|
|
1951
|
+
const stateFiles = (m) => readdirSync9(join15(root, m)).filter((f) => {
|
|
1952
|
+
const stem = f.replace(/\.json$/, "");
|
|
1953
|
+
return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
|
|
1954
|
+
});
|
|
1955
|
+
const hasLiveState = (m) => stateFiles(m).length > 0;
|
|
1956
|
+
const staleBefore = (m) => {
|
|
1957
|
+
if (!Number.isFinite(beforeMs))
|
|
1958
|
+
return false;
|
|
1959
|
+
for (const f of stateFiles(m)) {
|
|
1960
|
+
try {
|
|
1961
|
+
const ts = Date.parse(JSON.parse(readFileSync9(join15(root, m, f), "utf8")).updated_at ?? "") || 0;
|
|
1962
|
+
if (ts === 0 || ts >= beforeMs)
|
|
1963
|
+
return false;
|
|
1964
|
+
} catch {
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
return true;
|
|
1969
|
+
};
|
|
1970
|
+
if (!flag(flags.all)) {
|
|
1971
|
+
candidates = candidates.filter((m) => existsSync12(join15(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
|
|
1972
|
+
} else if (!flag(flags.force)) {
|
|
1973
|
+
const live = candidates.filter((m) => hasLiveState(m) && !staleBefore(m));
|
|
1974
|
+
if (live.length) {
|
|
1975
|
+
console.error(`✗ in-flight mission(s): ${live.join(", ")}. Use --force to archive them anyway.`);
|
|
1976
|
+
process.exit(1);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
if (!candidates.length) {
|
|
1980
|
+
console.log("nothing to clean.");
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
for (const m of candidates) {
|
|
1984
|
+
const r = archiveMission(projectDir, m, { dryRun });
|
|
1985
|
+
console.log(`${dryRun ? "would clean" : "cleaned"} ${m}${r.report ? ` → ${r.report}` : ""}`);
|
|
1986
|
+
if (r.index)
|
|
1987
|
+
console.log(`index updated: ${r.index}`);
|
|
1988
|
+
}
|
|
1560
1989
|
}
|
|
1561
1990
|
async function resolveOptions(flags) {
|
|
1562
1991
|
const interactive = !flag(flags.yes);
|
|
@@ -1573,7 +2002,7 @@ async function resolveOptions(flags) {
|
|
|
1573
2002
|
scope = await choose(rl, "Install scope?", ["global (user-wide)", "project (this repo)"]) === 0 ? "global" : "project";
|
|
1574
2003
|
}
|
|
1575
2004
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1576
|
-
if (scope === "project" && !
|
|
2005
|
+
if (scope === "project" && !existsSync12(projectDir))
|
|
1577
2006
|
throw new Error(`Project dir not found: ${projectDir}`);
|
|
1578
2007
|
let targetIds = str(flags.target)?.split(",").map((s) => s.trim()) ?? null;
|
|
1579
2008
|
if (targetIds && targetIds.includes("all"))
|
|
@@ -1598,7 +2027,7 @@ async function resolveOptions(flags) {
|
|
|
1598
2027
|
}
|
|
1599
2028
|
async function install(flags) {
|
|
1600
2029
|
const { scope, projectDir, targetIds } = await resolveOptions(flags);
|
|
1601
|
-
const home =
|
|
2030
|
+
const home = homedir4();
|
|
1602
2031
|
const allFiles = [];
|
|
1603
2032
|
const allNotes = [];
|
|
1604
2033
|
const installed = [];
|
|
@@ -1640,34 +2069,19 @@ Dry run — nothing written.`);
|
|
|
1640
2069
|
OK mugiwara ${VERSION} installed (manifest: ${file})`);
|
|
1641
2070
|
if (allNotes.length)
|
|
1642
2071
|
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
|
-
}
|
|
2072
|
+
console.log(`
|
|
2073
|
+
Next: edit .mugiwara/config to customise (mode, branch, coverage, depths).`);
|
|
1660
2074
|
}
|
|
1661
2075
|
async function uninstall(flags) {
|
|
1662
2076
|
const scope = flag(flags.global) ? "global" : "project";
|
|
1663
2077
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1664
|
-
const home =
|
|
2078
|
+
const home = homedir4();
|
|
1665
2079
|
const file = manifestPath({ scope, projectDir, home });
|
|
1666
2080
|
const manifest = readManifest(file);
|
|
1667
2081
|
if (!manifest) {
|
|
1668
2082
|
if (scope === "project") {
|
|
1669
2083
|
const globalFile = manifestPath({ scope: "global", projectDir, home });
|
|
1670
|
-
if (
|
|
2084
|
+
if (existsSync12(globalFile)) {
|
|
1671
2085
|
console.log(`No project manifest found, but a global install exists. Try:
|
|
1672
2086
|
mugiwara uninstall --global`);
|
|
1673
2087
|
return;
|
|
@@ -1704,13 +2118,13 @@ async function uninstall(flags) {
|
|
|
1704
2118
|
console.log(` note: ${n}`);
|
|
1705
2119
|
}
|
|
1706
2120
|
rmSync3(file);
|
|
1707
|
-
const mugiDir =
|
|
1708
|
-
if (
|
|
2121
|
+
const mugiDir = join15(scope === "global" ? home : projectDir, ".mugiwara");
|
|
2122
|
+
if (existsSync12(mugiDir) && readdirSync9(mugiDir).length === 0) {
|
|
1709
2123
|
rmSync3(mugiDir, { recursive: true, force: true });
|
|
1710
2124
|
}
|
|
1711
2125
|
if (manifest.targets.includes("opencode")) {
|
|
1712
|
-
const opencodeCache =
|
|
1713
|
-
if (
|
|
2126
|
+
const opencodeCache = join15(home, ".cache", "opencode", "packages", "@ionivetech");
|
|
2127
|
+
if (existsSync12(opencodeCache)) {
|
|
1714
2128
|
rmSync3(opencodeCache, { recursive: true, force: true });
|
|
1715
2129
|
console.log(" cleared opencode npm cache (stale plugin versions)");
|
|
1716
2130
|
}
|
|
@@ -1719,7 +2133,7 @@ async function uninstall(flags) {
|
|
|
1719
2133
|
console.log(`OK removed ${removed.length} files`);
|
|
1720
2134
|
}
|
|
1721
2135
|
function list(flags) {
|
|
1722
|
-
const home =
|
|
2136
|
+
const home = homedir4();
|
|
1723
2137
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1724
2138
|
let found = false;
|
|
1725
2139
|
for (const [label, file] of [
|
|
@@ -1731,7 +2145,7 @@ function list(flags) {
|
|
|
1731
2145
|
continue;
|
|
1732
2146
|
found = true;
|
|
1733
2147
|
if (flag(flags.check)) {
|
|
1734
|
-
const missing = m.files.filter((f) => !
|
|
2148
|
+
const missing = m.files.filter((f) => !existsSync12(f));
|
|
1735
2149
|
console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
|
|
1736
2150
|
} else {
|
|
1737
2151
|
console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} installed=${m.installedAt}`);
|
|
@@ -1753,6 +2167,10 @@ function continueCmd(flags, positionals) {
|
|
|
1753
2167
|
const r = resolveContinue(entries, mission, member);
|
|
1754
2168
|
if (r.kind === "resume") {
|
|
1755
2169
|
console.log(formatResume(r.entry));
|
|
2170
|
+
const st = readState(projectDir).find((s) => s.mission === r.entry.mission && s.member === r.entry.member);
|
|
2171
|
+
const stale = st ? stalenessLine(projectDir, st.base_sha) : null;
|
|
2172
|
+
if (stale)
|
|
2173
|
+
console.log(stale);
|
|
1756
2174
|
return;
|
|
1757
2175
|
}
|
|
1758
2176
|
if (r.kind === "none") {
|
|
@@ -1809,85 +2227,104 @@ function runCmd(flags, positionals) {
|
|
|
1809
2227
|
if (code !== 0)
|
|
1810
2228
|
process.exit(code);
|
|
1811
2229
|
}
|
|
1812
|
-
function
|
|
2230
|
+
function blameCmd(flags, positionals) {
|
|
1813
2231
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1814
|
-
const
|
|
1815
|
-
if (!
|
|
1816
|
-
console.error("usage: mugiwara
|
|
2232
|
+
const path = positionals[1];
|
|
2233
|
+
if (!path) {
|
|
2234
|
+
console.error("usage: mugiwara blame <file-path>");
|
|
1817
2235
|
process.exit(1);
|
|
1818
2236
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
2237
|
+
console.log(blamePath(projectDir, path));
|
|
2238
|
+
}
|
|
2239
|
+
function stalenessLine(projectDir, baseSha) {
|
|
2240
|
+
if (!baseSha || baseSha === "unknown")
|
|
2241
|
+
return null;
|
|
2242
|
+
const git3 = (args) => {
|
|
2243
|
+
try {
|
|
2244
|
+
return execFileSync6("git", args, { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
2245
|
+
} catch {
|
|
2246
|
+
return "";
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
let main = "";
|
|
2250
|
+
for (const ref of ["main", "master"]) {
|
|
2251
|
+
main = git3(["rev-parse", "--verify", ref]);
|
|
2252
|
+
if (main)
|
|
2253
|
+
break;
|
|
2254
|
+
}
|
|
2255
|
+
if (!main)
|
|
2256
|
+
return null;
|
|
2257
|
+
try {
|
|
2258
|
+
const behind = Number(git3(["rev-list", "--count", `${baseSha}..${main}`])) || 0;
|
|
2259
|
+
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;
|
|
2260
|
+
} catch {
|
|
2261
|
+
return null;
|
|
2262
|
+
}
|
|
1830
2263
|
}
|
|
1831
|
-
function
|
|
2264
|
+
function handoffCmd(flags, positionals) {
|
|
1832
2265
|
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1833
2266
|
const mission = positionals[1];
|
|
1834
2267
|
if (!mission) {
|
|
1835
|
-
console.error("usage: mugiwara
|
|
2268
|
+
console.error("usage: mugiwara handoff <mission> [--project <dir>]");
|
|
1836
2269
|
process.exit(1);
|
|
1837
2270
|
}
|
|
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}`);
|
|
2271
|
+
const states = readState(projectDir).filter((s) => s.mission === mission);
|
|
2272
|
+
if (!states.length) {
|
|
2273
|
+
console.error(`no in-flight mission "${mission}"`);
|
|
1852
2274
|
process.exit(1);
|
|
1853
2275
|
}
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2276
|
+
const lines = [
|
|
2277
|
+
`# Handoff: ${mission}`,
|
|
2278
|
+
"",
|
|
2279
|
+
`Generated ${new Date().toISOString()} by \`mugiwara handoff\`.`,
|
|
2280
|
+
"",
|
|
2281
|
+
"| | |",
|
|
2282
|
+
"|---|---|"
|
|
2283
|
+
];
|
|
2284
|
+
for (const s of states) {
|
|
2285
|
+
const scope = s.member ? ` [${s.member}]` : "";
|
|
2286
|
+
lines.push(`| Mission${scope} | flow ${s.flow}, tasks ${s.tasks_done}/${s.tasks_total}, lane ${s.lane}${s.lane_rose ? " (rose)" : ""}, mode ${s.mode} |`);
|
|
2287
|
+
lines.push(`| Branch | \`${s.branch}\` |`);
|
|
2288
|
+
lines.push(`| Actor | ${s.actor || "(unknown)"} |`);
|
|
2289
|
+
if (s.next_action)
|
|
2290
|
+
lines.push(`| Next action | ${s.next_action} |`);
|
|
2291
|
+
if (s.blockers_open)
|
|
2292
|
+
lines.push(`| Open blockers | ${s.blockers_open} |`);
|
|
2293
|
+
if (s.heal_cycle)
|
|
2294
|
+
lines.push(`| Heal cycles | ${s.heal_cycle}/${s.heal_max_cycles}${s.heal_halt ? " — HALTED" : ""} |`);
|
|
2295
|
+
if (s.evidence.length)
|
|
2296
|
+
lines.push(`| Evidence | ${s.evidence.join(", ")} |`);
|
|
2297
|
+
const stale = stalenessLine(projectDir, s.base_sha);
|
|
2298
|
+
if (stale)
|
|
2299
|
+
lines.push(`| Staleness | ${stale.replace("⚠ stale base: ", "")} |`);
|
|
2300
|
+
}
|
|
2301
|
+
lines.push("", "## Resuming", "", `\`mugiwara continue ${mission}\` prints the exact resume point.`);
|
|
2302
|
+
lines.push("Verify `next_action` against plan.md before executing — the table above is computed state, not judgement.");
|
|
2303
|
+
const out = join15(".mugiwara", "missions", mission, "handoff.md");
|
|
2304
|
+
writeFileSync8(resolve2(projectDir, out), lines.join(`
|
|
2305
|
+
`) + `
|
|
2306
|
+
`);
|
|
2307
|
+
console.log(lines.join(`
|
|
2308
|
+
`));
|
|
2309
|
+
console.log(`
|
|
2310
|
+
written: ${out}`);
|
|
2311
|
+
}
|
|
2312
|
+
function signCmd(flags, _) {
|
|
2313
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2314
|
+
const mission = _[1];
|
|
2315
|
+
if (!mission) {
|
|
2316
|
+
console.error("usage: mugiwara sign <mission> [--verify] [--project <dir>]");
|
|
2317
|
+
process.exit(1);
|
|
1875
2318
|
}
|
|
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);
|
|
2319
|
+
const missionDir = join15(projectDir, ".mugiwara", "missions", mission);
|
|
2320
|
+
if (!existsSync12(missionDir)) {
|
|
2321
|
+
console.error(`no mission dir: ${missionDir}`);
|
|
2322
|
+
process.exit(1);
|
|
1889
2323
|
}
|
|
1890
|
-
|
|
2324
|
+
const r = flag(flags.verify) ? verifyReport(projectDir, missionDir) : signReport(projectDir, missionDir);
|
|
2325
|
+
console.log(`${r.ok ? "✓" : "✗"} ${r.message}`);
|
|
2326
|
+
if (!r.ok)
|
|
2327
|
+
process.exit(1);
|
|
1891
2328
|
}
|
|
1892
2329
|
function help() {
|
|
1893
2330
|
console.log(`mugiwara ${VERSION} — the Straw Hat crew for AI agents
|
|
@@ -1898,21 +2335,23 @@ Usage:
|
|
|
1898
2335
|
mugiwara uninstall remove installed files via manifest
|
|
1899
2336
|
mugiwara list show installations
|
|
1900
2337
|
mugiwara list --check health check: show installations + missing files
|
|
1901
|
-
mugiwara reset wipe mission state (
|
|
1902
|
-
mugiwara archive <m>
|
|
1903
|
-
mugiwara
|
|
2338
|
+
mugiwara reset wipe mission state (missions/ + legacy dirs)
|
|
2339
|
+
mugiwara archive <m> fold a closed mission's waves into its report, then remove loose files
|
|
2340
|
+
mugiwara clean [--all] [--before <date>]
|
|
2341
|
+
batch-archive every closed mission (report.md present, no live state)
|
|
1904
2342
|
mugiwara continue list in-flight missions (exit 2 = pick one, nothing resumed)
|
|
1905
2343
|
mugiwara continue <m> [member]
|
|
1906
2344
|
print the exact resume point for that mission/member
|
|
1907
2345
|
mugiwara status computed mission state: wave, tasks, lane, blockers, budget
|
|
2346
|
+
mugiwara blame <path> provenance note on the last commit touching <path>
|
|
2347
|
+
(fetch notes first: git fetch origin 'refs/notes/mugiwara:refs/notes/mugiwara')
|
|
2348
|
+
mugiwara handoff <m> write .mugiwara/missions/<m>/handoff.md — a report the next
|
|
2349
|
+
engineer can act on (computed state + staleness check)
|
|
2350
|
+
mugiwara sign <m> optional attestation: minisign-sign report.md (--verify to check)
|
|
1908
2351
|
mugiwara run <script> [args...]
|
|
1909
2352
|
run a bundled harness script here (${RUNNABLE.join(", ")})
|
|
1910
|
-
mugiwara savepoint <mission> [member] [
|
|
2353
|
+
mugiwara savepoint <mission> [member] [flow] [mode]
|
|
1911
2354
|
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
2355
|
mugiwara --help this help
|
|
1917
2356
|
mugiwara --version print version
|
|
1918
2357
|
|
|
@@ -1924,8 +2363,10 @@ Flags:
|
|
|
1924
2363
|
--force overwrite differing files (with backup)
|
|
1925
2364
|
--dry-run print actions without writing
|
|
1926
2365
|
--check with list: report missing files (health check)
|
|
1927
|
-
|
|
1928
|
-
|
|
2366
|
+
--all with continue/status: every actor; with clean: include in-flight missions
|
|
2367
|
+
--force with clean --all: archive in-flight missions anyway
|
|
2368
|
+
--before <date> with clean: also archive missions untouched since this date
|
|
2369
|
+
--keep-logs with reset: keep lessons.md (lessons ledger survives)`);
|
|
1929
2370
|
}
|
|
1930
2371
|
var isMain = process.argv[1] !== undefined && fileURLToPath5(import.meta.url) === resolve2(process.argv[1]);
|
|
1931
2372
|
if (isMain) {
|
|
@@ -1935,5 +2376,6 @@ if (isMain) {
|
|
|
1935
2376
|
});
|
|
1936
2377
|
}
|
|
1937
2378
|
export {
|
|
1938
|
-
run
|
|
2379
|
+
run,
|
|
2380
|
+
stalenessLine
|
|
1939
2381
|
};
|