@geraldmaron/construct 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.env.example +9 -6
  2. package/LICENSE +201 -21
  3. package/README.md +132 -257
  4. package/agents/contracts.json +387 -0
  5. package/agents/prompts/cx-accessibility.md +8 -0
  6. package/agents/prompts/cx-ai-engineer.md +92 -0
  7. package/agents/prompts/cx-architect.md +10 -2
  8. package/agents/prompts/cx-business-strategist.md +13 -0
  9. package/agents/prompts/cx-data-analyst.md +80 -0
  10. package/agents/prompts/cx-data-engineer.md +4 -0
  11. package/agents/prompts/cx-debugger.md +8 -0
  12. package/agents/prompts/cx-designer.md +19 -0
  13. package/agents/prompts/cx-devil-advocate.md +4 -0
  14. package/agents/prompts/cx-docs-keeper.md +128 -0
  15. package/agents/prompts/cx-engineer.md +13 -0
  16. package/agents/prompts/cx-evaluator.md +4 -0
  17. package/agents/prompts/cx-explorer.md +12 -0
  18. package/agents/prompts/cx-legal-compliance.md +13 -0
  19. package/agents/prompts/cx-operations.md +13 -0
  20. package/agents/prompts/cx-orchestrator.md +107 -4
  21. package/agents/prompts/cx-platform-engineer.md +75 -0
  22. package/agents/prompts/cx-product-manager.md +8 -0
  23. package/agents/prompts/cx-qa.md +100 -0
  24. package/agents/prompts/cx-rd-lead.md +13 -0
  25. package/agents/prompts/cx-release-manager.md +8 -0
  26. package/agents/prompts/cx-researcher.md +21 -1
  27. package/agents/prompts/cx-reviewer.md +8 -0
  28. package/agents/prompts/cx-security.md +104 -0
  29. package/agents/prompts/cx-sre.md +104 -0
  30. package/agents/prompts/cx-test-automation.md +4 -0
  31. package/agents/prompts/cx-trace-reviewer.md +7 -3
  32. package/agents/prompts/cx-ux-researcher.md +4 -0
  33. package/agents/registry.json +365 -90
  34. package/agents/role-manifests.json +217 -0
  35. package/bin/construct +3345 -141
  36. package/bin/construct-postinstall.mjs +87 -0
  37. package/commands/build/feature.md +6 -6
  38. package/commands/plan/feature.md +6 -5
  39. package/commands/plan/requirements.md +1 -1
  40. package/commands/ship/status.md +1 -1
  41. package/commands/work/drive.md +3 -3
  42. package/commands/work/optimize-prompts.md +3 -3
  43. package/db/{migrations → schema}/001_init.sql +2 -0
  44. package/db/schema/002_pgvector.sql +182 -0
  45. package/db/schema/003_intake.sql +47 -0
  46. package/examples/README.md +85 -0
  47. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
  48. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
  49. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
  50. package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
  51. package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
  52. package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
  53. package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
  54. package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
  55. package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
  56. package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
  57. package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
  58. package/examples/personas/construct/bad/commit-without-approval.md +30 -0
  59. package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
  60. package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
  61. package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
  62. package/examples/provider-plugin/README.md +34 -0
  63. package/examples/provider-plugin/index.mjs +74 -0
  64. package/examples/provider-plugin/package.json +15 -0
  65. package/examples/seed-observations/README.md +38 -0
  66. package/examples/seed-observations/anti-patterns.md +44 -0
  67. package/examples/seed-observations/decisions.md +36 -0
  68. package/examples/seed-observations/patterns.md +42 -0
  69. package/lib/agent-contracts-enforce.mjs +158 -0
  70. package/lib/agent-contracts.mjs +231 -0
  71. package/lib/agents/postconditions.mjs +126 -0
  72. package/lib/agents/schema.mjs +124 -0
  73. package/lib/artifact-capture.mjs +183 -0
  74. package/lib/audit-trail.mjs +149 -0
  75. package/lib/auto-docs.mjs +245 -65
  76. package/lib/beads/auto-close.mjs +126 -0
  77. package/lib/beads/drift.mjs +171 -0
  78. package/lib/beads-automation.mjs +542 -0
  79. package/lib/beads-client.mjs +518 -0
  80. package/lib/beads-lock.mjs +377 -0
  81. package/lib/beads-optimistic.mjs +365 -0
  82. package/lib/bootstrap/built-ins.mjs +136 -0
  83. package/lib/bootstrap/lazy-install.mjs +161 -0
  84. package/lib/bootstrap/resources.mjs +120 -0
  85. package/lib/bootstrap.mjs +105 -0
  86. package/lib/cache-governor.js +213 -0
  87. package/lib/cache-strategy-anthropic.js +62 -0
  88. package/lib/cache-strategy-google.js +79 -0
  89. package/lib/cache-strategy-none.js +30 -0
  90. package/lib/cache-strategy-openai.js +48 -0
  91. package/lib/cache-strategy.js +91 -0
  92. package/lib/claude-allow.mjs +149 -0
  93. package/lib/cli-commands.mjs +413 -258
  94. package/lib/codex-config.mjs +3 -1
  95. package/lib/comment-lint.mjs +55 -6
  96. package/lib/completions.mjs +21 -6
  97. package/lib/config/alias.mjs +56 -0
  98. package/lib/config/project-config.mjs +335 -0
  99. package/lib/config/schema.mjs +159 -0
  100. package/lib/context-router.mjs +308 -0
  101. package/lib/cost-ledger.mjs +177 -0
  102. package/lib/cost.mjs +171 -10
  103. package/lib/dashboard-static.mjs +158 -0
  104. package/lib/deployment-mode.mjs +86 -0
  105. package/lib/deprecate.mjs +49 -0
  106. package/lib/dispatch-batch.js +183 -0
  107. package/lib/distill.mjs +21 -8
  108. package/lib/doc-stamp.mjs +164 -0
  109. package/lib/doc-verify.mjs +119 -0
  110. package/lib/docs-routing.mjs +89 -0
  111. package/lib/docs-verify.mjs +417 -0
  112. package/lib/doctor/audit.mjs +71 -0
  113. package/lib/doctor/cli.mjs +99 -0
  114. package/lib/doctor/escalate.mjs +29 -0
  115. package/lib/doctor/index.mjs +140 -0
  116. package/lib/doctor/report.mjs +170 -0
  117. package/lib/doctor/watchers/bd-watch.mjs +117 -0
  118. package/lib/doctor/watchers/cost.mjs +130 -0
  119. package/lib/doctor/watchers/disk.mjs +122 -0
  120. package/lib/doctor/watchers/handoffs.mjs +33 -0
  121. package/lib/doctor/watchers/process-pressure.mjs +60 -0
  122. package/lib/doctor/watchers/service-health.mjs +188 -0
  123. package/lib/document-extract.mjs +288 -0
  124. package/lib/document-ingest.mjs +230 -0
  125. package/lib/drop.mjs +282 -0
  126. package/lib/embed/approval-queue.mjs +176 -0
  127. package/lib/embed/artifact.mjs +349 -0
  128. package/lib/embed/authority-guard.mjs +155 -0
  129. package/lib/embed/cli.mjs +408 -0
  130. package/lib/embed/config.mjs +355 -0
  131. package/lib/embed/conflict-detection.mjs +264 -0
  132. package/lib/embed/customer-profiles.mjs +480 -0
  133. package/lib/embed/daemon.mjs +1309 -0
  134. package/lib/embed/demand-fetch.mjs +449 -0
  135. package/lib/embed/docs-lifecycle.mjs +349 -0
  136. package/lib/embed/inbox-live-watcher.mjs +119 -0
  137. package/lib/embed/inbox.mjs +343 -0
  138. package/lib/embed/intake-metrics.mjs +190 -0
  139. package/lib/embed/jobs/vector-sync.mjs +198 -0
  140. package/lib/embed/notifications.mjs +75 -0
  141. package/lib/embed/output.mjs +79 -0
  142. package/lib/embed/providers/github.mjs +295 -0
  143. package/lib/embed/providers/jira.mjs +192 -0
  144. package/lib/embed/providers/linear.mjs +186 -0
  145. package/lib/embed/providers/registry.mjs +115 -0
  146. package/lib/embed/providers/slack.mjs +203 -0
  147. package/lib/embed/recommendation-store.mjs +378 -0
  148. package/lib/embed/roadmap.mjs +374 -0
  149. package/lib/embed/role-framing.mjs +110 -0
  150. package/lib/embed/scheduler.mjs +99 -0
  151. package/lib/embed/semantic.mjs +325 -0
  152. package/lib/embed/snapshot.mjs +191 -0
  153. package/lib/embed/supervision.mjs +235 -0
  154. package/lib/embed/target-resolver.mjs +186 -0
  155. package/lib/embed/worker.mjs +62 -0
  156. package/lib/embed/workspaces.mjs +297 -0
  157. package/lib/engine/chunker-headings.mjs +110 -0
  158. package/lib/engine/compressor-heuristic.mjs +100 -0
  159. package/lib/engine/consolidate.mjs +287 -0
  160. package/lib/engine/contracts.mjs +126 -0
  161. package/lib/engine/defaults.mjs +129 -0
  162. package/lib/engine/eval-retrieval.mjs +148 -0
  163. package/lib/engine/fuser-rrf.mjs +62 -0
  164. package/lib/engine/index.mjs +37 -0
  165. package/lib/engine/registry.mjs +146 -0
  166. package/lib/engine/reranker-mmr.mjs +90 -0
  167. package/lib/engine/tokens.mjs +77 -0
  168. package/lib/entity-store.mjs +280 -0
  169. package/lib/env-config.mjs +69 -4
  170. package/lib/evals/retrieval-bench.mjs +159 -0
  171. package/lib/evaluator-optimizer.mjs +317 -0
  172. package/lib/features.mjs +159 -29
  173. package/lib/gates-audit.mjs +236 -0
  174. package/lib/git-hooks/prepare-commit-msg +58 -0
  175. package/lib/handoffs/cleanup.mjs +159 -0
  176. package/lib/handoffs/contract.mjs +162 -0
  177. package/lib/handoffs/inventory.mjs +120 -0
  178. package/lib/headhunt.mjs +28 -47
  179. package/lib/health-check.mjs +399 -0
  180. package/lib/hook-health.mjs +442 -0
  181. package/lib/hooks/_lib/log.mjs +82 -0
  182. package/lib/hooks/adaptive-lint.mjs +26 -2
  183. package/lib/hooks/agent-tracker.mjs +171 -14
  184. package/lib/hooks/audit-reads.mjs +109 -0
  185. package/lib/hooks/audit-trail.mjs +153 -0
  186. package/lib/hooks/bash-output-logger.mjs +71 -0
  187. package/lib/hooks/block-no-verify.mjs +41 -0
  188. package/lib/hooks/ci-status-check.mjs +82 -0
  189. package/lib/hooks/comment-lint.mjs +29 -7
  190. package/lib/hooks/config-protection.mjs +39 -18
  191. package/lib/hooks/context-watch.mjs +137 -0
  192. package/lib/hooks/context-window-recovery.mjs +4 -20
  193. package/lib/hooks/dep-audit.mjs +16 -0
  194. package/lib/hooks/doc-coupling-check.mjs +80 -0
  195. package/lib/hooks/edit-accumulator.mjs +3 -0
  196. package/lib/hooks/edit-error-recovery.mjs +3 -0
  197. package/lib/hooks/edit-guard.mjs +45 -4
  198. package/lib/hooks/env-check.mjs +3 -0
  199. package/lib/hooks/guard-bash.mjs +58 -1
  200. package/lib/hooks/mcp-audit.mjs +18 -20
  201. package/lib/hooks/mcp-health-check.mjs +36 -0
  202. package/lib/hooks/model-fallback.mjs +42 -56
  203. package/lib/hooks/policy-engine.mjs +209 -0
  204. package/lib/hooks/post-merge-docs-check.mjs +63 -0
  205. package/lib/hooks/pre-compact.mjs +4 -24
  206. package/lib/hooks/pre-push-gate.mjs +200 -37
  207. package/lib/hooks/proactive-activation.mjs +284 -0
  208. package/lib/hooks/read-tracker.mjs +27 -4
  209. package/lib/hooks/readme-age-check.mjs +77 -0
  210. package/lib/hooks/registry-sync.mjs +12 -5
  211. package/lib/hooks/scan-secrets.mjs +50 -7
  212. package/lib/hooks/session-optimize.mjs +311 -0
  213. package/lib/hooks/session-start.mjs +287 -28
  214. package/lib/hooks/stop-notify.mjs +236 -96
  215. package/lib/hooks/stop-typecheck.mjs +13 -1
  216. package/lib/hooks/test-watch.mjs +68 -0
  217. package/lib/host-capabilities.mjs +31 -10
  218. package/lib/init-docs.mjs +731 -291
  219. package/lib/init-unified.mjs +1000 -0
  220. package/lib/init-update.mjs +168 -0
  221. package/lib/init.mjs +107 -0
  222. package/lib/install/first-invocation.mjs +119 -0
  223. package/lib/install/stage-project.mjs +69 -0
  224. package/lib/intake/classify.mjs +278 -0
  225. package/lib/intake/feedback.mjs +273 -0
  226. package/lib/intake/filesystem-queue.mjs +158 -0
  227. package/lib/intake/intake-config.mjs +132 -0
  228. package/lib/intake/postgres-queue.mjs +198 -0
  229. package/lib/intake/prepare.mjs +139 -0
  230. package/lib/intake/queue.mjs +83 -0
  231. package/lib/intake/session-prelude.mjs +50 -0
  232. package/lib/integrations/intake-integrations.mjs +740 -0
  233. package/lib/intent-classifier.mjs +253 -0
  234. package/lib/knowledge/layout.mjs +72 -0
  235. package/lib/knowledge/rag.mjs +331 -0
  236. package/lib/knowledge/search.mjs +315 -0
  237. package/lib/knowledge/trends.mjs +261 -0
  238. package/lib/logger.mjs +85 -0
  239. package/lib/mcp/broker.mjs +124 -0
  240. package/lib/mcp/server.mjs +554 -854
  241. package/lib/mcp/tools/document.mjs +132 -0
  242. package/lib/mcp/tools/memory.mjs +209 -0
  243. package/lib/mcp/tools/project.mjs +349 -0
  244. package/lib/mcp/tools/skills.mjs +409 -0
  245. package/lib/mcp/tools/storage.mjs +60 -0
  246. package/lib/mcp/tools/telemetry.mjs +315 -0
  247. package/lib/mcp/tools/workflow.mjs +112 -0
  248. package/lib/mcp-catalog.json +54 -3
  249. package/lib/mcp-manager.mjs +96 -48
  250. package/lib/mcp-platform-config.mjs +22 -22
  251. package/lib/memory-stats.mjs +121 -0
  252. package/lib/mode-commands.mjs +124 -0
  253. package/lib/model-free-selector.mjs +184 -0
  254. package/lib/model-pricing.mjs +152 -0
  255. package/lib/model-registry.mjs +226 -0
  256. package/lib/model-router.mjs +362 -386
  257. package/lib/observation-store.mjs +391 -0
  258. package/lib/ollama-manager.mjs +428 -0
  259. package/lib/opencode-config.mjs +13 -3
  260. package/lib/opencode-runtime-plugin.mjs +70 -38
  261. package/lib/opencode-telemetry.mjs +64 -6
  262. package/lib/orchestration-policy.mjs +509 -6
  263. package/lib/overrides/resolver.mjs +207 -0
  264. package/lib/parity.mjs +147 -0
  265. package/lib/paths.mjs +33 -0
  266. package/lib/performance/generate.mjs +212 -0
  267. package/lib/plugin-registry.mjs +268 -0
  268. package/lib/policy/engine.mjs +130 -0
  269. package/lib/policy/unified-gates.mjs +96 -0
  270. package/lib/project-detection.mjs +129 -0
  271. package/lib/project-init-shared.mjs +272 -0
  272. package/lib/project-profile.mjs +447 -0
  273. package/lib/prompt-composer.js +434 -0
  274. package/lib/prompt-metadata.mjs +1 -1
  275. package/lib/provider-capabilities-anthropic.js +44 -0
  276. package/lib/provider-capabilities-deepseek.js +37 -0
  277. package/lib/provider-capabilities-generic.js +26 -0
  278. package/lib/provider-capabilities-google.js +47 -0
  279. package/lib/provider-capabilities-openai.js +45 -0
  280. package/lib/provider-capabilities.js +142 -0
  281. package/lib/providers/atlassian-confluence/index.mjs +103 -0
  282. package/lib/providers/atlassian-jira/index.mjs +100 -0
  283. package/lib/providers/auth-manager.mjs +126 -0
  284. package/lib/providers/circuit-breaker.mjs +124 -0
  285. package/lib/providers/contract.mjs +93 -0
  286. package/lib/providers/github/index.mjs +126 -0
  287. package/lib/providers/registry.mjs +184 -0
  288. package/lib/providers/salesforce/index.mjs +100 -0
  289. package/lib/providers/slack/index.mjs +80 -0
  290. package/lib/reflect.mjs +137 -0
  291. package/lib/research-lint.mjs +164 -0
  292. package/lib/resources/budget.mjs +259 -0
  293. package/lib/role-preload.mjs +21 -6
  294. package/lib/roles/approval-surface.mjs +54 -0
  295. package/lib/roles/cli.mjs +118 -0
  296. package/lib/roles/event-bus.mjs +79 -0
  297. package/lib/roles/fence.mjs +84 -0
  298. package/lib/roles/gateway.mjs +260 -0
  299. package/lib/roles/hook-emit.mjs +37 -0
  300. package/lib/roles/manifest.mjs +48 -0
  301. package/lib/roles/router.mjs +27 -0
  302. package/lib/runtime-pressure.mjs +360 -0
  303. package/lib/schema-artifact.mjs +134 -0
  304. package/lib/schema-infer.mjs +551 -0
  305. package/lib/server/auth.mjs +168 -0
  306. package/lib/server/chat.mjs +336 -0
  307. package/lib/server/cors.mjs +77 -0
  308. package/lib/server/csrf.mjs +91 -0
  309. package/lib/server/index.mjs +1927 -78
  310. package/lib/server/insights.mjs +765 -0
  311. package/lib/server/rate-limit.mjs +91 -0
  312. package/lib/server/static/assets/index-ab25c707.js +70 -0
  313. package/lib/server/static/assets/index-f0c80a2b.css +1 -0
  314. package/lib/server/static/index.html +12 -817
  315. package/lib/server/telemetry-login.mjs +108 -0
  316. package/lib/server/webhook.mjs +510 -0
  317. package/lib/service-manager.mjs +522 -58
  318. package/lib/services/pattern-promotion-service.mjs +167 -0
  319. package/lib/services/telemetry-backend.mjs +178 -0
  320. package/lib/session-store.mjs +374 -0
  321. package/lib/setup-prompts.mjs +96 -0
  322. package/lib/setup.mjs +523 -36
  323. package/lib/skills-apply.mjs +280 -0
  324. package/lib/skills-scope.mjs +118 -0
  325. package/lib/status.mjs +261 -70
  326. package/lib/storage/admin.mjs +355 -0
  327. package/lib/storage/backend.mjs +2 -1
  328. package/lib/storage/backup.mjs +347 -0
  329. package/lib/storage/embeddings-engine.mjs +133 -0
  330. package/lib/storage/embeddings-legacy.mjs +85 -0
  331. package/lib/storage/embeddings-local.mjs +108 -0
  332. package/lib/storage/embeddings-ollama.mjs +78 -0
  333. package/lib/storage/embeddings-openai.mjs +85 -0
  334. package/lib/storage/embeddings.mjs +92 -33
  335. package/lib/storage/file-lock.mjs +130 -0
  336. package/lib/storage/fusion.mjs +95 -0
  337. package/lib/storage/hybrid-query.mjs +34 -27
  338. package/lib/storage/migrations.mjs +187 -0
  339. package/lib/storage/postgres-backup.mjs +124 -0
  340. package/lib/storage/sql-store.mjs +5 -15
  341. package/lib/storage/state-source.mjs +12 -13
  342. package/lib/storage/store-version.mjs +115 -0
  343. package/lib/storage/sync.mjs +144 -35
  344. package/lib/storage/unified-storage.mjs +550 -0
  345. package/lib/storage/vector-client.mjs +286 -0
  346. package/lib/storage/vector-store.mjs +71 -30
  347. package/lib/task-graph/generate.mjs +135 -0
  348. package/lib/task-graph/schema.mjs +81 -0
  349. package/lib/task-graph/store.mjs +71 -0
  350. package/lib/telemetry/backends/local.mjs +62 -0
  351. package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
  352. package/lib/telemetry/backfill.mjs +180 -0
  353. package/lib/telemetry/eval-datasets.mjs +203 -0
  354. package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
  355. package/lib/telemetry/intent-verifications.mjs +86 -0
  356. package/lib/telemetry/llm-judge.mjs +350 -0
  357. package/lib/telemetry/model-pricing-catalog.mjs +557 -0
  358. package/lib/telemetry/setup.mjs +151 -0
  359. package/lib/telemetry/skill-calls.mjs +78 -0
  360. package/lib/telemetry/team-rollup.mjs +4 -4
  361. package/lib/token-engine.js +117 -0
  362. package/lib/token-estimator-anthropic.js +15 -0
  363. package/lib/token-estimator-deepseek.js +13 -0
  364. package/lib/token-estimator-default.js +13 -0
  365. package/lib/token-estimator-google.js +13 -0
  366. package/lib/token-estimator-openai.js +13 -0
  367. package/lib/toolkit-env.mjs +1 -1
  368. package/lib/tty-prompts.mjs +211 -0
  369. package/lib/uninstall/uninstall.mjs +423 -0
  370. package/lib/update.mjs +115 -0
  371. package/lib/upgrade.mjs +141 -0
  372. package/lib/validator.mjs +51 -2
  373. package/lib/validators/skills.mjs +142 -0
  374. package/lib/wireframe.mjs +422 -0
  375. package/lib/worker/entrypoint.mjs +241 -0
  376. package/lib/worker/evidence.mjs +107 -0
  377. package/lib/worker/run.mjs +154 -0
  378. package/lib/worker/trace.mjs +182 -0
  379. package/lib/workflow-state.mjs +14 -18
  380. package/package.json +21 -8
  381. package/personas/construct.md +53 -51
  382. package/platforms/claude/CLAUDE.md +1 -1
  383. package/platforms/claude/settings.template.json +115 -68
  384. package/platforms/opencode/config.template.json +3 -7
  385. package/rules/common/agents.md +11 -38
  386. package/rules/common/beads-hygiene.md +75 -0
  387. package/rules/common/code-review.md +11 -100
  388. package/rules/common/coding-style.md +5 -3
  389. package/rules/common/comments.md +30 -111
  390. package/rules/common/commit-approval.md +53 -0
  391. package/rules/common/cx-agent-routing.md +1 -0
  392. package/rules/common/development-workflow.md +23 -41
  393. package/rules/common/doc-ownership.md +54 -0
  394. package/rules/common/efficiency.md +57 -0
  395. package/rules/common/framing.md +76 -0
  396. package/rules/common/git-workflow.md +2 -4
  397. package/rules/common/patterns.md +3 -7
  398. package/rules/common/performance.md +15 -31
  399. package/rules/common/release-gates.md +69 -0
  400. package/rules/common/research.md +107 -0
  401. package/rules/common/security.md +6 -6
  402. package/rules/common/skill-composition.md +67 -0
  403. package/rules/common/testing.md +15 -27
  404. package/rules/golang/hooks.md +0 -4
  405. package/rules/policy/bootstrap.yaml +11 -0
  406. package/rules/policy/drive.yaml +8 -0
  407. package/rules/policy/task.yaml +9 -0
  408. package/rules/policy/workflow.yaml +9 -0
  409. package/rules/python/hooks.md +0 -4
  410. package/rules/swift/hooks.md +0 -4
  411. package/rules/typescript/hooks.md +0 -4
  412. package/rules/web/hooks.md +0 -2
  413. package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
  414. package/skills/ai/prompt-optimizer.md +7 -7
  415. package/skills/compliance/ai-disclosure.md +58 -0
  416. package/skills/compliance/data-privacy.md +46 -0
  417. package/skills/compliance/license-audit.md +40 -0
  418. package/skills/compliance/regulatory-review.md +61 -0
  419. package/skills/docs/document-ingest-workflow.md +52 -0
  420. package/skills/docs/evidence-ingest-workflow.md +9 -0
  421. package/skills/docs/init-docs.md +51 -18
  422. package/skills/docs/product-intelligence-workflow.md +12 -0
  423. package/skills/docs/product-signal-workflow.md +4 -0
  424. package/skills/docs/research-workflow.md +23 -8
  425. package/skills/docs/runbook-workflow.md +1 -1
  426. package/skills/operating/orchestration-reference.md +151 -0
  427. package/skills/roles/architect.md +5 -0
  428. package/skills/roles/engineer.md +32 -0
  429. package/skills/roles/operator.md +12 -0
  430. package/skills/roles/reviewer.md +23 -0
  431. package/skills/routing.md +1 -1
  432. package/templates/devcontainer/Dockerfile.devcontainer +38 -0
  433. package/templates/devcontainer/devcontainer.json +31 -0
  434. package/templates/distribution/bootstrap.ps1 +142 -0
  435. package/templates/distribution/bootstrap.sh +196 -0
  436. package/templates/distribution/run.mjs +187 -0
  437. package/templates/docs/adr.md +44 -8
  438. package/templates/docs/changelog-entry.md +43 -0
  439. package/templates/docs/construct_guide.md +149 -0
  440. package/templates/docs/evidence-brief.md +2 -2
  441. package/templates/docs/meta-prd.md +140 -19
  442. package/templates/docs/onboarding.md +57 -0
  443. package/templates/docs/prd.md +159 -15
  444. package/templates/docs/research-brief.md +4 -4
  445. package/templates/homebrew/construct.rb +67 -0
  446. package/langfuse/docker-compose.yml +0 -82
  447. package/lib/eval-harness.mjs +0 -59
  448. package/lib/hooks/bootstrap-guard.mjs +0 -90
  449. package/lib/hooks/console-warn.mjs +0 -43
  450. package/lib/hooks/continuation-enforcer.mjs +0 -72
  451. package/lib/hooks/drive-guard.mjs +0 -89
  452. package/lib/hooks/mcp-task-scope.mjs +0 -47
  453. package/lib/hooks/task-completed-guard.mjs +0 -43
  454. package/lib/hooks/teammate-idle-guard.mjs +0 -54
  455. package/lib/hooks/workflow-guard.mjs +0 -62
  456. package/lib/prompt-composer.mjs +0 -196
  457. package/lib/review.mjs +0 -429
  458. package/lib/server/static/app.js +0 -841
  459. package/lib/telemetry/langfuse-model-sync.mjs +0 -270
  460. package/rules/common/hooks.md +0 -35
  461. package/rules/zh/README.md +0 -113
  462. package/rules/zh/agents.md +0 -55
  463. package/rules/zh/code-review.md +0 -129
  464. package/rules/zh/coding-style.md +0 -53
  465. package/rules/zh/development-workflow.md +0 -49
  466. package/rules/zh/git-workflow.md +0 -29
  467. package/rules/zh/hooks.md +0 -35
  468. package/rules/zh/patterns.md +0 -36
  469. package/rules/zh/performance.md +0 -60
  470. package/rules/zh/security.md +0 -34
  471. package/rules/zh/testing.md +0 -34
@@ -0,0 +1,164 @@
1
+ /**
2
+ * lib/research-lint.mjs — enforce minimum structure for research and evidence artifacts.
3
+ *
4
+ * Validates markdown files under .cx/research/ and key .cx/knowledge/
5
+ * folders so weak or incomplete evidence docs are flagged before they become
6
+ * decision inputs. Backs `construct lint:research` and lightweight CI checks.
7
+ */
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+
11
+ const RESEARCH_RELPATH = /^\.cx\/research\/.+\.md$/;
12
+ const EVIDENCE_RELPATH = /^\.cx\/knowledge\/reference\/evidence-briefs\/.+\.md$/;
13
+ const SIGNAL_RELPATH = /^\.cx\/knowledge\/external\/signals\/.+\.md$/;
14
+
15
+ const REQUIRED_SECTIONS = {
16
+ research: ['Question', 'Method', 'Sources', 'Findings', 'Confidence', 'References'],
17
+ evidence: ['Decision this evidence informs', 'Evidence threshold', 'Sources', 'What we observed', 'Confidence', 'Recommendation'],
18
+ signal: ['Signal', 'Evidence', 'Why it matters', 'What would make this actionable', 'Follow-up'],
19
+ };
20
+
21
+ function relPath(rootDir, absPath) {
22
+ return path.relative(rootDir, absPath).replace(/\\/g, '/');
23
+ }
24
+
25
+ function artifactKind(rel) {
26
+ if (RESEARCH_RELPATH.test(rel)) return 'research';
27
+ if (EVIDENCE_RELPATH.test(rel)) return 'evidence';
28
+ if (SIGNAL_RELPATH.test(rel)) return 'signal';
29
+ return null;
30
+ }
31
+
32
+ function lineForMatch(content, pattern) {
33
+ const lines = content.split('\n');
34
+ for (let index = 0; index < lines.length; index += 1) {
35
+ if (pattern.test(lines[index])) return index + 1;
36
+ }
37
+ return 1;
38
+ }
39
+
40
+ function hasHeading(content, heading) {
41
+ const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
42
+ return new RegExp(`^##\\s+${escaped}\\s*$`, 'm').test(content);
43
+ }
44
+
45
+ function hasDateMetadata(content) {
46
+ return /^-\s+\*\*Date\*\*:\s+.+$/m.test(content)
47
+ || /\b(?:publication|access)\s+date\b/i.test(content)
48
+ || /\b\d{4}-\d{2}-\d{2}\b/.test(content);
49
+ }
50
+
51
+ function hasConfidenceCue(content) {
52
+ return /\bconfidence\b/i.test(content)
53
+ || /\bhigh\b|\bmedium\b|\blow\b/i.test(content)
54
+ || /\bweak signal\b|\bconfirmed\b|\binferred\b/i.test(content);
55
+ }
56
+
57
+ function hasSourceCue(content) {
58
+ return /\bsource\b/i.test(content)
59
+ || /\bhttps?:\/\//i.test(content)
60
+ || /\b\.cx\//.test(content)
61
+ || /\bdocs\//.test(content);
62
+ }
63
+
64
+ function hasObservationInferenceCue(content) {
65
+ return /\bobservation\b/i.test(content)
66
+ || /\binference\b/i.test(content)
67
+ || /\bwhat we observed\b/i.test(content);
68
+ }
69
+
70
+ export function lintResearchFile(filePath, { rootDir = process.cwd() } = {}) {
71
+ const rel = relPath(rootDir, filePath);
72
+ const kind = artifactKind(rel);
73
+ if (!kind) return { path: rel, kind: null, errors: [], warnings: [] };
74
+
75
+ let content = '';
76
+ try {
77
+ content = fs.readFileSync(filePath, 'utf8');
78
+ } catch {
79
+ return { path: rel, kind, errors: [{ line: 1, label: 'unable to read file' }], warnings: [] };
80
+ }
81
+
82
+ const errors = [];
83
+ const warnings = [];
84
+
85
+ for (const heading of REQUIRED_SECTIONS[kind]) {
86
+ if (!hasHeading(content, heading)) {
87
+ errors.push({
88
+ line: 1,
89
+ label: `missing required section: ${heading}`,
90
+ });
91
+ }
92
+ }
93
+
94
+ if (!hasSourceCue(content)) {
95
+ errors.push({
96
+ line: lineForMatch(content, /^##\s+Sources\s*$/m),
97
+ label: 'missing source references, links, or document paths',
98
+ });
99
+ }
100
+
101
+ if (!hasDateMetadata(content)) {
102
+ warnings.push({
103
+ line: 1,
104
+ label: 'missing explicit date or access-date basis',
105
+ });
106
+ }
107
+
108
+ if (!hasConfidenceCue(content)) {
109
+ errors.push({
110
+ line: 1,
111
+ label: 'missing confidence labeling or threshold language',
112
+ });
113
+ }
114
+
115
+ if ((kind === 'research' || kind === 'evidence') && !hasObservationInferenceCue(content)) {
116
+ warnings.push({
117
+ line: 1,
118
+ label: 'observation vs inference separation is not explicit',
119
+ });
120
+ }
121
+
122
+ return { path: rel, kind, errors, warnings };
123
+ }
124
+
125
+ function walkDir(dir, results = []) {
126
+ let entries;
127
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return results; }
128
+ for (const entry of entries) {
129
+ const full = path.join(dir, entry.name);
130
+ if (entry.isDirectory()) {
131
+ if (['.git', 'node_modules', 'site', '.construct', '.claude'].includes(entry.name)) continue;
132
+ walkDir(full, results);
133
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
134
+ results.push(full);
135
+ }
136
+ }
137
+ return results;
138
+ }
139
+
140
+ export function lintResearchRepo({ rootDir = process.cwd() } = {}) {
141
+ const files = walkDir(rootDir).filter((filePath) => artifactKind(relPath(rootDir, filePath)));
142
+ const results = files.map((filePath) => lintResearchFile(filePath, { rootDir }));
143
+ return results.filter((result) => result.errors.length || result.warnings.length);
144
+ }
145
+
146
+ export function formatResearchLintResults(results) {
147
+ if (!results.length) return { output: ' ✓ No research artifact issues found.\n', exitCode: 0 };
148
+
149
+ const lines = [];
150
+ let errorCount = 0;
151
+ let warnCount = 0;
152
+ for (const { path: filePath, errors, warnings } of results) {
153
+ for (const { line, label } of errors) {
154
+ lines.push(` error ${filePath}:${line} ${label}`);
155
+ errorCount += 1;
156
+ }
157
+ for (const { line, label } of warnings) {
158
+ lines.push(` warn ${filePath}:${line} ${label}`);
159
+ warnCount += 1;
160
+ }
161
+ }
162
+ lines.push(`\n ${errorCount} error(s), ${warnCount} warning(s)`);
163
+ return { output: `${lines.join('\n')}\n`, exitCode: errorCount > 0 ? 1 : 0 };
164
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * lib/resources/budget.mjs — disk-budget enforcement for .cx/ assets.
3
+ *
4
+ * Construct generates data continuously: traces, intake archives, task
5
+ * graphs, worker logs, session records, backups. None of it has a
6
+ * natural ceiling, so left unchecked the .cx/ tree grows until it
7
+ * threatens the operator's disk. The budget primitive caps each
8
+ * category against a config-defined ceiling.
9
+ *
10
+ * Two enforcement modes:
11
+ * - **Hard-reject** (traces, worker logs): observability is
12
+ * replaceable. When the cap is hit, refuse new writes with a
13
+ * typed error pointing at `construct prune` and the dashboard
14
+ * budget page. Better to lose a trace than crash the operator's
15
+ * machine.
16
+ * - **Soft-warn** (intake archive, task graphs): R&D state is
17
+ * load-bearing — never reject; emit a warning the operator can
18
+ * see in the doctor surface, and pruning gets aggressive.
19
+ *
20
+ * Defaults live in DEFAULT_PROJECT_CONFIG.resources. The operator
21
+ * raises them in construct.config.json or via the dashboard. Doctor
22
+ * surfaces usage vs cap at 80% (warn) and 100% (fail).
23
+ */
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import { loadProjectConfig } from '../config/project-config.mjs';
27
+
28
+ export const HARD_REJECT_CATEGORIES = new Set(['traces', 'worker-logs']);
29
+ export const SOFT_WARN_CATEGORIES = new Set(['intake-archive', 'task-graphs', 'sessions', 'backups', 'handoffs']);
30
+
31
+ const CATEGORY_PATHS = {
32
+ traces: '.cx/traces',
33
+ 'worker-logs': '.cx/runtime/worker',
34
+ 'intake-archive': '.cx/intake/processed',
35
+ 'intake-skipped': '.cx/intake/skipped',
36
+ 'task-graphs': '.cx/task-graphs',
37
+ sessions: '.cx/sessions',
38
+ backups: '.cx/backups',
39
+ handoffs: '.cx/handoffs',
40
+ };
41
+
42
+ function loadResourceBudgets(projectRoot, env) {
43
+ const { config } = loadProjectConfig(projectRoot, env);
44
+ return config?.resources ?? {};
45
+ }
46
+
47
+ function walkDirSize(dir) {
48
+ if (!fs.existsSync(dir)) return { bytes: 0, files: 0 };
49
+ let bytes = 0;
50
+ let files = 0;
51
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
52
+ const full = path.join(dir, entry.name);
53
+ if (entry.isDirectory()) {
54
+ const r = walkDirSize(full);
55
+ bytes += r.bytes;
56
+ files += r.files;
57
+ } else {
58
+ try {
59
+ bytes += fs.statSync(full).size;
60
+ files += 1;
61
+ } catch { /* race — file disappeared */ }
62
+ }
63
+ }
64
+ return { bytes, files };
65
+ }
66
+
67
+ export function measureUsage(projectRoot, env = process.env) {
68
+ const budgets = loadResourceBudgets(projectRoot, env);
69
+ const result = { categories: {}, totalCxBytes: 0, totalCxCap: (budgets?.disk?.totalCxMaxMb ?? 2000) * 1024 * 1024 };
70
+ for (const [name, rel] of Object.entries(CATEGORY_PATHS)) {
71
+ const full = path.join(projectRoot, rel);
72
+ const { bytes, files } = walkDirSize(full);
73
+ result.categories[name] = {
74
+ bytes,
75
+ files,
76
+ path: rel,
77
+ enforcement: HARD_REJECT_CATEGORIES.has(name) ? 'hard-reject' : 'soft-warn',
78
+ };
79
+ result.totalCxBytes += bytes;
80
+ }
81
+ result.totalCxUsageRatio = result.totalCxCap > 0 ? result.totalCxBytes / result.totalCxCap : 0;
82
+ return result;
83
+ }
84
+
85
+ export function reserveOrReject(projectRoot, category, sizeBytes, env = process.env) {
86
+ if (!CATEGORY_PATHS[category]) {
87
+ return { ok: true, source: 'unknown-category' };
88
+ }
89
+ const usage = measureUsage(projectRoot, env);
90
+ const totalAfter = usage.totalCxBytes + sizeBytes;
91
+ if (totalAfter > usage.totalCxCap) {
92
+ if (HARD_REJECT_CATEGORIES.has(category)) {
93
+ return {
94
+ ok: false,
95
+ reason: 'budget-exceeded',
96
+ message: `.cx/ total ${Math.round(totalAfter / 1024 / 1024)}MB would exceed cap ${Math.round(usage.totalCxCap / 1024 / 1024)}MB — run \`construct prune\` or raise resources.disk.totalCxMaxMb`,
97
+ };
98
+ }
99
+ return {
100
+ ok: true,
101
+ warn: true,
102
+ reason: 'budget-warning',
103
+ message: `.cx/ usage at ${Math.round((totalAfter / usage.totalCxCap) * 100)}% — \`construct prune\` recommended`,
104
+ };
105
+ }
106
+ const ratio = totalAfter / usage.totalCxCap;
107
+ if (ratio > 0.8) {
108
+ return { ok: true, warn: true, reason: 'budget-warning', message: `.cx/ usage at ${Math.round(ratio * 100)}% of cap` };
109
+ }
110
+ return { ok: true };
111
+ }
112
+
113
+ export function planPrune(projectRoot, env = process.env, opts = {}) {
114
+ const budgets = loadResourceBudgets(projectRoot, env);
115
+ const disk = budgets?.disk ?? {};
116
+ const now = opts.now ?? Date.now();
117
+ const actions = [];
118
+
119
+ const tracesMaxDays = disk.tracesMaxDays ?? 30;
120
+ const tracesDir = path.join(projectRoot, '.cx/traces');
121
+ if (fs.existsSync(tracesDir)) {
122
+ const cutoff = now - tracesMaxDays * 24 * 60 * 60 * 1000;
123
+ for (const f of fs.readdirSync(tracesDir)) {
124
+ if (!f.endsWith('.jsonl')) continue;
125
+ const full = path.join(tracesDir, f);
126
+ const stat = fs.statSync(full);
127
+ if (stat.mtimeMs < cutoff) {
128
+ actions.push({ category: 'traces', path: full, reason: `older than ${tracesMaxDays}d`, bytes: stat.size });
129
+ }
130
+ }
131
+ }
132
+
133
+ const intakeDir = path.join(projectRoot, '.cx/intake/processed');
134
+ const intakeMaxItems = disk.intakeArchiveMaxItems ?? 500;
135
+ const intakeMaxDays = disk.intakeArchiveMaxDays ?? 90;
136
+ if (fs.existsSync(intakeDir)) {
137
+ const cutoff = now - intakeMaxDays * 24 * 60 * 60 * 1000;
138
+ const files = fs.readdirSync(intakeDir)
139
+ .filter((f) => f.endsWith('.json'))
140
+ .map((f) => {
141
+ const full = path.join(intakeDir, f);
142
+ const stat = fs.statSync(full);
143
+ return { path: full, mtimeMs: stat.mtimeMs, bytes: stat.size };
144
+ })
145
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
146
+ for (let i = 0; i < files.length; i++) {
147
+ const f = files[i];
148
+ if (i >= intakeMaxItems) {
149
+ actions.push({ category: 'intake-archive', path: f.path, reason: `exceeds ${intakeMaxItems} items`, bytes: f.bytes });
150
+ } else if (f.mtimeMs < cutoff) {
151
+ actions.push({ category: 'intake-archive', path: f.path, reason: `older than ${intakeMaxDays}d`, bytes: f.bytes });
152
+ }
153
+ }
154
+ }
155
+
156
+ const graphsDir = path.join(projectRoot, '.cx/task-graphs');
157
+ const graphsMaxItems = disk.taskGraphsMaxItems ?? 200;
158
+ const graphsMaxDays = disk.taskGraphsMaxDays ?? 90;
159
+ if (fs.existsSync(graphsDir)) {
160
+ const cutoff = now - graphsMaxDays * 24 * 60 * 60 * 1000;
161
+ const files = fs.readdirSync(graphsDir)
162
+ .filter((f) => f.endsWith('.json'))
163
+ .map((f) => {
164
+ const full = path.join(graphsDir, f);
165
+ const stat = fs.statSync(full);
166
+ return { path: full, mtimeMs: stat.mtimeMs, bytes: stat.size };
167
+ })
168
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
169
+ for (let i = 0; i < files.length; i++) {
170
+ const f = files[i];
171
+ if (i >= graphsMaxItems) {
172
+ actions.push({ category: 'task-graphs', path: f.path, reason: `exceeds ${graphsMaxItems} items`, bytes: f.bytes });
173
+ } else if (f.mtimeMs < cutoff) {
174
+ actions.push({ category: 'task-graphs', path: f.path, reason: `older than ${graphsMaxDays}d`, bytes: f.bytes });
175
+ }
176
+ }
177
+ }
178
+
179
+ const workerDir = path.join(projectRoot, '.cx/runtime/worker');
180
+ const workerMaxMb = disk.workerLogsMaxMb ?? 100;
181
+ const workerMaxDays = disk.workerLogsMaxDays ?? 14;
182
+ if (fs.existsSync(workerDir)) {
183
+ const cutoff = now - workerMaxDays * 24 * 60 * 60 * 1000;
184
+ const files = fs.readdirSync(workerDir)
185
+ .filter((f) => f.endsWith('.log'))
186
+ .map((f) => {
187
+ const full = path.join(workerDir, f);
188
+ const stat = fs.statSync(full);
189
+ return { path: full, mtimeMs: stat.mtimeMs, bytes: stat.size };
190
+ })
191
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
192
+ let totalBytes = 0;
193
+ for (const f of files) {
194
+ totalBytes += f.bytes;
195
+ const overSize = totalBytes > workerMaxMb * 1024 * 1024;
196
+ const overAge = f.mtimeMs < cutoff;
197
+ if (overSize) {
198
+ actions.push({ category: 'worker-logs', path: f.path, reason: `worker logs > ${workerMaxMb}MB cap`, bytes: f.bytes });
199
+ } else if (overAge) {
200
+ actions.push({ category: 'worker-logs', path: f.path, reason: `older than ${workerMaxDays}d`, bytes: f.bytes });
201
+ }
202
+ }
203
+ }
204
+
205
+ const handoffsDir = path.join(projectRoot, '.cx/handoffs');
206
+ const handoffsMaxItems = disk.handoffsMaxItems ?? 50;
207
+ const handoffsMaxDays = disk.handoffsMaxDays ?? 30;
208
+ if (fs.existsSync(handoffsDir)) {
209
+ const cutoff = now - handoffsMaxDays * 24 * 60 * 60 * 1000;
210
+ const files = fs.readdirSync(handoffsDir)
211
+ .filter((f) => f.endsWith('.md'))
212
+ .map((f) => {
213
+ const full = path.join(handoffsDir, f);
214
+ const stat = fs.statSync(full);
215
+ return { path: full, mtimeMs: stat.mtimeMs, bytes: stat.size };
216
+ })
217
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
218
+ for (let i = 0; i < files.length; i++) {
219
+ const f = files[i];
220
+ if (i >= handoffsMaxItems) {
221
+ actions.push({ category: 'handoffs', path: f.path, reason: `exceeds ${handoffsMaxItems} items`, bytes: f.bytes });
222
+ } else if (f.mtimeMs < cutoff) {
223
+ actions.push({ category: 'handoffs', path: f.path, reason: `older than ${handoffsMaxDays}d`, bytes: f.bytes });
224
+ }
225
+ }
226
+ }
227
+
228
+ const backupsRoot = path.join(projectRoot, '.cx/backups');
229
+ const backupsMaxDays = disk.backupsMaxDays ?? 60;
230
+ if (fs.existsSync(backupsRoot)) {
231
+ const cutoff = now - backupsMaxDays * 24 * 60 * 60 * 1000;
232
+ const walk = (dir) => {
233
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
234
+ const full = path.join(dir, entry.name);
235
+ if (entry.isDirectory()) { walk(full); continue; }
236
+ const stat = fs.statSync(full);
237
+ if (stat.mtimeMs < cutoff) {
238
+ actions.push({ category: 'backups', path: full, reason: `older than ${backupsMaxDays}d`, bytes: stat.size });
239
+ }
240
+ }
241
+ };
242
+ walk(backupsRoot);
243
+ }
244
+
245
+ return actions;
246
+ }
247
+
248
+ export function executePrune(actions) {
249
+ const removed = [];
250
+ let bytesFreed = 0;
251
+ for (const a of actions) {
252
+ try {
253
+ fs.unlinkSync(a.path);
254
+ removed.push(a.path);
255
+ bytesFreed += a.bytes || 0;
256
+ } catch { /* race — already gone */ }
257
+ }
258
+ return { removed, bytesFreed };
259
+ }
@@ -2,13 +2,21 @@
2
2
  * lib/role-preload.mjs — inline role anti-pattern files into specialist prompts at sync time.
3
3
  *
4
4
  * Specialist prompts carry a marker: **Role guidance**: call `get_skill("roles/NAME")` before drafting.
5
- * buildPrompt() in sync-agents.mjs replaces that marker with the role body so the content is always
6
- * present no runtime get_skill dependency. Flavor overlays (e.g. engineer.ai) are concatenated
7
- * under the core role content.
5
+ *
6
+ * Default: on-demand. The directive stays in the prompt verbatim and the agent calls
7
+ * `get_skill("roles/NAME")` at runtime. All hosts Construct supports (Claude Code, OpenCode, Codex,
8
+ * Copilot) have the construct-mcp server exposing get_skill, so the runtime call is reliable.
9
+ * Saves ~1000+ words of prompt budget per agent by not inlining skills that may not be needed.
10
+ *
11
+ * Opt-in preload: when a registry entry sets `preloadRoleGuidance: true`, the role body is inlined
12
+ * at sync time. Use this only for agents deployed to hosts without reliable MCP access, or where the
13
+ * role content is load-bearing on every single invocation. See `rules/common/skill-composition.md`.
8
14
  */
9
15
  import fs from "node:fs";
10
16
  import path from "node:path";
11
17
 
18
+ import { logSkillCall } from "./telemetry/skill-calls.mjs";
19
+
12
20
  export const ROLE_DIRECTIVE_RE = /^[ \t]*\*\*Role guidance\*\*:\s*call\s+`get_skill\("roles\/([^"]+)"\)`\s*before\s*drafting\.[ \t]*$/m;
13
21
 
14
22
  export const PROMPT_WORD_CAP = 3600;
@@ -23,13 +31,20 @@ function stripTopHeading(body) {
23
31
  return body.replace(/^#\s+[^\n]*\n+/, "").replace(/^Load this[^\n]*\n+/, "");
24
32
  }
25
33
 
26
- export function readRoleFile(root, name) {
34
+ export function readRoleFile(root, name, opts = {}) {
27
35
  const p = path.join(root, "skills", "roles", `${name}.md`);
28
36
  if (!fs.existsSync(p)) return null;
29
- return stripTopHeading(stripFrontmatter(fs.readFileSync(p, "utf8"))).trim();
37
+ const body = stripTopHeading(stripFrontmatter(fs.readFileSync(p, "utf8"))).trim();
38
+ logSkillCall({
39
+ skillId: `roles/${name}`,
40
+ source: opts.source || 'role-preload',
41
+ callerContext: opts.callerContext,
42
+ });
43
+ return body;
30
44
  }
31
45
 
32
- export function inlineRoleAntiPatterns(prompt, root, agentName = "(unknown)", warn = console.warn) {
46
+ export function inlineRoleAntiPatterns(prompt, root, agentName = "(unknown)", warn = console.warn, opts = {}) {
47
+ if (!opts.preload) return prompt;
33
48
  const match = prompt.match(ROLE_DIRECTIVE_RE);
34
49
  if (!match) return prompt;
35
50
  const ref = match[1];
@@ -0,0 +1,54 @@
1
+ /**
2
+ * lib/roles/approval-surface.mjs — visibility for approval-required actions.
3
+ *
4
+ * When a persona-tagged session hits an action that lands in the fence's
5
+ * `approvalRequired` bucket (commit, push, out-of-fence edit, etc.), this
6
+ * module appends a marker to ~/.cx/approval-pending.jsonl and emits an SSE
7
+ * toast through the embed notification bus. The user sees a dashboard toast
8
+ * instead of having to grep stderr.
9
+ *
10
+ * Best-effort: any failure is swallowed so guard hooks never get blocked by
11
+ * notification problems.
12
+ */
13
+
14
+ import { existsSync, appendFileSync, mkdirSync } from 'node:fs';
15
+ import { homedir } from 'node:os';
16
+ import { join } from 'node:path';
17
+
18
+ const PENDING_PATH = join(homedir(), '.cx', 'approval-pending.jsonl');
19
+
20
+ function ensureDir() {
21
+ const dir = join(homedir(), '.cx');
22
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
23
+ }
24
+
25
+ export async function recordApprovalRequest({ personaId, action, target = '', reason = '', context = null }) {
26
+ if (process.env.CONSTRUCT_ROLES === 'off') return;
27
+ try {
28
+ ensureDir();
29
+ const entry = {
30
+ ts: Date.now(),
31
+ personaId,
32
+ cxId: `cx-${personaId}`,
33
+ action,
34
+ target: String(target).slice(0, 512),
35
+ reason,
36
+ context,
37
+ };
38
+ appendFileSync(PENDING_PATH, JSON.stringify(entry) + '\n');
39
+ } catch { /* best effort */ }
40
+
41
+ try {
42
+ const mod = await import('../embed/notifications.mjs');
43
+ if (typeof mod.emitEmbedNotification === 'function') {
44
+ mod.emitEmbedNotification({
45
+ type: 'warning',
46
+ source: 'roles',
47
+ message: `cx-${personaId} requesting approval: ${action} ${String(target).slice(0, 80)}`,
48
+ meta: { personaId, action, target, reason },
49
+ });
50
+ }
51
+ } catch { /* best effort */ }
52
+ }
53
+
54
+ export { PENDING_PATH };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * lib/roles/cli.mjs — `construct role <subcommand>` handler.
3
+ *
4
+ * Subcommands:
5
+ * list — show pending role invocations
6
+ * latest — show the most recent unresolved invocation (full brief)
7
+ * show <fingerprint> — show one invocation by fingerprint
8
+ * status — show onboarded personas + their event types
9
+ * reset — clear the pending queue (manual recovery)
10
+ */
11
+
12
+ import { listOnboardedPersonas, loadManifest } from './manifest.mjs';
13
+ import { listPending, markResolved, resetPending } from './gateway.mjs';
14
+ import { recent as recentEvents } from './event-bus.mjs';
15
+
16
+ function fmtTime(ts) {
17
+ const d = new Date(ts);
18
+ return d.toISOString().replace('T', ' ').slice(0, 19);
19
+ }
20
+
21
+ function brief(entry, manifest) {
22
+ const fence = manifest?.fence || {};
23
+ const lines = [
24
+ `# Role invocation — ${entry.cxId}`,
25
+ `bd issue: ${entry.bdIssueId || '(none)'}`,
26
+ `event: ${entry.eventType}`,
27
+ `fingerprint: ${entry.fingerprint}`,
28
+ `queued at: ${fmtTime(entry.ts)}`,
29
+ `summary: ${entry.summary}`,
30
+ ``,
31
+ `## Fence`,
32
+ `allowedPaths: ${(fence.allowedPaths || []).join(', ') || '(none)'}`,
33
+ `allowedLabels: ${(fence.allowedBdLabels || []).join(', ') || '(none)'}`,
34
+ `allowedCommands: ${(fence.allowedCommands || []).join(', ') || '(none)'}`,
35
+ `approvalReq'd: ${(fence.approvalRequired || []).join(', ') || '(none)'}`,
36
+ ``,
37
+ `## Handoff candidates`,
38
+ (manifest?.handoffCandidates || []).map((h) => `- cx-${h}`).join('\n') || '(none)',
39
+ ``,
40
+ `## To dispatch`,
41
+ `Run via existing Task path. Tell Construct:`,
42
+ `> Dispatch ${entry.cxId} for incident ${entry.bdIssueId} (fingerprint ${entry.fingerprint}).`,
43
+ `> Persona must stay inside fence above. Use \`next:cx-<role>\` bd label for handoff.`,
44
+ ];
45
+ return lines.join('\n');
46
+ }
47
+
48
+ export async function runCli(args) {
49
+ const sub = args[0] || 'list';
50
+
51
+ if (sub === 'list') {
52
+ const pending = listPending({ unresolved: true });
53
+ if (pending.length === 0) {
54
+ console.log('No pending role invocations.');
55
+ return 0;
56
+ }
57
+ console.log(`Pending role invocations: ${pending.length}`);
58
+ for (const p of pending) {
59
+ console.log(` ${fmtTime(p.ts)} ${p.cxId.padEnd(20)} ${(p.bdIssueId || '-').padEnd(14)} ${p.eventType.padEnd(28)} ${p.summary}`);
60
+ }
61
+ return 0;
62
+ }
63
+
64
+ if (sub === 'latest') {
65
+ const pending = listPending({ unresolved: true });
66
+ if (pending.length === 0) {
67
+ console.log('No pending role invocations.');
68
+ return 0;
69
+ }
70
+ const entry = pending[pending.length - 1];
71
+ const manifest = loadManifest(entry.personaId);
72
+ console.log(brief(entry, manifest));
73
+ return 0;
74
+ }
75
+
76
+ if (sub === 'show') {
77
+ const fp = args[1];
78
+ if (!fp) { console.error('Usage: construct role show <fingerprint>'); return 2; }
79
+ const pending = listPending({ unresolved: false });
80
+ const entry = pending.find((p) => p.fingerprint === fp || p.bdIssueId === fp);
81
+ if (!entry) { console.error(`No invocation found for ${fp}`); return 1; }
82
+ const manifest = loadManifest(entry.personaId);
83
+ console.log(brief(entry, manifest));
84
+ console.log('\n## Recent matching events');
85
+ for (const e of recentEvents({ fingerprint: entry.fingerprint, limit: 5 })) {
86
+ console.log(` ${fmtTime(e.ts)} ${e.type} ${e.summary?.split('\n')[0] || ''}`);
87
+ }
88
+ return 0;
89
+ }
90
+
91
+ if (sub === 'status') {
92
+ const onboarded = listOnboardedPersonas();
93
+ console.log(`Onboarded personas: ${onboarded.length}`);
94
+ for (const id of onboarded) {
95
+ const m = loadManifest(id);
96
+ console.log(` cx-${id.padEnd(20)} events: ${(m.events || []).join(', ')}`);
97
+ }
98
+ return 0;
99
+ }
100
+
101
+ if (sub === 'resolve') {
102
+ const fp = args[1];
103
+ if (!fp) { console.error('Usage: construct role resolve <fingerprint>'); return 2; }
104
+ const ok = markResolved(fp);
105
+ console.log(ok ? `Resolved ${fp}` : `No matching pending entry for ${fp}`);
106
+ return ok ? 0 : 1;
107
+ }
108
+
109
+ if (sub === 'reset') {
110
+ resetPending();
111
+ console.log('Pending queue cleared.');
112
+ return 0;
113
+ }
114
+
115
+ console.error(`Unknown role subcommand: ${sub}`);
116
+ console.error('Usage: construct role [list|latest|show <fp>|status|resolve <fp>|reset]');
117
+ return 2;
118
+ }