@kynetic-ai/spec 0.1.2 → 0.4.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 (540) 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 +107 -0
  32. package/dist/cli/batch-exec.d.ts.map +1 -0
  33. package/dist/cli/batch-exec.js +706 -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 +141 -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 +58 -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 +534 -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 +547 -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 +1109 -170
  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 +810 -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 +1267 -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 +56 -0
  156. package/dist/cli/commands/skill-install.d.ts.map +1 -0
  157. package/dist/cli/commands/skill-install.js +509 -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 +584 -350
  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 +225 -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 +483 -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 +237 -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 +402 -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 +457 -0
  309. package/dist/parser/config.d.ts.map +1 -0
  310. package/dist/parser/config.js +373 -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 +197 -0
  345. package/dist/parser/plan-document.d.ts.map +1 -0
  346. package/dist/parser/plan-document.js +341 -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 +301 -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 +94 -0
  401. package/dist/ralph/subagent.d.ts.map +1 -0
  402. package/dist/ralph/subagent.js +193 -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 +97 -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 +8 -2
  413. package/dist/schema/common.d.ts.map +1 -1
  414. package/dist/schema/common.js +42 -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 +241 -1
  448. package/dist/sessions/store.d.ts.map +1 -1
  449. package/dist/sessions/store.js +810 -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 +55 -0
  456. package/dist/strings/errors.d.ts.map +1 -1
  457. package/dist/strings/errors.js +138 -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/triage/actions.d.ts +27 -0
  474. package/dist/triage/actions.d.ts.map +1 -0
  475. package/dist/triage/actions.js +95 -0
  476. package/dist/triage/actions.js.map +1 -0
  477. package/dist/triage/constants.d.ts +6 -0
  478. package/dist/triage/constants.d.ts.map +1 -0
  479. package/dist/triage/constants.js +7 -0
  480. package/dist/triage/constants.js.map +1 -0
  481. package/dist/triage/index.d.ts +3 -0
  482. package/dist/triage/index.d.ts.map +1 -0
  483. package/dist/triage/index.js +3 -0
  484. package/dist/triage/index.js.map +1 -0
  485. package/dist/utils/commit.d.ts +1 -1
  486. package/dist/utils/commit.d.ts.map +1 -1
  487. package/dist/utils/commit.js +28 -26
  488. package/dist/utils/commit.js.map +1 -1
  489. package/dist/utils/git.d.ts +1 -1
  490. package/dist/utils/git.d.ts.map +1 -1
  491. package/dist/utils/git.js +40 -38
  492. package/dist/utils/git.js.map +1 -1
  493. package/dist/utils/grep.js +11 -11
  494. package/dist/utils/grep.js.map +1 -1
  495. package/dist/utils/index.d.ts +7 -7
  496. package/dist/utils/index.d.ts.map +1 -1
  497. package/dist/utils/index.js +4 -4
  498. package/dist/utils/index.js.map +1 -1
  499. package/dist/utils/time.d.ts.map +1 -1
  500. package/dist/utils/time.js +10 -10
  501. package/dist/utils/time.js.map +1 -1
  502. package/package.json +28 -5
  503. package/plugin/.claude-plugin/marketplace.json +17 -0
  504. package/plugin/.claude-plugin/plugin.json +5 -0
  505. package/plugin/plugins/kspec/skills/create-workflow/SKILL.md +235 -0
  506. package/plugin/plugins/kspec/skills/help/SKILL.md +42 -0
  507. package/plugin/plugins/kspec/skills/observations/SKILL.md +143 -0
  508. package/plugin/plugins/kspec/skills/plan/SKILL.md +343 -0
  509. package/plugin/plugins/kspec/skills/reflect/SKILL.md +161 -0
  510. package/plugin/plugins/kspec/skills/review/SKILL.md +193 -0
  511. package/plugin/plugins/kspec/skills/task-work/SKILL.md +303 -0
  512. package/plugin/plugins/kspec/skills/triage/SKILL.md +206 -0
  513. package/plugin/plugins/kspec/skills/triage/docs/automation.md +120 -0
  514. package/plugin/plugins/kspec/skills/triage/docs/inbox.md +144 -0
  515. package/plugin/plugins/kspec/skills/triage/docs/observations.md +85 -0
  516. package/plugin/plugins/kspec/skills/triage-automation/SKILL.md +140 -0
  517. package/plugin/plugins/kspec/skills/triage-inbox/SKILL.md +232 -0
  518. package/plugin/plugins/kspec/skills/writing-specs/SKILL.md +340 -0
  519. package/templates/agents-sections/01-quick-start.md +22 -0
  520. package/templates/agents-sections/02-shadow-branch.md +34 -0
  521. package/templates/agents-sections/03-task-lifecycle.md +48 -0
  522. package/templates/agents-sections/04-pr-workflow.md +17 -0
  523. package/templates/agents-sections/05-commit-convention.md +27 -0
  524. package/templates/agents-sections/06-ralph-loop.md +45 -0
  525. package/templates/hooks/pre-commit +34 -0
  526. package/templates/skills/create-workflow/SKILL.md +228 -0
  527. package/templates/skills/help/SKILL.md +37 -0
  528. package/templates/skills/manifest.yaml +60 -0
  529. package/templates/skills/observations/SKILL.md +137 -0
  530. package/templates/skills/plan/SKILL.md +336 -0
  531. package/templates/skills/reflect/SKILL.md +155 -0
  532. package/templates/skills/review/SKILL.md +186 -0
  533. package/templates/skills/task-work/SKILL.md +296 -0
  534. package/templates/skills/triage/SKILL.md +199 -0
  535. package/templates/skills/triage/docs/automation.md +120 -0
  536. package/templates/skills/triage/docs/inbox.md +144 -0
  537. package/templates/skills/triage/docs/observations.md +85 -0
  538. package/templates/skills/triage-automation/SKILL.md +134 -0
  539. package/templates/skills/triage-inbox/SKILL.md +225 -0
  540. package/templates/skills/writing-specs/SKILL.md +333 -0
@@ -1,12 +1,17 @@
1
- import chalk from 'chalk';
2
- import * as path from 'node:path';
3
- import { initContext, loadAllTasks, loadAllItems, saveTask, deleteTask, createTask, createNote, createTodo, syncSpecImplementationStatus, ReferenceIndex, checkSlugUniqueness, getAuthor, } from '../../parser/index.js';
4
- import { commitIfShadow } from '../../parser/shadow.js';
5
- import { output, formatTaskDetails, success, error, warn, info, isJsonMode, } from '../output.js';
6
- import { formatCommitGuidance, printCommitGuidance } from '../../utils/commit.js';
7
- import { alignmentCheck, errors } from '../../strings/index.js';
8
- import { executeBatchOperation, formatBatchOutput } from '../batch.js';
9
- import { EXIT_CODES } from '../exit-codes.js';
1
+ import chalk from "chalk";
2
+ import { markMutating } from "../command-annotations.js";
3
+ import { checkSlugUniqueness, createNote, createTask, createTodo, deleteTask, getAuthor, initContext, loadAllItems, loadAllTasks, ReferenceIndex, saveTask, scanTestCoverage, syncSpecImplementationStatus, } from "../../parser/index.js";
4
+ import { commitIfShadow } from "../../parser/shadow.js";
5
+ import { normalizeRefInput } from "../../schema/index.js";
6
+ import { alignmentCheck, errors } from "../../strings/index.js";
7
+ import { formatCommitGuidance, printCommitGuidance, } from "../../utils/commit.js";
8
+ import { executeBatchOperation, formatBatchOutput } from "../batch.js";
9
+ import { EXIT_CODES } from "../exit-codes.js";
10
+ import { parseTagsArray } from "../parse-utils.js";
11
+ import { annotateNotesWithSuperseded, error, formatTaskDetails, info, isJsonMode, output, success, warn, } from "../output.js";
12
+ import { parseIntOption, validateEnumOption, validateSpecRef, } from "../validators.js";
13
+ import { addListOptions, listTasksAction } from "./tasks.js";
14
+ import { findClosestCommand } from "../suggest.js";
10
15
  /**
11
16
  * Find a task by reference with detailed error reporting.
12
17
  * Returns the task or exits with appropriate error.
@@ -15,18 +20,18 @@ function resolveTaskRef(ref, tasks, index) {
15
20
  const result = index.resolve(ref);
16
21
  if (!result.ok) {
17
22
  switch (result.error) {
18
- case 'not_found':
23
+ case "not_found":
19
24
  error(errors.reference.taskNotFound(ref));
20
25
  break;
21
- case 'ambiguous':
26
+ case "ambiguous":
22
27
  error(errors.reference.ambiguous(ref));
23
28
  for (const candidate of result.candidates) {
24
- const task = tasks.find(t => t._ulid === candidate);
25
- const slug = task?.slugs[0] || '';
26
- console.error(` - ${index.shortUlid(candidate)} ${slug ? `(${slug})` : ''}`);
29
+ const task = tasks.find((t) => t._ulid === candidate);
30
+ const slug = task?.slugs[0] || "";
31
+ console.error(` - ${index.shortUlid(candidate)} ${slug ? `(${slug})` : ""}`);
27
32
  }
28
33
  break;
29
- case 'duplicate_slug':
34
+ case "duplicate_slug":
30
35
  error(errors.reference.slugMapsToMultiple(ref));
31
36
  for (const candidate of result.candidates) {
32
37
  console.error(` - ${index.shortUlid(candidate)}`);
@@ -37,7 +42,7 @@ function resolveTaskRef(ref, tasks, index) {
37
42
  process.exit(EXIT_CODES.NOT_FOUND);
38
43
  }
39
44
  // Check if it's actually a task
40
- const task = tasks.find(t => t._ulid === result.ulid);
45
+ const task = tasks.find((t) => t._ulid === result.ulid);
41
46
  if (!task) {
42
47
  error(errors.reference.notTask(ref));
43
48
  // AC: @cli-exit-codes consistent-usage - NOT_FOUND for missing resources
@@ -55,20 +60,20 @@ function resolveTaskRefForBatch(ref, tasks, index) {
55
60
  if (!result.ok) {
56
61
  let errorMsg;
57
62
  switch (result.error) {
58
- case 'not_found':
63
+ case "not_found":
59
64
  errorMsg = `Reference "${ref}" not found`;
60
65
  break;
61
- case 'ambiguous':
66
+ case "ambiguous":
62
67
  errorMsg = `Reference "${ref}" is ambiguous (matches ${result.candidates.length} items)`;
63
68
  break;
64
- case 'duplicate_slug':
69
+ case "duplicate_slug":
65
70
  errorMsg = `Slug "${ref}" maps to multiple items`;
66
71
  break;
67
72
  }
68
73
  return { task: null, error: errorMsg };
69
74
  }
70
75
  // Check if it's actually a task
71
- const task = tasks.find(t => t._ulid === result.ulid);
76
+ const task = tasks.find((t) => t._ulid === result.ulid);
72
77
  if (!task) {
73
78
  return { task: null, error: `Reference "${ref}" is not a task` };
74
79
  }
@@ -79,7 +84,7 @@ function resolveTaskRefForBatch(ref, tasks, index) {
79
84
  * Used by both single-ref and batch modes of task set.
80
85
  * AC: @spec-task-set-batch ac-1, ac-2, ac-4, ac-5
81
86
  */
82
- async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index, options) {
87
+ async function setTaskFields(foundTask, ctx, tasks, items, _allMetaItems, index, options) {
83
88
  try {
84
89
  // Check slug uniqueness if adding a new slug
85
90
  if (options.slug) {
@@ -96,7 +101,7 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
96
101
  const changes = [];
97
102
  if (options.title) {
98
103
  updatedTask.title = options.title;
99
- changes.push('title');
104
+ changes.push("title");
100
105
  }
101
106
  if (options.specRef) {
102
107
  // Validate the spec ref exists and is a spec item
@@ -108,15 +113,15 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
108
113
  };
109
114
  }
110
115
  // Check it's not a task
111
- const isTask = tasks.some(t => t._ulid === specResult.ulid);
116
+ const isTask = tasks.some((t) => t._ulid === specResult.ulid);
112
117
  if (isTask) {
113
118
  return {
114
119
  success: false,
115
120
  error: errors.reference.specRefIsTask(options.specRef),
116
121
  };
117
122
  }
118
- updatedTask.spec_ref = options.specRef;
119
- changes.push('spec_ref');
123
+ updatedTask.spec_ref = normalizeRefInput(options.specRef);
124
+ changes.push("spec_ref");
120
125
  }
121
126
  if (options.metaRef) {
122
127
  // Validate the meta ref exists and is a meta item
@@ -128,39 +133,77 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
128
133
  };
129
134
  }
130
135
  // Check if the resolved item is a meta item (not a spec item or task)
131
- const isTask = tasks.some(t => t._ulid === metaRefResult.ulid);
132
- const isSpecItem = items.some(i => i._ulid === metaRefResult.ulid);
136
+ const isTask = tasks.some((t) => t._ulid === metaRefResult.ulid);
137
+ const isSpecItem = items.some((i) => i._ulid === metaRefResult.ulid);
133
138
  if (isTask || isSpecItem) {
134
139
  return {
135
140
  success: false,
136
141
  error: errors.reference.metaRefPointsToSpec(options.metaRef),
137
142
  };
138
143
  }
139
- updatedTask.meta_ref = options.metaRef;
140
- changes.push('meta_ref');
144
+ updatedTask.meta_ref = normalizeRefInput(options.metaRef);
145
+ changes.push("meta_ref");
146
+ }
147
+ if (options.planRef !== undefined) {
148
+ // Handle 'null' string to clear plan_ref
149
+ if (options.planRef === "null") {
150
+ updatedTask.plan_ref = null;
151
+ changes.push("plan_ref: cleared");
152
+ }
153
+ else {
154
+ // First check if it's a task or spec item (wrong type)
155
+ const cleanRef = options.planRef.startsWith("@")
156
+ ? options.planRef.slice(1)
157
+ : options.planRef;
158
+ const isTask = tasks.some((t) => t.slugs.includes(cleanRef) ||
159
+ t._ulid === cleanRef ||
160
+ t._ulid.toLowerCase().startsWith(cleanRef.toLowerCase()));
161
+ const isSpecItem = items.some((i) => i.slugs.includes(cleanRef) ||
162
+ i._ulid === cleanRef ||
163
+ i._ulid.toLowerCase().startsWith(cleanRef.toLowerCase()));
164
+ if (isTask || isSpecItem) {
165
+ return {
166
+ success: false,
167
+ error: `Reference "${options.planRef}" is not a plan`,
168
+ };
169
+ }
170
+ // Now check if the plan exists
171
+ const { findPlanByRef } = await import("../../parser/plans.js");
172
+ const plan = await findPlanByRef(ctx, options.planRef);
173
+ if (!plan) {
174
+ return {
175
+ success: false,
176
+ error: `Plan reference not found: ${options.planRef}`,
177
+ };
178
+ }
179
+ updatedTask.plan_ref = normalizeRefInput(options.planRef);
180
+ changes.push("plan_ref");
181
+ }
141
182
  }
142
183
  if (options.priority) {
143
- const priority = parseInt(options.priority, 10);
144
- if (isNaN(priority) || priority < 1 || priority > 5) {
145
- return {
146
- success: false,
147
- error: 'Priority must be between 1 and 5',
148
- };
184
+ const priorityResult = parseIntOption(options.priority, {
185
+ min: 1,
186
+ max: 5,
187
+ name: "Priority",
188
+ });
189
+ if (!priorityResult.ok) {
190
+ return { success: false, error: priorityResult.error };
149
191
  }
150
- updatedTask.priority = priority;
151
- changes.push('priority');
192
+ updatedTask.priority = priorityResult.value;
193
+ changes.push("priority");
152
194
  }
153
195
  if (options.slug) {
154
196
  if (!updatedTask.slugs.includes(options.slug)) {
155
197
  updatedTask.slugs = [...updatedTask.slugs, options.slug];
156
- changes.push('slug');
198
+ changes.push("slug");
157
199
  }
158
200
  }
159
201
  if (options.tag) {
160
- const newTags = options.tag.filter((t) => !updatedTask.tags.includes(t));
202
+ const parsedTags = parseTagsArray(options.tag);
203
+ const newTags = parsedTags.filter((t) => !updatedTask.tags.includes(t));
161
204
  if (newTags.length > 0) {
162
205
  updatedTask.tags = [...updatedTask.tags, ...newTags];
163
- changes.push('tags');
206
+ changes.push("tags");
164
207
  }
165
208
  }
166
209
  if (options.dependsOn) {
@@ -173,9 +216,17 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
173
216
  error: errors.reference.depNotFound(depRef),
174
217
  };
175
218
  }
219
+ // Ensure the dependency is a task, not a spec item
220
+ const isTask = tasks.some((t) => t._ulid === depResult.ulid);
221
+ if (!isTask) {
222
+ return {
223
+ success: false,
224
+ error: `Reference "${depRef}" is not a task`,
225
+ };
226
+ }
176
227
  }
177
- updatedTask.depends_on = options.dependsOn;
178
- changes.push('depends_on');
228
+ updatedTask.depends_on = options.dependsOn.map(normalizeRefInput);
229
+ changes.push("depends_on");
179
230
  }
180
231
  // AC: @spec-task-clear-deps ac-1, ac-2 - Clear all dependencies
181
232
  if (options.clearDeps) {
@@ -183,14 +234,14 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
183
234
  // AC: @spec-task-clear-deps ac-2 - No changes needed
184
235
  return {
185
236
  success: true,
186
- message: 'No changes: task has no dependencies to clear',
237
+ message: "No changes: task has no dependencies to clear",
187
238
  };
188
239
  }
189
240
  updatedTask.depends_on = [];
190
- changes.push('depends_on');
241
+ changes.push("depends_on");
191
242
  // Add note documenting the change
192
243
  // AC: @task-set ac-author
193
- const note = createNote(`Dependencies cleared (was: ${foundTask.depends_on.join(', ')})`, getAuthor());
244
+ const note = createNote(`Dependencies cleared (was: ${foundTask.depends_on.join(", ")})`, getAuthor(ctx.config?.identity?.author));
194
245
  updatedTask.notes = [...updatedTask.notes, note];
195
246
  }
196
247
  // AC: @task-automation-eligibility ac-5, ac-11, ac-12, ac-18
@@ -199,46 +250,43 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
199
250
  if (options.automation === false) {
200
251
  // --no-automation flag clears the automation status (AC: ac-12)
201
252
  delete updatedTask.automation;
202
- changes.push('automation');
253
+ changes.push("automation");
203
254
  }
204
255
  else if (options.automation !== undefined) {
205
- const validStatuses = ['eligible', 'needs_review', 'manual_only'];
206
- if (!validStatuses.includes(options.automation)) {
207
- return {
208
- success: false,
209
- error: `Invalid automation status: ${options.automation}. Must be one of: ${validStatuses.join(', ')}`,
210
- };
256
+ const automationResult = validateEnumOption(options.automation, ["eligible", "needs_review", "manual_only"], "automation status");
257
+ if (!automationResult.ok) {
258
+ return { success: false, error: automationResult.error };
211
259
  }
212
260
  // AC: @task-automation-eligibility ac-18 - require reason for needs_review
213
- if (options.automation === 'needs_review' && !options.reason) {
261
+ if (options.automation === "needs_review" && !options.reason) {
214
262
  return {
215
263
  success: false,
216
- error: 'Setting automation to needs_review requires --reason flag explaining why',
264
+ error: "Setting automation to needs_review requires --reason flag explaining why",
217
265
  };
218
266
  }
219
- updatedTask.automation = options.automation;
220
- changes.push('automation');
267
+ updatedTask.automation = automationResult.value;
268
+ changes.push("automation");
221
269
  // If reason provided, add a note documenting the change
222
270
  // AC: @task-set ac-author
223
271
  if (options.reason) {
224
- const note = createNote(`Automation status set to ${options.automation}: ${options.reason}`, getAuthor());
272
+ const note = createNote(`Automation status set to ${automationResult.value}: ${options.reason}`, getAuthor(ctx.config?.identity?.author));
225
273
  updatedTask.notes = [...updatedTask.notes, note];
226
- changes.push('note');
274
+ changes.push("note");
227
275
  }
228
276
  }
229
277
  // AC: @spec-task-set-batch ac-4 - Warn on no changes, don't fail
230
278
  if (changes.length === 0) {
231
279
  return {
232
280
  success: true,
233
- message: 'No changes specified',
281
+ message: "No changes specified",
234
282
  data: { task: updatedTask },
235
283
  };
236
284
  }
237
285
  await saveTask(ctx, updatedTask);
238
- await commitIfShadow(ctx.shadow, 'task-set', foundTask.slugs[0] || index.shortUlid(foundTask._ulid), changes.join(', '));
286
+ await commitIfShadow(ctx.shadow, "task-set", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), changes.join(", "));
239
287
  return {
240
288
  success: true,
241
- message: `Updated task: ${index.shortUlid(updatedTask._ulid)} (${changes.join(', ')})`,
289
+ message: `Updated task: ${index.shortUlid(updatedTask._ulid)} (${changes.join(", ")})`,
242
290
  data: { task: updatedTask },
243
291
  };
244
292
  }
@@ -254,20 +302,68 @@ async function setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index,
254
302
  */
255
303
  export function registerTaskCommands(program) {
256
304
  const task = program
257
- .command('task')
258
- .description('Operations on individual tasks');
305
+ .command("task")
306
+ .description("Operations on individual tasks")
307
+ .allowUnknownOption()
308
+ .allowExcessArguments();
309
+ // AC: @command-group-default-actions ac-bare-task, ac-unknown-subcommand
310
+ // Default action when no subcommand is given (e.g. `kspec task` or `kspec task --status pending`)
311
+ task.action(async (_options, cmd) => {
312
+ const { Command: Cmd } = await import("commander");
313
+ const listCmd = addListOptions(new Cmd("_list"));
314
+ listCmd.exitOverride();
315
+ try {
316
+ listCmd.parse(cmd.args, { from: "user" });
317
+ }
318
+ catch {
319
+ console.error(chalk.gray(`Run 'kspec task --help' to see available subcommands`));
320
+ process.exit(EXIT_CODES.ERROR);
321
+ }
322
+ // AC: @command-group-default-actions ac-unknown-subcommand
323
+ if (listCmd.args.length > 0) {
324
+ const unknownCmd = listCmd.args[0];
325
+ const subcommandNames = cmd.commands.map((c) => c.name());
326
+ const suggestion = findClosestCommand(unknownCmd, subcommandNames);
327
+ console.error(chalk.red(`error: unknown command 'task ${unknownCmd}'`));
328
+ if (suggestion) {
329
+ console.error(chalk.yellow(`Did you mean: kspec task ${suggestion}?`));
330
+ }
331
+ else {
332
+ console.error(chalk.gray(`Run 'kspec task --help' to see available subcommands`));
333
+ }
334
+ process.exit(EXIT_CODES.ERROR);
335
+ }
336
+ // AC: @command-group-default-actions ac-bare-with-options
337
+ await listTasksAction(listCmd.opts());
338
+ });
339
+ // kspec task list - alias for 'kspec tasks list'
340
+ task
341
+ .command("list")
342
+ .description("List all tasks (alias for 'kspec tasks list')")
343
+ .option("-s, --status <status>", "Filter by status")
344
+ .option("-t, --type <type>", "Filter by type")
345
+ .option("--tag <tag>", "Filter by tag")
346
+ .option("--meta-ref <ref>", "Filter by meta reference")
347
+ .option("-g, --grep <pattern>", "Search content with regex pattern")
348
+ .option("-v, --verbose", "Show more details")
349
+ .option("--full", "Show full details (notes, todos, timestamps)")
350
+ .option("--count", "Show only the count of matching tasks")
351
+ .action(async (options) => {
352
+ await listTasksAction(options);
353
+ });
259
354
  // kspec task get <ref>
260
355
  task
261
- .command('get <ref>')
262
- .description('Get task details')
263
- .action(async (ref) => {
356
+ .command("get <ref>")
357
+ .description("Get task details")
358
+ .option("--all", "Show all notes including superseded ones")
359
+ .action(async (ref, options) => {
264
360
  try {
265
361
  const ctx = await initContext();
266
362
  const tasks = await loadAllTasks(ctx);
267
- const items = await loadAllItems(ctx);
363
+ const _items = await loadAllItems(ctx);
268
364
  // Build all indexes including TraitIndex
269
365
  const { refIndex: index, traitIndex } = await (async () => {
270
- const { buildIndexes } = await import('../../parser/index.js');
366
+ const { buildIndexes } = await import("../../parser/index.js");
271
367
  return buildIndexes(ctx);
272
368
  })();
273
369
  const foundTask = resolveTaskRef(ref, tasks, index);
@@ -284,14 +380,16 @@ export function registerTaskCommands(program) {
284
380
  if (!traitsByTrait.has(trait.ulid)) {
285
381
  traitsByTrait.set(trait.ulid, { trait, acs: [] });
286
382
  }
287
- traitsByTrait.get(trait.ulid).acs.push(ac);
383
+ traitsByTrait.get(trait.ulid)?.acs.push(ac);
288
384
  }
289
385
  inheritedTraits = Array.from(traitsByTrait.values());
290
386
  }
291
387
  }
292
388
  // Build JSON output with inherited traits (AC: @trait-display ac-2)
389
+ // Always include all notes in JSON output with superseded computed field
293
390
  const jsonOutput = {
294
391
  ...foundTask,
392
+ notes: annotateNotesWithSuperseded(foundTask.notes),
295
393
  ...(inheritedTraits.length > 0 && {
296
394
  inherited_traits: inheritedTraits.map(({ trait, acs }) => ({
297
395
  ref: `@${trait.slug}`,
@@ -301,13 +399,14 @@ export function registerTaskCommands(program) {
301
399
  }),
302
400
  };
303
401
  output(jsonOutput, () => {
304
- formatTaskDetails(foundTask, index);
402
+ formatTaskDetails(foundTask, index, { showAllNotes: options.all });
305
403
  // AC: @trait-display ac-3, ac-4, ac-5 - Show inherited AC per trait in labeled sections
306
404
  if (inheritedTraits.length > 0) {
307
405
  for (const { trait, acs } of inheritedTraits) {
308
406
  console.log(chalk.gray(`\n─── Inherited from @${trait.slug} ───`));
309
407
  for (const ac of acs) {
310
- console.log(chalk.cyan(` [${ac.id}]`) + chalk.gray(` (from @${trait.slug})`));
408
+ console.log(chalk.cyan(` [${ac.id}]`) +
409
+ chalk.gray(` (from @${trait.slug})`));
311
410
  if (ac.given)
312
411
  console.log(` Given: ${ac.given}`);
313
412
  if (ac.when)
@@ -325,25 +424,31 @@ export function registerTaskCommands(program) {
325
424
  }
326
425
  });
327
426
  // kspec task add
328
- task
329
- .command('add')
330
- .description('Create a new task')
331
- .requiredOption('--title <title>', 'Task title')
332
- .option('--description <description>', 'Task description')
333
- .option('--type <type>', 'Task type (task, epic, bug, spike, infra)', 'task')
334
- .option('--spec-ref <ref>', 'Reference to spec item')
335
- .option('--meta-ref <ref>', 'Reference to meta item (workflow, agent, or convention)')
336
- .option('--priority <n>', 'Priority (1-5)', '3')
337
- .option('--slug <slug>', 'Human-friendly slug')
338
- .option('--tag <tag...>', 'Tags')
339
- .option('--automation <status>', 'Automation eligibility (eligible, needs_review, manual_only)')
427
+ markMutating(task.command("add"))
428
+ .description("Create a new task")
429
+ .requiredOption("--title <title>", "Task title")
430
+ .option("--description <description>", "Task description")
431
+ .option("--type <type>", "Task type (task, epic, bug, spike, infra)", "task")
432
+ .option("--spec-ref <ref>", "Reference to spec item")
433
+ .option("--meta-ref <ref>", "Reference to meta item (workflow, agent, or convention)")
434
+ .option("--plan-ref <ref>", "Reference to plan this task is derived from")
435
+ .option("--priority <n>", "Priority (1-5)", "3")
436
+ .option("--slug <slug>", "Human-friendly slug")
437
+ .option("--tag <tag...>", "Tags")
438
+ .option("--depends-on <refs...>", "Set task dependencies")
439
+ .option("--automation <status>", "Automation eligibility (eligible, needs_review, manual_only)")
440
+ .addHelpText("after", `
441
+ Examples:
442
+ $ kspec task add --title "Implement feature" --spec-ref @feature-spec
443
+ $ kspec task add --title "Fix bug" --type bug --priority 1
444
+ $ kspec task add --title "Multi-tag task" --tag cli urgent`)
340
445
  .action(async (options) => {
341
446
  try {
342
447
  const ctx = await initContext();
343
448
  const tasks = await loadAllTasks(ctx);
344
449
  const items = await loadAllItems(ctx);
345
450
  // Load meta items for validation
346
- const { loadMetaContext } = await import('../../parser/meta.js');
451
+ const { loadMetaContext } = await import("../../parser/meta.js");
347
452
  const metaContext = await loadMetaContext(ctx);
348
453
  const allMetaItems = [
349
454
  ...metaContext.agents,
@@ -369,44 +474,106 @@ export function registerTaskCommands(program) {
369
474
  process.exit(EXIT_CODES.NOT_FOUND);
370
475
  }
371
476
  // Check if the resolved item is a meta item (not a spec item or task)
372
- const isTask = tasks.some(t => t._ulid === metaRefResult.ulid);
373
- const isSpecItem = items.some(i => i._ulid === metaRefResult.ulid);
477
+ const isTask = tasks.some((t) => t._ulid === metaRefResult.ulid);
478
+ const isSpecItem = items.some((i) => i._ulid === metaRefResult.ulid);
374
479
  if (isTask || isSpecItem) {
375
480
  error(errors.reference.metaRefPointsToSpec(options.metaRef));
376
481
  process.exit(EXIT_CODES.NOT_FOUND);
377
482
  }
378
483
  }
484
+ // Validate spec_ref if provided — must point to a spec item, not a task or meta item
485
+ if (options.specRef) {
486
+ const specRefResult = validateSpecRef(options.specRef, refIndex, tasks, items);
487
+ if (!specRefResult.ok) {
488
+ error(specRefResult.error);
489
+ process.exit(EXIT_CODES.NOT_FOUND);
490
+ }
491
+ }
379
492
  // AC: @task-automation-eligibility ac-13 - validate automation if provided
380
493
  let automationValue;
381
494
  if (options.automation) {
382
- const validStatuses = ['eligible', 'needs_review', 'manual_only'];
383
- if (!validStatuses.includes(options.automation)) {
384
- error(`Invalid automation status: ${options.automation}. Must be one of: ${validStatuses.join(', ')}`);
495
+ const automationResult = validateEnumOption(options.automation, ["eligible", "needs_review", "manual_only"], "automation status");
496
+ if (!automationResult.ok) {
497
+ error(automationResult.error);
385
498
  process.exit(EXIT_CODES.VALIDATION_FAILED);
386
499
  }
387
- automationValue = options.automation;
500
+ automationValue = automationResult.value;
501
+ }
502
+ // Validate plan_ref if provided (AC: @plan-derive ac-5, ac-6)
503
+ if (options.planRef) {
504
+ // First check if it's a task or spec item (wrong type)
505
+ const cleanRef = options.planRef.startsWith("@")
506
+ ? options.planRef.slice(1)
507
+ : options.planRef;
508
+ const isTask = tasks.some((t) => t.slugs.includes(cleanRef) ||
509
+ t._ulid === cleanRef ||
510
+ t._ulid.toLowerCase().startsWith(cleanRef.toLowerCase()));
511
+ const isSpecItem = items.some((i) => i.slugs.includes(cleanRef) ||
512
+ i._ulid === cleanRef ||
513
+ i._ulid.toLowerCase().startsWith(cleanRef.toLowerCase()));
514
+ if (isTask || isSpecItem) {
515
+ error(`Reference "${options.planRef}" is not a plan`);
516
+ process.exit(EXIT_CODES.NOT_FOUND);
517
+ }
518
+ // Now check if the plan exists
519
+ const { findPlanByRef } = await import("../../parser/plans.js");
520
+ const plan = await findPlanByRef(ctx, options.planRef);
521
+ if (!plan) {
522
+ error(`Plan reference not found: ${options.planRef}`);
523
+ process.exit(EXIT_CODES.NOT_FOUND);
524
+ }
525
+ }
526
+ // AC: @task-add-depends-on ac-2 - Validate dependency refs
527
+ if (options.dependsOn) {
528
+ for (const depRef of options.dependsOn) {
529
+ const depResult = refIndex.resolve(depRef);
530
+ if (!depResult.ok) {
531
+ error(errors.reference.depNotFound(depRef));
532
+ process.exit(EXIT_CODES.NOT_FOUND);
533
+ }
534
+ // Ensure the dependency is a task, not a spec item
535
+ const isTask = tasks.some((t) => t._ulid === depResult.ulid);
536
+ if (!isTask) {
537
+ error(`Reference "${depRef}" is not a task`);
538
+ process.exit(EXIT_CODES.NOT_FOUND);
539
+ }
540
+ }
541
+ }
542
+ // Validate priority
543
+ const priorityResult = parseIntOption(options.priority, {
544
+ min: 1,
545
+ max: 5,
546
+ name: "Priority",
547
+ });
548
+ if (!priorityResult.ok) {
549
+ error(priorityResult.error);
550
+ process.exit(EXIT_CODES.VALIDATION_FAILED);
388
551
  }
389
552
  // AC: @spec-task-add-description ac-6 - Omit description if empty string
390
- const descriptionValue = options.description && options.description.trim() !== ''
553
+ const descriptionValue = options.description && options.description.trim() !== ""
391
554
  ? options.description
392
555
  : undefined;
393
556
  const input = {
394
557
  title: options.title,
395
558
  description: descriptionValue,
396
559
  type: options.type,
397
- spec_ref: options.specRef || null,
398
- meta_ref: options.metaRef || null,
399
- priority: parseInt(options.priority, 10),
560
+ spec_ref: options.specRef ? normalizeRefInput(options.specRef) : null,
561
+ meta_ref: options.metaRef ? normalizeRefInput(options.metaRef) : null,
562
+ plan_ref: options.planRef ? normalizeRefInput(options.planRef) : null,
563
+ priority: priorityResult.value,
400
564
  slugs: options.slug ? [options.slug] : [],
401
- tags: options.tag || [],
565
+ tags: parseTagsArray(options.tag),
566
+ depends_on: (options.dependsOn || []).map(normalizeRefInput),
402
567
  automation: automationValue,
403
568
  };
404
569
  const newTask = createTask(input);
405
570
  await saveTask(ctx, newTask);
406
- await commitIfShadow(ctx.shadow, 'task-add', newTask.slugs[0] || newTask._ulid.slice(0, 8), newTask.title);
571
+ await commitIfShadow(ctx.shadow, "task-add", newTask.slugs[0] || newTask._ulid.slice(0, 8), newTask.title);
407
572
  // Build index including the new task for accurate short ULID
408
573
  const index = new ReferenceIndex([...tasks, newTask], items, allMetaItems);
409
- success(`Created task: ${index.shortUlid(newTask._ulid)}`, { task: newTask });
574
+ success(`Created task: ${index.shortUlid(newTask._ulid)}`, {
575
+ task: newTask,
576
+ });
410
577
  }
411
578
  catch (err) {
412
579
  error(errors.failures.createTask, err);
@@ -414,39 +581,45 @@ export function registerTaskCommands(program) {
414
581
  }
415
582
  });
416
583
  // kspec task set <ref>
417
- task
418
- .command('set [ref]')
419
- .description('Update task fields')
420
- .option('--refs <refs...>', 'Update multiple tasks (AC: @spec-task-set-batch ac-1)')
421
- .option('--title <title>', 'Update task title')
422
- .option('--spec-ref <ref>', 'Link to spec item')
423
- .option('--meta-ref <ref>', 'Link to meta item (workflow, agent, or convention)')
424
- .option('--priority <n>', 'Set priority (1-5)')
425
- .option('--slug <slug>', 'Add a slug alias')
426
- .option('--tag <tag...>', 'Add tags')
427
- .option('--depends-on <refs...>', 'Set dependencies (replaces existing)')
428
- .option('--clear-deps', 'Clear all dependencies')
429
- .option('--automation <status>', 'Set automation eligibility (eligible, needs_review, manual_only)')
430
- .option('--no-automation', 'Clear automation status (return to unassessed)')
431
- .option('--reason <reason>', 'Reason for status change (required when setting needs_review)')
432
- .option('--status <status>', 'Reject with error - use state transition commands instead')
584
+ markMutating(task.command("set [ref]"))
585
+ .description("Update task fields")
586
+ .option("--refs <refs...>", "Update multiple tasks (AC: @spec-task-set-batch ac-1)")
587
+ .option("--title <title>", "Update task title")
588
+ .option("--spec-ref <ref>", "Link to spec item")
589
+ .option("--meta-ref <ref>", "Link to meta item (workflow, agent, or convention)")
590
+ .option("--plan-ref <ref>", "Link to plan (use 'null' to clear)")
591
+ .option("--priority <n>", "Set priority (1-5)")
592
+ .option("--slug <slug>", "Add a slug alias")
593
+ .option("--tag <tag...>", "Add tags")
594
+ .option("--depends-on <refs...>", "Set dependencies (replaces existing)")
595
+ .option("--clear-deps", "Clear all dependencies")
596
+ .option("--automation <status>", "Set automation eligibility (eligible, needs_review, manual_only)")
597
+ .option("--no-automation", "Clear automation status (return to unassessed)")
598
+ .option("--reason <reason>", "Reason for status change (required when setting needs_review)")
599
+ .option("--status <status>", "Reject with error - use state transition commands instead")
600
+ .addHelpText("after", `
601
+ Examples:
602
+ $ kspec task set @task-slug --priority 2
603
+ $ kspec task set @task-slug --depends-on @dep1 @dep2
604
+ $ kspec task set @task-slug --tag cli urgent
605
+ $ kspec task set --refs @task1 @task2 --priority 3`)
433
606
  .action(async (ref, options) => {
434
607
  try {
435
608
  // AC: @spec-task-set-batch ac-3 - Reject --status flag
436
609
  if (options.status !== undefined) {
437
- error('Use state transition commands (start, complete, block, etc.) to change status');
610
+ error("Use state transition commands (start, complete, block, etc.) to change status");
438
611
  process.exit(EXIT_CODES.USAGE_ERROR);
439
612
  }
440
613
  // AC: @spec-task-clear-deps ac-3 - Mutual exclusivity check
441
614
  if (options.clearDeps && options.dependsOn) {
442
- error('Cannot use --clear-deps and --depends-on together');
615
+ error("Cannot use --clear-deps and --depends-on together");
443
616
  process.exit(EXIT_CODES.USAGE_ERROR);
444
617
  }
445
618
  const ctx = await initContext();
446
619
  const tasks = await loadAllTasks(ctx);
447
620
  const items = await loadAllItems(ctx);
448
621
  // Load meta items for validation
449
- const { loadMetaContext } = await import('../../parser/meta.js');
622
+ const { loadMetaContext } = await import("../../parser/meta.js");
450
623
  const metaContext = await loadMetaContext(ctx);
451
624
  const allMetaItems = [
452
625
  ...metaContext.agents,
@@ -456,7 +629,9 @@ export function registerTaskCommands(program) {
456
629
  ];
457
630
  const index = new ReferenceIndex(tasks, items, allMetaItems);
458
631
  // AC: @trait-multi-ref-batch ac-8 - Deduplicate refs
459
- const refsFlag = options.refs ? [...new Set(options.refs)] : undefined;
632
+ const refsFlag = options.refs
633
+ ? [...new Set(options.refs)]
634
+ : undefined;
460
635
  // Batch mode or single mode?
461
636
  if (refsFlag && refsFlag.length > 0) {
462
637
  // Batch mode - AC: @spec-task-set-batch ac-1, ac-2, ac-5
@@ -475,23 +650,23 @@ export function registerTaskCommands(program) {
475
650
  },
476
651
  getUlid: (task) => task._ulid,
477
652
  });
478
- formatBatchOutput(result, 'Set');
653
+ formatBatchOutput(result, "Set");
479
654
  }
480
655
  else {
481
656
  // Single mode - existing behavior
482
657
  if (!ref) {
483
- error('Either provide a positional ref or use --refs flag');
658
+ error("Either provide a positional ref or use --refs flag");
484
659
  process.exit(EXIT_CODES.USAGE_ERROR);
485
660
  }
486
661
  const foundTask = resolveTaskRef(ref, tasks, index);
487
662
  const result = await setTaskFields(foundTask, ctx, tasks, items, allMetaItems, index, options);
488
663
  if (!result.success) {
489
- error(result.error || 'Failed to update task');
664
+ error(result.error || "Failed to update task");
490
665
  process.exit(EXIT_CODES.ERROR);
491
666
  }
492
667
  if (result.message) {
493
668
  // AC: @spec-task-set-batch ac-4 - Warn on no changes
494
- if (result.message.includes('No changes')) {
669
+ if (result.message.includes("No changes")) {
495
670
  if (isJsonMode()) {
496
671
  output({ success: true, message: result.message });
497
672
  }
@@ -511,19 +686,18 @@ export function registerTaskCommands(program) {
511
686
  }
512
687
  });
513
688
  // kspec task patch <ref>
514
- task
515
- .command('patch <ref>')
516
- .description('Update task with JSON data')
517
- .option('--data <json>', 'JSON object with fields to update')
518
- .option('--dry-run', 'Show what would change without writing')
519
- .option('--allow-unknown', 'Allow unknown fields (for extending format)')
689
+ markMutating(task.command("patch <ref>"))
690
+ .description("Update task with JSON data")
691
+ .option("--data <json>", "JSON object with fields to update")
692
+ .option("--dry-run", "Show what would change without writing")
693
+ .option("--allow-unknown", "Allow unknown fields (for extending format)")
520
694
  .action(async (ref, options) => {
521
695
  try {
522
696
  const ctx = await initContext();
523
697
  const tasks = await loadAllTasks(ctx);
524
698
  const items = await loadAllItems(ctx);
525
699
  // Load meta items for validation
526
- const { loadMetaContext } = await import('../../parser/meta.js');
700
+ const { loadMetaContext } = await import("../../parser/meta.js");
527
701
  const metaContext = await loadMetaContext(ctx);
528
702
  const allMetaItems = [
529
703
  ...metaContext.agents,
@@ -544,7 +718,7 @@ export function registerTaskCommands(program) {
544
718
  for await (const chunk of process.stdin) {
545
719
  chunks.push(chunk);
546
720
  }
547
- jsonData = Buffer.concat(chunks).toString('utf-8');
721
+ jsonData = Buffer.concat(chunks).toString("utf-8");
548
722
  }
549
723
  // Parse JSON
550
724
  let patchData;
@@ -556,7 +730,7 @@ export function registerTaskCommands(program) {
556
730
  process.exit(EXIT_CODES.ERROR);
557
731
  }
558
732
  // Validate against TaskInputSchema (partial)
559
- const { TaskInputSchema } = await import('../../schema/index.js');
733
+ const { TaskInputSchema } = await import("../../schema/index.js");
560
734
  // Create a partial schema for validation
561
735
  const partialSchema = options.allowUnknown
562
736
  ? TaskInputSchema.partial().passthrough()
@@ -573,7 +747,7 @@ export function registerTaskCommands(program) {
573
747
  if (!options.allowUnknown) {
574
748
  const knownFields = Object.keys(TaskInputSchema.shape);
575
749
  const providedFields = Object.keys(patchData);
576
- const unknownFields = providedFields.filter(f => !knownFields.includes(f));
750
+ const unknownFields = providedFields.filter((f) => !knownFields.includes(f));
577
751
  if (unknownFields.length > 0) {
578
752
  error(errors.validation.unknownFields(unknownFields));
579
753
  process.exit(EXIT_CODES.ERROR);
@@ -584,17 +758,17 @@ export function registerTaskCommands(program) {
584
758
  // Track changes for output
585
759
  const changes = Object.keys(validatedPatch);
586
760
  if (options.dryRun) {
587
- info('Dry run - no changes will be written');
588
- info(`Would update: ${changes.join(', ')}`);
761
+ info("Dry run - no changes will be written");
762
+ info(`Would update: ${changes.join(", ")}`);
589
763
  output({ changes, updated: updatedTask }, () => {
590
- console.log(`\nChanges: ${changes.join(', ')}\n`);
764
+ console.log(`\nChanges: ${changes.join(", ")}\n`);
591
765
  return formatTaskDetails(updatedTask, index);
592
766
  });
593
767
  return;
594
768
  }
595
769
  await saveTask(ctx, updatedTask);
596
- await commitIfShadow(ctx.shadow, 'task-patch', foundTask.slugs[0] || index.shortUlid(foundTask._ulid), changes.join(', '));
597
- success(`Patched task: ${index.shortUlid(updatedTask._ulid)} (${changes.join(', ')})`, { task: updatedTask });
770
+ await commitIfShadow(ctx.shadow, "task-patch", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), changes.join(", "));
771
+ success(`Patched task: ${index.shortUlid(updatedTask._ulid)} (${changes.join(", ")})`, { task: updatedTask });
598
772
  }
599
773
  catch (err) {
600
774
  error(errors.failures.patchTask, err);
@@ -602,10 +776,9 @@ export function registerTaskCommands(program) {
602
776
  }
603
777
  });
604
778
  // kspec task start <ref>
605
- task
606
- .command('start <ref>')
607
- .description('Start working on a task (pending -> in_progress)')
608
- .option('--no-sync', 'Skip syncing spec implementation status')
779
+ markMutating(task.command("start <ref>"))
780
+ .description("Start working on a task (pending|needs_work -> in_progress)")
781
+ .option("--no-sync", "Skip syncing spec implementation status")
609
782
  .action(async (ref, options) => {
610
783
  try {
611
784
  const ctx = await initContext();
@@ -613,37 +786,40 @@ export function registerTaskCommands(program) {
613
786
  const items = await loadAllItems(ctx);
614
787
  const index = new ReferenceIndex(tasks, items);
615
788
  const foundTask = resolveTaskRef(ref, tasks, index);
616
- if (foundTask.status === 'in_progress') {
617
- warn('Task is already in progress');
789
+ if (foundTask.status === "in_progress") {
790
+ warn("Task is already in progress");
618
791
  output(foundTask, () => formatTaskDetails(foundTask));
619
792
  return;
620
793
  }
621
- if (foundTask.status !== 'pending') {
794
+ if (foundTask.status !== "pending" && foundTask.status !== "needs_work") {
622
795
  error(errors.status.cannotStart(foundTask.status));
623
796
  process.exit(EXIT_CODES.VALIDATION_FAILED); // Exit code 4 = invalid state
624
797
  }
625
798
  // Update status
626
799
  const updatedTask = {
627
800
  ...foundTask,
628
- status: 'in_progress',
801
+ status: "in_progress",
629
802
  started_at: new Date().toISOString(),
630
803
  };
631
804
  await saveTask(ctx, updatedTask);
632
- await commitIfShadow(ctx.shadow, 'task-start', foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
633
- success(`Started task: ${index.shortUlid(updatedTask._ulid)}`, { task: updatedTask });
805
+ await commitIfShadow(ctx.shadow, "task-start", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
806
+ success(`Started task: ${index.shortUlid(updatedTask._ulid)}`, {
807
+ task: updatedTask,
808
+ });
634
809
  // Show spec context and AC guidance (suppressed in JSON mode)
635
810
  if (!isJsonMode() && foundTask.spec_ref) {
636
811
  const specResult = index.resolve(foundTask.spec_ref);
637
812
  if (specResult.ok) {
638
- const specItem = items.find(i => i._ulid === specResult.ulid);
813
+ const specItem = items.find((i) => i._ulid === specResult.ulid);
639
814
  if (specItem) {
640
- console.log('');
641
- console.log('--- Spec Context ---');
815
+ console.log("");
816
+ console.log("--- Spec Context ---");
642
817
  console.log(`Implementing: ${specItem.title}`);
643
818
  if (specItem.description) {
644
819
  console.log(`\n${specItem.description}`);
645
820
  }
646
- if (specItem.acceptance_criteria && specItem.acceptance_criteria.length > 0) {
821
+ if (specItem.acceptance_criteria &&
822
+ specItem.acceptance_criteria.length > 0) {
647
823
  console.log(`\nAcceptance Criteria (${specItem.acceptance_criteria.length}):`);
648
824
  for (const ac of specItem.acceptance_criteria) {
649
825
  console.log(` [${ac.id}]`);
@@ -651,21 +827,21 @@ export function registerTaskCommands(program) {
651
827
  console.log(` When: ${ac.when}`);
652
828
  console.log(` Then: ${ac.then}`);
653
829
  }
654
- console.log('');
655
- console.log('Remember: Add test coverage for each AC and mark tests with // AC: @spec-ref ac-N');
830
+ console.log("");
831
+ console.log("Remember: Add test coverage for each AC and mark tests with // AC: @spec-ref ac-N");
656
832
  }
657
- console.log('');
833
+ console.log("");
658
834
  }
659
835
  }
660
836
  }
661
837
  // Sync spec implementation status (unless --no-sync)
662
838
  if (options.sync !== false && foundTask.spec_ref) {
663
- const updatedTasks = tasks.map(t => t._ulid === updatedTask._ulid ? { ...t, ...updatedTask } : t);
839
+ const updatedTasks = tasks.map((t) => t._ulid === updatedTask._ulid ? { ...t, ...updatedTask } : t);
664
840
  const syncResult = await syncSpecImplementationStatus(ctx, updatedTask, updatedTasks, items, index);
665
841
  if (syncResult) {
666
842
  info(`Synced spec "${syncResult.specTitle}" implementation: ${syncResult.previousStatus} -> ${syncResult.newStatus}`);
667
843
  // Commit the spec status change
668
- await commitIfShadow(ctx.shadow, 'spec-sync', syncResult.specUlid.slice(0, 8), `${syncResult.previousStatus} -> ${syncResult.newStatus}`);
844
+ await commitIfShadow(ctx.shadow, "spec-sync", syncResult.specUlid.slice(0, 8), `${syncResult.previousStatus} -> ${syncResult.newStatus}`);
669
845
  }
670
846
  }
671
847
  }
@@ -677,13 +853,18 @@ export function registerTaskCommands(program) {
677
853
  // kspec task complete <ref> | --refs <refs...>
678
854
  // AC: @multi-ref-batch ac-1 - Basic multi-ref syntax
679
855
  // AC: @multi-ref-batch ac-2 - Backward compatibility
680
- task
681
- .command('complete [ref]')
682
- .description('Complete a task (pending_review -> completed)')
683
- .option('--refs <refs...>', 'Complete multiple tasks by ref')
684
- .option('--reason <reason>', 'Completion reason/notes')
685
- .option('--skip-review', 'Skip review requirement (requires --reason)')
686
- .option('--no-sync', 'Skip syncing spec implementation status')
856
+ markMutating(task.command("complete [ref]"))
857
+ .description("Complete a task (pending_review -> completed)")
858
+ .option("--refs <refs...>", "Complete multiple tasks by ref")
859
+ .option("--reason <reason>", "Completion reason/notes")
860
+ .option("--skip-review", "Skip review requirement (requires --reason)")
861
+ .option("--force", "Force completion from any state (bypasses submit requirement)")
862
+ .option("--no-sync", "Skip syncing spec implementation status")
863
+ .addHelpText("after", `
864
+ Examples:
865
+ $ kspec task complete @task-slug --reason "Merged in PR #123"
866
+ $ kspec task complete @task-slug --force --reason "Design task, no code to review"
867
+ $ kspec task complete --refs @task1 @task2 --reason "Batch completion"`)
687
868
  .action(async (ref, options) => {
688
869
  try {
689
870
  const ctx = await initContext();
@@ -709,44 +890,46 @@ export function registerTaskCommands(program) {
709
890
  executeOperation: async (foundTask, { ctx, tasks, items, index, options }) => {
710
891
  try {
711
892
  // AC: @spec-completion-enforcement ac-6
712
- if (foundTask.status === 'completed') {
893
+ if (foundTask.status === "completed") {
713
894
  return {
714
895
  success: false,
715
896
  error: errors.status.completeAlreadyCompleted,
716
897
  };
717
898
  }
899
+ // AC: @task-commands ac-1 - Allow --force to bypass all state checks
900
+ const forcingCompletion = options.force;
718
901
  // AC: @spec-completion-enforcement ac-7 - Allow skip-review bypass
719
- if (!options.skipReview) {
902
+ if (!options.skipReview && !forcingCompletion) {
720
903
  // AC: @spec-completion-enforcement ac-2
721
- if (foundTask.status === 'in_progress') {
904
+ if (foundTask.status === "in_progress") {
722
905
  return {
723
906
  success: false,
724
907
  error: errors.status.completeRequiresReview,
725
908
  };
726
909
  }
727
910
  // AC: @spec-completion-enforcement ac-3
728
- if (foundTask.status === 'pending') {
911
+ if (foundTask.status === "pending") {
729
912
  return {
730
913
  success: false,
731
914
  error: errors.status.completeRequiresStart,
732
915
  };
733
916
  }
734
917
  // AC: @spec-completion-enforcement ac-4
735
- if (foundTask.status === 'blocked') {
918
+ if (foundTask.status === "blocked") {
736
919
  return {
737
920
  success: false,
738
921
  error: errors.status.completeBlockedTask,
739
922
  };
740
923
  }
741
924
  // AC: @spec-completion-enforcement ac-5
742
- if (foundTask.status === 'cancelled') {
925
+ if (foundTask.status === "cancelled") {
743
926
  return {
744
927
  success: false,
745
928
  error: errors.status.completeCancelledTask,
746
929
  };
747
930
  }
748
931
  // AC: @spec-completion-enforcement ac-1 - Only pending_review allowed
749
- if (foundTask.status !== 'pending_review') {
932
+ if (foundTask.status !== "pending_review") {
750
933
  return {
751
934
  success: false,
752
935
  error: errors.status.cannotComplete(foundTask.status),
@@ -758,44 +941,71 @@ export function registerTaskCommands(program) {
758
941
  // AC: @spec-completion-enforcement ac-author
759
942
  let taskNotes = foundTask.notes;
760
943
  if (options.skipReview && options.reason) {
761
- const skipNote = createNote(`Completed with --skip-review: ${options.reason}`, getAuthor());
944
+ const skipNote = createNote(`Completed with --skip-review: ${options.reason}`, getAuthor(ctx.config?.identity?.author));
762
945
  taskNotes = [...taskNotes, skipNote];
763
946
  }
947
+ // AC: @task-commands ac-1 - Document force completion from non-standard state
948
+ const forcedFromNonStandard = forcingCompletion &&
949
+ foundTask.status !== "pending_review";
950
+ let forceStateDetail;
951
+ if (forcedFromNonStandard) {
952
+ forceStateDetail = `from ${foundTask.status} state`;
953
+ if (foundTask.status === "blocked") {
954
+ const blockedBy = foundTask.blocked_by.join("; ");
955
+ forceStateDetail += `. Was blocked by: ${blockedBy || "(dependency-blocked)"}`;
956
+ }
957
+ let forceMessage = `Completed with --force ${forceStateDetail}`;
958
+ if (options.reason) {
959
+ forceMessage += `. Reason: ${options.reason}`;
960
+ }
961
+ const forceNote = createNote(forceMessage, getAuthor(ctx.config?.identity?.author));
962
+ taskNotes = [...taskNotes, forceNote];
963
+ }
764
964
  // Update status
765
965
  const updatedTask = {
766
966
  ...foundTask,
767
- status: 'completed',
967
+ status: "completed",
768
968
  completed_at: now,
769
969
  closed_reason: options.reason || null,
770
970
  started_at: foundTask.started_at || now,
771
971
  notes: taskNotes,
772
972
  };
773
973
  await saveTask(ctx, updatedTask);
774
- await commitIfShadow(ctx.shadow, 'task-complete', foundTask.slugs[0] || index.shortUlid(foundTask._ulid), options.reason);
974
+ await commitIfShadow(ctx.shadow, "task-complete", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), options.reason);
775
975
  // Sync spec implementation status (unless --no-sync)
776
976
  if (options.sync !== false && foundTask.spec_ref) {
777
- const updatedTasks = tasks.map(t => t._ulid === updatedTask._ulid ? { ...t, ...updatedTask } : t);
977
+ const updatedTasks = tasks.map((t) => t._ulid === updatedTask._ulid ? { ...t, ...updatedTask } : t);
778
978
  const syncResult = await syncSpecImplementationStatus(ctx, updatedTask, updatedTasks, items, index);
779
979
  if (syncResult && !isJsonMode()) {
780
980
  info(`Synced spec "${syncResult.specTitle}" implementation: ${syncResult.previousStatus} -> ${syncResult.newStatus}`);
781
- await commitIfShadow(ctx.shadow, 'spec-sync', syncResult.specUlid.slice(0, 8), `${syncResult.previousStatus} -> ${syncResult.newStatus}`);
981
+ await commitIfShadow(ctx.shadow, "spec-sync", syncResult.specUlid.slice(0, 8), `${syncResult.previousStatus} -> ${syncResult.newStatus}`);
782
982
  }
783
983
  }
784
984
  // Show AC reminder for single-ref mode only (not in batch)
785
985
  if (!options.refs && foundTask.spec_ref && !isJsonMode()) {
786
986
  const specResult = index.resolve(foundTask.spec_ref);
787
987
  if (specResult.ok && specResult.item) {
788
- const specItem = items.find(i => i._ulid === specResult.ulid);
789
- if (specItem && specItem.acceptance_criteria && specItem.acceptance_criteria.length > 0) {
988
+ const specItem = items.find((i) => i._ulid === specResult.ulid);
989
+ if (specItem?.acceptance_criteria &&
990
+ specItem.acceptance_criteria.length > 0) {
790
991
  const count = specItem.acceptance_criteria.length;
791
- console.log(`\n⚠ Linked spec ${foundTask.spec_ref} has ${count} acceptance criteri${count === 1 ? 'on' : 'a'} - verify they are covered\n`);
992
+ console.log(`\n⚠ Linked spec ${foundTask.spec_ref} has ${count} acceptance criteri${count === 1 ? "on" : "a"} - verify they are covered\n`);
792
993
  }
793
994
  }
794
995
  }
996
+ // AC: @task-commands ac-1 - Show warning when force-completing from non-standard state
997
+ let warningMsg;
998
+ if (forcedFromNonStandard) {
999
+ warningMsg = `Task was force-completed ${forceStateDetail}`;
1000
+ if (!isJsonMode()) {
1001
+ warn(warningMsg);
1002
+ }
1003
+ }
795
1004
  return {
796
1005
  success: true,
797
1006
  message: `Completed task: ${index.shortUlid(updatedTask._ulid)}`,
798
1007
  data: updatedTask,
1008
+ ...(warningMsg && { warning: warningMsg }),
799
1009
  };
800
1010
  }
801
1011
  catch (err) {
@@ -808,9 +1018,12 @@ export function registerTaskCommands(program) {
808
1018
  getUlid: (task) => task._ulid,
809
1019
  });
810
1020
  // AC: @multi-ref-batch ac-5, ac-6
811
- formatBatchOutput(result, 'Complete');
1021
+ formatBatchOutput(result, "Complete");
812
1022
  // Show commit guidance for single-ref mode only
813
- if (!options.refs && result.success && result.results.length === 1 && !isJsonMode()) {
1023
+ if (!options.refs &&
1024
+ result.success &&
1025
+ result.results.length === 1 &&
1026
+ !isJsonMode()) {
814
1027
  const taskData = result.results[0].data;
815
1028
  if (taskData) {
816
1029
  const guidance = formatCommitGuidance(taskData);
@@ -825,9 +1038,8 @@ export function registerTaskCommands(program) {
825
1038
  });
826
1039
  // kspec task submit <ref>
827
1040
  // Transitions in_progress → pending_review (code done, awaiting merge)
828
- task
829
- .command('submit <ref>')
830
- .description('Submit task for review (transitions to pending_review)')
1041
+ markMutating(task.command("submit <ref>"))
1042
+ .description("Submit task for review (transitions to pending_review)")
831
1043
  .action(async (ref) => {
832
1044
  try {
833
1045
  const ctx = await initContext();
@@ -835,16 +1047,16 @@ export function registerTaskCommands(program) {
835
1047
  const items = await loadAllItems(ctx);
836
1048
  const index = new ReferenceIndex(tasks, items);
837
1049
  const foundTask = resolveTaskRef(ref, tasks, index);
838
- if (foundTask.status !== 'in_progress') {
1050
+ if (foundTask.status !== "in_progress") {
839
1051
  error(`Cannot submit task with status: ${foundTask.status}. Task must be in_progress.`);
840
1052
  process.exit(EXIT_CODES.VALIDATION_FAILED);
841
1053
  }
842
1054
  const updatedTask = {
843
1055
  ...foundTask,
844
- status: 'pending_review',
1056
+ status: "pending_review",
845
1057
  };
846
1058
  await saveTask(ctx, updatedTask);
847
- await commitIfShadow(ctx.shadow, 'task-submit', foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
1059
+ await commitIfShadow(ctx.shadow, "task-submit", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
848
1060
  success(`Submitted task for review: ${index.shortUlid(updatedTask._ulid)}`, { task: updatedTask });
849
1061
  }
850
1062
  catch (err) {
@@ -852,11 +1064,44 @@ export function registerTaskCommands(program) {
852
1064
  process.exit(EXIT_CODES.ERROR);
853
1065
  }
854
1066
  });
1067
+ // kspec task needs-work <ref>
1068
+ // Reviewer kicks back a task for worker to fix
1069
+ markMutating(task.command("needs-work <ref>"))
1070
+ .description("Kick task back to worker for fixes (pending_review -> needs_work)")
1071
+ .requiredOption("--reason <reason>", "Description of issues found")
1072
+ .action(async (ref, options) => {
1073
+ try {
1074
+ const ctx = await initContext();
1075
+ const tasks = await loadAllTasks(ctx);
1076
+ const items = await loadAllItems(ctx);
1077
+ const index = new ReferenceIndex(tasks, items);
1078
+ const foundTask = resolveTaskRef(ref, tasks, index);
1079
+ if (foundTask.status !== "pending_review") {
1080
+ error(errors.status.cannotNeedsWork(foundTask.status));
1081
+ process.exit(EXIT_CODES.VALIDATION_FAILED);
1082
+ }
1083
+ // Track fix cycle count from existing kickback notes
1084
+ const existingKickbacks = foundTask.notes.filter((n) => n.content.includes("[FIX_CYCLE:")).length;
1085
+ const cycleNumber = existingKickbacks + 1;
1086
+ const note = createNote(`[FIX_CYCLE: ${cycleNumber}] Review findings: ${options.reason}`, getAuthor(ctx.config?.identity?.author));
1087
+ const updatedTask = {
1088
+ ...foundTask,
1089
+ status: "needs_work",
1090
+ notes: [...foundTask.notes, note],
1091
+ };
1092
+ await saveTask(ctx, updatedTask);
1093
+ await commitIfShadow(ctx.shadow, "task-needs-work", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), `cycle ${cycleNumber}`);
1094
+ success(`Kicked back task: ${index.shortUlid(updatedTask._ulid)} (fix cycle ${cycleNumber})`, { task: updatedTask });
1095
+ }
1096
+ catch (err) {
1097
+ error(errors.failures.needsWorkTask, err);
1098
+ process.exit(EXIT_CODES.ERROR);
1099
+ }
1100
+ });
855
1101
  // kspec task block <ref>
856
- task
857
- .command('block <ref>')
858
- .description('Block a task')
859
- .requiredOption('--reason <reason>', 'Reason for blocking')
1102
+ markMutating(task.command("block <ref>"))
1103
+ .description("Block a task")
1104
+ .requiredOption("--reason <reason>", "Reason for blocking")
860
1105
  .action(async (ref, options) => {
861
1106
  try {
862
1107
  const ctx = await initContext();
@@ -864,18 +1109,21 @@ export function registerTaskCommands(program) {
864
1109
  const items = await loadAllItems(ctx);
865
1110
  const index = new ReferenceIndex(tasks, items);
866
1111
  const foundTask = resolveTaskRef(ref, tasks, index);
867
- if (foundTask.status === 'completed' || foundTask.status === 'cancelled') {
1112
+ if (foundTask.status === "completed" ||
1113
+ foundTask.status === "cancelled") {
868
1114
  error(errors.status.cannotBlock(foundTask.status));
869
1115
  process.exit(EXIT_CODES.VALIDATION_FAILED);
870
1116
  }
871
1117
  const updatedTask = {
872
1118
  ...foundTask,
873
- status: 'blocked',
1119
+ status: "blocked",
874
1120
  blocked_by: [...foundTask.blocked_by, options.reason],
875
1121
  };
876
1122
  await saveTask(ctx, updatedTask);
877
- await commitIfShadow(ctx.shadow, 'task-block', foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
878
- success(`Blocked task: ${index.shortUlid(updatedTask._ulid)}`, { task: updatedTask });
1123
+ await commitIfShadow(ctx.shadow, "task-block", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
1124
+ success(`Blocked task: ${index.shortUlid(updatedTask._ulid)}`, {
1125
+ task: updatedTask,
1126
+ });
879
1127
  }
880
1128
  catch (err) {
881
1129
  error(errors.failures.blockTask, err);
@@ -883,9 +1131,8 @@ export function registerTaskCommands(program) {
883
1131
  }
884
1132
  });
885
1133
  // kspec task unblock <ref>
886
- task
887
- .command('unblock <ref>')
888
- .description('Unblock a task')
1134
+ markMutating(task.command("unblock <ref>"))
1135
+ .description("Unblock a task")
889
1136
  .action(async (ref) => {
890
1137
  try {
891
1138
  const ctx = await initContext();
@@ -893,18 +1140,20 @@ export function registerTaskCommands(program) {
893
1140
  const items = await loadAllItems(ctx);
894
1141
  const index = new ReferenceIndex(tasks, items);
895
1142
  const foundTask = resolveTaskRef(ref, tasks, index);
896
- if (foundTask.status !== 'blocked') {
897
- warn('Task is not blocked');
1143
+ if (foundTask.status !== "blocked") {
1144
+ warn("Task is not blocked");
898
1145
  return;
899
1146
  }
900
1147
  const updatedTask = {
901
1148
  ...foundTask,
902
- status: 'pending',
1149
+ status: "pending",
903
1150
  blocked_by: [],
904
1151
  };
905
1152
  await saveTask(ctx, updatedTask);
906
- await commitIfShadow(ctx.shadow, 'task-unblock', foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
907
- success(`Unblocked task: ${index.shortUlid(updatedTask._ulid)}`, { task: updatedTask });
1153
+ await commitIfShadow(ctx.shadow, "task-unblock", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
1154
+ success(`Unblocked task: ${index.shortUlid(updatedTask._ulid)}`, {
1155
+ task: updatedTask,
1156
+ });
908
1157
  }
909
1158
  catch (err) {
910
1159
  error(errors.failures.unblockTask, err);
@@ -913,11 +1162,14 @@ export function registerTaskCommands(program) {
913
1162
  });
914
1163
  // kspec task cancel <ref> | --refs <refs...>
915
1164
  // AC: @multi-ref-batch ac-1, ac-2
916
- task
917
- .command('cancel [ref]')
918
- .description('Cancel a task')
919
- .option('--refs <refs...>', 'Cancel multiple tasks by ref')
920
- .option('--reason <reason>', 'Cancellation reason')
1165
+ markMutating(task.command("cancel [ref]"))
1166
+ .description("Cancel a task")
1167
+ .option("--refs <refs...>", "Cancel multiple tasks by ref")
1168
+ .option("--reason <reason>", "Cancellation reason")
1169
+ .addHelpText("after", `
1170
+ Examples:
1171
+ $ kspec task cancel @task-slug --reason "No longer needed"
1172
+ $ kspec task cancel --refs @task1 @task2 --reason "Superseded by @new-task"`)
921
1173
  .action(async (ref, options) => {
922
1174
  try {
923
1175
  const ctx = await initContext();
@@ -936,7 +1188,8 @@ export function registerTaskCommands(program) {
936
1188
  },
937
1189
  executeOperation: async (foundTask, { ctx, index, options }) => {
938
1190
  try {
939
- if (foundTask.status === 'completed' || foundTask.status === 'cancelled') {
1191
+ if (foundTask.status === "completed" ||
1192
+ foundTask.status === "cancelled") {
940
1193
  return {
941
1194
  success: false,
942
1195
  error: `Task is already ${foundTask.status}`,
@@ -944,11 +1197,11 @@ export function registerTaskCommands(program) {
944
1197
  }
945
1198
  const updatedTask = {
946
1199
  ...foundTask,
947
- status: 'cancelled',
1200
+ status: "cancelled",
948
1201
  closed_reason: options.reason || null,
949
1202
  };
950
1203
  await saveTask(ctx, updatedTask);
951
- await commitIfShadow(ctx.shadow, 'task-cancel', foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
1204
+ await commitIfShadow(ctx.shadow, "task-cancel", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
952
1205
  return {
953
1206
  success: true,
954
1207
  message: `Cancelled task: ${index.shortUlid(updatedTask._ulid)}`,
@@ -964,7 +1217,7 @@ export function registerTaskCommands(program) {
964
1217
  },
965
1218
  getUlid: (task) => task._ulid,
966
1219
  });
967
- formatBatchOutput(result, 'Cancel');
1220
+ formatBatchOutput(result, "Cancel");
968
1221
  }
969
1222
  catch (err) {
970
1223
  error(errors.failures.cancelTask, err);
@@ -973,9 +1226,8 @@ export function registerTaskCommands(program) {
973
1226
  });
974
1227
  // kspec task reset <ref>
975
1228
  // AC: @spec-task-reset ac-1, ac-2, ac-3, ac-4, ac-5, ac-6
976
- task
977
- .command('reset <ref>')
978
- .description('Reset a task to pending state')
1229
+ markMutating(task.command("reset <ref>"))
1230
+ .description("Reset a task to pending state")
979
1231
  .action(async (ref) => {
980
1232
  try {
981
1233
  const ctx = await initContext();
@@ -984,72 +1236,81 @@ export function registerTaskCommands(program) {
984
1236
  const index = new ReferenceIndex(tasks, items);
985
1237
  const foundTask = resolveTaskRef(ref, tasks, index);
986
1238
  // AC: @spec-task-reset ac-2 - Error if already pending
987
- if (foundTask.status === 'pending') {
988
- error('Task is already pending');
1239
+ if (foundTask.status === "pending") {
1240
+ error("Task is already pending");
989
1241
  process.exit(EXIT_CODES.VALIDATION_FAILED);
990
1242
  }
991
1243
  // Track previous status and reason for note (AC-4)
992
1244
  const previousStatus = foundTask.status;
993
- const hadCancelReason = foundTask.closed_reason && foundTask.status === 'cancelled';
994
- const cancelReasonText = hadCancelReason ? ` (was cancelled: ${foundTask.closed_reason})` : '';
1245
+ const hadCancelReason = foundTask.closed_reason && foundTask.status === "cancelled";
1246
+ const cancelReasonText = hadCancelReason
1247
+ ? ` (was cancelled: ${foundTask.closed_reason})`
1248
+ : "";
995
1249
  // AC: @spec-task-reset ac-1 - Reset to pending, clear completion-related fields
996
1250
  const clearedFields = [];
997
1251
  const updatedTask = {
998
1252
  ...foundTask,
999
- status: 'pending',
1253
+ status: "pending",
1000
1254
  };
1001
1255
  // Clear timestamps and reasons based on previous status
1002
- if (foundTask.completed_at !== undefined && foundTask.completed_at !== null) {
1256
+ if (foundTask.completed_at !== undefined &&
1257
+ foundTask.completed_at !== null) {
1003
1258
  updatedTask.completed_at = null;
1004
- clearedFields.push('completed_at');
1259
+ clearedFields.push("completed_at");
1005
1260
  }
1006
- if (foundTask.started_at !== undefined && foundTask.started_at !== null) {
1261
+ if (foundTask.started_at !== undefined &&
1262
+ foundTask.started_at !== null) {
1007
1263
  updatedTask.started_at = null;
1008
- clearedFields.push('started_at');
1264
+ clearedFields.push("started_at");
1009
1265
  }
1010
- if (foundTask.closed_reason !== undefined && foundTask.closed_reason !== null) {
1266
+ if (foundTask.closed_reason !== undefined &&
1267
+ foundTask.closed_reason !== null) {
1011
1268
  updatedTask.closed_reason = null;
1012
- clearedFields.push('closed_reason');
1269
+ clearedFields.push("closed_reason");
1013
1270
  }
1014
1271
  if (foundTask.blocked_by.length > 0) {
1015
1272
  updatedTask.blocked_by = [];
1016
- clearedFields.push('blocked_by');
1273
+ clearedFields.push("blocked_by");
1017
1274
  }
1018
1275
  // AC: @spec-task-reset ac-4 - Add note documenting the reset
1019
1276
  // AC: @spec-task-reset ac-author
1020
1277
  const noteContent = `Reset from ${previousStatus} to pending${cancelReasonText}`;
1021
- const note = createNote(noteContent, getAuthor());
1278
+ const note = createNote(noteContent, getAuthor(ctx.config?.identity?.author));
1022
1279
  updatedTask.notes = [...updatedTask.notes, note];
1023
1280
  await saveTask(ctx, updatedTask);
1024
1281
  // AC: @spec-task-reset ac-3 - Shadow commit with message task-reset
1025
- await commitIfShadow(ctx.shadow, 'task-reset', foundTask.slugs[0] || index.shortUlid(foundTask._ulid), `from ${previousStatus}`);
1282
+ await commitIfShadow(ctx.shadow, "task-reset", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), `from ${previousStatus}`);
1026
1283
  // AC: @spec-task-reset ac-6 - JSON output includes previous_status, new_status, cleared_fields
1027
1284
  const jsonOutput = {
1028
1285
  task: updatedTask,
1029
1286
  previous_status: previousStatus,
1030
- new_status: 'pending',
1287
+ new_status: "pending",
1031
1288
  cleared_fields: clearedFields,
1032
1289
  };
1033
1290
  output(jsonOutput, () => {
1034
1291
  success(`Reset task: ${index.shortUlid(updatedTask._ulid)} (${previousStatus} → pending)`, undefined);
1035
1292
  if (clearedFields.length > 0) {
1036
- info(`Cleared fields: ${clearedFields.join(', ')}`);
1293
+ info(`Cleared fields: ${clearedFields.join(", ")}`);
1037
1294
  }
1038
1295
  });
1039
1296
  }
1040
1297
  catch (err) {
1041
- error('Failed to reset task', err);
1298
+ error("Failed to reset task", err);
1042
1299
  process.exit(EXIT_CODES.ERROR);
1043
1300
  }
1044
1301
  });
1045
1302
  // kspec task delete <ref> | --refs <refs...>
1046
1303
  // AC: @multi-ref-batch ac-1, ac-2
1047
- task
1048
- .command('delete [ref]')
1049
- .description('Delete a task permanently')
1050
- .option('--refs <refs...>', 'Delete multiple tasks by ref')
1051
- .option('--force', 'Skip confirmation (required for --refs)')
1052
- .option('--dry-run', 'Show what would be deleted without deleting')
1304
+ markMutating(task.command("delete [ref]"))
1305
+ .description("Delete a task permanently")
1306
+ .option("--refs <refs...>", "Delete multiple tasks by ref")
1307
+ .option("--force", "Skip confirmation (required for --refs)")
1308
+ .option("--dry-run", "Show what would be deleted without deleting")
1309
+ .addHelpText("after", `
1310
+ Examples:
1311
+ $ kspec task delete @task-slug
1312
+ $ kspec task delete --refs @task1 @task2 --force
1313
+ $ kspec task delete --refs @task1 @task2 --dry-run`)
1053
1314
  .action(async (ref, options) => {
1054
1315
  try {
1055
1316
  const ctx = await initContext();
@@ -1057,8 +1318,11 @@ export function registerTaskCommands(program) {
1057
1318
  const items = await loadAllItems(ctx);
1058
1319
  const index = new ReferenceIndex(tasks, items);
1059
1320
  // For batch mode (--refs), require --force
1060
- if (options.refs && options.refs.length > 0 && !options.force && !options.dryRun) {
1061
- error('Batch delete requires --force flag');
1321
+ if (options.refs &&
1322
+ options.refs.length > 0 &&
1323
+ !options.force &&
1324
+ !options.dryRun) {
1325
+ error("Batch delete requires --force flag");
1062
1326
  process.exit(EXIT_CODES.USAGE_ERROR);
1063
1327
  }
1064
1328
  const result = await executeBatchOperation({
@@ -1082,7 +1346,7 @@ export function registerTaskCommands(program) {
1082
1346
  }
1083
1347
  // For single-ref mode (not --refs), prompt for confirmation unless --force
1084
1348
  if (!options.refs && !options.force) {
1085
- const readline = await import('readline');
1349
+ const readline = await import("node:readline");
1086
1350
  const rl = readline.createInterface({
1087
1351
  input: process.stdin,
1088
1352
  output: process.stdout,
@@ -1091,15 +1355,15 @@ export function registerTaskCommands(program) {
1091
1355
  rl.question(`Delete task "${taskDisplay}"? [y/N] `, resolve);
1092
1356
  });
1093
1357
  rl.close();
1094
- if (answer.toLowerCase() !== 'y') {
1358
+ if (answer.toLowerCase() !== "y") {
1095
1359
  return {
1096
1360
  success: false,
1097
- error: 'Deletion cancelled by user',
1361
+ error: "Deletion cancelled by user",
1098
1362
  };
1099
1363
  }
1100
1364
  }
1101
1365
  await deleteTask(ctx, foundTask);
1102
- await commitIfShadow(ctx.shadow, 'task-delete', foundTask.slugs[0] || index.shortUlid(foundTask._ulid), foundTask.title);
1366
+ await commitIfShadow(ctx.shadow, "task-delete", foundTask.slugs[0] || index.shortUlid(foundTask._ulid), foundTask.title);
1103
1367
  return {
1104
1368
  success: true,
1105
1369
  message: `Deleted task: ${taskDisplay}`,
@@ -1114,7 +1378,7 @@ export function registerTaskCommands(program) {
1114
1378
  },
1115
1379
  getUlid: (task) => task._ulid,
1116
1380
  });
1117
- formatBatchOutput(result, 'Delete');
1381
+ formatBatchOutput(result, "Delete");
1118
1382
  }
1119
1383
  catch (err) {
1120
1384
  error(errors.failures.deleteTask, err);
@@ -1122,11 +1386,10 @@ export function registerTaskCommands(program) {
1122
1386
  }
1123
1387
  });
1124
1388
  // kspec task note <ref> <message>
1125
- task
1126
- .command('note <ref> <message>')
1127
- .description('Add a note to a task')
1128
- .option('--author <author>', 'Note author')
1129
- .option('--supersedes <ulid>', 'ULID of note this supersedes')
1389
+ markMutating(task.command("note <ref> <message>"))
1390
+ .description("Add a note to a task")
1391
+ .option("--author <author>", "Note author")
1392
+ .option("--supersedes <ulid>", "ULID of note this supersedes")
1130
1393
  .action(async (ref, message, options) => {
1131
1394
  try {
1132
1395
  const ctx = await initContext();
@@ -1140,11 +1403,13 @@ export function registerTaskCommands(program) {
1140
1403
  notes: [...foundTask.notes, note],
1141
1404
  };
1142
1405
  await saveTask(ctx, updatedTask);
1143
- await commitIfShadow(ctx.shadow, 'task-note', foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
1144
- success(`Added note to task: ${index.shortUlid(updatedTask._ulid)}`, { note });
1406
+ await commitIfShadow(ctx.shadow, "task-note", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
1407
+ success(`Added note to task: ${index.shortUlid(updatedTask._ulid)}`, {
1408
+ note,
1409
+ });
1145
1410
  // Proactive alignment guidance for tasks with spec_ref
1146
1411
  if (foundTask.spec_ref) {
1147
- console.log('');
1412
+ console.log("");
1148
1413
  console.log(alignmentCheck.header);
1149
1414
  console.log(alignmentCheck.beyondSpec);
1150
1415
  console.log(alignmentCheck.updateSpec(foundTask.spec_ref));
@@ -1153,8 +1418,9 @@ export function registerTaskCommands(program) {
1153
1418
  const specResult = index.resolve(foundTask.spec_ref);
1154
1419
  if (specResult.ok && specResult.item) {
1155
1420
  const specItem = specResult.item;
1156
- if (specItem.acceptance_criteria && specItem.acceptance_criteria.length > 0) {
1157
- console.log('');
1421
+ if (specItem.acceptance_criteria &&
1422
+ specItem.acceptance_criteria.length > 0) {
1423
+ console.log("");
1158
1424
  console.log(alignmentCheck.testCoverage(specItem.acceptance_criteria.length));
1159
1425
  }
1160
1426
  }
@@ -1167,8 +1433,8 @@ export function registerTaskCommands(program) {
1167
1433
  });
1168
1434
  // kspec task notes <ref>
1169
1435
  task
1170
- .command('notes <ref>')
1171
- .description('Show notes for a task')
1436
+ .command("notes <ref>")
1437
+ .description("Show notes for a task")
1172
1438
  .action(async (ref) => {
1173
1439
  try {
1174
1440
  const ctx = await initContext();
@@ -1178,14 +1444,14 @@ export function registerTaskCommands(program) {
1178
1444
  const foundTask = resolveTaskRef(ref, tasks, index);
1179
1445
  output(foundTask.notes, () => {
1180
1446
  if (foundTask.notes.length === 0) {
1181
- console.log('No notes');
1447
+ console.log("No notes");
1182
1448
  }
1183
1449
  else {
1184
1450
  for (const note of foundTask.notes) {
1185
- const author = note.author || 'unknown';
1451
+ const author = note.author || "unknown";
1186
1452
  console.log(`[${note.created_at}] ${author}:`);
1187
1453
  console.log(note.content);
1188
- console.log('');
1454
+ console.log("");
1189
1455
  }
1190
1456
  }
1191
1457
  });
@@ -1197,8 +1463,8 @@ export function registerTaskCommands(program) {
1197
1463
  });
1198
1464
  // kspec task review <ref>
1199
1465
  task
1200
- .command('review <ref>')
1201
- .description('Get task context for review (task details, spec, ACs, git diff)')
1466
+ .command("review <ref>")
1467
+ .description("Get task context for review (task details, spec, ACs, git diff)")
1202
1468
  .action(async (ref) => {
1203
1469
  try {
1204
1470
  const ctx = await initContext();
@@ -1207,42 +1473,7 @@ export function registerTaskCommands(program) {
1207
1473
  const index = new ReferenceIndex(tasks, items);
1208
1474
  const foundTask = resolveTaskRef(ref, tasks, index);
1209
1475
  // Import getDiffSince from utils
1210
- const { getDiffSince } = await import('../../utils/index.js');
1211
- // Import scanTestCoverage (we'll need to export it from validate.ts)
1212
- // For now, duplicate the logic here
1213
- const scanTestCoverage = async (rootDir) => {
1214
- const coveredACs = new Set();
1215
- const testsDir = path.join(rootDir, 'tests');
1216
- const fs = await import('node:fs/promises');
1217
- try {
1218
- await fs.access(testsDir);
1219
- const files = await fs.readdir(testsDir);
1220
- const testFiles = files.filter(f => f.endsWith('.test.ts') || f.endsWith('.test.js'));
1221
- for (const file of testFiles) {
1222
- const filePath = path.join(testsDir, file);
1223
- const content = await fs.readFile(filePath, 'utf-8');
1224
- const acPattern = /\/\/\s*AC:\s*(@[\w-]+)(?:\s+(ac-\d+(?:\s*,\s*ac-\d+)*))?/g;
1225
- let match;
1226
- while ((match = acPattern.exec(content)) !== null) {
1227
- const specRef = match[1];
1228
- const acList = match[2];
1229
- if (acList) {
1230
- const acs = acList.split(',').map(ac => ac.trim());
1231
- for (const ac of acs) {
1232
- coveredACs.add(`${specRef} ${ac}`);
1233
- }
1234
- }
1235
- else {
1236
- coveredACs.add(specRef);
1237
- }
1238
- }
1239
- }
1240
- }
1241
- catch (err) {
1242
- // Tests directory doesn't exist or can't be read
1243
- }
1244
- return coveredACs;
1245
- };
1476
+ const { getDiffSince } = await import("../../utils/index.js");
1246
1477
  // Gather review context
1247
1478
  const reviewContext = {
1248
1479
  task: foundTask,
@@ -1254,10 +1485,11 @@ export function registerTaskCommands(program) {
1254
1485
  if (foundTask.spec_ref) {
1255
1486
  const specResult = index.resolve(foundTask.spec_ref);
1256
1487
  if (specResult.ok) {
1257
- const specItem = items.find(i => i._ulid === specResult.ulid);
1488
+ const specItem = items.find((i) => i._ulid === specResult.ulid);
1258
1489
  reviewContext.spec = specItem || null;
1259
1490
  // Check test coverage for ACs if spec has them
1260
- if (specItem && specItem.acceptance_criteria && specItem.acceptance_criteria.length > 0) {
1491
+ if (specItem?.acceptance_criteria &&
1492
+ specItem.acceptance_criteria.length > 0) {
1261
1493
  const coveredACs = await scanTestCoverage(ctx.rootDir);
1262
1494
  const covered = [];
1263
1495
  const uncovered = [];
@@ -1270,7 +1502,7 @@ export function registerTaskCommands(program) {
1270
1502
  }
1271
1503
  possibleRefs.push(`@${specItem._ulid.slice(0, 8)} ${ac.id}`);
1272
1504
  possibleRefs.push(`@${specItem._ulid.slice(0, 8)}`);
1273
- const isCovered = possibleRefs.some(ref => coveredACs.has(ref));
1505
+ const isCovered = possibleRefs.some((ref) => coveredACs.has(ref));
1274
1506
  if (isCovered) {
1275
1507
  covered.push(ac.id);
1276
1508
  }
@@ -1288,29 +1520,32 @@ export function registerTaskCommands(program) {
1288
1520
  reviewContext.diff = getDiffSince(startedDate, ctx.rootDir);
1289
1521
  }
1290
1522
  output(reviewContext, () => {
1291
- console.log('='.repeat(60));
1292
- console.log('Task Review Context');
1293
- console.log('='.repeat(60));
1523
+ console.log("=".repeat(60));
1524
+ console.log("Task Review Context");
1525
+ console.log("=".repeat(60));
1294
1526
  console.log();
1295
1527
  // Task details
1296
- console.log('TASK DETAILS');
1297
- console.log('-'.repeat(60));
1528
+ console.log("TASK DETAILS");
1529
+ console.log("-".repeat(60));
1298
1530
  console.log(formatTaskDetails(foundTask, index));
1299
1531
  console.log();
1300
1532
  // Spec details
1301
1533
  if (reviewContext.spec) {
1302
- console.log('LINKED SPEC');
1303
- console.log('-'.repeat(60));
1534
+ console.log("LINKED SPEC");
1535
+ console.log("-".repeat(60));
1304
1536
  console.log(`Title: ${reviewContext.spec.title}`);
1305
1537
  console.log(`Type: ${reviewContext.spec.type}`);
1306
1538
  if (reviewContext.spec.description) {
1307
1539
  console.log(`\nDescription:\n${reviewContext.spec.description}`);
1308
1540
  }
1309
- if (reviewContext.spec.acceptance_criteria && reviewContext.spec.acceptance_criteria.length > 0) {
1541
+ if (reviewContext.spec.acceptance_criteria &&
1542
+ reviewContext.spec.acceptance_criteria.length > 0) {
1310
1543
  console.log(`\nAcceptance Criteria (${reviewContext.spec.acceptance_criteria.length}):`);
1311
1544
  for (const ac of reviewContext.spec.acceptance_criteria) {
1312
1545
  const isCovered = reviewContext.testCoverage?.covered.includes(ac.id);
1313
- const coverageMarker = isCovered ? chalk.green('✓') : chalk.yellow('○');
1546
+ const coverageMarker = isCovered
1547
+ ? chalk.green("✓")
1548
+ : chalk.yellow("○");
1314
1549
  console.log(` ${coverageMarker} [${ac.id}]`);
1315
1550
  console.log(` Given: ${ac.given}`);
1316
1551
  console.log(` When: ${ac.when}`);
@@ -1325,7 +1560,7 @@ export function registerTaskCommands(program) {
1325
1560
  }
1326
1561
  else {
1327
1562
  console.log(chalk.yellow(` Test coverage: ${covered.length}/${covered.length + uncovered.length} ACs covered`));
1328
- console.log(chalk.yellow(` Missing coverage for: ${uncovered.join(', ')}`));
1563
+ console.log(chalk.yellow(` Missing coverage for: ${uncovered.join(", ")}`));
1329
1564
  }
1330
1565
  }
1331
1566
  }
@@ -1333,40 +1568,40 @@ export function registerTaskCommands(program) {
1333
1568
  }
1334
1569
  // Git diff
1335
1570
  if (reviewContext.diff) {
1336
- console.log('CHANGES SINCE TASK STARTED');
1337
- console.log('-'.repeat(60));
1571
+ console.log("CHANGES SINCE TASK STARTED");
1572
+ console.log("-".repeat(60));
1338
1573
  console.log(`Started at: ${foundTask.started_at}`);
1339
1574
  console.log();
1340
1575
  console.log(reviewContext.diff);
1341
1576
  console.log();
1342
1577
  }
1343
1578
  else if (foundTask.started_at) {
1344
- console.log('CHANGES SINCE TASK STARTED');
1345
- console.log('-'.repeat(60));
1579
+ console.log("CHANGES SINCE TASK STARTED");
1580
+ console.log("-".repeat(60));
1346
1581
  console.log(`Started at: ${foundTask.started_at}`);
1347
- console.log('No changes detected');
1582
+ console.log("No changes detected");
1348
1583
  console.log();
1349
1584
  }
1350
- console.log('='.repeat(60));
1351
- console.log('Review Checklist:');
1352
- console.log('- Does the implementation match the task description?');
1585
+ console.log("=".repeat(60));
1586
+ console.log("Review Checklist:");
1587
+ console.log("- Does the implementation match the task description?");
1353
1588
  if (reviewContext.spec) {
1354
- console.log('- Are all acceptance criteria covered?');
1355
- console.log('- Is test coverage adequate?');
1589
+ console.log("- Are all acceptance criteria covered?");
1590
+ console.log("- Is test coverage adequate?");
1356
1591
  }
1357
- console.log('- Are there any gaps or issues?');
1358
- console.log('='.repeat(60));
1592
+ console.log("- Are there any gaps or issues?");
1593
+ console.log("=".repeat(60));
1359
1594
  });
1360
1595
  }
1361
1596
  catch (err) {
1362
- error('Failed to generate review context', err);
1597
+ error("Failed to generate review context", err);
1363
1598
  process.exit(EXIT_CODES.ERROR);
1364
1599
  }
1365
1600
  });
1366
1601
  // kspec task todos <ref>
1367
1602
  task
1368
- .command('todos <ref>')
1369
- .description('Show todos for a task')
1603
+ .command("todos <ref>")
1604
+ .description("Show todos for a task")
1370
1605
  .action(async (ref) => {
1371
1606
  try {
1372
1607
  const ctx = await initContext();
@@ -1376,12 +1611,12 @@ export function registerTaskCommands(program) {
1376
1611
  const foundTask = resolveTaskRef(ref, tasks, index);
1377
1612
  output(foundTask.todos, () => {
1378
1613
  if (foundTask.todos.length === 0) {
1379
- console.log('No todos');
1614
+ console.log("No todos");
1380
1615
  }
1381
1616
  else {
1382
1617
  for (const todo of foundTask.todos) {
1383
- const status = todo.done ? '[x]' : '[ ]';
1384
- const doneInfo = todo.done && todo.done_at ? ` (done ${todo.done_at})` : '';
1618
+ const status = todo.done ? "[x]" : "[ ]";
1619
+ const doneInfo = todo.done && todo.done_at ? ` (done ${todo.done_at})` : "";
1385
1620
  console.log(`${status} ${todo.id}. ${todo.text}${doneInfo}`);
1386
1621
  }
1387
1622
  }
@@ -1393,14 +1628,11 @@ export function registerTaskCommands(program) {
1393
1628
  }
1394
1629
  });
1395
1630
  // Create subcommand group for todo operations
1396
- const todoCmd = task
1397
- .command('todo')
1398
- .description('Manage task todos');
1631
+ const todoCmd = task.command("todo").description("Manage task todos");
1399
1632
  // kspec task todo add <ref> <text>
1400
- todoCmd
1401
- .command('add <ref> <text>')
1402
- .description('Add a todo to a task')
1403
- .option('--author <author>', 'Todo author')
1633
+ markMutating(todoCmd.command("add <ref> <text>"))
1634
+ .description("Add a todo to a task")
1635
+ .option("--author <author>", "Todo author")
1404
1636
  .action(async (ref, text, options) => {
1405
1637
  try {
1406
1638
  const ctx = await initContext();
@@ -1410,7 +1642,7 @@ export function registerTaskCommands(program) {
1410
1642
  const foundTask = resolveTaskRef(ref, tasks, index);
1411
1643
  // Calculate next ID (max existing + 1, or 1 if none)
1412
1644
  const nextId = foundTask.todos.length > 0
1413
- ? Math.max(...foundTask.todos.map(t => t.id)) + 1
1645
+ ? Math.max(...foundTask.todos.map((t) => t.id)) + 1
1414
1646
  : 1;
1415
1647
  const todo = createTodo(nextId, text, options.author);
1416
1648
  const updatedTask = {
@@ -1418,7 +1650,7 @@ export function registerTaskCommands(program) {
1418
1650
  todos: [...foundTask.todos, todo],
1419
1651
  };
1420
1652
  await saveTask(ctx, updatedTask);
1421
- await commitIfShadow(ctx.shadow, 'task-note', foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
1653
+ await commitIfShadow(ctx.shadow, "task-note", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
1422
1654
  success(`Added todo #${todo.id} to task: ${index.shortUlid(updatedTask._ulid)}`, { todo });
1423
1655
  }
1424
1656
  catch (err) {
@@ -1427,9 +1659,8 @@ export function registerTaskCommands(program) {
1427
1659
  }
1428
1660
  });
1429
1661
  // kspec task todo done <ref> <id>
1430
- todoCmd
1431
- .command('done <ref> <id>')
1432
- .description('Mark a todo as done')
1662
+ markMutating(todoCmd.command("done <ref> <id>"))
1663
+ .description("Mark a todo as done")
1433
1664
  .action(async (ref, idStr) => {
1434
1665
  try {
1435
1666
  const ctx = await initContext();
@@ -1438,11 +1669,11 @@ export function registerTaskCommands(program) {
1438
1669
  const index = new ReferenceIndex(tasks, items);
1439
1670
  const foundTask = resolveTaskRef(ref, tasks, index);
1440
1671
  const id = parseInt(idStr, 10);
1441
- if (isNaN(id)) {
1672
+ if (Number.isNaN(id)) {
1442
1673
  error(errors.todo.invalidId(idStr));
1443
1674
  process.exit(EXIT_CODES.USAGE_ERROR);
1444
1675
  }
1445
- const todoIndex = foundTask.todos.findIndex(t => t.id === id);
1676
+ const todoIndex = foundTask.todos.findIndex((t) => t.id === id);
1446
1677
  if (todoIndex === -1) {
1447
1678
  error(errors.todo.notFound(id));
1448
1679
  process.exit(EXIT_CODES.NOT_FOUND);
@@ -1462,8 +1693,10 @@ export function registerTaskCommands(program) {
1462
1693
  todos: updatedTodos,
1463
1694
  };
1464
1695
  await saveTask(ctx, updatedTask);
1465
- await commitIfShadow(ctx.shadow, 'task-note', foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
1466
- success(`Marked todo #${id} as done`, { todo: updatedTodos[todoIndex] });
1696
+ await commitIfShadow(ctx.shadow, "task-note", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
1697
+ success(`Marked todo #${id} as done`, {
1698
+ todo: updatedTodos[todoIndex],
1699
+ });
1467
1700
  }
1468
1701
  catch (err) {
1469
1702
  error(errors.failures.markTodoDone, err);
@@ -1471,9 +1704,8 @@ export function registerTaskCommands(program) {
1471
1704
  }
1472
1705
  });
1473
1706
  // kspec task todo undone <ref> <id>
1474
- todoCmd
1475
- .command('undone <ref> <id>')
1476
- .description('Mark a todo as not done')
1707
+ markMutating(todoCmd.command("undone <ref> <id>"))
1708
+ .description("Mark a todo as not done")
1477
1709
  .action(async (ref, idStr) => {
1478
1710
  try {
1479
1711
  const ctx = await initContext();
@@ -1482,11 +1714,11 @@ export function registerTaskCommands(program) {
1482
1714
  const index = new ReferenceIndex(tasks, items);
1483
1715
  const foundTask = resolveTaskRef(ref, tasks, index);
1484
1716
  const id = parseInt(idStr, 10);
1485
- if (isNaN(id)) {
1717
+ if (Number.isNaN(id)) {
1486
1718
  error(errors.todo.invalidId(idStr));
1487
1719
  process.exit(EXIT_CODES.USAGE_ERROR);
1488
1720
  }
1489
- const todoIndex = foundTask.todos.findIndex(t => t.id === id);
1721
+ const todoIndex = foundTask.todos.findIndex((t) => t.id === id);
1490
1722
  if (todoIndex === -1) {
1491
1723
  error(errors.todo.notFound(id));
1492
1724
  process.exit(EXIT_CODES.NOT_FOUND);
@@ -1506,8 +1738,10 @@ export function registerTaskCommands(program) {
1506
1738
  todos: updatedTodos,
1507
1739
  };
1508
1740
  await saveTask(ctx, updatedTask);
1509
- await commitIfShadow(ctx.shadow, 'task-note', foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
1510
- success(`Marked todo #${id} as not done`, { todo: updatedTodos[todoIndex] });
1741
+ await commitIfShadow(ctx.shadow, "task-note", foundTask.slugs[0] || index.shortUlid(foundTask._ulid));
1742
+ success(`Marked todo #${id} as not done`, {
1743
+ todo: updatedTodos[todoIndex],
1744
+ });
1511
1745
  }
1512
1746
  catch (err) {
1513
1747
  error(errors.failures.markTodoNotDone, err);