steroids-api 0.2.7

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 (328) hide show
  1. package/dist/API/src/index.d.ts +10 -0
  2. package/dist/API/src/index.d.ts.map +1 -0
  3. package/dist/API/src/index.js +130 -0
  4. package/dist/API/src/index.js.map +1 -0
  5. package/dist/API/src/routes/activity.d.ts +7 -0
  6. package/dist/API/src/routes/activity.d.ts.map +1 -0
  7. package/dist/API/src/routes/activity.js +252 -0
  8. package/dist/API/src/routes/activity.js.map +1 -0
  9. package/dist/API/src/routes/config.d.ts +7 -0
  10. package/dist/API/src/routes/config.d.ts.map +1 -0
  11. package/dist/API/src/routes/config.js +521 -0
  12. package/dist/API/src/routes/config.js.map +1 -0
  13. package/dist/API/src/routes/health.d.ts +7 -0
  14. package/dist/API/src/routes/health.d.ts.map +1 -0
  15. package/dist/API/src/routes/health.js +172 -0
  16. package/dist/API/src/routes/health.js.map +1 -0
  17. package/dist/API/src/routes/incidents.d.ts +7 -0
  18. package/dist/API/src/routes/incidents.d.ts.map +1 -0
  19. package/dist/API/src/routes/incidents.js +117 -0
  20. package/dist/API/src/routes/incidents.js.map +1 -0
  21. package/dist/API/src/routes/projects.d.ts +7 -0
  22. package/dist/API/src/routes/projects.d.ts.map +1 -0
  23. package/dist/API/src/routes/projects.js +398 -0
  24. package/dist/API/src/routes/projects.js.map +1 -0
  25. package/dist/API/src/routes/runners.d.ts +7 -0
  26. package/dist/API/src/routes/runners.d.ts.map +1 -0
  27. package/dist/API/src/routes/runners.js +242 -0
  28. package/dist/API/src/routes/runners.js.map +1 -0
  29. package/dist/API/src/routes/tasks.d.ts +7 -0
  30. package/dist/API/src/routes/tasks.d.ts.map +1 -0
  31. package/dist/API/src/routes/tasks.js +1007 -0
  32. package/dist/API/src/routes/tasks.js.map +1 -0
  33. package/dist/API/src/utils/validation.d.ts +22 -0
  34. package/dist/API/src/utils/validation.d.ts.map +1 -0
  35. package/dist/API/src/utils/validation.js +50 -0
  36. package/dist/API/src/utils/validation.js.map +1 -0
  37. package/dist/index.d.ts +10 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +184 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/routes/activity.d.ts +7 -0
  42. package/dist/routes/activity.d.ts.map +1 -0
  43. package/dist/routes/activity.js +252 -0
  44. package/dist/routes/activity.js.map +1 -0
  45. package/dist/routes/config.d.ts +7 -0
  46. package/dist/routes/config.d.ts.map +1 -0
  47. package/dist/routes/config.js +647 -0
  48. package/dist/routes/config.js.map +1 -0
  49. package/dist/routes/credit-alerts.d.ts +2 -0
  50. package/dist/routes/credit-alerts.d.ts.map +1 -0
  51. package/dist/routes/credit-alerts.js +97 -0
  52. package/dist/routes/credit-alerts.js.map +1 -0
  53. package/dist/routes/health.d.ts +7 -0
  54. package/dist/routes/health.d.ts.map +1 -0
  55. package/dist/routes/health.js +200 -0
  56. package/dist/routes/health.js.map +1 -0
  57. package/dist/routes/incidents.d.ts +7 -0
  58. package/dist/routes/incidents.d.ts.map +1 -0
  59. package/dist/routes/incidents.js +117 -0
  60. package/dist/routes/incidents.js.map +1 -0
  61. package/dist/routes/projects.d.ts +7 -0
  62. package/dist/routes/projects.d.ts.map +1 -0
  63. package/dist/routes/projects.js +643 -0
  64. package/dist/routes/projects.js.map +1 -0
  65. package/dist/routes/runners.d.ts +7 -0
  66. package/dist/routes/runners.d.ts.map +1 -0
  67. package/dist/routes/runners.js +299 -0
  68. package/dist/routes/runners.js.map +1 -0
  69. package/dist/routes/skills.d.ts +3 -0
  70. package/dist/routes/skills.d.ts.map +1 -0
  71. package/dist/routes/skills.js +109 -0
  72. package/dist/routes/skills.js.map +1 -0
  73. package/dist/routes/storage.d.ts +7 -0
  74. package/dist/routes/storage.d.ts.map +1 -0
  75. package/dist/routes/storage.js +93 -0
  76. package/dist/routes/storage.js.map +1 -0
  77. package/dist/routes/tasks.d.ts +7 -0
  78. package/dist/routes/tasks.d.ts.map +1 -0
  79. package/dist/routes/tasks.js +1145 -0
  80. package/dist/routes/tasks.js.map +1 -0
  81. package/dist/src/cleanup/invocation-logs.d.ts +30 -0
  82. package/dist/src/cleanup/invocation-logs.d.ts.map +1 -0
  83. package/dist/src/cleanup/invocation-logs.js +66 -0
  84. package/dist/src/cleanup/invocation-logs.js.map +1 -0
  85. package/dist/src/commands/loop-phases.d.ts +11 -0
  86. package/dist/src/commands/loop-phases.d.ts.map +1 -0
  87. package/dist/src/commands/loop-phases.js +304 -0
  88. package/dist/src/commands/loop-phases.js.map +1 -0
  89. package/dist/src/config/loader.d.ts +160 -0
  90. package/dist/src/config/loader.d.ts.map +1 -0
  91. package/dist/src/config/loader.js +276 -0
  92. package/dist/src/config/loader.js.map +1 -0
  93. package/dist/src/database/connection.d.ts +35 -0
  94. package/dist/src/database/connection.d.ts.map +1 -0
  95. package/dist/src/database/connection.js +197 -0
  96. package/dist/src/database/connection.js.map +1 -0
  97. package/dist/src/database/queries.d.ts +220 -0
  98. package/dist/src/database/queries.d.ts.map +1 -0
  99. package/dist/src/database/queries.js +589 -0
  100. package/dist/src/database/queries.js.map +1 -0
  101. package/dist/src/database/schema.d.ts +8 -0
  102. package/dist/src/database/schema.d.ts.map +1 -0
  103. package/dist/src/database/schema.js +184 -0
  104. package/dist/src/database/schema.js.map +1 -0
  105. package/dist/src/git/push.d.ts +26 -0
  106. package/dist/src/git/push.d.ts.map +1 -0
  107. package/dist/src/git/push.js +91 -0
  108. package/dist/src/git/push.js.map +1 -0
  109. package/dist/src/git/status.d.ts +83 -0
  110. package/dist/src/git/status.d.ts.map +1 -0
  111. package/dist/src/git/status.js +315 -0
  112. package/dist/src/git/status.js.map +1 -0
  113. package/dist/src/health/stuck-task-detector.d.ts +131 -0
  114. package/dist/src/health/stuck-task-detector.d.ts.map +1 -0
  115. package/dist/src/health/stuck-task-detector.js +233 -0
  116. package/dist/src/health/stuck-task-detector.js.map +1 -0
  117. package/dist/src/health/stuck-task-recovery.d.ts +45 -0
  118. package/dist/src/health/stuck-task-recovery.d.ts.map +1 -0
  119. package/dist/src/health/stuck-task-recovery.js +309 -0
  120. package/dist/src/health/stuck-task-recovery.js.map +1 -0
  121. package/dist/src/index.d.ts +10 -0
  122. package/dist/src/index.d.ts.map +1 -0
  123. package/dist/src/index.js +130 -0
  124. package/dist/src/index.js.map +1 -0
  125. package/dist/src/locking/queries.d.ts +116 -0
  126. package/dist/src/locking/queries.d.ts.map +1 -0
  127. package/dist/src/locking/queries.js +232 -0
  128. package/dist/src/locking/queries.js.map +1 -0
  129. package/dist/src/locking/section-lock.d.ts +74 -0
  130. package/dist/src/locking/section-lock.d.ts.map +1 -0
  131. package/dist/src/locking/section-lock.js +196 -0
  132. package/dist/src/locking/section-lock.js.map +1 -0
  133. package/dist/src/locking/task-lock.d.ts +92 -0
  134. package/dist/src/locking/task-lock.d.ts.map +1 -0
  135. package/dist/src/locking/task-lock.js +233 -0
  136. package/dist/src/locking/task-lock.js.map +1 -0
  137. package/dist/src/migrations/index.d.ts +7 -0
  138. package/dist/src/migrations/index.d.ts.map +1 -0
  139. package/dist/src/migrations/index.js +9 -0
  140. package/dist/src/migrations/index.js.map +1 -0
  141. package/dist/src/migrations/manifest.d.ts +92 -0
  142. package/dist/src/migrations/manifest.d.ts.map +1 -0
  143. package/dist/src/migrations/manifest.js +255 -0
  144. package/dist/src/migrations/manifest.js.map +1 -0
  145. package/dist/src/migrations/runner.d.ts +84 -0
  146. package/dist/src/migrations/runner.d.ts.map +1 -0
  147. package/dist/src/migrations/runner.js +338 -0
  148. package/dist/src/migrations/runner.js.map +1 -0
  149. package/dist/src/orchestrator/coder.d.ts +32 -0
  150. package/dist/src/orchestrator/coder.d.ts.map +1 -0
  151. package/dist/src/orchestrator/coder.js +170 -0
  152. package/dist/src/orchestrator/coder.js.map +1 -0
  153. package/dist/src/orchestrator/coordinator.d.ts +28 -0
  154. package/dist/src/orchestrator/coordinator.d.ts.map +1 -0
  155. package/dist/src/orchestrator/coordinator.js +252 -0
  156. package/dist/src/orchestrator/coordinator.js.map +1 -0
  157. package/dist/src/orchestrator/fallback-handler.d.ts +24 -0
  158. package/dist/src/orchestrator/fallback-handler.d.ts.map +1 -0
  159. package/dist/src/orchestrator/fallback-handler.js +280 -0
  160. package/dist/src/orchestrator/fallback-handler.js.map +1 -0
  161. package/dist/src/orchestrator/invoke.d.ts +14 -0
  162. package/dist/src/orchestrator/invoke.d.ts.map +1 -0
  163. package/dist/src/orchestrator/invoke.js +76 -0
  164. package/dist/src/orchestrator/invoke.js.map +1 -0
  165. package/dist/src/orchestrator/post-coder.d.ts +10 -0
  166. package/dist/src/orchestrator/post-coder.d.ts.map +1 -0
  167. package/dist/src/orchestrator/post-coder.js +198 -0
  168. package/dist/src/orchestrator/post-coder.js.map +1 -0
  169. package/dist/src/orchestrator/post-reviewer.d.ts +10 -0
  170. package/dist/src/orchestrator/post-reviewer.d.ts.map +1 -0
  171. package/dist/src/orchestrator/post-reviewer.js +199 -0
  172. package/dist/src/orchestrator/post-reviewer.js.map +1 -0
  173. package/dist/src/orchestrator/reviewer.d.ts +35 -0
  174. package/dist/src/orchestrator/reviewer.d.ts.map +1 -0
  175. package/dist/src/orchestrator/reviewer.js +237 -0
  176. package/dist/src/orchestrator/reviewer.js.map +1 -0
  177. package/dist/src/orchestrator/schemas.d.ts +10 -0
  178. package/dist/src/orchestrator/schemas.d.ts.map +1 -0
  179. package/dist/src/orchestrator/schemas.js +81 -0
  180. package/dist/src/orchestrator/schemas.js.map +1 -0
  181. package/dist/src/orchestrator/task-selector.d.ts +102 -0
  182. package/dist/src/orchestrator/task-selector.d.ts.map +1 -0
  183. package/dist/src/orchestrator/task-selector.js +326 -0
  184. package/dist/src/orchestrator/task-selector.js.map +1 -0
  185. package/dist/src/orchestrator/types.d.ts +74 -0
  186. package/dist/src/orchestrator/types.d.ts.map +1 -0
  187. package/dist/src/orchestrator/types.js +5 -0
  188. package/dist/src/orchestrator/types.js.map +1 -0
  189. package/dist/src/prompts/coder.d.ts +36 -0
  190. package/dist/src/prompts/coder.d.ts.map +1 -0
  191. package/dist/src/prompts/coder.js +303 -0
  192. package/dist/src/prompts/coder.js.map +1 -0
  193. package/dist/src/prompts/prompt-helpers.d.ts +51 -0
  194. package/dist/src/prompts/prompt-helpers.d.ts.map +1 -0
  195. package/dist/src/prompts/prompt-helpers.js +299 -0
  196. package/dist/src/prompts/prompt-helpers.js.map +1 -0
  197. package/dist/src/prompts/reviewer.d.ts +40 -0
  198. package/dist/src/prompts/reviewer.d.ts.map +1 -0
  199. package/dist/src/prompts/reviewer.js +416 -0
  200. package/dist/src/prompts/reviewer.js.map +1 -0
  201. package/dist/src/providers/claude.d.ts +53 -0
  202. package/dist/src/providers/claude.d.ts.map +1 -0
  203. package/dist/src/providers/claude.js +227 -0
  204. package/dist/src/providers/claude.js.map +1 -0
  205. package/dist/src/providers/codex.d.ts +53 -0
  206. package/dist/src/providers/codex.d.ts.map +1 -0
  207. package/dist/src/providers/codex.js +253 -0
  208. package/dist/src/providers/codex.js.map +1 -0
  209. package/dist/src/providers/gemini.d.ts +58 -0
  210. package/dist/src/providers/gemini.d.ts.map +1 -0
  211. package/dist/src/providers/gemini.js +240 -0
  212. package/dist/src/providers/gemini.js.map +1 -0
  213. package/dist/src/providers/interface.d.ts +185 -0
  214. package/dist/src/providers/interface.d.ts.map +1 -0
  215. package/dist/src/providers/interface.js +92 -0
  216. package/dist/src/providers/interface.js.map +1 -0
  217. package/dist/src/providers/invocation-logger.d.ts +97 -0
  218. package/dist/src/providers/invocation-logger.d.ts.map +1 -0
  219. package/dist/src/providers/invocation-logger.js +378 -0
  220. package/dist/src/providers/invocation-logger.js.map +1 -0
  221. package/dist/src/providers/openai.d.ts +53 -0
  222. package/dist/src/providers/openai.d.ts.map +1 -0
  223. package/dist/src/providers/openai.js +230 -0
  224. package/dist/src/providers/openai.js.map +1 -0
  225. package/dist/src/providers/registry.d.ts +100 -0
  226. package/dist/src/providers/registry.d.ts.map +1 -0
  227. package/dist/src/providers/registry.js +170 -0
  228. package/dist/src/providers/registry.js.map +1 -0
  229. package/dist/src/routes/activity.d.ts +7 -0
  230. package/dist/src/routes/activity.d.ts.map +1 -0
  231. package/dist/src/routes/activity.js +252 -0
  232. package/dist/src/routes/activity.js.map +1 -0
  233. package/dist/src/routes/config.d.ts +7 -0
  234. package/dist/src/routes/config.d.ts.map +1 -0
  235. package/dist/src/routes/config.js +521 -0
  236. package/dist/src/routes/config.js.map +1 -0
  237. package/dist/src/routes/health.d.ts +7 -0
  238. package/dist/src/routes/health.d.ts.map +1 -0
  239. package/dist/src/routes/health.js +172 -0
  240. package/dist/src/routes/health.js.map +1 -0
  241. package/dist/src/routes/incidents.d.ts +7 -0
  242. package/dist/src/routes/incidents.d.ts.map +1 -0
  243. package/dist/src/routes/incidents.js +117 -0
  244. package/dist/src/routes/incidents.js.map +1 -0
  245. package/dist/src/routes/projects.d.ts +7 -0
  246. package/dist/src/routes/projects.d.ts.map +1 -0
  247. package/dist/src/routes/projects.js +398 -0
  248. package/dist/src/routes/projects.js.map +1 -0
  249. package/dist/src/routes/runners.d.ts +7 -0
  250. package/dist/src/routes/runners.d.ts.map +1 -0
  251. package/dist/src/routes/runners.js +242 -0
  252. package/dist/src/routes/runners.js.map +1 -0
  253. package/dist/src/routes/tasks.d.ts +7 -0
  254. package/dist/src/routes/tasks.d.ts.map +1 -0
  255. package/dist/src/routes/tasks.js +1007 -0
  256. package/dist/src/routes/tasks.js.map +1 -0
  257. package/dist/src/runners/activity-log.d.ts +65 -0
  258. package/dist/src/runners/activity-log.d.ts.map +1 -0
  259. package/dist/src/runners/activity-log.js +140 -0
  260. package/dist/src/runners/activity-log.js.map +1 -0
  261. package/dist/src/runners/cron.d.ts +30 -0
  262. package/dist/src/runners/cron.d.ts.map +1 -0
  263. package/dist/src/runners/cron.js +333 -0
  264. package/dist/src/runners/cron.js.map +1 -0
  265. package/dist/src/runners/daemon.d.ts +71 -0
  266. package/dist/src/runners/daemon.d.ts.map +1 -0
  267. package/dist/src/runners/daemon.js +233 -0
  268. package/dist/src/runners/daemon.js.map +1 -0
  269. package/dist/src/runners/global-db.d.ts +31 -0
  270. package/dist/src/runners/global-db.d.ts.map +1 -0
  271. package/dist/src/runners/global-db.js +220 -0
  272. package/dist/src/runners/global-db.js.map +1 -0
  273. package/dist/src/runners/hang-detector.d.ts +38 -0
  274. package/dist/src/runners/hang-detector.d.ts.map +1 -0
  275. package/dist/src/runners/hang-detector.js +130 -0
  276. package/dist/src/runners/hang-detector.js.map +1 -0
  277. package/dist/src/runners/heartbeat.d.ts +39 -0
  278. package/dist/src/runners/heartbeat.d.ts.map +1 -0
  279. package/dist/src/runners/heartbeat.js +71 -0
  280. package/dist/src/runners/heartbeat.js.map +1 -0
  281. package/dist/src/runners/lock.d.ts +47 -0
  282. package/dist/src/runners/lock.d.ts.map +1 -0
  283. package/dist/src/runners/lock.js +140 -0
  284. package/dist/src/runners/lock.js.map +1 -0
  285. package/dist/src/runners/orchestrator-loop.d.ts +20 -0
  286. package/dist/src/runners/orchestrator-loop.d.ts.map +1 -0
  287. package/dist/src/runners/orchestrator-loop.js +208 -0
  288. package/dist/src/runners/orchestrator-loop.js.map +1 -0
  289. package/dist/src/runners/projects.d.ts +96 -0
  290. package/dist/src/runners/projects.d.ts.map +1 -0
  291. package/dist/src/runners/projects.js +243 -0
  292. package/dist/src/runners/projects.js.map +1 -0
  293. package/dist/src/runners/wakeup.d.ts +37 -0
  294. package/dist/src/runners/wakeup.d.ts.map +1 -0
  295. package/dist/src/runners/wakeup.js +355 -0
  296. package/dist/src/runners/wakeup.js.map +1 -0
  297. package/dist/src/utils/validation.d.ts +22 -0
  298. package/dist/src/utils/validation.d.ts.map +1 -0
  299. package/dist/src/utils/validation.js +50 -0
  300. package/dist/src/utils/validation.js.map +1 -0
  301. package/dist/utils/sqlite.d.ts +17 -0
  302. package/dist/utils/sqlite.d.ts.map +1 -0
  303. package/dist/utils/sqlite.js +27 -0
  304. package/dist/utils/sqlite.js.map +1 -0
  305. package/dist/utils/storage-cache.d.ts +33 -0
  306. package/dist/utils/storage-cache.d.ts.map +1 -0
  307. package/dist/utils/storage-cache.js +81 -0
  308. package/dist/utils/storage-cache.js.map +1 -0
  309. package/dist/utils/validation.d.ts +22 -0
  310. package/dist/utils/validation.d.ts.map +1 -0
  311. package/dist/utils/validation.js +51 -0
  312. package/dist/utils/validation.js.map +1 -0
  313. package/package.json +39 -0
  314. package/src/index.ts +199 -0
  315. package/src/routes/activity.ts +302 -0
  316. package/src/routes/config.ts +723 -0
  317. package/src/routes/credit-alerts.ts +73 -0
  318. package/src/routes/health.ts +219 -0
  319. package/src/routes/incidents.ts +131 -0
  320. package/src/routes/projects.ts +854 -0
  321. package/src/routes/runners.ts +357 -0
  322. package/src/routes/skills.ts +127 -0
  323. package/src/routes/storage.ts +108 -0
  324. package/src/routes/tasks.ts +1372 -0
  325. package/src/utils/sqlite.ts +36 -0
  326. package/src/utils/storage-cache.ts +107 -0
  327. package/src/utils/validation.ts +61 -0
  328. package/tsconfig.json +20 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Credit Alerts API routes — credit exhaustion incidents for dashboard notifications.
3
+ */
4
+ import { Router, Request, Response } from 'express';
5
+ import Database from 'better-sqlite3';
6
+ import { existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { openSqliteForRead } from '../utils/sqlite.js';
9
+ import { getRegisteredProjects } from '../../../dist/runners/projects.js';
10
+
11
+ export const creditAlertRoutes = Router();
12
+
13
+ interface IncidentRow { id: string; runner_id: string | null; details: string | null; created_at: string }
14
+
15
+ function openProjectDb(path: string, readonly: boolean): Database.Database | null {
16
+ const dbPath = join(path, '.steroids', 'steroids.db');
17
+ if (!existsSync(dbPath)) return null;
18
+ try { return readonly ? openSqliteForRead(dbPath) : new Database(dbPath); } catch { return null; }
19
+ }
20
+
21
+ function parseRow(row: IncidentRow) {
22
+ let provider = '', model = '', role = '', message = '';
23
+ if (row.details) {
24
+ try { const d = JSON.parse(row.details); provider = d.provider ?? ''; model = d.model ?? ''; role = d.role ?? ''; message = d.message ?? ''; } catch { /* malformed */ }
25
+ }
26
+ return { id: row.id, provider, model, role, message, runnerId: row.runner_id, createdAt: row.created_at };
27
+ }
28
+
29
+ const ALERT_SQL = `SELECT id, runner_id, details, created_at FROM incidents
30
+ WHERE failure_mode = 'credit_exhaustion' AND resolved_at IS NULL ORDER BY created_at DESC`;
31
+
32
+ const DISMISS_SQL = `UPDATE incidents SET resolved_at = datetime('now'), resolution = ?
33
+ WHERE id = ? AND resolved_at IS NULL AND failure_mode = 'credit_exhaustion'`;
34
+
35
+ const RETRY_SQL = `UPDATE incidents SET resolved_at = datetime('now'), resolution = 'retry'
36
+ WHERE id = ? AND resolved_at IS NULL AND failure_mode = 'credit_exhaustion'`;
37
+
38
+ function resolveIncident(req: Request, res: Response, sql: string, params: unknown[]) {
39
+ const projectPath = req.body?.project as string | undefined;
40
+ if (!projectPath) { res.status(400).json({ error: 'Missing body param: project' }); return; }
41
+ const db = openProjectDb(projectPath, false);
42
+ if (!db) { res.status(404).json({ error: 'Project database not found' }); return; }
43
+ try {
44
+ const r = db.prepare(sql).run(...params);
45
+ if (r.changes === 0) { res.status(404).json({ error: 'Incident not found or already resolved' }); return; }
46
+ res.json({ ok: true });
47
+ } catch { res.status(500).json({ error: 'Failed to update alert' }); } finally { db.close(); }
48
+ }
49
+
50
+ /** GET / — list active credit exhaustion incidents. project query param is optional. */
51
+ creditAlertRoutes.get('/', (req: Request, res: Response) => {
52
+ const project = req.query.project as string | undefined;
53
+ const paths = project ? [project] : getRegisteredProjects(false).map((p: { path: string }) => p.path);
54
+ const alerts: ReturnType<typeof parseRow>[] = [];
55
+ for (const p of paths) {
56
+ const db = openProjectDb(p, true);
57
+ if (!db) continue;
58
+ try { (db.prepare(ALERT_SQL).all() as IncidentRow[]).forEach((r) => alerts.push(parseRow(r))); } catch { /* best-effort */ } finally { db.close(); }
59
+ }
60
+ alerts.sort((a, b) => (b.createdAt > a.createdAt ? 1 : b.createdAt < a.createdAt ? -1 : 0));
61
+ res.json({ alerts });
62
+ });
63
+
64
+ /** POST /:id/dismiss — resolve a credit alert. Body: optional {resolution}, defaults "dismissed". */
65
+ creditAlertRoutes.post('/:id/dismiss', (req: Request, res: Response) => {
66
+ const resolution = (req.body?.resolution as string) || 'dismissed';
67
+ resolveIncident(req, res, DISMISS_SQL, [resolution, req.params.id]);
68
+ });
69
+
70
+ /** POST /:id/retry — resolve with retry signal. Body: {project} only. */
71
+ creditAlertRoutes.post('/:id/retry', (req: Request, res: Response) => {
72
+ resolveIncident(req, res, RETRY_SQL, [req.params.id]);
73
+ });
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Health API routes
3
+ * Exposes stuck-task detection health summary for the dashboard/monitor.
4
+ */
5
+
6
+ import { Router, Request, Response } from 'express';
7
+ import Database from 'better-sqlite3';
8
+ import { existsSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { loadConfig } from '../../../dist/config/loader.js';
11
+ import {
12
+ detectStuckTasks,
13
+ type StuckTaskDetectionConfig,
14
+ type StuckTaskDetectionReport,
15
+ } from '../../../dist/health/stuck-task-detector.js';
16
+ import { getGlobalDbPath } from '../../../dist/runners/global-db.js';
17
+ import { openSqliteForRead } from '../utils/sqlite.js';
18
+
19
+ const router = Router();
20
+
21
+ function openProjectDatabaseReadonly(projectPath: string): Database.Database | null {
22
+ const dbPath = join(projectPath, '.steroids', 'steroids.db');
23
+ if (!existsSync(dbPath)) return null;
24
+ try {
25
+ return openSqliteForRead(dbPath);
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function openGlobalDatabaseReadonlyOrMemory(): { db: Database.Database; close: () => void } {
32
+ const dbPath = getGlobalDbPath();
33
+ if (existsSync(dbPath)) {
34
+ const db = openSqliteForRead(dbPath);
35
+ return { db, close: () => db.close() };
36
+ }
37
+
38
+ // No global DB yet; use an in-memory DB with an empty runners table.
39
+ const db = new Database(':memory:');
40
+ db.exec(`
41
+ CREATE TABLE IF NOT EXISTS runners (
42
+ id TEXT PRIMARY KEY,
43
+ status TEXT NOT NULL DEFAULT 'idle',
44
+ pid INTEGER,
45
+ project_path TEXT,
46
+ current_task_id TEXT,
47
+ started_at TEXT,
48
+ heartbeat_at TEXT NOT NULL DEFAULT (datetime('now')),
49
+ section_id TEXT
50
+ );
51
+ `);
52
+ return { db, close: () => db.close() };
53
+ }
54
+
55
+ function parseBoolean(value: unknown): boolean | undefined {
56
+ if (value === undefined) return undefined;
57
+ if (value === 'true') return true;
58
+ if (value === 'false') return false;
59
+ return undefined;
60
+ }
61
+
62
+ function isLikelySchemaError(error: unknown): boolean {
63
+ if (!(error instanceof Error)) return false;
64
+ return /no such table|no such column|has no column|syntax error/i.test(error.message);
65
+ }
66
+
67
+ function fallbackStuckTaskReport(): StuckTaskDetectionReport {
68
+ return {
69
+ timestamp: new Date(),
70
+ orphanedTasks: [],
71
+ hangingInvocations: [],
72
+ zombieRunners: [],
73
+ deadRunners: [],
74
+ dbInconsistencies: [],
75
+ };
76
+ }
77
+
78
+ type HealthStatus = 'healthy' | 'degraded' | 'unhealthy';
79
+
80
+ function computeStatus(args: {
81
+ orphanedTasks: number;
82
+ hangingInvocations: number;
83
+ zombieRunners: number;
84
+ deadRunners: number;
85
+ activeIncidents: number;
86
+ }): HealthStatus {
87
+ if (args.deadRunners > 0 || args.zombieRunners > 0) return 'unhealthy';
88
+ if (args.hangingInvocations > 0 || args.orphanedTasks > 0 || args.activeIncidents > 0) return 'degraded';
89
+ return 'healthy';
90
+ }
91
+
92
+ /**
93
+ * GET /api/health
94
+ * Query params:
95
+ * - project: string (required) - project path
96
+ * - includeSignals: boolean (optional) - include raw signal arrays (default: false)
97
+ */
98
+ router.get('/health', (req: Request, res: Response) => {
99
+ const projectPath = req.query.project as string | undefined;
100
+ if (!projectPath) {
101
+ res.status(400).json({
102
+ success: false,
103
+ error: 'Project path required (query param: project)',
104
+ });
105
+ return;
106
+ }
107
+
108
+ const includeSignals = parseBoolean(req.query.includeSignals) ?? false;
109
+
110
+ const projectDb = openProjectDatabaseReadonly(projectPath);
111
+ if (!projectDb) {
112
+ res.status(404).json({
113
+ success: false,
114
+ error: 'Project database not found at .steroids/steroids.db',
115
+ project: projectPath,
116
+ });
117
+ return;
118
+ }
119
+
120
+ const { db: globalDb, close: closeGlobal } = openGlobalDatabaseReadonlyOrMemory();
121
+
122
+ try {
123
+ const cfg = loadConfig(projectPath);
124
+ const detectionConfig: StuckTaskDetectionConfig = {
125
+ orphanedTaskTimeoutSec: cfg.health?.orphanedTaskTimeout,
126
+ maxCoderDurationSec: cfg.health?.maxCoderDuration,
127
+ maxReviewerDurationSec: cfg.health?.maxReviewerDuration,
128
+ runnerHeartbeatTimeoutSec: cfg.health?.runnerHeartbeatTimeout,
129
+ invocationStalenessSec: cfg.health?.invocationStaleness,
130
+ };
131
+
132
+ let report: StuckTaskDetectionReport;
133
+ try {
134
+ report = detectStuckTasks({
135
+ projectPath,
136
+ projectDb,
137
+ globalDb,
138
+ config: detectionConfig,
139
+ });
140
+ } catch (error) {
141
+ if (!isLikelySchemaError(error)) {
142
+ throw error;
143
+ }
144
+ if (process.env.NODE_ENV !== 'test') {
145
+ console.warn('Falling back to minimal health signals due to missing schema:', error);
146
+ }
147
+ report = fallbackStuckTaskReport();
148
+ }
149
+
150
+ // Incident counts (best-effort: table might not exist)
151
+ let activeIncidents = 0;
152
+ let recentIncidents = 0;
153
+ try {
154
+ const rowActive = projectDb
155
+ .prepare(`SELECT COUNT(*) as count FROM incidents WHERE resolved_at IS NULL`)
156
+ .get() as { count: number } | undefined;
157
+ const rowRecent = projectDb
158
+ .prepare(`SELECT COUNT(*) as count FROM incidents WHERE detected_at >= datetime('now', '-24 hours')`)
159
+ .get() as { count: number } | undefined;
160
+ activeIncidents = rowActive?.count ?? 0;
161
+ recentIncidents = rowRecent?.count ?? 0;
162
+ } catch {
163
+ // ignore
164
+ }
165
+
166
+ const orphanedTasks = report.orphanedTasks.length;
167
+ const hangingInvocations = report.hangingInvocations.length;
168
+ const zombieRunners = report.zombieRunners.length;
169
+ const deadRunners = report.deadRunners.length;
170
+
171
+ const health = {
172
+ status: computeStatus({
173
+ orphanedTasks,
174
+ hangingInvocations,
175
+ zombieRunners,
176
+ deadRunners,
177
+ activeIncidents,
178
+ }),
179
+ lastCheck: new Date().toISOString(),
180
+ checks: [
181
+ { type: 'orphaned_tasks', healthy: orphanedTasks === 0, found: orphanedTasks },
182
+ { type: 'hanging_invocations', healthy: hangingInvocations === 0, found: hangingInvocations },
183
+ { type: 'zombie_runners', healthy: zombieRunners === 0, found: zombieRunners },
184
+ { type: 'dead_runners', healthy: deadRunners === 0, found: deadRunners },
185
+ ],
186
+ activeIncidents,
187
+ recentIncidents,
188
+ ...(includeSignals
189
+ ? {
190
+ signals: {
191
+ orphanedTasks: report.orphanedTasks,
192
+ hangingInvocations: report.hangingInvocations,
193
+ zombieRunners: report.zombieRunners,
194
+ deadRunners: report.deadRunners,
195
+ dbInconsistencies: report.dbInconsistencies,
196
+ },
197
+ }
198
+ : {}),
199
+ };
200
+
201
+ res.json({
202
+ success: true,
203
+ project: projectPath,
204
+ health,
205
+ });
206
+ } catch (error) {
207
+ console.error('Error computing health status:', error);
208
+ res.status(500).json({
209
+ success: false,
210
+ error: 'Failed to compute health status',
211
+ message: error instanceof Error ? error.message : 'Unknown error',
212
+ });
213
+ } finally {
214
+ projectDb.close();
215
+ closeGlobal();
216
+ }
217
+ });
218
+
219
+ export default router;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Incidents API routes
3
+ * Exposes stuck-task incident history for dashboard/monitor.
4
+ */
5
+
6
+ import { Router, Request, Response } from 'express';
7
+ import Database from 'better-sqlite3';
8
+ import { existsSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { openSqliteForRead } from '../utils/sqlite.js';
11
+
12
+ const router = Router();
13
+
14
+ interface IncidentRow {
15
+ id: string;
16
+ task_id: string | null;
17
+ runner_id: string | null;
18
+ failure_mode: string;
19
+ detected_at: string;
20
+ resolved_at: string | null;
21
+ resolution: string | null;
22
+ details: string | null;
23
+ created_at: string;
24
+ task_title?: string | null;
25
+ }
26
+
27
+ function openProjectDatabaseReadonly(projectPath: string): Database.Database | null {
28
+ const dbPath = join(projectPath, '.steroids', 'steroids.db');
29
+ if (!existsSync(dbPath)) return null;
30
+ try {
31
+ return openSqliteForRead(dbPath);
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function parsePositiveInt(value: unknown, fallback: number): number {
38
+ if (value === undefined) return fallback;
39
+ const n = parseInt(String(value), 10);
40
+ if (!Number.isFinite(n) || n <= 0) return fallback;
41
+ return n;
42
+ }
43
+
44
+ function parseBoolean(value: unknown): boolean | undefined {
45
+ if (value === undefined) return undefined;
46
+ if (value === 'true') return true;
47
+ if (value === 'false') return false;
48
+ return undefined;
49
+ }
50
+
51
+ /**
52
+ * GET /api/incidents
53
+ * Query params:
54
+ * - project: string (required) - project path
55
+ * - limit: number (optional, default 50, max 200)
56
+ * - task: string (optional) - filter by task ID prefix
57
+ * - unresolved: boolean (optional) - true => resolved_at IS NULL, false => resolved_at IS NOT NULL
58
+ */
59
+ router.get('/incidents', (req: Request, res: Response) => {
60
+ const projectPath = req.query.project as string | undefined;
61
+ if (!projectPath) {
62
+ res.status(400).json({
63
+ success: false,
64
+ error: 'Project path required (query param: project)',
65
+ });
66
+ return;
67
+ }
68
+
69
+ const limit = Math.min(parsePositiveInt(req.query.limit, 50), 200);
70
+ const taskPrefix = (req.query.task as string | undefined)?.trim();
71
+ const unresolved = parseBoolean(req.query.unresolved);
72
+
73
+ const db = openProjectDatabaseReadonly(projectPath);
74
+ if (!db) {
75
+ res.status(404).json({
76
+ success: false,
77
+ error: 'Project database not found at .steroids/steroids.db',
78
+ project: projectPath,
79
+ });
80
+ return;
81
+ }
82
+
83
+ try {
84
+ // Best-effort: incidents table may not exist (migrations disabled).
85
+ let incidents: IncidentRow[] = [];
86
+ try {
87
+ const where: string[] = [];
88
+ const params: Array<string | number> = [];
89
+
90
+ if (taskPrefix) {
91
+ where.push('i.task_id LIKE ?');
92
+ params.push(`${taskPrefix}%`);
93
+ }
94
+ if (unresolved === true) where.push('i.resolved_at IS NULL');
95
+ if (unresolved === false) where.push('i.resolved_at IS NOT NULL');
96
+
97
+ const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : '';
98
+ const sql = `
99
+ SELECT
100
+ i.id, i.task_id, i.runner_id, i.failure_mode, i.detected_at, i.resolved_at, i.resolution, i.details, i.created_at,
101
+ t.title as task_title
102
+ FROM incidents i
103
+ LEFT JOIN tasks t ON t.id = i.task_id
104
+ ${whereSql}
105
+ ORDER BY i.detected_at DESC
106
+ LIMIT ?
107
+ `;
108
+ incidents = db.prepare(sql).all(...params, limit) as IncidentRow[];
109
+ } catch {
110
+ incidents = [];
111
+ }
112
+
113
+ res.json({
114
+ success: true,
115
+ project: projectPath,
116
+ total: incidents.length,
117
+ incidents,
118
+ });
119
+ } catch (error) {
120
+ console.error('Error listing incidents:', error);
121
+ res.status(500).json({
122
+ success: false,
123
+ error: 'Failed to list incidents',
124
+ message: error instanceof Error ? error.message : 'Unknown error',
125
+ });
126
+ } finally {
127
+ db.close();
128
+ }
129
+ });
130
+
131
+ export default router;