@repo-os/repoos 0.5.45

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 (362) hide show
  1. package/LICENSE.md +110 -0
  2. package/README.md +272 -0
  3. package/dist/.build-info.json +4 -0
  4. package/dist/.build-stamp.json +3 -0
  5. package/dist/cli/colors.d.ts +13 -0
  6. package/dist/cli/colors.js +37 -0
  7. package/dist/cli/index.d.ts +2 -0
  8. package/dist/cli/index.js +221 -0
  9. package/dist/commands/check.d.ts +170 -0
  10. package/dist/commands/check.js +1227 -0
  11. package/dist/commands/docs.d.ts +2 -0
  12. package/dist/commands/docs.js +70 -0
  13. package/dist/commands/gc.d.ts +1 -0
  14. package/dist/commands/gc.js +57 -0
  15. package/dist/commands/init.d.ts +9 -0
  16. package/dist/commands/init.js +699 -0
  17. package/dist/commands/screenshots.d.ts +7 -0
  18. package/dist/commands/screenshots.js +77 -0
  19. package/dist/commands/serve.d.ts +55 -0
  20. package/dist/commands/serve.js +204 -0
  21. package/dist/commands/status.d.ts +140 -0
  22. package/dist/commands/status.js +569 -0
  23. package/dist/commands/stop.d.ts +1 -0
  24. package/dist/commands/stop.js +59 -0
  25. package/dist/commands/tasks.d.ts +47 -0
  26. package/dist/commands/tasks.js +334 -0
  27. package/dist/commands/tunnel.d.ts +1 -0
  28. package/dist/commands/tunnel.js +1271 -0
  29. package/dist/commands/ui-harness.d.ts +53 -0
  30. package/dist/commands/ui-harness.js +62 -0
  31. package/dist/commands/ui-smoke.d.ts +6 -0
  32. package/dist/commands/ui-smoke.js +220 -0
  33. package/dist/commands/upgrade.d.ts +15 -0
  34. package/dist/commands/upgrade.js +188 -0
  35. package/dist/core/auth-store.d.ts +81 -0
  36. package/dist/core/auth-store.js +530 -0
  37. package/dist/core/auth.d.ts +75 -0
  38. package/dist/core/auth.js +124 -0
  39. package/dist/core/bootstrap.d.ts +41 -0
  40. package/dist/core/bootstrap.js +236 -0
  41. package/dist/core/build.d.ts +92 -0
  42. package/dist/core/build.js +249 -0
  43. package/dist/core/canary.d.ts +10 -0
  44. package/dist/core/canary.js +14 -0
  45. package/dist/core/check-failure-summary.d.ts +10 -0
  46. package/dist/core/check-failure-summary.js +34 -0
  47. package/dist/core/config.d.ts +160 -0
  48. package/dist/core/config.js +1288 -0
  49. package/dist/core/context-pack.d.ts +74 -0
  50. package/dist/core/context-pack.js +778 -0
  51. package/dist/core/db.d.ts +236 -0
  52. package/dist/core/db.js +747 -0
  53. package/dist/core/detect.d.ts +69 -0
  54. package/dist/core/detect.js +251 -0
  55. package/dist/core/docs.d.ts +76 -0
  56. package/dist/core/docs.js +225 -0
  57. package/dist/core/frontmatter.d.ts +29 -0
  58. package/dist/core/frontmatter.js +215 -0
  59. package/dist/core/git.d.ts +505 -0
  60. package/dist/core/git.js +1453 -0
  61. package/dist/core/indexer.d.ts +33 -0
  62. package/dist/core/indexer.js +282 -0
  63. package/dist/core/input.d.ts +66 -0
  64. package/dist/core/input.js +222 -0
  65. package/dist/core/logger.d.ts +70 -0
  66. package/dist/core/logger.js +138 -0
  67. package/dist/core/models.d.ts +45 -0
  68. package/dist/core/models.js +291 -0
  69. package/dist/core/net-probe.d.ts +4 -0
  70. package/dist/core/net-probe.js +20 -0
  71. package/dist/core/providers/deepinfra.d.ts +23 -0
  72. package/dist/core/providers/deepinfra.js +44 -0
  73. package/dist/core/providers/index.d.ts +24 -0
  74. package/dist/core/providers/index.js +59 -0
  75. package/dist/core/providers/openrouter.d.ts +28 -0
  76. package/dist/core/providers/openrouter.js +82 -0
  77. package/dist/core/providers/spend.d.ts +86 -0
  78. package/dist/core/providers/spend.js +286 -0
  79. package/dist/core/providers/types.d.ts +56 -0
  80. package/dist/core/providers/types.js +39 -0
  81. package/dist/core/repoos.d.ts +59 -0
  82. package/dist/core/repoos.js +221 -0
  83. package/dist/core/runtime.d.ts +35 -0
  84. package/dist/core/runtime.js +167 -0
  85. package/dist/core/skills-registry.d.ts +39 -0
  86. package/dist/core/skills-registry.js +148 -0
  87. package/dist/core/skills-registry.test.d.ts +1 -0
  88. package/dist/core/skills-registry.test.js +52 -0
  89. package/dist/core/sysmem.d.ts +7 -0
  90. package/dist/core/sysmem.js +55 -0
  91. package/dist/core/tailscale.d.ts +2 -0
  92. package/dist/core/tailscale.js +19 -0
  93. package/dist/core/task.d.ts +53 -0
  94. package/dist/core/task.js +310 -0
  95. package/dist/core/toml-line.d.ts +20 -0
  96. package/dist/core/toml-line.js +52 -0
  97. package/dist/core/toml-validate.d.ts +44 -0
  98. package/dist/core/toml-validate.js +335 -0
  99. package/dist/core/tunnel-assistant.d.ts +30 -0
  100. package/dist/core/tunnel-assistant.js +65 -0
  101. package/dist/core/tunnel-registry.d.ts +96 -0
  102. package/dist/core/tunnel-registry.js +267 -0
  103. package/dist/core/tunnel-registry.test.d.ts +1 -0
  104. package/dist/core/tunnel-registry.test.js +309 -0
  105. package/dist/core/tunnel.d.ts +103 -0
  106. package/dist/core/tunnel.js +320 -0
  107. package/dist/core/types.d.ts +811 -0
  108. package/dist/core/types.js +27 -0
  109. package/dist/core/worktree-gc.d.ts +57 -0
  110. package/dist/core/worktree-gc.js +194 -0
  111. package/dist/server/__tests__/integration-job.test.d.ts +1 -0
  112. package/dist/server/__tests__/integration-job.test.js +129 -0
  113. package/dist/server/agents.d.ts +1020 -0
  114. package/dist/server/agents.js +4070 -0
  115. package/dist/server/attachments.d.ts +51 -0
  116. package/dist/server/attachments.js +130 -0
  117. package/dist/server/auto-engineering.d.ts +45 -0
  118. package/dist/server/auto-engineering.js +275 -0
  119. package/dist/server/branchless-release.d.ts +22 -0
  120. package/dist/server/branchless-release.js +67 -0
  121. package/dist/server/built-in-agents.d.ts +305 -0
  122. package/dist/server/built-in-agents.js +1838 -0
  123. package/dist/server/cto-monitor.d.ts +35 -0
  124. package/dist/server/cto-monitor.js +281 -0
  125. package/dist/server/cto.d.ts +125 -0
  126. package/dist/server/cto.js +590 -0
  127. package/dist/server/deployments.d.ts +119 -0
  128. package/dist/server/deployments.js +325 -0
  129. package/dist/server/diff-snapshot.d.ts +20 -0
  130. package/dist/server/diff-snapshot.js +73 -0
  131. package/dist/server/done.d.ts +160 -0
  132. package/dist/server/done.js +488 -0
  133. package/dist/server/freeform.d.ts +55 -0
  134. package/dist/server/freeform.js +179 -0
  135. package/dist/server/handoff.d.ts +99 -0
  136. package/dist/server/handoff.js +575 -0
  137. package/dist/server/hetzner.d.ts +45 -0
  138. package/dist/server/hetzner.js +117 -0
  139. package/dist/server/icons.d.ts +8 -0
  140. package/dist/server/icons.js +195 -0
  141. package/dist/server/integration-job.d.ts +75 -0
  142. package/dist/server/integration-job.js +144 -0
  143. package/dist/server/integration-orchestrator.d.ts +211 -0
  144. package/dist/server/integration-orchestrator.js +1299 -0
  145. package/dist/server/integration-status.d.ts +48 -0
  146. package/dist/server/integration-status.js +71 -0
  147. package/dist/server/live-index.d.ts +295 -0
  148. package/dist/server/live-index.js +452 -0
  149. package/dist/server/model-test.d.ts +20 -0
  150. package/dist/server/model-test.js +100 -0
  151. package/dist/server/ntfy.d.ts +57 -0
  152. package/dist/server/ntfy.js +112 -0
  153. package/dist/server/pm-attachments.d.ts +35 -0
  154. package/dist/server/pm-attachments.js +181 -0
  155. package/dist/server/pm-runs.d.ts +58 -0
  156. package/dist/server/pm-runs.js +80 -0
  157. package/dist/server/preview.d.ts +174 -0
  158. package/dist/server/preview.js +641 -0
  159. package/dist/server/release.d.ts +88 -0
  160. package/dist/server/release.js +449 -0
  161. package/dist/server/reload.d.ts +194 -0
  162. package/dist/server/reload.js +586 -0
  163. package/dist/server/remote-validation.d.ts +132 -0
  164. package/dist/server/remote-validation.js +478 -0
  165. package/dist/server/repo-lock.d.ts +36 -0
  166. package/dist/server/repo-lock.js +141 -0
  167. package/dist/server/review-guard.d.ts +14 -0
  168. package/dist/server/review-guard.js +159 -0
  169. package/dist/server/review.d.ts +243 -0
  170. package/dist/server/review.js +1211 -0
  171. package/dist/server/routes/agents.d.ts +6 -0
  172. package/dist/server/routes/agents.js +28 -0
  173. package/dist/server/routes/auth.d.ts +60 -0
  174. package/dist/server/routes/auth.js +755 -0
  175. package/dist/server/routes/config.d.ts +20 -0
  176. package/dist/server/routes/config.js +347 -0
  177. package/dist/server/routes/debugger.d.ts +33 -0
  178. package/dist/server/routes/debugger.js +360 -0
  179. package/dist/server/routes/deployments.d.ts +3 -0
  180. package/dist/server/routes/deployments.js +16 -0
  181. package/dist/server/routes/docs.d.ts +8 -0
  182. package/dist/server/routes/docs.js +135 -0
  183. package/dist/server/routes/helpers.d.ts +21 -0
  184. package/dist/server/routes/helpers.js +171 -0
  185. package/dist/server/routes/index.d.ts +19 -0
  186. package/dist/server/routes/index.js +18 -0
  187. package/dist/server/routes/info.d.ts +17 -0
  188. package/dist/server/routes/info.js +177 -0
  189. package/dist/server/routes/inputs.d.ts +7 -0
  190. package/dist/server/routes/inputs.js +125 -0
  191. package/dist/server/routes/model-providers.d.ts +27 -0
  192. package/dist/server/routes/model-providers.js +94 -0
  193. package/dist/server/routes/models.d.ts +3 -0
  194. package/dist/server/routes/models.js +36 -0
  195. package/dist/server/routes/notify.d.ts +2 -0
  196. package/dist/server/routes/notify.js +17 -0
  197. package/dist/server/routes/playground.d.ts +29 -0
  198. package/dist/server/routes/playground.js +81 -0
  199. package/dist/server/routes/release.d.ts +20 -0
  200. package/dist/server/routes/release.js +86 -0
  201. package/dist/server/routes/router.d.ts +12 -0
  202. package/dist/server/routes/router.js +32 -0
  203. package/dist/server/routes/skill-registry.d.ts +5 -0
  204. package/dist/server/routes/skill-registry.js +51 -0
  205. package/dist/server/routes/tasks.d.ts +58 -0
  206. package/dist/server/routes/tasks.js +1442 -0
  207. package/dist/server/routes/transcribe.d.ts +2 -0
  208. package/dist/server/routes/transcribe.js +124 -0
  209. package/dist/server/routes/types.d.ts +68 -0
  210. package/dist/server/routes/types.js +1 -0
  211. package/dist/server/routes/ui.d.ts +5 -0
  212. package/dist/server/routes/ui.js +81 -0
  213. package/dist/server/routes/utils.d.ts +3 -0
  214. package/dist/server/routes/utils.js +23 -0
  215. package/dist/server/serve-reaper.d.ts +135 -0
  216. package/dist/server/serve-reaper.js +491 -0
  217. package/dist/server/server.d.ts +49 -0
  218. package/dist/server/server.js +2171 -0
  219. package/dist/server/supervisor.d.ts +89 -0
  220. package/dist/server/supervisor.js +231 -0
  221. package/dist/server/system.d.ts +198 -0
  222. package/dist/server/system.js +480 -0
  223. package/dist/server/task-check.d.ts +45 -0
  224. package/dist/server/task-check.js +71 -0
  225. package/dist/server/task-transitions.d.ts +51 -0
  226. package/dist/server/task-transitions.js +78 -0
  227. package/dist/server/task-watchdog.d.ts +158 -0
  228. package/dist/server/task-watchdog.js +681 -0
  229. package/dist/server/test-run.d.ts +28 -0
  230. package/dist/server/test-run.js +81 -0
  231. package/dist/server/watcher.d.ts +21 -0
  232. package/dist/server/watcher.js +194 -0
  233. package/dist/server/write.d.ts +94 -0
  234. package/dist/server/write.js +345 -0
  235. package/dist/ui/assets/AgentsView-CF4wRvHh.css +1 -0
  236. package/dist/ui/assets/AgentsView-D-myL-kA.js +1 -0
  237. package/dist/ui/assets/ContextView-D3TC-RSp.css +1 -0
  238. package/dist/ui/assets/ContextView-DsOqAdyE.js +4 -0
  239. package/dist/ui/assets/DashboardView-BOhsIJqO.js +1 -0
  240. package/dist/ui/assets/DashboardView-Ck9o0RP2.css +1 -0
  241. package/dist/ui/assets/DeploymentsView--iIUVfEH.css +1 -0
  242. package/dist/ui/assets/DeploymentsView-DXu_FYR2.js +1 -0
  243. package/dist/ui/assets/InputsView-24HVbVPk.js +1 -0
  244. package/dist/ui/assets/InputsView-BLb7GHtx.css +1 -0
  245. package/dist/ui/assets/LoginView-6QlGg6cx.css +1 -0
  246. package/dist/ui/assets/LoginView-BqupI0Lt.js +1 -0
  247. package/dist/ui/assets/ReleasesView-BEG1A0YH.css +1 -0
  248. package/dist/ui/assets/ReleasesView-CAgMuxpe.js +3 -0
  249. package/dist/ui/assets/SettingsView-CA-NYSMK.css +1 -0
  250. package/dist/ui/assets/SettingsView-O3iZSnjg.js +3 -0
  251. package/dist/ui/assets/WorkView-D8Tl6qwy.js +1 -0
  252. package/dist/ui/assets/WorkView-DMYL2s3W.css +1 -0
  253. package/dist/ui/assets/abnfDiagram-VCTEODGH-J5lkSnEE.js +1 -0
  254. package/dist/ui/assets/arc-BHZnmhhM.js +1 -0
  255. package/dist/ui/assets/architecture-7GRP2DOG-pLdFt-xS.js +1 -0
  256. package/dist/ui/assets/architectureDiagram-5GKGNRK7-C3k89uKX.js +36 -0
  257. package/dist/ui/assets/array-BifhSqXX.js +1 -0
  258. package/dist/ui/assets/blockDiagram-I7D4REHJ-AWNoBzop.js +129 -0
  259. package/dist/ui/assets/button-lbikqSph.js +1 -0
  260. package/dist/ui/assets/c4Diagram-7LVT6UL2-DKG1D5gd.js +38 -0
  261. package/dist/ui/assets/channel-gA4qBv3A.js +1 -0
  262. package/dist/ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  263. package/dist/ui/assets/chunk-4HAMMTFA-D1YR8lF_.js +62 -0
  264. package/dist/ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  265. package/dist/ui/assets/chunk-75Z2AOVW-5GuBxfsL.js +2 -0
  266. package/dist/ui/assets/chunk-DU6HZSFF-DfdNNqwa.js +127 -0
  267. package/dist/ui/assets/chunk-F27PBJKO-BPZzinQ4.js +1 -0
  268. package/dist/ui/assets/chunk-FOHPRMQF-l2z4K2jD.js +161 -0
  269. package/dist/ui/assets/chunk-GMAD6QVW-DGWvSKj5.js +72 -0
  270. package/dist/ui/assets/chunk-GVQU2GXP-D6Aw75wJ.js +1 -0
  271. package/dist/ui/assets/chunk-IMKFNOWR-B8mMBKCW.js +231 -0
  272. package/dist/ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  273. package/dist/ui/assets/chunk-L3NEJ4N5-D5OJuXYE.js +1 -0
  274. package/dist/ui/assets/chunk-OSK3NFVY-B7r9w2UW.js +10 -0
  275. package/dist/ui/assets/chunk-P2QGCYS3-C74X4AQX.js +1 -0
  276. package/dist/ui/assets/chunk-POPQ4Y6H-C5_u1gUS.js +1 -0
  277. package/dist/ui/assets/chunk-PWAF6VOD-DfoTLtxn.js +1 -0
  278. package/dist/ui/assets/chunk-SHT3W25Y-D40m2eqz.js +168 -0
  279. package/dist/ui/assets/chunk-SVP7TREG-euxi0Kap.js +88 -0
  280. package/dist/ui/assets/chunk-TICWLB2K-fkdxngzN.js +206 -0
  281. package/dist/ui/assets/chunk-XXDRQBXY-DFBUG-OT.js +1 -0
  282. package/dist/ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  283. package/dist/ui/assets/classDiagram-ZZMXUADV-BzW8bq2P.js +1 -0
  284. package/dist/ui/assets/classDiagram-v2-VYDZK3BY-BzW8bq2P.js +1 -0
  285. package/dist/ui/assets/cose-bilkent-JH36ORCC-BTcgMon1.js +1 -0
  286. package/dist/ui/assets/cynefin-OW5HDTMX-B5m_COsS.js +1 -0
  287. package/dist/ui/assets/cynefinDiagram-5FMLGOSQ-C-AG6CGe.js +62 -0
  288. package/dist/ui/assets/cytoscape.esm-FBntH-9t.js +321 -0
  289. package/dist/ui/assets/dagre-GXQ25YYZ-DDfDnp8L.js +4 -0
  290. package/dist/ui/assets/dagre-PrKaheQc.js +1 -0
  291. package/dist/ui/assets/defaultLocale-BFoDCU3G.js +1 -0
  292. package/dist/ui/assets/diagram-S7CK7UJ4-DjQZAlEY.js +30 -0
  293. package/dist/ui/assets/diagram-UQ7AKVKN-DAZ9yp1B.js +41 -0
  294. package/dist/ui/assets/diagram-VSXAHHWV-BWepUKJD.js +3 -0
  295. package/dist/ui/assets/diagram-VX7I27RA-EcV1J81b.js +24 -0
  296. package/dist/ui/assets/diagram-Z3DM3KII-Dbz7XU_K.js +24 -0
  297. package/dist/ui/assets/dist-DTg6UBE_.js +1 -0
  298. package/dist/ui/assets/ebnfDiagram-PWID7BFC-BUyQw8Mm.js +1 -0
  299. package/dist/ui/assets/erDiagram-RLTQ6QDP-WVf8ukKt.js +99 -0
  300. package/dist/ui/assets/eventmodeling-NTZA5JFV-DHqyLOAn.js +1 -0
  301. package/dist/ui/assets/flowDiagram-HODETNUW-CYk_ze5K.js +1 -0
  302. package/dist/ui/assets/ganttDiagram-EL5Y4UJY-rB4MZuB-.js +292 -0
  303. package/dist/ui/assets/gitGraph-4MIJSDKK-BOTWM8dY.js +1 -0
  304. package/dist/ui/assets/gitGraphDiagram-WWUBYQGX-eYscuhtP.js +106 -0
  305. package/dist/ui/assets/graphlib-DS17s2tU.js +1 -0
  306. package/dist/ui/assets/index-BACBfnga.css +1 -0
  307. package/dist/ui/assets/index-BOQ-ust6.js +26 -0
  308. package/dist/ui/assets/info-A6RAGUB7-_67wOORe.js +1 -0
  309. package/dist/ui/assets/infoDiagram-27XIBGKW-XVXbddTA.js +2 -0
  310. package/dist/ui/assets/init-C-OQMol4.js +1 -0
  311. package/dist/ui/assets/ishikawaDiagram-5VMMS53U-ClLNiEru.js +70 -0
  312. package/dist/ui/assets/journeyDiagram-3NMN7TZE-NRawjR6i.js +139 -0
  313. package/dist/ui/assets/kanban-definition-UXKFOSKX-BgSzBfIx.js +89 -0
  314. package/dist/ui/assets/katex-DolUETbr.js +257 -0
  315. package/dist/ui/assets/line-KAyfuul4.js +1 -0
  316. package/dist/ui/assets/linear-cCCPOpu7.js +1 -0
  317. package/dist/ui/assets/mermaid-parser.core-CgXZk1iN.js +7 -0
  318. package/dist/ui/assets/mermaid.core-BUUkChHW.js +44 -0
  319. package/dist/ui/assets/mindmap-definition-YA3MSWOX-Cpn7vW4r.js +96 -0
  320. package/dist/ui/assets/ordinal-BDEzSJ7C.js +1 -0
  321. package/dist/ui/assets/packet-AYTQ26CC-0lZwM-d6.js +1 -0
  322. package/dist/ui/assets/path-COt_16Va.js +1 -0
  323. package/dist/ui/assets/pegDiagram-XKGWAZYB-impy0inn.js +1 -0
  324. package/dist/ui/assets/pie-WAS4IAKB-CqfdGHYg.js +1 -0
  325. package/dist/ui/assets/pieDiagram-E7YTZNPT-CxjL4I1h.js +39 -0
  326. package/dist/ui/assets/quadrantDiagram-AXDQQJYC-Cjd1QMIe.js +7 -0
  327. package/dist/ui/assets/radar-RG4KPBEZ-CRkQIwiv.js +1 -0
  328. package/dist/ui/assets/railroad-74A4TZTK-i67XSWTB.js +1 -0
  329. package/dist/ui/assets/railroad-abnf-HS5TGJTU-Cv9m5aS3.js +1 -0
  330. package/dist/ui/assets/railroad-ebnf-LZEXJU2U-C5ZIn-Jh.js +1 -0
  331. package/dist/ui/assets/railroad-peg-WCYAUIDC-CGra9Oq-.js +1 -0
  332. package/dist/ui/assets/railroadDiagram-O6MQD6OU-_pK0qyaY.js +1 -0
  333. package/dist/ui/assets/releases-B0WNV-SY.js +1 -0
  334. package/dist/ui/assets/repoos-cto-square.webp +0 -0
  335. package/dist/ui/assets/repoos-helpful-pm-square.webp +0 -0
  336. package/dist/ui/assets/repoos-orchestrator-square.webp +0 -0
  337. package/dist/ui/assets/repoos-performance-agent-square.webp +0 -0
  338. package/dist/ui/assets/repoos-ross-from-friends-square.webp +0 -0
  339. package/dist/ui/assets/repoos-tech-debt-agent-square.webp +0 -0
  340. package/dist/ui/assets/requirementDiagram-BXWQKSXE-CRrFX-yw.js +84 -0
  341. package/dist/ui/assets/rough.esm-By172zw-.js +1 -0
  342. package/dist/ui/assets/sankeyDiagram-P5KCCOFB-X_xFRj9E.js +40 -0
  343. package/dist/ui/assets/sequenceDiagram-WJ2MYXX4-Byb49lKf.js +162 -0
  344. package/dist/ui/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
  345. package/dist/ui/assets/src-BH-TyZbA.js +1 -0
  346. package/dist/ui/assets/stateDiagram-D77RDMKH-CzaPrqYF.js +1 -0
  347. package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-BCXckz7K.js +1 -0
  348. package/dist/ui/assets/swimlanes-42K2YHIH-Cx_1lnqr.js +1 -0
  349. package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-54qsKi6A.js +8 -0
  350. package/dist/ui/assets/timeline-definition-24CTP7MA-B1IbZPQ-.js +120 -0
  351. package/dist/ui/assets/treeView-Q6P3EWNA-DFWhVE3d.js +1 -0
  352. package/dist/ui/assets/treemap-WGGIJYW6-BmbYPThn.js +1 -0
  353. package/dist/ui/assets/vendor-ui-DPm7zLxi.js +3 -0
  354. package/dist/ui/assets/vendor-vue-CobgyEJp.js +1 -0
  355. package/dist/ui/assets/vennDiagram-4TSXK5OY-Ae2AtRpt.js +34 -0
  356. package/dist/ui/assets/wardley-WFR3VGLG-BtD4X4tL.js +1 -0
  357. package/dist/ui/assets/wardleyDiagram-VM6X3IG4-B3YN2fTd.js +78 -0
  358. package/dist/ui/assets/xychartDiagram-S5SC5T6Z-CIzJfMKA.js +7 -0
  359. package/dist/ui/favicon.svg +11 -0
  360. package/dist/ui/index.html +49 -0
  361. package/dist/ui/sw.js +48 -0
  362. package/package.json +91 -0
@@ -0,0 +1,681 @@
1
+ /**
2
+ * Background watchdog for `active` and `review` tasks whose agent session is
3
+ * dead or stalled (#0180/#0286).
4
+ *
5
+ * ACTIVE tasks (#0180): a task sitting silently in `active` — no live agent
6
+ * process, no recent activity — is detected, the reason it stopped is recorded
7
+ * in the task transcript (agent crashed / exited without a handoff / never
8
+ * started), and it is surfaced by auto-transitioning to a visible state.
9
+ *
10
+ * A task is considered stuck for the ACTIVE path when:
11
+ * - status === 'active'
12
+ * - No agent process is running (`runner.isRunning(id)` === false) and no
13
+ * server-side handoff is finalizing
14
+ * - Not already flagged `needsInput` (the human is on it)
15
+ * - Not deliberately paused (0070) — `runner.isPaused(id)` is true
16
+ * - No activity since the most recent `status →active` transition (or no
17
+ * activity at all) for longer than the staleness threshold
18
+ *
19
+ * REVIEW tasks (#0286): a task sitting in `review` whose REVIEWER agent session
20
+ * died silently — no report, no completion log entry, no running process — is
21
+ * detected the same way. The recovery differs: a fresh reviewer run is kicked
22
+ * off (the same action as the human's "Review again"), leaving an Activity
23
+ * trail. If the auto-retry is exhausted and the task is still stuck (no report,
24
+ * no running review), it is escalated to `needsInput` for a human rather than
25
+ * looped.
26
+ *
27
+ * On detection of an ACTIVE task the watchdog:
28
+ * 1. Classifies the failure reason — `never-started` (no session ever began),
29
+ * `crashed` (the persisted handoff log shows an interrupted turn), or
30
+ * `exited-without-handoff` (a session ran, its process ended, but no clean
31
+ * handoff signal followed).
32
+ * 2. Records that reason in the task's Activity log.
33
+ * 3. Surfaces the task by auto-transitioning it out of the silent `active`
34
+ * state (`watchdog.autoTransition`, default on): to `review` when the
35
+ * worktree holds work (committed or uncommitted — reviewable, never to be
36
+ * silently discarded, the #0172 lesson), else back to `ready` where it can
37
+ * be picked up again. The write goes through the live index, so the board
38
+ * updates and the existing ntfy status-change notification fires. When
39
+ * auto-transition is disabled it falls back to `needsInput` (the #0156
40
+ * escalation), which fires the existing needs-input notification.
41
+ *
42
+ * Guards:
43
+ * - Never fires while the server is mid-reload (`canRun` is observed every
44
+ * scan — the reload manager reports `isReloading()` during handover).
45
+ * - Never marks a task whose agent is legitimately paused.
46
+ * - Bounded: a task is surfaced/retried at most once per stuck episode
47
+ * (persisted Activity marker, so the bound survives reloads; the in-memory
48
+ * set is only a fast path).
49
+ */
50
+ import { writeFileSync, readFileSync, readdirSync, statSync } from "node:fs";
51
+ import { execFileSync } from "node:child_process";
52
+ import { join } from "node:path";
53
+ import { parseTask, serializeTask, recordChange } from "../core/task.js";
54
+ import { commitTaskFile, worktreeStatus, worktreePathForBranch } from "../core/git.js";
55
+ import { scheduleHandoffSignalRetry } from "./handoff.js";
56
+ const DEFAULT_STALENESS_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
57
+ const WATCHDOG_INTERVAL_MS = 60 * 1000; // Check every 1 minute
58
+ /** Activity-log marker the watchdog writes/reads to bound surfacing once. */
59
+ const SURFACED_ACTIVITY = /watchdog: auto-surfaced stuck task/;
60
+ const ESCALATED_ACTIVITY = /watchdog: escalated to needs_input/;
61
+ /** Activity-log marker the watchdog writes/reads to bound reviewer retry once. */
62
+ const REVIEW_RETRIED_ACTIVITY = /watchdog: auto-retried dead reviewer session/;
63
+ const HANDOFF_FAILURE_ACTIVITY = /handoff failed · (.+)$/m;
64
+ const HANDOFF_RETAINED = /handoff retained for recovery/;
65
+ const HANDOFF_RECOVERY_ATTEMPTED = /handoff recovery attempted/;
66
+ /**
67
+ * Best-effort next-step suggestion for an escalation note, matched from the
68
+ * captured failure reason. A generic fallback beats a wrong diagnosis.
69
+ */
70
+ export function suggestNextStep(reason) {
71
+ const r = reason.toLowerCase();
72
+ if (/retained for recovery/.test(r)) {
73
+ return "the handoff request was retained for recovery — it will be finalized automatically on the next server start";
74
+ }
75
+ if (/recovery attempted.*finalization failed/.test(r)) {
76
+ return "handoff recovery was attempted but failed — open the task and resume the session manually to finish and hand off";
77
+ }
78
+ if (/permission|approval|allowed|confirm|prompt/.test(r)) {
79
+ return ("check the transcript for an unanswered permission/approval prompt — see " +
80
+ "docs/adr/0005-agents-use-repoos-apis-for-privileged-operations.md");
81
+ }
82
+ if (/signal|handoff|rendered|ansi|kiro/.test(r)) {
83
+ return ("the handoff signal may not have been emitted on its own line — the agent's " +
84
+ "final line must be exactly `::repoos-handoff-ready::` (see #0154/#0155 for " +
85
+ "signal-line rendering bugs)");
86
+ }
87
+ if (/interrupted|stopped|killed|crash|exit/.test(r)) {
88
+ return "the agent turn was interrupted — open the task and resume the session in its worktree to finish and hand off";
89
+ }
90
+ if (/timeout|stall|hung|hang|busy/.test(r)) {
91
+ return "the agent stalled or timed out — see DEFAULT_STALL_TIMEOUT_MS in src/server/agents.ts";
92
+ }
93
+ return "resume the session manually from the task's worktree and check for uncommitted work";
94
+ }
95
+ /**
96
+ * Pure stuck-detection over a task's Activity log. True when the log has a
97
+ * `status →active` transition and no Activity entry after it for at least
98
+ * `stalenessMs` (a task with zero entries after going active is the purest
99
+ * stuck shape — its clock runs from the transition itself).
100
+ */
101
+ export function isStuckActiveTask(body, stalenessMs, now) {
102
+ const transitionAt = lastStatusTransitionTime(body, "active");
103
+ if (transitionAt === null)
104
+ return false;
105
+ return now - lastActivityTime(body, transitionAt) >= stalenessMs;
106
+ }
107
+ /**
108
+ * Pure stuck-detection over a task's Activity log for the `review` status.
109
+ * Mirrors `isStuckActiveTask` but bounds the episode by the most recent
110
+ * `status →review` transition: a review task is candidate-stuck when no
111
+ * Activity entry after it (or none at all) exists for at least `stalenessMs`.
112
+ *
113
+ * This is deliberately only the Activity-log staleness half of the decision.
114
+ * The "the review actually completed" half (a report file exists) and the
115
+ * "a review is genuinely in flight" half live in `isStuck`, because they need
116
+ * the `ReviewManager` and filesystem, not just the body string.
117
+ */
118
+ export function isStuckReviewTask(body, stalenessMs, now) {
119
+ const transitionAt = lastStatusTransitionTime(body, "review");
120
+ if (transitionAt === null)
121
+ return false;
122
+ return now - lastActivityTime(body, transitionAt) >= stalenessMs;
123
+ }
124
+ /** Directories never worth scanning for agent activity: irrelevant or huge. */
125
+ const ACTIVITY_SCAN_SKIP = new Set([".git", "node_modules"]);
126
+ /** Bound on files inspected per scan — a stuck-check must never become slow. */
127
+ const ACTIVITY_SCAN_FILE_LIMIT = 2000;
128
+ /**
129
+ * True when any file in `dir` was modified within `withinMs` of `now`.
130
+ * Depth-first, bounded by `ACTIVITY_SCAN_FILE_LIMIT`, returns at the first
131
+ * qualifying file found rather than walking the whole tree.
132
+ */
133
+ function hasRecentMtime(dir, withinMs, now, budget) {
134
+ let entries;
135
+ try {
136
+ entries = readdirSync(dir, { withFileTypes: true });
137
+ }
138
+ catch {
139
+ return false;
140
+ }
141
+ for (const e of entries) {
142
+ if (budget.left <= 0)
143
+ return false;
144
+ if (ACTIVITY_SCAN_SKIP.has(e.name))
145
+ continue;
146
+ const full = join(dir, e.name);
147
+ if (e.isDirectory()) {
148
+ if (hasRecentMtime(full, withinMs, now, budget))
149
+ return true;
150
+ continue;
151
+ }
152
+ if (!e.isFile())
153
+ continue;
154
+ budget.left--;
155
+ try {
156
+ if (now - statSync(full).mtimeMs <= withinMs)
157
+ return true;
158
+ }
159
+ catch {
160
+ /* file vanished mid-scan — not evidence either way */
161
+ }
162
+ }
163
+ return false;
164
+ }
165
+ /**
166
+ * True when a task's worktree has a file modified within `stalenessMs` of
167
+ * `now` — evidence of a genuinely live agent independent of the in-memory
168
+ * runner registry.
169
+ *
170
+ * The registry (`AgentRunner.isRunning`) is a plain in-memory Map: it goes
171
+ * empty on every server restart, with no record of which PID belonged to
172
+ * which task (#0214). During this repo's own reload-churn-heavy sessions that
173
+ * makes `isRunning()` report false for a still-working agent constantly, not
174
+ * as a rare edge case — the exact false-positive #0203 was filed against
175
+ * ("the agent was never dead: it kept running... and kept writing source
176
+ * files"). The registry cannot be trusted alone; the worktree's own file
177
+ * mtimes are a signal that survives a restart because they live on disk, not
178
+ * in server memory.
179
+ */
180
+ export function hasRecentWorktreeActivity(root, branch, stalenessMs, now) {
181
+ if (!branch)
182
+ return false;
183
+ const path = worktreePathForBranch(root, branch);
184
+ if (!path)
185
+ return false;
186
+ return hasRecentMtime(path, stalenessMs, now, { left: ACTIVITY_SCAN_FILE_LIMIT });
187
+ }
188
+ /**
189
+ * Classify why an active task's agent session is dead. The `task` is the
190
+ * freshly-parsed on-disk Task (its Activity log may carry a persisted
191
+ * `handoff failed · …` entry); `runner.output(id)` reveals whether the task
192
+ * ever had a session (in-memory or persisted) at all.
193
+ */
194
+ export function classifyDeadAgentReason(task, runner) {
195
+ // A persisted handoff failure is the strongest signal: the turn reached the
196
+ // handoff boundary but its process ended uncleanly (or the task could not be
197
+ // resolved) — the "killed mid-turn" shape.
198
+ const failed = extractHandoffFailure(task.body);
199
+ if (failed) {
200
+ // Check if this is a check failure that has been exhausted
201
+ if (/check failed after \d+ automatic retries/.test(failed)) {
202
+ return {
203
+ kind: "crashed",
204
+ reason: `check-failed-after-retries · ${failed}`,
205
+ };
206
+ }
207
+ // Recovery was attempted but finalization failed — surface that outcome,
208
+ // not the original "retained for recovery" which is now stale (#0235).
209
+ if (HANDOFF_RECOVERY_ATTEMPTED.test(task.body)) {
210
+ return {
211
+ kind: "crashed",
212
+ reason: "handoff recovery was attempted after an interrupted turn but finalization failed — manual intervention needed",
213
+ };
214
+ }
215
+ // A "retained for recovery" failure means the request was persisted and
216
+ // will be re-fired on the next server boot — not truly crashed (#0235).
217
+ if (HANDOFF_RETAINED.test(failed)) {
218
+ return {
219
+ kind: "crashed",
220
+ reason: "agent turn was interrupted with a pending handoff — retained for recovery on next server start",
221
+ };
222
+ }
223
+ return {
224
+ kind: "crashed",
225
+ reason: `agent crashed or was interrupted mid-turn — ${failed}`,
226
+ };
227
+ }
228
+ // No session ever existed: the task went active but no agent process was
229
+ // ever launched (or its session was never persisted).
230
+ if (!runner.output(task.id)) {
231
+ return {
232
+ kind: "never-started",
233
+ reason: "agent never started — no session exists for this task",
234
+ };
235
+ }
236
+ // A session ran and ended, but the agent never emitted a clean handoff.
237
+ return {
238
+ kind: "exited-without-handoff",
239
+ reason: "agent exited without emitting the handoff signal",
240
+ };
241
+ }
242
+ /**
243
+ * Where a stuck task lands when surfaced: `review` whenever its worktree holds
244
+ * any work (committed branch commits OR uncommitted changes — either way it is
245
+ * reviewable, and moving it to `ready` could discard it on a clean restart),
246
+ * else `ready` to go back in the pile. Without a branch/worktree there is no
247
+ * work to look at, so `ready`.
248
+ */
249
+ export function autoTransitionTarget(config, task) {
250
+ if (!task.branch)
251
+ return "ready";
252
+ if (task.hotfix) {
253
+ try {
254
+ const out = execFileSync("git", ["status", "--porcelain"], {
255
+ cwd: config.root,
256
+ encoding: "utf8",
257
+ timeout: 4000,
258
+ });
259
+ return out.trim() ? "review" : "ready";
260
+ }
261
+ catch {
262
+ return "ready";
263
+ }
264
+ }
265
+ const status = worktreeStatus(config.root, task.branch);
266
+ return status.dirty ? "review" : "ready";
267
+ }
268
+ /** The most recent persisted handoff-failure reason in an Activity log, if any. */
269
+ function extractHandoffFailure(body) {
270
+ const match = body.match(HANDOFF_FAILURE_ACTIVITY);
271
+ return match?.[1]?.trim() || null;
272
+ }
273
+ /**
274
+ * Whether the task's Activity log already shows a watchdog surface/escalation
275
+ * SINCE the most recent `status →active` transition. Scoped the same way
276
+ * `isStuckActiveTask`/`lastActivityTime` already are — an unscoped whole-body
277
+ * match was a confirmed bug (#0271 follow-up, task #0243): a task surfaced
278
+ * once, then restarted to `active` again (by a human or a fresh run), can get
279
+ * stuck a second time — but the OLD marker from the first, already-resolved
280
+ * session never leaves the append-only Activity log, so an unscoped check
281
+ * permanently disables the watchdog for that task for the rest of its life.
282
+ * #0243 sat silently `active` for 4+ days this way. Scanning only entries
283
+ * after the current transition-into-active line fixes it: each active
284
+ * session gets its own fresh "not yet surfaced" state.
285
+ */
286
+ function alreadySurfaced(body) {
287
+ const lines = body.split("\n");
288
+ const activityIndex = lines.findIndex((line) => line.trim() === "## Activity");
289
+ if (activityIndex === -1)
290
+ return false;
291
+ for (let i = lines.length - 1; i > activityIndex; i--) {
292
+ const line = lines[i];
293
+ if (SURFACED_ACTIVITY.test(line) || ESCALATED_ACTIVITY.test(line))
294
+ return true;
295
+ // The most recent transition INTO active bounds this session — anything
296
+ // above it (further back) belongs to an earlier, already-resolved one.
297
+ if (/^- \d{4}-\d{2}-\d{2}T\S+ · status [a-z_]+→active\b/.test(line))
298
+ return false;
299
+ }
300
+ return false;
301
+ }
302
+ /**
303
+ * Whether the watchdog has already auto-retried a dead REVIEWER since the most
304
+ * recent `status →review` transition. Scoped exactly like `alreadySurfaced`:
305
+ * scanning only entries after the current transition-into-review keeps each
306
+ * review episode's retry budget (at most one) independent of earlier episodes
307
+ * the append-only Activity log never forgets.
308
+ */
309
+ function alreadyRetriedReview(body) {
310
+ const lines = body.split("\n");
311
+ const activityIndex = lines.findIndex((line) => line.trim() === "## Activity");
312
+ if (activityIndex === -1)
313
+ return false;
314
+ for (let i = lines.length - 1; i > activityIndex; i--) {
315
+ const line = lines[i];
316
+ if (REVIEW_RETRIED_ACTIVITY.test(line))
317
+ return true;
318
+ // The most recent transition INTO review bounds this episode.
319
+ if (/^- \d{4}-\d{2}-\d{2}T\S+ · status [a-z_]+→review\b/.test(line))
320
+ return false;
321
+ }
322
+ return false;
323
+ }
324
+ /**
325
+ * Whether a stored review report belongs to the CURRENT review episode — i.e.
326
+ * its `at` timestamp post-dates the most recent `status →review` transition.
327
+ *
328
+ * The report file is never cleared when a new review round starts (only the
329
+ * session conversation is reset, #0110), so an old report from a PRIOR round
330
+ * still reads back and would otherwise mask a current dead reviewer. Comparing
331
+ * against the episode's transition time makes "a report exists" mean "this
332
+ * round's review really completed" instead of "some report ever existed".
333
+ */
334
+ function isReportForCurrentEpisode(reportAt, body) {
335
+ const transitionAt = lastStatusTransitionTime(body, "review");
336
+ if (transitionAt === null)
337
+ return true; // no transition on record — assume current
338
+ const t = Date.parse(reportAt);
339
+ if (Number.isNaN(t))
340
+ return false;
341
+ return t >= transitionAt;
342
+ }
343
+ export class TaskWatchdog {
344
+ config;
345
+ index;
346
+ runner;
347
+ autoTransition;
348
+ canRun;
349
+ reviews;
350
+ timer = null;
351
+ stalenessThresholdMs;
352
+ /** Re-entry guard: a scan already in flight is skipped by the next tick. */
353
+ scanning = false;
354
+ constructor(config, index, runner, stalenessThresholdMs = DEFAULT_STALENESS_THRESHOLD_MS, opts = {}) {
355
+ this.config = config;
356
+ this.index = index;
357
+ this.runner = runner;
358
+ this.stalenessThresholdMs = stalenessThresholdMs;
359
+ this.autoTransition = opts.autoTransition !== false;
360
+ this.canRun = opts.canRun;
361
+ this.reviews = opts.reviews;
362
+ }
363
+ start() {
364
+ if (this.timer)
365
+ return;
366
+ this.timer = setInterval(() => {
367
+ void this.checkNow();
368
+ }, WATCHDOG_INTERVAL_MS);
369
+ }
370
+ stop() {
371
+ if (this.timer) {
372
+ clearInterval(this.timer);
373
+ this.timer = null;
374
+ }
375
+ }
376
+ /** Run one full scan now (used by the timer and by tests). */
377
+ async checkNow() {
378
+ if (this.canRun && !this.canRun())
379
+ return;
380
+ // Re-entry guard: the timer fires every 60s and a scan is async; if an
381
+ // earlier scan somehow overlaps (a slow disk, a blocked write), a second
382
+ // would scan the same tasks concurrently. Skip it — the next tick covers
383
+ // anything new. The scan body itself only does fast file/state work: any
384
+ // long-lived work (a review run, a handoff retry) is fired fire-and-forget,
385
+ // so a scan never blocks on it.
386
+ if (this.scanning)
387
+ return;
388
+ this.scanning = true;
389
+ try {
390
+ // The `active` status is scanned for the dead/stalled ENGINEER shape
391
+ // (#0180). The `review` status is scanned too (#0286): a task whose
392
+ // REVIEWER session died silently sits in `review` forever with no report
393
+ // and no one to nudge it — the same failure mode as a dead engineer, just
394
+ // on the review side where the watchdog never looked before.
395
+ for (const status of ["active", "review"]) {
396
+ for (const task of this.index.getTasks(status)) {
397
+ if (this.isStuck(task)) {
398
+ this.handleStuck(task);
399
+ }
400
+ }
401
+ }
402
+ }
403
+ finally {
404
+ this.scanning = false;
405
+ }
406
+ }
407
+ isStuck(task) {
408
+ // A task the human is already flagged on is not stuck-but-unnoticed: don't
409
+ // auto-surface or re-escalate it.
410
+ if (task.needsInput)
411
+ return false;
412
+ // An agent process is running, or a server-side handoff is finalizing.
413
+ if (this.runner.isRunning(task.id))
414
+ return false;
415
+ if (this.runner.isHandoffInFlight(task.id))
416
+ return false;
417
+ // A deliberately paused agent is legitimate — never disturb it (#0180).
418
+ if (this.runner.isPaused(task.id))
419
+ return false;
420
+ const now = Date.now();
421
+ if (task.status === "review") {
422
+ return this.isStuckReview(task, now);
423
+ }
424
+ // Bounded: never surface a task the watchdog already surfaced.
425
+ if (alreadySurfaced(task.body))
426
+ return false;
427
+ // A task whose handoff was retained for recovery (#0235) is not stuck —
428
+ // the recovery is in progress or will fire on the next boot. But only
429
+ // when no recovery attempt has already been recorded: if recoverPending-
430
+ // Handoffs fired and finalization failed, the Activity log will contain a
431
+ // "handoff recovery attempted" entry and the watchdog must be free to
432
+ // surface/escalate the task again.
433
+ if (HANDOFF_RETAINED.test(task.body) && !HANDOFF_RECOVERY_ATTEMPTED.test(task.body))
434
+ return false;
435
+ if (!isStuckActiveTask(task.body, this.stalenessThresholdMs, now))
436
+ return false;
437
+ // The Activity log looks stale, but `isRunning()` above only reflects the
438
+ // in-memory registry, which is empty after every server restart with no
439
+ // memory of which PID belonged to this task (#0214). Before surfacing,
440
+ // check the worktree's own file mtimes — durable, on-disk evidence a
441
+ // registry reset can't erase. Skips the scan entirely once the log
442
+ // already proves recent activity, so the common case pays nothing extra.
443
+ if (hasRecentWorktreeActivity(this.config.root, task.branch, this.stalenessThresholdMs, now)) {
444
+ return false;
445
+ }
446
+ return true;
447
+ }
448
+ /**
449
+ * The `review`-status half of stuck detection. A review task is stuck when
450
+ * its reviewer session ended without ever producing a report for THIS round
451
+ * and no review is in flight — the silent-death shape (#0286). A report that
452
+ * EXISTS for the current episode (written after the current `→review`
453
+ * transition) means the review already completed and surfaced itself, so it
454
+ * is not stuck even if it "failed"; only a round that produced nothing at all
455
+ * needs the watchdog.
456
+ */
457
+ isStuckReview(task, now) {
458
+ if (!this.reviews)
459
+ return false; // no review manager wired — nothing to do
460
+ if (this.reviews.isRunning(task.id))
461
+ return false;
462
+ // A report post-dating this episode's →review transition means this round's
463
+ // review completed. A report from a PRIOR round (older `at`) is stale — the
464
+ // report file is never cleared between rounds — and must not mask a current
465
+ // dead reviewer.
466
+ const report = this.reviews.read(task.id);
467
+ if (report && isReportForCurrentEpisode(report.at, task.body))
468
+ return false;
469
+ if (!isStuckReviewTask(task.body, this.stalenessThresholdMs, now))
470
+ return false;
471
+ // Durable on-disk worktree evidence a live agent (engineer) is still
472
+ // active; a registry reset can't erase mtimes (#0214/#0203). The
473
+ // completed-review `reviews.read` check above already ruled out the
474
+ // finished case, so this is only a false-suppression guard, never a mask.
475
+ if (hasRecentWorktreeActivity(this.config.root, task.branch, this.stalenessThresholdMs, now)) {
476
+ return false;
477
+ }
478
+ return true;
479
+ }
480
+ handleStuck(task) {
481
+ // Re-read fresh from disk (the index copy can lag the canonical board) and
482
+ // re-check the guards against the on-disk reality.
483
+ const current = this.readCurrent(task);
484
+ if (current.status === "review") {
485
+ this.handleStuckReview(current);
486
+ return;
487
+ }
488
+ if (current.status !== "active")
489
+ return;
490
+ if (alreadySurfaced(current.body))
491
+ return;
492
+ const { kind, reason } = classifyDeadAgentReason(current, this.runner);
493
+ // Check if this is a terminal check failure that should immediately set needs_input
494
+ if (reason.startsWith("check-failed-after-retries")) {
495
+ this.escalateToNeedsInput(current, reason);
496
+ return;
497
+ }
498
+ // #0271 follow-up: `exited-without-handoff` is the one dead-session shape
499
+ // with a plausible quick fix — the agent may have simply mis-emitted the
500
+ // signal line (#0154/#0155) or just needs a nudge to finish. Give the
501
+ // SAME session a bounded number of automatic nudges before falling back
502
+ // to surfacing (which is still the correct outcome for `never-started` —
503
+ // nothing to resume — and `crashed`, which already has its own recovery
504
+ // path via the persisted-handoff-failure machinery).
505
+ if (kind === "exited-without-handoff" &&
506
+ scheduleHandoffSignalRetry(this.config, current, this.runner, (absPath) => this.index.applyFileChange(absPath, { guarded: true }))) {
507
+ return;
508
+ }
509
+ if (this.autoTransition) {
510
+ this.surfaceTask(current, autoTransitionTarget(this.config, current), reason);
511
+ return;
512
+ }
513
+ // Fallback (#0156): keep the task active but flag it for the human.
514
+ this.escalateToNeedsInput(current, reason);
515
+ }
516
+ /**
517
+ * Recover a task whose REVIEWER session died silently mid-review (#0286).
518
+ *
519
+ * Unlike the `active` path (which resumes/restarts the ENGINEER), a dead
520
+ * reviewer is recovered by kicking off a FRESH reviewer run — the same
521
+ * action the human takes manually via "Review again". It never touches
522
+ * engineer state: the task stays in `review` for a human to sign off.
523
+ *
524
+ * Bounded the same way the `active` path is bounded: one automatic retry per
525
+ * review episode (persisted Activity marker, scoped to the current
526
+ * `→review` transition). If the retry is exhausted and the task is still
527
+ * stuck — no report, no running review — it is escalated to `needsInput`
528
+ * for a human instead of looping forever on a genuinely broken reviewer.
529
+ */
530
+ handleStuckReview(task) {
531
+ if (!this.reviews)
532
+ return;
533
+ const current = this.readCurrent(task);
534
+ if (current.status !== "review")
535
+ return;
536
+ if (current.needsInput)
537
+ return;
538
+ if (this.reviews.isRunning(current.id))
539
+ return;
540
+ // This episode's review completed — only a stale report from a prior round
541
+ // would read back here, and that must not block recovery of a dead reviewer
542
+ // in the current round (#0286 review round 2 fix).
543
+ const report = this.reviews.read(current.id);
544
+ if (report && isReportForCurrentEpisode(report.at, current.body))
545
+ return;
546
+ // One automatic retry is the budget; a second stuck episode is a broken
547
+ // reviewer config, not bad luck — hand it to a human, tracked in the log.
548
+ if (alreadyRetriedReview(current.body)) {
549
+ this.escalateToNeedsInput(current, "reviewer session died and the automatic retry did not recover it — the review agent may be misconfigured");
550
+ return;
551
+ }
552
+ // Record the retry (trail + bound) before kicking off a fresh run, so the
553
+ // attempt survives even a mid-run restart and the retry stays at most one.
554
+ const note = "watchdog: auto-retried dead reviewer session · the reviewer agent produced no report and its session ended — starting a fresh review";
555
+ try {
556
+ recordChange(current, note);
557
+ this.writeTask(current);
558
+ commitTaskFile(this.config.root, current.absPath, `docs(${current.id}): update task`);
559
+ }
560
+ catch (err) {
561
+ console.error(`[repoos] watchdog: failed to record review retry for #${task.id}: ${err.message}`);
562
+ return;
563
+ }
564
+ // Fire-and-forget, never awaited: a review can run for up to REVIEW_TIMEOUT_MS
565
+ // (15 min), and `checkNow` is a background scan that must stay fast so it can
566
+ // keep detecting OTHER stuck tasks on the same sweep. The retry marker above
567
+ // already bounds it, and `ReviewManager.isRunning` short-circuits re-entry for
568
+ // this same task, so there is nothing to await. Mirrors how the route layer
569
+ // starts a review (`void reviews.run(...)`).
570
+ void this.reviews.run(current);
571
+ }
572
+ /**
573
+ * Move a stuck task out of the silent `active` state: write the status +
574
+ * reason in one Activity entry, then commit so main stays mergeable and the
575
+ * indexing write surfaces the change to the board + ntfy notification.
576
+ */
577
+ surfaceTask(task, target, reason) {
578
+ const current = this.readCurrent(task);
579
+ if (current.status !== "active")
580
+ return;
581
+ if (alreadySurfaced(current.body))
582
+ return;
583
+ try {
584
+ const note = `watchdog: auto-surfaced stuck task · status active→${target} · ${reason} · next step: ${suggestNextStep(reason)}`;
585
+ current.status = target;
586
+ current.needsInput = false;
587
+ current.needsInputReason = undefined;
588
+ recordChange(current, note);
589
+ this.writeTask(current);
590
+ commitTaskFile(this.config.root, current.absPath, `docs(${current.id}): update task`);
591
+ }
592
+ catch (err) {
593
+ console.error(`[repoos] watchdog: failed to surface #${task.id}: ${err.message}`);
594
+ }
595
+ }
596
+ /**
597
+ * Escalate a stuck task (#0156 fallback): set `needsInput: true` (which
598
+ * surfaces on the board and fires the existing `notifyNeedsInput`
599
+ * notification) and append an Activity entry with the failure reason and a
600
+ * suggested next step. The task stays `active` — visible, not silent.
601
+ */
602
+ escalateToNeedsInput(task, failureReason) {
603
+ let current;
604
+ try {
605
+ current = this.readCurrent(task);
606
+ }
607
+ catch (err) {
608
+ console.error(`[repoos] watchdog: failed to escalate #${task.id}: ${err.message}`);
609
+ return;
610
+ }
611
+ if (current.needsInput || alreadySurfaced(current.body))
612
+ return;
613
+ const reason = failureReason ?? extractHandoffFailure(current.body) ?? "handoff signal was not detected";
614
+ const note = `watchdog: escalated to needs_input · ${reason} · next step: ${suggestNextStep(reason)}`;
615
+ try {
616
+ current.needsInput = true;
617
+ current.needsInputReason = "watchdog-stuck";
618
+ recordChange(current, note);
619
+ this.writeTask(current);
620
+ }
621
+ catch (err) {
622
+ console.error(`[repoos] watchdog: failed to write escalation for #${task.id}: ${err.message}`);
623
+ }
624
+ }
625
+ readCurrent(task) {
626
+ return parseTask({
627
+ content: readFileSync(task.absPath, "utf8"),
628
+ absPath: task.absPath,
629
+ root: this.config.root,
630
+ defaultStatus: this.config.defaultStatus,
631
+ defaultAssignee: this.config.defaultAssignee,
632
+ });
633
+ }
634
+ writeTask(task) {
635
+ writeFileSync(task.absPath, serializeTask(task));
636
+ // Surface the change to the live index so the board updates and the
637
+ // existing status-change/needs-input notification subscribers fire.
638
+ this.index.applyFileChange(task.absPath);
639
+ }
640
+ }
641
+ function lastStatusTransitionTime(body, status) {
642
+ const lines = body.split("\n");
643
+ const activityIndex = lines.findIndex((line) => line.trim() === "## Activity");
644
+ if (activityIndex === -1)
645
+ return null;
646
+ for (let i = lines.length - 1; i > activityIndex; i--) {
647
+ const match = lines[i].match(/^- (\d{4}-\d{2}-\d{2}T[^\s]+) · status [a-z_]+→(\w+)/);
648
+ if (match && match[2] === status) {
649
+ const t = Date.parse(match[1]);
650
+ return Number.isNaN(t) ? null : t;
651
+ }
652
+ }
653
+ return null;
654
+ }
655
+ /**
656
+ * The timestamp of the most recent Activity entry after the `status →active`
657
+ * transition. Falls back to the transition time itself when there is none.
658
+ */
659
+ function lastActivityTime(body, afterTime) {
660
+ const lines = body.split("\n");
661
+ const activityIndex = lines.findIndex((line) => line.trim() === "## Activity");
662
+ if (activityIndex === -1)
663
+ return afterTime;
664
+ for (let i = lines.length - 1; i > activityIndex; i--) {
665
+ const line = lines[i].trim();
666
+ if (!line.startsWith("-"))
667
+ continue;
668
+ const match = line.match(/^- (\d{4}-\d{2}-\d{2}T[^\s]+) · /);
669
+ if (!match)
670
+ continue;
671
+ const t = Date.parse(match[1]);
672
+ if (Number.isNaN(t))
673
+ continue;
674
+ if (t > afterTime)
675
+ return t;
676
+ // Entries are append-only, so the first one at-or-before the transition
677
+ // means nothing newer exists.
678
+ return afterTime;
679
+ }
680
+ return afterTime;
681
+ }