@geraldmaron/construct 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.env.example +9 -6
  2. package/LICENSE +201 -21
  3. package/README.md +132 -257
  4. package/agents/contracts.json +387 -0
  5. package/agents/prompts/cx-accessibility.md +8 -0
  6. package/agents/prompts/cx-ai-engineer.md +92 -0
  7. package/agents/prompts/cx-architect.md +10 -2
  8. package/agents/prompts/cx-business-strategist.md +13 -0
  9. package/agents/prompts/cx-data-analyst.md +80 -0
  10. package/agents/prompts/cx-data-engineer.md +4 -0
  11. package/agents/prompts/cx-debugger.md +8 -0
  12. package/agents/prompts/cx-designer.md +19 -0
  13. package/agents/prompts/cx-devil-advocate.md +4 -0
  14. package/agents/prompts/cx-docs-keeper.md +128 -0
  15. package/agents/prompts/cx-engineer.md +13 -0
  16. package/agents/prompts/cx-evaluator.md +4 -0
  17. package/agents/prompts/cx-explorer.md +12 -0
  18. package/agents/prompts/cx-legal-compliance.md +13 -0
  19. package/agents/prompts/cx-operations.md +13 -0
  20. package/agents/prompts/cx-orchestrator.md +107 -4
  21. package/agents/prompts/cx-platform-engineer.md +75 -0
  22. package/agents/prompts/cx-product-manager.md +8 -0
  23. package/agents/prompts/cx-qa.md +100 -0
  24. package/agents/prompts/cx-rd-lead.md +13 -0
  25. package/agents/prompts/cx-release-manager.md +8 -0
  26. package/agents/prompts/cx-researcher.md +21 -1
  27. package/agents/prompts/cx-reviewer.md +8 -0
  28. package/agents/prompts/cx-security.md +104 -0
  29. package/agents/prompts/cx-sre.md +104 -0
  30. package/agents/prompts/cx-test-automation.md +4 -0
  31. package/agents/prompts/cx-trace-reviewer.md +7 -3
  32. package/agents/prompts/cx-ux-researcher.md +4 -0
  33. package/agents/registry.json +365 -90
  34. package/agents/role-manifests.json +217 -0
  35. package/bin/construct +3345 -141
  36. package/bin/construct-postinstall.mjs +87 -0
  37. package/commands/build/feature.md +6 -6
  38. package/commands/plan/feature.md +6 -5
  39. package/commands/plan/requirements.md +1 -1
  40. package/commands/ship/status.md +1 -1
  41. package/commands/work/drive.md +3 -3
  42. package/commands/work/optimize-prompts.md +3 -3
  43. package/db/{migrations → schema}/001_init.sql +2 -0
  44. package/db/schema/002_pgvector.sql +182 -0
  45. package/db/schema/003_intake.sql +47 -0
  46. package/examples/README.md +85 -0
  47. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
  48. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
  49. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
  50. package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
  51. package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
  52. package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
  53. package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
  54. package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
  55. package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
  56. package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
  57. package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
  58. package/examples/personas/construct/bad/commit-without-approval.md +30 -0
  59. package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
  60. package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
  61. package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
  62. package/examples/provider-plugin/README.md +34 -0
  63. package/examples/provider-plugin/index.mjs +74 -0
  64. package/examples/provider-plugin/package.json +15 -0
  65. package/examples/seed-observations/README.md +38 -0
  66. package/examples/seed-observations/anti-patterns.md +44 -0
  67. package/examples/seed-observations/decisions.md +36 -0
  68. package/examples/seed-observations/patterns.md +42 -0
  69. package/lib/agent-contracts-enforce.mjs +158 -0
  70. package/lib/agent-contracts.mjs +231 -0
  71. package/lib/agents/postconditions.mjs +126 -0
  72. package/lib/agents/schema.mjs +124 -0
  73. package/lib/artifact-capture.mjs +183 -0
  74. package/lib/audit-trail.mjs +149 -0
  75. package/lib/auto-docs.mjs +245 -65
  76. package/lib/beads/auto-close.mjs +126 -0
  77. package/lib/beads/drift.mjs +171 -0
  78. package/lib/beads-automation.mjs +542 -0
  79. package/lib/beads-client.mjs +518 -0
  80. package/lib/beads-lock.mjs +377 -0
  81. package/lib/beads-optimistic.mjs +365 -0
  82. package/lib/bootstrap/built-ins.mjs +136 -0
  83. package/lib/bootstrap/lazy-install.mjs +161 -0
  84. package/lib/bootstrap/resources.mjs +120 -0
  85. package/lib/bootstrap.mjs +105 -0
  86. package/lib/cache-governor.js +213 -0
  87. package/lib/cache-strategy-anthropic.js +62 -0
  88. package/lib/cache-strategy-google.js +79 -0
  89. package/lib/cache-strategy-none.js +30 -0
  90. package/lib/cache-strategy-openai.js +48 -0
  91. package/lib/cache-strategy.js +91 -0
  92. package/lib/claude-allow.mjs +149 -0
  93. package/lib/cli-commands.mjs +413 -258
  94. package/lib/codex-config.mjs +3 -1
  95. package/lib/comment-lint.mjs +55 -6
  96. package/lib/completions.mjs +21 -6
  97. package/lib/config/alias.mjs +56 -0
  98. package/lib/config/project-config.mjs +335 -0
  99. package/lib/config/schema.mjs +159 -0
  100. package/lib/context-router.mjs +308 -0
  101. package/lib/cost-ledger.mjs +177 -0
  102. package/lib/cost.mjs +171 -10
  103. package/lib/dashboard-static.mjs +158 -0
  104. package/lib/deployment-mode.mjs +86 -0
  105. package/lib/deprecate.mjs +49 -0
  106. package/lib/dispatch-batch.js +183 -0
  107. package/lib/distill.mjs +21 -8
  108. package/lib/doc-stamp.mjs +164 -0
  109. package/lib/doc-verify.mjs +119 -0
  110. package/lib/docs-routing.mjs +89 -0
  111. package/lib/docs-verify.mjs +417 -0
  112. package/lib/doctor/audit.mjs +71 -0
  113. package/lib/doctor/cli.mjs +99 -0
  114. package/lib/doctor/escalate.mjs +29 -0
  115. package/lib/doctor/index.mjs +140 -0
  116. package/lib/doctor/report.mjs +170 -0
  117. package/lib/doctor/watchers/bd-watch.mjs +117 -0
  118. package/lib/doctor/watchers/cost.mjs +130 -0
  119. package/lib/doctor/watchers/disk.mjs +122 -0
  120. package/lib/doctor/watchers/handoffs.mjs +33 -0
  121. package/lib/doctor/watchers/process-pressure.mjs +60 -0
  122. package/lib/doctor/watchers/service-health.mjs +188 -0
  123. package/lib/document-extract.mjs +288 -0
  124. package/lib/document-ingest.mjs +230 -0
  125. package/lib/drop.mjs +282 -0
  126. package/lib/embed/approval-queue.mjs +176 -0
  127. package/lib/embed/artifact.mjs +349 -0
  128. package/lib/embed/authority-guard.mjs +155 -0
  129. package/lib/embed/cli.mjs +408 -0
  130. package/lib/embed/config.mjs +355 -0
  131. package/lib/embed/conflict-detection.mjs +264 -0
  132. package/lib/embed/customer-profiles.mjs +480 -0
  133. package/lib/embed/daemon.mjs +1309 -0
  134. package/lib/embed/demand-fetch.mjs +449 -0
  135. package/lib/embed/docs-lifecycle.mjs +349 -0
  136. package/lib/embed/inbox-live-watcher.mjs +119 -0
  137. package/lib/embed/inbox.mjs +343 -0
  138. package/lib/embed/intake-metrics.mjs +190 -0
  139. package/lib/embed/jobs/vector-sync.mjs +198 -0
  140. package/lib/embed/notifications.mjs +75 -0
  141. package/lib/embed/output.mjs +79 -0
  142. package/lib/embed/providers/github.mjs +295 -0
  143. package/lib/embed/providers/jira.mjs +192 -0
  144. package/lib/embed/providers/linear.mjs +186 -0
  145. package/lib/embed/providers/registry.mjs +115 -0
  146. package/lib/embed/providers/slack.mjs +203 -0
  147. package/lib/embed/recommendation-store.mjs +378 -0
  148. package/lib/embed/roadmap.mjs +374 -0
  149. package/lib/embed/role-framing.mjs +110 -0
  150. package/lib/embed/scheduler.mjs +99 -0
  151. package/lib/embed/semantic.mjs +325 -0
  152. package/lib/embed/snapshot.mjs +191 -0
  153. package/lib/embed/supervision.mjs +235 -0
  154. package/lib/embed/target-resolver.mjs +186 -0
  155. package/lib/embed/worker.mjs +62 -0
  156. package/lib/embed/workspaces.mjs +297 -0
  157. package/lib/engine/chunker-headings.mjs +110 -0
  158. package/lib/engine/compressor-heuristic.mjs +100 -0
  159. package/lib/engine/consolidate.mjs +287 -0
  160. package/lib/engine/contracts.mjs +126 -0
  161. package/lib/engine/defaults.mjs +129 -0
  162. package/lib/engine/eval-retrieval.mjs +148 -0
  163. package/lib/engine/fuser-rrf.mjs +62 -0
  164. package/lib/engine/index.mjs +37 -0
  165. package/lib/engine/registry.mjs +146 -0
  166. package/lib/engine/reranker-mmr.mjs +90 -0
  167. package/lib/engine/tokens.mjs +77 -0
  168. package/lib/entity-store.mjs +280 -0
  169. package/lib/env-config.mjs +69 -4
  170. package/lib/evals/retrieval-bench.mjs +159 -0
  171. package/lib/evaluator-optimizer.mjs +317 -0
  172. package/lib/features.mjs +159 -29
  173. package/lib/gates-audit.mjs +236 -0
  174. package/lib/git-hooks/prepare-commit-msg +58 -0
  175. package/lib/handoffs/cleanup.mjs +159 -0
  176. package/lib/handoffs/contract.mjs +162 -0
  177. package/lib/handoffs/inventory.mjs +120 -0
  178. package/lib/headhunt.mjs +28 -47
  179. package/lib/health-check.mjs +399 -0
  180. package/lib/hook-health.mjs +442 -0
  181. package/lib/hooks/_lib/log.mjs +82 -0
  182. package/lib/hooks/adaptive-lint.mjs +26 -2
  183. package/lib/hooks/agent-tracker.mjs +171 -14
  184. package/lib/hooks/audit-reads.mjs +109 -0
  185. package/lib/hooks/audit-trail.mjs +153 -0
  186. package/lib/hooks/bash-output-logger.mjs +71 -0
  187. package/lib/hooks/block-no-verify.mjs +41 -0
  188. package/lib/hooks/ci-status-check.mjs +82 -0
  189. package/lib/hooks/comment-lint.mjs +29 -7
  190. package/lib/hooks/config-protection.mjs +39 -18
  191. package/lib/hooks/context-watch.mjs +137 -0
  192. package/lib/hooks/context-window-recovery.mjs +4 -20
  193. package/lib/hooks/dep-audit.mjs +16 -0
  194. package/lib/hooks/doc-coupling-check.mjs +80 -0
  195. package/lib/hooks/edit-accumulator.mjs +3 -0
  196. package/lib/hooks/edit-error-recovery.mjs +3 -0
  197. package/lib/hooks/edit-guard.mjs +45 -4
  198. package/lib/hooks/env-check.mjs +3 -0
  199. package/lib/hooks/guard-bash.mjs +58 -1
  200. package/lib/hooks/mcp-audit.mjs +18 -20
  201. package/lib/hooks/mcp-health-check.mjs +36 -0
  202. package/lib/hooks/model-fallback.mjs +42 -56
  203. package/lib/hooks/policy-engine.mjs +209 -0
  204. package/lib/hooks/post-merge-docs-check.mjs +63 -0
  205. package/lib/hooks/pre-compact.mjs +4 -24
  206. package/lib/hooks/pre-push-gate.mjs +200 -37
  207. package/lib/hooks/proactive-activation.mjs +284 -0
  208. package/lib/hooks/read-tracker.mjs +27 -4
  209. package/lib/hooks/readme-age-check.mjs +77 -0
  210. package/lib/hooks/registry-sync.mjs +12 -5
  211. package/lib/hooks/scan-secrets.mjs +50 -7
  212. package/lib/hooks/session-optimize.mjs +311 -0
  213. package/lib/hooks/session-start.mjs +287 -28
  214. package/lib/hooks/stop-notify.mjs +236 -96
  215. package/lib/hooks/stop-typecheck.mjs +13 -1
  216. package/lib/hooks/test-watch.mjs +68 -0
  217. package/lib/host-capabilities.mjs +31 -10
  218. package/lib/init-docs.mjs +731 -291
  219. package/lib/init-unified.mjs +1000 -0
  220. package/lib/init-update.mjs +168 -0
  221. package/lib/init.mjs +107 -0
  222. package/lib/install/first-invocation.mjs +119 -0
  223. package/lib/install/stage-project.mjs +69 -0
  224. package/lib/intake/classify.mjs +278 -0
  225. package/lib/intake/feedback.mjs +273 -0
  226. package/lib/intake/filesystem-queue.mjs +158 -0
  227. package/lib/intake/intake-config.mjs +132 -0
  228. package/lib/intake/postgres-queue.mjs +198 -0
  229. package/lib/intake/prepare.mjs +139 -0
  230. package/lib/intake/queue.mjs +83 -0
  231. package/lib/intake/session-prelude.mjs +50 -0
  232. package/lib/integrations/intake-integrations.mjs +740 -0
  233. package/lib/intent-classifier.mjs +253 -0
  234. package/lib/knowledge/layout.mjs +72 -0
  235. package/lib/knowledge/rag.mjs +331 -0
  236. package/lib/knowledge/search.mjs +315 -0
  237. package/lib/knowledge/trends.mjs +261 -0
  238. package/lib/logger.mjs +85 -0
  239. package/lib/mcp/broker.mjs +124 -0
  240. package/lib/mcp/server.mjs +554 -854
  241. package/lib/mcp/tools/document.mjs +132 -0
  242. package/lib/mcp/tools/memory.mjs +209 -0
  243. package/lib/mcp/tools/project.mjs +349 -0
  244. package/lib/mcp/tools/skills.mjs +409 -0
  245. package/lib/mcp/tools/storage.mjs +60 -0
  246. package/lib/mcp/tools/telemetry.mjs +315 -0
  247. package/lib/mcp/tools/workflow.mjs +112 -0
  248. package/lib/mcp-catalog.json +54 -3
  249. package/lib/mcp-manager.mjs +96 -48
  250. package/lib/mcp-platform-config.mjs +22 -22
  251. package/lib/memory-stats.mjs +121 -0
  252. package/lib/mode-commands.mjs +124 -0
  253. package/lib/model-free-selector.mjs +184 -0
  254. package/lib/model-pricing.mjs +152 -0
  255. package/lib/model-registry.mjs +226 -0
  256. package/lib/model-router.mjs +362 -386
  257. package/lib/observation-store.mjs +391 -0
  258. package/lib/ollama-manager.mjs +428 -0
  259. package/lib/opencode-config.mjs +13 -3
  260. package/lib/opencode-runtime-plugin.mjs +70 -38
  261. package/lib/opencode-telemetry.mjs +64 -6
  262. package/lib/orchestration-policy.mjs +509 -6
  263. package/lib/overrides/resolver.mjs +207 -0
  264. package/lib/parity.mjs +147 -0
  265. package/lib/paths.mjs +33 -0
  266. package/lib/performance/generate.mjs +212 -0
  267. package/lib/plugin-registry.mjs +268 -0
  268. package/lib/policy/engine.mjs +130 -0
  269. package/lib/policy/unified-gates.mjs +96 -0
  270. package/lib/project-detection.mjs +129 -0
  271. package/lib/project-init-shared.mjs +272 -0
  272. package/lib/project-profile.mjs +447 -0
  273. package/lib/prompt-composer.js +434 -0
  274. package/lib/prompt-metadata.mjs +1 -1
  275. package/lib/provider-capabilities-anthropic.js +44 -0
  276. package/lib/provider-capabilities-deepseek.js +37 -0
  277. package/lib/provider-capabilities-generic.js +26 -0
  278. package/lib/provider-capabilities-google.js +47 -0
  279. package/lib/provider-capabilities-openai.js +45 -0
  280. package/lib/provider-capabilities.js +142 -0
  281. package/lib/providers/atlassian-confluence/index.mjs +103 -0
  282. package/lib/providers/atlassian-jira/index.mjs +100 -0
  283. package/lib/providers/auth-manager.mjs +126 -0
  284. package/lib/providers/circuit-breaker.mjs +124 -0
  285. package/lib/providers/contract.mjs +93 -0
  286. package/lib/providers/github/index.mjs +126 -0
  287. package/lib/providers/registry.mjs +184 -0
  288. package/lib/providers/salesforce/index.mjs +100 -0
  289. package/lib/providers/slack/index.mjs +80 -0
  290. package/lib/reflect.mjs +137 -0
  291. package/lib/research-lint.mjs +164 -0
  292. package/lib/resources/budget.mjs +259 -0
  293. package/lib/role-preload.mjs +21 -6
  294. package/lib/roles/approval-surface.mjs +54 -0
  295. package/lib/roles/cli.mjs +118 -0
  296. package/lib/roles/event-bus.mjs +79 -0
  297. package/lib/roles/fence.mjs +84 -0
  298. package/lib/roles/gateway.mjs +260 -0
  299. package/lib/roles/hook-emit.mjs +37 -0
  300. package/lib/roles/manifest.mjs +48 -0
  301. package/lib/roles/router.mjs +27 -0
  302. package/lib/runtime-pressure.mjs +360 -0
  303. package/lib/schema-artifact.mjs +134 -0
  304. package/lib/schema-infer.mjs +551 -0
  305. package/lib/server/auth.mjs +168 -0
  306. package/lib/server/chat.mjs +336 -0
  307. package/lib/server/cors.mjs +77 -0
  308. package/lib/server/csrf.mjs +91 -0
  309. package/lib/server/index.mjs +1927 -78
  310. package/lib/server/insights.mjs +765 -0
  311. package/lib/server/rate-limit.mjs +91 -0
  312. package/lib/server/static/assets/index-ab25c707.js +70 -0
  313. package/lib/server/static/assets/index-f0c80a2b.css +1 -0
  314. package/lib/server/static/index.html +12 -817
  315. package/lib/server/telemetry-login.mjs +108 -0
  316. package/lib/server/webhook.mjs +510 -0
  317. package/lib/service-manager.mjs +522 -58
  318. package/lib/services/pattern-promotion-service.mjs +167 -0
  319. package/lib/services/telemetry-backend.mjs +178 -0
  320. package/lib/session-store.mjs +374 -0
  321. package/lib/setup-prompts.mjs +96 -0
  322. package/lib/setup.mjs +523 -36
  323. package/lib/skills-apply.mjs +280 -0
  324. package/lib/skills-scope.mjs +118 -0
  325. package/lib/status.mjs +261 -70
  326. package/lib/storage/admin.mjs +355 -0
  327. package/lib/storage/backend.mjs +2 -1
  328. package/lib/storage/backup.mjs +347 -0
  329. package/lib/storage/embeddings-engine.mjs +133 -0
  330. package/lib/storage/embeddings-legacy.mjs +85 -0
  331. package/lib/storage/embeddings-local.mjs +108 -0
  332. package/lib/storage/embeddings-ollama.mjs +78 -0
  333. package/lib/storage/embeddings-openai.mjs +85 -0
  334. package/lib/storage/embeddings.mjs +92 -33
  335. package/lib/storage/file-lock.mjs +130 -0
  336. package/lib/storage/fusion.mjs +95 -0
  337. package/lib/storage/hybrid-query.mjs +34 -27
  338. package/lib/storage/migrations.mjs +187 -0
  339. package/lib/storage/postgres-backup.mjs +124 -0
  340. package/lib/storage/sql-store.mjs +5 -15
  341. package/lib/storage/state-source.mjs +12 -13
  342. package/lib/storage/store-version.mjs +115 -0
  343. package/lib/storage/sync.mjs +144 -35
  344. package/lib/storage/unified-storage.mjs +550 -0
  345. package/lib/storage/vector-client.mjs +286 -0
  346. package/lib/storage/vector-store.mjs +71 -30
  347. package/lib/task-graph/generate.mjs +135 -0
  348. package/lib/task-graph/schema.mjs +81 -0
  349. package/lib/task-graph/store.mjs +71 -0
  350. package/lib/telemetry/backends/local.mjs +62 -0
  351. package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
  352. package/lib/telemetry/backfill.mjs +180 -0
  353. package/lib/telemetry/eval-datasets.mjs +203 -0
  354. package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
  355. package/lib/telemetry/intent-verifications.mjs +86 -0
  356. package/lib/telemetry/llm-judge.mjs +350 -0
  357. package/lib/telemetry/model-pricing-catalog.mjs +557 -0
  358. package/lib/telemetry/setup.mjs +151 -0
  359. package/lib/telemetry/skill-calls.mjs +78 -0
  360. package/lib/telemetry/team-rollup.mjs +4 -4
  361. package/lib/token-engine.js +117 -0
  362. package/lib/token-estimator-anthropic.js +15 -0
  363. package/lib/token-estimator-deepseek.js +13 -0
  364. package/lib/token-estimator-default.js +13 -0
  365. package/lib/token-estimator-google.js +13 -0
  366. package/lib/token-estimator-openai.js +13 -0
  367. package/lib/toolkit-env.mjs +1 -1
  368. package/lib/tty-prompts.mjs +211 -0
  369. package/lib/uninstall/uninstall.mjs +423 -0
  370. package/lib/update.mjs +115 -0
  371. package/lib/upgrade.mjs +141 -0
  372. package/lib/validator.mjs +51 -2
  373. package/lib/validators/skills.mjs +142 -0
  374. package/lib/wireframe.mjs +422 -0
  375. package/lib/worker/entrypoint.mjs +241 -0
  376. package/lib/worker/evidence.mjs +107 -0
  377. package/lib/worker/run.mjs +154 -0
  378. package/lib/worker/trace.mjs +182 -0
  379. package/lib/workflow-state.mjs +14 -18
  380. package/package.json +21 -8
  381. package/personas/construct.md +53 -51
  382. package/platforms/claude/CLAUDE.md +1 -1
  383. package/platforms/claude/settings.template.json +115 -68
  384. package/platforms/opencode/config.template.json +3 -7
  385. package/rules/common/agents.md +11 -38
  386. package/rules/common/beads-hygiene.md +75 -0
  387. package/rules/common/code-review.md +11 -100
  388. package/rules/common/coding-style.md +5 -3
  389. package/rules/common/comments.md +30 -111
  390. package/rules/common/commit-approval.md +53 -0
  391. package/rules/common/cx-agent-routing.md +1 -0
  392. package/rules/common/development-workflow.md +23 -41
  393. package/rules/common/doc-ownership.md +54 -0
  394. package/rules/common/efficiency.md +57 -0
  395. package/rules/common/framing.md +76 -0
  396. package/rules/common/git-workflow.md +2 -4
  397. package/rules/common/patterns.md +3 -7
  398. package/rules/common/performance.md +15 -31
  399. package/rules/common/release-gates.md +69 -0
  400. package/rules/common/research.md +107 -0
  401. package/rules/common/security.md +6 -6
  402. package/rules/common/skill-composition.md +67 -0
  403. package/rules/common/testing.md +15 -27
  404. package/rules/golang/hooks.md +0 -4
  405. package/rules/policy/bootstrap.yaml +11 -0
  406. package/rules/policy/drive.yaml +8 -0
  407. package/rules/policy/task.yaml +9 -0
  408. package/rules/policy/workflow.yaml +9 -0
  409. package/rules/python/hooks.md +0 -4
  410. package/rules/swift/hooks.md +0 -4
  411. package/rules/typescript/hooks.md +0 -4
  412. package/rules/web/hooks.md +0 -2
  413. package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
  414. package/skills/ai/prompt-optimizer.md +7 -7
  415. package/skills/compliance/ai-disclosure.md +58 -0
  416. package/skills/compliance/data-privacy.md +46 -0
  417. package/skills/compliance/license-audit.md +40 -0
  418. package/skills/compliance/regulatory-review.md +61 -0
  419. package/skills/docs/document-ingest-workflow.md +52 -0
  420. package/skills/docs/evidence-ingest-workflow.md +9 -0
  421. package/skills/docs/init-docs.md +51 -18
  422. package/skills/docs/product-intelligence-workflow.md +12 -0
  423. package/skills/docs/product-signal-workflow.md +4 -0
  424. package/skills/docs/research-workflow.md +23 -8
  425. package/skills/docs/runbook-workflow.md +1 -1
  426. package/skills/operating/orchestration-reference.md +151 -0
  427. package/skills/roles/architect.md +5 -0
  428. package/skills/roles/engineer.md +32 -0
  429. package/skills/roles/operator.md +12 -0
  430. package/skills/roles/reviewer.md +23 -0
  431. package/skills/routing.md +1 -1
  432. package/templates/devcontainer/Dockerfile.devcontainer +38 -0
  433. package/templates/devcontainer/devcontainer.json +31 -0
  434. package/templates/distribution/bootstrap.ps1 +142 -0
  435. package/templates/distribution/bootstrap.sh +196 -0
  436. package/templates/distribution/run.mjs +187 -0
  437. package/templates/docs/adr.md +44 -8
  438. package/templates/docs/changelog-entry.md +43 -0
  439. package/templates/docs/construct_guide.md +149 -0
  440. package/templates/docs/evidence-brief.md +2 -2
  441. package/templates/docs/meta-prd.md +140 -19
  442. package/templates/docs/onboarding.md +57 -0
  443. package/templates/docs/prd.md +159 -15
  444. package/templates/docs/research-brief.md +4 -4
  445. package/templates/homebrew/construct.rb +67 -0
  446. package/langfuse/docker-compose.yml +0 -82
  447. package/lib/eval-harness.mjs +0 -59
  448. package/lib/hooks/bootstrap-guard.mjs +0 -90
  449. package/lib/hooks/console-warn.mjs +0 -43
  450. package/lib/hooks/continuation-enforcer.mjs +0 -72
  451. package/lib/hooks/drive-guard.mjs +0 -89
  452. package/lib/hooks/mcp-task-scope.mjs +0 -47
  453. package/lib/hooks/task-completed-guard.mjs +0 -43
  454. package/lib/hooks/teammate-idle-guard.mjs +0 -54
  455. package/lib/hooks/workflow-guard.mjs +0 -62
  456. package/lib/prompt-composer.mjs +0 -196
  457. package/lib/review.mjs +0 -429
  458. package/lib/server/static/app.js +0 -841
  459. package/lib/telemetry/langfuse-model-sync.mjs +0 -270
  460. package/rules/common/hooks.md +0 -35
  461. package/rules/zh/README.md +0 -113
  462. package/rules/zh/agents.md +0 -55
  463. package/rules/zh/code-review.md +0 -129
  464. package/rules/zh/coding-style.md +0 -53
  465. package/rules/zh/development-workflow.md +0 -49
  466. package/rules/zh/git-workflow.md +0 -29
  467. package/rules/zh/hooks.md +0 -35
  468. package/rules/zh/patterns.md +0 -36
  469. package/rules/zh/performance.md +0 -60
  470. package/rules/zh/security.md +0 -34
  471. package/rules/zh/testing.md +0 -34
@@ -87,7 +87,9 @@ export function buildCodexMcpEntry(id, mcpDef, resolvedValues = {}) {
87
87
  tool_timeout_sec: 60,
88
88
  };
89
89
 
90
- if (id !== 'github') {
90
+ if (id === 'github') {
91
+ entry.bearer_token_env_var = 'GITHUB_TOKEN';
92
+ } else {
91
93
  const authorization = mcpDef.headers?.Authorization;
92
94
  const match = typeof authorization === 'string' ? authorization.match(/^Bearer __([A-Z0-9_]+)__$/) : null;
93
95
  if (match) entry.bearer_token_env_var = match[1];
@@ -35,8 +35,13 @@ function relPath(rootDir, absPath) {
35
35
 
36
36
  function requiresHeader(rel) {
37
37
  const ext = path.extname(rel);
38
+ if (['.yaml', '.yml', '.json', '.jsonl', '.toml'].includes(ext)) return { required: false, type: null };
38
39
  const jsMatch = JS_HEADER_GLOBS.some(r => r.test(rel));
39
40
  const mdMatch = MD_HEADER_GLOBS.some(r => r.test(rel));
41
+ // Exclude static assets under lib/server/static from requiring headers
42
+ if (rel.startsWith('lib/server/static/')) {
43
+ return { required: false, type: null };
44
+ }
40
45
  const type = (jsMatch && ext !== '.html') ? 'js' : (jsMatch || mdMatch) ? 'md' : null;
41
46
  return { required: jsMatch || mdMatch, type };
42
47
  }
@@ -54,22 +59,66 @@ function hasHeader(content, type) {
54
59
  }
55
60
 
56
61
  // --- banned comment patterns ---
62
+ //
63
+ // Comment-line prefix matches both `//` line comments and JSDoc body lines
64
+ // (those start with optional whitespace + `*`). The leading `^\s*\*(?!\/)`
65
+ // keeps it from matching `*/` block-end lines or arithmetic.
66
+
67
+ const CMT = '(?:\\/\\/|^\\s*\\*(?!\\/))';
57
68
 
58
69
  const BANNED = [
59
- { pattern: /\/\/.*\b(?:added for|added recently|recently added|just added|new:)\b/i, label: 'point-in-time: "added for / recently / just / new:"' },
60
- { pattern: /\/\/.*\b(?:used by|called from|only consumer)\b/i, label: 'caller reference: "used by / called from"' },
61
- { pattern: /\/\/.*\bpreviously\b.*\bwe used to\b/i, label: 'point-in-time: "previously / we used to"' },
62
- { pattern: /\/\/.*(?:#\d{3,}|GH-\d+|JIRA-\d+|closes #|fixes #|ticket )/i, label: 'issue/PR reference in source comment (put in commit message instead)' },
70
+ // point-in-time
71
+ { pattern: new RegExp(`${CMT}.*\\b(?:added for|added recently|recently added|just added|new:)\\b`, 'i'), label: 'point-in-time: "added for / recently / just / new:"' },
72
+ { pattern: new RegExp(`${CMT}.*\\b(?:previously|no longer|used to|was replaced|replaced by)\\b`, 'i'), label: 'point-in-time: history belongs in git log, not source' },
73
+ { pattern: new RegExp(`${CMT}.*\\b(?:replaces the (?:old|previous|prior)|consolidates what was|instead of (?:only|the|a) (?:old|prior|previous))\\b`, 'i'), label: 'comparative narrative: describe what the code does, not what it used to do' },
74
+ { pattern: new RegExp(`${CMT}.*\\b(?:the (?:new|current) (?:path|approach|impl|implementation|design) (?:uses|writes|reads|does|fires|spins))\\b`, 'i'), label: 'comparative narrative: "the new X uses/does Y" — describe the design directly' },
75
+ { pattern: new RegExp(`${CMT}.*(?:#\\d{3,}|GH-\\d+|JIRA-\\d+|closes #|fixes #|ticket )`, 'i'), label: 'issue/PR reference in source comment (put in commit message instead)' },
76
+ // narrative voice
77
+ { pattern: new RegExp(`${CMT}\\s+(?:We |This |It |Now )\\w`, 'i'), label: 'narrative voice: avoid "We/This/It/Now" — describe the constraint, not the story' },
78
+ // noise sentinels
79
+ { pattern: new RegExp(`${CMT}\\s*(?:ok|OK|skip|Skip|best effort)\\s*$`, 'i'), label: 'noise sentinel: "ok / skip / best effort" carries no decision content' },
80
+ // caller references
81
+ { pattern: new RegExp(`${CMT}.*\\b(?:used by|called from|only consumer)\\b`, 'i'), label: 'caller reference: "used by / called from"' },
82
+ // step markers — only flag in `//` comments. Ordered lists are
83
+ // conventional inside JSDoc decision-tree and flow documentation.
84
+ { pattern: /\/\/\s+\d+\.\s+\w/, label: 'step marker: use function names or block structure instead of "// 1. Step"' },
85
+ // markdown equivalents
63
86
  { pattern: /<!--.*\b(?:added for|added recently|just added|new:)\b.*-->/i, label: 'point-in-time in markdown comment' },
64
87
  { pattern: /<!--.*\b(?:used by|called from|only consumer)\b.*-->/i, label: 'caller reference in markdown comment' },
65
- { pattern: /\/\/\s*TODO(?:\((\w+)\))?:?(?!\s*\(\w+\):)/i, label: 'TODO without owner — use: TODO(owner): what and why' },
88
+ // TODO without owner
89
+ { pattern: new RegExp(`${CMT}\\s*TODO(?:\\((\\w+)\\))?:?(?!\\s*\\(\\w+\\):)`, 'i'), label: 'TODO without owner — use: TODO(owner): what and why' }, // construct-lint-ignore
90
+ ];
91
+
92
+ // Files whose content is authored precisely to demonstrate banned patterns
93
+ // (the linter itself, the canonical rule file, test fixtures that feed the
94
+ // linter banned strings, and teaching docs that show patterns to avoid).
95
+ // Linting them would flag the demonstrations as violations.
96
+ const BANNED_CHECK_SKIP = [
97
+ 'lib/comment-lint.mjs',
98
+ 'rules/common/comments.md',
99
+ 'tests/comment-lint.test.mjs',
100
+ 'commands/work/clean.md',
101
+ 'skills/utility/clean-code.md',
66
102
  ];
67
103
 
104
+ function isBannedCheckSkipped(filePath) {
105
+ if (!filePath) return false;
106
+ return BANNED_CHECK_SKIP.some((suffix) => filePath.endsWith(suffix));
107
+ }
108
+
68
109
  function checkBanned(content, filePath) {
69
- if (filePath && (filePath.endsWith('comment-lint.mjs') || filePath.endsWith('comments.md'))) return [];
110
+ if (isBannedCheckSkipped(filePath)) return [];
111
+
112
+ // Banned patterns are JS/TS comment conventions — they don't apply to markdown prose.
113
+ if (filePath && path.extname(filePath) === '.md') return [];
114
+
70
115
  const warnings = [];
71
116
  const lines = content.split('\n');
72
117
  for (let i = 0; i < lines.length; i++) {
118
+ // Per-line escape hatch for the rare legitimate case — a section label
119
+ // describing the pattern it detects, e.g. "// TODO/FIXME/HACK comments"
120
+ // in a detector. Marker must appear on the same line.
121
+ if (lines[i].includes('construct-lint-ignore')) continue;
73
122
  for (const { pattern, label } of BANNED) {
74
123
  if (pattern.test(lines[i])) {
75
124
  warnings.push({ line: i + 1, label });
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * completions.mjs — generates bash and zsh completion scripts for construct.
3
3
  *
4
- * Called by sync-agents.mjs after each sync.
4
+ * Called by scripts/sync-agents.mjs after each sync.
5
5
  * Outputs to a platform-appropriate per-user completions directory.
6
6
  */
7
7
 
@@ -14,13 +14,22 @@ const COMPLETIONS_DIR = process.platform === 'win32'
14
14
  ? join(process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming'), 'construct', 'completions')
15
15
  : join(homedir(), '.local', 'share', 'construct', 'completions');
16
16
 
17
+ function subcommandName(subcommand) {
18
+ return typeof subcommand === 'string' ? subcommand : subcommand.name;
19
+ }
20
+
21
+ function subcommandDesc(subcommand) {
22
+ if (typeof subcommand === 'string') return '';
23
+ return subcommand.desc || '';
24
+ }
25
+
17
26
  function buildBash() {
18
27
  const names = CLI_COMMANDS.map(c => c.name).join(' ');
19
28
 
20
29
  const subcommandCases = CLI_COMMANDS
21
30
  .filter(c => c.subcommands?.length)
22
31
  .map(c => {
23
- const subs = c.subcommands.map(s => s.name).join(' ');
32
+ const subs = c.subcommands.map(subcommandName).filter(Boolean).join(' ');
24
33
  return ` ${c.name}) COMPREPLY=($(compgen -W "${subs}" -- "$cur")) ;;`;
25
34
  })
26
35
  .join('\n');
@@ -86,21 +95,21 @@ complete -F _construct_completions construct
86
95
 
87
96
  function buildZsh() {
88
97
  const commandDescriptions = CLI_COMMANDS
89
- .map(c => ` '${c.name}:${c.emoji} ${c.description.replace(/'/g, "\\'")}' \\`)
98
+ .map(c => ` '${c.name}:${c.emoji || ''} ${(c.description || '').replace(/'/g, "\\'")}' \\`)
90
99
  .join('\n');
91
100
 
92
101
  const subcommandFunctions = CLI_COMMANDS
93
102
  .filter(c => c.subcommands?.length)
94
103
  .map(c => {
95
104
  const subs = c.subcommands
96
- .map(s => ` '${s.name}:${s.desc.replace(/'/g, "\\'")}'`)
105
+ .map(s => ` '${(subcommandName(s) || '')}:${subcommandDesc(s).replace(/'/g, "\\'")}'`)
97
106
  .join('\n');
98
- return `_construct_${c.name.replace(/-/g, '_')}() {
107
+ return `_construct_${(c.name || '').replace(/-/g, '_')}() {
99
108
  local subcmds
100
109
  subcmds=(
101
110
  ${subs}
102
111
  )
103
- _describe -t subcommands '${c.name} subcommand' subcmds
112
+ _describe -t subcommands '${c.name}' subcmds
104
113
  }`;
105
114
  })
106
115
  .join('\n\n');
@@ -153,6 +162,12 @@ _construct
153
162
  `;
154
163
  }
155
164
 
165
+ export function getCompletionScript(shell) {
166
+ if (shell === 'bash') return buildBash();
167
+ if (shell === 'zsh') return buildZsh();
168
+ throw new Error(`Unsupported shell: ${shell}`);
169
+ }
170
+
156
171
  export function generateCompletions() {
157
172
  try {
158
173
  mkdirSync(COMPLETIONS_DIR, { recursive: true });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * lib/config/alias.mjs — resolves the user-facing Construct alias.
3
+ *
4
+ * Lookup precedence:
5
+ * 1. CONSTRUCT_ALIAS env var (per-session override)
6
+ * 2. ~/.construct/config.json.aliasOverride (user-level, lets each
7
+ * user on the same project see their own branding without
8
+ * committing it)
9
+ * 3. construct.config.json.alias (project-level, committed)
10
+ * 4. "Construct" (the default)
11
+ *
12
+ * Pure resolution — no fs writes. The alias flows to the dashboard
13
+ * wordmark (via /api/alias), persona prompts ({{alias}} template at
14
+ * sync time), and CLI banner output.
15
+ */
16
+ import fs from 'node:fs';
17
+ import os from 'node:os';
18
+ import path from 'node:path';
19
+ import { loadProjectConfig } from './project-config.mjs';
20
+
21
+ export const DEFAULT_ALIAS = 'Construct';
22
+ export const ALIAS_ENV_KEY = 'CONSTRUCT_ALIAS';
23
+ export const ALIAS_TEMPLATE_TOKEN = /\{\{alias\}\}/g;
24
+
25
+ export function resolveAlias({ cwd = process.cwd(), env = process.env, homeDir = os.homedir() } = {}) {
26
+ const envAlias = env?.[ALIAS_ENV_KEY];
27
+ if (typeof envAlias === 'string' && envAlias.trim()) {
28
+ return { value: envAlias.trim(), source: 'env' };
29
+ }
30
+
31
+ try {
32
+ const userCfgPath = path.join(homeDir, '.construct', 'config.json');
33
+ if (fs.existsSync(userCfgPath)) {
34
+ const userCfg = JSON.parse(fs.readFileSync(userCfgPath, 'utf8'));
35
+ const userAlias = userCfg?.aliasOverride;
36
+ if (typeof userAlias === 'string' && userAlias.trim()) {
37
+ return { value: userAlias.trim(), source: 'user' };
38
+ }
39
+ }
40
+ } catch { /* user file is best-effort */ }
41
+
42
+ try {
43
+ const { config, source } = loadProjectConfig(cwd, env);
44
+ if (source === 'file' && typeof config?.alias === 'string' && config.alias.trim()) {
45
+ return { value: config.alias.trim(), source: 'project' };
46
+ }
47
+ } catch { /* project config is best-effort */ }
48
+
49
+ return { value: DEFAULT_ALIAS, source: 'default' };
50
+ }
51
+
52
+ export function applyAliasToTemplate(text, alias) {
53
+ if (typeof text !== 'string') return text;
54
+ const safe = String(alias ?? DEFAULT_ALIAS).trim() || DEFAULT_ALIAS;
55
+ return text.replace(ALIAS_TEMPLATE_TOKEN, safe);
56
+ }
@@ -0,0 +1,335 @@
1
+ /**
2
+ * lib/config/project-config.mjs — read and write construct.config.json.
3
+ *
4
+ * Source-of-truth loader for project-level Construct settings. Resolves
5
+ * the path by walking up from cwd (stops at git root or filesystem root),
6
+ * validates against the v1 schema, and applies secret interpolation:
7
+ * any string value of the form `$VAR_NAME` is replaced with the
8
+ * corresponding env-var value at load time. Pointers keep API keys in
9
+ * `.env` where they belong; the JSON config never sees a secret literal.
10
+ *
11
+ * Precedence rule for consumers: env var if set > config.json > default.
12
+ * `resolveSetting(config, jsonPath, env, envKey, default)` encodes it
13
+ * once so call sites stay consistent.
14
+ */
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { CONFIG_SCHEMA_VERSION, DEFAULT_PROJECT_CONFIG, validateProjectConfig, FIELD_RULES } from './schema.mjs';
18
+
19
+ export const PROJECT_CONFIG_FILENAME = 'construct.config.json';
20
+
21
+ export function findProjectConfigPath(cwd = process.cwd()) {
22
+ let dir = path.resolve(cwd);
23
+ const root = path.parse(dir).root;
24
+ while (dir !== root) {
25
+ const candidate = path.join(dir, PROJECT_CONFIG_FILENAME);
26
+ if (fs.existsSync(candidate)) return candidate;
27
+ if (fs.existsSync(path.join(dir, '.git'))) {
28
+ const inGitRoot = path.join(dir, PROJECT_CONFIG_FILENAME);
29
+ return fs.existsSync(inGitRoot) ? inGitRoot : null;
30
+ }
31
+ dir = path.dirname(dir);
32
+ }
33
+ return null;
34
+ }
35
+
36
+ const ENV_POINTER_RE = /^\$([A-Z_][A-Z0-9_]*)$/;
37
+
38
+ export function interpolateSecrets(value, env = process.env) {
39
+ if (typeof value === 'string') {
40
+ const match = value.match(ENV_POINTER_RE);
41
+ if (!match) return value;
42
+ const resolved = env[match[1]];
43
+ return resolved === undefined ? null : resolved;
44
+ }
45
+ if (Array.isArray(value)) return value.map((v) => interpolateSecrets(v, env));
46
+ if (value !== null && typeof value === 'object') {
47
+ const out = {};
48
+ for (const [k, v] of Object.entries(value)) out[k] = interpolateSecrets(v, env);
49
+ return out;
50
+ }
51
+ return value;
52
+ }
53
+
54
+ function deepMerge(base, override) {
55
+ if (override === undefined) return base;
56
+ if (override === null || typeof override !== 'object' || Array.isArray(override)) return override;
57
+ const out = Array.isArray(base) ? [...(base || [])] : { ...(base || {}) };
58
+ for (const [k, v] of Object.entries(override)) {
59
+ out[k] = deepMerge(base?.[k], v);
60
+ }
61
+ return out;
62
+ }
63
+
64
+ export function loadProjectConfig(cwd = process.cwd(), env = process.env) {
65
+ const configPath = findProjectConfigPath(cwd);
66
+ if (!configPath) {
67
+ return {
68
+ path: null,
69
+ raw: null,
70
+ config: structuredClone(DEFAULT_PROJECT_CONFIG),
71
+ source: 'default',
72
+ errors: [],
73
+ };
74
+ }
75
+ let raw;
76
+ try {
77
+ raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
78
+ } catch (err) {
79
+ return {
80
+ path: configPath,
81
+ raw: null,
82
+ config: structuredClone(DEFAULT_PROJECT_CONFIG),
83
+ source: 'invalid',
84
+ errors: [`failed to parse ${configPath}: ${err.message}`],
85
+ };
86
+ }
87
+ const validation = validateProjectConfig(raw);
88
+ if (!validation.valid) {
89
+ return {
90
+ path: configPath,
91
+ raw,
92
+ config: structuredClone(DEFAULT_PROJECT_CONFIG),
93
+ source: 'invalid',
94
+ errors: validation.errors,
95
+ };
96
+ }
97
+ const merged = deepMerge(structuredClone(DEFAULT_PROJECT_CONFIG), raw);
98
+ const resolved = interpolateSecrets(merged, env);
99
+ return {
100
+ path: configPath,
101
+ raw,
102
+ config: resolved,
103
+ source: 'file',
104
+ errors: [],
105
+ };
106
+ }
107
+
108
+ export function writeProjectConfig(filePath, config, options = {}) {
109
+ const {
110
+ validate = true, // Perform validation before write
111
+ interactive = false, // Prompt for fixes in interactive mode
112
+ dryRun = false, // Validate only, don't write
113
+ strict = false, // Reject unknown fields
114
+ } = options;
115
+
116
+ // Pre-validation: Check for common mistakes
117
+ const preErrors = [];
118
+ const warnings = [];
119
+
120
+ // Check 1: Version mismatch
121
+ if (config.version && config.version !== CONFIG_SCHEMA_VERSION) {
122
+ preErrors.push(`version mismatch: file has v${config.version}, expected v${CONFIG_SCHEMA_VERSION}`);
123
+ }
124
+
125
+ // Check 2: Unknown top-level fields (warning in non-strict mode)
126
+ if (typeof config === 'object' && config !== null) {
127
+ const knownFields = Object.keys(FIELD_RULES);
128
+ const unknownFields = Object.keys(config).filter(k => !knownFields.includes(k));
129
+ if (unknownFields.length > 0) {
130
+ const msg = `unknown fields will be ignored: ${unknownFields.join(', ')}`;
131
+ if (strict) {
132
+ preErrors.push(msg);
133
+ } else {
134
+ warnings.push(msg);
135
+ }
136
+ }
137
+ }
138
+
139
+ // Check 3: Secret literals (security warning)
140
+ const secretPattern = /(api[_-]?key|token|secret|password)/i;
141
+ function findSecretLiterals(obj, path = '') {
142
+ const found = [];
143
+ for (const [key, value] of Object.entries(obj)) {
144
+ const currentPath = path ? `${path}.${key}` : key;
145
+ if (secretPattern.test(key) && typeof value === 'string' && !value.startsWith('$')) {
146
+ found.push(currentPath);
147
+ }
148
+ if (typeof value === 'object' && value !== null) {
149
+ found.push(...findSecretLiterals(value, currentPath));
150
+ }
151
+ }
152
+ return found;
153
+ }
154
+
155
+ const secretPaths = findSecretLiterals(config);
156
+ if (secretPaths.length > 0) {
157
+ warnings.push(`potential secrets detected at: ${secretPaths.join(', ')}. Use "$ENV_VAR" syntax instead.`);
158
+ }
159
+
160
+ // Run full schema validation
161
+ let validation = { valid: true, errors: [] };
162
+ if (validate) {
163
+ validation = validateProjectConfig(config);
164
+ }
165
+
166
+ // Combine all errors (handle case where validation.errors is undefined)
167
+ const validationErrors = validation.errors || [];
168
+ const allErrors = [...preErrors, ...validationErrors];
169
+
170
+ // Build comprehensive feedback
171
+ const feedback = {
172
+ valid: allErrors.length === 0,
173
+ errors: allErrors,
174
+ warnings,
175
+ filePath,
176
+ wouldWrite: !dryRun && allErrors.length === 0,
177
+ };
178
+
179
+ // Throw on validation errors
180
+ if (allErrors.length > 0) {
181
+ const errorMessage = formatValidationErrors(allErrors, warnings, filePath);
182
+ throw new Error(errorMessage);
183
+ }
184
+
185
+ // Log warnings
186
+ if (warnings.length > 0 && !options.silent) {
187
+ console.error('Config warnings:');
188
+ warnings.forEach(w => console.error(` ⚠ ${w}`));
189
+ }
190
+
191
+ // Perform dry-run or actual write
192
+ if (dryRun) {
193
+ feedback.written = false;
194
+ return feedback;
195
+ }
196
+
197
+ // Write the file
198
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
199
+ fs.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`);
200
+
201
+ feedback.written = true;
202
+ return feedback;
203
+ }
204
+
205
+ function formatValidationErrors(errors, warnings, filePath) {
206
+ let msg = `refusing to write invalid config: ${errors.join('; ')}`;
207
+ msg += `\n\nInvalid config (${filePath}):\n\n`;
208
+
209
+ msg += 'Errors:\n';
210
+ errors.forEach(e => {
211
+ msg += ` ✗ ${e}\n`;
212
+ });
213
+
214
+ if (warnings.length > 0) {
215
+ msg += '\nWarnings:\n';
216
+ warnings.forEach(w => {
217
+ msg += ` ⚠ ${w}\n`;
218
+ });
219
+ }
220
+
221
+ msg += '\nFix these issues before the config can be saved.';
222
+ msg += '\nRun with --dry-run to validate without writing.';
223
+
224
+ return msg;
225
+ }
226
+
227
+ export function initProjectConfig(cwd = process.cwd(), overrides = {}) {
228
+ const filePath = path.join(cwd, PROJECT_CONFIG_FILENAME);
229
+ if (fs.existsSync(filePath)) {
230
+ throw new Error(`${PROJECT_CONFIG_FILENAME} already exists at ${filePath}`);
231
+ }
232
+ const config = deepMerge(structuredClone(DEFAULT_PROJECT_CONFIG), { version: CONFIG_SCHEMA_VERSION, ...overrides });
233
+ writeProjectConfig(filePath, config);
234
+ return filePath;
235
+ }
236
+
237
+ export function getConfigValue(config, keyPath, defaultValue) {
238
+ if (!keyPath) return config;
239
+ const parts = keyPath.split('.');
240
+ let cur = config;
241
+ for (const part of parts) {
242
+ if (cur === null || typeof cur !== 'object') return defaultValue;
243
+ cur = cur[part];
244
+ }
245
+ return cur === undefined ? defaultValue : cur;
246
+ }
247
+
248
+ export function setConfigValue(config, keyPath, value) {
249
+ if (!keyPath) throw new Error('keyPath required');
250
+ const parts = keyPath.split('.');
251
+ const out = structuredClone(config);
252
+ let cur = out;
253
+ for (let i = 0; i < parts.length - 1; i++) {
254
+ const part = parts[i];
255
+ if (cur[part] === null || typeof cur[part] !== 'object') cur[part] = {};
256
+ cur = cur[part];
257
+ }
258
+ cur[parts[parts.length - 1]] = value;
259
+ return out;
260
+ }
261
+
262
+ /**
263
+ * Set a config value with immediate validation and feedback.
264
+ * Recommended way to update configuration from CLI/commands.
265
+ *
266
+ * @param {string} keyPath - Dot-notation path (e.g., 'deployment.mode')
267
+ * @param {any} value - Value to set
268
+ * @param {Object} options
269
+ * @returns {Object} Validation result with immediate feedback
270
+ */
271
+ export function setConfigValueWithValidation(keyPath, value, options = {}) {
272
+ const { cwd = process.cwd(), dryRun = false } = options;
273
+
274
+ // Load current config
275
+ const current = loadProjectConfig(cwd);
276
+ const configPath = current.path || path.join(cwd, PROJECT_CONFIG_FILENAME);
277
+
278
+ // Apply the change
279
+ const newConfig = setConfigValue(current.config, keyPath, value);
280
+
281
+ // Validate before writing
282
+ const validation = validateProjectConfig(newConfig);
283
+
284
+ if (!validation.valid) {
285
+ return {
286
+ success: false,
287
+ keyPath,
288
+ value,
289
+ errors: validation.errors,
290
+ message: `Cannot set ${keyPath}: validation failed`,
291
+ };
292
+ }
293
+
294
+ // Write if not dry-run
295
+ if (!dryRun) {
296
+ try {
297
+ writeProjectConfig(configPath, newConfig, { validate: true });
298
+ return {
299
+ success: true,
300
+ keyPath,
301
+ value,
302
+ configPath,
303
+ message: `Set ${keyPath} = ${JSON.stringify(value)}`,
304
+ };
305
+ } catch (error) {
306
+ return {
307
+ success: false,
308
+ keyPath,
309
+ value,
310
+ errors: [error.message],
311
+ message: `Failed to write config: ${error.message}`,
312
+ };
313
+ }
314
+ }
315
+
316
+ // Dry-run mode
317
+ return {
318
+ success: true,
319
+ keyPath,
320
+ value,
321
+ dryRun: true,
322
+ message: `Would set ${keyPath} = ${JSON.stringify(value)} (dry-run)`,
323
+ };
324
+ }
325
+
326
+ export function resolveSetting({ config, jsonPath, env, envKey, defaultValue }) {
327
+ if (env && envKey && env[envKey] !== undefined && env[envKey] !== '') {
328
+ return { value: env[envKey], source: 'env', envKey };
329
+ }
330
+ const fromJson = getConfigValue(config, jsonPath, undefined);
331
+ if (fromJson !== undefined && fromJson !== null) {
332
+ return { value: fromJson, source: 'config', jsonPath };
333
+ }
334
+ return { value: defaultValue, source: 'default' };
335
+ }