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,1007 @@
1
+ /**
2
+ * Tasks API routes
3
+ * Exposes task details and logs for individual tasks
4
+ */
5
+ import { Router } from 'express';
6
+ import Database from 'better-sqlite3';
7
+ import { createReadStream, existsSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { execSync } from 'node:child_process';
10
+ import { Tail } from 'tail';
11
+ const router = Router();
12
+ const MAX_SSE_CONNECTIONS = Math.max(1, parseInt(process.env.MAX_SSE_CONNECTIONS || '100', 10) || 100);
13
+ let activeSseConnections = 0;
14
+ /**
15
+ * Get GitHub URL from git remote
16
+ * @param projectPath - Path to project root
17
+ * @returns GitHub base URL (e.g., https://github.com/owner/repo) or null
18
+ */
19
+ function getGitHubUrl(projectPath) {
20
+ try {
21
+ const remoteUrl = execSync('git remote get-url origin', {
22
+ cwd: projectPath,
23
+ encoding: 'utf-8',
24
+ }).trim();
25
+ // Convert SSH or HTTPS URL to web URL
26
+ // SSH: git@github.com:owner/repo.git
27
+ // HTTPS: https://github.com/owner/repo.git
28
+ let webUrl = null;
29
+ if (remoteUrl.startsWith('git@github.com:')) {
30
+ // SSH format
31
+ const path = remoteUrl.replace('git@github.com:', '').replace(/\.git$/, '');
32
+ webUrl = `https://github.com/${path}`;
33
+ }
34
+ else if (remoteUrl.includes('github.com')) {
35
+ // HTTPS format
36
+ webUrl = remoteUrl.replace(/\.git$/, '');
37
+ }
38
+ return webUrl;
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ /**
45
+ * Open project database
46
+ * @param projectPath - Path to project root
47
+ * @returns Database connection or null if not found
48
+ */
49
+ function openProjectDatabase(projectPath) {
50
+ const dbPath = join(projectPath, '.steroids', 'steroids.db');
51
+ if (!existsSync(dbPath)) {
52
+ return null;
53
+ }
54
+ try {
55
+ return new Database(dbPath, { readonly: true });
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * Calculate duration for each status from audit trail
63
+ * @param auditTrail - Array of audit entries sorted by created_at
64
+ * @returns Audit entries with duration_seconds added
65
+ */
66
+ function calculateDurations(auditTrail) {
67
+ // Sort by created_at ascending for duration calculation
68
+ const sorted = [...auditTrail].sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
69
+ return sorted.map((entry, index) => {
70
+ // Duration is time until next status change
71
+ if (index < sorted.length - 1) {
72
+ const startTime = new Date(entry.created_at).getTime();
73
+ const endTime = new Date(sorted[index + 1].created_at).getTime();
74
+ const durationSeconds = Math.round((endTime - startTime) / 1000);
75
+ return { ...entry, duration_seconds: durationSeconds };
76
+ }
77
+ // Current/last status - duration from entry until now
78
+ const startTime = new Date(entry.created_at).getTime();
79
+ const now = Date.now();
80
+ const durationSeconds = Math.round((now - startTime) / 1000);
81
+ return { ...entry, duration_seconds: durationSeconds };
82
+ });
83
+ }
84
+ function sleep(ms) {
85
+ return new Promise((resolve) => setTimeout(resolve, ms));
86
+ }
87
+ async function writeSSE(res, payload) {
88
+ if (res.writableEnded)
89
+ return;
90
+ const chunk = `data: ${JSON.stringify(payload)}\n\n`;
91
+ const ok = res.write(chunk);
92
+ if (ok)
93
+ return;
94
+ await new Promise((resolve, reject) => {
95
+ const onDrain = () => {
96
+ cleanup();
97
+ resolve();
98
+ };
99
+ const onError = (err) => {
100
+ cleanup();
101
+ reject(err);
102
+ };
103
+ const cleanup = () => {
104
+ res.off('drain', onDrain);
105
+ res.off('error', onError);
106
+ };
107
+ res.on('drain', onDrain);
108
+ res.on('error', onError);
109
+ });
110
+ }
111
+ async function writeSSEComment(res, comment) {
112
+ if (res.writableEnded)
113
+ return;
114
+ const chunk = `: ${comment}\n\n`;
115
+ const ok = res.write(chunk);
116
+ if (ok)
117
+ return;
118
+ await new Promise((resolve, reject) => {
119
+ const onDrain = () => {
120
+ cleanup();
121
+ resolve();
122
+ };
123
+ const onError = (err) => {
124
+ cleanup();
125
+ reject(err);
126
+ };
127
+ const cleanup = () => {
128
+ res.off('drain', onDrain);
129
+ res.off('error', onError);
130
+ };
131
+ res.on('drain', onDrain);
132
+ res.on('error', onError);
133
+ });
134
+ }
135
+ async function waitForFile(filePath, opts) {
136
+ const deadline = Date.now() + opts.timeoutMs;
137
+ while (Date.now() < deadline) {
138
+ if (opts.isAborted())
139
+ return false;
140
+ if (existsSync(filePath))
141
+ return true;
142
+ await sleep(opts.pollMs);
143
+ }
144
+ return existsSync(filePath);
145
+ }
146
+ async function streamJsonlFileToSSE(res, filePath, opts) {
147
+ const rs = createReadStream(filePath, { encoding: 'utf8' });
148
+ let buffer = '';
149
+ for await (const chunk of rs) {
150
+ if (opts.isAborted())
151
+ return;
152
+ buffer += chunk;
153
+ let nl = buffer.indexOf('\n');
154
+ while (nl >= 0) {
155
+ const line = buffer.slice(0, nl);
156
+ buffer = buffer.slice(nl + 1);
157
+ nl = buffer.indexOf('\n');
158
+ const trimmed = line.trim();
159
+ if (!trimmed)
160
+ continue;
161
+ try {
162
+ const entry = JSON.parse(trimmed);
163
+ await writeSSE(res, entry);
164
+ }
165
+ catch {
166
+ // ignore malformed JSONL lines
167
+ }
168
+ }
169
+ }
170
+ const tailLine = buffer.trim();
171
+ if (tailLine && !opts.isAborted()) {
172
+ try {
173
+ const entry = JSON.parse(tailLine);
174
+ await writeSSE(res, entry);
175
+ }
176
+ catch {
177
+ // ignore
178
+ }
179
+ }
180
+ }
181
+ async function readSampledJsonlEntries(filePath, opts) {
182
+ const rs = createReadStream(filePath, { encoding: 'utf8' });
183
+ let buffer = '';
184
+ let index = 0;
185
+ const out = [];
186
+ const keep = (entry, i) => {
187
+ if (opts.shouldKeep)
188
+ return opts.shouldKeep(entry, i);
189
+ // Keep all tools, and sample the rest.
190
+ return entry?.type === 'tool' || i % opts.keepEveryN === 0;
191
+ };
192
+ for await (const chunk of rs) {
193
+ buffer += chunk;
194
+ let nl = buffer.indexOf('\n');
195
+ while (nl >= 0) {
196
+ const line = buffer.slice(0, nl);
197
+ buffer = buffer.slice(nl + 1);
198
+ nl = buffer.indexOf('\n');
199
+ const trimmed = line.trim();
200
+ if (!trimmed)
201
+ continue;
202
+ try {
203
+ const entry = JSON.parse(trimmed);
204
+ if (keep(entry, index))
205
+ out.push(entry);
206
+ }
207
+ catch {
208
+ // ignore malformed JSONL lines
209
+ }
210
+ finally {
211
+ index++;
212
+ }
213
+ }
214
+ }
215
+ const tailLine = buffer.trim();
216
+ if (tailLine) {
217
+ try {
218
+ const entry = JSON.parse(tailLine);
219
+ if (keep(entry, index))
220
+ out.push(entry);
221
+ }
222
+ catch {
223
+ // ignore
224
+ }
225
+ }
226
+ return out;
227
+ }
228
+ /**
229
+ * GET /api/tasks/:taskId
230
+ * Get detailed information about a task including audit history
231
+ * Query params:
232
+ * - project: string (required) - project path
233
+ */
234
+ router.get('/tasks/:taskId', (req, res) => {
235
+ try {
236
+ const { taskId } = req.params;
237
+ const projectPath = req.query.project;
238
+ if (!projectPath) {
239
+ res.status(400).json({
240
+ success: false,
241
+ error: 'Missing required query parameter: project',
242
+ });
243
+ return;
244
+ }
245
+ const db = openProjectDatabase(projectPath);
246
+ if (!db) {
247
+ res.status(404).json({
248
+ success: false,
249
+ error: 'Project database not found',
250
+ project: projectPath,
251
+ });
252
+ return;
253
+ }
254
+ try {
255
+ // Get task details with section name
256
+ const task = db
257
+ .prepare(`SELECT
258
+ t.id, t.title, t.status, t.section_id,
259
+ s.name as section_name,
260
+ t.source_file, t.rejection_count,
261
+ t.created_at, t.updated_at
262
+ FROM tasks t
263
+ LEFT JOIN sections s ON t.section_id = s.id
264
+ WHERE t.id = ?`)
265
+ .get(taskId);
266
+ if (!task) {
267
+ res.status(404).json({
268
+ success: false,
269
+ error: 'Task not found',
270
+ task_id: taskId,
271
+ });
272
+ return;
273
+ }
274
+ // Get audit trail
275
+ const auditTrail = db
276
+ .prepare(`SELECT id, task_id, from_status, to_status, actor, actor_type, model, notes, commit_sha, created_at
277
+ FROM audit
278
+ WHERE task_id = ?
279
+ ORDER BY created_at ASC`)
280
+ .all(taskId);
281
+ // Get disputes for task
282
+ const disputes = db
283
+ .prepare(`SELECT * FROM disputes
284
+ WHERE task_id = ?
285
+ ORDER BY created_at DESC`)
286
+ .all(taskId);
287
+ // Get LLM invocations (exclude prompt/response to keep payload light)
288
+ const invocations = db
289
+ .prepare(`SELECT id, task_id, role, provider, model, exit_code, duration_ms, success, timed_out, rejection_number, created_at
290
+ FROM task_invocations
291
+ WHERE task_id = ?
292
+ ORDER BY created_at ASC`)
293
+ .all(taskId);
294
+ // Calculate durations for each status
295
+ const auditWithDurations = calculateDurations(auditTrail);
296
+ // Calculate total time in each status
297
+ let inProgressSeconds = 0;
298
+ let reviewSeconds = 0;
299
+ for (const entry of auditWithDurations) {
300
+ if (entry.to_status === 'in_progress' && entry.duration_seconds) {
301
+ inProgressSeconds += entry.duration_seconds;
302
+ }
303
+ else if (entry.to_status === 'review' && entry.duration_seconds) {
304
+ reviewSeconds += entry.duration_seconds;
305
+ }
306
+ }
307
+ // Total time is just the sum of active work time (coding + review)
308
+ const totalSeconds = inProgressSeconds + reviewSeconds;
309
+ // Get GitHub URL for commit links
310
+ const githubUrl = getGitHubUrl(projectPath);
311
+ const response = {
312
+ ...task,
313
+ duration: {
314
+ total_seconds: totalSeconds,
315
+ in_progress_seconds: inProgressSeconds,
316
+ review_seconds: reviewSeconds,
317
+ },
318
+ audit_trail: auditWithDurations.reverse(), // Most recent first for display
319
+ invocations, // Oldest first (chronological)
320
+ disputes,
321
+ github_url: githubUrl,
322
+ };
323
+ res.json({
324
+ success: true,
325
+ task: response,
326
+ });
327
+ }
328
+ finally {
329
+ db.close();
330
+ }
331
+ }
332
+ catch (error) {
333
+ console.error('Error getting task details:', error);
334
+ res.status(500).json({
335
+ success: false,
336
+ error: 'Failed to get task details',
337
+ message: error instanceof Error ? error.message : 'Unknown error',
338
+ });
339
+ }
340
+ });
341
+ /**
342
+ * GET /api/tasks/:taskId/stream
343
+ * Stream invocation activity (JSONL) for the currently-running invocation using SSE.
344
+ * Query params:
345
+ * - project: string (required) - project path
346
+ */
347
+ router.get('/tasks/:taskId/stream', async (req, res) => {
348
+ const { taskId } = req.params;
349
+ const projectPath = req.query.project;
350
+ if (!projectPath) {
351
+ res.status(400).json({
352
+ success: false,
353
+ error: 'Missing required query parameter: project',
354
+ });
355
+ return;
356
+ }
357
+ if (activeSseConnections >= MAX_SSE_CONNECTIONS) {
358
+ res.status(429).json({
359
+ success: false,
360
+ error: 'Too many active streams',
361
+ max: MAX_SSE_CONNECTIONS,
362
+ });
363
+ return;
364
+ }
365
+ activeSseConnections++;
366
+ let closed = false;
367
+ const close = () => {
368
+ if (closed)
369
+ return;
370
+ closed = true;
371
+ activeSseConnections = Math.max(0, activeSseConnections - 1);
372
+ try {
373
+ res.end();
374
+ }
375
+ catch {
376
+ // ignore
377
+ }
378
+ };
379
+ req.on('close', close);
380
+ // SSE headers
381
+ res.setHeader('Content-Type', 'text/event-stream');
382
+ res.setHeader('Cache-Control', 'no-cache');
383
+ res.setHeader('Connection', 'keep-alive');
384
+ res.setHeader('X-Accel-Buffering', 'no');
385
+ res.flushHeaders();
386
+ const db = openProjectDatabase(projectPath);
387
+ if (!db) {
388
+ await writeSSE(res, { type: 'error', error: 'Project database not found', project: projectPath });
389
+ close();
390
+ return;
391
+ }
392
+ let invocation;
393
+ try {
394
+ invocation = db
395
+ .prepare(`SELECT id, status
396
+ FROM task_invocations
397
+ WHERE task_id = ? AND status = 'running'
398
+ ORDER BY started_at_ms DESC
399
+ LIMIT 1`)
400
+ .get(taskId);
401
+ }
402
+ catch (error) {
403
+ await writeSSE(res, {
404
+ type: 'error',
405
+ error: 'Failed to query running invocation (is the database migrated?)',
406
+ message: error instanceof Error ? error.message : String(error),
407
+ });
408
+ close();
409
+ return;
410
+ }
411
+ finally {
412
+ try {
413
+ db.close();
414
+ }
415
+ catch {
416
+ // ignore
417
+ }
418
+ }
419
+ if (!invocation) {
420
+ await writeSSE(res, { type: 'no_active_invocation', taskId });
421
+ close();
422
+ return;
423
+ }
424
+ const logFile = join(projectPath, '.steroids', 'invocations', `${invocation.id}.log`);
425
+ const isAborted = () => closed || res.writableEnded;
426
+ // If the invocation just started, the log file may not exist yet.
427
+ if (!existsSync(logFile)) {
428
+ await writeSSE(res, { type: 'waiting_for_log', taskId, invocationId: invocation.id });
429
+ const ok = await waitForFile(logFile, { timeoutMs: 5000, pollMs: 100, isAborted });
430
+ if (!ok) {
431
+ await writeSSE(res, { type: 'log_not_found', taskId, invocationId: invocation.id });
432
+ close();
433
+ return;
434
+ }
435
+ }
436
+ try {
437
+ // 1) Send existing log entries first
438
+ await streamJsonlFileToSSE(res, logFile, { isAborted });
439
+ if (isAborted())
440
+ return;
441
+ // 2) Tail for new entries
442
+ const tail = new Tail(logFile, { follow: true, useWatchFile: true });
443
+ let writeChain = Promise.resolve();
444
+ const heartbeat = setInterval(() => {
445
+ // Keep proxies from timing out the connection.
446
+ writeChain = writeChain.then(() => writeSSEComment(res, 'heartbeat')).catch(() => { });
447
+ }, 30000);
448
+ const cleanup = () => {
449
+ clearInterval(heartbeat);
450
+ try {
451
+ tail.unwatch();
452
+ }
453
+ catch {
454
+ // ignore
455
+ }
456
+ close();
457
+ };
458
+ req.on('close', () => {
459
+ clearInterval(heartbeat);
460
+ try {
461
+ tail.unwatch();
462
+ }
463
+ catch { }
464
+ });
465
+ tail.on('line', (line) => {
466
+ if (isAborted())
467
+ return;
468
+ const trimmed = line.trim();
469
+ if (!trimmed)
470
+ return;
471
+ writeChain = writeChain
472
+ .then(async () => {
473
+ try {
474
+ const entry = JSON.parse(trimmed);
475
+ await writeSSE(res, entry);
476
+ if (entry?.type === 'complete' || entry?.type === 'error') {
477
+ cleanup();
478
+ }
479
+ }
480
+ catch {
481
+ // ignore malformed JSONL lines
482
+ }
483
+ })
484
+ .catch(() => { });
485
+ });
486
+ tail.on('error', (err) => {
487
+ void writeChain
488
+ .then(() => writeSSE(res, {
489
+ type: 'error',
490
+ error: 'Tail error',
491
+ message: err instanceof Error ? err.message : String(err),
492
+ }))
493
+ .finally(cleanup);
494
+ });
495
+ }
496
+ catch (error) {
497
+ await writeSSE(res, {
498
+ type: 'error',
499
+ error: 'Failed to stream invocation log',
500
+ message: error instanceof Error ? error.message : String(error),
501
+ });
502
+ close();
503
+ }
504
+ });
505
+ /**
506
+ * GET /api/tasks/:taskId/timeline
507
+ * Parse invocation JSONL activity logs on demand and return a sampled timeline.
508
+ * Query params:
509
+ * - project: string (required) - project path
510
+ */
511
+ router.get('/tasks/:taskId/timeline', async (req, res) => {
512
+ const { taskId } = req.params;
513
+ const projectPath = req.query.project;
514
+ if (!projectPath) {
515
+ res.status(400).json({
516
+ success: false,
517
+ error: 'Missing required query parameter: project',
518
+ });
519
+ return;
520
+ }
521
+ const db = openProjectDatabase(projectPath);
522
+ if (!db) {
523
+ res.status(404).json({
524
+ success: false,
525
+ error: 'Project database not found',
526
+ project: projectPath,
527
+ });
528
+ return;
529
+ }
530
+ let invocations = [];
531
+ try {
532
+ invocations = db
533
+ .prepare(`SELECT id, role, provider, model, started_at_ms, completed_at_ms, status
534
+ FROM task_invocations
535
+ WHERE task_id = ?
536
+ ORDER BY started_at_ms ASC`)
537
+ .all(taskId);
538
+ }
539
+ catch (error) {
540
+ res.status(500).json({
541
+ success: false,
542
+ error: 'Failed to query invocations (is the database migrated?)',
543
+ message: error instanceof Error ? error.message : String(error),
544
+ });
545
+ return;
546
+ }
547
+ finally {
548
+ try {
549
+ db.close();
550
+ }
551
+ catch { }
552
+ }
553
+ const timeline = [];
554
+ for (const inv of invocations) {
555
+ // Invocation start event (from DB lifecycle timestamps).
556
+ timeline.push({
557
+ ts: inv.started_at_ms,
558
+ type: 'invocation.started',
559
+ invocationId: inv.id,
560
+ role: inv.role,
561
+ provider: inv.provider,
562
+ model: inv.model,
563
+ });
564
+ const logFile = join(projectPath, '.steroids', 'invocations', `${inv.id}.log`);
565
+ if (existsSync(logFile)) {
566
+ try {
567
+ const sampled = await readSampledJsonlEntries(logFile, { keepEveryN: 10 });
568
+ for (const e of sampled)
569
+ timeline.push({ ...e, invocationId: inv.id });
570
+ }
571
+ catch {
572
+ // ignore per spec: timeline is best-effort
573
+ }
574
+ }
575
+ // Invocation completion event, when available.
576
+ if (inv.completed_at_ms) {
577
+ timeline.push({
578
+ ts: inv.completed_at_ms,
579
+ type: 'invocation.completed',
580
+ invocationId: inv.id,
581
+ success: inv.status === 'completed',
582
+ duration: inv.completed_at_ms - inv.started_at_ms,
583
+ });
584
+ }
585
+ }
586
+ res.json({ success: true, timeline });
587
+ });
588
+ /**
589
+ * GET /api/tasks/:taskId/logs
590
+ * Get execution logs/audit trail for a task
591
+ * Query params:
592
+ * - project: string (required) - project path
593
+ * - limit: number (optional) - max entries to return (default: 50)
594
+ * - offset: number (optional) - offset for pagination (default: 0)
595
+ */
596
+ router.get('/tasks/:taskId/logs', (req, res) => {
597
+ try {
598
+ const { taskId } = req.params;
599
+ const projectPath = req.query.project;
600
+ const limitParam = req.query.limit;
601
+ const offsetParam = req.query.offset;
602
+ if (!projectPath) {
603
+ res.status(400).json({
604
+ success: false,
605
+ error: 'Missing required query parameter: project',
606
+ });
607
+ return;
608
+ }
609
+ // Parse limit and offset
610
+ let limit = 50;
611
+ let offset = 0;
612
+ if (limitParam !== undefined) {
613
+ const parsed = parseInt(limitParam, 10);
614
+ if (!isNaN(parsed) && parsed > 0) {
615
+ limit = Math.min(parsed, 500); // Cap at 500
616
+ }
617
+ }
618
+ if (offsetParam !== undefined) {
619
+ const parsed = parseInt(offsetParam, 10);
620
+ if (!isNaN(parsed) && parsed >= 0) {
621
+ offset = parsed;
622
+ }
623
+ }
624
+ const db = openProjectDatabase(projectPath);
625
+ if (!db) {
626
+ res.status(404).json({
627
+ success: false,
628
+ error: 'Project database not found',
629
+ project: projectPath,
630
+ });
631
+ return;
632
+ }
633
+ try {
634
+ // Check task exists
635
+ const task = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?').get(taskId);
636
+ if (!task) {
637
+ res.status(404).json({
638
+ success: false,
639
+ error: 'Task not found',
640
+ task_id: taskId,
641
+ });
642
+ return;
643
+ }
644
+ // Get total count
645
+ const countResult = db
646
+ .prepare('SELECT COUNT(*) as count FROM audit WHERE task_id = ?')
647
+ .get(taskId);
648
+ // Get audit entries with pagination
649
+ const logs = db
650
+ .prepare(`SELECT id, task_id, from_status, to_status, actor, actor_type, model, notes, commit_sha, created_at
651
+ FROM audit
652
+ WHERE task_id = ?
653
+ ORDER BY created_at DESC
654
+ LIMIT ? OFFSET ?`)
655
+ .all(taskId, limit, offset);
656
+ res.json({
657
+ success: true,
658
+ task_id: taskId,
659
+ task_title: task.title,
660
+ task_status: task.status,
661
+ logs,
662
+ pagination: {
663
+ total: countResult.count,
664
+ limit,
665
+ offset,
666
+ has_more: offset + logs.length < countResult.count,
667
+ },
668
+ });
669
+ }
670
+ finally {
671
+ db.close();
672
+ }
673
+ }
674
+ catch (error) {
675
+ console.error('Error getting task logs:', error);
676
+ res.status(500).json({
677
+ success: false,
678
+ error: 'Failed to get task logs',
679
+ message: error instanceof Error ? error.message : 'Unknown error',
680
+ });
681
+ }
682
+ });
683
+ /**
684
+ * GET /api/projects/:projectPath/sections
685
+ * List all sections for a project with task counts by status
686
+ */
687
+ router.get('/projects/:projectPath(*)/sections', (req, res) => {
688
+ try {
689
+ const projectPath = decodeURIComponent(req.params.projectPath);
690
+ const db = openProjectDatabase(projectPath);
691
+ if (!db) {
692
+ res.status(404).json({
693
+ success: false,
694
+ error: 'Project database not found',
695
+ project: projectPath,
696
+ });
697
+ return;
698
+ }
699
+ try {
700
+ // Check if priority column exists (older databases may not have it)
701
+ const hasPriority = (() => {
702
+ try {
703
+ const cols = db.prepare("PRAGMA table_info(sections)").all();
704
+ return cols.some(c => c.name === 'priority');
705
+ }
706
+ catch {
707
+ return false;
708
+ }
709
+ })();
710
+ const prioritySelect = hasPriority ? 's.priority,' : '50 as priority,';
711
+ const orderBy = hasPriority ? 'ORDER BY s.priority DESC, s.name ASC' : 'ORDER BY s.name ASC';
712
+ // Get sections with task counts by status
713
+ const sections = db
714
+ .prepare(`SELECT
715
+ s.id,
716
+ s.name,
717
+ ${prioritySelect}
718
+ s.created_at,
719
+ COUNT(t.id) as total_tasks,
720
+ SUM(CASE WHEN t.status = 'pending' THEN 1 ELSE 0 END) as pending,
721
+ SUM(CASE WHEN t.status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
722
+ SUM(CASE WHEN t.status = 'review' THEN 1 ELSE 0 END) as review,
723
+ SUM(CASE WHEN t.status = 'completed' THEN 1 ELSE 0 END) as completed,
724
+ SUM(CASE WHEN t.status = 'failed' THEN 1 ELSE 0 END) as failed,
725
+ SUM(CASE WHEN t.status = 'skipped' THEN 1 ELSE 0 END) as skipped
726
+ FROM sections s
727
+ LEFT JOIN tasks t ON t.section_id = s.id
728
+ GROUP BY s.id
729
+ ${orderBy}`)
730
+ .all();
731
+ // Also get tasks without a section (null section_id)
732
+ const unassigned = db
733
+ .prepare(`SELECT
734
+ COUNT(*) as total_tasks,
735
+ SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
736
+ SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
737
+ SUM(CASE WHEN status = 'review' THEN 1 ELSE 0 END) as review,
738
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
739
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed,
740
+ SUM(CASE WHEN status = 'skipped' THEN 1 ELSE 0 END) as skipped
741
+ FROM tasks
742
+ WHERE section_id IS NULL`)
743
+ .get();
744
+ res.json({
745
+ success: true,
746
+ project: projectPath,
747
+ sections,
748
+ unassigned: unassigned.total_tasks > 0 ? unassigned : null,
749
+ });
750
+ }
751
+ finally {
752
+ db.close();
753
+ }
754
+ }
755
+ catch (error) {
756
+ console.error('Error listing project sections:', error);
757
+ res.status(500).json({
758
+ success: false,
759
+ error: 'Failed to list project sections',
760
+ message: error instanceof Error ? error.message : 'Unknown error',
761
+ });
762
+ }
763
+ });
764
+ /**
765
+ * GET /api/projects/:projectPath/tasks
766
+ * List all tasks for a project
767
+ * Query params:
768
+ * - status: string (optional) - filter by status
769
+ * - section: string (optional) - filter by section id
770
+ * - limit: number (optional) - max entries (default: 100)
771
+ */
772
+ router.get('/projects/:projectPath(*)/tasks', (req, res) => {
773
+ try {
774
+ // projectPath comes URL-encoded, decode it
775
+ const projectPath = decodeURIComponent(req.params.projectPath);
776
+ const statusFilter = req.query.status;
777
+ const sectionFilter = req.query.section;
778
+ const limitParam = req.query.limit;
779
+ let limit = 100;
780
+ if (limitParam !== undefined) {
781
+ const parsed = parseInt(limitParam, 10);
782
+ if (!isNaN(parsed) && parsed > 0) {
783
+ limit = Math.min(parsed, 500);
784
+ }
785
+ }
786
+ const db = openProjectDatabase(projectPath);
787
+ if (!db) {
788
+ res.status(404).json({
789
+ success: false,
790
+ error: 'Project database not found',
791
+ project: projectPath,
792
+ });
793
+ return;
794
+ }
795
+ try {
796
+ let query = `
797
+ SELECT
798
+ t.id, t.title, t.status, t.section_id,
799
+ s.name as section_name,
800
+ t.source_file, t.rejection_count,
801
+ t.created_at, t.updated_at
802
+ FROM tasks t
803
+ LEFT JOIN sections s ON t.section_id = s.id
804
+ WHERE 1=1
805
+ `;
806
+ const params = [];
807
+ if (statusFilter) {
808
+ query += ' AND t.status = ?';
809
+ params.push(statusFilter);
810
+ }
811
+ if (sectionFilter) {
812
+ query += ' AND t.section_id = ?';
813
+ params.push(sectionFilter);
814
+ }
815
+ query += ' ORDER BY t.created_at DESC LIMIT ?';
816
+ params.push(limit);
817
+ const tasks = db.prepare(query).all(...params);
818
+ // Get task counts by status
819
+ const statusCounts = db
820
+ .prepare(`SELECT status, COUNT(*) as count
821
+ FROM tasks
822
+ GROUP BY status`)
823
+ .all();
824
+ const counts = statusCounts.reduce((acc, { status, count }) => {
825
+ acc[status] = count;
826
+ return acc;
827
+ }, {});
828
+ res.json({
829
+ success: true,
830
+ project: projectPath,
831
+ tasks,
832
+ count: tasks.length,
833
+ status_counts: counts,
834
+ });
835
+ }
836
+ finally {
837
+ db.close();
838
+ }
839
+ }
840
+ catch (error) {
841
+ console.error('Error listing project tasks:', error);
842
+ res.status(500).json({
843
+ success: false,
844
+ error: 'Failed to list project tasks',
845
+ message: error instanceof Error ? error.message : 'Unknown error',
846
+ });
847
+ }
848
+ });
849
+ /**
850
+ * GET /api/tasks/:taskId/invocations/:invocationId
851
+ * Get full details for a specific invocation including prompt and response
852
+ * Query params:
853
+ * - project: string (required) - project path
854
+ */
855
+ router.get('/tasks/:taskId/invocations/:invocationId', (req, res) => {
856
+ try {
857
+ const { taskId, invocationId } = req.params;
858
+ const projectPath = req.query.project;
859
+ if (!projectPath) {
860
+ res.status(400).json({
861
+ success: false,
862
+ error: 'Missing required query parameter: project',
863
+ });
864
+ return;
865
+ }
866
+ const db = openProjectDatabase(projectPath);
867
+ if (!db) {
868
+ res.status(404).json({
869
+ success: false,
870
+ error: 'Project database not found',
871
+ project: projectPath,
872
+ });
873
+ return;
874
+ }
875
+ try {
876
+ // Get full invocation details including prompt and response
877
+ const invocation = db
878
+ .prepare(`SELECT id, task_id, role, provider, model, prompt, response, error, exit_code, duration_ms, success, timed_out, rejection_number, created_at
879
+ FROM task_invocations
880
+ WHERE id = ? AND task_id = ?`)
881
+ .get(invocationId, taskId);
882
+ if (!invocation) {
883
+ res.status(404).json({
884
+ success: false,
885
+ error: 'Invocation not found',
886
+ invocation_id: invocationId,
887
+ task_id: taskId,
888
+ });
889
+ return;
890
+ }
891
+ res.json({
892
+ success: true,
893
+ invocation,
894
+ });
895
+ }
896
+ finally {
897
+ db.close();
898
+ }
899
+ }
900
+ catch (error) {
901
+ console.error('Error getting invocation details:', error);
902
+ res.status(500).json({
903
+ success: false,
904
+ error: 'Failed to get invocation details',
905
+ message: error instanceof Error ? error.message : 'Unknown error',
906
+ });
907
+ }
908
+ });
909
+ /**
910
+ * POST /api/tasks/:taskId/restart
911
+ * Restart a failed/disputed task by resetting rejection count and setting status to pending
912
+ * Body: { project: string, notes?: string }
913
+ * Notes are stored in the audit entry as human guidance for the coder
914
+ */
915
+ router.post('/tasks/:taskId/restart', (req, res) => {
916
+ try {
917
+ const { taskId } = req.params;
918
+ const projectPath = req.body.project;
919
+ const notes = req.body.notes;
920
+ if (!projectPath) {
921
+ res.status(400).json({
922
+ success: false,
923
+ error: 'Missing required body parameter: project',
924
+ });
925
+ return;
926
+ }
927
+ const dbPath = join(projectPath, '.steroids', 'steroids.db');
928
+ if (!existsSync(dbPath)) {
929
+ res.status(404).json({
930
+ success: false,
931
+ error: 'Project database not found',
932
+ project: projectPath,
933
+ });
934
+ return;
935
+ }
936
+ let db;
937
+ try {
938
+ db = new Database(dbPath); // Writable mode
939
+ }
940
+ catch {
941
+ res.status(500).json({
942
+ success: false,
943
+ error: 'Failed to open project database',
944
+ });
945
+ return;
946
+ }
947
+ try {
948
+ // Check task exists
949
+ const task = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?').get(taskId);
950
+ if (!task) {
951
+ res.status(404).json({
952
+ success: false,
953
+ error: 'Task not found',
954
+ task_id: taskId,
955
+ });
956
+ return;
957
+ }
958
+ // Block restart for tasks already in progress
959
+ if (task.status === 'in_progress' || task.status === 'review') {
960
+ res.status(400).json({
961
+ success: false,
962
+ error: `Cannot restart task in ${task.status} status. Task is currently being worked on.`,
963
+ });
964
+ return;
965
+ }
966
+ // Resolve any open disputes for this task
967
+ const openDisputes = db
968
+ .prepare(`SELECT id FROM disputes WHERE task_id = ? AND status = 'open'`)
969
+ .all(taskId);
970
+ for (const dispute of openDisputes) {
971
+ db.prepare(`UPDATE disputes
972
+ SET status = 'resolved', resolution = 'custom', resolution_notes = ?,
973
+ resolved_by = 'human:webui', resolved_at = datetime('now')
974
+ WHERE id = ?`).run(notes || 'Resolved via WebUI restart', dispute.id);
975
+ }
976
+ // Reset task: set status to pending and rejection_count to 0
977
+ db.prepare(`UPDATE tasks
978
+ SET status = 'pending', rejection_count = 0, updated_at = datetime('now')
979
+ WHERE id = ?`).run(taskId);
980
+ // Add audit entry with human guidance notes
981
+ const auditNotes = notes
982
+ ? `Task restarted via WebUI. Human guidance: ${notes}`
983
+ : 'Task restarted via WebUI';
984
+ db.prepare(`INSERT INTO audit (task_id, from_status, to_status, actor, actor_type, notes, created_at)
985
+ VALUES (?, ?, 'pending', 'human:webui', 'human', ?, datetime('now'))`).run(taskId, task.status, auditNotes);
986
+ res.json({
987
+ success: true,
988
+ message: 'Task restarted successfully',
989
+ task_id: taskId,
990
+ disputes_resolved: openDisputes.length,
991
+ });
992
+ }
993
+ finally {
994
+ db.close();
995
+ }
996
+ }
997
+ catch (error) {
998
+ console.error('Error restarting task:', error);
999
+ res.status(500).json({
1000
+ success: false,
1001
+ error: 'Failed to restart task',
1002
+ message: error instanceof Error ? error.message : 'Unknown error',
1003
+ });
1004
+ }
1005
+ });
1006
+ export default router;
1007
+ //# sourceMappingURL=tasks.js.map