@geraldmaron/construct 1.0.0 → 1.0.2

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 (471) hide show
  1. package/.env.example +9 -6
  2. package/LICENSE +201 -21
  3. package/README.md +132 -257
  4. package/agents/contracts.json +387 -0
  5. package/agents/prompts/cx-accessibility.md +8 -0
  6. package/agents/prompts/cx-ai-engineer.md +92 -0
  7. package/agents/prompts/cx-architect.md +10 -2
  8. package/agents/prompts/cx-business-strategist.md +13 -0
  9. package/agents/prompts/cx-data-analyst.md +80 -0
  10. package/agents/prompts/cx-data-engineer.md +4 -0
  11. package/agents/prompts/cx-debugger.md +8 -0
  12. package/agents/prompts/cx-designer.md +19 -0
  13. package/agents/prompts/cx-devil-advocate.md +4 -0
  14. package/agents/prompts/cx-docs-keeper.md +128 -0
  15. package/agents/prompts/cx-engineer.md +13 -0
  16. package/agents/prompts/cx-evaluator.md +4 -0
  17. package/agents/prompts/cx-explorer.md +12 -0
  18. package/agents/prompts/cx-legal-compliance.md +13 -0
  19. package/agents/prompts/cx-operations.md +13 -0
  20. package/agents/prompts/cx-orchestrator.md +107 -4
  21. package/agents/prompts/cx-platform-engineer.md +75 -0
  22. package/agents/prompts/cx-product-manager.md +8 -0
  23. package/agents/prompts/cx-qa.md +100 -0
  24. package/agents/prompts/cx-rd-lead.md +13 -0
  25. package/agents/prompts/cx-release-manager.md +8 -0
  26. package/agents/prompts/cx-researcher.md +21 -1
  27. package/agents/prompts/cx-reviewer.md +8 -0
  28. package/agents/prompts/cx-security.md +104 -0
  29. package/agents/prompts/cx-sre.md +104 -0
  30. package/agents/prompts/cx-test-automation.md +4 -0
  31. package/agents/prompts/cx-trace-reviewer.md +7 -3
  32. package/agents/prompts/cx-ux-researcher.md +4 -0
  33. package/agents/registry.json +365 -90
  34. package/agents/role-manifests.json +217 -0
  35. package/bin/construct +3345 -141
  36. package/bin/construct-postinstall.mjs +87 -0
  37. package/commands/build/feature.md +6 -6
  38. package/commands/plan/feature.md +6 -5
  39. package/commands/plan/requirements.md +1 -1
  40. package/commands/ship/status.md +1 -1
  41. package/commands/work/drive.md +3 -3
  42. package/commands/work/optimize-prompts.md +3 -3
  43. package/db/{migrations → schema}/001_init.sql +2 -0
  44. package/db/schema/002_pgvector.sql +182 -0
  45. package/db/schema/003_intake.sql +47 -0
  46. package/examples/README.md +85 -0
  47. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
  48. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
  49. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
  50. package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
  51. package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
  52. package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
  53. package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
  54. package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
  55. package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
  56. package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
  57. package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
  58. package/examples/personas/construct/bad/commit-without-approval.md +30 -0
  59. package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
  60. package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
  61. package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
  62. package/examples/provider-plugin/README.md +34 -0
  63. package/examples/provider-plugin/index.mjs +74 -0
  64. package/examples/provider-plugin/package.json +15 -0
  65. package/examples/seed-observations/README.md +38 -0
  66. package/examples/seed-observations/anti-patterns.md +44 -0
  67. package/examples/seed-observations/decisions.md +36 -0
  68. package/examples/seed-observations/patterns.md +42 -0
  69. package/lib/agent-contracts-enforce.mjs +158 -0
  70. package/lib/agent-contracts.mjs +231 -0
  71. package/lib/agents/postconditions.mjs +126 -0
  72. package/lib/agents/schema.mjs +124 -0
  73. package/lib/artifact-capture.mjs +183 -0
  74. package/lib/audit-trail.mjs +149 -0
  75. package/lib/auto-docs.mjs +245 -65
  76. package/lib/beads/auto-close.mjs +126 -0
  77. package/lib/beads/drift.mjs +171 -0
  78. package/lib/beads-automation.mjs +542 -0
  79. package/lib/beads-client.mjs +518 -0
  80. package/lib/beads-lock.mjs +377 -0
  81. package/lib/beads-optimistic.mjs +365 -0
  82. package/lib/bootstrap/built-ins.mjs +136 -0
  83. package/lib/bootstrap/lazy-install.mjs +161 -0
  84. package/lib/bootstrap/resources.mjs +120 -0
  85. package/lib/bootstrap.mjs +105 -0
  86. package/lib/cache-governor.js +213 -0
  87. package/lib/cache-strategy-anthropic.js +62 -0
  88. package/lib/cache-strategy-google.js +79 -0
  89. package/lib/cache-strategy-none.js +30 -0
  90. package/lib/cache-strategy-openai.js +48 -0
  91. package/lib/cache-strategy.js +91 -0
  92. package/lib/claude-allow.mjs +149 -0
  93. package/lib/cli-commands.mjs +413 -258
  94. package/lib/codex-config.mjs +3 -1
  95. package/lib/comment-lint.mjs +55 -6
  96. package/lib/completions.mjs +21 -6
  97. package/lib/config/alias.mjs +56 -0
  98. package/lib/config/project-config.mjs +335 -0
  99. package/lib/config/schema.mjs +159 -0
  100. package/lib/context-router.mjs +308 -0
  101. package/lib/cost-ledger.mjs +177 -0
  102. package/lib/cost.mjs +171 -10
  103. package/lib/dashboard-static.mjs +158 -0
  104. package/lib/deployment-mode.mjs +86 -0
  105. package/lib/deprecate.mjs +49 -0
  106. package/lib/dispatch-batch.js +183 -0
  107. package/lib/distill.mjs +21 -8
  108. package/lib/doc-stamp.mjs +164 -0
  109. package/lib/doc-verify.mjs +119 -0
  110. package/lib/docs-routing.mjs +89 -0
  111. package/lib/docs-verify.mjs +417 -0
  112. package/lib/doctor/audit.mjs +71 -0
  113. package/lib/doctor/cli.mjs +99 -0
  114. package/lib/doctor/escalate.mjs +29 -0
  115. package/lib/doctor/index.mjs +140 -0
  116. package/lib/doctor/report.mjs +170 -0
  117. package/lib/doctor/watchers/bd-watch.mjs +117 -0
  118. package/lib/doctor/watchers/cost.mjs +130 -0
  119. package/lib/doctor/watchers/disk.mjs +122 -0
  120. package/lib/doctor/watchers/handoffs.mjs +33 -0
  121. package/lib/doctor/watchers/process-pressure.mjs +60 -0
  122. package/lib/doctor/watchers/service-health.mjs +188 -0
  123. package/lib/document-extract.mjs +288 -0
  124. package/lib/document-ingest.mjs +230 -0
  125. package/lib/drop.mjs +282 -0
  126. package/lib/embed/approval-queue.mjs +176 -0
  127. package/lib/embed/artifact.mjs +349 -0
  128. package/lib/embed/authority-guard.mjs +155 -0
  129. package/lib/embed/cli.mjs +408 -0
  130. package/lib/embed/config.mjs +355 -0
  131. package/lib/embed/conflict-detection.mjs +264 -0
  132. package/lib/embed/customer-profiles.mjs +480 -0
  133. package/lib/embed/daemon.mjs +1309 -0
  134. package/lib/embed/demand-fetch.mjs +449 -0
  135. package/lib/embed/docs-lifecycle.mjs +349 -0
  136. package/lib/embed/inbox-live-watcher.mjs +119 -0
  137. package/lib/embed/inbox.mjs +343 -0
  138. package/lib/embed/intake-metrics.mjs +190 -0
  139. package/lib/embed/jobs/vector-sync.mjs +198 -0
  140. package/lib/embed/notifications.mjs +75 -0
  141. package/lib/embed/output.mjs +79 -0
  142. package/lib/embed/providers/github.mjs +295 -0
  143. package/lib/embed/providers/jira.mjs +192 -0
  144. package/lib/embed/providers/linear.mjs +186 -0
  145. package/lib/embed/providers/registry.mjs +115 -0
  146. package/lib/embed/providers/slack.mjs +203 -0
  147. package/lib/embed/recommendation-store.mjs +378 -0
  148. package/lib/embed/roadmap.mjs +374 -0
  149. package/lib/embed/role-framing.mjs +110 -0
  150. package/lib/embed/scheduler.mjs +99 -0
  151. package/lib/embed/semantic.mjs +325 -0
  152. package/lib/embed/snapshot.mjs +191 -0
  153. package/lib/embed/supervision.mjs +235 -0
  154. package/lib/embed/target-resolver.mjs +186 -0
  155. package/lib/embed/worker.mjs +62 -0
  156. package/lib/embed/workspaces.mjs +297 -0
  157. package/lib/engine/chunker-headings.mjs +110 -0
  158. package/lib/engine/compressor-heuristic.mjs +100 -0
  159. package/lib/engine/consolidate.mjs +287 -0
  160. package/lib/engine/contracts.mjs +126 -0
  161. package/lib/engine/defaults.mjs +129 -0
  162. package/lib/engine/eval-retrieval.mjs +148 -0
  163. package/lib/engine/fuser-rrf.mjs +62 -0
  164. package/lib/engine/index.mjs +37 -0
  165. package/lib/engine/registry.mjs +146 -0
  166. package/lib/engine/reranker-mmr.mjs +90 -0
  167. package/lib/engine/tokens.mjs +77 -0
  168. package/lib/entity-store.mjs +280 -0
  169. package/lib/env-config.mjs +69 -4
  170. package/lib/evals/retrieval-bench.mjs +159 -0
  171. package/lib/evaluator-optimizer.mjs +317 -0
  172. package/lib/features.mjs +159 -29
  173. package/lib/gates-audit.mjs +236 -0
  174. package/lib/git-hooks/prepare-commit-msg +58 -0
  175. package/lib/handoffs/cleanup.mjs +159 -0
  176. package/lib/handoffs/contract.mjs +162 -0
  177. package/lib/handoffs/inventory.mjs +120 -0
  178. package/lib/headhunt.mjs +28 -47
  179. package/lib/health-check.mjs +399 -0
  180. package/lib/hook-health.mjs +442 -0
  181. package/lib/hooks/_lib/log.mjs +82 -0
  182. package/lib/hooks/adaptive-lint.mjs +26 -2
  183. package/lib/hooks/agent-tracker.mjs +171 -14
  184. package/lib/hooks/audit-reads.mjs +109 -0
  185. package/lib/hooks/audit-trail.mjs +153 -0
  186. package/lib/hooks/bash-output-logger.mjs +71 -0
  187. package/lib/hooks/block-no-verify.mjs +41 -0
  188. package/lib/hooks/ci-status-check.mjs +82 -0
  189. package/lib/hooks/comment-lint.mjs +29 -7
  190. package/lib/hooks/config-protection.mjs +39 -18
  191. package/lib/hooks/context-watch.mjs +137 -0
  192. package/lib/hooks/context-window-recovery.mjs +4 -20
  193. package/lib/hooks/dep-audit.mjs +16 -0
  194. package/lib/hooks/doc-coupling-check.mjs +80 -0
  195. package/lib/hooks/edit-accumulator.mjs +3 -0
  196. package/lib/hooks/edit-error-recovery.mjs +3 -0
  197. package/lib/hooks/edit-guard.mjs +45 -4
  198. package/lib/hooks/env-check.mjs +3 -0
  199. package/lib/hooks/guard-bash.mjs +58 -1
  200. package/lib/hooks/mcp-audit.mjs +18 -20
  201. package/lib/hooks/mcp-health-check.mjs +36 -0
  202. package/lib/hooks/model-fallback.mjs +42 -56
  203. package/lib/hooks/policy-engine.mjs +209 -0
  204. package/lib/hooks/post-merge-docs-check.mjs +63 -0
  205. package/lib/hooks/pre-compact.mjs +4 -24
  206. package/lib/hooks/pre-push-gate.mjs +200 -37
  207. package/lib/hooks/proactive-activation.mjs +284 -0
  208. package/lib/hooks/read-tracker.mjs +27 -4
  209. package/lib/hooks/readme-age-check.mjs +77 -0
  210. package/lib/hooks/registry-sync.mjs +12 -5
  211. package/lib/hooks/scan-secrets.mjs +50 -7
  212. package/lib/hooks/session-optimize.mjs +311 -0
  213. package/lib/hooks/session-start.mjs +287 -28
  214. package/lib/hooks/stop-notify.mjs +236 -96
  215. package/lib/hooks/stop-typecheck.mjs +13 -1
  216. package/lib/hooks/test-watch.mjs +68 -0
  217. package/lib/host-capabilities.mjs +31 -10
  218. package/lib/init-docs.mjs +731 -291
  219. package/lib/init-unified.mjs +1000 -0
  220. package/lib/init-update.mjs +168 -0
  221. package/lib/init.mjs +107 -0
  222. package/lib/install/first-invocation.mjs +119 -0
  223. package/lib/install/stage-project.mjs +69 -0
  224. package/lib/intake/classify.mjs +278 -0
  225. package/lib/intake/feedback.mjs +273 -0
  226. package/lib/intake/filesystem-queue.mjs +158 -0
  227. package/lib/intake/intake-config.mjs +132 -0
  228. package/lib/intake/postgres-queue.mjs +198 -0
  229. package/lib/intake/prepare.mjs +139 -0
  230. package/lib/intake/queue.mjs +83 -0
  231. package/lib/intake/session-prelude.mjs +50 -0
  232. package/lib/integrations/intake-integrations.mjs +740 -0
  233. package/lib/intent-classifier.mjs +253 -0
  234. package/lib/knowledge/layout.mjs +72 -0
  235. package/lib/knowledge/rag.mjs +331 -0
  236. package/lib/knowledge/search.mjs +315 -0
  237. package/lib/knowledge/trends.mjs +261 -0
  238. package/lib/logger.mjs +85 -0
  239. package/lib/mcp/broker.mjs +124 -0
  240. package/lib/mcp/server.mjs +554 -854
  241. package/lib/mcp/tools/document.mjs +132 -0
  242. package/lib/mcp/tools/memory.mjs +209 -0
  243. package/lib/mcp/tools/project.mjs +349 -0
  244. package/lib/mcp/tools/skills.mjs +409 -0
  245. package/lib/mcp/tools/storage.mjs +60 -0
  246. package/lib/mcp/tools/telemetry.mjs +315 -0
  247. package/lib/mcp/tools/workflow.mjs +112 -0
  248. package/lib/mcp-catalog.json +54 -3
  249. package/lib/mcp-manager.mjs +96 -48
  250. package/lib/mcp-platform-config.mjs +22 -22
  251. package/lib/memory-stats.mjs +121 -0
  252. package/lib/mode-commands.mjs +124 -0
  253. package/lib/model-free-selector.mjs +184 -0
  254. package/lib/model-pricing.mjs +152 -0
  255. package/lib/model-registry.mjs +226 -0
  256. package/lib/model-router.mjs +362 -386
  257. package/lib/observation-store.mjs +391 -0
  258. package/lib/ollama-manager.mjs +428 -0
  259. package/lib/opencode-config.mjs +13 -3
  260. package/lib/opencode-runtime-plugin.mjs +70 -38
  261. package/lib/opencode-telemetry.mjs +64 -6
  262. package/lib/orchestration-policy.mjs +509 -6
  263. package/lib/overrides/resolver.mjs +207 -0
  264. package/lib/parity.mjs +147 -0
  265. package/lib/paths.mjs +33 -0
  266. package/lib/performance/generate.mjs +212 -0
  267. package/lib/plugin-registry.mjs +268 -0
  268. package/lib/policy/engine.mjs +130 -0
  269. package/lib/policy/unified-gates.mjs +96 -0
  270. package/lib/project-detection.mjs +129 -0
  271. package/lib/project-init-shared.mjs +272 -0
  272. package/lib/project-profile.mjs +447 -0
  273. package/lib/prompt-composer.js +434 -0
  274. package/lib/prompt-metadata.mjs +1 -1
  275. package/lib/provider-capabilities-anthropic.js +44 -0
  276. package/lib/provider-capabilities-deepseek.js +37 -0
  277. package/lib/provider-capabilities-generic.js +26 -0
  278. package/lib/provider-capabilities-google.js +47 -0
  279. package/lib/provider-capabilities-openai.js +45 -0
  280. package/lib/provider-capabilities.js +142 -0
  281. package/lib/providers/atlassian-confluence/index.mjs +103 -0
  282. package/lib/providers/atlassian-jira/index.mjs +100 -0
  283. package/lib/providers/auth-manager.mjs +126 -0
  284. package/lib/providers/circuit-breaker.mjs +124 -0
  285. package/lib/providers/contract.mjs +93 -0
  286. package/lib/providers/github/index.mjs +126 -0
  287. package/lib/providers/registry.mjs +184 -0
  288. package/lib/providers/salesforce/index.mjs +100 -0
  289. package/lib/providers/slack/index.mjs +80 -0
  290. package/lib/reflect.mjs +137 -0
  291. package/lib/research-lint.mjs +164 -0
  292. package/lib/resources/budget.mjs +259 -0
  293. package/lib/role-preload.mjs +21 -6
  294. package/lib/roles/approval-surface.mjs +54 -0
  295. package/lib/roles/cli.mjs +118 -0
  296. package/lib/roles/event-bus.mjs +79 -0
  297. package/lib/roles/fence.mjs +84 -0
  298. package/lib/roles/gateway.mjs +260 -0
  299. package/lib/roles/hook-emit.mjs +37 -0
  300. package/lib/roles/manifest.mjs +48 -0
  301. package/lib/roles/router.mjs +27 -0
  302. package/lib/runtime-pressure.mjs +360 -0
  303. package/lib/schema-artifact.mjs +134 -0
  304. package/lib/schema-infer.mjs +551 -0
  305. package/lib/server/auth.mjs +168 -0
  306. package/lib/server/chat.mjs +336 -0
  307. package/lib/server/cors.mjs +77 -0
  308. package/lib/server/csrf.mjs +91 -0
  309. package/lib/server/index.mjs +1927 -78
  310. package/lib/server/insights.mjs +765 -0
  311. package/lib/server/rate-limit.mjs +91 -0
  312. package/lib/server/static/assets/index-ab25c707.js +70 -0
  313. package/lib/server/static/assets/index-f0c80a2b.css +1 -0
  314. package/lib/server/static/index.html +12 -817
  315. package/lib/server/telemetry-login.mjs +108 -0
  316. package/lib/server/webhook.mjs +510 -0
  317. package/lib/service-manager.mjs +522 -58
  318. package/lib/services/pattern-promotion-service.mjs +167 -0
  319. package/lib/services/telemetry-backend.mjs +178 -0
  320. package/lib/session-store.mjs +374 -0
  321. package/lib/setup-prompts.mjs +96 -0
  322. package/lib/setup.mjs +523 -36
  323. package/lib/skills-apply.mjs +280 -0
  324. package/lib/skills-scope.mjs +118 -0
  325. package/lib/status.mjs +261 -70
  326. package/lib/storage/admin.mjs +355 -0
  327. package/lib/storage/backend.mjs +2 -1
  328. package/lib/storage/backup.mjs +347 -0
  329. package/lib/storage/embeddings-engine.mjs +133 -0
  330. package/lib/storage/embeddings-legacy.mjs +85 -0
  331. package/lib/storage/embeddings-local.mjs +108 -0
  332. package/lib/storage/embeddings-ollama.mjs +78 -0
  333. package/lib/storage/embeddings-openai.mjs +85 -0
  334. package/lib/storage/embeddings.mjs +92 -33
  335. package/lib/storage/file-lock.mjs +130 -0
  336. package/lib/storage/fusion.mjs +95 -0
  337. package/lib/storage/hybrid-query.mjs +34 -27
  338. package/lib/storage/migrations.mjs +187 -0
  339. package/lib/storage/postgres-backup.mjs +124 -0
  340. package/lib/storage/sql-store.mjs +5 -15
  341. package/lib/storage/state-source.mjs +12 -13
  342. package/lib/storage/store-version.mjs +115 -0
  343. package/lib/storage/sync.mjs +144 -35
  344. package/lib/storage/unified-storage.mjs +550 -0
  345. package/lib/storage/vector-client.mjs +286 -0
  346. package/lib/storage/vector-store.mjs +71 -30
  347. package/lib/task-graph/generate.mjs +135 -0
  348. package/lib/task-graph/schema.mjs +81 -0
  349. package/lib/task-graph/store.mjs +71 -0
  350. package/lib/telemetry/backends/local.mjs +62 -0
  351. package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
  352. package/lib/telemetry/backfill.mjs +180 -0
  353. package/lib/telemetry/eval-datasets.mjs +203 -0
  354. package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
  355. package/lib/telemetry/intent-verifications.mjs +86 -0
  356. package/lib/telemetry/llm-judge.mjs +350 -0
  357. package/lib/telemetry/model-pricing-catalog.mjs +557 -0
  358. package/lib/telemetry/setup.mjs +151 -0
  359. package/lib/telemetry/skill-calls.mjs +78 -0
  360. package/lib/telemetry/team-rollup.mjs +4 -4
  361. package/lib/token-engine.js +117 -0
  362. package/lib/token-estimator-anthropic.js +15 -0
  363. package/lib/token-estimator-deepseek.js +13 -0
  364. package/lib/token-estimator-default.js +13 -0
  365. package/lib/token-estimator-google.js +13 -0
  366. package/lib/token-estimator-openai.js +13 -0
  367. package/lib/toolkit-env.mjs +1 -1
  368. package/lib/tty-prompts.mjs +211 -0
  369. package/lib/uninstall/uninstall.mjs +423 -0
  370. package/lib/update.mjs +115 -0
  371. package/lib/upgrade.mjs +141 -0
  372. package/lib/validator.mjs +51 -2
  373. package/lib/validators/skills.mjs +142 -0
  374. package/lib/wireframe.mjs +422 -0
  375. package/lib/worker/entrypoint.mjs +241 -0
  376. package/lib/worker/evidence.mjs +107 -0
  377. package/lib/worker/run.mjs +154 -0
  378. package/lib/worker/trace.mjs +182 -0
  379. package/lib/workflow-state.mjs +14 -18
  380. package/package.json +21 -8
  381. package/personas/construct.md +53 -51
  382. package/platforms/claude/CLAUDE.md +1 -1
  383. package/platforms/claude/settings.template.json +115 -68
  384. package/platforms/opencode/config.template.json +3 -7
  385. package/rules/common/agents.md +11 -38
  386. package/rules/common/beads-hygiene.md +75 -0
  387. package/rules/common/code-review.md +11 -100
  388. package/rules/common/coding-style.md +5 -3
  389. package/rules/common/comments.md +30 -111
  390. package/rules/common/commit-approval.md +53 -0
  391. package/rules/common/cx-agent-routing.md +1 -0
  392. package/rules/common/development-workflow.md +23 -41
  393. package/rules/common/doc-ownership.md +54 -0
  394. package/rules/common/efficiency.md +57 -0
  395. package/rules/common/framing.md +76 -0
  396. package/rules/common/git-workflow.md +2 -4
  397. package/rules/common/patterns.md +3 -7
  398. package/rules/common/performance.md +15 -31
  399. package/rules/common/release-gates.md +69 -0
  400. package/rules/common/research.md +107 -0
  401. package/rules/common/security.md +6 -6
  402. package/rules/common/skill-composition.md +67 -0
  403. package/rules/common/testing.md +15 -27
  404. package/rules/golang/hooks.md +0 -4
  405. package/rules/policy/bootstrap.yaml +11 -0
  406. package/rules/policy/drive.yaml +8 -0
  407. package/rules/policy/task.yaml +9 -0
  408. package/rules/policy/workflow.yaml +9 -0
  409. package/rules/python/hooks.md +0 -4
  410. package/rules/swift/hooks.md +0 -4
  411. package/rules/typescript/hooks.md +0 -4
  412. package/rules/web/hooks.md +0 -2
  413. package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
  414. package/skills/ai/prompt-optimizer.md +7 -7
  415. package/skills/compliance/ai-disclosure.md +58 -0
  416. package/skills/compliance/data-privacy.md +46 -0
  417. package/skills/compliance/license-audit.md +40 -0
  418. package/skills/compliance/regulatory-review.md +61 -0
  419. package/skills/docs/document-ingest-workflow.md +52 -0
  420. package/skills/docs/evidence-ingest-workflow.md +9 -0
  421. package/skills/docs/init-docs.md +51 -18
  422. package/skills/docs/product-intelligence-workflow.md +12 -0
  423. package/skills/docs/product-signal-workflow.md +4 -0
  424. package/skills/docs/research-workflow.md +23 -8
  425. package/skills/docs/runbook-workflow.md +1 -1
  426. package/skills/operating/orchestration-reference.md +151 -0
  427. package/skills/roles/architect.md +5 -0
  428. package/skills/roles/engineer.md +32 -0
  429. package/skills/roles/operator.md +12 -0
  430. package/skills/roles/reviewer.md +23 -0
  431. package/skills/routing.md +1 -1
  432. package/templates/devcontainer/Dockerfile.devcontainer +38 -0
  433. package/templates/devcontainer/devcontainer.json +31 -0
  434. package/templates/distribution/bootstrap.ps1 +142 -0
  435. package/templates/distribution/bootstrap.sh +196 -0
  436. package/templates/distribution/run.mjs +187 -0
  437. package/templates/docs/adr.md +44 -8
  438. package/templates/docs/changelog-entry.md +43 -0
  439. package/templates/docs/construct_guide.md +149 -0
  440. package/templates/docs/evidence-brief.md +2 -2
  441. package/templates/docs/meta-prd.md +140 -19
  442. package/templates/docs/onboarding.md +57 -0
  443. package/templates/docs/prd.md +159 -15
  444. package/templates/docs/research-brief.md +4 -4
  445. package/templates/homebrew/construct.rb +67 -0
  446. package/langfuse/docker-compose.yml +0 -82
  447. package/lib/eval-harness.mjs +0 -59
  448. package/lib/hooks/bootstrap-guard.mjs +0 -90
  449. package/lib/hooks/console-warn.mjs +0 -43
  450. package/lib/hooks/continuation-enforcer.mjs +0 -72
  451. package/lib/hooks/drive-guard.mjs +0 -89
  452. package/lib/hooks/mcp-task-scope.mjs +0 -47
  453. package/lib/hooks/task-completed-guard.mjs +0 -43
  454. package/lib/hooks/teammate-idle-guard.mjs +0 -54
  455. package/lib/hooks/workflow-guard.mjs +0 -62
  456. package/lib/prompt-composer.mjs +0 -196
  457. package/lib/review.mjs +0 -429
  458. package/lib/server/static/app.js +0 -841
  459. package/lib/telemetry/langfuse-model-sync.mjs +0 -270
  460. package/rules/common/hooks.md +0 -35
  461. package/rules/zh/README.md +0 -113
  462. package/rules/zh/agents.md +0 -55
  463. package/rules/zh/code-review.md +0 -129
  464. package/rules/zh/coding-style.md +0 -53
  465. package/rules/zh/development-workflow.md +0 -49
  466. package/rules/zh/git-workflow.md +0 -29
  467. package/rules/zh/hooks.md +0 -35
  468. package/rules/zh/patterns.md +0 -36
  469. package/rules/zh/performance.md +0 -60
  470. package/rules/zh/security.md +0 -34
  471. package/rules/zh/testing.md +0 -34
@@ -0,0 +1,198 @@
1
+ /**
2
+ * lib/embed/jobs/vector-sync.mjs — Automatic embedding pipeline for observations.
3
+ *
4
+ * Runs on a schedule (default: every 5 minutes) to embed new/modified observations
5
+ * and store them in Postgres (if available) and/or the local vector index.
6
+ *
7
+ * Design:
8
+ * - Scans .cx/observations/ for items not yet embedded
9
+ * - Embeds each using the configured model (local ONNX, OpenAI, Ollama)
10
+ * - Stores in Postgres construct_observations table (if DATABASE_URL set)
11
+ * - Falls back to local vectors.json if Postgres unavailable
12
+ * - Batched processing to avoid overwhelming the embedding API
13
+ * - Deduplicated via content hash comparison
14
+ */
15
+ import { existsSync, readFileSync, writeFileSync, statSync, mkdirSync } from 'node:fs';
16
+ import { join, dirname } from 'node:path';
17
+ import { homedir } from 'node:os';
18
+
19
+ const DEFAULT_BATCH_SIZE = 32;
20
+ const DEFAULT_INTERVAL_MS = 5 * 60 * 1000;
21
+
22
+ /**
23
+ * Find observations that haven't been embedded yet.
24
+ * @param {string} rootDir
25
+ * @returns {Array<{ id: string, project: string, role: string, category: string, summary: string, content: string, tags: string[], confidence: number, source: string, filePath: string }>}
26
+ */
27
+ function findUnembeddedObservations(rootDir) {
28
+ const obsDir = join(rootDir, '.cx', 'observations');
29
+ if (!existsSync(obsDir)) return [];
30
+
31
+ const indexPath = join(obsDir, 'index.json');
32
+ if (!existsSync(indexPath)) return [];
33
+
34
+ let index;
35
+ try {
36
+ index = JSON.parse(readFileSync(indexPath, 'utf8'));
37
+ } catch (err) {
38
+ process.stderr.write('[jobs/vector-sync.mjs] findUnembedded: ' + (err?.message ?? String(err)) + '\n');
39
+ return [];
40
+ }
41
+
42
+ // Check which observations have been embedded
43
+ const vectorsPath = join(obsDir, 'vectors.json');
44
+ const embeddedIds = new Set();
45
+ if (existsSync(vectorsPath)) {
46
+ try {
47
+ const vectors = JSON.parse(readFileSync(vectorsPath, 'utf8'));
48
+ for (const v of (vectors.records || vectors)) {
49
+ if (v.id) embeddedIds.add(v.id);
50
+ }
51
+ } catch { /* ignore */ }
52
+ }
53
+
54
+ // Also check Postgres if available
55
+ // (This is handled by the caller via VectorClient.isHealthy())
56
+
57
+ return (index || [])
58
+ .filter((obs) => obs.id && !embeddedIds.has(obs.id))
59
+ .map((obs) => ({
60
+ ...obs,
61
+ filePath: join(obsDir, `${obs.id}.json`),
62
+ }))
63
+ .filter((obs) => existsSync(obs.filePath));
64
+ }
65
+
66
+ /**
67
+ * Run the vector sync job.
68
+ * @param {object} opts
69
+ * @param {string} opts.rootDir - Root directory for observation store
70
+ * @param {object} opts.env - Environment variables
71
+ * @param {object} opts.logger - Optional logger with debug/info/error methods
72
+ * @param {object} opts.vectorClient - Optional VectorClient instance for SQL storage
73
+ * @returns {Promise<{ synced: number, errors: number, durationMs: number }>}
74
+ */
75
+ export async function runVectorSync({ rootDir, env = process.env, logger, vectorClient } = {}) {
76
+ const startTime = Date.now();
77
+ const batchSize = parseInt(env.CONSTRUCT_EMBEDDING_BATCH_SIZE, 10) || DEFAULT_BATCH_SIZE;
78
+
79
+ try {
80
+ const unembedded = findUnembeddedObservations(rootDir);
81
+ if (!unembedded.length) {
82
+ logger?.debug?.('Vector sync: no new observations to embed');
83
+ return { synced: 0, errors: 0, durationMs: Date.now() - startTime };
84
+ }
85
+
86
+ let synced = 0;
87
+ let errors = 0;
88
+
89
+ // Dynamically import embedding engine
90
+ const { embedBatch } = await import('../../storage/embeddings-engine.mjs');
91
+
92
+ for (let i = 0; i < unembedded.length; i += batchSize) {
93
+ const batch = unembedded.slice(i, i + batchSize);
94
+ const texts = batch.map((o) => `${o.summary}\n${o.content}`);
95
+
96
+ try {
97
+ const embeddings = await embedBatch(texts, { env });
98
+
99
+ for (let j = 0; j < batch.length; j++) {
100
+ const obs = batch[j];
101
+ const embedding = embeddings[j].embedding;
102
+
103
+ // Store in Postgres if available
104
+ if (vectorClient && await vectorClient.isHealthy()) {
105
+ try {
106
+ await vectorClient.storeObservation({
107
+ id: obs.id,
108
+ project: obs.project || rootDir.split('/').pop(),
109
+ role: obs.role,
110
+ category: obs.category,
111
+ summary: obs.summary,
112
+ content: obs.content,
113
+ tags: obs.tags || [],
114
+ confidence: obs.confidence ?? 0.8,
115
+ source: obs.source,
116
+ embedding,
117
+ });
118
+ } catch (err) {
119
+ logger?.error?.(`Vector sync: failed to store observation ${obs.id} in SQL: ${err.message}`);
120
+ errors++;
121
+ continue;
122
+ }
123
+ }
124
+
125
+ // Always update local vectors.json
126
+ updateLocalVectorIndex(rootDir, obs, embedding);
127
+ synced++;
128
+ }
129
+ } catch (err) {
130
+ logger?.error?.(`Vector sync: batch embedding failed: ${err.message}`);
131
+ errors += batch.length;
132
+ }
133
+ }
134
+
135
+ const durationMs = Date.now() - startTime;
136
+ logger?.info?.(`Vector sync: embedded ${synced} observation(s) in ${durationMs}ms (${errors} errors)`);
137
+ return { synced, errors, durationMs };
138
+ } catch (err) {
139
+ const durationMs = Date.now() - startTime;
140
+ logger?.error?.(`Vector sync failed: ${err.message}`);
141
+ return { synced: 0, errors: 1, durationMs };
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Update the local vectors.json index with a new embedding.
147
+ * @param {string} rootDir
148
+ * @param {object} obs - Observation record
149
+ * @param {Float32Array} embedding
150
+ */
151
+ function updateLocalVectorIndex(rootDir, obs, embedding) {
152
+ const obsDir = join(rootDir, '.cx', 'observations');
153
+ const vectorsPath = join(obsDir, 'vectors.json');
154
+
155
+ let vectors;
156
+ if (existsSync(vectorsPath)) {
157
+ try {
158
+ vectors = JSON.parse(readFileSync(vectorsPath, 'utf8'));
159
+ } catch (err) {
160
+ process.stderr.write('[jobs/vector-sync.mjs] updateLocalVectorIndex: ' + (err?.message ?? String(err)) + '\n');
161
+ vectors = { version: 1, model: 'hashing-bow-v1', updatedAt: null, records: [] };
162
+ }
163
+ } else {
164
+ vectors = { version: 1, model: 'hashing-bow-v1', updatedAt: null, records: [] };
165
+ }
166
+
167
+ // Add or update the record
168
+ const existingIdx = vectors.records.findIndex((r) => r.id === obs.id);
169
+ const record = {
170
+ id: obs.id,
171
+ embedding: Array.from(embedding),
172
+ summary: obs.summary,
173
+ content: obs.content,
174
+ tags: obs.tags || [],
175
+ project: obs.project,
176
+ role: obs.role,
177
+ category: obs.category,
178
+ updatedAt: new Date().toISOString(),
179
+ };
180
+
181
+ if (existingIdx >= 0) {
182
+ vectors.records[existingIdx] = record;
183
+ } else {
184
+ vectors.records.push(record);
185
+ }
186
+
187
+ // Cap at 1000 records (sliding window)
188
+ if (vectors.records.length > 1000) {
189
+ vectors.records = vectors.records.slice(-1000);
190
+ }
191
+
192
+ vectors.updatedAt = new Date().toISOString();
193
+
194
+ mkdirSync(dirname(vectorsPath), { recursive: true });
195
+ writeFileSync(vectorsPath, JSON.stringify(vectors, null, 2), 'utf8');
196
+ }
197
+
198
+ export { DEFAULT_BATCH_SIZE, DEFAULT_INTERVAL_MS };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * lib/embed/notifications.mjs — embed action notification bus.
3
+ *
4
+ * Decoupled event emitter for embed daemon → dashboard SSE.
5
+ * The dashboard server subscribes to this bus; the daemon emits events.
6
+ * Slack stub is wired but no-ops without config.
7
+ */
8
+
9
+ import { EventEmitter } from 'node:events';
10
+
11
+ const bus = new EventEmitter();
12
+ bus.setMaxListeners(50);
13
+
14
+ /**
15
+ * Emit a toast notification for an embed action.
16
+ *
17
+ * @param {object} event
18
+ * @param {string} event.type - 'info' | 'success' | 'warning' | 'error'
19
+ * @param {string} event.source - job name (e.g. 'docs-lifecycle', 'roadmap', 'snapshot')
20
+ * @param {string} event.message - human-readable description
21
+ * @param {object} [event.meta] - optional structured metadata
22
+ */
23
+ export function emitEmbedNotification(event) {
24
+ bus.emit('embed:action', {
25
+ ...event,
26
+ ts: Date.now(),
27
+ });
28
+ }
29
+
30
+ /**
31
+ * Subscribe to embed notifications.
32
+ * @param {(event: object) => void} handler
33
+ * @returns {() => void} unsubscribe function
34
+ */
35
+ export function onEmbedNotification(handler) {
36
+ bus.on('embed:action', handler);
37
+ return () => bus.off('embed:action', handler);
38
+ }
39
+
40
+ /**
41
+ * Stub Slack notification path.
42
+ * Posts to Slack when SLACK_WEBHOOK_URL is set, otherwise no-ops.
43
+ *
44
+ * @param {object} event - same shape as emitEmbedNotification
45
+ * @param {object} [env] - process.env override for testing
46
+ */
47
+ export async function notifySlack(event, env = process.env) {
48
+ const webhookUrl = env.SLACK_EMBED_WEBHOOK_URL ?? env.SLACK_WEBHOOK_URL;
49
+ if (!webhookUrl) return { sent: false, reason: 'no-webhook-url' };
50
+
51
+ try {
52
+ const body = JSON.stringify({
53
+ text: `[${event.source}] ${event.message}`,
54
+ blocks: [
55
+ {
56
+ type: 'section',
57
+ text: {
58
+ type: 'mrkdwn',
59
+ text: `*${event.source}* — ${event.message}`,
60
+ },
61
+ },
62
+ ],
63
+ });
64
+
65
+ const res = await fetch(webhookUrl, {
66
+ method: 'POST',
67
+ headers: { 'Content-Type': 'application/json' },
68
+ body,
69
+ });
70
+
71
+ return { sent: res.ok, status: res.status };
72
+ } catch (err) {
73
+ return { sent: false, reason: err.message };
74
+ }
75
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * lib/embed/output.mjs — snapshot output dispatcher.
3
+ *
4
+ * Routes a rendered snapshot to one or more configured output targets:
5
+ * - markdown: write to a file
6
+ * - slack: post to a channel via the slack provider (authority-guarded)
7
+ * - log: print to stdout (default / fallback)
8
+ */
9
+
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { renderMarkdown } from './snapshot.mjs';
13
+
14
+ /**
15
+ * Dispatch a snapshot to all configured outputs.
16
+ * @param {object} snapshot - Raw snapshot object from SnapshotEngine
17
+ * @param {object[]} outputs - Output configs from embed config
18
+ * @param {ProviderRegistry} registry
19
+ * @param {AuthorityGuard} [authorityGuard] - Optional guard; if omitted, Slack posts proceed unchecked
20
+ */
21
+ export async function dispatchOutputs(snapshot, outputs, registry, authorityGuard, rootDir) {
22
+ const markdown = renderMarkdown(snapshot);
23
+ const results = [];
24
+
25
+ for (const output of outputs ?? []) {
26
+ try {
27
+ switch (output.type) {
28
+ case 'markdown': {
29
+ const rel = output.path ?? '.cx/snapshot.md';
30
+ const filePath = path.isAbsolute(rel) ? rel : path.resolve(rootDir ?? process.cwd(), rel);
31
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
32
+ fs.writeFileSync(filePath, markdown, 'utf8');
33
+ results.push({ type: 'markdown', path: filePath, status: 'ok' });
34
+ break;
35
+ }
36
+ case 'slack': {
37
+ const provider = registry?.get(output.provider ?? 'slack');
38
+ if (!provider) throw new Error(`Slack provider not registered`);
39
+
40
+ // Authority check — Slack posts are externalPost actions
41
+ if (authorityGuard) {
42
+ const decision = await authorityGuard.check('externalPost', {
43
+ description: `Snapshot post to Slack channel ${output.channel}`,
44
+ payload: { channel: output.channel },
45
+ });
46
+ if (!decision.allowed) {
47
+ results.push({
48
+ type: 'slack',
49
+ channel: output.channel,
50
+ status: 'queued',
51
+ queueId: decision.queueId,
52
+ reason: decision.reason,
53
+ });
54
+ break;
55
+ }
56
+ }
57
+
58
+ await provider.write({
59
+ type: 'message',
60
+ channel: output.channel,
61
+ text: markdown.slice(0, 3000), // Slack message limit
62
+ });
63
+ results.push({ type: 'slack', channel: output.channel, status: 'ok' });
64
+ break;
65
+ }
66
+ case 'log':
67
+ default: {
68
+ process.stdout.write(markdown + '\n');
69
+ results.push({ type: 'log', status: 'ok' });
70
+ break;
71
+ }
72
+ }
73
+ } catch (err) {
74
+ results.push({ type: output.type, status: 'error', error: err.message });
75
+ }
76
+ }
77
+
78
+ return results;
79
+ }
@@ -0,0 +1,295 @@
1
+ /**
2
+ * lib/embed/providers/github.mjs — GitHub provider for embed mode.
3
+ *
4
+ * Reads PRs, issues, commits, repo metadata, README, and top-level docs from
5
+ * one or more repositories using the GitHub REST API. Zero external deps —
6
+ * uses Node's built-in fetch.
7
+ *
8
+ * Supported refs:
9
+ * prs Open pull requests
10
+ * issues Open issues (excludes PRs)
11
+ * commits Recent commits on the default branch
12
+ * meta Repo metadata (name, description, topics, language, visibility)
13
+ * readme README file content (decoded from base64)
14
+ * docs Markdown files in repo root and /docs directory
15
+ */
16
+
17
+ const GITHUB_API = 'https://api.github.com';
18
+
19
+ // Max chars to store per doc file — keeps observations scannable
20
+ const DOC_CONTENT_LIMIT = 8_000;
21
+
22
+ export class GitHubProvider {
23
+ #token;
24
+ #fetchFn;
25
+
26
+ constructor({ token, fetchFn = globalThis.fetch } = {}) {
27
+ if (!token) throw new Error('GitHubProvider requires a token');
28
+ this.#token = token;
29
+ this.#fetchFn = fetchFn;
30
+ }
31
+
32
+ /**
33
+ * Default sources used when no embed.yaml is present.
34
+ * Returns one source per enrichment group:
35
+ * - activity: prs + issues + commits (live work)
36
+ * - context: meta + readme + docs (repo understanding)
37
+ */
38
+ defaultSources(env = {}) {
39
+ const raw = env.GITHUB_REPOS ?? env.GITHUB_REPO ?? '';
40
+ const repos = raw.split(',').map((r) => r.trim()).filter(Boolean);
41
+ if (!repos.length) return [];
42
+ return [
43
+ {
44
+ provider: 'github',
45
+ repos,
46
+ refs: ['prs', 'issues', 'commits'],
47
+ limit: 25,
48
+ },
49
+ {
50
+ provider: 'github',
51
+ repos,
52
+ refs: ['meta', 'readme', 'docs'],
53
+ limit: 25,
54
+ },
55
+ ];
56
+ }
57
+
58
+ /**
59
+ * @param {string} ref - 'prs' | 'issues' | 'commits' | 'meta' | 'readme' | 'docs'
60
+ * @param {object} opts - source config (repo, repos, branch, limit)
61
+ * @returns {Promise<Item[]>}
62
+ */
63
+ async read(ref, opts = {}) {
64
+ const repos = resolveRepos(opts);
65
+ if (!repos.length) {
66
+ throw new Error(`GitHub source requires "repo" or "repos" field`);
67
+ }
68
+
69
+ const limit = Number(opts.limit ?? 25);
70
+ const results = [];
71
+
72
+ for (const repo of repos) {
73
+ try {
74
+ const items = await this.#readRepo(ref, repo, { branch: opts.branch, limit });
75
+ results.push(...items);
76
+ } catch (err) {
77
+ // Surface per-repo errors as items so the snapshot shows them
78
+ results.push({
79
+ type: 'error',
80
+ source: 'github',
81
+ repo,
82
+ ref,
83
+ message: err.message,
84
+ });
85
+ }
86
+ }
87
+
88
+ return results;
89
+ }
90
+
91
+ async #readRepo(ref, repo, { branch, limit }) {
92
+ switch (ref) {
93
+ case 'prs': return this.#listPRs(repo, limit);
94
+ case 'issues': return this.#listIssues(repo, limit);
95
+ case 'commits': return this.#listCommits(repo, { branch, limit });
96
+ case 'meta': return this.#fetchMeta(repo);
97
+ case 'readme': return this.#fetchReadme(repo);
98
+ case 'docs': return this.#fetchDocs(repo);
99
+ default: throw new Error(`GitHub provider: unknown ref "${ref}"`);
100
+ }
101
+ }
102
+
103
+ async #listPRs(repo, limit) {
104
+ const data = await this.#get(`/repos/${repo}/pulls?state=open&per_page=${limit}&sort=updated&direction=desc`);
105
+ return data.map((pr) => ({
106
+ type: 'pr',
107
+ source: 'github',
108
+ repo,
109
+ number: pr.number,
110
+ title: pr.title,
111
+ author: pr.user?.login,
112
+ state: pr.state,
113
+ draft: pr.draft ?? false,
114
+ url: pr.html_url,
115
+ createdAt: pr.created_at,
116
+ updatedAt: pr.updated_at,
117
+ labels: (pr.labels ?? []).map((l) => l.name),
118
+ reviewers: (pr.requested_reviewers ?? []).map((r) => r.login),
119
+ summary: `PR #${pr.number}: ${pr.title}`,
120
+ }));
121
+ }
122
+
123
+ async #listIssues(repo, limit) {
124
+ // GitHub issues endpoint returns PRs too — filter them out
125
+ const data = await this.#get(`/repos/${repo}/issues?state=open&per_page=${limit}&sort=updated&direction=desc`);
126
+ return data
127
+ .filter((issue) => !issue.pull_request)
128
+ .map((issue) => ({
129
+ type: 'issue',
130
+ source: 'github',
131
+ repo,
132
+ number: issue.number,
133
+ title: issue.title,
134
+ author: issue.user?.login,
135
+ state: issue.state,
136
+ url: issue.html_url,
137
+ createdAt: issue.created_at,
138
+ updatedAt: issue.updated_at,
139
+ labels: (issue.labels ?? []).map((l) => l.name),
140
+ summary: `Issue #${issue.number}: ${issue.title}`,
141
+ }));
142
+ }
143
+
144
+ async #listCommits(repo, { branch, limit }) {
145
+ const branchParam = branch ? `&sha=${encodeURIComponent(branch)}` : '';
146
+ const data = await this.#get(`/repos/${repo}/commits?per_page=${limit}${branchParam}`);
147
+ return data.map((c) => ({
148
+ type: 'commit',
149
+ source: 'github',
150
+ repo,
151
+ hash: c.sha,
152
+ subject: c.commit?.message?.split('\n')[0] ?? '',
153
+ author: c.commit?.author?.name ?? c.author?.login ?? 'unknown',
154
+ date: c.commit?.author?.date,
155
+ url: c.html_url,
156
+ summary: `${c.sha.slice(0, 7)} ${c.commit?.message?.split('\n')[0] ?? ''}`,
157
+ }));
158
+ }
159
+
160
+ async #fetchMeta(repo) {
161
+ const data = await this.#get(`/repos/${repo}`);
162
+ return [{
163
+ type: 'meta',
164
+ source: 'github',
165
+ repo,
166
+ name: data.name,
167
+ fullName: data.full_name,
168
+ description: data.description ?? '',
169
+ topics: data.topics ?? [],
170
+ language: data.language ?? null,
171
+ visibility: data.visibility ?? (data.private ? 'private' : 'public'),
172
+ defaultBranch: data.default_branch ?? 'main',
173
+ url: data.html_url,
174
+ pushedAt: data.pushed_at,
175
+ summary: `Repo ${data.full_name}: ${data.description ?? 'no description'}`,
176
+ }];
177
+ }
178
+
179
+ async #fetchReadme(repo) {
180
+ let data;
181
+ try {
182
+ data = await this.#get(`/repos/${repo}/readme`);
183
+ } catch (err) {
184
+ // 404 = no README — not an error worth surfacing
185
+ if (err.message.includes('404')) return [];
186
+ throw err;
187
+ }
188
+ const content = decodeBase64(data.content ?? '').slice(0, DOC_CONTENT_LIMIT);
189
+ return [{
190
+ type: 'doc',
191
+ source: 'github',
192
+ repo,
193
+ path: data.path ?? 'README.md',
194
+ url: data.html_url ?? `https://github.com/${repo}/blob/HEAD/${data.path ?? 'README.md'}`,
195
+ content,
196
+ summary: `README for ${repo}`,
197
+ }];
198
+ }
199
+
200
+ async #fetchDocs(repo) {
201
+ const results = [];
202
+
203
+ // Collect .md files from repo root and /docs, in parallel
204
+ const [rootTree, docsTree] = await Promise.all([
205
+ this.#getTree(repo, ''),
206
+ this.#getTree(repo, 'docs'),
207
+ ]);
208
+
209
+ const mdFiles = [
210
+ ...rootTree.filter((f) => f.type === 'file' && /\.md$/i.test(f.path) && !isReadme(f.path)),
211
+ ...docsTree.filter((f) => f.type === 'file' && /\.md$/i.test(f.path)),
212
+ ];
213
+
214
+ // Fetch files concurrently (cap at 10 to stay well under rate limits)
215
+ const batch = mdFiles.slice(0, 10);
216
+ const fetched = await Promise.allSettled(
217
+ batch.map((f) => this.#fetchFile(repo, f.path))
218
+ );
219
+
220
+ for (let i = 0; i < fetched.length; i++) {
221
+ const r = fetched[i];
222
+ if (r.status === 'fulfilled' && r.value) results.push(r.value);
223
+ }
224
+
225
+ return results;
226
+ }
227
+
228
+ async #getTree(repo, dir) {
229
+ try {
230
+ const path = dir ? `/repos/${repo}/contents/${dir}` : `/repos/${repo}/contents`;
231
+ const data = await this.#get(path);
232
+ return Array.isArray(data) ? data : [];
233
+ } catch (err) {
234
+ process.stderr.write('[providers/github.mjs] getTree: ' + (err?.message ?? String(err)) + '\n');
235
+ return [];
236
+ }
237
+ }
238
+
239
+ async #fetchFile(repo, path) {
240
+ try {
241
+ const data = await this.#get(`/repos/${repo}/contents/${path}`);
242
+ if (data.encoding !== 'base64' || !data.content) return null;
243
+ const content = decodeBase64(data.content).slice(0, DOC_CONTENT_LIMIT);
244
+ return {
245
+ type: 'doc',
246
+ source: 'github',
247
+ repo,
248
+ path,
249
+ url: data.html_url ?? `https://github.com/${repo}/blob/HEAD/${path}`,
250
+ content,
251
+ summary: `Doc ${path} in ${repo}`,
252
+ };
253
+ } catch (err) {
254
+ process.stderr.write('[providers/github.mjs] fetchFile: ' + (err?.message ?? String(err)) + '\n');
255
+ return null;
256
+ }
257
+ }
258
+
259
+ async #get(path) {
260
+ const controller = new AbortController();
261
+ const timer = setTimeout(() => controller.abort(), 10_000);
262
+ try {
263
+ const res = await this.#fetchFn(`${GITHUB_API}${path}`, {
264
+ headers: {
265
+ Authorization: `Bearer ${this.#token}`,
266
+ Accept: 'application/vnd.github+json',
267
+ 'X-GitHub-Api-Version': '2022-11-28',
268
+ },
269
+ signal: controller.signal,
270
+ });
271
+ if (!res.ok) {
272
+ const body = await res.text().catch(() => '');
273
+ throw new Error(`GitHub API ${res.status}: ${body.slice(0, 200)}`);
274
+ }
275
+ return res.json();
276
+ } finally {
277
+ clearTimeout(timer);
278
+ }
279
+ }
280
+ }
281
+
282
+ function resolveRepos(opts) {
283
+ if (Array.isArray(opts.repos)) return opts.repos.filter(Boolean);
284
+ if (opts.repo) return [opts.repo];
285
+ return [];
286
+ }
287
+
288
+ function decodeBase64(str) {
289
+ // GitHub base64 includes newlines — strip them first
290
+ return Buffer.from(str.replace(/\s/g, ''), 'base64').toString('utf8');
291
+ }
292
+
293
+ function isReadme(path) {
294
+ return /^readme(\.\w+)?$/i.test(path);
295
+ }