@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.
Files changed (87) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/.opencode/mugiwara-helpers.mjs +2 -2
  7. package/README.md +196 -330
  8. package/content/agents/brook-healing.md +1 -1
  9. package/content/agents/franky-gates.md +1 -1
  10. package/content/agents/luffy-orchestrator.md +2 -2
  11. package/content/agents/memory-keeper.md +5 -0
  12. package/content/agents/usopp-brainstorm.md +3 -2
  13. package/content/agents/zoro-execution.md +4 -3
  14. package/content/skills/mugiwara-backend/SKILL.md +52 -43
  15. package/content/skills/mugiwara-brainstorm/SKILL.md +5 -3
  16. package/content/skills/mugiwara-checkpoint/SKILL.md +21 -8
  17. package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
  18. package/content/skills/mugiwara-execution/SKILL.md +34 -33
  19. package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
  20. package/content/skills/mugiwara-execution/references/execution-phase-flows.md +18 -0
  21. package/content/skills/mugiwara-frontend/SKILL.md +44 -44
  22. package/content/skills/mugiwara-gates/SKILL.md +28 -16
  23. package/content/skills/mugiwara-healing/SKILL.md +30 -25
  24. package/content/skills/mugiwara-lessons/SKILL.md +3 -0
  25. package/content/skills/mugiwara-orchestration/SKILL.md +10 -9
  26. package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
  27. package/content/skills/mugiwara-planning/SKILL.md +28 -14
  28. package/content/skills/mugiwara-planning/references/large-campaign-subplan.md +41 -0
  29. package/content/skills/mugiwara-planning/references/plan-template.md +22 -0
  30. package/content/skills/mugiwara-quality/SKILL.md +8 -13
  31. package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
  32. package/content/skills/mugiwara-resume/SKILL.md +3 -9
  33. package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
  34. package/content/skills/mugiwara-review/SKILL.md +17 -24
  35. package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
  36. package/content/skills/mugiwara-security/SKILL.md +47 -35
  37. package/content/skills/mugiwara-ship/SKILL.md +2 -0
  38. package/content/skills/mugiwara-workflow/SKILL.md +13 -13
  39. package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
  40. package/content/skills/mugiwara-workflow/references/workspace-layout.md +6 -3
  41. package/dist/mugiwara.js +1802 -316
  42. package/gemini-extension.json +1 -1
  43. package/hooks/mugiwara-mode-tracker.js +24 -4
  44. package/hooks/mugiwara-mode-tracker.ts +36 -7
  45. package/hooks/pipeline-guard.js +1 -1
  46. package/hooks/pipeline-guard.ts +2 -1
  47. package/hooks/session-start.js +6 -1
  48. package/hooks/session-start.ts +8 -1
  49. package/package.json +2 -2
  50. package/plugin.json +1 -1
  51. package/references/cost-governor.md +104 -0
  52. package/references/multi-actor.md +21 -0
  53. package/references/posture-routing.md +31 -0
  54. package/references/wave-banners.md +1 -2
  55. package/scripts/benchmark-governor.ts +516 -0
  56. package/scripts/benchmark-thresholds.json +47 -0
  57. package/scripts/check-doc-links.ts +8 -2
  58. package/scripts/gate-selftest.ts +104 -21
  59. package/scripts/lib/lane-base.sh +4 -4
  60. package/scripts/retrieval-eval.ts +9 -3
  61. package/scripts/savepoint.sh +41 -2
  62. package/scripts/validate-content.ts +82 -3
  63. package/scripts/verify-install.ts +20 -0
  64. package/scripts/write-metrics.ts +73 -0
  65. package/src/adaptive-budget.ts +178 -0
  66. package/src/args.ts +3 -2
  67. package/src/budget.ts +18 -16
  68. package/src/check-artifacts.ts +45 -0
  69. package/src/cli.ts +221 -8
  70. package/src/cognition.ts +234 -0
  71. package/src/config.ts +113 -0
  72. package/src/context.ts +72 -0
  73. package/src/continue.ts +29 -0
  74. package/src/cost.ts +189 -0
  75. package/src/evidence.ts +160 -0
  76. package/src/installer.ts +2 -16
  77. package/src/integrity.ts +65 -16
  78. package/src/investigation.ts +72 -0
  79. package/src/mission.ts +246 -16
  80. package/src/policy.ts +355 -2
  81. package/src/posture.ts +86 -0
  82. package/src/provenance.ts +29 -9
  83. package/src/reporting.ts +225 -0
  84. package/src/scope.ts +321 -0
  85. package/src/sign.ts +234 -18
  86. package/src/slop.ts +306 -0
  87. package/src/work.ts +273 -0
@@ -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 drifted = original.replace(/\*\*Current:\*\* \d[\d,]* chars/, '**Current:** 1 chars');
95
- if (drifted === original) {
96
- console.log(' ⚠ "**Current:** N chars" pattern not found in cost.md — skipping');
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
- /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\)/,
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.log('D1 mutation pattern not found — skipping');
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
- /lane_rank\(\) \{\n case "\$1" in\n direct\) echo 0 ;;[\s\S]*?\n esac\n\}/,
169
+ d2Pattern,
163
170
  'lane_rank() {\n echo 0\n}'
164
171
  );
165
- if (broken === original) {
166
- console.log('D2 mutation pattern not found — skipping');
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
- /SENSITIVE_PATS=.*/,
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.log('D3 mutation pattern not found — skipping');
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
- if (broken === live || !live) {
222
- console.log(' ⚠ D3b: no D3 family tokens found in live SENSITIVE_PATS — skipping');
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(/SENSITIVE_PATS="[^"]*"/, brokenLine));
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
- /LOC_TOKENS=\$\(\( LOC_CHURN \* 12 \)\)/,
255
+ d4Pattern,
243
256
  'LOC_TOKENS=$(( LOC_DELTA > 0 ? LOC_DELTA * 12 : 0 ))'
244
257
  );
245
- if (broken === original) {
246
- console.log('D4 mutation pattern not found — skipping');
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
- /# --- continue\/<mission>\/<member>\.json \(D10\): machine-written resume point ---[\s\S]*?\nfi\n\n/,
282
+ d10Pattern,
268
283
  '# --- continue writer disabled (D10) ---\n\n'
269
284
  );
270
- if (broken === original) {
271
- console.log('D10 mutation pattern not found — skipping');
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);
@@ -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=7000
11
- LANE_BASE_standard=13000
12
- LANE_BASE_full=23000
13
- LANE_BASE_spike=1000
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 files = readdirSync(evalsDir).filter(f => f.endsWith('.json'));
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 f of files) {
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'));
@@ -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', '7,000'], ['standard', '13,000'], ['full', '23,000'],
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=7000')) errors.push('doc-integrity: source lane-base.sh lean base drifted (expected 7000)');
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));