@ionivetech/mugiwara 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.opencode/mugiwara-helpers.mjs +2 -2
- package/README.md +196 -330
- package/content/agents/brook-healing.md +1 -1
- package/content/agents/franky-gates.md +1 -1
- package/content/agents/luffy-orchestrator.md +2 -2
- package/content/agents/memory-keeper.md +5 -0
- package/content/agents/usopp-brainstorm.md +3 -2
- package/content/agents/zoro-execution.md +4 -3
- package/content/skills/mugiwara-backend/SKILL.md +52 -43
- package/content/skills/mugiwara-brainstorm/SKILL.md +5 -3
- package/content/skills/mugiwara-checkpoint/SKILL.md +21 -8
- package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
- package/content/skills/mugiwara-execution/SKILL.md +34 -33
- package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
- package/content/skills/mugiwara-execution/references/execution-phase-flows.md +18 -0
- package/content/skills/mugiwara-frontend/SKILL.md +44 -44
- package/content/skills/mugiwara-gates/SKILL.md +28 -16
- package/content/skills/mugiwara-healing/SKILL.md +30 -25
- package/content/skills/mugiwara-lessons/SKILL.md +3 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +10 -9
- package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
- package/content/skills/mugiwara-planning/SKILL.md +28 -14
- package/content/skills/mugiwara-planning/references/large-campaign-subplan.md +41 -0
- package/content/skills/mugiwara-planning/references/plan-template.md +22 -0
- package/content/skills/mugiwara-quality/SKILL.md +8 -13
- package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
- package/content/skills/mugiwara-resume/SKILL.md +3 -9
- package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
- package/content/skills/mugiwara-review/SKILL.md +17 -24
- package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
- package/content/skills/mugiwara-security/SKILL.md +47 -35
- package/content/skills/mugiwara-ship/SKILL.md +2 -0
- package/content/skills/mugiwara-workflow/SKILL.md +13 -13
- package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +6 -3
- package/dist/mugiwara.js +1802 -316
- package/gemini-extension.json +1 -1
- package/hooks/mugiwara-mode-tracker.js +24 -4
- package/hooks/mugiwara-mode-tracker.ts +36 -7
- package/hooks/pipeline-guard.js +1 -1
- package/hooks/pipeline-guard.ts +2 -1
- package/hooks/session-start.js +6 -1
- package/hooks/session-start.ts +8 -1
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/cost-governor.md +104 -0
- package/references/multi-actor.md +21 -0
- package/references/posture-routing.md +31 -0
- package/references/wave-banners.md +1 -2
- package/scripts/benchmark-governor.ts +516 -0
- package/scripts/benchmark-thresholds.json +47 -0
- package/scripts/check-doc-links.ts +8 -2
- package/scripts/gate-selftest.ts +104 -21
- package/scripts/lib/lane-base.sh +4 -4
- package/scripts/retrieval-eval.ts +9 -3
- package/scripts/savepoint.sh +41 -2
- package/scripts/validate-content.ts +82 -3
- package/scripts/verify-install.ts +20 -0
- package/scripts/write-metrics.ts +73 -0
- package/src/adaptive-budget.ts +178 -0
- package/src/args.ts +3 -2
- package/src/budget.ts +18 -16
- package/src/check-artifacts.ts +45 -0
- package/src/cli.ts +221 -8
- package/src/cognition.ts +234 -0
- package/src/config.ts +113 -0
- package/src/context.ts +72 -0
- package/src/continue.ts +29 -0
- package/src/cost.ts +189 -0
- package/src/evidence.ts +160 -0
- package/src/installer.ts +2 -16
- package/src/integrity.ts +65 -16
- package/src/investigation.ts +72 -0
- package/src/mission.ts +246 -16
- package/src/policy.ts +355 -2
- package/src/posture.ts +86 -0
- package/src/provenance.ts +29 -9
- package/src/reporting.ts +225 -0
- package/src/scope.ts +321 -0
- package/src/sign.ts +234 -18
- package/src/slop.ts +306 -0
- package/src/work.ts +273 -0
package/src/mission.ts
CHANGED
|
@@ -4,10 +4,17 @@ 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';
|
|
10
|
+
import { loadPolicy } from './policy.ts';
|
|
11
|
+
import { verifyReport } from './sign.ts';
|
|
9
12
|
import { rankFiles, renderRouting } from './routing.ts';
|
|
10
|
-
import { formatFootprint, measureContextChars, readBudgetConfig } from './budget.ts';
|
|
13
|
+
import { formatFootprint, measureContextChars, readBudgetConfig, shouldCompress, compressThreshold } from './budget.ts';
|
|
14
|
+
import { budgetForLane, costEnvelope, appendCostEvent, COMPRESSED_KIND } from './cost.ts';
|
|
15
|
+
import { loadRegistry } from './evidence.ts';
|
|
16
|
+
import { computeContextMetrics, contextStatus } from './context.ts';
|
|
17
|
+
import { buildCostLedger, renderAdaptationSection } from './reporting.ts';
|
|
11
18
|
|
|
12
19
|
function isStateFile(f: string): boolean {
|
|
13
20
|
// state.json (solo) or <member>.json (team) — never continue*.json
|
|
@@ -26,6 +33,52 @@ function primaryState(dir: string, files: string[]): Record<string, unknown> | n
|
|
|
26
33
|
}
|
|
27
34
|
}
|
|
28
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
|
+
|
|
29
82
|
/** Files the mission changed, base..branch. Empty on any git failure — routing is best-effort. */
|
|
30
83
|
function changedFiles(projectDir: string, state: Record<string, unknown> | null): string[] {
|
|
31
84
|
const base = typeof state?.base_sha === 'string' ? state.base_sha : '';
|
|
@@ -96,6 +149,11 @@ export function resetMission(projectDir: string, keepLogs: boolean, force?: bool
|
|
|
96
149
|
if (existsSync(join(root, 'lessons.md'))) kept.push('lessons.md');
|
|
97
150
|
else if (existsSync(join(root, join('logs', 'lessons.md')))) kept.push(join('logs', 'lessons.md'));
|
|
98
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
|
+
}
|
|
99
157
|
for (const f of ['config', 'manifest.json', 'backup']) {
|
|
100
158
|
if (existsSync(join(root, f))) kept.push(f);
|
|
101
159
|
}
|
|
@@ -117,10 +175,40 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
117
175
|
|
|
118
176
|
// Closure integrity gate: the trail validates itself before it
|
|
119
177
|
// folds. Dangling links, secrets, or missing evidence fail the archive.
|
|
178
|
+
// Card-number shapes are warn-only — they do not block archive.
|
|
120
179
|
if (!dryRun) {
|
|
121
180
|
const issues = checkTrail(dir, projectDir);
|
|
122
|
-
|
|
123
|
-
|
|
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 */ }
|
|
192
|
+
}
|
|
193
|
+
// Artifact gate (roadmap v0.8 item 4): Lane 2+ missions must carry
|
|
194
|
+
// plan.md + flows/* evidence — a mission without its trail does not fold.
|
|
195
|
+
const artifacts = checkMissionArtifacts(dir);
|
|
196
|
+
if (!artifacts.ok) {
|
|
197
|
+
throw new Error(`archive artifact gate failed — missing: ${artifacts.missing.join(', ')} (lane ${artifacts.lane}). Write the evidence trail before archiving.`);
|
|
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
|
|
124
212
|
}
|
|
125
213
|
}
|
|
126
214
|
|
|
@@ -142,18 +230,51 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
142
230
|
const chars = measureContextChars(dir);
|
|
143
231
|
const budget = readBudgetConfig(projectDir);
|
|
144
232
|
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
233
|
const est = typeof state.tokens_est === 'number' ? state.tokens_est : 0;
|
|
149
234
|
const src = typeof state.tokens_source === 'string' ? state.tokens_source : 'computed';
|
|
150
235
|
const lane = typeof state.lane === 'string' ? state.lane : 'unknown';
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
236
|
+
// C2: `status` gates on the LANE token budget (what savepoint.sh enforces),
|
|
237
|
+
// never on the context char budget. `contextStatus` below is its own gate.
|
|
238
|
+
const laneBudget = budgetForLane(lane);
|
|
239
|
+
// Q1/Q2: the normalized envelope computes planned/used/remaining/pct/status
|
|
240
|
+
// on the lane token budget — ONE computation (Q2), reused at render and for
|
|
241
|
+
// the closure event. `effBudget` stays only for the readable delta display.
|
|
242
|
+
const env = costEnvelope({ lane, budget: laneBudget, tokens_est: est });
|
|
243
|
+
const effBudget = budget || laneBudget; // display-only delta basis — never for status (C2)
|
|
155
244
|
const delta = effBudget ? (est <= effBudget ? `${(effBudget - est).toLocaleString()} under` : `${(est - effBudget).toLocaleString()} over`) : 'no budget configured';
|
|
156
245
|
const srcLabel = src === 'reported' ? 'provider-reported' : 'estimator';
|
|
246
|
+
const statusLabel = env.status.toUpperCase(); // derived from the single computation, not recomputed
|
|
247
|
+
// context status on context_budget_chars — separate gate, never token `est` (C2)
|
|
248
|
+
const ctxStatus = contextStatus(budget, chars);
|
|
249
|
+
// context-efficiency metrics from the evidence registry (reads), else all-zero
|
|
250
|
+
// with a note — a zero row must not be misread as "efficient" (risk row).
|
|
251
|
+
const registry = loadRegistry(dir);
|
|
252
|
+
const reads_total = registry.reduce((s, e) => s + e.reads, 0);
|
|
253
|
+
const repeated_reads = registry.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
|
|
254
|
+
// M1: honest char accounting — each registered entry carries the content
|
|
255
|
+
// length it holds (chars). total_chars = chars actually loaded (each entry
|
|
256
|
+
// × its reads); unique_chars = distinct payload bytes. computeContextMetrics
|
|
257
|
+
// derives duplicate_chars = total − unique (bytes re-read) and
|
|
258
|
+
// read_avoidance_chars = same (bytes not reloaded by reuse). When a
|
|
259
|
+
// registry exists but carries no char payloads (legacy/absent field), the
|
|
260
|
+
// char fields render as n/a — never fabricated 0 — so reuse_rate > 0 never
|
|
261
|
+
// sits beside a false "read_avoidance_chars: 0".
|
|
262
|
+
const unique_chars = registry.reduce((s, e) => s + (e.chars ?? 0), 0);
|
|
263
|
+
const total_chars = registry.reduce((s, e) => s + (e.chars ?? 0) * e.reads, 0);
|
|
264
|
+
const charTracked = registry.length > 0 && total_chars > 0;
|
|
265
|
+
const metrics = registry.length
|
|
266
|
+
? computeContextMetrics({
|
|
267
|
+
files_loaded: registry.length,
|
|
268
|
+
reads_total,
|
|
269
|
+
reads_reused: repeated_reads,
|
|
270
|
+
unique_chars,
|
|
271
|
+
total_chars,
|
|
272
|
+
repeated_reads,
|
|
273
|
+
})
|
|
274
|
+
: { files_loaded: 0, repeated_reads: 0, duplicate_chars: 0, reuse_rate: 0, read_avoidance_chars: 0 };
|
|
275
|
+
const ctxNote = registry.length
|
|
276
|
+
? (charTracked ? '' : ' (char data not tracked)')
|
|
277
|
+
: ' (no registry — reads not tracked)';
|
|
157
278
|
// provider-reported rollup when any stage reported
|
|
158
279
|
let reportedTotal = 0;
|
|
159
280
|
let hasReported = false;
|
|
@@ -176,14 +297,87 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
176
297
|
'| Metric | Value |',
|
|
177
298
|
'|--------|-------|',
|
|
178
299
|
`| **Tokens used** | ${est.toLocaleString()} (${srcLabel}) |`,
|
|
179
|
-
`| **Lane** | ${lane} (budget ${effBudget ? effBudget.toLocaleString() : '—'} · warn ${effBudget ?
|
|
180
|
-
`| **Budget status** | ${effBudget ? `${pct}% of budget · ${delta} · ${
|
|
300
|
+
`| **Lane** | ${lane} (budget ${effBudget ? effBudget.toLocaleString() : '—'} · warn ${effBudget ? env.warn_at.toLocaleString() : '—'} · stop ${effBudget ? env.stop_at.toLocaleString() : '—'}) |`,
|
|
301
|
+
`| **Budget status** | ${effBudget ? `${env.pct}% of budget · ${delta} · ${statusLabel}` : 'no lane budget'} |`,
|
|
181
302
|
`| **Context footprint** | ${chars.toLocaleString()} chars${budget ? ` (budget ${budget.toLocaleString()})` : ' (no context budget configured)'} |`,
|
|
303
|
+
`| **Context budget status** | ${ctxStatus.toUpperCase()}${budget ? ` (budget ${budget.toLocaleString()})` : ' (no context budget configured)'} |`,
|
|
304
|
+
`| **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
305
|
].join('\n');
|
|
183
306
|
if (hasReported) {
|
|
184
307
|
costSection += `\n| **Provider total** | ${reportedTotal.toLocaleString()} (provider-reported — sum of reported stages) |`;
|
|
185
308
|
}
|
|
309
|
+
// Phase 8 Reporting — ledger/avoided/efficiency/trail rows (§39/§43)
|
|
310
|
+
try {
|
|
311
|
+
const ledger = buildCostLedger({ missionDir: dir, envelope: env });
|
|
312
|
+
costSection += `\n| Budget | ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned}) |`;
|
|
313
|
+
costSection += `\n| Context | ${chars.toLocaleString()} chars, reuse ${ledger.efficiency.reuse_rate} |`;
|
|
314
|
+
costSection += `\n| Avoided | ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ${ledger.avoided.tokens_avoided_est} tokens est |`;
|
|
315
|
+
costSection += `\n| Efficiency | reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}% |`;
|
|
316
|
+
costSection += `\n| Trail | ${ledger.trail.length} decisions |`;
|
|
317
|
+
if (ledger.trail.length) {
|
|
318
|
+
const show = ledger.trail.slice(0, 5);
|
|
319
|
+
for (const t of show) costSection += `\n- ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ''}`;
|
|
320
|
+
if (ledger.trail.length > 5) costSection += `\n… ${ledger.trail.length - 5} more`;
|
|
321
|
+
}
|
|
322
|
+
} catch { /* ledger best-effort — trail parse failure never blocks archive */ }
|
|
186
323
|
costSection += '\n';
|
|
324
|
+
// Phase E — adaptation summary from the posture decision trail
|
|
325
|
+
try {
|
|
326
|
+
costSection += renderAdaptationSection(dir);
|
|
327
|
+
} catch { /* best-effort */ }
|
|
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
|
+
|
|
361
|
+
// Cost Governor: record the closure cost event — the mission's final
|
|
362
|
+
// cost snapshot, folded into report.md with the rest of the trail.
|
|
363
|
+
// (Phase 1 — native cost governor; pure append, never rewrites state.)
|
|
364
|
+
appendCostEvent(dir, {
|
|
365
|
+
kind: 'closure',
|
|
366
|
+
mission,
|
|
367
|
+
tokens_est: est,
|
|
368
|
+
budget: laneBudget,
|
|
369
|
+
status: env.status, // Q2: the single lane-token-budget computation
|
|
370
|
+
context_chars: chars,
|
|
371
|
+
context_status: ctxStatus,
|
|
372
|
+
context_metrics: metrics,
|
|
373
|
+
});
|
|
374
|
+
// M2: the closure event (with context_status possibly 'over') is recorded
|
|
375
|
+
// BEFORE the hard gate throws — an over-budget closure still leaves a
|
|
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.
|
|
378
|
+
if (budget && chars > budget) {
|
|
379
|
+
throw new Error(`closure context budget failed — ${footprintLine}. Trim the trail or raise context_budget_chars.`);
|
|
380
|
+
}
|
|
187
381
|
}
|
|
188
382
|
|
|
189
383
|
// Fold order: narrative artifacts first, wave evidence last (chronological).
|
|
@@ -194,13 +388,30 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
194
388
|
}
|
|
195
389
|
// Flow artifacts: flows/ is the current layout; a legacy mission that still
|
|
196
390
|
// keeps waves/ folds from there so an upgrade never strands a trail.
|
|
391
|
+
// 07-pr-verdict.md SURVIVES archive as a standalone `pr-verdict.md` at the
|
|
392
|
+
// mission root — it is the PR material handed to the user, so it must not
|
|
393
|
+
// fold away into report.md.
|
|
394
|
+
const PR_VERDICT = 'pr-verdict.md';
|
|
395
|
+
const PR_VERDICT_SRC = join('flows', '07-pr-verdict.md');
|
|
197
396
|
const flowsDir = join(dir, 'flows');
|
|
198
397
|
const legacyWavesDir = join(dir, 'waves');
|
|
199
398
|
const artDir = existsSync(flowsDir) ? flowsDir : existsSync(legacyWavesDir) ? legacyWavesDir : flowsDir;
|
|
200
399
|
const artRel = artDir === legacyWavesDir ? 'waves' : 'flows';
|
|
201
400
|
if (existsSync(artDir)) {
|
|
202
|
-
for (const f of readdirSync(artDir).sort())
|
|
401
|
+
for (const f of readdirSync(artDir).sort()) {
|
|
402
|
+
if (artRel === 'flows' && f === '07-pr-verdict.md') continue; // survives as pr-verdict.md
|
|
403
|
+
fold.push(join(artRel, f));
|
|
404
|
+
}
|
|
203
405
|
}
|
|
406
|
+
// Cost events ledger — appended by the closure event above (or a prior
|
|
407
|
+
// savepoint in a later phase); folds like any other trail artifact so
|
|
408
|
+
// nothing survives loose after archive.
|
|
409
|
+
if (existsSync(join(dir, 'cost-events.jsonl'))) fold.push('cost-events.jsonl');
|
|
410
|
+
// H1: the context registry is the same class of artifact as cost-events.jsonl
|
|
411
|
+
// (append-only JSONL ledger) — fold it into report.md and remove it so it does
|
|
412
|
+
// NOT survive loose after archive (survival parity; the fold loop below also
|
|
413
|
+
// removes every folded file).
|
|
414
|
+
if (existsSync(join(dir, 'context-registry.jsonl'))) fold.push('context-registry.jsonl');
|
|
204
415
|
|
|
205
416
|
// The report survives: an existing report.md wins; otherwise the closure
|
|
206
417
|
// wave seeds it; otherwise it starts empty.
|
|
@@ -211,6 +422,14 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
211
422
|
|
|
212
423
|
if (!dryRun) {
|
|
213
424
|
mkdirSync(dir, { recursive: true });
|
|
425
|
+
// PR verdict survives archive as a standalone file at the mission root —
|
|
426
|
+
// it is the PR material handed to the user and must not fold away.
|
|
427
|
+
const prVerdictSrc = join(dir, PR_VERDICT_SRC);
|
|
428
|
+
if (existsSync(prVerdictSrc)) {
|
|
429
|
+
const prVerdictPath = join(dir, PR_VERDICT);
|
|
430
|
+
writeFileSync(prVerdictPath, readFileSync(prVerdictSrc, 'utf8'));
|
|
431
|
+
kept.push(join('missions', mission, PR_VERDICT));
|
|
432
|
+
}
|
|
214
433
|
if (fold.length) {
|
|
215
434
|
const sections = fold.map((f) => {
|
|
216
435
|
const body = readFileSync(join(dir, f), 'utf8').trim();
|
|
@@ -235,6 +454,11 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
235
454
|
rmSync(join(dir, f), { force: true, recursive: true });
|
|
236
455
|
removed.push(join('missions', mission, f));
|
|
237
456
|
}
|
|
457
|
+
// the pr-verdict source was copied to the root — remove the flows/ copy
|
|
458
|
+
if (existsSync(prVerdictSrc)) {
|
|
459
|
+
rmSync(join(dir, PR_VERDICT_SRC), { force: true });
|
|
460
|
+
removed.push(join('missions', mission, PR_VERDICT_SRC));
|
|
461
|
+
}
|
|
238
462
|
// session state dies with the mission
|
|
239
463
|
for (const f of files.filter((f) => f.endsWith('.json'))) rmSync(join(dir, f), { force: true });
|
|
240
464
|
// flows/ may now be empty — remove the folder
|
|
@@ -254,17 +478,23 @@ export function archiveMission(projectDir: string, mission: string, opts: { dryR
|
|
|
254
478
|
});
|
|
255
479
|
if (rb) kept.push(join('missions', mission, rb.file));
|
|
256
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;
|
|
257
486
|
writeProvenance(projectDir, dir, {
|
|
258
487
|
mission,
|
|
259
488
|
actor: typeof state.actor === 'string' ? state.actor : '',
|
|
260
489
|
lane: typeof state.lane === 'string' ? state.lane : '',
|
|
261
490
|
mode: typeof state.mode === 'string' ? state.mode : '',
|
|
262
491
|
branch: state.branch,
|
|
263
|
-
tasks_done
|
|
264
|
-
tasks_total
|
|
492
|
+
tasks_done,
|
|
493
|
+
tasks_total,
|
|
265
494
|
evidence: Array.isArray(state.evidence) ? (state.evidence as string[]) : [],
|
|
266
495
|
models: stageModels,
|
|
267
|
-
|
|
496
|
+
base_sha: baseShaForNote,
|
|
497
|
+
} as never, baseShaForNote);
|
|
268
498
|
kept.push(join('missions', mission, 'provenance.md'));
|
|
269
499
|
} catch { /* provenance is additive; archive proceeds */ }
|
|
270
500
|
}
|