loki-mode 10.7.0 → 10.7.1

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 (96) hide show
  1. package/README.md +1 -0
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/dashboard/__init__.py +1 -1
  5. package/docs/INSTALLATION.md +1 -1
  6. package/docs/v10/CONTROL-PLANE.md +7 -0
  7. package/docs/v10/DECISIONS.md +41 -0
  8. package/docs/v10/ENGINE-LAWS.md +155 -0
  9. package/docs/v10/FAILURE-CLASSES.md +145 -0
  10. package/docs/v10/GUIDE.md +4 -2
  11. package/docs/v10/L0-WAVE1.md +81 -0
  12. package/docs/v10/MORNING-BRIEF-1003.md +11 -25
  13. package/docs/v10/OPERATING-MODEL.md +1 -0
  14. package/docs/v10/PROGRESS.md +28 -0
  15. package/loki-ts/dist/loki.js +802 -781
  16. package/loki-ts/package.json +1 -0
  17. package/mcp/__init__.py +1 -1
  18. package/package.json +1 -1
  19. package/packages/control-plane/dist/server.js +293 -19
  20. package/packages/control-plane/drizzle/0002_tiresome_fixer.sql +7 -0
  21. package/packages/control-plane/drizzle/0003_integrity.sql +4 -0
  22. package/packages/control-plane/drizzle/meta/0002_snapshot.json +409 -0
  23. package/packages/control-plane/drizzle/meta/0003_snapshot.json +437 -0
  24. package/packages/control-plane/drizzle/meta/_journal.json +14 -0
  25. package/packages/control-plane/src/db/migrate.ts +6 -2
  26. package/packages/control-plane/src/db/prune.ts +91 -0
  27. package/packages/control-plane/src/db/schema.ts +13 -0
  28. package/packages/control-plane/src/server/app.ts +20 -1
  29. package/packages/control-plane/src/server/integrity.ts +147 -0
  30. package/packages/control-plane/src/server/runs.ts +46 -4
  31. package/packages/control-plane/src/shipper/discover.ts +32 -3
  32. package/packages/control-plane/src/shipper/ship.ts +15 -0
  33. package/packages/control-plane/src/shipper/watch.ts +3 -1
  34. package/packages/control-plane/ui/dist/assets/index-DHDI_dHM.css +1 -0
  35. package/packages/control-plane/ui/dist/assets/index-DaQr0MPS.js +129 -0
  36. package/packages/control-plane/ui/dist/index.html +2 -2
  37. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  38. package/web-app/dist/assets/{AdminPage-BQPb5rc5.js → AdminPage-BJSU5hY1.js} +1 -1
  39. package/web-app/dist/assets/{Avatar-Cttco6FC.js → Avatar-CSOSpw9c.js} +1 -1
  40. package/web-app/dist/assets/{Badge-CPb6kpbH.js → Badge-COr7vj0D.js} +1 -1
  41. package/web-app/dist/assets/{Button-caTL28a4.js → Button-DGoWFERp.js} +1 -1
  42. package/web-app/dist/assets/{CockpitPage-bEbLx6ph.js → CockpitPage-CBpWkNXY.js} +1 -1
  43. package/web-app/dist/assets/{ComparePage-DNtJSij-.js → ComparePage-BwT8vqrL.js} +1 -1
  44. package/web-app/dist/assets/{ErrorBoundary-BrgC3K44.js → ErrorBoundary-D7SicDDA.js} +1 -1
  45. package/web-app/dist/assets/{EvidenceReceiptPanel-Q-mSrUUQ.js → EvidenceReceiptPanel-bx-5liG2.js} +1 -1
  46. package/web-app/dist/assets/{GitHubIssuesPanel-DbmYl_Er.js → GitHubIssuesPanel-BAeeWLqY.js} +1 -1
  47. package/web-app/dist/assets/{GitHubPRsPanel-C6PCHM4B.js → GitHubPRsPanel-CssD8lGI.js} +1 -1
  48. package/web-app/dist/assets/{HomePage-7zsL_k_e.js → HomePage-SL5PT3m5.js} +2 -2
  49. package/web-app/dist/assets/{LoginPage-Cl4ON8RQ.js → LoginPage-C8hFOZOM.js} +1 -1
  50. package/web-app/dist/assets/{MagicPage-BNEAhTqH.js → MagicPage-xHbHkrPf.js} +1 -1
  51. package/web-app/dist/assets/{MetricsPage-DyBH-88q.js → MetricsPage-B_2CKFcC.js} +1 -1
  52. package/web-app/dist/assets/{NotFoundPage-CIVr21yL.js → NotFoundPage-CXwXMq35.js} +1 -1
  53. package/web-app/dist/assets/{ProjectPage-BMeVYocF.js → ProjectPage-AaisMQI-.js} +3 -3
  54. package/web-app/dist/assets/{ProjectsPage-D-H2WRVU.js → ProjectsPage-Cej5ResX.js} +1 -1
  55. package/web-app/dist/assets/{SettingsPage-o0kR9RAt.js → SettingsPage-DKv4mn9H.js} +1 -1
  56. package/web-app/dist/assets/{ShowcasePage-DtfMDMjp.js → ShowcasePage-Dkiyzne4.js} +1 -1
  57. package/web-app/dist/assets/{SystemSettingsPage-CkXdpZ_T.js → SystemSettingsPage-DaBLQ5dU.js} +1 -1
  58. package/web-app/dist/assets/{TeamsPage-Cu5H9cQ0.js → TeamsPage-B2yUr31k.js} +1 -1
  59. package/web-app/dist/assets/{TemplatesPage-3wdbVwil.js → TemplatesPage-CfYiANhU.js} +1 -1
  60. package/web-app/dist/assets/{TerminalOutput-CWn301xh.js → TerminalOutput-CMXoexLM.js} +1 -1
  61. package/web-app/dist/assets/{activity-YGaAt4zJ.js → activity-B0dzoZtY.js} +1 -1
  62. package/web-app/dist/assets/{bell-C28q0GO7.js → bell-DFOTaSoo.js} +1 -1
  63. package/web-app/dist/assets/{bot-DLcQGI92.js → bot-Dz-WSQyP.js} +1 -1
  64. package/web-app/dist/assets/{check-w0sBNlcm.js → check-L1QIRueJ.js} +1 -1
  65. package/web-app/dist/assets/{chevron-left-W7O0vC_m.js → chevron-left-1TrnIEZS.js} +1 -1
  66. package/web-app/dist/assets/{circle-alert-Cxjq9fxB.js → circle-alert-8Bf6xEDx.js} +1 -1
  67. package/web-app/dist/assets/{clock-C8EqzmNl.js → clock-DB8NE6P5.js} +1 -1
  68. package/web-app/dist/assets/{cloud-xNgp4g3Y.js → cloud-DUSL3wZS.js} +1 -1
  69. package/web-app/dist/assets/{code-xml-B-lyNDLJ.js → code-xml-DbjryWNl.js} +1 -1
  70. package/web-app/dist/assets/{database-DoW5lmWc.js → database-DrezrMSs.js} +1 -1
  71. package/web-app/dist/assets/{dollar-sign-BcARA5sC.js → dollar-sign-CGbaLMzR.js} +1 -1
  72. package/web-app/dist/assets/{file-code-corner-CehcII0J.js → file-code-corner-Dfypa-r4.js} +1 -1
  73. package/web-app/dist/assets/{file-plus-DI9Zo5hP.js → file-plus-CzV5L2Gd.js} +1 -1
  74. package/web-app/dist/assets/{globe-CiZWBk3H.js → globe-BoYjTwPQ.js} +1 -1
  75. package/web-app/dist/assets/{hammer-3qv_T2N-.js → hammer-WO-HuiyJ.js} +1 -1
  76. package/web-app/dist/assets/{index-gF9grAz-.js → index-CjIOUjnd.js} +3 -3
  77. package/web-app/dist/assets/{layers-CN7d0By9.js → layers-Bn6jtKNk.js} +1 -1
  78. package/web-app/dist/assets/{loader-circle-DywcM-im.js → loader-circle-gERWRnmW.js} +1 -1
  79. package/web-app/dist/assets/{lock-_hdSmkU4.js → lock-BIiMLJq8.js} +1 -1
  80. package/web-app/dist/assets/{package-BV5zgqxJ.js → package-dLvDSVys.js} +1 -1
  81. package/web-app/dist/assets/{plus-CU_Hh4-x.js → plus-BHM3ZLhZ.js} +1 -1
  82. package/web-app/dist/assets/{refresh-cw-DMK-fNl-.js → refresh-cw-BAp_alv6.js} +1 -1
  83. package/web-app/dist/assets/{rotate-ccw-CEB3_v5_.js → rotate-ccw-CBhwq_MG.js} +1 -1
  84. package/web-app/dist/assets/{scroll-text-BSdXxcRN.js → scroll-text-DoS-6dfU.js} +1 -1
  85. package/web-app/dist/assets/{server-CAMlP1yJ.js → server-5JgdDWQO.js} +1 -1
  86. package/web-app/dist/assets/{shield-alert-B-r1Dt3h.js → shield-alert-DjmlLT34.js} +1 -1
  87. package/web-app/dist/assets/{thumbs-up-CYfxw9o4.js → thumbs-up-BtiEXl3p.js} +1 -1
  88. package/web-app/dist/assets/{trash-2-DX3nGPS8.js → trash-2-ClsWU_Vg.js} +1 -1
  89. package/web-app/dist/assets/{trending-down-Bt-hSntd.js → trending-down-DPS4M7_X.js} +1 -1
  90. package/web-app/dist/assets/{trending-up-DVSlvQp8.js → trending-up-D6OJ38-V.js} +1 -1
  91. package/web-app/dist/assets/{upload-D45RRHAP.js → upload-DOCzLUpF.js} +1 -1
  92. package/web-app/dist/assets/{usePolling-fq_Xj8iw.js → usePolling-CljQTWj_.js} +1 -1
  93. package/web-app/dist/assets/{user-BWcJkLlr.js → user-C0G82Weu.js} +1 -1
  94. package/web-app/dist/index.html +1 -1
  95. package/packages/control-plane/ui/dist/assets/index-CUQ65vJY.css +0 -1
  96. package/packages/control-plane/ui/dist/assets/index-CwwGfp6E.js +0 -129
@@ -0,0 +1,437 @@
1
+ {
2
+ "version": "6",
3
+ "dialect": "sqlite",
4
+ "id": "c1492d55-5d08-4208-940a-38fba6b33b3c",
5
+ "prevId": "2b4a930a-5fb1-4e32-8387-fac651ba5fca",
6
+ "tables": {
7
+ "audit": {
8
+ "name": "audit",
9
+ "columns": {
10
+ "id": {
11
+ "name": "id",
12
+ "type": "integer",
13
+ "primaryKey": true,
14
+ "notNull": true,
15
+ "autoincrement": true
16
+ },
17
+ "ts": {
18
+ "name": "ts",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true,
22
+ "autoincrement": false
23
+ },
24
+ "action": {
25
+ "name": "action",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": true,
29
+ "autoincrement": false
30
+ },
31
+ "actor": {
32
+ "name": "actor",
33
+ "type": "text",
34
+ "primaryKey": false,
35
+ "notNull": true,
36
+ "autoincrement": false
37
+ },
38
+ "detail": {
39
+ "name": "detail",
40
+ "type": "text",
41
+ "primaryKey": false,
42
+ "notNull": true,
43
+ "autoincrement": false
44
+ }
45
+ },
46
+ "indexes": {},
47
+ "foreignKeys": {},
48
+ "compositePrimaryKeys": {},
49
+ "uniqueConstraints": {},
50
+ "checkConstraints": {}
51
+ },
52
+ "events": {
53
+ "name": "events",
54
+ "columns": {
55
+ "source_id": {
56
+ "name": "source_id",
57
+ "type": "text",
58
+ "primaryKey": false,
59
+ "notNull": true,
60
+ "autoincrement": false
61
+ },
62
+ "run_id": {
63
+ "name": "run_id",
64
+ "type": "text",
65
+ "primaryKey": false,
66
+ "notNull": true,
67
+ "autoincrement": false
68
+ },
69
+ "seq": {
70
+ "name": "seq",
71
+ "type": "integer",
72
+ "primaryKey": false,
73
+ "notNull": true,
74
+ "autoincrement": false
75
+ },
76
+ "ts": {
77
+ "name": "ts",
78
+ "type": "text",
79
+ "primaryKey": false,
80
+ "notNull": true,
81
+ "autoincrement": false
82
+ },
83
+ "type": {
84
+ "name": "type",
85
+ "type": "text",
86
+ "primaryKey": false,
87
+ "notNull": true,
88
+ "autoincrement": false
89
+ },
90
+ "stage": {
91
+ "name": "stage",
92
+ "type": "text",
93
+ "primaryKey": false,
94
+ "notNull": false,
95
+ "autoincrement": false
96
+ },
97
+ "data": {
98
+ "name": "data",
99
+ "type": "text",
100
+ "primaryKey": false,
101
+ "notNull": true,
102
+ "autoincrement": false
103
+ },
104
+ "line_sha256": {
105
+ "name": "line_sha256",
106
+ "type": "text",
107
+ "primaryKey": false,
108
+ "notNull": true,
109
+ "autoincrement": false
110
+ },
111
+ "received_at": {
112
+ "name": "received_at",
113
+ "type": "text",
114
+ "primaryKey": false,
115
+ "notNull": true,
116
+ "autoincrement": false
117
+ }
118
+ },
119
+ "indexes": {
120
+ "events_run_seq": {
121
+ "name": "events_run_seq",
122
+ "columns": [
123
+ "run_id",
124
+ "seq"
125
+ ],
126
+ "isUnique": false
127
+ },
128
+ "events_type_ts": {
129
+ "name": "events_type_ts",
130
+ "columns": [
131
+ "type",
132
+ "ts"
133
+ ],
134
+ "isUnique": false
135
+ }
136
+ },
137
+ "foreignKeys": {},
138
+ "compositePrimaryKeys": {
139
+ "events_source_id_run_id_seq_pk": {
140
+ "columns": [
141
+ "source_id",
142
+ "run_id",
143
+ "seq"
144
+ ],
145
+ "name": "events_source_id_run_id_seq_pk"
146
+ }
147
+ },
148
+ "uniqueConstraints": {},
149
+ "checkConstraints": {}
150
+ },
151
+ "runs": {
152
+ "name": "runs",
153
+ "columns": {
154
+ "source_id": {
155
+ "name": "source_id",
156
+ "type": "text",
157
+ "primaryKey": false,
158
+ "notNull": true,
159
+ "autoincrement": false
160
+ },
161
+ "run_id": {
162
+ "name": "run_id",
163
+ "type": "text",
164
+ "primaryKey": false,
165
+ "notNull": true,
166
+ "autoincrement": false
167
+ },
168
+ "origin_repo": {
169
+ "name": "origin_repo",
170
+ "type": "text",
171
+ "primaryKey": false,
172
+ "notNull": false,
173
+ "autoincrement": false
174
+ },
175
+ "issue_ref": {
176
+ "name": "issue_ref",
177
+ "type": "text",
178
+ "primaryKey": false,
179
+ "notNull": false,
180
+ "autoincrement": false
181
+ },
182
+ "task_source": {
183
+ "name": "task_source",
184
+ "type": "text",
185
+ "primaryKey": false,
186
+ "notNull": false,
187
+ "autoincrement": false
188
+ },
189
+ "provider": {
190
+ "name": "provider",
191
+ "type": "text",
192
+ "primaryKey": false,
193
+ "notNull": false,
194
+ "autoincrement": false
195
+ },
196
+ "model": {
197
+ "name": "model",
198
+ "type": "text",
199
+ "primaryKey": false,
200
+ "notNull": false,
201
+ "autoincrement": false
202
+ },
203
+ "started_at": {
204
+ "name": "started_at",
205
+ "type": "text",
206
+ "primaryKey": false,
207
+ "notNull": false,
208
+ "autoincrement": false
209
+ },
210
+ "ended_at": {
211
+ "name": "ended_at",
212
+ "type": "text",
213
+ "primaryKey": false,
214
+ "notNull": false,
215
+ "autoincrement": false
216
+ },
217
+ "verdict": {
218
+ "name": "verdict",
219
+ "type": "text",
220
+ "primaryKey": false,
221
+ "notNull": false,
222
+ "autoincrement": false
223
+ },
224
+ "pr_url": {
225
+ "name": "pr_url",
226
+ "type": "text",
227
+ "primaryKey": false,
228
+ "notNull": false,
229
+ "autoincrement": false
230
+ },
231
+ "pr_draft": {
232
+ "name": "pr_draft",
233
+ "type": "integer",
234
+ "primaryKey": false,
235
+ "notNull": false,
236
+ "autoincrement": false
237
+ },
238
+ "cost_usd": {
239
+ "name": "cost_usd",
240
+ "type": "real",
241
+ "primaryKey": false,
242
+ "notNull": false,
243
+ "autoincrement": false
244
+ },
245
+ "partial_usd": {
246
+ "name": "partial_usd",
247
+ "type": "real",
248
+ "primaryKey": false,
249
+ "notNull": true,
250
+ "autoincrement": false
251
+ },
252
+ "measured_sessions": {
253
+ "name": "measured_sessions",
254
+ "type": "integer",
255
+ "primaryKey": false,
256
+ "notNull": true,
257
+ "autoincrement": false
258
+ },
259
+ "total_sessions": {
260
+ "name": "total_sessions",
261
+ "type": "integer",
262
+ "primaryKey": false,
263
+ "notNull": true,
264
+ "autoincrement": false
265
+ },
266
+ "input_tokens": {
267
+ "name": "input_tokens",
268
+ "type": "integer",
269
+ "primaryKey": false,
270
+ "notNull": true,
271
+ "autoincrement": false
272
+ },
273
+ "output_tokens": {
274
+ "name": "output_tokens",
275
+ "type": "integer",
276
+ "primaryKey": false,
277
+ "notNull": true,
278
+ "autoincrement": false
279
+ },
280
+ "wall_s": {
281
+ "name": "wall_s",
282
+ "type": "real",
283
+ "primaryKey": false,
284
+ "notNull": false,
285
+ "autoincrement": false
286
+ },
287
+ "last_seq": {
288
+ "name": "last_seq",
289
+ "type": "integer",
290
+ "primaryKey": false,
291
+ "notNull": true,
292
+ "autoincrement": false
293
+ },
294
+ "last_event_at": {
295
+ "name": "last_event_at",
296
+ "type": "text",
297
+ "primaryKey": false,
298
+ "notNull": false,
299
+ "autoincrement": false
300
+ },
301
+ "tampered": {
302
+ "name": "tampered",
303
+ "type": "integer",
304
+ "primaryKey": false,
305
+ "notNull": true,
306
+ "autoincrement": false
307
+ },
308
+ "conflict": {
309
+ "name": "conflict",
310
+ "type": "integer",
311
+ "primaryKey": false,
312
+ "notNull": true,
313
+ "autoincrement": false,
314
+ "default": 0
315
+ },
316
+ "group_id": {
317
+ "name": "group_id",
318
+ "type": "text",
319
+ "primaryKey": false,
320
+ "notNull": false,
321
+ "autoincrement": false
322
+ },
323
+ "unit_id": {
324
+ "name": "unit_id",
325
+ "type": "text",
326
+ "primaryKey": false,
327
+ "notNull": false,
328
+ "autoincrement": false
329
+ },
330
+ "attested": {
331
+ "name": "attested",
332
+ "type": "integer",
333
+ "primaryKey": false,
334
+ "notNull": false,
335
+ "autoincrement": false
336
+ },
337
+ "integrity_reasons": {
338
+ "name": "integrity_reasons",
339
+ "type": "text",
340
+ "primaryKey": false,
341
+ "notNull": false,
342
+ "autoincrement": false
343
+ },
344
+ "sig_checked": {
345
+ "name": "sig_checked",
346
+ "type": "integer",
347
+ "primaryKey": false,
348
+ "notNull": false,
349
+ "autoincrement": false
350
+ },
351
+ "integrity_key_fp": {
352
+ "name": "integrity_key_fp",
353
+ "type": "text",
354
+ "primaryKey": false,
355
+ "notNull": false,
356
+ "autoincrement": false
357
+ }
358
+ },
359
+ "indexes": {
360
+ "runs_started": {
361
+ "name": "runs_started",
362
+ "columns": [
363
+ "started_at"
364
+ ],
365
+ "isUnique": false
366
+ },
367
+ "runs_verdict": {
368
+ "name": "runs_verdict",
369
+ "columns": [
370
+ "verdict"
371
+ ],
372
+ "isUnique": false
373
+ },
374
+ "runs_group": {
375
+ "name": "runs_group",
376
+ "columns": [
377
+ "group_id"
378
+ ],
379
+ "isUnique": false
380
+ }
381
+ },
382
+ "foreignKeys": {},
383
+ "compositePrimaryKeys": {
384
+ "runs_source_id_run_id_pk": {
385
+ "columns": [
386
+ "source_id",
387
+ "run_id"
388
+ ],
389
+ "name": "runs_source_id_run_id_pk"
390
+ }
391
+ },
392
+ "uniqueConstraints": {},
393
+ "checkConstraints": {}
394
+ },
395
+ "sources": {
396
+ "name": "sources",
397
+ "columns": {
398
+ "id": {
399
+ "name": "id",
400
+ "type": "text",
401
+ "primaryKey": true,
402
+ "notNull": true,
403
+ "autoincrement": false
404
+ },
405
+ "first_seen": {
406
+ "name": "first_seen",
407
+ "type": "text",
408
+ "primaryKey": false,
409
+ "notNull": true,
410
+ "autoincrement": false
411
+ },
412
+ "last_seen": {
413
+ "name": "last_seen",
414
+ "type": "text",
415
+ "primaryKey": false,
416
+ "notNull": true,
417
+ "autoincrement": false
418
+ }
419
+ },
420
+ "indexes": {},
421
+ "foreignKeys": {},
422
+ "compositePrimaryKeys": {},
423
+ "uniqueConstraints": {},
424
+ "checkConstraints": {}
425
+ }
426
+ },
427
+ "views": {},
428
+ "enums": {},
429
+ "_meta": {
430
+ "schemas": {},
431
+ "tables": {},
432
+ "columns": {}
433
+ },
434
+ "internal": {
435
+ "indexes": {}
436
+ }
437
+ }
@@ -15,6 +15,20 @@
15
15
  "when": 1790956800000,
16
16
  "tag": "0001_group_id",
17
17
  "breakpoints": true
18
+ },
19
+ {
20
+ "idx": 2,
21
+ "version": "6",
22
+ "when": 1791046012148,
23
+ "tag": "0002_tiresome_fixer",
24
+ "breakpoints": true
25
+ },
26
+ {
27
+ "idx": 3,
28
+ "version": "6",
29
+ "when": 1791132412148,
30
+ "tag": "0003_integrity",
31
+ "breakpoints": true
18
32
  }
19
33
  ]
20
34
  }
@@ -12,8 +12,12 @@ export function openDb(path: string) {
12
12
  const sqlite = new Database(path, { create: true });
13
13
  sqlite.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;");
14
14
  const db = drizzle(sqlite, { schema });
15
- // src/db/ (source) and dist/ (bundled server) sit at different depths under packages/control-plane
16
- const migrationsFolder = [join(import.meta.dir, "../../drizzle"), join(import.meta.dir, "../drizzle")].find((d) => existsSync(join(d, "meta/_journal.json")));
15
+ // source, the bundled server and the bundled CLI sit at different depths
16
+ const migrationsFolder = [
17
+ join(import.meta.dir, "../../drizzle"), // src/db (source checkout)
18
+ join(import.meta.dir, "../drizzle"), // packages/control-plane/dist (bundled server)
19
+ join(import.meta.dir, "../../packages/control-plane/drizzle"), // loki-ts/dist (bundled CLI; also an installed npm package)
20
+ ].find((d) => existsSync(join(d, "meta/_journal.json")));
17
21
  if (!migrationsFolder) throw new Error("drizzle migrations folder not found");
18
22
  migrate(db, { migrationsFolder });
19
23
  return { db, sqlite };
@@ -0,0 +1,91 @@
1
+ // Removal of runs from the control DB: shared by `loki control prune` (direct DB) and DELETE /v1/runs/:source/:run (server).
2
+ // One transaction deletes the runs, their events, and any source left with neither. An audit row is written first, inside the same transaction.
3
+ import { Database } from "bun:sqlite";
4
+
5
+ export interface PruneFilter { repo?: string; before?: string }
6
+ export interface PruneCounts { runs: number; events: number; sources: number }
7
+
8
+ /** Strict ISO 8601 parse: YYYY-MM-DD or a date-time with an explicit Z/offset. Returns a normalized UTC instant or null. */
9
+ export function parseBefore(s: string): string | null {
10
+ if (!/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2}))?$/.test(s)) return null;
11
+ const t = Date.parse(s);
12
+ if (Number.isNaN(t)) return null;
13
+ // reject rolled-over dates such as 2026-02-31
14
+ if (new Date(`${s.slice(0, 10)}T00:00:00Z`).toISOString().slice(0, 10) !== s.slice(0, 10)) return null;
15
+ return new Date(t).toISOString();
16
+ }
17
+
18
+ type Key = { source_id: string; run_id: string };
19
+
20
+ function where(f: PruneFilter): { sql: string; args: string[] } {
21
+ const parts: string[] = [];
22
+ const args: string[] = [];
23
+ if (f.repo !== undefined) { parts.push("origin_repo = ?"); args.push(f.repo); }
24
+ if (f.before !== undefined) { parts.push("started_at IS NOT NULL AND julianday(started_at) < julianday(?)"); args.push(f.before); }
25
+ return { sql: parts.join(" AND "), args };
26
+ }
27
+
28
+ const n = (sqlite: Database, sql: string, ...args: string[]): number => (sqlite.query(sql).get(...args) as { n: number }).n;
29
+
30
+ /** What deleting these runs would remove: runs, their events, and sources that would be left holding nothing. */
31
+ function countFor(sqlite: Database, keys: Key[]): PruneCounts {
32
+ const bySource = new Map<string, string[]>();
33
+ for (const k of keys) bySource.set(k.source_id, [...(bySource.get(k.source_id) ?? []), k.run_id]);
34
+ let events = 0, sources = 0;
35
+ for (const [src, runIds] of bySource) {
36
+ let ev = 0;
37
+ for (const r of runIds) ev += n(sqlite, "select count(*) n from events where source_id = ? and run_id = ?", src, r);
38
+ events += ev;
39
+ const runsLeft = n(sqlite, "select count(*) n from runs where source_id = ?", src) - runIds.length;
40
+ const eventsLeft = n(sqlite, "select count(*) n from events where source_id = ?", src) - ev;
41
+ if (runsLeft <= 0 && eventsLeft <= 0 && sqlite.query("select 1 x from sources where id = ?").get(src)) sources++;
42
+ }
43
+ return { runs: keys.length, events, sources };
44
+ }
45
+
46
+ export function audit(sqlite: Database, action: string, actor: string, detail: unknown): void {
47
+ sqlite.query("insert into audit (ts, action, actor, detail) values (?, ?, ?, ?)").run(new Date().toISOString(), action, actor, JSON.stringify(detail));
48
+ }
49
+
50
+ /** Drops only the given sources, and only when they now hold neither runs nor events (matches what countFor reported). */
51
+ const dropOrphanSources = (sqlite: Database, ids: Iterable<string>): void => {
52
+ for (const id of ids) sqlite.query("delete from sources where id = ? and not exists (select 1 from runs where source_id = ?) and not exists (select 1 from events where source_id = ?)").run(id, id, id);
53
+ };
54
+
55
+ /** Read-only handle for dry runs: no migrations, no journal-mode change, no writes. */
56
+ export const openReadOnly = (path: string): Database => new Database(path, { readonly: true });
57
+
58
+ /** Removes the matching runs. dryRun counts only and writes nothing. At least one filter is required. */
59
+ export function pruneRuns(sqlite: Database, f: PruneFilter, opts: { dryRun?: boolean; actor?: string } = {}): PruneCounts {
60
+ if (f.repo === undefined && f.before === undefined) throw new Error("prune needs --repo or --before");
61
+ const w = where(f);
62
+ const select = (): Key[] => sqlite.query(`select source_id, run_id from runs where ${w.sql}`).all(...w.args) as Key[];
63
+ if (opts.dryRun) return countFor(sqlite, select());
64
+ let out: PruneCounts = { runs: 0, events: 0, sources: 0 };
65
+ sqlite.transaction(() => {
66
+ const keys = select();
67
+ if (keys.length === 0) return;
68
+ out = countFor(sqlite, keys);
69
+ audit(sqlite, "prune", opts.actor ?? "cli", { filter: f, counts: out });
70
+ for (const k of keys) {
71
+ sqlite.query("delete from events where source_id = ? and run_id = ?").run(k.source_id, k.run_id);
72
+ sqlite.query("delete from runs where source_id = ? and run_id = ?").run(k.source_id, k.run_id);
73
+ }
74
+ dropOrphanSources(sqlite, new Set(keys.map((k) => k.source_id)));
75
+ }).immediate();
76
+ return out;
77
+ }
78
+
79
+ /** Removes one run. Null when it does not exist (nothing audited, nothing deleted). Otherwise the counts removed. */
80
+ export function removeRun(sqlite: Database, source: string, run: string, actor = "http"): PruneCounts | null {
81
+ let out: PruneCounts | null = null;
82
+ sqlite.transaction(() => {
83
+ if (!sqlite.query("select 1 x from runs where source_id = ? and run_id = ?").get(source, run)) return;
84
+ out = countFor(sqlite, [{ source_id: source, run_id: run }]);
85
+ audit(sqlite, "run.remove", actor, { source_id: source, run_id: run, counts: out });
86
+ sqlite.query("delete from events where source_id = ? and run_id = ?").run(source, run);
87
+ sqlite.query("delete from runs where source_id = ? and run_id = ?").run(source, run);
88
+ dropOrphanSources(sqlite, [source]);
89
+ }).immediate();
90
+ return out;
91
+ }
@@ -49,9 +49,22 @@ export const runs = sqliteTable("runs", {
49
49
  conflict: integer("conflict").notNull().default(0),
50
50
  groupId: text("group_id"),
51
51
  unitId: text("unit_id"),
52
+ attested: integer("attested"), // EL-FC08b: null = never evaluated (reads as unattested)
53
+ sigChecked: integer("sig_checked"), // log seal signature verified against a configured key
54
+ integrityKeyFp: text("integrity_key_fp"), // key-set fingerprint the integrity result was computed under; a change triggers a boot recompute
55
+ integrityReasons: text("integrity_reasons"), // JSON string[]
52
56
  }, (t) => [
53
57
  primaryKey({ columns: [t.sourceId, t.runId] }),
54
58
  index("runs_started").on(t.startedAt),
55
59
  index("runs_verdict").on(t.verdict),
56
60
  index("runs_group").on(t.groupId),
57
61
  ]);
62
+
63
+ // Append-only record of destructive operator actions (run removal, prune). Written before the delete runs.
64
+ export const audit = sqliteTable("audit", {
65
+ id: integer("id").primaryKey({ autoIncrement: true }),
66
+ ts: text("ts").notNull(),
67
+ action: text("action").notNull(),
68
+ actor: text("actor").notNull(),
69
+ detail: text("detail", { mode: "json" }).notNull(),
70
+ });
@@ -4,9 +4,10 @@ import { Hono, type Context } from "hono";
4
4
  import { openDb } from "../db/migrate.ts";
5
5
  import { ingest } from "./ingest.ts";
6
6
  import { defaultAnswerDir, writeAnswer } from "./answer.ts";
7
- import { listRuns, runDetail } from "./runs.ts";
7
+ import { listRuns, recomputeLegacy, runDetail } from "./runs.ts";
8
8
  import { hostGuard, isLoopbackHost, tokenGuard } from "./auth.ts";
9
9
  import { backfill } from "../shipper/backfill.ts";
10
+ import { removeRun } from "../db/prune.ts";
10
11
  import { planStart, registryRepos, spawnStart } from "./actions.ts";
11
12
 
12
13
  const MAX_BODY = 1_000_000;
@@ -18,6 +19,7 @@ const defaultUiDir = () => [join(import.meta.dir, "../../ui/dist"), join(import.
18
19
  export function createApp(opts: { dbPath: string; uiDir?: string; answerDir?: string; token?: string; loopbackOnly?: boolean; repoDir?: string; startBin?: string; spawnImpl?: typeof spawnStart }) {
19
20
  const uiDir = opts.uiDir ?? defaultUiDir();
20
21
  const { db, sqlite } = openDb(opts.dbPath);
22
+ recomputeLegacy(db);
21
23
  let ready = true;
22
24
  const answerDir = opts.answerDir ?? defaultAnswerDir();
23
25
  const app = new Hono();
@@ -87,6 +89,23 @@ export function createApp(opts: { dbPath: string; uiDir?: string; answerDir?: st
87
89
  });
88
90
  return c.json({ runs: r.runs, sent: r.sent, failed: r.failed });
89
91
  });
92
+ // Remove one run (and its events). Loopback-only like /v1/start (never registered on a non-loopback server); bearer via tokenGuard when a token is set. JSON content type blocks cross-site form posts;
93
+ // a browser Origin must match the host this server is served on. Absent Origin (curl, the CLI) is allowed. The audit row is written inside the delete transaction, before the rows go.
94
+ act.delete("/v1/runs/:source/:run", async (c) => {
95
+ if (!peerIsLoopback(c) || !isLoopbackHost(c.req.header("host"))) return c.json({ error: "loopback only" }, 403);
96
+ if (!(c.req.header("content-type") ?? "").toLowerCase().startsWith("application/json")) return c.json({ error: "content-type must be application/json" }, 400);
97
+ const origin = c.req.header("origin");
98
+ if (origin !== undefined) {
99
+ let oh = "";
100
+ try { oh = new URL(origin).host.toLowerCase(); } catch { /* refused below */ }
101
+ if (!oh || oh !== (c.req.header("host") ?? "").toLowerCase()) return c.json({ error: "origin not allowed" }, 403);
102
+ }
103
+ const source = c.req.param("source"), run = c.req.param("run");
104
+ const r = removeRun(sqlite, source, run);
105
+ if (!r) return c.json({ error: "run not found" }, 404);
106
+ const left = (sqlite.query("select count(*) n from runs").get() as { n: number }).n;
107
+ return c.json({ ok: true, removed: r, remaining_runs: left });
108
+ });
90
109
  act.get("/v1/repos", (c) => peerIsLoopback(c) ? c.json({ repos: [...new Set<string>([repoDir, ...registryRepos()])] }) : c.json({ error: "loopback only" }, 403));
91
110
  act.post("/v1/start", async (c) => {
92
111
  if (!local(c)) return c.json({ error: "loopback JSON requests only" }, 403);