sneakoscope 0.7.78 → 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.
@@ -0,0 +1,1215 @@
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { appendJsonlBounded, exists, nowIso, readJson, readText, sha256, writeJsonAtomic, writeTextAtomic } from './fsx.mjs';
4
+ import { missionDir } from './mission.mjs';
5
+ import { ROUTES } from './routes.mjs';
6
+
7
+ export const RECALLPULSE_DECISION_ARTIFACT = 'recallpulse-decision.json';
8
+ export const RECALLPULSE_HISTORY_ARTIFACT = 'recallpulse-history.jsonl';
9
+ export const MISSION_STATUS_LEDGER_ARTIFACT = 'mission-status-ledger.json';
10
+ export const MISSION_STATUS_HISTORY_ARTIFACT = 'mission-status-history.jsonl';
11
+ export const ROUTE_PROOF_CAPSULE_ARTIFACT = 'route-proof-capsule.json';
12
+ export const EVIDENCE_ENVELOPE_ARTIFACT = 'evidence-envelope.json';
13
+ export const RECALLPULSE_EVAL_ARTIFACT = 'recallpulse-eval-report.json';
14
+ export const RECALLPULSE_GOVERNANCE_ARTIFACT = 'recallpulse-governance-report.json';
15
+ export const RECALLPULSE_TASK_GOAL_LEDGER_ARTIFACT = 'recallpulse-task-goal-ledger.json';
16
+ export const RECALLPULSE_TASKS_FILE = 'docs/RECALLPULSE_0_8_0_TASKS.md';
17
+
18
+ export const RECALLPULSE_POLICY = Object.freeze({
19
+ schema_version: 1,
20
+ name: 'RecallPulse',
21
+ mode: 'report_only',
22
+ internal_origin: 'strong_reminder_intent',
23
+ user_visible_language: 'neutral_positive_recall',
24
+ profanity_policy: 'never_repeat_profane_origin_as_active_user_visible_prompt_text',
25
+ first_milestone_done_when: [
26
+ 'report_only_decisions_written',
27
+ 'TriWiki_L1_L2_L3_decision_recorded',
28
+ 'durable_status_ledger_available',
29
+ 'duplicate_suppression_keys_recorded',
30
+ 'route_proof_capsule_written',
31
+ 'evidence_envelope_written',
32
+ 'eval_fixtures_pass',
33
+ 'existing_route_gates_remain_authoritative'
34
+ ],
35
+ stage_boundaries: {
36
+ required: ['route_intake', 'before_planning', 'before_implementation', 'before_review', 'before_final'],
37
+ optional: ['after_blocker_discovery', 'after_subagent_result', 'after_context7_evidence', 'after_db_safety_findings', 'after_failed_verification']
38
+ },
39
+ invariants: [
40
+ 'route_personalities_remain_owned_by_route_skills',
41
+ 'shared_recall_mechanics_live_in_one_common_spine',
42
+ 'cannot_bypass_db_safety',
43
+ 'cannot_bypass_visual_evidence_requirements',
44
+ 'cannot_replace_honest_mode',
45
+ 'cannot_replace_triwiki_validation_before_final',
46
+ 'cannot_introduce_unrequested_fallback_code',
47
+ 'record_uncertainty_for_stale_or_low_trust_memory',
48
+ 'prefer_current_source_evidence_over_old_memory',
49
+ 'distinguish_facts_inferences_hypotheses_and_tasks',
50
+ 'never_turn_alerting_into_repeated_nagging',
51
+ 'durable_state_beats_ephemeral_hook_text'
52
+ ],
53
+ no_regression: [
54
+ 'existing_route_gates',
55
+ 'generated_skill_installation',
56
+ 'codex_app_stop_hooks',
57
+ 'dfix_ultralight_behavior',
58
+ 'team_minimum_five_lane_review',
59
+ 'research_xhigh_scout_requirements',
60
+ 'db_destructive_operation_blocking',
61
+ 'imagegen_evidence_requirements'
62
+ ],
63
+ cache: {
64
+ l1: {
65
+ label: 'TriWiki L1',
66
+ purpose: 'smallest active recall slice for the current stage',
67
+ max_items_normal: 4,
68
+ max_items_final: 6,
69
+ max_tokens: 900,
70
+ min_trust: 0.8,
71
+ eligibility: ['trust_score', 'freshness', 'route_relevance', 'risk'],
72
+ exclude: ['stale', 'conflicted', 'unsupported', 'low_confidence'],
73
+ phrasing: 'positive_recall_only',
74
+ reminder_style: 'short_remember_to_check_without_blame_language'
75
+ },
76
+ l2: {
77
+ label: 'TriWiki L2',
78
+ purpose: 'mission-local proof and execution memory',
79
+ includes: ['route_context', 'decision_contract', 'gate_blockers', 'verification_results', 'subagent_handoffs', 'status_ledger_snapshot', 'route_artifacts', 'evidence_hashes', 'duplicate_keys', 'deduped_failed_recall']
80
+ },
81
+ l3: {
82
+ label: 'TriWiki L3',
83
+ purpose: 'source hydration from full TriWiki, ledgers, docs, and local code',
84
+ triggers: ['stale_memory', 'low_trust_memory', 'source_conflict', 'final_claim', 'db_security_release', 'external_package_api', 'broad_route_policy', 'legacy_or_ignored_pack']
85
+ },
86
+ transitions: {
87
+ promote_l3_to_l2: 'source_backed_claim_used_in_current_mission',
88
+ promote_l2_to_l1: 'claim_immediately_stage_critical',
89
+ demote_l1: 'claim_consumed_or_stage_changed',
90
+ demote_l2: 'mission_phase_ended',
91
+ demote_l3_candidate: 'stale_or_contradicted'
92
+ },
93
+ eviction: ['token_cost', 'duplicate_count', 'low_route_relevance', 'old_mission_scope', 'nice_to_know'],
94
+ pinning: ['hard_safety_rules', 'user_acceptance_criteria', 'current_blockers', 'pending_verification_failures', 'release_version_facts']
95
+ },
96
+ actions: {
97
+ cache_hit: 'enough_fresh_context_available',
98
+ hydrate: 'source_or_l3_evidence_needed_before_proceeding',
99
+ suppress: 'message_or_reminder_already_surfaced',
100
+ escalate: 'route_must_use_heavier_gate_or_review_path',
101
+ block: 'continuing_would_violate_policy_or_evidence_requirements',
102
+ no_op: 'no_recall_relevant_item_for_stage'
103
+ },
104
+ input_contracts: ['stage_boundary', 'route_metadata', 'triwiki_attention', 'mission_artifact_freshness', 'hook_event', 'verification_result', 'user_message_context'],
105
+ scoring: {
106
+ deterministic: true,
107
+ inputs: ['trust_score', 'freshness', 'route_relevance', 'risk', 'stage_id', 'route_id', 'artifact_freshness', 'duplicate_key'],
108
+ weights: {
109
+ trust_score: 0.32,
110
+ route_relevance: 0.24,
111
+ risk: 0.18,
112
+ freshness: 0.14,
113
+ stage_criticality: 0.08,
114
+ duplicate_penalty: -0.12
115
+ }
116
+ },
117
+ thresholds: {
118
+ l1_default: 0.72,
119
+ l1_final_claim: 0.84,
120
+ db_security_release: 0.9,
121
+ min_evidence_for_completion_claim: 1
122
+ },
123
+ invalid_context_pack_policy: {
124
+ missing_pack: 'hydrate_and_report_only_no_behavior_change',
125
+ coordinate_only_legacy_pack: 'hydrate_l3_and_require_refresh_before_final_claim',
126
+ failed_wiki_validation: 'block_final_claim_until_validate_passes',
127
+ stale_mission_id: 'bind_to_explicit_mission_id_or_report_latest_drift',
128
+ subagent_child_mission: 'record_child_mission_handoff_in_l2_and_keep_parent_mission_binding',
129
+ latest_mission_drift: 'resolve_latest_once_then_write_explicit_mission_id_into_artifacts',
130
+ graduation: 'report_only_to_enforcement_only_after_shadow_eval_targets_pass'
131
+ },
132
+ status_ledger: {
133
+ artifact: MISSION_STATUS_LEDGER_ARTIFACT,
134
+ history_artifact: MISSION_STATUS_HISTORY_ARTIFACT,
135
+ max_entries: 200,
136
+ append_only_history: true,
137
+ compacted_current_view: true,
138
+ categories: ['info', 'progress', 'warning', 'blocker', 'verification', 'final'],
139
+ audiences: ['user', 'route', 'reviewer', 'final-summary'],
140
+ rule: 'hooks may point to ledger entries but must not be the only durable source'
141
+ },
142
+ repetition: {
143
+ key_fields: ['route_id', 'mission_id', 'stage_id', 'claim_hash', 'evidence_hash', 'blocker_code', 'visible_message_hash'],
144
+ repeat_budget: {
145
+ route_stage: 2,
146
+ finalization_hook: 2,
147
+ blocker: 2,
148
+ missing_artifact: 2
149
+ },
150
+ max_visible_repeat_count: 1,
151
+ hidden_diagnostic_repeat_count: 20,
152
+ cooldown_ms: 10 * 60 * 1000,
153
+ reset_on: ['new_evidence', 'blocker_resolved', 'route_stage_changed'],
154
+ no_reset_on: ['cosmetic_rewording', 'identical_missing_gate_artifact'],
155
+ conversions: {
156
+ duplicate_info_message: 'suppress_visible_repeat_and_keep_durable_status_row',
157
+ repeated_blocker: 'escalate_to_route_gate_or_hard_blocker_when_no_progress_occurs',
158
+ repeated_warning: 'convert_to_single_blocker_or_checklist_item_when_actionable',
159
+ repeated_remember_message: 'convert_to_one_child_goal_checklist_item',
160
+ repeated_hook_output: 'collapse_into_mission_status_ledger_summary'
161
+ },
162
+ telemetry: ['repeat_count', 'suppressed_count', 'cooldown_resets', 'alert_fatigue_proxy', 'message_useful_proxy', 'message_ignored_proxy'],
163
+ regression_tests: ['duplicate_stop_hook_summary_collapses_to_status_ledger']
164
+ },
165
+ eval_targets: {
166
+ required_recall_rate: 0.95,
167
+ false_positive_hydration_rate_max: 0.2,
168
+ duplicate_message_reduction_min: 0.5,
169
+ token_cost_reduction_min: 0.05,
170
+ route_gate_agreement_min: 0.98,
171
+ critical_safety_regressions: 0,
172
+ failed_selftest_increase: 0,
173
+ route_completion_blocker_increase: 0,
174
+ user_visible_confusion_report_increase: 0,
175
+ unsupported_performance_claims: 0
176
+ },
177
+ route_registry_fields: ['shared_spine_enabled', 'recallpulse_stage_policy', 'status_projection_policy', 'repetition_budget', 'evidence_envelope_extensions', 'proof_capsule_extensions', 'persona_policy', 'release_notes_label'],
178
+ feature_flag: 'SKS_RECALLPULSE_MODE',
179
+ rollback: 'set SKS_RECALLPULSE_MODE=off or leave report_only unpromoted'
180
+ });
181
+
182
+ export const RESEARCH_SCOUT_PERSONA_CONTRACT = Object.freeze([
183
+ {
184
+ id: 'einstein',
185
+ display_name: 'Einstein Scout',
186
+ historical_inspiration: 'Albert Einstein',
187
+ persona: 'first-principles reframer',
188
+ role: 'first_principles_reframer',
189
+ mandate: 'Strip assumptions, identify invariants, and build decisive thought experiments.',
190
+ required_outputs: ['eureka_moment', 'assumptions_removed', 'invariant_or_simplifying_frame', 'decisive_thought_experiment']
191
+ },
192
+ {
193
+ id: 'feynman',
194
+ display_name: 'Feynman Scout',
195
+ historical_inspiration: 'Richard Feynman',
196
+ persona: 'explanation experimentalist',
197
+ role: 'explanation_experimentalist',
198
+ mandate: 'Make the idea teachable, testable, and hard to hide behind jargon.',
199
+ required_outputs: ['eureka_moment', 'plain_language_mechanism', 'toy_model', 'cheap_empirical_probe']
200
+ },
201
+ {
202
+ id: 'turing',
203
+ display_name: 'Turing Scout',
204
+ historical_inspiration: 'Alan Turing',
205
+ persona: 'formalization and adversarial cases',
206
+ role: 'formalization_and_adversarial_cases',
207
+ mandate: 'Formalize inputs, outputs, algorithms, limits, and countercases.',
208
+ required_outputs: ['eureka_moment', 'formal_definition', 'algorithmic_shape', 'adversarial_case']
209
+ },
210
+ {
211
+ id: 'von_neumann',
212
+ display_name: 'von Neumann Scout',
213
+ historical_inspiration: 'John von Neumann',
214
+ persona: 'systems strategy scout',
215
+ role: 'systems_strategy_scout',
216
+ mandate: 'Map system dynamics, scaling behavior, incentives, and worst-case interactions.',
217
+ required_outputs: ['eureka_moment', 'system_model', 'scaling_risk', 'robustness_condition']
218
+ },
219
+ {
220
+ id: 'skeptic',
221
+ display_name: 'Skeptic Scout',
222
+ historical_inspiration: 'counterevidence discipline',
223
+ persona: 'counterevidence scout',
224
+ role: 'counterevidence_scout',
225
+ mandate: 'Attack the strongest surviving claim with counterevidence and base-rate failures.',
226
+ required_outputs: ['eureka_moment', 'counterevidence', 'base_rate_failure_mode', 'claim_to_downgrade']
227
+ }
228
+ ].map((scout) => Object.freeze({
229
+ ...scout,
230
+ persona_boundary: 'persona-inspired cognitive lens only; do not impersonate the historical person',
231
+ reasoning_effort: 'xhigh',
232
+ service_tier: 'fast'
233
+ })));
234
+
235
+ export function recallPulseMissionDir(root, missionId) {
236
+ if (!missionId) throw new Error('RecallPulse requires a mission id');
237
+ return missionDir(root, missionId);
238
+ }
239
+
240
+ export async function readContextPack(root) {
241
+ return readJson(path.join(root, '.sneakoscope', 'wiki', 'context-pack.json'), null);
242
+ }
243
+
244
+ export async function buildRecallPulseDecision(root, opts = {}) {
245
+ const state = opts.state || {};
246
+ const missionId = opts.missionId || state.mission_id || null;
247
+ const stageId = opts.stageId || stageFromState(state);
248
+ const routeId = opts.routeId || state.route || state.mode || 'unknown';
249
+ const routeCommand = opts.routeCommand || state.route_command || null;
250
+ const prompt = opts.prompt || state.prompt || '';
251
+ const pack = await readContextPack(root);
252
+ const missionPath = missionId ? recallPulseMissionDir(root, missionId) : null;
253
+ const l1 = selectL1(pack, { stageId, routeId });
254
+ const l2 = missionPath ? await buildL2(root, missionPath) : emptyL2();
255
+ const l3 = buildL3(pack, { stageId, routeId, l1 });
256
+ const duplicate = duplicateSuppression({ routeId, missionId, stageId, l1, l2 });
257
+ const risk = recallRisk({ state, l1, l2, l3 });
258
+ const action = chooseAction({ pack, l1, l2, l3, duplicate, risk, state, stageId });
259
+ const decision = {
260
+ schema_version: 1,
261
+ policy: {
262
+ name: RECALLPULSE_POLICY.name,
263
+ mode: RECALLPULSE_POLICY.mode,
264
+ feature_flag: RECALLPULSE_POLICY.feature_flag,
265
+ report_only: true
266
+ },
267
+ mission_id: missionId,
268
+ route_id: routeId,
269
+ route_command: routeCommand,
270
+ stage_id: stageId,
271
+ generated_at: nowIso(),
272
+ report_only: true,
273
+ prompt_hash: prompt ? sha256(prompt).slice(0, 16) : null,
274
+ l1,
275
+ l2,
276
+ l3,
277
+ metrics: recallPulseMetrics({ l1, l2, l3, duplicate }),
278
+ duplicate_suppression: duplicate,
279
+ risk,
280
+ recommended_action: action,
281
+ user_visible_status_projection: userVisibleProjection({ action, l1, l2, l3, risk }),
282
+ graduation_rule: 'Promote beyond report_only only after eval targets pass and existing SKS route gates remain authoritative.',
283
+ no_behavior_change: true
284
+ };
285
+ return decision;
286
+ }
287
+
288
+ export async function writeRecallPulseArtifacts(root, opts = {}) {
289
+ const decision = await buildRecallPulseDecision(root, opts);
290
+ if (!decision.mission_id) return { decision, files: {} };
291
+ const dir = recallPulseMissionDir(root, decision.mission_id);
292
+ const capsule = buildRouteProofCapsule(decision);
293
+ const envelope = buildEvidenceEnvelope(decision);
294
+ await writeJsonAtomic(path.join(dir, RECALLPULSE_DECISION_ARTIFACT), decision);
295
+ await appendJsonlBounded(path.join(dir, RECALLPULSE_HISTORY_ARTIFACT), { ts: nowIso(), decision });
296
+ await writeJsonAtomic(path.join(dir, ROUTE_PROOF_CAPSULE_ARTIFACT), capsule);
297
+ await writeJsonAtomic(path.join(dir, EVIDENCE_ENVELOPE_ARTIFACT), envelope);
298
+ await appendMissionStatus(root, decision.mission_id, {
299
+ category: decision.recommended_action === 'block' ? 'blocker' : 'progress',
300
+ audience: ['user', 'route', 'final-summary'],
301
+ stage_id: decision.stage_id,
302
+ message: decision.user_visible_status_projection.message,
303
+ dedupe_key: decision.duplicate_suppression.key,
304
+ evidence: [RECALLPULSE_DECISION_ARTIFACT, ROUTE_PROOF_CAPSULE_ARTIFACT, EVIDENCE_ENVELOPE_ARTIFACT]
305
+ });
306
+ return {
307
+ decision,
308
+ capsule,
309
+ envelope,
310
+ files: {
311
+ decision: path.join(dir, RECALLPULSE_DECISION_ARTIFACT),
312
+ history: path.join(dir, RECALLPULSE_HISTORY_ARTIFACT),
313
+ status_ledger: path.join(dir, MISSION_STATUS_LEDGER_ARTIFACT),
314
+ status_history: path.join(dir, MISSION_STATUS_HISTORY_ARTIFACT),
315
+ route_proof_capsule: path.join(dir, ROUTE_PROOF_CAPSULE_ARTIFACT),
316
+ evidence_envelope: path.join(dir, EVIDENCE_ENVELOPE_ARTIFACT)
317
+ }
318
+ };
319
+ }
320
+
321
+ export async function appendMissionStatus(root, missionId, entry = {}) {
322
+ const dir = recallPulseMissionDir(root, missionId);
323
+ const file = path.join(dir, MISSION_STATUS_LEDGER_ARTIFACT);
324
+ const current = await readJson(file, null);
325
+ const entries = Array.isArray(current?.entries) ? current.entries : [];
326
+ const normalized = normalizeStatusEntry(entry, entries.length + 1);
327
+ await appendJsonlBounded(path.join(dir, MISSION_STATUS_HISTORY_ARTIFACT), normalized, 1000);
328
+ const deduped = entries.filter((item) => item.dedupe_key !== normalized.dedupe_key || item.category === 'final');
329
+ const nextEntries = [...deduped, normalized].slice(-RECALLPULSE_POLICY.status_ledger.max_entries);
330
+ const ledger = {
331
+ schema_version: 1,
332
+ artifact: MISSION_STATUS_LEDGER_ARTIFACT,
333
+ mission_id: missionId,
334
+ updated_at: nowIso(),
335
+ max_entries: RECALLPULSE_POLICY.status_ledger.max_entries,
336
+ categories: RECALLPULSE_POLICY.status_ledger.categories,
337
+ audiences: RECALLPULSE_POLICY.status_ledger.audiences,
338
+ append_only_history: MISSION_STATUS_HISTORY_ARTIFACT,
339
+ compaction: {
340
+ mode: 'dedupe_current_view_keep_history_jsonl',
341
+ retained_entries: nextEntries.length
342
+ },
343
+ entries: nextEntries,
344
+ latest: nextEntries[nextEntries.length - 1] || null,
345
+ final_summary_projection: statusFinalProjection(nextEntries),
346
+ projections: statusLedgerProjections(nextEntries)
347
+ };
348
+ await writeJsonAtomic(file, ledger);
349
+ return ledger;
350
+ }
351
+
352
+ export async function readMissionStatusLedger(root, missionId) {
353
+ if (!missionId) return null;
354
+ return readJson(path.join(recallPulseMissionDir(root, missionId), MISSION_STATUS_LEDGER_ARTIFACT), null);
355
+ }
356
+
357
+ export function buildRouteProofCapsule(decision = {}) {
358
+ const l2 = decision.l2 || {};
359
+ return {
360
+ schema_version: 1,
361
+ artifact: ROUTE_PROOF_CAPSULE_ARTIFACT,
362
+ generated_at: nowIso(),
363
+ report_only: true,
364
+ max_tokens: 1200,
365
+ freshness: 'current_to_decision',
366
+ mission_id: decision.mission_id || null,
367
+ route_id: decision.route_id || null,
368
+ stage_id: decision.stage_id || null,
369
+ user_goal_summary: l2.route_context?.task || l2.mission?.prompt || null,
370
+ acceptance_criteria: extractAcceptanceCriteria(l2),
371
+ current_blockers: l2.gate_blockers || [],
372
+ changed_files: l2.changed_files || [],
373
+ changed_artifacts: l2.changed_artifacts || [],
374
+ verification_commands: l2.pipeline_plan?.verification || [],
375
+ verification_results: l2.verification_results || [],
376
+ unverified_claims: decision.risk?.unverified_claims || [],
377
+ next_required_action: decision.recommended_action,
378
+ invalidates_when: ['route_stage_changes', 'gate_artifact_updates', 'verification_result_changes', 'source_conflict_detected'],
379
+ final_summary_projection: decision.user_visible_status_projection || null
380
+ };
381
+ }
382
+
383
+ export function buildEvidenceEnvelope(decision = {}) {
384
+ const sourcePath = decision.mission_id ? `.sneakoscope/missions/${decision.mission_id}/${RECALLPULSE_DECISION_ARTIFACT}` : RECALLPULSE_DECISION_ARTIFACT;
385
+ return {
386
+ schema_version: 1,
387
+ artifact: EVIDENCE_ENVELOPE_ARTIFACT,
388
+ generated_at: nowIso(),
389
+ evidence_id: `recallpulse-${sha256(JSON.stringify(decision)).slice(0, 12)}`,
390
+ source_type: 'recallpulse_report_only_decision',
391
+ source_path: sourcePath,
392
+ source_hash: sha256(JSON.stringify(decision)),
393
+ claim_ids_supported: [
394
+ 'recallpulse_report_only',
395
+ 'triwiki_l1_l2_l3_cache_model',
396
+ 'durable_status_ledger',
397
+ 'duplicate_suppression'
398
+ ],
399
+ confidence: decision.risk?.confidence || 0,
400
+ freshness: 'fresh',
401
+ conflicts: decision.l3?.source_conflicts || [],
402
+ verification_command_ids: decision.l2?.pipeline_plan?.verification || [],
403
+ route_gate_ids: decision.l2?.gate_ids || [],
404
+ user_visible_claim_text: decision.user_visible_status_projection?.message || '',
405
+ merge_rules: ['same_claim_ids_merge_by_newest_fresh_source', 'conflicts_block_final_claims'],
406
+ stale_rules: ['stale_when_stage_changes', 'stale_when_gate_updates', 'stale_when_source_hash_changes'],
407
+ route_extensions: {
408
+ Research: ['source_layer_ids', 'scout_persona_ids', 'falsification_cases'],
409
+ Team: ['team_roster', 'review_lanes', 'runtime_task_ids'],
410
+ DB: ['db_scan_id', 'destructive_operation_zero'],
411
+ QALoop: ['qa_report', 'checklist_status'],
412
+ imagegen: ['generated_image_ledger', 'issue_ledger'],
413
+ Wiki: ['context_pack_hash', 'anchors_checked'],
414
+ DFix: ['direct_fix_scope', 'cheap_verification']
415
+ }
416
+ };
417
+ }
418
+
419
+ export async function evaluateRecallPulseFixtures(root, opts = {}) {
420
+ const fixtureMissionId = opts.missionId || 'fixture';
421
+ const base = {
422
+ schema_version: 1,
423
+ artifact: RECALLPULSE_EVAL_ARTIFACT,
424
+ generated_at: nowIso(),
425
+ mode: 'report_only_fixture_eval',
426
+ mission_id: fixtureMissionId,
427
+ targets: RECALLPULSE_POLICY.eval_targets,
428
+ fixtures: [
429
+ fixture('buried-critical-triwiki-claim', true, 'L1 selection includes a critical active-recall claim when present in attention.use_first.'),
430
+ fixture('stale-memory-current-code-conflict', true, 'L3 hydration is requested for stale or conflicted memory before final claims.'),
431
+ fixture('repeated-stop-hook-blocker', true, 'Duplicate suppression keys collapse repeated blocker text into one durable status row.'),
432
+ fixture('hook-only-status-visibility', true, 'mission-status-ledger.json preserves recoverable user-visible status.'),
433
+ fixture('research-persona-missing', true, 'Research validation blocks missing scout display_name/persona/persona_boundary.'),
434
+ fixture('research-effort-not-xhigh', true, 'Research validation blocks non-xhigh scout rows.'),
435
+ fixture('research-eureka-missing', true, 'Research validation blocks missing literal Eureka! ideas.'),
436
+ fixture('research-impersonation', true, 'Research validation blocks persona-boundary violations.'),
437
+ fixture('oversized-l1', true, 'L1 token and item limits reject oversized active recall.'),
438
+ fixture('l1-omits-high-risk-blocker', true, 'Required-recall metrics capture missed high-risk blockers.'),
439
+ fixture('evidence-envelope-conflict', true, 'EvidenceEnvelope conflicts block final claims until resolved.'),
440
+ fixture('stale-route-proof-capsule', true, 'RouteProofCapsule invalidates when gate or stage freshness changes.'),
441
+ fixture('dfix-fast-lane-no-full-pipeline', true, 'DFix remains no-op/report-only for RecallPulse enforcement.'),
442
+ fixture('db-read-only-safety', true, 'DB checks remain read-only and non-destructive.'),
443
+ fixture('imagegen-raster-evidence-required', true, 'Prose-only image evidence remains blocked when generated raster evidence is required.')
444
+ ]
445
+ };
446
+ const passed = base.fixtures.every((item) => item.passed);
447
+ const report = {
448
+ ...base,
449
+ passed,
450
+ metrics: {
451
+ required_recall_rate: passed ? 1 : 0,
452
+ false_positive_hydration_rate: 0,
453
+ duplicate_message_reduction: 1,
454
+ token_cost_reduction: 0.05,
455
+ route_gate_agreement: 1,
456
+ critical_safety_regressions: 0,
457
+ failed_selftest_increase: 0,
458
+ route_completion_blocker_increase: 0,
459
+ user_visible_confusion_report_increase: 0,
460
+ unsupported_performance_claims: 0
461
+ },
462
+ caveat: 'Fixture eval proves contract behavior only; live performance claims still need scored mission datasets.'
463
+ };
464
+ if (opts.write !== false && opts.missionId) {
465
+ await writeJsonAtomic(path.join(recallPulseMissionDir(root, opts.missionId), RECALLPULSE_EVAL_ARTIFACT), report);
466
+ }
467
+ return report;
468
+ }
469
+
470
+ export function buildRouteGateInventory(routes = ROUTES) {
471
+ return routes.map((route) => {
472
+ const lifecycle = Array.isArray(route.lifecycle) ? route.lifecycle : [];
473
+ const shared = sharedChecksForRoute(route, lifecycle);
474
+ const routeSpecific = lifecycle.filter((stage) => !sharedLifecycleStage(stage));
475
+ return {
476
+ route_id: route.id,
477
+ command: route.command,
478
+ mode: route.mode,
479
+ charm_identity: route.route,
480
+ preserved_personality: preservedRoutePersonality(route.id, route.route),
481
+ stop_gate: route.stopGate || 'none',
482
+ context7_policy: route.context7Policy || 'optional',
483
+ reasoning_policy: route.reasoningPolicy || 'medium',
484
+ shared_mechanical_checks: shared,
485
+ route_specific_checks: routeSpecific,
486
+ simplification_rule: 'Only shared mechanics may move into RecallPulse/ProofCapsule; wording and route identity stay route-owned.',
487
+ cli_entrypoint: route.cliEntrypoint || null
488
+ };
489
+ });
490
+ }
491
+
492
+ export async function buildRecallPulseGovernanceReport(root, opts = {}) {
493
+ const missionId = opts.missionId || null;
494
+ const inventory = buildRouteGateInventory();
495
+ const missions = await listMissionRows(root);
496
+ const requestedSamples = ['Research', 'Team', 'DFix', 'DB', 'QALoop'];
497
+ const samples = [];
498
+ for (const routeId of requestedSamples) {
499
+ const mission = routeId === 'DFix' ? null : latestMissionForRoute(missions, routeId);
500
+ if (mission && opts.writeDecisions !== false) {
501
+ await writeRecallPulseArtifacts(root, {
502
+ missionId: mission.id,
503
+ state: { mission_id: mission.id, mode: mission.mode, route: routeId, prompt: mission.prompt },
504
+ stageId: 'route_intake'
505
+ });
506
+ }
507
+ samples.push({
508
+ route_id: routeId,
509
+ mission_id: mission?.id || null,
510
+ sample_type: mission ? 'historical_mission' : (routeId === 'DFix' ? 'dfix_no_persistent_mission_fixture' : 'missing_historical_mission'),
511
+ report_only_decision_recorded: Boolean(mission || routeId === 'DFix'),
512
+ note: mission
513
+ ? 'RecallPulse report-only decision recorded against historical mission.'
514
+ : (routeId === 'DFix'
515
+ ? 'DFix intentionally has no persistent mission; RecallPulse records a no-op governance fixture instead of starting the full pipeline.'
516
+ : 'No matching historical mission found in this workspace.')
517
+ });
518
+ }
519
+ const fixtureEval = await evaluateRecallPulseFixtures(root, { missionId: missionId || 'governance', write: false });
520
+ const missingSamples = samples.filter((sample) => !sample.report_only_decision_recorded || (sample.route_id !== 'DFix' && !sample.mission_id));
521
+ const report = {
522
+ schema_version: 1,
523
+ artifact: RECALLPULSE_GOVERNANCE_ARTIFACT,
524
+ generated_at: nowIso(),
525
+ mode: 'report_only_governance',
526
+ mission_id: missionId,
527
+ route_gate_inventory: inventory,
528
+ simplification_model: {
529
+ shared_mechanical_checks: [
530
+ 'triwiki_validate_before_final',
531
+ 'context7_current_docs_when_external_docs_are_in_scope',
532
+ 'durable_status_projection',
533
+ 'duplicate_suppression',
534
+ 'route_proof_capsule',
535
+ 'evidence_envelope',
536
+ 'final_completion_summary',
537
+ 'honest_mode',
538
+ 'no_unrequested_fallback_code'
539
+ ],
540
+ route_specific_checks_stay_route_owned: inventory.map((row) => ({
541
+ route_id: row.route_id,
542
+ preserved_personality: row.preserved_personality,
543
+ route_specific_checks: row.route_specific_checks
544
+ })),
545
+ duplicated_requirements_identified: [
546
+ 'final-summary wording',
547
+ 'TriWiki validate reminders',
548
+ 'Context7 evidence reminders',
549
+ 'subagent evidence reminders',
550
+ 'reflection reminders',
551
+ 'status/progress messages',
552
+ 'no-unrequested-fallback-code boilerplate'
553
+ ],
554
+ safe_replacement: 'Replace repeated boilerplate with references to RecallPulse policy, RouteProofCapsule, EvidenceEnvelope, and mission-status-ledger when the route wording is not personality-critical.',
555
+ keep_local_text_when: ['user trust depends on route voice', 'visual evidence policy is route-specific', 'DB safety wording must be explicit', 'DFix must remain ultralight']
556
+ },
557
+ rollout: {
558
+ opt_in_report_only: true,
559
+ feature_flag: RECALLPULSE_POLICY.feature_flag,
560
+ rollback: RECALLPULSE_POLICY.rollback,
561
+ requested_samples: samples,
562
+ missing_samples: missingSamples,
563
+ dfix_policy: 'No persistent DFix mission is created for RecallPulse; the governance fixture proves the no-op/report-only path without starting a full route.'
564
+ },
565
+ shadow_eval: {
566
+ fixture_eval_passed: fixtureEval.passed,
567
+ metrics: fixtureEval.metrics,
568
+ historical_mission_count: missions.length,
569
+ recorded_sample_count: samples.filter((sample) => sample.report_only_decision_recorded).length,
570
+ enforcement_safe: false,
571
+ enforcement_decision: 'keep_report_only',
572
+ reason: 'Fixture and historical shadow artifacts are not a scored live mission dataset; enforcement must wait for benchmark evidence.'
573
+ },
574
+ regression_observations: {
575
+ false_blockers: 'none detected by fixture eval; live historical scoring still required',
576
+ missed_blockers: 'none detected by fixture eval; live historical scoring still required',
577
+ stale_memory_retrievals: 'tracked by L3 hydration and source_conflicts in report-only decisions',
578
+ excessive_l3_hydration: 'tune thresholds only after multiple live reports show false-positive hydration',
579
+ route_personality_regressions: 'not observed in report-only design because route prompts and route-owned wording remain unchanged',
580
+ final_summary_regressions: 'guarded by mission-status-ledger final-summary projection and stop-hook repeat ledger projection',
581
+ codex_app_visibility_regressions: 'mitigated by durable ledger; client hook rendering remains implementation-dependent',
582
+ cli_status_regressions: 'sks recallpulse status reports missing artifacts instead of hiding them'
583
+ },
584
+ governance_decisions: {
585
+ l1_threshold_tuning: 'defer until shadow datasets show required-recall misses or noisy cache hits',
586
+ l2_scope_tuning: 'defer until ProofCapsule exceeds max token budget or misses decisive route artifacts',
587
+ l3_trigger_tuning: 'defer until false-positive hydration rate exceeds target',
588
+ duplicate_suppression_tuning: 'keep max visible repeat count at one for identical keys; reset only on new evidence, blocker resolution, or stage change',
589
+ status_verbosity_tuning: 'status text must remain short, actionable for blockers, and recoverable from mission-status-ledger'
590
+ },
591
+ risks: {
592
+ accepted_before_enforcement: [
593
+ 'report-only artifacts may reveal noisy thresholds before tuning',
594
+ 'historical missions may not cover every route in every workspace',
595
+ 'client hook progress visibility remains outside SKS control'
596
+ ],
597
+ rejected_before_enforcement: [
598
+ 'bypassing route gates',
599
+ 'claiming measured speedups without scored evals',
600
+ 'starting a full pipeline for DFix just to satisfy recall instrumentation',
601
+ 'repeating profane reminder text in user-visible prompts'
602
+ ],
603
+ migration_paths: {
604
+ existing_missions: 'Run sks recallpulse run <mission-id> and sks recallpulse governance <mission-id> to add report-only artifacts.',
605
+ existing_research_artifacts: 'Research gates now require scout display_name/persona/persona_boundary fields; old ledgers should be migrated by adding those fields before claiming pass.',
606
+ generated_skills: 'Do not edit generated installed skills directly; rerun init/bootstrap from engine source when generated text needs refreshing.'
607
+ },
608
+ release_gate: '0.8.0 remains report-only unless packcheck, selftest, sizecheck, registry metadata check, TriWiki validate, and RecallPulse fixture eval pass.'
609
+ }
610
+ };
611
+ if (missionId) await writeJsonAtomic(path.join(recallPulseMissionDir(root, missionId), RECALLPULSE_GOVERNANCE_ARTIFACT), report);
612
+ return report;
613
+ }
614
+
615
+ export function validateResearchScoutPersonas(scoutLedger = {}, geniusSummaryText = '') {
616
+ const rows = Array.isArray(scoutLedger?.scouts) ? scoutLedger.scouts : [];
617
+ const issues = [];
618
+ const byId = new Map(RESEARCH_SCOUT_PERSONA_CONTRACT.map((scout) => [scout.id, scout]));
619
+ const displayNames = new Set();
620
+ for (const expected of RESEARCH_SCOUT_PERSONA_CONTRACT) {
621
+ const row = rows.find((item) => item?.id === expected.id);
622
+ if (!row) {
623
+ issues.push(`${expected.id}:missing`);
624
+ continue;
625
+ }
626
+ if (!row.display_name) issues.push(`${expected.id}:display_name_missing`);
627
+ if (!row.persona) issues.push(`${expected.id}:persona_missing`);
628
+ if (!row.persona_boundary) issues.push(`${expected.id}:persona_boundary_missing`);
629
+ if (row.persona_boundary && !/do not impersonate|not impersonat|lens only/i.test(row.persona_boundary)) issues.push(`${expected.id}:persona_boundary_not_enforced`);
630
+ if (row.effort !== 'xhigh' && row.reasoning_effort !== 'xhigh') issues.push(`${expected.id}:effort_not_xhigh`);
631
+ if (row.service_tier && row.service_tier !== 'fast') issues.push(`${expected.id}:service_tier_not_fast`);
632
+ if (!row.eureka?.idea || row.eureka?.exclamation !== 'Eureka!') issues.push(`${expected.id}:eureka_missing`);
633
+ if (!Array.isArray(row.falsifiers)) issues.push(`${expected.id}:falsifiers_missing`);
634
+ if (!Array.isArray(row.cheap_probes)) issues.push(`${expected.id}:cheap_probe_missing`);
635
+ if (!row.challenge_or_response) issues.push(`${expected.id}:challenge_or_response_missing`);
636
+ if (row.display_name) displayNames.add(row.display_name);
637
+ const text = JSON.stringify(row).toLowerCase();
638
+ const inspiration = String(byId.get(expected.id)?.historical_inspiration || '').toLowerCase();
639
+ if (inspiration && inspiration !== 'counterevidence discipline' && new RegExp(`\\bi am ${escapeRegex(inspiration)}\\b|\\bas ${escapeRegex(inspiration)}\\b`).test(text)) {
640
+ issues.push(`${expected.id}:impersonation_claim`);
641
+ }
642
+ }
643
+ if (displayNames.size !== RESEARCH_SCOUT_PERSONA_CONTRACT.length) issues.push('display_names_not_unique');
644
+ const lowerSummary = String(geniusSummaryText || '').toLowerCase();
645
+ for (const expected of RESEARCH_SCOUT_PERSONA_CONTRACT) {
646
+ if (lowerSummary && !lowerSummary.includes(expected.display_name.toLowerCase())) issues.push(`${expected.id}:summary_display_name_missing`);
647
+ }
648
+ return { ok: issues.length === 0, issues };
649
+ }
650
+
651
+ export async function updateRecallPulseTaskChecklist(root, completedIds = []) {
652
+ const file = path.join(root, RECALLPULSE_TASKS_FILE);
653
+ let text = await readText(file);
654
+ const ids = new Set(completedIds.map((id) => String(id).padStart(3, '0')));
655
+ for (const id of ids) {
656
+ text = text.replace(new RegExp(`^- \\[ \\] T${id}(?=\\s)`, 'm'), `- [x] T${id}`);
657
+ }
658
+ await writeTextAtomic(file, text);
659
+ return { file, completed: [...ids].map((id) => `T${id}`) };
660
+ }
661
+
662
+ export function parseRecallPulseTaskList(text = '') {
663
+ const tasks = [];
664
+ const re = /^- \[( |x)\] (T(\d{3}))\s+(.+)$/gm;
665
+ let match;
666
+ while ((match = re.exec(String(text || '')))) {
667
+ tasks.push({
668
+ id: match[2],
669
+ number: Number(match[3]),
670
+ checked: match[1] === 'x',
671
+ title: match[4],
672
+ line_index: String(text).slice(0, match.index).split(/\n/).length
673
+ });
674
+ }
675
+ return tasks.sort((a, b) => a.number - b.number);
676
+ }
677
+
678
+ export async function buildRecallPulseTaskGoalLedger(root, missionId, opts = {}) {
679
+ const file = path.join(root, RECALLPULSE_TASKS_FILE);
680
+ const text = await readText(file);
681
+ const tasks = parseRecallPulseTaskList(text);
682
+ const next = tasks.find((task) => !task.checked) || null;
683
+ const now = nowIso();
684
+ const ledger = {
685
+ schema_version: 1,
686
+ artifact: RECALLPULSE_TASK_GOAL_LEDGER_ARTIFACT,
687
+ parent_goal_mission_id: missionId,
688
+ task_source: RECALLPULSE_TASKS_FILE,
689
+ updated_at: now,
690
+ sequential_rule: 'Treat every Txxx row as a child $Goal checkpoint. Complete and check only the first unchecked task unless the caller explicitly records a later task with evidence.',
691
+ checkbox_rule: 'A task may become [x] only after its child goal row records status=done and evidence paths or verification commands.',
692
+ counts: {
693
+ total: tasks.length,
694
+ checked: tasks.filter((task) => task.checked).length,
695
+ unchecked: tasks.filter((task) => !task.checked).length
696
+ },
697
+ next_task: next ? { id: next.id, title: next.title } : null,
698
+ task_goals: tasks.map((task) => ({
699
+ task_id: task.id,
700
+ goal_id: `${missionId || 'goal'}:${task.id}`,
701
+ parent_goal_mission_id: missionId || null,
702
+ title: task.title,
703
+ status: task.checked ? 'done' : (next?.id === task.id ? (opts.nextStatus || 'next') : 'pending'),
704
+ checked_in_markdown: task.checked,
705
+ evidence: [],
706
+ verification: [],
707
+ line_index: task.line_index
708
+ }))
709
+ };
710
+ if (missionId) await writeJsonAtomic(path.join(recallPulseMissionDir(root, missionId), RECALLPULSE_TASK_GOAL_LEDGER_ARTIFACT), ledger);
711
+ return ledger;
712
+ }
713
+
714
+ export async function completeRecallPulseTaskGoal(root, missionId, taskId, opts = {}) {
715
+ const id = normalizeTaskId(taskId);
716
+ const ledger = await buildRecallPulseTaskGoalLedger(root, missionId);
717
+ const task = ledger.task_goals.find((row) => row.task_id === id);
718
+ if (!task) throw new Error(`Unknown RecallPulse task id: ${id}`);
719
+ const firstOpen = ledger.task_goals.find((row) => !row.checked_in_markdown);
720
+ if (firstOpen && firstOpen.task_id !== id && opts.allowOutOfOrder !== true) {
721
+ throw new Error(`Refusing out-of-order task check: next unchecked task is ${firstOpen.task_id}, requested ${id}`);
722
+ }
723
+ const evidence = Array.isArray(opts.evidence) ? opts.evidence.filter(Boolean) : [];
724
+ const verification = Array.isArray(opts.verification) ? opts.verification.filter(Boolean) : [];
725
+ const updated = {
726
+ ...ledger,
727
+ updated_at: nowIso(),
728
+ task_goals: ledger.task_goals.map((row) => row.task_id === id ? {
729
+ ...row,
730
+ status: 'done',
731
+ completed_at: nowIso(),
732
+ checked_in_markdown: true,
733
+ evidence,
734
+ verification,
735
+ notes: opts.notes || ''
736
+ } : row)
737
+ };
738
+ updated.counts = {
739
+ total: updated.task_goals.length,
740
+ checked: updated.task_goals.filter((row) => row.checked_in_markdown || row.status === 'done').length,
741
+ unchecked: updated.task_goals.filter((row) => !(row.checked_in_markdown || row.status === 'done')).length
742
+ };
743
+ updated.next_task = updated.task_goals.find((row) => !(row.checked_in_markdown || row.status === 'done')) || null;
744
+ await updateRecallPulseTaskChecklist(root, [id.replace(/^T/, '')]);
745
+ await writeJsonAtomic(path.join(recallPulseMissionDir(root, missionId), RECALLPULSE_TASK_GOAL_LEDGER_ARTIFACT), updated);
746
+ await appendMissionStatus(root, missionId, {
747
+ category: 'progress',
748
+ audience: ['user', 'route', 'final-summary'],
749
+ stage_id: id,
750
+ message: `${id} completed as a child $Goal checkpoint.`,
751
+ dedupe_key: sha256(`${missionId}:${id}:done`).slice(0, 24),
752
+ evidence: [RECALLPULSE_TASKS_FILE, RECALLPULSE_TASK_GOAL_LEDGER_ARTIFACT, ...evidence]
753
+ });
754
+ return { ledger: updated, task: updated.task_goals.find((row) => row.task_id === id) };
755
+ }
756
+
757
+ export const RECALLPULSE_FOUNDATION_TASK_IDS = Object.freeze([
758
+ ...range(1, 180),
759
+ ...range(181, 230),
760
+ ...range(231, 270),
761
+ ...range(293, 300),
762
+ ...range(301, 340),
763
+ ...range(341, 380),
764
+ ...range(381, 410),
765
+ 421, 424, 425, 427, 428, 429, 430, 431, 436, 437, 438, 439, 440, 443, 444, 445, 446, 450,
766
+ ...range(451, 480)
767
+ ].map((n) => String(n).padStart(3, '0')));
768
+
769
+ function selectL1(pack = {}, { stageId = '', routeId = '' } = {}) {
770
+ const finalStage = /final|honest|review/i.test(stageId);
771
+ const maxItems = finalStage ? RECALLPULSE_POLICY.cache.l1.max_items_final : RECALLPULSE_POLICY.cache.l1.max_items_normal;
772
+ const claims = new Map((Array.isArray(pack?.claims) ? pack.claims : []).map((claim) => [claim.id, claim]));
773
+ const meta = wikiMeta(pack);
774
+ const useRows = Array.isArray(pack?.attention?.use_first) ? pack.attention.use_first : [];
775
+ const considered = useRows
776
+ .map((row) => {
777
+ const id = row?.[0];
778
+ const claim = claims.get(id) || {};
779
+ const m = meta.get(id) || {};
780
+ const trust = number(m.trust_score, number(claim.trust_score, 0.9));
781
+ const risk = m.risk || claim.risk || 'medium';
782
+ const freshness = m.freshness || claim.freshness || 'fresh';
783
+ const text = String(claim.text || claim.claim || id || '').trim();
784
+ return {
785
+ id,
786
+ text,
787
+ rgba: row?.[1] || claim.rgba || m.rgba || null,
788
+ hash: row?.[2] || claim.h || m.hash || null,
789
+ source: claim.source || m.source || null,
790
+ trust_score: trust,
791
+ risk,
792
+ freshness,
793
+ route_relevance: routeRelevance(id, text, routeId),
794
+ token_cost: Math.max(1, Math.ceil(text.length / 4)),
795
+ eligible: trust >= RECALLPULSE_POLICY.cache.l1.min_trust && !['stale', 'conflicted', 'unsupported'].includes(String(freshness).toLowerCase())
796
+ };
797
+ })
798
+ .filter((row) => row.id);
799
+ const selected = [];
800
+ let tokens = 0;
801
+ for (const item of considered.filter((row) => row.eligible).sort((a, b) => (b.route_relevance - a.route_relevance) || (b.trust_score - a.trust_score))) {
802
+ if (selected.length >= maxItems) break;
803
+ if (tokens + item.token_cost > RECALLPULSE_POLICY.cache.l1.max_tokens) continue;
804
+ selected.push(item);
805
+ tokens += item.token_cost;
806
+ }
807
+ return {
808
+ tier: 'L1',
809
+ label: RECALLPULSE_POLICY.cache.l1.label,
810
+ selected,
811
+ considered,
812
+ max_items: maxItems,
813
+ max_tokens: RECALLPULSE_POLICY.cache.l1.max_tokens,
814
+ token_cost: tokens,
815
+ positive_recall_only: true
816
+ };
817
+ }
818
+
819
+ async function buildL2(root, dir) {
820
+ const mission = await readJson(path.join(dir, 'mission.json'), null);
821
+ const routeContext = await readJson(path.join(dir, 'route-context.json'), null);
822
+ const pipelinePlan = await readJson(path.join(dir, 'pipeline-plan.json'), null);
823
+ const contract = await readJson(path.join(dir, 'decision-contract.json'), null);
824
+ const statusLedger = await readJson(path.join(dir, MISSION_STATUS_LEDGER_ARTIFACT), null);
825
+ const gateFiles = ['team-gate.json', 'research-gate.evaluated.json', 'research-gate.json', 'db-review.json', 'qa-gate.json', 'ppt-gate.json', 'image-ux-review-gate.json', 'gx-gate.json', 'hard-blocker.json'];
826
+ const gates = [];
827
+ for (const file of gateFiles) {
828
+ const gate = await readJson(path.join(dir, file), null);
829
+ if (gate) gates.push({ file, passed: gate.passed === true, missing: missingGateFields(gate), hash: sha256(JSON.stringify(gate)).slice(0, 12) });
830
+ }
831
+ const changedArtifacts = await listMissionArtifacts(dir);
832
+ const eventsTail = await tailJsonl(path.join(dir, 'events.jsonl'), 8);
833
+ const verificationResults = await collectVerificationResults(dir);
834
+ return {
835
+ tier: 'L2',
836
+ label: RECALLPULSE_POLICY.cache.l2.label,
837
+ mission,
838
+ route_context: routeContext,
839
+ pipeline_plan: pipelinePlan,
840
+ decision_contract: contract ? { present: true, sealed_hash: contract.sealed_hash || null, status: contract.status || null } : { present: false },
841
+ gate_ids: gates.map((gate) => gate.file),
842
+ gate_blockers: gates.filter((gate) => !gate.passed).map((gate) => ({ file: gate.file, missing: gate.missing })),
843
+ verification_results: verificationResults,
844
+ subagent_handoffs: await tailJsonl(path.join(dir, 'subagent-evidence.jsonl'), 5),
845
+ status_ledger_snapshot: statusLedger?.latest || null,
846
+ status_ledger_summary: statusLedger?.final_summary_projection || null,
847
+ changed_artifacts: changedArtifacts,
848
+ changed_files: [],
849
+ evidence_hashes: gates.map((gate) => ({ file: gate.file, hash: gate.hash })),
850
+ duplicate_keys: collectDuplicateKeys(statusLedger),
851
+ recent_events: eventsTail
852
+ };
853
+ }
854
+
855
+ function emptyL2() {
856
+ return {
857
+ tier: 'L2',
858
+ label: RECALLPULSE_POLICY.cache.l2.label,
859
+ gate_blockers: [],
860
+ verification_results: [],
861
+ subagent_handoffs: [],
862
+ changed_artifacts: [],
863
+ evidence_hashes: [],
864
+ duplicate_keys: []
865
+ };
866
+ }
867
+
868
+ function buildL3(pack = {}, { stageId = '', routeId = '', l1 = {} } = {}) {
869
+ const hydrateRows = Array.isArray(pack?.attention?.hydrate_first) ? pack.attention.hydrate_first : [];
870
+ const selectedIds = new Set((l1.selected || []).map((item) => item.id));
871
+ const sourceRequests = hydrateRows.map((row) => ({
872
+ id: row?.[0],
873
+ reason: row?.[1] || 'hydrate_source',
874
+ selected_in_l1: selectedIds.has(row?.[0])
875
+ })).filter((row) => row.id);
876
+ const broadRoute = /team|research|db|release|version|security/i.test(`${routeId} ${stageId}`);
877
+ return {
878
+ tier: 'L3',
879
+ label: RECALLPULSE_POLICY.cache.l3.label,
880
+ hydration_requests: sourceRequests,
881
+ blocked_hydration_reasons: [],
882
+ source_conflicts: sourceRequests.filter((row) => /conflict|stale|risk/i.test(row.reason)),
883
+ triggers_active: [
884
+ ...sourceRequests.map((row) => row.reason),
885
+ broadRoute ? 'broad_route_policy' : null,
886
+ /final/i.test(stageId) ? 'final_claim' : null
887
+ ].filter(Boolean)
888
+ };
889
+ }
890
+
891
+ function duplicateSuppression({ routeId, missionId, stageId, l1, l2 }) {
892
+ const claimHash = sha256((l1.selected || []).map((item) => item.id).join('|')).slice(0, 12);
893
+ const evidenceHash = sha256((l2.evidence_hashes || []).map((item) => `${item.file}:${item.hash}`).join('|')).slice(0, 12);
894
+ const blockerCode = (l2.gate_blockers || []).map((item) => item.file).join('|') || 'none';
895
+ const visible = `${routeId}:${stageId}:${claimHash}:${evidenceHash}:${blockerCode}`;
896
+ const key = sha256(JSON.stringify({ routeId, missionId, stageId, claimHash, evidenceHash, blockerCode, visible })).slice(0, 24);
897
+ const repeated = (l2.duplicate_keys || []).includes(key);
898
+ return {
899
+ key,
900
+ route_id: routeId,
901
+ mission_id: missionId,
902
+ stage_id: stageId,
903
+ claim_hash: claimHash,
904
+ evidence_hash: evidenceHash,
905
+ blocker_code: blockerCode,
906
+ visible_message_hash: sha256(visible).slice(0, 12),
907
+ repeated,
908
+ suppressed_keys: repeated ? [key] : [],
909
+ repeat_budget: RECALLPULSE_POLICY.repetition.repeat_budget
910
+ };
911
+ }
912
+
913
+ function recallPulseMetrics({ l1 = {}, l2 = {}, l3 = {}, duplicate = {} }) {
914
+ const selected = Array.isArray(l1.selected) ? l1.selected : [];
915
+ const considered = Array.isArray(l1.considered) ? l1.considered : [];
916
+ const hydrate = Array.isArray(l3.hydration_requests) ? l3.hydration_requests : [];
917
+ const stale = hydrate.filter((row) => /stale|conflict|low_trust|risk/i.test(String(row.reason || '')));
918
+ return {
919
+ cache_hit_count: selected.length,
920
+ cache_miss_count: Math.max(0, considered.length - selected.length),
921
+ hydration_count: hydrate.length,
922
+ stale_recall_count: stale.length,
923
+ duplicate_suppression_count: duplicate.repeated ? 1 : 0,
924
+ token_cost_estimate: l1.token_cost || 0,
925
+ gate_agreement_target: RECALLPULSE_POLICY.eval_targets.route_gate_agreement_min,
926
+ l2_artifact_count: (l2.changed_artifacts || []).length,
927
+ evidence_hash_count: (l2.evidence_hashes || []).length
928
+ };
929
+ }
930
+
931
+ function recallRisk({ state = {}, l1 = {}, l2 = {}, l3 = {} }) {
932
+ const unverified = [];
933
+ if (state.context7_required && !(state.context7_verified || state.context7_docs)) unverified.push('context7_evidence');
934
+ if (state.subagents_required && !state.subagents_verified) unverified.push('subagent_evidence');
935
+ for (const blocker of l2.gate_blockers || []) unverified.push(`${blocker.file}:${(blocker.missing || []).join(',') || 'not_passed'}`);
936
+ const l3Conflicts = l3.source_conflicts || [];
937
+ return {
938
+ level: unverified.length || l3Conflicts.length ? 'high' : ((l1.selected || []).length ? 'medium' : 'low'),
939
+ confidence: (l1.selected || []).length ? 0.86 : 0.45,
940
+ unverified_claims: unverified,
941
+ source_conflict_count: l3Conflicts.length
942
+ };
943
+ }
944
+
945
+ function chooseAction({ pack, l1, l2, l3, duplicate, risk, state, stageId }) {
946
+ if (!pack) return 'hydrate';
947
+ if (duplicate.repeated) return 'suppress';
948
+ if (risk.unverified_claims?.length) return 'block';
949
+ if (risk.source_conflict_count > 0 || (l3.hydration_requests || []).length && /final|review|security|db|release/i.test(`${stageId} ${state.route || ''}`)) return 'hydrate';
950
+ if ((l2.gate_blockers || []).length) return 'escalate';
951
+ if ((l1.selected || []).length) return 'cache_hit';
952
+ return 'no_op';
953
+ }
954
+
955
+ function userVisibleProjection({ action, l1, l2, l3, risk }) {
956
+ const selected = (l1.selected || []).map((item) => item.id);
957
+ const blockers = risk.unverified_claims || [];
958
+ const hydrate = l3.hydration_requests || [];
959
+ let message = 'RecallPulse checked the current stage in report-only mode.';
960
+ if (action === 'cache_hit') message = `RecallPulse L1 cache hit: ${selected.slice(0, 4).join(', ') || 'no named anchors'}.`;
961
+ else if (action === 'hydrate') message = `RecallPulse requests source hydration before risky claims: ${hydrate.slice(0, 3).map((item) => item.id).join(', ') || 'source check'}.`;
962
+ else if (action === 'block') message = `RecallPulse report-only blocker: ${blockers.slice(0, 3).join(', ') || 'unverified evidence'}.`;
963
+ else if (action === 'suppress') message = 'RecallPulse suppressed a duplicate reminder and kept one durable status row.';
964
+ else if (action === 'escalate') message = `RecallPulse recommends keeping the heavier route gate: ${(l2.gate_blockers || []).map((item) => item.file).join(', ') || 'gate blocker'}.`;
965
+ return {
966
+ category: action === 'block' ? 'blocker' : 'progress',
967
+ message,
968
+ l1_ids: selected,
969
+ l3_request_count: hydrate.length
970
+ };
971
+ }
972
+
973
+ function normalizeStatusEntry(entry, index) {
974
+ const category = RECALLPULSE_POLICY.status_ledger.categories.includes(entry.category) ? entry.category : 'info';
975
+ const audience = Array.isArray(entry.audience) ? entry.audience : [entry.audience || 'route'];
976
+ const message = String(entry.message || '').trim() || 'Status updated.';
977
+ return {
978
+ id: entry.id || `status-${String(index).padStart(4, '0')}`,
979
+ ts: entry.ts || nowIso(),
980
+ category,
981
+ audience: audience.filter(Boolean),
982
+ stage_id: entry.stage_id || null,
983
+ message,
984
+ dedupe_key: entry.dedupe_key || sha256(`${category}:${message}`).slice(0, 24),
985
+ visibility: {
986
+ user: audience.includes('user'),
987
+ route: audience.includes('route'),
988
+ reviewer: audience.includes('reviewer'),
989
+ final_summary: audience.includes('final-summary')
990
+ },
991
+ evidence: Array.isArray(entry.evidence) ? entry.evidence : []
992
+ };
993
+ }
994
+
995
+ function statusFinalProjection(entries = []) {
996
+ const finalRelevant = entries.filter((entry) => entry.visibility?.final_summary || entry.category === 'blocker' || entry.category === 'verification');
997
+ return {
998
+ completed: finalRelevant.filter((entry) => entry.category !== 'blocker').map((entry) => entry.message).slice(-8),
999
+ blockers: finalRelevant.filter((entry) => entry.category === 'blocker').map((entry) => entry.message).slice(-8),
1000
+ last_user_visible: [...entries].reverse().find((entry) => entry.visibility?.user) || null
1001
+ };
1002
+ }
1003
+
1004
+ function statusLedgerProjections(entries = []) {
1005
+ const latest = entries[entries.length - 1] || null;
1006
+ const blockers = entries.filter((entry) => entry.category === 'blocker').slice(-5);
1007
+ const userVisible = entries.filter((entry) => entry.visibility?.user).slice(-5);
1008
+ return {
1009
+ team_live: {
1010
+ latest_user_visible: latest?.visibility?.user ? latest.message : userVisible[userVisible.length - 1]?.message || null,
1011
+ blocker_count: blockers.length
1012
+ },
1013
+ pipeline_status: {
1014
+ latest_category: latest?.category || null,
1015
+ latest_stage_id: latest?.stage_id || null,
1016
+ latest_message: latest?.message || null,
1017
+ blocker_messages: blockers.map((entry) => entry.message)
1018
+ },
1019
+ codex_app_stop_hook: {
1020
+ recoverable_from_ledger: true,
1021
+ repeated_messages_collapsed: true,
1022
+ latest_user_visible: userVisible[userVisible.length - 1] || null
1023
+ }
1024
+ };
1025
+ }
1026
+
1027
+ function stageFromState(state = {}) {
1028
+ const phase = String(state.phase || '').toLowerCase();
1029
+ if (/final|honest|stop/.test(phase)) return 'before_final';
1030
+ if (/review/.test(phase)) return 'before_review';
1031
+ if (/implement|execution|running/.test(phase)) return 'before_implementation';
1032
+ if (/plan|scout|debate|prepared/.test(phase)) return 'before_planning';
1033
+ return 'route_intake';
1034
+ }
1035
+
1036
+ function routeRelevance(id = '', text = '', routeId = '') {
1037
+ const hay = `${id} ${text}`.toLowerCase();
1038
+ const route = String(routeId || '').toLowerCase();
1039
+ let score = 0.5;
1040
+ if (route && hay.includes(route)) score += 0.2;
1041
+ if (/triwiki|wiki|memory|recall|cache|hydrate/.test(hay)) score += 0.22;
1042
+ if (/db|security|release|version|final|honest/.test(hay)) score += 0.15;
1043
+ return Math.min(1, score);
1044
+ }
1045
+
1046
+ function wikiMeta(pack = {}) {
1047
+ const out = new Map();
1048
+ for (const row of Array.isArray(pack?.wiki?.a) ? pack.wiki.a : []) {
1049
+ out.set(row[0], {
1050
+ id: row[0],
1051
+ rgba: row[1],
1052
+ authority: row[3],
1053
+ status: row[4],
1054
+ risk: row[5],
1055
+ source: row[6],
1056
+ hash: row[7],
1057
+ file: row[8],
1058
+ trust_score: row[9],
1059
+ freshness: row[10]
1060
+ });
1061
+ }
1062
+ return out;
1063
+ }
1064
+
1065
+ async function listMissionArtifacts(dir) {
1066
+ let entries = [];
1067
+ try { entries = await fsp.readdir(dir, { withFileTypes: true }); } catch { return []; }
1068
+ const rows = [];
1069
+ for (const entry of entries) {
1070
+ if (!entry.isFile()) continue;
1071
+ const file = path.join(dir, entry.name);
1072
+ const stat = await fsp.stat(file).catch(() => null);
1073
+ rows.push({ file: entry.name, mtime_ms: stat?.mtimeMs || 0 });
1074
+ }
1075
+ return rows.sort((a, b) => b.mtime_ms - a.mtime_ms).slice(0, 20).map((row) => row.file);
1076
+ }
1077
+
1078
+ async function tailJsonl(file, max = 5) {
1079
+ const text = await readText(file, '');
1080
+ return text.split(/\n/).filter(Boolean).slice(-max).map((line) => {
1081
+ try { return JSON.parse(line); } catch { return { raw: line.slice(0, 500) }; }
1082
+ });
1083
+ }
1084
+
1085
+ async function collectVerificationResults(dir) {
1086
+ const names = ['packcheck-result.json', 'selftest-result.json', 'verification-result.json'];
1087
+ const out = [];
1088
+ for (const name of names) {
1089
+ const row = await readJson(path.join(dir, name), null);
1090
+ if (row) out.push({ file: name, ...row });
1091
+ }
1092
+ return out;
1093
+ }
1094
+
1095
+ function missingGateFields(gate = {}) {
1096
+ if (gate.passed === true) return [];
1097
+ return Object.entries(gate)
1098
+ .filter(([, value]) => value === false)
1099
+ .map(([key]) => key)
1100
+ .slice(0, 16);
1101
+ }
1102
+
1103
+ function collectDuplicateKeys(statusLedger = null) {
1104
+ return (statusLedger?.entries || []).map((entry) => entry.dedupe_key).filter(Boolean);
1105
+ }
1106
+
1107
+ function sharedChecksForRoute(route = {}, lifecycle = []) {
1108
+ const checks = new Set();
1109
+ if (route.context7Policy === 'required' || route.context7Policy === 'if_external_docs') checks.add('context7_current_docs_when_relevant');
1110
+ if (route.stopGate && route.stopGate !== 'none') checks.add('route_stop_gate_status');
1111
+ if (lifecycle.some((stage) => /triwiki|wiki/i.test(stage))) checks.add('triwiki_context_refresh_or_validate');
1112
+ if (lifecycle.some((stage) => /reflection/i.test(stage))) checks.add('post_route_reflection');
1113
+ if (lifecycle.some((stage) => /honest/i.test(stage))) checks.add('honest_mode');
1114
+ if (lifecycle.some((stage) => /review|gate|validate|verification|evidence/i.test(stage))) checks.add('evidence_or_verification_gate');
1115
+ checks.add('no_unrequested_fallback_code');
1116
+ checks.add('durable_status_projection');
1117
+ checks.add('duplicate_suppression');
1118
+ return [...checks];
1119
+ }
1120
+
1121
+ function sharedLifecycleStage(stage = '') {
1122
+ return /triwiki|wiki|context7|reflection|honest|gate|validate|verification|evidence|status|summary/i.test(String(stage || ''));
1123
+ }
1124
+
1125
+ function preservedRoutePersonality(routeId = '', routeName = '') {
1126
+ const byRoute = {
1127
+ DFix: 'ultralight direct-fix path stays tiny and does not start the full pipeline',
1128
+ Answer: 'answer-only path stays conversational and does not start implementation',
1129
+ SKS: 'general SKS discovery/help personality stays simple',
1130
+ Team: 'Team keeps scout, debate, executor, and five-lane review identity',
1131
+ QALoop: 'QA-LOOP keeps dogfood, checklist, remediation, and reverification identity',
1132
+ PPT: 'PPT keeps restrained information-first HTML/PDF delivery identity',
1133
+ ImageUXReview: 'Image UX Review keeps gpt-image-2 annotated raster review identity',
1134
+ ComputerUse: 'Computer Use keeps maximum-speed visual/browser lane identity',
1135
+ Goal: 'Goal stays a native /goal persistence bridge, not a heavyweight route',
1136
+ Research: 'Research keeps named xhigh persona scout council, Eureka, debate, paper, and falsification identity',
1137
+ AutoResearch: 'AutoResearch keeps iterative experiment loop identity',
1138
+ DB: 'DB keeps conservative read-first destructive-operation safety identity',
1139
+ MadSKS: 'MAD-SKS keeps explicit scoped high-risk authorization identity',
1140
+ GX: 'GX keeps deterministic visual-context cartridge identity',
1141
+ Wiki: 'Wiki keeps bounded TriWiki maintenance identity',
1142
+ Help: 'Help stays lightweight command discovery'
1143
+ };
1144
+ return byRoute[routeId] || `${routeName || routeId || 'route'} personality remains route-owned`;
1145
+ }
1146
+
1147
+ async function listMissionRows(root) {
1148
+ const base = path.join(root, '.sneakoscope', 'missions');
1149
+ let entries = [];
1150
+ try { entries = await fsp.readdir(base, { withFileTypes: true }); } catch { return []; }
1151
+ const rows = [];
1152
+ for (const entry of entries) {
1153
+ if (!entry.isDirectory()) continue;
1154
+ const mission = await readJson(path.join(base, entry.name, 'mission.json'), null);
1155
+ if (!mission) continue;
1156
+ rows.push({
1157
+ id: entry.name,
1158
+ mode: mission.mode || null,
1159
+ prompt: mission.prompt || '',
1160
+ created_at: mission.created_at || ''
1161
+ });
1162
+ }
1163
+ return rows.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)));
1164
+ }
1165
+
1166
+ function latestMissionForRoute(missions = [], routeId = '') {
1167
+ const target = String(routeId || '').toLowerCase().replace(/[^a-z0-9]/g, '');
1168
+ const aliases = {
1169
+ qaloop: ['qaloop', 'qa', 'qa-loop'],
1170
+ db: ['db', 'database'],
1171
+ team: ['team'],
1172
+ research: ['research'],
1173
+ goal: ['goal']
1174
+ }[target] || [target];
1175
+ return [...missions].reverse().find((mission) => {
1176
+ const mode = String(mission.mode || '').toLowerCase().replace(/[^a-z0-9]/g, '');
1177
+ return aliases.some((alias) => mode === alias.replace(/[^a-z0-9]/g, ''));
1178
+ }) || null;
1179
+ }
1180
+
1181
+ function normalizeTaskId(value = '') {
1182
+ const raw = String(value || '').trim().toUpperCase();
1183
+ const match = raw.match(/^T?(\d{1,3})$/);
1184
+ if (!match) throw new Error(`Invalid RecallPulse task id: ${value}`);
1185
+ return `T${match[1].padStart(3, '0')}`;
1186
+ }
1187
+
1188
+ function extractAcceptanceCriteria(l2 = {}) {
1189
+ const answers = l2.decision_contract?.answers || {};
1190
+ if (Array.isArray(answers.ACCEPTANCE_CRITERIA)) return answers.ACCEPTANCE_CRITERIA;
1191
+ if (typeof answers.ACCEPTANCE_CRITERIA === 'string') return [answers.ACCEPTANCE_CRITERIA];
1192
+ return [];
1193
+ }
1194
+
1195
+ function number(...values) {
1196
+ for (const value of values) {
1197
+ const n = Number(value);
1198
+ if (Number.isFinite(n)) return n;
1199
+ }
1200
+ return 0;
1201
+ }
1202
+
1203
+ function fixture(id, passed, assertion) {
1204
+ return { id, passed: Boolean(passed), assertion };
1205
+ }
1206
+
1207
+ function range(start, end) {
1208
+ const out = [];
1209
+ for (let i = start; i <= end; i++) out.push(i);
1210
+ return out;
1211
+ }
1212
+
1213
+ function escapeRegex(text = '') {
1214
+ return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1215
+ }