@kynetic-ai/spec 0.10.0 → 0.12.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 (487) hide show
  1. package/README.md +55 -455
  2. package/dist/agent-runtime/bootstrap.d.ts +31 -0
  3. package/dist/agent-runtime/bootstrap.d.ts.map +1 -0
  4. package/dist/agent-runtime/bootstrap.js +302 -0
  5. package/dist/agent-runtime/bootstrap.js.map +1 -0
  6. package/dist/agent-runtime/dispatch.d.ts +150 -10
  7. package/dist/agent-runtime/dispatch.d.ts.map +1 -1
  8. package/dist/agent-runtime/dispatch.js +1248 -244
  9. package/dist/agent-runtime/dispatch.js.map +1 -1
  10. package/dist/agent-runtime/invocation.d.ts +28 -1
  11. package/dist/agent-runtime/invocation.d.ts.map +1 -1
  12. package/dist/agent-runtime/invocation.js +172 -60
  13. package/dist/agent-runtime/invocation.js.map +1 -1
  14. package/dist/agent-runtime/prompts.d.ts +9 -0
  15. package/dist/agent-runtime/prompts.d.ts.map +1 -1
  16. package/dist/agent-runtime/prompts.js +42 -7
  17. package/dist/agent-runtime/prompts.js.map +1 -1
  18. package/dist/agent-runtime/session-event-accumulator.d.ts +83 -0
  19. package/dist/agent-runtime/session-event-accumulator.d.ts.map +1 -0
  20. package/dist/agent-runtime/session-event-accumulator.js +203 -0
  21. package/dist/agent-runtime/session-event-accumulator.js.map +1 -0
  22. package/dist/agent-runtime/session-event-types.d.ts +67 -0
  23. package/dist/agent-runtime/session-event-types.d.ts.map +1 -0
  24. package/dist/agent-runtime/session-event-types.js +13 -0
  25. package/dist/agent-runtime/session-event-types.js.map +1 -0
  26. package/dist/agent-runtime/workspace.d.ts +244 -0
  27. package/dist/agent-runtime/workspace.d.ts.map +1 -0
  28. package/dist/agent-runtime/workspace.js +2025 -0
  29. package/dist/agent-runtime/workspace.js.map +1 -0
  30. package/dist/agents/adapters.d.ts.map +1 -1
  31. package/dist/agents/adapters.js +58 -13
  32. package/dist/agents/adapters.js.map +1 -1
  33. package/dist/agents/spawner.d.ts +8 -0
  34. package/dist/agents/spawner.d.ts.map +1 -1
  35. package/dist/agents/spawner.js +25 -3
  36. package/dist/agents/spawner.js.map +1 -1
  37. package/dist/cli/batch-exec.js +1 -1
  38. package/dist/cli/batch-exec.js.map +1 -1
  39. package/dist/cli/command-annotations.d.ts +15 -3
  40. package/dist/cli/command-annotations.d.ts.map +1 -1
  41. package/dist/cli/command-annotations.js +23 -3
  42. package/dist/cli/command-annotations.js.map +1 -1
  43. package/dist/cli/commands/agent.d.ts +2 -0
  44. package/dist/cli/commands/agent.d.ts.map +1 -1
  45. package/dist/cli/commands/agent.js +144 -27
  46. package/dist/cli/commands/agent.js.map +1 -1
  47. package/dist/cli/commands/agents.d.ts.map +1 -1
  48. package/dist/cli/commands/agents.js +5 -5
  49. package/dist/cli/commands/agents.js.map +1 -1
  50. package/dist/cli/commands/derive.d.ts.map +1 -1
  51. package/dist/cli/commands/derive.js +118 -3
  52. package/dist/cli/commands/derive.js.map +1 -1
  53. package/dist/cli/commands/guard.d.ts.map +1 -1
  54. package/dist/cli/commands/guard.js +8 -6
  55. package/dist/cli/commands/guard.js.map +1 -1
  56. package/dist/cli/commands/index.d.ts +1 -0
  57. package/dist/cli/commands/index.d.ts.map +1 -1
  58. package/dist/cli/commands/index.js +1 -0
  59. package/dist/cli/commands/index.js.map +1 -1
  60. package/dist/cli/commands/init.d.ts.map +1 -1
  61. package/dist/cli/commands/init.js +20 -0
  62. package/dist/cli/commands/init.js.map +1 -1
  63. package/dist/cli/commands/item.d.ts.map +1 -1
  64. package/dist/cli/commands/item.js +205 -47
  65. package/dist/cli/commands/item.js.map +1 -1
  66. package/dist/cli/commands/log.d.ts.map +1 -1
  67. package/dist/cli/commands/log.js +24 -10
  68. package/dist/cli/commands/log.js.map +1 -1
  69. package/dist/cli/commands/meta.d.ts.map +1 -1
  70. package/dist/cli/commands/meta.js +10 -1
  71. package/dist/cli/commands/meta.js.map +1 -1
  72. package/dist/cli/commands/plan-import.d.ts +3 -3
  73. package/dist/cli/commands/plan-import.d.ts.map +1 -1
  74. package/dist/cli/commands/plan-import.js +213 -528
  75. package/dist/cli/commands/plan-import.js.map +1 -1
  76. package/dist/cli/commands/plan.d.ts.map +1 -1
  77. package/dist/cli/commands/plan.js +533 -83
  78. package/dist/cli/commands/plan.js.map +1 -1
  79. package/dist/cli/commands/review.d.ts +14 -0
  80. package/dist/cli/commands/review.d.ts.map +1 -0
  81. package/dist/cli/commands/review.js +1142 -0
  82. package/dist/cli/commands/review.js.map +1 -0
  83. package/dist/cli/commands/serve.d.ts +1 -0
  84. package/dist/cli/commands/serve.d.ts.map +1 -1
  85. package/dist/cli/commands/serve.js +33 -10
  86. package/dist/cli/commands/serve.js.map +1 -1
  87. package/dist/cli/commands/session/checkpoint.d.ts +2 -4
  88. package/dist/cli/commands/session/checkpoint.d.ts.map +1 -1
  89. package/dist/cli/commands/session/checkpoint.js +6 -107
  90. package/dist/cli/commands/session/checkpoint.js.map +1 -1
  91. package/dist/cli/commands/session/commands.d.ts.map +1 -1
  92. package/dist/cli/commands/session/commands.js +33 -23
  93. package/dist/cli/commands/session/commands.js.map +1 -1
  94. package/dist/cli/commands/session/compact.js +4 -4
  95. package/dist/cli/commands/session/compact.js.map +1 -1
  96. package/dist/cli/commands/session/create.js +2 -2
  97. package/dist/cli/commands/session/create.js.map +1 -1
  98. package/dist/cli/commands/session/format.d.ts.map +1 -1
  99. package/dist/cli/commands/session/format.js +1 -6
  100. package/dist/cli/commands/session/format.js.map +1 -1
  101. package/dist/cli/commands/session/log.d.ts +32 -7
  102. package/dist/cli/commands/session/log.d.ts.map +1 -1
  103. package/dist/cli/commands/session/log.js +166 -60
  104. package/dist/cli/commands/session/log.js.map +1 -1
  105. package/dist/cli/commands/session/migrate.d.ts +9 -0
  106. package/dist/cli/commands/session/migrate.d.ts.map +1 -0
  107. package/dist/cli/commands/session/migrate.js +46 -0
  108. package/dist/cli/commands/session/migrate.js.map +1 -0
  109. package/dist/cli/commands/session/stale-close.d.ts.map +1 -1
  110. package/dist/cli/commands/session/stale-close.js +5 -8
  111. package/dist/cli/commands/session/stale-close.js.map +1 -1
  112. package/dist/cli/commands/session/types.d.ts +1 -1
  113. package/dist/cli/commands/session/types.d.ts.map +1 -1
  114. package/dist/cli/commands/setup.d.ts +2 -2
  115. package/dist/cli/commands/setup.d.ts.map +1 -1
  116. package/dist/cli/commands/setup.js +287 -257
  117. package/dist/cli/commands/setup.js.map +1 -1
  118. package/dist/cli/commands/shadow.d.ts.map +1 -1
  119. package/dist/cli/commands/shadow.js +147 -31
  120. package/dist/cli/commands/shadow.js.map +1 -1
  121. package/dist/cli/commands/skill-crud.d.ts +7 -0
  122. package/dist/cli/commands/skill-crud.d.ts.map +1 -1
  123. package/dist/cli/commands/skill-crud.js +41 -18
  124. package/dist/cli/commands/skill-crud.js.map +1 -1
  125. package/dist/cli/commands/skill-diff.d.ts.map +1 -1
  126. package/dist/cli/commands/skill-diff.js +29 -3
  127. package/dist/cli/commands/skill-diff.js.map +1 -1
  128. package/dist/cli/commands/skill-install.d.ts.map +1 -1
  129. package/dist/cli/commands/skill-install.js +5 -4
  130. package/dist/cli/commands/skill-install.js.map +1 -1
  131. package/dist/cli/commands/task.d.ts.map +1 -1
  132. package/dist/cli/commands/task.js +359 -49
  133. package/dist/cli/commands/task.js.map +1 -1
  134. package/dist/cli/commands/trait.d.ts.map +1 -1
  135. package/dist/cli/commands/trait.js +5 -27
  136. package/dist/cli/commands/trait.js.map +1 -1
  137. package/dist/cli/commands/validate.d.ts.map +1 -1
  138. package/dist/cli/commands/validate.js +113 -52
  139. package/dist/cli/commands/validate.js.map +1 -1
  140. package/dist/cli/index.d.ts.map +1 -1
  141. package/dist/cli/index.js +69 -2
  142. package/dist/cli/index.js.map +1 -1
  143. package/dist/cli/output.d.ts +26 -0
  144. package/dist/cli/output.d.ts.map +1 -1
  145. package/dist/cli/output.js +108 -1
  146. package/dist/cli/output.js.map +1 -1
  147. package/dist/cli/sync-mode.d.ts +44 -0
  148. package/dist/cli/sync-mode.d.ts.map +1 -0
  149. package/dist/cli/sync-mode.js +64 -0
  150. package/dist/cli/sync-mode.js.map +1 -0
  151. package/dist/daemon/middleware/project-context.ts +25 -7
  152. package/dist/daemon/project-context.ts +18 -0
  153. package/dist/daemon/routes/agent-dispatch.ts +107 -23
  154. package/dist/daemon/routes/aggregation.ts +184 -0
  155. package/dist/daemon/routes/inbox.ts +5 -0
  156. package/dist/daemon/routes/items.ts +167 -0
  157. package/dist/daemon/routes/meta.ts +141 -1
  158. package/dist/daemon/routes/plans.ts +147 -0
  159. package/dist/daemon/routes/projects.ts +28 -6
  160. package/dist/daemon/routes/ref-resolution.ts +119 -0
  161. package/dist/daemon/routes/refs.ts +42 -0
  162. package/dist/daemon/routes/session-related.ts +140 -0
  163. package/dist/daemon/routes/sessions.ts +581 -0
  164. package/dist/daemon/routes/tasks.ts +257 -2
  165. package/dist/daemon/routes/triage.ts +40 -1
  166. package/dist/daemon/routes/validation.ts +1 -1
  167. package/dist/daemon/server.ts +165 -50
  168. package/dist/daemon/session-sync.ts +11 -0
  169. package/dist/daemon/shadow-sync.ts +11 -0
  170. package/dist/daemon/watcher.ts +56 -5
  171. package/dist/daemon/websocket/project-resolution.ts +77 -0
  172. package/dist/export/json.d.ts.map +1 -1
  173. package/dist/export/json.js +104 -1
  174. package/dist/export/json.js.map +1 -1
  175. package/dist/export/types.d.ts +52 -1
  176. package/dist/export/types.d.ts.map +1 -1
  177. package/dist/index.d.ts +1 -0
  178. package/dist/index.d.ts.map +1 -1
  179. package/dist/index.js +1 -0
  180. package/dist/index.js.map +1 -1
  181. package/dist/parser/agent-detection.d.ts +1 -1
  182. package/dist/parser/agent-detection.d.ts.map +1 -1
  183. package/dist/parser/agent-detection.js +10 -0
  184. package/dist/parser/agent-detection.js.map +1 -1
  185. package/dist/parser/alignment.d.ts.map +1 -1
  186. package/dist/parser/alignment.js +4 -2
  187. package/dist/parser/alignment.js.map +1 -1
  188. package/dist/parser/config.d.ts +397 -2
  189. package/dist/parser/config.d.ts.map +1 -1
  190. package/dist/parser/config.js +125 -3
  191. package/dist/parser/config.js.map +1 -1
  192. package/dist/parser/dispatch-workspaces.d.ts +18 -0
  193. package/dist/parser/dispatch-workspaces.d.ts.map +1 -0
  194. package/dist/parser/dispatch-workspaces.js +209 -0
  195. package/dist/parser/dispatch-workspaces.js.map +1 -0
  196. package/dist/parser/doctor.d.ts.map +1 -1
  197. package/dist/parser/doctor.js +27 -8
  198. package/dist/parser/doctor.js.map +1 -1
  199. package/dist/parser/file-lock.d.ts.map +1 -1
  200. package/dist/parser/file-lock.js +9 -2
  201. package/dist/parser/file-lock.js.map +1 -1
  202. package/dist/parser/index.d.ts +6 -0
  203. package/dist/parser/index.d.ts.map +1 -1
  204. package/dist/parser/index.js +6 -0
  205. package/dist/parser/index.js.map +1 -1
  206. package/dist/parser/plans.d.ts.map +1 -1
  207. package/dist/parser/plans.js +1 -0
  208. package/dist/parser/plans.js.map +1 -1
  209. package/dist/parser/refs.d.ts +8 -1
  210. package/dist/parser/refs.d.ts.map +1 -1
  211. package/dist/parser/refs.js +27 -1
  212. package/dist/parser/refs.js.map +1 -1
  213. package/dist/parser/review-operations.d.ts +72 -0
  214. package/dist/parser/review-operations.d.ts.map +1 -0
  215. package/dist/parser/review-operations.js +185 -0
  216. package/dist/parser/review-operations.js.map +1 -0
  217. package/dist/parser/review-task-integration.d.ts +78 -0
  218. package/dist/parser/review-task-integration.d.ts.map +1 -0
  219. package/dist/parser/review-task-integration.js +173 -0
  220. package/dist/parser/review-task-integration.js.map +1 -0
  221. package/dist/parser/review-threads.d.ts +101 -0
  222. package/dist/parser/review-threads.d.ts.map +1 -0
  223. package/dist/parser/review-threads.js +222 -0
  224. package/dist/parser/review-threads.js.map +1 -0
  225. package/dist/parser/review-validation.d.ts +69 -0
  226. package/dist/parser/review-validation.d.ts.map +1 -0
  227. package/dist/parser/review-validation.js +207 -0
  228. package/dist/parser/review-validation.js.map +1 -0
  229. package/dist/parser/reviews.d.ts +58 -0
  230. package/dist/parser/reviews.d.ts.map +1 -0
  231. package/dist/parser/reviews.js +230 -0
  232. package/dist/parser/reviews.js.map +1 -0
  233. package/dist/parser/session-branch.d.ts +91 -0
  234. package/dist/parser/session-branch.d.ts.map +1 -0
  235. package/dist/parser/session-branch.js +565 -0
  236. package/dist/parser/session-branch.js.map +1 -0
  237. package/dist/parser/session-sync-scheduler.d.ts +53 -0
  238. package/dist/parser/session-sync-scheduler.d.ts.map +1 -0
  239. package/dist/parser/session-sync-scheduler.js +100 -0
  240. package/dist/parser/session-sync-scheduler.js.map +1 -0
  241. package/dist/parser/setup-status.d.ts +7 -1
  242. package/dist/parser/setup-status.d.ts.map +1 -1
  243. package/dist/parser/setup-status.js +104 -39
  244. package/dist/parser/setup-status.js.map +1 -1
  245. package/dist/parser/shadow-sync-scheduler.d.ts +71 -0
  246. package/dist/parser/shadow-sync-scheduler.d.ts.map +1 -0
  247. package/dist/parser/shadow-sync-scheduler.js +139 -0
  248. package/dist/parser/shadow-sync-scheduler.js.map +1 -0
  249. package/dist/parser/shadow.d.ts +121 -14
  250. package/dist/parser/shadow.d.ts.map +1 -1
  251. package/dist/parser/shadow.js +752 -27
  252. package/dist/parser/shadow.js.map +1 -1
  253. package/dist/parser/skill-render.d.ts +24 -0
  254. package/dist/parser/skill-render.d.ts.map +1 -1
  255. package/dist/parser/skill-render.js +98 -26
  256. package/dist/parser/skill-render.js.map +1 -1
  257. package/dist/parser/validate.d.ts +43 -3
  258. package/dist/parser/validate.d.ts.map +1 -1
  259. package/dist/parser/validate.js +204 -30
  260. package/dist/parser/validate.js.map +1 -1
  261. package/dist/parser/yaml.d.ts +47 -11
  262. package/dist/parser/yaml.d.ts.map +1 -1
  263. package/dist/parser/yaml.js +329 -149
  264. package/dist/parser/yaml.js.map +1 -1
  265. package/dist/review/checks.d.ts +97 -0
  266. package/dist/review/checks.d.ts.map +1 -0
  267. package/dist/review/checks.js +175 -0
  268. package/dist/review/checks.js.map +1 -0
  269. package/dist/review/index.d.ts +3 -0
  270. package/dist/review/index.d.ts.map +1 -0
  271. package/dist/review/index.js +3 -0
  272. package/dist/review/index.js.map +1 -0
  273. package/dist/review/subject-bindings.d.ts +83 -0
  274. package/dist/review/subject-bindings.d.ts.map +1 -0
  275. package/dist/review/subject-bindings.js +175 -0
  276. package/dist/review/subject-bindings.js.map +1 -0
  277. package/dist/schema/common.d.ts +26 -0
  278. package/dist/schema/common.d.ts.map +1 -1
  279. package/dist/schema/common.js +13 -0
  280. package/dist/schema/common.js.map +1 -1
  281. package/dist/schema/dispatch-workspace.d.ts +2643 -0
  282. package/dist/schema/dispatch-workspace.d.ts.map +1 -0
  283. package/dist/schema/dispatch-workspace.js +187 -0
  284. package/dist/schema/dispatch-workspace.js.map +1 -0
  285. package/dist/schema/inbox.d.ts +8 -8
  286. package/dist/schema/index.d.ts +2 -0
  287. package/dist/schema/index.d.ts.map +1 -1
  288. package/dist/schema/index.js +2 -0
  289. package/dist/schema/index.js.map +1 -1
  290. package/dist/schema/meta.d.ts +663 -116
  291. package/dist/schema/meta.d.ts.map +1 -1
  292. package/dist/schema/meta.js +28 -0
  293. package/dist/schema/meta.js.map +1 -1
  294. package/dist/schema/plan.d.ts +30 -19
  295. package/dist/schema/plan.d.ts.map +1 -1
  296. package/dist/schema/plan.js +3 -1
  297. package/dist/schema/plan.js.map +1 -1
  298. package/dist/schema/review-records.d.ts +2676 -0
  299. package/dist/schema/review-records.d.ts.map +1 -0
  300. package/dist/schema/review-records.js +232 -0
  301. package/dist/schema/review-records.js.map +1 -0
  302. package/dist/schema/spec.d.ts +32 -14
  303. package/dist/schema/spec.d.ts.map +1 -1
  304. package/dist/schema/spec.js +5 -0
  305. package/dist/schema/spec.js.map +1 -1
  306. package/dist/schema/task.d.ts +187 -29
  307. package/dist/schema/task.d.ts.map +1 -1
  308. package/dist/schema/task.js +12 -2
  309. package/dist/schema/task.js.map +1 -1
  310. package/dist/schema/triage.d.ts +22 -22
  311. package/dist/sessions/cache.d.ts +119 -0
  312. package/dist/sessions/cache.d.ts.map +1 -0
  313. package/dist/sessions/cache.js +284 -0
  314. package/dist/sessions/cache.js.map +1 -0
  315. package/dist/sessions/index.d.ts +1 -0
  316. package/dist/sessions/index.d.ts.map +1 -1
  317. package/dist/sessions/index.js +2 -0
  318. package/dist/sessions/index.js.map +1 -1
  319. package/dist/sessions/legacy.d.ts +77 -0
  320. package/dist/sessions/legacy.d.ts.map +1 -0
  321. package/dist/sessions/legacy.js +146 -0
  322. package/dist/sessions/legacy.js.map +1 -0
  323. package/dist/sessions/store.d.ts +115 -71
  324. package/dist/sessions/store.d.ts.map +1 -1
  325. package/dist/sessions/store.js +357 -182
  326. package/dist/sessions/store.js.map +1 -1
  327. package/dist/sessions/types.d.ts +44 -16
  328. package/dist/sessions/types.d.ts.map +1 -1
  329. package/dist/sessions/types.js +11 -2
  330. package/dist/sessions/types.js.map +1 -1
  331. package/dist/strings/errors.d.ts +32 -0
  332. package/dist/strings/errors.d.ts.map +1 -1
  333. package/dist/strings/errors.js +17 -0
  334. package/dist/strings/errors.js.map +1 -1
  335. package/dist/strings/labels.d.ts +1 -0
  336. package/dist/strings/labels.d.ts.map +1 -1
  337. package/dist/strings/labels.js +1 -0
  338. package/dist/strings/labels.js.map +1 -1
  339. package/dist/utils/activity.d.ts +101 -0
  340. package/dist/utils/activity.d.ts.map +1 -0
  341. package/dist/utils/activity.js +408 -0
  342. package/dist/utils/activity.js.map +1 -0
  343. package/dist/utils/git.d.ts +31 -0
  344. package/dist/utils/git.d.ts.map +1 -1
  345. package/dist/utils/git.js +87 -0
  346. package/dist/utils/git.js.map +1 -1
  347. package/dist/utils/index.d.ts +2 -0
  348. package/dist/utils/index.d.ts.map +1 -1
  349. package/dist/utils/index.js +1 -0
  350. package/dist/utils/index.js.map +1 -1
  351. package/dist/web-ui/_app/immutable/assets/0.tmlwn-Ih.css +1 -0
  352. package/dist/web-ui/_app/immutable/assets/9.BwwJybWx.css +1 -0
  353. package/dist/web-ui/_app/immutable/chunks/2KqE8gtn.js +1 -0
  354. package/dist/web-ui/_app/immutable/chunks/70-t_QvE.js +1 -0
  355. package/dist/web-ui/_app/immutable/chunks/AiWQj974.js +1 -0
  356. package/dist/web-ui/_app/immutable/chunks/B25nWFyA.js +5 -0
  357. package/dist/web-ui/_app/immutable/chunks/B2bcA_Q_.js +1 -0
  358. package/dist/web-ui/_app/immutable/chunks/B5e5HYyB.js +1 -0
  359. package/dist/web-ui/_app/immutable/chunks/B7-5z6eA.js +1 -0
  360. package/dist/web-ui/_app/immutable/chunks/B7bGmhK0.js +1 -0
  361. package/dist/web-ui/_app/immutable/chunks/B8tYZKAE.js +1 -0
  362. package/dist/web-ui/_app/immutable/chunks/BFGAyJjD.js +1 -0
  363. package/dist/web-ui/_app/immutable/chunks/BG0850zf.js +1 -0
  364. package/dist/web-ui/_app/immutable/chunks/BG8eSzAd.js +1 -0
  365. package/dist/web-ui/_app/immutable/chunks/BIMxXS8I.js +1 -0
  366. package/dist/web-ui/_app/immutable/chunks/BSzL1fpU.js +1 -0
  367. package/dist/web-ui/_app/immutable/chunks/BYtjHfeq.js +1 -0
  368. package/dist/web-ui/_app/immutable/chunks/{D1ArdqNb.js → Bp5pFYXL.js} +1 -1
  369. package/dist/web-ui/_app/immutable/chunks/BsJFsuAT.js +1 -0
  370. package/dist/web-ui/_app/immutable/chunks/BvpNHcD6.js +1 -0
  371. package/dist/web-ui/_app/immutable/chunks/BypqA25-.js +1 -0
  372. package/dist/web-ui/_app/immutable/chunks/C0w6WDm5.js +1 -0
  373. package/dist/web-ui/_app/immutable/chunks/C5_PAZ0y.js +1 -0
  374. package/dist/web-ui/_app/immutable/chunks/CDRO15Iv.js +1 -0
  375. package/dist/web-ui/_app/immutable/chunks/CF1CoqD5.js +1 -0
  376. package/dist/web-ui/_app/immutable/chunks/CS2sa4_m.js +1 -0
  377. package/dist/web-ui/_app/immutable/chunks/CWUQwB9H.js +1 -0
  378. package/dist/web-ui/_app/immutable/chunks/CY5FDdSU.js +1 -0
  379. package/dist/web-ui/_app/immutable/chunks/C_7MTDoj.js +1 -0
  380. package/dist/web-ui/_app/immutable/chunks/CaAJD3dl.js +1 -0
  381. package/dist/web-ui/_app/immutable/chunks/{i-XnOIX0.js → ChB5iyEL.js} +1 -1
  382. package/dist/web-ui/_app/immutable/chunks/ChQD-6N8.js +1 -0
  383. package/dist/web-ui/_app/immutable/chunks/{BCkp8Hs8.js → CqbsoCwA.js} +1 -1
  384. package/dist/web-ui/_app/immutable/chunks/DCeJW50p.js +1 -0
  385. package/dist/web-ui/_app/immutable/chunks/DJtZNgcs.js +1 -0
  386. package/dist/web-ui/_app/immutable/chunks/DKIeaprD.js +1 -0
  387. package/dist/web-ui/_app/immutable/chunks/DLd2uVIA.js +1 -0
  388. package/dist/web-ui/_app/immutable/chunks/DW_subyT.js +2 -0
  389. package/dist/web-ui/_app/immutable/chunks/DbU6lVn0.js +1 -0
  390. package/dist/web-ui/_app/immutable/chunks/Dc7ZCC5m.js +1 -0
  391. package/dist/web-ui/_app/immutable/chunks/Dd5umPsk.js +2 -0
  392. package/dist/web-ui/_app/immutable/chunks/Dg_zDpDS.js +1 -0
  393. package/dist/web-ui/_app/immutable/chunks/Dgqu8Yuc.js +1 -0
  394. package/dist/web-ui/_app/immutable/chunks/DmxsPZTB.js +1 -0
  395. package/dist/web-ui/_app/immutable/chunks/DphTaFUB.js +1 -0
  396. package/dist/web-ui/_app/immutable/chunks/DqK4iHp0.js +1 -0
  397. package/dist/web-ui/_app/immutable/chunks/DqT6OH_u.js +2 -0
  398. package/dist/web-ui/_app/immutable/chunks/Ds9I9wQb.js +1 -0
  399. package/dist/web-ui/_app/immutable/chunks/Du5ng3u4.js +1 -0
  400. package/dist/web-ui/_app/immutable/chunks/DxJw79Wi.js +1 -0
  401. package/dist/web-ui/_app/immutable/chunks/GFTX8GgV.js +1 -0
  402. package/dist/web-ui/_app/immutable/chunks/HNjs76Zz.js +1 -0
  403. package/dist/web-ui/_app/immutable/chunks/HVMjDi4_.js +1 -0
  404. package/dist/web-ui/_app/immutable/chunks/P0A_fJvS.js +1 -0
  405. package/dist/web-ui/_app/immutable/chunks/T3vGWjIL.js +1 -0
  406. package/dist/web-ui/_app/immutable/chunks/VTmrX9Qu.js +1 -0
  407. package/dist/web-ui/_app/immutable/chunks/Xvwhx_F1.js +1 -0
  408. package/dist/web-ui/_app/immutable/chunks/Yyz1XMQA.js +1 -0
  409. package/dist/web-ui/_app/immutable/chunks/dh5HeqUr.js +1 -0
  410. package/dist/web-ui/_app/immutable/chunks/fZMteyca.js +62 -0
  411. package/dist/web-ui/_app/immutable/chunks/{D28BF5MJ.js → gPrj-hqC.js} +1 -1
  412. package/dist/web-ui/_app/immutable/chunks/htcWMiYN.js +1 -0
  413. package/dist/web-ui/_app/immutable/chunks/oTsvd9y4.js +1 -0
  414. package/dist/web-ui/_app/immutable/chunks/qJfLUwU4.js +1 -0
  415. package/dist/web-ui/_app/immutable/chunks/xCtiO_JE.js +1 -0
  416. package/dist/web-ui/_app/immutable/chunks/y4GeEH6k.js +1 -0
  417. package/dist/web-ui/_app/immutable/entry/app.C4h_eOn6.js +2 -0
  418. package/dist/web-ui/_app/immutable/entry/start.CQFTf9ep.js +1 -0
  419. package/dist/web-ui/_app/immutable/nodes/0.Dh1xO970.js +1 -0
  420. package/dist/web-ui/_app/immutable/nodes/1.l75D3Opx.js +1 -0
  421. package/dist/web-ui/_app/immutable/nodes/10.DBidBPc-.js +1 -0
  422. package/dist/web-ui/_app/immutable/nodes/11.Ab0gUKWe.js +1 -0
  423. package/dist/web-ui/_app/immutable/nodes/12.CMsnoxfs.js +1 -0
  424. package/dist/web-ui/_app/immutable/nodes/13.D8YKuknB.js +1 -0
  425. package/dist/web-ui/_app/immutable/nodes/14.DZ0aan7y.js +1 -0
  426. package/dist/web-ui/_app/immutable/nodes/15.CUIKreDL.js +2 -0
  427. package/dist/web-ui/_app/immutable/nodes/16.BWc8--BO.js +1 -0
  428. package/dist/web-ui/_app/immutable/nodes/2.CDUonbuh.js +1 -0
  429. package/dist/web-ui/_app/immutable/nodes/3.Ctg3M00i.js +1 -0
  430. package/dist/web-ui/_app/immutable/nodes/4.Ci-JDwbA.js +2 -0
  431. package/dist/web-ui/_app/immutable/nodes/5.CTyEDAq0.js +1 -0
  432. package/dist/web-ui/_app/immutable/nodes/6.BTZZqsAb.js +1 -0
  433. package/dist/web-ui/_app/immutable/nodes/7.BI52g_Jo.js +137 -0
  434. package/dist/web-ui/_app/immutable/nodes/8.3hZPaB9x.js +1 -0
  435. package/dist/web-ui/_app/immutable/nodes/9.DS49kvwl.js +29 -0
  436. package/dist/web-ui/_app/version.json +1 -1
  437. package/dist/web-ui/favicon-192.png +0 -0
  438. package/dist/web-ui/favicon-32.png +0 -0
  439. package/dist/web-ui/favicon.ico +0 -0
  440. package/dist/web-ui/index.html +14 -11
  441. package/package.json +14 -7
  442. package/plugin/.claude-plugin/marketplace.json +1 -1
  443. package/plugin/.claude-plugin/plugin.json +1 -1
  444. package/plugin/plugins/kspec/skills/merge/SKILL.md +127 -0
  445. package/plugin/plugins/kspec/skills/plan/SKILL.md +55 -26
  446. package/plugin/plugins/kspec/skills/review/SKILL.md +350 -133
  447. package/plugin/plugins/kspec/skills/task-work/SKILL.md +96 -106
  448. package/templates/agents-sections/04-pr-workflow.md +15 -12
  449. package/templates/agents-sections/06-ralph-loop.md +15 -10
  450. package/templates/skills/manifest.yaml +25 -7
  451. package/templates/skills/merge/SKILL.md +120 -0
  452. package/templates/skills/plan/SKILL.md +55 -26
  453. package/templates/skills/review/SKILL.md +346 -130
  454. package/templates/skills/task-work/SKILL.md +93 -103
  455. package/dist/web-ui/_app/immutable/assets/0.BxCxvrZR.css +0 -1
  456. package/dist/web-ui/_app/immutable/chunks/B-CZR0q8.js +0 -1
  457. package/dist/web-ui/_app/immutable/chunks/B1IR5Su5.js +0 -1
  458. package/dist/web-ui/_app/immutable/chunks/B_Cvvtc4.js +0 -1
  459. package/dist/web-ui/_app/immutable/chunks/BtFaGGII.js +0 -1
  460. package/dist/web-ui/_app/immutable/chunks/Bu8JVsCH.js +0 -1
  461. package/dist/web-ui/_app/immutable/chunks/C87u-CNA.js +0 -1
  462. package/dist/web-ui/_app/immutable/chunks/CrFkBTYp.js +0 -1
  463. package/dist/web-ui/_app/immutable/chunks/D6RtLpzL.js +0 -1
  464. package/dist/web-ui/_app/immutable/chunks/D7FHSgx2.js +0 -1
  465. package/dist/web-ui/_app/immutable/chunks/DBXrsxZQ.js +0 -2
  466. package/dist/web-ui/_app/immutable/chunks/Da_hHMuA.js +0 -1
  467. package/dist/web-ui/_app/immutable/chunks/Do6LchSF.js +0 -1
  468. package/dist/web-ui/_app/immutable/chunks/DoNPtcAw.js +0 -1
  469. package/dist/web-ui/_app/immutable/chunks/DtUbXRZz.js +0 -1
  470. package/dist/web-ui/_app/immutable/chunks/DyFPRlLl.js +0 -1
  471. package/dist/web-ui/_app/immutable/chunks/DzAP8lRM.js +0 -1
  472. package/dist/web-ui/_app/immutable/chunks/DzVXElzN.js +0 -2
  473. package/dist/web-ui/_app/immutable/chunks/aoPBFken.js +0 -1
  474. package/dist/web-ui/_app/immutable/chunks/laxtrUO3.js +0 -1
  475. package/dist/web-ui/_app/immutable/chunks/q1nIWgqB.js +0 -1
  476. package/dist/web-ui/_app/immutable/chunks/sTLbk5Nm.js +0 -1
  477. package/dist/web-ui/_app/immutable/chunks/vwKgQu5P.js +0 -5
  478. package/dist/web-ui/_app/immutable/entry/app.BCwMcqnT.js +0 -2
  479. package/dist/web-ui/_app/immutable/entry/start.wKCQH-tt.js +0 -1
  480. package/dist/web-ui/_app/immutable/nodes/0.CjGVMG74.js +0 -1
  481. package/dist/web-ui/_app/immutable/nodes/1.B6_AIPan.js +0 -1
  482. package/dist/web-ui/_app/immutable/nodes/2.q4oCS7Ws.js +0 -1
  483. package/dist/web-ui/_app/immutable/nodes/3.rTKZf9o2.js +0 -1
  484. package/dist/web-ui/_app/immutable/nodes/4.DVIDRu1d.js +0 -1
  485. package/dist/web-ui/_app/immutable/nodes/5.8PtPXIOd.js +0 -1
  486. package/dist/web-ui/_app/immutable/nodes/6.ZZrTemy_.js +0 -1
  487. package/dist/web-ui/_app/immutable/nodes/7.IP-gxCxi.js +0 -1
@@ -7,7 +7,7 @@
7
7
  * - Session metadata management
8
8
  *
9
9
  * Storage structure:
10
- * .kspec/sessions/{session-id}/
10
+ * .kspec-sessions/{session-id}/
11
11
  * session.yaml # Metadata
12
12
  * events.jsonl # Append-only event log
13
13
  */
@@ -17,10 +17,9 @@ import * as path from "node:path";
17
17
  import { createHash, randomUUID } from "node:crypto";
18
18
  import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
19
19
  import * as YAML from "yaml";
20
- import { shadowAutoCommit } from "../parser/shadow.js";
21
20
  import { SessionEventSchema, SessionMetadataSchema, TaskBudgetSchema, } from "./types.js";
21
+ import { sessionBranchAutoCommit, } from "../parser/session-branch.js";
22
22
  // ─── Constants ───────────────────────────────────────────────────────────────
23
- const SESSIONS_DIR = "sessions";
24
23
  const METADATA_FILE = "session.yaml";
25
24
  const EVENTS_FILE = "events.jsonl";
26
25
  const BUDGET_FILE = "budget.json";
@@ -31,64 +30,104 @@ const EVENT_LINE_MAX_BYTES = 256 * 1024;
31
30
  const EVENT_FIELD_EXTERNALIZE_BYTES = 16 * 1024;
32
31
  const EVENT_PREVIEW_MAX_BYTES = 512;
33
32
  const EVENT_SEQ_TAIL_READ_BYTES = EVENT_LINE_MAX_BYTES + 1024;
33
+ // ─── Session Branch Auto-Commit ──────────────────────────────────────────────
34
+ // AC: @session-branch-worktree ac-commit-boundaries
35
+ // When sessionsDir is a git worktree (sessions.storage=branch), auto-commit at
36
+ // lifecycle boundaries (create, close, stale cleanup, compact).
37
+ // Event appends are NOT committed individually.
38
+ /**
39
+ * Check if sessionsDir is a git worktree (has a .git file, not directory).
40
+ * Cached per path to avoid repeated filesystem checks.
41
+ */
42
+ const worktreeCache = new Map();
43
+ async function isSessionWorktree(sessionsDir) {
44
+ const cached = worktreeCache.get(sessionsDir);
45
+ if (cached !== undefined)
46
+ return cached;
47
+ try {
48
+ const gitPath = path.join(sessionsDir, ".git");
49
+ const stat = await fsPromises.stat(gitPath);
50
+ // Worktrees have a .git FILE pointing to the main repo
51
+ const isWorktree = stat.isFile();
52
+ worktreeCache.set(sessionsDir, isWorktree);
53
+ return isWorktree;
54
+ }
55
+ catch {
56
+ worktreeCache.set(sessionsDir, false);
57
+ return false;
58
+ }
59
+ }
60
+ /**
61
+ * Auto-commit to session branch at lifecycle boundaries.
62
+ * No-op if sessionsDir is not a git worktree.
63
+ */
64
+ async function commitAtLifecycleBoundary(sessionsDir, message) {
65
+ if (await isSessionWorktree(sessionsDir)) {
66
+ await sessionBranchAutoCommit(sessionsDir, message);
67
+ }
68
+ }
34
69
  // ─── Path Helpers ────────────────────────────────────────────────────────────
70
+ // AC: @session-storage-path-resolution ac-resolver ac-path-helpers
71
+ // All path helpers accept sessionsDir (.kspec-sessions/ at project root),
72
+ // not sessionsDir (.kspec/). This decouples session storage from the shadow branch.
35
73
  /**
36
- * Get the sessions directory path within a spec directory.
74
+ * Get the sessions root directory.
75
+ * @deprecated Use ctx.sessionsDir directly. Kept for backward compatibility.
37
76
  */
38
- export function getSessionsDir(specDir) {
39
- return path.join(specDir, SESSIONS_DIR);
77
+ export function getSessionsDir(sessionsDir) {
78
+ return sessionsDir;
40
79
  }
41
80
  /**
42
81
  * Get the path to a specific session's directory.
43
82
  */
44
- export function getSessionDir(specDir, sessionId) {
45
- return path.join(getSessionsDir(specDir), sessionId);
83
+ export function getSessionDir(sessionsDir, sessionId) {
84
+ return path.join(sessionsDir, sessionId);
46
85
  }
47
86
  /**
48
87
  * Get the path to a session's metadata file.
49
88
  */
50
- export function getSessionMetadataPath(specDir, sessionId) {
51
- return path.join(getSessionDir(specDir, sessionId), METADATA_FILE);
89
+ export function getSessionMetadataPath(sessionsDir, sessionId) {
90
+ return path.join(getSessionDir(sessionsDir, sessionId), METADATA_FILE);
52
91
  }
53
92
  /**
54
93
  * Get the path to a session's events file.
55
94
  */
56
- export function getSessionEventsPath(specDir, sessionId) {
57
- return path.join(getSessionDir(specDir, sessionId), EVENTS_FILE);
95
+ export function getSessionEventsPath(sessionsDir, sessionId) {
96
+ return path.join(getSessionDir(sessionsDir, sessionId), EVENTS_FILE);
58
97
  }
59
98
  /**
60
99
  * Get the path to a session's context snapshot file for a given iteration.
61
100
  */
62
- export function getSessionContextPath(specDir, sessionId, iteration) {
63
- return path.join(getSessionDir(specDir, sessionId), `context-iter-${iteration}.json`);
101
+ export function getSessionContextPath(sessionsDir, sessionId, iteration) {
102
+ return path.join(getSessionDir(sessionsDir, sessionId), `context-iter-${iteration}.json`);
64
103
  }
65
104
  /**
66
105
  * Get the path to a session's budget file.
67
106
  * AC: @session-creation-and-env-injection ac-budget-local
68
107
  */
69
- export function getSessionBudgetPath(specDir, sessionId) {
70
- return path.join(getSessionDir(specDir, sessionId), BUDGET_FILE);
108
+ export function getSessionBudgetPath(sessionsDir, sessionId) {
109
+ return path.join(getSessionDir(sessionsDir, sessionId), BUDGET_FILE);
71
110
  }
72
111
  /**
73
112
  * Get the path to a session's blob directory.
74
113
  */
75
- export function getSessionBlobDir(specDir, sessionId) {
76
- return path.join(getSessionDir(specDir, sessionId), BLOBS_DIR);
114
+ export function getSessionBlobDir(sessionsDir, sessionId) {
115
+ return path.join(getSessionDir(sessionsDir, sessionId), BLOBS_DIR);
77
116
  }
78
117
  // ─── Session CRUD ────────────────────────────────────────────────────────────
79
118
  /**
80
119
  * Create a new session with metadata.
81
120
  *
82
- * AC-1: Creates .kspec/sessions/{id}/ directory with session.yaml metadata file.
121
+ * AC-1: Creates .kspec-sessions/{id}/ directory with session.yaml metadata file.
83
122
  * AC-5: Metadata includes task_id (optional), agent_type, status, started_at, ended_at.
84
123
  *
85
- * @param specDir - The .kspec directory path
124
+ * @param sessionsDir - The .kspec directory path
86
125
  * @param input - Session metadata input
87
126
  * @returns The created session metadata
88
127
  */
89
- export async function createSession(specDir, input) {
90
- const sessionDir = getSessionDir(specDir, input.id);
91
- const metadataPath = getSessionMetadataPath(specDir, input.id);
128
+ export async function createSession(sessionsDir, input) {
129
+ const sessionDir = getSessionDir(sessionsDir, input.id);
130
+ const metadataPath = getSessionMetadataPath(sessionsDir, input.id);
92
131
  // Create session directory
93
132
  await fsPromises.mkdir(sessionDir, { recursive: true });
94
133
  // Build full metadata
@@ -111,17 +150,19 @@ export async function createSession(specDir, input) {
111
150
  sortMapEntries: false,
112
151
  });
113
152
  await fsPromises.writeFile(metadataPath, content, "utf-8");
153
+ // AC: @session-branch-worktree ac-commit-boundaries — commit on session create
154
+ await commitAtLifecycleBoundary(sessionsDir, `session: create (${input.id})`);
114
155
  return validated;
115
156
  }
116
157
  /**
117
158
  * Read session metadata.
118
159
  *
119
- * @param specDir - The .kspec directory path
160
+ * @param sessionsDir - The .kspec directory path
120
161
  * @param sessionId - Session ID
121
162
  * @returns Session metadata or null if not found
122
163
  */
123
- export async function getSession(specDir, sessionId) {
124
- const metadataPath = getSessionMetadataPath(specDir, sessionId);
164
+ export async function getSession(sessionsDir, sessionId) {
165
+ const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
125
166
  try {
126
167
  const content = await fsPromises.readFile(metadataPath, "utf-8");
127
168
  const raw = YAML.parse(content);
@@ -142,13 +183,13 @@ export async function getSession(specDir, sessionId) {
142
183
  *
143
184
  * AC-6: Updates metadata with status and ended_at timestamp when session ends.
144
185
  *
145
- * @param specDir - The .kspec directory path
186
+ * @param sessionsDir - The .kspec directory path
146
187
  * @param sessionId - Session ID
147
188
  * @param status - New status
148
189
  * @returns Updated metadata or null if session not found
149
190
  */
150
- export async function updateSessionStatus(specDir, sessionId, status) {
151
- const metadata = await getSession(specDir, sessionId);
191
+ export async function updateSessionStatus(sessionsDir, sessionId, status) {
192
+ const metadata = await getSession(sessionsDir, sessionId);
152
193
  if (!metadata) {
153
194
  return null;
154
195
  }
@@ -158,7 +199,7 @@ export async function updateSessionStatus(specDir, sessionId, status) {
158
199
  status,
159
200
  ended_at: status !== "active" ? new Date().toISOString() : metadata.ended_at,
160
201
  };
161
- const metadataPath = getSessionMetadataPath(specDir, sessionId);
202
+ const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
162
203
  const content = YAML.stringify(updated, {
163
204
  indent: 2,
164
205
  lineWidth: 100,
@@ -170,11 +211,10 @@ export async function updateSessionStatus(specDir, sessionId, status) {
170
211
  /**
171
212
  * List all sessions.
172
213
  *
173
- * @param specDir - The .kspec directory path
214
+ * @param sessionsDir - The .kspec directory path
174
215
  * @returns Array of session IDs
175
216
  */
176
- export async function listSessions(specDir) {
177
- const sessionsDir = getSessionsDir(specDir);
217
+ export async function listSessions(sessionsDir) {
178
218
  try {
179
219
  const entries = await fsPromises.readdir(sessionsDir, {
180
220
  withFileTypes: true,
@@ -185,11 +225,35 @@ export async function listSessions(specDir) {
185
225
  return [];
186
226
  }
187
227
  }
228
+ /**
229
+ * Count completed sessions grouped by agent_id.
230
+ *
231
+ * Reads only session metadata (not events.jsonl) for performance.
232
+ * A session counts as "completed" if its status is "completed".
233
+ *
234
+ * @param sessionsDir - The .kspec directory path
235
+ * @returns Map of agent_id to completed session count
236
+ */
237
+ export async function getCompletedSessionCountsByAgent(sessionsDir) {
238
+ const sessionIds = await listSessions(sessionsDir);
239
+ const counts = {};
240
+ // Read metadata in parallel for performance
241
+ const metadataResults = await Promise.all(sessionIds.map((id) => getSession(sessionsDir, id)));
242
+ for (const metadata of metadataResults) {
243
+ if (!metadata)
244
+ continue;
245
+ if (metadata.status !== "completed")
246
+ continue;
247
+ const agentId = metadata.agent_id ?? metadata.agent_type;
248
+ counts[agentId] = (counts[agentId] || 0) + 1;
249
+ }
250
+ return counts;
251
+ }
188
252
  /**
189
253
  * Check if a session exists.
190
254
  */
191
- export async function sessionExists(specDir, sessionId) {
192
- const metadataPath = getSessionMetadataPath(specDir, sessionId);
255
+ export async function sessionExists(sessionsDir, sessionId) {
256
+ const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
193
257
  try {
194
258
  await fsPromises.access(metadataPath);
195
259
  return true;
@@ -207,13 +271,13 @@ export async function sessionExists(specDir, sessionId) {
207
271
  *
208
272
  * AC: @session-end-loop-signal ac-signal
209
273
  *
210
- * @param specDir - The .kspec directory path
274
+ * @param sessionsDir - The .kspec directory path
211
275
  * @param sessionId - Session ID
212
276
  * @param reason - Optional reason for ending the loop
213
277
  * @returns Updated metadata or null if session not found
214
278
  */
215
- export async function requestEndLoop(specDir, sessionId, reason) {
216
- const metadata = await getSession(specDir, sessionId);
279
+ export async function requestEndLoop(sessionsDir, sessionId, reason) {
280
+ const metadata = await getSession(sessionsDir, sessionId);
217
281
  if (!metadata) {
218
282
  return null;
219
283
  }
@@ -222,7 +286,7 @@ export async function requestEndLoop(specDir, sessionId, reason) {
222
286
  end_requested: true,
223
287
  end_reason: reason,
224
288
  };
225
- const metadataPath = getSessionMetadataPath(specDir, sessionId);
289
+ const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
226
290
  const content = YAML.stringify(updated, {
227
291
  indent: 2,
228
292
  lineWidth: 100,
@@ -240,12 +304,12 @@ export async function requestEndLoop(specDir, sessionId, reason) {
240
304
  *
241
305
  * AC: @session-end-loop-signal ac-detect
242
306
  *
243
- * @param specDir - The .kspec directory path
307
+ * @param sessionsDir - The .kspec directory path
244
308
  * @param sessionId - Session ID
245
309
  * @returns Object with requested flag and optional reason, or null if session not found
246
310
  */
247
- export async function isEndLoopRequested(specDir, sessionId) {
248
- const metadata = await getSession(specDir, sessionId);
311
+ export async function isEndLoopRequested(sessionsDir, sessionId) {
312
+ const metadata = await getSession(sessionsDir, sessionId);
249
313
  if (!metadata) {
250
314
  return null;
251
315
  }
@@ -263,30 +327,42 @@ export async function isEndLoopRequested(specDir, sessionId) {
263
327
  * AC: @session-end-loop-signal ac-session-close-signal
264
328
  * AC: @session-end-loop-signal ac-session-close-error
265
329
  *
266
- * @param specDir - The .kspec directory path
330
+ * @param sessionsDir - The .kspec directory path
267
331
  * @param sessionId - Session ID
268
332
  * @param status - New status (completed or abandoned)
269
333
  * @param reason - Reason for closing
270
334
  * @returns Updated metadata or null if session not found
271
335
  */
272
- export async function closeSession(specDir, sessionId, status, reason) {
273
- const metadata = await getSession(specDir, sessionId);
336
+ export async function closeSession(sessionsDir, sessionId, status, reason) {
337
+ const metadata = await getSession(sessionsDir, sessionId);
274
338
  if (!metadata) {
275
339
  return null;
276
340
  }
341
+ // AC: @session-summary-cache ac-persist-on-close — compute stats from events.jsonl
342
+ // and persist in session metadata so list endpoints never need to scan event data.
343
+ const [eventCount, iterationCount, tasksCompleted] = await Promise.all([
344
+ countEventLines(sessionsDir, sessionId),
345
+ countIterations(sessionsDir, sessionId),
346
+ countTaskCompletions(sessionsDir, sessionId),
347
+ ]);
277
348
  const updated = {
278
349
  ...metadata,
279
350
  status,
280
351
  ended_at: new Date().toISOString(),
281
352
  close_reason: reason,
353
+ event_count: eventCount,
354
+ iteration_count: iterationCount,
355
+ tasks_completed: tasksCompleted,
282
356
  };
283
- const metadataPath = getSessionMetadataPath(specDir, sessionId);
357
+ const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
284
358
  const content = YAML.stringify(updated, {
285
359
  indent: 2,
286
360
  lineWidth: 100,
287
361
  sortMapEntries: false,
288
362
  });
289
363
  await fsPromises.writeFile(metadataPath, content, "utf-8");
364
+ // AC: @session-branch-worktree ac-commit-boundaries — commit on session close
365
+ await commitAtLifecycleBoundary(sessionsDir, `session: close ${status} (${sessionId})`);
290
366
  return updated;
291
367
  }
292
368
  function isRecord(value) {
@@ -360,7 +436,7 @@ function shouldExternalizeField(pathSegments, value) {
360
436
  }
361
437
  return false;
362
438
  }
363
- async function createBlobPointer(specDir, sessionId, seq, pathSegments, value, context) {
439
+ async function createBlobPointer(sessionsDir, sessionId, seq, pathSegments, value, context) {
364
440
  const content = stringifyPayload(value);
365
441
  const bytes = Buffer.byteLength(content, "utf-8");
366
442
  const sha256 = createHash("sha256").update(content).digest("hex");
@@ -375,7 +451,7 @@ async function createBlobPointer(specDir, sessionId, seq, pathSegments, value, c
375
451
  await fsPromises.mkdir(context.blobDir, { recursive: true });
376
452
  context.ensuredDir = true;
377
453
  }
378
- const absolutePath = path.join(getSessionDir(specDir, sessionId), relativePath);
454
+ const absolutePath = path.join(getSessionDir(sessionsDir, sessionId), relativePath);
379
455
  await fsPromises.writeFile(absolutePath, content, "utf-8");
380
456
  }
381
457
  context.createdBlobs = (context.createdBlobs ?? 0) + 1;
@@ -388,15 +464,15 @@ async function createBlobPointer(specDir, sessionId, seq, pathSegments, value, c
388
464
  preview: toPreview(content),
389
465
  };
390
466
  }
391
- async function externalizeOversizedPayloads(specDir, sessionId, seq, value, pathSegments, context) {
467
+ async function externalizeOversizedPayloads(sessionsDir, sessionId, seq, value, pathSegments, context) {
392
468
  if (isSessionBlobPointer(value)) {
393
469
  return value;
394
470
  }
395
471
  if (shouldExternalizeField(pathSegments, value)) {
396
- return createBlobPointer(specDir, sessionId, seq, pathSegments, value, context);
472
+ return createBlobPointer(sessionsDir, sessionId, seq, pathSegments, value, context);
397
473
  }
398
474
  if (Array.isArray(value)) {
399
- return Promise.all(value.map((entry, idx) => externalizeOversizedPayloads(specDir, sessionId, seq, entry, [
475
+ return Promise.all(value.map((entry, idx) => externalizeOversizedPayloads(sessionsDir, sessionId, seq, entry, [
400
476
  ...pathSegments,
401
477
  String(idx),
402
478
  ], context)));
@@ -404,14 +480,14 @@ async function externalizeOversizedPayloads(specDir, sessionId, seq, value, path
404
480
  if (isRecord(value)) {
405
481
  const next = {};
406
482
  for (const [key, child] of Object.entries(value)) {
407
- next[key] = await externalizeOversizedPayloads(specDir, sessionId, seq, child, [...pathSegments, key], context);
483
+ next[key] = await externalizeOversizedPayloads(sessionsDir, sessionId, seq, child, [...pathSegments, key], context);
408
484
  }
409
485
  return next;
410
486
  }
411
487
  return value;
412
488
  }
413
- function resolveBlobAbsolutePath(specDir, sessionId, relativePath) {
414
- const sessionDir = path.resolve(getSessionDir(specDir, sessionId));
489
+ function resolveBlobAbsolutePath(sessionsDir, sessionId, relativePath) {
490
+ const sessionDir = path.resolve(getSessionDir(sessionsDir, sessionId));
415
491
  const absolutePath = path.resolve(sessionDir, relativePath);
416
492
  if (absolutePath === sessionDir ||
417
493
  absolutePath.startsWith(`${sessionDir}${path.sep}`)) {
@@ -419,8 +495,8 @@ function resolveBlobAbsolutePath(specDir, sessionId, relativePath) {
419
495
  }
420
496
  return null;
421
497
  }
422
- async function resolveBlobPointer(specDir, sessionId, pointer) {
423
- const absolutePath = resolveBlobAbsolutePath(specDir, sessionId, pointer.path);
498
+ async function resolveBlobPointer(sessionsDir, sessionId, pointer) {
499
+ const absolutePath = resolveBlobAbsolutePath(sessionsDir, sessionId, pointer.path);
424
500
  if (!absolutePath) {
425
501
  return { ...pointer, content: pointer.preview };
426
502
  }
@@ -438,17 +514,17 @@ async function resolveBlobPointer(specDir, sessionId, pointer) {
438
514
  * Default flows keep compact pointer objects (preview-only). This helper powers
439
515
  * explicit on-demand blob resolution in session log commands.
440
516
  */
441
- export async function resolveSessionBlobPointers(specDir, sessionId, value) {
517
+ export async function resolveSessionBlobPointers(sessionsDir, sessionId, value) {
442
518
  if (isSessionBlobPointer(value)) {
443
- return resolveBlobPointer(specDir, sessionId, value);
519
+ return resolveBlobPointer(sessionsDir, sessionId, value);
444
520
  }
445
521
  if (Array.isArray(value)) {
446
- return Promise.all(value.map((entry) => resolveSessionBlobPointers(specDir, sessionId, entry)));
522
+ return Promise.all(value.map((entry) => resolveSessionBlobPointers(sessionsDir, sessionId, entry)));
447
523
  }
448
524
  if (isRecord(value)) {
449
525
  const next = {};
450
526
  for (const [key, child] of Object.entries(value)) {
451
- next[key] = await resolveSessionBlobPointers(specDir, sessionId, child);
527
+ next[key] = await resolveSessionBlobPointers(sessionsDir, sessionId, child);
452
528
  }
453
529
  return next;
454
530
  }
@@ -482,8 +558,8 @@ function extractLastEventSeq(content) {
482
558
  * Reads a bounded tail slice for O(1) seq lookup; falls back to full scan only
483
559
  * if the tail slice cannot be parsed (for example, partial line boundary).
484
560
  */
485
- async function getNextEventSeq(specDir, sessionId) {
486
- const eventsPath = getSessionEventsPath(specDir, sessionId);
561
+ async function getNextEventSeq(sessionsDir, sessionId) {
562
+ const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
487
563
  let fileHandle = null;
488
564
  try {
489
565
  fileHandle = await fsPromises.open(eventsPath, "r");
@@ -537,17 +613,17 @@ async function getNextEventSeq(specDir, sessionId) {
537
613
  * (single-process, sequential event logging). If concurrent access is needed
538
614
  * in the future, consider file locking or an in-memory counter per session.
539
615
  *
540
- * @param specDir - The .kspec directory path
616
+ * @param sessionsDir - The .kspec directory path
541
617
  * @param input - Event input (ts and seq are auto-assigned if not provided)
542
618
  * @returns The appended event with auto-assigned fields
543
619
  */
544
- export async function appendEvent(specDir, input) {
545
- const sessionDir = getSessionDir(specDir, input.session_id);
546
- const eventsPath = getSessionEventsPath(specDir, input.session_id);
620
+ export async function appendEvent(sessionsDir, input) {
621
+ const sessionDir = getSessionDir(sessionsDir, input.session_id);
622
+ const eventsPath = getSessionEventsPath(sessionsDir, input.session_id);
547
623
  // Ensure session directory exists (lazy creation)
548
624
  await fsPromises.mkdir(sessionDir, { recursive: true });
549
625
  // Derive next sequence number from the last stored event.
550
- const seq = input.seq ?? (await getNextEventSeq(specDir, input.session_id));
626
+ const seq = input.seq ?? (await getNextEventSeq(sessionsDir, input.session_id));
551
627
  // Build full event
552
628
  const event = {
553
629
  ts: input.ts ?? Date.now(),
@@ -559,8 +635,8 @@ export async function appendEvent(specDir, input) {
559
635
  };
560
636
  // AC: @session-events ac-8, ac-9 - Externalize oversized payload fields
561
637
  // before writing to events.jsonl.
562
- const externalizedData = await externalizeOversizedPayloads(specDir, input.session_id, seq, event.data, [], {
563
- blobDir: getSessionBlobDir(specDir, input.session_id),
638
+ const externalizedData = await externalizeOversizedPayloads(sessionsDir, input.session_id, seq, event.data, [], {
639
+ blobDir: getSessionBlobDir(sessionsDir, input.session_id),
564
640
  ensuredDir: false,
565
641
  });
566
642
  const eventWithGuardrails = {
@@ -574,10 +650,10 @@ export async function appendEvent(specDir, input) {
574
650
  let line = JSON.stringify(validated);
575
651
  if (Buffer.byteLength(line, "utf-8") > EVENT_LINE_MAX_BYTES) {
576
652
  const blobContext = {
577
- blobDir: getSessionBlobDir(specDir, input.session_id),
653
+ blobDir: getSessionBlobDir(sessionsDir, input.session_id),
578
654
  ensuredDir: false,
579
655
  };
580
- const fullDataPointer = await createBlobPointer(specDir, input.session_id, seq, [], validated.data, blobContext);
656
+ const fullDataPointer = await createBlobPointer(sessionsDir, input.session_id, seq, [], validated.data, blobContext);
581
657
  validated = SessionEventSchema.parse({
582
658
  ...validated,
583
659
  data: fullDataPointer,
@@ -599,10 +675,10 @@ export async function appendEvent(specDir, input) {
599
675
  * Writes are atomic (temp-file then rename). When dryRun is enabled, no files
600
676
  * are modified and no blob files are written.
601
677
  */
602
- export async function compactSessionEvents(specDir, sessionId, options = {}) {
678
+ export async function compactSessionEvents(sessionsDir, sessionId, options = {}) {
603
679
  const dryRun = options.dryRun === true;
604
680
  const renameFn = options.renameFn ?? fsPromises.rename;
605
- const eventsPath = getSessionEventsPath(specDir, sessionId);
681
+ const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
606
682
  let content;
607
683
  try {
608
684
  content = await fsPromises.readFile(eventsPath, "utf-8");
@@ -639,7 +715,7 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
639
715
  };
640
716
  }
641
717
  const blobContext = {
642
- blobDir: getSessionBlobDir(specDir, sessionId),
718
+ blobDir: getSessionBlobDir(sessionsDir, sessionId),
643
719
  ensuredDir: false,
644
720
  dryRun,
645
721
  createdBlobs: 0,
@@ -656,14 +732,14 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
656
732
  throw new Error(`Invalid JSON in events log at line ${i + 1}: ${err instanceof Error ? err.message : String(err)}`);
657
733
  }
658
734
  const event = SessionEventSchema.parse(parsed);
659
- const externalizedData = await externalizeOversizedPayloads(specDir, sessionId, event.seq, event.data, [], blobContext);
735
+ const externalizedData = await externalizeOversizedPayloads(sessionsDir, sessionId, event.seq, event.data, [], blobContext);
660
736
  let validated = SessionEventSchema.parse({
661
737
  ...event,
662
738
  data: externalizedData,
663
739
  });
664
740
  let compactedLine = JSON.stringify(validated);
665
741
  if (Buffer.byteLength(compactedLine, "utf-8") > EVENT_LINE_MAX_BYTES) {
666
- const fullDataPointer = await createBlobPointer(specDir, sessionId, event.seq, [], validated.data, blobContext);
742
+ const fullDataPointer = await createBlobPointer(sessionsDir, sessionId, event.seq, [], validated.data, blobContext);
667
743
  validated = SessionEventSchema.parse({
668
744
  ...validated,
669
745
  data: fullDataPointer,
@@ -688,7 +764,7 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
688
764
  };
689
765
  }
690
766
  if (!dryRun) {
691
- const sessionDir = getSessionDir(specDir, sessionId);
767
+ const sessionDir = getSessionDir(sessionsDir, sessionId);
692
768
  const tmpPath = path.join(sessionDir, `.${EVENTS_FILE}.${process.pid}.${Date.now()}.tmp`);
693
769
  try {
694
770
  await fsPromises.writeFile(tmpPath, compactedContent, "utf-8");
@@ -699,6 +775,10 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
699
775
  throw err;
700
776
  }
701
777
  }
778
+ // AC: @session-branch-worktree ac-commit-boundaries — commit on compact
779
+ if (!dryRun) {
780
+ await commitAtLifecycleBoundary(sessionsDir, `session: compact (${sessionId})`);
781
+ }
702
782
  return {
703
783
  events_processed: sourceLines.length,
704
784
  blobs_created: blobContext.createdBlobs ?? 0,
@@ -715,12 +795,12 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
715
795
  *
716
796
  * AC-4: Returns all events in sequence order.
717
797
  *
718
- * @param specDir - The .kspec directory path
798
+ * @param sessionsDir - The .kspec directory path
719
799
  * @param sessionId - Session ID
720
800
  * @returns Array of events sorted by sequence number
721
801
  */
722
- export async function readEvents(specDir, sessionId) {
723
- const eventsPath = getSessionEventsPath(specDir, sessionId);
802
+ export async function readEvents(sessionsDir, sessionId) {
803
+ const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
724
804
  try {
725
805
  const content = await fsPromises.readFile(eventsPath, "utf-8");
726
806
  const lines = content
@@ -745,6 +825,45 @@ export async function readEvents(specDir, sessionId) {
745
825
  return [];
746
826
  }
747
827
  }
828
+ /**
829
+ * Read a single event by sequence number from a session's event log.
830
+ *
831
+ * Scans events.jsonl line-by-line and stops as soon as the matching seq is
832
+ * found, avoiding full parsing of all events for large sessions.
833
+ *
834
+ * AC: @session-event-detail-endpoint ac-single-event-fetch
835
+ *
836
+ * @param sessionsDir - The sessions directory path
837
+ * @param sessionId - Session ID
838
+ * @param seq - Sequence number to find
839
+ * @returns The matching event, or null if not found
840
+ */
841
+ export async function readEventBySeq(sessionsDir, sessionId, seq) {
842
+ const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
843
+ let content;
844
+ try {
845
+ content = await fsPromises.readFile(eventsPath, "utf-8");
846
+ }
847
+ catch {
848
+ return null;
849
+ }
850
+ const lines = content.split("\n");
851
+ for (const line of lines) {
852
+ const trimmed = line.trim();
853
+ if (trimmed.length === 0)
854
+ continue;
855
+ try {
856
+ const raw = JSON.parse(trimmed);
857
+ if (isRecord(raw) && raw.seq === seq) {
858
+ return SessionEventSchema.parse(raw);
859
+ }
860
+ }
861
+ catch {
862
+ // Skip invalid lines
863
+ }
864
+ }
865
+ return null;
866
+ }
748
867
  /**
749
868
  * Deduplicate phased tool_call events.
750
869
  *
@@ -794,14 +913,14 @@ export function deduplicatePhasedToolCalls(events) {
794
913
  /**
795
914
  * Read events within a time range.
796
915
  *
797
- * @param specDir - The .kspec directory path
916
+ * @param sessionsDir - The .kspec directory path
798
917
  * @param sessionId - Session ID
799
918
  * @param since - Start timestamp (inclusive)
800
919
  * @param until - End timestamp (inclusive)
801
920
  * @returns Array of events within the time range
802
921
  */
803
- export async function readEventsSince(specDir, sessionId, since, until) {
804
- const events = await readEvents(specDir, sessionId);
922
+ export async function readEventsSince(sessionsDir, sessionId, since, until) {
923
+ const events = await readEvents(sessionsDir, sessionId);
805
924
  return events.filter((e) => {
806
925
  if (e.ts < since)
807
926
  return false;
@@ -813,12 +932,12 @@ export async function readEventsSince(specDir, sessionId, since, until) {
813
932
  /**
814
933
  * Get the last event in a session.
815
934
  *
816
- * @param specDir - The .kspec directory path
935
+ * @param sessionsDir - The .kspec directory path
817
936
  * @param sessionId - Session ID
818
937
  * @returns The last event or null if no events
819
938
  */
820
- export async function getLastEvent(specDir, sessionId) {
821
- const events = await readEvents(specDir, sessionId);
939
+ export async function getLastEvent(sessionsDir, sessionId) {
940
+ const events = await readEvents(sessionsDir, sessionId);
822
941
  if (events.length === 0) {
823
942
  return null;
824
943
  }
@@ -906,8 +1025,8 @@ export function resolveStaleSessionCriteria(input) {
906
1025
  * Unlike readEvents(), this is strict: corrupt events are surfaced as failures
907
1026
  * so stale auto-close can skip unsafe sessions.
908
1027
  */
909
- export async function getSessionActivityForStaleCheck(specDir, sessionId) {
910
- const metadata = await getSession(specDir, sessionId);
1028
+ export async function getSessionActivityForStaleCheck(sessionsDir, sessionId) {
1029
+ const metadata = await getSession(sessionsDir, sessionId);
911
1030
  if (!metadata) {
912
1031
  return {
913
1032
  ok: false,
@@ -923,7 +1042,7 @@ export async function getSessionActivityForStaleCheck(specDir, sessionId) {
923
1042
  detail: `Session ${sessionId} has invalid started_at timestamp`,
924
1043
  };
925
1044
  }
926
- const eventsPath = getSessionEventsPath(specDir, sessionId);
1045
+ const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
927
1046
  let content;
928
1047
  try {
929
1048
  content = await fsPromises.readFile(eventsPath, "utf-8");
@@ -1019,21 +1138,21 @@ export function evaluateStaleSession(startedAt, activity, criteria, nowMs = Date
1019
1138
  eligible,
1020
1139
  };
1021
1140
  }
1022
- export async function selectStaleActiveSessions(specDir, criteriaInput = {}, nowMs = Date.now()) {
1141
+ export async function selectStaleActiveSessions(sessionsDir, criteriaInput = {}, nowMs = Date.now()) {
1023
1142
  const criteriaResolved = resolveStaleSessionCriteria(criteriaInput);
1024
1143
  if (!criteriaResolved.ok) {
1025
1144
  throw new Error(`${criteriaResolved.message}. ${criteriaResolved.guidance}`);
1026
1145
  }
1027
1146
  const criteria = criteriaResolved.criteria;
1028
- const sessionIds = await listSessions(specDir);
1147
+ const sessionIds = await listSessions(sessionsDir);
1029
1148
  const evaluations = [];
1030
1149
  const candidates = [];
1031
1150
  const skipped = [];
1032
1151
  for (const sessionId of sessionIds) {
1033
- const metadata = await getSession(specDir, sessionId);
1152
+ const metadata = await getSession(sessionsDir, sessionId);
1034
1153
  if (!metadata || metadata.status !== "active")
1035
1154
  continue;
1036
- const activityResult = await getSessionActivityForStaleCheck(specDir, sessionId);
1155
+ const activityResult = await getSessionActivityForStaleCheck(sessionsDir, sessionId);
1037
1156
  if (!activityResult.ok) {
1038
1157
  skipped.push({
1039
1158
  sessionId,
@@ -1081,9 +1200,9 @@ export function buildAutoAbandonedCloseReason(criteria, evaluation) {
1081
1200
  * Apply abandoned metadata to stale session candidates.
1082
1201
  *
1083
1202
  * All updates in a single invocation share one ended_at timestamp, which lets
1084
- * the caller persist and commit the batch atomically with one shadow commit.
1203
+ * the caller persist and commit the batch atomically.
1085
1204
  */
1086
- export async function applyAutoAbandonMetadata(specDir, selection, options) {
1205
+ export async function applyAutoAbandonMetadata(sessionsDir, selection, options) {
1087
1206
  const dryRun = options?.dryRun === true;
1088
1207
  const endedAt = new Date(options?.nowMs ?? Date.now()).toISOString();
1089
1208
  const updates = [];
@@ -1097,7 +1216,7 @@ export async function applyAutoAbandonMetadata(specDir, selection, options) {
1097
1216
  });
1098
1217
  if (dryRun)
1099
1218
  continue;
1100
- const metadata = await getSession(specDir, candidate.sessionId);
1219
+ const metadata = await getSession(sessionsDir, candidate.sessionId);
1101
1220
  if (!metadata)
1102
1221
  continue;
1103
1222
  const updated = {
@@ -1106,7 +1225,7 @@ export async function applyAutoAbandonMetadata(specDir, selection, options) {
1106
1225
  ended_at: endedAt,
1107
1226
  close_reason: closeReason,
1108
1227
  };
1109
- const metadataPath = getSessionMetadataPath(specDir, candidate.sessionId);
1228
+ const metadataPath = getSessionMetadataPath(sessionsDir, candidate.sessionId);
1110
1229
  const content = YAML.stringify(updated, {
1111
1230
  indent: 2,
1112
1231
  lineWidth: 100,
@@ -1114,15 +1233,14 @@ export async function applyAutoAbandonMetadata(specDir, selection, options) {
1114
1233
  });
1115
1234
  await fsPromises.writeFile(metadataPath, content, "utf-8");
1116
1235
  }
1117
- let shadowCommitted;
1118
- if (!dryRun && updates.length > 0 && options?.shadowCommitMessage) {
1119
- shadowCommitted = await shadowAutoCommit(specDir, options.shadowCommitMessage);
1236
+ // AC: @session-branch-worktree ac-commit-boundaries — commit on stale cleanup
1237
+ if (!dryRun && updates.length > 0) {
1238
+ await commitAtLifecycleBoundary(sessionsDir, `session: stale cleanup (${updates.length} abandoned)`);
1120
1239
  }
1121
1240
  return {
1122
1241
  dryRun,
1123
1242
  updatedCount: updates.length,
1124
1243
  updates,
1125
- shadowCommitted,
1126
1244
  };
1127
1245
  }
1128
1246
  // ─── Session Log Summaries ───────────────────────────────────────────────────
@@ -1143,8 +1261,8 @@ function resolveSessionType(metadata) {
1143
1261
  * Count lines in events.jsonl without parsing JSON.
1144
1262
  * Much faster than readEvents() for large files.
1145
1263
  */
1146
- async function countEventLines(specDir, sessionId) {
1147
- const eventsPath = getSessionEventsPath(specDir, sessionId);
1264
+ async function countEventLines(sessionsDir, sessionId) {
1265
+ const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
1148
1266
  try {
1149
1267
  const content = await fsPromises.readFile(eventsPath, "utf-8");
1150
1268
  if (!content.trim())
@@ -1158,8 +1276,8 @@ async function countEventLines(specDir, sessionId) {
1158
1276
  /**
1159
1277
  * Count context-iter-*.json files for a session (iteration count).
1160
1278
  */
1161
- async function countIterations(specDir, sessionId) {
1162
- const sessionDir = getSessionDir(specDir, sessionId);
1279
+ async function countIterations(sessionsDir, sessionId) {
1280
+ const sessionDir = getSessionDir(sessionsDir, sessionId);
1163
1281
  try {
1164
1282
  const entries = await fsPromises.readdir(sessionDir);
1165
1283
  return entries.filter((e) => e.startsWith("context-iter-") && e.endsWith(".json")).length;
@@ -1181,8 +1299,8 @@ async function countIterations(specDir, sessionId) {
1181
1299
  *
1182
1300
  * We use a fast substring check before JSON parsing for performance.
1183
1301
  */
1184
- async function countTaskCompletions(specDir, sessionId) {
1185
- const eventsPath = getSessionEventsPath(specDir, sessionId);
1302
+ async function countTaskCompletions(sessionsDir, sessionId) {
1303
+ const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
1186
1304
  try {
1187
1305
  const content = await fsPromises.readFile(eventsPath, "utf-8");
1188
1306
  if (!content.trim())
@@ -1221,18 +1339,18 @@ async function countTaskCompletions(specDir, sessionId) {
1221
1339
  *
1222
1340
  * Gathers metadata and computes metrics lazily (only parses what's needed).
1223
1341
  *
1224
- * @param specDir - The .kspec directory path
1342
+ * @param sessionsDir - The .kspec directory path
1225
1343
  * @param sessionId - Session ID
1226
1344
  * @returns Session summary or null if session doesn't exist
1227
1345
  */
1228
- export async function getSessionLogSummary(specDir, sessionId) {
1229
- const metadata = await getSession(specDir, sessionId);
1346
+ export async function getSessionLogSummary(sessionsDir, sessionId) {
1347
+ const metadata = await getSession(sessionsDir, sessionId);
1230
1348
  if (!metadata)
1231
1349
  return null;
1232
1350
  const [eventCount, iterationCount, tasksCompleted] = await Promise.all([
1233
- countEventLines(specDir, sessionId),
1234
- countIterationsBoundaryAware(specDir, sessionId),
1235
- countTaskCompletions(specDir, sessionId),
1351
+ countEventLines(sessionsDir, sessionId),
1352
+ countIterationsBoundaryAware(sessionsDir, sessionId),
1353
+ countTaskCompletions(sessionsDir, sessionId),
1236
1354
  ]);
1237
1355
  const startMs = new Date(metadata.started_at).getTime();
1238
1356
  const endMs = metadata.ended_at
@@ -1243,7 +1361,10 @@ export async function getSessionLogSummary(specDir, sessionId) {
1243
1361
  id: metadata.id,
1244
1362
  status: metadata.status,
1245
1363
  agent_type: metadata.agent_type,
1364
+ agent_id: metadata.agent_id,
1246
1365
  session_type: resolveSessionType(metadata),
1366
+ trigger: metadata.trigger,
1367
+ task_id: metadata.task_id,
1247
1368
  started_at: metadata.started_at,
1248
1369
  ended_at: metadata.ended_at,
1249
1370
  duration_ms: durationMs,
@@ -1252,15 +1373,48 @@ export async function getSessionLogSummary(specDir, sessionId) {
1252
1373
  tasks_completed: tasksCompleted,
1253
1374
  };
1254
1375
  }
1376
+ /**
1377
+ * Get session metadata only — reads session.yaml without touching events.jsonl.
1378
+ *
1379
+ * AC: @session-list-pagination-api ac-metadata-only — List endpoint reads only
1380
+ * session.yaml metadata.
1381
+ * AC: @session-summary-cache ac-persist-on-close — Closed sessions have stats
1382
+ * persisted in metadata; read them instead of hardcoding 0.
1383
+ */
1384
+ export async function getSessionMetadataOnly(sessionsDir, sessionId) {
1385
+ const metadata = await getSession(sessionsDir, sessionId);
1386
+ if (!metadata)
1387
+ return null;
1388
+ const startMs = new Date(metadata.started_at).getTime();
1389
+ const endMs = metadata.ended_at
1390
+ ? new Date(metadata.ended_at).getTime()
1391
+ : Date.now();
1392
+ const durationMs = endMs - startMs;
1393
+ return {
1394
+ id: metadata.id,
1395
+ status: metadata.status,
1396
+ agent_type: metadata.agent_type,
1397
+ agent_id: metadata.agent_id,
1398
+ session_type: resolveSessionType(metadata),
1399
+ trigger: metadata.trigger,
1400
+ task_id: metadata.task_id,
1401
+ started_at: metadata.started_at,
1402
+ ended_at: metadata.ended_at,
1403
+ duration_ms: durationMs,
1404
+ event_count: metadata.event_count ?? 0,
1405
+ iteration_count: metadata.iteration_count ?? 0,
1406
+ tasks_completed: metadata.tasks_completed ?? 0,
1407
+ };
1408
+ }
1255
1409
  /**
1256
1410
  * Get summaries for all sessions.
1257
1411
  *
1258
- * @param specDir - The .kspec directory path
1412
+ * @param sessionsDir - The .kspec directory path
1259
1413
  * @returns Array of session summaries
1260
1414
  */
1261
- export async function getAllSessionLogSummaries(specDir) {
1262
- const sessionIds = await listSessions(specDir);
1263
- const summaries = await Promise.all(sessionIds.map((id) => getSessionLogSummary(specDir, id)));
1415
+ export async function getAllSessionLogSummaries(sessionsDir) {
1416
+ const sessionIds = await listSessions(sessionsDir);
1417
+ const summaries = await Promise.all(sessionIds.map((id) => getSessionLogSummary(sessionsDir, id)));
1264
1418
  return summaries.filter((s) => s !== null);
1265
1419
  }
1266
1420
  // ─── Context Snapshots ───────────────────────────────────────────────────────
@@ -1270,14 +1424,14 @@ export async function getAllSessionLogSummaries(specDir) {
1270
1424
  * This creates an audit trail of what context the agent saw at each iteration,
1271
1425
  * useful for debugging and reviewing agent behavior.
1272
1426
  *
1273
- * @param specDir - The .kspec directory path
1427
+ * @param sessionsDir - The .kspec directory path
1274
1428
  * @param sessionId - Session ID
1275
1429
  * @param iteration - Iteration number
1276
1430
  * @param context - The session context data
1277
1431
  */
1278
- export async function saveSessionContext(specDir, sessionId, iteration, context) {
1279
- const sessionDir = getSessionDir(specDir, sessionId);
1280
- const contextPath = getSessionContextPath(specDir, sessionId, iteration);
1432
+ export async function saveSessionContext(sessionsDir, sessionId, iteration, context) {
1433
+ const sessionDir = getSessionDir(sessionsDir, sessionId);
1434
+ const contextPath = getSessionContextPath(sessionsDir, sessionId, iteration);
1281
1435
  // Ensure session directory exists
1282
1436
  await fsPromises.mkdir(sessionDir, { recursive: true });
1283
1437
  // Write context snapshot as pretty JSON
@@ -1287,13 +1441,13 @@ export async function saveSessionContext(specDir, sessionId, iteration, context)
1287
1441
  /**
1288
1442
  * Read session context snapshot for a given iteration.
1289
1443
  *
1290
- * @param specDir - The .kspec directory path
1444
+ * @param sessionsDir - The .kspec directory path
1291
1445
  * @param sessionId - Session ID
1292
1446
  * @param iteration - Iteration number
1293
1447
  * @returns The context snapshot or null if not found
1294
1448
  */
1295
- export async function readSessionContext(specDir, sessionId, iteration) {
1296
- const contextPath = getSessionContextPath(specDir, sessionId, iteration);
1449
+ export async function readSessionContext(sessionsDir, sessionId, iteration) {
1450
+ const contextPath = getSessionContextPath(sessionsDir, sessionId, iteration);
1297
1451
  try {
1298
1452
  const content = await fsPromises.readFile(contextPath, "utf-8");
1299
1453
  return JSON.parse(content);
@@ -1307,12 +1461,12 @@ export async function readSessionContext(specDir, sessionId, iteration) {
1307
1461
  *
1308
1462
  * AC: @session-log-show ac-7, ac-8, ac-9
1309
1463
  *
1310
- * @param specDir - The .kspec directory path
1464
+ * @param sessionsDir - The .kspec directory path
1311
1465
  * @param idOrPrefix - Full session ID or prefix (e.g., first 8 chars)
1312
1466
  * @returns Resolution result
1313
1467
  */
1314
- export async function resolveSessionId(specDir, idOrPrefix) {
1315
- const sessionIds = await listSessions(specDir);
1468
+ export async function resolveSessionId(sessionsDir, idOrPrefix) {
1469
+ const sessionIds = await listSessions(sessionsDir);
1316
1470
  // First, try exact match
1317
1471
  if (sessionIds.includes(idOrPrefix)) {
1318
1472
  return { ok: true, id: idOrPrefix };
@@ -1331,8 +1485,8 @@ export async function resolveSessionId(specDir, idOrPrefix) {
1331
1485
  /**
1332
1486
  * Get iteration number from a context snapshot file.
1333
1487
  */
1334
- async function getIterationNumbers(specDir, sessionId) {
1335
- const sessionDir = getSessionDir(specDir, sessionId);
1488
+ async function getIterationNumbers(sessionsDir, sessionId) {
1489
+ const sessionDir = getSessionDir(sessionsDir, sessionId);
1336
1490
  try {
1337
1491
  const entries = await fsPromises.readdir(sessionDir);
1338
1492
  const iterations = [];
@@ -1524,14 +1678,14 @@ function boundaryIterationGrouping(events, boundaries) {
1524
1678
  *
1525
1679
  * AC: @session-log-show ac-2, ac-10
1526
1680
  */
1527
- async function computeIterationSummaries(specDir, sessionId) {
1528
- const events = await readEvents(specDir, sessionId);
1681
+ async function computeIterationSummaries(sessionsDir, sessionId) {
1682
+ const events = await readEvents(sessionsDir, sessionId);
1529
1683
  const boundaries = findIterationBoundaries(events);
1530
1684
  if (boundaries.length > 0) {
1531
1685
  return boundaryIterationGrouping(events, boundaries);
1532
1686
  }
1533
1687
  // Legacy fallback: no prompt.sent boundaries with phase "task-work"
1534
- const snapshotIterations = await getIterationNumbers(specDir, sessionId);
1688
+ const snapshotIterations = await getIterationNumbers(sessionsDir, sessionId);
1535
1689
  return legacyIterationGrouping(events, snapshotIterations);
1536
1690
  }
1537
1691
  /**
@@ -1542,14 +1696,14 @@ async function computeIterationSummaries(specDir, sessionId) {
1542
1696
  *
1543
1697
  * Falls back to counting context-iter-*.json files when no boundaries exist.
1544
1698
  */
1545
- async function countIterationsBoundaryAware(specDir, sessionId) {
1546
- const events = await readEvents(specDir, sessionId);
1699
+ async function countIterationsBoundaryAware(sessionsDir, sessionId) {
1700
+ const events = await readEvents(sessionsDir, sessionId);
1547
1701
  const boundaries = findIterationBoundaries(events);
1548
1702
  if (boundaries.length > 0) {
1549
1703
  return boundaries.length;
1550
1704
  }
1551
1705
  // Legacy fallback: count from context snapshots and event data
1552
- const snapshotIterations = await getIterationNumbers(specDir, sessionId);
1706
+ const snapshotIterations = await getIterationNumbers(sessionsDir, sessionId);
1553
1707
  const allIterations = new Set(snapshotIterations);
1554
1708
  for (const event of events) {
1555
1709
  const data = event.data;
@@ -1562,17 +1716,17 @@ async function countIterationsBoundaryAware(specDir, sessionId) {
1562
1716
  /**
1563
1717
  * Get full session detail for session log show.
1564
1718
  *
1565
- * @param specDir - The .kspec directory path
1719
+ * @param sessionsDir - The .kspec directory path
1566
1720
  * @param sessionId - Session ID (must be resolved first)
1567
1721
  * @returns Session detail or null if not found
1568
1722
  */
1569
- export async function getSessionLogDetail(specDir, sessionId) {
1570
- const metadata = await getSession(specDir, sessionId);
1723
+ export async function getSessionLogDetail(sessionsDir, sessionId) {
1724
+ const metadata = await getSession(sessionsDir, sessionId);
1571
1725
  if (!metadata)
1572
1726
  return null;
1573
1727
  const [eventCount, iterations] = await Promise.all([
1574
- countEventLines(specDir, sessionId),
1575
- computeIterationSummaries(specDir, sessionId),
1728
+ countEventLines(sessionsDir, sessionId),
1729
+ computeIterationSummaries(sessionsDir, sessionId),
1576
1730
  ]);
1577
1731
  const startMs = new Date(metadata.started_at).getTime();
1578
1732
  const endMs = metadata.ended_at
@@ -1624,6 +1778,7 @@ export function computeSessionLogStats(summaries) {
1624
1778
  abandoned: 0,
1625
1779
  timed_out: 0,
1626
1780
  failed: 0,
1781
+ stalled: 0,
1627
1782
  };
1628
1783
  for (const s of summaries) {
1629
1784
  totalEvents += s.event_count;
@@ -1635,7 +1790,7 @@ export function computeSessionLogStats(summaries) {
1635
1790
  const n = summaries.length;
1636
1791
  // Build status breakdown
1637
1792
  const statusBreakdown = [];
1638
- for (const status of ["completed", "active", "abandoned", "timed_out", "failed"]) {
1793
+ for (const status of ["completed", "active", "abandoned", "timed_out", "failed", "stalled"]) {
1639
1794
  const count = statusCounts[status] || 0;
1640
1795
  if (count > 0) {
1641
1796
  statusBreakdown.push({
@@ -1665,11 +1820,11 @@ export function computeSessionLogStats(summaries) {
1665
1820
  *
1666
1821
  * AC: @session-log-stats ac-6
1667
1822
  */
1668
- export async function computeToolUsageStats(specDir, sessionIds, limit = 10) {
1823
+ export async function computeToolUsageStats(sessionsDir, sessionIds, limit = 10) {
1669
1824
  const toolCounts = {};
1670
1825
  let totalToolCalls = 0;
1671
1826
  for (const sessionId of sessionIds) {
1672
- const eventsPath = getSessionEventsPath(specDir, sessionId);
1827
+ const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
1673
1828
  try {
1674
1829
  const content = await fsPromises.readFile(eventsPath, "utf-8");
1675
1830
  if (!content.trim())
@@ -1824,34 +1979,45 @@ function extractContentExcerpt(data, pattern, maxLength = 200) {
1824
1979
  *
1825
1980
  * AC: @session-log-search ac-1, ac-2, ac-3, ac-5, ac-7
1826
1981
  *
1827
- * @param specDir - The .kspec directory path
1982
+ * @param sessionsDir - The .kspec directory path
1828
1983
  * @param pattern - Case-insensitive substring to search for
1829
1984
  * @param options - Search filtering options
1830
1985
  * @returns Array of search results grouped by session
1831
1986
  */
1832
- export async function searchSessionEvents(specDir, pattern, options = {}) {
1987
+ export async function searchSessionEvents(sessionsDir, pattern, options = {}) {
1833
1988
  // Defense-in-depth: normalize limit to a valid positive integer
1834
1989
  const rawLimit = options.limit ?? 50;
1835
1990
  const limit = Number.isNaN(rawLimit) || rawLimit <= 0 ? 50 : rawLimit;
1836
1991
  const lowerPattern = pattern.toLowerCase();
1837
1992
  const resolveBlobs = options.resolveBlobs ?? false;
1838
- // Get all session summaries for metadata filtering
1839
- const allSummaries = await getAllSessionLogSummaries(specDir);
1840
- // AC: @session-log-search ac-3 - Pre-filter by --since
1841
- let filteredSummaries = allSummaries;
1842
- if (options.sinceDate) {
1843
- filteredSummaries = filteredSummaries.filter((s) => new Date(s.started_at) >= options.sinceDate);
1993
+ // Use pre-filtered session IDs if provided, otherwise load and filter
1994
+ let filteredSummaries;
1995
+ if (options.sessionSummaries) {
1996
+ filteredSummaries = [...options.sessionSummaries];
1844
1997
  }
1845
- // AC: @session-log-search ac-7 - Pre-filter by --agent
1846
- if (options.agentType) {
1847
- filteredSummaries = filteredSummaries.filter((s) => s.agent_type === options.agentType);
1998
+ else if (options.sessionIds) {
1999
+ const idSet = new Set(options.sessionIds);
2000
+ const allSummaries = await getAllSessionLogSummaries(sessionsDir);
2001
+ filteredSummaries = allSummaries.filter((s) => idSet.has(s.id));
2002
+ }
2003
+ else {
2004
+ const allSummaries = await getAllSessionLogSummaries(sessionsDir);
2005
+ // AC: @session-log-search ac-3 - Pre-filter by --since
2006
+ filteredSummaries = allSummaries;
2007
+ if (options.sinceDate) {
2008
+ filteredSummaries = filteredSummaries.filter((s) => new Date(s.started_at) >= options.sinceDate);
2009
+ }
2010
+ // AC: @session-log-search ac-7 - Pre-filter by --agent
2011
+ if (options.agentType) {
2012
+ filteredSummaries = filteredSummaries.filter((s) => s.agent_type === options.agentType);
2013
+ }
1848
2014
  }
1849
2015
  const results = [];
1850
2016
  let totalMatches = 0;
1851
2017
  for (const summary of filteredSummaries) {
1852
2018
  if (totalMatches >= limit)
1853
2019
  break;
1854
- const eventsPath = getSessionEventsPath(specDir, summary.id);
2020
+ const eventsPath = getSessionEventsPath(sessionsDir, summary.id);
1855
2021
  let content;
1856
2022
  try {
1857
2023
  content = await fsPromises.readFile(eventsPath, "utf-8");
@@ -1890,7 +2056,7 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
1890
2056
  }
1891
2057
  }
1892
2058
  const searchableData = resolveBlobs
1893
- ? await resolveSessionBlobPointers(specDir, summary.id, event.data)
2059
+ ? await resolveSessionBlobPointers(sessionsDir, summary.id, event.data)
1894
2060
  : event.data;
1895
2061
  // Verify match in stringified data (not just line, in case pattern
1896
2062
  // appears in metadata)
@@ -1900,6 +2066,7 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
1900
2066
  // AC: @session-log-search ac-4 - Create match with excerpt
1901
2067
  matches.push({
1902
2068
  session_id: summary.id,
2069
+ event_seq: typeof event.seq === "number" ? event.seq : -1,
1903
2070
  timestamp: event.ts,
1904
2071
  event_type: event.type,
1905
2072
  content_excerpt: extractContentExcerpt(searchableData, pattern, 200),
@@ -1934,13 +2101,13 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
1934
2101
  * AC: @session-creation-and-env-injection ac-budget-local
1935
2102
  * AC: @session-creation-and-env-injection ac-library
1936
2103
  *
1937
- * @param specDir - The .kspec directory path
2104
+ * @param sessionsDir - The .kspec directory path
1938
2105
  * @param input - Session creation parameters
1939
2106
  * @returns Session metadata and optional budget (no console output)
1940
2107
  */
1941
- export async function createSessionWithBudget(specDir, input) {
2108
+ export async function createSessionWithBudget(sessionsDir, input) {
1942
2109
  // Create session
1943
- const session = await createSession(specDir, {
2110
+ const session = await createSession(sessionsDir, {
1944
2111
  id: input.id,
1945
2112
  agent_type: input.agent_type,
1946
2113
  task_id: input.task_id,
@@ -1948,7 +2115,7 @@ export async function createSessionWithBudget(specDir, input) {
1948
2115
  // Optionally create budget
1949
2116
  let budget = null;
1950
2117
  if (input.budget !== undefined && input.budget > 0) {
1951
- budget = await createBudget(specDir, input.id, input.budget);
2118
+ budget = await createBudget(sessionsDir, input.id, input.budget);
1952
2119
  }
1953
2120
  return {
1954
2121
  session_id: input.id,
@@ -2315,13 +2482,13 @@ export async function removeEnvForAdapter(adapterId, previousValue) {
2315
2482
  *
2316
2483
  * AC: @session-creation-and-env-injection ac-invalid-session
2317
2484
  *
2318
- * @param specDir - The .kspec directory path
2485
+ * @param sessionsDir - The .kspec directory path
2319
2486
  * @param sessionId - The session ID to validate
2320
2487
  * @returns Validation result with error details if invalid
2321
2488
  */
2322
- export async function validateSessionId(specDir, sessionId) {
2489
+ export async function validateSessionId(sessionsDir, sessionId) {
2323
2490
  // Check if session directory exists
2324
- const exists = await sessionExists(specDir, sessionId);
2491
+ const exists = await sessionExists(sessionsDir, sessionId);
2325
2492
  if (!exists) {
2326
2493
  return {
2327
2494
  valid: false,
@@ -2330,7 +2497,7 @@ export async function validateSessionId(specDir, sessionId) {
2330
2497
  };
2331
2498
  }
2332
2499
  // Try to read and validate session metadata
2333
- const session = await getSession(specDir, sessionId);
2500
+ const session = await getSession(sessionsDir, sessionId);
2334
2501
  if (!session) {
2335
2502
  return {
2336
2503
  valid: false,
@@ -2357,24 +2524,24 @@ async function writeBudgetAtomic(filePath, budget) {
2357
2524
  /**
2358
2525
  * Create a budget for a session.
2359
2526
  *
2360
- * Writes budget.json to .kspec/sessions/{id}/ on the local filesystem
2527
+ * Writes budget.json to .kspec-sessions/{id}/ on the local filesystem
2361
2528
  * (NOT committed to shadow branch).
2362
2529
  *
2363
2530
  * AC: @session-creation-and-env-injection ac-budget
2364
2531
  * AC: @session-creation-and-env-injection ac-budget-local
2365
2532
  *
2366
- * @param specDir - The .kspec directory path
2533
+ * @param sessionsDir - The .kspec directory path
2367
2534
  * @param sessionId - Session ID
2368
2535
  * @param maxPerCycle - Maximum tasks allowed per cycle
2369
2536
  * @returns The created budget
2370
2537
  */
2371
- export async function createBudget(specDir, sessionId, maxPerCycle) {
2538
+ export async function createBudget(sessionsDir, sessionId, maxPerCycle) {
2372
2539
  const budget = {
2373
2540
  max_per_cycle: maxPerCycle,
2374
2541
  started_this_cycle: 0,
2375
2542
  };
2376
2543
  const validated = TaskBudgetSchema.parse(budget);
2377
- const budgetPath = getSessionBudgetPath(specDir, sessionId);
2544
+ const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
2378
2545
  await writeBudgetAtomic(budgetPath, validated);
2379
2546
  return validated;
2380
2547
  }
@@ -2383,12 +2550,12 @@ export async function createBudget(specDir, sessionId, maxPerCycle) {
2383
2550
  *
2384
2551
  * AC: @task-budget-enforcement ac-no-budget
2385
2552
  *
2386
- * @param specDir - The .kspec directory path
2553
+ * @param sessionsDir - The .kspec directory path
2387
2554
  * @param sessionId - Session ID
2388
2555
  * @returns Budget or null if no budget configured (opt-in)
2389
2556
  */
2390
- export async function getBudget(specDir, sessionId) {
2391
- const budgetPath = getSessionBudgetPath(specDir, sessionId);
2557
+ export async function getBudget(sessionsDir, sessionId) {
2558
+ const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
2392
2559
  let content;
2393
2560
  try {
2394
2561
  content = await fsPromises.readFile(budgetPath, "utf-8");
@@ -2414,16 +2581,24 @@ export async function getBudget(specDir, sessionId) {
2414
2581
  * AC: @task-budget-enforcement ac-no-budget
2415
2582
  * AC: @task-budget-enforcement ac-no-session
2416
2583
  *
2417
- * @param specDir - The .kspec directory path
2584
+ * @param sessionsDir - The .kspec directory path
2418
2585
  * @param sessionId - Session ID, or undefined if KSPEC_SESSION_ID not set
2419
2586
  * @returns Budget check result
2420
2587
  */
2421
- export async function checkBudget(specDir, sessionId) {
2588
+ export async function checkBudget(sessionsDir, sessionId) {
2422
2589
  // AC: @task-budget-enforcement ac-no-session — no session means no check
2423
2590
  if (!sessionId) {
2424
2591
  return { allowed: true };
2425
2592
  }
2426
- const budget = await getBudget(specDir, sessionId);
2593
+ // Skip budget enforcement when session exists but is not active (stale KSPEC_SESSION_ID).
2594
+ // A completed, abandoned, timed_out, failed, or stalled session should not block task starts.
2595
+ // If session metadata is missing, proceed with normal budget checks — the budget file
2596
+ // itself is the authority on whether enforcement applies.
2597
+ const session = await getSession(sessionsDir, sessionId);
2598
+ if (session && session.status !== "active") {
2599
+ return { allowed: true };
2600
+ }
2601
+ const budget = await getBudget(sessionsDir, sessionId);
2427
2602
  // AC: @task-budget-enforcement ac-no-budget — no budget means no check
2428
2603
  if (!budget) {
2429
2604
  return { allowed: true };
@@ -2448,12 +2623,12 @@ export async function checkBudget(specDir, sessionId) {
2448
2623
  * AC: @task-budget-enforcement ac-increment
2449
2624
  * AC: @task-budget-enforcement ac-atomic-write
2450
2625
  *
2451
- * @param specDir - The .kspec directory path
2626
+ * @param sessionsDir - The .kspec directory path
2452
2627
  * @param sessionId - Session ID
2453
2628
  * @returns Updated budget, or null if no budget configured
2454
2629
  */
2455
- export async function incrementBudget(specDir, sessionId) {
2456
- const budget = await getBudget(specDir, sessionId);
2630
+ export async function incrementBudget(sessionsDir, sessionId) {
2631
+ const budget = await getBudget(sessionsDir, sessionId);
2457
2632
  if (!budget) {
2458
2633
  return null;
2459
2634
  }
@@ -2462,7 +2637,7 @@ export async function incrementBudget(specDir, sessionId) {
2462
2637
  started_this_cycle: budget.started_this_cycle + 1,
2463
2638
  };
2464
2639
  const validated = TaskBudgetSchema.parse(updated);
2465
- const budgetPath = getSessionBudgetPath(specDir, sessionId);
2640
+ const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
2466
2641
  await writeBudgetAtomic(budgetPath, validated);
2467
2642
  return validated;
2468
2643
  }
@@ -2475,12 +2650,12 @@ export async function incrementBudget(specDir, sessionId) {
2475
2650
  * AC: @task-budget-enforcement ac-reset
2476
2651
  * AC: @task-budget-enforcement ac-atomic-write
2477
2652
  *
2478
- * @param specDir - The .kspec directory path
2653
+ * @param sessionsDir - The .kspec directory path
2479
2654
  * @param sessionId - Session ID
2480
2655
  * @returns Updated budget, or null if no budget configured
2481
2656
  */
2482
- export async function resetBudget(specDir, sessionId) {
2483
- const budget = await getBudget(specDir, sessionId);
2657
+ export async function resetBudget(sessionsDir, sessionId) {
2658
+ const budget = await getBudget(sessionsDir, sessionId);
2484
2659
  if (!budget) {
2485
2660
  return null;
2486
2661
  }
@@ -2489,7 +2664,7 @@ export async function resetBudget(specDir, sessionId) {
2489
2664
  started_this_cycle: 0,
2490
2665
  };
2491
2666
  const validated = TaskBudgetSchema.parse(updated);
2492
- const budgetPath = getSessionBudgetPath(specDir, sessionId);
2667
+ const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
2493
2668
  await writeBudgetAtomic(budgetPath, validated);
2494
2669
  return validated;
2495
2670
  }