spectra-cli 4.2.0 → 4.3.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 (365) hide show
  1. package/README.md +12 -5
  2. package/dist/.spectra-build-meta.json +9 -0
  3. package/dist/adapters/language-adapter.d.ts +7 -12
  4. package/dist/adapters/language-adapter.d.ts.map +1 -1
  5. package/dist/adapters/python-adapter.d.ts.map +1 -1
  6. package/dist/adapters/python-adapter.js +11 -2
  7. package/dist/adapters/python-adapter.js.map +1 -1
  8. package/dist/batch/batch-orchestrator.d.ts +44 -1
  9. package/dist/batch/batch-orchestrator.d.ts.map +1 -1
  10. package/dist/batch/batch-orchestrator.js +312 -76
  11. package/dist/batch/batch-orchestrator.js.map +1 -1
  12. package/dist/batch/delta-regenerator.d.ts.map +1 -1
  13. package/dist/batch/delta-regenerator.js +21 -28
  14. package/dist/batch/delta-regenerator.js.map +1 -1
  15. package/dist/batch/module-grouper.d.ts +7 -0
  16. package/dist/batch/module-grouper.d.ts.map +1 -1
  17. package/dist/batch/module-grouper.js +2 -0
  18. package/dist/batch/module-grouper.js.map +1 -1
  19. package/dist/batch/regen-plan.d.ts +78 -0
  20. package/dist/batch/regen-plan.d.ts.map +1 -0
  21. package/dist/batch/regen-plan.js +93 -0
  22. package/dist/batch/regen-plan.js.map +1 -0
  23. package/dist/cli/commands/batch.d.ts.map +1 -1
  24. package/dist/cli/commands/batch.js +36 -8
  25. package/dist/cli/commands/batch.js.map +1 -1
  26. package/dist/cli/commands/scaffold-kb.d.ts +9 -0
  27. package/dist/cli/commands/scaffold-kb.d.ts.map +1 -0
  28. package/dist/cli/commands/scaffold-kb.js +179 -0
  29. package/dist/cli/commands/scaffold-kb.js.map +1 -0
  30. package/dist/cli/index.js +24 -5
  31. package/dist/cli/index.js.map +1 -1
  32. package/dist/cli/utils/error-handler.d.ts +0 -6
  33. package/dist/cli/utils/error-handler.d.ts.map +1 -1
  34. package/dist/cli/utils/error-handler.js +0 -13
  35. package/dist/cli/utils/error-handler.js.map +1 -1
  36. package/dist/cli/utils/parse-args.d.ts +43 -3
  37. package/dist/cli/utils/parse-args.d.ts.map +1 -1
  38. package/dist/cli/utils/parse-args.js +98 -12
  39. package/dist/cli/utils/parse-args.js.map +1 -1
  40. package/dist/cli/version-meta.d.ts +16 -0
  41. package/dist/cli/version-meta.d.ts.map +1 -0
  42. package/dist/cli/version-meta.js +28 -0
  43. package/dist/cli/version-meta.js.map +1 -0
  44. package/dist/core/ast-analyzer.d.ts.map +1 -1
  45. package/dist/core/ast-analyzer.js +7 -11
  46. package/dist/core/ast-analyzer.js.map +1 -1
  47. package/dist/core/import-resolver.d.ts +70 -67
  48. package/dist/core/import-resolver.d.ts.map +1 -1
  49. package/dist/core/import-resolver.js +341 -200
  50. package/dist/core/import-resolver.js.map +1 -1
  51. package/dist/core/single-spec-orchestrator.d.ts +21 -0
  52. package/dist/core/single-spec-orchestrator.d.ts.map +1 -1
  53. package/dist/core/single-spec-orchestrator.js +41 -14
  54. package/dist/core/single-spec-orchestrator.js.map +1 -1
  55. package/dist/core/skeleton-hash.d.ts +33 -0
  56. package/dist/core/skeleton-hash.d.ts.map +1 -0
  57. package/dist/core/skeleton-hash.js +76 -0
  58. package/dist/core/skeleton-hash.js.map +1 -0
  59. package/dist/core/tree-sitter-analyzer.d.ts +3 -2
  60. package/dist/core/tree-sitter-analyzer.d.ts.map +1 -1
  61. package/dist/core/tree-sitter-analyzer.js +5 -6
  62. package/dist/core/tree-sitter-analyzer.js.map +1 -1
  63. package/dist/core/tree-sitter-fallback.d.ts +7 -2
  64. package/dist/core/tree-sitter-fallback.d.ts.map +1 -1
  65. package/dist/core/tree-sitter-fallback.js +18 -15
  66. package/dist/core/tree-sitter-fallback.js.map +1 -1
  67. package/dist/extraction/image-extractor.d.ts.map +1 -1
  68. package/dist/extraction/image-extractor.js +3 -1
  69. package/dist/extraction/image-extractor.js.map +1 -1
  70. package/dist/extraction/markdown-extractor.d.ts.map +1 -1
  71. package/dist/extraction/markdown-extractor.js +3 -1
  72. package/dist/extraction/markdown-extractor.js.map +1 -1
  73. package/dist/generator/frontmatter.d.ts +5 -0
  74. package/dist/generator/frontmatter.d.ts.map +1 -1
  75. package/dist/generator/frontmatter.js +4 -0
  76. package/dist/generator/frontmatter.js.map +1 -1
  77. package/dist/kb-mcp/index.d.ts +14 -0
  78. package/dist/kb-mcp/index.d.ts.map +1 -0
  79. package/dist/kb-mcp/index.js +24 -0
  80. package/dist/kb-mcp/index.js.map +1 -0
  81. package/dist/kb-mcp/lib/kb-error.d.ts +21 -0
  82. package/dist/kb-mcp/lib/kb-error.d.ts.map +1 -0
  83. package/dist/kb-mcp/lib/kb-error.js +28 -0
  84. package/dist/kb-mcp/lib/kb-error.js.map +1 -0
  85. package/dist/kb-mcp/lib/kb-locator.d.ts +58 -0
  86. package/dist/kb-mcp/lib/kb-locator.d.ts.map +1 -0
  87. package/dist/kb-mcp/lib/kb-locator.js +85 -0
  88. package/dist/kb-mcp/lib/kb-locator.js.map +1 -0
  89. package/dist/kb-mcp/lib/result-merger.d.ts +35 -0
  90. package/dist/kb-mcp/lib/result-merger.d.ts.map +1 -0
  91. package/dist/kb-mcp/lib/result-merger.js +64 -0
  92. package/dist/kb-mcp/lib/result-merger.js.map +1 -0
  93. package/dist/kb-mcp/server.d.ts +12 -0
  94. package/dist/kb-mcp/server.d.ts.map +1 -0
  95. package/dist/kb-mcp/server.js +29 -0
  96. package/dist/kb-mcp/server.js.map +1 -0
  97. package/dist/kb-mcp/tools/kb-api-lookup.d.ts +28 -0
  98. package/dist/kb-mcp/tools/kb-api-lookup.d.ts.map +1 -0
  99. package/dist/kb-mcp/tools/kb-api-lookup.js +237 -0
  100. package/dist/kb-mcp/tools/kb-api-lookup.js.map +1 -0
  101. package/dist/kb-mcp/tools/kb-doc-lookup.d.ts +20 -0
  102. package/dist/kb-mcp/tools/kb-doc-lookup.d.ts.map +1 -0
  103. package/dist/kb-mcp/tools/kb-doc-lookup.js +92 -0
  104. package/dist/kb-mcp/tools/kb-doc-lookup.js.map +1 -0
  105. package/dist/kb-mcp/tools/kb-search.d.ts +23 -0
  106. package/dist/kb-mcp/tools/kb-search.d.ts.map +1 -0
  107. package/dist/kb-mcp/tools/kb-search.js +126 -0
  108. package/dist/kb-mcp/tools/kb-search.js.map +1 -0
  109. package/dist/knowledge-graph/import-resolver.d.ts +11 -86
  110. package/dist/knowledge-graph/import-resolver.d.ts.map +1 -1
  111. package/dist/knowledge-graph/import-resolver.js +10 -333
  112. package/dist/knowledge-graph/import-resolver.js.map +1 -1
  113. package/dist/knowledge-graph/incremental.d.ts +25 -4
  114. package/dist/knowledge-graph/incremental.d.ts.map +1 -1
  115. package/dist/knowledge-graph/incremental.js +37 -15
  116. package/dist/knowledge-graph/incremental.js.map +1 -1
  117. package/dist/knowledge-graph/index.d.ts +11 -0
  118. package/dist/knowledge-graph/index.d.ts.map +1 -1
  119. package/dist/knowledge-graph/index.js +43 -1
  120. package/dist/knowledge-graph/index.js.map +1 -1
  121. package/dist/knowledge-graph/module-derivation.d.ts +14 -0
  122. package/dist/knowledge-graph/module-derivation.d.ts.map +1 -1
  123. package/dist/knowledge-graph/module-derivation.js +44 -44
  124. package/dist/knowledge-graph/module-derivation.js.map +1 -1
  125. package/dist/knowledge-graph/persistence.d.ts +56 -16
  126. package/dist/knowledge-graph/persistence.d.ts.map +1 -1
  127. package/dist/knowledge-graph/persistence.js +86 -32
  128. package/dist/knowledge-graph/persistence.js.map +1 -1
  129. package/dist/knowledge-graph/query-helpers.d.ts +39 -12
  130. package/dist/knowledge-graph/query-helpers.d.ts.map +1 -1
  131. package/dist/knowledge-graph/query-helpers.js +199 -34
  132. package/dist/knowledge-graph/query-helpers.js.map +1 -1
  133. package/dist/knowledge-graph/relativize.d.ts +51 -0
  134. package/dist/knowledge-graph/relativize.d.ts.map +1 -0
  135. package/dist/knowledge-graph/relativize.js +104 -0
  136. package/dist/knowledge-graph/relativize.js.map +1 -0
  137. package/dist/mcp/agent-context-tools.d.ts +3 -54
  138. package/dist/mcp/agent-context-tools.d.ts.map +1 -1
  139. package/dist/mcp/agent-context-tools.js +141 -189
  140. package/dist/mcp/agent-context-tools.js.map +1 -1
  141. package/dist/mcp/file-nav-tools.d.ts +42 -0
  142. package/dist/mcp/file-nav-tools.d.ts.map +1 -0
  143. package/dist/mcp/file-nav-tools.js +318 -0
  144. package/dist/mcp/file-nav-tools.js.map +1 -0
  145. package/dist/mcp/graph-tools.d.ts +2 -0
  146. package/dist/mcp/graph-tools.d.ts.map +1 -1
  147. package/dist/mcp/graph-tools.js +134 -123
  148. package/dist/mcp/graph-tools.js.map +1 -1
  149. package/dist/mcp/lib/file-nav-helpers.d.ts +128 -0
  150. package/dist/mcp/lib/file-nav-helpers.d.ts.map +1 -0
  151. package/dist/mcp/lib/file-nav-helpers.js +305 -0
  152. package/dist/mcp/lib/file-nav-helpers.js.map +1 -0
  153. package/dist/mcp/lib/telemetry.d.ts +69 -0
  154. package/dist/mcp/lib/telemetry.d.ts.map +1 -0
  155. package/dist/mcp/lib/telemetry.js +113 -0
  156. package/dist/mcp/lib/telemetry.js.map +1 -0
  157. package/dist/mcp/lib/tool-response.d.ts +46 -0
  158. package/dist/mcp/lib/tool-response.d.ts.map +1 -0
  159. package/dist/mcp/lib/tool-response.js +83 -0
  160. package/dist/mcp/lib/tool-response.js.map +1 -0
  161. package/dist/mcp/server.d.ts +13 -2
  162. package/dist/mcp/server.d.ts.map +1 -1
  163. package/dist/mcp/server.js +209 -150
  164. package/dist/mcp/server.js.map +1 -1
  165. package/dist/models/code-skeleton.d.ts +4 -2
  166. package/dist/models/code-skeleton.d.ts.map +1 -1
  167. package/dist/models/code-skeleton.js +2 -1
  168. package/dist/models/code-skeleton.js.map +1 -1
  169. package/dist/models/module-spec.d.ts +18 -0
  170. package/dist/models/module-spec.d.ts.map +1 -1
  171. package/dist/models/module-spec.js +6 -0
  172. package/dist/models/module-spec.js.map +1 -1
  173. package/dist/panoramic/builders/doc-graph-builder.d.ts +13 -0
  174. package/dist/panoramic/builders/doc-graph-builder.d.ts.map +1 -1
  175. package/dist/panoramic/builders/doc-graph-builder.js +20 -0
  176. package/dist/panoramic/builders/doc-graph-builder.js.map +1 -1
  177. package/dist/panoramic/graph/graph-builder.d.ts +72 -2
  178. package/dist/panoramic/graph/graph-builder.d.ts.map +1 -1
  179. package/dist/panoramic/graph/graph-builder.js +257 -51
  180. package/dist/panoramic/graph/graph-builder.js.map +1 -1
  181. package/dist/panoramic/graph/graph-query.d.ts +16 -0
  182. package/dist/panoramic/graph/graph-query.d.ts.map +1 -1
  183. package/dist/panoramic/graph/graph-query.js +49 -1
  184. package/dist/panoramic/graph/graph-query.js.map +1 -1
  185. package/dist/panoramic/graph/index.d.ts +2 -1
  186. package/dist/panoramic/graph/index.d.ts.map +1 -1
  187. package/dist/panoramic/graph/index.js +1 -1
  188. package/dist/panoramic/graph/index.js.map +1 -1
  189. package/dist/panoramic/pipelines/adr-evidence-verifier.d.ts.map +1 -1
  190. package/dist/panoramic/pipelines/adr-evidence-verifier.js +1 -29
  191. package/dist/panoramic/pipelines/adr-evidence-verifier.js.map +1 -1
  192. package/dist/panoramic/query.d.ts +1 -0
  193. package/dist/panoramic/query.d.ts.map +1 -1
  194. package/dist/panoramic/query.js +5 -0
  195. package/dist/panoramic/query.js.map +1 -1
  196. package/dist/scaffold-kb/api-entities-serializer.d.ts +12 -0
  197. package/dist/scaffold-kb/api-entities-serializer.d.ts.map +1 -0
  198. package/dist/scaffold-kb/api-entities-serializer.js +144 -0
  199. package/dist/scaffold-kb/api-entities-serializer.js.map +1 -0
  200. package/dist/scaffold-kb/arbitration.d.ts +44 -0
  201. package/dist/scaffold-kb/arbitration.d.ts.map +1 -0
  202. package/dist/scaffold-kb/arbitration.js +144 -0
  203. package/dist/scaffold-kb/arbitration.js.map +1 -0
  204. package/dist/scaffold-kb/chunk-splitter.d.ts +20 -0
  205. package/dist/scaffold-kb/chunk-splitter.d.ts.map +1 -0
  206. package/dist/scaffold-kb/chunk-splitter.js +309 -0
  207. package/dist/scaffold-kb/chunk-splitter.js.map +1 -0
  208. package/dist/scaffold-kb/doc-graph-builder.d.ts +26 -0
  209. package/dist/scaffold-kb/doc-graph-builder.d.ts.map +1 -0
  210. package/dist/scaffold-kb/doc-graph-builder.js +72 -0
  211. package/dist/scaffold-kb/doc-graph-builder.js.map +1 -0
  212. package/dist/scaffold-kb/entity-extractor.d.ts +40 -0
  213. package/dist/scaffold-kb/entity-extractor.d.ts.map +1 -0
  214. package/dist/scaffold-kb/entity-extractor.js +0 -0
  215. package/dist/scaffold-kb/entity-extractor.js.map +1 -0
  216. package/dist/scaffold-kb/entity-heuristic.d.ts +14 -0
  217. package/dist/scaffold-kb/entity-heuristic.d.ts.map +1 -0
  218. package/dist/scaffold-kb/entity-heuristic.js +107 -0
  219. package/dist/scaffold-kb/entity-heuristic.js.map +1 -0
  220. package/dist/scaffold-kb/entity-matcher.d.ts +20 -0
  221. package/dist/scaffold-kb/entity-matcher.d.ts.map +1 -0
  222. package/dist/scaffold-kb/entity-matcher.js +53 -0
  223. package/dist/scaffold-kb/entity-matcher.js.map +1 -0
  224. package/dist/scaffold-kb/entity-util.d.ts +11 -0
  225. package/dist/scaffold-kb/entity-util.d.ts.map +1 -0
  226. package/dist/scaffold-kb/entity-util.js +23 -0
  227. package/dist/scaffold-kb/entity-util.js.map +1 -0
  228. package/dist/scaffold-kb/evidence-envelope.d.ts +17 -0
  229. package/dist/scaffold-kb/evidence-envelope.d.ts.map +1 -0
  230. package/dist/scaffold-kb/evidence-envelope.js +32 -0
  231. package/dist/scaffold-kb/evidence-envelope.js.map +1 -0
  232. package/dist/scaffold-kb/index.d.ts +20 -0
  233. package/dist/scaffold-kb/index.d.ts.map +1 -0
  234. package/dist/scaffold-kb/index.js +102 -0
  235. package/dist/scaffold-kb/index.js.map +1 -0
  236. package/dist/scaffold-kb/ingest/ingest-core.d.ts +47 -0
  237. package/dist/scaffold-kb/ingest/ingest-core.d.ts.map +1 -0
  238. package/dist/scaffold-kb/ingest/ingest-core.js +231 -0
  239. package/dist/scaffold-kb/ingest/ingest-core.js.map +1 -0
  240. package/dist/scaffold-kb/ingest/office-parser.d.ts +33 -0
  241. package/dist/scaffold-kb/ingest/office-parser.d.ts.map +1 -0
  242. package/dist/scaffold-kb/ingest/office-parser.js +187 -0
  243. package/dist/scaffold-kb/ingest/office-parser.js.map +1 -0
  244. package/dist/scaffold-kb/ingest/url-fetcher.d.ts +43 -0
  245. package/dist/scaffold-kb/ingest/url-fetcher.d.ts.map +1 -0
  246. package/dist/scaffold-kb/ingest/url-fetcher.js +245 -0
  247. package/dist/scaffold-kb/ingest/url-fetcher.js.map +1 -0
  248. package/dist/scaffold-kb/ingester.d.ts +38 -0
  249. package/dist/scaffold-kb/ingester.d.ts.map +1 -0
  250. package/dist/scaffold-kb/ingester.js +261 -0
  251. package/dist/scaffold-kb/ingester.js.map +1 -0
  252. package/dist/scaffold-kb/injection-format.d.ts +25 -0
  253. package/dist/scaffold-kb/injection-format.d.ts.map +1 -0
  254. package/dist/scaffold-kb/injection-format.js +55 -0
  255. package/dist/scaffold-kb/injection-format.js.map +1 -0
  256. package/dist/scaffold-kb/kb-writer.d.ts +13 -0
  257. package/dist/scaffold-kb/kb-writer.d.ts.map +1 -0
  258. package/dist/scaffold-kb/kb-writer.js +69 -0
  259. package/dist/scaffold-kb/kb-writer.js.map +1 -0
  260. package/dist/scaffold-kb/keyword-extract.d.ts +16 -0
  261. package/dist/scaffold-kb/keyword-extract.d.ts.map +1 -0
  262. package/dist/scaffold-kb/keyword-extract.js +49 -0
  263. package/dist/scaffold-kb/keyword-extract.js.map +1 -0
  264. package/dist/scaffold-kb/query-sanitizer.d.ts +28 -0
  265. package/dist/scaffold-kb/query-sanitizer.d.ts.map +1 -0
  266. package/dist/scaffold-kb/query-sanitizer.js +38 -0
  267. package/dist/scaffold-kb/query-sanitizer.js.map +1 -0
  268. package/dist/scaffold-kb/recall-eval.d.ts +48 -0
  269. package/dist/scaffold-kb/recall-eval.d.ts.map +1 -0
  270. package/dist/scaffold-kb/recall-eval.js +55 -0
  271. package/dist/scaffold-kb/recall-eval.js.map +1 -0
  272. package/dist/scaffold-kb/schema-compat.d.ts +22 -0
  273. package/dist/scaffold-kb/schema-compat.d.ts.map +1 -0
  274. package/dist/scaffold-kb/schema-compat.js +32 -0
  275. package/dist/scaffold-kb/schema-compat.js.map +1 -0
  276. package/dist/scaffold-kb/search-core.d.ts +42 -0
  277. package/dist/scaffold-kb/search-core.d.ts.map +1 -0
  278. package/dist/scaffold-kb/search-core.js +89 -0
  279. package/dist/scaffold-kb/search-core.js.map +1 -0
  280. package/dist/scaffold-kb/sqlite-engine.d.ts +58 -0
  281. package/dist/scaffold-kb/sqlite-engine.d.ts.map +1 -0
  282. package/dist/scaffold-kb/sqlite-engine.js +61 -0
  283. package/dist/scaffold-kb/sqlite-engine.js.map +1 -0
  284. package/dist/scaffold-kb/sqlite-writer.d.ts +18 -0
  285. package/dist/scaffold-kb/sqlite-writer.d.ts.map +1 -0
  286. package/dist/scaffold-kb/sqlite-writer.js +76 -0
  287. package/dist/scaffold-kb/sqlite-writer.js.map +1 -0
  288. package/dist/scaffold-kb/tokenizer.d.ts +26 -0
  289. package/dist/scaffold-kb/tokenizer.d.ts.map +1 -0
  290. package/dist/scaffold-kb/tokenizer.js +97 -0
  291. package/dist/scaffold-kb/tokenizer.js.map +1 -0
  292. package/dist/scaffold-kb/types.d.ts +157 -0
  293. package/dist/scaffold-kb/types.d.ts.map +1 -0
  294. package/dist/scaffold-kb/types.js +5 -0
  295. package/dist/scaffold-kb/types.js.map +1 -0
  296. package/dist/utils/file-scanner.d.ts +15 -0
  297. package/dist/utils/file-scanner.d.ts.map +1 -1
  298. package/dist/utils/file-scanner.js +17 -0
  299. package/dist/utils/file-scanner.js.map +1 -1
  300. package/dist/utils/string-distance.d.ts +20 -0
  301. package/dist/utils/string-distance.d.ts.map +1 -0
  302. package/dist/utils/string-distance.js +38 -0
  303. package/dist/utils/string-distance.js.map +1 -0
  304. package/package.json +11 -2
  305. package/plugins/demo-kb-en/.claude-plugin/plugin.json +11 -0
  306. package/plugins/demo-kb-en/.mcp.json +8 -0
  307. package/plugins/demo-kb-en/FIXTURE.json +23 -0
  308. package/plugins/demo-kb-en/FIXTURE.md +53 -0
  309. package/plugins/demo-kb-en/ingest-samples/meeting-notes.md +13 -0
  310. package/plugins/demo-kb-en/kb/api-entities.json +3445 -0
  311. package/plugins/demo-kb-en/kb/chunks.sqlite +0 -0
  312. package/plugins/demo-kb-en/kb/doc-graph.json +113 -0
  313. package/plugins/demo-kb-zh/.claude-plugin/plugin.json +11 -0
  314. package/plugins/demo-kb-zh/.mcp.json +8 -0
  315. package/plugins/demo-kb-zh/FIXTURE.json +23 -0
  316. package/plugins/demo-kb-zh/FIXTURE.md +63 -0
  317. package/plugins/demo-kb-zh/ingest-samples/meeting-notes.md +13 -0
  318. package/plugins/demo-kb-zh/kb/api-entities.json +990 -0
  319. package/plugins/demo-kb-zh/kb/chunks.sqlite +0 -0
  320. package/plugins/demo-kb-zh/kb/doc-graph.json +113 -0
  321. package/plugins/spec-driver/.claude-plugin/plugin.json +1 -1
  322. package/plugins/spec-driver/README.md +33 -1
  323. package/plugins/spec-driver/agents/verify.md +88 -0
  324. package/plugins/spec-driver/config/orchestration.yaml +39 -31
  325. package/plugins/spec-driver/contracts/orchestration-overrides-contract.yaml +27 -0
  326. package/plugins/spec-driver/contracts/orchestration-schema.mjs +272 -227
  327. package/plugins/spec-driver/contracts/wrapper-source-of-truth.yaml +3 -0
  328. package/plugins/spec-driver/hooks/hooks.json +9 -0
  329. package/plugins/spec-driver/hooks/stop-fix-compliance-check.sh +36 -0
  330. package/plugins/spec-driver/lib/delegation-contract.mjs +84 -0
  331. package/plugins/spec-driver/lib/orchestration-resolver.mjs +49 -0
  332. package/plugins/spec-driver/lib/orchestrator.mjs +29 -1
  333. package/plugins/spec-driver/scripts/codex-skills.sh +28 -28
  334. package/plugins/spec-driver/scripts/dev/spike-fix-compliance-e2e.mjs +130 -0
  335. package/plugins/spec-driver/scripts/fix-compliance-judge.mjs +415 -0
  336. package/plugins/spec-driver/scripts/generate-adoption-insights.mjs +48 -1
  337. package/plugins/spec-driver/scripts/goal-loop-cli.mjs +307 -0
  338. package/plugins/spec-driver/scripts/init-project.sh +56 -0
  339. package/plugins/spec-driver/scripts/kb-prequery.mjs +125 -0
  340. package/plugins/spec-driver/scripts/lib/config-schema.mjs +200 -94
  341. package/plugins/spec-driver/scripts/lib/ensure-gitignore.sh +277 -0
  342. package/plugins/spec-driver/scripts/lib/extract-wrapper-body.mjs +136 -0
  343. package/plugins/spec-driver/scripts/lib/fix-compliance-core.mjs +434 -0
  344. package/plugins/spec-driver/scripts/lib/fix-compliance-io.mjs +335 -0
  345. package/plugins/spec-driver/scripts/lib/goal-loop-core.mjs +783 -0
  346. package/plugins/spec-driver/scripts/lib/init-project-output.sh +28 -0
  347. package/plugins/spec-driver/scripts/lib/load-zod.mjs +62 -0
  348. package/plugins/spec-driver/scripts/lib/project-profile-resolver.mjs +141 -49
  349. package/plugins/spec-driver/scripts/lib/project-profile-schema.mjs +88 -54
  350. package/plugins/spec-driver/scripts/postinstall.sh +13 -1
  351. package/plugins/spec-driver/scripts/record-workflow-run.mjs +75 -0
  352. package/plugins/spec-driver/scripts/sync-delegation-contract.mjs +187 -0
  353. package/plugins/spec-driver/scripts/validate-orchestrator-models.mjs +156 -0
  354. package/plugins/spec-driver/scripts/validate-wrapper-sources.mjs +46 -1
  355. package/plugins/spec-driver/skills/spec-driver-feature/SKILL.md +292 -0
  356. package/plugins/spec-driver/skills/spec-driver-fix/SKILL.md +87 -8
  357. package/plugins/spec-driver/skills/spec-driver-implement/SKILL.md +8 -0
  358. package/plugins/spec-driver/skills/spec-driver-resume/SKILL.md +12 -1
  359. package/plugins/spec-driver/skills/spec-driver-story/SKILL.md +20 -0
  360. package/plugins/spec-driver/templates/delegation-contract.md +27 -0
  361. package/plugins/spec-driver/templates/goal-loop-override-template.yaml +284 -0
  362. package/plugins/spec-driver/templates/specify-base/project-context-template.yaml +10 -0
  363. package/plugins/spectra/.claude-plugin/plugin.json +1 -1
  364. package/plugins/spectra/README.md +1 -1
  365. package/templates/module-spec.hbs +1 -0
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * goal-loop-cli.mjs
5
+ * Feature 201 — goal_loop core 的薄 CLI 包装(仿 orchestrator-cli.mjs)
6
+ *
7
+ * 设计契约(plan §1.1 / tasks T018,修正 Codex C-02):
8
+ * 所有"需要复杂结构输入"的子命令一律接受**单个 JSON payload**(文件路径),
9
+ * 输出 JSON 到 stdout;简单标量子命令用位置参数。SKILL.md 散文必须按本契约调用。
10
+ *
11
+ * 用法:
12
+ * node goal-loop-cli.mjs parse-report <reportJsonFile>
13
+ * node goal-loop-cli.mjs classify-command <cmdJsonFile>
14
+ * node goal-loop-cli.mjs decide-stop <payloadJsonFile> # payload={report,round,config,prevReports,rollbackResult}
15
+ * node goal-loop-cli.mjs plan-snapshot <isClean:true|false>
16
+ * node goal-loop-cli.mjs plan-rollback <snapshotJsonFile> # snapshot={clean,ref}
17
+ * node goal-loop-cli.mjs select-verify-mode <round> <max> <aboutToExit:true|false>
18
+ * node goal-loop-cli.mjs decide-dispatch <phaseId> <agentMode>
19
+ * node goal-loop-cli.mjs interpret-impact <mcpResultJsonFile>
20
+ * node goal-loop-cli.mjs format-iteration-log-entry <entryJsonFile> # 输出 markdown 块到 stdout
21
+ * node goal-loop-cli.mjs assess-preserved-config-safety <porcelainFile|-(stdin)>
22
+ * # 输入 = `git status --porcelain --untracked-files=all -- <PRESERVED_CONFIG_PATHSPECS>` 原始 stdout
23
+ * # (MUST 带 --untracked-files=all:避免 untracked 目录折叠成 `?? .specify/` 漏检 preserved 文件)
24
+ * # CLI 内部 parsePreservedConfigStates(porcelain, PRESERVED_CONFIG_PATHSPECS) → assessPreservedConfigSafety
25
+ * node goal-loop-cli.mjs is-clean-excluding-preserved <porcelainFile|-(stdin)>
26
+ * # 输入 = **全仓** `git status --porcelain --untracked-files=all` 原始 stdout(不带 -- pathspec)
27
+ * # (MUST 带 --untracked-files=all:默认会把 untracked 目录折叠成 `?? .specify/`,折叠形式喂入
28
+ * # 会使 isClean 误判 false → 空 stash 抓无关旧 stash,CRITICAL-7 漏网根因)
29
+ * # 输出 {isClean:boolean}:排除 PRESERVED_CONFIG_PATHSPECS 后是否干净(F203 CRITICAL-7)
30
+ * node goal-loop-cli.mjs acquire-lock <lockPath>
31
+ * node goal-loop-cli.mjs release-lock <lockPath>
32
+ */
33
+
34
+ import fs from 'node:fs';
35
+ import { fileURLToPath } from 'node:url';
36
+ import {
37
+ classifyCommand,
38
+ decideStop,
39
+ decideDispatch,
40
+ selectVerifyMode,
41
+ planSnapshotCommands,
42
+ planRollbackCommands,
43
+ parseReport,
44
+ interpretImpactResult,
45
+ formatIterationLogEntry,
46
+ assessPreservedConfigSafety,
47
+ parsePreservedConfigStates,
48
+ isCleanExcludingPreserved,
49
+ PRESERVED_CONFIG_PATHSPECS,
50
+ } from './lib/goal-loop-core.mjs';
51
+
52
+ // ──────────────────────────────────────────────────────────────────────────
53
+ // I/O 边界:单实例文件锁(FR-018)—— 非纯函数,故放 CLI 不放纯 core
54
+ // ──────────────────────────────────────────────────────────────────────────
55
+
56
+ /**
57
+ * 写锁 payload 到已打开的 fd 并关闭
58
+ * @param {number} fd
59
+ */
60
+ function writeLockPayload(fd) {
61
+ const payload = JSON.stringify({ pid: process.pid, start_time: new Date().toISOString() });
62
+ try {
63
+ fs.writeSync(fd, payload);
64
+ } finally {
65
+ fs.closeSync(fd);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * 判定现有锁是否为 stale(仅当持锁进程已不存在 或 锁内容损坏)(Codex W3 / Phase B 复审修正)
71
+ *
72
+ * 关键不变量(FR-018 单实例保证):**持锁 PID 存活时永不视为 stale**,无论锁多老。
73
+ * 早期版本曾用 TTL(30min)兜底接管"超龄但存活"的锁,但真实 goal_loop 跑满
74
+ * max_iterations × max_verify_seconds 很容易超 30min,会导致新实例强抢活锁、破坏单实例保证。
75
+ * 因此移除 TTL 接管路径:接管只发生在"持有者证明已死"或"锁文件损坏无法证明存活"两种情况。
76
+ *
77
+ * 已知罕见残留:PID 复用——死 goal_loop 的 PID 被无关进程复用后,process.kill(pid,0) 误判存活,
78
+ * 锁将无法自动接管,需人工清理 .lock 文件。我们接受这个罕见 case,不引入 TTL 接管活锁的
79
+ * 更大风险(误杀正在跑的长任务)来换取它。
80
+ * @param {string} lockPath
81
+ * @returns {{ stale: boolean, holderPid: number|null, reason: string }}
82
+ */
83
+ function inspectLock(lockPath) {
84
+ let payload;
85
+ try {
86
+ payload = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
87
+ } catch {
88
+ // 锁内容损坏/不可解析 → 无法证明持有者存活,视为 stale 可接管
89
+ return { stale: true, holderPid: null, reason: 'lock_payload_unreadable' };
90
+ }
91
+ const holderPid = typeof payload.pid === 'number' ? payload.pid : null;
92
+
93
+ // 进程存活探测:process.kill(pid, 0) 不发信号,仅校验进程存在;ESRCH = 进程不存在
94
+ if (holderPid !== null) {
95
+ try {
96
+ process.kill(holderPid, 0);
97
+ } catch (err) {
98
+ if (err.code === 'ESRCH') {
99
+ return { stale: true, holderPid, reason: 'holder_process_gone' };
100
+ }
101
+ // EPERM = 进程存在但无权限发信号 → 持有者存活(保守),非 stale
102
+ }
103
+ } else {
104
+ // payload 缺合法 pid 字段 → 无法证明存活,视为 stale 可接管
105
+ return { stale: true, holderPid: null, reason: 'lock_payload_unreadable' };
106
+ }
107
+
108
+ // 走到这里:holderPid 存活(kill 未抛 ESRCH)→ 永不接管,无论锁龄
109
+ return { stale: false, holderPid, reason: 'holder_alive' };
110
+ }
111
+
112
+ /**
113
+ * 单实例文件锁:以 O_EXCL 原子创建 .lock(FR-018)
114
+ *
115
+ * stale 恢复(Codex W3 / Phase B 复审修正):遇 EEXIST 时校验现有锁——仅当持锁进程不存在(ESRCH)
116
+ * 或锁文件损坏无法证明持有者存活时,原子清理 stale 锁后重试一次获取;否则(持有者存活)返回
117
+ * lock_exists(含 holder pid)。不再用 TTL 接管"超龄但存活"的活锁(见 inspectLock 注释)。
118
+ * @param {string} lockPath
119
+ * @returns {{ acquired: true }|{ acquired: false, reason: string, holderPid?: number|null }}
120
+ */
121
+ export function acquireLock(lockPath) {
122
+ let fd;
123
+ try {
124
+ // 'wx' = O_CREAT | O_EXCL | O_WRONLY:文件已存在则抛 EEXIST(原子,防竞态)
125
+ fd = fs.openSync(lockPath, 'wx');
126
+ } catch (err) {
127
+ if (err.code === 'EEXIST') {
128
+ const info = inspectLock(lockPath);
129
+ if (!info.stale) {
130
+ return { acquired: false, reason: 'lock_exists', holderPid: info.holderPid };
131
+ }
132
+ // stale 锁:原子清理后重试一次获取。
133
+ // 用第二个 'wx' 而非先 unlink 再 open,避免清理与重建之间的竞态(仅重试一次防活锁)。
134
+ try {
135
+ fs.unlinkSync(lockPath);
136
+ } catch (unlinkErr) {
137
+ if (unlinkErr.code !== 'ENOENT') throw unlinkErr;
138
+ // ENOENT:他人已抢先清理,继续尝试创建
139
+ }
140
+ try {
141
+ fd = fs.openSync(lockPath, 'wx');
142
+ } catch (retryErr) {
143
+ if (retryErr.code === 'EEXIST') {
144
+ // 清理后被其他进程抢占 → 让对方持有,返回 lock_exists
145
+ const after = inspectLock(lockPath);
146
+ return { acquired: false, reason: 'lock_exists', holderPid: after.holderPid };
147
+ }
148
+ throw retryErr;
149
+ }
150
+ writeLockPayload(fd);
151
+ return { acquired: true };
152
+ }
153
+ throw err;
154
+ }
155
+ writeLockPayload(fd);
156
+ return { acquired: true };
157
+ }
158
+
159
+ /**
160
+ * 释放单实例文件锁(FR-018)
161
+ * @param {string} lockPath
162
+ * @returns {{ released: boolean }}
163
+ */
164
+ export function releaseLock(lockPath) {
165
+ try {
166
+ fs.unlinkSync(lockPath);
167
+ } catch (err) {
168
+ if (err.code === 'ENOENT') {
169
+ // 锁不存在视为已释放(幂等)
170
+ return { released: true };
171
+ }
172
+ throw err;
173
+ }
174
+ return { released: true };
175
+ }
176
+
177
+ // ──────────────────────────────────────────────────────────────────────────
178
+ // CLI dispatch
179
+ // ──────────────────────────────────────────────────────────────────────────
180
+
181
+ function output(data) {
182
+ console.log(JSON.stringify(data, null, 2));
183
+ }
184
+
185
+ function fail(message) {
186
+ console.error(JSON.stringify({ success: false, error: message }, null, 2));
187
+ process.exit(1);
188
+ }
189
+
190
+ function readJsonFile(filePath) {
191
+ if (!filePath) {
192
+ fail('缺少 JSON payload 文件路径参数');
193
+ }
194
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
195
+ }
196
+
197
+ /** 同步读取 stdin(fd 0)全部内容为 UTF-8 文本 */
198
+ function readStdin() {
199
+ return fs.readFileSync(0, 'utf-8');
200
+ }
201
+
202
+ function parseBool(value, name) {
203
+ if (value === 'true') return true;
204
+ if (value === 'false') return false;
205
+ fail(`参数 ${name} 必须为 true|false,实际为 "${value}"`);
206
+ }
207
+
208
+ function main(argv) {
209
+ const [subcommand, ...args] = argv;
210
+
211
+ switch (subcommand) {
212
+ case 'parse-report': {
213
+ // parseReport 接受原始文本(含可能非法 JSON),故直接读文本不预解析
214
+ const filePath = args[0];
215
+ if (!filePath) fail('parse-report 需要 <reportJsonFile>');
216
+ const text = fs.readFileSync(filePath, 'utf-8');
217
+ output(parseReport(text));
218
+ break;
219
+ }
220
+ case 'classify-command': {
221
+ output({ status: classifyCommand(readJsonFile(args[0])) });
222
+ break;
223
+ }
224
+ case 'decide-stop': {
225
+ const payload = readJsonFile(args[0]);
226
+ output(
227
+ decideStop({
228
+ report: payload.report,
229
+ round: payload.round,
230
+ config: payload.config,
231
+ prevReports: payload.prevReports ?? [],
232
+ rollbackResult: payload.rollbackResult ?? null,
233
+ }),
234
+ );
235
+ break;
236
+ }
237
+ case 'plan-snapshot': {
238
+ output({ commands: planSnapshotCommands(parseBool(args[0], 'isClean')) });
239
+ break;
240
+ }
241
+ case 'plan-rollback': {
242
+ output({ commands: planRollbackCommands(readJsonFile(args[0])) });
243
+ break;
244
+ }
245
+ case 'select-verify-mode': {
246
+ const round = Number(args[0]);
247
+ const max = Number(args[1]);
248
+ const aboutToExit = parseBool(args[2], 'aboutToExit');
249
+ if (Number.isNaN(round) || Number.isNaN(max)) {
250
+ fail('select-verify-mode 需要数字 <round> <max>');
251
+ }
252
+ output({ mode: selectVerifyMode(round, max, aboutToExit) });
253
+ break;
254
+ }
255
+ case 'decide-dispatch': {
256
+ if (args.length < 2) fail('decide-dispatch 需要 <phaseId> <agentMode>');
257
+ output(decideDispatch(args[0], args[1]));
258
+ break;
259
+ }
260
+ case 'interpret-impact': {
261
+ output(interpretImpactResult(readJsonFile(args[0])));
262
+ break;
263
+ }
264
+ case 'format-iteration-log-entry': {
265
+ // 输出 markdown 块(含内嵌 ```json 围栏)到 stdout,供编排器直接追加写入 iteration-log.md。
266
+ // 与其它子命令不同:本子命令输出原始 markdown 文本,而非 JSON.stringify 包裹(散文直接拼日志)。
267
+ if (!args[0]) fail('format-iteration-log-entry 需要 <entryJsonFile>');
268
+ process.stdout.write(formatIterationLogEntry(readJsonFile(args[0])));
269
+ break;
270
+ }
271
+ case 'assess-preserved-config-safety': {
272
+ // 吃原始 `git status --porcelain` 文本(解析在 core,编排器零解析):
273
+ // porcelain → state 由 parsePreservedConfigStates;state → 安全 由 assessPreservedConfigSafety。
274
+ if (!args[0]) fail('assess-preserved-config-safety 需要 <porcelainFile|-(stdin)>');
275
+ const porcelain = args[0] === '-' ? readStdin() : fs.readFileSync(args[0], 'utf-8');
276
+ const entries = parsePreservedConfigStates(porcelain, PRESERVED_CONFIG_PATHSPECS);
277
+ output(assessPreservedConfigSafety(entries));
278
+ break;
279
+ }
280
+ case 'is-clean-excluding-preserved': {
281
+ // 吃**全仓** `git status --porcelain` 文本(不带 pathspec),排除 PRESERVED_CONFIG_PATHSPECS
282
+ // 后判 isClean——杜绝"唯一 dirty 是 preserved override 却按全仓判 false → 空 stash 抓无关旧
283
+ // stash"的 CRITICAL-7 危险路径。解析全在已单测 core。
284
+ if (!args[0]) fail('is-clean-excluding-preserved 需要 <porcelainFile|-(stdin)>');
285
+ const porcelainAll = args[0] === '-' ? readStdin() : fs.readFileSync(args[0], 'utf-8');
286
+ output({ isClean: isCleanExcludingPreserved(porcelainAll, PRESERVED_CONFIG_PATHSPECS) });
287
+ break;
288
+ }
289
+ case 'acquire-lock': {
290
+ if (!args[0]) fail('acquire-lock 需要 <lockPath>');
291
+ output(acquireLock(args[0]));
292
+ break;
293
+ }
294
+ case 'release-lock': {
295
+ if (!args[0]) fail('release-lock 需要 <lockPath>');
296
+ output(releaseLock(args[0]));
297
+ break;
298
+ }
299
+ default:
300
+ fail(`未知子命令: ${subcommand ?? '(空)'}`);
301
+ }
302
+ }
303
+
304
+ // 仅当作为入口脚本直接运行时执行 dispatch(被 import 时不触发,便于单测)
305
+ if (process.argv[1] && fileURLToPath(import.meta.url) === fs.realpathSync(process.argv[1])) {
306
+ main(process.argv.slice(2));
307
+ }
@@ -46,6 +46,7 @@ ALT_CONFIG_FILE="${SPECIFY_DIR}/spec-driver.config.yaml"
46
46
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
47
47
  PLUGIN_DIR="$(dirname "$SCRIPT_DIR")"
48
48
  source "${SCRIPT_DIR}/lib/init-project-output.sh"
49
+ source "${SCRIPT_DIR}/lib/ensure-gitignore.sh"
49
50
  SPECIFY_BASE_TEMPLATES_DIR="${PLUGIN_DIR}/templates/specify-base"
50
51
  FALLBACK_SPECIFY_TEMPLATES_DIR="${PLUGIN_DIR}/../../.specify/templates"
51
52
  DEFAULT_SCORECARD_DIR="${PLUGIN_DIR}/scorecards"
@@ -291,8 +292,63 @@ detect_spec_driver_skills() {
291
292
  fi
292
293
  }
293
294
 
295
+ # 步骤 1a: 确保 Spec Driver 落盘产物的 ignore 规则就位(Feature 207,分层防线)
296
+ # 分两条防线:
297
+ # (1) .git/info/exclude — 非 tracked 的主防线(零 diff 污染),仅 .git 为目录时执行
298
+ # (2) .gitignore — 团队共享配置(.git/info/exclude 无法共享的兜底,覆盖 worktree/非 git 场景)
299
+ # 防御:吞掉共享库的非零返回(极端只读文件系统等),只记录信号,绝不 exit 中断初始化
300
+ ensure_gitignore_step() {
301
+ # 主防线:.git/info/exclude(非 git / worktree 场景返回 skipped)
302
+ local exclude_result
303
+ exclude_result="$(ensure_spec_driver_git_exclude "$PROJECT_ROOT")" || {
304
+ INIT_RESULTS+=("git_exclude:skip_error")
305
+ exclude_result="__handled__"
306
+ }
307
+ if [[ "$exclude_result" != "__handled__" ]]; then
308
+ case "$exclude_result" in
309
+ created:*) INIT_RESULTS+=("git_exclude:created:${exclude_result#created:}") ;;
310
+ appended:*) INIT_RESULTS+=("git_exclude:injected:${exclude_result#appended:}") ;;
311
+ ready:*) INIT_RESULTS+=("git_exclude:ready") ;;
312
+ failed:*) INIT_RESULTS+=("git_exclude:skip_error") ;;
313
+ skipped:*) INIT_RESULTS+=("git_exclude:skipped") ;;
314
+ *) INIT_RESULTS+=("git_exclude:unknown") ;;
315
+ esac
316
+ fi
317
+
318
+ # 兜底防线:.gitignore(团队共享配置)
319
+ local result
320
+ result="$(ensure_spec_driver_gitignore "$PROJECT_ROOT")" || {
321
+ INIT_RESULTS+=("gitignore:skip_error")
322
+ return 0
323
+ }
324
+
325
+ case "$result" in
326
+ created:*)
327
+ INIT_RESULTS+=("gitignore:created:${result#created:}")
328
+ ;;
329
+ appended:*)
330
+ INIT_RESULTS+=("gitignore:injected:${result#appended:}")
331
+ ;;
332
+ ready:*)
333
+ INIT_RESULTS+=("gitignore:ready")
334
+ ;;
335
+ failed:*)
336
+ # 写入失败(只读 FS 等)→ 复用既有 skip_error 文本分支
337
+ INIT_RESULTS+=("gitignore:skip_error")
338
+ ;;
339
+ skipped:*)
340
+ # 并发会话持锁 → 本次跳过检查,下次会话自然补齐(不等同 ready,勿撒谎为已就位)
341
+ INIT_RESULTS+=("gitignore:lock_skipped")
342
+ ;;
343
+ *)
344
+ INIT_RESULTS+=("gitignore:unknown")
345
+ ;;
346
+ esac
347
+ }
348
+
294
349
  run_init_checks() {
295
350
  init_specify_dir
351
+ ensure_gitignore_step
296
352
  sync_specify_templates
297
353
  sync_scorecard_defaults
298
354
  ensure_project_context
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * F191 kb-prequery — spec-driver 编排器在 specify 阶段前的 KB 预查(确定性脚本)。
4
+ *
5
+ * 读 resolved knowledge_sources → bin 发现 + 能力探测 → shell out `spectra scaffold-kb query`
6
+ * → 注入块透传 stdout。退出**始终 0**(不阻断 spec-driver 流程);降级原因写 stderr。
7
+ * keyword 提取 / 检索 / 格式化 / 字符 cap 全在 spectra query 侧(本脚本只薄编排)。
8
+ */
9
+
10
+ import { spawnSync } from 'node:child_process';
11
+ import { existsSync } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const PROBE_SENTINEL = 'scaffold-kb-query:1';
17
+ const SPAWN_TIMEOUT_MS = 10_000;
18
+ const MAX_BUFFER = 4 * 1024 * 1024;
19
+
20
+ function parseArgs(argv) {
21
+ const out = {};
22
+ for (let i = 0; i < argv.length; i++) {
23
+ if (argv[i] === '--requirement') out.requirement = argv[++i];
24
+ else if (argv[i] === '--project-root') out.projectRoot = argv[++i];
25
+ }
26
+ return out;
27
+ }
28
+
29
+ function diag(reason) {
30
+ process.stderr.write(`[kb-prequery] ${reason}\n`);
31
+ }
32
+
33
+ /** 经 resolve-project-context.mjs --json 读取 resolvedProfile.knowledgeSources */
34
+ function resolveKnowledgeSources(projectRoot) {
35
+ const resolver = join(__dirname, 'resolve-project-context.mjs');
36
+ const r = spawnSync('node', [resolver, '--project-root', projectRoot, '--json'], {
37
+ encoding: 'utf-8',
38
+ timeout: SPAWN_TIMEOUT_MS,
39
+ maxBuffer: MAX_BUFFER,
40
+ });
41
+ if (r.status !== 0 || typeof r.stdout !== 'string') return null;
42
+ try {
43
+ return JSON.parse(r.stdout).resolvedProfile?.knowledgeSources ?? null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /** bin 发现(覆盖优先):$SPECTRA_BIN → 项目 node_modules/.bin → PATH */
50
+ function discoverSpectraBin(projectRoot) {
51
+ const envBin = process.env['SPECTRA_BIN'];
52
+ if (envBin && existsSync(envBin)) return envBin;
53
+ const local = join(projectRoot, 'node_modules', '.bin', 'spectra');
54
+ if (existsSync(local)) return local;
55
+ return 'spectra'; // 交给 PATH;不存在则 spawnSync ENOENT
56
+ }
57
+
58
+ /**
59
+ * 能力探测:区分 ok / missing(bin 不存在 ENOENT)/ too-old(能跑但无 sentinel = 旧版无 query)。
60
+ * 修 Codex W2:原实现全归 spectra-unavailable,无法区分缺 bin 与旧版。
61
+ */
62
+ function probeBin(bin) {
63
+ const r = spawnSync(bin, ['scaffold-kb', 'query', '--probe'], {
64
+ encoding: 'utf-8',
65
+ timeout: SPAWN_TIMEOUT_MS,
66
+ maxBuffer: MAX_BUFFER,
67
+ });
68
+ if (r.error && r.error.code === 'ENOENT') return 'missing';
69
+ if (r.status === 0 && typeof r.stdout === 'string' && r.stdout.includes(PROBE_SENTINEL)) return 'ok';
70
+ return 'too-old';
71
+ }
72
+
73
+ function main() {
74
+ const { requirement, projectRoot = process.cwd() } = parseArgs(process.argv.slice(2));
75
+ if (!requirement) {
76
+ diag('no-requirement');
77
+ return;
78
+ }
79
+
80
+ const ks = resolveKnowledgeSources(projectRoot);
81
+ if (!ks || ks.enabled !== true || !ks.vendorKb) {
82
+ diag('disabled-or-unconfigured');
83
+ return;
84
+ }
85
+
86
+ const bin = discoverSpectraBin(projectRoot);
87
+ const probeResult = probeBin(bin);
88
+ if (probeResult === 'missing') {
89
+ diag(`spectra-unavailable (bin=${bin}; 未安装 spectra)`);
90
+ return;
91
+ }
92
+ if (probeResult === 'too-old') {
93
+ diag(`spectra-too-old (bin=${bin}; 版本过旧,无 scaffold-kb query 子命令)`);
94
+ return;
95
+ }
96
+
97
+ const args = [
98
+ 'scaffold-kb', 'query',
99
+ '--requirement', requirement,
100
+ '--vendor-kb', ks.vendorKb,
101
+ '--top-k', String(ks.topK ?? 3),
102
+ '--max-inject-chars', String(ks.maxInjectChars ?? 6000),
103
+ '--format', 'markdown',
104
+ ];
105
+ if (ks.projectKb) args.push('--project-kb', ks.projectKb);
106
+
107
+ const q = spawnSync(bin, args, {
108
+ encoding: 'utf-8',
109
+ timeout: SPAWN_TIMEOUT_MS,
110
+ maxBuffer: MAX_BUFFER,
111
+ });
112
+ if (q.status !== 0) {
113
+ diag(`query-failed (status=${q.status}): ${(q.stderr ?? '').trim()}`);
114
+ return;
115
+ }
116
+ const block = (q.stdout ?? '').trim();
117
+ if (block.length === 0) {
118
+ // 透传 query 自身的结构化降级原因(kb-missing / no-hit / no-query),不再一律标 no-hit(修 Codex W2)
119
+ diag(`no-injection: ${(q.stderr ?? 'no-hit').trim()}`);
120
+ return;
121
+ }
122
+ process.stdout.write(`${block}\n`);
123
+ }
124
+
125
+ main();