wyrm-mcp 7.3.2 → 7.3.3

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 (184) hide show
  1. package/README.md +8 -14
  2. package/dist/activation.js +59 -1
  3. package/dist/agent-daemon.js +281 -4
  4. package/dist/agent-loop.js +332 -7
  5. package/dist/analytics.js +236 -13
  6. package/dist/attribution.js +49 -1
  7. package/dist/audit.js +457 -2
  8. package/dist/auto-capture.js +138 -3
  9. package/dist/auto-orchestrator.js +325 -1
  10. package/dist/autoconfig.d.ts +50 -0
  11. package/dist/autoconfig.d.ts.map +1 -1
  12. package/dist/autoconfig.js +1115 -39
  13. package/dist/autoconfig.js.map +1 -1
  14. package/dist/buddy-runner.js +109 -1
  15. package/dist/buddy.js +564 -14
  16. package/dist/build-flags.js +15 -1
  17. package/dist/capabilities.js +183 -3
  18. package/dist/capture.js +56 -1
  19. package/dist/causality.js +148 -8
  20. package/dist/cli.js +281 -20
  21. package/dist/cloud/cli.js +541 -5
  22. package/dist/cloud/client.js +221 -1
  23. package/dist/cloud/crypto.js +85 -1
  24. package/dist/cloud/machine-id.js +113 -2
  25. package/dist/cloud/recovery.js +60 -1
  26. package/dist/cloud/sync-engine.js +543 -7
  27. package/dist/cloud-backup.js +579 -5
  28. package/dist/cloud-profile.js +138 -1
  29. package/dist/cloud-sync-entrypoint.js +47 -1
  30. package/dist/cloud-sync.js +309 -2
  31. package/dist/connectors/bridge-source.d.ts +46 -0
  32. package/dist/connectors/bridge-source.d.ts.map +1 -0
  33. package/dist/connectors/bridge-source.js +77 -0
  34. package/dist/connectors/bridge-source.js.map +1 -0
  35. package/dist/connectors/index.d.ts +24 -0
  36. package/dist/connectors/index.d.ts.map +1 -0
  37. package/dist/connectors/index.js +69 -0
  38. package/dist/connectors/index.js.map +1 -0
  39. package/dist/connectors/ingest.d.ts +16 -0
  40. package/dist/connectors/ingest.d.ts.map +1 -0
  41. package/dist/connectors/ingest.js +116 -0
  42. package/dist/connectors/ingest.js.map +1 -0
  43. package/dist/connectors/types.d.ts +99 -0
  44. package/dist/connectors/types.d.ts.map +1 -0
  45. package/dist/connectors/types.js +17 -0
  46. package/dist/connectors/types.js.map +1 -0
  47. package/dist/constellation.js +168 -12
  48. package/dist/content-signature.js +45 -1
  49. package/dist/context-build-budgeted.js +144 -4
  50. package/dist/context-ranking.js +69 -1
  51. package/dist/crypto.js +179 -1
  52. package/dist/daemon-write-endpoint.js +290 -1
  53. package/dist/daemon-writer.js +406 -2
  54. package/dist/database.js +1278 -53
  55. package/dist/deprecations.js +162 -2
  56. package/dist/design.js +141 -13
  57. package/dist/event-replication.js +112 -1
  58. package/dist/events-sse.js +43 -7
  59. package/dist/events.js +238 -6
  60. package/dist/failure-patterns.d.ts +107 -0
  61. package/dist/failure-patterns.d.ts.map +1 -1
  62. package/dist/failure-patterns.js +924 -43
  63. package/dist/failure-patterns.js.map +1 -1
  64. package/dist/federation.js +236 -12
  65. package/dist/goals.js +101 -13
  66. package/dist/golden.js +355 -3
  67. package/dist/handlers/agent.js +165 -4
  68. package/dist/handlers/alias-adapters.js +129 -1
  69. package/dist/handlers/aliases.js +171 -1
  70. package/dist/handlers/audit.js +87 -1
  71. package/dist/handlers/boundary.js +221 -1
  72. package/dist/handlers/capture.js +1114 -73
  73. package/dist/handlers/causality.js +119 -9
  74. package/dist/handlers/cloud.js +382 -85
  75. package/dist/handlers/companion.js +459 -28
  76. package/dist/handlers/datalake.js +187 -7
  77. package/dist/handlers/dispatch-context.js +22 -0
  78. package/dist/handlers/entity.js +256 -25
  79. package/dist/handlers/events.js +335 -16
  80. package/dist/handlers/failure.d.ts.map +1 -1
  81. package/dist/handlers/failure.js +408 -13
  82. package/dist/handlers/failure.js.map +1 -1
  83. package/dist/handlers/goals.js +296 -4
  84. package/dist/handlers/intelligence.js +681 -126
  85. package/dist/handlers/invoicing.js +70 -1
  86. package/dist/handlers/mcpclient.js +137 -6
  87. package/dist/handlers/orchestration.js +125 -40
  88. package/dist/handlers/output-schemas.js +24 -1
  89. package/dist/handlers/presence.js +99 -3
  90. package/dist/handlers/project.js +182 -28
  91. package/dist/handlers/prompts.js +157 -6
  92. package/dist/handlers/quest.js +224 -4
  93. package/dist/handlers/recall.js +237 -13
  94. package/dist/handlers/registry.js +167 -1
  95. package/dist/handlers/resources.js +288 -1
  96. package/dist/handlers/review.js +74 -11
  97. package/dist/handlers/run.js +498 -16
  98. package/dist/handlers/search.js +338 -15
  99. package/dist/handlers/session.js +643 -31
  100. package/dist/handlers/share.js +184 -8
  101. package/dist/handlers/shims.js +464 -1
  102. package/dist/handlers/skill.js +449 -67
  103. package/dist/handlers/survivors.js +120 -1
  104. package/dist/handlers/symbols.js +109 -8
  105. package/dist/handlers/syncops.js +302 -4
  106. package/dist/handlers/types.js +27 -1
  107. package/dist/harvest.js +191 -5
  108. package/dist/hours.js +156 -7
  109. package/dist/http-auth.js +321 -3
  110. package/dist/http-fast.js +1302 -22
  111. package/dist/icons.js +47 -1
  112. package/dist/importers.js +268 -1
  113. package/dist/index.js +840 -2
  114. package/dist/indexer.js +145 -4
  115. package/dist/intelligence.js +261 -31
  116. package/dist/internal-dispatch.js +212 -3
  117. package/dist/keyset.js +110 -1
  118. package/dist/knowledge-graph.js +176 -12
  119. package/dist/license.js +441 -2
  120. package/dist/logger.js +199 -2
  121. package/dist/maintenance.js +148 -2
  122. package/dist/mcp-client.js +262 -6
  123. package/dist/memory-artifacts.js +596 -32
  124. package/dist/migrate-prompt.js +124 -2
  125. package/dist/migrations.d.ts.map +1 -1
  126. package/dist/migrations.js +799 -42
  127. package/dist/migrations.js.map +1 -1
  128. package/dist/performance.js +228 -1
  129. package/dist/presence.js +140 -11
  130. package/dist/priority-embed.js +164 -5
  131. package/dist/providers/embedding-provider.js +196 -1
  132. package/dist/readonly-gate.js +29 -1
  133. package/dist/receipt.js +43 -1
  134. package/dist/rehydration.js +157 -9
  135. package/dist/reindex.js +88 -1
  136. package/dist/render-target.js +544 -21
  137. package/dist/render.js +280 -4
  138. package/dist/repl-guard.js +173 -1
  139. package/dist/replication-daemon-entrypoint.js +31 -1
  140. package/dist/replication-daemon.js +262 -2
  141. package/dist/rerank.js +142 -1
  142. package/dist/resilience.js +591 -1
  143. package/dist/reverse-bridge.js +360 -5
  144. package/dist/security.js +244 -1
  145. package/dist/session-seen.js +51 -3
  146. package/dist/setup.js +260 -1
  147. package/dist/skill-author.js +168 -5
  148. package/dist/spec-kit.js +191 -1
  149. package/dist/sqlite-busy.js +154 -1
  150. package/dist/statusline.js +315 -11
  151. package/dist/sub-agent.js +262 -13
  152. package/dist/summarizer.js +139 -13
  153. package/dist/symbols.js +283 -7
  154. package/dist/sync.js +359 -5
  155. package/dist/tasks-dispatch.js +84 -1
  156. package/dist/tasks.js +282 -1
  157. package/dist/token-budget.js +143 -1
  158. package/dist/tool-analytics.js +129 -7
  159. package/dist/tool-annotations.js +365 -1
  160. package/dist/tool-manifest-v2.json +1 -1
  161. package/dist/tool-manifest.json +1 -1
  162. package/dist/tool-profiles.js +75 -1
  163. package/dist/trace-harvest.js +244 -6
  164. package/dist/types.js +30 -1
  165. package/dist/ui-dashboard.js +50 -41
  166. package/dist/ulid.js +81 -1
  167. package/dist/usage-tracker.js +66 -1
  168. package/dist/validate.js +129 -1
  169. package/dist/vault.js +534 -1
  170. package/dist/vector-init.js +67 -1
  171. package/dist/vectors.js +184 -3
  172. package/dist/version-check.js +136 -4
  173. package/dist/visibility.js +155 -19
  174. package/dist/wyrm-cli.js +2845 -101
  175. package/dist/wyrm-cli.js.map +1 -1
  176. package/dist/wyrm-guard.d.ts.map +1 -1
  177. package/dist/wyrm-guard.js +475 -14
  178. package/dist/wyrm-guard.js.map +1 -1
  179. package/dist/wyrm-loop.js +150 -3
  180. package/dist/wyrm-manifest.json +1 -1
  181. package/dist/wyrm-statusline-daemon.js +11 -1
  182. package/dist/wyrm-statusline.js +56 -4
  183. package/dist/wyrm-ui.js +77 -9
  184. package/package.json +1 -1
@@ -1,4 +1,22 @@
1
- const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, quests, context, data_lake, skills, FTS5",up:e=>{e.exec(`
1
+ /**
2
+ * Wyrm Database Migrations — Versioned schema management
3
+ *
4
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
5
+ * @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
6
+ *
7
+ * Each migration is a function that receives a better-sqlite3 Database instance.
8
+ * Migrations run in order and are tracked in the schema_versions table.
9
+ * Once applied, a migration is never re-run.
10
+ */
11
+ /**
12
+ * All migrations in order. Append new migrations to the end — never modify existing ones.
13
+ */
14
+ export const migrations = [
15
+ {
16
+ version: 1,
17
+ description: 'Baseline schema — projects, sessions, quests, context, data_lake, skills, FTS5',
18
+ up: (db) => {
19
+ db.exec(`
2
20
  -- Core tables
3
21
  CREATE TABLE IF NOT EXISTS projects (
4
22
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -197,7 +215,14 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
197
215
  VALUES('delete', old.id, old.key, old.value);
198
216
  INSERT INTO data_lake_fts(rowid, key, value) VALUES (new.id, new.key, new.value);
199
217
  END;
200
- `)}},{version:2,description:"Add usage_events table for analytics tracking",up:e=>{e.exec(`
218
+ `);
219
+ },
220
+ },
221
+ {
222
+ version: 2,
223
+ description: 'Add usage_events table for analytics tracking',
224
+ up: (db) => {
225
+ db.exec(`
201
226
  CREATE TABLE IF NOT EXISTS usage_events (
202
227
  id INTEGER PRIMARY KEY AUTOINCREMENT,
203
228
  tool_name TEXT NOT NULL,
@@ -210,7 +235,34 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
210
235
  timestamp TEXT DEFAULT (datetime('now'))
211
236
  );
212
237
  CREATE INDEX IF NOT EXISTS idx_usage_timestamp ON usage_events(timestamp);
213
- `)}},{version:3,description:"Add visibility and created_by columns for future multi-user support",up:e=>{const E=["sessions","quests","context","data_lake"];for(const t of E){const i=e.prepare(`PRAGMA table_info(${t})`).all().map(T=>T.name);i.includes("visibility")||e.exec(`ALTER TABLE ${t} ADD COLUMN visibility TEXT DEFAULT 'private'`),i.includes("created_by")||e.exec(`ALTER TABLE ${t} ADD COLUMN created_by TEXT DEFAULT 'local'`)}}},{version:4,description:"Knowledge graph \u2014 entities, aliases, relationships with FTS5",up:e=>{e.exec(`
238
+ `);
239
+ },
240
+ },
241
+ {
242
+ version: 3,
243
+ description: 'Add visibility and created_by columns for future multi-user support',
244
+ up: (db) => {
245
+ // Add visibility + created_by to tables that will need them.
246
+ // Using ALTER TABLE with defaults so existing rows get sensible values.
247
+ const tables = ['sessions', 'quests', 'context', 'data_lake'];
248
+ for (const table of tables) {
249
+ // Check if column already exists (idempotent)
250
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all();
251
+ const colNames = cols.map(c => c.name);
252
+ if (!colNames.includes('visibility')) {
253
+ db.exec(`ALTER TABLE ${table} ADD COLUMN visibility TEXT DEFAULT 'private'`);
254
+ }
255
+ if (!colNames.includes('created_by')) {
256
+ db.exec(`ALTER TABLE ${table} ADD COLUMN created_by TEXT DEFAULT 'local'`);
257
+ }
258
+ }
259
+ },
260
+ },
261
+ {
262
+ version: 4,
263
+ description: 'Knowledge graph — entities, aliases, relationships with FTS5',
264
+ up: (db) => {
265
+ db.exec(`
214
266
  -- Entities: named things that can be linked together
215
267
  CREATE TABLE IF NOT EXISTS entities (
216
268
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -239,7 +291,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
239
291
  CREATE INDEX IF NOT EXISTS idx_entity_aliases_entity ON entity_aliases(entity_id);
240
292
  CREATE INDEX IF NOT EXISTS idx_entity_aliases_alias ON entity_aliases(alias);
241
293
 
242
- -- Relationships between entities \u2014 directed, typed, with provenance
294
+ -- Relationships between entities directed, typed, with provenance
243
295
  CREATE TABLE IF NOT EXISTS relationships (
244
296
  id INTEGER PRIMARY KEY AUTOINCREMENT,
245
297
  project_id INTEGER NOT NULL,
@@ -285,7 +337,14 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
285
337
  INSERT INTO entities_fts(rowid, name, type, metadata)
286
338
  VALUES (new.id, new.name, new.type, new.metadata);
287
339
  END;
288
- `)}},{version:5,description:"Memory artifacts \u2014 distilled knowledge for intelligence amplification",up:e=>{e.exec(`
340
+ `);
341
+ },
342
+ },
343
+ {
344
+ version: 5,
345
+ description: 'Memory artifacts — distilled knowledge for intelligence amplification',
346
+ up: (db) => {
347
+ db.exec(`
289
348
  -- Memory artifacts: distilled problem-solution records, lessons, patterns, anti-patterns.
290
349
  -- Used by wyrm_context_build to assemble task-specific memory briefs for AI models.
291
350
  CREATE TABLE IF NOT EXISTS memory_artifacts (
@@ -340,7 +399,14 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
340
399
  INSERT INTO memory_artifacts_fts(rowid, problem, constraints, validated_fix, why_it_worked, tags)
341
400
  VALUES (new.id, new.problem, new.constraints, new.validated_fix, new.why_it_worked, new.tags);
342
401
  END;
343
- `)}},{version:6,description:"Intelligence amplification \u2014 ground truths, reasoning scaffolds, distillation review queue",up:e=>{e.exec(`
402
+ `);
403
+ },
404
+ },
405
+ {
406
+ version: 6,
407
+ description: 'Intelligence amplification — ground truths, reasoning scaffolds, distillation review queue',
408
+ up: (db) => {
409
+ db.exec(`
344
410
  -- Ground truths: authoritative, versioned project facts.
345
411
  -- Use is_current=1 for the live value. Superseded rows kept for history.
346
412
  CREATE TABLE IF NOT EXISTS ground_truths (
@@ -410,12 +476,33 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
410
476
  -- needs_review=1 means it was auto-created by wyrm_distill and awaits approval
411
477
  ALTER TABLE memory_artifacts ADD COLUMN needs_review INTEGER DEFAULT 0;
412
478
  CREATE INDEX IF NOT EXISTS idx_artifacts_review ON memory_artifacts(project_id, needs_review);
413
- `)}},{version:7,description:"Ground truth TTL \u2014 temporal decay support for staleness detection",up:e=>{e.exec(`
479
+ `);
480
+ },
481
+ },
482
+ {
483
+ version: 7,
484
+ description: 'Ground truth TTL — temporal decay support for staleness detection',
485
+ up: (db) => {
486
+ db.exec(`
414
487
  ALTER TABLE ground_truths ADD COLUMN ttl_days INTEGER CHECK(ttl_days IS NULL OR ttl_days > 0);
415
- `)}},{version:8,description:"Memory artifact decay \u2014 access tracking for auto-prune",up:e=>{e.exec(`
488
+ `);
489
+ },
490
+ },
491
+ {
492
+ version: 8,
493
+ description: 'Memory artifact decay — access tracking for auto-prune',
494
+ up: (db) => {
495
+ db.exec(`
416
496
  ALTER TABLE memory_artifacts ADD COLUMN last_accessed_at TEXT DEFAULT (datetime('now'));
417
497
  ALTER TABLE memory_artifacts ADD COLUMN access_count INTEGER NOT NULL DEFAULT 0;
418
- `)}},{version:9,description:"v3.9.0 \u2014 failure patterns, agent presence, causality, symbol index, tool analytics",up:e=>{e.exec(`
498
+ `);
499
+ },
500
+ },
501
+ {
502
+ version: 9,
503
+ description: 'v3.9.0 — failure patterns, agent presence, causality, symbol index, tool analytics',
504
+ up: (db) => {
505
+ db.exec(`
419
506
  -- ============================================================
420
507
  -- Tier 1.1: Counter-pattern detection
421
508
  -- Records edits/commands that failed so the predictive push can
@@ -562,9 +649,16 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
562
649
  CREATE INDEX IF NOT EXISTS idx_toolcall_name ON tool_call_log(tool_name, called_at DESC);
563
650
  CREATE INDEX IF NOT EXISTS idx_toolcall_project ON tool_call_log(project_id, called_at DESC);
564
651
  CREATE INDEX IF NOT EXISTS idx_toolcall_success ON tool_call_log(success, called_at DESC);
565
- `)}},{version:10,description:"v4.0.0 \u2014 federated sync, hash-chained audit, sub-agent embedding (wyrm_ask), session-rehydration helpers",up:e=>{e.exec(`
652
+ `);
653
+ },
654
+ },
655
+ {
656
+ version: 10,
657
+ description: 'v4.0.0 — federated sync, hash-chained audit, sub-agent embedding (wyrm_ask), session-rehydration helpers',
658
+ up: (db) => {
659
+ db.exec(`
566
660
  -- ============================================================
567
- -- Tier 2.8: Federated team Wyrm \u2014 selective sharing
661
+ -- Tier 2.8: Federated team Wyrm selective sharing
568
662
  -- A single "is_shared" flag on the major user-facing tables.
569
663
  -- A team server pulls only is_shared=1 rows during sync.
570
664
  -- ============================================================
@@ -614,7 +708,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
614
708
  ON sync_log(remote_origin, direction, synced_at DESC);
615
709
 
616
710
  -- ============================================================
617
- -- Tier 3.9: Compliance audit trail \u2014 hash-chained event log
711
+ -- Tier 3.9: Compliance audit trail hash-chained event log
618
712
  -- Each row references the hash of the previous row. Tampering with
619
713
  -- any historical entry breaks the chain. Combined with optional
620
714
  -- Ed25519 signing this provides a SOC2/HIPAA-grade audit surface.
@@ -636,7 +730,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
636
730
  CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_log(actor, logged_at DESC);
637
731
 
638
732
  -- ============================================================
639
- -- Tier 3.10: Sub-agent embedding \u2014 wyrm_ask query log
733
+ -- Tier 3.10: Sub-agent embedding wyrm_ask query log
640
734
  -- Tracks every wyrm_ask invocation: query, context window, model,
641
735
  -- response, latency, tokens. Cost tracking + answer quality
642
736
  -- analysis. Doubles as a public-facing audit if wyrm_ask is
@@ -659,9 +753,16 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
659
753
  );
660
754
  CREATE INDEX IF NOT EXISTS idx_llm_project ON llm_query_log(project_id, asked_at DESC);
661
755
  CREATE INDEX IF NOT EXISTS idx_llm_model ON llm_query_log(model, asked_at DESC);
662
- `)}},{version:11,description:"v5.0.0 \u2014 goals, agent loop, outbound MCP clients, action log",up:e=>{e.exec(`
756
+ `);
757
+ },
758
+ },
759
+ {
760
+ version: 11,
761
+ description: 'v5.0.0 — goals, agent loop, outbound MCP clients, action log',
762
+ up: (db) => {
763
+ db.exec(`
663
764
  -- ============================================================
664
- -- v5.0.0: Goals \u2014 persistent objectives Wyrm pursues
765
+ -- v5.0.0: Goals persistent objectives Wyrm pursues
665
766
  -- ============================================================
666
767
  CREATE TABLE IF NOT EXISTS goals (
667
768
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -709,7 +810,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
709
810
  CREATE INDEX IF NOT EXISTS idx_goal_iter_goal ON goal_iterations(goal_id, iteration_num);
710
811
 
711
812
  -- ============================================================
712
- -- v5.0.0: Outbound MCP clients \u2014 Wyrm calls other servers
813
+ -- v5.0.0: Outbound MCP clients Wyrm calls other servers
713
814
  -- Config rows for MCP servers Wyrm should connect to (stdio).
714
815
  -- ============================================================
715
816
  CREATE TABLE IF NOT EXISTS mcp_client_configs (
@@ -717,7 +818,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
717
818
  server_name TEXT UNIQUE NOT NULL, -- 'github', 'slack', 'phantomdragon', etc.
718
819
  command TEXT NOT NULL, -- 'npx' or absolute path
719
820
  args TEXT, -- JSON array of args
720
- env TEXT, -- JSON object of env vars (NEVER store secrets here in plaintext for prod \u2014 but local dev is fine)
821
+ env TEXT, -- JSON object of env vars (NEVER store secrets here in plaintext for prod but local dev is fine)
721
822
  enabled INTEGER NOT NULL DEFAULT 1,
722
823
  added_at TEXT DEFAULT (datetime('now')),
723
824
  last_used_at TEXT,
@@ -738,7 +839,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
738
839
  CREATE INDEX IF NOT EXISTS idx_ext_call_server ON external_call_log(server_name, called_at DESC);
739
840
 
740
841
  -- ============================================================
741
- -- v5.0.0: Agent actions \u2014 the audit-trail of autonomous loop ticks
842
+ -- v5.0.0: Agent actions the audit-trail of autonomous loop ticks
742
843
  -- Higher-level than goal_iterations; one row per wyrm-loop tick.
743
844
  -- ============================================================
744
845
  CREATE TABLE IF NOT EXISTS agent_actions (
@@ -753,7 +854,14 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
753
854
  );
754
855
  CREATE INDEX IF NOT EXISTS idx_agent_actions_goal ON agent_actions(goal_id, ran_at DESC);
755
856
  CREATE INDEX IF NOT EXISTS idx_agent_actions_actor ON agent_actions(actor, ran_at DESC);
756
- `)}},{version:12,description:"session_seen_artifacts (spec 014) + token-budget telemetry columns",up:e=>{e.exec(`
857
+ `);
858
+ },
859
+ },
860
+ {
861
+ version: 12,
862
+ description: 'session_seen_artifacts (spec 014) + token-budget telemetry columns',
863
+ up: (db) => {
864
+ db.exec(`
757
865
  -- session_seen_artifacts: per-session dedup so wyrm_context_build
758
866
  -- doesn't re-emit fully-rendered content the AI already saw this
759
867
  -- session. Pruned by wyrm_maintenance after WYRM_SEEN_TTL_DAYS
@@ -770,7 +878,14 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
770
878
  ON session_seen_artifacts(session_id, shown_at DESC);
771
879
  CREATE INDEX IF NOT EXISTS idx_session_seen_prune
772
880
  ON session_seen_artifacts(shown_at);
773
- `)}},{version:13,description:"design_tokens + design_references \u2014 creative workflow tables (5.9.0)",up:e=>{e.exec(`
881
+ `);
882
+ },
883
+ },
884
+ {
885
+ version: 13,
886
+ description: 'design_tokens + design_references — creative workflow tables (5.9.0)',
887
+ up: (db) => {
888
+ db.exec(`
774
889
  CREATE TABLE IF NOT EXISTS design_tokens (
775
890
  id INTEGER PRIMARY KEY AUTOINCREMENT,
776
891
  project_id INTEGER NOT NULL,
@@ -821,7 +936,14 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
821
936
  INSERT INTO design_references_fts(rowid, title, notes, tags)
822
937
  VALUES (new.id, new.title, new.notes, new.tags);
823
938
  END;
824
- `)}},{version:14,description:"token_savings_log + cross_project_visibility \u2014 Wyrm 6.0 substrate (spec 018)",up:e=>{e.exec(`
939
+ `);
940
+ },
941
+ },
942
+ {
943
+ version: 14,
944
+ description: 'token_savings_log + cross_project_visibility — Wyrm 6.0 substrate (spec 018)',
945
+ up: (db) => {
946
+ db.exec(`
825
947
  -- Token-savings telemetry: every Wyrm tool that has a counterfactual
826
948
  -- logs an estimate so the statusline and digest can show real ROI.
827
949
  CREATE TABLE IF NOT EXISTS token_savings_log (
@@ -844,10 +966,36 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
844
966
  ON token_savings_log(session_id, timestamp DESC);
845
967
  CREATE INDEX IF NOT EXISTS idx_savings_category
846
968
  ON token_savings_log(category, timestamp DESC);
847
- `);const E=["ground_truths","memory_artifacts","quests","decision_edges","design_references","design_tokens"];for(const t of E)try{if(e.prepare(`PRAGMA table_info(${t})`).all().some(i=>i.name==="cross_project_visibility"))continue;e.exec(`
969
+ `);
970
+ // Cross-project visibility flags on every searchable kind.
971
+ // Default 'within' — no existing data leaks across projects.
972
+ const tables = [
973
+ 'ground_truths',
974
+ 'memory_artifacts',
975
+ 'quests',
976
+ 'decision_edges', // was 'decisions' (a table that never existed) — fixed in v17; kept correct here for fresh installs
977
+ 'design_references',
978
+ 'design_tokens',
979
+ ];
980
+ for (const t of tables) {
981
+ try {
982
+ const cols = db.prepare(`PRAGMA table_info(${t})`).all();
983
+ if (cols.some((c) => c.name === 'cross_project_visibility'))
984
+ continue;
985
+ db.exec(`
848
986
  ALTER TABLE ${t} ADD COLUMN cross_project_visibility TEXT NOT NULL DEFAULT 'within'
849
987
  CHECK (cross_project_visibility IN ('within', 'org', 'public'));
850
- `)}catch{}}},{version:15,description:"Live Memory v6.4 \u2014 append-only events log + wyrm_meta (device identity)",up:e=>{e.exec(`
988
+ `);
989
+ }
990
+ catch { /* table may not exist on older installs; safe to skip */ }
991
+ }
992
+ },
993
+ },
994
+ {
995
+ version: 15,
996
+ description: 'Live Memory v6.4 — append-only events log + wyrm_meta (device identity)',
997
+ up: (db) => {
998
+ db.exec(`
851
999
  -- Node-local KV (stable device id for cross-device event identity, etc.)
852
1000
  CREATE TABLE IF NOT EXISTS wyrm_meta (
853
1001
  key TEXT PRIMARY KEY,
@@ -856,7 +1004,7 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
856
1004
 
857
1005
  -- Append-only event log (Live Memory v6.4). The existing tables stay the
858
1006
  -- system of record; this is a derived stream for live propagation. Writes
859
- -- emit here as a FAILURE-ISOLATED side effect (see events.ts) \u2014 a broken
1007
+ -- emit here as a FAILURE-ISOLATED side effect (see events.ts) a broken
860
1008
  -- events table can never regress the canonical write. 'cursor' is the
861
1009
  -- LOCAL subscription cursor (monotonic per node); cross-device identity is
862
1010
  -- (origin_device, origin_seq), with replication via INSERT OR IGNORE.
@@ -877,7 +1025,19 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
877
1025
  );
878
1026
  CREATE INDEX IF NOT EXISTS idx_events_project_cursor ON events(project_id, cursor);
879
1027
  CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind, created_at);
880
- `)}},{version:16,description:"Rebuild FTS tables with porter stemming (override\u2248overrides\u2248overriding) for better recall",up:e=>{e.exec(`
1028
+ `);
1029
+ },
1030
+ },
1031
+ {
1032
+ version: 16,
1033
+ description: 'Rebuild FTS tables with porter stemming (override≈overrides≈overriding) for better recall',
1034
+ up: (db) => {
1035
+ // The base-table triggers reference these FTS tables BY NAME, so recreating
1036
+ // them with the same name + the porter tokenizer keeps the triggers valid —
1037
+ // no need to drop/recreate triggers. `rebuild` repopulates from the
1038
+ // external-content base table. Migrations run single-threaded at startup,
1039
+ // so there's no concurrent write between DROP and CREATE.
1040
+ db.exec(`
881
1041
  DROP TABLE IF EXISTS quests_fts;
882
1042
  CREATE VIRTUAL TABLE quests_fts USING fts5(
883
1043
  title, description, content='quests', content_rowid='id',
@@ -905,13 +1065,62 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
905
1065
  tokenize='porter unicode61'
906
1066
  );
907
1067
  INSERT INTO skills_fts(skills_fts) VALUES('rebuild');
908
- `)}},{version:17,description:'Backfill cross_project_visibility on decision_edges (migration 14 named a non-existent "decisions" table)',up:e=>{try{e.prepare("PRAGMA table_info(decision_edges)").all().some(t=>t.name==="cross_project_visibility")||e.exec(`
1068
+ `);
1069
+ },
1070
+ },
1071
+ {
1072
+ version: 17,
1073
+ description: 'Backfill cross_project_visibility on decision_edges (migration 14 named a non-existent "decisions" table)',
1074
+ up: (db) => {
1075
+ // Migration 14 looped over a table called 'decisions' that never existed —
1076
+ // the real table is 'decision_edges' — so it silently never got the
1077
+ // cross_project_visibility column. Add it here (guarded + idempotent) so
1078
+ // already-migrated DBs converge with fresh installs (which now get it in v14).
1079
+ try {
1080
+ const cols = db.prepare(`PRAGMA table_info(decision_edges)`).all();
1081
+ if (!cols.some((c) => c.name === 'cross_project_visibility')) {
1082
+ db.exec(`
909
1083
  ALTER TABLE decision_edges ADD COLUMN cross_project_visibility TEXT NOT NULL DEFAULT 'within'
910
1084
  CHECK (cross_project_visibility IN ('within', 'org', 'public'));
911
- `)}catch{}}},{version:18,description:"GOD-SKILL SPEC v2 \u2014 skill tier/governs/composes + spec-kit registry (specs table)",up:e=>{const E=e.prepare("PRAGMA table_info(skills)").all(),t=s=>E.some(i=>i.name===s);t("tier")||e.exec(`
1085
+ `);
1086
+ }
1087
+ }
1088
+ catch { /* table may not exist on a very old install; safe to skip */ }
1089
+ },
1090
+ },
1091
+ {
1092
+ version: 18,
1093
+ description: 'GOD-SKILL SPEC v2 — skill tier/governs/composes + spec-kit registry (specs table)',
1094
+ up: (db) => {
1095
+ // --- Skill governance columns (additive, safe defaults) ---
1096
+ // tier: atomic|mega|god — apex skill tier routing. Default 'atomic' so
1097
+ // every pre-existing skill keeps its current (leaf) behaviour.
1098
+ // governs: JSON string array of skill names this node routes DOWN to
1099
+ // (god → megas, mega → atomics). Default '[]'.
1100
+ // composes: JSON string array of skill names this node pulls in laterally.
1101
+ // Default '[]'.
1102
+ // All three are guarded so re-running against a partially-migrated DB
1103
+ // (or a fresh install that already has them) is a no-op.
1104
+ const skillCols = db.prepare(`PRAGMA table_info(skills)`).all();
1105
+ const hasSkillCol = (c) => skillCols.some((x) => x.name === c);
1106
+ if (!hasSkillCol('tier')) {
1107
+ db.exec(`
912
1108
  ALTER TABLE skills ADD COLUMN tier TEXT NOT NULL DEFAULT 'atomic'
913
1109
  CHECK (tier IN ('atomic', 'mega', 'god'));
914
- `),t("governs")||e.exec("ALTER TABLE skills ADD COLUMN governs TEXT NOT NULL DEFAULT '[]';"),t("composes")||e.exec("ALTER TABLE skills ADD COLUMN composes TEXT NOT NULL DEFAULT '[]';"),e.exec("CREATE INDEX IF NOT EXISTS idx_skills_tier ON skills(tier);"),e.exec(`
1110
+ `);
1111
+ }
1112
+ if (!hasSkillCol('governs')) {
1113
+ db.exec(`ALTER TABLE skills ADD COLUMN governs TEXT NOT NULL DEFAULT '[]';`);
1114
+ }
1115
+ if (!hasSkillCol('composes')) {
1116
+ db.exec(`ALTER TABLE skills ADD COLUMN composes TEXT NOT NULL DEFAULT '[]';`);
1117
+ }
1118
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_skills_tier ON skills(tier);`);
1119
+ // --- Spec-kit registry (GHOSTMESH spec-kit specs → Wyrm-native) ---
1120
+ // One row per registered spec directory, linked to a project. The
1121
+ // generated quests carry a deterministic tag signature so wyrm_spec_register
1122
+ // is idempotent (re-run updates the same quests instead of duplicating).
1123
+ db.exec(`
915
1124
  CREATE TABLE IF NOT EXISTS specs (
916
1125
  id INTEGER PRIMARY KEY AUTOINCREMENT,
917
1126
  project_id INTEGER NOT NULL,
@@ -926,11 +1135,86 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
926
1135
  );
927
1136
  CREATE INDEX IF NOT EXISTS idx_specs_project ON specs(project_id);
928
1137
  CREATE INDEX IF NOT EXISTS idx_specs_dir ON specs(spec_dir);
929
- `)}},{version:19,description:"Grove sync policy: projects.sync_policy gate (private by default) for cloud + federation isolation",up:e=>{e.prepare("PRAGMA table_info(projects)").all().some(o=>o.name==="sync_policy")||e.exec(`
1138
+ `);
1139
+ },
1140
+ },
1141
+ {
1142
+ version: 19,
1143
+ description: 'Grove sync policy: projects.sync_policy gate (private by default) for cloud + federation isolation',
1144
+ up: (db) => {
1145
+ // Grove-level replication gate. A project's rows may leave the local box
1146
+ // only if its grove opts in:
1147
+ // 'private' (default): never replicates by any channel.
1148
+ // 'cloud': may replicate to the operator's OWN cloud backup
1149
+ // (per-row cross_project_visibility still applies).
1150
+ // 'team': may also federate to a team Wyrm
1151
+ // (per-row is_shared still applies).
1152
+ // This is a SECOND, grove-level gate on top of the existing per-row flags,
1153
+ // so one accidental flag flip can never alone leak a private grove.
1154
+ const pcols = db.prepare(`PRAGMA table_info(projects)`).all();
1155
+ if (!pcols.some((c) => c.name === 'sync_policy')) {
1156
+ db.exec(`
930
1157
  ALTER TABLE projects ADD COLUMN sync_policy TEXT NOT NULL DEFAULT 'private'
931
1158
  CHECK (sync_policy IN ('private', 'cloud', 'team'));
932
- `);const t=(o,a)=>{try{return e.prepare(`PRAGMA table_info(${o})`).all().some(r=>r.name===a)}catch{return!1}},s=o=>{try{return e.prepare(o).all().map(a=>a.pid)}catch{return[]}},i=new Set;for(const o of["ground_truths","memory_artifacts","quests","design_tokens","design_references"])if(!(!t(o,"cross_project_visibility")||!t(o,"project_id")))for(const a of s(`SELECT DISTINCT project_id AS pid FROM ${o}
933
- WHERE cross_project_visibility IN ('org', 'public') AND project_id IS NOT NULL`))i.add(a);const T=e.prepare("UPDATE projects SET sync_policy = 'cloud' WHERE id = ? AND sync_policy = 'private'");for(const o of i)T.run(o)}},{version:20,description:"v7 F2 (T008) \u2014 run-native provenance: runs + run_agents tables, agent_id/run_id attribution on 9 tables, failure_patterns.quarantine_scope",up:e=>{e.exec(`
1159
+ `);
1160
+ }
1161
+ // Backward-compatible backfill, CONSERVATIVE: a project that already had
1162
+ // cloud-eligible rows (cross_project_visibility org/public) keeps its cloud
1163
+ // backup ability (cloud is per-operator + encrypted, low risk, no silent
1164
+ // regression). We deliberately DO NOT auto-promote any grove to 'team':
1165
+ // federation is the actual team-exposure axis, so it is ALWAYS an explicit
1166
+ // opt-in (via `wyrm grove policy <proj> team`). This guarantees a grove that
1167
+ // merely had an is_shared row in the past is never silently moved off
1168
+ // 'private' on upgrade. Everything not cloud-eligible stays 'private'.
1169
+ const hasCol = (t, c) => {
1170
+ try {
1171
+ return db.prepare(`PRAGMA table_info(${t})`).all().some((x) => x.name === c);
1172
+ }
1173
+ catch {
1174
+ return false;
1175
+ }
1176
+ };
1177
+ const distinctPids = (sql) => {
1178
+ try {
1179
+ return db.prepare(sql).all().map((r) => r.pid);
1180
+ }
1181
+ catch {
1182
+ return [];
1183
+ }
1184
+ };
1185
+ const cloudIds = new Set();
1186
+ for (const t of ['ground_truths', 'memory_artifacts', 'quests', 'design_tokens', 'design_references']) {
1187
+ if (!hasCol(t, 'cross_project_visibility') || !hasCol(t, 'project_id'))
1188
+ continue;
1189
+ for (const pid of distinctPids(`SELECT DISTINCT project_id AS pid FROM ${t}
1190
+ WHERE cross_project_visibility IN ('org', 'public') AND project_id IS NOT NULL`))
1191
+ cloudIds.add(pid);
1192
+ }
1193
+ const setCloud = db.prepare(`UPDATE projects SET sync_policy = 'cloud' WHERE id = ? AND sync_policy = 'private'`);
1194
+ for (const id of cloudIds)
1195
+ setCloud.run(id);
1196
+ },
1197
+ },
1198
+ {
1199
+ version: 20,
1200
+ description: 'v7 F2 (T008) — run-native provenance: runs + run_agents tables, agent_id/run_id attribution on 9 tables, failure_patterns.quarantine_scope',
1201
+ up: (db) => {
1202
+ // ============================================================
1203
+ // v7 BROOD FR-1: run-native provenance core.
1204
+ // Additive + guarded (Article VI): new tables use IF NOT EXISTS, every
1205
+ // ALTER is PRAGMA-checked, and the only UPDATE is a one-time backfill
1206
+ // INSIDE its column guard. A v6.x DB opens under this migration with
1207
+ // 100% of its rows; nothing is renamed, dropped, or rewritten.
1208
+ // ============================================================
1209
+ // ---- runs: one row per orchestrated fleet run ----
1210
+ // run_id is a ULID (src/ulid.ts: 26-char Crockford base32, monotonic,
1211
+ // crypto-random) so run ids sort lexicographically in creation order.
1212
+ // parent_run_id nests runs (orchestrator spawning sub-orchestrators).
1213
+ // debrief_artifact_id links the run's debrief memory_artifact when the
1214
+ // orchestrator files one. No FK from run_agents.agent_id to
1215
+ // agent_presence: presence rows are TTL-pruned heartbeats, while run
1216
+ // membership is durable provenance that must outlive them.
1217
+ db.exec(`
934
1218
  CREATE TABLE IF NOT EXISTS runs (
935
1219
  run_id TEXT PRIMARY KEY,
936
1220
  parent_run_id TEXT,
@@ -955,14 +1239,98 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
955
1239
  FOREIGN KEY (run_id) REFERENCES runs(run_id) ON DELETE CASCADE
956
1240
  );
957
1241
  CREATE INDEX IF NOT EXISTS idx_run_agents_agent ON run_agents(agent_id);
958
- `);const E=(s,i)=>{try{return e.prepare(`PRAGMA table_info(${s})`).all().some(T=>T.name===i)}catch{return!1}},t=["memory_artifacts","failure_patterns","decision_edges","ground_truths","quests","sessions","quest_claims","events","audit_log"];for(const s of t)E(s,"agent_id")||e.exec(`ALTER TABLE ${s} ADD COLUMN agent_id TEXT;`),E(s,"run_id")||e.exec(`ALTER TABLE ${s} ADD COLUMN run_id TEXT;`);e.exec(`
1242
+ `);
1243
+ // ---- nullable agent_id/run_id attribution on the 9 provenance tables ----
1244
+ // Soft references on purpose (no FK to runs): attribution is provenance
1245
+ // metadata — pruning or never-creating a runs row must never block or
1246
+ // cascade-delete canonical memory rows.
1247
+ //
1248
+ // On `events`, attribution EXTENDS the existing `actor` column — NOT a
1249
+ // parallel concept. `actor` stays the human display identity exactly as
1250
+ // 6.x wrote it; `agent_id` (machine identity) and `run_id` land
1251
+ // alongside. Precedence at read sites that surface attribution
1252
+ // (src/attribution.ts owns this rule):
1253
+ // 1. agent_id — machine identity (v7 fleet writes)
1254
+ // 2. actor — display identity (6.x writes, human labels)
1255
+ // 3. 'legacy' — neither set (a pre-v7 row); read-time only, never
1256
+ // written back. audit_log.actor follows the same rule.
1257
+ //
1258
+ // quest_claims already has agent_id (NOT NULL, migration 9) — the guard
1259
+ // skips it and only run_id is added there. All other tables gain both.
1260
+ const hasCol = (t, c) => {
1261
+ try {
1262
+ return db.prepare(`PRAGMA table_info(${t})`).all()
1263
+ .some((x) => x.name === c);
1264
+ }
1265
+ catch {
1266
+ return false;
1267
+ }
1268
+ };
1269
+ const ATTRIBUTED_TABLES = [
1270
+ 'memory_artifacts', 'failure_patterns', 'decision_edges',
1271
+ 'ground_truths', 'quests', 'sessions', 'quest_claims',
1272
+ 'events', 'audit_log',
1273
+ ];
1274
+ for (const t of ATTRIBUTED_TABLES) {
1275
+ if (!hasCol(t, 'agent_id'))
1276
+ db.exec(`ALTER TABLE ${t} ADD COLUMN agent_id TEXT;`);
1277
+ if (!hasCol(t, 'run_id'))
1278
+ db.exec(`ALTER TABLE ${t} ADD COLUMN run_id TEXT;`);
1279
+ }
1280
+ // Partial indexes for the run-scoped hot paths (failure quarantine,
1281
+ // run-tagged event fan-out, debrief assembly). Partial so the 6.x
1282
+ // all-NULL bulk costs nothing.
1283
+ db.exec(`
959
1284
  CREATE INDEX IF NOT EXISTS idx_failure_run ON failure_patterns(run_id) WHERE run_id IS NOT NULL;
960
1285
  CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id) WHERE run_id IS NOT NULL;
961
1286
  CREATE INDEX IF NOT EXISTS idx_artifacts_run ON memory_artifacts(run_id) WHERE run_id IS NOT NULL;
962
- `),E("failure_patterns","quarantine_scope")||(e.exec(`
1287
+ `);
1288
+ // ---- failure_patterns quarantine scope (run|project|global) ----
1289
+ // The v7 spec names this column `scope`, but failure_patterns.scope has
1290
+ // existed since migration 9 with DIFFERENT semantics: it is the signature
1291
+ // MATCH dimension ('file'|'symbol'|'command'|'prompt') baked into every
1292
+ // stored signature by FailurePatterns.signature(). Renaming or
1293
+ // repurposing it would be a destructive rewrite (Article VI) and corrupt
1294
+ // every existing signature, so the run|project|global QUARANTINE/
1295
+ // authority scope lands as `quarantine_scope`.
1296
+ //
1297
+ // Default 'project' preserves 6.x semantics: a 6.x failure recorded with
1298
+ // a project_id was authoritative for its whole project the moment it was
1299
+ // written (check() filters by project_id). The guarded one-time backfill
1300
+ // marks project_id-IS-NULL rows 'global' because 6.x check() matched
1301
+ // those rows from EVERY project (`OR project_id IS NULL`) — without it,
1302
+ // v7 quarantine enforcement would silently narrow their reach.
1303
+ if (!hasCol('failure_patterns', 'quarantine_scope')) {
1304
+ db.exec(`
963
1305
  ALTER TABLE failure_patterns ADD COLUMN quarantine_scope TEXT NOT NULL DEFAULT 'project'
964
1306
  CHECK (quarantine_scope IN ('run', 'project', 'global'));
965
- `),e.exec("UPDATE failure_patterns SET quarantine_scope = 'global' WHERE project_id IS NULL;")),e.exec("CREATE INDEX IF NOT EXISTS idx_failure_quarantine ON failure_patterns(quarantine_scope, resolved);")}},{version:21,description:"v7 F2 (T015) \u2014 failure_confirmations: distinct-agent confirmation ledger behind run-quarantine auto-promotion",up:e=>{e.exec(`
1307
+ `);
1308
+ db.exec(`UPDATE failure_patterns SET quarantine_scope = 'global' WHERE project_id IS NULL;`);
1309
+ }
1310
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_failure_quarantine ON failure_patterns(quarantine_scope, resolved);`);
1311
+ },
1312
+ },
1313
+ {
1314
+ version: 21,
1315
+ description: 'v7 F2 (T015) — failure_confirmations: distinct-agent confirmation ledger behind run-quarantine auto-promotion',
1316
+ up: (db) => {
1317
+ // ============================================================
1318
+ // v7 BROOD FR-2 (T015): run-scoped failure quarantine promotion.
1319
+ // One row per (failure, agent) that recorded/confirmed the same failure
1320
+ // signature — the evidence ledger behind the debrief-INDEPENDENT
1321
+ // "≥2 distinct agent_id confirmations → promote to project scope" rule
1322
+ // (src/failure-patterns.ts noteConfirmation()).
1323
+ //
1324
+ // Additive + guarded (Article VI): IF NOT EXISTS only — re-running this
1325
+ // up() is a no-op and a v6.x DB opens with 100% of its rows untouched.
1326
+ //
1327
+ // PK (failure_id, agent_id) makes COUNT(*) per failure_id the DISTINCT
1328
+ // agent count by construction: the same agent re-recording N times is
1329
+ // ONE confirmation. run_id records where a confirmation came from (T017
1330
+ // analytics input); it is NOT part of the identity. The FK cascades with
1331
+ // the failure row — confirmations are evidence ABOUT a canonical row,
1332
+ // never canonical memory themselves (a delete() must not orphan them).
1333
+ db.exec(`
966
1334
  CREATE TABLE IF NOT EXISTS failure_confirmations (
967
1335
  failure_id INTEGER NOT NULL,
968
1336
  agent_id TEXT NOT NULL,
@@ -971,7 +1339,42 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
971
1339
  PRIMARY KEY (failure_id, agent_id),
972
1340
  FOREIGN KEY (failure_id) REFERENCES failure_patterns(id) ON DELETE CASCADE
973
1341
  );
974
- `)}},{version:22,description:"v7 F2 (T017) \u2014 failure_blocks: prevented-repeat ledger behind wyrm_stats view=failures",up:e=>{e.exec(`
1342
+ `);
1343
+ },
1344
+ },
1345
+ {
1346
+ version: 22,
1347
+ description: 'v7 F2 (T017) — failure_blocks: prevented-repeat ledger behind wyrm_stats view=failures',
1348
+ up: (db) => {
1349
+ // ============================================================
1350
+ // v7 BROOD FR-2 (T017): prevented-repeat analytics.
1351
+ // One row per BLOCKED failure_check verdict (blocked:true) — the unit of
1352
+ // "a repeat was prevented". Attribution columns name the CHECKING
1353
+ // agent/run (the agent that was about to repeat the failure), NOT the
1354
+ // recorder — failure_patterns.agent_id/run_id already name the recorder.
1355
+ // failure_id is the verdict's top blocker (matches[0]).
1356
+ //
1357
+ // STORAGE CHOICE, documented (the task offered counter-table vs
1358
+ // events-derived):
1359
+ // - NOT events-derived: emitEvent is failure-isolated best-effort AND
1360
+ // the events log is retention-pruned (WYRM_EVENT_RETAIN_DAYS /
1361
+ // maxPerProject in wyrm_maintenance) — analytics derived from it
1362
+ // would silently undercount over time (same rationale that kept the
1363
+ // T015 confirmation ledger off the events log). failure_check is
1364
+ // also a READ that emits no events today; emitting one per check
1365
+ // would bloat the private event stream for one integer of analytics.
1366
+ // - An APPEND-ONLY ledger (not an UPSERTed counter row): one INSERT
1367
+ // per blocked verdict is exactly correct under multi-process fleet
1368
+ // concurrency with no read-modify-write race, and read-time
1369
+ // COUNT/GROUP BY is deterministic (Article III: pure SQL, zero LLM).
1370
+ // - Private by construction: this is not an events surface — block
1371
+ // rows never ride SSE/replication (failures stay is_shared=0).
1372
+ //
1373
+ // Additive + guarded (Article VI): IF NOT EXISTS only — re-running this
1374
+ // up() is a no-op and a v6.x DB opens with 100% of its rows untouched.
1375
+ // FK cascades with the failure row (the migration-21 precedent): blocks
1376
+ // are analytics ABOUT a canonical row, never canonical memory.
1377
+ db.exec(`
975
1378
  CREATE TABLE IF NOT EXISTS failure_blocks (
976
1379
  id INTEGER PRIMARY KEY AUTOINCREMENT,
977
1380
  failure_id INTEGER NOT NULL,
@@ -981,7 +1384,41 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
981
1384
  blocked_at TEXT DEFAULT (datetime('now')),
982
1385
  FOREIGN KEY (failure_id) REFERENCES failure_patterns(id) ON DELETE CASCADE
983
1386
  );
984
- `),e.exec("CREATE INDEX IF NOT EXISTS idx_failure_blocks_run ON failure_blocks(run_id) WHERE run_id IS NOT NULL;"),e.exec("CREATE INDEX IF NOT EXISTS idx_failure_blocks_failure ON failure_blocks(failure_id);")}},{version:23,description:"v7 F3 (T028) \u2014 run_briefs: byte-stable fleet session_prime brief cache, one row per (run_id, role)",up:e=>{e.exec(`
1387
+ `);
1388
+ // "N repeats blocked this run" is the hot read — partial index in the
1389
+ // style of migration 20's idx_failure_run/idx_events_run.
1390
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_failure_blocks_run ON failure_blocks(run_id) WHERE run_id IS NOT NULL;`);
1391
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_failure_blocks_failure ON failure_blocks(failure_id);`);
1392
+ },
1393
+ },
1394
+ {
1395
+ version: 23,
1396
+ description: 'v7 F3 (T028) — run_briefs: byte-stable fleet session_prime brief cache, one row per (run_id, role)',
1397
+ up: (db) => {
1398
+ // ============================================================
1399
+ // v7 BROOD FR-5 (T028): fleet-mode session_prime.
1400
+ // The FIRST prime of a (run_id, role) compiles the role-sliced brief
1401
+ // and CAS-inserts it here (INSERT OR IGNORE on the PK, then every
1402
+ // caller — winner and losers alike — reads the row back), so 12
1403
+ // simultaneous primes across 12 PROCESSES return the byte-identical
1404
+ // cached prefix (spec §7 criterion 8). An in-memory cache cannot give
1405
+ // that guarantee: fleet agents are separate stdio server processes
1406
+ // sharing only this SQLite file.
1407
+ //
1408
+ // body_json is the full structured session_prime body (the T026
1409
+ // sections[] shape) — the text channel re-derives from it through the
1410
+ // T019 renderer, so text and structuredContent cannot drift.
1411
+ //
1412
+ // run_id is a SOFT reference to runs (no FK), the migration-20
1413
+ // precedent: a prime may legitimately race `wyrm_run action=start`
1414
+ // (the F2 "orchestrator already exported WYRM_RUN_ID" orphan shape),
1415
+ // and a cache row must never block or cascade with run lifecycle.
1416
+ // Retention: wyrm_maintenance prunes by created_at (T029, same
1417
+ // WYRM_EVENT_RETAIN_DAYS knob as the event log).
1418
+ //
1419
+ // Additive + guarded (Article VI): IF NOT EXISTS only — re-running
1420
+ // this up() is a no-op and a v6.x DB opens with 100% of its rows.
1421
+ db.exec(`
985
1422
  CREATE TABLE IF NOT EXISTS run_briefs (
986
1423
  run_id TEXT NOT NULL,
987
1424
  role TEXT NOT NULL DEFAULT '',
@@ -990,17 +1427,169 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
990
1427
  created_at TEXT DEFAULT (datetime('now')),
991
1428
  PRIMARY KEY (run_id, role)
992
1429
  );
993
- `)}},{version:24,description:"v7 F3 (T029) \u2014 claims/presence hardening: role on quest_claims, run_id/role on agent_presence, run-scoped partial indexes",up:e=>{const E=(t,s)=>{try{return e.prepare(`PRAGMA table_info(${t})`).all().some(i=>i.name===s)}catch{return!1}};E("quest_claims","role")||e.exec("ALTER TABLE quest_claims ADD COLUMN role TEXT;"),E("agent_presence","run_id")||e.exec("ALTER TABLE agent_presence ADD COLUMN run_id TEXT;"),E("agent_presence","role")||e.exec("ALTER TABLE agent_presence ADD COLUMN role TEXT;"),e.exec(`
1430
+ `);
1431
+ },
1432
+ },
1433
+ {
1434
+ version: 24,
1435
+ description: 'v7 F3 (T029) — claims/presence hardening: role on quest_claims, run_id/role on agent_presence, run-scoped partial indexes',
1436
+ up: (db) => {
1437
+ // ============================================================
1438
+ // v7 BROOD FR-5 (T029): claims/presence run attribution completed.
1439
+ // quest_claims got run_id in migration 20; `role` lands here so an
1440
+ // orchestrator auditing the claim board sees WHICH fleet role holds
1441
+ // each lease. agent_presence persists rows (TTL heartbeats, table
1442
+ // since migration 9) so it gains BOTH run_id and role — presence
1443
+ // dashboards become per-run. All three are stamped at write time from
1444
+ // the ambient actor envelope + run_agents membership (presence.ts);
1445
+ // NULL outside a run (the migration-20 soft-ref rule: never an FK —
1446
+ // attribution metadata must never block or cascade canonical rows).
1447
+ //
1448
+ // Claims stay atomic via the existing PK-conflict CAS — this migration
1449
+ // adds attribution columns only, no locking changes (the spec is
1450
+ // explicit: there is no race to fix).
1451
+ //
1452
+ // Additive + guarded (Article VI): PRAGMA-checked ALTERs, IF NOT
1453
+ // EXISTS indexes — a v6.x DB opens with 100% of its rows; pre-v7 rows
1454
+ // carry NULL and read as 'legacy' at display time only.
1455
+ const hasCol = (t, c) => {
1456
+ try {
1457
+ return db.prepare(`PRAGMA table_info(${t})`).all()
1458
+ .some((x) => x.name === c);
1459
+ }
1460
+ catch {
1461
+ return false;
1462
+ }
1463
+ };
1464
+ if (!hasCol('quest_claims', 'role'))
1465
+ db.exec(`ALTER TABLE quest_claims ADD COLUMN role TEXT;`);
1466
+ if (!hasCol('agent_presence', 'run_id'))
1467
+ db.exec(`ALTER TABLE agent_presence ADD COLUMN run_id TEXT;`);
1468
+ if (!hasCol('agent_presence', 'role'))
1469
+ db.exec(`ALTER TABLE agent_presence ADD COLUMN role TEXT;`);
1470
+ // Partial indexes in the migration-20 style: the all-NULL 6.x bulk
1471
+ // costs nothing; wyrm_run status + per-run presence views are the
1472
+ // hot reads.
1473
+ db.exec(`
994
1474
  CREATE INDEX IF NOT EXISTS idx_quest_claims_run ON quest_claims(run_id) WHERE run_id IS NOT NULL;
995
1475
  CREATE INDEX IF NOT EXISTS idx_presence_run ON agent_presence(run_id) WHERE run_id IS NOT NULL;
996
- `)}},{version:25,description:"Skills portability \u2014 store SKILL.md content (+ sha/ts) in the registry and make skills cloud-sync-eligible behind the per-row visibility gate",up:e=>{const E=(t,s)=>{try{return e.prepare(`PRAGMA table_info(${t})`).all().some(i=>i.name===s)}catch{return!1}};E("skills","content")||e.exec("ALTER TABLE skills ADD COLUMN content TEXT;"),E("skills","content_sha256")||e.exec("ALTER TABLE skills ADD COLUMN content_sha256 TEXT;"),E("skills","content_updated_at")||e.exec("ALTER TABLE skills ADD COLUMN content_updated_at TEXT;"),E("skills","cross_project_visibility")||e.exec("ALTER TABLE skills ADD COLUMN cross_project_visibility TEXT NOT NULL DEFAULT 'within';"),E("skills","is_shared")||e.exec("ALTER TABLE skills ADD COLUMN is_shared INTEGER NOT NULL DEFAULT 0;"),e.exec("CREATE INDEX IF NOT EXISTS idx_skills_visibility ON skills(cross_project_visibility) WHERE cross_project_visibility != 'within';")}},{version:26,description:"Reverse-bridge rejection tombstones \u2014 remember a once-rejected outside-prose sig so the next sweep never re-queues content the operator already deleted",up:e=>{e.exec(`
1476
+ `);
1477
+ },
1478
+ },
1479
+ {
1480
+ version: 25,
1481
+ description: 'Skills portability — store SKILL.md content (+ sha/ts) in the registry and make skills cloud-sync-eligible behind the per-row visibility gate',
1482
+ up: (db) => {
1483
+ // ============================================================
1484
+ // SKILLS CLOUD SYNC: carry skill BODIES across machines.
1485
+ // The `skills` table has only ever held metadata + a `skill_path`
1486
+ // pointer into ~/.copilot/skills/<slug>/SKILL.md — a pointer that is
1487
+ // meaningless on another machine. To make a registered skill portable
1488
+ // (this Fedora box → a Mac mini) we store the SKILL.md text itself.
1489
+ //
1490
+ // Additive + guarded (Article VI), the migration 20/24 style: every
1491
+ // ALTER is PRAGMA-checked, nothing is renamed/dropped/rewritten, and a
1492
+ // v7.0.1 DB opens under this migration with 100% of its rows preserved.
1493
+ //
1494
+ // content — the full SKILL.md text (NULL until backfilled;
1495
+ // a skill whose file is unreadable stays NULL).
1496
+ // content_sha256 — sha256 of `content`, for idempotent backfill
1497
+ // (skip unchanged) and export dedup.
1498
+ // content_updated_at — when `content` last changed (sync/merge aid).
1499
+ //
1500
+ // CRITICAL — FTS: skills_fts is an EXTERNAL-CONTENT FTS5 over
1501
+ // (name, description, tags) ONLY (content='skills'). Adding plain base
1502
+ // columns does NOT change the FTS table's indexed columns, so the
1503
+ // external-content contract and its insert/delete/update triggers are
1504
+ // untouched. (Verified by the post-migration FTS search test.)
1505
+ const hasCol = (t, c) => {
1506
+ try {
1507
+ return db.prepare(`PRAGMA table_info(${t})`).all()
1508
+ .some((x) => x.name === c);
1509
+ }
1510
+ catch {
1511
+ return false;
1512
+ }
1513
+ };
1514
+ if (!hasCol('skills', 'content'))
1515
+ db.exec(`ALTER TABLE skills ADD COLUMN content TEXT;`);
1516
+ if (!hasCol('skills', 'content_sha256'))
1517
+ db.exec(`ALTER TABLE skills ADD COLUMN content_sha256 TEXT;`);
1518
+ if (!hasCol('skills', 'content_updated_at'))
1519
+ db.exec(`ALTER TABLE skills ADD COLUMN content_updated_at TEXT;`);
1520
+ // ---- cloud-sync eligibility, private-by-default (Article I/IX) ----
1521
+ // Skills are global (no project_id), so the grove (project) sync_policy
1522
+ // gate cannot apply per-skill. We instead make skills sync the SAME way
1523
+ // ground_truths/memory_artifacts do: a per-row `cross_project_visibility`
1524
+ // marker, DEFAULT 'within' (operator-private). The sync engine's visClause
1525
+ // (`cross_project_visibility IN ('org','public')`) is the egress gate —
1526
+ // a brand-new skills column defaulting to 'within' means NO skill leaves
1527
+ // this machine until the operator explicitly promotes it. `is_shared`
1528
+ // (default 0) lands too so the team-federation gate has its flag.
1529
+ // This adds skills to the existing privacy lattice without weakening the
1530
+ // default, exactly per the constitution's private-by-default rule.
1531
+ if (!hasCol('skills', 'cross_project_visibility')) {
1532
+ db.exec(`ALTER TABLE skills ADD COLUMN cross_project_visibility TEXT NOT NULL DEFAULT 'within';`);
1533
+ }
1534
+ if (!hasCol('skills', 'is_shared')) {
1535
+ db.exec(`ALTER TABLE skills ADD COLUMN is_shared INTEGER NOT NULL DEFAULT 0;`);
1536
+ }
1537
+ // Partial index for the egress hot path (collectChangedRows filters on
1538
+ // promoted rows), migration 20/24 style — the all-'within' default bulk
1539
+ // costs nothing.
1540
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_skills_visibility ON skills(cross_project_visibility) WHERE cross_project_visibility != 'within';`);
1541
+ },
1542
+ },
1543
+ {
1544
+ version: 26,
1545
+ description: 'Reverse-bridge rejection tombstones — remember a once-rejected outside-prose sig so the next sweep never re-queues content the operator already deleted',
1546
+ up: (db) => {
1547
+ // ============================================================
1548
+ // REVERSE-BRIDGE REJECTION MEMORY (security pass #2, finding #1).
1549
+ //
1550
+ // detectEdits() always re-emits the operator prose OUTSIDE the Wyrm
1551
+ // markers as an 'outside' edit on EVERY sweep — there is no diff against
1552
+ // a prior state. Re-queueing is suppressed only by existsBySig() against
1553
+ // the live review queue. So if the operator REVIEWS and REJECTS (deletes)
1554
+ // a reverse-bridge candidate, the sig disappears from the DB and the next
1555
+ // sweep re-extracts + re-queues the identical prose — a review-queue loop
1556
+ // for content the operator already said no to.
1557
+ //
1558
+ // Fix: a tombstone of rejected `rb:` sigs. wyrm_review's reject path
1559
+ // records the sig here; makeBridgeDeps.existsBySig() additionally consults
1560
+ // it, so a once-rejected outside-prose edit is remembered as rejected and
1561
+ // never re-offered. Purely local, per-project, never replicated.
1562
+ //
1563
+ // Additive + guarded (Article VI), migration 20/24/25 style: a brand-new
1564
+ // table, nothing renamed/dropped, a v7.0.x DB opens with 100% of its rows.
1565
+ db.exec(`
997
1566
  CREATE TABLE IF NOT EXISTS reverse_bridge_tombstones (
998
1567
  project_id INTEGER NOT NULL,
999
1568
  sig TEXT NOT NULL,
1000
1569
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
1001
1570
  PRIMARY KEY (project_id, sig)
1002
1571
  );
1003
- `)}},{version:27,description:'Continuity Layer \u2014 the rolling per-project living thread ("where we left off / what we were talking and thinking about"), captured continuously and restored on every new session, for any agent/system that primes Wyrm',up:e=>{e.exec(`
1572
+ `);
1573
+ },
1574
+ },
1575
+ {
1576
+ version: 27,
1577
+ description: 'Continuity Layer — the rolling per-project living thread ("where we left off / what we were talking and thinking about"), captured continuously and restored on every new session, for any agent/system that primes Wyrm',
1578
+ up: (db) => {
1579
+ // ============================================================
1580
+ // CONTINUITY LAYER. Ground truths/quests/memories capture CURATED facts;
1581
+ // they do not capture the LIVING THREAD of a working session — what we were
1582
+ // just discussing, deciding, and thinking through. So a fresh chat (or a
1583
+ // different surface: phone, Hermes, Ember, Gemini) re-opens cold.
1584
+ //
1585
+ // One evolving narrative per project: appended to continuously and bounded
1586
+ // (oldest trimmed with a marker) so it never grows unbounded but never loses
1587
+ // the gist. wyrm_session_prime / rehydrate restore it verbatim up front; the
1588
+ // row syncs like any other so every device/agent inherits the same thread.
1589
+ //
1590
+ // Additive + guarded (Article VI), migration 25/26 style: brand-new table,
1591
+ // nothing renamed/dropped; an older DB opens with 100% of its rows.
1592
+ db.exec(`
1004
1593
  CREATE TABLE IF NOT EXISTS project_continuity (
1005
1594
  project_id INTEGER PRIMARY KEY,
1006
1595
  thread TEXT NOT NULL DEFAULT '',
@@ -1009,7 +1598,46 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
1009
1598
  turn_count INTEGER NOT NULL DEFAULT 0,
1010
1599
  FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
1011
1600
  );
1012
- `)}},{version:28,description:"Routing memory \u2014 record which model/tool succeeded (or failed) per task_type so the dragon-cli multi-model router can do negative learning over past routing outcomes",up:e=>{e.exec(`
1601
+ `);
1602
+ },
1603
+ },
1604
+ {
1605
+ version: 28,
1606
+ description: 'Routing memory — record which model/tool succeeded (or failed) per task_type so the dragon-cli multi-model router can do negative learning over past routing outcomes',
1607
+ up: (db) => {
1608
+ // ============================================================
1609
+ // ROUTING MEMORY (Ghost Protocol local multi-model ROUTER, subsystem
1610
+ // wyrm-routing-rerank). The dragon-cli router classifies a task into a
1611
+ // task_type, selects a model + tool set, delegates, and logs the outcome
1612
+ // to ~/.dragon/routing.jsonl. That trace is LOCAL to one machine and one
1613
+ // process — it cannot inform another agent, a fresh CLI session, or the
1614
+ // fleet. This table is the durable, queryable home for the SAME signal so
1615
+ // the router can ask Wyrm "which model/tool succeeded for THIS task_type"
1616
+ // and steer toward winners + away from past failures (negative learning).
1617
+ //
1618
+ // One row per routing DECISION+OUTCOME (an append-only event log, like
1619
+ // the routing.jsonl line that produced it):
1620
+ // project_id — the project the task ran under (-1 = global/unscoped;
1621
+ // NOT an FK so a router that hasn't registered a project
1622
+ // can still record, and a project delete never orphans
1623
+ // the routing history — attribution metadata must never
1624
+ // block or cascade, the migration 20/24 soft-ref rule).
1625
+ // task_type — the router's classification (e.g. 'reason', 'tools',
1626
+ // 'code', 'chat'); the recall key.
1627
+ // model_id — the model that ran it (e.g. 'vibethinker', 'mistral-
1628
+ // nemo', 'ember', 'ollama:qwen2.5:1.5b').
1629
+ // tool_names — comma-joined tools used on that run (NULL when none).
1630
+ // success — 1 = the routed run succeeded, 0 = it failed. The whole
1631
+ // point of negative learning: a 0 is as valuable as a 1.
1632
+ // latency_ms — optional wall-clock of the run (NULL when not measured)
1633
+ // — lets recall break ties toward the FASTER winner.
1634
+ // ts — when it ran (router clock). Aggregation/recency use it.
1635
+ //
1636
+ // Additive + guarded (Article VI), migration 25/26/27 style: a brand-new
1637
+ // table, nothing renamed/dropped, an older DB opens with 100% of its
1638
+ // rows. Indexed for the hot recall path: (project_id, task_type) is the
1639
+ // exact WHERE the router queries by, with ts DESC for recency windows.
1640
+ db.exec(`
1013
1641
  CREATE TABLE IF NOT EXISTS routing_memory (
1014
1642
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1015
1643
  project_id INTEGER NOT NULL DEFAULT -1,
@@ -1024,10 +1652,139 @@ const n=[{version:1,description:"Baseline schema \u2014 projects, sessions, ques
1024
1652
  ON routing_memory(project_id, task_type, ts DESC);
1025
1653
  CREATE INDEX IF NOT EXISTS idx_routing_memory_task
1026
1654
  ON routing_memory(task_type, ts DESC);
1027
- `)}},{version:29,description:"Failure Firewall v2 \u2014 content tier: a normalized signature of the CODE that broke, so the opt-in PreToolUse guard can block an edit that re-introduces a recorded failure pattern (not just an identical target path)",up:e=>{e.prepare("PRAGMA table_info(failure_patterns)").all().some(t=>t.name==="content_sig")||e.exec("ALTER TABLE failure_patterns ADD COLUMN content_sig TEXT;")}}];function d(e){e.exec(`
1655
+ `);
1656
+ },
1657
+ },
1658
+ {
1659
+ version: 29,
1660
+ description: 'Failure Firewall v2 — content tier: a normalized signature of the CODE that broke, so the opt-in PreToolUse guard can block an edit that re-introduces a recorded failure pattern (not just an identical target path)',
1661
+ up: (db) => {
1662
+ // Additive + guarded (Article VI): one nullable column, nothing renamed or
1663
+ // dropped — an older DB opens with 100% of its rows; content_sig is NULL on
1664
+ // every existing failure (the content tier only matches rows recorded WITH
1665
+ // code content, and is itself opt-in via WYRM_GUARD_CONTENT in the guard).
1666
+ // IDEMPOTENT: the grove-sync-policy test re-runs migrations >= 19, and
1667
+ // SQLite has no `ADD COLUMN IF NOT EXISTS`, so only add when absent.
1668
+ const hasCol = db.prepare(`PRAGMA table_info(failure_patterns)`).all()
1669
+ .some((c) => c.name === 'content_sig');
1670
+ if (!hasCol)
1671
+ db.exec(`ALTER TABLE failure_patterns ADD COLUMN content_sig TEXT;`);
1672
+ },
1673
+ },
1674
+ {
1675
+ version: 30,
1676
+ description: 'Failure Firewall staleness oracle — failure_anchors: the source files a failure was recorded against (project-relative path + normalized-content sha + stat fast-path fields), so the firewall can tell a still-true block from one whose source has since changed and downgrade it to advisory',
1677
+ up: (db) => {
1678
+ // ============================================================
1679
+ // STALENESS ANCHORS (Failure Firewall staleness oracle). A recorded
1680
+ // failure can be ANCHORED to the source files it is about (the
1681
+ // undeclared `files` param of wyrm_failure_record — the content-param
1682
+ // precedent, T022 lean-union rule: the advertised ListTools surface
1683
+ // does not change). At check time the shared oracle
1684
+ // (FailurePatterns.staleAnchorIds) stats each anchor: size+mtime_ns
1685
+ // equal ⇒ fresh with zero hashing; on stat drift it re-hashes the
1686
+ // NORMALIZED content (normalizeContentSignature — whitespace/reformat
1687
+ // churn never false-flags) and only a POSITIVE hash mismatch marks the
1688
+ // match stale, downgrading a file/symbol-scoped hard block to advisory
1689
+ // (stale_anchor). Exact command/edit signature matches keep their
1690
+ // block — code moving on does not unprove a command failure
1691
+ // (PROTOCOL.md §10.2.5).
1692
+ //
1693
+ // Columns:
1694
+ // failure_id — the anchored failure (references failure_patterns;
1695
+ // rows are pruned in FailurePatterns.delete()).
1696
+ // rel_path — PROJECT-RELATIVE path (never absolute): anchors must
1697
+ // replicate meaningfully across fleet machines whose
1698
+ // checkouts live at different roots. Traversal-escaping
1699
+ // paths are rejected at write time.
1700
+ // sha — hex sha256 of normalizeContentSignature(content) at
1701
+ // record time (crypto.ts WyrmCrypto.hash).
1702
+ // size — byte size at record time (stat fast path).
1703
+ // mtime_ns — mtime in NANOSECONDS at record time (stat fast path;
1704
+ // INT64 — read back via safeIntegers, the value exceeds
1705
+ // JS safe-integer range).
1706
+ // recorded_at — when the anchor was taken.
1707
+ //
1708
+ // Cap: at most 8 DISTINCT anchored files per failure, enforced at
1709
+ // WRITE time (FailurePatterns.recordAnchors) — a schema CHECK cannot
1710
+ // count rows.
1711
+ //
1712
+ // ONE row per (failure_id, rel_path): record() coalesces repeat
1713
+ // failures onto one id, so recordAnchors REFRESHES an existing path's
1714
+ // anchor (delete-then-insert) instead of appending a second row beside
1715
+ // the old-sha one — otherwise a failure just re-confirmed against
1716
+ // CURRENT source would read permanently 'stale' (anchorFreshness is
1717
+ // 'stale' on ANY drifted row) and never regain its hard block. The
1718
+ // UNIQUE index enforces that shape at the schema level; failure_id
1719
+ // being its leftmost column, it also serves the exact per-failure
1720
+ // lookup the check path does per matched failure.
1721
+ //
1722
+ // Additive + guarded (Article VI), migration 25/26/27/28 style: a
1723
+ // brand-new table, nothing renamed/dropped; an older DB opens with
1724
+ // 100% of its rows and a pre-30 DB simply has no anchors (the oracle
1725
+ // fails open to "unanchored").
1726
+ db.exec(`
1727
+ CREATE TABLE IF NOT EXISTS failure_anchors (
1728
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1729
+ failure_id INTEGER NOT NULL REFERENCES failure_patterns(id) ON DELETE CASCADE,
1730
+ rel_path TEXT NOT NULL,
1731
+ sha TEXT NOT NULL,
1732
+ size INTEGER NOT NULL,
1733
+ mtime_ns INTEGER NOT NULL,
1734
+ recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
1735
+ );
1736
+ CREATE UNIQUE INDEX IF NOT EXISTS ux_failure_anchors_failure_rel
1737
+ ON failure_anchors(failure_id, rel_path);
1738
+ `);
1739
+ },
1740
+ },
1741
+ ];
1742
+ /**
1743
+ * Run all pending migrations on the database.
1744
+ * Creates the schema_versions table if it doesn't exist.
1745
+ * Returns the list of migrations that were applied.
1746
+ */
1747
+ export function runMigrations(db) {
1748
+ // Create the version tracking table (always safe to re-run)
1749
+ db.exec(`
1028
1750
  CREATE TABLE IF NOT EXISTS schema_versions (
1029
1751
  version INTEGER PRIMARY KEY,
1030
1752
  description TEXT NOT NULL,
1031
1753
  applied_at TEXT DEFAULT (datetime('now'))
1032
1754
  );
1033
- `);const E=e.prepare("SELECT COALESCE(MAX(version), 0) as v FROM schema_versions").get().v,t=n.filter(T=>T.version>E).slice().sort((T,o)=>T.version-o.version);if(t.length===0)return[];const s=[],i=e.prepare("INSERT INTO schema_versions (version, description) VALUES (?, ?)");for(const T of t)e.transaction(()=>{T.up(e),i.run(T.version,T.description)})(),s.push(T);return s}function N(e){try{return e.prepare("SELECT COALESCE(MAX(version), 0) as v FROM schema_versions").get().v}catch{return 0}}export{N as getSchemaVersion,n as migrations,d as runMigrations};
1755
+ `);
1756
+ const currentVersion = db.prepare('SELECT COALESCE(MAX(version), 0) as v FROM schema_versions').get().v;
1757
+ // Always apply in ascending version order, regardless of array order in
1758
+ // `migrations` (defensive — v3.9 added a migration that was authored out
1759
+ // of array order). Sorted copy so we don't mutate the exported array.
1760
+ const pending = migrations
1761
+ .filter(m => m.version > currentVersion)
1762
+ .slice()
1763
+ .sort((a, b) => a.version - b.version);
1764
+ if (pending.length === 0)
1765
+ return [];
1766
+ const applied = [];
1767
+ const insertVersion = db.prepare('INSERT INTO schema_versions (version, description) VALUES (?, ?)');
1768
+ for (const migration of pending) {
1769
+ // Each migration runs in its own transaction for atomicity
1770
+ const runOne = db.transaction(() => {
1771
+ migration.up(db);
1772
+ insertVersion.run(migration.version, migration.description);
1773
+ });
1774
+ runOne();
1775
+ applied.push(migration);
1776
+ }
1777
+ return applied;
1778
+ }
1779
+ /**
1780
+ * Get the current schema version.
1781
+ */
1782
+ export function getSchemaVersion(db) {
1783
+ try {
1784
+ return db.prepare('SELECT COALESCE(MAX(version), 0) as v FROM schema_versions').get().v;
1785
+ }
1786
+ catch {
1787
+ return 0; // schema_versions table doesn't exist yet
1788
+ }
1789
+ }
1790
+ //# sourceMappingURL=migrations.js.map