@ionivetech/mugiwara 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.opencode/commands/mugiwara-onboard.md +15 -0
- package/.opencode/mugiwara-helpers.mjs +24 -0
- package/.opencode/plugins/mugiwara.mjs +16 -7
- package/AGENTS.md +1 -1
- package/README.md +118 -63
- package/content/agents/luffy-orchestrator.md +14 -4
- package/content/agents/onboarding-guide.md +24 -45
- package/content/agents/zoro-execution.md +4 -0
- package/content/skills/mugiwara-backend/SKILL.md +1 -1
- package/content/skills/mugiwara-brainstorm/SKILL.md +7 -0
- package/content/skills/mugiwara-checkpoint/SKILL.md +1 -1
- package/content/skills/mugiwara-claim-audit/SKILL.md +1 -1
- package/content/skills/mugiwara-execution/SKILL.md +44 -44
- package/content/skills/mugiwara-execution/references/dispatch.md +42 -0
- package/content/skills/mugiwara-frontend/SKILL.md +1 -1
- package/content/skills/mugiwara-healing/SKILL.md +1 -1
- package/content/skills/mugiwara-orchestration/SKILL.md +50 -49
- package/content/skills/mugiwara-orchestration/references/check-ins.md +34 -0
- package/content/skills/mugiwara-orchestration/references/closure.md +34 -0
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +12 -11
- package/content/skills/mugiwara-planning/SKILL.md +6 -0
- package/content/skills/mugiwara-pr/SKILL.md +18 -8
- package/content/skills/mugiwara-quality/SKILL.md +7 -0
- package/content/skills/mugiwara-ship/SKILL.md +11 -9
- package/content/skills/mugiwara-sunset/SKILL.md +1 -1
- package/content/skills/mugiwara-testcases/SKILL.md +7 -0
- package/content/skills/mugiwara-workflow/SKILL.md +4 -2
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +7 -6
- package/content/skills/using-mugiwara/SKILL.md +11 -1
- package/dist/mugiwara.js +186 -13
- package/gemini-extension.json +1 -1
- package/hooks/mugiwara-mode-tracker.ts +0 -0
- package/hooks/session-start.ts +0 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/evidence.sh +16 -1
- package/scripts/gate-selftest.ts +52 -1
- package/scripts/initiative.ts +34 -20
- package/scripts/lane.sh +4 -2
- package/scripts/mission-report.sh +152 -29
- package/scripts/onboard.ts +3 -29
- package/scripts/release-notes.ts +152 -75
- package/scripts/savepoint.sh +67 -15
- package/scripts/validate-content.ts +20 -0
- package/src/cli.ts +20 -3
- package/src/installer.ts +37 -1
- package/src/mission.ts +108 -1
- package/src/targets/claude.ts +29 -8
- package/src/targets/opencode.ts +12 -8
|
@@ -46,7 +46,7 @@ Waves are phases, not files. The plan doc defines them. The harness runs inline.
|
|
|
46
46
|
|
|
47
47
|
**One role at a time.** The main thread embodies ONE crew role per response — completes that role's report, then moves to the next. Never role-bleeds two personas into one response; never starts the next role before the current one returns its output.
|
|
48
48
|
|
|
49
|
-
**Banners.**
|
|
49
|
+
**Banners.** Every wave opens with a visible main-thread heading `## Wave N — <crew> (<skill>)` and closes with the handoff line `→ Wave N+1 — <crew>`. No wave starts without its banner.
|
|
50
50
|
|
|
51
51
|
**Subagents only for parallelism.** `[PARALLEL]` task batches, parallel review, parallel heal workers. Crew members never dispatch crew members.
|
|
52
52
|
|
|
@@ -76,6 +76,7 @@ Luffy classifies every request 8 ways:
|
|
|
76
76
|
Precedence: class decides whether there is work; lane decides how much process — class first, lane second.
|
|
77
77
|
|
|
78
78
|
Lane: 0=Direct (<20 LOC), 1=Lean (1-2 files), 2=Standard (3-8 files), 3=Full (9+ or sensitive), 4=Spike. Record route in `.mugiwara/logs/`.
|
|
79
|
+
Read-only investigation (no file change) → Answer/Explore — no crew, no Luffy subagent.
|
|
79
80
|
|
|
80
81
|
## Session handoff
|
|
81
82
|
|
|
@@ -91,7 +92,7 @@ Brook reads this at Wave 8. Never silently work around a blocker.
|
|
|
91
92
|
|
|
92
93
|
## Cleanup (Wave 9)
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
Archive, never delete: run `mugiwara archive <mission>` — folds `logs/`, `spec/`, `review/`, `issues/` into the mission report and removes the loose files. Step results `results/<mission>/01..05` + `todos.md` are EVIDENCE — KEEP them in place; they feed `reports/` and closure links. Keep: everything under `results/<mission>/`, `plans/`, `reports/`, `config`, `state.json`, `logs/lessons.md`. Full layout: `references/workspace-layout.md`.
|
|
95
96
|
|
|
96
97
|
## Rules
|
|
97
98
|
|
|
@@ -106,6 +107,7 @@ Delete consumed: `results/<mission>/01-execution.md`…`05-healing.md` + `todos.
|
|
|
106
107
|
## Iron Law
|
|
107
108
|
|
|
108
109
|
EVIDENCE OVER CLAIMS. "Done" = command re-run, output captured, evidence fresh.
|
|
110
|
+
Every evidence pointer is a CLICKABLE markdown link — `[path](relative/path)` — so reports link straight to the artifact. Step results in results/<mission>/01..05 are EVIDENCE: never deleted at cleanup, they feed the mission report.
|
|
109
111
|
|
|
110
112
|
## Red flags
|
|
111
113
|
|
|
@@ -42,9 +42,10 @@ artifacts go outside `.mugiwara/`.
|
|
|
42
42
|
|
|
43
43
|
## Cleanup (Wave 9)
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
`
|
|
47
|
-
`
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
`manifest.json`). List
|
|
45
|
+
Step results are evidence — KEEP every file in `results/<mission>/`
|
|
46
|
+
(`01-execution.md` through `05-healing.md`, `todos.md`, `06-closure.md`,
|
|
47
|
+
`07-pr-verdict.md`); they feed the mission report and closure links. Delete
|
|
48
|
+
only consumed cross-artifacts: `logs/`, `spec/`, `review/`, `issues/`. Keep
|
|
49
|
+
`plans/`, `reports/`, `config`, `state.json`, `logs/lessons.md` (canonical
|
|
50
|
+
lessons ledger; cross-mission state: `backup/`, `manifest.json`). List
|
|
51
|
+
candidates before deleting.
|
|
@@ -8,7 +8,7 @@ description: How Mugiwara works — crew overview, pipeline summary. Documentati
|
|
|
8
8
|
|
|
9
9
|
- `mugiwara-orchestration` is the gatekeeper — it auto-loads for task routing and classification. This skill is a documentation reference only.
|
|
10
10
|
|
|
11
|
-
Mugiwara is a governed engineering team in your coding agent.
|
|
11
|
+
Mugiwara is a governed engineering team in your coding agent. 12 specialists — triage, brainstorm, plan, execute, audit, quality, gates, review, security, heal — with evidence at every step and cost tracking. Runs inline in the main conversation.
|
|
12
12
|
|
|
13
13
|
## How it works
|
|
14
14
|
|
|
@@ -18,6 +18,10 @@ Mugiwara is a governed engineering team in your coding agent. 11 specialists —
|
|
|
18
18
|
4. Evidence over claims — no wave passes on assertion. Checks must be re-run.
|
|
19
19
|
5. Autonomy modes: `/mugiwara guided|semi|auto`. Flip applies next wave.
|
|
20
20
|
6. Workspace: `.mugiwara/` at repo root — plans, results, issues, logs, state.
|
|
21
|
+
7. The main thread embodies the active role — it is never "plain Claude"
|
|
22
|
+
mid-mission. Shortcuts skip waves, never roles. Write-scope in each agent's
|
|
23
|
+
frontmatter: artifacts agents delegate source edits to Zoro, never attempt
|
|
24
|
+
them.
|
|
21
25
|
|
|
22
26
|
## Crew
|
|
23
27
|
|
|
@@ -39,3 +43,9 @@ For task routing and classification, `mugiwara-orchestration` auto-loads as gate
|
|
|
39
43
|
This skill is documentation — load manually with `/using-mugiwara` or similar trigger phrases.
|
|
40
44
|
Full pipeline: see skills/mugiwara-workflow.
|
|
41
45
|
First time? Run `/mugiwara onboard` for guided setup. See `content/agents/onboarding-guide.md`.
|
|
46
|
+
|
|
47
|
+
## Red flags
|
|
48
|
+
|
|
49
|
+
- Stating an agent or skill count that drifts from content/.
|
|
50
|
+
- Claiming feature parity that contradicts the harness matrix.
|
|
51
|
+
- Answering routing questions instead of deferring to mugiwara-orchestration.
|
package/dist/mugiwara.js
CHANGED
|
@@ -112,8 +112,13 @@ ${body}`;
|
|
|
112
112
|
|
|
113
113
|
// src/targets/claude.ts
|
|
114
114
|
var here = dirname(fileURLToPath(import.meta.url));
|
|
115
|
-
var
|
|
115
|
+
var HOOKS_SRC = join(here, "..", "..", "hooks");
|
|
116
116
|
var COMMANDS_SRC = join(here, "..", "..", ".claude", "commands");
|
|
117
|
+
function toolsFromScope(scope) {
|
|
118
|
+
if (scope === "artifacts")
|
|
119
|
+
return "Read, Grep, Glob, Write, Bash, WebFetch, WebSearch";
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
117
122
|
var target = {
|
|
118
123
|
id: "claude",
|
|
119
124
|
label: "Claude Code",
|
|
@@ -133,6 +138,11 @@ var target = {
|
|
|
133
138
|
const fm = { name: data.name, description: data.description };
|
|
134
139
|
if (data.tools)
|
|
135
140
|
fm.tools = data.tools;
|
|
141
|
+
else if (data["internal-agent"] === "true") {
|
|
142
|
+
const generated = toolsFromScope(data["write-scope"]);
|
|
143
|
+
if (generated)
|
|
144
|
+
fm.tools = generated;
|
|
145
|
+
}
|
|
136
146
|
return { relPath: `${data.name}.md`, text: stringifyFrontmatter(fm, body) };
|
|
137
147
|
},
|
|
138
148
|
refsDir({ scope, projectDir, home }, skillName) {
|
|
@@ -141,16 +151,22 @@ var target = {
|
|
|
141
151
|
},
|
|
142
152
|
postInstall({ scope, projectDir, home, dryRun }) {
|
|
143
153
|
const root = scope === "global" ? join(home, ".claude") : join(projectDir, ".claude");
|
|
144
|
-
const hookFile = join(root, "hooks", "session-start.ts");
|
|
145
154
|
const written = [];
|
|
146
155
|
const notes = [];
|
|
147
156
|
if (dryRun)
|
|
148
157
|
return { written: [], notes: [] };
|
|
149
|
-
if (existsSync(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
if (existsSync(HOOKS_SRC)) {
|
|
159
|
+
for (const f of readdirSync(HOOKS_SRC)) {
|
|
160
|
+
if (!f.endsWith(".ts"))
|
|
161
|
+
continue;
|
|
162
|
+
const dst = join(root, "hooks", f);
|
|
163
|
+
if (!existsSync(dst)) {
|
|
164
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
165
|
+
copyFileSync(join(HOOKS_SRC, f), dst);
|
|
166
|
+
chmodSync(dst, 493);
|
|
167
|
+
written.push(dst);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
154
170
|
}
|
|
155
171
|
if (existsSync(COMMANDS_SRC)) {
|
|
156
172
|
const dstDir = join(root, "commands");
|
|
@@ -200,12 +216,12 @@ function permissionFromScope(scope) {
|
|
|
200
216
|
return { edit: { "*": "deny", ".mugiwara/**": "allow" } };
|
|
201
217
|
return;
|
|
202
218
|
}
|
|
203
|
-
function agentFrontmatter(name, description, writeScope) {
|
|
219
|
+
function agentFrontmatter(name, description, internal, writeScope) {
|
|
204
220
|
const crew = CREW[name];
|
|
205
221
|
const lines = [`description: ${description}`, `mode: all`];
|
|
206
222
|
if (crew) {
|
|
207
223
|
lines.push(`color: '${crew.color}'`, `temperature: ${crew.temperature}`, `steps: ${crew.steps}`);
|
|
208
|
-
const perm = permissionFromScope(writeScope);
|
|
224
|
+
const perm = internal ? permissionFromScope(writeScope) : undefined;
|
|
209
225
|
if (perm) {
|
|
210
226
|
lines.push("permission:");
|
|
211
227
|
for (const [k, v] of Object.entries(perm)) {
|
|
@@ -239,7 +255,7 @@ var target2 = {
|
|
|
239
255
|
},
|
|
240
256
|
transformAgent(data, body) {
|
|
241
257
|
const desc = data["internal-agent"] === "true" ? `[INTERNAL] ${data.description}` : data.description;
|
|
242
|
-
const fm = agentFrontmatter(data.name, desc, data["write-scope"]);
|
|
258
|
+
const fm = agentFrontmatter(data.name, desc, data["internal-agent"] === "true", data["write-scope"]);
|
|
243
259
|
return { relPath: `${data.name}.md`, text: `---
|
|
244
260
|
${fm}
|
|
245
261
|
---
|
|
@@ -461,7 +477,7 @@ var targets = { claude: target, opencode: target2, copilot: target3, gemini: tar
|
|
|
461
477
|
var TARGET_IDS = Object.keys(targets);
|
|
462
478
|
|
|
463
479
|
// src/installer.ts
|
|
464
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, readdirSync as readdirSync3, writeFileSync as writeFileSync2, copyFileSync as copyFileSync3, rmSync } from "node:fs";
|
|
480
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, readdirSync as readdirSync3, writeFileSync as writeFileSync2, copyFileSync as copyFileSync3, rmSync, lstatSync } from "node:fs";
|
|
465
481
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
466
482
|
import { homedir } from "node:os";
|
|
467
483
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
@@ -598,6 +614,44 @@ function removeInstalled(manifest, { dryRun = false } = {}) {
|
|
|
598
614
|
}
|
|
599
615
|
return removed;
|
|
600
616
|
}
|
|
617
|
+
function assertNotSymlink(file) {
|
|
618
|
+
if (!existsSync4(file))
|
|
619
|
+
return;
|
|
620
|
+
try {
|
|
621
|
+
if (lstatSync(file).isSymbolicLink())
|
|
622
|
+
throw new Error(`refusing to follow symlink: ${file}`);
|
|
623
|
+
} catch (e) {
|
|
624
|
+
if (e.code === "ENOENT")
|
|
625
|
+
return;
|
|
626
|
+
throw e;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
var GITIGNORE_MARKER = "# mugiwara";
|
|
630
|
+
var GITIGNORE_BLOCK = `# mugiwara — audit trail is the product: commit reports/, results/, logs/, spec/, plans/.
|
|
631
|
+
# Ignore session state and regenerated files.
|
|
632
|
+
.mugiwara/state.json
|
|
633
|
+
.mugiwara/state-*.json
|
|
634
|
+
.mugiwara/config
|
|
635
|
+
.mugiwara/continue.md
|
|
636
|
+
.mugiwara/refs/
|
|
637
|
+
`;
|
|
638
|
+
function ensureProjectGitignore(projectDir, opts = {}) {
|
|
639
|
+
const { dryRun = false } = opts;
|
|
640
|
+
const path = join5(projectDir, ".gitignore");
|
|
641
|
+
assertNotSymlink(path);
|
|
642
|
+
if (existsSync4(path) && readFileSync2(path, "utf8").includes(GITIGNORE_MARKER)) {
|
|
643
|
+
return { appended: false, notes: [] };
|
|
644
|
+
}
|
|
645
|
+
const existing = existsSync4(path) ? readFileSync2(path, "utf8") : "";
|
|
646
|
+
const separator = existing.length && !existing.endsWith(`
|
|
647
|
+
`) ? `
|
|
648
|
+
` : "";
|
|
649
|
+
if (!dryRun) {
|
|
650
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
651
|
+
writeFileSync2(path, existing + separator + GITIGNORE_BLOCK);
|
|
652
|
+
}
|
|
653
|
+
return { appended: true, notes: [`.gitignore ${dryRun ? "would append" : "appended"} mugiwara audit-trail block`] };
|
|
654
|
+
}
|
|
601
655
|
|
|
602
656
|
// src/manifest.ts
|
|
603
657
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
@@ -615,7 +669,7 @@ function writeManifest(file, data) {
|
|
|
615
669
|
}
|
|
616
670
|
|
|
617
671
|
// src/mission.ts
|
|
618
|
-
import { existsSync as existsSync6, rmSync as rmSync2, readFileSync as readFileSync4, readdirSync as readdirSync4 } from "node:fs";
|
|
672
|
+
import { existsSync as existsSync6, rmSync as rmSync2, readFileSync as readFileSync4, readdirSync as readdirSync4, mkdirSync as mkdirSync5, appendFileSync } from "node:fs";
|
|
619
673
|
import { join as join7 } from "node:path";
|
|
620
674
|
function activeActor(projectDir) {
|
|
621
675
|
const stateFile = join7(projectDir, ".mugiwara", "state.json");
|
|
@@ -671,12 +725,107 @@ function resetMission(projectDir, keepLogs, force) {
|
|
|
671
725
|
}
|
|
672
726
|
return { removed, kept };
|
|
673
727
|
}
|
|
728
|
+
function archiveMission(projectDir, mission, opts = {}) {
|
|
729
|
+
const { dryRun = false } = opts;
|
|
730
|
+
const root = join7(projectDir, ".mugiwara");
|
|
731
|
+
if (!mission || /[^a-zA-Z0-9._-]/.test(mission) || /^\.+$/.test(mission))
|
|
732
|
+
throw new Error(`invalid mission name "${mission}" (allowlist: [a-zA-Z0-9._-], not a dot-path)`);
|
|
733
|
+
const removed = [];
|
|
734
|
+
const kept = [];
|
|
735
|
+
const belongs = (f) => {
|
|
736
|
+
const base = f.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
737
|
+
return base === `${mission}.md` || base.startsWith(`${mission}-`);
|
|
738
|
+
};
|
|
739
|
+
let report = null;
|
|
740
|
+
const reportsDir = join7(root, "reports");
|
|
741
|
+
if (existsSync6(reportsDir)) {
|
|
742
|
+
const f = readdirSync4(reportsDir).find((n) => {
|
|
743
|
+
const m = n.match(/^(\d{4}-\d{2}-\d{2})-(.+)\.md$/);
|
|
744
|
+
return !!m && m[2] === mission;
|
|
745
|
+
});
|
|
746
|
+
if (f)
|
|
747
|
+
report = join7("reports", f);
|
|
748
|
+
}
|
|
749
|
+
const resultsDir = join7(root, "results", mission);
|
|
750
|
+
if (existsSync6(resultsDir)) {
|
|
751
|
+
for (const f of readdirSync4(resultsDir)) {
|
|
752
|
+
kept.push(join7("results", mission, f));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const specDir = join7(root, "spec");
|
|
756
|
+
if (existsSync6(specDir)) {
|
|
757
|
+
for (const f of readdirSync4(specDir)) {
|
|
758
|
+
if (!belongs(f))
|
|
759
|
+
continue;
|
|
760
|
+
const p = join7(specDir, f);
|
|
761
|
+
if (!dryRun)
|
|
762
|
+
rmSync2(p);
|
|
763
|
+
removed.push(join7("spec", f));
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
for (const dir of ["review", "issues"]) {
|
|
767
|
+
const d = join7(root, dir);
|
|
768
|
+
if (!existsSync6(d))
|
|
769
|
+
continue;
|
|
770
|
+
for (const f of readdirSync4(d)) {
|
|
771
|
+
if (!belongs(f))
|
|
772
|
+
continue;
|
|
773
|
+
const p = join7(d, f);
|
|
774
|
+
if (!dryRun)
|
|
775
|
+
rmSync2(p, { force: true });
|
|
776
|
+
removed.push(join7(dir, f));
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const logsDir = join7(root, "logs");
|
|
780
|
+
if (existsSync6(logsDir)) {
|
|
781
|
+
for (const f of readdirSync4(logsDir)) {
|
|
782
|
+
if (!belongs(f))
|
|
783
|
+
continue;
|
|
784
|
+
const p = join7(logsDir, f);
|
|
785
|
+
if (!dryRun)
|
|
786
|
+
rmSync2(p);
|
|
787
|
+
removed.push(join7("logs", f));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const cont = join7(root, "continue.md");
|
|
791
|
+
if (existsSync6(cont)) {
|
|
792
|
+
try {
|
|
793
|
+
if (readFileSync4(cont, "utf8").includes(mission)) {
|
|
794
|
+
if (!dryRun)
|
|
795
|
+
rmSync2(cont);
|
|
796
|
+
removed.push("continue.md");
|
|
797
|
+
}
|
|
798
|
+
} catch {}
|
|
799
|
+
}
|
|
800
|
+
if (report)
|
|
801
|
+
kept.push(report);
|
|
802
|
+
for (const k of ["plans", "config", "state.json", join7("logs", "lessons.md")]) {
|
|
803
|
+
if (existsSync6(join7(root, k)))
|
|
804
|
+
kept.push(k);
|
|
805
|
+
}
|
|
806
|
+
let index;
|
|
807
|
+
const indexFile = join7(root, "reports", "index.md");
|
|
808
|
+
const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}${report ? ` → ${report}` : ""}
|
|
809
|
+
`;
|
|
810
|
+
if (!dryRun) {
|
|
811
|
+
mkdirSync5(join7(root, "reports"), { recursive: true });
|
|
812
|
+
const existing = existsSync6(indexFile) ? readFileSync4(indexFile, "utf8") : "";
|
|
813
|
+
if (!existing.split(/\r?\n/).some((l) => l.startsWith(`- ${mission} —`))) {
|
|
814
|
+
const header = existing ? "" : `# Mission index
|
|
815
|
+
|
|
816
|
+
`;
|
|
817
|
+
appendFileSync(indexFile, header + line);
|
|
818
|
+
}
|
|
819
|
+
index = join7("reports", "index.md");
|
|
820
|
+
}
|
|
821
|
+
return { report, removed, kept, index };
|
|
822
|
+
}
|
|
674
823
|
|
|
675
824
|
// src/cli.ts
|
|
676
825
|
var str = (v) => typeof v === "string" ? v : undefined;
|
|
677
826
|
var flag = (v) => v === true;
|
|
678
827
|
async function run(argv) {
|
|
679
|
-
const { command, flags } = parseArgs(argv);
|
|
828
|
+
const { command, flags, _ } = parseArgs(argv);
|
|
680
829
|
if (flag(flags.help) || command === "help")
|
|
681
830
|
return help();
|
|
682
831
|
if (flag(flags.version)) {
|
|
@@ -694,6 +843,8 @@ async function run(argv) {
|
|
|
694
843
|
return list(flags);
|
|
695
844
|
case "reset":
|
|
696
845
|
return resetCmd(flags);
|
|
846
|
+
case "archive":
|
|
847
|
+
return archive(flags, _);
|
|
697
848
|
default:
|
|
698
849
|
throw new Error(`Unknown command: ${command}`);
|
|
699
850
|
}
|
|
@@ -713,6 +864,23 @@ function resetCmd(flags) {
|
|
|
713
864
|
if (result.kept.length)
|
|
714
865
|
console.log(`kept: ${result.kept.join(", ")}`);
|
|
715
866
|
}
|
|
867
|
+
function archive(flags, positionals) {
|
|
868
|
+
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
869
|
+
const mission = positionals[1];
|
|
870
|
+
if (!mission) {
|
|
871
|
+
console.error("usage: mugiwara archive <mission> [--project <dir>] [--dry-run]");
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
const result = archiveMission(projectDir, mission, { dryRun: flag(flags.dryRun) });
|
|
875
|
+
if (result.report)
|
|
876
|
+
console.log(`archive target: ${result.report}`);
|
|
877
|
+
if (result.removed.length)
|
|
878
|
+
console.log(`${flag(flags.dryRun) ? "would remove" : "removed"}: ${result.removed.join(", ")}`);
|
|
879
|
+
if (result.kept.length)
|
|
880
|
+
console.log(`kept: ${result.kept.join(", ")}`);
|
|
881
|
+
if (result.index)
|
|
882
|
+
console.log(`index updated: ${result.index}`);
|
|
883
|
+
}
|
|
716
884
|
async function resolveOptions(flags) {
|
|
717
885
|
const interactive = !flag(flags.yes);
|
|
718
886
|
if (interactive && !process.stdin.isTTY) {
|
|
@@ -773,6 +941,10 @@ async function install(flags) {
|
|
|
773
941
|
allFiles.push(...r.written);
|
|
774
942
|
allNotes.push(...r.notes);
|
|
775
943
|
}
|
|
944
|
+
if (scope === "project") {
|
|
945
|
+
const gi = ensureProjectGitignore(projectDir, { dryRun: flag(flags.dryRun) });
|
|
946
|
+
allNotes.push(...gi.notes);
|
|
947
|
+
}
|
|
776
948
|
if (flag(flags.dryRun)) {
|
|
777
949
|
console.log(`
|
|
778
950
|
Dry run — nothing written.`);
|
|
@@ -869,6 +1041,7 @@ Usage:
|
|
|
869
1041
|
mugiwara list show installations
|
|
870
1042
|
mugiwara list --check health check: show installations + missing files
|
|
871
1043
|
mugiwara reset wipe mission state (spec/plans/results/review/issues[/logs])
|
|
1044
|
+
mugiwara archive <m> fold a closed mission's evidence into its report, then remove loose files
|
|
872
1045
|
mugiwara --help this help
|
|
873
1046
|
mugiwara --version print version
|
|
874
1047
|
|
package/gemini-extension.json
CHANGED
|
File without changes
|
package/hooks/session-start.ts
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ionivetech/mugiwara",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, self-healing. Installs into Claude Code, opencode, Copilot, Gemini, Codex, Cursor, Kimi, pi, Windsurf, Cline, Kilo, Antigravity.",
|
|
5
5
|
"homepage": "https://github.com/ionivetech/mugiwara#readme",
|
|
6
6
|
"repository": {
|
package/plugin.json
CHANGED
package/scripts/evidence.sh
CHANGED
|
@@ -8,7 +8,11 @@ die() { echo "evidence: $*" >&2; exit 1; }
|
|
|
8
8
|
|
|
9
9
|
MISSION="${1:-}"
|
|
10
10
|
LABEL="${2:-}"
|
|
11
|
-
shift 2>/dev/null || true
|
|
11
|
+
shift 2 2>/dev/null || true
|
|
12
|
+
|
|
13
|
+
# strip an optional "--" separator between <label> and the command
|
|
14
|
+
[ "${1:-}" = "--" ] && shift
|
|
15
|
+
|
|
12
16
|
[ -z "$MISSION" ] && die "usage: evidence.sh <mission> <label> [-- command args...]"
|
|
13
17
|
[ -z "$LABEL" ] && die "usage: evidence.sh <mission> <label> [-- command args...]"
|
|
14
18
|
|
|
@@ -17,6 +21,17 @@ case "$MISSION" in
|
|
|
17
21
|
*[!a-zA-Z0-9._-]*) die "invalid mission name \"$MISSION\" (allowlist: [a-zA-Z0-9._-])" ;;
|
|
18
22
|
esac
|
|
19
23
|
|
|
24
|
+
# label allowlist — LABEL feeds the output filename; traversal or shell
|
|
25
|
+
# metacharacters must not reach the filesystem (same rule as MISSION)
|
|
26
|
+
case "$LABEL" in
|
|
27
|
+
*[!a-zA-Z0-9._-]*) die "invalid label \"$LABEL\" (allowlist: [a-zA-Z0-9._-])" ;;
|
|
28
|
+
esac
|
|
29
|
+
# dot-only labels (".", "..", "...") pass the char allowlist but escape the
|
|
30
|
+
# results dir — reject them before any filename is built from LABEL.
|
|
31
|
+
if [[ "$LABEL" =~ ^\.+$ ]]; then
|
|
32
|
+
die "invalid label \"$LABEL\" (allowlist: [a-zA-Z0-9._-], not a dot-path)"
|
|
33
|
+
fi
|
|
34
|
+
|
|
20
35
|
MUGIWARA_DIR="${MUGIWARA_DIR:-.mugiwara}"
|
|
21
36
|
RESULTS_DIR="$MUGIWARA_DIR/results/$MISSION"
|
|
22
37
|
mkdir -p "$RESULTS_DIR"
|
package/scripts/gate-selftest.ts
CHANGED
|
@@ -54,7 +54,7 @@ if (!existsSync(join(root, 'scripts', 'validate-content.ts'))) {
|
|
|
54
54
|
const makeBody = (sectionLines: number) => {
|
|
55
55
|
let body = '---\nname: test-skill\ndescription: Test skill for section-length gate, twenty chars\n---\n\n# Test\n\n## Skip when\n\n- Test\n\n## Long Section\n\n';
|
|
56
56
|
for (let i = 0; i < sectionLines; i++) body += `Line ${i + 1} of content for section length test.\n`;
|
|
57
|
-
body += '\n';
|
|
57
|
+
body += '\n## Red flags\n\n- Test flag\n';
|
|
58
58
|
return body;
|
|
59
59
|
};
|
|
60
60
|
|
|
@@ -262,5 +262,56 @@ if (!existsSync(brainstormSkill)) {
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
// --- F2: red-flags gate — strip the heading from one skill, prove red ---
|
|
266
|
+
console.log('\nF2 — red-flags gate');
|
|
267
|
+
const redSkill = join(root, 'content', 'skills', 'mugiwara-workflow', 'SKILL.md');
|
|
268
|
+
if (!existsSync(redSkill)) {
|
|
269
|
+
console.log(' ⚠ mugiwara-workflow/SKILL.md not found, skipping');
|
|
270
|
+
} else {
|
|
271
|
+
const original = readFileSync(redSkill, 'utf8');
|
|
272
|
+
try {
|
|
273
|
+
const broken = original.replace('## Red flags', '## Disabled');
|
|
274
|
+
writeFileSync(redSkill, broken);
|
|
275
|
+
assert('missing Red flags → exit 1', false, () => run('F2-redflags', 'bun scripts/validate-content.ts'));
|
|
276
|
+
} finally {
|
|
277
|
+
writeFileSync(redSkill, original);
|
|
278
|
+
assert('restored → exit 0', true, () => run('F2-redflags', 'bun scripts/validate-content.ts --check-manifest --check-docs'));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// --- W1: write-boundary gate — strip the section, prove red ---
|
|
283
|
+
console.log('\nW1 — write-boundary gate');
|
|
284
|
+
const wbSkill = join(root, 'content', 'skills', 'mugiwara-orchestration', 'SKILL.md');
|
|
285
|
+
if (!existsSync(wbSkill)) {
|
|
286
|
+
console.log(' ⚠ mugiwara-orchestration/SKILL.md not found, skipping');
|
|
287
|
+
} else {
|
|
288
|
+
const original = readFileSync(wbSkill, 'utf8');
|
|
289
|
+
try {
|
|
290
|
+
const broken = original.replace('## Write boundary', '## Boundary');
|
|
291
|
+
writeFileSync(wbSkill, broken);
|
|
292
|
+
assert('missing write boundary → exit 1', false, () => run('W1-wb', 'bun scripts/validate-content.ts'));
|
|
293
|
+
} finally {
|
|
294
|
+
writeFileSync(wbSkill, original);
|
|
295
|
+
assert('restored → exit 0', true, () => run('W1-wb', 'bun scripts/validate-content.ts --check-manifest --check-docs'));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --- F14: agent-count gate — flip an internal agent to user-facing, prove red ---
|
|
300
|
+
console.log('\nF14 — agent-count gate');
|
|
301
|
+
const countAgent = join(root, 'content', 'agents', 'eval-runner.md');
|
|
302
|
+
if (!existsSync(countAgent)) {
|
|
303
|
+
console.log(' ⚠ eval-runner.md not found, skipping');
|
|
304
|
+
} else {
|
|
305
|
+
const original = readFileSync(countAgent, 'utf8');
|
|
306
|
+
try {
|
|
307
|
+
const broken = original.replace('internal: true', 'internal: false');
|
|
308
|
+
writeFileSync(countAgent, broken);
|
|
309
|
+
assert('count drift → exit 1', false, () => run('F14-count', 'bun scripts/validate-content.ts'));
|
|
310
|
+
} finally {
|
|
311
|
+
writeFileSync(countAgent, original);
|
|
312
|
+
assert('restored → exit 0', true, () => run('F14-count', 'bun scripts/validate-content.ts --check-manifest --check-docs'));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
265
316
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
266
317
|
process.exit(failed > 0 ? 1 : 0);
|
package/scripts/initiative.ts
CHANGED
|
@@ -46,6 +46,15 @@ function resolvePlan(planFile: string): string {
|
|
|
46
46
|
throw new Error(err);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// A plan is a markdown document. Garbage (no markdown structure) is not a
|
|
50
|
+
// "solo mission" — it is a malformed plan. Heading check keeps the
|
|
51
|
+
// no-sub-missions-but-valid-markdown case working as solo mission.
|
|
52
|
+
function assertPlanContent(content: string, planFile: string): void {
|
|
53
|
+
if (!/^#{1,6}\s/m.test(content)) {
|
|
54
|
+
throw new Error(`Not a valid plan file: "${planFile}" — expected markdown (no heading found)`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
function parseSubMissions(content: string): SubMission[] {
|
|
50
59
|
const lines = content.split('\n');
|
|
51
60
|
let inTable = false;
|
|
@@ -93,6 +102,7 @@ function parseSubMissions(content: string): SubMission[] {
|
|
|
93
102
|
|
|
94
103
|
function cmdStatus(planFile: string): void {
|
|
95
104
|
const content = readFileSync(planFile, 'utf8');
|
|
105
|
+
assertPlanContent(content, planFile);
|
|
96
106
|
const subs = parseSubMissions(content);
|
|
97
107
|
|
|
98
108
|
if (subs.length === 0) {
|
|
@@ -122,6 +132,7 @@ function cmdStatus(planFile: string): void {
|
|
|
122
132
|
|
|
123
133
|
function cmdConflictCheck(planFile: string): void {
|
|
124
134
|
const content = readFileSync(planFile, 'utf8');
|
|
135
|
+
assertPlanContent(content, planFile);
|
|
125
136
|
const subs = parseSubMissions(content);
|
|
126
137
|
|
|
127
138
|
if (subs.length === 0) {
|
|
@@ -161,15 +172,13 @@ function cmdConflictCheck(planFile: string): void {
|
|
|
161
172
|
}
|
|
162
173
|
|
|
163
174
|
function cmdSetStatus(planFile: string, id: string, status: string): void {
|
|
164
|
-
if (!STATUS_MARKERS[id]) {
|
|
165
|
-
// `status` here is the status string from args — check it
|
|
166
|
-
}
|
|
167
175
|
if (!STATUS_MARKERS[status]) {
|
|
168
176
|
console.error(`Invalid status: "${status}". Use: pending, in-progress, done, blocked`);
|
|
169
177
|
process.exit(1);
|
|
170
178
|
}
|
|
171
179
|
|
|
172
180
|
const content = readFileSync(planFile, 'utf8');
|
|
181
|
+
assertPlanContent(content, planFile);
|
|
173
182
|
const lines = content.split('\n');
|
|
174
183
|
const newMarker = STATUS_MARKERS[status];
|
|
175
184
|
let inTable = false;
|
|
@@ -225,24 +234,29 @@ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
|
225
234
|
|
|
226
235
|
const cmd = args[0];
|
|
227
236
|
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
try {
|
|
238
|
+
if (cmd === 'status') {
|
|
239
|
+
if (args.length < 2) { console.error('Missing plan-file argument.'); process.exit(1); }
|
|
240
|
+
cmdStatus(resolvePlan(args[1]));
|
|
241
|
+
} else if (cmd === 'conflict-check') {
|
|
242
|
+
if (args.length < 2) { console.error('Missing plan-file argument.'); process.exit(1); }
|
|
243
|
+
cmdConflictCheck(resolvePlan(args[1]));
|
|
244
|
+
} else if (cmd === 'set-status') {
|
|
245
|
+
const idIdx = args.indexOf('--id');
|
|
246
|
+
const statusIdx = args.indexOf('--status');
|
|
247
|
+
if (idIdx === -1 || statusIdx === -1 || args.length < 6) {
|
|
248
|
+
console.error('set-status requires --id <id> --status <status>');
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
const id = args[idIdx + 1];
|
|
252
|
+
const statusVal = args[statusIdx + 1];
|
|
253
|
+
cmdSetStatus(resolvePlan(args[1]), id, statusVal);
|
|
254
|
+
} else {
|
|
255
|
+
console.error(`Unknown command: "${cmd}"`);
|
|
256
|
+
console.log(USAGE);
|
|
239
257
|
process.exit(1);
|
|
240
258
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
cmdSetStatus(resolvePlan(args[1]), id, statusVal);
|
|
244
|
-
} else {
|
|
245
|
-
console.error(`Unknown command: "${cmd}"`);
|
|
246
|
-
console.log(USAGE);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
247
261
|
process.exit(1);
|
|
248
262
|
}
|
package/scripts/lane.sh
CHANGED
|
@@ -51,7 +51,9 @@ else
|
|
|
51
51
|
REASON="$FILE_COUNT files"
|
|
52
52
|
fi
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
# Sensitive-path escalation is unconditional — it wins over any count-based
|
|
55
|
+
# lane AND over the docs-only downgrade below (bug C9).
|
|
56
|
+
if [ "$HAS_SENSITIVE" -eq 1 ]; then
|
|
55
57
|
PREV="$LANE"
|
|
56
58
|
LANE="full"
|
|
57
59
|
REASON="sensitive paths ($SENSITIVE) — escalated from $PREV"
|
|
@@ -63,7 +65,7 @@ fi
|
|
|
63
65
|
# Product surface for mugiwara: content/, src/, scripts/, test/, hooks/,
|
|
64
66
|
# .opencode/, .claude/, evals/ — everything else is docs/config/asset.
|
|
65
67
|
PRODUCT_PAT="^content/|^src/|^scripts/|^test/|^hooks/|^\.opencode/|^\.claude/|^evals/"
|
|
66
|
-
if [ "$LANE" = "full" ] && [ -n "$CHANGED" ]; then
|
|
68
|
+
if [ "$LANE" = "full" ] && [ "$HAS_SENSITIVE" -eq 0 ] && [ -n "$CHANGED" ]; then
|
|
67
69
|
CODE_COUNT=$(echo "$CHANGED" | grep -E "$PRODUCT_PAT" 2>/dev/null | grep -c . || true)
|
|
68
70
|
if [ -z "$CODE_COUNT" ] || [ "$CODE_COUNT" -eq 0 ] 2>/dev/null; then
|
|
69
71
|
PREV="$LANE"
|