@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.
Files changed (87) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/.opencode/mugiwara-helpers.mjs +2 -2
  7. package/README.md +196 -330
  8. package/content/agents/brook-healing.md +1 -1
  9. package/content/agents/franky-gates.md +1 -1
  10. package/content/agents/luffy-orchestrator.md +2 -2
  11. package/content/agents/memory-keeper.md +5 -0
  12. package/content/agents/usopp-brainstorm.md +3 -2
  13. package/content/agents/zoro-execution.md +4 -3
  14. package/content/skills/mugiwara-backend/SKILL.md +52 -43
  15. package/content/skills/mugiwara-brainstorm/SKILL.md +5 -3
  16. package/content/skills/mugiwara-checkpoint/SKILL.md +21 -8
  17. package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
  18. package/content/skills/mugiwara-execution/SKILL.md +34 -33
  19. package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
  20. package/content/skills/mugiwara-execution/references/execution-phase-flows.md +18 -0
  21. package/content/skills/mugiwara-frontend/SKILL.md +44 -44
  22. package/content/skills/mugiwara-gates/SKILL.md +28 -16
  23. package/content/skills/mugiwara-healing/SKILL.md +30 -25
  24. package/content/skills/mugiwara-lessons/SKILL.md +3 -0
  25. package/content/skills/mugiwara-orchestration/SKILL.md +10 -9
  26. package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
  27. package/content/skills/mugiwara-planning/SKILL.md +28 -14
  28. package/content/skills/mugiwara-planning/references/large-campaign-subplan.md +41 -0
  29. package/content/skills/mugiwara-planning/references/plan-template.md +22 -0
  30. package/content/skills/mugiwara-quality/SKILL.md +8 -13
  31. package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
  32. package/content/skills/mugiwara-resume/SKILL.md +3 -9
  33. package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
  34. package/content/skills/mugiwara-review/SKILL.md +17 -24
  35. package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
  36. package/content/skills/mugiwara-security/SKILL.md +47 -35
  37. package/content/skills/mugiwara-ship/SKILL.md +2 -0
  38. package/content/skills/mugiwara-workflow/SKILL.md +13 -13
  39. package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
  40. package/content/skills/mugiwara-workflow/references/workspace-layout.md +6 -3
  41. package/dist/mugiwara.js +1802 -316
  42. package/gemini-extension.json +1 -1
  43. package/hooks/mugiwara-mode-tracker.js +24 -4
  44. package/hooks/mugiwara-mode-tracker.ts +36 -7
  45. package/hooks/pipeline-guard.js +1 -1
  46. package/hooks/pipeline-guard.ts +2 -1
  47. package/hooks/session-start.js +6 -1
  48. package/hooks/session-start.ts +8 -1
  49. package/package.json +2 -2
  50. package/plugin.json +1 -1
  51. package/references/cost-governor.md +104 -0
  52. package/references/multi-actor.md +21 -0
  53. package/references/posture-routing.md +31 -0
  54. package/references/wave-banners.md +1 -2
  55. package/scripts/benchmark-governor.ts +516 -0
  56. package/scripts/benchmark-thresholds.json +47 -0
  57. package/scripts/check-doc-links.ts +8 -2
  58. package/scripts/gate-selftest.ts +104 -21
  59. package/scripts/lib/lane-base.sh +4 -4
  60. package/scripts/retrieval-eval.ts +9 -3
  61. package/scripts/savepoint.sh +41 -2
  62. package/scripts/validate-content.ts +82 -3
  63. package/scripts/verify-install.ts +20 -0
  64. package/scripts/write-metrics.ts +73 -0
  65. package/src/adaptive-budget.ts +178 -0
  66. package/src/args.ts +3 -2
  67. package/src/budget.ts +18 -16
  68. package/src/check-artifacts.ts +45 -0
  69. package/src/cli.ts +221 -8
  70. package/src/cognition.ts +234 -0
  71. package/src/config.ts +113 -0
  72. package/src/context.ts +72 -0
  73. package/src/continue.ts +29 -0
  74. package/src/cost.ts +189 -0
  75. package/src/evidence.ts +160 -0
  76. package/src/installer.ts +2 -16
  77. package/src/integrity.ts +65 -16
  78. package/src/investigation.ts +72 -0
  79. package/src/mission.ts +246 -16
  80. package/src/policy.ts +355 -2
  81. package/src/posture.ts +86 -0
  82. package/src/provenance.ts +29 -9
  83. package/src/reporting.ts +225 -0
  84. package/src/scope.ts +321 -0
  85. package/src/sign.ts +234 -18
  86. package/src/slop.ts +306 -0
  87. 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
- if (issues.length) {
123
- throw new Error(`closure integrity gate failed — fix these before archiving:\n${formatIssues(issues)}`);
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
- // lane budgets for readable delta (from lane-base.sh, mirrored here for display only)
152
- const laneBudget = lane === 'lean' ? 12000 : lane === 'standard' ? 25000 : lane === 'full' ? 50000 : lane === 'spike' ? 3000 : 0;
153
- const effBudget = budget || laneBudget;
154
- const pct = effBudget ? Math.round((est / effBudget) * 100) : 0;
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 ? Math.round(effBudget * 1.5).toLocaleString() : '—'} · stop ${effBudget ? (effBudget * 3).toLocaleString() : '—'}) |`,
180
- `| **Budget status** | ${effBudget ? `${pct}% of budget · ${delta} · ${est >= effBudget * 3 ? 'STOP' : est >= effBudget * 1.5 ? 'WARN' : 'OK'}` : 'no lane budget'} |`,
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()) fold.push(join(artRel, f));
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: Number(state.tasks_done) || 0,
264
- tasks_total: Number(state.tasks_total) || 0,
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
  }