@ionivetech/mugiwara 0.6.3 → 0.6.4

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.
@@ -5,13 +5,13 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "The Straw Hat crew for AI agents",
8
- "version": "0.6.3"
8
+ "version": "0.6.4"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "mugiwara",
13
13
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, healing.",
14
- "version": "0.6.3",
14
+ "version": "0.6.4",
15
15
  "source": "./"
16
16
  }
17
17
  ]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mugiwara",
3
3
  "displayName": "Mugiwara",
4
- "version": "0.6.3",
4
+ "version": "0.6.4",
5
5
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, healing.",
6
6
  "author": {
7
7
  "name": "ionivetech"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mugiwara",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, healing.",
5
5
  "author": {
6
6
  "name": "ionivetech"
@@ -2,7 +2,7 @@
2
2
  "name": "mugiwara",
3
3
  "displayName": "Mugiwara",
4
4
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, healing.",
5
- "version": "0.6.3",
5
+ "version": "0.6.4",
6
6
  "author": {
7
7
  "name": "ionivetech"
8
8
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mugiwara",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, healing.",
5
5
  "author": {
6
6
  "name": "ionivetech"
@@ -93,6 +93,7 @@ export const DEFAULT_CONFIG_LINES = [
93
93
  'mode=guided',
94
94
  'branch=feature/{type}-{issue}-{slug}',
95
95
  'commit=conventional',
96
+ 'auto_commit=on',
96
97
  'coverage_new=90',
97
98
  'coverage_modified=80',
98
99
  'review_depth=full',
@@ -6,6 +6,11 @@
6
6
  // single export because OpenCode's legacy loader calls every exported function
7
7
  // as a plugin (same constraint ponytail documents).
8
8
  //
9
+ // Crew COLORS come from references/wave-banners.md (single source, shared
10
+ // references dir); the CREW map below is the cold-path
11
+ // fallback only. Temperature/steps stay here (runtime tuning, not banner
12
+ // material).
13
+ //
9
14
  // Install: add to opencode.json
10
15
  // { "plugin": ["@ionivetech/mugiwara"] }
11
16
  // or from the git repo:
@@ -22,26 +27,47 @@ const contentDir = join(__dirname, '..', '..', 'content');
22
27
  const skillsDir = join(contentDir, 'skills');
23
28
  const agentsDir = join(contentDir, 'agents');
24
29
 
25
- const ANNOUNCE =
26
- "Mugiwara crew available. The workflow auto-activates for non-trivial requests — no need to call `/using-mugiwara` at session start (it is an optional router). Run the crew pipeline inline in the main conversation: embody ONE crew role at a time using its skill, wait for its report, then move to the next. Never Task-dispatch a crew member — the crew runs in the main thread; subagents only for [PARALLEL] task batches, concurrent review/security, and independent re-run checks. Progress shows as checkpoint reports at wave/stage boundaries, pausing on failure or risk. Switch mode with `/mugiwara` (guided|semi|auto). See skills/mugiwara-workflow.";
27
-
30
+ // Colors are fallbacks — the wave-banners table is the source of truth.
28
31
  const CREW = {
29
32
  'luffy-orchestrator': { color: '#ef4444', temperature: 0.2, steps: 30 },
30
- 'usopp-brainstorm': { color: '#f59e0b', temperature: 0.6, steps: 30 },
33
+ 'usopp-brainstorm': { color: '#b45309', temperature: 0.6, steps: 30 },
31
34
  'nami-planner': { color: '#f97316', temperature: 0.2, steps: 30 },
32
35
  'zoro-execution': { color: '#22c55e', temperature: 0.1, steps: 50 },
33
- 'chopper-checkpoint': { color: '#3b82f6', temperature: 0.1, steps: 30 },
34
- 'sanji-quality': { color: '#a855f7', temperature: 0.1, steps: 40 },
36
+ 'chopper-checkpoint': { color: '#60a5fa', temperature: 0.1, steps: 30 },
37
+ 'sanji-quality': { color: '#facc15', temperature: 0.1, steps: 40 },
35
38
  'franky-gates': { color: '#06b6d4', temperature: 0.1, steps: 40 },
36
39
  'robin-reviewer': { color: '#8b5cf6', temperature: 0.2, steps: 30 },
37
40
  'jinbe-security': { color: '#6366f1', temperature: 0.2, steps: 30 },
38
- 'brook-healing': { color: '#ec4899', temperature: 0.1, steps: 30 },
41
+ 'brook-healing': { color: '#2dd4bf', temperature: 0.1, steps: 30 },
39
42
  'skeptic-verifier': { color: '#64748b', temperature: 0.1, steps: 30 },
40
43
  'eval-runner': { color: '#14b8a6', temperature: 0.2, steps: 30 },
41
44
  'resume-coordinator': { color: '#d97706', temperature: 0.2, steps: 30 },
42
45
  'memory-keeper': { color: '#d946ef', temperature: 0.2, steps: 30 },
43
46
  };
44
47
 
48
+ // Read the crew color table (single source of truth, shared references/).
49
+ // Returns {} on any failure — callers fall back to the CREW map. The regex
50
+ // anchors the exact table shape: | agent-id | role | hex | ansi-256 | emoji |
51
+ function readBannerColors() {
52
+ try {
53
+ const path = join(__dirname, '..', '..', 'references', 'wave-banners.md');
54
+ if (!existsSync(path)) return {};
55
+ const text = readFileSync(path, 'utf8');
56
+ // null-prototype: agent ids are trusted repo content, but a future
57
+ // `__proto__` id must never write the object's prototype
58
+ const colors = Object.create(null);
59
+ for (const m of text.matchAll(/^\| ([\w-]+) \| [^|]+ \| (#[0-9a-f]{6}) \| (\d+) \| (\S+) \|\r?$/gm)) {
60
+ colors[m[1]] = m[2];
61
+ }
62
+ return colors;
63
+ } catch {
64
+ return {};
65
+ }
66
+ }
67
+
68
+ const ANNOUNCE =
69
+ "Mugiwara crew available. The workflow auto-activates for non-trivial requests — no need to call `/using-mugiwara` at session start (it is an optional router). Run the crew pipeline inline in the main conversation: embody ONE crew role at a time using its skill, wait for its report, then move to the next. Never Task-dispatch a crew member — the crew runs in the main thread; subagents only for [PARALLEL] task batches, concurrent review/security, and independent re-run checks. Progress shows as checkpoint reports at wave/stage boundaries, pausing on failure or risk. Switch mode with `/mugiwara` (guided|semi|auto). See skills/mugiwara-workflow.";
70
+
45
71
  function parseFrontmatter(text) {
46
72
  const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
47
73
  if (!m) throw new Error('Missing frontmatter fence (---)');
@@ -68,6 +94,7 @@ function permissionFromScope(scope) {
68
94
 
69
95
  function readAgents(stepsEnabled = true) {
70
96
  const agents = {};
97
+ const bannerColors = readBannerColors();
71
98
  let files;
72
99
  try {
73
100
  files = readdirSync(agentsDir).filter((f) => f.endsWith('.md'));
@@ -99,6 +126,11 @@ function readAgents(stepsEnabled = true) {
99
126
  agents[name] = { ...agents[name], ...rest };
100
127
  if (stepsEnabled) agents[name] = { ...agents[name], steps };
101
128
  }
129
+ // color comes from the wave-banners table (single source); CREW fallback
130
+ // covers agents the table does not list (and vice versa)
131
+ if (bannerColors[name] || CREW[name]) {
132
+ agents[name] = { ...agents[name], color: bannerColors[name] ?? CREW[name].color };
133
+ }
102
134
  const perm = permissionFromScope(parsed.data['write-scope']);
103
135
  if (perm && agents[name].mode === 'subagent') agents[name].permission = perm;
104
136
  }
package/README.md CHANGED
@@ -245,6 +245,7 @@ Switch mode any time: `/mugiwara guided | semi | auto`. Or edit `.mugiwara/confi
245
245
  | `mode` | guided | guided / semi / auto |
246
246
  | `branch` | `feature/{type}-{issue}-{slug}` | Branch naming |
247
247
  | `commit` | conventional | conventional / gitmoji / plain |
248
+ | `auto_commit` | on | on / off — off disables commit+push in guided/semi |
248
249
  | `coverage_new` | 90 | Coverage threshold for new files (%) |
249
250
  | `coverage_modified` | 80 | Coverage threshold for modified files (%) |
250
251
  | `review_depth` | full | full / standard / quick — Robin's review depth |
@@ -31,7 +31,7 @@ Wave 3 of `mugiwara-workflow`, with the plan doc path.
31
31
  ## Rules
32
32
 
33
33
  1. Follow `mugiwara-execution` exactly (ingestion, dispatch rules, per-task discipline).
34
- 2. Before touching code, follow the mode's branch/commit rule (per mode config): `guided` ASKS THE USER (auto branch for the mission or current branch; auto commit per task or user-controlled checkpoints); `semi`/`auto` auto-create the mission branch per the config `branch` key and auto-commit per task in the config `commit` style — no ask. Record the mode + branch + commit style in the decision log (`.mugiwara/logs/`) and todos. State-mutating consent still applies in every mode.
34
+ 2. Before touching code, follow the mode's branch/commit rule (per mode config): `guided` ASKS THE USER (auto branch for the mission or current branch; auto commit per task or user-controlled checkpoints); `semi` auto-creates the mission branch per the config `branch` key and auto-commits per task in the config `commit` style — no ask; `auto` same but commits ALWAYS. `auto_commit=off` (config, default on): guided and semi leave all changes uncommitted for the user — no commits, no push; auto mode ignores it. Record the mode + branch + commit style + auto_commit in the decision log (`.mugiwara/logs/`) and todos. State-mutating consent still applies in every mode.
35
35
  3. Sequential tasks and chains run INLINE in the main thread — no subagent round-trips for ordered work. Only `[PARALLEL]` task batches dispatch WORKER subagents (one task per worker); never another crew member; return your execution report inline to the conversation, which routes to Chopper.
36
36
  4. Every task done = evidence attached (command output / file inspection); run acceptance criteria, do not assert them.
37
37
  5. Apply `mugiwara-git` as you go: atomic commits per LOGICAL task (when auto-commit is on) — a task is a meaningful unit of work, not a micro-step; adjacent trivial changes fold into the neighboring task's commit. Save-points before risky work, commit style matched to the repo history.
@@ -14,8 +14,10 @@ Execute the plan exactly. No silent reordering, no skipping steps, no "close eno
14
14
 
15
15
  ## Ask before working
16
16
 
17
- - `guided`: before touching any code, ASK THE USER — auto branch (dedicated mission branch, recommended, keeps `main` clean) or work on the current branch; auto commit per task or commit at user-controlled checkpoints.
18
- - `semi`/`auto`: auto-create the mission branch per the config `branch` key (default `feature/{type}-{issue}-{slug}`) and auto-commit per task using the config `commit` style (default conventional). No branch/commit ask. Record mode + branch + commit style in the decision log (`.mugiwara/logs/YYYY-MM-DD-<mission>.md`) and in `.mugiwara/results/<mission>/todos.md`.
17
+ - `guided`: before touching any code, ASK THE USER — auto branch (dedicated mission branch, recommended, keeps `main` clean) or work on the current branch; auto commit per task or commit at user-controlled checkpoints. With `auto_commit=off`: the branch ask stays, the commit question is skipped — changes stay uncommitted.
18
+ - `semi`: auto-create the mission branch per the config `branch` key; auto-commit per task in the config `commit` style ONLY when `auto_commit=on` (default). Off leave every task's changes uncommitted; the user commits manually.
19
+ - `auto`: auto-create the branch and auto-commit per task ALWAYS — `auto_commit=off` has no effect in auto mode.
20
+ Record mode + branch + commit style + `auto_commit` in the decision log (`.mugiwara/logs/YYYY-MM-DD-<mission>.md`) and in `.mugiwara/results/<mission>/todos.md` — every mode.
19
21
 
20
22
  Code to the installed version's docs, not memory: `_shared/references/source-grounding.md`.
21
23
 
@@ -28,9 +30,10 @@ Before touching code:
28
30
  1. Create `.mugiwara/results/<mission>/todos.md` — one checkbox per task, derived from the plan.
29
31
  2. Check each box off only when the task completes, WITH its evidence link (`[path](relative/path)`, clickable).
30
32
  3. Re-check the whole list after each task and after each batch; unmarked boxes mean the mission is not done.
31
- 4. Mirror every change into the host's native todo tool (`todowrite` on
32
- opencode; `TaskUpdate` on Claude Code; none on tier 2/3 — plan doc only).
33
- Per-host table: `docs/reference/harness-matrix.md`.
33
+ 4. Mirror EVERY transition into the host's native todo tool (`todowrite` on
34
+ opencode; `TaskUpdate` on Claude Code; none on tier 2/3 — plan doc only) in
35
+ the SAME response the task's evidence lands — one transition per call,
36
+ never batched at wave end. Per-host table: `docs/reference/harness-matrix.md`.
34
37
 
35
38
  ## Wave execution
36
39
 
@@ -63,12 +66,10 @@ Real worker dispatch exists only where the harness has subagents — tier 1
63
66
  (Claude Code, opencode) plus Copilot. Gate the context-pressure trigger on
64
67
  that capability: if the harness cannot dispatch, do not promise fresh workers.
65
68
 
66
- Where workers are unavailable and context pressure crosses the threshold, fall
67
- back to the mechanism that already exists: write a savepoint, run the
68
- checkpoint, and suggest a fresh session via `resume`. Announce the fallback:
69
-
70
- `⚠ context 62% — no worker dispatch on this harness; savepoint written,
71
- resume in a fresh session (plan order unchanged).`
69
+ Where workers are unavailable and context pressure crosses the threshold:
70
+ write a savepoint, run the checkpoint, and suggest a fresh session via
71
+ `resume`. Announce: `⚠ context 62% no worker dispatch on this harness;
72
+ savepoint written, resume in a fresh session (plan order unchanged).`
72
73
 
73
74
  ## Batch resume
74
75
 
@@ -35,7 +35,7 @@ In `auto` mode the AI decides everything; any requirement that stays unclear aft
35
35
 
36
36
  ## Mode read (Wave 0)
37
37
 
38
- Read the runtime mode via mode config at Wave 0: `.mugiwara/config` (project) then `~/.mugiwara/config` (global); a key missing from both = `guided`. Record the active mode in the decision log. Read once per wave at dispatch; a flip applies from the next wave, never mid-wave. Declared test source (per `mugiwara-testcases`) also recorded in decision log; no source declared → no user tests.
38
+ Read the runtime mode via mode config at Wave 0: `.mugiwara/config` (project) then `~/.mugiwara/config` (global); a key missing from both = `guided`. Record the active mode AND `auto_commit` (default on) in the decision log. Read once per wave at dispatch; a flip applies from the next wave, never mid-wave. Declared test source (per `mugiwara-testcases`) also recorded in decision log; no source declared → no user tests.
39
39
 
40
40
  ## Request classifier (Wave 0) — 8 classes
41
41
 
@@ -80,7 +80,10 @@ Shortcuts ("skip X", "just do it") reroute work inside the pipeline — never ou
80
80
 
81
81
  ## Wave transitions (visibility)
82
82
 
83
- Banner `## Wave N <crew>` opens every wave; a skip is recorded, never silent. See `references/check-ins.md`.
83
+ Banner in the owning agent's color opens every wave terminal equals line
84
+ `===== WAVE 3 — ZORO (EXECUTION) =====`, markdown UI emoji heading
85
+ `## ⚔️ WAVE 3 — ZORO (EXECUTION)`. Spec + colors: `_shared/references/wave-banners.md`.
86
+ A skip is recorded, never silent.
84
87
 
85
88
  ## Work splitting
86
89
 
@@ -96,7 +99,7 @@ Recognize the in-session phrase `mugiwara mode <guided|semi|auto>`: write the pr
96
99
 
97
100
  ## Closure (Wave 9)
98
101
 
99
- Gate — every task's acceptance criteria verified, every gate passed, findings resolved or deferred with an owner, blocker ledger reviewed. Step results `results/<mission>/01..05` are evidence — kept, never deleted; only consumed cross-artifacts (`logs/`, `spec/`, `review/`, `issues/`) are removed. Run `scripts/savepoint.sh <mission>` to write final state, then `scripts/mission-report.sh <mission>` to generate the aggregate mission report at `.mugiwara/reports/YYYY-MM-DD-<mission>.md`. Write the closure summary to `.mugiwara/results/<mission>/06-closure.md`. The plan doc stays untouched. Full detail: `references/closure.md`.
102
+ Gate — every task's acceptance criteria verified, every gate passed, findings resolved or deferred with an owner, blocker ledger reviewed. Step results `results/<mission>/01..05` are evidence — kept, never deleted; only consumed cross-artifacts (`logs/`, `spec/`, `review/`, `issues/`) are removed. Run `scripts/savepoint.sh <mission>` to write final state, then `scripts/mission-report.sh <mission>` to generate the aggregate mission report at `.mugiwara/reports/YYYY-MM-DD-<mission>.md`. Write the closure summary to `.mugiwara/results/<mission>/06-closure.md`. The plan doc stays untouched. Full detail: `references/closure.md`. With `auto_commit=off` (guided/semi): skip the save-point commit and push — hand the uncommitted tree + verdict to the user; auto always pushes.
100
103
 
101
104
  ## Spirit vs letter
102
105
 
@@ -27,15 +27,26 @@ After every wave AND at the end of each execution batch, verify:
27
27
  never verbatim instructions.
28
28
  7. **Host todo synced** — the main thread mirrors the plan doc's task list into the host's native todo mechanism
29
29
  (opencode `todowrite`; Claude Code `TaskCreate`/`TaskUpdate`/`TaskList` — `TodoWrite` is deprecated since
30
- v2.1.142; tier 2/3 hosts have no native tool — plan doc only) and updates it at every task AND wave boundary
31
- (seed it at Wave 2, mark done/in_progress as tasks land). The host todo is a mirror; the plan doc stays the
32
- source of truth. Per-host table: `docs/reference/harness-matrix.md`.
30
+ v2.1.142; tier 2/3 hosts have no native tool — plan doc only). Seed it at Wave 2 (tasks + wave list 4-9);
31
+ update it in the SAME response each task's evidence lands one transition per call, never deferred to
32
+ batch or wave end; flip each wave to in_progress when its banner opens. The host todo is a mirror; the plan
33
+ doc stays the source of truth. A task done in the plan doc but not yet in the host tool is incomplete.
34
+ Per-host table: `docs/reference/harness-matrix.md`.
33
35
 
34
36
  By mode (per mode config): `guided` checks in with the user as today; `semi`/`auto` write the check-in verdicts to the decision log without pausing the pipeline.
35
37
 
36
38
  ## Wave transitions (visibility)
37
39
 
38
- Every wave opens with a visible main-thread banner `## Wave N — <crew> (<skill>)` and closes with the handoff line `→ Wave N+1 — <crew>` (Wave 9: `→ closure`). No wave starts without its banner. A wave intentionally omitted is never silent — record wave, owner, and reason in the decision log before moving on. The user must always see which crew runs now and who takes over next.
40
+ Every wave opens with a colored banner in the owning agent's color and closes
41
+ with the handoff line `→ Wave N+1 — <crew>` (Wave 9: `→ closure`). Terminal:
42
+ equals line `===== WAVE 3 — ZORO (EXECUTION) =====` wrapped in ANSI truecolor
43
+ `\x1b[38;2;R;G;Bm...\x1b[0m` (256 fallback `38;5;N`); markdown UIs: emoji
44
+ heading `## ⚔️ WAVE 3 — ZORO (EXECUTION)`, no ANSI. The literal `WAVE N —`
45
+ text must stay exact (savepoint's heal counter greps `wave 8`). Colors, emoji,
46
+ and the full spec: `_shared/references/wave-banners.md`. No wave starts without its banner. A wave intentionally
47
+ omitted is never silent — record wave, owner, and reason in the decision log
48
+ before moving on. The user must always see which crew runs now and who takes
49
+ over next.
39
50
 
40
51
  ## On drift
41
52
 
@@ -22,6 +22,15 @@ Save-point commit → push branch with plain `git push -u origin <branch>` → w
22
22
  verdict to user. Crew never creates PR, never merges, never deploys. On push
23
23
  failure, fall back to local closure report.
24
24
 
25
+ **`auto_commit=off` (guided/semi only).** No save-point commit, no push — the
26
+ working tree stays uncommitted. Write the verdict file exactly as usual, then
27
+ hand the user: the branch name, the exact commands to commit and push
28
+ (`git status` first, then `git add` of the mission's files only — never bare
29
+ `git add -A`, which would stage unrelated or secret files — then
30
+ `git commit -m "<suggested message>" && git push -u origin <branch>`), and the
31
+ verdict pointer. In `auto` mode `auto_commit` is ignored — the terminal step
32
+ runs unchanged.
33
+
25
34
  When this mission is a sub-mission of a team initiative, after closure run
26
35
  `bun scripts/initiative.ts set-status <initiative-plan> --id <sub-id> --status done`.
27
36
  When all sub-missions show `[x]`, present initiative-level closure summary.
@@ -14,32 +14,25 @@ Mugiwara's evidence lands where the team reviews. At terminal, push the mission
14
14
 
15
15
  ## Verdict file
16
16
 
17
- Write `.mugiwara/results/<mission>/07-pr-verdict.md`:
18
-
19
- - **PR Title first** `{type}: {Title Case summary}` — mandatory Title case (significant words capitalized), e.g. `Feat: Add Evidence Links To Mugiwara Reports`.
20
- - **PR Body** everything below lives in the body, in this exact order:
21
- 1. Summary — goal, mode, waves, task count.
22
- 2. Per-wave evidence table — wave, task, status, evidence link (`[path](relative/path)`).
23
- 3. Gates — quality, gates, review + security dispositions, each verdict with evidence.
24
- 4. Review & security — findings with dispositions.
25
- 5. User tests — when declared, the ATDD oracle result (per `mugiwara-testcases`), from real runs, never asserted.
26
- 6. Closure report link — `[06-closure.md](.mugiwara/results/<mission>/06-closure.md)`.
27
- 7. Verdict — PASS / FAIL with the single blocking reason, if any.
17
+ Write `.mugiwara/results/<mission>/07-pr-verdict.md` — ONE document that IS
18
+ the ready PR material (no separate report + PR-body copy). Exact order —
19
+ Title Summary (key-point bullets) What changed (compact file inventory
20
+ paragraph) Per-wave evidence Tests Checks Verdict. Full spec:
21
+ `references/verdict-format.md`.
28
22
 
29
23
  ## PR summary
30
24
 
31
- Prepare the PR description so the user can paste and submit without writing it:
32
-
33
- - Title `{type}: {Title Case summary}` (mandatory Title case), FIRST in the paste block.
34
- - Body the verdict-file PR summary block (what changed, evidence, checks).
35
- - Body order — Summary → Per-wave evidence → Gates → Review & security → User tests → Closure report link → Verdict (mirrors the verdict file).
36
- - Validate every interpolated value against the safe charset and quote it.
25
+ The verdict file IS the PR summary. No second block: the user pastes the file
26
+ — title line into the PR title, the rest into the body. Order mirrors the
27
+ verdict file (title summary what changed per-wave evidence tests
28
+ checks verdict). Validate every interpolated value against the safe charset
29
+ and quote it.
37
30
 
38
31
  The summary is material, never posted — the crew stops at push.
39
32
 
40
33
  ## Handoff rule
41
34
 
42
- Push the branch + write the verdict file at terminal, after every wave passes (never a draft state — the user opens the PR when they choose). The verdict is delivered as a file, not posted; the user pastes it into their PR. Never per-wave (reviewer noise).
35
+ Push the branch + write the verdict file at terminal, after every wave passes (never a draft state — the user opens the PR when they choose). The verdict is delivered as a file, not posted; the user pastes it into their PR. Never per-wave (reviewer noise). With `auto_commit=off` (guided/semi only): nothing to push — write the verdict file, hand the UNCOMMITTED working tree to the user with the exact commit + push commands; `auto` mode always pushes.
43
36
 
44
37
  ## Push adapter (plain git, no gh)
45
38
 
@@ -0,0 +1,31 @@
1
+ # Verdict file format
2
+
3
+ `.mugiwara/results/<mission>/07-pr-verdict.md` — ONE document that IS the
4
+ ready PR material (the user copies the title line and the body as-is). No
5
+ separate report section plus a PR-body copy: one flow, in this exact order:
6
+
7
+ 1. **Title** — `# {type}: {Title Case summary}` — mandatory Title case, e.g.
8
+ `# Feat: Add Evidence Links To Mugiwara Reports`.
9
+ 2. **Summary** — goal, mode, waves, task count, branch/stacking note,
10
+ closure report link (`[06-closure.md](.mugiwara/results/<mission>/06-closure.md)`);
11
+ then the mission's key points as compact bullets (what each defect/feature
12
+ does — never a file list).
13
+ 3. **What changed** — ONE compact paragraph, file inventory only: `<N> files:
14
+ <comma-separated paths>, <grouped counts>, docs (dir or file list), README.`
15
+ Feature detail lives in Summary, not here.
16
+ 4. **Per-wave evidence** — wave, task, status, evidence link
17
+ (`[path](relative/path)`). Gates, review, security, and heal rows live here
18
+ with their dispositions.
19
+ 5. **Tests** — captured test counts (never asserted); the ATDD oracle result
20
+ when user tests were declared (per `mugiwara-testcases`).
21
+ 6. **Checks** — one `- [x]` checkbox bullet per gate: typecheck, tests
22
+ (`N/N`), build, content/manifest/doc-integrity, lane-base, npm pack,
23
+ evals + retrieval, verify-install (`N/N` pointers), gate-selftest
24
+ (`N/N` mutations prove red).
25
+ 7. **Verdict** — PASS / FAIL with the single blocking reason, if any.
26
+
27
+ The file IS the PR summary. No second block: the user pastes the file — title
28
+ line into the PR title, the rest into the body. Order mirrors the verdict file
29
+ (title → summary → what changed → per-wave evidence → tests → checks →
30
+ verdict). Validate every interpolated value against the safe charset and quote
31
+ it.
@@ -46,7 +46,10 @@ 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.** 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.
49
+ **Banners.** Every wave opens with a banner in the owning agent's color and
50
+ closes with a handoff line — terminal ANSI equals line, markdown-UIs emoji
51
+ heading; handoff `→ Wave 4 — Chopper (Checkpoint)`. Keep literal `WAVE N —`
52
+ (savepoint's heal counter greps it). Spec + colors: `_shared/references/wave-banners.md`.
50
53
 
51
54
  **Subagents only for parallelism.** `[PARALLEL]` task batches, parallel review, parallel heal workers. Crew members never dispatch crew members.
52
55
 
@@ -103,6 +106,7 @@ Archive, never delete: run `mugiwara archive <mission>` — folds `logs/`, `spec
103
106
  5. Plan doc is source of truth from Wave 2.
104
107
  6. Resume via `resume-coordinator` before any wave — never restart.
105
108
  7. Push branch + hand verdict to user; crew never merges or deploys.
109
+ 8. Host todo mirrors the plan doc every task + wave — same response as evidence.
106
110
 
107
111
  ## Iron Law
108
112
 
package/dist/mugiwara.js CHANGED
@@ -188,27 +188,43 @@ var target = {
188
188
  };
189
189
 
190
190
  // src/targets/opencode.ts
191
- import { existsSync as existsSync2, readdirSync as readdirSync2, copyFileSync as copyFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
191
+ import { existsSync as existsSync2, readdirSync as readdirSync2, copyFileSync as copyFileSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2 } from "node:fs";
192
192
  import { dirname as dirname2, join as join2 } from "node:path";
193
193
  import { fileURLToPath as fileURLToPath2 } from "node:url";
194
194
  var here2 = dirname2(fileURLToPath2(import.meta.url));
195
195
  var COMMANDS_SRC2 = join2(here2, "..", "..", ".opencode", "commands");
196
+ var BANNER_TABLE = join2(here2, "..", "..", "references", "wave-banners.md");
196
197
  var CREW = {
197
198
  "luffy-orchestrator": { color: "#ef4444", temperature: 0.2, steps: 15 },
198
- "usopp-brainstorm": { color: "#f59e0b", temperature: 0.6, steps: 15 },
199
+ "usopp-brainstorm": { color: "#b45309", temperature: 0.6, steps: 15 },
199
200
  "nami-planner": { color: "#f97316", temperature: 0.2, steps: 15 },
200
201
  "zoro-execution": { color: "#22c55e", temperature: 0.1, steps: 30 },
201
- "chopper-checkpoint": { color: "#3b82f6", temperature: 0.1, steps: 15 },
202
- "sanji-quality": { color: "#a855f7", temperature: 0.1, steps: 10 },
202
+ "chopper-checkpoint": { color: "#60a5fa", temperature: 0.1, steps: 15 },
203
+ "sanji-quality": { color: "#facc15", temperature: 0.1, steps: 10 },
203
204
  "franky-gates": { color: "#06b6d4", temperature: 0.1, steps: 10 },
204
205
  "robin-reviewer": { color: "#8b5cf6", temperature: 0.2, steps: 15 },
205
206
  "jinbe-security": { color: "#6366f1", temperature: 0.2, steps: 15 },
206
- "brook-healing": { color: "#ec4899", temperature: 0.1, steps: 20 },
207
+ "brook-healing": { color: "#2dd4bf", temperature: 0.1, steps: 20 },
207
208
  "skeptic-verifier": { color: "#64748b", temperature: 0.1, steps: 12 },
208
209
  "eval-runner": { color: "#14b8a6", temperature: 0.2, steps: 15 },
209
210
  "resume-coordinator": { color: "#d97706", temperature: 0.2, steps: 10 },
210
211
  "memory-keeper": { color: "#d946ef", temperature: 0.2, steps: 8 }
211
212
  };
213
+ function readBannerColors() {
214
+ try {
215
+ if (!existsSync2(BANNER_TABLE))
216
+ return {};
217
+ const text = readFileSync2(BANNER_TABLE, "utf8");
218
+ const colors = Object.create(null);
219
+ for (const m of text.matchAll(/^\| ([\w-]+) \| [^|]+ \| (#[0-9a-f]{6}) \| (\d+) \| (\S+) \|\r?$/gm)) {
220
+ colors[m[1]] = m[2];
221
+ }
222
+ return colors;
223
+ } catch {
224
+ return {};
225
+ }
226
+ }
227
+ var BANNER_COLORS = readBannerColors();
212
228
  function permissionFromScope(scope) {
213
229
  if (scope === "source")
214
230
  return { edit: "allow" };
@@ -219,8 +235,11 @@ function permissionFromScope(scope) {
219
235
  function agentFrontmatter(name, description, internal, writeScope) {
220
236
  const crew = CREW[name];
221
237
  const lines = [`description: ${description}`, `mode: all`];
238
+ const color = BANNER_COLORS[name] ?? crew?.color;
239
+ if (color)
240
+ lines.push(`color: '${color}'`);
222
241
  if (crew) {
223
- lines.push(`color: '${crew.color}'`, `temperature: ${crew.temperature}`, `steps: ${crew.steps}`);
242
+ lines.push(`temperature: ${crew.temperature}`, `steps: ${crew.steps}`);
224
243
  const perm = internal ? permissionFromScope(writeScope) : undefined;
225
244
  if (perm) {
226
245
  lines.push("permission:");
@@ -477,21 +496,21 @@ var targets = { claude: target, opencode: target2, copilot: target3, gemini: tar
477
496
  var TARGET_IDS = Object.keys(targets);
478
497
 
479
498
  // src/installer.ts
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";
499
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync3, writeFileSync as writeFileSync2, copyFileSync as copyFileSync3, rmSync, lstatSync } from "node:fs";
481
500
  import { dirname as dirname3, join as join5 } from "node:path";
482
501
  import { homedir } from "node:os";
483
502
  import { fileURLToPath as fileURLToPath3 } from "node:url";
484
503
  var CONTENT_DIR = join5(dirname3(fileURLToPath3(import.meta.url)), "..", "content");
485
- var pkg = JSON.parse(readFileSync2(join5(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf8"));
504
+ var pkg = JSON.parse(readFileSync3(join5(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf8"));
486
505
  var VERSION = pkg.version;
487
506
  function collectContent() {
488
507
  const skillNames = readdirSync3(join5(CONTENT_DIR, "skills"), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
489
508
  const skills = skillNames.map((name) => {
490
- const { data, body } = parseFrontmatter(readFileSync2(join5(CONTENT_DIR, "skills", name, "SKILL.md"), "utf8"));
509
+ const { data, body } = parseFrontmatter(readFileSync3(join5(CONTENT_DIR, "skills", name, "SKILL.md"), "utf8"));
491
510
  return { name, data, body, refs: collectRefs(join5(CONTENT_DIR, "skills", name)) };
492
511
  });
493
512
  const agents = readdirSync3(join5(CONTENT_DIR, "agents")).filter((f) => f.endsWith(".md")).map((f) => {
494
- const { data, body } = parseFrontmatter(readFileSync2(join5(CONTENT_DIR, "agents", f), "utf8"));
513
+ const { data, body } = parseFrontmatter(readFileSync3(join5(CONTENT_DIR, "agents", f), "utf8"));
495
514
  return { name: f.replace(/\.md$/, ""), data, body, refs: [], internal: data.internal === "true" };
496
515
  });
497
516
  const sharedRefsDir = join5(dirname3(CONTENT_DIR), "references");
@@ -500,7 +519,7 @@ function collectContent() {
500
519
  for (const f of readdirSync3(sharedRefsDir)) {
501
520
  if (!f.endsWith(".md"))
502
521
  continue;
503
- sharedRefs.push({ relPath: f, text: readFileSync2(join5(sharedRefsDir, f), "utf8") });
522
+ sharedRefs.push({ relPath: f, text: readFileSync3(join5(sharedRefsDir, f), "utf8") });
504
523
  }
505
524
  }
506
525
  return { skills, agents, sharedRefs };
@@ -509,7 +528,7 @@ function collectRefs(skillDir) {
509
528
  const refsDir = join5(skillDir, "references");
510
529
  if (!existsSync4(refsDir))
511
530
  return [];
512
- return readdirSync3(refsDir, { recursive: true }).map((f) => String(f)).filter((f) => f.endsWith(".md")).map((rel) => ({ relPath: rel, text: readFileSync2(join5(refsDir, rel), "utf8") }));
531
+ return readdirSync3(refsDir, { recursive: true }).map((f) => String(f)).filter((f) => f.endsWith(".md")).map((rel) => ({ relPath: rel, text: readFileSync3(join5(refsDir, rel), "utf8") }));
513
532
  }
514
533
  function installTo(target10, opts) {
515
534
  const { scope, projectDir, dryRun = false, force = false } = opts;
@@ -520,7 +539,7 @@ function installTo(target10, opts) {
520
539
  const result = { written: [], skipped: [], backedUp: [], notes: [] };
521
540
  const writeOne = (absPath, text) => {
522
541
  if (existsSync4(absPath)) {
523
- if (readFileSync2(absPath, "utf8") === text) {
542
+ if (readFileSync3(absPath, "utf8") === text) {
524
543
  result.skipped.push(absPath);
525
544
  return;
526
545
  }
@@ -641,7 +660,7 @@ function ensureProjectGitignore(projectDir, opts = {}) {
641
660
  const { dryRun = false } = opts;
642
661
  const path = join5(projectDir, ".gitignore");
643
662
  assertNotSymlink(path);
644
- let existing = existsSync4(path) ? readFileSync2(path, "utf8") : "";
663
+ let existing = existsSync4(path) ? readFileSync3(path, "utf8") : "";
645
664
  if (existing) {
646
665
  if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes(".mugiwara/state/")) {
647
666
  return { appended: false, notes: [] };
@@ -649,7 +668,7 @@ function ensureProjectGitignore(projectDir, opts = {}) {
649
668
  const hadOld = existing.includes(GITIGNORE_MARKER) || existing.includes(GITIGNORE_BLOCK_START);
650
669
  if (hadOld) {
651
670
  const clean = removeProjectGitignore(projectDir, { dryRun });
652
- existing = clean.removed ? existsSync4(path) ? readFileSync2(path, "utf8") : "" : existing;
671
+ existing = clean.removed ? existsSync4(path) ? readFileSync3(path, "utf8") : "" : existing;
653
672
  }
654
673
  }
655
674
  const separator = existing.length && !existing.endsWith(`
@@ -666,7 +685,7 @@ function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
666
685
  assertNotSymlink(path);
667
686
  if (!existsSync4(path))
668
687
  return { removed: false, notes: [] };
669
- const current = readFileSync2(path, "utf8");
688
+ const current = readFileSync3(path, "utf8");
670
689
  const delimited = current.includes(GITIGNORE_BLOCK_START);
671
690
  let cleaned;
672
691
  if (delimited) {
@@ -709,13 +728,13 @@ function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
709
728
  }
710
729
 
711
730
  // src/manifest.ts
712
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
731
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
713
732
  import { dirname as dirname4, join as join6 } from "node:path";
714
733
  function manifestPath({ scope, projectDir, home }) {
715
734
  return scope === "global" ? join6(home, ".mugiwara", "manifest.json") : join6(projectDir, ".mugiwara", "manifest.json");
716
735
  }
717
736
  function readManifest(file) {
718
- return existsSync5(file) ? JSON.parse(readFileSync3(file, "utf8")) : null;
737
+ return existsSync5(file) ? JSON.parse(readFileSync4(file, "utf8")) : null;
719
738
  }
720
739
  function writeManifest(file, data) {
721
740
  mkdirSync4(dirname4(file), { recursive: true });
@@ -724,7 +743,7 @@ function writeManifest(file, data) {
724
743
  }
725
744
 
726
745
  // src/mission.ts
727
- import { existsSync as existsSync6, rmSync as rmSync2, readFileSync as readFileSync4, readdirSync as readdirSync4, mkdirSync as mkdirSync5, appendFileSync } from "node:fs";
746
+ import { existsSync as existsSync6, rmSync as rmSync2, readFileSync as readFileSync5, readdirSync as readdirSync4, mkdirSync as mkdirSync5, appendFileSync } from "node:fs";
728
747
  import { join as join7 } from "node:path";
729
748
  function activeActor(projectDir) {
730
749
  const stateDir = join7(projectDir, ".mugiwara", "state");
@@ -736,7 +755,7 @@ function activeActor(projectDir) {
736
755
  const d = join7(stateDir, mission);
737
756
  for (const f of readdirSync4(d).filter((f2) => f2.endsWith(".json"))) {
738
757
  try {
739
- const s = JSON.parse(readFileSync4(join7(d, f), "utf8"));
758
+ const s = JSON.parse(readFileSync5(join7(d, f), "utf8"));
740
759
  const t = Date.parse(s.updated_at || "") || 0;
741
760
  if (!latest || t > latest.updated)
742
761
  latest = { actor: s.actor || null, updated: t };
@@ -881,7 +900,7 @@ function archiveMission(projectDir, mission, opts = {}) {
881
900
  `;
882
901
  if (!dryRun) {
883
902
  mkdirSync5(join7(root, "reports"), { recursive: true });
884
- const existing = existsSync6(indexFile) ? readFileSync4(indexFile, "utf8") : "";
903
+ const existing = existsSync6(indexFile) ? readFileSync5(indexFile, "utf8") : "";
885
904
  if (!existing.split(/\r?\n/).some((l) => l.startsWith(`- ${mission} —`))) {
886
905
  const header = existing ? "" : `# Mission index
887
906
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mugiwara",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ionivetech/mugiwara",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
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.3",
4
+ "version": "0.6.4",
5
5
  "author": {
6
6
  "name": "ionivetech"
7
7
  },
@@ -0,0 +1,75 @@
1
+ # Wave Banners — formats + crew colors
2
+
3
+ The single source for wave-banner rendering and crew colors. The opencode
4
+ plugin and the opencode-target generator derive agent UI colors from the table
5
+ below — change a color HERE, never in code. This file is machine-parsed:
6
+ keep the table format exact (one row per agent, pipes, no extra columns).
7
+
8
+ ## Terminal format (ANSI truecolor)
9
+
10
+ Every wave opens with the banner line in the owning agent's color, and closes
11
+ with a handoff line. Wrap the whole line in the ANSI truecolor of the agent:
12
+
13
+ ```
14
+ \x1b[38;2;R;G;Bm==================== WAVE 3 — ZORO (EXECUTION) ====================\x1b[0m
15
+ → Wave 4 — Chopper (Checkpoint)
16
+ ```
17
+
18
+ - RGB values come from the `hex` column below; R/G/B are the hex channels in
19
+ decimal (truecolor `38;2;R;G;B`). If the terminal lacks truecolor, use the
20
+ `ansi-256` index: `\x1b[38;5;Nm`.
21
+ - Wave 9's handoff: `→ closure`.
22
+ - The literal `WAVE N —` text must stay exact: `scripts/savepoint.sh` counts
23
+ heal cycles by grepping `wave 8` (case-insensitive) in the trace. A banner
24
+ that drops the literal silently resets the heal loop.
25
+ - Never convey the wave by color alone — the crew name and emoji always
26
+ accompany the color.
27
+
28
+ ## UI format (markdown UIs)
29
+
30
+ Rich UIs (Claude Code UI, VSCode, Codex) strip or garble ANSI escapes. Emit
31
+ the emoji-heading form instead — no ANSI, no equals line:
32
+
33
+ ```
34
+ ## ⚔️ WAVE 3 — ZORO (EXECUTION)
35
+ → Wave 4 — Chopper (Checkpoint)
36
+ ```
37
+
38
+ Same literal `WAVE N —` rule applies.
39
+
40
+ ## Crew colors
41
+
42
+ | agent-id | role | hex | ansi-256 | emoji |
43
+ |----------|------|-----|----------|-------|
44
+ | luffy-orchestrator | Luffy | #ef4444 | 196 | 🏴‍☠️ |
45
+ | usopp-brainstorm | Usopp | #b45309 | 130 | 🎯 |
46
+ | nami-planner | Nami | #f97316 | 208 | 🧭 |
47
+ | zoro-execution | Zoro | #22c55e | 34 | ⚔️ |
48
+ | chopper-checkpoint | Chopper | #60a5fa | 75 | 🩺 |
49
+ | sanji-quality | Sanji | #facc15 | 220 | 🍳 |
50
+ | franky-gates | Franky | #06b6d4 | 45 | 🔧 |
51
+ | robin-reviewer | Robin | #8b5cf6 | 99 | 📚 |
52
+ | jinbe-security | Jinbe | #6366f1 | 63 | 🌊 |
53
+ | brook-healing | Brook | #2dd4bf | 43 | 🎻 |
54
+ | skeptic-verifier | Skeptic | #64748b | 245 | 🔍 |
55
+ | eval-runner | EvalRunner | #14b8a6 | 37 | 🧪 |
56
+ | resume-coordinator | Resume | #d97706 | 172 | 🔄 |
57
+ | memory-keeper | MemoryKeeper | #d946ef | 200 | 🧠 |
58
+ | onboarding-guide | Guide | #0ea5e9 | 75 | 🚀 |
59
+
60
+ Skeptic, EvalRunner, Resume, MemoryKeeper and Guide are internal/dispatch or
61
+ wizard agents — their banners appear only when a wave or worker names them.
62
+
63
+ ## Rules
64
+
65
+ 1. Banner before EVERY wave; handoff after it. No wave starts without its
66
+ banner (orchestration red flag).
67
+ 2. The color comes from this table only — never invent a hex mid-mission.
68
+ 3. Terminal variant when the session looks like a terminal; UI variant for
69
+ markdown-rendering UIs. When unsure, the UI variant is safe everywhere.
70
+ 4. Only the crew table's colors and the two SGR forms above (truecolor,
71
+ 256-index) may appear in a banner — never other escape families (OSC,
72
+ title, cursor, other SGR codes). The banner is a fixed template, not a
73
+ formatting playground.
74
+ 5. Adding a crew member? Add the row here first; the plugin and target
75
+ generator pick it up automatically.
@@ -163,6 +163,37 @@ if (!existsSync(patternsFile)) {
163
163
  }
164
164
  }
165
165
 
166
+ // --- D3b mutation: strip the NEW pattern categories → lane-integrity red ---
167
+ console.log('\nD3b — new category strip mutation');
168
+ if (!existsSync(patternsFile)) {
169
+ console.log(' ⚠ patterns.sh not found, skipping');
170
+ } else {
171
+ const original = readFileSync(patternsFile, 'utf8');
172
+ try {
173
+ // derive the broken list from the LIVE source, stripping the exact tokens
174
+ // of the v0.6.4 D3 families — a hard-coded baseline rots the moment a
175
+ // category is added (G3). New family tokens must be added here too.
176
+ const D3B_FAMILY_TOKENS = new Set([
177
+ // raw regex tokens as they appear in live SENSITIVE_PATS (backslashes included)
178
+ 'oauth2?/', 'credential', 'sessions?/', 'tokens?/', 'rbac', 'permissions?/', 'acls?/', 'iam/',
179
+ '\\.p12$', '\\.key$', '\\.pem$', 'migrate/', 'Dockerfile', 'docker-compose', '\\.github/workflows/',
180
+ 'webhooks?/', 'secret/', 'secrets?\\.ya?ml$', '\\.tfvars$', '\\.env$', '\\.env\\.',
181
+ ]);
182
+ const live = original.match(/SENSITIVE_PATS="([^"]+)"/)?.[1] ?? '';
183
+ const broken = live.split('|').filter(t => !D3B_FAMILY_TOKENS.has(t)).join('|');
184
+ const brokenLine = `SENSITIVE_PATS="${broken}"`;
185
+ if (broken === live || !live) {
186
+ console.log(' ⚠ D3b: no D3 family tokens found in live SENSITIVE_PATS — skipping');
187
+ } else {
188
+ writeFileSync(patternsFile, original.replace(/SENSITIVE_PATS="[^"]*"/, brokenLine));
189
+ assert('missing new categories → lane-integrity fails', false, () => run('D3b', 'bun run test -- lane-integrity -t "sensitive-paths"'));
190
+ }
191
+ } finally {
192
+ writeFileSync(patternsFile, original);
193
+ assert('restored → lane-integrity passes', true, () => run('D3b', 'bun run test -- lane-integrity -t "sensitive-paths"'));
194
+ }
195
+ }
196
+
166
197
  // --- D4 mutation: zero LOC_TOKENS → lane-integrity red ---
167
198
  console.log('\nD4 — churn token mutation');
168
199
  if (!existsSync(savepointFile)) {
package/scripts/lane.sh CHANGED
@@ -23,7 +23,7 @@ CHANGED=$(git diff --name-only "$BASE"..HEAD 2>/dev/null || git diff --name-only
23
23
  FILE_COUNT=0
24
24
  [ -n "$CHANGED" ] && FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
25
25
 
26
- SENSITIVE=$(echo "$CHANGED" | grep -E "$SENSITIVE_PATS" 2>/dev/null | head -5 | tr '\n' ',' | sed 's/,$//' || true)
26
+ SENSITIVE=$(echo "$CHANGED" | grep -E "$SENSITIVE_PATS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
27
27
  HAS_SENSITIVE=0
28
28
  [ -n "$SENSITIVE" ] && HAS_SENSITIVE=1
29
29
 
@@ -4,10 +4,16 @@
4
4
 
5
5
  # Sensitive-path escalation patterns. A changed file matching any of these
6
6
  # always escalates to Lane 3 (Full), regardless of file count. Plural forms
7
- # included (payments/, migrations/) the singular-only list missed them (D3).
7
+ # included (payments/, migrations/) and, since v0.6.4, the D3 categories:
8
+ # oauth, credential(s), session(s)/, token(s)/, rbac, permission(s), acl(s)/,
9
+ # iam/, cert keys (.pem/.key/.p12), migrate/, Dockerfile, docker-compose,
10
+ # .github/workflows/, webhooks?/, secret yaml, .tfvars, and .env variants
11
+ # (.env.local, .env.production — .env$ alone missed them, D3 follow-up).
12
+ # Dir-anchored (not bare): oauth2?/, permissions?/, tokens?/, sessions?/,
13
+ # acls?/ — bare forms over-matched docs (oauth-guide.md, permissionless.ts).
8
14
  # Deliberately NOT matched: package.json (dependency churn is policy-as-code,
9
15
  # not a sensitive lane trigger), authors/ (contains "auth" but never auth/).
10
- SENSITIVE_PATS="auth/|payment/|payments/|billing/|crypto/|secrets/|\.env$|config/.*key|migration/|migrations/|\.sql$|schema\.|\.prisma$|\.terraform|\.tf$"
16
+ SENSITIVE_PATS="auth/|oauth2?/|payment/|payments/|billing/|crypto/|secrets/|credential|sessions?/|tokens?/|rbac|permissions?/|acls?/|iam/|\.env$|\.env\.|config/.*key|\.p12$|\.key$|\.pem$|migration/|migrations/|migrate/|\.sql$|schema\.|\.prisma$|\.terraform|\.tf$|Dockerfile|docker-compose|\.github/workflows/|webhooks?/|secret/|secrets?\.ya?ml$|\.tfvars$"
11
17
 
12
18
  # Product surface: paths that count toward file-based lane sizing. Changes
13
19
  # outside this surface are docs/config/asset and never escalate to full on
@@ -1,5 +1,5 @@
1
1
  // src/targets/opencode.ts
2
- import { existsSync, readdirSync, copyFileSync, mkdirSync } from 'node:fs';
2
+ import { existsSync, readdirSync, copyFileSync, mkdirSync, readFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { stringifyFrontmatter, type FrontmatterData } from '../frontmatter.ts';
@@ -7,6 +7,7 @@ import type { Target } from '../installer.ts';
7
7
 
8
8
  const here = dirname(fileURLToPath(import.meta.url));
9
9
  const COMMANDS_SRC = join(here, '..', '..', '.opencode', 'commands');
10
+ const BANNER_TABLE = join(here, '..', '..', 'references', 'wave-banners.md');
10
11
 
11
12
  type CrewConfig = {
12
13
  color: string;
@@ -15,23 +16,46 @@ type CrewConfig = {
15
16
  permission?: Record<string, string>;
16
17
  };
17
18
 
19
+ // Colors are fallbacks — the wave-banners table is the source of truth.
20
+ // Temperature/steps stay here (runtime tuning, not banner material).
18
21
  const CREW: Record<string, CrewConfig> = {
19
22
  'luffy-orchestrator': { color: '#ef4444', temperature: 0.2, steps: 15 },
20
- 'usopp-brainstorm': { color: '#f59e0b', temperature: 0.6, steps: 15 },
23
+ 'usopp-brainstorm': { color: '#b45309', temperature: 0.6, steps: 15 },
21
24
  'nami-planner': { color: '#f97316', temperature: 0.2, steps: 15 },
22
25
  'zoro-execution': { color: '#22c55e', temperature: 0.1, steps: 30 },
23
- 'chopper-checkpoint': { color: '#3b82f6', temperature: 0.1, steps: 15 },
24
- 'sanji-quality': { color: '#a855f7', temperature: 0.1, steps: 10 },
26
+ 'chopper-checkpoint': { color: '#60a5fa', temperature: 0.1, steps: 15 },
27
+ 'sanji-quality': { color: '#facc15', temperature: 0.1, steps: 10 },
25
28
  'franky-gates': { color: '#06b6d4', temperature: 0.1, steps: 10 },
26
29
  'robin-reviewer': { color: '#8b5cf6', temperature: 0.2, steps: 15 },
27
30
  'jinbe-security': { color: '#6366f1', temperature: 0.2, steps: 15 },
28
- 'brook-healing': { color: '#ec4899', temperature: 0.1, steps: 20 },
31
+ 'brook-healing': { color: '#2dd4bf', temperature: 0.1, steps: 20 },
29
32
  'skeptic-verifier': { color: '#64748b', temperature: 0.1, steps: 12 },
30
33
  'eval-runner': { color: '#14b8a6', temperature: 0.2, steps: 15 },
31
34
  'resume-coordinator': { color: '#d97706', temperature: 0.2, steps: 10 },
32
35
  'memory-keeper': { color: '#d946ef', temperature: 0.2, steps: 8 },
33
36
  };
34
37
 
38
+ // Crew colors from the wave-banners table (single source of truth). Returns
39
+ // {} on any failure — callers fall back to the CREW map. Same table shape as
40
+ // the opencode plugin parses: | agent-id | role | hex | ansi-256 | emoji |
41
+ function readBannerColors(): Record<string, string> {
42
+ try {
43
+ if (!existsSync(BANNER_TABLE)) return {};
44
+ const text = readFileSync(BANNER_TABLE, 'utf8');
45
+ // null-prototype + CRLF-tolerant: agent ids are trusted repo content, but
46
+ // a future `__proto__` id must never write the object's prototype
47
+ const colors: Record<string, string> = Object.create(null);
48
+ for (const m of text.matchAll(/^\| ([\w-]+) \| [^|]+ \| (#[0-9a-f]{6}) \| (\d+) \| (\S+) \|\r?$/gm)) {
49
+ colors[m[1]] = m[2];
50
+ }
51
+ return colors;
52
+ } catch {
53
+ return {};
54
+ }
55
+ }
56
+
57
+ const BANNER_COLORS = readBannerColors();
58
+
35
59
  // write-scope is the single source of truth (content/agents/*.md frontmatter),
36
60
  // but it is a RULE for user-facing crew agents: they run inline in the main
37
61
  // thread, so a runtime permission bound to the active-agent identity would
@@ -50,8 +74,12 @@ function permissionFromScope(scope: string | undefined): Record<string, string |
50
74
  function agentFrontmatter(name: string, description: string, internal: boolean, writeScope?: string) {
51
75
  const crew = CREW[name];
52
76
  const lines = [`description: ${description}`, `mode: all`];
77
+ // color: wave-banners table wins; CREW fallback for agents the table lacks.
78
+ // temperature/steps only exist for CREW members (runtime tuning).
79
+ const color = BANNER_COLORS[name] ?? crew?.color;
80
+ if (color) lines.push(`color: '${color}'`);
53
81
  if (crew) {
54
- lines.push(`color: '${crew.color}'`, `temperature: ${crew.temperature}`, `steps: ${crew.steps}`);
82
+ lines.push(`temperature: ${crew.temperature}`, `steps: ${crew.steps}`);
55
83
  const perm = internal ? permissionFromScope(writeScope) : undefined;
56
84
  if (perm) {
57
85
  lines.push('permission:');