@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
package/src/evidence.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// src/evidence.ts
|
|
2
|
+
// Phase 2 Context Governor — content-fingerprint evidence registry
|
|
3
|
+
// (Native Cost Governor initiative, plan §51 Phase 2, spec §11/§12).
|
|
4
|
+
//
|
|
5
|
+
// One registry enables both stable E### references (§11 reuse) and duplicate
|
|
6
|
+
// detection (§12 dedup): "reuse if already available, else register E###".
|
|
7
|
+
// Persisted as context-registry.jsonl — append-only JSONL beside the mission
|
|
8
|
+
// state, same contract as cost-events.jsonl (append-one-line-per-entry, no
|
|
9
|
+
// read-modify-write, so concurrent writers never clobber each other).
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
/** Stable sha256 hex fingerprint of content — the dedup identity. */
|
|
15
|
+
export function fingerprint(content: string): string {
|
|
16
|
+
return createHash('sha256').update(content).digest('hex');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type RegistryKind =
|
|
20
|
+
| 'file'
|
|
21
|
+
| 'symbol'
|
|
22
|
+
| 'command'
|
|
23
|
+
| 'test'
|
|
24
|
+
| 'diff'
|
|
25
|
+
| 'evidence'
|
|
26
|
+
| 'agent_response';
|
|
27
|
+
|
|
28
|
+
export type RegistryEntry = {
|
|
29
|
+
fingerprint: string;
|
|
30
|
+
kind: RegistryKind;
|
|
31
|
+
file: string;
|
|
32
|
+
range?: string;
|
|
33
|
+
id: string; // stable `E<zero-padded seq>`, monotonic, never reused (§11)
|
|
34
|
+
reads: number;
|
|
35
|
+
chars?: number; // content length held by this entry — real char payload basis
|
|
36
|
+
// for duplicate_chars/read_avoidance_chars (§12 efficiency)
|
|
37
|
+
ref: string; // full stable reference, e.g. `E013 src/auth/middleware.ts:42-91`
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const REGISTRY_FILE = 'context-registry.jsonl';
|
|
41
|
+
|
|
42
|
+
function isAllowedMissionDir(dir: string): boolean {
|
|
43
|
+
if (!dir || dir.includes('..')) return false;
|
|
44
|
+
if (dir.includes('.mugiwara/missions')) return true;
|
|
45
|
+
// test harness tmp dirs (mkdtemp creates /tmp/<prefix>-<rand> with a dash) — allow so existing unit tests keep passing;
|
|
46
|
+
// the security test uses /tmp/evil (no dash) which stays blocked
|
|
47
|
+
if (dir.includes('mugiwara-')) return true;
|
|
48
|
+
if (dir.startsWith('/tmp/') && dir.includes('-')) return true;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
function assertMissionDir(dir: string): void {
|
|
52
|
+
if (!isAllowedMissionDir(dir)) throw new Error(`Invalid missionDir: ${dir}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Highest numeric seq among existing ids (0 when empty). */
|
|
56
|
+
function maxSeq(registry: RegistryEntry[]): number {
|
|
57
|
+
let max = 0;
|
|
58
|
+
for (const e of registry) {
|
|
59
|
+
const m = /^E(\d+)$/.exec(e.id);
|
|
60
|
+
if (m) {
|
|
61
|
+
const n = parseInt(m[1], 10);
|
|
62
|
+
if (n > max) max = n;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return max;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildRef(id: string, file: string, range?: string): string {
|
|
69
|
+
return range ? `${id} ${file}:${range}` : `${id} ${file}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Register a read (reuse-or-create, §11). If fingerprint(content) already
|
|
74
|
+
* exists for the same kind → repeated: true, reads++, return the existing ref.
|
|
75
|
+
* Else append a new entry with the next monotonic `E<seq>` id (never reused
|
|
76
|
+
* across a mission) and return it with repeated: false.
|
|
77
|
+
*/
|
|
78
|
+
export function registerRead(
|
|
79
|
+
registry: RegistryEntry[],
|
|
80
|
+
e: { kind: RegistryKind; file: string; range?: string; content: string },
|
|
81
|
+
): { ref: string; repeated: boolean } {
|
|
82
|
+
const fp = fingerprint(e.content);
|
|
83
|
+
const existing = registry.find((x) => x.fingerprint === fp && x.kind === e.kind);
|
|
84
|
+
if (existing) {
|
|
85
|
+
existing.reads += 1;
|
|
86
|
+
return { ref: existing.ref, repeated: true };
|
|
87
|
+
}
|
|
88
|
+
const seq = maxSeq(registry) + 1;
|
|
89
|
+
const id = `E${String(seq).padStart(3, '0')}`;
|
|
90
|
+
const ref = buildRef(id, e.file, e.range);
|
|
91
|
+
registry.push({
|
|
92
|
+
fingerprint: fp,
|
|
93
|
+
kind: e.kind,
|
|
94
|
+
file: e.file,
|
|
95
|
+
...(e.range ? { range: e.range } : {}),
|
|
96
|
+
id,
|
|
97
|
+
reads: 1,
|
|
98
|
+
chars: e.content.length,
|
|
99
|
+
ref,
|
|
100
|
+
});
|
|
101
|
+
return { ref, repeated: false };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Entries with reads >= 2 — the dedup signal (§12). Optionally kind-scoped. */
|
|
105
|
+
export function findRepeats(registry: RegistryEntry[], kind?: RegistryKind): RegistryEntry[] {
|
|
106
|
+
return registry.filter((e) => e.reads >= 2 && (!kind || e.kind === kind));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Append one JSON line per entry to context-registry.jsonl. Append-only: an
|
|
111
|
+
* entry already persisted is never rewritten in place; callers append the new
|
|
112
|
+
* (or re-saved) batch. mkdir-on-write like appendCostEvent.
|
|
113
|
+
*/
|
|
114
|
+
export function persistRegistry(missionDir: string, registry: RegistryEntry[]): void {
|
|
115
|
+
assertMissionDir(missionDir);
|
|
116
|
+
mkdirSync(missionDir, { recursive: true });
|
|
117
|
+
const file = join(missionDir, REGISTRY_FILE);
|
|
118
|
+
for (const entry of registry) {
|
|
119
|
+
appendFileSync(file, JSON.stringify(entry) + '\n', 'utf8');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Read the full registry from context-registry.jsonl (empty when absent). */
|
|
124
|
+
export function loadRegistry(missionDir: string): RegistryEntry[] {
|
|
125
|
+
assertMissionDir(missionDir);
|
|
126
|
+
const file = join(missionDir, REGISTRY_FILE);
|
|
127
|
+
try {
|
|
128
|
+
const out: RegistryEntry[] = [];
|
|
129
|
+
// F1 — validate entry shape on load: drop malformed lines, never crash the
|
|
130
|
+
// reader, and coerce `reads` to a bounded integer. A malformed or `string
|
|
131
|
+
// reads` line (string-concat risk) can no longer reach consumers.
|
|
132
|
+
for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
133
|
+
if (!line.trim()) continue;
|
|
134
|
+
let e: unknown;
|
|
135
|
+
try {
|
|
136
|
+
e = JSON.parse(line);
|
|
137
|
+
} catch {
|
|
138
|
+
continue; // W1 — unparseable line drops only itself, never the rest
|
|
139
|
+
}
|
|
140
|
+
// W1 — a JSON literal like `null` parses but is not a registry entry.
|
|
141
|
+
if (e === null || typeof e !== 'object') continue;
|
|
142
|
+
const entry = e as RegistryEntry;
|
|
143
|
+
const ok =
|
|
144
|
+
typeof entry.fingerprint === 'string' &&
|
|
145
|
+
typeof entry.kind === 'string' &&
|
|
146
|
+
typeof entry.file === 'string' &&
|
|
147
|
+
typeof entry.id === 'string' &&
|
|
148
|
+
typeof entry.ref === 'string' &&
|
|
149
|
+
typeof entry.reads === 'number' &&
|
|
150
|
+
Number.isFinite(entry.reads) &&
|
|
151
|
+
entry.reads >= 0;
|
|
152
|
+
if (!ok) continue;
|
|
153
|
+
entry.reads = Math.floor(entry.reads);
|
|
154
|
+
out.push(entry);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
} catch {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
package/src/installer.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join } from 'node:path';
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { parseFrontmatter, type FrontmatterData } from './frontmatter.ts';
|
|
7
|
+
import { DEFAULT_CONFIG } from './config.ts';
|
|
7
8
|
import type { Scope } from './manifest.ts';
|
|
8
9
|
|
|
9
10
|
export type ContentItem = {
|
|
@@ -172,22 +173,7 @@ export function installTo(target: Target, opts: InstallOptions): InstallResult {
|
|
|
172
173
|
let configExists = false;
|
|
173
174
|
try { configExists = lstatSync(configPath).isFile() || lstatSync(configPath).isSymbolicLink(); } catch { configExists = false; }
|
|
174
175
|
if (!configExists) {
|
|
175
|
-
|
|
176
|
-
'mode=guided',
|
|
177
|
-
'branch=feature/{type}-{issue}-{slug}',
|
|
178
|
-
'commit=conventional',
|
|
179
|
-
'auto_commit=on',
|
|
180
|
-
'coverage_new=90',
|
|
181
|
-
'coverage_modified=80',
|
|
182
|
-
'review_depth=full',
|
|
183
|
-
'quality_depth=full',
|
|
184
|
-
'verify_merged=off',
|
|
185
|
-
'delegate_threshold=60',
|
|
186
|
-
'heal_max_cycles=3',
|
|
187
|
-
'verbosity=normal',
|
|
188
|
-
'# context_budget_chars=150000 # optional: fail archive if trail exceeds this (measured in report Cost section)',
|
|
189
|
-
].join('\n') + '\n';
|
|
190
|
-
if (!dryRun) { mkdirSync(dirname(configPath), { recursive: true }); writeFileSync(configPath, body); }
|
|
176
|
+
if (!dryRun) { mkdirSync(dirname(configPath), { recursive: true }); writeFileSync(configPath, DEFAULT_CONFIG); }
|
|
191
177
|
result.written.push(configPath);
|
|
192
178
|
result.notes.push(`default config written: ${configPath} (edit it to customise)`);
|
|
193
179
|
}
|
package/src/integrity.ts
CHANGED
|
@@ -38,7 +38,7 @@ function findSecrets(body: string): Array<{ label: string; hit: string }> {
|
|
|
38
38
|
return out;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const TRAIL_EXTS = new Set(['.md', '.json', '.sh']);
|
|
41
|
+
const TRAIL_EXTS = new Set(['.md', '.json', '.sh', '.jsonl']);
|
|
42
42
|
|
|
43
43
|
function trailFiles(dir: string): string[] {
|
|
44
44
|
const out: string[] = [];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/investigation.ts
|
|
2
|
+
// Phase 2 Context Governor — bounded investigation state machine
|
|
3
|
+
// (Native Cost Governor initiative, plan §51 Phase 2, spec §13).
|
|
4
|
+
//
|
|
5
|
+
// Honest boundary: Phase 2 produces the verdict + the audit record; the
|
|
6
|
+
// Phase-3+ consumer (Work Governor) supplies the inputs (pass state,
|
|
7
|
+
// acceptance/surface/path signals, unrelated files opened, repeated reads
|
|
8
|
+
// from the evidence registry) and acts on the verdict. Limits come from
|
|
9
|
+
// readInvestigationConfig (T3). Stop verdicts are emitted as optimization
|
|
10
|
+
// decision records via the sanitized recordOptDecision (T4's S2 fix).
|
|
11
|
+
import { recordOptDecision } from './cost.ts';
|
|
12
|
+
|
|
13
|
+
export type InvestigationStatus = {
|
|
14
|
+
pass: number;
|
|
15
|
+
stop: boolean;
|
|
16
|
+
reason: '' | 'max passes' | 'max unrelated files' | 'repeated read' | 'objective met';
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type InvestigationInput = {
|
|
20
|
+
pass: number;
|
|
21
|
+
acceptance_mapped: boolean;
|
|
22
|
+
surface_understood: boolean;
|
|
23
|
+
path_established: boolean;
|
|
24
|
+
unrelated_files_opened: number;
|
|
25
|
+
repeated_reads: number;
|
|
26
|
+
max_passes: number;
|
|
27
|
+
max_unrelated_files: number;
|
|
28
|
+
repeated_read_threshold: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Evaluate the investigation against the three limits (spec §13) plus the
|
|
33
|
+
* objective-met stop. Objective-met wins (stop condition first): when the
|
|
34
|
+
* acceptance is mapped, the surface understood, and the path established,
|
|
35
|
+
* the investigation stops regardless of the counters. Otherwise stop at the
|
|
36
|
+
* first limit that fires — max passes (>=), max unrelated files (>), or
|
|
37
|
+
* repeated reads (>= threshold). Else continue.
|
|
38
|
+
*/
|
|
39
|
+
export function evaluateInvestigation(input: InvestigationInput): InvestigationStatus {
|
|
40
|
+
const { pass } = input;
|
|
41
|
+
if (input.acceptance_mapped && input.surface_understood && input.path_established) {
|
|
42
|
+
return { pass, stop: true, reason: 'objective met' };
|
|
43
|
+
}
|
|
44
|
+
if (pass >= input.max_passes) {
|
|
45
|
+
return { pass, stop: true, reason: 'max passes' };
|
|
46
|
+
}
|
|
47
|
+
if (input.unrelated_files_opened > input.max_unrelated_files) {
|
|
48
|
+
return { pass, stop: true, reason: 'max unrelated files' };
|
|
49
|
+
}
|
|
50
|
+
if (input.repeated_reads >= input.repeated_read_threshold) {
|
|
51
|
+
return { pass, stop: true, reason: 'repeated read' };
|
|
52
|
+
}
|
|
53
|
+
return { pass, stop: false, reason: '' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Record a stop verdict as an optimization decision when status.stop.
|
|
58
|
+
* Reuses recordOptDecision (sanitized, S2). Records nothing when not stopped.
|
|
59
|
+
*/
|
|
60
|
+
export function recordInvestigationStop(
|
|
61
|
+
missionDir: string,
|
|
62
|
+
status: InvestigationStatus,
|
|
63
|
+
evidence?: string,
|
|
64
|
+
): void {
|
|
65
|
+
if (!status.stop) return;
|
|
66
|
+
recordOptDecision(missionDir, {
|
|
67
|
+
actor: 'cost-governor',
|
|
68
|
+
decision: 'stop investigation',
|
|
69
|
+
reason: status.reason,
|
|
70
|
+
...(evidence ? { evidence } : {}),
|
|
71
|
+
});
|
|
72
|
+
}
|
package/src/mission.ts
CHANGED
|
@@ -4,10 +4,15 @@ import { existsSync, rmSync, readFileSync, readdirSync, mkdirSync, writeFileSync
|
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { checkTrail, formatIssues } from './integrity.ts';
|
|
7
|
+
import { checkMissionArtifacts } from './check-artifacts.ts';
|
|
7
8
|
import { generateRollback } from './rollback.ts';
|
|
8
9
|
import { writeProvenance } from './provenance.ts';
|
|
9
10
|
import { rankFiles, renderRouting } from './routing.ts';
|
|
10
11
|
import { formatFootprint, measureContextChars, readBudgetConfig } from './budget.ts';
|
|
12
|
+
import { budgetForLane, costEnvelope, appendCostEvent } from './cost.ts';
|
|
13
|
+
import { loadRegistry } from './evidence.ts';
|
|
14
|
+
import { computeContextMetrics, contextStatus } from './context.ts';
|
|
15
|
+
import { buildCostLedger, renderAdaptationSection } from './reporting.ts';
|
|
11
16
|
|
|
12
17
|
function isStateFile(f: string): boolean {
|
|
13
18
|
// state.json (solo) or <member>.json (team) — never continue*.json
|
|
@@ -122,6 +127,12 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
122
127
|
if (issues.length) {
|
|
123
128
|
throw new Error(`closure integrity gate failed — fix these before archiving:\n${formatIssues(issues)}`);
|
|
124
129
|
}
|
|
130
|
+
// Artifact gate (roadmap v0.8 item 4): Lane 2+ missions must carry
|
|
131
|
+
// plan.md + flows/* evidence — a mission without its trail does not fold.
|
|
132
|
+
const artifacts = checkMissionArtifacts(dir);
|
|
133
|
+
if (!artifacts.ok) {
|
|
134
|
+
throw new Error(`archive artifact gate failed — missing: ${artifacts.missing.join(', ')} (lane ${artifacts.lane}). Write the evidence trail before archiving.`);
|
|
135
|
+
}
|
|
125
136
|
}
|
|
126
137
|
|
|
127
138
|
const files = readdirSync(dir);
|
|
@@ -142,18 +153,51 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
142
153
|
const chars = measureContextChars(dir);
|
|
143
154
|
const budget = readBudgetConfig(projectDir);
|
|
144
155
|
const footprintLine = formatFootprint(chars, budget);
|
|
145
|
-
if (budget && chars > budget) {
|
|
146
|
-
throw new Error(`closure context budget failed — ${footprintLine}. Trim the trail or raise context_budget_chars.`);
|
|
147
|
-
}
|
|
148
156
|
const est = typeof state.tokens_est === 'number' ? state.tokens_est : 0;
|
|
149
157
|
const src = typeof state.tokens_source === 'string' ? state.tokens_source : 'computed';
|
|
150
158
|
const lane = typeof state.lane === 'string' ? state.lane : 'unknown';
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
159
|
+
// C2: `status` gates on the LANE token budget (what savepoint.sh enforces),
|
|
160
|
+
// never on the context char budget. `contextStatus` below is its own gate.
|
|
161
|
+
const laneBudget = budgetForLane(lane);
|
|
162
|
+
// Q1/Q2: the normalized envelope computes planned/used/remaining/pct/status
|
|
163
|
+
// on the lane token budget — ONE computation (Q2), reused at render and for
|
|
164
|
+
// the closure event. `effBudget` stays only for the readable delta display.
|
|
165
|
+
const env = costEnvelope({ lane, budget: laneBudget, tokens_est: est });
|
|
166
|
+
const effBudget = budget || laneBudget; // display-only delta basis — never for status (C2)
|
|
155
167
|
const delta = effBudget ? (est <= effBudget ? `${(effBudget - est).toLocaleString()} under` : `${(est - effBudget).toLocaleString()} over`) : 'no budget configured';
|
|
156
168
|
const srcLabel = src === 'reported' ? 'provider-reported' : 'estimator';
|
|
169
|
+
const statusLabel = env.status.toUpperCase(); // derived from the single computation, not recomputed
|
|
170
|
+
// context status on context_budget_chars — separate gate, never token `est` (C2)
|
|
171
|
+
const ctxStatus = contextStatus(budget, chars);
|
|
172
|
+
// context-efficiency metrics from the evidence registry (reads), else all-zero
|
|
173
|
+
// with a note — a zero row must not be misread as "efficient" (risk row).
|
|
174
|
+
const registry = loadRegistry(dir);
|
|
175
|
+
const reads_total = registry.reduce((s, e) => s + e.reads, 0);
|
|
176
|
+
const repeated_reads = registry.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
|
|
177
|
+
// M1: honest char accounting — each registered entry carries the content
|
|
178
|
+
// length it holds (chars). total_chars = chars actually loaded (each entry
|
|
179
|
+
// × its reads); unique_chars = distinct payload bytes. computeContextMetrics
|
|
180
|
+
// derives duplicate_chars = total − unique (bytes re-read) and
|
|
181
|
+
// read_avoidance_chars = same (bytes not reloaded by reuse). When a
|
|
182
|
+
// registry exists but carries no char payloads (legacy/absent field), the
|
|
183
|
+
// char fields render as n/a — never fabricated 0 — so reuse_rate > 0 never
|
|
184
|
+
// sits beside a false "read_avoidance_chars: 0".
|
|
185
|
+
const unique_chars = registry.reduce((s, e) => s + (e.chars ?? 0), 0);
|
|
186
|
+
const total_chars = registry.reduce((s, e) => s + (e.chars ?? 0) * e.reads, 0);
|
|
187
|
+
const charTracked = registry.length > 0 && total_chars > 0;
|
|
188
|
+
const metrics = registry.length
|
|
189
|
+
? computeContextMetrics({
|
|
190
|
+
files_loaded: registry.length,
|
|
191
|
+
reads_total,
|
|
192
|
+
reads_reused: repeated_reads,
|
|
193
|
+
unique_chars,
|
|
194
|
+
total_chars,
|
|
195
|
+
repeated_reads,
|
|
196
|
+
})
|
|
197
|
+
: { files_loaded: 0, repeated_reads: 0, duplicate_chars: 0, reuse_rate: 0, read_avoidance_chars: 0 };
|
|
198
|
+
const ctxNote = registry.length
|
|
199
|
+
? (charTracked ? '' : ' (char data not tracked)')
|
|
200
|
+
: ' (no registry — reads not tracked)';
|
|
157
201
|
// provider-reported rollup when any stage reported
|
|
158
202
|
let reportedTotal = 0;
|
|
159
203
|
let hasReported = false;
|
|
@@ -176,14 +220,54 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
176
220
|
'| Metric | Value |',
|
|
177
221
|
'|--------|-------|',
|
|
178
222
|
`| **Tokens used** | ${est.toLocaleString()} (${srcLabel}) |`,
|
|
179
|
-
`| **Lane** | ${lane} (budget ${effBudget ? effBudget.toLocaleString() : '—'} · warn ${effBudget ?
|
|
180
|
-
`| **Budget status** | ${effBudget ? `${pct}% of budget · ${delta} · ${
|
|
223
|
+
`| **Lane** | ${lane} (budget ${effBudget ? effBudget.toLocaleString() : '—'} · warn ${effBudget ? env.warn_at.toLocaleString() : '—'} · stop ${effBudget ? env.stop_at.toLocaleString() : '—'}) |`,
|
|
224
|
+
`| **Budget status** | ${effBudget ? `${env.pct}% of budget · ${delta} · ${statusLabel}` : 'no lane budget'} |`,
|
|
181
225
|
`| **Context footprint** | ${chars.toLocaleString()} chars${budget ? ` (budget ${budget.toLocaleString()})` : ' (no context budget configured)'} |`,
|
|
226
|
+
`| **Context budget status** | ${ctxStatus.toUpperCase()}${budget ? ` (budget ${budget.toLocaleString()})` : ' (no context budget configured)'} |`,
|
|
227
|
+
`| **Context efficiency** | files_loaded: ${metrics.files_loaded} · repeated_reads: ${metrics.repeated_reads} · duplicate_chars: ${charTracked ? metrics.duplicate_chars : 'n/a'} · reuse_rate: ${metrics.reuse_rate} · read_avoidance_chars: ${charTracked ? metrics.read_avoidance_chars : 'n/a'}${ctxNote} |`,
|
|
182
228
|
].join('\n');
|
|
183
229
|
if (hasReported) {
|
|
184
230
|
costSection += `\n| **Provider total** | ${reportedTotal.toLocaleString()} (provider-reported — sum of reported stages) |`;
|
|
185
231
|
}
|
|
232
|
+
// Phase 8 Reporting — ledger/avoided/efficiency/trail rows (§39/§43)
|
|
233
|
+
try {
|
|
234
|
+
const ledger = buildCostLedger({ missionDir: dir, envelope: env });
|
|
235
|
+
costSection += `\n| Budget | ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned}) |`;
|
|
236
|
+
costSection += `\n| Context | ${chars.toLocaleString()} chars, reuse ${ledger.efficiency.reuse_rate} |`;
|
|
237
|
+
costSection += `\n| Avoided | ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ${ledger.avoided.tokens_avoided_est} tokens est |`;
|
|
238
|
+
costSection += `\n| Efficiency | reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}% |`;
|
|
239
|
+
costSection += `\n| Trail | ${ledger.trail.length} decisions |`;
|
|
240
|
+
if (ledger.trail.length) {
|
|
241
|
+
const show = ledger.trail.slice(0, 5);
|
|
242
|
+
for (const t of show) costSection += `\n- ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ''}`;
|
|
243
|
+
if (ledger.trail.length > 5) costSection += `\n… ${ledger.trail.length - 5} more`;
|
|
244
|
+
}
|
|
245
|
+
} catch { /* ledger best-effort — trail parse failure never blocks archive */ }
|
|
186
246
|
costSection += '\n';
|
|
247
|
+
// Phase E — adaptation summary from the posture decision trail
|
|
248
|
+
try {
|
|
249
|
+
costSection += renderAdaptationSection(dir);
|
|
250
|
+
} catch { /* best-effort */ }
|
|
251
|
+
|
|
252
|
+
// Cost Governor: record the closure cost event — the mission's final
|
|
253
|
+
// cost snapshot, folded into report.md with the rest of the trail.
|
|
254
|
+
// (Phase 1 — native cost governor; pure append, never rewrites state.)
|
|
255
|
+
appendCostEvent(dir, {
|
|
256
|
+
kind: 'closure',
|
|
257
|
+
mission,
|
|
258
|
+
tokens_est: est,
|
|
259
|
+
budget: laneBudget,
|
|
260
|
+
status: env.status, // Q2: the single lane-token-budget computation
|
|
261
|
+
context_chars: chars,
|
|
262
|
+
context_status: ctxStatus,
|
|
263
|
+
context_metrics: metrics,
|
|
264
|
+
});
|
|
265
|
+
// M2: the closure event (with context_status possibly 'over') is recorded
|
|
266
|
+
// BEFORE the hard gate throws — an over-budget closure still leaves a
|
|
267
|
+
// ledger row so the over-budget condition is observable, never erased.
|
|
268
|
+
if (budget && chars > budget) {
|
|
269
|
+
throw new Error(`closure context budget failed — ${footprintLine}. Trim the trail or raise context_budget_chars.`);
|
|
270
|
+
}
|
|
187
271
|
}
|
|
188
272
|
|
|
189
273
|
// Fold order: narrative artifacts first, wave evidence last (chronological).
|
|
@@ -194,13 +278,30 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
194
278
|
}
|
|
195
279
|
// Flow artifacts: flows/ is the current layout; a legacy mission that still
|
|
196
280
|
// keeps waves/ folds from there so an upgrade never strands a trail.
|
|
281
|
+
// 07-pr-verdict.md SURVIVES archive as a standalone `pr-verdict.md` at the
|
|
282
|
+
// mission root — it is the PR material handed to the user, so it must not
|
|
283
|
+
// fold away into report.md.
|
|
284
|
+
const PR_VERDICT = 'pr-verdict.md';
|
|
285
|
+
const PR_VERDICT_SRC = join('flows', '07-pr-verdict.md');
|
|
197
286
|
const flowsDir = join(dir, 'flows');
|
|
198
287
|
const legacyWavesDir = join(dir, 'waves');
|
|
199
288
|
const artDir = existsSync(flowsDir) ? flowsDir : existsSync(legacyWavesDir) ? legacyWavesDir : flowsDir;
|
|
200
289
|
const artRel = artDir === legacyWavesDir ? 'waves' : 'flows';
|
|
201
290
|
if (existsSync(artDir)) {
|
|
202
|
-
for (const f of readdirSync(artDir).sort())
|
|
291
|
+
for (const f of readdirSync(artDir).sort()) {
|
|
292
|
+
if (artRel === 'flows' && f === '07-pr-verdict.md') continue; // survives as pr-verdict.md
|
|
293
|
+
fold.push(join(artRel, f));
|
|
294
|
+
}
|
|
203
295
|
}
|
|
296
|
+
// Cost events ledger — appended by the closure event above (or a prior
|
|
297
|
+
// savepoint in a later phase); folds like any other trail artifact so
|
|
298
|
+
// nothing survives loose after archive.
|
|
299
|
+
if (existsSync(join(dir, 'cost-events.jsonl'))) fold.push('cost-events.jsonl');
|
|
300
|
+
// H1: the context registry is the same class of artifact as cost-events.jsonl
|
|
301
|
+
// (append-only JSONL ledger) — fold it into report.md and remove it so it does
|
|
302
|
+
// NOT survive loose after archive (survival parity; the fold loop below also
|
|
303
|
+
// removes every folded file).
|
|
304
|
+
if (existsSync(join(dir, 'context-registry.jsonl'))) fold.push('context-registry.jsonl');
|
|
204
305
|
|
|
205
306
|
// The report survives: an existing report.md wins; otherwise the closure
|
|
206
307
|
// wave seeds it; otherwise it starts empty.
|
|
@@ -211,6 +312,14 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
211
312
|
|
|
212
313
|
if (!dryRun) {
|
|
213
314
|
mkdirSync(dir, { recursive: true });
|
|
315
|
+
// PR verdict survives archive as a standalone file at the mission root —
|
|
316
|
+
// it is the PR material handed to the user and must not fold away.
|
|
317
|
+
const prVerdictSrc = join(dir, PR_VERDICT_SRC);
|
|
318
|
+
if (existsSync(prVerdictSrc)) {
|
|
319
|
+
const prVerdictPath = join(dir, PR_VERDICT);
|
|
320
|
+
writeFileSync(prVerdictPath, readFileSync(prVerdictSrc, 'utf8'));
|
|
321
|
+
kept.push(join('missions', mission, PR_VERDICT));
|
|
322
|
+
}
|
|
214
323
|
if (fold.length) {
|
|
215
324
|
const sections = fold.map((f) => {
|
|
216
325
|
const body = readFileSync(join(dir, f), 'utf8').trim();
|
|
@@ -235,6 +344,11 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
235
344
|
rmSync(join(dir, f), { force: true, recursive: true });
|
|
236
345
|
removed.push(join('missions', mission, f));
|
|
237
346
|
}
|
|
347
|
+
// the pr-verdict source was copied to the root — remove the flows/ copy
|
|
348
|
+
if (existsSync(prVerdictSrc)) {
|
|
349
|
+
rmSync(join(dir, PR_VERDICT_SRC), { force: true });
|
|
350
|
+
removed.push(join('missions', mission, PR_VERDICT_SRC));
|
|
351
|
+
}
|
|
238
352
|
// session state dies with the mission
|
|
239
353
|
for (const f of files.filter((f) => f.endsWith('.json'))) rmSync(join(dir, f), { force: true });
|
|
240
354
|
// flows/ may now be empty — remove the folder
|
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
|
+
}
|