@kynetic-ai/spec 0.1.2 → 0.3.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 (510) hide show
  1. package/README.md +250 -17
  2. package/dist/acp/client.d.ts +18 -4
  3. package/dist/acp/client.d.ts.map +1 -1
  4. package/dist/acp/client.js +44 -26
  5. package/dist/acp/client.js.map +1 -1
  6. package/dist/acp/framing.d.ts +2 -2
  7. package/dist/acp/framing.d.ts.map +1 -1
  8. package/dist/acp/framing.js +37 -29
  9. package/dist/acp/framing.js.map +1 -1
  10. package/dist/acp/index.d.ts +6 -7
  11. package/dist/acp/index.d.ts.map +1 -1
  12. package/dist/acp/index.js +3 -3
  13. package/dist/acp/index.js.map +1 -1
  14. package/dist/acp/types.d.ts +5 -5
  15. package/dist/acp/types.d.ts.map +1 -1
  16. package/dist/acp/types.js +18 -18
  17. package/dist/acp/types.js.map +1 -1
  18. package/dist/agents/adapters.d.ts.map +1 -1
  19. package/dist/agents/adapters.js +24 -13
  20. package/dist/agents/adapters.js.map +1 -1
  21. package/dist/agents/index.d.ts +2 -2
  22. package/dist/agents/index.js +2 -2
  23. package/dist/agents/spawner.d.ts +4 -4
  24. package/dist/agents/spawner.d.ts.map +1 -1
  25. package/dist/agents/spawner.js +6 -6
  26. package/dist/agents/spawner.js.map +1 -1
  27. package/dist/cli/batch-context.d.ts +43 -0
  28. package/dist/cli/batch-context.d.ts.map +1 -0
  29. package/dist/cli/batch-context.js +93 -0
  30. package/dist/cli/batch-context.js.map +1 -0
  31. package/dist/cli/batch-exec.d.ts +116 -0
  32. package/dist/cli/batch-exec.d.ts.map +1 -0
  33. package/dist/cli/batch-exec.js +694 -0
  34. package/dist/cli/batch-exec.js.map +1 -0
  35. package/dist/cli/batch.d.ts +4 -2
  36. package/dist/cli/batch.d.ts.map +1 -1
  37. package/dist/cli/batch.js +15 -14
  38. package/dist/cli/batch.js.map +1 -1
  39. package/dist/cli/command-annotations.d.ts +23 -0
  40. package/dist/cli/command-annotations.d.ts.map +1 -0
  41. package/dist/cli/command-annotations.js +27 -0
  42. package/dist/cli/command-annotations.js.map +1 -0
  43. package/dist/cli/commands/agents.d.ts +46 -0
  44. package/dist/cli/commands/agents.d.ts.map +1 -0
  45. package/dist/cli/commands/agents.js +377 -0
  46. package/dist/cli/commands/agents.js.map +1 -0
  47. package/dist/cli/commands/batch.d.ts +20 -0
  48. package/dist/cli/commands/batch.d.ts.map +1 -0
  49. package/dist/cli/commands/batch.js +214 -0
  50. package/dist/cli/commands/batch.js.map +1 -0
  51. package/dist/cli/commands/clone-for-testing.d.ts +1 -1
  52. package/dist/cli/commands/clone-for-testing.d.ts.map +1 -1
  53. package/dist/cli/commands/clone-for-testing.js +37 -47
  54. package/dist/cli/commands/clone-for-testing.js.map +1 -1
  55. package/dist/cli/commands/derive.d.ts +1 -1
  56. package/dist/cli/commands/derive.d.ts.map +1 -1
  57. package/dist/cli/commands/derive.js +140 -88
  58. package/dist/cli/commands/derive.js.map +1 -1
  59. package/dist/cli/commands/doctor.d.ts +11 -0
  60. package/dist/cli/commands/doctor.d.ts.map +1 -0
  61. package/dist/cli/commands/doctor.js +152 -0
  62. package/dist/cli/commands/doctor.js.map +1 -0
  63. package/dist/cli/commands/export.d.ts +12 -0
  64. package/dist/cli/commands/export.d.ts.map +1 -0
  65. package/dist/cli/commands/export.js +134 -0
  66. package/dist/cli/commands/export.js.map +1 -0
  67. package/dist/cli/commands/help.d.ts +1 -1
  68. package/dist/cli/commands/help.d.ts.map +1 -1
  69. package/dist/cli/commands/help.js +163 -37
  70. package/dist/cli/commands/help.js.map +1 -1
  71. package/dist/cli/commands/inbox.d.ts +1 -1
  72. package/dist/cli/commands/inbox.d.ts.map +1 -1
  73. package/dist/cli/commands/inbox.js +178 -56
  74. package/dist/cli/commands/inbox.js.map +1 -1
  75. package/dist/cli/commands/index.d.ts +31 -19
  76. package/dist/cli/commands/index.d.ts.map +1 -1
  77. package/dist/cli/commands/index.js +31 -19
  78. package/dist/cli/commands/index.js.map +1 -1
  79. package/dist/cli/commands/init.d.ts +5 -1
  80. package/dist/cli/commands/init.d.ts.map +1 -1
  81. package/dist/cli/commands/init.js +108 -57
  82. package/dist/cli/commands/init.js.map +1 -1
  83. package/dist/cli/commands/item.d.ts +1 -1
  84. package/dist/cli/commands/item.d.ts.map +1 -1
  85. package/dist/cli/commands/item.js +557 -274
  86. package/dist/cli/commands/item.js.map +1 -1
  87. package/dist/cli/commands/link.d.ts +1 -1
  88. package/dist/cli/commands/link.d.ts.map +1 -1
  89. package/dist/cli/commands/link.js +55 -46
  90. package/dist/cli/commands/link.js.map +1 -1
  91. package/dist/cli/commands/log.d.ts +1 -1
  92. package/dist/cli/commands/log.d.ts.map +1 -1
  93. package/dist/cli/commands/log.js +57 -51
  94. package/dist/cli/commands/log.js.map +1 -1
  95. package/dist/cli/commands/merge-driver.d.ts +19 -0
  96. package/dist/cli/commands/merge-driver.d.ts.map +1 -0
  97. package/dist/cli/commands/merge-driver.js +398 -0
  98. package/dist/cli/commands/merge-driver.js.map +1 -0
  99. package/dist/cli/commands/meta.d.ts +1 -1
  100. package/dist/cli/commands/meta.d.ts.map +1 -1
  101. package/dist/cli/commands/meta.js +533 -399
  102. package/dist/cli/commands/meta.js.map +1 -1
  103. package/dist/cli/commands/module.d.ts +1 -1
  104. package/dist/cli/commands/module.d.ts.map +1 -1
  105. package/dist/cli/commands/module.js +30 -25
  106. package/dist/cli/commands/module.js.map +1 -1
  107. package/dist/cli/commands/plan-import.d.ts +11 -0
  108. package/dist/cli/commands/plan-import.d.ts.map +1 -0
  109. package/dist/cli/commands/plan-import.js +516 -0
  110. package/dist/cli/commands/plan-import.js.map +1 -0
  111. package/dist/cli/commands/plan.d.ts +10 -0
  112. package/dist/cli/commands/plan.d.ts.map +1 -0
  113. package/dist/cli/commands/plan.js +421 -0
  114. package/dist/cli/commands/plan.js.map +1 -0
  115. package/dist/cli/commands/ralph.d.ts +1 -1
  116. package/dist/cli/commands/ralph.d.ts.map +1 -1
  117. package/dist/cli/commands/ralph.js +1097 -169
  118. package/dist/cli/commands/ralph.js.map +1 -1
  119. package/dist/cli/commands/refs.d.ts +13 -0
  120. package/dist/cli/commands/refs.d.ts.map +1 -0
  121. package/dist/cli/commands/refs.js +283 -0
  122. package/dist/cli/commands/refs.js.map +1 -0
  123. package/dist/cli/commands/search.d.ts +1 -1
  124. package/dist/cli/commands/search.d.ts.map +1 -1
  125. package/dist/cli/commands/search.js +199 -37
  126. package/dist/cli/commands/search.js.map +1 -1
  127. package/dist/cli/commands/serve.d.ts +10 -0
  128. package/dist/cli/commands/serve.d.ts.map +1 -0
  129. package/dist/cli/commands/serve.js +491 -0
  130. package/dist/cli/commands/serve.js.map +1 -0
  131. package/dist/cli/commands/session.d.ts +25 -6
  132. package/dist/cli/commands/session.d.ts.map +1 -1
  133. package/dist/cli/commands/session.js +811 -127
  134. package/dist/cli/commands/session.js.map +1 -1
  135. package/dist/cli/commands/setup-seeding.d.ts +81 -0
  136. package/dist/cli/commands/setup-seeding.d.ts.map +1 -0
  137. package/dist/cli/commands/setup-seeding.js +292 -0
  138. package/dist/cli/commands/setup-seeding.js.map +1 -0
  139. package/dist/cli/commands/setup.d.ts +77 -3
  140. package/dist/cli/commands/setup.d.ts.map +1 -1
  141. package/dist/cli/commands/setup.js +1233 -274
  142. package/dist/cli/commands/setup.js.map +1 -1
  143. package/dist/cli/commands/shadow.d.ts +1 -1
  144. package/dist/cli/commands/shadow.d.ts.map +1 -1
  145. package/dist/cli/commands/shadow.js +70 -50
  146. package/dist/cli/commands/shadow.js.map +1 -1
  147. package/dist/cli/commands/skill-crud.d.ts +58 -0
  148. package/dist/cli/commands/skill-crud.d.ts.map +1 -0
  149. package/dist/cli/commands/skill-crud.js +753 -0
  150. package/dist/cli/commands/skill-crud.js.map +1 -0
  151. package/dist/cli/commands/skill-diff.d.ts +27 -0
  152. package/dist/cli/commands/skill-diff.d.ts.map +1 -0
  153. package/dist/cli/commands/skill-diff.js +840 -0
  154. package/dist/cli/commands/skill-diff.js.map +1 -0
  155. package/dist/cli/commands/skill-install.d.ts +53 -0
  156. package/dist/cli/commands/skill-install.d.ts.map +1 -0
  157. package/dist/cli/commands/skill-install.js +452 -0
  158. package/dist/cli/commands/skill-install.js.map +1 -0
  159. package/dist/cli/commands/skill.d.ts +20 -0
  160. package/dist/cli/commands/skill.d.ts.map +1 -0
  161. package/dist/cli/commands/skill.js +36 -0
  162. package/dist/cli/commands/skill.js.map +1 -0
  163. package/dist/cli/commands/task.d.ts +1 -1
  164. package/dist/cli/commands/task.d.ts.map +1 -1
  165. package/dist/cli/commands/task.js +569 -346
  166. package/dist/cli/commands/task.js.map +1 -1
  167. package/dist/cli/commands/tasks.d.ts +26 -1
  168. package/dist/cli/commands/tasks.d.ts.map +1 -1
  169. package/dist/cli/commands/tasks.js +227 -122
  170. package/dist/cli/commands/tasks.js.map +1 -1
  171. package/dist/cli/commands/trait.d.ts +1 -1
  172. package/dist/cli/commands/trait.d.ts.map +1 -1
  173. package/dist/cli/commands/trait.js +166 -101
  174. package/dist/cli/commands/trait.js.map +1 -1
  175. package/dist/cli/commands/triage.d.ts +7 -0
  176. package/dist/cli/commands/triage.d.ts.map +1 -0
  177. package/dist/cli/commands/triage.js +569 -0
  178. package/dist/cli/commands/triage.js.map +1 -0
  179. package/dist/cli/commands/util.d.ts +7 -0
  180. package/dist/cli/commands/util.d.ts.map +1 -0
  181. package/dist/cli/commands/util.js +30 -0
  182. package/dist/cli/commands/util.js.map +1 -0
  183. package/dist/cli/commands/validate.d.ts +1 -1
  184. package/dist/cli/commands/validate.d.ts.map +1 -1
  185. package/dist/cli/commands/validate.js +264 -83
  186. package/dist/cli/commands/validate.js.map +1 -1
  187. package/dist/cli/commands/workflow.d.ts +16 -0
  188. package/dist/cli/commands/workflow.d.ts.map +1 -0
  189. package/dist/cli/commands/workflow.js +851 -0
  190. package/dist/cli/commands/workflow.js.map +1 -0
  191. package/dist/cli/exit-codes.d.ts +7 -0
  192. package/dist/cli/exit-codes.d.ts.map +1 -1
  193. package/dist/cli/exit-codes.js +26 -18
  194. package/dist/cli/exit-codes.js.map +1 -1
  195. package/dist/cli/help/content.d.ts.map +1 -1
  196. package/dist/cli/help/content.js +86 -71
  197. package/dist/cli/help/content.js.map +1 -1
  198. package/dist/cli/index.d.ts +1 -1
  199. package/dist/cli/index.d.ts.map +1 -1
  200. package/dist/cli/index.js +131 -19
  201. package/dist/cli/index.js.map +1 -1
  202. package/dist/cli/introspection.d.ts +6 -2
  203. package/dist/cli/introspection.d.ts.map +1 -1
  204. package/dist/cli/introspection.js +11 -8
  205. package/dist/cli/introspection.js.map +1 -1
  206. package/dist/cli/output.d.ts +64 -4
  207. package/dist/cli/output.d.ts.map +1 -1
  208. package/dist/cli/output.js +235 -85
  209. package/dist/cli/output.js.map +1 -1
  210. package/dist/cli/parse-utils.d.ts +21 -0
  211. package/dist/cli/parse-utils.d.ts.map +1 -0
  212. package/dist/cli/parse-utils.js +32 -0
  213. package/dist/cli/parse-utils.js.map +1 -0
  214. package/dist/cli/pid-utils.d.ts +72 -0
  215. package/dist/cli/pid-utils.d.ts.map +1 -0
  216. package/dist/cli/pid-utils.js +174 -0
  217. package/dist/cli/pid-utils.js.map +1 -0
  218. package/dist/cli/suggest.d.ts.map +1 -1
  219. package/dist/cli/suggest.js +1 -2
  220. package/dist/cli/suggest.js.map +1 -1
  221. package/dist/cli/validators.d.ts +43 -0
  222. package/dist/cli/validators.d.ts.map +1 -0
  223. package/dist/cli/validators.js +84 -0
  224. package/dist/cli/validators.js.map +1 -0
  225. package/dist/daemon/index.ts +52 -0
  226. package/dist/daemon/middleware/project-context.ts +126 -0
  227. package/dist/daemon/pid.ts +179 -0
  228. package/dist/daemon/project-context.ts +343 -0
  229. package/dist/daemon/routes/inbox.ts +164 -0
  230. package/dist/daemon/routes/items.ts +322 -0
  231. package/dist/daemon/routes/meta.ts +118 -0
  232. package/dist/daemon/routes/projects.ts +162 -0
  233. package/dist/daemon/routes/tasks.ts +327 -0
  234. package/dist/daemon/routes/triage.ts +468 -0
  235. package/dist/daemon/routes/validation.ts +248 -0
  236. package/dist/daemon/server.ts +408 -0
  237. package/dist/daemon/watcher.ts +195 -0
  238. package/dist/daemon/websocket/handler.ts +138 -0
  239. package/dist/daemon/websocket/heartbeat.ts +71 -0
  240. package/dist/daemon/websocket/pubsub.ts +125 -0
  241. package/dist/daemon/websocket/types.ts +66 -0
  242. package/dist/export/html.d.ts +19 -0
  243. package/dist/export/html.d.ts.map +1 -0
  244. package/dist/export/html.js +239 -0
  245. package/dist/export/html.js.map +1 -0
  246. package/dist/export/index.d.ts +10 -0
  247. package/dist/export/index.d.ts.map +1 -0
  248. package/dist/export/index.js +10 -0
  249. package/dist/export/index.js.map +1 -0
  250. package/dist/export/json.d.ts +24 -0
  251. package/dist/export/json.d.ts.map +1 -0
  252. package/dist/export/json.js +198 -0
  253. package/dist/export/json.js.map +1 -0
  254. package/dist/export/triage.d.ts +51 -0
  255. package/dist/export/triage.d.ts.map +1 -0
  256. package/dist/export/triage.js +83 -0
  257. package/dist/export/triage.js.map +1 -0
  258. package/dist/export/types.d.ts +122 -0
  259. package/dist/export/types.d.ts.map +1 -0
  260. package/dist/export/types.js +9 -0
  261. package/dist/export/types.js.map +1 -0
  262. package/dist/index.d.ts +2 -2
  263. package/dist/index.js +2 -2
  264. package/dist/lib/claude-plugin-registry.d.ts +66 -0
  265. package/dist/lib/claude-plugin-registry.d.ts.map +1 -0
  266. package/dist/lib/claude-plugin-registry.js +318 -0
  267. package/dist/lib/claude-plugin-registry.js.map +1 -0
  268. package/dist/merge/arrays.d.ts +87 -0
  269. package/dist/merge/arrays.d.ts.map +1 -0
  270. package/dist/merge/arrays.js +164 -0
  271. package/dist/merge/arrays.js.map +1 -0
  272. package/dist/merge/file-type.d.ts +32 -0
  273. package/dist/merge/file-type.d.ts.map +1 -0
  274. package/dist/merge/file-type.js +70 -0
  275. package/dist/merge/file-type.js.map +1 -0
  276. package/dist/merge/index.d.ts +14 -0
  277. package/dist/merge/index.d.ts.map +1 -0
  278. package/dist/merge/index.js +11 -0
  279. package/dist/merge/index.js.map +1 -0
  280. package/dist/merge/objects.d.ts +46 -0
  281. package/dist/merge/objects.d.ts.map +1 -0
  282. package/dist/merge/objects.js +193 -0
  283. package/dist/merge/objects.js.map +1 -0
  284. package/dist/merge/parse.d.ts +23 -0
  285. package/dist/merge/parse.d.ts.map +1 -0
  286. package/dist/merge/parse.js +78 -0
  287. package/dist/merge/parse.js.map +1 -0
  288. package/dist/merge/resolve.d.ts +66 -0
  289. package/dist/merge/resolve.d.ts.map +1 -0
  290. package/dist/merge/resolve.js +189 -0
  291. package/dist/merge/resolve.js.map +1 -0
  292. package/dist/merge/types.d.ts +82 -0
  293. package/dist/merge/types.d.ts.map +1 -0
  294. package/dist/merge/types.js +8 -0
  295. package/dist/merge/types.js.map +1 -0
  296. package/dist/parser/agent-data-sections.d.ts +53 -0
  297. package/dist/parser/agent-data-sections.d.ts.map +1 -0
  298. package/dist/parser/agent-data-sections.js +118 -0
  299. package/dist/parser/agent-data-sections.js.map +1 -0
  300. package/dist/parser/alignment.d.ts +4 -4
  301. package/dist/parser/alignment.d.ts.map +1 -1
  302. package/dist/parser/alignment.js +27 -22
  303. package/dist/parser/alignment.js.map +1 -1
  304. package/dist/parser/assess.d.ts +5 -5
  305. package/dist/parser/assess.d.ts.map +1 -1
  306. package/dist/parser/assess.js +36 -32
  307. package/dist/parser/assess.js.map +1 -1
  308. package/dist/parser/config.d.ts +351 -0
  309. package/dist/parser/config.d.ts.map +1 -0
  310. package/dist/parser/config.js +326 -0
  311. package/dist/parser/config.js.map +1 -0
  312. package/dist/parser/convention-validation.d.ts +1 -1
  313. package/dist/parser/convention-validation.d.ts.map +1 -1
  314. package/dist/parser/convention-validation.js +21 -16
  315. package/dist/parser/convention-validation.js.map +1 -1
  316. package/dist/parser/coverage-cache.d.ts +49 -0
  317. package/dist/parser/coverage-cache.d.ts.map +1 -0
  318. package/dist/parser/coverage-cache.js +123 -0
  319. package/dist/parser/coverage-cache.js.map +1 -0
  320. package/dist/parser/daemon-status.d.ts +37 -0
  321. package/dist/parser/daemon-status.d.ts.map +1 -0
  322. package/dist/parser/daemon-status.js +67 -0
  323. package/dist/parser/daemon-status.js.map +1 -0
  324. package/dist/parser/doctor.d.ts +107 -0
  325. package/dist/parser/doctor.d.ts.map +1 -0
  326. package/dist/parser/doctor.js +366 -0
  327. package/dist/parser/doctor.js.map +1 -0
  328. package/dist/parser/fix.d.ts +1 -1
  329. package/dist/parser/fix.d.ts.map +1 -1
  330. package/dist/parser/fix.js +31 -27
  331. package/dist/parser/fix.js.map +1 -1
  332. package/dist/parser/index.d.ts +16 -11
  333. package/dist/parser/index.d.ts.map +1 -1
  334. package/dist/parser/index.js +16 -11
  335. package/dist/parser/index.js.map +1 -1
  336. package/dist/parser/items.d.ts +8 -2
  337. package/dist/parser/items.d.ts.map +1 -1
  338. package/dist/parser/items.js +71 -35
  339. package/dist/parser/items.js.map +1 -1
  340. package/dist/parser/meta.d.ts +167 -9
  341. package/dist/parser/meta.d.ts.map +1 -1
  342. package/dist/parser/meta.js +379 -46
  343. package/dist/parser/meta.js.map +1 -1
  344. package/dist/parser/plan-document.d.ts +189 -0
  345. package/dist/parser/plan-document.d.ts.map +1 -0
  346. package/dist/parser/plan-document.js +340 -0
  347. package/dist/parser/plan-document.js.map +1 -0
  348. package/dist/parser/plans.d.ts +59 -0
  349. package/dist/parser/plans.d.ts.map +1 -0
  350. package/dist/parser/plans.js +239 -0
  351. package/dist/parser/plans.js.map +1 -0
  352. package/dist/parser/refs.d.ts +22 -9
  353. package/dist/parser/refs.d.ts.map +1 -1
  354. package/dist/parser/refs.js +102 -50
  355. package/dist/parser/refs.js.map +1 -1
  356. package/dist/parser/setup-status.d.ts +71 -0
  357. package/dist/parser/setup-status.d.ts.map +1 -0
  358. package/dist/parser/setup-status.js +269 -0
  359. package/dist/parser/setup-status.js.map +1 -0
  360. package/dist/parser/shadow.d.ts +150 -19
  361. package/dist/parser/shadow.d.ts.map +1 -1
  362. package/dist/parser/shadow.js +548 -187
  363. package/dist/parser/shadow.js.map +1 -1
  364. package/dist/parser/skill-render.d.ts +317 -0
  365. package/dist/parser/skill-render.d.ts.map +1 -0
  366. package/dist/parser/skill-render.js +943 -0
  367. package/dist/parser/skill-render.js.map +1 -0
  368. package/dist/parser/traits.d.ts +3 -3
  369. package/dist/parser/traits.d.ts.map +1 -1
  370. package/dist/parser/traits.js +2 -2
  371. package/dist/parser/traits.js.map +1 -1
  372. package/dist/parser/validate-skills.d.ts +32 -0
  373. package/dist/parser/validate-skills.d.ts.map +1 -0
  374. package/dist/parser/validate-skills.js +202 -0
  375. package/dist/parser/validate-skills.js.map +1 -0
  376. package/dist/parser/validate.d.ts +45 -3
  377. package/dist/parser/validate.d.ts.map +1 -1
  378. package/dist/parser/validate.js +622 -105
  379. package/dist/parser/validate.js.map +1 -1
  380. package/dist/parser/yaml.d.ts +83 -19
  381. package/dist/parser/yaml.d.ts.map +1 -1
  382. package/dist/parser/yaml.js +478 -173
  383. package/dist/parser/yaml.js.map +1 -1
  384. package/dist/ralph/cli-renderer.d.ts +8 -1
  385. package/dist/ralph/cli-renderer.d.ts.map +1 -1
  386. package/dist/ralph/cli-renderer.js +105 -34
  387. package/dist/ralph/cli-renderer.js.map +1 -1
  388. package/dist/ralph/events.d.ts +10 -10
  389. package/dist/ralph/events.d.ts.map +1 -1
  390. package/dist/ralph/events.js +277 -98
  391. package/dist/ralph/events.js.map +1 -1
  392. package/dist/ralph/index.d.ts +5 -2
  393. package/dist/ralph/index.d.ts.map +1 -1
  394. package/dist/ralph/index.js +9 -3
  395. package/dist/ralph/index.js.map +1 -1
  396. package/dist/ralph/loop-errors.d.ts +83 -0
  397. package/dist/ralph/loop-errors.d.ts.map +1 -0
  398. package/dist/ralph/loop-errors.js +150 -0
  399. package/dist/ralph/loop-errors.js.map +1 -0
  400. package/dist/ralph/subagent.d.ts +83 -0
  401. package/dist/ralph/subagent.d.ts.map +1 -0
  402. package/dist/ralph/subagent.js +174 -0
  403. package/dist/ralph/subagent.js.map +1 -0
  404. package/dist/ralph/wrap-up.d.ts +125 -0
  405. package/dist/ralph/wrap-up.d.ts.map +1 -0
  406. package/dist/ralph/wrap-up.js +270 -0
  407. package/dist/ralph/wrap-up.js.map +1 -0
  408. package/dist/schema/batch.d.ts +95 -0
  409. package/dist/schema/batch.d.ts.map +1 -0
  410. package/dist/schema/batch.js +24 -0
  411. package/dist/schema/batch.js.map +1 -0
  412. package/dist/schema/common.d.ts +2 -2
  413. package/dist/schema/common.d.ts.map +1 -1
  414. package/dist/schema/common.js +34 -31
  415. package/dist/schema/common.js.map +1 -1
  416. package/dist/schema/inbox.d.ts +12 -12
  417. package/dist/schema/inbox.js +4 -4
  418. package/dist/schema/inbox.js.map +1 -1
  419. package/dist/schema/index.d.ts +8 -5
  420. package/dist/schema/index.d.ts.map +1 -1
  421. package/dist/schema/index.js +8 -5
  422. package/dist/schema/index.js.map +1 -1
  423. package/dist/schema/meta.d.ts +1454 -27
  424. package/dist/schema/meta.d.ts.map +1 -1
  425. package/dist/schema/meta.js +198 -21
  426. package/dist/schema/meta.js.map +1 -1
  427. package/dist/schema/plan.d.ts +285 -0
  428. package/dist/schema/plan.d.ts.map +1 -0
  429. package/dist/schema/plan.js +81 -0
  430. package/dist/schema/plan.js.map +1 -0
  431. package/dist/schema/spec.d.ts +72 -33
  432. package/dist/schema/spec.d.ts.map +1 -1
  433. package/dist/schema/spec.js +22 -9
  434. package/dist/schema/spec.js.map +1 -1
  435. package/dist/schema/task.d.ts +172 -161
  436. package/dist/schema/task.d.ts.map +1 -1
  437. package/dist/schema/task.js +21 -12
  438. package/dist/schema/task.js.map +1 -1
  439. package/dist/schema/triage.d.ts +266 -0
  440. package/dist/schema/triage.d.ts.map +1 -0
  441. package/dist/schema/triage.js +134 -0
  442. package/dist/schema/triage.js.map +1 -0
  443. package/dist/sessions/index.d.ts +2 -2
  444. package/dist/sessions/index.d.ts.map +1 -1
  445. package/dist/sessions/index.js +3 -3
  446. package/dist/sessions/index.js.map +1 -1
  447. package/dist/sessions/store.d.ts +233 -1
  448. package/dist/sessions/store.d.ts.map +1 -1
  449. package/dist/sessions/store.js +628 -31
  450. package/dist/sessions/store.js.map +1 -1
  451. package/dist/sessions/types.d.ts +10 -10
  452. package/dist/sessions/types.d.ts.map +1 -1
  453. package/dist/sessions/types.js +10 -9
  454. package/dist/sessions/types.js.map +1 -1
  455. package/dist/strings/errors.d.ts +51 -0
  456. package/dist/strings/errors.d.ts.map +1 -1
  457. package/dist/strings/errors.js +136 -106
  458. package/dist/strings/errors.js.map +1 -1
  459. package/dist/strings/guidance.d.ts.map +1 -1
  460. package/dist/strings/guidance.js +16 -16
  461. package/dist/strings/guidance.js.map +1 -1
  462. package/dist/strings/index.d.ts +4 -4
  463. package/dist/strings/index.d.ts.map +1 -1
  464. package/dist/strings/index.js +4 -4
  465. package/dist/strings/index.js.map +1 -1
  466. package/dist/strings/labels.d.ts +4 -0
  467. package/dist/strings/labels.d.ts.map +1 -1
  468. package/dist/strings/labels.js +45 -41
  469. package/dist/strings/labels.js.map +1 -1
  470. package/dist/strings/validation.d.ts.map +1 -1
  471. package/dist/strings/validation.js +71 -71
  472. package/dist/strings/validation.js.map +1 -1
  473. package/dist/utils/commit.d.ts +1 -1
  474. package/dist/utils/commit.d.ts.map +1 -1
  475. package/dist/utils/commit.js +28 -26
  476. package/dist/utils/commit.js.map +1 -1
  477. package/dist/utils/git.d.ts +1 -1
  478. package/dist/utils/git.d.ts.map +1 -1
  479. package/dist/utils/git.js +40 -38
  480. package/dist/utils/git.js.map +1 -1
  481. package/dist/utils/grep.js +11 -11
  482. package/dist/utils/grep.js.map +1 -1
  483. package/dist/utils/index.d.ts +7 -7
  484. package/dist/utils/index.d.ts.map +1 -1
  485. package/dist/utils/index.js +4 -4
  486. package/dist/utils/index.js.map +1 -1
  487. package/dist/utils/time.d.ts.map +1 -1
  488. package/dist/utils/time.js +10 -10
  489. package/dist/utils/time.js.map +1 -1
  490. package/package.json +28 -5
  491. package/plugin/.claude-plugin/marketplace.json +17 -0
  492. package/plugin/.claude-plugin/plugin.json +5 -0
  493. package/plugin/plugins/kspec/skills/help/SKILL.md +42 -0
  494. package/plugin/plugins/kspec/skills/triage/SKILL.md +206 -0
  495. package/plugin/plugins/kspec/skills/triage/docs/automation.md +120 -0
  496. package/plugin/plugins/kspec/skills/triage/docs/inbox.md +144 -0
  497. package/plugin/plugins/kspec/skills/triage/docs/observations.md +85 -0
  498. package/templates/agents-sections/01-quick-start.md +22 -0
  499. package/templates/agents-sections/02-shadow-branch.md +34 -0
  500. package/templates/agents-sections/03-task-lifecycle.md +48 -0
  501. package/templates/agents-sections/04-pr-workflow.md +17 -0
  502. package/templates/agents-sections/05-commit-convention.md +27 -0
  503. package/templates/agents-sections/06-ralph-loop.md +45 -0
  504. package/templates/hooks/pre-commit +34 -0
  505. package/templates/skills/help/SKILL.md +37 -0
  506. package/templates/skills/manifest.yaml +15 -0
  507. package/templates/skills/triage/SKILL.md +199 -0
  508. package/templates/skills/triage/docs/automation.md +120 -0
  509. package/templates/skills/triage/docs/inbox.md +144 -0
  510. package/templates/skills/triage/docs/observations.md +85 -0
@@ -1,26 +1,31 @@
1
- import chalk from 'chalk';
2
- import { initContext, buildIndexes, createSpecItem, deleteSpecItem, updateSpecItem, addChildItem, loadAllItems, loadAllTasks, ReferenceIndex, AlignmentIndex, checkSlugUniqueness, patchSpecItems, findChildItems, findTraitImplementors, createNote, } from '../../parser/index.js';
3
- import { commitIfShadow } from '../../parser/shadow.js';
4
- import { SpecItemPatchSchema } from '../../schema/index.js';
5
- import { output, error, success, warn, isJsonMode } from '../output.js';
6
- import { grepItem, formatMatchedFields } from '../../utils/grep.js';
7
- import { errors } from '../../strings/errors.js';
8
- import { fieldLabels, sectionHeaders } from '../../strings/labels.js';
9
- import { EXIT_CODES } from '../exit-codes.js';
1
+ import chalk from "chalk";
2
+ import { markMutating } from "../command-annotations.js";
3
+ import { AlignmentIndex, addChildItem, buildIndexes, checkSlugUniqueness, createNote, createSpecItem, deleteSpecItem, findChildItems, findDescendantItems, findTraitImplementors, initContext, loadAllItems, loadAllTasks, patchSpecItems, ReferenceIndex, updateSpecItem, } from "../../parser/index.js";
4
+ import { commitIfShadow } from "../../parser/shadow.js";
5
+ import { SpecItemPatchSchema } from "../../schema/index.js";
6
+ import { errors } from "../../strings/errors.js";
7
+ import { fieldLabels, sectionHeaders } from "../../strings/labels.js";
8
+ import { formatMatchedFields, grepItem } from "../../utils/grep.js";
9
+ import { EXIT_CODES } from "../exit-codes.js";
10
+ import { error, isJsonMode, output, success, warn } from "../output.js";
11
+ import { parseTagsArray } from "../parse-utils.js";
10
12
  /**
11
13
  * Format a spec item for display
12
14
  */
13
15
  function formatItem(item, verbose = false, grepPattern) {
14
16
  const shortId = item._ulid.slice(0, 8);
15
- const slugStr = item.slugs.length > 0 ? chalk.cyan(`@${item.slugs[0]}`) : '';
17
+ const slugStr = item.slugs.length > 0 ? chalk.cyan(`@${item.slugs[0]}`) : "";
16
18
  const typeStr = chalk.gray(`[${item.type}]`);
17
- let status = '';
18
- if (item.status && typeof item.status === 'object') {
19
+ let status = "";
20
+ if (item.status && typeof item.status === "object") {
19
21
  const s = item.status;
20
22
  if (s.implementation) {
21
- const implColor = s.implementation === 'verified' ? chalk.green
22
- : s.implementation === 'implemented' ? chalk.cyan
23
- : s.implementation === 'in_progress' ? chalk.yellow
23
+ const implColor = s.implementation === "verified"
24
+ ? chalk.green
25
+ : s.implementation === "implemented"
26
+ ? chalk.cyan
27
+ : s.implementation === "in_progress"
28
+ ? chalk.yellow
24
29
  : chalk.gray;
25
30
  status = implColor(s.implementation);
26
31
  }
@@ -34,16 +39,18 @@ function formatItem(item, verbose = false, grepPattern) {
34
39
  if (status)
35
40
  line += ` ${status}`;
36
41
  if (verbose) {
37
- const tags = 'tags' in item && Array.isArray(item.tags) ? item.tags : [];
42
+ const tags = "tags" in item && Array.isArray(item.tags) ? item.tags : [];
38
43
  if (tags.length > 0) {
39
- line += chalk.blue(` #${tags.join(' #')}`);
44
+ line += chalk.blue(` #${tags.join(" #")}`);
40
45
  }
41
46
  }
42
47
  // Show matched fields if grep pattern provided
43
48
  if (grepPattern) {
44
49
  const match = grepItem(item, grepPattern);
45
50
  if (match && match.matchedFields.length > 0) {
46
- line += '\n ' + chalk.gray(`matched: ${formatMatchedFields(match.matchedFields)}`);
51
+ line +=
52
+ "\n " +
53
+ chalk.gray(`matched: ${formatMatchedFields(match.matchedFields)}`);
47
54
  }
48
55
  }
49
56
  return line;
@@ -53,7 +60,7 @@ function formatItem(item, verbose = false, grepPattern) {
53
60
  */
54
61
  function formatItemList(items, verbose = false, grepPattern) {
55
62
  if (items.length === 0) {
56
- console.log(chalk.gray('No items found'));
63
+ console.log(chalk.gray("No items found"));
57
64
  return;
58
65
  }
59
66
  for (const item of items) {
@@ -66,37 +73,37 @@ function formatItemList(items, verbose = false, grepPattern) {
66
73
  */
67
74
  function formatItemTree(items, verbose = false, grepPattern) {
68
75
  if (items.length === 0) {
69
- console.log(chalk.gray('No items found'));
76
+ console.log(chalk.gray("No items found"));
70
77
  return;
71
78
  }
72
79
  // Build parent-child map
73
80
  const childrenMap = new Map();
74
81
  const rootItems = [];
75
82
  for (const item of items) {
76
- const path = item._path || '';
83
+ const path = item._path || "";
77
84
  // Determine parent path
78
- let parentPath = '';
85
+ let parentPath = "";
79
86
  if (path) {
80
87
  // Extract parent path from current path
81
88
  // e.g., "features[0].requirements[1]" -> "features[0]"
82
- const lastDotIndex = path.lastIndexOf('.');
89
+ const lastDotIndex = path.lastIndexOf(".");
83
90
  if (lastDotIndex !== -1) {
84
91
  parentPath = path.substring(0, lastDotIndex);
85
92
  }
86
93
  }
87
- if (parentPath === '') {
94
+ if (parentPath === "") {
88
95
  // Root level item
89
96
  rootItems.push(item);
90
97
  }
91
98
  else {
92
99
  // Find parent by path
93
- const parent = items.find(i => i._path === parentPath);
100
+ const parent = items.find((i) => i._path === parentPath);
94
101
  if (parent) {
95
102
  const parentUlid = parent._ulid;
96
103
  if (!childrenMap.has(parentUlid)) {
97
104
  childrenMap.set(parentUlid, []);
98
105
  }
99
- childrenMap.get(parentUlid).push(item);
106
+ childrenMap.get(parentUlid)?.push(item);
100
107
  }
101
108
  else {
102
109
  // Parent not in filtered list, show at root
@@ -105,14 +112,14 @@ function formatItemTree(items, verbose = false, grepPattern) {
105
112
  }
106
113
  }
107
114
  // Recursive function to print tree
108
- function printTree(item, prefix = '', isLast = true) {
115
+ function printTree(item, prefix = "", isLast = true) {
109
116
  // Print current item with tree prefix
110
- const connector = isLast ? '└── ' : '├── ';
117
+ const connector = isLast ? "└── " : "├── ";
111
118
  const itemLine = formatItem(item, verbose, grepPattern);
112
119
  console.log(prefix + connector + itemLine);
113
120
  // Print children
114
121
  const children = childrenMap.get(item._ulid) || [];
115
- const childPrefix = prefix + (isLast ? ' ' : '');
122
+ const childPrefix = prefix + (isLast ? " " : "");
116
123
  children.forEach((child, index) => {
117
124
  const isLastChild = index === children.length - 1;
118
125
  printTree(child, childPrefix, isLastChild);
@@ -121,7 +128,7 @@ function formatItemTree(items, verbose = false, grepPattern) {
121
128
  // Print all root items
122
129
  rootItems.forEach((item, index) => {
123
130
  const isLast = index === rootItems.length - 1;
124
- printTree(item, '', isLast);
131
+ printTree(item, "", isLast);
125
132
  });
126
133
  console.log(chalk.gray(`\n${items.length} item(s)`));
127
134
  }
@@ -140,7 +147,7 @@ async function handleStatusCascade(ctx, parent, newStatus, allItems, refIndex) {
140
147
  return [];
141
148
  }
142
149
  // Prompt user for cascade
143
- const readline = await import('readline');
150
+ const readline = await import("node:readline");
144
151
  const rl = readline.createInterface({
145
152
  input: process.stdin,
146
153
  output: process.stdout,
@@ -149,18 +156,21 @@ async function handleStatusCascade(ctx, parent, newStatus, allItems, refIndex) {
149
156
  rl.question(`Update ${children.length} child item(s) to ${newStatus}? [y/n] `, resolve);
150
157
  });
151
158
  rl.close();
152
- if (answer.toLowerCase() !== 'y') {
159
+ if (answer.toLowerCase() !== "y") {
153
160
  return [];
154
161
  }
155
162
  // Update children
156
163
  const updatedChildren = [];
157
164
  for (const child of children) {
158
- const currentStatus = child.status && typeof child.status === 'object'
165
+ const currentStatus = child.status && typeof child.status === "object"
159
166
  ? child.status
160
- : { maturity: 'draft', implementation: 'not_started' };
167
+ : {
168
+ maturity: "draft",
169
+ implementation: "not_started",
170
+ };
161
171
  const updates = {
162
172
  status: {
163
- maturity: currentStatus.maturity || 'draft',
173
+ maturity: currentStatus.maturity || "draft",
164
174
  implementation: newStatus,
165
175
  },
166
176
  };
@@ -176,27 +186,27 @@ async function handleStatusCascade(ctx, parent, newStatus, allItems, refIndex) {
176
186
  * Register item commands
177
187
  */
178
188
  export function registerItemCommands(program) {
179
- const item = program
180
- .command('item')
181
- .description('Spec item commands');
189
+ const item = program.command("item").description("Spec item commands");
182
190
  // kspec item list
183
191
  item
184
- .command('list')
185
- .description('List spec items with optional filters')
186
- .option('-t, --type <type>', 'Filter by item type (module, feature, requirement, constraint, decision)')
187
- .option('-s, --status <status>', 'Filter by implementation status (not_started, in_progress, implemented, verified)')
188
- .option('-m, --maturity <maturity>', 'Filter by maturity (draft, proposed, stable, deferred, deprecated)')
189
- .option('--tag <tag>', 'Filter by tag (can specify multiple)', (val, prev) => [...prev, val], [])
190
- .option('--has <field>', 'Filter items that have field present', (val, prev) => [...prev, val], [])
191
- .option('-q, --search <text>', 'Search in title')
192
- .option('-g, --grep <pattern>', 'Search content with regex pattern')
193
- .option('-v, --verbose', 'Show more details')
194
- .option('--tree', 'Show parent/child hierarchy')
195
- .option('--limit <n>', 'Limit results', '50')
192
+ .command("list")
193
+ .description("List spec items with optional filters")
194
+ .option("-t, --type <type>", "Filter by item type (module, feature, requirement, constraint, decision)")
195
+ .option("-s, --status <status>", "Filter by implementation status (not_started, in_progress, implemented, verified)")
196
+ .option("-m, --maturity <maturity>", "Filter by maturity (draft, proposed, stable, deferred, deprecated)")
197
+ .option("--tag <tag>", "Filter by tag (can specify multiple)", (val, prev) => [...prev, val], [])
198
+ .option("--has <field>", "Filter items that have field present", (val, prev) => [...prev, val], [])
199
+ .option("-q, --search <text>", "Search in title")
200
+ .option("-g, --grep <pattern>", "Search content with regex pattern")
201
+ .option("-v, --verbose", "Show more details")
202
+ .option("--tree", "Show parent/child hierarchy")
203
+ .option("--under <ref>", "Scope to descendants of a module or parent item")
204
+ .option("--limit <n>", "Limit results", "50")
205
+ .option("--count", "Show only the count of matching items")
196
206
  .action(async (options) => {
197
207
  try {
198
208
  const ctx = await initContext();
199
- const { itemIndex, items } = await buildIndexes(ctx);
209
+ const { itemIndex, items, refIndex } = await buildIndexes(ctx);
200
210
  // Build filter from options
201
211
  const filter = {
202
212
  specItemsOnly: true, // Only spec items, not tasks
@@ -204,14 +214,20 @@ export function registerItemCommands(program) {
204
214
  if (options.type) {
205
215
  filter.type = options.type;
206
216
  }
217
+ // AC: @multi-value-status-filter ac-item-list-parity, ac-invalid-item-status
207
218
  if (options.status) {
208
- filter.implementation = options.status;
219
+ const { parseMultiStatus } = await import("./tasks.js");
220
+ const { ImplementationStatusSchema } = await import("../../schema/common.js");
221
+ const statuses = parseMultiStatus(options.status, ImplementationStatusSchema.options, "implementation status");
222
+ if (statuses) {
223
+ filter.implementation = statuses.length === 1 ? statuses[0] : statuses;
224
+ }
209
225
  }
210
226
  if (options.maturity) {
211
227
  filter.maturity = options.maturity;
212
228
  }
213
229
  if (options.tag && options.tag.length > 0) {
214
- filter.tags = options.tag;
230
+ filter.tags = parseTagsArray(options.tag);
215
231
  }
216
232
  if (options.has && options.has.length > 0) {
217
233
  filter.hasFields = options.has;
@@ -222,18 +238,67 @@ export function registerItemCommands(program) {
222
238
  if (options.grep) {
223
239
  filter.grepSearch = options.grep;
224
240
  }
241
+ // AC: @module-scoped-item-listing ac-under-filter, ac-under-invalid-ref
242
+ // Handle --under: scope to descendants of a module or parent item
243
+ let underRoot;
244
+ let underDescendantUlids;
245
+ if (options.under) {
246
+ const underResult = refIndex.resolve(options.under);
247
+ if (!underResult.ok) {
248
+ // AC: @module-scoped-item-listing ac-under-invalid-ref
249
+ error(`Reference not found: ${options.under}. Check with: kspec item get ${options.under}`);
250
+ process.exit(EXIT_CODES.NOT_FOUND);
251
+ }
252
+ underRoot = underResult.item;
253
+ // Check it's not a task
254
+ if ("status" in underRoot && typeof underRoot.status === "string") {
255
+ error(`Reference ${options.under} is a task, not a spec item`);
256
+ process.exit(EXIT_CODES.USAGE_ERROR);
257
+ }
258
+ // AC: @module-scoped-item-listing ac-nested-descendants
259
+ // Find all descendants based on _path and _sourceFile
260
+ const descendants = findDescendantItems(underRoot, items);
261
+ underDescendantUlids = new Set([underRoot._ulid, ...descendants.map(d => d._ulid)]);
262
+ }
225
263
  const limit = parseInt(options.limit, 10) || 50;
226
- const result = itemIndex.queryPaginated(filter, 0, limit);
227
- // Filter to only LoadedSpecItem (not tasks)
228
- const specItems = result.items.filter((item) => !('status' in item && typeof item.status === 'string'));
264
+ // When --under is used, we need to get all items first, then filter by scope,
265
+ // because pagination before scoping could miss items
266
+ let specItems;
267
+ let effectiveTotal;
268
+ if (underDescendantUlids) {
269
+ // AC: @module-scoped-item-listing ac-under-filter, ac-under-with-other-filters
270
+ // Get all items matching filters, then scope to descendants
271
+ const allResults = itemIndex.query(filter);
272
+ const allSpecItems = allResults.filter((item) => !("status" in item && typeof item.status === "string"));
273
+ // Apply --under filtering (AND logic with other filters)
274
+ const scopedItems = allSpecItems.filter(item => underDescendantUlids.has(item._ulid));
275
+ effectiveTotal = scopedItems.length;
276
+ specItems = scopedItems.slice(0, limit);
277
+ }
278
+ else {
279
+ const result = itemIndex.queryPaginated(filter, 0, limit);
280
+ // Filter to only LoadedSpecItem (not tasks)
281
+ specItems = result.items.filter((item) => !("status" in item && typeof item.status === "string"));
282
+ effectiveTotal = result.total;
283
+ }
284
+ // AC: @module-scoped-item-listing ac-count-with-under
285
+ // AC: @trait-filterable-list ac-8
286
+ if (options.count) {
287
+ output({ count: effectiveTotal }, () => {
288
+ console.log(effectiveTotal);
289
+ });
290
+ return;
291
+ }
229
292
  output({
230
293
  items: specItems,
231
- total: result.total,
294
+ total: effectiveTotal,
232
295
  showing: specItems.length,
233
296
  grepPattern: options.grep,
234
297
  tree: options.tree,
298
+ under: options.under,
235
299
  }, () => {
236
300
  if (options.tree) {
301
+ // AC: @module-scoped-item-listing ac-under-with-tree
237
302
  formatItemTree(specItems, options.verbose, options.grep);
238
303
  }
239
304
  else {
@@ -248,8 +313,8 @@ export function registerItemCommands(program) {
248
313
  });
249
314
  // kspec item get <ref>
250
315
  item
251
- .command('get <ref>')
252
- .description('Get details for a specific item')
316
+ .command("get <ref>")
317
+ .description("Get details for a specific item")
253
318
  .action(async (ref) => {
254
319
  try {
255
320
  const ctx = await initContext();
@@ -267,7 +332,7 @@ export function registerItemCommands(program) {
267
332
  if (!traitsByTrait.has(trait.ulid)) {
268
333
  traitsByTrait.set(trait.ulid, { trait, acs: [] });
269
334
  }
270
- traitsByTrait.get(trait.ulid).acs.push(ac);
335
+ traitsByTrait.get(trait.ulid)?.acs.push(ac);
271
336
  }
272
337
  // Build JSON output with inherited traits
273
338
  const jsonOutput = {
@@ -280,31 +345,62 @@ export function registerItemCommands(program) {
280
345
  };
281
346
  output(jsonOutput, () => {
282
347
  console.log(chalk.bold(item.title));
283
- console.log(chalk.gray(''.repeat(40)));
348
+ console.log(chalk.gray("".repeat(40)));
284
349
  console.log(`${fieldLabels.ulid} ${item._ulid}`);
285
350
  if (item.slugs.length > 0) {
286
- console.log(`${fieldLabels.slugs} ${item.slugs.join(', ')}`);
351
+ console.log(`${fieldLabels.slugs} ${item.slugs.join(", ")}`);
287
352
  }
288
353
  console.log(`${fieldLabels.type} ${item.type}`);
289
- if (item.status && typeof item.status === 'object') {
354
+ if (item.status && typeof item.status === "object") {
290
355
  const s = item.status;
291
356
  if (s.maturity)
292
357
  console.log(`${fieldLabels.maturity} ${s.maturity}`);
293
- if (s.implementation)
294
- console.log(`${fieldLabels.implementation}${s.implementation}`);
358
+ if (s.implementation) {
359
+ // AC: @trait-retrospective ac-4
360
+ // Show retrospective verification source
361
+ const isRetrospective = item.traits?.includes("@trait-retrospective");
362
+ const statusLabel = isRetrospective
363
+ ? `${s.implementation} (retrospective)`
364
+ : s.implementation;
365
+ console.log(`${fieldLabels.implementation}${statusLabel}`);
366
+ }
367
+ }
368
+ // AC: @trait-retrospective ac-4
369
+ // Show verification metadata for retrospective specs
370
+ const isRetrospective = item.traits?.includes("@trait-retrospective");
371
+ if (isRetrospective && (item.verified_at || item.verified_by)) {
372
+ const verifiedDate = item.verified_at
373
+ ? new Date(item.verified_at).toISOString().split("T")[0]
374
+ : "unknown";
375
+ const verifiedBy = item.verified_by || "unknown";
376
+ console.log(`Verified: ${verifiedDate} by ${verifiedBy}`);
377
+ }
378
+ if ("tags" in item &&
379
+ Array.isArray(item.tags) &&
380
+ item.tags.length > 0) {
381
+ console.log(`${fieldLabels.tags} ${item.tags.join(", ")}`);
382
+ }
383
+ // AC: @item-get ac-4
384
+ if (Array.isArray(item.depends_on) && item.depends_on.length > 0) {
385
+ console.log(`${fieldLabels.dependsOn} ${item.depends_on.join(", ")}`);
295
386
  }
296
- if ('tags' in item && Array.isArray(item.tags) && item.tags.length > 0) {
297
- console.log(`${fieldLabels.tags} ${item.tags.join(', ')}`);
387
+ if (Array.isArray(item.implements) && item.implements.length > 0) {
388
+ console.log(`${fieldLabels.implements} ${item.implements.join(", ")}`);
389
+ }
390
+ if (Array.isArray(item.relates_to) && item.relates_to.length > 0) {
391
+ console.log(`${fieldLabels.relatesTo} ${item.relates_to.join(", ")}`);
298
392
  }
299
393
  if (item.description) {
300
- console.log('\n' + sectionHeaders.description);
394
+ console.log(`\n${sectionHeaders.description}`);
301
395
  console.log(item.description);
302
396
  }
303
397
  // AC: @trait-display ac-1 - Show own AC first
304
- if ('acceptance_criteria' in item && Array.isArray(item.acceptance_criteria) && item.acceptance_criteria.length > 0) {
305
- console.log('\n' + sectionHeaders.acceptanceCriteria);
398
+ if ("acceptance_criteria" in item &&
399
+ Array.isArray(item.acceptance_criteria) &&
400
+ item.acceptance_criteria.length > 0) {
401
+ console.log(`\n${sectionHeaders.acceptanceCriteria}`);
306
402
  for (const ac of item.acceptance_criteria) {
307
- if (ac && typeof ac === 'object' && 'id' in ac) {
403
+ if (ac && typeof ac === "object" && "id" in ac) {
308
404
  const acObj = ac;
309
405
  console.log(chalk.cyan(` [${acObj.id}]`));
310
406
  if (acObj.given)
@@ -321,7 +417,8 @@ export function registerItemCommands(program) {
321
417
  for (const { trait, acs } of traitsByTrait.values()) {
322
418
  console.log(chalk.gray(`\n─── Inherited from @${trait.slug} ───`));
323
419
  for (const ac of acs) {
324
- console.log(chalk.cyan(` [${ac.id}]`) + chalk.gray(` (from @${trait.slug})`));
420
+ console.log(chalk.cyan(` [${ac.id}]`) +
421
+ chalk.gray(` (from @${trait.slug})`));
325
422
  if (ac.given)
326
423
  console.log(` Given: ${ac.given}`);
327
424
  if (ac.when)
@@ -340,16 +437,16 @@ export function registerItemCommands(program) {
340
437
  });
341
438
  // kspec item types - show available types and counts
342
439
  item
343
- .command('types')
344
- .description('Show item types and counts')
440
+ .command("types")
441
+ .description("Show item types and counts")
345
442
  .action(async () => {
346
443
  try {
347
444
  const ctx = await initContext();
348
445
  const { itemIndex } = await buildIndexes(ctx);
349
446
  const typeCounts = itemIndex.getTypeCounts();
350
447
  output(Object.fromEntries(typeCounts), () => {
351
- console.log(chalk.bold('Item Types'));
352
- console.log(chalk.gray(''.repeat(30)));
448
+ console.log(chalk.bold("Item Types"));
449
+ console.log(chalk.gray("".repeat(30)));
353
450
  for (const [type, count] of typeCounts) {
354
451
  console.log(` ${type}: ${count}`);
355
452
  }
@@ -363,16 +460,16 @@ export function registerItemCommands(program) {
363
460
  });
364
461
  // kspec item tags - show available tags and counts
365
462
  item
366
- .command('tags')
367
- .description('Show tags and counts')
463
+ .command("tags")
464
+ .description("Show tags and counts")
368
465
  .action(async () => {
369
466
  try {
370
467
  const ctx = await initContext();
371
468
  const { itemIndex } = await buildIndexes(ctx);
372
469
  const tagCounts = itemIndex.getTagCounts();
373
470
  output(Object.fromEntries(tagCounts), () => {
374
- console.log(chalk.bold('Tags'));
375
- console.log(chalk.gray(''.repeat(30)));
471
+ console.log(chalk.bold("Tags"));
472
+ console.log(chalk.gray("".repeat(30)));
376
473
  for (const [tag, count] of tagCounts) {
377
474
  console.log(` #${tag}: ${count}`);
378
475
  }
@@ -384,17 +481,22 @@ export function registerItemCommands(program) {
384
481
  }
385
482
  });
386
483
  // kspec item add - create a new spec item under a parent
387
- item
388
- .command('add')
389
- .description('Create a new spec item under a parent')
390
- .requiredOption('--under <ref>', 'Parent item reference (e.g., @core-primitives)')
391
- .requiredOption('--title <title>', 'Item title')
392
- .option('--type <type>', 'Item type (feature, requirement, constraint, decision)', 'feature')
393
- .option('--slug <slug>', 'Human-friendly slug')
394
- .option('--priority <priority>', 'Priority (high, medium, low)')
395
- .option('--tag <tag...>', 'Tags')
396
- .option('--description <desc>', 'Description')
397
- .option('--as <field>', 'Child field override (e.g., requirements, constraints)')
484
+ markMutating(item.command("add"))
485
+ .description("Create a new spec item under a parent")
486
+ .requiredOption("--under <ref>", "Parent item reference (e.g., @core-primitives)")
487
+ .requiredOption("--title <title>", "Item title")
488
+ .option("--type <type>", "Item type (feature, requirement, constraint, decision)", "feature")
489
+ .option("--slug <slug>", "Human-friendly slug")
490
+ .option("--priority <priority>", "Priority (high, medium, low)")
491
+ .option("--tag <tag...>", "Tags")
492
+ .option("--trait <trait...>", "Traits to apply (e.g., @trait-testable)")
493
+ .option("--description <desc>", "Description")
494
+ .option("--as <field>", "Child field override (e.g., requirements, constraints)")
495
+ .addHelpText("after", `
496
+ Examples:
497
+ $ kspec item add --under @parent --title "Feature name" --type feature
498
+ $ kspec item add --under @parent --title "Multi-tag" --tag api public
499
+ $ kspec item add --under @parent --title "API endpoint" --trait @trait-api-endpoint`)
398
500
  .action(async (options) => {
399
501
  try {
400
502
  const ctx = await initContext();
@@ -407,7 +509,7 @@ export function registerItemCommands(program) {
407
509
  }
408
510
  const parent = parentResult.item;
409
511
  // Check it's not a task
410
- if ('status' in parent && typeof parent.status === 'string') {
512
+ if ("status" in parent && typeof parent.status === "string") {
411
513
  error(errors.reference.parentIsTask(options.under));
412
514
  process.exit(EXIT_CODES.ERROR);
413
515
  }
@@ -419,33 +521,66 @@ export function registerItemCommands(program) {
419
521
  process.exit(EXIT_CODES.CONFLICT);
420
522
  }
421
523
  }
524
+ // Validate and canonicalize traits
525
+ const validatedTraits = [];
526
+ const seenTraitUlids = new Set();
527
+ let hasTraitErrors = false;
528
+ if (options.trait) {
529
+ for (const traitRef of options.trait) {
530
+ const traitResult = refIndex.resolve(traitRef);
531
+ if (!traitResult.ok) {
532
+ error(`Trait not found: ${traitRef}`);
533
+ hasTraitErrors = true;
534
+ continue;
535
+ }
536
+ const traitItem = traitResult.item;
537
+ if (traitItem.type !== "trait") {
538
+ error(`${traitRef} is not a trait (type: ${traitItem.type})`);
539
+ hasTraitErrors = true;
540
+ continue;
541
+ }
542
+ // Deduplicate by ULID
543
+ if (seenTraitUlids.has(traitItem._ulid)) {
544
+ continue;
545
+ }
546
+ seenTraitUlids.add(traitItem._ulid);
547
+ // Store canonical ref (prefer slug over ULID)
548
+ const canonicalRef = `@${traitItem.slugs[0] || traitItem._ulid}`;
549
+ validatedTraits.push(canonicalRef);
550
+ }
551
+ }
552
+ if (hasTraitErrors) {
553
+ process.exit(EXIT_CODES.NOT_FOUND);
554
+ }
422
555
  const input = {
423
556
  title: options.title,
424
557
  type: options.type,
425
558
  slugs: options.slug ? [options.slug] : [],
426
559
  priority: options.priority,
427
- tags: options.tag || [],
560
+ tags: parseTagsArray(options.tag),
428
561
  description: options.description,
429
562
  depends_on: [],
430
563
  implements: [],
431
564
  relates_to: [],
432
565
  tests: [],
433
- traits: [],
566
+ traits: validatedTraits,
434
567
  notes: [],
435
568
  };
436
569
  const newItem = createSpecItem(input);
437
570
  const result = await addChildItem(ctx, parent, newItem, options.as);
438
571
  // Build index including the new item for accurate short ULID
439
572
  const index = new ReferenceIndex([], [...items, result.item]);
440
- const itemSlug = result.item.slugs?.[0] || index.shortUlid(result.item._ulid);
441
- await commitIfShadow(ctx.shadow, 'item-add', itemSlug);
573
+ const itemSlug = result.item.slugs?.[0] ||
574
+ index.shortUlid(result.item._ulid);
575
+ await commitIfShadow(ctx.shadow, "item-add", itemSlug);
442
576
  success(`Created item: ${index.shortUlid(result.item._ulid)} under @${parent.slugs[0] || parent._ulid.slice(0, 8)}`, {
443
577
  item: result.item,
444
578
  path: result.path,
445
579
  });
446
580
  // Derive hint
447
581
  if (!isJsonMode()) {
448
- const refSlug = result.item.slugs?.[0] || index.shortUlid(result.item._ulid);
582
+ const refSlug = result.item.slugs?.[0] ||
583
+ index.shortUlid(result.item._ulid);
449
584
  console.log(chalk.gray(`\nDerive implementation task? kspec derive @${refSlug}`));
450
585
  }
451
586
  }
@@ -455,22 +590,38 @@ export function registerItemCommands(program) {
455
590
  }
456
591
  });
457
592
  // kspec item set - update a spec item field
458
- item
459
- .command('set <ref>')
460
- .description('Update a spec item field')
461
- .option('--title <title>', 'Set title')
462
- .option('--type <type>', 'Set type')
463
- .option('--slug <slug>', 'Add a slug')
464
- .option('--remove-slug <slug>', 'Remove a slug')
465
- .option('--priority <priority>', 'Set priority')
466
- .option('--tag <tag...>', 'Set tags (replaces existing)')
467
- .option('--description <desc>', 'Set description')
468
- .option('--status <status>', 'Set implementation status (not_started, in_progress, implemented, verified)')
469
- .option('--maturity <maturity>', 'Set maturity (draft, proposed, stable, deferred, deprecated)')
593
+ markMutating(item.command("set <ref>"))
594
+ .description("Update a spec item field")
595
+ .option("--title <title>", "Set title")
596
+ .option("--type <type>", "Set type")
597
+ .option("--slug <slug>", "Add a slug")
598
+ .option("--remove-slug <slug>", "Remove a slug")
599
+ .option("--priority <priority>", "Set priority")
600
+ .option("--tag <tag...>", "Set tags (replaces existing)")
601
+ .option("--description <desc>", "Set description")
602
+ .option("--status <status>", "Set implementation status (not_started, in_progress, implemented, verified)")
603
+ .option("--maturity <maturity>", "Set maturity (draft, proposed, stable, deferred, deprecated)")
604
+ .option("--verified-by <agent-ref>", "Set verified_by (for retrospective specs)")
605
+ .option("--verified-at <iso-timestamp>", "Set verified_at (defaults to now if --verified-by provided)")
606
+ .option("--trait <trait...>", "Set traits (replaces existing)")
607
+ .option("--relates-to <ref>", "Add a relates_to reference")
608
+ .option("--implements <ref>", "Add an implements reference")
609
+ .option("--depends-on <ref>", "Add a depends_on reference")
610
+ .option("--clear-relates-to", "Clear all relates_to references")
611
+ .option("--clear-implements", "Clear all implements references")
612
+ .option("--clear-depends-on", "Clear all depends_on references")
613
+ .addHelpText("after", `
614
+ Examples:
615
+ $ kspec item set @item-ref --title "New title"
616
+ $ kspec item set @item-ref --tag api internal security
617
+ $ kspec item set @item-ref --trait reusable testable
618
+ $ kspec item set @item-ref --relates-to @other-item
619
+ $ kspec item set @item-ref --implements @feature-spec
620
+ $ kspec item set @item-ref --depends-on @prereq-spec`)
470
621
  .action(async (ref, options) => {
471
622
  try {
472
623
  const ctx = await initContext();
473
- const { refIndex, items } = await buildIndexes(ctx);
624
+ const { refIndex, items, tasks } = await buildIndexes(ctx);
474
625
  const result = refIndex.resolve(ref);
475
626
  if (!result.ok) {
476
627
  error(errors.reference.itemNotFound(ref));
@@ -478,7 +629,7 @@ export function registerItemCommands(program) {
478
629
  }
479
630
  const foundItem = result.item;
480
631
  // Check if it's a task (tasks should use task commands)
481
- if ('status' in foundItem && typeof foundItem.status === 'string') {
632
+ if ("status" in foundItem && typeof foundItem.status === "string") {
482
633
  error(errors.reference.taskUseTaskCommands(ref));
483
634
  process.exit(EXIT_CODES.ERROR);
484
635
  }
@@ -502,6 +653,66 @@ export function registerItemCommands(program) {
502
653
  process.exit(EXIT_CODES.ERROR);
503
654
  }
504
655
  }
656
+ // Mutual exclusivity: cannot add and clear same field
657
+ // Check this before ref resolution so usage errors take precedence
658
+ if (options.relatesTo && options.clearRelatesTo) {
659
+ error("Cannot use --relates-to and --clear-relates-to together");
660
+ process.exit(EXIT_CODES.USAGE_ERROR);
661
+ }
662
+ if (options.implements && options.clearImplements) {
663
+ error("Cannot use --implements and --clear-implements together");
664
+ process.exit(EXIT_CODES.USAGE_ERROR);
665
+ }
666
+ if (options.dependsOn && options.clearDependsOn) {
667
+ error("Cannot use --depends-on and --clear-depends-on together");
668
+ process.exit(EXIT_CODES.USAGE_ERROR);
669
+ }
670
+ // Helper to validate relationship refs (must exist and be a spec item, not a task)
671
+ // Returns { ulid, canonicalRef } for deduplication and user-friendly storage
672
+ const validateRelationshipRef = (refStr, flagName) => {
673
+ const refResult = refIndex.resolve(refStr);
674
+ if (!refResult.ok) {
675
+ error(errors.reference.itemNotFound(refStr));
676
+ process.exit(EXIT_CODES.NOT_FOUND);
677
+ }
678
+ // Ensure it's a spec item, not a task
679
+ const isTask = tasks.some((t) => t._ulid === refResult.ulid);
680
+ if (isTask) {
681
+ error(`${flagName} reference must be a spec item, not a task: ${refStr}`);
682
+ process.exit(EXIT_CODES.USAGE_ERROR);
683
+ }
684
+ // Use primary slug if available for user-friendly storage, otherwise ULID
685
+ const item = refResult.item;
686
+ const canonicalRef = item.slugs?.[0] ? `@${item.slugs[0]}` : `@${refResult.ulid}`;
687
+ return { ulid: refResult.ulid, canonicalRef };
688
+ };
689
+ // Helper to resolve existing refs to ULIDs for deduplication
690
+ const resolveRefsToUlids = (refs) => {
691
+ const ulids = new Set();
692
+ for (const ref of refs) {
693
+ const result = refIndex.resolve(ref);
694
+ if (result.ok) {
695
+ ulids.add(result.ulid);
696
+ }
697
+ }
698
+ return ulids;
699
+ };
700
+ // AC: @item-set ac-5 - --relates-to validation
701
+ // Store resolved ULID for deduplication and canonical ref for storage
702
+ let relatesToResolved;
703
+ if (options.relatesTo) {
704
+ relatesToResolved = validateRelationshipRef(options.relatesTo, "--relates-to");
705
+ }
706
+ // AC: @item-set ac-6 - --implements validation
707
+ let implementsResolved;
708
+ if (options.implements) {
709
+ implementsResolved = validateRelationshipRef(options.implements, "--implements");
710
+ }
711
+ // AC: @item-set ac-7 - --depends-on validation
712
+ let dependsOnResolved;
713
+ if (options.dependsOn) {
714
+ dependsOnResolved = validateRelationshipRef(options.dependsOn, "--depends-on");
715
+ }
505
716
  // Build updates object
506
717
  const updates = {};
507
718
  if (options.title)
@@ -511,7 +722,7 @@ export function registerItemCommands(program) {
511
722
  if (options.slug || options.removeSlug) {
512
723
  let slugs = [...(foundItem.slugs || [])];
513
724
  if (options.removeSlug) {
514
- slugs = slugs.filter(s => s !== options.removeSlug);
725
+ slugs = slugs.filter((s) => s !== options.removeSlug);
515
726
  }
516
727
  if (options.slug) {
517
728
  slugs.push(options.slug);
@@ -521,12 +732,14 @@ export function registerItemCommands(program) {
521
732
  if (options.priority)
522
733
  updates.priority = options.priority;
523
734
  if (options.tag)
524
- updates.tags = options.tag;
735
+ updates.tags = parseTagsArray(options.tag);
736
+ if (options.trait)
737
+ updates.traits = options.trait;
525
738
  if (options.description)
526
739
  updates.description = options.description;
527
740
  // Handle status updates
528
741
  if (options.status || options.maturity) {
529
- const currentStatus = foundItem.status && typeof foundItem.status === 'object'
742
+ const currentStatus = foundItem.status && typeof foundItem.status === "object"
530
743
  ? foundItem.status
531
744
  : {};
532
745
  updates.status = {
@@ -535,8 +748,53 @@ export function registerItemCommands(program) {
535
748
  ...(options.maturity && { maturity: options.maturity }),
536
749
  };
537
750
  }
751
+ // Handle verification metadata (for retrospective specs)
752
+ if (options.verifiedBy) {
753
+ updates.verified_by = options.verifiedBy;
754
+ // Default verified_at to now if not specified
755
+ updates.verified_at = options.verifiedAt || new Date().toISOString();
756
+ }
757
+ else if (options.verifiedAt) {
758
+ updates.verified_at = options.verifiedAt;
759
+ }
760
+ // AC: @item-set ac-5 - Handle relates_to (append semantics)
761
+ // Uses resolved ULIDs for deduplication and stores canonical slug format
762
+ if (relatesToResolved) {
763
+ const current = foundItem.relates_to || [];
764
+ const existingUlids = resolveRefsToUlids(current);
765
+ if (!existingUlids.has(relatesToResolved.ulid)) {
766
+ updates.relates_to = [...current, relatesToResolved.canonicalRef];
767
+ }
768
+ }
769
+ if (options.clearRelatesTo) {
770
+ updates.relates_to = [];
771
+ }
772
+ // AC: @item-set ac-6 - Handle implements (append semantics)
773
+ // Uses resolved ULIDs for deduplication and stores canonical slug format
774
+ if (implementsResolved) {
775
+ const current = foundItem.implements || [];
776
+ const existingUlids = resolveRefsToUlids(current);
777
+ if (!existingUlids.has(implementsResolved.ulid)) {
778
+ updates.implements = [...current, implementsResolved.canonicalRef];
779
+ }
780
+ }
781
+ if (options.clearImplements) {
782
+ updates.implements = [];
783
+ }
784
+ // AC: @item-set ac-7 - Handle depends_on (append semantics)
785
+ // Uses resolved ULIDs for deduplication and stores canonical slug format
786
+ if (dependsOnResolved) {
787
+ const current = foundItem.depends_on || [];
788
+ const existingUlids = resolveRefsToUlids(current);
789
+ if (!existingUlids.has(dependsOnResolved.ulid)) {
790
+ updates.depends_on = [...current, dependsOnResolved.canonicalRef];
791
+ }
792
+ }
793
+ if (options.clearDependsOn) {
794
+ updates.depends_on = [];
795
+ }
538
796
  if (Object.keys(updates).length === 0) {
539
- warn('No updates specified');
797
+ warn("No updates specified");
540
798
  return;
541
799
  }
542
800
  const updated = await updateSpecItem(ctx, foundItem, updates);
@@ -547,8 +805,10 @@ export function registerItemCommands(program) {
547
805
  const cascadeResult = await handleStatusCascade(ctx, updated, options.status, items, refIndex);
548
806
  updatedItems.push(...cascadeResult);
549
807
  }
550
- await commitIfShadow(ctx.shadow, 'item-set', itemSlug);
551
- success(`Updated item: ${refIndex.shortUlid(updated._ulid)}`, { item: updated });
808
+ await commitIfShadow(ctx.shadow, "item-set", itemSlug);
809
+ success(`Updated item: ${refIndex.shortUlid(updated._ulid)}`, {
810
+ item: updated,
811
+ });
552
812
  // Derive hint
553
813
  if (!isJsonMode()) {
554
814
  const refSlug = updated.slugs?.[0] || refIndex.shortUlid(updated._ulid);
@@ -561,11 +821,10 @@ export function registerItemCommands(program) {
561
821
  }
562
822
  });
563
823
  // kspec item delete - delete a spec item
564
- item
565
- .command('delete <ref>')
566
- .description('Delete a spec item (including nested items)')
567
- .option('--force', 'Skip confirmation')
568
- .option('--cascade', 'Delete item and all descendants')
824
+ markMutating(item.command("delete <ref>"))
825
+ .description("Delete a spec item (including nested items)")
826
+ .option("--force", "Skip confirmation")
827
+ .option("--cascade", "Delete item and all descendants")
569
828
  .action(async (ref, options) => {
570
829
  try {
571
830
  const ctx = await initContext();
@@ -577,7 +836,7 @@ export function registerItemCommands(program) {
577
836
  }
578
837
  const foundItem = result.item;
579
838
  // Check if it's a task
580
- if ('status' in foundItem && typeof foundItem.status === 'string') {
839
+ if ("status" in foundItem && typeof foundItem.status === "string") {
581
840
  error(errors.reference.itemUseTaskCancel(ref));
582
841
  process.exit(EXIT_CODES.ERROR);
583
842
  }
@@ -585,15 +844,17 @@ export function registerItemCommands(program) {
585
844
  error(errors.operation.cannotDeleteNoSource);
586
845
  process.exit(EXIT_CODES.ERROR);
587
846
  }
588
- // AC-7: Check if this is a trait with implementors
847
+ // AC: @spec-item-delete-children ac-7 - Check if this is a trait with implementors
589
848
  const implementors = findTraitImplementors(foundItem, items);
590
849
  if (implementors.length > 0) {
591
- const implementorRefs = implementors.map(i => `@${i.slugs[0] || i._ulid.slice(0, 8)}`).join(', ');
850
+ const implementorRefs = implementors
851
+ .map((i) => `@${i.slugs[0] || i._ulid.slice(0, 8)}`)
852
+ .join(", ");
592
853
  const errorMsg = `Cannot delete: trait is used by ${implementors.length} specs. Remove trait from specs first: ${implementorRefs}`;
593
854
  if (isJsonMode()) {
594
855
  error(errorMsg, {
595
- error: 'trait_in_use',
596
- implementors: implementors.map(i => ({
856
+ error: "trait_in_use",
857
+ implementors: implementors.map((i) => ({
597
858
  ulid: i._ulid,
598
859
  slug: i.slugs[0],
599
860
  title: i.title,
@@ -605,16 +866,16 @@ export function registerItemCommands(program) {
605
866
  }
606
867
  process.exit(EXIT_CODES.ERROR);
607
868
  }
608
- // AC-1/AC-8: Check for child items (nested YAML items, not relates_to refs)
869
+ // AC: @spec-item-delete-children ac-1 ac-8 - Check for child items (nested YAML items, not relates_to refs)
609
870
  const children = findChildItems(foundItem, items);
610
871
  if (children.length > 0 && !options.cascade) {
611
- // AC-1: Block deletion if children exist without --cascade
872
+ // AC: @spec-item-delete-children ac-1 - Block deletion if children exist without --cascade
612
873
  const errorMsg = `Cannot delete: item has ${children.length} children. Use --cascade to delete recursively`;
613
874
  if (isJsonMode()) {
614
- // AC-10: JSON error includes children array
875
+ // AC: @spec-item-delete-children ac-10 - JSON error includes children array
615
876
  error(errorMsg, {
616
- error: 'has_children',
617
- children: children.map(c => ({
877
+ error: "has_children",
878
+ children: children.map((c) => ({
618
879
  ulid: c._ulid,
619
880
  slug: c.slugs[0],
620
881
  title: c.title,
@@ -627,44 +888,46 @@ export function registerItemCommands(program) {
627
888
  }
628
889
  process.exit(EXIT_CODES.ERROR);
629
890
  }
630
- // AC-9: Custom confirmation prompt for cascade
891
+ // AC: @spec-item-delete-children ac-9 - Custom confirmation prompt for cascade
631
892
  if (children.length > 0 && options.cascade && !options.force) {
632
893
  const itemRef = `@${foundItem.slugs[0] || foundItem._ulid.slice(0, 8)}`;
633
894
  // Check for JSON mode - requires --force
634
895
  if (isJsonMode()) {
635
- error('Confirmation required. Use --force with --json');
896
+ error("Confirmation required. Use --force with --json");
636
897
  process.exit(EXIT_CODES.ERROR);
637
898
  }
638
899
  // Check for non-interactive environment
639
- const isTTY = process.env.KSPEC_TEST_TTY === 'true' || process.stdin.isTTY;
900
+ const isTTY = process.env.KSPEC_TEST_TTY === "true" || process.stdin.isTTY;
640
901
  if (!isTTY) {
641
- error('Non-interactive environment. Use --force to proceed');
902
+ error("Non-interactive environment. Use --force to proceed");
642
903
  process.exit(EXIT_CODES.ERROR);
643
904
  }
644
905
  // Show confirmation prompt
645
- const readline = await import('readline');
906
+ const readline = await import("node:readline");
646
907
  const rl = readline.createInterface({
647
908
  input: process.stdin,
648
909
  output: process.stdout,
649
910
  });
650
- const response = await new Promise(resolve => {
651
- rl.question(chalk.yellow(`Delete ${itemRef} and ${children.length} descendant items? [y/N] `), answer => {
911
+ const response = await new Promise((resolve) => {
912
+ rl.question(chalk.yellow(`Delete ${itemRef} and ${children.length} descendant items? [y/N] `), (answer) => {
652
913
  rl.close();
653
914
  resolve(answer);
654
915
  });
655
916
  });
656
- if (response.toLowerCase() !== 'y') {
657
- console.log(chalk.gray('Operation cancelled'));
917
+ if (response.toLowerCase() !== "y") {
918
+ console.log(chalk.gray("Operation cancelled"));
658
919
  process.exit(EXIT_CODES.USAGE_ERROR);
659
920
  }
660
921
  }
661
- // AC-2/AC-3: Delete item and all descendants with cascade
662
- const itemsToDelete = options.cascade ? [foundItem, ...children] : [foundItem];
922
+ // AC: @spec-item-delete-children ac-2 ac-3 - Delete item and all descendants with cascade
923
+ const itemsToDelete = options.cascade
924
+ ? [foundItem, ...children]
925
+ : [foundItem];
663
926
  let deletedCount = 0;
664
927
  // Delete in reverse order (deepest first) to avoid path issues
665
928
  const sortedItems = [...itemsToDelete].sort((a, b) => {
666
- const aDepth = a._path ? a._path.split('.').length : 0;
667
- const bDepth = b._path ? b._path.split('.').length : 0;
929
+ const aDepth = a._path ? a._path.split(".").length : 0;
930
+ const bDepth = b._path ? b._path.split(".").length : 0;
668
931
  return bDepth - aDepth;
669
932
  });
670
933
  for (const itemToDelete of sortedItems) {
@@ -674,20 +937,26 @@ export function registerItemCommands(program) {
674
937
  }
675
938
  }
676
939
  if (deletedCount > 0) {
677
- // AC-6: Single shadow commit with all deletions
940
+ // AC: @spec-item-delete-children ac-6 - Single shadow commit with all deletions
678
941
  const itemSlug = foundItem.slugs[0] || refIndex.shortUlid(foundItem._ulid);
679
942
  const commitMsg = deletedCount > 1 ? `${deletedCount} items` : itemSlug;
680
- await commitIfShadow(ctx.shadow, 'item-delete', commitMsg);
943
+ await commitIfShadow(ctx.shadow, "item-delete", commitMsg);
681
944
  if (deletedCount > 1) {
682
- success(`Deleted ${deletedCount} items`, { deleted: deletedCount, root_ulid: foundItem._ulid });
945
+ success(`Deleted ${deletedCount} items`, {
946
+ deleted: deletedCount,
947
+ root_ulid: foundItem._ulid,
948
+ });
683
949
  }
684
950
  else {
685
- success(`Deleted item: ${foundItem.title}`, { deleted: true, ulid: foundItem._ulid });
951
+ success(`Deleted item: ${foundItem.title}`, {
952
+ deleted: true,
953
+ ulid: foundItem._ulid,
954
+ });
686
955
  }
687
956
  }
688
957
  else {
689
958
  error(errors.failures.deleteItem);
690
- console.log(chalk.gray('Edit the source file directly: ' + foundItem._sourceFile));
959
+ console.log(chalk.gray(`Edit the source file directly: ${foundItem._sourceFile}`));
691
960
  process.exit(EXIT_CODES.ERROR);
692
961
  }
693
962
  }
@@ -697,14 +966,13 @@ export function registerItemCommands(program) {
697
966
  }
698
967
  });
699
968
  // kspec item patch - update item fields via JSON
700
- item
701
- .command('patch [ref]')
702
- .description('Update spec item fields via JSON patch')
703
- .option('--data <json>', 'JSON data to patch')
704
- .option('--bulk', 'Read patches from stdin (JSONL or JSON array)')
705
- .option('--allow-unknown', 'Allow fields not in schema')
706
- .option('--dry-run', 'Preview changes without applying')
707
- .option('--fail-fast', 'Stop on first error (bulk mode)')
969
+ markMutating(item.command("patch [ref]"))
970
+ .description("Update spec item fields via JSON patch")
971
+ .option("--data <json>", "JSON data to patch")
972
+ .option("--bulk", "Read patches from stdin (JSONL or JSON array)")
973
+ .option("--allow-unknown", "Allow fields not in schema")
974
+ .option("--dry-run", "Preview changes without applying")
975
+ .option("--fail-fast", "Stop on first error (bulk mode)")
708
976
  .action(async (ref, options) => {
709
977
  try {
710
978
  const ctx = await initContext();
@@ -735,7 +1003,7 @@ export function registerItemCommands(program) {
735
1003
  });
736
1004
  // Shadow commit if any updates
737
1005
  if (!options.dryRun && result.summary.updated > 0) {
738
- await commitIfShadow(ctx.shadow, 'item-patch', `${result.summary.updated} items`);
1006
+ await commitIfShadow(ctx.shadow, "item-patch", `${result.summary.updated} items`);
739
1007
  }
740
1008
  output(result, () => formatBulkPatchResult(result, options.dryRun));
741
1009
  if (result.summary.failed > 0) {
@@ -755,7 +1023,7 @@ export function registerItemCommands(program) {
755
1023
  data = JSON.parse(options.data);
756
1024
  }
757
1025
  catch (err) {
758
- error(errors.validation.invalidJsonInData(err instanceof Error ? err.message : ''));
1026
+ error(errors.validation.invalidJsonInData(err instanceof Error ? err.message : ""));
759
1027
  process.exit(EXIT_CODES.ERROR);
760
1028
  }
761
1029
  }
@@ -766,7 +1034,7 @@ export function registerItemCommands(program) {
766
1034
  data = JSON.parse(stdin.trim());
767
1035
  }
768
1036
  catch (err) {
769
- error(errors.validation.invalidJsonFromStdin(err instanceof Error ? err.message : ''));
1037
+ error(errors.validation.invalidJsonFromStdin(err instanceof Error ? err.message : ""));
770
1038
  process.exit(EXIT_CODES.ERROR);
771
1039
  }
772
1040
  }
@@ -781,7 +1049,9 @@ export function registerItemCommands(program) {
781
1049
  const strictSchema = SpecItemPatchSchema.strict();
782
1050
  const parseResult = strictSchema.safeParse(data);
783
1051
  if (!parseResult.success) {
784
- const issues = parseResult.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ');
1052
+ const issues = parseResult.error.issues
1053
+ .map((i) => `${i.path.join(".")}: ${i.message}`)
1054
+ .join("; ");
785
1055
  error(errors.validation.invalidPatchDataWithIssues(issues));
786
1056
  process.exit(EXIT_CODES.ERROR);
787
1057
  }
@@ -794,23 +1064,28 @@ export function registerItemCommands(program) {
794
1064
  process.exit(EXIT_CODES.ERROR);
795
1065
  }
796
1066
  // Find the item
797
- const foundItem = items.find(i => i._ulid === resolved.ulid);
1067
+ const foundItem = items.find((i) => i._ulid === resolved.ulid);
798
1068
  if (!foundItem) {
799
1069
  error(errors.reference.notItem(ref));
800
1070
  process.exit(EXIT_CODES.ERROR);
801
1071
  }
802
1072
  if (options.dryRun) {
803
- output({ ref, data, wouldApplyTo: foundItem.title, ulid: foundItem._ulid }, () => {
804
- console.log(chalk.yellow('Would patch:'), foundItem.title);
805
- console.log(chalk.gray('ULID:'), foundItem._ulid.slice(0, 8));
806
- console.log(chalk.gray('Changes:'));
1073
+ output({
1074
+ ref,
1075
+ data,
1076
+ wouldApplyTo: foundItem.title,
1077
+ ulid: foundItem._ulid,
1078
+ }, () => {
1079
+ console.log(chalk.yellow("Would patch:"), foundItem.title);
1080
+ console.log(chalk.gray("ULID:"), foundItem._ulid.slice(0, 8));
1081
+ console.log(chalk.gray("Changes:"));
807
1082
  console.log(JSON.stringify(data, null, 2));
808
1083
  });
809
1084
  return;
810
1085
  }
811
1086
  const updated = await updateSpecItem(ctx, foundItem, data);
812
1087
  const itemSlug = foundItem.slugs[0] || refIndex.shortUlid(foundItem._ulid);
813
- await commitIfShadow(ctx.shadow, 'item-patch', itemSlug);
1088
+ await commitIfShadow(ctx.shadow, "item-patch", itemSlug);
814
1089
  success(`Patched item: ${itemSlug}`, { item: updated });
815
1090
  }
816
1091
  }
@@ -821,8 +1096,8 @@ export function registerItemCommands(program) {
821
1096
  });
822
1097
  // kspec item status - show implementation status with linked tasks
823
1098
  item
824
- .command('status <ref>')
825
- .description('Show implementation status and linked tasks for a spec item')
1099
+ .command("status <ref>")
1100
+ .description("Show implementation status and linked tasks for a spec item")
826
1101
  .action(async (ref) => {
827
1102
  try {
828
1103
  const ctx = await initContext();
@@ -836,7 +1111,7 @@ export function registerItemCommands(program) {
836
1111
  }
837
1112
  const foundItem = result.item;
838
1113
  // Check if it's a task
839
- if ('status' in foundItem && typeof foundItem.status === 'string') {
1114
+ if ("status" in foundItem && typeof foundItem.status === "string") {
840
1115
  error(errors.reference.notItem(ref));
841
1116
  process.exit(EXIT_CODES.ERROR);
842
1117
  }
@@ -850,34 +1125,40 @@ export function registerItemCommands(program) {
850
1125
  }
851
1126
  output(summary, () => {
852
1127
  console.log(chalk.bold(foundItem.title));
853
- console.log(chalk.gray(''.repeat(40)));
1128
+ console.log(chalk.gray("".repeat(40)));
854
1129
  // Status
855
- const currentColor = summary.currentStatus === 'implemented' ? chalk.green
856
- : summary.currentStatus === 'in_progress' ? chalk.yellow
1130
+ const currentColor = summary.currentStatus === "implemented"
1131
+ ? chalk.green
1132
+ : summary.currentStatus === "in_progress"
1133
+ ? chalk.yellow
857
1134
  : chalk.gray;
858
- const expectedColor = summary.expectedStatus === 'implemented' ? chalk.green
859
- : summary.expectedStatus === 'in_progress' ? chalk.yellow
1135
+ const expectedColor = summary.expectedStatus === "implemented"
1136
+ ? chalk.green
1137
+ : summary.expectedStatus === "in_progress"
1138
+ ? chalk.yellow
860
1139
  : chalk.gray;
861
1140
  console.log(`Current status: ${currentColor(summary.currentStatus)}`);
862
1141
  console.log(`Expected status: ${expectedColor(summary.expectedStatus)}`);
863
1142
  if (!summary.isAligned) {
864
- console.log(chalk.yellow('\n⚠ Status mismatch - run task complete to sync'));
1143
+ console.log(chalk.yellow("\n⚠ Status mismatch - run task complete to sync"));
865
1144
  }
866
1145
  else {
867
- console.log(chalk.green('\n✓ Aligned'));
1146
+ console.log(chalk.green("\n✓ Aligned"));
868
1147
  }
869
1148
  // Linked tasks
870
- console.log(chalk.bold('\nLinked Tasks:'));
1149
+ console.log(chalk.bold("\nLinked Tasks:"));
871
1150
  if (summary.linkedTasks.length === 0) {
872
- console.log(chalk.gray(' No tasks reference this spec item'));
1151
+ console.log(chalk.gray(" No tasks reference this spec item"));
873
1152
  }
874
1153
  else {
875
1154
  for (const task of summary.linkedTasks) {
876
- const statusColor = task.taskStatus === 'completed' ? chalk.green
877
- : task.taskStatus === 'in_progress' ? chalk.blue
1155
+ const statusColor = task.taskStatus === "completed"
1156
+ ? chalk.green
1157
+ : task.taskStatus === "in_progress"
1158
+ ? chalk.blue
878
1159
  : chalk.gray;
879
1160
  const shortId = task.taskUlid.slice(0, 8);
880
- const notes = task.hasNotes ? chalk.gray(' (has notes)') : '';
1161
+ const notes = task.hasNotes ? chalk.gray(" (has notes)") : "";
881
1162
  console.log(` ${statusColor(`[${task.taskStatus}]`)} ${shortId} ${task.taskTitle}${notes}`);
882
1163
  }
883
1164
  }
@@ -889,11 +1170,10 @@ export function registerItemCommands(program) {
889
1170
  }
890
1171
  });
891
1172
  // kspec item note <ref> <message>
892
- item
893
- .command('note <ref> <message>')
894
- .description('Add a note to a spec item')
895
- .option('--author <author>', 'Note author')
896
- .option('--supersedes <ulid>', 'ULID of note this supersedes')
1173
+ markMutating(item.command("note <ref> <message>"))
1174
+ .description("Add a note to a spec item")
1175
+ .option("--author <author>", "Note author")
1176
+ .option("--supersedes <ulid>", "ULID of note this supersedes")
897
1177
  .action(async (ref, message, options) => {
898
1178
  try {
899
1179
  const ctx = await initContext();
@@ -905,7 +1185,7 @@ export function registerItemCommands(program) {
905
1185
  error(errors.reference.itemNotFound(ref));
906
1186
  process.exit(EXIT_CODES.ERROR);
907
1187
  }
908
- const foundItem = items.find(i => i._ulid === result.ulid);
1188
+ const foundItem = items.find((i) => i._ulid === result.ulid);
909
1189
  if (!foundItem) {
910
1190
  error(errors.reference.itemNotFound(ref));
911
1191
  process.exit(EXIT_CODES.ERROR);
@@ -914,7 +1194,7 @@ export function registerItemCommands(program) {
914
1194
  const updatedNotes = [...(foundItem.notes || []), note];
915
1195
  await updateSpecItem(ctx, foundItem, { notes: updatedNotes });
916
1196
  const itemSlug = foundItem.slugs[0] || refIndex.shortUlid(foundItem._ulid);
917
- await commitIfShadow(ctx.shadow, 'item-note', itemSlug);
1197
+ await commitIfShadow(ctx.shadow, "item-note", itemSlug);
918
1198
  success(`Added note to spec item: ${refIndex.shortUlid(foundItem._ulid)}`, { note });
919
1199
  }
920
1200
  catch (err) {
@@ -924,8 +1204,8 @@ export function registerItemCommands(program) {
924
1204
  });
925
1205
  // kspec item notes <ref>
926
1206
  item
927
- .command('notes <ref>')
928
- .description('Show notes for a spec item')
1207
+ .command("notes <ref>")
1208
+ .description("Show notes for a spec item")
929
1209
  .action(async (ref) => {
930
1210
  try {
931
1211
  const ctx = await initContext();
@@ -937,7 +1217,7 @@ export function registerItemCommands(program) {
937
1217
  error(errors.reference.itemNotFound(ref));
938
1218
  process.exit(EXIT_CODES.ERROR);
939
1219
  }
940
- const foundItem = items.find(i => i._ulid === result.ulid);
1220
+ const foundItem = items.find((i) => i._ulid === result.ulid);
941
1221
  if (!foundItem) {
942
1222
  error(errors.reference.itemNotFound(ref));
943
1223
  process.exit(EXIT_CODES.ERROR);
@@ -945,14 +1225,14 @@ export function registerItemCommands(program) {
945
1225
  const notes = foundItem.notes || [];
946
1226
  output(notes, () => {
947
1227
  if (notes.length === 0) {
948
- console.log('No notes');
1228
+ console.log("No notes");
949
1229
  }
950
1230
  else {
951
1231
  for (const note of notes) {
952
- const author = note.author || 'unknown';
1232
+ const author = note.author || "unknown";
953
1233
  console.log(`[${note.created_at}] ${author}:`);
954
1234
  console.log(note.content);
955
- console.log('');
1235
+ console.log("");
956
1236
  }
957
1237
  }
958
1238
  });
@@ -964,14 +1244,14 @@ export function registerItemCommands(program) {
964
1244
  });
965
1245
  // Create subcommand group for acceptance criteria operations
966
1246
  const acCmd = item
967
- .command('ac')
968
- .description('Manage acceptance criteria on spec items');
1247
+ .command("ac")
1248
+ .description("Manage acceptance criteria on spec items");
969
1249
  // Helper: Generate next AC ID based on existing AC
970
1250
  function generateNextAcId(existingAc) {
971
1251
  if (!existingAc || existingAc.length === 0)
972
- return 'ac-1';
1252
+ return "ac-1";
973
1253
  const numericIds = existingAc
974
- .map(ac => ac.id.match(/^ac-(\d+)$/)?.[1])
1254
+ .map((ac) => ac.id.match(/^ac-(\d+)$/)?.[1])
975
1255
  .filter((id) => id !== null && id !== undefined)
976
1256
  .map(Number);
977
1257
  const maxId = numericIds.length > 0 ? Math.max(...numericIds) : 0;
@@ -988,7 +1268,7 @@ export function registerItemCommands(program) {
988
1268
  }
989
1269
  const foundItem = result.item;
990
1270
  // Check if it's a task
991
- if ('status' in foundItem && typeof foundItem.status === 'string') {
1271
+ if ("status" in foundItem && typeof foundItem.status === "string") {
992
1272
  error(errors.operation.tasksNoAcceptanceCriteria(ref));
993
1273
  process.exit(EXIT_CODES.NOT_FOUND);
994
1274
  }
@@ -996,8 +1276,8 @@ export function registerItemCommands(program) {
996
1276
  }
997
1277
  // kspec item ac list <ref>
998
1278
  acCmd
999
- .command('list <ref>')
1000
- .description('List acceptance criteria for a spec item')
1279
+ .command("list <ref>")
1280
+ .description("List acceptance criteria for a spec item")
1001
1281
  .action(async (ref) => {
1002
1282
  try {
1003
1283
  const { item, refIndex } = await resolveSpecItem(ref);
@@ -1006,7 +1286,7 @@ export function registerItemCommands(program) {
1006
1286
  console.log(chalk.bold(`Acceptance Criteria for: ${item.title} (@${item.slugs[0] || refIndex.shortUlid(item._ulid)})`));
1007
1287
  console.log();
1008
1288
  if (ac.length === 0) {
1009
- console.log(chalk.gray('No acceptance criteria'));
1289
+ console.log(chalk.gray("No acceptance criteria"));
1010
1290
  }
1011
1291
  else {
1012
1292
  for (const criterion of ac) {
@@ -1026,13 +1306,12 @@ export function registerItemCommands(program) {
1026
1306
  }
1027
1307
  });
1028
1308
  // kspec item ac add <ref>
1029
- acCmd
1030
- .command('add <ref>')
1031
- .description('Add an acceptance criterion to a spec item')
1032
- .option('--id <id>', 'AC identifier (auto-generated if not provided)')
1033
- .requiredOption('--given <text>', 'The precondition (Given...)')
1034
- .requiredOption('--when <text>', 'The action/trigger (When...)')
1035
- .requiredOption('--then <text>', 'The expected outcome (Then...)')
1309
+ markMutating(acCmd.command("add <ref>"))
1310
+ .description("Add an acceptance criterion to a spec item")
1311
+ .option("--id <id>", "AC identifier (auto-generated if not provided)")
1312
+ .requiredOption("--given <text>", "The precondition (Given...)")
1313
+ .requiredOption("--when <text>", "The action/trigger (When...)")
1314
+ .requiredOption("--then <text>", "The expected outcome (Then...)")
1036
1315
  .action(async (ref, options) => {
1037
1316
  try {
1038
1317
  const { ctx, item, refIndex } = await resolveSpecItem(ref);
@@ -1040,7 +1319,7 @@ export function registerItemCommands(program) {
1040
1319
  // Determine ID
1041
1320
  const acId = options.id || generateNextAcId(existingAc);
1042
1321
  // Check for duplicate ID
1043
- if (existingAc.some(ac => ac.id === acId)) {
1322
+ if (existingAc.some((ac) => ac.id === acId)) {
1044
1323
  const itemRef = item.slugs[0] || refIndex.shortUlid(item._ulid);
1045
1324
  error(errors.conflict.acAlreadyExists(acId, itemRef));
1046
1325
  process.exit(EXIT_CODES.CONFLICT);
@@ -1056,8 +1335,10 @@ export function registerItemCommands(program) {
1056
1335
  const updatedAc = [...existingAc, newAc];
1057
1336
  await updateSpecItem(ctx, item, { acceptance_criteria: updatedAc });
1058
1337
  const itemSlug = item.slugs[0] || refIndex.shortUlid(item._ulid);
1059
- await commitIfShadow(ctx.shadow, 'item-ac-add', itemSlug);
1060
- success(`Added acceptance criterion: ${acId} to @${itemSlug}`, { ac: newAc });
1338
+ await commitIfShadow(ctx.shadow, "item-ac-add", itemSlug);
1339
+ success(`Added acceptance criterion: ${acId} to @${itemSlug}`, {
1340
+ ac: newAc,
1341
+ });
1061
1342
  }
1062
1343
  catch (err) {
1063
1344
  error(errors.failures.addAc, err);
@@ -1065,19 +1346,18 @@ export function registerItemCommands(program) {
1065
1346
  }
1066
1347
  });
1067
1348
  // kspec item ac set <ref> <ac-id>
1068
- acCmd
1069
- .command('set <ref> <acId>')
1070
- .description('Update an acceptance criterion')
1071
- .option('--id <newId>', 'Rename the AC ID')
1072
- .option('--given <text>', 'Update the precondition')
1073
- .option('--when <text>', 'Update the action/trigger')
1074
- .option('--then <text>', 'Update the expected outcome')
1349
+ markMutating(acCmd.command("set <ref> <acId>"))
1350
+ .description("Update an acceptance criterion")
1351
+ .option("--id <newId>", "Rename the AC ID")
1352
+ .option("--given <text>", "Update the precondition")
1353
+ .option("--when <text>", "Update the action/trigger")
1354
+ .option("--then <text>", "Update the expected outcome")
1075
1355
  .action(async (ref, acId, options) => {
1076
1356
  try {
1077
1357
  const { ctx, item, refIndex } = await resolveSpecItem(ref);
1078
1358
  const existingAc = item.acceptance_criteria || [];
1079
1359
  // Find the AC
1080
- const acIndex = existingAc.findIndex(ac => ac.id === acId);
1360
+ const acIndex = existingAc.findIndex((ac) => ac.id === acId);
1081
1361
  if (acIndex === -1) {
1082
1362
  const itemRef = item.slugs[0] || refIndex.shortUlid(item._ulid);
1083
1363
  error(errors.reference.acNotFound(acId, itemRef));
@@ -1085,11 +1365,13 @@ export function registerItemCommands(program) {
1085
1365
  }
1086
1366
  // Check for no updates
1087
1367
  if (!options.id && !options.given && !options.when && !options.then) {
1088
- warn('No updates specified');
1368
+ warn("No updates specified");
1089
1369
  return;
1090
1370
  }
1091
1371
  // Check for duplicate ID if renaming
1092
- if (options.id && options.id !== acId && existingAc.some(ac => ac.id === options.id)) {
1372
+ if (options.id &&
1373
+ options.id !== acId &&
1374
+ existingAc.some((ac) => ac.id === options.id)) {
1093
1375
  error(errors.conflict.acIdAlreadyExists(options.id));
1094
1376
  process.exit(EXIT_CODES.CONFLICT);
1095
1377
  }
@@ -1104,18 +1386,18 @@ export function registerItemCommands(program) {
1104
1386
  ...(options.then && { then: options.then }),
1105
1387
  };
1106
1388
  if (options.id)
1107
- updatedFields.push('id');
1389
+ updatedFields.push("id");
1108
1390
  if (options.given)
1109
- updatedFields.push('given');
1391
+ updatedFields.push("given");
1110
1392
  if (options.when)
1111
- updatedFields.push('when');
1393
+ updatedFields.push("when");
1112
1394
  if (options.then)
1113
- updatedFields.push('then');
1395
+ updatedFields.push("then");
1114
1396
  // Update item
1115
1397
  await updateSpecItem(ctx, item, { acceptance_criteria: updatedAc });
1116
1398
  const itemSlug = item.slugs[0] || refIndex.shortUlid(item._ulid);
1117
- await commitIfShadow(ctx.shadow, 'item-ac-set', itemSlug);
1118
- success(`Updated acceptance criterion: ${acId} on @${itemSlug} (${updatedFields.join(', ')})`, { ac: updatedAc[acIndex] });
1399
+ await commitIfShadow(ctx.shadow, "item-ac-set", itemSlug);
1400
+ success(`Updated acceptance criterion: ${acId} on @${itemSlug} (${updatedFields.join(", ")})`, { ac: updatedAc[acIndex] });
1119
1401
  }
1120
1402
  catch (err) {
1121
1403
  error(errors.failures.updateAc, err);
@@ -1123,16 +1405,15 @@ export function registerItemCommands(program) {
1123
1405
  }
1124
1406
  });
1125
1407
  // kspec item ac remove <ref> <ac-id>
1126
- acCmd
1127
- .command('remove <ref> <acId>')
1128
- .description('Remove an acceptance criterion')
1129
- .option('--force', 'Skip confirmation')
1408
+ markMutating(acCmd.command("remove <ref> <acId>"))
1409
+ .description("Remove an acceptance criterion")
1410
+ .option("--force", "Skip confirmation")
1130
1411
  .action(async (ref, acId, options) => {
1131
1412
  try {
1132
1413
  const { ctx, item, refIndex } = await resolveSpecItem(ref);
1133
1414
  const existingAc = item.acceptance_criteria || [];
1134
1415
  // Find the AC
1135
- const acIndex = existingAc.findIndex(ac => ac.id === acId);
1416
+ const acIndex = existingAc.findIndex((ac) => ac.id === acId);
1136
1417
  if (acIndex === -1) {
1137
1418
  const itemRef = item.slugs[0] || refIndex.shortUlid(item._ulid);
1138
1419
  error(errors.reference.acNotFound(acId, itemRef));
@@ -1140,20 +1421,20 @@ export function registerItemCommands(program) {
1140
1421
  }
1141
1422
  // Confirmation required unless --force
1142
1423
  if (!options.force) {
1143
- // AC-5: JSON mode requires --force
1424
+ // AC: @spec-item-delete-children ac-5 - JSON mode requires --force
1144
1425
  if (isJsonMode()) {
1145
- error('Confirmation required. Use --force with --json');
1426
+ error("Confirmation required. Use --force with --json");
1146
1427
  process.exit(EXIT_CODES.ERROR);
1147
1428
  }
1148
- // AC-6: Non-interactive environment requires --force
1429
+ // AC: @spec-item-delete-children ac-6 - Non-interactive environment requires --force
1149
1430
  // Allow KSPEC_TEST_TTY for testing interactive prompts
1150
- const isTTY = process.env.KSPEC_TEST_TTY === '1' || process.stdin.isTTY;
1431
+ const isTTY = process.env.KSPEC_TEST_TTY === "1" || process.stdin.isTTY;
1151
1432
  if (!isTTY) {
1152
- error('Non-interactive environment. Use --force to proceed');
1433
+ error("Non-interactive environment. Use --force to proceed");
1153
1434
  process.exit(EXIT_CODES.ERROR);
1154
1435
  }
1155
- // AC-1: Prompt for confirmation
1156
- const readline = await import('readline');
1436
+ // AC: @spec-item-delete-children ac-1 - Prompt for confirmation
1437
+ const readline = await import("node:readline");
1157
1438
  const rl = readline.createInterface({
1158
1439
  input: process.stdin,
1159
1440
  output: process.stdout,
@@ -1162,19 +1443,21 @@ export function registerItemCommands(program) {
1162
1443
  rl.question(`Remove acceptance criterion ${acId}? [y/N] `, resolve);
1163
1444
  });
1164
1445
  rl.close();
1165
- // AC-3: User declines (n, N, or empty)
1166
- if (answer.toLowerCase() !== 'y') {
1167
- error('Operation cancelled');
1446
+ // AC: @spec-item-delete-children ac-3 - User declines (n, N, or empty)
1447
+ if (answer.toLowerCase() !== "y") {
1448
+ error("Operation cancelled");
1168
1449
  process.exit(EXIT_CODES.USAGE_ERROR);
1169
1450
  }
1170
1451
  }
1171
- // AC-4: With --force, proceed immediately without prompt
1172
- // AC-2: User confirmed, proceed with removal
1173
- const updatedAc = existingAc.filter(ac => ac.id !== acId);
1452
+ // AC: @spec-item-delete-children ac-4 - With --force, proceed immediately without prompt
1453
+ // AC: @spec-item-delete-children ac-2 - User confirmed, proceed with removal
1454
+ const updatedAc = existingAc.filter((ac) => ac.id !== acId);
1174
1455
  await updateSpecItem(ctx, item, { acceptance_criteria: updatedAc });
1175
1456
  const itemSlug = item.slugs[0] || refIndex.shortUlid(item._ulid);
1176
- await commitIfShadow(ctx.shadow, 'item-ac-remove', itemSlug);
1177
- success(`Removed acceptance criterion: ${acId} from @${itemSlug}`, { removed: acId });
1457
+ await commitIfShadow(ctx.shadow, "item-ac-remove", itemSlug);
1458
+ success(`Removed acceptance criterion: ${acId} from @${itemSlug}`, {
1459
+ removed: acId,
1460
+ });
1178
1461
  }
1179
1462
  catch (err) {
1180
1463
  error(errors.failures.removeAc, err);
@@ -1192,20 +1475,20 @@ async function readStdinFully() {
1192
1475
  return null;
1193
1476
  }
1194
1477
  return new Promise((resolve) => {
1195
- let data = '';
1478
+ let data = "";
1196
1479
  const timeout = setTimeout(() => {
1197
1480
  process.stdin.removeAllListeners();
1198
1481
  resolve(data || null);
1199
1482
  }, 5000); // 5 second timeout for bulk input
1200
- process.stdin.setEncoding('utf8');
1201
- process.stdin.on('data', (chunk) => {
1483
+ process.stdin.setEncoding("utf8");
1484
+ process.stdin.on("data", (chunk) => {
1202
1485
  data += chunk;
1203
1486
  });
1204
- process.stdin.on('end', () => {
1487
+ process.stdin.on("end", () => {
1205
1488
  clearTimeout(timeout);
1206
1489
  resolve(data || null);
1207
1490
  });
1208
- process.stdin.on('error', () => {
1491
+ process.stdin.on("error", () => {
1209
1492
  clearTimeout(timeout);
1210
1493
  resolve(null);
1211
1494
  });
@@ -1221,20 +1504,20 @@ async function readStdinIfAvailable() {
1221
1504
  return null;
1222
1505
  }
1223
1506
  return new Promise((resolve) => {
1224
- let data = '';
1507
+ let data = "";
1225
1508
  const timeout = setTimeout(() => {
1226
1509
  process.stdin.removeAllListeners();
1227
1510
  resolve(data || null);
1228
1511
  }, 100); // 100ms timeout for quick check
1229
- process.stdin.setEncoding('utf8');
1230
- process.stdin.on('data', (chunk) => {
1512
+ process.stdin.setEncoding("utf8");
1513
+ process.stdin.on("data", (chunk) => {
1231
1514
  data += chunk;
1232
1515
  });
1233
- process.stdin.on('end', () => {
1516
+ process.stdin.on("end", () => {
1234
1517
  clearTimeout(timeout);
1235
1518
  resolve(data || null);
1236
1519
  });
1237
- process.stdin.on('error', () => {
1520
+ process.stdin.on("error", () => {
1238
1521
  clearTimeout(timeout);
1239
1522
  resolve(null);
1240
1523
  });
@@ -1247,7 +1530,7 @@ async function readStdinIfAvailable() {
1247
1530
  function parseBulkInput(input) {
1248
1531
  const trimmed = input.trim();
1249
1532
  // Try JSON array first
1250
- if (trimmed.startsWith('[')) {
1533
+ if (trimmed.startsWith("[")) {
1251
1534
  const parsed = JSON.parse(trimmed);
1252
1535
  if (!Array.isArray(parsed)) {
1253
1536
  throw new Error(errors.validation.expectedJsonArray);
@@ -1255,13 +1538,13 @@ function parseBulkInput(input) {
1255
1538
  return parsed.map((item, i) => validatePatchOperation(item, i));
1256
1539
  }
1257
1540
  // Parse as JSONL (one JSON object per line)
1258
- const lines = trimmed.split('\n').filter(line => line.trim());
1541
+ const lines = trimmed.split("\n").filter((line) => line.trim());
1259
1542
  return lines.map((line, i) => {
1260
1543
  try {
1261
1544
  return validatePatchOperation(JSON.parse(line), i);
1262
1545
  }
1263
1546
  catch (err) {
1264
- throw new Error(errors.validation.jsonLineError(i + 1, err instanceof Error ? err.message : 'Invalid JSON'));
1547
+ throw new Error(errors.validation.jsonLineError(i + 1, err instanceof Error ? err.message : "Invalid JSON"));
1265
1548
  }
1266
1549
  });
1267
1550
  }
@@ -1269,14 +1552,14 @@ function parseBulkInput(input) {
1269
1552
  * Validate a patch operation object
1270
1553
  */
1271
1554
  function validatePatchOperation(obj, index) {
1272
- if (!obj || typeof obj !== 'object') {
1555
+ if (!obj || typeof obj !== "object") {
1273
1556
  throw new Error(errors.validation.patchMustBeObject(index));
1274
1557
  }
1275
1558
  const op = obj;
1276
- if (typeof op.ref !== 'string' || !op.ref) {
1559
+ if (typeof op.ref !== "string" || !op.ref) {
1277
1560
  throw new Error(errors.validation.patchMustHaveRef(index));
1278
1561
  }
1279
- if (!op.data || typeof op.data !== 'object') {
1562
+ if (!op.data || typeof op.data !== "object") {
1280
1563
  throw new Error(errors.validation.patchMustHaveData(index));
1281
1564
  }
1282
1565
  return { ref: op.ref, data: op.data };
@@ -1285,20 +1568,20 @@ function validatePatchOperation(obj, index) {
1285
1568
  * Format bulk patch result for human output
1286
1569
  */
1287
1570
  function formatBulkPatchResult(result, isDryRun = false) {
1288
- const prefix = isDryRun ? 'Would patch' : 'Patched';
1571
+ const prefix = isDryRun ? "Would patch" : "Patched";
1289
1572
  for (const r of result.results) {
1290
- if (r.status === 'updated') {
1291
- console.log(chalk.green('OK'), `${prefix}: ${r.ref} (${r.ulid?.slice(0, 8)})`);
1573
+ if (r.status === "updated") {
1574
+ console.log(chalk.green("OK"), `${prefix}: ${r.ref} (${r.ulid?.slice(0, 8)})`);
1292
1575
  }
1293
- else if (r.status === 'error') {
1294
- console.log(chalk.red('ERR'), `${r.ref}: ${r.error}`);
1576
+ else if (r.status === "error") {
1577
+ console.log(chalk.red("ERR"), `${r.ref}: ${r.error}`);
1295
1578
  }
1296
1579
  else {
1297
- console.log(chalk.gray('SKIP'), r.ref);
1580
+ console.log(chalk.gray("SKIP"), r.ref);
1298
1581
  }
1299
1582
  }
1300
- console.log('');
1301
- console.log(chalk.bold('Summary:'));
1583
+ console.log("");
1584
+ console.log(chalk.bold("Summary:"));
1302
1585
  console.log(` Total: ${result.summary.total}`);
1303
1586
  console.log(chalk.green(` Updated: ${result.summary.updated}`));
1304
1587
  if (result.summary.failed > 0) {