@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,158 @@
1
+ /**
2
+ * lib/agent-contracts-enforce.mjs — runtime contract enforcement.
3
+ *
4
+ * Wraps `validatePacket` from `lib/agent-contracts.mjs` with three production
5
+ * concerns:
6
+ *
7
+ * 1. Block on violation. `enforcePacket` throws a `ContractViolationError`
8
+ * when the packet doesn't satisfy the contract direction. Callers can
9
+ * catch and degrade if they want advisory mode, but the default is hard
10
+ * enforcement so contract bugs surface loudly.
11
+ *
12
+ * 2. Tamper-evident violation log. Every violation appends a JSONL record
13
+ * to `~/.cx/contract-violations.jsonl` with a sha256 chain-hash over
14
+ * the prior line. Same pattern as the mutation audit trail. Operators
15
+ * can replay the chain to detect after-the-fact tampering.
16
+ *
17
+ * 3. Visibility into `construct doctor`. `recentViolations(windowMs)`
18
+ * returns the last N violations so doctor can surface
19
+ * "3 contract violations in the last 24h — see ~/.cx/contract-violations.jsonl".
20
+ *
21
+ * The enforcement is identity-aware. Each violation logs the active agent
22
+ * (via `~/.cx/last-agent.json` like audit-trail does) so operators can
23
+ * identify which producer or consumer is at fault.
24
+ */
25
+
26
+ import { existsSync, mkdirSync, statSync, readFileSync, appendFileSync } from 'node:fs';
27
+ import { join } from 'node:path';
28
+ import { homedir } from 'node:os';
29
+ import { createHash } from 'node:crypto';
30
+ import { getContractById, validatePacket } from './agent-contracts.mjs';
31
+ import { validatePostconditions } from './agents/postconditions.mjs';
32
+
33
+ const CX_DIR = join(homedir(), '.cx');
34
+ const LOG_FILE = join(CX_DIR, 'contract-violations.jsonl');
35
+ const LAST_AGENT = join(CX_DIR, 'last-agent.json');
36
+
37
+ export class ContractViolationError extends Error {
38
+ constructor({ contractId, direction, missing, verdict = 'CONTRACT_VIOLATION', postconditionFailures = [] }) {
39
+ const detail = postconditionFailures.length > 0
40
+ ? `postcondition(s) failed: ${postconditionFailures.map((f) => f.id).join(', ')}`
41
+ : `missing field(s): ${(missing || []).join(', ')}`;
42
+ super(`contract '${contractId}' (${direction}) violated — ${detail}`);
43
+ this.name = 'ContractViolationError';
44
+ this.contractId = contractId;
45
+ this.direction = direction;
46
+ this.missing = missing || [];
47
+ this.verdict = verdict;
48
+ this.postconditionFailures = postconditionFailures;
49
+ }
50
+ }
51
+
52
+ function sha256(input) { return createHash('sha256').update(input).digest('hex'); }
53
+
54
+ function readLastAgent() {
55
+ try { return JSON.parse(readFileSync(LAST_AGENT, 'utf8'))?.agent || 'construct'; }
56
+ catch { return 'construct'; }
57
+ }
58
+
59
+ function readPrevLineHash() {
60
+ try {
61
+ if (!existsSync(LOG_FILE)) return null;
62
+ const size = statSync(LOG_FILE).size;
63
+ if (size === 0) return null;
64
+ const tail = readFileSync(LOG_FILE, 'utf8').slice(Math.max(0, size - 2048));
65
+ const lines = tail.split('\n').filter(Boolean);
66
+ return lines.length === 0 ? null : sha256(lines[lines.length - 1]);
67
+ } catch { return null; }
68
+ }
69
+
70
+ function logViolation(contractId, direction, missing, packet, extra = {}) {
71
+ try {
72
+ mkdirSync(CX_DIR, { recursive: true });
73
+ const record = {
74
+ ts: new Date().toISOString(),
75
+ agent: readLastAgent(),
76
+ contractId,
77
+ direction,
78
+ missing,
79
+ packet_keys: packet && typeof packet === 'object' ? Object.keys(packet) : null,
80
+ prev_line_hash: readPrevLineHash(),
81
+ ...extra,
82
+ };
83
+ appendFileSync(LOG_FILE, JSON.stringify(record) + '\n', 'utf8');
84
+ } catch { /* logging is best-effort */ }
85
+ }
86
+
87
+ /**
88
+ * Validate `packet` against `contractId` in the given `direction`. Throws
89
+ * a `ContractViolationError` on failure (and logs the violation). Returns
90
+ * the validation result on success so callers can inspect the resolved
91
+ * contract metadata.
92
+ *
93
+ * For `direction === 'output'`, also evaluates the producer's binary
94
+ * postconditions from `lib/agents/postconditions.mjs`. A failed postcondition
95
+ * raises with `verdict: 'BLOCKED_CONTRACT'` so the dispatcher can branch on
96
+ * the typed verdict rather than parsing the error message.
97
+ *
98
+ * @param {string} contractId
99
+ * @param {object} packet
100
+ * @param {'input'|'output'} [direction]
101
+ * @returns {{ ok: true, contract, direction }}
102
+ */
103
+ export function enforcePacket(contractId, packet, direction = 'input') {
104
+ const result = validatePacket(contractId, packet, direction);
105
+ if (!result.ok) {
106
+ if (result.reason === 'contract-not-found') {
107
+ throw new ContractViolationError({
108
+ contractId,
109
+ direction,
110
+ missing: ['(contract not found)'],
111
+ });
112
+ }
113
+ logViolation(contractId, direction, result.missing || [], packet);
114
+ throw new ContractViolationError({
115
+ contractId,
116
+ direction,
117
+ missing: result.missing || [],
118
+ });
119
+ }
120
+
121
+ if (direction === 'output') {
122
+ const contract = result.contract || getContractById(contractId);
123
+ const producer = contract?.producer;
124
+ if (producer) {
125
+ const postcondition = validatePostconditions(producer, packet);
126
+ if (!postcondition.ok) {
127
+ logViolation(contractId, direction, [], packet, {
128
+ verdict: 'BLOCKED_CONTRACT',
129
+ postconditionFailures: postcondition.failures,
130
+ });
131
+ throw new ContractViolationError({
132
+ contractId,
133
+ direction,
134
+ verdict: 'BLOCKED_CONTRACT',
135
+ postconditionFailures: postcondition.failures,
136
+ });
137
+ }
138
+ }
139
+ }
140
+
141
+ return result;
142
+ }
143
+
144
+ /**
145
+ * Read recent violations from the chain log. Returns oldest-first.
146
+ */
147
+ export function recentViolations({ windowMs = 24 * 60 * 60 * 1000 } = {}) {
148
+ if (!existsSync(LOG_FILE)) return [];
149
+ try {
150
+ const cutoff = Date.now() - windowMs;
151
+ return readFileSync(LOG_FILE, 'utf8')
152
+ .trim()
153
+ .split('\n')
154
+ .filter(Boolean)
155
+ .map((line) => { try { return JSON.parse(line); } catch { return null; } })
156
+ .filter((r) => r && new Date(r.ts).getTime() >= cutoff);
157
+ } catch { return []; }
158
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * lib/agent-contracts.mjs — loader + query layer for agents/contracts.json.
3
+ *
4
+ * Agent contracts are explicit service contracts between Construct roles —
5
+ * "when cx-product-manager hands off to cx-architect, the packet MUST contain
6
+ * X and the response MUST be shape Y." Single machine-readable source of
7
+ * truth for handoff schemas, doc ownership, specialist mappings, and the
8
+ * framing/research gates.
9
+ *
10
+ * Consumed by:
11
+ * - lib/orchestration-policy.mjs → routeRequest returns the contract chain
12
+ * for a request, not just a list of specialists.
13
+ * - lib/mcp/server.mjs → exposes agent_contract MCP tool for specialists
14
+ * to introspect their inputs and expected outputs at handoff time.
15
+ * - future: handoff validators that check packet shape before dispatch.
16
+ */
17
+ import { existsSync, readFileSync } from 'node:fs';
18
+ import { dirname, join, resolve } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
22
+ const REPO_ROOT = resolve(MODULE_DIR, '..');
23
+ const CONTRACTS_PATH = join(REPO_ROOT, 'agents', 'contracts.json');
24
+
25
+ let _cache = null;
26
+
27
+ function loadRaw() {
28
+ if (_cache) return _cache;
29
+ if (!existsSync(CONTRACTS_PATH)) {
30
+ _cache = { version: 0, contracts: [] };
31
+ return _cache;
32
+ }
33
+ try {
34
+ _cache = JSON.parse(readFileSync(CONTRACTS_PATH, 'utf8'));
35
+ } catch (err) {
36
+ _cache = { version: 0, contracts: [], parseError: err?.message || String(err) };
37
+ }
38
+ return _cache;
39
+ }
40
+
41
+ export function getAllContracts() {
42
+ return loadRaw().contracts || [];
43
+ }
44
+
45
+ export function getContractById(id) {
46
+ return getAllContracts().find((c) => c.id === id) || null;
47
+ }
48
+
49
+ /**
50
+ * Returns the contract for a specific producer→consumer pair. If multiple
51
+ * contracts match (e.g. wildcard producer "*"), returns the most specific one.
52
+ */
53
+ export function getContract(producer, consumer) {
54
+ const all = getAllContracts();
55
+ const exact = all.find((c) => c.producer === producer && c.consumer === consumer);
56
+ if (exact) return exact;
57
+ return all.find((c) => (c.producer === '*' && c.consumer === consumer)) || null;
58
+ }
59
+
60
+ /**
61
+ * Returns all contracts where the given agent is the consumer — the set of
62
+ * inbound packets the agent is expected to receive from upstream producers.
63
+ */
64
+ export function getIncomingContracts(consumer) {
65
+ return getAllContracts().filter((c) => c.consumer === consumer);
66
+ }
67
+
68
+ /**
69
+ * Returns all contracts where the given agent is the producer — the set of
70
+ * outbound packets the agent is expected to send to downstream consumers.
71
+ */
72
+ export function getOutgoingContracts(producer) {
73
+ return getAllContracts().filter((c) => c.producer === producer || c.producer === '*');
74
+ }
75
+
76
+ /**
77
+ * Matches contracts whose `trigger` is satisfied by the given routing
78
+ * context. Surfaces which contracts apply to a dispatch plan so routeRequest
79
+ * can attach them to the response.
80
+ *
81
+ * Trigger language:
82
+ * { always: true } → always fires
83
+ * { intent: "fix" | ["fix","investigation"] } → intent match
84
+ * { workCategory: "deep" } → work category match
85
+ * { track: "orchestrated" } → execution track match
86
+ * { riskFlags: ["security"] } → any-of risk flags set
87
+ * { "framingChallenge.required": true } → gate required
88
+ * { "externalResearch.required": true } → gate required
89
+ * { "docAuthoring.docType": ["prd","adr"] } → authoring intent match
90
+ * { postConsumer: true, changedCoreDocs: true } → fires after consumer ran and core docs changed
91
+ */
92
+ export function triggerMatches(contract, ctx = {}) {
93
+ const t = contract.trigger || {};
94
+ if (t.always === true) return true;
95
+
96
+ // intent: string | string[]
97
+ if (t.intent) {
98
+ const intents = Array.isArray(t.intent) ? t.intent : [t.intent];
99
+ if (!intents.includes(ctx.intent)) return false;
100
+ }
101
+ if (t.workCategory) {
102
+ const cats = Array.isArray(t.workCategory) ? t.workCategory : [t.workCategory];
103
+ if (!cats.includes(ctx.workCategory)) return false;
104
+ }
105
+ if (t.track) {
106
+ const tracks = Array.isArray(t.track) ? t.track : [t.track];
107
+ if (!tracks.includes(ctx.track)) return false;
108
+ }
109
+ if (Array.isArray(t.riskFlags)) {
110
+ const anyMatch = t.riskFlags.some((flag) => ctx.riskFlags?.[flag]);
111
+ if (!anyMatch) return false;
112
+ }
113
+ if (t['framingChallenge.required'] === true) {
114
+ if (!ctx.framingChallenge?.required) return false;
115
+ }
116
+ if (t['externalResearch.required'] === true) {
117
+ if (!ctx.externalResearch?.required) return false;
118
+ }
119
+ if (t['docAuthoring.docType']) {
120
+ const docTypes = Array.isArray(t['docAuthoring.docType'])
121
+ ? t['docAuthoring.docType']
122
+ : [t['docAuthoring.docType']];
123
+ if (!ctx.docAuthoring?.docType || !docTypes.includes(ctx.docAuthoring.docType)) return false;
124
+ }
125
+ // Post-consumer contracts (e.g. docs-keeper) fire as side effects, not
126
+ // from the initial routing decision. They're returned by
127
+ // resolveContractChain but marked so callers can defer them.
128
+ return true;
129
+ }
130
+
131
+ function contractParticipantsApply(contract, ctx = {}) {
132
+ const specialists = Array.isArray(ctx.specialists) ? ctx.specialists : null;
133
+ if (!specialists) return true;
134
+
135
+ const participants = new Set(['user', 'construct', ...specialists]);
136
+ const producerApplies = contract.producer === '*' || participants.has(contract.producer);
137
+ const consumerApplies = participants.has(contract.consumer);
138
+ return producerApplies && consumerApplies;
139
+ }
140
+
141
+ /**
142
+ * Returns the ordered contract chain that applies to a routing context.
143
+ * Each element is `{ contract, stage }` where stage is "precheck" (runs
144
+ * before work begins), "handoff" (runs between specialists), or "followup"
145
+ * (runs after DONE, e.g. docs-keeper).
146
+ */
147
+ export function resolveContractChain(ctx = {}) {
148
+ const chain = [];
149
+ for (const contract of getAllContracts()) {
150
+ if (!triggerMatches(contract, ctx)) continue;
151
+ if (!contractParticipantsApply(contract, ctx)) continue;
152
+ const stage = contract.trigger?.postConsumer ? 'followup'
153
+ : contract.producer === 'user' ? 'precheck'
154
+ : 'handoff';
155
+ chain.push({ contract, stage });
156
+ }
157
+ return chain;
158
+ }
159
+
160
+ function fieldPresent(packet, key) {
161
+ const value = packet?.[key];
162
+ if (value === undefined || value === null) return false;
163
+ if (typeof value === 'string' && value.trim() === '') return false;
164
+ return true;
165
+ }
166
+
167
+ /**
168
+ * Lightweight shape validation for handoff packets. Checks three shapes:
169
+ * mustContain — every key must be present (non-null, non-empty-string).
170
+ * mustContainOneOf — array of OR groups; at least one field in each group
171
+ * must be present. Missing entries report joined keys:
172
+ * ["findings", "noIssuesFoundAt"] → "findings|noIssuesFoundAt".
173
+ * mustMatchEnum — map of key → allowed values. Mismatch reports
174
+ * "<key>!=<allowed>" so callers can branch on it.
175
+ *
176
+ * Empty arrays and empty objects count as present — they signal "declared
177
+ * but empty," a legitimate state. Use `mustHaveNonEmpty` in a future spec
178
+ * if you need a stricter check.
179
+ */
180
+ export function validatePacket(contractId, packet, direction = 'input') {
181
+ const contract = getContractById(contractId);
182
+ if (!contract) return { ok: false, reason: 'contract-not-found', contractId };
183
+ const spec = direction === 'input' ? contract.input : contract.output;
184
+ if (!spec) return { ok: true, contract, direction };
185
+
186
+ const missing = [];
187
+
188
+ if (Array.isArray(spec.mustContain)) {
189
+ for (const key of spec.mustContain) {
190
+ if (!fieldPresent(packet, key)) missing.push(key);
191
+ }
192
+ }
193
+
194
+ if (Array.isArray(spec.mustContainOneOf)) {
195
+ for (const group of spec.mustContainOneOf) {
196
+ if (!Array.isArray(group) || group.length === 0) continue;
197
+ const anyPresent = group.some((key) => fieldPresent(packet, key));
198
+ if (!anyPresent) missing.push(group.join('|'));
199
+ }
200
+ }
201
+
202
+ if (spec.mustMatchEnum && typeof spec.mustMatchEnum === 'object') {
203
+ for (const [key, allowed] of Object.entries(spec.mustMatchEnum)) {
204
+ if (!Array.isArray(allowed) || allowed.length === 0) continue;
205
+ if (!fieldPresent(packet, key)) continue;
206
+ if (!allowed.includes(packet[key])) missing.push(`${key}!=${allowed.join('|')}`);
207
+ }
208
+ }
209
+
210
+ return { ok: missing.length === 0, missing, contract, direction };
211
+ }
212
+
213
+ /**
214
+ * Summary statistics for `construct status` / `construct doctor` surfaces.
215
+ */
216
+ export function summarize() {
217
+ const all = getAllContracts();
218
+ const byProducer = {};
219
+ const byConsumer = {};
220
+ for (const c of all) {
221
+ byProducer[c.producer] = (byProducer[c.producer] || 0) + 1;
222
+ byConsumer[c.consumer] = (byConsumer[c.consumer] || 0) + 1;
223
+ }
224
+ return {
225
+ total: all.length,
226
+ producers: Object.keys(byProducer).length,
227
+ consumers: Object.keys(byConsumer).length,
228
+ byProducer,
229
+ byConsumer,
230
+ };
231
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * lib/agents/postconditions.mjs — hard binary postconditions per producer persona.
3
+ *
4
+ * `agents/contracts.json` carries free-text postconditions for documentation
5
+ * purposes. This module adds binary, programmatically-validated assertions
6
+ * that the persona's output packet must satisfy before a handoff is allowed
7
+ * to proceed. Failure throws `ContractViolationError` with reason
8
+ * `BLOCKED_CONTRACT` so the dispatcher refuses to forward a packet that
9
+ * silently rubber-stamps, fixes symptoms without root cause, ships stale
10
+ * docs, or skips accessibility review.
11
+ *
12
+ * Each rule is identified by a stable id so violations are greppable in
13
+ * `~/.cx/contract-violations.jsonl`. Rules are pure functions of the
14
+ * output packet — no IO, no LLM calls — so they're deterministic and
15
+ * cheap to evaluate.
16
+ *
17
+ * Producers covered (CF3 round 1 from the 2026-05-13 UX audit):
18
+ * - cx-reviewer prevents silent rubber-stamp reviews
19
+ * - cx-security prevents post-hoc threat models
20
+ * - cx-debugger prevents symptom-only fixes
21
+ * - cx-docs-keeper prevents stale-doc PRs
22
+ * - cx-designer prevents post-hoc accessibility
23
+ */
24
+
25
+ const ROOT_CAUSE_SOURCES = new Set(['reproduction', 'trace', 'test']);
26
+
27
+ function isNonEmptyArray(value) {
28
+ return Array.isArray(value) && value.length > 0;
29
+ }
30
+
31
+ function isNonEmptyString(value) {
32
+ return typeof value === 'string' && value.trim().length > 0;
33
+ }
34
+
35
+ function isLaterOrEqual(a, b) {
36
+ const ta = a instanceof Date ? a.getTime() : Date.parse(a);
37
+ const tb = b instanceof Date ? b.getTime() : Date.parse(b);
38
+ if (!Number.isFinite(ta) || !Number.isFinite(tb)) return false;
39
+ return ta >= tb;
40
+ }
41
+
42
+ /**
43
+ * Rule table. Keyed by producer name (matching agents/registry.json).
44
+ * Each rule:
45
+ * id — stable identifier for violation logs
46
+ * description — single-line documentation
47
+ * check — (packet) => boolean — true when satisfied
48
+ * reason — explanation surfaced when violated
49
+ */
50
+ export const POSTCONDITIONS = {
51
+ 'cx-reviewer': [
52
+ {
53
+ id: 'reviewer.findings-or-explicit-clear',
54
+ description: 'Reviewer must either return at least one finding or explicitly state "no issues found at: <paths>".',
55
+ check: (p) => isNonEmptyArray(p?.findings) || isNonEmptyArray(p?.noIssuesFoundAt) || isNonEmptyString(p?.noIssuesFoundStatement),
56
+ reason: 'Reviewer output rubber-stamped: empty findings and no explicit "no issues found at: <paths>" statement.',
57
+ },
58
+ ],
59
+ 'cx-security': [
60
+ {
61
+ id: 'security.threat-model-not-post-hoc',
62
+ description: 'Threat model must be updated at or after the contract start (not retrofitted).',
63
+ check: (p) => {
64
+ if (!p?.threatModelUpdatedAt || !p?.contractStart) return false;
65
+ return isLaterOrEqual(p.threatModelUpdatedAt, p.contractStart);
66
+ },
67
+ reason: 'Threat model missing or older than the contract start — likely retrofitted after implementation.',
68
+ },
69
+ ],
70
+ 'cx-debugger': [
71
+ {
72
+ id: 'debugger.root-cause-confirmed-via',
73
+ description: 'Root cause must be confirmed via reproduction, trace, or test (not inferred).',
74
+ check: (p) => typeof p?.rootCauseConfirmedVia === 'string' && ROOT_CAUSE_SOURCES.has(p.rootCauseConfirmedVia),
75
+ reason: `rootCauseConfirmedVia must be one of: ${[...ROOT_CAUSE_SOURCES].join(', ')}.`,
76
+ },
77
+ ],
78
+ 'cx-docs-keeper': [
79
+ {
80
+ id: 'docs-keeper.cross-doc-coherence-check-ran',
81
+ description: 'Docs-keeper must run the cross-doc coherence check and attach a named diff.',
82
+ check: (p) => p?.crossDocCoherenceCheckRan === true && isNonEmptyString(p?.coherenceDiff),
83
+ reason: 'crossDocCoherenceCheckRan must be true AND coherenceDiff must be a non-empty named diff.',
84
+ },
85
+ ],
86
+ 'cx-designer': [
87
+ {
88
+ id: 'designer.accessibility-check-ran',
89
+ description: 'Designer must run the accessibility check before handoff (no post-hoc a11y).',
90
+ check: (p) => p?.accessibilityCheckRan === true,
91
+ reason: 'accessibilityCheckRan must be true — accessibility review is a precondition for any visual deliverable.',
92
+ },
93
+ ],
94
+ };
95
+
96
+ /**
97
+ * Evaluate the binary postcondition rules for a producer's output packet.
98
+ *
99
+ * @param {string} producer — persona name (e.g. `cx-reviewer`)
100
+ * @param {object} packet — the output packet about to hand off
101
+ * @returns {{ ok: boolean, producer: string, failures: Array<{id, reason}> }}
102
+ */
103
+ export function validatePostconditions(producer, packet) {
104
+ const rules = POSTCONDITIONS[producer] || [];
105
+ if (rules.length === 0) return { ok: true, producer, failures: [] };
106
+ const failures = [];
107
+ for (const rule of rules) {
108
+ let satisfied;
109
+ try {
110
+ satisfied = rule.check(packet);
111
+ } catch {
112
+ satisfied = false;
113
+ }
114
+ if (!satisfied) failures.push({ id: rule.id, reason: rule.reason });
115
+ }
116
+ return { ok: failures.length === 0, producer, failures };
117
+ }
118
+
119
+ /**
120
+ * List the rule ids registered for a producer — useful for tests and
121
+ * for the `agent_contract` MCP tool when surfacing what a persona must
122
+ * produce.
123
+ */
124
+ export function describePostconditions(producer) {
125
+ return (POSTCONDITIONS[producer] || []).map((r) => ({ id: r.id, description: r.description }));
126
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * lib/agents/schema.mjs — validation contract for agents/registry.json entries.
3
+ *
4
+ * Catches drift before it ships: missing description, unknown tool names,
5
+ * promptFile pointing at a path that does not resolve, unrecognized
6
+ * modelTier values. Run via `construct lint:agents` and in CI so a typo
7
+ * in the registry can't silently disable an agent.
8
+ */
9
+
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+
13
+ const REQUIRED_FIELDS = ['name', 'description', 'promptFile'];
14
+
15
+ // Tool names that can appear in claudeTools. Anything outside this allowlist
16
+ // surfaces as an error; the host platform will silently drop unknown tool
17
+ // references and the agent will appear to "work" without the tool, which is
18
+ // the failure mode this lint catches.
19
+
20
+ export const ALLOWED_TOOLS = new Set([
21
+ // Anthropic Claude Code built-ins
22
+ 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'LS', 'Bash', 'BashOutput',
23
+ 'WebSearch', 'WebFetch', 'TodoWrite', 'NotebookEdit', 'Task',
24
+ // Construct MCP tools (from construct-mcp server)
25
+ 'list_skills', 'get_skill', 'search_skills',
26
+ 'list_teams', 'get_team',
27
+ 'list_templates', 'get_template',
28
+ 'agent_contract', 'orchestration_policy',
29
+ 'cx_trace', 'cx_score',
30
+ // Memory MCP server
31
+ 'memory_search', 'memory_add_observations', 'memory_open_nodes',
32
+ 'memory_create_entities', 'memory_create_relations',
33
+ // Other MCP servers Construct ships with
34
+ 'context7_resolve', 'context7_docs',
35
+ 'sequential_thinking',
36
+ 'playwright_navigate', 'playwright_screenshot',
37
+ 'github_search', 'github_get_pull_request',
38
+ ]);
39
+
40
+ /**
41
+ * Validate one agent record. Returns an array of error strings; empty means
42
+ * the record passes.
43
+ */
44
+ export function validateAgentRecord(record, { rootDir, registryKey } = {}) {
45
+ const errors = [];
46
+ const id = record?.name || `#${registryKey ?? '?'}`;
47
+
48
+ if (!record || typeof record !== 'object') {
49
+ return [`${id}: agent record must be an object`];
50
+ }
51
+
52
+ for (const field of REQUIRED_FIELDS) {
53
+ if (!record[field]) errors.push(`${id}: missing required field "${field}"`);
54
+ }
55
+
56
+ if (record.description) {
57
+ const desc = String(record.description).trim();
58
+ if (desc.length < 20) errors.push(`${id}: description is too short (<20 chars) — the description is load-bearing for routing`);
59
+ if (desc.length > 500) errors.push(`${id}: description is too long (>500 chars) — keep it scannable`);
60
+ }
61
+
62
+ if (record.when_to_use !== undefined && record.when_to_use !== null) {
63
+ const wtu = String(record.when_to_use).trim();
64
+ if (wtu.length < 20) errors.push(`${id}: when_to_use is too short (<20 chars) — should describe the trigger conditions`);
65
+ if (wtu.length > 500) errors.push(`${id}: when_to_use is too long (>500 chars) — keep it scannable for routing`);
66
+ }
67
+
68
+ if (record.promptFile && rootDir) {
69
+ const promptPath = path.join(rootDir, record.promptFile);
70
+ if (!fs.existsSync(promptPath)) errors.push(`${id}: promptFile "${record.promptFile}" does not exist`);
71
+ }
72
+
73
+ if (record.claudeTools) {
74
+ const tools = String(record.claudeTools).split(',').map((t) => t.trim()).filter(Boolean);
75
+ for (const tool of tools) {
76
+ if (!ALLOWED_TOOLS.has(tool)) {
77
+ errors.push(`${id}: unknown tool "${tool}" in claudeTools — host platforms silently drop unknown tool names, causing this agent to appear to work without it`);
78
+ }
79
+ }
80
+ }
81
+
82
+ if (record.modelTier && !['fast', 'standard', 'reasoning'].includes(record.modelTier)) {
83
+ errors.push(`${id}: modelTier must be one of fast | standard | reasoning (got "${record.modelTier}")`);
84
+ }
85
+
86
+ return errors;
87
+ }
88
+
89
+ /**
90
+ * Validate every agent in a registry. Returns { errors: string[], agentCount }.
91
+ */
92
+ export function validateRegistry(registry, { rootDir } = {}) {
93
+ if (!registry || !Array.isArray(registry.agents)) {
94
+ return { errors: ['registry.agents is missing or not an array'], agentCount: 0 };
95
+ }
96
+ const errors = [];
97
+ const seenNames = new Set();
98
+ for (let i = 0; i < registry.agents.length; i += 1) {
99
+ const record = registry.agents[i];
100
+ const recordErrors = validateAgentRecord(record, { rootDir, registryKey: i });
101
+ errors.push(...recordErrors);
102
+ if (record?.name) {
103
+ if (seenNames.has(record.name)) errors.push(`${record.name}: duplicate agent name at registry index ${i}`);
104
+ seenNames.add(record.name);
105
+ }
106
+ }
107
+ return { errors, agentCount: registry.agents.length };
108
+ }
109
+
110
+ /**
111
+ * Convenience: load registry.json from disk and validate it.
112
+ */
113
+ export function validateRegistryFile({ registryPath, rootDir } = {}) {
114
+ if (!fs.existsSync(registryPath)) {
115
+ return { errors: [`registry not found at ${registryPath}`], agentCount: 0 };
116
+ }
117
+ let registry;
118
+ try {
119
+ registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
120
+ } catch (err) {
121
+ return { errors: [`registry parse error: ${err.message}`], agentCount: 0 };
122
+ }
123
+ return validateRegistry(registry, { rootDir });
124
+ }