@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
@@ -0,0 +1,1142 @@
1
+ /**
2
+ * Review CLI commands
3
+ *
4
+ * AC: @review-cli-commands ac-1 — CLI provides commands for core review workflow
5
+ * AC: @review-cli-commands ac-2 — Output includes subject, lifecycle, disposition, gate, threads, linkage
6
+ * AC: @review-cli-commands ac-3 — Compatible with batch-oriented mutation flows
7
+ *
8
+ * AC: @review-cli-creation-and-query ac-1, ac-2, ac-3, ac-4, ac-5
9
+ * AC: @review-cli-mutation-commands ac-1, ac-1b, ac-2, ac-3, ac-4, ac-5, ac-6, ac-7
10
+ * AC: @review-cli-task-linkage ac-1, ac-2
11
+ */
12
+ import { ulid } from "ulid";
13
+ import { markMutating } from "../command-annotations.js";
14
+ import { createReviewRecord, findReviewByRef, getAuthor, handleVerdictTaskTransition, initContext, linkReviewToTasks, loadAllTasks, loadReviewRecords, mutateReviewAtomically, saveReviewRecord, shortestUniqueUlid, } from "../../parser/index.js";
15
+ import { commitIfShadow } from "../../parser/shadow.js";
16
+ import { errors } from "../../strings/index.js";
17
+ import { EXIT_CODES } from "../exit-codes.js";
18
+ import { error, info, isJsonMode, output, success } from "../output.js";
19
+ import { formatRelativeTime as formatRelativeTimeUtil } from "../../utils/time.js";
20
+ // --- Helpers ---
21
+ function formatRelativeTime(dateStr) {
22
+ return formatRelativeTimeUtil(new Date(dateStr));
23
+ }
24
+ /**
25
+ * Exit with error guidance for review commands.
26
+ * AC: @trait-error-guidance ac-1, ac-2
27
+ */
28
+ function exitWithGuidance(message, exitCode, suggestion, details) {
29
+ if (suggestion) {
30
+ if (isJsonMode()) {
31
+ error(message, {
32
+ ...details,
33
+ suggestion,
34
+ guidance: suggestion,
35
+ });
36
+ }
37
+ else {
38
+ error(message);
39
+ console.error(`Suggestion: ${suggestion}`);
40
+ }
41
+ }
42
+ else {
43
+ error(message, isJsonMode() ? details : undefined);
44
+ }
45
+ process.exit(exitCode);
46
+ }
47
+ /**
48
+ * Resolve a review reference (ULID, short ULID, or slug).
49
+ * AC: @trait-error-guidance ac-3
50
+ */
51
+ function resolveReviewRef(ref, reviews) {
52
+ const found = findReviewByRef(reviews, ref);
53
+ if (!found) {
54
+ exitWithGuidance(errors.reference.reviewNotFound(ref), EXIT_CODES.NOT_FOUND, "Check available reviews with: kspec review list", { ref, entity: "review" });
55
+ }
56
+ return found;
57
+ }
58
+ function shortReviewRef(review, reviews) {
59
+ return shortestUniqueUlid(review._ulid, reviews.map((r) => r._ulid));
60
+ }
61
+ /**
62
+ * Compute disposition from verdicts.
63
+ * AC: @review-cli-commands ac-2
64
+ */
65
+ function computeDisposition(review) {
66
+ if (review.verdicts.length === 0)
67
+ return "pending";
68
+ const hasChangesRequested = review.verdicts.some((v) => v.decision === "request_changes");
69
+ if (hasChangesRequested)
70
+ return "changes_requested";
71
+ const hasApproval = review.verdicts.some((v) => v.decision === "approve");
72
+ if (hasApproval)
73
+ return "approved";
74
+ return "pending";
75
+ }
76
+ /**
77
+ * Compute gate state from checks.
78
+ * AC: @review-cli-commands ac-2
79
+ */
80
+ function computeGateState(review) {
81
+ const requiredChecks = review.checks.filter((c) => c.required);
82
+ if (requiredChecks.length === 0)
83
+ return "pending";
84
+ const allPassing = requiredChecks.every((c) => c.status === "pass");
85
+ if (allPassing)
86
+ return "passing";
87
+ const hasFailing = requiredChecks.some((c) => c.status === "fail");
88
+ if (hasFailing)
89
+ return "failing";
90
+ return "pending";
91
+ }
92
+ /**
93
+ * Compute thread state summary.
94
+ */
95
+ function computeThreadState(review) {
96
+ const total = review.threads.length;
97
+ const resolved = review.threads.filter((t) => t.resolved_at).length;
98
+ const unresolved = total - resolved;
99
+ const blockers_unresolved = review.threads.filter((t) => t.kind === "blocker" && !t.resolved_at).length;
100
+ return { total, resolved, unresolved, blockers_unresolved };
101
+ }
102
+ /**
103
+ * Format subject for display.
104
+ */
105
+ function formatSubject(subject) {
106
+ switch (subject.type) {
107
+ case "code":
108
+ return `code: ${subject.base_commit.slice(0, 8)}..${subject.head_commit.slice(0, 8)}${subject.head_branch ? ` (${subject.head_branch})` : ""}`;
109
+ case "plan":
110
+ return `plan: ${subject.ref}`;
111
+ case "task":
112
+ return `task: ${subject.ref}`;
113
+ case "spec":
114
+ return `spec: ${subject.ref}`;
115
+ case "external":
116
+ return `external: ${subject.url}`;
117
+ }
118
+ }
119
+ /**
120
+ * Build JSON output for a review with computed fields.
121
+ * AC: @review-cli-commands ac-2
122
+ * AC: @trait-json-output ac-2, ac-4, ac-5
123
+ */
124
+ function buildReviewOutput(review, reviews) {
125
+ const threadState = computeThreadState(review);
126
+ return {
127
+ _ulid: review._ulid,
128
+ ref: `@${review.slugs[0] || review._ulid}`,
129
+ slugs: review.slugs,
130
+ title: review.title,
131
+ lifecycle_state: review.lifecycle_state,
132
+ disposition: computeDisposition(review),
133
+ gate_state: computeGateState(review),
134
+ subject: review.subject,
135
+ author: review.author,
136
+ related_refs: review.related_refs,
137
+ threads: review.threads,
138
+ thread_state: threadState,
139
+ checks: review.checks,
140
+ verdicts: review.verdicts,
141
+ events: review.events,
142
+ notes: review.notes,
143
+ external_links: review.external_links,
144
+ examined_commit: review.examined_commit,
145
+ created_at: review.created_at,
146
+ updated_at: review.updated_at,
147
+ };
148
+ }
149
+ /**
150
+ * Format review details for human-readable output.
151
+ * AC: @review-cli-commands ac-2
152
+ */
153
+ function formatReviewDetails(review, reviews) {
154
+ const shortRef = shortReviewRef(review, reviews);
155
+ const disposition = computeDisposition(review);
156
+ const gateState = computeGateState(review);
157
+ const threadState = computeThreadState(review);
158
+ console.log(review.title);
159
+ console.log("─".repeat(40));
160
+ console.log(`ULID: ${review._ulid}`);
161
+ if (review.slugs.length > 0) {
162
+ console.log(`Slugs: ${review.slugs.join(", ")}`);
163
+ }
164
+ console.log(`Lifecycle: ${review.lifecycle_state}`);
165
+ console.log(`Disposition: ${disposition}`);
166
+ console.log(`Gate: ${gateState}`);
167
+ console.log(`Subject: ${formatSubject(review.subject)}`);
168
+ console.log(`Author: ${review.author}`);
169
+ if (review.examined_commit) {
170
+ console.log(`Examined: ${review.examined_commit}`);
171
+ }
172
+ console.log(`Created: ${review.created_at} (${formatRelativeTime(review.created_at)})`);
173
+ if (review.updated_at) {
174
+ console.log(`Updated: ${review.updated_at} (${formatRelativeTime(review.updated_at)})`);
175
+ }
176
+ if (review.related_refs.length > 0) {
177
+ console.log(`\n─── Related Refs ───`);
178
+ for (const ref of review.related_refs) {
179
+ console.log(` ${ref}`);
180
+ }
181
+ }
182
+ if (review.external_links.length > 0) {
183
+ console.log(`\n─── External Links ───`);
184
+ for (const link of review.external_links) {
185
+ console.log(` ${link.label || link.url}${link.provider ? ` (${link.provider})` : ""}`);
186
+ if (link.label)
187
+ console.log(` ${link.url}`);
188
+ }
189
+ }
190
+ if (review.threads.length > 0) {
191
+ console.log(`\n─── Threads (${threadState.unresolved} unresolved, ${threadState.blockers_unresolved} blockers) ───`);
192
+ for (const thread of review.threads) {
193
+ const resolved = thread.resolved_at ? "✓" : "○";
194
+ const anchor = thread.anchor
195
+ ? thread.anchor.type === "code"
196
+ ? ` ${thread.anchor.path}:${thread.anchor.line_start}`
197
+ : thread.anchor.type === "structured"
198
+ ? ` ${thread.anchor.section || thread.anchor.field || thread.anchor.ref || ""}`
199
+ : ""
200
+ : "";
201
+ console.log(` ${resolved} [${thread.kind}]${anchor} (${thread.entries.length} entries)`);
202
+ if (thread.entries.length > 0) {
203
+ const first = thread.entries[0];
204
+ const body = first.body.length > 80 ? first.body.slice(0, 77) + "..." : first.body;
205
+ console.log(` ${first.author}: ${body}`);
206
+ }
207
+ }
208
+ }
209
+ if (review.checks.length > 0) {
210
+ console.log(`\n─── Checks ───`);
211
+ for (const check of review.checks) {
212
+ const required = check.required ? "(required)" : "(optional)";
213
+ console.log(` ${check.status === "pass" ? "✓" : check.status === "fail" ? "✗" : "○"} ${check.name} ${required} — ${check.status}`);
214
+ }
215
+ }
216
+ if (review.verdicts.length > 0) {
217
+ console.log(`\n─── Verdicts ───`);
218
+ for (const verdict of review.verdicts) {
219
+ console.log(` ${verdict.reviewer} (${verdict.role}): ${verdict.decision} — ${formatRelativeTime(verdict.created_at)}`);
220
+ }
221
+ }
222
+ if (review.events.length > 0) {
223
+ console.log(`\n─── Events (${review.events.length}) ───`);
224
+ for (const event of review.events.slice(-10)) {
225
+ console.log(` ${event.event_type} by ${event.actor} — ${formatRelativeTime(event.timestamp)}`);
226
+ }
227
+ if (review.events.length > 10) {
228
+ console.log(` ... and ${review.events.length - 10} more`);
229
+ }
230
+ }
231
+ }
232
+ /**
233
+ * Create an event entry.
234
+ * AC: @review-record-core-model ac-4
235
+ */
236
+ function createEvent(eventType, actor, payload = {}) {
237
+ return {
238
+ _ulid: ulid(),
239
+ event_type: eventType,
240
+ actor,
241
+ timestamp: new Date().toISOString(),
242
+ payload,
243
+ };
244
+ }
245
+ /**
246
+ * Parse subject from CLI flags.
247
+ * AC: @review-cli-creation-and-query ac-1, ac-2
248
+ */
249
+ function parseSubjectFromOptions(options) {
250
+ const subjectType = options.subjectType;
251
+ if (subjectType === "code" || options.base) {
252
+ if (!options.base || !options.head) {
253
+ exitWithGuidance("Code subject requires --base and --head commit refs", EXIT_CODES.USAGE_ERROR, "Usage: kspec review add --title '...' --subject-type code --base <commit> --head <commit>", { field: "base/head", value: "missing" });
254
+ }
255
+ const subject = {
256
+ type: "code",
257
+ base_commit: options.base,
258
+ head_commit: options.head,
259
+ };
260
+ if (options.mergeBase) {
261
+ subject.merge_base_commit = options.mergeBase;
262
+ }
263
+ if (options.baseBranch) {
264
+ subject.base_branch = options.baseBranch;
265
+ }
266
+ if (options.headBranch) {
267
+ subject.head_branch = options.headBranch;
268
+ }
269
+ return subject;
270
+ }
271
+ if (subjectType === "external" || options.url) {
272
+ if (!options.url) {
273
+ exitWithGuidance("External subject requires --url", EXIT_CODES.USAGE_ERROR, "Usage: kspec review add --title '...' --subject-type external --url <url>", { field: "url", value: "missing" });
274
+ }
275
+ const subject = {
276
+ type: "external",
277
+ url: options.url,
278
+ };
279
+ if (options.externalId) {
280
+ subject.external_id = options.externalId;
281
+ }
282
+ if (options.provider) {
283
+ subject.provider = options.provider;
284
+ }
285
+ return subject;
286
+ }
287
+ // Ref-backed subjects (plan, task, spec)
288
+ if (!options.subjectRef) {
289
+ exitWithGuidance("Subject is required. Provide --subject-ref for plan/task/spec, or --base/--head for code, or --url for external", EXIT_CODES.USAGE_ERROR, "Usage: kspec review add --title '...' --subject-ref @ref [--subject-type plan|task|spec]", { field: "subject", value: "missing" });
290
+ }
291
+ const ref = options.subjectRef;
292
+ const type = (subjectType || "task");
293
+ if (!["plan", "task", "spec"].includes(type)) {
294
+ exitWithGuidance(`Invalid subject type: ${type}. Must be one of: plan, task, spec, code, external`, EXIT_CODES.USAGE_ERROR, "Valid subject types: plan, task, spec, code, external", { field: "subject-type", value: type });
295
+ }
296
+ return {
297
+ type,
298
+ ref: ref.startsWith("@") ? ref : `@${ref}`,
299
+ shadow_commit: "",
300
+ content_hash: "",
301
+ };
302
+ }
303
+ /**
304
+ * Parse applies_to_version from CLI flags.
305
+ */
306
+ function parseVersionFromOptions(options) {
307
+ if (options.versionBase && options.versionHead) {
308
+ return {
309
+ type: "code_compare",
310
+ base_commit: options.versionBase,
311
+ head_commit: options.versionHead,
312
+ };
313
+ }
314
+ if (options.versionHash) {
315
+ return {
316
+ type: "entity_version",
317
+ content_hash: options.versionHash,
318
+ };
319
+ }
320
+ // Default code compare with empty commits (must be provided)
321
+ exitWithGuidance("Version context is required. Provide --version-base and --version-head for code, or --version-hash for entity", EXIT_CODES.USAGE_ERROR, "Usage: --version-base <commit> --version-head <commit> OR --version-hash <hash>", { field: "version", value: "missing" });
322
+ }
323
+ // --- Command Registration ---
324
+ export function registerReviewCommands(program) {
325
+ const review = program
326
+ .command("review")
327
+ .description("Manage first-party review records");
328
+ // --- review add ---
329
+ // AC: @review-cli-creation-and-query ac-1, ac-2, ac-5
330
+ // AC: @review-cli-commands ac-1
331
+ markMutating(review
332
+ .command("add")
333
+ .description("Create a new review record")
334
+ .requiredOption("--title <title>", "Review title")
335
+ .option("--slug <slug>", "Custom slug for the review")
336
+ .option("--subject-type <type>", "Subject type: plan, task, spec, code, external")
337
+ .option("--subject-ref <ref>", "Subject reference (for plan/task/spec)")
338
+ .option("--base <commit>", "Base commit (for code subjects)")
339
+ .option("--head <commit>", "Head commit (for code subjects)")
340
+ .option("--merge-base <commit>", "Merge base commit (for code subjects)")
341
+ .option("--base-branch <branch>", "Base branch name (for code subjects)")
342
+ .option("--head-branch <branch>", "Head branch name (for code subjects)")
343
+ .option("--url <url>", "External URL (for external subjects)")
344
+ .option("--external-id <id>", "External identifier (for external subjects)")
345
+ .option("--provider <provider>", "External provider (for external subjects)")
346
+ .option("--related-ref <ref...>", "Related references (e.g. task refs)")
347
+ .option("--author <author>", "Review author (defaults to configured author)")
348
+ .option("--examined-commit <commit>", "Commit hash the reviewer is examining")
349
+ .action(async (options) => {
350
+ try {
351
+ const ctx = await initContext();
352
+ const author = options.author || getAuthor(ctx.config?.identity?.author) || "unknown";
353
+ const subject = parseSubjectFromOptions(options);
354
+ // AC: @review-fix-cycle-diff ac-1 — capture examined commit
355
+ const examinedCommit = options.examinedCommit ||
356
+ process.env.KSPEC_DISPATCH_CANONICAL_HEAD ||
357
+ null;
358
+ const review = createReviewRecord({
359
+ title: options.title,
360
+ slugs: options.slug ? [options.slug] : [],
361
+ subject,
362
+ author,
363
+ related_refs: options.relatedRef || [],
364
+ examined_commit: examinedCommit,
365
+ events: [createEvent("lifecycle_change", author, { to: "draft" })],
366
+ });
367
+ await saveReviewRecord(ctx, { ...review, _sourceFile: undefined });
368
+ // AC: @trait-shadow-commit ac-1, ac-2, ac-3
369
+ await commitIfShadow(ctx.shadow, "review-add", review.slugs[0] || review._ulid.slice(0, 8), options.title);
370
+ // AC: @review-task-lifecycle-integration ac-2, ac-3
371
+ // Auto-link review to task(s) via review_ref
372
+ const allTasks = await loadAllTasks(ctx);
373
+ const linkResult = await linkReviewToTasks(ctx, review, allTasks);
374
+ if (linkResult.linkedTasks.length > 0) {
375
+ await commitIfShadow(ctx.shadow, "review-task-link", review.slugs[0] || review._ulid.slice(0, 8), `linked to ${linkResult.linkedTasks.length} task(s)`);
376
+ }
377
+ const reviews = await loadReviewRecords(ctx);
378
+ const shortRef = shortReviewRef({ ...review, _sourceFile: undefined }, reviews);
379
+ output(buildReviewOutput({ ...review, _sourceFile: undefined }, reviews), () => {
380
+ success(`Created review: ${shortRef}${review.slugs.length > 0 ? ` (${review.slugs[0]})` : ""}`);
381
+ });
382
+ }
383
+ catch (err) {
384
+ error(errors.failures.createReview, err);
385
+ process.exit(EXIT_CODES.ERROR);
386
+ }
387
+ }));
388
+ // --- review get ---
389
+ // AC: @review-cli-creation-and-query ac-3
390
+ // AC: @review-cli-commands ac-2
391
+ review
392
+ .command("get <ref>")
393
+ .description("Show review details")
394
+ .action(async (ref) => {
395
+ try {
396
+ const ctx = await initContext();
397
+ const reviews = await loadReviewRecords(ctx);
398
+ const found = resolveReviewRef(ref, reviews);
399
+ output(buildReviewOutput(found, reviews), () => {
400
+ formatReviewDetails(found, reviews);
401
+ });
402
+ }
403
+ catch (err) {
404
+ error(errors.failures.getReview, err);
405
+ process.exit(EXIT_CODES.ERROR);
406
+ }
407
+ });
408
+ // --- review list ---
409
+ // AC: @review-cli-creation-and-query ac-4
410
+ // AC: @trait-filterable-list ac-1, ac-3, ac-4, ac-5, ac-6, ac-7, ac-8
411
+ review
412
+ .command("list")
413
+ .description("List review records")
414
+ .option("--status <status>", "Filter by lifecycle state (draft, open, closed, archived)")
415
+ .option("--disposition <disposition>", "Filter by computed disposition (pending, approved, changes_requested)")
416
+ .option("--subject-type <type>", "Filter by subject type")
417
+ .option("--reviewer <reviewer>", "Filter by reviewer who has submitted a verdict")
418
+ .option("--task <ref>", "Filter reviews linked to a specific task (via subject, related_refs, or review_ref)")
419
+ .option("--limit <n>", "Limit results", parseInt)
420
+ .option("--offset <n>", "Skip first N results", parseInt)
421
+ .option("--count", "Show only the count of matching items")
422
+ .action(async (options) => {
423
+ try {
424
+ const ctx = await initContext();
425
+ let reviews = await loadReviewRecords(ctx);
426
+ // Apply filters
427
+ // AC: @trait-filterable-list ac-1
428
+ if (options.status) {
429
+ reviews = reviews.filter((r) => r.lifecycle_state === options.status);
430
+ }
431
+ if (options.disposition) {
432
+ reviews = reviews.filter((r) => computeDisposition(r) === options.disposition);
433
+ }
434
+ if (options.subjectType) {
435
+ reviews = reviews.filter((r) => r.subject.type === options.subjectType);
436
+ }
437
+ if (options.reviewer) {
438
+ reviews = reviews.filter((r) => r.verdicts.some((v) => v.reviewer === options.reviewer));
439
+ }
440
+ // AC: @review-cli-task-linkage ac-1, ac-2 — filter by task ref
441
+ if (options.task) {
442
+ const taskRef = options.task.startsWith("@") ? options.task : `@${options.task}`;
443
+ const taskRefNoAt = taskRef.slice(1);
444
+ // Also check task.review_ref to find reviews linked via the task schema
445
+ const tasks = await loadAllTasks(ctx);
446
+ const task = tasks.find((t) => t._ulid === taskRefNoAt ||
447
+ t._ulid.toLowerCase().startsWith(taskRefNoAt.toLowerCase()) ||
448
+ t.slugs.includes(taskRefNoAt));
449
+ const reviewRefFromTask = task?.review_ref ?? null;
450
+ reviews = reviews.filter((r) => r.related_refs.includes(taskRef) ||
451
+ (r.subject.type === "task" && r.subject.ref === taskRef) ||
452
+ (reviewRefFromTask && (r._ulid === reviewRefFromTask.replace(/^@/, "") ||
453
+ r.slugs.includes(reviewRefFromTask.replace(/^@/, "")))));
454
+ }
455
+ const total = reviews.length;
456
+ // AC: @trait-filterable-list ac-8 — count mode
457
+ if (options.count) {
458
+ output({ count: total }, () => {
459
+ console.log(String(total));
460
+ });
461
+ return;
462
+ }
463
+ // AC: @trait-filterable-list ac-4 — offset
464
+ if (options.offset) {
465
+ reviews = reviews.slice(options.offset);
466
+ }
467
+ // AC: @trait-filterable-list ac-3 — limit
468
+ if (options.limit) {
469
+ reviews = reviews.slice(0, options.limit);
470
+ }
471
+ // AC: @trait-filterable-list ac-6 — empty results
472
+ if (total === 0) {
473
+ output({ reviews: [], total: 0, message: "No reviews found" }, () => {
474
+ info("No reviews found");
475
+ });
476
+ return;
477
+ }
478
+ // Build output data
479
+ const allReviews = await loadReviewRecords(ctx);
480
+ const outputData = {
481
+ reviews: reviews.map((r) => ({
482
+ _ulid: r._ulid,
483
+ ref: `@${r.slugs[0] || r._ulid}`,
484
+ slugs: r.slugs,
485
+ title: r.title,
486
+ lifecycle_state: r.lifecycle_state,
487
+ disposition: computeDisposition(r),
488
+ gate_state: computeGateState(r),
489
+ subject_type: r.subject.type,
490
+ author: r.author,
491
+ threads: computeThreadState(r),
492
+ created_at: r.created_at,
493
+ })),
494
+ total,
495
+ showing: reviews.length,
496
+ };
497
+ // AC: @trait-filterable-list ac-7 — summary
498
+ output(outputData, () => {
499
+ console.log(`Reviews (${reviews.length}/${total}):`);
500
+ for (const r of reviews) {
501
+ const shortRef = shortReviewRef(r, allReviews);
502
+ const disposition = computeDisposition(r);
503
+ const threadState = computeThreadState(r);
504
+ const threadSummary = threadState.total > 0
505
+ ? ` [${threadState.unresolved}/${threadState.total} threads]`
506
+ : "";
507
+ console.log(` ${shortRef} ${r.lifecycle_state}/${disposition} ${r.title}${threadSummary}`);
508
+ }
509
+ });
510
+ }
511
+ catch (err) {
512
+ error(errors.failures.listReviews, err);
513
+ process.exit(EXIT_CODES.ERROR);
514
+ }
515
+ });
516
+ // --- review comment add ---
517
+ // AC: @review-cli-mutation-commands ac-1
518
+ markMutating(review
519
+ .command("comment <ref>")
520
+ .description("Add a comment thread to a review")
521
+ .requiredOption("--body <body>", "Comment body")
522
+ .option("--kind <kind>", "Thread kind: blocker, question, nit", "nit")
523
+ .option("--path <path>", "Code anchor: file path")
524
+ .option("--side <side>", "Code anchor: base or head")
525
+ .option("--line-start <n>", "Code anchor: start line", parseInt)
526
+ .option("--line-end <n>", "Code anchor: end line", parseInt)
527
+ .option("--commit <commit>", "Code anchor: commit")
528
+ .option("--section <section>", "Structured anchor: section")
529
+ .option("--field <field>", "Structured anchor: field")
530
+ .option("--anchor-ref <ref>", "Structured anchor: ref")
531
+ .option("--author <author>", "Comment author")
532
+ .action(async (ref, options) => {
533
+ try {
534
+ const ctx = await initContext();
535
+ const reviews = await loadReviewRecords(ctx);
536
+ const found = resolveReviewRef(ref, reviews);
537
+ const author = options.author || getAuthor(ctx.config?.identity?.author) || "unknown";
538
+ // Validate thread kind
539
+ // AC: @trait-error-guidance ac-5
540
+ const validKinds = ["blocker", "question", "nit"];
541
+ if (!validKinds.includes(options.kind)) {
542
+ exitWithGuidance(`Invalid thread kind: ${options.kind}`, EXIT_CODES.USAGE_ERROR, `Valid kinds: ${validKinds.join(", ")}`, { field: "kind", value: options.kind });
543
+ }
544
+ // Build anchor if provided
545
+ let anchor;
546
+ if (options.path) {
547
+ anchor = {
548
+ type: "code",
549
+ path: options.path,
550
+ side: (options.side || "head"),
551
+ line_start: options.lineStart || 1,
552
+ line_end: options.lineEnd || options.lineStart || 1,
553
+ commit: options.commit || "",
554
+ };
555
+ }
556
+ else if (options.section || options.field || options.anchorRef) {
557
+ anchor = {
558
+ type: "structured",
559
+ ...(options.section ? { section: options.section } : {}),
560
+ ...(options.field ? { field: options.field } : {}),
561
+ ...(options.anchorRef ? { ref: options.anchorRef } : {}),
562
+ };
563
+ }
564
+ const threadId = ulid();
565
+ const entryId = ulid();
566
+ const now = new Date().toISOString();
567
+ const newThread = {
568
+ _ulid: threadId,
569
+ kind: options.kind,
570
+ ...(anchor ? { anchor } : {}),
571
+ entries: [
572
+ {
573
+ _ulid: entryId,
574
+ author,
575
+ body: options.body,
576
+ created_at: now,
577
+ },
578
+ ],
579
+ };
580
+ const updated = await mutateReviewAtomically(ctx, found, (latest) => ({
581
+ ...latest,
582
+ threads: [...latest.threads, newThread],
583
+ events: [
584
+ ...latest.events,
585
+ createEvent("thread_created", author, {
586
+ thread_ulid: threadId,
587
+ kind: options.kind,
588
+ }),
589
+ ],
590
+ updated_at: now,
591
+ }));
592
+ await commitIfShadow(ctx.shadow, "review-comment", found.slugs[0] || found._ulid.slice(0, 8));
593
+ output({ thread_ulid: threadId, review_ulid: found._ulid }, () => {
594
+ success(`Added ${options.kind} thread to review ${shortReviewRef(found, reviews)}`);
595
+ });
596
+ }
597
+ catch (err) {
598
+ error(errors.failures.addReviewComment, err);
599
+ process.exit(EXIT_CODES.ERROR);
600
+ }
601
+ }));
602
+ // --- review reply ---
603
+ // AC: @review-cli-mutation-commands ac-1b
604
+ markMutating(review
605
+ .command("reply <ref>")
606
+ .description("Reply to an existing review thread")
607
+ .requiredOption("--thread <ulid>", "Thread ULID to reply to")
608
+ .requiredOption("--body <body>", "Reply body")
609
+ .option("--author <author>", "Reply author")
610
+ .action(async (ref, options) => {
611
+ try {
612
+ const ctx = await initContext();
613
+ const reviews = await loadReviewRecords(ctx);
614
+ const found = resolveReviewRef(ref, reviews);
615
+ const author = options.author || getAuthor(ctx.config?.identity?.author) || "unknown";
616
+ const now = new Date().toISOString();
617
+ const threadRef = options.thread.startsWith("@")
618
+ ? options.thread.slice(1)
619
+ : options.thread;
620
+ const threadIndex = found.threads.findIndex((t) => t._ulid === threadRef ||
621
+ t._ulid.toLowerCase().startsWith(threadRef.toLowerCase()));
622
+ if (threadIndex === -1) {
623
+ exitWithGuidance(`Thread not found: ${options.thread}`, EXIT_CODES.NOT_FOUND, `Check threads with: kspec review get ${ref}`, { ref: options.thread, entity: "thread" });
624
+ }
625
+ const entryId = ulid();
626
+ const updated = await mutateReviewAtomically(ctx, found, (latest) => {
627
+ const threads = [...latest.threads];
628
+ threads[threadIndex] = {
629
+ ...threads[threadIndex],
630
+ entries: [
631
+ ...threads[threadIndex].entries,
632
+ {
633
+ _ulid: entryId,
634
+ author,
635
+ body: options.body,
636
+ created_at: now,
637
+ },
638
+ ],
639
+ };
640
+ return {
641
+ ...latest,
642
+ threads,
643
+ events: [
644
+ ...latest.events,
645
+ createEvent("thread_replied", author, {
646
+ thread_ulid: found.threads[threadIndex]._ulid,
647
+ }),
648
+ ],
649
+ updated_at: now,
650
+ };
651
+ });
652
+ await commitIfShadow(ctx.shadow, "review-reply", found.slugs[0] || found._ulid.slice(0, 8));
653
+ output({ thread_ulid: found.threads[threadIndex]._ulid, review_ulid: found._ulid }, () => {
654
+ success(`Replied to thread on review ${shortReviewRef(found, reviews)}`);
655
+ });
656
+ }
657
+ catch (err) {
658
+ error(errors.failures.replyToReviewThread, err);
659
+ process.exit(EXIT_CODES.ERROR);
660
+ }
661
+ }));
662
+ // --- review check ---
663
+ // AC: @review-cli-mutation-commands ac-2
664
+ markMutating(review
665
+ .command("check <ref>")
666
+ .description("Add a check result to a review")
667
+ .requiredOption("--name <name>", "Check name")
668
+ .requiredOption("--status <status>", "Check status: pass, fail, running, skipped")
669
+ .option("--required", "Mark check as required (default: true)")
670
+ .option("--no-required", "Mark check as not required")
671
+ .option("--runner <runner>", "Check runner identifier")
672
+ .option("--evidence <evidence>", "Evidence payload or link")
673
+ .option("--version-base <commit>", "Applies-to version: base commit (for code)")
674
+ .option("--version-head <commit>", "Applies-to version: head commit (for code)")
675
+ .option("--version-hash <hash>", "Applies-to version: content hash (for entities)")
676
+ .option("--author <author>", "Actor for the event")
677
+ .action(async (ref, options) => {
678
+ try {
679
+ const ctx = await initContext();
680
+ const reviews = await loadReviewRecords(ctx);
681
+ const found = resolveReviewRef(ref, reviews);
682
+ const author = options.author || getAuthor(ctx.config?.identity?.author) || "unknown";
683
+ const now = new Date().toISOString();
684
+ // Validate check status
685
+ // AC: @trait-error-guidance ac-5
686
+ const validStatuses = ["pass", "fail", "running", "skipped"];
687
+ if (!validStatuses.includes(options.status)) {
688
+ exitWithGuidance(`Invalid check status: ${options.status}`, EXIT_CODES.USAGE_ERROR, `Valid statuses: ${validStatuses.join(", ")}`, { field: "status", value: options.status });
689
+ }
690
+ const version = parseVersionFromOptions(options);
691
+ const newCheck = {
692
+ name: options.name,
693
+ status: options.status,
694
+ required: options.required !== false,
695
+ ...(options.runner ? { runner: options.runner } : {}),
696
+ ...(options.evidence ? { evidence: options.evidence } : {}),
697
+ applies_to_version: version,
698
+ created_at: now,
699
+ completed_at: options.status !== "running" ? now : null,
700
+ };
701
+ const updated = await mutateReviewAtomically(ctx, found, (latest) => ({
702
+ ...latest,
703
+ checks: [...latest.checks, newCheck],
704
+ events: [
705
+ ...latest.events,
706
+ createEvent("check_added", author, {
707
+ name: options.name,
708
+ status: options.status,
709
+ }),
710
+ ],
711
+ updated_at: now,
712
+ }));
713
+ await commitIfShadow(ctx.shadow, "review-check", found.slugs[0] || found._ulid.slice(0, 8), options.name);
714
+ output({ check_name: options.name, status: options.status, review_ulid: found._ulid }, () => {
715
+ success(`Added check "${options.name}" (${options.status}) to review ${shortReviewRef(found, reviews)}`);
716
+ });
717
+ }
718
+ catch (err) {
719
+ error(errors.failures.addReviewCheck, err);
720
+ process.exit(EXIT_CODES.ERROR);
721
+ }
722
+ }));
723
+ // --- review verdict ---
724
+ // AC: @review-cli-mutation-commands ac-3
725
+ markMutating(review
726
+ .command("verdict <ref>")
727
+ .description("Set a verdict on a review")
728
+ .requiredOption("--decision <decision>", "Verdict: approve, request_changes, comment")
729
+ .option("--reviewer <reviewer>", "Reviewer identity")
730
+ .option("--role <role>", "Reviewer role", "reviewer")
731
+ .option("--version-base <commit>", "Applies-to version: base commit (for code)")
732
+ .option("--version-head <commit>", "Applies-to version: head commit (for code)")
733
+ .option("--version-hash <hash>", "Applies-to version: content hash (for entities)")
734
+ .action(async (ref, options) => {
735
+ try {
736
+ const ctx = await initContext();
737
+ const reviews = await loadReviewRecords(ctx);
738
+ const found = resolveReviewRef(ref, reviews);
739
+ const reviewer = options.reviewer || getAuthor(ctx.config?.identity?.author) || "unknown";
740
+ const now = new Date().toISOString();
741
+ // Validate decision
742
+ // AC: @trait-error-guidance ac-5
743
+ const validDecisions = ["approve", "request_changes", "comment"];
744
+ if (!validDecisions.includes(options.decision)) {
745
+ exitWithGuidance(`Invalid verdict decision: ${options.decision}`, EXIT_CODES.USAGE_ERROR, `Valid decisions: ${validDecisions.join(", ")}`, { field: "decision", value: options.decision });
746
+ }
747
+ const version = parseVersionFromOptions(options);
748
+ const newVerdict = {
749
+ reviewer,
750
+ role: options.role,
751
+ decision: options.decision,
752
+ applies_to_version: version,
753
+ created_at: now,
754
+ };
755
+ // AC: @review-record-per-cycle-lifecycle ac-1 — auto-close on approve/request_changes
756
+ const shouldAutoClose = options.decision === "approve" || options.decision === "request_changes";
757
+ const updated = await mutateReviewAtomically(ctx, found, (latest) => ({
758
+ ...latest,
759
+ verdicts: [...latest.verdicts, newVerdict],
760
+ ...(shouldAutoClose && { lifecycle_state: "closed" }),
761
+ events: [
762
+ ...latest.events,
763
+ createEvent("verdict_submitted", reviewer, {
764
+ decision: options.decision,
765
+ }),
766
+ ...(shouldAutoClose
767
+ ? [
768
+ createEvent("lifecycle_change", reviewer, {
769
+ from: latest.lifecycle_state,
770
+ to: "closed",
771
+ }),
772
+ ]
773
+ : []),
774
+ ],
775
+ updated_at: now,
776
+ }));
777
+ await commitIfShadow(ctx.shadow, "review-verdict", found.slugs[0] || found._ulid.slice(0, 8), `${options.decision}${shouldAutoClose ? " (auto-closed)" : ""}`);
778
+ // AC: @review-task-lifecycle-integration ac-4
779
+ // Auto-transition tasks to needs_work on changes_requested verdict
780
+ const allTasks = await loadAllTasks(ctx);
781
+ const transitioned = await handleVerdictTaskTransition(ctx, found, options.decision, allTasks, reviewer);
782
+ if (transitioned.some((t) => t.transitioned)) {
783
+ await commitIfShadow(ctx.shadow, "review-verdict-task-transition", found.slugs[0] || found._ulid.slice(0, 8), `tasks transitioned to needs_work`);
784
+ }
785
+ output({ decision: options.decision, reviewer, review_ulid: found._ulid, lifecycle_state: updated.lifecycle_state }, () => {
786
+ success(`Recorded verdict "${options.decision}" by ${reviewer} on review ${shortReviewRef(found, reviews)}`);
787
+ if (shouldAutoClose) {
788
+ info(`Review auto-closed`);
789
+ }
790
+ for (const t of transitioned.filter((t) => t.transitioned)) {
791
+ info(`Task @${t.slug || t.ulid} transitioned to needs_work`);
792
+ }
793
+ });
794
+ }
795
+ catch (err) {
796
+ error(errors.failures.setReviewVerdict, err);
797
+ process.exit(EXIT_CODES.ERROR);
798
+ }
799
+ }));
800
+ // --- review resolve ---
801
+ // AC: @review-cli-mutation-commands ac-4
802
+ markMutating(review
803
+ .command("resolve <ref>")
804
+ .description("Resolve a review thread")
805
+ .requiredOption("--thread <ulid>", "Thread ULID to resolve")
806
+ .option("--author <author>", "Actor")
807
+ .action(async (ref, options) => {
808
+ try {
809
+ const ctx = await initContext();
810
+ const reviews = await loadReviewRecords(ctx);
811
+ const found = resolveReviewRef(ref, reviews);
812
+ const author = options.author || getAuthor(ctx.config?.identity?.author) || "unknown";
813
+ const now = new Date().toISOString();
814
+ const threadRef = options.thread.startsWith("@")
815
+ ? options.thread.slice(1)
816
+ : options.thread;
817
+ const threadIndex = found.threads.findIndex((t) => t._ulid === threadRef ||
818
+ t._ulid.toLowerCase().startsWith(threadRef.toLowerCase()));
819
+ if (threadIndex === -1) {
820
+ exitWithGuidance(`Thread not found: ${options.thread}`, EXIT_CODES.NOT_FOUND, `Check threads with: kspec review get ${ref}`, { ref: options.thread, entity: "thread" });
821
+ }
822
+ if (found.threads[threadIndex].resolved_at) {
823
+ exitWithGuidance(`Thread is already resolved`, EXIT_CODES.USAGE_ERROR, `Use kspec review reopen ${ref} --thread ${options.thread} to reopen`, { current_state: "resolved", valid_next: ["reopen"] });
824
+ }
825
+ const updated = await mutateReviewAtomically(ctx, found, (latest) => {
826
+ const threads = [...latest.threads];
827
+ threads[threadIndex] = {
828
+ ...threads[threadIndex],
829
+ resolved_at: now,
830
+ resolved_by: author,
831
+ };
832
+ return {
833
+ ...latest,
834
+ threads,
835
+ events: [
836
+ ...latest.events,
837
+ createEvent("thread_resolved", author, {
838
+ thread_ulid: found.threads[threadIndex]._ulid,
839
+ }),
840
+ ],
841
+ updated_at: now,
842
+ };
843
+ });
844
+ await commitIfShadow(ctx.shadow, "review-resolve", found.slugs[0] || found._ulid.slice(0, 8));
845
+ output({ thread_ulid: found.threads[threadIndex]._ulid, review_ulid: found._ulid }, () => {
846
+ success(`Resolved thread on review ${shortReviewRef(found, reviews)}`);
847
+ });
848
+ }
849
+ catch (err) {
850
+ error(errors.failures.resolveReviewThread, err);
851
+ process.exit(EXIT_CODES.ERROR);
852
+ }
853
+ }));
854
+ // --- review reopen ---
855
+ // AC: @review-cli-mutation-commands ac-4
856
+ markMutating(review
857
+ .command("reopen <ref>")
858
+ .description("Reopen a resolved review thread")
859
+ .requiredOption("--thread <ulid>", "Thread ULID to reopen")
860
+ .option("--author <author>", "Actor")
861
+ .action(async (ref, options) => {
862
+ try {
863
+ const ctx = await initContext();
864
+ const reviews = await loadReviewRecords(ctx);
865
+ const found = resolveReviewRef(ref, reviews);
866
+ const author = options.author || getAuthor(ctx.config?.identity?.author) || "unknown";
867
+ const now = new Date().toISOString();
868
+ const threadRef = options.thread.startsWith("@")
869
+ ? options.thread.slice(1)
870
+ : options.thread;
871
+ const threadIndex = found.threads.findIndex((t) => t._ulid === threadRef ||
872
+ t._ulid.toLowerCase().startsWith(threadRef.toLowerCase()));
873
+ if (threadIndex === -1) {
874
+ exitWithGuidance(`Thread not found: ${options.thread}`, EXIT_CODES.NOT_FOUND, `Check threads with: kspec review get ${ref}`, { ref: options.thread, entity: "thread" });
875
+ }
876
+ if (!found.threads[threadIndex].resolved_at) {
877
+ exitWithGuidance(`Thread is not resolved`, EXIT_CODES.USAGE_ERROR, `Use kspec review resolve ${ref} --thread ${options.thread} to resolve`, { current_state: "unresolved", valid_next: ["resolve"] });
878
+ }
879
+ const updated = await mutateReviewAtomically(ctx, found, (latest) => {
880
+ const threads = [...latest.threads];
881
+ threads[threadIndex] = {
882
+ ...threads[threadIndex],
883
+ resolved_at: null,
884
+ resolved_by: null,
885
+ };
886
+ return {
887
+ ...latest,
888
+ threads,
889
+ events: [
890
+ ...latest.events,
891
+ createEvent("thread_reopened", author, {
892
+ thread_ulid: found.threads[threadIndex]._ulid,
893
+ }),
894
+ ],
895
+ updated_at: now,
896
+ };
897
+ });
898
+ await commitIfShadow(ctx.shadow, "review-reopen", found.slugs[0] || found._ulid.slice(0, 8));
899
+ output({ thread_ulid: found.threads[threadIndex]._ulid, review_ulid: found._ulid }, () => {
900
+ success(`Reopened thread on review ${shortReviewRef(found, reviews)}`);
901
+ });
902
+ }
903
+ catch (err) {
904
+ error(errors.failures.reopenReviewThread, err);
905
+ process.exit(EXIT_CODES.ERROR);
906
+ }
907
+ }));
908
+ // --- review open ---
909
+ // AC: @review-cli-mutation-commands ac-5
910
+ markMutating(review
911
+ .command("open <ref>")
912
+ .description("Open a review (transition from draft to open)")
913
+ .option("--author <author>", "Actor")
914
+ .action(async (ref, options) => {
915
+ try {
916
+ const ctx = await initContext();
917
+ const reviews = await loadReviewRecords(ctx);
918
+ const found = resolveReviewRef(ref, reviews);
919
+ const author = options.author || getAuthor(ctx.config?.identity?.author) || "unknown";
920
+ const now = new Date().toISOString();
921
+ // AC: @trait-error-guidance ac-4
922
+ if (found.lifecycle_state !== "draft" && found.lifecycle_state !== "closed") {
923
+ exitWithGuidance(`Cannot open review: current state is ${found.lifecycle_state}`, EXIT_CODES.VALIDATION_FAILED, `Review can only be opened from draft or closed state`, {
924
+ current_state: found.lifecycle_state,
925
+ valid_from: ["draft", "closed"],
926
+ });
927
+ }
928
+ const updated = await mutateReviewAtomically(ctx, found, (latest) => ({
929
+ ...latest,
930
+ lifecycle_state: "open",
931
+ events: [
932
+ ...latest.events,
933
+ createEvent("lifecycle_change", author, {
934
+ from: latest.lifecycle_state,
935
+ to: "open",
936
+ }),
937
+ ],
938
+ updated_at: now,
939
+ }));
940
+ await commitIfShadow(ctx.shadow, "review-open", found.slugs[0] || found._ulid.slice(0, 8));
941
+ output({ lifecycle_state: "open", review_ulid: found._ulid }, () => {
942
+ success(`Opened review ${shortReviewRef(found, reviews)}`);
943
+ });
944
+ }
945
+ catch (err) {
946
+ error(errors.failures.openReview, err);
947
+ process.exit(EXIT_CODES.ERROR);
948
+ }
949
+ }));
950
+ // --- review close ---
951
+ // AC: @review-cli-mutation-commands ac-5
952
+ markMutating(review
953
+ .command("close <ref>")
954
+ .description("Close a review")
955
+ .option("--author <author>", "Actor")
956
+ .action(async (ref, options) => {
957
+ try {
958
+ const ctx = await initContext();
959
+ const reviews = await loadReviewRecords(ctx);
960
+ const found = resolveReviewRef(ref, reviews);
961
+ const author = options.author || getAuthor(ctx.config?.identity?.author) || "unknown";
962
+ const now = new Date().toISOString();
963
+ // AC: @trait-error-guidance ac-4
964
+ if (found.lifecycle_state === "closed") {
965
+ exitWithGuidance(`Review is already closed`, EXIT_CODES.VALIDATION_FAILED, `Current state: closed`, { current_state: "closed", valid_next: ["open", "archive"] });
966
+ }
967
+ if (found.lifecycle_state === "archived") {
968
+ exitWithGuidance(`Cannot close review: current state is archived`, EXIT_CODES.VALIDATION_FAILED, `Archived reviews cannot be modified`, { current_state: "archived", valid_next: [] });
969
+ }
970
+ const updated = await mutateReviewAtomically(ctx, found, (latest) => ({
971
+ ...latest,
972
+ lifecycle_state: "closed",
973
+ events: [
974
+ ...latest.events,
975
+ createEvent("lifecycle_change", author, {
976
+ from: latest.lifecycle_state,
977
+ to: "closed",
978
+ }),
979
+ ],
980
+ updated_at: now,
981
+ }));
982
+ await commitIfShadow(ctx.shadow, "review-close", found.slugs[0] || found._ulid.slice(0, 8));
983
+ output({ lifecycle_state: "closed", review_ulid: found._ulid }, () => {
984
+ success(`Closed review ${shortReviewRef(found, reviews)}`);
985
+ });
986
+ }
987
+ catch (err) {
988
+ error(errors.failures.closeReview, err);
989
+ process.exit(EXIT_CODES.ERROR);
990
+ }
991
+ }));
992
+ // --- review archive ---
993
+ // AC: @review-cli-mutation-commands ac-5
994
+ // AC: @review-cli-mutation-commands ac-7 — No delete command exists; destructive operations are
995
+ // deferred to future work and will require explicit safety behavior (--force / confirmation)
996
+ // separate from close/archive lifecycle transitions.
997
+ markMutating(review
998
+ .command("archive <ref>")
999
+ .description("Archive a review (permanent)")
1000
+ .option("--author <author>", "Actor")
1001
+ .action(async (ref, options) => {
1002
+ try {
1003
+ const ctx = await initContext();
1004
+ const reviews = await loadReviewRecords(ctx);
1005
+ const found = resolveReviewRef(ref, reviews);
1006
+ const author = options.author || getAuthor(ctx.config?.identity?.author) || "unknown";
1007
+ const now = new Date().toISOString();
1008
+ // AC: @trait-error-guidance ac-4
1009
+ if (found.lifecycle_state === "archived") {
1010
+ exitWithGuidance(`Review is already archived`, EXIT_CODES.VALIDATION_FAILED, `Current state: archived`, { current_state: "archived", valid_next: [] });
1011
+ }
1012
+ const updated = await mutateReviewAtomically(ctx, found, (latest) => ({
1013
+ ...latest,
1014
+ lifecycle_state: "archived",
1015
+ events: [
1016
+ ...latest.events,
1017
+ createEvent("lifecycle_change", author, {
1018
+ from: latest.lifecycle_state,
1019
+ to: "archived",
1020
+ }),
1021
+ ],
1022
+ updated_at: now,
1023
+ }));
1024
+ await commitIfShadow(ctx.shadow, "review-archive", found.slugs[0] || found._ulid.slice(0, 8));
1025
+ output({ lifecycle_state: "archived", review_ulid: found._ulid }, () => {
1026
+ success(`Archived review ${shortReviewRef(found, reviews)}`);
1027
+ });
1028
+ }
1029
+ catch (err) {
1030
+ error(errors.failures.archiveReview, err);
1031
+ process.exit(EXIT_CODES.ERROR);
1032
+ }
1033
+ }));
1034
+ // --- review refresh ---
1035
+ // AC: @review-cli-mutation-commands ac-6
1036
+ markMutating(review
1037
+ .command("refresh <ref>")
1038
+ .description("Update subject compare context after new commits")
1039
+ .requiredOption("--head <commit>", "New head commit")
1040
+ .option("--base <commit>", "New base commit (if changed)")
1041
+ .option("--author <author>", "Actor")
1042
+ .action(async (ref, options) => {
1043
+ try {
1044
+ const ctx = await initContext();
1045
+ const reviews = await loadReviewRecords(ctx);
1046
+ const found = resolveReviewRef(ref, reviews);
1047
+ const author = options.author || getAuthor(ctx.config?.identity?.author) || "unknown";
1048
+ const now = new Date().toISOString();
1049
+ if (found.subject.type !== "code") {
1050
+ exitWithGuidance(`Refresh is only supported for code subjects (current: ${found.subject.type})`, EXIT_CODES.USAGE_ERROR, "Only code reviews support refresh with --head/--base", { field: "subject.type", value: found.subject.type });
1051
+ }
1052
+ const updated = await mutateReviewAtomically(ctx, found, (latest) => {
1053
+ const subject = { ...latest.subject };
1054
+ const previousHead = subject.head_commit;
1055
+ subject.head_commit = options.head;
1056
+ if (options.base) {
1057
+ subject.base_commit = options.base;
1058
+ }
1059
+ return {
1060
+ ...latest,
1061
+ subject: subject,
1062
+ events: [
1063
+ ...latest.events,
1064
+ createEvent("subject_refreshed", author, {
1065
+ previous_head: previousHead,
1066
+ new_head: options.head,
1067
+ ...(options.base ? { new_base: options.base } : {}),
1068
+ }),
1069
+ ],
1070
+ updated_at: now,
1071
+ };
1072
+ });
1073
+ await commitIfShadow(ctx.shadow, "review-refresh", found.slugs[0] || found._ulid.slice(0, 8));
1074
+ output({ review_ulid: found._ulid, new_head: options.head }, () => {
1075
+ success(`Refreshed subject on review ${shortReviewRef(found, reviews)}`);
1076
+ });
1077
+ }
1078
+ catch (err) {
1079
+ error(errors.failures.refreshReview, err);
1080
+ process.exit(EXIT_CODES.ERROR);
1081
+ }
1082
+ }));
1083
+ // --- review for-task ---
1084
+ // AC: @review-cli-task-linkage ac-1, ac-2
1085
+ review
1086
+ .command("for-task <ref>")
1087
+ .description("Find reviews linked to a task")
1088
+ .action(async (ref) => {
1089
+ try {
1090
+ const ctx = await initContext();
1091
+ const reviews = await loadReviewRecords(ctx);
1092
+ const tasks = await loadAllTasks(ctx);
1093
+ const cleanRef = ref.startsWith("@") ? ref : `@${ref}`;
1094
+ const cleanRefNoAt = cleanRef.startsWith("@") ? cleanRef.slice(1) : cleanRef;
1095
+ // Find reviews by related_refs or subject ref
1096
+ const matches = reviews.filter((r) => r.related_refs.includes(cleanRef) ||
1097
+ (r.subject.type === "task" && r.subject.ref === cleanRef));
1098
+ // AC: @review-cli-task-linkage ac-2 — also resolve via task's review_ref field
1099
+ const task = tasks.find((t) => t._ulid === cleanRefNoAt ||
1100
+ t._ulid.toLowerCase().startsWith(cleanRefNoAt.toLowerCase()) ||
1101
+ t.slugs.includes(cleanRefNoAt));
1102
+ if (task?.review_ref) {
1103
+ const linkedReview = findReviewByRef(reviews, task.review_ref);
1104
+ if (linkedReview && !matches.some((m) => m._ulid === linkedReview._ulid)) {
1105
+ matches.push(linkedReview);
1106
+ }
1107
+ }
1108
+ if (matches.length === 0) {
1109
+ output({ reviews: [], total: 0, task_ref: cleanRef }, () => {
1110
+ info(`No reviews found for task ${cleanRef}`);
1111
+ });
1112
+ return;
1113
+ }
1114
+ const outputData = {
1115
+ reviews: matches.map((r) => ({
1116
+ _ulid: r._ulid,
1117
+ ref: `@${r.slugs[0] || r._ulid}`,
1118
+ title: r.title,
1119
+ lifecycle_state: r.lifecycle_state,
1120
+ disposition: computeDisposition(r),
1121
+ gate_state: computeGateState(r),
1122
+ created_at: r.created_at,
1123
+ })),
1124
+ total: matches.length,
1125
+ task_ref: cleanRef,
1126
+ };
1127
+ output(outputData, () => {
1128
+ console.log(`Reviews for ${cleanRef} (${matches.length}):`);
1129
+ for (const r of matches) {
1130
+ const shortRef = shortReviewRef(r, reviews);
1131
+ const disposition = computeDisposition(r);
1132
+ console.log(` ${shortRef} ${r.lifecycle_state}/${disposition} ${r.title}`);
1133
+ }
1134
+ });
1135
+ }
1136
+ catch (err) {
1137
+ error(errors.failures.findReviewsForTask, err);
1138
+ process.exit(EXIT_CODES.ERROR);
1139
+ }
1140
+ });
1141
+ }
1142
+ //# sourceMappingURL=review.js.map