@ionivetech/mugiwara 0.5.0 → 0.5.2
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/.opencode/plugins/mugiwara.mjs +7 -11
- package/content/skills/mugiwara-context-budget/SKILL.md +1 -1
- package/content/skills/mugiwara-frontend/SKILL.md +1 -1
- package/content/skills/mugiwara-healing/SKILL.md +1 -1
- package/content/skills/mugiwara-orchestration/SKILL.md +1 -1
- package/content/skills/mugiwara-planning/SKILL.md +1 -1
- package/content/skills/mugiwara-proof-order/SKILL.md +1 -1
- package/content/skills/mugiwara-quality/SKILL.md +1 -1
- package/content/skills/mugiwara-review/SKILL.md +1 -1
- package/content/skills/mugiwara-root-cause/SKILL.md +1 -1
- package/content/skills/mugiwara-security/SKILL.md +1 -1
- package/content/skills/mugiwara-sunset/SKILL.md +1 -1
- package/content/skills/mugiwara-testcases/SKILL.md +1 -1
- package/content/skills/mugiwara-workflow/SKILL.md +1 -1
- package/dist/mugiwara.js +36 -4
- package/evals/cases/_no-skill.json +16 -0
- package/evals/cases/adversarial-pressure-fake-pass.json +21 -8
- package/evals/cases/adversarial-pressure-skip-review.json +19 -7
- package/evals/cases/lane-exploratory-vague.json +18 -6
- package/evals/cases/lane-sensitivity-payment.json +18 -6
- package/evals/cases/positive-refactor-existing-tests.json +21 -7
- package/evals/cases/positive-resume-mid-mission.json +20 -7
- package/evals/cases/routing-agent-security.json +25 -0
- package/evals/cases/routing-auth-feature.json +20 -7
- package/evals/cases/routing-backend.json +25 -0
- package/evals/cases/routing-bug-one-file.json +20 -7
- package/evals/cases/routing-claim-audit.json +25 -0
- package/evals/cases/routing-context-budget.json +25 -0
- package/evals/cases/routing-contract-first.json +25 -0
- package/evals/cases/routing-execution.json +25 -0
- package/evals/cases/routing-frontend.json +26 -0
- package/evals/cases/routing-gates.json +25 -0
- package/evals/cases/routing-git.json +25 -0
- package/evals/cases/routing-healing.json +25 -0
- package/evals/cases/routing-lessons.json +25 -0
- package/evals/cases/routing-orchestration.json +25 -0
- package/evals/cases/routing-planning.json +26 -0
- package/evals/cases/routing-pr.json +25 -0
- package/evals/cases/routing-proof-order.json +25 -0
- package/evals/cases/routing-quality.json +25 -0
- package/evals/cases/routing-ship.json +26 -0
- package/evals/cases/routing-sunset.json +25 -0
- package/evals/cases/routing-workflow.json +25 -0
- package/evals/floor.json +6 -0
- package/package.json +2 -1
- package/scripts/probe.ts +40 -0
- package/scripts/retrieval-eval.ts +178 -69
- package/scripts/run-evals.ts +56 -20
- package/scripts/savepoint.sh +5 -4
- package/src/targets/opencode.ts +40 -3
- package/evals/cases/negative-secrets-typo.json +0 -12
- package/evals/cases/negative-security-docs-change.json +0 -12
- package/evals/cases/routing-typo.json +0 -13
|
@@ -24,25 +24,25 @@ function tokenize(text: string): string[] {
|
|
|
24
24
|
function buildIndex(): Index {
|
|
25
25
|
const index: Index = { terms: new Map(), docs: new Map(), docCount: 0 };
|
|
26
26
|
const dirs = readdirSync(skillsDir).filter(d => statSync(join(skillsDir, d)).isDirectory());
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
for (const dir of dirs) {
|
|
29
29
|
const file = join(skillsDir, dir, 'SKILL.md');
|
|
30
30
|
if (!existsSync(file)) continue;
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
const { data } = parseFrontmatter(readFileSync(file, 'utf8'));
|
|
33
33
|
const desc = data.description ?? '';
|
|
34
34
|
const tokens = tokenize(desc);
|
|
35
35
|
const tf = new Map<string, number>();
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
for (const t of tokens) {
|
|
38
38
|
tf.set(t, (tf.get(t) || 0) + 1);
|
|
39
39
|
index.terms.set(t, (index.terms.get(t) || 0) + 1);
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
index.docs.set(dir, tf);
|
|
43
43
|
index.docCount++;
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
return index;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -56,7 +56,7 @@ function tfidf(index: Index, doc: string, term: string): number {
|
|
|
56
56
|
function score(index: Index, prompt: string): { skill: string; score: number }[] {
|
|
57
57
|
const promptTerms = tokenize(prompt);
|
|
58
58
|
const results: { skill: string; score: number }[] = [];
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
for (const [doc] of index.docs) {
|
|
61
61
|
let total = 0;
|
|
62
62
|
for (const t of promptTerms) {
|
|
@@ -64,89 +64,198 @@ function score(index: Index, prompt: string): { skill: string; score: number }[]
|
|
|
64
64
|
}
|
|
65
65
|
results.push({ skill: doc, score: total });
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
return results.sort((a, b) => b.score - a.score);
|
|
67
|
+
|
|
68
|
+
return results.sort((a, b) => b.score - a.score || a.skill.localeCompare(b.skill));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// ---
|
|
72
|
-
interface
|
|
73
|
-
|
|
71
|
+
// --- case schema ---
|
|
72
|
+
interface CaseFile {
|
|
73
|
+
name: string;
|
|
74
|
+
skill: string;
|
|
75
|
+
type?: string;
|
|
76
|
+
task: string;
|
|
77
|
+
rubric: string[];
|
|
78
|
+
expect_lane?: string;
|
|
79
|
+
trigger?: {
|
|
80
|
+
positive?: { prompt: string; top_k?: number }[];
|
|
81
|
+
negative?: { prompt: string }[];
|
|
82
|
+
};
|
|
83
|
+
behavioral?: { task: string; rubric: string[] }[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface Probe {
|
|
87
|
+
kind: 'positive' | 'negative';
|
|
74
88
|
skill: string;
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
prompt: string;
|
|
90
|
+
topK: number;
|
|
77
91
|
}
|
|
78
92
|
|
|
93
|
+
// --- build index ---
|
|
79
94
|
const index = buildIndex();
|
|
80
95
|
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
// --- load cases ---
|
|
97
|
+
const skills = readdirSync(skillsDir).filter(d => statSync(join(skillsDir, d)).isDirectory());
|
|
98
|
+
const files = readdirSync(evalsDir).filter(f => f.endsWith('.json'));
|
|
99
|
+
const probes: Probe[] = [];
|
|
100
|
+
const noSkillProbes: Probe[] = [];
|
|
101
|
+
const covered = new Set<string>();
|
|
102
|
+
|
|
103
|
+
for (const f of files) {
|
|
104
|
+
let c: CaseFile;
|
|
105
|
+
try {
|
|
106
|
+
c = JSON.parse(readFileSync(join(evalsDir, f), 'utf8'));
|
|
107
|
+
} catch (e) {
|
|
108
|
+
throw new Error(`eval case ${f} is not valid JSON: ${e}`);
|
|
92
109
|
}
|
|
93
|
-
}
|
|
110
|
+
if (!c.skill) throw new Error(`eval case ${f} has no "skill"`);
|
|
111
|
+
covered.add(c.skill);
|
|
94
112
|
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
];
|
|
113
|
+
const target = c.skill === '_no-skill' ? noSkillProbes : probes;
|
|
114
|
+
for (const p of c.trigger?.positive ?? [])
|
|
115
|
+
target.push({ kind: 'positive', skill: c.skill, prompt: p.prompt, topK: p.top_k ?? 3 });
|
|
116
|
+
for (const p of c.trigger?.negative ?? [])
|
|
117
|
+
target.push({ kind: 'negative', skill: c.skill, prompt: p.prompt, topK: 3 });
|
|
118
|
+
}
|
|
102
119
|
|
|
103
|
-
|
|
120
|
+
// --- coverage gate ---
|
|
121
|
+
const missing = skills.filter(s => !covered.has(s));
|
|
122
|
+
if (missing.length) {
|
|
123
|
+
console.error(`missing eval cases for ${missing.length} skills:`);
|
|
124
|
+
for (const s of missing) console.error(` ${s}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
104
127
|
|
|
105
|
-
// --- run ---
|
|
106
|
-
let
|
|
107
|
-
|
|
128
|
+
// --- run probes ---
|
|
129
|
+
let rank1 = 0, inTopK = 0, negPass = 0, noSkillPass = 0;
|
|
130
|
+
const positives = probes.filter(p => p.kind === 'positive');
|
|
131
|
+
const negatives = probes.filter(p => p.kind === 'negative');
|
|
132
|
+
const nsPositives = noSkillProbes.filter(p => p.kind === 'positive');
|
|
133
|
+
const nsNegatives = noSkillProbes.filter(p => p.kind === 'negative');
|
|
134
|
+
const failures: string[] = [];
|
|
108
135
|
const results: Record<string, { rank: number; score: number; top_k: number; passed: boolean }> = {};
|
|
109
136
|
|
|
110
|
-
for (const
|
|
111
|
-
const
|
|
112
|
-
const rank =
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
137
|
+
for (const p of positives) {
|
|
138
|
+
const ranked = score(index, p.prompt);
|
|
139
|
+
const rank = ranked.findIndex(r => r.skill === p.skill) + 1;
|
|
140
|
+
const key = `${p.skill}: ${p.prompt}`;
|
|
141
|
+
const entryScore = rank > 0 ? ranked[rank - 1].score : 0;
|
|
142
|
+
const topScore = ranked[0]?.score ?? 0;
|
|
143
|
+
const passed = entryScore > 0 && rank > 0 && rank <= p.topK;
|
|
144
|
+
results[key] = { rank, score: entryScore, top_k: p.topK, passed };
|
|
145
|
+
if (passed && rank === 1) rank1++;
|
|
146
|
+
if (passed) inTopK++;
|
|
147
|
+
else failures.push(`positive "${p.prompt}" → ${p.skill} ranked ${rank || 'unranked'} (want <=${p.topK}), got ${ranked[0]?.skill ?? 'none'}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const p of negatives) {
|
|
151
|
+
const ranked = score(index, p.prompt);
|
|
152
|
+
const top = ranked[0];
|
|
153
|
+
const pass = (top?.score ?? 0) === 0 || top?.skill !== p.skill;
|
|
154
|
+
const key = `!${p.skill}: ${p.prompt}`;
|
|
155
|
+
results[key] = { rank: 0, score: top?.score ?? 0, top_k: 3, passed: pass };
|
|
156
|
+
if (pass) negPass++;
|
|
157
|
+
else failures.push(`negative "${p.prompt}" wrongly ranked ${p.skill} first`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const p of nsPositives) {
|
|
161
|
+
const ranked = score(index, p.prompt);
|
|
162
|
+
const top = ranked[0];
|
|
163
|
+
const pass = !top || top.score < 3.5;
|
|
164
|
+
const key = `_no-skill+: ${p.prompt}`;
|
|
165
|
+
results[key] = { rank: 0, score: top?.score ?? 0, top_k: 3, passed: pass };
|
|
166
|
+
if (pass) noSkillPass++;
|
|
167
|
+
else failures.push(`no-skill positive "${p.prompt}" → ${top.skill} score ${top.score.toFixed(2)} (want <3.5)`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const p of nsNegatives) {
|
|
171
|
+
const ranked = score(index, p.prompt);
|
|
172
|
+
const top = ranked[0];
|
|
173
|
+
const pass = !!top && top.skill.startsWith('mugiwara-') && top.score >= 3.5;
|
|
174
|
+
const key = `_no-skill-: ${p.prompt}`;
|
|
175
|
+
results[key] = { rank: 0, score: top?.score ?? 0, top_k: 3, passed: pass };
|
|
176
|
+
if (pass) noSkillPass++;
|
|
177
|
+
else failures.push(`no-skill negative "${p.prompt}" → ${top?.skill ?? 'none'} score ${top?.score?.toFixed(2) ?? '0'} (want mugiwara skill >=3.5)`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- compute rates ---
|
|
181
|
+
const rank1Rate = positives.length ? (rank1 / positives.length) * 100 : 0;
|
|
182
|
+
const topKRate = positives.length ? (inTopK / positives.length) * 100 : 0;
|
|
183
|
+
const negRate = negatives.length ? (negPass / negatives.length) * 100 : 0;
|
|
184
|
+
const nsTotal = nsPositives.length + nsNegatives.length;
|
|
185
|
+
const nsRate = nsTotal ? (noSkillPass / nsTotal) * 100 : 100;
|
|
186
|
+
|
|
187
|
+
const allPassed = Object.values(results).filter(r => r.passed).length;
|
|
188
|
+
const allFailed = Object.values(results).filter(r => !r.passed).length;
|
|
189
|
+
const totalProbes = Object.keys(results).length;
|
|
190
|
+
|
|
191
|
+
// --- floor / ratchet ---
|
|
192
|
+
const floorPath = join(root, 'evals', 'floor.json');
|
|
193
|
+
const updateFloor = process.argv.includes('--update-floor');
|
|
194
|
+
|
|
195
|
+
if (!existsSync(floorPath)) {
|
|
196
|
+
if (updateFloor) {
|
|
197
|
+
const initial = { rank1: Math.round(rank1Rate * 10) / 10, topk: Math.round(topKRate * 10) / 10, negatives: Math.round(negRate * 10) / 10, updated: new Date().toISOString().split('T')[0] };
|
|
198
|
+
writeJson(floorPath, initial);
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
const floor = JSON.parse(readFileSync(floorPath, 'utf8'));
|
|
202
|
+
const TOL = 0.5;
|
|
203
|
+
|
|
204
|
+
if (updateFloor) {
|
|
205
|
+
floor.rank1 = Math.round(rank1Rate * 10) / 10;
|
|
206
|
+
floor.topk = Math.round(topKRate * 10) / 10;
|
|
207
|
+
floor.negatives = Math.round(negRate * 10) / 10;
|
|
208
|
+
floor.updated = new Date().toISOString().split('T')[0];
|
|
209
|
+
writeJson(floorPath, floor);
|
|
121
210
|
} else {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
211
|
+
const regressions: string[] = [];
|
|
212
|
+
if (rank1Rate < floor.rank1 - TOL) regressions.push(`rank-1 ${rank1Rate.toFixed(1)}% < floor ${floor.rank1}%`);
|
|
213
|
+
if (topKRate < floor.topk - TOL) regressions.push(`top-k ${topKRate.toFixed(1)}% < floor ${floor.topk}%`);
|
|
214
|
+
if (negRate < floor.negatives - TOL) regressions.push(`negatives ${negRate.toFixed(1)}% < floor ${floor.negatives}%`);
|
|
215
|
+
|
|
216
|
+
console.log(`rank-1 ${rank1Rate.toFixed(1)}% top-3 ${topKRate.toFixed(1)}% negatives ${negRate.toFixed(1)}% no-skill ${nsRate.toFixed(1)}% (${positives.length}p / ${negatives.length}n / ${nsTotal}ns over ${skills.length} skills)`);
|
|
217
|
+
for (const f of failures) console.error(` FAIL ${f}`);
|
|
218
|
+
if (regressions.length) {
|
|
219
|
+
regressions.forEach(r => console.error(`REGRESSION: ${r}`));
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
126
222
|
}
|
|
127
223
|
}
|
|
128
224
|
|
|
129
225
|
// --- report ---
|
|
130
|
-
const rank1Count = Object.values(results).filter(r => 'rank' in r && r.rank === 1).length;
|
|
131
|
-
const total = allCases.length;
|
|
132
|
-
const rank1Rate = total > 0 ? (rank1Count / total * 100).toFixed(1) : '0';
|
|
133
|
-
|
|
134
|
-
const report = {
|
|
135
|
-
index_size: index.docCount,
|
|
136
|
-
index_terms: index.terms.size,
|
|
137
|
-
cases: total,
|
|
138
|
-
passed,
|
|
139
|
-
failed,
|
|
140
|
-
rank1_count: rank1Count,
|
|
141
|
-
rank1_rate: `${rank1Rate}%`,
|
|
142
|
-
results,
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
// Output JSON for CI
|
|
146
226
|
const ciArg = process.argv.indexOf('--json');
|
|
147
227
|
if (ciArg !== -1) {
|
|
228
|
+
const report = {
|
|
229
|
+
index_size: index.docCount,
|
|
230
|
+
index_terms: index.terms.size,
|
|
231
|
+
probes: totalProbes,
|
|
232
|
+
positives: positives.length,
|
|
233
|
+
negatives: negatives.length,
|
|
234
|
+
no_skill: nsTotal,
|
|
235
|
+
rank1_count: rank1,
|
|
236
|
+
rank1_rate: `${rank1Rate.toFixed(1)}%`,
|
|
237
|
+
topk_count: inTopK,
|
|
238
|
+
topk_rate: `${topKRate.toFixed(1)}%`,
|
|
239
|
+
negative_pass: negPass,
|
|
240
|
+
negative_rate: `${negRate.toFixed(1)}%`,
|
|
241
|
+
no_skill_pass: noSkillPass,
|
|
242
|
+
no_skill_rate: `${nsRate.toFixed(1)}%`,
|
|
243
|
+
passed: allPassed,
|
|
244
|
+
failed: allFailed,
|
|
245
|
+
failures: failures.length > 0 ? failures : undefined,
|
|
246
|
+
results,
|
|
247
|
+
};
|
|
148
248
|
console.log(JSON.stringify(report, null, 2));
|
|
149
249
|
} else {
|
|
150
|
-
console.log(
|
|
151
|
-
if (
|
|
250
|
+
console.log(`\nRetrieval eval: ${allPassed}/${totalProbes} passed, rank-1 ${rank1Rate.toFixed(1)}%, top-3 ${topKRate.toFixed(1)}%, neg ${negRate.toFixed(1)}%, ns ${nsRate.toFixed(1)}%`);
|
|
251
|
+
if (failures.length > 0) {
|
|
252
|
+
console.error(`\n${failures.length} failures:`);
|
|
253
|
+
for (const f of failures) console.error(` ${f}`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function writeJson(path: string, obj: object) {
|
|
259
|
+
const { writeFileSync } = require('node:fs');
|
|
260
|
+
writeFileSync(path, JSON.stringify(obj, null, 2) + '\n');
|
|
152
261
|
}
|
package/scripts/run-evals.ts
CHANGED
|
@@ -16,12 +16,16 @@ const casesDir = join(root, 'evals', 'cases');
|
|
|
16
16
|
const skillsDir = join(root, 'content', 'skills');
|
|
17
17
|
const errors: string[] = [];
|
|
18
18
|
|
|
19
|
+
type BehavioralTest = {
|
|
20
|
+
task: string;
|
|
21
|
+
rubric: string[];
|
|
22
|
+
};
|
|
23
|
+
|
|
19
24
|
type Case = {
|
|
20
25
|
name: string;
|
|
21
26
|
skill: string;
|
|
22
27
|
type?: 'positive' | 'negative' | 'adversarial' | 'lane';
|
|
23
|
-
|
|
24
|
-
rubric: string[];
|
|
28
|
+
behavioral?: BehavioralTest[];
|
|
25
29
|
expect_lane?: string;
|
|
26
30
|
};
|
|
27
31
|
|
|
@@ -40,34 +44,65 @@ function validateSuite(): Case[] {
|
|
|
40
44
|
const cases: Case[] = [];
|
|
41
45
|
const files = listCases(casesDir).sort();
|
|
42
46
|
for (const rel of files) {
|
|
43
|
-
let
|
|
44
|
-
try {
|
|
47
|
+
let raw: any;
|
|
48
|
+
try { raw = JSON.parse(readFileSync(join(casesDir, rel), 'utf8')); }
|
|
45
49
|
catch (e) { errors.push(`${rel}: invalid JSON (${(e as Error).message})`); continue; }
|
|
46
|
-
if (!
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
if (!raw.name || !raw.skill) { errors.push(`${rel}: missing name/skill`); continue; }
|
|
51
|
+
|
|
52
|
+
// skip files with no behavioral section (e.g. _no-skill.json)
|
|
53
|
+
if (!raw.behavioral || !Array.isArray(raw.behavioral) || raw.behavioral.length === 0) continue;
|
|
54
|
+
if (raw.skill === '_no-skill') continue;
|
|
55
|
+
|
|
56
|
+
// infer type from filename if not declared
|
|
57
|
+
const type = raw.type ?? (
|
|
58
|
+
rel.includes('positive') ? 'positive' :
|
|
59
|
+
rel.includes('negative') ? 'negative' :
|
|
60
|
+
rel.includes('adversarial') ? 'adversarial' :
|
|
61
|
+
rel.includes('lane') ? 'lane' :
|
|
62
|
+
undefined
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// validate skill exists
|
|
66
|
+
if (!existsSync(join(skillsDir, raw.skill)))
|
|
67
|
+
errors.push(`${rel}: unknown skill "${raw.skill}"`);
|
|
68
|
+
|
|
69
|
+
if (type && !['positive', 'negative', 'adversarial', 'lane'].includes(type))
|
|
70
|
+
errors.push(`${rel}: bad type "${type}"`);
|
|
71
|
+
|
|
72
|
+
// register one case per behavioral test
|
|
73
|
+
for (const b of raw.behavioral) {
|
|
74
|
+
if (!b.task || !Array.isArray(b.rubric) || !b.rubric.length) {
|
|
75
|
+
errors.push(`${rel}: behavioral entry missing task/nonempty rubric`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
cases.push({
|
|
79
|
+
name: raw.name,
|
|
80
|
+
skill: raw.skill,
|
|
81
|
+
type,
|
|
82
|
+
behavioral: [b],
|
|
83
|
+
expect_lane: raw.expect_lane,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
52
86
|
}
|
|
53
|
-
|
|
87
|
+
|
|
88
|
+
if (!cases.length) { errors.push('no behavioral cases in evals/cases/'); }
|
|
54
89
|
|
|
55
90
|
// coverage gates: the suite must exercise routing in all directions.
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
if (
|
|
60
|
-
if (!types.has('lane')) errors.push('need ≥1 lane case');
|
|
91
|
+
const explicitTypes = cases.filter(c => c.type);
|
|
92
|
+
const allTypes = new Set(explicitTypes.map(c => c.type).filter(Boolean));
|
|
93
|
+
if (explicitTypes.filter(c => c.type === 'adversarial').length < 2) errors.push('need >=2 adversarial cases');
|
|
94
|
+
if (!allTypes.has('lane')) errors.push('need >=1 lane case');
|
|
61
95
|
return cases;
|
|
62
96
|
}
|
|
63
97
|
|
|
64
98
|
function scoreRubric(answer: string, c: Case): { pass: number; total: number; matched: string[] } {
|
|
65
99
|
const a = answer.toLowerCase();
|
|
66
|
-
const
|
|
100
|
+
const rubric = c.behavioral?.[0]?.rubric ?? [];
|
|
101
|
+
const matched = rubric.filter(r => {
|
|
67
102
|
const terms = r.toLowerCase().split(/[^a-z0-9]+/).filter(w => w.length > 3 && !['does', 'not', 'with', 'into', 'from'].includes(w));
|
|
68
103
|
return terms.some(t => a.includes(t));
|
|
69
104
|
});
|
|
70
|
-
return { pass: matched.length, total:
|
|
105
|
+
return { pass: matched.length, total: rubric.length, matched };
|
|
71
106
|
}
|
|
72
107
|
|
|
73
108
|
async function runCases(env: { execFileSync: typeof import('node:child_process').execFileSync; bin: string; pre: string[] }): Promise<void> {
|
|
@@ -76,7 +111,8 @@ async function runCases(env: { execFileSync: typeof import('node:child_process')
|
|
|
76
111
|
const { execFileSync, bin, pre } = env;
|
|
77
112
|
let total = 0, passed = 0;
|
|
78
113
|
for (const c of cases) {
|
|
79
|
-
const
|
|
114
|
+
const task = c.behavioral?.[0]?.task ?? '';
|
|
115
|
+
const prompt = `Task: ${task}\n\nWhich mugiwara skill should run and why? Be concrete.`;
|
|
80
116
|
let raw = '';
|
|
81
117
|
try {
|
|
82
118
|
raw = execFileSync(bin, [...pre, prompt], { encoding: 'utf8', timeout: 60000, maxBuffer: 10 * 1024 * 1024 });
|
|
@@ -89,7 +125,7 @@ async function runCases(env: { execFileSync: typeof import('node:child_process')
|
|
|
89
125
|
const pct = Math.round((pass / t) * 100);
|
|
90
126
|
console.log(`${pct >= 70 ? '✓' : '✗'} ${c.name} — ${pass}/${t} rubric (${pct}%)`);
|
|
91
127
|
if (pass < t) {
|
|
92
|
-
console.log(` task: ${
|
|
128
|
+
console.log(` task: ${task}`);
|
|
93
129
|
console.log(` matched: ${matched.length ? matched.join('; ') : 'none'}`);
|
|
94
130
|
}
|
|
95
131
|
}
|
package/scripts/savepoint.sh
CHANGED
|
@@ -35,7 +35,7 @@ fi
|
|
|
35
35
|
[ -d .git ] || die "not a git repository"
|
|
36
36
|
|
|
37
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")
|
|
38
|
+
BASE_SHA=$(git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null || git merge-base HEAD "$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')" 2>/dev/null || git rev-parse HEAD~1 2>/dev/null || echo "unknown")
|
|
39
39
|
HEAD_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
|
40
40
|
|
|
41
41
|
CHANGED_FILES=$(git diff --name-only "$BASE_SHA"..HEAD 2>/dev/null || git diff --name-only --cached 2>/dev/null || true)
|
|
@@ -43,11 +43,12 @@ FILES_TOUCHED=$( [ -n "$CHANGED_FILES" ] && echo "$CHANGED_FILES" | wc -l | tr -
|
|
|
43
43
|
|
|
44
44
|
LOC_DELTA=0
|
|
45
45
|
if [ "$BASE_SHA" != "unknown" ]; then
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
STAT=$(git diff --shortstat "$BASE_SHA"..HEAD 2>/dev/null || echo "")
|
|
47
|
+
INS=$(echo "$STAT" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)
|
|
48
|
+
DEL=$(echo "$STAT" | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo 0)
|
|
49
|
+
LOC_DELTA=$(( ${INS:-0} - ${DEL:-0} ))
|
|
48
50
|
fi
|
|
49
51
|
[ -z "$LOC_DELTA" ] && LOC_DELTA=0
|
|
50
|
-
[ "$LOC_DELTA" = "0" ] || LOC_DELTA=${LOC_DELTA//[^0-9-]/}
|
|
51
52
|
|
|
52
53
|
SENSITIVE_PATTERNS="auth/|payment/|billing/|crypto/|secrets/|\.env|config/|migration/|\.sql$|schema\.|\.prisma$"
|
|
53
54
|
SENSITIVE_PATHS=$(echo "$CHANGED_FILES" | grep -E "$SENSITIVE_PATTERNS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
|
package/src/targets/opencode.ts
CHANGED
|
@@ -3,6 +3,44 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { stringifyFrontmatter, type FrontmatterData } from '../frontmatter.ts';
|
|
4
4
|
import type { Target } from '../installer.ts';
|
|
5
5
|
|
|
6
|
+
type CrewConfig = {
|
|
7
|
+
color: string;
|
|
8
|
+
temperature: number;
|
|
9
|
+
steps: number;
|
|
10
|
+
permission?: Record<string, string>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const CREW: Record<string, CrewConfig> = {
|
|
14
|
+
'using-mugiwara': { color: '#84cc16', temperature: 0.2, steps: 10 },
|
|
15
|
+
'luffy-orchestrator': { color: '#ef4444', temperature: 0.2, steps: 15 },
|
|
16
|
+
'usopp-brainstorm': { color: '#f59e0b', temperature: 0.6, steps: 15 },
|
|
17
|
+
'nami-planner': { color: '#f97316', temperature: 0.2, steps: 15 },
|
|
18
|
+
'zoro-execution': { color: '#22c55e', temperature: 0.1, steps: 30 },
|
|
19
|
+
'chopper-checkpoint': { color: '#3b82f6', temperature: 0.1, permission: { edit: 'deny' }, steps: 15 },
|
|
20
|
+
'sanji-quality': { color: '#a855f7', temperature: 0.1, permission: { edit: 'deny' }, steps: 10 },
|
|
21
|
+
'franky-gates': { color: '#06b6d4', temperature: 0.1, permission: { edit: 'deny' }, steps: 10 },
|
|
22
|
+
'robin-reviewer': { color: '#8b5cf6', temperature: 0.2, permission: { edit: 'deny' }, steps: 15 },
|
|
23
|
+
'jinbe-security': { color: '#6366f1', temperature: 0.2, permission: { edit: 'deny' }, steps: 15 },
|
|
24
|
+
'brook-healing': { color: '#ec4899', temperature: 0.1, steps: 20 },
|
|
25
|
+
'skeptic-verifier': { color: '#64748b', temperature: 0.1, permission: { edit: 'deny' }, steps: 12 },
|
|
26
|
+
'eval-runner': { color: '#14b8a6', temperature: 0.2, steps: 15 },
|
|
27
|
+
'resume-coordinator': { color: '#d97706', temperature: 0.2, steps: 10 },
|
|
28
|
+
'memory-keeper': { color: '#d946ef', temperature: 0.2, steps: 8 },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function agentFrontmatter(name: string, description: string) {
|
|
32
|
+
const crew = CREW[name];
|
|
33
|
+
const lines = [`description: ${description}`, `mode: all`];
|
|
34
|
+
if (crew) {
|
|
35
|
+
lines.push(`color: '${crew.color}'`, `temperature: ${crew.temperature}`, `steps: ${crew.steps}`);
|
|
36
|
+
if (crew.permission) {
|
|
37
|
+
lines.push('permission:');
|
|
38
|
+
for (const [k, v] of Object.entries(crew.permission)) lines.push(` ${k}: ${v}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
6
44
|
export const target: Target = {
|
|
7
45
|
id: 'opencode',
|
|
8
46
|
label: 'opencode',
|
|
@@ -18,9 +56,8 @@ export const target: Target = {
|
|
|
18
56
|
};
|
|
19
57
|
},
|
|
20
58
|
transformAgent(data: FrontmatterData, body: string) {
|
|
21
|
-
const fm
|
|
22
|
-
|
|
23
|
-
return { relPath: `${data.name}.md`, text: stringifyFrontmatter(fm, body) };
|
|
59
|
+
const fm = agentFrontmatter(data.name, data.description);
|
|
60
|
+
return { relPath: `${data.name}.md`, text: `---\n${fm}\n---\n${body}` };
|
|
24
61
|
},
|
|
25
62
|
refsDir({ scope, projectDir, home }, skillName: string) {
|
|
26
63
|
const root = scope === 'global' ? join(home, '.config', 'opencode') : join(projectDir, '.opencode');
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "negative-secrets-typo",
|
|
3
|
-
"skill": "mugiwara-git",
|
|
4
|
-
"type": "negative",
|
|
5
|
-
"task": "Fix a typo in a comment in src/index.ts (one word). This is a trivial change.",
|
|
6
|
-
"rubric": [
|
|
7
|
-
"does NOT rank mugiwara-security first (no trust boundary crossed)",
|
|
8
|
-
"does NOT rank mugiwara-systematic-debugging first (cause known)",
|
|
9
|
-
"treats it as lane 0 direct work"
|
|
10
|
-
],
|
|
11
|
-
"expect_lane": "direct"
|
|
12
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "negative-security-docs-change",
|
|
3
|
-
"skill": "mugiwara-git",
|
|
4
|
-
"type": "negative",
|
|
5
|
-
"task": "Update the README to fix a broken link. No code, no config, no data flow changes.",
|
|
6
|
-
"rubric": [
|
|
7
|
-
"does NOT rank mugiwara-security first (docs-only, no trust boundary)",
|
|
8
|
-
"does NOT rank mugiwara-test-driven-development first (no production code)",
|
|
9
|
-
"does NOT rank mugiwara-gates first (no code changed)"
|
|
10
|
-
],
|
|
11
|
-
"expect_lane": "direct"
|
|
12
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "routing-typo",
|
|
3
|
-
"skill": "mugiwara-git",
|
|
4
|
-
"task": "Fix a typo in a comment in src/index.ts (one word).",
|
|
5
|
-
"rubric": [
|
|
6
|
-
"treats it as lane 0 direct work",
|
|
7
|
-
"makes the one-line change without brainstorm or plan",
|
|
8
|
-
"does not invoke the crew pipeline",
|
|
9
|
-
"commits with a conventional commit message"
|
|
10
|
-
],
|
|
11
|
-
"lane": "0",
|
|
12
|
-
"expect_lane": "direct"
|
|
13
|
-
}
|