@cleocode/core 2026.4.11 → 2026.4.12

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 (169) hide show
  1. package/dist/codebase-map/analyzers/architecture.d.ts.map +1 -1
  2. package/dist/codebase-map/analyzers/architecture.js +0 -1
  3. package/dist/codebase-map/analyzers/architecture.js.map +1 -1
  4. package/dist/conduit/local-transport.d.ts +18 -8
  5. package/dist/conduit/local-transport.d.ts.map +1 -1
  6. package/dist/conduit/local-transport.js +23 -13
  7. package/dist/conduit/local-transport.js.map +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +0 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/errors.d.ts +19 -0
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/errors.js +6 -0
  14. package/dist/errors.js.map +1 -1
  15. package/dist/index.js +175 -68950
  16. package/dist/index.js.map +1 -7
  17. package/dist/init.d.ts +1 -2
  18. package/dist/init.d.ts.map +1 -1
  19. package/dist/init.js +1 -2
  20. package/dist/init.js.map +1 -1
  21. package/dist/internal.d.ts +8 -3
  22. package/dist/internal.d.ts.map +1 -1
  23. package/dist/internal.js +13 -6
  24. package/dist/internal.js.map +1 -1
  25. package/dist/memory/learnings.d.ts +2 -2
  26. package/dist/memory/patterns.d.ts +6 -6
  27. package/dist/output.d.ts +32 -11
  28. package/dist/output.d.ts.map +1 -1
  29. package/dist/output.js +67 -67
  30. package/dist/output.js.map +1 -1
  31. package/dist/paths.js +80 -14
  32. package/dist/paths.js.map +1 -1
  33. package/dist/skills/dynamic-skill-generator.d.ts +0 -2
  34. package/dist/skills/dynamic-skill-generator.d.ts.map +1 -1
  35. package/dist/skills/dynamic-skill-generator.js.map +1 -1
  36. package/dist/store/agent-registry-accessor.d.ts +203 -12
  37. package/dist/store/agent-registry-accessor.d.ts.map +1 -1
  38. package/dist/store/agent-registry-accessor.js +618 -100
  39. package/dist/store/agent-registry-accessor.js.map +1 -1
  40. package/dist/store/api-key-kdf.d.ts +73 -0
  41. package/dist/store/api-key-kdf.d.ts.map +1 -0
  42. package/dist/store/api-key-kdf.js +84 -0
  43. package/dist/store/api-key-kdf.js.map +1 -0
  44. package/dist/store/cleanup-legacy.js +171 -0
  45. package/dist/store/cleanup-legacy.js.map +1 -0
  46. package/dist/store/conduit-sqlite.d.ts +184 -0
  47. package/dist/store/conduit-sqlite.d.ts.map +1 -0
  48. package/dist/store/conduit-sqlite.js +570 -0
  49. package/dist/store/conduit-sqlite.js.map +1 -0
  50. package/dist/store/global-salt.d.ts +78 -0
  51. package/dist/store/global-salt.d.ts.map +1 -0
  52. package/dist/store/global-salt.js +147 -0
  53. package/dist/store/global-salt.js.map +1 -0
  54. package/dist/store/migrate-signaldock-to-conduit.d.ts +81 -0
  55. package/dist/store/migrate-signaldock-to-conduit.d.ts.map +1 -0
  56. package/dist/store/migrate-signaldock-to-conduit.js +555 -0
  57. package/dist/store/migrate-signaldock-to-conduit.js.map +1 -0
  58. package/dist/store/nexus-sqlite.js +28 -3
  59. package/dist/store/nexus-sqlite.js.map +1 -1
  60. package/dist/store/signaldock-sqlite.d.ts +122 -19
  61. package/dist/store/signaldock-sqlite.d.ts.map +1 -1
  62. package/dist/store/signaldock-sqlite.js +401 -251
  63. package/dist/store/signaldock-sqlite.js.map +1 -1
  64. package/dist/store/sqlite-backup.js +122 -4
  65. package/dist/store/sqlite-backup.js.map +1 -1
  66. package/dist/system/backup.d.ts +0 -26
  67. package/dist/system/backup.d.ts.map +1 -1
  68. package/dist/system/runtime.d.ts +0 -2
  69. package/dist/system/runtime.d.ts.map +1 -1
  70. package/dist/system/runtime.js +3 -3
  71. package/dist/system/runtime.js.map +1 -1
  72. package/dist/tasks/add.d.ts +1 -1
  73. package/dist/tasks/add.d.ts.map +1 -1
  74. package/dist/tasks/add.js +98 -23
  75. package/dist/tasks/add.js.map +1 -1
  76. package/dist/tasks/complete.d.ts.map +1 -1
  77. package/dist/tasks/complete.js +4 -1
  78. package/dist/tasks/complete.js.map +1 -1
  79. package/dist/tasks/find.d.ts.map +1 -1
  80. package/dist/tasks/find.js +4 -1
  81. package/dist/tasks/find.js.map +1 -1
  82. package/dist/tasks/labels.d.ts.map +1 -1
  83. package/dist/tasks/labels.js +4 -1
  84. package/dist/tasks/labels.js.map +1 -1
  85. package/dist/tasks/relates.d.ts.map +1 -1
  86. package/dist/tasks/relates.js +16 -4
  87. package/dist/tasks/relates.js.map +1 -1
  88. package/dist/tasks/show.d.ts.map +1 -1
  89. package/dist/tasks/show.js +4 -1
  90. package/dist/tasks/show.js.map +1 -1
  91. package/dist/tasks/update.d.ts.map +1 -1
  92. package/dist/tasks/update.js +32 -6
  93. package/dist/tasks/update.js.map +1 -1
  94. package/dist/validation/engine.d.ts.map +1 -1
  95. package/dist/validation/engine.js +16 -4
  96. package/dist/validation/engine.js.map +1 -1
  97. package/dist/validation/param-utils.d.ts +5 -3
  98. package/dist/validation/param-utils.d.ts.map +1 -1
  99. package/dist/validation/param-utils.js +8 -6
  100. package/dist/validation/param-utils.js.map +1 -1
  101. package/dist/validation/protocols/_shared.d.ts.map +1 -1
  102. package/dist/validation/protocols/_shared.js +13 -6
  103. package/dist/validation/protocols/_shared.js.map +1 -1
  104. package/package.json +7 -7
  105. package/src/adapters/__tests__/manager.test.ts +0 -1
  106. package/src/codebase-map/analyzers/architecture.ts +0 -1
  107. package/src/conduit/__tests__/local-credential-flow.test.ts +20 -18
  108. package/src/conduit/__tests__/local-transport.test.ts +14 -12
  109. package/src/conduit/local-transport.ts +23 -13
  110. package/src/config.ts +0 -1
  111. package/src/errors.ts +24 -0
  112. package/src/hooks/handlers/__tests__/hook-automation-e2e.test.ts +2 -5
  113. package/src/init.ts +1 -2
  114. package/src/internal.ts +49 -2
  115. package/src/lifecycle/cant/lifecycle-rcasd.cant +133 -0
  116. package/src/memory/__tests__/engine-compat.test.ts +2 -2
  117. package/src/memory/__tests__/pipeline-manifest-sqlite.test.ts +4 -4
  118. package/src/observability/__tests__/index.test.ts +4 -4
  119. package/src/observability/__tests__/log-filter.test.ts +4 -4
  120. package/src/output.ts +73 -75
  121. package/src/sessions/__tests__/session-grade.integration.test.ts +1 -1
  122. package/src/sessions/__tests__/session-grade.test.ts +2 -2
  123. package/src/skills/__tests__/dynamic-skill-generator.test.ts +0 -2
  124. package/src/skills/dynamic-skill-generator.ts +0 -2
  125. package/src/store/__tests__/agent-registry-accessor.test.ts +807 -0
  126. package/src/store/__tests__/api-key-kdf.test.ts +113 -0
  127. package/src/store/__tests__/conduit-sqlite.test.ts +413 -0
  128. package/src/store/__tests__/global-salt.test.ts +195 -0
  129. package/src/store/__tests__/migrate-signaldock-to-conduit.test.ts +715 -0
  130. package/src/store/__tests__/signaldock-sqlite.test.ts +652 -0
  131. package/src/store/__tests__/sqlite-backup-global.test.ts +307 -3
  132. package/src/store/__tests__/sqlite-backup.test.ts +5 -1
  133. package/src/store/__tests__/t310-integration.test.ts +1150 -0
  134. package/src/store/agent-registry-accessor.ts +847 -140
  135. package/src/store/api-key-kdf.ts +104 -0
  136. package/src/store/conduit-sqlite.ts +655 -0
  137. package/src/store/global-salt.ts +175 -0
  138. package/src/store/migrate-signaldock-to-conduit.ts +669 -0
  139. package/src/store/signaldock-sqlite.ts +431 -254
  140. package/src/store/sqlite-backup.ts +185 -10
  141. package/src/system/backup.ts +2 -62
  142. package/src/system/runtime.ts +4 -6
  143. package/src/tasks/__tests__/error-hints.test.ts +256 -0
  144. package/src/tasks/add.ts +99 -9
  145. package/src/tasks/complete.ts +4 -1
  146. package/src/tasks/find.ts +4 -1
  147. package/src/tasks/labels.ts +4 -1
  148. package/src/tasks/relates.ts +16 -4
  149. package/src/tasks/show.ts +4 -1
  150. package/src/tasks/update.ts +32 -3
  151. package/src/validation/__tests__/error-hints.test.ts +97 -0
  152. package/src/validation/engine.ts +16 -1
  153. package/src/validation/param-utils.ts +10 -7
  154. package/src/validation/protocols/_shared.ts +14 -6
  155. package/src/validation/protocols/cant/architecture-decision.cant +80 -0
  156. package/src/validation/protocols/cant/artifact-publish.cant +95 -0
  157. package/src/validation/protocols/cant/consensus.cant +74 -0
  158. package/src/validation/protocols/cant/contribution.cant +82 -0
  159. package/src/validation/protocols/cant/decomposition.cant +92 -0
  160. package/src/validation/protocols/cant/implementation.cant +67 -0
  161. package/src/validation/protocols/cant/provenance.cant +88 -0
  162. package/src/validation/protocols/cant/release.cant +96 -0
  163. package/src/validation/protocols/cant/research.cant +66 -0
  164. package/src/validation/protocols/cant/specification.cant +67 -0
  165. package/src/validation/protocols/cant/testing.cant +88 -0
  166. package/src/validation/protocols/cant/validation.cant +65 -0
  167. package/src/validation/protocols/protocols-markdown/decomposition.md +0 -4
  168. package/templates/config.template.json +0 -1
  169. package/templates/global-config.template.json +0 -1
@@ -0,0 +1,669 @@
1
+ /**
2
+ * Automatic first-run migration executor: project-tier signaldock.db → conduit.db.
3
+ *
4
+ * Implements ADR-037 §8 — on the first cleo invocation after upgrading to
5
+ * v2026.4.12, if a project-tier `.cleo/signaldock.db` exists and
6
+ * `.cleo/conduit.db` does not, this module migrates all project-local data to
7
+ * conduit.db and copies global-identity rows to the global-tier signaldock.db.
8
+ *
9
+ * Key properties:
10
+ * - Idempotent: `needsSignaldockToConduitMigration` returns false once conduit.db exists.
11
+ * - Atomic per file: transactions are used for conduit.db and global signaldock.db writes.
12
+ * - Non-destructive: legacy signaldock.db is renamed to `.pre-t310.bak`, not deleted.
13
+ * - Multi-project safe: `INSERT OR IGNORE` prevents duplicate global rows when multiple
14
+ * projects migrate the same agent.
15
+ * - Migrated agents flagged `requires_reauth=1` (new KDF scheme, ADR-037 §5).
16
+ *
17
+ * @task T358
18
+ * @epic T310
19
+ * @why ADR-037 §8 — automatic first-run migration from the pre-T310 project-tier
20
+ * signaldock.db to the new T310 topology (conduit.db + global signaldock.db
21
+ * + global-salt). Runs once per project on first cleo invocation after upgrade.
22
+ */
23
+
24
+ import { existsSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
25
+ import { createRequire } from 'node:module';
26
+ import { join } from 'node:path';
27
+ import type { DatabaseSync as _DatabaseSyncType } from 'node:sqlite';
28
+ import { getLogger } from '../logger.js';
29
+ import { getCleoHome } from '../paths.js';
30
+ import { ensureConduitDb } from './conduit-sqlite.js';
31
+ import { getGlobalSalt } from './global-salt.js';
32
+ import { ensureGlobalSignaldockDb } from './signaldock-sqlite.js';
33
+
34
+ const _require = createRequire(import.meta.url);
35
+ type DatabaseSync = _DatabaseSyncType;
36
+ const { DatabaseSync } = _require('node:sqlite') as {
37
+ DatabaseSync: new (...args: ConstructorParameters<typeof _DatabaseSyncType>) => DatabaseSync;
38
+ };
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Types
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /**
45
+ * Result returned by `migrateSignaldockToConduit`.
46
+ *
47
+ * @task T358
48
+ * @epic T310
49
+ */
50
+ export interface MigrationResult {
51
+ /** `migrated` = migration ran and succeeded; `no-op` = not needed; `failed` = error occurred. */
52
+ status: 'migrated' | 'no-op' | 'failed';
53
+ /** Absolute path to the project root that was migrated. */
54
+ projectRoot: string;
55
+ /** Number of agents copied to global signaldock.db. */
56
+ agentsCopied: number;
57
+ /** Absolute path to the conduit.db that was created. */
58
+ conduitPath: string;
59
+ /** Absolute path to the global signaldock.db. */
60
+ globalSignaldockPath: string;
61
+ /** Absolute path to the legacy `.pre-t310.bak` file, or null if rename did not complete. */
62
+ bakPath: string | null;
63
+ /** Errors encountered during migration steps. Never thrown — always captured here. */
64
+ errors: Array<{ step: string; error: string }>;
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Constants
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /** Project-local tables to copy verbatim from legacy signaldock.db to conduit.db. */
72
+ const PROJECT_TIER_TABLES = [
73
+ 'messages',
74
+ 'conversations',
75
+ 'delivery_jobs',
76
+ 'dead_letters',
77
+ 'message_pins',
78
+ 'attachments',
79
+ 'attachment_versions',
80
+ 'attachment_approvals',
81
+ 'attachment_contributors',
82
+ ] as const;
83
+
84
+ /** Global-identity tables to copy using INSERT OR IGNORE from legacy to global signaldock.db. */
85
+ const GLOBAL_IDENTITY_TABLES = [
86
+ 'agents',
87
+ 'capabilities',
88
+ 'skills',
89
+ 'agent_capabilities',
90
+ 'agent_skills',
91
+ 'agent_connections',
92
+ 'users',
93
+ 'accounts',
94
+ 'sessions',
95
+ 'verifications',
96
+ 'organization',
97
+ 'claim_codes',
98
+ 'org_agent_keys',
99
+ ] as const;
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Detection
103
+ // ---------------------------------------------------------------------------
104
+
105
+ /**
106
+ * Returns true when the legacy migration is needed for the given project.
107
+ *
108
+ * Detection heuristic (ADR-037 §8):
109
+ * - `.cleo/signaldock.db` EXISTS AND `.cleo/conduit.db` DOES NOT EXIST
110
+ *
111
+ * Idempotent: returns false once conduit.db is present, regardless of .bak state.
112
+ *
113
+ * @param projectRoot - Absolute path to the project root directory.
114
+ * @returns True if migration is needed; false otherwise.
115
+ *
116
+ * @task T358
117
+ * @epic T310
118
+ */
119
+ export function needsSignaldockToConduitMigration(projectRoot: string): boolean {
120
+ const legacyPath = join(projectRoot, '.cleo', 'signaldock.db');
121
+ const conduitPath = join(projectRoot, '.cleo', 'conduit.db');
122
+ return existsSync(legacyPath) && !existsSync(conduitPath);
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Helpers
127
+ // ---------------------------------------------------------------------------
128
+
129
+ /**
130
+ * Check whether a table exists in the given database.
131
+ *
132
+ * @param db - An open DatabaseSync handle.
133
+ * @param tableName - Name of the table to check.
134
+ * @returns True if the table exists in sqlite_master.
135
+ */
136
+ function tableExistsInDb(db: DatabaseSync, tableName: string): boolean {
137
+ const row = db
138
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?")
139
+ .get(tableName) as { name: string } | undefined;
140
+ return !!row;
141
+ }
142
+
143
+ /**
144
+ * Get column names from a table in the given database.
145
+ *
146
+ * @param db - An open DatabaseSync handle.
147
+ * @param tableName - Name of the table.
148
+ * @returns Array of column names.
149
+ */
150
+ function getTableColumns(db: DatabaseSync, tableName: string): string[] {
151
+ const rows = db.prepare(`PRAGMA table_info("${tableName}")`).all() as Array<{
152
+ name: string;
153
+ cid: number;
154
+ type: string;
155
+ notnull: number;
156
+ dflt_value: string | null;
157
+ pk: number;
158
+ }>;
159
+ return rows.map((r) => r.name);
160
+ }
161
+
162
+ /**
163
+ * Copy all rows from `tableName` in `srcDb` to the same table in `destDb`.
164
+ * Only columns that exist in both source and destination are copied (schema safety).
165
+ * Uses INSERT OR IGNORE to handle unique constraint conflicts gracefully.
166
+ *
167
+ * @param srcDb - Source database (legacy signaldock.db).
168
+ * @param destDb - Destination database (conduit.db or global signaldock.db).
169
+ * @param tableName - Table to copy.
170
+ * @param ignoreConflicts - When true uses INSERT OR IGNORE; when false uses INSERT.
171
+ * @returns Number of rows found in source (not necessarily inserted — some may have been ignored).
172
+ */
173
+ function copyTableRows(
174
+ srcDb: DatabaseSync,
175
+ destDb: DatabaseSync,
176
+ tableName: string,
177
+ ignoreConflicts = false,
178
+ ): number {
179
+ const srcCols = getTableColumns(srcDb, tableName);
180
+ const destCols = getTableColumns(destDb, tableName);
181
+ // Only copy columns that exist in both tables
182
+ const cols = srcCols.filter((c) => destCols.includes(c));
183
+ if (cols.length === 0) return 0;
184
+
185
+ const colList = cols.map((c) => `"${c}"`).join(', ');
186
+ const placeholders = cols.map(() => '?').join(', ');
187
+ const conflictClause = ignoreConflicts ? 'OR IGNORE' : '';
188
+ const insertSql = `INSERT ${conflictClause} INTO "${tableName}" (${colList}) VALUES (${placeholders})`;
189
+
190
+ const rows = srcDb.prepare(`SELECT ${colList} FROM "${tableName}"`).all() as Record<
191
+ string,
192
+ unknown
193
+ >[];
194
+ if (rows.length === 0) return 0;
195
+
196
+ const stmt = destDb.prepare(insertSql);
197
+ for (const row of rows) {
198
+ const values = cols.map((c) => row[c] ?? null);
199
+ stmt.run(...values);
200
+ }
201
+ return rows.length;
202
+ }
203
+
204
+ /**
205
+ * Run PRAGMA integrity_check on `db` and return true if it passes.
206
+ *
207
+ * @param db - An open DatabaseSync handle.
208
+ * @returns True if integrity_check returns 'ok'.
209
+ */
210
+ function integrityCheckPasses(db: DatabaseSync): boolean {
211
+ const rows = db.prepare('PRAGMA integrity_check').all() as Array<{ integrity_check: string }>;
212
+ return rows.length === 1 && rows[0]?.integrity_check === 'ok';
213
+ }
214
+
215
+ /**
216
+ * Generate a timestamp suffix for broken-file names: `YYYYMMDD-HHmmss-mmm`.
217
+ */
218
+ function brokenTimestamp(): string {
219
+ const now = new Date();
220
+ const pad2 = (n: number) => String(n).padStart(2, '0');
221
+ const pad3 = (n: number) => String(n).padStart(3, '0');
222
+ return (
223
+ `${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}` +
224
+ `-${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}` +
225
+ `-${pad3(now.getMilliseconds())}`
226
+ );
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // Main executor
231
+ // ---------------------------------------------------------------------------
232
+
233
+ /**
234
+ * Runs the signaldock.db → conduit.db migration for the given project.
235
+ *
236
+ * The migration is atomic per file (SQLite transactions for DB writes, atomic
237
+ * rename for the backup step). Failures are captured in `result.errors` and
238
+ * the function never throws — callers receive a `MigrationResult` with
239
+ * `status: 'failed'` on error.
240
+ *
241
+ * Safe to call when no migration is needed (returns `{status: 'no-op', ...}`).
242
+ * Safe to call on a partially-migrated install (idempotent via needsMigration check).
243
+ *
244
+ * @param projectRoot - Absolute path to the project root directory.
245
+ * @returns A `MigrationResult` describing what was done and any errors encountered.
246
+ *
247
+ * @task T358
248
+ * @epic T310
249
+ */
250
+ export function migrateSignaldockToConduit(projectRoot: string): MigrationResult {
251
+ const log = getLogger('migrate-signaldock-to-conduit');
252
+ const legacyPath = join(projectRoot, '.cleo', 'signaldock.db');
253
+ const conduitPath = join(projectRoot, '.cleo', 'conduit.db');
254
+ const globalSignaldockPath = join(getCleoHome(), 'signaldock.db');
255
+ const bakPath = `${legacyPath}.pre-t310.bak`;
256
+
257
+ const result: MigrationResult = {
258
+ status: 'no-op',
259
+ projectRoot,
260
+ agentsCopied: 0,
261
+ conduitPath,
262
+ globalSignaldockPath,
263
+ bakPath: null,
264
+ errors: [],
265
+ };
266
+
267
+ // -----------------------------------------------------------------------
268
+ // Step 1: Check if migration is needed
269
+ // -----------------------------------------------------------------------
270
+ if (!needsSignaldockToConduitMigration(projectRoot)) {
271
+ return result;
272
+ }
273
+
274
+ log.info({ projectRoot, legacyPath }, 'T310 migration: starting signaldock.db → conduit.db');
275
+
276
+ // -----------------------------------------------------------------------
277
+ // Step 2: Open legacy signaldock.db in READ-ONLY mode
278
+ // -----------------------------------------------------------------------
279
+ let legacy: DatabaseSync | null = null;
280
+ try {
281
+ legacy = new DatabaseSync(legacyPath, { readonly: true });
282
+ } catch (err) {
283
+ const message = err instanceof Error ? err.message : String(err);
284
+ log.error({ legacyPath, error: message }, 'T310 migration: cannot open legacy signaldock.db');
285
+ result.errors.push({ step: 'step-2-open-legacy', error: message });
286
+ result.status = 'failed';
287
+ return result;
288
+ }
289
+
290
+ // -----------------------------------------------------------------------
291
+ // Step 3: Verify legacy integrity
292
+ // -----------------------------------------------------------------------
293
+ try {
294
+ if (!integrityCheckPasses(legacy)) {
295
+ const msg =
296
+ 'Legacy signaldock.db failed PRAGMA integrity_check. ' +
297
+ 'Migration aborted — no changes written. ' +
298
+ 'Recovery: inspect the database with sqlite3 and attempt manual repair before re-running.';
299
+ log.error({ legacyPath }, msg);
300
+ result.errors.push({ step: 'step-3-legacy-integrity', error: msg });
301
+ result.status = 'failed';
302
+ legacy.close();
303
+ return result;
304
+ }
305
+ } catch (err) {
306
+ const message = err instanceof Error ? err.message : String(err);
307
+ log.error({ legacyPath, error: message }, 'T310 migration: integrity_check threw on legacy DB');
308
+ result.errors.push({ step: 'step-3-legacy-integrity', error: message });
309
+ result.status = 'failed';
310
+ legacy.close();
311
+ return result;
312
+ }
313
+
314
+ // -----------------------------------------------------------------------
315
+ // Steps 4–5: Ensure global cleo home + global signaldock.db exist
316
+ // -----------------------------------------------------------------------
317
+ const cleoHome = getCleoHome();
318
+ try {
319
+ if (!existsSync(cleoHome)) {
320
+ mkdirSync(cleoHome, { recursive: true });
321
+ }
322
+ // ensureGlobalSignaldockDb is async but we need it sync here; open directly.
323
+ // The global signaldock schema is applied by ensureGlobalSignaldockDb() when
324
+ // the CLI starts. For migration purposes we open the DB (creating it if needed)
325
+ // and rely on the schema already having been applied (or apply it inline).
326
+ // We call ensureGlobalSignaldockDb() in a fire-and-forget style here, and
327
+ // handle the open synchronously below with DatabaseSync.
328
+ void ensureGlobalSignaldockDb();
329
+ } catch {
330
+ // Non-fatal: the global DB open below will create the file if needed.
331
+ }
332
+
333
+ // -----------------------------------------------------------------------
334
+ // Step 6: Ensure global-salt exists (generates if absent)
335
+ // -----------------------------------------------------------------------
336
+ try {
337
+ getGlobalSalt();
338
+ } catch (err) {
339
+ const message = err instanceof Error ? err.message : String(err);
340
+ log.error({ error: message }, 'T310 migration: getGlobalSalt failed — migration aborted');
341
+ result.errors.push({ step: 'step-6-global-salt', error: message });
342
+ result.status = 'failed';
343
+ legacy.close();
344
+ return result;
345
+ }
346
+
347
+ // -----------------------------------------------------------------------
348
+ // Step 7: Create conduit.db via ensureConduitDb (applies full schema)
349
+ // -----------------------------------------------------------------------
350
+ let conduit: DatabaseSync | null = null;
351
+ try {
352
+ const ensureResult = ensureConduitDb(projectRoot);
353
+ // Open a direct handle for migration writes (ensureConduitDb returns singleton;
354
+ // we open a fresh handle to avoid interfering with any singleton state).
355
+ conduit = new DatabaseSync(ensureResult.path);
356
+ conduit.exec('PRAGMA journal_mode = WAL');
357
+ conduit.exec('PRAGMA foreign_keys = OFF'); // Disable FK checks during bulk copy
358
+ } catch (err) {
359
+ const message = err instanceof Error ? err.message : String(err);
360
+ log.error({ conduitPath, error: message }, 'T310 migration: failed to create conduit.db');
361
+ result.errors.push({ step: 'step-7-create-conduit', error: message });
362
+ result.status = 'failed';
363
+ legacy.close();
364
+ return result;
365
+ }
366
+
367
+ // -----------------------------------------------------------------------
368
+ // Steps 8–9: Copy project-tier tables + derive project_agent_refs + COMMIT
369
+ // -----------------------------------------------------------------------
370
+ try {
371
+ conduit.exec('BEGIN TRANSACTION');
372
+
373
+ // Copy project-local messaging tables verbatim
374
+ for (const tableName of PROJECT_TIER_TABLES) {
375
+ if (tableExistsInDb(legacy, tableName) && tableExistsInDb(conduit, tableName)) {
376
+ copyTableRows(legacy, conduit, tableName, false);
377
+ }
378
+ }
379
+
380
+ // Rebuild FTS after message copy
381
+ if (tableExistsInDb(conduit, 'messages_fts')) {
382
+ try {
383
+ conduit.exec("INSERT INTO messages_fts(messages_fts) VALUES('rebuild')");
384
+ } catch {
385
+ // FTS rebuild failure is non-fatal — the triggers will populate it over time
386
+ }
387
+ }
388
+
389
+ // Derive project_agent_refs from legacy agents table
390
+ let agentsCountForConduit = 0;
391
+ if (tableExistsInDb(legacy, 'agents')) {
392
+ const legacyAgents = legacy.prepare('SELECT * FROM agents').all() as Array<
393
+ Record<string, unknown>
394
+ >;
395
+
396
+ const hasClassificationCol = getTableColumns(legacy, 'agents').includes('classification');
397
+ const hasLastUsedAtCol = getTableColumns(legacy, 'agents').includes('last_used_at');
398
+ const hasCreatedAtCol = getTableColumns(legacy, 'agents').includes('created_at');
399
+ const hasAgentIdCol = getTableColumns(legacy, 'agents').includes('agent_id');
400
+
401
+ for (const agent of legacyAgents) {
402
+ const agentId = hasAgentIdCol ? (agent['agent_id'] as string) : (agent['id'] as string);
403
+ if (!agentId) continue;
404
+
405
+ const createdAtRaw = hasCreatedAtCol ? (agent['created_at'] as number | null) : null;
406
+ const attachedAt =
407
+ createdAtRaw != null
408
+ ? new Date(createdAtRaw * 1000).toISOString()
409
+ : new Date().toISOString();
410
+
411
+ const role = hasClassificationCol ? (agent['classification'] as string | null) : null;
412
+
413
+ const lastUsedAtRaw = hasLastUsedAtCol ? (agent['last_used_at'] as number | null) : null;
414
+ const lastUsedAt =
415
+ lastUsedAtRaw != null ? new Date(lastUsedAtRaw * 1000).toISOString() : null;
416
+
417
+ conduit
418
+ .prepare(
419
+ `INSERT OR IGNORE INTO project_agent_refs
420
+ (agent_id, attached_at, role, capabilities_override, last_used_at, enabled)
421
+ VALUES (?, ?, ?, NULL, ?, 1)`,
422
+ )
423
+ .run(agentId, attachedAt, role, lastUsedAt);
424
+
425
+ agentsCountForConduit++;
426
+ }
427
+ }
428
+
429
+ conduit.exec('COMMIT');
430
+ result.agentsCopied = agentsCountForConduit;
431
+ } catch (err) {
432
+ const message = err instanceof Error ? err.message : String(err);
433
+ log.error({ error: message }, 'T310 migration: conduit.db write failed — rolling back');
434
+ result.errors.push({ step: 'step-8-conduit-write', error: message });
435
+ result.status = 'failed';
436
+
437
+ try {
438
+ conduit.exec('ROLLBACK');
439
+ } catch {
440
+ // Ignore rollback errors
441
+ }
442
+ conduit.close();
443
+ conduit = null;
444
+ // Delete partial conduit.db
445
+ try {
446
+ if (existsSync(conduitPath)) {
447
+ unlinkSync(conduitPath);
448
+ }
449
+ } catch {
450
+ // Ignore deletion errors
451
+ }
452
+ legacy.close();
453
+ return result;
454
+ }
455
+
456
+ // -----------------------------------------------------------------------
457
+ // Step 10: PRAGMA integrity_check on conduit.db
458
+ // -----------------------------------------------------------------------
459
+ try {
460
+ if (!integrityCheckPasses(conduit)) {
461
+ const msg = 'conduit.db failed PRAGMA integrity_check after write';
462
+ log.error({ conduitPath }, msg);
463
+ result.errors.push({ step: 'step-10-conduit-integrity', error: msg });
464
+ result.status = 'failed';
465
+ conduit.close();
466
+ conduit = null;
467
+ // Move broken conduit.db
468
+ const brokenPath = `${conduitPath}.broken-${brokenTimestamp()}`;
469
+ try {
470
+ renameSync(conduitPath, brokenPath);
471
+ } catch {
472
+ // Ignore rename errors
473
+ }
474
+ legacy.close();
475
+ return result;
476
+ }
477
+ } catch (err) {
478
+ const message = err instanceof Error ? err.message : String(err);
479
+ log.error({ error: message }, 'T310 migration: conduit.db integrity_check threw');
480
+ result.errors.push({ step: 'step-10-conduit-integrity', error: message });
481
+ result.status = 'failed';
482
+ conduit.close();
483
+ conduit = null;
484
+ legacy.close();
485
+ return result;
486
+ }
487
+
488
+ // Close our migration handle for conduit; the singleton from ensureConduitDb is untouched
489
+ conduit.close();
490
+ conduit = null;
491
+
492
+ // -----------------------------------------------------------------------
493
+ // Steps 11–13: Copy global-identity rows to global signaldock.db
494
+ // -----------------------------------------------------------------------
495
+ let globalDb: DatabaseSync | null = null;
496
+ try {
497
+ if (!existsSync(globalSignaldockPath)) {
498
+ // Ensure the schema exists — open and apply minimal schema
499
+ // ensureGlobalSignaldockDb() was called above (async); give it a chance
500
+ // to complete by opening directly here.
501
+ mkdirSync(cleoHome, { recursive: true });
502
+ }
503
+ globalDb = new DatabaseSync(globalSignaldockPath);
504
+ globalDb.exec('PRAGMA journal_mode = WAL');
505
+ globalDb.exec('PRAGMA foreign_keys = OFF'); // Disable FK checks during bulk copy
506
+ } catch (err) {
507
+ const message = err instanceof Error ? err.message : String(err);
508
+ log.error(
509
+ { globalSignaldockPath, error: message },
510
+ 'T310 migration: cannot open global signaldock.db',
511
+ );
512
+ result.errors.push({ step: 'step-11-open-global', error: message });
513
+ result.status = 'failed';
514
+ legacy.close();
515
+ return result;
516
+ }
517
+
518
+ let agentsCopiedToGlobal = 0;
519
+ try {
520
+ globalDb.exec('BEGIN TRANSACTION');
521
+
522
+ // Copy all global-identity tables using INSERT OR IGNORE
523
+ for (const tableName of GLOBAL_IDENTITY_TABLES) {
524
+ if (tableExistsInDb(legacy, tableName) && tableExistsInDb(globalDb, tableName)) {
525
+ copyTableRows(legacy, globalDb, tableName, true /* ignoreConflicts */);
526
+ }
527
+ }
528
+
529
+ // Count agents actually in legacy
530
+ if (tableExistsInDb(legacy, 'agents')) {
531
+ const countRow = legacy.prepare('SELECT COUNT(*) as cnt FROM agents').get() as {
532
+ cnt: number;
533
+ };
534
+ agentsCopiedToGlobal = countRow.cnt;
535
+ }
536
+
537
+ // Mark ALL migrated agents as requiring reauth (new KDF scheme)
538
+ // Only mark rows that came from THIS legacy (i.e., all rows — INSERT OR IGNORE
539
+ // means existing rows were skipped; their requires_reauth stays unchanged).
540
+ // We mark every agent we inserted (agent_id from legacy) as requires_reauth=1.
541
+ if (tableExistsInDb(legacy, 'agents') && tableExistsInDb(globalDb, 'agents')) {
542
+ const legacyAgentIds = legacy.prepare('SELECT agent_id FROM agents').all() as Array<{
543
+ agent_id: string;
544
+ }>;
545
+
546
+ for (const { agent_id } of legacyAgentIds) {
547
+ // Only update if this is a newly-inserted row (requires_reauth=0 means it was
548
+ // either newly inserted or pre-existing with requires_reauth not set).
549
+ // We unconditionally set requires_reauth=1 for all agents from this migration;
550
+ // if the row was already present (INSERT OR IGNORE skipped it), we leave it.
551
+ // To detect new insertions: compare changes() or use a marker.
552
+ // Per spec: mark all migrated agents requires_reauth=1.
553
+ // We use a conservative approach: only update if the row was NOT pre-existing.
554
+ // Since INSERT OR IGNORE doesn't tell us if a row was inserted or ignored,
555
+ // we use the approach of: set requires_reauth=1 WHERE requires_reauth=0.
556
+ // This safely marks newly-inserted agents and leaves pre-existing ones unchanged.
557
+ globalDb
558
+ .prepare(
559
+ 'UPDATE agents SET requires_reauth = 1 WHERE agent_id = ? AND requires_reauth = 0',
560
+ )
561
+ .run(agent_id);
562
+ }
563
+ }
564
+
565
+ globalDb.exec('COMMIT');
566
+ result.agentsCopied = agentsCopiedToGlobal;
567
+ } catch (err) {
568
+ const message = err instanceof Error ? err.message : String(err);
569
+ log.error(
570
+ { error: message },
571
+ 'T310 migration: global signaldock.db write failed — rolling back',
572
+ );
573
+ result.errors.push({ step: 'step-12-global-write', error: message });
574
+ result.status = 'failed';
575
+
576
+ try {
577
+ globalDb.exec('ROLLBACK');
578
+ } catch {
579
+ // Ignore rollback errors
580
+ }
581
+ globalDb.close();
582
+ globalDb = null;
583
+ legacy.close();
584
+ // conduit.db is valid — leave it in place
585
+ return result;
586
+ }
587
+
588
+ // -----------------------------------------------------------------------
589
+ // Step 14: PRAGMA integrity_check on global signaldock.db
590
+ // -----------------------------------------------------------------------
591
+ try {
592
+ if (!integrityCheckPasses(globalDb)) {
593
+ const msg = 'Global signaldock.db failed PRAGMA integrity_check after write';
594
+ log.error({ globalSignaldockPath }, msg);
595
+ result.errors.push({ step: 'step-14-global-integrity', error: msg });
596
+ result.status = 'failed';
597
+ globalDb.close();
598
+ globalDb = null;
599
+ const brokenPath = `${globalSignaldockPath}.broken-${brokenTimestamp()}`;
600
+ try {
601
+ renameSync(globalSignaldockPath, brokenPath);
602
+ } catch {
603
+ // Ignore rename errors
604
+ }
605
+ legacy.close();
606
+ return result;
607
+ }
608
+ } catch (err) {
609
+ const message = err instanceof Error ? err.message : String(err);
610
+ log.error({ error: message }, 'T310 migration: global signaldock.db integrity_check threw');
611
+ result.errors.push({ step: 'step-14-global-integrity', error: message });
612
+ result.status = 'failed';
613
+ globalDb.close();
614
+ globalDb = null;
615
+ legacy.close();
616
+ return result;
617
+ }
618
+
619
+ globalDb.close();
620
+ globalDb = null;
621
+
622
+ // -----------------------------------------------------------------------
623
+ // Step 15: Ensure global-salt exists (already done in step 6, re-check)
624
+ // -----------------------------------------------------------------------
625
+ // Salt was already ensured in step 6 — no repeat action needed.
626
+
627
+ // -----------------------------------------------------------------------
628
+ // Step 16: Atomic rename legacy → .pre-t310.bak
629
+ // -----------------------------------------------------------------------
630
+ legacy.close();
631
+ legacy = null;
632
+
633
+ try {
634
+ renameSync(legacyPath, bakPath);
635
+ result.bakPath = bakPath;
636
+ } catch (err) {
637
+ const message = err instanceof Error ? err.message : String(err);
638
+ // Non-fatal per spec §4.4: next run will re-attempt (conduit.db absent check
639
+ // handles idempotency). But here conduit.db IS present, so needsMigration
640
+ // returns false on next run — the legacy file stays around harmlessly.
641
+ log.error(
642
+ { legacyPath, bakPath, error: message },
643
+ 'T310 migration: rename to .pre-t310.bak failed — legacy file left in place (harmless)',
644
+ );
645
+ result.errors.push({ step: 'step-16-rename-bak', error: message });
646
+ // Still consider the migration successful — conduit.db and global signaldock.db are valid.
647
+ }
648
+
649
+ // -----------------------------------------------------------------------
650
+ // Step 17: Success logs
651
+ // -----------------------------------------------------------------------
652
+ log.info(
653
+ { projectRoot, agentsCopied: result.agentsCopied, conduitPath, bakPath: result.bakPath },
654
+ `T310 migration complete: ${result.agentsCopied} agents migrated to global, conduit.db created`,
655
+ );
656
+ log.warn(
657
+ {},
658
+ 'T310 migration: API keys have been re-keyed. External systems holding old API keys ' +
659
+ '(CI env vars, remote agent configs) must be updated.',
660
+ );
661
+ log.info(
662
+ { legacyPath, bakPath: result.bakPath, conduitPath },
663
+ 'T310 migration recovery: if problems occur, rename .pre-t310.bak to signaldock.db ' +
664
+ 'and delete conduit.db to re-run migration.',
665
+ );
666
+
667
+ result.status = 'migrated';
668
+ return result;
669
+ }