@seanmars/tospec 0.19.0-beta.8 → 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 (421) hide show
  1. package/CHANGELOG.md +60 -310
  2. package/README.md +69 -82
  3. package/assets/dashboard/app.js +11 -0
  4. package/assets/dashboard/style.css +7 -0
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +89 -106
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/commands/config.d.ts +9 -17
  9. package/dist/commands/config.d.ts.map +1 -1
  10. package/dist/commands/config.js +293 -145
  11. package/dist/commands/config.js.map +1 -1
  12. package/dist/commands/dashboard.d.ts +57 -99
  13. package/dist/commands/dashboard.d.ts.map +1 -1
  14. package/dist/commands/dashboard.js +190 -313
  15. package/dist/commands/dashboard.js.map +1 -1
  16. package/dist/commands/decision.d.ts +38 -27
  17. package/dist/commands/decision.d.ts.map +1 -1
  18. package/dist/commands/decision.js +296 -128
  19. package/dist/commands/decision.js.map +1 -1
  20. package/dist/commands/metrics.d.ts +28 -51
  21. package/dist/commands/metrics.d.ts.map +1 -1
  22. package/dist/commands/metrics.js +62 -93
  23. package/dist/commands/metrics.js.map +1 -1
  24. package/dist/commands/shared-output.d.ts +12 -27
  25. package/dist/commands/shared-output.d.ts.map +1 -1
  26. package/dist/commands/shared-output.js +22 -45
  27. package/dist/commands/shared-output.js.map +1 -1
  28. package/dist/commands/show.d.ts +4 -7
  29. package/dist/commands/show.d.ts.map +1 -1
  30. package/dist/commands/show.js +23 -11
  31. package/dist/commands/show.js.map +1 -1
  32. package/dist/commands/validate.d.ts +34 -58
  33. package/dist/commands/validate.d.ts.map +1 -1
  34. package/dist/commands/validate.js +225 -141
  35. package/dist/commands/validate.js.map +1 -1
  36. package/dist/commands/workflow/index.d.ts +1 -5
  37. package/dist/commands/workflow/index.d.ts.map +1 -1
  38. package/dist/commands/workflow/index.js +1 -5
  39. package/dist/commands/workflow/index.js.map +1 -1
  40. package/dist/commands/workflow/instructions.d.ts +14 -24
  41. package/dist/commands/workflow/instructions.d.ts.map +1 -1
  42. package/dist/commands/workflow/instructions.js +219 -128
  43. package/dist/commands/workflow/instructions.js.map +1 -1
  44. package/dist/commands/workflow/new-change.d.ts +2 -5
  45. package/dist/commands/workflow/new-change.d.ts.map +1 -1
  46. package/dist/commands/workflow/new-change.js +69 -32
  47. package/dist/commands/workflow/new-change.js.map +1 -1
  48. package/dist/commands/workflow/schemas.d.ts +1 -5
  49. package/dist/commands/workflow/schemas.d.ts.map +1 -1
  50. package/dist/commands/workflow/schemas.js +6 -17
  51. package/dist/commands/workflow/schemas.js.map +1 -1
  52. package/dist/commands/workflow/shared.d.ts +37 -42
  53. package/dist/commands/workflow/shared.d.ts.map +1 -1
  54. package/dist/commands/workflow/shared.js +23 -54
  55. package/dist/commands/workflow/shared.js.map +1 -1
  56. package/dist/commands/workflow/status.d.ts +7 -17
  57. package/dist/commands/workflow/status.d.ts.map +1 -1
  58. package/dist/commands/workflow/status.js +57 -72
  59. package/dist/commands/workflow/status.js.map +1 -1
  60. package/dist/commands/workflow/templates.d.ts +8 -8
  61. package/dist/commands/workflow/templates.d.ts.map +1 -1
  62. package/dist/commands/workflow/templates.js +32 -46
  63. package/dist/commands/workflow/templates.js.map +1 -1
  64. package/dist/core/archive.d.ts +22 -31
  65. package/dist/core/archive.d.ts.map +1 -1
  66. package/dist/core/archive.js +296 -283
  67. package/dist/core/archive.js.map +1 -1
  68. package/dist/core/artifact-graph/graph.d.ts +25 -42
  69. package/dist/core/artifact-graph/graph.d.ts.map +1 -1
  70. package/dist/core/artifact-graph/graph.js +45 -63
  71. package/dist/core/artifact-graph/graph.js.map +1 -1
  72. package/dist/core/artifact-graph/index.d.ts +1 -1
  73. package/dist/core/artifact-graph/index.d.ts.map +1 -1
  74. package/dist/core/artifact-graph/index.js +1 -1
  75. package/dist/core/artifact-graph/index.js.map +1 -1
  76. package/dist/core/artifact-graph/instruction-loader.d.ts +54 -120
  77. package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
  78. package/dist/core/artifact-graph/instruction-loader.js +129 -111
  79. package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
  80. package/dist/core/artifact-graph/outputs.d.ts +9 -23
  81. package/dist/core/artifact-graph/outputs.d.ts.map +1 -1
  82. package/dist/core/artifact-graph/outputs.js +45 -38
  83. package/dist/core/artifact-graph/outputs.js.map +1 -1
  84. package/dist/core/artifact-graph/resolver.d.ts +36 -81
  85. package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
  86. package/dist/core/artifact-graph/resolver.js +60 -101
  87. package/dist/core/artifact-graph/resolver.js.map +1 -1
  88. package/dist/core/artifact-graph/schema.d.ts +0 -6
  89. package/dist/core/artifact-graph/schema.d.ts.map +1 -1
  90. package/dist/core/artifact-graph/schema.js +7 -32
  91. package/dist/core/artifact-graph/schema.js.map +1 -1
  92. package/dist/core/artifact-graph/state.d.ts +1 -8
  93. package/dist/core/artifact-graph/state.d.ts.map +1 -1
  94. package/dist/core/artifact-graph/state.js +2 -17
  95. package/dist/core/artifact-graph/state.js.map +1 -1
  96. package/dist/core/artifact-graph/stub-detection.d.ts +6 -14
  97. package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -1
  98. package/dist/core/artifact-graph/stub-detection.js +13 -16
  99. package/dist/core/artifact-graph/stub-detection.js.map +1 -1
  100. package/dist/core/artifact-graph/types.d.ts +4 -0
  101. package/dist/core/artifact-graph/types.d.ts.map +1 -1
  102. package/dist/core/artifact-graph/types.js +30 -10
  103. package/dist/core/artifact-graph/types.js.map +1 -1
  104. package/dist/core/available-tools.d.ts +3 -12
  105. package/dist/core/available-tools.d.ts.map +1 -1
  106. package/dist/core/available-tools.js +4 -13
  107. package/dist/core/available-tools.js.map +1 -1
  108. package/dist/core/change-metadata/schema.d.ts +1 -1
  109. package/dist/core/change-metadata/schema.d.ts.map +1 -1
  110. package/dist/core/change-metadata/schema.js +10 -7
  111. package/dist/core/change-metadata/schema.js.map +1 -1
  112. package/dist/core/change-presenter.d.ts +16 -27
  113. package/dist/core/change-presenter.d.ts.map +1 -1
  114. package/dist/core/change-presenter.js +53 -53
  115. package/dist/core/change-presenter.js.map +1 -1
  116. package/dist/core/change-status-policy.d.ts +4 -8
  117. package/dist/core/change-status-policy.d.ts.map +1 -1
  118. package/dist/core/change-status-policy.js +9 -17
  119. package/dist/core/change-status-policy.js.map +1 -1
  120. package/dist/core/codex-metrics.d.ts +25 -45
  121. package/dist/core/codex-metrics.d.ts.map +1 -1
  122. package/dist/core/codex-metrics.js +44 -88
  123. package/dist/core/codex-metrics.js.map +1 -1
  124. package/dist/core/codex-residue.d.ts +14 -15
  125. package/dist/core/codex-residue.d.ts.map +1 -1
  126. package/dist/core/codex-residue.js +18 -22
  127. package/dist/core/codex-residue.js.map +1 -1
  128. package/dist/core/command-generation/adapters/claude.d.ts +2 -9
  129. package/dist/core/command-generation/adapters/claude.d.ts.map +1 -1
  130. package/dist/core/command-generation/adapters/claude.js +2 -12
  131. package/dist/core/command-generation/adapters/claude.js.map +1 -1
  132. package/dist/core/command-generation/adapters/index.d.ts +1 -9
  133. package/dist/core/command-generation/adapters/index.d.ts.map +1 -1
  134. package/dist/core/command-generation/adapters/index.js +1 -9
  135. package/dist/core/command-generation/adapters/index.js.map +1 -1
  136. package/dist/core/command-generation/generator.d.ts +0 -17
  137. package/dist/core/command-generation/generator.d.ts.map +1 -1
  138. package/dist/core/command-generation/generator.js +0 -17
  139. package/dist/core/command-generation/generator.js.map +1 -1
  140. package/dist/core/command-generation/index.d.ts +2 -5
  141. package/dist/core/command-generation/index.d.ts.map +1 -1
  142. package/dist/core/command-generation/index.js +0 -9
  143. package/dist/core/command-generation/index.js.map +1 -1
  144. package/dist/core/command-generation/types.d.ts +10 -36
  145. package/dist/core/command-generation/types.d.ts.map +1 -1
  146. package/dist/core/command-generation/types.js +0 -6
  147. package/dist/core/command-generation/types.js.map +1 -1
  148. package/dist/core/command-generation/yaml.d.ts +3 -18
  149. package/dist/core/command-generation/yaml.d.ts.map +1 -1
  150. package/dist/core/command-generation/yaml.js +5 -23
  151. package/dist/core/command-generation/yaml.js.map +1 -1
  152. package/dist/core/config-prompts.d.ts +2 -4
  153. package/dist/core/config-prompts.d.ts.map +1 -1
  154. package/dist/core/config-prompts.js +2 -7
  155. package/dist/core/config-prompts.js.map +1 -1
  156. package/dist/core/config-schema.d.ts +7 -41
  157. package/dist/core/config-schema.d.ts.map +1 -1
  158. package/dist/core/config-schema.js +35 -74
  159. package/dist/core/config-schema.js.map +1 -1
  160. package/dist/core/config.d.ts +25 -49
  161. package/dist/core/config.d.ts.map +1 -1
  162. package/dist/core/config.js +22 -45
  163. package/dist/core/config.js.map +1 -1
  164. package/dist/core/dashboard-activity.d.ts +7 -9
  165. package/dist/core/dashboard-activity.d.ts.map +1 -1
  166. package/dist/core/dashboard-activity.js +26 -24
  167. package/dist/core/dashboard-activity.js.map +1 -1
  168. package/dist/core/dashboard-data.d.ts +35 -22
  169. package/dist/core/dashboard-data.d.ts.map +1 -1
  170. package/dist/core/dashboard-data.js +57 -72
  171. package/dist/core/dashboard-data.js.map +1 -1
  172. package/dist/core/global-config.d.ts +24 -53
  173. package/dist/core/global-config.d.ts.map +1 -1
  174. package/dist/core/global-config.js +38 -67
  175. package/dist/core/global-config.js.map +1 -1
  176. package/dist/core/init.d.ts +12 -28
  177. package/dist/core/init.d.ts.map +1 -1
  178. package/dist/core/init.js +90 -168
  179. package/dist/core/init.js.map +1 -1
  180. package/dist/core/list.d.ts.map +1 -1
  181. package/dist/core/list.js +80 -38
  182. package/dist/core/list.js.map +1 -1
  183. package/dist/core/local-server.d.ts +41 -83
  184. package/dist/core/local-server.d.ts.map +1 -1
  185. package/dist/core/local-server.js +53 -98
  186. package/dist/core/local-server.js.map +1 -1
  187. package/dist/core/markdown-render.d.ts +15 -23
  188. package/dist/core/markdown-render.d.ts.map +1 -1
  189. package/dist/core/markdown-render.js +25 -34
  190. package/dist/core/markdown-render.js.map +1 -1
  191. package/dist/core/migrate.d.ts +19 -16
  192. package/dist/core/migrate.d.ts.map +1 -1
  193. package/dist/core/migrate.js +151 -135
  194. package/dist/core/migrate.js.map +1 -1
  195. package/dist/core/parsers/change-parser.d.ts +7 -10
  196. package/dist/core/parsers/change-parser.d.ts.map +1 -1
  197. package/dist/core/parsers/change-parser.js +48 -56
  198. package/dist/core/parsers/change-parser.js.map +1 -1
  199. package/dist/core/parsers/markdown-parser.d.ts +8 -9
  200. package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
  201. package/dist/core/parsers/markdown-parser.js +23 -30
  202. package/dist/core/parsers/markdown-parser.js.map +1 -1
  203. package/dist/core/parsers/requirement-blocks.d.ts +43 -15
  204. package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
  205. package/dist/core/parsers/requirement-blocks.js +142 -70
  206. package/dist/core/parsers/requirement-blocks.js.map +1 -1
  207. package/dist/core/parsers/requirement-text.d.ts +73 -79
  208. package/dist/core/parsers/requirement-text.d.ts.map +1 -1
  209. package/dist/core/parsers/requirement-text.js +137 -79
  210. package/dist/core/parsers/requirement-text.js.map +1 -1
  211. package/dist/core/parsers/spec-structure.d.ts.map +1 -1
  212. package/dist/core/parsers/spec-structure.js +10 -6
  213. package/dist/core/parsers/spec-structure.js.map +1 -1
  214. package/dist/core/profiles.d.ts +3 -10
  215. package/dist/core/profiles.d.ts.map +1 -1
  216. package/dist/core/profiles.js +5 -12
  217. package/dist/core/profiles.js.map +1 -1
  218. package/dist/core/project-config.d.ts +43 -44
  219. package/dist/core/project-config.d.ts.map +1 -1
  220. package/dist/core/project-config.js +107 -82
  221. package/dist/core/project-config.js.map +1 -1
  222. package/dist/core/project-layout.d.ts +9 -17
  223. package/dist/core/project-layout.d.ts.map +1 -1
  224. package/dist/core/project-layout.js +16 -26
  225. package/dist/core/project-layout.js.map +1 -1
  226. package/dist/core/root-selection.d.ts +8 -14
  227. package/dist/core/root-selection.d.ts.map +1 -1
  228. package/dist/core/root-selection.js +3 -6
  229. package/dist/core/root-selection.js.map +1 -1
  230. package/dist/core/rules.d.ts.map +1 -1
  231. package/dist/core/rules.js +2 -3
  232. package/dist/core/rules.js.map +1 -1
  233. package/dist/core/schema-names.d.ts +16 -0
  234. package/dist/core/schema-names.d.ts.map +1 -0
  235. package/dist/core/schema-names.js +16 -0
  236. package/dist/core/schema-names.js.map +1 -0
  237. package/dist/core/schemas/base.schema.d.ts.map +1 -1
  238. package/dist/core/schemas/base.schema.js +6 -12
  239. package/dist/core/schemas/base.schema.js.map +1 -1
  240. package/dist/core/schemas/change.schema.d.ts +8 -0
  241. package/dist/core/schemas/change.schema.d.ts.map +1 -1
  242. package/dist/core/schemas/change.schema.js +41 -10
  243. package/dist/core/schemas/change.schema.js.map +1 -1
  244. package/dist/core/shared/index.d.ts +2 -7
  245. package/dist/core/shared/index.d.ts.map +1 -1
  246. package/dist/core/shared/index.js +2 -7
  247. package/dist/core/shared/index.js.map +1 -1
  248. package/dist/core/shared/rules-generation.d.ts +5 -15
  249. package/dist/core/shared/rules-generation.d.ts.map +1 -1
  250. package/dist/core/shared/rules-generation.js +33 -37
  251. package/dist/core/shared/rules-generation.js.map +1 -1
  252. package/dist/core/shared/skill-generation.d.ts +28 -43
  253. package/dist/core/shared/skill-generation.d.ts.map +1 -1
  254. package/dist/core/shared/skill-generation.js +82 -51
  255. package/dist/core/shared/skill-generation.js.map +1 -1
  256. package/dist/core/shared/tool-detection.d.ts +35 -76
  257. package/dist/core/shared/tool-detection.d.ts.map +1 -1
  258. package/dist/core/shared/tool-detection.js +73 -93
  259. package/dist/core/shared/tool-detection.js.map +1 -1
  260. package/dist/core/skill-metrics.d.ts +36 -63
  261. package/dist/core/skill-metrics.d.ts.map +1 -1
  262. package/dist/core/skill-metrics.js +34 -73
  263. package/dist/core/skill-metrics.js.map +1 -1
  264. package/dist/core/spec-presenter.d.ts.map +1 -1
  265. package/dist/core/spec-presenter.js +5 -10
  266. package/dist/core/spec-presenter.js.map +1 -1
  267. package/dist/core/specs-apply.d.ts +16 -31
  268. package/dist/core/specs-apply.d.ts.map +1 -1
  269. package/dist/core/specs-apply.js +146 -195
  270. package/dist/core/specs-apply.js.map +1 -1
  271. package/dist/core/templates/fragments/interview.d.ts +2 -6
  272. package/dist/core/templates/fragments/interview.d.ts.map +1 -1
  273. package/dist/core/templates/fragments/interview.js +2 -6
  274. package/dist/core/templates/fragments/interview.js.map +1 -1
  275. package/dist/core/templates/fragments/next-step.d.ts +4 -8
  276. package/dist/core/templates/fragments/next-step.d.ts.map +1 -1
  277. package/dist/core/templates/fragments/next-step.js +4 -8
  278. package/dist/core/templates/fragments/next-step.js.map +1 -1
  279. package/dist/core/templates/fragments/validate.d.ts +13 -0
  280. package/dist/core/templates/fragments/validate.d.ts.map +1 -0
  281. package/dist/core/templates/fragments/validate.js +13 -0
  282. package/dist/core/templates/fragments/validate.js.map +1 -0
  283. package/dist/core/templates/fragments/verify.d.ts +9 -12
  284. package/dist/core/templates/fragments/verify.d.ts.map +1 -1
  285. package/dist/core/templates/fragments/verify.js +9 -12
  286. package/dist/core/templates/fragments/verify.js.map +1 -1
  287. package/dist/core/templates/index.d.ts +0 -6
  288. package/dist/core/templates/index.d.ts.map +1 -1
  289. package/dist/core/templates/index.js +0 -7
  290. package/dist/core/templates/index.js.map +1 -1
  291. package/dist/core/templates/skill-templates.d.ts +1 -5
  292. package/dist/core/templates/skill-templates.d.ts.map +1 -1
  293. package/dist/core/templates/skill-templates.js +0 -5
  294. package/dist/core/templates/skill-templates.js.map +1 -1
  295. package/dist/core/templates/types.d.ts +3 -7
  296. package/dist/core/templates/types.d.ts.map +1 -1
  297. package/dist/core/templates/types.js +0 -3
  298. package/dist/core/templates/types.js.map +1 -1
  299. package/dist/core/templates/workflows/apply.d.ts +3 -9
  300. package/dist/core/templates/workflows/apply.d.ts.map +1 -1
  301. package/dist/core/templates/workflows/apply.js +11 -13
  302. package/dist/core/templates/workflows/apply.js.map +1 -1
  303. package/dist/core/templates/workflows/archive.d.ts +0 -6
  304. package/dist/core/templates/workflows/archive.d.ts.map +1 -1
  305. package/dist/core/templates/workflows/archive.js +16 -5
  306. package/dist/core/templates/workflows/archive.js.map +1 -1
  307. package/dist/core/templates/workflows/decision.js +3 -3
  308. package/dist/core/templates/workflows/decision.js.map +1 -1
  309. package/dist/core/templates/workflows/explore.js +1 -1
  310. package/dist/core/templates/workflows/grill.d.ts.map +1 -1
  311. package/dist/core/templates/workflows/grill.js +0 -2
  312. package/dist/core/templates/workflows/grill.js.map +1 -1
  313. package/dist/core/templates/workflows/issue.d.ts +0 -6
  314. package/dist/core/templates/workflows/issue.d.ts.map +1 -1
  315. package/dist/core/templates/workflows/issue.js +3 -2
  316. package/dist/core/templates/workflows/issue.js.map +1 -1
  317. package/dist/core/templates/workflows/propose.d.ts +0 -6
  318. package/dist/core/templates/workflows/propose.d.ts.map +1 -1
  319. package/dist/core/templates/workflows/propose.js +2 -2
  320. package/dist/core/templates/workflows/propose.js.map +1 -1
  321. package/dist/core/templates/workflows/sync.d.ts +2 -8
  322. package/dist/core/templates/workflows/sync.d.ts.map +1 -1
  323. package/dist/core/templates/workflows/sync.js +4 -3
  324. package/dist/core/templates/workflows/sync.js.map +1 -1
  325. package/dist/core/templates/workflows/update.d.ts +0 -6
  326. package/dist/core/templates/workflows/update.d.ts.map +1 -1
  327. package/dist/core/templates/workflows/update.js +9 -2
  328. package/dist/core/templates/workflows/update.js.map +1 -1
  329. package/dist/core/update.d.ts +10 -36
  330. package/dist/core/update.d.ts.map +1 -1
  331. package/dist/core/update.js +59 -126
  332. package/dist/core/update.js.map +1 -1
  333. package/dist/core/user-state-migration.d.ts +13 -15
  334. package/dist/core/user-state-migration.d.ts.map +1 -1
  335. package/dist/core/user-state-migration.js +16 -20
  336. package/dist/core/user-state-migration.js.map +1 -1
  337. package/dist/core/validation/constants.d.ts +4 -10
  338. package/dist/core/validation/constants.d.ts.map +1 -1
  339. package/dist/core/validation/constants.js +25 -25
  340. package/dist/core/validation/constants.js.map +1 -1
  341. package/dist/core/validation/prose-length.d.ts +15 -0
  342. package/dist/core/validation/prose-length.d.ts.map +1 -0
  343. package/dist/core/validation/prose-length.js +29 -0
  344. package/dist/core/validation/prose-length.js.map +1 -0
  345. package/dist/core/validation/purpose-placeholder.d.ts +9 -16
  346. package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
  347. package/dist/core/validation/purpose-placeholder.js +30 -44
  348. package/dist/core/validation/purpose-placeholder.js.map +1 -1
  349. package/dist/core/validation/section-validator.d.ts +4 -4
  350. package/dist/core/validation/section-validator.d.ts.map +1 -1
  351. package/dist/core/validation/section-validator.js +30 -12
  352. package/dist/core/validation/section-validator.js.map +1 -1
  353. package/dist/core/validation/task-numbering.d.ts +6 -3
  354. package/dist/core/validation/task-numbering.d.ts.map +1 -1
  355. package/dist/core/validation/task-numbering.js +23 -11
  356. package/dist/core/validation/task-numbering.js.map +1 -1
  357. package/dist/core/validation/types.d.ts +18 -0
  358. package/dist/core/validation/types.d.ts.map +1 -1
  359. package/dist/core/validation/types.js +12 -1
  360. package/dist/core/validation/types.js.map +1 -1
  361. package/dist/core/validation/validator.d.ts +42 -68
  362. package/dist/core/validation/validator.d.ts.map +1 -1
  363. package/dist/core/validation/validator.js +468 -285
  364. package/dist/core/validation/validator.js.map +1 -1
  365. package/dist/prompts/searchable-multi-select.d.ts +3 -8
  366. package/dist/prompts/searchable-multi-select.d.ts.map +1 -1
  367. package/dist/prompts/searchable-multi-select.js +16 -39
  368. package/dist/prompts/searchable-multi-select.js.map +1 -1
  369. package/dist/utils/change-metadata.d.ts +11 -50
  370. package/dist/utils/change-metadata.d.ts.map +1 -1
  371. package/dist/utils/change-metadata.js +48 -67
  372. package/dist/utils/change-metadata.js.map +1 -1
  373. package/dist/utils/change-utils.d.ts +24 -76
  374. package/dist/utils/change-utils.d.ts.map +1 -1
  375. package/dist/utils/change-utils.js +95 -142
  376. package/dist/utils/change-utils.js.map +1 -1
  377. package/dist/utils/file-lock.d.ts +39 -0
  378. package/dist/utils/file-lock.d.ts.map +1 -0
  379. package/dist/utils/file-lock.js +149 -0
  380. package/dist/utils/file-lock.js.map +1 -0
  381. package/dist/utils/file-system.d.ts +12 -32
  382. package/dist/utils/file-system.d.ts.map +1 -1
  383. package/dist/utils/file-system.js +16 -40
  384. package/dist/utils/file-system.js.map +1 -1
  385. package/dist/utils/frontmatter.d.ts +7 -11
  386. package/dist/utils/frontmatter.d.ts.map +1 -1
  387. package/dist/utils/frontmatter.js +11 -11
  388. package/dist/utils/frontmatter.js.map +1 -1
  389. package/dist/utils/interactive.d.ts +4 -9
  390. package/dist/utils/interactive.d.ts.map +1 -1
  391. package/dist/utils/interactive.js +2 -4
  392. package/dist/utils/interactive.js.map +1 -1
  393. package/dist/utils/item-discovery.d.ts +10 -20
  394. package/dist/utils/item-discovery.d.ts.map +1 -1
  395. package/dist/utils/item-discovery.js +31 -55
  396. package/dist/utils/item-discovery.js.map +1 -1
  397. package/dist/utils/link.d.ts +9 -18
  398. package/dist/utils/link.d.ts.map +1 -1
  399. package/dist/utils/link.js +9 -18
  400. package/dist/utils/link.js.map +1 -1
  401. package/dist/utils/requirement-diff.d.ts +13 -23
  402. package/dist/utils/requirement-diff.d.ts.map +1 -1
  403. package/dist/utils/requirement-diff.js +13 -23
  404. package/dist/utils/requirement-diff.js.map +1 -1
  405. package/dist/utils/spec-files.d.ts +7 -20
  406. package/dist/utils/spec-files.d.ts.map +1 -1
  407. package/dist/utils/spec-files.js +26 -51
  408. package/dist/utils/spec-files.js.map +1 -1
  409. package/dist/utils/task-progress.d.ts +11 -9
  410. package/dist/utils/task-progress.d.ts.map +1 -1
  411. package/dist/utils/task-progress.js +53 -32
  412. package/dist/utils/task-progress.js.map +1 -1
  413. package/dist/utils/timestamp.d.ts +5 -8
  414. package/dist/utils/timestamp.d.ts.map +1 -1
  415. package/dist/utils/timestamp.js +5 -8
  416. package/dist/utils/timestamp.js.map +1 -1
  417. package/package.json +2 -3
  418. package/schemas/issue/schema.yaml +8 -1
  419. package/schemas/issue/templates/spec.md +20 -3
  420. package/schemas/sdd/schema.yaml +20 -1
  421. package/schemas/sdd/templates/spec.md +20 -3
@@ -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,28 +79,19 @@ 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
  }
82
90
  /**
83
- * The finding that a stored `workflows` list will not be used, or null.
84
- *
85
- * Only the `custom` profile consults the list, so under any other profile it is
86
- * stored, validated, displayed — and ignored. `config set` warned about it once,
87
- * at the moment of setting; `config list` then showed the list again with
88
- * nothing to say it does not apply (round 4 report 16). Worded to match
89
- * `update`'s `update_workflows_ignored`, so the two commands say one thing.
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`.
90
95
  */
91
96
  function ignoredWorkflowsDiagnostic(config) {
92
97
  const profile = config.profile ?? 'core';
@@ -101,6 +106,25 @@ function ignoredWorkflowsDiagnostic(config) {
101
106
  fix: 'Run "tospec config profile custom" to apply it, or "tospec config unset workflows" to drop it.',
102
107
  };
103
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
+ }
104
128
  function stableWorkflowOrder(workflows) {
105
129
  const seen = new Set();
106
130
  const ordered = [];
@@ -120,9 +144,6 @@ function stableWorkflowOrder(workflows) {
120
144
  }
121
145
  return ordered;
122
146
  }
123
- /**
124
- * Build a user-facing diff summary between two profile states.
125
- */
126
147
  export function diffProfileState(before, after) {
127
148
  const lines = [];
128
149
  if (before.profile !== after.profile) {
@@ -150,33 +171,26 @@ export function diffProfileState(before, after) {
150
171
  };
151
172
  }
152
173
  /**
153
- * The config file's raw contents, used only to tell an explicit value from a
154
- * default — `getGlobalConfig()` has already merged the two and cannot answer it.
155
- *
156
- * Reads the file that was actually loaded, not the write target: the two differ
157
- * until a legacy install migrates.
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.
158
177
  */
159
178
  function rawGlobalConfig() {
160
179
  try {
161
180
  const configPath = getEffectiveConfigPath();
162
181
  if (fs.existsSync(configPath)) {
163
- return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
182
+ return JSON.parse(stripBom(fs.readFileSync(configPath, 'utf-8')));
164
183
  }
165
184
  }
166
185
  catch {
167
- // Unreadable or malformed: treat every value as a default rather than
168
- // failing a read-only listing.
186
+ // Unreadable: treat every value as a default rather than fail a listing.
169
187
  }
170
188
  return {};
171
189
  }
172
190
  /**
173
191
  * The workflows a profile resolves to — `core` expands to the full set, any
174
- * other profile uses its explicit list.
175
- *
176
- * Shared by the text and JSON branches of `config list`. The JSON branch used to
177
- * report only the stored config, leaving a caller unable to turn
178
- * `profile: "core"` into the workflow names the text branch prints; deriving
179
- * both from here is what keeps the two answers the same.
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.
180
194
  */
181
195
  function resolveListedWorkflows(config) {
182
196
  if (config.profile === 'core')
@@ -186,34 +200,40 @@ function resolveListedWorkflows(config) {
186
200
  function printConfigProfileApplyGuidance() {
187
201
  console.log('Config updated. Run `tospec update` in your projects to apply.');
188
202
  }
189
- /**
190
- * Register the config command and all its subcommands.
191
- *
192
- * @param program - The Commander program instance
193
- */
194
203
  export function registerConfigCommand(program) {
195
204
  const configCmd = program
196
205
  .command('config')
197
206
  .description('View and modify global tospec configuration')
198
207
  .option('--scope <scope>', 'Config scope (only "global" supported currently)')
199
- .hook('preAction', (thisCommand) => {
208
+ .hook('preAction', (thisCommand, actionCommand) => {
200
209
  const opts = thisCommand.opts();
201
210
  if (opts.scope && opts.scope !== 'global') {
202
- console.error('Error: Project-local config is not yet implemented');
203
- // Not `process.exit(1)`: this hook cannot stop the action by returning,
204
- // but exiting here would kill the process before stderr is guaranteed
205
- // to have flushed. runCli turns this into `process.exitCode = 1`.
206
- 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);
207
224
  }
208
225
  });
209
- // config path
210
226
  configCmd
211
227
  .command('path')
212
228
  .description('Show config file location')
213
- .action(() => {
229
+ .option('--json', 'Output as JSON')
230
+ .action((options) => {
231
+ if (options.json) {
232
+ emitSuccess({ path: getGlobalConfigPath() });
233
+ return;
234
+ }
214
235
  console.log(getGlobalConfigPath());
215
236
  });
216
- // config list
217
237
  configCmd
218
238
  .command('list')
219
239
  .description('Show all current settings')
@@ -221,22 +241,18 @@ export function registerConfigCommand(program) {
221
241
  .action((options) => {
222
242
  const config = getGlobalConfig();
223
243
  if (options.json) {
224
- // Nested, not spread. `GlobalConfigSchema` is `.passthrough()` and
244
+ // Nested, not spread: the schema is `.passthrough()` and
225
245
  // `config set --allow-unknown` writes any top-level key, so spreading
226
- // the config into the envelope let user data shadow the envelope's own
227
- // reserved fields — `config set version 9 --allow-unknown` replaced the
228
- // envelope `version`, and `root`/`status` were open the same way.
229
- // Giving the config its own key is what makes that structurally
230
- // impossible instead of patching one field name at a time.
231
- // See tospec/decisions/20260916_121544-config-list-json-nests-under-config-key.md
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
232
249
  //
233
- // `profileSettings` mirrors what the human branch prints below. The JSON
234
- // used to stop at the stored config, so a caller could read
235
- // `profile: "core"` and still not know which workflows that resolves to
236
- // — the one fact the human output spells out and the one an agent needs
237
- // to know whether a skill it wants is installed. Resolved here, beside
238
- // the text form, so the two cannot answer differently.
239
- const ignored = ignoredWorkflowsDiagnostic(config);
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);
240
256
  emitSuccess({
241
257
  config,
242
258
  profileSettings: {
@@ -244,22 +260,19 @@ export function registerConfigCommand(program) {
244
260
  explicit: rawGlobalConfig().profile !== undefined,
245
261
  workflows: resolveListedWorkflows(config),
246
262
  },
247
- ...(ignored ? { status: [ignored] } : {}),
263
+ ...(listStatus.length ? { status: listStatus } : {}),
248
264
  });
249
265
  }
250
266
  else {
251
267
  const rawConfig = rawGlobalConfig();
252
268
  console.log(yaml.stringify(config).trimEnd());
253
- // Round 4 report 16: the dump above is the stored config, so under a
254
- // profile that ignores `workflows` this output gave two different
255
- // answers for the same key — the stored list here and the profile's
256
- // resolved one below — with nothing saying the first is not used.
257
- // `config set` said so once, at the moment of setting, and never again.
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.
258
272
  const ignored = ignoredWorkflowsDiagnostic(config);
259
273
  if (ignored) {
260
274
  console.log(` ^ ${ignored.message}`);
261
275
  }
262
- // Annotate profile settings
263
276
  const profileSource = rawConfig.profile !== undefined ? '(explicit)' : '(default)';
264
277
  console.log(`\nProfile settings:`);
265
278
  console.log(` profile: ${config.profile} ${profileSource}`);
@@ -275,41 +288,74 @@ export function registerConfigCommand(program) {
275
288
  console.log();
276
289
  console.log(ignored.fix);
277
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);
297
+ }
278
298
  }
279
299
  });
280
- // config get
281
300
  configCmd
282
301
  .command('get <key>')
283
302
  .description('Get a specific value (raw, scriptable)')
284
303
  .option('--allow-unknown', 'Allow reading unknown keys')
304
+ .option('--json', 'Output as JSON')
285
305
  .action((key, options) => {
286
306
  const allowUnknown = Boolean(options.allowUnknown);
287
307
  const keyValidation = validateConfigKeyPath(key);
288
- // Paired with the same flag on `set`, which without this was a one-way
289
- // door: a key written with --allow-unknown showed up in `config list` and
290
- // could be removed by `unset`, but `get` — the scriptable reader the flag
291
- // exists to feed — refused to read it back.
292
- //
293
- // Relaxes the known-key check only, never the prototype-safety one; the
294
- // same split `set` makes, for the same reason.
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.
295
311
  const unsafeKey = hasUnsafeKeySegment(key);
296
312
  if (!keyValidation.valid && (!allowUnknown || unsafeKey)) {
297
313
  const reason = keyValidation.reason ? ` ${keyValidation.reason}.` : '';
298
- console.error(`Error: Invalid configuration key "${key}".${reason}`);
299
- console.error('Use "tospec config list" to see available keys.');
314
+ const fixLines = ['Use "tospec config list" to see available keys.'];
300
315
  if (!allowUnknown && !unsafeKey) {
301
- console.error('Pass --allow-unknown to read a key set that way.');
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;
302
326
  }
327
+ console.error(`Error: Invalid configuration key "${key}".${reason}`);
328
+ for (const line of fixLines)
329
+ console.error(line);
303
330
  process.exitCode = 1;
304
331
  return;
305
332
  }
306
333
  const config = getGlobalConfig();
307
334
  const value = getNestedValue(config, key);
308
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
+ }
309
349
  console.error(`Error: Key "${key}" is not set.`);
310
350
  process.exitCode = 1;
311
351
  return;
312
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
+ }
313
359
  if (typeof value === 'object' && value !== null) {
314
360
  console.log(JSON.stringify(value));
315
361
  }
@@ -317,13 +363,25 @@ export function registerConfigCommand(program) {
317
363
  console.log(String(value));
318
364
  }
319
365
  });
320
- // config set
321
366
  configCmd
322
367
  .command('set <key> <value>')
323
368
  .description('Set a value (auto-coerce types)')
324
369
  .option('--string', 'Force value to be stored as string')
325
370
  .option('--allow-unknown', 'Allow setting unknown keys')
371
+ .option('--json', 'Output as JSON')
326
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
+ };
327
385
  const allowUnknown = Boolean(options.allowUnknown);
328
386
  const keyValidation = validateConfigKeyPath(key);
329
387
  // --allow-unknown relaxes the known-key check, but never the
@@ -331,86 +389,131 @@ export function registerConfigCommand(program) {
331
389
  const unsafeKey = hasUnsafeKeySegment(key);
332
390
  if (!keyValidation.valid && (!allowUnknown || unsafeKey)) {
333
391
  const reason = keyValidation.reason ? ` ${keyValidation.reason}.` : '';
334
- console.error(`Error: Invalid configuration key "${key}".${reason}`);
335
- console.error('Use "tospec config list" to see available keys.');
392
+ const fixLines = ['Use "tospec config list" to see available keys.'];
336
393
  if (!allowUnknown && !unsafeKey) {
337
- console.error('Pass --allow-unknown to bypass this check.');
394
+ fixLines.push('Pass --allow-unknown to bypass this check.');
338
395
  }
339
- process.exitCode = 1;
396
+ fail(`Invalid configuration key "${key}".${reason}`, fixLines.join('\n'));
340
397
  return;
341
398
  }
342
399
  const config = getGlobalConfig();
343
400
  const coercedValue = coerceValue(value, options.string || false, key);
344
- // Create a copy to validate before saving
401
+ // Validate a copy before touching the config that is saved.
345
402
  const newConfig = JSON.parse(JSON.stringify(config));
346
403
  setNestedValue(newConfig, key, coercedValue);
347
- // Validate the new config
348
404
  const validation = validateConfig(newConfig);
349
405
  if (!validation.success) {
350
- console.error(`Error: Invalid configuration - ${validation.error}`);
351
- process.exitCode = 1;
406
+ fail(`Invalid configuration - ${validation.error}`);
352
407
  return;
353
408
  }
354
409
  // The schema types `workflows` as string[], so a misspelled id passes it
355
- // and is then dropped by `update`'s filter against ALL_WORKFLOWS —
356
- // installing fewer skills than asked for, silently. Caught here because
357
- // this is the only point that still has the word the user typed.
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.
358
412
  if (key === 'workflows' && Array.isArray(coercedValue)) {
359
413
  const unknown = coercedValue.filter((entry) => !ALL_WORKFLOWS.includes(String(entry)));
360
414
  if (unknown.length > 0) {
361
- console.error(`Error: Unknown workflow(s): ${unknown.join(', ')}`);
362
- console.error(`Available: ${ALL_WORKFLOWS.join(', ')}`);
363
- process.exitCode = 1;
415
+ fail(`Unknown workflow(s): ${unknown.join(', ')}`, `Available: ${ALL_WORKFLOWS.join(', ')}`);
364
416
  return;
365
417
  }
366
418
  }
367
- // Apply changes and save
368
419
  setNestedValue(config, key, coercedValue);
369
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
+ }
370
457
  const displayValue = typeof coercedValue === 'string' ? `"${coercedValue}"` : String(coercedValue);
371
458
  console.log(`Set ${key} = ${displayValue}`);
372
- // `workflows` is only read when the profile is `custom`; under `core`,
373
- // getProfileWorkflows discards it and returns ALL_WORKFLOWS. Everything up
374
- // to here — the key check, the workflow-name check, the "Set workflows =
375
- // ..." line, and `config list` showing it — tells the user it took effect.
376
- // Said after the confirmation rather than instead of it, because the value
377
- // genuinely was stored; it just is not what `update` will act on.
378
- if (key === 'workflows') {
379
- const profile = config.profile ?? 'core';
380
- if (profile !== 'custom') {
381
- console.error(`Warning: profile '${profile}' installs every workflow, so this list is not used.`);
382
- console.error('Run "tospec config profile custom" to make it take effect.');
383
- }
459
+ for (const warning of warnings) {
460
+ console.error(`Warning: ${warning.message}`);
461
+ if (warning.fix)
462
+ console.error(warning.fix);
384
463
  }
385
464
  });
386
- // config unset
387
465
  configCmd
388
466
  .command('unset <key>')
389
467
  .description('Remove a key (revert to default)')
390
- .action((key) => {
468
+ .option('--json', 'Output as JSON')
469
+ .action((key, options) => {
391
470
  const config = getGlobalConfig();
392
471
  const existed = deleteNestedValue(config, key);
393
- if (existed) {
472
+ if (existed)
394
473
  saveGlobalConfig(config);
395
- console.log(`Unset ${key} (reverted to default)`);
396
- }
397
- else {
398
- 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;
399
479
  }
480
+ console.log(existed ? `Unset ${key} (reverted to default)` : `Key "${key}" was not set`);
400
481
  });
401
- // config reset
402
482
  configCmd
403
483
  .command('reset')
404
484
  .description('Reset configuration to defaults')
405
485
  .option('--all', 'Reset all configuration (required)')
406
486
  .option('-y, --yes', 'Skip confirmation prompts')
487
+ .option('--json', 'Output as JSON (non-interactive; requires --all and --yes)')
407
488
  .action(async (options) => {
489
+ // `list`, `get` and `set` all speak --json, so reset must too.
408
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
+ }
409
500
  console.error('Error: --all flag is required for reset');
410
501
  console.error('Usage: tospec config reset --all [-y]');
411
502
  process.exitCode = 1;
412
503
  return;
413
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
+ }
414
517
  if (!options.yes) {
415
518
  const { confirm } = await import('@inquirer/prompts');
416
519
  let confirmed;
@@ -434,9 +537,12 @@ export function registerConfigCommand(program) {
434
537
  }
435
538
  }
436
539
  saveGlobalConfig({ ...DEFAULT_CONFIG });
540
+ if (options.json) {
541
+ emitSuccess({ config: getGlobalConfig() });
542
+ return;
543
+ }
437
544
  console.log('Configuration reset to defaults');
438
545
  });
439
- // config edit
440
546
  configCmd
441
547
  .command('edit')
442
548
  .description('Open config in $EDITOR')
@@ -450,13 +556,10 @@ export function registerConfigCommand(program) {
450
556
  return;
451
557
  }
452
558
  const configPath = getGlobalConfigPath();
453
- // Ensure config file exists with defaults
454
559
  if (!fs.existsSync(configPath)) {
455
560
  saveGlobalConfig({ ...DEFAULT_CONFIG });
456
561
  }
457
- // Spawn editor and wait for it to close
458
- // Avoid shell parsing to correctly handle paths with spaces in both
459
- // the editor path and config path
562
+ // shell: false so a space in the editor or config path is not re-parsed.
460
563
  const child = spawn(editor, [configPath], {
461
564
  stdio: 'inherit',
462
565
  shell: false,
@@ -474,7 +577,11 @@ export function registerConfigCommand(program) {
474
577
  });
475
578
  try {
476
579
  const rawConfig = fs.readFileSync(configPath, 'utf-8');
477
- 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));
478
585
  const validation = validateConfig(parsedConfig);
479
586
  if (!validation.success) {
480
587
  console.error(`Error: Invalid configuration - ${validation.error}`);
@@ -495,32 +602,71 @@ export function registerConfigCommand(program) {
495
602
  process.exitCode = 1;
496
603
  }
497
604
  });
498
- // config profile [preset]
499
605
  configCmd
500
606
  .command('profile [preset]')
501
607
  .description('Configure workflow profile (interactive picker or preset shortcut)')
502
- .action(async (preset) => {
503
- // 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.
504
631
  if (preset === 'core') {
505
632
  const config = getGlobalConfig();
506
633
  config.profile = 'core';
507
- config.workflows = [...ALL_WORKFLOWS];
634
+ delete config.workflows;
508
635
  saveGlobalConfig(config);
509
- 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);
510
652
  return;
511
653
  }
512
654
  if (preset) {
513
- console.error(`Error: Unknown profile preset "${preset}". Available presets: core`);
514
- 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');
515
663
  return;
516
664
  }
517
- // Non-interactive check
518
665
  if (!process.stdout.isTTY) {
519
- 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.');
520
667
  process.exitCode = 1;
521
668
  return;
522
669
  }
523
- // Interactive picker
524
670
  const { select, checkbox, confirm } = await import('@inquirer/prompts');
525
671
  try {
526
672
  const config = getGlobalConfig();
@@ -554,9 +700,8 @@ export function registerConfigCommand(program) {
554
700
  };
555
701
  if (action === 'workflows') {
556
702
  const formatWorkflowChoice = (workflow) => {
557
- // The map is complete by construction (Record<WorkflowId>); this
558
- // fallback is only for an id a user's custom config carries that
559
- // 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.
560
705
  const metadata = WORKFLOW_PROMPT_META[workflow] ?? {
561
706
  name: workflow,
562
707
  description: `Workflow: ${workflow}`,
@@ -594,9 +739,13 @@ export function registerConfigCommand(program) {
594
739
  }
595
740
  console.log();
596
741
  config.profile = nextState.profile;
597
- 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;
598
748
  saveGlobalConfig(config);
599
- // Check if inside an Tospec project
600
749
  const projectDir = process.cwd();
601
750
  const tospecDir = path.join(projectDir, TOSPEC_DIR_NAME);
602
751
  if (fs.existsSync(tospecDir)) {
@@ -606,9 +755,8 @@ export function registerConfigCommand(program) {
606
755
  });
607
756
  if (applyNow) {
608
757
  try {
609
- // Run in-process instead of `npx tospec update`, which depends on
610
- // tospec/npx resolving on PATH and fails under global installs or
611
- // monorepos.
758
+ // In-process, not `npx tospec update`: that depends on PATH and
759
+ // fails under global installs and monorepos.
612
760
  await new UpdateCommand().execute(projectDir);
613
761
  console.log('Run `tospec update` in your other projects to apply.');
614
762
  }