@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.
Files changed (54) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/.opencode/commands/mugiwara-onboard.md +15 -0
  7. package/.opencode/mugiwara-helpers.mjs +24 -0
  8. package/.opencode/plugins/mugiwara.mjs +16 -7
  9. package/AGENTS.md +1 -1
  10. package/README.md +118 -63
  11. package/content/agents/luffy-orchestrator.md +14 -4
  12. package/content/agents/onboarding-guide.md +24 -45
  13. package/content/agents/zoro-execution.md +4 -0
  14. package/content/skills/mugiwara-backend/SKILL.md +1 -1
  15. package/content/skills/mugiwara-brainstorm/SKILL.md +7 -0
  16. package/content/skills/mugiwara-checkpoint/SKILL.md +1 -1
  17. package/content/skills/mugiwara-claim-audit/SKILL.md +1 -1
  18. package/content/skills/mugiwara-execution/SKILL.md +44 -44
  19. package/content/skills/mugiwara-execution/references/dispatch.md +42 -0
  20. package/content/skills/mugiwara-frontend/SKILL.md +1 -1
  21. package/content/skills/mugiwara-healing/SKILL.md +1 -1
  22. package/content/skills/mugiwara-orchestration/SKILL.md +50 -49
  23. package/content/skills/mugiwara-orchestration/references/check-ins.md +34 -0
  24. package/content/skills/mugiwara-orchestration/references/closure.md +34 -0
  25. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +12 -11
  26. package/content/skills/mugiwara-planning/SKILL.md +6 -0
  27. package/content/skills/mugiwara-pr/SKILL.md +18 -8
  28. package/content/skills/mugiwara-quality/SKILL.md +7 -0
  29. package/content/skills/mugiwara-ship/SKILL.md +11 -9
  30. package/content/skills/mugiwara-sunset/SKILL.md +1 -1
  31. package/content/skills/mugiwara-testcases/SKILL.md +7 -0
  32. package/content/skills/mugiwara-workflow/SKILL.md +4 -2
  33. package/content/skills/mugiwara-workflow/references/workspace-layout.md +7 -6
  34. package/content/skills/using-mugiwara/SKILL.md +11 -1
  35. package/dist/mugiwara.js +186 -13
  36. package/gemini-extension.json +1 -1
  37. package/hooks/mugiwara-mode-tracker.ts +0 -0
  38. package/hooks/session-start.ts +0 -0
  39. package/package.json +1 -1
  40. package/plugin.json +1 -1
  41. package/scripts/evidence.sh +16 -1
  42. package/scripts/gate-selftest.ts +52 -1
  43. package/scripts/initiative.ts +34 -20
  44. package/scripts/lane.sh +4 -2
  45. package/scripts/mission-report.sh +152 -29
  46. package/scripts/onboard.ts +3 -29
  47. package/scripts/release-notes.ts +152 -75
  48. package/scripts/savepoint.sh +67 -15
  49. package/scripts/validate-content.ts +20 -0
  50. package/src/cli.ts +20 -3
  51. package/src/installer.ts +37 -1
  52. package/src/mission.ts +108 -1
  53. package/src/targets/claude.ts +29 -8
  54. 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.** Waves open with `==================== WAVE N - CREW (SKILL) ====================` and close with `→ Wave N+1 — Crew`.
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
- Delete consumed: `results/<mission>/01-execution.md`…`05-healing.md` + `todos.md`, `logs/` (except `logs/lessons.md`), `spec/`, `review/`, `issues/`. Keep: `results/<mission>/06-closure.md`, `results/<mission>/07-pr-verdict.md`, `plans/`, `reports/`, `config`, `state.json`, `logs/lessons.md`. Full layout: `references/workspace-layout.md`.
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
- Delete consumed: `results/<mission>/01-execution.md` through
46
- `05-healing.md` and `todos.md` (wave artifacts), `logs/`, `spec/`, `review/`,
47
- `issues/`. Keep: `results/<mission>/06-closure.md`,
48
- `results/<mission>/07-pr-verdict.md`, `plans/`, `reports/`, `config`,
49
- `state.json`, `lessons.md` (cross-mission state: `logs/lessons.md`, `backup/`,
50
- `manifest.json`). List candidates before deleting.
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 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.
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 HOOK_SRC = join(here, "..", "..", "hooks", "session-start.ts");
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(HOOK_SRC) && !existsSync(hookFile)) {
150
- mkdirSync(dirname(hookFile), { recursive: true });
151
- copyFileSync(HOOK_SRC, hookFile);
152
- chmodSync(hookFile, 493);
153
- written.push(hookFile);
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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mugiwara",
3
- "version": "0.6.0",
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, healing.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ionivetech/mugiwara",
3
- "version": "0.6.0",
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mugiwara",
3
3
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, healing.",
4
- "version": "0.6.0",
4
+ "version": "0.6.2",
5
5
  "author": {
6
6
  "name": "ionivetech"
7
7
  },
@@ -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"
@@ -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);
@@ -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
- if (cmd === 'status') {
229
- if (args.length < 2) { console.error('Missing plan-file argument.'); process.exit(1); }
230
- cmdStatus(resolvePlan(args[1]));
231
- } else if (cmd === 'conflict-check') {
232
- if (args.length < 2) { console.error('Missing plan-file argument.'); process.exit(1); }
233
- cmdConflictCheck(resolvePlan(args[1]));
234
- } else if (cmd === 'set-status') {
235
- const idIdx = args.indexOf('--id');
236
- const statusIdx = args.indexOf('--status');
237
- if (idIdx === -1 || statusIdx === -1 || args.length < 6) {
238
- console.error('set-status requires --id <id> --status <status>');
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
- const id = args[idIdx + 1];
242
- const statusVal = args[statusIdx + 1];
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
- if [ "$HAS_SENSITIVE" -eq 1 ] && [ "$LANE" != "full" ]; then
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"