@specship/specship 0.11.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 (879) hide show
  1. package/.claude-plugin/plugin.json +6 -0
  2. package/LICENSE +21 -0
  3. package/README.md +583 -0
  4. package/agents/specship-explorer.md +29 -0
  5. package/commands/ss-behaviour.md +116 -0
  6. package/commands/ss-check.md +43 -0
  7. package/commands/ss-design-implement.md +84 -0
  8. package/commands/ss-design-loop.md +125 -0
  9. package/commands/ss-explore.md +43 -0
  10. package/commands/ss-spec.md +118 -0
  11. package/dist/activation/starter-prompt.d.ts +57 -0
  12. package/dist/activation/starter-prompt.d.ts.map +1 -0
  13. package/dist/activation/starter-prompt.js +164 -0
  14. package/dist/activation/starter-prompt.js.map +1 -0
  15. package/dist/analytics/specship-impact.d.ts +72 -0
  16. package/dist/analytics/specship-impact.d.ts.map +1 -0
  17. package/dist/analytics/specship-impact.js +216 -0
  18. package/dist/analytics/specship-impact.js.map +1 -0
  19. package/dist/behaviour/behaviour-surface.d.ts +59 -0
  20. package/dist/behaviour/behaviour-surface.d.ts.map +1 -0
  21. package/dist/behaviour/behaviour-surface.js +112 -0
  22. package/dist/behaviour/behaviour-surface.js.map +1 -0
  23. package/dist/bin/node-version-check.d.ts +37 -0
  24. package/dist/bin/node-version-check.d.ts.map +1 -0
  25. package/dist/bin/node-version-check.js +79 -0
  26. package/dist/bin/node-version-check.js.map +1 -0
  27. package/dist/bin/specship.d.ts +25 -0
  28. package/dist/bin/specship.d.ts.map +1 -0
  29. package/dist/bin/specship.js +2823 -0
  30. package/dist/bin/specship.js.map +1 -0
  31. package/dist/bin/uninstall.d.ts +13 -0
  32. package/dist/bin/uninstall.d.ts.map +1 -0
  33. package/dist/bin/uninstall.js +35 -0
  34. package/dist/bin/uninstall.js.map +1 -0
  35. package/dist/context/formatter.d.ts +30 -0
  36. package/dist/context/formatter.d.ts.map +1 -0
  37. package/dist/context/formatter.js +263 -0
  38. package/dist/context/formatter.js.map +1 -0
  39. package/dist/context/index.d.ts +119 -0
  40. package/dist/context/index.d.ts.map +1 -0
  41. package/dist/context/index.js +1289 -0
  42. package/dist/context/index.js.map +1 -0
  43. package/dist/context/markers.d.ts +19 -0
  44. package/dist/context/markers.d.ts.map +1 -0
  45. package/dist/context/markers.js +22 -0
  46. package/dist/context/markers.js.map +1 -0
  47. package/dist/db/index.d.ts +103 -0
  48. package/dist/db/index.d.ts.map +1 -0
  49. package/dist/db/index.js +279 -0
  50. package/dist/db/index.js.map +1 -0
  51. package/dist/db/migrations.d.ts +44 -0
  52. package/dist/db/migrations.d.ts.map +1 -0
  53. package/dist/db/migrations.js +503 -0
  54. package/dist/db/migrations.js.map +1 -0
  55. package/dist/db/queries.d.ts +357 -0
  56. package/dist/db/queries.d.ts.map +1 -0
  57. package/dist/db/queries.js +1504 -0
  58. package/dist/db/queries.js.map +1 -0
  59. package/dist/db/schema.sql +451 -0
  60. package/dist/db/spec-queries.d.ts +130 -0
  61. package/dist/db/spec-queries.d.ts.map +1 -0
  62. package/dist/db/spec-queries.js +738 -0
  63. package/dist/db/spec-queries.js.map +1 -0
  64. package/dist/db/sqlite-adapter.d.ts +65 -0
  65. package/dist/db/sqlite-adapter.d.ts.map +1 -0
  66. package/dist/db/sqlite-adapter.js +214 -0
  67. package/dist/db/sqlite-adapter.js.map +1 -0
  68. package/dist/designer/artifact-store.js +54 -0
  69. package/dist/designer/browser.js +141 -0
  70. package/dist/designer/cdp-ensure.js +60 -0
  71. package/dist/designer/cdp-env.js +18 -0
  72. package/dist/designer/cdp-trace.js +599 -0
  73. package/dist/designer/cross-platform.js +74 -0
  74. package/dist/designer/designer-controller.js +1413 -0
  75. package/dist/designer/file-panel.js +39 -0
  76. package/dist/designer/interstitials.js +97 -0
  77. package/dist/designer/oopif-reader.js +176 -0
  78. package/dist/designer/package-meta.js +18 -0
  79. package/dist/designer/preview-host.js +50 -0
  80. package/dist/designer/repo-root.js +31 -0
  81. package/dist/designer/run-state.js +353 -0
  82. package/dist/designer/session-store.js +59 -0
  83. package/dist/designer/ui-anchors.js +651 -0
  84. package/dist/directory.d.ts +67 -0
  85. package/dist/directory.d.ts.map +1 -0
  86. package/dist/directory.js +267 -0
  87. package/dist/directory.js.map +1 -0
  88. package/dist/enforce/enforce.d.ts +70 -0
  89. package/dist/enforce/enforce.d.ts.map +1 -0
  90. package/dist/enforce/enforce.js +125 -0
  91. package/dist/enforce/enforce.js.map +1 -0
  92. package/dist/errors.d.ts +136 -0
  93. package/dist/errors.d.ts.map +1 -0
  94. package/dist/errors.js +219 -0
  95. package/dist/errors.js.map +1 -0
  96. package/dist/extraction/dfm-extractor.d.ts +31 -0
  97. package/dist/extraction/dfm-extractor.d.ts.map +1 -0
  98. package/dist/extraction/dfm-extractor.js +151 -0
  99. package/dist/extraction/dfm-extractor.js.map +1 -0
  100. package/dist/extraction/generated-detection.d.ts +30 -0
  101. package/dist/extraction/generated-detection.d.ts.map +1 -0
  102. package/dist/extraction/generated-detection.js +80 -0
  103. package/dist/extraction/generated-detection.js.map +1 -0
  104. package/dist/extraction/grammars.d.ts +100 -0
  105. package/dist/extraction/grammars.d.ts.map +1 -0
  106. package/dist/extraction/grammars.js +426 -0
  107. package/dist/extraction/grammars.js.map +1 -0
  108. package/dist/extraction/index.d.ts +138 -0
  109. package/dist/extraction/index.d.ts.map +1 -0
  110. package/dist/extraction/index.js +1394 -0
  111. package/dist/extraction/index.js.map +1 -0
  112. package/dist/extraction/languages/c-cpp.d.ts +4 -0
  113. package/dist/extraction/languages/c-cpp.d.ts.map +1 -0
  114. package/dist/extraction/languages/c-cpp.js +171 -0
  115. package/dist/extraction/languages/c-cpp.js.map +1 -0
  116. package/dist/extraction/languages/csharp.d.ts +3 -0
  117. package/dist/extraction/languages/csharp.d.ts.map +1 -0
  118. package/dist/extraction/languages/csharp.js +73 -0
  119. package/dist/extraction/languages/csharp.js.map +1 -0
  120. package/dist/extraction/languages/dart.d.ts +3 -0
  121. package/dist/extraction/languages/dart.d.ts.map +1 -0
  122. package/dist/extraction/languages/dart.js +192 -0
  123. package/dist/extraction/languages/dart.js.map +1 -0
  124. package/dist/extraction/languages/go.d.ts +3 -0
  125. package/dist/extraction/languages/go.d.ts.map +1 -0
  126. package/dist/extraction/languages/go.js +74 -0
  127. package/dist/extraction/languages/go.js.map +1 -0
  128. package/dist/extraction/languages/index.d.ts +10 -0
  129. package/dist/extraction/languages/index.d.ts.map +1 -0
  130. package/dist/extraction/languages/index.js +51 -0
  131. package/dist/extraction/languages/index.js.map +1 -0
  132. package/dist/extraction/languages/java.d.ts +3 -0
  133. package/dist/extraction/languages/java.d.ts.map +1 -0
  134. package/dist/extraction/languages/java.js +70 -0
  135. package/dist/extraction/languages/java.js.map +1 -0
  136. package/dist/extraction/languages/javascript.d.ts +3 -0
  137. package/dist/extraction/languages/javascript.d.ts.map +1 -0
  138. package/dist/extraction/languages/javascript.js +90 -0
  139. package/dist/extraction/languages/javascript.js.map +1 -0
  140. package/dist/extraction/languages/kotlin.d.ts +3 -0
  141. package/dist/extraction/languages/kotlin.d.ts.map +1 -0
  142. package/dist/extraction/languages/kotlin.js +259 -0
  143. package/dist/extraction/languages/kotlin.js.map +1 -0
  144. package/dist/extraction/languages/lua.d.ts +3 -0
  145. package/dist/extraction/languages/lua.d.ts.map +1 -0
  146. package/dist/extraction/languages/lua.js +150 -0
  147. package/dist/extraction/languages/lua.js.map +1 -0
  148. package/dist/extraction/languages/luau.d.ts +3 -0
  149. package/dist/extraction/languages/luau.d.ts.map +1 -0
  150. package/dist/extraction/languages/luau.js +37 -0
  151. package/dist/extraction/languages/luau.js.map +1 -0
  152. package/dist/extraction/languages/objc.d.ts +3 -0
  153. package/dist/extraction/languages/objc.d.ts.map +1 -0
  154. package/dist/extraction/languages/objc.js +133 -0
  155. package/dist/extraction/languages/objc.js.map +1 -0
  156. package/dist/extraction/languages/pascal.d.ts +3 -0
  157. package/dist/extraction/languages/pascal.d.ts.map +1 -0
  158. package/dist/extraction/languages/pascal.js +66 -0
  159. package/dist/extraction/languages/pascal.js.map +1 -0
  160. package/dist/extraction/languages/php.d.ts +3 -0
  161. package/dist/extraction/languages/php.d.ts.map +1 -0
  162. package/dist/extraction/languages/php.js +107 -0
  163. package/dist/extraction/languages/php.js.map +1 -0
  164. package/dist/extraction/languages/python.d.ts +3 -0
  165. package/dist/extraction/languages/python.d.ts.map +1 -0
  166. package/dist/extraction/languages/python.js +56 -0
  167. package/dist/extraction/languages/python.js.map +1 -0
  168. package/dist/extraction/languages/ruby.d.ts +3 -0
  169. package/dist/extraction/languages/ruby.d.ts.map +1 -0
  170. package/dist/extraction/languages/ruby.js +114 -0
  171. package/dist/extraction/languages/ruby.js.map +1 -0
  172. package/dist/extraction/languages/rust.d.ts +3 -0
  173. package/dist/extraction/languages/rust.d.ts.map +1 -0
  174. package/dist/extraction/languages/rust.js +109 -0
  175. package/dist/extraction/languages/rust.js.map +1 -0
  176. package/dist/extraction/languages/scala.d.ts +3 -0
  177. package/dist/extraction/languages/scala.d.ts.map +1 -0
  178. package/dist/extraction/languages/scala.js +139 -0
  179. package/dist/extraction/languages/scala.js.map +1 -0
  180. package/dist/extraction/languages/swift.d.ts +3 -0
  181. package/dist/extraction/languages/swift.d.ts.map +1 -0
  182. package/dist/extraction/languages/swift.js +91 -0
  183. package/dist/extraction/languages/swift.js.map +1 -0
  184. package/dist/extraction/languages/typescript.d.ts +3 -0
  185. package/dist/extraction/languages/typescript.d.ts.map +1 -0
  186. package/dist/extraction/languages/typescript.js +129 -0
  187. package/dist/extraction/languages/typescript.js.map +1 -0
  188. package/dist/extraction/liquid-extractor.d.ts +52 -0
  189. package/dist/extraction/liquid-extractor.d.ts.map +1 -0
  190. package/dist/extraction/liquid-extractor.js +313 -0
  191. package/dist/extraction/liquid-extractor.js.map +1 -0
  192. package/dist/extraction/mybatis-extractor.d.ts +48 -0
  193. package/dist/extraction/mybatis-extractor.d.ts.map +1 -0
  194. package/dist/extraction/mybatis-extractor.js +198 -0
  195. package/dist/extraction/mybatis-extractor.js.map +1 -0
  196. package/dist/extraction/parse-worker.d.ts +8 -0
  197. package/dist/extraction/parse-worker.d.ts.map +1 -0
  198. package/dist/extraction/parse-worker.js +94 -0
  199. package/dist/extraction/parse-worker.js.map +1 -0
  200. package/dist/extraction/specs/markdown-spec-extractor.d.ts +114 -0
  201. package/dist/extraction/specs/markdown-spec-extractor.d.ts.map +1 -0
  202. package/dist/extraction/specs/markdown-spec-extractor.js +699 -0
  203. package/dist/extraction/specs/markdown-spec-extractor.js.map +1 -0
  204. package/dist/extraction/specs/types.d.ts +39 -0
  205. package/dist/extraction/specs/types.d.ts.map +1 -0
  206. package/dist/extraction/specs/types.js +8 -0
  207. package/dist/extraction/specs/types.js.map +1 -0
  208. package/dist/extraction/svelte-extractor.d.ts +56 -0
  209. package/dist/extraction/svelte-extractor.d.ts.map +1 -0
  210. package/dist/extraction/svelte-extractor.js +272 -0
  211. package/dist/extraction/svelte-extractor.js.map +1 -0
  212. package/dist/extraction/tree-sitter-helpers.d.ts +28 -0
  213. package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -0
  214. package/dist/extraction/tree-sitter-helpers.js +103 -0
  215. package/dist/extraction/tree-sitter-helpers.js.map +1 -0
  216. package/dist/extraction/tree-sitter-types.d.ts +193 -0
  217. package/dist/extraction/tree-sitter-types.d.ts.map +1 -0
  218. package/dist/extraction/tree-sitter-types.js +10 -0
  219. package/dist/extraction/tree-sitter-types.js.map +1 -0
  220. package/dist/extraction/tree-sitter.d.ts +317 -0
  221. package/dist/extraction/tree-sitter.d.ts.map +1 -0
  222. package/dist/extraction/tree-sitter.js +3092 -0
  223. package/dist/extraction/tree-sitter.js.map +1 -0
  224. package/dist/extraction/vue-extractor.d.ts +51 -0
  225. package/dist/extraction/vue-extractor.d.ts.map +1 -0
  226. package/dist/extraction/vue-extractor.js +251 -0
  227. package/dist/extraction/vue-extractor.js.map +1 -0
  228. package/dist/extraction/wasm/tree-sitter-lua.wasm +0 -0
  229. package/dist/extraction/wasm/tree-sitter-luau.wasm +0 -0
  230. package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  231. package/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
  232. package/dist/extraction/wasm-runtime-flags.d.ts +38 -0
  233. package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -0
  234. package/dist/extraction/wasm-runtime-flags.js +106 -0
  235. package/dist/extraction/wasm-runtime-flags.js.map +1 -0
  236. package/dist/fitness/fitness.d.ts +75 -0
  237. package/dist/fitness/fitness.d.ts.map +1 -0
  238. package/dist/fitness/fitness.js +204 -0
  239. package/dist/fitness/fitness.js.map +1 -0
  240. package/dist/graph/index.d.ts +8 -0
  241. package/dist/graph/index.d.ts.map +1 -0
  242. package/dist/graph/index.js +13 -0
  243. package/dist/graph/index.js.map +1 -0
  244. package/dist/graph/maintainability.d.ts +115 -0
  245. package/dist/graph/maintainability.d.ts.map +1 -0
  246. package/dist/graph/maintainability.js +299 -0
  247. package/dist/graph/maintainability.js.map +1 -0
  248. package/dist/graph/queries.d.ts +106 -0
  249. package/dist/graph/queries.d.ts.map +1 -0
  250. package/dist/graph/queries.js +366 -0
  251. package/dist/graph/queries.js.map +1 -0
  252. package/dist/graph/traversal.d.ts +127 -0
  253. package/dist/graph/traversal.d.ts.map +1 -0
  254. package/dist/graph/traversal.js +531 -0
  255. package/dist/graph/traversal.js.map +1 -0
  256. package/dist/health/smoke-check.d.ts +85 -0
  257. package/dist/health/smoke-check.d.ts.map +1 -0
  258. package/dist/health/smoke-check.js +246 -0
  259. package/dist/health/smoke-check.js.map +1 -0
  260. package/dist/index.d.ts +674 -0
  261. package/dist/index.d.ts.map +1 -0
  262. package/dist/index.js +1473 -0
  263. package/dist/index.js.map +1 -0
  264. package/dist/installer/config-writer.d.ts +28 -0
  265. package/dist/installer/config-writer.d.ts.map +1 -0
  266. package/dist/installer/config-writer.js +91 -0
  267. package/dist/installer/config-writer.js.map +1 -0
  268. package/dist/installer/index.d.ts +100 -0
  269. package/dist/installer/index.d.ts.map +1 -0
  270. package/dist/installer/index.js +442 -0
  271. package/dist/installer/index.js.map +1 -0
  272. package/dist/installer/init-offer.d.ts +31 -0
  273. package/dist/installer/init-offer.d.ts.map +1 -0
  274. package/dist/installer/init-offer.js +30 -0
  275. package/dist/installer/init-offer.js.map +1 -0
  276. package/dist/installer/instructions-template.d.ts +36 -0
  277. package/dist/installer/instructions-template.d.ts.map +1 -0
  278. package/dist/installer/instructions-template.js +57 -0
  279. package/dist/installer/instructions-template.js.map +1 -0
  280. package/dist/installer/targets/claude.d.ts +146 -0
  281. package/dist/installer/targets/claude.d.ts.map +1 -0
  282. package/dist/installer/targets/claude.js +864 -0
  283. package/dist/installer/targets/claude.js.map +1 -0
  284. package/dist/installer/targets/registry.d.ts +19 -0
  285. package/dist/installer/targets/registry.d.ts.map +1 -0
  286. package/dist/installer/targets/registry.js +31 -0
  287. package/dist/installer/targets/registry.js.map +1 -0
  288. package/dist/installer/targets/shared.d.ts +76 -0
  289. package/dist/installer/targets/shared.d.ts.map +1 -0
  290. package/dist/installer/targets/shared.js +260 -0
  291. package/dist/installer/targets/shared.js.map +1 -0
  292. package/dist/installer/targets/types.d.ts +95 -0
  293. package/dist/installer/targets/types.d.ts.map +1 -0
  294. package/dist/installer/targets/types.js +12 -0
  295. package/dist/installer/targets/types.js.map +1 -0
  296. package/dist/isolation/worktree.d.ts +65 -0
  297. package/dist/isolation/worktree.d.ts.map +1 -0
  298. package/dist/isolation/worktree.js +231 -0
  299. package/dist/isolation/worktree.js.map +1 -0
  300. package/dist/mcp/daemon-paths.d.ts +46 -0
  301. package/dist/mcp/daemon-paths.d.ts.map +1 -0
  302. package/dist/mcp/daemon-paths.js +125 -0
  303. package/dist/mcp/daemon-paths.js.map +1 -0
  304. package/dist/mcp/daemon.d.ts +161 -0
  305. package/dist/mcp/daemon.d.ts.map +1 -0
  306. package/dist/mcp/daemon.js +403 -0
  307. package/dist/mcp/daemon.js.map +1 -0
  308. package/dist/mcp/designer-tools.d.ts +33 -0
  309. package/dist/mcp/designer-tools.d.ts.map +1 -0
  310. package/dist/mcp/designer-tools.js +313 -0
  311. package/dist/mcp/designer-tools.js.map +1 -0
  312. package/dist/mcp/engine.d.ts +105 -0
  313. package/dist/mcp/engine.d.ts.map +1 -0
  314. package/dist/mcp/engine.js +270 -0
  315. package/dist/mcp/engine.js.map +1 -0
  316. package/dist/mcp/fitness-tool.d.ts +12 -0
  317. package/dist/mcp/fitness-tool.d.ts.map +1 -0
  318. package/dist/mcp/fitness-tool.js +46 -0
  319. package/dist/mcp/fitness-tool.js.map +1 -0
  320. package/dist/mcp/index.d.ts +112 -0
  321. package/dist/mcp/index.d.ts.map +1 -0
  322. package/dist/mcp/index.js +477 -0
  323. package/dist/mcp/index.js.map +1 -0
  324. package/dist/mcp/maintainability-tool.d.ts +13 -0
  325. package/dist/mcp/maintainability-tool.d.ts.map +1 -0
  326. package/dist/mcp/maintainability-tool.js +64 -0
  327. package/dist/mcp/maintainability-tool.js.map +1 -0
  328. package/dist/mcp/proxy.d.ts +81 -0
  329. package/dist/mcp/proxy.d.ts.map +1 -0
  330. package/dist/mcp/proxy.js +510 -0
  331. package/dist/mcp/proxy.js.map +1 -0
  332. package/dist/mcp/server-instructions.d.ts +18 -0
  333. package/dist/mcp/server-instructions.d.ts.map +1 -0
  334. package/dist/mcp/server-instructions.js +79 -0
  335. package/dist/mcp/server-instructions.js.map +1 -0
  336. package/dist/mcp/session.d.ts +77 -0
  337. package/dist/mcp/session.d.ts.map +1 -0
  338. package/dist/mcp/session.js +294 -0
  339. package/dist/mcp/session.js.map +1 -0
  340. package/dist/mcp/spec-tools.d.ts +39 -0
  341. package/dist/mcp/spec-tools.d.ts.map +1 -0
  342. package/dist/mcp/spec-tools.js +534 -0
  343. package/dist/mcp/spec-tools.js.map +1 -0
  344. package/dist/mcp/tools.d.ts +417 -0
  345. package/dist/mcp/tools.d.ts.map +1 -0
  346. package/dist/mcp/tools.js +3179 -0
  347. package/dist/mcp/tools.js.map +1 -0
  348. package/dist/mcp/transport.d.ts +188 -0
  349. package/dist/mcp/transport.d.ts.map +1 -0
  350. package/dist/mcp/transport.js +343 -0
  351. package/dist/mcp/transport.js.map +1 -0
  352. package/dist/mcp/version.d.ts +19 -0
  353. package/dist/mcp/version.d.ts.map +1 -0
  354. package/dist/mcp/version.js +71 -0
  355. package/dist/mcp/version.js.map +1 -0
  356. package/dist/reflect/apply.d.ts +31 -0
  357. package/dist/reflect/apply.d.ts.map +1 -0
  358. package/dist/reflect/apply.js +286 -0
  359. package/dist/reflect/apply.js.map +1 -0
  360. package/dist/reflect/hash.d.ts +20 -0
  361. package/dist/reflect/hash.d.ts.map +1 -0
  362. package/dist/reflect/hash.js +36 -0
  363. package/dist/reflect/hash.js.map +1 -0
  364. package/dist/reflect/index.d.ts +16 -0
  365. package/dist/reflect/index.d.ts.map +1 -0
  366. package/dist/reflect/index.js +43 -0
  367. package/dist/reflect/index.js.map +1 -0
  368. package/dist/reflect/miner.d.ts +21 -0
  369. package/dist/reflect/miner.d.ts.map +1 -0
  370. package/dist/reflect/miner.js +463 -0
  371. package/dist/reflect/miner.js.map +1 -0
  372. package/dist/reflect/store.d.ts +31 -0
  373. package/dist/reflect/store.d.ts.map +1 -0
  374. package/dist/reflect/store.js +101 -0
  375. package/dist/reflect/store.js.map +1 -0
  376. package/dist/reflect/sweep.d.ts +26 -0
  377. package/dist/reflect/sweep.d.ts.map +1 -0
  378. package/dist/reflect/sweep.js +42 -0
  379. package/dist/reflect/sweep.js.map +1 -0
  380. package/dist/reflect/targets.d.ts +52 -0
  381. package/dist/reflect/targets.d.ts.map +1 -0
  382. package/dist/reflect/targets.js +192 -0
  383. package/dist/reflect/targets.js.map +1 -0
  384. package/dist/reflect/types.d.ts +96 -0
  385. package/dist/reflect/types.d.ts.map +1 -0
  386. package/dist/reflect/types.js +13 -0
  387. package/dist/reflect/types.js.map +1 -0
  388. package/dist/resolution/brief-link-resolver.d.ts +114 -0
  389. package/dist/resolution/brief-link-resolver.d.ts.map +1 -0
  390. package/dist/resolution/brief-link-resolver.js +261 -0
  391. package/dist/resolution/brief-link-resolver.js.map +1 -0
  392. package/dist/resolution/callback-synthesizer.d.ts +10 -0
  393. package/dist/resolution/callback-synthesizer.d.ts.map +1 -0
  394. package/dist/resolution/callback-synthesizer.js +1300 -0
  395. package/dist/resolution/callback-synthesizer.js.map +1 -0
  396. package/dist/resolution/domain-gap-seed.d.ts +60 -0
  397. package/dist/resolution/domain-gap-seed.d.ts.map +1 -0
  398. package/dist/resolution/domain-gap-seed.js +87 -0
  399. package/dist/resolution/domain-gap-seed.js.map +1 -0
  400. package/dist/resolution/frameworks/cargo-workspace.d.ts +18 -0
  401. package/dist/resolution/frameworks/cargo-workspace.d.ts.map +1 -0
  402. package/dist/resolution/frameworks/cargo-workspace.js +225 -0
  403. package/dist/resolution/frameworks/cargo-workspace.js.map +1 -0
  404. package/dist/resolution/frameworks/csharp.d.ts +8 -0
  405. package/dist/resolution/frameworks/csharp.d.ts.map +1 -0
  406. package/dist/resolution/frameworks/csharp.js +241 -0
  407. package/dist/resolution/frameworks/csharp.js.map +1 -0
  408. package/dist/resolution/frameworks/drupal.d.ts +51 -0
  409. package/dist/resolution/frameworks/drupal.d.ts.map +1 -0
  410. package/dist/resolution/frameworks/drupal.js +367 -0
  411. package/dist/resolution/frameworks/drupal.js.map +1 -0
  412. package/dist/resolution/frameworks/expo-modules.d.ts +3 -0
  413. package/dist/resolution/frameworks/expo-modules.d.ts.map +1 -0
  414. package/dist/resolution/frameworks/expo-modules.js +143 -0
  415. package/dist/resolution/frameworks/expo-modules.js.map +1 -0
  416. package/dist/resolution/frameworks/express.d.ts +8 -0
  417. package/dist/resolution/frameworks/express.d.ts.map +1 -0
  418. package/dist/resolution/frameworks/express.js +308 -0
  419. package/dist/resolution/frameworks/express.js.map +1 -0
  420. package/dist/resolution/frameworks/fabric.d.ts +3 -0
  421. package/dist/resolution/frameworks/fabric.d.ts.map +1 -0
  422. package/dist/resolution/frameworks/fabric.js +354 -0
  423. package/dist/resolution/frameworks/fabric.js.map +1 -0
  424. package/dist/resolution/frameworks/go.d.ts +8 -0
  425. package/dist/resolution/frameworks/go.d.ts.map +1 -0
  426. package/dist/resolution/frameworks/go.js +161 -0
  427. package/dist/resolution/frameworks/go.js.map +1 -0
  428. package/dist/resolution/frameworks/index.d.ts +48 -0
  429. package/dist/resolution/frameworks/index.d.ts.map +1 -0
  430. package/dist/resolution/frameworks/index.js +161 -0
  431. package/dist/resolution/frameworks/index.js.map +1 -0
  432. package/dist/resolution/frameworks/java.d.ts +8 -0
  433. package/dist/resolution/frameworks/java.d.ts.map +1 -0
  434. package/dist/resolution/frameworks/java.js +504 -0
  435. package/dist/resolution/frameworks/java.js.map +1 -0
  436. package/dist/resolution/frameworks/laravel.d.ts +13 -0
  437. package/dist/resolution/frameworks/laravel.d.ts.map +1 -0
  438. package/dist/resolution/frameworks/laravel.js +257 -0
  439. package/dist/resolution/frameworks/laravel.js.map +1 -0
  440. package/dist/resolution/frameworks/nestjs.d.ts +26 -0
  441. package/dist/resolution/frameworks/nestjs.d.ts.map +1 -0
  442. package/dist/resolution/frameworks/nestjs.js +698 -0
  443. package/dist/resolution/frameworks/nestjs.js.map +1 -0
  444. package/dist/resolution/frameworks/play.d.ts +19 -0
  445. package/dist/resolution/frameworks/play.d.ts.map +1 -0
  446. package/dist/resolution/frameworks/play.js +111 -0
  447. package/dist/resolution/frameworks/play.js.map +1 -0
  448. package/dist/resolution/frameworks/python.d.ts +10 -0
  449. package/dist/resolution/frameworks/python.d.ts.map +1 -0
  450. package/dist/resolution/frameworks/python.js +396 -0
  451. package/dist/resolution/frameworks/python.js.map +1 -0
  452. package/dist/resolution/frameworks/react-native.d.ts +3 -0
  453. package/dist/resolution/frameworks/react-native.d.ts.map +1 -0
  454. package/dist/resolution/frameworks/react-native.js +360 -0
  455. package/dist/resolution/frameworks/react-native.js.map +1 -0
  456. package/dist/resolution/frameworks/react.d.ts +8 -0
  457. package/dist/resolution/frameworks/react.d.ts.map +1 -0
  458. package/dist/resolution/frameworks/react.js +365 -0
  459. package/dist/resolution/frameworks/react.js.map +1 -0
  460. package/dist/resolution/frameworks/ruby.d.ts +8 -0
  461. package/dist/resolution/frameworks/ruby.d.ts.map +1 -0
  462. package/dist/resolution/frameworks/ruby.js +302 -0
  463. package/dist/resolution/frameworks/ruby.js.map +1 -0
  464. package/dist/resolution/frameworks/rust.d.ts +8 -0
  465. package/dist/resolution/frameworks/rust.d.ts.map +1 -0
  466. package/dist/resolution/frameworks/rust.js +304 -0
  467. package/dist/resolution/frameworks/rust.js.map +1 -0
  468. package/dist/resolution/frameworks/svelte.d.ts +9 -0
  469. package/dist/resolution/frameworks/svelte.d.ts.map +1 -0
  470. package/dist/resolution/frameworks/svelte.js +249 -0
  471. package/dist/resolution/frameworks/svelte.js.map +1 -0
  472. package/dist/resolution/frameworks/swift-objc.d.ts +37 -0
  473. package/dist/resolution/frameworks/swift-objc.d.ts.map +1 -0
  474. package/dist/resolution/frameworks/swift-objc.js +252 -0
  475. package/dist/resolution/frameworks/swift-objc.js.map +1 -0
  476. package/dist/resolution/frameworks/swift.d.ts +10 -0
  477. package/dist/resolution/frameworks/swift.d.ts.map +1 -0
  478. package/dist/resolution/frameworks/swift.js +400 -0
  479. package/dist/resolution/frameworks/swift.js.map +1 -0
  480. package/dist/resolution/frameworks/vue.d.ts +9 -0
  481. package/dist/resolution/frameworks/vue.d.ts.map +1 -0
  482. package/dist/resolution/frameworks/vue.js +306 -0
  483. package/dist/resolution/frameworks/vue.js.map +1 -0
  484. package/dist/resolution/go-module.d.ts +26 -0
  485. package/dist/resolution/go-module.d.ts.map +1 -0
  486. package/dist/resolution/go-module.js +78 -0
  487. package/dist/resolution/go-module.js.map +1 -0
  488. package/dist/resolution/import-resolver.d.ts +68 -0
  489. package/dist/resolution/import-resolver.d.ts.map +1 -0
  490. package/dist/resolution/import-resolver.js +1275 -0
  491. package/dist/resolution/import-resolver.js.map +1 -0
  492. package/dist/resolution/index.d.ts +117 -0
  493. package/dist/resolution/index.d.ts.map +1 -0
  494. package/dist/resolution/index.js +895 -0
  495. package/dist/resolution/index.js.map +1 -0
  496. package/dist/resolution/lru-cache.d.ts +24 -0
  497. package/dist/resolution/lru-cache.d.ts.map +1 -0
  498. package/dist/resolution/lru-cache.js +62 -0
  499. package/dist/resolution/lru-cache.js.map +1 -0
  500. package/dist/resolution/name-matcher.d.ts +32 -0
  501. package/dist/resolution/name-matcher.d.ts.map +1 -0
  502. package/dist/resolution/name-matcher.js +596 -0
  503. package/dist/resolution/name-matcher.js.map +1 -0
  504. package/dist/resolution/path-aliases.d.ts +68 -0
  505. package/dist/resolution/path-aliases.d.ts.map +1 -0
  506. package/dist/resolution/path-aliases.js +238 -0
  507. package/dist/resolution/path-aliases.js.map +1 -0
  508. package/dist/resolution/spec-link-resolver.d.ts +148 -0
  509. package/dist/resolution/spec-link-resolver.d.ts.map +1 -0
  510. package/dist/resolution/spec-link-resolver.js +337 -0
  511. package/dist/resolution/spec-link-resolver.js.map +1 -0
  512. package/dist/resolution/strip-comments.d.ts +27 -0
  513. package/dist/resolution/strip-comments.d.ts.map +1 -0
  514. package/dist/resolution/strip-comments.js +441 -0
  515. package/dist/resolution/strip-comments.js.map +1 -0
  516. package/dist/resolution/swift-objc-bridge.d.ts +134 -0
  517. package/dist/resolution/swift-objc-bridge.d.ts.map +1 -0
  518. package/dist/resolution/swift-objc-bridge.js +256 -0
  519. package/dist/resolution/swift-objc-bridge.js.map +1 -0
  520. package/dist/resolution/types.d.ts +216 -0
  521. package/dist/resolution/types.d.ts.map +1 -0
  522. package/dist/resolution/types.js +8 -0
  523. package/dist/resolution/types.js.map +1 -0
  524. package/dist/resolution/workspace-packages.d.ts +48 -0
  525. package/dist/resolution/workspace-packages.d.ts.map +1 -0
  526. package/dist/resolution/workspace-packages.js +208 -0
  527. package/dist/resolution/workspace-packages.js.map +1 -0
  528. package/dist/search/query-parser.d.ts +57 -0
  529. package/dist/search/query-parser.d.ts.map +1 -0
  530. package/dist/search/query-parser.js +177 -0
  531. package/dist/search/query-parser.js.map +1 -0
  532. package/dist/search/query-utils.d.ts +71 -0
  533. package/dist/search/query-utils.d.ts.map +1 -0
  534. package/dist/search/query-utils.js +380 -0
  535. package/dist/search/query-utils.js.map +1 -0
  536. package/dist/server/cli.js +152 -0
  537. package/dist/server/index.js +12 -0
  538. package/dist/server/ingest/impact-backfill.js +69 -0
  539. package/dist/server/ingest/impact-query.js +343 -0
  540. package/dist/server/ingest/index.js +19 -0
  541. package/dist/server/ingest/ingestor.js +541 -0
  542. package/dist/server/ingest/parser.js +104 -0
  543. package/dist/server/ingest/pricing.js +78 -0
  544. package/dist/server/ingest/specship-classify.js +153 -0
  545. package/dist/server/ingest/types.js +9 -0
  546. package/dist/server/ingest/watcher.js +77 -0
  547. package/dist/server/package.json +3 -0
  548. package/dist/server/project-registry.js +101 -0
  549. package/dist/server/routes/claude.js +907 -0
  550. package/dist/server/routes/domain.js +0 -0
  551. package/dist/server/routes/events.js +134 -0
  552. package/dist/server/routes/graph.js +248 -0
  553. package/dist/server/routes/maintainability.js +18 -0
  554. package/dist/server/routes/memory.js +272 -0
  555. package/dist/server/routes/projects.js +197 -0
  556. package/dist/server/routes/reflect.js +93 -0
  557. package/dist/server/routes/spec.js +373 -0
  558. package/dist/server/routes/status.js +112 -0
  559. package/dist/server/routes/workflow.js +253 -0
  560. package/dist/server/server.js +238 -0
  561. package/dist/server/static-handler.js +87 -0
  562. package/dist/statusline/active-run.d.ts +16 -0
  563. package/dist/statusline/active-run.d.ts.map +1 -0
  564. package/dist/statusline/active-run.js +73 -0
  565. package/dist/statusline/active-run.js.map +1 -0
  566. package/dist/statusline/cache.d.ts +21 -0
  567. package/dist/statusline/cache.d.ts.map +1 -0
  568. package/dist/statusline/cache.js +34 -0
  569. package/dist/statusline/cache.js.map +1 -0
  570. package/dist/statusline/index.d.ts +24 -0
  571. package/dist/statusline/index.d.ts.map +1 -0
  572. package/dist/statusline/index.js +128 -0
  573. package/dist/statusline/index.js.map +1 -0
  574. package/dist/statusline/paths.d.ts +27 -0
  575. package/dist/statusline/paths.d.ts.map +1 -0
  576. package/dist/statusline/paths.js +101 -0
  577. package/dist/statusline/paths.js.map +1 -0
  578. package/dist/statusline/render.d.ts +25 -0
  579. package/dist/statusline/render.d.ts.map +1 -0
  580. package/dist/statusline/render.js +72 -0
  581. package/dist/statusline/render.js.map +1 -0
  582. package/dist/statusline/session-marker.d.ts +30 -0
  583. package/dist/statusline/session-marker.d.ts.map +1 -0
  584. package/dist/statusline/session-marker.js +71 -0
  585. package/dist/statusline/session-marker.js.map +1 -0
  586. package/dist/statusline/types.d.ts +67 -0
  587. package/dist/statusline/types.d.ts.map +1 -0
  588. package/dist/statusline/types.js +16 -0
  589. package/dist/statusline/types.js.map +1 -0
  590. package/dist/sync/git-hooks.d.ts +45 -0
  591. package/dist/sync/git-hooks.d.ts.map +1 -0
  592. package/dist/sync/git-hooks.js +225 -0
  593. package/dist/sync/git-hooks.js.map +1 -0
  594. package/dist/sync/index.d.ts +19 -0
  595. package/dist/sync/index.d.ts.map +1 -0
  596. package/dist/sync/index.js +35 -0
  597. package/dist/sync/index.js.map +1 -0
  598. package/dist/sync/watch-policy.d.ts +48 -0
  599. package/dist/sync/watch-policy.d.ts.map +1 -0
  600. package/dist/sync/watch-policy.js +124 -0
  601. package/dist/sync/watch-policy.js.map +1 -0
  602. package/dist/sync/watcher.d.ts +283 -0
  603. package/dist/sync/watcher.d.ts.map +1 -0
  604. package/dist/sync/watcher.js +606 -0
  605. package/dist/sync/watcher.js.map +1 -0
  606. package/dist/sync/worktree.d.ts +54 -0
  607. package/dist/sync/worktree.d.ts.map +1 -0
  608. package/dist/sync/worktree.js +137 -0
  609. package/dist/sync/worktree.js.map +1 -0
  610. package/dist/types.d.ts +625 -0
  611. package/dist/types.d.ts.map +1 -0
  612. package/dist/types.js +118 -0
  613. package/dist/types.js.map +1 -0
  614. package/dist/ui/glyphs.d.ts +42 -0
  615. package/dist/ui/glyphs.d.ts.map +1 -0
  616. package/dist/ui/glyphs.js +78 -0
  617. package/dist/ui/glyphs.js.map +1 -0
  618. package/dist/ui/shimmer-progress.d.ts +11 -0
  619. package/dist/ui/shimmer-progress.d.ts.map +1 -0
  620. package/dist/ui/shimmer-progress.js +90 -0
  621. package/dist/ui/shimmer-progress.js.map +1 -0
  622. package/dist/ui/shimmer-worker.d.ts +2 -0
  623. package/dist/ui/shimmer-worker.d.ts.map +1 -0
  624. package/dist/ui/shimmer-worker.js +118 -0
  625. package/dist/ui/shimmer-worker.js.map +1 -0
  626. package/dist/ui/types.d.ts +17 -0
  627. package/dist/ui/types.d.ts.map +1 -0
  628. package/dist/ui/types.js +3 -0
  629. package/dist/ui/types.js.map +1 -0
  630. package/dist/utils.d.ts +205 -0
  631. package/dist/utils.d.ts.map +1 -0
  632. package/dist/utils.js +549 -0
  633. package/dist/utils.js.map +1 -0
  634. package/dist/web/chunk-2AJCHB7P.js +1 -0
  635. package/dist/web/chunk-2CPLUFCH.js +2 -0
  636. package/dist/web/chunk-2I7L37NS.js +1 -0
  637. package/dist/web/chunk-2NAWAJB5.js +1 -0
  638. package/dist/web/chunk-2OJBIPE4.js +1 -0
  639. package/dist/web/chunk-372AYXK6.js +17 -0
  640. package/dist/web/chunk-3E2WB6D5.js +1 -0
  641. package/dist/web/chunk-3EBFYSCH.js +2 -0
  642. package/dist/web/chunk-3QCQ4BXS.js +1 -0
  643. package/dist/web/chunk-42XVAQ6I.js +1 -0
  644. package/dist/web/chunk-4IMMPEYM.js +1 -0
  645. package/dist/web/chunk-4JYHAP7B.js +1 -0
  646. package/dist/web/chunk-4TJQJPCZ.js +1 -0
  647. package/dist/web/chunk-4WZIHTPC.js +1 -0
  648. package/dist/web/chunk-4YVSYOSD.js +1 -0
  649. package/dist/web/chunk-52PO6IMB.js +2 -0
  650. package/dist/web/chunk-54D6RFSW.js +1 -0
  651. package/dist/web/chunk-5BQIOYKW.js +1 -0
  652. package/dist/web/chunk-5HGWHUJA.js +1 -0
  653. package/dist/web/chunk-5XRUOPZE.js +1 -0
  654. package/dist/web/chunk-5Y244R4G.js +1 -0
  655. package/dist/web/chunk-6O7Z3P2M.js +1 -0
  656. package/dist/web/chunk-6QXULGLG.js +1 -0
  657. package/dist/web/chunk-6RRDPT5Z.js +1 -0
  658. package/dist/web/chunk-6VKB2ZWM.js +1 -0
  659. package/dist/web/chunk-7DMFVTU4.js +1 -0
  660. package/dist/web/chunk-7P5CVBJZ.js +1 -0
  661. package/dist/web/chunk-7SMPKVEP.js +1 -0
  662. package/dist/web/chunk-AHLX543M.js +1 -0
  663. package/dist/web/chunk-AMGJBO7D.js +3 -0
  664. package/dist/web/chunk-AZJVTPLU.js +1 -0
  665. package/dist/web/chunk-B3CWIVBW.js +1 -0
  666. package/dist/web/chunk-BLBRMCN2.js +1 -0
  667. package/dist/web/chunk-BMIAXD2V.js +2 -0
  668. package/dist/web/chunk-BPCJLNBS.js +47 -0
  669. package/dist/web/chunk-BRHEUDLY.js +6 -0
  670. package/dist/web/chunk-BUXWEHIY.js +1 -0
  671. package/dist/web/chunk-CD5IZM7Y.js +1 -0
  672. package/dist/web/chunk-DLQPZWSI.css +1 -0
  673. package/dist/web/chunk-DSGNOCKQ.js +1 -0
  674. package/dist/web/chunk-DT5LJYFX.js +1 -0
  675. package/dist/web/chunk-DYRFLPJA.js +1 -0
  676. package/dist/web/chunk-E3J3CXR5.js +1 -0
  677. package/dist/web/chunk-E73OX2P7.js +1 -0
  678. package/dist/web/chunk-EAXRKDLV.js +1 -0
  679. package/dist/web/chunk-EBKKDHYI.js +1 -0
  680. package/dist/web/chunk-EE7V7Q5P.js +1 -0
  681. package/dist/web/chunk-EKY2FUHU.js +1 -0
  682. package/dist/web/chunk-EP6XOPXH.js +1 -0
  683. package/dist/web/chunk-ESGDLJOJ.js +1 -0
  684. package/dist/web/chunk-ETJG7NCY.js +1 -0
  685. package/dist/web/chunk-EUUEFEDI.js +1 -0
  686. package/dist/web/chunk-EX4ZHR4F.js +1 -0
  687. package/dist/web/chunk-F5UNCSXP.js +1 -0
  688. package/dist/web/chunk-FFGJXUHI.js +1 -0
  689. package/dist/web/chunk-FGNZDHTL.js +11 -0
  690. package/dist/web/chunk-FIJW2UNJ.js +1 -0
  691. package/dist/web/chunk-FMV5PXRC.js +5 -0
  692. package/dist/web/chunk-G7VZT5KB.js +3 -0
  693. package/dist/web/chunk-GCOM4JPR.js +2 -0
  694. package/dist/web/chunk-GEIIDO6C.js +1 -0
  695. package/dist/web/chunk-GRZYXPSO.js +7 -0
  696. package/dist/web/chunk-GWBABPZ5.js +1 -0
  697. package/dist/web/chunk-GYGPS3AN.js +1 -0
  698. package/dist/web/chunk-H4GLRD3Q.js +1 -0
  699. package/dist/web/chunk-H5TWEFYX.js +1 -0
  700. package/dist/web/chunk-H7AF7YS4.js +1 -0
  701. package/dist/web/chunk-HCB2N2KH.js +1 -0
  702. package/dist/web/chunk-HDZDQILN.js +1 -0
  703. package/dist/web/chunk-HMK6UO6N.js +1 -0
  704. package/dist/web/chunk-HVVXPI4D.js +1 -0
  705. package/dist/web/chunk-IHEE5NYJ.js +1 -0
  706. package/dist/web/chunk-IPB746BT.js +1 -0
  707. package/dist/web/chunk-ISNEBICW.js +1 -0
  708. package/dist/web/chunk-J2GZVLHH.js +1 -0
  709. package/dist/web/chunk-JTFXTIPE.js +903 -0
  710. package/dist/web/chunk-KHU5M2AL.js +1 -0
  711. package/dist/web/chunk-KW3DHCFV.js +1 -0
  712. package/dist/web/chunk-LB6JPLX2.js +1 -0
  713. package/dist/web/chunk-LBXLFPVN.js +1 -0
  714. package/dist/web/chunk-LGNSHRCE.js +1 -0
  715. package/dist/web/chunk-LNSVDHCI.js +1 -0
  716. package/dist/web/chunk-LVGIY3SO.js +1 -0
  717. package/dist/web/chunk-LXLHIHEN.js +1 -0
  718. package/dist/web/chunk-MFHO2F2U.js +4 -0
  719. package/dist/web/chunk-N5OSSQFZ.js +1 -0
  720. package/dist/web/chunk-N6SS4G6S.js +1 -0
  721. package/dist/web/chunk-NAJYJNHS.js +1 -0
  722. package/dist/web/chunk-NHD66NOI.js +1 -0
  723. package/dist/web/chunk-NNLJ55MY.js +1 -0
  724. package/dist/web/chunk-NTBJG6SJ.js +1 -0
  725. package/dist/web/chunk-NUDB3Q2Y.js +3 -0
  726. package/dist/web/chunk-OM7JVWQQ.js +1 -0
  727. package/dist/web/chunk-OXEF5E3E.js +1 -0
  728. package/dist/web/chunk-PGGJPDJG.js +1 -0
  729. package/dist/web/chunk-PUYSJNJR.js +1 -0
  730. package/dist/web/chunk-Q2RVFS45.js +1 -0
  731. package/dist/web/chunk-Q7L6LLAK.js +1 -0
  732. package/dist/web/chunk-QCMKJIWY.js +1 -0
  733. package/dist/web/chunk-QEQRY4QQ.js +1 -0
  734. package/dist/web/chunk-QH6CF3M3.js +1 -0
  735. package/dist/web/chunk-QQ5LD7PI.js +1 -0
  736. package/dist/web/chunk-QR6L3KAC.js +1 -0
  737. package/dist/web/chunk-QXJS6F3U.js +1 -0
  738. package/dist/web/chunk-R2DLK4HO.js +1 -0
  739. package/dist/web/chunk-RD6TVPOT.js +1 -0
  740. package/dist/web/chunk-RKY4EJYJ.js +1 -0
  741. package/dist/web/chunk-RONYWVY7.js +1 -0
  742. package/dist/web/chunk-RSZZWGGC.js +1 -0
  743. package/dist/web/chunk-RXKXYF2C.js +1 -0
  744. package/dist/web/chunk-SCNDZRN2.js +1 -0
  745. package/dist/web/chunk-SH6UVHQC.js +1 -0
  746. package/dist/web/chunk-SWKJRNYY.js +1 -0
  747. package/dist/web/chunk-T7AZ65JP.js +1 -0
  748. package/dist/web/chunk-TCZDVOHD.js +1 -0
  749. package/dist/web/chunk-TF5TF6IP.js +1 -0
  750. package/dist/web/chunk-TPTITA3V.js +1 -0
  751. package/dist/web/chunk-TR335633.js +1 -0
  752. package/dist/web/chunk-UR5KDXPX.js +1 -0
  753. package/dist/web/chunk-UR6O2GEH.js +1 -0
  754. package/dist/web/chunk-UTNMGWTP.js +1 -0
  755. package/dist/web/chunk-VECWMHJP.js +1 -0
  756. package/dist/web/chunk-VUACT35R.js +3 -0
  757. package/dist/web/chunk-VZI7H4SZ.js +1 -0
  758. package/dist/web/chunk-W22AVG3N.js +1 -0
  759. package/dist/web/chunk-W6NGHRHX.js +1 -0
  760. package/dist/web/chunk-WB6YHOD4.js +1 -0
  761. package/dist/web/chunk-WBT64AWV.js +1 -0
  762. package/dist/web/chunk-WFXJIXZE.js +4 -0
  763. package/dist/web/chunk-WTGYRH3Z.js +298 -0
  764. package/dist/web/chunk-WXTCVDTP.js +1 -0
  765. package/dist/web/chunk-XCDHWLVH.js +1 -0
  766. package/dist/web/chunk-Y3H6FFUZ.js +1 -0
  767. package/dist/web/chunk-Y4F5ULGJ.js +1 -0
  768. package/dist/web/chunk-YEGKAAEE.js +1 -0
  769. package/dist/web/chunk-YM2KU57F.js +1 -0
  770. package/dist/web/chunk-YRERBP6T.js +1 -0
  771. package/dist/web/chunk-ZLV4VCDG.js +3 -0
  772. package/dist/web/chunk-ZQUJMA5K.js +4 -0
  773. package/dist/web/chunk-ZTVI5KFF.js +1 -0
  774. package/dist/web/favicon-16.png +0 -0
  775. package/dist/web/favicon-180.png +0 -0
  776. package/dist/web/favicon-32.png +0 -0
  777. package/dist/web/favicon-512.png +0 -0
  778. package/dist/web/favicon-small.svg +15 -0
  779. package/dist/web/favicon.ico +0 -0
  780. package/dist/web/favicon.svg +20 -0
  781. package/dist/web/icon-192.png +0 -0
  782. package/dist/web/icon-512.png +0 -0
  783. package/dist/web/index.html +146 -0
  784. package/dist/web/main-ESADRXN2.css +1 -0
  785. package/dist/web/main-SQFUMVQA.js +1 -0
  786. package/dist/web/manifest.webmanifest +15 -0
  787. package/dist/web/media/codicon-LN6W7LCM.ttf +0 -0
  788. package/dist/web/styles-KSOPUVDA.css +1 -0
  789. package/dist/web/sw.js +69 -0
  790. package/dist/workflows/condition-evaluator.d.ts +75 -0
  791. package/dist/workflows/condition-evaluator.d.ts.map +1 -0
  792. package/dist/workflows/condition-evaluator.js +282 -0
  793. package/dist/workflows/condition-evaluator.js.map +1 -0
  794. package/dist/workflows/defaults/claude-design-implement.yaml +336 -0
  795. package/dist/workflows/defaults/index.d.ts +26 -0
  796. package/dist/workflows/defaults/index.d.ts.map +1 -0
  797. package/dist/workflows/defaults/index.js +94 -0
  798. package/dist/workflows/defaults/index.js.map +1 -0
  799. package/dist/workflows/defaults/spec-author.yaml +214 -0
  800. package/dist/workflows/defaults/spec-fix.yaml +110 -0
  801. package/dist/workflows/defaults/spec-implement.yaml +150 -0
  802. package/dist/workflows/defaults/spec-relink.yaml +81 -0
  803. package/dist/workflows/defaults/spec-verify.yaml +51 -0
  804. package/dist/workflows/discovery.d.ts +46 -0
  805. package/dist/workflows/discovery.d.ts.map +1 -0
  806. package/dist/workflows/discovery.js +193 -0
  807. package/dist/workflows/discovery.js.map +1 -0
  808. package/dist/workflows/executor.d.ts +98 -0
  809. package/dist/workflows/executor.d.ts.map +1 -0
  810. package/dist/workflows/executor.js +664 -0
  811. package/dist/workflows/executor.js.map +1 -0
  812. package/dist/workflows/runners/approval.d.ts +18 -0
  813. package/dist/workflows/runners/approval.d.ts.map +1 -0
  814. package/dist/workflows/runners/approval.js +34 -0
  815. package/dist/workflows/runners/approval.js.map +1 -0
  816. package/dist/workflows/runners/bash.d.ts +13 -0
  817. package/dist/workflows/runners/bash.d.ts.map +1 -0
  818. package/dist/workflows/runners/bash.js +143 -0
  819. package/dist/workflows/runners/bash.js.map +1 -0
  820. package/dist/workflows/runners/cancel.d.ts +10 -0
  821. package/dist/workflows/runners/cancel.d.ts.map +1 -0
  822. package/dist/workflows/runners/cancel.js +19 -0
  823. package/dist/workflows/runners/cancel.js.map +1 -0
  824. package/dist/workflows/runners/prompt.d.ts +51 -0
  825. package/dist/workflows/runners/prompt.d.ts.map +1 -0
  826. package/dist/workflows/runners/prompt.js +306 -0
  827. package/dist/workflows/runners/prompt.js.map +1 -0
  828. package/dist/workflows/runners/script.d.ts +17 -0
  829. package/dist/workflows/runners/script.d.ts.map +1 -0
  830. package/dist/workflows/runners/script.js +155 -0
  831. package/dist/workflows/runners/script.js.map +1 -0
  832. package/dist/workflows/runners/types.d.ts +57 -0
  833. package/dist/workflows/runners/types.d.ts.map +1 -0
  834. package/dist/workflows/runners/types.js +13 -0
  835. package/dist/workflows/runners/types.js.map +1 -0
  836. package/dist/workflows/schemas/workflow.d.ts +166 -0
  837. package/dist/workflows/schemas/workflow.d.ts.map +1 -0
  838. package/dist/workflows/schemas/workflow.js +437 -0
  839. package/dist/workflows/schemas/workflow.js.map +1 -0
  840. package/hooks/hooks.json +38 -0
  841. package/package.json +78 -0
  842. package/scripts/add-lang/bench.sh +60 -0
  843. package/scripts/add-lang/check-grammar.mjs +75 -0
  844. package/scripts/add-lang/dump-ast.mjs +103 -0
  845. package/scripts/add-lang/verify-extraction.mjs +70 -0
  846. package/scripts/agent-eval/arms-F.sh +21 -0
  847. package/scripts/agent-eval/arms-matrix.sh +37 -0
  848. package/scripts/agent-eval/audit.sh +68 -0
  849. package/scripts/agent-eval/bench-readme.sh +28 -0
  850. package/scripts/agent-eval/bench-why-repo.sh +22 -0
  851. package/scripts/agent-eval/block-read-hook.sh +19 -0
  852. package/scripts/agent-eval/hook-settings.json +15 -0
  853. package/scripts/agent-eval/itrun.sh +120 -0
  854. package/scripts/agent-eval/parse-arms.mjs +116 -0
  855. package/scripts/agent-eval/parse-bench-readme.mjs +84 -0
  856. package/scripts/agent-eval/parse-run.mjs +45 -0
  857. package/scripts/agent-eval/parse-session.mjs +93 -0
  858. package/scripts/agent-eval/probe-context.mjs +21 -0
  859. package/scripts/agent-eval/probe-explore.mjs +40 -0
  860. package/scripts/agent-eval/probe-node.mjs +20 -0
  861. package/scripts/agent-eval/probe-sweep.mjs +119 -0
  862. package/scripts/agent-eval/probe-trace.mjs +20 -0
  863. package/scripts/agent-eval/run-agent.sh +34 -0
  864. package/scripts/agent-eval/run-all.sh +67 -0
  865. package/scripts/agent-eval/run-arms.sh +56 -0
  866. package/scripts/agent-eval/seq-matrix.mjs +137 -0
  867. package/scripts/build-bundle.sh +118 -0
  868. package/scripts/build-server-bundle.mjs +80 -0
  869. package/scripts/build-web-bundle.mjs +66 -0
  870. package/scripts/extract-release-notes.mjs +130 -0
  871. package/scripts/local-install.sh +41 -0
  872. package/scripts/npm-sdk.js +75 -0
  873. package/scripts/npm-shim.js +246 -0
  874. package/scripts/offline-install.ps1 +148 -0
  875. package/scripts/offline-install.sh +149 -0
  876. package/scripts/pack-npm.sh +119 -0
  877. package/scripts/prepare-release.mjs +270 -0
  878. package/scripts/sync-shim-version.mjs +64 -0
  879. package/selectors.json +41 -0
@@ -0,0 +1,2823 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * SpecShip CLI
5
+ *
6
+ * Command-line interface for SpecShip code intelligence.
7
+ *
8
+ * Usage:
9
+ * specship Run interactive installer (when no args)
10
+ * specship install Run interactive installer
11
+ * specship uninstall Remove SpecShip from your agents
12
+ * specship init [path] Initialize SpecShip in a project
13
+ * specship uninit [path] Remove SpecShip from a project
14
+ * specship index [path] Index all files in the project
15
+ * specship sync [path] Sync changes since last index
16
+ * specship status [path] Show index status
17
+ * specship query <search> Search for symbols
18
+ * specship files [options] Show project file structure
19
+ * specship context <task> Build context for a task
20
+ * specship callers <symbol> Find what calls a function/method
21
+ * specship callees <symbol> Find what a function/method calls
22
+ * specship impact <symbol> Analyze what code is affected by changing a symbol
23
+ * specship affected [files] Find test files affected by changes
24
+ */
25
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
26
+ if (k2 === undefined) k2 = k;
27
+ var desc = Object.getOwnPropertyDescriptor(m, k);
28
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
29
+ desc = { enumerable: true, get: function() { return m[k]; } };
30
+ }
31
+ Object.defineProperty(o, k2, desc);
32
+ }) : (function(o, m, k, k2) {
33
+ if (k2 === undefined) k2 = k;
34
+ o[k2] = m[k];
35
+ }));
36
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
37
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
38
+ }) : function(o, v) {
39
+ o["default"] = v;
40
+ });
41
+ var __importStar = (this && this.__importStar) || (function () {
42
+ var ownKeys = function(o) {
43
+ ownKeys = Object.getOwnPropertyNames || function (o) {
44
+ var ar = [];
45
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
46
+ return ar;
47
+ };
48
+ return ownKeys(o);
49
+ };
50
+ return function (mod) {
51
+ if (mod && mod.__esModule) return mod;
52
+ var result = {};
53
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
54
+ __setModuleDefault(result, mod);
55
+ return result;
56
+ };
57
+ })();
58
+ Object.defineProperty(exports, "__esModule", { value: true });
59
+ const commander_1 = require("commander");
60
+ const path = __importStar(require("path"));
61
+ const fs = __importStar(require("fs"));
62
+ const os = __importStar(require("os"));
63
+ const directory_1 = require("../directory");
64
+ const worktree_1 = require("../sync/worktree");
65
+ const shimmer_progress_1 = require("../ui/shimmer-progress");
66
+ const glyphs_1 = require("../ui/glyphs");
67
+ const node_version_check_1 = require("./node-version-check");
68
+ const wasm_runtime_flags_1 = require("../extraction/wasm-runtime-flags");
69
+ // Lazy-load heavy modules (SpecShip, runInstaller) to keep CLI startup fast.
70
+ async function loadSpecShip() {
71
+ try {
72
+ return await Promise.resolve().then(() => __importStar(require('../index')));
73
+ }
74
+ catch (err) {
75
+ const msg = err instanceof Error ? err.message : String(err);
76
+ console.error(`\x1b[31m${(0, glyphs_1.getGlyphs)().err}\x1b[0m Failed to load SpecShip modules.`);
77
+ console.error(`\n Node: ${process.version} Platform: ${process.platform} ${process.arch}`);
78
+ console.error(`\n Error: ${msg}`);
79
+ console.error('\n Try reinstalling with: npm install -g @selvakumaresra/specship\n');
80
+ process.exit(1);
81
+ }
82
+ }
83
+ async function loadServerPackage() {
84
+ // Resolution order:
85
+ // 1. Bundled mode — `dist/server/index.js` next to this CLI. This is
86
+ // what every `npm i -g @selvakumaresra/specship` install hits, since
87
+ // the publish pipeline copies the compiled server into the root's
88
+ // `dist/`.
89
+ // 2. npm dep `@selvakumaresra/specship-server` if some downstream
90
+ // consumer ever wires it as a separate package (kept for forward
91
+ // compatibility — not the shipped path).
92
+ // 3. Dev/workspace sibling: `packages/server/dist/index.js` for
93
+ // running from a checkout without a prior `npm run build`.
94
+ const candidates = [
95
+ path.resolve(__dirname, '..', 'server', 'index.js'),
96
+ '@selvakumaresra/specship-server',
97
+ path.resolve(__dirname, '..', '..', 'packages', 'server', 'dist', 'index.js'),
98
+ path.resolve(__dirname, '..', '..', '..', 'packages', 'server', 'dist', 'index.js'),
99
+ ];
100
+ let lastErr = null;
101
+ for (const c of candidates) {
102
+ try {
103
+ return (await Promise.resolve(`${c}`).then(s => __importStar(require(s))));
104
+ }
105
+ catch (e) {
106
+ lastErr = e;
107
+ }
108
+ }
109
+ console.error(`\x1b[31m${(0, glyphs_1.getGlyphs)().err}\x1b[0m Could not load the bundled HTTP server. Re-run \`npm run build\` (or the bundled npm install is corrupt).`);
110
+ if (lastErr)
111
+ console.error(' ' + (lastErr instanceof Error ? lastErr.message : String(lastErr)));
112
+ process.exit(1);
113
+ }
114
+ /**
115
+ * The JSONL ingest watcher now ships inside `@selvakumaresra/specship-server`
116
+ * itself — `createServer({ ingest: true })` starts it. No separate package.
117
+ */
118
+ /**
119
+ * Auto-pick a default project for `specship serve --ui` when the user
120
+ * didn't pass `-p` and the cwd isn't initialized either. Walks
121
+ * `~/.claude/projects/*` (Claude Code's per-project transcript dirs),
122
+ * decodes each slug back to a real path, and returns the most-recently-
123
+ * touched one that's still on disk AND has been `specship init`'d.
124
+ *
125
+ * Returns null when nothing qualifies — the server boots projectless and
126
+ * the desktop UI prompts the user to pick.
127
+ */
128
+ async function pickRecentInitializedProject() {
129
+ const claudeRoot = path.join(os.homedir(), '.claude', 'projects');
130
+ let entries;
131
+ try {
132
+ entries = fs.readdirSync(claudeRoot, { withFileTypes: true });
133
+ }
134
+ catch {
135
+ return null;
136
+ }
137
+ const candidates = [];
138
+ for (const ent of entries) {
139
+ if (!ent.isDirectory())
140
+ continue;
141
+ const slug = ent.name;
142
+ // Same encoding the server's decodeProjectSlug uses: '-' → '/'.
143
+ const decoded = slug.startsWith('-') ? '/' + slug.slice(1).replace(/-/g, '/') : slug;
144
+ try {
145
+ const st = fs.statSync(decoded);
146
+ if (!st.isDirectory())
147
+ continue;
148
+ }
149
+ catch {
150
+ continue;
151
+ }
152
+ if (!(0, directory_1.isInitialized)(decoded))
153
+ continue;
154
+ // Use the newest .jsonl mtime in the slug dir as the activity signal.
155
+ let lastTouched = 0;
156
+ try {
157
+ const files = fs.readdirSync(path.join(claudeRoot, slug), { withFileTypes: true });
158
+ for (const f of files) {
159
+ if (!f.isFile() || !f.name.toLowerCase().endsWith('.jsonl'))
160
+ continue;
161
+ try {
162
+ const st = fs.statSync(path.join(claudeRoot, slug, f.name));
163
+ if (st.mtimeMs > lastTouched)
164
+ lastTouched = st.mtimeMs;
165
+ }
166
+ catch { /* ignore */ }
167
+ }
168
+ }
169
+ catch { /* ignore */ }
170
+ candidates.push({ decoded, lastTouched });
171
+ }
172
+ candidates.sort((a, b) => b.lastTouched - a.lastTouched);
173
+ return candidates[0]?.decoded ?? null;
174
+ }
175
+ /**
176
+ * Locate the built Angular UI for `specship serve --ui`. Mirrors the
177
+ * lookup order in `packages/server/src/cli.ts:locateWebDir` so both entry
178
+ * points behave identically. Returns null when nothing is found — the
179
+ * server then runs API-only.
180
+ */
181
+ function locateWebDir(explicit) {
182
+ const envDir = process.env.SPECSHIP_WEB_DIR ?? process.env.SPECSHIP_WEB_DIR;
183
+ const candidates = [];
184
+ if (explicit)
185
+ candidates.push(path.resolve(explicit));
186
+ if (envDir)
187
+ candidates.push(path.resolve(envDir));
188
+ // 1. Bundled mode — `dist/web/` next to this CLI. This is what every
189
+ // `npm i -g @selvakumaresra/specship` install hits.
190
+ candidates.push(path.resolve(__dirname, '..', 'web'));
191
+ // 2. Legacy / forward-compat: the old `@selvakumaresra/specship-server`
192
+ // used to ship the SPA at <pkg>/public/web.
193
+ try {
194
+ const pkgJson = require.resolve('@selvakumaresra/specship-server/package.json');
195
+ candidates.push(path.resolve(path.dirname(pkgJson), 'public', 'web'));
196
+ }
197
+ catch { /* not installed as separate package — fall through */ }
198
+ // 3. Dev monorepo layouts (running from a checkout).
199
+ candidates.push(path.resolve(__dirname, '..', '..', 'packages', 'server', 'public', 'web'));
200
+ candidates.push(path.resolve(__dirname, '..', '..', '..', 'packages', 'server', 'public', 'web'));
201
+ candidates.push(path.resolve(__dirname, '..', '..', 'packages', 'web-ng', 'dist', 'web-ng', 'browser'));
202
+ candidates.push(path.resolve(__dirname, '..', '..', '..', 'packages', 'web-ng', 'dist', 'web-ng', 'browser'));
203
+ // 4. Walk up from cwd in case the bin is run inside a checkout.
204
+ let cur = process.cwd();
205
+ for (let depth = 0; depth < 6; depth++) {
206
+ candidates.push(path.join(cur, 'dist', 'web'));
207
+ candidates.push(path.join(cur, 'packages', 'server', 'public', 'web'));
208
+ candidates.push(path.join(cur, 'packages', 'web-ng', 'dist', 'web-ng', 'browser'));
209
+ const parent = path.dirname(cur);
210
+ if (parent === cur)
211
+ break;
212
+ cur = parent;
213
+ }
214
+ for (const c of candidates) {
215
+ try {
216
+ if (fs.existsSync(path.join(c, 'index.html')))
217
+ return c;
218
+ }
219
+ catch { /* ignore */ }
220
+ }
221
+ return null;
222
+ }
223
+ // Dynamic import helper — tsc compiles import() to require() in CJS mode,
224
+ // which fails for ESM-only packages. This bypasses the transformation.
225
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
226
+ const importESM = new Function('specifier', 'return import(specifier)');
227
+ // Block SpecShip on Node.js 25.x — V8's turboshaft WASM JIT has a Zone
228
+ // allocator bug that reliably crashes when compiling tree-sitter
229
+ // grammars (see #54, #81, #140). The previous behaviour was a soft
230
+ // console.warn that scrolls off-screen before the OOM crash 30 seconds
231
+ // later, leading to a steady stream of "what is this OOM" reports.
232
+ // Hard-exit before any WASM work; allow override via env var for users
233
+ // who patched V8 themselves or want to test a future fix.
234
+ const nodeVersion = process.versions.node;
235
+ const nodeMajor = parseInt(nodeVersion.split('.')[0] ?? '0', 10);
236
+ if (nodeMajor >= 25) {
237
+ process.stderr.write((0, node_version_check_1.buildNode25BlockBanner)(nodeVersion) + '\n');
238
+ if (!process.env.SPECSHIP_ALLOW_UNSAFE_NODE) {
239
+ process.exit(1);
240
+ }
241
+ // Override active — banner shown for visibility, continuing.
242
+ }
243
+ // Enforce the supported Node floor. `engines` in package.json only *warns* on
244
+ // install (unless engine-strict), so hard-block here to actually keep users off
245
+ // unsupported versions. Mirrors the 25+ block above. See package.json `engines`.
246
+ if (nodeMajor < node_version_check_1.MIN_NODE_MAJOR) {
247
+ process.stderr.write((0, node_version_check_1.buildNodeTooOldBanner)(nodeVersion) + '\n');
248
+ if (!process.env.SPECSHIP_ALLOW_UNSAFE_NODE) {
249
+ process.exit(1);
250
+ }
251
+ // Override active — banner shown for visibility, continuing.
252
+ }
253
+ // Re-exec with V8's `--liftoff-only` if it isn't already set, so tree-sitter's
254
+ // large WASM grammars never hit the turboshaft Zone OOM (`Fatal process out of
255
+ // memory: Zone`) on Node >= 22. No-op under the bundled launcher, which already
256
+ // passes the flag. Must run before any grammar (in the parse worker, which
257
+ // inherits this process's flags) is compiled. See ../extraction/wasm-runtime-flags.
258
+ (0, wasm_runtime_flags_1.relaunchWithWasmRuntimeFlagsIfNeeded)(__filename);
259
+ // Check if running with no arguments - run installer
260
+ if (process.argv.length === 2) {
261
+ Promise.resolve().then(() => __importStar(require('../installer'))).then(({ runInstaller }) => runInstaller()).catch((err) => {
262
+ console.error('Installation failed:', err instanceof Error ? err.message : String(err));
263
+ process.exit(1);
264
+ });
265
+ }
266
+ else {
267
+ // Normal CLI flow
268
+ main();
269
+ }
270
+ process.on('uncaughtException', (error) => {
271
+ console.error('[SpecShip] Uncaught exception:', error);
272
+ });
273
+ process.on('unhandledRejection', (reason) => {
274
+ console.error('[SpecShip] Unhandled rejection:', reason);
275
+ });
276
+ function main() {
277
+ const program = new commander_1.Command();
278
+ // Version from package.json
279
+ const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf-8'));
280
+ // =============================================================================
281
+ // ANSI Color Helpers (avoid chalk ESM issues)
282
+ // =============================================================================
283
+ const colors = {
284
+ reset: '\x1b[0m',
285
+ bold: '\x1b[1m',
286
+ dim: '\x1b[2m',
287
+ red: '\x1b[31m',
288
+ green: '\x1b[32m',
289
+ yellow: '\x1b[33m',
290
+ blue: '\x1b[34m',
291
+ cyan: '\x1b[36m',
292
+ white: '\x1b[37m',
293
+ gray: '\x1b[90m',
294
+ };
295
+ const chalk = {
296
+ bold: (s) => `${colors.bold}${s}${colors.reset}`,
297
+ dim: (s) => `${colors.dim}${s}${colors.reset}`,
298
+ red: (s) => `${colors.red}${s}${colors.reset}`,
299
+ green: (s) => `${colors.green}${s}${colors.reset}`,
300
+ yellow: (s) => `${colors.yellow}${s}${colors.reset}`,
301
+ blue: (s) => `${colors.blue}${s}${colors.reset}`,
302
+ cyan: (s) => `${colors.cyan}${s}${colors.reset}`,
303
+ white: (s) => `${colors.white}${s}${colors.reset}`,
304
+ gray: (s) => `${colors.gray}${s}${colors.reset}`,
305
+ };
306
+ program
307
+ .name('specship')
308
+ .description('Code intelligence and knowledge graph for any codebase')
309
+ .version(packageJson.version);
310
+ // =============================================================================
311
+ // Helper Functions
312
+ // =============================================================================
313
+ /**
314
+ * Resolve project path from argument or current directory
315
+ * Walks up parent directories to find nearest initialized SpecShip project
316
+ * (must have .specship/specship.db, not just .specship/lessons.db)
317
+ */
318
+ function resolveProjectPath(pathArg) {
319
+ const absolutePath = path.resolve(pathArg || process.cwd());
320
+ // If exact path is initialized (has specship.db), use it
321
+ if ((0, directory_1.isInitialized)(absolutePath)) {
322
+ return absolutePath;
323
+ }
324
+ // Walk up to find nearest parent with SpecShip initialized
325
+ // Note: findNearestSpecShipRoot finds any .specship folder, but we need one with specship.db
326
+ let current = absolutePath;
327
+ const root = path.parse(current).root;
328
+ while (current !== root) {
329
+ const parent = path.dirname(current);
330
+ if (parent === current)
331
+ break;
332
+ current = parent;
333
+ if ((0, directory_1.isInitialized)(current)) {
334
+ return current;
335
+ }
336
+ }
337
+ // Not found - return original path (will fail later with helpful error)
338
+ return absolutePath;
339
+ }
340
+ /**
341
+ * Format a number with commas
342
+ */
343
+ function formatNumber(n) {
344
+ return n.toLocaleString();
345
+ }
346
+ /**
347
+ * Format duration in milliseconds to human readable
348
+ */
349
+ function formatDuration(ms) {
350
+ if (ms < 1000) {
351
+ return `${ms}ms`;
352
+ }
353
+ const seconds = ms / 1000;
354
+ if (seconds < 60) {
355
+ return `${seconds.toFixed(1)}s`;
356
+ }
357
+ const minutes = Math.floor(seconds / 60);
358
+ const remainingSeconds = seconds % 60;
359
+ return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
360
+ }
361
+ // Shimmer progress renderer (runs in a worker thread for smooth animation)
362
+ // Imported at top of file from '../ui/shimmer-progress'
363
+ /**
364
+ * Create a plain-text progress callback for --verbose mode.
365
+ * No animations, no ANSI tricks — just timestamped lines to stdout.
366
+ */
367
+ function createVerboseProgress() {
368
+ let lastPhase = '';
369
+ let lastPct = -1;
370
+ const startTime = Date.now();
371
+ return (progress) => {
372
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
373
+ if (progress.phase !== lastPhase) {
374
+ lastPhase = progress.phase;
375
+ lastPct = -1;
376
+ console.log(`[${elapsed}s] Phase: ${progress.phase}`);
377
+ }
378
+ if (progress.total > 0) {
379
+ const pct = Math.floor((progress.current / progress.total) * 100);
380
+ // Log every 5% to keep output manageable
381
+ if (pct >= lastPct + 5 || progress.current === progress.total) {
382
+ lastPct = pct;
383
+ console.log(`[${elapsed}s] ${progress.current}/${progress.total} (${pct}%)${progress.currentFile ? ` ${(0, glyphs_1.getGlyphs)().dash} ${progress.currentFile}` : ''}`);
384
+ }
385
+ }
386
+ else if (progress.current > 0) {
387
+ // Scanning phase (no total yet) — log periodically
388
+ if (progress.current % 1000 === 0 || progress.current === 1) {
389
+ console.log(`[${elapsed}s] ${formatNumber(progress.current)} files found`);
390
+ }
391
+ }
392
+ };
393
+ }
394
+ /**
395
+ * Print success message
396
+ */
397
+ function success(message) {
398
+ console.log(chalk.green((0, glyphs_1.getGlyphs)().ok) + ' ' + message);
399
+ }
400
+ /**
401
+ * Print error message
402
+ */
403
+ function error(message) {
404
+ console.error(chalk.red((0, glyphs_1.getGlyphs)().err) + ' ' + message);
405
+ }
406
+ /**
407
+ * Print info message
408
+ */
409
+ function info(message) {
410
+ console.log(chalk.blue((0, glyphs_1.getGlyphs)().info) + ' ' + message);
411
+ }
412
+ /**
413
+ * Print warning message
414
+ */
415
+ function warn(message) {
416
+ console.log(chalk.yellow((0, glyphs_1.getGlyphs)().warn) + ' ' + message);
417
+ }
418
+ /**
419
+ * Render a smoke-check result (INSTALL-HANDSHAKE-DOC) as ✓/✗ lines with
420
+ * remediation. A failing blocking item is red; a failing non-blocking item is a
421
+ * yellow bullet. Shared by `install` (advisory) and `doctor` (gating).
422
+ */
423
+ function renderSmokeCheck(result) {
424
+ const g = (0, glyphs_1.getGlyphs)();
425
+ for (const item of result.items) {
426
+ const mark = item.ok
427
+ ? chalk.green(g.ok)
428
+ : item.blocking
429
+ ? chalk.red(g.err)
430
+ : chalk.yellow(g.warn);
431
+ console.log(` ${mark} ${item.label.padEnd(26)} ${chalk.dim(item.detail)}`);
432
+ if (!item.ok && item.remediation)
433
+ console.log(chalk.dim(` ${item.remediation}`));
434
+ }
435
+ }
436
+ /**
437
+ * Initialize + build the index for a project (REQ-HANDSHAKE-004 offer). Mirrors
438
+ * the `init` command's default-index behaviour with the shimmer progress.
439
+ */
440
+ async function buildProjectIndex(projectRoot) {
441
+ const { default: SpecShip } = await loadSpecShip();
442
+ const cg = await SpecShip.init(projectRoot, { index: false });
443
+ const progress = (0, shimmer_progress_1.createShimmerProgress)();
444
+ let stopped = false;
445
+ const stop = async () => {
446
+ if (!stopped) {
447
+ stopped = true;
448
+ await progress.stop();
449
+ }
450
+ };
451
+ try {
452
+ await cg.indexAll({ onProgress: progress.onProgress });
453
+ await stop();
454
+ await printStarterPrompt(cg);
455
+ }
456
+ finally {
457
+ await stop();
458
+ cg.destroy();
459
+ }
460
+ }
461
+ /**
462
+ * Print the manufactured first-run flow/impact prompt (REQ-ACTIVATION-002.A1):
463
+ * the closing line of `init` / the install index step. No-op when the graph
464
+ * yields no confidently-good prompt.
465
+ */
466
+ async function printStarterPrompt(cg) {
467
+ try {
468
+ const { generateStarterPrompt } = await Promise.resolve().then(() => __importStar(require('../activation/starter-prompt')));
469
+ const sp = generateStarterPrompt(cg);
470
+ if (!sp)
471
+ return;
472
+ console.log();
473
+ console.log(chalk.bold('Try this first — ask Claude:'));
474
+ console.log(' ' + chalk.cyan(sp.prompt));
475
+ console.log(chalk.dim(" (it'll explore the index instead of reading files)"));
476
+ }
477
+ catch {
478
+ /* best effort — never block init on the suggestion */
479
+ }
480
+ }
481
+ /**
482
+ * Print indexing results using clack log methods
483
+ */
484
+ function printIndexResult(clack, result, projectPath) {
485
+ const hasErrors = result.filesErrored > 0;
486
+ // Surface non-file-level failures (e.g. lock-acquisition failure
487
+ // when another indexer is running) before the file-count branches.
488
+ // Without this the CLI falls through to "No files found to index",
489
+ // which is actively misleading — the index DID run, it just couldn't
490
+ // get the lock.
491
+ //
492
+ // If success is false but no severity:'error' entry exists in
493
+ // `result.errors` (degenerate case — shouldn't happen in practice
494
+ // but worth guarding because the result shape is plumbed through
495
+ // multiple call sites), fall back to a generic message rather than
496
+ // continuing to the misleading "No files found" branch or throwing.
497
+ if (!result.success && !hasErrors && result.filesIndexed === 0) {
498
+ const generic = result.errors.find((e) => e.severity === 'error');
499
+ clack.log.error(generic?.message ?? `Indexing failed ${(0, glyphs_1.getGlyphs)().dash} no further details available`);
500
+ return;
501
+ }
502
+ if (result.filesIndexed > 0) {
503
+ if (hasErrors) {
504
+ clack.log.success(`Indexed ${formatNumber(result.filesIndexed)} files (${formatNumber(result.filesErrored)} could not be parsed)`);
505
+ }
506
+ else {
507
+ clack.log.success(`Indexed ${formatNumber(result.filesIndexed)} files`);
508
+ }
509
+ clack.log.info(`${formatNumber(result.nodesCreated)} nodes, ${formatNumber(result.edgesCreated)} edges in ${formatDuration(result.durationMs)}`);
510
+ }
511
+ else if (hasErrors) {
512
+ clack.log.error(`Indexing failed ${(0, glyphs_1.getGlyphs)().dash} all ${formatNumber(result.filesErrored)} files had errors`);
513
+ }
514
+ else {
515
+ clack.log.warn('No files found to index');
516
+ }
517
+ if (hasErrors) {
518
+ const errorsByCode = new Map();
519
+ for (const err of result.errors) {
520
+ if (err.severity === 'error') {
521
+ const code = err.code || 'unknown';
522
+ errorsByCode.set(code, (errorsByCode.get(code) || 0) + 1);
523
+ }
524
+ }
525
+ const codeLabels = {
526
+ parse_error: 'files failed to parse',
527
+ read_error: 'files could not be read',
528
+ size_exceeded: 'files exceeded size limit',
529
+ path_traversal: 'blocked paths',
530
+ unsupported_language: 'unsupported language',
531
+ parser_error: 'parser initialization failures',
532
+ };
533
+ const breakdown = Array.from(errorsByCode)
534
+ .map(([code, count]) => `${formatNumber(count)} ${codeLabels[code] || code}`)
535
+ .join('\n');
536
+ clack.note(breakdown, 'Error breakdown');
537
+ if (projectPath) {
538
+ writeErrorLog(projectPath, result.errors);
539
+ clack.log.info('See .specship/errors.log for details');
540
+ }
541
+ if (result.filesIndexed > 0) {
542
+ clack.log.info(`The index is fully usable ${(0, glyphs_1.getGlyphs)().dash} only the failed files are missing.`);
543
+ }
544
+ }
545
+ else if (projectPath) {
546
+ const logPath = path.join((0, directory_1.getSpecShipDir)(projectPath), 'errors.log');
547
+ if (fs.existsSync(logPath)) {
548
+ fs.unlinkSync(logPath);
549
+ }
550
+ }
551
+ }
552
+ /**
553
+ * Write detailed error log into the project's SpecShip data dir.
554
+ * Path follows whichever layout is active (home folder by default).
555
+ */
556
+ function writeErrorLog(projectPath, errors) {
557
+ const cgDir = (0, directory_1.getSpecShipDir)(projectPath);
558
+ if (!fs.existsSync(cgDir))
559
+ return;
560
+ const logPath = path.join(cgDir, 'errors.log');
561
+ // Group errors by file path
562
+ const errorsByFile = new Map();
563
+ const noFileErrors = [];
564
+ for (const err of errors) {
565
+ if (err.severity !== 'error')
566
+ continue;
567
+ if (err.filePath) {
568
+ let list = errorsByFile.get(err.filePath);
569
+ if (!list) {
570
+ list = [];
571
+ errorsByFile.set(err.filePath, list);
572
+ }
573
+ list.push({ message: err.message, code: err.code });
574
+ }
575
+ else {
576
+ noFileErrors.push({ message: err.message, code: err.code });
577
+ }
578
+ }
579
+ const lines = [
580
+ `SpecShip Error Log - ${new Date().toISOString()}`,
581
+ `${errorsByFile.size} files with errors`,
582
+ '',
583
+ ];
584
+ for (const [filePath, fileErrors] of errorsByFile) {
585
+ for (const err of fileErrors) {
586
+ lines.push(`${filePath}: ${err.message}`);
587
+ }
588
+ }
589
+ for (const err of noFileErrors) {
590
+ lines.push(err.message);
591
+ }
592
+ fs.writeFileSync(logPath, lines.join('\n') + '\n');
593
+ }
594
+ // =============================================================================
595
+ // Commands
596
+ // =============================================================================
597
+ /**
598
+ * specship init [path]
599
+ */
600
+ program
601
+ .command('init [path]')
602
+ .description('Initialize SpecShip in a project directory and build the initial index')
603
+ .option('-i, --index', 'Deprecated: indexing now runs by default; flag accepted for backward compatibility')
604
+ .option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
605
+ .action(async (pathArg, options) => {
606
+ const projectPath = path.resolve(pathArg || process.cwd());
607
+ const clack = await importESM('@clack/prompts');
608
+ clack.intro('Initializing SpecShip');
609
+ try {
610
+ if ((0, directory_1.isInitialized)(projectPath)) {
611
+ clack.log.warn(`Already initialized in ${projectPath}`);
612
+ clack.log.info('Use "specship index" to re-index or "specship sync" to update');
613
+ try {
614
+ const { offerWatchFallback } = await Promise.resolve().then(() => __importStar(require('../installer')));
615
+ await offerWatchFallback(clack, projectPath);
616
+ }
617
+ catch { /* non-fatal */ }
618
+ clack.outro('');
619
+ return;
620
+ }
621
+ const { default: SpecShip } = await loadSpecShip();
622
+ const cg = await SpecShip.init(projectPath, { index: false });
623
+ clack.log.success(`Initialized in ${projectPath}`);
624
+ // Indexing runs by default now. The legacy -i/--index flag is still
625
+ // accepted (so existing muscle memory and scripts don't break) but is a
626
+ // no-op — initializing always builds the initial index.
627
+ let result;
628
+ if (options.verbose) {
629
+ result = await cg.indexAll({
630
+ onProgress: createVerboseProgress(),
631
+ verbose: true,
632
+ });
633
+ }
634
+ else {
635
+ process.stdout.write(`${colors.dim}${(0, glyphs_1.getGlyphs)().rail}${colors.reset}\n`);
636
+ const progress = (0, shimmer_progress_1.createShimmerProgress)();
637
+ result = await cg.indexAll({
638
+ onProgress: progress.onProgress,
639
+ });
640
+ await progress.stop();
641
+ }
642
+ printIndexResult(clack, result, projectPath);
643
+ try {
644
+ const { offerWatchFallback } = await Promise.resolve().then(() => __importStar(require('../installer')));
645
+ await offerWatchFallback(clack, projectPath);
646
+ }
647
+ catch { /* non-fatal */ }
648
+ // Manufactured first-run moment (REQ-ACTIVATION-002.A1).
649
+ await printStarterPrompt(cg);
650
+ clack.outro('Done');
651
+ cg.destroy();
652
+ }
653
+ catch (err) {
654
+ clack.log.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
655
+ process.exit(1);
656
+ }
657
+ });
658
+ /**
659
+ * specship uninit [path]
660
+ */
661
+ program
662
+ .command('uninit [path]')
663
+ .description('Remove SpecShip from a project (deletes .specship/ directory)')
664
+ .option('-f, --force', 'Skip confirmation prompt')
665
+ .action(async (pathArg, options) => {
666
+ const projectPath = resolveProjectPath(pathArg);
667
+ try {
668
+ if (!(0, directory_1.isInitialized)(projectPath)) {
669
+ warn(`SpecShip is not initialized in ${projectPath}`);
670
+ return;
671
+ }
672
+ if (!options.force) {
673
+ // Confirm with user
674
+ const readline = await Promise.resolve().then(() => __importStar(require('readline')));
675
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
676
+ const answer = await new Promise((resolve) => {
677
+ rl.question(chalk.yellow(`${(0, glyphs_1.getGlyphs)().warn} This will permanently delete all SpecShip data. Continue? (y/N) `), resolve);
678
+ });
679
+ rl.close();
680
+ if (answer.toLowerCase() !== 'y') {
681
+ info('Cancelled');
682
+ return;
683
+ }
684
+ }
685
+ const { default: SpecShip } = await loadSpecShip();
686
+ const cg = SpecShip.openSync(projectPath);
687
+ cg.uninitialize();
688
+ // Clean up any git sync hooks we installed (no-op if none / not a repo).
689
+ try {
690
+ const { removeGitSyncHook } = await Promise.resolve().then(() => __importStar(require('../sync/git-hooks')));
691
+ const removed = removeGitSyncHook(projectPath);
692
+ if (removed.installed.length > 0) {
693
+ info(`Removed git ${removed.installed.join(', ')} sync hook${removed.installed.length > 1 ? 's' : ''}`);
694
+ }
695
+ }
696
+ catch { /* non-fatal */ }
697
+ success(`Removed SpecShip from ${projectPath}`);
698
+ }
699
+ catch (err) {
700
+ error(`Failed to uninitialize: ${err instanceof Error ? err.message : String(err)}`);
701
+ process.exit(1);
702
+ }
703
+ });
704
+ /**
705
+ * specship index [path]
706
+ */
707
+ program
708
+ .command('index [path]')
709
+ .description('Index all files in the project')
710
+ .option('-f, --force', 'Force full re-index even if already indexed')
711
+ .option('-q, --quiet', 'Suppress progress output')
712
+ .option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
713
+ .action(async (pathArg, options) => {
714
+ const projectPath = resolveProjectPath(pathArg);
715
+ try {
716
+ if (!(0, directory_1.isInitialized)(projectPath)) {
717
+ error(`SpecShip not initialized in ${projectPath}`);
718
+ info('Run "specship init" first');
719
+ process.exit(1);
720
+ }
721
+ const { default: SpecShip } = await loadSpecShip();
722
+ const cg = await SpecShip.open(projectPath);
723
+ if (options.quiet) {
724
+ // Quiet mode: no UI, just run
725
+ if (options.force)
726
+ cg.clear();
727
+ const result = await cg.indexAll();
728
+ if (!result.success)
729
+ process.exit(1);
730
+ cg.destroy();
731
+ return;
732
+ }
733
+ const clack = await importESM('@clack/prompts');
734
+ clack.intro('Indexing project');
735
+ if (options.force) {
736
+ cg.clear();
737
+ clack.log.info('Cleared existing index');
738
+ }
739
+ let result;
740
+ if (options.verbose) {
741
+ result = await cg.indexAll({
742
+ onProgress: createVerboseProgress(),
743
+ verbose: true,
744
+ });
745
+ }
746
+ else {
747
+ process.stdout.write(`${colors.dim}${(0, glyphs_1.getGlyphs)().rail}${colors.reset}\n`);
748
+ const progress = (0, shimmer_progress_1.createShimmerProgress)();
749
+ result = await cg.indexAll({
750
+ onProgress: progress.onProgress,
751
+ });
752
+ await progress.stop();
753
+ }
754
+ printIndexResult(clack, result, projectPath);
755
+ if (!result.success) {
756
+ process.exit(1);
757
+ }
758
+ clack.outro('Done');
759
+ cg.destroy();
760
+ }
761
+ catch (err) {
762
+ error(`Failed to index: ${err instanceof Error ? err.message : String(err)}`);
763
+ process.exit(1);
764
+ }
765
+ });
766
+ /**
767
+ * specship sync [path]
768
+ */
769
+ program
770
+ .command('sync [path]')
771
+ .description('Sync changes since last index')
772
+ .option('-q, --quiet', 'Suppress output (for git hooks)')
773
+ .action(async (pathArg, options) => {
774
+ const projectPath = resolveProjectPath(pathArg);
775
+ try {
776
+ if (!(0, directory_1.isInitialized)(projectPath)) {
777
+ if (!options.quiet) {
778
+ error(`SpecShip not initialized in ${projectPath}`);
779
+ }
780
+ process.exit(1);
781
+ }
782
+ const { default: SpecShip } = await loadSpecShip();
783
+ const cg = await SpecShip.open(projectPath);
784
+ if (options.quiet) {
785
+ await cg.sync();
786
+ cg.destroy();
787
+ return;
788
+ }
789
+ const clack = await importESM('@clack/prompts');
790
+ clack.intro('Syncing SpecShip');
791
+ process.stdout.write(`${colors.dim}${(0, glyphs_1.getGlyphs)().rail}${colors.reset}\n`);
792
+ const progress = (0, shimmer_progress_1.createShimmerProgress)();
793
+ const result = await cg.sync({
794
+ onProgress: progress.onProgress,
795
+ });
796
+ await progress.stop();
797
+ const totalChanges = result.filesAdded + result.filesModified + result.filesRemoved;
798
+ if (totalChanges === 0) {
799
+ clack.log.info('Already up to date');
800
+ }
801
+ else {
802
+ clack.log.success(`Synced ${formatNumber(totalChanges)} changed files`);
803
+ const details = [];
804
+ if (result.filesAdded > 0)
805
+ details.push(`Added: ${result.filesAdded}`);
806
+ if (result.filesModified > 0)
807
+ details.push(`Modified: ${result.filesModified}`);
808
+ if (result.filesRemoved > 0)
809
+ details.push(`Removed: ${result.filesRemoved}`);
810
+ clack.log.info(`${details.join(', ')} ${(0, glyphs_1.getGlyphs)().dash} ${formatNumber(result.nodesUpdated)} nodes in ${formatDuration(result.durationMs)}`);
811
+ }
812
+ clack.outro('Done');
813
+ cg.destroy();
814
+ }
815
+ catch (err) {
816
+ if (!options.quiet) {
817
+ error(`Failed to sync: ${err instanceof Error ? err.message : String(err)}`);
818
+ }
819
+ process.exit(1);
820
+ }
821
+ });
822
+ /**
823
+ * specship status [path]
824
+ */
825
+ program
826
+ .command('status [path]')
827
+ .description('Show index status and statistics')
828
+ .option('-j, --json', 'Output as JSON')
829
+ .action(async (pathArg, options) => {
830
+ const projectPath = resolveProjectPath(pathArg);
831
+ // The directory the user actually ran from, before walking up to the index
832
+ // root. Used to detect when the resolved index lives in a different git
833
+ // working tree (e.g. a nested worktree borrowing the main checkout's index).
834
+ const startPath = path.resolve(pathArg || process.cwd());
835
+ const worktreeMismatch = (0, worktree_1.detectWorktreeIndexMismatch)(startPath, projectPath);
836
+ try {
837
+ if (!(0, directory_1.isInitialized)(projectPath)) {
838
+ if (options.json) {
839
+ console.log(JSON.stringify({
840
+ initialized: false,
841
+ version: packageJson.version,
842
+ projectPath,
843
+ indexPath: (0, directory_1.getSpecShipDir)(projectPath),
844
+ lastIndexed: null,
845
+ }));
846
+ return;
847
+ }
848
+ console.log(chalk.bold('\nSpecShip Status\n'));
849
+ info(`Project: ${projectPath}`);
850
+ warn('Not initialized');
851
+ info('Run "specship init" to initialize');
852
+ return;
853
+ }
854
+ const { default: SpecShip } = await loadSpecShip();
855
+ const cg = await SpecShip.open(projectPath);
856
+ const stats = cg.getStats();
857
+ const changes = cg.getChangedFiles();
858
+ const backend = cg.getBackend();
859
+ const journalMode = cg.getJournalMode();
860
+ // JSON output mode
861
+ if (options.json) {
862
+ const lastIndexedMs = cg.getLastIndexedAt();
863
+ console.log(JSON.stringify({
864
+ initialized: true,
865
+ version: packageJson.version,
866
+ projectPath,
867
+ indexPath: (0, directory_1.getSpecShipDir)(projectPath),
868
+ lastIndexed: lastIndexedMs != null ? new Date(lastIndexedMs).toISOString() : null,
869
+ fileCount: stats.fileCount,
870
+ nodeCount: stats.nodeCount,
871
+ edgeCount: stats.edgeCount,
872
+ dbSizeBytes: stats.dbSizeBytes,
873
+ backend,
874
+ journalMode,
875
+ nodesByKind: stats.nodesByKind,
876
+ languages: Object.entries(stats.filesByLanguage).filter(([, count]) => count > 0).map(([lang]) => lang),
877
+ pendingChanges: {
878
+ added: changes.added.length,
879
+ modified: changes.modified.length,
880
+ removed: changes.removed.length,
881
+ },
882
+ worktreeMismatch: worktreeMismatch
883
+ ? { worktreeRoot: worktreeMismatch.worktreeRoot, indexRoot: worktreeMismatch.indexRoot }
884
+ : null,
885
+ }));
886
+ cg.destroy();
887
+ return;
888
+ }
889
+ console.log(chalk.bold('\nSpecShip Status\n'));
890
+ // Project info
891
+ console.log(chalk.cyan('Project:'), projectPath);
892
+ if (worktreeMismatch) {
893
+ warn((0, worktree_1.worktreeMismatchWarning)(worktreeMismatch));
894
+ }
895
+ console.log();
896
+ // Index stats
897
+ console.log(chalk.bold('Index Statistics:'));
898
+ console.log(` Files: ${formatNumber(stats.fileCount)}`);
899
+ console.log(` Nodes: ${formatNumber(stats.nodeCount)}`);
900
+ console.log(` Edges: ${formatNumber(stats.edgeCount)}`);
901
+ console.log(` DB Size: ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`);
902
+ // Surface the active SQLite backend. Two paths:
903
+ // - 'node-sqlite' → Node's built-in module (production: bundled Node 24 has FTS5).
904
+ // - 'better-sqlite3' → optional devDep that ships its own SQLite with FTS5.
905
+ const backendLabel = backend === 'better-sqlite3'
906
+ ? chalk.green(`better-sqlite3 ${(0, glyphs_1.getGlyphs)().dash} ships its own SQLite (FTS5)`)
907
+ : chalk.green(`node:sqlite ${(0, glyphs_1.getGlyphs)().dash} built-in (full WAL)`);
908
+ console.log(` Backend: ${backendLabel}`);
909
+ // Effective journal mode: 'wal' means concurrent reads never block on a
910
+ // writer; anything else means they can ("database is locked"). node:sqlite
911
+ // supports WAL everywhere, so a non-wal mode means the filesystem can't
912
+ // (network mounts, WSL2 /mnt). See issue #238.
913
+ const journalLabel = journalMode === 'wal'
914
+ ? chalk.green('wal')
915
+ : chalk.yellow(`${journalMode || 'unknown'} ${(0, glyphs_1.getGlyphs)().dash} WAL inactive; reads can block on writes`);
916
+ console.log(` Journal: ${journalLabel}`);
917
+ console.log();
918
+ // Node breakdown
919
+ console.log(chalk.bold('Nodes by Kind:'));
920
+ const nodesByKind = Object.entries(stats.nodesByKind)
921
+ .filter(([, count]) => count > 0)
922
+ .sort((a, b) => b[1] - a[1]);
923
+ for (const [kind, count] of nodesByKind) {
924
+ console.log(` ${kind.padEnd(15)} ${formatNumber(count)}`);
925
+ }
926
+ console.log();
927
+ // Language breakdown
928
+ console.log(chalk.bold('Files by Language:'));
929
+ const filesByLang = Object.entries(stats.filesByLanguage)
930
+ .filter(([, count]) => count > 0)
931
+ .sort((a, b) => b[1] - a[1]);
932
+ for (const [lang, count] of filesByLang) {
933
+ console.log(` ${lang.padEnd(15)} ${formatNumber(count)}`);
934
+ }
935
+ console.log();
936
+ // Pending changes
937
+ const totalChanges = changes.added.length + changes.modified.length + changes.removed.length;
938
+ if (totalChanges > 0) {
939
+ console.log(chalk.bold('Pending Changes:'));
940
+ if (changes.added.length > 0) {
941
+ console.log(` Added: ${changes.added.length} files`);
942
+ }
943
+ if (changes.modified.length > 0) {
944
+ console.log(` Modified: ${changes.modified.length} files`);
945
+ }
946
+ if (changes.removed.length > 0) {
947
+ console.log(` Removed: ${changes.removed.length} files`);
948
+ }
949
+ info('Run "specship sync" to update the index');
950
+ }
951
+ else {
952
+ success('Index is up to date');
953
+ }
954
+ console.log();
955
+ cg.destroy();
956
+ }
957
+ catch (err) {
958
+ error(`Failed to get status: ${err instanceof Error ? err.message : String(err)}`);
959
+ process.exit(1);
960
+ }
961
+ });
962
+ /**
963
+ * specship statusline
964
+ *
965
+ * Reads Claude Code's status-line JSON on stdin and prints ONE composable
966
+ * segment on stdout (sync state · backend health · session calls · active
967
+ * run). Resolves entirely from cache files — never opens the database — so it
968
+ * stays within the sub-second status-line render budget (SHIP-STATUSLINE-DOC).
969
+ * Append it to your own status-line script.
970
+ */
971
+ program
972
+ .command('statusline')
973
+ .description('Print a SpecShip status-line segment (reads Claude Code JSON on stdin)')
974
+ .action(async () => {
975
+ const { buildSegment } = await Promise.resolve().then(() => __importStar(require('../statusline/index')));
976
+ let raw = '';
977
+ try {
978
+ const chunks = [];
979
+ for await (const c of process.stdin)
980
+ chunks.push(c);
981
+ raw = Buffer.concat(chunks).toString('utf-8');
982
+ }
983
+ catch {
984
+ /* no stdin — buildSegment falls back to cwd */
985
+ }
986
+ // buildSegment never throws; print the segment with no trailing newline so
987
+ // a composing script controls line layout.
988
+ process.stdout.write(buildSegment(raw));
989
+ });
990
+ /**
991
+ * specship query <search>
992
+ */
993
+ program
994
+ .command('query <search>')
995
+ .description('Search for symbols in the codebase')
996
+ .option('-p, --path <path>', 'Project path')
997
+ .option('-l, --limit <number>', 'Maximum results', '10')
998
+ .option('-k, --kind <kind>', 'Filter by node kind (function, class, etc.)')
999
+ .option('-j, --json', 'Output as JSON')
1000
+ .action(async (search, options) => {
1001
+ const projectPath = resolveProjectPath(options.path);
1002
+ try {
1003
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1004
+ error(`SpecShip not initialized in ${projectPath}`);
1005
+ process.exit(1);
1006
+ }
1007
+ const { default: SpecShip } = await loadSpecShip();
1008
+ const cg = await SpecShip.open(projectPath);
1009
+ const limit = parseInt(options.limit || '10', 10);
1010
+ const rawResults = cg.searchNodes(search, {
1011
+ limit,
1012
+ kinds: options.kind ? [options.kind] : undefined,
1013
+ });
1014
+ // Mirror the MCP search down-rank so the CLI also surfaces the
1015
+ // hand-written implementation before protobuf/gRPC scaffolding
1016
+ // when both share a name. See extraction/generated-detection.ts.
1017
+ const { isGeneratedFile } = await Promise.resolve().then(() => __importStar(require('../extraction/generated-detection')));
1018
+ const results = [...rawResults].sort((a, b) => {
1019
+ const aGen = isGeneratedFile(a.node.filePath) ? 1 : 0;
1020
+ const bGen = isGeneratedFile(b.node.filePath) ? 1 : 0;
1021
+ return aGen - bGen;
1022
+ });
1023
+ if (options.json) {
1024
+ console.log(JSON.stringify(results, null, 2));
1025
+ }
1026
+ else {
1027
+ if (results.length === 0) {
1028
+ info(`No results found for "${search}"`);
1029
+ }
1030
+ else {
1031
+ console.log(chalk.bold(`\nSearch Results for "${search}":\n`));
1032
+ for (const result of results) {
1033
+ const node = result.node;
1034
+ const location = `${node.filePath}:${node.startLine}`;
1035
+ const score = chalk.dim(`(${(result.score * 100).toFixed(0)}%)`);
1036
+ console.log(chalk.cyan(node.kind.padEnd(12)) +
1037
+ chalk.white(node.name) +
1038
+ ' ' + score);
1039
+ console.log(chalk.dim(` ${location}`));
1040
+ if (node.signature) {
1041
+ console.log(chalk.dim(` ${node.signature}`));
1042
+ }
1043
+ console.log();
1044
+ }
1045
+ }
1046
+ }
1047
+ cg.destroy();
1048
+ }
1049
+ catch (err) {
1050
+ error(`Search failed: ${err instanceof Error ? err.message : String(err)}`);
1051
+ process.exit(1);
1052
+ }
1053
+ });
1054
+ /**
1055
+ * specship files [path]
1056
+ */
1057
+ program
1058
+ .command('files')
1059
+ .description('Show project file structure from the index')
1060
+ .option('-p, --path <path>', 'Project path')
1061
+ .option('--filter <dir>', 'Filter to files under this directory')
1062
+ .option('--pattern <glob>', 'Filter files matching this glob pattern')
1063
+ .option('--format <format>', 'Output format (tree, flat, grouped)', 'tree')
1064
+ .option('--max-depth <number>', 'Maximum directory depth for tree format')
1065
+ .option('--no-metadata', 'Hide file metadata (language, symbol count)')
1066
+ .option('-j, --json', 'Output as JSON')
1067
+ .action(async (options) => {
1068
+ const projectPath = resolveProjectPath(options.path);
1069
+ try {
1070
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1071
+ error(`SpecShip not initialized in ${projectPath}`);
1072
+ process.exit(1);
1073
+ }
1074
+ const { default: SpecShip } = await loadSpecShip();
1075
+ const cg = await SpecShip.open(projectPath);
1076
+ let files = cg.getFiles();
1077
+ if (files.length === 0) {
1078
+ info('No files indexed. Run "specship index" first.');
1079
+ cg.destroy();
1080
+ return;
1081
+ }
1082
+ // Filter by path prefix
1083
+ if (options.filter) {
1084
+ const filter = options.filter;
1085
+ files = files.filter(f => f.path.startsWith(filter) || f.path.startsWith('./' + filter));
1086
+ }
1087
+ // Filter by glob pattern
1088
+ if (options.pattern) {
1089
+ const regex = globToRegex(options.pattern);
1090
+ files = files.filter(f => regex.test(f.path));
1091
+ }
1092
+ if (files.length === 0) {
1093
+ info('No files found matching the criteria.');
1094
+ cg.destroy();
1095
+ return;
1096
+ }
1097
+ // JSON output
1098
+ if (options.json) {
1099
+ const output = files.map(f => ({
1100
+ path: f.path,
1101
+ language: f.language,
1102
+ nodeCount: f.nodeCount,
1103
+ size: f.size,
1104
+ }));
1105
+ console.log(JSON.stringify(output, null, 2));
1106
+ cg.destroy();
1107
+ return;
1108
+ }
1109
+ const includeMetadata = options.metadata !== false;
1110
+ const format = options.format || 'tree';
1111
+ const maxDepth = options.maxDepth ? parseInt(options.maxDepth, 10) : undefined;
1112
+ // Format output
1113
+ switch (format) {
1114
+ case 'flat':
1115
+ console.log(chalk.bold(`\nFiles (${files.length}):\n`));
1116
+ for (const file of files.sort((a, b) => a.path.localeCompare(b.path))) {
1117
+ if (includeMetadata) {
1118
+ console.log(` ${file.path} ${chalk.dim(`(${file.language}, ${file.nodeCount} symbols)`)}`);
1119
+ }
1120
+ else {
1121
+ console.log(` ${file.path}`);
1122
+ }
1123
+ }
1124
+ break;
1125
+ case 'grouped':
1126
+ console.log(chalk.bold(`\nFiles by Language (${files.length} total):\n`));
1127
+ const byLang = new Map();
1128
+ for (const file of files) {
1129
+ const existing = byLang.get(file.language) || [];
1130
+ existing.push(file);
1131
+ byLang.set(file.language, existing);
1132
+ }
1133
+ const sortedLangs = [...byLang.entries()].sort((a, b) => b[1].length - a[1].length);
1134
+ for (const [lang, langFiles] of sortedLangs) {
1135
+ console.log(chalk.cyan(`${lang} (${langFiles.length}):`));
1136
+ for (const file of langFiles.sort((a, b) => a.path.localeCompare(b.path))) {
1137
+ if (includeMetadata) {
1138
+ console.log(` ${file.path} ${chalk.dim(`(${file.nodeCount} symbols)`)}`);
1139
+ }
1140
+ else {
1141
+ console.log(` ${file.path}`);
1142
+ }
1143
+ }
1144
+ console.log();
1145
+ }
1146
+ break;
1147
+ case 'tree':
1148
+ default:
1149
+ console.log(chalk.bold(`\nProject Structure (${files.length} files):\n`));
1150
+ printFileTree(files, includeMetadata, maxDepth, chalk);
1151
+ break;
1152
+ }
1153
+ console.log();
1154
+ cg.destroy();
1155
+ }
1156
+ catch (err) {
1157
+ error(`Failed to list files: ${err instanceof Error ? err.message : String(err)}`);
1158
+ process.exit(1);
1159
+ }
1160
+ });
1161
+ /**
1162
+ * Convert glob pattern to regex
1163
+ */
1164
+ function globToRegex(pattern) {
1165
+ const escaped = pattern
1166
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
1167
+ .replace(/\*\*/g, '{{GLOBSTAR}}')
1168
+ .replace(/\*/g, '[^/]*')
1169
+ .replace(/\?/g, '[^/]')
1170
+ .replace(/\{\{GLOBSTAR\}\}/g, '.*');
1171
+ return new RegExp(escaped);
1172
+ }
1173
+ /**
1174
+ * Print files as a tree
1175
+ */
1176
+ function printFileTree(files, includeMetadata, maxDepth, chalk) {
1177
+ const root = { name: '', children: new Map() };
1178
+ for (const file of files) {
1179
+ const parts = file.path.split('/');
1180
+ let current = root;
1181
+ for (let i = 0; i < parts.length; i++) {
1182
+ const part = parts[i];
1183
+ if (!part)
1184
+ continue;
1185
+ if (!current.children.has(part)) {
1186
+ current.children.set(part, { name: part, children: new Map() });
1187
+ }
1188
+ current = current.children.get(part);
1189
+ if (i === parts.length - 1) {
1190
+ current.file = { language: file.language, nodeCount: file.nodeCount };
1191
+ }
1192
+ }
1193
+ }
1194
+ const renderNode = (node, prefix, isLast, depth) => {
1195
+ if (maxDepth !== undefined && depth > maxDepth)
1196
+ return;
1197
+ const glyphs = (0, glyphs_1.getGlyphs)();
1198
+ const connector = isLast ? glyphs.treeLast : glyphs.treeBranch;
1199
+ const childPrefix = isLast ? ' ' : glyphs.treePipe;
1200
+ if (node.name) {
1201
+ let line = prefix + connector + node.name;
1202
+ if (node.file && includeMetadata) {
1203
+ line += chalk.dim(` (${node.file.language}, ${node.file.nodeCount} symbols)`);
1204
+ }
1205
+ console.log(line);
1206
+ }
1207
+ const children = [...node.children.values()];
1208
+ children.sort((a, b) => {
1209
+ const aIsDir = a.children.size > 0 && !a.file;
1210
+ const bIsDir = b.children.size > 0 && !b.file;
1211
+ if (aIsDir !== bIsDir)
1212
+ return aIsDir ? -1 : 1;
1213
+ return a.name.localeCompare(b.name);
1214
+ });
1215
+ for (let i = 0; i < children.length; i++) {
1216
+ const child = children[i];
1217
+ const nextPrefix = node.name ? prefix + childPrefix : prefix;
1218
+ renderNode(child, nextPrefix, i === children.length - 1, depth + 1);
1219
+ }
1220
+ };
1221
+ renderNode(root, '', true, 0);
1222
+ }
1223
+ /**
1224
+ * specship reflect
1225
+ *
1226
+ * Run a reflection pass over the ingested Claude Code transcripts and print the
1227
+ * self-improvement proposals it surfaces (REQ-REFLECT-006.A1). Headless — no
1228
+ * dashboard required. With no usable transcript history, prints an empty state.
1229
+ */
1230
+ program
1231
+ .command('reflect [path]')
1232
+ .description('Mine ingested transcripts for self-improvement proposals (memory rules, skills, hooks)')
1233
+ .option('-j, --json', 'Output as JSON')
1234
+ .action(async (pathArg, options) => {
1235
+ const projectPath = resolveProjectPath(pathArg);
1236
+ try {
1237
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1238
+ error(`SpecShip not initialized in ${projectPath}`);
1239
+ process.exit(1);
1240
+ }
1241
+ const { default: SpecShip } = await loadSpecShip();
1242
+ const cg = await SpecShip.open(projectPath);
1243
+ const result = cg.reflectAnalyze();
1244
+ if (options.json) {
1245
+ console.log(JSON.stringify(result, null, 2));
1246
+ cg.destroy();
1247
+ return;
1248
+ }
1249
+ if (result.empty || result.open.length === 0) {
1250
+ info('No proposals yet — not enough signal in the ingested transcripts.');
1251
+ info('Tip: run the dashboard with `specship serve --ui --ingest` to build transcript history.');
1252
+ cg.destroy();
1253
+ return;
1254
+ }
1255
+ const sevColor = {
1256
+ high: chalk.red,
1257
+ warn: chalk.yellow,
1258
+ info: chalk.cyan,
1259
+ };
1260
+ const typeLabel = {
1261
+ memory_rule: 'memory/rule',
1262
+ skill: 'skill',
1263
+ hook: 'hook',
1264
+ };
1265
+ console.log(chalk.bold(`\nReflection proposals (${result.open.length}):\n`));
1266
+ for (const p of result.open) {
1267
+ const sev = (sevColor[p.severity] ?? chalk.white)(p.severity.toUpperCase().padEnd(5));
1268
+ console.log(`${sev} ${chalk.bold(p.title)}`);
1269
+ console.log(chalk.dim(` ${typeLabel[p.type] ?? p.type} → ${p.targetPath}`));
1270
+ console.log(chalk.dim(` ${p.evidence.detail}`));
1271
+ console.log(chalk.dim(` ${p.body}`));
1272
+ console.log();
1273
+ }
1274
+ info('Review and apply proposals from the dashboard Improvements page (preview-diff → confirm).');
1275
+ cg.destroy();
1276
+ }
1277
+ catch (err) {
1278
+ error(`reflect failed: ${err instanceof Error ? err.message : String(err)}`);
1279
+ process.exit(1);
1280
+ }
1281
+ });
1282
+ /**
1283
+ * specship maintainability
1284
+ *
1285
+ * Report graph-derived maintainability signals (REQ-MAINT-003) — coupling, size
1286
+ * hotspots, dependency cycles, dead-code candidates. Advisory (exit 0); the
1287
+ * `--strict` flag is the gating-ready shape consumed later by enforcement mode.
1288
+ */
1289
+ program
1290
+ .command('maintainability [path]')
1291
+ .alias('maint')
1292
+ .description('Report graph-derived maintainability signals (coupling, size, cycles, dead code)')
1293
+ .option('-j, --json', 'Output as JSON')
1294
+ .option('--deep', 'Also show lower-confidence findings (dead-code candidates, coupling). Hidden by default.')
1295
+ .option('--strict', 'Exit non-zero if any signal has findings (gating-ready; default advisory)')
1296
+ .action(async (pathArg, options) => {
1297
+ const projectPath = resolveProjectPath(pathArg);
1298
+ try {
1299
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1300
+ error(`SpecShip not initialized in ${projectPath}`);
1301
+ process.exit(1);
1302
+ }
1303
+ const { default: SpecShip } = await loadSpecShip();
1304
+ const cg = await SpecShip.open(projectPath);
1305
+ const r = cg.getMaintainability();
1306
+ // The report is tiered (HEALTH-GATEWAY-DOC). The high-precision classes —
1307
+ // oversized symbols, god files, dependency cycles — are demonstrably
1308
+ // accurate and shown by default. Dead-code and coupling are lower-confidence
1309
+ // (volume + name-collision artifacts) and surface only with --deep. `--json`
1310
+ // always returns the full set, labelled by tier, so CI/tooling can choose
1311
+ // what to gate on (REQ-HEALTH-001/002/003).
1312
+ const { HIGH_PRECISION_CLASSES, LOW_CONFIDENCE_CLASSES, highPrecisionClean } = await Promise.resolve().then(() => __importStar(require('../graph/maintainability')));
1313
+ if (options.json) {
1314
+ console.log(JSON.stringify({ ...r, precision: { highPrecision: HIGH_PRECISION_CLASSES, lowConfidence: LOW_CONFIDENCE_CLASSES } }, null, 2));
1315
+ cg.destroy();
1316
+ if (options.strict && !r.clean)
1317
+ process.exit(1);
1318
+ return;
1319
+ }
1320
+ const deep = options.deep === true;
1321
+ const highClean = highPrecisionClean(r);
1322
+ const lowCount = r.coupling.length + r.deadCode.length;
1323
+ const CAP = 10;
1324
+ const section = (title, count) => console.log(chalk.bold(`\n${title} (${count})`));
1325
+ const overflow = (n) => { if (n > CAP)
1326
+ console.log(chalk.dim(` …and ${n - CAP} more`)); };
1327
+ // High-precision tier — always shown (REQ-HEALTH-001).
1328
+ if (r.oversized.length) {
1329
+ section('Oversized symbols', r.oversized.length);
1330
+ for (const o of r.oversized.slice(0, CAP))
1331
+ console.log(chalk.dim(' ') + `${o.name} ${chalk.dim(`(${o.reason}) — ${o.filePath}`)}`);
1332
+ overflow(r.oversized.length);
1333
+ }
1334
+ if (r.godFiles.length) {
1335
+ section('God files', r.godFiles.length);
1336
+ for (const f of r.godFiles.slice(0, CAP))
1337
+ console.log(chalk.dim(' ') + `${f.filePath} ${chalk.dim(`(${f.reason})`)}`);
1338
+ overflow(r.godFiles.length);
1339
+ }
1340
+ if (r.cycles.length) {
1341
+ section('Dependency cycles', r.cycles.length);
1342
+ for (const c of r.cycles.slice(0, CAP))
1343
+ console.log(chalk.dim(' ') + c.files.join(' → '));
1344
+ overflow(r.cycles.length);
1345
+ }
1346
+ if (highClean && !deep) {
1347
+ success('Maintainability: clean — no high-precision findings past threshold.');
1348
+ if (lowCount > 0)
1349
+ info(`${lowCount} lower-confidence finding(s) hidden (dead-code: ${r.deadCode.length}, coupling: ${r.coupling.length}) — run \`specship maintainability --deep\` to include them.`);
1350
+ cg.destroy();
1351
+ return;
1352
+ }
1353
+ // Low-confidence tier — opt-in via --deep (REQ-HEALTH-002), each finding
1354
+ // attributed to a single concrete definition (file + qualified symbol).
1355
+ if (deep) {
1356
+ if (r.coupling.length) {
1357
+ section('Coupling hotspots (lower-confidence)', r.coupling.length);
1358
+ for (const c of r.coupling.slice(0, CAP))
1359
+ console.log(chalk.dim(' ') + `${c.qualifiedName} ${chalk.dim(`(${c.reason}) — ${c.filePath}`)}`);
1360
+ overflow(r.coupling.length);
1361
+ }
1362
+ if (r.deadCode.length) {
1363
+ section('Dead-code candidates (lower-confidence)', r.deadCode.length);
1364
+ for (const d of r.deadCode.slice(0, CAP))
1365
+ console.log(chalk.dim(' ') + `${d.qualifiedName} ${chalk.dim(`— ${d.filePath}:${d.startLine}`)}`);
1366
+ overflow(r.deadCode.length);
1367
+ }
1368
+ }
1369
+ else if (lowCount > 0) {
1370
+ console.log();
1371
+ info(`${lowCount} lower-confidence finding(s) hidden (dead-code: ${r.deadCode.length}, coupling: ${r.coupling.length}) — run \`specship maintainability --deep\` to include them.`);
1372
+ }
1373
+ console.log();
1374
+ info(`Thresholds: highDegree=${r.thresholds.highDegree} largeSymbolLines=${r.thresholds.largeSymbolLines} godFileSymbols=${r.thresholds.godFileSymbols} — override in specship.config.json`);
1375
+ cg.destroy();
1376
+ // --strict gates on what's shown: high-precision by default, the full set with --deep.
1377
+ if (options.strict && (deep ? !r.clean : !highClean))
1378
+ process.exit(1);
1379
+ }
1380
+ catch (err) {
1381
+ error(`maintainability failed: ${err instanceof Error ? err.message : String(err)}`);
1382
+ process.exit(1);
1383
+ }
1384
+ });
1385
+ /**
1386
+ * specship fitness
1387
+ *
1388
+ * Evaluate the project's architecture-fitness rules (specship.config.json
1389
+ * `fitness.rules`) against the graph (REQ-FITNESS-003). Headless CI gate: exits
1390
+ * non-zero on any violation OR config error (a no-match rule is a config error,
1391
+ * never a silent pass).
1392
+ */
1393
+ program
1394
+ .command('fitness [path]')
1395
+ .description('Check architecture-fitness rules against the code graph (CI gate; exits non-zero on violation)')
1396
+ .option('-j, --json', 'Output as JSON')
1397
+ .action(async (pathArg, options) => {
1398
+ const projectPath = resolveProjectPath(pathArg);
1399
+ try {
1400
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1401
+ error(`SpecShip not initialized in ${projectPath}`);
1402
+ process.exit(1);
1403
+ }
1404
+ const { default: SpecShip } = await loadSpecShip();
1405
+ const cg = await SpecShip.open(projectPath);
1406
+ const r = cg.getFitness();
1407
+ const fail = r.violations.length > 0 || r.configErrors.length > 0;
1408
+ if (options.json) {
1409
+ console.log(JSON.stringify(r, null, 2));
1410
+ cg.destroy();
1411
+ process.exit(fail ? 1 : 0);
1412
+ }
1413
+ if (r.ruleCount === 0) {
1414
+ info('No architecture-fitness rules declared. Add a `fitness.rules` array to specship.config.json.');
1415
+ cg.destroy();
1416
+ return;
1417
+ }
1418
+ if (r.clean) {
1419
+ success(`Architecture fitness: all ${r.ruleCount} rule(s) pass.`);
1420
+ cg.destroy();
1421
+ return;
1422
+ }
1423
+ if (r.configErrors.length) {
1424
+ console.log(chalk.bold(chalk.red(`\nConfig errors (${r.configErrors.length}):`)));
1425
+ for (const e of r.configErrors)
1426
+ console.log(chalk.red(` ✗ ${e.rule}: ${e.message}`));
1427
+ }
1428
+ if (r.violations.length) {
1429
+ console.log(chalk.bold(chalk.red(`\nViolations (${r.violations.length}):`)));
1430
+ for (const v of r.violations.slice(0, 50)) {
1431
+ console.log(` ${chalk.red('✗')} [${v.rule}] ${v.source} → ${v.target}`);
1432
+ console.log(chalk.dim(` ${v.detail} — ${v.location}`));
1433
+ }
1434
+ if (r.violations.length > 50)
1435
+ console.log(chalk.dim(` …and ${r.violations.length - 50} more`));
1436
+ }
1437
+ cg.destroy();
1438
+ process.exit(1);
1439
+ }
1440
+ catch (err) {
1441
+ error(`fitness failed: ${err instanceof Error ? err.message : String(err)}`);
1442
+ process.exit(1);
1443
+ }
1444
+ });
1445
+ /**
1446
+ * specship check
1447
+ *
1448
+ * The enforcement gate (REQ-ENFORCE-001/002/003): runs the harness checks
1449
+ * (drift + fitness + maintainability + behaviour) and exits non-zero if any
1450
+ * GATING check fails. Which checks gate vs advise is configured in
1451
+ * specship.config.json `enforce.gate`; with no config every check is advisory
1452
+ * and the command exits 0 (opt-in — never breaks an existing repo).
1453
+ */
1454
+ program
1455
+ .command('check [path]')
1456
+ .description('Run the enforcement gate (drift + fitness + maintainability + behaviour); exits non-zero on a gating failure')
1457
+ .option('-j, --json', 'Output as JSON')
1458
+ .action(async (pathArg, options) => {
1459
+ const projectPath = resolveProjectPath(pathArg);
1460
+ try {
1461
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1462
+ error(`SpecShip not initialized in ${projectPath}`);
1463
+ process.exit(1);
1464
+ }
1465
+ const { default: SpecShip } = await loadSpecShip();
1466
+ const cg = await SpecShip.open(projectPath);
1467
+ const r = cg.getEnforce();
1468
+ if (options.json) {
1469
+ console.log(JSON.stringify(r, null, 2));
1470
+ cg.destroy();
1471
+ process.exit(r.passed ? 0 : 1);
1472
+ }
1473
+ console.log(chalk.bold('\nEnforcement gate\n'));
1474
+ for (const c of r.checks) {
1475
+ const tag = c.gating ? chalk.dim('[gating]') : chalk.dim('[advisory]');
1476
+ const mark = c.passed ? chalk.green('✓') : (c.gating ? chalk.red('✗') : chalk.yellow('•'));
1477
+ console.log(` ${mark} ${c.check.padEnd(16)} ${tag} ${c.passed ? 'pass' : `${c.findings.length} finding(s)`}`);
1478
+ if (!c.passed)
1479
+ for (const f of c.findings.slice(0, 8))
1480
+ console.log(chalk.dim(` ${f}`));
1481
+ if (!c.passed && c.findings.length > 8)
1482
+ console.log(chalk.dim(` …and ${c.findings.length - 8} more`));
1483
+ }
1484
+ console.log();
1485
+ if (r.passed) {
1486
+ success(r.gatedFailures.length === 0 && r.checks.some((c) => c.gating)
1487
+ ? 'All gating checks pass.'
1488
+ : 'Pass (no gating checks failed).');
1489
+ cg.destroy();
1490
+ return;
1491
+ }
1492
+ error(`Gating checks failed: ${r.gatedFailures.join(', ')}`);
1493
+ cg.destroy();
1494
+ process.exit(1);
1495
+ }
1496
+ catch (err) {
1497
+ error(`check failed: ${err instanceof Error ? err.message : String(err)}`);
1498
+ process.exit(1);
1499
+ }
1500
+ });
1501
+ /**
1502
+ * specship serve
1503
+ */
1504
+ program
1505
+ .command('serve')
1506
+ .description('Start SpecShip as an MCP server for AI assistants, an HTTP API for the desktop UI, or both')
1507
+ .option('-p, --path <path>', 'Project path (optional for MCP mode, uses rootUri from client)')
1508
+ .option('--mcp', 'Run as MCP server (stdio transport)')
1509
+ .option('--ui', 'Run as HTTP API server for the SpecShip Desktop UI (binds 127.0.0.1)')
1510
+ .option('--port <n>', 'HTTP port when --ui is set (default 4242)')
1511
+ .option('--host <h>', 'HTTP bind host when --ui is set (default 127.0.0.1)')
1512
+ .option('--ingest', 'Enable Claude Code JSONL transcript watcher (only when --ui is set)')
1513
+ .option('--web-dir <path>', 'Path to a built Angular UI (index.html lives here); auto-detected by default')
1514
+ .option('--no-web', 'Run --ui headless (API only, no SPA)')
1515
+ .option('--no-watch', 'Disable the file watcher (no auto-sync; useful on slow filesystems like WSL2 /mnt drives)')
1516
+ .action(async (options) => {
1517
+ const projectPath = options.path ? resolveProjectPath(options.path) : undefined;
1518
+ // Commander sets watch=false when --no-watch is passed. Route it through
1519
+ // the same env-var chokepoint the watcher and MCP server already honor.
1520
+ if (options.watch === false) {
1521
+ process.env.SPECSHIP_NO_WATCH = '1';
1522
+ }
1523
+ try {
1524
+ if (options.ui) {
1525
+ // HTTP API mode. Boots the specship server + optional JSONL
1526
+ // ingest watcher. Optionally also starts MCP stdio in parallel.
1527
+ //
1528
+ // Project root resolution:
1529
+ // 1. `-p <path>` if passed.
1530
+ // 2. The current cwd if it has been `specship init`-ed.
1531
+ // 3. Most-recently-touched initialized project under
1532
+ // ~/.claude/projects/ — so a user who runs `specship serve --ui`
1533
+ // from anywhere lands on the project they were last active in.
1534
+ // 4. None — server boots projectless, the desktop picker prompts
1535
+ // the user to choose one (analytics endpoints return 409 until
1536
+ // a primary exists).
1537
+ let root = null;
1538
+ if (projectPath) {
1539
+ root = projectPath;
1540
+ if (!(0, directory_1.isInitialized)(root)) {
1541
+ error(`SpecShip not initialized in ${root}. Run \`specship init -i\` first.`);
1542
+ process.exit(1);
1543
+ }
1544
+ }
1545
+ else if ((0, directory_1.isInitialized)(process.cwd())) {
1546
+ root = process.cwd();
1547
+ }
1548
+ else {
1549
+ root = await pickRecentInitializedProject();
1550
+ }
1551
+ const port = options.port ? parseInt(options.port, 10) : 4242;
1552
+ const host = options.host ?? '127.0.0.1';
1553
+ // Lazy-load the server package via dist path. The npm bin is
1554
+ // packaged with the server already built under
1555
+ // node_modules/@selvakumaresra/specship-server, OR (dev) the
1556
+ // sibling packages/server/dist directory.
1557
+ const { createServer } = await loadServerPackage();
1558
+ // Locate the built Angular UI so the same port serves the SPA at /.
1559
+ // `--no-web` opts out (Commander sets web=false then). Otherwise we
1560
+ // try the explicit --web-dir, then auto-detect.
1561
+ const webDir = options.web === false ? null : locateWebDir(options.webDir ?? null);
1562
+ // The JSONL ingest watcher now starts in-process inside createServer
1563
+ // when `ingest: true`. The server owns its lifecycle; CLI just toggles.
1564
+ const handle = await createServer({
1565
+ projectRoot: root ?? undefined,
1566
+ host,
1567
+ port,
1568
+ ingest: options.ingest !== false,
1569
+ webDir,
1570
+ verbose: false,
1571
+ });
1572
+ console.error(chalk.bold('\nSpecShip Desktop server\n'));
1573
+ console.error(chalk.green((0, glyphs_1.getGlyphs)().ok) + ` HTTP API: ${handle.url}`);
1574
+ if (root) {
1575
+ console.error(chalk.dim(` project: ${root}`));
1576
+ }
1577
+ else {
1578
+ console.error(chalk.yellow((0, glyphs_1.getGlyphs)().warn) + ' no primary project — pick one in the desktop UI');
1579
+ console.error(chalk.dim(' analytics endpoints will return 409 until one is selected'));
1580
+ }
1581
+ if (webDir) {
1582
+ console.error(chalk.green((0, glyphs_1.getGlyphs)().ok) + ` SPA: ${handle.url}/ (from ${webDir})`);
1583
+ }
1584
+ else if (options.web !== false) {
1585
+ console.error(chalk.yellow((0, glyphs_1.getGlyphs)().warn) + ' UI not found — running API-only.');
1586
+ console.error(chalk.dim(' Build it first: cd packages/server && npm run build:all'));
1587
+ }
1588
+ if (options.ingest !== false) {
1589
+ console.error(chalk.dim(' Claude Code transcript ingest watcher active'));
1590
+ }
1591
+ if (options.mcp) {
1592
+ if (!root) {
1593
+ console.error(chalk.yellow((0, glyphs_1.getGlyphs)().warn) + ' --mcp needs an initialized project — skipping MCP stdio');
1594
+ }
1595
+ else {
1596
+ // Also start MCP stdio in this process. The two are unrelated
1597
+ // surfaces hitting the same specship instance, both safe under WAL.
1598
+ const { MCPServer } = await Promise.resolve().then(() => __importStar(require('../mcp/index')));
1599
+ const mcp = new MCPServer(root);
1600
+ void mcp.start();
1601
+ console.error(chalk.green((0, glyphs_1.getGlyphs)().ok) + ' MCP stdio: running');
1602
+ }
1603
+ }
1604
+ const shutdown = async () => {
1605
+ console.error(chalk.dim('shutting down…'));
1606
+ await handle.stop();
1607
+ process.exit(0);
1608
+ };
1609
+ process.on('SIGINT', () => { void shutdown(); });
1610
+ process.on('SIGTERM', () => { void shutdown(); });
1611
+ // Server now runs until terminated.
1612
+ return;
1613
+ }
1614
+ if (options.mcp) {
1615
+ // Start MCP server - it handles initialization lazily based on rootUri from client
1616
+ const { MCPServer } = await Promise.resolve().then(() => __importStar(require('../mcp/index')));
1617
+ const server = new MCPServer(projectPath);
1618
+ await server.start();
1619
+ // Server will run until terminated
1620
+ }
1621
+ else {
1622
+ // Default: show info about MCP mode.
1623
+ // Use stderr so stdout stays clean for any piped/stdio usage.
1624
+ console.error(chalk.bold('\nSpecShip MCP Server\n'));
1625
+ console.error(chalk.blue((0, glyphs_1.getGlyphs)().info) + ' Use --mcp flag to start the MCP server');
1626
+ console.error('\nTo use with Claude Code, add to your MCP configuration:');
1627
+ console.error(chalk.dim(`
1628
+ {
1629
+ "mcpServers": {
1630
+ "specship": {
1631
+ "command": "specship",
1632
+ "args": ["serve", "--mcp"]
1633
+ }
1634
+ }
1635
+ }
1636
+ `));
1637
+ console.error('Available tools:');
1638
+ console.error(chalk.cyan(' specship_explore') + ' - Primary: source of the relevant symbols for any question');
1639
+ console.error(chalk.cyan(' specship_search') + ' - Search for code symbols');
1640
+ console.error(chalk.cyan(' specship_callers') + ' - Find callers of a symbol');
1641
+ console.error(chalk.cyan(' specship_callees') + ' - Find what a symbol calls');
1642
+ console.error(chalk.cyan(' specship_impact') + ' - Analyze impact of changes');
1643
+ console.error(chalk.cyan(' specship_node') + ' - Get symbol details');
1644
+ console.error(chalk.cyan(' specship_files') + ' - Get project file structure');
1645
+ console.error(chalk.cyan(' specship_status') + ' - Get index status');
1646
+ }
1647
+ }
1648
+ catch (err) {
1649
+ error(`Failed to start server: ${err instanceof Error ? err.message : String(err)}`);
1650
+ process.exit(1);
1651
+ }
1652
+ });
1653
+ /**
1654
+ * specship unlock [path]
1655
+ */
1656
+ program
1657
+ .command('unlock [path]')
1658
+ .description('Remove a stale lock file that is blocking indexing')
1659
+ .action(async (pathArg) => {
1660
+ const projectPath = resolveProjectPath(pathArg);
1661
+ try {
1662
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1663
+ error(`SpecShip not initialized in ${projectPath}`);
1664
+ return;
1665
+ }
1666
+ const lockPath = path.join((0, directory_1.getSpecShipDir)(projectPath), 'specship.lock');
1667
+ if (!fs.existsSync(lockPath)) {
1668
+ info(`No lock file found ${(0, glyphs_1.getGlyphs)().dash} nothing to do`);
1669
+ return;
1670
+ }
1671
+ fs.unlinkSync(lockPath);
1672
+ success('Removed lock file. You can now run indexing again.');
1673
+ }
1674
+ catch (err) {
1675
+ error(`Failed to remove lock: ${err instanceof Error ? err.message : String(err)}`);
1676
+ process.exit(1);
1677
+ }
1678
+ });
1679
+ /**
1680
+ * specship callers <symbol>
1681
+ *
1682
+ * CLI parity with the MCP graph tools (specship_callers/callees/impact) so the
1683
+ * traversal queries work in scripts, CI, and git hooks without a running MCP
1684
+ * server.
1685
+ */
1686
+ program
1687
+ .command('callers <symbol>')
1688
+ .description('Find all functions/methods that call a specific symbol')
1689
+ .option('-p, --path <path>', 'Project path')
1690
+ .option('-l, --limit <number>', 'Maximum results', '20')
1691
+ .option('-j, --json', 'Output as JSON')
1692
+ .action(async (symbol, options) => {
1693
+ const projectPath = resolveProjectPath(options.path);
1694
+ try {
1695
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1696
+ error(`SpecShip not initialized in ${projectPath}`);
1697
+ process.exit(1);
1698
+ }
1699
+ const { default: SpecShip } = await loadSpecShip();
1700
+ const cg = await SpecShip.open(projectPath);
1701
+ const limit = parseInt(options.limit || '20', 10);
1702
+ const matches = cg.searchNodes(symbol, { limit: 50 });
1703
+ if (matches.length === 0) {
1704
+ info(`Symbol "${symbol}" not found`);
1705
+ cg.destroy();
1706
+ return;
1707
+ }
1708
+ const seen = new Set();
1709
+ const allCallers = [];
1710
+ for (const match of matches) {
1711
+ const exactMatch = match.node.name === symbol || match.node.name.endsWith(`.${symbol}`) || match.node.name.endsWith(`::${symbol}`);
1712
+ if (!exactMatch && matches.length > 1)
1713
+ continue;
1714
+ for (const c of cg.getCallers(match.node.id)) {
1715
+ if (!seen.has(c.node.id)) {
1716
+ seen.add(c.node.id);
1717
+ allCallers.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
1718
+ }
1719
+ }
1720
+ }
1721
+ // Fallback: if exact filter removed everything, use the top match
1722
+ if (allCallers.length === 0 && matches[0]) {
1723
+ for (const c of cg.getCallers(matches[0].node.id)) {
1724
+ if (!seen.has(c.node.id)) {
1725
+ seen.add(c.node.id);
1726
+ allCallers.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
1727
+ }
1728
+ }
1729
+ }
1730
+ const limited = allCallers.slice(0, limit);
1731
+ if (options.json) {
1732
+ console.log(JSON.stringify({ symbol, callers: limited }, null, 2));
1733
+ }
1734
+ else if (limited.length === 0) {
1735
+ info(`No callers found for "${symbol}"`);
1736
+ }
1737
+ else {
1738
+ console.log(chalk.bold(`\nCallers of "${symbol}" (${limited.length}):\n`));
1739
+ for (const node of limited) {
1740
+ const loc = node.startLine ? `:${node.startLine}` : '';
1741
+ console.log(chalk.cyan(node.kind.padEnd(12)) +
1742
+ chalk.white(node.name));
1743
+ console.log(chalk.dim(` ${node.filePath}${loc}`));
1744
+ console.log();
1745
+ }
1746
+ }
1747
+ cg.destroy();
1748
+ }
1749
+ catch (err) {
1750
+ error(`callers failed: ${err instanceof Error ? err.message : String(err)}`);
1751
+ process.exit(1);
1752
+ }
1753
+ });
1754
+ /**
1755
+ * specship callees <symbol>
1756
+ */
1757
+ program
1758
+ .command('callees <symbol>')
1759
+ .description('Find all functions/methods that a specific symbol calls')
1760
+ .option('-p, --path <path>', 'Project path')
1761
+ .option('-l, --limit <number>', 'Maximum results', '20')
1762
+ .option('-j, --json', 'Output as JSON')
1763
+ .action(async (symbol, options) => {
1764
+ const projectPath = resolveProjectPath(options.path);
1765
+ try {
1766
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1767
+ error(`SpecShip not initialized in ${projectPath}`);
1768
+ process.exit(1);
1769
+ }
1770
+ const { default: SpecShip } = await loadSpecShip();
1771
+ const cg = await SpecShip.open(projectPath);
1772
+ const limit = parseInt(options.limit || '20', 10);
1773
+ const matches = cg.searchNodes(symbol, { limit: 50 });
1774
+ if (matches.length === 0) {
1775
+ info(`Symbol "${symbol}" not found`);
1776
+ cg.destroy();
1777
+ return;
1778
+ }
1779
+ const seen = new Set();
1780
+ const allCallees = [];
1781
+ for (const match of matches) {
1782
+ const exactMatch = match.node.name === symbol || match.node.name.endsWith(`.${symbol}`) || match.node.name.endsWith(`::${symbol}`);
1783
+ if (!exactMatch && matches.length > 1)
1784
+ continue;
1785
+ for (const c of cg.getCallees(match.node.id)) {
1786
+ if (!seen.has(c.node.id)) {
1787
+ seen.add(c.node.id);
1788
+ allCallees.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
1789
+ }
1790
+ }
1791
+ }
1792
+ if (allCallees.length === 0 && matches[0]) {
1793
+ for (const c of cg.getCallees(matches[0].node.id)) {
1794
+ if (!seen.has(c.node.id)) {
1795
+ seen.add(c.node.id);
1796
+ allCallees.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
1797
+ }
1798
+ }
1799
+ }
1800
+ const limited = allCallees.slice(0, limit);
1801
+ if (options.json) {
1802
+ console.log(JSON.stringify({ symbol, callees: limited }, null, 2));
1803
+ }
1804
+ else if (limited.length === 0) {
1805
+ info(`No callees found for "${symbol}"`);
1806
+ }
1807
+ else {
1808
+ console.log(chalk.bold(`\nCallees of "${symbol}" (${limited.length}):\n`));
1809
+ for (const node of limited) {
1810
+ const loc = node.startLine ? `:${node.startLine}` : '';
1811
+ console.log(chalk.cyan(node.kind.padEnd(12)) +
1812
+ chalk.white(node.name));
1813
+ console.log(chalk.dim(` ${node.filePath}${loc}`));
1814
+ console.log();
1815
+ }
1816
+ }
1817
+ cg.destroy();
1818
+ }
1819
+ catch (err) {
1820
+ error(`callees failed: ${err instanceof Error ? err.message : String(err)}`);
1821
+ process.exit(1);
1822
+ }
1823
+ });
1824
+ /**
1825
+ * specship impact <symbol>
1826
+ */
1827
+ program
1828
+ .command('impact <symbol>')
1829
+ .description('Analyze what code is affected by changing a symbol')
1830
+ .option('-p, --path <path>', 'Project path')
1831
+ .option('-d, --depth <number>', 'Traversal depth', '2')
1832
+ .option('-j, --json', 'Output as JSON')
1833
+ .action(async (symbol, options) => {
1834
+ const projectPath = resolveProjectPath(options.path);
1835
+ try {
1836
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1837
+ error(`SpecShip not initialized in ${projectPath}`);
1838
+ process.exit(1);
1839
+ }
1840
+ const { default: SpecShip } = await loadSpecShip();
1841
+ const cg = await SpecShip.open(projectPath);
1842
+ const depth = Math.min(Math.max(parseInt(options.depth || '2', 10), 1), 10);
1843
+ const matches = cg.searchNodes(symbol, { limit: 50 });
1844
+ if (matches.length === 0) {
1845
+ info(`Symbol "${symbol}" not found`);
1846
+ cg.destroy();
1847
+ return;
1848
+ }
1849
+ // Merge impact subgraphs across all exact-matching symbols
1850
+ const mergedNodes = new Map();
1851
+ const seenEdges = new Set();
1852
+ let edgeCount = 0;
1853
+ for (const match of matches) {
1854
+ const exactMatch = match.node.name === symbol || match.node.name.endsWith(`.${symbol}`) || match.node.name.endsWith(`::${symbol}`);
1855
+ if (!exactMatch && matches.length > 1)
1856
+ continue;
1857
+ const impact = cg.getImpactRadius(match.node.id, depth);
1858
+ for (const [id, n] of impact.nodes) {
1859
+ mergedNodes.set(id, { name: n.name, kind: n.kind, filePath: n.filePath, startLine: n.startLine });
1860
+ }
1861
+ for (const e of impact.edges) {
1862
+ const key = `${e.source}->${e.target}:${e.kind}`;
1863
+ if (!seenEdges.has(key)) {
1864
+ seenEdges.add(key);
1865
+ edgeCount++;
1866
+ }
1867
+ }
1868
+ }
1869
+ // Fallback to top match if exact filter removed everything
1870
+ if (mergedNodes.size === 0 && matches[0]) {
1871
+ const impact = cg.getImpactRadius(matches[0].node.id, depth);
1872
+ for (const [id, n] of impact.nodes) {
1873
+ mergedNodes.set(id, { name: n.name, kind: n.kind, filePath: n.filePath, startLine: n.startLine });
1874
+ }
1875
+ edgeCount = impact.edges.length;
1876
+ }
1877
+ if (options.json) {
1878
+ console.log(JSON.stringify({
1879
+ symbol,
1880
+ depth,
1881
+ nodeCount: mergedNodes.size,
1882
+ edgeCount,
1883
+ affected: Array.from(mergedNodes.values()),
1884
+ }, null, 2));
1885
+ }
1886
+ else if (mergedNodes.size === 0) {
1887
+ info(`No affected symbols found for "${symbol}"`);
1888
+ }
1889
+ else {
1890
+ console.log(chalk.bold(`\nImpact of changing "${symbol}" — ${mergedNodes.size} affected symbols:\n`));
1891
+ // Group by file
1892
+ const byFile = new Map();
1893
+ for (const node of mergedNodes.values()) {
1894
+ const list = byFile.get(node.filePath) || [];
1895
+ list.push({ name: node.name, kind: node.kind, startLine: node.startLine });
1896
+ byFile.set(node.filePath, list);
1897
+ }
1898
+ for (const [file, nodes] of byFile) {
1899
+ console.log(chalk.cyan(file));
1900
+ for (const node of nodes) {
1901
+ const loc = node.startLine ? `:${node.startLine}` : '';
1902
+ console.log(` ${chalk.dim(node.kind.padEnd(12))}${node.name}${chalk.dim(loc)}`);
1903
+ }
1904
+ console.log();
1905
+ }
1906
+ }
1907
+ cg.destroy();
1908
+ }
1909
+ catch (err) {
1910
+ error(`impact failed: ${err instanceof Error ? err.message : String(err)}`);
1911
+ process.exit(1);
1912
+ }
1913
+ });
1914
+ /**
1915
+ * specship affected [files...]
1916
+ *
1917
+ * Find test files affected by the given source files.
1918
+ * Traces dependency edges transitively to find test files that depend on changed code.
1919
+ *
1920
+ * Usage:
1921
+ * git diff --name-only | specship affected --stdin
1922
+ * specship affected src/lib/components/Editor.svelte src/routes/+page.svelte
1923
+ */
1924
+ program
1925
+ .command('affected [files...]')
1926
+ .description('Find test files affected by changed source files')
1927
+ .option('-p, --path <path>', 'Project path')
1928
+ .option('--stdin', 'Read file list from stdin (one per line)')
1929
+ .option('-d, --depth <number>', 'Max dependency traversal depth', '5')
1930
+ .option('-f, --filter <glob>', 'Custom glob filter for test files (e.g. "e2e/*.spec.ts")')
1931
+ .option('-j, --json', 'Output as JSON')
1932
+ .option('-q, --quiet', 'Only output file paths, no decoration')
1933
+ .action(async (fileArgs, options) => {
1934
+ const projectPath = resolveProjectPath(options.path);
1935
+ try {
1936
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1937
+ error(`SpecShip not initialized in ${projectPath}`);
1938
+ process.exit(1);
1939
+ }
1940
+ // Collect changed files from args or stdin
1941
+ let changedFiles = [...(fileArgs || [])];
1942
+ if (options.stdin) {
1943
+ const stdinData = fs.readFileSync(0, 'utf-8');
1944
+ const stdinFiles = stdinData.split('\n').map(f => f.trim()).filter(Boolean);
1945
+ changedFiles.push(...stdinFiles);
1946
+ }
1947
+ if (changedFiles.length === 0) {
1948
+ if (!options.quiet)
1949
+ info('No files provided. Use file arguments or --stdin.');
1950
+ process.exit(0);
1951
+ }
1952
+ const { default: SpecShip } = await loadSpecShip();
1953
+ const cg = await SpecShip.open(projectPath);
1954
+ const maxDepth = parseInt(options.depth || '5', 10);
1955
+ // Common test file patterns
1956
+ const defaultTestPatterns = [
1957
+ /\.spec\./,
1958
+ /\.test\./,
1959
+ /\/__tests__\//,
1960
+ /\/tests?\//,
1961
+ /\/e2e\//,
1962
+ /\/spec\//,
1963
+ ];
1964
+ // Custom filter pattern
1965
+ let customFilter = null;
1966
+ if (options.filter) {
1967
+ // Convert glob to regex: ** → .+, * → [^/]*, . → \.
1968
+ const regex = options.filter
1969
+ .replace(/[+[\]{}()^$|\\]/g, '\\$&')
1970
+ .replace(/\./g, '\\.')
1971
+ .replace(/\*\*/g, '.+')
1972
+ .replace(/\*/g, '[^/]*');
1973
+ customFilter = new RegExp(regex);
1974
+ }
1975
+ function isTestFile(filePath) {
1976
+ if (customFilter)
1977
+ return customFilter.test(filePath);
1978
+ return defaultTestPatterns.some(p => p.test(filePath));
1979
+ }
1980
+ // BFS to find all transitive dependents of changed files, filtered to test files
1981
+ const affectedTests = new Set();
1982
+ const allDependents = new Set();
1983
+ for (const file of changedFiles) {
1984
+ // If the changed file is itself a test file, include it
1985
+ if (isTestFile(file)) {
1986
+ affectedTests.add(file);
1987
+ continue;
1988
+ }
1989
+ // BFS through dependents
1990
+ const queue = [{ file, depth: 0 }];
1991
+ const visited = new Set();
1992
+ visited.add(file);
1993
+ while (queue.length > 0) {
1994
+ const current = queue.shift();
1995
+ if (current.depth >= maxDepth)
1996
+ continue;
1997
+ const dependents = cg.getFileDependents(current.file);
1998
+ for (const dep of dependents) {
1999
+ if (visited.has(dep))
2000
+ continue;
2001
+ visited.add(dep);
2002
+ allDependents.add(dep);
2003
+ if (isTestFile(dep)) {
2004
+ affectedTests.add(dep);
2005
+ }
2006
+ else {
2007
+ queue.push({ file: dep, depth: current.depth + 1 });
2008
+ }
2009
+ }
2010
+ }
2011
+ }
2012
+ const sortedTests = Array.from(affectedTests).sort();
2013
+ // Output
2014
+ if (options.json) {
2015
+ console.log(JSON.stringify({
2016
+ changedFiles,
2017
+ affectedTests: sortedTests,
2018
+ totalDependentsTraversed: allDependents.size,
2019
+ }, null, 2));
2020
+ }
2021
+ else if (options.quiet) {
2022
+ for (const t of sortedTests)
2023
+ console.log(t);
2024
+ }
2025
+ else {
2026
+ if (sortedTests.length === 0) {
2027
+ info('No test files affected by the changed files.');
2028
+ }
2029
+ else {
2030
+ console.log(chalk.bold(`\nAffected test files (${sortedTests.length}):\n`));
2031
+ for (const t of sortedTests) {
2032
+ console.log(' ' + chalk.cyan(t));
2033
+ }
2034
+ console.log();
2035
+ }
2036
+ }
2037
+ cg.destroy();
2038
+ }
2039
+ catch (err) {
2040
+ error(`Affected analysis failed: ${err instanceof Error ? err.message : String(err)}`);
2041
+ process.exit(1);
2042
+ }
2043
+ });
2044
+ /**
2045
+ * specship install
2046
+ */
2047
+ program
2048
+ .command('install')
2049
+ .description('Install specship MCP server into Claude Code')
2050
+ .option('-l, --location <where>', 'Install location: "global" or "local". Default: prompt (local)')
2051
+ .option('-y, --yes', 'Non-interactive: defaults to --location=local, auto-allow on')
2052
+ .option('--no-permissions', 'Skip writing the auto-allow permissions list')
2053
+ .option('--sdd', 'Also install the spec-driven-development governance tier (spec/authoring/review/design commands + the spec-author nudge hook). Off by default — a plain install provisions only the retrieval tier.')
2054
+ .option('--statusline', 'Wire the SpecShip status-line segment into Claude (skips the prompt; never overwrites an existing status line)')
2055
+ .option('--skip-statusline', 'Do not add the status-line segment (skips the prompt)')
2056
+ .option('--skip-index', 'Do not offer to index the current project (an explicit opt-out for automation)')
2057
+ .option('--print-config', 'Print MCP config snippet for Claude Code and exit (no file writes)')
2058
+ // -t/--target is vestigial — kept so existing `--target claude` / `--target auto`
2059
+ // invocations (including our own offline-install scripts) keep working.
2060
+ .option('-t, --target <ids>', '(vestigial) accepted: "claude" | "auto" | "all" | "none"')
2061
+ .action(async (opts) => {
2062
+ if (opts.printConfig) {
2063
+ const { claudeTarget } = await Promise.resolve().then(() => __importStar(require('../installer/targets/claude')));
2064
+ const loc = (opts.location === 'local' ? 'local' : 'global');
2065
+ process.stdout.write(claudeTarget.printConfig(loc));
2066
+ return;
2067
+ }
2068
+ const { runInstallerWithOptions } = await Promise.resolve().then(() => __importStar(require('../installer')));
2069
+ if (opts.location && opts.location !== 'global' && opts.location !== 'local') {
2070
+ error(`--location must be "global" or "local" (got "${opts.location}").`);
2071
+ process.exit(1);
2072
+ }
2073
+ try {
2074
+ // Commander's `--no-permissions` makes `opts.permissions === false`;
2075
+ // omitting the flag leaves it `true` (the positive-form default).
2076
+ // We MUST treat the default-true as "user did not override — let
2077
+ // the orchestrator prompt" and only forward an explicit `false`
2078
+ // (or `true` when --yes implies it). Otherwise the auto-allow
2079
+ // prompt is silently skipped on every interactive run.
2080
+ const explicitNoPermissions = opts.permissions === false;
2081
+ const autoAllow = explicitNoPermissions
2082
+ ? false
2083
+ : opts.yes
2084
+ ? true
2085
+ : undefined;
2086
+ // Status-line opt-in: `--statusline` forces on, `--skip-statusline`
2087
+ // forces off (both skip the prompt); neither ⇒ undefined ⇒ ask
2088
+ // interactively (default no). Distinct flag names dodge commander's
2089
+ // `--no-*` default-true coupling that would auto-install.
2090
+ const statusLine = opts.statusline
2091
+ ? true
2092
+ : opts.skipStatusline
2093
+ ? false
2094
+ : undefined;
2095
+ // The governance tier is opt-in (INSTALL-WEDGE-DOC): `--sdd` makes
2096
+ // `opts.sdd === true`; omitting it leaves it undefined → retrieval-only.
2097
+ // Forward `true` only when the user explicitly opted in.
2098
+ await runInstallerWithOptions({
2099
+ target: opts.target,
2100
+ location: opts.location,
2101
+ autoAllow,
2102
+ sdd: opts.sdd === true ? true : undefined,
2103
+ statusLine,
2104
+ yes: opts.yes,
2105
+ });
2106
+ // Offer to index the current project (REQ-HANDSHAKE-004) before the smoke
2107
+ // check, so an accepted index is reflected by the index-queryable item.
2108
+ const cwd = process.cwd();
2109
+ const { decideInstallInit } = await Promise.resolve().then(() => __importStar(require('../installer/init-offer')));
2110
+ const { isGitRepo } = await Promise.resolve().then(() => __importStar(require('../sync/git-hooks')));
2111
+ const initDecision = decideInstallInit({
2112
+ isGitRepo: isGitRepo(cwd),
2113
+ isInitialized: (0, directory_1.isInitialized)(cwd),
2114
+ yes: opts.yes === true,
2115
+ skipIndex: opts.skipIndex === true,
2116
+ });
2117
+ let doIndex = initDecision === 'auto-index';
2118
+ if (initDecision === 'offer') {
2119
+ const clack = await importESM('@clack/prompts');
2120
+ const ans = await clack.confirm({
2121
+ message: `Index this project (${cwd}) now, so Claude can explore it?`,
2122
+ });
2123
+ doIndex = ans === true; // a cancel (symbol) or "no" both decline (REQ-HANDSHAKE-004.A2)
2124
+ }
2125
+ if (doIndex) {
2126
+ console.log();
2127
+ info(`Indexing ${cwd} …`);
2128
+ try {
2129
+ await buildProjectIndex(cwd);
2130
+ success('Project indexed.');
2131
+ }
2132
+ catch (e) {
2133
+ warn(`Indexing failed (run \`specship init -i\` later): ${e instanceof Error ? e.message : String(e)}`);
2134
+ }
2135
+ }
2136
+ // Post-install smoke check (REQ-HANDSHAKE-002). Advisory: report failing
2137
+ // items but never exit non-zero (REQ-HANDSHAKE-002.A4), so a provisioning
2138
+ // script is never broken by it — the gating equivalent is `specship doctor`.
2139
+ const { runSmokeCheck } = await Promise.resolve().then(() => __importStar(require('../health/smoke-check')));
2140
+ const smoke = await runSmokeCheck({ projectRoot: process.cwd() });
2141
+ console.log('\n' + chalk.bold('Install check'));
2142
+ renderSmokeCheck(smoke);
2143
+ if (!smoke.ok) {
2144
+ console.log(chalk.dim(' (advisory — diagnose anytime with `specship doctor`)'));
2145
+ }
2146
+ // Restart reminder (REQ-HANDSHAKE-001): an MCP server added to the config
2147
+ // is NOT visible to a Claude Code session that was already open.
2148
+ console.log();
2149
+ info('Restart Claude Code (or run `/mcp`) to load the SpecShip server — it is not visible in a session that is already open.');
2150
+ }
2151
+ catch (err) {
2152
+ error(err instanceof Error ? err.message : String(err));
2153
+ process.exit(1);
2154
+ }
2155
+ });
2156
+ /**
2157
+ * specship doctor — diagnose an install (REQ-HANDSHAKE-003). Read-only: runs the
2158
+ * same checks as the post-install smoke check and writes nothing. Exits non-zero
2159
+ * on a usage-blocking failure so it can gate a script or CI step.
2160
+ */
2161
+ program
2162
+ .command('doctor [path]')
2163
+ .description('Diagnose a SpecShip install (runtime · FTS5 · MCP boot · index). Exits non-zero on a usage-blocking failure.')
2164
+ .option('-j, --json', 'Output as JSON')
2165
+ .action(async (pathArg, options) => {
2166
+ const projectRoot = path.resolve(pathArg ?? process.cwd());
2167
+ const { runSmokeCheck, doctorExitCode } = await Promise.resolve().then(() => __importStar(require('../health/smoke-check')));
2168
+ const result = await runSmokeCheck({ projectRoot });
2169
+ const code = doctorExitCode(result);
2170
+ if (options.json) {
2171
+ console.log(JSON.stringify(result, null, 2));
2172
+ process.exit(code);
2173
+ }
2174
+ console.log(chalk.bold('\nSpecShip doctor\n'));
2175
+ renderSmokeCheck(result);
2176
+ console.log();
2177
+ if (code === 0) {
2178
+ success('No usage-blocking problems detected.');
2179
+ }
2180
+ else {
2181
+ error(`Usage-blocking checks failed: ${result.blockingFailures.map((i) => i.id).join(', ')}`);
2182
+ }
2183
+ process.exit(code);
2184
+ });
2185
+ /**
2186
+ * specship starter-prompt — print the manufactured first-run flow/impact prompt
2187
+ * (ACTIVATION-DOC). The single delivery mechanism for the bare `/ss-explore`
2188
+ * door: it self-gates (prints nothing if the index can't be queried —
2189
+ * REQ-ACTIVATION-004) and self-retires (prints nothing once a real specship
2190
+ * lookup has been recorded this session — REQ-ACTIVATION-003).
2191
+ */
2192
+ program
2193
+ .command('starter-prompt [path]')
2194
+ .description('Print a suggested first flow/impact prompt for this project (used by the /ss-explore door).')
2195
+ .option('-j, --json', 'Output as JSON')
2196
+ .action(async (pathArg, options) => {
2197
+ const projectRoot = path.resolve(pathArg ?? process.cwd());
2198
+ // Not indexed → nothing (REQ-ACTIVATION-004.A2: the door shows its own guidance).
2199
+ if (!(0, directory_1.isInitialized)(projectRoot))
2200
+ return;
2201
+ // Retire once the agent has actually used retrieval this session
2202
+ // (REQ-ACTIVATION-003) — the per-session marker counts specship lookups.
2203
+ try {
2204
+ const { readSessionMarker } = await Promise.resolve().then(() => __importStar(require('../statusline/session-marker')));
2205
+ const marker = readSessionMarker(projectRoot);
2206
+ if (marker && marker.calls > 0)
2207
+ return;
2208
+ }
2209
+ catch {
2210
+ /* no marker yet → not retired */
2211
+ }
2212
+ try {
2213
+ const { default: SpecShip } = await loadSpecShip();
2214
+ const cg = await SpecShip.open(projectRoot);
2215
+ try {
2216
+ const { generateStarterPrompt } = await Promise.resolve().then(() => __importStar(require('../activation/starter-prompt')));
2217
+ const sp = generateStarterPrompt(cg);
2218
+ if (!sp)
2219
+ return; // can't generate (unqueryable index / empty graph) → nothing (REQ-ACTIVATION-004)
2220
+ if (options.json) {
2221
+ console.log(JSON.stringify(sp));
2222
+ }
2223
+ else {
2224
+ console.log(sp.prompt);
2225
+ }
2226
+ }
2227
+ finally {
2228
+ cg.destroy();
2229
+ }
2230
+ }
2231
+ catch {
2232
+ // Index unreadable / FTS5 missing → print nothing (REQ-ACTIVATION-004.A1).
2233
+ }
2234
+ });
2235
+ /**
2236
+ * specship spec-nudge (internal — installed as a UserPromptSubmit hook)
2237
+ *
2238
+ * Reads the UserPromptSubmit JSON payload from stdin and, when the prompt
2239
+ * looks like feature/bug work, prints a non-blocking reminder (as
2240
+ * `hookSpecificOutput.additionalContext`) to author the spec via spec-author
2241
+ * first. Always exits 0 so the prompt proceeds. Conservative: skips clear
2242
+ * questions / read-only asks so it doesn't nag (SDD-INSTALL-DOC, REQ-SDD-002).
2243
+ */
2244
+ program
2245
+ .command('spec-nudge')
2246
+ .description('Internal hook: nudge toward spec-author on feature/bug intent (UserPromptSubmit)')
2247
+ .action(async () => {
2248
+ const shouldNudge = (prompt) => {
2249
+ const text = (prompt || '').trim();
2250
+ if (text.length < 12)
2251
+ return false;
2252
+ if (/[?]\s*$/.test(text))
2253
+ return false; // a question
2254
+ const lower = text.toLowerCase();
2255
+ // Read-only / interrogative openers — never spec-shaped.
2256
+ if (/^(what|why|how|where|who|when|is|are|does|do|can|could|should|explain|show|list|describe|tell|summar|read|find|search|look|run|why)\b/.test(lower))
2257
+ return false;
2258
+ // Feature / bug-shaped intent verbs.
2259
+ return /\b(add|implement|build|create|introduce|support|enhance|fix|bug|broken|refactor|rewrite|migrate|change|modify|update|wire|integrate|feature|spec out)\b/.test(lower);
2260
+ };
2261
+ const chunks = [];
2262
+ try {
2263
+ for await (const c of process.stdin)
2264
+ chunks.push(c);
2265
+ }
2266
+ catch { /* no stdin — nothing to nudge */ }
2267
+ const raw = Buffer.concat(chunks).toString('utf-8').trim();
2268
+ let prompt = '';
2269
+ if (raw) {
2270
+ try {
2271
+ const parsed = JSON.parse(raw);
2272
+ prompt = typeof parsed?.prompt === 'string' ? parsed.prompt : raw;
2273
+ }
2274
+ catch {
2275
+ prompt = raw; // not JSON — treat as the raw prompt text
2276
+ }
2277
+ }
2278
+ if (shouldNudge(prompt)) {
2279
+ const additionalContext = 'This repo uses spec-driven development (SpecShip). Before any brainstorming or ' +
2280
+ 'planning skill, FIRST invoke spec-author to author the spec under specs/ for this ' +
2281
+ 'work — the spec is the contract; implement from it with /ss-spec implement.';
2282
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext } }) + '\n');
2283
+ }
2284
+ process.exit(0);
2285
+ });
2286
+ /**
2287
+ * specship uninstall
2288
+ *
2289
+ * Inverse of `install`. Removes the specship MCP server entry and
2290
+ * permissions from Claude Code. Prompts global-vs-local when not
2291
+ * given. Does NOT delete the `.specship/` index — that's
2292
+ * `specship uninit`.
2293
+ */
2294
+ program
2295
+ .command('uninstall')
2296
+ .description('Remove specship from Claude Code')
2297
+ .option('-l, --location <where>', 'Uninstall location: "global" or "local". Default: prompt (local)')
2298
+ .option('-y, --yes', 'Non-interactive: defaults to --location=local')
2299
+ // vestigial — kept so existing `--target claude` invocations keep working.
2300
+ .option('-t, --target <ids>', '(vestigial) accepted: "claude" | "auto" | "all" | "none"')
2301
+ .action(async (opts) => {
2302
+ const { runUninstaller } = await Promise.resolve().then(() => __importStar(require('../installer')));
2303
+ if (opts.location && opts.location !== 'global' && opts.location !== 'local') {
2304
+ error(`--location must be "global" or "local" (got "${opts.location}").`);
2305
+ process.exit(1);
2306
+ }
2307
+ try {
2308
+ await runUninstaller({
2309
+ target: opts.target,
2310
+ location: opts.location,
2311
+ yes: opts.yes,
2312
+ });
2313
+ }
2314
+ catch (err) {
2315
+ error(err instanceof Error ? err.message : String(err));
2316
+ process.exit(1);
2317
+ }
2318
+ });
2319
+ // =============================================================================
2320
+ // Spec layer / Workflow engine commands (v5)
2321
+ // =============================================================================
2322
+ program
2323
+ .command('drifted [path]')
2324
+ .description('List spec links in concerning states (drifted, broken, orphaned).')
2325
+ .option('-s, --state <list>', 'comma-separated states (default: drifted,broken,orphaned)')
2326
+ .option('-l, --limit <n>', 'max links to print (default: 50)')
2327
+ .option('--fail-on <list>', 'comma-separated states that cause non-zero exit (CI gate)')
2328
+ .option('--json', 'emit JSON')
2329
+ .action(async (pathArg, options) => {
2330
+ const projectRoot = path.resolve(pathArg ?? process.cwd());
2331
+ if (!(0, directory_1.isInitialized)(projectRoot)) {
2332
+ error(`SpecShip not initialized in ${projectRoot}. Run \`specship init -i\` first.`);
2333
+ process.exit(1);
2334
+ }
2335
+ const { SpecShip } = await loadSpecShip();
2336
+ const cg = await SpecShip.open(projectRoot);
2337
+ try {
2338
+ const states = (options.state ?? 'drifted,broken,orphaned')
2339
+ .split(',')
2340
+ .map((s) => s.trim())
2341
+ .filter(Boolean);
2342
+ const limit = options.limit ? Math.max(1, parseInt(options.limit, 10) || 50) : 50;
2343
+ const links = cg.getSpecQueries().getLinksByState(states).slice(0, limit);
2344
+ if (options.json) {
2345
+ // eslint-disable-next-line no-console
2346
+ console.log(JSON.stringify({ count: links.length, links }, null, 2));
2347
+ }
2348
+ else if (links.length === 0) {
2349
+ // eslint-disable-next-line no-console
2350
+ console.log(`No links in states: ${states.join(', ')}. ✨ Drift queue is clean.`);
2351
+ }
2352
+ else {
2353
+ for (const link of links) {
2354
+ const drift = link.driftAxis ? ` [drift=${link.driftAxis}]` : '';
2355
+ // eslint-disable-next-line no-console
2356
+ console.log(`#${link.id} [${link.state}${drift}] ${link.specId} → ${link.targetFilePath}:${link.targetQualifiedName} <${link.provenance}>`);
2357
+ }
2358
+ }
2359
+ if (options.failOn) {
2360
+ const failStates = options.failOn.split(',').map((s) => s.trim()).filter(Boolean);
2361
+ const offending = links.filter((l) => failStates.includes(l.state));
2362
+ if (offending.length > 0) {
2363
+ process.exit(1);
2364
+ }
2365
+ }
2366
+ }
2367
+ finally {
2368
+ cg.close();
2369
+ }
2370
+ });
2371
+ // @implements REQ-DOMAIN-003
2372
+ // Thin surface over the read-only gap-seed pass (SpecShip.getDomainGapSeed,
2373
+ // REQ-DOMAIN-003) so the `/ss-spec domain` capture command can cite the SAME real
2374
+ // undocumented entities/specs the library computes (REQ-DOMAIN-004.A4) without a
2375
+ // new MCP tool (REQ-DOMAIN-005) or a runtime package import. Writes nothing.
2376
+ program
2377
+ .command('domain-gaps [path]')
2378
+ .description('List code entities and specs not yet covered by a domain fact (the domain gap-seed). Feeds the /ss-spec domain capture interview.')
2379
+ .option('-l, --limit <n>', 'max entities and specs to print in text mode (default: 50)')
2380
+ .option('--json', 'emit JSON')
2381
+ .action(async (pathArg, options) => {
2382
+ const projectRoot = path.resolve(pathArg ?? process.cwd());
2383
+ if (!(0, directory_1.isInitialized)(projectRoot)) {
2384
+ error(`SpecShip not initialized in ${projectRoot}. Run \`specship init -i\` first.`);
2385
+ process.exit(1);
2386
+ }
2387
+ const { default: SpecShip } = await loadSpecShip();
2388
+ const cg = await SpecShip.open(projectRoot);
2389
+ try {
2390
+ const seed = cg.getDomainGapSeed();
2391
+ if (options.json) {
2392
+ // eslint-disable-next-line no-console
2393
+ console.log(JSON.stringify(seed, null, 2));
2394
+ return;
2395
+ }
2396
+ const limit = options.limit ? Math.max(1, parseInt(options.limit, 10) || 50) : 50;
2397
+ const { documented, gaps } = seed.coverage;
2398
+ const total = documented + gaps;
2399
+ /* eslint-disable no-console */
2400
+ console.log(`Domain coverage: ${documented}/${total} documented · ${gaps} gap${gaps === 1 ? '' : 's'}`);
2401
+ if (gaps === 0) {
2402
+ console.log('✨ Every in-scope entity and spec is covered by a domain fact.');
2403
+ }
2404
+ else {
2405
+ if (seed.entities.length > 0) {
2406
+ console.log(`\nUndocumented entities (${seed.entities.length}):`);
2407
+ for (const e of seed.entities.slice(0, limit)) {
2408
+ console.log(` [${e.kind}] ${e.qualifiedName} — ${e.filePath}`);
2409
+ }
2410
+ if (seed.entities.length > limit)
2411
+ console.log(` … and ${seed.entities.length - limit} more`);
2412
+ }
2413
+ if (seed.specs.length > 0) {
2414
+ console.log(`\nUndocumented specs (${seed.specs.length}):`);
2415
+ for (const s of seed.specs.slice(0, limit)) {
2416
+ console.log(` [${s.kind}] ${s.id} — ${s.title}`);
2417
+ }
2418
+ if (seed.specs.length > limit)
2419
+ console.log(` … and ${seed.specs.length - limit} more`);
2420
+ }
2421
+ console.log(`\nCapture a fact for any of these with \`/ss-spec domain\`.`);
2422
+ }
2423
+ /* eslint-enable no-console */
2424
+ }
2425
+ finally {
2426
+ cg.close();
2427
+ }
2428
+ });
2429
+ // @implements REQ-FUNNEL-004
2430
+ program
2431
+ .command('spec [id]')
2432
+ .description('Spec lifecycle funnel (idea → spec → implemented). With an id, show that spec/brief detail.')
2433
+ .option('--json', 'emit JSON')
2434
+ .action(async (id, options) => {
2435
+ const projectRoot = path.resolve(process.cwd());
2436
+ if (!(0, directory_1.isInitialized)(projectRoot)) {
2437
+ error(`SpecShip not initialized in ${projectRoot}. Run \`specship init -i\` first.`);
2438
+ process.exit(1);
2439
+ }
2440
+ const { default: SpecShip } = await loadSpecShip();
2441
+ const { summarizeBriefFunnel, resolveBriefLink, findBriefsForSpec } = await Promise.resolve().then(() => __importStar(require('../resolution/brief-link-resolver')));
2442
+ const cg = await SpecShip.open(projectRoot);
2443
+ try {
2444
+ const sq = cg.getSpecQueries();
2445
+ // Implementation rollup for a document's requirements.
2446
+ const docRollup = (docId) => {
2447
+ const reqs = sq.getSpecsByParent(docId).filter((s) => s.kind === 'requirement');
2448
+ const r = { requirements: reqs.length, implemented: 0, verified: 0, drifted: 0, broken: 0, orphaned: 0 };
2449
+ for (const req of reqs) {
2450
+ for (const lk of sq.getLinksBySpec(req.id)) {
2451
+ if (lk.state === 'implemented')
2452
+ r.implemented++;
2453
+ else if (lk.state === 'verified')
2454
+ r.verified++;
2455
+ else if (lk.state === 'drifted')
2456
+ r.drifted++;
2457
+ else if (lk.state === 'broken')
2458
+ r.broken++;
2459
+ else if (lk.state === 'orphaned')
2460
+ r.orphaned++;
2461
+ }
2462
+ }
2463
+ return r;
2464
+ };
2465
+ const degraded = (r) => r.drifted + r.broken + r.orphaned;
2466
+ // ---- Detail view (an id was given) ----
2467
+ if (id) {
2468
+ const spec = sq.getSpecById(id);
2469
+ if (!spec) {
2470
+ if (options.json) {
2471
+ // eslint-disable-next-line no-console
2472
+ console.log(JSON.stringify({ error: 'not_found', id }, null, 2));
2473
+ }
2474
+ else {
2475
+ error(`No spec or brief with id "${id}".`);
2476
+ }
2477
+ process.exit(1);
2478
+ }
2479
+ if (spec.kind === 'brief') {
2480
+ const entry = summarizeBriefFunnel(sq, spec);
2481
+ const link = resolveBriefLink(sq, spec);
2482
+ if (options.json) {
2483
+ // eslint-disable-next-line no-console
2484
+ console.log(JSON.stringify({ ...entry, briefSide: link.briefSide, specSide: link.specSide }, null, 2));
2485
+ }
2486
+ else {
2487
+ /* eslint-disable no-console */
2488
+ console.log(`${spec.id} (brief) ${spec.title}`);
2489
+ console.log(` state: ${entry.state}`);
2490
+ if (entry.state === 'conflict') {
2491
+ console.log(` ⚠ conflict: brief → ${link.briefSide}, spec → ${link.specSide} (resolve the mismatched pointer)`);
2492
+ }
2493
+ else if (entry.linkedSpecId) {
2494
+ console.log(` linked spec: ${entry.linkedSpecId}`);
2495
+ }
2496
+ if (entry.rollup) {
2497
+ const r = entry.rollup;
2498
+ console.log(` rollup: ${r.requirements} reqs · ${r.implemented} implemented · ${r.verified} verified · ${degraded(r)} degraded`);
2499
+ }
2500
+ /* eslint-enable no-console */
2501
+ }
2502
+ }
2503
+ else {
2504
+ const links = sq.getLinksBySpec(spec.id);
2505
+ const briefs = spec.kind === 'document' ? findBriefsForSpec(sq, spec.id) : [];
2506
+ if (options.json) {
2507
+ const detail = { id: spec.id, kind: spec.kind, title: spec.title, links };
2508
+ if (spec.kind === 'document')
2509
+ detail.rollup = docRollup(spec.id);
2510
+ detail.briefs = briefs.map((b) => b.briefId);
2511
+ // eslint-disable-next-line no-console
2512
+ console.log(JSON.stringify(detail, null, 2));
2513
+ }
2514
+ else {
2515
+ /* eslint-disable no-console */
2516
+ console.log(`${spec.id} (${spec.kind}) ${spec.title}`);
2517
+ if (spec.kind === 'document') {
2518
+ const r = docRollup(spec.id);
2519
+ console.log(` rollup: ${r.requirements} reqs · ${r.implemented} implemented · ${r.verified} verified · ${degraded(r)} degraded`);
2520
+ if (briefs.length)
2521
+ console.log(` from briefs: ${briefs.map((b) => b.briefId).join(', ')}`);
2522
+ }
2523
+ for (const lk of links) {
2524
+ console.log(` [${lk.state}] → ${lk.targetFilePath}:${lk.targetQualifiedName}`);
2525
+ }
2526
+ /* eslint-enable no-console */
2527
+ }
2528
+ }
2529
+ return;
2530
+ }
2531
+ // ---- Funnel view (no id) ----
2532
+ const all = sq.getAllSpecs();
2533
+ const briefs = all.filter((s) => s.kind === 'brief');
2534
+ const documents = all.filter((s) => s.kind === 'document');
2535
+ const requirements = all.filter((s) => s.kind === 'requirement');
2536
+ const entries = briefs.map((b) => summarizeBriefFunnel(sq, b));
2537
+ const byState = (st) => entries.filter((e) => e.state === st);
2538
+ const links = sq.getAllLinks();
2539
+ const lc = (st) => links.filter((l) => l.state === st).length;
2540
+ if (all.length === 0) {
2541
+ if (options.json) {
2542
+ // eslint-disable-next-line no-console
2543
+ console.log(JSON.stringify({ summary: { ideas: 0, specified: 0, conflicts: 0, documents: 0, requirements: 0 }, specs: [], briefs: [] }, null, 2));
2544
+ }
2545
+ else {
2546
+ // eslint-disable-next-line no-console
2547
+ console.log('No specs or briefs found. Author one with /ss-spec new.');
2548
+ }
2549
+ return;
2550
+ }
2551
+ if (options.json) {
2552
+ // eslint-disable-next-line no-console
2553
+ console.log(JSON.stringify({
2554
+ summary: {
2555
+ ideas: byState('idea').length,
2556
+ specified: byState('specified').length,
2557
+ conflicts: byState('conflict').length,
2558
+ documents: documents.length,
2559
+ requirements: requirements.length,
2560
+ links: { implemented: lc('implemented'), verified: lc('verified'), drifted: lc('drifted'), broken: lc('broken'), orphaned: lc('orphaned') },
2561
+ },
2562
+ specs: documents.map((d) => ({ id: d.id, title: d.title, rollup: docRollup(d.id) })),
2563
+ briefs: entries,
2564
+ }, null, 2));
2565
+ return;
2566
+ }
2567
+ /* eslint-disable no-console */
2568
+ console.log('Spec lifecycle funnel');
2569
+ console.log(` ideas ${byState('idea').length}`);
2570
+ console.log(` specified ${byState('specified').length}`);
2571
+ if (byState('conflict').length)
2572
+ console.log(` conflicts ${byState('conflict').length} ⚠`);
2573
+ console.log(` documents ${documents.length}`);
2574
+ console.log(` requirements ${requirements.length} (implemented ${lc('implemented')} · verified ${lc('verified')} · degraded ${lc('drifted') + lc('broken') + lc('orphaned')})`);
2575
+ if (documents.length) {
2576
+ console.log('\nDocuments:');
2577
+ for (const d of documents) {
2578
+ const r = docRollup(d.id);
2579
+ console.log(` ${d.id} — ${d.title} [${r.requirements} reqs · ${r.implemented} impl · ${r.verified} ver${degraded(r) ? ` · ${degraded(r)} degraded` : ''}]`);
2580
+ }
2581
+ }
2582
+ const ideas = byState('idea');
2583
+ if (ideas.length) {
2584
+ console.log('\nIdeas (unlinked briefs):');
2585
+ for (const e of ideas) {
2586
+ const b = sq.getSpecById(e.briefId);
2587
+ console.log(` ${e.briefId} — ${b?.title ?? ''}`);
2588
+ }
2589
+ }
2590
+ const conflicts = byState('conflict');
2591
+ if (conflicts.length) {
2592
+ console.log('\nConflicts (mismatched brief↔spec pointers):');
2593
+ for (const e of conflicts)
2594
+ console.log(` ${e.briefId} ⚠`);
2595
+ }
2596
+ /* eslint-enable no-console */
2597
+ }
2598
+ finally {
2599
+ cg.close();
2600
+ }
2601
+ });
2602
+ program
2603
+ .command('workflow <action> [arg]')
2604
+ .description('Workflow engine: list | run <name> | resume <runId> | cancel <runId> | approve <runId> | reject <runId> | runs')
2605
+ .option('-i, --input <kv...>', 'workflow inputs as KEY=VALUE (repeatable)')
2606
+ .option('--path <projectRoot>', 'project root (default: cwd)')
2607
+ .option('--comment <text>', 'comment for approve/reject')
2608
+ .option('--reason <text>', 'reason for cancel/reject')
2609
+ .option('--json', 'emit JSON where applicable')
2610
+ .action(async (action, arg, options) => {
2611
+ const projectRoot = path.resolve(options.path ?? process.cwd());
2612
+ if (!(0, directory_1.isInitialized)(projectRoot)) {
2613
+ error(`SpecShip not initialized in ${projectRoot}. Run \`specship init -i\` first.`);
2614
+ process.exit(1);
2615
+ }
2616
+ const { SpecShip } = await loadSpecShip();
2617
+ const { discoverWorkflows, loadWorkflowByName } = await Promise.resolve().then(() => __importStar(require('../workflows/discovery')));
2618
+ const { WorkflowExecutor } = await Promise.resolve().then(() => __importStar(require('../workflows/executor')));
2619
+ const { WorktreeProvider } = await Promise.resolve().then(() => __importStar(require('../isolation/worktree')));
2620
+ const cg = await SpecShip.open(projectRoot);
2621
+ try {
2622
+ const specQueries = cg.getSpecQueries();
2623
+ const worktrees = new WorktreeProvider(specQueries);
2624
+ const executor = new WorkflowExecutor(specQueries, worktrees, projectRoot);
2625
+ switch (action) {
2626
+ case 'list': {
2627
+ const result = discoverWorkflows(projectRoot);
2628
+ if (options.json) {
2629
+ // eslint-disable-next-line no-console
2630
+ console.log(JSON.stringify({
2631
+ workflows: result.workflows.map((w) => ({
2632
+ name: w.workflow.name,
2633
+ scope: w.scope,
2634
+ sourcePath: w.sourcePath,
2635
+ description: w.workflow.description,
2636
+ nodes: w.workflow.nodes.length,
2637
+ requires: w.workflow.requires,
2638
+ })),
2639
+ errors: result.errors,
2640
+ }, null, 2));
2641
+ }
2642
+ else {
2643
+ for (const w of result.workflows) {
2644
+ // eslint-disable-next-line no-console
2645
+ console.log(` [${w.scope}] ${w.workflow.name} (${w.workflow.nodes.length} nodes) — ${w.workflow.description ?? ''}`);
2646
+ }
2647
+ if (result.errors.length > 0) {
2648
+ // eslint-disable-next-line no-console
2649
+ console.error(`\n${result.errors.length} workflow file(s) had errors:`);
2650
+ for (const e of result.errors) {
2651
+ // eslint-disable-next-line no-console
2652
+ console.error(` ${e.sourcePath}:`);
2653
+ for (const er of e.errors) {
2654
+ // eslint-disable-next-line no-console
2655
+ console.error(` ${er.path}: ${er.message}`);
2656
+ }
2657
+ }
2658
+ }
2659
+ }
2660
+ break;
2661
+ }
2662
+ case 'run': {
2663
+ if (!arg) {
2664
+ error('workflow run requires a workflow name');
2665
+ process.exit(1);
2666
+ }
2667
+ const loaded = loadWorkflowByName(projectRoot, arg);
2668
+ if (!loaded) {
2669
+ error(`Workflow "${arg}" not found. Use \`specship workflow list\` to see available workflows.`);
2670
+ process.exit(1);
2671
+ }
2672
+ const inputs = {};
2673
+ for (const kv of options.input ?? []) {
2674
+ const eqIdx = kv.indexOf('=');
2675
+ if (eqIdx <= 0) {
2676
+ error(`--input must be KEY=VALUE (got "${kv}")`);
2677
+ process.exit(1);
2678
+ }
2679
+ inputs[kv.slice(0, eqIdx)] = kv.slice(eqIdx + 1);
2680
+ }
2681
+ // Check required inputs.
2682
+ for (const inp of loaded.workflow.inputs ?? []) {
2683
+ if (inp.required && !(inp.name in inputs)) {
2684
+ error(`Required input missing: --input ${inp.name}=...`);
2685
+ process.exit(1);
2686
+ }
2687
+ }
2688
+ // Apply declared defaults for any optional input not passed. This
2689
+ // makes the schema's `default` field actually take effect and means
2690
+ // a `$INPUT.X` reference to a declared-but-omitted optional input
2691
+ // resolves to its default (or "") instead of throwing OutputRefError
2692
+ // mid-run. Required inputs are already enforced above.
2693
+ for (const inp of loaded.workflow.inputs ?? []) {
2694
+ if (!(inp.name in inputs)) {
2695
+ inputs[inp.name] = inp.default ?? '';
2696
+ }
2697
+ }
2698
+ const result = await executor.start(loaded.workflow, {
2699
+ projectRoot,
2700
+ inputs,
2701
+ variables: {
2702
+ ARTIFACTS_DIR: path.join((0, directory_1.getSpecShipDir)(projectRoot), 'artifacts'),
2703
+ CONTEXT: projectRoot,
2704
+ },
2705
+ });
2706
+ if (options.json) {
2707
+ // eslint-disable-next-line no-console
2708
+ console.log(JSON.stringify({
2709
+ runId: result.run.id,
2710
+ status: result.run.status,
2711
+ isolationEnvId: result.run.isolationEnvId,
2712
+ nodeStates: Object.fromEntries(result.nodeStates),
2713
+ }, null, 2));
2714
+ }
2715
+ else {
2716
+ // eslint-disable-next-line no-console
2717
+ console.log(`Run ${result.run.id} → ${result.run.status}`);
2718
+ if (result.run.isolationEnvId) {
2719
+ // eslint-disable-next-line no-console
2720
+ console.log(`Worktree: ${result.run.isolationEnvId}`);
2721
+ }
2722
+ if (result.run.errorMessage) {
2723
+ // eslint-disable-next-line no-console
2724
+ console.log(`Error: ${result.run.errorMessage}`);
2725
+ }
2726
+ }
2727
+ if (result.run.status === 'failed')
2728
+ process.exit(1);
2729
+ break;
2730
+ }
2731
+ case 'resume': {
2732
+ if (!arg) {
2733
+ error('workflow resume requires a runId');
2734
+ process.exit(1);
2735
+ }
2736
+ const run = specQueries.getWorkflowRunById(arg);
2737
+ if (!run) {
2738
+ error(`Run ${arg} not found`);
2739
+ process.exit(1);
2740
+ }
2741
+ const loaded = loadWorkflowByName(projectRoot, run.workflowName);
2742
+ if (!loaded) {
2743
+ error(`Workflow "${run.workflowName}" no longer exists`);
2744
+ process.exit(1);
2745
+ }
2746
+ const result = await executor.resume(loaded.workflow, arg, {
2747
+ projectRoot,
2748
+ inputs: run.inputs,
2749
+ variables: {
2750
+ ARTIFACTS_DIR: path.join((0, directory_1.getSpecShipDir)(projectRoot), 'artifacts'),
2751
+ CONTEXT: projectRoot,
2752
+ },
2753
+ });
2754
+ // eslint-disable-next-line no-console
2755
+ console.log(`Run ${result.run.id} → ${result.run.status}`);
2756
+ if (result.run.status === 'failed')
2757
+ process.exit(1);
2758
+ break;
2759
+ }
2760
+ case 'cancel': {
2761
+ if (!arg) {
2762
+ error('workflow cancel requires a runId');
2763
+ process.exit(1);
2764
+ }
2765
+ executor.cancel(arg, options.reason ?? 'cancelled via CLI');
2766
+ // eslint-disable-next-line no-console
2767
+ console.log(`Run ${arg} cancelled`);
2768
+ break;
2769
+ }
2770
+ case 'approve': {
2771
+ if (!arg) {
2772
+ error('workflow approve requires a runId');
2773
+ process.exit(1);
2774
+ }
2775
+ executor.approve(arg, options.comment);
2776
+ // eslint-disable-next-line no-console
2777
+ console.log(`Run ${arg} approved. Call \`specship workflow resume ${arg}\` to continue.`);
2778
+ break;
2779
+ }
2780
+ case 'reject': {
2781
+ if (!arg) {
2782
+ error('workflow reject requires a runId');
2783
+ process.exit(1);
2784
+ }
2785
+ executor.reject(arg, options.reason);
2786
+ // eslint-disable-next-line no-console
2787
+ console.log(`Run ${arg} rejected`);
2788
+ break;
2789
+ }
2790
+ case 'runs': {
2791
+ const runs = specQueries.getAllWorkflowRuns(50);
2792
+ if (options.json) {
2793
+ // eslint-disable-next-line no-console
2794
+ console.log(JSON.stringify(runs, null, 2));
2795
+ }
2796
+ else if (runs.length === 0) {
2797
+ // eslint-disable-next-line no-console
2798
+ console.log('No runs yet.');
2799
+ }
2800
+ else {
2801
+ for (const r of runs) {
2802
+ const dur = r.completedAt
2803
+ ? `${Math.round((r.completedAt - (r.startedAt ?? r.createdAt)) / 1000)}s`
2804
+ : '—';
2805
+ // eslint-disable-next-line no-console
2806
+ console.log(`${r.id.substring(0, 8)} [${r.status}] ${r.workflowName} (${dur})`);
2807
+ }
2808
+ }
2809
+ break;
2810
+ }
2811
+ default:
2812
+ error(`Unknown workflow action "${action}". Use: list | run | resume | cancel | approve | reject | runs`);
2813
+ process.exit(1);
2814
+ }
2815
+ }
2816
+ finally {
2817
+ cg.close();
2818
+ }
2819
+ });
2820
+ // Parse and run
2821
+ program.parse();
2822
+ } // end main()
2823
+ //# sourceMappingURL=specship.js.map