@cluesmith/codev 3.2.1 → 3.2.2

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 (146) hide show
  1. package/dashboard-dist/assets/{index-DM7OU-EI.js → index-BZ1x7x1X.js} +34 -34
  2. package/dashboard-dist/assets/index-BZ1x7x1X.js.map +1 -0
  3. package/dashboard-dist/index.html +1 -1
  4. package/dist/agent-farm/cli.d.ts.map +1 -1
  5. package/dist/agent-farm/cli.js +17 -0
  6. package/dist/agent-farm/cli.js.map +1 -1
  7. package/dist/agent-farm/commands/architect.d.ts.map +1 -1
  8. package/dist/agent-farm/commands/architect.js +6 -17
  9. package/dist/agent-farm/commands/architect.js.map +1 -1
  10. package/dist/agent-farm/commands/cleanup.js +3 -2
  11. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  12. package/dist/agent-farm/commands/db.d.ts +10 -0
  13. package/dist/agent-farm/commands/db.d.ts.map +1 -1
  14. package/dist/agent-farm/commands/db.js +77 -0
  15. package/dist/agent-farm/commands/db.js.map +1 -1
  16. package/dist/agent-farm/commands/send.d.ts +40 -12
  17. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  18. package/dist/agent-farm/commands/send.js +101 -34
  19. package/dist/agent-farm/commands/send.js.map +1 -1
  20. package/dist/agent-farm/commands/spawn-worktree.d.ts +12 -7
  21. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  22. package/dist/agent-farm/commands/spawn-worktree.js +40 -19
  23. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  24. package/dist/agent-farm/commands/spawn.d.ts +15 -6
  25. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  26. package/dist/agent-farm/commands/spawn.js +24 -17
  27. package/dist/agent-farm/commands/spawn.js.map +1 -1
  28. package/dist/agent-farm/commands/stop.js +2 -2
  29. package/dist/agent-farm/commands/stop.js.map +1 -1
  30. package/dist/agent-farm/commands/tower.d.ts +1 -0
  31. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  32. package/dist/agent-farm/commands/tower.js +50 -0
  33. package/dist/agent-farm/commands/tower.js.map +1 -1
  34. package/dist/agent-farm/db/consolidate.d.ts +66 -0
  35. package/dist/agent-farm/db/consolidate.d.ts.map +1 -0
  36. package/dist/agent-farm/db/consolidate.js +406 -0
  37. package/dist/agent-farm/db/consolidate.js.map +1 -0
  38. package/dist/agent-farm/db/index.d.ts +13 -4
  39. package/dist/agent-farm/db/index.d.ts.map +1 -1
  40. package/dist/agent-farm/db/index.js +100 -470
  41. package/dist/agent-farm/db/index.js.map +1 -1
  42. package/dist/agent-farm/db/schema.d.ts +11 -4
  43. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  44. package/dist/agent-farm/db/schema.js +91 -2
  45. package/dist/agent-farm/db/schema.js.map +1 -1
  46. package/dist/agent-farm/db/types.d.ts +2 -0
  47. package/dist/agent-farm/db/types.d.ts.map +1 -1
  48. package/dist/agent-farm/db/types.js +1 -0
  49. package/dist/agent-farm/db/types.js.map +1 -1
  50. package/dist/agent-farm/lib/builder-lookup.d.ts.map +1 -1
  51. package/dist/agent-farm/lib/builder-lookup.js +9 -3
  52. package/dist/agent-farm/lib/builder-lookup.js.map +1 -1
  53. package/dist/agent-farm/servers/codev-config-watcher.d.ts +37 -0
  54. package/dist/agent-farm/servers/codev-config-watcher.d.ts.map +1 -0
  55. package/dist/agent-farm/servers/{worktree-config-watcher.js → codev-config-watcher.js} +15 -14
  56. package/dist/agent-farm/servers/codev-config-watcher.js.map +1 -0
  57. package/dist/agent-farm/servers/command-relay.d.ts +54 -0
  58. package/dist/agent-farm/servers/command-relay.d.ts.map +1 -0
  59. package/dist/agent-farm/servers/command-relay.js +94 -0
  60. package/dist/agent-farm/servers/command-relay.js.map +1 -0
  61. package/dist/agent-farm/servers/overview.d.ts.map +1 -1
  62. package/dist/agent-farm/servers/overview.js +12 -6
  63. package/dist/agent-farm/servers/overview.js.map +1 -1
  64. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  65. package/dist/agent-farm/servers/tower-instances.js +65 -55
  66. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  67. package/dist/agent-farm/servers/tower-messages.d.ts +9 -0
  68. package/dist/agent-farm/servers/tower-messages.d.ts.map +1 -1
  69. package/dist/agent-farm/servers/tower-messages.js +24 -0
  70. package/dist/agent-farm/servers/tower-messages.js.map +1 -1
  71. package/dist/agent-farm/servers/tower-routes.d.ts +1 -1
  72. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  73. package/dist/agent-farm/servers/tower-routes.js +111 -35
  74. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  75. package/dist/agent-farm/servers/tower-server.js +41 -27
  76. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  77. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  78. package/dist/agent-farm/servers/tower-terminals.js +22 -5
  79. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  80. package/dist/agent-farm/servers/tower-types.d.ts +2 -0
  81. package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
  82. package/dist/agent-farm/servers/tower-utils.d.ts +50 -5
  83. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
  84. package/dist/agent-farm/servers/tower-utils.js +64 -14
  85. package/dist/agent-farm/servers/tower-utils.js.map +1 -1
  86. package/dist/agent-farm/state.d.ts +34 -21
  87. package/dist/agent-farm/state.d.ts.map +1 -1
  88. package/dist/agent-farm/state.js +125 -79
  89. package/dist/agent-farm/state.js.map +1 -1
  90. package/dist/agent-farm/types.d.ts +14 -0
  91. package/dist/agent-farm/types.d.ts.map +1 -1
  92. package/dist/agent-farm/utils/config.d.ts +24 -1
  93. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  94. package/dist/agent-farm/utils/config.js +59 -3
  95. package/dist/agent-farm/utils/config.js.map +1 -1
  96. package/dist/agent-farm/utils/harness.d.ts +45 -2
  97. package/dist/agent-farm/utils/harness.d.ts.map +1 -1
  98. package/dist/agent-farm/utils/harness.js +21 -0
  99. package/dist/agent-farm/utils/harness.js.map +1 -1
  100. package/dist/agent-farm/utils/workspace-path.d.ts +16 -0
  101. package/dist/agent-farm/utils/workspace-path.d.ts.map +1 -0
  102. package/dist/agent-farm/utils/workspace-path.js +26 -0
  103. package/dist/agent-farm/utils/workspace-path.js.map +1 -0
  104. package/dist/agent-farm/utils/worktree-write-guard.d.ts +56 -0
  105. package/dist/agent-farm/utils/worktree-write-guard.d.ts.map +1 -0
  106. package/dist/agent-farm/utils/worktree-write-guard.js +222 -0
  107. package/dist/agent-farm/utils/worktree-write-guard.js.map +1 -0
  108. package/dist/cli.d.ts.map +1 -1
  109. package/dist/cli.js +2 -0
  110. package/dist/cli.js.map +1 -1
  111. package/dist/commands/consult/index.d.ts +50 -2
  112. package/dist/commands/consult/index.d.ts.map +1 -1
  113. package/dist/commands/consult/index.js +121 -5
  114. package/dist/commands/consult/index.js.map +1 -1
  115. package/dist/commands/doctor.d.ts.map +1 -1
  116. package/dist/commands/doctor.js +24 -2
  117. package/dist/commands/doctor.js.map +1 -1
  118. package/dist/commands/porch/verdict.js +2 -2
  119. package/dist/commands/porch/verdict.js.map +1 -1
  120. package/dist/lib/config.d.ts +10 -0
  121. package/dist/lib/config.d.ts.map +1 -1
  122. package/dist/lib/config.js.map +1 -1
  123. package/dist/lib/forge-contracts.d.ts +8 -0
  124. package/dist/lib/forge-contracts.d.ts.map +1 -1
  125. package/dist/lib/forge-contracts.js.map +1 -1
  126. package/dist/lib/managed-block.d.ts +8 -2
  127. package/dist/lib/managed-block.d.ts.map +1 -1
  128. package/dist/lib/managed-block.js +32 -7
  129. package/dist/lib/managed-block.js.map +1 -1
  130. package/package.json +3 -3
  131. package/scripts/forge/gitea/issue-view.sh +4 -1
  132. package/scripts/forge/github/issue-view.sh +2 -2
  133. package/scripts/forge/gitlab/issue-view.sh +4 -1
  134. package/scripts/forge/linear/issue-view.sh +3 -2
  135. package/skeleton/resources/commands/consult.md +14 -0
  136. package/skeleton/roles/builder.md +21 -0
  137. package/skeleton/templates/AGENTS.md +16 -0
  138. package/skeleton/templates/CLAUDE.md +16 -0
  139. package/dashboard-dist/assets/index-DM7OU-EI.js.map +0 -1
  140. package/dist/agent-farm/db/migrate.d.ts +0 -20
  141. package/dist/agent-farm/db/migrate.d.ts.map +0 -1
  142. package/dist/agent-farm/db/migrate.js +0 -107
  143. package/dist/agent-farm/db/migrate.js.map +0 -1
  144. package/dist/agent-farm/servers/worktree-config-watcher.d.ts +0 -36
  145. package/dist/agent-farm/servers/worktree-config-watcher.d.ts.map +0 -1
  146. package/dist/agent-farm/servers/worktree-config-watcher.js.map +0 -1
@@ -5,15 +5,14 @@
5
5
  * Uses better-sqlite3 for synchronous operations with proper concurrency handling.
6
6
  */
7
7
  import Database from 'better-sqlite3';
8
- import { existsSync, mkdirSync, copyFileSync, unlinkSync, readdirSync, renameSync } from 'node:fs';
8
+ import { existsSync, mkdirSync, readdirSync, renameSync } from 'node:fs';
9
9
  import { homedir } from 'node:os';
10
10
  import { resolve, dirname, join } from 'node:path';
11
11
  import { AGENT_FARM_DIR } from '../lib/tower-client.js';
12
- import { LOCAL_SCHEMA, GLOBAL_SCHEMA } from './schema.js';
13
- import { migrateLocalFromJson } from './migrate.js';
14
- import { getConfig } from '../utils/index.js';
15
- // Singleton instances
16
- let _localDb = null;
12
+ import { GLOBAL_SCHEMA } from './schema.js';
13
+ // Singleton instance. Issue #1118: there is now a single user-global database
14
+ // (~/.agent-farm/global.db). getDb() and getGlobalDb() both return it; the
15
+ // retired per-workspace state.db is no longer opened.
17
16
  let _globalDb = null;
18
17
  /**
19
18
  * Ensure a directory exists
@@ -40,14 +39,17 @@ function configurePragmas(db) {
40
39
  db.pragma('foreign_keys = ON');
41
40
  }
42
41
  /**
43
- * Get the local database instance (state.db)
44
- * Creates and initializes the database if it doesn't exist
42
+ * Get the database instance.
43
+ *
44
+ * Issue #1118: state.db is retired. getDb() now returns the single user-global
45
+ * global.db connection — the same instance as getGlobalDb(). Per-workspace rows
46
+ * (architect, builders) are disambiguated by their `workspace_path` column
47
+ * within the shared file, so the connection no longer depends on Tower's
48
+ * start-cwd. Kept as a distinct export so the many existing callsites that read
49
+ * dashboard state (architect/builders/utils/annotations) don't churn.
45
50
  */
46
51
  export function getDb() {
47
- if (!_localDb) {
48
- _localDb = ensureLocalDatabase();
49
- }
50
- return _localDb;
52
+ return getGlobalDb();
51
53
  }
52
54
  /**
53
55
  * Get the global database instance (global.db)
@@ -60,13 +62,12 @@ export function getGlobalDb() {
60
62
  return _globalDb;
61
63
  }
62
64
  /**
63
- * Close the local database connection
65
+ * Close the database connection.
66
+ * Issue #1118: getDb() aliases the global connection, so this closes the shared
67
+ * global.db. Kept for callsites that historically closed "the local db".
64
68
  */
65
69
  export function closeDb() {
66
- if (_localDb) {
67
- _localDb.close();
68
- _localDb = null;
69
- }
70
+ closeGlobalDb();
70
71
  }
71
72
  /**
72
73
  * Close the global database connection
@@ -85,11 +86,11 @@ export function closeAllDbs() {
85
86
  closeGlobalDb();
86
87
  }
87
88
  /**
88
- * Get the path to the local database
89
+ * Get the path to the database.
90
+ * Issue #1118: the local db path is now the global db path.
89
91
  */
90
92
  export function getDbPath() {
91
- const config = getConfig();
92
- return resolve(config.stateDir, 'state.db');
93
+ return getGlobalDbPath();
93
94
  }
94
95
  /**
95
96
  * Get the path to the global database.
@@ -105,455 +106,6 @@ export function getGlobalDbPath() {
105
106
  }
106
107
  return resolve(AGENT_FARM_DIR, dbName);
107
108
  }
108
- /**
109
- * Initialize the local database (state.db)
110
- */
111
- function ensureLocalDatabase() {
112
- const config = getConfig();
113
- const dbPath = resolve(config.stateDir, 'state.db');
114
- const jsonPath = resolve(config.stateDir, 'state.json');
115
- // Ensure directory exists
116
- ensureDir(config.stateDir);
117
- // Create/open database
118
- const db = new Database(dbPath);
119
- configurePragmas(db);
120
- // Run schema (creates tables if they don't exist)
121
- db.exec(LOCAL_SCHEMA);
122
- // Check if migration is needed
123
- const migrated = db.prepare('SELECT version FROM _migrations WHERE version = 1').get();
124
- if (!migrated && existsSync(jsonPath)) {
125
- // Migrate from JSON.
126
- // Bugfix #826: pass workspaceRoot so the architect row gets tagged
127
- // with workspace_path (the new composite-PK column).
128
- migrateLocalFromJson(db, jsonPath, config.workspaceRoot);
129
- // Record migration
130
- db.prepare('INSERT INTO _migrations (version) VALUES (1)').run();
131
- // Backup original JSON and remove it
132
- copyFileSync(jsonPath, jsonPath + '.bak');
133
- unlinkSync(jsonPath);
134
- console.log('[info] Migrated state.json to state.db (backup at state.json.bak)');
135
- }
136
- else if (!migrated) {
137
- // Fresh install, just mark migration as done
138
- db.prepare('INSERT OR IGNORE INTO _migrations (version) VALUES (1)').run();
139
- console.log('[info] Created new state.db at', dbPath);
140
- }
141
- // Migration v2: Add terminal_id columns (node-pty rewrite)
142
- const v2 = db.prepare('SELECT version FROM _migrations WHERE version = 2').get();
143
- if (!v2) {
144
- // Add terminal_id to tables that may already exist without it
145
- const tables = ['architect', 'builders', 'utils'];
146
- for (const table of tables) {
147
- try {
148
- db.exec(`ALTER TABLE ${table} ADD COLUMN terminal_id TEXT`);
149
- }
150
- catch {
151
- // Column already exists (fresh install ran full schema)
152
- }
153
- }
154
- db.prepare('INSERT INTO _migrations (version) VALUES (2)').run();
155
- }
156
- // Migration v3: Remove UNIQUE constraint from utils.port (node-pty shells use port=0)
157
- const v3 = db.prepare('SELECT version FROM _migrations WHERE version = 3').get();
158
- if (!v3) {
159
- // Check if utils table has the UNIQUE constraint on port
160
- const tableInfo = db
161
- .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='utils'")
162
- .get();
163
- if (tableInfo?.sql?.includes('port INTEGER NOT NULL UNIQUE')) {
164
- // SQLite can't drop constraints, so recreate table
165
- db.exec(`
166
- CREATE TABLE utils_new (
167
- id TEXT PRIMARY KEY,
168
- name TEXT NOT NULL,
169
- port INTEGER NOT NULL DEFAULT 0,
170
- pid INTEGER NOT NULL DEFAULT 0,
171
- tmux_session TEXT,
172
- terminal_id TEXT,
173
- started_at TEXT NOT NULL DEFAULT (datetime('now'))
174
- );
175
- INSERT INTO utils_new SELECT id, name, port, pid, tmux_session, terminal_id, started_at FROM utils;
176
- DROP TABLE utils;
177
- ALTER TABLE utils_new RENAME TO utils;
178
- `);
179
- console.log('[info] Migrated utils table: removed UNIQUE constraint from port');
180
- }
181
- db.prepare('INSERT INTO _migrations (version) VALUES (3)').run();
182
- }
183
- // Migration v4: Remove UNIQUE constraint from builders.port (PTY-backed builders use port=0)
184
- const v4 = db.prepare('SELECT version FROM _migrations WHERE version = 4').get();
185
- if (!v4) {
186
- const tableInfo = db
187
- .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='builders'")
188
- .get();
189
- if (tableInfo?.sql?.includes('port INTEGER NOT NULL UNIQUE')) {
190
- // SQLite can't drop constraints, so recreate table
191
- db.exec(`
192
- CREATE TABLE builders_new (
193
- id TEXT PRIMARY KEY,
194
- name TEXT NOT NULL,
195
- port INTEGER NOT NULL DEFAULT 0,
196
- pid INTEGER NOT NULL DEFAULT 0,
197
- status TEXT NOT NULL DEFAULT 'spawning'
198
- CHECK(status IN ('spawning', 'implementing', 'blocked', 'pr', 'complete')),
199
- phase TEXT NOT NULL DEFAULT '',
200
- worktree TEXT NOT NULL,
201
- branch TEXT NOT NULL,
202
- tmux_session TEXT,
203
- type TEXT NOT NULL DEFAULT 'spec'
204
- CHECK(type IN ('spec', 'task', 'protocol', 'shell', 'worktree', 'bugfix')),
205
- task_text TEXT,
206
- protocol_name TEXT,
207
- issue_number INTEGER,
208
- terminal_id TEXT,
209
- started_at TEXT NOT NULL DEFAULT (datetime('now')),
210
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
211
- );
212
- INSERT INTO builders_new SELECT * FROM builders;
213
- DROP TABLE builders;
214
- ALTER TABLE builders_new RENAME TO builders;
215
- CREATE INDEX IF NOT EXISTS idx_builders_status ON builders(status);
216
- CREATE INDEX IF NOT EXISTS idx_builders_port ON builders(port);
217
- CREATE TRIGGER IF NOT EXISTS builders_updated_at
218
- AFTER UPDATE ON builders
219
- FOR EACH ROW
220
- BEGIN
221
- UPDATE builders SET updated_at = datetime('now') WHERE id = NEW.id;
222
- END;
223
- `);
224
- console.log('[info] Migrated builders table: removed UNIQUE constraint from port');
225
- }
226
- db.prepare('INSERT INTO _migrations (version) VALUES (4)').run();
227
- }
228
- // Migration v5: Remove UNIQUE constraint from annotations.port (all annotations use port=0)
229
- const v5 = db.prepare('SELECT version FROM _migrations WHERE version = 5').get();
230
- if (!v5) {
231
- const tableInfo = db
232
- .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='annotations'")
233
- .get();
234
- if (tableInfo?.sql?.includes('port INTEGER NOT NULL UNIQUE')) {
235
- db.exec(`
236
- CREATE TABLE annotations_new (
237
- id TEXT PRIMARY KEY,
238
- file TEXT NOT NULL,
239
- port INTEGER NOT NULL DEFAULT 0,
240
- pid INTEGER NOT NULL DEFAULT 0,
241
- parent_type TEXT NOT NULL CHECK(parent_type IN ('architect', 'builder', 'util')),
242
- parent_id TEXT,
243
- started_at TEXT NOT NULL DEFAULT (datetime('now'))
244
- );
245
- INSERT INTO annotations_new SELECT id, file, port, pid, parent_type, parent_id, started_at FROM annotations;
246
- DROP TABLE annotations;
247
- ALTER TABLE annotations_new RENAME TO annotations;
248
- `);
249
- console.log('[info] Migrated annotations table: removed UNIQUE constraint from port');
250
- }
251
- db.prepare('INSERT INTO _migrations (version) VALUES (5)').run();
252
- }
253
- // Migration v6: Drop tmux_session columns (Spec 0104 - tmux replaced by shepherd)
254
- const v6 = db.prepare('SELECT version FROM _migrations WHERE version = 6').get();
255
- if (!v6) {
256
- const tables = ['architect', 'builders', 'utils'];
257
- for (const table of tables) {
258
- try {
259
- db.exec(`ALTER TABLE ${table} DROP COLUMN tmux_session`);
260
- }
261
- catch {
262
- // Column may not exist (fresh install with updated schema)
263
- }
264
- }
265
- db.prepare('INSERT INTO _migrations (version) VALUES (6)').run();
266
- }
267
- // Migration v7: Rename builder status 'pr-ready' → 'pr' (Bugfix #368)
268
- const v7 = db.prepare('SELECT version FROM _migrations WHERE version = 7').get();
269
- if (!v7) {
270
- const tableInfo = db
271
- .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='builders'")
272
- .get();
273
- if (tableInfo?.sql?.includes('pr-ready')) {
274
- // SQLite can't alter CHECK constraints, so recreate table
275
- db.exec(`
276
- CREATE TABLE builders_new (
277
- id TEXT PRIMARY KEY,
278
- name TEXT NOT NULL,
279
- port INTEGER NOT NULL DEFAULT 0,
280
- pid INTEGER NOT NULL DEFAULT 0,
281
- status TEXT NOT NULL DEFAULT 'spawning'
282
- CHECK(status IN ('spawning', 'implementing', 'blocked', 'pr', 'complete')),
283
- phase TEXT NOT NULL DEFAULT '',
284
- worktree TEXT NOT NULL,
285
- branch TEXT NOT NULL,
286
- type TEXT NOT NULL DEFAULT 'spec'
287
- CHECK(type IN ('spec', 'task', 'protocol', 'shell', 'worktree', 'bugfix')),
288
- task_text TEXT,
289
- protocol_name TEXT,
290
- issue_number INTEGER,
291
- terminal_id TEXT,
292
- started_at TEXT NOT NULL DEFAULT (datetime('now')),
293
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
294
- );
295
- INSERT INTO builders_new
296
- SELECT id, name, port, pid,
297
- CASE WHEN status = 'pr-ready' THEN 'pr' ELSE status END,
298
- phase, worktree, branch, type, task_text, protocol_name,
299
- issue_number, terminal_id, started_at, updated_at
300
- FROM builders;
301
- DROP TABLE builders;
302
- ALTER TABLE builders_new RENAME TO builders;
303
- CREATE INDEX IF NOT EXISTS idx_builders_status ON builders(status);
304
- CREATE INDEX IF NOT EXISTS idx_builders_port ON builders(port);
305
- CREATE TRIGGER IF NOT EXISTS builders_updated_at
306
- AFTER UPDATE ON builders
307
- FOR EACH ROW
308
- BEGIN
309
- UPDATE builders SET updated_at = datetime('now') WHERE id = NEW.id;
310
- END;
311
- `);
312
- console.log('[info] Migrated builders table: renamed status pr-ready to pr');
313
- }
314
- db.prepare('INSERT INTO _migrations (version) VALUES (7)').run();
315
- }
316
- // Migration v8: Widen issue_number from INTEGER to TEXT (Linear identifiers like "ENG-123")
317
- const v8 = db.prepare('SELECT version FROM _migrations WHERE version = 8').get();
318
- if (!v8) {
319
- const tableInfo = db
320
- .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='builders'")
321
- .get();
322
- if (tableInfo?.sql?.includes('issue_number INTEGER')) {
323
- db.exec(`
324
- CREATE TABLE builders_new (
325
- id TEXT PRIMARY KEY,
326
- name TEXT NOT NULL,
327
- port INTEGER NOT NULL DEFAULT 0,
328
- pid INTEGER NOT NULL DEFAULT 0,
329
- status TEXT NOT NULL DEFAULT 'spawning'
330
- CHECK(status IN ('spawning', 'implementing', 'blocked', 'pr', 'complete')),
331
- phase TEXT NOT NULL DEFAULT '',
332
- worktree TEXT NOT NULL,
333
- branch TEXT NOT NULL,
334
- type TEXT NOT NULL DEFAULT 'spec'
335
- CHECK(type IN ('spec', 'task', 'protocol', 'shell', 'worktree', 'bugfix')),
336
- task_text TEXT,
337
- protocol_name TEXT,
338
- issue_number TEXT,
339
- terminal_id TEXT,
340
- started_at TEXT NOT NULL DEFAULT (datetime('now')),
341
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
342
- );
343
- INSERT INTO builders_new
344
- SELECT id, name, port, pid, status, phase, worktree, branch, type,
345
- task_text, protocol_name, CAST(issue_number AS TEXT),
346
- terminal_id, started_at, updated_at
347
- FROM builders;
348
- DROP TABLE builders;
349
- ALTER TABLE builders_new RENAME TO builders;
350
- CREATE INDEX IF NOT EXISTS idx_builders_status ON builders(status);
351
- CREATE INDEX IF NOT EXISTS idx_builders_port ON builders(port);
352
- CREATE TRIGGER IF NOT EXISTS builders_updated_at
353
- AFTER UPDATE ON builders
354
- FOR EACH ROW
355
- BEGIN
356
- UPDATE builders SET updated_at = datetime('now') WHERE id = NEW.id;
357
- END;
358
- `);
359
- console.log('[info] Migrated builders table: widened issue_number to TEXT');
360
- }
361
- db.prepare('INSERT INTO _migrations (version) VALUES (8)').run();
362
- }
363
- // Migration v9: Multi-architect support (Spec 755)
364
- // - Rebuild architect table: drop CHECK(id=1), change id to TEXT PRIMARY KEY.
365
- // Rekey the existing singleton row's id from 1 to 'main'.
366
- // - Add builders.spawned_by_architect TEXT column (nullable).
367
- const v9 = db.prepare('SELECT version FROM _migrations WHERE version = 9').get();
368
- if (!v9) {
369
- const tableInfo = db
370
- .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='architect'")
371
- .get();
372
- // Architect table rebuild — only if it still has the old integer/CHECK shape.
373
- // Detect via 'CHECK (id = 1)' (or normalized variants) in the stored DDL.
374
- if (tableInfo?.sql && /CHECK\s*\(\s*id\s*=\s*1\s*\)/i.test(tableInfo.sql)) {
375
- db.exec(`
376
- CREATE TABLE architect_v9 (
377
- id TEXT PRIMARY KEY,
378
- pid INTEGER NOT NULL,
379
- port INTEGER NOT NULL,
380
- cmd TEXT NOT NULL,
381
- started_at TEXT NOT NULL DEFAULT (datetime('now')),
382
- terminal_id TEXT
383
- );
384
- INSERT INTO architect_v9 (id, pid, port, cmd, started_at, terminal_id)
385
- SELECT 'main', pid, port, cmd, started_at, terminal_id FROM architect;
386
- DROP TABLE architect;
387
- ALTER TABLE architect_v9 RENAME TO architect;
388
- `);
389
- console.log('[info] Migrated architect table: multi-architect support (Spec 755)');
390
- }
391
- // Add spawned_by_architect column to builders if absent.
392
- try {
393
- db.exec(`ALTER TABLE builders ADD COLUMN spawned_by_architect TEXT`);
394
- }
395
- catch {
396
- // Column already exists (fresh install ran the updated schema).
397
- }
398
- db.prepare('INSERT INTO _migrations (version) VALUES (9)').run();
399
- }
400
- // Migration v10: Add 'pir' to builders.type CHECK constraint (Issue 691).
401
- // Reintroduced post main-merge (the original collided with main's v9).
402
- // Drift-robust: derive builders_new from the LIVE builders DDL (only the
403
- // table name + the type CHECK literal are rewritten), so `SELECT *` can
404
- // never column-mismatch regardless of columns earlier migrations added
405
- // (e.g. v9's spawned_by_architect). Idempotent: skips if 'pir' is already
406
- // in the constraint (covers fresh installs and DBs that ran the old v9).
407
- const v10 = db.prepare('SELECT version FROM _migrations WHERE version = 10').get();
408
- if (!v10) {
409
- const builders = db
410
- .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='builders'")
411
- .get();
412
- if (builders?.sql && !builders.sql.includes("'pir'")) {
413
- const newSql = builders.sql
414
- .replace(/^CREATE TABLE\s+(?:IF NOT EXISTS\s+)?["'`]?builders["'`]?/i, 'CREATE TABLE builders_new')
415
- .replace(/(CHECK\s*\(\s*type\s+IN\s*\([^)]*)\)/i, "$1, 'pir')");
416
- db.exec(`
417
- ${newSql};
418
- INSERT INTO builders_new SELECT * FROM builders;
419
- DROP TABLE builders;
420
- ALTER TABLE builders_new RENAME TO builders;
421
- CREATE INDEX IF NOT EXISTS idx_builders_status ON builders(status);
422
- CREATE INDEX IF NOT EXISTS idx_builders_port ON builders(port);
423
- CREATE TRIGGER IF NOT EXISTS builders_updated_at
424
- AFTER UPDATE ON builders
425
- FOR EACH ROW
426
- BEGIN
427
- UPDATE builders SET updated_at = datetime('now') WHERE id = NEW.id;
428
- END;
429
- `);
430
- console.log("[info] Migrated builders table: added 'pir' to type CHECK constraint (v10)");
431
- }
432
- db.prepare('INSERT INTO _migrations (version) VALUES (10)').run();
433
- }
434
- // Migration v11: Workspace-scoped architect rows (Bugfix #826).
435
- //
436
- // The architect table was global per Tower process, anchored to Tower's
437
- // startup CWD. With multi-architect (Spec 755 / 786), this meant siblings
438
- // registered in workspace A were re-spawned into workspace B at the next
439
- // launchInstance(B) — a real PTY leak. Fix by construction: add
440
- // `workspace_path` as part of the primary key so rows are explicitly scoped.
441
- //
442
- // Backfill source: global.db.terminal_sessions has per-architect rows with
443
- // workspace_path (its `role_id` column holds the architect name). We ATTACH
444
- // global.db and copy that mapping into the new architect rows. Architects
445
- // with no current terminal_session row are orphans (their PTY died and was
446
- // cleaned up before this migration ran) — they are dropped.
447
- //
448
- // Idempotent: skips when workspace_path column already exists (handles fresh
449
- // installs that ran the updated schema, and re-runs after partial failure).
450
- const v11 = db.prepare('SELECT version FROM _migrations WHERE version = 11').get();
451
- if (!v11) {
452
- // Check if workspace_path column already exists (fresh install via LOCAL_SCHEMA).
453
- const cols = db.prepare('PRAGMA table_info(architect)').all();
454
- const alreadyMigrated = cols.some(c => c.name === 'workspace_path');
455
- if (!alreadyMigrated) {
456
- const globalDbPath = getGlobalDbPath();
457
- // Step 1: rebuild table with composite PK (workspace_path, id).
458
- // - Create _v11 table with the new shape.
459
- // - Backfill workspace_path from global.db.terminal_sessions via ATTACH.
460
- // Use LIMIT 1 to handle the (rare) case where multiple rows match.
461
- // - Drop orphans (workspace_path IS NULL).
462
- // - DETACH global.db before swapping table names.
463
- // - Atomic swap: DROP architect, RENAME _v11 → architect.
464
- db.exec(`
465
- CREATE TABLE architect_v11 (
466
- workspace_path TEXT NOT NULL,
467
- id TEXT NOT NULL,
468
- pid INTEGER NOT NULL,
469
- port INTEGER NOT NULL,
470
- cmd TEXT NOT NULL,
471
- started_at TEXT NOT NULL DEFAULT (datetime('now')),
472
- terminal_id TEXT,
473
- PRIMARY KEY (workspace_path, id)
474
- );
475
- `);
476
- try {
477
- // ATTACH global.db so we can read terminal_sessions for the backfill.
478
- db.prepare("ATTACH DATABASE ? AS globaldb").run(globalDbPath);
479
- try {
480
- // Backfill: for each architect row, look up its workspace_path in
481
- // global.db.terminal_sessions.
482
- //
483
- // Disambiguation (Bugfix #826 iter-5): for users already hit by the
484
- // leak, the same architect NAME can appear in MULTIPLE workspaces'
485
- // terminal_sessions rows. Matching by `role_id` alone with `LIMIT 1`
486
- // would pick non-deterministically and could migrate ob-refine to
487
- // the wrong workspace. Use `terminal_id` as the primary disambiguator
488
- // — it's the stable session UUID and uniquely identifies one
489
- // terminal_session row. Fall back to `role_id` only when
490
- // `terminal_id` doesn't match (legacy rows where terminal_id is NULL
491
- // or its target row has been cleaned up).
492
- db.exec(`
493
- INSERT INTO architect_v11 (workspace_path, id, pid, port, cmd, started_at, terminal_id)
494
- SELECT
495
- COALESCE(
496
- (SELECT ts.workspace_path
497
- FROM globaldb.terminal_sessions ts
498
- WHERE ts.id = a.terminal_id
499
- AND ts.type = 'architect'),
500
- (SELECT ts.workspace_path
501
- FROM globaldb.terminal_sessions ts
502
- WHERE ts.role_id = a.id
503
- AND ts.type = 'architect'
504
- LIMIT 1)
505
- ) AS workspace_path,
506
- a.id,
507
- a.pid,
508
- a.port,
509
- a.cmd,
510
- a.started_at,
511
- a.terminal_id
512
- FROM architect a
513
- WHERE EXISTS (
514
- SELECT 1 FROM globaldb.terminal_sessions ts
515
- WHERE (ts.id = a.terminal_id OR ts.role_id = a.id)
516
- AND ts.type = 'architect'
517
- );
518
- `);
519
- }
520
- finally {
521
- db.prepare('DETACH DATABASE globaldb').run();
522
- }
523
- }
524
- catch (err) {
525
- // If global.db can't be opened (e.g. doesn't exist yet in a fresh
526
- // install where Tower hasn't started), the architect table must be
527
- // empty for the migration to succeed. Verify and proceed; if there
528
- // are pre-existing architect rows we genuinely can't backfill, drop
529
- // them — they correspond to a never-started workspace.
530
- const remaining = db.prepare('SELECT COUNT(*) AS n FROM architect').get();
531
- if (remaining.n > 0) {
532
- console.log(`[warn] Migration v11: dropping ${remaining.n} architect row(s) — global.db unavailable for backfill: ${err.message}`);
533
- }
534
- // architect_v11 is empty in this path; carry on.
535
- }
536
- // Step 2: atomic swap.
537
- db.exec(`
538
- DROP TABLE architect;
539
- ALTER TABLE architect_v11 RENAME TO architect;
540
- `);
541
- console.log('[info] Migrated architect table: workspace-scoped rows (Bugfix #826)');
542
- }
543
- // Bugfix #826 iter-7: create the workspace_path index here, OUTSIDE the
544
- // alreadyMigrated guard. The index is NOT in LOCAL_SCHEMA because
545
- // db.exec(LOCAL_SCHEMA) runs before migrations on every open — and on a
546
- // pre-v11 install the architect table lacks workspace_path, so CREATE
547
- // INDEX would throw 'no such column' and abort ensureLocalDatabase before
548
- // v11 can run. Placing it here ensures the index is created on both
549
- // upgrade installs (after the migration body) and fresh installs (where
550
- // LOCAL_SCHEMA already created the v11 table shape and the inner block
551
- // was skipped). `IF NOT EXISTS` makes the second-open case a no-op.
552
- db.exec('CREATE INDEX IF NOT EXISTS idx_architect_workspace ON architect(workspace_path);');
553
- db.prepare('INSERT INTO _migrations (version) VALUES (11)').run();
554
- }
555
- return db;
556
- }
557
109
  /**
558
110
  * Initialize the global database (global.db)
559
111
  */
@@ -566,7 +118,7 @@ function ensureGlobalDatabase() {
566
118
  const db = new Database(dbPath);
567
119
  configurePragmas(db);
568
120
  // Current migration version — bump when adding new migrations
569
- const GLOBAL_CURRENT_VERSION = 13;
121
+ const GLOBAL_CURRENT_VERSION = 14;
570
122
  // Detect fresh vs existing database by checking if content tables exist.
571
123
  // On existing databases, GLOBAL_SCHEMA must NOT run because it references column names
572
124
  // (workspace_path) that don't exist until migration v9 renames them from project_path.
@@ -866,6 +418,84 @@ function ensureGlobalDatabase() {
866
418
  db.prepare('INSERT INTO _migrations (version) VALUES (13)').run();
867
419
  console.log('[info] Backfilled architect role_id with \'main\' (Spec 755)');
868
420
  }
421
+ // Migration v14: Absorb the retired state.db tables (Issue #1118).
422
+ // Creates architect/builders/utils/annotations in global.db at their final
423
+ // shape. architect/utils/annotations move as-is; builders is RESHAPED with a
424
+ // workspace_path column + composite PK (workspace_path, id) so the same
425
+ // builder id can exist in multiple workspaces. Idempotent via
426
+ // `CREATE TABLE IF NOT EXISTS`. The one-time data migration of legacy
427
+ // state.db files is a separate, marker-gated step run at Tower boot
428
+ // (db/consolidate.ts) — NOT here — so opening global.db never moves data.
429
+ const v14 = db.prepare('SELECT version FROM _migrations WHERE version = 14').get();
430
+ if (!v14) {
431
+ db.exec(`
432
+ CREATE TABLE IF NOT EXISTS architect (
433
+ workspace_path TEXT NOT NULL,
434
+ id TEXT NOT NULL,
435
+ pid INTEGER NOT NULL,
436
+ port INTEGER NOT NULL,
437
+ cmd TEXT NOT NULL,
438
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
439
+ terminal_id TEXT,
440
+ session_id TEXT,
441
+ PRIMARY KEY (workspace_path, id)
442
+ );
443
+ CREATE INDEX IF NOT EXISTS idx_architect_workspace ON architect(workspace_path);
444
+
445
+ CREATE TABLE IF NOT EXISTS builders (
446
+ workspace_path TEXT NOT NULL,
447
+ id TEXT NOT NULL,
448
+ name TEXT NOT NULL,
449
+ port INTEGER NOT NULL DEFAULT 0,
450
+ pid INTEGER NOT NULL DEFAULT 0,
451
+ status TEXT NOT NULL DEFAULT 'spawning'
452
+ CHECK(status IN ('spawning', 'implementing', 'blocked', 'pr', 'complete')),
453
+ phase TEXT NOT NULL DEFAULT '',
454
+ worktree TEXT NOT NULL,
455
+ branch TEXT NOT NULL,
456
+ type TEXT NOT NULL DEFAULT 'spec'
457
+ CHECK(type IN ('spec', 'task', 'protocol', 'shell', 'worktree', 'bugfix', 'pir')),
458
+ task_text TEXT,
459
+ protocol_name TEXT,
460
+ issue_number TEXT,
461
+ terminal_id TEXT,
462
+ spawned_by_architect TEXT,
463
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
464
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
465
+ PRIMARY KEY (workspace_path, id)
466
+ );
467
+ CREATE INDEX IF NOT EXISTS idx_builders_status ON builders(status);
468
+ CREATE INDEX IF NOT EXISTS idx_builders_port ON builders(port);
469
+ CREATE TRIGGER IF NOT EXISTS builders_updated_at
470
+ AFTER UPDATE ON builders
471
+ FOR EACH ROW
472
+ BEGIN
473
+ UPDATE builders SET updated_at = datetime('now')
474
+ WHERE workspace_path = NEW.workspace_path AND id = NEW.id;
475
+ END;
476
+
477
+ CREATE TABLE IF NOT EXISTS utils (
478
+ id TEXT PRIMARY KEY,
479
+ name TEXT NOT NULL,
480
+ port INTEGER NOT NULL DEFAULT 0,
481
+ pid INTEGER NOT NULL DEFAULT 0,
482
+ terminal_id TEXT,
483
+ started_at TEXT NOT NULL DEFAULT (datetime('now'))
484
+ );
485
+
486
+ CREATE TABLE IF NOT EXISTS annotations (
487
+ id TEXT PRIMARY KEY,
488
+ file TEXT NOT NULL,
489
+ port INTEGER NOT NULL DEFAULT 0,
490
+ pid INTEGER NOT NULL DEFAULT 0,
491
+ parent_type TEXT NOT NULL CHECK(parent_type IN ('architect', 'builder', 'util')),
492
+ parent_id TEXT,
493
+ started_at TEXT NOT NULL DEFAULT (datetime('now'))
494
+ );
495
+ `);
496
+ db.prepare('INSERT INTO _migrations (version) VALUES (14)').run();
497
+ console.log('[info] Absorbed state.db tables into global.db (Issue #1118)');
498
+ }
869
499
  return db;
870
500
  }
871
501
  // Re-export types and utilities