brainclaw 1.8.0 → 1.9.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.
Files changed (140) hide show
  1. package/README.md +12 -11
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +138 -13
  4. package/dist/commands/add-step.js +1 -1
  5. package/dist/commands/bootstrap.js +2 -26
  6. package/dist/commands/check-security-mcp.js +50 -33
  7. package/dist/commands/check-security.js +86 -43
  8. package/dist/commands/claim.js +22 -21
  9. package/dist/commands/confirm.js +26 -0
  10. package/dist/commands/context-diff.js +1 -1
  11. package/dist/commands/dispatch-watch.js +142 -0
  12. package/dist/commands/doctor.js +113 -2
  13. package/dist/commands/estimation-report.js +115 -16
  14. package/dist/commands/harvest.js +285 -22
  15. package/dist/commands/init.js +123 -21
  16. package/dist/commands/loops-handlers.js +4 -0
  17. package/dist/commands/mcp-read-handlers.js +198 -29
  18. package/dist/commands/mcp.js +588 -92
  19. package/dist/commands/memory.js +21 -17
  20. package/dist/commands/migrate.js +81 -17
  21. package/dist/commands/prune.js +78 -4
  22. package/dist/commands/reflect.js +26 -20
  23. package/dist/commands/register-agent.js +57 -1
  24. package/dist/commands/repair.js +20 -0
  25. package/dist/commands/session-end.js +15 -6
  26. package/dist/commands/session-start.js +18 -1
  27. package/dist/commands/setup-security.js +39 -18
  28. package/dist/commands/setup.js +26 -27
  29. package/dist/commands/stale.js +16 -2
  30. package/dist/commands/uninstall.js +126 -34
  31. package/dist/commands/update-step.js +6 -0
  32. package/dist/commands/worktree.js +60 -0
  33. package/dist/core/actions.js +12 -3
  34. package/dist/core/agent-capability.js +11 -13
  35. package/dist/core/agent-files.js +844 -547
  36. package/dist/core/agent-integrations.js +0 -3
  37. package/dist/core/agent-inventory.js +67 -0
  38. package/dist/core/agent-registry.js +163 -29
  39. package/dist/core/agentrun-reconciler.js +33 -2
  40. package/dist/core/agentruns.js +7 -1
  41. package/dist/core/ai-agent-detection.js +31 -44
  42. package/dist/core/archival.js +15 -9
  43. package/dist/core/assignment-reconciler.js +56 -0
  44. package/dist/core/assignment-sweeper.js +127 -4
  45. package/dist/core/assignments.js +69 -11
  46. package/dist/core/bootstrap.js +233 -67
  47. package/dist/core/brainclaw-version.js +22 -0
  48. package/dist/core/candidates.js +21 -1
  49. package/dist/core/claims.js +313 -150
  50. package/dist/core/config.js +6 -1
  51. package/dist/core/context-diff.js +148 -20
  52. package/dist/core/context.js +129 -8
  53. package/dist/core/coordination.js +22 -3
  54. package/dist/core/dispatch-status.js +79 -5
  55. package/dist/core/dispatcher.js +64 -11
  56. package/dist/core/entity-operations.js +45 -24
  57. package/dist/core/entity-registry.js +31 -5
  58. package/dist/core/event-log.js +138 -21
  59. package/dist/core/events/checkpoint.js +258 -0
  60. package/dist/core/events/genesis.js +220 -0
  61. package/dist/core/events/journal.js +507 -0
  62. package/dist/core/events/materialize.js +126 -0
  63. package/dist/core/events/registry-post-image.js +110 -0
  64. package/dist/core/events/verify.js +109 -0
  65. package/dist/core/execution-adapters.js +23 -0
  66. package/dist/core/facade-schema.js +38 -0
  67. package/dist/core/gc-semantic.js +130 -5
  68. package/dist/core/handoff-snapshot.js +68 -0
  69. package/dist/core/ids.js +19 -8
  70. package/dist/core/instruction-templates.js +34 -115
  71. package/dist/core/io.js +39 -3
  72. package/dist/core/json-store.js +10 -1
  73. package/dist/core/lock.js +153 -28
  74. package/dist/core/loops/bootstrap-acquire.js +25 -1
  75. package/dist/core/loops/facade-schema.js +2 -0
  76. package/dist/core/loops/hooks/survey-signals-baseline.js +36 -0
  77. package/dist/core/loops/index.js +1 -0
  78. package/dist/core/loops/presets/bootstrap.js +7 -0
  79. package/dist/core/loops/store.js +17 -0
  80. package/dist/core/loops/verbs.js +24 -1
  81. package/dist/core/markdown.js +8 -76
  82. package/dist/core/mcp-command-resolution.js +245 -0
  83. package/dist/core/memory-compactor.js +5 -3
  84. package/dist/core/memory-lifecycle.js +282 -0
  85. package/dist/core/merge-risk.js +150 -0
  86. package/dist/core/messaging.js +8 -1
  87. package/dist/core/migration.js +11 -1
  88. package/dist/core/observer-mode.js +26 -0
  89. package/dist/core/operations/memory-mutation.js +90 -65
  90. package/dist/core/operations/plan.js +27 -1
  91. package/dist/core/protocol-skills.js +210 -0
  92. package/dist/core/reflection-safety.js +6 -7
  93. package/dist/core/reputation.js +84 -2
  94. package/dist/core/runtime-signals.js +71 -9
  95. package/dist/core/runtime.js +84 -1
  96. package/dist/core/schema.js +114 -0
  97. package/dist/core/security-detectors.js +125 -0
  98. package/dist/core/security-extract.js +189 -0
  99. package/dist/core/security-guard.js +107 -29
  100. package/dist/core/security-packages.js +121 -0
  101. package/dist/core/security-scoring.js +76 -9
  102. package/dist/core/security.js +34 -2
  103. package/dist/core/sequence.js +11 -2
  104. package/dist/core/setup-flow.js +141 -13
  105. package/dist/core/staleness.js +72 -1
  106. package/dist/core/state.js +250 -54
  107. package/dist/core/store-resolution.js +19 -5
  108. package/dist/core/worktree.js +72 -8
  109. package/dist/facts.js +8 -8
  110. package/dist/facts.json +7 -7
  111. package/docs/PROTOCOL.md +223 -0
  112. package/docs/cli.md +11 -10
  113. package/docs/concepts/coordinator-runbook.md +129 -0
  114. package/docs/concepts/event-log-store-critique-A.md +333 -0
  115. package/docs/concepts/event-log-store-critique-B.md +353 -0
  116. package/docs/concepts/event-log-store-phase0-measurements.md +58 -0
  117. package/docs/concepts/event-log-store-proposal-A.md +365 -0
  118. package/docs/concepts/event-log-store-proposal-B.md +404 -0
  119. package/docs/concepts/event-log-store.md +928 -0
  120. package/docs/concepts/identity-model-proposal.md +371 -0
  121. package/docs/concepts/memory.md +5 -4
  122. package/docs/concepts/observer-protocol.md +361 -0
  123. package/docs/concepts/parallel-merge-protocol.md +71 -0
  124. package/docs/concepts/plans-and-claims.md +43 -0
  125. package/docs/concepts/skills.md +78 -0
  126. package/docs/concepts/workspace-bootstrapping.md +61 -0
  127. package/docs/integrations/agents.md +4 -4
  128. package/docs/integrations/cline.md +10 -11
  129. package/docs/integrations/codex.md +2 -2
  130. package/docs/integrations/continue.md +5 -5
  131. package/docs/integrations/copilot.md +14 -12
  132. package/docs/integrations/openclaw.md +7 -6
  133. package/docs/integrations/overview.md +7 -7
  134. package/docs/integrations/roo.md +3 -3
  135. package/docs/integrations/windsurf.md +6 -6
  136. package/docs/mcp-schema-changelog.md +29 -2
  137. package/docs/quickstart.md +48 -47
  138. package/docs/security.md +174 -15
  139. package/docs/storage.md +4 -2
  140. package/package.json +8 -6
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Journal-derived checkpoints (pln#543 Phase 3 / pln#566 Inc0, slice 1).
3
+ *
4
+ * A checkpoint is a snapshot of the materialized live-entity set at a covered
5
+ * `head_seq`, plus a manifest that BINDS it to the journal lineage. Per the
6
+ * round-3 review (NF1): checkpoints are derived from the JOURNAL ONLY — never
7
+ * from projection files, never consulting `projection_watermark` — so the
8
+ * journal is the single root of trust and there is no checkpoint↔watermark
9
+ * cycle. The cold-read fast path (a later slice) loads a checkpoint + replays
10
+ * only the sealed tail (head_seq+1..tail); on ANY trust-chain failure it falls
11
+ * back to projection files.
12
+ *
13
+ * This slice is strictly ADDITIVE: it builds/verifies/replays checkpoints but
14
+ * is not yet wired into the read path, so it carries zero read-path risk.
15
+ *
16
+ * @module
17
+ */
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import crypto from 'node:crypto';
21
+ import { journalDir, readJournalRecords, journalHeadSeq } from './journal.js';
22
+ import { applyRecordsToLive, projectLiveToState } from './materialize.js';
23
+ import { loadConfig } from '../config.js';
24
+ import { writeFileAtomic } from '../io.js';
25
+ import { nowISO } from '../ids.js';
26
+ const CHECKPOINT_SCHEMA_VERSION = 1;
27
+ export const BASELINE_CAPABILITIES = {
28
+ checkpointRead: false,
29
+ readReconcile: false,
30
+ tombstoneDelete: false,
31
+ perEntityPatch: false,
32
+ };
33
+ function checkpointsDir(cwd) {
34
+ return path.join(journalDir(cwd), 'checkpoints');
35
+ }
36
+ function sha256(s) {
37
+ return crypto.createHash('sha256').update(s).digest('hex');
38
+ }
39
+ /**
40
+ * pln#566 F4 guard: materialize (the reducer behind both checkpoint build and
41
+ * checkpoint+tail replay) only consumes inline `rec.payload`; it does NOT yet
42
+ * dereference `payload_ref`. So if ANY journal record externalized its payload,
43
+ * a checkpoint built/served from the journal would silently DROP that entity.
44
+ * Until payload_ref dereference lands, refuse to build or serve a checkpoint
45
+ * when externalized payloads exist — the read path falls back to projection
46
+ * files, which always carry the full content. Conservative (whole-journal, not
47
+ * just memory-tier) and cheap.
48
+ */
49
+ function journalHasExternalizedPayload(records) {
50
+ return records.some(r => r.payload_ref != null);
51
+ }
52
+ /** Identity of a record for journal-lineage binding: stable across reads, changes if the head record changes. */
53
+ function recordIdentity(rec) {
54
+ return sha256(`${rec.seq}|${rec.writer}|${rec.ts}|${rec.action}|${rec.item_type}|${rec.item_id ?? ''}`);
55
+ }
56
+ /** The record carrying the maximum seq (the covered head). Undefined for an empty journal. */
57
+ function headRecord(records) {
58
+ let head;
59
+ for (const rec of records) {
60
+ if (!head || rec.seq > head.seq)
61
+ head = rec;
62
+ }
63
+ return head;
64
+ }
65
+ function manifestPath(cwd, headSeq) {
66
+ return path.join(checkpointsDir(cwd), `${String(headSeq).padStart(12, '0')}.manifest.json`);
67
+ }
68
+ function snapshotPath(cwd, headSeq) {
69
+ return path.join(checkpointsDir(cwd), `${String(headSeq).padStart(12, '0')}.snapshot.json`);
70
+ }
71
+ /**
72
+ * Build a journal-derived checkpoint at the current journal head. Materializes
73
+ * the live entity set from the journal (NOT from projections), writes the
74
+ * snapshot blob, then publishes the manifest (the manifest is the commit
75
+ * point — an orphan snapshot without a manifest is ignored).
76
+ *
77
+ * NOTE (F6): this slice materializes from the full journal on demand. The
78
+ * incremental "latest verified checkpoint + sealed tail, built outside the hot
79
+ * lock" optimization is a later slice; on-demand build here is not on the hot
80
+ * write path.
81
+ */
82
+ export function createCheckpoint(cwd, capabilities = BASELINE_CAPABILITIES) {
83
+ // Cap to the COMMITTED head (meta.next_seq-1, published after fsync). The
84
+ // raw segment read is lock-free, so a concurrent append may have written —
85
+ // but not yet fsync'd/published — records beyond the committed head; excluding
86
+ // them keeps the manifest bound to durable journal state (codex review MED).
87
+ const committedHead = journalHeadSeq(cwd);
88
+ const records = readJournalRecords(cwd).filter(r => r.seq <= committedHead);
89
+ const head = headRecord(records);
90
+ if (!head)
91
+ return { created: false, reason: 'empty journal — nothing to checkpoint' };
92
+ if (journalHasExternalizedPayload(records)) {
93
+ return { created: false, reason: 'journal has externalized payload_ref records; materialize cannot dereference them yet (pln#566 F4)' };
94
+ }
95
+ const live = applyRecordsToLive(records, new Map());
96
+ const entities = [...live.values()];
97
+ const snapshot = JSON.stringify(entities);
98
+ const manifest = {
99
+ schema_version: CHECKPOINT_SCHEMA_VERSION,
100
+ store_id: loadConfig(cwd).project_id ?? 'unknown',
101
+ head_seq: head.seq,
102
+ head_identity: recordIdentity(head),
103
+ snapshot_sha256: sha256(snapshot),
104
+ entity_count: entities.length,
105
+ capability_vector: capabilities,
106
+ created_at: nowISO(),
107
+ };
108
+ const dir = checkpointsDir(cwd);
109
+ fs.mkdirSync(dir, { recursive: true });
110
+ // Snapshot first, manifest last (commit point).
111
+ writeFileAtomic(snapshotPath(cwd, head.seq), snapshot);
112
+ writeFileAtomic(manifestPath(cwd, head.seq), JSON.stringify(manifest));
113
+ return { created: true, manifest };
114
+ }
115
+ /** Default: create a fresh checkpoint once the journal has grown this many
116
+ * records past the last checkpoint head. Bounds the sealed tail so
117
+ * checkpointRead's gap-replay stays cheap, without checkpointing every persist. */
118
+ export const DEFAULT_CHECKPOINT_INTERVAL = 500;
119
+ /**
120
+ * Create a checkpoint ONLY if the journal has grown >= interval records past
121
+ * the last checkpoint head (cheap head-seq check; no full scan unless building).
122
+ * Intended for off-hot-path callers (session-start maintenance). Journal-derived
123
+ * (F6). No-op when the journal is off/empty or hasn't grown enough.
124
+ */
125
+ export function maybeCreateCheckpoint(cwd, interval = DEFAULT_CHECKPOINT_INTERVAL) {
126
+ const head = journalHeadSeq(cwd);
127
+ if (head === 0)
128
+ return { created: false, gap: 0, reason: 'journal empty/off' };
129
+ const last = loadLatestCheckpointManifest(cwd)?.head_seq ?? 0;
130
+ const gap = head - last;
131
+ if (gap < interval)
132
+ return { created: false, gap, reason: `gap ${gap} < interval ${interval}` };
133
+ const res = createCheckpoint(cwd);
134
+ return { created: res.created, gap, reason: res.reason };
135
+ }
136
+ /**
137
+ * Highest-head_seq manifest on disk, or undefined if none. Selects by NUMERIC
138
+ * head_seq parsed from the filename — lexicographic order on the 12-zero-pad
139
+ * breaks once seq exceeds 12 digits (codex review LOW).
140
+ */
141
+ export function loadLatestCheckpointManifest(cwd) {
142
+ const dir = checkpointsDir(cwd);
143
+ if (!fs.existsSync(dir))
144
+ return undefined;
145
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.manifest.json'))
146
+ .sort((a, b) => (parseInt(b, 10) || 0) - (parseInt(a, 10) || 0)); // numeric desc on the seq prefix
147
+ for (const f of files) {
148
+ try {
149
+ return JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
150
+ }
151
+ catch { /* skip corrupt manifest, try the next */ }
152
+ }
153
+ return undefined;
154
+ }
155
+ function isMaterializedEntityArray(v) {
156
+ return Array.isArray(v) && v.every(e => !!e && typeof e === 'object'
157
+ && typeof e.item_type === 'string'
158
+ && typeof e.item_id === 'string'
159
+ && !!e.payload && typeof e.payload === 'object');
160
+ }
161
+ /**
162
+ * Verify a checkpoint against an ALREADY-READ record set — never throws,
163
+ * validates snapshot SHAPE (not just sha256), and binds to the SAME journal
164
+ * view the caller will serve from (no verify/serve TOCTOU). WITHOUT reading
165
+ * projection files (F3). Returns the parsed entities when valid.
166
+ */
167
+ function verifyCheckpointAgainstRecords(manifest, snapshotRaw, records, cwd) {
168
+ try {
169
+ if (manifest.schema_version !== CHECKPOINT_SCHEMA_VERSION) {
170
+ return { valid: false, reason: `unsupported checkpoint schema_version ${manifest.schema_version}` };
171
+ }
172
+ if (sha256(snapshotRaw) !== manifest.snapshot_sha256) {
173
+ return { valid: false, reason: 'snapshot sha256 mismatch (corrupt/tampered blob)' };
174
+ }
175
+ let parsed;
176
+ try {
177
+ parsed = JSON.parse(snapshotRaw);
178
+ }
179
+ catch {
180
+ return { valid: false, reason: 'snapshot is not valid JSON' };
181
+ }
182
+ if (!isMaterializedEntityArray(parsed))
183
+ return { valid: false, reason: 'snapshot is not a MaterializedEntity[]' };
184
+ const expectedStore = loadConfig(cwd).project_id ?? 'unknown';
185
+ if (manifest.store_id !== expectedStore) {
186
+ return { valid: false, reason: `store_id mismatch (manifest ${manifest.store_id} vs ${expectedStore}) — copied/wrong-branch checkpoint` };
187
+ }
188
+ const head = records.find(r => r.seq === manifest.head_seq);
189
+ if (!head)
190
+ return { valid: false, reason: `head_seq ${manifest.head_seq} not found in journal` };
191
+ if (recordIdentity(head) !== manifest.head_identity) {
192
+ return { valid: false, reason: 'head record identity mismatch — journal lineage diverged from checkpoint' };
193
+ }
194
+ return { valid: true, entities: parsed };
195
+ }
196
+ catch (err) {
197
+ return { valid: false, reason: `verification error: ${err instanceof Error ? err.message : String(err)}` };
198
+ }
199
+ }
200
+ /**
201
+ * Public no-throw verification: snapshot integrity + shape, store binding, and
202
+ * journal-lineage binding. Reads the journal once. Returns valid:false on any
203
+ * failure (never throws) so callers fall back.
204
+ */
205
+ export function verifyCheckpoint(manifest, snapshotRaw, cwd) {
206
+ let records;
207
+ try {
208
+ records = readJournalRecords(cwd);
209
+ }
210
+ catch (err) {
211
+ return { valid: false, reason: `journal read failed: ${err instanceof Error ? err.message : String(err)}` };
212
+ }
213
+ const r = verifyCheckpointAgainstRecords(manifest, snapshotRaw, records, cwd);
214
+ return { valid: r.valid, reason: r.reason };
215
+ }
216
+ /**
217
+ * Materialize state from the latest VERIFIED checkpoint + the sealed tail
218
+ * (records with seq > head_seq), using the same reducer + projector as
219
+ * full-journal materialization. Reads the journal exactly ONCE and uses that
220
+ * single view for verification, the F4 payload_ref guard, and the tail replay
221
+ * (no TOCTOU). Returns null on any failure (caller falls back to projections).
222
+ */
223
+ export function materializeStateFromCheckpoint(cwd) {
224
+ const manifest = loadLatestCheckpointManifest(cwd);
225
+ if (!manifest)
226
+ return null;
227
+ let snapshotRaw;
228
+ try {
229
+ snapshotRaw = fs.readFileSync(snapshotPath(cwd, manifest.head_seq), 'utf-8');
230
+ }
231
+ catch {
232
+ return null; // orphan manifest without a readable snapshot
233
+ }
234
+ let records;
235
+ // Same committed-head cap as the build path: only durable records (seq <=
236
+ // meta.next_seq-1) drive verification, the F4 guard, and the tail replay.
237
+ try {
238
+ const committedHead = journalHeadSeq(cwd);
239
+ records = readJournalRecords(cwd).filter(r => r.seq <= committedHead);
240
+ }
241
+ catch {
242
+ return null;
243
+ }
244
+ const verdict = verifyCheckpointAgainstRecords(manifest, snapshotRaw, records, cwd);
245
+ if (!verdict.valid || !verdict.entities)
246
+ return null;
247
+ // F4 guard: an externalized payload (which materialize can't deref) means the
248
+ // checkpoint/tail would drop entities — refuse to serve, fall back.
249
+ if (journalHasExternalizedPayload(records))
250
+ return null;
251
+ const live = new Map();
252
+ for (const e of verdict.entities)
253
+ live.set(`${e.item_type}:${e.item_id}`, e);
254
+ // Replay only the sealed tail (head_seq+1..end) from the SAME record view.
255
+ applyRecordsToLive(records.filter(r => r.seq > manifest.head_seq), live);
256
+ return projectLiveToState(live);
257
+ }
258
+ //# sourceMappingURL=checkpoint.js.map
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Journal genesis migration + rollback (pln#543 step 4, spec §4 phase 1).
3
+ *
4
+ * Genesis seeds the v2 journal from the current v1 projection store: one
5
+ * `journal_note` kind `genesis` followed by one `backfill` record per live
6
+ * memory entity (entity_rev 1), all under a single lock hold. It is the
7
+ * baseline the journal grows from and that materialize/verify check against.
8
+ *
9
+ * Discipline (matches the house upgrade rule, feedback_no_init_force):
10
+ * - MANDATORY backup before writing — projections copied to a timestamped
11
+ * park dir; nothing is ever deleted.
12
+ * - Refuses to clobber an existing genesis unless `force` (which parks the
13
+ * prior journal first — park-don't-delete).
14
+ * - Rollback parks the journal directory; projections are untouched (in
15
+ * dual mode they were always the source of truth), so it is a safe,
16
+ * reversible "stop using the journal" operation.
17
+ */
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { memoryDir } from '../io.js';
21
+ import { loadState } from '../state.js';
22
+ import { preparePersistedDocument } from '../migration.js';
23
+ import { nowISO } from '../ids.js';
24
+ import { logger } from '../logger.js';
25
+ import { forceAppendJournalRecords, journalDir, readJournalRecords, resolveJournalMode, } from './journal.js';
26
+ import { REGISTRY_FAMILIES } from './registry-post-image.js';
27
+ import { listClaims } from '../claims.js';
28
+ import { listAssignments } from '../assignments.js';
29
+ import { listAgentRuns } from '../agentruns.js';
30
+ import { listActionRequired } from '../actions.js';
31
+ import { listCandidates } from '../candidates.js';
32
+ import { listSequences } from '../sequence.js';
33
+ import { listSharedJournaledRuntimeNotes } from '../runtime.js';
34
+ const MEMORY_FAMILIES = [
35
+ { collection: 'active_constraints', itemType: 'constraint' },
36
+ { collection: 'recent_decisions', itemType: 'decision' },
37
+ { collection: 'known_traps', itemType: 'trap' },
38
+ { collection: 'open_handoffs', itemType: 'handoff' },
39
+ { collection: 'plan_items', itemType: 'plan' },
40
+ ];
41
+ /** True once a `journal_note` kind `genesis` exists in the journal. */
42
+ export function hasGenesis(cwd) {
43
+ return readJournalRecords(cwd).some(r => r.action === 'journal_note' && r.payload?.kind === 'genesis');
44
+ }
45
+ /**
46
+ * Copy the live projection store to a timestamped park dir. Returns the path.
47
+ * `ts` is injected (the codebase forbids Date.now()/new Date() in some layers;
48
+ * callers pass nowISO()-derived stamps) — defaults to a lexically-sortable ISO.
49
+ */
50
+ export function backupStore(cwd, stamp) {
51
+ const base = memoryDir(cwd);
52
+ const memorySrc = path.join(base, 'memory');
53
+ const safeStamp = stamp.replace(/[:.]/g, '-');
54
+ const backupDir = path.join(base, 'migration-backups', `genesis-${safeStamp}`);
55
+ fs.mkdirSync(backupDir, { recursive: true });
56
+ if (fs.existsSync(memorySrc)) {
57
+ fs.cpSync(memorySrc, path.join(backupDir, 'memory'), { recursive: true });
58
+ }
59
+ return backupDir;
60
+ }
61
+ export function runGenesisMigration(options = {}) {
62
+ const cwd = options.cwd ?? process.cwd();
63
+ const state = loadState(cwd);
64
+ const perFamily = {};
65
+ const backfill = [];
66
+ for (const { collection, itemType } of MEMORY_FAMILIES) {
67
+ const items = state[collection];
68
+ perFamily[itemType] = items.length;
69
+ for (const item of items) {
70
+ backfill.push({
71
+ action: 'backfill',
72
+ item_type: itemType,
73
+ item_id: item.id,
74
+ agent: 'system',
75
+ payload: preparePersistedDocument(itemType, item),
76
+ });
77
+ }
78
+ }
79
+ const total = backfill.length;
80
+ if (options.dryRun) {
81
+ return { status: 'dry_run', backfilled: total, per_family: perFamily };
82
+ }
83
+ if (hasGenesis(cwd)) {
84
+ if (!options.force) {
85
+ return { status: 'already_present', backfilled: 0, per_family: perFamily };
86
+ }
87
+ parkJournal(cwd, nowISO());
88
+ }
89
+ // Genesis is the phase-1 (dual) seed (spec §4). Running it with the flag
90
+ // off lays the seed but mutations after will not dual-write — the journal
91
+ // then silently diverges from projections until BRAINCLAW_JOURNAL_MODE is
92
+ // flipped to dual. Warn so the operator flips the flag (or accepts the
93
+ // seed-then-flip sequence deliberately).
94
+ if (resolveJournalMode(cwd) === 'off') {
95
+ logger.warn('runGenesisMigration: BRAINCLAW_JOURNAL_MODE=off — genesis will seed the journal, but subsequent mutations will not dual-write, so the journal will diverge from projections until you set BRAINCLAW_JOURNAL_MODE=dual.');
96
+ }
97
+ const backupPath = backupStore(cwd, nowISO());
98
+ // genesis note first, then the backfill batch — all under one lock hold via
99
+ // a single forced append call (appendLocked stamps them with consecutive seqs).
100
+ const genesisNote = {
101
+ action: 'journal_note',
102
+ item_type: 'journal',
103
+ agent: 'system',
104
+ payload: {
105
+ kind: 'genesis',
106
+ migrated_from: 'v1',
107
+ backfill_count: total,
108
+ per_family: perFamily,
109
+ backup_path: backupPath,
110
+ at: nowISO(),
111
+ },
112
+ };
113
+ const written = forceAppendJournalRecords([genesisNote, ...backfill], cwd);
114
+ // Locate the genesis note by action+kind, not array position: appendLocked
115
+ // can prepend a `seq_repair` or `journal_note kind torn_tail_adjudicated`
116
+ // when meta is stale or the prior segment tail is torn, which would shift
117
+ // written[0] off the genesis note and report the wrong genesis_seq.
118
+ const genesisSeq = written.find(r => r.action === 'journal_note' && r.payload?.kind === 'genesis')?.seq;
119
+ logger.debug(`journal genesis: ${total} entities backfilled at seq ${genesisSeq}, backup ${backupPath}`);
120
+ return { status: 'migrated', genesis_seq: genesisSeq, backfilled: total, backup_path: backupPath, per_family: perFamily };
121
+ }
122
+ // ── Registry genesis supplement (pln#568 slice 3 — cutover signal O2) ──────
123
+ /**
124
+ * The registry / coordination families backfilled by the registry genesis
125
+ * supplement, each mapped to its projection reader. Mirrors verify.ts's
126
+ * VERIFIED_REGISTRY_FAMILIES so a supplemented store passes `doctor
127
+ * --verify-journal` with zero registry drift. Runtime notes are shared-only
128
+ * (private/machine never enter the shared journal, pln#568).
129
+ */
130
+ const REGISTRY_GENESIS_FAMILIES = [
131
+ { family: 'claim', list: listClaims },
132
+ // listActionRequired performs the server's sweep-on-read expiration, which can
133
+ // update dependent assignments/runs. Run it before snapshotting those families
134
+ // so the supplement cannot append stale assignment/run post-images after the
135
+ // sweep's fresh journal records. Dry-run disables the sweep to honor the
136
+ // no-write contract.
137
+ { family: 'action', list: (cwd, options) => listActionRequired(cwd, {}, { expireStale: !options.dryRun }) },
138
+ { family: 'assignment', list: (cwd) => listAssignments(cwd) },
139
+ { family: 'agent_run', list: (cwd) => listAgentRuns(cwd) },
140
+ { family: 'candidate', list: (cwd) => listCandidates('pending', cwd) },
141
+ { family: 'runtime_note', list: listSharedJournaledRuntimeNotes },
142
+ { family: 'sequence', list: listSequences },
143
+ ];
144
+ /** True once a `journal_note` kind `registry_genesis` exists — the cutover
145
+ * signal (O2) the observer reads to trust the journal as AUTHORITATIVE for the
146
+ * registry families (drop the board_summary MCP seed). */
147
+ export function hasRegistryGenesis(cwd) {
148
+ return readJournalRecords(cwd).some(r => r.action === 'journal_note' && r.payload?.kind === 'registry_genesis');
149
+ }
150
+ /**
151
+ * Backfill the registry / coordination families into the journal and emit the
152
+ * `registry_genesis` cutover marker (pln#568 slice 3). INCREMENTAL by design:
153
+ * it appends to the existing journal (preserving the memory genesis + all
154
+ * accumulated post-image history) rather than parking/re-seeding — a re-genesis
155
+ * would reset seq to 1 and break live observers' seq cursors. Idempotent: a
156
+ * second run no-ops once the marker is present.
157
+ *
158
+ * The marker is the safe authority signal: an observer must not switch a
159
+ * registry family from the MCP seed to the journal until EVERY pre-existing
160
+ * entity has a post-image, else it undercounts (the trp#559 badge regression).
161
+ * This backfill establishes that guarantee, then the marker announces it.
162
+ */
163
+ export function runRegistryGenesisSupplement(options = {}) {
164
+ const cwd = options.cwd ?? process.cwd();
165
+ const perFamily = {};
166
+ const backfill = [];
167
+ for (const { family, list } of REGISTRY_GENESIS_FAMILIES) {
168
+ const spec = REGISTRY_FAMILIES[family];
169
+ const items = list(cwd, options);
170
+ perFamily[spec.journalItemType] = items.length;
171
+ for (const item of items) {
172
+ backfill.push({
173
+ action: 'backfill',
174
+ item_type: spec.journalItemType,
175
+ item_id: item.id,
176
+ agent: 'system',
177
+ payload: preparePersistedDocument(spec.docType, item),
178
+ });
179
+ }
180
+ }
181
+ const total = backfill.length;
182
+ if (options.dryRun) {
183
+ return { status: 'dry_run', backfilled: total, per_family: perFamily };
184
+ }
185
+ if (hasRegistryGenesis(cwd)) {
186
+ return { status: 'already_present', backfilled: 0, per_family: perFamily };
187
+ }
188
+ const marker = {
189
+ action: 'journal_note',
190
+ item_type: 'journal',
191
+ agent: 'system',
192
+ payload: { kind: 'registry_genesis', backfill_count: total, per_family: perFamily, at: nowISO() },
193
+ };
194
+ forceAppendJournalRecords([...backfill, marker], cwd);
195
+ logger.debug(`registry genesis: ${total} registry entities backfilled, cutover marker emitted`);
196
+ return { status: 'migrated', backfilled: total, per_family: perFamily };
197
+ }
198
+ /**
199
+ * Park the journal directory (events/) to a timestamped archive — the
200
+ * reversible "stop using the journal" operation. Projections are untouched.
201
+ * Returns the park path, or undefined if there was no journal.
202
+ */
203
+ export function parkJournal(cwd, stamp) {
204
+ const dir = journalDir(cwd);
205
+ if (!fs.existsSync(dir))
206
+ return undefined;
207
+ const safeStamp = stamp.replace(/[:.]/g, '-');
208
+ const parked = path.join(memoryDir(cwd), 'migration-backups', `journal-parked-${safeStamp}`);
209
+ fs.mkdirSync(path.dirname(parked), { recursive: true });
210
+ fs.renameSync(dir, parked);
211
+ return parked;
212
+ }
213
+ export function rollbackJournal(options = {}) {
214
+ const cwd = options.cwd ?? process.cwd();
215
+ const parked = parkJournal(cwd, nowISO());
216
+ return parked
217
+ ? { status: 'rolled_back', parked_path: parked }
218
+ : { status: 'nothing_to_roll_back' };
219
+ }
220
+ //# sourceMappingURL=genesis.js.map