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

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