@ionivetech/mugiwara 0.8.0 → 0.8.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.
Files changed (62) 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/README.md +2 -2
  7. package/content/agents/brook-healing.md +1 -1
  8. package/content/agents/memory-keeper.md +5 -0
  9. package/content/agents/usopp-brainstorm.md +3 -2
  10. package/content/agents/zoro-execution.md +4 -3
  11. package/content/skills/mugiwara-brainstorm/SKILL.md +5 -3
  12. package/content/skills/mugiwara-checkpoint/SKILL.md +2 -0
  13. package/content/skills/mugiwara-execution/SKILL.md +4 -3
  14. package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
  15. package/content/skills/mugiwara-gates/SKILL.md +6 -0
  16. package/content/skills/mugiwara-healing/SKILL.md +5 -1
  17. package/content/skills/mugiwara-lessons/SKILL.md +3 -0
  18. package/content/skills/mugiwara-orchestration/SKILL.md +7 -6
  19. package/content/skills/mugiwara-planning/SKILL.md +2 -0
  20. package/content/skills/mugiwara-quality/SKILL.md +3 -14
  21. package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
  22. package/content/skills/mugiwara-resume/SKILL.md +3 -14
  23. package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
  24. package/content/skills/mugiwara-review/SKILL.md +3 -15
  25. package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
  26. package/content/skills/mugiwara-security/SKILL.md +1 -0
  27. package/content/skills/mugiwara-ship/SKILL.md +2 -0
  28. package/content/skills/mugiwara-workflow/SKILL.md +28 -25
  29. package/dist/mugiwara.js +1323 -402
  30. package/gemini-extension.json +1 -1
  31. package/hooks/mugiwara-mode-tracker.js +24 -4
  32. package/hooks/mugiwara-mode-tracker.ts +36 -7
  33. package/hooks/session-start.js +6 -1
  34. package/hooks/session-start.ts +8 -1
  35. package/package.json +2 -2
  36. package/plugin.json +1 -1
  37. package/references/cost-governor.md +104 -0
  38. package/references/wave-banners.md +1 -2
  39. package/scripts/gate-selftest.ts +239 -21
  40. package/scripts/lane-base.ts +16 -0
  41. package/scripts/lane.sh +5 -1
  42. package/scripts/lib/lane-base.sh +1 -1
  43. package/scripts/savepoint.sh +48 -5
  44. package/scripts/validate-content.ts +60 -0
  45. package/scripts/verify-install.ts +20 -0
  46. package/scripts/write-metrics.ts +73 -0
  47. package/src/budget.ts +11 -0
  48. package/src/cli.ts +185 -28
  49. package/src/config.ts +6 -0
  50. package/src/continue.ts +36 -1
  51. package/src/cost.ts +4 -1
  52. package/src/installer.ts +27 -4
  53. package/src/integrity.ts +105 -25
  54. package/src/mission.ts +123 -7
  55. package/src/policy.ts +372 -4
  56. package/src/provenance.ts +29 -9
  57. package/src/sign.ts +45 -3
  58. package/content/skills/mugiwara-workflow/references/adaptive-budget-governor.md +0 -5
  59. package/content/skills/mugiwara-workflow/references/benchmark-governor.md +0 -53
  60. package/content/skills/mugiwara-workflow/references/cognitive-output-governor.md +0 -5
  61. package/content/skills/mugiwara-workflow/references/scope-code-governor.md +0 -14
  62. package/content/skills/mugiwara-workflow/references/stop-slop-governor.md +0 -14
@@ -107,6 +107,22 @@ for (const lane of lanes) {
107
107
  }
108
108
  }
109
109
 
110
+ // A lane whose base exceeds its budget is born in `warn` — the budget is then
111
+ // noise rather than a signal. (B5)
112
+ for (const lane of ['lean', 'standard', 'full', 'spike']) {
113
+ const base = constants[lane]?.base ?? 0;
114
+ const budget = constants[lane]?.budget ?? 0;
115
+ if (base >= budget) {
116
+ console.log(` ✗ LANE_BASE_${lane} (${base}) >= BUDGET_${lane} (${budget}) — every mission starts over budget`);
117
+ failures++;
118
+ }
119
+ const pct = budget ? Math.round((base / budget) * 100) : 100;
120
+ if (pct > 80) {
121
+ console.log(` ✗ LANE_BASE_${lane} is ${pct}% of BUDGET_${lane} — leaves no headroom (target ≤70%)`);
122
+ failures++;
123
+ }
124
+ }
125
+
110
126
  if (failures > 0) {
111
127
  console.log(`\nlane-base: ${failures} constant(s) drifted from content load`);
112
128
  process.exit(1);
package/scripts/lane.sh CHANGED
@@ -11,7 +11,11 @@ BASE="${1:-main}"
11
11
  JSON_OUT=0
12
12
  [ "${2:-}" = "--json" ] && JSON_OUT=1
13
13
 
14
- [ -d .git ] || { echo "lane: not a git repository" >&2; exit 1; }
14
+ # Resolve the repo root: handles subdirectories and git worktrees, where .git
15
+ # is a file rather than a directory. (B4)
16
+ REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || {
17
+ echo "lane: not a git repository" >&2; exit 1; }
18
+ cd "$REPO_ROOT" || { echo "lane: cannot enter repo root" >&2; exit 1; }
15
19
 
16
20
  # resolve base
17
21
  if ! git rev-parse "$BASE" >/dev/null 2>&1; then
@@ -16,4 +16,4 @@ LANE_BASE_spike=5411
16
16
  BUDGET_lean=12000
17
17
  BUDGET_standard=25000
18
18
  BUDGET_full=50000
19
- BUDGET_spike=3000
19
+ BUDGET_spike=9000
@@ -8,6 +8,18 @@ set -u
8
8
 
9
9
  die() { echo "savepoint: $*" >&2; exit 1; }
10
10
 
11
+ # count_boxes <file> <char-class> — count markdown checkboxes.
12
+ # Anchored so prose mentioning "- [x]" is not counted; skips fenced code blocks
13
+ # so documentation examples are not counted; matches [x] and [X] alike. (B3)
14
+ count_boxes() {
15
+ [ -f "$1" ] || { echo 0; return; }
16
+ awk -v pat="$2" '
17
+ /^[[:space:]]*```/ { inblock = !inblock; next }
18
+ !inblock && $0 ~ ("^[[:space:]]*-[[:space:]]*\\[" pat "\\]") { n++ }
19
+ END { print n+0 }
20
+ ' "$1"
21
+ }
22
+
11
23
  MUGIWARA_DIR="${MUGIWARA_DIR:-.mugiwara}"
12
24
 
13
25
  # optional provider-reported tokens file (T4): --tokens-file <path> JSON {input_tokens, output_tokens}
@@ -154,6 +166,11 @@ esac
154
166
  # (BSD/macOS-safe: no \+ BRE).
155
167
  BRANCH_SLUG=$(echo "$BRANCH" | tr '/' '-' | tr -cd 'A-Za-z0-9._-' | sed 's/^\.\{1,\}$//' )
156
168
 
169
+ # Resolve the repo root: handles subdirectories and git worktrees, where .git
170
+ # is a file rather than a directory. (B4)
171
+ REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || die "not a git repository"
172
+ cd "$REPO_ROOT" || die "cannot enter repo root"
173
+
157
174
  # state + continue live in the mission dir. Solo (member empty) → state.json
158
175
  # + continue.json; team writes <member>.json + continue-<member>.json so
159
176
  # parallel members never clobber each other.
@@ -167,7 +184,6 @@ else
167
184
  fi
168
185
 
169
186
  [ -z "$MISSION" ] && die "usage: savepoint.sh <mission> [member] [wave] [mode] [lane]"
170
- [ -d .git ] || die "not a git repository"
171
187
 
172
188
  # --- computed fields ---
173
189
  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")
@@ -328,9 +344,23 @@ if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
328
344
  # total counts ALL task lines (checked + unchecked); done counts checked only.
329
345
  # A fully-completed plan must read total=N done=N, never total=0 (the old
330
346
  # unchecked-only grep degenerated a done plan to tasks.total=0).
331
- TASKS_TOTAL=$(grep -cE '^\s*-\s*\[[ xX]\]' "$PLAN_FILE" 2>/dev/null || true)
332
- TASKS_DONE=$(grep -c '\[x\]' "$PLAN_FILE" 2>/dev/null || true)
347
+ TASKS_TOTAL=$(count_boxes "$PLAN_FILE" '[ xX]')
348
+ TASKS_DONE=$(count_boxes "$PLAN_FILE" '[xX]')
349
+ fi
350
+ # Fallback for large campaigns (>3 phases, >1500 lines) where master plan.md is an index
351
+ # and tasks live in sub-plan/*.md — only when plan.md has zero checkbox tasks to
352
+ # keep simple missions unchanged.
353
+ if [ "${TASKS_TOTAL:-0}" -eq 0 ] 2>/dev/null && [ -d "$MISSION_DIR/sub-plan" ]; then
354
+ TASKS_TOTAL=0; TASKS_DONE=0
355
+ for _sp in "$MISSION_DIR"/sub-plan/*.md; do
356
+ [ -f "$_sp" ] || continue
357
+ TASKS_TOTAL=$(( TASKS_TOTAL + $(count_boxes "$_sp" '[ xX]') ))
358
+ TASKS_DONE=$(( TASKS_DONE + $(count_boxes "$_sp" '[xX]') ))
359
+ done
333
360
  fi
361
+ # done ≤ total is an invariant of the audit trail — never let a report show
362
+ # progress above 100%, whatever the plan file contains. (B3)
363
+ [ "${TASKS_DONE:-0}" -gt "${TASKS_TOTAL:-0}" ] 2>/dev/null && TASKS_DONE="$TASKS_TOTAL"
334
364
 
335
365
  # blocker count
336
366
  BLOCKERS_FILE="$MISSION_DIR/blockers.md"
@@ -365,6 +395,16 @@ if [ "$HEAL_CYCLE" -ge "$HEAL_MAX_CYCLES" ] 2>/dev/null; then
365
395
  HEAL_HALT=true
366
396
  fi
367
397
 
398
+ # slop — context (repeated reads) per cost-governor §§21-24,31-32 — T5 wire all crews Luffy/Nami/Zoro/Brook
399
+ REPEATED_READS=0
400
+ REPEATED_THRESHOLD=3
401
+ REGISTRY_FILE="$MISSION_DIR/context-registry.jsonl"
402
+ if [ -f "$REGISTRY_FILE" ]; then
403
+ 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)
404
+ REPEATED_READS=$(( ${REPEATED_READS:-0} + 0 ))
405
+ fi
406
+ # 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
407
+
368
408
  # depth flags — advisory → measured (roadmap v0.8 item 4). Read from config
369
409
  # like the other keys; computed into state.json so enforcement is a fact the
370
410
  # gates flow stage can read, not prose.
@@ -523,7 +563,9 @@ const data = {
523
563
  budget_status: process.argv[19],
524
564
  skill_version: process.argv[20],
525
565
  evidence: process.argv[21] ? process.argv[21].split(',').filter(Boolean) : [],
526
- updated_at: process.argv[22]
566
+ updated_at: process.argv[22],
567
+ schema_version: 2,
568
+ repeated_reads: parseInt(process.argv[41], 10) || 0
527
569
  };
528
570
  require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\n');
529
571
  " \
@@ -536,7 +578,8 @@ require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\
536
578
  "$STATE_FILE" "$LANE_PREV" "$LANE_ROSE" "$TOKENS_SOURCE" "$LANE_PEAK" \
537
579
  "$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER" "$VERBOSITY" \
538
580
  "$HEAL_MAX_CYCLES" "$HEAL_HALT" "$DELEGATE_THRESHOLD" "$DELEGATE_DUE" \
539
- "$MODEL" "$DEPTH_REVIEW" "$DEPTH_QUALITY" "$DEPTH_VERIFY"
581
+ "$MODEL" "$DEPTH_REVIEW" "$DEPTH_QUALITY" "$DEPTH_VERIFY" \
582
+ "$REPEATED_READS"
540
583
 
541
584
  if [ "$LANE_ROSE" = true ]; then
542
585
  echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
@@ -417,6 +417,66 @@ if (integrityArg !== -1) {
417
417
  }
418
418
  }
419
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
+
420
480
  // Conditional-assertion guard: an expect() reachable only inside a truthiness
421
481
  // check silently passes when the value is absent. This class produced 9 defects.
422
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));
package/src/budget.ts CHANGED
@@ -45,3 +45,14 @@ export function formatFootprint(chars: number, budget: number): string {
45
45
  ? `${base} — OVER budget ${budget}`
46
46
  : `${base} (budget ${budget})`;
47
47
  }
48
+
49
+ // ── Auto-compress threshold (T4) — 80% of budget ───────────────────────────
50
+ export const COMPRESS_THRESHOLD_PCT = 0.8;
51
+
52
+ export function shouldCompress(budget: number, chars: number): boolean {
53
+ return budget > 0 && chars > Math.floor(budget * COMPRESS_THRESHOLD_PCT);
54
+ }
55
+
56
+ export function compressThreshold(budget: number): number {
57
+ return Math.floor(budget * COMPRESS_THRESHOLD_PCT);
58
+ }