@ionivetech/mugiwara 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +3 -2
  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-continue.md +16 -0
  7. package/.opencode/commands/mugiwara-execute.md +6 -4
  8. package/.opencode/commands/mugiwara-heal.md +6 -4
  9. package/.opencode/commands/mugiwara-plan.md +6 -4
  10. package/.opencode/commands/mugiwara-review.md +6 -4
  11. package/.opencode/commands/mugiwara-security.md +6 -4
  12. package/.opencode/commands/mugiwara-ship.md +6 -4
  13. package/.opencode/commands/mugiwara.md +4 -2
  14. package/.opencode/commands/using-mugiwara.md +7 -13
  15. package/.opencode/plugins/mugiwara.mjs +18 -6
  16. package/AGENTS.md +1 -1
  17. package/GEMINI.md +1 -1
  18. package/README.md +251 -120
  19. package/content/agents/brook-healing.md +17 -3
  20. package/content/agents/chopper-checkpoint.md +17 -3
  21. package/content/agents/eval-runner.md +17 -2
  22. package/content/agents/franky-gates.md +19 -5
  23. package/content/agents/jinbe-security.md +19 -3
  24. package/content/agents/luffy-orchestrator.md +6 -3
  25. package/content/agents/memory-keeper.md +15 -0
  26. package/content/agents/nami-planner.md +21 -6
  27. package/content/agents/onboarding-guide.md +145 -0
  28. package/content/agents/resume-coordinator.md +21 -5
  29. package/content/agents/robin-reviewer.md +18 -3
  30. package/content/agents/sanji-quality.md +19 -4
  31. package/content/agents/skeptic-verifier.md +18 -3
  32. package/content/agents/usopp-brainstorm.md +17 -3
  33. package/content/agents/zoro-execution.md +18 -3
  34. package/content/skills/mugiwara-backend/SKILL.md +1 -1
  35. package/content/skills/mugiwara-brainstorm/SKILL.md +5 -1
  36. package/content/skills/mugiwara-checkpoint/SKILL.md +2 -2
  37. package/content/skills/mugiwara-contract-first/SKILL.md +2 -36
  38. package/content/skills/mugiwara-contract-first/references/process.md +37 -0
  39. package/content/skills/mugiwara-execution/SKILL.md +30 -13
  40. package/content/skills/mugiwara-execution/references/resume-batching.md +32 -0
  41. package/content/skills/mugiwara-frontend/SKILL.md +1 -1
  42. package/content/skills/mugiwara-gates/SKILL.md +32 -30
  43. package/content/skills/mugiwara-healing/SKILL.md +1 -1
  44. package/content/skills/mugiwara-orchestration/SKILL.md +41 -25
  45. package/content/skills/mugiwara-orchestration/references/delegation-pillars.md +40 -0
  46. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +96 -0
  47. package/content/skills/mugiwara-planning/SKILL.md +23 -27
  48. package/content/skills/mugiwara-planning/references/plan-template.md +42 -0
  49. package/content/skills/mugiwara-pr/SKILL.md +2 -2
  50. package/content/skills/mugiwara-quality/SKILL.md +10 -6
  51. package/content/skills/mugiwara-resume/SKILL.md +7 -2
  52. package/content/skills/mugiwara-review/SKILL.md +20 -1
  53. package/content/skills/mugiwara-review/references/code-attributes.md +38 -0
  54. package/content/skills/mugiwara-root-cause/SKILL.md +3 -30
  55. package/content/skills/mugiwara-root-cause/references/process.md +35 -0
  56. package/content/skills/mugiwara-security/SKILL.md +17 -32
  57. package/content/skills/mugiwara-security/references/owasp-top10.md +12 -0
  58. package/content/skills/mugiwara-security/references/rationalizations.md +8 -0
  59. package/content/skills/mugiwara-ship/SKILL.md +5 -4
  60. package/content/skills/mugiwara-workflow/SKILL.md +74 -77
  61. package/content/skills/mugiwara-workflow/references/workspace-layout.md +50 -0
  62. package/content/skills/using-mugiwara/SKILL.md +8 -19
  63. package/dist/mugiwara.js +107 -41
  64. package/gemini-extension.json +1 -1
  65. package/hooks/session-start.ts +1 -1
  66. package/package.json +11 -4
  67. package/plugin.json +1 -1
  68. package/references/multi-actor.md +1 -1
  69. package/scripts/evidence.sh +12 -5
  70. package/scripts/gate-selftest.ts +266 -0
  71. package/scripts/initiative.ts +248 -0
  72. package/scripts/lane.sh +15 -0
  73. package/scripts/mission-report.sh +90 -12
  74. package/scripts/onboard.ts +292 -0
  75. package/scripts/release-notes.ts +42 -13
  76. package/scripts/savepoint.sh +97 -50
  77. package/scripts/validate-content.ts +77 -0
  78. package/scripts/verify-install.ts +72 -0
  79. package/src/installer.ts +34 -7
  80. package/src/mission.ts +7 -5
  81. package/src/targets/claude.ts +20 -3
  82. package/src/targets/generic.ts +1 -1
  83. package/src/targets/opencode.ts +31 -10
  84. package/docs/concepts/agents.md +0 -53
  85. package/docs/concepts/audit-trail.md +0 -65
  86. package/docs/concepts/comparison.md +0 -58
  87. package/docs/concepts/config.md +0 -55
  88. package/docs/concepts/cost.md +0 -45
  89. package/docs/concepts/execution-model.md +0 -92
  90. package/docs/concepts/git-strategy.md +0 -62
  91. package/docs/concepts/lanes.md +0 -82
  92. package/docs/concepts/modes.md +0 -73
  93. package/docs/concepts/pr-summary.md +0 -54
  94. package/docs/concepts/skills.md +0 -55
  95. package/docs/concepts/workflow.md +0 -89
  96. package/docs/getting-started.md +0 -158
  97. package/docs/index.md +0 -56
  98. package/docs/install/antigravity.md +0 -45
  99. package/docs/install/claude.md +0 -77
  100. package/docs/install/cli.md +0 -115
  101. package/docs/install/codex.md +0 -44
  102. package/docs/install/copilot.md +0 -45
  103. package/docs/install/cursor.md +0 -45
  104. package/docs/install/gemini.md +0 -44
  105. package/docs/install/index.md +0 -53
  106. package/docs/install/kimi.md +0 -45
  107. package/docs/install/opencode.md +0 -143
  108. package/docs/install/pi.md +0 -46
  109. package/docs/reference/adoption-guide.md +0 -72
  110. package/docs/reference/agent-anatomy.md +0 -72
  111. package/docs/reference/compliance-matrix.md +0 -81
  112. package/docs/reference/developer-onboarding.md +0 -89
  113. package/docs/reference/enforcement.md +0 -35
  114. package/docs/reference/harness-matrix.md +0 -41
  115. package/docs/reference/skill-anatomy.md +0 -71
  116. package/docs/troubleshooting.md +0 -91
  117. package/evals/cases/_no-skill.json +0 -16
  118. package/evals/cases/adversarial-pressure-fake-pass.json +0 -25
  119. package/evals/cases/adversarial-pressure-skip-review.json +0 -25
  120. package/evals/cases/lane-exploratory-vague.json +0 -24
  121. package/evals/cases/lane-sensitivity-payment.json +0 -24
  122. package/evals/cases/positive-refactor-existing-tests.json +0 -25
  123. package/evals/cases/positive-resume-mid-mission.json +0 -24
  124. package/evals/cases/routing-agent-security.json +0 -25
  125. package/evals/cases/routing-auth-feature.json +0 -26
  126. package/evals/cases/routing-backend.json +0 -25
  127. package/evals/cases/routing-bug-one-file.json +0 -26
  128. package/evals/cases/routing-claim-audit.json +0 -25
  129. package/evals/cases/routing-context-budget.json +0 -25
  130. package/evals/cases/routing-contract-first.json +0 -25
  131. package/evals/cases/routing-execution.json +0 -25
  132. package/evals/cases/routing-frontend.json +0 -26
  133. package/evals/cases/routing-gates.json +0 -25
  134. package/evals/cases/routing-git.json +0 -25
  135. package/evals/cases/routing-healing.json +0 -25
  136. package/evals/cases/routing-lessons.json +0 -25
  137. package/evals/cases/routing-orchestration.json +0 -25
  138. package/evals/cases/routing-planning.json +0 -26
  139. package/evals/cases/routing-pr.json +0 -25
  140. package/evals/cases/routing-quality.json +0 -25
  141. package/evals/cases/routing-ship.json +0 -26
  142. package/evals/cases/routing-sunset.json +0 -25
  143. package/evals/cases/routing-using-mugiwara.json +0 -25
  144. package/evals/cases/routing-workflow.json +0 -25
  145. package/evals/floor.json +0 -6
@@ -8,21 +8,30 @@ die() { echo "savepoint: $*" >&2; exit 1; }
8
8
  MUGIWARA_DIR="${MUGIWARA_DIR:-.mugiwara}"
9
9
 
10
10
  # --- parse mission args ---
11
- MISSION="${1:-${STATE_MISSION:-}}"
12
- ACTOR="${2:-${STATE_ACTOR:-${GIT_AUTHOR_NAME:-${USER:-}}}}"
13
- BRANCH="${3:-$(git branch --show-current 2>/dev/null || echo 'unknown')}"
14
- WAVE="${4:-${STATE_WAVE:-1}}"
15
- MODE="${5:-${STATE_MODE:-guided}}"
16
-
17
- # --branch flag for per-branch state
11
+ # --branch flag must parse FIRST — it shifts positionals
18
12
  BRANCH_MODE=0
19
13
  if [ "${1:-}" = "--branch" ]; then
20
14
  BRANCH_MODE=1
21
- MISSION="${2:-${STATE_MISSION:-}}"
15
+ shift
16
+ MISSION="${1:-${STATE_MISSION:-}}"
17
+ ACTOR="${2:-${STATE_ACTOR:-${GIT_AUTHOR_NAME:-${USER:-}}}}"
18
+ BRANCH="${3:-$(git branch --show-current 2>/dev/null || echo 'unknown')}"
19
+ WAVE="${4:-${STATE_WAVE:-1}}"
20
+ MODE="${5:-${STATE_MODE:-guided}}"
21
+ else
22
+ MISSION="${1:-${STATE_MISSION:-}}"
23
+ ACTOR="${2:-${STATE_ACTOR:-${GIT_AUTHOR_NAME:-${USER:-}}}}"
22
24
  BRANCH="${3:-$(git branch --show-current 2>/dev/null || echo 'unknown')}"
23
- shift 2 2>/dev/null || true
25
+ WAVE="${4:-${STATE_WAVE:-1}}"
26
+ MODE="${5:-${STATE_MODE:-guided}}"
24
27
  fi
25
28
 
29
+ # mission allowlist — MISSION feeds paths + sed + node argv; traversal or
30
+ # sed metacharacters must not reach filesystem operations
31
+ case "$MISSION" in
32
+ ""|*[!a-zA-Z0-9._-]*) die "invalid mission name \"$MISSION\" (allowlist: [a-zA-Z0-9._-])" ;;
33
+ esac
34
+
26
35
  # per-branch state file when --branch used
27
36
  BRANCH_SLUG=$(echo "$BRANCH" | tr '/' '-')
28
37
  if [ "$BRANCH_MODE" -eq 1 ]; then
@@ -69,6 +78,19 @@ else
69
78
  LANE_REASON="$FILES_TOUCHED file(s) under 20 LOC"
70
79
  fi
71
80
 
81
+ # path-weighted sizing (mirrors lane.sh): docs-only changes outside the product
82
+ # surface never escalate to full from file count alone; sensitive-path
83
+ # escalation above still wins.
84
+ PRODUCT_PAT="^content/|^src/|^scripts/|^test/|^hooks/|^\.opencode/|^\.claude/|^evals/"
85
+ if [ "$LANE" = "full" ] && [ -n "$CHANGED_FILES" ]; then
86
+ CODE_COUNT=$(echo "$CHANGED_FILES" | grep -E "$PRODUCT_PAT" 2>/dev/null | grep -c . || true)
87
+ if [ -z "$CODE_COUNT" ] || [ "$CODE_COUNT" -eq 0 ] 2>/dev/null; then
88
+ PREV="$LANE"
89
+ LANE="standard"
90
+ LANE_REASON="$FILES_TOUCHED files, docs-only — path-weighted down from $PREV"
91
+ fi
92
+ fi
93
+
72
94
  # task counts from plan doc
73
95
  PLAN_FILE=$(ls "$MUGIWARA_DIR/plans/${MISSION}.md" 2>/dev/null || true)
74
96
  TASKS_DONE=0
@@ -85,22 +107,29 @@ if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
85
107
  BLOCKERS_OPEN=$(grep -c '|' "$BLOCKERS_FILE" 2>/dev/null || echo 0)
86
108
  fi
87
109
 
88
- # heal cycle
110
+ # heal cycle — count WAVE-8 banner occurrences in the trace, not the word
111
+ # "heal" anywhere (heal workers/healing text would inflate the counter and
112
+ # cause a premature halt)
89
113
  HEAL_CYCLE=1
90
- TRACE_FILE=$(ls "$MUGIWARA_DIR/results/${MISSION}-trace.md" 2>/dev/null || true)
114
+ TRACE_FILE=$(ls "$MUGIWARA_DIR/results/${MISSION}/"*trace*.md 2>/dev/null | head -1 || true)
91
115
  if [ -n "$TRACE_FILE" ] && [ -f "$TRACE_FILE" ]; then
92
- HEAL_COUNT=$(grep -ci 'Wave 8\|wave 8\|heal' "$TRACE_FILE" 2>/dev/null || echo 0)
116
+ HEAL_COUNT=$(grep -ci '^.*Wave 8.*\|wave 8' "$TRACE_FILE" 2>/dev/null || echo 0)
93
117
  HEAL_CYCLE=$((HEAL_COUNT + 1))
94
118
  fi
95
119
 
96
- # evidence paths
97
- EVIDENCE=$(ls "$MUGIWARA_DIR/results/" 2>/dev/null | grep "$MISSION" | sed 's|^|.mugiwara/results/|' | tr '\n' ',' | sed 's/,$//' || true)
120
+ # evidence paths — per-mission results folder (quoted printf, no sed — mission
121
+ # name is allowlisted above, but avoid sed metacharacter semantics entirely)
122
+ EVIDENCE=""
123
+ for evf in "$MUGIWARA_DIR/results/${MISSION}/"*.md; do
124
+ [ -f "$evf" ] || continue
125
+ EVIDENCE="${EVIDENCE}${EVIDENCE:+,}.mugiwara/results/${MISSION}/$(basename "$evf")"
126
+ done
98
127
 
99
- # skill version from package.json
128
+ # skill version from package.json (argv-passing — never interpolate paths into node -e)
100
129
  SKILL_VERSION="1"
101
130
  if [ -f package.json ]; then
102
131
  PKG_JSON="$MUGIWARA_DIR/../package.json"
103
- [ -f "$PKG_JSON" ] && SKILL_VERSION=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('$PKG_JSON','utf8')).version.split('.')[0])}catch(e){console.log('1')}" 2>/dev/null || echo "1")
132
+ [ -f "$PKG_JSON" ] && SKILL_VERSION=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version.split('.')[0])}catch(e){console.log('1')}" "$PKG_JSON" 2>/dev/null || echo "1")
104
133
  fi
105
134
 
106
135
  # tokens from env var (harness exports estimated tokens consumed)
@@ -126,41 +155,59 @@ elif [ "$BUDGET" -gt 0 ] && [ "$TOKENS_EST" -ge "$WARN_AT" ] 2>/dev/null; then
126
155
  STATUS="warn"
127
156
  fi
128
157
 
129
- SENSITIVE_ARR=""
130
- if [ -n "$SENSITIVE_PATHS" ]; then
131
- SENSITIVE_ARR=$(echo "$SENSITIVE_PATHS" | tr ',' '\n' | sed 's/^/"/;s/$/"/' | tr '\n' ',' | sed 's/,$//')
132
- fi
133
- EVIDENCE_ARR=""
134
- if [ -n "$EVIDENCE" ]; then
135
- EVIDENCE_ARR=$(echo "$EVIDENCE" | tr ',' '\n' | sed 's/^/"/;s/$/"/' | tr '\n' ',' | sed 's/,$//')
136
- fi
137
-
138
158
  mkdir -p "$MUGIWARA_DIR"
139
159
 
140
- cat > "$STATE_FILE" <<JSON
141
- {
142
- "mission": "$MISSION",
143
- "actor": "$ACTOR",
144
- "branch": "$BRANCH",
145
- "lane": "$LANE",
146
- "lane_reason": "$LANE_REASON",
147
- "wave": $WAVE,
148
- "mode": "$MODE",
149
- "base_sha": "$BASE_SHA",
150
- "head_sha": "$HEAD_SHA",
151
- "files_touched": $FILES_TOUCHED,
152
- "loc_delta": $LOC_DELTA,
153
- "sensitive_paths": [${SENSITIVE_ARR}],
154
- "tasks": { "done": $TASKS_DONE, "total": $TASKS_TOTAL },
155
- "blockers_open": $BLOCKERS_OPEN,
156
- "heal_cycle": $HEAL_CYCLE,
157
- "tokens_est": $TOKENS_EST,
158
- "budget": $BUDGET,
159
- "budget_status": "$STATUS",
160
- "skill_version": "$SKILL_VERSION",
161
- "evidence": [${EVIDENCE_ARR}],
162
- "updated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
163
- }
164
- JSON
160
+ # --- lane-rise compare (F9): read previous state, flag escalation ---
161
+ LANE_PREV=""
162
+ LANE_ROSE=false
163
+ if [ -f "$STATE_FILE" ]; then
164
+ LANE_PREV=$(node -e "try{const s=require(process.argv[1]);process.stdout.write(s.lane||'')}catch(e){process.stdout.write('')}" "$STATE_FILE" 2>/dev/null || true)
165
+ if [ -n "$LANE_PREV" ] && [ "$LANE_PREV" != "$LANE" ]; then
166
+ # lane order: direct < lean < standard < full < spike (spike resizes, not a rise)
167
+ case "$LANE_PREV:$LANE" in
168
+ direct:lean|direct:standard|direct:full|lean:standard|lean:full|standard:full) LANE_ROSE=true ;;
169
+ *) LANE_ROSE=false ;;
170
+ esac
171
+ fi
172
+ fi
165
173
 
174
+ node -e "
175
+ const data = {
176
+ mission: process.argv[1],
177
+ actor: process.argv[2],
178
+ branch: process.argv[3],
179
+ lane: process.argv[4],
180
+ lane_reason: process.argv[5],
181
+ lane_prev: process.argv[24] || null,
182
+ lane_rose: process.argv[25] === 'true',
183
+ wave: parseInt(process.argv[6], 10),
184
+ mode: process.argv[7],
185
+ base_sha: process.argv[8],
186
+ head_sha: process.argv[9],
187
+ files_touched: parseInt(process.argv[10], 10),
188
+ loc_delta: parseInt(process.argv[11], 10),
189
+ sensitive_paths: process.argv[12] ? process.argv[12].split(',').filter(Boolean) : [],
190
+ tasks: { done: parseInt(process.argv[13], 10), total: parseInt(process.argv[14], 10) },
191
+ blockers_open: parseInt(process.argv[15], 10),
192
+ heal_cycle: parseInt(process.argv[16], 10),
193
+ tokens_est: parseInt(process.argv[17], 10) || 0,
194
+ budget: parseInt(process.argv[18], 10) || 0,
195
+ budget_status: process.argv[19],
196
+ skill_version: process.argv[20],
197
+ evidence: process.argv[21] ? process.argv[21].split(',').filter(Boolean) : [],
198
+ updated_at: process.argv[22]
199
+ };
200
+ require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\n');
201
+ " \
202
+ "$MISSION" "$ACTOR" "$BRANCH" "$LANE" "$LANE_REASON" \
203
+ "$WAVE" "$MODE" "$BASE_SHA" "$HEAD_SHA" "$FILES_TOUCHED" \
204
+ "$LOC_DELTA" "$SENSITIVE_PATHS" "$TASKS_DONE" "$TASKS_TOTAL" \
205
+ "$BLOCKERS_OPEN" "$HEAL_CYCLE" "$TOKENS_EST" "$BUDGET" \
206
+ "$STATUS" "$SKILL_VERSION" "$EVIDENCE" \
207
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
208
+ "$STATE_FILE" "$LANE_PREV" "$LANE_ROSE"
209
+
210
+ if [ "$LANE_ROSE" = true ]; then
211
+ echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
212
+ fi
166
213
  echo "✓ savepoint written: $STATE_FILE (lane=$LANE, wave=$WAVE, files=$FILES_TOUCHED)"
@@ -27,6 +27,30 @@ function checkFile(file: string, wantName: string, kind: 'skill' | 'agent'): Rec
27
27
  if (bullets.length === 0) errors.push(`skill ${file}: "## Skip when" needs ≥1 bullet`);
28
28
  if (bullets.length > 4) errors.push(`skill ${file}: "## Skip when" block exceeds 4 bullets`);
29
29
  }
30
+ if (kind === 'skill') {
31
+ const lines = body.split(/\r?\n/);
32
+ const headingRe = /^## /;
33
+ const sections: { heading: string; start: number; end: number }[] = [];
34
+ for (let i = 0; i < lines.length; i++) {
35
+ if (headingRe.test(lines[i])) sections.push({ heading: lines[i].replace(/^## /, ''), start: i, end: 0 });
36
+ }
37
+ for (let si = 0; si < sections.length; si++) {
38
+ sections[si].end = si + 1 < sections.length ? sections[si + 1].start : lines.length;
39
+ // count non-empty, non-table, non-code-fence content lines
40
+ let inFence = false;
41
+ let contentLines = 0;
42
+ for (let li = sections[si].start; li < sections[si].end; li++) {
43
+ const l = lines[li].trim();
44
+ if (!l) continue;
45
+ if (l.startsWith('```')) { inFence = !inFence; continue; }
46
+ if (inFence) continue;
47
+ if (l.startsWith('|') || l.startsWith('- |')) continue;
48
+ contentLines++;
49
+ }
50
+ if (contentLines >= 20) errors.push(`skill ${file}: section "${sections[si].heading}" is ${contentLines} content lines (≥20) — move to references/`);
51
+ else if (contentLines >= 15) console.warn(`⚠ skill ${file}: section "${sections[si].heading}" is ${contentLines} content lines (≥15, consider moving to references/)`);
52
+ }
53
+ }
30
54
  return data;
31
55
  }
32
56
 
@@ -110,6 +134,59 @@ for (const dir of skillDirs) {
110
134
  if (dir !== 'mugiwara-workflow' && !usedSkills.has(dir)) errors.push(`skill ${dir}: not referenced by any agent`);
111
135
  }
112
136
 
137
+ // --- write-scope gate (F2): every agent declares a scope; only executors may write source ---
138
+ const SOURCE_SCOPED = new Set(['zoro-execution', 'brook-healing']);
139
+ for (const f of agentFiles) {
140
+ const name = f.replace(/\.md$/, '');
141
+ const parsed = parseFrontmatter(readFileSync(join(agentDir, f), 'utf8'));
142
+ const scope = parsed.data['write-scope'];
143
+ if (!scope) { errors.push(`agent ${f}: missing write-scope (artifacts | source)`); continue; }
144
+ if (scope !== 'artifacts' && scope !== 'source') errors.push(`agent ${f}: write-scope must be artifacts | source (got "${scope}")`);
145
+ if (scope === 'source' && !SOURCE_SCOPED.has(name)) errors.push(`agent ${f}: only zoro-execution and brook-healing may declare write-scope: source`);
146
+ }
147
+
148
+ // --- hub-rule gate (F3): every non-Luffy agent carries both hub sections ---
149
+ for (const f of agentFiles) {
150
+ const name = f.replace(/\.md$/, '');
151
+ if (name === 'luffy-orchestrator') continue;
152
+ const text = readFileSync(join(agentDir, f), 'utf8');
153
+ if (!text.includes('## Before you start')) errors.push(`agent ${f}: missing "## Before you start" entry protocol`);
154
+ if (!text.includes('## Return to Luffy')) errors.push(`agent ${f}: missing "## Return to Luffy" hub rule`);
155
+ }
156
+
157
+ // --- hub-skill gate (F3): every agent lists mugiwara-orchestration (the hub rule's home) ---
158
+ for (const f of agentFiles) {
159
+ const parsed = parseFrontmatter(readFileSync(join(agentDir, f), 'utf8'));
160
+ const skills = (parsed.data.skills ?? '').split(',').map(s => s.trim());
161
+ if (!skills.includes('mugiwara-orchestration')) errors.push(`agent ${f}: missing mugiwara-orchestration in skills (hub rule lives there)`);
162
+ }
163
+
164
+ // --- handoff-target gate (F4): a body naming another crew member as a handoff
165
+ // target without Luffy in the same line fails CI ---
166
+ const HANDOFF_CREW = ['usopp', 'nami', 'zoro', 'chopper', 'sanji', 'franky', 'robin', 'jinbe', 'brook', 'skeptic', 'memory-keeper', 'resume-coordinator', 'eval-runner'];
167
+ // Verb class covers direct handoff language plus synonyms (route/send/pass/
168
+ // escalate/delegate/transfer) so the F4 gate does not leak through paraphrase.
169
+ const handoffRe = new RegExp(`\\b(hand to|hand off to|dispatch|route to|send to|pass to|escalate to|delegate to|transfer to)\\s+(?:a\\s+|another\\s+|the\\s+)?(?:crew members?|${HANDOFF_CREW.join('|')})`, 'i');
170
+ // Negation must DIRECTLY precede the handoff verb, not sit anywhere in the
171
+ // line ("do not hand off" later in the same line is a different clause).
172
+ const NEGATION_VERB = /\b(never|not|no|don't|do not)\s+(hand|dispatch|route|send|pass|escalate|delegate|transfer)\b/i;
173
+ for (const f of [...agentFiles, ...skillDirs.map(d => join(root, 'skills', d, 'SKILL.md'))]) {
174
+ if (!existsSync(f)) continue;
175
+ const text = readFileSync(f, 'utf8');
176
+ const lines = text.split(/\r?\n/);
177
+ for (let i = 0; i < lines.length; i++) {
178
+ const line = lines[i];
179
+ const m = handoffRe.exec(line);
180
+ if (!m) continue;
181
+ // clause boundary: only the clause containing the handoff verb counts
182
+ const clauseStart = line.lastIndexOf('.', m.index) + 1;
183
+ const clause = line.slice(clauseStart, m.index + m[0].length);
184
+ if (NEGATION_VERB.test(clause)) continue; // "never dispatch X" — constraint
185
+ if (/\bLuffy\b|to Luffy\b/.test(line)) continue; // hub named in same line ("main thread" is NOT the hub — Luffy is)
186
+ errors.push(`handoff leak ${f.replace(root + '/', '')}:${i + 1}: "${line.trim()}" — handoff target must be Luffy or name Luffy in the same line`);
187
+ }
188
+ }
189
+
113
190
  // --- manifest-sync check: .claude-plugin/plugin.json must set-equal content/ ---
114
191
  const manifestArg = process.argv.indexOf('--check-manifest');
115
192
  if (manifestArg !== -1) {
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bun
2
+ // scripts/verify-install.ts — G1: verify all references/*.md pointers resolve after install.
3
+ // Installs to temp dir for one target per tier, then checks every pointer.
4
+
5
+ import { existsSync, mkdtempSync, rmSync, readFileSync, readdirSync } from 'node:fs';
6
+ import { join, dirname } from 'node:path';
7
+ import { tmpdir } from 'node:os';
8
+ import { installTo } from '../src/installer.ts';
9
+ import { targets } from '../src/targets/index.ts';
10
+
11
+ const tierTargets = [
12
+ { id: 'claude', tier: 1 },
13
+ { id: 'codex', tier: 2 },
14
+ { id: 'kilo', tier: 3 },
15
+ ];
16
+
17
+ let total = 0;
18
+ let broken = 0;
19
+
20
+ for (const tt of tierTargets) {
21
+ const target = targets[tt.id];
22
+ if (!target) { console.log(`⚠ target ${tt.id} not found, skipping`); continue; }
23
+ const dir = mkdtempSync(join(tmpdir(), `mugi-verify-${tt.id}-`));
24
+ try {
25
+ installTo(target, { scope: 'project', projectDir: dir, dryRun: false, force: true });
26
+
27
+ const skillsDir = target.paths({ scope: 'project', projectDir: dir, home: '' }).skillsDir;
28
+ const mugiwaraRefsDir = join(dir, '.mugiwara', 'refs');
29
+
30
+ function findSkillFiles(root: string): string[] {
31
+ const out: string[] = [];
32
+ if (!existsSync(root)) return out;
33
+ for (const ent of readdirSync(root, { withFileTypes: true })) {
34
+ const p = join(root, ent.name);
35
+ if (ent.isDirectory()) out.push(...findSkillFiles(p));
36
+ else if (ent.name.endsWith('.md')) out.push(p);
37
+ }
38
+ return out;
39
+ }
40
+
41
+ const skillFiles = findSkillFiles(skillsDir)
42
+ .filter(f => !f.replace(skillsDir, '').includes('/references/'));
43
+
44
+ for (const file of skillFiles) {
45
+ const body = readFileSync(file, 'utf8');
46
+ const refs = [...body.matchAll(/`([^`]*references\/[^`]+\.md)`/g)];
47
+
48
+ for (const m of refs) {
49
+ total++;
50
+ const pointer = m[1];
51
+ const localResolve = join(dirname(file), pointer);
52
+ const mugiwaraResolve = join(mugiwaraRefsDir, pointer.replace(/^(?:_shared\/)?references\//, ''));
53
+ const resolved = [localResolve, mugiwaraResolve].find(p => existsSync(p));
54
+
55
+ if (!resolved) {
56
+ broken++;
57
+ const relFile = file.replace(dir + '/', '');
58
+ console.log(`✗ ${relFile}: \`${pointer}\` → not found`);
59
+ }
60
+ }
61
+ }
62
+ } finally {
63
+ rmSync(dir, { recursive: true, force: true });
64
+ }
65
+ }
66
+
67
+ if (broken > 0) {
68
+ console.log(`\n✗ ${broken}/${total} pointers cannot resolve after install`);
69
+ process.exit(1);
70
+ }
71
+
72
+ console.log(`✓ ${total} pointers checked across ${tierTargets.length} targets — all resolve`);
package/src/installer.ts CHANGED
@@ -11,6 +11,7 @@ export type ContentItem = {
11
11
  data: FrontmatterData;
12
12
  body: string;
13
13
  refs: { relPath: string; text: string }[];
14
+ internal?: boolean;
14
15
  };
15
16
 
16
17
  export type InstallOptions = {
@@ -35,6 +36,7 @@ export interface Target {
35
36
  label: string;
36
37
  native: boolean;
37
38
  tier?: 1 | 2 | 3;
39
+ refPointerPrefix?: string;
38
40
  paths(opts: { scope: Scope; projectDir: string; home: string }): { skillsDir: string; agentsDir: string };
39
41
  transformSkill(data: FrontmatterData, body: string): TransformOut;
40
42
  transformAgent(data: FrontmatterData, body: string): TransformOut;
@@ -48,7 +50,7 @@ export const CONTENT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '
48
50
  const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8')) as { version: string };
49
51
  export const VERSION = pkg.version;
50
52
 
51
- export function collectContent(): { skills: ContentItem[]; agents: ContentItem[] } {
53
+ export function collectContent(): { skills: ContentItem[]; agents: ContentItem[]; sharedRefs: { relPath: string; text: string }[] } {
52
54
  const skillNames = readdirSync(join(CONTENT_DIR, 'skills'), { withFileTypes: true })
53
55
  .filter(e => e.isDirectory()).map(e => e.name);
54
56
  const skills = skillNames.map(name => {
@@ -59,9 +61,17 @@ export function collectContent(): { skills: ContentItem[]; agents: ContentItem[]
59
61
  .filter(f => f.endsWith('.md'))
60
62
  .map(f => {
61
63
  const { data, body } = parseFrontmatter(readFileSync(join(CONTENT_DIR, 'agents', f), 'utf8'));
62
- return { name: f.replace(/\.md$/, ''), data, body, refs: [] as { relPath: string; text: string }[] };
64
+ return { name: f.replace(/\.md$/, ''), data, body, refs: [] as { relPath: string; text: string }[], internal: data.internal === 'true' };
63
65
  });
64
- return { skills, agents };
66
+ const sharedRefsDir = join(dirname(CONTENT_DIR), 'references');
67
+ const sharedRefs: { relPath: string; text: string }[] = [];
68
+ if (existsSync(sharedRefsDir)) {
69
+ for (const f of readdirSync(sharedRefsDir)) {
70
+ if (!f.endsWith('.md')) continue;
71
+ sharedRefs.push({ relPath: f, text: readFileSync(join(sharedRefsDir, f), 'utf8') });
72
+ }
73
+ }
74
+ return { skills, agents, sharedRefs };
65
75
  }
66
76
 
67
77
  function collectRefs(skillDir: string): { relPath: string; text: string }[] {
@@ -74,7 +84,7 @@ function collectRefs(skillDir: string): { relPath: string; text: string }[] {
74
84
  export function installTo(target: Target, opts: InstallOptions): InstallResult {
75
85
  const { scope, projectDir, dryRun = false, force = false } = opts;
76
86
  const home = opts.home ?? homedir();
77
- const { skills, agents } = collectContent();
87
+ const { skills, agents, sharedRefs } = collectContent();
78
88
  const dirs = target.paths({ scope, projectDir, home });
79
89
  const backupRoot = join(scope === 'global' ? home : projectDir, '.mugiwara');
80
90
  const result: InstallResult = { written: [], skipped: [], backedUp: [], notes: [] };
@@ -100,10 +110,22 @@ export function installTo(target: Target, opts: InstallOptions): InstallResult {
100
110
 
101
111
  for (const s of skills) {
102
112
  const out = target.transformSkill(s.data, s.body);
103
- if (out) writeOne(join(dirs.skillsDir, out.relPath), out.text);
113
+ if (out) {
114
+ let text = out.text;
115
+ if (target.refPointerPrefix !== undefined && target.refPointerPrefix !== '') {
116
+ text = text.replace(/`_shared\/references\//g, '`' + target.refPointerPrefix + '_shared/references/');
117
+ }
118
+ writeOne(join(dirs.skillsDir, out.relPath), text);
119
+ }
104
120
  if (target.transformSkillFull) {
105
121
  const full = target.transformSkillFull(s.data, s.body);
106
- if (full && target.refsDir) writeOne(join(target.refsDir({ scope, projectDir, home }, s.name), full.relPath), full.text);
122
+ if (full && target.refsDir) {
123
+ let text = full.text;
124
+ if (target.refPointerPrefix !== undefined && target.refPointerPrefix !== '') {
125
+ text = text.replace(/`_shared\/references\//g, '`' + target.refPointerPrefix + '_shared/references/');
126
+ }
127
+ writeOne(join(target.refsDir({ scope, projectDir, home }, s.name), full.relPath), text);
128
+ }
107
129
  }
108
130
  if (s.refs.length && target.refsDir) {
109
131
  const refsRoot = target.refsDir({ scope, projectDir, home }, s.name);
@@ -111,7 +133,7 @@ export function installTo(target: Target, opts: InstallOptions): InstallResult {
111
133
  }
112
134
  }
113
135
  for (const a of agents) {
114
- const out = target.transformAgent(a.data, a.body);
136
+ const out = target.transformAgent({ ...a.data, ...(a.internal ? { 'internal-agent': 'true' } : {}) }, a.body);
115
137
  if (out) writeOne(join(dirs.agentsDir, out.relPath), out.text);
116
138
  if (target.transformAgentFull) {
117
139
  const full = target.transformAgentFull(a.data, a.body);
@@ -119,6 +141,11 @@ export function installTo(target: Target, opts: InstallOptions): InstallResult {
119
141
  }
120
142
  }
121
143
 
144
+ if (sharedRefs.length) {
145
+ const sharedRoot = join(dirs.skillsDir, '_shared', 'references');
146
+ for (const r of sharedRefs) writeOne(join(sharedRoot, r.relPath), r.text);
147
+ }
148
+
122
149
  if (target.postInstall) {
123
150
  const post = target.postInstall({ scope, projectDir, home, dryRun, files: result.written });
124
151
  result.written.push(...post.written);
package/src/mission.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/mission.ts
2
2
  // Mission-state helpers for the mugiwara CLI (installer + reset only).
3
- import { existsSync, rmSync, readFileSync } from 'node:fs';
3
+ import { existsSync, rmSync, readFileSync, readdirSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
 
6
6
  function activeActor(projectDir: string): string | null {
@@ -30,10 +30,12 @@ export function resetMission(projectDir: string, keepLogs: boolean, force?: bool
30
30
  const p = join(root, dir);
31
31
  if (existsSync(p)) { rmSync(p, { recursive: true, force: true }); removed.push(dir); }
32
32
  }
33
- // mission state files
34
- for (const f of ['state.json']) {
35
- const p = join(root, f);
36
- if (existsSync(p)) { rmSync(p); removed.push(f); }
33
+ // mission state files — state.json + branch-specific state-*.json
34
+ for (const f of readdirSync(root)) {
35
+ if (/^state(-.+)?\.json$/.test(f)) {
36
+ const p = join(root, f);
37
+ if (existsSync(p)) { rmSync(p); removed.push(f); }
38
+ }
37
39
  }
38
40
  if (!keepLogs) {
39
41
  const p = join(root, 'logs');
@@ -1,5 +1,5 @@
1
1
  // src/targets/claude.ts
2
- import { existsSync, readFileSync, mkdirSync, copyFileSync } from 'node:fs';
2
+ import { existsSync, readFileSync, mkdirSync, copyFileSync, chmodSync, readdirSync } 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,11 +7,13 @@ import type { Target } from '../installer.ts';
7
7
 
8
8
  const here = dirname(fileURLToPath(import.meta.url));
9
9
  const HOOK_SRC = join(here, '..', '..', 'hooks', 'session-start.ts');
10
+ const COMMANDS_SRC = join(here, '..', '..', '.claude', 'commands');
10
11
 
11
12
  export const target: Target = {
12
13
  id: 'claude',
13
14
  label: 'Claude Code',
14
15
  native: true,
16
+ refPointerPrefix: '../',
15
17
  paths({ scope, projectDir, home }) {
16
18
  const root = scope === 'global' ? join(home, '.claude') : join(projectDir, '.claude');
17
19
  return { skillsDir: join(root, 'skills'), agentsDir: join(root, 'agents') };
@@ -35,12 +37,27 @@ export const target: Target = {
35
37
  // Wire the SessionStart hook (inline doctrine) into the installed .claude dir.
36
38
  const root = scope === 'global' ? join(home, '.claude') : join(projectDir, '.claude');
37
39
  const hookFile = join(root, 'hooks', 'session-start.ts');
40
+ const written: string[] = [];
41
+ const notes: string[] = [];
38
42
  if (dryRun) return { written: [], notes: [] };
39
43
  if (existsSync(HOOK_SRC) && !existsSync(hookFile)) {
40
44
  mkdirSync(dirname(hookFile), { recursive: true });
41
45
  copyFileSync(HOOK_SRC, hookFile);
42
- return { written: [hookFile], notes: [] };
46
+ chmodSync(hookFile, 0o755);
47
+ written.push(hookFile);
43
48
  }
44
- return { written: [], notes: [] };
49
+ // Port the /mugiwara commands into the installed .claude dir.
50
+ if (existsSync(COMMANDS_SRC)) {
51
+ const dstDir = join(root, 'commands');
52
+ mkdirSync(dstDir, { recursive: true });
53
+ for (const f of readdirSync(COMMANDS_SRC)) {
54
+ if (!f.endsWith('.md')) continue;
55
+ const src = join(COMMANDS_SRC, f);
56
+ const dst = join(dstDir, f);
57
+ if (!existsSync(dst)) { copyFileSync(src, dst); written.push(dst); }
58
+ else notes.push(`existing command kept: ${dst}`);
59
+ }
60
+ }
61
+ return { written, notes };
45
62
  },
46
63
  };
@@ -44,7 +44,7 @@ export function makeGeneric(opts: {
44
44
  if (stubOnly) {
45
45
  return {
46
46
  relPath: `agent-${data.name}.md`,
47
- text: `# Agent: ${data.name}\n\n> ${data.description}\n\nSkills: ${data.skills ?? ''}. Read \`.mugiwara/refs/${data.name}.md\` when embodying this role.`,
47
+ text: `# Agent: ${data.name}\n\n> ${data.description}\n\nSkills: ${data.skills ?? ''}. Read \`.mugiwara/refs/${data.name}.md\` when embodying this role.\n\nOnly zoro-execution and brook-healing may modify source code.\nReturn your output to luffy-orchestrator; do not choose the next step.`,
48
48
  };
49
49
  }
50
50
  return { relPath: `agent-${data.name}.md`, text: `# Agent: ${data.name}\n\n> ${data.description}\n\nSkills used: ${data.skills ?? ''}\n\n${body}` };
@@ -20,26 +20,45 @@ const CREW: Record<string, CrewConfig> = {
20
20
  'usopp-brainstorm': { color: '#f59e0b', temperature: 0.6, steps: 15 },
21
21
  'nami-planner': { color: '#f97316', temperature: 0.2, steps: 15 },
22
22
  'zoro-execution': { color: '#22c55e', temperature: 0.1, steps: 30 },
23
- 'chopper-checkpoint': { color: '#3b82f6', temperature: 0.1, permission: { edit: 'deny' }, steps: 15 },
24
- 'sanji-quality': { color: '#a855f7', temperature: 0.1, permission: { edit: 'deny' }, steps: 10 },
25
- 'franky-gates': { color: '#06b6d4', temperature: 0.1, permission: { edit: 'deny' }, steps: 10 },
26
- 'robin-reviewer': { color: '#8b5cf6', temperature: 0.2, permission: { edit: 'deny' }, steps: 15 },
27
- 'jinbe-security': { color: '#6366f1', temperature: 0.2, permission: { edit: 'deny' }, steps: 15 },
23
+ 'chopper-checkpoint': { color: '#3b82f6', temperature: 0.1, steps: 15 },
24
+ 'sanji-quality': { color: '#a855f7', temperature: 0.1, steps: 10 },
25
+ 'franky-gates': { color: '#06b6d4', temperature: 0.1, steps: 10 },
26
+ 'robin-reviewer': { color: '#8b5cf6', temperature: 0.2, steps: 15 },
27
+ 'jinbe-security': { color: '#6366f1', temperature: 0.2, steps: 15 },
28
28
  'brook-healing': { color: '#ec4899', temperature: 0.1, steps: 20 },
29
- 'skeptic-verifier': { color: '#64748b', temperature: 0.1, permission: { edit: 'deny' }, steps: 12 },
29
+ 'skeptic-verifier': { color: '#64748b', temperature: 0.1, steps: 12 },
30
30
  'eval-runner': { color: '#14b8a6', temperature: 0.2, steps: 15 },
31
31
  'resume-coordinator': { color: '#d97706', temperature: 0.2, steps: 10 },
32
32
  'memory-keeper': { color: '#d946ef', temperature: 0.2, steps: 8 },
33
33
  };
34
34
 
35
- function agentFrontmatter(name: string, description: string) {
35
+ // write-scope is the single source of truth (content/agents/*.md frontmatter).
36
+ // The path boundary (artifacts vs source) IS expressible in opencode:
37
+ // permission.edit accepts glob/pattern -> action, last match wins. Artifacts
38
+ // agents get deny-all-edit except .mugiwara/**; source agents (zoro, brook)
39
+ // get full edit allow. Derived at install time from the frontmatter field.
40
+ function permissionFromScope(scope: string | undefined): Record<string, string | Record<string, string>> | undefined {
41
+ if (scope === 'source') return { edit: 'allow' };
42
+ if (scope === 'artifacts') return { edit: { '*': 'deny', '.mugiwara/**': 'allow' } };
43
+ return undefined;
44
+ }
45
+
46
+ function agentFrontmatter(name: string, description: string, writeScope?: string) {
36
47
  const crew = CREW[name];
37
48
  const lines = [`description: ${description}`, `mode: all`];
38
49
  if (crew) {
39
50
  lines.push(`color: '${crew.color}'`, `temperature: ${crew.temperature}`, `steps: ${crew.steps}`);
40
- if (crew.permission) {
51
+ const perm = permissionFromScope(writeScope);
52
+ if (perm) {
41
53
  lines.push('permission:');
42
- for (const [k, v] of Object.entries(crew.permission)) lines.push(` ${k}: ${v}`);
54
+ for (const [k, v] of Object.entries(perm)) {
55
+ if (typeof v === 'string') {
56
+ lines.push(` ${k}: ${v}`);
57
+ } else {
58
+ lines.push(` ${k}:`);
59
+ for (const [pk, pv] of Object.entries(v)) lines.push(` "${pk}": ${pv}`);
60
+ }
61
+ }
43
62
  }
44
63
  }
45
64
  return lines.join('\n');
@@ -49,6 +68,7 @@ export const target: Target = {
49
68
  id: 'opencode',
50
69
  label: 'opencode',
51
70
  native: true,
71
+ refPointerPrefix: '../',
52
72
  paths({ scope, projectDir, home }) {
53
73
  const root = scope === 'global' ? join(home, '.config', 'opencode') : join(projectDir, '.opencode');
54
74
  return { skillsDir: join(root, 'skills'), agentsDir: join(root, 'agents') };
@@ -60,7 +80,8 @@ export const target: Target = {
60
80
  };
61
81
  },
62
82
  transformAgent(data: FrontmatterData, body: string) {
63
- const fm = agentFrontmatter(data.name, data.description);
83
+ const desc = data['internal-agent'] === 'true' ? `[INTERNAL] ${data.description}` : data.description;
84
+ const fm = agentFrontmatter(data.name, desc, data['write-scope']);
64
85
  return { relPath: `${data.name}.md`, text: `---\n${fm}\n---\n${body}` };
65
86
  },
66
87
  refsDir({ scope, projectDir, home }, skillName: string) {