@seanmars/tospec 0.19.0-beta.7 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (429) hide show
  1. package/CHANGELOG.md +60 -231
  2. package/README.md +69 -82
  3. package/assets/dashboard/app.js +14 -3
  4. package/assets/dashboard/style.css +7 -0
  5. package/assets/rules/tospec/decision.md +3 -0
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +227 -86
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/commands/config.d.ts +9 -17
  10. package/dist/commands/config.d.ts.map +1 -1
  11. package/dist/commands/config.js +366 -107
  12. package/dist/commands/config.js.map +1 -1
  13. package/dist/commands/dashboard.d.ts +57 -99
  14. package/dist/commands/dashboard.d.ts.map +1 -1
  15. package/dist/commands/dashboard.js +224 -282
  16. package/dist/commands/dashboard.js.map +1 -1
  17. package/dist/commands/decision.d.ts +41 -19
  18. package/dist/commands/decision.d.ts.map +1 -1
  19. package/dist/commands/decision.js +385 -79
  20. package/dist/commands/decision.js.map +1 -1
  21. package/dist/commands/metrics.d.ts +28 -51
  22. package/dist/commands/metrics.d.ts.map +1 -1
  23. package/dist/commands/metrics.js +62 -93
  24. package/dist/commands/metrics.js.map +1 -1
  25. package/dist/commands/shared-output.d.ts +18 -21
  26. package/dist/commands/shared-output.d.ts.map +1 -1
  27. package/dist/commands/shared-output.js +49 -23
  28. package/dist/commands/shared-output.js.map +1 -1
  29. package/dist/commands/show.d.ts +7 -0
  30. package/dist/commands/show.d.ts.map +1 -1
  31. package/dist/commands/show.js +29 -7
  32. package/dist/commands/show.js.map +1 -1
  33. package/dist/commands/validate.d.ts +43 -45
  34. package/dist/commands/validate.d.ts.map +1 -1
  35. package/dist/commands/validate.js +262 -129
  36. package/dist/commands/validate.js.map +1 -1
  37. package/dist/commands/workflow/index.d.ts +6 -10
  38. package/dist/commands/workflow/index.d.ts.map +1 -1
  39. package/dist/commands/workflow/index.js +6 -10
  40. package/dist/commands/workflow/index.js.map +1 -1
  41. package/dist/commands/workflow/instructions.d.ts +21 -8
  42. package/dist/commands/workflow/instructions.d.ts.map +1 -1
  43. package/dist/commands/workflow/instructions.js +254 -95
  44. package/dist/commands/workflow/instructions.js.map +1 -1
  45. package/dist/commands/workflow/new-change.d.ts +4 -5
  46. package/dist/commands/workflow/new-change.d.ts.map +1 -1
  47. package/dist/commands/workflow/new-change.js +90 -25
  48. package/dist/commands/workflow/new-change.js.map +1 -1
  49. package/dist/commands/workflow/schemas.d.ts +3 -5
  50. package/dist/commands/workflow/schemas.d.ts.map +1 -1
  51. package/dist/commands/workflow/schemas.js +37 -11
  52. package/dist/commands/workflow/schemas.js.map +1 -1
  53. package/dist/commands/workflow/shared.d.ts +48 -21
  54. package/dist/commands/workflow/shared.d.ts.map +1 -1
  55. package/dist/commands/workflow/shared.js +36 -33
  56. package/dist/commands/workflow/shared.js.map +1 -1
  57. package/dist/commands/workflow/status.d.ts +10 -6
  58. package/dist/commands/workflow/status.d.ts.map +1 -1
  59. package/dist/commands/workflow/status.js +81 -42
  60. package/dist/commands/workflow/status.js.map +1 -1
  61. package/dist/commands/workflow/templates.d.ts +10 -3
  62. package/dist/commands/workflow/templates.d.ts.map +1 -1
  63. package/dist/commands/workflow/templates.js +39 -40
  64. package/dist/commands/workflow/templates.js.map +1 -1
  65. package/dist/core/archive.d.ts +26 -21
  66. package/dist/core/archive.d.ts.map +1 -1
  67. package/dist/core/archive.js +378 -235
  68. package/dist/core/archive.js.map +1 -1
  69. package/dist/core/artifact-graph/graph.d.ts +25 -42
  70. package/dist/core/artifact-graph/graph.d.ts.map +1 -1
  71. package/dist/core/artifact-graph/graph.js +45 -63
  72. package/dist/core/artifact-graph/graph.js.map +1 -1
  73. package/dist/core/artifact-graph/index.d.ts +1 -1
  74. package/dist/core/artifact-graph/index.d.ts.map +1 -1
  75. package/dist/core/artifact-graph/index.js +1 -1
  76. package/dist/core/artifact-graph/index.js.map +1 -1
  77. package/dist/core/artifact-graph/instruction-loader.d.ts +97 -103
  78. package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
  79. package/dist/core/artifact-graph/instruction-loader.js +146 -119
  80. package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
  81. package/dist/core/artifact-graph/outputs.d.ts +9 -23
  82. package/dist/core/artifact-graph/outputs.d.ts.map +1 -1
  83. package/dist/core/artifact-graph/outputs.js +45 -38
  84. package/dist/core/artifact-graph/outputs.js.map +1 -1
  85. package/dist/core/artifact-graph/resolver.d.ts +44 -63
  86. package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
  87. package/dist/core/artifact-graph/resolver.js +85 -86
  88. package/dist/core/artifact-graph/resolver.js.map +1 -1
  89. package/dist/core/artifact-graph/schema.d.ts +0 -6
  90. package/dist/core/artifact-graph/schema.d.ts.map +1 -1
  91. package/dist/core/artifact-graph/schema.js +7 -32
  92. package/dist/core/artifact-graph/schema.js.map +1 -1
  93. package/dist/core/artifact-graph/state.d.ts +1 -8
  94. package/dist/core/artifact-graph/state.d.ts.map +1 -1
  95. package/dist/core/artifact-graph/state.js +2 -17
  96. package/dist/core/artifact-graph/state.js.map +1 -1
  97. package/dist/core/artifact-graph/stub-detection.d.ts +12 -0
  98. package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -0
  99. package/dist/core/artifact-graph/stub-detection.js +39 -0
  100. package/dist/core/artifact-graph/stub-detection.js.map +1 -0
  101. package/dist/core/artifact-graph/types.d.ts +4 -0
  102. package/dist/core/artifact-graph/types.d.ts.map +1 -1
  103. package/dist/core/artifact-graph/types.js +30 -10
  104. package/dist/core/artifact-graph/types.js.map +1 -1
  105. package/dist/core/available-tools.d.ts +3 -12
  106. package/dist/core/available-tools.d.ts.map +1 -1
  107. package/dist/core/available-tools.js +4 -13
  108. package/dist/core/available-tools.js.map +1 -1
  109. package/dist/core/change-metadata/schema.d.ts +1 -1
  110. package/dist/core/change-metadata/schema.d.ts.map +1 -1
  111. package/dist/core/change-metadata/schema.js +10 -7
  112. package/dist/core/change-metadata/schema.js.map +1 -1
  113. package/dist/core/change-presenter.d.ts +23 -18
  114. package/dist/core/change-presenter.d.ts.map +1 -1
  115. package/dist/core/change-presenter.js +102 -43
  116. package/dist/core/change-presenter.js.map +1 -1
  117. package/dist/core/change-status-policy.d.ts +8 -1
  118. package/dist/core/change-status-policy.d.ts.map +1 -1
  119. package/dist/core/change-status-policy.js +25 -1
  120. package/dist/core/change-status-policy.js.map +1 -1
  121. package/dist/core/codex-metrics.d.ts +25 -45
  122. package/dist/core/codex-metrics.d.ts.map +1 -1
  123. package/dist/core/codex-metrics.js +44 -88
  124. package/dist/core/codex-metrics.js.map +1 -1
  125. package/dist/core/codex-residue.d.ts +14 -15
  126. package/dist/core/codex-residue.d.ts.map +1 -1
  127. package/dist/core/codex-residue.js +18 -22
  128. package/dist/core/codex-residue.js.map +1 -1
  129. package/dist/core/command-generation/adapters/claude.d.ts +2 -9
  130. package/dist/core/command-generation/adapters/claude.d.ts.map +1 -1
  131. package/dist/core/command-generation/adapters/claude.js +2 -12
  132. package/dist/core/command-generation/adapters/claude.js.map +1 -1
  133. package/dist/core/command-generation/adapters/index.d.ts +1 -9
  134. package/dist/core/command-generation/adapters/index.d.ts.map +1 -1
  135. package/dist/core/command-generation/adapters/index.js +1 -9
  136. package/dist/core/command-generation/adapters/index.js.map +1 -1
  137. package/dist/core/command-generation/generator.d.ts +0 -17
  138. package/dist/core/command-generation/generator.d.ts.map +1 -1
  139. package/dist/core/command-generation/generator.js +0 -17
  140. package/dist/core/command-generation/generator.js.map +1 -1
  141. package/dist/core/command-generation/index.d.ts +2 -5
  142. package/dist/core/command-generation/index.d.ts.map +1 -1
  143. package/dist/core/command-generation/index.js +0 -9
  144. package/dist/core/command-generation/index.js.map +1 -1
  145. package/dist/core/command-generation/types.d.ts +10 -36
  146. package/dist/core/command-generation/types.d.ts.map +1 -1
  147. package/dist/core/command-generation/types.js +0 -6
  148. package/dist/core/command-generation/types.js.map +1 -1
  149. package/dist/core/command-generation/yaml.d.ts +3 -18
  150. package/dist/core/command-generation/yaml.d.ts.map +1 -1
  151. package/dist/core/command-generation/yaml.js +5 -23
  152. package/dist/core/command-generation/yaml.js.map +1 -1
  153. package/dist/core/config-prompts.d.ts +2 -4
  154. package/dist/core/config-prompts.d.ts.map +1 -1
  155. package/dist/core/config-prompts.js +2 -7
  156. package/dist/core/config-prompts.js.map +1 -1
  157. package/dist/core/config-schema.d.ts +8 -53
  158. package/dist/core/config-schema.d.ts.map +1 -1
  159. package/dist/core/config-schema.js +49 -62
  160. package/dist/core/config-schema.js.map +1 -1
  161. package/dist/core/config.d.ts +39 -26
  162. package/dist/core/config.d.ts.map +1 -1
  163. package/dist/core/config.js +36 -22
  164. package/dist/core/config.js.map +1 -1
  165. package/dist/core/dashboard-activity.d.ts +7 -9
  166. package/dist/core/dashboard-activity.d.ts.map +1 -1
  167. package/dist/core/dashboard-activity.js +26 -24
  168. package/dist/core/dashboard-activity.js.map +1 -1
  169. package/dist/core/dashboard-data.d.ts +40 -22
  170. package/dist/core/dashboard-data.d.ts.map +1 -1
  171. package/dist/core/dashboard-data.js +62 -70
  172. package/dist/core/dashboard-data.js.map +1 -1
  173. package/dist/core/global-config.d.ts +24 -53
  174. package/dist/core/global-config.d.ts.map +1 -1
  175. package/dist/core/global-config.js +38 -67
  176. package/dist/core/global-config.js.map +1 -1
  177. package/dist/core/init.d.ts +25 -19
  178. package/dist/core/init.d.ts.map +1 -1
  179. package/dist/core/init.js +168 -181
  180. package/dist/core/init.js.map +1 -1
  181. package/dist/core/list.d.ts +1 -1
  182. package/dist/core/list.d.ts.map +1 -1
  183. package/dist/core/list.js +121 -28
  184. package/dist/core/list.js.map +1 -1
  185. package/dist/core/local-server.d.ts +41 -83
  186. package/dist/core/local-server.d.ts.map +1 -1
  187. package/dist/core/local-server.js +53 -98
  188. package/dist/core/local-server.js.map +1 -1
  189. package/dist/core/markdown-render.d.ts +25 -0
  190. package/dist/core/markdown-render.d.ts.map +1 -0
  191. package/dist/core/markdown-render.js +94 -0
  192. package/dist/core/markdown-render.js.map +1 -0
  193. package/dist/core/migrate.d.ts +29 -14
  194. package/dist/core/migrate.d.ts.map +1 -1
  195. package/dist/core/migrate.js +184 -122
  196. package/dist/core/migrate.js.map +1 -1
  197. package/dist/core/parsers/change-parser.d.ts +7 -10
  198. package/dist/core/parsers/change-parser.d.ts.map +1 -1
  199. package/dist/core/parsers/change-parser.js +48 -56
  200. package/dist/core/parsers/change-parser.js.map +1 -1
  201. package/dist/core/parsers/markdown-parser.d.ts +8 -9
  202. package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
  203. package/dist/core/parsers/markdown-parser.js +31 -22
  204. package/dist/core/parsers/markdown-parser.js.map +1 -1
  205. package/dist/core/parsers/requirement-blocks.d.ts +43 -15
  206. package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
  207. package/dist/core/parsers/requirement-blocks.js +142 -70
  208. package/dist/core/parsers/requirement-blocks.js.map +1 -1
  209. package/dist/core/parsers/requirement-text.d.ts +73 -79
  210. package/dist/core/parsers/requirement-text.d.ts.map +1 -1
  211. package/dist/core/parsers/requirement-text.js +137 -79
  212. package/dist/core/parsers/requirement-text.js.map +1 -1
  213. package/dist/core/parsers/spec-structure.d.ts.map +1 -1
  214. package/dist/core/parsers/spec-structure.js +10 -6
  215. package/dist/core/parsers/spec-structure.js.map +1 -1
  216. package/dist/core/profiles.d.ts +3 -10
  217. package/dist/core/profiles.d.ts.map +1 -1
  218. package/dist/core/profiles.js +5 -12
  219. package/dist/core/profiles.js.map +1 -1
  220. package/dist/core/project-config.d.ts +43 -44
  221. package/dist/core/project-config.d.ts.map +1 -1
  222. package/dist/core/project-config.js +107 -82
  223. package/dist/core/project-config.js.map +1 -1
  224. package/dist/core/project-layout.d.ts +9 -17
  225. package/dist/core/project-layout.d.ts.map +1 -1
  226. package/dist/core/project-layout.js +16 -26
  227. package/dist/core/project-layout.js.map +1 -1
  228. package/dist/core/root-selection.d.ts +11 -7
  229. package/dist/core/root-selection.d.ts.map +1 -1
  230. package/dist/core/root-selection.js +7 -8
  231. package/dist/core/root-selection.js.map +1 -1
  232. package/dist/core/rules.d.ts +6 -1
  233. package/dist/core/rules.d.ts.map +1 -1
  234. package/dist/core/rules.js +23 -2
  235. package/dist/core/rules.js.map +1 -1
  236. package/dist/core/schema-names.d.ts +16 -0
  237. package/dist/core/schema-names.d.ts.map +1 -0
  238. package/dist/core/schema-names.js +16 -0
  239. package/dist/core/schema-names.js.map +1 -0
  240. package/dist/core/schemas/base.schema.d.ts +3 -0
  241. package/dist/core/schemas/base.schema.d.ts.map +1 -1
  242. package/dist/core/schemas/base.schema.js +22 -6
  243. package/dist/core/schemas/base.schema.js.map +1 -1
  244. package/dist/core/schemas/change.schema.d.ts +16 -0
  245. package/dist/core/schemas/change.schema.d.ts.map +1 -1
  246. package/dist/core/schemas/change.schema.js +41 -10
  247. package/dist/core/schemas/change.schema.js.map +1 -1
  248. package/dist/core/schemas/spec.schema.d.ts +2 -0
  249. package/dist/core/schemas/spec.schema.d.ts.map +1 -1
  250. package/dist/core/shared/index.d.ts +3 -8
  251. package/dist/core/shared/index.d.ts.map +1 -1
  252. package/dist/core/shared/index.js +3 -8
  253. package/dist/core/shared/index.js.map +1 -1
  254. package/dist/core/shared/rules-generation.d.ts +19 -8
  255. package/dist/core/shared/rules-generation.d.ts.map +1 -1
  256. package/dist/core/shared/rules-generation.js +117 -50
  257. package/dist/core/shared/rules-generation.js.map +1 -1
  258. package/dist/core/shared/skill-generation.d.ts +28 -43
  259. package/dist/core/shared/skill-generation.d.ts.map +1 -1
  260. package/dist/core/shared/skill-generation.js +82 -51
  261. package/dist/core/shared/skill-generation.js.map +1 -1
  262. package/dist/core/shared/tool-detection.d.ts +39 -61
  263. package/dist/core/shared/tool-detection.d.ts.map +1 -1
  264. package/dist/core/shared/tool-detection.js +88 -80
  265. package/dist/core/shared/tool-detection.js.map +1 -1
  266. package/dist/core/skill-metrics.d.ts +36 -63
  267. package/dist/core/skill-metrics.d.ts.map +1 -1
  268. package/dist/core/skill-metrics.js +34 -73
  269. package/dist/core/skill-metrics.js.map +1 -1
  270. package/dist/core/spec-presenter.js +6 -6
  271. package/dist/core/spec-presenter.js.map +1 -1
  272. package/dist/core/specs-apply.d.ts +22 -23
  273. package/dist/core/specs-apply.d.ts.map +1 -1
  274. package/dist/core/specs-apply.js +166 -193
  275. package/dist/core/specs-apply.js.map +1 -1
  276. package/dist/core/templates/fragments/interview.d.ts +2 -6
  277. package/dist/core/templates/fragments/interview.d.ts.map +1 -1
  278. package/dist/core/templates/fragments/interview.js +2 -6
  279. package/dist/core/templates/fragments/interview.js.map +1 -1
  280. package/dist/core/templates/fragments/next-step.d.ts +4 -8
  281. package/dist/core/templates/fragments/next-step.d.ts.map +1 -1
  282. package/dist/core/templates/fragments/next-step.js +4 -8
  283. package/dist/core/templates/fragments/next-step.js.map +1 -1
  284. package/dist/core/templates/fragments/validate.d.ts +13 -0
  285. package/dist/core/templates/fragments/validate.d.ts.map +1 -0
  286. package/dist/core/templates/fragments/validate.js +13 -0
  287. package/dist/core/templates/fragments/validate.js.map +1 -0
  288. package/dist/core/templates/fragments/verify.d.ts +9 -12
  289. package/dist/core/templates/fragments/verify.d.ts.map +1 -1
  290. package/dist/core/templates/fragments/verify.js +9 -12
  291. package/dist/core/templates/fragments/verify.js.map +1 -1
  292. package/dist/core/templates/index.d.ts +0 -6
  293. package/dist/core/templates/index.d.ts.map +1 -1
  294. package/dist/core/templates/index.js +0 -7
  295. package/dist/core/templates/index.js.map +1 -1
  296. package/dist/core/templates/skill-templates.d.ts +1 -5
  297. package/dist/core/templates/skill-templates.d.ts.map +1 -1
  298. package/dist/core/templates/skill-templates.js +0 -5
  299. package/dist/core/templates/skill-templates.js.map +1 -1
  300. package/dist/core/templates/types.d.ts +3 -7
  301. package/dist/core/templates/types.d.ts.map +1 -1
  302. package/dist/core/templates/types.js +0 -3
  303. package/dist/core/templates/types.js.map +1 -1
  304. package/dist/core/templates/workflows/apply.d.ts +3 -9
  305. package/dist/core/templates/workflows/apply.d.ts.map +1 -1
  306. package/dist/core/templates/workflows/apply.js +11 -13
  307. package/dist/core/templates/workflows/apply.js.map +1 -1
  308. package/dist/core/templates/workflows/archive.d.ts +0 -6
  309. package/dist/core/templates/workflows/archive.d.ts.map +1 -1
  310. package/dist/core/templates/workflows/archive.js +19 -6
  311. package/dist/core/templates/workflows/archive.js.map +1 -1
  312. package/dist/core/templates/workflows/decision.js +4 -4
  313. package/dist/core/templates/workflows/decision.js.map +1 -1
  314. package/dist/core/templates/workflows/explore.js +1 -1
  315. package/dist/core/templates/workflows/grill.d.ts.map +1 -1
  316. package/dist/core/templates/workflows/grill.js +0 -2
  317. package/dist/core/templates/workflows/grill.js.map +1 -1
  318. package/dist/core/templates/workflows/issue.d.ts +0 -6
  319. package/dist/core/templates/workflows/issue.d.ts.map +1 -1
  320. package/dist/core/templates/workflows/issue.js +3 -2
  321. package/dist/core/templates/workflows/issue.js.map +1 -1
  322. package/dist/core/templates/workflows/propose.d.ts +0 -6
  323. package/dist/core/templates/workflows/propose.d.ts.map +1 -1
  324. package/dist/core/templates/workflows/propose.js +2 -2
  325. package/dist/core/templates/workflows/propose.js.map +1 -1
  326. package/dist/core/templates/workflows/sync.d.ts +2 -8
  327. package/dist/core/templates/workflows/sync.d.ts.map +1 -1
  328. package/dist/core/templates/workflows/sync.js +4 -3
  329. package/dist/core/templates/workflows/sync.js.map +1 -1
  330. package/dist/core/templates/workflows/update.d.ts +0 -6
  331. package/dist/core/templates/workflows/update.d.ts.map +1 -1
  332. package/dist/core/templates/workflows/update.js +9 -2
  333. package/dist/core/templates/workflows/update.js.map +1 -1
  334. package/dist/core/update.d.ts +23 -23
  335. package/dist/core/update.d.ts.map +1 -1
  336. package/dist/core/update.js +139 -118
  337. package/dist/core/update.js.map +1 -1
  338. package/dist/core/user-state-migration.d.ts +13 -15
  339. package/dist/core/user-state-migration.d.ts.map +1 -1
  340. package/dist/core/user-state-migration.js +16 -20
  341. package/dist/core/user-state-migration.js.map +1 -1
  342. package/dist/core/validation/constants.d.ts +4 -10
  343. package/dist/core/validation/constants.d.ts.map +1 -1
  344. package/dist/core/validation/constants.js +25 -20
  345. package/dist/core/validation/constants.js.map +1 -1
  346. package/dist/core/validation/prose-length.d.ts +15 -0
  347. package/dist/core/validation/prose-length.d.ts.map +1 -0
  348. package/dist/core/validation/prose-length.js +29 -0
  349. package/dist/core/validation/prose-length.js.map +1 -0
  350. package/dist/core/validation/purpose-placeholder.d.ts +9 -16
  351. package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
  352. package/dist/core/validation/purpose-placeholder.js +30 -44
  353. package/dist/core/validation/purpose-placeholder.js.map +1 -1
  354. package/dist/core/validation/section-validator.d.ts +4 -4
  355. package/dist/core/validation/section-validator.d.ts.map +1 -1
  356. package/dist/core/validation/section-validator.js +43 -7
  357. package/dist/core/validation/section-validator.js.map +1 -1
  358. package/dist/core/validation/task-numbering.d.ts +6 -3
  359. package/dist/core/validation/task-numbering.d.ts.map +1 -1
  360. package/dist/core/validation/task-numbering.js +23 -11
  361. package/dist/core/validation/task-numbering.js.map +1 -1
  362. package/dist/core/validation/types.d.ts +18 -0
  363. package/dist/core/validation/types.d.ts.map +1 -1
  364. package/dist/core/validation/types.js +12 -1
  365. package/dist/core/validation/types.js.map +1 -1
  366. package/dist/core/validation/validator.d.ts +43 -63
  367. package/dist/core/validation/validator.d.ts.map +1 -1
  368. package/dist/core/validation/validator.js +500 -267
  369. package/dist/core/validation/validator.js.map +1 -1
  370. package/dist/prompts/searchable-multi-select.d.ts +3 -8
  371. package/dist/prompts/searchable-multi-select.d.ts.map +1 -1
  372. package/dist/prompts/searchable-multi-select.js +16 -39
  373. package/dist/prompts/searchable-multi-select.js.map +1 -1
  374. package/dist/utils/change-metadata.d.ts +11 -50
  375. package/dist/utils/change-metadata.d.ts.map +1 -1
  376. package/dist/utils/change-metadata.js +48 -67
  377. package/dist/utils/change-metadata.js.map +1 -1
  378. package/dist/utils/change-utils.d.ts +30 -54
  379. package/dist/utils/change-utils.d.ts.map +1 -1
  380. package/dist/utils/change-utils.js +133 -91
  381. package/dist/utils/change-utils.js.map +1 -1
  382. package/dist/utils/file-lock.d.ts +39 -0
  383. package/dist/utils/file-lock.d.ts.map +1 -0
  384. package/dist/utils/file-lock.js +149 -0
  385. package/dist/utils/file-lock.js.map +1 -0
  386. package/dist/utils/file-system.d.ts +12 -32
  387. package/dist/utils/file-system.d.ts.map +1 -1
  388. package/dist/utils/file-system.js +16 -40
  389. package/dist/utils/file-system.js.map +1 -1
  390. package/dist/utils/frontmatter.d.ts +7 -11
  391. package/dist/utils/frontmatter.d.ts.map +1 -1
  392. package/dist/utils/frontmatter.js +11 -11
  393. package/dist/utils/frontmatter.js.map +1 -1
  394. package/dist/utils/interactive.d.ts +4 -9
  395. package/dist/utils/interactive.d.ts.map +1 -1
  396. package/dist/utils/interactive.js +2 -4
  397. package/dist/utils/interactive.js.map +1 -1
  398. package/dist/utils/item-discovery.d.ts +15 -10
  399. package/dist/utils/item-discovery.d.ts.map +1 -1
  400. package/dist/utils/item-discovery.js +42 -47
  401. package/dist/utils/item-discovery.js.map +1 -1
  402. package/dist/utils/link.d.ts +9 -18
  403. package/dist/utils/link.d.ts.map +1 -1
  404. package/dist/utils/link.js +9 -18
  405. package/dist/utils/link.js.map +1 -1
  406. package/dist/utils/requirement-diff.d.ts +13 -23
  407. package/dist/utils/requirement-diff.d.ts.map +1 -1
  408. package/dist/utils/requirement-diff.js +13 -23
  409. package/dist/utils/requirement-diff.js.map +1 -1
  410. package/dist/utils/spec-files.d.ts +7 -20
  411. package/dist/utils/spec-files.d.ts.map +1 -1
  412. package/dist/utils/spec-files.js +26 -51
  413. package/dist/utils/spec-files.js.map +1 -1
  414. package/dist/utils/task-progress.d.ts +11 -9
  415. package/dist/utils/task-progress.d.ts.map +1 -1
  416. package/dist/utils/task-progress.js +53 -32
  417. package/dist/utils/task-progress.js.map +1 -1
  418. package/dist/utils/timestamp.d.ts +5 -8
  419. package/dist/utils/timestamp.d.ts.map +1 -1
  420. package/dist/utils/timestamp.js +5 -8
  421. package/dist/utils/timestamp.js.map +1 -1
  422. package/package.json +2 -3
  423. package/schemas/decision/templates/decision.md +3 -1
  424. package/schemas/decision/templates/index.md +2 -2
  425. package/schemas/issue/schema.yaml +8 -1
  426. package/schemas/issue/templates/spec.md +27 -2
  427. package/schemas/sdd/schema.yaml +20 -1
  428. package/schemas/sdd/templates/spec.md +27 -2
  429. /package/assets/rules/tospec/{single-sourc-of-truth.md → single-source-of-truth.md} +0 -0
package/dist/core/init.js CHANGED
@@ -1,8 +1,4 @@
1
- /**
2
- * Init Command
3
- *
4
- * Sets up tospec Agent Skills for Claude Code and Codex.
5
- */
1
+ /** Sets up tospec Agent Skills for Claude Code and Codex. */
6
2
  import path from 'path';
7
3
  import * as fs from 'fs';
8
4
  import { createRequire } from 'module';
@@ -12,109 +8,150 @@ import { AI_TOOLS, TOSPEC_DIR_NAME, fallbackToolIds, resolveToolSelection, } fro
12
8
  import { isInteractive } from '../utils/interactive.js';
13
9
  import { serializeConfig } from './config-prompts.js';
14
10
  import { copyDir } from '../utils/link.js';
15
- import { removeLegacySkillDirs, removeLegacyToolSkillLayout, getToolsWithSkillsDir, getToolStates, getSkillTemplates, writeSkillDir, writeToolCommands, writeToolRules, } from './shared/index.js';
11
+ import { removeLegacySkillDirs, removeUnselectedSkillDirs, removeLegacyToolSkillLayout, getToolsWithSkillsDir, getToolStates, getSkillTemplates, writeSkillDir, writeToolCommands, findWorkflowReferenceGaps, describeWorkflowReferenceGaps, } from './shared/index.js';
16
12
  import { getGlobalConfig } from './global-config.js';
17
13
  import { runLegacyUserStateMigration } from './user-state-migration.js';
18
14
  import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
19
15
  import { getAvailableTools } from './available-tools.js';
20
16
  import { resolveConfigFileInDir } from './project-config.js';
21
17
  import { planWorkflowRules, writeWorkflowRules } from './shared/rules-generation.js';
22
- import { hasResidualCodexOutput, RESIDUAL_CODEX_NOTICE } from './codex-residue.js';
18
+ import { hasResidualCodexOutput, RESIDUAL_CODEX_NOTICE, RESIDUAL_CODEX_FIX } from './codex-residue.js';
19
+ import { emitSuccess } from '../commands/shared-output.js';
23
20
  const require = createRequire(import.meta.url);
24
21
  const { version: TOSPEC_VERSION } = require('../../package.json');
25
- // -----------------------------------------------------------------------------
26
- // Constants
27
- // -----------------------------------------------------------------------------
28
22
  const DEFAULT_SCHEMA = 'sdd';
29
- /**
30
- * The canonical skills directory every dependent target is copied from. Named
31
- * once because two things now key off it — the loop that skips copying a
32
- * directory onto itself, and the summary line that only applies to targets
33
- * holding a copy.
34
- */
23
+ /** The canonical skills directory every dependent target is copied from. */
35
24
  function agentsSkillsDirFor(projectPath) {
36
25
  return path.join(projectPath, '.agents', 'skills');
37
26
  }
38
- // -----------------------------------------------------------------------------
39
- // Init Command Class
40
- // -----------------------------------------------------------------------------
27
+ /** The `--json` null-shape; mirrors the success payload with `init` nulled. */
28
+ export const INIT_FAILURE_PAYLOAD = { init: null, root: null };
41
29
  export class InitCommand {
42
30
  toolsArg;
43
31
  force;
44
32
  interactiveOption;
45
33
  profileOverride;
34
+ json;
46
35
  constructor(options = {}) {
47
36
  this.toolsArg = options.tools;
48
37
  this.force = options.force ?? false;
49
38
  this.interactiveOption = options.interactive;
50
39
  this.profileOverride = options.profile;
40
+ this.json = options.json ?? false;
51
41
  }
42
+ /**
43
+ * Progress prose, suppressed under `--json`: the envelope contract allows only
44
+ * one document on stdout.
45
+ */
46
+ say(...args) {
47
+ if (!this.json)
48
+ console.log(...args);
49
+ }
50
+ /**
51
+ * Unlike `say`, a warning has no other carrier — dropping it under `--json`
52
+ * would leave an agent caller less informed than a human.
53
+ */
54
+ warn(code, message, fix) {
55
+ this.jsonWarnings.push({ severity: 'warning', code, message, ...(fix ? { fix } : {}) });
56
+ if (!this.json) {
57
+ console.log(`Warning: ${message}`);
58
+ if (fix)
59
+ console.log(fix);
60
+ }
61
+ }
62
+ /** Emitted alongside the success payload; empty means the key is omitted. */
63
+ jsonWarnings = [];
52
64
  async execute(targetPath) {
53
65
  const projectPath = path.resolve(targetPath);
54
66
  const tospecDir = TOSPEC_DIR_NAME;
55
67
  const tospecPath = path.join(projectPath, tospecDir);
56
68
  // Detect without writing: even the permission probe must follow rule preflight.
57
69
  const extendMode = await FileSystemUtils.directoryExists(tospecPath);
58
- // Detect available tools in the project (task 7.1)
59
70
  const detectedTools = getAvailableTools(projectPath);
60
- // Show animated welcome screen (interactive mode only)
61
71
  const canPrompt = this.canPromptInteractively();
62
72
  if (canPrompt) {
63
73
  const { showWelcomeScreen } = await import('../ui/welcome-screen.js');
64
74
  await showWelcomeScreen();
65
75
  }
66
- // Validate profile override early so invalid values fail before tool setup.
67
- // The resolved value is consumed later when generation reads effective config.
76
+ // Validated early so an invalid value fails before tool setup; the resolved
77
+ // value is read again later, when generation needs the effective config.
68
78
  this.resolveProfileOverride();
69
- // Get tool states before processing
70
79
  const toolStates = getToolStates(projectPath);
71
- // Get tool selection (pass detected tools for pre-selection)
72
80
  const selectedToolIds = await this.getSelectedTools(toolStates, extendMode, detectedTools, projectPath);
73
- // Validate selected tools
74
81
  const validatedTools = this.validateTools(selectedToolIds, toolStates);
75
82
  const rulePlan = await planWorkflowRules(projectPath, validatedTools.map((tool) => tool.value));
76
83
  // Migration remains before every getGlobalConfig(), but after conflict checks.
77
84
  await runLegacyUserStateMigration();
78
- await this.validate(projectPath, tospecPath);
79
- // The decision rule is always installed (only skipped when no agent dir exists).
80
- const installRules = validatedTools.length > 0;
81
- // Create directory structure and config
85
+ await this.validate(projectPath);
82
86
  await this.createDirectoryStructure(tospecPath, extendMode);
83
- // Create config.yaml if needed — must run before skill/command generation.
87
+ // Must run before skill/command generation.
84
88
  const configStatus = await this.createConfig(tospecPath, extendMode);
85
- // Generate skills and commands for each tool
86
- const results = await this.generateSkillsAndCommands(projectPath, validatedTools, installRules);
89
+ // A profile left at `custom` with nothing picked resolves to zero workflows,
90
+ // so init would report both tools `created` with no skill on disk. Raised
91
+ // before the write, and only when a tool was asked for — "no tools selected"
92
+ // has its own line and is not a profile problem.
93
+ const initConfig = getGlobalConfig();
94
+ const initProfile = this.profileOverride ?? initConfig.profile ?? 'core';
95
+ if (validatedTools.length > 0 &&
96
+ getProfileWorkflows(initProfile, initConfig.workflows).length === 0) {
97
+ this.warn('init_profile_selects_no_workflows', `Profile '${initProfile}' selects no workflows, so this init writes no tospec skills.`, 'Run "tospec config profile core" then "tospec init" to install them.');
98
+ }
99
+ // The same check `update` runs, for the same reason: the skills about to be
100
+ // written name their siblings in fixed prose, so a partial selection
101
+ // installs instructions that point at workflows this project will not have.
102
+ if (validatedTools.length > 0) {
103
+ const referenceGaps = findWorkflowReferenceGaps(getProfileWorkflows(initProfile, initConfig.workflows));
104
+ if (referenceGaps.length > 0) {
105
+ const missing = [...new Set(referenceGaps.map((gap) => gap.missing))].sort();
106
+ this.warn('init_workflow_reference_missing', `Installed skills point at workflows this profile leaves out: ${describeWorkflowReferenceGaps(referenceGaps)}.`, `Add them with "tospec config set workflows" (missing: ${missing.join(', ')}), or expect agents to hit instructions they cannot follow.`);
107
+ }
108
+ }
109
+ const results = await this.generateSkillsAndCommands(projectPath, validatedTools);
87
110
  const successfulIds = new Set([...results.createdTools, ...results.refreshedTools].map((tool) => tool.value));
88
- await writeWorkflowRules(rulePlan.filter((entry) => successfulIds.has(entry.toolId)));
89
- // Display success message
111
+ await writeWorkflowRules(rulePlan.filter((entry) => successfulIds.has(entry.toolId)), {
112
+ silent: this.json,
113
+ });
90
114
  this.displaySuccessMessage(projectPath, validatedTools, results, configStatus);
91
- // Last, so a run that dies in rule preflight never leaves this behind as
92
- // advice about a state it did not reach.
115
+ // After every write, so a run that dies in rule preflight never leaves
116
+ // advice about a state it did not reach; before the JSON envelope, or the
117
+ // warning is recorded into a document already on stdout.
93
118
  if (hasResidualCodexOutput(projectPath)) {
94
- console.log(RESIDUAL_CODEX_NOTICE);
119
+ this.warn('codex_residue', RESIDUAL_CODEX_NOTICE, RESIDUAL_CODEX_FIX);
120
+ }
121
+ if (this.json) {
122
+ const globalCfg = getGlobalConfig();
123
+ const activeProfile = this.profileOverride ?? globalCfg.profile ?? 'core';
124
+ const activeWorkflows = [...getProfileWorkflows(activeProfile, globalCfg.workflows)];
125
+ emitSuccess({
126
+ init: {
127
+ profile: activeProfile,
128
+ workflows: activeWorkflows,
129
+ created: results.createdTools.map((t) => ({ tool: t.value, dir: t.skillsDir })),
130
+ refreshed: results.refreshedTools.map((t) => ({ tool: t.value, dir: t.skillsDir })),
131
+ failed: results.failedTools.map((f) => ({ name: f.name, error: f.error.message })),
132
+ // `created` only records that a tool was configured, so this is the
133
+ // only field saying what was installed, and it must count what
134
+ // reached a tool directory: `--tools none` writes nothing anywhere.
135
+ skills: results.createdTools.length + results.refreshedTools.length > 0
136
+ ? getSkillTemplates(activeWorkflows).length
137
+ : 0,
138
+ config: configStatus,
139
+ },
140
+ ...(this.jsonWarnings.length ? { status: this.jsonWarnings } : {}),
141
+ }, { path: projectPath, source: 'explicit' });
95
142
  }
96
143
  }
97
- // ═══════════════════════════════════════════════════════════
98
- // VALIDATION & SETUP
99
- // ═══════════════════════════════════════════════════════════
100
- async validate(projectPath, tospecPath) {
101
- const extendMode = await FileSystemUtils.directoryExists(tospecPath);
102
- // Check write permissions
144
+ async validate(projectPath) {
103
145
  if (!(await FileSystemUtils.ensureWritePermissions(projectPath))) {
104
146
  throw new Error(`Insufficient permissions to write to ${projectPath}`);
105
147
  }
106
- return extendMode;
107
148
  }
108
149
  /**
109
- * Every row in the menu is now a real choice, so a row can still arrive
110
- * uninvited — by being required by another. That has to be said *before* the
111
- * prompt: an explanation reaching the user after they answered cannot inform
112
- * the answer. Derived from the table so a future dependency documents itself.
150
+ * A row can arrive uninvited by being required by another, which has to be
151
+ * said *before* the prompt to inform the answer. Derived from the table so a
152
+ * future dependency documents itself.
113
153
  *
114
- * Which is also why it is called from the prompt path only. A run driven by
115
- * `--tools` or `--interactive=false` has no answer to inform, so the same
116
- * lines there are just an unprompted paragraph about targets the user may not
117
- * have picked — `tospec init --tools agents` explaining Claude Code.
154
+ * Prompt path only: a `--tools` run has no answer to inform.
118
155
  */
119
156
  discloseToolDependencies() {
120
157
  for (const tool of AI_TOOLS) {
@@ -127,7 +164,7 @@ export class InitCommand {
127
164
  continue;
128
165
  const names = required.map((candidate) => candidate.name).join(', ');
129
166
  const dirs = required.map((candidate) => `${candidate.skillsDir}/skills/`).join(', ');
130
- console.log(`${tool.name} links its skills into ${dirs}, so selecting it also selects ${names}.`);
167
+ this.say(`${tool.name} copies its skills from ${dirs}, so selecting it also selects ${names}.`);
131
168
  }
132
169
  }
133
170
  canPromptInteractively() {
@@ -146,11 +183,7 @@ export class InitCommand {
146
183
  }
147
184
  throw new Error(`Invalid profile "${this.profileOverride}". Available profiles: core, custom`);
148
185
  }
149
- // ═══════════════════════════════════════════════════════════
150
- // TOOL SELECTION
151
- // ═══════════════════════════════════════════════════════════
152
186
  async getSelectedTools(toolStates, extendMode, detectedTools, projectPath) {
153
- // Check for --tools flag first
154
187
  const nonInteractiveSelection = this.resolveToolsArg();
155
188
  if (nonInteractiveSelection !== null) {
156
189
  return nonInteractiveSelection;
@@ -162,11 +195,9 @@ export class InitCommand {
162
195
  .map(([toolId]) => toolId));
163
196
  const shouldPreselectDetected = !extendMode && configuredToolIds.size === 0;
164
197
  const canPrompt = this.canPromptInteractively();
165
- // Non-interactive mode: whatever was detected, expanded along `requires`.
166
- // Detecting nothing is not a failure — it is an absence of information, not
167
- // a decision to write nothing, so it falls back to the base target rather
168
- // than leaving a first-time non-interactive setup with no skills at all.
169
- // (An explicitly cleared menu *is* a decision, and writes nothing.)
198
+ // Detecting nothing is an absence of information, not a decision to write
199
+ // nothing, so it falls back to the base target. (A cleared menu *is* a
200
+ // decision.)
170
201
  if (!canPrompt) {
171
202
  const detected = [...detectedToolIds];
172
203
  return resolveToolSelection(detected.length > 0 ? detected : fallbackToolIds());
@@ -174,14 +205,11 @@ export class InitCommand {
174
205
  if (validTools.length === 0) {
175
206
  throw new Error(`No tools available for skill generation.`);
176
207
  }
177
- // Interactive mode: show searchable multi-select
178
208
  const { searchableMultiSelect } = await import('../prompts/searchable-multi-select.js');
179
- // Every target with an output directory is offered, including the one
180
- // others link into. What the list shows is what gets written; a row that is
181
- // pulled in by another is pulled in by an expansion the user was told about
182
- // before answering, not by a checkbox they cannot clear.
209
+ // Every target with an output directory is offered, including the one others
210
+ // copy from: what the list shows is what gets written, and a row pulled in
211
+ // by another comes from a disclosed expansion, not a locked checkbox.
183
212
  const offeredTools = getToolsWithSkillsDir();
184
- // Build choices: pre-select configured tools; keep detected tools visible but unselected.
185
213
  const sortedChoices = offeredTools
186
214
  .map((toolId) => {
187
215
  const tool = AI_TOOLS.find((t) => t.value === toolId);
@@ -197,7 +225,7 @@ export class InitCommand {
197
225
  };
198
226
  })
199
227
  .sort((a, b) => {
200
- // Configured tools first, then detected (not configured), then everything else.
228
+ // Configured first, then detected-but-unconfigured, then the rest.
201
229
  if (a.configured && !b.configured)
202
230
  return -1;
203
231
  if (!a.configured && b.configured)
@@ -212,7 +240,7 @@ export class InitCommand {
212
240
  .filter((toolId) => configuredToolIds.has(toolId))
213
241
  .map((toolId) => AI_TOOLS.find((t) => t.value === toolId)?.name || toolId);
214
242
  if (configuredNames.length > 0) {
215
- console.log(`tospec configured: ${configuredNames.join(', ')} (pre-selected)`);
243
+ this.say(`tospec configured: ${configuredNames.join(', ')} (pre-selected)`);
216
244
  }
217
245
  const detectedOnlyNames = detectedTools
218
246
  .filter((tool) => !configuredToolIds.has(tool.value) && offeredTools.includes(tool.value))
@@ -221,14 +249,13 @@ export class InitCommand {
221
249
  const detectionLabel = shouldPreselectDetected
222
250
  ? 'pre-selected for first-time setup'
223
251
  : 'not pre-selected';
224
- console.log(`Detected tool directories: ${detectedOnlyNames.join(', ')} (${detectionLabel})`);
252
+ this.say(`Detected tool directories: ${detectedOnlyNames.join(', ')} (${detectionLabel})`);
225
253
  }
226
254
  // Last thing before the question, so the dependency is on screen while it
227
255
  // can still change the answer.
228
256
  this.discloseToolDependencies();
229
- // No minimum: clearing every checkbox means "write no agent content", the
230
- // same as `--tools none`. Refusing it would reject an answer tospec can
231
- // honour, and honouring it partially would make the list a suggestion.
257
+ // No minimum: clearing every checkbox means "write no agent content", as
258
+ // `--tools none` does.
232
259
  const selectedTools = await searchableMultiSelect({
233
260
  message: `Select tools to set up (${offeredTools.length} available)`,
234
261
  pageSize: 15,
@@ -270,7 +297,7 @@ export class InitCommand {
270
297
  throw new Error(`Invalid tool(s): ${invalidTokens.join(', ')}. Available values: ${availableList}`);
271
298
  }
272
299
  // Expansion deduplicates and orders dependencies first, so naming a tool
273
- // whose skills link into another never produces links with no target.
300
+ // whose skills copy from another never produces a copy with no source.
274
301
  return resolveToolSelection(normalizedTokens);
275
302
  }
276
303
  validateTools(toolIds, toolStates) {
@@ -295,20 +322,14 @@ export class InitCommand {
295
322
  }
296
323
  return validatedTools;
297
324
  }
298
- // ═══════════════════════════════════════════════════════════
299
- // DIRECTORY STRUCTURE
300
- // ═══════════════════════════════════════════════════════════
301
325
  /**
302
- * Anchoring runs on every init, not only the first: a project that predates
303
- * the rule reaches the same state by re-running the command it already runs
304
- * to refresh generated files, with no separate migration step.
305
- *
306
- * extendMode differs only in staying quiet — the work itself is identical, so
307
- * it is a pair of guards rather than a second copy of the loop.
326
+ * Anchoring runs on every init, not only the first, so a project predating the
327
+ * rule reaches the same state without a migration step. `extendMode` differs
328
+ * only in staying quiet.
308
329
  */
309
330
  async createDirectoryStructure(tospecPath, extendMode) {
310
331
  if (!extendMode)
311
- console.log('Creating tospec structure...');
332
+ this.say('Creating tospec structure...');
312
333
  const layout = tospecLayoutDirs(tospecPath);
313
334
  for (const dir of [tospecPath, ...layout]) {
314
335
  await FileSystemUtils.createDirectory(dir);
@@ -317,71 +338,56 @@ export class InitCommand {
317
338
  await ensureDirectoryAnchor(dir);
318
339
  }
319
340
  if (!extendMode)
320
- console.log('Created tospec structure');
341
+ this.say('Created tospec structure');
321
342
  }
322
- // ═══════════════════════════════════════════════════════════
323
- // SKILL GENERATION
324
- // ═══════════════════════════════════════════════════════════
325
- async generateSkillsAndCommands(projectPath, tools, installRules) {
343
+ async generateSkillsAndCommands(projectPath, tools) {
326
344
  const createdTools = [];
327
345
  const refreshedTools = [];
328
346
  const failedTools = [];
329
347
  let removedCommandCount = 0;
330
- // Read global config for profile settings (use --profile override if set)
331
348
  const globalConfig = getGlobalConfig();
332
349
  const profile = this.resolveProfileOverride() ?? globalConfig.profile ?? 'core';
333
350
  const workflows = getProfileWorkflows(profile, globalConfig.workflows);
334
351
  const skillTemplates = getSkillTemplates(workflows);
335
- // Write canonical files once to .agents/ — the single source of truth
336
- // every dependent target is copied from.
337
- //
338
- // Gated on the selection rather than unconditional, so an empty selection
339
- // (and `--tools none`) really writes nothing. The condition is "any target
340
- // at all" rather than "is .agents selected": dependency expansion already
341
- // guarantees it is present whenever anything else is, and a selection that
342
- // somehow omitted it would still need canonical content or its dependents
343
- // would link at nothing.
352
+ // Canonical files, written once to .agents/. Gated on "any target at all" so
353
+ // `--tools none` writes nothing; dependency expansion guarantees .agents is
354
+ // present whenever anything else is.
344
355
  const agentsSkillsDir = agentsSkillsDirFor(projectPath);
345
356
  if (tools.length > 0) {
346
357
  for (const { template, dirName } of skillTemplates) {
347
358
  await writeSkillDir(agentsSkillsDir, dirName, template, TOSPEC_VERSION);
348
359
  }
349
360
  }
350
- // Outside the gate: retiring a workflow tospec itself generated is not
351
- // conditional on this run writing anything new. Gated, `--tools none` — the
352
- // way a user stops tospec writing agent content — would leave a retired
353
- // skill dir on disk and loadable by every agent that reads .agents/skills/,
354
- // which is the opposite of what they asked for.
361
+ // Both sweeps sit outside that gate: retiring a workflow is not conditional
362
+ // on this run writing anything. Gated, `--tools none` would leave a retired
363
+ // skill dir loadable by every agent reading .agents/skills/, and a dropped
364
+ // workflow would come back on the next copy from the canonical directory.
355
365
  await removeLegacySkillDirs(agentsSkillsDir);
366
+ await removeUnselectedSkillDirs(agentsSkillsDir, workflows);
356
367
  removedCommandCount += await this.removeLegacyCommands(projectPath);
357
- // Process each tool
358
368
  for (const tool of tools) {
359
- console.log(`Setting up ${tool.name}...`);
369
+ this.say(`Setting up ${tool.name}...`);
360
370
  try {
361
- // Copy each skill dir out of .agents/skills/ into the tool's standard
362
- // `skills/` dir. Project-scope skills can't live in a nested sub-folder,
363
- // so skills stay flat and prefixed:
364
- // `<tool>/skills/tospec-<workflow>/SKILL.md`. Clean up the intermediate
365
- // grouped/bare layouts (and the retired verify skill) from older builds first.
371
+ // Project-scope skills cannot live in a nested sub-folder, so they stay
372
+ // flat and prefixed: `<tool>/skills/tospec-<workflow>/SKILL.md`.
366
373
  const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
367
374
  await removeLegacyToolSkillLayout(path.join(projectPath, tool.skillsDir));
368
375
  await removeLegacySkillDirs(skillsDir);
369
- // Independent copies, not links: see the header of utils/link.ts. The
370
- // canonical target *is* .agents/skills, and copyDir clears its dest
371
- // first, so pointing it at itself would delete the source.
376
+ // Independent copies, not links: see the header of utils/link.ts.
377
+ // copyDir clears its dest, so copying .agents/skills onto itself would
378
+ // delete the source.
372
379
  if (path.resolve(skillsDir) !== path.resolve(agentsSkillsDir)) {
373
380
  for (const { dirName } of skillTemplates) {
374
381
  await copyDir(path.join(agentsSkillsDir, dirName), path.join(skillsDir, dirName));
375
382
  }
376
383
  }
377
- // Commands: for tools with a registered adapter (Claude Code), write
378
- // `.claude/commands/tosx/<id>.md` so they can be invoked as `/tosx:<id>`.
384
+ // Per-tool prune. The canonical sweep above cannot reach a copy that a
385
+ // previous, wider profile left in this tool's directory.
386
+ await removeUnselectedSkillDirs(skillsDir, workflows);
387
+ // Tools with a registered adapter (Claude Code) get
388
+ // `.claude/commands/tosx/<id>.md`, invokable as `/tosx:<id>`.
379
389
  await writeToolCommands(projectPath, tool.value, workflows);
380
- // Rules: opt-in static guidance under `<tool>/rules/tospec/`.
381
- if (installRules) {
382
- await writeToolRules(projectPath, tool.skillsDir);
383
- }
384
- console.log(`Setup complete for ${tool.name}`);
390
+ this.say(`Setup complete for ${tool.name}`);
385
391
  if (tool.wasConfigured) {
386
392
  refreshedTools.push(tool);
387
393
  }
@@ -390,7 +396,7 @@ export class InitCommand {
390
396
  }
391
397
  }
392
398
  catch (error) {
393
- console.log(`Failed for ${tool.name}`);
399
+ this.say(`Failed for ${tool.name}`);
394
400
  failedTools.push({ name: tool.name, error: error });
395
401
  }
396
402
  }
@@ -401,9 +407,6 @@ export class InitCommand {
401
407
  removedCommandCount,
402
408
  };
403
409
  }
404
- // ═══════════════════════════════════════════════════════════
405
- // CONFIG FILE
406
- // ═══════════════════════════════════════════════════════════
407
410
  async createConfig(tospecPath, extendMode) {
408
411
  const configPath = path.join(tospecPath, 'config.yaml');
409
412
  if (resolveConfigFileInDir(tospecPath)) {
@@ -418,102 +421,86 @@ export class InitCommand {
418
421
  return 'skipped';
419
422
  }
420
423
  }
421
- // ═══════════════════════════════════════════════════════════
422
- // UI & OUTPUT
423
- // ═══════════════════════════════════════════════════════════
424
424
  displaySuccessMessage(projectPath, tools, results, configStatus) {
425
- console.log();
426
- console.log('tospec Setup Complete');
427
- console.log();
428
- // Show created vs refreshed tools
425
+ const globalConfig = getGlobalConfig();
426
+ const activeProfile = this.profileOverride ?? globalConfig.profile ?? 'core';
427
+ const activeWorkflows = [...getProfileWorkflows(activeProfile, globalConfig.workflows)];
428
+ this.say();
429
+ this.say('tospec Setup Complete');
430
+ this.say();
429
431
  if (results.createdTools.length > 0) {
430
- console.log(`Created: ${results.createdTools.map((t) => t.name).join(', ')}`);
432
+ this.say(`Created: ${results.createdTools.map((t) => t.name).join(', ')}`);
431
433
  }
432
434
  if (results.refreshedTools.length > 0) {
433
- console.log(`Refreshed: ${results.refreshedTools.map((t) => t.name).join(', ')}`);
435
+ this.say(`Refreshed: ${results.refreshedTools.map((t) => t.name).join(', ')}`);
434
436
  }
435
- // An empty selection is an answer tospec honours by writing nothing, so the
436
- // report has to say that. Every block below is keyed off a non-empty
437
- // selection, which left the run printing the "Setup Complete" banner and a
438
- // config line with no statement anywhere that no skills exist — the user
439
- // finds out by looking for them.
437
+ // Every block below is keyed off a non-empty selection, so an empty one
438
+ // needs its own line saying no skills exist.
440
439
  if (tools.length === 0) {
441
- console.log('No tools selected: no skills, commands or rules were written.');
440
+ this.say('No tools selected: no skills, commands or rules were written.');
442
441
  }
443
- // Show counts (respecting profile filter)
444
442
  const successfulTools = [...results.createdTools, ...results.refreshedTools];
445
443
  if (successfulTools.length > 0) {
446
- const globalConfig = getGlobalConfig();
447
- const profile = this.profileOverride ?? globalConfig.profile ?? 'core';
448
- const workflows = getProfileWorkflows(profile, globalConfig.workflows);
449
444
  // Trailing slash per directory, not once at the end: with more than one
450
445
  // target ".agents, .claude/" reads as a single odd path.
451
446
  const toolDirs = [...new Set(successfulTools.map((t) => `${t.skillsDir}/`))].join(', ');
452
- const skillCount = getSkillTemplates(workflows).length;
453
- console.log(`${skillCount} skills in ${toolDirs}`);
447
+ const skillCount = getSkillTemplates(activeWorkflows).length;
448
+ this.say(`${skillCount} skills in ${toolDirs}`);
454
449
  // Dependent targets hold independent copies, so an edit to the canonical
455
- // skill does not reach them on its own. Saying so is what stops a user
456
- // editing .agents/skills/ and wondering why the tool never changed.
450
+ // skill does not reach them. Only when something was copied: under an
451
+ // empty profile this would claim a copy from a directory holding none.
457
452
  const dependents = successfulTools.filter((t) => path.resolve(projectPath, t.skillsDir, 'skills') !== path.resolve(agentsSkillsDirFor(projectPath)));
458
- if (dependents.length > 0) {
459
- console.log(`Copied from .agents/skills/: ${dependents.map((t) => t.name).join(', ')} — rerun 'tospec update' after editing the canonical skills`);
453
+ if (skillCount > 0 && dependents.length > 0) {
454
+ this.say(`Copied from .agents/skills/: ${dependents.map((t) => t.name).join(', ')} — rerun 'tospec update' after editing the canonical skills`);
460
455
  }
461
456
  }
462
- // Show failures
463
457
  if (results.failedTools.length > 0) {
464
- console.log(`Failed: ${results.failedTools.map((f) => `${f.name} (${f.error.message})`).join(', ')}`);
458
+ this.say(`Failed: ${results.failedTools.map((f) => `${f.name} (${f.error.message})`).join(', ')}`);
465
459
  }
466
460
  if (results.removedCommandCount > 0) {
467
- console.log(`Removed: ${results.removedCommandCount} legacy command entries`);
461
+ this.say(`Removed: ${results.removedCommandCount} legacy command entries`);
468
462
  }
469
- // Config status
470
463
  if (configStatus === 'created') {
471
- console.log(`Config: tospec/config.yaml (schema: ${DEFAULT_SCHEMA})`);
464
+ this.say(`Config: tospec/config.yaml (schema: ${DEFAULT_SCHEMA})`);
472
465
  }
473
466
  else if (configStatus === 'exists') {
474
- // Show actual filename (config.yaml or config.yml)
467
+ // Name the file that is actually there: config.yaml or config.yml.
475
468
  const configFile = resolveConfigFileInDir(path.join(projectPath, TOSPEC_DIR_NAME));
476
469
  const configName = configFile ? path.basename(configFile) : 'config.yaml';
477
- console.log(`Config: tospec/${configName} (exists)`);
470
+ this.say(`Config: tospec/${configName} (exists)`);
478
471
  }
479
472
  else {
480
- console.log(`Config: skipped (non-interactive mode)`);
473
+ this.say(`Config: skipped (non-interactive mode)`);
481
474
  }
482
- // Getting started (task 7.6: show propose if in profile)
483
- const globalCfg = getGlobalConfig();
484
- const activeProfile = this.profileOverride ?? globalCfg.profile ?? 'core';
485
- const activeWorkflows = [...getProfileWorkflows(activeProfile, globalCfg.workflows)];
486
- console.log();
475
+ this.say();
487
476
  if (tools.length === 0) {
488
- // `tospec-propose` is a skill, and this run wrote none — advising it here
489
- // would send the user to invoke something that is not on disk.
490
- console.log('Getting started:');
491
- console.log(" Add agent skills: tospec init --tools <tool> (or rerun 'tospec init')");
477
+ // `tospec-propose` is a skill and this run wrote none, so advising it
478
+ // would name something not on disk.
479
+ this.say('Getting started:');
480
+ this.say(" Add agent skills: tospec init --tools <tool> (or rerun 'tospec init')");
492
481
  }
493
482
  else if (activeWorkflows.includes('propose')) {
494
- console.log('Getting started:');
495
- console.log(' Start your first change: tospec-propose "your idea"');
483
+ this.say('Getting started:');
484
+ this.say(' Start your first change: tospec-propose "your idea"');
496
485
  }
497
486
  else {
498
- console.log("Done. Run 'tospec config profile' to configure your workflows.");
487
+ this.say("Done. Run 'tospec config profile' to configure your workflows.");
499
488
  }
500
- // Restart instruction if any tools were configured
501
489
  if (results.createdTools.length > 0 || results.refreshedTools.length > 0) {
502
- console.log();
503
- console.log('Restart your agent for skills to take effect.');
490
+ this.say();
491
+ this.say('Restart your agent for skills to take effect.');
504
492
  }
505
- console.log();
493
+ this.say();
506
494
  }
507
495
  async removeLegacyCommands(projectPath) {
508
496
  let removed = 0;
509
- // Commands now live under `.claude/commands/tosx/` (managed by
510
- // writeToolCommands). These are unused/legacy command locations — including
511
- // the old `.claude/commands/tospec/` namespace this replaced.
497
+ // Commands live under `.claude/commands/tosx/` now (writeToolCommands);
498
+ // these are the locations it replaced.
512
499
  const legacyPaths = [
513
500
  path.join(projectPath, '.agents', 'commands', 'tospec'),
514
501
  path.join(projectPath, '.claude', 'commands', 'tospec'),
515
- // Cursor support was removed; this cleanup stays so existing users'
516
- // stale .cursor command files still get swept away on init/update.
502
+ // Cursor support was removed; this stays so stale .cursor command files
503
+ // are still swept away.
517
504
  ...ALL_WORKFLOWS.map((workflow) => path.join(projectPath, '.cursor', 'commands', `tospec-${workflow}.md`)),
518
505
  ];
519
506
  for (const legacyPath of legacyPaths) {