@ionivetech/mugiwara 0.8.0 → 0.8.2
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/README.md +2 -2
- package/content/agents/brook-healing.md +1 -1
- 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-brainstorm/SKILL.md +5 -3
- package/content/skills/mugiwara-checkpoint/SKILL.md +2 -0
- package/content/skills/mugiwara-execution/SKILL.md +4 -3
- package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
- package/content/skills/mugiwara-gates/SKILL.md +6 -0
- package/content/skills/mugiwara-healing/SKILL.md +5 -1
- package/content/skills/mugiwara-lessons/SKILL.md +3 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +7 -6
- package/content/skills/mugiwara-planning/SKILL.md +2 -0
- package/content/skills/mugiwara-quality/SKILL.md +3 -14
- package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
- package/content/skills/mugiwara-resume/SKILL.md +3 -14
- package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
- package/content/skills/mugiwara-review/SKILL.md +3 -15
- package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
- package/content/skills/mugiwara-security/SKILL.md +1 -0
- package/content/skills/mugiwara-ship/SKILL.md +2 -0
- package/content/skills/mugiwara-workflow/SKILL.md +28 -25
- package/dist/mugiwara.js +1323 -402
- package/gemini-extension.json +1 -1
- package/hooks/mugiwara-mode-tracker.js +24 -4
- package/hooks/mugiwara-mode-tracker.ts +36 -7
- 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/wave-banners.md +1 -2
- package/scripts/gate-selftest.ts +239 -21
- package/scripts/lane-base.ts +16 -0
- package/scripts/lane.sh +5 -1
- package/scripts/lib/lane-base.sh +1 -1
- package/scripts/savepoint.sh +48 -5
- package/scripts/validate-content.ts +60 -0
- package/scripts/verify-install.ts +20 -0
- package/scripts/write-metrics.ts +73 -0
- package/src/budget.ts +11 -0
- package/src/cli.ts +185 -28
- package/src/config.ts +6 -0
- package/src/continue.ts +36 -1
- package/src/cost.ts +4 -1
- package/src/installer.ts +27 -4
- package/src/integrity.ts +105 -25
- package/src/mission.ts +123 -7
- package/src/policy.ts +372 -4
- package/src/provenance.ts +29 -9
- package/src/sign.ts +45 -3
- package/content/skills/mugiwara-workflow/references/adaptive-budget-governor.md +0 -5
- package/content/skills/mugiwara-workflow/references/benchmark-governor.md +0 -53
- package/content/skills/mugiwara-workflow/references/cognitive-output-governor.md +0 -5
- package/content/skills/mugiwara-workflow/references/scope-code-governor.md +0 -14
- package/content/skills/mugiwara-workflow/references/stop-slop-governor.md +0 -14
package/src/integrity.ts
CHANGED
|
@@ -10,29 +10,73 @@
|
|
|
10
10
|
// 3. Evidence — cited wave/evidence paths exist.
|
|
11
11
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
12
12
|
import { isAbsolute, join, relative } from 'node:path';
|
|
13
|
+
import { loadPolicy } from './policy.ts';
|
|
13
14
|
|
|
14
|
-
export type IntegrityIssue = {
|
|
15
|
+
export type IntegrityIssue = {
|
|
16
|
+
kind: 'dangling-path' | 'secret' | 'secret-warn' | 'evidence' | 'evidence-thin';
|
|
17
|
+
detail: string;
|
|
18
|
+
severity?: 'block' | 'warn';
|
|
19
|
+
};
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
[/
|
|
21
|
-
[
|
|
22
|
-
[/
|
|
23
|
-
[/
|
|
21
|
+
export type SecretSeverity = 'block' | 'warn';
|
|
22
|
+
export type SecretPattern = [RegExp, string, SecretSeverity];
|
|
23
|
+
|
|
24
|
+
export const SECRET_PATTERNS: Array<SecretPattern> = [
|
|
25
|
+
[/AKIA[0-9A-Z]{16}/, 'AWS access key id', 'block'],
|
|
26
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----/, 'private key block', 'block'],
|
|
27
|
+
[/gh[pousr]_[A-Za-z0-9]{20,}/, 'GitHub token', 'block'],
|
|
28
|
+
[/xox[baprs]-[A-Za-z0-9-]{10,}/, 'Slack token', 'block'],
|
|
29
|
+
[/sk-[A-Za-z0-9]{32,}/, 'API key (sk-…)', 'block'],
|
|
30
|
+
[/eyJhbGciOi[A-Za-z0-9_.-]{20,}/, 'JWT pasted verbatim', 'block'],
|
|
31
|
+
[/(api[_-]?key|secret|passwd|password)\s*[=:]\s*["'][^"'\s]{8,}["']/i, 'credential assignment', 'block'],
|
|
32
|
+
[/AIza[0-9A-Za-z_-]{35}/, 'Google API key', 'block'],
|
|
33
|
+
[/ya29\.[0-9A-Za-z_-]{20,}/, 'Google OAuth token', 'block'],
|
|
34
|
+
[/\b[a-z][a-z0-9+.-]*:\/\/[^\s:@/]+:[^\s@/]{4,}@[^\s/]+/i, 'connection string with inline credential', 'block'],
|
|
35
|
+
[/\bAC[a-f0-9]{32}\b/, 'Twilio account SID', 'block'],
|
|
36
|
+
[/\bSK[a-f0-9]{32}\b/, 'Twilio API key', 'block'],
|
|
37
|
+
[/\bglpat-[A-Za-z0-9_-]{20,}/, 'GitLab token', 'block'],
|
|
38
|
+
[/\bnpm_[A-Za-z0-9]{36}\b/, 'npm token', 'block'],
|
|
39
|
+
[/\bdop_v1_[a-f0-9]{64}\b/, 'DigitalOcean token', 'block'],
|
|
40
|
+
[/\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13})\b/, 'card-number shape (verify before committing)', 'warn'],
|
|
24
41
|
];
|
|
25
42
|
|
|
26
43
|
const ALLOW_SECRET = 'mugiwara:allow-secret';
|
|
27
44
|
|
|
45
|
+
function loadExtraPatterns(projectRoot: string): Array<SecretPattern> {
|
|
46
|
+
try {
|
|
47
|
+
const policy = loadPolicy(projectRoot);
|
|
48
|
+
const extras = (policy as unknown as { integrity?: { extra_secret_patterns?: Array<{ pattern: string; label: string; severity?: SecretSeverity }> } })?.integrity?.extra_secret_patterns;
|
|
49
|
+
if (!extras || !Array.isArray(extras)) return [];
|
|
50
|
+
const out: Array<SecretPattern> = [];
|
|
51
|
+
for (const e of extras) {
|
|
52
|
+
if (!e || typeof (e as Record<string, unknown>).pattern !== 'string' || typeof (e as Record<string, unknown>).label !== 'string') continue;
|
|
53
|
+
const rec = e as { pattern: string; label: string; severity?: SecretSeverity };
|
|
54
|
+
const sev: SecretSeverity = rec.severity === 'warn' ? 'warn' : 'block';
|
|
55
|
+
try {
|
|
56
|
+
const re = new RegExp(rec.pattern);
|
|
57
|
+
out.push([re, rec.label, sev]);
|
|
58
|
+
} catch {
|
|
59
|
+
// invalid regex — skip
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
28
68
|
/** Secret shapes per line; a line carrying the allow marker is skipped — deliberate examples stay possible. */
|
|
29
|
-
function findSecrets(
|
|
30
|
-
|
|
69
|
+
export function findSecrets(
|
|
70
|
+
body: string,
|
|
71
|
+
extra?: Array<SecretPattern>,
|
|
72
|
+
): Array<{ label: string; hit: string; severity: SecretSeverity }> {
|
|
73
|
+
const out: Array<{ label: string; hit: string; severity: SecretSeverity }> = [];
|
|
74
|
+
const patterns: Array<SecretPattern> = extra ? [...SECRET_PATTERNS, ...extra] : SECRET_PATTERNS;
|
|
31
75
|
for (const line of body.split(/\r?\n/)) {
|
|
32
76
|
if (line.includes(ALLOW_SECRET)) continue;
|
|
33
|
-
for (const [re, label] of
|
|
77
|
+
for (const [re, label, severity] of patterns) {
|
|
34
78
|
const hit = line.match(re);
|
|
35
|
-
if (hit) out.push({ label, hit: hit[0] });
|
|
79
|
+
if (hit) out.push({ label, hit: hit[0], severity: (severity ?? 'block') as SecretSeverity });
|
|
36
80
|
}
|
|
37
81
|
}
|
|
38
82
|
return out;
|
|
@@ -89,6 +133,7 @@ function collectPassCitedPaths(missionDir: string): string[] {
|
|
|
89
133
|
export function checkTrail(missionDir: string, projectRoot: string): IntegrityIssue[] {
|
|
90
134
|
const issues: IntegrityIssue[] = [];
|
|
91
135
|
const files = trailFiles(missionDir);
|
|
136
|
+
const extraPatterns = loadExtraPatterns(projectRoot);
|
|
92
137
|
|
|
93
138
|
// 1 + 2: per-file link resolution and secret scan
|
|
94
139
|
for (const f of files) {
|
|
@@ -105,33 +150,66 @@ export function checkTrail(missionDir: string, projectRoot: string): IntegrityIs
|
|
|
105
150
|
});
|
|
106
151
|
}
|
|
107
152
|
}
|
|
108
|
-
for (const { label, hit } of findSecrets(body)) {
|
|
153
|
+
for (const { label, hit, severity } of findSecrets(body, extraPatterns.length ? extraPatterns : undefined)) {
|
|
154
|
+
const isWarn = severity === 'warn';
|
|
109
155
|
issues.push({
|
|
110
|
-
kind: 'secret',
|
|
156
|
+
kind: isWarn ? 'secret-warn' : 'secret',
|
|
111
157
|
detail: `${relative(projectRoot, f)} matches ${label}: ${hit.slice(0, 12)}…`,
|
|
158
|
+
severity: isWarn ? 'warn' : 'block',
|
|
112
159
|
});
|
|
113
160
|
}
|
|
114
161
|
}
|
|
115
162
|
|
|
116
163
|
// 3: evidence entries recorded as repo paths must exist
|
|
117
164
|
const evidencePaths: string[] = [];
|
|
118
|
-
|
|
119
|
-
|
|
165
|
+
// Solo layout writes state.json; team layout writes <member>.json per member.
|
|
166
|
+
// Reading only state.json left the evidence gate dead on the team path (B2).
|
|
167
|
+
const stateFiles = existsSync(missionDir)
|
|
168
|
+
? readdirSync(missionDir)
|
|
169
|
+
.filter((n) => n.endsWith('.json') && n !== 'continue.json' && !n.startsWith('continue-'))
|
|
170
|
+
.sort()
|
|
171
|
+
: [];
|
|
172
|
+
for (const name of stateFiles) {
|
|
173
|
+
const evidenceFile = join(missionDir, name);
|
|
120
174
|
try {
|
|
121
175
|
const s = JSON.parse(readFileSync(evidenceFile, 'utf8')) as { evidence?: unknown };
|
|
122
|
-
if (Array.isArray(s.evidence))
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
176
|
+
if (!Array.isArray(s.evidence)) continue;
|
|
177
|
+
for (const e of s.evidence) {
|
|
178
|
+
if (typeof e !== 'string' || !e.trim()) continue;
|
|
179
|
+
evidencePaths.push(e);
|
|
180
|
+
const cand = join(projectRoot, e);
|
|
181
|
+
if (!isAbsolute(e) && !existsSync(cand) && !existsSync(join(missionDir, e))) {
|
|
182
|
+
issues.push({ kind: 'evidence', detail: `${name} evidence "${e}" does not exist` });
|
|
130
183
|
}
|
|
131
184
|
}
|
|
132
185
|
} catch { /* corrupt state — the state reader owns that error */ }
|
|
133
186
|
}
|
|
134
187
|
|
|
188
|
+
// Iron Law: no evidence = not complete. Absent evidence is a different failure
|
|
189
|
+
// from thin evidence, and previously went unreported entirely. (B7)
|
|
190
|
+
if (evidencePaths.length === 0) {
|
|
191
|
+
let severity: 'warn' | 'block' = 'warn';
|
|
192
|
+
try {
|
|
193
|
+
const policy = loadPolicy(projectRoot);
|
|
194
|
+
const lanes = (policy as unknown as { evidence?: { require_nonempty_for_lanes?: string[] } })?.evidence?.require_nonempty_for_lanes;
|
|
195
|
+
if (Array.isArray(lanes) && lanes.length) {
|
|
196
|
+
const stateLanes = new Set<string>();
|
|
197
|
+
for (const name of stateFiles) {
|
|
198
|
+
try {
|
|
199
|
+
const s = JSON.parse(readFileSync(join(missionDir, name), 'utf8')) as { lane?: unknown };
|
|
200
|
+
if (typeof s.lane === 'string') stateLanes.add(s.lane);
|
|
201
|
+
} catch { /* ignore */ }
|
|
202
|
+
}
|
|
203
|
+
if ([...stateLanes].some((l) => lanes.includes(l))) severity = 'block';
|
|
204
|
+
}
|
|
205
|
+
} catch { /* policy read failure -> keep warn */ }
|
|
206
|
+
issues.push({
|
|
207
|
+
kind: 'evidence',
|
|
208
|
+
severity,
|
|
209
|
+
detail: 'mission declares no evidence — closing with zero recorded checks',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
135
213
|
// 4: evidence-content spot check (T7): a PASS verdict that cites an evidence
|
|
136
214
|
// path must point at a file that exists AND contains command-output shape
|
|
137
215
|
// (backticked command or exit-status token). Fake-but-consistent trails
|
|
@@ -139,6 +217,8 @@ export function checkTrail(missionDir: string, projectRoot: string): IntegrityIs
|
|
|
139
217
|
const passCited = collectPassCitedPaths(missionDir);
|
|
140
218
|
for (const e of passCited) {
|
|
141
219
|
if (!e.trim() || isAbsolute(e)) continue;
|
|
220
|
+
// state/continue are machine-generated JSON, not evidence with command output — skip thin check
|
|
221
|
+
if (/(?:^|\/)(state|continue)(-[^\/]*)?\.json$/.test(e)) continue;
|
|
142
222
|
const candMission = join(missionDir, e);
|
|
143
223
|
const candRoot = join(projectRoot, e);
|
|
144
224
|
const resolved = existsSync(candMission) ? candMission : existsSync(candRoot) ? candRoot : null;
|
package/src/mission.ts
CHANGED
|
@@ -7,9 +7,11 @@ import { checkTrail, formatIssues } from './integrity.ts';
|
|
|
7
7
|
import { checkMissionArtifacts } from './check-artifacts.ts';
|
|
8
8
|
import { generateRollback } from './rollback.ts';
|
|
9
9
|
import { writeProvenance } from './provenance.ts';
|
|
10
|
+
import { loadPolicy } from './policy.ts';
|
|
11
|
+
import { verifyReport } from './sign.ts';
|
|
10
12
|
import { rankFiles, renderRouting } from './routing.ts';
|
|
11
|
-
import { formatFootprint, measureContextChars, readBudgetConfig } from './budget.ts';
|
|
12
|
-
import { budgetForLane, costEnvelope, appendCostEvent } from './cost.ts';
|
|
13
|
+
import { formatFootprint, measureContextChars, readBudgetConfig, shouldCompress, compressThreshold } from './budget.ts';
|
|
14
|
+
import { budgetForLane, costEnvelope, appendCostEvent, COMPRESSED_KIND } from './cost.ts';
|
|
13
15
|
import { loadRegistry } from './evidence.ts';
|
|
14
16
|
import { computeContextMetrics, contextStatus } from './context.ts';
|
|
15
17
|
import { buildCostLedger, renderAdaptationSection } from './reporting.ts';
|
|
@@ -31,6 +33,52 @@ function primaryState(dir: string, files: string[]): Record<string, unknown> | n
|
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/** Extract tasks from state.json — handles nested tasks:{done,total} (current savepoint) and legacy flat fields. */
|
|
37
|
+
function tasksFromState(state: Record<string, unknown> | null): { done: number; total: number } {
|
|
38
|
+
if (!state) return { done: 0, total: 0 };
|
|
39
|
+
const t = (state as Record<string, unknown>).tasks as { done?: unknown; total?: unknown } | undefined;
|
|
40
|
+
if (t && typeof t.done === 'number' && typeof t.total === 'number') return { done: t.done, total: t.total };
|
|
41
|
+
const done = Number((state as Record<string, unknown>).tasks_done);
|
|
42
|
+
const total = Number((state as Record<string, unknown>).tasks_total);
|
|
43
|
+
return { done: Number.isFinite(done) ? done : 0, total: Number.isFinite(total) ? total : 0 };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Fresh task counts from plan.md, with sub-plan fallback for large campaigns (>3 phases). */
|
|
47
|
+
function countPlanTasks(missionDir: string): { done: number; total: number } {
|
|
48
|
+
let total = 0;
|
|
49
|
+
let done = 0;
|
|
50
|
+
const planFile = join(missionDir, 'plan.md');
|
|
51
|
+
if (existsSync(planFile)) {
|
|
52
|
+
try {
|
|
53
|
+
const text = readFileSync(planFile, 'utf8');
|
|
54
|
+
for (const line of text.split(/\r?\n/)) {
|
|
55
|
+
if (/^\s*-\s*\[[ xX]\]/.test(line)) {
|
|
56
|
+
total++;
|
|
57
|
+
if (/^\s*-\s*\[[xX]\]/.test(line)) done++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch { /* best-effort */ }
|
|
61
|
+
}
|
|
62
|
+
if (total === 0) {
|
|
63
|
+
const subPlanDir = join(missionDir, 'sub-plan');
|
|
64
|
+
if (existsSync(subPlanDir)) {
|
|
65
|
+
try {
|
|
66
|
+
for (const f of readdirSync(subPlanDir)) {
|
|
67
|
+
if (!f.endsWith('.md')) continue;
|
|
68
|
+
const text = readFileSync(join(subPlanDir, f), 'utf8');
|
|
69
|
+
for (const line of text.split(/\r?\n/)) {
|
|
70
|
+
if (/^\s*-\s*\[[ xX]\]/.test(line)) {
|
|
71
|
+
total++;
|
|
72
|
+
if (/^\s*-\s*\[[xX]\]/.test(line)) done++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch { /* best-effort */ }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { done, total };
|
|
80
|
+
}
|
|
81
|
+
|
|
34
82
|
/** Files the mission changed, base..branch. Empty on any git failure — routing is best-effort. */
|
|
35
83
|
function changedFiles(projectDir: string, state: Record<string, unknown> | null): string[] {
|
|
36
84
|
const base = typeof state?.base_sha === 'string' ? state.base_sha : '';
|
|
@@ -101,6 +149,11 @@ export function resetMission(projectDir: string, keepLogs: boolean, force?: bool
|
|
|
101
149
|
if (existsSync(join(root, 'lessons.md'))) kept.push('lessons.md');
|
|
102
150
|
else if (existsSync(join(root, join('logs', 'lessons.md')))) kept.push(join('logs', 'lessons.md'));
|
|
103
151
|
}
|
|
152
|
+
// index.md is the archive history — after a full reset (missions gone) its entries are dangling (point to deleted report.md). A reset is a fresh start, so clear it; next archive recreates "# Mission index" header.
|
|
153
|
+
if (removed.includes('missions') && existsSync(join(root, 'index.md'))) {
|
|
154
|
+
rmSync(join(root, 'index.md'));
|
|
155
|
+
removed.push('index.md');
|
|
156
|
+
}
|
|
104
157
|
for (const f of ['config', 'manifest.json', 'backup']) {
|
|
105
158
|
if (existsSync(join(root, f))) kept.push(f);
|
|
106
159
|
}
|
|
@@ -122,10 +175,20 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
122
175
|
|
|
123
176
|
// Closure integrity gate: the trail validates itself before it
|
|
124
177
|
// folds. Dangling links, secrets, or missing evidence fail the archive.
|
|
178
|
+
// Card-number shapes are warn-only — they do not block archive.
|
|
125
179
|
if (!dryRun) {
|
|
126
180
|
const issues = checkTrail(dir, projectDir);
|
|
127
|
-
|
|
128
|
-
|
|
181
|
+
const blocking = issues.filter((i) => i.kind !== 'secret-warn' && i.severity !== 'warn');
|
|
182
|
+
if (blocking.length) {
|
|
183
|
+
throw new Error(`closure integrity gate failed — fix these before archiving:\n${formatIssues(blocking)}`);
|
|
184
|
+
}
|
|
185
|
+
if (issues.length && blocking.length === 0) {
|
|
186
|
+
// warn-only — log but do not fail
|
|
187
|
+
// console.warn is best-effort; archive proceeds
|
|
188
|
+
try {
|
|
189
|
+
const warnText = formatIssues(issues);
|
|
190
|
+
if (warnText) console.warn(`closure integrity warnings (non-blocking):\n${warnText}`);
|
|
191
|
+
} catch { /* ignore */ }
|
|
129
192
|
}
|
|
130
193
|
// Artifact gate (roadmap v0.8 item 4): Lane 2+ missions must carry
|
|
131
194
|
// plan.md + flows/* evidence — a mission without its trail does not fold.
|
|
@@ -133,6 +196,20 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
133
196
|
if (!artifacts.ok) {
|
|
134
197
|
throw new Error(`archive artifact gate failed — missing: ${artifacts.missing.join(', ')} (lane ${artifacts.lane}). Write the evidence trail before archiving.`);
|
|
135
198
|
}
|
|
199
|
+
// Attestation gate (D4): when attestation.required true, report must be signed and trusted.
|
|
200
|
+
try {
|
|
201
|
+
const policy = loadPolicy(projectDir);
|
|
202
|
+
if (policy?.attestation?.required) {
|
|
203
|
+
const v = verifyReport(projectDir, dir);
|
|
204
|
+
if (!v.ok) {
|
|
205
|
+
throw new Error(`closure integrity gate failed — attestation required but report not signed/trusted: ${v.message}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
if ((e as Error).message.startsWith('closure integrity gate failed — attestation required')) throw e;
|
|
210
|
+
if ((e as Error).message.startsWith('unknown policy key')) throw e;
|
|
211
|
+
// other policy load errors are best-effort — do not block archive
|
|
212
|
+
}
|
|
136
213
|
}
|
|
137
214
|
|
|
138
215
|
const files = readdirSync(dir);
|
|
@@ -249,6 +326,38 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
249
326
|
costSection += renderAdaptationSection(dir);
|
|
250
327
|
} catch { /* best-effort */ }
|
|
251
328
|
|
|
329
|
+
// T4: auto-compress when context >80% budget — compress flows → stub, not throw
|
|
330
|
+
// (record compressed event; closure still recorded; hard gate only at 100%)
|
|
331
|
+
if (shouldCompress(budget, chars)) {
|
|
332
|
+
try {
|
|
333
|
+
const flowsDir = join(dir, 'flows');
|
|
334
|
+
const wavesDir = join(dir, 'waves');
|
|
335
|
+
const targetDir = existsSync(flowsDir) ? flowsDir : existsSync(wavesDir) ? wavesDir : null;
|
|
336
|
+
if (targetDir && existsSync(targetDir)) {
|
|
337
|
+
const flowFiles = readdirSync(targetDir).filter(f => f.endsWith('.md'));
|
|
338
|
+
if (flowFiles.length) {
|
|
339
|
+
const pct = Math.round((chars / budget) * 100);
|
|
340
|
+
const stub = `# Compressed trail\n\nTrail ${chars} chars exceeds ${pct}% of budget ${budget} (threshold ${compressThreshold(budget)}) — flows archived as stub to preserve budget. Original flows: ${flowFiles.join(', ')}\n`;
|
|
341
|
+
for (const f of flowFiles) {
|
|
342
|
+
try { rmSync(join(targetDir, f), { force: true }); } catch {}
|
|
343
|
+
}
|
|
344
|
+
writeFileSync(join(targetDir, '00-compressed.md'), stub);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
appendCostEvent(dir, {
|
|
348
|
+
kind: COMPRESSED_KIND,
|
|
349
|
+
mission,
|
|
350
|
+
tokens_est: est,
|
|
351
|
+
budget: laneBudget,
|
|
352
|
+
status: 'compressed',
|
|
353
|
+
context_chars: chars,
|
|
354
|
+
context_status: ctxStatus,
|
|
355
|
+
context_metrics: metrics,
|
|
356
|
+
});
|
|
357
|
+
costSection += `\n| **Compressed** | yes — ${chars} chars >80% of ${budget} — flows stubbed as 00-compressed.md |`;
|
|
358
|
+
} catch { /* compress best-effort — never blocks archive */ }
|
|
359
|
+
}
|
|
360
|
+
|
|
252
361
|
// Cost Governor: record the closure cost event — the mission's final
|
|
253
362
|
// cost snapshot, folded into report.md with the rest of the trail.
|
|
254
363
|
// (Phase 1 — native cost governor; pure append, never rewrites state.)
|
|
@@ -265,6 +374,7 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
265
374
|
// M2: the closure event (with context_status possibly 'over') is recorded
|
|
266
375
|
// BEFORE the hard gate throws — an over-budget closure still leaves a
|
|
267
376
|
// ledger row so the over-budget condition is observable, never erased.
|
|
377
|
+
// T4: over 80% already compressed above; hard fail only at 100% preserves gate-selftest.
|
|
268
378
|
if (budget && chars > budget) {
|
|
269
379
|
throw new Error(`closure context budget failed — ${footprintLine}. Trim the trail or raise context_budget_chars.`);
|
|
270
380
|
}
|
|
@@ -368,17 +478,23 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
368
478
|
});
|
|
369
479
|
if (rb) kept.push(join('missions', mission, rb.file));
|
|
370
480
|
try {
|
|
481
|
+
const st = tasksFromState(state);
|
|
482
|
+
const pt = countPlanTasks(dir);
|
|
483
|
+
const tasks_done = pt.total > 0 ? pt.done : st.done;
|
|
484
|
+
const tasks_total = pt.total > 0 ? pt.total : st.total;
|
|
485
|
+
const baseShaForNote = typeof state.base_sha === 'string' ? state.base_sha : undefined;
|
|
371
486
|
writeProvenance(projectDir, dir, {
|
|
372
487
|
mission,
|
|
373
488
|
actor: typeof state.actor === 'string' ? state.actor : '',
|
|
374
489
|
lane: typeof state.lane === 'string' ? state.lane : '',
|
|
375
490
|
mode: typeof state.mode === 'string' ? state.mode : '',
|
|
376
491
|
branch: state.branch,
|
|
377
|
-
tasks_done
|
|
378
|
-
tasks_total
|
|
492
|
+
tasks_done,
|
|
493
|
+
tasks_total,
|
|
379
494
|
evidence: Array.isArray(state.evidence) ? (state.evidence as string[]) : [],
|
|
380
495
|
models: stageModels,
|
|
381
|
-
|
|
496
|
+
base_sha: baseShaForNote,
|
|
497
|
+
} as never, baseShaForNote);
|
|
382
498
|
kept.push(join('missions', mission, 'provenance.md'));
|
|
383
499
|
} catch { /* provenance is additive; archive proceeds */ }
|
|
384
500
|
}
|