@ionivetech/mugiwara 0.5.4 → 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 (158) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +4 -3
  3. package/.codex-plugin/plugin.json +2 -2
  4. package/.cursor-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +2 -2
  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 +2 -2
  17. package/GEMINI.md +1 -1
  18. package/README.md +240 -217
  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 +19 -5
  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 +206 -87
  64. package/gemini-extension.json +1 -1
  65. package/hooks/session-start.ts +1 -1
  66. package/package.json +12 -5
  67. package/plugin.json +2 -2
  68. package/references/multi-actor.md +1 -1
  69. package/scripts/evidence.sh +13 -6
  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 +141 -76
  74. package/scripts/onboard.ts +292 -0
  75. package/scripts/release-notes.ts +42 -13
  76. package/scripts/retrieval-eval.ts +1 -2
  77. package/scripts/run-evals.ts +1 -1
  78. package/scripts/savepoint.sh +97 -50
  79. package/scripts/validate-content.ts +84 -7
  80. package/scripts/verify-install.ts +72 -0
  81. package/src/args.ts +1 -1
  82. package/src/cli.ts +49 -22
  83. package/src/frontmatter.ts +3 -3
  84. package/src/installer.ts +44 -15
  85. package/src/mission.ts +7 -5
  86. package/src/targets/claude.ts +20 -3
  87. package/src/targets/generic.ts +1 -1
  88. package/src/targets/opencode.ts +53 -11
  89. package/docs/adoption-guide.md +0 -72
  90. package/docs/agent-anatomy.md +0 -72
  91. package/docs/agents.md +0 -53
  92. package/docs/audit-trail.md +0 -65
  93. package/docs/claude-setup.md +0 -43
  94. package/docs/codex-setup.md +0 -26
  95. package/docs/comparison.md +0 -58
  96. package/docs/compliance-matrix.md +0 -81
  97. package/docs/config.md +0 -55
  98. package/docs/copilot-setup.md +0 -29
  99. package/docs/cost.md +0 -45
  100. package/docs/cursor-setup.md +0 -25
  101. package/docs/developer-onboarding.md +0 -89
  102. package/docs/enforcement.md +0 -35
  103. package/docs/execution-model.md +0 -92
  104. package/docs/gemini-setup.md +0 -27
  105. package/docs/getting-started.md +0 -158
  106. package/docs/git-strategy.md +0 -62
  107. package/docs/harness-matrix.md +0 -41
  108. package/docs/index.md +0 -56
  109. package/docs/install-antigravity.md +0 -45
  110. package/docs/install-claude.md +0 -75
  111. package/docs/install-cli.md +0 -103
  112. package/docs/install-codex.md +0 -44
  113. package/docs/install-copilot.md +0 -45
  114. package/docs/install-cursor.md +0 -45
  115. package/docs/install-gemini.md +0 -44
  116. package/docs/install-kimi.md +0 -45
  117. package/docs/install-opencode.md +0 -129
  118. package/docs/install-pi.md +0 -46
  119. package/docs/install.md +0 -53
  120. package/docs/lanes.md +0 -82
  121. package/docs/modes.md +0 -74
  122. package/docs/opencode-setup.md +0 -54
  123. package/docs/pr-summary.md +0 -54
  124. package/docs/rule-based-setup.md +0 -31
  125. package/docs/skill-anatomy.md +0 -71
  126. package/docs/skills.md +0 -55
  127. package/docs/troubleshooting.md +0 -91
  128. package/docs/windsurf-setup.md +0 -18
  129. package/docs/workflow.md +0 -89
  130. package/evals/cases/_no-skill.json +0 -16
  131. package/evals/cases/adversarial-pressure-fake-pass.json +0 -25
  132. package/evals/cases/adversarial-pressure-skip-review.json +0 -25
  133. package/evals/cases/lane-exploratory-vague.json +0 -24
  134. package/evals/cases/lane-sensitivity-payment.json +0 -24
  135. package/evals/cases/positive-refactor-existing-tests.json +0 -25
  136. package/evals/cases/positive-resume-mid-mission.json +0 -24
  137. package/evals/cases/routing-agent-security.json +0 -25
  138. package/evals/cases/routing-auth-feature.json +0 -26
  139. package/evals/cases/routing-backend.json +0 -25
  140. package/evals/cases/routing-bug-one-file.json +0 -26
  141. package/evals/cases/routing-claim-audit.json +0 -25
  142. package/evals/cases/routing-context-budget.json +0 -25
  143. package/evals/cases/routing-contract-first.json +0 -25
  144. package/evals/cases/routing-execution.json +0 -25
  145. package/evals/cases/routing-frontend.json +0 -26
  146. package/evals/cases/routing-gates.json +0 -25
  147. package/evals/cases/routing-git.json +0 -25
  148. package/evals/cases/routing-healing.json +0 -25
  149. package/evals/cases/routing-lessons.json +0 -25
  150. package/evals/cases/routing-orchestration.json +0 -25
  151. package/evals/cases/routing-planning.json +0 -26
  152. package/evals/cases/routing-pr.json +0 -25
  153. package/evals/cases/routing-quality.json +0 -25
  154. package/evals/cases/routing-ship.json +0 -26
  155. package/evals/cases/routing-sunset.json +0 -25
  156. package/evals/cases/routing-using-mugiwara.json +0 -25
  157. package/evals/cases/routing-workflow.json +0 -25
  158. 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=$(python3 -c "import json; v=json.load(open('$PKG_JSON')).get('version','1'); print(v.split('.')[0])" 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)"
@@ -14,7 +14,7 @@ function checkFile(file: string, wantName: string, kind: 'skill' | 'agent'): Rec
14
14
  const { data, body } = parsed;
15
15
  if (data.name !== wantName) errors.push(`${kind} ${file}: name "${data.name}" != "${wantName}"`);
16
16
  const d = data.description ?? '';
17
- if (kind === 'skill' && (d.length < 20 || d.length > 500)) errors.push(`skill ${file}: description must be 20-500 chars (got ${d.length})`);
17
+ if (kind === 'skill' && (d.length < 20 || d.length > 220)) errors.push(`skill ${file}: description must be 20-220 chars (got ${d.length})`);
18
18
  if (kind === 'agent' && d.length < 20) errors.push(`agent ${file}: description too short`);
19
19
  if (kind === 'skill' && body.replace(/\r?\n$/, '').split(/\r?\n/).length > 120) errors.push(`skill ${file}: body exceeds 120 lines`);
20
20
  if (kind === 'skill' && !body.includes('## Skip when')) errors.push(`skill ${file}: missing required "## Skip when" block (≤4 lines, numeric threshold)`);
@@ -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) {
@@ -188,20 +265,20 @@ if (totalDescChars > INDEX_BUDGET) {
188
265
  const docsArg = process.argv.indexOf('--check-docs');
189
266
  if (docsArg !== -1) {
190
267
  const docsDir = join(import.meta.dirname, '..', 'docs');
191
- const skillsDoc = join(docsDir, 'skills.md');
192
- const agentsDoc = join(docsDir, 'agents.md');
268
+ const skillsDoc = join(docsDir, 'concepts', 'skills.md');
269
+ const agentsDoc = join(docsDir, 'concepts', 'agents.md');
193
270
  let docErrors = 0;
194
271
 
195
272
  if (existsSync(skillsDoc)) {
196
273
  const content = readFileSync(skillsDoc, 'utf8');
197
274
  for (const dir of skillDirs) {
198
275
  if (!content.includes(dir)) {
199
- errors.push(`docs/skills.md: missing skill "${dir}"`);
276
+ errors.push(`docs/concepts/skills.md: missing skill "${dir}"`);
200
277
  docErrors++;
201
278
  }
202
279
  }
203
280
  } else {
204
- errors.push('docs/skills.md: file not found');
281
+ errors.push('docs/concepts/skills.md: file not found');
205
282
  docErrors++;
206
283
  }
207
284
 
@@ -210,12 +287,12 @@ if (docsArg !== -1) {
210
287
  for (const f of agentFiles) {
211
288
  const name = f.replace(/\.md$/, '');
212
289
  if (!content.includes(name)) {
213
- errors.push(`docs/agents.md: missing agent "${name}"`);
290
+ errors.push(`docs/concepts/agents.md: missing agent "${name}"`);
214
291
  docErrors++;
215
292
  }
216
293
  }
217
294
  } else {
218
- errors.push('docs/agents.md: file not found');
295
+ errors.push('docs/concepts/agents.md: file not found');
219
296
  docErrors++;
220
297
  }
221
298
 
@@ -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/args.ts CHANGED
@@ -9,7 +9,7 @@ export type Args = {
9
9
  const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target' };
10
10
  const BOOL_FLAGS: Record<string, string> = {
11
11
  '--global': 'global', '--yes': 'yes', '-y': 'yes', '--force': 'force',
12
- '--dry-run': 'dryRun', '--keep-logs': 'keepLogs',
12
+ '--dry-run': 'dryRun', '--keep-logs': 'keepLogs', '--check': 'check',
13
13
  '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version',
14
14
  };
15
15
 
package/src/cli.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // src/cli.ts
3
- import { existsSync, readFileSync, realpathSync, rmSync } from 'node:fs';
3
+ import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
4
4
  import { homedir } from 'node:os';
5
- import { resolve } from 'node:path';
6
- import { pathToFileURL } from 'node:url';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
7
  import { parseArgs, type FlagValue, type Args } from './args.ts';
8
8
  import { createRl, choose, multiChoose, confirm } from './prompt.ts';
9
9
  import { targets, TARGET_IDS } from './targets/index.ts';
@@ -43,12 +43,15 @@ function resetCmd(flags: Args['flags']): void {
43
43
 
44
44
  async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; projectDir: string; targetIds: string[] }> {
45
45
  const interactive = !flag(flags.yes);
46
+ if (interactive && !process.stdin.isTTY) {
47
+ throw new Error('Not a terminal. Run with --yes and --global or --project <dir> --target <ids|all>');
48
+ }
46
49
  const rl = interactive ? createRl() : null;
47
50
  try {
48
51
  let scope: Scope | null = flag(flags.global) ? 'global' : str(flags.project) ? 'project' : null;
49
52
  if (!scope) {
50
- if (!interactive) throw new Error('Specify --global or --project <dir> with --yes');
51
- scope = (await choose(rl!, 'Install scope?', ['global (user-wide)', 'project (this repo)'])) === 0 ? 'global' : 'project';
53
+ if (!interactive) { scope = 'project'; }
54
+ else scope = (await choose(rl!, 'Install scope?', ['global (user-wide)', 'project (this repo)'])) === 0 ? 'global' : 'project';
52
55
  }
53
56
  const projectDir = resolve(str(flags.project) ?? process.cwd());
54
57
  if (scope === 'project' && !existsSync(projectDir)) throw new Error(`Project dir not found: ${projectDir}`);
@@ -56,9 +59,11 @@ async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; pro
56
59
  let targetIds = str(flags.target)?.split(',').map(s => s.trim()) ?? null;
57
60
  if (targetIds && targetIds.includes('all')) targetIds = [...TARGET_IDS];
58
61
  if (!targetIds) {
59
- if (!interactive) throw new Error('Specify --target <ids|all> with --yes');
60
- const idx = await multiChoose(rl!, 'Target AI agents?', ['all targets', ...TARGET_IDS]);
61
- targetIds = idx.includes(0) ? [...TARGET_IDS] : idx.map(i => TARGET_IDS[i - 1]);
62
+ if (!interactive) { targetIds = [...TARGET_IDS]; }
63
+ else {
64
+ const idx = await multiChoose(rl!, 'Target AI agents?', ['all targets', ...TARGET_IDS]);
65
+ targetIds = idx.includes(0) ? [...TARGET_IDS] : idx.map(i => TARGET_IDS[i - 1]);
66
+ }
62
67
  }
63
68
  for (const id of targetIds) {
64
69
  if (!targets[id]) throw new Error(`Unknown target: ${id} (valid: ${TARGET_IDS.join(', ')}, all)`);
@@ -110,7 +115,17 @@ async function uninstall(flags: Args['flags']): Promise<void> {
110
115
  const home = homedir();
111
116
  const file = manifestPath({ scope, projectDir, home });
112
117
  const manifest = readManifest(file);
113
- if (!manifest) { console.log('Nothing installed (no manifest found).'); return; }
118
+ if (!manifest) {
119
+ if (scope === 'project') {
120
+ const globalFile = manifestPath({ scope: 'global', projectDir, home });
121
+ if (existsSync(globalFile)) {
122
+ console.log('No project manifest found, but a global install exists. Try:\n mugiwara uninstall --global');
123
+ return;
124
+ }
125
+ }
126
+ console.log('Nothing installed (no manifest found).');
127
+ return;
128
+ }
114
129
  console.log(`Will remove ${manifest.files.length} files (targets: ${manifest.targets.join(', ')}).`);
115
130
  if (!flag(flags.yes)) {
116
131
  const rl = createRl();
@@ -119,7 +134,20 @@ async function uninstall(flags: Args['flags']): Promise<void> {
119
134
  if (!ok) { console.log('Aborted.'); return; }
120
135
  }
121
136
  const removed = removeInstalled(manifest, { dryRun: flag(flags.dryRun) });
122
- if (!flag(flags.dryRun)) rmSync(file);
137
+ if (!flag(flags.dryRun)) {
138
+ rmSync(file);
139
+ const mugiDir = join(scope === 'global' ? home : projectDir, '.mugiwara');
140
+ if (existsSync(mugiDir) && readdirSync(mugiDir).length === 0) {
141
+ rmSync(mugiDir, { recursive: true, force: true });
142
+ }
143
+ if (manifest.targets.includes('opencode')) {
144
+ const opencodeCache = join(home, '.cache', 'opencode', 'packages', '@ionivetech');
145
+ if (existsSync(opencodeCache)) {
146
+ rmSync(opencodeCache, { recursive: true, force: true });
147
+ console.log(' cleared opencode npm cache (stale plugin versions)');
148
+ }
149
+ }
150
+ }
123
151
  console.log(`OK removed ${removed.length} files`);
124
152
  }
125
153
 
@@ -134,7 +162,12 @@ function list(flags: Args['flags']): void {
134
162
  const m = readManifest(file);
135
163
  if (!m) continue;
136
164
  found = true;
137
- console.log(`${label}: v${m.version} targets=${m.targets.join(',')} files=${m.files.length} installed=${m.installedAt}`);
165
+ if (flag(flags.check)) {
166
+ const missing = m.files.filter(f => !existsSync(f));
167
+ console.log(`${label}: v${m.version} targets=${m.targets.join(',')} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
168
+ } else {
169
+ console.log(`${label}: v${m.version} targets=${m.targets.join(',')} files=${m.files.length} installed=${m.installedAt}`);
170
+ }
138
171
  }
139
172
  if (!found) console.log('No mugiwara installation found.');
140
173
  }
@@ -143,10 +176,11 @@ function help(): void {
143
176
  console.log(`mugiwara ${VERSION} — the Straw Hat crew for AI agents
144
177
 
145
178
  Usage:
146
- mugiwara [install] install the crew (default; wizard when flags missing)
179
+ mugiwara [install] install the crew (wizard; with --yes uses project + all)
147
180
  mugiwara update replace existing files (backs up differences first)
148
181
  mugiwara uninstall remove installed files via manifest
149
182
  mugiwara list show installations
183
+ mugiwara list --check health check: show installations + missing files
150
184
  mugiwara reset wipe mission state (spec/plans/results/review/issues[/logs])
151
185
  mugiwara --help this help
152
186
  mugiwara --version print version
@@ -155,21 +189,14 @@ Flags:
155
189
  --global user-wide install
156
190
  --project <dir> project install (default: cwd)
157
191
  --target <ids|all> comma-separated: ${TARGET_IDS.join(', ')}
158
- --yes, -y non-interactive (needs --global/--project, --target)
192
+ --yes, -y non-interactive (defaults: project, all targets)
159
193
  --force overwrite differing files (with backup)
160
194
  --dry-run print actions without writing
195
+ --check with list: report missing files (health check)
161
196
  --keep-logs with reset: keep .mugiwara/logs (lessons ledger survives)`);
162
197
  }
163
198
 
164
- let entry = process.argv[1] !== undefined ? resolve(process.argv[1]) : undefined;
165
- if (entry !== undefined) {
166
- try {
167
- entry = realpathSync(entry);
168
- } catch {
169
- // ponytail: realpath can throw on a missing/odd path — fall back to the raw resolve
170
- }
171
- }
172
- const isMain = entry !== undefined && import.meta.url === pathToFileURL(entry).href;
199
+ const isMain = process.argv[1] !== undefined && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
173
200
  if (isMain) {
174
201
  run(process.argv.slice(2)).catch(err => {
175
202
  console.error(`mugiwara: ${err.message}`);
@@ -7,9 +7,9 @@ export function parseFrontmatter(text: string): { data: FrontmatterData; body: s
7
7
  const data: FrontmatterData = {};
8
8
  for (const line of m[1].split(/\r?\n/)) {
9
9
  if (!line.trim()) continue;
10
- const i = line.indexOf(':');
11
- if (i === -1) throw new Error(`Bad frontmatter line: ${line}`);
12
- data[line.slice(0, i).trim()] = line.slice(i + 1).trim();
10
+ const i = line.indexOf(': ');
11
+ if (i === -1) throw new Error(`Bad frontmatter line (missing ': ' separator): ${line}`);
12
+ data[line.slice(0, i).trim()] = line.slice(i + 2).trim();
13
13
  }
14
14
  return { data, body: text.slice(m[0].length) };
15
15
  }