@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.
- 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 +194 -328
- package/content/agents/franky-gates.md +1 -1
- package/content/agents/luffy-orchestrator.md +2 -2
- package/content/skills/mugiwara-backend/SKILL.md +52 -43
- package/content/skills/mugiwara-checkpoint/SKILL.md +19 -8
- package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
- package/content/skills/mugiwara-execution/SKILL.md +32 -32
- 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 +22 -16
- package/content/skills/mugiwara-healing/SKILL.md +26 -25
- package/content/skills/mugiwara-orchestration/SKILL.md +6 -6
- package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
- package/content/skills/mugiwara-planning/SKILL.md +26 -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 +19 -13
- package/content/skills/mugiwara-resume/SKILL.md +6 -1
- package/content/skills/mugiwara-review/SKILL.md +17 -12
- package/content/skills/mugiwara-security/SKILL.md +46 -35
- package/content/skills/mugiwara-workflow/SKILL.md +6 -9
- package/content/skills/mugiwara-workflow/references/adaptive-budget-governor.md +5 -0
- package/content/skills/mugiwara-workflow/references/benchmark-governor.md +53 -0
- package/content/skills/mugiwara-workflow/references/cognitive-output-governor.md +5 -0
- package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
- package/content/skills/mugiwara-workflow/references/scope-code-governor.md +14 -0
- package/content/skills/mugiwara-workflow/references/stop-slop-governor.md +14 -0
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +6 -3
- package/dist/mugiwara.js +925 -253
- package/gemini-extension.json +1 -1
- package/hooks/pipeline-guard.js +1 -1
- package/hooks/pipeline-guard.ts +2 -1
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/multi-actor.md +21 -0
- package/references/posture-routing.md +31 -0
- 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 +20 -0
- package/scripts/lib/lane-base.sh +4 -4
- package/scripts/retrieval-eval.ts +9 -3
- package/scripts/savepoint.sh +20 -1
- package/scripts/validate-content.ts +22 -3
- package/src/adaptive-budget.ts +178 -0
- package/src/args.ts +3 -2
- package/src/budget.ts +7 -16
- package/src/check-artifacts.ts +45 -0
- package/src/cli.ts +102 -4
- package/src/cognition.ts +234 -0
- package/src/config.ts +107 -0
- package/src/context.ts +72 -0
- package/src/cost.ts +186 -0
- package/src/evidence.ts +160 -0
- package/src/installer.ts +2 -16
- package/src/integrity.ts +1 -1
- package/src/investigation.ts +72 -0
- package/src/mission.ts +124 -10
- package/src/posture.ts +86 -0
- package/src/reporting.ts +225 -0
- package/src/scope.ts +321 -0
- package/src/sign.ts +194 -20
- package/src/slop.ts +306 -0
- 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"])
|
|
20
|
-
|
|
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) {
|
package/scripts/gate-selftest.ts
CHANGED
|
@@ -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
|
{
|
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
|
@@ -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', '
|
|
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=
|
|
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,
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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>
|
|
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]
|