@ionivetech/mugiwara 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.opencode/mugiwara-helpers.mjs +2 -2
- package/README.md +196 -330
- package/content/agents/brook-healing.md +1 -1
- package/content/agents/franky-gates.md +1 -1
- package/content/agents/luffy-orchestrator.md +2 -2
- package/content/agents/memory-keeper.md +5 -0
- package/content/agents/usopp-brainstorm.md +3 -2
- package/content/agents/zoro-execution.md +4 -3
- package/content/skills/mugiwara-backend/SKILL.md +52 -43
- package/content/skills/mugiwara-brainstorm/SKILL.md +5 -3
- package/content/skills/mugiwara-checkpoint/SKILL.md +21 -8
- package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
- package/content/skills/mugiwara-execution/SKILL.md +34 -33
- package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
- package/content/skills/mugiwara-execution/references/execution-phase-flows.md +18 -0
- package/content/skills/mugiwara-frontend/SKILL.md +44 -44
- package/content/skills/mugiwara-gates/SKILL.md +28 -16
- package/content/skills/mugiwara-healing/SKILL.md +30 -25
- package/content/skills/mugiwara-lessons/SKILL.md +3 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +10 -9
- package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
- package/content/skills/mugiwara-planning/SKILL.md +28 -14
- package/content/skills/mugiwara-planning/references/large-campaign-subplan.md +41 -0
- package/content/skills/mugiwara-planning/references/plan-template.md +22 -0
- package/content/skills/mugiwara-quality/SKILL.md +8 -13
- package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
- package/content/skills/mugiwara-resume/SKILL.md +3 -9
- package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
- package/content/skills/mugiwara-review/SKILL.md +17 -24
- package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
- package/content/skills/mugiwara-security/SKILL.md +47 -35
- package/content/skills/mugiwara-ship/SKILL.md +2 -0
- package/content/skills/mugiwara-workflow/SKILL.md +13 -13
- package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +6 -3
- package/dist/mugiwara.js +1802 -316
- package/gemini-extension.json +1 -1
- package/hooks/mugiwara-mode-tracker.js +24 -4
- package/hooks/mugiwara-mode-tracker.ts +36 -7
- package/hooks/pipeline-guard.js +1 -1
- package/hooks/pipeline-guard.ts +2 -1
- package/hooks/session-start.js +6 -1
- package/hooks/session-start.ts +8 -1
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/cost-governor.md +104 -0
- package/references/multi-actor.md +21 -0
- package/references/posture-routing.md +31 -0
- package/references/wave-banners.md +1 -2
- package/scripts/benchmark-governor.ts +516 -0
- package/scripts/benchmark-thresholds.json +47 -0
- package/scripts/check-doc-links.ts +8 -2
- package/scripts/gate-selftest.ts +104 -21
- package/scripts/lib/lane-base.sh +4 -4
- package/scripts/retrieval-eval.ts +9 -3
- package/scripts/savepoint.sh +41 -2
- package/scripts/validate-content.ts +82 -3
- package/scripts/verify-install.ts +20 -0
- package/scripts/write-metrics.ts +73 -0
- package/src/adaptive-budget.ts +178 -0
- package/src/args.ts +3 -2
- package/src/budget.ts +18 -16
- package/src/check-artifacts.ts +45 -0
- package/src/cli.ts +221 -8
- package/src/cognition.ts +234 -0
- package/src/config.ts +113 -0
- package/src/context.ts +72 -0
- package/src/continue.ts +29 -0
- package/src/cost.ts +189 -0
- package/src/evidence.ts +160 -0
- package/src/installer.ts +2 -16
- package/src/integrity.ts +65 -16
- package/src/investigation.ts +72 -0
- package/src/mission.ts +246 -16
- package/src/policy.ts +355 -2
- package/src/posture.ts +86 -0
- package/src/provenance.ts +29 -9
- package/src/reporting.ts +225 -0
- package/src/scope.ts +321 -0
- package/src/sign.ts +234 -18
- package/src/slop.ts +306 -0
- package/src/work.ts +273 -0
|
@@ -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). */
|
|
@@ -54,3 +45,14 @@ export function formatFootprint(chars: number, budget: number): string {
|
|
|
54
45
|
? `${base} — OVER budget ${budget}`
|
|
55
46
|
: `${base} (budget ${budget})`;
|
|
56
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
|
+
}
|
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// src/cli.ts
|
|
3
|
-
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -12,9 +12,15 @@ import { installTo, removeInstalled, VERSION, ensureProjectGitignore, removeProj
|
|
|
12
12
|
import { manifestPath, readManifest, writeManifest, type Scope } from './manifest.ts';
|
|
13
13
|
import { resetMission, archiveMission } from './mission.ts';
|
|
14
14
|
import { runScript, RUNNABLE } from './run.ts';
|
|
15
|
-
import { readContinue, readState, resolveContinue, formatTable, formatResume, gitActor } from './continue.ts';
|
|
15
|
+
import { readContinue, readState, resolveContinue, formatTable, formatResume, gitActor, hasLegacyLayout, CURRENT_SCHEMA_VERSION } 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';
|
|
23
|
+
import { enforceHarnessPolicy } from './policy.ts';
|
|
18
24
|
|
|
19
25
|
const str = (v: FlagValue): string | undefined => (typeof v === 'string' ? v : undefined);
|
|
20
26
|
const flag = (v: FlagValue): boolean => v === true;
|
|
@@ -23,6 +29,30 @@ export async function run(argv: string[]): Promise<void> {
|
|
|
23
29
|
const { command, flags, _ } = parseArgs(argv);
|
|
24
30
|
if (flag(flags.help) || command === 'help') return help();
|
|
25
31
|
if (flag(flags.version)) { console.log(`mugiwara ${VERSION}`); return; }
|
|
32
|
+
// harness.require_enforcement — enterprise gate: refuse rules-based harnesses
|
|
33
|
+
// (only opencode is runtime-enforced). Covers run/savepoint/archive/status
|
|
34
|
+
// + other workflow commands; install/update/uninstall/list are setup and bypass.
|
|
35
|
+
{
|
|
36
|
+
const bypass = new Set(['install', 'update', 'uninstall', 'list']);
|
|
37
|
+
if (!bypass.has(command)) {
|
|
38
|
+
const projectDirForHarness = resolve(str(flags.project) ?? process.cwd());
|
|
39
|
+
enforceHarnessPolicy(projectDirForHarness);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Bootstrap default .mugiwara/config on any command when missing — including
|
|
43
|
+
// `continue`/`status` — so tier-3 agents that only mkdir .mugiwara still get
|
|
44
|
+
// a config. Also covered by readConfig() auto-create for non-CLI entry.
|
|
45
|
+
// Skipped for install/update --dry-run: a dry run must not mutate the project.
|
|
46
|
+
const isDryRunInstall = (command === 'install' || command === 'update') && flag(flags.dryRun);
|
|
47
|
+
if (!isDryRunInstall) {
|
|
48
|
+
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
49
|
+
if (ensureConfig(projectDir)) {
|
|
50
|
+
console.log(`default .mugiwara/config written at ${join(projectDir, '.mugiwara', 'config')} (edit it to customise)`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (command === 'continue' || command === 'status') {
|
|
54
|
+
return command === 'continue' ? continueCmd(flags, _) : statusCmd(flags);
|
|
55
|
+
}
|
|
26
56
|
switch (command) {
|
|
27
57
|
case 'install': return install(flags);
|
|
28
58
|
case 'update': return install({ ...flags, force: true });
|
|
@@ -33,11 +63,13 @@ export async function run(argv: string[]): Promise<void> {
|
|
|
33
63
|
case 'clean': return cleanCmd(flags);
|
|
34
64
|
case 'continue': return continueCmd(flags, _);
|
|
35
65
|
case 'status': return statusCmd(flags);
|
|
66
|
+
case 'cost': return costCmd(flags, _);
|
|
36
67
|
case 'run': return runCmd(flags, _);
|
|
37
68
|
case 'savepoint': return runCmd(flags, ['run', 'savepoint.sh', ..._.slice(1)]);
|
|
38
69
|
case 'blame': return blameCmd(flags, _);
|
|
39
70
|
case 'handoff': return handoffCmd(flags, _);
|
|
40
71
|
case 'sign': return signCmd(flags, _);
|
|
72
|
+
case 'migrate': return migrateCmd(flags);
|
|
41
73
|
default: throw new Error(`Unknown command: ${command}`);
|
|
42
74
|
}
|
|
43
75
|
}
|
|
@@ -259,9 +291,27 @@ async function uninstall(flags: Args['flags']): Promise<void> {
|
|
|
259
291
|
console.log(`OK removed ${removed.length} files`);
|
|
260
292
|
}
|
|
261
293
|
|
|
294
|
+
function legacyWarning(projectDir: string): void {
|
|
295
|
+
if (hasLegacyLayout(projectDir)) {
|
|
296
|
+
console.error('⚠ legacy layout detected (v0.6 .mugiwara/state/ — run `mugiwara migrate` to move to missions/)');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function schemaWarnings(projectDir: string): void {
|
|
301
|
+
const states = readState(projectDir);
|
|
302
|
+
for (const s of states) {
|
|
303
|
+
const v = s.schema_version;
|
|
304
|
+
if (v !== CURRENT_SCHEMA_VERSION) {
|
|
305
|
+
const wrote = v === null || v === undefined || v === '' ? 'unknown' : String(v);
|
|
306
|
+
console.error(`⚠ state written by v${wrote} (mission ${s.mission}${s.member ? `/${s.member}` : ''}) — current expects v${CURRENT_SCHEMA_VERSION} — run \`mugiwara migrate\``);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
262
311
|
function list(flags: Args['flags']): void {
|
|
263
312
|
const home = homedir();
|
|
264
313
|
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
314
|
+
legacyWarning(projectDir);
|
|
265
315
|
let found = false;
|
|
266
316
|
for (const [label, file] of [
|
|
267
317
|
['project', manifestPath({ scope: 'project', projectDir, home })],
|
|
@@ -293,6 +343,8 @@ function list(flags: Args['flags']): void {
|
|
|
293
343
|
*/
|
|
294
344
|
function continueCmd(flags: Args['flags'], positionals: string[]): void {
|
|
295
345
|
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
346
|
+
legacyWarning(projectDir);
|
|
347
|
+
schemaWarnings(projectDir);
|
|
296
348
|
const [mission, member] = positionals.slice(1);
|
|
297
349
|
let entries = readContinue(projectDir);
|
|
298
350
|
|
|
@@ -335,6 +387,8 @@ function continueCmd(flags: Args['flags'], positionals: string[]): void {
|
|
|
335
387
|
/** `mugiwara status` — one screen of computed mission state, no model needed. */
|
|
336
388
|
function statusCmd(flags: Args['flags']): void {
|
|
337
389
|
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
390
|
+
legacyWarning(projectDir);
|
|
391
|
+
schemaWarnings(projectDir);
|
|
338
392
|
const states = readState(projectDir);
|
|
339
393
|
if (!states.length) { console.log('No mission state on disk.'); return; }
|
|
340
394
|
const actor = flag(flags.all) ? null : gitActor(projectDir);
|
|
@@ -350,6 +404,61 @@ function statusCmd(flags: Args['flags']): void {
|
|
|
350
404
|
}
|
|
351
405
|
}
|
|
352
406
|
|
|
407
|
+
/** `mugiwara cost [--mission <id>] [--json] [--ledger]` — show cost ledger, avoided work, efficiency, trail. */
|
|
408
|
+
function costCmd(flags: Args['flags'], positionals: string[]): void {
|
|
409
|
+
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
410
|
+
const mission = str(flags.mission) ?? positionals[1] ?? (() => {
|
|
411
|
+
const states = readState(projectDir);
|
|
412
|
+
if (states.length === 1) return states[0].mission;
|
|
413
|
+
if (states.length > 1) {
|
|
414
|
+
console.error('multiple missions in flight — specify --mission <id>');
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
})();
|
|
419
|
+
if (!mission) {
|
|
420
|
+
console.error('usage: mugiwara cost [--mission <id>] [--json] [--ledger] [--project <dir>]');
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
const missionDir = join(projectDir, '.mugiwara', 'missions', mission);
|
|
424
|
+
if (!existsSync(missionDir)) {
|
|
425
|
+
console.error(`No cost ledger found for mission "${mission}"`);
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
const states = readState(projectDir).filter((s) => s.mission === mission);
|
|
429
|
+
const envelope = states.length
|
|
430
|
+
? 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 })
|
|
431
|
+
: costEnvelope({ lane: 'full', tokens_est: 0 });
|
|
432
|
+
// live slop (§3.3): run existing detectors over state already available
|
|
433
|
+
// (heal cycle, context registry repeated reads) so slop_interventions is real.
|
|
434
|
+
const state0 = states[0] as unknown as { heal_cycle?: number };
|
|
435
|
+
let repeatedReads = 0;
|
|
436
|
+
try {
|
|
437
|
+
const reg = loadRegistry(missionDir);
|
|
438
|
+
repeatedReads = reg.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
|
|
439
|
+
} catch {
|
|
440
|
+
repeatedReads = 0;
|
|
441
|
+
}
|
|
442
|
+
const liveSlop = computeLiveSlop({ heal_cycle: state0?.heal_cycle ?? 0, repeated_reads: repeatedReads });
|
|
443
|
+
const ledger = buildCostLedger({ missionDir, envelope, slopSummary: { interventions: liveSlop.interventions } });
|
|
444
|
+
if (flag(flags.json)) {
|
|
445
|
+
console.log(toCostJSON(ledger));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
console.log(`Cost envelope: ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned})`);
|
|
449
|
+
console.log(`Avoided: ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ~${ledger.avoided.tokens_avoided_est} tokens`);
|
|
450
|
+
console.log(`Efficiency: reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}%`);
|
|
451
|
+
if (ledger.avoided.slop_interventions > 0) {
|
|
452
|
+
const roles = Object.entries(liveSlop.perRole).map(([r, n]) => `${r}:${n}`).join(', ');
|
|
453
|
+
console.log(`Slop: ${ledger.avoided.slop_interventions} intervention(s) — ${roles}`);
|
|
454
|
+
}
|
|
455
|
+
console.log(`Trail: ${ledger.trail.length} decisions`);
|
|
456
|
+
if (flag(flags.ledger) && ledger.trail.length) {
|
|
457
|
+
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}` : ''}`);
|
|
458
|
+
if (ledger.trail.length > 20) console.log(`… ${ledger.trail.length - 20} more`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
353
462
|
/** `mugiwara run <script.sh> [args]` — run a bundled harness script here. */
|
|
354
463
|
function runCmd(flags: Args['flags'], positionals: string[]): void {
|
|
355
464
|
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
@@ -430,11 +539,109 @@ function handoffCmd(flags: Args['flags'], positionals: string[]): void {
|
|
|
430
539
|
console.log(`\nwritten: ${out}`);
|
|
431
540
|
}
|
|
432
541
|
|
|
433
|
-
|
|
542
|
+
export function migrateCmd(flags: Args['flags']): void {
|
|
543
|
+
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
544
|
+
const dryRun = flag(flags.dryRun);
|
|
545
|
+
const legacyState = join(projectDir, '.mugiwara', 'state');
|
|
546
|
+
const legacyContinue = join(projectDir, '.mugiwara', 'continue');
|
|
547
|
+
const missionsRoot = join(projectDir, '.mugiwara', 'missions');
|
|
548
|
+
const moves: Array<{ src: string; dest: string }> = [];
|
|
549
|
+
|
|
550
|
+
const collect = (srcRoot: string, isContinue: boolean) => {
|
|
551
|
+
if (!existsSync(srcRoot)) return;
|
|
552
|
+
const walk = (dir: string) => {
|
|
553
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
554
|
+
const full = join(dir, e.name);
|
|
555
|
+
if (e.isDirectory()) walk(full);
|
|
556
|
+
else if (e.isFile() && e.name.endsWith('.json')) {
|
|
557
|
+
const rel = full.slice(srcRoot.length + 1);
|
|
558
|
+
let destRel: string;
|
|
559
|
+
if (isContinue) {
|
|
560
|
+
const parts = rel.split('/');
|
|
561
|
+
const file = parts.pop()!;
|
|
562
|
+
const mission = parts.join('/');
|
|
563
|
+
const stem = file.slice(0, -'.json'.length);
|
|
564
|
+
let destFile: string;
|
|
565
|
+
if (stem === 'state') destFile = 'continue.json';
|
|
566
|
+
else destFile = `continue-${stem}.json`;
|
|
567
|
+
destRel = mission ? join(mission, destFile) : destFile;
|
|
568
|
+
} else {
|
|
569
|
+
destRel = rel;
|
|
570
|
+
}
|
|
571
|
+
moves.push({ src: full, dest: join(missionsRoot, destRel) });
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
walk(srcRoot);
|
|
576
|
+
};
|
|
577
|
+
collect(legacyState, false);
|
|
578
|
+
collect(legacyContinue, true);
|
|
579
|
+
|
|
580
|
+
// legacy flat: .mugiwara/state.json style? treat any top-level .mugiwara/state*.json as not legacy missions but still warn
|
|
581
|
+
// Already covered by state/ dir; nothing more to collect.
|
|
582
|
+
|
|
583
|
+
if (!moves.length) {
|
|
584
|
+
if (!existsSync(legacyState) && !existsSync(legacyContinue)) {
|
|
585
|
+
console.log('no legacy layout found (.mugiwara/state/ does not exist)');
|
|
586
|
+
} else {
|
|
587
|
+
console.log('no legacy state files to migrate');
|
|
588
|
+
}
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
for (const m of moves) {
|
|
593
|
+
console.log(`${dryRun ? 'would migrate' : 'migrated'} ${m.src} → ${m.dest}`);
|
|
594
|
+
if (!dryRun) {
|
|
595
|
+
mkdirSync(dirname(m.dest), { recursive: true });
|
|
596
|
+
try {
|
|
597
|
+
const raw = JSON.parse(readFileSync(m.src, 'utf8')) as Record<string, unknown>;
|
|
598
|
+
raw.schema_version = CURRENT_SCHEMA_VERSION;
|
|
599
|
+
writeFileSync(m.dest, JSON.stringify(raw, null, 2) + '\n');
|
|
600
|
+
rmSync(m.src, { force: true });
|
|
601
|
+
} catch {
|
|
602
|
+
try { renameSync(m.src, m.dest); } catch { /* ignore */ }
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (!dryRun) {
|
|
607
|
+
const prune = (root: string) => {
|
|
608
|
+
if (!existsSync(root)) return;
|
|
609
|
+
const walkPrune = (dir: string) => {
|
|
610
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
611
|
+
if (e.isDirectory()) walkPrune(join(dir, e.name));
|
|
612
|
+
}
|
|
613
|
+
try { if (readdirSync(dir).length === 0) rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
614
|
+
};
|
|
615
|
+
walkPrune(root);
|
|
616
|
+
try { if (existsSync(root) && readdirSync(root).length === 0) rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
617
|
+
};
|
|
618
|
+
prune(legacyState);
|
|
619
|
+
prune(legacyContinue);
|
|
620
|
+
}
|
|
621
|
+
console.log(`${dryRun ? 'would migrate' : 'migrated'} ${moves.length} file(s)${dryRun ? ' (dry run)' : ''}`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** `mugiwara sign <mission>` / `--verify` / `--gen-key` — optional attestation. */
|
|
434
625
|
function signCmd(flags: Args['flags'], _: string[]): void {
|
|
435
626
|
const projectDir = resolve(str(flags.project) ?? process.cwd());
|
|
627
|
+
if (flag(flags.genKey)) {
|
|
628
|
+
const backend = str(flags.backend) ?? 'auto';
|
|
629
|
+
const home = homedir();
|
|
630
|
+
if (backend === 'minisign') {
|
|
631
|
+
if (!hasMinisign()) { console.error('✗ minisign not installed — cannot generate keys with this backend'); process.exit(1); }
|
|
632
|
+
try {
|
|
633
|
+
execFileSync('minisign', ['-G'], { stdio: 'inherit' });
|
|
634
|
+
console.log('✓ minisign key pair generated in ~/.mugiwara/');
|
|
635
|
+
return;
|
|
636
|
+
} catch { console.error('✗ key generation failed'); process.exit(1); }
|
|
637
|
+
}
|
|
638
|
+
// pure (default)
|
|
639
|
+
const dir = ensurePureKey(home);
|
|
640
|
+
console.log(`✓ pure ed25519 key pair ready: ${join(dir, 'mugiwara.key')} / ${join(dir, 'mugiwara.pub')}`);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
436
643
|
const mission = _[1];
|
|
437
|
-
if (!mission) { console.error('usage: mugiwara sign <mission> [--verify] [--project <dir>]'); process.exit(1); }
|
|
644
|
+
if (!mission) { console.error('usage: mugiwara sign <mission> [--verify] [--gen-key [--backend pure|minisign]] [--project <dir>]'); process.exit(1); }
|
|
438
645
|
const missionDir = join(projectDir, '.mugiwara', 'missions', mission);
|
|
439
646
|
if (!existsSync(missionDir)) { console.error(`no mission dir: ${missionDir}`); process.exit(1); }
|
|
440
647
|
const r = flag(flags.verify) ? verifyReport(projectDir, missionDir) : signReport(projectDir, missionDir);
|
|
@@ -459,15 +666,21 @@ Usage:
|
|
|
459
666
|
mugiwara continue <m> [member]
|
|
460
667
|
print the exact resume point for that mission/member
|
|
461
668
|
mugiwara status computed mission state: wave, tasks, lane, blockers, budget
|
|
669
|
+
mugiwara cost [--mission <id>] [--json] [--ledger]
|
|
670
|
+
show cost ledger, avoided work, efficiency, trail (human + JSON)
|
|
462
671
|
mugiwara blame <path> provenance note on the last commit touching <path>
|
|
463
672
|
(fetch notes first: git fetch origin 'refs/notes/mugiwara:refs/notes/mugiwara')
|
|
464
673
|
mugiwara handoff <m> write .mugiwara/missions/<m>/handoff.md — a report the next
|
|
465
674
|
engineer can act on (computed state + staleness check)
|
|
466
|
-
mugiwara sign <m>
|
|
675
|
+
mugiwara sign <m> attestation: sign report.md (auto/minisign/pure/off; --verify to check)
|
|
676
|
+
mugiwara sign --gen-key [--backend pure|minisign]
|
|
677
|
+
create signing keys (pure ed25519 default)
|
|
678
|
+
mugiwara migrate [--dry-run] [--project <dir>]
|
|
679
|
+
move legacy .mugiwara/state/ layout to .mugiwara/missions/
|
|
467
680
|
mugiwara run <script> [args...]
|
|
468
|
-
|
|
681
|
+
run a bundled harness script here (${RUNNABLE.join(', ')})
|
|
469
682
|
mugiwara savepoint <mission> [member] [flow] [mode]
|
|
470
|
-
|
|
683
|
+
shorthand for: mugiwara run savepoint.sh ...
|
|
471
684
|
mugiwara --help this help
|
|
472
685
|
mugiwara --version print version
|
|
473
686
|
|