@ionivetech/mugiwara 0.3.0 → 0.5.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 (119) hide show
  1. package/.opencode/commands/mugiwara-plan.md +4 -0
  2. package/README.md +214 -418
  3. package/content/agents/brook-healing.md +2 -2
  4. package/content/agents/chopper-checkpoint.md +2 -1
  5. package/content/agents/eval-runner.md +4 -4
  6. package/content/agents/franky-gates.md +1 -1
  7. package/content/agents/jinbe-security.md +2 -1
  8. package/content/agents/luffy-orchestrator.md +3 -3
  9. package/content/agents/memory-keeper.md +1 -1
  10. package/content/agents/nami-planner.md +2 -2
  11. package/content/agents/resume-coordinator.md +8 -9
  12. package/content/agents/robin-reviewer.md +3 -2
  13. package/content/agents/sanji-quality.md +1 -1
  14. package/content/agents/skeptic-verifier.md +4 -3
  15. package/content/agents/using-mugiwara.md +3 -3
  16. package/content/agents/usopp-brainstorm.md +3 -3
  17. package/content/agents/zoro-execution.md +4 -3
  18. package/content/skills/mugiwara-agent-security/SKILL.md +19 -2
  19. package/content/skills/mugiwara-backend/SKILL.md +7 -8
  20. package/content/skills/mugiwara-backend/references/source-backed-example.md +77 -0
  21. package/content/skills/mugiwara-brainstorm/SKILL.md +7 -2
  22. package/content/skills/mugiwara-checkpoint/SKILL.md +7 -2
  23. package/content/skills/mugiwara-checkpoint/references/ledger-format.md +34 -0
  24. package/content/skills/{mugiwara-doubt-driven-development → mugiwara-claim-audit}/SKILL.md +7 -2
  25. package/content/skills/{mugiwara-context-engineering → mugiwara-context-budget}/SKILL.md +7 -2
  26. package/content/skills/mugiwara-context-budget/references/context-budget.md +37 -0
  27. package/content/skills/{mugiwara-api-and-interface-design → mugiwara-contract-first}/SKILL.md +9 -2
  28. package/content/skills/mugiwara-contract-first/references/versioning-playbook.md +36 -0
  29. package/content/skills/mugiwara-execution/SKILL.md +9 -2
  30. package/content/skills/mugiwara-execution/references/worked-example.md +88 -0
  31. package/content/skills/mugiwara-frontend/SKILL.md +21 -22
  32. package/content/skills/mugiwara-frontend/references/design-tokens.md +25 -0
  33. package/content/skills/mugiwara-frontend/references/slop-catalog.md +19 -0
  34. package/content/skills/mugiwara-gates/SKILL.md +9 -4
  35. package/content/skills/mugiwara-git/SKILL.md +6 -1
  36. package/content/skills/mugiwara-healing/SKILL.md +39 -7
  37. package/content/skills/mugiwara-healing/references/failure-taxonomy.md +23 -0
  38. package/content/skills/mugiwara-lessons/SKILL.md +6 -1
  39. package/content/skills/mugiwara-orchestration/SKILL.md +18 -5
  40. package/content/skills/mugiwara-planning/SKILL.md +16 -15
  41. package/content/skills/mugiwara-planning/references/anti-patterns.md +17 -0
  42. package/content/skills/mugiwara-planning/references/plan-template.md +46 -0
  43. package/content/skills/mugiwara-pr/SKILL.md +6 -1
  44. package/content/skills/{mugiwara-test-driven-development → mugiwara-proof-order}/SKILL.md +7 -2
  45. package/content/skills/mugiwara-proof-order/references/proof-order-examples.md +62 -0
  46. package/content/skills/mugiwara-quality/SKILL.md +8 -3
  47. package/content/skills/mugiwara-resume/SKILL.md +46 -30
  48. package/content/skills/mugiwara-review/SKILL.md +6 -1
  49. package/content/skills/mugiwara-review/references/five-axis-worksheet.md +36 -0
  50. package/content/skills/mugiwara-review/references/severity-rubric.md +20 -0
  51. package/content/skills/{mugiwara-systematic-debugging → mugiwara-root-cause}/SKILL.md +9 -2
  52. package/content/skills/mugiwara-root-cause/references/four-phase-worked.md +71 -0
  53. package/content/skills/mugiwara-security/SKILL.md +18 -18
  54. package/content/skills/mugiwara-security/references/owasp-mapping.md +30 -0
  55. package/content/skills/mugiwara-security/references/stride-worksheet.md +37 -0
  56. package/content/skills/mugiwara-ship/SKILL.md +6 -1
  57. package/content/skills/{mugiwara-deprecation → mugiwara-sunset}/SKILL.md +7 -2
  58. package/content/skills/mugiwara-testcases/SKILL.md +6 -1
  59. package/content/skills/mugiwara-testcases/references/intake-formats.md +42 -0
  60. package/content/skills/mugiwara-workflow/SKILL.md +26 -24
  61. package/dist/mugiwara.js +165 -28
  62. package/docs/adoption-guide.md +2 -2
  63. package/docs/agents.md +2 -2
  64. package/docs/audit-trail.md +65 -0
  65. package/docs/comparison.md +58 -63
  66. package/docs/compliance-matrix.md +81 -0
  67. package/docs/config.md +7 -2
  68. package/docs/cost.md +45 -0
  69. package/docs/developer-onboarding.md +17 -13
  70. package/docs/enforcement.md +35 -0
  71. package/docs/getting-started.md +103 -41
  72. package/docs/harness-matrix.md +41 -0
  73. package/docs/index.md +11 -5
  74. package/docs/lanes.md +82 -0
  75. package/docs/skill-anatomy.md +42 -49
  76. package/docs/skills.md +36 -54
  77. package/docs/troubleshooting.md +91 -0
  78. package/docs/workflow.md +15 -6
  79. package/evals/cases/adversarial-pressure-fake-pass.json +12 -0
  80. package/evals/cases/adversarial-pressure-skip-review.json +13 -0
  81. package/evals/cases/lane-exploratory-vague.json +12 -0
  82. package/evals/cases/lane-sensitivity-payment.json +12 -0
  83. package/evals/cases/negative-secrets-typo.json +12 -0
  84. package/evals/cases/negative-security-docs-change.json +12 -0
  85. package/evals/cases/positive-refactor-existing-tests.json +11 -0
  86. package/evals/cases/positive-resume-mid-mission.json +11 -0
  87. package/evals/cases/routing-auth-feature.json +13 -0
  88. package/evals/cases/routing-bug-one-file.json +13 -0
  89. package/evals/cases/routing-typo.json +13 -0
  90. package/package.json +6 -4
  91. package/references/definition-of-done.md +42 -0
  92. package/references/multi-actor.md +44 -0
  93. package/references/skill-versioning.md +44 -0
  94. package/references/source-grounding.md +23 -0
  95. package/references/token-budget.md +34 -0
  96. package/scripts/evidence.sh +37 -0
  97. package/scripts/lane.sh +74 -0
  98. package/scripts/mission-report.sh +88 -0
  99. package/scripts/release-notes.ts +65 -0
  100. package/scripts/retrieval-eval.ts +152 -0
  101. package/scripts/run-evals.ts +113 -0
  102. package/scripts/savepoint.sh +165 -0
  103. package/scripts/sync-version.ts +30 -0
  104. package/scripts/validate-content.ts +226 -0
  105. package/src/args.ts +2 -1
  106. package/src/cli.ts +21 -20
  107. package/src/installer.ts +31 -3
  108. package/src/mission.ts +48 -0
  109. package/src/targets/claude.ts +4 -0
  110. package/src/targets/copilot.ts +4 -0
  111. package/src/targets/generic.ts +33 -1
  112. package/src/targets/kilo.ts +1 -0
  113. package/src/targets/opencode.ts +4 -0
  114. package/content/skills/mugiwara-dynamic-workflow/SKILL.md +0 -85
  115. package/content/skills/mugiwara-eval/SKILL.md +0 -82
  116. package/content/skills/mugiwara-git-worktrees/SKILL.md +0 -62
  117. package/content/skills/mugiwara-mode/SKILL.md +0 -72
  118. package/content/skills/mugiwara-observability/SKILL.md +0 -54
  119. package/content/skills/mugiwara-writing-skills/SKILL.md +0 -60
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bun
2
+ // scripts/run-evals.ts — the mugiwara eval harness.
3
+ // Two modes:
4
+ // bun scripts/run-evals.ts validate the suite (structure + coverage). CI-safe, no model.
5
+ // bun scripts/run-evals.ts --run [cmd] execute each case against a model CLI and score the rubric.
6
+ //
7
+ // --run uses the command to invoke a model that prints only its answer to the
8
+ // task; the harness scores the answer against the rubric with a heuristic
9
+ // keyword match, then reports pass/fail per case and a rank-1 style score.
10
+ // The command defaults to "claude -p" — override with MUGIWARA_EVAL_CMD.
11
+ import { readdirSync, readFileSync, existsSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+
14
+ const root = join(import.meta.dirname, '..');
15
+ const casesDir = join(root, 'evals', 'cases');
16
+ const skillsDir = join(root, 'content', 'skills');
17
+ const errors: string[] = [];
18
+
19
+ type Case = {
20
+ name: string;
21
+ skill: string;
22
+ type?: 'positive' | 'negative' | 'adversarial' | 'lane';
23
+ task: string;
24
+ rubric: string[];
25
+ expect_lane?: string;
26
+ };
27
+
28
+ function listCases(dir: string, prefix = ''): string[] {
29
+ if (!existsSync(dir)) return [];
30
+ const out: string[] = [];
31
+ for (const ent of readdirSync(dir, { withFileTypes: true })) {
32
+ const p = join(dir, ent.name);
33
+ if (ent.isDirectory()) out.push(...listCases(p, join(prefix, ent.name)));
34
+ else if (ent.name.endsWith('.json')) out.push(join(prefix, ent.name));
35
+ }
36
+ return out;
37
+ }
38
+
39
+ function validateSuite(): Case[] {
40
+ const cases: Case[] = [];
41
+ const files = listCases(casesDir).sort();
42
+ for (const rel of files) {
43
+ let c: Case;
44
+ try { c = JSON.parse(readFileSync(join(casesDir, rel), 'utf8')); }
45
+ catch (e) { errors.push(`${rel}: invalid JSON (${(e as Error).message})`); continue; }
46
+ if (!c.name || !c.skill || !c.task || !Array.isArray(c.rubric) || !c.rubric.length)
47
+ errors.push(`${rel}: missing name/skill/task/nonempty rubric`);
48
+ else if (!existsSync(join(skillsDir, c.skill))) errors.push(`${rel}: unknown skill "${c.skill}"`);
49
+ if (c.type && !['positive', 'negative', 'adversarial', 'lane'].includes(c.type))
50
+ errors.push(`${rel}: bad type "${c.type}"`);
51
+ cases.push(c);
52
+ }
53
+ if (!cases.length) { errors.push('no cases in evals/cases/'); }
54
+
55
+ // coverage gates: the suite must exercise routing in all directions.
56
+ const types = new Set(cases.map(c => c.type).filter(Boolean));
57
+ if (cases.filter(c => c.type === 'positive').length < 2) errors.push('need ≥2 positive cases');
58
+ if (cases.filter(c => c.type === 'negative').length < 2) errors.push('need ≥2 negative cases');
59
+ if (cases.filter(c => c.type === 'adversarial').length < 2) errors.push('need ≥2 adversarial cases');
60
+ if (!types.has('lane')) errors.push('need ≥1 lane case');
61
+ return cases;
62
+ }
63
+
64
+ function scoreRubric(answer: string, c: Case): { pass: number; total: number; matched: string[] } {
65
+ const a = answer.toLowerCase();
66
+ const matched = c.rubric.filter(r => {
67
+ const terms = r.toLowerCase().split(/[^a-z0-9]+/).filter(w => w.length > 3 && !['does', 'not', 'with', 'into', 'from'].includes(w));
68
+ return terms.some(t => a.includes(t));
69
+ });
70
+ return { pass: matched.length, total: c.rubric.length, matched };
71
+ }
72
+
73
+ async function runCases(env: { execFileSync: typeof import('node:child_process').execFileSync; bin: string; pre: string[] }): Promise<void> {
74
+ const cases = validateSuite();
75
+ if (errors.length) { console.error(errors.map(e => `✗ ${e}`).join('\n')); process.exit(1); }
76
+ const { execFileSync, bin, pre } = env;
77
+ let total = 0, passed = 0;
78
+ for (const c of cases) {
79
+ const prompt = `Task: ${c.task}\n\nWhich mugiwara skill should run and why? Be concrete.`;
80
+ let raw = '';
81
+ try {
82
+ raw = execFileSync(bin, [...pre, prompt], { encoding: 'utf8', timeout: 60000, maxBuffer: 10 * 1024 * 1024 });
83
+ } catch (e: any) {
84
+ raw = e.stdout?.toString?.() ?? String(e.message);
85
+ }
86
+ const { pass, total: t, matched } = scoreRubric(raw, c);
87
+ total += t;
88
+ passed += pass;
89
+ const pct = Math.round((pass / t) * 100);
90
+ console.log(`${pct >= 70 ? '✓' : '✗'} ${c.name} — ${pass}/${t} rubric (${pct}%)`);
91
+ if (pass < t) {
92
+ console.log(` task: ${c.task}`);
93
+ console.log(` matched: ${matched.length ? matched.join('; ') : 'none'}`);
94
+ }
95
+ }
96
+ const overall = Math.round((passed / total) * 100);
97
+ console.log(`\nOVERALL ${passed}/${total} (${overall}%)`);
98
+ if (overall < 70) process.exitCode = 1;
99
+ }
100
+
101
+ const runArg = process.argv.indexOf('--run');
102
+ if (runArg !== -1) {
103
+ const { execFileSync } = require('node:child_process');
104
+ const cmd = process.env.MUGIWARA_EVAL_CMD ?? 'claude -p';
105
+ const [bin, ...pre] = cmd.split(' ');
106
+ runCases({ execFileSync, bin, pre });
107
+ process.exit(0);
108
+ }
109
+
110
+ const cases = validateSuite();
111
+ if (errors.length) { console.error(errors.map(e => `✗ ${e}`).join('\n')); process.exit(1); }
112
+ console.log(`✓ eval suite OK: ${cases.length} cases (${cases.filter(c => c.type === 'positive').length} positive, ${cases.filter(c => c.type === 'negative').length} negative, ${cases.filter(c => c.type === 'adversarial').length} adversarial, ${cases.filter(c => c.type === 'lane').length} lane)`);
113
+ for (const c of cases) console.log(` ${c.name} [${c.type ?? 'default'}]`);
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/savepoint.sh — write .mugiwara/state.json at every wave boundary.
3
+ # Computed from git + file counts; zero model judgement.
4
+ set -u
5
+
6
+ die() { echo "savepoint: $*" >&2; exit 1; }
7
+
8
+ MUGIWARA_DIR="${MUGIWARA_DIR:-.mugiwara}"
9
+
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
18
+ BRANCH_MODE=0
19
+ if [ "${1:-}" = "--branch" ]; then
20
+ BRANCH_MODE=1
21
+ MISSION="${2:-${STATE_MISSION:-}}"
22
+ BRANCH="${3:-$(git branch --show-current 2>/dev/null || echo 'unknown')}"
23
+ shift 2 2>/dev/null || true
24
+ fi
25
+
26
+ # per-branch state file when --branch used
27
+ BRANCH_SLUG=$(echo "$BRANCH" | tr '/' '-')
28
+ if [ "$BRANCH_MODE" -eq 1 ]; then
29
+ STATE_FILE="$MUGIWARA_DIR/state-${BRANCH_SLUG}.json"
30
+ else
31
+ STATE_FILE="$MUGIWARA_DIR/state.json"
32
+ fi
33
+
34
+ [ -z "$MISSION" ] && die "usage: savepoint.sh <mission> [actor] [branch] [wave] [mode]"
35
+ [ -d .git ] || die "not a git repository"
36
+
37
+ # --- computed fields ---
38
+ BASE_SHA=$(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null || git rev-parse HEAD~1 2>/dev/null || echo "unknown")
39
+ HEAD_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
40
+
41
+ CHANGED_FILES=$(git diff --name-only "$BASE_SHA"..HEAD 2>/dev/null || git diff --name-only --cached 2>/dev/null || true)
42
+ FILES_TOUCHED=$( [ -n "$CHANGED_FILES" ] && echo "$CHANGED_FILES" | wc -l | tr -d ' ' || echo 0 )
43
+
44
+ LOC_DELTA=0
45
+ if [ "$BASE_SHA" != "unknown" ]; then
46
+ LOC_DELTA=$(git diff --shortstat "$BASE_SHA"..HEAD 2>/dev/null | \
47
+ awk '{ins=0; del=0; for(i=1;i<=NF;i++){if($i=="insertion") ins=$(i-1); if($i=="deletion") del=$(i-1)} print ins-del}' || echo 0)
48
+ fi
49
+ [ -z "$LOC_DELTA" ] && LOC_DELTA=0
50
+ [ "$LOC_DELTA" = "0" ] || LOC_DELTA=${LOC_DELTA//[^0-9-]/}
51
+
52
+ SENSITIVE_PATTERNS="auth/|payment/|billing/|crypto/|secrets/|\.env|config/|migration/|\.sql$|schema\.|\.prisma$"
53
+ SENSITIVE_PATHS=$(echo "$CHANGED_FILES" | grep -E "$SENSITIVE_PATTERNS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
54
+
55
+ LANE="direct"
56
+ LANE_REASON=""
57
+ if [ "$FILES_TOUCHED" -ge 9 ] 2>/dev/null || [ -n "$SENSITIVE_PATHS" ]; then
58
+ LANE="full"
59
+ LANE_REASON="$( [ -n "$SENSITIVE_PATHS" ] && echo "sensitive paths: $SENSITIVE_PATHS" || echo "$FILES_TOUCHED files")"
60
+ elif [ "$FILES_TOUCHED" -ge 3 ] 2>/dev/null; then
61
+ LANE="standard"
62
+ LANE_REASON="$FILES_TOUCHED files"
63
+ elif [ "$FILES_TOUCHED" -ge 2 ] 2>/dev/null; then
64
+ LANE="lean"
65
+ LANE_REASON="$FILES_TOUCHED files"
66
+ else
67
+ LANE="direct"
68
+ LANE_REASON="$FILES_TOUCHED file(s) under 20 LOC"
69
+ fi
70
+
71
+ # task counts from plan doc
72
+ PLAN_FILE=$(ls "$MUGIWARA_DIR/plans/${MISSION}.md" 2>/dev/null || true)
73
+ TASKS_DONE=0
74
+ TASKS_TOTAL=0
75
+ if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
76
+ TASKS_TOTAL=$(grep -c '\[ \]' "$PLAN_FILE" 2>/dev/null || echo 0)
77
+ TASKS_DONE=$(grep -c '\[x\]' "$PLAN_FILE" 2>/dev/null || echo 0)
78
+ fi
79
+
80
+ # blocker count
81
+ BLOCKERS_FILE=$(ls "$MUGIWARA_DIR/issues/${MISSION}-blockers.md" 2>/dev/null || true)
82
+ BLOCKERS_OPEN=0
83
+ if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
84
+ BLOCKERS_OPEN=$(grep -c '|' "$BLOCKERS_FILE" 2>/dev/null || echo 0)
85
+ fi
86
+
87
+ # heal cycle
88
+ HEAL_CYCLE=1
89
+ TRACE_FILE=$(ls "$MUGIWARA_DIR/results/${MISSION}-trace.md" 2>/dev/null || true)
90
+ if [ -n "$TRACE_FILE" ] && [ -f "$TRACE_FILE" ]; then
91
+ HEAL_COUNT=$(grep -ci 'Wave 8\|wave 8\|heal' "$TRACE_FILE" 2>/dev/null || echo 0)
92
+ HEAL_CYCLE=$((HEAL_COUNT + 1))
93
+ fi
94
+
95
+ # evidence paths
96
+ EVIDENCE=$(ls "$MUGIWARA_DIR/results/" 2>/dev/null | grep "$MISSION" | sed 's|^|.mugiwara/results/|' | tr '\n' ',' | sed 's/,$//' || true)
97
+
98
+ # skill version from package.json
99
+ SKILL_VERSION="1"
100
+ if [ -f package.json ]; then
101
+ PKG_JSON="$MUGIWARA_DIR/../package.json"
102
+ [ -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")
103
+ fi
104
+
105
+ # tokens from env var (harness exports estimated tokens consumed)
106
+ TOKENS_EST=${MUGIWARA_TOKENS:-0}
107
+
108
+ # budget per lane
109
+ BUDGET=0
110
+ case "$LANE" in
111
+ lean) BUDGET=4000 ;;
112
+ standard) BUDGET=10000 ;;
113
+ full) BUDGET=20000 ;;
114
+ spike) BUDGET=3000 ;;
115
+ *) BUDGET=0 ;;
116
+ esac
117
+
118
+ # token budget gate
119
+ WARN_AT=$(( BUDGET * 3 / 2 ))
120
+ STOP_AT=$(( BUDGET * 3 ))
121
+ STATUS="ok"
122
+ if [ "$BUDGET" -gt 0 ] && [ "$TOKENS_EST" -ge "$STOP_AT" ] 2>/dev/null; then
123
+ STATUS="stop"
124
+ elif [ "$BUDGET" -gt 0 ] && [ "$TOKENS_EST" -ge "$WARN_AT" ] 2>/dev/null; then
125
+ STATUS="warn"
126
+ fi
127
+
128
+ SENSITIVE_ARR=""
129
+ if [ -n "$SENSITIVE_PATHS" ]; then
130
+ SENSITIVE_ARR=$(echo "$SENSITIVE_PATHS" | tr ',' '\n' | sed 's/^/"/;s/$/"/' | tr '\n' ',' | sed 's/,$//')
131
+ fi
132
+ EVIDENCE_ARR=""
133
+ if [ -n "$EVIDENCE" ]; then
134
+ EVIDENCE_ARR=$(echo "$EVIDENCE" | tr ',' '\n' | sed 's/^/"/;s/$/"/' | tr '\n' ',' | sed 's/,$//')
135
+ fi
136
+
137
+ mkdir -p "$MUGIWARA_DIR"
138
+
139
+ cat > "$STATE_FILE" <<JSON
140
+ {
141
+ "mission": "$MISSION",
142
+ "actor": "$ACTOR",
143
+ "branch": "$BRANCH",
144
+ "lane": "$LANE",
145
+ "lane_reason": "$LANE_REASON",
146
+ "wave": $WAVE,
147
+ "mode": "$MODE",
148
+ "base_sha": "$BASE_SHA",
149
+ "head_sha": "$HEAD_SHA",
150
+ "files_touched": $FILES_TOUCHED,
151
+ "loc_delta": $LOC_DELTA,
152
+ "sensitive_paths": [${SENSITIVE_ARR}],
153
+ "tasks": { "done": $TASKS_DONE, "total": $TASKS_TOTAL },
154
+ "blockers_open": $BLOCKERS_OPEN,
155
+ "heal_cycle": $HEAL_CYCLE,
156
+ "tokens_est": $TOKENS_EST,
157
+ "budget": $BUDGET,
158
+ "budget_status": "$STATUS",
159
+ "skill_version": "$SKILL_VERSION",
160
+ "evidence": [${EVIDENCE_ARR}],
161
+ "updated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
162
+ }
163
+ JSON
164
+
165
+ echo "✓ savepoint written: $STATE_FILE (lane=$LANE, wave=$WAVE, files=$FILES_TOUCHED)"
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bun
2
+ // scripts/sync-version.ts — syncs package.json version into the plugin manifests
3
+ // so Claude/Copilot plugin version always matches the published npm version.
4
+ import { readFileSync, writeFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ const root = join(import.meta.dirname, '..');
8
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
9
+ const v = pkg.version;
10
+
11
+ const targets = [
12
+ join(root, '.claude-plugin', 'plugin.json'),
13
+ join(root, 'plugin.json'),
14
+ join(root, '.claude-plugin', 'marketplace.json'),
15
+ join(root, 'gemini-extension.json'),
16
+ join(root, '.codex-plugin', 'plugin.json'),
17
+ join(root, '.cursor-plugin', 'plugin.json'),
18
+ join(root, '.kimi-plugin', 'plugin.json'),
19
+ ];
20
+
21
+ for (const file of targets) {
22
+ const doc = JSON.parse(readFileSync(file, 'utf8'));
23
+ if (doc.version !== undefined) doc.version = v;
24
+ if (doc.metadata?.version !== undefined) doc.metadata.version = v;
25
+ if (Array.isArray(doc.plugins)) {
26
+ for (const p of doc.plugins) if (p.version !== undefined) p.version = v;
27
+ }
28
+ writeFileSync(file, JSON.stringify(doc, null, 2) + '\n');
29
+ console.log(`synced ${file} → ${v}`);
30
+ }
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env bun
2
+ // scripts/validate-content.ts
3
+ import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
4
+ import { join, basename } from 'node:path';
5
+ import { parseFrontmatter } from '../src/frontmatter.ts';
6
+
7
+ const root = join(import.meta.dirname, '..', 'content');
8
+ const errors: string[] = [];
9
+
10
+ function checkFile(file: string, wantName: string, kind: 'skill' | 'agent'): Record<string, string> | null {
11
+ let parsed;
12
+ try { parsed = parseFrontmatter(readFileSync(file, 'utf8')); }
13
+ catch (e) { errors.push(`${kind} ${file}: ${(e as Error).message}`); return null; }
14
+ const { data, body } = parsed;
15
+ if (data.name !== wantName) errors.push(`${kind} ${file}: name "${data.name}" != "${wantName}"`);
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})`);
18
+ if (kind === 'agent' && d.length < 20) errors.push(`agent ${file}: description too short`);
19
+ if (kind === 'skill' && body.replace(/\r?\n$/, '').split(/\r?\n/).length > 120) errors.push(`skill ${file}: body exceeds 120 lines`);
20
+ if (kind === 'skill' && !body.includes('## Skip when')) errors.push(`skill ${file}: missing required "## Skip when" block (≤4 lines, numeric threshold)`);
21
+ if (kind === 'skill' && body.includes('## Skip when')) {
22
+ const lines = body.split(/\r?\n/);
23
+ const idx = lines.findIndex(l => l.startsWith('## Skip when'));
24
+ const gateLines = lines.slice(idx + 1).findIndex(l => l.startsWith('## '));
25
+ const end = gateLines === -1 ? lines.length : idx + 1 + gateLines;
26
+ const bullets = lines.slice(idx + 1, end).filter(l => l.trim().startsWith('-'));
27
+ if (bullets.length === 0) errors.push(`skill ${file}: "## Skip when" needs ≥1 bullet`);
28
+ if (bullets.length > 4) errors.push(`skill ${file}: "## Skip when" block exceeds 4 bullets`);
29
+ }
30
+ return data;
31
+ }
32
+
33
+ function listFiles(dir: string, prefix = ''): string[] {
34
+ if (!existsSync(dir)) return [];
35
+ const out: string[] = [];
36
+ for (const ent of readdirSync(dir, { withFileTypes: true })) {
37
+ const p = join(dir, ent.name);
38
+ if (ent.isDirectory()) out.push(...listFiles(p, join(prefix, ent.name)));
39
+ else out.push(join(prefix, ent.name));
40
+ }
41
+ return out;
42
+ }
43
+
44
+ const syncArg = process.argv.indexOf('--check-sync');
45
+ if (syncArg !== -1) {
46
+ const pairs = [['content/skills', 'skills'], ['content/agents', 'agents']] as const;
47
+ const diffs: string[] = [];
48
+ for (const [from, to] of pairs) {
49
+ const fromRoot = join(import.meta.dirname, '..', from);
50
+ const toRoot = join(import.meta.dirname, '..', to);
51
+ const fromFiles = listFiles(fromRoot).sort();
52
+ const toFiles = listFiles(toRoot).sort();
53
+ for (const rel of fromFiles) {
54
+ const f = join(fromRoot, rel), t = join(toRoot, rel);
55
+ if (!existsSync(t)) diffs.push(`missing copy: ${to}/${rel} (run .claude-plugin/sync.sh)`);
56
+ else if (readFileSync(f, 'utf8') !== readFileSync(t, 'utf8')) diffs.push(`out of sync: ${to}/${rel}`);
57
+ }
58
+ for (const rel of toFiles) {
59
+ if (!fromFiles.includes(rel)) diffs.push(`stale copy: ${to}/${rel} (not in content/, run .claude-plugin/sync.sh)`);
60
+ }
61
+ }
62
+ if (diffs.length) { console.error(diffs.map(d => `✗ ${d}`).join('\n')); process.exit(1); }
63
+ console.log('✓ plugin copies in sync with content/');
64
+ process.exit(0);
65
+ }
66
+
67
+ const skillDirs = existsSync(join(root, 'skills'))
68
+ ? readdirSync(join(root, 'skills')).filter(d => statSync(join(root, 'skills', d)).isDirectory())
69
+ : [];
70
+ const names = new Map<string, string>();
71
+ const usedSkills = new Set<string>();
72
+
73
+ const checkArg = process.argv.indexOf('--check');
74
+ if (checkArg !== -1) {
75
+ const file = process.argv[checkArg + 1];
76
+ const isSkill = file.includes('skills');
77
+ const want = isSkill ? basename(join(file, '..')) : basename(file).replace(/\.md$/, '');
78
+ checkFile(file, want, isSkill ? 'skill' : 'agent');
79
+ if (errors.length) { console.error(errors.map(e => `✗ ${e}`).join('\n')); process.exit(1); }
80
+ console.log(`✓ ${file}`);
81
+ process.exit(0);
82
+ }
83
+
84
+ for (const dir of skillDirs) {
85
+ const file = join(root, 'skills', dir, 'SKILL.md');
86
+ if (!existsSync(file)) { errors.push(`skill ${dir}: missing SKILL.md`); continue; }
87
+ const data = checkFile(file, dir, 'skill');
88
+ if (data) {
89
+ if (names.has(data.name)) errors.push(`duplicate name: ${data.name}`);
90
+ names.set(data.name, file);
91
+ }
92
+ }
93
+
94
+ const agentDir = join(root, 'agents');
95
+ const agentFiles = existsSync(agentDir) ? readdirSync(agentDir).filter(f => f.endsWith('.md')) : [];
96
+ for (const f of agentFiles) {
97
+ const data = checkFile(join(agentDir, f), f.replace(/\.md$/, ''), 'agent');
98
+ if (!data) continue;
99
+ if (names.has(data.name)) errors.push(`duplicate name: ${data.name}`);
100
+ names.set(data.name, f);
101
+ const skills = (data.skills ?? '').split(',').map(s => s.trim()).filter(Boolean);
102
+ if (skills.length === 0) errors.push(`agent ${f}: skills field missing/empty`);
103
+ for (const s of skills) {
104
+ usedSkills.add(s);
105
+ if (!skillDirs.includes(s)) errors.push(`agent ${f}: unknown skill "${s}"`);
106
+ }
107
+ }
108
+
109
+ for (const dir of skillDirs) {
110
+ if (dir !== 'mugiwara-workflow' && !usedSkills.has(dir)) errors.push(`skill ${dir}: not referenced by any agent`);
111
+ }
112
+
113
+ // --- manifest-sync check: .claude-plugin/plugin.json must set-equal content/ ---
114
+ const manifestArg = process.argv.indexOf('--check-manifest');
115
+ if (manifestArg !== -1) {
116
+ const manifests = ['.claude-plugin/plugin.json'];
117
+ let manifestErrors = 0;
118
+
119
+ for (const mp of manifests) {
120
+ const mpath = join(import.meta.dirname, '..', mp);
121
+ if (!existsSync(mpath)) { errors.push(`manifest ${mp}: file not found`); continue; }
122
+ let mdata;
123
+ try { mdata = JSON.parse(readFileSync(mpath, 'utf8')); }
124
+ catch (e) { errors.push(`manifest ${mp}: invalid JSON`); continue; }
125
+
126
+ const mSkills = mdata?.metadata?.skills;
127
+ const mAgents = mdata?.metadata?.agents;
128
+ if (!Array.isArray(mSkills)) { errors.push(`manifest ${mp}: missing metadata.skills array`); }
129
+ if (!Array.isArray(mAgents)) { errors.push(`manifest ${mp}: missing metadata.agents array`); }
130
+ if (!mSkills || !mAgents) continue;
131
+
132
+ const mSkillSet = new Set(mSkills);
133
+ const mAgentSet = new Set(mAgents);
134
+
135
+ // every skill in content/ must be in manifest
136
+ for (const dir of skillDirs) {
137
+ if (!mSkillSet.has(dir)) {
138
+ errors.push(`manifest ${mp}: missing skill "${dir}"`);
139
+ manifestErrors++;
140
+ }
141
+ }
142
+ // no stale entries in manifest
143
+ for (const s of mSkills) {
144
+ if (!skillDirs.includes(s)) {
145
+ errors.push(`manifest ${mp}: stale skill "${s}" (not in content/skills/)`);
146
+ manifestErrors++;
147
+ }
148
+ }
149
+ // every agent in content/ must be in manifest
150
+ for (const f of agentFiles) {
151
+ const name = f.replace(/\.md$/, '');
152
+ if (!mAgentSet.has(name)) {
153
+ errors.push(`manifest ${mp}: missing agent "${name}"`);
154
+ manifestErrors++;
155
+ }
156
+ }
157
+ for (const a of mAgents) {
158
+ if (!agentFiles.includes(`${a}.md`)) {
159
+ errors.push(`manifest ${mp}: stale agent "${a}" (not in content/agents/)`);
160
+ manifestErrors++;
161
+ }
162
+ }
163
+ }
164
+
165
+ if (manifestErrors === 0) console.log('✓ manifest in sync with content/');
166
+ }
167
+
168
+ // --- index budget gate ---
169
+ const INDEX_BUDGET = 5500; // chars, all skill + agent descriptions combined
170
+ let totalDescChars = 0;
171
+ for (const dir of skillDirs) {
172
+ const file = join(root, 'skills', dir, 'SKILL.md');
173
+ if (!existsSync(file)) continue;
174
+ const parsed = parseFrontmatter(readFileSync(file, 'utf8'));
175
+ totalDescChars += (parsed.data.description ?? '').length;
176
+ }
177
+ for (const f of agentFiles) {
178
+ const parsed = parseFrontmatter(readFileSync(join(agentDir, f), 'utf8'));
179
+ totalDescChars += (parsed.data.description ?? '').length;
180
+ }
181
+ if (totalDescChars > INDEX_BUDGET) {
182
+ errors.push(`index budget exceeded: ${totalDescChars}/${INDEX_BUDGET} chars (skill + agent descriptions)`);
183
+ } else {
184
+ console.log(`✓ index budget: ${totalDescChars}/${INDEX_BUDGET} chars`);
185
+ }
186
+
187
+ // --- docs-drift check: docs/skills.md and docs/agents.md must reference all content/ entries ---
188
+ const docsArg = process.argv.indexOf('--check-docs');
189
+ if (docsArg !== -1) {
190
+ const docsDir = join(import.meta.dirname, '..', 'docs');
191
+ const skillsDoc = join(docsDir, 'skills.md');
192
+ const agentsDoc = join(docsDir, 'agents.md');
193
+ let docErrors = 0;
194
+
195
+ if (existsSync(skillsDoc)) {
196
+ const content = readFileSync(skillsDoc, 'utf8');
197
+ for (const dir of skillDirs) {
198
+ if (!content.includes(dir)) {
199
+ errors.push(`docs/skills.md: missing skill "${dir}"`);
200
+ docErrors++;
201
+ }
202
+ }
203
+ } else {
204
+ errors.push('docs/skills.md: file not found');
205
+ docErrors++;
206
+ }
207
+
208
+ if (existsSync(agentsDoc)) {
209
+ const content = readFileSync(agentsDoc, 'utf8');
210
+ for (const f of agentFiles) {
211
+ const name = f.replace(/\.md$/, '');
212
+ if (!content.includes(name)) {
213
+ errors.push(`docs/agents.md: missing agent "${name}"`);
214
+ docErrors++;
215
+ }
216
+ }
217
+ } else {
218
+ errors.push('docs/agents.md: file not found');
219
+ docErrors++;
220
+ }
221
+
222
+ if (docErrors === 0) console.log('✓ docs in sync with content/');
223
+ }
224
+
225
+ if (errors.length) { console.error(errors.map(e => `✗ ${e}`).join('\n')); process.exit(1); }
226
+ console.log(`✓ content valid: ${skillDirs.length} skills, ${agentFiles.length} agents`);
package/src/args.ts CHANGED
@@ -9,7 +9,8 @@ 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', '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version',
12
+ '--dry-run': 'dryRun', '--keep-logs': 'keepLogs',
13
+ '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version',
13
14
  };
14
15
 
15
16
  export function parseArgs(argv: string[]): Args {
package/src/cli.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  // src/cli.ts
3
- import { existsSync, readFileSync, readdirSync, realpathSync, rmSync } from 'node:fs';
3
+ import { existsSync, readFileSync, realpathSync, rmSync } from 'node:fs';
4
4
  import { homedir } from 'node:os';
5
- import { join, resolve } from 'node:path';
5
+ import { resolve } from 'node:path';
6
6
  import { pathToFileURL } 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, CONTENT_DIR } from './installer.ts';
10
+ import { installTo, removeInstalled, VERSION } from './installer.ts';
11
11
  import { manifestPath, readManifest, writeManifest, type Scope } from './manifest.ts';
12
- import { parseFrontmatter } from './frontmatter.ts';
12
+ import { resetMission } 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;
@@ -23,11 +23,24 @@ export async function run(argv: string[]): Promise<void> {
23
23
  case 'update': return install({ ...flags, force: true });
24
24
  case 'uninstall': return uninstall(flags);
25
25
  case 'list': return list(flags);
26
- case 'skills': return skills();
26
+ case 'reset': return resetCmd(flags);
27
27
  default: throw new Error(`Unknown command: ${command}`);
28
28
  }
29
29
  }
30
30
 
31
+ function resetCmd(flags: Args['flags']): void {
32
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
33
+ const force = flag(flags.force);
34
+ const result = resetMission(projectDir, flag(flags.keepLogs), force);
35
+ if (result.blocked) {
36
+ console.error(`✗ ${result.blocked}`);
37
+ process.exit(1);
38
+ }
39
+ if (result.removed.length) console.log(`removed: ${result.removed.join(', ')}`);
40
+ else console.log('nothing to remove.');
41
+ if (result.kept.length) console.log(`kept: ${result.kept.join(', ')}`);
42
+ }
43
+
31
44
  async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; projectDir: string; targetIds: string[] }> {
32
45
  const interactive = !flag(flags.yes);
33
46
  const rl = interactive ? createRl() : null;
@@ -126,19 +139,6 @@ function list(flags: Args['flags']): void {
126
139
  if (!found) console.log('No mugiwara installation found.');
127
140
  }
128
141
 
129
- function skills(): void {
130
- const dir = join(CONTENT_DIR, 'skills');
131
- const names = readdirSync(dir, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name).sort();
132
- const rows = names.map(name => {
133
- const { data } = parseFrontmatter(readFileSync(join(dir, name, 'SKILL.md'), 'utf8'));
134
- return [name, data.description ?? ''] as const;
135
- });
136
- const w = Math.max(...rows.map(r => r[0].length)) + 2;
137
- console.log(`mugiwara ${VERSION} — ${rows.length} skills (agentskills.io format):\n`);
138
- for (const [name, description] of rows) console.log(` ${name.padEnd(w)}${description}`);
139
- console.log(`\nInstall skills into any agent via skills.sh:\n npx skills add ionivetech/mugiwara`);
140
- }
141
-
142
142
  function help(): void {
143
143
  console.log(`mugiwara ${VERSION} — the Straw Hat crew for AI agents
144
144
 
@@ -147,7 +147,7 @@ Usage:
147
147
  mugiwara update replace existing files (backs up differences first)
148
148
  mugiwara uninstall remove installed files via manifest
149
149
  mugiwara list show installations
150
- mugiwara skills list installable skills (agentskills.io)
150
+ mugiwara reset wipe mission state (spec/plans/results/review/issues[/logs])
151
151
  mugiwara --help this help
152
152
  mugiwara --version print version
153
153
 
@@ -157,7 +157,8 @@ Flags:
157
157
  --target <ids|all> comma-separated: ${TARGET_IDS.join(', ')}
158
158
  --yes, -y non-interactive (needs --global/--project, --target)
159
159
  --force overwrite differing files (with backup)
160
- --dry-run print actions without writing`);
160
+ --dry-run print actions without writing
161
+ --keep-logs with reset: keep .mugiwara/logs (lessons ledger survives)`);
161
162
  }
162
163
 
163
164
  let entry = process.argv[1] !== undefined ? resolve(process.argv[1]) : undefined;