@geraldmaron/construct 1.0.0 → 1.0.1

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 +10 -7
  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 +1116 -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 +525 -38
  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,33 @@
1
+ /**
2
+ * lib/doctor/watchers/handoffs.mjs — periodic handoff cleanup tick.
3
+ *
4
+ * Runs `autoCleanHandoffs` against the project root on a 30-min schedule.
5
+ * Resolved handoffs older than `handoffsMaxDays` move to .cx/handoffs/archive/;
6
+ * archived handoffs older than 2× retention get deleted. Live handoffs are
7
+ * never touched — open work is sacred. Each tick records a one-line audit
8
+ * entry so the doctor log shows what was done and why.
9
+ */
10
+
11
+ import { record } from '../audit.mjs';
12
+ import { autoCleanHandoffs } from '../../handoffs/cleanup.mjs';
13
+
14
+ export const name = 'handoffs';
15
+ export const intervalMs = 30 * 60 * 1000;
16
+
17
+ export async function tick() {
18
+ try {
19
+ const { result } = autoCleanHandoffs(process.cwd(), process.env);
20
+ if (result.moved.length === 0 && result.deleted.length === 0 && (result.warnings || []).length === 0) {
21
+ return;
22
+ }
23
+ record({
24
+ watcher: name,
25
+ event: 'cleanup',
26
+ moved: result.moved.map((a) => ({ from: a.from, to: a.to, bytes: a.bytes, reason: a.reason })),
27
+ deleted: result.deleted.map((a) => ({ path: a.path, bytes: a.bytes, reason: a.reason })),
28
+ warnings: result.warnings || [],
29
+ });
30
+ } catch (err) {
31
+ record({ watcher: name, event: 'error', message: err?.message || 'unknown' });
32
+ }
33
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * lib/doctor/watchers/process-pressure.mjs — continuous process pressure guard.
3
+ *
4
+ * Runs the runtime-pressure cleanup on a recurring tick so stale Construct
5
+ * helpers don't accumulate between `construct up` invocations. Any killed
6
+ * process is recorded; if the same service-class has been killed >=2 times
7
+ * in the last hour, escalates so cx-sre can investigate the underlying churn.
8
+ *
9
+ * Tick: 60s.
10
+ */
11
+
12
+ import { record, recent } from '../audit.mjs';
13
+ import { escalate } from '../escalate.mjs';
14
+ import { runPressureRelease } from '../../runtime-pressure.mjs';
15
+
16
+ export const name = 'process-pressure';
17
+ export const intervalMs = 60 * 1000;
18
+
19
+ export async function tick() {
20
+ const actions = [];
21
+ const escalations = [];
22
+
23
+ const report = runPressureRelease({ env: process.env });
24
+
25
+ if (Array.isArray(report?.killed) && report.killed.length > 0) {
26
+ for (const proc of report.killed) {
27
+ record({
28
+ kind: 'action',
29
+ watcher: name,
30
+ action: 'kill',
31
+ target: proc.command || proc.pid || 'unknown',
32
+ summary: `killed stale process ${proc.command || ''} (pid ${proc.pid || '?'}, ${proc.reason || 'pressure rule'})`,
33
+ context: proc,
34
+ });
35
+ actions.push({ type: 'kill', target: proc.command || proc.pid });
36
+ }
37
+
38
+ const hourAgo = Date.now() - 60 * 60 * 1000;
39
+ const recentKills = recent({ watcher: name, kind: 'action', since: hourAgo, limit: 50 })
40
+ .filter((e) => e.action === 'kill');
41
+ const byClass = {};
42
+ for (const k of recentKills) {
43
+ const cls = String(k.target || '').split(/[\s/]/)[0] || 'unknown';
44
+ byClass[cls] = (byClass[cls] || 0) + 1;
45
+ }
46
+ for (const [cls, count] of Object.entries(byClass)) {
47
+ if (count >= 2) {
48
+ const r = await escalate({
49
+ watcher: name,
50
+ eventType: 'service.down',
51
+ summary: `${cls} killed ${count} times in last hour — repeated pressure churn`,
52
+ context: { class: cls, killCount: count, report },
53
+ });
54
+ escalations.push({ eventType: 'service.down', class: cls, result: r });
55
+ }
56
+ }
57
+ }
58
+
59
+ return { actions, escalations, notes: [{ pressureTriggered: !!report?.pressureTriggered, killed: report?.killed?.length || 0 }] };
60
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * lib/doctor/watchers/service-health.mjs — continuous service probes.
3
+ *
4
+ * Probes the services `construct up` starts (postgres, dashboard,
5
+ * cm, opencode). On 2 consecutive probe failures, attempts a restart of
6
+ * docker-backed services (postgres). On 2 consecutive failed
7
+ * restarts, escalates `service.down` for cx-sre. Non-docker services log +
8
+ * escalate after 3 consecutive failures — auto-restart needs original spawn
9
+ * args and is deferred to a follow-up.
10
+ *
11
+ * Tick: 60s.
12
+ */
13
+
14
+ import { spawnSync, spawn } from 'node:child_process';
15
+ import { existsSync, readFileSync, openSync, mkdirSync } from 'node:fs';
16
+ import { homedir } from 'node:os';
17
+ import { dirname, join } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ import { record } from '../audit.mjs';
21
+ import { escalate } from '../escalate.mjs';
22
+
23
+ export const name = 'service-health';
24
+ export const intervalMs = 60 * 1000;
25
+
26
+ const failureCounts = {};
27
+ const restartAttempts = {};
28
+
29
+ const __dirname = dirname(fileURLToPath(import.meta.url));
30
+ const REPO_ROOT = join(__dirname, '..', '..', '..');
31
+
32
+ const services = [
33
+ {
34
+ id: 'postgres',
35
+ probe: () => probeTcp('127.0.0.1', 54329),
36
+ restart: () => dockerRestart('construct-postgres'),
37
+ restartable: true,
38
+ },
39
+ {
40
+ id: 'dashboard',
41
+ probe: () => probeDashboardPort(),
42
+ restart: () => respawnViaConstructUp('dashboard'),
43
+ restartable: true,
44
+ },
45
+ {
46
+ id: 'cm',
47
+ probe: () => probeMemoryHealth(),
48
+ restart: () => respawnCm(),
49
+ restartable: true,
50
+ },
51
+ ];
52
+
53
+ function probeTcp(host, port, timeoutMs = 1500) {
54
+ const r = spawnSync('bash', ['-c', `(echo > /dev/tcp/${host}/${port}) 2>/dev/null`], { timeout: timeoutMs });
55
+ return r.status === 0;
56
+ }
57
+
58
+ function probeDashboardPort() {
59
+ const statePath = join(homedir(), '.construct', 'dashboard.json');
60
+ if (!existsSync(statePath)) return true;
61
+ try {
62
+ const state = JSON.parse(readFileSync(statePath, 'utf8'));
63
+ if (!state.port) return true;
64
+ return probeTcp('127.0.0.1', state.port);
65
+ } catch {
66
+ return true;
67
+ }
68
+ }
69
+
70
+ function probeMemoryHealth() {
71
+ const envPath = join(homedir(), '.construct', 'config.env');
72
+ if (!existsSync(envPath)) return true;
73
+ try {
74
+ const env = readFileSync(envPath, 'utf8');
75
+ const match = env.match(/MEMORY_PORT=(\d+)/);
76
+ if (!match) return true;
77
+ return probeTcp('127.0.0.1', parseInt(match[1], 10));
78
+ } catch {
79
+ return true;
80
+ }
81
+ }
82
+
83
+ function dockerRestart(container) {
84
+ const r = spawnSync('docker', ['restart', container], { encoding: 'utf8', timeout: 15000 });
85
+ return { ok: r.status === 0, stderr: r.stderr };
86
+ }
87
+
88
+ function detachSpawn(command, args, logName) {
89
+ try {
90
+ const runtimeDir = join(homedir(), '.construct', '.runtime');
91
+ if (!existsSync(runtimeDir)) mkdirSync(runtimeDir, { recursive: true });
92
+ const logPath = join(runtimeDir, logName);
93
+ const fd = openSync(logPath, 'a');
94
+ const child = spawn(command, args, { detached: true, stdio: ['ignore', fd, fd] });
95
+ child.unref();
96
+ return { ok: true, logPath };
97
+ } catch (err) {
98
+ return { ok: false, error: String(err) };
99
+ }
100
+ }
101
+
102
+ function respawnViaConstructUp() {
103
+ const bin = join(REPO_ROOT, 'bin', 'construct');
104
+ if (!existsSync(bin)) return { ok: false, error: 'bin/construct not found' };
105
+ return detachSpawn('node', [bin, 'up'], 'doctor-respawn-up.log');
106
+ }
107
+
108
+ function respawnCm() {
109
+ const envPath = join(homedir(), '.construct', 'config.env');
110
+ let port = '4231';
111
+ try {
112
+ if (existsSync(envPath)) {
113
+ const m = readFileSync(envPath, 'utf8').match(/MEMORY_PORT=(\d+)/);
114
+ if (m) port = m[1];
115
+ }
116
+ } catch { /* fall through */ }
117
+ return detachSpawn('cm', ['serve', '--port', port], 'cm-respawn.log');
118
+ }
119
+
120
+ export async function tick() {
121
+ const actions = [];
122
+ const escalations = [];
123
+ const samples = [];
124
+
125
+ for (const svc of services) {
126
+ let healthy = true;
127
+ try { healthy = await svc.probe(); } catch { healthy = false; }
128
+ samples.push({ id: svc.id, healthy });
129
+
130
+ if (healthy) {
131
+ if (failureCounts[svc.id]) {
132
+ record({
133
+ kind: 'recovery',
134
+ watcher: name,
135
+ target: svc.id,
136
+ summary: `${svc.id} recovered after ${failureCounts[svc.id]} consecutive probe failure(s)`,
137
+ context: { previousFailures: failureCounts[svc.id] },
138
+ });
139
+ }
140
+ failureCounts[svc.id] = 0;
141
+ restartAttempts[svc.id] = 0;
142
+ continue;
143
+ }
144
+
145
+ failureCounts[svc.id] = (failureCounts[svc.id] || 0) + 1;
146
+ record({
147
+ kind: 'sample',
148
+ watcher: name,
149
+ target: svc.id,
150
+ result: 'down',
151
+ summary: `${svc.id} probe failed (${failureCounts[svc.id]} consecutive)`,
152
+ context: { failureCount: failureCounts[svc.id] },
153
+ });
154
+
155
+ if (svc.restartable && failureCounts[svc.id] >= 2 && (restartAttempts[svc.id] || 0) < 2) {
156
+ const restart = svc.restart();
157
+ restartAttempts[svc.id] = (restartAttempts[svc.id] || 0) + 1;
158
+ record({
159
+ kind: 'action',
160
+ watcher: name,
161
+ action: 'restart',
162
+ target: svc.id,
163
+ result: restart.ok ? 'ok' : 'failed',
164
+ summary: `${svc.id} restart attempt ${restartAttempts[svc.id]} — ${restart.ok ? 'succeeded' : 'failed'}`,
165
+ context: restart,
166
+ });
167
+ actions.push({ type: 'restart', target: svc.id, ok: restart.ok });
168
+ if (restart.ok) failureCounts[svc.id] = 0;
169
+ }
170
+
171
+ const escalateThreshold = svc.restartable ? 2 : 3;
172
+ const restartFailures = restartAttempts[svc.id] || 0;
173
+ const shouldEscalate = svc.restartable
174
+ ? restartFailures >= 2 && failureCounts[svc.id] >= 2
175
+ : failureCounts[svc.id] >= escalateThreshold;
176
+ if (shouldEscalate) {
177
+ const r = await escalate({
178
+ watcher: name,
179
+ eventType: 'service.down',
180
+ summary: `${svc.id} down (${failureCounts[svc.id]} consecutive probes, ${restartFailures} restart attempt(s) failed)`,
181
+ context: { service: svc.id, failureCount: failureCounts[svc.id], restartAttempts: restartFailures },
182
+ });
183
+ escalations.push({ eventType: 'service.down', service: svc.id, result: r });
184
+ }
185
+ }
186
+
187
+ return { actions, escalations, notes: samples };
188
+ }
@@ -0,0 +1,288 @@
1
+ /**
2
+ * lib/document-extract.mjs — shared local document extraction for retrieval and MCP reads.
3
+ *
4
+ * Handles plain-text formats directly, XML/HTML via lightweight tag stripping,
5
+ * Office zip containers via unzip, and PDFs via pdftotext or macOS-native fallbacks.
6
+ * Also provides extractDocumentMetadata() for structured metadata (title, authors,
7
+ * dates, links) from source document frontmatter and content.
8
+ */
9
+ import { existsSync, readFileSync, statSync } from 'node:fs';
10
+ import { basename, extname, resolve } from 'node:path';
11
+ import { spawnSync } from 'node:child_process';
12
+
13
+ export const UTF8_TEXT_EXTS = new Set([
14
+ '.md', '.txt', '.rst', '.adoc', '.json', '.yaml', '.yml', '.toml',
15
+ '.js', '.mjs', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs', '.sh', '.bash',
16
+ '.html', '.css', '.csv', '.tsv', '.xml', '.env', '.env.example', '.conf', '.ini', '.tf', '.hcl',
17
+ '.sql', '.log',
18
+ '.vtt', '.srt', '.lrc', '.transcript',
19
+ ]);
20
+
21
+ export const ZIP_DOCUMENT_EXTS = new Set(['.docx', '.xlsx', '.pptx', '.odt', '.ods']);
22
+ export const RICH_TEXT_EXTS = new Set(['.doc', '.rtf']);
23
+ export const MDLS_DOCUMENT_EXTS = new Set(['.xls', '.ppt', '.pages', '.numbers', '.key']);
24
+ export const EXTRACTABLE_DOCUMENT_EXTS = new Set([
25
+ ...UTF8_TEXT_EXTS,
26
+ ...ZIP_DOCUMENT_EXTS,
27
+ ...RICH_TEXT_EXTS,
28
+ ...MDLS_DOCUMENT_EXTS,
29
+ '.pdf',
30
+ ]);
31
+
32
+ function normalizeText(value) {
33
+ return String(value)
34
+ .replace(/\r\n/g, '\n')
35
+ .replace(/\u0000/g, '')
36
+ .replace(/[ \t]+\n/g, '\n')
37
+ .replace(/\n{3,}/g, '\n\n')
38
+ .trim();
39
+ }
40
+
41
+ function decodeXmlEntities(value) {
42
+ return value
43
+ .replace(/&lt;/g, '<')
44
+ .replace(/&gt;/g, '>')
45
+ .replace(/&amp;/g, '&')
46
+ .replace(/&quot;/g, '"')
47
+ .replace(/&apos;/g, "'")
48
+ .replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
49
+ .replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCharCode(parseInt(code, 16)));
50
+ }
51
+
52
+ function stripMarkup(value) {
53
+ return normalizeText(
54
+ decodeXmlEntities(String(value))
55
+ .replace(/<w:tab\/>/g, '\t')
56
+ .replace(/<w:br\/>/g, '\n')
57
+ .replace(/<\/(?:w:p|text:p|table:table-row|tr|p|div|h\d)>/g, '\n')
58
+ .replace(/<\/(?:w:tc|table:table-cell|td)>/g, '\t')
59
+ .replace(/<[^>]+>/g, ' ')
60
+ );
61
+ }
62
+
63
+ function run(command, args) {
64
+ const result = spawnSync(command, args, {
65
+ encoding: 'utf8',
66
+ stdio: ['ignore', 'pipe', 'pipe'],
67
+ });
68
+ if (result.status !== 0) {
69
+ throw new Error((result.stderr || result.stdout || `${command} failed`).trim());
70
+ }
71
+ return result.stdout;
72
+ }
73
+
74
+ function commandExists(command) {
75
+ const checker = process.platform === 'win32' ? 'where' : 'which';
76
+ return spawnSync(checker, [command], { stdio: 'ignore' }).status === 0;
77
+ }
78
+
79
+ function extractUtf8(filePath, extension) {
80
+ const raw = readFileSync(filePath, 'utf8');
81
+ const text = extension === '.html' || extension === '.xml'
82
+ ? stripMarkup(raw)
83
+ : normalizeText(raw);
84
+ return { text, method: 'utf8' };
85
+ }
86
+
87
+ function zipEntries(filePath) {
88
+ return run('unzip', ['-Z1', filePath]).split('\n').map((line) => line.trim()).filter(Boolean);
89
+ }
90
+
91
+ function unzipEntry(filePath, entryPath) {
92
+ return run('unzip', ['-p', filePath, entryPath]);
93
+ }
94
+
95
+ function extractZipDocument(filePath, extension) {
96
+ const entries = zipEntries(filePath);
97
+ let targets = [];
98
+
99
+ if (extension === '.docx') {
100
+ targets = entries.filter((entry) => entry === 'word/document.xml' || entry.startsWith('word/header') || entry.startsWith('word/footer'));
101
+ } else if (extension === '.xlsx') {
102
+ targets = entries.filter((entry) => entry === 'xl/sharedStrings.xml' || /^xl\/worksheets\/sheet\d+\.xml$/.test(entry));
103
+ } else if (extension === '.pptx') {
104
+ targets = entries.filter((entry) => /^ppt\/slides\/slide\d+\.xml$/.test(entry));
105
+ } else if (extension === '.odt' || extension === '.ods') {
106
+ targets = entries.filter((entry) => entry === 'content.xml');
107
+ }
108
+
109
+ if (targets.length === 0) throw new Error(`No readable document entries found for ${extension}`);
110
+
111
+ const text = normalizeText(targets.map((entry) => stripMarkup(unzipEntry(filePath, entry))).join('\n\n'));
112
+ return { text, method: 'zip-xml' };
113
+ }
114
+
115
+ function extractRichText(filePath) {
116
+ if (!commandExists('textutil')) {
117
+ throw new Error('textutil not available for rich-text extraction');
118
+ }
119
+ return {
120
+ text: normalizeText(run('textutil', ['-convert', 'txt', '-stdout', filePath])),
121
+ method: 'textutil',
122
+ };
123
+ }
124
+
125
+ function extractPdfWithPdftotext(filePath) {
126
+ return {
127
+ text: normalizeText(run('pdftotext', ['-layout', '-nopgbrk', filePath, '-'])),
128
+ method: 'pdftotext',
129
+ };
130
+ }
131
+
132
+ function extractPdfWithSwift(filePath) {
133
+ const script = `
134
+ import Foundation
135
+ import PDFKit
136
+
137
+ let path = CommandLine.arguments[1]
138
+ let url = URL(fileURLWithPath: path)
139
+ guard let document = PDFDocument(url: url) else {
140
+ fputs("Unable to open PDF\\n", stderr)
141
+ exit(1)
142
+ }
143
+ print(document.string ?? "")
144
+ `;
145
+ return {
146
+ text: normalizeText(run('swift', ['-e', script, filePath])),
147
+ method: 'swift-pdfkit',
148
+ };
149
+ }
150
+
151
+ function extractPdfWithMdls(filePath) {
152
+ const text = extractWithMdls(filePath);
153
+ return { text, method: 'mdls' };
154
+ }
155
+
156
+ function extractWithMdls(filePath) {
157
+ const text = normalizeText(run('mdls', ['-raw', '-name', 'kMDItemTextContent', filePath]));
158
+ if (!text || text === '(null)') throw new Error('Spotlight text extraction unavailable');
159
+ return text;
160
+ }
161
+
162
+ function extractPdf(filePath) {
163
+ if (commandExists('pdftotext')) return extractPdfWithPdftotext(filePath);
164
+ if (process.platform === 'darwin' && commandExists('swift')) {
165
+ try {
166
+ return extractPdfWithSwift(filePath);
167
+ } catch {
168
+ return extractPdfWithMdls(filePath);
169
+ }
170
+ }
171
+ if (process.platform === 'darwin' && commandExists('mdls')) return extractPdfWithMdls(filePath);
172
+ throw new Error('No PDF extraction backend available');
173
+ }
174
+
175
+ export function isExtractableDocumentPath(filePath) {
176
+ return EXTRACTABLE_DOCUMENT_EXTS.has(extname(filePath).toLowerCase());
177
+ }
178
+
179
+ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
180
+ const URL_RE = /https?:\/\/[^\s)>\]"'`]+/g;
181
+ const DATE_KEYS = ['date', 'created', 'created_at', 'updated', 'updated_at', 'published', 'last_modified'];
182
+ const AUTHOR_KEYS = ['author', 'authors', 'by', 'contributor', 'contributors'];
183
+
184
+ function parseFrontmatter(text) {
185
+ const match = text.match(FRONTMATTER_RE);
186
+ if (!match) return { fields: {}, body: text };
187
+ const fields = {};
188
+ for (const line of match[1].split('\n')) {
189
+ const idx = line.indexOf(':');
190
+ if (idx === -1) continue;
191
+ const key = line.slice(0, idx).trim().toLowerCase();
192
+ const val = line.slice(idx + 1).trim();
193
+ if (!key) continue;
194
+ if (val.startsWith('[') && val.endsWith(']')) {
195
+ fields[key] = val.slice(1, -1).split(',').map((s) => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
196
+ } else {
197
+ fields[key] = val.replace(/^["']|["']$/g, '');
198
+ }
199
+ }
200
+ return { fields, body: text.slice(match[0].length) };
201
+ }
202
+
203
+ function extractTitleFromBody(body, extension) {
204
+ if (['.md', '.mdx', '.rst', '.adoc'].includes(extension)) {
205
+ const h1 = body.match(/^#\s+(.+)/m);
206
+ if (h1) return h1[1].trim();
207
+ }
208
+ if (['.html', '.xml'].includes(extension)) {
209
+ const title = body.match(/<title[^>]*>([^<]+)<\/title>/i);
210
+ if (title) return normalizeText(title[1]);
211
+ }
212
+ return null;
213
+ }
214
+
215
+ export function extractDocumentMetadata(filePath) {
216
+ const resolvedPath = resolve(filePath);
217
+ if (!existsSync(resolvedPath)) return { error: 'file not found' };
218
+
219
+ const extension = extname(resolvedPath).toLowerCase();
220
+ const stat = statSync(resolvedPath);
221
+ const result = { filePath: resolvedPath, extension, title: null, frontmatter: {}, authors: [], dates: {}, links: [] };
222
+
223
+ if (!UTF8_TEXT_EXTS.has(extension)) {
224
+ result.title = basename(resolvedPath).replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ').trim();
225
+ result.dates.modified = new Date(stat.mtimeMs).toISOString();
226
+ result.dates.created = new Date(stat.birthtimeMs || stat.mtimeMs).toISOString();
227
+ return result;
228
+ }
229
+
230
+ const raw = readFileSync(resolvedPath, 'utf8');
231
+ const { fields, body } = parseFrontmatter(raw);
232
+ result.frontmatter = fields;
233
+
234
+ result.title = fields.title || extractTitleFromBody(body, extension) || basename(resolvedPath).replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ').trim();
235
+
236
+ for (const key of AUTHOR_KEYS) {
237
+ if (fields[key]) {
238
+ result.authors = Array.isArray(fields[key]) ? fields[key] : [fields[key]];
239
+ break;
240
+ }
241
+ }
242
+
243
+ for (const key of DATE_KEYS) {
244
+ if (fields[key]) result.dates[key] = fields[key];
245
+ }
246
+ if (!Object.keys(result.dates).length) {
247
+ result.dates.modified = new Date(stat.mtimeMs).toISOString();
248
+ result.dates.created = new Date(stat.birthtimeMs || stat.mtimeMs).toISOString();
249
+ }
250
+
251
+ const urls = body.match(URL_RE) || [];
252
+ result.links = [...new Set(urls)].slice(0, 100);
253
+
254
+ return result;
255
+ }
256
+
257
+ export function extractDocumentText(filePath, { maxChars = null } = {}) {
258
+ const resolvedPath = resolve(filePath);
259
+ if (!existsSync(resolvedPath)) throw new Error(`File not found: ${resolvedPath}`);
260
+
261
+ const extension = extname(resolvedPath).toLowerCase();
262
+ if (!EXTRACTABLE_DOCUMENT_EXTS.has(extension)) {
263
+ throw new Error(`Unsupported document type: ${extension || 'unknown'}`);
264
+ }
265
+
266
+ let extracted;
267
+ if (UTF8_TEXT_EXTS.has(extension)) extracted = extractUtf8(resolvedPath, extension);
268
+ else if (ZIP_DOCUMENT_EXTS.has(extension)) extracted = extractZipDocument(resolvedPath, extension);
269
+ else if (RICH_TEXT_EXTS.has(extension)) extracted = extractRichText(resolvedPath);
270
+ else if (MDLS_DOCUMENT_EXTS.has(extension) && process.platform === 'darwin' && commandExists('mdls')) extracted = { text: extractWithMdls(resolvedPath), method: 'mdls' };
271
+ else if (extension === '.pdf') extracted = extractPdf(resolvedPath);
272
+ else throw new Error(`Unsupported document type: ${extension || 'unknown'}`);
273
+
274
+ const text = extracted.text;
275
+ const limit = Number.isFinite(Number(maxChars)) && Number(maxChars) > 0
276
+ ? Math.min(Number(maxChars), 200_000)
277
+ : null;
278
+ const truncated = limit !== null && text.length > limit;
279
+
280
+ return {
281
+ filePath: resolvedPath,
282
+ extension,
283
+ extractionMethod: extracted.method,
284
+ text: truncated ? `${text.slice(0, limit)}\n` : text,
285
+ truncated,
286
+ characters: text.length,
287
+ };
288
+ }