@seanmars/tospec 0.23.0 → 0.26.0-beta

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 (358) hide show
  1. package/CHANGELOG.md +281 -0
  2. package/README.md +27 -9
  3. package/assets/claude-mod/tospec-dashboard/.claude-plugin/plugin.json +7 -0
  4. package/assets/claude-mod/tospec-dashboard/hooks/hooks.json +1 -0
  5. package/assets/claude-mod/tospec-dashboard/hooks/register.ts +695 -0
  6. package/assets/claude-mod/tospec-dashboard/tests/register.test.ts +448 -0
  7. package/assets/claude-mod/tospec-dashboard/types/index.d.ts +8 -0
  8. package/assets/dashboard/app.js +218 -1180
  9. package/assets/dashboard/index.html +31 -22
  10. package/assets/dashboard/lib/charts.js +355 -0
  11. package/assets/dashboard/lib/derive.js +375 -0
  12. package/assets/dashboard/lib/dom.js +121 -0
  13. package/assets/dashboard/lib/routes.js +52 -0
  14. package/assets/dashboard/lib/store.js +326 -0
  15. package/assets/dashboard/style.css +3103 -664
  16. package/assets/dashboard/views/alerts.js +170 -0
  17. package/assets/dashboard/views/change.js +234 -0
  18. package/assets/dashboard/views/docs.js +214 -0
  19. package/assets/dashboard/views/home.js +357 -0
  20. package/assets/dashboard/views/project.js +168 -0
  21. package/assets/dashboard/views/session.js +270 -0
  22. package/assets/dashboard/views/sidebar.js +131 -0
  23. package/dist/cli/index.d.ts.map +1 -1
  24. package/dist/cli/index.js +116 -102
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/commands/config.d.ts.map +1 -1
  27. package/dist/commands/config.js +213 -100
  28. package/dist/commands/config.js.map +1 -1
  29. package/dist/commands/dashboard-relay.d.ts +11 -0
  30. package/dist/commands/dashboard-relay.d.ts.map +1 -0
  31. package/dist/commands/dashboard-relay.js +88 -0
  32. package/dist/commands/dashboard-relay.js.map +1 -0
  33. package/dist/commands/dashboard.d.ts +69 -100
  34. package/dist/commands/dashboard.d.ts.map +1 -1
  35. package/dist/commands/dashboard.js +420 -598
  36. package/dist/commands/dashboard.js.map +1 -1
  37. package/dist/commands/decision.d.ts +38 -1
  38. package/dist/commands/decision.d.ts.map +1 -1
  39. package/dist/commands/decision.js +303 -27
  40. package/dist/commands/decision.js.map +1 -1
  41. package/dist/commands/metrics.d.ts +3 -4
  42. package/dist/commands/metrics.d.ts.map +1 -1
  43. package/dist/commands/metrics.js +3 -5
  44. package/dist/commands/metrics.js.map +1 -1
  45. package/dist/commands/shared-output.d.ts +24 -0
  46. package/dist/commands/shared-output.d.ts.map +1 -1
  47. package/dist/commands/shared-output.js +21 -2
  48. package/dist/commands/shared-output.js.map +1 -1
  49. package/dist/commands/show.d.ts.map +1 -1
  50. package/dist/commands/show.js +12 -9
  51. package/dist/commands/show.js.map +1 -1
  52. package/dist/commands/validate.d.ts +19 -0
  53. package/dist/commands/validate.d.ts.map +1 -1
  54. package/dist/commands/validate.js +243 -64
  55. package/dist/commands/validate.js.map +1 -1
  56. package/dist/commands/workflow/instructions.d.ts +7 -1
  57. package/dist/commands/workflow/instructions.d.ts.map +1 -1
  58. package/dist/commands/workflow/instructions.js +299 -81
  59. package/dist/commands/workflow/instructions.js.map +1 -1
  60. package/dist/commands/workflow/new-change.d.ts.map +1 -1
  61. package/dist/commands/workflow/new-change.js +26 -17
  62. package/dist/commands/workflow/new-change.js.map +1 -1
  63. package/dist/commands/workflow/prompt-escape.d.ts +25 -0
  64. package/dist/commands/workflow/prompt-escape.d.ts.map +1 -0
  65. package/dist/commands/workflow/prompt-escape.js +62 -0
  66. package/dist/commands/workflow/prompt-escape.js.map +1 -0
  67. package/dist/commands/workflow/schemas.d.ts.map +1 -1
  68. package/dist/commands/workflow/schemas.js +34 -16
  69. package/dist/commands/workflow/schemas.js.map +1 -1
  70. package/dist/commands/workflow/shared.d.ts +46 -10
  71. package/dist/commands/workflow/shared.d.ts.map +1 -1
  72. package/dist/commands/workflow/shared.js +66 -30
  73. package/dist/commands/workflow/shared.js.map +1 -1
  74. package/dist/commands/workflow/status.d.ts +5 -2
  75. package/dist/commands/workflow/status.d.ts.map +1 -1
  76. package/dist/commands/workflow/status.js +62 -28
  77. package/dist/commands/workflow/status.js.map +1 -1
  78. package/dist/core/archive.d.ts +38 -0
  79. package/dist/core/archive.d.ts.map +1 -1
  80. package/dist/core/archive.js +492 -75
  81. package/dist/core/archive.js.map +1 -1
  82. package/dist/core/artifact-graph/index.d.ts +2 -2
  83. package/dist/core/artifact-graph/index.d.ts.map +1 -1
  84. package/dist/core/artifact-graph/index.js +2 -2
  85. package/dist/core/artifact-graph/index.js.map +1 -1
  86. package/dist/core/artifact-graph/instruction-loader.d.ts +8 -0
  87. package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
  88. package/dist/core/artifact-graph/instruction-loader.js +58 -36
  89. package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
  90. package/dist/core/artifact-graph/resolver.d.ts +39 -12
  91. package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
  92. package/dist/core/artifact-graph/resolver.js +82 -36
  93. package/dist/core/artifact-graph/resolver.js.map +1 -1
  94. package/dist/core/artifact-graph/schema.d.ts +24 -0
  95. package/dist/core/artifact-graph/schema.d.ts.map +1 -1
  96. package/dist/core/artifact-graph/schema.js +32 -1
  97. package/dist/core/artifact-graph/schema.js.map +1 -1
  98. package/dist/core/artifact-graph/stub-detection.d.ts +10 -2
  99. package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -1
  100. package/dist/core/artifact-graph/stub-detection.js +18 -11
  101. package/dist/core/artifact-graph/stub-detection.js.map +1 -1
  102. package/dist/core/artifact-graph/types.d.ts +2 -2
  103. package/dist/core/artifact-graph/types.d.ts.map +1 -1
  104. package/dist/core/artifact-graph/types.js +10 -1
  105. package/dist/core/artifact-graph/types.js.map +1 -1
  106. package/dist/core/change-metadata/schema.d.ts.map +1 -1
  107. package/dist/core/change-metadata/schema.js +18 -4
  108. package/dist/core/change-metadata/schema.js.map +1 -1
  109. package/dist/core/change-presenter.d.ts.map +1 -1
  110. package/dist/core/change-presenter.js +13 -8
  111. package/dist/core/change-presenter.js.map +1 -1
  112. package/dist/core/change-status-policy.d.ts.map +1 -1
  113. package/dist/core/change-status-policy.js +3 -1
  114. package/dist/core/change-status-policy.js.map +1 -1
  115. package/dist/core/codex-metrics.d.ts +1 -1
  116. package/dist/core/codex-metrics.js +1 -1
  117. package/dist/core/config-schema.d.ts +8 -0
  118. package/dist/core/config-schema.d.ts.map +1 -1
  119. package/dist/core/config-schema.js +10 -0
  120. package/dist/core/config-schema.js.map +1 -1
  121. package/dist/core/dashboard-activity.d.ts +4 -4
  122. package/dist/core/dashboard-activity.js +4 -4
  123. package/dist/core/dashboard-data.d.ts +11 -5
  124. package/dist/core/dashboard-data.d.ts.map +1 -1
  125. package/dist/core/dashboard-data.js +54 -28
  126. package/dist/core/dashboard-data.js.map +1 -1
  127. package/dist/core/dashboard-hub.d.ts +258 -0
  128. package/dist/core/dashboard-hub.d.ts.map +1 -0
  129. package/dist/core/dashboard-hub.js +655 -0
  130. package/dist/core/dashboard-hub.js.map +1 -0
  131. package/dist/core/dashboard-routes.d.ts +57 -0
  132. package/dist/core/dashboard-routes.d.ts.map +1 -0
  133. package/dist/core/dashboard-routes.js +398 -0
  134. package/dist/core/dashboard-routes.js.map +1 -0
  135. package/dist/core/global-config.d.ts +71 -2
  136. package/dist/core/global-config.d.ts.map +1 -1
  137. package/dist/core/global-config.js +149 -4
  138. package/dist/core/global-config.js.map +1 -1
  139. package/dist/core/id.d.ts +9 -0
  140. package/dist/core/id.d.ts.map +1 -1
  141. package/dist/core/id.js +37 -0
  142. package/dist/core/id.js.map +1 -1
  143. package/dist/core/init.d.ts +9 -2
  144. package/dist/core/init.d.ts.map +1 -1
  145. package/dist/core/init.js +119 -33
  146. package/dist/core/init.js.map +1 -1
  147. package/dist/core/list.d.ts.map +1 -1
  148. package/dist/core/list.js +69 -45
  149. package/dist/core/list.js.map +1 -1
  150. package/dist/core/local-server.d.ts +10 -3
  151. package/dist/core/local-server.d.ts.map +1 -1
  152. package/dist/core/local-server.js +25 -5
  153. package/dist/core/local-server.js.map +1 -1
  154. package/dist/core/markdown-render.d.ts.map +1 -1
  155. package/dist/core/markdown-render.js +7 -2
  156. package/dist/core/markdown-render.js.map +1 -1
  157. package/dist/core/migrate.d.ts +24 -2
  158. package/dist/core/migrate.d.ts.map +1 -1
  159. package/dist/core/migrate.js +335 -59
  160. package/dist/core/migrate.js.map +1 -1
  161. package/dist/core/parsers/change-parser.d.ts +20 -1
  162. package/dist/core/parsers/change-parser.d.ts.map +1 -1
  163. package/dist/core/parsers/change-parser.js +76 -73
  164. package/dist/core/parsers/change-parser.js.map +1 -1
  165. package/dist/core/parsers/markdown-parser.d.ts +24 -0
  166. package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
  167. package/dist/core/parsers/markdown-parser.js +65 -10
  168. package/dist/core/parsers/markdown-parser.js.map +1 -1
  169. package/dist/core/parsers/requirement-blocks.d.ts +127 -6
  170. package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
  171. package/dist/core/parsers/requirement-blocks.js +349 -56
  172. package/dist/core/parsers/requirement-blocks.js.map +1 -1
  173. package/dist/core/parsers/requirement-text.d.ts +25 -1
  174. package/dist/core/parsers/requirement-text.d.ts.map +1 -1
  175. package/dist/core/parsers/requirement-text.js +62 -6
  176. package/dist/core/parsers/requirement-text.js.map +1 -1
  177. package/dist/core/parsers/spec-structure.d.ts.map +1 -1
  178. package/dist/core/parsers/spec-structure.js +36 -24
  179. package/dist/core/parsers/spec-structure.js.map +1 -1
  180. package/dist/core/project-config.d.ts +9 -2
  181. package/dist/core/project-config.d.ts.map +1 -1
  182. package/dist/core/project-config.js +30 -7
  183. package/dist/core/project-config.js.map +1 -1
  184. package/dist/core/project-layout.d.ts +26 -0
  185. package/dist/core/project-layout.d.ts.map +1 -1
  186. package/dist/core/project-layout.js +51 -1
  187. package/dist/core/project-layout.js.map +1 -1
  188. package/dist/core/schema-names.d.ts +17 -0
  189. package/dist/core/schema-names.d.ts.map +1 -1
  190. package/dist/core/schema-names.js +24 -0
  191. package/dist/core/schema-names.js.map +1 -1
  192. package/dist/core/shared/dashboard-mod.d.ts +24 -0
  193. package/dist/core/shared/dashboard-mod.d.ts.map +1 -0
  194. package/dist/core/shared/dashboard-mod.js +44 -0
  195. package/dist/core/shared/dashboard-mod.js.map +1 -0
  196. package/dist/core/shared/index.d.ts +2 -1
  197. package/dist/core/shared/index.d.ts.map +1 -1
  198. package/dist/core/shared/index.js +2 -1
  199. package/dist/core/shared/index.js.map +1 -1
  200. package/dist/core/shared/rules-generation.d.ts +12 -0
  201. package/dist/core/shared/rules-generation.d.ts.map +1 -1
  202. package/dist/core/shared/rules-generation.js +26 -2
  203. package/dist/core/shared/rules-generation.js.map +1 -1
  204. package/dist/core/shared/skill-generation.d.ts +7 -0
  205. package/dist/core/shared/skill-generation.d.ts.map +1 -1
  206. package/dist/core/shared/skill-generation.js +19 -0
  207. package/dist/core/shared/skill-generation.js.map +1 -1
  208. package/dist/core/shared/tool-detection.d.ts +10 -1
  209. package/dist/core/shared/tool-detection.d.ts.map +1 -1
  210. package/dist/core/shared/tool-detection.js +37 -6
  211. package/dist/core/shared/tool-detection.js.map +1 -1
  212. package/dist/core/spec-presenter.d.ts.map +1 -1
  213. package/dist/core/spec-presenter.js +23 -1
  214. package/dist/core/spec-presenter.js.map +1 -1
  215. package/dist/core/specs-apply.d.ts +98 -1
  216. package/dist/core/specs-apply.d.ts.map +1 -1
  217. package/dist/core/specs-apply.js +426 -63
  218. package/dist/core/specs-apply.js.map +1 -1
  219. package/dist/core/templates/fragments/change-diff.d.ts +37 -0
  220. package/dist/core/templates/fragments/change-diff.d.ts.map +1 -0
  221. package/dist/core/templates/fragments/change-diff.js +37 -0
  222. package/dist/core/templates/fragments/change-diff.js.map +1 -0
  223. package/dist/core/templates/fragments/sync-fail.d.ts +8 -0
  224. package/dist/core/templates/fragments/sync-fail.d.ts.map +1 -0
  225. package/dist/core/templates/fragments/sync-fail.js +8 -0
  226. package/dist/core/templates/fragments/sync-fail.js.map +1 -0
  227. package/dist/core/templates/fragments/validate.d.ts +14 -1
  228. package/dist/core/templates/fragments/validate.d.ts.map +1 -1
  229. package/dist/core/templates/fragments/validate.js +14 -1
  230. package/dist/core/templates/fragments/validate.js.map +1 -1
  231. package/dist/core/templates/fragments/verify.d.ts +2 -2
  232. package/dist/core/templates/fragments/verify.d.ts.map +1 -1
  233. package/dist/core/templates/fragments/verify.js +8 -5
  234. package/dist/core/templates/fragments/verify.js.map +1 -1
  235. package/dist/core/templates/workflows/apply.d.ts +1 -1
  236. package/dist/core/templates/workflows/apply.d.ts.map +1 -1
  237. package/dist/core/templates/workflows/apply.js +29 -10
  238. package/dist/core/templates/workflows/apply.js.map +1 -1
  239. package/dist/core/templates/workflows/archive.d.ts.map +1 -1
  240. package/dist/core/templates/workflows/archive.js +27 -9
  241. package/dist/core/templates/workflows/archive.js.map +1 -1
  242. package/dist/core/templates/workflows/decision.d.ts.map +1 -1
  243. package/dist/core/templates/workflows/decision.js +10 -4
  244. package/dist/core/templates/workflows/decision.js.map +1 -1
  245. package/dist/core/templates/workflows/explore.d.ts +2 -2
  246. package/dist/core/templates/workflows/explore.d.ts.map +1 -1
  247. package/dist/core/templates/workflows/explore.js +7 -3
  248. package/dist/core/templates/workflows/explore.js.map +1 -1
  249. package/dist/core/templates/workflows/grill.d.ts +2 -2
  250. package/dist/core/templates/workflows/grill.d.ts.map +1 -1
  251. package/dist/core/templates/workflows/grill.js +5 -4
  252. package/dist/core/templates/workflows/grill.js.map +1 -1
  253. package/dist/core/templates/workflows/issue.d.ts.map +1 -1
  254. package/dist/core/templates/workflows/issue.js +9 -3
  255. package/dist/core/templates/workflows/issue.js.map +1 -1
  256. package/dist/core/templates/workflows/propose.d.ts.map +1 -1
  257. package/dist/core/templates/workflows/propose.js +4 -1
  258. package/dist/core/templates/workflows/propose.js.map +1 -1
  259. package/dist/core/templates/workflows/sync.d.ts.map +1 -1
  260. package/dist/core/templates/workflows/sync.js +25 -11
  261. package/dist/core/templates/workflows/sync.js.map +1 -1
  262. package/dist/core/templates/workflows/update.d.ts.map +1 -1
  263. package/dist/core/templates/workflows/update.js +17 -10
  264. package/dist/core/templates/workflows/update.js.map +1 -1
  265. package/dist/core/update.d.ts +5 -0
  266. package/dist/core/update.d.ts.map +1 -1
  267. package/dist/core/update.js +69 -29
  268. package/dist/core/update.js.map +1 -1
  269. package/dist/core/validation/constants.d.ts +7 -1
  270. package/dist/core/validation/constants.d.ts.map +1 -1
  271. package/dist/core/validation/constants.js +16 -2
  272. package/dist/core/validation/constants.js.map +1 -1
  273. package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
  274. package/dist/core/validation/purpose-placeholder.js +35 -11
  275. package/dist/core/validation/purpose-placeholder.js.map +1 -1
  276. package/dist/core/validation/section-validator.d.ts +7 -0
  277. package/dist/core/validation/section-validator.d.ts.map +1 -1
  278. package/dist/core/validation/section-validator.js +22 -2
  279. package/dist/core/validation/section-validator.js.map +1 -1
  280. package/dist/core/validation/sister-changes.d.ts +93 -0
  281. package/dist/core/validation/sister-changes.d.ts.map +1 -0
  282. package/dist/core/validation/sister-changes.js +180 -0
  283. package/dist/core/validation/sister-changes.js.map +1 -0
  284. package/dist/core/validation/task-markers.d.ts +9 -0
  285. package/dist/core/validation/task-markers.d.ts.map +1 -0
  286. package/dist/core/validation/task-markers.js +71 -0
  287. package/dist/core/validation/task-markers.js.map +1 -0
  288. package/dist/core/validation/types.d.ts +58 -0
  289. package/dist/core/validation/types.d.ts.map +1 -1
  290. package/dist/core/validation/types.js +22 -0
  291. package/dist/core/validation/types.js.map +1 -1
  292. package/dist/core/validation/validator.d.ts +79 -2
  293. package/dist/core/validation/validator.d.ts.map +1 -1
  294. package/dist/core/validation/validator.js +565 -84
  295. package/dist/core/validation/validator.js.map +1 -1
  296. package/dist/utils/change-metadata.d.ts +62 -5
  297. package/dist/utils/change-metadata.d.ts.map +1 -1
  298. package/dist/utils/change-metadata.js +193 -31
  299. package/dist/utils/change-metadata.js.map +1 -1
  300. package/dist/utils/change-utils.d.ts +21 -0
  301. package/dist/utils/change-utils.d.ts.map +1 -1
  302. package/dist/utils/change-utils.js +168 -53
  303. package/dist/utils/change-utils.js.map +1 -1
  304. package/dist/utils/command-line.d.ts +11 -0
  305. package/dist/utils/command-line.d.ts.map +1 -0
  306. package/dist/utils/command-line.js +43 -0
  307. package/dist/utils/command-line.js.map +1 -0
  308. package/dist/utils/display-width.d.ts +10 -0
  309. package/dist/utils/display-width.d.ts.map +1 -0
  310. package/dist/utils/display-width.js +15 -0
  311. package/dist/utils/display-width.js.map +1 -0
  312. package/dist/utils/file-system.d.ts +16 -0
  313. package/dist/utils/file-system.d.ts.map +1 -1
  314. package/dist/utils/file-system.js +49 -2
  315. package/dist/utils/file-system.js.map +1 -1
  316. package/dist/utils/frontmatter.d.ts.map +1 -1
  317. package/dist/utils/frontmatter.js +11 -5
  318. package/dist/utils/frontmatter.js.map +1 -1
  319. package/dist/utils/interactive.d.ts.map +1 -1
  320. package/dist/utils/interactive.js +3 -1
  321. package/dist/utils/interactive.js.map +1 -1
  322. package/dist/utils/item-discovery.d.ts +3 -2
  323. package/dist/utils/item-discovery.d.ts.map +1 -1
  324. package/dist/utils/item-discovery.js +34 -16
  325. package/dist/utils/item-discovery.js.map +1 -1
  326. package/dist/utils/link.d.ts +0 -1
  327. package/dist/utils/link.d.ts.map +1 -1
  328. package/dist/utils/link.js +0 -1
  329. package/dist/utils/link.js.map +1 -1
  330. package/dist/utils/match.d.ts.map +1 -1
  331. package/dist/utils/match.js +10 -1
  332. package/dist/utils/match.js.map +1 -1
  333. package/dist/utils/option-flags.d.ts +9 -0
  334. package/dist/utils/option-flags.d.ts.map +1 -0
  335. package/dist/utils/option-flags.js +12 -0
  336. package/dist/utils/option-flags.js.map +1 -0
  337. package/dist/utils/sentence.d.ts +9 -0
  338. package/dist/utils/sentence.d.ts.map +1 -0
  339. package/dist/utils/sentence.js +12 -0
  340. package/dist/utils/sentence.js.map +1 -0
  341. package/dist/utils/spec-files.d.ts +84 -1
  342. package/dist/utils/spec-files.d.ts.map +1 -1
  343. package/dist/utils/spec-files.js +192 -3
  344. package/dist/utils/spec-files.js.map +1 -1
  345. package/dist/utils/task-progress.d.ts.map +1 -1
  346. package/dist/utils/task-progress.js +32 -9
  347. package/dist/utils/task-progress.js.map +1 -1
  348. package/dist/utils/timestamp.d.ts +27 -0
  349. package/dist/utils/timestamp.d.ts.map +1 -1
  350. package/dist/utils/timestamp.js +37 -0
  351. package/dist/utils/timestamp.js.map +1 -1
  352. package/package.json +6 -2
  353. package/schemas/decision/templates/decision.md +2 -2
  354. package/schemas/issue/schema.yaml +12 -6
  355. package/schemas/issue/templates/task.md +1 -1
  356. package/schemas/sdd/schema.yaml +21 -10
  357. package/schemas/sdd/templates/proposal.md +1 -1
  358. package/assets/dashboard/chart.umd.js +0 -14
@@ -1,15 +1,17 @@
1
- import { spawn } from 'node:child_process';
1
+ import crossSpawn from 'cross-spawn';
2
+ import { splitCommandLine } from '../utils/command-line.js';
2
3
  import * as fs from 'node:fs';
3
4
  import * as path from 'node:path';
4
- import { getGlobalConfigPath, getEffectiveConfigPath, getGlobalConfig, saveGlobalConfig, } from '../core/global-config.js';
5
+ import { getGlobalConfigPath, getGlobalConfig, getStoredGlobalConfig, getStoredGlobalConfigFields, saveGlobalConfig, assertGlobalConfigReadable, assertGlobalConfigWritable, } from '../core/global-config.js';
5
6
  import * as yaml from 'yaml';
6
- import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, validateConfigKeyPath, hasUnsafeKeySegment, validateConfig, DEFAULT_CONFIG, } from '../core/config-schema.js';
7
+ import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, validateConfigKeyPath, hasUnsafeKeySegment, validateConfig, findInvalidConfigFields, } from '../core/config-schema.js';
7
8
  import { ALL_WORKFLOWS, getProfileWorkflows } from '../core/profiles.js';
8
- import { describeWorkflowReferenceGaps, findWorkflowReferenceGaps, } from '../core/shared/skill-generation.js';
9
+ import { closeWorkflowReferences, describeWorkflowReferenceGaps, findWorkflowReferenceGaps, } from '../core/shared/skill-generation.js';
9
10
  import { TOSPEC_DIR_NAME } from '../core/config.js';
10
11
  import { stripBom } from '../core/parsers/requirement-text.js';
11
12
  import { UpdateCommand } from '../core/update.js';
12
- import { isPromptCancellationError, asErrorMessage, CliAbortError, emitSuccess, emitFailureStatus, } from './shared-output.js';
13
+ import { isInteractive } from '../utils/interactive.js';
14
+ import { isPromptCancellationError, asErrorMessage, CliAbortError, emitSuccess, emitFailureStatus, formatLabeled, } from './shared-output.js';
13
15
  /**
14
16
  * The `--json` null-shapes, one per subcommand, keyed by the data key each
15
17
  * success payload carries. Shared by the commander-level failure table in
@@ -106,6 +108,41 @@ function ignoredWorkflowsDiagnostic(config) {
106
108
  fix: 'Run "tospec config profile custom" to apply it, or "tospec config unset workflows" to drop it.',
107
109
  };
108
110
  }
111
+ /**
112
+ * The fix for a `custom` profile with no workflows list. The `set` example is
113
+ * the reference closure, not a fixed few ids: the old `explore,grill,propose`
114
+ * named siblings it left out, so following it drew
115
+ * `config_workflow_reference_missing` on the next `config list`. Seeded with
116
+ * `explore`, the entry point the old example started from; the closure decides
117
+ * the rest.
118
+ */
119
+ function workflowsListFix() {
120
+ const example = closeWorkflowReferences(['explore']).join(',');
121
+ return [
122
+ 'Run "tospec config profile core" to install every workflow, or pick a subset with "tospec config profile".',
123
+ `To set the list directly: tospec config set workflows "${example}"`,
124
+ ].join('\n');
125
+ }
126
+ /**
127
+ * The finding that the `custom` profile has no workflows to install, or null.
128
+ * `config profile custom` refuses that state, but `set` and `unset` are raw
129
+ * writes and stay permissive — so they have to say what the write means, or
130
+ * the next `init` / `update` installs zero skills unannounced. Any write that
131
+ * can reach the state checks it: `set profile custom`, `set workflows ""` and
132
+ * `unset workflows` alike.
133
+ */
134
+ function profileNeedsWorkflowsDiagnostic(config) {
135
+ if (config.profile !== 'custom')
136
+ return null;
137
+ if (config.workflows && config.workflows.length > 0)
138
+ return null;
139
+ return {
140
+ severity: 'warning',
141
+ code: 'config_profile_needs_workflows',
142
+ message: 'Profile "custom" needs a workflows list, and none is configured, so init and update install no skills.',
143
+ fix: workflowsListFix(),
144
+ };
145
+ }
109
146
  /**
110
147
  * The finding that the selected workflows' own instructions name workflows the
111
148
  * selection leaves out, or null. Raised here as well as at install time because
@@ -171,21 +208,14 @@ export function diffProfileState(before, after) {
171
208
  };
172
209
  }
173
210
  /**
174
- * The config file's raw contents, only to tell an explicit value from a default —
211
+ * Whether the loaded profile came from the file rather than the defaults —
175
212
  * `getGlobalConfig()` has merged the two. Reads the file actually loaded, not
176
- * the write target: the two differ until a legacy install migrates.
213
+ * the write target: the two differ until a legacy install migrates. A file
214
+ * with an invalid field is ignored whole, so nothing in it counts.
177
215
  */
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 {};
216
+ function isProfileExplicit() {
217
+ const stored = getStoredGlobalConfigFields();
218
+ return findInvalidConfigFields(stored).length === 0 && stored.profile !== undefined;
189
219
  }
190
220
  /**
191
221
  * The workflows a profile resolves to — `core` expands to the full set, any
@@ -209,14 +239,16 @@ export function registerConfigCommand(program) {
209
239
  const opts = thisCommand.opts();
210
240
  if (opts.scope && opts.scope !== 'global') {
211
241
  const message = 'Project-local config is not yet implemented';
242
+ const fix = 'Omit --scope, or pass --scope global.';
212
243
  // The subcommand's --json, not the parent's: `config --scope project
213
244
  // get x --json` used to abort with nothing on stdout, so an agent saw
214
245
  // exit 1 and no document at all.
215
246
  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.' });
247
+ emitFailureStatus(CONFIG_FAILURE_PAYLOADS[actionCommand.name()] ?? CONFIG_SCOPE_FAILURE_PAYLOAD, { severity: 'error', code: 'config_scope_unsupported', message, fix });
217
248
  }
218
249
  else {
219
250
  console.error(`Error: ${message}`);
251
+ console.error(`Fix: ${fix}`);
220
252
  }
221
253
  // Not `process.exit(1)`: exiting here kills the process before stderr
222
254
  // has flushed.
@@ -245,7 +277,6 @@ export function registerConfigCommand(program) {
245
277
  // `config set --allow-unknown` writes any top-level key, so spreading
246
278
  // lets user data shadow the envelope's reserved fields (`version`,
247
279
  // `root`, `status`).
248
- // tospec/decisions/20260916_121544-config-list-json-nests-under-config-key.md
249
280
  //
250
281
  // `profileSettings` mirrors the human branch below, the only other place
251
282
  // saying which workflows `profile: "core"` resolves to.
@@ -257,14 +288,13 @@ export function registerConfigCommand(program) {
257
288
  config,
258
289
  profileSettings: {
259
290
  profile: config.profile ?? 'core',
260
- explicit: rawGlobalConfig().profile !== undefined,
291
+ explicit: isProfileExplicit(),
261
292
  workflows: resolveListedWorkflows(config),
262
293
  },
263
294
  ...(listStatus.length ? { status: listStatus } : {}),
264
295
  });
265
296
  }
266
297
  else {
267
- const rawConfig = rawGlobalConfig();
268
298
  console.log(yaml.stringify(config).trimEnd());
269
299
  // The dump above is the stored config, so under a profile that ignores
270
300
  // `workflows` it and the resolved list below are two different answers
@@ -273,7 +303,7 @@ export function registerConfigCommand(program) {
273
303
  if (ignored) {
274
304
  console.log(` ^ ${ignored.message}`);
275
305
  }
276
- const profileSource = rawConfig.profile !== undefined ? '(explicit)' : '(default)';
306
+ const profileSource = isProfileExplicit() ? '(explicit)' : '(default)';
277
307
  console.log(`\nProfile settings:`);
278
308
  console.log(` profile: ${config.profile} ${profileSource}`);
279
309
  const workflows = resolveListedWorkflows(config);
@@ -286,14 +316,14 @@ export function registerConfigCommand(program) {
286
316
  }
287
317
  if (ignored?.fix) {
288
318
  console.log();
289
- console.log(ignored.fix);
319
+ console.log(`Fix: ${ignored.fix}`);
290
320
  }
291
321
  const referenceGap = workflowReferenceDiagnostic(config);
292
322
  if (referenceGap) {
293
323
  console.log();
294
324
  console.log(`Warning: ${referenceGap.message}`);
295
325
  if (referenceGap.fix)
296
- console.log(referenceGap.fix);
326
+ console.log(`Fix: ${referenceGap.fix}`);
297
327
  }
298
328
  }
299
329
  });
@@ -325,28 +355,24 @@ export function registerConfigCommand(program) {
325
355
  return;
326
356
  }
327
357
  console.error(`Error: Invalid configuration key "${key}".${reason}`);
328
- for (const line of fixLines)
329
- console.error(line);
358
+ console.error(formatLabeled('Fix', fixLines.join('\n')));
330
359
  process.exitCode = 1;
331
360
  return;
332
361
  }
333
362
  const config = getGlobalConfig();
334
363
  const value = getNestedValue(config, key);
335
364
  if (value === undefined) {
365
+ // `workflows` is a list, so the generic `<value>` placeholder hides the
366
+ // one thing a caller needs: the comma-separated form.
367
+ const fix = key === 'workflows'
368
+ ? workflowsListFix()
369
+ : `tospec config set ${key} <value>`;
336
370
  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
- });
371
+ emitFailureStatus({ get: null }, { severity: 'error', code: 'config_error', message: `Key "${key}" is not set.`, fix });
347
372
  return;
348
373
  }
349
374
  console.error(`Error: Key "${key}" is not set.`);
375
+ console.error(formatLabeled('Fix', fix));
350
376
  process.exitCode = 1;
351
377
  return;
352
378
  }
@@ -376,10 +402,9 @@ export function registerConfigCommand(program) {
376
402
  emitFailureStatus(CONFIG_FAILURE_PAYLOADS.set, { severity: 'error', code: 'config_error', message, ...(fix ? { fix } : {}) });
377
403
  return;
378
404
  }
379
- console.error(`Error: ${message}`);
405
+ console.error(formatLabeled('Error', message));
380
406
  if (fix)
381
- for (const line of fix.split('\n'))
382
- console.error(line);
407
+ console.error(formatLabeled('Fix', fix));
383
408
  process.exitCode = 1;
384
409
  };
385
410
  const allowUnknown = Boolean(options.allowUnknown);
@@ -396,14 +421,22 @@ export function registerConfigCommand(program) {
396
421
  fail(`Invalid configuration key "${key}".${reason}`, fixLines.join('\n'));
397
422
  return;
398
423
  }
399
- const config = getGlobalConfig();
400
- const coercedValue = coerceValue(value, options.string || false, key);
424
+ const rootKey = key.split('.')[0];
425
+ assertGlobalConfigWritable([rootKey]);
426
+ const config = getStoredGlobalConfig();
427
+ const parsedValue = coerceValue(value, options.string || false, key);
428
+ // A repeated id installs nothing extra, but `config list` and the profile
429
+ // summary would count it twice. Dropped rather than refused, as `init
430
+ // --tools` drops a repeated tool: what `explore,explore` asks for is clear.
431
+ const coercedValue = key === 'workflows' && Array.isArray(parsedValue) ? [...new Set(parsedValue)] : parsedValue;
401
432
  // Validate a copy before touching the config that is saved.
402
433
  const newConfig = JSON.parse(JSON.stringify(config));
403
434
  setNestedValue(newConfig, key, coercedValue);
404
- const validation = validateConfig(newConfig);
405
- if (!validation.success) {
406
- fail(`Invalid configuration - ${validation.error}`);
435
+ // This key's refusals only: when the write repairs one of two invalid
436
+ // fields, the other still fails, and that is not about this value.
437
+ const refused = findInvalidConfigFields(newConfig).filter((entry) => entry.field === rootKey);
438
+ if (refused.length > 0) {
439
+ fail(`Invalid configuration - ${refused.map((entry) => entry.detail).join('; ')}`);
407
440
  return;
408
441
  }
409
442
  // The schema types `workflows` as string[], so a misspelled id passes it
@@ -422,14 +455,16 @@ export function registerConfigCommand(program) {
422
455
  // discards it. Said alongside the confirmation, not instead of it: the
423
456
  // value genuinely was stored, it is just not what `update` acts on.
424
457
  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);
458
+ // An empty list is no list, the same rule `ignoredWorkflowsDiagnostic`
459
+ // applies, so there is nothing to call unused.
460
+ const workflowsIgnored = key === 'workflows' &&
461
+ profile !== 'custom' &&
462
+ !(Array.isArray(coercedValue) && coercedValue.length === 0);
463
+ // Keyed on the written root, so an unrelated `set` under an already
464
+ // empty custom profile does not repeat the warning on every write.
465
+ const profileNeedsWorkflows = rootKey === 'profile' || rootKey === 'workflows'
466
+ ? profileNeedsWorkflowsDiagnostic(config)
467
+ : null;
433
468
  const warnings = [];
434
469
  if (workflowsIgnored) {
435
470
  warnings.push({
@@ -439,14 +474,8 @@ export function registerConfigCommand(program) {
439
474
  fix: 'Run "tospec config profile custom" to make it take effect.',
440
475
  });
441
476
  }
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
- }
477
+ if (profileNeedsWorkflows)
478
+ warnings.push(profileNeedsWorkflows);
450
479
  if (options.json) {
451
480
  emitSuccess({
452
481
  set: { key, value: coercedValue },
@@ -454,12 +483,18 @@ export function registerConfigCommand(program) {
454
483
  });
455
484
  return;
456
485
  }
457
- const displayValue = typeof coercedValue === 'string' ? `"${coercedValue}"` : String(coercedValue);
486
+ // `String([])` is empty, which reads as a missing value; `(none)` is
487
+ // what `config list` prints for the same empty list.
488
+ const displayValue = typeof coercedValue === 'string'
489
+ ? `"${coercedValue}"`
490
+ : Array.isArray(coercedValue) && coercedValue.length === 0
491
+ ? '(none)'
492
+ : String(coercedValue);
458
493
  console.log(`Set ${key} = ${displayValue}`);
459
494
  for (const warning of warnings) {
460
495
  console.error(`Warning: ${warning.message}`);
461
496
  if (warning.fix)
462
- console.error(warning.fix);
497
+ console.error(formatLabeled('Fix', warning.fix));
463
498
  }
464
499
  });
465
500
  configCmd
@@ -467,17 +502,49 @@ export function registerConfigCommand(program) {
467
502
  .description('Remove a key (revert to default)')
468
503
  .option('--json', 'Output as JSON')
469
504
  .action((key, options) => {
470
- const config = getGlobalConfig();
505
+ assertGlobalConfigReadable();
506
+ const config = getStoredGlobalConfig();
507
+ // Unlike `get`, no --allow-unknown here: a key the file already holds is
508
+ // removed even when this build does not know it, so a key written with
509
+ // `set --allow-unknown`, or left behind by an older schema, stays
510
+ // removable without a flag. Only an unknown key the file lacks is
511
+ // refused; a typo there would otherwise report success.
512
+ const keyValidation = validateConfigKeyPath(key);
513
+ if (!keyValidation.valid &&
514
+ (hasUnsafeKeySegment(key) || getNestedValue(config, key) === undefined)) {
515
+ const reason = keyValidation.reason ? ` ${keyValidation.reason}.` : '';
516
+ const message = `Invalid configuration key "${key}".${reason}`;
517
+ const fix = 'Use "tospec config list" to see available keys.';
518
+ if (options.json) {
519
+ emitFailureStatus(CONFIG_FAILURE_PAYLOADS.unset, { severity: 'error', code: 'config_error', message, fix });
520
+ return;
521
+ }
522
+ console.error(formatLabeled('Error', message));
523
+ console.error(formatLabeled('Fix', fix));
524
+ process.exitCode = 1;
525
+ return;
526
+ }
527
+ assertGlobalConfigWritable([key.split('.')[0]]);
471
528
  const existed = deleteNestedValue(config, key);
472
529
  if (existed)
473
530
  saveGlobalConfig(config);
531
+ // Only removing `workflows` can leave `custom` empty: removing `profile`
532
+ // reverts to `core`, which ignores the list.
533
+ const warning = key.split('.')[0] === 'workflows'
534
+ ? profileNeedsWorkflowsDiagnostic(config)
535
+ : null;
474
536
  if (options.json) {
475
537
  // Success either way: unsetting an absent key is the state the caller
476
538
  // asked for. `existed` is what says whether anything changed.
477
- emitSuccess({ unset: { key, existed } });
539
+ emitSuccess({ unset: { key, existed }, ...(warning ? { status: [warning] } : {}) });
478
540
  return;
479
541
  }
480
542
  console.log(existed ? `Unset ${key} (reverted to default)` : `Key "${key}" was not set`);
543
+ if (warning) {
544
+ console.error(`Warning: ${warning.message}`);
545
+ if (warning.fix)
546
+ console.error(formatLabeled('Fix', warning.fix));
547
+ }
481
548
  });
482
549
  configCmd
483
550
  .command('reset')
@@ -497,8 +564,7 @@ export function registerConfigCommand(program) {
497
564
  });
498
565
  return;
499
566
  }
500
- console.error('Error: --all flag is required for reset');
501
- console.error('Usage: tospec config reset --all [-y]');
567
+ console.error(formatLabeled('Error', '--all flag is required for reset\nUsage: tospec config reset --all [-y]'));
502
568
  process.exitCode = 1;
503
569
  return;
504
570
  }
@@ -514,6 +580,14 @@ export function registerConfigCommand(program) {
514
580
  });
515
581
  return;
516
582
  }
583
+ // Without a terminal the prompt reads stdin's EOF as a cancel, and the run
584
+ // ended in exit 130, the code that means the user pressed Ctrl-C.
585
+ if (!options.yes && !isInteractive()) {
586
+ console.error(formatLabeled('Error', 'Cannot confirm the reset: no interactive terminal.'));
587
+ console.error(formatLabeled('Fix', 'tospec config reset --all --yes'));
588
+ process.exitCode = 1;
589
+ return;
590
+ }
517
591
  if (!options.yes) {
518
592
  const { confirm } = await import('@inquirer/prompts');
519
593
  let confirmed;
@@ -536,7 +610,14 @@ export function registerConfigCommand(program) {
536
610
  return;
537
611
  }
538
612
  }
539
- saveGlobalConfig({ ...DEFAULT_CONFIG });
613
+ // The one write allowed over an unparseable file: replacing it is the
614
+ // point, and without it a broken config has no recovery from the CLI.
615
+ //
616
+ // An empty object, not DEFAULT_CONFIG: written out, the defaults became
617
+ // stored values, and `list` labelled the reset profile "(explicit)". Not
618
+ // a deleted file either: with none here, reads fall back to a legacy
619
+ // config and the reset would bring its settings back.
620
+ saveGlobalConfig({}, { overwriteUnreadable: true });
540
621
  if (options.json) {
541
622
  emitSuccess({ config: getGlobalConfig() });
542
623
  return;
@@ -549,32 +630,51 @@ export function registerConfigCommand(program) {
549
630
  .action(async () => {
550
631
  const editor = process.env.EDITOR || process.env.VISUAL;
551
632
  if (!editor) {
552
- console.error('Error: No editor configured');
553
- console.error('Set the EDITOR or VISUAL environment variable to your preferred editor');
554
- console.error('Example: export EDITOR=vim');
633
+ console.error(formatLabeled('Error', 'No editor configured\nSet the EDITOR or VISUAL environment variable to your preferred editor\nExample: export EDITOR=vim'));
555
634
  process.exitCode = 1;
556
635
  return;
557
636
  }
558
637
  const configPath = getGlobalConfigPath();
638
+ // Runs only when the write target is absent, so it overwrites nothing; the
639
+ // unreadable guard would otherwise send a user whose only file is a broken
640
+ // legacy one back to this very command.
641
+ //
642
+ // Seeded empty for the reason `reset` writes `{}`: a seeded default is a
643
+ // stored value, so closing the editor unchanged left the profile
644
+ // "(explicit)" though nobody had chosen it.
559
645
  if (!fs.existsSync(configPath)) {
560
- saveGlobalConfig({ ...DEFAULT_CONFIG });
646
+ saveGlobalConfig({}, { overwriteUnreadable: true });
647
+ }
648
+ // `EDITOR="code --wait"` is a command line, not a program name: spawned
649
+ // whole, it was looked up as a file literally named "code --wait" and
650
+ // failed with ENOENT. Split here, never handed to a shell — `shell: true`
651
+ // would also run whatever else the variable contains.
652
+ const [command, ...editorArgs] = splitCommandLine(editor);
653
+ if (!command) {
654
+ console.error('Error: EDITOR is set but names no program.');
655
+ process.exitCode = 1;
656
+ return;
561
657
  }
562
- // shell: false so a space in the editor or config path is not re-parsed.
563
- const child = spawn(editor, [configPath], {
564
- stdio: 'inherit',
565
- shell: false,
566
- });
567
- await new Promise((resolve, reject) => {
568
- child.on('close', (code) => {
569
- if (code === 0) {
570
- resolve();
571
- }
572
- else {
573
- reject(new Error(`Editor exited with code ${code}`));
574
- }
575
- });
576
- child.on('error', reject);
658
+ // cross-spawn, not child_process.spawn: on Windows `code` is `code.cmd`,
659
+ // which only cmd.exe can run, and reaching it safely means cmd.exe's
660
+ // quoting rules (`%`, `^`, `&`, `"`) for every argument, the config path
661
+ // included. That escaping is the library's whole purpose; a hand-rolled
662
+ // copy is the BatBadBut bug class Node itself had to patch.
663
+ const outcome = await new Promise((resolve) => {
664
+ const child = crossSpawn(command, [...editorArgs, configPath], { stdio: 'inherit' });
665
+ child.on('close', (code) => resolve({ code }));
666
+ child.on('error', (error) => resolve({ error }));
577
667
  });
668
+ if ('error' in outcome) {
669
+ console.error(`Error: Could not start editor "${command}": ${outcome.error.message}`);
670
+ process.exitCode = 1;
671
+ return;
672
+ }
673
+ if (outcome.code !== 0) {
674
+ console.error(`Error: Editor "${command}" exited with code ${outcome.code}.`);
675
+ process.exitCode = 1;
676
+ return;
677
+ }
578
678
  try {
579
679
  const rawConfig = fs.readFileSync(configPath, 'utf-8');
580
680
  // `config edit` just handed this file to the user's editor, and a
@@ -593,8 +693,7 @@ export function registerConfigCommand(program) {
593
693
  console.error(`Error: Config file not found at ${configPath}`);
594
694
  }
595
695
  else if (error instanceof SyntaxError) {
596
- console.error(`Error: Invalid JSON in ${configPath}`);
597
- console.error(error.message);
696
+ console.error(formatLabeled('Error', `Invalid JSON in ${configPath}\n${error.message}`));
598
697
  }
599
698
  else {
600
699
  console.error(`Error: Unable to validate configuration - ${error instanceof Error ? error.message : String(error)}`);
@@ -615,7 +714,7 @@ export function registerConfigCommand(program) {
615
714
  }
616
715
  console.error(`Error: ${message}`);
617
716
  if (fix)
618
- console.error(`Fix: ${fix}`);
717
+ console.error(formatLabeled('Fix', fix));
619
718
  process.exitCode = 1;
620
719
  };
621
720
  /** The resolved state, so a caller need not re-read it with `config list`. */
@@ -629,7 +728,8 @@ export function registerConfigCommand(program) {
629
728
  // The stored list is dropped, not rewritten to ALL_WORKFLOWS: rewriting it
630
729
  // manufactures the state `ignoredWorkflowsDiagnostic` warns about.
631
730
  if (preset === 'core') {
632
- const config = getGlobalConfig();
731
+ assertGlobalConfigWritable(['profile', 'workflows']);
732
+ const config = getStoredGlobalConfig();
633
733
  config.profile = 'core';
634
734
  delete config.workflows;
635
735
  saveGlobalConfig(config);
@@ -639,11 +739,17 @@ export function registerConfigCommand(program) {
639
739
  // The command `config set workflows` and `config list` both name as the
640
740
  // way to make a workflow list take effect.
641
741
  if (preset === 'custom') {
642
- const config = getGlobalConfig();
742
+ // Before the read: an unparseable file reads as the defaults, which have
743
+ // no workflows list, so the refusal below would send the user to
744
+ // `config set workflows` — a write the same file then refuses.
745
+ assertGlobalConfigReadable();
746
+ // An invalid `workflows` is refused here, not counted as a list below.
747
+ assertGlobalConfigWritable(['profile']);
748
+ const config = getStoredGlobalConfig();
643
749
  // Without a list, `custom` resolves to zero workflows and the next
644
750
  // `tospec update` deletes every installed skill directory.
645
751
  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(',')}"`);
752
+ fail('Profile "custom" needs a workflows list, and none is configured.', workflowsListFix());
647
753
  return;
648
754
  }
649
755
  config.profile = 'custom';
@@ -663,13 +769,20 @@ export function registerConfigCommand(program) {
663
769
  return;
664
770
  }
665
771
  if (!process.stdout.isTTY) {
666
- console.error('Interactive mode required. Use `tospec config profile core|custom` or set config via environment/flags.');
772
+ console.error('Error: Interactive mode required. Use `tospec config profile core|custom` or set config via environment/flags.');
667
773
  process.exitCode = 1;
668
774
  return;
669
775
  }
776
+ // Before the picker: the save at its end would refuse the file anyway, after
777
+ // the user had answered every prompt against defaults shown as "current".
778
+ assertGlobalConfigReadable();
670
779
  const { select, checkbox, confirm } = await import('@inquirer/prompts');
671
780
  try {
781
+ // Shown from what reads resolve, saved onto what the file holds: for a
782
+ // file with an invalid field the two differ, and saving the defaults
783
+ // would erase the user's other settings.
672
784
  const config = getGlobalConfig();
785
+ const stored = getStoredGlobalConfig();
673
786
  const currentState = resolveCurrentProfileState(config);
674
787
  console.log('\nCurrent profile settings');
675
788
  console.log(` Workflows: ${formatWorkflowSummary(currentState.workflows, currentState.profile)}`);
@@ -745,14 +858,14 @@ export function registerConfigCommand(program) {
745
858
  console.log(` ${line}`);
746
859
  }
747
860
  console.log();
748
- config.profile = nextState.profile;
861
+ stored.profile = nextState.profile;
749
862
  // Same rule as the `core` preset above: a list stored under `core` is an
750
863
  // ignored key every later `config list` warns about.
751
864
  if (nextState.profile === 'core')
752
- delete config.workflows;
865
+ delete stored.workflows;
753
866
  else
754
- config.workflows = nextState.workflows;
755
- saveGlobalConfig(config);
867
+ stored.workflows = nextState.workflows;
868
+ saveGlobalConfig(stored);
756
869
  const projectDir = process.cwd();
757
870
  const tospecDir = path.join(projectDir, TOSPEC_DIR_NAME);
758
871
  if (fs.existsSync(tospecDir)) {
@@ -768,8 +881,8 @@ export function registerConfigCommand(program) {
768
881
  console.log('Run `tospec update` in your other projects to apply.');
769
882
  }
770
883
  catch (error) {
771
- console.error(`\`tospec update\` failed: ${asErrorMessage(error)}`);
772
- console.error('Please run it manually to apply the profile changes.');
884
+ console.error(`Error: \`tospec update\` failed: ${asErrorMessage(error)}`);
885
+ console.error('Fix: Run `tospec update` manually to apply the profile changes.');
773
886
  process.exitCode = 1;
774
887
  }
775
888
  return;