@straiffi/archon 1.8.19 → 1.8.21

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 (139) hide show
  1. package/dist/client/assets/{ActivityView-BaoAJ8JM.js → ActivityView-BcguXTZ5.js} +1 -1
  2. package/dist/client/assets/GitHubActionsDialog-BGecTzoq.js +1 -0
  3. package/dist/client/assets/VerificationDialog-D4QkMLZr.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-8hDT9YFW.js → architectureDiagram-3BPJPVTR-DnC7Twj0.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-DrSgZhh1.js → blockDiagram-GPEHLZMM-D0VbjiKd.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-BjowoLmV.js → c4Diagram-AAUBKEIU-CzQ3l15K.js} +1 -1
  7. package/dist/client/assets/channel-BwRO4uiw.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-BwZDhXTf.js → chunk-2J33WTMH-B5UGiqLW.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-CVc43Ukf.js → chunk-3OPIFGDE-SCksTqtu.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-D5FDCGhy.js → chunk-5ZQYHXKU-IOkwLOtG.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-CJSR5Mh2.js → chunk-727SXJPM-Do60Xs9p.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-B6kTsClX.js → chunk-AQP2D5EJ-CuQT3imB.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-C5UH_dAR.js → chunk-CSCIHK7Q-BkxMIESX.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-D_rzcB2b.js → chunk-KSCS5N6A-uUU3sWgR.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-CztP7jdd.js → chunk-L5ZTLDWV-DLYccV6J.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-wTLCL7Pg.js → chunk-LZXEDZCA-CgJFfCRZ.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-B5AHIrUy.js → chunk-ND2GUHAM-D7IQSQfK.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-D3MtrIk4.js → chunk-NZK2D7GU-B4O6JWAo.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-iIPXNV3t.js → chunk-O5CBEL6O-DidlQEqb.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-Cwu-Oylq.js → chunk-WU5MYG2G-2-oq_Gue.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-BIszcze-.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-D5dvxldS.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-D3wiIOnk.js → dagre-BM42HDAG-TubuN0hR.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-Bysb-ntU.js → diagram-2AECGRRQ-DNcUk6rV.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-CtS_bpE5.js → diagram-5GNKFQAL-B1sJn4w_.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-_PI56fZq.js → diagram-KO2AKTUF-CnW6ZuQe.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-BNgWnFoz.js → diagram-LMA3HP47-YHUmDzyS.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-BM-wG_lJ.js → diagram-OG6HWLK6-BkOQphxq.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-BPqNBsVc.js → erDiagram-TEJ5UH35-oNMxAdxN.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-BgA6MiBD.js → flowDiagram-I6XJVG4X-ByuCV-Am.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-BMDzgC7L.js → ganttDiagram-6RSMTGT7-CRkNH8Ft.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-C0cZUYmG.js → gitGraphDiagram-PVQCEYII-DRXzXjd8.js} +1 -1
  33. package/dist/client/assets/index-CYPgEn1h.css +2 -0
  34. package/dist/client/assets/index-Ck2OKMsl.js +276 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-DelAWwhC.js → infoDiagram-5YYISTIA-CJ4-hWch.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-D62h0NJG.js → ishikawaDiagram-YF4QCWOH-CW1xqvim.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-BXwYQf_j.js → journeyDiagram-JHISSGLW-BVVFg8_h.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-x5KfcIYo.js → kanban-definition-UN3LZRKU-AcLZ63tA.js} +1 -1
  39. package/dist/client/assets/{line-Blzshjst.js → line-CG_TG6Ci.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-Dnu-fpL9.js → mermaid-parser.core-UDLSm_c3.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-BngDHztH.js → mermaid.core-IvBk22DF.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BRB9WnNL.js → mindmap-definition-RKZ34NQL-BflWNtnl.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-Ddzhxmp2.js → pieDiagram-4H26LBE5-Dm1iGMm7.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-CWKRt2y6.js → quadrantDiagram-W4KKPZXB-CM4zJd_R.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-D309qLEz.js → requirementDiagram-4Y6WPE33-miuP86cp.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-0Ukkluzd.js → sankeyDiagram-5OEKKPKP-zUcRagtr.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DkvDbOkQ.js → sequenceDiagram-3UESZ5HK-BRYqVpsT.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-LRTZRjK3.js → stateDiagram-AJRCARHV-DwcF0DJm.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DmICWsmn.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-CTwwDhpg.js → timeline-definition-PNZ67QCA-BzH929s_.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-BqldUpJn.js → vennDiagram-CIIHVFJN-BIq8rooy.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DPCPZAsx.js → wardleyDiagram-YWT4CUSO-CyV_gL5J.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DWkIjgNu.js → xychartDiagram-2RQKCTM6-Dxo-U-qe.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +431 -1
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +77 -17
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agent.js +8 -4
  60. package/dist/server/lib/agent.js.map +1 -1
  61. package/dist/server/lib/agentProvider.js +8 -1
  62. package/dist/server/lib/agentProvider.js.map +1 -1
  63. package/dist/server/lib/automationProgression.js +44 -14
  64. package/dist/server/lib/automationProgression.js.map +1 -1
  65. package/dist/server/lib/buildCompletionContinuation.js +14 -3
  66. package/dist/server/lib/buildCompletionContinuation.js.map +1 -1
  67. package/dist/server/lib/buildExecutionEnvironment.js +226 -59
  68. package/dist/server/lib/buildExecutionEnvironment.js.map +1 -1
  69. package/dist/server/lib/buildOpenQuestionOperations.js +47 -3
  70. package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -1
  71. package/dist/server/lib/buildSequences.js +267 -18
  72. package/dist/server/lib/buildSequences.js.map +1 -1
  73. package/dist/server/lib/buildSetupAuthority.js +138 -0
  74. package/dist/server/lib/buildSetupAuthority.js.map +1 -0
  75. package/dist/server/lib/bundleReviewOperations.js +15 -4
  76. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  77. package/dist/server/lib/dockerSandboxCredentials.js +64 -10
  78. package/dist/server/lib/dockerSandboxCredentials.js.map +1 -1
  79. package/dist/server/lib/dockerSandboxExecutionProvider.js +11 -2
  80. package/dist/server/lib/dockerSandboxExecutionProvider.js.map +1 -1
  81. package/dist/server/lib/executionCredentialCapabilities.js +106 -0
  82. package/dist/server/lib/executionCredentialCapabilities.js.map +1 -0
  83. package/dist/server/lib/executionEnvironment.js.map +1 -1
  84. package/dist/server/lib/executionEnvironmentContexts.js +55 -1
  85. package/dist/server/lib/executionEnvironmentContexts.js.map +1 -1
  86. package/dist/server/lib/executionEnvironmentRoutes.js +76 -5
  87. package/dist/server/lib/executionEnvironmentRoutes.js.map +1 -1
  88. package/dist/server/lib/executionEnvironmentValidation.js +116 -44
  89. package/dist/server/lib/executionEnvironmentValidation.js.map +1 -1
  90. package/dist/server/lib/executionEnvironmentValidationOperations.js +48 -8
  91. package/dist/server/lib/executionEnvironmentValidationOperations.js.map +1 -1
  92. package/dist/server/lib/executionEnvironments.js +92 -9
  93. package/dist/server/lib/executionEnvironments.js.map +1 -1
  94. package/dist/server/lib/executionProfileRoutes.js +67 -0
  95. package/dist/server/lib/executionProfileRoutes.js.map +1 -0
  96. package/dist/server/lib/executionProfileStore.js +108 -0
  97. package/dist/server/lib/executionProfileStore.js.map +1 -0
  98. package/dist/server/lib/executionProfiles.js +201 -0
  99. package/dist/server/lib/executionProfiles.js.map +1 -0
  100. package/dist/server/lib/executionReconciliation.js +151 -66
  101. package/dist/server/lib/executionReconciliation.js.map +1 -1
  102. package/dist/server/lib/executionRetry.js +24 -5
  103. package/dist/server/lib/executionRetry.js.map +1 -1
  104. package/dist/server/lib/executionRunAuthority.js +299 -0
  105. package/dist/server/lib/executionRunAuthority.js.map +1 -0
  106. package/dist/server/lib/executionRuns.js +4 -0
  107. package/dist/server/lib/executionRuns.js.map +1 -1
  108. package/dist/server/lib/git.js +25 -0
  109. package/dist/server/lib/git.js.map +1 -1
  110. package/dist/server/lib/hostExecutionEnvironment.js +7 -1
  111. package/dist/server/lib/hostExecutionEnvironment.js.map +1 -1
  112. package/dist/server/lib/integrations/github.js +44 -0
  113. package/dist/server/lib/integrations/github.js.map +1 -1
  114. package/dist/server/lib/projects.js +1 -0
  115. package/dist/server/lib/projects.js.map +1 -1
  116. package/dist/server/lib/run.js +58 -2
  117. package/dist/server/lib/run.js.map +1 -1
  118. package/dist/server/lib/ticketFollowUpOperations.js +32 -6
  119. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  120. package/dist/server/lib/transcriptRuns.js +3 -0
  121. package/dist/server/lib/transcriptRuns.js.map +1 -1
  122. package/dist/server/lib/types.js.map +1 -1
  123. package/dist/server/lib/workspaceProcessEnvironment.js +72 -1
  124. package/dist/server/lib/workspaceProcessEnvironment.js.map +1 -1
  125. package/dist/server/workers/build.js +214 -46
  126. package/dist/server/workers/build.js.map +1 -1
  127. package/dist/server/workers/followUp.js +74 -1
  128. package/dist/server/workers/followUp.js.map +1 -1
  129. package/dist/server/workers/review.js +15 -4
  130. package/dist/server/workers/review.js.map +1 -1
  131. package/package.json +1 -1
  132. package/dist/client/assets/GitHubActionsDialog-D7iT7xTt.js +0 -1
  133. package/dist/client/assets/VerificationDialog-ayyiyvGt.js +0 -4
  134. package/dist/client/assets/channel-CGc3oFdr.js +0 -1
  135. package/dist/client/assets/classDiagram-4FO5ZUOK-CBhybRSO.js +0 -1
  136. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C9ErMU7v.js +0 -1
  137. package/dist/client/assets/index-Ci2Or91O.js +0 -275
  138. package/dist/client/assets/index-CsZfTdMO.css +0 -2
  139. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BDLRoPbQ.js +0 -1
package/dist/server/db.js CHANGED
@@ -3,6 +3,7 @@ import { createHash } from 'crypto';
3
3
  import { parseCheckDefinitionsJsonStrict, parseLegacyTestCommandsJsonStrict, projectCheckDefinitionsToTestCommands, } from './lib/checkDefinitions.js';
4
4
  import { ensureParentDirectory, resolveArchonDbPath } from './lib/runtimePaths.js';
5
5
  import { TRANSCRIPT_RUN_TEXT_LIMIT_BYTES, truncateUtf8 } from './lib/transcriptRuns.js';
6
+ import { createExecutionProfile, fingerprintExecutionProfile, serializeExecutionProfile, } from './lib/executionProfiles.js';
6
7
  const dbPath = ensureParentDirectory(resolveArchonDbPath());
7
8
  const db = new Database(dbPath);
8
9
  db.pragma('foreign_keys = ON');
@@ -55,6 +56,7 @@ db.exec(`
55
56
  name TEXT NOT NULL,
56
57
  repo_path TEXT NOT NULL,
57
58
  build_execution_provider TEXT NOT NULL DEFAULT 'host' CHECK (build_execution_provider IN ('host', 'docker_sandbox')),
59
+ active_execution_profile_revision_id TEXT NOT NULL DEFAULT '',
58
60
  active_target_kind TEXT NOT NULL DEFAULT 'repo_root',
59
61
  active_target_bundle_id TEXT,
60
62
  worktree_cmd TEXT,
@@ -148,6 +150,7 @@ db.exec('UPDATE worktree_bundles SET required_check_policy_override = NULL WHERE
148
150
  ensureColumn('projects', 'worktree_sync', 'TEXT');
149
151
  ensureColumn('projects', 'build_execution_provider', "TEXT NOT NULL DEFAULT 'host'");
150
152
  db.exec("UPDATE projects SET build_execution_provider = 'host' WHERE build_execution_provider IS NULL OR build_execution_provider NOT IN ('host', 'docker_sandbox')");
153
+ ensureColumn('projects', 'active_execution_profile_revision_id', "TEXT NOT NULL DEFAULT ''");
151
154
  ensureColumn('projects', 'active_target_kind', "TEXT NOT NULL DEFAULT 'repo_root'");
152
155
  ensureColumn('projects', 'active_target_bundle_id', 'TEXT');
153
156
  ensureColumn('projects', 'check_definitions', 'TEXT');
@@ -173,6 +176,175 @@ ensureColumn('projects', 'spec_aware_code_review_enabled', 'INTEGER NOT NULL DEF
173
176
  ensureColumn('projects', 'deletion_started_at', 'DATETIME');
174
177
  ensureColumn('projects', 'deletion_claim_id', 'TEXT');
175
178
  ensureColumn('projects', 'deletion_cleanup_started_at', 'DATETIME');
179
+ db.exec(`
180
+ CREATE TABLE IF NOT EXISTS execution_profile_revisions (
181
+ id TEXT PRIMARY KEY,
182
+ project_id TEXT NOT NULL,
183
+ revision INTEGER NOT NULL CHECK (revision > 0),
184
+ schema_version INTEGER NOT NULL CHECK (schema_version = 1),
185
+ fingerprint TEXT NOT NULL CHECK (length(fingerprint) = 64),
186
+ profile_json TEXT NOT NULL CHECK (
187
+ json_valid(profile_json)
188
+ AND json_type(profile_json) = 'object'
189
+ AND length(CAST(profile_json AS BLOB)) BETWEEN 2 AND 16384
190
+ ),
191
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
192
+ UNIQUE (project_id, revision),
193
+ UNIQUE (project_id, fingerprint),
194
+ UNIQUE (project_id, id),
195
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
196
+ );
197
+
198
+ CREATE TABLE IF NOT EXISTS execution_profile_audit_events (
199
+ id TEXT PRIMARY KEY,
200
+ project_id TEXT NOT NULL,
201
+ event_type TEXT NOT NULL CHECK (event_type IN ('revision_created', 'revision_activated')),
202
+ profile_revision_id TEXT NOT NULL,
203
+ previous_profile_revision_id TEXT,
204
+ metadata_json TEXT CHECK (
205
+ metadata_json IS NULL OR (
206
+ json_valid(metadata_json)
207
+ AND json_type(metadata_json) = 'object'
208
+ AND length(CAST(metadata_json AS BLOB)) BETWEEN 2 AND 16384
209
+ )
210
+ ),
211
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
212
+ FOREIGN KEY (project_id, profile_revision_id)
213
+ REFERENCES execution_profile_revisions(project_id, id) ON DELETE CASCADE,
214
+ FOREIGN KEY (project_id, previous_profile_revision_id)
215
+ REFERENCES execution_profile_revisions(project_id, id) ON DELETE CASCADE
216
+ );
217
+
218
+ CREATE INDEX IF NOT EXISTS idx_execution_profile_revisions_project
219
+ ON execution_profile_revisions(project_id, revision DESC);
220
+ CREATE INDEX IF NOT EXISTS idx_execution_profile_audit_project
221
+ ON execution_profile_audit_events(project_id, created_at DESC, id DESC);
222
+ `);
223
+ const backfillExecutionProfiles = () => {
224
+ const projects = db.prepare(`
225
+ SELECT id, build_execution_provider, active_execution_profile_revision_id
226
+ FROM projects
227
+ `).all();
228
+ const findOwnedRevision = db.prepare(`
229
+ SELECT 1 FROM execution_profile_revisions WHERE project_id = ? AND id = ?
230
+ `);
231
+ const insertRevision = db.prepare(`
232
+ INSERT OR IGNORE INTO execution_profile_revisions (
233
+ id, project_id, revision, schema_version, fingerprint, profile_json
234
+ ) VALUES (?, ?, 1, 1, ?, ?)
235
+ `);
236
+ const activateRevision = db.prepare(`
237
+ UPDATE projects SET active_execution_profile_revision_id = ? WHERE id = ?
238
+ `);
239
+ const insertEvent = db.prepare(`
240
+ INSERT OR IGNORE INTO execution_profile_audit_events (
241
+ id, project_id, event_type, profile_revision_id, previous_profile_revision_id
242
+ ) VALUES (?, ?, 'revision_created', ?, NULL)
243
+ `);
244
+ db.transaction(() => {
245
+ for (const project of projects) {
246
+ if (project.active_execution_profile_revision_id
247
+ && findOwnedRevision.get(project.id, project.active_execution_profile_revision_id)) {
248
+ continue;
249
+ }
250
+ const profile = createExecutionProfile(project.build_execution_provider);
251
+ const profileJson = serializeExecutionProfile(profile);
252
+ const fingerprint = fingerprintExecutionProfile(profile);
253
+ const revisionId = `${project.id}:execution-profile:1`;
254
+ insertRevision.run(revisionId, project.id, fingerprint, profileJson);
255
+ activateRevision.run(revisionId, project.id);
256
+ insertEvent.run(`${revisionId}:created`, project.id, revisionId);
257
+ }
258
+ })();
259
+ };
260
+ backfillExecutionProfiles();
261
+ const hostCompatibilityProfile = createExecutionProfile('host');
262
+ const hostCompatibilityProfileJson = serializeExecutionProfile(hostCompatibilityProfile).replaceAll("'", "''");
263
+ const hostCompatibilityFingerprint = fingerprintExecutionProfile(hostCompatibilityProfile);
264
+ const dockerProfile = createExecutionProfile('docker_sandbox');
265
+ const dockerProfileJson = serializeExecutionProfile(dockerProfile).replaceAll("'", "''");
266
+ const dockerProfileFingerprint = fingerprintExecutionProfile(dockerProfile);
267
+ db.exec(`
268
+ CREATE TRIGGER IF NOT EXISTS create_project_execution_profile
269
+ AFTER INSERT ON projects
270
+ WHEN NEW.active_execution_profile_revision_id = ''
271
+ BEGIN
272
+ INSERT INTO execution_profile_revisions (
273
+ id, project_id, revision, schema_version, fingerprint, profile_json
274
+ ) VALUES (
275
+ NEW.id || ':execution-profile:1', NEW.id, 1, 1,
276
+ CASE WHEN NEW.build_execution_provider = 'docker_sandbox'
277
+ THEN '${dockerProfileFingerprint}' ELSE '${hostCompatibilityFingerprint}' END,
278
+ CASE WHEN NEW.build_execution_provider = 'docker_sandbox'
279
+ THEN '${dockerProfileJson}' ELSE '${hostCompatibilityProfileJson}' END
280
+ );
281
+ UPDATE projects
282
+ SET active_execution_profile_revision_id = NEW.id || ':execution-profile:1'
283
+ WHERE id = NEW.id;
284
+ INSERT INTO execution_profile_audit_events (
285
+ id, project_id, event_type, profile_revision_id, previous_profile_revision_id
286
+ ) VALUES (
287
+ NEW.id || ':execution-profile:1:created', NEW.id, 'revision_created',
288
+ NEW.id || ':execution-profile:1', NULL
289
+ );
290
+ END;
291
+
292
+ CREATE TRIGGER IF NOT EXISTS validate_project_execution_profile_owner_insert
293
+ BEFORE INSERT ON projects
294
+ WHEN NEW.active_execution_profile_revision_id <> '' AND NOT EXISTS (
295
+ SELECT 1 FROM execution_profile_revisions
296
+ WHERE project_id = NEW.id AND id = NEW.active_execution_profile_revision_id
297
+ )
298
+ BEGIN
299
+ SELECT RAISE(ABORT, 'Active execution profile revision is not owned by project');
300
+ END;
301
+
302
+ CREATE TRIGGER IF NOT EXISTS validate_project_execution_profile_owner_update
303
+ BEFORE UPDATE OF active_execution_profile_revision_id ON projects
304
+ WHEN NEW.active_execution_profile_revision_id = '' OR NOT EXISTS (
305
+ SELECT 1 FROM execution_profile_revisions
306
+ WHERE project_id = NEW.id AND id = NEW.active_execution_profile_revision_id
307
+ )
308
+ BEGIN
309
+ SELECT RAISE(ABORT, 'Active execution profile revision is not owned by project');
310
+ END;
311
+
312
+ CREATE TRIGGER IF NOT EXISTS validate_project_execution_profile_projection_insert
313
+ BEFORE INSERT ON projects
314
+ WHEN NEW.active_execution_profile_revision_id <> '' AND NOT EXISTS (
315
+ SELECT 1 FROM execution_profile_revisions
316
+ WHERE project_id = NEW.id
317
+ AND id = NEW.active_execution_profile_revision_id
318
+ AND json_extract(profile_json, '$.provider_preference') = NEW.build_execution_provider
319
+ )
320
+ BEGIN
321
+ SELECT RAISE(ABORT, 'Build execution provider must project the active execution profile');
322
+ END;
323
+
324
+ CREATE TRIGGER IF NOT EXISTS validate_project_execution_profile_projection_update
325
+ BEFORE UPDATE OF active_execution_profile_revision_id, build_execution_provider ON projects
326
+ WHEN NOT EXISTS (
327
+ SELECT 1 FROM execution_profile_revisions
328
+ WHERE project_id = NEW.id
329
+ AND id = NEW.active_execution_profile_revision_id
330
+ AND json_extract(profile_json, '$.provider_preference') = NEW.build_execution_provider
331
+ )
332
+ BEGIN
333
+ SELECT RAISE(ABORT, 'Build execution provider must project the active execution profile');
334
+ END;
335
+
336
+ CREATE TRIGGER IF NOT EXISTS preserve_execution_profile_revision
337
+ BEFORE UPDATE ON execution_profile_revisions
338
+ BEGIN
339
+ SELECT RAISE(ABORT, 'Execution profile revisions are immutable');
340
+ END;
341
+
342
+ CREATE TRIGGER IF NOT EXISTS preserve_execution_profile_audit_event
343
+ BEFORE UPDATE ON execution_profile_audit_events
344
+ BEGIN
345
+ SELECT RAISE(ABORT, 'Execution profile audit events are immutable');
346
+ END;
347
+ `);
176
348
  const backfillProjectCheckDefinitions = () => {
177
349
  const rows = db.prepare(`
178
350
  SELECT id, check_definitions, test_commands
@@ -946,11 +1118,115 @@ db.exec(`
946
1118
  entered_queue_at DATETIME DEFAULT CURRENT_TIMESTAMP,
947
1119
  started_at DATETIME,
948
1120
  finished_at DATETIME,
1121
+ execution_profile_revision_id TEXT,
1122
+ execution_profile_fingerprint TEXT,
1123
+ execution_profile_selection_reason TEXT,
1124
+ activation_execution_run_id TEXT,
949
1125
  PRIMARY KEY (build_sequence_id, ticket_id),
950
1126
  FOREIGN KEY (build_sequence_id) REFERENCES build_sequences(id) ON DELETE CASCADE,
951
1127
  FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE
952
1128
  )
953
1129
  `);
1130
+ db.exec(`
1131
+ CREATE TABLE IF NOT EXISTS build_sequence_ticket_replacements (
1132
+ id TEXT PRIMARY KEY,
1133
+ build_sequence_id TEXT NOT NULL,
1134
+ ticket_id TEXT NOT NULL,
1135
+ position INTEGER NOT NULL,
1136
+ execution_profile_revision_id TEXT NOT NULL,
1137
+ execution_profile_fingerprint TEXT NOT NULL,
1138
+ execution_profile_selection_reason TEXT NOT NULL,
1139
+ replaced_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1140
+ FOREIGN KEY (build_sequence_id) REFERENCES build_sequences(id) ON DELETE CASCADE,
1141
+ FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE
1142
+ )
1143
+ `);
1144
+ ensureColumn('build_sequence_tickets', 'execution_profile_revision_id', 'TEXT');
1145
+ ensureColumn('build_sequence_tickets', 'execution_profile_fingerprint', 'TEXT');
1146
+ ensureColumn('build_sequence_tickets', 'execution_profile_selection_reason', 'TEXT');
1147
+ ensureColumn('build_sequence_tickets', 'activation_execution_run_id', 'TEXT');
1148
+ db.transaction(() => {
1149
+ const incompleteSequenceIds = db.prepare(`
1150
+ SELECT DISTINCT build_sequence_id
1151
+ FROM build_sequence_tickets
1152
+ WHERE status IN ('active', 'queued')
1153
+ AND (
1154
+ execution_profile_revision_id IS NULL
1155
+ OR execution_profile_fingerprint IS NULL
1156
+ OR execution_profile_selection_reason IS NULL
1157
+ )
1158
+ `).all();
1159
+ const failActive = db.prepare(`
1160
+ UPDATE build_sequence_tickets
1161
+ SET status = 'failed', finished_at = COALESCE(finished_at, CURRENT_TIMESTAMP)
1162
+ WHERE build_sequence_id = ? AND status = 'active'
1163
+ `);
1164
+ const cancelQueued = db.prepare(`
1165
+ UPDATE build_sequence_tickets
1166
+ SET status = 'cancelled', finished_at = COALESCE(finished_at, CURRENT_TIMESTAMP)
1167
+ WHERE build_sequence_id = ? AND status = 'queued'
1168
+ `);
1169
+ const failSequence = db.prepare(`
1170
+ UPDATE build_sequences SET status = 'failed', updated_at = CURRENT_TIMESTAMP
1171
+ WHERE id = ? AND status = 'active'
1172
+ `);
1173
+ for (const row of incompleteSequenceIds) {
1174
+ failActive.run(row.build_sequence_id);
1175
+ cancelQueued.run(row.build_sequence_id);
1176
+ failSequence.run(row.build_sequence_id);
1177
+ }
1178
+ })();
1179
+ db.exec(`
1180
+ CREATE TRIGGER IF NOT EXISTS validate_build_sequence_ticket_profile_insert
1181
+ BEFORE INSERT ON build_sequence_tickets
1182
+ WHEN NEW.status IN ('active', 'queued')
1183
+ OR NEW.execution_profile_revision_id IS NOT NULL OR NEW.execution_profile_fingerprint IS NOT NULL
1184
+ OR NEW.execution_profile_selection_reason IS NOT NULL
1185
+ BEGIN
1186
+ SELECT CASE WHEN
1187
+ NEW.execution_profile_revision_id IS NULL
1188
+ OR NEW.execution_profile_fingerprint IS NULL
1189
+ OR NEW.execution_profile_selection_reason NOT IN ('project_active_at_acceptance', 'historical_lineage')
1190
+ OR NOT EXISTS (
1191
+ SELECT 1
1192
+ FROM execution_profile_revisions revision
1193
+ JOIN build_sequences sequence ON sequence.id = NEW.build_sequence_id
1194
+ WHERE revision.id = NEW.execution_profile_revision_id
1195
+ AND revision.project_id = sequence.project_id
1196
+ AND revision.fingerprint = NEW.execution_profile_fingerprint
1197
+ )
1198
+ THEN RAISE(ABORT, 'Invalid Build sequence execution profile selection') END;
1199
+ END;
1200
+
1201
+ CREATE TRIGGER IF NOT EXISTS validate_build_sequence_ticket_profile_activation
1202
+ BEFORE UPDATE OF status ON build_sequence_tickets
1203
+ WHEN NEW.status IN ('active', 'queued') AND (
1204
+ NEW.execution_profile_revision_id IS NULL
1205
+ OR NEW.execution_profile_fingerprint IS NULL
1206
+ OR NEW.execution_profile_selection_reason NOT IN ('project_active_at_acceptance', 'historical_lineage')
1207
+ OR NOT EXISTS (
1208
+ SELECT 1
1209
+ FROM execution_profile_revisions revision
1210
+ JOIN build_sequences sequence ON sequence.id = NEW.build_sequence_id
1211
+ WHERE revision.id = NEW.execution_profile_revision_id
1212
+ AND revision.project_id = sequence.project_id
1213
+ AND revision.fingerprint = NEW.execution_profile_fingerprint
1214
+ )
1215
+ )
1216
+ BEGIN
1217
+ SELECT RAISE(ABORT, 'Invalid Build sequence execution profile selection');
1218
+ END;
1219
+
1220
+ CREATE TRIGGER IF NOT EXISTS preserve_build_sequence_ticket_profile
1221
+ BEFORE UPDATE OF execution_profile_revision_id, execution_profile_fingerprint, execution_profile_selection_reason
1222
+ ON build_sequence_tickets
1223
+ WHEN NEW.execution_profile_revision_id IS NOT OLD.execution_profile_revision_id
1224
+ OR NEW.execution_profile_fingerprint IS NOT OLD.execution_profile_fingerprint
1225
+ OR NEW.execution_profile_selection_reason IS NOT OLD.execution_profile_selection_reason
1226
+ BEGIN
1227
+ SELECT RAISE(ABORT, 'Build sequence execution profile selection is immutable');
1228
+ END;
1229
+ `);
954
1230
  db.exec(`
955
1231
  CREATE TABLE IF NOT EXISTS ticket_dependencies (
956
1232
  blocker_ticket_id TEXT NOT NULL,
@@ -2132,6 +2408,9 @@ db.exec(`
2132
2408
  environment_revision INTEGER NOT NULL,
2133
2409
  workspace_mode TEXT NOT NULL,
2134
2410
  environment_validation_fingerprint TEXT NOT NULL,
2411
+ execution_profile_revision_id TEXT,
2412
+ execution_profile_fingerprint TEXT,
2413
+ credential_requirements_fingerprint TEXT,
2135
2414
  template_identity TEXT,
2136
2415
  template_digest TEXT,
2137
2416
  agent_identity TEXT,
@@ -2151,6 +2430,7 @@ db.exec(`
2151
2430
  last_used_at DATETIME,
2152
2431
  stopped_at DATETIME,
2153
2432
  retirement_started_at DATETIME,
2433
+ retirement_requested_at DATETIME,
2154
2434
  cleanup_pending_at DATETIME,
2155
2435
  retired_at DATETIME,
2156
2436
  failed_at DATETIME,
@@ -2169,6 +2449,10 @@ db.exec(`
2169
2449
  CHECK (environment_revision >= 1),
2170
2450
  CHECK (workspace_mode = 'direct'),
2171
2451
  CHECK (length(environment_validation_fingerprint) = 64 AND environment_validation_fingerprint NOT GLOB '*[^0-9a-f]*'),
2452
+ CHECK ((execution_profile_revision_id IS NULL) = (execution_profile_fingerprint IS NULL)),
2453
+ CHECK ((execution_profile_fingerprint IS NULL) = (credential_requirements_fingerprint IS NULL)),
2454
+ CHECK (execution_profile_fingerprint IS NULL OR (length(execution_profile_fingerprint) = 64 AND execution_profile_fingerprint NOT GLOB '*[^0-9a-f]*')),
2455
+ CHECK (credential_requirements_fingerprint IS NULL OR (length(credential_requirements_fingerprint) = 64 AND credential_requirements_fingerprint NOT GLOB '*[^0-9a-f]*')),
2172
2456
  CHECK ((template_identity IS NULL) <> (template_digest IS NULL)),
2173
2457
  CHECK (template_identity IS NULL OR length(CAST(template_identity AS BLOB)) BETWEEN 1 AND 500),
2174
2458
  CHECK (template_digest IS NULL OR length(CAST(template_digest AS BLOB)) BETWEEN 1 AND 500),
@@ -2214,6 +2498,7 @@ db.exec(`
2214
2498
  relationship TEXT NOT NULL,
2215
2499
  environment_revision INTEGER NOT NULL,
2216
2500
  environment_validation_fingerprint TEXT NOT NULL,
2501
+ execution_profile_fingerprint TEXT,
2217
2502
  authority_json TEXT,
2218
2503
  placement_json TEXT,
2219
2504
  bound_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -2224,6 +2509,7 @@ db.exec(`
2224
2509
  CHECK (relationship IN ('validation', 'execution')),
2225
2510
  CHECK (environment_revision >= 1),
2226
2511
  CHECK (length(environment_validation_fingerprint) = 64 AND environment_validation_fingerprint NOT GLOB '*[^0-9a-f]*'),
2512
+ CHECK (execution_profile_fingerprint IS NULL OR (length(execution_profile_fingerprint) = 64 AND execution_profile_fingerprint NOT GLOB '*[^0-9a-f]*')),
2227
2513
  CHECK (relationship <> 'execution' OR length(CAST(authority_json AS BLOB)) BETWEEN 2 AND 16384),
2228
2514
  CHECK (placement_json IS NULL OR (relationship = 'execution' AND length(CAST(placement_json AS BLOB)) BETWEEN 2 AND 16384))
2229
2515
  );
@@ -2298,8 +2584,146 @@ db.exec(`
2298
2584
  SELECT RAISE(ABORT, 'Execution environment run binding identity is immutable');
2299
2585
  END;
2300
2586
  `);
2587
+ ensureColumn('execution_environments', 'execution_profile_revision_id', 'TEXT');
2588
+ ensureColumn('execution_environments', 'execution_profile_fingerprint', 'TEXT');
2589
+ ensureColumn('execution_environments', 'credential_requirements_fingerprint', 'TEXT');
2590
+ ensureColumn('execution_environments', 'retirement_requested_at', 'DATETIME');
2591
+ ensureColumn('execution_environment_run_bindings', 'execution_profile_fingerprint', 'TEXT');
2301
2592
  ensureColumn('execution_environment_run_bindings', 'authority_json', 'TEXT');
2302
2593
  ensureColumn('execution_environment_run_bindings', 'placement_json', 'TEXT');
2594
+ db.exec(`
2595
+ CREATE TABLE IF NOT EXISTS execution_run_authorities (
2596
+ execution_run_id TEXT NOT NULL,
2597
+ project_id TEXT NOT NULL,
2598
+ execution_profile_revision_id TEXT NOT NULL,
2599
+ execution_profile_fingerprint TEXT NOT NULL,
2600
+ authority_json TEXT NOT NULL,
2601
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2602
+ PRIMARY KEY (execution_run_id),
2603
+ FOREIGN KEY (execution_run_id, project_id) REFERENCES execution_runs(id, project_id) ON DELETE CASCADE,
2604
+ FOREIGN KEY (project_id, execution_profile_revision_id)
2605
+ REFERENCES execution_profile_revisions(project_id, id) ON DELETE RESTRICT,
2606
+ CHECK (length(execution_profile_fingerprint) = 64 AND execution_profile_fingerprint NOT GLOB '*[^0-9a-f]*'),
2607
+ CHECK (length(CAST(authority_json AS BLOB)) BETWEEN 2 AND 16384)
2608
+ );
2609
+
2610
+ CREATE TRIGGER IF NOT EXISTS preserve_execution_run_authority
2611
+ BEFORE UPDATE ON execution_run_authorities
2612
+ BEGIN
2613
+ SELECT RAISE(ABORT, 'Execution run authority is immutable');
2614
+ END;
2615
+
2616
+ `);
2617
+ db.exec(`
2618
+ CREATE TABLE IF NOT EXISTS build_setup_evidence (
2619
+ id TEXT PRIMARY KEY,
2620
+ project_id TEXT NOT NULL,
2621
+ execution_run_id TEXT NOT NULL,
2622
+ target_kind TEXT NOT NULL,
2623
+ target_key TEXT NOT NULL,
2624
+ target_workspace_path TEXT NOT NULL,
2625
+ workspace_evidence_state TEXT NOT NULL,
2626
+ workspace_evidence_fingerprint TEXT,
2627
+ setup_configuration_fingerprint TEXT NOT NULL,
2628
+ execution_profile_fingerprint TEXT NOT NULL,
2629
+ provider TEXT NOT NULL,
2630
+ environment_revision INTEGER,
2631
+ command_count INTEGER NOT NULL,
2632
+ outcome TEXT NOT NULL,
2633
+ reused_from_evidence_id TEXT,
2634
+ started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2635
+ completed_at DATETIME,
2636
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
2637
+ FOREIGN KEY (execution_run_id, project_id) REFERENCES execution_runs(id, project_id) ON DELETE CASCADE,
2638
+ FOREIGN KEY (reused_from_evidence_id) REFERENCES build_setup_evidence(id) ON DELETE CASCADE,
2639
+ CHECK (length(id) BETWEEN 1 AND 128),
2640
+ CHECK (length(target_kind) BETWEEN 1 AND 100),
2641
+ CHECK (length(CAST(target_key AS BLOB)) BETWEEN 1 AND 1000),
2642
+ CHECK (length(CAST(target_workspace_path AS BLOB)) BETWEEN 1 AND 4096),
2643
+ CHECK (workspace_evidence_state IN ('available', 'unavailable')),
2644
+ CHECK ((workspace_evidence_state = 'available') = (workspace_evidence_fingerprint IS NOT NULL)),
2645
+ CHECK (workspace_evidence_fingerprint IS NULL OR (length(workspace_evidence_fingerprint) = 64 AND workspace_evidence_fingerprint NOT GLOB '*[^0-9a-f]*')),
2646
+ CHECK (length(setup_configuration_fingerprint) = 64 AND setup_configuration_fingerprint NOT GLOB '*[^0-9a-f]*'),
2647
+ CHECK (length(execution_profile_fingerprint) = 64 AND execution_profile_fingerprint NOT GLOB '*[^0-9a-f]*'),
2648
+ CHECK (provider IN ('host', 'docker_sandbox')),
2649
+ CHECK ((provider = 'host') = (environment_revision IS NULL)),
2650
+ CHECK (environment_revision IS NULL OR environment_revision >= 1),
2651
+ CHECK (command_count >= 0),
2652
+ CHECK (outcome IN ('running', 'completed', 'noop', 'reused', 'failed')),
2653
+ CHECK ((outcome = 'reused') = (reused_from_evidence_id IS NOT NULL)),
2654
+ CHECK ((outcome = 'running') = (completed_at IS NULL))
2655
+ );
2656
+
2657
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_build_setup_evidence_active_target
2658
+ ON build_setup_evidence(project_id, target_key)
2659
+ WHERE outcome = 'running';
2660
+
2661
+ CREATE INDEX IF NOT EXISTS idx_build_setup_evidence_reuse
2662
+ ON build_setup_evidence(
2663
+ project_id, target_key, workspace_evidence_fingerprint,
2664
+ setup_configuration_fingerprint, execution_profile_fingerprint,
2665
+ provider, environment_revision, completed_at DESC
2666
+ )
2667
+ WHERE outcome IN ('completed', 'noop', 'reused');
2668
+
2669
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_build_setup_evidence_execution_run
2670
+ ON build_setup_evidence(execution_run_id);
2671
+
2672
+ CREATE TRIGGER IF NOT EXISTS preserve_build_setup_evidence_identity
2673
+ BEFORE UPDATE OF id, project_id, execution_run_id, target_kind, target_key,
2674
+ target_workspace_path, workspace_evidence_state, workspace_evidence_fingerprint,
2675
+ setup_configuration_fingerprint, execution_profile_fingerprint, provider,
2676
+ environment_revision, command_count, reused_from_evidence_id, started_at
2677
+ ON build_setup_evidence
2678
+ BEGIN
2679
+ SELECT RAISE(ABORT, 'Build setup evidence identity is immutable');
2680
+ END;
2681
+ `);
2682
+ db.exec('DROP TRIGGER IF EXISTS preserve_execution_environment_identity');
2683
+ db.exec('DROP TRIGGER IF EXISTS validate_execution_environment_binding_identity');
2684
+ db.exec('DROP TRIGGER IF EXISTS preserve_execution_environment_binding_identity');
2685
+ db.exec(`
2686
+ CREATE TRIGGER preserve_execution_environment_identity
2687
+ BEFORE UPDATE OF id, project_id, provider_kind, provider_version, target_kind, target_key,
2688
+ target_workspace_path, environment_revision, workspace_mode,
2689
+ environment_validation_fingerprint, execution_profile_revision_id, execution_profile_fingerprint,
2690
+ credential_requirements_fingerprint, template_identity, template_digest,
2691
+ agent_identity, compatible_agents_json, created_at ON execution_environments
2692
+ WHEN NEW.id <> OLD.id OR NEW.project_id <> OLD.project_id OR NEW.provider_kind <> OLD.provider_kind
2693
+ OR NEW.provider_version <> OLD.provider_version OR NEW.target_kind <> OLD.target_kind
2694
+ OR NEW.target_key <> OLD.target_key OR NEW.target_workspace_path <> OLD.target_workspace_path
2695
+ OR NEW.environment_revision <> OLD.environment_revision OR NEW.workspace_mode <> OLD.workspace_mode
2696
+ OR NEW.environment_validation_fingerprint <> OLD.environment_validation_fingerprint
2697
+ OR NEW.execution_profile_revision_id IS NOT OLD.execution_profile_revision_id
2698
+ OR NEW.execution_profile_fingerprint IS NOT OLD.execution_profile_fingerprint
2699
+ OR NEW.credential_requirements_fingerprint IS NOT OLD.credential_requirements_fingerprint
2700
+ OR NEW.template_identity IS NOT OLD.template_identity OR NEW.template_digest IS NOT OLD.template_digest
2701
+ OR NEW.agent_identity IS NOT OLD.agent_identity OR NEW.compatible_agents_json <> OLD.compatible_agents_json
2702
+ OR NEW.created_at <> OLD.created_at
2703
+ BEGIN SELECT RAISE(ABORT, 'Execution environment identity is immutable'); END;
2704
+
2705
+ CREATE TRIGGER validate_execution_environment_binding_identity
2706
+ BEFORE INSERT ON execution_environment_run_bindings
2707
+ WHEN NOT EXISTS (
2708
+ SELECT 1 FROM execution_environments WHERE id = NEW.environment_id AND project_id = NEW.project_id
2709
+ AND environment_revision = NEW.environment_revision
2710
+ AND environment_validation_fingerprint = NEW.environment_validation_fingerprint
2711
+ AND execution_profile_fingerprint IS NEW.execution_profile_fingerprint
2712
+ )
2713
+ BEGIN SELECT RAISE(ABORT, 'Execution environment binding identity does not match'); END;
2714
+
2715
+ CREATE TRIGGER preserve_execution_environment_binding_identity
2716
+ BEFORE UPDATE OF environment_id, execution_run_id, project_id, relationship,
2717
+ environment_revision, environment_validation_fingerprint, execution_profile_fingerprint, bound_at
2718
+ ON execution_environment_run_bindings
2719
+ WHEN NEW.environment_id <> OLD.environment_id OR NEW.execution_run_id <> OLD.execution_run_id
2720
+ OR NEW.project_id <> OLD.project_id OR NEW.relationship <> OLD.relationship
2721
+ OR NEW.environment_revision <> OLD.environment_revision
2722
+ OR NEW.environment_validation_fingerprint <> OLD.environment_validation_fingerprint
2723
+ OR NEW.execution_profile_fingerprint IS NOT OLD.execution_profile_fingerprint
2724
+ OR NEW.bound_at <> OLD.bound_at
2725
+ BEGIN SELECT RAISE(ABORT, 'Execution environment run binding identity is immutable'); END;
2726
+ `);
2303
2727
  db.exec('DROP TRIGGER IF EXISTS validate_execution_environment_binding_authority');
2304
2728
  db.exec(`
2305
2729
  CREATE TRIGGER IF NOT EXISTS validate_project_build_execution_provider_insert
@@ -2344,7 +2768,8 @@ db.exec(`
2344
2768
  SELECT RAISE(ABORT, 'Execution environment binding evidence is immutable');
2345
2769
  END;
2346
2770
  `);
2347
- db.exec('CREATE INDEX IF NOT EXISTS idx_execution_environments_reuse ON execution_environments(project_id, provider_kind, target_key, target_workspace_path, environment_revision, workspace_mode, environment_validation_fingerprint, lifecycle_state)');
2771
+ db.exec('DROP INDEX IF EXISTS idx_execution_environments_reuse');
2772
+ db.exec('CREATE INDEX idx_execution_environments_reuse ON execution_environments(project_id, provider_kind, target_key, target_workspace_path, environment_revision, workspace_mode, environment_validation_fingerprint, execution_profile_fingerprint, credential_requirements_fingerprint, retirement_requested_at, lifecycle_state)');
2348
2773
  db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_execution_environments_external_id ON execution_environments(provider_kind, external_id) WHERE external_id IS NOT NULL');
2349
2774
  db.exec('CREATE INDEX IF NOT EXISTS idx_execution_environments_cleanup ON execution_environments(lifecycle_state, cleanup_claim_id, updated_at, cleanup_attempts)');
2350
2775
  db.exec('CREATE INDEX IF NOT EXISTS idx_execution_environment_events_sequence ON execution_environment_events(project_id, environment_id, sequence)');
@@ -4327,6 +4752,11 @@ db.exec('CREATE INDEX IF NOT EXISTS idx_project_links_linked_project_id ON proje
4327
4752
  db.exec('CREATE INDEX IF NOT EXISTS idx_build_sequences_project_status ON build_sequences(project_id, status)');
4328
4753
  db.exec('CREATE INDEX IF NOT EXISTS idx_build_sequences_bundle_status ON build_sequences(worktree_bundle_id, status)');
4329
4754
  db.exec('CREATE INDEX IF NOT EXISTS idx_build_sequence_tickets_sequence_position ON build_sequence_tickets(build_sequence_id, position)');
4755
+ db.exec(`
4756
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_build_sequence_tickets_activation_run
4757
+ ON build_sequence_tickets(activation_execution_run_id)
4758
+ WHERE activation_execution_run_id IS NOT NULL
4759
+ `);
4330
4760
  db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_integration_connections_provider_project ON integration_connections(provider, project_id)');
4331
4761
  db.exec('CREATE INDEX IF NOT EXISTS idx_tickets_external_key ON tickets(external_provider, external_key)');
4332
4762
  db.exec('CREATE INDEX IF NOT EXISTS idx_bundle_automation_state_project_bundle ON bundle_automation_state(project_id, worktree_bundle_id)');