@kynetic-ai/spec 0.11.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 (501) 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 +119 -10
  7. package/dist/agent-runtime/dispatch.d.ts.map +1 -1
  8. package/dist/agent-runtime/dispatch.js +1154 -219
  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 +171 -59
  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/plan-import.d.ts +3 -3
  70. package/dist/cli/commands/plan-import.d.ts.map +1 -1
  71. package/dist/cli/commands/plan-import.js +213 -528
  72. package/dist/cli/commands/plan-import.js.map +1 -1
  73. package/dist/cli/commands/plan.d.ts.map +1 -1
  74. package/dist/cli/commands/plan.js +533 -83
  75. package/dist/cli/commands/plan.js.map +1 -1
  76. package/dist/cli/commands/review.d.ts +14 -0
  77. package/dist/cli/commands/review.d.ts.map +1 -0
  78. package/dist/cli/commands/review.js +1142 -0
  79. package/dist/cli/commands/review.js.map +1 -0
  80. package/dist/cli/commands/serve.d.ts +1 -0
  81. package/dist/cli/commands/serve.d.ts.map +1 -1
  82. package/dist/cli/commands/serve.js +33 -10
  83. package/dist/cli/commands/serve.js.map +1 -1
  84. package/dist/cli/commands/session/checkpoint.d.ts +2 -4
  85. package/dist/cli/commands/session/checkpoint.d.ts.map +1 -1
  86. package/dist/cli/commands/session/checkpoint.js +6 -107
  87. package/dist/cli/commands/session/checkpoint.js.map +1 -1
  88. package/dist/cli/commands/session/commands.d.ts.map +1 -1
  89. package/dist/cli/commands/session/commands.js +33 -23
  90. package/dist/cli/commands/session/commands.js.map +1 -1
  91. package/dist/cli/commands/session/compact.js +4 -4
  92. package/dist/cli/commands/session/compact.js.map +1 -1
  93. package/dist/cli/commands/session/create.js +2 -2
  94. package/dist/cli/commands/session/create.js.map +1 -1
  95. package/dist/cli/commands/session/format.d.ts.map +1 -1
  96. package/dist/cli/commands/session/format.js +1 -6
  97. package/dist/cli/commands/session/format.js.map +1 -1
  98. package/dist/cli/commands/session/log.d.ts +32 -7
  99. package/dist/cli/commands/session/log.d.ts.map +1 -1
  100. package/dist/cli/commands/session/log.js +166 -60
  101. package/dist/cli/commands/session/log.js.map +1 -1
  102. package/dist/cli/commands/session/migrate.d.ts +9 -0
  103. package/dist/cli/commands/session/migrate.d.ts.map +1 -0
  104. package/dist/cli/commands/session/migrate.js +46 -0
  105. package/dist/cli/commands/session/migrate.js.map +1 -0
  106. package/dist/cli/commands/session/stale-close.d.ts.map +1 -1
  107. package/dist/cli/commands/session/stale-close.js +5 -8
  108. package/dist/cli/commands/session/stale-close.js.map +1 -1
  109. package/dist/cli/commands/session/types.d.ts +1 -1
  110. package/dist/cli/commands/session/types.d.ts.map +1 -1
  111. package/dist/cli/commands/setup.d.ts +2 -2
  112. package/dist/cli/commands/setup.d.ts.map +1 -1
  113. package/dist/cli/commands/setup.js +287 -257
  114. package/dist/cli/commands/setup.js.map +1 -1
  115. package/dist/cli/commands/shadow.d.ts.map +1 -1
  116. package/dist/cli/commands/shadow.js +147 -31
  117. package/dist/cli/commands/shadow.js.map +1 -1
  118. package/dist/cli/commands/skill-crud.d.ts +7 -0
  119. package/dist/cli/commands/skill-crud.d.ts.map +1 -1
  120. package/dist/cli/commands/skill-crud.js +41 -18
  121. package/dist/cli/commands/skill-crud.js.map +1 -1
  122. package/dist/cli/commands/skill-diff.d.ts.map +1 -1
  123. package/dist/cli/commands/skill-diff.js +29 -3
  124. package/dist/cli/commands/skill-diff.js.map +1 -1
  125. package/dist/cli/commands/skill-install.d.ts.map +1 -1
  126. package/dist/cli/commands/skill-install.js +5 -4
  127. package/dist/cli/commands/skill-install.js.map +1 -1
  128. package/dist/cli/commands/task.d.ts.map +1 -1
  129. package/dist/cli/commands/task.js +359 -49
  130. package/dist/cli/commands/task.js.map +1 -1
  131. package/dist/cli/commands/trait.d.ts.map +1 -1
  132. package/dist/cli/commands/trait.js +5 -27
  133. package/dist/cli/commands/trait.js.map +1 -1
  134. package/dist/cli/commands/validate.d.ts.map +1 -1
  135. package/dist/cli/commands/validate.js +113 -52
  136. package/dist/cli/commands/validate.js.map +1 -1
  137. package/dist/cli/index.d.ts.map +1 -1
  138. package/dist/cli/index.js +69 -2
  139. package/dist/cli/index.js.map +1 -1
  140. package/dist/cli/output.d.ts +26 -0
  141. package/dist/cli/output.d.ts.map +1 -1
  142. package/dist/cli/output.js +108 -1
  143. package/dist/cli/output.js.map +1 -1
  144. package/dist/cli/sync-mode.d.ts +44 -0
  145. package/dist/cli/sync-mode.d.ts.map +1 -0
  146. package/dist/cli/sync-mode.js +64 -0
  147. package/dist/cli/sync-mode.js.map +1 -0
  148. package/dist/daemon/middleware/project-context.ts +25 -7
  149. package/dist/daemon/project-context.ts +18 -0
  150. package/dist/daemon/routes/agent-dispatch.ts +99 -22
  151. package/dist/daemon/routes/aggregation.ts +184 -0
  152. package/dist/daemon/routes/inbox.ts +5 -0
  153. package/dist/daemon/routes/items.ts +145 -0
  154. package/dist/daemon/routes/meta.ts +1 -1
  155. package/dist/daemon/routes/projects.ts +28 -6
  156. package/dist/daemon/routes/ref-resolution.ts +119 -0
  157. package/dist/daemon/routes/refs.ts +42 -0
  158. package/dist/daemon/routes/session-related.ts +140 -0
  159. package/dist/daemon/routes/sessions.ts +420 -19
  160. package/dist/daemon/routes/tasks.ts +62 -5
  161. package/dist/daemon/routes/triage.ts +40 -1
  162. package/dist/daemon/server.ts +143 -49
  163. package/dist/daemon/session-sync.ts +11 -0
  164. package/dist/daemon/shadow-sync.ts +11 -0
  165. package/dist/daemon/watcher.ts +56 -5
  166. package/dist/daemon/websocket/project-resolution.ts +77 -0
  167. package/dist/export/json.d.ts.map +1 -1
  168. package/dist/export/json.js +104 -1
  169. package/dist/export/json.js.map +1 -1
  170. package/dist/export/types.d.ts +52 -1
  171. package/dist/export/types.d.ts.map +1 -1
  172. package/dist/index.d.ts +1 -0
  173. package/dist/index.d.ts.map +1 -1
  174. package/dist/index.js +1 -0
  175. package/dist/index.js.map +1 -1
  176. package/dist/parser/agent-detection.d.ts +1 -1
  177. package/dist/parser/agent-detection.d.ts.map +1 -1
  178. package/dist/parser/agent-detection.js +10 -0
  179. package/dist/parser/agent-detection.js.map +1 -1
  180. package/dist/parser/config.d.ts +397 -2
  181. package/dist/parser/config.d.ts.map +1 -1
  182. package/dist/parser/config.js +125 -3
  183. package/dist/parser/config.js.map +1 -1
  184. package/dist/parser/dispatch-workspaces.d.ts +18 -0
  185. package/dist/parser/dispatch-workspaces.d.ts.map +1 -0
  186. package/dist/parser/dispatch-workspaces.js +209 -0
  187. package/dist/parser/dispatch-workspaces.js.map +1 -0
  188. package/dist/parser/doctor.d.ts.map +1 -1
  189. package/dist/parser/doctor.js +27 -8
  190. package/dist/parser/doctor.js.map +1 -1
  191. package/dist/parser/file-lock.d.ts.map +1 -1
  192. package/dist/parser/file-lock.js +9 -2
  193. package/dist/parser/file-lock.js.map +1 -1
  194. package/dist/parser/index.d.ts +6 -0
  195. package/dist/parser/index.d.ts.map +1 -1
  196. package/dist/parser/index.js +6 -0
  197. package/dist/parser/index.js.map +1 -1
  198. package/dist/parser/plans.d.ts.map +1 -1
  199. package/dist/parser/plans.js +1 -0
  200. package/dist/parser/plans.js.map +1 -1
  201. package/dist/parser/refs.d.ts +8 -1
  202. package/dist/parser/refs.d.ts.map +1 -1
  203. package/dist/parser/refs.js +27 -1
  204. package/dist/parser/refs.js.map +1 -1
  205. package/dist/parser/review-operations.d.ts +72 -0
  206. package/dist/parser/review-operations.d.ts.map +1 -0
  207. package/dist/parser/review-operations.js +185 -0
  208. package/dist/parser/review-operations.js.map +1 -0
  209. package/dist/parser/review-task-integration.d.ts +78 -0
  210. package/dist/parser/review-task-integration.d.ts.map +1 -0
  211. package/dist/parser/review-task-integration.js +173 -0
  212. package/dist/parser/review-task-integration.js.map +1 -0
  213. package/dist/parser/review-threads.d.ts +101 -0
  214. package/dist/parser/review-threads.d.ts.map +1 -0
  215. package/dist/parser/review-threads.js +222 -0
  216. package/dist/parser/review-threads.js.map +1 -0
  217. package/dist/parser/review-validation.d.ts +69 -0
  218. package/dist/parser/review-validation.d.ts.map +1 -0
  219. package/dist/parser/review-validation.js +207 -0
  220. package/dist/parser/review-validation.js.map +1 -0
  221. package/dist/parser/reviews.d.ts +58 -0
  222. package/dist/parser/reviews.d.ts.map +1 -0
  223. package/dist/parser/reviews.js +230 -0
  224. package/dist/parser/reviews.js.map +1 -0
  225. package/dist/parser/session-branch.d.ts +91 -0
  226. package/dist/parser/session-branch.d.ts.map +1 -0
  227. package/dist/parser/session-branch.js +565 -0
  228. package/dist/parser/session-branch.js.map +1 -0
  229. package/dist/parser/session-sync-scheduler.d.ts +53 -0
  230. package/dist/parser/session-sync-scheduler.d.ts.map +1 -0
  231. package/dist/parser/session-sync-scheduler.js +100 -0
  232. package/dist/parser/session-sync-scheduler.js.map +1 -0
  233. package/dist/parser/setup-status.d.ts +7 -1
  234. package/dist/parser/setup-status.d.ts.map +1 -1
  235. package/dist/parser/setup-status.js +104 -39
  236. package/dist/parser/setup-status.js.map +1 -1
  237. package/dist/parser/shadow-sync-scheduler.d.ts +71 -0
  238. package/dist/parser/shadow-sync-scheduler.d.ts.map +1 -0
  239. package/dist/parser/shadow-sync-scheduler.js +139 -0
  240. package/dist/parser/shadow-sync-scheduler.js.map +1 -0
  241. package/dist/parser/shadow.d.ts +121 -14
  242. package/dist/parser/shadow.d.ts.map +1 -1
  243. package/dist/parser/shadow.js +752 -27
  244. package/dist/parser/shadow.js.map +1 -1
  245. package/dist/parser/skill-render.d.ts +24 -0
  246. package/dist/parser/skill-render.d.ts.map +1 -1
  247. package/dist/parser/skill-render.js +98 -26
  248. package/dist/parser/skill-render.js.map +1 -1
  249. package/dist/parser/validate.d.ts +43 -3
  250. package/dist/parser/validate.d.ts.map +1 -1
  251. package/dist/parser/validate.js +204 -30
  252. package/dist/parser/validate.js.map +1 -1
  253. package/dist/parser/yaml.d.ts +47 -11
  254. package/dist/parser/yaml.d.ts.map +1 -1
  255. package/dist/parser/yaml.js +329 -149
  256. package/dist/parser/yaml.js.map +1 -1
  257. package/dist/review/checks.d.ts +97 -0
  258. package/dist/review/checks.d.ts.map +1 -0
  259. package/dist/review/checks.js +175 -0
  260. package/dist/review/checks.js.map +1 -0
  261. package/dist/review/index.d.ts +3 -0
  262. package/dist/review/index.d.ts.map +1 -0
  263. package/dist/review/index.js +3 -0
  264. package/dist/review/index.js.map +1 -0
  265. package/dist/review/subject-bindings.d.ts +83 -0
  266. package/dist/review/subject-bindings.d.ts.map +1 -0
  267. package/dist/review/subject-bindings.js +175 -0
  268. package/dist/review/subject-bindings.js.map +1 -0
  269. package/dist/schema/common.d.ts +26 -0
  270. package/dist/schema/common.d.ts.map +1 -1
  271. package/dist/schema/common.js +13 -0
  272. package/dist/schema/common.js.map +1 -1
  273. package/dist/schema/dispatch-workspace.d.ts +2643 -0
  274. package/dist/schema/dispatch-workspace.d.ts.map +1 -0
  275. package/dist/schema/dispatch-workspace.js +187 -0
  276. package/dist/schema/dispatch-workspace.js.map +1 -0
  277. package/dist/schema/inbox.d.ts +8 -8
  278. package/dist/schema/index.d.ts +2 -0
  279. package/dist/schema/index.d.ts.map +1 -1
  280. package/dist/schema/index.js +2 -0
  281. package/dist/schema/index.js.map +1 -1
  282. package/dist/schema/meta.d.ts +648 -116
  283. package/dist/schema/meta.d.ts.map +1 -1
  284. package/dist/schema/meta.js +27 -0
  285. package/dist/schema/meta.js.map +1 -1
  286. package/dist/schema/plan.d.ts +30 -19
  287. package/dist/schema/plan.d.ts.map +1 -1
  288. package/dist/schema/plan.js +3 -1
  289. package/dist/schema/plan.js.map +1 -1
  290. package/dist/schema/review-records.d.ts +2676 -0
  291. package/dist/schema/review-records.d.ts.map +1 -0
  292. package/dist/schema/review-records.js +232 -0
  293. package/dist/schema/review-records.js.map +1 -0
  294. package/dist/schema/spec.d.ts +32 -14
  295. package/dist/schema/spec.d.ts.map +1 -1
  296. package/dist/schema/spec.js +5 -0
  297. package/dist/schema/spec.js.map +1 -1
  298. package/dist/schema/task.d.ts +187 -29
  299. package/dist/schema/task.d.ts.map +1 -1
  300. package/dist/schema/task.js +12 -2
  301. package/dist/schema/task.js.map +1 -1
  302. package/dist/schema/triage.d.ts +22 -22
  303. package/dist/sessions/cache.d.ts +119 -0
  304. package/dist/sessions/cache.d.ts.map +1 -0
  305. package/dist/sessions/cache.js +284 -0
  306. package/dist/sessions/cache.js.map +1 -0
  307. package/dist/sessions/index.d.ts +1 -0
  308. package/dist/sessions/index.d.ts.map +1 -1
  309. package/dist/sessions/index.js +2 -0
  310. package/dist/sessions/index.js.map +1 -1
  311. package/dist/sessions/legacy.d.ts +77 -0
  312. package/dist/sessions/legacy.d.ts.map +1 -0
  313. package/dist/sessions/legacy.js +146 -0
  314. package/dist/sessions/legacy.js.map +1 -0
  315. package/dist/sessions/store.d.ts +103 -73
  316. package/dist/sessions/store.d.ts.map +1 -1
  317. package/dist/sessions/store.js +335 -186
  318. package/dist/sessions/store.js.map +1 -1
  319. package/dist/sessions/types.d.ts +44 -16
  320. package/dist/sessions/types.d.ts.map +1 -1
  321. package/dist/sessions/types.js +11 -2
  322. package/dist/sessions/types.js.map +1 -1
  323. package/dist/strings/errors.d.ts +32 -0
  324. package/dist/strings/errors.d.ts.map +1 -1
  325. package/dist/strings/errors.js +17 -0
  326. package/dist/strings/errors.js.map +1 -1
  327. package/dist/strings/labels.d.ts +1 -0
  328. package/dist/strings/labels.d.ts.map +1 -1
  329. package/dist/strings/labels.js +1 -0
  330. package/dist/strings/labels.js.map +1 -1
  331. package/dist/utils/activity.d.ts +101 -0
  332. package/dist/utils/activity.d.ts.map +1 -0
  333. package/dist/utils/activity.js +408 -0
  334. package/dist/utils/activity.js.map +1 -0
  335. package/dist/utils/git.d.ts +31 -0
  336. package/dist/utils/git.d.ts.map +1 -1
  337. package/dist/utils/git.js +87 -0
  338. package/dist/utils/git.js.map +1 -1
  339. package/dist/utils/index.d.ts +2 -0
  340. package/dist/utils/index.d.ts.map +1 -1
  341. package/dist/utils/index.js +1 -0
  342. package/dist/utils/index.js.map +1 -1
  343. package/dist/web-ui/_app/immutable/assets/0.tmlwn-Ih.css +1 -0
  344. package/dist/web-ui/_app/immutable/assets/9.BwwJybWx.css +1 -0
  345. package/dist/web-ui/_app/immutable/chunks/2KqE8gtn.js +1 -0
  346. package/dist/web-ui/_app/immutable/chunks/70-t_QvE.js +1 -0
  347. package/dist/web-ui/_app/immutable/chunks/AiWQj974.js +1 -0
  348. package/dist/web-ui/_app/immutable/chunks/{CPPfDSei.js → B25nWFyA.js} +4 -4
  349. package/dist/web-ui/_app/immutable/chunks/{DBYE9jOd.js → B2bcA_Q_.js} +1 -1
  350. package/dist/web-ui/_app/immutable/chunks/B5e5HYyB.js +1 -0
  351. package/dist/web-ui/_app/immutable/chunks/B7-5z6eA.js +1 -0
  352. package/dist/web-ui/_app/immutable/chunks/B7bGmhK0.js +1 -0
  353. package/dist/web-ui/_app/immutable/chunks/{DzO4hlg9.js → B8tYZKAE.js} +1 -1
  354. package/dist/web-ui/_app/immutable/chunks/{B5LJFxqa.js → BFGAyJjD.js} +1 -1
  355. package/dist/web-ui/_app/immutable/chunks/BG0850zf.js +1 -0
  356. package/dist/web-ui/_app/immutable/chunks/{DAMmvwn4.js → BG8eSzAd.js} +1 -1
  357. package/dist/web-ui/_app/immutable/chunks/BIMxXS8I.js +1 -0
  358. package/dist/web-ui/_app/immutable/chunks/BSzL1fpU.js +1 -0
  359. package/dist/web-ui/_app/immutable/chunks/BYtjHfeq.js +1 -0
  360. package/dist/web-ui/_app/immutable/chunks/{DxCk-KHc.js → Bp5pFYXL.js} +1 -1
  361. package/dist/web-ui/_app/immutable/chunks/{B8a0xDxR.js → BsJFsuAT.js} +1 -1
  362. package/dist/web-ui/_app/immutable/chunks/BvpNHcD6.js +1 -0
  363. package/dist/web-ui/_app/immutable/chunks/BypqA25-.js +1 -0
  364. package/dist/web-ui/_app/immutable/chunks/{BVA9Exy-.js → C0w6WDm5.js} +1 -1
  365. package/dist/web-ui/_app/immutable/chunks/C5_PAZ0y.js +1 -0
  366. package/dist/web-ui/_app/immutable/chunks/CDRO15Iv.js +1 -0
  367. package/dist/web-ui/_app/immutable/chunks/CF1CoqD5.js +1 -0
  368. package/dist/web-ui/_app/immutable/chunks/CS2sa4_m.js +1 -0
  369. package/dist/web-ui/_app/immutable/chunks/{BJ0JX3ea.js → CWUQwB9H.js} +1 -1
  370. package/dist/web-ui/_app/immutable/chunks/CY5FDdSU.js +1 -0
  371. package/dist/web-ui/_app/immutable/chunks/C_7MTDoj.js +1 -0
  372. package/dist/web-ui/_app/immutable/chunks/{D3vxvonu.js → CaAJD3dl.js} +1 -1
  373. package/dist/web-ui/_app/immutable/chunks/{BP352uRn.js → ChB5iyEL.js} +1 -1
  374. package/dist/web-ui/_app/immutable/chunks/{pE6cYWlS.js → ChQD-6N8.js} +1 -1
  375. package/dist/web-ui/_app/immutable/chunks/{Eo4gF7ih.js → CqbsoCwA.js} +1 -1
  376. package/dist/web-ui/_app/immutable/chunks/DCeJW50p.js +1 -0
  377. package/dist/web-ui/_app/immutable/chunks/{Cncwi6fQ.js → DJtZNgcs.js} +1 -1
  378. package/dist/web-ui/_app/immutable/chunks/DKIeaprD.js +1 -0
  379. package/dist/web-ui/_app/immutable/chunks/DLd2uVIA.js +1 -0
  380. package/dist/web-ui/_app/immutable/chunks/{DjcCz-PU.js → DW_subyT.js} +2 -2
  381. package/dist/web-ui/_app/immutable/chunks/DbU6lVn0.js +1 -0
  382. package/dist/web-ui/_app/immutable/chunks/Dc7ZCC5m.js +1 -0
  383. package/dist/web-ui/_app/immutable/chunks/Dd5umPsk.js +2 -0
  384. package/dist/web-ui/_app/immutable/chunks/{BysXJlZb.js → Dg_zDpDS.js} +1 -1
  385. package/dist/web-ui/_app/immutable/chunks/Dgqu8Yuc.js +1 -0
  386. package/dist/web-ui/_app/immutable/chunks/DmxsPZTB.js +1 -0
  387. package/dist/web-ui/_app/immutable/chunks/DphTaFUB.js +1 -0
  388. package/dist/web-ui/_app/immutable/chunks/DqK4iHp0.js +1 -0
  389. package/dist/web-ui/_app/immutable/chunks/{D9QNBZM2.js → DqT6OH_u.js} +2 -2
  390. package/dist/web-ui/_app/immutable/chunks/Ds9I9wQb.js +1 -0
  391. package/dist/web-ui/_app/immutable/chunks/Du5ng3u4.js +1 -0
  392. package/dist/web-ui/_app/immutable/chunks/DxJw79Wi.js +1 -0
  393. package/dist/web-ui/_app/immutable/chunks/GFTX8GgV.js +1 -0
  394. package/dist/web-ui/_app/immutable/chunks/{C076q4JN.js → HNjs76Zz.js} +1 -1
  395. package/dist/web-ui/_app/immutable/chunks/HVMjDi4_.js +1 -0
  396. package/dist/web-ui/_app/immutable/chunks/{BkOJ8DkV.js → P0A_fJvS.js} +1 -1
  397. package/dist/web-ui/_app/immutable/chunks/T3vGWjIL.js +1 -0
  398. package/dist/web-ui/_app/immutable/chunks/VTmrX9Qu.js +1 -0
  399. package/dist/web-ui/_app/immutable/chunks/{k_Qegko0.js → Xvwhx_F1.js} +1 -1
  400. package/dist/web-ui/_app/immutable/chunks/Yyz1XMQA.js +1 -0
  401. package/dist/web-ui/_app/immutable/chunks/{62JVKtnb.js → dh5HeqUr.js} +1 -1
  402. package/dist/web-ui/_app/immutable/chunks/fZMteyca.js +62 -0
  403. package/dist/web-ui/_app/immutable/chunks/{D82RulSH.js → gPrj-hqC.js} +1 -1
  404. package/dist/web-ui/_app/immutable/chunks/htcWMiYN.js +1 -0
  405. package/dist/web-ui/_app/immutable/chunks/{CwELQvbx.js → oTsvd9y4.js} +1 -1
  406. package/dist/web-ui/_app/immutable/chunks/qJfLUwU4.js +1 -0
  407. package/dist/web-ui/_app/immutable/chunks/xCtiO_JE.js +1 -0
  408. package/dist/web-ui/_app/immutable/chunks/{DvA-KON-.js → y4GeEH6k.js} +1 -1
  409. package/dist/web-ui/_app/immutable/entry/app.C4h_eOn6.js +2 -0
  410. package/dist/web-ui/_app/immutable/entry/start.CQFTf9ep.js +1 -0
  411. package/dist/web-ui/_app/immutable/nodes/0.Dh1xO970.js +1 -0
  412. package/dist/web-ui/_app/immutable/nodes/1.l75D3Opx.js +1 -0
  413. package/dist/web-ui/_app/immutable/nodes/10.DBidBPc-.js +1 -0
  414. package/dist/web-ui/_app/immutable/nodes/11.Ab0gUKWe.js +1 -0
  415. package/dist/web-ui/_app/immutable/nodes/12.CMsnoxfs.js +1 -0
  416. package/dist/web-ui/_app/immutable/nodes/13.D8YKuknB.js +1 -0
  417. package/dist/web-ui/_app/immutable/nodes/14.DZ0aan7y.js +1 -0
  418. package/dist/web-ui/_app/immutable/nodes/15.CUIKreDL.js +2 -0
  419. package/dist/web-ui/_app/immutable/nodes/16.BWc8--BO.js +1 -0
  420. package/dist/web-ui/_app/immutable/nodes/2.CDUonbuh.js +1 -0
  421. package/dist/web-ui/_app/immutable/nodes/3.Ctg3M00i.js +1 -0
  422. package/dist/web-ui/_app/immutable/nodes/4.Ci-JDwbA.js +2 -0
  423. package/dist/web-ui/_app/immutable/nodes/5.CTyEDAq0.js +1 -0
  424. package/dist/web-ui/_app/immutable/nodes/6.BTZZqsAb.js +1 -0
  425. package/dist/web-ui/_app/immutable/nodes/7.BI52g_Jo.js +137 -0
  426. package/dist/web-ui/_app/immutable/nodes/8.3hZPaB9x.js +1 -0
  427. package/dist/web-ui/_app/immutable/nodes/9.DS49kvwl.js +29 -0
  428. package/dist/web-ui/_app/version.json +1 -1
  429. package/dist/web-ui/favicon-192.png +0 -0
  430. package/dist/web-ui/favicon-32.png +0 -0
  431. package/dist/web-ui/favicon.ico +0 -0
  432. package/dist/web-ui/index.html +14 -14
  433. package/package.json +12 -6
  434. package/plugin/.claude-plugin/marketplace.json +1 -1
  435. package/plugin/.claude-plugin/plugin.json +1 -1
  436. package/plugin/plugins/kspec/skills/merge/SKILL.md +127 -0
  437. package/plugin/plugins/kspec/skills/plan/SKILL.md +55 -26
  438. package/plugin/plugins/kspec/skills/review/SKILL.md +350 -133
  439. package/plugin/plugins/kspec/skills/task-work/SKILL.md +96 -106
  440. package/templates/agents-sections/04-pr-workflow.md +15 -12
  441. package/templates/agents-sections/06-ralph-loop.md +15 -10
  442. package/templates/skills/manifest.yaml +25 -7
  443. package/templates/skills/merge/SKILL.md +120 -0
  444. package/templates/skills/plan/SKILL.md +55 -26
  445. package/templates/skills/review/SKILL.md +346 -130
  446. package/templates/skills/task-work/SKILL.md +93 -103
  447. package/dist/web-ui/_app/immutable/assets/0.BJaYkGW2.css +0 -1
  448. package/dist/web-ui/_app/immutable/assets/9.SzGLxi4x.css +0 -1
  449. package/dist/web-ui/_app/immutable/chunks/-lc0BifF.js +0 -1
  450. package/dist/web-ui/_app/immutable/chunks/8RBjHMN1.js +0 -1
  451. package/dist/web-ui/_app/immutable/chunks/B5wTVqxm.js +0 -1
  452. package/dist/web-ui/_app/immutable/chunks/B6VSmczZ.js +0 -1
  453. package/dist/web-ui/_app/immutable/chunks/BEOQc37C.js +0 -1
  454. package/dist/web-ui/_app/immutable/chunks/BHtYorjv.js +0 -1
  455. package/dist/web-ui/_app/immutable/chunks/BMuCqDX8.js +0 -1
  456. package/dist/web-ui/_app/immutable/chunks/BUZujXJ2.js +0 -1
  457. package/dist/web-ui/_app/immutable/chunks/BWET-efb.js +0 -1
  458. package/dist/web-ui/_app/immutable/chunks/BXkNecpt.js +0 -1
  459. package/dist/web-ui/_app/immutable/chunks/BYzrIfX8.js +0 -1
  460. package/dist/web-ui/_app/immutable/chunks/BpuwufMc.js +0 -1
  461. package/dist/web-ui/_app/immutable/chunks/BwMO4RrG.js +0 -1
  462. package/dist/web-ui/_app/immutable/chunks/C33JaVbg.js +0 -1
  463. package/dist/web-ui/_app/immutable/chunks/CGtqifKp.js +0 -1
  464. package/dist/web-ui/_app/immutable/chunks/CHDZZ7OG.js +0 -1
  465. package/dist/web-ui/_app/immutable/chunks/CUir3f4J.js +0 -60
  466. package/dist/web-ui/_app/immutable/chunks/CrCIbn0C.js +0 -1
  467. package/dist/web-ui/_app/immutable/chunks/D6TVmR9T.js +0 -1
  468. package/dist/web-ui/_app/immutable/chunks/D7LTux4W.js +0 -1
  469. package/dist/web-ui/_app/immutable/chunks/DAh4Wfku.js +0 -1
  470. package/dist/web-ui/_app/immutable/chunks/DAx07bEQ.js +0 -1
  471. package/dist/web-ui/_app/immutable/chunks/DOno4cA2.js +0 -1
  472. package/dist/web-ui/_app/immutable/chunks/DQA8NZIH.js +0 -2
  473. package/dist/web-ui/_app/immutable/chunks/DRfPm2bo.js +0 -1
  474. package/dist/web-ui/_app/immutable/chunks/DhQhksaB.js +0 -1
  475. package/dist/web-ui/_app/immutable/chunks/DjG7s6hm.js +0 -1
  476. package/dist/web-ui/_app/immutable/chunks/DkltRNvh.js +0 -1
  477. package/dist/web-ui/_app/immutable/chunks/DlaTnPKL.js +0 -1
  478. package/dist/web-ui/_app/immutable/chunks/ExCq5swK.js +0 -1
  479. package/dist/web-ui/_app/immutable/chunks/T3zZGv51.js +0 -1
  480. package/dist/web-ui/_app/immutable/chunks/XZumBYeP.js +0 -1
  481. package/dist/web-ui/_app/immutable/chunks/_ySfNjkF.js +0 -1
  482. package/dist/web-ui/_app/immutable/chunks/iEtR5cV6.js +0 -1
  483. package/dist/web-ui/_app/immutable/entry/app.Cgu6uKeS.js +0 -2
  484. package/dist/web-ui/_app/immutable/entry/start.9XifnLoB.js +0 -1
  485. package/dist/web-ui/_app/immutable/nodes/0.DISwcKSK.js +0 -1
  486. package/dist/web-ui/_app/immutable/nodes/1.Cx2Ufqp1.js +0 -1
  487. package/dist/web-ui/_app/immutable/nodes/10.C3z8ijXL.js +0 -1
  488. package/dist/web-ui/_app/immutable/nodes/11.DZdIjZmM.js +0 -1
  489. package/dist/web-ui/_app/immutable/nodes/12.FsIGfAOa.js +0 -1
  490. package/dist/web-ui/_app/immutable/nodes/13.DZoFwagf.js +0 -1
  491. package/dist/web-ui/_app/immutable/nodes/14.DaIzDKbQ.js +0 -1
  492. package/dist/web-ui/_app/immutable/nodes/15.BYyt4XWF.js +0 -2
  493. package/dist/web-ui/_app/immutable/nodes/16.CQkSqpOe.js +0 -1
  494. package/dist/web-ui/_app/immutable/nodes/2.Bkf_j2UJ.js +0 -1
  495. package/dist/web-ui/_app/immutable/nodes/3.kaMCurJG.js +0 -1
  496. package/dist/web-ui/_app/immutable/nodes/4.BSsFPTHG.js +0 -2
  497. package/dist/web-ui/_app/immutable/nodes/5.CpPlcCEZ.js +0 -1
  498. package/dist/web-ui/_app/immutable/nodes/6.BN4FqQmY.js +0 -1
  499. package/dist/web-ui/_app/immutable/nodes/7.9kBYIZik.js +0 -1
  500. package/dist/web-ui/_app/immutable/nodes/8.BuijtZ6B.js +0 -1
  501. package/dist/web-ui/_app/immutable/nodes/9.C-Weba8R.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,
@@ -191,14 +231,14 @@ export async function listSessions(specDir) {
191
231
  * Reads only session metadata (not events.jsonl) for performance.
192
232
  * A session counts as "completed" if its status is "completed".
193
233
  *
194
- * @param specDir - The .kspec directory path
234
+ * @param sessionsDir - The .kspec directory path
195
235
  * @returns Map of agent_id to completed session count
196
236
  */
197
- export async function getCompletedSessionCountsByAgent(specDir) {
198
- const sessionIds = await listSessions(specDir);
237
+ export async function getCompletedSessionCountsByAgent(sessionsDir) {
238
+ const sessionIds = await listSessions(sessionsDir);
199
239
  const counts = {};
200
240
  // Read metadata in parallel for performance
201
- const metadataResults = await Promise.all(sessionIds.map((id) => getSession(specDir, id)));
241
+ const metadataResults = await Promise.all(sessionIds.map((id) => getSession(sessionsDir, id)));
202
242
  for (const metadata of metadataResults) {
203
243
  if (!metadata)
204
244
  continue;
@@ -212,8 +252,8 @@ export async function getCompletedSessionCountsByAgent(specDir) {
212
252
  /**
213
253
  * Check if a session exists.
214
254
  */
215
- export async function sessionExists(specDir, sessionId) {
216
- const metadataPath = getSessionMetadataPath(specDir, sessionId);
255
+ export async function sessionExists(sessionsDir, sessionId) {
256
+ const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
217
257
  try {
218
258
  await fsPromises.access(metadataPath);
219
259
  return true;
@@ -231,13 +271,13 @@ export async function sessionExists(specDir, sessionId) {
231
271
  *
232
272
  * AC: @session-end-loop-signal ac-signal
233
273
  *
234
- * @param specDir - The .kspec directory path
274
+ * @param sessionsDir - The .kspec directory path
235
275
  * @param sessionId - Session ID
236
276
  * @param reason - Optional reason for ending the loop
237
277
  * @returns Updated metadata or null if session not found
238
278
  */
239
- export async function requestEndLoop(specDir, sessionId, reason) {
240
- const metadata = await getSession(specDir, sessionId);
279
+ export async function requestEndLoop(sessionsDir, sessionId, reason) {
280
+ const metadata = await getSession(sessionsDir, sessionId);
241
281
  if (!metadata) {
242
282
  return null;
243
283
  }
@@ -246,7 +286,7 @@ export async function requestEndLoop(specDir, sessionId, reason) {
246
286
  end_requested: true,
247
287
  end_reason: reason,
248
288
  };
249
- const metadataPath = getSessionMetadataPath(specDir, sessionId);
289
+ const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
250
290
  const content = YAML.stringify(updated, {
251
291
  indent: 2,
252
292
  lineWidth: 100,
@@ -264,12 +304,12 @@ export async function requestEndLoop(specDir, sessionId, reason) {
264
304
  *
265
305
  * AC: @session-end-loop-signal ac-detect
266
306
  *
267
- * @param specDir - The .kspec directory path
307
+ * @param sessionsDir - The .kspec directory path
268
308
  * @param sessionId - Session ID
269
309
  * @returns Object with requested flag and optional reason, or null if session not found
270
310
  */
271
- export async function isEndLoopRequested(specDir, sessionId) {
272
- const metadata = await getSession(specDir, sessionId);
311
+ export async function isEndLoopRequested(sessionsDir, sessionId) {
312
+ const metadata = await getSession(sessionsDir, sessionId);
273
313
  if (!metadata) {
274
314
  return null;
275
315
  }
@@ -287,30 +327,42 @@ export async function isEndLoopRequested(specDir, sessionId) {
287
327
  * AC: @session-end-loop-signal ac-session-close-signal
288
328
  * AC: @session-end-loop-signal ac-session-close-error
289
329
  *
290
- * @param specDir - The .kspec directory path
330
+ * @param sessionsDir - The .kspec directory path
291
331
  * @param sessionId - Session ID
292
332
  * @param status - New status (completed or abandoned)
293
333
  * @param reason - Reason for closing
294
334
  * @returns Updated metadata or null if session not found
295
335
  */
296
- export async function closeSession(specDir, sessionId, status, reason) {
297
- const metadata = await getSession(specDir, sessionId);
336
+ export async function closeSession(sessionsDir, sessionId, status, reason) {
337
+ const metadata = await getSession(sessionsDir, sessionId);
298
338
  if (!metadata) {
299
339
  return null;
300
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
+ ]);
301
348
  const updated = {
302
349
  ...metadata,
303
350
  status,
304
351
  ended_at: new Date().toISOString(),
305
352
  close_reason: reason,
353
+ event_count: eventCount,
354
+ iteration_count: iterationCount,
355
+ tasks_completed: tasksCompleted,
306
356
  };
307
- const metadataPath = getSessionMetadataPath(specDir, sessionId);
357
+ const metadataPath = getSessionMetadataPath(sessionsDir, sessionId);
308
358
  const content = YAML.stringify(updated, {
309
359
  indent: 2,
310
360
  lineWidth: 100,
311
361
  sortMapEntries: false,
312
362
  });
313
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})`);
314
366
  return updated;
315
367
  }
316
368
  function isRecord(value) {
@@ -384,7 +436,7 @@ function shouldExternalizeField(pathSegments, value) {
384
436
  }
385
437
  return false;
386
438
  }
387
- async function createBlobPointer(specDir, sessionId, seq, pathSegments, value, context) {
439
+ async function createBlobPointer(sessionsDir, sessionId, seq, pathSegments, value, context) {
388
440
  const content = stringifyPayload(value);
389
441
  const bytes = Buffer.byteLength(content, "utf-8");
390
442
  const sha256 = createHash("sha256").update(content).digest("hex");
@@ -399,7 +451,7 @@ async function createBlobPointer(specDir, sessionId, seq, pathSegments, value, c
399
451
  await fsPromises.mkdir(context.blobDir, { recursive: true });
400
452
  context.ensuredDir = true;
401
453
  }
402
- const absolutePath = path.join(getSessionDir(specDir, sessionId), relativePath);
454
+ const absolutePath = path.join(getSessionDir(sessionsDir, sessionId), relativePath);
403
455
  await fsPromises.writeFile(absolutePath, content, "utf-8");
404
456
  }
405
457
  context.createdBlobs = (context.createdBlobs ?? 0) + 1;
@@ -412,15 +464,15 @@ async function createBlobPointer(specDir, sessionId, seq, pathSegments, value, c
412
464
  preview: toPreview(content),
413
465
  };
414
466
  }
415
- async function externalizeOversizedPayloads(specDir, sessionId, seq, value, pathSegments, context) {
467
+ async function externalizeOversizedPayloads(sessionsDir, sessionId, seq, value, pathSegments, context) {
416
468
  if (isSessionBlobPointer(value)) {
417
469
  return value;
418
470
  }
419
471
  if (shouldExternalizeField(pathSegments, value)) {
420
- return createBlobPointer(specDir, sessionId, seq, pathSegments, value, context);
472
+ return createBlobPointer(sessionsDir, sessionId, seq, pathSegments, value, context);
421
473
  }
422
474
  if (Array.isArray(value)) {
423
- 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, [
424
476
  ...pathSegments,
425
477
  String(idx),
426
478
  ], context)));
@@ -428,14 +480,14 @@ async function externalizeOversizedPayloads(specDir, sessionId, seq, value, path
428
480
  if (isRecord(value)) {
429
481
  const next = {};
430
482
  for (const [key, child] of Object.entries(value)) {
431
- next[key] = await externalizeOversizedPayloads(specDir, sessionId, seq, child, [...pathSegments, key], context);
483
+ next[key] = await externalizeOversizedPayloads(sessionsDir, sessionId, seq, child, [...pathSegments, key], context);
432
484
  }
433
485
  return next;
434
486
  }
435
487
  return value;
436
488
  }
437
- function resolveBlobAbsolutePath(specDir, sessionId, relativePath) {
438
- const sessionDir = path.resolve(getSessionDir(specDir, sessionId));
489
+ function resolveBlobAbsolutePath(sessionsDir, sessionId, relativePath) {
490
+ const sessionDir = path.resolve(getSessionDir(sessionsDir, sessionId));
439
491
  const absolutePath = path.resolve(sessionDir, relativePath);
440
492
  if (absolutePath === sessionDir ||
441
493
  absolutePath.startsWith(`${sessionDir}${path.sep}`)) {
@@ -443,8 +495,8 @@ function resolveBlobAbsolutePath(specDir, sessionId, relativePath) {
443
495
  }
444
496
  return null;
445
497
  }
446
- async function resolveBlobPointer(specDir, sessionId, pointer) {
447
- const absolutePath = resolveBlobAbsolutePath(specDir, sessionId, pointer.path);
498
+ async function resolveBlobPointer(sessionsDir, sessionId, pointer) {
499
+ const absolutePath = resolveBlobAbsolutePath(sessionsDir, sessionId, pointer.path);
448
500
  if (!absolutePath) {
449
501
  return { ...pointer, content: pointer.preview };
450
502
  }
@@ -462,17 +514,17 @@ async function resolveBlobPointer(specDir, sessionId, pointer) {
462
514
  * Default flows keep compact pointer objects (preview-only). This helper powers
463
515
  * explicit on-demand blob resolution in session log commands.
464
516
  */
465
- export async function resolveSessionBlobPointers(specDir, sessionId, value) {
517
+ export async function resolveSessionBlobPointers(sessionsDir, sessionId, value) {
466
518
  if (isSessionBlobPointer(value)) {
467
- return resolveBlobPointer(specDir, sessionId, value);
519
+ return resolveBlobPointer(sessionsDir, sessionId, value);
468
520
  }
469
521
  if (Array.isArray(value)) {
470
- return Promise.all(value.map((entry) => resolveSessionBlobPointers(specDir, sessionId, entry)));
522
+ return Promise.all(value.map((entry) => resolveSessionBlobPointers(sessionsDir, sessionId, entry)));
471
523
  }
472
524
  if (isRecord(value)) {
473
525
  const next = {};
474
526
  for (const [key, child] of Object.entries(value)) {
475
- next[key] = await resolveSessionBlobPointers(specDir, sessionId, child);
527
+ next[key] = await resolveSessionBlobPointers(sessionsDir, sessionId, child);
476
528
  }
477
529
  return next;
478
530
  }
@@ -506,8 +558,8 @@ function extractLastEventSeq(content) {
506
558
  * Reads a bounded tail slice for O(1) seq lookup; falls back to full scan only
507
559
  * if the tail slice cannot be parsed (for example, partial line boundary).
508
560
  */
509
- async function getNextEventSeq(specDir, sessionId) {
510
- const eventsPath = getSessionEventsPath(specDir, sessionId);
561
+ async function getNextEventSeq(sessionsDir, sessionId) {
562
+ const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
511
563
  let fileHandle = null;
512
564
  try {
513
565
  fileHandle = await fsPromises.open(eventsPath, "r");
@@ -561,17 +613,17 @@ async function getNextEventSeq(specDir, sessionId) {
561
613
  * (single-process, sequential event logging). If concurrent access is needed
562
614
  * in the future, consider file locking or an in-memory counter per session.
563
615
  *
564
- * @param specDir - The .kspec directory path
616
+ * @param sessionsDir - The .kspec directory path
565
617
  * @param input - Event input (ts and seq are auto-assigned if not provided)
566
618
  * @returns The appended event with auto-assigned fields
567
619
  */
568
- export async function appendEvent(specDir, input) {
569
- const sessionDir = getSessionDir(specDir, input.session_id);
570
- 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);
571
623
  // Ensure session directory exists (lazy creation)
572
624
  await fsPromises.mkdir(sessionDir, { recursive: true });
573
625
  // Derive next sequence number from the last stored event.
574
- const seq = input.seq ?? (await getNextEventSeq(specDir, input.session_id));
626
+ const seq = input.seq ?? (await getNextEventSeq(sessionsDir, input.session_id));
575
627
  // Build full event
576
628
  const event = {
577
629
  ts: input.ts ?? Date.now(),
@@ -583,8 +635,8 @@ export async function appendEvent(specDir, input) {
583
635
  };
584
636
  // AC: @session-events ac-8, ac-9 - Externalize oversized payload fields
585
637
  // before writing to events.jsonl.
586
- const externalizedData = await externalizeOversizedPayloads(specDir, input.session_id, seq, event.data, [], {
587
- 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),
588
640
  ensuredDir: false,
589
641
  });
590
642
  const eventWithGuardrails = {
@@ -598,10 +650,10 @@ export async function appendEvent(specDir, input) {
598
650
  let line = JSON.stringify(validated);
599
651
  if (Buffer.byteLength(line, "utf-8") > EVENT_LINE_MAX_BYTES) {
600
652
  const blobContext = {
601
- blobDir: getSessionBlobDir(specDir, input.session_id),
653
+ blobDir: getSessionBlobDir(sessionsDir, input.session_id),
602
654
  ensuredDir: false,
603
655
  };
604
- 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);
605
657
  validated = SessionEventSchema.parse({
606
658
  ...validated,
607
659
  data: fullDataPointer,
@@ -623,10 +675,10 @@ export async function appendEvent(specDir, input) {
623
675
  * Writes are atomic (temp-file then rename). When dryRun is enabled, no files
624
676
  * are modified and no blob files are written.
625
677
  */
626
- export async function compactSessionEvents(specDir, sessionId, options = {}) {
678
+ export async function compactSessionEvents(sessionsDir, sessionId, options = {}) {
627
679
  const dryRun = options.dryRun === true;
628
680
  const renameFn = options.renameFn ?? fsPromises.rename;
629
- const eventsPath = getSessionEventsPath(specDir, sessionId);
681
+ const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
630
682
  let content;
631
683
  try {
632
684
  content = await fsPromises.readFile(eventsPath, "utf-8");
@@ -663,7 +715,7 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
663
715
  };
664
716
  }
665
717
  const blobContext = {
666
- blobDir: getSessionBlobDir(specDir, sessionId),
718
+ blobDir: getSessionBlobDir(sessionsDir, sessionId),
667
719
  ensuredDir: false,
668
720
  dryRun,
669
721
  createdBlobs: 0,
@@ -680,14 +732,14 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
680
732
  throw new Error(`Invalid JSON in events log at line ${i + 1}: ${err instanceof Error ? err.message : String(err)}`);
681
733
  }
682
734
  const event = SessionEventSchema.parse(parsed);
683
- const externalizedData = await externalizeOversizedPayloads(specDir, sessionId, event.seq, event.data, [], blobContext);
735
+ const externalizedData = await externalizeOversizedPayloads(sessionsDir, sessionId, event.seq, event.data, [], blobContext);
684
736
  let validated = SessionEventSchema.parse({
685
737
  ...event,
686
738
  data: externalizedData,
687
739
  });
688
740
  let compactedLine = JSON.stringify(validated);
689
741
  if (Buffer.byteLength(compactedLine, "utf-8") > EVENT_LINE_MAX_BYTES) {
690
- const fullDataPointer = await createBlobPointer(specDir, sessionId, event.seq, [], validated.data, blobContext);
742
+ const fullDataPointer = await createBlobPointer(sessionsDir, sessionId, event.seq, [], validated.data, blobContext);
691
743
  validated = SessionEventSchema.parse({
692
744
  ...validated,
693
745
  data: fullDataPointer,
@@ -712,7 +764,7 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
712
764
  };
713
765
  }
714
766
  if (!dryRun) {
715
- const sessionDir = getSessionDir(specDir, sessionId);
767
+ const sessionDir = getSessionDir(sessionsDir, sessionId);
716
768
  const tmpPath = path.join(sessionDir, `.${EVENTS_FILE}.${process.pid}.${Date.now()}.tmp`);
717
769
  try {
718
770
  await fsPromises.writeFile(tmpPath, compactedContent, "utf-8");
@@ -723,6 +775,10 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
723
775
  throw err;
724
776
  }
725
777
  }
778
+ // AC: @session-branch-worktree ac-commit-boundaries — commit on compact
779
+ if (!dryRun) {
780
+ await commitAtLifecycleBoundary(sessionsDir, `session: compact (${sessionId})`);
781
+ }
726
782
  return {
727
783
  events_processed: sourceLines.length,
728
784
  blobs_created: blobContext.createdBlobs ?? 0,
@@ -739,12 +795,12 @@ export async function compactSessionEvents(specDir, sessionId, options = {}) {
739
795
  *
740
796
  * AC-4: Returns all events in sequence order.
741
797
  *
742
- * @param specDir - The .kspec directory path
798
+ * @param sessionsDir - The .kspec directory path
743
799
  * @param sessionId - Session ID
744
800
  * @returns Array of events sorted by sequence number
745
801
  */
746
- export async function readEvents(specDir, sessionId) {
747
- const eventsPath = getSessionEventsPath(specDir, sessionId);
802
+ export async function readEvents(sessionsDir, sessionId) {
803
+ const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
748
804
  try {
749
805
  const content = await fsPromises.readFile(eventsPath, "utf-8");
750
806
  const lines = content
@@ -769,6 +825,45 @@ export async function readEvents(specDir, sessionId) {
769
825
  return [];
770
826
  }
771
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
+ }
772
867
  /**
773
868
  * Deduplicate phased tool_call events.
774
869
  *
@@ -818,14 +913,14 @@ export function deduplicatePhasedToolCalls(events) {
818
913
  /**
819
914
  * Read events within a time range.
820
915
  *
821
- * @param specDir - The .kspec directory path
916
+ * @param sessionsDir - The .kspec directory path
822
917
  * @param sessionId - Session ID
823
918
  * @param since - Start timestamp (inclusive)
824
919
  * @param until - End timestamp (inclusive)
825
920
  * @returns Array of events within the time range
826
921
  */
827
- export async function readEventsSince(specDir, sessionId, since, until) {
828
- const events = await readEvents(specDir, sessionId);
922
+ export async function readEventsSince(sessionsDir, sessionId, since, until) {
923
+ const events = await readEvents(sessionsDir, sessionId);
829
924
  return events.filter((e) => {
830
925
  if (e.ts < since)
831
926
  return false;
@@ -837,12 +932,12 @@ export async function readEventsSince(specDir, sessionId, since, until) {
837
932
  /**
838
933
  * Get the last event in a session.
839
934
  *
840
- * @param specDir - The .kspec directory path
935
+ * @param sessionsDir - The .kspec directory path
841
936
  * @param sessionId - Session ID
842
937
  * @returns The last event or null if no events
843
938
  */
844
- export async function getLastEvent(specDir, sessionId) {
845
- const events = await readEvents(specDir, sessionId);
939
+ export async function getLastEvent(sessionsDir, sessionId) {
940
+ const events = await readEvents(sessionsDir, sessionId);
846
941
  if (events.length === 0) {
847
942
  return null;
848
943
  }
@@ -930,8 +1025,8 @@ export function resolveStaleSessionCriteria(input) {
930
1025
  * Unlike readEvents(), this is strict: corrupt events are surfaced as failures
931
1026
  * so stale auto-close can skip unsafe sessions.
932
1027
  */
933
- export async function getSessionActivityForStaleCheck(specDir, sessionId) {
934
- const metadata = await getSession(specDir, sessionId);
1028
+ export async function getSessionActivityForStaleCheck(sessionsDir, sessionId) {
1029
+ const metadata = await getSession(sessionsDir, sessionId);
935
1030
  if (!metadata) {
936
1031
  return {
937
1032
  ok: false,
@@ -947,7 +1042,7 @@ export async function getSessionActivityForStaleCheck(specDir, sessionId) {
947
1042
  detail: `Session ${sessionId} has invalid started_at timestamp`,
948
1043
  };
949
1044
  }
950
- const eventsPath = getSessionEventsPath(specDir, sessionId);
1045
+ const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
951
1046
  let content;
952
1047
  try {
953
1048
  content = await fsPromises.readFile(eventsPath, "utf-8");
@@ -1043,21 +1138,21 @@ export function evaluateStaleSession(startedAt, activity, criteria, nowMs = Date
1043
1138
  eligible,
1044
1139
  };
1045
1140
  }
1046
- export async function selectStaleActiveSessions(specDir, criteriaInput = {}, nowMs = Date.now()) {
1141
+ export async function selectStaleActiveSessions(sessionsDir, criteriaInput = {}, nowMs = Date.now()) {
1047
1142
  const criteriaResolved = resolveStaleSessionCriteria(criteriaInput);
1048
1143
  if (!criteriaResolved.ok) {
1049
1144
  throw new Error(`${criteriaResolved.message}. ${criteriaResolved.guidance}`);
1050
1145
  }
1051
1146
  const criteria = criteriaResolved.criteria;
1052
- const sessionIds = await listSessions(specDir);
1147
+ const sessionIds = await listSessions(sessionsDir);
1053
1148
  const evaluations = [];
1054
1149
  const candidates = [];
1055
1150
  const skipped = [];
1056
1151
  for (const sessionId of sessionIds) {
1057
- const metadata = await getSession(specDir, sessionId);
1152
+ const metadata = await getSession(sessionsDir, sessionId);
1058
1153
  if (!metadata || metadata.status !== "active")
1059
1154
  continue;
1060
- const activityResult = await getSessionActivityForStaleCheck(specDir, sessionId);
1155
+ const activityResult = await getSessionActivityForStaleCheck(sessionsDir, sessionId);
1061
1156
  if (!activityResult.ok) {
1062
1157
  skipped.push({
1063
1158
  sessionId,
@@ -1105,9 +1200,9 @@ export function buildAutoAbandonedCloseReason(criteria, evaluation) {
1105
1200
  * Apply abandoned metadata to stale session candidates.
1106
1201
  *
1107
1202
  * All updates in a single invocation share one ended_at timestamp, which lets
1108
- * the caller persist and commit the batch atomically with one shadow commit.
1203
+ * the caller persist and commit the batch atomically.
1109
1204
  */
1110
- export async function applyAutoAbandonMetadata(specDir, selection, options) {
1205
+ export async function applyAutoAbandonMetadata(sessionsDir, selection, options) {
1111
1206
  const dryRun = options?.dryRun === true;
1112
1207
  const endedAt = new Date(options?.nowMs ?? Date.now()).toISOString();
1113
1208
  const updates = [];
@@ -1121,7 +1216,7 @@ export async function applyAutoAbandonMetadata(specDir, selection, options) {
1121
1216
  });
1122
1217
  if (dryRun)
1123
1218
  continue;
1124
- const metadata = await getSession(specDir, candidate.sessionId);
1219
+ const metadata = await getSession(sessionsDir, candidate.sessionId);
1125
1220
  if (!metadata)
1126
1221
  continue;
1127
1222
  const updated = {
@@ -1130,7 +1225,7 @@ export async function applyAutoAbandonMetadata(specDir, selection, options) {
1130
1225
  ended_at: endedAt,
1131
1226
  close_reason: closeReason,
1132
1227
  };
1133
- const metadataPath = getSessionMetadataPath(specDir, candidate.sessionId);
1228
+ const metadataPath = getSessionMetadataPath(sessionsDir, candidate.sessionId);
1134
1229
  const content = YAML.stringify(updated, {
1135
1230
  indent: 2,
1136
1231
  lineWidth: 100,
@@ -1138,15 +1233,14 @@ export async function applyAutoAbandonMetadata(specDir, selection, options) {
1138
1233
  });
1139
1234
  await fsPromises.writeFile(metadataPath, content, "utf-8");
1140
1235
  }
1141
- let shadowCommitted;
1142
- if (!dryRun && updates.length > 0 && options?.shadowCommitMessage) {
1143
- 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)`);
1144
1239
  }
1145
1240
  return {
1146
1241
  dryRun,
1147
1242
  updatedCount: updates.length,
1148
1243
  updates,
1149
- shadowCommitted,
1150
1244
  };
1151
1245
  }
1152
1246
  // ─── Session Log Summaries ───────────────────────────────────────────────────
@@ -1167,8 +1261,8 @@ function resolveSessionType(metadata) {
1167
1261
  * Count lines in events.jsonl without parsing JSON.
1168
1262
  * Much faster than readEvents() for large files.
1169
1263
  */
1170
- async function countEventLines(specDir, sessionId) {
1171
- const eventsPath = getSessionEventsPath(specDir, sessionId);
1264
+ async function countEventLines(sessionsDir, sessionId) {
1265
+ const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
1172
1266
  try {
1173
1267
  const content = await fsPromises.readFile(eventsPath, "utf-8");
1174
1268
  if (!content.trim())
@@ -1182,8 +1276,8 @@ async function countEventLines(specDir, sessionId) {
1182
1276
  /**
1183
1277
  * Count context-iter-*.json files for a session (iteration count).
1184
1278
  */
1185
- async function countIterations(specDir, sessionId) {
1186
- const sessionDir = getSessionDir(specDir, sessionId);
1279
+ async function countIterations(sessionsDir, sessionId) {
1280
+ const sessionDir = getSessionDir(sessionsDir, sessionId);
1187
1281
  try {
1188
1282
  const entries = await fsPromises.readdir(sessionDir);
1189
1283
  return entries.filter((e) => e.startsWith("context-iter-") && e.endsWith(".json")).length;
@@ -1205,8 +1299,8 @@ async function countIterations(specDir, sessionId) {
1205
1299
  *
1206
1300
  * We use a fast substring check before JSON parsing for performance.
1207
1301
  */
1208
- async function countTaskCompletions(specDir, sessionId) {
1209
- const eventsPath = getSessionEventsPath(specDir, sessionId);
1302
+ async function countTaskCompletions(sessionsDir, sessionId) {
1303
+ const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
1210
1304
  try {
1211
1305
  const content = await fsPromises.readFile(eventsPath, "utf-8");
1212
1306
  if (!content.trim())
@@ -1245,18 +1339,18 @@ async function countTaskCompletions(specDir, sessionId) {
1245
1339
  *
1246
1340
  * Gathers metadata and computes metrics lazily (only parses what's needed).
1247
1341
  *
1248
- * @param specDir - The .kspec directory path
1342
+ * @param sessionsDir - The .kspec directory path
1249
1343
  * @param sessionId - Session ID
1250
1344
  * @returns Session summary or null if session doesn't exist
1251
1345
  */
1252
- export async function getSessionLogSummary(specDir, sessionId) {
1253
- const metadata = await getSession(specDir, sessionId);
1346
+ export async function getSessionLogSummary(sessionsDir, sessionId) {
1347
+ const metadata = await getSession(sessionsDir, sessionId);
1254
1348
  if (!metadata)
1255
1349
  return null;
1256
1350
  const [eventCount, iterationCount, tasksCompleted] = await Promise.all([
1257
- countEventLines(specDir, sessionId),
1258
- countIterationsBoundaryAware(specDir, sessionId),
1259
- countTaskCompletions(specDir, sessionId),
1351
+ countEventLines(sessionsDir, sessionId),
1352
+ countIterationsBoundaryAware(sessionsDir, sessionId),
1353
+ countTaskCompletions(sessionsDir, sessionId),
1260
1354
  ]);
1261
1355
  const startMs = new Date(metadata.started_at).getTime();
1262
1356
  const endMs = metadata.ended_at
@@ -1267,6 +1361,7 @@ export async function getSessionLogSummary(specDir, sessionId) {
1267
1361
  id: metadata.id,
1268
1362
  status: metadata.status,
1269
1363
  agent_type: metadata.agent_type,
1364
+ agent_id: metadata.agent_id,
1270
1365
  session_type: resolveSessionType(metadata),
1271
1366
  trigger: metadata.trigger,
1272
1367
  task_id: metadata.task_id,
@@ -1278,15 +1373,48 @@ export async function getSessionLogSummary(specDir, sessionId) {
1278
1373
  tasks_completed: tasksCompleted,
1279
1374
  };
1280
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
+ }
1281
1409
  /**
1282
1410
  * Get summaries for all sessions.
1283
1411
  *
1284
- * @param specDir - The .kspec directory path
1412
+ * @param sessionsDir - The .kspec directory path
1285
1413
  * @returns Array of session summaries
1286
1414
  */
1287
- export async function getAllSessionLogSummaries(specDir) {
1288
- const sessionIds = await listSessions(specDir);
1289
- 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)));
1290
1418
  return summaries.filter((s) => s !== null);
1291
1419
  }
1292
1420
  // ─── Context Snapshots ───────────────────────────────────────────────────────
@@ -1296,14 +1424,14 @@ export async function getAllSessionLogSummaries(specDir) {
1296
1424
  * This creates an audit trail of what context the agent saw at each iteration,
1297
1425
  * useful for debugging and reviewing agent behavior.
1298
1426
  *
1299
- * @param specDir - The .kspec directory path
1427
+ * @param sessionsDir - The .kspec directory path
1300
1428
  * @param sessionId - Session ID
1301
1429
  * @param iteration - Iteration number
1302
1430
  * @param context - The session context data
1303
1431
  */
1304
- export async function saveSessionContext(specDir, sessionId, iteration, context) {
1305
- const sessionDir = getSessionDir(specDir, sessionId);
1306
- 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);
1307
1435
  // Ensure session directory exists
1308
1436
  await fsPromises.mkdir(sessionDir, { recursive: true });
1309
1437
  // Write context snapshot as pretty JSON
@@ -1313,13 +1441,13 @@ export async function saveSessionContext(specDir, sessionId, iteration, context)
1313
1441
  /**
1314
1442
  * Read session context snapshot for a given iteration.
1315
1443
  *
1316
- * @param specDir - The .kspec directory path
1444
+ * @param sessionsDir - The .kspec directory path
1317
1445
  * @param sessionId - Session ID
1318
1446
  * @param iteration - Iteration number
1319
1447
  * @returns The context snapshot or null if not found
1320
1448
  */
1321
- export async function readSessionContext(specDir, sessionId, iteration) {
1322
- const contextPath = getSessionContextPath(specDir, sessionId, iteration);
1449
+ export async function readSessionContext(sessionsDir, sessionId, iteration) {
1450
+ const contextPath = getSessionContextPath(sessionsDir, sessionId, iteration);
1323
1451
  try {
1324
1452
  const content = await fsPromises.readFile(contextPath, "utf-8");
1325
1453
  return JSON.parse(content);
@@ -1333,12 +1461,12 @@ export async function readSessionContext(specDir, sessionId, iteration) {
1333
1461
  *
1334
1462
  * AC: @session-log-show ac-7, ac-8, ac-9
1335
1463
  *
1336
- * @param specDir - The .kspec directory path
1464
+ * @param sessionsDir - The .kspec directory path
1337
1465
  * @param idOrPrefix - Full session ID or prefix (e.g., first 8 chars)
1338
1466
  * @returns Resolution result
1339
1467
  */
1340
- export async function resolveSessionId(specDir, idOrPrefix) {
1341
- const sessionIds = await listSessions(specDir);
1468
+ export async function resolveSessionId(sessionsDir, idOrPrefix) {
1469
+ const sessionIds = await listSessions(sessionsDir);
1342
1470
  // First, try exact match
1343
1471
  if (sessionIds.includes(idOrPrefix)) {
1344
1472
  return { ok: true, id: idOrPrefix };
@@ -1357,8 +1485,8 @@ export async function resolveSessionId(specDir, idOrPrefix) {
1357
1485
  /**
1358
1486
  * Get iteration number from a context snapshot file.
1359
1487
  */
1360
- async function getIterationNumbers(specDir, sessionId) {
1361
- const sessionDir = getSessionDir(specDir, sessionId);
1488
+ async function getIterationNumbers(sessionsDir, sessionId) {
1489
+ const sessionDir = getSessionDir(sessionsDir, sessionId);
1362
1490
  try {
1363
1491
  const entries = await fsPromises.readdir(sessionDir);
1364
1492
  const iterations = [];
@@ -1550,14 +1678,14 @@ function boundaryIterationGrouping(events, boundaries) {
1550
1678
  *
1551
1679
  * AC: @session-log-show ac-2, ac-10
1552
1680
  */
1553
- async function computeIterationSummaries(specDir, sessionId) {
1554
- const events = await readEvents(specDir, sessionId);
1681
+ async function computeIterationSummaries(sessionsDir, sessionId) {
1682
+ const events = await readEvents(sessionsDir, sessionId);
1555
1683
  const boundaries = findIterationBoundaries(events);
1556
1684
  if (boundaries.length > 0) {
1557
1685
  return boundaryIterationGrouping(events, boundaries);
1558
1686
  }
1559
1687
  // Legacy fallback: no prompt.sent boundaries with phase "task-work"
1560
- const snapshotIterations = await getIterationNumbers(specDir, sessionId);
1688
+ const snapshotIterations = await getIterationNumbers(sessionsDir, sessionId);
1561
1689
  return legacyIterationGrouping(events, snapshotIterations);
1562
1690
  }
1563
1691
  /**
@@ -1568,14 +1696,14 @@ async function computeIterationSummaries(specDir, sessionId) {
1568
1696
  *
1569
1697
  * Falls back to counting context-iter-*.json files when no boundaries exist.
1570
1698
  */
1571
- async function countIterationsBoundaryAware(specDir, sessionId) {
1572
- const events = await readEvents(specDir, sessionId);
1699
+ async function countIterationsBoundaryAware(sessionsDir, sessionId) {
1700
+ const events = await readEvents(sessionsDir, sessionId);
1573
1701
  const boundaries = findIterationBoundaries(events);
1574
1702
  if (boundaries.length > 0) {
1575
1703
  return boundaries.length;
1576
1704
  }
1577
1705
  // Legacy fallback: count from context snapshots and event data
1578
- const snapshotIterations = await getIterationNumbers(specDir, sessionId);
1706
+ const snapshotIterations = await getIterationNumbers(sessionsDir, sessionId);
1579
1707
  const allIterations = new Set(snapshotIterations);
1580
1708
  for (const event of events) {
1581
1709
  const data = event.data;
@@ -1588,17 +1716,17 @@ async function countIterationsBoundaryAware(specDir, sessionId) {
1588
1716
  /**
1589
1717
  * Get full session detail for session log show.
1590
1718
  *
1591
- * @param specDir - The .kspec directory path
1719
+ * @param sessionsDir - The .kspec directory path
1592
1720
  * @param sessionId - Session ID (must be resolved first)
1593
1721
  * @returns Session detail or null if not found
1594
1722
  */
1595
- export async function getSessionLogDetail(specDir, sessionId) {
1596
- const metadata = await getSession(specDir, sessionId);
1723
+ export async function getSessionLogDetail(sessionsDir, sessionId) {
1724
+ const metadata = await getSession(sessionsDir, sessionId);
1597
1725
  if (!metadata)
1598
1726
  return null;
1599
1727
  const [eventCount, iterations] = await Promise.all([
1600
- countEventLines(specDir, sessionId),
1601
- computeIterationSummaries(specDir, sessionId),
1728
+ countEventLines(sessionsDir, sessionId),
1729
+ computeIterationSummaries(sessionsDir, sessionId),
1602
1730
  ]);
1603
1731
  const startMs = new Date(metadata.started_at).getTime();
1604
1732
  const endMs = metadata.ended_at
@@ -1650,6 +1778,7 @@ export function computeSessionLogStats(summaries) {
1650
1778
  abandoned: 0,
1651
1779
  timed_out: 0,
1652
1780
  failed: 0,
1781
+ stalled: 0,
1653
1782
  };
1654
1783
  for (const s of summaries) {
1655
1784
  totalEvents += s.event_count;
@@ -1661,7 +1790,7 @@ export function computeSessionLogStats(summaries) {
1661
1790
  const n = summaries.length;
1662
1791
  // Build status breakdown
1663
1792
  const statusBreakdown = [];
1664
- for (const status of ["completed", "active", "abandoned", "timed_out", "failed"]) {
1793
+ for (const status of ["completed", "active", "abandoned", "timed_out", "failed", "stalled"]) {
1665
1794
  const count = statusCounts[status] || 0;
1666
1795
  if (count > 0) {
1667
1796
  statusBreakdown.push({
@@ -1691,11 +1820,11 @@ export function computeSessionLogStats(summaries) {
1691
1820
  *
1692
1821
  * AC: @session-log-stats ac-6
1693
1822
  */
1694
- export async function computeToolUsageStats(specDir, sessionIds, limit = 10) {
1823
+ export async function computeToolUsageStats(sessionsDir, sessionIds, limit = 10) {
1695
1824
  const toolCounts = {};
1696
1825
  let totalToolCalls = 0;
1697
1826
  for (const sessionId of sessionIds) {
1698
- const eventsPath = getSessionEventsPath(specDir, sessionId);
1827
+ const eventsPath = getSessionEventsPath(sessionsDir, sessionId);
1699
1828
  try {
1700
1829
  const content = await fsPromises.readFile(eventsPath, "utf-8");
1701
1830
  if (!content.trim())
@@ -1850,34 +1979,45 @@ function extractContentExcerpt(data, pattern, maxLength = 200) {
1850
1979
  *
1851
1980
  * AC: @session-log-search ac-1, ac-2, ac-3, ac-5, ac-7
1852
1981
  *
1853
- * @param specDir - The .kspec directory path
1982
+ * @param sessionsDir - The .kspec directory path
1854
1983
  * @param pattern - Case-insensitive substring to search for
1855
1984
  * @param options - Search filtering options
1856
1985
  * @returns Array of search results grouped by session
1857
1986
  */
1858
- export async function searchSessionEvents(specDir, pattern, options = {}) {
1987
+ export async function searchSessionEvents(sessionsDir, pattern, options = {}) {
1859
1988
  // Defense-in-depth: normalize limit to a valid positive integer
1860
1989
  const rawLimit = options.limit ?? 50;
1861
1990
  const limit = Number.isNaN(rawLimit) || rawLimit <= 0 ? 50 : rawLimit;
1862
1991
  const lowerPattern = pattern.toLowerCase();
1863
1992
  const resolveBlobs = options.resolveBlobs ?? false;
1864
- // Get all session summaries for metadata filtering
1865
- const allSummaries = await getAllSessionLogSummaries(specDir);
1866
- // AC: @session-log-search ac-3 - Pre-filter by --since
1867
- let filteredSummaries = allSummaries;
1868
- if (options.sinceDate) {
1869
- 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];
1870
1997
  }
1871
- // AC: @session-log-search ac-7 - Pre-filter by --agent
1872
- if (options.agentType) {
1873
- 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
+ }
1874
2014
  }
1875
2015
  const results = [];
1876
2016
  let totalMatches = 0;
1877
2017
  for (const summary of filteredSummaries) {
1878
2018
  if (totalMatches >= limit)
1879
2019
  break;
1880
- const eventsPath = getSessionEventsPath(specDir, summary.id);
2020
+ const eventsPath = getSessionEventsPath(sessionsDir, summary.id);
1881
2021
  let content;
1882
2022
  try {
1883
2023
  content = await fsPromises.readFile(eventsPath, "utf-8");
@@ -1916,7 +2056,7 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
1916
2056
  }
1917
2057
  }
1918
2058
  const searchableData = resolveBlobs
1919
- ? await resolveSessionBlobPointers(specDir, summary.id, event.data)
2059
+ ? await resolveSessionBlobPointers(sessionsDir, summary.id, event.data)
1920
2060
  : event.data;
1921
2061
  // Verify match in stringified data (not just line, in case pattern
1922
2062
  // appears in metadata)
@@ -1926,6 +2066,7 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
1926
2066
  // AC: @session-log-search ac-4 - Create match with excerpt
1927
2067
  matches.push({
1928
2068
  session_id: summary.id,
2069
+ event_seq: typeof event.seq === "number" ? event.seq : -1,
1929
2070
  timestamp: event.ts,
1930
2071
  event_type: event.type,
1931
2072
  content_excerpt: extractContentExcerpt(searchableData, pattern, 200),
@@ -1960,13 +2101,13 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
1960
2101
  * AC: @session-creation-and-env-injection ac-budget-local
1961
2102
  * AC: @session-creation-and-env-injection ac-library
1962
2103
  *
1963
- * @param specDir - The .kspec directory path
2104
+ * @param sessionsDir - The .kspec directory path
1964
2105
  * @param input - Session creation parameters
1965
2106
  * @returns Session metadata and optional budget (no console output)
1966
2107
  */
1967
- export async function createSessionWithBudget(specDir, input) {
2108
+ export async function createSessionWithBudget(sessionsDir, input) {
1968
2109
  // Create session
1969
- const session = await createSession(specDir, {
2110
+ const session = await createSession(sessionsDir, {
1970
2111
  id: input.id,
1971
2112
  agent_type: input.agent_type,
1972
2113
  task_id: input.task_id,
@@ -1974,7 +2115,7 @@ export async function createSessionWithBudget(specDir, input) {
1974
2115
  // Optionally create budget
1975
2116
  let budget = null;
1976
2117
  if (input.budget !== undefined && input.budget > 0) {
1977
- budget = await createBudget(specDir, input.id, input.budget);
2118
+ budget = await createBudget(sessionsDir, input.id, input.budget);
1978
2119
  }
1979
2120
  return {
1980
2121
  session_id: input.id,
@@ -2341,13 +2482,13 @@ export async function removeEnvForAdapter(adapterId, previousValue) {
2341
2482
  *
2342
2483
  * AC: @session-creation-and-env-injection ac-invalid-session
2343
2484
  *
2344
- * @param specDir - The .kspec directory path
2485
+ * @param sessionsDir - The .kspec directory path
2345
2486
  * @param sessionId - The session ID to validate
2346
2487
  * @returns Validation result with error details if invalid
2347
2488
  */
2348
- export async function validateSessionId(specDir, sessionId) {
2489
+ export async function validateSessionId(sessionsDir, sessionId) {
2349
2490
  // Check if session directory exists
2350
- const exists = await sessionExists(specDir, sessionId);
2491
+ const exists = await sessionExists(sessionsDir, sessionId);
2351
2492
  if (!exists) {
2352
2493
  return {
2353
2494
  valid: false,
@@ -2356,7 +2497,7 @@ export async function validateSessionId(specDir, sessionId) {
2356
2497
  };
2357
2498
  }
2358
2499
  // Try to read and validate session metadata
2359
- const session = await getSession(specDir, sessionId);
2500
+ const session = await getSession(sessionsDir, sessionId);
2360
2501
  if (!session) {
2361
2502
  return {
2362
2503
  valid: false,
@@ -2383,24 +2524,24 @@ async function writeBudgetAtomic(filePath, budget) {
2383
2524
  /**
2384
2525
  * Create a budget for a session.
2385
2526
  *
2386
- * Writes budget.json to .kspec/sessions/{id}/ on the local filesystem
2527
+ * Writes budget.json to .kspec-sessions/{id}/ on the local filesystem
2387
2528
  * (NOT committed to shadow branch).
2388
2529
  *
2389
2530
  * AC: @session-creation-and-env-injection ac-budget
2390
2531
  * AC: @session-creation-and-env-injection ac-budget-local
2391
2532
  *
2392
- * @param specDir - The .kspec directory path
2533
+ * @param sessionsDir - The .kspec directory path
2393
2534
  * @param sessionId - Session ID
2394
2535
  * @param maxPerCycle - Maximum tasks allowed per cycle
2395
2536
  * @returns The created budget
2396
2537
  */
2397
- export async function createBudget(specDir, sessionId, maxPerCycle) {
2538
+ export async function createBudget(sessionsDir, sessionId, maxPerCycle) {
2398
2539
  const budget = {
2399
2540
  max_per_cycle: maxPerCycle,
2400
2541
  started_this_cycle: 0,
2401
2542
  };
2402
2543
  const validated = TaskBudgetSchema.parse(budget);
2403
- const budgetPath = getSessionBudgetPath(specDir, sessionId);
2544
+ const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
2404
2545
  await writeBudgetAtomic(budgetPath, validated);
2405
2546
  return validated;
2406
2547
  }
@@ -2409,12 +2550,12 @@ export async function createBudget(specDir, sessionId, maxPerCycle) {
2409
2550
  *
2410
2551
  * AC: @task-budget-enforcement ac-no-budget
2411
2552
  *
2412
- * @param specDir - The .kspec directory path
2553
+ * @param sessionsDir - The .kspec directory path
2413
2554
  * @param sessionId - Session ID
2414
2555
  * @returns Budget or null if no budget configured (opt-in)
2415
2556
  */
2416
- export async function getBudget(specDir, sessionId) {
2417
- const budgetPath = getSessionBudgetPath(specDir, sessionId);
2557
+ export async function getBudget(sessionsDir, sessionId) {
2558
+ const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
2418
2559
  let content;
2419
2560
  try {
2420
2561
  content = await fsPromises.readFile(budgetPath, "utf-8");
@@ -2440,16 +2581,24 @@ export async function getBudget(specDir, sessionId) {
2440
2581
  * AC: @task-budget-enforcement ac-no-budget
2441
2582
  * AC: @task-budget-enforcement ac-no-session
2442
2583
  *
2443
- * @param specDir - The .kspec directory path
2584
+ * @param sessionsDir - The .kspec directory path
2444
2585
  * @param sessionId - Session ID, or undefined if KSPEC_SESSION_ID not set
2445
2586
  * @returns Budget check result
2446
2587
  */
2447
- export async function checkBudget(specDir, sessionId) {
2588
+ export async function checkBudget(sessionsDir, sessionId) {
2448
2589
  // AC: @task-budget-enforcement ac-no-session — no session means no check
2449
2590
  if (!sessionId) {
2450
2591
  return { allowed: true };
2451
2592
  }
2452
- 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);
2453
2602
  // AC: @task-budget-enforcement ac-no-budget — no budget means no check
2454
2603
  if (!budget) {
2455
2604
  return { allowed: true };
@@ -2474,12 +2623,12 @@ export async function checkBudget(specDir, sessionId) {
2474
2623
  * AC: @task-budget-enforcement ac-increment
2475
2624
  * AC: @task-budget-enforcement ac-atomic-write
2476
2625
  *
2477
- * @param specDir - The .kspec directory path
2626
+ * @param sessionsDir - The .kspec directory path
2478
2627
  * @param sessionId - Session ID
2479
2628
  * @returns Updated budget, or null if no budget configured
2480
2629
  */
2481
- export async function incrementBudget(specDir, sessionId) {
2482
- const budget = await getBudget(specDir, sessionId);
2630
+ export async function incrementBudget(sessionsDir, sessionId) {
2631
+ const budget = await getBudget(sessionsDir, sessionId);
2483
2632
  if (!budget) {
2484
2633
  return null;
2485
2634
  }
@@ -2488,7 +2637,7 @@ export async function incrementBudget(specDir, sessionId) {
2488
2637
  started_this_cycle: budget.started_this_cycle + 1,
2489
2638
  };
2490
2639
  const validated = TaskBudgetSchema.parse(updated);
2491
- const budgetPath = getSessionBudgetPath(specDir, sessionId);
2640
+ const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
2492
2641
  await writeBudgetAtomic(budgetPath, validated);
2493
2642
  return validated;
2494
2643
  }
@@ -2501,12 +2650,12 @@ export async function incrementBudget(specDir, sessionId) {
2501
2650
  * AC: @task-budget-enforcement ac-reset
2502
2651
  * AC: @task-budget-enforcement ac-atomic-write
2503
2652
  *
2504
- * @param specDir - The .kspec directory path
2653
+ * @param sessionsDir - The .kspec directory path
2505
2654
  * @param sessionId - Session ID
2506
2655
  * @returns Updated budget, or null if no budget configured
2507
2656
  */
2508
- export async function resetBudget(specDir, sessionId) {
2509
- const budget = await getBudget(specDir, sessionId);
2657
+ export async function resetBudget(sessionsDir, sessionId) {
2658
+ const budget = await getBudget(sessionsDir, sessionId);
2510
2659
  if (!budget) {
2511
2660
  return null;
2512
2661
  }
@@ -2515,7 +2664,7 @@ export async function resetBudget(specDir, sessionId) {
2515
2664
  started_this_cycle: 0,
2516
2665
  };
2517
2666
  const validated = TaskBudgetSchema.parse(updated);
2518
- const budgetPath = getSessionBudgetPath(specDir, sessionId);
2667
+ const budgetPath = getSessionBudgetPath(sessionsDir, sessionId);
2519
2668
  await writeBudgetAtomic(budgetPath, validated);
2520
2669
  return validated;
2521
2670
  }