@straiffi/archon 1.8.21 → 1.8.23

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 (133) hide show
  1. package/dist/client/assets/{ActivityView-BcguXTZ5.js → ActivityView-BaIZ8eHC.js} +1 -1
  2. package/dist/client/assets/GitHubActionsDialog-BFCqikDY.js +1 -0
  3. package/dist/client/assets/VerificationDialog-B1_6HMdt.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-DnC7Twj0.js → architectureDiagram-3BPJPVTR-Dl5UF67P.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-D0VbjiKd.js → blockDiagram-GPEHLZMM-DCcERfrZ.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-CzQ3l15K.js → c4Diagram-AAUBKEIU-BGhbASFm.js} +1 -1
  7. package/dist/client/assets/channel-BfskvH2B.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-B5UGiqLW.js → chunk-2J33WTMH-C1ARozTV.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-SCksTqtu.js → chunk-3OPIFGDE-BEovLizF.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-IOkwLOtG.js → chunk-5ZQYHXKU-C5MmlLa_.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-Do60Xs9p.js → chunk-727SXJPM-8y8jBk4n.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-CuQT3imB.js → chunk-AQP2D5EJ-q-mByAd3.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-BkxMIESX.js → chunk-CSCIHK7Q-BOx2mLSz.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-uUU3sWgR.js → chunk-KSCS5N6A-zEwQFq7q.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-DLYccV6J.js → chunk-L5ZTLDWV-C80z6rY_.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-CgJFfCRZ.js → chunk-LZXEDZCA-BLaEsGPd.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-D7IQSQfK.js → chunk-ND2GUHAM-x26BZ95L.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-B4O6JWAo.js → chunk-NZK2D7GU-D2WAxr1p.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-DidlQEqb.js → chunk-O5CBEL6O-DDPCrzKh.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-2-oq_Gue.js → chunk-WU5MYG2G-CE-JcvIc.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-D4HPfxL3.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DQQ_WqAU.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-TubuN0hR.js → dagre-BM42HDAG-Bf7wpUk_.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-DNcUk6rV.js → diagram-2AECGRRQ-BQoKmc3B.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-B1sJn4w_.js → diagram-5GNKFQAL-D2jarPLn.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-CnW6ZuQe.js → diagram-KO2AKTUF-Bcbbjvsl.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-YHUmDzyS.js → diagram-LMA3HP47-t862RRBQ.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-BkOQphxq.js → diagram-OG6HWLK6-CacbMcO0.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-oNMxAdxN.js → erDiagram-TEJ5UH35-DRQ79baI.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-ByuCV-Am.js → flowDiagram-I6XJVG4X-BfW4KEdc.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRkNH8Ft.js → ganttDiagram-6RSMTGT7-BPIl0TAj.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DRXzXjd8.js → gitGraphDiagram-PVQCEYII-CRIusBMV.js} +1 -1
  33. package/dist/client/assets/index-CZ1z72fo.js +276 -0
  34. package/dist/client/assets/index-SMj5ZmMs.css +2 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-CJ4-hWch.js → infoDiagram-5YYISTIA-DTKzK8EJ.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CW1xqvim.js → ishikawaDiagram-YF4QCWOH-DVIHZLvL.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-BVVFg8_h.js → journeyDiagram-JHISSGLW-CQBZ4tCx.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-AcLZ63tA.js → kanban-definition-UN3LZRKU-D7naegR0.js} +1 -1
  39. package/dist/client/assets/{line-CG_TG6Ci.js → line-CizZlaeV.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-UDLSm_c3.js → mermaid-parser.core-NrDqgpZa.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-IvBk22DF.js → mermaid.core-CwVO3oX0.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BflWNtnl.js → mindmap-definition-RKZ34NQL-BWxy3Aa_.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-Dm1iGMm7.js → pieDiagram-4H26LBE5-B3gXpmBn.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-CM4zJd_R.js → quadrantDiagram-W4KKPZXB-Dpp4g8Al.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-miuP86cp.js → requirementDiagram-4Y6WPE33-lFKXnxml.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-zUcRagtr.js → sankeyDiagram-5OEKKPKP-BBdtwqrS.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BRYqVpsT.js → sequenceDiagram-3UESZ5HK-ANpVZzqm.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-DwcF0DJm.js → stateDiagram-AJRCARHV-D58ZxmKY.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Cv8cmDfz.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-BzH929s_.js → timeline-definition-PNZ67QCA-B6FK8IQC.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-BIq8rooy.js → vennDiagram-CIIHVFJN-lFWtZEEY.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-CyV_gL5J.js → wardleyDiagram-YWT4CUSO-DteAVp4l.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-Dxo-U-qe.js → xychartDiagram-2RQKCTM6-CNcDlRLN.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +359 -21
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +152 -4
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/approvalReconciliation.js +101 -0
  60. package/dist/server/lib/approvalReconciliation.js.map +1 -0
  61. package/dist/server/lib/approvalRequests.js +587 -0
  62. package/dist/server/lib/approvalRequests.js.map +1 -0
  63. package/dist/server/lib/approvalRoutes.js +143 -0
  64. package/dist/server/lib/approvalRoutes.js.map +1 -0
  65. package/dist/server/lib/buildExecutionEnvironment.js +128 -11
  66. package/dist/server/lib/buildExecutionEnvironment.js.map +1 -1
  67. package/dist/server/lib/checkDefinitions.js +29 -1
  68. package/dist/server/lib/checkDefinitions.js.map +1 -1
  69. package/dist/server/lib/checkRunner.js +306 -27
  70. package/dist/server/lib/checkRunner.js.map +1 -1
  71. package/dist/server/lib/checkRuns.js +4 -2
  72. package/dist/server/lib/checkRuns.js.map +1 -1
  73. package/dist/server/lib/consequentialActionAuthority.js +142 -0
  74. package/dist/server/lib/consequentialActionAuthority.js.map +1 -0
  75. package/dist/server/lib/consequentialActions.js +231 -0
  76. package/dist/server/lib/consequentialActions.js.map +1 -0
  77. package/dist/server/lib/dockerSandboxEnvironmentOperations.js +119 -0
  78. package/dist/server/lib/dockerSandboxEnvironmentOperations.js.map +1 -1
  79. package/dist/server/lib/dockerSandboxExecutionEnvironment.js +194 -14
  80. package/dist/server/lib/dockerSandboxExecutionEnvironment.js.map +1 -1
  81. package/dist/server/lib/dockerSandboxExecutionProvider.js +1 -1
  82. package/dist/server/lib/dockerSandboxExecutionProvider.js.map +1 -1
  83. package/dist/server/lib/dockerSandboxNetworkPolicy.js +222 -0
  84. package/dist/server/lib/dockerSandboxNetworkPolicy.js.map +1 -0
  85. package/dist/server/lib/executionEnvironmentContexts.js +4 -2
  86. package/dist/server/lib/executionEnvironmentContexts.js.map +1 -1
  87. package/dist/server/lib/executionEnvironmentLifecycle.js +34 -0
  88. package/dist/server/lib/executionEnvironmentLifecycle.js.map +1 -1
  89. package/dist/server/lib/executionEnvironmentRoutes.js +68 -6
  90. package/dist/server/lib/executionEnvironmentRoutes.js.map +1 -1
  91. package/dist/server/lib/executionEnvironmentValidation.js +71 -5
  92. package/dist/server/lib/executionEnvironmentValidation.js.map +1 -1
  93. package/dist/server/lib/executionEnvironmentValidationOperations.js +155 -11
  94. package/dist/server/lib/executionEnvironmentValidationOperations.js.map +1 -1
  95. package/dist/server/lib/executionEnvironments.js +11 -5
  96. package/dist/server/lib/executionEnvironments.js.map +1 -1
  97. package/dist/server/lib/executionProfileStore.js +12 -3
  98. package/dist/server/lib/executionProfileStore.js.map +1 -1
  99. package/dist/server/lib/executionProfiles.js +131 -8
  100. package/dist/server/lib/executionProfiles.js.map +1 -1
  101. package/dist/server/lib/executionReconciliation.js +206 -143
  102. package/dist/server/lib/executionReconciliation.js.map +1 -1
  103. package/dist/server/lib/executionRunAuthority.js +175 -7
  104. package/dist/server/lib/executionRunAuthority.js.map +1 -1
  105. package/dist/server/lib/executionRuns.js +26 -4
  106. package/dist/server/lib/executionRuns.js.map +1 -1
  107. package/dist/server/lib/executionTimeline.js +2 -1
  108. package/dist/server/lib/executionTimeline.js.map +1 -1
  109. package/dist/server/lib/git.js +52 -10
  110. package/dist/server/lib/git.js.map +1 -1
  111. package/dist/server/lib/gitPushApprovals.js +367 -0
  112. package/dist/server/lib/gitPushApprovals.js.map +1 -0
  113. package/dist/server/lib/mobileAccess.js +115 -1
  114. package/dist/server/lib/mobileAccess.js.map +1 -1
  115. package/dist/server/lib/productionCheckApprovals.js +106 -0
  116. package/dist/server/lib/productionCheckApprovals.js.map +1 -0
  117. package/dist/server/lib/reviewFindings.js +14 -8
  118. package/dist/server/lib/reviewFindings.js.map +1 -1
  119. package/dist/server/lib/types.js +40 -0
  120. package/dist/server/lib/types.js.map +1 -1
  121. package/dist/server/lib/workspaceProcessEnvironment.js +2 -2
  122. package/dist/server/lib/workspaceProcessEnvironment.js.map +1 -1
  123. package/dist/server/workers/review.js +19 -11
  124. package/dist/server/workers/review.js.map +1 -1
  125. package/package.json +1 -1
  126. package/dist/client/assets/GitHubActionsDialog-BGecTzoq.js +0 -1
  127. package/dist/client/assets/VerificationDialog-D4QkMLZr.js +0 -4
  128. package/dist/client/assets/channel-BwRO4uiw.js +0 -1
  129. package/dist/client/assets/classDiagram-4FO5ZUOK-BIszcze-.js +0 -1
  130. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-D5dvxldS.js +0 -1
  131. package/dist/client/assets/index-CYPgEn1h.css +0 -2
  132. package/dist/client/assets/index-Ck2OKMsl.js +0 -276
  133. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DmICWsmn.js +0 -1
package/dist/server/db.js CHANGED
@@ -3,7 +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
+ import { createExecutionProfile, EXECUTION_PROFILE_SCHEMA_VERSION, fingerprintExecutionProfile, normalizeExecutionProfile, serializeExecutionProfile, } from './lib/executionProfiles.js';
7
7
  const dbPath = ensureParentDirectory(resolveArchonDbPath());
8
8
  const db = new Database(dbPath);
9
9
  db.pragma('foreign_keys = ON');
@@ -181,7 +181,7 @@ db.exec(`
181
181
  id TEXT PRIMARY KEY,
182
182
  project_id TEXT NOT NULL,
183
183
  revision INTEGER NOT NULL CHECK (revision > 0),
184
- schema_version INTEGER NOT NULL CHECK (schema_version = 1),
184
+ schema_version INTEGER NOT NULL CHECK (schema_version IN (1, 2)),
185
185
  fingerprint TEXT NOT NULL CHECK (length(fingerprint) = 64),
186
186
  profile_json TEXT NOT NULL CHECK (
187
187
  json_valid(profile_json)
@@ -220,6 +220,46 @@ db.exec(`
220
220
  CREATE INDEX IF NOT EXISTS idx_execution_profile_audit_project
221
221
  ON execution_profile_audit_events(project_id, created_at DESC, id DESC);
222
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
+ }
223
263
  const backfillExecutionProfiles = () => {
224
264
  const projects = db.prepare(`
225
265
  SELECT id, build_execution_provider, active_execution_profile_revision_id
@@ -231,7 +271,7 @@ const backfillExecutionProfiles = () => {
231
271
  const insertRevision = db.prepare(`
232
272
  INSERT OR IGNORE INTO execution_profile_revisions (
233
273
  id, project_id, revision, schema_version, fingerprint, profile_json
234
- ) VALUES (?, ?, 1, 1, ?, ?)
274
+ ) VALUES (?, ?, 1, ${EXECUTION_PROFILE_SCHEMA_VERSION}, ?, ?)
235
275
  `);
236
276
  const activateRevision = db.prepare(`
237
277
  UPDATE projects SET active_execution_profile_revision_id = ? WHERE id = ?
@@ -257,7 +297,70 @@ const backfillExecutionProfiles = () => {
257
297
  }
258
298
  })();
259
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
+ };
260
362
  backfillExecutionProfiles();
363
+ backfillExecutionProfilesToSchemaV2();
261
364
  const hostCompatibilityProfile = createExecutionProfile('host');
262
365
  const hostCompatibilityProfileJson = serializeExecutionProfile(hostCompatibilityProfile).replaceAll("'", "''");
263
366
  const hostCompatibilityFingerprint = fingerprintExecutionProfile(hostCompatibilityProfile);
@@ -272,7 +375,7 @@ db.exec(`
272
375
  INSERT INTO execution_profile_revisions (
273
376
  id, project_id, revision, schema_version, fingerprint, profile_json
274
377
  ) VALUES (
275
- NEW.id || ':execution-profile:1', NEW.id, 1, 1,
378
+ NEW.id || ':execution-profile:1', NEW.id, 1, ${EXECUTION_PROFILE_SCHEMA_VERSION},
276
379
  CASE WHEN NEW.build_execution_provider = 'docker_sandbox'
277
380
  THEN '${dockerProfileFingerprint}' ELSE '${hostCompatibilityFingerprint}' END,
278
381
  CASE WHEN NEW.build_execution_provider = 'docker_sandbox'
@@ -2092,6 +2195,7 @@ db.exec(`
2092
2195
  stage TEXT NOT NULL,
2093
2196
  diff_signature TEXT,
2094
2197
  workspace_signature TEXT,
2198
+ change_scope TEXT,
2095
2199
  message_id TEXT,
2096
2200
  model TEXT,
2097
2201
  variant TEXT,
@@ -2109,6 +2213,7 @@ const migrateReviewRunsLifecycleSchema = () => {
2109
2213
  const workspaceSignatureColumn = columns.find(column => column.name === 'workspace_signature');
2110
2214
  const needsMigration = diffSignatureColumn?.notnull === 1
2111
2215
  || workspaceSignatureColumn?.notnull === 1
2216
+ || !columns.some(column => column.name === 'change_scope')
2112
2217
  || !columns.some(column => column.name === 'model')
2113
2218
  || !columns.some(column => column.name === 'variant')
2114
2219
  || !columns.some(column => column.name === 'failure_message')
@@ -2129,6 +2234,7 @@ const migrateReviewRunsLifecycleSchema = () => {
2129
2234
  stage TEXT NOT NULL,
2130
2235
  diff_signature TEXT,
2131
2236
  workspace_signature TEXT,
2237
+ change_scope TEXT,
2132
2238
  message_id TEXT,
2133
2239
  model TEXT,
2134
2240
  variant TEXT,
@@ -2143,12 +2249,13 @@ const migrateReviewRunsLifecycleSchema = () => {
2143
2249
  db.exec(`
2144
2250
  INSERT INTO review_runs_v2 (
2145
2251
  id, ticket_id, project_id, worktree_bundle_id, stage, diff_signature,
2146
- workspace_signature, message_id, model, variant, failure_message,
2252
+ workspace_signature, change_scope, message_id, model, variant, failure_message,
2147
2253
  status, created_at, updated_at
2148
2254
  )
2149
2255
  SELECT
2150
2256
  id, ticket_id, project_id, worktree_bundle_id, stage, diff_signature,
2151
- ${selectColumn('workspace_signature', 'NULL')}, message_id,
2257
+ ${selectColumn('workspace_signature', 'NULL')},
2258
+ ${selectColumn('change_scope', 'NULL')}, message_id,
2152
2259
  ${selectColumn('model', 'NULL')}, ${selectColumn('variant', 'NULL')},
2153
2260
  ${selectColumn('failure_message', 'NULL')}, status, created_at,
2154
2261
  ${selectColumn('updated_at', 'created_at')}
@@ -2614,6 +2721,217 @@ db.exec(`
2614
2721
  END;
2615
2722
 
2616
2723
  `);
2724
+ db.exec(`
2725
+ CREATE TABLE IF NOT EXISTS approval_requests (
2726
+ id TEXT PRIMARY KEY,
2727
+ project_id TEXT NOT NULL,
2728
+ execution_run_id TEXT NOT NULL,
2729
+ action_kind TEXT NOT NULL,
2730
+ action_schema_version INTEGER NOT NULL,
2731
+ action_json TEXT NOT NULL,
2732
+ action_fingerprint TEXT NOT NULL,
2733
+ authority_schema_version INTEGER NOT NULL,
2734
+ authority_json TEXT NOT NULL,
2735
+ status TEXT NOT NULL DEFAULT 'pending',
2736
+ expires_at DATETIME NOT NULL,
2737
+ decision_at DATETIME,
2738
+ consumed_at DATETIME,
2739
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2740
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2741
+ UNIQUE (id, project_id),
2742
+ UNIQUE (execution_run_id),
2743
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
2744
+ FOREIGN KEY (execution_run_id, project_id) REFERENCES execution_runs(id, project_id) ON DELETE CASCADE,
2745
+ CHECK (length(id) BETWEEN 1 AND 128),
2746
+ CHECK (action_kind IN ('git_push', 'custom_check')),
2747
+ CHECK (action_schema_version = 1),
2748
+ CHECK (length(CAST(action_json AS BLOB)) BETWEEN 2 AND 65536),
2749
+ CHECK (length(action_fingerprint) = 64 AND action_fingerprint NOT GLOB '*[^0-9a-f]*'),
2750
+ CHECK (authority_schema_version = 1),
2751
+ CHECK (length(CAST(authority_json AS BLOB)) BETWEEN 2 AND 16384),
2752
+ CHECK (status IN ('pending', 'approved', 'denied', 'cancelled', 'expired', 'invalidated', 'consumed')),
2753
+ CHECK (status NOT IN ('approved', 'denied', 'consumed') OR decision_at IS NOT NULL),
2754
+ CHECK (status <> 'pending' OR decision_at IS NULL),
2755
+ CHECK ((status = 'consumed') = (consumed_at IS NOT NULL))
2756
+ );
2757
+
2758
+ CREATE TABLE IF NOT EXISTS approval_request_events (
2759
+ id TEXT PRIMARY KEY,
2760
+ project_id TEXT NOT NULL,
2761
+ approval_request_id TEXT NOT NULL,
2762
+ sequence INTEGER NOT NULL,
2763
+ event_type TEXT NOT NULL,
2764
+ from_status TEXT,
2765
+ to_status TEXT NOT NULL,
2766
+ actor_classification TEXT NOT NULL,
2767
+ actor_reference TEXT,
2768
+ reason TEXT,
2769
+ metadata_schema_version INTEGER NOT NULL DEFAULT 1,
2770
+ metadata_json TEXT,
2771
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2772
+ UNIQUE (approval_request_id, sequence),
2773
+ FOREIGN KEY (approval_request_id, project_id) REFERENCES approval_requests(id, project_id) ON DELETE CASCADE,
2774
+ CHECK (sequence >= 1),
2775
+ CHECK (length(event_type) BETWEEN 1 AND 100),
2776
+ CHECK (from_status IS NULL OR from_status IN ('pending', 'approved', 'denied', 'cancelled', 'expired', 'invalidated', 'consumed')),
2777
+ CHECK (to_status IN ('pending', 'approved', 'denied', 'cancelled', 'expired', 'invalidated', 'consumed')),
2778
+ CHECK (actor_classification IN ('human', 'system', 'action_executor')),
2779
+ CHECK (actor_reference IS NULL OR length(CAST(actor_reference AS BLOB)) BETWEEN 1 AND 200),
2780
+ CHECK (reason IS NULL OR length(CAST(reason AS BLOB)) <= 2000),
2781
+ CHECK (metadata_schema_version = 1),
2782
+ CHECK (metadata_json IS NULL OR length(CAST(metadata_json AS BLOB)) <= 4096)
2783
+ );
2784
+
2785
+ CREATE TRIGGER IF NOT EXISTS preserve_approval_request_identity
2786
+ BEFORE UPDATE OF id, project_id, execution_run_id, action_kind, action_schema_version,
2787
+ action_json, action_fingerprint, authority_schema_version, authority_json, expires_at, created_at
2788
+ ON approval_requests
2789
+ BEGIN
2790
+ SELECT RAISE(ABORT, 'Approval request identity and evidence are immutable');
2791
+ END;
2792
+
2793
+ CREATE TRIGGER IF NOT EXISTS preserve_approval_request_decision_evidence
2794
+ BEFORE UPDATE OF decision_at, consumed_at ON approval_requests
2795
+ WHEN (OLD.decision_at IS NOT NULL AND NEW.decision_at IS NOT OLD.decision_at)
2796
+ OR (OLD.consumed_at IS NOT NULL AND NEW.consumed_at IS NOT OLD.consumed_at)
2797
+ BEGIN
2798
+ SELECT RAISE(ABORT, 'Approval request decision evidence is immutable');
2799
+ END;
2800
+
2801
+ CREATE TRIGGER IF NOT EXISTS validate_approval_request_status_transition
2802
+ BEFORE UPDATE OF status ON approval_requests
2803
+ WHEN NOT (
2804
+ (OLD.status = 'pending' AND NEW.status IN ('approved', 'denied', 'cancelled', 'expired', 'invalidated'))
2805
+ OR (OLD.status = 'approved' AND NEW.status IN ('consumed', 'expired', 'invalidated'))
2806
+ )
2807
+ BEGIN
2808
+ SELECT RAISE(ABORT, 'Invalid approval request status transition');
2809
+ END;
2810
+
2811
+ CREATE TRIGGER IF NOT EXISTS preserve_approval_request_events
2812
+ BEFORE UPDATE ON approval_request_events
2813
+ BEGIN
2814
+ SELECT RAISE(ABORT, 'Approval request events are append-only');
2815
+ END;
2816
+
2817
+ CREATE TRIGGER IF NOT EXISTS prevent_approval_request_event_deletion
2818
+ BEFORE DELETE ON approval_request_events
2819
+ WHEN EXISTS (
2820
+ SELECT 1 FROM projects
2821
+ WHERE id = OLD.project_id AND deletion_started_at IS NULL
2822
+ )
2823
+ BEGIN
2824
+ SELECT RAISE(ABORT, 'Approval request events are append-only');
2825
+ END;
2826
+
2827
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_project_created
2828
+ ON approval_requests(project_id, created_at DESC, id DESC);
2829
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_project_status_expiry
2830
+ ON approval_requests(project_id, status, expires_at, id);
2831
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_action_kind
2832
+ ON approval_requests(project_id, action_kind, created_at DESC, id DESC);
2833
+ CREATE INDEX IF NOT EXISTS idx_approval_request_events_sequence
2834
+ ON approval_request_events(project_id, approval_request_id, sequence ASC);
2835
+ `);
2836
+ const migrateApprovalRequestActionLimit = () => {
2837
+ const tableSql = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'approval_requests'").get()?.sql ?? '';
2838
+ if (tableSql.includes('BETWEEN 2 AND 65536')) {
2839
+ return;
2840
+ }
2841
+ db.exec('PRAGMA foreign_keys = OFF');
2842
+ try {
2843
+ db.exec('BEGIN');
2844
+ db.exec(`
2845
+ CREATE TABLE approval_requests_v2 (
2846
+ id TEXT PRIMARY KEY,
2847
+ project_id TEXT NOT NULL,
2848
+ execution_run_id TEXT NOT NULL,
2849
+ action_kind TEXT NOT NULL,
2850
+ action_schema_version INTEGER NOT NULL,
2851
+ action_json TEXT NOT NULL,
2852
+ action_fingerprint TEXT NOT NULL,
2853
+ authority_schema_version INTEGER NOT NULL,
2854
+ authority_json TEXT NOT NULL,
2855
+ status TEXT NOT NULL DEFAULT 'pending',
2856
+ expires_at DATETIME NOT NULL,
2857
+ decision_at DATETIME,
2858
+ consumed_at DATETIME,
2859
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2860
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
2861
+ UNIQUE (id, project_id),
2862
+ UNIQUE (execution_run_id),
2863
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
2864
+ FOREIGN KEY (execution_run_id, project_id) REFERENCES execution_runs(id, project_id) ON DELETE CASCADE,
2865
+ CHECK (length(id) BETWEEN 1 AND 128),
2866
+ CHECK (action_kind IN ('git_push', 'custom_check')),
2867
+ CHECK (action_schema_version = 1),
2868
+ CHECK (length(CAST(action_json AS BLOB)) BETWEEN 2 AND 65536),
2869
+ CHECK (length(action_fingerprint) = 64 AND action_fingerprint NOT GLOB '*[^0-9a-f]*'),
2870
+ CHECK (authority_schema_version = 1),
2871
+ CHECK (length(CAST(authority_json AS BLOB)) BETWEEN 2 AND 16384),
2872
+ CHECK (status IN ('pending', 'approved', 'denied', 'cancelled', 'expired', 'invalidated', 'consumed')),
2873
+ CHECK (status NOT IN ('approved', 'denied', 'consumed') OR decision_at IS NOT NULL),
2874
+ CHECK (status <> 'pending' OR decision_at IS NULL),
2875
+ CHECK ((status = 'consumed') = (consumed_at IS NOT NULL))
2876
+ );
2877
+ INSERT INTO approval_requests_v2 (
2878
+ id, project_id, execution_run_id, action_kind, action_schema_version, action_json,
2879
+ action_fingerprint, authority_schema_version, authority_json, status, expires_at,
2880
+ decision_at, consumed_at, created_at, updated_at
2881
+ )
2882
+ SELECT
2883
+ id, project_id, execution_run_id, action_kind, action_schema_version, action_json,
2884
+ action_fingerprint, authority_schema_version, authority_json, status, expires_at,
2885
+ decision_at, consumed_at, created_at, updated_at
2886
+ FROM approval_requests;
2887
+ DROP TABLE approval_requests;
2888
+ ALTER TABLE approval_requests_v2 RENAME TO approval_requests;
2889
+ `);
2890
+ db.exec('COMMIT');
2891
+ }
2892
+ catch (error) {
2893
+ db.exec('ROLLBACK');
2894
+ throw error;
2895
+ }
2896
+ finally {
2897
+ db.exec('PRAGMA foreign_keys = ON');
2898
+ }
2899
+ db.exec(`
2900
+ CREATE TRIGGER preserve_approval_request_identity
2901
+ BEFORE UPDATE OF id, project_id, execution_run_id, action_kind, action_schema_version,
2902
+ action_json, action_fingerprint, authority_schema_version, authority_json, expires_at, created_at
2903
+ ON approval_requests
2904
+ BEGIN
2905
+ SELECT RAISE(ABORT, 'Approval request identity and evidence are immutable');
2906
+ END;
2907
+
2908
+ CREATE TRIGGER preserve_approval_request_decision_evidence
2909
+ BEFORE UPDATE OF decision_at, consumed_at ON approval_requests
2910
+ WHEN (OLD.decision_at IS NOT NULL AND NEW.decision_at IS NOT OLD.decision_at)
2911
+ OR (OLD.consumed_at IS NOT NULL AND NEW.consumed_at IS NOT OLD.consumed_at)
2912
+ BEGIN
2913
+ SELECT RAISE(ABORT, 'Approval request decision evidence is immutable');
2914
+ END;
2915
+
2916
+ CREATE TRIGGER validate_approval_request_status_transition
2917
+ BEFORE UPDATE OF status ON approval_requests
2918
+ WHEN NOT (
2919
+ (OLD.status = 'pending' AND NEW.status IN ('approved', 'denied', 'cancelled', 'expired', 'invalidated'))
2920
+ OR (OLD.status = 'approved' AND NEW.status IN ('consumed', 'expired', 'invalidated'))
2921
+ )
2922
+ BEGIN
2923
+ SELECT RAISE(ABORT, 'Invalid approval request status transition');
2924
+ END;
2925
+
2926
+ CREATE INDEX idx_approval_requests_project_created
2927
+ ON approval_requests(project_id, created_at DESC, id DESC);
2928
+ CREATE INDEX idx_approval_requests_project_status_expiry
2929
+ ON approval_requests(project_id, status, expires_at, id);
2930
+ CREATE INDEX idx_approval_requests_action_kind
2931
+ ON approval_requests(project_id, action_kind, created_at DESC, id DESC);
2932
+ `);
2933
+ };
2934
+ migrateApprovalRequestActionLimit();
2617
2935
  db.exec(`
2618
2936
  CREATE TABLE IF NOT EXISTS build_setup_evidence (
2619
2937
  id TEXT PRIMARY KEY,
@@ -3102,7 +3420,7 @@ db.exec(`
3102
3420
  CHECK (length(definition_fingerprint) = 64 AND definition_fingerprint NOT GLOB '*[^0-9a-f]*'),
3103
3421
  CHECK (outcome IS NULL OR outcome IN ('passed', 'command_failure', 'spawn_failure',
3104
3422
  'infrastructure_failure', 'evidence_persistence_failure', 'timeout',
3105
- 'workspace_changed_before_start', 'user_stop', 'restart_loss', 'shutdown_loss')),
3423
+ 'workspace_changed_before_start', 'user_stop', 'restart_loss', 'shutdown_loss', 'ambiguous_process_loss')),
3106
3424
  CHECK (output_revision >= 0 AND retained_start_offset >= 0 AND retained_end_offset >= retained_start_offset
3107
3425
  AND total_persisted_bytes >= retained_end_offset),
3108
3426
  CHECK (output_retention_state IN ('retained', 'expired'))
@@ -3113,14 +3431,23 @@ ensureColumn('check_attempt_details', 'retained_output', "TEXT NOT NULL DEFAULT
3113
3431
  const checkAttemptSchema = db.prepare(`
3114
3432
  SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'check_attempt_details'
3115
3433
  `).get();
3116
- if (checkAttemptSchema && !checkAttemptSchema.sql.includes('evidence_persistence_failure')) {
3117
- db.transaction(() => {
3118
- db.exec(`
3119
- DROP TRIGGER IF EXISTS preserve_check_attempt_snapshots;
3120
- DROP TRIGGER IF EXISTS preserve_completed_check_attempt_details;
3121
- ALTER TABLE check_attempt_details RENAME TO check_attempt_details_legacy;
3122
-
3123
- CREATE TABLE check_attempt_details (
3434
+ if (checkAttemptSchema && (!checkAttemptSchema.sql.includes('evidence_persistence_failure')
3435
+ || !checkAttemptSchema.sql.includes('ambiguous_process_loss'))) {
3436
+ const foreignKeysEnabled = db.pragma('foreign_keys', { simple: true }) === 1;
3437
+ if (foreignKeysEnabled) {
3438
+ db.pragma('foreign_keys = OFF');
3439
+ }
3440
+ try {
3441
+ const referencingTriggers = db.prepare(`
3442
+ SELECT name FROM sqlite_master
3443
+ WHERE type = 'trigger' AND sql LIKE '%check_attempt_details%'
3444
+ `).all();
3445
+ for (const trigger of referencingTriggers) {
3446
+ db.exec(`DROP TRIGGER IF EXISTS "${trigger.name.replaceAll('"', '""')}"`);
3447
+ }
3448
+ db.transaction(() => {
3449
+ db.exec(`
3450
+ CREATE TABLE check_attempt_details_replacement (
3124
3451
  execution_run_id TEXT PRIMARY KEY,
3125
3452
  batch_run_id TEXT NOT NULL,
3126
3453
  definition_id TEXT NOT NULL,
@@ -3146,13 +3473,13 @@ if (checkAttemptSchema && !checkAttemptSchema.sql.includes('evidence_persistence
3146
3473
  CHECK (length(definition_fingerprint) = 64 AND definition_fingerprint NOT GLOB '*[^0-9a-f]*'),
3147
3474
  CHECK (outcome IS NULL OR outcome IN ('passed', 'command_failure', 'spawn_failure',
3148
3475
  'infrastructure_failure', 'evidence_persistence_failure', 'timeout',
3149
- 'workspace_changed_before_start', 'user_stop', 'restart_loss', 'shutdown_loss')),
3476
+ 'workspace_changed_before_start', 'user_stop', 'restart_loss', 'shutdown_loss', 'ambiguous_process_loss')),
3150
3477
  CHECK (output_revision >= 0 AND retained_start_offset >= 0 AND retained_end_offset >= retained_start_offset
3151
3478
  AND total_persisted_bytes >= retained_end_offset),
3152
3479
  CHECK (output_retention_state IN ('retained', 'expired'))
3153
3480
  );
3154
3481
 
3155
- INSERT INTO check_attempt_details (
3482
+ INSERT INTO check_attempt_details_replacement (
3156
3483
  execution_run_id, batch_run_id, definition_id, command_snapshot_json, definition_fingerprint,
3157
3484
  initial_workspace_identity_json, final_workspace_identity_json, outcome, rerun_of_attempt_run_id,
3158
3485
  exit_code, exit_signal, exit_error, output_revision, retained_start_offset, retained_end_offset,
@@ -3163,11 +3490,22 @@ if (checkAttemptSchema && !checkAttemptSchema.sql.includes('evidence_persistence
3163
3490
  initial_workspace_identity_json, final_workspace_identity_json, outcome, rerun_of_attempt_run_id,
3164
3491
  exit_code, exit_signal, exit_error, output_revision, retained_start_offset, retained_end_offset,
3165
3492
  total_persisted_bytes, retained_output, output_retention_state
3166
- FROM check_attempt_details_legacy;
3493
+ FROM check_attempt_details;
3167
3494
 
3168
- DROP TABLE check_attempt_details_legacy;
3169
- `);
3170
- })();
3495
+ DROP TABLE check_attempt_details;
3496
+ ALTER TABLE check_attempt_details_replacement RENAME TO check_attempt_details;
3497
+ `);
3498
+ })();
3499
+ }
3500
+ finally {
3501
+ if (foreignKeysEnabled) {
3502
+ db.pragma('foreign_keys = ON');
3503
+ }
3504
+ }
3505
+ const foreignKeyViolation = db.pragma('foreign_key_check')[0];
3506
+ if (foreignKeyViolation) {
3507
+ throw new Error(`Check attempt migration left an invalid foreign key: ${JSON.stringify(foreignKeyViolation)}`);
3508
+ }
3171
3509
  }
3172
3510
  db.exec(`
3173
3511
  CREATE TABLE IF NOT EXISTS check_attempt_diagnostic_parsers (