@straiffi/archon 1.7.2 → 1.7.4

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 (176) hide show
  1. package/dist/client/assets/ActivityView-BgMz3NNT.js +1 -0
  2. package/dist/client/assets/GitHubActionsDialog-CNqWDS3E.js +1 -0
  3. package/dist/client/assets/TestsDialog-Cll7xJe0.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-Do9nbC8v.js → architectureDiagram-3BPJPVTR-BL5dYiul.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-DGGOLHF8.js → blockDiagram-GPEHLZMM-Bi5x2v80.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-Bz7tQybs.js → c4Diagram-AAUBKEIU-esEm54Ay.js} +1 -1
  7. package/dist/client/assets/channel-CqNHF3FD.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-CJO-GmWJ.js → chunk-2J33WTMH-DACfY8xM.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-DPAVagpC.js → chunk-3OPIFGDE-DZNUiaUs.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-Cof75Mu7.js → chunk-5ZQYHXKU-ClDfkMAp.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-COuPqQ8_.js → chunk-727SXJPM-CCwA3GWT.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-CFj5KabW.js → chunk-AQP2D5EJ-Byl4DRZ9.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-C4M6w5cT.js → chunk-CSCIHK7Q-CXymZQ7A.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-ByaiwVue.js → chunk-KSCS5N6A-VV3D8g6a.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-Cx5jeot9.js → chunk-L5ZTLDWV-BL9N5GRI.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-DmGtztud.js → chunk-LZXEDZCA-DKNN-zno.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-i3yuspqL.js → chunk-ND2GUHAM-CHCpQ1yr.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-CQ4E_jP4.js → chunk-NZK2D7GU-B0Dm74mt.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-D2g4VDCd.js → chunk-O5CBEL6O-DAa5z2xG.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-exIu-zQ2.js → chunk-WU5MYG2G-DrVJWKVR.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-BjEgQwKg.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-gGomH-F_.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-DvF75yLZ.js → dagre-BM42HDAG-B0fsl8mE.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-DFv0HxLL.js → diagram-2AECGRRQ-Cx_VEbSS.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-C_LRir0-.js → diagram-5GNKFQAL-Cv7VVn5r.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-DGZxRDRy.js → diagram-KO2AKTUF-C8Sl5czV.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-UXt0uti4.js → diagram-LMA3HP47-C3dZaLz2.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-CFbZS4n5.js → diagram-OG6HWLK6-BAU9DtTr.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-CfAwZDdf.js → erDiagram-TEJ5UH35-DpNc-eBD.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-Cdgi5z3n.js → flowDiagram-I6XJVG4X-DGwTAT1S.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DOrkFkhu.js → ganttDiagram-6RSMTGT7-MPZhEgzJ.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-cEox0peD.js → gitGraphDiagram-PVQCEYII-CY0tHL5N.js} +1 -1
  33. package/dist/client/assets/index-CJRgmWwY.js +273 -0
  34. package/dist/client/assets/index-mRkJWu70.css +2 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-gm51u2Nn.js → infoDiagram-5YYISTIA-BBZeKjf4.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CjnoOIja.js → ishikawaDiagram-YF4QCWOH-CM_iFrES.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-Dlg4DsiO.js → journeyDiagram-JHISSGLW-Do_lY94X.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-BU0XZaSd.js → kanban-definition-UN3LZRKU-brxg3j37.js} +1 -1
  39. package/dist/client/assets/{line-Bert22-_.js → line-PHHbm9rj.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-BvQSGDmL.js → mermaid-parser.core-CLxAkW69.js} +1 -1
  41. package/dist/client/assets/{mermaid.core--qSAHJRj.js → mermaid.core-CebvSmx9.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BgFIvWWD.js → mindmap-definition-RKZ34NQL-CX_-ZaJF.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-DGcbXD0M.js → pieDiagram-4H26LBE5-BJuHvql_.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-ClkBUeia.js → quadrantDiagram-W4KKPZXB-DiJll7l9.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-B8CwGToU.js → requirementDiagram-4Y6WPE33-Bg-tXgcj.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DTk3jL6B.js → sankeyDiagram-5OEKKPKP-BoK2egfZ.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQVrcnKf.js → sequenceDiagram-3UESZ5HK-DcrDWNzv.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-DD4yMZPB.js → stateDiagram-AJRCARHV-B995qt1S.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Dn8P7Sa1.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-BnCBVqcd.js → timeline-definition-PNZ67QCA-DqSVAhqL.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-BDmLMNXS.js → vennDiagram-CIIHVFJN-D9wVraVY.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DSLuQiEv.js → wardleyDiagram-YWT4CUSO-CLff9zVd.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DAq_LA4r.js → xychartDiagram-2RQKCTM6-BuHUMMH2.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +472 -39
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +1311 -212
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agent.js +228 -71
  60. package/dist/server/lib/agent.js.map +1 -1
  61. package/dist/server/lib/agentRuns.js +205 -38
  62. package/dist/server/lib/agentRuns.js.map +1 -1
  63. package/dist/server/lib/attachments.js +24 -0
  64. package/dist/server/lib/attachments.js.map +1 -1
  65. package/dist/server/lib/automationProgression.js +464 -0
  66. package/dist/server/lib/automationProgression.js.map +1 -0
  67. package/dist/server/lib/automationReconciliation.js +498 -0
  68. package/dist/server/lib/automationReconciliation.js.map +1 -0
  69. package/dist/server/lib/automationStageDetails.js +232 -0
  70. package/dist/server/lib/automationStageDetails.js.map +1 -0
  71. package/dist/server/lib/buildOpenQuestionOperations.js +238 -0
  72. package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -0
  73. package/dist/server/lib/buildOpenQuestions.js +44 -0
  74. package/dist/server/lib/buildOpenQuestions.js.map +1 -1
  75. package/dist/server/lib/buildSequences.js +130 -46
  76. package/dist/server/lib/buildSequences.js.map +1 -1
  77. package/dist/server/lib/bundleReviewOperations.js +91 -9
  78. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  79. package/dist/server/lib/chatOperations.js +67 -12
  80. package/dist/server/lib/chatOperations.js.map +1 -1
  81. package/dist/server/lib/executionReconciliation.js +518 -0
  82. package/dist/server/lib/executionReconciliation.js.map +1 -0
  83. package/dist/server/lib/executionRetry.js +694 -0
  84. package/dist/server/lib/executionRetry.js.map +1 -0
  85. package/dist/server/lib/executionRetryContext.js +489 -0
  86. package/dist/server/lib/executionRetryContext.js.map +1 -0
  87. package/dist/server/lib/executionRetryPredicates.js +74 -0
  88. package/dist/server/lib/executionRetryPredicates.js.map +1 -0
  89. package/dist/server/lib/executionRunRetention.js +141 -0
  90. package/dist/server/lib/executionRunRetention.js.map +1 -0
  91. package/dist/server/lib/executionRuns.js +1367 -0
  92. package/dist/server/lib/executionRuns.js.map +1 -0
  93. package/dist/server/lib/executionTimeline.js +45 -0
  94. package/dist/server/lib/executionTimeline.js.map +1 -0
  95. package/dist/server/lib/git.js +385 -6
  96. package/dist/server/lib/git.js.map +1 -1
  97. package/dist/server/lib/integrations/github.js +43 -0
  98. package/dist/server/lib/integrations/github.js.map +1 -1
  99. package/dist/server/lib/integrations/githubActions.js +1 -0
  100. package/dist/server/lib/integrations/githubActions.js.map +1 -1
  101. package/dist/server/lib/integrations/githubActionsSessions.js +337 -114
  102. package/dist/server/lib/integrations/githubActionsSessions.js.map +1 -1
  103. package/dist/server/lib/projectMemorySelections.js +7 -3
  104. package/dist/server/lib/projectMemorySelections.js.map +1 -1
  105. package/dist/server/lib/shutdown.js +2 -2
  106. package/dist/server/lib/shutdown.js.map +1 -1
  107. package/dist/server/lib/specArtifact.js +3 -0
  108. package/dist/server/lib/specArtifact.js.map +1 -1
  109. package/dist/server/lib/specFormatting.js +1 -1
  110. package/dist/server/lib/specFormatting.js.map +1 -1
  111. package/dist/server/lib/specOperations.js +78 -12
  112. package/dist/server/lib/specOperations.js.map +1 -1
  113. package/dist/server/lib/specReviewContext.js +54 -18
  114. package/dist/server/lib/specReviewContext.js.map +1 -1
  115. package/dist/server/lib/specReviewDraft.js +3 -2
  116. package/dist/server/lib/specReviewDraft.js.map +1 -1
  117. package/dist/server/lib/specReviewOperations.js +74 -9
  118. package/dist/server/lib/specReviewOperations.js.map +1 -1
  119. package/dist/server/lib/specReviewValidation.js +217 -0
  120. package/dist/server/lib/specReviewValidation.js.map +1 -0
  121. package/dist/server/lib/specReviews.js +30 -2
  122. package/dist/server/lib/specReviews.js.map +1 -1
  123. package/dist/server/lib/specs.js +18 -1
  124. package/dist/server/lib/specs.js.map +1 -1
  125. package/dist/server/lib/stageSnapshots.js +197 -0
  126. package/dist/server/lib/stageSnapshots.js.map +1 -0
  127. package/dist/server/lib/testSessions.js +160 -18
  128. package/dist/server/lib/testSessions.js.map +1 -1
  129. package/dist/server/lib/ticketAutomation.js +80 -4
  130. package/dist/server/lib/ticketAutomation.js.map +1 -1
  131. package/dist/server/lib/ticketAutomationCommit.js +466 -91
  132. package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
  133. package/dist/server/lib/ticketFollowUpOperations.js +270 -29
  134. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  135. package/dist/server/lib/ticketMessages.js +16 -0
  136. package/dist/server/lib/ticketMessages.js.map +1 -1
  137. package/dist/server/lib/ticketRunOperations.js +51 -6
  138. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  139. package/dist/server/lib/ticketUndo.js +77 -168
  140. package/dist/server/lib/ticketUndo.js.map +1 -1
  141. package/dist/server/lib/ticketWorkflowOperations.js +132 -28
  142. package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
  143. package/dist/server/lib/tickets.js +41 -27
  144. package/dist/server/lib/tickets.js.map +1 -1
  145. package/dist/server/lib/transcriptRuns.js +3 -3
  146. package/dist/server/lib/transcriptRuns.js.map +1 -1
  147. package/dist/server/lib/turnExecutionErrors.js +12 -0
  148. package/dist/server/lib/turnExecutionErrors.js.map +1 -0
  149. package/dist/server/lib/types.js +58 -1
  150. package/dist/server/lib/types.js.map +1 -1
  151. package/dist/server/workers/build.js +596 -184
  152. package/dist/server/workers/build.js.map +1 -1
  153. package/dist/server/workers/chat.js +269 -132
  154. package/dist/server/workers/chat.js.map +1 -1
  155. package/dist/server/workers/followUp.js +731 -203
  156. package/dist/server/workers/followUp.js.map +1 -1
  157. package/dist/server/workers/plan.js +546 -188
  158. package/dist/server/workers/plan.js.map +1 -1
  159. package/dist/server/workers/planFollowUp.js +240 -75
  160. package/dist/server/workers/planFollowUp.js.map +1 -1
  161. package/dist/server/workers/review.js +790 -297
  162. package/dist/server/workers/review.js.map +1 -1
  163. package/dist/server/workers/specReview.js +212 -46
  164. package/dist/server/workers/specReview.js.map +1 -1
  165. package/dist/server/workers/specSession.js +434 -254
  166. package/dist/server/workers/specSession.js.map +1 -1
  167. package/package.json +1 -1
  168. package/dist/client/assets/GitHubActionsDialog-DAD5REmY.js +0 -1
  169. package/dist/client/assets/TestsDialog-B5NVrroq.js +0 -4
  170. package/dist/client/assets/TicketChangesModal-By-_gTGd.js +0 -1
  171. package/dist/client/assets/channel-Dd5Len0X.js +0 -1
  172. package/dist/client/assets/classDiagram-4FO5ZUOK-B7KUg6r8.js +0 -1
  173. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CQm-z5c4.js +0 -1
  174. package/dist/client/assets/index-BHq6-Eir.js +0 -270
  175. package/dist/client/assets/index-BxTc5qQT.css +0 -2
  176. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-C8yO6POT.js +0 -1
package/dist/server/db.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import { createHash } from 'crypto';
3
3
  import { ensureParentDirectory, resolveArchonDbPath } from './lib/runtimePaths.js';
4
+ import { TRANSCRIPT_RUN_TEXT_LIMIT_BYTES, truncateUtf8 } from './lib/transcriptRuns.js';
4
5
  const dbPath = ensureParentDirectory(resolveArchonDbPath());
5
6
  const db = new Database(dbPath);
6
7
  db.pragma('foreign_keys = ON');
@@ -11,6 +12,7 @@ db.exec(`
11
12
  description TEXT,
12
13
  state TEXT NOT NULL DEFAULT 'plan',
13
14
  agent_status TEXT,
15
+ agent_operation TEXT,
14
16
  parked_at DATETIME,
15
17
  auto_park_dismissed_at DATETIME,
16
18
  branch TEXT,
@@ -89,6 +91,7 @@ const ensureColumn = (table, column, type) => {
89
91
  }
90
92
  };
91
93
  ensureColumn('tickets', 'model', 'TEXT');
94
+ ensureColumn('tickets', 'agent_operation', 'TEXT');
92
95
  ensureColumn('tickets', 'variant', 'TEXT');
93
96
  ensureColumn('tickets', 'skills_json', 'TEXT');
94
97
  ensureColumn('tickets', 'session_id', 'TEXT');
@@ -1241,6 +1244,7 @@ ensureColumn('review_runs', 'workspace_signature', 'TEXT');
1241
1244
  db.exec(`
1242
1245
  CREATE TABLE IF NOT EXISTS agent_runs (
1243
1246
  id TEXT PRIMARY KEY,
1247
+ execution_run_id TEXT,
1244
1248
  parent_type TEXT NOT NULL,
1245
1249
  parent_id TEXT NOT NULL,
1246
1250
  run_index INTEGER NOT NULL,
@@ -1250,12 +1254,451 @@ db.exec(`
1250
1254
  agent_log TEXT,
1251
1255
  streaming_response TEXT,
1252
1256
  response_text TEXT,
1257
+ failure_message TEXT,
1253
1258
  session_id TEXT,
1259
+ completed_at DATETIME,
1254
1260
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1255
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
1261
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1262
+ FOREIGN KEY (execution_run_id) REFERENCES execution_runs(id) ON DELETE CASCADE,
1263
+ CHECK (failure_message IS NULL OR length(CAST(failure_message AS BLOB)) <= 16384)
1256
1264
  )
1257
1265
  `);
1266
+ const executionRunsTableSql = `
1267
+ CREATE TABLE execution_runs (
1268
+ id TEXT PRIMARY KEY,
1269
+ project_id TEXT NOT NULL,
1270
+ kind TEXT NOT NULL,
1271
+ status TEXT NOT NULL DEFAULT 'queued',
1272
+ domain_status TEXT,
1273
+ source_kind TEXT,
1274
+ source_id TEXT,
1275
+ source_label TEXT,
1276
+ source_url TEXT,
1277
+ correlation_id TEXT NOT NULL,
1278
+ initiated_by TEXT NOT NULL DEFAULT 'user',
1279
+ metadata_schema_version INTEGER NOT NULL DEFAULT 1,
1280
+ metadata_json TEXT,
1281
+ parent_run_id TEXT,
1282
+ retry_of_run_id TEXT,
1283
+ attempt INTEGER NOT NULL DEFAULT 1,
1284
+ failure_code TEXT,
1285
+ failure_message TEXT,
1286
+ failure_details_json TEXT,
1287
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
1288
+ started_at DATETIME,
1289
+ completed_at DATETIME,
1290
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
1291
+ UNIQUE (id, project_id),
1292
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
1293
+ FOREIGN KEY (parent_run_id) REFERENCES execution_runs(id) ON DELETE RESTRICT,
1294
+ FOREIGN KEY (retry_of_run_id) REFERENCES execution_runs(id) ON DELETE RESTRICT,
1295
+ CHECK (length(kind) BETWEEN 1 AND 100),
1296
+ CHECK (status IN ('queued', 'running', 'waiting', 'completed', 'failed', 'cancelled', 'interrupted')),
1297
+ CHECK (length(correlation_id) >= 1),
1298
+ CHECK (initiated_by IN ('user', 'automation', 'recovery', 'system')),
1299
+ CHECK (metadata_schema_version >= 1),
1300
+ CHECK (metadata_json IS NULL OR length(metadata_json) <= 16384),
1301
+ CHECK (attempt >= 1)
1302
+ )
1303
+ `;
1304
+ const lifecycleEventsTableSql = `
1305
+ CREATE TABLE lifecycle_events (
1306
+ id TEXT PRIMARY KEY,
1307
+ project_id TEXT NOT NULL,
1308
+ execution_run_id TEXT NOT NULL,
1309
+ correlation_id TEXT NOT NULL,
1310
+ sequence INTEGER NOT NULL,
1311
+ event_type TEXT NOT NULL,
1312
+ from_status TEXT,
1313
+ to_status TEXT,
1314
+ domain_status TEXT,
1315
+ message TEXT,
1316
+ metadata_schema_version INTEGER NOT NULL DEFAULT 1,
1317
+ metadata_json TEXT,
1318
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
1319
+ UNIQUE (execution_run_id, sequence),
1320
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
1321
+ FOREIGN KEY (execution_run_id, project_id) REFERENCES execution_runs(id, project_id) ON DELETE CASCADE,
1322
+ CHECK (length(correlation_id) >= 1),
1323
+ CHECK (sequence >= 1),
1324
+ CHECK (from_status IS NULL OR from_status IN ('queued', 'running', 'waiting', 'completed', 'failed', 'cancelled', 'interrupted')),
1325
+ CHECK (to_status IS NULL OR to_status IN ('queued', 'running', 'waiting', 'completed', 'failed', 'cancelled', 'interrupted')),
1326
+ CHECK (metadata_schema_version >= 1)
1327
+ )
1328
+ `;
1329
+ db.exec(executionRunsTableSql.replace('CREATE TABLE execution_runs', 'CREATE TABLE IF NOT EXISTS execution_runs'));
1330
+ db.exec(lifecycleEventsTableSql.replace('CREATE TABLE lifecycle_events', 'CREATE TABLE IF NOT EXISTS lifecycle_events'));
1331
+ const migrateExecutionLifecycleSchema = () => {
1332
+ const runColumns = db.prepare('PRAGMA table_info(execution_runs)').all();
1333
+ const eventColumns = db.prepare('PRAGMA table_info(lifecycle_events)').all();
1334
+ const runTableSql = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'execution_runs'").get()?.sql ?? '';
1335
+ const correlationColumn = runColumns.find(column => column.name === 'correlation_id');
1336
+ const needsMigration = !runColumns.some(column => column.name === 'initiated_by')
1337
+ || !runColumns.some(column => column.name === 'metadata_json')
1338
+ || correlationColumn?.notnull !== 1
1339
+ || !eventColumns.some(column => column.name === 'sequence')
1340
+ || !eventColumns.some(column => column.name === 'correlation_id')
1341
+ || /CHECK\s*\(\s*kind\s+IN/i.test(runTableSql)
1342
+ || /length\s*\(\s*correlation_id\s*\)\s+BETWEEN/i.test(runTableSql);
1343
+ if (!needsMigration) {
1344
+ return;
1345
+ }
1346
+ const sourceLabel = runColumns.some(column => column.name === 'source_label') ? 'source_label' : 'NULL';
1347
+ const legacyCorrelationId = runColumns.some(column => column.name === 'correlation_id')
1348
+ ? 'CASE WHEN correlation_id IS NOT NULL AND length(trim(correlation_id)) > 0 THEN correlation_id ELSE NULL END'
1349
+ : 'NULL';
1350
+ db.exec('PRAGMA foreign_keys = OFF');
1351
+ try {
1352
+ db.exec('BEGIN');
1353
+ db.exec(executionRunsTableSql.replace('execution_runs', 'execution_runs_v2'));
1354
+ db.exec(lifecycleEventsTableSql.replace('lifecycle_events', 'lifecycle_events_v2').replaceAll('execution_runs', 'execution_runs_v2'));
1355
+ db.exec(`
1356
+ CREATE TEMP TABLE execution_run_correlations (
1357
+ id TEXT PRIMARY KEY,
1358
+ correlation_id TEXT
1359
+ )
1360
+ `);
1361
+ db.exec(`
1362
+ INSERT INTO execution_run_correlations (id, correlation_id)
1363
+ SELECT
1364
+ id,
1365
+ COALESCE(
1366
+ ${legacyCorrelationId},
1367
+ CASE WHEN parent_run_id IS NULL THEN CASE WHEN length(id) > 0 THEN id ELSE 'legacy-run' END END
1368
+ )
1369
+ FROM execution_runs
1370
+ `);
1371
+ const inheritCorrelation = db.prepare(`
1372
+ UPDATE execution_run_correlations
1373
+ SET correlation_id = (
1374
+ SELECT parent.correlation_id
1375
+ FROM execution_runs child
1376
+ JOIN execution_run_correlations parent ON parent.id = child.parent_run_id
1377
+ WHERE child.id = execution_run_correlations.id
1378
+ )
1379
+ WHERE correlation_id IS NULL
1380
+ AND EXISTS (
1381
+ SELECT 1
1382
+ FROM execution_runs child
1383
+ JOIN execution_run_correlations parent ON parent.id = child.parent_run_id
1384
+ WHERE child.id = execution_run_correlations.id
1385
+ AND parent.correlation_id IS NOT NULL
1386
+ )
1387
+ `);
1388
+ while (inheritCorrelation.run().changes > 0) {
1389
+ // Resolve one lineage level at a time so descendants inherit the migrated root correlation.
1390
+ }
1391
+ db.exec(`
1392
+ UPDATE execution_run_correlations
1393
+ SET correlation_id = CASE WHEN length(id) > 0 THEN id ELSE 'legacy-run' END
1394
+ WHERE correlation_id IS NULL
1395
+ `);
1396
+ db.exec(`
1397
+ INSERT INTO execution_runs_v2 (
1398
+ id, project_id, kind, status, domain_status, source_kind, source_id, source_label,
1399
+ source_url, correlation_id, initiated_by, metadata_schema_version, metadata_json,
1400
+ parent_run_id, retry_of_run_id, attempt, failure_code, failure_message,
1401
+ failure_details_json, created_at, started_at, completed_at, updated_at
1402
+ )
1403
+ SELECT
1404
+ id, project_id, kind, status, domain_status, source_kind, source_id, ${sourceLabel},
1405
+ source_url,
1406
+ correlations.correlation_id,
1407
+ 'system', 1, NULL, parent_run_id, retry_of_run_id, attempt, failure_code,
1408
+ failure_message, failure_details_json, created_at, started_at, completed_at, updated_at
1409
+ FROM execution_runs
1410
+ JOIN execution_run_correlations correlations USING (id)
1411
+ `);
1412
+ db.exec(`
1413
+ INSERT INTO lifecycle_events_v2 (
1414
+ id, project_id, execution_run_id, correlation_id, sequence, event_type,
1415
+ from_status, to_status, domain_status, message, metadata_schema_version,
1416
+ metadata_json, created_at
1417
+ )
1418
+ SELECT
1419
+ event.id,
1420
+ event.project_id,
1421
+ event.execution_run_id,
1422
+ run.correlation_id,
1423
+ 1 + (
1424
+ SELECT COUNT(*)
1425
+ FROM lifecycle_events preceding
1426
+ WHERE preceding.execution_run_id = event.execution_run_id
1427
+ AND (
1428
+ preceding.created_at < event.created_at
1429
+ OR (preceding.created_at = event.created_at AND preceding.id < event.id)
1430
+ )
1431
+ ),
1432
+ event.event_type,
1433
+ event.from_status,
1434
+ event.to_status,
1435
+ event.domain_status,
1436
+ event.message,
1437
+ event.metadata_schema_version,
1438
+ event.metadata_json,
1439
+ event.created_at
1440
+ FROM lifecycle_events event
1441
+ JOIN execution_runs_v2 run
1442
+ ON run.id = event.execution_run_id AND run.project_id = event.project_id
1443
+ `);
1444
+ db.exec('DROP TABLE lifecycle_events');
1445
+ db.exec('DROP TABLE execution_runs');
1446
+ db.exec('ALTER TABLE execution_runs_v2 RENAME TO execution_runs');
1447
+ db.exec('ALTER TABLE lifecycle_events_v2 RENAME TO lifecycle_events');
1448
+ db.exec('DROP TABLE execution_run_correlations');
1449
+ const violations = db.prepare('PRAGMA foreign_key_check').all();
1450
+ if (violations.some(violation => ['execution_runs', 'lifecycle_events', 'test_execution_details', 'github_actions_run_details'].includes(violation.table))) {
1451
+ throw new Error('Execution lifecycle migration produced foreign key violations');
1452
+ }
1453
+ db.exec('COMMIT');
1454
+ }
1455
+ catch (error) {
1456
+ db.exec('ROLLBACK');
1457
+ throw error;
1458
+ }
1459
+ finally {
1460
+ db.exec('PRAGMA foreign_keys = ON');
1461
+ }
1462
+ };
1463
+ migrateExecutionLifecycleSchema();
1464
+ const migrateAgentRunsSchema = () => {
1465
+ const columns = db.prepare('PRAGMA table_info(agent_runs)').all();
1466
+ const tableSql = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'agent_runs'").get()?.sql ?? '';
1467
+ const needsMigration = !columns.some(column => column.name === 'execution_run_id')
1468
+ || !columns.some(column => column.name === 'completed_at')
1469
+ || !columns.some(column => column.name === 'failure_message')
1470
+ || !/REFERENCES\s+execution_runs/i.test(tableSql)
1471
+ || !/failure_message[\s\S]*16384/i.test(tableSql);
1472
+ if (!needsMigration) {
1473
+ return;
1474
+ }
1475
+ const executionRunId = columns.some(column => column.name === 'execution_run_id')
1476
+ ? 'CASE WHEN execution_run_id IN (SELECT id FROM execution_runs) THEN execution_run_id ELSE NULL END'
1477
+ : 'NULL';
1478
+ const completedAt = columns.some(column => column.name === 'completed_at') ? 'completed_at' : 'NULL';
1479
+ const failureMessage = columns.some(column => column.name === 'failure_message')
1480
+ ? 'CASE WHEN failure_message IS NULL OR length(CAST(failure_message AS BLOB)) <= 16384 THEN failure_message ELSE NULL END'
1481
+ : 'NULL';
1482
+ db.exec('PRAGMA foreign_keys = OFF');
1483
+ try {
1484
+ db.exec('BEGIN');
1485
+ db.exec(`
1486
+ CREATE TABLE agent_runs_v2 (
1487
+ id TEXT PRIMARY KEY,
1488
+ execution_run_id TEXT,
1489
+ parent_type TEXT NOT NULL,
1490
+ parent_id TEXT NOT NULL,
1491
+ run_index INTEGER NOT NULL,
1492
+ label TEXT NOT NULL,
1493
+ role TEXT NOT NULL,
1494
+ agent_status TEXT NOT NULL,
1495
+ agent_log TEXT,
1496
+ streaming_response TEXT,
1497
+ response_text TEXT,
1498
+ failure_message TEXT,
1499
+ session_id TEXT,
1500
+ completed_at DATETIME,
1501
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1502
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1503
+ FOREIGN KEY (execution_run_id) REFERENCES execution_runs(id) ON DELETE CASCADE,
1504
+ CHECK (failure_message IS NULL OR length(CAST(failure_message AS BLOB)) <= 16384)
1505
+ )
1506
+ `);
1507
+ db.exec(`
1508
+ INSERT INTO agent_runs_v2 (
1509
+ id, execution_run_id, parent_type, parent_id, run_index, label, role,
1510
+ agent_status, agent_log, streaming_response, response_text, failure_message,
1511
+ session_id, completed_at, created_at, updated_at
1512
+ )
1513
+ SELECT
1514
+ id, ${executionRunId}, parent_type, parent_id, run_index, label, role,
1515
+ agent_status, agent_log, streaming_response, response_text, ${failureMessage},
1516
+ session_id, ${completedAt}, created_at, updated_at
1517
+ FROM agent_runs
1518
+ `);
1519
+ db.exec('DROP TABLE agent_runs');
1520
+ db.exec('ALTER TABLE agent_runs_v2 RENAME TO agent_runs');
1521
+ const violations = db.prepare('PRAGMA foreign_key_check').all();
1522
+ if (violations.some(violation => violation.table === 'agent_runs' || violation.table === 'project_memory_selections')) {
1523
+ throw new Error('Agent run migration produced foreign key violations');
1524
+ }
1525
+ db.exec('COMMIT');
1526
+ }
1527
+ catch (error) {
1528
+ db.exec('ROLLBACK');
1529
+ throw error;
1530
+ }
1531
+ finally {
1532
+ db.exec('PRAGMA foreign_keys = ON');
1533
+ }
1534
+ };
1535
+ migrateAgentRunsSchema();
1536
+ const oversizedAgentRunRows = db.prepare(`
1537
+ SELECT id, agent_log, streaming_response, response_text, failure_message
1538
+ FROM agent_runs
1539
+ WHERE length(CAST(agent_log AS BLOB)) > ?
1540
+ OR length(CAST(streaming_response AS BLOB)) > ?
1541
+ OR length(CAST(response_text AS BLOB)) > ?
1542
+ OR length(CAST(failure_message AS BLOB)) > 16384
1543
+ `).all(TRANSCRIPT_RUN_TEXT_LIMIT_BYTES, TRANSCRIPT_RUN_TEXT_LIMIT_BYTES, TRANSCRIPT_RUN_TEXT_LIMIT_BYTES);
1544
+ const boundAgentRunEvidenceStatement = db.prepare(`
1545
+ UPDATE agent_runs
1546
+ SET agent_log = ?, streaming_response = ?, response_text = ?, failure_message = ?
1547
+ WHERE id = ?
1548
+ `);
1549
+ const boundAgentRunEvidence = db.transaction(() => {
1550
+ for (const row of oversizedAgentRunRows) {
1551
+ boundAgentRunEvidenceStatement.run(truncateUtf8(row.agent_log).text, truncateUtf8(row.streaming_response).text, truncateUtf8(row.response_text).text, truncateUtf8(row.failure_message, 16384).text, row.id);
1552
+ }
1553
+ });
1554
+ boundAgentRunEvidence();
1258
1555
  db.exec('CREATE INDEX IF NOT EXISTS idx_agent_runs_parent ON agent_runs(parent_type, parent_id)');
1556
+ db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_runs_execution_run_index ON agent_runs(execution_run_id, run_index) WHERE execution_run_id IS NOT NULL');
1557
+ db.exec('CREATE INDEX IF NOT EXISTS idx_agent_runs_active_parent ON agent_runs(parent_type, parent_id, execution_run_id, agent_status, created_at DESC, id DESC)');
1558
+ db.exec(`
1559
+ CREATE TABLE IF NOT EXISTS execution_run_links (
1560
+ execution_run_id TEXT NOT NULL,
1561
+ project_id TEXT NOT NULL,
1562
+ entity_type TEXT NOT NULL,
1563
+ entity_id TEXT NOT NULL,
1564
+ relationship TEXT NOT NULL,
1565
+ label_snapshot TEXT,
1566
+ url_snapshot TEXT,
1567
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
1568
+ PRIMARY KEY (execution_run_id, entity_type, entity_id, relationship),
1569
+ FOREIGN KEY (execution_run_id, project_id) REFERENCES execution_runs(id, project_id) ON DELETE CASCADE,
1570
+ CHECK (length(entity_type) BETWEEN 1 AND 100),
1571
+ CHECK (length(entity_id) BETWEEN 1 AND 500),
1572
+ CHECK (relationship IN ('primary', 'context', 'result', 'trigger')),
1573
+ CHECK (label_snapshot IS NULL OR length(label_snapshot) <= 500),
1574
+ CHECK (url_snapshot IS NULL OR length(url_snapshot) <= 2000)
1575
+ )
1576
+ `);
1577
+ db.exec(`
1578
+ INSERT OR IGNORE INTO execution_run_links (
1579
+ execution_run_id, project_id, entity_type, entity_id, relationship,
1580
+ label_snapshot, url_snapshot, created_at
1581
+ )
1582
+ SELECT
1583
+ id, project_id, substr(source_kind, 1, 100), substr(source_id, 1, 500), 'primary',
1584
+ CASE WHEN source_label IS NULL THEN NULL ELSE substr(source_label, 1, 500) END,
1585
+ CASE WHEN source_url IS NULL THEN NULL ELSE substr(source_url, 1, 2000) END,
1586
+ created_at
1587
+ FROM execution_runs
1588
+ WHERE source_kind IS NOT NULL AND length(trim(source_kind)) > 0
1589
+ AND source_id IS NOT NULL AND length(trim(source_id)) > 0
1590
+ `);
1591
+ db.exec(`
1592
+ CREATE TABLE IF NOT EXISTS test_execution_details (
1593
+ execution_run_id TEXT PRIMARY KEY,
1594
+ command_id TEXT,
1595
+ command_name TEXT,
1596
+ command_runner TEXT,
1597
+ command_snapshot TEXT,
1598
+ active_command TEXT,
1599
+ target_kind TEXT,
1600
+ target_bundle_id TEXT,
1601
+ target_label TEXT,
1602
+ ticket_id TEXT,
1603
+ selection_json TEXT,
1604
+ summary_json TEXT,
1605
+ exit_code INTEGER,
1606
+ duration_ms INTEGER,
1607
+ output TEXT NOT NULL DEFAULT '',
1608
+ output_truncated INTEGER NOT NULL DEFAULT 0,
1609
+ output_retention_state TEXT NOT NULL DEFAULT 'retained',
1610
+ output_expired_at DATETIME,
1611
+ output_byte_count INTEGER,
1612
+ FOREIGN KEY (execution_run_id) REFERENCES execution_runs(id) ON DELETE CASCADE
1613
+ )
1614
+ `);
1615
+ db.exec(`
1616
+ CREATE TRIGGER IF NOT EXISTS validate_test_execution_details_target_insert
1617
+ BEFORE INSERT ON test_execution_details
1618
+ WHEN NEW.target_kind IS NULL
1619
+ OR NEW.target_kind NOT IN ('repo_root', 'bundle')
1620
+ OR (NEW.target_kind = 'repo_root' AND NEW.target_bundle_id IS NOT NULL)
1621
+ OR (NEW.target_kind = 'bundle' AND (NEW.target_bundle_id IS NULL OR length(trim(NEW.target_bundle_id)) = 0))
1622
+ BEGIN
1623
+ SELECT RAISE(ABORT, 'Invalid test execution target');
1624
+ END
1625
+ `);
1626
+ db.exec(`
1627
+ CREATE TRIGGER IF NOT EXISTS validate_test_execution_details_target_update
1628
+ BEFORE UPDATE OF target_kind, target_bundle_id ON test_execution_details
1629
+ WHEN NEW.target_kind IS NULL
1630
+ OR NEW.target_kind NOT IN ('repo_root', 'bundle')
1631
+ OR (NEW.target_kind = 'repo_root' AND NEW.target_bundle_id IS NOT NULL)
1632
+ OR (NEW.target_kind = 'bundle' AND (NEW.target_bundle_id IS NULL OR length(trim(NEW.target_bundle_id)) = 0))
1633
+ BEGIN
1634
+ SELECT RAISE(ABORT, 'Invalid test execution target');
1635
+ END
1636
+ `);
1637
+ ensureColumn('test_execution_details', 'command_snapshot', 'TEXT');
1638
+ ensureColumn('test_execution_details', 'active_command', 'TEXT');
1639
+ ensureColumn('test_execution_details', 'target_label', 'TEXT');
1640
+ ensureColumn('test_execution_details', 'output', "TEXT NOT NULL DEFAULT ''");
1641
+ ensureColumn('test_execution_details', 'output_truncated', 'INTEGER NOT NULL DEFAULT 0');
1642
+ ensureColumn('test_execution_details', 'output_retention_state', "TEXT NOT NULL DEFAULT 'retained'");
1643
+ ensureColumn('test_execution_details', 'output_expired_at', 'DATETIME');
1644
+ ensureColumn('test_execution_details', 'output_byte_count', 'INTEGER');
1645
+ db.prepare(`
1646
+ UPDATE test_execution_details
1647
+ SET output_byte_count = length(CAST(output AS BLOB))
1648
+ WHERE output_byte_count IS NULL AND output_retention_state = 'retained'
1649
+ `).run();
1650
+ db.exec(`
1651
+ CREATE TABLE IF NOT EXISTS github_actions_run_details (
1652
+ execution_run_id TEXT PRIMARY KEY,
1653
+ workflow_id INTEGER NOT NULL,
1654
+ workflow_name TEXT,
1655
+ ref TEXT,
1656
+ input_names_json TEXT,
1657
+ input_fingerprint TEXT,
1658
+ resolution_attempts INTEGER NOT NULL DEFAULT 0,
1659
+ provider_error_count INTEGER NOT NULL DEFAULT 0,
1660
+ provider_run_id INTEGER,
1661
+ provider_status TEXT,
1662
+ conclusion TEXT,
1663
+ html_url TEXT,
1664
+ head_sha TEXT,
1665
+ head_branch TEXT,
1666
+ provider_created_at DATETIME,
1667
+ provider_updated_at DATETIME,
1668
+ provider_started_at DATETIME,
1669
+ dispatched_at DATETIME,
1670
+ last_synced_at DATETIME,
1671
+ jobs_json TEXT,
1672
+ jobs_retention_state TEXT NOT NULL DEFAULT 'retained',
1673
+ jobs_expired_at DATETIME,
1674
+ jobs_count INTEGER,
1675
+ FOREIGN KEY (execution_run_id) REFERENCES execution_runs(id) ON DELETE CASCADE
1676
+ )
1677
+ `);
1678
+ ensureColumn('github_actions_run_details', 'ref', 'TEXT');
1679
+ ensureColumn('github_actions_run_details', 'input_names_json', 'TEXT');
1680
+ ensureColumn('github_actions_run_details', 'input_fingerprint', 'TEXT');
1681
+ ensureColumn('github_actions_run_details', 'resolution_attempts', 'INTEGER NOT NULL DEFAULT 0');
1682
+ ensureColumn('github_actions_run_details', 'provider_error_count', 'INTEGER NOT NULL DEFAULT 0');
1683
+ ensureColumn('github_actions_run_details', 'jobs_json', 'TEXT');
1684
+ ensureColumn('github_actions_run_details', 'provider_created_at', 'DATETIME');
1685
+ ensureColumn('github_actions_run_details', 'provider_updated_at', 'DATETIME');
1686
+ ensureColumn('github_actions_run_details', 'provider_started_at', 'DATETIME');
1687
+ ensureColumn('github_actions_run_details', 'jobs_retention_state', "TEXT NOT NULL DEFAULT 'retained'");
1688
+ ensureColumn('github_actions_run_details', 'jobs_expired_at', 'DATETIME');
1689
+ ensureColumn('github_actions_run_details', 'jobs_count', 'INTEGER');
1690
+ db.exec('CREATE INDEX IF NOT EXISTS idx_execution_runs_project_created ON execution_runs(project_id, created_at DESC, id DESC)');
1691
+ db.exec('CREATE INDEX IF NOT EXISTS idx_execution_runs_project_kind_status ON execution_runs(project_id, kind, status, created_at DESC, id DESC)');
1692
+ db.exec('CREATE INDEX IF NOT EXISTS idx_execution_runs_parent ON execution_runs(parent_run_id)');
1693
+ db.exec('CREATE INDEX IF NOT EXISTS idx_execution_runs_retry ON execution_runs(retry_of_run_id)');
1694
+ db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_execution_runs_direct_retry ON execution_runs(retry_of_run_id) WHERE retry_of_run_id IS NOT NULL');
1695
+ db.exec('CREATE INDEX IF NOT EXISTS idx_execution_runs_correlation ON execution_runs(project_id, correlation_id)');
1696
+ db.exec('CREATE INDEX IF NOT EXISTS idx_execution_run_links_project_entity ON execution_run_links(project_id, entity_type, entity_id, created_at DESC)');
1697
+ db.exec('CREATE INDEX IF NOT EXISTS idx_execution_run_links_run_relationship ON execution_run_links(execution_run_id, relationship, created_at ASC)');
1698
+ db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_execution_run_links_primary ON execution_run_links(execution_run_id) WHERE relationship = 'primary'");
1699
+ db.exec('CREATE INDEX IF NOT EXISTS idx_lifecycle_events_run_sequence ON lifecycle_events(project_id, execution_run_id, sequence ASC)');
1700
+ db.exec('CREATE INDEX IF NOT EXISTS idx_lifecycle_events_project_created ON lifecycle_events(project_id, created_at DESC, id DESC)');
1701
+ db.exec('CREATE INDEX IF NOT EXISTS idx_github_actions_details_dispatch ON github_actions_run_details(workflow_id, ref, dispatched_at DESC)');
1259
1702
  db.exec(`
1260
1703
  CREATE TABLE IF NOT EXISTS project_memory_selections (
1261
1704
  id TEXT PRIMARY KEY,
@@ -1374,6 +1817,9 @@ db.exec(`
1374
1817
  worktree_bundle_id TEXT NOT NULL,
1375
1818
  review_iteration_count INTEGER NOT NULL DEFAULT 0,
1376
1819
  fix_loop_active INTEGER NOT NULL DEFAULT 0,
1820
+ automation_progression_run_id TEXT,
1821
+ automation_paused_stage_run_id TEXT,
1822
+ automation_pause_reason_code TEXT,
1377
1823
  automation_paused_code TEXT,
1378
1824
  automation_paused_reason TEXT,
1379
1825
  automation_paused_at DATETIME,
@@ -1385,6 +1831,29 @@ db.exec(`
1385
1831
  )
1386
1832
  `);
1387
1833
  ensureColumn('bundle_automation_state', 'automation_paused_code', 'TEXT');
1834
+ ensureColumn('bundle_automation_state', 'automation_progression_run_id', 'TEXT');
1835
+ ensureColumn('bundle_automation_state', 'automation_paused_stage_run_id', 'TEXT');
1836
+ ensureColumn('bundle_automation_state', 'automation_pause_reason_code', 'TEXT');
1837
+ db.exec(`
1838
+ CREATE TABLE IF NOT EXISTS automation_stage_details (
1839
+ execution_run_id TEXT PRIMARY KEY,
1840
+ project_id TEXT NOT NULL,
1841
+ worktree_bundle_id TEXT NOT NULL,
1842
+ stage_kind TEXT NOT NULL,
1843
+ schema_version INTEGER NOT NULL,
1844
+ intent_json TEXT NOT NULL,
1845
+ result_json TEXT,
1846
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
1847
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
1848
+ FOREIGN KEY (execution_run_id, project_id) REFERENCES execution_runs(id, project_id) ON DELETE CASCADE,
1849
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
1850
+ FOREIGN KEY (worktree_bundle_id) REFERENCES worktree_bundles(id) ON DELETE CASCADE,
1851
+ CHECK (stage_kind IN ('automation_commit', 'automation_push', 'automation_pull_request')),
1852
+ CHECK (schema_version = 1),
1853
+ CHECK (length(CAST(intent_json AS BLOB)) BETWEEN 2 AND 16384),
1854
+ CHECK (result_json IS NULL OR length(CAST(result_json AS BLOB)) BETWEEN 2 AND 16384)
1855
+ )
1856
+ `);
1388
1857
  db.exec(`
1389
1858
  CREATE TABLE IF NOT EXISTS build_open_question_batches (
1390
1859
  id TEXT PRIMARY KEY,
@@ -1447,47 +1916,11 @@ db.exec('CREATE INDEX IF NOT EXISTS idx_build_sequence_tickets_sequence_position
1447
1916
  db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_integration_connections_provider_project ON integration_connections(provider, project_id)');
1448
1917
  db.exec('CREATE INDEX IF NOT EXISTS idx_tickets_external_key ON tickets(external_provider, external_key)');
1449
1918
  db.exec('CREATE INDEX IF NOT EXISTS idx_bundle_automation_state_project_bundle ON bundle_automation_state(project_id, worktree_bundle_id)');
1919
+ db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_bundle_automation_state_progression ON bundle_automation_state(automation_progression_run_id) WHERE automation_progression_run_id IS NOT NULL');
1920
+ db.exec('CREATE INDEX IF NOT EXISTS idx_automation_stage_details_bundle_stage ON automation_stage_details(project_id, worktree_bundle_id, stage_kind, created_at DESC)');
1450
1921
  db.exec('CREATE INDEX IF NOT EXISTS idx_build_open_questions_ticket_status ON build_open_questions(ticket_id, status)');
1451
1922
  db.exec('CREATE INDEX IF NOT EXISTS idx_build_open_questions_bundle_status ON build_open_questions(project_id, worktree_bundle_id, status)');
1452
1923
  db.exec('CREATE INDEX IF NOT EXISTS idx_build_open_question_batches_bundle_status ON build_open_question_batches(project_id, worktree_bundle_id, status)');
1453
- const recoverInterruptedBuildOpenQuestionBatches = () => {
1454
- const interrupted = db.prepare(`
1455
- SELECT id, project_id, worktree_bundle_id
1456
- FROM build_open_question_batches
1457
- WHERE status IN ('pending', 'running')
1458
- `).all();
1459
- if (interrupted.length === 0) {
1460
- return;
1461
- }
1462
- const recover = db.transaction(() => {
1463
- for (const batch of interrupted) {
1464
- db.prepare(`
1465
- UPDATE build_open_question_batches
1466
- SET status = 'error', error_message = ?, completed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
1467
- WHERE id = ? AND status IN ('pending', 'running')
1468
- `).run('Server restarted before the Build open question follow-up completed', batch.id);
1469
- db.prepare(`
1470
- UPDATE build_open_questions
1471
- SET status = 'open', answer_text = NULL, answered_at = NULL,
1472
- dismissed_at = NULL, dismissed_by = NULL, dismissal_reason = NULL,
1473
- resolution_batch_id = NULL, updated_at = CURRENT_TIMESTAMP
1474
- WHERE resolution_batch_id = ? AND status IN ('answered', 'dismissed')
1475
- `).run(batch.id);
1476
- db.prepare(`
1477
- INSERT INTO bundle_automation_state (
1478
- project_id, worktree_bundle_id, automation_paused_code, automation_paused_reason, automation_paused_at
1479
- ) VALUES (?, ?, 'open_questions', ?, CURRENT_TIMESTAMP)
1480
- ON CONFLICT(project_id, worktree_bundle_id) DO UPDATE SET
1481
- automation_paused_code = 'open_questions',
1482
- automation_paused_reason = excluded.automation_paused_reason,
1483
- automation_paused_at = CURRENT_TIMESTAMP,
1484
- updated_at = CURRENT_TIMESTAMP
1485
- `).run(batch.project_id, batch.worktree_bundle_id, 'Build open question follow-up was interrupted by a server restart');
1486
- }
1487
- });
1488
- recover();
1489
- };
1490
- recoverInterruptedBuildOpenQuestionBatches();
1491
1924
  const migrateLegacyBuildOpenQuestions = () => {
1492
1925
  const rows = db.prepare(`
1493
1926
  SELECT id, project_id, worktree_bundle_id, planning_context_json