@seanmars/tospec 0.19.0-beta.0 → 0.19.0-beta.13

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 (431) hide show
  1. package/CHANGELOG.md +578 -0
  2. package/README.md +71 -77
  3. package/assets/dashboard/app.js +14 -3
  4. package/assets/dashboard/style.css +7 -0
  5. package/assets/metrics/app.js +22 -0
  6. package/assets/metrics/style.css +8 -0
  7. package/assets/rules/tospec/decision.md +3 -0
  8. package/assets/rules/tospec/single-source-of-truth.md +19 -0
  9. package/dist/cli/index.d.ts.map +1 -1
  10. package/dist/cli/index.js +266 -82
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/commands/config.d.ts +9 -17
  13. package/dist/commands/config.d.ts.map +1 -1
  14. package/dist/commands/config.js +374 -95
  15. package/dist/commands/config.js.map +1 -1
  16. package/dist/commands/dashboard.d.ts +64 -81
  17. package/dist/commands/dashboard.d.ts.map +1 -1
  18. package/dist/commands/dashboard.js +350 -218
  19. package/dist/commands/dashboard.js.map +1 -1
  20. package/dist/commands/decision.d.ts +41 -19
  21. package/dist/commands/decision.d.ts.map +1 -1
  22. package/dist/commands/decision.js +400 -70
  23. package/dist/commands/decision.js.map +1 -1
  24. package/dist/commands/metrics.d.ts +34 -48
  25. package/dist/commands/metrics.d.ts.map +1 -1
  26. package/dist/commands/metrics.js +74 -84
  27. package/dist/commands/metrics.js.map +1 -1
  28. package/dist/commands/shared-output.d.ts +24 -10
  29. package/dist/commands/shared-output.d.ts.map +1 -1
  30. package/dist/commands/shared-output.js +62 -11
  31. package/dist/commands/shared-output.js.map +1 -1
  32. package/dist/commands/show.d.ts +7 -0
  33. package/dist/commands/show.d.ts.map +1 -1
  34. package/dist/commands/show.js +39 -8
  35. package/dist/commands/show.js.map +1 -1
  36. package/dist/commands/validate.d.ts +47 -30
  37. package/dist/commands/validate.d.ts.map +1 -1
  38. package/dist/commands/validate.js +282 -108
  39. package/dist/commands/validate.js.map +1 -1
  40. package/dist/commands/workflow/index.d.ts +6 -10
  41. package/dist/commands/workflow/index.d.ts.map +1 -1
  42. package/dist/commands/workflow/index.js +6 -10
  43. package/dist/commands/workflow/index.js.map +1 -1
  44. package/dist/commands/workflow/instructions.d.ts +21 -8
  45. package/dist/commands/workflow/instructions.d.ts.map +1 -1
  46. package/dist/commands/workflow/instructions.js +254 -95
  47. package/dist/commands/workflow/instructions.js.map +1 -1
  48. package/dist/commands/workflow/new-change.d.ts +4 -5
  49. package/dist/commands/workflow/new-change.d.ts.map +1 -1
  50. package/dist/commands/workflow/new-change.js +90 -25
  51. package/dist/commands/workflow/new-change.js.map +1 -1
  52. package/dist/commands/workflow/schemas.d.ts +3 -5
  53. package/dist/commands/workflow/schemas.d.ts.map +1 -1
  54. package/dist/commands/workflow/schemas.js +37 -11
  55. package/dist/commands/workflow/schemas.js.map +1 -1
  56. package/dist/commands/workflow/shared.d.ts +48 -21
  57. package/dist/commands/workflow/shared.d.ts.map +1 -1
  58. package/dist/commands/workflow/shared.js +36 -33
  59. package/dist/commands/workflow/shared.js.map +1 -1
  60. package/dist/commands/workflow/status.d.ts +10 -6
  61. package/dist/commands/workflow/status.d.ts.map +1 -1
  62. package/dist/commands/workflow/status.js +84 -42
  63. package/dist/commands/workflow/status.js.map +1 -1
  64. package/dist/commands/workflow/templates.d.ts +10 -3
  65. package/dist/commands/workflow/templates.d.ts.map +1 -1
  66. package/dist/commands/workflow/templates.js +39 -40
  67. package/dist/commands/workflow/templates.js.map +1 -1
  68. package/dist/core/archive.d.ts +26 -21
  69. package/dist/core/archive.d.ts.map +1 -1
  70. package/dist/core/archive.js +415 -201
  71. package/dist/core/archive.js.map +1 -1
  72. package/dist/core/artifact-graph/graph.d.ts +29 -36
  73. package/dist/core/artifact-graph/graph.d.ts.map +1 -1
  74. package/dist/core/artifact-graph/graph.js +50 -58
  75. package/dist/core/artifact-graph/graph.js.map +1 -1
  76. package/dist/core/artifact-graph/index.d.ts +2 -2
  77. package/dist/core/artifact-graph/index.d.ts.map +1 -1
  78. package/dist/core/artifact-graph/index.js +2 -2
  79. package/dist/core/artifact-graph/index.js.map +1 -1
  80. package/dist/core/artifact-graph/instruction-loader.d.ts +105 -100
  81. package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
  82. package/dist/core/artifact-graph/instruction-loader.js +176 -110
  83. package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
  84. package/dist/core/artifact-graph/outputs.d.ts +13 -6
  85. package/dist/core/artifact-graph/outputs.d.ts.map +1 -1
  86. package/dist/core/artifact-graph/outputs.js +146 -8
  87. package/dist/core/artifact-graph/outputs.js.map +1 -1
  88. package/dist/core/artifact-graph/resolver.d.ts +44 -63
  89. package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
  90. package/dist/core/artifact-graph/resolver.js +85 -86
  91. package/dist/core/artifact-graph/resolver.js.map +1 -1
  92. package/dist/core/artifact-graph/schema.d.ts +0 -6
  93. package/dist/core/artifact-graph/schema.d.ts.map +1 -1
  94. package/dist/core/artifact-graph/schema.js +7 -32
  95. package/dist/core/artifact-graph/schema.js.map +1 -1
  96. package/dist/core/artifact-graph/state.d.ts +1 -8
  97. package/dist/core/artifact-graph/state.d.ts.map +1 -1
  98. package/dist/core/artifact-graph/state.js +2 -17
  99. package/dist/core/artifact-graph/state.js.map +1 -1
  100. package/dist/core/artifact-graph/stub-detection.d.ts +12 -0
  101. package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -0
  102. package/dist/core/artifact-graph/stub-detection.js +39 -0
  103. package/dist/core/artifact-graph/stub-detection.js.map +1 -0
  104. package/dist/core/artifact-graph/types.d.ts +4 -0
  105. package/dist/core/artifact-graph/types.d.ts.map +1 -1
  106. package/dist/core/artifact-graph/types.js +30 -10
  107. package/dist/core/artifact-graph/types.js.map +1 -1
  108. package/dist/core/available-tools.d.ts +3 -12
  109. package/dist/core/available-tools.d.ts.map +1 -1
  110. package/dist/core/available-tools.js +4 -13
  111. package/dist/core/available-tools.js.map +1 -1
  112. package/dist/core/change-metadata/schema.d.ts +1 -1
  113. package/dist/core/change-metadata/schema.d.ts.map +1 -1
  114. package/dist/core/change-metadata/schema.js +10 -7
  115. package/dist/core/change-metadata/schema.js.map +1 -1
  116. package/dist/core/change-presenter.d.ts +23 -18
  117. package/dist/core/change-presenter.d.ts.map +1 -1
  118. package/dist/core/change-presenter.js +102 -43
  119. package/dist/core/change-presenter.js.map +1 -1
  120. package/dist/core/change-status-policy.d.ts +8 -1
  121. package/dist/core/change-status-policy.d.ts.map +1 -1
  122. package/dist/core/change-status-policy.js +25 -1
  123. package/dist/core/change-status-policy.js.map +1 -1
  124. package/dist/core/codex-metrics.d.ts +25 -45
  125. package/dist/core/codex-metrics.d.ts.map +1 -1
  126. package/dist/core/codex-metrics.js +44 -88
  127. package/dist/core/codex-metrics.js.map +1 -1
  128. package/dist/core/codex-residue.d.ts +22 -0
  129. package/dist/core/codex-residue.d.ts.map +1 -0
  130. package/dist/core/codex-residue.js +61 -0
  131. package/dist/core/codex-residue.js.map +1 -0
  132. package/dist/core/command-generation/adapters/claude.d.ts +2 -9
  133. package/dist/core/command-generation/adapters/claude.d.ts.map +1 -1
  134. package/dist/core/command-generation/adapters/claude.js +2 -12
  135. package/dist/core/command-generation/adapters/claude.js.map +1 -1
  136. package/dist/core/command-generation/adapters/index.d.ts +1 -9
  137. package/dist/core/command-generation/adapters/index.d.ts.map +1 -1
  138. package/dist/core/command-generation/adapters/index.js +1 -9
  139. package/dist/core/command-generation/adapters/index.js.map +1 -1
  140. package/dist/core/command-generation/generator.d.ts +0 -17
  141. package/dist/core/command-generation/generator.d.ts.map +1 -1
  142. package/dist/core/command-generation/generator.js +0 -17
  143. package/dist/core/command-generation/generator.js.map +1 -1
  144. package/dist/core/command-generation/index.d.ts +2 -5
  145. package/dist/core/command-generation/index.d.ts.map +1 -1
  146. package/dist/core/command-generation/index.js +0 -9
  147. package/dist/core/command-generation/index.js.map +1 -1
  148. package/dist/core/command-generation/types.d.ts +10 -36
  149. package/dist/core/command-generation/types.d.ts.map +1 -1
  150. package/dist/core/command-generation/types.js +0 -6
  151. package/dist/core/command-generation/types.js.map +1 -1
  152. package/dist/core/command-generation/yaml.d.ts +3 -18
  153. package/dist/core/command-generation/yaml.d.ts.map +1 -1
  154. package/dist/core/command-generation/yaml.js +5 -23
  155. package/dist/core/command-generation/yaml.js.map +1 -1
  156. package/dist/core/config-prompts.d.ts +2 -4
  157. package/dist/core/config-prompts.d.ts.map +1 -1
  158. package/dist/core/config-prompts.js +2 -7
  159. package/dist/core/config-prompts.js.map +1 -1
  160. package/dist/core/config-schema.d.ts +8 -53
  161. package/dist/core/config-schema.d.ts.map +1 -1
  162. package/dist/core/config-schema.js +49 -62
  163. package/dist/core/config-schema.js.map +1 -1
  164. package/dist/core/config.d.ts +56 -0
  165. package/dist/core/config.d.ts.map +1 -1
  166. package/dist/core/config.js +73 -2
  167. package/dist/core/config.js.map +1 -1
  168. package/dist/core/converters/json-converter.d.ts.map +1 -1
  169. package/dist/core/dashboard-activity.d.ts +7 -9
  170. package/dist/core/dashboard-activity.d.ts.map +1 -1
  171. package/dist/core/dashboard-activity.js +26 -24
  172. package/dist/core/dashboard-activity.js.map +1 -1
  173. package/dist/core/dashboard-data.d.ts +40 -22
  174. package/dist/core/dashboard-data.d.ts.map +1 -1
  175. package/dist/core/dashboard-data.js +84 -68
  176. package/dist/core/dashboard-data.js.map +1 -1
  177. package/dist/core/global-config.d.ts +24 -53
  178. package/dist/core/global-config.d.ts.map +1 -1
  179. package/dist/core/global-config.js +43 -62
  180. package/dist/core/global-config.js.map +1 -1
  181. package/dist/core/init.d.ts +29 -11
  182. package/dist/core/init.d.ts.map +1 -1
  183. package/dist/core/init.js +232 -164
  184. package/dist/core/init.js.map +1 -1
  185. package/dist/core/list.d.ts +1 -1
  186. package/dist/core/list.d.ts.map +1 -1
  187. package/dist/core/list.js +121 -28
  188. package/dist/core/list.js.map +1 -1
  189. package/dist/core/local-server.d.ts +63 -39
  190. package/dist/core/local-server.d.ts.map +1 -1
  191. package/dist/core/local-server.js +99 -53
  192. package/dist/core/local-server.js.map +1 -1
  193. package/dist/core/markdown-render.d.ts +25 -0
  194. package/dist/core/markdown-render.d.ts.map +1 -0
  195. package/dist/core/markdown-render.js +94 -0
  196. package/dist/core/markdown-render.js.map +1 -0
  197. package/dist/core/migrate.d.ts +32 -15
  198. package/dist/core/migrate.d.ts.map +1 -1
  199. package/dist/core/migrate.js +220 -108
  200. package/dist/core/migrate.js.map +1 -1
  201. package/dist/core/parsers/change-parser.d.ts +7 -10
  202. package/dist/core/parsers/change-parser.d.ts.map +1 -1
  203. package/dist/core/parsers/change-parser.js +48 -56
  204. package/dist/core/parsers/change-parser.js.map +1 -1
  205. package/dist/core/parsers/markdown-parser.d.ts +8 -9
  206. package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
  207. package/dist/core/parsers/markdown-parser.js +31 -22
  208. package/dist/core/parsers/markdown-parser.js.map +1 -1
  209. package/dist/core/parsers/requirement-blocks.d.ts +53 -11
  210. package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
  211. package/dist/core/parsers/requirement-blocks.js +200 -60
  212. package/dist/core/parsers/requirement-blocks.js.map +1 -1
  213. package/dist/core/parsers/requirement-text.d.ts +73 -79
  214. package/dist/core/parsers/requirement-text.d.ts.map +1 -1
  215. package/dist/core/parsers/requirement-text.js +137 -79
  216. package/dist/core/parsers/requirement-text.js.map +1 -1
  217. package/dist/core/parsers/spec-structure.d.ts +1 -1
  218. package/dist/core/parsers/spec-structure.d.ts.map +1 -1
  219. package/dist/core/parsers/spec-structure.js +30 -3
  220. package/dist/core/parsers/spec-structure.js.map +1 -1
  221. package/dist/core/planning-home.js.map +1 -1
  222. package/dist/core/profiles.d.ts +3 -10
  223. package/dist/core/profiles.d.ts.map +1 -1
  224. package/dist/core/profiles.js +5 -12
  225. package/dist/core/profiles.js.map +1 -1
  226. package/dist/core/project-config.d.ts +43 -44
  227. package/dist/core/project-config.d.ts.map +1 -1
  228. package/dist/core/project-config.js +107 -82
  229. package/dist/core/project-config.js.map +1 -1
  230. package/dist/core/project-layout.d.ts +10 -18
  231. package/dist/core/project-layout.d.ts.map +1 -1
  232. package/dist/core/project-layout.js +16 -26
  233. package/dist/core/project-layout.js.map +1 -1
  234. package/dist/core/root-selection.d.ts +11 -7
  235. package/dist/core/root-selection.d.ts.map +1 -1
  236. package/dist/core/root-selection.js +7 -8
  237. package/dist/core/root-selection.js.map +1 -1
  238. package/dist/core/rules.d.ts +10 -0
  239. package/dist/core/rules.d.ts.map +1 -0
  240. package/dist/core/rules.js +43 -0
  241. package/dist/core/rules.js.map +1 -0
  242. package/dist/core/schema-names.d.ts +16 -0
  243. package/dist/core/schema-names.d.ts.map +1 -0
  244. package/dist/core/schema-names.js +16 -0
  245. package/dist/core/schema-names.js.map +1 -0
  246. package/dist/core/schemas/base.schema.d.ts +3 -0
  247. package/dist/core/schemas/base.schema.d.ts.map +1 -1
  248. package/dist/core/schemas/base.schema.js +22 -6
  249. package/dist/core/schemas/base.schema.js.map +1 -1
  250. package/dist/core/schemas/change.schema.d.ts +16 -0
  251. package/dist/core/schemas/change.schema.d.ts.map +1 -1
  252. package/dist/core/schemas/change.schema.js +41 -10
  253. package/dist/core/schemas/change.schema.js.map +1 -1
  254. package/dist/core/schemas/spec.schema.d.ts +2 -0
  255. package/dist/core/schemas/spec.schema.d.ts.map +1 -1
  256. package/dist/core/shared/index.d.ts +3 -8
  257. package/dist/core/shared/index.d.ts.map +1 -1
  258. package/dist/core/shared/index.js +3 -8
  259. package/dist/core/shared/index.js.map +1 -1
  260. package/dist/core/shared/rules-generation.d.ts +27 -8
  261. package/dist/core/shared/rules-generation.d.ts.map +1 -1
  262. package/dist/core/shared/rules-generation.js +151 -16
  263. package/dist/core/shared/rules-generation.js.map +1 -1
  264. package/dist/core/shared/skill-generation.d.ts +38 -53
  265. package/dist/core/shared/skill-generation.d.ts.map +1 -1
  266. package/dist/core/shared/skill-generation.js +82 -51
  267. package/dist/core/shared/skill-generation.js.map +1 -1
  268. package/dist/core/shared/tool-detection.d.ts +40 -62
  269. package/dist/core/shared/tool-detection.d.ts.map +1 -1
  270. package/dist/core/shared/tool-detection.js +88 -80
  271. package/dist/core/shared/tool-detection.js.map +1 -1
  272. package/dist/core/skill-metrics.d.ts +36 -63
  273. package/dist/core/skill-metrics.d.ts.map +1 -1
  274. package/dist/core/skill-metrics.js +34 -73
  275. package/dist/core/skill-metrics.js.map +1 -1
  276. package/dist/core/spec-presenter.d.ts.map +1 -1
  277. package/dist/core/spec-presenter.js +6 -6
  278. package/dist/core/spec-presenter.js.map +1 -1
  279. package/dist/core/specs-apply.d.ts +24 -23
  280. package/dist/core/specs-apply.d.ts.map +1 -1
  281. package/dist/core/specs-apply.js +188 -173
  282. package/dist/core/specs-apply.js.map +1 -1
  283. package/dist/core/templates/fragments/interview.d.ts +2 -6
  284. package/dist/core/templates/fragments/interview.d.ts.map +1 -1
  285. package/dist/core/templates/fragments/interview.js +2 -6
  286. package/dist/core/templates/fragments/interview.js.map +1 -1
  287. package/dist/core/templates/fragments/next-step.d.ts +4 -8
  288. package/dist/core/templates/fragments/next-step.d.ts.map +1 -1
  289. package/dist/core/templates/fragments/next-step.js +4 -8
  290. package/dist/core/templates/fragments/next-step.js.map +1 -1
  291. package/dist/core/templates/fragments/verify.d.ts +9 -12
  292. package/dist/core/templates/fragments/verify.d.ts.map +1 -1
  293. package/dist/core/templates/fragments/verify.js +9 -12
  294. package/dist/core/templates/fragments/verify.js.map +1 -1
  295. package/dist/core/templates/index.d.ts +0 -6
  296. package/dist/core/templates/index.d.ts.map +1 -1
  297. package/dist/core/templates/index.js +0 -7
  298. package/dist/core/templates/index.js.map +1 -1
  299. package/dist/core/templates/skill-templates.d.ts +1 -5
  300. package/dist/core/templates/skill-templates.d.ts.map +1 -1
  301. package/dist/core/templates/skill-templates.js +0 -5
  302. package/dist/core/templates/skill-templates.js.map +1 -1
  303. package/dist/core/templates/types.d.ts +3 -7
  304. package/dist/core/templates/types.d.ts.map +1 -1
  305. package/dist/core/templates/types.js +0 -3
  306. package/dist/core/templates/types.js.map +1 -1
  307. package/dist/core/templates/workflows/apply.d.ts +3 -9
  308. package/dist/core/templates/workflows/apply.d.ts.map +1 -1
  309. package/dist/core/templates/workflows/apply.js +9 -12
  310. package/dist/core/templates/workflows/apply.js.map +1 -1
  311. package/dist/core/templates/workflows/archive.d.ts +0 -6
  312. package/dist/core/templates/workflows/archive.d.ts.map +1 -1
  313. package/dist/core/templates/workflows/archive.js +7 -5
  314. package/dist/core/templates/workflows/archive.js.map +1 -1
  315. package/dist/core/templates/workflows/decision.js +4 -4
  316. package/dist/core/templates/workflows/decision.js.map +1 -1
  317. package/dist/core/templates/workflows/explore.js +1 -1
  318. package/dist/core/templates/workflows/grill.d.ts.map +1 -1
  319. package/dist/core/templates/workflows/grill.js +0 -2
  320. package/dist/core/templates/workflows/grill.js.map +1 -1
  321. package/dist/core/templates/workflows/issue.d.ts +0 -6
  322. package/dist/core/templates/workflows/issue.d.ts.map +1 -1
  323. package/dist/core/templates/workflows/issue.js +3 -0
  324. package/dist/core/templates/workflows/issue.js.map +1 -1
  325. package/dist/core/templates/workflows/propose.d.ts +0 -6
  326. package/dist/core/templates/workflows/propose.d.ts.map +1 -1
  327. package/dist/core/templates/workflows/propose.js +0 -1
  328. package/dist/core/templates/workflows/propose.js.map +1 -1
  329. package/dist/core/templates/workflows/sync.d.ts +2 -8
  330. package/dist/core/templates/workflows/sync.d.ts.map +1 -1
  331. package/dist/core/templates/workflows/sync.js +2 -2
  332. package/dist/core/templates/workflows/sync.js.map +1 -1
  333. package/dist/core/templates/workflows/update.d.ts +0 -6
  334. package/dist/core/templates/workflows/update.d.ts.map +1 -1
  335. package/dist/core/templates/workflows/update.js.map +1 -1
  336. package/dist/core/update.d.ts +26 -21
  337. package/dist/core/update.d.ts.map +1 -1
  338. package/dist/core/update.js +165 -116
  339. package/dist/core/update.js.map +1 -1
  340. package/dist/core/user-state-migration.d.ts +13 -15
  341. package/dist/core/user-state-migration.d.ts.map +1 -1
  342. package/dist/core/user-state-migration.js +16 -20
  343. package/dist/core/user-state-migration.js.map +1 -1
  344. package/dist/core/validation/constants.d.ts +19 -25
  345. package/dist/core/validation/constants.d.ts.map +1 -1
  346. package/dist/core/validation/constants.js +25 -20
  347. package/dist/core/validation/constants.js.map +1 -1
  348. package/dist/core/validation/prose-length.d.ts +15 -0
  349. package/dist/core/validation/prose-length.d.ts.map +1 -0
  350. package/dist/core/validation/prose-length.js +29 -0
  351. package/dist/core/validation/prose-length.js.map +1 -0
  352. package/dist/core/validation/purpose-placeholder.d.ts +9 -16
  353. package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
  354. package/dist/core/validation/purpose-placeholder.js +30 -44
  355. package/dist/core/validation/purpose-placeholder.js.map +1 -1
  356. package/dist/core/validation/section-validator.d.ts +4 -4
  357. package/dist/core/validation/section-validator.d.ts.map +1 -1
  358. package/dist/core/validation/section-validator.js +43 -7
  359. package/dist/core/validation/section-validator.js.map +1 -1
  360. package/dist/core/validation/task-numbering.d.ts +6 -3
  361. package/dist/core/validation/task-numbering.d.ts.map +1 -1
  362. package/dist/core/validation/task-numbering.js +23 -11
  363. package/dist/core/validation/task-numbering.js.map +1 -1
  364. package/dist/core/validation/types.d.ts +18 -0
  365. package/dist/core/validation/types.d.ts.map +1 -1
  366. package/dist/core/validation/types.js +12 -1
  367. package/dist/core/validation/types.js.map +1 -1
  368. package/dist/core/validation/validator.d.ts +50 -51
  369. package/dist/core/validation/validator.d.ts.map +1 -1
  370. package/dist/core/validation/validator.js +486 -263
  371. package/dist/core/validation/validator.js.map +1 -1
  372. package/dist/prompts/searchable-multi-select.d.ts +3 -8
  373. package/dist/prompts/searchable-multi-select.d.ts.map +1 -1
  374. package/dist/prompts/searchable-multi-select.js +16 -39
  375. package/dist/prompts/searchable-multi-select.js.map +1 -1
  376. package/dist/utils/change-metadata.d.ts +11 -50
  377. package/dist/utils/change-metadata.d.ts.map +1 -1
  378. package/dist/utils/change-metadata.js +48 -67
  379. package/dist/utils/change-metadata.js.map +1 -1
  380. package/dist/utils/change-utils.d.ts +31 -54
  381. package/dist/utils/change-utils.d.ts.map +1 -1
  382. package/dist/utils/change-utils.js +143 -100
  383. package/dist/utils/change-utils.js.map +1 -1
  384. package/dist/utils/file-lock.d.ts +39 -0
  385. package/dist/utils/file-lock.d.ts.map +1 -0
  386. package/dist/utils/file-lock.js +149 -0
  387. package/dist/utils/file-lock.js.map +1 -0
  388. package/dist/utils/file-system.d.ts +12 -32
  389. package/dist/utils/file-system.d.ts.map +1 -1
  390. package/dist/utils/file-system.js +16 -40
  391. package/dist/utils/file-system.js.map +1 -1
  392. package/dist/utils/frontmatter.d.ts +7 -11
  393. package/dist/utils/frontmatter.d.ts.map +1 -1
  394. package/dist/utils/frontmatter.js +11 -11
  395. package/dist/utils/frontmatter.js.map +1 -1
  396. package/dist/utils/interactive.d.ts +4 -9
  397. package/dist/utils/interactive.d.ts.map +1 -1
  398. package/dist/utils/interactive.js +2 -4
  399. package/dist/utils/interactive.js.map +1 -1
  400. package/dist/utils/item-discovery.d.ts +15 -10
  401. package/dist/utils/item-discovery.d.ts.map +1 -1
  402. package/dist/utils/item-discovery.js +42 -47
  403. package/dist/utils/item-discovery.js.map +1 -1
  404. package/dist/utils/link.d.ts +13 -4
  405. package/dist/utils/link.d.ts.map +1 -1
  406. package/dist/utils/link.js +13 -4
  407. package/dist/utils/link.js.map +1 -1
  408. package/dist/utils/match.js.map +1 -1
  409. package/dist/utils/requirement-diff.d.ts +13 -23
  410. package/dist/utils/requirement-diff.d.ts.map +1 -1
  411. package/dist/utils/requirement-diff.js +13 -23
  412. package/dist/utils/requirement-diff.js.map +1 -1
  413. package/dist/utils/spec-files.d.ts +10 -11
  414. package/dist/utils/spec-files.d.ts.map +1 -1
  415. package/dist/utils/spec-files.js +31 -22
  416. package/dist/utils/spec-files.js.map +1 -1
  417. package/dist/utils/task-progress.d.ts +11 -9
  418. package/dist/utils/task-progress.d.ts.map +1 -1
  419. package/dist/utils/task-progress.js +53 -32
  420. package/dist/utils/task-progress.js.map +1 -1
  421. package/dist/utils/timestamp.d.ts +5 -8
  422. package/dist/utils/timestamp.d.ts.map +1 -1
  423. package/dist/utils/timestamp.js +5 -8
  424. package/dist/utils/timestamp.js.map +1 -1
  425. package/package.json +9 -10
  426. package/schemas/decision/templates/decision.md +3 -1
  427. package/schemas/decision/templates/index.md +2 -2
  428. package/schemas/issue/schema.yaml +11 -2
  429. package/schemas/issue/templates/spec.md +37 -3
  430. package/schemas/sdd/schema.yaml +24 -1
  431. package/schemas/sdd/templates/spec.md +37 -3
package/dist/cli/index.js CHANGED
@@ -1,45 +1,79 @@
1
- import { emitFailure } from '../commands/shared-output.js';
2
- import { Command, Option, InvalidArgumentError } from 'commander';
1
+ import { emitFailure, isCliAbort, emitFailureStatus, normalizeCommanderCode, } from '../commands/shared-output.js';
2
+ import { Command, CommanderError, Option, InvalidArgumentError } from 'commander';
3
3
  import { createRequire } from 'module';
4
4
  import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { promises as fs } from 'fs';
7
7
  import { AI_TOOLS } from '../core/config.js';
8
- import { UpdateCommand } from '../core/update.js';
9
- import { ListCommand } from '../core/list.js';
10
- import { ArchiveCommand } from '../core/archive.js';
11
- import { MigrateCommand } from '../core/migrate.js';
8
+ import { UpdateCommand, UPDATE_FAILURE_PAYLOAD } from '../core/update.js';
9
+ import { INIT_FAILURE_PAYLOAD } from '../core/init.js';
10
+ import { RULES_FAILURE_PAYLOAD } from '../core/rules.js';
11
+ import { MigrateCommand, MIGRATE_FAILURE_PAYLOAD } from '../core/migrate.js';
12
12
  import { resolveRootForCommand, resolveTospecRoot, isRootSelectionError, toRootOutput, } from '../core/root-selection.js';
13
- import { ValidateCommand } from '../commands/validate.js';
14
- import { ShowCommand } from '../commands/show.js';
13
+ import { ValidateCommand, VALIDATE_FAILURE_PAYLOAD } from '../commands/validate.js';
14
+ import { ShowCommand, SHOW_FAILURE_PAYLOAD } from '../commands/show.js';
15
15
  import { isInteractive } from '../utils/interactive.js';
16
- import { registerConfigCommand } from '../commands/config.js';
17
- import { registerDecisionCommand } from '../commands/decision.js';
16
+ import { registerConfigCommand, CONFIG_FAILURE_PAYLOADS } from '../commands/config.js';
17
+ import { registerDecisionCommand, DECISION_NEW_FAILURE_PAYLOAD, DECISION_LIST_FAILURE_PAYLOAD, } from '../commands/decision.js';
18
18
  import { registerMetricsCommand } from '../commands/metrics.js';
19
- import { statusCommand, BATCH_STATUS_FAILURE_PAYLOAD, instructionsCommand, applyInstructionsCommand, templatesCommand, schemasCommand, newChangeCommand, DEFAULT_SCHEMA, } from '../commands/workflow/index.js';
19
+ import { statusCommand, BATCH_STATUS_FAILURE_PAYLOAD, STATUS_FAILURE_PAYLOAD, instructionsCommand, applyInstructionsCommand, INSTRUCTIONS_FAILURE_PAYLOAD, APPLY_ARGUMENT, templatesCommand, TEMPLATES_FAILURE_PAYLOAD, schemasCommand, SCHEMAS_FAILURE_PAYLOAD, newChangeCommand, NEW_CHANGE_FAILURE_PAYLOAD, DEFAULT_SCHEMA, } from '../commands/workflow/index.js';
20
20
  /**
21
21
  * The options the user actually typed.
22
22
  *
23
- * Commander materialises a negatable option's default: `--no-scenarios` puts
24
- * `scenarios: true` into the object even when nobody typed it. So "the key is
25
- * present" and "the user asked for this" are different questions, and a command
26
- * body testing presence answers the wrong one — `show <change>` warned about an
27
- * ignored `scenarios` flag on every single run.
28
- *
29
- * Dropping the defaults here makes the two questions the same, rather than
30
- * teaching each consumer a table of per-flag defaults to keep in sync. Safe
31
- * because every consumer already treats a missing flag and its default
32
- * identically (`options.scenarios !== false`, `options.interactive === false`).
23
+ * Commander materialises a negatable option's default (`--no-scenarios` puts
24
+ * `scenarios: true` in even when nobody typed it), so a body testing presence
25
+ * answers the wrong question. Safe to drop the defaults here because every
26
+ * consumer treats a missing flag and its default identically.
33
27
  */
34
28
  function userSuppliedOptions(options, command) {
35
29
  if (!options)
36
30
  return {};
37
31
  return Object.fromEntries(Object.entries(options).filter(([key]) => command.getOptionValueSource(key) !== 'default'));
38
32
  }
33
+ /** `--concurrency` / `--port`: digits only, so `''`, `1.5` and `0x10` are all refused. */
34
+ function positiveIntegerArg(value) {
35
+ if (!/^\d+$/.test(value.trim()) || Number(value) <= 0) {
36
+ throw new InvalidArgumentError('Expected a positive integer.');
37
+ }
38
+ return value;
39
+ }
40
+ function portArg(value) {
41
+ if (!/^\d+$/.test(value.trim()) || Number(value) > 65535) {
42
+ throw new InvalidArgumentError('Expected an integer between 0 and 65535.');
43
+ }
44
+ return value;
45
+ }
46
+ const jsonFailureShapes = new WeakMap();
47
+ function withJsonFailureShape(command, shape) {
48
+ jsonFailureShapes.set(command, shape);
49
+ return command;
50
+ }
51
+ /**
52
+ * Makes commander throw instead of exiting, for the whole command tree. Applied
53
+ * after every subcommand is registered: commander copies `_exitCallback` into a
54
+ * subcommand at `.command()` time, so setting it on the root alone leaves every
55
+ * subcommand still calling `process.exit`.
56
+ */
57
+ function throwOnCommanderExit(command) {
58
+ command.exitOverride((error) => {
59
+ // Attach the command so runCli can recover its null-shape; commander's
60
+ // error carries only a code and a message.
61
+ error.tospecCommand = command;
62
+ throw error;
63
+ });
64
+ for (const child of command.commands)
65
+ throwOnCommanderExit(child);
66
+ }
67
+ /**
68
+ * The root output for a command that takes its root as a path argument rather
69
+ * than searching: `init`, `update`, `rules`, `migrate`. Their success payloads
70
+ * report `source: 'explicit'`, so their failures say the same.
71
+ */
72
+ function explicitRootOutput(targetPath) {
73
+ return { path: path.resolve(targetPath), source: 'explicit' };
74
+ }
75
+ /** The agent contract: one JSON document on stdout per `--json` failure. */
39
76
  function failWithError(error, json) {
40
- // The agent contract: every --json failure leaves exactly one JSON
41
- // document on stdout (the command's null-shape plus a status array).
42
- // Delegates to the one shared failure sink (shared-output.emitFailure).
43
77
  emitFailure(json?.enabled, json?.payload ?? {}, error, json?.fallbackCode ?? 'command_error');
44
78
  }
45
79
  const program = new Command();
@@ -49,20 +83,24 @@ program
49
83
  .name('tospec')
50
84
  .description('Spec-driven development CLI for requirements and issue workflows')
51
85
  .version(version);
52
- // Kept for backward compatibility with existing invocations; output has no
53
- // color to disable now, so this is otherwise a no-op.
86
+ // Kept for backward compatibility; output has no color to disable, so this is a
87
+ // no-op.
54
88
  program.option('--no-color', 'Disable color output (no-op: output is plain text)');
55
- // -----------------------------------------------------------------------------
56
- // Install layer
57
- // -----------------------------------------------------------------------------
58
89
  const availableToolIds = AI_TOOLS.filter((tool) => tool.skillsDir).map((tool) => tool.value);
59
- const toolsOptionDescription = `Configure AI tools non-interactively. Use "all", "none", or a comma-separated list of: ${availableToolIds.join(', ')}`;
90
+ // Derived from the same table so a future dependency documents itself: a value
91
+ // that drags another target in otherwise reads as "pick this or don't".
92
+ const toolDependencyNotes = AI_TOOLS
93
+ .filter((tool) => tool.requires?.length)
94
+ .map((tool) => `${tool.value} implies ${tool.requires.join(', ')}`)
95
+ .join('; ');
96
+ const toolsOptionDescription = `Configure AI tools non-interactively. Use "all", "none", or a comma-separated list of: ${availableToolIds.join(', ')}${toolDependencyNotes ? ` (${toolDependencyNotes})` : ''}`;
60
97
  program
61
98
  .command('init [path]')
62
99
  .description('Initialize tospec in your project')
63
100
  .option('--tools <tools>', toolsOptionDescription)
64
- .option('--force', 'Refresh existing generated files without prompting')
101
+ .option('--force', 'Refresh existing generated files without prompting; does not bypass rule conflicts')
65
102
  .option('--profile <profile>', 'Override global config profile (core or custom)')
103
+ .option('--json', 'Output as JSON (for programmatic use)')
66
104
  .action(async (targetPath = '.', options) => {
67
105
  try {
68
106
  const resolvedPath = path.resolve(targetPath);
@@ -88,37 +126,70 @@ program
88
126
  tools: options?.tools,
89
127
  force: options?.force,
90
128
  profile: options?.profile,
129
+ json: options?.json,
91
130
  });
92
131
  await initCommand.execute(targetPath);
93
132
  }
94
133
  catch (error) {
95
- failWithError(error);
134
+ failWithError(error, {
135
+ enabled: options?.json,
136
+ payload: { ...INIT_FAILURE_PAYLOAD, root: explicitRootOutput(targetPath) },
137
+ fallbackCode: 'init_error',
138
+ });
96
139
  }
97
140
  });
98
141
  program
99
142
  .command('update [path]')
100
143
  .description('Update tospec instruction files')
101
- .option('--force', 'Force update even when tools are up to date')
144
+ .option('--force', 'Force update even when tools are up to date; does not bypass rule conflicts')
145
+ .option('--json', 'Output as JSON (for programmatic use)')
102
146
  .action(async (targetPath = '.', options) => {
103
147
  try {
104
- const updateCommand = new UpdateCommand({ force: options?.force });
148
+ const updateCommand = new UpdateCommand({ force: options?.force, json: options?.json });
105
149
  await updateCommand.execute(targetPath);
106
150
  }
107
151
  catch (error) {
108
- failWithError(error);
152
+ failWithError(error, {
153
+ enabled: options?.json,
154
+ payload: { ...UPDATE_FAILURE_PAYLOAD, root: explicitRootOutput(targetPath) },
155
+ fallbackCode: 'update_error',
156
+ });
157
+ }
158
+ });
159
+ program
160
+ .command('rules [path]')
161
+ .description('Refresh workflow rules for tools with installed tospec skills')
162
+ .option('--json', 'Output as JSON (for programmatic use)')
163
+ .action(async (targetPath = '.', options) => {
164
+ try {
165
+ const { RulesCommand } = await import('../core/rules.js');
166
+ await new RulesCommand().execute(targetPath, { json: options?.json });
167
+ }
168
+ catch (error) {
169
+ failWithError(error, {
170
+ enabled: options?.json,
171
+ payload: { ...RULES_FAILURE_PAYLOAD, root: explicitRootOutput(targetPath) },
172
+ fallbackCode: 'rules_error',
173
+ });
109
174
  }
110
175
  });
111
176
  program
112
177
  .command('migrate [openspec-dir]')
113
178
  .description('Migrate an OpenSpec project into tospec format (default source: ./openspec)')
114
179
  .option('-f, --force', 'Skip confirmation prompt')
180
+ .option('--json', 'Output as JSON (non-interactive; requires --force)')
115
181
  .action(async (openspecDir, options) => {
116
182
  try {
117
183
  const migrateCommand = new MigrateCommand();
118
184
  await migrateCommand.execute(openspecDir, options);
119
185
  }
120
186
  catch (error) {
121
- failWithError(error);
187
+ failWithError(error, {
188
+ enabled: options?.json,
189
+ // migrate always writes `tospec/` into the cwd, so the root is known here.
190
+ payload: { ...MIGRATE_FAILURE_PAYLOAD, root: explicitRootOutput(process.cwd()) },
191
+ fallbackCode: 'migrate_error',
192
+ });
122
193
  }
123
194
  });
124
195
  program
@@ -130,31 +201,53 @@ program
130
201
  .option('--type <type>', 'Filter changes by type: "requirement" or "issue"')
131
202
  .option('--json', 'Output as JSON (for programmatic use)')
132
203
  .action(async (options) => {
204
+ // Resolve first so a failure can still report where the CLI was pointed.
205
+ // The data key is `null`, never `[]`: an empty array is indistinguishable
206
+ // from the successful "no active changes" answer.
207
+ let resolvedRoot = null;
208
+ const failurePayload = () => ({
209
+ ...(options?.specs ? { specs: null } : { changes: null }),
210
+ root: resolvedRoot ? toRootOutput(resolvedRoot) : null,
211
+ });
133
212
  try {
134
- if (options?.type && options.type !== 'requirement' && options.type !== 'issue') {
135
- throw new Error(`Invalid --type '${options.type}'. Expected "requirement" or "issue".`);
136
- }
137
- const root = await resolveRootForCommand({}, {
213
+ resolvedRoot = await resolveRootForCommand({}, {
138
214
  json: options?.json,
139
- failurePayload: options?.specs ? { specs: [], root: null } : { changes: [], root: null },
215
+ // An implicit root means no ancestor is a tospec project, and listing
216
+ // "no changes" there answers about a place that was never checked.
217
+ allowImplicitRoot: false,
218
+ failurePayload: failurePayload(),
140
219
  });
141
- if (!root) {
220
+ if (!resolvedRoot) {
142
221
  return;
143
222
  }
223
+ if (options?.type && options.type !== 'requirement' && options.type !== 'issue') {
224
+ throw new Error(`Invalid --type '${options.type}'. Expected "requirement" or "issue".`);
225
+ }
226
+ // Two listings with no ordering between them; picking one silently
227
+ // answers a question the user did not ask.
228
+ if (options?.specs && options?.changes) {
229
+ throw new Error('Pass either --specs or --changes, not both: they select different listings and there is no ordering between them.');
230
+ }
231
+ // Specs have no type. A warning, not an error: the requested listing is
232
+ // still well-defined. On stderr, so `--json` stdout stays one document.
233
+ if (options?.specs && options?.type) {
234
+ console.error('Warning: Ignoring flags not applicable to --specs: type');
235
+ }
236
+ const { ListCommand } = await import('../core/list.js');
144
237
  const listCommand = new ListCommand();
145
238
  const mode = options?.specs ? 'specs' : 'changes';
146
239
  const sort = options?.sort === 'name' ? 'name' : 'recent';
147
- await listCommand.execute(root.path, mode, {
240
+ await listCommand.execute(resolvedRoot.path, mode, {
148
241
  sort,
149
242
  json: options?.json,
150
243
  type: options?.type,
151
- ...(options?.json ? { root: toRootOutput(root) } : {}),
244
+ ...(options?.json ? { root: toRootOutput(resolvedRoot) } : {}),
152
245
  });
153
246
  }
154
247
  catch (error) {
155
248
  failWithError(error, {
156
249
  enabled: options?.json,
157
- payload: options?.specs ? { specs: [], root: null } : { changes: [], root: null },
250
+ payload: failurePayload(),
158
251
  fallbackCode: 'list_error',
159
252
  });
160
253
  }
@@ -163,12 +256,13 @@ program
163
256
  .command('archive [change-name]')
164
257
  .description('Archive a completed change and update main specs')
165
258
  .option('-y, --yes', 'Skip confirmation prompts')
166
- .option('--skip-specs', 'Skip spec update operations (useful for infrastructure, tooling, or doc-only changes)')
259
+ .option('--skip-specs', 'Skip merging delta specs into the main specs for this run only; validation still applies, so an sdd change with no deltas also needs skip_specs: true in its .tospec.yaml (schemas whose specs are optional, such as issue, do not)')
167
260
  .option('--no-validate', 'Skip validation (not recommended, requires confirmation)')
168
261
  .option('--require-sync', 'Require a passing sync-report.md in the change directory before archiving')
169
262
  .option('--json', 'Output as JSON (non-interactive)')
170
263
  .action(async (changeName, options) => {
171
264
  try {
265
+ const { ArchiveCommand } = await import('../core/archive.js');
172
266
  const archiveCommand = new ArchiveCommand();
173
267
  await archiveCommand.execute(changeName, options);
174
268
  }
@@ -182,17 +276,13 @@ program
182
276
  .option('--all', 'Validate all changes and specs')
183
277
  .option('--changes', 'Validate all changes')
184
278
  .option('--specs', 'Validate all specs')
185
- .option('--type <type>', 'Specify item type when ambiguous: change|spec')
279
+ // `.choices`, as `show --type` does: an unknown value used to fall through
280
+ // normalizeType as undefined and silently validate by auto-detection.
281
+ .addOption(new Option('--type <type>', 'Specify item type when ambiguous').choices(['change', 'spec']))
186
282
  .option('--strict', 'Enable strict validation mode')
187
283
  .option('--json', 'Output validation results as JSON')
188
284
  .addOption(new Option('--concurrency <n>', 'Max concurrent validations (defaults to env TOSPEC_CONCURRENCY or 6)')
189
- .argParser((value) => {
190
- const n = parseInt(value, 10);
191
- if (Number.isNaN(n) || n <= 0 || String(n) !== value.trim()) {
192
- throw new InvalidArgumentError('Expected a positive integer.');
193
- }
194
- return value;
195
- }))
285
+ .argParser(positiveIntegerArg))
196
286
  .option('--no-interactive', 'Disable interactive prompts')
197
287
  .action(async (itemName, options) => {
198
288
  try {
@@ -200,17 +290,26 @@ program
200
290
  await validateCommand.execute(itemName, options);
201
291
  }
202
292
  catch (error) {
203
- failWithError(error, { enabled: options?.json, fallbackCode: 'validate_error' });
293
+ failWithError(error, {
294
+ enabled: options?.json,
295
+ payload: VALIDATE_FAILURE_PAYLOAD,
296
+ fallbackCode: 'validate_error',
297
+ });
204
298
  }
205
299
  });
206
300
  program
207
301
  .command('show [item-name]')
208
302
  .description('Show a change or spec')
209
303
  .option('--json', 'Output as JSON')
210
- .option('--type <type>', 'Specify item type when ambiguous: change|spec')
304
+ .addOption(new Option('--type <type>', 'Specify item type when ambiguous: change|spec').choices([
305
+ 'change',
306
+ 'spec',
307
+ ]))
211
308
  .option('--no-interactive', 'Disable interactive prompts')
212
- .option('--deltas-only', 'Show only deltas (JSON only, change)')
213
- .option('--requirements-only', 'Alias for --deltas-only (deprecated, change)')
309
+ // No-ops kept for compatibility: a change's --json payload is always
310
+ // id/title/deltaCount/deltas, with no wider shape to narrow.
311
+ .option('--deltas-only', 'No-op: a change\'s JSON is always deltas (kept for compatibility)')
312
+ .option('--requirements-only', 'No-op: deprecated alias for --deltas-only')
214
313
  .option('--diff', 'Show per-requirement diffs against the main specs (change)')
215
314
  .option('--requirements', 'JSON only: Show only requirements (exclude scenarios)')
216
315
  .option('--no-scenarios', 'JSON only: Exclude scenario content')
@@ -218,17 +317,16 @@ program
218
317
  .action(async (itemName, options, command) => {
219
318
  try {
220
319
  const showCommand = new ShowCommand();
221
- // Defaults stripped so warnIrrelevantFlags reports the flags the user
222
- // typed, not the ones commander filled in.
223
320
  await showCommand.execute(itemName, userSuppliedOptions(options, command));
224
321
  }
225
322
  catch (error) {
226
- failWithError(error, { enabled: options?.json, fallbackCode: 'show_error' });
323
+ failWithError(error, {
324
+ enabled: options?.json,
325
+ payload: SHOW_FAILURE_PAYLOAD,
326
+ fallbackCode: 'show_error',
327
+ });
227
328
  }
228
329
  });
229
- // ═══════════════════════════════════════════════════════════
230
- // Workflow / engine commands (agent contract)
231
- // ═══════════════════════════════════════════════════════════
232
330
  program
233
331
  .command('status')
234
332
  .description('Display artifact completion status for a change')
@@ -243,9 +341,7 @@ program
243
341
  catch (error) {
244
342
  failWithError(error, {
245
343
  enabled: options.json,
246
- // The batch null-shape; the single-change failure shape is
247
- // pre-existing contract and stays payload-free.
248
- payload: options.all ? BATCH_STATUS_FAILURE_PAYLOAD : undefined,
344
+ payload: options.all ? BATCH_STATUS_FAILURE_PAYLOAD : STATUS_FAILURE_PAYLOAD,
249
345
  fallbackCode: 'change_error',
250
346
  });
251
347
  }
@@ -258,7 +354,7 @@ program
258
354
  .option('--json', 'Output as JSON')
259
355
  .action(async (artifactId, options) => {
260
356
  try {
261
- if (artifactId === 'apply') {
357
+ if (artifactId === APPLY_ARGUMENT) {
262
358
  await applyInstructionsCommand(options);
263
359
  }
264
360
  else {
@@ -266,7 +362,11 @@ program
266
362
  }
267
363
  }
268
364
  catch (error) {
269
- failWithError(error, { enabled: options.json, fallbackCode: 'change_error' });
365
+ failWithError(error, {
366
+ enabled: options.json,
367
+ payload: INSTRUCTIONS_FAILURE_PAYLOAD,
368
+ fallbackCode: 'change_error',
369
+ });
270
370
  }
271
371
  });
272
372
  program
@@ -279,7 +379,7 @@ program
279
379
  await templatesCommand(options);
280
380
  }
281
381
  catch (error) {
282
- failWithError(error, { enabled: options.json, payload: { templates: null }, fallbackCode: 'templates_error' });
382
+ failWithError(error, { enabled: options.json, payload: TEMPLATES_FAILURE_PAYLOAD, fallbackCode: 'templates_error' });
283
383
  }
284
384
  });
285
385
  program
@@ -291,7 +391,7 @@ program
291
391
  await schemasCommand(options);
292
392
  }
293
393
  catch (error) {
294
- failWithError(error, { enabled: options.json, payload: { schemas: null }, fallbackCode: 'schemas_error' });
394
+ failWithError(error, { enabled: options.json, payload: SCHEMAS_FAILURE_PAYLOAD, fallbackCode: 'schemas_error' });
295
395
  }
296
396
  });
297
397
  const newCmd = program.command('new').description('Create new items');
@@ -315,8 +415,10 @@ newCmd
315
415
  });
316
416
  program
317
417
  .command('dashboard [path]')
318
- .description('Start a local read-only web dashboard for this tospec project')
319
- .option('-p, --port <n>', 'Port to listen on', '5620')
418
+ .description('Start a local dashboard; task checkbox updates are the only writes, confined to this tospec root and blocked for archived or sync-certified changes')
419
+ // Parsed like --concurrency: `Number('')` is 0, so a bare `--port ''` used to
420
+ // pass the range check and bind an OS-chosen port.
421
+ .addOption(new Option('-p, --port <n>', 'Port to listen on (0-65535)').default('5620').argParser(portArg))
320
422
  .option('--host <addr>', 'Host to bind', '127.0.0.1')
321
423
  .option('-o, --open', 'Open the dashboard in a browser after starting')
322
424
  .option('-d, --detach', 'Run the dashboard in the background and return immediately')
@@ -326,10 +428,10 @@ program
326
428
  .action(async (targetPath = '.', options) => {
327
429
  try {
328
430
  const dashboard = await import('../commands/dashboard.js');
329
- // --list is global: don't require being inside a tospec project.
431
+ // --list is global: no tospec project required.
330
432
  if (options?.list) {
331
- // Same candidate set as --stop, so the two never disagree about what
332
- // is running — `--stop` points at this command when it cannot prompt.
433
+ // Same candidate set as --stop, which points here when it cannot
434
+ // prompt, so the two never disagree about what is running.
333
435
  const running = await dashboard.collectStoppableDashboards(null);
334
436
  if (running.length === 0) {
335
437
  console.log('No dashboards are running.');
@@ -345,7 +447,7 @@ program
345
447
  const startPath = path.resolve(targetPath);
346
448
  // --stop is project-independent, like --list: a path with no tospec root
347
449
  // has no dashboard of its own, which is the cue to offer the full list
348
- // rather than fail. Only a root *selection* failure degrades to null.
450
+ // rather than fail.
349
451
  if (options?.stop) {
350
452
  const stopRoot = await resolveTospecRoot({ startPath, allowImplicitRoot: false }).catch((err) => {
351
453
  if (isRootSelectionError(err))
@@ -360,9 +462,8 @@ program
360
462
  }
361
463
  let targets = dashboard.selectStopTargets(candidates, stopRootPath);
362
464
  if (targets.length === 0) {
363
- // Nothing here matches this path — let the user pick which to stop.
364
- // `--stop` runs outside a project too, so this branch is reachable
365
- // non-interactively, where there is nobody to answer the prompt.
465
+ // Nothing matches this path, so let the user pick. Reachable
466
+ // non-interactively too, where there is nobody to answer.
366
467
  const listed = candidates.map((d) => ` ${dashboard.formatRunningDashboard(d, ' ')}`);
367
468
  if (!isInteractive()) {
368
469
  throw new dashboard.DashboardStopError(['No dashboard for this path, and no terminal to choose one from.', ...listed].join('\n'), 'Re-run from an interactive terminal, or see tospec dashboard --list.');
@@ -392,10 +493,12 @@ program
392
493
  }
393
494
  return;
394
495
  }
496
+ // Already validated by portArg; the default is the only unparsed value.
497
+ const portValue = Number(options?.port ?? 5620);
395
498
  // Starting a dashboard still requires a root — only --stop is lenient.
396
499
  const root = await resolveTospecRoot({ startPath, allowImplicitRoot: false });
397
500
  const runOpts = {
398
- port: Number(options?.port ?? 5620),
501
+ port: portValue,
399
502
  host: options?.host ?? '127.0.0.1',
400
503
  open: options?.open,
401
504
  allowRemote: options?.allowRemote,
@@ -413,9 +516,90 @@ program
413
516
  registerConfigCommand(program);
414
517
  registerDecisionCommand(program);
415
518
  registerMetricsCommand(program);
519
+ function commandAt(...names) {
520
+ let current = program;
521
+ for (const name of names) {
522
+ const next = current.commands.find((c) => c.name() === name);
523
+ if (!next)
524
+ throw new Error(`No such command to register a JSON failure shape for: ${names.join(' ')}`);
525
+ current = next;
526
+ }
527
+ return current;
528
+ }
529
+ // The `--json` null-shapes in one table, so the contract rule (a failure payload
530
+ // mirrors its success payload with the data keys nulled) is checkable in one
531
+ // place. Each entry reuses the object the command's own catch block passes to
532
+ // `failWithError`, never a literal: commander gives every command two failure
533
+ // layers, and copies drift.
534
+ withJsonFailureShape(commandAt('init'), () => INIT_FAILURE_PAYLOAD);
535
+ withJsonFailureShape(commandAt('update'), () => UPDATE_FAILURE_PAYLOAD);
536
+ withJsonFailureShape(commandAt('rules'), () => RULES_FAILURE_PAYLOAD);
537
+ withJsonFailureShape(commandAt('migrate'), () => MIGRATE_FAILURE_PAYLOAD);
538
+ withJsonFailureShape(commandAt('list'), (argv) => argv.includes('--specs') ? { specs: null, root: null } : { changes: null, root: null });
539
+ withJsonFailureShape(commandAt('archive'), () => ({ archive: null, root: null }));
540
+ withJsonFailureShape(commandAt('validate'), () => VALIDATE_FAILURE_PAYLOAD);
541
+ withJsonFailureShape(commandAt('show'), () => SHOW_FAILURE_PAYLOAD);
542
+ withJsonFailureShape(commandAt('status'), (argv) => argv.includes('--all') ? BATCH_STATUS_FAILURE_PAYLOAD : STATUS_FAILURE_PAYLOAD);
543
+ withJsonFailureShape(commandAt('instructions'), () => INSTRUCTIONS_FAILURE_PAYLOAD);
544
+ withJsonFailureShape(commandAt('templates'), () => TEMPLATES_FAILURE_PAYLOAD);
545
+ withJsonFailureShape(commandAt('schemas'), () => SCHEMAS_FAILURE_PAYLOAD);
546
+ withJsonFailureShape(commandAt('new', 'change'), () => NEW_CHANGE_FAILURE_PAYLOAD);
547
+ withJsonFailureShape(commandAt('decision', 'new'), () => DECISION_NEW_FAILURE_PAYLOAD);
548
+ withJsonFailureShape(commandAt('decision', 'list'), () => DECISION_LIST_FAILURE_PAYLOAD);
549
+ // `edit` speaks no --json and is deliberately absent.
550
+ for (const [subcommand, payload] of Object.entries(CONFIG_FAILURE_PAYLOADS)) {
551
+ withJsonFailureShape(commandAt('config', subcommand), () => payload);
552
+ }
416
553
  export { program };
554
+ /**
555
+ * The command's full invocation path, not just its leaf name. `command.name()`
556
+ * is wrong for a nested command: `--help` advice naming the leaf of `new change`
557
+ * points at no command at all, and the leaf of `decision new` is a real command
558
+ * answering a different question.
559
+ */
560
+ function commandPath(command) {
561
+ const names = [];
562
+ // Stops at the program itself, whose name the caller's template already has.
563
+ for (let node = command; node?.parent; node = node.parent) {
564
+ names.unshift(node.name());
565
+ }
566
+ return names.join(' ');
567
+ }
417
568
  export function runCli(argv = process.argv) {
418
- program.parse(argv);
569
+ throwOnCommanderExit(program);
570
+ try {
571
+ program.parse(argv);
572
+ }
573
+ catch (error) {
574
+ if (error instanceof CommanderError) {
575
+ // `--help` and `--version` come through here having already printed what
576
+ // they were asked for, so only a non-zero exit is a failure.
577
+ if (error.exitCode === 0)
578
+ return;
579
+ const wantsJson = argv.includes('--json');
580
+ if (wantsJson) {
581
+ const command = error.tospecCommand;
582
+ const shape = command ? jsonFailureShapes.get(command) : undefined;
583
+ emitFailureStatus(shape ? shape(argv) : {}, {
584
+ severity: 'error',
585
+ code: normalizeCommanderCode(error.code),
586
+ message: error.message,
587
+ fix: `See tospec ${commandPath(command)} --help`.replace(/\s+/g, ' ').trim(),
588
+ });
589
+ return;
590
+ }
591
+ process.exitCode = error.exitCode || 1;
592
+ return;
593
+ }
594
+ // The one abort mechanism for places that cannot stop the run by returning
595
+ // (commander's preAction hooks). Setting the exit code and returning lets
596
+ // stdout flush, which `process.exit` does not guarantee.
597
+ if (isCliAbort(error)) {
598
+ process.exitCode = 1;
599
+ return;
600
+ }
601
+ throw error;
602
+ }
419
603
  }
420
604
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
421
605
  runCli();