@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,147 @@
1
+ /**
2
+ * Plans API Routes
3
+ *
4
+ * REST endpoints for plan operations:
5
+ * - GET /api/plans - list plans with status filter and task progress
6
+ * - GET /api/plans/:ref - get single plan with content
7
+ *
8
+ * AC Coverage:
9
+ * - @ui-plans-view ac-1: Plan list with title, status, dates, linked counts, progress
10
+ * - @ui-plans-view ac-2: Plan detail with content for expand/detail view
11
+ */
12
+
13
+ import { Elysia, t } from 'elysia';
14
+ import {
15
+ initContext,
16
+ loadPlans,
17
+ loadAllTasks,
18
+ findPlanByRef,
19
+ type LoadedPlan,
20
+ } from '../../parser/index.js';
21
+ import type { PlanSummary, PlanDetail } from '@kynetic-ai/shared';
22
+
23
+ interface PlansRouteOptions {}
24
+
25
+ /**
26
+ * Build a task status lookup map from loaded tasks.
27
+ */
28
+ function buildTaskStatusMap(tasks: Array<{ _ulid: string; slugs: string[]; status: string }>) {
29
+ const tasksByRef = new Map<string, { status: string }>();
30
+ for (const task of tasks) {
31
+ tasksByRef.set(task._ulid, { status: task.status });
32
+ for (const slug of task.slugs) {
33
+ tasksByRef.set(slug, { status: task.status });
34
+ }
35
+ }
36
+ return tasksByRef;
37
+ }
38
+
39
+ /**
40
+ * Compute task progress for a plan's derived tasks.
41
+ */
42
+ function computeTaskProgress(
43
+ derivedTasks: string[],
44
+ tasksByRef: Map<string, { status: string }>
45
+ ) {
46
+ const progress = { total: 0, completed: 0, in_progress: 0, pending: 0, blocked: 0 };
47
+ for (const ref of derivedTasks) {
48
+ const cleanRef = ref.startsWith('@') ? ref.slice(1) : ref;
49
+ const task = tasksByRef.get(cleanRef);
50
+ if (task) {
51
+ progress.total++;
52
+ if (task.status === 'completed') progress.completed++;
53
+ else if (task.status === 'in_progress' || task.status === 'pending_review' || task.status === 'needs_work') progress.in_progress++;
54
+ else if (task.status === 'blocked') progress.blocked++;
55
+ else progress.pending++;
56
+ }
57
+ }
58
+ return progress;
59
+ }
60
+
61
+ /**
62
+ * Map a loaded plan to a PlanSummary.
63
+ */
64
+ function toPlanSummary(
65
+ plan: LoadedPlan,
66
+ tasksByRef: Map<string, { status: string }>
67
+ ): PlanSummary {
68
+ return {
69
+ _ulid: plan._ulid,
70
+ slugs: plan.slugs,
71
+ title: plan.title,
72
+ status: plan.status,
73
+ created_at: plan.created_at,
74
+ approved_at: plan.approved_at ?? undefined,
75
+ completed_at: plan.completed_at ?? undefined,
76
+ derived_specs: plan.derived_specs,
77
+ derived_tasks: plan.derived_tasks,
78
+ spec_count: plan.derived_specs.length,
79
+ task_count: plan.derived_tasks.length,
80
+ task_progress: computeTaskProgress(plan.derived_tasks, tasksByRef),
81
+ };
82
+ }
83
+
84
+ export function createPlansRoutes(options: PlansRouteOptions = {}) {
85
+ return new Elysia({ prefix: '/api/plans' })
86
+ // AC: @ui-plans-view ac-1 - List plans with progress
87
+ .get(
88
+ '/',
89
+ async ({ query, projectContext }) => {
90
+ const ctx = await initContext(projectContext.path);
91
+ const plans = await loadPlans(ctx);
92
+ const tasks = await loadAllTasks(ctx);
93
+ const tasksByRef = buildTaskStatusMap(tasks);
94
+
95
+ // Apply status filter
96
+ let filtered: LoadedPlan[] = plans;
97
+ if (query.status) {
98
+ const statusFilters = Array.isArray(query.status) ? query.status : [query.status];
99
+ filtered = filtered.filter((plan) => statusFilters.includes(plan.status));
100
+ }
101
+
102
+ // Sort by created_at descending (newest first)
103
+ const sorted = [...filtered].sort(
104
+ (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
105
+ );
106
+
107
+ // AC: @ui-plans-view ac-1 - Compute progress for each plan
108
+ const items: PlanSummary[] = sorted.map((plan) => toPlanSummary(plan, tasksByRef));
109
+
110
+ return {
111
+ items,
112
+ total: items.length,
113
+ };
114
+ },
115
+ {
116
+ query: t.Object({
117
+ status: t.Optional(t.Union([t.String(), t.Array(t.String())])),
118
+ }),
119
+ }
120
+ )
121
+ // AC: @ui-plans-view ac-2 - Get single plan with content (lazy-loaded by UI on expand)
122
+ .get(
123
+ '/:ref',
124
+ async ({ params, error: errorResponse, projectContext }) => {
125
+ const ctx = await initContext(projectContext.path);
126
+ const plan = await findPlanByRef(ctx, params.ref);
127
+
128
+ if (!plan) {
129
+ return errorResponse(404, {
130
+ error: 'not_found',
131
+ message: `Plan reference "${params.ref}" not found`,
132
+ suggestion: 'Use kspec plan list to find valid plan references',
133
+ });
134
+ }
135
+
136
+ const tasks = await loadAllTasks(ctx);
137
+ const tasksByRef = buildTaskStatusMap(tasks);
138
+
139
+ const detail: PlanDetail = {
140
+ ...toPlanSummary(plan, tasksByRef),
141
+ content: plan.content,
142
+ };
143
+
144
+ return detail;
145
+ }
146
+ );
147
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Session API Routes
3
+ *
4
+ * REST endpoints for session data:
5
+ * - GET /api/sessions - list sessions with summaries
6
+ * - GET /api/sessions/:id - get session metadata and detail
7
+ * - GET /api/sessions/:id/events - get session events from events.jsonl
8
+ *
9
+ * AC Coverage:
10
+ * - @ui-session-stream ac-1: Session events as structured blocks
11
+ * - @ui-session-stream ac-4: Session metadata, spec context, budget for context panel
12
+ */
13
+
14
+ import { Elysia, t } from 'elysia';
15
+ import {
16
+ getSession,
17
+ listSessions,
18
+ readEvents,
19
+ deduplicatePhasedToolCalls,
20
+ getSessionLogSummary,
21
+ getAllSessionLogSummaries,
22
+ resolveSessionId,
23
+ getBudget,
24
+ } from '../../sessions/store.js';
25
+ import {
26
+ initContext,
27
+ loadAllTasks,
28
+ loadAllItems,
29
+ ReferenceIndex,
30
+ } from '../../parser/index.js';
31
+
32
+ export function createSessionRoutes() {
33
+ return new Elysia({ prefix: '/api/sessions' })
34
+
35
+ // List all sessions with summaries
36
+ .get('/', async ({ projectContext }) => {
37
+ const ctx = await initContext(projectContext.path);
38
+ const summaries = await getAllSessionLogSummaries(ctx.specDir);
39
+
40
+ // Sort by started_at descending (most recent first)
41
+ summaries.sort((a, b) =>
42
+ new Date(b.started_at).getTime() - new Date(a.started_at).getTime()
43
+ );
44
+
45
+ return {
46
+ items: summaries,
47
+ total: summaries.length,
48
+ };
49
+ })
50
+
51
+ // Get single session metadata
52
+ // AC: @ui-session-stream ac-4 — Includes spec context, budget, and task info
53
+ .get('/:id', async ({ params, error: errorResponse, projectContext }) => {
54
+ const ctx = await initContext(projectContext.path);
55
+
56
+ // Resolve session ID (supports prefix matching)
57
+ const resolution = await resolveSessionId(ctx.specDir, params.id);
58
+ if (!resolution.ok) {
59
+ if (resolution.error === 'ambiguous') {
60
+ return errorResponse(400, {
61
+ error: 'ambiguous_id',
62
+ message: `Ambiguous session ID: ${params.id} matches ${resolution.matches.length} sessions`,
63
+ suggestion: 'Provide a longer prefix to uniquely identify the session',
64
+ });
65
+ }
66
+ return errorResponse(404, {
67
+ error: 'not_found',
68
+ message: `Session not found: ${params.id}`,
69
+ suggestion: 'Use GET /api/sessions to list available sessions',
70
+ });
71
+ }
72
+
73
+ const detail = await getSessionLogSummary(ctx.specDir, resolution.id);
74
+ if (!detail) {
75
+ return errorResponse(404, {
76
+ error: 'not_found',
77
+ message: `Session not found: ${params.id}`,
78
+ suggestion: 'Use GET /api/sessions to list available sessions',
79
+ });
80
+ }
81
+
82
+ const metadata = await getSession(ctx.specDir, resolution.id);
83
+
84
+ // AC: @ui-session-stream ac-4 — Resolve spec context from task's spec_ref
85
+ let spec_context: {
86
+ spec_ref: string;
87
+ title: string;
88
+ acceptance_criteria: Array<{ id: string; description: string }>;
89
+ } | null = null;
90
+
91
+ if (metadata?.task_id) {
92
+ try {
93
+ const tasks = await loadAllTasks(ctx);
94
+ const items = await loadAllItems(ctx);
95
+ const index = new ReferenceIndex(tasks, items);
96
+ const taskResult = index.resolve(metadata.task_id);
97
+ if (taskResult.ok) {
98
+ const task = taskResult.item as { spec_ref?: string };
99
+ if (task.spec_ref) {
100
+ const specResult = index.resolve(task.spec_ref);
101
+ if (specResult.ok) {
102
+ const specItem = specResult.item as {
103
+ title: string;
104
+ acceptance_criteria?: Array<{ description?: string; given?: string }>;
105
+ };
106
+ spec_context = {
107
+ spec_ref: task.spec_ref,
108
+ title: specItem.title,
109
+ acceptance_criteria: (specItem.acceptance_criteria ?? []).map((ac, i) => ({
110
+ id: `ac-${i + 1}`,
111
+ description: ac.description ?? ac.given ?? '',
112
+ })),
113
+ };
114
+ }
115
+ }
116
+ }
117
+ } catch {
118
+ // Non-critical — spec context is optional
119
+ }
120
+ }
121
+
122
+ // AC: @ui-session-stream ac-4 — Include budget info
123
+ let budget: { max_per_cycle: number; started_this_cycle: number } | null = null;
124
+ try {
125
+ budget = await getBudget(ctx.specDir, resolution.id);
126
+ } catch {
127
+ // No budget configured — that's fine
128
+ }
129
+
130
+ return {
131
+ ...detail,
132
+ task_id: metadata?.task_id,
133
+ agent_id: metadata?.agent_id,
134
+ trigger: metadata?.trigger ?? 'legacy',
135
+ spec_context,
136
+ budget,
137
+ };
138
+ })
139
+
140
+ // Get session events
141
+ .get('/:id/events', async ({ params, query, error: errorResponse, projectContext }) => {
142
+ const ctx = await initContext(projectContext.path);
143
+
144
+ const resolution = await resolveSessionId(ctx.specDir, params.id);
145
+ if (!resolution.ok) {
146
+ if (resolution.error === 'ambiguous') {
147
+ return errorResponse(400, {
148
+ error: 'ambiguous_id',
149
+ message: `Ambiguous session ID: ${params.id} matches ${resolution.matches.length} sessions`,
150
+ suggestion: 'Provide a longer prefix to uniquely identify the session',
151
+ });
152
+ }
153
+ return errorResponse(404, {
154
+ error: 'not_found',
155
+ message: `Session not found: ${params.id}`,
156
+ suggestion: 'Use GET /api/sessions to list available sessions',
157
+ });
158
+ }
159
+
160
+ let events = await readEvents(ctx.specDir, resolution.id);
161
+
162
+ // Deduplicate phased tool calls
163
+ events = deduplicatePhasedToolCalls(events);
164
+
165
+ // Filter by since_seq if provided (for incremental loading)
166
+ const sinceSeq = query.since_seq !== undefined ? parseInt(query.since_seq, 10) : undefined;
167
+ if (sinceSeq !== undefined && !isNaN(sinceSeq)) {
168
+ events = events.filter(e => e.seq > sinceSeq);
169
+ }
170
+
171
+ return {
172
+ events,
173
+ total: events.length,
174
+ };
175
+ }, {
176
+ query: t.Object({
177
+ since_seq: t.Optional(t.String()),
178
+ }),
179
+ });
180
+ }
@@ -14,6 +14,9 @@
14
14
  * - ac-5: GET /api/tasks/:ref resolves via ReferenceIndex
15
15
  * - ac-6: POST /api/tasks/:ref/start transitions state
16
16
  * - ac-7: POST /api/tasks/:ref/note appends note
17
+ * - @ui-task-board ac-6: POST /api/tasks/:ref/submit transitions to pending_review
18
+ * - @ui-task-board ac-6: POST /api/tasks/:ref/complete transitions to completed
19
+ * - @ui-task-board ac-6: POST /api/tasks/:ref/block transitions to blocked
17
20
  */
18
21
 
19
22
  import { Elysia, t } from 'elysia';
@@ -21,6 +24,7 @@ import {
21
24
  initContext,
22
25
  loadAllTasks,
23
26
  loadAllItems,
27
+ loadPlans,
24
28
  ReferenceIndex,
25
29
  createNote,
26
30
  saveTask,
@@ -72,6 +76,31 @@ export function createTasksRoutes(options: TasksRouteOptions) {
72
76
  );
73
77
  }
74
78
 
79
+ // Automation filter — filter by automation eligibility status
80
+ if (query.automation) {
81
+ filtered = filtered.filter((task) => task.automation === query.automation);
82
+ }
83
+
84
+ // Plan filter — show only tasks derived from a given plan
85
+ if (query.plan) {
86
+ const plans = await loadPlans(ctx);
87
+ const plan = plans.find(
88
+ (p) => p._ulid === query.plan || p.slugs.includes(query.plan!)
89
+ );
90
+ if (plan) {
91
+ const derivedRefs = new Set(
92
+ plan.derived_tasks.map((r) => (r.startsWith('@') ? r.slice(1) : r))
93
+ );
94
+ filtered = filtered.filter(
95
+ (task) =>
96
+ derivedRefs.has(task._ulid) ||
97
+ task.slugs.some((s) => derivedRefs.has(s))
98
+ );
99
+ } else {
100
+ filtered = [];
101
+ }
102
+ }
103
+
75
104
  // AC: @api-contract ac-4 - Pagination
76
105
  const total = filtered.length;
77
106
  const offset = Number(query.offset) || 0;
@@ -85,12 +114,14 @@ export function createTasksRoutes(options: TasksRouteOptions) {
85
114
  _ulid: task._ulid,
86
115
  slugs: task.slugs,
87
116
  title: task.title,
117
+ type: task.type || 'task',
88
118
  status: task.status,
89
119
  priority: task.priority,
90
120
  spec_ref: task.spec_ref,
91
121
  meta_ref: task.meta_ref,
92
122
  tags: task.tags,
93
123
  depends_on: task.depends_on || [],
124
+ automation: task.automation,
94
125
  notes_count: task.notes?.length || 0,
95
126
  todos_count: task.todos?.length || 0,
96
127
  started_at: task.started_at,
@@ -111,6 +142,8 @@ export function createTasksRoutes(options: TasksRouteOptions) {
111
142
  status: t.Optional(t.Union([t.String(), t.Array(t.String())])),
112
143
  type: t.Optional(t.Union([t.String(), t.Array(t.String())])),
113
144
  tag: t.Optional(t.Union([t.String(), t.Array(t.String())])),
145
+ automation: t.Optional(t.String()),
146
+ plan: t.Optional(t.String()),
114
147
  limit: t.Optional(t.String()),
115
148
  offset: t.Optional(t.String()),
116
149
  }),
@@ -150,19 +183,31 @@ export function createTasksRoutes(options: TasksRouteOptions) {
150
183
  }
151
184
 
152
185
  // AC: @api-contract ac-5 - Return full task with notes, todos, dependencies
186
+ // AC: @ui-task-board ac-3 - Include type, description, blocked_by, vcs_refs, plan_ref, session_ref
153
187
  return {
154
188
  _ulid: task._ulid,
155
189
  slugs: task.slugs,
156
190
  title: task.title,
191
+ type: task.type || 'task',
157
192
  status: task.status,
158
193
  priority: task.priority,
159
194
  spec_ref: task.spec_ref,
160
195
  meta_ref: task.meta_ref,
161
196
  tags: task.tags,
162
197
  description: task.description,
198
+ derivation: task.derivation,
163
199
  depends_on: task.depends_on,
200
+ blocked_by: task.blocked_by || [],
201
+ context: task.context || [],
202
+ vcs_refs: (task.vcs_refs || []).map((v) =>
203
+ typeof v === 'string' ? v : v.type ? `${v.type}:${v.ref}` : v.ref
204
+ ),
205
+ plan_ref: task.plan_ref,
206
+ session_ref: task.session_id,
164
207
  notes: task.notes,
208
+ notes_count: task.notes?.length || 0,
165
209
  todos: task.todos,
210
+ todos_count: task.todos?.length || 0,
166
211
  started_at: task.started_at,
167
212
  completed_at: task.completed_at,
168
213
  cancelled_at: task.cancelled_at,
@@ -323,5 +368,158 @@ export function createTasksRoutes(options: TasksRouteOptions) {
323
368
  content: t.String(),
324
369
  }),
325
370
  }
371
+ )
372
+
373
+ // AC: @ui-task-board ac-6 - Submit task for review
374
+ .post(
375
+ '/:ref/submit',
376
+ async ({ params, error: errorResponse, projectContext }) => {
377
+ const ctx = await initContext(projectContext.path);
378
+ const tasks = await loadAllTasks(ctx);
379
+ const items = await loadAllItems(ctx);
380
+ const index = new ReferenceIndex(tasks, items);
381
+
382
+ const result = index.resolve(params.ref);
383
+ if (!result.ok) {
384
+ return errorResponse(404, {
385
+ error: 'not_found',
386
+ message: `Task reference "${params.ref}" not found`,
387
+ });
388
+ }
389
+
390
+ const task = tasks.find((t) => t._ulid === result.ulid);
391
+ if (!task) {
392
+ return errorResponse(404, {
393
+ error: 'not_found',
394
+ message: `Reference "${params.ref}" is not a task`,
395
+ });
396
+ }
397
+
398
+ if (task.status !== 'in_progress' && task.status !== 'needs_work') {
399
+ return errorResponse(409, {
400
+ error: 'invalid_transition',
401
+ message: `Cannot submit task with status "${task.status}". Must be in_progress or needs_work.`,
402
+ current: task.status,
403
+ valid_transitions: ['pending_review'],
404
+ });
405
+ }
406
+
407
+ const updatedTask: LoadedTask = { ...task, status: 'pending_review' };
408
+ await saveTask(ctx, updatedTask);
409
+ await syncSpecImplementationStatus(ctx, updatedTask, tasks, items, index);
410
+ await commitIfShadow(ctx.shadow, `task: submit ${params.ref}`);
411
+
412
+ pubsub.broadcast('tasks:updates', 'task_updated', {
413
+ ref: params.ref,
414
+ ulid: task._ulid,
415
+ action: 'submit',
416
+ status: 'pending_review',
417
+ }, projectContext.path);
418
+
419
+ return updatedTask;
420
+ },
421
+ { params: t.Object({ ref: t.String() }) }
422
+ )
423
+
424
+ // AC: @ui-task-board ac-6 - Complete task
425
+ .post(
426
+ '/:ref/complete',
427
+ async ({ params, body, error: errorResponse, projectContext }) => {
428
+ const ctx = await initContext(projectContext.path);
429
+ const tasks = await loadAllTasks(ctx);
430
+ const items = await loadAllItems(ctx);
431
+ const index = new ReferenceIndex(tasks, items);
432
+
433
+ const result = index.resolve(params.ref);
434
+ if (!result.ok) {
435
+ return errorResponse(404, {
436
+ error: 'not_found',
437
+ message: `Task reference "${params.ref}" not found`,
438
+ });
439
+ }
440
+
441
+ const task = tasks.find((t) => t._ulid === result.ulid);
442
+ if (!task) {
443
+ return errorResponse(404, {
444
+ error: 'not_found',
445
+ message: `Reference "${params.ref}" is not a task`,
446
+ });
447
+ }
448
+
449
+ const updatedTask: LoadedTask = {
450
+ ...task,
451
+ status: 'completed',
452
+ completed_at: new Date().toISOString(),
453
+ closed_reason: body.reason,
454
+ };
455
+ await saveTask(ctx, updatedTask);
456
+ await syncSpecImplementationStatus(ctx, updatedTask, tasks, items, index);
457
+ await commitIfShadow(ctx.shadow, `task: complete ${params.ref}`);
458
+
459
+ pubsub.broadcast('tasks:updates', 'task_updated', {
460
+ ref: params.ref,
461
+ ulid: task._ulid,
462
+ action: 'complete',
463
+ status: 'completed',
464
+ }, projectContext.path);
465
+
466
+ return updatedTask;
467
+ },
468
+ {
469
+ params: t.Object({ ref: t.String() }),
470
+ body: t.Object({ reason: t.String() }),
471
+ }
472
+ )
473
+
474
+ // AC: @ui-task-board ac-6 - Block task
475
+ .post(
476
+ '/:ref/block',
477
+ async ({ params, body, error: errorResponse, projectContext }) => {
478
+ const ctx = await initContext(projectContext.path);
479
+ const tasks = await loadAllTasks(ctx);
480
+ const items = await loadAllItems(ctx);
481
+ const index = new ReferenceIndex(tasks, items);
482
+
483
+ const result = index.resolve(params.ref);
484
+ if (!result.ok) {
485
+ return errorResponse(404, {
486
+ error: 'not_found',
487
+ message: `Task reference "${params.ref}" not found`,
488
+ });
489
+ }
490
+
491
+ const task = tasks.find((t) => t._ulid === result.ulid);
492
+ if (!task) {
493
+ return errorResponse(404, {
494
+ error: 'not_found',
495
+ message: `Reference "${params.ref}" is not a task`,
496
+ });
497
+ }
498
+
499
+ const author = getAuthor(ctx.config?.identity?.author);
500
+ const note = createNote(`Blocked: ${body.reason}`, author);
501
+
502
+ const updatedTask: LoadedTask = {
503
+ ...task,
504
+ status: 'blocked',
505
+ notes: [...(task.notes || []), note],
506
+ };
507
+ await saveTask(ctx, updatedTask);
508
+ await syncSpecImplementationStatus(ctx, updatedTask, tasks, items, index);
509
+ await commitIfShadow(ctx.shadow, `task: block ${params.ref}`);
510
+
511
+ pubsub.broadcast('tasks:updates', 'task_updated', {
512
+ ref: params.ref,
513
+ ulid: task._ulid,
514
+ action: 'block',
515
+ status: 'blocked',
516
+ }, projectContext.path);
517
+
518
+ return updatedTask;
519
+ },
520
+ {
521
+ params: t.Object({ ref: t.String() }),
522
+ body: t.Object({ reason: t.String() }),
523
+ }
326
524
  );
327
525
  }
@@ -218,7 +218,7 @@ export function createValidationRoutes(options: ValidationRouteOptions = {}) {
218
218
  refWarnings: result.refWarnings,
219
219
  orphans: result.orphans,
220
220
  completenessWarnings: result.completenessWarnings,
221
- traitCycles: result.traitCycles,
221
+ traitCycles: result.traitCycleErrors,
222
222
  };
223
223
  })
224
224