@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.
- package/README.md +231 -371
- package/content/agents/brook-healing.md +2 -2
- package/content/agents/chopper-checkpoint.md +2 -1
- package/content/agents/eval-runner.md +4 -4
- package/content/agents/franky-gates.md +1 -1
- package/content/agents/jinbe-security.md +2 -1
- package/content/agents/luffy-orchestrator.md +3 -3
- package/content/agents/memory-keeper.md +1 -1
- package/content/agents/nami-planner.md +2 -2
- package/content/agents/resume-coordinator.md +8 -9
- package/content/agents/robin-reviewer.md +3 -2
- package/content/agents/sanji-quality.md +1 -1
- package/content/agents/skeptic-verifier.md +4 -3
- package/content/agents/using-mugiwara.md +3 -3
- package/content/agents/usopp-brainstorm.md +3 -3
- package/content/agents/zoro-execution.md +4 -3
- package/content/skills/mugiwara-agent-security/SKILL.md +14 -2
- package/content/skills/mugiwara-backend/SKILL.md +2 -8
- package/content/skills/mugiwara-backend/references/source-backed-example.md +77 -0
- package/content/skills/mugiwara-brainstorm/SKILL.md +2 -2
- package/content/skills/mugiwara-checkpoint/SKILL.md +2 -2
- package/content/skills/mugiwara-checkpoint/references/ledger-format.md +34 -0
- package/content/skills/{mugiwara-doubt-driven-development → mugiwara-claim-audit}/SKILL.md +2 -2
- package/content/skills/{mugiwara-context-engineering → mugiwara-context-budget}/SKILL.md +2 -2
- package/content/skills/mugiwara-context-budget/references/context-budget.md +37 -0
- package/content/skills/{mugiwara-api-and-interface-design → mugiwara-contract-first}/SKILL.md +4 -2
- package/content/skills/mugiwara-contract-first/references/versioning-playbook.md +36 -0
- package/content/skills/mugiwara-execution/SKILL.md +4 -2
- package/content/skills/mugiwara-execution/references/worked-example.md +88 -0
- package/content/skills/mugiwara-frontend/SKILL.md +13 -14
- package/content/skills/mugiwara-frontend/references/design-tokens.md +25 -0
- package/content/skills/mugiwara-frontend/references/slop-catalog.md +19 -0
- package/content/skills/mugiwara-gates/SKILL.md +4 -4
- package/content/skills/mugiwara-git/SKILL.md +1 -1
- package/content/skills/mugiwara-healing/SKILL.md +34 -7
- package/content/skills/mugiwara-healing/references/failure-taxonomy.md +23 -0
- package/content/skills/mugiwara-lessons/SKILL.md +1 -1
- package/content/skills/mugiwara-orchestration/SKILL.md +5 -5
- package/content/skills/mugiwara-planning/SKILL.md +2 -2
- package/content/skills/mugiwara-planning/references/anti-patterns.md +17 -0
- package/content/skills/mugiwara-planning/references/plan-template.md +46 -0
- package/content/skills/mugiwara-pr/SKILL.md +1 -1
- package/content/skills/{mugiwara-test-driven-development → mugiwara-proof-order}/SKILL.md +2 -2
- package/content/skills/mugiwara-proof-order/references/proof-order-examples.md +62 -0
- package/content/skills/mugiwara-quality/SKILL.md +3 -3
- package/content/skills/mugiwara-resume/SKILL.md +44 -33
- package/content/skills/mugiwara-review/SKILL.md +1 -1
- package/content/skills/mugiwara-review/references/five-axis-worksheet.md +36 -0
- package/content/skills/mugiwara-review/references/severity-rubric.md +20 -0
- package/content/skills/{mugiwara-systematic-debugging → mugiwara-root-cause}/SKILL.md +4 -2
- package/content/skills/mugiwara-root-cause/references/four-phase-worked.md +71 -0
- package/content/skills/mugiwara-security/SKILL.md +1 -1
- package/content/skills/mugiwara-security/references/owasp-mapping.md +30 -0
- package/content/skills/mugiwara-security/references/stride-worksheet.md +37 -0
- package/content/skills/mugiwara-ship/SKILL.md +1 -1
- package/content/skills/{mugiwara-deprecation → mugiwara-sunset}/SKILL.md +2 -2
- package/content/skills/mugiwara-testcases/SKILL.md +1 -1
- package/content/skills/mugiwara-testcases/references/intake-formats.md +42 -0
- package/content/skills/mugiwara-workflow/SKILL.md +20 -23
- package/dist/mugiwara.js +37 -8
- package/docs/agents.md +2 -2
- package/docs/audit-trail.md +65 -0
- package/docs/comparison.md +43 -170
- package/docs/compliance-matrix.md +81 -0
- package/docs/config.md +4 -2
- package/docs/cost.md +45 -0
- package/docs/enforcement.md +32 -35
- package/docs/getting-started.md +103 -41
- package/docs/harness-matrix.md +41 -0
- package/docs/lanes.md +65 -23
- package/docs/skill-anatomy.md +42 -54
- package/docs/skills.md +36 -54
- package/evals/cases/positive-refactor-existing-tests.json +1 -1
- package/evals/cases/routing-bug-one-file.json +1 -1
- package/package.json +5 -4
- package/references/definition-of-done.md +42 -0
- package/references/multi-actor.md +44 -0
- package/references/skill-versioning.md +44 -0
- package/references/source-grounding.md +23 -0
- package/references/token-budget.md +34 -0
- package/scripts/evidence.sh +37 -0
- package/scripts/lane.sh +74 -0
- package/scripts/mission-report.sh +88 -0
- package/scripts/release-notes.ts +65 -0
- package/scripts/retrieval-eval.ts +152 -0
- package/scripts/run-evals.ts +113 -0
- package/scripts/savepoint.sh +165 -0
- package/scripts/sync-version.ts +30 -0
- package/scripts/validate-content.ts +226 -0
- package/src/cli.ts +8 -3
- package/src/mission.ts +26 -3
- package/content/skills/mugiwara-dynamic-workflow/SKILL.md +0 -90
- package/content/skills/mugiwara-eval/SKILL.md +0 -87
- package/content/skills/mugiwara-git-worktrees/SKILL.md +0 -67
- package/content/skills/mugiwara-mode/SKILL.md +0 -77
- package/content/skills/mugiwara-observability/SKILL.md +0 -59
- 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
|
|
34
|
-
|
|
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
|
-
|
|
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'); }
|