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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (428) hide show
  1. package/CHANGELOG.md +530 -0
  2. package/README.md +71 -77
  3. package/assets/dashboard/app.js +14 -3
  4. package/assets/dashboard/style.css +7 -0
  5. package/assets/rules/tospec/decision.md +3 -0
  6. package/assets/rules/tospec/single-source-of-truth.md +19 -0
  7. package/dist/cli/index.d.ts.map +1 -1
  8. package/dist/cli/index.js +254 -83
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/commands/config.d.ts +9 -17
  11. package/dist/commands/config.d.ts.map +1 -1
  12. package/dist/commands/config.js +374 -98
  13. package/dist/commands/config.js.map +1 -1
  14. package/dist/commands/dashboard.d.ts +64 -81
  15. package/dist/commands/dashboard.d.ts.map +1 -1
  16. package/dist/commands/dashboard.js +346 -230
  17. package/dist/commands/dashboard.js.map +1 -1
  18. package/dist/commands/decision.d.ts +41 -19
  19. package/dist/commands/decision.d.ts.map +1 -1
  20. package/dist/commands/decision.js +400 -70
  21. package/dist/commands/decision.js.map +1 -1
  22. package/dist/commands/metrics.d.ts +28 -51
  23. package/dist/commands/metrics.d.ts.map +1 -1
  24. package/dist/commands/metrics.js +69 -85
  25. package/dist/commands/metrics.js.map +1 -1
  26. package/dist/commands/shared-output.d.ts +18 -21
  27. package/dist/commands/shared-output.d.ts.map +1 -1
  28. package/dist/commands/shared-output.js +49 -23
  29. package/dist/commands/shared-output.js.map +1 -1
  30. package/dist/commands/show.d.ts +7 -0
  31. package/dist/commands/show.d.ts.map +1 -1
  32. package/dist/commands/show.js +39 -8
  33. package/dist/commands/show.js.map +1 -1
  34. package/dist/commands/validate.d.ts +47 -30
  35. package/dist/commands/validate.d.ts.map +1 -1
  36. package/dist/commands/validate.js +282 -108
  37. package/dist/commands/validate.js.map +1 -1
  38. package/dist/commands/workflow/index.d.ts +6 -10
  39. package/dist/commands/workflow/index.d.ts.map +1 -1
  40. package/dist/commands/workflow/index.js +6 -10
  41. package/dist/commands/workflow/index.js.map +1 -1
  42. package/dist/commands/workflow/instructions.d.ts +21 -8
  43. package/dist/commands/workflow/instructions.d.ts.map +1 -1
  44. package/dist/commands/workflow/instructions.js +254 -95
  45. package/dist/commands/workflow/instructions.js.map +1 -1
  46. package/dist/commands/workflow/new-change.d.ts +4 -5
  47. package/dist/commands/workflow/new-change.d.ts.map +1 -1
  48. package/dist/commands/workflow/new-change.js +90 -25
  49. package/dist/commands/workflow/new-change.js.map +1 -1
  50. package/dist/commands/workflow/schemas.d.ts +3 -5
  51. package/dist/commands/workflow/schemas.d.ts.map +1 -1
  52. package/dist/commands/workflow/schemas.js +37 -11
  53. package/dist/commands/workflow/schemas.js.map +1 -1
  54. package/dist/commands/workflow/shared.d.ts +48 -21
  55. package/dist/commands/workflow/shared.d.ts.map +1 -1
  56. package/dist/commands/workflow/shared.js +36 -33
  57. package/dist/commands/workflow/shared.js.map +1 -1
  58. package/dist/commands/workflow/status.d.ts +10 -6
  59. package/dist/commands/workflow/status.d.ts.map +1 -1
  60. package/dist/commands/workflow/status.js +84 -42
  61. package/dist/commands/workflow/status.js.map +1 -1
  62. package/dist/commands/workflow/templates.d.ts +10 -3
  63. package/dist/commands/workflow/templates.d.ts.map +1 -1
  64. package/dist/commands/workflow/templates.js +39 -40
  65. package/dist/commands/workflow/templates.js.map +1 -1
  66. package/dist/core/archive.d.ts +26 -21
  67. package/dist/core/archive.d.ts.map +1 -1
  68. package/dist/core/archive.js +386 -205
  69. package/dist/core/archive.js.map +1 -1
  70. package/dist/core/artifact-graph/graph.d.ts +29 -36
  71. package/dist/core/artifact-graph/graph.d.ts.map +1 -1
  72. package/dist/core/artifact-graph/graph.js +50 -58
  73. package/dist/core/artifact-graph/graph.js.map +1 -1
  74. package/dist/core/artifact-graph/index.d.ts +1 -1
  75. package/dist/core/artifact-graph/index.d.ts.map +1 -1
  76. package/dist/core/artifact-graph/index.js +1 -1
  77. package/dist/core/artifact-graph/index.js.map +1 -1
  78. package/dist/core/artifact-graph/instruction-loader.d.ts +105 -100
  79. package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
  80. package/dist/core/artifact-graph/instruction-loader.js +176 -110
  81. package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
  82. package/dist/core/artifact-graph/outputs.d.ts +11 -14
  83. package/dist/core/artifact-graph/outputs.d.ts.map +1 -1
  84. package/dist/core/artifact-graph/outputs.js +47 -29
  85. package/dist/core/artifact-graph/outputs.js.map +1 -1
  86. package/dist/core/artifact-graph/resolver.d.ts +44 -63
  87. package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
  88. package/dist/core/artifact-graph/resolver.js +85 -86
  89. package/dist/core/artifact-graph/resolver.js.map +1 -1
  90. package/dist/core/artifact-graph/schema.d.ts +0 -6
  91. package/dist/core/artifact-graph/schema.d.ts.map +1 -1
  92. package/dist/core/artifact-graph/schema.js +7 -32
  93. package/dist/core/artifact-graph/schema.js.map +1 -1
  94. package/dist/core/artifact-graph/state.d.ts +1 -8
  95. package/dist/core/artifact-graph/state.d.ts.map +1 -1
  96. package/dist/core/artifact-graph/state.js +2 -17
  97. package/dist/core/artifact-graph/state.js.map +1 -1
  98. package/dist/core/artifact-graph/stub-detection.d.ts +12 -0
  99. package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -0
  100. package/dist/core/artifact-graph/stub-detection.js +39 -0
  101. package/dist/core/artifact-graph/stub-detection.js.map +1 -0
  102. package/dist/core/artifact-graph/types.d.ts +4 -0
  103. package/dist/core/artifact-graph/types.d.ts.map +1 -1
  104. package/dist/core/artifact-graph/types.js +30 -10
  105. package/dist/core/artifact-graph/types.js.map +1 -1
  106. package/dist/core/available-tools.d.ts +3 -12
  107. package/dist/core/available-tools.d.ts.map +1 -1
  108. package/dist/core/available-tools.js +4 -13
  109. package/dist/core/available-tools.js.map +1 -1
  110. package/dist/core/change-metadata/schema.d.ts +1 -1
  111. package/dist/core/change-metadata/schema.d.ts.map +1 -1
  112. package/dist/core/change-metadata/schema.js +10 -7
  113. package/dist/core/change-metadata/schema.js.map +1 -1
  114. package/dist/core/change-presenter.d.ts +23 -18
  115. package/dist/core/change-presenter.d.ts.map +1 -1
  116. package/dist/core/change-presenter.js +102 -43
  117. package/dist/core/change-presenter.js.map +1 -1
  118. package/dist/core/change-status-policy.d.ts +8 -1
  119. package/dist/core/change-status-policy.d.ts.map +1 -1
  120. package/dist/core/change-status-policy.js +25 -1
  121. package/dist/core/change-status-policy.js.map +1 -1
  122. package/dist/core/codex-metrics.d.ts +25 -45
  123. package/dist/core/codex-metrics.d.ts.map +1 -1
  124. package/dist/core/codex-metrics.js +44 -88
  125. package/dist/core/codex-metrics.js.map +1 -1
  126. package/dist/core/codex-residue.d.ts +22 -0
  127. package/dist/core/codex-residue.d.ts.map +1 -0
  128. package/dist/core/codex-residue.js +61 -0
  129. package/dist/core/codex-residue.js.map +1 -0
  130. package/dist/core/command-generation/adapters/claude.d.ts +2 -9
  131. package/dist/core/command-generation/adapters/claude.d.ts.map +1 -1
  132. package/dist/core/command-generation/adapters/claude.js +2 -12
  133. package/dist/core/command-generation/adapters/claude.js.map +1 -1
  134. package/dist/core/command-generation/adapters/index.d.ts +1 -9
  135. package/dist/core/command-generation/adapters/index.d.ts.map +1 -1
  136. package/dist/core/command-generation/adapters/index.js +1 -9
  137. package/dist/core/command-generation/adapters/index.js.map +1 -1
  138. package/dist/core/command-generation/generator.d.ts +0 -17
  139. package/dist/core/command-generation/generator.d.ts.map +1 -1
  140. package/dist/core/command-generation/generator.js +0 -17
  141. package/dist/core/command-generation/generator.js.map +1 -1
  142. package/dist/core/command-generation/index.d.ts +2 -5
  143. package/dist/core/command-generation/index.d.ts.map +1 -1
  144. package/dist/core/command-generation/index.js +0 -9
  145. package/dist/core/command-generation/index.js.map +1 -1
  146. package/dist/core/command-generation/types.d.ts +10 -36
  147. package/dist/core/command-generation/types.d.ts.map +1 -1
  148. package/dist/core/command-generation/types.js +0 -6
  149. package/dist/core/command-generation/types.js.map +1 -1
  150. package/dist/core/command-generation/yaml.d.ts +3 -18
  151. package/dist/core/command-generation/yaml.d.ts.map +1 -1
  152. package/dist/core/command-generation/yaml.js +5 -23
  153. package/dist/core/command-generation/yaml.js.map +1 -1
  154. package/dist/core/config-prompts.d.ts +2 -4
  155. package/dist/core/config-prompts.d.ts.map +1 -1
  156. package/dist/core/config-prompts.js +2 -7
  157. package/dist/core/config-prompts.js.map +1 -1
  158. package/dist/core/config-schema.d.ts +8 -53
  159. package/dist/core/config-schema.d.ts.map +1 -1
  160. package/dist/core/config-schema.js +49 -62
  161. package/dist/core/config-schema.js.map +1 -1
  162. package/dist/core/config.d.ts +56 -0
  163. package/dist/core/config.d.ts.map +1 -1
  164. package/dist/core/config.js +73 -2
  165. package/dist/core/config.js.map +1 -1
  166. package/dist/core/converters/json-converter.d.ts.map +1 -1
  167. package/dist/core/dashboard-activity.d.ts +7 -9
  168. package/dist/core/dashboard-activity.d.ts.map +1 -1
  169. package/dist/core/dashboard-activity.js +26 -24
  170. package/dist/core/dashboard-activity.js.map +1 -1
  171. package/dist/core/dashboard-data.d.ts +40 -22
  172. package/dist/core/dashboard-data.d.ts.map +1 -1
  173. package/dist/core/dashboard-data.js +62 -70
  174. package/dist/core/dashboard-data.js.map +1 -1
  175. package/dist/core/global-config.d.ts +24 -53
  176. package/dist/core/global-config.d.ts.map +1 -1
  177. package/dist/core/global-config.js +38 -67
  178. package/dist/core/global-config.js.map +1 -1
  179. package/dist/core/init.d.ts +29 -11
  180. package/dist/core/init.d.ts.map +1 -1
  181. package/dist/core/init.js +232 -164
  182. package/dist/core/init.js.map +1 -1
  183. package/dist/core/list.d.ts +1 -1
  184. package/dist/core/list.d.ts.map +1 -1
  185. package/dist/core/list.js +121 -28
  186. package/dist/core/list.js.map +1 -1
  187. package/dist/core/local-server.d.ts +60 -50
  188. package/dist/core/local-server.d.ts.map +1 -1
  189. package/dist/core/local-server.js +94 -66
  190. package/dist/core/local-server.js.map +1 -1
  191. package/dist/core/markdown-render.d.ts +25 -0
  192. package/dist/core/markdown-render.d.ts.map +1 -0
  193. package/dist/core/markdown-render.js +94 -0
  194. package/dist/core/markdown-render.js.map +1 -0
  195. package/dist/core/migrate.d.ts +32 -15
  196. package/dist/core/migrate.d.ts.map +1 -1
  197. package/dist/core/migrate.js +220 -108
  198. package/dist/core/migrate.js.map +1 -1
  199. package/dist/core/parsers/change-parser.d.ts +7 -10
  200. package/dist/core/parsers/change-parser.d.ts.map +1 -1
  201. package/dist/core/parsers/change-parser.js +48 -56
  202. package/dist/core/parsers/change-parser.js.map +1 -1
  203. package/dist/core/parsers/markdown-parser.d.ts +8 -9
  204. package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
  205. package/dist/core/parsers/markdown-parser.js +31 -22
  206. package/dist/core/parsers/markdown-parser.js.map +1 -1
  207. package/dist/core/parsers/requirement-blocks.d.ts +43 -15
  208. package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
  209. package/dist/core/parsers/requirement-blocks.js +169 -54
  210. package/dist/core/parsers/requirement-blocks.js.map +1 -1
  211. package/dist/core/parsers/requirement-text.d.ts +73 -79
  212. package/dist/core/parsers/requirement-text.d.ts.map +1 -1
  213. package/dist/core/parsers/requirement-text.js +137 -79
  214. package/dist/core/parsers/requirement-text.js.map +1 -1
  215. package/dist/core/parsers/spec-structure.d.ts.map +1 -1
  216. package/dist/core/parsers/spec-structure.js +10 -6
  217. package/dist/core/parsers/spec-structure.js.map +1 -1
  218. package/dist/core/planning-home.js.map +1 -1
  219. package/dist/core/profiles.d.ts +3 -10
  220. package/dist/core/profiles.d.ts.map +1 -1
  221. package/dist/core/profiles.js +5 -12
  222. package/dist/core/profiles.js.map +1 -1
  223. package/dist/core/project-config.d.ts +43 -44
  224. package/dist/core/project-config.d.ts.map +1 -1
  225. package/dist/core/project-config.js +107 -82
  226. package/dist/core/project-config.js.map +1 -1
  227. package/dist/core/project-layout.d.ts +10 -18
  228. package/dist/core/project-layout.d.ts.map +1 -1
  229. package/dist/core/project-layout.js +16 -26
  230. package/dist/core/project-layout.js.map +1 -1
  231. package/dist/core/root-selection.d.ts +11 -7
  232. package/dist/core/root-selection.d.ts.map +1 -1
  233. package/dist/core/root-selection.js +7 -8
  234. package/dist/core/root-selection.js.map +1 -1
  235. package/dist/core/rules.d.ts +10 -0
  236. package/dist/core/rules.d.ts.map +1 -0
  237. package/dist/core/rules.js +43 -0
  238. package/dist/core/rules.js.map +1 -0
  239. package/dist/core/schema-names.d.ts +16 -0
  240. package/dist/core/schema-names.d.ts.map +1 -0
  241. package/dist/core/schema-names.js +16 -0
  242. package/dist/core/schema-names.js.map +1 -0
  243. package/dist/core/schemas/base.schema.d.ts +3 -0
  244. package/dist/core/schemas/base.schema.d.ts.map +1 -1
  245. package/dist/core/schemas/base.schema.js +22 -6
  246. package/dist/core/schemas/base.schema.js.map +1 -1
  247. package/dist/core/schemas/change.schema.d.ts +16 -0
  248. package/dist/core/schemas/change.schema.d.ts.map +1 -1
  249. package/dist/core/schemas/change.schema.js +41 -10
  250. package/dist/core/schemas/change.schema.js.map +1 -1
  251. package/dist/core/schemas/spec.schema.d.ts +2 -0
  252. package/dist/core/schemas/spec.schema.d.ts.map +1 -1
  253. package/dist/core/shared/index.d.ts +3 -8
  254. package/dist/core/shared/index.d.ts.map +1 -1
  255. package/dist/core/shared/index.js +3 -8
  256. package/dist/core/shared/index.js.map +1 -1
  257. package/dist/core/shared/rules-generation.d.ts +27 -8
  258. package/dist/core/shared/rules-generation.d.ts.map +1 -1
  259. package/dist/core/shared/rules-generation.js +151 -16
  260. package/dist/core/shared/rules-generation.js.map +1 -1
  261. package/dist/core/shared/skill-generation.d.ts +38 -53
  262. package/dist/core/shared/skill-generation.d.ts.map +1 -1
  263. package/dist/core/shared/skill-generation.js +82 -51
  264. package/dist/core/shared/skill-generation.js.map +1 -1
  265. package/dist/core/shared/tool-detection.d.ts +40 -62
  266. package/dist/core/shared/tool-detection.d.ts.map +1 -1
  267. package/dist/core/shared/tool-detection.js +88 -80
  268. package/dist/core/shared/tool-detection.js.map +1 -1
  269. package/dist/core/skill-metrics.d.ts +36 -63
  270. package/dist/core/skill-metrics.d.ts.map +1 -1
  271. package/dist/core/skill-metrics.js +34 -73
  272. package/dist/core/skill-metrics.js.map +1 -1
  273. package/dist/core/spec-presenter.d.ts.map +1 -1
  274. package/dist/core/spec-presenter.js +6 -6
  275. package/dist/core/spec-presenter.js.map +1 -1
  276. package/dist/core/specs-apply.d.ts +22 -23
  277. package/dist/core/specs-apply.d.ts.map +1 -1
  278. package/dist/core/specs-apply.js +166 -193
  279. package/dist/core/specs-apply.js.map +1 -1
  280. package/dist/core/templates/fragments/interview.d.ts +2 -6
  281. package/dist/core/templates/fragments/interview.d.ts.map +1 -1
  282. package/dist/core/templates/fragments/interview.js +2 -6
  283. package/dist/core/templates/fragments/interview.js.map +1 -1
  284. package/dist/core/templates/fragments/next-step.d.ts +4 -8
  285. package/dist/core/templates/fragments/next-step.d.ts.map +1 -1
  286. package/dist/core/templates/fragments/next-step.js +4 -8
  287. package/dist/core/templates/fragments/next-step.js.map +1 -1
  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 +9 -12
  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 +7 -5
  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 -0
  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 +0 -1
  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 +2 -2
  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.map +1 -1
  333. package/dist/core/update.d.ts +26 -21
  334. package/dist/core/update.d.ts.map +1 -1
  335. package/dist/core/update.js +165 -116
  336. package/dist/core/update.js.map +1 -1
  337. package/dist/core/user-state-migration.d.ts +13 -15
  338. package/dist/core/user-state-migration.d.ts.map +1 -1
  339. package/dist/core/user-state-migration.js +16 -20
  340. package/dist/core/user-state-migration.js.map +1 -1
  341. package/dist/core/validation/constants.d.ts +19 -25
  342. package/dist/core/validation/constants.d.ts.map +1 -1
  343. package/dist/core/validation/constants.js +25 -20
  344. package/dist/core/validation/constants.js.map +1 -1
  345. package/dist/core/validation/prose-length.d.ts +15 -0
  346. package/dist/core/validation/prose-length.d.ts.map +1 -0
  347. package/dist/core/validation/prose-length.js +29 -0
  348. package/dist/core/validation/prose-length.js.map +1 -0
  349. package/dist/core/validation/purpose-placeholder.d.ts +9 -16
  350. package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
  351. package/dist/core/validation/purpose-placeholder.js +30 -44
  352. package/dist/core/validation/purpose-placeholder.js.map +1 -1
  353. package/dist/core/validation/section-validator.d.ts +4 -4
  354. package/dist/core/validation/section-validator.d.ts.map +1 -1
  355. package/dist/core/validation/section-validator.js +43 -7
  356. package/dist/core/validation/section-validator.js.map +1 -1
  357. package/dist/core/validation/task-numbering.d.ts +6 -3
  358. package/dist/core/validation/task-numbering.d.ts.map +1 -1
  359. package/dist/core/validation/task-numbering.js +23 -11
  360. package/dist/core/validation/task-numbering.js.map +1 -1
  361. package/dist/core/validation/types.d.ts +18 -0
  362. package/dist/core/validation/types.d.ts.map +1 -1
  363. package/dist/core/validation/types.js +12 -1
  364. package/dist/core/validation/types.js.map +1 -1
  365. package/dist/core/validation/validator.d.ts +50 -51
  366. package/dist/core/validation/validator.d.ts.map +1 -1
  367. package/dist/core/validation/validator.js +478 -269
  368. package/dist/core/validation/validator.js.map +1 -1
  369. package/dist/prompts/searchable-multi-select.d.ts +3 -8
  370. package/dist/prompts/searchable-multi-select.d.ts.map +1 -1
  371. package/dist/prompts/searchable-multi-select.js +16 -39
  372. package/dist/prompts/searchable-multi-select.js.map +1 -1
  373. package/dist/utils/change-metadata.d.ts +11 -50
  374. package/dist/utils/change-metadata.d.ts.map +1 -1
  375. package/dist/utils/change-metadata.js +48 -67
  376. package/dist/utils/change-metadata.js.map +1 -1
  377. package/dist/utils/change-utils.d.ts +31 -54
  378. package/dist/utils/change-utils.d.ts.map +1 -1
  379. package/dist/utils/change-utils.js +143 -100
  380. package/dist/utils/change-utils.js.map +1 -1
  381. package/dist/utils/file-lock.d.ts +39 -0
  382. package/dist/utils/file-lock.d.ts.map +1 -0
  383. package/dist/utils/file-lock.js +149 -0
  384. package/dist/utils/file-lock.js.map +1 -0
  385. package/dist/utils/file-system.d.ts +12 -32
  386. package/dist/utils/file-system.d.ts.map +1 -1
  387. package/dist/utils/file-system.js +16 -40
  388. package/dist/utils/file-system.js.map +1 -1
  389. package/dist/utils/frontmatter.d.ts +7 -11
  390. package/dist/utils/frontmatter.d.ts.map +1 -1
  391. package/dist/utils/frontmatter.js +11 -11
  392. package/dist/utils/frontmatter.js.map +1 -1
  393. package/dist/utils/interactive.d.ts +4 -9
  394. package/dist/utils/interactive.d.ts.map +1 -1
  395. package/dist/utils/interactive.js +2 -4
  396. package/dist/utils/interactive.js.map +1 -1
  397. package/dist/utils/item-discovery.d.ts +15 -10
  398. package/dist/utils/item-discovery.d.ts.map +1 -1
  399. package/dist/utils/item-discovery.js +42 -47
  400. package/dist/utils/item-discovery.js.map +1 -1
  401. package/dist/utils/link.d.ts +13 -4
  402. package/dist/utils/link.d.ts.map +1 -1
  403. package/dist/utils/link.js +13 -4
  404. package/dist/utils/link.js.map +1 -1
  405. package/dist/utils/match.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 +10 -11
  411. package/dist/utils/spec-files.d.ts.map +1 -1
  412. package/dist/utils/spec-files.js +31 -22
  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 +9 -10
  423. package/schemas/decision/templates/decision.md +3 -1
  424. package/schemas/decision/templates/index.md +2 -2
  425. package/schemas/issue/schema.yaml +11 -2
  426. package/schemas/issue/templates/spec.md +37 -3
  427. package/schemas/sdd/schema.yaml +24 -1
  428. package/schemas/sdd/templates/spec.md +37 -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 } 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,49 +241,121 @@ export function registerConfigCommand(program) {
165
241
  .action((options) => {
166
242
  const config = getGlobalConfig();
167
243
  if (options.json) {
168
- console.log(JSON.stringify(config, null, 2));
244
+ // Nested, not spread: the schema is `.passthrough()` and
245
+ // `config set --allow-unknown` writes any top-level key, so spreading
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
+ });
169
265
  }
170
266
  else {
171
- // Read raw config to determine which values are explicit vs defaults.
172
- // Must be the file that was actually read, not the write target — they
173
- // differ until a legacy install migrates.
174
- const configPath = getEffectiveConfigPath();
175
- let rawConfig = {};
176
- try {
177
- if (fs.existsSync(configPath)) {
178
- rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
179
- }
180
- }
181
- catch {
182
- // If reading fails, treat all as defaults
183
- }
267
+ const rawConfig = rawGlobalConfig();
184
268
  console.log(yaml.stringify(config).trimEnd());
185
- // 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
+ }
186
276
  const profileSource = rawConfig.profile !== undefined ? '(explicit)' : '(default)';
187
277
  console.log(`\nProfile settings:`);
188
278
  console.log(` profile: ${config.profile} ${profileSource}`);
189
- if (config.profile === 'core') {
190
- console.log(` workflows: ${ALL_WORKFLOWS.join(', ')} (from core profile)`);
191
- }
192
- else if (config.workflows && config.workflows.length > 0) {
193
- console.log(` workflows: ${config.workflows.join(', ')} (explicit)`);
279
+ const workflows = resolveListedWorkflows(config);
280
+ if (workflows.length === 0) {
281
+ console.log(` workflows: (none)`);
194
282
  }
195
283
  else {
196
- 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);
197
297
  }
198
298
  }
199
299
  });
200
- // config get
201
300
  configCmd
202
301
  .command('get <key>')
203
302
  .description('Get a specific value (raw, scriptable)')
204
- .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);
307
+ const keyValidation = validateConfigKeyPath(key);
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)) {
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
+ }
327
+ console.error(`Error: Invalid configuration key "${key}".${reason}`);
328
+ for (const line of fixLines)
329
+ console.error(line);
330
+ process.exitCode = 1;
331
+ return;
332
+ }
205
333
  const config = getGlobalConfig();
206
334
  const value = getNestedValue(config, key);
207
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
+ }
349
+ console.error(`Error: Key "${key}" is not set.`);
208
350
  process.exitCode = 1;
209
351
  return;
210
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
+ }
211
359
  if (typeof value === 'object' && value !== null) {
212
360
  console.log(JSON.stringify(value));
213
361
  }
@@ -215,13 +363,25 @@ export function registerConfigCommand(program) {
215
363
  console.log(String(value));
216
364
  }
217
365
  });
218
- // config set
219
366
  configCmd
220
367
  .command('set <key> <value>')
221
368
  .description('Set a value (auto-coerce types)')
222
369
  .option('--string', 'Force value to be stored as string')
223
370
  .option('--allow-unknown', 'Allow setting unknown keys')
371
+ .option('--json', 'Output as JSON')
224
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
+ };
225
385
  const allowUnknown = Boolean(options.allowUnknown);
226
386
  const keyValidation = validateConfigKeyPath(key);
227
387
  // --allow-unknown relaxes the known-key check, but never the
@@ -229,60 +389,131 @@ export function registerConfigCommand(program) {
229
389
  const unsafeKey = hasUnsafeKeySegment(key);
230
390
  if (!keyValidation.valid && (!allowUnknown || unsafeKey)) {
231
391
  const reason = keyValidation.reason ? ` ${keyValidation.reason}.` : '';
232
- console.error(`Error: Invalid configuration key "${key}".${reason}`);
233
- console.error('Use "tospec config list" to see available keys.');
392
+ const fixLines = ['Use "tospec config list" to see available keys.'];
234
393
  if (!allowUnknown && !unsafeKey) {
235
- console.error('Pass --allow-unknown to bypass this check.');
394
+ fixLines.push('Pass --allow-unknown to bypass this check.');
236
395
  }
237
- process.exitCode = 1;
396
+ fail(`Invalid configuration key "${key}".${reason}`, fixLines.join('\n'));
238
397
  return;
239
398
  }
240
399
  const config = getGlobalConfig();
241
- const coercedValue = coerceValue(value, options.string || false);
242
- // 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.
243
402
  const newConfig = JSON.parse(JSON.stringify(config));
244
403
  setNestedValue(newConfig, key, coercedValue);
245
- // Validate the new config
246
404
  const validation = validateConfig(newConfig);
247
405
  if (!validation.success) {
248
- console.error(`Error: Invalid configuration - ${validation.error}`);
249
- process.exitCode = 1;
406
+ fail(`Invalid configuration - ${validation.error}`);
250
407
  return;
251
408
  }
252
- // 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
+ }
253
419
  setNestedValue(config, key, coercedValue);
254
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
+ }
255
457
  const displayValue = typeof coercedValue === 'string' ? `"${coercedValue}"` : String(coercedValue);
256
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
+ }
257
464
  });
258
- // config unset
259
465
  configCmd
260
466
  .command('unset <key>')
261
467
  .description('Remove a key (revert to default)')
262
- .action((key) => {
468
+ .option('--json', 'Output as JSON')
469
+ .action((key, options) => {
263
470
  const config = getGlobalConfig();
264
471
  const existed = deleteNestedValue(config, key);
265
- if (existed) {
472
+ if (existed)
266
473
  saveGlobalConfig(config);
267
- console.log(`Unset ${key} (reverted to default)`);
268
- }
269
- else {
270
- 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;
271
479
  }
480
+ console.log(existed ? `Unset ${key} (reverted to default)` : `Key "${key}" was not set`);
272
481
  });
273
- // config reset
274
482
  configCmd
275
483
  .command('reset')
276
484
  .description('Reset configuration to defaults')
277
485
  .option('--all', 'Reset all configuration (required)')
278
486
  .option('-y, --yes', 'Skip confirmation prompts')
487
+ .option('--json', 'Output as JSON (non-interactive; requires --all and --yes)')
279
488
  .action(async (options) => {
489
+ // `list`, `get` and `set` all speak --json, so reset must too.
280
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
+ }
281
500
  console.error('Error: --all flag is required for reset');
282
501
  console.error('Usage: tospec config reset --all [-y]');
283
502
  process.exitCode = 1;
284
503
  return;
285
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
+ }
286
517
  if (!options.yes) {
287
518
  const { confirm } = await import('@inquirer/prompts');
288
519
  let confirmed;
@@ -306,9 +537,12 @@ export function registerConfigCommand(program) {
306
537
  }
307
538
  }
308
539
  saveGlobalConfig({ ...DEFAULT_CONFIG });
540
+ if (options.json) {
541
+ emitSuccess({ config: getGlobalConfig() });
542
+ return;
543
+ }
309
544
  console.log('Configuration reset to defaults');
310
545
  });
311
- // config edit
312
546
  configCmd
313
547
  .command('edit')
314
548
  .description('Open config in $EDITOR')
@@ -322,13 +556,10 @@ export function registerConfigCommand(program) {
322
556
  return;
323
557
  }
324
558
  const configPath = getGlobalConfigPath();
325
- // Ensure config file exists with defaults
326
559
  if (!fs.existsSync(configPath)) {
327
560
  saveGlobalConfig({ ...DEFAULT_CONFIG });
328
561
  }
329
- // Spawn editor and wait for it to close
330
- // Avoid shell parsing to correctly handle paths with spaces in both
331
- // the editor path and config path
562
+ // shell: false so a space in the editor or config path is not re-parsed.
332
563
  const child = spawn(editor, [configPath], {
333
564
  stdio: 'inherit',
334
565
  shell: false,
@@ -346,7 +577,11 @@ export function registerConfigCommand(program) {
346
577
  });
347
578
  try {
348
579
  const rawConfig = fs.readFileSync(configPath, 'utf-8');
349
- 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));
350
585
  const validation = validateConfig(parsedConfig);
351
586
  if (!validation.success) {
352
587
  console.error(`Error: Invalid configuration - ${validation.error}`);
@@ -367,32 +602,71 @@ export function registerConfigCommand(program) {
367
602
  process.exitCode = 1;
368
603
  }
369
604
  });
370
- // config profile [preset]
371
605
  configCmd
372
606
  .command('profile [preset]')
373
607
  .description('Configure workflow profile (interactive picker or preset shortcut)')
374
- .action(async (preset) => {
375
- // 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.
376
631
  if (preset === 'core') {
377
632
  const config = getGlobalConfig();
378
633
  config.profile = 'core';
379
- config.workflows = [...ALL_WORKFLOWS];
634
+ delete config.workflows;
380
635
  saveGlobalConfig(config);
381
- 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);
382
652
  return;
383
653
  }
384
654
  if (preset) {
385
- console.error(`Error: Unknown profile preset "${preset}". Available presets: core`);
386
- 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');
387
663
  return;
388
664
  }
389
- // Non-interactive check
390
665
  if (!process.stdout.isTTY) {
391
- 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.');
392
667
  process.exitCode = 1;
393
668
  return;
394
669
  }
395
- // Interactive picker
396
670
  const { select, checkbox, confirm } = await import('@inquirer/prompts');
397
671
  try {
398
672
  const config = getGlobalConfig();
@@ -426,9 +700,8 @@ export function registerConfigCommand(program) {
426
700
  };
427
701
  if (action === 'workflows') {
428
702
  const formatWorkflowChoice = (workflow) => {
429
- // The map is complete by construction (Record<WorkflowId>); this
430
- // fallback is only for an id a user's custom config carries that
431
- // 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.
432
705
  const metadata = WORKFLOW_PROMPT_META[workflow] ?? {
433
706
  name: workflow,
434
707
  description: `Workflow: ${workflow}`,
@@ -466,9 +739,13 @@ export function registerConfigCommand(program) {
466
739
  }
467
740
  console.log();
468
741
  config.profile = nextState.profile;
469
- 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;
470
748
  saveGlobalConfig(config);
471
- // Check if inside an Tospec project
472
749
  const projectDir = process.cwd();
473
750
  const tospecDir = path.join(projectDir, TOSPEC_DIR_NAME);
474
751
  if (fs.existsSync(tospecDir)) {
@@ -478,9 +755,8 @@ export function registerConfigCommand(program) {
478
755
  });
479
756
  if (applyNow) {
480
757
  try {
481
- // Run in-process instead of `npx tospec update`, which depends on
482
- // tospec/npx resolving on PATH and fails under global installs or
483
- // monorepos.
758
+ // In-process, not `npx tospec update`: that depends on PATH and
759
+ // fails under global installs and monorepos.
484
760
  await new UpdateCommand().execute(projectDir);
485
761
  console.log('Run `tospec update` in your other projects to apply.');
486
762
  }