@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
@@ -0,0 +1,831 @@
1
+ /**
2
+ * kspec agent commands — manage and run agents.
3
+ *
4
+ * Provides subcommands for listing agent definitions, running one-shot
5
+ * invocations, and managing the dispatch engine lifecycle via the daemon.
6
+ *
7
+ * AC: @cli-agent-commands ac-1 through ac-10
8
+ * AC: @trait-json-output ac-1 through ac-6
9
+ * AC: @trait-semantic-exit-codes ac-1 through ac-8
10
+ * AC: @trait-error-guidance ac-1 through ac-6
11
+ * AC: @trait-dry-run ac-1 through ac-6
12
+ * AC: @trait-filterable-list ac-1 through ac-8
13
+ */
14
+ import chalk from "chalk";
15
+ import { initContext, loadMetaContext, } from "../../parser/index.js";
16
+ import { runInvocation } from "../../agent-runtime/invocation.js";
17
+ import { buildPromptWithSkills } from "../../agent-runtime/prompts.js";
18
+ import { resolveAdapter } from "../../agents/adapters.js";
19
+ import { EXIT_CODES } from "../exit-codes.js";
20
+ import { error, info, output, success, warn, isJsonMode } from "../output.js";
21
+ import { parseIntOption } from "../validators.js";
22
+ import { PidFileManager } from "../pid-utils.js";
23
+ import { errors } from "../../strings/errors.js";
24
+ import { requestEndLoop } from "../../sessions/index.js";
25
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
26
+ /**
27
+ * Get the daemon URL from the PID file manager.
28
+ * Returns null if the daemon is not running.
29
+ * AC: @cli-agent-commands ac-10
30
+ */
31
+ function getDaemonUrl() {
32
+ const pidManager = new PidFileManager();
33
+ if (!pidManager.isDaemonRunning())
34
+ return null;
35
+ try {
36
+ const port = pidManager.readPort();
37
+ return { url: `http://localhost:${port}`, port };
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ /**
44
+ * Format dispatch rules summary for display.
45
+ */
46
+ function formatDispatchRules(agent) {
47
+ if (!agent.dispatch || agent.dispatch.length === 0) {
48
+ return "(none)";
49
+ }
50
+ return agent.dispatch.map((r) => {
51
+ const filterParts = [];
52
+ if (r.filter?.automation)
53
+ filterParts.push(`automation=${r.filter.automation}`);
54
+ if (r.filter?.tags?.length)
55
+ filterParts.push(`tags=${r.filter.tags.join(",")}`);
56
+ if (r.filter?.priority !== undefined)
57
+ filterParts.push(`priority=${r.filter.priority}`);
58
+ const filterStr = filterParts.length > 0 ? ` [${filterParts.join(", ")}]` : "";
59
+ return `${r.on}${filterStr}`;
60
+ }).join(", ");
61
+ }
62
+ // ─── Command Registration ─────────────────────────────────────────────────────
63
+ /**
64
+ * Register the kspec agent command family.
65
+ * AC: @cli-agent-commands ac-1 through ac-10
66
+ */
67
+ export function registerAgentCommands(program) {
68
+ const agent = program
69
+ .command("agent")
70
+ .description("Manage and run agents");
71
+ // ─── kspec agent list ─────────────────────────────────────────────────────
72
+ // AC: @cli-agent-commands ac-1
73
+ // AC: @trait-filterable-list ac-1 through ac-8
74
+ agent
75
+ .command("list")
76
+ .description("List all agent definitions")
77
+ .option("--json", "Output as JSON")
78
+ .option("--status <status>", "Filter by automation status (eligible|ineligible)")
79
+ .option("--tag <tag>", "Filter by tag (repeatable)", (val, arr) => [...arr, val], [])
80
+ .option("--limit <n>", "Maximum number of results")
81
+ .option("--offset <n>", "Skip first N results")
82
+ .option("--count", "Output only the count")
83
+ .action(async (opts) => {
84
+ try {
85
+ const ctx = await initContext();
86
+ if (!ctx.manifestPath) {
87
+ error(errors.project.noKspecProject);
88
+ process.exit(EXIT_CODES.VALIDATION_FAILED);
89
+ }
90
+ const meta = await loadMetaContext(ctx);
91
+ let agents = meta.agents;
92
+ // AC: @trait-filterable-list ac-1 - automation status filter
93
+ if (opts.status) {
94
+ agents = agents.filter((a) => a.automation === opts.status);
95
+ }
96
+ // AC: @trait-filterable-list ac-2 - tag filter
97
+ const tags = opts.tag ?? [];
98
+ if (tags.length > 0) {
99
+ agents = agents.filter((a) => {
100
+ const agentTags = a.tags ?? [];
101
+ return tags.every((t) => agentTags.includes(t));
102
+ });
103
+ }
104
+ const total = agents.length;
105
+ // AC: @trait-filterable-list ac-8 - count mode
106
+ if (opts.count) {
107
+ output({ count: total }, () => {
108
+ console.log(total);
109
+ });
110
+ return;
111
+ }
112
+ // AC: @trait-filterable-list ac-3, ac-4 - pagination
113
+ // AC: @trait-semantic-exit-codes ac-2 - invalid numeric input exits with validation error
114
+ let limit = total;
115
+ if (opts.limit !== undefined) {
116
+ const parsed = parseIntOption(opts.limit, {
117
+ min: 0,
118
+ max: Number.MAX_SAFE_INTEGER,
119
+ name: "Limit",
120
+ });
121
+ if (!parsed.ok) {
122
+ error(`Invalid --limit value: ${parsed.error}`, { suggestion: "Example: --limit 10" });
123
+ process.exit(EXIT_CODES.VALIDATION_FAILED);
124
+ }
125
+ limit = parsed.value;
126
+ }
127
+ let offset = 0;
128
+ if (opts.offset !== undefined) {
129
+ const parsed = parseIntOption(opts.offset, {
130
+ min: 0,
131
+ max: Number.MAX_SAFE_INTEGER,
132
+ name: "Offset",
133
+ });
134
+ if (!parsed.ok) {
135
+ error(`Invalid --offset value: ${parsed.error}`, { suggestion: "Example: --offset 5" });
136
+ process.exit(EXIT_CODES.VALIDATION_FAILED);
137
+ }
138
+ offset = parsed.value;
139
+ }
140
+ const paginated = agents.slice(offset, offset + limit);
141
+ // AC: @trait-semantic-exit-codes ac-5 - empty result set exits 0
142
+ // AC: @trait-filterable-list ac-6 - empty list with informative message
143
+ if (paginated.length === 0) {
144
+ output({ items: [], total, offset, limit }, () => {
145
+ if (opts.status || tags.length > 0) {
146
+ console.log("No agents match the specified filters.");
147
+ }
148
+ else {
149
+ console.log("No agent definitions found.");
150
+ }
151
+ });
152
+ return;
153
+ }
154
+ // AC: @trait-json-output ac-1 through ac-5
155
+ output({
156
+ items: paginated.map((a) => ({
157
+ id: a.id,
158
+ name: a.name,
159
+ adapter: a.adapter ?? "claude-agent-acp",
160
+ dispatch: a.dispatch ?? [],
161
+ concurrency: a.concurrency ?? { max_concurrent: 1 },
162
+ })),
163
+ total,
164
+ offset,
165
+ limit,
166
+ }, () => {
167
+ // AC: @trait-filterable-list ac-7 - summary with total and filter state
168
+ const filterDesc = [
169
+ opts.status ? `status=${opts.status}` : "",
170
+ tags.length > 0 ? `tags=${tags.join(",")}` : "",
171
+ ].filter(Boolean).join(", ");
172
+ const summaryStr = filterDesc ? ` (filtered: ${filterDesc})` : "";
173
+ console.log(chalk.bold(`Agents${summaryStr}: ${paginated.length} of ${total}`));
174
+ console.log();
175
+ for (const a of paginated) {
176
+ console.log(` ${chalk.cyan(a.id)} ${chalk.gray(a.adapter ?? "claude-agent-acp")}`);
177
+ console.log(` ${chalk.gray("dispatch:")} ${formatDispatchRules(a)}`);
178
+ console.log(` ${chalk.gray("concurrency:")} max ${a.concurrency?.max_concurrent ?? 1}`);
179
+ }
180
+ });
181
+ }
182
+ catch (err) {
183
+ error("Failed to list agents", err);
184
+ process.exit(EXIT_CODES.ERROR);
185
+ }
186
+ });
187
+ // ─── kspec agent run ──────────────────────────────────────────────────────
188
+ // AC: @cli-agent-commands ac-2, ac-3, ac-7, ac-8
189
+ // AC: @trait-dry-run ac-1 through ac-6
190
+ agent
191
+ .command("run <agent-id> [prompt]")
192
+ .description("Run a one-shot agent invocation")
193
+ .option("--task <ref>", "Task reference to target")
194
+ .option("--timeout <minutes>", "Timeout in minutes (overrides agent default)")
195
+ .option("--budget <n>", "Budget override (max tasks)")
196
+ .option("--adapter <id>", "Adapter override")
197
+ .option("--dry-run", "Show prompt without spawning agent")
198
+ .option("--json", "Output as JSON")
199
+ .action(async (agentId, prompt, opts) => {
200
+ try {
201
+ const ctx = await initContext();
202
+ if (!ctx.manifestPath) {
203
+ error(errors.project.noKspecProject);
204
+ process.exit(EXIT_CODES.VALIDATION_FAILED);
205
+ }
206
+ const meta = await loadMetaContext(ctx);
207
+ const agentDef = meta.agents.find((a) => a.id === agentId);
208
+ // AC: @trait-error-guidance ac-3 - not found error with suggestion
209
+ if (!agentDef) {
210
+ error(`Agent "${agentId}" not found.`, {
211
+ suggestion: `Check available agents with: kspec agent list`,
212
+ });
213
+ process.exit(EXIT_CODES.VALIDATION_FAILED);
214
+ }
215
+ // Resolve the effective adapter
216
+ const adapterId = opts.adapter ?? agentDef.adapter ?? "claude-agent-acp";
217
+ const adapter = resolveAdapter(adapterId);
218
+ // Build the prompt
219
+ const taskRef = opts.task;
220
+ const basePrompt = prompt ?? (taskRef
221
+ ? `Work on task ${taskRef} according to your configuration and skills.`
222
+ : `Run as requested.`);
223
+ // Note: buildPromptWithSkills is called here for the dry-run preview path.
224
+ // runInvocation also calls buildPromptWithSkills internally, so we pass basePrompt
225
+ // to runInvocation (not fullPrompt) to avoid double-expansion.
226
+ const fullPromptForPreview = await buildPromptWithSkills({
227
+ basePrompt,
228
+ skillIds: agentDef.skills ?? [],
229
+ specDir: ctx.specDir,
230
+ adapterId,
231
+ });
232
+ // AC: @trait-dry-run ac-1, ac-2, ac-3 - dry run shows prompt, no changes
233
+ if (opts.dryRun) {
234
+ // Pre-compute overrides so dry-run reflects what the actual invocation would use
235
+ // AC: @cli-agent-commands ac-7 - overrides are visible in dry-run output
236
+ // AC: @trait-semantic-exit-codes ac-2 - validate numeric inputs even in dry-run
237
+ let dryTimeoutOverride;
238
+ if (opts.timeout) {
239
+ const parsed = parseIntOption(opts.timeout, { min: 1, max: 10080, name: "Timeout" });
240
+ if (!parsed.ok) {
241
+ error(`Invalid --timeout value: ${parsed.error}`, { suggestion: "Example: --timeout 30" });
242
+ process.exit(EXIT_CODES.VALIDATION_FAILED);
243
+ }
244
+ dryTimeoutOverride = parsed.value;
245
+ }
246
+ let dryBudgetOverride;
247
+ if (opts.budget) {
248
+ const parsed = parseIntOption(opts.budget, { min: 1, max: 99999, name: "Budget" });
249
+ if (!parsed.ok) {
250
+ error(`Invalid --budget value: ${parsed.error}`, { suggestion: "Example: --budget 10" });
251
+ process.exit(EXIT_CODES.VALIDATION_FAILED);
252
+ }
253
+ dryBudgetOverride = parsed.value;
254
+ }
255
+ const effectiveTimeoutMinutes = dryTimeoutOverride ?? agentDef.budget?.timeout_minutes;
256
+ const effectiveMaxTasks = dryBudgetOverride ?? agentDef.budget?.max_tasks;
257
+ // AC: @trait-dry-run ac-6 - JSON output includes dry_run field
258
+ output({
259
+ dry_run: true,
260
+ agent_id: agentId,
261
+ adapter: adapterId,
262
+ task_ref: taskRef ?? null,
263
+ timeout_minutes: effectiveTimeoutMinutes ?? null,
264
+ max_tasks: effectiveMaxTasks ?? null,
265
+ prompt: fullPromptForPreview,
266
+ }, () => {
267
+ console.log(chalk.yellow("DRY RUN - No agent will be spawned"));
268
+ console.log();
269
+ console.log(chalk.gray(`Agent: ${agentId}`));
270
+ console.log(chalk.gray(`Adapter: ${adapterId}`));
271
+ if (taskRef) {
272
+ console.log(chalk.gray(`Task: ${taskRef}`));
273
+ }
274
+ if (effectiveTimeoutMinutes !== undefined) {
275
+ console.log(chalk.gray(`Timeout: ${effectiveTimeoutMinutes} min`));
276
+ }
277
+ console.log();
278
+ console.log(chalk.gray("--- Prompt that would be sent ---"));
279
+ console.log(fullPromptForPreview);
280
+ console.log(chalk.gray("--- End prompt ---"));
281
+ });
282
+ return;
283
+ }
284
+ // AC: @cli-agent-commands ac-2 - one-shot invocation with task binding
285
+ // AC: @cli-agent-commands ac-3 - one-shot invocation with custom prompt (no task binding)
286
+ // AC: @cli-agent-commands ac-7 - CLI overrides agent defaults
287
+ let timeoutOverride;
288
+ if (opts.timeout) {
289
+ const parsed = parseIntOption(opts.timeout, { min: 1, max: 10080, name: "Timeout" });
290
+ if (!parsed.ok) {
291
+ error(`Invalid --timeout value: ${parsed.error}`, { suggestion: "Example: --timeout 30" });
292
+ process.exit(EXIT_CODES.VALIDATION_FAILED);
293
+ }
294
+ timeoutOverride = parsed.value;
295
+ }
296
+ let budgetOverride;
297
+ if (opts.budget) {
298
+ const parsed = parseIntOption(opts.budget, { min: 1, max: 99999, name: "Budget" });
299
+ if (!parsed.ok) {
300
+ error(`Invalid --budget value: ${parsed.error}`, { suggestion: "Example: --budget 10" });
301
+ process.exit(EXIT_CODES.VALIDATION_FAILED);
302
+ }
303
+ budgetOverride = parsed.value;
304
+ }
305
+ const effectiveAgent = {
306
+ ...agentDef,
307
+ adapter: adapterId,
308
+ budget: timeoutOverride !== undefined || budgetOverride !== undefined
309
+ ? {
310
+ ...agentDef.budget,
311
+ timeout_minutes: timeoutOverride ?? agentDef.budget?.timeout_minutes,
312
+ max_tasks: budgetOverride ?? agentDef.budget?.max_tasks,
313
+ }
314
+ : agentDef.budget,
315
+ };
316
+ console.log(chalk.gray(`Running agent "${agentId}"...`));
317
+ // AC: @cli-agent-commands ac-12 — stream text to stdout in interactive mode
318
+ // AC: @cli-agent-commands ac-11 — suppress streaming in --json mode
319
+ let didStream = false;
320
+ const onUpdate = isJsonMode()
321
+ ? undefined
322
+ : (update) => {
323
+ if (update.sessionUpdate === "agent_message_chunk" &&
324
+ update.content.type === "text") {
325
+ process.stdout.write(update.content.text);
326
+ didStream = true;
327
+ }
328
+ };
329
+ // AC: @cli-agent-commands ac-3 - no task binding when --task not provided
330
+ // Pass basePrompt (not fullPromptForPreview) — runInvocation expands skills internally
331
+ const result = await runInvocation({
332
+ agent: effectiveAgent,
333
+ specDir: ctx.specDir,
334
+ cwd: ctx.rootDir,
335
+ taskRef: taskRef ?? undefined,
336
+ prompt: basePrompt,
337
+ trigger: "manual",
338
+ onUpdate,
339
+ });
340
+ // Ensure summary starts on its own line after streamed content
341
+ if (didStream)
342
+ process.stdout.write("\n");
343
+ output({
344
+ outcome: result.outcome,
345
+ session_id: result.session.id,
346
+ duration_ms: result.durationMs,
347
+ stop_reason: result.stopReason,
348
+ }, () => {
349
+ if (result.outcome === "success") {
350
+ success(`Agent invocation completed`, {
351
+ session: result.session.id,
352
+ duration: `${Math.round(result.durationMs / 1000)}s`,
353
+ });
354
+ }
355
+ else {
356
+ error(`Agent invocation ${result.outcome}: ${result.error ?? "unknown"}`);
357
+ }
358
+ });
359
+ if (result.outcome !== "success") {
360
+ process.exit(EXIT_CODES.ERROR);
361
+ }
362
+ }
363
+ catch (err) {
364
+ error("Failed to run agent", err);
365
+ process.exit(EXIT_CODES.ERROR);
366
+ }
367
+ });
368
+ // ─── kspec agent status ───────────────────────────────────────────────────
369
+ // AC: @cli-agent-commands ac-6
370
+ agent
371
+ .command("status")
372
+ .description("Show active and queued agent invocations")
373
+ .option("--json", "Output as JSON")
374
+ .action(async (opts) => {
375
+ try {
376
+ const daemonConn = getDaemonUrl();
377
+ // AC: @trait-error-guidance ac-1, ac-2
378
+ if (!daemonConn) {
379
+ error("Daemon is not running. Cannot retrieve agent status.", { suggestion: "Start the daemon with: kspec serve" });
380
+ process.exit(EXIT_CODES.ERROR);
381
+ }
382
+ const ctx = await initContext();
383
+ const headers = {};
384
+ if (ctx.rootDir) {
385
+ headers["X-Kspec-Dir"] = ctx.rootDir;
386
+ }
387
+ const response = await fetch(`${daemonConn.url}/api/agent/dispatch/status`, { headers });
388
+ if (!response.ok) {
389
+ error(`Daemon returned error: ${response.status} ${response.statusText}`);
390
+ process.exit(EXIT_CODES.ERROR);
391
+ }
392
+ // AC: @cli-agent-commands ac-6
393
+ const data = await response.json();
394
+ output(data, () => {
395
+ console.log(chalk.bold("Agent Status"));
396
+ console.log();
397
+ console.log(` Dispatch engine: ${data.running ? chalk.green("running") : chalk.gray("stopped")}`);
398
+ console.log(` Active invocations: ${chalk.cyan(String(data.activeInvocations))}`);
399
+ console.log(` Queued invocations: ${chalk.cyan(String(data.queuedInvocations))}`);
400
+ const invocations = data.invocations ?? [];
401
+ if (invocations.length > 0) {
402
+ console.log();
403
+ console.log(chalk.bold("Active:"));
404
+ for (const inv of invocations) {
405
+ const elapsed = Math.round(inv.elapsedMs / 1000);
406
+ const taskStr = inv.taskRef ? ` task: ${chalk.yellow(inv.taskRef)}` : "";
407
+ console.log(` ${chalk.cyan(inv.agentId)} ${chalk.gray(inv.agentName)}`);
408
+ console.log(` session: ${chalk.gray(inv.sessionId)} elapsed: ${elapsed}s${taskStr}`);
409
+ }
410
+ }
411
+ const queuedItems = data.queued ?? [];
412
+ if (queuedItems.length > 0) {
413
+ console.log();
414
+ console.log(chalk.bold("Queued:"));
415
+ for (const q of queuedItems) {
416
+ const wait = Math.round(q.waitMs / 1000);
417
+ const taskStr = q.taskRef ? ` task: ${chalk.yellow(q.taskRef)}` : "";
418
+ console.log(` ${chalk.cyan(q.agentId)} ${chalk.gray(q.agentName)}`);
419
+ console.log(` waiting: ${wait}s${taskStr}`);
420
+ }
421
+ }
422
+ });
423
+ }
424
+ catch (err) {
425
+ error("Failed to get agent status", err);
426
+ process.exit(EXIT_CODES.ERROR);
427
+ }
428
+ });
429
+ // ─── kspec agent dispatch ─────────────────────────────────────────────────
430
+ const dispatch = agent
431
+ .command("dispatch")
432
+ .description("Manage the agent dispatch engine");
433
+ // AC: @cli-agent-commands ac-4
434
+ // AC: @cli-agent-commands ac-10
435
+ dispatch
436
+ .command("start")
437
+ .description("Start the dispatch engine (daemon must be running)")
438
+ .option("--json", "Output as JSON")
439
+ .action(async (opts) => {
440
+ try {
441
+ const daemonConn = getDaemonUrl();
442
+ // AC: @cli-agent-commands ac-10 - error when daemon not running
443
+ if (!daemonConn) {
444
+ error("Daemon is not running. The dispatch engine requires the daemon.", { suggestion: "Start the daemon first with: kspec serve" });
445
+ process.exit(EXIT_CODES.ERROR);
446
+ }
447
+ const ctx = await initContext();
448
+ const headers = {
449
+ "Content-Type": "application/json",
450
+ };
451
+ if (ctx.rootDir) {
452
+ headers["X-Kspec-Dir"] = ctx.rootDir;
453
+ }
454
+ const response = await fetch(`${daemonConn.url}/api/agent/dispatch/start`, {
455
+ method: "POST",
456
+ headers,
457
+ });
458
+ if (!response.ok) {
459
+ const body = await response.text();
460
+ error(`Failed to start dispatch engine: ${response.status} - ${body}`);
461
+ process.exit(EXIT_CODES.ERROR);
462
+ }
463
+ const data = await response.json();
464
+ output(data, () => {
465
+ if (data.started) {
466
+ success("Dispatch engine started");
467
+ }
468
+ else {
469
+ console.log(chalk.yellow(`Dispatch engine: ${data.reason ?? "already running"}`));
470
+ }
471
+ });
472
+ }
473
+ catch (err) {
474
+ error("Failed to start dispatch engine", err);
475
+ process.exit(EXIT_CODES.ERROR);
476
+ }
477
+ });
478
+ // AC: @cli-agent-commands ac-5
479
+ dispatch
480
+ .command("stop")
481
+ .description("Stop the dispatch engine gracefully")
482
+ .option("--json", "Output as JSON")
483
+ .action(async (opts) => {
484
+ try {
485
+ const daemonConn = getDaemonUrl();
486
+ if (!daemonConn) {
487
+ error("Daemon is not running.", { suggestion: "Start the daemon with: kspec serve" });
488
+ process.exit(EXIT_CODES.ERROR);
489
+ }
490
+ const ctx = await initContext();
491
+ const headers = {
492
+ "Content-Type": "application/json",
493
+ };
494
+ if (ctx.rootDir) {
495
+ headers["X-Kspec-Dir"] = ctx.rootDir;
496
+ }
497
+ const response = await fetch(`${daemonConn.url}/api/agent/dispatch/stop`, {
498
+ method: "POST",
499
+ headers,
500
+ });
501
+ if (!response.ok) {
502
+ const body = await response.text();
503
+ error(`Failed to stop dispatch engine: ${response.status} - ${body}`);
504
+ process.exit(EXIT_CODES.ERROR);
505
+ }
506
+ const data = await response.json();
507
+ output(data, () => {
508
+ if (data.stopped) {
509
+ success("Dispatch engine stopped");
510
+ }
511
+ else {
512
+ console.log(chalk.yellow(`Dispatch engine: ${data.reason ?? "not running"}`));
513
+ }
514
+ });
515
+ }
516
+ catch (err) {
517
+ error("Failed to stop dispatch engine", err);
518
+ process.exit(EXIT_CODES.ERROR);
519
+ }
520
+ });
521
+ // AC: @cli-agent-commands ac-9
522
+ dispatch
523
+ .command("status")
524
+ .description("Show dispatch engine status and loaded agents")
525
+ .option("--json", "Output as JSON")
526
+ .action(async (opts) => {
527
+ try {
528
+ const daemonConn = getDaemonUrl();
529
+ if (!daemonConn) {
530
+ // Daemon not running — show as disabled
531
+ output({ running: false, activeInvocations: 0, queuedInvocations: 0, agents: [] }, () => {
532
+ console.log(chalk.bold("Dispatch Status"));
533
+ console.log();
534
+ console.log(` Dispatch engine: ${chalk.gray("not available (daemon offline)")}`);
535
+ console.log(chalk.gray(" Start daemon with: kspec serve"));
536
+ });
537
+ return;
538
+ }
539
+ const ctx = await initContext();
540
+ const headers = {};
541
+ if (ctx.rootDir) {
542
+ headers["X-Kspec-Dir"] = ctx.rootDir;
543
+ }
544
+ // Get dispatch status
545
+ const statusResponse = await fetch(`${daemonConn.url}/api/agent/dispatch/status`, { headers });
546
+ if (!statusResponse.ok) {
547
+ error(`Daemon returned error: ${statusResponse.status}`);
548
+ process.exit(EXIT_CODES.ERROR);
549
+ }
550
+ const statusData = await statusResponse.json();
551
+ // Get loaded agents
552
+ let agents = [];
553
+ try {
554
+ const meta = await loadMetaContext(ctx);
555
+ agents = meta.agents.map((a) => ({ id: a.id, name: a.name }));
556
+ }
557
+ catch {
558
+ // Meta may not be available
559
+ }
560
+ const fullData = { ...statusData, agents };
561
+ output(fullData, () => {
562
+ console.log(chalk.bold("Dispatch Status"));
563
+ console.log();
564
+ console.log(` Engine: ${statusData.running ? chalk.green("enabled") : chalk.yellow("disabled")}`);
565
+ console.log(` Active invocations: ${chalk.cyan(String(statusData.activeInvocations))}`);
566
+ console.log(` Queued invocations: ${chalk.cyan(String(statusData.queuedInvocations))}`);
567
+ console.log();
568
+ console.log(chalk.bold("Loaded Agents:"));
569
+ if (agents.length === 0) {
570
+ console.log(chalk.gray(" (none defined)"));
571
+ }
572
+ else {
573
+ for (const a of agents) {
574
+ console.log(` ${chalk.cyan(a.id)} ${chalk.gray(a.name)}`);
575
+ }
576
+ }
577
+ });
578
+ }
579
+ catch (err) {
580
+ error("Failed to get dispatch status", err);
581
+ process.exit(EXIT_CODES.ERROR);
582
+ }
583
+ });
584
+ // ─── kspec agent dispatch watch ───────────────────────────────────────────
585
+ // AC: @cli-agent-commands ac-13 through ac-18
586
+ dispatch
587
+ .command("watch")
588
+ .description("Stream agent text output from the dispatch engine in real time")
589
+ .option("--agent <name>", "Only show output from this agent")
590
+ .option("--session <id>", "Only show output from this session")
591
+ .option("--retries <n>", "Number of reconnect attempts on disconnect (default 5)", "5")
592
+ .action(async (opts) => {
593
+ const DEFAULT_RETRIES = 5;
594
+ const RETRY_BASE_MS = 1000;
595
+ const MAX_RETRY_MS = 30_000;
596
+ // AC: @cli-agent-commands ac-15 — error when daemon not running
597
+ const daemonConn = getDaemonUrl();
598
+ if (!daemonConn) {
599
+ error("Daemon is not running. The watch command requires the daemon.");
600
+ info("Suggestion: Start the daemon with: kspec serve");
601
+ // AC: @cli-agent-commands ac-15 — exit code 3
602
+ process.exit(EXIT_CODES.NOT_FOUND);
603
+ return;
604
+ }
605
+ const parsedRetries = parseIntOption(opts.retries, {
606
+ min: 0,
607
+ max: Number.MAX_SAFE_INTEGER,
608
+ name: "Retries",
609
+ });
610
+ if (!parsedRetries.ok) {
611
+ error(`Invalid --retries value: ${parsedRetries.error}`, {
612
+ suggestion: "Example: --retries 5",
613
+ });
614
+ process.exit(EXIT_CODES.VALIDATION_FAILED);
615
+ return;
616
+ }
617
+ const retryLimit = parsedRetries.value;
618
+ const agentFilter = opts.agent;
619
+ const sessionFilter = opts.session;
620
+ const streamStates = new Map();
621
+ let activeStreamKey = null;
622
+ let outputAtLineStart = true;
623
+ function getStreamState(streamKey) {
624
+ let state = streamStates.get(streamKey);
625
+ if (!state) {
626
+ state = { hasRenderedBody: false, spacerPending: false };
627
+ streamStates.set(streamKey, state);
628
+ }
629
+ return state;
630
+ }
631
+ function writeRaw(text) {
632
+ if (!text)
633
+ return;
634
+ process.stdout.write(text);
635
+ outputAtLineStart = text.endsWith("\n");
636
+ }
637
+ function writeSpeakerText(text) {
638
+ if (!text)
639
+ return;
640
+ writeRaw(text);
641
+ }
642
+ function ensureLineBreak() {
643
+ if (!outputAtLineStart) {
644
+ writeRaw("\n");
645
+ }
646
+ }
647
+ function startSpeakerSection(streamKey, prefix) {
648
+ if (activeStreamKey === streamKey)
649
+ return;
650
+ if (activeStreamKey) {
651
+ ensureLineBreak();
652
+ }
653
+ writeRaw(`${prefix}\n`);
654
+ activeStreamKey = streamKey;
655
+ }
656
+ function queuePrefixedChunk(streamKey, prefix, text) {
657
+ if (!text)
658
+ return;
659
+ const switchingSpeaker = activeStreamKey !== null && activeStreamKey !== streamKey;
660
+ startSpeakerSection(streamKey, prefix);
661
+ const state = getStreamState(streamKey);
662
+ if (switchingSpeaker) {
663
+ // Marker change already separates context; don't carry stale spacer
664
+ // into the top of a newly active speaker section.
665
+ state.spacerPending = false;
666
+ }
667
+ else if (state.spacerPending && state.hasRenderedBody) {
668
+ ensureLineBreak();
669
+ writeRaw("\n");
670
+ state.spacerPending = false;
671
+ }
672
+ writeSpeakerText(text);
673
+ state.hasRenderedBody = true;
674
+ }
675
+ function markMessageBoundary(streamKey) {
676
+ // Boundary signals are stream-local; ignore inactive streams.
677
+ if (activeStreamKey !== streamKey)
678
+ return;
679
+ const state = getStreamState(streamKey);
680
+ ensureLineBreak();
681
+ // Coalesce repeated boundary events into one pending spacer.
682
+ if (state.hasRenderedBody) {
683
+ state.spacerPending = true;
684
+ }
685
+ }
686
+ function formatSessionIdForDisplay(sessionId) {
687
+ // AC: @cli-agent-commands ac-17 — shorten session ULID in watch prefix.
688
+ return sessionId ? sessionId.slice(0, 8) : "";
689
+ }
690
+ // Resolve project dir for WebSocket project binding
691
+ let projectDir;
692
+ try {
693
+ const ctx = await initContext();
694
+ projectDir = ctx.rootDir;
695
+ }
696
+ catch {
697
+ // non-fatal: WebSocket will use daemon default
698
+ }
699
+ let retryCount = 0;
700
+ let shouldReconnect = true;
701
+ function connect() {
702
+ const wsUrl = new URL(`ws://localhost:${daemonConn.port}/ws`);
703
+ if (projectDir) {
704
+ wsUrl.searchParams.set("project", projectDir);
705
+ }
706
+ // AC: @cli-agent-commands ac-15 — Node 22+ has global WebSocket
707
+ const ws = new WebSocket(wsUrl.toString());
708
+ ws.onopen = () => {
709
+ retryCount = 0;
710
+ // Subscribe to agents topic
711
+ ws.send(JSON.stringify({
712
+ action: "subscribe",
713
+ request_id: "watch-subscribe",
714
+ payload: { topics: ["agents"] },
715
+ }));
716
+ };
717
+ ws.onmessage = (event) => {
718
+ let msg;
719
+ try {
720
+ msg = JSON.parse(event.data);
721
+ }
722
+ catch {
723
+ return;
724
+ }
725
+ // AC: @cli-agent-commands ac-18 — fail fast on subscribe handshake rejection.
726
+ if (msg.ack === true && msg.request_id === "watch-subscribe") {
727
+ if (msg.success === false) {
728
+ shouldReconnect = false;
729
+ const reasonParts = [msg.error, msg.details]
730
+ .filter((value) => typeof value === "string" && value.length > 0);
731
+ const reason = reasonParts.length > 0 ? ` (${reasonParts.join(": ")})` : "";
732
+ if (activeStreamKey) {
733
+ ensureLineBreak();
734
+ }
735
+ error(`Failed to subscribe to daemon agent output stream${reason}.`);
736
+ info("Suggestion: Verify daemon logs for subscribe errors, then restart with: kspec serve");
737
+ process.exit(EXIT_CODES.NOT_FOUND);
738
+ }
739
+ return;
740
+ }
741
+ // AC: @cli-agent-commands ac-13 — stream text with per-line [agent-id session-id] prefixes
742
+ if (msg.event === "agent_text_chunk" && msg.data) {
743
+ const data = msg.data;
744
+ const sessionId = data.session_id ?? "";
745
+ const agentId = data.agent_id ?? "";
746
+ const text = data.text ?? "";
747
+ // AC: @cli-agent-commands ac-16 — filter by agent/session if specified
748
+ if (agentFilter && agentId !== agentFilter)
749
+ return;
750
+ if (sessionFilter && sessionId !== sessionFilter)
751
+ return;
752
+ const streamKey = `${agentId}\u0000${sessionId}`;
753
+ const displaySessionId = formatSessionIdForDisplay(sessionId);
754
+ const prefix = `[${agentId} ${displaySessionId}]`;
755
+ // Match old Ralph rendering semantics: empty chunk marks message end.
756
+ if (text.length === 0) {
757
+ markMessageBoundary(streamKey);
758
+ return;
759
+ }
760
+ queuePrefixedChunk(streamKey, prefix, text);
761
+ }
762
+ };
763
+ ws.onerror = () => {
764
+ // error event fires before close, handled in onclose
765
+ };
766
+ ws.onclose = () => {
767
+ if (!shouldReconnect)
768
+ return;
769
+ if (activeStreamKey) {
770
+ ensureLineBreak();
771
+ }
772
+ if (retryCount >= retryLimit) {
773
+ // AC: @cli-agent-commands ac-14 — exit code 3 when retries exhausted
774
+ error(`WebSocket connection lost and reconnection failed after ${retryLimit} attempt(s).`);
775
+ info("Suggestion: Verify the daemon is still running with: kspec serve");
776
+ process.exit(EXIT_CODES.NOT_FOUND);
777
+ return;
778
+ }
779
+ retryCount++;
780
+ const backoffMs = Math.min(RETRY_BASE_MS * Math.pow(2, retryCount - 1), MAX_RETRY_MS);
781
+ // AC: @cli-agent-commands ac-14 — print reconnecting message
782
+ process.stderr.write(`[watch] Connection lost. Reconnecting in ${Math.round(backoffMs / 1000)}s (attempt ${retryCount}/${retryLimit})...\n`);
783
+ setTimeout(connect, backoffMs);
784
+ };
785
+ }
786
+ connect();
787
+ // Keep process alive (WebSocket is non-blocking in Node)
788
+ // Users interrupt with Ctrl+C
789
+ await new Promise(() => { });
790
+ });
791
+ // ─── kspec agent end-loop ─────────────────────────────────────────────────
792
+ // AC: @ralph-replacement ac-1 — equivalent to kspec ralph end-loop
793
+ // AC: @session-end-loop-signal ac-signal
794
+ agent
795
+ .command("end-loop")
796
+ .description("Signal the agent dispatch engine to stop after current iteration")
797
+ .option("--reason <reason>", "Reason for ending the loop")
798
+ .action(async (options) => {
799
+ try {
800
+ const ctx = await initContext();
801
+ const sessionId = process.env.KSPEC_SESSION_ID;
802
+ if (!sessionId) {
803
+ // AC: @trait-error-guidance ac-1, ac-2
804
+ warn("No active agent session detected (KSPEC_SESSION_ID not set).");
805
+ info("This command requires an active session. It is designed to be called by agents during a dispatch invocation.");
806
+ info("Suggestion: Ensure KSPEC_SESSION_ID is set, or start a session with: kspec session create");
807
+ process.exit(EXIT_CODES.VALIDATION_FAILED);
808
+ return;
809
+ }
810
+ // Write end-loop state to session
811
+ const updated = await requestEndLoop(ctx.specDir, sessionId, options.reason);
812
+ if (!updated) {
813
+ // AC: @trait-error-guidance ac-1, ac-2
814
+ error(`Session not found: ${sessionId}`);
815
+ info("Suggestion: Check session ID with: kspec session log list");
816
+ process.exit(EXIT_CODES.NOT_FOUND);
817
+ return;
818
+ }
819
+ success("Loop end signal sent");
820
+ if (options.reason) {
821
+ info(`Reason: ${options.reason}`);
822
+ }
823
+ }
824
+ catch (err) {
825
+ // AC: @trait-error-guidance ac-1
826
+ error("Failed to signal end-loop", err);
827
+ process.exit(EXIT_CODES.ERROR);
828
+ }
829
+ });
830
+ }
831
+ //# sourceMappingURL=agent.js.map