@specship/specship-win32-arm64 0.24.0 → 0.28.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 (341) hide show
  1. package/bin/specship.cmd +31 -1
  2. package/lib/dist/activation/cheatsheet.d.ts +17 -1
  3. package/lib/dist/activation/cheatsheet.d.ts.map +1 -1
  4. package/lib/dist/activation/cheatsheet.js +47 -3
  5. package/lib/dist/activation/cheatsheet.js.map +1 -1
  6. package/lib/dist/activation/hook-output.d.ts +80 -0
  7. package/lib/dist/activation/hook-output.d.ts.map +1 -0
  8. package/lib/dist/activation/hook-output.js +103 -0
  9. package/lib/dist/activation/hook-output.js.map +1 -0
  10. package/lib/dist/activation/search-intercept.d.ts +60 -29
  11. package/lib/dist/activation/search-intercept.d.ts.map +1 -1
  12. package/lib/dist/activation/search-intercept.js +159 -55
  13. package/lib/dist/activation/search-intercept.js.map +1 -1
  14. package/lib/dist/activation/steering.d.ts +11 -13
  15. package/lib/dist/activation/steering.d.ts.map +1 -1
  16. package/lib/dist/activation/steering.js +25 -10
  17. package/lib/dist/activation/steering.js.map +1 -1
  18. package/lib/dist/behaviour/behaviour-surface.d.ts +47 -0
  19. package/lib/dist/behaviour/behaviour-surface.d.ts.map +1 -1
  20. package/lib/dist/behaviour/behaviour-surface.js +108 -2
  21. package/lib/dist/behaviour/behaviour-surface.js.map +1 -1
  22. package/lib/dist/bin/node-version-check.d.ts +22 -7
  23. package/lib/dist/bin/node-version-check.d.ts.map +1 -1
  24. package/lib/dist/bin/node-version-check.js +39 -9
  25. package/lib/dist/bin/node-version-check.js.map +1 -1
  26. package/lib/dist/bin/specship.js +989 -116
  27. package/lib/dist/bin/specship.js.map +1 -1
  28. package/lib/dist/commands/specship/learn.md +27 -3
  29. package/lib/dist/commands/specship/spec.md +85 -15
  30. package/lib/dist/db/spec-queries.d.ts +73 -3
  31. package/lib/dist/db/spec-queries.d.ts.map +1 -1
  32. package/lib/dist/db/spec-queries.js +139 -5
  33. package/lib/dist/db/spec-queries.js.map +1 -1
  34. package/lib/dist/db/sqlite-adapter.d.ts +7 -5
  35. package/lib/dist/db/sqlite-adapter.d.ts.map +1 -1
  36. package/lib/dist/db/sqlite-adapter.js +11 -9
  37. package/lib/dist/db/sqlite-adapter.js.map +1 -1
  38. package/lib/dist/enforce/changed-files.d.ts +39 -0
  39. package/lib/dist/enforce/changed-files.d.ts.map +1 -0
  40. package/lib/dist/enforce/changed-files.js +114 -0
  41. package/lib/dist/enforce/changed-files.js.map +1 -0
  42. package/lib/dist/enforce/enforce.d.ts +53 -2
  43. package/lib/dist/enforce/enforce.d.ts.map +1 -1
  44. package/lib/dist/enforce/enforce.js +95 -8
  45. package/lib/dist/enforce/enforce.js.map +1 -1
  46. package/lib/dist/enforce/mutation.d.ts +46 -0
  47. package/lib/dist/enforce/mutation.d.ts.map +1 -0
  48. package/lib/dist/enforce/mutation.js +104 -0
  49. package/lib/dist/enforce/mutation.js.map +1 -0
  50. package/lib/dist/enforce/suppressions.d.ts +33 -0
  51. package/lib/dist/enforce/suppressions.d.ts.map +1 -0
  52. package/lib/dist/enforce/suppressions.js +159 -0
  53. package/lib/dist/enforce/suppressions.js.map +1 -0
  54. package/lib/dist/extraction/grammars.d.ts +10 -0
  55. package/lib/dist/extraction/grammars.d.ts.map +1 -1
  56. package/lib/dist/extraction/grammars.js +32 -3
  57. package/lib/dist/extraction/grammars.js.map +1 -1
  58. package/lib/dist/extraction/index.d.ts +7 -0
  59. package/lib/dist/extraction/index.d.ts.map +1 -1
  60. package/lib/dist/extraction/index.js.map +1 -1
  61. package/lib/dist/extraction/specs/markdown-spec-extractor.d.ts +7 -0
  62. package/lib/dist/extraction/specs/markdown-spec-extractor.d.ts.map +1 -1
  63. package/lib/dist/extraction/specs/markdown-spec-extractor.js +97 -10
  64. package/lib/dist/extraction/specs/markdown-spec-extractor.js.map +1 -1
  65. package/lib/dist/extraction/wasm/tree-sitter-pascal.wasm.gz +0 -0
  66. package/lib/dist/extraction/wasm/tree-sitter-scala.wasm.gz +0 -0
  67. package/lib/dist/graph/spec-coverage.d.ts +142 -0
  68. package/lib/dist/graph/spec-coverage.d.ts.map +1 -0
  69. package/lib/dist/graph/spec-coverage.js +276 -0
  70. package/lib/dist/graph/spec-coverage.js.map +1 -0
  71. package/lib/dist/health/smoke-check.js +2 -2
  72. package/lib/dist/health/smoke-check.js.map +1 -1
  73. package/lib/dist/index.d.ts +75 -7
  74. package/lib/dist/index.d.ts.map +1 -1
  75. package/lib/dist/index.js +191 -23
  76. package/lib/dist/index.js.map +1 -1
  77. package/lib/dist/installer/gemini-commands.d.ts +107 -0
  78. package/lib/dist/installer/gemini-commands.d.ts.map +1 -0
  79. package/lib/dist/installer/gemini-commands.js +264 -0
  80. package/lib/dist/installer/gemini-commands.js.map +1 -0
  81. package/lib/dist/installer/index.d.ts +55 -8
  82. package/lib/dist/installer/index.d.ts.map +1 -1
  83. package/lib/dist/installer/index.js +72 -53
  84. package/lib/dist/installer/index.js.map +1 -1
  85. package/lib/dist/installer/init-offer.d.ts +14 -9
  86. package/lib/dist/installer/init-offer.d.ts.map +1 -1
  87. package/lib/dist/installer/init-offer.js +13 -9
  88. package/lib/dist/installer/init-offer.js.map +1 -1
  89. package/lib/dist/installer/install-defaults.d.ts +81 -0
  90. package/lib/dist/installer/install-defaults.d.ts.map +1 -0
  91. package/lib/dist/installer/install-defaults.js +95 -0
  92. package/lib/dist/installer/install-defaults.js.map +1 -0
  93. package/lib/dist/installer/instructions-template.d.ts +22 -0
  94. package/lib/dist/installer/instructions-template.d.ts.map +1 -1
  95. package/lib/dist/installer/instructions-template.js +37 -1
  96. package/lib/dist/installer/instructions-template.js.map +1 -1
  97. package/lib/dist/installer/targets/claude.d.ts +18 -0
  98. package/lib/dist/installer/targets/claude.d.ts.map +1 -1
  99. package/lib/dist/installer/targets/claude.js +70 -23
  100. package/lib/dist/installer/targets/claude.js.map +1 -1
  101. package/lib/dist/installer/targets/gemini.d.ts +85 -12
  102. package/lib/dist/installer/targets/gemini.d.ts.map +1 -1
  103. package/lib/dist/installer/targets/gemini.js +527 -30
  104. package/lib/dist/installer/targets/gemini.js.map +1 -1
  105. package/lib/dist/installer/targets/shared.d.ts +13 -0
  106. package/lib/dist/installer/targets/shared.d.ts.map +1 -1
  107. package/lib/dist/installer/targets/shared.js +22 -4
  108. package/lib/dist/installer/targets/shared.js.map +1 -1
  109. package/lib/dist/installer/targets/types.d.ts +12 -0
  110. package/lib/dist/installer/targets/types.d.ts.map +1 -1
  111. package/lib/dist/jira/auto-publish.d.ts +71 -4
  112. package/lib/dist/jira/auto-publish.d.ts.map +1 -1
  113. package/lib/dist/jira/auto-publish.js +186 -17
  114. package/lib/dist/jira/auto-publish.js.map +1 -1
  115. package/lib/dist/jira/reconcile.d.ts +24 -1
  116. package/lib/dist/jira/reconcile.d.ts.map +1 -1
  117. package/lib/dist/jira/reconcile.js +46 -2
  118. package/lib/dist/jira/reconcile.js.map +1 -1
  119. package/lib/dist/jira/repo-config.d.ts +9 -0
  120. package/lib/dist/jira/repo-config.d.ts.map +1 -1
  121. package/lib/dist/jira/repo-config.js +11 -0
  122. package/lib/dist/jira/repo-config.js.map +1 -1
  123. package/lib/dist/jira/types.d.ts +7 -0
  124. package/lib/dist/jira/types.d.ts.map +1 -1
  125. package/lib/dist/jira/types.js.map +1 -1
  126. package/lib/dist/knowledge/enforce.d.ts +75 -0
  127. package/lib/dist/knowledge/enforce.d.ts.map +1 -0
  128. package/lib/dist/knowledge/enforce.js +250 -0
  129. package/lib/dist/knowledge/enforce.js.map +1 -0
  130. package/lib/dist/knowledge/facts.d.ts +33 -0
  131. package/lib/dist/knowledge/facts.d.ts.map +1 -0
  132. package/lib/dist/knowledge/facts.js +117 -0
  133. package/lib/dist/knowledge/facts.js.map +1 -0
  134. package/lib/dist/knowledge/proposals.d.ts +88 -0
  135. package/lib/dist/knowledge/proposals.d.ts.map +1 -0
  136. package/lib/dist/knowledge/proposals.js +360 -0
  137. package/lib/dist/knowledge/proposals.js.map +1 -0
  138. package/lib/dist/knowledge/stale.d.ts +64 -0
  139. package/lib/dist/knowledge/stale.d.ts.map +1 -0
  140. package/lib/dist/knowledge/stale.js +174 -0
  141. package/lib/dist/knowledge/stale.js.map +1 -0
  142. package/lib/dist/mcp/fitness-tool.d.ts +5 -6
  143. package/lib/dist/mcp/fitness-tool.d.ts.map +1 -1
  144. package/lib/dist/mcp/fitness-tool.js +0 -16
  145. package/lib/dist/mcp/fitness-tool.js.map +1 -1
  146. package/lib/dist/mcp/gemini-session.d.ts +50 -0
  147. package/lib/dist/mcp/gemini-session.d.ts.map +1 -0
  148. package/lib/dist/mcp/gemini-session.js +159 -0
  149. package/lib/dist/mcp/gemini-session.js.map +1 -0
  150. package/lib/dist/mcp/health-tool.d.ts +26 -0
  151. package/lib/dist/mcp/health-tool.d.ts.map +1 -0
  152. package/lib/dist/mcp/health-tool.js +50 -0
  153. package/lib/dist/mcp/health-tool.js.map +1 -0
  154. package/lib/dist/mcp/jira-tools.d.ts.map +1 -1
  155. package/lib/dist/mcp/jira-tools.js +25 -0
  156. package/lib/dist/mcp/jira-tools.js.map +1 -1
  157. package/lib/dist/mcp/maintainability-tool.d.ts +5 -7
  158. package/lib/dist/mcp/maintainability-tool.d.ts.map +1 -1
  159. package/lib/dist/mcp/maintainability-tool.js +0 -16
  160. package/lib/dist/mcp/maintainability-tool.js.map +1 -1
  161. package/lib/dist/mcp/model-context.d.ts +31 -7
  162. package/lib/dist/mcp/model-context.d.ts.map +1 -1
  163. package/lib/dist/mcp/model-context.js +70 -21
  164. package/lib/dist/mcp/model-context.js.map +1 -1
  165. package/lib/dist/mcp/proxy.d.ts.map +1 -1
  166. package/lib/dist/mcp/proxy.js +5 -2
  167. package/lib/dist/mcp/proxy.js.map +1 -1
  168. package/lib/dist/mcp/server-instructions.d.ts +16 -15
  169. package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
  170. package/lib/dist/mcp/server-instructions.js +72 -5
  171. package/lib/dist/mcp/server-instructions.js.map +1 -1
  172. package/lib/dist/mcp/session.js +1 -1
  173. package/lib/dist/mcp/session.js.map +1 -1
  174. package/lib/dist/mcp/spec-tools.d.ts +9 -1
  175. package/lib/dist/mcp/spec-tools.d.ts.map +1 -1
  176. package/lib/dist/mcp/spec-tools.js +282 -49
  177. package/lib/dist/mcp/spec-tools.js.map +1 -1
  178. package/lib/dist/mcp/tools.d.ts +49 -9
  179. package/lib/dist/mcp/tools.d.ts.map +1 -1
  180. package/lib/dist/mcp/tools.js +210 -58
  181. package/lib/dist/mcp/tools.js.map +1 -1
  182. package/lib/dist/pulse/pulse.d.ts +93 -0
  183. package/lib/dist/pulse/pulse.d.ts.map +1 -0
  184. package/lib/dist/pulse/pulse.js +401 -0
  185. package/lib/dist/pulse/pulse.js.map +1 -0
  186. package/lib/dist/reflect/apply.d.ts.map +1 -1
  187. package/lib/dist/reflect/apply.js +38 -3
  188. package/lib/dist/reflect/apply.js.map +1 -1
  189. package/lib/dist/reflect/auto-sweep.d.ts +26 -0
  190. package/lib/dist/reflect/auto-sweep.d.ts.map +1 -0
  191. package/lib/dist/reflect/auto-sweep.js +90 -0
  192. package/lib/dist/reflect/auto-sweep.js.map +1 -0
  193. package/lib/dist/reflect/cli.d.ts +62 -0
  194. package/lib/dist/reflect/cli.d.ts.map +1 -0
  195. package/lib/dist/reflect/cli.js +163 -0
  196. package/lib/dist/reflect/cli.js.map +1 -0
  197. package/lib/dist/reflect/miner.d.ts.map +1 -1
  198. package/lib/dist/reflect/miner.js +119 -0
  199. package/lib/dist/reflect/miner.js.map +1 -1
  200. package/lib/dist/reflect/sweep.d.ts +1 -0
  201. package/lib/dist/reflect/sweep.d.ts.map +1 -1
  202. package/lib/dist/reflect/sweep.js +62 -13
  203. package/lib/dist/reflect/sweep.js.map +1 -1
  204. package/lib/dist/reflect/targets.d.ts +2 -0
  205. package/lib/dist/reflect/targets.d.ts.map +1 -1
  206. package/lib/dist/reflect/targets.js +33 -0
  207. package/lib/dist/reflect/targets.js.map +1 -1
  208. package/lib/dist/reflect/types.d.ts +19 -2
  209. package/lib/dist/reflect/types.d.ts.map +1 -1
  210. package/lib/dist/resolution/spec-link-resolver.d.ts +44 -2
  211. package/lib/dist/resolution/spec-link-resolver.d.ts.map +1 -1
  212. package/lib/dist/resolution/spec-link-resolver.js +132 -0
  213. package/lib/dist/resolution/spec-link-resolver.js.map +1 -1
  214. package/lib/dist/resolution/test-title-links.d.ts +62 -0
  215. package/lib/dist/resolution/test-title-links.d.ts.map +1 -0
  216. package/lib/dist/resolution/test-title-links.js +97 -0
  217. package/lib/dist/resolution/test-title-links.js.map +1 -0
  218. package/lib/dist/search/query-parser.d.ts.map +1 -1
  219. package/lib/dist/search/query-parser.js +6 -1
  220. package/lib/dist/search/query-parser.js.map +1 -1
  221. package/lib/dist/server/project-registry.js +1 -1
  222. package/lib/dist/server/routes/claude.js +24 -43
  223. package/lib/dist/server/routes/graph.js +53 -7
  224. package/lib/dist/server/routes/knowledge.js +118 -0
  225. package/lib/dist/server/routes/mcp.js +4 -3
  226. package/lib/dist/server/routes/projects.js +4 -4
  227. package/lib/dist/server/routes/spec.js +49 -7
  228. package/lib/dist/server/server.js +31 -17
  229. package/lib/dist/skills/spec-author/SKILL.md +164 -0
  230. package/lib/dist/skills/spec-author/references/format.md +210 -0
  231. package/lib/dist/skills/spec-author/references/gap-questions.md +152 -0
  232. package/lib/dist/skills/spec-author/references/quality-rubric.md +125 -0
  233. package/lib/dist/skills/spec-author/references/review-checklist.md +175 -0
  234. package/lib/dist/spec-lint/lint.d.ts +82 -0
  235. package/lib/dist/spec-lint/lint.d.ts.map +1 -0
  236. package/lib/dist/spec-lint/lint.js +491 -0
  237. package/lib/dist/spec-lint/lint.js.map +1 -0
  238. package/lib/dist/spec-quality/criterion-ids.d.ts +13 -0
  239. package/lib/dist/spec-quality/criterion-ids.d.ts.map +1 -0
  240. package/lib/dist/spec-quality/criterion-ids.js +38 -0
  241. package/lib/dist/spec-quality/criterion-ids.js.map +1 -0
  242. package/lib/dist/spec-quality/evidence-kind.d.ts +54 -0
  243. package/lib/dist/spec-quality/evidence-kind.d.ts.map +1 -0
  244. package/lib/dist/spec-quality/evidence-kind.js +183 -0
  245. package/lib/dist/spec-quality/evidence-kind.js.map +1 -0
  246. package/lib/dist/spec-quality/link-persist.d.ts +65 -0
  247. package/lib/dist/spec-quality/link-persist.d.ts.map +1 -0
  248. package/lib/dist/spec-quality/link-persist.js +249 -0
  249. package/lib/dist/spec-quality/link-persist.js.map +1 -0
  250. package/lib/dist/spec-quality/plan-scope.d.ts +38 -0
  251. package/lib/dist/spec-quality/plan-scope.d.ts.map +1 -0
  252. package/lib/dist/spec-quality/plan-scope.js +129 -0
  253. package/lib/dist/spec-quality/plan-scope.js.map +1 -0
  254. package/lib/dist/spec-quality/postcheck.d.ts +85 -0
  255. package/lib/dist/spec-quality/postcheck.d.ts.map +1 -0
  256. package/lib/dist/spec-quality/postcheck.js +390 -0
  257. package/lib/dist/spec-quality/postcheck.js.map +1 -0
  258. package/lib/dist/spec-quality/readiness.d.ts +72 -0
  259. package/lib/dist/spec-quality/readiness.d.ts.map +1 -0
  260. package/lib/dist/spec-quality/readiness.js +226 -0
  261. package/lib/dist/spec-quality/readiness.js.map +1 -0
  262. package/lib/dist/statusline/index.js.map +1 -1
  263. package/lib/dist/statusline/render.d.ts +1 -1
  264. package/lib/dist/statusline/render.d.ts.map +1 -1
  265. package/lib/dist/statusline/render.js +3 -1
  266. package/lib/dist/statusline/render.js.map +1 -1
  267. package/lib/dist/ui/assets/index-BzuUWUAE.js +50 -0
  268. package/lib/dist/ui/index.html +1 -1
  269. package/lib/dist/verify/report-ingest.d.ts +88 -0
  270. package/lib/dist/verify/report-ingest.d.ts.map +1 -0
  271. package/lib/dist/verify/report-ingest.js +269 -0
  272. package/lib/dist/verify/report-ingest.js.map +1 -0
  273. package/lib/dist/workflows/agents/claude.d.ts +16 -0
  274. package/lib/dist/workflows/agents/claude.d.ts.map +1 -0
  275. package/lib/dist/workflows/agents/claude.js +82 -0
  276. package/lib/dist/workflows/agents/claude.js.map +1 -0
  277. package/lib/dist/workflows/agents/gemini.d.ts +21 -0
  278. package/lib/dist/workflows/agents/gemini.d.ts.map +1 -0
  279. package/lib/dist/workflows/agents/gemini.js +124 -0
  280. package/lib/dist/workflows/agents/gemini.js.map +1 -0
  281. package/lib/dist/workflows/agents/index.d.ts +28 -0
  282. package/lib/dist/workflows/agents/index.d.ts.map +1 -0
  283. package/lib/dist/workflows/agents/index.js +104 -0
  284. package/lib/dist/workflows/agents/index.js.map +1 -0
  285. package/lib/dist/workflows/agents/types.d.ts +55 -0
  286. package/lib/dist/workflows/agents/types.d.ts.map +1 -0
  287. package/lib/dist/workflows/agents/types.js +46 -0
  288. package/lib/dist/workflows/agents/types.js.map +1 -0
  289. package/lib/dist/workflows/batch.d.ts +63 -0
  290. package/lib/dist/workflows/batch.d.ts.map +1 -0
  291. package/lib/dist/workflows/batch.js +151 -0
  292. package/lib/dist/workflows/batch.js.map +1 -0
  293. package/lib/dist/workflows/defaults/claude-design-implement.yaml +3 -4
  294. package/lib/dist/workflows/defaults/spec-author.yaml +11 -9
  295. package/lib/dist/workflows/defaults/spec-fix.yaml +103 -12
  296. package/lib/dist/workflows/defaults/spec-implement-mixed.yaml +21 -193
  297. package/lib/dist/workflows/defaults/spec-implement.yaml +701 -41
  298. package/lib/dist/workflows/defaults/spec-verify.yaml +60 -9
  299. package/lib/dist/workflows/discovery.d.ts +13 -0
  300. package/lib/dist/workflows/discovery.d.ts.map +1 -1
  301. package/lib/dist/workflows/discovery.js +79 -17
  302. package/lib/dist/workflows/discovery.js.map +1 -1
  303. package/lib/dist/workflows/executor.d.ts +18 -0
  304. package/lib/dist/workflows/executor.d.ts.map +1 -1
  305. package/lib/dist/workflows/executor.js +130 -33
  306. package/lib/dist/workflows/executor.js.map +1 -1
  307. package/lib/dist/workflows/gc.d.ts +50 -0
  308. package/lib/dist/workflows/gc.d.ts.map +1 -0
  309. package/lib/dist/workflows/gc.js +120 -0
  310. package/lib/dist/workflows/gc.js.map +1 -0
  311. package/lib/dist/workflows/merge-queue.d.ts +64 -0
  312. package/lib/dist/workflows/merge-queue.d.ts.map +1 -0
  313. package/lib/dist/workflows/merge-queue.js +162 -0
  314. package/lib/dist/workflows/merge-queue.js.map +1 -0
  315. package/lib/dist/workflows/runners/approval.d.ts +1 -1
  316. package/lib/dist/workflows/runners/approval.d.ts.map +1 -1
  317. package/lib/dist/workflows/runners/approval.js +11 -2
  318. package/lib/dist/workflows/runners/approval.js.map +1 -1
  319. package/lib/dist/workflows/runners/bash.d.ts +15 -0
  320. package/lib/dist/workflows/runners/bash.d.ts.map +1 -1
  321. package/lib/dist/workflows/runners/bash.js +50 -1
  322. package/lib/dist/workflows/runners/bash.js.map +1 -1
  323. package/lib/dist/workflows/runners/prompt.d.ts +16 -41
  324. package/lib/dist/workflows/runners/prompt.d.ts.map +1 -1
  325. package/lib/dist/workflows/runners/prompt.js +55 -120
  326. package/lib/dist/workflows/runners/prompt.js.map +1 -1
  327. package/lib/dist/workflows/runners/types.d.ts +12 -0
  328. package/lib/dist/workflows/runners/types.d.ts.map +1 -1
  329. package/lib/dist/workflows/schemas/workflow.d.ts +17 -0
  330. package/lib/dist/workflows/schemas/workflow.d.ts.map +1 -1
  331. package/lib/dist/workflows/schemas/workflow.js +27 -0
  332. package/lib/dist/workflows/schemas/workflow.js.map +1 -1
  333. package/lib/node_modules/.package-lock.json +1 -1
  334. package/lib/package.json +8 -4
  335. package/package.json +5 -3
  336. package/lib/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  337. package/lib/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
  338. package/lib/dist/server/ingest/impact-query.js +0 -343
  339. package/lib/dist/server/routes/events.js +0 -152
  340. package/lib/dist/ui/assets/index-CjjaftDw.js +0 -50
  341. package/node.exe +0 -4
@@ -158,7 +158,8 @@ export async function registerGraphRoutes(app) {
158
158
  /**
159
159
  * GET /api/graph/health — feeds the Graph overview panel:
160
160
  * - linkHealth: spec-link counts per state (verified/drifted/broken/orphaned/…)
161
- * - edgeKinds: edge counts grouped into calls / implements-documents / tests
161
+ * - edgeKinds: calls / implements-documents edge counts, plus `tests` —
162
+ * resolved test-evidence spec_links, not an edge bucket
162
163
  * - hubs: the most-connected nodes (by total degree), for the "Most connected" list
163
164
  * - anchored: nodes that are spec_links targets (by degree), for the "Anchored" list
164
165
  */
@@ -173,13 +174,12 @@ export async function registerGraphRoutes(app) {
173
174
  }
174
175
  const db = getDb(cg);
175
176
  // Edge kinds — bucket by the node kinds the edge connects, mirroring the
176
- // design's "calls / implements / tests" legend. spec endpoints → implements,
177
- // test endpoints → tests, everything else → calls.
177
+ // design's "calls / implements" legend. spec endpoints → implements,
178
+ // everything else → calls.
178
179
  const edgeKinds = db.prepare(`
179
180
  SELECT
180
181
  CASE
181
182
  WHEN ns.kind = 'spec' OR nt.kind = 'spec' THEN 'implements'
182
- WHEN ns.kind = 'test' OR nt.kind = 'test' THEN 'tests'
183
183
  ELSE 'calls'
184
184
  END as bucket,
185
185
  COUNT(*) as count
@@ -191,6 +191,16 @@ export async function registerGraphRoutes(app) {
191
191
  const edgeKindMap = { calls: 0, implements: 0, tests: 0 };
192
192
  for (const r of edgeKinds)
193
193
  edgeKindMap[r.bucket] = r.count;
194
+ // Test evidence is a spec_links relationship, not an edge between node
195
+ // kinds — there is no `test` NodeKind, so the old `ns.kind = 'test'`
196
+ // bucket was permanently 0 (REQ-REVINT-008.A1). Count the resolved
197
+ // tests/validates links instead: a spec whose `verifies:` block points at
198
+ // a real symbol is exactly one test-evidence relationship.
199
+ const testEvidence = db.prepare(`
200
+ SELECT COUNT(*) as c FROM spec_links
201
+ WHERE kind IN ('tests', 'validates') AND resolved_node_id IS NOT NULL
202
+ `).get();
203
+ edgeKindMap['tests'] = testEvidence.c ?? 0;
194
204
  // Synthesized (heuristic) edge count — dashed in the legend.
195
205
  const synth = db.prepare(`SELECT COUNT(*) as c FROM edges WHERE provenance = 'heuristic'`).get();
196
206
  edgeKindMap['synth'] = synth.c ?? 0;
@@ -236,6 +246,13 @@ export async function registerGraphRoutes(app) {
236
246
  * among them. Isolated (degree-0) nodes are omitted — an overview shows the
237
247
  * connected core. `total` reports the full node count so the UI can say
238
248
  * "showing N of M".
249
+ *
250
+ * Traceability is unioned in on top of that cut (REQ-TVIZ-010.A1). A spec
251
+ * node's degree is 1–2, so the top-N pass pruned every spec↔code edge out of
252
+ * the whole-repo view: the one relationship the product exists to show was
253
+ * the first thing dropped. Spec nodes that participate in an edge — and the
254
+ * code nodes on the other end — are therefore kept regardless of degree. A
255
+ * spec node with no edges is still omitted; it would draw as an island.
239
256
  */
240
257
  app.get('/api/graph/full', async (req, reply) => {
241
258
  const cg = await resolveCg(app, req, reply);
@@ -255,15 +272,44 @@ export async function registerGraphRoutes(app) {
255
272
  JOIN nodes n ON n.id = deg.node
256
273
  ORDER BY deg.degree DESC
257
274
  `).all(limit);
275
+ // Spec nodes with at least one edge, plus whatever sits at the other end
276
+ // of those edges — the traceability union (REQ-TVIZ-010.A1).
277
+ const traceRows = db.prepare(`
278
+ SELECT n.id, n.name, n.kind, n.file_path as filePath, COALESCE(deg.degree, 0) as degree
279
+ FROM nodes n
280
+ LEFT JOIN (
281
+ SELECT node, COUNT(*) as degree FROM (
282
+ SELECT source as node FROM edges
283
+ UNION ALL
284
+ SELECT target as node FROM edges
285
+ ) GROUP BY node
286
+ ) deg ON deg.node = n.id
287
+ WHERE n.id IN (
288
+ SELECT e.source FROM edges e JOIN nodes s ON s.id = e.source WHERE s.kind = 'spec'
289
+ UNION
290
+ SELECT e.target FROM edges e JOIN nodes t ON t.id = e.target WHERE t.kind = 'spec'
291
+ UNION
292
+ SELECT e.target FROM edges e JOIN nodes s ON s.id = e.source WHERE s.kind = 'spec'
293
+ UNION
294
+ SELECT e.source FROM edges e JOIN nodes t ON t.id = e.target WHERE t.kind = 'spec'
295
+ )
296
+ `).all();
258
297
  const keep = new Set(nodeRows.map((n) => n.id));
298
+ const nodes = nodeRows.map((n) => ({ id: n.id, name: n.name, kind: n.kind, filePath: n.filePath, degree: n.degree }));
299
+ for (const n of traceRows) {
300
+ if (keep.has(n.id))
301
+ continue;
302
+ keep.add(n.id);
303
+ nodes.push({ id: n.id, name: n.name, kind: n.kind, filePath: n.filePath, degree: n.degree });
304
+ }
259
305
  const allEdges = db.prepare(`SELECT source as "from", target as "to", kind, provenance FROM edges`).all();
260
306
  const edges = allEdges.filter((e) => keep.has(e.from) && keep.has(e.to));
261
307
  const totalRow = db.prepare(`SELECT COUNT(*) as c FROM nodes`).get();
262
308
  return {
263
- nodes: nodeRows.map((n) => ({ id: n.id, name: n.name, kind: n.kind, filePath: n.filePath, degree: n.degree })),
309
+ nodes,
264
310
  edges,
265
- total: totalRow.c ?? nodeRows.length,
266
- shown: nodeRows.length,
311
+ total: totalRow.c ?? nodes.length,
312
+ shown: nodes.length,
267
313
  };
268
314
  });
269
315
  }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * GET /api/knowledge (REQ-KNOW-007).
3
+ *
4
+ * Read-only knowledge view: every domain fact with its id, title, type and the
5
+ * requirement ids it links to at the spec tier (`parentId` + `depends_on`),
6
+ * plus — for `type: external` facts — the dependency it is pinned to, the
7
+ * version it was verified against, and its freshness against the version
8
+ * installed in the project (`fresh` / `stale` / `unverifiable`).
9
+ *
10
+ * Like the domain route, it reaches the engine only through the dynamically
11
+ * loaded instance (`cg.getSpecQueries()`, `cg.getProjectRoot()`) and never
12
+ * runtime-imports the bare `@specship/specship` package (DOM-SPECSHIP-004).
13
+ */
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+ import path from 'node:path';
16
+ function linkedIds(parentId, dependsOn) {
17
+ const out = [];
18
+ if (typeof parentId === 'string' && parentId.length > 0)
19
+ out.push(parentId);
20
+ if (Array.isArray(dependsOn)) {
21
+ for (const d of dependsOn)
22
+ if (typeof d === 'string' && d.length > 0)
23
+ out.push(d);
24
+ }
25
+ else if (typeof dependsOn === 'string' && dependsOn.length > 0) {
26
+ for (const d of dependsOn.split(','))
27
+ if (d.trim())
28
+ out.push(d.trim());
29
+ }
30
+ return [...new Set(out)];
31
+ }
32
+ /** Read one flat `key: value` from a markdown file's frontmatter. */
33
+ function frontmatterValue(file, key) {
34
+ let source;
35
+ try {
36
+ source = readFileSync(file, 'utf-8');
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ const lines = source.split(/\r?\n/);
42
+ if ((lines[0] ?? '').trim() !== '---')
43
+ return undefined;
44
+ for (let i = 1; i < lines.length; i++) {
45
+ const line = (lines[i] ?? '').trim();
46
+ if (line === '---')
47
+ break;
48
+ const idx = line.indexOf(':');
49
+ if (idx > 0 && line.slice(0, idx).trim() === key) {
50
+ return line.slice(idx + 1).trim().replace(/^(['"])(.*)\1$/, '$2') || undefined;
51
+ }
52
+ }
53
+ return undefined;
54
+ }
55
+ /** The version of `dependency` installed under `<projectRoot>/node_modules`. */
56
+ function installedVersion(projectRoot, dependency) {
57
+ if (dependency.includes('..'))
58
+ return undefined;
59
+ const pkg = path.join(projectRoot, 'node_modules', dependency, 'package.json');
60
+ if (!existsSync(pkg))
61
+ return undefined;
62
+ try {
63
+ const v = JSON.parse(readFileSync(pkg, 'utf-8')).version;
64
+ return typeof v === 'string' ? v : undefined;
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ }
70
+ export async function registerKnowledgeRoutes(app) {
71
+ app.get('/api/knowledge', async (req, reply) => {
72
+ const cg = await app.activeCg(req);
73
+ if (!cg) {
74
+ reply.code(409).send({ error: 'no project selected', code: 'no_project' });
75
+ return;
76
+ }
77
+ const projectRoot = cg.getProjectRoot();
78
+ const facts = [];
79
+ for (const f of cg.getSpecQueries().getSpecsByKind('domain')) {
80
+ const meta = (f.metadata ?? {});
81
+ const type = typeof meta.type === 'string' ? meta.type : '';
82
+ const fact = {
83
+ id: f.id,
84
+ title: f.title,
85
+ type,
86
+ linkedRequirementIds: linkedIds(f.parentId, meta.depends_on),
87
+ };
88
+ if (type === 'external') {
89
+ const dependency = typeof meta.dependency === 'string' ? meta.dependency : undefined;
90
+ // `version` is a known top-level frontmatter key the extractor keeps as
91
+ // the numeric spec version, so a semver string may not survive into
92
+ // metadata — fall back to the fact's own frontmatter on disk.
93
+ const version = typeof meta.version === 'string'
94
+ ? meta.version
95
+ : frontmatterValue(path.join(projectRoot, f.sourcePath), 'version');
96
+ // Freshness from the project's own node_modules only. The CLI's
97
+ // `specship knowledge stale` (findStaleExternalFacts) also consults the
98
+ // global npm root; the server stays local-only and never imports the
99
+ // engine package directly (DOM-SPECSHIP-004), so a dependency installed
100
+ // only globally reads `unverifiable` here — never `fresh`.
101
+ const installed = dependency ? installedVersion(projectRoot, dependency) : undefined;
102
+ fact.dependency = dependency;
103
+ fact.version = version;
104
+ fact.stale =
105
+ installed === undefined || version === undefined
106
+ ? 'unverifiable'
107
+ : installed === version
108
+ ? 'fresh'
109
+ : 'stale';
110
+ if (installed !== undefined)
111
+ fact.installedVersion = installed;
112
+ }
113
+ facts.push(fact);
114
+ }
115
+ facts.sort((a, b) => a.id.localeCompare(b.id));
116
+ return { facts };
117
+ });
118
+ }
@@ -400,17 +400,18 @@ function collectUsage(cg) {
400
400
  if (!s || s.example)
401
401
  continue;
402
402
  try {
403
- s.example = { tool, args: JSON.parse(row.input_json) };
403
+ s.example = { tool, args: JSON.parse(row.input_json), synthesized: false };
404
404
  }
405
405
  catch { /* skip bad row */ }
406
406
  }
407
- // Fallback example: the server's top tool with empty args.
407
+ // Fallback example: the server's top tool with empty args. Flagged
408
+ // synthesized so the page can't pass it off as a recorded call.
408
409
  for (const s of byServer.values()) {
409
410
  if (s.example)
410
411
  continue;
411
412
  const top = [...s.tools.entries()].sort((a, b) => b[1].calls - a[1].calls)[0];
412
413
  if (top)
413
- s.example = { tool: top[0], args: {} };
414
+ s.example = { tool: top[0], args: {}, synthesized: true };
414
415
  }
415
416
  return byServer;
416
417
  }
@@ -79,10 +79,10 @@ export async function enumerate(claudeRoot, resolverOpts) {
79
79
  return out;
80
80
  }
81
81
  /**
82
- * Bound on how many projects get a live driftCount per request. Mirrors the
83
- * /api/events sweep: every lookup opens the project's graph through the
84
- * registry LRU (maxOpen 16), so an unbounded pass over a large project list
85
- * churns open/close cycles on every GET. enumerate() sorts by recency, so
82
+ * Bound on how many projects get a live driftCount per request: every lookup
83
+ * opens the project's graph through the registry LRU (maxOpen 16), so an
84
+ * unbounded pass over a large project list churns open/close cycles on every
85
+ * GET. enumerate() sorts by recency, so
86
86
  * the bounded prefix is exactly what the switcher surfaces first; the rest
87
87
  * stay `null` (unknown), which the UI renders as no pill.
88
88
  */
@@ -130,6 +130,24 @@ export async function registerSpecRoutes(app) {
130
130
  return;
131
131
  return cg.getSpecFunnel();
132
132
  });
133
+ /**
134
+ * GET /api/spec/coverage[?spec=<id>] — the test-coverage rollup
135
+ * (REQ-VSTATE-006.A2).
136
+ *
137
+ * Registered before `/api/spec/:id` and matched statically (same shape as
138
+ * `/api/spec/funnel`), so `coverage` is never read as a spec id. Computed on
139
+ * the dynamically-loaded instance — no runtime import of the package here.
140
+ *
141
+ * The verdicts come from `tests` links only, which is the point: the
142
+ * dashboard previously counted an `implements` link as "met".
143
+ */
144
+ app.get('/api/spec/coverage', async (req, reply) => {
145
+ const cg = await resolveCg(app, req, reply);
146
+ if (!cg)
147
+ return;
148
+ const scope = req.query.spec && req.query.spec.length > 0 ? req.query.spec : undefined;
149
+ return cg.getSpecCoverage(scope);
150
+ });
133
151
  app.get('/api/spec/:id', async (req, reply) => {
134
152
  const cg = await resolveCg(app, req, reply);
135
153
  if (!cg)
@@ -244,25 +262,32 @@ export async function registerSpecRoutes(app) {
244
262
  const spec = sq.getSpecById(body.spec_id);
245
263
  if (!spec)
246
264
  return reply.code(404).send({ error: 'spec not found' });
265
+ // Validate the target against the graph before recording (REQ-REVINT-003.A3
266
+ // — same rule as the MCP tool). The dashboard's caller can't retry from a
267
+ // refusal the way an agent can, so an unresolvable target is recorded as
268
+ // `orphaned` and reported back as such — never as `implemented`.
269
+ const nodeKind = body.target_node_kind ?? 'function';
270
+ const node = cg
271
+ .getSpecLinkResolver()
272
+ .findLogicalTarget(body.target_file_path, body.target_qualified_name, nodeKind);
247
273
  const now = Date.now();
248
274
  const id = sq.upsertSpecLink({
249
275
  specId: body.spec_id,
250
276
  targetFilePath: body.target_file_path,
251
277
  targetQualifiedName: body.target_qualified_name,
252
- targetNodeKind: body.target_node_kind ?? 'function',
253
- resolvedNodeId: undefined,
278
+ targetNodeKind: nodeKind,
279
+ resolvedNodeId: node?.id,
254
280
  kind: body.kind ?? 'implements',
255
- state: 'implemented',
281
+ state: node ? 'implemented' : 'orphaned',
256
282
  driftAxis: null,
257
283
  specHashAtLink: spec.contentHash,
258
- nodeSigAtLink: undefined,
284
+ nodeSigAtLink: node?.signature,
259
285
  provenance: 'agent-asserted',
260
286
  confidence: 1.0,
261
287
  createdAt: now,
262
288
  updatedAt: now,
263
289
  });
264
- cg.getSpecLinkResolver().resolveLinksForFiles([body.target_file_path]);
265
- return { id, ok: true };
290
+ return { id, ok: true, resolved: node !== null, state: node ? 'implemented' : 'orphaned' };
266
291
  });
267
292
  app.post('/api/spec/link-verify', async (req, reply) => {
268
293
  const cg = await resolveCg(app, req, reply);
@@ -280,8 +305,13 @@ export async function registerSpecRoutes(app) {
280
305
  // tool: promoting an `implements` link to verified requires the spec to
281
306
  // have at least one test-evidence link (kind='tests'). Without this the
282
307
  // dashboard's Verify button would bypass the gate.
308
+ // File-derived evidence only (REQ-REVINT-005.A2): an agent-asserted `tests`
309
+ // link is a DB-only row, so it can't underwrite a promotion.
283
310
  if (body.result === 'pass' && link.kind === 'implements') {
284
- const hasEvidence = sq.getLinksBySpec(link.specId).some((l) => l.kind === 'tests');
311
+ const hasEvidence = sq
312
+ .getLinksBySpec(link.specId)
313
+ .some((l) => l.kind === 'tests' &&
314
+ (l.provenance === 'spec-declaration' || l.provenance === 'code-comment'));
285
315
  if (!hasEvidence) {
286
316
  return reply.code(409).send({
287
317
  error: `no test evidence linked to ${link.specId} — declare its proving test via a "verifies:" block in the spec (or an @verifies comment on the test) before promoting to verified`,
@@ -290,6 +320,18 @@ export async function registerSpecRoutes(app) {
290
320
  }
291
321
  }
292
322
  sq.updateSpecLinkState(body.link_id, body.result === 'pass' ? 'verified' : 'broken', null);
323
+ // Persist the evidence behind the verdict (REQ-REVINT-004) — same record
324
+ // shape as the MCP tool, so both surfaces render one audit trail.
325
+ const verification = {
326
+ result: body.result,
327
+ verifiedAt: new Date().toISOString(),
328
+ };
329
+ if (body.reason)
330
+ verification.reason = body.reason;
331
+ const evidence = (body.evidence ?? []).filter((e) => typeof e === 'string' && e.length > 0);
332
+ if (evidence.length > 0)
333
+ verification.evidence = evidence;
334
+ sq.updateSpecLinkMetadata(body.link_id, { ...(link.metadata ?? {}), verification });
293
335
  return { ok: true, state: body.result === 'pass' ? 'verified' : 'broken' };
294
336
  });
295
337
  /**
@@ -17,7 +17,6 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
17
17
  import Fastify from 'fastify';
18
18
  import cors from '@fastify/cors';
19
19
  import { startWatcher, primaryProjectMatcher } from './ingest/index.js';
20
- import { backfillDisplaced } from './ingest/impact-backfill.js';
21
20
  import { recostUnpricedPrompts } from './ingest/pricing-backfill.js';
22
21
  import { ProjectRegistry } from './project-registry.js';
23
22
  import { makeStaticHandler, isAssetPath } from './static-handler.js';
@@ -28,10 +27,10 @@ import { registerClaudeRoutes } from './routes/claude.js';
28
27
  import { registerStatusRoutes } from './routes/status.js';
29
28
  import { registerMemoryRoutes } from './routes/memory.js';
30
29
  import { registerProjectsRoutes } from './routes/projects.js';
31
- import { registerEventsRoutes } from './routes/events.js';
32
30
  import { registerReflectRoutes } from './routes/reflect.js';
33
31
  import { registerMaintainabilityRoutes } from './routes/maintainability.js';
34
32
  import { registerDomainRoutes } from './routes/domain.js';
33
+ import { registerKnowledgeRoutes } from './routes/knowledge.js';
35
34
  import { registerMcpRoutes } from './routes/mcp.js';
36
35
  import { registerConfigRoutes } from './routes/config.js';
37
36
  import { readServerConfig } from './server-config.js';
@@ -112,12 +111,36 @@ export async function createServer(options) {
112
111
  credentials: false,
113
112
  });
114
113
  }
114
+ // Test-only fault injection (REQ-TVIZ-004.A2). `E2E_FAULT` holds a
115
+ // comma-separated list of `/api` path substrings; a matching request is
116
+ // answered 500 instead of reaching its handler, so the e2e suite can prove
117
+ // the SPA degrades to an error state on any route without a code change per
118
+ // test. The hook is registered ONLY when the variable is set — with it unset
119
+ // (every production boot) there is no hook and no per-request work at all.
120
+ const faultPatterns = (process.env.E2E_FAULT ?? '')
121
+ .split(',')
122
+ .map((s) => s.trim())
123
+ .filter(Boolean);
124
+ if (faultPatterns.length > 0) {
125
+ console.error(`[specship-server] E2E_FAULT active — 500ing /api paths matching: ${faultPatterns.join(', ')}`);
126
+ app.addHook('onRequest', async (req, reply) => {
127
+ const pathOnly = req.url.split('?')[0] ?? req.url;
128
+ if (!pathOnly.startsWith('/api/'))
129
+ return;
130
+ const hit = faultPatterns.find((p) => pathOnly.includes(p));
131
+ if (!hit)
132
+ return;
133
+ await reply.code(500).send({ error: `injected fault (E2E_FAULT=${hit})`, code: 'e2e_fault' });
134
+ });
135
+ }
115
136
  // Lazy-load specship. Used as the open() impl for the registry, and
116
137
  // (only when a primary path is set) to open the primary instance below.
117
138
  const cgMod = await loadSpecShip();
118
- // maxOpen must exceed the /api/events cross-project sweep size (10) plus
119
- // the primary and a user-picked project, or the sweep churns open/close
120
- // cycles through the LRU on every poll.
139
+ // maxOpen must exceed the /api/projects drift sweep size
140
+ // (DRIFT_SWEEP_LIMIT = 10 in routes/projects.ts) plus the primary and a
141
+ // user-picked project, or the sweep churns open/close cycles through the
142
+ // LRU on every request. (The removed cross-project alert poller swept the
143
+ // same width — REQ-SURF-001.A2.)
121
144
  const registry = new ProjectRegistry({ verbose, maxOpen: 16 }, (p) => cgMod.SpecShip.open(p));
122
145
  // Primary project (optional). When set, specship-scoped routes default to
123
146
  // it when no `?project=<slug>` is provided, and the analytics ingest
@@ -171,17 +194,8 @@ export async function createServer(options) {
171
194
  ownedWatcher = true;
172
195
  if (verbose)
173
196
  console.error('[specship-server] JSONL ingest watcher started');
174
- // Backfill displaced_files / resolution for pre-upgrade rows (is_specship=1,
175
- // resolution IS NULL). Idempotent — safe to run on every boot. Non-fatal:
176
- // a failure here must never abort server startup.
177
- try {
178
- backfillDisplaced(dbHandle, resolveGraph);
179
- if (verbose)
180
- console.error('[specship-server] specship-impact backfill complete');
181
- }
182
- catch (err) {
183
- console.error('[specship-server] specship-impact backfill failed (non-fatal):', err instanceof Error ? err.message : String(err));
184
- }
197
+ // The specship-impact boot backfill is gone with the engine it fed
198
+ // (REQ-SURF-005) — nothing reads displaced_files/resolution any more.
185
199
  // Re-cost prompts ingested while their model family was unpriced,
186
200
  // e.g. fable sessions stuck at $0 (REQ-DASHINT-001.A2). Idempotent.
187
201
  try {
@@ -259,10 +273,10 @@ export async function createServer(options) {
259
273
  await registerClaudeRoutes(app);
260
274
  await registerMemoryRoutes(app);
261
275
  await registerProjectsRoutes(app);
262
- await registerEventsRoutes(app);
263
276
  await registerReflectRoutes(app);
264
277
  await registerMaintainabilityRoutes(app);
265
278
  await registerDomainRoutes(app);
279
+ await registerKnowledgeRoutes(app);
266
280
  // Chat routes removed (CHAT-REMOVE-DOC) — /api/chat and /api/chat/stream
267
281
  // intentionally 404; the reviewer loop lives on Workflows + Runs.
268
282
  await registerMcpRoutes(app);
@@ -0,0 +1,164 @@
1
+ ---
2
+ name: spec-author
3
+ description: Author SpecShip-compatible specs from a feature description — draft the markdown, walk a gap-filling checklist, run a quality review, and write the file under `specs/`. Use this whenever the user wants to write a new requirement, draft a spec for a feature they're about to build, capture acceptance criteria, define a contract, or asks "what should the spec for X look like." Also trigger on phrases like "write a spec", "draft a requirement", "create a REQ", "new spec for", "I want to add a feature and capture the requirements", "spec out", "let's write the requirements for". The skill produces a spec that `specship sync` parses cleanly, grounded in the project's existing code via `mcp__specship__specship_explore`. Do NOT use this skill for reverse-engineering existing code into specs, or for implementing an already-written spec (use `/specship:spec implement` for that).
4
+ ---
5
+
6
+ # Spec Author
7
+
8
+ You're helping the user write a SpecShip-compatible Markdown spec from scratch. Your job is to produce a draft that (a) parses cleanly under `specship sync`, (b) follows the project's quality rubric, and (c) is grounded in the project's actual code so the resulting implementation has a real target.
9
+
10
+ The user usually starts with a one-line feature description. You're going to turn that into a complete spec, asking only the questions the description doesn't already answer.
11
+
12
+ ## What this skill does NOT do
13
+
14
+ - **It does not invent business motivation.** If the user doesn't tell you why a feature is needed, don't make it up. Mark the purpose as `[needs user confirmation]` instead.
15
+ - **It does not implement code.** Spec only. Implementation is `/specship:spec implement`'s job.
16
+ - **It does not promise a perfect spec.** Specs are negotiable; the skill produces a first draft that the user reviews. Every uncertain section carries a `[needs review]` flag so the user knows where to focus.
17
+ - **It does not bulk-author.** One requirement, or one document with N children, per invocation. "Spec the whole app" gets refused.
18
+
19
+ ## The format your output must follow
20
+
21
+ Read `references/format.md` before writing the file. The spec parser at `src/extraction/specs/markdown-spec-extractor.ts` is strict about one thing: **every heading must have an `<!-- id: REQ-X -->` comment immediately above it**. Get this wrong and `specship sync` errors out.
22
+
23
+ Short version:
24
+
25
+ ```markdown
26
+ ---
27
+ id: AUTH-DOC
28
+ title: Authentication
29
+ owner: security
30
+ priority: high
31
+ ---
32
+
33
+ <!-- id: AUTH-DOC -->
34
+ # Authentication
35
+
36
+ This document covers login, session management, and rate-limiting.
37
+
38
+ <!-- id: REQ-AUTH-001 -->
39
+ ## Failed logins MUST be rate-limited
40
+
41
+ The login endpoint rejects more than 5 failed attempts per IP per minute.
42
+
43
+ implementations:
44
+ - src/auth/login.ts:authenticate
45
+ - src/auth/rate-limit.ts:enforce
46
+
47
+ verifies:
48
+ - __tests__/auth-rate-limit.test.ts:rejectsSixthAttempt
49
+
50
+ ## Acceptance
51
+ <!-- id: REQ-AUTH-001.A1 -->
52
+ - A 6th failed attempt within 60 s from the same IP returns 429 with `Retry-After`.
53
+ <!-- id: REQ-AUTH-001.A2 -->
54
+ - A successful login resets the counter for that IP.
55
+ ```
56
+
57
+ `references/format.md` covers the edge cases: optional frontmatter fields, the `implementations:` and `verifies:` block syntax, hierarchy by heading depth, kind inference (document / requirement / acceptance), `.A1`-style child IDs for acceptance bullets.
58
+
59
+ ## The loop
60
+
61
+ ### Step 1 — Scope check
62
+
63
+ Confirm the user wants:
64
+ - **One requirement** under an existing document (most common — pick this if unsure), or
65
+ - **A new document with N child requirements** (when they're spec'ing a whole feature area), or
66
+ - **Refine an existing draft** (they paste markdown; you review and gap-fill).
67
+
68
+ If they say "spec the whole app", refuse and ask them to pick a feature area. Bulk-authored specs without per-REQ thinking are unreviewable.
69
+
70
+ ### Step 2 — Ground in the code
71
+
72
+ If `mcp__specship__specship_explore` is available, call it on terms from the feature description before drafting. Two things this gives you:
73
+
74
+ - **Realistic file paths and symbol names** for the `implementations:` block. Don't write `implementations: - src/auth.ts:login` if the project has `src/services/auth-service.ts:AuthService.handleLogin`.
75
+ - **Existing conventions to align with.** If the codebase already has rate-limiting, your draft should reference the existing rate limiter rather than inventing a new one.
76
+
77
+ Skip this step only if (a) you don't have specship tools available, or (b) the feature is greenfield (no existing code to ground in).
78
+
79
+ ### Step 3 — Draft the spec
80
+
81
+ Produce the full markdown body. Cover:
82
+
83
+ - **Frontmatter** (optional but recommended) — `id`, `title`, `owner`, `priority`.
84
+ - **Document body or single requirement** — depending on scope. Use RFC 2119 keywords (MUST / SHOULD / MAY) in the requirement title.
85
+ - **Acceptance criteria** — under a `## Acceptance` heading, one bullet per testable condition, each with its own `.A<N>` ID. Each bullet must be independently checkable — "MUST reject more than 5 attempts" not "MUST rate-limit".
86
+ - **`implementations:` block** — when you have grounding from Step 2, list the file paths + symbol names the implementation will live in. Speculative; the spec writer can adjust before implementation runs.
87
+ - **`verifies:` block** — the test-evidence declaration. Leave it out of a fresh spec unless the tests already exist; it's what promotes a link to `verified`.
88
+ - **`[needs user confirmation]` flags** — wherever you made an assumption (owner, priority, edge case behavior, exact threshold). Don't ask the user every question in one pass; mark the gap in the draft and surface it in Step 4.
89
+
90
+ Apply `references/quality-rubric.md`: no implementation leak, one concern per REQ, testable bullets, parent/child structure, RFC 2119 keywords used correctly.
91
+
92
+ ### Step 4 — Gap-fill
93
+
94
+ Walk the checklist in `references/gap-questions.md` against your draft. Most categories — failure modes, TTLs, concurrency, observability, security, ownership — will already be answered by Steps 2–3. **Ask the user only about the gaps that remain**, no more than 3–5 questions in one round.
95
+
96
+ The catalog's **Project invariants** section is the one part you should always walk: a spec that contradicts a house invariant gets rejected at review, no matter how well-formed it is.
97
+
98
+ Phrase questions so the user can answer in one line each:
99
+
100
+ > A few details to nail down before I write the file:
101
+ > 1. Owner — `security`, or someone else?
102
+ > 2. When the 5-attempt threshold is hit, do we just return 429, or also fire a webhook / log?
103
+ > 3. Counter reset on successful login — only for that IP, or for all sessions on the account?
104
+
105
+ Update the draft with their answers. If they say "skip these for now," leave the markers in and proceed.
106
+
107
+ ### Step 5 — Review
108
+
109
+ Run `references/review-checklist.md` against the final draft. Surface findings as a short numbered list:
110
+
111
+ > Quality review:
112
+ > 1. REQ-AUTH-001.A1 — could be more specific. Suggest naming the response header (`Retry-After`) and the duration.
113
+ > 2. The `implementations:` block points at `src/auth/login.ts:authenticate` — that symbol exists per `specship_node` ✓.
114
+ > 3. No explicit acceptance criterion for "counter reset on successful login" — add as A3?
115
+
116
+ Apply the user's choices to the draft.
117
+
118
+ ### Step 6 — Write the file
119
+
120
+ Use the `Write` tool against `<project-root>/specs/<slug>.md`. Choose the slug from the feature name (kebab-case, no date prefix — the date is in git). If a file with that name already exists, append to it (with the new REQ markers) rather than overwriting; tell the user what you did.
121
+
122
+ After writing:
123
+
124
+ ```
125
+ Wrote specs/<slug>.md.
126
+
127
+ Next steps:
128
+ 1. Review the file (a few `[needs review]` flags remain if you skipped questions in Step 4).
129
+ 2. specship sync # picks up the new spec into the graph
130
+ 3. /specship:spec implement <REQ-ID> # when you're ready to build it
131
+ ```
132
+
133
+ ## When to use the workflow instead of the skill directly
134
+
135
+ This skill is the **lightweight path** — runs conversationally in Claude Code, no formal approval gates. For team contexts that want the same discipline `spec-implement` enforces, point the user at:
136
+
137
+ ```
138
+ specship workflow run spec-author --input DESCRIPTION="..."
139
+ ```
140
+
141
+ That workflow wraps this same loop with two approval gates — one after gap-fill, one before file write — and a git-worktree-isolated artifact directory. Use it when (a) the spec author is reviewing a teammate's draft, or (b) the team has agreed every new spec gets a written review step.
142
+
143
+ ## Anti-patterns
144
+
145
+ - **Writing the spec without grounding in code.** The result is a beautiful document that points at symbols which don't exist. Use `specship_explore` first.
146
+ - **Asking the user every question in the catalog.** Five questions is plenty per round; the rest become `[needs review]` markers the user resolves at their pace.
147
+ - **Specifying the implementation.** "MUST use JWT signed with RS256" is implementation. "MUST be cryptographically tamper-evident with a 24 h TTL" is contract. The skill steers toward contract; review and rewrite if you find yourself naming algorithms.
148
+ - **One REQ that does too much.** "MUST handle login, logout, and password reset" is three REQs. Split before drafting.
149
+ - **Forgetting the embedded `<!-- id: -->` markers.** Every heading needs one immediately above. Without it `specship sync` fails the file with `spec_missing_id`.
150
+ - **Reusing an ID already used in the same file.** The later occurrence wins (the store does `INSERT OR REPLACE`), so the earlier heading's content is silently overwritten.
151
+ - **Using today's date in the slug.** The date lives in git; the slug should be the feature name (`payment-retry.md`, not `2026-06-18-payment-retry.md`).
152
+
153
+ ## Reference files
154
+
155
+ The references/ directory is organized by stage:
156
+
157
+ | File | Stage | What it covers |
158
+ |---|---|---|
159
+ | `references/format.md` | Step 3 (draft) | Exact markdown the parser accepts |
160
+ | `references/quality-rubric.md` | Step 3 (draft) | What makes a "good" spec |
161
+ | `references/gap-questions.md` | Step 4 (gap-fill) | Catalog of questions to walk, plus the project invariants |
162
+ | `references/review-checklist.md` | Step 5 (review) | What to scan for before finalizing |
163
+
164
+ Read the relevant reference before each step. Don't paraphrase from memory — the format file in particular is the parser's contract.