@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
@@ -4,13 +4,18 @@
4
4
  */
5
5
  import path from 'path';
6
6
  import * as fs from 'fs';
7
- import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputs, } from '../../core/artifact-graph/index.js';
7
+ import { loadChangeContext, generateInstructions, resolveSchema, resolveArtifactOutputs, findSchemaWarnings, } from '../../core/artifact-graph/index.js';
8
8
  import { getChangeDir, resolveCurrentPlanningHomeSync, } from '../../core/planning-home.js';
9
9
  import { resolveRootForCommand, toPlanningHome, toRootOutput, } from '../../core/root-selection.js';
10
- import { APPLY_RULES_KEY, readProjectConfigWithDiagnostics } from '../../core/project-config.js';
10
+ import { APPLY_RULES_KEY, printProjectConfigDiagnostic, readProjectConfigWithDiagnostics, } from '../../core/project-config.js';
11
11
  import { parseTasksFromContent, resolveTaskFiles } from '../../utils/task-progress.js';
12
- import { validateChangeExists, validateSchemaExists, assertChangeSchema, } from './shared.js';
12
+ import { readSkipSpecsMarker } from '../../utils/change-metadata.js';
13
+ import { findMisplacedSpecFiles, findSkipSpecsContradictions } from '../../utils/spec-files.js';
14
+ import { changeRequiresDeltas } from '../validate.js';
15
+ import { Validator, headerCorrectionClause } from '../../core/validation/validator.js';
16
+ import { validateChangeExists, validateChangeSchema, changeUnreadable, ChangeUnreadableError, } from './shared.js';
13
17
  import { emitSuccess } from '../shared-output.js';
18
+ import { attributeValue, inlineField, neutralizeTags } from './prompt-escape.js';
14
19
  /**
15
20
  * The `--json` null-shape. This command's success payload is flat, so there is
16
21
  * no wrapper key to null; `instruction` is the one key both success shapes
@@ -42,25 +47,37 @@ async function resolveInstructionsTarget(options) {
42
47
  }
43
48
  const projectRoot = root.path;
44
49
  if (options.schema) {
45
- validateSchemaExists(options.schema, projectRoot);
46
- assertChangeSchema(options.schema);
50
+ validateChangeSchema(options.schema, projectRoot);
47
51
  }
48
52
  const changeName = await validateChangeExists(options.change, projectRoot, root.changesDir, {
49
53
  newChangeHint: 'tospec new change <name>',
50
54
  });
51
55
  return { root, planningHome: toPlanningHome(root), projectRoot, changeName };
52
56
  }
57
+ /**
58
+ * The change's context, or `change_unreadable` when it exists but does not
59
+ * load. The schema comes from the change's metadata when not given. Only the
60
+ * load is wrapped, as in `status --change`: a missing change and a bad
61
+ * `--schema` are refused before it, under their own codes.
62
+ */
63
+ function loadInstructionsContext(projectRoot, changeName, schemaName, planningHome) {
64
+ try {
65
+ return loadChangeContext(projectRoot, changeName, schemaName, {
66
+ changeDir: getChangeDir(planningHome, changeName),
67
+ planningHome,
68
+ });
69
+ }
70
+ catch (error) {
71
+ throw new ChangeUnreadableError(changeUnreadable(changeName, error, { planningHome, projectRoot, schema: schemaName }).diagnostic);
72
+ }
73
+ }
53
74
  export async function instructionsCommand(artifactId, options) {
54
75
  const target = await resolveInstructionsTarget(options);
55
76
  if (!target) {
56
77
  return;
57
78
  }
58
79
  const { root, planningHome, projectRoot, changeName } = target;
59
- // The schema comes from the change's metadata when not given.
60
- const context = loadChangeContext(projectRoot, changeName, options.schema, {
61
- changeDir: getChangeDir(planningHome, changeName),
62
- planningHome,
63
- });
80
+ const context = loadInstructionsContext(projectRoot, changeName, options.schema, planningHome);
64
81
  // Includes `apply`: the schema's artifacts alone omit the one valid argument
65
82
  // that is not an artifact.
66
83
  const validArgumentList = () => [...context.graph.getAllArtifacts().map((a) => a.id), APPLY_ARGUMENT].join('\n ');
@@ -84,9 +101,7 @@ export async function instructionsCommand(artifactId, options) {
84
101
  // while `status` called it ready — and sent the agent to write the specs the
85
102
  // change had just declared it has none of.
86
103
  const unmetDependencies = instructions.dependencies
87
- .filter((d) => !d.done &&
88
- !(context.skippedArtifacts?.has(d.id) ?? false) &&
89
- context.graph.getArtifact(d.id)?.optional !== true)
104
+ .filter((d) => !d.done && !isMetWithoutFile(d))
90
105
  .map((d) => d.id);
91
106
  // `status` says "skipped" for this artifact, so a brief that reads as a
92
107
  // plain "write it" sends an agent into the contradiction `validate` rejects
@@ -155,25 +170,28 @@ function blockedDiagnostics(instructions, changeName, missing) {
155
170
  }
156
171
  export function printInstructionsText(instructions, unmetDependencies, flags = {}) {
157
172
  const { artifactId, changeName, schemaName, changeDir, resolvedOutputPath, description, instruction, context, rules, template, validation, dependencies, unlocks, changeMetadata, } = instructions;
158
- console.log(`<artifact id="${artifactId}" change="${changeName}" schema="${schemaName}">`);
173
+ // Every value below is neutralized before it is printed (prompt-escape.ts):
174
+ // config, metadata and schemas are repo-controlled, and an unescaped closing
175
+ // tag in any of them would end the block it sits in.
176
+ console.log(`<artifact id="${attributeValue(artifactId)}" change="${attributeValue(changeName)}" schema="${attributeValue(schemaName)}">`);
159
177
  console.log();
160
178
  if (flags.skipped) {
161
179
  console.log('<warning>');
162
- console.log(skippedArtifactMessage(artifactId, changeName));
163
- console.log(`Remove skip_specs from the change's .tospec.yaml before writing ${artifactId}.`);
180
+ console.log(inlineField(skippedArtifactMessage(artifactId, changeName)));
181
+ console.log(inlineField(`Remove skip_specs from the change's .tospec.yaml before writing ${artifactId}.`));
164
182
  console.log('</warning>');
165
183
  console.log();
166
184
  }
167
185
  if (unmetDependencies.length > 0) {
168
186
  console.log('<warning>');
169
187
  console.log('This artifact has unmet dependencies. Complete them first or proceed with caution.');
170
- console.log(`Missing: ${unmetDependencies.join(', ')}`);
188
+ console.log(`Missing: ${inlineField(unmetDependencies.join(', '))}`);
171
189
  console.log('</warning>');
172
190
  console.log();
173
191
  }
174
192
  console.log('<task>');
175
- console.log(`Create the ${artifactId} artifact for change "${changeName}".`);
176
- console.log(description);
193
+ console.log(inlineField(`Create the ${artifactId} artifact for change "${changeName}".`));
194
+ console.log(neutralizeTags(description));
177
195
  console.log('</task>');
178
196
  console.log();
179
197
  // What `tospec new change` recorded, same as the JSON payload's
@@ -183,12 +201,12 @@ export function printInstructionsText(instructions, unmetDependencies, flags = {
183
201
  if (changeMetadata.goal || decisions.length > 0) {
184
202
  console.log('<change_context>');
185
203
  if (changeMetadata.goal) {
186
- console.log(`Goal: ${changeMetadata.goal}`);
204
+ console.log(`Goal: ${inlineField(changeMetadata.goal)}`);
187
205
  }
188
206
  if (decisions.length > 0) {
189
207
  console.log('Decisions driving this change (read them under tospec/decisions/):');
190
208
  for (const file of decisions) {
191
- console.log(`- ${file}`);
209
+ console.log(`- ${inlineField(file)}`);
192
210
  }
193
211
  }
194
212
  console.log('</change_context>');
@@ -197,7 +215,7 @@ export function printInstructionsText(instructions, unmetDependencies, flags = {
197
215
  if (context) {
198
216
  console.log('<project_context>');
199
217
  console.log('<!-- This is background information for you. Do NOT include this in your output. -->');
200
- console.log(context);
218
+ console.log(neutralizeTags(context));
201
219
  console.log('</project_context>');
202
220
  console.log();
203
221
  }
@@ -205,39 +223,52 @@ export function printInstructionsText(instructions, unmetDependencies, flags = {
205
223
  console.log('<rules>');
206
224
  console.log('<!-- These are constraints for you to follow. Do NOT include this in your output. -->');
207
225
  for (const rule of rules) {
208
- console.log(`- ${rule}`);
226
+ console.log(`- ${inlineField(rule)}`);
209
227
  }
210
228
  console.log('</rules>');
211
229
  console.log();
212
230
  }
231
+ // A dependency met without a file stays in the block, labelled as `status`
232
+ // labels it, but outside the read instruction and without a path: listed
233
+ // there, a skipped `specs` sent the agent after a glob that matches nothing.
234
+ const toRead = dependencies.filter((dep) => !isMetWithoutFile(dep));
235
+ const metWithoutFile = dependencies.filter(isMetWithoutFile);
213
236
  if (dependencies.length > 0) {
214
237
  console.log('<dependencies>');
215
- console.log('Read the current contents of these files before creating this artifact (re-read them from disk even if you saw them earlier - they may have been edited since):');
216
- console.log();
217
- for (const dep of dependencies) {
218
- const status = dep.done ? 'done' : 'missing';
219
- const fullPath = path.join(changeDir, dep.path);
220
- console.log(`<dependency id="${dep.id}" status="${status}">`);
221
- console.log(` <path>${fullPath}</path>`);
222
- console.log(` <description>${dep.description}</description>`);
223
- console.log('</dependency>');
238
+ if (toRead.length > 0) {
239
+ console.log('Read the current contents of these files before creating this artifact (re-read them from disk even if you saw them earlier - they may have been edited since):');
240
+ console.log();
241
+ for (const dep of toRead) {
242
+ printDependency(dep, dep.done ? 'done' : 'missing', path.join(changeDir, dep.path));
243
+ }
244
+ }
245
+ if (metWithoutFile.length > 0) {
246
+ if (toRead.length > 0)
247
+ console.log();
248
+ console.log('Not written, and this change does not need them, so there is no file to read:');
249
+ console.log();
250
+ for (const dep of metWithoutFile) {
251
+ // `skipped` first, as `status` ranks it: the change's declaration is
252
+ // the stronger statement than the schema's "may be omitted".
253
+ printDependency(dep, dep.skipped ? 'skipped' : 'optional');
254
+ }
224
255
  }
225
256
  console.log('</dependencies>');
226
257
  console.log();
227
258
  }
228
259
  console.log('<output>');
229
- console.log(`Write to: ${resolvedOutputPath}`);
260
+ console.log(`Write to: ${inlineField(resolvedOutputPath)}`);
230
261
  console.log('</output>');
231
262
  console.log();
232
263
  if (instruction) {
233
264
  console.log('<instruction>');
234
- console.log(instruction.trim());
265
+ console.log(neutralizeTags(instruction.trim()));
235
266
  console.log('</instruction>');
236
267
  console.log();
237
268
  }
238
269
  console.log('<template>');
239
270
  console.log('<!-- Use this as the structure for your output file. Fill in the sections. -->');
240
- console.log(template.trim());
271
+ console.log(neutralizeTags(template.trim()));
241
272
  console.log('</template>');
242
273
  console.log();
243
274
  // The schema's own acceptance criteria, present only when it declares some:
@@ -245,25 +276,41 @@ export function printInstructionsText(instructions, unmetDependencies, flags = {
245
276
  if (validation) {
246
277
  console.log('<success_criteria>');
247
278
  if (validation.requiredSections.length > 0) {
248
- console.log(`Required sections: ${validation.requiredSections.map((s) => `## ${s}`).join(', ')}`);
279
+ console.log(`Required sections: ${inlineField(validation.requiredSections.map((s) => `## ${s}`).join(', '))}`);
249
280
  }
250
281
  for (const [section, minLength] of Object.entries(validation.minSectionLength)) {
251
- console.log(`Section "${section}" must be at least ${minLength} characters.`);
282
+ console.log(`Section "${inlineField(section)}" must be at least ${minLength} characters.`);
252
283
  }
253
284
  for (const [section, maxLength] of Object.entries(validation.maxSectionLength ?? {})) {
254
- console.log(`Section "${section}" should stay under ${maxLength} characters.`);
285
+ console.log(`Section "${inlineField(section)}" should stay under ${maxLength} characters.`);
255
286
  }
256
287
  console.log('</success_criteria>');
257
288
  console.log();
258
289
  }
259
290
  if (unlocks.length > 0) {
260
291
  console.log('<unlocks>');
261
- console.log(`Completing this artifact enables: ${unlocks.join(', ')}`);
292
+ console.log(`Completing this artifact enables: ${inlineField(unlocks.join(', '))}`);
262
293
  console.log('</unlocks>');
263
294
  console.log();
264
295
  }
265
296
  console.log('</artifact>');
266
297
  }
298
+ /**
299
+ * Declared away by the change, or optional and not written: the graph counts
300
+ * either as met, and neither has a file to read.
301
+ */
302
+ function isMetWithoutFile(dep) {
303
+ return !dep.done && (dep.skipped === true || dep.optional);
304
+ }
305
+ /** One `<dependency>` element; `fullPath` is omitted when there is no file to read. */
306
+ function printDependency(dep, status, fullPath) {
307
+ console.log(`<dependency id="${attributeValue(dep.id)}" status="${status}">`);
308
+ if (fullPath !== undefined) {
309
+ console.log(` <path>${inlineField(fullPath)}</path>`);
310
+ }
311
+ console.log(` <description>${inlineField(dep.description)}</description>`);
312
+ console.log('</dependency>');
313
+ }
267
314
  /**
268
315
  * The planning skill that produces a schema's artifacts, so blocked-state
269
316
  * instructions point at a skill that exists.
@@ -275,6 +322,18 @@ function planningSkillFor(schemaName) {
275
322
  return 'tospec-propose';
276
323
  return 'tospec-propose (sdd) or tospec-issue (issue)';
277
324
  }
325
+ /**
326
+ * The `apply_archive_would_refuse` fix for a refusal that carries
327
+ * `suggestedHeader`. Only a sister change's spelling needs the sentence on
328
+ * archive order: that refusal is the one a folded match once promised the
329
+ * sister's archive would settle. A main-spec name is settled already.
330
+ */
331
+ function headerCorrectionFix({ operation, source }, suggestedHeader) {
332
+ const edit = headerCorrectionClause(operation, suggestedHeader);
333
+ return source === 'sister-change'
334
+ ? `${edit}, the name the in-progress change that adds it uses. Archive matches requirement names exactly, so archiving that change first does not resolve it.`
335
+ : `${edit}, the name the main spec uses. Archive matches requirement names exactly.`;
336
+ }
278
337
  /**
279
338
  * How to name the tracked task file in a message to the user. `path.basename`
280
339
  * alone turns a glob (`tasks/*.md`) into a bare `*.md`, which names no file, so
@@ -286,13 +345,19 @@ function describeTracksFile(tracksFile) {
286
345
  /**
287
346
  * Shares `parseTasksFromContent` with task progress so the apply instructions
288
347
  * and the reported `x/y` can never disagree about what counts as a task.
348
+ * Exported so the backtracking test clock holds only the parse.
289
349
  */
290
- function parseTasksFile(content) {
350
+ export function parseTasksFile(content) {
291
351
  return parseTasksFromContent(content).map((task, i) => {
292
352
  // The number the file gives the task, not this array's index: they diverge
293
353
  // the moment a slice starts at 2.1 or a task is deleted, and the id is what
294
354
  // a caller quotes back. Falls back to the ordinal when there is none.
295
- const numbered = /^(\d+(?:\.\d+)*)\s+(.*)$/.exec(task.text);
355
+ //
356
+ // `[^\r\n]*` as in TASK_PATTERN, whose text can hold U+2028/U+2029: `.*`
357
+ // stopped there, so the id fell back to the ordinal, and `\s+` re-split the
358
+ // blanks before it, 2.6 s for `1.1` + 80k blanks + `a<U+2028>b`. Task text
359
+ // holds no CR or LF, so this always reaches its end.
360
+ const numbered = /^(\d+(?:\.\d+)*)\s+([^\r\n]*)$/.exec(task.text);
296
361
  return {
297
362
  id: numbered?.[1] ?? `${i + 1}`,
298
363
  // Without that number, since `id` carries it and a consumer joining the
@@ -308,11 +373,7 @@ function parseTasksFile(content) {
308
373
  */
309
374
  export async function generateApplyInstructions(projectRoot, changeName, schemaName, options = {}) {
310
375
  const planningHome = options.planningHome ?? resolveCurrentPlanningHomeSync({ startPath: projectRoot });
311
- // The schema comes from the change's metadata when not given.
312
- const context = loadChangeContext(projectRoot, changeName, schemaName, {
313
- changeDir: getChangeDir(planningHome, changeName),
314
- planningHome,
315
- });
376
+ const context = loadInstructionsContext(projectRoot, changeName, schemaName, planningHome);
316
377
  const changeDir = context.changeDir;
317
378
  const schema = resolveSchema(context.schemaName, projectRoot);
318
379
  const applyConfig = schema.apply;
@@ -320,13 +381,27 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
320
381
  const requiredArtifactIds = applyConfig?.requires ?? schema.artifacts.map((a) => a.id);
321
382
  const tracksFile = applyConfig?.tracks ?? null;
322
383
  const schemaInstruction = applyConfig?.instruction ?? null;
323
- const missingArtifacts = [];
384
+ // A required artifact still empty or holding its template blocks like an
385
+ // absent one: counted by existence alone, a copied tasks template reached
386
+ // `ready` and its placeholders were handed over as the work, while `status`
387
+ // called it a stub. Read from `context.stubs`, so the two cannot disagree.
388
+ // Kept apart from the absent ones only for the wording: a stub's file is on
389
+ // disk, and calling it "missing" sends the reader looking for a file that is
390
+ // right there.
391
+ const absentArtifacts = [];
392
+ const stubArtifacts = [];
324
393
  for (const artifactId of requiredArtifactIds) {
325
394
  const artifact = schema.artifacts.find((a) => a.id === artifactId);
326
- if (artifact && resolveArtifactOutputs(changeDir, artifact.generates).length === 0) {
327
- missingArtifacts.push(artifactId);
395
+ if (!artifact)
396
+ continue;
397
+ if (resolveArtifactOutputs(changeDir, artifact.generates).length === 0) {
398
+ absentArtifacts.push(artifactId);
399
+ }
400
+ else if (context.stubs?.has(artifactId)) {
401
+ stubArtifacts.push(artifactId);
328
402
  }
329
403
  }
404
+ const missingArtifacts = [...absentArtifacts, ...stubArtifacts];
330
405
  const contextFiles = {};
331
406
  for (const artifact of schema.artifacts) {
332
407
  const outputs = resolveArtifactOutputs(changeDir, artifact.generates);
@@ -353,9 +428,70 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
353
428
  let state;
354
429
  let instruction;
355
430
  const planningSkill = planningSkillFor(context.schemaName);
431
+ // Absent artifacts `apply.requires` does not insist on: reported, not
432
+ // blocking. sdd's apply requires only `tasks`, so a change missing
433
+ // proposal/design/specs reaches `ready` while its instruction still refers
434
+ // to them.
435
+ const missingContext = schema.artifacts
436
+ .filter((artifact) => !contextFiles[artifact.id] && !missingArtifacts.includes(artifact.id))
437
+ .map((artifact) => artifact.id);
438
+ // `specs` is the one absent artifact apply must not wave away: when the
439
+ // schema requires it and the change did not declare it away (skip_specs,
440
+ // scoped to that id like every other reader of the marker), validate and
441
+ // archive refuse the change. Told to ignore it, an agent finished the work
442
+ // and only learned that at archive time. Decided before `state`, because
443
+ // `all_done` must not call such a change ready to archive.
444
+ const specsArtifact = schema.artifacts.find((artifact) => artifact.id === 'specs');
445
+ const specsRequiredButMissing = specsArtifact !== undefined &&
446
+ !specsArtifact.optional &&
447
+ missingContext.includes('specs') &&
448
+ !(context.skippedArtifacts?.has('specs') ?? false);
449
+ // The opposite fault: skip_specs: true beside spec files. validate refuses
450
+ // it, and so archive, yet only validate reported it, at apply's step 6, once
451
+ // the work was done. Read through validate's own helper and under any
452
+ // schema, as validate reads it, so the two cannot disagree about a change.
453
+ const skipSpecsMarker = readSkipSpecsMarker(changeDir);
454
+ const contradictingSpecs = await findSkipSpecsContradictions(changeDir, skipSpecsMarker);
455
+ // Spec files validate refuses for where they sit (nested, a dot folder, a
456
+ // misnamed file, specs/spec.md). Neither warning above names them: under
457
+ // skip_specs they contradict nothing, so apply said ready, while the
458
+ // "Not present in this change: specs" line told the agent to ignore them,
459
+ // and validate refused the change only at step 6.
460
+ const misplacedSpecs = await findMisplacedSpecFiles(path.join(changeDir, 'specs'), {
461
+ requireDelta: await changeRequiresDeltas(projectRoot, changeDir, { marker: skipSpecsMarker, contradictingSpecs }),
462
+ });
463
+ // Deltas archive's merge would refuse (a MODIFIED, REMOVED or RENAMED on a capability
464
+ // with no main spec, an ADDED that already exists). validate reported them
465
+ // and apply said ready, so the work was done before archive refused it.
466
+ // Through validate's own dry run, not a second list of preconditions here,
467
+ // which would be free to drift from archive's.
468
+ const archiveRefusals = fs.existsSync(path.join(changeDir, 'specs'))
469
+ ? await new Validator().findArchiveRefusals(changeDir, path.join(projectRoot, 'tospec', 'specs'))
470
+ : [];
471
+ // What keeps an all-done change from archiving. apply_specs_missing and
472
+ // apply_skip_specs_contradicted cannot both hold, as a declared marker drops
473
+ // the first; a misplaced file can sit beside either, and is named after
474
+ // them. A refused delta is named last: the faults above decide which files
475
+ // archive merges at all.
476
+ const archiveBlocker = specsRequiredButMissing
477
+ ? 'it has no delta specs (see the apply_specs_missing warning)'
478
+ : contradictingSpecs.length > 0
479
+ ? 'it sets skip_specs: true beside delta spec files (see the apply_skip_specs_contradicted warning)'
480
+ : misplacedSpecs.length > 0
481
+ ? 'it has spec files validate refuses for where they sit (see the apply_specs_misplaced warning)'
482
+ : archiveRefusals.length > 0
483
+ ? 'archive would refuse its delta specs (see the apply_archive_would_refuse warning)'
484
+ : undefined;
356
485
  if (missingArtifacts.length > 0) {
357
486
  state = 'blocked';
358
- instruction = `Cannot apply this change yet. Missing artifacts: ${missingArtifacts.join(', ')}.\nStop and send the user back to ${planningSkill} to create the missing artifacts first.`;
487
+ const reasons = [
488
+ ...(absentArtifacts.length > 0 ? [`Missing artifacts: ${absentArtifacts.join(', ')}.`] : []),
489
+ // "empty or": a stub is either, and an empty file called a template sends
490
+ // the reader comparing it against one.
491
+ ...(stubArtifacts.length > 0 ? [`Still empty or the unfilled template: ${stubArtifacts.join(', ')}.`] : []),
492
+ ];
493
+ const action = stubArtifacts.length === 0 ? 'create the missing artifacts' : absentArtifacts.length === 0 ? 'fill them in' : 'create and fill them in';
494
+ instruction = `Cannot apply this change yet. ${reasons.join(' ')}\nStop and send the user back to ${planningSkill} to ${action} first.`;
359
495
  }
360
496
  else if (tracksFile && !tracksFileExists) {
361
497
  const tracksFilename = describeTracksFile(tracksFile);
@@ -369,7 +505,9 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
369
505
  }
370
506
  else if (tracksFile && remaining === 0 && total > 0) {
371
507
  state = 'all_done';
372
- instruction = 'All tasks are complete! This change is ready to be archived.\nConsider running tests and reviewing the changes before archiving.';
508
+ instruction = archiveBlocker
509
+ ? `All tasks are complete, but this change cannot be archived yet: ${archiveBlocker}.\nResolve that warning before archiving.`
510
+ : 'All tasks are complete! This change is ready to be archived.\nConsider running tests and reviewing the changes before archiving.';
373
511
  }
374
512
  else if (!tracksFile) {
375
513
  state = 'ready';
@@ -379,17 +517,73 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
379
517
  state = 'ready';
380
518
  instruction = schemaInstruction?.trim() ?? 'Read context files, work through pending tasks, mark complete as you go.\nPause if you hit blockers or need clarification.';
381
519
  }
382
- // Absent artifacts `apply.requires` does not insist on: reported, not
383
- // blocking. sdd's apply requires only `tasks`, so a change missing
384
- // proposal/design/specs reaches `ready` while its instruction still refers
385
- // to them.
386
- const missingContext = schema.artifacts
387
- .filter((artifact) => !contextFiles[artifact.id] && !missingArtifacts.includes(artifact.id))
388
- .map((artifact) => artifact.id);
389
- if (missingContext.length > 0 && state !== 'blocked') {
520
+ // Reported even when blocked: an unmatched `apply.tracks` is what produces a
521
+ // "file is missing" block for a file that exists, so the block needs it.
522
+ const warnings = findSchemaWarnings(schema);
523
+ if (specsRequiredButMissing && state !== 'blocked') {
524
+ // A mistyped value (`skip_specs: "yes"`) is a declaration that failed, not
525
+ // an absent one: saying "does not declare" sent the user looking for a key
526
+ // they had written.
527
+ const marker = context.skipSpecsInvalidReason
528
+ ? `its skip_specs value is invalid (${context.skipSpecsInvalidReason.replace(/\.$/, '')})`
529
+ : 'does not set skip_specs: true';
530
+ warnings.push({
531
+ code: 'apply_specs_missing',
532
+ message: `Change '${changeName}' has no delta specs and ${marker}, so validate and archive will refuse it once the work is done.`,
533
+ fix: `Write the delta specs with ${planningSkill} before implementing, or set \`skip_specs: true\` in the change's .tospec.yaml only when the user confirms it needs no delta spec.`,
534
+ });
535
+ }
536
+ // Reported even when blocked, unlike apply_specs_missing: the planning skill
537
+ // a block sends the user to does not settle it. Only the user can say
538
+ // whether the flag or the spec files are wrong.
539
+ if (contradictingSpecs.length > 0) {
540
+ warnings.push({
541
+ code: 'apply_skip_specs_contradicted',
542
+ message: `Change '${changeName}' sets skip_specs: true but has ${contradictingSpecs.length} spec file(s) (${contradictingSpecs.join(', ')}), so validate and archive will refuse it once the work is done.`,
543
+ fix: `Remove skip_specs from the change's .tospec.yaml when the user wants these delta specs; delete the spec files only when the user confirms this change needs no delta spec.`,
544
+ });
545
+ }
546
+ // Reported even when blocked, like the contradiction: the planning skill a
547
+ // block sends the user to does not move files.
548
+ if (misplacedSpecs.length > 0) {
549
+ const listed = misplacedSpecs.map((file) => `specs/${file}`).join(', ');
550
+ warnings.push({
551
+ code: 'apply_specs_misplaced',
552
+ message: `Change '${changeName}' has ${misplacedSpecs.length} spec file(s) that archive does not merge where they sit (${listed}), so validate and archive will refuse it once the work is done.`,
553
+ fix: `Move each file to specs/<capability>/spec.md; \`tospec validate ${changeName}\` names the exact path for each.`,
554
+ });
555
+ }
556
+ // Reported even when blocked, like the two above: the planning skill a block
557
+ // sends the user to writes deltas against the main specs it reads, and when
558
+ // the cause is a sister change still unarchived, no edit there settles it.
559
+ // validate's text verbatim, so its "Archive '<change>' first" hint survives.
560
+ // `blockedBy` is validate's own field, carried as is, so an agent reads the
561
+ // order from one place in both commands instead of from the message.
562
+ // `suggestedHeader` likewise: the refusal's text is the merge's and does not
563
+ // name the spelling the sister change uses, so the fix has to. For a near
564
+ // miss of the main spec the merge's text does name it, but the field is what
565
+ // an agent reads, so the fix names it the same way.
566
+ for (const { issue: refusal, headerCorrection } of archiveRefusals) {
567
+ const blockedBy = refusal.blockedBy ?? [];
568
+ const { suggestedHeader } = refusal;
569
+ const validateNote = `\`tospec validate ${changeName}\` reports the same finding.`;
570
+ warnings.push({
571
+ code: 'apply_archive_would_refuse',
572
+ message: `Change '${changeName}', specs/${refusal.path}: ${refusal.message}`,
573
+ fix: blockedBy.length > 0
574
+ ? `Archive ${blockedBy.map((name) => `'${name}'`).join(', ')} first, then run apply again; no edit to this delta resolves it. ${validateNote}`
575
+ : suggestedHeader !== undefined && headerCorrection !== undefined
576
+ ? `Correct the delta with ${planningSkill} before implementing: ${headerCorrectionFix(headerCorrection, suggestedHeader)} ${validateNote}`
577
+ : `Correct the delta with ${planningSkill} before implementing. ${validateNote}`,
578
+ ...(blockedBy.length > 0 ? { blockedBy } : {}),
579
+ ...(suggestedHeader === undefined ? {} : { suggestedHeader }),
580
+ });
581
+ }
582
+ const ignorable = specsRequiredButMissing ? missingContext.filter((id) => id !== 'specs') : missingContext;
583
+ if (ignorable.length > 0 && state !== 'blocked') {
390
584
  instruction += `
391
585
 
392
- Not present in this change: ${missingContext.join(', ')}. Ignore any part of the above that refers to them.`;
586
+ Not present in this change: ${ignorable.join(', ')}. Ignore any part of the above that refers to them.`;
393
587
  }
394
588
  // The project's own context and rules, as every artifact's instructions carry.
395
589
  const { config: projectConfig, diagnostics: configWarnings } = readProjectConfigWithDiagnostics(projectRoot);
@@ -409,6 +603,7 @@ Not present in this change: ${missingContext.join(', ')}. Ignore any part of the
409
603
  ...(configContext ? { context: configContext } : {}),
410
604
  ...(applyRules?.length ? { rules: applyRules } : {}),
411
605
  ...(configWarnings.length ? { configWarnings } : {}),
606
+ ...(warnings.length ? { warnings } : {}),
412
607
  };
413
608
  }
414
609
  export async function applyInstructionsCommand(options) {
@@ -420,43 +615,58 @@ export async function applyInstructionsCommand(options) {
420
615
  const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
421
616
  planningHome,
422
617
  });
618
+ // Printed in both modes, as `instructions <artifact>` does: the human text
619
+ // otherwise never said that the context or rules it shows were cut short.
620
+ for (const warning of instructions.configWarnings ?? []) {
621
+ printProjectConfigDiagnostic(warning.message);
622
+ }
423
623
  if (options.json) {
424
624
  // As in the artifact payload: `configWarnings` becomes the envelope's
425
625
  // `status[]`, not a field of its own.
426
- const { configWarnings, ...payload } = instructions;
626
+ const { configWarnings, warnings, ...payload } = instructions;
627
+ // The change's own warning first: it is the one that decides whether the
628
+ // work about to start can be archived at all.
629
+ const status = [
630
+ ...(warnings ?? []).map(({ code, message, fix, blockedBy, suggestedHeader }) => ({
631
+ severity: 'warning',
632
+ code,
633
+ message,
634
+ ...(fix ? { fix } : {}),
635
+ ...(blockedBy ? { blockedBy } : {}),
636
+ ...(suggestedHeader ? { suggestedHeader } : {}),
637
+ })),
638
+ ...(configWarnings ?? []).map(({ code, message }) => ({
639
+ severity: 'warning',
640
+ code,
641
+ message,
642
+ })),
643
+ ];
427
644
  emitSuccess({
428
645
  ...payload,
429
- ...(configWarnings?.length
430
- ? {
431
- status: configWarnings.map(({ code, message }) => ({
432
- severity: 'warning',
433
- code,
434
- message,
435
- })),
436
- }
437
- : {}),
646
+ ...(status.length ? { status } : {}),
438
647
  }, toRootOutput(root));
439
648
  return;
440
649
  }
441
650
  printApplyInstructionsText(instructions);
442
651
  }
443
652
  export function printApplyInstructionsText(instructions) {
444
- const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction, context, rules, } = instructions;
445
- console.log(`## Apply: ${changeName}`);
446
- console.log(`Schema: ${schemaName}`);
653
+ const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction, context, rules, warnings, } = instructions;
654
+ console.log(`## Apply: ${inlineField(changeName)}`);
655
+ console.log(`Schema: ${inlineField(schemaName)}`);
447
656
  console.log();
448
657
  // The same tags the artifact instructions use, so an agent reading either
449
- // treats project constraints the same way.
658
+ // treats project constraints the same way — and the same escaping, since the
659
+ // same repo-controlled text fills them.
450
660
  if (context) {
451
661
  console.log('<project_context>');
452
- console.log(context);
662
+ console.log(neutralizeTags(context));
453
663
  console.log('</project_context>');
454
664
  console.log();
455
665
  }
456
666
  if (rules && rules.length > 0) {
457
667
  console.log('<rules>');
458
668
  for (const rule of rules) {
459
- console.log(`- ${rule}`);
669
+ console.log(`- ${inlineField(rule)}`);
460
670
  }
461
671
  console.log('</rules>');
462
672
  console.log();
@@ -464,8 +674,10 @@ export function printApplyInstructionsText(instructions) {
464
674
  if (state === 'blocked' && missingArtifacts) {
465
675
  console.log('### ⚠️ Blocked');
466
676
  console.log();
467
- console.log(`Missing artifacts: ${missingArtifacts.join(', ')}`);
468
- console.log(`Use the ${planningSkillFor(schemaName)} skill to create these first.`);
677
+ // Archive's wording for the same list: it holds unfilled templates too, and
678
+ // the instruction below says which is which.
679
+ console.log(`Missing or incomplete artifacts: ${missingArtifacts.map(inlineField).join(', ')}`);
680
+ console.log(`Use the ${planningSkillFor(schemaName)} skill to complete these first.`);
469
681
  console.log();
470
682
  }
471
683
  const contextFileEntries = Object.entries(contextFiles);
@@ -473,7 +685,7 @@ export function printApplyInstructionsText(instructions) {
473
685
  console.log('### Context Files');
474
686
  for (const [artifactId, filePaths] of contextFileEntries) {
475
687
  for (const filePath of filePaths) {
476
- console.log(`- ${artifactId}: ${filePath}`);
688
+ console.log(`- ${inlineField(artifactId)}: ${inlineField(filePath)}`);
477
689
  }
478
690
  }
479
691
  console.log();
@@ -494,11 +706,17 @@ export function printApplyInstructionsText(instructions) {
494
706
  const checkbox = task.done ? '[x]' : '[ ]';
495
707
  // Joined again here: the number is stored apart only so a caller can
496
708
  // address a task by it.
497
- console.log(`- ${checkbox} ${task.id} ${task.description}`);
709
+ console.log(`- ${checkbox} ${task.id} ${inlineField(task.description)}`);
498
710
  }
499
711
  console.log();
500
712
  }
713
+ for (const warning of warnings ?? []) {
714
+ console.log(`Warning: ${inlineField(warning.message)}`);
715
+ if (warning.fix)
716
+ console.log(`Fix: ${inlineField(warning.fix)}`);
717
+ console.log();
718
+ }
501
719
  console.log('### Instruction');
502
- console.log(instruction);
720
+ console.log(neutralizeTags(instruction));
503
721
  }
504
722
  //# sourceMappingURL=instructions.js.map