loki-mode 10.6.14 → 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 (123) hide show
  1. package/README.md +9 -5
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/acp-agent.py +128 -0
  5. package/autonomy/docker-run.sh +8 -0
  6. package/autonomy/issue-parser.sh +7 -7
  7. package/autonomy/issue-providers.sh +159 -1
  8. package/autonomy/lib/engine10-push.sh +4 -1
  9. package/autonomy/lib/review_risk.py +91 -0
  10. package/autonomy/loki +209 -19
  11. package/autonomy/mobile-verify.sh +66 -0
  12. package/autonomy/run.sh +44 -2
  13. package/completions/_loki +2 -0
  14. package/completions/loki.bash +1 -1
  15. package/dashboard/__init__.py +1 -1
  16. package/dashboard/api_runs_v1.py +80 -0
  17. package/dashboard/server.py +2 -0
  18. package/docs/INSTALLATION.md +5 -1
  19. package/docs/acp-editors.md +29 -0
  20. package/docs/api-runs-v1.md +12 -0
  21. package/docs/dev/architecture-reference.md +4 -0
  22. package/docs/merge-queue.md +13 -0
  23. package/docs/mobile-verify.md +11 -0
  24. package/docs/sentry-intake.md +13 -0
  25. package/docs/v10/BOARD.md +31 -6
  26. package/docs/v10/CONTROL-PLANE.md +7 -0
  27. package/docs/v10/CP-ENTERPRISE-UI.md +276 -0
  28. package/docs/v10/D61-SPEED.md +4 -0
  29. package/docs/v10/DECISIONS.md +115 -0
  30. package/docs/v10/ENGINE-LAWS.md +155 -0
  31. package/docs/v10/FAILURE-CLASSES.md +145 -0
  32. package/docs/v10/FOUNDER-QUEUE.md +1 -0
  33. package/docs/v10/GAP-RESEARCH-1003.md +32 -0
  34. package/docs/v10/GUIDE.md +4 -2
  35. package/docs/v10/L0-WAVE1.md +81 -0
  36. package/docs/v10/MORNING-BRIEF-1003.md +25 -0
  37. package/docs/v10/OPERATING-MODEL.md +1 -0
  38. package/docs/v10/PROGRESS.md +99 -0
  39. package/docs/v10/SWARM.md +32 -0
  40. package/docs/visual-evidence.md +5 -0
  41. package/loki-ts/dist/loki.js +805 -777
  42. package/loki-ts/package.json +1 -0
  43. package/mcp/__init__.py +1 -1
  44. package/package.json +1 -1
  45. package/packages/control-plane/dist/server.js +549 -21
  46. package/packages/control-plane/drizzle/0002_tiresome_fixer.sql +7 -0
  47. package/packages/control-plane/drizzle/0003_integrity.sql +4 -0
  48. package/packages/control-plane/drizzle/meta/0002_snapshot.json +409 -0
  49. package/packages/control-plane/drizzle/meta/0003_snapshot.json +437 -0
  50. package/packages/control-plane/drizzle/meta/_journal.json +14 -0
  51. package/packages/control-plane/src/db/migrate.ts +6 -2
  52. package/packages/control-plane/src/db/prune.ts +91 -0
  53. package/packages/control-plane/src/db/schema.ts +13 -0
  54. package/packages/control-plane/src/server/actions.ts +60 -0
  55. package/packages/control-plane/src/server/app.ts +59 -4
  56. package/packages/control-plane/src/server/integrity.ts +147 -0
  57. package/packages/control-plane/src/server/runs.ts +46 -4
  58. package/packages/control-plane/src/shipper/discover.ts +32 -3
  59. package/packages/control-plane/src/shipper/ship.ts +15 -0
  60. package/packages/control-plane/src/shipper/watch.ts +66 -0
  61. package/packages/control-plane/ui/dist/assets/index-DHDI_dHM.css +1 -0
  62. package/packages/control-plane/ui/dist/assets/index-DaQr0MPS.js +129 -0
  63. package/packages/control-plane/ui/dist/index.html +3 -3
  64. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  65. package/web-app/dist/assets/{AdminPage-oAFMNfKH.js → AdminPage-BJSU5hY1.js} +1 -1
  66. package/web-app/dist/assets/{Avatar-BA2gnzyf.js → Avatar-CSOSpw9c.js} +1 -1
  67. package/web-app/dist/assets/{Badge-Dsluv8n1.js → Badge-COr7vj0D.js} +1 -1
  68. package/web-app/dist/assets/{Button-WmW8A-iG.js → Button-DGoWFERp.js} +1 -1
  69. package/web-app/dist/assets/{CockpitPage-BR5o_RgL.js → CockpitPage-CBpWkNXY.js} +1 -1
  70. package/web-app/dist/assets/{ComparePage-3Eg-pKvO.js → ComparePage-BwT8vqrL.js} +1 -1
  71. package/web-app/dist/assets/{ErrorBoundary-DAfrnnsQ.js → ErrorBoundary-D7SicDDA.js} +1 -1
  72. package/web-app/dist/assets/{EvidenceReceiptPanel-wLZI4OTr.js → EvidenceReceiptPanel-bx-5liG2.js} +1 -1
  73. package/web-app/dist/assets/{GitHubIssuesPanel-BOuadisf.js → GitHubIssuesPanel-BAeeWLqY.js} +1 -1
  74. package/web-app/dist/assets/{GitHubPRsPanel-BTVoVpB3.js → GitHubPRsPanel-CssD8lGI.js} +1 -1
  75. package/web-app/dist/assets/{HomePage-s3GWdNEe.js → HomePage-SL5PT3m5.js} +2 -2
  76. package/web-app/dist/assets/{LoginPage-BfOZ566t.js → LoginPage-C8hFOZOM.js} +1 -1
  77. package/web-app/dist/assets/{MagicPage-9EZ83xOt.js → MagicPage-xHbHkrPf.js} +1 -1
  78. package/web-app/dist/assets/{MetricsPage-OJMOYhHP.js → MetricsPage-B_2CKFcC.js} +1 -1
  79. package/web-app/dist/assets/{NotFoundPage-_LoJ6KEv.js → NotFoundPage-CXwXMq35.js} +1 -1
  80. package/web-app/dist/assets/{ProjectPage-Duj5LSt6.js → ProjectPage-AaisMQI-.js} +3 -3
  81. package/web-app/dist/assets/{ProjectsPage-BBud3JrO.js → ProjectsPage-Cej5ResX.js} +1 -1
  82. package/web-app/dist/assets/{SettingsPage-C4zdgKno.js → SettingsPage-DKv4mn9H.js} +1 -1
  83. package/web-app/dist/assets/{ShowcasePage-Nb3ACjMR.js → ShowcasePage-Dkiyzne4.js} +1 -1
  84. package/web-app/dist/assets/{SystemSettingsPage-BDi49TDh.js → SystemSettingsPage-DaBLQ5dU.js} +1 -1
  85. package/web-app/dist/assets/{TeamsPage-HkcqkjLa.js → TeamsPage-B2yUr31k.js} +1 -1
  86. package/web-app/dist/assets/{TemplatesPage-CK796Y1F.js → TemplatesPage-CfYiANhU.js} +1 -1
  87. package/web-app/dist/assets/{TerminalOutput-BkvdNuT6.js → TerminalOutput-CMXoexLM.js} +1 -1
  88. package/web-app/dist/assets/{activity-DFgMgETR.js → activity-B0dzoZtY.js} +1 -1
  89. package/web-app/dist/assets/{bell-BInGGOMZ.js → bell-DFOTaSoo.js} +1 -1
  90. package/web-app/dist/assets/{bot-D43Yjsj4.js → bot-Dz-WSQyP.js} +1 -1
  91. package/web-app/dist/assets/{check-DFAGTLM9.js → check-L1QIRueJ.js} +1 -1
  92. package/web-app/dist/assets/{chevron-left-Drwin7n1.js → chevron-left-1TrnIEZS.js} +1 -1
  93. package/web-app/dist/assets/{circle-alert-DLWdb7V9.js → circle-alert-8Bf6xEDx.js} +1 -1
  94. package/web-app/dist/assets/{clock-Dsbi59u5.js → clock-DB8NE6P5.js} +1 -1
  95. package/web-app/dist/assets/{cloud-CUU-dXTP.js → cloud-DUSL3wZS.js} +1 -1
  96. package/web-app/dist/assets/{code-xml-CqIh7Wui.js → code-xml-DbjryWNl.js} +1 -1
  97. package/web-app/dist/assets/{database-DASN5Bfh.js → database-DrezrMSs.js} +1 -1
  98. package/web-app/dist/assets/{dollar-sign-ZvXzAihc.js → dollar-sign-CGbaLMzR.js} +1 -1
  99. package/web-app/dist/assets/{file-code-corner-ED_gN-N8.js → file-code-corner-Dfypa-r4.js} +1 -1
  100. package/web-app/dist/assets/{file-plus-BmBHndXt.js → file-plus-CzV5L2Gd.js} +1 -1
  101. package/web-app/dist/assets/{globe-Ck_iEzO4.js → globe-BoYjTwPQ.js} +1 -1
  102. package/web-app/dist/assets/{hammer-B8YKMard.js → hammer-WO-HuiyJ.js} +1 -1
  103. package/web-app/dist/assets/{index-D7SxmjVQ.js → index-CjIOUjnd.js} +3 -3
  104. package/web-app/dist/assets/{layers-CGVsj3wB.js → layers-Bn6jtKNk.js} +1 -1
  105. package/web-app/dist/assets/{loader-circle-FIH7UJDF.js → loader-circle-gERWRnmW.js} +1 -1
  106. package/web-app/dist/assets/{lock-vR8HoAnK.js → lock-BIiMLJq8.js} +1 -1
  107. package/web-app/dist/assets/{package-mZ6LFwXp.js → package-dLvDSVys.js} +1 -1
  108. package/web-app/dist/assets/{plus-CtnSVu0w.js → plus-BHM3ZLhZ.js} +1 -1
  109. package/web-app/dist/assets/{refresh-cw-BIapF6Gj.js → refresh-cw-BAp_alv6.js} +1 -1
  110. package/web-app/dist/assets/{rotate-ccw-LMkSsm21.js → rotate-ccw-CBhwq_MG.js} +1 -1
  111. package/web-app/dist/assets/{scroll-text-DQVlIhr9.js → scroll-text-DoS-6dfU.js} +1 -1
  112. package/web-app/dist/assets/{server-GqyhQxmz.js → server-5JgdDWQO.js} +1 -1
  113. package/web-app/dist/assets/{shield-alert-BrFTNJ-I.js → shield-alert-DjmlLT34.js} +1 -1
  114. package/web-app/dist/assets/{thumbs-up-lgwAEJFW.js → thumbs-up-BtiEXl3p.js} +1 -1
  115. package/web-app/dist/assets/{trash-2-C1cNysK4.js → trash-2-ClsWU_Vg.js} +1 -1
  116. package/web-app/dist/assets/{trending-down-DQFZ6j5s.js → trending-down-DPS4M7_X.js} +1 -1
  117. package/web-app/dist/assets/{trending-up-BJG_Zo8V.js → trending-up-D6OJ38-V.js} +1 -1
  118. package/web-app/dist/assets/{upload-BOY1aTpA.js → upload-DOCzLUpF.js} +1 -1
  119. package/web-app/dist/assets/{usePolling-DoAmsTNy.js → usePolling-CljQTWj_.js} +1 -1
  120. package/web-app/dist/assets/{user-iOfCe0_v.js → user-C0G82Weu.js} +1 -1
  121. package/web-app/dist/index.html +1 -1
  122. package/packages/control-plane/ui/dist/assets/index-CFfrqCx2.css +0 -1
  123. package/packages/control-plane/ui/dist/assets/index-Jt31nrK-.js +0 -114
@@ -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
+ });
@@ -0,0 +1,60 @@
1
+ // Start-a-run and repo-picker helpers for the UI. A run is started by spawning the same `loki start` path an operator would type (argv array, no shell).
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync, readFileSync, statSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+
7
+ const ISSUE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}\/[A-Za-z0-9._-]{1,100}#[1-9][0-9]{0,8}$/;
8
+ // A free-text task: letters, digits, spaces and a small punctuation set. No colon, quotes, backticks, $, ;, &, |, <, >, parens, braces, backslash or control chars.
9
+ const TASK = /^[A-Za-z0-9][A-Za-z0-9 .,_/#@+=-]{0,499}$/;
10
+ const hasDotDot = (t: string): boolean => t.split(/[\\/ ]/).some((seg) => seg.startsWith(".."));
11
+
12
+ /** Child env: the server's own secrets and bind config never reach a spawned run. */
13
+ export function childEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
14
+ const e: NodeJS.ProcessEnv = { ...env, LOKI_NO_BROWSER: "1" };
15
+ for (const k of ["LOKI_CONTROL_TOKEN", "LOKI_CONTROL_DB", "LOKI_CONTROL_HOST", "PORT"]) delete e[k];
16
+ return e;
17
+ }
18
+
19
+ export type StartPlan = { ok: true; argv: string[]; cwd: string } | { ok: false; error: string };
20
+
21
+ /** Registered project paths from ~/.loki/dashboard/projects.json (the `loki projects` registry), existing directories only. */
22
+ export function registryRepos(env: NodeJS.ProcessEnv = process.env): string[] {
23
+ try {
24
+ const j = JSON.parse(readFileSync(join(env.HOME || homedir(), ".loki", "dashboard", "projects.json"), "utf8")) as { projects?: Record<string, { path?: unknown }> };
25
+ return Object.values(j.projects ?? {}).map((p) => (typeof p?.path === "string" ? p.path : "")).filter((p) => p && existsSync(p) && statSync(p).isDirectory());
26
+ } catch { return []; }
27
+ }
28
+
29
+ /** Validate a start request. The repo must be one the server already knows (registry or cwd), never an arbitrary path. */
30
+ export function planStart(body: unknown, known: string[], bin = "loki"): StartPlan {
31
+ const b = (body && typeof body === "object" ? body : {}) as { target?: unknown; repo?: unknown };
32
+ if (typeof b.target !== "string") return { ok: false, error: "target must be a string" };
33
+ const target = b.target.trim();
34
+ const issue = ISSUE.test(target);
35
+ if (!issue && !TASK.test(target)) return { ok: false, error: "target must be owner/repo#N or a plain task (letters, digits, spaces, . , _ / # @ + = -)" };
36
+ if (hasDotDot(target)) return { ok: false, error: "target must not contain a .. path segment" };
37
+ let cwd = process.cwd();
38
+ if (b.repo !== undefined && b.repo !== "") {
39
+ if (typeof b.repo !== "string" || b.repo.includes("\0")) return { ok: false, error: "repo must be a path string" };
40
+ const want = resolve(b.repo);
41
+ const hit = known.map((k) => resolve(k)).find((k) => k === want);
42
+ if (!hit) return { ok: false, error: "repo is not a known project" };
43
+ cwd = hit;
44
+ }
45
+ // Free text goes as an explicit brief, never auto-detected as a PRD path.
46
+ return { ok: true, argv: issue ? [bin, "start", target] : [bin, "start", "--brief", target], cwd };
47
+ }
48
+
49
+ /** Spawn detached with no shell; resolves with the pid once the process exists, or an error if it cannot be launched. */
50
+ export function spawnStart(argv: string[], cwd: string, onExit: () => void = () => {}): Promise<{ pid: number } | { error: string }> {
51
+ return new Promise((done) => {
52
+ try {
53
+ const [cmd, ...args] = argv;
54
+ const child = spawn(cmd!, args, { cwd, detached: true, stdio: "ignore", shell: false, env: childEnv() });
55
+ child.once("exit", onExit);
56
+ child.once("error", (e) => { onExit(); done({ error: `could not start loki: ${e.message}` }); });
57
+ child.once("spawn", () => { child.unref(); done({ pid: child.pid ?? 0 }); });
58
+ } catch (e) { done({ error: `could not start loki: ${(e as Error).message}` }); }
59
+ });
60
+ }