@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,360 @@
1
+ /**
2
+ * lib/runtime-pressure.mjs — Manage local memory-pressure guard defaults.
3
+ *
4
+ * Defines configurable thresholds for helper cleanup, emits environment
5
+ * values consumed by setup flows, and runs local diagnostics that back the
6
+ * guard verification tests.
7
+ */
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import { spawnSync } from 'node:child_process';
12
+
13
+ const DEFAULTS = Object.freeze({
14
+ enabled: '1',
15
+ intervalSeconds: '300',
16
+ swapGb: '6',
17
+ maxOpencode: '6',
18
+ maxOpencodeAgeHours: '24',
19
+ opencodeWarnGraceMinutes: '15',
20
+ maxHelperAgeHours: '2',
21
+ maxCassIndexAgeHours: '8',
22
+ });
23
+
24
+ function parsePositiveInt(value, fallback) {
25
+ const parsed = Number.parseInt(String(value ?? ''), 10);
26
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
27
+ }
28
+
29
+ function thresholdConfig(env = process.env) {
30
+ return {
31
+ enabled: String(env.CONSTRUCT_PRESSURE_GUARD_ENABLED ?? DEFAULTS.enabled) !== '0',
32
+ intervalSeconds: parsePositiveInt(env.CONSTRUCT_PRESSURE_GUARD_INTERVAL_SECONDS, Number(DEFAULTS.intervalSeconds)),
33
+ swapGb: parsePositiveInt(env.CONSTRUCT_PRESSURE_GUARD_SWAP_GB, Number(DEFAULTS.swapGb)),
34
+ maxOpencodeProcesses: parsePositiveInt(env.CONSTRUCT_PRESSURE_GUARD_MAX_OPENCODE, Number(DEFAULTS.maxOpencode)),
35
+ maxOpencodeAgeHours: parsePositiveInt(env.CONSTRUCT_PRESSURE_GUARD_MAX_OPENCODE_AGE_HOURS, Number(DEFAULTS.maxOpencodeAgeHours)),
36
+ opencodeWarnGraceMinutes: parsePositiveInt(
37
+ env.CONSTRUCT_PRESSURE_GUARD_OPENCODE_WARN_GRACE_MINUTES,
38
+ Number(DEFAULTS.opencodeWarnGraceMinutes),
39
+ ),
40
+ maxHelperAgeHours: parsePositiveInt(env.CONSTRUCT_PRESSURE_GUARD_MAX_HELPER_AGE_HOURS, Number(DEFAULTS.maxHelperAgeHours)),
41
+ maxCassIndexAgeHours: parsePositiveInt(env.CONSTRUCT_PRESSURE_GUARD_MAX_CASS_INDEX_AGE_HOURS, Number(DEFAULTS.maxCassIndexAgeHours)),
42
+ };
43
+ }
44
+
45
+ export function buildPressureGuardValues({ env = process.env } = {}) {
46
+ const config = thresholdConfig(env);
47
+ return {
48
+ CONSTRUCT_PRESSURE_GUARD_ENABLED: config.enabled ? '1' : '0',
49
+ CONSTRUCT_PRESSURE_GUARD_INTERVAL_SECONDS: String(config.intervalSeconds),
50
+ CONSTRUCT_PRESSURE_GUARD_SWAP_GB: String(config.swapGb),
51
+ CONSTRUCT_PRESSURE_GUARD_MAX_OPENCODE: String(config.maxOpencodeProcesses),
52
+ CONSTRUCT_PRESSURE_GUARD_MAX_OPENCODE_AGE_HOURS: String(config.maxOpencodeAgeHours),
53
+ CONSTRUCT_PRESSURE_GUARD_OPENCODE_WARN_GRACE_MINUTES: String(config.opencodeWarnGraceMinutes),
54
+ CONSTRUCT_PRESSURE_GUARD_MAX_HELPER_AGE_HOURS: String(config.maxHelperAgeHours),
55
+ CONSTRUCT_PRESSURE_GUARD_MAX_CASS_INDEX_AGE_HOURS: String(config.maxCassIndexAgeHours),
56
+ };
57
+ }
58
+
59
+ export function parseElapsedSeconds(input) {
60
+ const raw = String(input ?? '').trim();
61
+ if (!raw) return 0;
62
+ if (/^\d+$/.test(raw)) return Number(raw);
63
+ const daySplit = raw.split('-');
64
+ const clock = daySplit.pop();
65
+ const days = daySplit.length ? Number.parseInt(daySplit[0], 10) || 0 : 0;
66
+ const parts = clock.split(':').map((part) => Number.parseInt(part, 10) || 0);
67
+ if (parts.length === 2) return (days * 86400) + (parts[0] * 60) + parts[1];
68
+ if (parts.length === 3) return (days * 86400) + (parts[0] * 3600) + (parts[1] * 60) + parts[2];
69
+ return 0;
70
+ }
71
+
72
+ export function parseSwapUsage(output) {
73
+ const text = String(output ?? '');
74
+ const total = text.match(/total = ([0-9.]+)M/);
75
+ const used = text.match(/used = ([0-9.]+)M/);
76
+ const free = text.match(/free = ([0-9.]+)M/);
77
+ if (!total || !used || !free) return null;
78
+ const toBytes = (value) => Math.round(Number.parseFloat(value) * 1024 * 1024);
79
+ return {
80
+ totalBytes: toBytes(total[1]),
81
+ usedBytes: toBytes(used[1]),
82
+ freeBytes: toBytes(free[1]),
83
+ };
84
+ }
85
+
86
+ function isOpencodeCommand(command) {
87
+ return /^opencode(?:\s|$)/.test(command);
88
+ }
89
+
90
+ function isHelperCommand(command) {
91
+ return /(context7-mcp|playwright-mcp|mcp-server-sequential-thinking)/.test(command);
92
+ }
93
+
94
+ function isCassIndexCommand(command) {
95
+ return /\bcass index\b/.test(command);
96
+ }
97
+
98
+ function summarizeTermination(entries) {
99
+ if (!entries.length) return 'No stale dev-agent processes matched the current policy.';
100
+ return `Pressure guard selected ${entries.length} processes for cleanup.`;
101
+ }
102
+
103
+ function warningStatePath(env = process.env) {
104
+ const homeDir = env.HOME || os.homedir();
105
+ return path.join(homeDir, '.construct', 'runtime', 'process-pressure-warnings.json');
106
+ }
107
+
108
+ function readWarningState(env = process.env) {
109
+ const statePath = warningStatePath(env);
110
+ try {
111
+ const raw = fs.readFileSync(statePath, 'utf8');
112
+ const parsed = JSON.parse(raw);
113
+ return parsed && typeof parsed === 'object' ? parsed : {};
114
+ } catch {
115
+ return {};
116
+ }
117
+ }
118
+
119
+ function writeWarningState(entries, env = process.env) {
120
+ const statePath = warningStatePath(env);
121
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
122
+ fs.writeFileSync(statePath, `${JSON.stringify(entries, null, 2)}\n`);
123
+ }
124
+
125
+ function warningKey(entry) {
126
+ return `${entry.reason}:${entry.pid}`;
127
+ }
128
+
129
+ export function buildCleanupPlan(processEntries = [], thresholds = {}) {
130
+ const maxOpencodeProcesses = thresholds.maxOpencodeProcesses ?? 2;
131
+ const maxOpencodeAgeSeconds = (thresholds.maxOpencodeAgeHours ?? 24) * 3600;
132
+ const maxHelperAgeSeconds = (thresholds.maxHelperAgeHours ?? 2) * 3600;
133
+ const maxCassIndexAgeSeconds = (thresholds.maxCassIndexAgeHours ?? 8) * 3600;
134
+
135
+ const terminate = [];
136
+ const seen = new Set();
137
+
138
+ const opencode = processEntries.filter((entry) => isOpencodeCommand(entry.command));
139
+ const staleOpencode = opencode
140
+ .filter((entry) => entry.elapsedSeconds >= maxOpencodeAgeSeconds)
141
+ .sort((a, b) => b.elapsedSeconds - a.elapsedSeconds);
142
+ for (const entry of staleOpencode) {
143
+ if (!seen.has(entry.pid)) {
144
+ seen.add(entry.pid);
145
+ terminate.push({ ...entry, reason: 'stale-opencode', warnFirst: true });
146
+ }
147
+ }
148
+ const duplicateOpencode = opencode
149
+ .slice()
150
+ .sort((a, b) => b.elapsedSeconds - a.elapsedSeconds)
151
+ .slice(Math.max(0, maxOpencodeProcesses));
152
+ for (const entry of duplicateOpencode) {
153
+ if (!seen.has(entry.pid)) {
154
+ seen.add(entry.pid);
155
+ terminate.push({ ...entry, reason: 'duplicate-opencode', warnFirst: true });
156
+ }
157
+ }
158
+
159
+ const helpers = processEntries
160
+ .filter((entry) => isHelperCommand(entry.command) && entry.elapsedSeconds >= maxHelperAgeSeconds)
161
+ .sort((a, b) => b.elapsedSeconds - a.elapsedSeconds);
162
+ for (const entry of helpers) {
163
+ if (!seen.has(entry.pid)) {
164
+ seen.add(entry.pid);
165
+ terminate.push({ ...entry, reason: 'stale-helper' });
166
+ }
167
+ }
168
+
169
+ const cassCandidates = processEntries
170
+ .filter((entry) => isCassIndexCommand(entry.command) && entry.elapsedSeconds >= maxCassIndexAgeSeconds)
171
+ .sort((a, b) => b.elapsedSeconds - a.elapsedSeconds)
172
+ .map((entry) => ({ ...entry, reason: 'stale-cass-index' }));
173
+
174
+ return {
175
+ terminate,
176
+ cassCandidates,
177
+ summary: summarizeTermination(terminate),
178
+ };
179
+ }
180
+
181
+ function parsePsLine(line) {
182
+ const match = line.match(/^\s*(\d+)\s+(\d+)\s+([0-9:-]+)\s+(.*)$/);
183
+ if (!match) return null;
184
+ return {
185
+ pid: Number(match[1]),
186
+ rssKb: Number(match[2]),
187
+ elapsedSeconds: parseElapsedSeconds(match[3]),
188
+ command: match[4],
189
+ };
190
+ }
191
+
192
+ export function listPressureProcesses({ spawnSyncFn = spawnSync } = {}) {
193
+ const result = spawnSyncFn('ps', ['-ax', '-o', 'pid=,rss=,etime=,command='], {
194
+ encoding: 'utf8',
195
+ stdio: ['ignore', 'pipe', 'ignore'],
196
+ });
197
+ if (result.status !== 0) return [];
198
+ return String(result.stdout || '')
199
+ .split(/\r?\n/)
200
+ .map(parsePsLine)
201
+ .filter(Boolean)
202
+ .filter((entry) => isOpencodeCommand(entry.command) || isHelperCommand(entry.command) || isCassIndexCommand(entry.command));
203
+ }
204
+
205
+ export function readSwapUsage({ spawnSyncFn = spawnSync } = {}) {
206
+ const result = spawnSyncFn('sysctl', ['vm.swapusage'], {
207
+ encoding: 'utf8',
208
+ stdio: ['ignore', 'pipe', 'ignore'],
209
+ });
210
+ if (result.status !== 0) return null;
211
+ return parseSwapUsage(result.stdout);
212
+ }
213
+
214
+ function thresholdBytes(gb) {
215
+ return gb * 1024 * 1024 * 1024;
216
+ }
217
+
218
+ export function runPressureRelease({
219
+ env = process.env,
220
+ processEntries,
221
+ swapUsage,
222
+ spawnSyncFn = spawnSync,
223
+ killFn = process.kill.bind(process),
224
+ force = false,
225
+ } = {}) {
226
+ const config = thresholdConfig(env);
227
+ const entries = processEntries ?? listPressureProcesses({ spawnSyncFn });
228
+ const swap = swapUsage ?? readSwapUsage({ spawnSyncFn });
229
+ const plan = buildCleanupPlan(entries, config);
230
+ const pressureTriggered = force || Boolean(swap && swap.usedBytes >= thresholdBytes(config.swapGb));
231
+ const warningState = readWarningState(env);
232
+ const warningGraceSeconds = config.opencodeWarnGraceMinutes * 60;
233
+ const targets = [];
234
+ const warnings = [];
235
+ const nextWarningState = {};
236
+
237
+ for (const entry of plan.terminate) {
238
+ if (!entry.warnFirst) {
239
+ targets.push(entry);
240
+ continue;
241
+ }
242
+
243
+ const key = warningKey(entry);
244
+ const previous = warningState[key];
245
+ const warnedAt = typeof previous?.warnedAt === 'number' ? previous.warnedAt : null;
246
+ const now = Date.now();
247
+
248
+ if (warnedAt && ((now - warnedAt) / 1000) >= warningGraceSeconds) {
249
+ targets.push(entry);
250
+ continue;
251
+ }
252
+
253
+ const record = {
254
+ pid: entry.pid,
255
+ command: entry.command,
256
+ reason: entry.reason,
257
+ warnedAt: warnedAt ?? now,
258
+ };
259
+ nextWarningState[key] = record;
260
+ warnings.push({
261
+ ...entry,
262
+ warningIssued: !warnedAt,
263
+ graceMinutes: config.opencodeWarnGraceMinutes,
264
+ });
265
+ }
266
+
267
+ if (pressureTriggered) targets.push(...plan.cassCandidates);
268
+
269
+ const killed = [];
270
+ for (const entry of targets) {
271
+ try {
272
+ killFn(entry.pid, 'SIGTERM');
273
+ killed.push({ pid: entry.pid, reason: entry.reason });
274
+ } catch {
275
+ // Process may already be gone or we lack permission — pressure relief
276
+ // continues regardless; the loop is opportunistic per-pid, not transactional.
277
+ }
278
+ }
279
+
280
+ for (const entry of targets) {
281
+ if (!entry.warnFirst) continue;
282
+ delete nextWarningState[warningKey(entry)];
283
+ }
284
+ writeWarningState(nextWarningState, env);
285
+
286
+ return {
287
+ pressureTriggered,
288
+ swapUsage: swap,
289
+ config,
290
+ plan,
291
+ warnings,
292
+ killed,
293
+ };
294
+ }
295
+
296
+ function plistEscape(value) {
297
+ return String(value)
298
+ .replaceAll('&', '&')
299
+ .replaceAll('<', '&lt;')
300
+ .replaceAll('>', '&gt;');
301
+ }
302
+
303
+ export function installPressureGuardLaunchAgent({
304
+ homeDir = os.homedir(),
305
+ rootDir,
306
+ intervalSeconds = Number(DEFAULTS.intervalSeconds),
307
+ nodePath = process.execPath,
308
+ } = {}) {
309
+ const launchAgentsDir = path.join(homeDir, 'Library', 'LaunchAgents');
310
+ fs.mkdirSync(launchAgentsDir, { recursive: true });
311
+ const plistPath = path.join(launchAgentsDir, 'dev.construct.pressure-release.plist');
312
+ const programArgs = [
313
+ nodePath,
314
+ path.join(rootDir, 'bin', 'construct'),
315
+ 'cleanup',
316
+ '--pressure-release',
317
+ ];
318
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
319
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
320
+ <plist version="1.0">
321
+ <dict>
322
+ <key>Label</key>
323
+ <string>dev.construct.pressure-release</string>
324
+ <key>ProgramArguments</key>
325
+ <array>
326
+ ${programArgs.map((arg) => ` <string>${plistEscape(arg)}</string>`).join('\n')}
327
+ </array>
328
+ <key>RunAtLoad</key>
329
+ <true/>
330
+ <key>StartInterval</key>
331
+ <integer>${intervalSeconds}</integer>
332
+ <key>StandardOutPath</key>
333
+ <string>${plistEscape(path.join(homeDir, '.construct', 'runtime', 'pressure-release.log'))}</string>
334
+ <key>StandardErrorPath</key>
335
+ <string>${plistEscape(path.join(homeDir, '.construct', 'runtime', 'pressure-release.log'))}</string>
336
+ </dict>
337
+ </plist>
338
+ `;
339
+ fs.writeFileSync(plistPath, plist, 'utf8');
340
+ return { installed: true, plistPath };
341
+ }
342
+
343
+ export function loadPressureGuardLaunchAgent({
344
+ plistPath,
345
+ spawnSyncFn = spawnSync,
346
+ } = {}) {
347
+ if (!plistPath || !fs.existsSync(plistPath)) return { loaded: false, reason: 'missing-plist' };
348
+ spawnSyncFn('launchctl', ['unload', plistPath], {
349
+ stdio: ['ignore', 'ignore', 'ignore'],
350
+ });
351
+ const result = spawnSyncFn('launchctl', ['load', '-w', plistPath], {
352
+ encoding: 'utf8',
353
+ stdio: ['ignore', 'pipe', 'pipe'],
354
+ });
355
+ if (result.status === 0) return { loaded: true };
356
+ return {
357
+ loaded: false,
358
+ reason: (result.stderr || result.stdout || 'launchctl load failed').trim().split('\n')[0],
359
+ };
360
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * lib/schema-artifact.mjs — persist and read inferred schema results.
3
+ *
4
+ * Writes SchemaInferenceResult and UnifiedSchemaResult objects as JSON artifacts
5
+ * under .cx/knowledge/reference/schemas/. File names are derived from the source document
6
+ * basename so results are easy to find and diff.
7
+ *
8
+ * Schema artifacts are intentionally separate from ingested markdown so they
9
+ * can be consumed programmatically without parsing markdown.
10
+ */
11
+
12
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
13
+ import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve } from 'node:path';
14
+
15
+ const DEFAULT_SCHEMA_DIR = '.cx/knowledge/reference/schemas';
16
+
17
+ function slugify(value) {
18
+ return String(value)
19
+ .normalize('NFKD')
20
+ .replace(/[^\w.-]+/g, '-')
21
+ .replace(/-+/g, '-')
22
+ .replace(/^-|-$/g, '')
23
+ .toLowerCase();
24
+ }
25
+
26
+ function ensureDir(dirPath) {
27
+ mkdirSync(dirPath, { recursive: true });
28
+ }
29
+
30
+ function nextAvailablePath(targetPath) {
31
+ if (!existsSync(targetPath)) return targetPath;
32
+ const parsed = parse(targetPath);
33
+ let index = 2;
34
+ while (true) {
35
+ const candidate = join(parsed.dir, `${parsed.name}-${index}${parsed.ext}`);
36
+ if (!existsSync(candidate)) return candidate;
37
+ index += 1;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Write a schema inference result to disk.
43
+ *
44
+ * @param {object} schema SchemaInferenceResult or UnifiedSchemaResult
45
+ * @param {object} opts
46
+ * @param {string} [opts.cwd] Project root for resolving default output dir.
47
+ * @param {string} [opts.outputPath] Explicit output path (overrides outputDir).
48
+ * @param {string} [opts.outputDir] Directory to write into (default: .cx/knowledge/reference/schemas).
49
+ * @param {string} [opts.label] Optional label that derives the filename.
50
+ * @returns {{ outputPath: string, relativePath: string }}
51
+ */
52
+ export function writeSchemaArtifact(schema, { cwd = process.cwd(), outputPath = null, outputDir = null, label = null } = {}) {
53
+ const resolvedCwd = resolve(cwd);
54
+ const resolvedDir = outputDir
55
+ ? (isAbsolute(outputDir) ? outputDir : resolve(resolvedCwd, outputDir))
56
+ : resolve(resolvedCwd, DEFAULT_SCHEMA_DIR);
57
+
58
+ let targetPath;
59
+ if (outputPath) {
60
+ targetPath = isAbsolute(outputPath) ? outputPath : resolve(resolvedCwd, outputPath);
61
+ } else {
62
+ // Derive filename from source_path (single) or label (unified).
63
+ const nameBase = label
64
+ ? slugify(label)
65
+ : schema.source_path
66
+ ? slugify(basename(schema.source_path))
67
+ : 'schema';
68
+ targetPath = join(resolvedDir, `${nameBase}.schema.json`);
69
+ }
70
+
71
+ ensureDir(dirname(targetPath));
72
+ const finalPath = nextAvailablePath(targetPath);
73
+
74
+ writeFileSync(finalPath, JSON.stringify(schema, null, 2) + '\n', 'utf8');
75
+
76
+ return {
77
+ outputPath: finalPath,
78
+ relativePath: relative(resolvedCwd, finalPath),
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Read a schema artifact from disk.
84
+ *
85
+ * @param {string} schemaPath Absolute or relative path to a .schema.json file.
86
+ * @param {string} [cwd]
87
+ * @returns {object}
88
+ */
89
+ export function readSchemaArtifact(schemaPath, cwd = process.cwd()) {
90
+ const resolved = isAbsolute(schemaPath) ? schemaPath : resolve(cwd, schemaPath);
91
+ if (!existsSync(resolved)) throw new Error(`Schema artifact not found: ${resolved}`);
92
+ return JSON.parse(readFileSync(resolved, 'utf8'));
93
+ }
94
+
95
+ /**
96
+ * List all schema artifacts in a directory.
97
+ *
98
+ * @param {object} opts
99
+ * @param {string} [opts.cwd]
100
+ * @param {string} [opts.schemaDir]
101
+ * @returns {Array<{ path: string, relativePath: string, stat: object }>}
102
+ */
103
+ export function listSchemaArtifacts({ cwd = process.cwd(), schemaDir = null } = {}) {
104
+ const resolvedCwd = resolve(cwd);
105
+ const dir = schemaDir
106
+ ? (isAbsolute(schemaDir) ? schemaDir : resolve(resolvedCwd, schemaDir))
107
+ : resolve(resolvedCwd, DEFAULT_SCHEMA_DIR);
108
+
109
+ if (!existsSync(dir)) return [];
110
+
111
+ const entries = [];
112
+ const stack = [dir];
113
+ while (stack.length > 0) {
114
+ const current = stack.pop();
115
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
116
+ const full = join(current, entry.name);
117
+ if (entry.isDirectory()) {
118
+ stack.push(full);
119
+ } else if (entry.isFile() && entry.name.endsWith('.schema.json')) {
120
+ entries.push({
121
+ path: full,
122
+ relativePath: relative(resolvedCwd, full),
123
+ stat: statSync(full),
124
+ });
125
+ }
126
+ }
127
+ }
128
+ return entries.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
129
+ }
130
+
131
+ /**
132
+ * Default schema output dir relative to project root.
133
+ */
134
+ export const SCHEMA_DIR = DEFAULT_SCHEMA_DIR;