@kynetic-ai/spec 0.1.2 → 0.3.0

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