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