@ionivetech/mugiwara 0.6.6 → 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 +2 -7
- package/.codex-plugin/plugin.json +2 -7
- package/.cursor-plugin/plugin.json +2 -7
- package/.kimi-plugin/plugin.json +2 -7
- package/.opencode/mugiwara-helpers.mjs +2 -2
- package/.opencode/plugins/mugiwara.mjs +3 -18
- package/AGENTS.md +5 -4
- package/GEMINI.md +3 -3
- package/README.md +203 -331
- package/content/agents/brook-healing.md +8 -4
- package/content/agents/chopper-checkpoint.md +7 -3
- package/content/agents/eval-runner.md +4 -4
- package/content/agents/franky-gates.md +4 -4
- package/content/agents/jinbe-security.md +6 -7
- package/content/agents/luffy-orchestrator.md +11 -13
- package/content/agents/memory-keeper.md +4 -4
- package/content/agents/nami-planner.md +4 -4
- package/content/agents/resume-coordinator.md +7 -7
- package/content/agents/robin-reviewer.md +7 -3
- package/content/agents/sanji-quality.md +2 -2
- package/content/agents/skeptic-verifier.md +2 -2
- package/content/agents/usopp-brainstorm.md +3 -3
- package/content/agents/zoro-execution.md +6 -6
- package/content/skills/mugiwara-backend/SKILL.md +54 -43
- package/content/skills/mugiwara-backend/references/database.md +61 -0
- package/content/skills/mugiwara-brainstorm/SKILL.md +1 -1
- package/content/skills/mugiwara-checkpoint/SKILL.md +23 -11
- package/content/skills/mugiwara-checkpoint/references/ledger-format.md +1 -1
- package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
- package/content/skills/mugiwara-execution/SKILL.md +37 -37
- 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-execution/references/resume-batching.md +2 -2
- package/content/skills/mugiwara-frontend/SKILL.md +44 -43
- package/content/skills/mugiwara-frontend/references/checklist.md +14 -0
- package/content/skills/mugiwara-gates/SKILL.md +23 -14
- package/content/skills/mugiwara-healing/SKILL.md +26 -25
- package/content/skills/mugiwara-lessons/SKILL.md +2 -2
- package/content/skills/mugiwara-orchestration/SKILL.md +11 -11
- package/content/skills/mugiwara-orchestration/references/check-ins.md +2 -2
- package/content/skills/mugiwara-orchestration/references/closure.md +17 -12
- package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +11 -1
- package/content/skills/mugiwara-planning/SKILL.md +26 -29
- package/content/skills/mugiwara-planning/references/large-campaign-subplan.md +41 -0
- package/content/skills/mugiwara-planning/references/plan-template.md +24 -2
- package/content/skills/mugiwara-quality/SKILL.md +21 -12
- package/content/skills/mugiwara-resume/SKILL.md +8 -3
- package/content/skills/mugiwara-review/SKILL.md +19 -13
- package/content/skills/mugiwara-security/SKILL.md +47 -36
- package/content/skills/mugiwara-ship/SKILL.md +1 -1
- package/content/skills/mugiwara-ship/references/cleanup.md +17 -16
- package/content/skills/mugiwara-workflow/SKILL.md +11 -17
- 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 +42 -38
- package/dist/mugiwara.js +1642 -528
- package/gemini-extension.json +1 -1
- package/hooks/auto-savepoint.js +18 -10
- package/hooks/auto-savepoint.ts +23 -23
- package/hooks/engagement-marker.js +1 -1
- package/hooks/engagement-marker.ts +1 -1
- package/hooks/pipeline-guard.js +17 -13
- package/hooks/pipeline-guard.ts +24 -20
- package/hooks/session-start.js +13 -10
- package/hooks/session-start.ts +24 -19
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/complexity.md +27 -4
- package/references/definition-of-done.md +1 -1
- package/references/multi-actor.md +20 -2
- package/references/posture-routing.md +31 -0
- package/references/prose-style.md +54 -0
- package/scripts/benchmark-governor.ts +516 -0
- package/scripts/benchmark-thresholds.json +47 -0
- package/scripts/check-doc-links.ts +46 -0
- package/scripts/conformance.ts +6 -16
- package/scripts/coverage-gate.ts +20 -6
- package/scripts/gate-selftest.ts +138 -7
- package/scripts/lane.sh +54 -8
- package/scripts/lib/lane-base.sh +4 -4
- package/scripts/lib/patterns.sh +5 -0
- package/scripts/policy-force.ts +22 -0
- package/scripts/retrieval-eval.ts +10 -4
- package/scripts/savepoint.sh +155 -58
- package/scripts/validate-content.ts +43 -11
- package/scripts/verify-install.ts +9 -7
- package/src/adaptive-budget.ts +178 -0
- package/src/args.ts +4 -3
- package/src/budget.ts +47 -0
- package/src/check-artifacts.ts +45 -0
- package/src/cli.ts +257 -108
- package/src/cognition.ts +234 -0
- package/src/config.ts +107 -0
- package/src/context.ts +72 -0
- package/src/continue.ts +29 -10
- package/src/cost.ts +186 -0
- package/src/evidence.ts +160 -0
- package/src/installer.ts +25 -34
- package/src/integrity.ts +158 -0
- package/src/investigation.ts +72 -0
- package/src/mission.ts +341 -94
- package/src/policy.ts +156 -0
- package/src/posture.ts +86 -0
- package/src/provenance.ts +116 -0
- package/src/reporting.ts +225 -0
- package/src/rollback.ts +95 -0
- package/src/routing.ts +69 -0
- package/src/run.ts +2 -2
- package/src/scope.ts +321 -0
- package/src/sign.ts +235 -0
- package/src/slop.ts +306 -0
- package/src/targets/claude.ts +2 -2
- package/src/work.ts +273 -0
- package/.opencode/commands/mugiwara-execute.md +0 -13
- package/.opencode/commands/mugiwara-heal.md +0 -13
- package/.opencode/commands/mugiwara-onboard.md +0 -14
- package/.opencode/commands/mugiwara-plan.md +0 -17
- package/.opencode/commands/mugiwara-ship.md +0 -13
- package/.opencode/commands/using-mugiwara.md +0 -20
- package/content/skills/mugiwara-agent-security/SKILL.md +0 -58
- package/content/skills/mugiwara-agent-security/references/checklist.md +0 -20
- package/content/skills/mugiwara-context-budget/SKILL.md +0 -66
- package/content/skills/mugiwara-context-budget/references/context-budget.md +0 -37
- package/content/skills/mugiwara-pr/SKILL.md +0 -69
- package/content/skills/mugiwara-pr/references/verdict-format.md +0 -31
- package/content/skills/mugiwara-sunset/SKILL.md +0 -82
- package/content/skills/using-mugiwara/SKILL.md +0 -51
- package/references/token-budget.md +0 -56
- package/scripts/evidence.sh +0 -81
- package/scripts/initiative.ts +0 -296
- package/scripts/mission-report.sh +0 -293
- package/src/onboard.ts +0 -207
package/src/posture.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/posture.ts
|
|
2
|
+
// Mugiwara Phase B — deterministic posture selection matrix. Maps existing
|
|
3
|
+
// lane/risk/dependency/context/governor inputs to an execution posture with a
|
|
4
|
+
// concrete reason + evidence refs — never an opaque score. Pure, testable.
|
|
5
|
+
//
|
|
6
|
+
// Posture is independent of control mode (guided/semi/auto). The governor's
|
|
7
|
+
// verdict can pause but never silently changes mode or crew roles.
|
|
8
|
+
|
|
9
|
+
export type Posture =
|
|
10
|
+
| 'inline-sequential'
|
|
11
|
+
| 'inline-batched'
|
|
12
|
+
| 'parallel-workers'
|
|
13
|
+
| 'context-relief'
|
|
14
|
+
| 'phase-isolated'
|
|
15
|
+
| 'team-scoped';
|
|
16
|
+
|
|
17
|
+
export type GovernorVerdict = 'normal' | 'avoid' | 'stop';
|
|
18
|
+
|
|
19
|
+
export type PostureInput = {
|
|
20
|
+
lane: 'direct' | 'lean' | 'standard' | 'full' | 'spike';
|
|
21
|
+
risk: 'low' | 'medium' | 'high';
|
|
22
|
+
independent_tasks: number;
|
|
23
|
+
order_dependent: boolean;
|
|
24
|
+
context_pressure: boolean;
|
|
25
|
+
team_members: number;
|
|
26
|
+
phases: number;
|
|
27
|
+
plan_lines: number;
|
|
28
|
+
governor: GovernorVerdict;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type PostureResult = {
|
|
32
|
+
posture: Posture;
|
|
33
|
+
pause: boolean;
|
|
34
|
+
reason: string;
|
|
35
|
+
evidence_refs: string[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function selectPosture(input: PostureInput): PostureResult {
|
|
39
|
+
// stop verdict → safe pause, keep prior topology (recorded, never silent)
|
|
40
|
+
if (input.governor === 'stop') {
|
|
41
|
+
return {
|
|
42
|
+
posture: 'inline-sequential',
|
|
43
|
+
pause: true,
|
|
44
|
+
reason: 'governor stop — pause safely, keep inline; state + continue emitted',
|
|
45
|
+
evidence_refs: ['governor circuit-breaker', 'state.json'],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (input.team_members > 1) {
|
|
49
|
+
return {
|
|
50
|
+
posture: 'team-scoped',
|
|
51
|
+
pause: false,
|
|
52
|
+
reason: `${input.team_members} team members with non-overlapping scope`,
|
|
53
|
+
evidence_refs: ['plan ownership map'],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (input.phases > 3 || input.plan_lines > 1500) {
|
|
57
|
+
return {
|
|
58
|
+
posture: 'phase-isolated',
|
|
59
|
+
pause: false,
|
|
60
|
+
reason: `large campaign — ${input.phases} phases / ${input.plan_lines} lines`,
|
|
61
|
+
evidence_refs: ['plan.md', 'large-campaign-subplan.md'],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (input.context_pressure && input.order_dependent) {
|
|
65
|
+
return {
|
|
66
|
+
posture: 'context-relief',
|
|
67
|
+
pause: false,
|
|
68
|
+
reason: 'context pressure with ordered dependent tasks — one worker at a time, order preserved',
|
|
69
|
+
evidence_refs: ['state context metrics', 'remaining task order'],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (input.independent_tasks >= 2) {
|
|
73
|
+
return {
|
|
74
|
+
posture: 'parallel-workers',
|
|
75
|
+
pause: false,
|
|
76
|
+
reason: `${input.independent_tasks} independent tasks, no shared files/interfaces`,
|
|
77
|
+
evidence_refs: ['Nami dependency map', 'work-governor delegation verdict'],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
posture: 'inline-sequential',
|
|
82
|
+
pause: false,
|
|
83
|
+
reason: 'no parallel/phase/team/relief trigger — default inline in plan order',
|
|
84
|
+
evidence_refs: ['triage route', 'lane'],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// src/provenance.ts
|
|
2
|
+
// Provenance ledger: line-of-sight from a commit to the
|
|
3
|
+
// mission that produced it — which agent persona, which lane, verified by
|
|
4
|
+
// what evidence. Distributed in two layers:
|
|
5
|
+
// 1. a git note on refs/notes/mugiwara attached to the branch head
|
|
6
|
+
// (local precision archive; survives rebase via notes.rewriteRef)
|
|
7
|
+
// 2. provenance.md in the mission dir, ready to paste as a PR comment
|
|
8
|
+
// (the layer every hosting UI can show)
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
import { writeFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
const NOTES_REF = 'refs/notes/mugiwara';
|
|
14
|
+
|
|
15
|
+
/** Structural subset any mission state satisfies — keeps this import-free. */
|
|
16
|
+
export type NoteSource = {
|
|
17
|
+
mission: string;
|
|
18
|
+
actor: string;
|
|
19
|
+
lane: string;
|
|
20
|
+
mode: string;
|
|
21
|
+
branch: string;
|
|
22
|
+
tasks_done: number;
|
|
23
|
+
tasks_total: number;
|
|
24
|
+
evidence: string[];
|
|
25
|
+
models?: string[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function git(cwd: string, args: string[]): string {
|
|
29
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function modelLabel(): string {
|
|
33
|
+
return process.env.MUGIWARA_MODEL?.trim()
|
|
34
|
+
|| process.env.ANTHROPIC_MODEL?.trim()
|
|
35
|
+
|| 'model-unrecorded (set MUGIWARA_MODEL to attribute)';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The provenance block — identical wording in the note and the md file. */
|
|
39
|
+
export function buildNote(s: {
|
|
40
|
+
mission: string; actor: string; lane: string; mode: string; branch: string;
|
|
41
|
+
tasks_done: number; tasks_total: number; evidence: string[]; model?: string; models?: string[];
|
|
42
|
+
}): string {
|
|
43
|
+
const gates = s.evidence.length ? s.evidence.join(' · ') : 'no evidence recorded';
|
|
44
|
+
// Per-stage attribution (A4): when flow history recorded models, render the
|
|
45
|
+
// unique set — a mid-mission switch must not collapse to the last env value.
|
|
46
|
+
// With nothing recorded, keep the env-fallback label wording.
|
|
47
|
+
const uniqModels = [...new Set((s.models ?? []).filter(Boolean))];
|
|
48
|
+
const modelPart = uniqModels.length ? `model(s): ${uniqModels.join(', ')}` : (s.model ?? modelLabel());
|
|
49
|
+
return [
|
|
50
|
+
`mission: ${s.mission}`,
|
|
51
|
+
`agent: ${s.actor || 'unknown'} · ${modelPart} · lane ${s.lane} · mode ${s.mode}`,
|
|
52
|
+
`tasks: ${s.tasks_done}/${s.tasks_total}`,
|
|
53
|
+
`gates/evidence: ${gates}`,
|
|
54
|
+
`branch: ${s.branch}`,
|
|
55
|
+
'human review: pending (PR review is the terminal gate)',
|
|
56
|
+
].join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** PR-paste-ready markdown wrapper around the same facts. */
|
|
60
|
+
export function renderProvenanceMd(note: string, sha: string | null): string {
|
|
61
|
+
const lines = [
|
|
62
|
+
'# Provenance',
|
|
63
|
+
'',
|
|
64
|
+
'<!-- paste below into the PR description or a PR comment -->',
|
|
65
|
+
'',
|
|
66
|
+
'```',
|
|
67
|
+
note,
|
|
68
|
+
'```',
|
|
69
|
+
'',
|
|
70
|
+
sha ? `Commit: ${sha}` : 'Commit: not recorded (no git head resolved at closure)',
|
|
71
|
+
'',
|
|
72
|
+
'Query locally after pushing notes:',
|
|
73
|
+
'`git fetch origin refs/notes/mugiwara:refs/notes/mugiwara` then `mugiwara blame <path>`.',
|
|
74
|
+
];
|
|
75
|
+
return lines.join('\n') + '\n';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function attachGitNote(projectDir: string, branch: string, note: string): { sha: string } | null {
|
|
79
|
+
try {
|
|
80
|
+
let sha: string;
|
|
81
|
+
try {
|
|
82
|
+
sha = git(projectDir, ['rev-parse', '--verify', branch]);
|
|
83
|
+
} catch {
|
|
84
|
+
sha = git(projectDir, ['rev-parse', 'HEAD']);
|
|
85
|
+
}
|
|
86
|
+
git(projectDir, ['notes', '--ref=mugiwara', 'add', '-f', '-m', note, sha]);
|
|
87
|
+
return { sha };
|
|
88
|
+
} catch {
|
|
89
|
+
// not a repo, detached oddities, or notes disabled — degrade honestly
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `mugiwara blame <path>` — last commit that touched the path + its note. */
|
|
95
|
+
export function blamePath(projectDir: string, path: string): string {
|
|
96
|
+
let sha: string;
|
|
97
|
+
try {
|
|
98
|
+
sha = git(projectDir, ['log', '-1', '--format=%H', '--', path]);
|
|
99
|
+
} catch {
|
|
100
|
+
return `blame: not a git repository (${projectDir})`;
|
|
101
|
+
}
|
|
102
|
+
if (!sha) return `blame: no commit touches "${path}"`;
|
|
103
|
+
try {
|
|
104
|
+
const note = git(projectDir, ['notes', '--ref=mugiwara', 'show', sha]);
|
|
105
|
+
return `${path} @ ${sha.slice(0, 7)}\n${note}`;
|
|
106
|
+
} catch {
|
|
107
|
+
return `${path} @ ${sha.slice(0, 7)}\n(no mugiwara provenance note on this commit)`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Closure hook: write provenance.md + attach the git note. */
|
|
112
|
+
export function writeProvenance(projectDir: string, missionDir: string, state: NoteSource): void {
|
|
113
|
+
const note = buildNote(state);
|
|
114
|
+
const attached = attachGitNote(projectDir, state.branch, note);
|
|
115
|
+
writeFileSync(join(missionDir, 'provenance.md'), renderProvenanceMd(note, attached ? attached.sha : null));
|
|
116
|
+
}
|
package/src/reporting.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// src/reporting.ts
|
|
2
|
+
// Phase 8 Reporting & CLI — cost ledger, avoided work, efficiency, trail (§39/§41–§43).
|
|
3
|
+
// Pure view over existing persisted files: cost-events.jsonl (Phase 1),
|
|
4
|
+
// context-registry.jsonl (Phase 2), decisions.md trail (§41). No new store.
|
|
5
|
+
// note: ledger is a view over existing files, no new store
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import type { CostEvent } from './cost.ts';
|
|
9
|
+
import type { CostEnvelope } from './cost.ts';
|
|
10
|
+
import { loadRegistry } from './evidence.ts';
|
|
11
|
+
|
|
12
|
+
// ── allowlist — F3: every missionDir FS read is allowlisted to .mugiwara/missions/<id> ──
|
|
13
|
+
function isAllowedMissionDir(dir: string): boolean {
|
|
14
|
+
if (!dir || dir.includes('..')) return false;
|
|
15
|
+
if (dir.includes('.mugiwara/missions')) return true;
|
|
16
|
+
if (dir.includes('mugiwara-')) return true;
|
|
17
|
+
if (dir.startsWith('/tmp/') && dir.includes('-')) return true;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
function assertMissionDir(dir: string): void {
|
|
21
|
+
if (!isAllowedMissionDir(dir)) throw new Error(`Invalid missionDir: ${dir}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── ledger types ──
|
|
25
|
+
export type Avoided = {
|
|
26
|
+
stages_avoided: number;
|
|
27
|
+
contexts_avoided: number;
|
|
28
|
+
slop_interventions: number;
|
|
29
|
+
tokens_avoided_est: number;
|
|
30
|
+
};
|
|
31
|
+
export type Efficiency = {
|
|
32
|
+
reuse_rate: number;
|
|
33
|
+
duplicate_avoidance_chars: number;
|
|
34
|
+
budget_efficiency_pct: number;
|
|
35
|
+
};
|
|
36
|
+
export type CostLedger = {
|
|
37
|
+
envelope: CostEnvelope;
|
|
38
|
+
ledger: { events: CostEvent[]; registrySize: number; decisions: { ts: string; actor: string; decision: string; reason: string; evidence?: string }[] };
|
|
39
|
+
avoided: Avoided;
|
|
40
|
+
efficiency: Efficiency;
|
|
41
|
+
trail: { ts: string; actor: string; decision: string; reason: string; evidence?: string }[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ── trail parsing ──
|
|
45
|
+
export function parseDecisionTrail(missionDir: string): { ts: string; actor: string; decision: string; reason: string; evidence?: string }[] {
|
|
46
|
+
assertMissionDir(missionDir);
|
|
47
|
+
const file = join(missionDir, 'decisions.md');
|
|
48
|
+
if (!existsSync(file)) return [];
|
|
49
|
+
const raw = readFileSync(file, 'utf8');
|
|
50
|
+
const lines = raw.split(/\r?\n/);
|
|
51
|
+
// find section header; if missing, scan whole file for bullets with — actor: pattern
|
|
52
|
+
const headerIdx = lines.findIndex((l) => l.trim() === '## Cost governor decisions' || l.trim() === '## Budget');
|
|
53
|
+
const scan = headerIdx >= 0 ? lines.slice(headerIdx) : lines;
|
|
54
|
+
const out: { ts: string; actor: string; decision: string; reason: string; evidence?: string }[] = [];
|
|
55
|
+
// bullet pattern: - <ts> — <actor>: <decision> — reason: <reason> [— evidence: <evidence>]
|
|
56
|
+
// also support budget-governor/work-governor etc
|
|
57
|
+
const bulletRe = /^-\s+(.+?)\s+—\s+(.+?):\s+(.+?)\s+—\s+reason:\s+(.+?)(?:\s+—\s+evidence:\s+(.+))?\s*$/;
|
|
58
|
+
for (const line of scan) {
|
|
59
|
+
const m = bulletRe.exec(line.trim());
|
|
60
|
+
if (!m) continue;
|
|
61
|
+
out.push({ ts: m[1], actor: m[2], decision: m[3], reason: m[4], ...(m[5] ? { evidence: m[5] } : {}) });
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── cost events ──
|
|
67
|
+
export function loadCostEvents(missionDir: string): CostEvent[] {
|
|
68
|
+
assertMissionDir(missionDir);
|
|
69
|
+
const file = join(missionDir, 'cost-events.jsonl');
|
|
70
|
+
if (!existsSync(file)) return [];
|
|
71
|
+
const out: CostEvent[] = [];
|
|
72
|
+
for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
73
|
+
if (!line.trim()) continue;
|
|
74
|
+
try {
|
|
75
|
+
const e = JSON.parse(line);
|
|
76
|
+
if (e && typeof e === 'object' && typeof (e as CostEvent).mission === 'string') out.push(e as CostEvent);
|
|
77
|
+
} catch {
|
|
78
|
+
continue; // selective-drop
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── avoided / efficiency ──
|
|
85
|
+
export function computeAvoidedMetrics(input: {
|
|
86
|
+
registryMetrics?: { duplicateCount: number; repeatedReads: number };
|
|
87
|
+
workMetrics?: { stagesAvoided: number };
|
|
88
|
+
slopMetrics?: { interventions: number };
|
|
89
|
+
}): Avoided {
|
|
90
|
+
const dup = input.registryMetrics?.duplicateCount ?? 0;
|
|
91
|
+
const rep = input.registryMetrics?.repeatedReads ?? 0;
|
|
92
|
+
const contexts_avoided = dup + rep;
|
|
93
|
+
const stages_avoided = input.workMetrics?.stagesAvoided ?? 0;
|
|
94
|
+
const slop_interventions = input.slopMetrics?.interventions ?? 0;
|
|
95
|
+
// note: heuristic 150 tokens per avoided read, tune with §39 if needed
|
|
96
|
+
const tokens_avoided_est = contexts_avoided * 150;
|
|
97
|
+
return { stages_avoided, contexts_avoided, slop_interventions, tokens_avoided_est };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function computeEfficiencyMetrics(input: {
|
|
101
|
+
totalReads: number;
|
|
102
|
+
reuseHits: number;
|
|
103
|
+
duplicateChars: number;
|
|
104
|
+
budget: number;
|
|
105
|
+
used: number;
|
|
106
|
+
}): Efficiency {
|
|
107
|
+
const reuse_rate = input.totalReads > 0 ? Math.round((input.reuseHits / input.totalReads) * 100) / 100 : 0;
|
|
108
|
+
const duplicate_avoidance_chars = input.duplicateChars;
|
|
109
|
+
const budget_efficiency_pct = input.budget > 0 ? Math.round((input.used / input.budget) * 100) : 0;
|
|
110
|
+
return { reuse_rate, duplicate_avoidance_chars, budget_efficiency_pct };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── ledger ──
|
|
114
|
+
export function buildCostLedger(input: {
|
|
115
|
+
missionDir: string;
|
|
116
|
+
envelope: CostEnvelope;
|
|
117
|
+
contextMetrics?: { files_loaded?: number; reads_total?: number; reads_reused?: number; unique_chars?: number; total_chars?: number; duplicate_chars?: number; reuse_rate?: number };
|
|
118
|
+
workSummary?: { stagesAvoided?: number };
|
|
119
|
+
slopSummary?: { interventions?: number };
|
|
120
|
+
budgetSummary?: unknown;
|
|
121
|
+
}): CostLedger {
|
|
122
|
+
assertMissionDir(input.missionDir);
|
|
123
|
+
const events = loadCostEvents(input.missionDir);
|
|
124
|
+
let registrySize = 0;
|
|
125
|
+
let totalReads = 0;
|
|
126
|
+
let reuseHits = 0;
|
|
127
|
+
let duplicateChars = 0;
|
|
128
|
+
try {
|
|
129
|
+
const reg = loadRegistry(input.missionDir);
|
|
130
|
+
registrySize = reg.length;
|
|
131
|
+
totalReads = reg.reduce((s, e) => s + e.reads, 0);
|
|
132
|
+
reuseHits = reg.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
|
|
133
|
+
const unique = reg.reduce((s, e) => s + (e.chars ?? 0), 0);
|
|
134
|
+
const total = reg.reduce((s, e) => s + (e.chars ?? 0) * e.reads, 0);
|
|
135
|
+
duplicateChars = total - unique;
|
|
136
|
+
if (duplicateChars < 0) duplicateChars = 0;
|
|
137
|
+
} catch {
|
|
138
|
+
// loadRegistry throws on Invalid missionDir — already asserted above, but keep safe
|
|
139
|
+
registrySize = 0;
|
|
140
|
+
}
|
|
141
|
+
const decisions = parseDecisionTrail(input.missionDir);
|
|
142
|
+
// derive avoided from registry + summaries
|
|
143
|
+
const repeatedReads = reuseHits;
|
|
144
|
+
const duplicateCount = duplicateChars > 0 ? 1 : 0; // at least one duplicate group when duplicateChars >0; fallback to 0 — avoided metric also supplied via summaries in tests direct call
|
|
145
|
+
// For ledger, contexts_avoided = repeatedReads (plus duplicateCount heuristic 0); prefer direct reuseHits
|
|
146
|
+
const avoided = computeAvoidedMetrics({
|
|
147
|
+
registryMetrics: { duplicateCount: 0, repeatedReads: reuseHits },
|
|
148
|
+
workMetrics: { stagesAvoided: input.workSummary?.stagesAvoided ?? 0 },
|
|
149
|
+
slopMetrics: { interventions: input.slopSummary?.interventions ?? 0 },
|
|
150
|
+
});
|
|
151
|
+
// if duplicateChars>0 and reuseHits==0 (single dup entry reads=1 still has dup chars) — count it as avoided context
|
|
152
|
+
if (duplicateChars > 0 && avoided.contexts_avoided === 0) {
|
|
153
|
+
avoided.contexts_avoided = 1;
|
|
154
|
+
avoided.tokens_avoided_est = 150;
|
|
155
|
+
}
|
|
156
|
+
const efficiency = computeEfficiencyMetrics({
|
|
157
|
+
totalReads,
|
|
158
|
+
reuseHits,
|
|
159
|
+
duplicateChars,
|
|
160
|
+
budget: input.envelope.planned,
|
|
161
|
+
used: input.envelope.used,
|
|
162
|
+
});
|
|
163
|
+
return {
|
|
164
|
+
envelope: input.envelope,
|
|
165
|
+
ledger: { events, registrySize, decisions },
|
|
166
|
+
avoided,
|
|
167
|
+
efficiency,
|
|
168
|
+
trail: decisions,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── rendering ──
|
|
173
|
+
export function renderCostSection(ledger: CostLedger): string {
|
|
174
|
+
const env = ledger.envelope;
|
|
175
|
+
const lines: string[] = [
|
|
176
|
+
'## Cost',
|
|
177
|
+
'',
|
|
178
|
+
'| Dimension | Value |',
|
|
179
|
+
'|-----------|-------|',
|
|
180
|
+
`| Budget | ${env.status} ${env.pct}% (${env.used}/${env.planned}) |`,
|
|
181
|
+
`| Context | ${env.planned} chars, reuse ${ledger.efficiency.reuse_rate} |`,
|
|
182
|
+
`| Avoided | ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ${ledger.avoided.slop_interventions} slop, ~${ledger.avoided.tokens_avoided_est} tokens est |`,
|
|
183
|
+
`| Efficiency | reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}% |`,
|
|
184
|
+
`| Trail | ${ledger.trail.length} decisions |`,
|
|
185
|
+
];
|
|
186
|
+
if (ledger.trail.length) {
|
|
187
|
+
lines.push('');
|
|
188
|
+
const show = ledger.trail.slice(0, 5);
|
|
189
|
+
for (const t of show) {
|
|
190
|
+
lines.push(`- ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ''}`);
|
|
191
|
+
}
|
|
192
|
+
if (ledger.trail.length > 5) lines.push(`… ${ledger.trail.length - 5} more`);
|
|
193
|
+
}
|
|
194
|
+
return lines.join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function toCostJSON(ledger: CostLedger): string {
|
|
198
|
+
// stable key order via explicit object
|
|
199
|
+
return JSON.stringify(
|
|
200
|
+
{ envelope: ledger.envelope, ledger: ledger.ledger, avoided: ledger.avoided, efficiency: ledger.efficiency, trail: ledger.trail },
|
|
201
|
+
null,
|
|
202
|
+
2,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── adaptation summary (Phase E) ──
|
|
207
|
+
// Posture decisions are recorded in decisions.md (via Phase C). Summarize them
|
|
208
|
+
// from the existing trail — no second store.
|
|
209
|
+
export function summarizeAdaptation(missionDir: string): { count: number; rows: { ts: string; decision: string; reason: string }[] } {
|
|
210
|
+
const trail = parseDecisionTrail(missionDir);
|
|
211
|
+
const postureRe = /posture|switch|adapt|pause|parallel-workers|context-relief|phase-isolated|team-scoped/i;
|
|
212
|
+
const rows = trail
|
|
213
|
+
.filter((t) => postureRe.test(`${t.decision} ${t.reason}`))
|
|
214
|
+
.map((t) => ({ ts: t.ts, decision: t.decision, reason: t.reason }));
|
|
215
|
+
return { count: rows.length, rows };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function renderAdaptationSection(missionDir: string): string {
|
|
219
|
+
const { count, rows } = summarizeAdaptation(missionDir);
|
|
220
|
+
if (count === 0) return '';
|
|
221
|
+
const lines = ['', '## Adaptation', '', '| Time | Decision | Reason |', '|---|---|---|'];
|
|
222
|
+
for (const r of rows) lines.push(`| ${r.ts} | ${r.decision} | ${r.reason} |`);
|
|
223
|
+
if (rows.length === 0) lines.push('| — | (posture rows recorded in decisions.md) | |');
|
|
224
|
+
return lines.join('\n');
|
|
225
|
+
}
|
package/src/rollback.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// src/rollback.ts
|
|
2
|
+
// Executable rollback map: recovery you can run, not prose.
|
|
3
|
+
// Generated from git + state at closure; the human runs it, mugiwara never does.
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
export type RollbackInput = {
|
|
9
|
+
mission: string;
|
|
10
|
+
branch: string;
|
|
11
|
+
baseSha: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function git(cwd: string, args: string[]): string {
|
|
15
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build the rollback script body. Pure given the inputs — the shas are read
|
|
20
|
+
* from the repo by the caller so this stays unit-testable without git.
|
|
21
|
+
*/
|
|
22
|
+
export function buildRollback(input: RollbackInput, commitsNewestFirst: string[], filesTouched: string[]): string {
|
|
23
|
+
const lines = [
|
|
24
|
+
'#!/usr/bin/env bash',
|
|
25
|
+
`# Rollback map for mission "${input.mission}" — generated at closure.`,
|
|
26
|
+
'# Human-executed. Review before running; mugiwara never runs this.',
|
|
27
|
+
`# Branch: ${input.branch}`,
|
|
28
|
+
`# Base: ${input.baseSha}`,
|
|
29
|
+
'',
|
|
30
|
+
'set -euo pipefail',
|
|
31
|
+
'',
|
|
32
|
+
];
|
|
33
|
+
if (!commitsNewestFirst.length && !filesTouched.length) {
|
|
34
|
+
lines.push('# No commits between base and HEAD on this branch — nothing to revert.', '');
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
|
37
|
+
if (!commitsNewestFirst.length) {
|
|
38
|
+
// Squash-merge workflows: the branch's changes reached the base ref as one
|
|
39
|
+
// squashed commit, so `rev-list base..branch` can come back empty while
|
|
40
|
+
// `git diff base branch` is not. "Nothing to revert" would be a lie — the
|
|
41
|
+
// changes are live. Emit loud, human-executable guidance and exit 1 so a
|
|
42
|
+
// careless run fails instead of silently doing nothing.
|
|
43
|
+
lines.push(
|
|
44
|
+
'# UNRESOLVED: squash-merged state detected.',
|
|
45
|
+
`# git rev-list ${input.baseSha}..${input.branch} is empty, but the diff`,
|
|
46
|
+
`# ${input.baseSha}..${input.branch} touches ${filesTouched.length} file(s):`,
|
|
47
|
+
'# the mission\'s changes were collapsed into commit(s) already on the base',
|
|
48
|
+
'# ref. No per-commit revert list can be derived automatically.',
|
|
49
|
+
'# Locate the squash commit, review it, then revert it:',
|
|
50
|
+
`# git log --oneline ${input.baseSha}..HEAD --grep="${input.mission}"`,
|
|
51
|
+
'# git revert <squash-commit>',
|
|
52
|
+
'# Files carrying the squashed changes (verify each after reverting):',
|
|
53
|
+
...filesTouched.map((f) => `# ${f}`),
|
|
54
|
+
'',
|
|
55
|
+
'echo "ROLLBACK INCOMPLETE: squash-merged state — locate and revert the squash commit manually" >&2',
|
|
56
|
+
'exit 1',
|
|
57
|
+
'',
|
|
58
|
+
);
|
|
59
|
+
return lines.join('\n');
|
|
60
|
+
}
|
|
61
|
+
lines.push(
|
|
62
|
+
'# Revert newest-first so earlier reverts never conflict with later ones.',
|
|
63
|
+
'git revert --no-edit \\',
|
|
64
|
+
commitsNewestFirst.map((c) => ` ${c}`).join(' \\\n'),
|
|
65
|
+
'',
|
|
66
|
+
);
|
|
67
|
+
if (filesTouched.length) {
|
|
68
|
+
lines.push(
|
|
69
|
+
'# Files this mission touched (verify the working tree is clean afterwards):',
|
|
70
|
+
...filesTouched.map((f) => `# ${f}`),
|
|
71
|
+
'',
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return lines.join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Read the repo for everything the script needs, then write rollback.sh. */
|
|
78
|
+
export function generateRollback(projectDir: string, missionDir: string, input: RollbackInput): { file: string; commits: number } | null {
|
|
79
|
+
try {
|
|
80
|
+
const range = `${input.baseSha}..${input.branch}`;
|
|
81
|
+
const revList = git(projectDir, ['rev-list', '--reverse', range]).split(/\r?\n/).filter(Boolean);
|
|
82
|
+
const commitsNewestFirst = [...revList].reverse();
|
|
83
|
+
const filesTouched = input.baseSha === 'unknown'
|
|
84
|
+
? []
|
|
85
|
+
: git(projectDir, ['diff', '--name-only', input.baseSha, input.branch]).split(/\r?\n/).filter(Boolean);
|
|
86
|
+
const body = buildRollback(input, commitsNewestFirst, filesTouched);
|
|
87
|
+
const file = join(missionDir, 'rollback.sh');
|
|
88
|
+
writeFileSync(file, body, { mode: 0o755 });
|
|
89
|
+
return { file: 'rollback.sh', commits: commitsNewestFirst.length };
|
|
90
|
+
} catch {
|
|
91
|
+
// No git, unknown base, or an empty branch — a rollback map cannot be
|
|
92
|
+
// derived. Absent file beats a wrong one.
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/routing.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/routing.ts
|
|
2
|
+
// Review routing: tell the reviewer where to look. A ranked
|
|
3
|
+
// reading order computed from what the mission touched — not a flat diff.
|
|
4
|
+
// Heuristic and labeled as such: the ranking decides reading ORDER, never
|
|
5
|
+
// whether a line is correct.
|
|
6
|
+
|
|
7
|
+
export type RankedFile = { path: string; score: number; reasons: string[] };
|
|
8
|
+
|
|
9
|
+
const DOC_PAT = /\.(md|txt|rst)$|^docs\/|^(CHANGELOG|LICENSE|README)/;
|
|
10
|
+
const TEST_PAT = /\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)(tests?|__tests__|specs?)\//;
|
|
11
|
+
|
|
12
|
+
export type RoutingSource = { evidence: string[]; sensitive_paths?: string[] };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Score one path. Sensitive paths dominate; production code next; tests and
|
|
16
|
+
* docs sink. Files the evidence trail never mentions get a bump — absence of
|
|
17
|
+
* evidence is exactly where review attention pays.
|
|
18
|
+
*/
|
|
19
|
+
export function scorePath(path: string, sensitivePaths: string[], evidenceJoined: string): { score: number; reasons: string[] } {
|
|
20
|
+
const reasons: string[] = [];
|
|
21
|
+
let score = 0;
|
|
22
|
+
const sensitiveHit = sensitivePaths.some((s) => s && (path === s || path.startsWith(s.replace(/\/?$/, '/')) || new RegExp(s.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*')).test(path)));
|
|
23
|
+
if (sensitiveHit) {
|
|
24
|
+
score += 100;
|
|
25
|
+
reasons.push('sensitive path');
|
|
26
|
+
}
|
|
27
|
+
if (!DOC_PAT.test(path) && !TEST_PAT.test(path)) {
|
|
28
|
+
score += 50;
|
|
29
|
+
reasons.push('production code');
|
|
30
|
+
} else if (TEST_PAT.test(path)) {
|
|
31
|
+
score += 10;
|
|
32
|
+
reasons.push('test scaffolding — skim unless behavior changed');
|
|
33
|
+
} else {
|
|
34
|
+
score += 5;
|
|
35
|
+
reasons.push('docs/config');
|
|
36
|
+
}
|
|
37
|
+
if (!evidenceJoined.includes(path)) {
|
|
38
|
+
score += 20;
|
|
39
|
+
reasons.push('not covered by recorded evidence');
|
|
40
|
+
}
|
|
41
|
+
return { score, reasons };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function rankFiles(paths: string[], state: RoutingSource): RankedFile[] {
|
|
45
|
+
const sensitive = state.sensitive_paths ?? [];
|
|
46
|
+
const ev = state.evidence.join(' ');
|
|
47
|
+
return paths
|
|
48
|
+
.map((path) => {
|
|
49
|
+
const { score, reasons } = scorePath(path, sensitive, ev);
|
|
50
|
+
return { path, score, reasons };
|
|
51
|
+
})
|
|
52
|
+
.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function renderRouting(ranked: RankedFile[], mission: string): string {
|
|
56
|
+
if (!ranked.length) return '';
|
|
57
|
+
const lines = [
|
|
58
|
+
'',
|
|
59
|
+
'## Review routing',
|
|
60
|
+
'',
|
|
61
|
+
`Ranked reading order for \`${mission}\` (heuristic ordering — it decides where to look first, never correctness):`,
|
|
62
|
+
'',
|
|
63
|
+
];
|
|
64
|
+
ranked.forEach((r, i) => {
|
|
65
|
+
lines.push(`${i + 1}. \`${r.path}\` — ${r.reasons.join('; ')}`);
|
|
66
|
+
});
|
|
67
|
+
lines.push('');
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
}
|
package/src/run.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Why this exists: the skills tell the crew to run `scripts/savepoint.sh`, but
|
|
6
6
|
// the installer only ever copied `content/` and `references/` into a project.
|
|
7
7
|
// That path resolved against the project's cwd, where the file does not exist,
|
|
8
|
-
// so every savepoint / lane
|
|
8
|
+
// so every savepoint / lane call silently did
|
|
9
9
|
// nothing. Resolving from the package root fixes it for every install target at
|
|
10
10
|
// once.
|
|
11
11
|
import { existsSync, readdirSync } from 'node:fs';
|
|
@@ -18,7 +18,7 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
18
18
|
export const SCRIPTS_DIR = join(here, '..', 'scripts');
|
|
19
19
|
|
|
20
20
|
/** Scripts a project is meant to call. Anything else stays internal tooling. */
|
|
21
|
-
export const RUNNABLE = ['savepoint.sh', 'lane.sh'
|
|
21
|
+
export const RUNNABLE = ['savepoint.sh', 'lane.sh'] as const;
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Locate a POSIX shell. Windows has none natively, but Git for Windows ships
|