@seanmars/tospec 0.19.0-beta.7 → 0.20.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 (429) hide show
  1. package/CHANGELOG.md +60 -231
  2. package/README.md +69 -82
  3. package/assets/dashboard/app.js +14 -3
  4. package/assets/dashboard/style.css +7 -0
  5. package/assets/rules/tospec/decision.md +3 -0
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +227 -86
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/commands/config.d.ts +9 -17
  10. package/dist/commands/config.d.ts.map +1 -1
  11. package/dist/commands/config.js +366 -107
  12. package/dist/commands/config.js.map +1 -1
  13. package/dist/commands/dashboard.d.ts +57 -99
  14. package/dist/commands/dashboard.d.ts.map +1 -1
  15. package/dist/commands/dashboard.js +224 -282
  16. package/dist/commands/dashboard.js.map +1 -1
  17. package/dist/commands/decision.d.ts +41 -19
  18. package/dist/commands/decision.d.ts.map +1 -1
  19. package/dist/commands/decision.js +385 -79
  20. package/dist/commands/decision.js.map +1 -1
  21. package/dist/commands/metrics.d.ts +28 -51
  22. package/dist/commands/metrics.d.ts.map +1 -1
  23. package/dist/commands/metrics.js +62 -93
  24. package/dist/commands/metrics.js.map +1 -1
  25. package/dist/commands/shared-output.d.ts +18 -21
  26. package/dist/commands/shared-output.d.ts.map +1 -1
  27. package/dist/commands/shared-output.js +49 -23
  28. package/dist/commands/shared-output.js.map +1 -1
  29. package/dist/commands/show.d.ts +7 -0
  30. package/dist/commands/show.d.ts.map +1 -1
  31. package/dist/commands/show.js +29 -7
  32. package/dist/commands/show.js.map +1 -1
  33. package/dist/commands/validate.d.ts +43 -45
  34. package/dist/commands/validate.d.ts.map +1 -1
  35. package/dist/commands/validate.js +262 -129
  36. package/dist/commands/validate.js.map +1 -1
  37. package/dist/commands/workflow/index.d.ts +6 -10
  38. package/dist/commands/workflow/index.d.ts.map +1 -1
  39. package/dist/commands/workflow/index.js +6 -10
  40. package/dist/commands/workflow/index.js.map +1 -1
  41. package/dist/commands/workflow/instructions.d.ts +21 -8
  42. package/dist/commands/workflow/instructions.d.ts.map +1 -1
  43. package/dist/commands/workflow/instructions.js +254 -95
  44. package/dist/commands/workflow/instructions.js.map +1 -1
  45. package/dist/commands/workflow/new-change.d.ts +4 -5
  46. package/dist/commands/workflow/new-change.d.ts.map +1 -1
  47. package/dist/commands/workflow/new-change.js +90 -25
  48. package/dist/commands/workflow/new-change.js.map +1 -1
  49. package/dist/commands/workflow/schemas.d.ts +3 -5
  50. package/dist/commands/workflow/schemas.d.ts.map +1 -1
  51. package/dist/commands/workflow/schemas.js +37 -11
  52. package/dist/commands/workflow/schemas.js.map +1 -1
  53. package/dist/commands/workflow/shared.d.ts +48 -21
  54. package/dist/commands/workflow/shared.d.ts.map +1 -1
  55. package/dist/commands/workflow/shared.js +36 -33
  56. package/dist/commands/workflow/shared.js.map +1 -1
  57. package/dist/commands/workflow/status.d.ts +10 -6
  58. package/dist/commands/workflow/status.d.ts.map +1 -1
  59. package/dist/commands/workflow/status.js +81 -42
  60. package/dist/commands/workflow/status.js.map +1 -1
  61. package/dist/commands/workflow/templates.d.ts +10 -3
  62. package/dist/commands/workflow/templates.d.ts.map +1 -1
  63. package/dist/commands/workflow/templates.js +39 -40
  64. package/dist/commands/workflow/templates.js.map +1 -1
  65. package/dist/core/archive.d.ts +26 -21
  66. package/dist/core/archive.d.ts.map +1 -1
  67. package/dist/core/archive.js +378 -235
  68. package/dist/core/archive.js.map +1 -1
  69. package/dist/core/artifact-graph/graph.d.ts +25 -42
  70. package/dist/core/artifact-graph/graph.d.ts.map +1 -1
  71. package/dist/core/artifact-graph/graph.js +45 -63
  72. package/dist/core/artifact-graph/graph.js.map +1 -1
  73. package/dist/core/artifact-graph/index.d.ts +1 -1
  74. package/dist/core/artifact-graph/index.d.ts.map +1 -1
  75. package/dist/core/artifact-graph/index.js +1 -1
  76. package/dist/core/artifact-graph/index.js.map +1 -1
  77. package/dist/core/artifact-graph/instruction-loader.d.ts +97 -103
  78. package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
  79. package/dist/core/artifact-graph/instruction-loader.js +146 -119
  80. package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
  81. package/dist/core/artifact-graph/outputs.d.ts +9 -23
  82. package/dist/core/artifact-graph/outputs.d.ts.map +1 -1
  83. package/dist/core/artifact-graph/outputs.js +45 -38
  84. package/dist/core/artifact-graph/outputs.js.map +1 -1
  85. package/dist/core/artifact-graph/resolver.d.ts +44 -63
  86. package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
  87. package/dist/core/artifact-graph/resolver.js +85 -86
  88. package/dist/core/artifact-graph/resolver.js.map +1 -1
  89. package/dist/core/artifact-graph/schema.d.ts +0 -6
  90. package/dist/core/artifact-graph/schema.d.ts.map +1 -1
  91. package/dist/core/artifact-graph/schema.js +7 -32
  92. package/dist/core/artifact-graph/schema.js.map +1 -1
  93. package/dist/core/artifact-graph/state.d.ts +1 -8
  94. package/dist/core/artifact-graph/state.d.ts.map +1 -1
  95. package/dist/core/artifact-graph/state.js +2 -17
  96. package/dist/core/artifact-graph/state.js.map +1 -1
  97. package/dist/core/artifact-graph/stub-detection.d.ts +12 -0
  98. package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -0
  99. package/dist/core/artifact-graph/stub-detection.js +39 -0
  100. package/dist/core/artifact-graph/stub-detection.js.map +1 -0
  101. package/dist/core/artifact-graph/types.d.ts +4 -0
  102. package/dist/core/artifact-graph/types.d.ts.map +1 -1
  103. package/dist/core/artifact-graph/types.js +30 -10
  104. package/dist/core/artifact-graph/types.js.map +1 -1
  105. package/dist/core/available-tools.d.ts +3 -12
  106. package/dist/core/available-tools.d.ts.map +1 -1
  107. package/dist/core/available-tools.js +4 -13
  108. package/dist/core/available-tools.js.map +1 -1
  109. package/dist/core/change-metadata/schema.d.ts +1 -1
  110. package/dist/core/change-metadata/schema.d.ts.map +1 -1
  111. package/dist/core/change-metadata/schema.js +10 -7
  112. package/dist/core/change-metadata/schema.js.map +1 -1
  113. package/dist/core/change-presenter.d.ts +23 -18
  114. package/dist/core/change-presenter.d.ts.map +1 -1
  115. package/dist/core/change-presenter.js +102 -43
  116. package/dist/core/change-presenter.js.map +1 -1
  117. package/dist/core/change-status-policy.d.ts +8 -1
  118. package/dist/core/change-status-policy.d.ts.map +1 -1
  119. package/dist/core/change-status-policy.js +25 -1
  120. package/dist/core/change-status-policy.js.map +1 -1
  121. package/dist/core/codex-metrics.d.ts +25 -45
  122. package/dist/core/codex-metrics.d.ts.map +1 -1
  123. package/dist/core/codex-metrics.js +44 -88
  124. package/dist/core/codex-metrics.js.map +1 -1
  125. package/dist/core/codex-residue.d.ts +14 -15
  126. package/dist/core/codex-residue.d.ts.map +1 -1
  127. package/dist/core/codex-residue.js +18 -22
  128. package/dist/core/codex-residue.js.map +1 -1
  129. package/dist/core/command-generation/adapters/claude.d.ts +2 -9
  130. package/dist/core/command-generation/adapters/claude.d.ts.map +1 -1
  131. package/dist/core/command-generation/adapters/claude.js +2 -12
  132. package/dist/core/command-generation/adapters/claude.js.map +1 -1
  133. package/dist/core/command-generation/adapters/index.d.ts +1 -9
  134. package/dist/core/command-generation/adapters/index.d.ts.map +1 -1
  135. package/dist/core/command-generation/adapters/index.js +1 -9
  136. package/dist/core/command-generation/adapters/index.js.map +1 -1
  137. package/dist/core/command-generation/generator.d.ts +0 -17
  138. package/dist/core/command-generation/generator.d.ts.map +1 -1
  139. package/dist/core/command-generation/generator.js +0 -17
  140. package/dist/core/command-generation/generator.js.map +1 -1
  141. package/dist/core/command-generation/index.d.ts +2 -5
  142. package/dist/core/command-generation/index.d.ts.map +1 -1
  143. package/dist/core/command-generation/index.js +0 -9
  144. package/dist/core/command-generation/index.js.map +1 -1
  145. package/dist/core/command-generation/types.d.ts +10 -36
  146. package/dist/core/command-generation/types.d.ts.map +1 -1
  147. package/dist/core/command-generation/types.js +0 -6
  148. package/dist/core/command-generation/types.js.map +1 -1
  149. package/dist/core/command-generation/yaml.d.ts +3 -18
  150. package/dist/core/command-generation/yaml.d.ts.map +1 -1
  151. package/dist/core/command-generation/yaml.js +5 -23
  152. package/dist/core/command-generation/yaml.js.map +1 -1
  153. package/dist/core/config-prompts.d.ts +2 -4
  154. package/dist/core/config-prompts.d.ts.map +1 -1
  155. package/dist/core/config-prompts.js +2 -7
  156. package/dist/core/config-prompts.js.map +1 -1
  157. package/dist/core/config-schema.d.ts +8 -53
  158. package/dist/core/config-schema.d.ts.map +1 -1
  159. package/dist/core/config-schema.js +49 -62
  160. package/dist/core/config-schema.js.map +1 -1
  161. package/dist/core/config.d.ts +39 -26
  162. package/dist/core/config.d.ts.map +1 -1
  163. package/dist/core/config.js +36 -22
  164. package/dist/core/config.js.map +1 -1
  165. package/dist/core/dashboard-activity.d.ts +7 -9
  166. package/dist/core/dashboard-activity.d.ts.map +1 -1
  167. package/dist/core/dashboard-activity.js +26 -24
  168. package/dist/core/dashboard-activity.js.map +1 -1
  169. package/dist/core/dashboard-data.d.ts +40 -22
  170. package/dist/core/dashboard-data.d.ts.map +1 -1
  171. package/dist/core/dashboard-data.js +62 -70
  172. package/dist/core/dashboard-data.js.map +1 -1
  173. package/dist/core/global-config.d.ts +24 -53
  174. package/dist/core/global-config.d.ts.map +1 -1
  175. package/dist/core/global-config.js +38 -67
  176. package/dist/core/global-config.js.map +1 -1
  177. package/dist/core/init.d.ts +25 -19
  178. package/dist/core/init.d.ts.map +1 -1
  179. package/dist/core/init.js +168 -181
  180. package/dist/core/init.js.map +1 -1
  181. package/dist/core/list.d.ts +1 -1
  182. package/dist/core/list.d.ts.map +1 -1
  183. package/dist/core/list.js +121 -28
  184. package/dist/core/list.js.map +1 -1
  185. package/dist/core/local-server.d.ts +41 -83
  186. package/dist/core/local-server.d.ts.map +1 -1
  187. package/dist/core/local-server.js +53 -98
  188. package/dist/core/local-server.js.map +1 -1
  189. package/dist/core/markdown-render.d.ts +25 -0
  190. package/dist/core/markdown-render.d.ts.map +1 -0
  191. package/dist/core/markdown-render.js +94 -0
  192. package/dist/core/markdown-render.js.map +1 -0
  193. package/dist/core/migrate.d.ts +29 -14
  194. package/dist/core/migrate.d.ts.map +1 -1
  195. package/dist/core/migrate.js +184 -122
  196. package/dist/core/migrate.js.map +1 -1
  197. package/dist/core/parsers/change-parser.d.ts +7 -10
  198. package/dist/core/parsers/change-parser.d.ts.map +1 -1
  199. package/dist/core/parsers/change-parser.js +48 -56
  200. package/dist/core/parsers/change-parser.js.map +1 -1
  201. package/dist/core/parsers/markdown-parser.d.ts +8 -9
  202. package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
  203. package/dist/core/parsers/markdown-parser.js +31 -22
  204. package/dist/core/parsers/markdown-parser.js.map +1 -1
  205. package/dist/core/parsers/requirement-blocks.d.ts +43 -15
  206. package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
  207. package/dist/core/parsers/requirement-blocks.js +142 -70
  208. package/dist/core/parsers/requirement-blocks.js.map +1 -1
  209. package/dist/core/parsers/requirement-text.d.ts +73 -79
  210. package/dist/core/parsers/requirement-text.d.ts.map +1 -1
  211. package/dist/core/parsers/requirement-text.js +137 -79
  212. package/dist/core/parsers/requirement-text.js.map +1 -1
  213. package/dist/core/parsers/spec-structure.d.ts.map +1 -1
  214. package/dist/core/parsers/spec-structure.js +10 -6
  215. package/dist/core/parsers/spec-structure.js.map +1 -1
  216. package/dist/core/profiles.d.ts +3 -10
  217. package/dist/core/profiles.d.ts.map +1 -1
  218. package/dist/core/profiles.js +5 -12
  219. package/dist/core/profiles.js.map +1 -1
  220. package/dist/core/project-config.d.ts +43 -44
  221. package/dist/core/project-config.d.ts.map +1 -1
  222. package/dist/core/project-config.js +107 -82
  223. package/dist/core/project-config.js.map +1 -1
  224. package/dist/core/project-layout.d.ts +9 -17
  225. package/dist/core/project-layout.d.ts.map +1 -1
  226. package/dist/core/project-layout.js +16 -26
  227. package/dist/core/project-layout.js.map +1 -1
  228. package/dist/core/root-selection.d.ts +11 -7
  229. package/dist/core/root-selection.d.ts.map +1 -1
  230. package/dist/core/root-selection.js +7 -8
  231. package/dist/core/root-selection.js.map +1 -1
  232. package/dist/core/rules.d.ts +6 -1
  233. package/dist/core/rules.d.ts.map +1 -1
  234. package/dist/core/rules.js +23 -2
  235. package/dist/core/rules.js.map +1 -1
  236. package/dist/core/schema-names.d.ts +16 -0
  237. package/dist/core/schema-names.d.ts.map +1 -0
  238. package/dist/core/schema-names.js +16 -0
  239. package/dist/core/schema-names.js.map +1 -0
  240. package/dist/core/schemas/base.schema.d.ts +3 -0
  241. package/dist/core/schemas/base.schema.d.ts.map +1 -1
  242. package/dist/core/schemas/base.schema.js +22 -6
  243. package/dist/core/schemas/base.schema.js.map +1 -1
  244. package/dist/core/schemas/change.schema.d.ts +16 -0
  245. package/dist/core/schemas/change.schema.d.ts.map +1 -1
  246. package/dist/core/schemas/change.schema.js +41 -10
  247. package/dist/core/schemas/change.schema.js.map +1 -1
  248. package/dist/core/schemas/spec.schema.d.ts +2 -0
  249. package/dist/core/schemas/spec.schema.d.ts.map +1 -1
  250. package/dist/core/shared/index.d.ts +3 -8
  251. package/dist/core/shared/index.d.ts.map +1 -1
  252. package/dist/core/shared/index.js +3 -8
  253. package/dist/core/shared/index.js.map +1 -1
  254. package/dist/core/shared/rules-generation.d.ts +19 -8
  255. package/dist/core/shared/rules-generation.d.ts.map +1 -1
  256. package/dist/core/shared/rules-generation.js +117 -50
  257. package/dist/core/shared/rules-generation.js.map +1 -1
  258. package/dist/core/shared/skill-generation.d.ts +28 -43
  259. package/dist/core/shared/skill-generation.d.ts.map +1 -1
  260. package/dist/core/shared/skill-generation.js +82 -51
  261. package/dist/core/shared/skill-generation.js.map +1 -1
  262. package/dist/core/shared/tool-detection.d.ts +39 -61
  263. package/dist/core/shared/tool-detection.d.ts.map +1 -1
  264. package/dist/core/shared/tool-detection.js +88 -80
  265. package/dist/core/shared/tool-detection.js.map +1 -1
  266. package/dist/core/skill-metrics.d.ts +36 -63
  267. package/dist/core/skill-metrics.d.ts.map +1 -1
  268. package/dist/core/skill-metrics.js +34 -73
  269. package/dist/core/skill-metrics.js.map +1 -1
  270. package/dist/core/spec-presenter.js +6 -6
  271. package/dist/core/spec-presenter.js.map +1 -1
  272. package/dist/core/specs-apply.d.ts +22 -23
  273. package/dist/core/specs-apply.d.ts.map +1 -1
  274. package/dist/core/specs-apply.js +166 -193
  275. package/dist/core/specs-apply.js.map +1 -1
  276. package/dist/core/templates/fragments/interview.d.ts +2 -6
  277. package/dist/core/templates/fragments/interview.d.ts.map +1 -1
  278. package/dist/core/templates/fragments/interview.js +2 -6
  279. package/dist/core/templates/fragments/interview.js.map +1 -1
  280. package/dist/core/templates/fragments/next-step.d.ts +4 -8
  281. package/dist/core/templates/fragments/next-step.d.ts.map +1 -1
  282. package/dist/core/templates/fragments/next-step.js +4 -8
  283. package/dist/core/templates/fragments/next-step.js.map +1 -1
  284. package/dist/core/templates/fragments/validate.d.ts +13 -0
  285. package/dist/core/templates/fragments/validate.d.ts.map +1 -0
  286. package/dist/core/templates/fragments/validate.js +13 -0
  287. package/dist/core/templates/fragments/validate.js.map +1 -0
  288. package/dist/core/templates/fragments/verify.d.ts +9 -12
  289. package/dist/core/templates/fragments/verify.d.ts.map +1 -1
  290. package/dist/core/templates/fragments/verify.js +9 -12
  291. package/dist/core/templates/fragments/verify.js.map +1 -1
  292. package/dist/core/templates/index.d.ts +0 -6
  293. package/dist/core/templates/index.d.ts.map +1 -1
  294. package/dist/core/templates/index.js +0 -7
  295. package/dist/core/templates/index.js.map +1 -1
  296. package/dist/core/templates/skill-templates.d.ts +1 -5
  297. package/dist/core/templates/skill-templates.d.ts.map +1 -1
  298. package/dist/core/templates/skill-templates.js +0 -5
  299. package/dist/core/templates/skill-templates.js.map +1 -1
  300. package/dist/core/templates/types.d.ts +3 -7
  301. package/dist/core/templates/types.d.ts.map +1 -1
  302. package/dist/core/templates/types.js +0 -3
  303. package/dist/core/templates/types.js.map +1 -1
  304. package/dist/core/templates/workflows/apply.d.ts +3 -9
  305. package/dist/core/templates/workflows/apply.d.ts.map +1 -1
  306. package/dist/core/templates/workflows/apply.js +11 -13
  307. package/dist/core/templates/workflows/apply.js.map +1 -1
  308. package/dist/core/templates/workflows/archive.d.ts +0 -6
  309. package/dist/core/templates/workflows/archive.d.ts.map +1 -1
  310. package/dist/core/templates/workflows/archive.js +19 -6
  311. package/dist/core/templates/workflows/archive.js.map +1 -1
  312. package/dist/core/templates/workflows/decision.js +4 -4
  313. package/dist/core/templates/workflows/decision.js.map +1 -1
  314. package/dist/core/templates/workflows/explore.js +1 -1
  315. package/dist/core/templates/workflows/grill.d.ts.map +1 -1
  316. package/dist/core/templates/workflows/grill.js +0 -2
  317. package/dist/core/templates/workflows/grill.js.map +1 -1
  318. package/dist/core/templates/workflows/issue.d.ts +0 -6
  319. package/dist/core/templates/workflows/issue.d.ts.map +1 -1
  320. package/dist/core/templates/workflows/issue.js +3 -2
  321. package/dist/core/templates/workflows/issue.js.map +1 -1
  322. package/dist/core/templates/workflows/propose.d.ts +0 -6
  323. package/dist/core/templates/workflows/propose.d.ts.map +1 -1
  324. package/dist/core/templates/workflows/propose.js +2 -2
  325. package/dist/core/templates/workflows/propose.js.map +1 -1
  326. package/dist/core/templates/workflows/sync.d.ts +2 -8
  327. package/dist/core/templates/workflows/sync.d.ts.map +1 -1
  328. package/dist/core/templates/workflows/sync.js +4 -3
  329. package/dist/core/templates/workflows/sync.js.map +1 -1
  330. package/dist/core/templates/workflows/update.d.ts +0 -6
  331. package/dist/core/templates/workflows/update.d.ts.map +1 -1
  332. package/dist/core/templates/workflows/update.js +9 -2
  333. package/dist/core/templates/workflows/update.js.map +1 -1
  334. package/dist/core/update.d.ts +23 -23
  335. package/dist/core/update.d.ts.map +1 -1
  336. package/dist/core/update.js +139 -118
  337. package/dist/core/update.js.map +1 -1
  338. package/dist/core/user-state-migration.d.ts +13 -15
  339. package/dist/core/user-state-migration.d.ts.map +1 -1
  340. package/dist/core/user-state-migration.js +16 -20
  341. package/dist/core/user-state-migration.js.map +1 -1
  342. package/dist/core/validation/constants.d.ts +4 -10
  343. package/dist/core/validation/constants.d.ts.map +1 -1
  344. package/dist/core/validation/constants.js +25 -20
  345. package/dist/core/validation/constants.js.map +1 -1
  346. package/dist/core/validation/prose-length.d.ts +15 -0
  347. package/dist/core/validation/prose-length.d.ts.map +1 -0
  348. package/dist/core/validation/prose-length.js +29 -0
  349. package/dist/core/validation/prose-length.js.map +1 -0
  350. package/dist/core/validation/purpose-placeholder.d.ts +9 -16
  351. package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
  352. package/dist/core/validation/purpose-placeholder.js +30 -44
  353. package/dist/core/validation/purpose-placeholder.js.map +1 -1
  354. package/dist/core/validation/section-validator.d.ts +4 -4
  355. package/dist/core/validation/section-validator.d.ts.map +1 -1
  356. package/dist/core/validation/section-validator.js +43 -7
  357. package/dist/core/validation/section-validator.js.map +1 -1
  358. package/dist/core/validation/task-numbering.d.ts +6 -3
  359. package/dist/core/validation/task-numbering.d.ts.map +1 -1
  360. package/dist/core/validation/task-numbering.js +23 -11
  361. package/dist/core/validation/task-numbering.js.map +1 -1
  362. package/dist/core/validation/types.d.ts +18 -0
  363. package/dist/core/validation/types.d.ts.map +1 -1
  364. package/dist/core/validation/types.js +12 -1
  365. package/dist/core/validation/types.js.map +1 -1
  366. package/dist/core/validation/validator.d.ts +43 -63
  367. package/dist/core/validation/validator.d.ts.map +1 -1
  368. package/dist/core/validation/validator.js +500 -267
  369. package/dist/core/validation/validator.js.map +1 -1
  370. package/dist/prompts/searchable-multi-select.d.ts +3 -8
  371. package/dist/prompts/searchable-multi-select.d.ts.map +1 -1
  372. package/dist/prompts/searchable-multi-select.js +16 -39
  373. package/dist/prompts/searchable-multi-select.js.map +1 -1
  374. package/dist/utils/change-metadata.d.ts +11 -50
  375. package/dist/utils/change-metadata.d.ts.map +1 -1
  376. package/dist/utils/change-metadata.js +48 -67
  377. package/dist/utils/change-metadata.js.map +1 -1
  378. package/dist/utils/change-utils.d.ts +30 -54
  379. package/dist/utils/change-utils.d.ts.map +1 -1
  380. package/dist/utils/change-utils.js +133 -91
  381. package/dist/utils/change-utils.js.map +1 -1
  382. package/dist/utils/file-lock.d.ts +39 -0
  383. package/dist/utils/file-lock.d.ts.map +1 -0
  384. package/dist/utils/file-lock.js +149 -0
  385. package/dist/utils/file-lock.js.map +1 -0
  386. package/dist/utils/file-system.d.ts +12 -32
  387. package/dist/utils/file-system.d.ts.map +1 -1
  388. package/dist/utils/file-system.js +16 -40
  389. package/dist/utils/file-system.js.map +1 -1
  390. package/dist/utils/frontmatter.d.ts +7 -11
  391. package/dist/utils/frontmatter.d.ts.map +1 -1
  392. package/dist/utils/frontmatter.js +11 -11
  393. package/dist/utils/frontmatter.js.map +1 -1
  394. package/dist/utils/interactive.d.ts +4 -9
  395. package/dist/utils/interactive.d.ts.map +1 -1
  396. package/dist/utils/interactive.js +2 -4
  397. package/dist/utils/interactive.js.map +1 -1
  398. package/dist/utils/item-discovery.d.ts +15 -10
  399. package/dist/utils/item-discovery.d.ts.map +1 -1
  400. package/dist/utils/item-discovery.js +42 -47
  401. package/dist/utils/item-discovery.js.map +1 -1
  402. package/dist/utils/link.d.ts +9 -18
  403. package/dist/utils/link.d.ts.map +1 -1
  404. package/dist/utils/link.js +9 -18
  405. package/dist/utils/link.js.map +1 -1
  406. package/dist/utils/requirement-diff.d.ts +13 -23
  407. package/dist/utils/requirement-diff.d.ts.map +1 -1
  408. package/dist/utils/requirement-diff.js +13 -23
  409. package/dist/utils/requirement-diff.js.map +1 -1
  410. package/dist/utils/spec-files.d.ts +7 -20
  411. package/dist/utils/spec-files.d.ts.map +1 -1
  412. package/dist/utils/spec-files.js +26 -51
  413. package/dist/utils/spec-files.js.map +1 -1
  414. package/dist/utils/task-progress.d.ts +11 -9
  415. package/dist/utils/task-progress.d.ts.map +1 -1
  416. package/dist/utils/task-progress.js +53 -32
  417. package/dist/utils/task-progress.js.map +1 -1
  418. package/dist/utils/timestamp.d.ts +5 -8
  419. package/dist/utils/timestamp.d.ts.map +1 -1
  420. package/dist/utils/timestamp.js +5 -8
  421. package/dist/utils/timestamp.js.map +1 -1
  422. package/package.json +2 -3
  423. package/schemas/decision/templates/decision.md +3 -1
  424. package/schemas/decision/templates/index.md +2 -2
  425. package/schemas/issue/schema.yaml +8 -1
  426. package/schemas/issue/templates/spec.md +27 -2
  427. package/schemas/sdd/schema.yaml +20 -1
  428. package/schemas/sdd/templates/spec.md +27 -2
  429. /package/assets/rules/tospec/{single-sourc-of-truth.md → single-source-of-truth.md} +0 -0
@@ -5,14 +5,31 @@ import { getGlobalConfigPath, getEffectiveConfigPath, getGlobalConfig, saveGloba
5
5
  import * as yaml from 'yaml';
6
6
  import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, validateConfigKeyPath, hasUnsafeKeySegment, validateConfig, DEFAULT_CONFIG, } from '../core/config-schema.js';
7
7
  import { ALL_WORKFLOWS, getProfileWorkflows } from '../core/profiles.js';
8
+ import { describeWorkflowReferenceGaps, findWorkflowReferenceGaps, } from '../core/shared/skill-generation.js';
8
9
  import { TOSPEC_DIR_NAME } from '../core/config.js';
10
+ import { stripBom } from '../core/parsers/requirement-text.js';
9
11
  import { UpdateCommand } from '../core/update.js';
10
- import { isPromptCancellationError, asErrorMessage, CliAbortError, emitSuccess } from './shared-output.js';
11
- // Keyed by WorkflowId, not string: this map is a second hand-maintained list
12
- // parallel to WORKFLOW_DEFS, and with a `string` key a renamed or added workflow
13
- // drifted silently — the lookup below falls back instead of failing. Both apply
14
- // entries sat stale from the 0.13.0 verify change for exactly that reason.
15
- // Typing it shut turns the next rename into a compile error.
12
+ import { isPromptCancellationError, asErrorMessage, CliAbortError, emitSuccess, emitFailureStatus, } from './shared-output.js';
13
+ /**
14
+ * The `--json` null-shapes, one per subcommand, keyed by the data key each
15
+ * success payload carries. Shared by the commander-level failure table in
16
+ * cli/index.ts and the catch sites here, so a parse-time failure (`--bogus`)
17
+ * mirrors the same shape an action-time failure does.
18
+ */
19
+ export const CONFIG_FAILURE_PAYLOADS = {
20
+ path: { path: null },
21
+ list: { config: null, profileSettings: null },
22
+ get: { get: null },
23
+ set: { set: null },
24
+ unset: { unset: null },
25
+ reset: { config: null },
26
+ profile: { profile: null },
27
+ };
28
+ /** The parent `config` command has no data of its own; its preAction abort reports this. */
29
+ export const CONFIG_SCOPE_FAILURE_PAYLOAD = { config: null };
30
+ // Keyed by WorkflowId, not string: this list runs parallel to WORKFLOW_DEFS, and
31
+ // under a `string` key a renamed workflow drifts silently because the lookup
32
+ // below falls back instead of failing. Typed, the next rename is a compile error.
16
33
  const WORKFLOW_PROMPT_META = {
17
34
  explore: {
18
35
  name: 'Explore ideas',
@@ -55,9 +72,6 @@ const WORKFLOW_PROMPT_META = {
55
72
  description: 'Run a spec/code sync when warranted, then finalize and archive',
56
73
  },
57
74
  };
58
- /**
59
- * Resolve the effective current profile state from global config defaults.
60
- */
61
75
  export function resolveCurrentProfileState(config) {
62
76
  const profile = config.profile || 'core';
63
77
  const workflows = [
@@ -65,20 +79,52 @@ export function resolveCurrentProfileState(config) {
65
79
  ];
66
80
  return { profile, workflows };
67
81
  }
68
- /**
69
- * Derive profile type from selected workflows.
70
- */
71
82
  export function deriveProfileFromWorkflowSelection(selectedWorkflows) {
72
83
  const isCoreMatch = selectedWorkflows.length === ALL_WORKFLOWS.length &&
73
84
  ALL_WORKFLOWS.every((w) => selectedWorkflows.includes(w));
74
85
  return isCoreMatch ? 'core' : 'custom';
75
86
  }
76
- /**
77
- * Format a compact workflow summary for the profile header.
78
- */
79
87
  export function formatWorkflowSummary(workflows, profile) {
80
88
  return `${workflows.length} selected (${profile})`;
81
89
  }
90
+ /**
91
+ * The finding that a stored `workflows` list will not be used, or null. Only the
92
+ * `custom` profile consults it, so under any other profile it is stored,
93
+ * validated, displayed — and ignored. Worded to match `update`'s
94
+ * `update_workflows_ignored`.
95
+ */
96
+ function ignoredWorkflowsDiagnostic(config) {
97
+ const profile = config.profile ?? 'core';
98
+ if (profile === 'custom')
99
+ return null;
100
+ if (!config.workflows || config.workflows.length === 0)
101
+ return null;
102
+ return {
103
+ severity: 'warning',
104
+ code: 'config_workflows_ignored',
105
+ message: `Profile '${profile}' installs every workflow, so the configured workflows list is not used.`,
106
+ fix: 'Run "tospec config profile custom" to apply it, or "tospec config unset workflows" to drop it.',
107
+ };
108
+ }
109
+ /**
110
+ * The finding that the selected workflows' own instructions name workflows the
111
+ * selection leaves out, or null. Raised here as well as at install time because
112
+ * this is where the selection is made: `config set workflows
113
+ * "propose,apply,archive"` otherwise reports success while the resulting
114
+ * `tospec-archive` still tells an agent to run an uninstalled `tospec-sync`.
115
+ */
116
+ function workflowReferenceDiagnostic(config) {
117
+ const gaps = findWorkflowReferenceGaps(resolveListedWorkflows(config));
118
+ if (gaps.length === 0)
119
+ return null;
120
+ const missing = [...new Set(gaps.map((gap) => gap.missing))].sort();
121
+ return {
122
+ severity: 'warning',
123
+ code: 'config_workflow_reference_missing',
124
+ message: `Selected skills point at workflows this profile leaves out: ${describeWorkflowReferenceGaps(gaps)}.`,
125
+ fix: `Add them with "tospec config set workflows" (missing: ${missing.join(', ')}), or expect agents to hit instructions they cannot follow.`,
126
+ };
127
+ }
82
128
  function stableWorkflowOrder(workflows) {
83
129
  const seen = new Set();
84
130
  const ordered = [];
@@ -98,9 +144,6 @@ function stableWorkflowOrder(workflows) {
98
144
  }
99
145
  return ordered;
100
146
  }
101
- /**
102
- * Build a user-facing diff summary between two profile states.
103
- */
104
147
  export function diffProfileState(before, after) {
105
148
  const lines = [];
106
149
  if (before.profile !== after.profile) {
@@ -127,37 +170,70 @@ export function diffProfileState(before, after) {
127
170
  lines,
128
171
  };
129
172
  }
130
- function printConfigProfileApplyGuidance() {
131
- console.log('Config updated. Run `tospec update` in your projects to apply.');
173
+ /**
174
+ * The config file's raw contents, only to tell an explicit value from a default —
175
+ * `getGlobalConfig()` has merged the two. Reads the file actually loaded, not
176
+ * the write target: the two differ until a legacy install migrates.
177
+ */
178
+ function rawGlobalConfig() {
179
+ try {
180
+ const configPath = getEffectiveConfigPath();
181
+ if (fs.existsSync(configPath)) {
182
+ return JSON.parse(stripBom(fs.readFileSync(configPath, 'utf-8')));
183
+ }
184
+ }
185
+ catch {
186
+ // Unreadable: treat every value as a default rather than fail a listing.
187
+ }
188
+ return {};
132
189
  }
133
190
  /**
134
- * Register the config command and all its subcommands.
135
- *
136
- * @param program - The Commander program instance
191
+ * The workflows a profile resolves to — `core` expands to the full set, any
192
+ * other profile uses its explicit list. Shared by both branches of `config
193
+ * list`, so a `--json` caller can turn `profile: "core"` into workflow names.
137
194
  */
195
+ function resolveListedWorkflows(config) {
196
+ if (config.profile === 'core')
197
+ return [...ALL_WORKFLOWS];
198
+ return [...(config.workflows ?? [])];
199
+ }
200
+ function printConfigProfileApplyGuidance() {
201
+ console.log('Config updated. Run `tospec update` in your projects to apply.');
202
+ }
138
203
  export function registerConfigCommand(program) {
139
204
  const configCmd = program
140
205
  .command('config')
141
206
  .description('View and modify global tospec configuration')
142
207
  .option('--scope <scope>', 'Config scope (only "global" supported currently)')
143
- .hook('preAction', (thisCommand) => {
208
+ .hook('preAction', (thisCommand, actionCommand) => {
144
209
  const opts = thisCommand.opts();
145
210
  if (opts.scope && opts.scope !== 'global') {
146
- console.error('Error: Project-local config is not yet implemented');
147
- // Not `process.exit(1)`: this hook cannot stop the action by returning,
148
- // but exiting here would kill the process before stderr is guaranteed
149
- // to have flushed. runCli turns this into `process.exitCode = 1`.
150
- throw new CliAbortError('Project-local config is not yet implemented');
211
+ const message = 'Project-local config is not yet implemented';
212
+ // The subcommand's --json, not the parent's: `config --scope project
213
+ // get x --json` used to abort with nothing on stdout, so an agent saw
214
+ // exit 1 and no document at all.
215
+ if (actionCommand.opts().json) {
216
+ emitFailureStatus(CONFIG_FAILURE_PAYLOADS[actionCommand.name()] ?? CONFIG_SCOPE_FAILURE_PAYLOAD, { severity: 'error', code: 'config_scope_unsupported', message, fix: 'Omit --scope, or pass --scope global.' });
217
+ }
218
+ else {
219
+ console.error(`Error: ${message}`);
220
+ }
221
+ // Not `process.exit(1)`: exiting here kills the process before stderr
222
+ // has flushed.
223
+ throw new CliAbortError(message);
151
224
  }
152
225
  });
153
- // config path
154
226
  configCmd
155
227
  .command('path')
156
228
  .description('Show config file location')
157
- .action(() => {
229
+ .option('--json', 'Output as JSON')
230
+ .action((options) => {
231
+ if (options.json) {
232
+ emitSuccess({ path: getGlobalConfigPath() });
233
+ return;
234
+ }
158
235
  console.log(getGlobalConfigPath());
159
236
  });
160
- // config list
161
237
  configCmd
162
238
  .command('list')
163
239
  .description('Show all current settings')
@@ -165,66 +241,121 @@ export function registerConfigCommand(program) {
165
241
  .action((options) => {
166
242
  const config = getGlobalConfig();
167
243
  if (options.json) {
168
- // Nested, not spread. `GlobalConfigSchema` is `.passthrough()` and
244
+ // Nested, not spread: the schema is `.passthrough()` and
169
245
  // `config set --allow-unknown` writes any top-level key, so spreading
170
- // the config into the envelope let user data shadow the envelope's own
171
- // reserved fields — `config set version 9 --allow-unknown` replaced the
172
- // envelope `version`, and `root`/`status` were open the same way.
173
- // Giving the config its own key is what makes that structurally
174
- // impossible instead of patching one field name at a time.
175
- // See tospec/decisions/20260916_121544-config-list-json-nests-under-config-key.md
176
- emitSuccess({ config });
246
+ // lets user data shadow the envelope's reserved fields (`version`,
247
+ // `root`, `status`).
248
+ // tospec/decisions/20260916_121544-config-list-json-nests-under-config-key.md
249
+ //
250
+ // `profileSettings` mirrors the human branch below, the only other place
251
+ // saying which workflows `profile: "core"` resolves to.
252
+ const listStatus = [
253
+ ignoredWorkflowsDiagnostic(config),
254
+ workflowReferenceDiagnostic(config),
255
+ ].filter((entry) => entry !== null);
256
+ emitSuccess({
257
+ config,
258
+ profileSettings: {
259
+ profile: config.profile ?? 'core',
260
+ explicit: rawGlobalConfig().profile !== undefined,
261
+ workflows: resolveListedWorkflows(config),
262
+ },
263
+ ...(listStatus.length ? { status: listStatus } : {}),
264
+ });
177
265
  }
178
266
  else {
179
- // Read raw config to determine which values are explicit vs defaults.
180
- // Must be the file that was actually read, not the write target — they
181
- // differ until a legacy install migrates.
182
- const configPath = getEffectiveConfigPath();
183
- let rawConfig = {};
184
- try {
185
- if (fs.existsSync(configPath)) {
186
- rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
187
- }
188
- }
189
- catch {
190
- // If reading fails, treat all as defaults
191
- }
267
+ const rawConfig = rawGlobalConfig();
192
268
  console.log(yaml.stringify(config).trimEnd());
193
- // Annotate profile settings
269
+ // The dump above is the stored config, so under a profile that ignores
270
+ // `workflows` it and the resolved list below are two different answers
271
+ // for one key; this marks which is which.
272
+ const ignored = ignoredWorkflowsDiagnostic(config);
273
+ if (ignored) {
274
+ console.log(` ^ ${ignored.message}`);
275
+ }
194
276
  const profileSource = rawConfig.profile !== undefined ? '(explicit)' : '(default)';
195
277
  console.log(`\nProfile settings:`);
196
278
  console.log(` profile: ${config.profile} ${profileSource}`);
197
- if (config.profile === 'core') {
198
- console.log(` workflows: ${ALL_WORKFLOWS.join(', ')} (from core profile)`);
199
- }
200
- else if (config.workflows && config.workflows.length > 0) {
201
- console.log(` workflows: ${config.workflows.join(', ')} (explicit)`);
279
+ const workflows = resolveListedWorkflows(config);
280
+ if (workflows.length === 0) {
281
+ console.log(` workflows: (none)`);
202
282
  }
203
283
  else {
204
- console.log(` workflows: (none)`);
284
+ const origin = config.profile === 'core' ? '(from core profile)' : '(explicit)';
285
+ console.log(` workflows: ${workflows.join(', ')} ${origin}`);
286
+ }
287
+ if (ignored?.fix) {
288
+ console.log();
289
+ console.log(ignored.fix);
290
+ }
291
+ const referenceGap = workflowReferenceDiagnostic(config);
292
+ if (referenceGap) {
293
+ console.log();
294
+ console.log(`Warning: ${referenceGap.message}`);
295
+ if (referenceGap.fix)
296
+ console.log(referenceGap.fix);
205
297
  }
206
298
  }
207
299
  });
208
- // config get
209
300
  configCmd
210
301
  .command('get <key>')
211
302
  .description('Get a specific value (raw, scriptable)')
212
- .action((key) => {
303
+ .option('--allow-unknown', 'Allow reading unknown keys')
304
+ .option('--json', 'Output as JSON')
305
+ .action((key, options) => {
306
+ const allowUnknown = Boolean(options.allowUnknown);
213
307
  const keyValidation = validateConfigKeyPath(key);
214
- if (!keyValidation.valid) {
308
+ // Paired with the same flag on `set`: without it a key written with
309
+ // --allow-unknown could be listed and unset but never read back. Relaxes
310
+ // the known-key check only, never the prototype-safety one.
311
+ const unsafeKey = hasUnsafeKeySegment(key);
312
+ if (!keyValidation.valid && (!allowUnknown || unsafeKey)) {
215
313
  const reason = keyValidation.reason ? ` ${keyValidation.reason}.` : '';
314
+ const fixLines = ['Use "tospec config list" to see available keys.'];
315
+ if (!allowUnknown && !unsafeKey) {
316
+ fixLines.push('Pass --allow-unknown to read a key set that way.');
317
+ }
318
+ if (options.json) {
319
+ emitFailureStatus({ get: null }, {
320
+ severity: 'error',
321
+ code: 'config_error',
322
+ message: `Invalid configuration key "${key}".${reason}`,
323
+ fix: fixLines.join('\n'),
324
+ });
325
+ return;
326
+ }
216
327
  console.error(`Error: Invalid configuration key "${key}".${reason}`);
217
- console.error('Use "tospec config list" to see available keys.');
328
+ for (const line of fixLines)
329
+ console.error(line);
218
330
  process.exitCode = 1;
219
331
  return;
220
332
  }
221
333
  const config = getGlobalConfig();
222
334
  const value = getNestedValue(config, key);
223
335
  if (value === undefined) {
336
+ if (options.json) {
337
+ emitFailureStatus({ get: null }, {
338
+ severity: 'error',
339
+ code: 'config_error',
340
+ message: `Key "${key}" is not set.`,
341
+ // `workflows` is a list, so the generic `<value>` placeholder
342
+ // hides the one thing a caller needs: the comma-separated form.
343
+ fix: key === 'workflows'
344
+ ? `tospec config set workflows "${ALL_WORKFLOWS.slice(0, 3).join(',')}"`
345
+ : `tospec config set ${key} <value>`,
346
+ });
347
+ return;
348
+ }
224
349
  console.error(`Error: Key "${key}" is not set.`);
225
350
  process.exitCode = 1;
226
351
  return;
227
352
  }
353
+ if (options.json) {
354
+ // The value as stored, not stringified: the human branch below exists
355
+ // to be scriptable in a shell, which JSON does not need.
356
+ emitSuccess({ get: { key, value } });
357
+ return;
358
+ }
228
359
  if (typeof value === 'object' && value !== null) {
229
360
  console.log(JSON.stringify(value));
230
361
  }
@@ -232,13 +363,25 @@ export function registerConfigCommand(program) {
232
363
  console.log(String(value));
233
364
  }
234
365
  });
235
- // config set
236
366
  configCmd
237
367
  .command('set <key> <value>')
238
368
  .description('Set a value (auto-coerce types)')
239
369
  .option('--string', 'Force value to be stored as string')
240
370
  .option('--allow-unknown', 'Allow setting unknown keys')
371
+ .option('--json', 'Output as JSON')
241
372
  .action((key, value, options) => {
373
+ /** Every rejection below shares this null-shape, per the envelope contract. */
374
+ const fail = (message, fix) => {
375
+ if (options.json) {
376
+ emitFailureStatus(CONFIG_FAILURE_PAYLOADS.set, { severity: 'error', code: 'config_error', message, ...(fix ? { fix } : {}) });
377
+ return;
378
+ }
379
+ console.error(`Error: ${message}`);
380
+ if (fix)
381
+ for (const line of fix.split('\n'))
382
+ console.error(line);
383
+ process.exitCode = 1;
384
+ };
242
385
  const allowUnknown = Boolean(options.allowUnknown);
243
386
  const keyValidation = validateConfigKeyPath(key);
244
387
  // --allow-unknown relaxes the known-key check, but never the
@@ -246,60 +389,131 @@ export function registerConfigCommand(program) {
246
389
  const unsafeKey = hasUnsafeKeySegment(key);
247
390
  if (!keyValidation.valid && (!allowUnknown || unsafeKey)) {
248
391
  const reason = keyValidation.reason ? ` ${keyValidation.reason}.` : '';
249
- console.error(`Error: Invalid configuration key "${key}".${reason}`);
250
- console.error('Use "tospec config list" to see available keys.');
392
+ const fixLines = ['Use "tospec config list" to see available keys.'];
251
393
  if (!allowUnknown && !unsafeKey) {
252
- console.error('Pass --allow-unknown to bypass this check.');
394
+ fixLines.push('Pass --allow-unknown to bypass this check.');
253
395
  }
254
- process.exitCode = 1;
396
+ fail(`Invalid configuration key "${key}".${reason}`, fixLines.join('\n'));
255
397
  return;
256
398
  }
257
399
  const config = getGlobalConfig();
258
- const coercedValue = coerceValue(value, options.string || false);
259
- // Create a copy to validate before saving
400
+ const coercedValue = coerceValue(value, options.string || false, key);
401
+ // Validate a copy before touching the config that is saved.
260
402
  const newConfig = JSON.parse(JSON.stringify(config));
261
403
  setNestedValue(newConfig, key, coercedValue);
262
- // Validate the new config
263
404
  const validation = validateConfig(newConfig);
264
405
  if (!validation.success) {
265
- console.error(`Error: Invalid configuration - ${validation.error}`);
266
- process.exitCode = 1;
406
+ fail(`Invalid configuration - ${validation.error}`);
267
407
  return;
268
408
  }
269
- // Apply changes and save
409
+ // The schema types `workflows` as string[], so a misspelled id passes it
410
+ // and is later dropped by `update`'s filter against ALL_WORKFLOWS. This is
411
+ // the last point that still has the word the user typed.
412
+ if (key === 'workflows' && Array.isArray(coercedValue)) {
413
+ const unknown = coercedValue.filter((entry) => !ALL_WORKFLOWS.includes(String(entry)));
414
+ if (unknown.length > 0) {
415
+ fail(`Unknown workflow(s): ${unknown.join(', ')}`, `Available: ${ALL_WORKFLOWS.join(', ')}`);
416
+ return;
417
+ }
418
+ }
270
419
  setNestedValue(config, key, coercedValue);
271
420
  saveGlobalConfig(config);
421
+ // Only `custom` reads `workflows`; under `core` getProfileWorkflows
422
+ // discards it. Said alongside the confirmation, not instead of it: the
423
+ // value genuinely was stored, it is just not what `update` acts on.
424
+ const profile = config.profile ?? 'core';
425
+ const workflowsIgnored = key === 'workflows' && profile !== 'custom';
426
+ // The mirror image: `config profile custom` refuses without a list, but
427
+ // `set` is the raw write and stays permissive — so it has to say what
428
+ // that write means, or the next `init` installs zero skills unannounced.
429
+ const storedWorkflows = config.workflows;
430
+ const profileNeedsWorkflows = key === 'profile' &&
431
+ coercedValue === 'custom' &&
432
+ (!storedWorkflows || storedWorkflows.length === 0);
433
+ const warnings = [];
434
+ if (workflowsIgnored) {
435
+ warnings.push({
436
+ severity: 'warning',
437
+ code: 'config_workflows_ignored',
438
+ message: `Profile '${profile}' installs every workflow, so this list is not used.`,
439
+ fix: 'Run "tospec config profile custom" to make it take effect.',
440
+ });
441
+ }
442
+ if (profileNeedsWorkflows) {
443
+ warnings.push({
444
+ severity: 'warning',
445
+ code: 'config_profile_needs_workflows',
446
+ message: 'Profile "custom" needs a workflows list, and none is configured, so init and update install no skills.',
447
+ fix: `tospec config set workflows "${ALL_WORKFLOWS.slice(0, 3).join(',')}"`,
448
+ });
449
+ }
450
+ if (options.json) {
451
+ emitSuccess({
452
+ set: { key, value: coercedValue },
453
+ ...(warnings.length > 0 ? { status: warnings } : {}),
454
+ });
455
+ return;
456
+ }
272
457
  const displayValue = typeof coercedValue === 'string' ? `"${coercedValue}"` : String(coercedValue);
273
458
  console.log(`Set ${key} = ${displayValue}`);
459
+ for (const warning of warnings) {
460
+ console.error(`Warning: ${warning.message}`);
461
+ if (warning.fix)
462
+ console.error(warning.fix);
463
+ }
274
464
  });
275
- // config unset
276
465
  configCmd
277
466
  .command('unset <key>')
278
467
  .description('Remove a key (revert to default)')
279
- .action((key) => {
468
+ .option('--json', 'Output as JSON')
469
+ .action((key, options) => {
280
470
  const config = getGlobalConfig();
281
471
  const existed = deleteNestedValue(config, key);
282
- if (existed) {
472
+ if (existed)
283
473
  saveGlobalConfig(config);
284
- console.log(`Unset ${key} (reverted to default)`);
285
- }
286
- else {
287
- console.log(`Key "${key}" was not set`);
474
+ if (options.json) {
475
+ // Success either way: unsetting an absent key is the state the caller
476
+ // asked for. `existed` is what says whether anything changed.
477
+ emitSuccess({ unset: { key, existed } });
478
+ return;
288
479
  }
480
+ console.log(existed ? `Unset ${key} (reverted to default)` : `Key "${key}" was not set`);
289
481
  });
290
- // config reset
291
482
  configCmd
292
483
  .command('reset')
293
484
  .description('Reset configuration to defaults')
294
485
  .option('--all', 'Reset all configuration (required)')
295
486
  .option('-y, --yes', 'Skip confirmation prompts')
487
+ .option('--json', 'Output as JSON (non-interactive; requires --all and --yes)')
296
488
  .action(async (options) => {
489
+ // `list`, `get` and `set` all speak --json, so reset must too.
297
490
  if (!options.all) {
491
+ if (options.json) {
492
+ emitFailureStatus({ config: null }, {
493
+ severity: 'error',
494
+ code: 'config_error',
495
+ message: '--all flag is required for reset.',
496
+ fix: 'tospec config reset --all --yes --json',
497
+ });
498
+ return;
499
+ }
298
500
  console.error('Error: --all flag is required for reset');
299
501
  console.error('Usage: tospec config reset --all [-y]');
300
502
  process.exitCode = 1;
301
503
  return;
302
504
  }
505
+ // --json is non-interactive by contract: a prompt would block a caller
506
+ // that cannot answer and put non-JSON on stdout. Refuse rather than reset
507
+ // unasked — the flag must not become a quieter `--yes`.
508
+ if (options.json && !options.yes) {
509
+ emitFailureStatus({ config: null }, {
510
+ severity: 'error',
511
+ code: 'config_error',
512
+ message: '--json cannot prompt for confirmation.',
513
+ fix: 'tospec config reset --all --yes --json',
514
+ });
515
+ return;
516
+ }
303
517
  if (!options.yes) {
304
518
  const { confirm } = await import('@inquirer/prompts');
305
519
  let confirmed;
@@ -323,9 +537,12 @@ export function registerConfigCommand(program) {
323
537
  }
324
538
  }
325
539
  saveGlobalConfig({ ...DEFAULT_CONFIG });
540
+ if (options.json) {
541
+ emitSuccess({ config: getGlobalConfig() });
542
+ return;
543
+ }
326
544
  console.log('Configuration reset to defaults');
327
545
  });
328
- // config edit
329
546
  configCmd
330
547
  .command('edit')
331
548
  .description('Open config in $EDITOR')
@@ -339,13 +556,10 @@ export function registerConfigCommand(program) {
339
556
  return;
340
557
  }
341
558
  const configPath = getGlobalConfigPath();
342
- // Ensure config file exists with defaults
343
559
  if (!fs.existsSync(configPath)) {
344
560
  saveGlobalConfig({ ...DEFAULT_CONFIG });
345
561
  }
346
- // Spawn editor and wait for it to close
347
- // Avoid shell parsing to correctly handle paths with spaces in both
348
- // the editor path and config path
562
+ // shell: false so a space in the editor or config path is not re-parsed.
349
563
  const child = spawn(editor, [configPath], {
350
564
  stdio: 'inherit',
351
565
  shell: false,
@@ -363,7 +577,11 @@ export function registerConfigCommand(program) {
363
577
  });
364
578
  try {
365
579
  const rawConfig = fs.readFileSync(configPath, 'utf-8');
366
- const parsedConfig = JSON.parse(rawConfig);
580
+ // `config edit` just handed this file to the user's editor, and a
581
+ // Windows editor saving it back adds a BOM nobody asked for. Rejecting
582
+ // the result as "Invalid JSON" would blame the user for an edit tospec
583
+ // invited.
584
+ const parsedConfig = JSON.parse(stripBom(rawConfig));
367
585
  const validation = validateConfig(parsedConfig);
368
586
  if (!validation.success) {
369
587
  console.error(`Error: Invalid configuration - ${validation.error}`);
@@ -384,32 +602,71 @@ export function registerConfigCommand(program) {
384
602
  process.exitCode = 1;
385
603
  }
386
604
  });
387
- // config profile [preset]
388
605
  configCmd
389
606
  .command('profile [preset]')
390
607
  .description('Configure workflow profile (interactive picker or preset shortcut)')
391
- .action(async (preset) => {
392
- // Preset shortcut: `tospec config profile core`
608
+ .option('--json', 'Output as JSON (non-interactive; requires a preset)')
609
+ .action(async (preset, options) => {
610
+ /** Every rejection below shares this null-shape, per the envelope contract. */
611
+ const fail = (message, fix) => {
612
+ if (options.json) {
613
+ emitFailureStatus(CONFIG_FAILURE_PAYLOADS.profile, { severity: 'error', code: 'config_error', message, ...(fix ? { fix } : {}) });
614
+ return;
615
+ }
616
+ console.error(`Error: ${message}`);
617
+ if (fix)
618
+ console.error(`Fix: ${fix}`);
619
+ process.exitCode = 1;
620
+ };
621
+ /** The resolved state, so a caller need not re-read it with `config list`. */
622
+ const reportApplied = (config) => {
623
+ if (options.json) {
624
+ emitSuccess({ profile: resolveCurrentProfileState(config) });
625
+ return;
626
+ }
627
+ printConfigProfileApplyGuidance();
628
+ };
629
+ // The stored list is dropped, not rewritten to ALL_WORKFLOWS: rewriting it
630
+ // manufactures the state `ignoredWorkflowsDiagnostic` warns about.
393
631
  if (preset === 'core') {
394
632
  const config = getGlobalConfig();
395
633
  config.profile = 'core';
396
- config.workflows = [...ALL_WORKFLOWS];
634
+ delete config.workflows;
397
635
  saveGlobalConfig(config);
398
- printConfigProfileApplyGuidance();
636
+ reportApplied(config);
637
+ return;
638
+ }
639
+ // The command `config set workflows` and `config list` both name as the
640
+ // way to make a workflow list take effect.
641
+ if (preset === 'custom') {
642
+ const config = getGlobalConfig();
643
+ // Without a list, `custom` resolves to zero workflows and the next
644
+ // `tospec update` deletes every installed skill directory.
645
+ if (!config.workflows || config.workflows.length === 0) {
646
+ fail('Profile "custom" needs a workflows list, and none is configured.', `tospec config set workflows "${ALL_WORKFLOWS.slice(0, 3).join(',')}"`);
647
+ return;
648
+ }
649
+ config.profile = 'custom';
650
+ saveGlobalConfig(config);
651
+ reportApplied(config);
399
652
  return;
400
653
  }
401
654
  if (preset) {
402
- console.error(`Error: Unknown profile preset "${preset}". Available presets: core`);
403
- process.exitCode = 1;
655
+ fail(`Unknown profile preset "${preset}". Available presets: core, custom`);
656
+ return;
657
+ }
658
+ // --json has no way to answer a picker, so it never opens one: the
659
+ // failure names the presets rather than hanging on a prompt no caller
660
+ // will see.
661
+ if (options.json) {
662
+ fail('Interactive profile selection is not available under --json.', 'tospec config profile core --json | tospec config profile custom --json');
404
663
  return;
405
664
  }
406
- // Non-interactive check
407
665
  if (!process.stdout.isTTY) {
408
- console.error('Interactive mode required. Use `tospec config profile core` or set config via environment/flags.');
666
+ console.error('Interactive mode required. Use `tospec config profile core|custom` or set config via environment/flags.');
409
667
  process.exitCode = 1;
410
668
  return;
411
669
  }
412
- // Interactive picker
413
670
  const { select, checkbox, confirm } = await import('@inquirer/prompts');
414
671
  try {
415
672
  const config = getGlobalConfig();
@@ -443,9 +700,8 @@ export function registerConfigCommand(program) {
443
700
  };
444
701
  if (action === 'workflows') {
445
702
  const formatWorkflowChoice = (workflow) => {
446
- // The map is complete by construction (Record<WorkflowId>); this
447
- // fallback is only for an id a user's custom config carries that
448
- // this build no longer ships.
703
+ // The map is complete by construction; this only catches an id a
704
+ // user's custom config carries that this build no longer ships.
449
705
  const metadata = WORKFLOW_PROMPT_META[workflow] ?? {
450
706
  name: workflow,
451
707
  description: `Workflow: ${workflow}`,
@@ -483,9 +739,13 @@ export function registerConfigCommand(program) {
483
739
  }
484
740
  console.log();
485
741
  config.profile = nextState.profile;
486
- config.workflows = nextState.workflows;
742
+ // Same rule as the `core` preset above: a list stored under `core` is an
743
+ // ignored key every later `config list` warns about.
744
+ if (nextState.profile === 'core')
745
+ delete config.workflows;
746
+ else
747
+ config.workflows = nextState.workflows;
487
748
  saveGlobalConfig(config);
488
- // Check if inside an Tospec project
489
749
  const projectDir = process.cwd();
490
750
  const tospecDir = path.join(projectDir, TOSPEC_DIR_NAME);
491
751
  if (fs.existsSync(tospecDir)) {
@@ -495,9 +755,8 @@ export function registerConfigCommand(program) {
495
755
  });
496
756
  if (applyNow) {
497
757
  try {
498
- // Run in-process instead of `npx tospec update`, which depends on
499
- // tospec/npx resolving on PATH and fails under global installs or
500
- // monorepos.
758
+ // In-process, not `npx tospec update`: that depends on PATH and
759
+ // fails under global installs and monorepos.
501
760
  await new UpdateCommand().execute(projectDir);
502
761
  console.log('Run `tospec update` in your other projects to apply.');
503
762
  }