@ionivetech/mugiwara 0.5.5 → 0.6.1

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 (151) 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/mugiwara-helpers.mjs +24 -0
  16. package/.opencode/plugins/mugiwara.mjs +28 -8
  17. package/AGENTS.md +1 -1
  18. package/GEMINI.md +1 -1
  19. package/README.md +311 -127
  20. package/content/agents/brook-healing.md +17 -3
  21. package/content/agents/chopper-checkpoint.md +17 -3
  22. package/content/agents/eval-runner.md +17 -2
  23. package/content/agents/franky-gates.md +19 -5
  24. package/content/agents/jinbe-security.md +19 -3
  25. package/content/agents/luffy-orchestrator.md +6 -3
  26. package/content/agents/memory-keeper.md +15 -0
  27. package/content/agents/nami-planner.md +21 -6
  28. package/content/agents/onboarding-guide.md +145 -0
  29. package/content/agents/resume-coordinator.md +21 -5
  30. package/content/agents/robin-reviewer.md +18 -3
  31. package/content/agents/sanji-quality.md +19 -4
  32. package/content/agents/skeptic-verifier.md +18 -3
  33. package/content/agents/usopp-brainstorm.md +17 -3
  34. package/content/agents/zoro-execution.md +18 -3
  35. package/content/skills/mugiwara-backend/SKILL.md +1 -1
  36. package/content/skills/mugiwara-brainstorm/SKILL.md +12 -1
  37. package/content/skills/mugiwara-checkpoint/SKILL.md +2 -2
  38. package/content/skills/mugiwara-contract-first/SKILL.md +2 -36
  39. package/content/skills/mugiwara-contract-first/references/process.md +37 -0
  40. package/content/skills/mugiwara-execution/SKILL.md +51 -34
  41. package/content/skills/mugiwara-execution/references/dispatch.md +41 -0
  42. package/content/skills/mugiwara-execution/references/resume-batching.md +32 -0
  43. package/content/skills/mugiwara-frontend/SKILL.md +1 -1
  44. package/content/skills/mugiwara-gates/SKILL.md +32 -30
  45. package/content/skills/mugiwara-healing/SKILL.md +1 -1
  46. package/content/skills/mugiwara-orchestration/SKILL.md +59 -39
  47. package/content/skills/mugiwara-orchestration/references/closure.md +34 -0
  48. package/content/skills/mugiwara-orchestration/references/delegation-pillars.md +40 -0
  49. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +97 -0
  50. package/content/skills/mugiwara-planning/SKILL.md +29 -27
  51. package/content/skills/mugiwara-planning/references/plan-template.md +42 -0
  52. package/content/skills/mugiwara-pr/SKILL.md +9 -2
  53. package/content/skills/mugiwara-quality/SKILL.md +17 -6
  54. package/content/skills/mugiwara-resume/SKILL.md +7 -2
  55. package/content/skills/mugiwara-review/SKILL.md +20 -1
  56. package/content/skills/mugiwara-review/references/code-attributes.md +38 -0
  57. package/content/skills/mugiwara-root-cause/SKILL.md +3 -30
  58. package/content/skills/mugiwara-root-cause/references/process.md +35 -0
  59. package/content/skills/mugiwara-security/SKILL.md +17 -32
  60. package/content/skills/mugiwara-security/references/owasp-top10.md +12 -0
  61. package/content/skills/mugiwara-security/references/rationalizations.md +8 -0
  62. package/content/skills/mugiwara-ship/SKILL.md +13 -10
  63. package/content/skills/mugiwara-testcases/SKILL.md +7 -0
  64. package/content/skills/mugiwara-workflow/SKILL.md +74 -77
  65. package/content/skills/mugiwara-workflow/references/workspace-layout.md +50 -0
  66. package/content/skills/using-mugiwara/SKILL.md +13 -18
  67. package/dist/mugiwara.js +293 -47
  68. package/gemini-extension.json +1 -1
  69. package/hooks/mugiwara-mode-tracker.ts +0 -0
  70. package/hooks/session-start.ts +1 -1
  71. package/package.json +11 -4
  72. package/plugin.json +1 -1
  73. package/references/multi-actor.md +1 -1
  74. package/scripts/evidence.sh +28 -6
  75. package/scripts/gate-selftest.ts +317 -0
  76. package/scripts/initiative.ts +262 -0
  77. package/scripts/lane.sh +18 -1
  78. package/scripts/mission-report.sh +237 -36
  79. package/scripts/onboard.ts +292 -0
  80. package/scripts/release-notes.ts +42 -13
  81. package/scripts/savepoint.sh +151 -60
  82. package/scripts/validate-content.ts +97 -0
  83. package/scripts/verify-install.ts +72 -0
  84. package/src/cli.ts +20 -3
  85. package/src/installer.ts +71 -8
  86. package/src/mission.ts +117 -5
  87. package/src/targets/claude.ts +45 -9
  88. package/src/targets/generic.ts +1 -1
  89. package/src/targets/opencode.ts +31 -10
  90. package/docs/concepts/agents.md +0 -53
  91. package/docs/concepts/audit-trail.md +0 -65
  92. package/docs/concepts/comparison.md +0 -58
  93. package/docs/concepts/config.md +0 -55
  94. package/docs/concepts/cost.md +0 -45
  95. package/docs/concepts/execution-model.md +0 -92
  96. package/docs/concepts/git-strategy.md +0 -62
  97. package/docs/concepts/lanes.md +0 -82
  98. package/docs/concepts/modes.md +0 -73
  99. package/docs/concepts/pr-summary.md +0 -54
  100. package/docs/concepts/skills.md +0 -55
  101. package/docs/concepts/workflow.md +0 -89
  102. package/docs/getting-started.md +0 -158
  103. package/docs/index.md +0 -56
  104. package/docs/install/antigravity.md +0 -45
  105. package/docs/install/claude.md +0 -77
  106. package/docs/install/cli.md +0 -115
  107. package/docs/install/codex.md +0 -44
  108. package/docs/install/copilot.md +0 -45
  109. package/docs/install/cursor.md +0 -45
  110. package/docs/install/gemini.md +0 -44
  111. package/docs/install/index.md +0 -53
  112. package/docs/install/kimi.md +0 -45
  113. package/docs/install/opencode.md +0 -143
  114. package/docs/install/pi.md +0 -46
  115. package/docs/reference/adoption-guide.md +0 -72
  116. package/docs/reference/agent-anatomy.md +0 -72
  117. package/docs/reference/compliance-matrix.md +0 -81
  118. package/docs/reference/developer-onboarding.md +0 -89
  119. package/docs/reference/enforcement.md +0 -35
  120. package/docs/reference/harness-matrix.md +0 -41
  121. package/docs/reference/skill-anatomy.md +0 -71
  122. package/docs/troubleshooting.md +0 -91
  123. package/evals/cases/_no-skill.json +0 -16
  124. package/evals/cases/adversarial-pressure-fake-pass.json +0 -25
  125. package/evals/cases/adversarial-pressure-skip-review.json +0 -25
  126. package/evals/cases/lane-exploratory-vague.json +0 -24
  127. package/evals/cases/lane-sensitivity-payment.json +0 -24
  128. package/evals/cases/positive-refactor-existing-tests.json +0 -25
  129. package/evals/cases/positive-resume-mid-mission.json +0 -24
  130. package/evals/cases/routing-agent-security.json +0 -25
  131. package/evals/cases/routing-auth-feature.json +0 -26
  132. package/evals/cases/routing-backend.json +0 -25
  133. package/evals/cases/routing-bug-one-file.json +0 -26
  134. package/evals/cases/routing-claim-audit.json +0 -25
  135. package/evals/cases/routing-context-budget.json +0 -25
  136. package/evals/cases/routing-contract-first.json +0 -25
  137. package/evals/cases/routing-execution.json +0 -25
  138. package/evals/cases/routing-frontend.json +0 -26
  139. package/evals/cases/routing-gates.json +0 -25
  140. package/evals/cases/routing-git.json +0 -25
  141. package/evals/cases/routing-healing.json +0 -25
  142. package/evals/cases/routing-lessons.json +0 -25
  143. package/evals/cases/routing-orchestration.json +0 -25
  144. package/evals/cases/routing-planning.json +0 -26
  145. package/evals/cases/routing-pr.json +0 -25
  146. package/evals/cases/routing-quality.json +0 -25
  147. package/evals/cases/routing-ship.json +0 -26
  148. package/evals/cases/routing-sunset.json +0 -25
  149. package/evals/cases/routing-using-mugiwara.json +0 -25
  150. package/evals/cases/routing-workflow.json +0 -25
  151. package/evals/floor.json +0 -6
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env bun
2
- // scripts/release-notes.ts — generates a human-readable release description
3
- // from git history between the previous tag and HEAD. Grouped by change type,
4
- // conventional-commit aware, no gh dependency.
2
+ // scripts/release-notes.ts — generates a detailed GitHub Release description
3
+ // from git history between the previous tag and HEAD. Pulls the full commit
4
+ // message (subject + body), groups by conventional-commit type, flags breaking
5
+ // changes, and lists the affected scopes. No gh dependency.
5
6
  //
6
- // bun scripts/release-notes.ts notes since the last tag → stdout
7
+ // bun scripts/release-notes.ts notes since the last tag → stdout
7
8
  // bun scripts/release-notes.ts --since v0.3.0 notes from that tag → stdout
8
9
  import { execFileSync } from 'node:child_process';
9
10
 
@@ -22,15 +23,22 @@ if (!since) {
22
23
  }
23
24
 
24
25
  const range = since ? `${since}..HEAD` : '';
25
- const raw = execFileSync('git', ['log', '--pretty=%s', ...(range ? [range] : [])], { encoding: 'utf8' });
26
- const commits = raw.split(/\r?\n/).filter(Boolean);
26
+ const raw = execFileSync('git', ['log', '--pretty=%H%n%s%n%n%b%n__END__', ...(range ? [range] : [])], { encoding: 'utf8' });
27
+ const commits = raw
28
+ .split('__END__\n')
29
+ .map(c => c.trim())
30
+ .filter(Boolean)
31
+ .map(c => {
32
+ const [sha, subject, ...rest] = c.split('\n');
33
+ return { sha: sha.slice(0, 7), subject: subject ?? '', body: rest.join('\n').trim() };
34
+ });
27
35
 
28
36
  const groups: Record<string, { label: string; items: string[] }> = {
29
37
  feat: { label: 'New', items: [] },
30
38
  fix: { label: 'Fixed', items: [] },
31
- docs: { label: 'Docs', items: [] },
32
39
  refactor: { label: 'Refactored', items: [] },
33
40
  perf: { label: 'Performance', items: [] },
41
+ docs: { label: 'Docs', items: [] },
34
42
  chore: { label: 'Housekeeping', items: [] },
35
43
  test: { label: 'Housekeeping', items: [] },
36
44
  ci: { label: 'Housekeeping', items: [] },
@@ -38,12 +46,33 @@ const groups: Record<string, { label: string; items: string[] }> = {
38
46
 
39
47
  const order = ['feat', 'fix', 'refactor', 'perf', 'docs', 'chore'];
40
48
 
41
- for (const c of commits) {
42
- const m = /^(\w+)(?:\(.*\))?!?: (.*)/.exec(c);
49
+ // strip signature trailers (Co-authored-by, Signed-off-by, review notes)
50
+ const TRAILER = /^(Co-authored-by|Signed-off-by|Reviewed-by|Helped-by|Reported-by|Tested-by|Acked-by):/i;
51
+
52
+ for (const commit of commits) {
53
+ const m = /^(\w+)(?:\((.*?)\))?!?: (.*)/.exec(commit.subject);
43
54
  const type = m ? m[1] : 'chore';
44
- const text = m ? m[2] : c;
55
+ const scope = m?.[2] || '';
56
+ const text = m ? m[3] : commit.subject;
57
+ const breaking = Boolean(m?.[0]?.includes('!')) || /^BREAKING CHANGE:/m.test(commit.body);
58
+
59
+ const title = text.charAt(0).toUpperCase() + text.slice(1);
60
+ const scopeTag = scope ? ` \`${scope}\`` : '';
61
+ const breakingTag = breaking ? ' ⚠️ **BREAKING**' : '';
62
+
63
+ const bodyLines = commit.body
64
+ .split(/\r?\n/)
65
+ .map(l => l.trim())
66
+ .filter(l => l && !TRAILER.test(l) && l !== '---')
67
+ // the release-version marker commit repeats the subject as body — drop it
68
+ .filter(l => l !== text);
69
+
70
+ let entry = `- ${title}${scopeTag}${breakingTag} \`${commit.sha}\``;
71
+ if (bodyLines.length) {
72
+ entry += '\n' + bodyLines.map(l => ` - ${l}`).join('\n');
73
+ }
45
74
  const g = groups[type] ?? groups.chore;
46
- g.items.push(text.charAt(0).toUpperCase() + text.slice(1));
75
+ g.items.push(entry);
47
76
  }
48
77
 
49
78
  const lines: string[] = [];
@@ -52,11 +81,11 @@ for (const t of order) {
52
81
  if (!g.items.length) continue;
53
82
  lines.push(`## ${g.label}`);
54
83
  lines.push('');
55
- for (const item of g.items) lines.push(`- ${item}`);
84
+ for (const item of g.items) lines.push(item);
56
85
  lines.push('');
57
86
  }
58
87
 
59
- const bumpIdx = commits.findIndex(c => /chore: release|chore: bump/.test(c));
88
+ const bumpIdx = commits.findIndex(c => /chore: release|chore: bump/.test(c.subject));
60
89
  const body = commits.slice(0, bumpIdx === -1 ? commits.length : bumpIdx);
61
90
  const count = body.length || commits.length;
62
91
 
@@ -8,19 +8,33 @@ 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}}"
27
+ fi
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
+ # dot-only names (".", "..", "...") pass the char allowlist but resolve upward
35
+ # through join(...,"..") — reject them before any path is built from MISSION.
36
+ if [[ "$MISSION" =~ ^\.+$ ]]; then
37
+ die "invalid mission name \"$MISSION\" (allowlist: [a-zA-Z0-9._-], not a dot-path)"
24
38
  fi
25
39
 
26
40
  # per-branch state file when --branch used
@@ -50,61 +64,119 @@ if [ "$BASE_SHA" != "unknown" ]; then
50
64
  fi
51
65
  [ -z "$LOC_DELTA" ] && LOC_DELTA=0
52
66
 
53
- SENSITIVE_PATTERNS="auth/|payment/|billing/|crypto/|secrets/|\.env|config/|migration/|\.sql$|schema\.|\.prisma$"
67
+ SENSITIVE_PATTERNS="auth/|payment/|billing/|crypto/|secrets/|\.env$|config/.*key|migration/|\.sql$|schema\.|\.prisma$|\.terraform|\.tf$"
54
68
  SENSITIVE_PATHS=$(echo "$CHANGED_FILES" | grep -E "$SENSITIVE_PATTERNS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
55
69
 
56
70
  LANE="direct"
57
71
  LANE_REASON=""
58
- if [ "$FILES_TOUCHED" -ge 9 ] 2>/dev/null || [ -n "$SENSITIVE_PATHS" ]; then
72
+ # Sensitive-path escalation is unconditional it wins over any count-based
73
+ # lane AND over the docs-only downgrade below (bug C9).
74
+ if [ -n "$SENSITIVE_PATHS" ]; then
75
+ LANE="full"
76
+ LANE_REASON="sensitive paths: $SENSITIVE_PATHS"
77
+ elif [ "$FILES_TOUCHED" -ge 9 ] 2>/dev/null; then
59
78
  LANE="full"
60
- LANE_REASON="$( [ -n "$SENSITIVE_PATHS" ] && echo "sensitive paths: $SENSITIVE_PATHS" || echo "$FILES_TOUCHED files")"
79
+ LANE_REASON="$FILES_TOUCHED files"
61
80
  elif [ "$FILES_TOUCHED" -ge 3 ] 2>/dev/null; then
62
81
  LANE="standard"
63
82
  LANE_REASON="$FILES_TOUCHED files"
64
83
  elif [ "$FILES_TOUCHED" -ge 2 ] 2>/dev/null; then
65
84
  LANE="lean"
66
85
  LANE_REASON="$FILES_TOUCHED files"
86
+ elif [ "$FILES_TOUCHED" -eq 1 ] 2>/dev/null; then
87
+ # 1-file rule mirrors lane.sh: >=20 added LOC -> lean, else direct
88
+ ADDED=$(git diff --numstat "$BASE_SHA"..HEAD 2>/dev/null | awk '{s+=$1} END {print s+0}')
89
+ if [ "${ADDED:-0}" -ge 20 ] 2>/dev/null; then
90
+ LANE="lean"
91
+ LANE_REASON="1 file, $ADDED LOC"
92
+ else
93
+ LANE="direct"
94
+ LANE_REASON="1 file, <20 LOC"
95
+ fi
67
96
  else
68
97
  LANE="direct"
69
98
  LANE_REASON="$FILES_TOUCHED file(s) under 20 LOC"
70
99
  fi
71
100
 
72
- # task counts from plan doc
73
- PLAN_FILE=$(ls "$MUGIWARA_DIR/plans/${MISSION}.md" 2>/dev/null || true)
101
+ # path-weighted sizing (mirrors lane.sh): docs-only changes outside the product
102
+ # surface never escalate to full from file count alone; sensitive-path
103
+ # escalation above still wins.
104
+ PRODUCT_PAT="^content/|^src/|^scripts/|^test/|^hooks/|^\.opencode/|^\.claude/|^evals/"
105
+ if [ "$LANE" = "full" ] && [ -z "$SENSITIVE_PATHS" ] && [ -n "$CHANGED_FILES" ]; then
106
+ CODE_COUNT=$(echo "$CHANGED_FILES" | grep -E "$PRODUCT_PAT" 2>/dev/null | grep -c . || true)
107
+ if [ -z "$CODE_COUNT" ] || [ "$CODE_COUNT" -eq 0 ] 2>/dev/null; then
108
+ PREV="$LANE"
109
+ LANE="standard"
110
+ LANE_REASON="$FILES_TOUCHED files, docs-only — path-weighted down from $PREV"
111
+ fi
112
+ fi
113
+
114
+ # task counts from plan doc — plan is written date-prefixed (plans/YYYY-MM-DD-<mission>.md)
115
+ # or bare (plans/<mission>.md); glob both, first match wins.
116
+ PLAN_FILE=$(ls "$MUGIWARA_DIR"/plans/${MISSION}.md "$MUGIWARA_DIR"/plans/*-${MISSION}.md 2>/dev/null | head -1 || true)
74
117
  TASKS_DONE=0
75
118
  TASKS_TOTAL=0
76
119
  if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
77
- TASKS_TOTAL=$(grep -c '\[ \]' "$PLAN_FILE" 2>/dev/null || echo 0)
78
- TASKS_DONE=$(grep -c '\[x\]' "$PLAN_FILE" 2>/dev/null || echo 0)
120
+ # total counts ALL task lines (checked + unchecked); done counts checked only.
121
+ # A fully-completed plan must read total=N done=N, never total=0 (the old
122
+ # unchecked-only grep degenerated a done plan to tasks.total=0).
123
+ TASKS_TOTAL=$(grep -cE '^\s*-\s*\[[ xX]\]' "$PLAN_FILE" 2>/dev/null || true)
124
+ TASKS_DONE=$(grep -c '\[x\]' "$PLAN_FILE" 2>/dev/null || true)
79
125
  fi
80
126
 
81
127
  # blocker count
82
128
  BLOCKERS_FILE=$(ls "$MUGIWARA_DIR/issues/${MISSION}-blockers.md" 2>/dev/null || true)
83
129
  BLOCKERS_OPEN=0
84
130
  if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
85
- BLOCKERS_OPEN=$(grep -c '|' "$BLOCKERS_FILE" 2>/dev/null || echo 0)
131
+ # data rows start with a wave number; header "| wave |" and separator
132
+ # "|---|" are excluded by the ^\| ?[0-9]+ \| pattern
133
+ BLOCKERS_OPEN=$(grep -cE '^\| ?[0-9]+ ?\|' "$BLOCKERS_FILE" 2>/dev/null || true)
86
134
  fi
87
135
 
88
- # heal cycle
136
+ # heal cycle — count WAVE-8 banner occurrences in the trace, not the word
137
+ # "heal" anywhere (heal workers/healing text would inflate the counter and
138
+ # cause a premature halt)
89
139
  HEAL_CYCLE=1
90
- TRACE_FILE=$(ls "$MUGIWARA_DIR/results/${MISSION}-trace.md" 2>/dev/null || true)
140
+ TRACE_FILE=$(ls "$MUGIWARA_DIR/results/${MISSION}/"*trace*.md 2>/dev/null | head -1 || true)
91
141
  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)
142
+ HEAL_COUNT=$(grep -ci '^.*Wave 8.*\|wave 8' "$TRACE_FILE" 2>/dev/null || true)
93
143
  HEAL_CYCLE=$((HEAL_COUNT + 1))
94
144
  fi
95
145
 
96
- # evidence paths
97
- EVIDENCE=$(ls "$MUGIWARA_DIR/results/" 2>/dev/null | grep "$MISSION" | sed 's|^|.mugiwara/results/|' | tr '\n' ',' | sed 's/,$//' || true)
146
+ # evidence paths — per-mission results folder (quoted printf, no sed — mission
147
+ # name is allowlisted above, but avoid sed metacharacter semantics entirely)
148
+ EVIDENCE=""
149
+ for evf in "$MUGIWARA_DIR/results/${MISSION}/"*.md; do
150
+ [ -f "$evf" ] || continue
151
+ EVIDENCE="${EVIDENCE}${EVIDENCE:+,}.mugiwara/results/${MISSION}/$(basename "$evf")"
152
+ done
98
153
 
99
- # skill version from package.json
154
+ # skill version from package.json (argv-passing — never interpolate paths into node -e)
100
155
  SKILL_VERSION="1"
101
156
  if [ -f package.json ]; then
102
157
  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")
158
+ [ -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
159
  fi
105
160
 
106
- # tokens from env var (harness exports estimated tokens consumed)
107
- TOKENS_EST=${MUGIWARA_TOKENS:-0}
161
+ # tokens proxy (F7): deterministic estimate when the harness does not report
162
+ # real usage. Monotonic beats precise — LANE_BASE stands in for the skills
163
+ # loaded this lane; loc_delta and written-artifact words scale with growth.
164
+ # MUGIWARA_TOKENS overrides as the reported value.
165
+ LANE_BASE=0
166
+ case "$LANE" in
167
+ lean) LANE_BASE=1500 ;;
168
+ standard) LANE_BASE=4000 ;;
169
+ full) LANE_BASE=9000 ;;
170
+ spike) LANE_BASE=1000 ;;
171
+ esac
172
+ DOC_WORDS=$(cat "$MUGIWARA_DIR"/results/${MISSION}/*.md "$MUGIWARA_DIR"/plans/${MISSION}.md "$MUGIWARA_DIR"/plans/*-${MISSION}.md "$MUGIWARA_DIR"/spec/${MISSION}.md "$MUGIWARA_DIR"/spec/*-${MISSION}.md "$MUGIWARA_DIR"/logs/${MISSION}.md "$MUGIWARA_DIR"/logs/*-${MISSION}.md 2>/dev/null | wc -w | tr -d ' ')
173
+ LOC_TOKENS=$(( LOC_DELTA > 0 ? LOC_DELTA * 12 : 0 ))
174
+ TOKENS_SOURCE="computed"
175
+ TOKENS_EST=$(( LANE_BASE + DOC_WORDS * 135 / 100 + LOC_TOKENS ))
176
+ if [ -n "${MUGIWARA_TOKENS:-}" ]; then
177
+ TOKENS_EST="${MUGIWARA_TOKENS}"
178
+ TOKENS_SOURCE="reported"
179
+ fi
108
180
 
109
181
  # budget per lane
110
182
  BUDGET=0
@@ -126,41 +198,60 @@ elif [ "$BUDGET" -gt 0 ] && [ "$TOKENS_EST" -ge "$WARN_AT" ] 2>/dev/null; then
126
198
  STATUS="warn"
127
199
  fi
128
200
 
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
201
  mkdir -p "$MUGIWARA_DIR"
139
202
 
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
203
+ # --- lane-rise compare (F9): read previous state, flag escalation ---
204
+ LANE_PREV=""
205
+ LANE_ROSE=false
206
+ if [ -f "$STATE_FILE" ]; then
207
+ 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)
208
+ if [ -n "$LANE_PREV" ] && [ "$LANE_PREV" != "$LANE" ]; then
209
+ # lane order: direct < lean < standard < full < spike (spike resizes, not a rise)
210
+ case "$LANE_PREV:$LANE" in
211
+ direct:lean|direct:standard|direct:full|lean:standard|lean:full|standard:full) LANE_ROSE=true ;;
212
+ *) LANE_ROSE=false ;;
213
+ esac
214
+ fi
215
+ fi
165
216
 
217
+ node -e "
218
+ const data = {
219
+ mission: process.argv[1],
220
+ actor: process.argv[2],
221
+ branch: process.argv[3],
222
+ lane: process.argv[4],
223
+ lane_reason: process.argv[5],
224
+ lane_prev: process.argv[24] || null,
225
+ lane_rose: process.argv[25] === 'true',
226
+ wave: parseInt(process.argv[6], 10),
227
+ mode: process.argv[7],
228
+ base_sha: process.argv[8],
229
+ head_sha: process.argv[9],
230
+ files_touched: parseInt(process.argv[10], 10),
231
+ loc_delta: parseInt(process.argv[11], 10),
232
+ sensitive_paths: process.argv[12] ? process.argv[12].split(',').filter(Boolean) : [],
233
+ tasks: { done: parseInt(process.argv[13], 10), total: parseInt(process.argv[14], 10) },
234
+ blockers_open: parseInt(process.argv[15], 10),
235
+ heal_cycle: parseInt(process.argv[16], 10),
236
+ tokens_est: parseInt(process.argv[17], 10) || 0,
237
+ tokens_source: process.argv[26] || 'computed',
238
+ budget: parseInt(process.argv[18], 10) || 0,
239
+ budget_status: process.argv[19],
240
+ skill_version: process.argv[20],
241
+ evidence: process.argv[21] ? process.argv[21].split(',').filter(Boolean) : [],
242
+ updated_at: process.argv[22]
243
+ };
244
+ require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\n');
245
+ " \
246
+ "$MISSION" "$ACTOR" "$BRANCH" "$LANE" "$LANE_REASON" \
247
+ "$WAVE" "$MODE" "$BASE_SHA" "$HEAD_SHA" "$FILES_TOUCHED" \
248
+ "$LOC_DELTA" "$SENSITIVE_PATHS" "$TASKS_DONE" "$TASKS_TOTAL" \
249
+ "$BLOCKERS_OPEN" "$HEAL_CYCLE" "$TOKENS_EST" "$BUDGET" \
250
+ "$STATUS" "$SKILL_VERSION" "$EVIDENCE" \
251
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
252
+ "$STATE_FILE" "$LANE_PREV" "$LANE_ROSE" "$TOKENS_SOURCE"
253
+
254
+ if [ "$LANE_ROSE" = true ]; then
255
+ echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
256
+ fi
166
257
  echo "✓ savepoint written: $STATE_FILE (lane=$LANE, wave=$WAVE, files=$FILES_TOUCHED)"
@@ -27,6 +27,31 @@ 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' && !body.includes('## Red flags')) errors.push(`skill ${file}: missing required "## Red flags" block`);
31
+ if (kind === 'skill') {
32
+ const lines = body.split(/\r?\n/);
33
+ const headingRe = /^## /;
34
+ const sections: { heading: string; start: number; end: number }[] = [];
35
+ for (let i = 0; i < lines.length; i++) {
36
+ if (headingRe.test(lines[i])) sections.push({ heading: lines[i].replace(/^## /, ''), start: i, end: 0 });
37
+ }
38
+ for (let si = 0; si < sections.length; si++) {
39
+ sections[si].end = si + 1 < sections.length ? sections[si + 1].start : lines.length;
40
+ // count non-empty, non-table, non-code-fence content lines
41
+ let inFence = false;
42
+ let contentLines = 0;
43
+ for (let li = sections[si].start; li < sections[si].end; li++) {
44
+ const l = lines[li].trim();
45
+ if (!l) continue;
46
+ if (l.startsWith('```')) { inFence = !inFence; continue; }
47
+ if (inFence) continue;
48
+ if (l.startsWith('|') || l.startsWith('- |')) continue;
49
+ contentLines++;
50
+ }
51
+ if (contentLines >= 20) errors.push(`skill ${file}: section "${sections[si].heading}" is ${contentLines} content lines (≥20) — move to references/`);
52
+ else if (contentLines >= 15) console.warn(`⚠ skill ${file}: section "${sections[si].heading}" is ${contentLines} content lines (≥15, consider moving to references/)`);
53
+ }
54
+ }
30
55
  return data;
31
56
  }
32
57
 
@@ -110,6 +135,78 @@ for (const dir of skillDirs) {
110
135
  if (dir !== 'mugiwara-workflow' && !usedSkills.has(dir)) errors.push(`skill ${dir}: not referenced by any agent`);
111
136
  }
112
137
 
138
+ // --- write-scope gate (F2): every agent declares a scope; only executors may write source ---
139
+ const SOURCE_SCOPED = new Set(['zoro-execution', 'brook-healing']);
140
+ for (const f of agentFiles) {
141
+ const name = f.replace(/\.md$/, '');
142
+ const parsed = parseFrontmatter(readFileSync(join(agentDir, f), 'utf8'));
143
+ const scope = parsed.data['write-scope'];
144
+ if (!scope) { errors.push(`agent ${f}: missing write-scope (artifacts | source)`); continue; }
145
+ if (scope !== 'artifacts' && scope !== 'source') errors.push(`agent ${f}: write-scope must be artifacts | source (got "${scope}")`);
146
+ if (scope === 'source' && !SOURCE_SCOPED.has(name)) errors.push(`agent ${f}: only zoro-execution and brook-healing may declare write-scope: source`);
147
+ }
148
+
149
+ // --- write-boundary gate (W1): the hub skill must carry the refusal rule ---
150
+ const hubFile = join(root, 'skills', 'mugiwara-orchestration', 'SKILL.md');
151
+ if (existsSync(hubFile) && !readFileSync(hubFile, 'utf8').includes('## Write boundary')) {
152
+ errors.push('skill mugiwara-orchestration: missing "## Write boundary" section (non-executors refuse source writes)');
153
+ }
154
+
155
+ // --- agent-count gate (F14): user-facing/internal split derivable from content/agents/ ---
156
+ const internalAgents = agentFiles.filter(f => {
157
+ const parsed = parseFrontmatter(readFileSync(join(agentDir, f), 'utf8'));
158
+ return parsed.data.internal === 'true';
159
+ });
160
+ const canonicalCount = `${agentFiles.length - internalAgents.length} agents (+${internalAgents.length} internal)`;
161
+ for (const doc of ['README.md', 'docs/index.md', 'docs/concepts/agents.md']) {
162
+ const p = join(import.meta.dirname, '..', doc);
163
+ if (existsSync(p) && !readFileSync(p, 'utf8').includes(canonicalCount)) {
164
+ errors.push(`${doc}: missing canonical agent count "${canonicalCount}"`);
165
+ }
166
+ }
167
+
168
+ // --- hub-rule gate (F3): every non-Luffy agent carries both hub sections ---
169
+ for (const f of agentFiles) {
170
+ const name = f.replace(/\.md$/, '');
171
+ if (name === 'luffy-orchestrator') continue;
172
+ const text = readFileSync(join(agentDir, f), 'utf8');
173
+ if (!text.includes('## Before you start')) errors.push(`agent ${f}: missing "## Before you start" entry protocol`);
174
+ if (!text.includes('## Return to Luffy')) errors.push(`agent ${f}: missing "## Return to Luffy" hub rule`);
175
+ }
176
+
177
+ // --- hub-skill gate (F3): every agent lists mugiwara-orchestration (the hub rule's home) ---
178
+ for (const f of agentFiles) {
179
+ const parsed = parseFrontmatter(readFileSync(join(agentDir, f), 'utf8'));
180
+ const skills = (parsed.data.skills ?? '').split(',').map(s => s.trim());
181
+ if (!skills.includes('mugiwara-orchestration')) errors.push(`agent ${f}: missing mugiwara-orchestration in skills (hub rule lives there)`);
182
+ }
183
+
184
+ // --- handoff-target gate (F4): a body naming another crew member as a handoff
185
+ // target without Luffy in the same line fails CI ---
186
+ const HANDOFF_CREW = ['usopp', 'nami', 'zoro', 'chopper', 'sanji', 'franky', 'robin', 'jinbe', 'brook', 'skeptic', 'memory-keeper', 'resume-coordinator', 'eval-runner'];
187
+ // Verb class covers direct handoff language plus synonyms (route/send/pass/
188
+ // escalate/delegate/transfer) so the F4 gate does not leak through paraphrase.
189
+ 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');
190
+ // Negation must DIRECTLY precede the handoff verb, not sit anywhere in the
191
+ // line ("do not hand off" later in the same line is a different clause).
192
+ const NEGATION_VERB = /\b(never|not|no|don't|do not)\s+(hand|dispatch|route|send|pass|escalate|delegate|transfer)\b/i;
193
+ for (const f of [...agentFiles, ...skillDirs.map(d => join(root, 'skills', d, 'SKILL.md'))]) {
194
+ if (!existsSync(f)) continue;
195
+ const text = readFileSync(f, 'utf8');
196
+ const lines = text.split(/\r?\n/);
197
+ for (let i = 0; i < lines.length; i++) {
198
+ const line = lines[i];
199
+ const m = handoffRe.exec(line);
200
+ if (!m) continue;
201
+ // clause boundary: only the clause containing the handoff verb counts
202
+ const clauseStart = line.lastIndexOf('.', m.index) + 1;
203
+ const clause = line.slice(clauseStart, m.index + m[0].length);
204
+ if (NEGATION_VERB.test(clause)) continue; // "never dispatch X" — constraint
205
+ if (/\bLuffy\b|to Luffy\b/.test(line)) continue; // hub named in same line ("main thread" is NOT the hub — Luffy is)
206
+ errors.push(`handoff leak ${f.replace(root + '/', '')}:${i + 1}: "${line.trim()}" — handoff target must be Luffy or name Luffy in the same line`);
207
+ }
208
+ }
209
+
113
210
  // --- manifest-sync check: .claude-plugin/plugin.json must set-equal content/ ---
114
211
  const manifestArg = process.argv.indexOf('--check-manifest');
115
212
  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/cli.ts CHANGED
@@ -7,15 +7,15 @@ 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';
10
- import { installTo, removeInstalled, VERSION } from './installer.ts';
10
+ import { installTo, removeInstalled, VERSION, ensureProjectGitignore } from './installer.ts';
11
11
  import { manifestPath, readManifest, writeManifest, type Scope } from './manifest.ts';
12
- import { resetMission } from './mission.ts';
12
+ import { resetMission, archiveMission } from './mission.ts';
13
13
 
14
14
  const str = (v: FlagValue): string | undefined => (typeof v === 'string' ? v : undefined);
15
15
  const flag = (v: FlagValue): boolean => v === true;
16
16
 
17
17
  export async function run(argv: string[]): Promise<void> {
18
- const { command, flags } = parseArgs(argv);
18
+ const { command, flags, _ } = parseArgs(argv);
19
19
  if (flag(flags.help) || command === 'help') return help();
20
20
  if (flag(flags.version)) { console.log(`mugiwara ${VERSION}`); return; }
21
21
  switch (command) {
@@ -24,6 +24,7 @@ export async function run(argv: string[]): Promise<void> {
24
24
  case 'uninstall': return uninstall(flags);
25
25
  case 'list': return list(flags);
26
26
  case 'reset': return resetCmd(flags);
27
+ case 'archive': return archive(flags, _);
27
28
  default: throw new Error(`Unknown command: ${command}`);
28
29
  }
29
30
  }
@@ -41,6 +42,17 @@ function resetCmd(flags: Args['flags']): void {
41
42
  if (result.kept.length) console.log(`kept: ${result.kept.join(', ')}`);
42
43
  }
43
44
 
45
+ function archive(flags: Args['flags'], positionals: string[]): void {
46
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
47
+ const mission = positionals[1];
48
+ if (!mission) { console.error('usage: mugiwara archive <mission> [--project <dir>] [--dry-run]'); process.exit(1); }
49
+ const result = archiveMission(projectDir, mission, { dryRun: flag(flags.dryRun) });
50
+ if (result.report) console.log(`archive target: ${result.report}`);
51
+ if (result.removed.length) console.log(`${flag(flags.dryRun) ? 'would remove' : 'removed'}: ${result.removed.join(', ')}`);
52
+ if (result.kept.length) console.log(`kept: ${result.kept.join(', ')}`);
53
+ if (result.index) console.log(`index updated: ${result.index}`);
54
+ }
55
+
44
56
  async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; projectDir: string; targetIds: string[] }> {
45
57
  const interactive = !flag(flags.yes);
46
58
  if (interactive && !process.stdin.isTTY) {
@@ -95,6 +107,10 @@ async function install(flags: Args['flags']): Promise<void> {
95
107
  allFiles.push(...r.written);
96
108
  allNotes.push(...r.notes);
97
109
  }
110
+ if (scope === 'project') {
111
+ const gi = ensureProjectGitignore(projectDir, { dryRun: flag(flags.dryRun) });
112
+ allNotes.push(...gi.notes);
113
+ }
98
114
  if (flag(flags.dryRun)) { console.log('\nDry run — nothing written.'); return; }
99
115
  const file = manifestPath({ scope, projectDir, home });
100
116
  const prev = readManifest(file);
@@ -182,6 +198,7 @@ Usage:
182
198
  mugiwara list show installations
183
199
  mugiwara list --check health check: show installations + missing files
184
200
  mugiwara reset wipe mission state (spec/plans/results/review/issues[/logs])
201
+ mugiwara archive <m> fold a closed mission's evidence into its report, then remove loose files
185
202
  mugiwara --help this help
186
203
  mugiwara --version print version
187
204