@kynetic-ai/spec 0.1.2 → 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 +1097 -169
  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 +131 -19
  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,108 +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 { createRequire } from 'node:module';
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
13
  // Read version from package.json for ACP client info
14
14
  const require = createRequire(import.meta.url);
15
- const { version: packageVersion } = require('../../../package.json');
16
- import { initContext } from '../../parser/index.js';
17
- import { error, info, success } from '../output.js';
18
- import { gatherSessionContext } from './session.js';
19
- import { resolveAdapter, registerAdapter } from '../../agents/index.js';
20
- import { spawnAndInitialize } from '../../agents/spawner.js';
21
- import { createSession, updateSessionStatus, appendEvent, saveSessionContext, } from '../../sessions/index.js';
22
- import { createTranslator, createCliRenderer } from '../../ralph/index.js';
23
- import { errors } from '../../strings/index.js';
24
- import { EXIT_CODES } from '../exit-codes.js';
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
+ }
25
267
  // ─── Prompt Template ─────────────────────────────────────────────────────────
26
- function buildPrompt(sessionCtx, iteration, maxLoops, focus) {
27
- const isFinal = iteration === maxLoops;
28
- 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
+ ? `
29
272
  ## Session Focus (applies to ALL iterations)
30
273
 
31
274
  > **${focus}**
32
275
 
33
276
  Keep this focus in mind throughout your work. It takes priority over default task selection.
34
- ` : '';
35
- 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
36
293
 
37
- You are running as part of a kspec automation loop. This is iteration ${iteration} of ${maxLoops}.
38
- ${focusSection}
294
+ **Session ID:** \`${sessionId}\`
295
+ **Iteration:** ${iteration} of ${maxLoops}
296
+ **Mode:** Automated (no human in the loop)
297
+ ${focusSection}${taskScopeSection}
39
298
 
40
299
  ## Current State
41
300
  \`\`\`json
42
301
  ${JSON.stringify(sessionCtx, null, 2)}
43
302
  \`\`\`
44
303
 
45
- ## Working Procedure
304
+ ## Instructions
46
305
 
47
- 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:
48
307
 
49
- 2. **Start the task** (if not already in_progress):
50
- \`\`\`bash
51
- kspec task start @task-ref
52
- \`\`\`
53
-
54
- 3. **Do the work**:
55
- - Read relevant files to understand the task
56
- - Make changes as needed
57
- - Run tests if applicable
58
- - Document as you go with task notes
308
+ \`\`\`
309
+ /task-work loop
310
+ \`\`\`
59
311
 
60
- 4. **Document progress**:
61
- \`\`\`bash
62
- kspec task note @task-ref "What you did, decisions made, etc."
63
- \`\`\`
312
+ ${modeDescription}
64
313
 
65
- 5. **Submit or checkpoint**:
66
- - If code is DONE (ready for PR):
67
- \`\`\`bash
68
- kspec task submit @task-ref
69
- \`\`\`
70
- - If task is NOT done (WIP):
71
- \`\`\`bash
72
- kspec task note @task-ref "WIP: What's done, what remains..."
73
- \`\`\`
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.
74
316
 
75
- 6. **Commit your work**:
76
- \`\`\`bash
77
- 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
78
328
 
79
- Task: @task-ref"
80
- \`\`\`
329
+ **Session ID:** \`${sessionId}\`
330
+ **Iteration:** ${iteration} of ${maxLoops}
331
+ **Phase:** Post-task reflection
81
332
 
82
- 7. **Reflect on this iteration**:
83
- Think about what you learned, any friction points, or patterns worth remembering.
333
+ ## Instructions
84
334
 
85
- For **systemic patterns** (friction or success worth documenting):
86
- \`\`\`bash
87
- kspec meta observe friction "Description of systemic issue..."
88
- kspec meta observe success "Pattern worth replicating..."
89
- \`\`\`
335
+ Run the reflect skill in loop mode:
90
336
 
91
- For **actionable improvements** (specific ideas that could become tasks):
92
- \`\`\`bash
93
- kspec inbox add "Improvement idea..." --tag reflection
94
- \`\`\`
337
+ \`\`\`
338
+ /reflect loop
339
+ \`\`\`
95
340
 
96
- ## Important Notes
97
- - Stay focused on ONE task per iteration
98
- - The loop continues automatically - don't worry about picking the next task
99
- - kspec tracks state across iterations via task status and notes
100
- - Always commit before the iteration ends
101
- - Always reflect and capture at least one observation
102
- ${isFinal ? `
103
- ## FINAL ITERATION
104
- This is the last iteration of the loop. After completing your work:
105
- 1. Commit any remaining changes
106
- 2. Reflect on the overall session
107
- 3. Capture any final insights as observations
108
- ` : ''}`;
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
+ `;
109
349
  }
110
350
  // ─── Streaming Output ────────────────────────────────────────────────────────
111
351
  // Translator and renderer are created per-session in the action handler.
@@ -121,9 +361,9 @@ This is the last iteration of the loop. After completing your work:
121
361
  function validateAdapter(adapterPackage) {
122
362
  // Use npx --no-install with --version to check if package exists
123
363
  // This checks both global and local node_modules, handles scoped packages
124
- const result = spawnSync('npx', ['--no-install', adapterPackage, '--version'], {
125
- encoding: 'utf-8',
126
- stdio: 'pipe',
364
+ const result = spawnSync("npx", ["--no-install", adapterPackage, "--version"], {
365
+ encoding: "utf-8",
366
+ stdio: "pipe",
127
367
  });
128
368
  if (result.status !== 0) {
129
369
  error(`Adapter package not found: ${adapterPackage}. Install with: npm install -g ${adapterPackage}`);
@@ -136,48 +376,50 @@ function validateAdapter(adapterPackage) {
136
376
  * Implements file operations, terminal commands, and permission handling.
137
377
  */
138
378
  async function handleRequest(client, id, method, params, yolo) {
139
- const p = params;
140
379
  try {
141
380
  switch (method) {
142
- case 'session/request_permission': {
381
+ case "session/request_permission": {
382
+ const p = params;
143
383
  // In yolo mode, auto-approve all permissions
144
384
  // In normal mode, would need to implement permission UI
145
385
  const options = p.options || [];
146
386
  if (yolo) {
147
387
  // Find an "allow" option (prefer allow_always, then allow_once)
148
- const allowOption = options.find(o => o.kind === 'allow_always')
149
- || 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");
150
390
  if (allowOption) {
151
391
  client.respondPermission(id, {
152
- outcome: { outcome: 'selected', optionId: allowOption.optionId },
392
+ outcome: { outcome: "selected", optionId: allowOption.optionId },
153
393
  });
154
394
  }
155
395
  else {
156
396
  // No allow option available - cancel
157
- client.respondPermission(id, { outcome: { outcome: 'cancelled' } });
397
+ client.respondPermission(id, { outcome: { outcome: "cancelled" } });
158
398
  }
159
399
  }
160
400
  else {
161
401
  // TODO: Implement permission prompting
162
- client.respondPermission(id, { outcome: { outcome: 'cancelled' } });
402
+ client.respondPermission(id, { outcome: { outcome: "cancelled" } });
163
403
  }
164
404
  break;
165
405
  }
166
- case 'file/read': {
167
- const filePath = p.path;
168
- const content = await fs.readFile(filePath, 'utf-8');
169
- 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 });
170
410
  break;
171
411
  }
172
- case 'file/write': {
173
- const filePath = p.path;
174
- const content = p.content;
175
- await fs.mkdir(path.dirname(filePath), { recursive: true });
176
- await fs.writeFile(filePath, content, 'utf-8');
177
- 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, {});
178
417
  break;
179
418
  }
180
- 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;
181
423
  const command = p.command;
182
424
  const cwd = p.cwd || process.cwd();
183
425
  const timeout = p.timeout || 60000;
@@ -187,21 +429,22 @@ async function handleRequest(client, id, method, params, yolo) {
187
429
  shell: true,
188
430
  timeout,
189
431
  });
190
- let stdout = '';
191
- let stderr = '';
192
- child.stdout?.on('data', (data) => {
432
+ let stdout = "";
433
+ let stderr = "";
434
+ child.stdout?.on("data", (data) => {
193
435
  stdout += data.toString();
194
436
  });
195
- child.stderr?.on('data', (data) => {
437
+ child.stderr?.on("data", (data) => {
196
438
  stderr += data.toString();
197
439
  });
198
- child.on('close', (code) => {
440
+ child.on("close", (code) => {
199
441
  resolve({ stdout, stderr, exitCode: code ?? 1 });
200
442
  });
201
- child.on('error', (err) => {
443
+ child.on("error", (err) => {
202
444
  resolve({ stdout, stderr: err.message, exitCode: 1 });
203
445
  });
204
446
  });
447
+ // Using generic respond() since this is a custom method
205
448
  client.respond(id, result);
206
449
  break;
207
450
  }
@@ -215,65 +458,439 @@ async function handleRequest(client, id, method, params, yolo) {
215
458
  client.respondError(id, -32000, message);
216
459
  }
217
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
+ }
218
731
  // ─── Command Registration ────────────────────────────────────────────────────
219
732
  export function registerRalphCommand(program) {
220
- program
221
- .command('ralph')
222
- .description('Run ACP agent in a loop to process ready tasks')
223
- .option('--max-loops <n>', 'Maximum iterations', '5')
224
- .option('--max-retries <n>', 'Max retries per iteration on error', '3')
225
- .option('--max-failures <n>', 'Max consecutive failed iterations before exit', '3')
226
- .option('--dry-run', 'Show prompt without executing')
227
- .option('--yolo', 'Use dangerously-skip-permissions (default)', true)
228
- .option('--no-yolo', 'Require normal permission prompts')
229
- .option('--adapter <id>', 'Agent adapter to use', 'claude-code-acp')
230
- .option('--adapter-cmd <cmd>', 'Custom adapter command (for testing)')
231
- .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")
232
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
+ }
233
806
  try {
234
807
  const maxLoops = parseInt(options.maxLoops, 10);
235
808
  const maxRetries = parseInt(options.maxRetries, 10);
236
809
  const maxFailures = parseInt(options.maxFailures, 10);
237
- if (isNaN(maxLoops) || maxLoops < 1) {
810
+ if (Number.isNaN(maxLoops) || maxLoops < 1) {
238
811
  error(errors.usage.maxLoopsPositive);
239
812
  process.exit(EXIT_CODES.ERROR);
240
813
  }
241
- if (isNaN(maxRetries) || maxRetries < 0) {
814
+ if (Number.isNaN(maxRetries) || maxRetries < 0) {
242
815
  error(errors.usage.maxRetriesNonNegative);
243
816
  process.exit(EXIT_CODES.ERROR);
244
817
  }
245
- if (isNaN(maxFailures) || maxFailures < 1) {
818
+ if (Number.isNaN(maxFailures) || maxFailures < 1) {
246
819
  error(errors.usage.maxFailuresPositive);
247
820
  process.exit(EXIT_CODES.ERROR);
248
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
+ }
249
838
  // Handle custom adapter command for testing
250
839
  if (options.adapterCmd) {
251
840
  const parts = options.adapterCmd.split(/\s+/);
252
841
  const customAdapter = {
253
842
  command: parts[0],
254
843
  args: parts.slice(1),
255
- description: 'Custom adapter via --adapter-cmd',
844
+ description: "Custom adapter via --adapter-cmd",
256
845
  };
257
- registerAdapter('custom', customAdapter);
258
- options.adapter = 'custom';
846
+ registerAdapter("custom", customAdapter);
847
+ options.adapter = "custom";
259
848
  }
260
849
  // Resolve adapter
261
850
  const adapter = resolveAdapter(options.adapter);
262
851
  // Validate adapter package exists before proceeding
263
- // Skip validation for custom adapters (--adapter-cmd) and non-npx adapters
264
- 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) {
265
865
  validateAdapter(adapter.args[0]);
266
866
  }
267
- // Add yolo flag to adapter args if needed
268
- if (options.yolo && options.adapter === 'claude-code-acp') {
269
- 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
+ }
270
886
  }
271
- 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})`);
272
891
  if (options.focus) {
273
892
  info(`Focus: ${options.focus}`);
274
893
  }
275
- // Initialize kspec context
276
- const ctx = await initContext();
277
894
  const specDir = ctx.specDir;
278
895
  // Create session for event tracking
279
896
  const sessionId = ulid();
@@ -285,53 +902,181 @@ export function registerRalphCommand(program) {
285
902
  // Log session start
286
903
  await appendEvent(specDir, {
287
904
  session_id: sessionId,
288
- type: 'session.start',
905
+ type: "session.start",
289
906
  data: {
290
907
  adapter: options.adapter,
291
908
  maxLoops,
292
909
  maxRetries,
293
910
  maxFailures,
911
+ maxTasks,
294
912
  yolo: options.yolo,
295
913
  focus: options.focus,
914
+ explicitTasks: explicitTaskScope?.refs,
296
915
  },
297
916
  });
298
917
  let consecutiveFailures = 0;
299
918
  let agent = null;
300
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);
301
942
  // Create translator and renderer for this session
302
943
  const translator = createTranslator();
303
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 = [];
304
957
  try {
305
958
  for (let iteration = 1; iteration <= maxLoops; iteration++) {
306
959
  renderer.newSection?.(`Iteration ${iteration}/${maxLoops}`);
307
- // Gather fresh context each iteration (only automation-eligible tasks)
308
- // AC: @cli-ralph ac-16
309
- const sessionCtx = await gatherSessionContext(ctx, { limit: '10', eligible: true });
310
- // Check for ready tasks or active tasks
311
- const hasActiveTasks = sessionCtx.active_tasks.length > 0;
312
- 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;
313
1033
  if (!hasActiveTasks && !hasReadyTasks) {
314
- 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;
315
1042
  break;
316
1043
  }
317
- // Build prompt
318
- 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
319
1052
  if (options.dryRun) {
320
- console.log(chalk.yellow('=== DRY RUN - Prompt that would be sent ===\n'));
321
- console.log(prompt);
322
- 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 ==="));
323
1067
  break;
324
1068
  }
325
- // Log prompt
1069
+ // Log task-work prompt
326
1070
  await appendEvent(specDir, {
327
1071
  session_id: sessionId,
328
- type: 'prompt.sent',
1072
+ type: "prompt.sent",
329
1073
  data: {
330
1074
  iteration,
331
- prompt,
1075
+ phase: "task-work",
1076
+ prompt: taskWorkPrompt,
332
1077
  tasks: {
333
- active: sessionCtx.active_tasks.map(t => t.ref),
334
- 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),
335
1080
  },
336
1081
  },
337
1082
  });
@@ -345,63 +1090,156 @@ export function registerRalphCommand(program) {
345
1090
  try {
346
1091
  // Spawn agent if not already running
347
1092
  if (!agent) {
348
- info('Spawning ACP agent...');
1093
+ info("Spawning ACP agent...");
349
1094
  agent = await spawnAndInitialize(adapter, {
350
1095
  cwd: process.cwd(),
351
1096
  clientOptions: {
352
1097
  clientInfo: {
353
- name: 'kspec-ralph',
1098
+ name: "kspec-ralph",
354
1099
  version: packageVersion,
355
1100
  },
1101
+ methodTimeouts: {
1102
+ "session/prompt": RALPH_PROMPT_TIMEOUT,
1103
+ "session/resume": RALPH_PROMPT_TIMEOUT,
1104
+ },
356
1105
  },
357
1106
  });
358
1107
  // Set up streaming update handler with translator + renderer
359
- agent.client.on('update', (_sid, update) => {
1108
+ agent.client.on("update", (_sid, update) => {
360
1109
  // Translate ACP event to RalphEvent and render
361
1110
  const event = translator.translate(update);
362
1111
  if (event) {
363
1112
  renderer.render(event);
364
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
+ }
365
1172
  // Log raw update event (async, non-blocking)
366
1173
  appendEvent(specDir, {
367
1174
  session_id: sessionId,
368
- type: 'session.update',
1175
+ type: "session.update",
369
1176
  data: { iteration, update },
370
1177
  }).catch(() => {
371
1178
  // Ignore logging errors during streaming
372
1179
  });
373
1180
  });
374
1181
  // Set up tool request handler
375
- 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
376
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
377
1186
  agent.client.respondError(reqId, -32000, err.message);
378
1187
  });
379
1188
  });
380
1189
  }
381
1190
  // Create fresh ACP session per iteration to keep context clean
382
- info('Creating ACP session...');
1191
+ info("Creating ACP session...");
383
1192
  acpSessionId = await agent.client.newSession({
384
1193
  cwd: process.cwd(),
385
1194
  mcpServers: [], // No MCP servers for now
386
1195
  });
387
- info('Sending prompt to agent...');
388
- // Send prompt and wait for completion
389
- 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({
390
1199
  sessionId: acpSessionId,
391
- prompt: [{ type: 'text', text: prompt }],
1200
+ prompt: [{ type: "text", text: taskWorkPrompt }],
392
1201
  });
393
- // Log completion
1202
+ // Log task-work completion
394
1203
  await appendEvent(specDir, {
395
1204
  session_id: sessionId,
396
- type: 'session.update',
1205
+ type: "session.update",
397
1206
  data: {
398
1207
  iteration,
399
- stopReason: response.stopReason,
1208
+ phase: "task-work",
1209
+ stopReason: taskWorkResponse.stopReason,
400
1210
  completed: true,
401
1211
  },
402
1212
  });
403
- // Check stop reason
404
- 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") {
405
1243
  throw new Error(errors.usage.agentPromptCancelled);
406
1244
  }
407
1245
  succeeded = true;
@@ -424,6 +1262,31 @@ export function registerRalphCommand(program) {
424
1262
  await saveSessionContext(specDir, sessionId, iteration, sessionCtx);
425
1263
  success(`Completed iteration ${iteration}`);
426
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
+ }
427
1290
  }
428
1291
  else {
429
1292
  consecutiveFailures++;
@@ -431,34 +1294,99 @@ export function registerRalphCommand(program) {
431
1294
  if (lastError) {
432
1295
  error(errors.failures.lastError(lastError.message));
433
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);
434
1300
  if (consecutiveFailures >= maxFailures) {
435
1301
  error(errors.failures.reachedMaxFailures(maxFailures));
1302
+ exitReason = "max_failures";
1303
+ lastErrorMessage = lastError?.message;
1304
+ lastIterationCtx = sessionCtx;
436
1305
  break;
437
1306
  }
438
- info('Continuing to next iteration...');
1307
+ info("Continuing to next iteration...");
439
1308
  }
440
1309
  }
1310
+ // If loop completed all iterations without breaking
1311
+ if (exitReason === null) {
1312
+ exitReason = "max_iterations";
1313
+ }
441
1314
  }
442
1315
  finally {
1316
+ // Remove signal handlers to avoid double cleanup
1317
+ process.off("SIGINT", sigintHandler);
1318
+ process.off("SIGTERM", sigtermHandler);
443
1319
  // Clean up agent
444
1320
  if (agent) {
445
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("");
446
1373
  }
447
1374
  // Log session end
448
- const status = consecutiveFailures >= maxFailures ? 'abandoned' : 'completed';
1375
+ const status = consecutiveFailures >= maxFailures ? "abandoned" : "completed";
449
1376
  await appendEvent(specDir, {
450
1377
  session_id: sessionId,
451
- type: 'session.end',
1378
+ type: "session.end",
452
1379
  data: {
453
1380
  status,
454
1381
  consecutiveFailures,
1382
+ exitReason,
455
1383
  },
456
1384
  });
457
1385
  await updateSessionStatus(specDir, sessionId, status);
458
1386
  }
459
- console.log(chalk.green(`\n${''.repeat(60)}`));
460
- success('Ralph loop completed');
461
- 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`));
462
1390
  }
463
1391
  catch (err) {
464
1392
  error(errors.failures.ralphLoop, err);