@ionivetech/mugiwara 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.opencode/mugiwara-helpers.mjs +2 -2
- package/README.md +196 -330
- package/content/agents/brook-healing.md +1 -1
- package/content/agents/franky-gates.md +1 -1
- package/content/agents/luffy-orchestrator.md +2 -2
- package/content/agents/memory-keeper.md +5 -0
- package/content/agents/usopp-brainstorm.md +3 -2
- package/content/agents/zoro-execution.md +4 -3
- package/content/skills/mugiwara-backend/SKILL.md +52 -43
- package/content/skills/mugiwara-brainstorm/SKILL.md +5 -3
- package/content/skills/mugiwara-checkpoint/SKILL.md +21 -8
- package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
- package/content/skills/mugiwara-execution/SKILL.md +34 -33
- package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
- package/content/skills/mugiwara-execution/references/execution-phase-flows.md +18 -0
- package/content/skills/mugiwara-frontend/SKILL.md +44 -44
- package/content/skills/mugiwara-gates/SKILL.md +28 -16
- package/content/skills/mugiwara-healing/SKILL.md +30 -25
- package/content/skills/mugiwara-lessons/SKILL.md +3 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +10 -9
- package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
- package/content/skills/mugiwara-planning/SKILL.md +28 -14
- package/content/skills/mugiwara-planning/references/large-campaign-subplan.md +41 -0
- package/content/skills/mugiwara-planning/references/plan-template.md +22 -0
- package/content/skills/mugiwara-quality/SKILL.md +8 -13
- package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
- package/content/skills/mugiwara-resume/SKILL.md +3 -9
- package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
- package/content/skills/mugiwara-review/SKILL.md +17 -24
- package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
- package/content/skills/mugiwara-security/SKILL.md +47 -35
- package/content/skills/mugiwara-ship/SKILL.md +2 -0
- package/content/skills/mugiwara-workflow/SKILL.md +13 -13
- package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +6 -3
- package/dist/mugiwara.js +1802 -316
- package/gemini-extension.json +1 -1
- package/hooks/mugiwara-mode-tracker.js +24 -4
- package/hooks/mugiwara-mode-tracker.ts +36 -7
- package/hooks/pipeline-guard.js +1 -1
- package/hooks/pipeline-guard.ts +2 -1
- package/hooks/session-start.js +6 -1
- package/hooks/session-start.ts +8 -1
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/cost-governor.md +104 -0
- package/references/multi-actor.md +21 -0
- package/references/posture-routing.md +31 -0
- package/references/wave-banners.md +1 -2
- package/scripts/benchmark-governor.ts +516 -0
- package/scripts/benchmark-thresholds.json +47 -0
- package/scripts/check-doc-links.ts +8 -2
- package/scripts/gate-selftest.ts +104 -21
- package/scripts/lib/lane-base.sh +4 -4
- package/scripts/retrieval-eval.ts +9 -3
- package/scripts/savepoint.sh +41 -2
- package/scripts/validate-content.ts +82 -3
- package/scripts/verify-install.ts +20 -0
- package/scripts/write-metrics.ts +73 -0
- package/src/adaptive-budget.ts +178 -0
- package/src/args.ts +3 -2
- package/src/budget.ts +18 -16
- package/src/check-artifacts.ts +45 -0
- package/src/cli.ts +221 -8
- package/src/cognition.ts +234 -0
- package/src/config.ts +113 -0
- package/src/context.ts +72 -0
- package/src/continue.ts +29 -0
- package/src/cost.ts +189 -0
- package/src/evidence.ts +160 -0
- package/src/installer.ts +2 -16
- package/src/integrity.ts +65 -16
- package/src/investigation.ts +72 -0
- package/src/mission.ts +246 -16
- package/src/policy.ts +355 -2
- package/src/posture.ts +86 -0
- package/src/provenance.ts +29 -9
- package/src/reporting.ts +225 -0
- package/src/scope.ts +321 -0
- package/src/sign.ts +234 -18
- package/src/slop.ts +306 -0
- package/src/work.ts +273 -0
package/scripts/gate-selftest.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { execSync } from 'node:child_process';
|
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync, copyFileSync, renameSync, unlinkSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
7
7
|
import { join, dirname } from 'node:path';
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
|
+
import { gatesForLane } from '../src/policy.ts';
|
|
10
|
+
import { budgetForLane } from '../src/cost.ts';
|
|
9
11
|
|
|
10
12
|
const root = join(import.meta.dirname, '..');
|
|
11
13
|
let passed = 0;
|
|
@@ -91,9 +93,11 @@ console.log('\nCost gate — measured vs stated index chars');
|
|
|
91
93
|
const costFile = join(root, 'docs', 'concepts', 'cost.md');
|
|
92
94
|
const original = readFileSync(costFile, 'utf8');
|
|
93
95
|
try {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
const costPattern = /\*\*Current:\*\* \d[\d,]* chars/;
|
|
97
|
+
const drifted = original.replace(costPattern, '**Current:** 1 chars');
|
|
98
|
+
if (!costPattern.test(original) || drifted === original) {
|
|
99
|
+
console.error('✗ COST: mutation target not found — the gate it guards may be dead.');
|
|
100
|
+
failed++;
|
|
97
101
|
} else {
|
|
98
102
|
writeFileSync(costFile, drifted);
|
|
99
103
|
assert('drifted stated index chars → exit 1', false, () => run('COST', 'bun scripts/validate-content.ts'));
|
|
@@ -134,12 +138,14 @@ if (!existsSync(savepointFile)) {
|
|
|
134
138
|
const original = readFileSync(savepointFile, 'utf8');
|
|
135
139
|
try {
|
|
136
140
|
// reintroduce the D1 defect: read lane_prev with require() of a relative path
|
|
141
|
+
const d1Pattern = /PREV_JSON=\$\(node -e "try\{const fs=require\('fs'\);const s=JSON\.parse\(fs\.readFileSync\(process\.argv\[1\],'utf8'\)\);process\.stdout\.write\(JSON\.stringify\(\{mission:s\.mission\|\|'',lane:s\.lane\|\|'',peak:s\.lane_peak\|\|''\}\)\)\}catch\(e\)\{process\.stdout\.write\('\{\}'\)\}" "\$STATE_FILE" 2>\/dev\/null \|\| true\)/;
|
|
137
142
|
const broken = original.replace(
|
|
138
|
-
|
|
143
|
+
d1Pattern,
|
|
139
144
|
"PREV_JSON=$(node -e \"try{const s=require(process.argv[1]);process.stdout.write(JSON.stringify({mission:s.mission||'',lane:s.lane||'',peak:s.lane_peak||''}))}catch(e){process.stdout.write('{}')}\" \"$STATE_FILE\" 2>/dev/null || true)"
|
|
140
145
|
);
|
|
141
|
-
if (broken === original) {
|
|
142
|
-
console.
|
|
146
|
+
if (!d1Pattern.test(original) || broken === original) {
|
|
147
|
+
console.error('✗ D1: mutation target not found — the gate it guards may be dead.');
|
|
148
|
+
failed++;
|
|
143
149
|
} else {
|
|
144
150
|
writeFileSync(savepointFile, broken);
|
|
145
151
|
assert('broken LANE_PREV resolve → lane-integrity fails', false, () => run('D1', 'bun run test -- lane-integrity -t "lane_prev"'));
|
|
@@ -158,12 +164,14 @@ if (!existsSync(savepointFile)) {
|
|
|
158
164
|
const original = readFileSync(savepointFile, 'utf8');
|
|
159
165
|
try {
|
|
160
166
|
// neuter the clamp: make lane_rank always return 0 so a drop never holds
|
|
167
|
+
const d2Pattern = /lane_rank\(\) \{\n case "\$1" in\n direct\) echo 0 ;;[\s\S]*?\n esac\n\}/;
|
|
161
168
|
const broken = original.replace(
|
|
162
|
-
|
|
169
|
+
d2Pattern,
|
|
163
170
|
'lane_rank() {\n echo 0\n}'
|
|
164
171
|
);
|
|
165
|
-
if (broken === original) {
|
|
166
|
-
console.
|
|
172
|
+
if (!d2Pattern.test(original) || broken === original) {
|
|
173
|
+
console.error('✗ D2: mutation target not found — the gate it guards may be dead.');
|
|
174
|
+
failed++;
|
|
167
175
|
} else {
|
|
168
176
|
writeFileSync(savepointFile, broken);
|
|
169
177
|
assert('broken clamp → lane-integrity fails', false, () => run('D2', 'bun run test -- lane-integrity -t "clamp"'));
|
|
@@ -183,12 +191,14 @@ if (!existsSync(patternsFile)) {
|
|
|
183
191
|
const original = readFileSync(patternsFile, 'utf8');
|
|
184
192
|
try {
|
|
185
193
|
// reintroduce the D3 defect: singular-only list (no payments/, migrations/)
|
|
194
|
+
const d3Pattern = /SENSITIVE_PATS=.*/;
|
|
186
195
|
const broken = original.replace(
|
|
187
|
-
|
|
196
|
+
d3Pattern,
|
|
188
197
|
'SENSITIVE_PATS="auth/|payment/|billing/|crypto/|secrets/|\\.env$|config/.*key|migration/|\\.sql$|schema\\.|\\.prisma$|\\.terraform|\\.tf$"'
|
|
189
198
|
);
|
|
190
|
-
if (broken === original) {
|
|
191
|
-
console.
|
|
199
|
+
if (!d3Pattern.test(original) || broken === original) {
|
|
200
|
+
console.error('✗ D3: mutation target not found — the gate it guards may be dead.');
|
|
201
|
+
failed++;
|
|
192
202
|
} else {
|
|
193
203
|
writeFileSync(patternsFile, broken);
|
|
194
204
|
assert('singular sensitive patterns → lane-integrity fails', false, () => run('D3', 'bun run test -- lane-integrity -t "payments"'));
|
|
@@ -218,10 +228,12 @@ if (!existsSync(patternsFile)) {
|
|
|
218
228
|
const live = original.match(/SENSITIVE_PATS="([^"]+)"/)?.[1] ?? '';
|
|
219
229
|
const broken = live.split('|').filter(t => !D3B_FAMILY_TOKENS.has(t)).join('|');
|
|
220
230
|
const brokenLine = `SENSITIVE_PATS="${broken}"`;
|
|
221
|
-
|
|
222
|
-
|
|
231
|
+
const d3bPattern = /SENSITIVE_PATS="[^"]*"/;
|
|
232
|
+
if (!d3bPattern.test(original) || broken === live || !live) {
|
|
233
|
+
console.error('✗ D3b: mutation target not found — the gate it guards may be dead.');
|
|
234
|
+
failed++;
|
|
223
235
|
} else {
|
|
224
|
-
writeFileSync(patternsFile, original.replace(
|
|
236
|
+
writeFileSync(patternsFile, original.replace(d3bPattern, brokenLine));
|
|
225
237
|
assert('missing new categories → lane-integrity fails', false, () => run('D3b', 'bun run test -- lane-integrity -t "sensitive-paths"'));
|
|
226
238
|
}
|
|
227
239
|
} finally {
|
|
@@ -238,12 +250,14 @@ if (!existsSync(savepointFile)) {
|
|
|
238
250
|
const original = readFileSync(savepointFile, 'utf8');
|
|
239
251
|
try {
|
|
240
252
|
// revert to delta-based (0 on deletions/refactors)
|
|
253
|
+
const d4Pattern = /LOC_TOKENS=\$\(\( LOC_CHURN \* 12 \)\)/;
|
|
241
254
|
const broken = original.replace(
|
|
242
|
-
|
|
255
|
+
d4Pattern,
|
|
243
256
|
'LOC_TOKENS=$(( LOC_DELTA > 0 ? LOC_DELTA * 12 : 0 ))'
|
|
244
257
|
);
|
|
245
|
-
if (broken === original) {
|
|
246
|
-
console.
|
|
258
|
+
if (!d4Pattern.test(original) || broken === original) {
|
|
259
|
+
console.error('✗ D4: mutation target not found — the gate it guards may be dead.');
|
|
260
|
+
failed++;
|
|
247
261
|
} else {
|
|
248
262
|
writeFileSync(savepointFile, broken);
|
|
249
263
|
assert('zero churn tokens → lane-integrity fails', false, () => run('D4', 'bun run test -- lane-integrity -t "churn"'));
|
|
@@ -263,12 +277,14 @@ if (!existsSync(savepointFile)) {
|
|
|
263
277
|
try {
|
|
264
278
|
// silently drop the continue writer block (make it a no-op). Anchor on the
|
|
265
279
|
// D10 header comment so the regex hits the writer, not the STATE_FILE if.
|
|
280
|
+
const d10Pattern = /# --- continue.*\(D10\): machine-written resume point ---[\s\S]*?\nfi\n\n/;
|
|
266
281
|
const broken = original.replace(
|
|
267
|
-
|
|
282
|
+
d10Pattern,
|
|
268
283
|
'# --- continue writer disabled (D10) ---\n\n'
|
|
269
284
|
);
|
|
270
|
-
if (broken === original) {
|
|
271
|
-
console.
|
|
285
|
+
if (!d10Pattern.test(original) || broken === original) {
|
|
286
|
+
console.error('✗ D10: mutation target not found — the gate it guards may be dead.');
|
|
287
|
+
failed++;
|
|
272
288
|
} else {
|
|
273
289
|
writeFileSync(savepointFile, broken);
|
|
274
290
|
assert('broken continue writer → savepoint fails', false, () => run('D10', 'bun run test -- savepoint -t "D10"'));
|
|
@@ -578,6 +594,26 @@ console.log('\nT7 — evidence-thin gate');
|
|
|
578
594
|
}
|
|
579
595
|
}
|
|
580
596
|
|
|
597
|
+
// --- Benchmark governor — tamper thresholds, prove harness goes red ---
|
|
598
|
+
console.log('\nBenchmark governor — thresholds tamper');
|
|
599
|
+
{
|
|
600
|
+
const threshFile = join(root, 'scripts', 'benchmark-thresholds.json');
|
|
601
|
+
if (!existsSync(threshFile)) {
|
|
602
|
+
console.log(' ⚠ benchmark-thresholds.json not found, skipping');
|
|
603
|
+
} else {
|
|
604
|
+
const original = readFileSync(threshFile, 'utf8');
|
|
605
|
+
try {
|
|
606
|
+
const data = JSON.parse(original);
|
|
607
|
+
data.workloads = data.workloads.map((w: Record<string, unknown>) => ({ ...w, projected: 0, overhead: 0 }));
|
|
608
|
+
writeFileSync(threshFile, JSON.stringify(data, null, 2));
|
|
609
|
+
assert('tampered thresholds → benchmark-governor fails', false, () => run('bench', 'bun scripts/benchmark-governor.ts'));
|
|
610
|
+
} finally {
|
|
611
|
+
writeFileSync(threshFile, original);
|
|
612
|
+
assert('restored → benchmark-governor passes', true, () => run('bench', 'bun scripts/benchmark-governor.ts'));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
581
617
|
// --- DOCLINKS: a relative .md link that does not resolve must fail the gate ---
|
|
582
618
|
console.log('\nDOCLINKS — doc link resolution');
|
|
583
619
|
{
|
|
@@ -592,5 +628,52 @@ console.log('\nDOCLINKS — doc link resolution');
|
|
|
592
628
|
}
|
|
593
629
|
}
|
|
594
630
|
|
|
631
|
+
// --- T3: lane-aware gates — direct 3 steps, full 12 steps ---
|
|
632
|
+
console.log('\nT3 — lane-aware gates');
|
|
633
|
+
{
|
|
634
|
+
const policyFile = join(root, 'src', 'policy.ts');
|
|
635
|
+
const originalPolicy = readFileSync(policyFile, 'utf8');
|
|
636
|
+
try {
|
|
637
|
+
assert('direct lane → 3 steps', true, () => gatesForLane('direct').length === 3);
|
|
638
|
+
assert('direct lane includes typecheck+build', true, () => {
|
|
639
|
+
const s = gatesForLane('direct');
|
|
640
|
+
return s.includes('typecheck') && s.includes('build');
|
|
641
|
+
});
|
|
642
|
+
assert('lean lane → 6 steps with validate-content', true, () => {
|
|
643
|
+
const s = gatesForLane('lean');
|
|
644
|
+
return s.length === 6 && s.includes('validate-content');
|
|
645
|
+
});
|
|
646
|
+
assert('standard lane → 9 steps', true, () => gatesForLane('standard').length === 9);
|
|
647
|
+
assert('full lane → 12 steps with evals/retrieval/conformance', true, () => {
|
|
648
|
+
const s = gatesForLane('full');
|
|
649
|
+
return s.length === 12 && s.includes('run-evals') && s.includes('retrieval-eval') && s.includes('conformance');
|
|
650
|
+
});
|
|
651
|
+
assert('budget direct → 0, full → 50000', true, () => budgetForLane('direct') === 0 && budgetForLane('full') === 50000);
|
|
652
|
+
assert('budget spike → 3000 (direct fixture 3k)', true, () => budgetForLane('spike') === 3000);
|
|
653
|
+
// mutation: break direct step count → should fail (file content shows not 3)
|
|
654
|
+
const broken = originalPolicy.replace(
|
|
655
|
+
"direct: ['build-hooks:check', 'typecheck', 'build']",
|
|
656
|
+
"direct: ['typecheck']"
|
|
657
|
+
);
|
|
658
|
+
if (broken !== originalPolicy) {
|
|
659
|
+
writeFileSync(policyFile, broken);
|
|
660
|
+
assert('broken direct gate → not 3 steps', false, () => readFileSync(policyFile, 'utf8').includes("direct: ['build-hooks:check', 'typecheck', 'build']"));
|
|
661
|
+
} else {
|
|
662
|
+
console.error('✗ T3: mutation target not found — the gate it guards may be dead.');
|
|
663
|
+
failed++;
|
|
664
|
+
}
|
|
665
|
+
} finally {
|
|
666
|
+
writeFileSync(policyFile, originalPolicy);
|
|
667
|
+
assert('restored → direct 3 steps', true, () => {
|
|
668
|
+
const txt = readFileSync(policyFile, 'utf8');
|
|
669
|
+
return txt.includes("direct: ['build-hooks:check', 'typecheck', 'build']");
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
assert('full still includes conformance (conformance 71→74)', true, () => {
|
|
673
|
+
const txt = readFileSync(policyFile, 'utf8');
|
|
674
|
+
return txt.includes("'conformance'") && txt.includes("full:");
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
595
678
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
596
679
|
process.exit(failed > 0 ? 1 : 0);
|
package/scripts/lib/lane-base.sh
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
# LANE_BASE: token estimate for skills/agents loaded in this lane. Derived
|
|
8
8
|
# from content word-sums × 1.35 (see scripts/lane-base.ts). spike is a
|
|
9
9
|
# resize, not a rise — small base, tiny budget.
|
|
10
|
-
LANE_BASE_lean=
|
|
11
|
-
LANE_BASE_standard=
|
|
12
|
-
LANE_BASE_full=
|
|
13
|
-
LANE_BASE_spike=
|
|
10
|
+
LANE_BASE_lean=8421
|
|
11
|
+
LANE_BASE_standard=13325
|
|
12
|
+
LANE_BASE_full=22016
|
|
13
|
+
LANE_BASE_spike=5411
|
|
14
14
|
|
|
15
15
|
# BUDGET: warn at 1.5×, stop at 3×.
|
|
16
16
|
BUDGET_lean=12000
|
|
@@ -93,14 +93,20 @@ interface Probe {
|
|
|
93
93
|
// --- build index ---
|
|
94
94
|
const index = buildIndex();
|
|
95
95
|
|
|
96
|
-
// --- load cases ---
|
|
96
|
+
// --- load cases (root + retrieval/ subdir for per-skill probes) ---
|
|
97
97
|
const skills = readdirSync(skillsDir).filter(d => statSync(join(skillsDir, d)).isDirectory());
|
|
98
|
-
const
|
|
98
|
+
const rootCases = readdirSync(evalsDir).filter(f => f.endsWith('.json'));
|
|
99
|
+
const retrievalDir = join(evalsDir, 'retrieval');
|
|
100
|
+
const retrievalCases = existsSync(retrievalDir)
|
|
101
|
+
? readdirSync(retrievalDir).filter(f => f.endsWith('.json')).map(f => join('retrieval', f))
|
|
102
|
+
: [];
|
|
103
|
+
const files = [...rootCases, ...retrievalCases];
|
|
99
104
|
const probes: Probe[] = [];
|
|
100
105
|
const noSkillProbes: Probe[] = [];
|
|
101
106
|
const covered = new Set<string>();
|
|
102
107
|
|
|
103
|
-
for (const
|
|
108
|
+
for (const rel of files) {
|
|
109
|
+
const f = rel.includes('/') ? rel : rel;
|
|
104
110
|
let c: CaseFile;
|
|
105
111
|
try {
|
|
106
112
|
c = JSON.parse(readFileSync(join(evalsDir, f), 'utf8'));
|
package/scripts/savepoint.sh
CHANGED
|
@@ -331,6 +331,13 @@ if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
|
|
|
331
331
|
TASKS_TOTAL=$(grep -cE '^\s*-\s*\[[ xX]\]' "$PLAN_FILE" 2>/dev/null || true)
|
|
332
332
|
TASKS_DONE=$(grep -c '\[x\]' "$PLAN_FILE" 2>/dev/null || true)
|
|
333
333
|
fi
|
|
334
|
+
# Fallback for large campaigns (>3 phases, >1500 lines) where master plan.md is an index
|
|
335
|
+
# and tasks live in sub-plan/*.md — only when plan.md has zero checkbox tasks to
|
|
336
|
+
# keep simple missions unchanged.
|
|
337
|
+
if [ "${TASKS_TOTAL:-0}" -eq 0 ] 2>/dev/null && [ -d "$MISSION_DIR/sub-plan" ]; then
|
|
338
|
+
TASKS_TOTAL=$(grep -rcE '^\s*-\s*\[[ xX]\]' "$MISSION_DIR/sub-plan" 2>/dev/null | awk -F: '{s+=$2} END {print s+0}' || true)
|
|
339
|
+
TASKS_DONE=$(grep -rc '\[x\]' "$MISSION_DIR/sub-plan" 2>/dev/null | awk -F: '{s+=$2} END {print s+0}' || true)
|
|
340
|
+
fi
|
|
334
341
|
|
|
335
342
|
# blocker count
|
|
336
343
|
BLOCKERS_FILE="$MISSION_DIR/blockers.md"
|
|
@@ -365,6 +372,32 @@ if [ "$HEAL_CYCLE" -ge "$HEAL_MAX_CYCLES" ] 2>/dev/null; then
|
|
|
365
372
|
HEAL_HALT=true
|
|
366
373
|
fi
|
|
367
374
|
|
|
375
|
+
# slop — context (repeated reads) per cost-governor §§21-24,31-32 — T5 wire all crews Luffy/Nami/Zoro/Brook
|
|
376
|
+
REPEATED_READS=0
|
|
377
|
+
REPEATED_THRESHOLD=3
|
|
378
|
+
REGISTRY_FILE="$MISSION_DIR/context-registry.jsonl"
|
|
379
|
+
if [ -f "$REGISTRY_FILE" ]; then
|
|
380
|
+
REPEATED_READS=$(node -e "try{const fs=require('fs');const t=fs.readFileSync(process.argv[1],'utf8');let s=0;for(const l of t.split(/\r?\n/)){if(!l.trim())continue;try{const e=JSON.parse(l);if(typeof e.reads==='number'&&e.reads>=2)s+=Math.floor(e.reads)-1}catch{}}console.log(s)}catch(e){console.log(0)}" "$REGISTRY_FILE" 2>/dev/null || echo 0)
|
|
381
|
+
REPEATED_READS=$(( ${REPEATED_READS:-0} + 0 ))
|
|
382
|
+
fi
|
|
383
|
+
# repeated_reads > threshold → context slop — crew must skip re-read/compress before dispatch (§22,31); heal_cycle≥max → halt/escalate (§21.7/32) — cost-governor §§20,21-24
|
|
384
|
+
|
|
385
|
+
# depth flags — advisory → measured (roadmap v0.8 item 4). Read from config
|
|
386
|
+
# like the other keys; computed into state.json so enforcement is a fact the
|
|
387
|
+
# gates flow stage can read, not prose.
|
|
388
|
+
DEPTH_REVIEW="full"; DEPTH_QUALITY="full"; DEPTH_VERIFY="off"
|
|
389
|
+
if [ -f "$MUGIWARA_DIR/config" ]; then
|
|
390
|
+
_cfg_r=$(grep -E '^review_depth=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
|
|
391
|
+
[ -n "$_cfg_r" ] && DEPTH_REVIEW="$_cfg_r"
|
|
392
|
+
_cfg_q=$(grep -E '^quality_depth=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
|
|
393
|
+
[ -n "$_cfg_q" ] && DEPTH_QUALITY="$_cfg_q"
|
|
394
|
+
_cfg_v=$(grep -E '^verify_merged=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
|
|
395
|
+
[ -n "$_cfg_v" ] && DEPTH_VERIFY="$_cfg_v"
|
|
396
|
+
fi
|
|
397
|
+
case "$DEPTH_REVIEW" in full|standard|lean) ;; *) DEPTH_REVIEW="full" ;; esac
|
|
398
|
+
case "$DEPTH_QUALITY" in full|standard|lean) ;; *) DEPTH_QUALITY="full" ;; esac
|
|
399
|
+
case "$DEPTH_VERIFY" in on|off) ;; *) DEPTH_VERIFY="off" ;; esac
|
|
400
|
+
|
|
368
401
|
# evidence paths — the mission's flow folder (quoted printf, no sed — mission
|
|
369
402
|
# name is allowlisted above, but avoid sed metacharacter semantics entirely).
|
|
370
403
|
# New missions use flows/; a legacy mission that already keeps waves/ stays on
|
|
@@ -498,13 +531,18 @@ const data = {
|
|
|
498
531
|
heal_halt: process.argv[34] === 'true',
|
|
499
532
|
delegate_threshold: parseInt(process.argv[35], 10) || 60,
|
|
500
533
|
delegate_due: process.argv[36] === 'true',
|
|
534
|
+
review_depth: process.argv[38] || 'full',
|
|
535
|
+
quality_depth: process.argv[39] || 'full',
|
|
536
|
+
verify_merged: process.argv[40] || 'off',
|
|
501
537
|
tokens_est: parseInt(process.argv[17], 10) || 0,
|
|
502
538
|
tokens_source: process.argv[26] || 'computed',
|
|
503
539
|
budget: parseInt(process.argv[18], 10) || 0,
|
|
504
540
|
budget_status: process.argv[19],
|
|
505
541
|
skill_version: process.argv[20],
|
|
506
542
|
evidence: process.argv[21] ? process.argv[21].split(',').filter(Boolean) : [],
|
|
507
|
-
updated_at: process.argv[22]
|
|
543
|
+
updated_at: process.argv[22],
|
|
544
|
+
schema_version: 2,
|
|
545
|
+
repeated_reads: parseInt(process.argv[41], 10) || 0
|
|
508
546
|
};
|
|
509
547
|
require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\n');
|
|
510
548
|
" \
|
|
@@ -517,7 +555,8 @@ require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\
|
|
|
517
555
|
"$STATE_FILE" "$LANE_PREV" "$LANE_ROSE" "$TOKENS_SOURCE" "$LANE_PEAK" \
|
|
518
556
|
"$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER" "$VERBOSITY" \
|
|
519
557
|
"$HEAL_MAX_CYCLES" "$HEAL_HALT" "$DELEGATE_THRESHOLD" "$DELEGATE_DUE" \
|
|
520
|
-
"$MODEL"
|
|
558
|
+
"$MODEL" "$DEPTH_REVIEW" "$DEPTH_QUALITY" "$DEPTH_VERIFY" \
|
|
559
|
+
"$REPEATED_READS"
|
|
521
560
|
|
|
522
561
|
if [ "$LANE_ROSE" = true ]; then
|
|
523
562
|
echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// scripts/validate-content.ts
|
|
3
3
|
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
|
|
4
|
-
import { join, basename } from 'node:path';
|
|
4
|
+
import { join, basename, dirname } from 'node:path';
|
|
5
5
|
import { parseFrontmatter } from '../src/frontmatter.ts';
|
|
6
6
|
|
|
7
7
|
const root = join(import.meta.dirname, '..', 'content');
|
|
@@ -28,6 +28,25 @@ function checkFile(file: string, wantName: string, kind: 'skill' | 'agent'): Rec
|
|
|
28
28
|
if (bullets.length > 4) errors.push(`skill ${file}: "## Skip when" block exceeds 4 bullets`);
|
|
29
29
|
}
|
|
30
30
|
if (kind === 'skill' && !body.includes('## Red flags')) errors.push(`skill ${file}: missing required "## Red flags" block`);
|
|
31
|
+
if (kind === 'skill') {
|
|
32
|
+
// gate_artifact (roadmap item 2): a declared artifact must name a
|
|
33
|
+
// verifiable path (flows/ mission evidence, plan.md, or a references/
|
|
34
|
+
// file that must actually exist beside the skill).
|
|
35
|
+
const ga = (data as Record<string, unknown>).gate_artifact;
|
|
36
|
+
if (ga !== undefined) {
|
|
37
|
+
const v = String(ga).trim();
|
|
38
|
+
if (!v) errors.push(`skill ${file}: gate_artifact declared but empty`);
|
|
39
|
+
else if (!/(flows\/|plan\.md|references\/)/.test(v)) {
|
|
40
|
+
errors.push(`skill ${file}: gate_artifact "${v}" must name a flows/, plan.md, or references/ path`);
|
|
41
|
+
} else {
|
|
42
|
+
const refMatch = v.match(/(references\/[\w./-]+\.md)/);
|
|
43
|
+
if (refMatch) {
|
|
44
|
+
const refPath = join(dirname(file), refMatch[1]);
|
|
45
|
+
if (!existsSync(refPath)) errors.push(`skill ${file}: gate_artifact references "${refMatch[1]}" but file does not exist`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
31
50
|
if (kind === 'skill') {
|
|
32
51
|
const lines = body.split(/\r?\n/);
|
|
33
52
|
const headingRe = /^## /;
|
|
@@ -357,7 +376,7 @@ if (integrityArg !== -1) {
|
|
|
357
376
|
const constants = readFileSync(src, 'utf8');
|
|
358
377
|
const docs = ['docs/concepts/cost.md', 'docs/concepts/lanes.md', 'README.md'];
|
|
359
378
|
const expected: [string, string][] = [
|
|
360
|
-
['lean', '
|
|
379
|
+
['lean', '8,000'], ['standard', '13,000'], ['full', '22,000'],
|
|
361
380
|
['lean', '12,000'], ['standard', '25,000'], ['full', '50,000'],
|
|
362
381
|
];
|
|
363
382
|
for (const doc of docs) {
|
|
@@ -393,11 +412,71 @@ if (integrityArg !== -1) {
|
|
|
393
412
|
}
|
|
394
413
|
}
|
|
395
414
|
}
|
|
396
|
-
if (!constants.includes('LANE_BASE_lean=
|
|
415
|
+
if (!constants.includes('LANE_BASE_lean=8421')) errors.push('doc-integrity: source lane-base.sh lean base drifted (expected 8421)');
|
|
397
416
|
if (!constants.includes('BUDGET_full=50000')) errors.push('doc-integrity: source lane-base.sh full budget drifted (expected 50000)');
|
|
398
417
|
}
|
|
399
418
|
}
|
|
400
419
|
|
|
420
|
+
// --- README metrics gate (D3): README table must match .metrics/latest.json ---
|
|
421
|
+
if (process.argv.includes('--check-readme-metrics')) {
|
|
422
|
+
const metricsPath = join(import.meta.dirname, '..', '.metrics', 'latest.json');
|
|
423
|
+
if (!existsSync(metricsPath)) {
|
|
424
|
+
errors.push(`README metrics: ${metricsPath} not found — run bun scripts/write-metrics.ts`);
|
|
425
|
+
} else {
|
|
426
|
+
let metrics: any;
|
|
427
|
+
try { metrics = JSON.parse(readFileSync(metricsPath, 'utf8')); }
|
|
428
|
+
catch (e) { errors.push(`README metrics: invalid JSON in ${metricsPath}: ${e}`); }
|
|
429
|
+
if (metrics) {
|
|
430
|
+
const readmePath = join(import.meta.dirname, '..', 'README.md');
|
|
431
|
+
if (!existsSync(readmePath)) {
|
|
432
|
+
errors.push('README metrics: README.md not found');
|
|
433
|
+
} else {
|
|
434
|
+
const readme = readFileSync(readmePath, 'utf8');
|
|
435
|
+
// rank-1: **95.9%**, 216 probes
|
|
436
|
+
const rankMatch = readme.match(/Retrieval routing rank-1[^\n]*?(\d+\.\d+)%[^\n]*?(\d+)\s+probes/i);
|
|
437
|
+
if (!rankMatch) {
|
|
438
|
+
errors.push('README metrics: could not parse Retrieval routing rank-1 row (expected "**X.Y%**, N probes")');
|
|
439
|
+
} else {
|
|
440
|
+
const readmeRank = parseFloat(rankMatch[1]);
|
|
441
|
+
const readmeProbes = parseInt(rankMatch[2], 10);
|
|
442
|
+
const wantRank = Number(metrics.retrieval_rank1);
|
|
443
|
+
const wantProbes = Number(metrics.retrieval_probes);
|
|
444
|
+
if (Math.abs(readmeRank - wantRank) > 0.05) {
|
|
445
|
+
errors.push(`README metrics: rank-1 ${readmeRank}% != metrics ${wantRank}% (probes ${readmeProbes} vs ${wantProbes}) — run bun scripts/write-metrics.ts and update README`);
|
|
446
|
+
}
|
|
447
|
+
if (readmeProbes !== wantProbes) {
|
|
448
|
+
errors.push(`README metrics: probes ${readmeProbes} != metrics ${wantProbes} (rank ${readmeRank}% vs ${wantRank}%) — run bun scripts/write-metrics.ts and update README`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// pointers: **286/286**, 9 targets (or tiers)
|
|
452
|
+
const ptrMatch = readme.match(/Reference pointers resolve[^\n]*?\*\*(\d+)\/(\d+)\*\*[^\n]*?(\d+)\s+(tiers|targets)/i);
|
|
453
|
+
if (!ptrMatch) {
|
|
454
|
+
errors.push('README metrics: could not parse Reference pointers row (expected "**N/N**, M targets")');
|
|
455
|
+
} else {
|
|
456
|
+
const a = parseInt(ptrMatch[1], 10);
|
|
457
|
+
const b = parseInt(ptrMatch[2], 10);
|
|
458
|
+
const count = parseInt(ptrMatch[3], 10);
|
|
459
|
+
const wantTotal = Number(metrics.pointers_total);
|
|
460
|
+
const wantTargets = Number(metrics.pointers_targets);
|
|
461
|
+
if (a !== wantTotal || b !== wantTotal) {
|
|
462
|
+
errors.push(`README metrics: pointers ${a}/${b} != metrics ${wantTotal}/${wantTotal} — run bun scripts/write-metrics.ts and update README`);
|
|
463
|
+
}
|
|
464
|
+
if (count !== wantTargets) {
|
|
465
|
+
errors.push(`README metrics: targets/tiers ${count} != metrics ${wantTargets} — run bun scripts/write-metrics.ts and update README (expected ${wantTargets} targets)`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// sanity: table still claims "Nothing in this table is an estimate"
|
|
469
|
+
if (!readme.includes('Nothing in this table is an estimate')) {
|
|
470
|
+
errors.push('README metrics: missing "Nothing in this table is an estimate" line');
|
|
471
|
+
}
|
|
472
|
+
if (errors.filter(e => e.startsWith('README metrics:')).length === 0) {
|
|
473
|
+
console.log(`✓ README metrics match .metrics/latest.json (rank-1 ${metrics.retrieval_rank1}% ${metrics.retrieval_probes} probes, ${metrics.pointers_total}/${metrics.pointers_total} pointers, ${metrics.pointers_targets} targets)`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
401
480
|
// Conditional-assertion guard: an expect() reachable only inside a truthiness
|
|
402
481
|
// check silently passes when the value is absent. This class produced 9 defects.
|
|
403
482
|
// Allowed: checks keyed on a declared invariant (tier, fixture keys).
|
|
@@ -18,6 +18,7 @@ import { targets, TARGET_IDS } from '../src/targets/index.ts';
|
|
|
18
18
|
|
|
19
19
|
const repoRoot = join(import.meta.dirname, '..');
|
|
20
20
|
const fail: string[] = [];
|
|
21
|
+
const isJson = process.argv.includes('--json');
|
|
21
22
|
|
|
22
23
|
function findMd(root: string, out: string[] = []): string[] {
|
|
23
24
|
if (!existsSync(root)) return out;
|
|
@@ -169,6 +170,25 @@ if (orphans.length > ORPHAN_BASELINE) {
|
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
// ---------------------------------------------------------------------------
|
|
173
|
+
if (isJson) {
|
|
174
|
+
const payload = {
|
|
175
|
+
pointers_total: pointers,
|
|
176
|
+
pointers_targets: TARGET_IDS.length,
|
|
177
|
+
pointers_broken: brokenPointers,
|
|
178
|
+
prose_paths: prosePaths,
|
|
179
|
+
prose_files: proseFiles.length,
|
|
180
|
+
orphans,
|
|
181
|
+
orphans_count: orphans.length,
|
|
182
|
+
ref_files: refFiles.length,
|
|
183
|
+
targets: TARGET_IDS.length,
|
|
184
|
+
pointers: pointers,
|
|
185
|
+
broken_pointers: brokenPointers,
|
|
186
|
+
};
|
|
187
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
188
|
+
if (fail.length) process.exit(1);
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
172
192
|
console.log(` ${pointers} pointers checked across ${TARGET_IDS.length} targets`);
|
|
173
193
|
console.log(` ${prosePaths} prose paths checked in ${proseFiles.length} files`);
|
|
174
194
|
console.log(` ${orphans.length}/${refFiles.length} reference files unreachable (baseline ${ORPHAN_BASELINE})`);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// scripts/write-metrics.ts — generate .metrics/latest.json from gate outputs
|
|
3
|
+
// Deterministic, no network. Runs retrieval-eval --json and verify-install --json.
|
|
4
|
+
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const root = join(import.meta.dirname, '..');
|
|
10
|
+
|
|
11
|
+
function extractJson(output: string): any {
|
|
12
|
+
const idx = output.indexOf('{');
|
|
13
|
+
if (idx === -1) throw new Error('no JSON found in output: ' + output.slice(0, 200));
|
|
14
|
+
return JSON.parse(output.slice(idx));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function runJson(cmd: string): any {
|
|
18
|
+
// execSync returns stdout only; retrieval prints a rank line before JSON on stdout
|
|
19
|
+
// so we slice from first '{'
|
|
20
|
+
const out = execSync(cmd, { cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
21
|
+
return extractJson(out);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// retrieval: need probes, rank1_rate, index_size
|
|
25
|
+
let ret: any;
|
|
26
|
+
let ver: any;
|
|
27
|
+
try {
|
|
28
|
+
ret = runJson('bun scripts/retrieval-eval.ts --json');
|
|
29
|
+
} catch (e: any) {
|
|
30
|
+
// if process exits non-zero, stdout still contains JSON + rank line; try to parse from error stdout
|
|
31
|
+
const out = e.stdout?.toString() ?? e.message ?? '';
|
|
32
|
+
if (out.includes('{')) ret = extractJson(out);
|
|
33
|
+
else throw e;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
ver = runJson('bun scripts/verify-install.ts --json');
|
|
38
|
+
} catch (e: any) {
|
|
39
|
+
const out = e.stdout?.toString() ?? e.message ?? '';
|
|
40
|
+
if (out.includes('{')) ver = extractJson(out);
|
|
41
|
+
else throw e;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rank1Str: string = ret.rank1_rate ?? ret.rank1 ?? '';
|
|
45
|
+
const rank1Num = typeof rank1Str === 'string' ? parseFloat(rank1Str.replace('%', '')) : Number(rank1Str);
|
|
46
|
+
const probes = ret.probes ?? ret.totalProbes ?? 0;
|
|
47
|
+
const pointersTotal = ver.pointers_total ?? ver.pointers ?? 0;
|
|
48
|
+
const pointersTargets = ver.pointers_targets ?? ver.targets ?? 0;
|
|
49
|
+
const indexSize = ret.index_size ?? 0;
|
|
50
|
+
const updated = new Date().toISOString().split('T')[0];
|
|
51
|
+
|
|
52
|
+
const metrics = {
|
|
53
|
+
retrieval_rank1: rank1Num,
|
|
54
|
+
retrieval_rank1_rate: rank1Str,
|
|
55
|
+
retrieval_probes: probes,
|
|
56
|
+
retrieval_rank1_count: ret.rank1_count ?? null,
|
|
57
|
+
retrieval_positives: ret.positives ?? null,
|
|
58
|
+
retrieval_negatives: ret.negatives ?? null,
|
|
59
|
+
retrieval_index_size: indexSize,
|
|
60
|
+
retrieval_index_terms: ret.index_terms ?? null,
|
|
61
|
+
pointers_total: pointersTotal,
|
|
62
|
+
pointers_targets: pointersTargets,
|
|
63
|
+
pointers_broken: ver.pointers_broken ?? ver.broken_pointers ?? 0,
|
|
64
|
+
index_size: indexSize,
|
|
65
|
+
updated,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const outDir = join(root, '.metrics');
|
|
69
|
+
mkdirSync(outDir, { recursive: true });
|
|
70
|
+
const outPath = join(outDir, 'latest.json');
|
|
71
|
+
writeFileSync(outPath, JSON.stringify(metrics, null, 2) + '\n');
|
|
72
|
+
console.log(`✓ wrote ${outPath}`);
|
|
73
|
+
console.log(JSON.stringify(metrics, null, 2));
|