@ionivetech/mugiwara 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) 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 +194 -328
  8. package/content/agents/franky-gates.md +1 -1
  9. package/content/agents/luffy-orchestrator.md +2 -2
  10. package/content/skills/mugiwara-backend/SKILL.md +52 -43
  11. package/content/skills/mugiwara-checkpoint/SKILL.md +19 -8
  12. package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
  13. package/content/skills/mugiwara-execution/SKILL.md +32 -32
  14. package/content/skills/mugiwara-execution/references/execution-phase-flows.md +18 -0
  15. package/content/skills/mugiwara-frontend/SKILL.md +44 -44
  16. package/content/skills/mugiwara-gates/SKILL.md +22 -16
  17. package/content/skills/mugiwara-healing/SKILL.md +26 -25
  18. package/content/skills/mugiwara-orchestration/SKILL.md +6 -6
  19. package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
  20. package/content/skills/mugiwara-planning/SKILL.md +26 -14
  21. package/content/skills/mugiwara-planning/references/large-campaign-subplan.md +41 -0
  22. package/content/skills/mugiwara-planning/references/plan-template.md +22 -0
  23. package/content/skills/mugiwara-quality/SKILL.md +19 -13
  24. package/content/skills/mugiwara-resume/SKILL.md +6 -1
  25. package/content/skills/mugiwara-review/SKILL.md +17 -12
  26. package/content/skills/mugiwara-security/SKILL.md +46 -35
  27. package/content/skills/mugiwara-workflow/SKILL.md +6 -9
  28. package/content/skills/mugiwara-workflow/references/adaptive-budget-governor.md +5 -0
  29. package/content/skills/mugiwara-workflow/references/benchmark-governor.md +53 -0
  30. package/content/skills/mugiwara-workflow/references/cognitive-output-governor.md +5 -0
  31. package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
  32. package/content/skills/mugiwara-workflow/references/scope-code-governor.md +14 -0
  33. package/content/skills/mugiwara-workflow/references/stop-slop-governor.md +14 -0
  34. package/content/skills/mugiwara-workflow/references/workspace-layout.md +6 -3
  35. package/dist/mugiwara.js +925 -253
  36. package/gemini-extension.json +1 -1
  37. package/hooks/pipeline-guard.js +1 -1
  38. package/hooks/pipeline-guard.ts +2 -1
  39. package/package.json +2 -2
  40. package/plugin.json +1 -1
  41. package/references/multi-actor.md +21 -0
  42. package/references/posture-routing.md +31 -0
  43. package/scripts/benchmark-governor.ts +516 -0
  44. package/scripts/benchmark-thresholds.json +47 -0
  45. package/scripts/check-doc-links.ts +8 -2
  46. package/scripts/gate-selftest.ts +20 -0
  47. package/scripts/lib/lane-base.sh +4 -4
  48. package/scripts/retrieval-eval.ts +9 -3
  49. package/scripts/savepoint.sh +20 -1
  50. package/scripts/validate-content.ts +22 -3
  51. package/src/adaptive-budget.ts +178 -0
  52. package/src/args.ts +3 -2
  53. package/src/budget.ts +7 -16
  54. package/src/check-artifacts.ts +45 -0
  55. package/src/cli.ts +102 -4
  56. package/src/cognition.ts +234 -0
  57. package/src/config.ts +107 -0
  58. package/src/context.ts +72 -0
  59. package/src/cost.ts +186 -0
  60. package/src/evidence.ts +160 -0
  61. package/src/installer.ts +2 -16
  62. package/src/integrity.ts +1 -1
  63. package/src/investigation.ts +72 -0
  64. package/src/mission.ts +124 -10
  65. package/src/posture.ts +86 -0
  66. package/src/reporting.ts +225 -0
  67. package/src/scope.ts +321 -0
  68. package/src/sign.ts +194 -20
  69. package/src/slop.ts +306 -0
  70. package/src/work.ts +273 -0
@@ -0,0 +1,47 @@
1
+ {
2
+ "workloads": [
3
+ {
4
+ "id": "lean-trivial",
5
+ "projected": 8000,
6
+ "overhead": 1000,
7
+ "context_max": 20000,
8
+ "evidence_min": 1,
9
+ "expected_surface": { "files": 2, "loc": 50 }
10
+ },
11
+ {
12
+ "id": "standard-feature",
13
+ "projected": 15000,
14
+ "overhead": 1500,
15
+ "context_max": 40000,
16
+ "evidence_min": 3,
17
+ "expected_surface": { "files": 8, "loc": 300 }
18
+ },
19
+ {
20
+ "id": "large-repo",
21
+ "projected": 22000,
22
+ "overhead": 2200,
23
+ "context_max": 80000,
24
+ "evidence_min": 5,
25
+ "expected_surface": { "files": 50, "loc": 2000 }
26
+ },
27
+ {
28
+ "id": "long-mission",
29
+ "projected": 23000,
30
+ "overhead": 2300,
31
+ "context_max": 90000,
32
+ "evidence_min": 5,
33
+ "expected_surface": { "files": 30, "loc": 1500 }
34
+ }
35
+ ],
36
+ "slop_floors": { "max_slop_events": 0 },
37
+ "regression": {
38
+ "allow_cost_down_only_when": ["correctness", "evidence", "security", "quality", "scope"]
39
+ },
40
+ "baselines": {
41
+ "correctness": 100,
42
+ "evidence": 5,
43
+ "security": 100,
44
+ "quality": 100,
45
+ "scope": 100
46
+ }
47
+ }
@@ -16,8 +16,14 @@ function walk(d: string, out: string[] = []): string[] {
16
16
  return out;
17
17
  }
18
18
  const files: string[] = [];
19
- for (const r of ["docs", "examples"]) files.push(...walk(join(root, r)));
20
- for (const r of ["README.md", "ROADMAP.md"]) files.push(resolve(root, r));
19
+ for (const r of ["docs", "examples"]) {
20
+ const p = join(root, r);
21
+ if (existsSync(p)) files.push(...walk(p));
22
+ }
23
+ for (const r of ["README.md", "ROADMAP.md"]) {
24
+ const p = resolve(root, r);
25
+ if (existsSync(p)) files.push(p);
26
+ }
21
27
 
22
28
  let bad = 0;
23
29
  for (const f of files) {
@@ -578,6 +578,26 @@ console.log('\nT7 — evidence-thin gate');
578
578
  }
579
579
  }
580
580
 
581
+ // --- Benchmark governor — tamper thresholds, prove harness goes red ---
582
+ console.log('\nBenchmark governor — thresholds tamper');
583
+ {
584
+ const threshFile = join(root, 'scripts', 'benchmark-thresholds.json');
585
+ if (!existsSync(threshFile)) {
586
+ console.log(' ⚠ benchmark-thresholds.json not found, skipping');
587
+ } else {
588
+ const original = readFileSync(threshFile, 'utf8');
589
+ try {
590
+ const data = JSON.parse(original);
591
+ data.workloads = data.workloads.map((w: Record<string, unknown>) => ({ ...w, projected: 0, overhead: 0 }));
592
+ writeFileSync(threshFile, JSON.stringify(data, null, 2));
593
+ assert('tampered thresholds → benchmark-governor fails', false, () => run('bench', 'bun scripts/benchmark-governor.ts'));
594
+ } finally {
595
+ writeFileSync(threshFile, original);
596
+ assert('restored → benchmark-governor passes', true, () => run('bench', 'bun scripts/benchmark-governor.ts'));
597
+ }
598
+ }
599
+ }
600
+
581
601
  // --- DOCLINKS: a relative .md link that does not resolve must fail the gate ---
582
602
  console.log('\nDOCLINKS — doc link resolution');
583
603
  {
@@ -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'));
@@ -365,6 +365,22 @@ if [ "$HEAL_CYCLE" -ge "$HEAL_MAX_CYCLES" ] 2>/dev/null; then
365
365
  HEAL_HALT=true
366
366
  fi
367
367
 
368
+ # depth flags — advisory → measured (roadmap v0.8 item 4). Read from config
369
+ # like the other keys; computed into state.json so enforcement is a fact the
370
+ # gates flow stage can read, not prose.
371
+ DEPTH_REVIEW="full"; DEPTH_QUALITY="full"; DEPTH_VERIFY="off"
372
+ if [ -f "$MUGIWARA_DIR/config" ]; then
373
+ _cfg_r=$(grep -E '^review_depth=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
374
+ [ -n "$_cfg_r" ] && DEPTH_REVIEW="$_cfg_r"
375
+ _cfg_q=$(grep -E '^quality_depth=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
376
+ [ -n "$_cfg_q" ] && DEPTH_QUALITY="$_cfg_q"
377
+ _cfg_v=$(grep -E '^verify_merged=' "$MUGIWARA_DIR/config" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]')
378
+ [ -n "$_cfg_v" ] && DEPTH_VERIFY="$_cfg_v"
379
+ fi
380
+ case "$DEPTH_REVIEW" in full|standard|lean) ;; *) DEPTH_REVIEW="full" ;; esac
381
+ case "$DEPTH_QUALITY" in full|standard|lean) ;; *) DEPTH_QUALITY="full" ;; esac
382
+ case "$DEPTH_VERIFY" in on|off) ;; *) DEPTH_VERIFY="off" ;; esac
383
+
368
384
  # evidence paths — the mission's flow folder (quoted printf, no sed — mission
369
385
  # name is allowlisted above, but avoid sed metacharacter semantics entirely).
370
386
  # New missions use flows/; a legacy mission that already keeps waves/ stays on
@@ -498,6 +514,9 @@ const data = {
498
514
  heal_halt: process.argv[34] === 'true',
499
515
  delegate_threshold: parseInt(process.argv[35], 10) || 60,
500
516
  delegate_due: process.argv[36] === 'true',
517
+ review_depth: process.argv[38] || 'full',
518
+ quality_depth: process.argv[39] || 'full',
519
+ verify_merged: process.argv[40] || 'off',
501
520
  tokens_est: parseInt(process.argv[17], 10) || 0,
502
521
  tokens_source: process.argv[26] || 'computed',
503
522
  budget: parseInt(process.argv[18], 10) || 0,
@@ -517,7 +536,7 @@ require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\
517
536
  "$STATE_FILE" "$LANE_PREV" "$LANE_ROSE" "$TOKENS_SOURCE" "$LANE_PEAK" \
518
537
  "$LOC_INS" "$LOC_DEL" "$LOC_CHURN" "$MEMBER" "$VERBOSITY" \
519
538
  "$HEAL_MAX_CYCLES" "$HEAL_HALT" "$DELEGATE_THRESHOLD" "$DELEGATE_DUE" \
520
- "$MODEL"
539
+ "$MODEL" "$DEPTH_REVIEW" "$DEPTH_QUALITY" "$DEPTH_VERIFY"
521
540
 
522
541
  if [ "$LANE_ROSE" = true ]; then
523
542
  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,7 +412,7 @@ 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
  }
@@ -0,0 +1,178 @@
1
+ // src/adaptive-budget.ts
2
+ // Phase 7 Adaptive Budget & Circuit Breaker — reservation, projection,
3
+ // adaptive/expansion, progressive thresholds, circuit breaker, anomaly
4
+ // (Native Cost Governor, plan §51 Phase 7, §24–§29).
5
+ //
6
+ // Boundary: pure verdict functions over explicit inputs (unit-testable), plus a
7
+ // record helper that persists via the sanitized recordOptDecision (§41). No new
8
+ // config keys; savepoint.sh/lane-base.sh untouched. The crew acts — this module
9
+ // records.
10
+
11
+ import { recordOptDecision } from './cost.ts';
12
+
13
+ // ── Budget reservation (§25) ──
14
+
15
+ export type BudgetReservation = { remaining: number; expected_max: number; available: number; reserved: number };
16
+
17
+ export function reserveBudget(input: { remaining: number; expected_max: number }): BudgetReservation {
18
+ const reserved = input.expected_max;
19
+ const available = Math.max(0, input.remaining - reserved);
20
+ return { remaining: input.remaining, expected_max: input.expected_max, available, reserved };
21
+ }
22
+
23
+ // ── Budget projection (§26) ──
24
+
25
+ export type BudgetProjection = {
26
+ current: number;
27
+ remaining_required: number;
28
+ expected_conditional: number;
29
+ possible_healing: number;
30
+ projected_min: number;
31
+ projected_max: number;
32
+ };
33
+
34
+ export function projectBudget(input: {
35
+ current: number;
36
+ remaining_required: number;
37
+ expected_conditional: number;
38
+ possible_healing: number;
39
+ }): BudgetProjection {
40
+ const projected_min = input.current + input.remaining_required + input.expected_conditional;
41
+ const projected_max = projected_min + input.possible_healing;
42
+ return {
43
+ current: input.current,
44
+ remaining_required: input.remaining_required,
45
+ expected_conditional: input.expected_conditional,
46
+ possible_healing: input.possible_healing,
47
+ projected_min,
48
+ projected_max,
49
+ };
50
+ }
51
+
52
+ // ── Evidence-backed expansion (§27) ──
53
+
54
+ export type ExpansionInput = {
55
+ reason: string;
56
+ has_evidence: boolean;
57
+ scope_expanded?: boolean;
58
+ security_path?: boolean;
59
+ test_surface_expanded?: boolean;
60
+ architecture_dependency?: boolean;
61
+ legitimate_healing?: boolean;
62
+ };
63
+
64
+ export type ExpansionVerdict = { allowed: boolean; reason: string };
65
+
66
+ const VALID_REASONS = new Set([
67
+ 'scope legitimately expanded',
68
+ 'security-sensitive path',
69
+ 'test surface larger',
70
+ 'architecture dependency',
71
+ 'legitimate healing',
72
+ ]);
73
+
74
+ function isValidReason(reason: string): boolean {
75
+ return VALID_REASONS.has(reason);
76
+ }
77
+
78
+ function hasValidFlag(input: ExpansionInput): boolean {
79
+ return !!(
80
+ input.scope_expanded ||
81
+ input.security_path ||
82
+ input.test_surface_expanded ||
83
+ input.architecture_dependency ||
84
+ input.legitimate_healing
85
+ );
86
+ }
87
+
88
+ export function evaluateExpansion(input: ExpansionInput): ExpansionVerdict {
89
+ if (!input.has_evidence) {
90
+ return { allowed: false, reason: 'deny — no evidence' };
91
+ }
92
+ if (!input.reason || !isValidReason(input.reason)) {
93
+ return { allowed: false, reason: `deny — invalid reason: ${input.reason || '(empty)'}` };
94
+ }
95
+ if (!hasValidFlag(input)) {
96
+ return { allowed: false, reason: `deny — valid reason ${input.reason} but no matching flag set` };
97
+ }
98
+ return { allowed: true, reason: `allow — ${input.reason} with evidence` };
99
+ }
100
+
101
+ // ── Progressive thresholds (§28) ──
102
+
103
+ export type AdaptiveStatus = 'ok' | 'optimize' | 'aggressive' | 'protect' | 'pause' | 'warning' | 'stop';
104
+
105
+ export function checkProgressiveThreshold(input: { budget: number; used: number }): { status: AdaptiveStatus; pct: number } {
106
+ const pct = input.budget > 0 ? Math.round((input.used / input.budget) * 100) : 0;
107
+ let status: AdaptiveStatus = 'ok';
108
+ if (pct >= 300) status = 'stop';
109
+ else if (pct >= 150) status = 'warning';
110
+ else if (pct >= 100) status = 'pause';
111
+ else if (pct >= 90) status = 'protect';
112
+ else if (pct >= 75) status = 'aggressive';
113
+ else if (pct >= 60) status = 'optimize';
114
+ return { status, pct };
115
+ }
116
+
117
+ // ── Cost circuit breaker (§29) ──
118
+
119
+ export type CircuitBreakerInput = {
120
+ expected: number;
121
+ actual: number;
122
+ progress_delta: number;
123
+ scope_expanded: boolean;
124
+ evidence_delta: number;
125
+ };
126
+
127
+ export type CircuitBreakerVerdict = { tripped: boolean; reason: string };
128
+
129
+ export function checkCircuitBreaker(input: CircuitBreakerInput): CircuitBreakerVerdict {
130
+ // note: double-threshold (actual >= expected*2), tune if §29 needs finer signal
131
+ const doubled = input.expected * 2;
132
+ const noProgress = input.progress_delta === 0;
133
+ const noScopeOrEvidence = !input.scope_expanded && input.evidence_delta === 0;
134
+ const overDoubled = input.actual >= doubled;
135
+ if (overDoubled && noProgress && noScopeOrEvidence) {
136
+ return { tripped: true, reason: `breaker tripped — actual ${input.actual} ≥ 2× expected ${input.expected} with no progress/scope/evidence` };
137
+ }
138
+ if (!overDoubled) return { tripped: false, reason: `no breaker — actual ${input.actual} < 2× expected ${input.expected}` };
139
+ if (!noProgress) return { tripped: false, reason: 'no breaker — progress made' };
140
+ return { tripped: false, reason: 'no breaker — scope expanded or evidence gained' };
141
+ }
142
+
143
+ // ── Budget anomaly (§24, re-consumes slop 5k-zero-progress signal) ──
144
+
145
+ export type BudgetAnomalyInput = {
146
+ progress_before: number;
147
+ progress_after: number;
148
+ tokens_before: number;
149
+ tokens_after: number;
150
+ };
151
+
152
+ export type BudgetAnomaly = { anomaly: boolean; reason: string };
153
+
154
+ export function detectBudgetAnomaly(input: BudgetAnomalyInput): BudgetAnomaly {
155
+ const tokens_delta = input.tokens_after - input.tokens_before;
156
+ const progress_delta = input.progress_after - input.progress_before;
157
+ if (tokens_delta >= 5000 && progress_delta === 0) {
158
+ return { anomaly: true, reason: `anomaly — ${tokens_delta} tokens with no progress` };
159
+ }
160
+ if (tokens_delta < 5000 && progress_delta === 0) {
161
+ return { anomaly: false, reason: `no anomaly — ${tokens_delta} tokens below 5k floor` };
162
+ }
163
+ return { anomaly: false, reason: `no anomaly — progress ${progress_delta} over ${tokens_delta} tokens` };
164
+ }
165
+
166
+ // ── Decision trail (§41) ──
167
+
168
+ export function recordBudgetDecision(
169
+ missionDir: string,
170
+ d: { decision: string; reason: string; evidence?: string },
171
+ ): void {
172
+ recordOptDecision(missionDir, {
173
+ actor: 'budget-governor',
174
+ decision: d.decision,
175
+ reason: d.reason,
176
+ evidence: d.evidence,
177
+ });
178
+ }
package/src/args.ts CHANGED
@@ -6,11 +6,12 @@ export type Args = {
6
6
  flags: Record<string, FlagValue>;
7
7
  };
8
8
 
9
- const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target', '--before': 'before' };
9
+ const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target', '--before': 'before', '--backend': 'backend', '--mission': 'mission' };
10
10
  const BOOL_FLAGS: Record<string, string> = {
11
11
  '--global': 'global', '--yes': 'yes', '-y': 'yes', '--force': 'force',
12
12
  '--dry-run': 'dryRun', '--keep-logs': 'keepLogs', '--check': 'check', '--all': 'all', '--verify': 'verify',
13
- '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version',
13
+ '--gen-key': 'genKey', '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version',
14
+ '--json': 'json', '--ledger': 'ledger',
14
15
  };
15
16
 
16
17
  export function parseArgs(argv: string[]): Args {
package/src/budget.ts CHANGED
@@ -8,25 +8,16 @@
8
8
  // Token telemetry stays honest elsewhere: the estimator remains the default;
9
9
  // `tokens_source: reported` activates only where the harness exposes real
10
10
  // usage (see docs/concepts/cost.md). This module does not estimate tokens.
11
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
12
- import { homedir } from 'node:os';
11
+ import { existsSync, readdirSync, statSync } from 'node:fs';
13
12
  import { join } from 'node:path';
13
+ import { readConfig } from './config.ts';
14
14
 
15
15
  export function readBudgetConfig(projectDir: string): number {
16
- for (const base of [projectDir, homedir()]) {
17
- const file = join(base, '.mugiwara', 'config');
18
- if (!existsSync(file)) continue;
19
- for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
20
- const t = line.trim();
21
- if (!t || t.startsWith('#')) continue;
22
- const eq = t.indexOf('=');
23
- if (eq === -1) continue;
24
- if (t.slice(0, eq).trim() !== 'context_budget_chars') continue;
25
- const n = Number(t.slice(eq + 1).trim());
26
- return Number.isFinite(n) && n > 0 ? n : 0;
27
- }
28
- }
29
- return 0; // unset — measurement still recorded, never enforced
16
+ const cfg = readConfig(projectDir);
17
+ const raw = cfg.context_budget_chars;
18
+ if (raw === undefined || raw === '') return 0;
19
+ const n = Number(raw);
20
+ return Number.isFinite(n) && n > 0 ? n : 0;
30
21
  }
31
22
 
32
23
  /** Sum of bytes across the trail: top-level *.md + flows/* (legacy waves/* counts too). */
@@ -0,0 +1,45 @@
1
+ // src/check-artifacts.ts
2
+ // Mission artifact gate (roadmap v0.8 item 4): every Lane 2+ mission must
3
+ // carry its evidence trail — plan.md (the contract) and flows/* execution
4
+ // evidence — or the archive refuses to fold, same as the secret gate.
5
+ // Lane 0/1 missions are audit-lite: no plan/flows required.
6
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+
9
+ export interface ArtifactCheck {
10
+ ok: boolean;
11
+ missing: string[];
12
+ lane: string | null;
13
+ }
14
+
15
+ const LANE_MIN = new Set(['standard', 'full', 'spike']);
16
+
17
+ /** Lane 2+ (standard/full/spike) missions require the evidence trail. */
18
+ export function checkMissionArtifacts(missionDir: string): ArtifactCheck {
19
+ const statePath = join(missionDir, 'state.json');
20
+ if (!existsSync(statePath)) {
21
+ // archived mission (report.md survives, state folded away) — nothing to check
22
+ return { ok: true, missing: [], lane: null };
23
+ }
24
+ let lane = 'unknown';
25
+ try {
26
+ const s = JSON.parse(readFileSyncSafe(statePath)) as Record<string, unknown>;
27
+ if (typeof s.lane === 'string') lane = s.lane;
28
+ } catch { /* unreadable state → treat as unknown lane */ }
29
+
30
+ if (!LANE_MIN.has(lane)) {
31
+ return { ok: true, missing: [], lane }; // audit-lite lanes pass without trail
32
+ }
33
+
34
+ const missing: string[] = [];
35
+ if (!existsSync(join(missionDir, 'plan.md'))) missing.push('plan.md');
36
+ const flowsDir = join(missionDir, 'flows');
37
+ const hasFlows = existsSync(flowsDir) && readdirSync(flowsDir).length > 0;
38
+ if (!hasFlows) missing.push('flows/ (no execution evidence)');
39
+
40
+ return { ok: missing.length === 0, missing, lane };
41
+ }
42
+
43
+ function readFileSyncSafe(p: string): string {
44
+ try { return readFileSync(p, 'utf8'); } catch { return ''; }
45
+ }
package/src/cli.ts CHANGED
@@ -14,7 +14,12 @@ import { resetMission, archiveMission } from './mission.ts';
14
14
  import { runScript, RUNNABLE } from './run.ts';
15
15
  import { readContinue, readState, resolveContinue, formatTable, formatResume, gitActor } from './continue.ts';
16
16
  import { blamePath } from './provenance.ts';
17
- import { signReport, verifyReport } from './sign.ts';
17
+ import { signReport, verifyReport, ensurePureKey, hasMinisign } from './sign.ts';
18
+ import { ensureConfig } from './config.ts';
19
+ import { costEnvelope } from './cost.ts';
20
+ import { computeLiveSlop } from './slop.ts';
21
+ import { loadRegistry } from './evidence.ts';
22
+ import { buildCostLedger, toCostJSON } from './reporting.ts';
18
23
 
19
24
  const str = (v: FlagValue): string | undefined => (typeof v === 'string' ? v : undefined);
20
25
  const flag = (v: FlagValue): boolean => v === true;
@@ -23,6 +28,23 @@ export async function run(argv: string[]): Promise<void> {
23
28
  const { command, flags, _ } = parseArgs(argv);
24
29
  if (flag(flags.help) || command === 'help') return help();
25
30
  if (flag(flags.version)) { console.log(`mugiwara ${VERSION}`); return; }
31
+ // `continue` and `status` are read-only position commands: dispatch before
32
+ // config bootstrap so a fresh project never gets a .mugiwara/config created
33
+ // and no setup chatter is printed before missions/members are listed.
34
+ if (command === 'continue' || command === 'status') {
35
+ return command === 'continue' ? continueCmd(flags, _) : statusCmd(flags);
36
+ }
37
+ // A command on a fresh project must be immediately usable — bootstrap the
38
+ // default .mugiwara/config when it is missing (not only at install time).
39
+ // Skipped for install/update --dry-run: a dry run must not mutate the
40
+ // project (the installer writes the config itself on a real install).
41
+ const isDryRunInstall = (command === 'install' || command === 'update') && flag(flags.dryRun);
42
+ if (!isDryRunInstall) {
43
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
44
+ if (ensureConfig(projectDir)) {
45
+ console.log(`default .mugiwara/config written at ${join(projectDir, '.mugiwara', 'config')} (edit it to customise)`);
46
+ }
47
+ }
26
48
  switch (command) {
27
49
  case 'install': return install(flags);
28
50
  case 'update': return install({ ...flags, force: true });
@@ -33,6 +55,7 @@ export async function run(argv: string[]): Promise<void> {
33
55
  case 'clean': return cleanCmd(flags);
34
56
  case 'continue': return continueCmd(flags, _);
35
57
  case 'status': return statusCmd(flags);
58
+ case 'cost': return costCmd(flags, _);
36
59
  case 'run': return runCmd(flags, _);
37
60
  case 'savepoint': return runCmd(flags, ['run', 'savepoint.sh', ..._.slice(1)]);
38
61
  case 'blame': return blameCmd(flags, _);
@@ -350,6 +373,61 @@ function statusCmd(flags: Args['flags']): void {
350
373
  }
351
374
  }
352
375
 
376
+ /** `mugiwara cost [--mission <id>] [--json] [--ledger]` — show cost ledger, avoided work, efficiency, trail. */
377
+ function costCmd(flags: Args['flags'], positionals: string[]): void {
378
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
379
+ const mission = str(flags.mission) ?? positionals[1] ?? (() => {
380
+ const states = readState(projectDir);
381
+ if (states.length === 1) return states[0].mission;
382
+ if (states.length > 1) {
383
+ console.error('multiple missions in flight — specify --mission <id>');
384
+ process.exit(1);
385
+ }
386
+ return null;
387
+ })();
388
+ if (!mission) {
389
+ console.error('usage: mugiwara cost [--mission <id>] [--json] [--ledger] [--project <dir>]');
390
+ process.exit(1);
391
+ }
392
+ const missionDir = join(projectDir, '.mugiwara', 'missions', mission);
393
+ if (!existsSync(missionDir)) {
394
+ console.error(`No cost ledger found for mission "${mission}"`);
395
+ process.exit(1);
396
+ }
397
+ const states = readState(projectDir).filter((s) => s.mission === mission);
398
+ const envelope = states.length
399
+ ? costEnvelope({ lane: (states[0] as unknown as { lane?: string }).lane, budget: (states[0] as unknown as { budget?: number }).budget, tokens_est: (states[0] as unknown as { tokens_est?: number }).tokens_est })
400
+ : costEnvelope({ lane: 'full', tokens_est: 0 });
401
+ // live slop (§3.3): run existing detectors over state already available
402
+ // (heal cycle, context registry repeated reads) so slop_interventions is real.
403
+ const state0 = states[0] as unknown as { heal_cycle?: number };
404
+ let repeatedReads = 0;
405
+ try {
406
+ const reg = loadRegistry(missionDir);
407
+ repeatedReads = reg.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
408
+ } catch {
409
+ repeatedReads = 0;
410
+ }
411
+ const liveSlop = computeLiveSlop({ heal_cycle: state0?.heal_cycle ?? 0, repeated_reads: repeatedReads });
412
+ const ledger = buildCostLedger({ missionDir, envelope, slopSummary: { interventions: liveSlop.interventions } });
413
+ if (flag(flags.json)) {
414
+ console.log(toCostJSON(ledger));
415
+ return;
416
+ }
417
+ console.log(`Cost envelope: ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned})`);
418
+ console.log(`Avoided: ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ~${ledger.avoided.tokens_avoided_est} tokens`);
419
+ console.log(`Efficiency: reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}%`);
420
+ if (ledger.avoided.slop_interventions > 0) {
421
+ const roles = Object.entries(liveSlop.perRole).map(([r, n]) => `${r}:${n}`).join(', ');
422
+ console.log(`Slop: ${ledger.avoided.slop_interventions} intervention(s) — ${roles}`);
423
+ }
424
+ console.log(`Trail: ${ledger.trail.length} decisions`);
425
+ if (flag(flags.ledger) && ledger.trail.length) {
426
+ for (const t of ledger.trail.slice(0, 20)) console.log(`- ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ''}`);
427
+ if (ledger.trail.length > 20) console.log(`… ${ledger.trail.length - 20} more`);
428
+ }
429
+ }
430
+
353
431
  /** `mugiwara run <script.sh> [args]` — run a bundled harness script here. */
354
432
  function runCmd(flags: Args['flags'], positionals: string[]): void {
355
433
  const projectDir = resolve(str(flags.project) ?? process.cwd());
@@ -430,11 +508,27 @@ function handoffCmd(flags: Args['flags'], positionals: string[]): void {
430
508
  console.log(`\nwritten: ${out}`);
431
509
  }
432
510
 
433
- /** `mugiwara sign <mission>` / `--verify` — optional minisign attestation. */
511
+ /** `mugiwara sign <mission>` / `--verify` / `--gen-key` — optional attestation. */
434
512
  function signCmd(flags: Args['flags'], _: string[]): void {
435
513
  const projectDir = resolve(str(flags.project) ?? process.cwd());
514
+ if (flag(flags.genKey)) {
515
+ const backend = str(flags.backend) ?? 'auto';
516
+ const home = homedir();
517
+ if (backend === 'minisign') {
518
+ if (!hasMinisign()) { console.error('✗ minisign not installed — cannot generate keys with this backend'); process.exit(1); }
519
+ try {
520
+ execFileSync('minisign', ['-G'], { stdio: 'inherit' });
521
+ console.log('✓ minisign key pair generated in ~/.mugiwara/');
522
+ return;
523
+ } catch { console.error('✗ key generation failed'); process.exit(1); }
524
+ }
525
+ // pure (default)
526
+ const dir = ensurePureKey(home);
527
+ console.log(`✓ pure ed25519 key pair ready: ${join(dir, 'mugiwara.key')} / ${join(dir, 'mugiwara.pub')}`);
528
+ return;
529
+ }
436
530
  const mission = _[1];
437
- if (!mission) { console.error('usage: mugiwara sign <mission> [--verify] [--project <dir>]'); process.exit(1); }
531
+ if (!mission) { console.error('usage: mugiwara sign <mission> [--verify] [--gen-key [--backend pure|minisign]] [--project <dir>]'); process.exit(1); }
438
532
  const missionDir = join(projectDir, '.mugiwara', 'missions', mission);
439
533
  if (!existsSync(missionDir)) { console.error(`no mission dir: ${missionDir}`); process.exit(1); }
440
534
  const r = flag(flags.verify) ? verifyReport(projectDir, missionDir) : signReport(projectDir, missionDir);
@@ -459,11 +553,15 @@ Usage:
459
553
  mugiwara continue <m> [member]
460
554
  print the exact resume point for that mission/member
461
555
  mugiwara status computed mission state: wave, tasks, lane, blockers, budget
556
+ mugiwara cost [--mission <id>] [--json] [--ledger]
557
+ show cost ledger, avoided work, efficiency, trail (human + JSON)
462
558
  mugiwara blame <path> provenance note on the last commit touching <path>
463
559
  (fetch notes first: git fetch origin 'refs/notes/mugiwara:refs/notes/mugiwara')
464
560
  mugiwara handoff <m> write .mugiwara/missions/<m>/handoff.md — a report the next
465
561
  engineer can act on (computed state + staleness check)
466
- mugiwara sign <m> optional attestation: minisign-sign report.md (--verify to check)
562
+ mugiwara sign <m> attestation: sign report.md (auto/minisign/pure/off; --verify to check)
563
+ mugiwara sign --gen-key [--backend pure|minisign]
564
+ create signing keys (pure ed25519 default)
467
565
  mugiwara run <script> [args...]
468
566
  run a bundled harness script here (${RUNNABLE.join(', ')})
469
567
  mugiwara savepoint <mission> [member] [flow] [mode]