@straiffi/archon 1.8.20 → 1.8.22

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 (170) hide show
  1. package/dist/client/assets/{ActivityView-DDE3U1zB.js → ActivityView-CdEPjmqD.js} +1 -1
  2. package/dist/client/assets/GitHubActionsDialog-CA189pr2.js +1 -0
  3. package/dist/client/assets/VerificationDialog-BPVkhKRj.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-oPUmRpZr.js → architectureDiagram-3BPJPVTR-BrZ08mqq.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-0sq1jkkp.js → blockDiagram-GPEHLZMM-b93XwJ6Z.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-BeDxMy7J.js → c4Diagram-AAUBKEIU-BKOdbZ_v.js} +1 -1
  7. package/dist/client/assets/channel-D0zqzBT_.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-D8VGsje2.js → chunk-2J33WTMH-D7Vxn_1m.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-DuTUTeBe.js → chunk-3OPIFGDE-B1zo2UZq.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-BzX5dPNW.js → chunk-5ZQYHXKU-BajvkDdQ.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-DS9putxN.js → chunk-727SXJPM-CSH7cVpV.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-CWzFZRBv.js → chunk-AQP2D5EJ-DE-wgUNP.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-DXZ-ADYW.js → chunk-CSCIHK7Q-Ba7JgCEf.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-BD7mL3KK.js → chunk-KSCS5N6A-0o3YWkHM.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-DmYQi9AP.js → chunk-L5ZTLDWV-DihWczP8.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-CZyFYHvS.js → chunk-LZXEDZCA-Bxnu-hme.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-BxdjvT9I.js → chunk-ND2GUHAM-CoaR5kmP.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-BwuZcZ_9.js → chunk-NZK2D7GU-By92Rd3d.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-CC-TZXr3.js → chunk-O5CBEL6O-D83Pkgfu.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-CJZJkca2.js → chunk-WU5MYG2G-v8OVf5Lz.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-ChhyvHH2.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Cl02kurK.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-D2L5RvPF.js → dagre-BM42HDAG-Dq1ALF7Z.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-DBJzxSqZ.js → diagram-2AECGRRQ-1t1_TKtu.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-J_1M-Wou.js → diagram-5GNKFQAL-DtvYRL6f.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-CmT1Vbmc.js → diagram-KO2AKTUF-PRNrSX5F.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-DfPoJyQI.js → diagram-LMA3HP47-DaTX0DhT.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-D4uEb6vo.js → diagram-OG6HWLK6-C2p9p-92.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-l7yIFj0e.js → erDiagram-TEJ5UH35-DKzLtnp-.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-B3caTMes.js → flowDiagram-I6XJVG4X-GdL8lm6e.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-9Am36v9m.js → ganttDiagram-6RSMTGT7-Ok-4XYwT.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-B7OjYsv_.js → gitGraphDiagram-PVQCEYII-DSMrOCK7.js} +1 -1
  33. package/dist/client/assets/index-Bs8KsdV9.js +276 -0
  34. package/dist/client/assets/index-DdwM8Wgt.css +2 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-ZAcNejkW.js → infoDiagram-5YYISTIA-Dl4hRXis.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BaaJwuN0.js → ishikawaDiagram-YF4QCWOH-C1Dc1fTT.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-DjzTU3kM.js → journeyDiagram-JHISSGLW-CUscmJze.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-DYRchzub.js → kanban-definition-UN3LZRKU-C0nwYjQV.js} +1 -1
  39. package/dist/client/assets/{line-X0j9gj61.js → line-ByiuEIg_.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-DCKG87Vl.js → mermaid-parser.core-B-XdAyDR.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-BxI5T2gs.js → mermaid.core-D_bIfuZD.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BE7MZzQO.js → mindmap-definition-RKZ34NQL-BGNiPJDK.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-D3fTYovg.js → pieDiagram-4H26LBE5-CP6FpAoO.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-B35nXRdQ.js → quadrantDiagram-W4KKPZXB-dsAWBAQn.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-CLBcHvP0.js → requirementDiagram-4Y6WPE33-DJH-zHqF.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DdBCxGHF.js → sankeyDiagram-5OEKKPKP-BJMnZrqH.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-KcLt0X1K.js → sequenceDiagram-3UESZ5HK-CzNjUSlM.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-Bq23ObLK.js → stateDiagram-AJRCARHV-jppMhOwh.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CLR1WgdV.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-SK3oVUD9.js → timeline-definition-PNZ67QCA-BX2CI5H_.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-COVtAlEC.js → vennDiagram-CIIHVFJN-Bd1KZaXh.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BvfOOwga.js → wardleyDiagram-YWT4CUSO-D-0BNz49.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DFnAD9k4.js → xychartDiagram-2RQKCTM6-DbYa0ZWK.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +780 -16
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +198 -20
  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/approvalReconciliation.js +101 -0
  64. package/dist/server/lib/approvalReconciliation.js.map +1 -0
  65. package/dist/server/lib/approvalRequests.js +587 -0
  66. package/dist/server/lib/approvalRequests.js.map +1 -0
  67. package/dist/server/lib/approvalRoutes.js +143 -0
  68. package/dist/server/lib/approvalRoutes.js.map +1 -0
  69. package/dist/server/lib/automationProgression.js +44 -14
  70. package/dist/server/lib/automationProgression.js.map +1 -1
  71. package/dist/server/lib/buildCompletionContinuation.js +14 -3
  72. package/dist/server/lib/buildCompletionContinuation.js.map +1 -1
  73. package/dist/server/lib/buildExecutionEnvironment.js +350 -66
  74. package/dist/server/lib/buildExecutionEnvironment.js.map +1 -1
  75. package/dist/server/lib/buildOpenQuestionOperations.js +47 -3
  76. package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -1
  77. package/dist/server/lib/buildSequences.js +267 -18
  78. package/dist/server/lib/buildSequences.js.map +1 -1
  79. package/dist/server/lib/buildSetupAuthority.js +138 -0
  80. package/dist/server/lib/buildSetupAuthority.js.map +1 -0
  81. package/dist/server/lib/bundleReviewOperations.js +15 -4
  82. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  83. package/dist/server/lib/checkDefinitions.js +29 -1
  84. package/dist/server/lib/checkDefinitions.js.map +1 -1
  85. package/dist/server/lib/checkRunner.js +306 -27
  86. package/dist/server/lib/checkRunner.js.map +1 -1
  87. package/dist/server/lib/checkRuns.js +4 -2
  88. package/dist/server/lib/checkRuns.js.map +1 -1
  89. package/dist/server/lib/consequentialActionAuthority.js +142 -0
  90. package/dist/server/lib/consequentialActionAuthority.js.map +1 -0
  91. package/dist/server/lib/consequentialActions.js +231 -0
  92. package/dist/server/lib/consequentialActions.js.map +1 -0
  93. package/dist/server/lib/dockerSandboxCredentials.js +64 -10
  94. package/dist/server/lib/dockerSandboxCredentials.js.map +1 -1
  95. package/dist/server/lib/dockerSandboxEnvironmentOperations.js +119 -0
  96. package/dist/server/lib/dockerSandboxEnvironmentOperations.js.map +1 -1
  97. package/dist/server/lib/dockerSandboxExecutionEnvironment.js +174 -11
  98. package/dist/server/lib/dockerSandboxExecutionEnvironment.js.map +1 -1
  99. package/dist/server/lib/dockerSandboxExecutionProvider.js +12 -3
  100. package/dist/server/lib/dockerSandboxExecutionProvider.js.map +1 -1
  101. package/dist/server/lib/dockerSandboxNetworkPolicy.js +222 -0
  102. package/dist/server/lib/dockerSandboxNetworkPolicy.js.map +1 -0
  103. package/dist/server/lib/executionCredentialCapabilities.js +106 -0
  104. package/dist/server/lib/executionCredentialCapabilities.js.map +1 -0
  105. package/dist/server/lib/executionEnvironment.js.map +1 -1
  106. package/dist/server/lib/executionEnvironmentContexts.js +59 -3
  107. package/dist/server/lib/executionEnvironmentContexts.js.map +1 -1
  108. package/dist/server/lib/executionEnvironmentLifecycle.js +34 -0
  109. package/dist/server/lib/executionEnvironmentLifecycle.js.map +1 -1
  110. package/dist/server/lib/executionEnvironmentRoutes.js +127 -9
  111. package/dist/server/lib/executionEnvironmentRoutes.js.map +1 -1
  112. package/dist/server/lib/executionEnvironmentValidation.js +169 -45
  113. package/dist/server/lib/executionEnvironmentValidation.js.map +1 -1
  114. package/dist/server/lib/executionEnvironmentValidationOperations.js +203 -19
  115. package/dist/server/lib/executionEnvironmentValidationOperations.js.map +1 -1
  116. package/dist/server/lib/executionEnvironments.js +103 -14
  117. package/dist/server/lib/executionEnvironments.js.map +1 -1
  118. package/dist/server/lib/executionProfileRoutes.js +67 -0
  119. package/dist/server/lib/executionProfileRoutes.js.map +1 -0
  120. package/dist/server/lib/executionProfileStore.js +117 -0
  121. package/dist/server/lib/executionProfileStore.js.map +1 -0
  122. package/dist/server/lib/executionProfiles.js +324 -0
  123. package/dist/server/lib/executionProfiles.js.map +1 -0
  124. package/dist/server/lib/executionReconciliation.js +345 -197
  125. package/dist/server/lib/executionReconciliation.js.map +1 -1
  126. package/dist/server/lib/executionRetry.js +24 -5
  127. package/dist/server/lib/executionRetry.js.map +1 -1
  128. package/dist/server/lib/executionRunAuthority.js +467 -0
  129. package/dist/server/lib/executionRunAuthority.js.map +1 -0
  130. package/dist/server/lib/executionRuns.js +30 -4
  131. package/dist/server/lib/executionRuns.js.map +1 -1
  132. package/dist/server/lib/executionTimeline.js +2 -1
  133. package/dist/server/lib/executionTimeline.js.map +1 -1
  134. package/dist/server/lib/git.js +77 -10
  135. package/dist/server/lib/git.js.map +1 -1
  136. package/dist/server/lib/gitPushApprovals.js +367 -0
  137. package/dist/server/lib/gitPushApprovals.js.map +1 -0
  138. package/dist/server/lib/hostExecutionEnvironment.js +7 -1
  139. package/dist/server/lib/hostExecutionEnvironment.js.map +1 -1
  140. package/dist/server/lib/mobileAccess.js +115 -1
  141. package/dist/server/lib/mobileAccess.js.map +1 -1
  142. package/dist/server/lib/productionCheckApprovals.js +106 -0
  143. package/dist/server/lib/productionCheckApprovals.js.map +1 -0
  144. package/dist/server/lib/projects.js +1 -0
  145. package/dist/server/lib/projects.js.map +1 -1
  146. package/dist/server/lib/run.js +58 -2
  147. package/dist/server/lib/run.js.map +1 -1
  148. package/dist/server/lib/ticketFollowUpOperations.js +32 -6
  149. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  150. package/dist/server/lib/transcriptRuns.js +3 -0
  151. package/dist/server/lib/transcriptRuns.js.map +1 -1
  152. package/dist/server/lib/types.js +40 -0
  153. package/dist/server/lib/types.js.map +1 -1
  154. package/dist/server/lib/workspaceProcessEnvironment.js +72 -1
  155. package/dist/server/lib/workspaceProcessEnvironment.js.map +1 -1
  156. package/dist/server/workers/build.js +214 -46
  157. package/dist/server/workers/build.js.map +1 -1
  158. package/dist/server/workers/followUp.js +74 -1
  159. package/dist/server/workers/followUp.js.map +1 -1
  160. package/dist/server/workers/review.js +30 -13
  161. package/dist/server/workers/review.js.map +1 -1
  162. package/package.json +1 -1
  163. package/dist/client/assets/GitHubActionsDialog-CM1z-amZ.js +0 -1
  164. package/dist/client/assets/VerificationDialog-oHjNllh6.js +0 -4
  165. package/dist/client/assets/channel-CF8hkOdu.js +0 -1
  166. package/dist/client/assets/classDiagram-4FO5ZUOK-D6FiG9_B.js +0 -1
  167. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Bim05ijr.js +0 -1
  168. package/dist/client/assets/index-CsZfTdMO.css +0 -2
  169. package/dist/client/assets/index-DDefs3iM.js +0 -275
  170. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BLzZdVo0.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, EXECUTION_PROFILE_SCHEMA_VERSION, fingerprintExecutionProfile, normalizeExecutionProfile, 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,278 @@ 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 IN (1, 2)),
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 executionProfileRevisionsTableSql = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'execution_profile_revisions'").get()?.sql ?? '';
224
+ if (!executionProfileRevisionsTableSql.includes('schema_version IN (1, 2)')) {
225
+ db.pragma('foreign_keys = OFF');
226
+ db.transaction(() => {
227
+ const referencingTriggers = db.prepare(`
228
+ SELECT name FROM sqlite_master
229
+ WHERE type = 'trigger' AND instr(sql, 'execution_profile_revisions') > 0
230
+ `).all();
231
+ for (const trigger of referencingTriggers) {
232
+ db.exec(`DROP TRIGGER IF EXISTS "${trigger.name.replaceAll('"', '""')}"`);
233
+ }
234
+ db.exec(`
235
+ CREATE TABLE execution_profile_revisions_v2 (
236
+ id TEXT PRIMARY KEY,
237
+ project_id TEXT NOT NULL,
238
+ revision INTEGER NOT NULL CHECK (revision > 0),
239
+ schema_version INTEGER NOT NULL CHECK (schema_version IN (1, 2)),
240
+ fingerprint TEXT NOT NULL CHECK (length(fingerprint) = 64),
241
+ profile_json TEXT NOT NULL CHECK (
242
+ json_valid(profile_json)
243
+ AND json_type(profile_json) = 'object'
244
+ AND length(CAST(profile_json AS BLOB)) BETWEEN 2 AND 16384
245
+ ),
246
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
247
+ UNIQUE (project_id, revision),
248
+ UNIQUE (project_id, fingerprint),
249
+ UNIQUE (project_id, id),
250
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
251
+ );
252
+ INSERT INTO execution_profile_revisions_v2 (
253
+ id, project_id, revision, schema_version, fingerprint, profile_json, created_at
254
+ )
255
+ SELECT id, project_id, revision, schema_version, fingerprint, profile_json, created_at
256
+ FROM execution_profile_revisions;
257
+ DROP TABLE execution_profile_revisions;
258
+ ALTER TABLE execution_profile_revisions_v2 RENAME TO execution_profile_revisions;
259
+ `);
260
+ })();
261
+ db.pragma('foreign_keys = ON');
262
+ }
263
+ const backfillExecutionProfiles = () => {
264
+ const projects = db.prepare(`
265
+ SELECT id, build_execution_provider, active_execution_profile_revision_id
266
+ FROM projects
267
+ `).all();
268
+ const findOwnedRevision = db.prepare(`
269
+ SELECT 1 FROM execution_profile_revisions WHERE project_id = ? AND id = ?
270
+ `);
271
+ const insertRevision = db.prepare(`
272
+ INSERT OR IGNORE INTO execution_profile_revisions (
273
+ id, project_id, revision, schema_version, fingerprint, profile_json
274
+ ) VALUES (?, ?, 1, ${EXECUTION_PROFILE_SCHEMA_VERSION}, ?, ?)
275
+ `);
276
+ const activateRevision = db.prepare(`
277
+ UPDATE projects SET active_execution_profile_revision_id = ? WHERE id = ?
278
+ `);
279
+ const insertEvent = db.prepare(`
280
+ INSERT OR IGNORE INTO execution_profile_audit_events (
281
+ id, project_id, event_type, profile_revision_id, previous_profile_revision_id
282
+ ) VALUES (?, ?, 'revision_created', ?, NULL)
283
+ `);
284
+ db.transaction(() => {
285
+ for (const project of projects) {
286
+ if (project.active_execution_profile_revision_id
287
+ && findOwnedRevision.get(project.id, project.active_execution_profile_revision_id)) {
288
+ continue;
289
+ }
290
+ const profile = createExecutionProfile(project.build_execution_provider);
291
+ const profileJson = serializeExecutionProfile(profile);
292
+ const fingerprint = fingerprintExecutionProfile(profile);
293
+ const revisionId = `${project.id}:execution-profile:1`;
294
+ insertRevision.run(revisionId, project.id, fingerprint, profileJson);
295
+ activateRevision.run(revisionId, project.id);
296
+ insertEvent.run(`${revisionId}:created`, project.id, revisionId);
297
+ }
298
+ })();
299
+ };
300
+ const backfillExecutionProfilesToSchemaV2 = () => {
301
+ const legacyRows = db.prepare(`
302
+ SELECT projects.id AS project_id,
303
+ revisions.id AS revision_id,
304
+ revisions.profile_json
305
+ FROM projects
306
+ INNER JOIN execution_profile_revisions revisions
307
+ ON revisions.id = projects.active_execution_profile_revision_id
308
+ AND revisions.project_id = projects.id
309
+ WHERE revisions.schema_version = 1 AND projects.deletion_started_at IS NULL
310
+ `).all();
311
+ const nextRevisionNumber = db.prepare(`
312
+ SELECT COALESCE(MAX(revision), 0) + 1 AS revision
313
+ FROM execution_profile_revisions WHERE project_id = ?
314
+ `);
315
+ const findRevisionByFingerprint = db.prepare(`
316
+ SELECT id FROM execution_profile_revisions WHERE project_id = ? AND fingerprint = ?
317
+ `);
318
+ const insertRevision = db.prepare(`
319
+ INSERT OR IGNORE INTO execution_profile_revisions (
320
+ id, project_id, revision, schema_version, fingerprint, profile_json
321
+ ) VALUES (?, ?, ?, ${EXECUTION_PROFILE_SCHEMA_VERSION}, ?, ?)
322
+ `);
323
+ const activateRevision = db.prepare(`
324
+ UPDATE projects SET active_execution_profile_revision_id = ?
325
+ WHERE id = ? AND active_execution_profile_revision_id = ?
326
+ `);
327
+ const insertEvent = db.prepare(`
328
+ INSERT OR IGNORE INTO execution_profile_audit_events (
329
+ id, project_id, event_type, profile_revision_id, previous_profile_revision_id
330
+ ) VALUES (?, ?, 'revision_activated', ?, ?)
331
+ `);
332
+ db.transaction(() => {
333
+ for (const row of legacyRows) {
334
+ let normalized;
335
+ try {
336
+ normalized = normalizeExecutionProfile(JSON.parse(row.profile_json));
337
+ }
338
+ catch {
339
+ normalized = null;
340
+ }
341
+ if (!normalized?.ok) {
342
+ throw new Error(`Stored execution profile revision ${row.revision_id} is invalid`);
343
+ }
344
+ const fingerprint = fingerprintExecutionProfile(normalized.profile);
345
+ const existing = findRevisionByFingerprint.get(row.project_id, fingerprint);
346
+ let revisionId;
347
+ if (existing) {
348
+ revisionId = existing.id;
349
+ }
350
+ else {
351
+ revisionId = `${row.project_id}:execution-profile:v2`;
352
+ const revisionNumber = nextRevisionNumber.get(row.project_id).revision;
353
+ insertRevision.run(revisionId, row.project_id, revisionNumber, fingerprint, serializeExecutionProfile(normalized.profile));
354
+ }
355
+ const activated = activateRevision.run(revisionId, row.project_id, row.revision_id);
356
+ if (activated.changes > 0) {
357
+ insertEvent.run(`${revisionId}:v2-backfill`, row.project_id, revisionId, row.revision_id);
358
+ }
359
+ }
360
+ })();
361
+ };
362
+ backfillExecutionProfiles();
363
+ backfillExecutionProfilesToSchemaV2();
364
+ const hostCompatibilityProfile = createExecutionProfile('host');
365
+ const hostCompatibilityProfileJson = serializeExecutionProfile(hostCompatibilityProfile).replaceAll("'", "''");
366
+ const hostCompatibilityFingerprint = fingerprintExecutionProfile(hostCompatibilityProfile);
367
+ const dockerProfile = createExecutionProfile('docker_sandbox');
368
+ const dockerProfileJson = serializeExecutionProfile(dockerProfile).replaceAll("'", "''");
369
+ const dockerProfileFingerprint = fingerprintExecutionProfile(dockerProfile);
370
+ db.exec(`
371
+ CREATE TRIGGER IF NOT EXISTS create_project_execution_profile
372
+ AFTER INSERT ON projects
373
+ WHEN NEW.active_execution_profile_revision_id = ''
374
+ BEGIN
375
+ INSERT INTO execution_profile_revisions (
376
+ id, project_id, revision, schema_version, fingerprint, profile_json
377
+ ) VALUES (
378
+ NEW.id || ':execution-profile:1', NEW.id, 1, ${EXECUTION_PROFILE_SCHEMA_VERSION},
379
+ CASE WHEN NEW.build_execution_provider = 'docker_sandbox'
380
+ THEN '${dockerProfileFingerprint}' ELSE '${hostCompatibilityFingerprint}' END,
381
+ CASE WHEN NEW.build_execution_provider = 'docker_sandbox'
382
+ THEN '${dockerProfileJson}' ELSE '${hostCompatibilityProfileJson}' END
383
+ );
384
+ UPDATE projects
385
+ SET active_execution_profile_revision_id = NEW.id || ':execution-profile:1'
386
+ WHERE id = NEW.id;
387
+ INSERT INTO execution_profile_audit_events (
388
+ id, project_id, event_type, profile_revision_id, previous_profile_revision_id
389
+ ) VALUES (
390
+ NEW.id || ':execution-profile:1:created', NEW.id, 'revision_created',
391
+ NEW.id || ':execution-profile:1', NULL
392
+ );
393
+ END;
394
+
395
+ CREATE TRIGGER IF NOT EXISTS validate_project_execution_profile_owner_insert
396
+ BEFORE INSERT ON projects
397
+ WHEN NEW.active_execution_profile_revision_id <> '' AND NOT EXISTS (
398
+ SELECT 1 FROM execution_profile_revisions
399
+ WHERE project_id = NEW.id AND id = NEW.active_execution_profile_revision_id
400
+ )
401
+ BEGIN
402
+ SELECT RAISE(ABORT, 'Active execution profile revision is not owned by project');
403
+ END;
404
+
405
+ CREATE TRIGGER IF NOT EXISTS validate_project_execution_profile_owner_update
406
+ BEFORE UPDATE OF active_execution_profile_revision_id ON projects
407
+ WHEN NEW.active_execution_profile_revision_id = '' OR NOT EXISTS (
408
+ SELECT 1 FROM execution_profile_revisions
409
+ WHERE project_id = NEW.id AND id = NEW.active_execution_profile_revision_id
410
+ )
411
+ BEGIN
412
+ SELECT RAISE(ABORT, 'Active execution profile revision is not owned by project');
413
+ END;
414
+
415
+ CREATE TRIGGER IF NOT EXISTS validate_project_execution_profile_projection_insert
416
+ BEFORE INSERT ON projects
417
+ WHEN NEW.active_execution_profile_revision_id <> '' AND NOT EXISTS (
418
+ SELECT 1 FROM execution_profile_revisions
419
+ WHERE project_id = NEW.id
420
+ AND id = NEW.active_execution_profile_revision_id
421
+ AND json_extract(profile_json, '$.provider_preference') = NEW.build_execution_provider
422
+ )
423
+ BEGIN
424
+ SELECT RAISE(ABORT, 'Build execution provider must project the active execution profile');
425
+ END;
426
+
427
+ CREATE TRIGGER IF NOT EXISTS validate_project_execution_profile_projection_update
428
+ BEFORE UPDATE OF active_execution_profile_revision_id, build_execution_provider ON projects
429
+ WHEN NOT EXISTS (
430
+ SELECT 1 FROM execution_profile_revisions
431
+ WHERE project_id = NEW.id
432
+ AND id = NEW.active_execution_profile_revision_id
433
+ AND json_extract(profile_json, '$.provider_preference') = NEW.build_execution_provider
434
+ )
435
+ BEGIN
436
+ SELECT RAISE(ABORT, 'Build execution provider must project the active execution profile');
437
+ END;
438
+
439
+ CREATE TRIGGER IF NOT EXISTS preserve_execution_profile_revision
440
+ BEFORE UPDATE ON execution_profile_revisions
441
+ BEGIN
442
+ SELECT RAISE(ABORT, 'Execution profile revisions are immutable');
443
+ END;
444
+
445
+ CREATE TRIGGER IF NOT EXISTS preserve_execution_profile_audit_event
446
+ BEFORE UPDATE ON execution_profile_audit_events
447
+ BEGIN
448
+ SELECT RAISE(ABORT, 'Execution profile audit events are immutable');
449
+ END;
450
+ `);
176
451
  const backfillProjectCheckDefinitions = () => {
177
452
  const rows = db.prepare(`
178
453
  SELECT id, check_definitions, test_commands
@@ -946,11 +1221,115 @@ db.exec(`
946
1221
  entered_queue_at DATETIME DEFAULT CURRENT_TIMESTAMP,
947
1222
  started_at DATETIME,
948
1223
  finished_at DATETIME,
1224
+ execution_profile_revision_id TEXT,
1225
+ execution_profile_fingerprint TEXT,
1226
+ execution_profile_selection_reason TEXT,
1227
+ activation_execution_run_id TEXT,
949
1228
  PRIMARY KEY (build_sequence_id, ticket_id),
950
1229
  FOREIGN KEY (build_sequence_id) REFERENCES build_sequences(id) ON DELETE CASCADE,
951
1230
  FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE
952
1231
  )
953
1232
  `);
1233
+ db.exec(`
1234
+ CREATE TABLE IF NOT EXISTS build_sequence_ticket_replacements (
1235
+ id TEXT PRIMARY KEY,
1236
+ build_sequence_id TEXT NOT NULL,
1237
+ ticket_id TEXT NOT NULL,
1238
+ position INTEGER NOT NULL,
1239
+ execution_profile_revision_id TEXT NOT NULL,
1240
+ execution_profile_fingerprint TEXT NOT NULL,
1241
+ execution_profile_selection_reason TEXT NOT NULL,
1242
+ replaced_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1243
+ FOREIGN KEY (build_sequence_id) REFERENCES build_sequences(id) ON DELETE CASCADE,
1244
+ FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE
1245
+ )
1246
+ `);
1247
+ ensureColumn('build_sequence_tickets', 'execution_profile_revision_id', 'TEXT');
1248
+ ensureColumn('build_sequence_tickets', 'execution_profile_fingerprint', 'TEXT');
1249
+ ensureColumn('build_sequence_tickets', 'execution_profile_selection_reason', 'TEXT');
1250
+ ensureColumn('build_sequence_tickets', 'activation_execution_run_id', 'TEXT');
1251
+ db.transaction(() => {
1252
+ const incompleteSequenceIds = db.prepare(`
1253
+ SELECT DISTINCT build_sequence_id
1254
+ FROM build_sequence_tickets
1255
+ WHERE status IN ('active', 'queued')
1256
+ AND (
1257
+ execution_profile_revision_id IS NULL
1258
+ OR execution_profile_fingerprint IS NULL
1259
+ OR execution_profile_selection_reason IS NULL
1260
+ )
1261
+ `).all();
1262
+ const failActive = db.prepare(`
1263
+ UPDATE build_sequence_tickets
1264
+ SET status = 'failed', finished_at = COALESCE(finished_at, CURRENT_TIMESTAMP)
1265
+ WHERE build_sequence_id = ? AND status = 'active'
1266
+ `);
1267
+ const cancelQueued = db.prepare(`
1268
+ UPDATE build_sequence_tickets
1269
+ SET status = 'cancelled', finished_at = COALESCE(finished_at, CURRENT_TIMESTAMP)
1270
+ WHERE build_sequence_id = ? AND status = 'queued'
1271
+ `);
1272
+ const failSequence = db.prepare(`
1273
+ UPDATE build_sequences SET status = 'failed', updated_at = CURRENT_TIMESTAMP
1274
+ WHERE id = ? AND status = 'active'
1275
+ `);
1276
+ for (const row of incompleteSequenceIds) {
1277
+ failActive.run(row.build_sequence_id);
1278
+ cancelQueued.run(row.build_sequence_id);
1279
+ failSequence.run(row.build_sequence_id);
1280
+ }
1281
+ })();
1282
+ db.exec(`
1283
+ CREATE TRIGGER IF NOT EXISTS validate_build_sequence_ticket_profile_insert
1284
+ BEFORE INSERT ON build_sequence_tickets
1285
+ WHEN NEW.status IN ('active', 'queued')
1286
+ OR NEW.execution_profile_revision_id IS NOT NULL OR NEW.execution_profile_fingerprint IS NOT NULL
1287
+ OR NEW.execution_profile_selection_reason IS NOT NULL
1288
+ BEGIN
1289
+ SELECT CASE WHEN
1290
+ NEW.execution_profile_revision_id IS NULL
1291
+ OR NEW.execution_profile_fingerprint IS NULL
1292
+ OR NEW.execution_profile_selection_reason NOT IN ('project_active_at_acceptance', 'historical_lineage')
1293
+ OR NOT EXISTS (
1294
+ SELECT 1
1295
+ FROM execution_profile_revisions revision
1296
+ JOIN build_sequences sequence ON sequence.id = NEW.build_sequence_id
1297
+ WHERE revision.id = NEW.execution_profile_revision_id
1298
+ AND revision.project_id = sequence.project_id
1299
+ AND revision.fingerprint = NEW.execution_profile_fingerprint
1300
+ )
1301
+ THEN RAISE(ABORT, 'Invalid Build sequence execution profile selection') END;
1302
+ END;
1303
+
1304
+ CREATE TRIGGER IF NOT EXISTS validate_build_sequence_ticket_profile_activation
1305
+ BEFORE UPDATE OF status ON build_sequence_tickets
1306
+ WHEN NEW.status IN ('active', 'queued') AND (
1307
+ NEW.execution_profile_revision_id IS NULL
1308
+ OR NEW.execution_profile_fingerprint IS NULL
1309
+ OR NEW.execution_profile_selection_reason NOT IN ('project_active_at_acceptance', 'historical_lineage')
1310
+ OR NOT EXISTS (
1311
+ SELECT 1
1312
+ FROM execution_profile_revisions revision
1313
+ JOIN build_sequences sequence ON sequence.id = NEW.build_sequence_id
1314
+ WHERE revision.id = NEW.execution_profile_revision_id
1315
+ AND revision.project_id = sequence.project_id
1316
+ AND revision.fingerprint = NEW.execution_profile_fingerprint
1317
+ )
1318
+ )
1319
+ BEGIN
1320
+ SELECT RAISE(ABORT, 'Invalid Build sequence execution profile selection');
1321
+ END;
1322
+
1323
+ CREATE TRIGGER IF NOT EXISTS preserve_build_sequence_ticket_profile
1324
+ BEFORE UPDATE OF execution_profile_revision_id, execution_profile_fingerprint, execution_profile_selection_reason
1325
+ ON build_sequence_tickets
1326
+ WHEN NEW.execution_profile_revision_id IS NOT OLD.execution_profile_revision_id
1327
+ OR NEW.execution_profile_fingerprint IS NOT OLD.execution_profile_fingerprint
1328
+ OR NEW.execution_profile_selection_reason IS NOT OLD.execution_profile_selection_reason
1329
+ BEGIN
1330
+ SELECT RAISE(ABORT, 'Build sequence execution profile selection is immutable');
1331
+ END;
1332
+ `);
954
1333
  db.exec(`
955
1334
  CREATE TABLE IF NOT EXISTS ticket_dependencies (
956
1335
  blocker_ticket_id TEXT NOT NULL,
@@ -2132,6 +2511,9 @@ db.exec(`
2132
2511
  environment_revision INTEGER NOT NULL,
2133
2512
  workspace_mode TEXT NOT NULL,
2134
2513
  environment_validation_fingerprint TEXT NOT NULL,
2514
+ execution_profile_revision_id TEXT,
2515
+ execution_profile_fingerprint TEXT,
2516
+ credential_requirements_fingerprint TEXT,
2135
2517
  template_identity TEXT,
2136
2518
  template_digest TEXT,
2137
2519
  agent_identity TEXT,
@@ -2151,6 +2533,7 @@ db.exec(`
2151
2533
  last_used_at DATETIME,
2152
2534
  stopped_at DATETIME,
2153
2535
  retirement_started_at DATETIME,
2536
+ retirement_requested_at DATETIME,
2154
2537
  cleanup_pending_at DATETIME,
2155
2538
  retired_at DATETIME,
2156
2539
  failed_at DATETIME,
@@ -2169,6 +2552,10 @@ db.exec(`
2169
2552
  CHECK (environment_revision >= 1),
2170
2553
  CHECK (workspace_mode = 'direct'),
2171
2554
  CHECK (length(environment_validation_fingerprint) = 64 AND environment_validation_fingerprint NOT GLOB '*[^0-9a-f]*'),
2555
+ CHECK ((execution_profile_revision_id IS NULL) = (execution_profile_fingerprint IS NULL)),
2556
+ CHECK ((execution_profile_fingerprint IS NULL) = (credential_requirements_fingerprint IS NULL)),
2557
+ CHECK (execution_profile_fingerprint IS NULL OR (length(execution_profile_fingerprint) = 64 AND execution_profile_fingerprint NOT GLOB '*[^0-9a-f]*')),
2558
+ CHECK (credential_requirements_fingerprint IS NULL OR (length(credential_requirements_fingerprint) = 64 AND credential_requirements_fingerprint NOT GLOB '*[^0-9a-f]*')),
2172
2559
  CHECK ((template_identity IS NULL) <> (template_digest IS NULL)),
2173
2560
  CHECK (template_identity IS NULL OR length(CAST(template_identity AS BLOB)) BETWEEN 1 AND 500),
2174
2561
  CHECK (template_digest IS NULL OR length(CAST(template_digest AS BLOB)) BETWEEN 1 AND 500),
@@ -2214,6 +2601,7 @@ db.exec(`
2214
2601
  relationship TEXT NOT NULL,
2215
2602
  environment_revision INTEGER NOT NULL,
2216
2603
  environment_validation_fingerprint TEXT NOT NULL,
2604
+ execution_profile_fingerprint TEXT,
2217
2605
  authority_json TEXT,
2218
2606
  placement_json TEXT,
2219
2607
  bound_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -2224,6 +2612,7 @@ db.exec(`
2224
2612
  CHECK (relationship IN ('validation', 'execution')),
2225
2613
  CHECK (environment_revision >= 1),
2226
2614
  CHECK (length(environment_validation_fingerprint) = 64 AND environment_validation_fingerprint NOT GLOB '*[^0-9a-f]*'),
2615
+ CHECK (execution_profile_fingerprint IS NULL OR (length(execution_profile_fingerprint) = 64 AND execution_profile_fingerprint NOT GLOB '*[^0-9a-f]*')),
2227
2616
  CHECK (relationship <> 'execution' OR length(CAST(authority_json AS BLOB)) BETWEEN 2 AND 16384),
2228
2617
  CHECK (placement_json IS NULL OR (relationship = 'execution' AND length(CAST(placement_json AS BLOB)) BETWEEN 2 AND 16384))
2229
2618
  );
@@ -2298,8 +2687,357 @@ db.exec(`
2298
2687
  SELECT RAISE(ABORT, 'Execution environment run binding identity is immutable');
2299
2688
  END;
2300
2689
  `);
2690
+ ensureColumn('execution_environments', 'execution_profile_revision_id', 'TEXT');
2691
+ ensureColumn('execution_environments', 'execution_profile_fingerprint', 'TEXT');
2692
+ ensureColumn('execution_environments', 'credential_requirements_fingerprint', 'TEXT');
2693
+ ensureColumn('execution_environments', 'retirement_requested_at', 'DATETIME');
2694
+ ensureColumn('execution_environment_run_bindings', 'execution_profile_fingerprint', 'TEXT');
2301
2695
  ensureColumn('execution_environment_run_bindings', 'authority_json', 'TEXT');
2302
2696
  ensureColumn('execution_environment_run_bindings', 'placement_json', 'TEXT');
2697
+ db.exec(`
2698
+ CREATE TABLE IF NOT EXISTS execution_run_authorities (
2699
+ execution_run_id TEXT NOT NULL,
2700
+ project_id TEXT NOT NULL,
2701
+ execution_profile_revision_id TEXT NOT NULL,
2702
+ execution_profile_fingerprint TEXT NOT NULL,
2703
+ authority_json TEXT NOT NULL,
2704
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2705
+ PRIMARY KEY (execution_run_id),
2706
+ FOREIGN KEY (execution_run_id, project_id) REFERENCES execution_runs(id, project_id) ON DELETE CASCADE,
2707
+ FOREIGN KEY (project_id, execution_profile_revision_id)
2708
+ REFERENCES execution_profile_revisions(project_id, id) ON DELETE RESTRICT,
2709
+ CHECK (length(execution_profile_fingerprint) = 64 AND execution_profile_fingerprint NOT GLOB '*[^0-9a-f]*'),
2710
+ CHECK (length(CAST(authority_json AS BLOB)) BETWEEN 2 AND 16384)
2711
+ );
2712
+
2713
+ CREATE TRIGGER IF NOT EXISTS preserve_execution_run_authority
2714
+ BEFORE UPDATE ON execution_run_authorities
2715
+ BEGIN
2716
+ SELECT RAISE(ABORT, 'Execution run authority is immutable');
2717
+ END;
2718
+
2719
+ `);
2720
+ db.exec(`
2721
+ CREATE TABLE IF NOT EXISTS approval_requests (
2722
+ id TEXT PRIMARY KEY,
2723
+ project_id TEXT NOT NULL,
2724
+ execution_run_id TEXT NOT NULL,
2725
+ action_kind TEXT NOT NULL,
2726
+ action_schema_version INTEGER NOT NULL,
2727
+ action_json TEXT NOT NULL,
2728
+ action_fingerprint TEXT NOT NULL,
2729
+ authority_schema_version INTEGER NOT NULL,
2730
+ authority_json TEXT NOT NULL,
2731
+ status TEXT NOT NULL DEFAULT 'pending',
2732
+ expires_at DATETIME NOT NULL,
2733
+ decision_at DATETIME,
2734
+ consumed_at DATETIME,
2735
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2736
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2737
+ UNIQUE (id, project_id),
2738
+ UNIQUE (execution_run_id),
2739
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
2740
+ FOREIGN KEY (execution_run_id, project_id) REFERENCES execution_runs(id, project_id) ON DELETE CASCADE,
2741
+ CHECK (length(id) BETWEEN 1 AND 128),
2742
+ CHECK (action_kind IN ('git_push', 'custom_check')),
2743
+ CHECK (action_schema_version = 1),
2744
+ CHECK (length(CAST(action_json AS BLOB)) BETWEEN 2 AND 65536),
2745
+ CHECK (length(action_fingerprint) = 64 AND action_fingerprint NOT GLOB '*[^0-9a-f]*'),
2746
+ CHECK (authority_schema_version = 1),
2747
+ CHECK (length(CAST(authority_json AS BLOB)) BETWEEN 2 AND 16384),
2748
+ CHECK (status IN ('pending', 'approved', 'denied', 'cancelled', 'expired', 'invalidated', 'consumed')),
2749
+ CHECK (status NOT IN ('approved', 'denied', 'consumed') OR decision_at IS NOT NULL),
2750
+ CHECK (status <> 'pending' OR decision_at IS NULL),
2751
+ CHECK ((status = 'consumed') = (consumed_at IS NOT NULL))
2752
+ );
2753
+
2754
+ CREATE TABLE IF NOT EXISTS approval_request_events (
2755
+ id TEXT PRIMARY KEY,
2756
+ project_id TEXT NOT NULL,
2757
+ approval_request_id TEXT NOT NULL,
2758
+ sequence INTEGER NOT NULL,
2759
+ event_type TEXT NOT NULL,
2760
+ from_status TEXT,
2761
+ to_status TEXT NOT NULL,
2762
+ actor_classification TEXT NOT NULL,
2763
+ actor_reference TEXT,
2764
+ reason TEXT,
2765
+ metadata_schema_version INTEGER NOT NULL DEFAULT 1,
2766
+ metadata_json TEXT,
2767
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2768
+ UNIQUE (approval_request_id, sequence),
2769
+ FOREIGN KEY (approval_request_id, project_id) REFERENCES approval_requests(id, project_id) ON DELETE CASCADE,
2770
+ CHECK (sequence >= 1),
2771
+ CHECK (length(event_type) BETWEEN 1 AND 100),
2772
+ CHECK (from_status IS NULL OR from_status IN ('pending', 'approved', 'denied', 'cancelled', 'expired', 'invalidated', 'consumed')),
2773
+ CHECK (to_status IN ('pending', 'approved', 'denied', 'cancelled', 'expired', 'invalidated', 'consumed')),
2774
+ CHECK (actor_classification IN ('human', 'system', 'action_executor')),
2775
+ CHECK (actor_reference IS NULL OR length(CAST(actor_reference AS BLOB)) BETWEEN 1 AND 200),
2776
+ CHECK (reason IS NULL OR length(CAST(reason AS BLOB)) <= 2000),
2777
+ CHECK (metadata_schema_version = 1),
2778
+ CHECK (metadata_json IS NULL OR length(CAST(metadata_json AS BLOB)) <= 4096)
2779
+ );
2780
+
2781
+ CREATE TRIGGER IF NOT EXISTS preserve_approval_request_identity
2782
+ BEFORE UPDATE OF id, project_id, execution_run_id, action_kind, action_schema_version,
2783
+ action_json, action_fingerprint, authority_schema_version, authority_json, expires_at, created_at
2784
+ ON approval_requests
2785
+ BEGIN
2786
+ SELECT RAISE(ABORT, 'Approval request identity and evidence are immutable');
2787
+ END;
2788
+
2789
+ CREATE TRIGGER IF NOT EXISTS preserve_approval_request_decision_evidence
2790
+ BEFORE UPDATE OF decision_at, consumed_at ON approval_requests
2791
+ WHEN (OLD.decision_at IS NOT NULL AND NEW.decision_at IS NOT OLD.decision_at)
2792
+ OR (OLD.consumed_at IS NOT NULL AND NEW.consumed_at IS NOT OLD.consumed_at)
2793
+ BEGIN
2794
+ SELECT RAISE(ABORT, 'Approval request decision evidence is immutable');
2795
+ END;
2796
+
2797
+ CREATE TRIGGER IF NOT EXISTS validate_approval_request_status_transition
2798
+ BEFORE UPDATE OF status ON approval_requests
2799
+ WHEN NOT (
2800
+ (OLD.status = 'pending' AND NEW.status IN ('approved', 'denied', 'cancelled', 'expired', 'invalidated'))
2801
+ OR (OLD.status = 'approved' AND NEW.status IN ('consumed', 'expired', 'invalidated'))
2802
+ )
2803
+ BEGIN
2804
+ SELECT RAISE(ABORT, 'Invalid approval request status transition');
2805
+ END;
2806
+
2807
+ CREATE TRIGGER IF NOT EXISTS preserve_approval_request_events
2808
+ BEFORE UPDATE ON approval_request_events
2809
+ BEGIN
2810
+ SELECT RAISE(ABORT, 'Approval request events are append-only');
2811
+ END;
2812
+
2813
+ CREATE TRIGGER IF NOT EXISTS prevent_approval_request_event_deletion
2814
+ BEFORE DELETE ON approval_request_events
2815
+ WHEN EXISTS (
2816
+ SELECT 1 FROM projects
2817
+ WHERE id = OLD.project_id AND deletion_started_at IS NULL
2818
+ )
2819
+ BEGIN
2820
+ SELECT RAISE(ABORT, 'Approval request events are append-only');
2821
+ END;
2822
+
2823
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_project_created
2824
+ ON approval_requests(project_id, created_at DESC, id DESC);
2825
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_project_status_expiry
2826
+ ON approval_requests(project_id, status, expires_at, id);
2827
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_action_kind
2828
+ ON approval_requests(project_id, action_kind, created_at DESC, id DESC);
2829
+ CREATE INDEX IF NOT EXISTS idx_approval_request_events_sequence
2830
+ ON approval_request_events(project_id, approval_request_id, sequence ASC);
2831
+ `);
2832
+ const migrateApprovalRequestActionLimit = () => {
2833
+ const tableSql = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'approval_requests'").get()?.sql ?? '';
2834
+ if (tableSql.includes('BETWEEN 2 AND 65536')) {
2835
+ return;
2836
+ }
2837
+ db.exec('PRAGMA foreign_keys = OFF');
2838
+ try {
2839
+ db.exec('BEGIN');
2840
+ db.exec(`
2841
+ CREATE TABLE approval_requests_v2 (
2842
+ id TEXT PRIMARY KEY,
2843
+ project_id TEXT NOT NULL,
2844
+ execution_run_id TEXT NOT NULL,
2845
+ action_kind TEXT NOT NULL,
2846
+ action_schema_version INTEGER NOT NULL,
2847
+ action_json TEXT NOT NULL,
2848
+ action_fingerprint TEXT NOT NULL,
2849
+ authority_schema_version INTEGER NOT NULL,
2850
+ authority_json TEXT NOT NULL,
2851
+ status TEXT NOT NULL DEFAULT 'pending',
2852
+ expires_at DATETIME NOT NULL,
2853
+ decision_at DATETIME,
2854
+ consumed_at DATETIME,
2855
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2856
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2857
+ UNIQUE (id, project_id),
2858
+ UNIQUE (execution_run_id),
2859
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
2860
+ FOREIGN KEY (execution_run_id, project_id) REFERENCES execution_runs(id, project_id) ON DELETE CASCADE,
2861
+ CHECK (length(id) BETWEEN 1 AND 128),
2862
+ CHECK (action_kind IN ('git_push', 'custom_check')),
2863
+ CHECK (action_schema_version = 1),
2864
+ CHECK (length(CAST(action_json AS BLOB)) BETWEEN 2 AND 65536),
2865
+ CHECK (length(action_fingerprint) = 64 AND action_fingerprint NOT GLOB '*[^0-9a-f]*'),
2866
+ CHECK (authority_schema_version = 1),
2867
+ CHECK (length(CAST(authority_json AS BLOB)) BETWEEN 2 AND 16384),
2868
+ CHECK (status IN ('pending', 'approved', 'denied', 'cancelled', 'expired', 'invalidated', 'consumed')),
2869
+ CHECK (status NOT IN ('approved', 'denied', 'consumed') OR decision_at IS NOT NULL),
2870
+ CHECK (status <> 'pending' OR decision_at IS NULL),
2871
+ CHECK ((status = 'consumed') = (consumed_at IS NOT NULL))
2872
+ );
2873
+ INSERT INTO approval_requests_v2 (
2874
+ id, project_id, execution_run_id, action_kind, action_schema_version, action_json,
2875
+ action_fingerprint, authority_schema_version, authority_json, status, expires_at,
2876
+ decision_at, consumed_at, created_at, updated_at
2877
+ )
2878
+ SELECT
2879
+ id, project_id, execution_run_id, action_kind, action_schema_version, action_json,
2880
+ action_fingerprint, authority_schema_version, authority_json, status, expires_at,
2881
+ decision_at, consumed_at, created_at, updated_at
2882
+ FROM approval_requests;
2883
+ DROP TABLE approval_requests;
2884
+ ALTER TABLE approval_requests_v2 RENAME TO approval_requests;
2885
+ `);
2886
+ db.exec('COMMIT');
2887
+ }
2888
+ catch (error) {
2889
+ db.exec('ROLLBACK');
2890
+ throw error;
2891
+ }
2892
+ finally {
2893
+ db.exec('PRAGMA foreign_keys = ON');
2894
+ }
2895
+ db.exec(`
2896
+ CREATE TRIGGER preserve_approval_request_identity
2897
+ BEFORE UPDATE OF id, project_id, execution_run_id, action_kind, action_schema_version,
2898
+ action_json, action_fingerprint, authority_schema_version, authority_json, expires_at, created_at
2899
+ ON approval_requests
2900
+ BEGIN
2901
+ SELECT RAISE(ABORT, 'Approval request identity and evidence are immutable');
2902
+ END;
2903
+
2904
+ CREATE TRIGGER preserve_approval_request_decision_evidence
2905
+ BEFORE UPDATE OF decision_at, consumed_at ON approval_requests
2906
+ WHEN (OLD.decision_at IS NOT NULL AND NEW.decision_at IS NOT OLD.decision_at)
2907
+ OR (OLD.consumed_at IS NOT NULL AND NEW.consumed_at IS NOT OLD.consumed_at)
2908
+ BEGIN
2909
+ SELECT RAISE(ABORT, 'Approval request decision evidence is immutable');
2910
+ END;
2911
+
2912
+ CREATE TRIGGER validate_approval_request_status_transition
2913
+ BEFORE UPDATE OF status ON approval_requests
2914
+ WHEN NOT (
2915
+ (OLD.status = 'pending' AND NEW.status IN ('approved', 'denied', 'cancelled', 'expired', 'invalidated'))
2916
+ OR (OLD.status = 'approved' AND NEW.status IN ('consumed', 'expired', 'invalidated'))
2917
+ )
2918
+ BEGIN
2919
+ SELECT RAISE(ABORT, 'Invalid approval request status transition');
2920
+ END;
2921
+
2922
+ CREATE INDEX idx_approval_requests_project_created
2923
+ ON approval_requests(project_id, created_at DESC, id DESC);
2924
+ CREATE INDEX idx_approval_requests_project_status_expiry
2925
+ ON approval_requests(project_id, status, expires_at, id);
2926
+ CREATE INDEX idx_approval_requests_action_kind
2927
+ ON approval_requests(project_id, action_kind, created_at DESC, id DESC);
2928
+ `);
2929
+ };
2930
+ migrateApprovalRequestActionLimit();
2931
+ db.exec(`
2932
+ CREATE TABLE IF NOT EXISTS build_setup_evidence (
2933
+ id TEXT PRIMARY KEY,
2934
+ project_id TEXT NOT NULL,
2935
+ execution_run_id TEXT NOT NULL,
2936
+ target_kind TEXT NOT NULL,
2937
+ target_key TEXT NOT NULL,
2938
+ target_workspace_path TEXT NOT NULL,
2939
+ workspace_evidence_state TEXT NOT NULL,
2940
+ workspace_evidence_fingerprint TEXT,
2941
+ setup_configuration_fingerprint TEXT NOT NULL,
2942
+ execution_profile_fingerprint TEXT NOT NULL,
2943
+ provider TEXT NOT NULL,
2944
+ environment_revision INTEGER,
2945
+ command_count INTEGER NOT NULL,
2946
+ outcome TEXT NOT NULL,
2947
+ reused_from_evidence_id TEXT,
2948
+ started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2949
+ completed_at DATETIME,
2950
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
2951
+ FOREIGN KEY (execution_run_id, project_id) REFERENCES execution_runs(id, project_id) ON DELETE CASCADE,
2952
+ FOREIGN KEY (reused_from_evidence_id) REFERENCES build_setup_evidence(id) ON DELETE CASCADE,
2953
+ CHECK (length(id) BETWEEN 1 AND 128),
2954
+ CHECK (length(target_kind) BETWEEN 1 AND 100),
2955
+ CHECK (length(CAST(target_key AS BLOB)) BETWEEN 1 AND 1000),
2956
+ CHECK (length(CAST(target_workspace_path AS BLOB)) BETWEEN 1 AND 4096),
2957
+ CHECK (workspace_evidence_state IN ('available', 'unavailable')),
2958
+ CHECK ((workspace_evidence_state = 'available') = (workspace_evidence_fingerprint IS NOT NULL)),
2959
+ CHECK (workspace_evidence_fingerprint IS NULL OR (length(workspace_evidence_fingerprint) = 64 AND workspace_evidence_fingerprint NOT GLOB '*[^0-9a-f]*')),
2960
+ CHECK (length(setup_configuration_fingerprint) = 64 AND setup_configuration_fingerprint NOT GLOB '*[^0-9a-f]*'),
2961
+ CHECK (length(execution_profile_fingerprint) = 64 AND execution_profile_fingerprint NOT GLOB '*[^0-9a-f]*'),
2962
+ CHECK (provider IN ('host', 'docker_sandbox')),
2963
+ CHECK ((provider = 'host') = (environment_revision IS NULL)),
2964
+ CHECK (environment_revision IS NULL OR environment_revision >= 1),
2965
+ CHECK (command_count >= 0),
2966
+ CHECK (outcome IN ('running', 'completed', 'noop', 'reused', 'failed')),
2967
+ CHECK ((outcome = 'reused') = (reused_from_evidence_id IS NOT NULL)),
2968
+ CHECK ((outcome = 'running') = (completed_at IS NULL))
2969
+ );
2970
+
2971
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_build_setup_evidence_active_target
2972
+ ON build_setup_evidence(project_id, target_key)
2973
+ WHERE outcome = 'running';
2974
+
2975
+ CREATE INDEX IF NOT EXISTS idx_build_setup_evidence_reuse
2976
+ ON build_setup_evidence(
2977
+ project_id, target_key, workspace_evidence_fingerprint,
2978
+ setup_configuration_fingerprint, execution_profile_fingerprint,
2979
+ provider, environment_revision, completed_at DESC
2980
+ )
2981
+ WHERE outcome IN ('completed', 'noop', 'reused');
2982
+
2983
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_build_setup_evidence_execution_run
2984
+ ON build_setup_evidence(execution_run_id);
2985
+
2986
+ CREATE TRIGGER IF NOT EXISTS preserve_build_setup_evidence_identity
2987
+ BEFORE UPDATE OF id, project_id, execution_run_id, target_kind, target_key,
2988
+ target_workspace_path, workspace_evidence_state, workspace_evidence_fingerprint,
2989
+ setup_configuration_fingerprint, execution_profile_fingerprint, provider,
2990
+ environment_revision, command_count, reused_from_evidence_id, started_at
2991
+ ON build_setup_evidence
2992
+ BEGIN
2993
+ SELECT RAISE(ABORT, 'Build setup evidence identity is immutable');
2994
+ END;
2995
+ `);
2996
+ db.exec('DROP TRIGGER IF EXISTS preserve_execution_environment_identity');
2997
+ db.exec('DROP TRIGGER IF EXISTS validate_execution_environment_binding_identity');
2998
+ db.exec('DROP TRIGGER IF EXISTS preserve_execution_environment_binding_identity');
2999
+ db.exec(`
3000
+ CREATE TRIGGER preserve_execution_environment_identity
3001
+ BEFORE UPDATE OF id, project_id, provider_kind, provider_version, target_kind, target_key,
3002
+ target_workspace_path, environment_revision, workspace_mode,
3003
+ environment_validation_fingerprint, execution_profile_revision_id, execution_profile_fingerprint,
3004
+ credential_requirements_fingerprint, template_identity, template_digest,
3005
+ agent_identity, compatible_agents_json, created_at ON execution_environments
3006
+ WHEN NEW.id <> OLD.id OR NEW.project_id <> OLD.project_id OR NEW.provider_kind <> OLD.provider_kind
3007
+ OR NEW.provider_version <> OLD.provider_version OR NEW.target_kind <> OLD.target_kind
3008
+ OR NEW.target_key <> OLD.target_key OR NEW.target_workspace_path <> OLD.target_workspace_path
3009
+ OR NEW.environment_revision <> OLD.environment_revision OR NEW.workspace_mode <> OLD.workspace_mode
3010
+ OR NEW.environment_validation_fingerprint <> OLD.environment_validation_fingerprint
3011
+ OR NEW.execution_profile_revision_id IS NOT OLD.execution_profile_revision_id
3012
+ OR NEW.execution_profile_fingerprint IS NOT OLD.execution_profile_fingerprint
3013
+ OR NEW.credential_requirements_fingerprint IS NOT OLD.credential_requirements_fingerprint
3014
+ OR NEW.template_identity IS NOT OLD.template_identity OR NEW.template_digest IS NOT OLD.template_digest
3015
+ OR NEW.agent_identity IS NOT OLD.agent_identity OR NEW.compatible_agents_json <> OLD.compatible_agents_json
3016
+ OR NEW.created_at <> OLD.created_at
3017
+ BEGIN SELECT RAISE(ABORT, 'Execution environment identity is immutable'); END;
3018
+
3019
+ CREATE TRIGGER validate_execution_environment_binding_identity
3020
+ BEFORE INSERT ON execution_environment_run_bindings
3021
+ WHEN NOT EXISTS (
3022
+ SELECT 1 FROM execution_environments WHERE id = NEW.environment_id AND project_id = NEW.project_id
3023
+ AND environment_revision = NEW.environment_revision
3024
+ AND environment_validation_fingerprint = NEW.environment_validation_fingerprint
3025
+ AND execution_profile_fingerprint IS NEW.execution_profile_fingerprint
3026
+ )
3027
+ BEGIN SELECT RAISE(ABORT, 'Execution environment binding identity does not match'); END;
3028
+
3029
+ CREATE TRIGGER preserve_execution_environment_binding_identity
3030
+ BEFORE UPDATE OF environment_id, execution_run_id, project_id, relationship,
3031
+ environment_revision, environment_validation_fingerprint, execution_profile_fingerprint, bound_at
3032
+ ON execution_environment_run_bindings
3033
+ WHEN NEW.environment_id <> OLD.environment_id OR NEW.execution_run_id <> OLD.execution_run_id
3034
+ OR NEW.project_id <> OLD.project_id OR NEW.relationship <> OLD.relationship
3035
+ OR NEW.environment_revision <> OLD.environment_revision
3036
+ OR NEW.environment_validation_fingerprint <> OLD.environment_validation_fingerprint
3037
+ OR NEW.execution_profile_fingerprint IS NOT OLD.execution_profile_fingerprint
3038
+ OR NEW.bound_at <> OLD.bound_at
3039
+ BEGIN SELECT RAISE(ABORT, 'Execution environment run binding identity is immutable'); END;
3040
+ `);
2303
3041
  db.exec('DROP TRIGGER IF EXISTS validate_execution_environment_binding_authority');
2304
3042
  db.exec(`
2305
3043
  CREATE TRIGGER IF NOT EXISTS validate_project_build_execution_provider_insert
@@ -2344,7 +3082,8 @@ db.exec(`
2344
3082
  SELECT RAISE(ABORT, 'Execution environment binding evidence is immutable');
2345
3083
  END;
2346
3084
  `);
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)');
3085
+ db.exec('DROP INDEX IF EXISTS idx_execution_environments_reuse');
3086
+ 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
3087
  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
3088
  db.exec('CREATE INDEX IF NOT EXISTS idx_execution_environments_cleanup ON execution_environments(lifecycle_state, cleanup_claim_id, updated_at, cleanup_attempts)');
2350
3089
  db.exec('CREATE INDEX IF NOT EXISTS idx_execution_environment_events_sequence ON execution_environment_events(project_id, environment_id, sequence)');
@@ -2677,7 +3416,7 @@ db.exec(`
2677
3416
  CHECK (length(definition_fingerprint) = 64 AND definition_fingerprint NOT GLOB '*[^0-9a-f]*'),
2678
3417
  CHECK (outcome IS NULL OR outcome IN ('passed', 'command_failure', 'spawn_failure',
2679
3418
  'infrastructure_failure', 'evidence_persistence_failure', 'timeout',
2680
- 'workspace_changed_before_start', 'user_stop', 'restart_loss', 'shutdown_loss')),
3419
+ 'workspace_changed_before_start', 'user_stop', 'restart_loss', 'shutdown_loss', 'ambiguous_process_loss')),
2681
3420
  CHECK (output_revision >= 0 AND retained_start_offset >= 0 AND retained_end_offset >= retained_start_offset
2682
3421
  AND total_persisted_bytes >= retained_end_offset),
2683
3422
  CHECK (output_retention_state IN ('retained', 'expired'))
@@ -2688,14 +3427,23 @@ ensureColumn('check_attempt_details', 'retained_output', "TEXT NOT NULL DEFAULT
2688
3427
  const checkAttemptSchema = db.prepare(`
2689
3428
  SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'check_attempt_details'
2690
3429
  `).get();
2691
- if (checkAttemptSchema && !checkAttemptSchema.sql.includes('evidence_persistence_failure')) {
2692
- db.transaction(() => {
2693
- db.exec(`
2694
- DROP TRIGGER IF EXISTS preserve_check_attempt_snapshots;
2695
- DROP TRIGGER IF EXISTS preserve_completed_check_attempt_details;
2696
- ALTER TABLE check_attempt_details RENAME TO check_attempt_details_legacy;
2697
-
2698
- CREATE TABLE check_attempt_details (
3430
+ if (checkAttemptSchema && (!checkAttemptSchema.sql.includes('evidence_persistence_failure')
3431
+ || !checkAttemptSchema.sql.includes('ambiguous_process_loss'))) {
3432
+ const foreignKeysEnabled = db.pragma('foreign_keys', { simple: true }) === 1;
3433
+ if (foreignKeysEnabled) {
3434
+ db.pragma('foreign_keys = OFF');
3435
+ }
3436
+ try {
3437
+ const referencingTriggers = db.prepare(`
3438
+ SELECT name FROM sqlite_master
3439
+ WHERE type = 'trigger' AND sql LIKE '%check_attempt_details%'
3440
+ `).all();
3441
+ for (const trigger of referencingTriggers) {
3442
+ db.exec(`DROP TRIGGER IF EXISTS "${trigger.name.replaceAll('"', '""')}"`);
3443
+ }
3444
+ db.transaction(() => {
3445
+ db.exec(`
3446
+ CREATE TABLE check_attempt_details_replacement (
2699
3447
  execution_run_id TEXT PRIMARY KEY,
2700
3448
  batch_run_id TEXT NOT NULL,
2701
3449
  definition_id TEXT NOT NULL,
@@ -2721,13 +3469,13 @@ if (checkAttemptSchema && !checkAttemptSchema.sql.includes('evidence_persistence
2721
3469
  CHECK (length(definition_fingerprint) = 64 AND definition_fingerprint NOT GLOB '*[^0-9a-f]*'),
2722
3470
  CHECK (outcome IS NULL OR outcome IN ('passed', 'command_failure', 'spawn_failure',
2723
3471
  'infrastructure_failure', 'evidence_persistence_failure', 'timeout',
2724
- 'workspace_changed_before_start', 'user_stop', 'restart_loss', 'shutdown_loss')),
3472
+ 'workspace_changed_before_start', 'user_stop', 'restart_loss', 'shutdown_loss', 'ambiguous_process_loss')),
2725
3473
  CHECK (output_revision >= 0 AND retained_start_offset >= 0 AND retained_end_offset >= retained_start_offset
2726
3474
  AND total_persisted_bytes >= retained_end_offset),
2727
3475
  CHECK (output_retention_state IN ('retained', 'expired'))
2728
3476
  );
2729
3477
 
2730
- INSERT INTO check_attempt_details (
3478
+ INSERT INTO check_attempt_details_replacement (
2731
3479
  execution_run_id, batch_run_id, definition_id, command_snapshot_json, definition_fingerprint,
2732
3480
  initial_workspace_identity_json, final_workspace_identity_json, outcome, rerun_of_attempt_run_id,
2733
3481
  exit_code, exit_signal, exit_error, output_revision, retained_start_offset, retained_end_offset,
@@ -2738,11 +3486,22 @@ if (checkAttemptSchema && !checkAttemptSchema.sql.includes('evidence_persistence
2738
3486
  initial_workspace_identity_json, final_workspace_identity_json, outcome, rerun_of_attempt_run_id,
2739
3487
  exit_code, exit_signal, exit_error, output_revision, retained_start_offset, retained_end_offset,
2740
3488
  total_persisted_bytes, retained_output, output_retention_state
2741
- FROM check_attempt_details_legacy;
3489
+ FROM check_attempt_details;
2742
3490
 
2743
- DROP TABLE check_attempt_details_legacy;
2744
- `);
2745
- })();
3491
+ DROP TABLE check_attempt_details;
3492
+ ALTER TABLE check_attempt_details_replacement RENAME TO check_attempt_details;
3493
+ `);
3494
+ })();
3495
+ }
3496
+ finally {
3497
+ if (foreignKeysEnabled) {
3498
+ db.pragma('foreign_keys = ON');
3499
+ }
3500
+ }
3501
+ const foreignKeyViolation = db.pragma('foreign_key_check')[0];
3502
+ if (foreignKeyViolation) {
3503
+ throw new Error(`Check attempt migration left an invalid foreign key: ${JSON.stringify(foreignKeyViolation)}`);
3504
+ }
2746
3505
  }
2747
3506
  db.exec(`
2748
3507
  CREATE TABLE IF NOT EXISTS check_attempt_diagnostic_parsers (
@@ -4327,6 +5086,11 @@ db.exec('CREATE INDEX IF NOT EXISTS idx_project_links_linked_project_id ON proje
4327
5086
  db.exec('CREATE INDEX IF NOT EXISTS idx_build_sequences_project_status ON build_sequences(project_id, status)');
4328
5087
  db.exec('CREATE INDEX IF NOT EXISTS idx_build_sequences_bundle_status ON build_sequences(worktree_bundle_id, status)');
4329
5088
  db.exec('CREATE INDEX IF NOT EXISTS idx_build_sequence_tickets_sequence_position ON build_sequence_tickets(build_sequence_id, position)');
5089
+ db.exec(`
5090
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_build_sequence_tickets_activation_run
5091
+ ON build_sequence_tickets(activation_execution_run_id)
5092
+ WHERE activation_execution_run_id IS NOT NULL
5093
+ `);
4330
5094
  db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_integration_connections_provider_project ON integration_connections(provider, project_id)');
4331
5095
  db.exec('CREATE INDEX IF NOT EXISTS idx_tickets_external_key ON tickets(external_provider, external_key)');
4332
5096
  db.exec('CREATE INDEX IF NOT EXISTS idx_bundle_automation_state_project_bundle ON bundle_automation_state(project_id, worktree_bundle_id)');