@kynetic-ai/spec 0.1.1 → 0.3.0

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 (510) hide show
  1. package/README.md +250 -17
  2. package/dist/acp/client.d.ts +18 -4
  3. package/dist/acp/client.d.ts.map +1 -1
  4. package/dist/acp/client.js +44 -26
  5. package/dist/acp/client.js.map +1 -1
  6. package/dist/acp/framing.d.ts +2 -2
  7. package/dist/acp/framing.d.ts.map +1 -1
  8. package/dist/acp/framing.js +37 -29
  9. package/dist/acp/framing.js.map +1 -1
  10. package/dist/acp/index.d.ts +6 -7
  11. package/dist/acp/index.d.ts.map +1 -1
  12. package/dist/acp/index.js +3 -3
  13. package/dist/acp/index.js.map +1 -1
  14. package/dist/acp/types.d.ts +5 -5
  15. package/dist/acp/types.d.ts.map +1 -1
  16. package/dist/acp/types.js +18 -18
  17. package/dist/acp/types.js.map +1 -1
  18. package/dist/agents/adapters.d.ts.map +1 -1
  19. package/dist/agents/adapters.js +24 -13
  20. package/dist/agents/adapters.js.map +1 -1
  21. package/dist/agents/index.d.ts +2 -2
  22. package/dist/agents/index.js +2 -2
  23. package/dist/agents/spawner.d.ts +4 -4
  24. package/dist/agents/spawner.d.ts.map +1 -1
  25. package/dist/agents/spawner.js +6 -6
  26. package/dist/agents/spawner.js.map +1 -1
  27. package/dist/cli/batch-context.d.ts +43 -0
  28. package/dist/cli/batch-context.d.ts.map +1 -0
  29. package/dist/cli/batch-context.js +93 -0
  30. package/dist/cli/batch-context.js.map +1 -0
  31. package/dist/cli/batch-exec.d.ts +116 -0
  32. package/dist/cli/batch-exec.d.ts.map +1 -0
  33. package/dist/cli/batch-exec.js +694 -0
  34. package/dist/cli/batch-exec.js.map +1 -0
  35. package/dist/cli/batch.d.ts +4 -2
  36. package/dist/cli/batch.d.ts.map +1 -1
  37. package/dist/cli/batch.js +15 -14
  38. package/dist/cli/batch.js.map +1 -1
  39. package/dist/cli/command-annotations.d.ts +23 -0
  40. package/dist/cli/command-annotations.d.ts.map +1 -0
  41. package/dist/cli/command-annotations.js +27 -0
  42. package/dist/cli/command-annotations.js.map +1 -0
  43. package/dist/cli/commands/agents.d.ts +46 -0
  44. package/dist/cli/commands/agents.d.ts.map +1 -0
  45. package/dist/cli/commands/agents.js +377 -0
  46. package/dist/cli/commands/agents.js.map +1 -0
  47. package/dist/cli/commands/batch.d.ts +20 -0
  48. package/dist/cli/commands/batch.d.ts.map +1 -0
  49. package/dist/cli/commands/batch.js +214 -0
  50. package/dist/cli/commands/batch.js.map +1 -0
  51. package/dist/cli/commands/clone-for-testing.d.ts +1 -1
  52. package/dist/cli/commands/clone-for-testing.d.ts.map +1 -1
  53. package/dist/cli/commands/clone-for-testing.js +37 -47
  54. package/dist/cli/commands/clone-for-testing.js.map +1 -1
  55. package/dist/cli/commands/derive.d.ts +1 -1
  56. package/dist/cli/commands/derive.d.ts.map +1 -1
  57. package/dist/cli/commands/derive.js +140 -88
  58. package/dist/cli/commands/derive.js.map +1 -1
  59. package/dist/cli/commands/doctor.d.ts +11 -0
  60. package/dist/cli/commands/doctor.d.ts.map +1 -0
  61. package/dist/cli/commands/doctor.js +152 -0
  62. package/dist/cli/commands/doctor.js.map +1 -0
  63. package/dist/cli/commands/export.d.ts +12 -0
  64. package/dist/cli/commands/export.d.ts.map +1 -0
  65. package/dist/cli/commands/export.js +134 -0
  66. package/dist/cli/commands/export.js.map +1 -0
  67. package/dist/cli/commands/help.d.ts +1 -1
  68. package/dist/cli/commands/help.d.ts.map +1 -1
  69. package/dist/cli/commands/help.js +163 -37
  70. package/dist/cli/commands/help.js.map +1 -1
  71. package/dist/cli/commands/inbox.d.ts +1 -1
  72. package/dist/cli/commands/inbox.d.ts.map +1 -1
  73. package/dist/cli/commands/inbox.js +178 -56
  74. package/dist/cli/commands/inbox.js.map +1 -1
  75. package/dist/cli/commands/index.d.ts +31 -19
  76. package/dist/cli/commands/index.d.ts.map +1 -1
  77. package/dist/cli/commands/index.js +31 -19
  78. package/dist/cli/commands/index.js.map +1 -1
  79. package/dist/cli/commands/init.d.ts +5 -1
  80. package/dist/cli/commands/init.d.ts.map +1 -1
  81. package/dist/cli/commands/init.js +108 -57
  82. package/dist/cli/commands/init.js.map +1 -1
  83. package/dist/cli/commands/item.d.ts +1 -1
  84. package/dist/cli/commands/item.d.ts.map +1 -1
  85. package/dist/cli/commands/item.js +557 -274
  86. package/dist/cli/commands/item.js.map +1 -1
  87. package/dist/cli/commands/link.d.ts +1 -1
  88. package/dist/cli/commands/link.d.ts.map +1 -1
  89. package/dist/cli/commands/link.js +55 -46
  90. package/dist/cli/commands/link.js.map +1 -1
  91. package/dist/cli/commands/log.d.ts +1 -1
  92. package/dist/cli/commands/log.d.ts.map +1 -1
  93. package/dist/cli/commands/log.js +57 -51
  94. package/dist/cli/commands/log.js.map +1 -1
  95. package/dist/cli/commands/merge-driver.d.ts +19 -0
  96. package/dist/cli/commands/merge-driver.d.ts.map +1 -0
  97. package/dist/cli/commands/merge-driver.js +398 -0
  98. package/dist/cli/commands/merge-driver.js.map +1 -0
  99. package/dist/cli/commands/meta.d.ts +1 -1
  100. package/dist/cli/commands/meta.d.ts.map +1 -1
  101. package/dist/cli/commands/meta.js +533 -399
  102. package/dist/cli/commands/meta.js.map +1 -1
  103. package/dist/cli/commands/module.d.ts +1 -1
  104. package/dist/cli/commands/module.d.ts.map +1 -1
  105. package/dist/cli/commands/module.js +30 -25
  106. package/dist/cli/commands/module.js.map +1 -1
  107. package/dist/cli/commands/plan-import.d.ts +11 -0
  108. package/dist/cli/commands/plan-import.d.ts.map +1 -0
  109. package/dist/cli/commands/plan-import.js +516 -0
  110. package/dist/cli/commands/plan-import.js.map +1 -0
  111. package/dist/cli/commands/plan.d.ts +10 -0
  112. package/dist/cli/commands/plan.d.ts.map +1 -0
  113. package/dist/cli/commands/plan.js +421 -0
  114. package/dist/cli/commands/plan.js.map +1 -0
  115. package/dist/cli/commands/ralph.d.ts +1 -1
  116. package/dist/cli/commands/ralph.d.ts.map +1 -1
  117. package/dist/cli/commands/ralph.js +1100 -168
  118. package/dist/cli/commands/ralph.js.map +1 -1
  119. package/dist/cli/commands/refs.d.ts +13 -0
  120. package/dist/cli/commands/refs.d.ts.map +1 -0
  121. package/dist/cli/commands/refs.js +283 -0
  122. package/dist/cli/commands/refs.js.map +1 -0
  123. package/dist/cli/commands/search.d.ts +1 -1
  124. package/dist/cli/commands/search.d.ts.map +1 -1
  125. package/dist/cli/commands/search.js +199 -37
  126. package/dist/cli/commands/search.js.map +1 -1
  127. package/dist/cli/commands/serve.d.ts +10 -0
  128. package/dist/cli/commands/serve.d.ts.map +1 -0
  129. package/dist/cli/commands/serve.js +491 -0
  130. package/dist/cli/commands/serve.js.map +1 -0
  131. package/dist/cli/commands/session.d.ts +25 -6
  132. package/dist/cli/commands/session.d.ts.map +1 -1
  133. package/dist/cli/commands/session.js +811 -127
  134. package/dist/cli/commands/session.js.map +1 -1
  135. package/dist/cli/commands/setup-seeding.d.ts +81 -0
  136. package/dist/cli/commands/setup-seeding.d.ts.map +1 -0
  137. package/dist/cli/commands/setup-seeding.js +292 -0
  138. package/dist/cli/commands/setup-seeding.js.map +1 -0
  139. package/dist/cli/commands/setup.d.ts +77 -3
  140. package/dist/cli/commands/setup.d.ts.map +1 -1
  141. package/dist/cli/commands/setup.js +1233 -274
  142. package/dist/cli/commands/setup.js.map +1 -1
  143. package/dist/cli/commands/shadow.d.ts +1 -1
  144. package/dist/cli/commands/shadow.d.ts.map +1 -1
  145. package/dist/cli/commands/shadow.js +70 -50
  146. package/dist/cli/commands/shadow.js.map +1 -1
  147. package/dist/cli/commands/skill-crud.d.ts +58 -0
  148. package/dist/cli/commands/skill-crud.d.ts.map +1 -0
  149. package/dist/cli/commands/skill-crud.js +753 -0
  150. package/dist/cli/commands/skill-crud.js.map +1 -0
  151. package/dist/cli/commands/skill-diff.d.ts +27 -0
  152. package/dist/cli/commands/skill-diff.d.ts.map +1 -0
  153. package/dist/cli/commands/skill-diff.js +840 -0
  154. package/dist/cli/commands/skill-diff.js.map +1 -0
  155. package/dist/cli/commands/skill-install.d.ts +53 -0
  156. package/dist/cli/commands/skill-install.d.ts.map +1 -0
  157. package/dist/cli/commands/skill-install.js +452 -0
  158. package/dist/cli/commands/skill-install.js.map +1 -0
  159. package/dist/cli/commands/skill.d.ts +20 -0
  160. package/dist/cli/commands/skill.d.ts.map +1 -0
  161. package/dist/cli/commands/skill.js +36 -0
  162. package/dist/cli/commands/skill.js.map +1 -0
  163. package/dist/cli/commands/task.d.ts +1 -1
  164. package/dist/cli/commands/task.d.ts.map +1 -1
  165. package/dist/cli/commands/task.js +569 -346
  166. package/dist/cli/commands/task.js.map +1 -1
  167. package/dist/cli/commands/tasks.d.ts +26 -1
  168. package/dist/cli/commands/tasks.d.ts.map +1 -1
  169. package/dist/cli/commands/tasks.js +227 -122
  170. package/dist/cli/commands/tasks.js.map +1 -1
  171. package/dist/cli/commands/trait.d.ts +1 -1
  172. package/dist/cli/commands/trait.d.ts.map +1 -1
  173. package/dist/cli/commands/trait.js +166 -101
  174. package/dist/cli/commands/trait.js.map +1 -1
  175. package/dist/cli/commands/triage.d.ts +7 -0
  176. package/dist/cli/commands/triage.d.ts.map +1 -0
  177. package/dist/cli/commands/triage.js +569 -0
  178. package/dist/cli/commands/triage.js.map +1 -0
  179. package/dist/cli/commands/util.d.ts +7 -0
  180. package/dist/cli/commands/util.d.ts.map +1 -0
  181. package/dist/cli/commands/util.js +30 -0
  182. package/dist/cli/commands/util.js.map +1 -0
  183. package/dist/cli/commands/validate.d.ts +1 -1
  184. package/dist/cli/commands/validate.d.ts.map +1 -1
  185. package/dist/cli/commands/validate.js +264 -83
  186. package/dist/cli/commands/validate.js.map +1 -1
  187. package/dist/cli/commands/workflow.d.ts +16 -0
  188. package/dist/cli/commands/workflow.d.ts.map +1 -0
  189. package/dist/cli/commands/workflow.js +851 -0
  190. package/dist/cli/commands/workflow.js.map +1 -0
  191. package/dist/cli/exit-codes.d.ts +7 -0
  192. package/dist/cli/exit-codes.d.ts.map +1 -1
  193. package/dist/cli/exit-codes.js +26 -18
  194. package/dist/cli/exit-codes.js.map +1 -1
  195. package/dist/cli/help/content.d.ts.map +1 -1
  196. package/dist/cli/help/content.js +86 -71
  197. package/dist/cli/help/content.js.map +1 -1
  198. package/dist/cli/index.d.ts +1 -1
  199. package/dist/cli/index.d.ts.map +1 -1
  200. package/dist/cli/index.js +135 -18
  201. package/dist/cli/index.js.map +1 -1
  202. package/dist/cli/introspection.d.ts +6 -2
  203. package/dist/cli/introspection.d.ts.map +1 -1
  204. package/dist/cli/introspection.js +11 -8
  205. package/dist/cli/introspection.js.map +1 -1
  206. package/dist/cli/output.d.ts +64 -4
  207. package/dist/cli/output.d.ts.map +1 -1
  208. package/dist/cli/output.js +235 -85
  209. package/dist/cli/output.js.map +1 -1
  210. package/dist/cli/parse-utils.d.ts +21 -0
  211. package/dist/cli/parse-utils.d.ts.map +1 -0
  212. package/dist/cli/parse-utils.js +32 -0
  213. package/dist/cli/parse-utils.js.map +1 -0
  214. package/dist/cli/pid-utils.d.ts +72 -0
  215. package/dist/cli/pid-utils.d.ts.map +1 -0
  216. package/dist/cli/pid-utils.js +174 -0
  217. package/dist/cli/pid-utils.js.map +1 -0
  218. package/dist/cli/suggest.d.ts.map +1 -1
  219. package/dist/cli/suggest.js +1 -2
  220. package/dist/cli/suggest.js.map +1 -1
  221. package/dist/cli/validators.d.ts +43 -0
  222. package/dist/cli/validators.d.ts.map +1 -0
  223. package/dist/cli/validators.js +84 -0
  224. package/dist/cli/validators.js.map +1 -0
  225. package/dist/daemon/index.ts +52 -0
  226. package/dist/daemon/middleware/project-context.ts +126 -0
  227. package/dist/daemon/pid.ts +179 -0
  228. package/dist/daemon/project-context.ts +343 -0
  229. package/dist/daemon/routes/inbox.ts +164 -0
  230. package/dist/daemon/routes/items.ts +322 -0
  231. package/dist/daemon/routes/meta.ts +118 -0
  232. package/dist/daemon/routes/projects.ts +162 -0
  233. package/dist/daemon/routes/tasks.ts +327 -0
  234. package/dist/daemon/routes/triage.ts +468 -0
  235. package/dist/daemon/routes/validation.ts +248 -0
  236. package/dist/daemon/server.ts +408 -0
  237. package/dist/daemon/watcher.ts +195 -0
  238. package/dist/daemon/websocket/handler.ts +138 -0
  239. package/dist/daemon/websocket/heartbeat.ts +71 -0
  240. package/dist/daemon/websocket/pubsub.ts +125 -0
  241. package/dist/daemon/websocket/types.ts +66 -0
  242. package/dist/export/html.d.ts +19 -0
  243. package/dist/export/html.d.ts.map +1 -0
  244. package/dist/export/html.js +239 -0
  245. package/dist/export/html.js.map +1 -0
  246. package/dist/export/index.d.ts +10 -0
  247. package/dist/export/index.d.ts.map +1 -0
  248. package/dist/export/index.js +10 -0
  249. package/dist/export/index.js.map +1 -0
  250. package/dist/export/json.d.ts +24 -0
  251. package/dist/export/json.d.ts.map +1 -0
  252. package/dist/export/json.js +198 -0
  253. package/dist/export/json.js.map +1 -0
  254. package/dist/export/triage.d.ts +51 -0
  255. package/dist/export/triage.d.ts.map +1 -0
  256. package/dist/export/triage.js +83 -0
  257. package/dist/export/triage.js.map +1 -0
  258. package/dist/export/types.d.ts +122 -0
  259. package/dist/export/types.d.ts.map +1 -0
  260. package/dist/export/types.js +9 -0
  261. package/dist/export/types.js.map +1 -0
  262. package/dist/index.d.ts +2 -2
  263. package/dist/index.js +2 -2
  264. package/dist/lib/claude-plugin-registry.d.ts +66 -0
  265. package/dist/lib/claude-plugin-registry.d.ts.map +1 -0
  266. package/dist/lib/claude-plugin-registry.js +318 -0
  267. package/dist/lib/claude-plugin-registry.js.map +1 -0
  268. package/dist/merge/arrays.d.ts +87 -0
  269. package/dist/merge/arrays.d.ts.map +1 -0
  270. package/dist/merge/arrays.js +164 -0
  271. package/dist/merge/arrays.js.map +1 -0
  272. package/dist/merge/file-type.d.ts +32 -0
  273. package/dist/merge/file-type.d.ts.map +1 -0
  274. package/dist/merge/file-type.js +70 -0
  275. package/dist/merge/file-type.js.map +1 -0
  276. package/dist/merge/index.d.ts +14 -0
  277. package/dist/merge/index.d.ts.map +1 -0
  278. package/dist/merge/index.js +11 -0
  279. package/dist/merge/index.js.map +1 -0
  280. package/dist/merge/objects.d.ts +46 -0
  281. package/dist/merge/objects.d.ts.map +1 -0
  282. package/dist/merge/objects.js +193 -0
  283. package/dist/merge/objects.js.map +1 -0
  284. package/dist/merge/parse.d.ts +23 -0
  285. package/dist/merge/parse.d.ts.map +1 -0
  286. package/dist/merge/parse.js +78 -0
  287. package/dist/merge/parse.js.map +1 -0
  288. package/dist/merge/resolve.d.ts +66 -0
  289. package/dist/merge/resolve.d.ts.map +1 -0
  290. package/dist/merge/resolve.js +189 -0
  291. package/dist/merge/resolve.js.map +1 -0
  292. package/dist/merge/types.d.ts +82 -0
  293. package/dist/merge/types.d.ts.map +1 -0
  294. package/dist/merge/types.js +8 -0
  295. package/dist/merge/types.js.map +1 -0
  296. package/dist/parser/agent-data-sections.d.ts +53 -0
  297. package/dist/parser/agent-data-sections.d.ts.map +1 -0
  298. package/dist/parser/agent-data-sections.js +118 -0
  299. package/dist/parser/agent-data-sections.js.map +1 -0
  300. package/dist/parser/alignment.d.ts +4 -4
  301. package/dist/parser/alignment.d.ts.map +1 -1
  302. package/dist/parser/alignment.js +27 -22
  303. package/dist/parser/alignment.js.map +1 -1
  304. package/dist/parser/assess.d.ts +5 -5
  305. package/dist/parser/assess.d.ts.map +1 -1
  306. package/dist/parser/assess.js +36 -32
  307. package/dist/parser/assess.js.map +1 -1
  308. package/dist/parser/config.d.ts +351 -0
  309. package/dist/parser/config.d.ts.map +1 -0
  310. package/dist/parser/config.js +326 -0
  311. package/dist/parser/config.js.map +1 -0
  312. package/dist/parser/convention-validation.d.ts +1 -1
  313. package/dist/parser/convention-validation.d.ts.map +1 -1
  314. package/dist/parser/convention-validation.js +21 -16
  315. package/dist/parser/convention-validation.js.map +1 -1
  316. package/dist/parser/coverage-cache.d.ts +49 -0
  317. package/dist/parser/coverage-cache.d.ts.map +1 -0
  318. package/dist/parser/coverage-cache.js +123 -0
  319. package/dist/parser/coverage-cache.js.map +1 -0
  320. package/dist/parser/daemon-status.d.ts +37 -0
  321. package/dist/parser/daemon-status.d.ts.map +1 -0
  322. package/dist/parser/daemon-status.js +67 -0
  323. package/dist/parser/daemon-status.js.map +1 -0
  324. package/dist/parser/doctor.d.ts +107 -0
  325. package/dist/parser/doctor.d.ts.map +1 -0
  326. package/dist/parser/doctor.js +366 -0
  327. package/dist/parser/doctor.js.map +1 -0
  328. package/dist/parser/fix.d.ts +1 -1
  329. package/dist/parser/fix.d.ts.map +1 -1
  330. package/dist/parser/fix.js +31 -27
  331. package/dist/parser/fix.js.map +1 -1
  332. package/dist/parser/index.d.ts +16 -11
  333. package/dist/parser/index.d.ts.map +1 -1
  334. package/dist/parser/index.js +16 -11
  335. package/dist/parser/index.js.map +1 -1
  336. package/dist/parser/items.d.ts +8 -2
  337. package/dist/parser/items.d.ts.map +1 -1
  338. package/dist/parser/items.js +71 -35
  339. package/dist/parser/items.js.map +1 -1
  340. package/dist/parser/meta.d.ts +167 -9
  341. package/dist/parser/meta.d.ts.map +1 -1
  342. package/dist/parser/meta.js +379 -46
  343. package/dist/parser/meta.js.map +1 -1
  344. package/dist/parser/plan-document.d.ts +189 -0
  345. package/dist/parser/plan-document.d.ts.map +1 -0
  346. package/dist/parser/plan-document.js +340 -0
  347. package/dist/parser/plan-document.js.map +1 -0
  348. package/dist/parser/plans.d.ts +59 -0
  349. package/dist/parser/plans.d.ts.map +1 -0
  350. package/dist/parser/plans.js +239 -0
  351. package/dist/parser/plans.js.map +1 -0
  352. package/dist/parser/refs.d.ts +22 -9
  353. package/dist/parser/refs.d.ts.map +1 -1
  354. package/dist/parser/refs.js +102 -50
  355. package/dist/parser/refs.js.map +1 -1
  356. package/dist/parser/setup-status.d.ts +71 -0
  357. package/dist/parser/setup-status.d.ts.map +1 -0
  358. package/dist/parser/setup-status.js +269 -0
  359. package/dist/parser/setup-status.js.map +1 -0
  360. package/dist/parser/shadow.d.ts +150 -19
  361. package/dist/parser/shadow.d.ts.map +1 -1
  362. package/dist/parser/shadow.js +548 -187
  363. package/dist/parser/shadow.js.map +1 -1
  364. package/dist/parser/skill-render.d.ts +317 -0
  365. package/dist/parser/skill-render.d.ts.map +1 -0
  366. package/dist/parser/skill-render.js +943 -0
  367. package/dist/parser/skill-render.js.map +1 -0
  368. package/dist/parser/traits.d.ts +3 -3
  369. package/dist/parser/traits.d.ts.map +1 -1
  370. package/dist/parser/traits.js +2 -2
  371. package/dist/parser/traits.js.map +1 -1
  372. package/dist/parser/validate-skills.d.ts +32 -0
  373. package/dist/parser/validate-skills.d.ts.map +1 -0
  374. package/dist/parser/validate-skills.js +202 -0
  375. package/dist/parser/validate-skills.js.map +1 -0
  376. package/dist/parser/validate.d.ts +45 -3
  377. package/dist/parser/validate.d.ts.map +1 -1
  378. package/dist/parser/validate.js +622 -105
  379. package/dist/parser/validate.js.map +1 -1
  380. package/dist/parser/yaml.d.ts +83 -19
  381. package/dist/parser/yaml.d.ts.map +1 -1
  382. package/dist/parser/yaml.js +478 -173
  383. package/dist/parser/yaml.js.map +1 -1
  384. package/dist/ralph/cli-renderer.d.ts +8 -1
  385. package/dist/ralph/cli-renderer.d.ts.map +1 -1
  386. package/dist/ralph/cli-renderer.js +105 -34
  387. package/dist/ralph/cli-renderer.js.map +1 -1
  388. package/dist/ralph/events.d.ts +10 -10
  389. package/dist/ralph/events.d.ts.map +1 -1
  390. package/dist/ralph/events.js +277 -98
  391. package/dist/ralph/events.js.map +1 -1
  392. package/dist/ralph/index.d.ts +5 -2
  393. package/dist/ralph/index.d.ts.map +1 -1
  394. package/dist/ralph/index.js +9 -3
  395. package/dist/ralph/index.js.map +1 -1
  396. package/dist/ralph/loop-errors.d.ts +83 -0
  397. package/dist/ralph/loop-errors.d.ts.map +1 -0
  398. package/dist/ralph/loop-errors.js +150 -0
  399. package/dist/ralph/loop-errors.js.map +1 -0
  400. package/dist/ralph/subagent.d.ts +83 -0
  401. package/dist/ralph/subagent.d.ts.map +1 -0
  402. package/dist/ralph/subagent.js +174 -0
  403. package/dist/ralph/subagent.js.map +1 -0
  404. package/dist/ralph/wrap-up.d.ts +125 -0
  405. package/dist/ralph/wrap-up.d.ts.map +1 -0
  406. package/dist/ralph/wrap-up.js +270 -0
  407. package/dist/ralph/wrap-up.js.map +1 -0
  408. package/dist/schema/batch.d.ts +95 -0
  409. package/dist/schema/batch.d.ts.map +1 -0
  410. package/dist/schema/batch.js +24 -0
  411. package/dist/schema/batch.js.map +1 -0
  412. package/dist/schema/common.d.ts +2 -2
  413. package/dist/schema/common.d.ts.map +1 -1
  414. package/dist/schema/common.js +34 -31
  415. package/dist/schema/common.js.map +1 -1
  416. package/dist/schema/inbox.d.ts +12 -12
  417. package/dist/schema/inbox.js +4 -4
  418. package/dist/schema/inbox.js.map +1 -1
  419. package/dist/schema/index.d.ts +8 -5
  420. package/dist/schema/index.d.ts.map +1 -1
  421. package/dist/schema/index.js +8 -5
  422. package/dist/schema/index.js.map +1 -1
  423. package/dist/schema/meta.d.ts +1454 -27
  424. package/dist/schema/meta.d.ts.map +1 -1
  425. package/dist/schema/meta.js +198 -21
  426. package/dist/schema/meta.js.map +1 -1
  427. package/dist/schema/plan.d.ts +285 -0
  428. package/dist/schema/plan.d.ts.map +1 -0
  429. package/dist/schema/plan.js +81 -0
  430. package/dist/schema/plan.js.map +1 -0
  431. package/dist/schema/spec.d.ts +72 -33
  432. package/dist/schema/spec.d.ts.map +1 -1
  433. package/dist/schema/spec.js +22 -9
  434. package/dist/schema/spec.js.map +1 -1
  435. package/dist/schema/task.d.ts +172 -161
  436. package/dist/schema/task.d.ts.map +1 -1
  437. package/dist/schema/task.js +21 -12
  438. package/dist/schema/task.js.map +1 -1
  439. package/dist/schema/triage.d.ts +266 -0
  440. package/dist/schema/triage.d.ts.map +1 -0
  441. package/dist/schema/triage.js +134 -0
  442. package/dist/schema/triage.js.map +1 -0
  443. package/dist/sessions/index.d.ts +2 -2
  444. package/dist/sessions/index.d.ts.map +1 -1
  445. package/dist/sessions/index.js +3 -3
  446. package/dist/sessions/index.js.map +1 -1
  447. package/dist/sessions/store.d.ts +233 -1
  448. package/dist/sessions/store.d.ts.map +1 -1
  449. package/dist/sessions/store.js +628 -31
  450. package/dist/sessions/store.js.map +1 -1
  451. package/dist/sessions/types.d.ts +10 -10
  452. package/dist/sessions/types.d.ts.map +1 -1
  453. package/dist/sessions/types.js +10 -9
  454. package/dist/sessions/types.js.map +1 -1
  455. package/dist/strings/errors.d.ts +51 -0
  456. package/dist/strings/errors.d.ts.map +1 -1
  457. package/dist/strings/errors.js +136 -106
  458. package/dist/strings/errors.js.map +1 -1
  459. package/dist/strings/guidance.d.ts.map +1 -1
  460. package/dist/strings/guidance.js +16 -16
  461. package/dist/strings/guidance.js.map +1 -1
  462. package/dist/strings/index.d.ts +4 -4
  463. package/dist/strings/index.d.ts.map +1 -1
  464. package/dist/strings/index.js +4 -4
  465. package/dist/strings/index.js.map +1 -1
  466. package/dist/strings/labels.d.ts +4 -0
  467. package/dist/strings/labels.d.ts.map +1 -1
  468. package/dist/strings/labels.js +45 -41
  469. package/dist/strings/labels.js.map +1 -1
  470. package/dist/strings/validation.d.ts.map +1 -1
  471. package/dist/strings/validation.js +71 -71
  472. package/dist/strings/validation.js.map +1 -1
  473. package/dist/utils/commit.d.ts +1 -1
  474. package/dist/utils/commit.d.ts.map +1 -1
  475. package/dist/utils/commit.js +28 -26
  476. package/dist/utils/commit.js.map +1 -1
  477. package/dist/utils/git.d.ts +1 -1
  478. package/dist/utils/git.d.ts.map +1 -1
  479. package/dist/utils/git.js +40 -38
  480. package/dist/utils/git.js.map +1 -1
  481. package/dist/utils/grep.js +11 -11
  482. package/dist/utils/grep.js.map +1 -1
  483. package/dist/utils/index.d.ts +7 -7
  484. package/dist/utils/index.d.ts.map +1 -1
  485. package/dist/utils/index.js +4 -4
  486. package/dist/utils/index.js.map +1 -1
  487. package/dist/utils/time.d.ts.map +1 -1
  488. package/dist/utils/time.js +10 -10
  489. package/dist/utils/time.js.map +1 -1
  490. package/package.json +28 -5
  491. package/plugin/.claude-plugin/marketplace.json +17 -0
  492. package/plugin/.claude-plugin/plugin.json +5 -0
  493. package/plugin/plugins/kspec/skills/help/SKILL.md +42 -0
  494. package/plugin/plugins/kspec/skills/triage/SKILL.md +206 -0
  495. package/plugin/plugins/kspec/skills/triage/docs/automation.md +120 -0
  496. package/plugin/plugins/kspec/skills/triage/docs/inbox.md +144 -0
  497. package/plugin/plugins/kspec/skills/triage/docs/observations.md +85 -0
  498. package/templates/agents-sections/01-quick-start.md +22 -0
  499. package/templates/agents-sections/02-shadow-branch.md +34 -0
  500. package/templates/agents-sections/03-task-lifecycle.md +48 -0
  501. package/templates/agents-sections/04-pr-workflow.md +17 -0
  502. package/templates/agents-sections/05-commit-convention.md +27 -0
  503. package/templates/agents-sections/06-ralph-loop.md +45 -0
  504. package/templates/hooks/pre-commit +34 -0
  505. package/templates/skills/help/SKILL.md +37 -0
  506. package/templates/skills/manifest.yaml +15 -0
  507. package/templates/skills/triage/SKILL.md +199 -0
  508. package/templates/skills/triage/docs/automation.md +120 -0
  509. package/templates/skills/triage/docs/inbox.md +144 -0
  510. package/templates/skills/triage/docs/observations.md +85 -0
@@ -4,104 +4,348 @@
4
4
  * Runs an ACP-compliant agent in a loop to process tasks autonomously.
5
5
  * Uses session event storage for full audit trail and streaming output.
6
6
  */
7
- import chalk from 'chalk';
8
- import { ulid } from 'ulid';
9
- import * as fs from 'node:fs/promises';
10
- import * as path from 'node:path';
11
- import { spawn, spawnSync } from 'node:child_process';
12
- import { initContext } from '../../parser/index.js';
13
- import { error, info, success } from '../output.js';
14
- import { gatherSessionContext } from './session.js';
15
- import { resolveAdapter, registerAdapter } from '../../agents/index.js';
16
- import { spawnAndInitialize } from '../../agents/spawner.js';
17
- import { createSession, updateSessionStatus, appendEvent, saveSessionContext, } from '../../sessions/index.js';
18
- import { createTranslator, createCliRenderer } from '../../ralph/index.js';
19
- import { errors } from '../../strings/index.js';
20
- import { EXIT_CODES } from '../exit-codes.js';
7
+ import { spawn, spawnSync } from "node:child_process";
8
+ import * as fs from "node:fs/promises";
9
+ import { createRequire } from "node:module";
10
+ import * as path from "node:path";
11
+ import chalk from "chalk";
12
+ import { ulid } from "ulid";
13
+ // Read version from package.json for ACP client info
14
+ const require = createRequire(import.meta.url);
15
+ const { version: packageVersion } = require("../../../package.json");
16
+ import { registerAdapter, resolveAdapter, } from "../../agents/index.js";
17
+ import { spawnAndInitialize } from "../../agents/spawner.js";
18
+ import { initContext, loadAllItems, loadAllTasks, ReferenceIndex, } from "../../parser/index.js";
19
+ import { buildWrapUpContext, createCliRenderer, createTranslator, DEFAULT_SUBAGENT_PREFIX, DEFAULT_WRAPUP_TIMEOUT, RALPH_PROMPT_TIMEOUT, runSubagent, runWrapUpAgent, WRAPUP_AGENT_PREFIX, } from "../../ralph/index.js";
20
+ import { appendEvent, createSession, saveSessionContext, updateSessionStatus, } from "../../sessions/index.js";
21
+ import { errors } from "../../strings/index.js";
22
+ import { getCurrentBranch } from "../../utils/git.js";
23
+ import { EXIT_CODES } from "../exit-codes.js";
24
+ import { error, info, success, warn } from "../output.js";
25
+ import { gatherSessionContext, getIterationStats, } from "./session.js";
26
+ const TASK_LIMIT_MARKER_PATH = ".claude/ralph-task-limit.json";
27
+ const END_LOOP_MARKER_PATH = ".claude/ralph-end-loop.json";
28
+ const STALE_MARKER_THRESHOLD_MS = 60 * 60 * 1000; // 1 hour
29
+ /**
30
+ * Write task limit marker file.
31
+ * AC: @ralph-task-limit ac-wrapup, ac-marker-format
32
+ */
33
+ async function writeTaskLimitMarker(rootDir, marker) {
34
+ const markerPath = path.join(rootDir, TASK_LIMIT_MARKER_PATH);
35
+ const dir = path.dirname(markerPath);
36
+ await fs.mkdir(dir, { recursive: true });
37
+ await fs.writeFile(markerPath, JSON.stringify(marker, null, 2));
38
+ }
39
+ /**
40
+ * Read task limit marker file if it exists.
41
+ */
42
+ async function readTaskLimitMarker(rootDir) {
43
+ const markerPath = path.join(rootDir, TASK_LIMIT_MARKER_PATH);
44
+ try {
45
+ const content = await fs.readFile(markerPath, "utf-8");
46
+ return JSON.parse(content);
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ /**
53
+ * Clear task limit marker file.
54
+ * AC: @ralph-task-limit ac-reset
55
+ */
56
+ async function clearTaskLimitMarker(rootDir) {
57
+ const markerPath = path.join(rootDir, TASK_LIMIT_MARKER_PATH);
58
+ try {
59
+ await fs.unlink(markerPath);
60
+ }
61
+ catch {
62
+ // Ignore if file doesn't exist
63
+ }
64
+ }
65
+ /**
66
+ * Clear stale marker files (older than 1 hour).
67
+ * AC: @ralph-task-limit ac-reset
68
+ */
69
+ async function clearStaleMarker(rootDir) {
70
+ const marker = await readTaskLimitMarker(rootDir);
71
+ if (!marker)
72
+ return false;
73
+ const markerAge = Date.now() - new Date(marker.since).getTime();
74
+ if (markerAge > STALE_MARKER_THRESHOLD_MS) {
75
+ await clearTaskLimitMarker(rootDir);
76
+ return true;
77
+ }
78
+ return false;
79
+ }
80
+ /**
81
+ * Write end-loop marker file.
82
+ * AC: @ralph-end-loop ac-cmd
83
+ */
84
+ async function writeEndLoopMarker(rootDir, reason) {
85
+ const markerPath = path.join(rootDir, END_LOOP_MARKER_PATH);
86
+ const dir = path.dirname(markerPath);
87
+ await fs.mkdir(dir, { recursive: true });
88
+ const marker = {
89
+ requested: true,
90
+ timestamp: new Date().toISOString(),
91
+ reason,
92
+ };
93
+ await fs.writeFile(markerPath, JSON.stringify(marker, null, 2));
94
+ }
95
+ /**
96
+ * Read end-loop marker file if it exists.
97
+ * AC: @ralph-end-loop ac-detect
98
+ */
99
+ async function readEndLoopMarker(rootDir) {
100
+ const markerPath = path.join(rootDir, END_LOOP_MARKER_PATH);
101
+ try {
102
+ const content = await fs.readFile(markerPath, "utf-8");
103
+ return JSON.parse(content);
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ }
109
+ /**
110
+ * Clear end-loop marker file.
111
+ * AC: @ralph-end-loop ac-cleanup
112
+ */
113
+ async function clearEndLoopMarker(rootDir) {
114
+ const markerPath = path.join(rootDir, END_LOOP_MARKER_PATH);
115
+ try {
116
+ await fs.unlink(markerPath);
117
+ }
118
+ catch {
119
+ // Ignore if file doesn't exist
120
+ }
121
+ }
122
+ /**
123
+ * Clear stale end-loop markers (older than 1 hour).
124
+ * AC: @ralph-end-loop ac-cleanup
125
+ */
126
+ async function clearStaleEndLoopMarker(rootDir) {
127
+ const marker = await readEndLoopMarker(rootDir);
128
+ if (!marker)
129
+ return false;
130
+ const markerAge = Date.now() - new Date(marker.timestamp).getTime();
131
+ if (markerAge > STALE_MARKER_THRESHOLD_MS) {
132
+ await clearEndLoopMarker(rootDir);
133
+ return true;
134
+ }
135
+ return false;
136
+ }
137
+ /**
138
+ * Detect if a Bash command is a task complete command.
139
+ * AC: @ralph-task-limit ac-detection
140
+ */
141
+ function detectTaskCompleteCommand(command) {
142
+ // Match variations of "kspec task complete"
143
+ // Don't match "kspec task submit" - that's just status change to pending_review
144
+ return /\bkspec\s+task\s+complete\b/.test(command);
145
+ }
146
+ /**
147
+ * Detect if a Bash command is an end-loop command.
148
+ * AC: @ralph-end-loop ac-detect
149
+ */
150
+ function detectEndLoopCommand(command) {
151
+ // Match "kspec ralph end-loop" with any arguments
152
+ return /\bkspec\s+ralph\s+end-loop\b/.test(command);
153
+ }
154
+ /**
155
+ * Extract Bash command from SessionUpdate if it's a tool_call or tool_call_update event.
156
+ * Returns null if not a Bash tool call.
157
+ */
158
+ function extractBashCommand(update) {
159
+ const u = update;
160
+ // Check if this is a tool call event
161
+ if (u.sessionUpdate !== "tool_call" && u.sessionUpdate !== "tool_call_update") {
162
+ return null;
163
+ }
164
+ // Extract tool name - check various locations Claude Code uses
165
+ let toolName;
166
+ // Try _meta.claudeCode.toolName first (Claude Code pattern)
167
+ const meta = u._meta;
168
+ if (meta) {
169
+ const claudeCode = meta.claudeCode;
170
+ if (claudeCode?.toolName) {
171
+ toolName = String(claudeCode.toolName);
172
+ }
173
+ else if (meta.toolName) {
174
+ toolName = String(meta.toolName);
175
+ }
176
+ }
177
+ // Fall back to name or title field
178
+ if (!toolName && u.name) {
179
+ toolName = String(u.name);
180
+ }
181
+ if (!toolName && u.title) {
182
+ toolName = String(u.title);
183
+ }
184
+ // Check if it's a Bash tool (handle MCP prefix variations)
185
+ if (!toolName)
186
+ return null;
187
+ const isBash = toolName === "Bash" || toolName.endsWith("__Bash");
188
+ if (!isBash)
189
+ return null;
190
+ // Extract command from input
191
+ const input = (u.rawInput || u.input || u.params);
192
+ if (!input)
193
+ return null;
194
+ const command = input.command;
195
+ if (typeof command !== "string")
196
+ return null;
197
+ return command;
198
+ }
199
+ /**
200
+ * Parse and validate --tasks flag value.
201
+ * Returns resolved ULIDs for the specified task refs.
202
+ * AC: @cli-ralph ac-21
203
+ *
204
+ * @throws Error if any ref cannot be resolved or is not a task
205
+ */
206
+ async function parseExplicitTasks(ctx, tasksArg) {
207
+ const refs = tasksArg.split(",").map((r) => r.trim()).filter(Boolean);
208
+ if (refs.length === 0) {
209
+ throw new Error("--tasks requires at least one task reference");
210
+ }
211
+ // Load tasks and items for resolution
212
+ const tasks = await loadAllTasks(ctx);
213
+ const items = await loadAllItems(ctx);
214
+ const index = new ReferenceIndex(tasks, items);
215
+ const ulids = [];
216
+ for (const ref of refs) {
217
+ const result = index.resolve(ref);
218
+ if (!result.ok) {
219
+ throw new Error(`Cannot resolve task reference: ${ref}`);
220
+ }
221
+ // Verify it's a task (not a spec item)
222
+ const task = tasks.find((t) => t._ulid === result.ulid);
223
+ if (!task) {
224
+ throw new Error(`Reference ${ref} is not a task`);
225
+ }
226
+ ulids.push(result.ulid);
227
+ }
228
+ return { refs, ulids };
229
+ }
230
+ /**
231
+ * Filter session context to only include tasks from explicit scope.
232
+ * AC: @cli-ralph ac-21
233
+ */
234
+ function filterByExplicitTasks(ctx, scope) {
235
+ // Task refs in context are short ULIDs (variable length from shortUlid())
236
+ // Check if the context ref is a prefix of any explicit ULID
237
+ const matchesScope = (taskRef) => {
238
+ return scope.ulids.some((ulid) => ulid.startsWith(taskRef));
239
+ };
240
+ return {
241
+ ...ctx,
242
+ active_tasks: ctx.active_tasks.filter((t) => matchesScope(t.ref)),
243
+ pending_review_tasks: ctx.pending_review_tasks.filter((t) => matchesScope(t.ref)),
244
+ ready_tasks: ctx.ready_tasks.filter((t) => matchesScope(t.ref)),
245
+ };
246
+ }
247
+ /**
248
+ * Check if all explicit tasks are completed or blocked.
249
+ * AC: @cli-ralph ac-21
250
+ */
251
+ async function allExplicitTasksDone(ctx, scope) {
252
+ const tasks = await loadAllTasks(ctx);
253
+ const statuses = new Map();
254
+ for (const ulid of scope.ulids) {
255
+ const task = tasks.find((t) => t._ulid === ulid);
256
+ if (task) {
257
+ statuses.set(ulid.slice(0, 8), task.status);
258
+ }
259
+ }
260
+ // Check if all are completed or blocked
261
+ const done = scope.ulids.every((ulid) => {
262
+ const status = statuses.get(ulid.slice(0, 8));
263
+ return status === "completed" || status === "blocked";
264
+ });
265
+ return { done, statuses };
266
+ }
21
267
  // ─── Prompt Template ─────────────────────────────────────────────────────────
22
- function buildPrompt(sessionCtx, iteration, maxLoops, focus) {
23
- const isFinal = iteration === maxLoops;
24
- const focusSection = focus ? `
268
+ // AC: @ralph-skill-delegation ac-1, ac-2, ac-3
269
+ function buildTaskWorkPrompt(sessionCtx, iteration, maxLoops, sessionId, focus, explicitTaskScope) {
270
+ const focusSection = focus
271
+ ? `
25
272
  ## Session Focus (applies to ALL iterations)
26
273
 
27
274
  > **${focus}**
28
275
 
29
276
  Keep this focus in mind throughout your work. It takes priority over default task selection.
30
- ` : '';
31
- return `# Kspec Automation Session
277
+ `
278
+ : "";
279
+ // AC: @cli-ralph ac-21 - Explicit task scope indicator in prompt
280
+ const taskScopeSection = explicitTaskScope
281
+ ? `
282
+ ## Explicit Task Scope
283
+
284
+ This session is scoped to specific tasks: ${explicitTaskScope.refs.join(", ")}
285
+
286
+ **Only work on these tasks.** The loop will exit when all listed tasks are completed or blocked.
287
+ `
288
+ : "";
289
+ const modeDescription = explicitTaskScope
290
+ ? "Loop mode means: no confirmations, auto-resolve decisions, explicit task scope (only the listed tasks)."
291
+ : "Loop mode means: no confirmations, auto-resolve decisions, automation-eligible tasks only.";
292
+ return `# Kspec Automation Session - Task Work
32
293
 
33
- You are running as part of a kspec automation loop. This is iteration ${iteration} of ${maxLoops}.
34
- ${focusSection}
294
+ **Session ID:** \`${sessionId}\`
295
+ **Iteration:** ${iteration} of ${maxLoops}
296
+ **Mode:** Automated (no human in the loop)
297
+ ${focusSection}${taskScopeSection}
35
298
 
36
299
  ## Current State
37
300
  \`\`\`json
38
301
  ${JSON.stringify(sessionCtx, null, 2)}
39
302
  \`\`\`
40
303
 
41
- ## Working Procedure
304
+ ## Instructions
42
305
 
43
- 1. **Pick a task**: Review ready_tasks above. Pick the highest priority task (lowest number = higher priority). If there's an active (in_progress) task, continue that instead.
306
+ Run the task-work skill in loop mode:
44
307
 
45
- 2. **Start the task** (if not already in_progress):
46
- \`\`\`bash
47
- kspec task start @task-ref
48
- \`\`\`
49
-
50
- 3. **Do the work**:
51
- - Read relevant files to understand the task
52
- - Make changes as needed
53
- - Run tests if applicable
54
- - Document as you go with task notes
308
+ \`\`\`
309
+ /task-work loop
310
+ \`\`\`
55
311
 
56
- 4. **Document progress**:
57
- \`\`\`bash
58
- kspec task note @task-ref "What you did, decisions made, etc."
59
- \`\`\`
312
+ ${modeDescription}
60
313
 
61
- 5. **Submit or checkpoint**:
62
- - If code is DONE (ready for PR):
63
- \`\`\`bash
64
- kspec task submit @task-ref
65
- \`\`\`
66
- - If task is NOT done (WIP):
67
- \`\`\`bash
68
- kspec task note @task-ref "WIP: What's done, what remains..."
69
- \`\`\`
314
+ **Normal flow:** Work on a task, create a PR, then stop responding. Ralph continues automatically —
315
+ it checks for remaining eligible tasks at the start of each iteration and exits the loop itself when none remain.
70
316
 
71
- 6. **Commit your work**:
72
- \`\`\`bash
73
- git add -A && git commit -m "feat/fix/chore: description
317
+ **Do NOT call \`end-loop\` after completing a task.** Simply stop responding.
318
+ \`end-loop\` is a rare escape hatch for when work is stalling across multiple iterations with no progress — not a normal exit path.
319
+ `;
320
+ }
321
+ /**
322
+ * Build the reflect prompt sent after task-work completes.
323
+ * Ralph sends this as a separate prompt to ensure reflection always happens.
324
+ */
325
+ function buildReflectPrompt(iteration, maxLoops, sessionId) {
326
+ const isFinal = iteration === maxLoops;
327
+ return `# Kspec Automation Session - Reflection
74
328
 
75
- Task: @task-ref"
76
- \`\`\`
329
+ **Session ID:** \`${sessionId}\`
330
+ **Iteration:** ${iteration} of ${maxLoops}
331
+ **Phase:** Post-task reflection
77
332
 
78
- 7. **Reflect on this iteration**:
79
- Think about what you learned, any friction points, or patterns worth remembering.
333
+ ## Instructions
80
334
 
81
- For **systemic patterns** (friction or success worth documenting):
82
- \`\`\`bash
83
- kspec meta observe friction "Description of systemic issue..."
84
- kspec meta observe success "Pattern worth replicating..."
85
- \`\`\`
335
+ Run the reflect skill in loop mode:
86
336
 
87
- For **actionable improvements** (specific ideas that could become tasks):
88
- \`\`\`bash
89
- kspec inbox add "Improvement idea..." --tag reflection
90
- \`\`\`
337
+ \`\`\`
338
+ /reflect loop
339
+ \`\`\`
91
340
 
92
- ## Important Notes
93
- - Stay focused on ONE task per iteration
94
- - The loop continues automatically - don't worry about picking the next task
95
- - kspec tracks state across iterations via task status and notes
96
- - Always commit before the iteration ends
97
- - Always reflect and capture at least one observation
98
- ${isFinal ? `
99
- ## FINAL ITERATION
100
- This is the last iteration of the loop. After completing your work:
101
- 1. Commit any remaining changes
102
- 2. Reflect on the overall session
103
- 3. Capture any final insights as observations
104
- ` : ''}`;
341
+ Loop mode means: high-confidence captures only, must search existing before capturing, no user prompts.
342
+ ${isFinal
343
+ ? `
344
+ **FINAL ITERATION** - This is the last chance to capture insights from this session.
345
+ `
346
+ : ""}
347
+ Exit when reflection is complete.
348
+ `;
105
349
  }
106
350
  // ─── Streaming Output ────────────────────────────────────────────────────────
107
351
  // Translator and renderer are created per-session in the action handler.
@@ -117,9 +361,9 @@ This is the last iteration of the loop. After completing your work:
117
361
  function validateAdapter(adapterPackage) {
118
362
  // Use npx --no-install with --version to check if package exists
119
363
  // This checks both global and local node_modules, handles scoped packages
120
- const result = spawnSync('npx', ['--no-install', adapterPackage, '--version'], {
121
- encoding: 'utf-8',
122
- stdio: 'pipe',
364
+ const result = spawnSync("npx", ["--no-install", adapterPackage, "--version"], {
365
+ encoding: "utf-8",
366
+ stdio: "pipe",
123
367
  });
124
368
  if (result.status !== 0) {
125
369
  error(`Adapter package not found: ${adapterPackage}. Install with: npm install -g ${adapterPackage}`);
@@ -132,48 +376,50 @@ function validateAdapter(adapterPackage) {
132
376
  * Implements file operations, terminal commands, and permission handling.
133
377
  */
134
378
  async function handleRequest(client, id, method, params, yolo) {
135
- const p = params;
136
379
  try {
137
380
  switch (method) {
138
- case 'session/request_permission': {
381
+ case "session/request_permission": {
382
+ const p = params;
139
383
  // In yolo mode, auto-approve all permissions
140
384
  // In normal mode, would need to implement permission UI
141
385
  const options = p.options || [];
142
386
  if (yolo) {
143
387
  // Find an "allow" option (prefer allow_always, then allow_once)
144
- const allowOption = options.find(o => o.kind === 'allow_always')
145
- || options.find(o => o.kind === 'allow_once');
388
+ const allowOption = options.find((o) => o.kind === "allow_always") ||
389
+ options.find((o) => o.kind === "allow_once");
146
390
  if (allowOption) {
147
391
  client.respondPermission(id, {
148
- outcome: { outcome: 'selected', optionId: allowOption.optionId },
392
+ outcome: { outcome: "selected", optionId: allowOption.optionId },
149
393
  });
150
394
  }
151
395
  else {
152
396
  // No allow option available - cancel
153
- client.respondPermission(id, { outcome: { outcome: 'cancelled' } });
397
+ client.respondPermission(id, { outcome: { outcome: "cancelled" } });
154
398
  }
155
399
  }
156
400
  else {
157
401
  // TODO: Implement permission prompting
158
- client.respondPermission(id, { outcome: { outcome: 'cancelled' } });
402
+ client.respondPermission(id, { outcome: { outcome: "cancelled" } });
159
403
  }
160
404
  break;
161
405
  }
162
- case 'file/read': {
163
- const filePath = p.path;
164
- const content = await fs.readFile(filePath, 'utf-8');
165
- client.respond(id, { content });
406
+ case "file/read": {
407
+ const p = params;
408
+ const content = await fs.readFile(p.path, "utf-8");
409
+ client.respondReadTextFile(id, { content });
166
410
  break;
167
411
  }
168
- case 'file/write': {
169
- const filePath = p.path;
170
- const content = p.content;
171
- await fs.mkdir(path.dirname(filePath), { recursive: true });
172
- await fs.writeFile(filePath, content, 'utf-8');
173
- client.respond(id, {});
412
+ case "file/write": {
413
+ const p = params;
414
+ await fs.mkdir(path.dirname(p.path), { recursive: true });
415
+ await fs.writeFile(p.path, p.content, "utf-8");
416
+ client.respondWriteTextFile(id, {});
174
417
  break;
175
418
  }
176
- case 'terminal/run': {
419
+ case "terminal/run": {
420
+ // Custom method (not part of ACP spec - ACP uses createTerminal instead)
421
+ // TODO: Consider migrating to standard ACP terminal methods
422
+ const p = params;
177
423
  const command = p.command;
178
424
  const cwd = p.cwd || process.cwd();
179
425
  const timeout = p.timeout || 60000;
@@ -183,21 +429,22 @@ async function handleRequest(client, id, method, params, yolo) {
183
429
  shell: true,
184
430
  timeout,
185
431
  });
186
- let stdout = '';
187
- let stderr = '';
188
- child.stdout?.on('data', (data) => {
432
+ let stdout = "";
433
+ let stderr = "";
434
+ child.stdout?.on("data", (data) => {
189
435
  stdout += data.toString();
190
436
  });
191
- child.stderr?.on('data', (data) => {
437
+ child.stderr?.on("data", (data) => {
192
438
  stderr += data.toString();
193
439
  });
194
- child.on('close', (code) => {
440
+ child.on("close", (code) => {
195
441
  resolve({ stdout, stderr, exitCode: code ?? 1 });
196
442
  });
197
- child.on('error', (err) => {
443
+ child.on("error", (err) => {
198
444
  resolve({ stdout, stderr: err.message, exitCode: 1 });
199
445
  });
200
446
  });
447
+ // Using generic respond() since this is a custom method
201
448
  client.respond(id, result);
202
449
  break;
203
450
  }
@@ -211,65 +458,439 @@ async function handleRequest(client, id, method, params, yolo) {
211
458
  client.respondError(id, -32000, message);
212
459
  }
213
460
  }
461
+ // ─── Subagent Support ─────────────────────────────────────────────────────────
462
+ /**
463
+ * Build context for a PR review subagent.
464
+ * AC: @ralph-subagent-spawning ac-10
465
+ */
466
+ async function buildSubagentContext(ctx, taskRef) {
467
+ // Load all tasks and items
468
+ const tasks = await loadAllTasks(ctx);
469
+ const items = await loadAllItems(ctx);
470
+ const index = new ReferenceIndex(tasks, items);
471
+ // Resolve task reference
472
+ const taskResult = index.resolve(taskRef);
473
+ if (!taskResult.ok) {
474
+ throw new Error(`Task not found: ${taskRef}`);
475
+ }
476
+ const task = tasks.find((t) => t._ulid === taskResult.ulid);
477
+ if (!task) {
478
+ throw new Error(`Task not found by ULID: ${taskResult.ulid}`);
479
+ }
480
+ // Get linked spec with ACs if spec_ref exists
481
+ let specWithACs = null;
482
+ if (task.spec_ref) {
483
+ const specResult = index.resolve(task.spec_ref);
484
+ if (specResult.ok) {
485
+ const item = items.find((i) => i._ulid === specResult.ulid);
486
+ if (item) {
487
+ specWithACs = item;
488
+ }
489
+ }
490
+ }
491
+ // Get git branch
492
+ const gitBranch = getCurrentBranch(ctx.rootDir) || "unknown";
493
+ return {
494
+ taskRef,
495
+ taskDetails: task,
496
+ specWithACs,
497
+ gitBranch,
498
+ };
499
+ }
500
+ /**
501
+ * Get the current status of a task.
502
+ * AC: @ralph-subagent-spawning ac-12
503
+ */
504
+ function getTaskStatus(taskRef) {
505
+ const result = spawnSync("kspec", ["task", "get", taskRef, "--json"], {
506
+ encoding: "utf-8",
507
+ stdio: "pipe",
508
+ });
509
+ if (result.status !== 0) {
510
+ warn(`Failed to check task status for ${taskRef}: ${result.stderr}`);
511
+ return null;
512
+ }
513
+ try {
514
+ return JSON.parse(result.stdout).status;
515
+ }
516
+ catch {
517
+ warn(`Failed to parse task status for ${taskRef}`);
518
+ return null;
519
+ }
520
+ }
521
+ /**
522
+ * Mark a task as needing review due to subagent timeout.
523
+ * AC: @ralph-subagent-spawning ac-9
524
+ */
525
+ async function markTaskNeedsReview(taskRef, reason) {
526
+ const { spawnSync } = await import("node:child_process");
527
+ // Use kspec CLI to set automation status
528
+ const result = spawnSync("kspec", ["task", "set-automation", taskRef, "needs_review"], {
529
+ encoding: "utf-8",
530
+ stdio: "pipe",
531
+ });
532
+ if (result.status !== 0) {
533
+ warn(`Failed to mark task ${taskRef} as needs_review: ${result.stderr}`);
534
+ }
535
+ // Add a note explaining the timeout
536
+ const noteResult = spawnSync("kspec", ["task", "note", taskRef, `[RALPH SUBAGENT] ${reason}`], {
537
+ encoding: "utf-8",
538
+ stdio: "pipe",
539
+ });
540
+ if (noteResult.status !== 0) {
541
+ warn(`Failed to add timeout note to task ${taskRef}: ${noteResult.stderr}`);
542
+ }
543
+ }
544
+ /**
545
+ * Post a comment on the open PR for a task's branch, noting incomplete review.
546
+ * Uses `gh pr list --head <branch>` to find the PR and add a warning.
547
+ */
548
+ async function commentOnPRReviewIncomplete(branch, reason) {
549
+ if (!branch || branch === "unknown") {
550
+ return;
551
+ }
552
+ const prListResult = spawnSync("gh", ["pr", "list", "--state", "open", "--head", branch, "--json", "number", "--jq", ".[0].number"], { encoding: "utf-8", stdio: "pipe" });
553
+ const prNumber = prListResult.stdout?.trim();
554
+ if (!prNumber || prListResult.status !== 0) {
555
+ // No open PR found — may already be merged or branch has no PR
556
+ return;
557
+ }
558
+ const body = `⚠️ **Review incomplete**: ${reason}\n\nThis PR was not fully reviewed by the ralph review subagent. Manual review recommended before merging.`;
559
+ const commentResult = spawnSync("gh", ["pr", "comment", prNumber, "--body", body], { encoding: "utf-8", stdio: "pipe" });
560
+ if (commentResult.status !== 0) {
561
+ warn(`Failed to comment on PR #${prNumber}: ${commentResult.stderr}`);
562
+ }
563
+ else {
564
+ info(`${DEFAULT_SUBAGENT_PREFIX} Posted review-incomplete comment on PR #${prNumber}`);
565
+ }
566
+ }
567
+ /**
568
+ * Handle failed iteration by tracking per-task failures and escalating at threshold.
569
+ * AC: @loop-mode-error-handling ac-1, ac-2, ac-3, ac-4, ac-5, ac-8
570
+ */
571
+ async function handleIterationFailure(ctx, tasksInProgressAtStart, iterationStartTime, errorDescription) {
572
+ if (tasksInProgressAtStart.length === 0) {
573
+ return;
574
+ }
575
+ // Re-load current tasks to check progress
576
+ const currentTasks = await loadAllTasks(ctx);
577
+ const index = new ReferenceIndex(currentTasks, await loadAllItems(ctx));
578
+ // Convert ActiveTaskSummary to Task-like objects for processing
579
+ const tasksInProgressFull = tasksInProgressAtStart
580
+ .map((summary) => {
581
+ const resolved = index.resolve(summary.ref);
582
+ if (!resolved.ok)
583
+ return undefined;
584
+ // Check if the resolved item is a task (not a spec item or meta item)
585
+ const item = resolved.item;
586
+ if (!("status" in item))
587
+ return undefined; // Spec items don't have status
588
+ return currentTasks.find((t) => t._ulid === resolved.ulid);
589
+ })
590
+ .filter((t) => t !== undefined && t.status === "in_progress");
591
+ if (tasksInProgressFull.length === 0) {
592
+ return;
593
+ }
594
+ // Process failures
595
+ const { processFailedIteration, createFailureNote, getTaskFailureCount } = await import("../../ralph/index.js");
596
+ const results = processFailedIteration(tasksInProgressFull, currentTasks, iterationStartTime, errorDescription);
597
+ // Add notes and escalate tasks
598
+ for (const result of results) {
599
+ const taskRef = result.taskRef;
600
+ const task = currentTasks.find((t) => t._ulid === taskRef);
601
+ if (!task)
602
+ continue;
603
+ const priorCount = result.failureCount - 1;
604
+ const noteContent = createFailureNote(taskRef, errorDescription, priorCount);
605
+ // Add LOOP-FAIL note
606
+ const noteResult = spawnSync("kspec", ["task", "note", `@${taskRef}`, noteContent], {
607
+ encoding: "utf-8",
608
+ stdio: "pipe",
609
+ cwd: process.cwd(),
610
+ });
611
+ if (noteResult.status !== 0) {
612
+ warn(`Failed to add failure note to task ${taskRef}: ${noteResult.stderr}`);
613
+ continue;
614
+ }
615
+ // AC: @loop-mode-error-handling ac-5 - Escalate at threshold
616
+ if (result.escalated) {
617
+ const escalateResult = spawnSync("kspec", [
618
+ "task",
619
+ "set",
620
+ `@${taskRef}`,
621
+ "--automation",
622
+ "needs_review",
623
+ "--reason",
624
+ `Loop mode: 3 consecutive failures without progress`,
625
+ ], {
626
+ encoding: "utf-8",
627
+ stdio: "pipe",
628
+ cwd: process.cwd(),
629
+ });
630
+ if (escalateResult.status !== 0) {
631
+ warn(`Failed to escalate task ${taskRef}: ${escalateResult.stderr}`);
632
+ }
633
+ else {
634
+ info(`Escalated task ${taskRef} to automation:needs_review after 3 failures`);
635
+ }
636
+ }
637
+ }
638
+ }
639
+ /**
640
+ * Process pending_review tasks by spawning subagents.
641
+ * AC: @ralph-subagent-spawning ac-6, ac-8
642
+ */
643
+ async function processPendingReviewTasks(ctx, adapter, pendingReviewTasks, options, consecutiveFailures) {
644
+ if (pendingReviewTasks.length === 0) {
645
+ return true;
646
+ }
647
+ // Visual separator for subagent section
648
+ console.log("");
649
+ console.log(chalk.cyan(`${"═".repeat(60)}`));
650
+ console.log(chalk.cyan.bold(`${DEFAULT_SUBAGENT_PREFIX} Processing Pending Review Tasks`));
651
+ console.log(chalk.cyan(`${"═".repeat(60)}`));
652
+ console.log("");
653
+ info(`${DEFAULT_SUBAGENT_PREFIX} Found ${pendingReviewTasks.length} pending_review task(s)`);
654
+ // AC: @ralph-subagent-spawning ac-6 - Process one at a time
655
+ for (const task of pendingReviewTasks) {
656
+ info(`${DEFAULT_SUBAGENT_PREFIX} Processing: ${task.ref} - ${task.title}`);
657
+ try {
658
+ // Build context for this task
659
+ const subagentCtx = await buildSubagentContext(ctx, task.ref);
660
+ // AC: @ralph-subagent-spawning ac-1, ac-3 - Spawn and wait
661
+ const result = await runSubagent(adapter, subagentCtx, {
662
+ timeout: options.subagentTimeout,
663
+ outputPrefix: DEFAULT_SUBAGENT_PREFIX,
664
+ }, {
665
+ yolo: options.yolo,
666
+ cwd: options.cwd,
667
+ handleRequest: (client, reqId, method, params) => handleRequest(client, reqId, method, params, options.yolo),
668
+ });
669
+ if (result.timedOut) {
670
+ // AC: @ralph-subagent-spawning ac-9
671
+ warn(`${DEFAULT_SUBAGENT_PREFIX} Subagent timed out for ${task.ref}`);
672
+ const timeoutMinutes = Math.round(options.subagentTimeout / 60000);
673
+ await markTaskNeedsReview(task.ref, `Subagent timed out after ${timeoutMinutes} minutes`);
674
+ await commentOnPRReviewIncomplete(subagentCtx.gitBranch, `Review subagent timed out after ${timeoutMinutes} minutes for task ${task.ref}.`);
675
+ consecutiveFailures.count++;
676
+ }
677
+ else if (!result.success) {
678
+ // AC: @ralph-subagent-spawning ac-7
679
+ error(`${DEFAULT_SUBAGENT_PREFIX} Subagent failed for ${task.ref}: ${result.error}`);
680
+ await commentOnPRReviewIncomplete(subagentCtx.gitBranch, `Review subagent failed for task ${task.ref}: ${result.error}`);
681
+ consecutiveFailures.count++;
682
+ }
683
+ else {
684
+ // AC: @ralph-subagent-spawning ac-12 - Verify task outcome
685
+ const currentStatus = getTaskStatus(task.ref);
686
+ if (currentStatus === "completed") {
687
+ success(`${DEFAULT_SUBAGENT_PREFIX} Completed: ${task.ref}`);
688
+ consecutiveFailures.count = 0;
689
+ }
690
+ else if (currentStatus === "needs_work") {
691
+ // Expected: reviewer found issues, kicked back to worker
692
+ info(`${DEFAULT_SUBAGENT_PREFIX} Review completed for ${task.ref} — issues found, kicked back to worker`);
693
+ // NOT a failure — the review worked correctly
694
+ consecutiveFailures.count = 0;
695
+ }
696
+ else if (currentStatus === "pending_review") {
697
+ // Subagent didn't transition or merge — count as soft failure
698
+ warn(`${DEFAULT_SUBAGENT_PREFIX} Subagent completed but task ${task.ref} unchanged`);
699
+ await markTaskNeedsReview(task.ref, "Subagent completed but did not merge or kick back. Review required.");
700
+ consecutiveFailures.count++;
701
+ }
702
+ else {
703
+ warn(`${DEFAULT_SUBAGENT_PREFIX} Task ${task.ref} in unexpected state: ${currentStatus}`);
704
+ consecutiveFailures.count++;
705
+ }
706
+ }
707
+ // Check if we've hit max failures
708
+ if (consecutiveFailures.count >= options.maxFailures) {
709
+ error(`${DEFAULT_SUBAGENT_PREFIX} Reached max failures (${options.maxFailures})`);
710
+ return false;
711
+ }
712
+ }
713
+ catch (err) {
714
+ const message = err instanceof Error ? err.message : String(err);
715
+ error(`${DEFAULT_SUBAGENT_PREFIX} Error processing ${task.ref}: ${message}`);
716
+ consecutiveFailures.count++;
717
+ if (consecutiveFailures.count >= options.maxFailures) {
718
+ error(`${DEFAULT_SUBAGENT_PREFIX} Reached max failures (${options.maxFailures})`);
719
+ return false;
720
+ }
721
+ }
722
+ }
723
+ // Visual separator at end of subagent section
724
+ console.log("");
725
+ console.log(chalk.cyan(`${"═".repeat(60)}`));
726
+ console.log(chalk.cyan.bold(`${DEFAULT_SUBAGENT_PREFIX} Completed Review Processing`));
727
+ console.log(chalk.cyan(`${"═".repeat(60)}`));
728
+ console.log("");
729
+ return true;
730
+ }
214
731
  // ─── Command Registration ────────────────────────────────────────────────────
215
732
  export function registerRalphCommand(program) {
216
- program
217
- .command('ralph')
218
- .description('Run ACP agent in a loop to process ready tasks')
219
- .option('--max-loops <n>', 'Maximum iterations', '5')
220
- .option('--max-retries <n>', 'Max retries per iteration on error', '3')
221
- .option('--max-failures <n>', 'Max consecutive failed iterations before exit', '3')
222
- .option('--dry-run', 'Show prompt without executing')
223
- .option('--yolo', 'Use dangerously-skip-permissions (default)', true)
224
- .option('--no-yolo', 'Require normal permission prompts')
225
- .option('--adapter <id>', 'Agent adapter to use', 'claude-code-acp')
226
- .option('--adapter-cmd <cmd>', 'Custom adapter command (for testing)')
227
- .option('--focus <instructions>', 'Focus instructions included in every iteration prompt')
733
+ const ralph = program
734
+ .command("ralph")
735
+ .description("Ralph automated task loop and agent control");
736
+ // end-loop subcommand - allows agent to signal loop termination
737
+ // AC: @ralph-end-loop ac-cmd, ac-reason, ac-noop-outside
738
+ ralph
739
+ .command("end-loop")
740
+ .description("End the ralph loop gracefully (stops all remaining iterations)")
741
+ .option("--reason <reason>", "Reason for ending the loop")
228
742
  .action(async (options) => {
743
+ try {
744
+ const ctx = await initContext();
745
+ // Check if we're in a ralph session by looking for any ralph marker
746
+ const taskLimitMarker = await readTaskLimitMarker(ctx.rootDir);
747
+ const endLoopMarker = await readEndLoopMarker(ctx.rootDir);
748
+ // Write the marker with reason if provided
749
+ await writeEndLoopMarker(ctx.rootDir, options.reason);
750
+ // Determine if we're likely in a ralph session
751
+ const inRalphSession = taskLimitMarker !== null || endLoopMarker !== null;
752
+ if (!inRalphSession) {
753
+ // AC: @ralph-end-loop ac-noop-outside
754
+ warn("No active ralph session detected. Marker written but may have no effect.");
755
+ info("This command is designed to be called by agents during a ralph loop.");
756
+ }
757
+ else {
758
+ success("Loop end signal sent");
759
+ }
760
+ if (options.reason) {
761
+ info(`Reason: ${options.reason}`);
762
+ }
763
+ }
764
+ catch (err) {
765
+ error("Failed to signal end-loop", err);
766
+ process.exit(EXIT_CODES.ERROR);
767
+ }
768
+ });
769
+ // Main ralph run command (default behavior when ralph is called directly)
770
+ ralph
771
+ .command("run", { isDefault: true })
772
+ .description("Run ACP agent in a loop to process ready tasks")
773
+ .argument("[args...]", "")
774
+ .option("--max-loops <n>", "Maximum iterations", "5")
775
+ .option("--max-retries <n>", "Max retries per iteration on error", "3")
776
+ .option("--max-failures <n>", "Max consecutive failed iterations before exit", "3")
777
+ .option("--dry-run", "Show prompt without executing")
778
+ .option("--yolo", "Use dangerously-skip-permissions (default)", true)
779
+ .option("--no-yolo", "Require normal permission prompts")
780
+ .option("--subagent-timeout <minutes>", "Review subagent timeout in minutes", "20")
781
+ .option("--adapter <id>", "Agent adapter to use", "claude-agent-acp")
782
+ .option("--adapter-cmd <cmd>", "Custom adapter command (for testing)")
783
+ .option("--restart-every <n>", "Restart agent every N iterations to prevent OOM (0 = never)", "10")
784
+ .option("--focus <instructions>", "Focus instructions included in every iteration prompt")
785
+ .option("--max-tasks <n>", "Max tasks per iteration (0 = unlimited)", "1")
786
+ .option("--tasks <refs>", "Explicit task scope: only work on these tasks (comma-separated refs, e.g., @task1,@task2)")
787
+ .action(async (args, options) => {
788
+ // Check for unknown subcommands that fell through to default
789
+ // Only check args that look like subcommand names (alphanumeric with hyphens, no quotes)
790
+ if (args.length > 0) {
791
+ const unknownCmd = args[0];
792
+ // Skip if it looks like a malformed option or quoted argument
793
+ const looksLikeSubcommand = /^[a-z][a-z0-9-]*$/i.test(unknownCmd);
794
+ if (looksLikeSubcommand) {
795
+ if (unknownCmd === "end-iteration") {
796
+ error(`Unknown command: ${unknownCmd}. Did you mean 'end-loop'?`);
797
+ info("The command was renamed from 'end-iteration' to 'end-loop' to clarify it ends the entire loop.");
798
+ }
799
+ else {
800
+ error(`Unknown command: ${unknownCmd}`);
801
+ }
802
+ info("Run 'kspec ralph --help' to see available commands.");
803
+ process.exit(EXIT_CODES.USAGE_ERROR);
804
+ }
805
+ }
229
806
  try {
230
807
  const maxLoops = parseInt(options.maxLoops, 10);
231
808
  const maxRetries = parseInt(options.maxRetries, 10);
232
809
  const maxFailures = parseInt(options.maxFailures, 10);
233
- if (isNaN(maxLoops) || maxLoops < 1) {
810
+ if (Number.isNaN(maxLoops) || maxLoops < 1) {
234
811
  error(errors.usage.maxLoopsPositive);
235
812
  process.exit(EXIT_CODES.ERROR);
236
813
  }
237
- if (isNaN(maxRetries) || maxRetries < 0) {
814
+ if (Number.isNaN(maxRetries) || maxRetries < 0) {
238
815
  error(errors.usage.maxRetriesNonNegative);
239
816
  process.exit(EXIT_CODES.ERROR);
240
817
  }
241
- if (isNaN(maxFailures) || maxFailures < 1) {
818
+ if (Number.isNaN(maxFailures) || maxFailures < 1) {
242
819
  error(errors.usage.maxFailuresPositive);
243
820
  process.exit(EXIT_CODES.ERROR);
244
821
  }
822
+ const subagentTimeout = parseInt(options.subagentTimeout, 10);
823
+ if (Number.isNaN(subagentTimeout) || subagentTimeout < 1) {
824
+ error("--subagent-timeout must be a positive integer (minutes)");
825
+ process.exit(EXIT_CODES.ERROR);
826
+ }
827
+ const restartEvery = parseInt(options.restartEvery, 10);
828
+ if (Number.isNaN(restartEvery) || restartEvery < 0) {
829
+ error("--restart-every must be a non-negative integer");
830
+ process.exit(EXIT_CODES.ERROR);
831
+ }
832
+ // AC: @ralph-task-limit ac-flag
833
+ const maxTasks = parseInt(options.maxTasks, 10);
834
+ if (Number.isNaN(maxTasks) || maxTasks < 0 || maxTasks > 999) {
835
+ error("--max-tasks must be 0 (unlimited) or a positive integer up to 999");
836
+ process.exit(EXIT_CODES.USAGE_ERROR);
837
+ }
245
838
  // Handle custom adapter command for testing
246
839
  if (options.adapterCmd) {
247
840
  const parts = options.adapterCmd.split(/\s+/);
248
841
  const customAdapter = {
249
842
  command: parts[0],
250
843
  args: parts.slice(1),
251
- description: 'Custom adapter via --adapter-cmd',
844
+ description: "Custom adapter via --adapter-cmd",
252
845
  };
253
- registerAdapter('custom', customAdapter);
254
- options.adapter = 'custom';
846
+ registerAdapter("custom", customAdapter);
847
+ options.adapter = "custom";
255
848
  }
256
849
  // Resolve adapter
257
850
  const adapter = resolveAdapter(options.adapter);
258
851
  // Validate adapter package exists before proceeding
259
- // Skip validation for custom adapters (--adapter-cmd) and non-npx adapters
260
- if (!options.adapterCmd && adapter.command === 'npx' && adapter.args[0]) {
852
+ // Skip validation for:
853
+ // - Custom adapters (--adapter-cmd)
854
+ // - Non-npx adapters
855
+ // - Dry-run mode with default adapter (doesn't spawn agent, default may not be installed in CI)
856
+ // Note: If user explicitly specifies --adapter, validate even in dry-run to catch typos
857
+ // Accept both new and deprecated adapter names
858
+ const isDefaultAdapter = options.adapter === "claude-agent-acp" ||
859
+ options.adapter === "claude-code-acp";
860
+ const skipValidation = options.adapterCmd ||
861
+ adapter.command !== "npx" ||
862
+ !adapter.args[0] ||
863
+ (options.dryRun && isDefaultAdapter);
864
+ if (!skipValidation) {
261
865
  validateAdapter(adapter.args[0]);
262
866
  }
263
- // Add yolo flag to adapter args if needed
264
- if (options.yolo && options.adapter === 'claude-code-acp') {
265
- adapter.args = [...adapter.args, '--dangerously-skip-permissions'];
867
+ // Add yolo flag to adapter args if needed (accept both new and deprecated names)
868
+ if (options.yolo && isDefaultAdapter) {
869
+ adapter.args = [...adapter.args, "--dangerously-skip-permissions"];
870
+ }
871
+ const restartInfo = restartEvery > 0 ? `, restart every ${restartEvery}` : "";
872
+ const maxTasksInfo = maxTasks === 0 ? "unlimited" : `${maxTasks}`;
873
+ // Initialize kspec context early to validate --tasks
874
+ const ctx = await initContext();
875
+ // AC: @cli-ralph ac-21 - Parse explicit task scope
876
+ let explicitTaskScope;
877
+ if (options.tasks) {
878
+ try {
879
+ explicitTaskScope = await parseExplicitTasks(ctx, options.tasks);
880
+ info(`Explicit task scope: ${explicitTaskScope.refs.join(", ")}`);
881
+ }
882
+ catch (err) {
883
+ error(`Invalid --tasks argument: ${err.message}`);
884
+ process.exit(EXIT_CODES.VALIDATION_FAILED);
885
+ }
266
886
  }
267
- info(`Starting ralph loop (adapter=${options.adapter}, max ${maxLoops} iterations, ${maxRetries} retries, ${maxFailures} max failures)`);
887
+ const taskScopeInfo = explicitTaskScope
888
+ ? `, tasks=${explicitTaskScope.refs.join(",")}`
889
+ : "";
890
+ info(`Starting ralph loop (adapter=${options.adapter}, max ${maxLoops} iterations, ${maxRetries} retries, ${maxFailures} max failures${restartInfo}, max-tasks=${maxTasksInfo}${taskScopeInfo})`);
268
891
  if (options.focus) {
269
892
  info(`Focus: ${options.focus}`);
270
893
  }
271
- // Initialize kspec context
272
- const ctx = await initContext();
273
894
  const specDir = ctx.specDir;
274
895
  // Create session for event tracking
275
896
  const sessionId = ulid();
@@ -281,53 +902,181 @@ export function registerRalphCommand(program) {
281
902
  // Log session start
282
903
  await appendEvent(specDir, {
283
904
  session_id: sessionId,
284
- type: 'session.start',
905
+ type: "session.start",
285
906
  data: {
286
907
  adapter: options.adapter,
287
908
  maxLoops,
288
909
  maxRetries,
289
910
  maxFailures,
911
+ maxTasks,
290
912
  yolo: options.yolo,
291
913
  focus: options.focus,
914
+ explicitTasks: explicitTaskScope?.refs,
292
915
  },
293
916
  });
294
917
  let consecutiveFailures = 0;
295
918
  let agent = null;
296
919
  let acpSessionId = null;
920
+ // AC: @ralph-end-loop ac-signal-cleanup, @ralph-task-limit ac-signal-cleanup
921
+ // Signal handlers for cleanup on Ctrl+C or kill
922
+ // Note: Signal handlers must be synchronous, so we use Promise.finally()
923
+ // to ensure cleanup completes before exit
924
+ const signalCleanup = (signal) => {
925
+ info(`Received ${signal}, cleaning up...`);
926
+ // Kill agent if running
927
+ if (agent) {
928
+ agent.kill();
929
+ }
930
+ // Clean up marker files, then exit after cleanup completes
931
+ Promise.all([
932
+ clearTaskLimitMarker(ctx.rootDir),
933
+ clearEndLoopMarker(ctx.rootDir),
934
+ ]).finally(() => {
935
+ process.exit(0);
936
+ });
937
+ };
938
+ const sigintHandler = () => { signalCleanup("SIGINT"); };
939
+ const sigtermHandler = () => { signalCleanup("SIGTERM"); };
940
+ process.on("SIGINT", sigintHandler);
941
+ process.on("SIGTERM", sigtermHandler);
297
942
  // Create translator and renderer for this session
298
943
  const translator = createTranslator();
299
944
  const renderer = createCliRenderer();
945
+ // Task limit state - tracks completions per iteration
946
+ // AC: @ralph-task-limit ac-reset, ac-wrapup
947
+ let taskLimitReached = false;
948
+ let tasksCompletedThisIteration = 0;
949
+ // End-loop signal state
950
+ // AC: @ralph-end-loop ac-detect, ac-graceful
951
+ let endLoopRequested = false;
952
+ // AC: @ralph-wrap-up-agent-on-loop-exit ac-1 - Track exit reason for wrap-up
953
+ let exitReason = null;
954
+ let lastIterationCtx = null;
955
+ let lastErrorMessage;
956
+ const recentTaskRefs = [];
300
957
  try {
301
958
  for (let iteration = 1; iteration <= maxLoops; iteration++) {
302
959
  renderer.newSection?.(`Iteration ${iteration}/${maxLoops}`);
303
- // Gather fresh context each iteration (only automation-eligible tasks)
304
- // AC: @cli-ralph ac-16
305
- const sessionCtx = await gatherSessionContext(ctx, { limit: '10', eligible: true });
306
- // Check for ready tasks or active tasks
307
- const hasActiveTasks = sessionCtx.active_tasks.length > 0;
308
- const hasReadyTasks = sessionCtx.ready_tasks.length > 0;
960
+ // AC: @ralph-task-limit ac-reset - Reset counter and clear stale markers at iteration start
961
+ taskLimitReached = false;
962
+ tasksCompletedThisIteration = 0;
963
+ const wasStale = await clearStaleMarker(ctx.rootDir);
964
+ if (wasStale) {
965
+ info("Cleared stale task limit marker from previous session");
966
+ }
967
+ // Also clear any marker from previous iteration of this session
968
+ await clearTaskLimitMarker(ctx.rootDir);
969
+ // AC: @ralph-end-loop ac-cleanup - Reset end-loop state
970
+ endLoopRequested = false;
971
+ const wasStaleEndLoop = await clearStaleEndLoopMarker(ctx.rootDir);
972
+ if (wasStaleEndLoop) {
973
+ info("Cleared stale end-loop marker from previous session");
974
+ }
975
+ await clearEndLoopMarker(ctx.rootDir);
976
+ // Gather fresh context each iteration
977
+ // AC: @cli-ralph ac-16 - Only automation-eligible tasks (unless explicit scope)
978
+ // AC: @cli-ralph ac-21 - With explicit task scope, ignore automation eligibility
979
+ let sessionCtx = await gatherSessionContext(ctx, {
980
+ limit: "10",
981
+ eligible: !explicitTaskScope, // Skip eligibility filter if explicit scope
982
+ });
983
+ // AC: @cli-ralph ac-21 - Filter to explicit tasks if scope is set
984
+ if (explicitTaskScope) {
985
+ sessionCtx = filterByExplicitTasks(sessionCtx, explicitTaskScope);
986
+ }
987
+ // AC: @ralph-subagent-spawning ac-8 - Process pending_review tasks BEFORE main iteration
988
+ // This wraps consecutiveFailures in an object so it can be mutated by the helper
989
+ const failureTracker = { count: consecutiveFailures };
990
+ const continueLoop = await processPendingReviewTasks(ctx, adapter, sessionCtx.pending_review_tasks, {
991
+ yolo: options.yolo,
992
+ maxRetries,
993
+ maxFailures,
994
+ cwd: process.cwd(),
995
+ subagentTimeout: subagentTimeout * 60 * 1000,
996
+ }, failureTracker);
997
+ consecutiveFailures = failureTracker.count;
998
+ if (!continueLoop) {
999
+ exitReason = "max_failures";
1000
+ lastIterationCtx = sessionCtx;
1001
+ break;
1002
+ }
1003
+ // AC: @cli-ralph ac-20 - Refresh context after pending_review processing
1004
+ // If pending_review tasks were processed, they may have completed and unblocked
1005
+ // dependent tasks. Re-gather context to detect newly available tasks.
1006
+ let currentCtx = sessionCtx;
1007
+ if (sessionCtx.pending_review_tasks.length > 0) {
1008
+ currentCtx = await gatherSessionContext(ctx, {
1009
+ limit: "10",
1010
+ eligible: !explicitTaskScope,
1011
+ });
1012
+ if (explicitTaskScope) {
1013
+ currentCtx = filterByExplicitTasks(currentCtx, explicitTaskScope);
1014
+ }
1015
+ }
1016
+ // AC: @cli-ralph ac-21 - Check explicit task completion
1017
+ if (explicitTaskScope) {
1018
+ const { done, statuses } = await allExplicitTasksDone(ctx, explicitTaskScope);
1019
+ if (done) {
1020
+ const statusList = Array.from(statuses.entries())
1021
+ .map(([ref, status]) => `${ref}: ${status}`)
1022
+ .join(", ");
1023
+ info(`All explicit tasks completed or blocked (${statusList}). Exiting loop.`);
1024
+ exitReason = "explicit_tasks_done";
1025
+ lastIterationCtx = currentCtx;
1026
+ break;
1027
+ }
1028
+ }
1029
+ // Check for automation-eligible tasks (ready or in_progress)
1030
+ // AC: @cli-ralph ac-19
1031
+ const hasActiveTasks = currentCtx.active_tasks.length > 0;
1032
+ const hasReadyTasks = currentCtx.ready_tasks.length > 0;
309
1033
  if (!hasActiveTasks && !hasReadyTasks) {
310
- info('No active or eligible ready tasks. Exiting loop.');
1034
+ if (explicitTaskScope) {
1035
+ info("No explicit tasks available (ready or in_progress). Exiting loop.");
1036
+ }
1037
+ else {
1038
+ info("No automation-eligible tasks (ready or in_progress). Exiting loop.");
1039
+ }
1040
+ exitReason = "no_tasks";
1041
+ lastIterationCtx = currentCtx;
311
1042
  break;
312
1043
  }
313
- // Build prompt
314
- const prompt = buildPrompt(sessionCtx, iteration, maxLoops, options.focus);
1044
+ // AC: @loop-mode-error-handling - Track tasks in progress for failure handling
1045
+ const tasksInProgressAtStart = sessionCtx.active_tasks;
1046
+ const iterationStartTime = new Date();
1047
+ // Build prompts - task-work first, then reflect
1048
+ // AC: @cli-ralph ac-21 - Include explicit task scope in prompt
1049
+ const taskWorkPrompt = buildTaskWorkPrompt(currentCtx, iteration, maxLoops, sessionId, options.focus, explicitTaskScope);
1050
+ const reflectPrompt = buildReflectPrompt(iteration, maxLoops, sessionId);
1051
+ // AC: @ralph-task-limit ac-dryrun, @cli-ralph ac-21
315
1052
  if (options.dryRun) {
316
- console.log(chalk.yellow('=== DRY RUN - Prompt that would be sent ===\n'));
317
- console.log(prompt);
318
- console.log(chalk.yellow('\n=== END DRY RUN ==='));
1053
+ console.log(chalk.yellow("=== DRY RUN - Configuration ===\n"));
1054
+ console.log(` max-loops: ${maxLoops}`);
1055
+ console.log(` max-tasks: ${maxTasks === 0 ? "unlimited" : maxTasks}`);
1056
+ console.log(` max-retries: ${maxRetries}`);
1057
+ console.log(` max-failures: ${maxFailures}`);
1058
+ console.log(` restart-every: ${restartEvery === 0 ? "never" : restartEvery}`);
1059
+ if (explicitTaskScope) {
1060
+ console.log(` explicit-tasks: ${explicitTaskScope.refs.join(", ")}`);
1061
+ }
1062
+ console.log(chalk.yellow("\n=== Task Work Prompt ===\n"));
1063
+ console.log(taskWorkPrompt);
1064
+ console.log(chalk.yellow("\n=== Reflect Prompt ===\n"));
1065
+ console.log(reflectPrompt);
1066
+ console.log(chalk.yellow("\n=== END DRY RUN ==="));
319
1067
  break;
320
1068
  }
321
- // Log prompt
1069
+ // Log task-work prompt
322
1070
  await appendEvent(specDir, {
323
1071
  session_id: sessionId,
324
- type: 'prompt.sent',
1072
+ type: "prompt.sent",
325
1073
  data: {
326
1074
  iteration,
327
- prompt,
1075
+ phase: "task-work",
1076
+ prompt: taskWorkPrompt,
328
1077
  tasks: {
329
- active: sessionCtx.active_tasks.map(t => t.ref),
330
- ready: sessionCtx.ready_tasks.map(t => t.ref),
1078
+ active: currentCtx.active_tasks.map((t) => t.ref),
1079
+ ready: currentCtx.ready_tasks.map((t) => t.ref),
331
1080
  },
332
1081
  },
333
1082
  });
@@ -341,63 +1090,156 @@ export function registerRalphCommand(program) {
341
1090
  try {
342
1091
  // Spawn agent if not already running
343
1092
  if (!agent) {
344
- info('Spawning ACP agent...');
1093
+ info("Spawning ACP agent...");
345
1094
  agent = await spawnAndInitialize(adapter, {
346
1095
  cwd: process.cwd(),
347
1096
  clientOptions: {
348
1097
  clientInfo: {
349
- name: 'kspec-ralph',
350
- version: '0.1.0',
1098
+ name: "kspec-ralph",
1099
+ version: packageVersion,
1100
+ },
1101
+ methodTimeouts: {
1102
+ "session/prompt": RALPH_PROMPT_TIMEOUT,
1103
+ "session/resume": RALPH_PROMPT_TIMEOUT,
351
1104
  },
352
1105
  },
353
1106
  });
354
1107
  // Set up streaming update handler with translator + renderer
355
- agent.client.on('update', (_sid, update) => {
1108
+ agent.client.on("update", (_sid, update) => {
356
1109
  // Translate ACP event to RalphEvent and render
357
1110
  const event = translator.translate(update);
358
1111
  if (event) {
359
1112
  renderer.render(event);
360
1113
  }
1114
+ // AC: @ralph-task-limit ac-detection, ac-wrapup
1115
+ // Detect task completions for limit enforcement
1116
+ if (maxTasks > 0 && !taskLimitReached) {
1117
+ const bashCmd = extractBashCommand(update);
1118
+ if (bashCmd && detectTaskCompleteCommand(bashCmd)) {
1119
+ // Pattern matched - verify via kspec query
1120
+ getIterationStats(ctx, iterationStartTime)
1121
+ .then(async (stats) => {
1122
+ if (stats.tasks_completed >= maxTasks && !taskLimitReached) {
1123
+ taskLimitReached = true;
1124
+ tasksCompletedThisIteration = stats.tasks_completed;
1125
+ info(`Task limit reached (${stats.tasks_completed}/${maxTasks})`);
1126
+ // AC: @ralph-task-limit ac-marker-format, ac-wrapup
1127
+ // Write marker file for hook enforcement
1128
+ const marker = {
1129
+ active: true,
1130
+ since: iterationStartTime.toISOString(),
1131
+ max: maxTasks,
1132
+ completed: stats.tasks_completed,
1133
+ sessionId,
1134
+ };
1135
+ await writeTaskLimitMarker(ctx.rootDir, marker);
1136
+ // Inject wrap-up message to agent
1137
+ if (agent && acpSessionId) {
1138
+ const wrapUpMsg = `\n\n**TASK LIMIT REACHED** - ${stats.tasks_completed} task(s) completed this iteration (limit: ${maxTasks}).\n\nPlease wrap up your current work and exit cleanly. Do not start new tasks.\n\nCompleted tasks this iteration: ${stats.completed_refs.join(", ")}`;
1139
+ agent.client.prompt({
1140
+ sessionId: acpSessionId,
1141
+ prompt: [{ type: "text", text: wrapUpMsg }],
1142
+ }).catch(() => {
1143
+ // Ignore if message injection fails
1144
+ });
1145
+ }
1146
+ }
1147
+ })
1148
+ .catch(() => {
1149
+ // Ignore query failures - detection is best-effort
1150
+ });
1151
+ }
1152
+ }
1153
+ // AC: @ralph-end-loop ac-detect
1154
+ // Detect explicit end-loop command
1155
+ if (!endLoopRequested) {
1156
+ const bashCmd = extractBashCommand(update);
1157
+ if (bashCmd && detectEndLoopCommand(bashCmd)) {
1158
+ endLoopRequested = true;
1159
+ // Read marker to get reason if present
1160
+ readEndLoopMarker(ctx.rootDir)
1161
+ .then((marker) => {
1162
+ const reason = marker?.reason
1163
+ ? ` (${marker.reason})`
1164
+ : "";
1165
+ info(`End-loop signal received${reason}`);
1166
+ })
1167
+ .catch(() => {
1168
+ info("End-loop signal received");
1169
+ });
1170
+ }
1171
+ }
361
1172
  // Log raw update event (async, non-blocking)
362
1173
  appendEvent(specDir, {
363
1174
  session_id: sessionId,
364
- type: 'session.update',
1175
+ type: "session.update",
365
1176
  data: { iteration, update },
366
1177
  }).catch(() => {
367
1178
  // Ignore logging errors during streaming
368
1179
  });
369
1180
  });
370
1181
  // Set up tool request handler
371
- agent.client.on('request', (reqId, method, params) => {
1182
+ agent.client.on("request", (reqId, method, params) => {
1183
+ // biome-ignore lint/style/noNonNullAssertion: agent is guaranteed to exist when callback is registered
372
1184
  handleRequest(agent.client, reqId, method, params, options.yolo).catch((err) => {
1185
+ // biome-ignore lint/style/noNonNullAssertion: agent is guaranteed to exist when callback is registered
373
1186
  agent.client.respondError(reqId, -32000, err.message);
374
1187
  });
375
1188
  });
376
1189
  }
377
1190
  // Create fresh ACP session per iteration to keep context clean
378
- info('Creating ACP session...');
1191
+ info("Creating ACP session...");
379
1192
  acpSessionId = await agent.client.newSession({
380
1193
  cwd: process.cwd(),
381
1194
  mcpServers: [], // No MCP servers for now
382
1195
  });
383
- info('Sending prompt to agent...');
384
- // Send prompt and wait for completion
385
- const response = await agent.client.prompt({
1196
+ // Phase 1: Task Work
1197
+ info("Sending task-work prompt to agent...");
1198
+ const taskWorkResponse = await agent.client.prompt({
386
1199
  sessionId: acpSessionId,
387
- prompt: [{ type: 'text', text: prompt }],
1200
+ prompt: [{ type: "text", text: taskWorkPrompt }],
388
1201
  });
389
- // Log completion
1202
+ // Log task-work completion
390
1203
  await appendEvent(specDir, {
391
1204
  session_id: sessionId,
392
- type: 'session.update',
1205
+ type: "session.update",
393
1206
  data: {
394
1207
  iteration,
395
- stopReason: response.stopReason,
1208
+ phase: "task-work",
1209
+ stopReason: taskWorkResponse.stopReason,
396
1210
  completed: true,
397
1211
  },
398
1212
  });
399
- // Check stop reason
400
- if (response.stopReason === 'cancelled') {
1213
+ if (taskWorkResponse.stopReason === "cancelled") {
1214
+ throw new Error(errors.usage.agentPromptCancelled);
1215
+ }
1216
+ // Phase 2: Reflect (always sent after task-work completes)
1217
+ info("Sending reflect prompt to agent...");
1218
+ await appendEvent(specDir, {
1219
+ session_id: sessionId,
1220
+ type: "prompt.sent",
1221
+ data: {
1222
+ iteration,
1223
+ phase: "reflect",
1224
+ prompt: reflectPrompt,
1225
+ },
1226
+ });
1227
+ const reflectResponse = await agent.client.prompt({
1228
+ sessionId: acpSessionId,
1229
+ prompt: [{ type: "text", text: reflectPrompt }],
1230
+ });
1231
+ // Log reflect completion
1232
+ await appendEvent(specDir, {
1233
+ session_id: sessionId,
1234
+ type: "session.update",
1235
+ data: {
1236
+ iteration,
1237
+ phase: "reflect",
1238
+ stopReason: reflectResponse.stopReason,
1239
+ completed: true,
1240
+ },
1241
+ });
1242
+ if (reflectResponse.stopReason === "cancelled") {
401
1243
  throw new Error(errors.usage.agentPromptCancelled);
402
1244
  }
403
1245
  succeeded = true;
@@ -420,6 +1262,31 @@ export function registerRalphCommand(program) {
420
1262
  await saveSessionContext(specDir, sessionId, iteration, sessionCtx);
421
1263
  success(`Completed iteration ${iteration}`);
422
1264
  consecutiveFailures = 0;
1265
+ // Track task refs from this iteration for wrap-up context
1266
+ for (const t of sessionCtx.active_tasks) {
1267
+ if (!recentTaskRefs.includes(t.ref)) {
1268
+ recentTaskRefs.push(t.ref);
1269
+ }
1270
+ }
1271
+ lastIterationCtx = sessionCtx;
1272
+ // AC: @ralph-end-loop ac-graceful - Check for end-loop signal
1273
+ if (endLoopRequested) {
1274
+ info("Agent requested end of loop. Exiting gracefully.");
1275
+ exitReason = "end_loop_signal";
1276
+ break;
1277
+ }
1278
+ // Periodic agent restart to prevent OOM
1279
+ // AC: @cli-ralph ac-restart-periodic
1280
+ if (restartEvery > 0 &&
1281
+ iteration % restartEvery === 0 &&
1282
+ iteration < maxLoops) {
1283
+ info(`Restarting agent to prevent memory buildup (every ${restartEvery} iterations)...`);
1284
+ if (agent) {
1285
+ agent.kill();
1286
+ agent = null;
1287
+ acpSessionId = null;
1288
+ }
1289
+ }
423
1290
  }
424
1291
  else {
425
1292
  consecutiveFailures++;
@@ -427,34 +1294,99 @@ export function registerRalphCommand(program) {
427
1294
  if (lastError) {
428
1295
  error(errors.failures.lastError(lastError.message));
429
1296
  }
1297
+ // AC: @loop-mode-error-handling - Track per-task failures
1298
+ const errorDesc = lastError?.message || "Iteration failed after retries";
1299
+ await handleIterationFailure(ctx, tasksInProgressAtStart, iterationStartTime, errorDesc);
430
1300
  if (consecutiveFailures >= maxFailures) {
431
1301
  error(errors.failures.reachedMaxFailures(maxFailures));
1302
+ exitReason = "max_failures";
1303
+ lastErrorMessage = lastError?.message;
1304
+ lastIterationCtx = sessionCtx;
432
1305
  break;
433
1306
  }
434
- info('Continuing to next iteration...');
1307
+ info("Continuing to next iteration...");
435
1308
  }
436
1309
  }
1310
+ // If loop completed all iterations without breaking
1311
+ if (exitReason === null) {
1312
+ exitReason = "max_iterations";
1313
+ }
437
1314
  }
438
1315
  finally {
1316
+ // Remove signal handlers to avoid double cleanup
1317
+ process.off("SIGINT", sigintHandler);
1318
+ process.off("SIGTERM", sigtermHandler);
439
1319
  // Clean up agent
440
1320
  if (agent) {
441
1321
  agent.kill();
1322
+ agent = null;
1323
+ }
1324
+ // AC: @ralph-task-limit ac-reset - Clear marker file when session ends
1325
+ await clearTaskLimitMarker(ctx.rootDir);
1326
+ // AC: @ralph-end-loop ac-cleanup - Clear end-loop marker when session ends
1327
+ await clearEndLoopMarker(ctx.rootDir);
1328
+ // AC: @ralph-wrap-up-agent-on-loop-exit ac-1, ac-2, ac-3, ac-4, ac-5
1329
+ // Spawn wrap-up agent if not dry-run and we have an exit reason
1330
+ if (!options.dryRun && exitReason) {
1331
+ console.log("");
1332
+ console.log(chalk.cyan(`${"═".repeat(60)}`));
1333
+ console.log(chalk.cyan.bold(`${WRAPUP_AGENT_PREFIX} Starting Wrap-Up`));
1334
+ console.log(chalk.cyan(`${"═".repeat(60)}`));
1335
+ console.log("");
1336
+ const inProgressTasks = lastIterationCtx?.active_tasks || [];
1337
+ const pendingReviewTasks = lastIterationCtx?.pending_review_tasks || [];
1338
+ const wrapUpCtx = buildWrapUpContext(exitReason, sessionId, maxLoops, // Use maxLoops as iteration (we're at the end)
1339
+ maxLoops, inProgressTasks, pendingReviewTasks, recentTaskRefs, process.cwd(), lastErrorMessage);
1340
+ info(`Exit reason: ${exitReason}`);
1341
+ info(`Working tree: ${wrapUpCtx.workingTree.clean ? "clean" : "has uncommitted changes"}`);
1342
+ const wrapUpResult = await runWrapUpAgent(adapter, wrapUpCtx, {
1343
+ yolo: options.yolo,
1344
+ cwd: process.cwd(),
1345
+ handleRequest: (client, reqId, method, params) => handleRequest(client, reqId, method, params, options.yolo),
1346
+ }, DEFAULT_WRAPUP_TIMEOUT);
1347
+ // Log wrap-up result
1348
+ await appendEvent(specDir, {
1349
+ session_id: sessionId,
1350
+ type: "session.wrapup",
1351
+ data: {
1352
+ exitReason,
1353
+ result: wrapUpResult,
1354
+ },
1355
+ });
1356
+ if (wrapUpResult.skipped) {
1357
+ info(`${WRAPUP_AGENT_PREFIX} Skipped: ${wrapUpResult.skipReason}`);
1358
+ }
1359
+ else if (wrapUpResult.timedOut) {
1360
+ warn(`${WRAPUP_AGENT_PREFIX} Timed out after ${DEFAULT_WRAPUP_TIMEOUT / 1000}s`);
1361
+ }
1362
+ else if (!wrapUpResult.success) {
1363
+ warn(`${WRAPUP_AGENT_PREFIX} Failed: ${wrapUpResult.error}`);
1364
+ }
1365
+ else {
1366
+ success(`${WRAPUP_AGENT_PREFIX} Completed`);
1367
+ }
1368
+ console.log("");
1369
+ console.log(chalk.cyan(`${"═".repeat(60)}`));
1370
+ console.log(chalk.cyan.bold(`${WRAPUP_AGENT_PREFIX} Wrap-Up Complete`));
1371
+ console.log(chalk.cyan(`${"═".repeat(60)}`));
1372
+ console.log("");
442
1373
  }
443
1374
  // Log session end
444
- const status = consecutiveFailures >= maxFailures ? 'abandoned' : 'completed';
1375
+ const status = consecutiveFailures >= maxFailures ? "abandoned" : "completed";
445
1376
  await appendEvent(specDir, {
446
1377
  session_id: sessionId,
447
- type: 'session.end',
1378
+ type: "session.end",
448
1379
  data: {
449
1380
  status,
450
1381
  consecutiveFailures,
1382
+ exitReason,
451
1383
  },
452
1384
  });
453
1385
  await updateSessionStatus(specDir, sessionId, status);
454
1386
  }
455
- console.log(chalk.green(`\n${''.repeat(60)}`));
456
- success('Ralph loop completed');
457
- console.log(chalk.green(`${''.repeat(60)}\n`));
1387
+ console.log(chalk.green(`\n${"".repeat(60)}`));
1388
+ success("Ralph loop completed");
1389
+ console.log(chalk.green(`${"".repeat(60)}\n`));
458
1390
  }
459
1391
  catch (err) {
460
1392
  error(errors.failures.ralphLoop, err);