@ionivetech/mugiwara 0.4.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 (97) hide show
  1. package/README.md +231 -371
  2. package/content/agents/brook-healing.md +2 -2
  3. package/content/agents/chopper-checkpoint.md +2 -1
  4. package/content/agents/eval-runner.md +4 -4
  5. package/content/agents/franky-gates.md +1 -1
  6. package/content/agents/jinbe-security.md +2 -1
  7. package/content/agents/luffy-orchestrator.md +3 -3
  8. package/content/agents/memory-keeper.md +1 -1
  9. package/content/agents/nami-planner.md +2 -2
  10. package/content/agents/resume-coordinator.md +8 -9
  11. package/content/agents/robin-reviewer.md +3 -2
  12. package/content/agents/sanji-quality.md +1 -1
  13. package/content/agents/skeptic-verifier.md +4 -3
  14. package/content/agents/using-mugiwara.md +3 -3
  15. package/content/agents/usopp-brainstorm.md +3 -3
  16. package/content/agents/zoro-execution.md +4 -3
  17. package/content/skills/mugiwara-agent-security/SKILL.md +14 -2
  18. package/content/skills/mugiwara-backend/SKILL.md +2 -8
  19. package/content/skills/mugiwara-backend/references/source-backed-example.md +77 -0
  20. package/content/skills/mugiwara-brainstorm/SKILL.md +2 -2
  21. package/content/skills/mugiwara-checkpoint/SKILL.md +2 -2
  22. package/content/skills/mugiwara-checkpoint/references/ledger-format.md +34 -0
  23. package/content/skills/{mugiwara-doubt-driven-development → mugiwara-claim-audit}/SKILL.md +2 -2
  24. package/content/skills/{mugiwara-context-engineering → mugiwara-context-budget}/SKILL.md +2 -2
  25. package/content/skills/mugiwara-context-budget/references/context-budget.md +37 -0
  26. package/content/skills/{mugiwara-api-and-interface-design → mugiwara-contract-first}/SKILL.md +4 -2
  27. package/content/skills/mugiwara-contract-first/references/versioning-playbook.md +36 -0
  28. package/content/skills/mugiwara-execution/SKILL.md +4 -2
  29. package/content/skills/mugiwara-execution/references/worked-example.md +88 -0
  30. package/content/skills/mugiwara-frontend/SKILL.md +13 -14
  31. package/content/skills/mugiwara-frontend/references/design-tokens.md +25 -0
  32. package/content/skills/mugiwara-frontend/references/slop-catalog.md +19 -0
  33. package/content/skills/mugiwara-gates/SKILL.md +4 -4
  34. package/content/skills/mugiwara-git/SKILL.md +1 -1
  35. package/content/skills/mugiwara-healing/SKILL.md +34 -7
  36. package/content/skills/mugiwara-healing/references/failure-taxonomy.md +23 -0
  37. package/content/skills/mugiwara-lessons/SKILL.md +1 -1
  38. package/content/skills/mugiwara-orchestration/SKILL.md +5 -5
  39. package/content/skills/mugiwara-planning/SKILL.md +2 -2
  40. package/content/skills/mugiwara-planning/references/anti-patterns.md +17 -0
  41. package/content/skills/mugiwara-planning/references/plan-template.md +46 -0
  42. package/content/skills/mugiwara-pr/SKILL.md +1 -1
  43. package/content/skills/{mugiwara-test-driven-development → mugiwara-proof-order}/SKILL.md +2 -2
  44. package/content/skills/mugiwara-proof-order/references/proof-order-examples.md +62 -0
  45. package/content/skills/mugiwara-quality/SKILL.md +3 -3
  46. package/content/skills/mugiwara-resume/SKILL.md +44 -33
  47. package/content/skills/mugiwara-review/SKILL.md +1 -1
  48. package/content/skills/mugiwara-review/references/five-axis-worksheet.md +36 -0
  49. package/content/skills/mugiwara-review/references/severity-rubric.md +20 -0
  50. package/content/skills/{mugiwara-systematic-debugging → mugiwara-root-cause}/SKILL.md +4 -2
  51. package/content/skills/mugiwara-root-cause/references/four-phase-worked.md +71 -0
  52. package/content/skills/mugiwara-security/SKILL.md +1 -1
  53. package/content/skills/mugiwara-security/references/owasp-mapping.md +30 -0
  54. package/content/skills/mugiwara-security/references/stride-worksheet.md +37 -0
  55. package/content/skills/mugiwara-ship/SKILL.md +1 -1
  56. package/content/skills/{mugiwara-deprecation → mugiwara-sunset}/SKILL.md +2 -2
  57. package/content/skills/mugiwara-testcases/SKILL.md +1 -1
  58. package/content/skills/mugiwara-testcases/references/intake-formats.md +42 -0
  59. package/content/skills/mugiwara-workflow/SKILL.md +20 -23
  60. package/dist/mugiwara.js +37 -8
  61. package/docs/agents.md +2 -2
  62. package/docs/audit-trail.md +65 -0
  63. package/docs/comparison.md +43 -170
  64. package/docs/compliance-matrix.md +81 -0
  65. package/docs/config.md +4 -2
  66. package/docs/cost.md +45 -0
  67. package/docs/enforcement.md +32 -35
  68. package/docs/getting-started.md +103 -41
  69. package/docs/harness-matrix.md +41 -0
  70. package/docs/lanes.md +65 -23
  71. package/docs/skill-anatomy.md +42 -54
  72. package/docs/skills.md +36 -54
  73. package/evals/cases/positive-refactor-existing-tests.json +1 -1
  74. package/evals/cases/routing-bug-one-file.json +1 -1
  75. package/package.json +5 -4
  76. package/references/definition-of-done.md +42 -0
  77. package/references/multi-actor.md +44 -0
  78. package/references/skill-versioning.md +44 -0
  79. package/references/source-grounding.md +23 -0
  80. package/references/token-budget.md +34 -0
  81. package/scripts/evidence.sh +37 -0
  82. package/scripts/lane.sh +74 -0
  83. package/scripts/mission-report.sh +88 -0
  84. package/scripts/release-notes.ts +65 -0
  85. package/scripts/retrieval-eval.ts +152 -0
  86. package/scripts/run-evals.ts +113 -0
  87. package/scripts/savepoint.sh +165 -0
  88. package/scripts/sync-version.ts +30 -0
  89. package/scripts/validate-content.ts +226 -0
  90. package/src/cli.ts +8 -3
  91. package/src/mission.ts +26 -3
  92. package/content/skills/mugiwara-dynamic-workflow/SKILL.md +0 -90
  93. package/content/skills/mugiwara-eval/SKILL.md +0 -87
  94. package/content/skills/mugiwara-git-worktrees/SKILL.md +0 -67
  95. package/content/skills/mugiwara-mode/SKILL.md +0 -77
  96. package/content/skills/mugiwara-observability/SKILL.md +0 -59
  97. package/content/skills/mugiwara-writing-skills/SKILL.md +0 -65
@@ -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/cli.ts CHANGED
@@ -30,10 +30,15 @@ export async function run(argv: string[]): Promise<void> {
30
30
 
31
31
  function resetCmd(flags: Args['flags']): void {
32
32
  const projectDir = resolve(str(flags.project) ?? process.cwd());
33
- const { removed, kept } = resetMission(projectDir, flag(flags.keepLogs));
34
- if (removed.length) console.log(`removed: ${removed.join(', ')}`);
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(', ')}`);
35
40
  else console.log('nothing to remove.');
36
- if (kept.length) console.log(`kept: ${kept.join(', ')}`);
41
+ if (result.kept.length) console.log(`kept: ${result.kept.join(', ')}`);
37
42
  }
38
43
 
39
44
  async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; projectDir: string; targetIds: string[] }> {
package/src/mission.ts CHANGED
@@ -1,17 +1,40 @@
1
1
  // src/mission.ts
2
2
  // Mission-state helpers for the mugiwara CLI (installer + reset only).
3
- import { existsSync, rmSync } from 'node:fs';
3
+ import { existsSync, rmSync, readFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
 
6
- export function resetMission(projectDir: string, keepLogs: boolean): { removed: string[]; kept: string[] } {
6
+ function activeActor(projectDir: string): string | null {
7
+ const stateFile = join(projectDir, '.mugiwara', 'state.json');
8
+ if (!existsSync(stateFile)) return null;
9
+ try {
10
+ const state = JSON.parse(readFileSync(stateFile, 'utf8'));
11
+ return state.actor || null;
12
+ } catch { return null; }
13
+ }
14
+
15
+ export function resetMission(projectDir: string, keepLogs: boolean, force?: boolean): { removed: string[]; kept: string[]; blocked?: string } {
7
16
  const root = join(projectDir, '.mugiwara');
8
17
  if (!existsSync(root)) return { removed: [], kept: [] };
18
+
19
+ // safe multi-actor: refuse to wipe another actor's live mission
20
+ if (!force) {
21
+ const actor = activeActor(projectDir);
22
+ if (actor) {
23
+ return { removed: [], kept: [], blocked: `Active mission for '${actor}'. Use --force to override.` };
24
+ }
25
+ }
26
+
9
27
  const removed: string[] = [];
10
28
  const kept: string[] = [];
11
- for (const dir of ['spec', 'plans', 'results', 'review', 'issues']) {
29
+ for (const dir of ['spec', 'plans', 'results', 'review', 'issues', 'reports']) {
12
30
  const p = join(root, dir);
13
31
  if (existsSync(p)) { rmSync(p, { recursive: true, force: true }); removed.push(dir); }
14
32
  }
33
+ // mission state files
34
+ for (const f of ['state.json']) {
35
+ const p = join(root, f);
36
+ if (existsSync(p)) { rmSync(p); removed.push(f); }
37
+ }
15
38
  if (!keepLogs) {
16
39
  const p = join(root, 'logs');
17
40
  if (existsSync(p)) { rmSync(p, { recursive: true, force: true }); removed.push('logs'); }