@kynetic-ai/spec 0.9.1 → 0.11.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 (310) hide show
  1. package/README.md +2 -1
  2. package/dist/acp/client.d.ts +6 -1
  3. package/dist/acp/client.d.ts.map +1 -1
  4. package/dist/acp/client.js +7 -2
  5. package/dist/acp/client.js.map +1 -1
  6. package/dist/acp/framing.d.ts +12 -1
  7. package/dist/acp/framing.d.ts.map +1 -1
  8. package/dist/acp/framing.js +27 -4
  9. package/dist/acp/framing.js.map +1 -1
  10. package/dist/agent-runtime/dispatch.d.ts +292 -0
  11. package/dist/agent-runtime/dispatch.d.ts.map +1 -0
  12. package/dist/agent-runtime/dispatch.js +860 -0
  13. package/dist/agent-runtime/dispatch.js.map +1 -0
  14. package/dist/agent-runtime/index.d.ts +11 -0
  15. package/dist/agent-runtime/index.d.ts.map +1 -0
  16. package/dist/agent-runtime/index.js +11 -0
  17. package/dist/agent-runtime/index.js.map +1 -0
  18. package/dist/agent-runtime/invocation.d.ts +86 -0
  19. package/dist/agent-runtime/invocation.d.ts.map +1 -0
  20. package/dist/agent-runtime/invocation.js +442 -0
  21. package/dist/agent-runtime/invocation.js.map +1 -0
  22. package/dist/agent-runtime/prompts.d.ts +50 -0
  23. package/dist/agent-runtime/prompts.d.ts.map +1 -0
  24. package/dist/agent-runtime/prompts.js +108 -0
  25. package/dist/agent-runtime/prompts.js.map +1 -0
  26. package/dist/agents/spawner.d.ts.map +1 -1
  27. package/dist/agents/spawner.js +60 -4
  28. package/dist/agents/spawner.js.map +1 -1
  29. package/dist/cli/batch-exec.d.ts.map +1 -1
  30. package/dist/cli/batch-exec.js +140 -62
  31. package/dist/cli/batch-exec.js.map +1 -1
  32. package/dist/cli/batch-write-buffer.d.ts +141 -0
  33. package/dist/cli/batch-write-buffer.d.ts.map +1 -0
  34. package/dist/cli/batch-write-buffer.js +400 -0
  35. package/dist/cli/batch-write-buffer.js.map +1 -0
  36. package/dist/cli/commands/agent.d.ts +20 -0
  37. package/dist/cli/commands/agent.d.ts.map +1 -0
  38. package/dist/cli/commands/agent.js +831 -0
  39. package/dist/cli/commands/agent.js.map +1 -0
  40. package/dist/cli/commands/inbox.d.ts.map +1 -1
  41. package/dist/cli/commands/inbox.js +46 -22
  42. package/dist/cli/commands/inbox.js.map +1 -1
  43. package/dist/cli/commands/index.d.ts +1 -0
  44. package/dist/cli/commands/index.d.ts.map +1 -1
  45. package/dist/cli/commands/index.js +1 -0
  46. package/dist/cli/commands/index.js.map +1 -1
  47. package/dist/cli/commands/item.d.ts.map +1 -1
  48. package/dist/cli/commands/item.js +22 -16
  49. package/dist/cli/commands/item.js.map +1 -1
  50. package/dist/cli/commands/log.js +1 -1
  51. package/dist/cli/commands/log.js.map +1 -1
  52. package/dist/cli/commands/meta.d.ts.map +1 -1
  53. package/dist/cli/commands/meta.js +168 -6
  54. package/dist/cli/commands/meta.js.map +1 -1
  55. package/dist/cli/commands/module.d.ts.map +1 -1
  56. package/dist/cli/commands/module.js +2 -1
  57. package/dist/cli/commands/module.js.map +1 -1
  58. package/dist/cli/commands/plan-import.js +19 -3
  59. package/dist/cli/commands/plan-import.js.map +1 -1
  60. package/dist/cli/commands/plan.d.ts.map +1 -1
  61. package/dist/cli/commands/plan.js +87 -43
  62. package/dist/cli/commands/plan.js.map +1 -1
  63. package/dist/cli/commands/ralph.d.ts +5 -56
  64. package/dist/cli/commands/ralph.d.ts.map +1 -1
  65. package/dist/cli/commands/ralph.js +52 -1502
  66. package/dist/cli/commands/ralph.js.map +1 -1
  67. package/dist/cli/commands/search.d.ts.map +1 -1
  68. package/dist/cli/commands/search.js +22 -13
  69. package/dist/cli/commands/search.js.map +1 -1
  70. package/dist/cli/commands/serve.d.ts.map +1 -1
  71. package/dist/cli/commands/serve.js +70 -11
  72. package/dist/cli/commands/serve.js.map +1 -1
  73. package/dist/cli/commands/session/checkpoint.d.ts.map +1 -1
  74. package/dist/cli/commands/session/checkpoint.js +7 -2
  75. package/dist/cli/commands/session/checkpoint.js.map +1 -1
  76. package/dist/cli/commands/session/commands.d.ts.map +1 -1
  77. package/dist/cli/commands/session/commands.js +15 -0
  78. package/dist/cli/commands/session/commands.js.map +1 -1
  79. package/dist/cli/commands/session/context.d.ts.map +1 -1
  80. package/dist/cli/commands/session/context.js +10 -5
  81. package/dist/cli/commands/session/context.js.map +1 -1
  82. package/dist/cli/commands/session/log.d.ts +1 -0
  83. package/dist/cli/commands/session/log.d.ts.map +1 -1
  84. package/dist/cli/commands/session/log.js +124 -8
  85. package/dist/cli/commands/session/log.js.map +1 -1
  86. package/dist/cli/commands/session/stale-close.d.ts +17 -0
  87. package/dist/cli/commands/session/stale-close.d.ts.map +1 -0
  88. package/dist/cli/commands/session/stale-close.js +378 -0
  89. package/dist/cli/commands/session/stale-close.js.map +1 -0
  90. package/dist/cli/commands/setup.d.ts.map +1 -1
  91. package/dist/cli/commands/setup.js +95 -0
  92. package/dist/cli/commands/setup.js.map +1 -1
  93. package/dist/cli/commands/skill-crud.d.ts.map +1 -1
  94. package/dist/cli/commands/skill-crud.js +4 -3
  95. package/dist/cli/commands/skill-crud.js.map +1 -1
  96. package/dist/cli/commands/skill-diff.d.ts.map +1 -1
  97. package/dist/cli/commands/skill-diff.js +15 -0
  98. package/dist/cli/commands/skill-diff.js.map +1 -1
  99. package/dist/cli/commands/skill-install.d.ts.map +1 -1
  100. package/dist/cli/commands/skill-install.js +50 -18
  101. package/dist/cli/commands/skill-install.js.map +1 -1
  102. package/dist/cli/commands/task.d.ts.map +1 -1
  103. package/dist/cli/commands/task.js +536 -310
  104. package/dist/cli/commands/task.js.map +1 -1
  105. package/dist/cli/commands/tasks.js +1 -1
  106. package/dist/cli/commands/tasks.js.map +1 -1
  107. package/dist/cli/commands/triage.d.ts.map +1 -1
  108. package/dist/cli/commands/triage.js +37 -13
  109. package/dist/cli/commands/triage.js.map +1 -1
  110. package/dist/cli/commands/validate.d.ts.map +1 -1
  111. package/dist/cli/commands/validate.js +65 -25
  112. package/dist/cli/commands/validate.js.map +1 -1
  113. package/dist/cli/help/content.d.ts.map +1 -1
  114. package/dist/cli/help/content.js +5 -0
  115. package/dist/cli/help/content.js.map +1 -1
  116. package/dist/cli/index.d.ts.map +1 -1
  117. package/dist/cli/index.js +2 -1
  118. package/dist/cli/index.js.map +1 -1
  119. package/dist/cli/output.d.ts.map +1 -1
  120. package/dist/cli/output.js +5 -1
  121. package/dist/cli/output.js.map +1 -1
  122. package/dist/daemon/project-context.ts +22 -0
  123. package/dist/daemon/routes/agent-dispatch.ts +279 -0
  124. package/dist/daemon/routes/items.ts +22 -0
  125. package/dist/daemon/routes/meta.ts +141 -1
  126. package/dist/daemon/routes/plans.ts +147 -0
  127. package/dist/daemon/routes/sessions.ts +180 -0
  128. package/dist/daemon/routes/tasks.ts +198 -0
  129. package/dist/daemon/routes/validation.ts +1 -1
  130. package/dist/daemon/server.ts +77 -21
  131. package/dist/daemon/websocket/handler.ts +67 -6
  132. package/dist/daemon/websocket/lifecycle.ts +19 -0
  133. package/dist/daemon/websocket/pubsub.ts +74 -3
  134. package/dist/export/html.d.ts.map +1 -1
  135. package/dist/export/html.js +5 -2
  136. package/dist/export/html.js.map +1 -1
  137. package/dist/export/triage.d.ts +1 -1
  138. package/dist/export/triage.d.ts.map +1 -1
  139. package/dist/export/triage.js +5 -3
  140. package/dist/export/triage.js.map +1 -1
  141. package/dist/parser/alignment.d.ts.map +1 -1
  142. package/dist/parser/alignment.js +10 -5
  143. package/dist/parser/alignment.js.map +1 -1
  144. package/dist/parser/assess.js +1 -1
  145. package/dist/parser/assess.js.map +1 -1
  146. package/dist/parser/config.d.ts +6 -6
  147. package/dist/parser/meta.d.ts.map +1 -1
  148. package/dist/parser/meta.js +9 -8
  149. package/dist/parser/meta.js.map +1 -1
  150. package/dist/parser/plan-document.d.ts +12 -12
  151. package/dist/parser/plans.d.ts +7 -0
  152. package/dist/parser/plans.d.ts.map +1 -1
  153. package/dist/parser/plans.js +100 -15
  154. package/dist/parser/plans.js.map +1 -1
  155. package/dist/parser/refs.d.ts +5 -0
  156. package/dist/parser/refs.d.ts.map +1 -1
  157. package/dist/parser/refs.js +17 -12
  158. package/dist/parser/refs.js.map +1 -1
  159. package/dist/parser/shadow.d.ts +1 -1
  160. package/dist/parser/shadow.d.ts.map +1 -1
  161. package/dist/parser/shadow.js +71 -4
  162. package/dist/parser/shadow.js.map +1 -1
  163. package/dist/parser/skill-render.d.ts.map +1 -1
  164. package/dist/parser/skill-render.js +6 -3
  165. package/dist/parser/skill-render.js.map +1 -1
  166. package/dist/parser/validate.d.ts.map +1 -1
  167. package/dist/parser/validate.js +35 -76
  168. package/dist/parser/validate.js.map +1 -1
  169. package/dist/parser/yaml.d.ts +24 -5
  170. package/dist/parser/yaml.d.ts.map +1 -1
  171. package/dist/parser/yaml.js +224 -64
  172. package/dist/parser/yaml.js.map +1 -1
  173. package/dist/schema/meta.d.ts +457 -119
  174. package/dist/schema/meta.d.ts.map +1 -1
  175. package/dist/schema/meta.js +56 -0
  176. package/dist/schema/meta.js.map +1 -1
  177. package/dist/schema/plan.d.ts +22 -22
  178. package/dist/schema/spec.d.ts +39 -39
  179. package/dist/schema/task.d.ts +43 -32
  180. package/dist/schema/task.d.ts.map +1 -1
  181. package/dist/schema/task.js +5 -0
  182. package/dist/schema/task.js.map +1 -1
  183. package/dist/sessions/store.d.ts +126 -0
  184. package/dist/sessions/store.d.ts.map +1 -1
  185. package/dist/sessions/store.js +440 -22
  186. package/dist/sessions/store.js.map +1 -1
  187. package/dist/sessions/types.d.ts +75 -17
  188. package/dist/sessions/types.d.ts.map +1 -1
  189. package/dist/sessions/types.js +51 -1
  190. package/dist/sessions/types.js.map +1 -1
  191. package/dist/triage/actions.d.ts +1 -0
  192. package/dist/triage/actions.d.ts.map +1 -1
  193. package/dist/triage/actions.js +34 -7
  194. package/dist/triage/actions.js.map +1 -1
  195. package/dist/utils/commit.js +1 -1
  196. package/dist/utils/commit.js.map +1 -1
  197. package/dist/web-ui/_app/env.js +1 -0
  198. package/dist/web-ui/_app/immutable/assets/0.BJaYkGW2.css +1 -0
  199. package/dist/web-ui/_app/immutable/assets/9.SzGLxi4x.css +1 -0
  200. package/dist/web-ui/_app/immutable/assets/select-trigger.CV-KWLNP.css +1 -0
  201. package/dist/web-ui/_app/immutable/chunks/-lc0BifF.js +1 -0
  202. package/dist/web-ui/_app/immutable/chunks/62JVKtnb.js +1 -0
  203. package/dist/web-ui/_app/immutable/chunks/8RBjHMN1.js +1 -0
  204. package/dist/web-ui/_app/immutable/chunks/B5LJFxqa.js +1 -0
  205. package/dist/web-ui/_app/immutable/chunks/B5wTVqxm.js +1 -0
  206. package/dist/web-ui/_app/immutable/chunks/B6VSmczZ.js +1 -0
  207. package/dist/web-ui/_app/immutable/chunks/B8a0xDxR.js +1 -0
  208. package/dist/web-ui/_app/immutable/chunks/BEOQc37C.js +1 -0
  209. package/dist/web-ui/_app/immutable/chunks/BHtYorjv.js +1 -0
  210. package/dist/web-ui/_app/immutable/chunks/BJ0JX3ea.js +1 -0
  211. package/dist/web-ui/_app/immutable/chunks/BMuCqDX8.js +1 -0
  212. package/dist/web-ui/_app/immutable/chunks/BP352uRn.js +1 -0
  213. package/dist/web-ui/_app/immutable/chunks/BUZujXJ2.js +1 -0
  214. package/dist/web-ui/_app/immutable/chunks/BVA9Exy-.js +1 -0
  215. package/dist/web-ui/_app/immutable/chunks/BWET-efb.js +1 -0
  216. package/dist/web-ui/_app/immutable/chunks/BXkNecpt.js +1 -0
  217. package/dist/web-ui/_app/immutable/chunks/BYzrIfX8.js +1 -0
  218. package/dist/web-ui/_app/immutable/chunks/BkOJ8DkV.js +1 -0
  219. package/dist/web-ui/_app/immutable/chunks/BpuwufMc.js +1 -0
  220. package/dist/web-ui/_app/immutable/chunks/BwMO4RrG.js +1 -0
  221. package/dist/web-ui/_app/immutable/chunks/BysXJlZb.js +1 -0
  222. package/dist/web-ui/_app/immutable/chunks/C076q4JN.js +1 -0
  223. package/dist/web-ui/_app/immutable/chunks/C33JaVbg.js +1 -0
  224. package/dist/web-ui/_app/immutable/chunks/CGtqifKp.js +1 -0
  225. package/dist/web-ui/_app/immutable/chunks/CHDZZ7OG.js +1 -0
  226. package/dist/web-ui/_app/immutable/chunks/CPPfDSei.js +5 -0
  227. package/dist/web-ui/_app/immutable/chunks/CUir3f4J.js +60 -0
  228. package/dist/web-ui/_app/immutable/chunks/Cncwi6fQ.js +1 -0
  229. package/dist/web-ui/_app/immutable/chunks/CrCIbn0C.js +1 -0
  230. package/dist/web-ui/_app/immutable/chunks/CwELQvbx.js +1 -0
  231. package/dist/web-ui/_app/immutable/chunks/D3vxvonu.js +1 -0
  232. package/dist/web-ui/_app/immutable/chunks/D6TVmR9T.js +1 -0
  233. package/dist/web-ui/_app/immutable/chunks/D7LTux4W.js +1 -0
  234. package/dist/web-ui/_app/immutable/chunks/D82RulSH.js +1 -0
  235. package/dist/web-ui/_app/immutable/chunks/D9QNBZM2.js +2 -0
  236. package/dist/web-ui/_app/immutable/chunks/DAMmvwn4.js +1 -0
  237. package/dist/web-ui/_app/immutable/chunks/DAh4Wfku.js +1 -0
  238. package/dist/web-ui/_app/immutable/chunks/DAx07bEQ.js +1 -0
  239. package/dist/web-ui/_app/immutable/chunks/DBYE9jOd.js +1 -0
  240. package/dist/web-ui/_app/immutable/chunks/DOno4cA2.js +1 -0
  241. package/dist/web-ui/_app/immutable/chunks/DQA8NZIH.js +2 -0
  242. package/dist/web-ui/_app/immutable/chunks/DRfPm2bo.js +1 -0
  243. package/dist/web-ui/_app/immutable/chunks/DhQhksaB.js +1 -0
  244. package/dist/web-ui/_app/immutable/chunks/DjG7s6hm.js +1 -0
  245. package/dist/web-ui/_app/immutable/chunks/DjcCz-PU.js +2 -0
  246. package/dist/web-ui/_app/immutable/chunks/DkltRNvh.js +1 -0
  247. package/dist/web-ui/_app/immutable/chunks/DlaTnPKL.js +1 -0
  248. package/dist/web-ui/_app/immutable/chunks/DvA-KON-.js +1 -0
  249. package/dist/web-ui/_app/immutable/chunks/DxCk-KHc.js +1 -0
  250. package/dist/web-ui/_app/immutable/chunks/DzO4hlg9.js +1 -0
  251. package/dist/web-ui/_app/immutable/chunks/Eo4gF7ih.js +1 -0
  252. package/dist/web-ui/_app/immutable/chunks/ExCq5swK.js +1 -0
  253. package/dist/web-ui/_app/immutable/chunks/T3zZGv51.js +1 -0
  254. package/dist/web-ui/_app/immutable/chunks/XZumBYeP.js +1 -0
  255. package/dist/web-ui/_app/immutable/chunks/_ySfNjkF.js +1 -0
  256. package/dist/web-ui/_app/immutable/chunks/iEtR5cV6.js +1 -0
  257. package/dist/web-ui/_app/immutable/chunks/k_Qegko0.js +1 -0
  258. package/dist/web-ui/_app/immutable/chunks/pE6cYWlS.js +1 -0
  259. package/dist/web-ui/_app/immutable/entry/app.Cgu6uKeS.js +2 -0
  260. package/dist/web-ui/_app/immutable/entry/start.9XifnLoB.js +1 -0
  261. package/dist/web-ui/_app/immutable/nodes/0.DISwcKSK.js +1 -0
  262. package/dist/web-ui/_app/immutable/nodes/1.Cx2Ufqp1.js +1 -0
  263. package/dist/web-ui/_app/immutable/nodes/10.C3z8ijXL.js +1 -0
  264. package/dist/web-ui/_app/immutable/nodes/11.DZdIjZmM.js +1 -0
  265. package/dist/web-ui/_app/immutable/nodes/12.FsIGfAOa.js +1 -0
  266. package/dist/web-ui/_app/immutable/nodes/13.DZoFwagf.js +1 -0
  267. package/dist/web-ui/_app/immutable/nodes/14.DaIzDKbQ.js +1 -0
  268. package/dist/web-ui/_app/immutable/nodes/15.BYyt4XWF.js +2 -0
  269. package/dist/web-ui/_app/immutable/nodes/16.CQkSqpOe.js +1 -0
  270. package/dist/web-ui/_app/immutable/nodes/2.Bkf_j2UJ.js +1 -0
  271. package/dist/web-ui/_app/immutable/nodes/3.kaMCurJG.js +1 -0
  272. package/dist/web-ui/_app/immutable/nodes/4.BSsFPTHG.js +2 -0
  273. package/dist/web-ui/_app/immutable/nodes/5.CpPlcCEZ.js +1 -0
  274. package/dist/web-ui/_app/immutable/nodes/6.BN4FqQmY.js +1 -0
  275. package/dist/web-ui/_app/immutable/nodes/7.9kBYIZik.js +1 -0
  276. package/dist/web-ui/_app/immutable/nodes/8.BuijtZ6B.js +1 -0
  277. package/dist/web-ui/_app/immutable/nodes/9.C-Weba8R.js +1 -0
  278. package/dist/web-ui/_app/version.json +1 -0
  279. package/dist/web-ui/index.html +39 -0
  280. package/dist/web-ui/robots.txt +3 -0
  281. package/package.json +4 -2
  282. package/plugin/.claude-plugin/marketplace.json +1 -1
  283. package/plugin/.claude-plugin/plugin.json +1 -1
  284. package/plugin/plugins/kspec/skills/task-work/SKILL.md +25 -2
  285. package/templates/agents-sections/06-ralph-loop.md +64 -11
  286. package/templates/skills/task-work/SKILL.md +25 -2
  287. package/dist/ralph/cli-renderer.d.ts +0 -27
  288. package/dist/ralph/cli-renderer.d.ts.map +0 -1
  289. package/dist/ralph/cli-renderer.js +0 -250
  290. package/dist/ralph/cli-renderer.js.map +0 -1
  291. package/dist/ralph/events.d.ts +0 -65
  292. package/dist/ralph/events.d.ts.map +0 -1
  293. package/dist/ralph/events.js +0 -600
  294. package/dist/ralph/events.js.map +0 -1
  295. package/dist/ralph/index.d.ts +0 -11
  296. package/dist/ralph/index.d.ts.map +0 -1
  297. package/dist/ralph/index.js +0 -16
  298. package/dist/ralph/index.js.map +0 -1
  299. package/dist/ralph/loop-errors.d.ts +0 -83
  300. package/dist/ralph/loop-errors.d.ts.map +0 -1
  301. package/dist/ralph/loop-errors.js +0 -150
  302. package/dist/ralph/loop-errors.js.map +0 -1
  303. package/dist/ralph/subagent.d.ts +0 -127
  304. package/dist/ralph/subagent.d.ts.map +0 -1
  305. package/dist/ralph/subagent.js +0 -268
  306. package/dist/ralph/subagent.js.map +0 -1
  307. package/dist/ralph/wrap-up.d.ts +0 -127
  308. package/dist/ralph/wrap-up.d.ts.map +0 -1
  309. package/dist/ralph/wrap-up.js +0 -271
  310. package/dist/ralph/wrap-up.js.map +0 -1
@@ -1,1521 +1,71 @@
1
1
  /**
2
- * Ralph command - automated task loop via ACP.
2
+ * Ralph command - deprecated.
3
3
  *
4
- * Runs an ACP-compliant agent in a loop to process tasks autonomously.
5
- * Uses session event storage for full audit trail and streaming output.
4
+ * Ralph has been replaced by `kspec agent`. This stub provides a helpful
5
+ * migration error message when users run `kspec ralph`.
6
+ *
7
+ * AC: @ralph-replacement ac-1
6
8
  */
7
- import { spawn, spawnSync } from "node:child_process";
8
- import { createWriteStream } from "node:fs";
9
- import * as fs from "node:fs/promises";
10
- import { createRequire } from "node:module";
11
- import * as path from "node:path";
12
- import { fileURLToPath } from "node:url";
13
9
  import chalk from "chalk";
14
- import { ulid } from "ulid";
15
- // Read version from package.json for ACP client info
16
- const require = createRequire(import.meta.url);
17
- const { version: packageVersion } = require("../../../package.json");
18
- const __filename = fileURLToPath(import.meta.url);
19
- const __dirname = path.dirname(__filename);
20
- const DEFAULT_KSPEC_CLI_PATH = path.resolve(__dirname, "..", "index.js");
21
- import { registerAdapter, resolveAdapter, } from "../../agents/index.js";
22
- import { spawnAndInitialize } from "../../agents/spawner.js";
23
- import { initContext, loadAllItems, loadMetaContext, loadAllTasks, ReferenceIndex, } from "../../parser/index.js";
24
- import { resolveSkillReferenceTokensForPlatform } from "../../parser/skill-render.js";
25
- import { buildWrapUpContext, createCliRenderer, createTranslator, DEFAULT_SUBAGENT_PREFIX, DEFAULT_WRAPUP_TIMEOUT, formatJsonSection, RALPH_PROMPT_TIMEOUT, runSubagent, runWrapUpAgent, truncatePromptIfNeeded, WRAPUP_AGENT_PREFIX, } from "../../ralph/index.js";
26
- import { appendEvent, closeSession, createSessionWithBudget, getSessionBudgetPath, getSessionDir, injectEnvForAdapter, isEndLoopRequested, removeEnvForAdapter, requestEndLoop, resetBudget, saveSessionContext, } from "../../sessions/index.js";
27
- import { errors } from "../../strings/index.js";
28
- import { getCurrentBranch } from "../../utils/git.js";
29
10
  import { EXIT_CODES } from "../exit-codes.js";
30
- import { error, info, success, warn } from "../output.js";
31
- import { gatherSessionContext, } from "./session.js";
32
- /**
33
- * Parse and validate --tasks flag value.
34
- * Returns resolved ULIDs for the specified task refs.
35
- * AC: @cli-ralph ac-21
36
- *
37
- * @throws Error if any ref cannot be resolved or is not a task
38
- */
39
- async function parseExplicitTasks(ctx, tasksArg) {
40
- const refs = tasksArg.split(",").map((r) => r.trim()).filter(Boolean);
41
- if (refs.length === 0) {
42
- throw new Error("--tasks requires at least one task reference");
43
- }
44
- // Load tasks and items for resolution
45
- const tasks = await loadAllTasks(ctx);
46
- const items = await loadAllItems(ctx);
47
- const index = new ReferenceIndex(tasks, items);
48
- const ulids = [];
49
- for (const ref of refs) {
50
- const result = index.resolve(ref);
51
- if (!result.ok) {
52
- throw new Error(`Cannot resolve task reference: ${ref}`);
53
- }
54
- // Verify it's a task (not a spec item)
55
- const task = tasks.find((t) => t._ulid === result.ulid);
56
- if (!task) {
57
- throw new Error(`Reference ${ref} is not a task`);
58
- }
59
- ulids.push(result.ulid);
60
- }
61
- return { refs, ulids };
62
- }
63
- /**
64
- * Filter session context to only include tasks from explicit scope.
65
- * AC: @cli-ralph ac-21
66
- */
67
- function filterByExplicitTasks(ctx, scope) {
68
- // Task refs in context are short ULIDs (variable length from shortUlid())
69
- // Check if the context ref is a prefix of any explicit ULID
70
- const matchesScope = (taskRef) => {
71
- return scope.ulids.some((ulid) => ulid.startsWith(taskRef));
72
- };
73
- return {
74
- ...ctx,
75
- active_tasks: ctx.active_tasks.filter((t) => matchesScope(t.ref)),
76
- pending_review_tasks: ctx.pending_review_tasks.filter((t) => matchesScope(t.ref)),
77
- ready_tasks: ctx.ready_tasks.filter((t) => matchesScope(t.ref)),
78
- };
79
- }
80
- /**
81
- * Check if all explicit tasks are completed or blocked.
82
- * AC: @cli-ralph ac-21
83
- */
84
- async function allExplicitTasksDone(ctx, scope) {
85
- const tasks = await loadAllTasks(ctx);
86
- const statuses = new Map();
87
- for (const ulid of scope.ulids) {
88
- const task = tasks.find((t) => t._ulid === ulid);
89
- if (task) {
90
- statuses.set(ulid.slice(0, 8), task.status);
91
- }
92
- }
93
- // Check if all are completed or blocked
94
- const done = scope.ulids.every((ulid) => {
95
- const status = statuses.get(ulid.slice(0, 8));
96
- return status === "completed" || status === "blocked";
97
- });
98
- return { done, statuses };
99
- }
100
- const FALLBACK_CORE_SKILLS = new Set(["task-work", "reflect", "review"]);
101
- const ADAPTER_VALIDATION_PROBES = [["--help"], ["--version"]];
102
- const TERMINAL_PREVIEW_MAX_BYTES = 64 * 1024;
103
- const TOOL_OUTPUT_DIR = "tool-output";
104
- const RECENT_TASK_REF_LIMIT = 50;
105
- export function pushRecentTaskRef(recentTaskRefs, ref, limit = RECENT_TASK_REF_LIMIT) {
106
- const existingIndex = recentTaskRefs.indexOf(ref);
107
- if (existingIndex !== -1) {
108
- recentTaskRefs.splice(existingIndex, 1);
109
- }
110
- recentTaskRefs.push(ref);
111
- if (recentTaskRefs.length > limit) {
112
- recentTaskRefs.splice(0, recentTaskRefs.length - limit);
113
- }
114
- }
115
- export function setSessionIteration(sessionIterationMap, sessionId, iteration) {
116
- sessionIterationMap.clear();
117
- sessionIterationMap.set(sessionId, iteration);
118
- }
119
- export function disposeSpawnedAgent(spawned) {
120
- if (!spawned) {
121
- return null;
122
- }
123
- // Remove callbacks first so captured loop state can be garbage-collected.
124
- spawned.client.removeAllListeners();
125
- spawned.kill();
126
- if (!spawned.client.isClosed()) {
127
- spawned.client.close();
128
- }
129
- return null;
130
- }
131
- /**
132
- * Map adapter IDs to prompt rendering platforms.
133
- */
134
- export function getPromptPlatformForAdapter(adapterId) {
135
- switch (adapterId) {
136
- case "claude-agent-acp":
137
- case "claude-code-acp":
138
- return "claude-code";
139
- case "codex-acp":
140
- return "codex";
141
- default:
142
- return "unknown";
143
- }
144
- }
145
- /**
146
- * Build skill origin map from meta skills.
147
- */
148
- async function loadSkillOriginsForRalph(ctx) {
149
- const meta = await loadMetaContext(ctx);
150
- const origins = new Map();
151
- for (const skill of meta.skills) {
152
- origins.set(skill.id, skill.origin);
153
- }
154
- // Fallback for core skills frequently used by ralph, even if core skills
155
- // were not loaded into project meta for any reason.
156
- for (const coreSkill of FALLBACK_CORE_SKILLS) {
157
- if (!origins.has(coreSkill)) {
158
- origins.set(coreSkill, "core");
159
- }
160
- }
161
- return origins;
162
- }
163
- /**
164
- * Normalize legacy literal invocation syntax for a target platform.
165
- * Keeps backward compatibility for existing slash-style config values.
166
- */
167
- function normalizeLegacyInvocation(invocation, platform) {
168
- if (platform === "codex") {
169
- if (/^\/kspec:([a-z0-9][a-z0-9-]*)$/.test(invocation)) {
170
- return invocation.replace(/^\/kspec:([a-z0-9][a-z0-9-]*)$/, (_m, skillId) => `$kspec-${skillId}`);
171
- }
172
- if (/^\/([a-z0-9][a-z0-9-]*)$/.test(invocation)) {
173
- return invocation.replace(/^\/([a-z0-9][a-z0-9-]*)$/, (_m, skillId) => `$${skillId}`);
174
- }
175
- }
176
- if (platform === "claude-code") {
177
- if (/^\$kspec-([a-z0-9][a-z0-9-]*)$/.test(invocation)) {
178
- return invocation.replace(/^\$kspec-([a-z0-9][a-z0-9-]*)$/, (_m, skillId) => `/kspec:${skillId}`);
179
- }
180
- if (/^\$([a-z0-9][a-z0-9-]*)$/.test(invocation)) {
181
- return invocation.replace(/^\$([a-z0-9][a-z0-9-]*)$/, (_m, skillId) => `/${skillId}`);
182
- }
183
- }
184
- return invocation;
185
- }
186
- /**
187
- * Resolve configured skill invocation string for a specific platform.
188
- * Supports portable {skill:<id>} syntax and legacy literal strings.
189
- */
190
- export function resolveRalphSkillInvocation(invocation, platform, skillOrigins) {
191
- if (platform === "unknown") {
192
- return invocation;
193
- }
194
- const tokenResolved = resolveSkillReferenceTokensForPlatform(invocation, platform, skillOrigins);
195
- if (tokenResolved !== invocation) {
196
- return tokenResolved;
197
- }
198
- return normalizeLegacyInvocation(invocation, platform);
199
- }
200
- // AC: @ralph-skill-delegation ac-1, ac-2, ac-3
201
- function buildTaskWorkPrompt(sessionCtx, iteration, maxLoops, sessionId, skillTaskWork, focus, explicitTaskScope) {
202
- const focusSection = focus
203
- ? `
204
- ## Session Focus (applies to ALL iterations)
205
-
206
- > **${focus}**
207
-
208
- Keep this focus in mind throughout your work. It takes priority over default task selection.
209
- `
210
- : "";
211
- // AC: @cli-ralph ac-21 - Explicit task scope indicator in prompt
212
- const taskScopeSection = explicitTaskScope
213
- ? `
214
- ## Explicit Task Scope
215
-
216
- This session is scoped to specific tasks: ${explicitTaskScope.refs.join(", ")}
217
-
218
- **Only work on these tasks.** The loop will exit when all listed tasks are completed or blocked.
219
- `
220
- : "";
221
- const modeDescription = explicitTaskScope
222
- ? "Loop mode means: no confirmations, auto-resolve decisions, explicit task scope (only the listed tasks)."
223
- : "Loop mode means: no confirmations, auto-resolve decisions, automation-eligible tasks only.";
224
- const stateSection = formatJsonSection(sessionCtx, "Current State", `kspec session start --json`);
225
- const sections = [stateSection.section];
226
- const prompt = `# Kspec Automation Session - Task Work
227
-
228
- **Session ID:** \`${sessionId}\`
229
- **Iteration:** ${iteration} of ${maxLoops}
230
- **Mode:** Automated (no human in the loop)
231
- ${focusSection}${taskScopeSection}
232
-
233
- ${stateSection.text}
234
-
235
- ## Instructions
236
-
237
- Run the task-work skill in loop mode:
238
-
239
- \`\`\`
240
- ${skillTaskWork} loop
241
- \`\`\`
242
-
243
- ${modeDescription}
244
-
245
- **Normal flow:** Work on a task, create a PR, then stop responding. Ralph continues automatically —
246
- it checks for remaining eligible tasks at the start of each iteration and exits the loop itself when none remain.
247
-
248
- **Do NOT call \`end-loop\` after completing a task.** Simply stop responding.
249
- \`end-loop\` is a rare escape hatch for when work is stalling across multiple iterations with no progress — not a normal exit path.
250
- `;
251
- return truncatePromptIfNeeded(prompt, sections);
252
- }
253
- /**
254
- * Build the reflect prompt sent after task-work completes.
255
- * Ralph sends this as a separate prompt to ensure reflection always happens.
256
- */
257
- function buildReflectPrompt(iteration, maxLoops, sessionId, skillReflect) {
258
- const isFinal = iteration === maxLoops;
259
- return `# Kspec Automation Session - Reflection
260
-
261
- **Session ID:** \`${sessionId}\`
262
- **Iteration:** ${iteration} of ${maxLoops}
263
- **Phase:** Post-task reflection
264
-
265
- ## Instructions
266
-
267
- Run the reflect skill in loop mode:
268
-
269
- \`\`\`
270
- ${skillReflect} loop
271
- \`\`\`
272
-
273
- Loop mode means: high-confidence captures only, must search existing before capturing, no user prompts.
274
- ${isFinal
275
- ? `
276
- **FINAL ITERATION** - This is the last chance to capture insights from this session.
277
- `
278
- : ""}
279
- Exit when reflection is complete.
280
- `;
281
- }
282
- /**
283
- * Check whether an adapter package appears to be installed and executable.
284
- * Uses multiple non-installing probes because CLIs differ on supported flags.
285
- */
286
- export function isAdapterPackageAvailable(adapterPackage, runner = spawnSync) {
287
- for (const probeArgs of ADAPTER_VALIDATION_PROBES) {
288
- const result = runner("npx", ["--no-install", adapterPackage, ...probeArgs], {
289
- encoding: "utf-8",
290
- stdio: "pipe",
291
- });
292
- if (result.status === 0) {
293
- return true;
294
- }
295
- }
296
- return false;
297
- }
298
- /**
299
- * Validate that the specified ACP adapter package exists.
300
- * Uses npx --no-install probes to check both global and local node_modules.
301
- *
302
- * @throws {Error} Never throws - exits process with code 3 if validation fails
303
- */
304
- function validateAdapter(adapterPackage, adapterId) {
305
- if (!isAdapterPackageAvailable(adapterPackage)) {
306
- const label = adapterId && adapterId !== adapterPackage
307
- ? `${adapterId} (${adapterPackage})`
308
- : adapterPackage;
309
- error(`Adapter not found: ${label}. Install with: npm install -g ${adapterPackage}`);
310
- process.exit(EXIT_CODES.NOT_FOUND);
311
- }
312
- }
313
- function sanitizeToolCallId(toolCallId) {
314
- const raw = String(toolCallId).trim();
315
- if (!raw) {
316
- return "tool-call";
317
- }
318
- return raw.replace(/[^a-zA-Z0-9._-]/g, "_");
319
- }
320
- function updateStreamPreview(state, chunk, maxPreviewBytes) {
321
- state.bytes += chunk.length;
322
- const remaining = maxPreviewBytes - state.previewBytes;
323
- if (remaining <= 0) {
324
- state.truncated = true;
325
- return;
326
- }
327
- if (chunk.length > remaining) {
328
- state.previewParts.push(chunk.subarray(0, remaining).toString("utf-8"));
329
- state.previewBytes += remaining;
330
- state.truncated = true;
331
- return;
332
- }
333
- state.previewParts.push(chunk.toString("utf-8"));
334
- state.previewBytes += chunk.length;
335
- }
336
- function closeStream(stream) {
337
- if (!stream) {
338
- return Promise.resolve();
339
- }
340
- return new Promise((resolve, reject) => {
341
- const onError = (err) => {
342
- stream.off("finish", onFinish);
343
- reject(err);
344
- };
345
- const onFinish = () => {
346
- stream.off("error", onError);
347
- resolve();
348
- };
349
- stream.once("error", onError);
350
- stream.once("finish", onFinish);
351
- stream.end();
352
- });
353
- }
354
- /**
355
- * Execute terminal/run request with bounded in-memory preview and streamed
356
- * session artifacts for full stdout/stderr retention.
357
- */
358
- export async function runTerminalCommandWithArtifacts(options) {
359
- const previewMaxBytes = options.previewMaxBytes ?? TERMINAL_PREVIEW_MAX_BYTES;
360
- const shouldWriteArtifacts = Boolean(options.specDir && options.sessionId);
361
- let stdoutPath;
362
- let stderrPath;
363
- if (shouldWriteArtifacts) {
364
- const outputDir = path.join(getSessionDir(options.specDir, options.sessionId), TOOL_OUTPUT_DIR);
365
- await fs.mkdir(outputDir, { recursive: true });
366
- const safeToolCallId = sanitizeToolCallId(options.toolCallId);
367
- stdoutPath = path.join(outputDir, `${safeToolCallId}.stdout.log`);
368
- stderrPath = path.join(outputDir, `${safeToolCallId}.stderr.log`);
369
- }
370
- const stdoutState = {
371
- bytes: 0,
372
- previewBytes: 0,
373
- previewParts: [],
374
- truncated: false,
375
- stream: stdoutPath ? createWriteStream(stdoutPath) : undefined,
376
- };
377
- const stderrState = {
378
- bytes: 0,
379
- previewBytes: 0,
380
- previewParts: [],
381
- truncated: false,
382
- stream: stderrPath ? createWriteStream(stderrPath) : undefined,
383
- };
384
- return await new Promise((resolve, reject) => {
385
- let settled = false;
386
- const child = spawn(options.command, [], {
387
- cwd: options.cwd,
388
- shell: true,
389
- timeout: options.timeout,
390
- });
391
- const finalize = async (exitCode, errorMessage) => {
392
- if (settled) {
393
- return;
394
- }
395
- settled = true;
396
- if (errorMessage) {
397
- const errChunk = Buffer.from(errorMessage, "utf-8");
398
- stderrState.stream?.write(errChunk);
399
- updateStreamPreview(stderrState, errChunk, previewMaxBytes);
400
- }
401
- try {
402
- await Promise.all([
403
- closeStream(stdoutState.stream),
404
- closeStream(stderrState.stream),
405
- ]);
406
- }
407
- catch (streamErr) {
408
- reject(streamErr);
409
- return;
410
- }
411
- resolve({
412
- stdout: stdoutState.previewParts.join(""),
413
- stderr: stderrState.previewParts.join(""),
414
- exitCode,
415
- stdout_path: stdoutPath,
416
- stderr_path: stderrPath,
417
- stdout_bytes: stdoutState.bytes,
418
- stderr_bytes: stderrState.bytes,
419
- preview_truncated: stdoutState.truncated || stderrState.truncated,
420
- });
421
- };
422
- child.stdout?.on("data", (data) => {
423
- const chunk = Buffer.isBuffer(data)
424
- ? data
425
- : Buffer.from(String(data), "utf-8");
426
- stdoutState.stream?.write(chunk);
427
- updateStreamPreview(stdoutState, chunk, previewMaxBytes);
428
- });
429
- child.stderr?.on("data", (data) => {
430
- const chunk = Buffer.isBuffer(data)
431
- ? data
432
- : Buffer.from(String(data), "utf-8");
433
- stderrState.stream?.write(chunk);
434
- updateStreamPreview(stderrState, chunk, previewMaxBytes);
435
- });
436
- child.on("close", (code) => {
437
- void finalize(code ?? 1);
438
- });
439
- child.on("error", (err) => {
440
- void finalize(1, err.message);
441
- });
442
- });
443
- }
444
- // ─── Tool Request Handler ────────────────────────────────────────────────────
445
- /**
446
- * Handle tool requests from ACP agent.
447
- * Implements file operations, terminal commands, and permission handling.
448
- */
449
- async function handleRequest(client, id, method, params, options) {
450
- try {
451
- switch (method) {
452
- case "session/request_permission": {
453
- const p = params;
454
- // In yolo mode, auto-approve all permissions
455
- // In normal mode, would need to implement permission UI
456
- const permissionOptions = p.options || [];
457
- if (options.yolo) {
458
- // Find an "allow" option (prefer allow_always, then allow_once)
459
- const allowOption = permissionOptions.find((o) => o.kind === "allow_always") ||
460
- permissionOptions.find((o) => o.kind === "allow_once");
461
- if (allowOption) {
462
- client.respondPermission(id, {
463
- outcome: { outcome: "selected", optionId: allowOption.optionId },
464
- });
465
- }
466
- else {
467
- // No allow option available - cancel
468
- client.respondPermission(id, { outcome: { outcome: "cancelled" } });
469
- }
470
- }
471
- else {
472
- // TODO: Implement permission prompting
473
- client.respondPermission(id, { outcome: { outcome: "cancelled" } });
474
- }
475
- break;
476
- }
477
- case "file/read": {
478
- const p = params;
479
- const content = await fs.readFile(p.path, "utf-8");
480
- client.respondReadTextFile(id, { content });
481
- break;
482
- }
483
- case "file/write": {
484
- const p = params;
485
- await fs.mkdir(path.dirname(p.path), { recursive: true });
486
- await fs.writeFile(p.path, p.content, "utf-8");
487
- client.respondWriteTextFile(id, {});
488
- break;
489
- }
490
- case "terminal/run": {
491
- // Custom method (not part of ACP spec - ACP uses createTerminal instead)
492
- // TODO: Consider migrating to standard ACP terminal methods
493
- const p = params;
494
- const command = p.command;
495
- const cwd = p.cwd || process.cwd();
496
- const timeout = p.timeout || 60000;
497
- const result = await runTerminalCommandWithArtifacts({
498
- command,
499
- cwd,
500
- timeout,
501
- toolCallId: id,
502
- specDir: options.specDir,
503
- sessionId: options.sessionId,
504
- });
505
- // Using generic respond() since this is a custom method
506
- client.respond(id, result);
507
- break;
508
- }
509
- default:
510
- // Unknown method - return error
511
- client.respondError(id, -32601, `Method not found: ${method}`);
512
- }
513
- }
514
- catch (err) {
515
- const message = err instanceof Error ? err.message : String(err);
516
- client.respondError(id, -32000, message);
517
- }
518
- }
519
- // ─── Subagent Support ─────────────────────────────────────────────────────────
520
- /**
521
- * Build context for a PR review subagent.
522
- * AC: @ralph-subagent-spawning ac-10
523
- */
524
- async function buildSubagentContext(ctx, taskRef) {
525
- // Load all tasks and items
526
- const tasks = await loadAllTasks(ctx);
527
- const items = await loadAllItems(ctx);
528
- const index = new ReferenceIndex(tasks, items);
529
- // Resolve task reference
530
- const taskResult = index.resolve(taskRef);
531
- if (!taskResult.ok) {
532
- throw new Error(`Task not found: ${taskRef}`);
533
- }
534
- const task = tasks.find((t) => t._ulid === taskResult.ulid);
535
- if (!task) {
536
- throw new Error(`Task not found by ULID: ${taskResult.ulid}`);
537
- }
538
- // Get linked spec with ACs if spec_ref exists
539
- let specWithACs = null;
540
- if (task.spec_ref) {
541
- const specResult = index.resolve(task.spec_ref);
542
- if (specResult.ok) {
543
- const item = items.find((i) => i._ulid === specResult.ulid);
544
- if (item) {
545
- specWithACs = item;
546
- }
547
- }
548
- }
549
- // Get git branch
550
- const gitBranch = getCurrentBranch(ctx.rootDir) || "unknown";
551
- return {
552
- taskRef,
553
- taskDetails: task,
554
- specWithACs,
555
- gitBranch,
556
- };
557
- }
558
- /**
559
- * Get the current status of a task.
560
- * AC: @ralph-subagent-spawning ac-12
561
- */
562
- function runKspecCli(args) {
563
- const cliPath = process.env.KSPEC_CLI_PATH || DEFAULT_KSPEC_CLI_PATH;
564
- return spawnSync(process.execPath, [cliPath, ...args], {
565
- encoding: "utf-8",
566
- stdio: "pipe",
567
- cwd: process.cwd(),
568
- });
569
- }
570
- function getTaskStatus(taskRef) {
571
- const result = runKspecCli(["task", "get", taskRef, "--json"]);
572
- if (result.status !== 0) {
573
- warn(`Failed to check task status for ${taskRef}: ${result.stderr}`);
574
- return null;
575
- }
576
- try {
577
- return JSON.parse(result.stdout).status;
578
- }
579
- catch {
580
- warn(`Failed to parse task status for ${taskRef}`);
581
- return null;
582
- }
583
- }
584
- /**
585
- * Mark a task as needing review due to subagent timeout.
586
- * AC: @ralph-subagent-spawning ac-9
587
- */
588
- async function markTaskNeedsReview(taskRef, reason) {
589
- // Use kspec CLI to set automation status
590
- const result = runKspecCli(["task", "set", taskRef, "--automation", "needs_review", "--reason", reason]);
591
- if (result.status !== 0) {
592
- warn(`Failed to mark task ${taskRef} as needs_review: ${result.stderr}`);
593
- }
594
- // Add a note explaining the timeout
595
- const noteResult = runKspecCli(["task", "note", taskRef, `[RALPH SUBAGENT] ${reason}`]);
596
- if (noteResult.status !== 0) {
597
- warn(`Failed to add timeout note to task ${taskRef}: ${noteResult.stderr}`);
598
- }
599
- }
600
- /**
601
- * Post a comment on the open PR for a task's branch, noting incomplete review.
602
- * Uses `gh pr list --head <branch>` to find the PR and add a warning.
603
- */
604
- async function commentOnPRReviewIncomplete(branch, reason) {
605
- if (!branch || branch === "unknown") {
606
- return;
607
- }
608
- const prListResult = spawnSync("gh", ["pr", "list", "--state", "open", "--head", branch, "--json", "number", "--jq", ".[0].number"], { encoding: "utf-8", stdio: "pipe" });
609
- const prNumber = prListResult.stdout?.trim();
610
- if (!prNumber || prListResult.status !== 0) {
611
- // No open PR found — may already be merged or branch has no PR
612
- return;
613
- }
614
- const body = `⚠️ **Review incomplete**: ${reason}\n\nThis PR was not fully reviewed by the ralph review subagent. Manual review recommended before merging.`;
615
- const commentResult = spawnSync("gh", ["pr", "comment", prNumber, "--body", body], { encoding: "utf-8", stdio: "pipe" });
616
- if (commentResult.status !== 0) {
617
- warn(`Failed to comment on PR #${prNumber}: ${commentResult.stderr}`);
618
- }
619
- else {
620
- info(`${DEFAULT_SUBAGENT_PREFIX} Posted review-incomplete comment on PR #${prNumber}`);
621
- }
622
- }
623
- /**
624
- * Handle failed iteration by tracking per-task failures and escalating at threshold.
625
- * AC: @loop-mode-error-handling ac-1, ac-2, ac-3, ac-4, ac-5, ac-8
626
- */
627
- async function handleIterationFailure(ctx, tasksInProgressAtStart, iterationStartTime, errorDescription) {
628
- if (tasksInProgressAtStart.length === 0) {
629
- return;
630
- }
631
- // Re-load current tasks to check progress
632
- const currentTasks = await loadAllTasks(ctx);
633
- const index = new ReferenceIndex(currentTasks, await loadAllItems(ctx));
634
- // Convert ActiveTaskSummary to Task-like objects for processing
635
- const tasksInProgressFull = tasksInProgressAtStart
636
- .map((summary) => {
637
- const resolved = index.resolve(summary.ref);
638
- if (!resolved.ok)
639
- return undefined;
640
- // Check if the resolved item is a task (not a spec item or meta item)
641
- const item = resolved.item;
642
- if (!("status" in item))
643
- return undefined; // Spec items don't have status
644
- return currentTasks.find((t) => t._ulid === resolved.ulid);
645
- })
646
- .filter((t) => t !== undefined && t.status === "in_progress");
647
- if (tasksInProgressFull.length === 0) {
648
- return;
649
- }
650
- // Process failures
651
- const { processFailedIteration, createFailureNote, getTaskFailureCount } = await import("../../ralph/index.js");
652
- const results = processFailedIteration(tasksInProgressFull, currentTasks, iterationStartTime, errorDescription);
653
- // Add notes and escalate tasks
654
- for (const result of results) {
655
- const taskRef = result.taskRef;
656
- const task = currentTasks.find((t) => t._ulid === taskRef);
657
- if (!task)
658
- continue;
659
- const priorCount = result.failureCount - 1;
660
- const noteContent = createFailureNote(taskRef, errorDescription, priorCount);
661
- // Add LOOP-FAIL note
662
- const noteResult = spawnSync("kspec", ["task", "note", `@${taskRef}`, noteContent], {
663
- encoding: "utf-8",
664
- stdio: "pipe",
665
- cwd: process.cwd(),
666
- });
667
- if (noteResult.status !== 0) {
668
- warn(`Failed to add failure note to task ${taskRef}: ${noteResult.stderr}`);
669
- continue;
670
- }
671
- // AC: @loop-mode-error-handling ac-5 - Escalate at threshold
672
- if (result.escalated) {
673
- const escalateResult = spawnSync("kspec", [
674
- "task",
675
- "set",
676
- `@${taskRef}`,
677
- "--automation",
678
- "needs_review",
679
- "--reason",
680
- `Loop mode: 3 consecutive failures without progress`,
681
- ], {
682
- encoding: "utf-8",
683
- stdio: "pipe",
684
- cwd: process.cwd(),
685
- });
686
- if (escalateResult.status !== 0) {
687
- warn(`Failed to escalate task ${taskRef}: ${escalateResult.stderr}`);
688
- }
689
- else {
690
- info(`Escalated task ${taskRef} to automation:needs_review after 3 failures`);
691
- }
692
- }
693
- }
694
- }
695
- /**
696
- * Process pending_review tasks by spawning subagents.
697
- * AC: @ralph-subagent-spawning ac-6, ac-8
698
- */
699
- async function processPendingReviewTasks(ctx, adapter, pendingReviewTasks, options, consecutiveFailures) {
700
- if (pendingReviewTasks.length === 0) {
701
- return true;
702
- }
703
- // Visual separator for subagent section
704
- console.log("");
705
- console.log(chalk.cyan(`${"═".repeat(60)}`));
706
- console.log(chalk.cyan.bold(`${DEFAULT_SUBAGENT_PREFIX} Processing Pending Review Tasks`));
707
- console.log(chalk.cyan(`${"═".repeat(60)}`));
708
- console.log("");
709
- info(`${DEFAULT_SUBAGENT_PREFIX} Found ${pendingReviewTasks.length} pending_review task(s)`);
710
- // AC: @ralph-subagent-spawning ac-6 - Process one at a time
711
- for (const task of pendingReviewTasks) {
712
- info(`${DEFAULT_SUBAGENT_PREFIX} Processing: ${task.ref} - ${task.title}`);
713
- try {
714
- // Build context for this task
715
- const subagentCtx = await buildSubagentContext(ctx, task.ref);
716
- // AC: @ralph-subagent-spawning ac-1, ac-3 - Spawn and wait
717
- const result = await runSubagent(adapter, subagentCtx, {
718
- timeout: options.subagentTimeout,
719
- outputPrefix: DEFAULT_SUBAGENT_PREFIX,
720
- skillName: options.prReviewSkillName,
721
- }, {
722
- yolo: options.yolo,
723
- cwd: options.cwd,
724
- extraArgs: options.autoApproveArgs,
725
- handleRequest: (client, reqId, method, params) => handleRequest(client, reqId, method, params, {
726
- yolo: options.yolo,
727
- specDir: options.specDir,
728
- sessionId: options.sessionId,
729
- }),
730
- });
731
- if (result.timedOut) {
732
- // AC: @ralph-subagent-spawning ac-9
733
- warn(`${DEFAULT_SUBAGENT_PREFIX} Subagent timed out for ${task.ref}`);
734
- const timeoutMinutes = Math.round(options.subagentTimeout / 60000);
735
- await markTaskNeedsReview(task.ref, `Subagent timed out after ${timeoutMinutes} minutes`);
736
- await commentOnPRReviewIncomplete(subagentCtx.gitBranch, `Review subagent timed out after ${timeoutMinutes} minutes for task ${task.ref}.`);
737
- consecutiveFailures.count++;
738
- }
739
- else if (!result.success) {
740
- // AC: @ralph-subagent-spawning ac-7
741
- error(`${DEFAULT_SUBAGENT_PREFIX} Subagent failed for ${task.ref}: ${result.error}`);
742
- await commentOnPRReviewIncomplete(subagentCtx.gitBranch, `Review subagent failed for task ${task.ref}: ${result.error}`);
743
- consecutiveFailures.count++;
744
- }
745
- else {
746
- // AC: @ralph-subagent-spawning ac-12 - Verify task outcome
747
- const currentStatus = getTaskStatus(task.ref);
748
- if (currentStatus === "completed") {
749
- success(`${DEFAULT_SUBAGENT_PREFIX} Completed: ${task.ref}`);
750
- consecutiveFailures.count = 0;
751
- }
752
- else if (currentStatus === "needs_work") {
753
- // Expected: reviewer found issues, kicked back to worker
754
- info(`${DEFAULT_SUBAGENT_PREFIX} Review completed for ${task.ref} — issues found, kicked back to worker`);
755
- // NOT a failure — the review worked correctly
756
- consecutiveFailures.count = 0;
757
- }
758
- else if (currentStatus === "pending_review") {
759
- // Subagent didn't transition or merge — count as soft failure
760
- warn(`${DEFAULT_SUBAGENT_PREFIX} Subagent completed but task ${task.ref} unchanged`);
761
- await markTaskNeedsReview(task.ref, "Subagent completed but did not merge or kick back. Review required.");
762
- consecutiveFailures.count++;
763
- }
764
- else {
765
- warn(`${DEFAULT_SUBAGENT_PREFIX} Task ${task.ref} in unexpected state: ${currentStatus}`);
766
- consecutiveFailures.count++;
767
- }
768
- }
769
- // Check if we've hit max failures
770
- if (consecutiveFailures.count >= options.maxFailures) {
771
- error(`${DEFAULT_SUBAGENT_PREFIX} Reached max failures (${options.maxFailures})`);
772
- return false;
773
- }
774
- }
775
- catch (err) {
776
- const message = err instanceof Error ? err.message : String(err);
777
- error(`${DEFAULT_SUBAGENT_PREFIX} Error processing ${task.ref}: ${message}`);
778
- consecutiveFailures.count++;
779
- if (consecutiveFailures.count >= options.maxFailures) {
780
- error(`${DEFAULT_SUBAGENT_PREFIX} Reached max failures (${options.maxFailures})`);
781
- return false;
782
- }
783
- }
784
- }
785
- // Visual separator at end of subagent section
786
- console.log("");
787
- console.log(chalk.cyan(`${"═".repeat(60)}`));
788
- console.log(chalk.cyan.bold(`${DEFAULT_SUBAGENT_PREFIX} Completed Review Processing`));
789
- console.log(chalk.cyan(`${"═".repeat(60)}`));
790
- console.log("");
791
- return true;
792
- }
793
11
  // ─── Command Registration ────────────────────────────────────────────────────
794
12
  export function registerRalphCommand(program) {
795
13
  const ralph = program
796
14
  .command("ralph")
797
- .description("Ralph automated task loop and agent control");
798
- // end-loop subcommand - allows agent to signal loop termination
799
- // AC: @session-end-loop-signal ac-signal
15
+ .description("[deprecated] Use kspec agent instead");
16
+ // end-loop subcommand - deprecated
17
+ // AC: @ralph-replacement ac-1
800
18
  ralph
801
19
  .command("end-loop")
802
- .description("End the ralph loop gracefully (stops all remaining iterations)")
20
+ .description("[deprecated] Use kspec agent end-loop instead")
803
21
  .option("--reason <reason>", "Reason for ending the loop")
804
- .action(async (options) => {
805
- try {
806
- const ctx = await initContext();
807
- const sessionId = process.env.KSPEC_SESSION_ID;
808
- if (!sessionId) {
809
- // AC: @trait-error-guidance ac-1, ac-2
810
- warn("No active ralph session detected (KSPEC_SESSION_ID not set).");
811
- info("This command requires an active session. It is designed to be called by agents during a ralph loop.");
812
- info("Suggestion: Ensure KSPEC_SESSION_ID is set, or start a session with: kspec session create --agent-type ralph");
813
- process.exit(EXIT_CODES.VALIDATION_FAILED);
814
- return;
815
- }
816
- // AC: @session-end-loop-signal ac-signal - Write end-loop state to session
817
- const updated = await requestEndLoop(ctx.specDir, sessionId, options.reason);
818
- if (!updated) {
819
- // AC: @trait-error-guidance ac-1, ac-2
820
- error(`Session not found: ${sessionId}`);
821
- info("Suggestion: Check session ID with: kspec session log list");
822
- process.exit(EXIT_CODES.NOT_FOUND);
823
- return;
824
- }
825
- success("Loop end signal sent");
826
- if (options.reason) {
827
- info(`Reason: ${options.reason}`);
828
- }
829
- }
830
- catch (err) {
831
- // AC: @trait-error-guidance ac-1
832
- error("Failed to signal end-loop", err);
833
- process.exit(EXIT_CODES.ERROR);
834
- }
22
+ .action(() => {
23
+ showRalphDeprecationError();
835
24
  });
836
- // Main ralph run command (default behavior when ralph is called directly)
25
+ // Main ralph run command (default behavior)
26
+ // AC: @ralph-replacement ac-1
837
27
  ralph
838
28
  .command("run", { isDefault: true })
839
- .description("Run ACP agent in a loop to process ready tasks")
29
+ .description("[deprecated] Use kspec agent dispatch start instead")
840
30
  .argument("[args...]", "")
841
- .option("--max-loops <n>", "Maximum iterations", "5")
842
- .option("--max-retries <n>", "Max retries per iteration on error", "3")
843
- .option("--max-failures <n>", "Max consecutive failed iterations before exit", "3")
844
- .option("--dry-run", "Show prompt without executing")
845
- .option("--yolo", "Use dangerously-skip-permissions (default)", true)
846
- .option("--no-yolo", "Require normal permission prompts")
847
- .option("--subagent-timeout <minutes>", "Review subagent timeout in minutes", "20")
848
- .option("--adapter <id>", "Agent adapter to use", "claude-agent-acp")
849
- .option("--worker-adapter <id>", "Adapter for task-work agent (overrides --adapter)")
850
- .option("--reviewer-adapter <id>", "Adapter for review subagent (overrides --adapter)")
851
- .option("--adapter-cmd <cmd>", "Custom adapter command (for testing)")
852
- .option("--restart-every <n>", "Restart agent every N iterations to prevent OOM (0 = never)", "10")
853
- .option("--focus <instructions>", "Focus instructions included in every iteration prompt")
854
- .option("--max-tasks <n>", "Max tasks per iteration (0 = unlimited)", "1")
855
- .option("--tasks <refs>", "Explicit task scope: only work on these tasks (comma-separated refs, e.g., @task1,@task2)")
856
- .action(async (args, options) => {
857
- // Check for unknown subcommands that fell through to default
858
- // Only check args that look like subcommand names (alphanumeric with hyphens, no quotes)
859
- if (args.length > 0) {
860
- const unknownCmd = args[0];
861
- // Skip if it looks like a malformed option or quoted argument
862
- const looksLikeSubcommand = /^[a-z][a-z0-9-]*$/i.test(unknownCmd);
863
- if (looksLikeSubcommand) {
864
- if (unknownCmd === "end-iteration") {
865
- error(`Unknown command: ${unknownCmd}. Did you mean 'end-loop'?`);
866
- info("The command was renamed from 'end-iteration' to 'end-loop' to clarify it ends the entire loop.");
867
- }
868
- else {
869
- error(`Unknown command: ${unknownCmd}`);
870
- }
871
- info("Run 'kspec ralph --help' to see available commands.");
872
- process.exit(EXIT_CODES.USAGE_ERROR);
873
- }
874
- }
875
- try {
876
- const maxLoops = parseInt(options.maxLoops, 10);
877
- const maxRetries = parseInt(options.maxRetries, 10);
878
- const maxFailures = parseInt(options.maxFailures, 10);
879
- if (Number.isNaN(maxLoops) || maxLoops < 1) {
880
- error(errors.usage.maxLoopsPositive);
881
- process.exit(EXIT_CODES.ERROR);
882
- }
883
- if (Number.isNaN(maxRetries) || maxRetries < 0) {
884
- error(errors.usage.maxRetriesNonNegative);
885
- process.exit(EXIT_CODES.ERROR);
886
- }
887
- if (Number.isNaN(maxFailures) || maxFailures < 1) {
888
- error(errors.usage.maxFailuresPositive);
889
- process.exit(EXIT_CODES.ERROR);
890
- }
891
- const subagentTimeout = parseInt(options.subagentTimeout, 10);
892
- if (Number.isNaN(subagentTimeout) || subagentTimeout < 1) {
893
- error("--subagent-timeout must be a positive integer (minutes)");
894
- process.exit(EXIT_CODES.ERROR);
895
- }
896
- const restartEvery = parseInt(options.restartEvery, 10);
897
- if (Number.isNaN(restartEvery) || restartEvery < 0) {
898
- error("--restart-every must be a non-negative integer");
899
- process.exit(EXIT_CODES.ERROR);
900
- }
901
- // AC: @ralph-session-budget-integration ac-create-budget
902
- const maxTasks = parseInt(options.maxTasks, 10);
903
- if (Number.isNaN(maxTasks) || maxTasks < 0 || maxTasks > 999) {
904
- error("--max-tasks must be 0 (unlimited) or a positive integer up to 999");
905
- process.exit(EXIT_CODES.USAGE_ERROR);
906
- }
907
- // Handle custom adapter command for testing
908
- if (options.adapterCmd) {
909
- const parts = options.adapterCmd.split(/\s+/);
910
- const customAdapter = {
911
- command: parts[0],
912
- args: parts.slice(1),
913
- description: "Custom adapter via --adapter-cmd",
914
- };
915
- registerAdapter("custom", customAdapter);
916
- options.adapter = "custom";
917
- }
918
- // AC: @ralph-per-role-adapters ac-3, ac-4, ac-5
919
- // Resolve per-role adapters with precedence: role flag > --adapter > default
920
- const workerAdapterId = options.workerAdapter ?? options.adapter;
921
- const reviewerAdapterId = options.reviewerAdapter ?? options.adapter;
922
- const workerAdapter = resolveAdapter(workerAdapterId);
923
- const reviewerAdapter = resolveAdapter(reviewerAdapterId);
924
- // AC: @ralph-per-role-adapters ac-6, ac-9, ac-11
925
- // Validate adapter packages — deduplicate when same ID
926
- const adapterIdsToValidate = new Set([workerAdapterId, reviewerAdapterId]);
927
- for (const id of adapterIdsToValidate) {
928
- const resolved = resolveAdapter(id);
929
- const isDefault = id === "claude-agent-acp" || id === "claude-code-acp";
930
- const skip = resolved.command !== "npx" ||
931
- !resolved.args[0] ||
932
- (options.dryRun && isDefault);
933
- if (!skip) {
934
- validateAdapter(resolved.args[0], id);
935
- }
936
- }
937
- // Build auto-approve extra args per adapter (applied per-spawn to prevent cross-role leakage)
938
- const workerAutoApproveArgs = options.yolo
939
- ? workerAdapter.autoApproveArgs
940
- : undefined;
941
- const reviewerAutoApproveArgs = options.yolo
942
- ? reviewerAdapter.autoApproveArgs
943
- : undefined;
944
- const restartInfo = restartEvery > 0 ? `, restart every ${restartEvery}` : "";
945
- const maxTasksInfo = maxTasks === 0 ? "unlimited" : `${maxTasks}`;
946
- // Initialize kspec context early to validate --tasks
947
- const ctx = await initContext();
948
- // AC: @cli-ralph ac-21 - Parse explicit task scope
949
- let explicitTaskScope;
950
- if (options.tasks) {
951
- try {
952
- explicitTaskScope = await parseExplicitTasks(ctx, options.tasks);
953
- info(`Explicit task scope: ${explicitTaskScope.refs.join(", ")}`);
954
- }
955
- catch (err) {
956
- error(`Invalid --tasks argument: ${err.message}`);
957
- process.exit(EXIT_CODES.VALIDATION_FAILED);
958
- }
959
- }
960
- const skillOrigins = await loadSkillOriginsForRalph(ctx);
961
- const workerPromptPlatform = getPromptPlatformForAdapter(workerAdapterId);
962
- const reviewerPromptPlatform = getPromptPlatformForAdapter(reviewerAdapterId);
963
- const workerTaskWorkSkill = resolveRalphSkillInvocation(ctx.config.ralph.skills.task_work, workerPromptPlatform, skillOrigins);
964
- const workerReflectSkill = resolveRalphSkillInvocation(ctx.config.ralph.skills.reflect, workerPromptPlatform, skillOrigins);
965
- const reviewerPrReviewSkill = resolveRalphSkillInvocation(ctx.config.ralph.skills.pr_review, reviewerPromptPlatform, skillOrigins);
966
- const taskScopeInfo = explicitTaskScope
967
- ? `, tasks=${explicitTaskScope.refs.join(",")}`
968
- : "";
969
- const adapterInfo = workerAdapterId === reviewerAdapterId
970
- ? `adapter=${workerAdapterId}`
971
- : `worker=${workerAdapterId}, reviewer=${reviewerAdapterId}`;
972
- info(`Starting ralph loop (${adapterInfo}, max ${maxLoops} iterations, ${maxRetries} retries, ${maxFailures} max failures${restartInfo}, max-tasks=${maxTasksInfo}${taskScopeInfo})`);
973
- if (options.focus) {
974
- info(`Focus: ${options.focus}`);
975
- }
976
- const specDir = ctx.specDir;
977
- // Create session for event tracking
978
- const sessionId = ulid();
979
- // Set session env vars on this process so all spawned agents
980
- // (main worker, subagent, wrap-up) inherit them via process.env.
981
- // KSPEC_RALPH_SESSION: Used by codex skill safety guard to detect ralph context.
982
- // KSPEC_SESSION_ID: Used by kspec task start for budget enforcement.
983
- // AC: @ralph-session-budget-integration ac-env-inject
984
- process.env.KSPEC_RALPH_SESSION = sessionId;
985
- process.env.KSPEC_SESSION_ID = sessionId;
986
- // AC: @ralph-session-budget-integration ac-create-budget
987
- // Create session with budget. When maxTasks=0 (unlimited), no budget.json is created.
988
- await createSessionWithBudget(specDir, {
989
- id: sessionId,
990
- agent_type: workerAdapterId,
991
- budget: maxTasks,
992
- });
993
- // AC: @ralph-per-role-adapters ac-6, ac-7
994
- // Adapter IDs for harness-specific env injection/cleanup.
995
- // Deduplicate by harness target, not just adapter ID. claude-code-acp is
996
- // an alias for claude-agent-acp — both inject to the same Claude Code
997
- // settings file. Without normalization, injecting twice would clobber the
998
- // previousValue and break cleanup restoration.
999
- const normalizeForEnv = (id) => id === "claude-code-acp" ? "claude-agent-acp" : id;
1000
- const uniqueAdapterIds = [...new Set([
1001
- normalizeForEnv(workerAdapterId),
1002
- normalizeForEnv(reviewerAdapterId),
1003
- ])];
1004
- // Everything after session creation is wrapped in try/finally to guarantee
1005
- // budget cleanup even if pre-loop setup (event logging, signal handlers) throws.
1006
- // AC: @ralph-session-budget-integration ac-session-close-all-paths
1007
- let consecutiveFailures = 0;
1008
- let agent = null;
1009
- let acpSessionId = null;
1010
- let exitReason = null;
1011
- let lastIterationCtx = null;
1012
- let lastErrorMessage;
1013
- let latestIteration = 0;
1014
- // AC: @ralph-per-role-adapters ac-7
1015
- // Track previous env values per adapter for cleanup restoration
1016
- const previousEnvValues = new Map();
1017
- const recentTaskRefs = [];
1018
- const sessionIterationMap = new Map();
1019
- const endAcpSession = (spawned, sessionToEnd) => {
1020
- if (!spawned || !sessionToEnd) {
1021
- return null;
1022
- }
1023
- spawned.client.endSession(sessionToEnd);
1024
- sessionIterationMap.delete(sessionToEnd);
1025
- return null;
1026
- };
1027
- // Signal handler refs — declared here so finally can remove them
1028
- // AC: @ralph-task-limit ac-signal-cleanup
1029
- const signalCleanup = (signal) => {
1030
- info(`Received ${signal}, cleaning up...`);
1031
- if (agent) {
1032
- acpSessionId = endAcpSession(agent, acpSessionId);
1033
- agent = disposeSpawnedAgent(agent);
1034
- }
1035
- // AC: @ralph-session-budget-integration ac-session-close-all-paths
1036
- // Must use async IIFE — signal handlers are called synchronously,
1037
- // but cleanup needs async I/O. The IIFE keeps the event loop alive
1038
- // until cleanup completes, then exits explicitly.
1039
- void (async () => {
1040
- try {
1041
- await Promise.all([
1042
- fs.unlink(getSessionBudgetPath(specDir, sessionId)).catch(() => { }),
1043
- closeSession(specDir, sessionId, "abandoned", `Received ${signal}`),
1044
- ...uniqueAdapterIds.map((id) => removeEnvForAdapter(id, previousEnvValues.get(id))),
1045
- ]);
1046
- }
1047
- catch {
1048
- // Best-effort cleanup — don't let errors prevent exit
1049
- }
1050
- finally {
1051
- process.exit(0);
1052
- }
1053
- })();
1054
- };
1055
- const sigintHandler = () => { signalCleanup("SIGINT"); };
1056
- const sigtermHandler = () => { signalCleanup("SIGTERM"); };
1057
- try {
1058
- // AC: @session-end-loop-signal ac-session-close-signal
1059
- // Install signal handlers FIRST, before any async work, so signals
1060
- // during startup (e.g. during appendEvent) still trigger cleanup.
1061
- // AC: @ralph-session-budget-integration ac-session-close-all-paths
1062
- process.on("SIGINT", sigintHandler);
1063
- process.on("SIGTERM", sigtermHandler);
1064
- // AC: @ralph-per-role-adapters ac-6, ac-7
1065
- // Inject KSPEC_SESSION_ID into agent harness config for each unique adapter.
1066
- // Process env alone is insufficient — some harnesses (e.g., Claude Code)
1067
- // sandbox child processes and don't forward arbitrary parent env vars.
1068
- // AC: @ralph-session-budget-integration ac-env-inject
1069
- for (const id of uniqueAdapterIds) {
1070
- const injectionResult = await injectEnvForAdapter(id, sessionId);
1071
- previousEnvValues.set(id, injectionResult?.previousValue);
1072
- }
1073
- // AC: @ralph-per-role-adapters ac-12
1074
- // Log session start with both adapter IDs
1075
- await appendEvent(specDir, {
1076
- session_id: sessionId,
1077
- type: "session.start",
1078
- data: {
1079
- adapter: workerAdapterId,
1080
- workerAdapter: workerAdapterId,
1081
- reviewerAdapter: reviewerAdapterId,
1082
- maxLoops,
1083
- maxRetries,
1084
- maxFailures,
1085
- maxTasks,
1086
- yolo: options.yolo,
1087
- focus: options.focus,
1088
- explicitTasks: explicitTaskScope?.refs,
1089
- },
1090
- });
1091
- // Create translator and renderer for this session
1092
- const translator = createTranslator();
1093
- const renderer = createCliRenderer();
1094
- for (let iteration = 1; iteration <= maxLoops; iteration++) {
1095
- latestIteration = iteration;
1096
- renderer.newSection?.(`Iteration ${iteration}/${maxLoops}`);
1097
- // AC: @ralph-session-budget-integration ac-reset-iteration
1098
- // Reset budget counter at iteration start (no-op when no budget exists)
1099
- await resetBudget(specDir, sessionId);
1100
- // AC: @session-end-loop-signal ac-detect - Check session state for end-loop
1101
- const endLoopState = await isEndLoopRequested(specDir, sessionId);
1102
- if (endLoopState?.requested) {
1103
- info(`End-loop already requested for this session. Exiting.`);
1104
- exitReason = "end_loop_signal";
1105
- break;
1106
- }
1107
- // Gather fresh context each iteration
1108
- // AC: @cli-ralph ac-16 - Only automation-eligible tasks (unless explicit scope)
1109
- // AC: @cli-ralph ac-21 - With explicit task scope, ignore automation eligibility
1110
- let sessionCtx = await gatherSessionContext(ctx, {
1111
- limit: "10",
1112
- eligible: !explicitTaskScope, // Skip eligibility filter if explicit scope
1113
- });
1114
- // AC: @cli-ralph ac-21 - Filter to explicit tasks if scope is set
1115
- if (explicitTaskScope) {
1116
- sessionCtx = filterByExplicitTasks(sessionCtx, explicitTaskScope);
1117
- }
1118
- // AC: @ralph-subagent-spawning ac-8 - Process pending_review tasks BEFORE main iteration
1119
- // AC: @ralph-per-role-adapters ac-2 - Use reviewer adapter for review subagents
1120
- // This wraps consecutiveFailures in an object so it can be mutated by the helper
1121
- const failureTracker = { count: consecutiveFailures };
1122
- const continueLoop = await processPendingReviewTasks(ctx, reviewerAdapter, sessionCtx.pending_review_tasks, {
1123
- yolo: options.yolo,
1124
- maxRetries,
1125
- maxFailures,
1126
- cwd: process.cwd(),
1127
- specDir,
1128
- sessionId,
1129
- subagentTimeout: subagentTimeout * 60 * 1000,
1130
- autoApproveArgs: reviewerAutoApproveArgs,
1131
- prReviewSkillName: reviewerPrReviewSkill,
1132
- }, failureTracker);
1133
- consecutiveFailures = failureTracker.count;
1134
- if (!continueLoop) {
1135
- exitReason = "max_failures";
1136
- lastIterationCtx = sessionCtx;
1137
- break;
1138
- }
1139
- // AC: @cli-ralph ac-20 - Refresh context after pending_review processing
1140
- // If pending_review tasks were processed, they may have completed and unblocked
1141
- // dependent tasks. Re-gather context to detect newly available tasks.
1142
- let currentCtx = sessionCtx;
1143
- if (sessionCtx.pending_review_tasks.length > 0) {
1144
- currentCtx = await gatherSessionContext(ctx, {
1145
- limit: "10",
1146
- eligible: !explicitTaskScope,
1147
- });
1148
- if (explicitTaskScope) {
1149
- currentCtx = filterByExplicitTasks(currentCtx, explicitTaskScope);
1150
- }
1151
- }
1152
- // AC: @cli-ralph ac-21 - Check explicit task completion
1153
- if (explicitTaskScope) {
1154
- const { done, statuses } = await allExplicitTasksDone(ctx, explicitTaskScope);
1155
- if (done) {
1156
- const statusList = Array.from(statuses.entries())
1157
- .map(([ref, status]) => `${ref}: ${status}`)
1158
- .join(", ");
1159
- info(`All explicit tasks completed or blocked (${statusList}). Exiting loop.`);
1160
- exitReason = "explicit_tasks_done";
1161
- lastIterationCtx = currentCtx;
1162
- break;
1163
- }
1164
- }
1165
- // Check for automation-eligible tasks (ready or in_progress)
1166
- // AC: @cli-ralph ac-19
1167
- const hasActiveTasks = currentCtx.active_tasks.length > 0;
1168
- const hasReadyTasks = currentCtx.ready_tasks.length > 0;
1169
- if (!hasActiveTasks && !hasReadyTasks) {
1170
- if (explicitTaskScope) {
1171
- info("No explicit tasks available (ready or in_progress). Exiting loop.");
1172
- }
1173
- else {
1174
- info("No automation-eligible tasks (ready or in_progress). Exiting loop.");
1175
- }
1176
- exitReason = "no_tasks";
1177
- lastIterationCtx = currentCtx;
1178
- break;
1179
- }
1180
- // AC: @loop-mode-error-handling - Track tasks in progress for failure handling
1181
- const tasksInProgressAtStart = sessionCtx.active_tasks;
1182
- const iterationStartTime = new Date();
1183
- // Build prompts - task-work first, then reflect
1184
- // AC: @cli-ralph ac-21 - Include explicit task scope in prompt
1185
- const taskWorkPrompt = buildTaskWorkPrompt(currentCtx, iteration, maxLoops, sessionId, workerTaskWorkSkill, options.focus, explicitTaskScope);
1186
- const reflectPrompt = buildReflectPrompt(iteration, maxLoops, sessionId, workerReflectSkill);
1187
- // AC: @cli-ralph ac-21
1188
- // AC: @ralph-per-role-adapters ac-10
1189
- if (options.dryRun) {
1190
- console.log(chalk.yellow("=== DRY RUN - Configuration ===\n"));
1191
- console.log(` worker-adapter: ${workerAdapterId}`);
1192
- console.log(` reviewer-adapter: ${reviewerAdapterId}`);
1193
- console.log(` max-loops: ${maxLoops}`);
1194
- console.log(` max-tasks: ${maxTasks === 0 ? "unlimited" : maxTasks}`);
1195
- console.log(` max-retries: ${maxRetries}`);
1196
- console.log(` max-failures: ${maxFailures}`);
1197
- console.log(` restart-every: ${restartEvery === 0 ? "never" : restartEvery}`);
1198
- console.log(` worker-task-work-skill: ${workerTaskWorkSkill}`);
1199
- console.log(` worker-reflect-skill: ${workerReflectSkill}`);
1200
- console.log(` reviewer-pr-review-skill: ${reviewerPrReviewSkill}`);
1201
- if (explicitTaskScope) {
1202
- console.log(` explicit-tasks: ${explicitTaskScope.refs.join(", ")}`);
1203
- }
1204
- console.log(chalk.yellow("\n=== Task Work Prompt ===\n"));
1205
- console.log(taskWorkPrompt);
1206
- console.log(chalk.yellow("\n=== Reflect Prompt ===\n"));
1207
- console.log(reflectPrompt);
1208
- console.log(chalk.yellow("\n=== END DRY RUN ==="));
1209
- break;
1210
- }
1211
- // Log task-work prompt
1212
- await appendEvent(specDir, {
1213
- session_id: sessionId,
1214
- type: "prompt.sent",
1215
- data: {
1216
- iteration,
1217
- phase: "task-work",
1218
- prompt: taskWorkPrompt,
1219
- tasks: {
1220
- active: currentCtx.active_tasks.map((t) => t.ref),
1221
- ready: currentCtx.ready_tasks.map((t) => t.ref),
1222
- },
1223
- },
1224
- });
1225
- // Retry loop for this iteration
1226
- let lastError = null;
1227
- let succeeded = false;
1228
- for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
1229
- if (attempt > 1) {
1230
- console.log(chalk.yellow(`\nRetry attempt ${attempt - 1}/${maxRetries}...`));
1231
- }
1232
- try {
1233
- // Spawn agent if not already running
1234
- // AC: @ralph-per-role-adapters ac-1 - Use worker adapter for task-work
1235
- if (!agent) {
1236
- info("Spawning ACP agent...");
1237
- // AC: @ralph-session-budget-integration ac-env-inject
1238
- // AC: @ralph-adapter-auto-approve ac-1, ac-2, ac-3
1239
- agent = await spawnAndInitialize(workerAdapter, {
1240
- cwd: process.cwd(),
1241
- env: { KSPEC_SESSION_ID: sessionId },
1242
- extraArgs: workerAutoApproveArgs,
1243
- clientOptions: {
1244
- clientInfo: {
1245
- name: "kspec-ralph",
1246
- version: packageVersion,
1247
- },
1248
- methodTimeouts: {
1249
- "session/prompt": RALPH_PROMPT_TIMEOUT,
1250
- "session/resume": RALPH_PROMPT_TIMEOUT,
1251
- },
1252
- },
1253
- });
1254
- // Set up streaming update handler with translator + renderer
1255
- agent.client.on("update", (_sid, update) => {
1256
- // Translate ACP event to RalphEvent and render
1257
- const event = translator.translate(update);
1258
- if (event) {
1259
- renderer.render(event);
1260
- }
1261
- // Log raw update event (async, non-blocking)
1262
- // Look up iteration by ACP session ID so late updates from
1263
- // a previous session are attributed to the correct iteration
1264
- const eventIteration = sessionIterationMap.get(_sid) ?? latestIteration;
1265
- appendEvent(specDir, {
1266
- session_id: sessionId,
1267
- type: "session.update",
1268
- data: { iteration: eventIteration, update },
1269
- }).catch(() => {
1270
- // Ignore logging errors during streaming
1271
- });
1272
- });
1273
- // Set up tool request handler
1274
- agent.client.on("request", (reqId, method, params) => {
1275
- // biome-ignore lint/style/noNonNullAssertion: agent is guaranteed to exist when callback is registered
1276
- handleRequest(agent.client, reqId, method, params, {
1277
- yolo: options.yolo,
1278
- specDir,
1279
- sessionId,
1280
- }).catch((err) => {
1281
- // biome-ignore lint/style/noNonNullAssertion: agent is guaranteed to exist when callback is registered
1282
- agent.client.respondError(reqId, -32000, err.message);
1283
- });
1284
- });
1285
- }
1286
- // Create fresh ACP session per iteration to keep context clean
1287
- info("Creating ACP session...");
1288
- acpSessionId = await agent.client.newSession({
1289
- cwd: process.cwd(),
1290
- mcpServers: [], // No MCP servers for now
1291
- });
1292
- setSessionIteration(sessionIterationMap, acpSessionId, iteration);
1293
- // Phase 1: Task Work
1294
- info("Sending task-work prompt to agent...");
1295
- const taskWorkResponse = await agent.client.prompt({
1296
- sessionId: acpSessionId,
1297
- prompt: [{ type: "text", text: taskWorkPrompt }],
1298
- });
1299
- // Log task-work completion
1300
- await appendEvent(specDir, {
1301
- session_id: sessionId,
1302
- type: "session.update",
1303
- data: {
1304
- iteration,
1305
- phase: "task-work",
1306
- stopReason: taskWorkResponse.stopReason,
1307
- completed: true,
1308
- },
1309
- });
1310
- if (taskWorkResponse.stopReason === "cancelled") {
1311
- throw new Error(errors.usage.agentPromptCancelled);
1312
- }
1313
- // Phase 2: Reflect (always sent after task-work completes)
1314
- info("Sending reflect prompt to agent...");
1315
- await appendEvent(specDir, {
1316
- session_id: sessionId,
1317
- type: "prompt.sent",
1318
- data: {
1319
- iteration,
1320
- phase: "reflect",
1321
- prompt: reflectPrompt,
1322
- },
1323
- });
1324
- const reflectResponse = await agent.client.prompt({
1325
- sessionId: acpSessionId,
1326
- prompt: [{ type: "text", text: reflectPrompt }],
1327
- });
1328
- // Log reflect completion
1329
- await appendEvent(specDir, {
1330
- session_id: sessionId,
1331
- type: "session.update",
1332
- data: {
1333
- iteration,
1334
- phase: "reflect",
1335
- stopReason: reflectResponse.stopReason,
1336
- completed: true,
1337
- },
1338
- });
1339
- if (reflectResponse.stopReason === "cancelled") {
1340
- throw new Error(errors.usage.agentPromptCancelled);
1341
- }
1342
- acpSessionId = endAcpSession(agent, acpSessionId);
1343
- succeeded = true;
1344
- break;
1345
- }
1346
- catch (err) {
1347
- lastError = err;
1348
- error(errors.failures.iterationFailed(lastError.message));
1349
- // Clean up agent on error - will respawn next attempt
1350
- if (agent) {
1351
- acpSessionId = endAcpSession(agent, acpSessionId);
1352
- agent = disposeSpawnedAgent(agent);
1353
- }
1354
- }
1355
- }
1356
- if (succeeded) {
1357
- console.log(); // Newline after streaming output
1358
- // Save session context snapshot for audit trail
1359
- await saveSessionContext(specDir, sessionId, iteration, sessionCtx);
1360
- success(`Completed iteration ${iteration}`);
1361
- consecutiveFailures = 0;
1362
- // Track task refs from this iteration for wrap-up context
1363
- for (const t of sessionCtx.active_tasks) {
1364
- pushRecentTaskRef(recentTaskRefs, t.ref);
1365
- }
1366
- lastIterationCtx = sessionCtx;
1367
- // Periodic agent restart to prevent OOM
1368
- // AC: @cli-ralph ac-restart-periodic
1369
- if (restartEvery > 0 &&
1370
- iteration % restartEvery === 0 &&
1371
- iteration < maxLoops) {
1372
- info(`Restarting agent to prevent memory buildup (every ${restartEvery} iterations)...`);
1373
- if (agent) {
1374
- acpSessionId = endAcpSession(agent, acpSessionId);
1375
- agent = disposeSpawnedAgent(agent);
1376
- }
1377
- }
1378
- }
1379
- else {
1380
- consecutiveFailures++;
1381
- error(errors.failures.iterationFailedAfterRetries(iteration, maxRetries, consecutiveFailures, maxFailures));
1382
- if (lastError) {
1383
- error(errors.failures.lastError(lastError.message));
1384
- }
1385
- // AC: @loop-mode-error-handling - Track per-task failures
1386
- const errorDesc = lastError?.message || "Iteration failed after retries";
1387
- await handleIterationFailure(ctx, tasksInProgressAtStart, iterationStartTime, errorDesc);
1388
- if (consecutiveFailures >= maxFailures) {
1389
- error(errors.failures.reachedMaxFailures(maxFailures));
1390
- exitReason = "max_failures";
1391
- lastErrorMessage = lastError?.message;
1392
- lastIterationCtx = sessionCtx;
1393
- break;
1394
- }
1395
- info("Continuing to next iteration...");
1396
- }
1397
- }
1398
- // If loop completed all iterations without breaking
1399
- if (exitReason === null) {
1400
- exitReason = "max_iterations";
1401
- }
1402
- }
1403
- catch (loopErr) {
1404
- // AC: @session-end-loop-signal ac-session-close-error
1405
- // Unrecoverable error during loop execution
1406
- exitReason = exitReason ?? "error";
1407
- lastErrorMessage = loopErr.message;
1408
- error("Unrecoverable error in ralph loop", loopErr);
1409
- }
1410
- finally {
1411
- // Remove signal handlers to avoid double cleanup
1412
- process.off("SIGINT", sigintHandler);
1413
- process.off("SIGTERM", sigtermHandler);
1414
- // Clean up agent
1415
- if (agent) {
1416
- acpSessionId = endAcpSession(agent, acpSessionId);
1417
- agent = disposeSpawnedAgent(agent);
1418
- }
1419
- // AC: @ralph-session-budget-integration ac-session-close-all-paths
1420
- // AC: @ralph-per-role-adapters ac-7 - Clean up env for all unique adapters
1421
- await fs.unlink(getSessionBudgetPath(specDir, sessionId)).catch(() => { });
1422
- for (const id of uniqueAdapterIds) {
1423
- await removeEnvForAdapter(id, previousEnvValues.get(id));
1424
- }
1425
- // Clean up session env vars
1426
- delete process.env.KSPEC_RALPH_SESSION;
1427
- delete process.env.KSPEC_SESSION_ID;
1428
- // AC: @ralph-wrap-up-agent-on-loop-exit ac-1, ac-2, ac-3, ac-4, ac-5
1429
- // Spawn wrap-up agent if not dry-run and we have an exit reason
1430
- if (!options.dryRun && exitReason) {
1431
- console.log("");
1432
- console.log(chalk.cyan(`${"═".repeat(60)}`));
1433
- console.log(chalk.cyan.bold(`${WRAPUP_AGENT_PREFIX} Starting Wrap-Up`));
1434
- console.log(chalk.cyan(`${"═".repeat(60)}`));
1435
- console.log("");
1436
- const inProgressTasks = lastIterationCtx?.active_tasks || [];
1437
- const pendingReviewTasks = lastIterationCtx?.pending_review_tasks || [];
1438
- const wrapUpCtx = buildWrapUpContext(exitReason, sessionId, maxLoops, // Use maxLoops as iteration (we're at the end)
1439
- maxLoops, inProgressTasks, pendingReviewTasks, recentTaskRefs, process.cwd(), lastErrorMessage);
1440
- info(`Exit reason: ${exitReason}`);
1441
- info(`Working tree: ${wrapUpCtx.workingTree.clean ? "clean" : "has uncommitted changes"}`);
1442
- // AC: @ralph-per-role-adapters ac-8 - Wrap-up uses worker adapter
1443
- const wrapUpResult = await runWrapUpAgent(workerAdapter, wrapUpCtx, {
1444
- yolo: options.yolo,
1445
- cwd: process.cwd(),
1446
- extraArgs: workerAutoApproveArgs,
1447
- handleRequest: (client, reqId, method, params) => handleRequest(client, reqId, method, params, {
1448
- yolo: options.yolo,
1449
- specDir,
1450
- sessionId,
1451
- }),
1452
- }, DEFAULT_WRAPUP_TIMEOUT);
1453
- // Log wrap-up result
1454
- await appendEvent(specDir, {
1455
- session_id: sessionId,
1456
- type: "session.wrapup",
1457
- data: {
1458
- exitReason,
1459
- result: wrapUpResult,
1460
- },
1461
- });
1462
- if (wrapUpResult.skipped) {
1463
- info(`${WRAPUP_AGENT_PREFIX} Skipped: ${wrapUpResult.skipReason}`);
1464
- }
1465
- else if (wrapUpResult.timedOut) {
1466
- warn(`${WRAPUP_AGENT_PREFIX} Timed out after ${DEFAULT_WRAPUP_TIMEOUT / 1000}s`);
1467
- }
1468
- else if (!wrapUpResult.success) {
1469
- warn(`${WRAPUP_AGENT_PREFIX} Failed: ${wrapUpResult.error}`);
1470
- }
1471
- else {
1472
- success(`${WRAPUP_AGENT_PREFIX} Completed`);
1473
- }
1474
- console.log("");
1475
- console.log(chalk.cyan(`${"═".repeat(60)}`));
1476
- console.log(chalk.cyan.bold(`${WRAPUP_AGENT_PREFIX} Wrap-Up Complete`));
1477
- console.log(chalk.cyan(`${"═".repeat(60)}`));
1478
- console.log("");
1479
- }
1480
- // Log session end and close session with appropriate status/reason
1481
- // AC: @session-end-loop-signal ac-session-close-normal, ac-session-close-error
1482
- const isErrorExit = consecutiveFailures >= maxFailures ||
1483
- exitReason === "max_failures" ||
1484
- exitReason === "error";
1485
- const status = isErrorExit ? "abandoned" : "completed";
1486
- const closeReason = exitReason === "max_failures"
1487
- ? `Max failures reached (${consecutiveFailures}/${maxFailures})${lastErrorMessage ? `: ${lastErrorMessage}` : ""}`
1488
- : exitReason === "error"
1489
- ? `Unrecoverable error${lastErrorMessage ? `: ${lastErrorMessage}` : ""}`
1490
- : exitReason === "end_loop_signal"
1491
- ? "Agent requested end of loop"
1492
- : exitReason === "max_iterations"
1493
- ? `Completed all ${maxLoops} iterations`
1494
- : exitReason === "no_tasks"
1495
- ? "No eligible tasks remaining"
1496
- : exitReason === "explicit_tasks_done"
1497
- ? "All explicit tasks completed"
1498
- : `Loop ended: ${exitReason}`;
1499
- await appendEvent(specDir, {
1500
- session_id: sessionId,
1501
- type: "session.end",
1502
- data: {
1503
- status,
1504
- consecutiveFailures,
1505
- exitReason,
1506
- closeReason,
1507
- },
1508
- });
1509
- await closeSession(specDir, sessionId, status, closeReason);
1510
- }
1511
- console.log(chalk.green(`\n${"─".repeat(60)}`));
1512
- success("Ralph loop completed");
1513
- console.log(chalk.green(`${"─".repeat(60)}\n`));
1514
- }
1515
- catch (err) {
1516
- error(errors.failures.ralphLoop, err);
1517
- process.exit(EXIT_CODES.ERROR);
1518
- }
31
+ .allowUnknownOption()
32
+ .action(() => {
33
+ showRalphDeprecationError();
1519
34
  });
1520
35
  }
36
+ /**
37
+ * Display a migration error message explaining that ralph has been replaced.
38
+ *
39
+ * AC: @ralph-replacement ac-1 — error message lists equivalent commands for
40
+ * common ralph operations (run, end-loop, dry-run)
41
+ * AC: @trait-error-guidance ac-1 — includes description of what went wrong
42
+ * AC: @trait-error-guidance ac-2 — includes suggested action to resolve
43
+ */
44
+ function showRalphDeprecationError() {
45
+ const header = chalk.red("✗ kspec ralph has been replaced by kspec agent");
46
+ const msg = [
47
+ header,
48
+ "",
49
+ chalk.bold("kspec ralph has been removed.") +
50
+ " Use " +
51
+ chalk.cyan("kspec agent") +
52
+ " for equivalent functionality.",
53
+ "",
54
+ chalk.bold("Equivalent commands:"),
55
+ ` ${chalk.yellow("kspec ralph run")} → ${chalk.cyan("kspec agent dispatch start")}`,
56
+ ` ${chalk.yellow("kspec ralph --dry-run")} → ${chalk.cyan("kspec agent dispatch start --dry-run")}`,
57
+ ` ${chalk.yellow("kspec ralph end-loop")} → ${chalk.cyan("kspec agent end-loop")}`,
58
+ "",
59
+ chalk.bold("Getting started:"),
60
+ ` List configured agents: ${chalk.cyan("kspec agent list")}`,
61
+ ` Run a specific agent: ${chalk.cyan("kspec agent run <agent-id>")}`,
62
+ ` Start dispatch engine: ${chalk.cyan("kspec agent dispatch start")}`,
63
+ ` Check dispatch status: ${chalk.cyan("kspec agent dispatch status")}`,
64
+ "",
65
+ `Run ${chalk.cyan("kspec setup")} to create built-in worker and reviewer agent definitions.`,
66
+ `Run ${chalk.cyan("kspec agent --help")} for full documentation.`,
67
+ ].join("\n");
68
+ process.stderr.write(msg + "\n");
69
+ process.exit(EXIT_CODES.ERROR);
70
+ }
1521
71
  //# sourceMappingURL=ralph.js.map