@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,241 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/worker/entrypoint.mjs — long-running worker loop for team / enterprise mode.
4
+ *
5
+ * Claims pending intake items from the Postgres-backed queue and runs them
6
+ * through `runJob`, attaching evidence to the originating task graph node.
7
+ * Exits when the queue stays empty for `--idle-timeout-seconds` (default
8
+ * 300s) so a Kubernetes / nomad scheduler can scale workers to zero.
9
+ *
10
+ * The container's path policy is the workspace mount: `runJob` enforces
11
+ * `allowedPaths: [workspace]` so the worker can only touch the
12
+ * bind-mounted project tree even if a malicious command tries `..`.
13
+ *
14
+ * Invocation (inside the image):
15
+ * node lib/worker/entrypoint.mjs \
16
+ * --project=<project-name> \
17
+ * --workspace=/work \
18
+ * --idle-timeout-seconds=300 \
19
+ * --poll-interval-ms=2000
20
+ *
21
+ * Env: DATABASE_URL must be set. The container reads it from the platform
22
+ * (compose / k8s secret); the entrypoint does not source ~/.construct/config.env.
23
+ */
24
+
25
+ import path from 'node:path';
26
+
27
+ import { createSqlClient, closeSqlClient } from '../storage/backend.mjs';
28
+ import { PostgresIntakeQueue } from '../intake/postgres-queue.mjs';
29
+ import { FilesystemTaskGraphStore } from '../task-graph/store.mjs';
30
+ import { runJob } from './run.mjs';
31
+ import { evidenceFromJobResult, recordEvidence, blockedPacket } from './evidence.mjs';
32
+ import { emitTraceEvent, newTraceId } from './trace.mjs';
33
+
34
+ const DEFAULT_POLL_INTERVAL_MS = 2_000;
35
+ const DEFAULT_IDLE_TIMEOUT_SECONDS = 300;
36
+ const DEFAULT_TIMEOUT_SECONDS = 300;
37
+
38
+ function parseArgs(argv) {
39
+ const out = {};
40
+ for (const arg of argv.slice(2)) {
41
+ const m = arg.match(/^--([a-z-]+)(?:=(.*))?$/);
42
+ if (!m) continue;
43
+ out[m[1]] = m[2] === undefined ? true : m[2];
44
+ }
45
+ return out;
46
+ }
47
+
48
+ function readWorkerName() {
49
+ return process.env.HOSTNAME || `worker-${process.pid}`;
50
+ }
51
+
52
+ function logEvent(rootDir, eventType, fields = {}) {
53
+ try {
54
+ emitTraceEvent({
55
+ rootDir,
56
+ eventType,
57
+ ...fields,
58
+ });
59
+ } catch { /* observability never breaks the worker loop */ }
60
+ }
61
+
62
+ function sleep(ms) {
63
+ return new Promise((resolve) => setTimeout(resolve, ms));
64
+ }
65
+
66
+ /**
67
+ * Map an intake packet to a worker job. The packet's triage names the
68
+ * recommendedAction; we read the project's verification requirements
69
+ * from the task graph (if one was auto-generated) or fall back to a
70
+ * conservative `node --version` smoke command so the worker always
71
+ * has something to execute against and the queue drains.
72
+ */
73
+ function jobFromPacket(packet, { workspace, defaultCommand }) {
74
+ const command = packet?.workerCommand || defaultCommand;
75
+ return {
76
+ jobId: `${packet.id}-${Date.now()}`,
77
+ taskId: packet.id,
78
+ project: packet.intake?.project || null,
79
+ workspaceRef: workspace,
80
+ command,
81
+ args: [],
82
+ timeoutSeconds: packet?.timeoutSeconds || DEFAULT_TIMEOUT_SECONDS,
83
+ envPolicy: 'restricted',
84
+ allowedPaths: [workspace],
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Process one claimed packet end-to-end: run the command, record evidence
90
+ * if a graph node is named in the packet, mark the queue item processed
91
+ * or blocked. Returns true on success, false on failure (which surfaces
92
+ * a blocked packet for human review).
93
+ */
94
+ async function processClaim({ packet, queue, store, rootDir, workspace, defaultCommand, workerName }) {
95
+ const traceId = packet?.traceId || newTraceId();
96
+ const job = jobFromPacket(packet, { workspace, defaultCommand });
97
+
98
+ let result;
99
+ try {
100
+ result = await runJob({
101
+ rootDir,
102
+ job: { ...job, traceId },
103
+ });
104
+ } catch (err) {
105
+ logEvent(rootDir, 'worker.completed', {
106
+ traceId,
107
+ project: job.project,
108
+ taskId: job.taskId,
109
+ metadata: { status: 'errored', error: err?.message || String(err) },
110
+ });
111
+ await queue.markSkipped(packet.id, {
112
+ skippedBy: workerName,
113
+ reason: `worker exec error: ${err?.message || String(err)}`,
114
+ });
115
+ return false;
116
+ }
117
+
118
+ // If the packet carries graphId + nodeId, attach evidence to that node.
119
+ if (packet?.graphId && packet?.nodeId) {
120
+ try {
121
+ const evidence = evidenceFromJobResult(result, {
122
+ evidenceType: packet.evidenceType || 'test-result',
123
+ });
124
+ recordEvidence({
125
+ store,
126
+ graphId: packet.graphId,
127
+ nodeId: packet.nodeId,
128
+ evidence,
129
+ rootDir,
130
+ });
131
+ } catch { /* evidence write best-effort */ }
132
+ }
133
+
134
+ if (result.status === 'passed') {
135
+ await queue.markProcessed(packet.id, {
136
+ processedBy: workerName,
137
+ notes: `passed in ${result.durationMs}ms`,
138
+ });
139
+ return true;
140
+ }
141
+
142
+ // Failure path: surface as a blocked packet with attempted steps so the
143
+ // main agent can pick it up and decide next.
144
+ const reason = result.status === 'timeout'
145
+ ? `command timed out after ${packet?.timeoutSeconds || DEFAULT_TIMEOUT_SECONDS}s`
146
+ : `command failed with exit code ${result.exitCode}`;
147
+ const attempted = [job.command];
148
+ const blocked = blockedPacket({ taskId: packet.id, reason, attempted });
149
+ await queue.markSkipped(packet.id, {
150
+ skippedBy: workerName,
151
+ reason: `${reason}; stdout=${result.stdoutPath}; stderr=${result.stderrPath}`,
152
+ });
153
+ logEvent(rootDir, 'worker.completed', {
154
+ traceId,
155
+ project: job.project,
156
+ taskId: job.taskId,
157
+ metadata: { status: result.status, exitCode: result.exitCode, blocked, durationMs: result.durationMs },
158
+ });
159
+ return false;
160
+ }
161
+
162
+ /**
163
+ * Long-running worker loop. Resolves with the run statistics when the
164
+ * idle timeout elapses or when `stopAfter` jobs have completed (test mode).
165
+ */
166
+ export async function runWorkerLoop({
167
+ rootDir,
168
+ workspace,
169
+ project,
170
+ sql,
171
+ queue: injectedQueue = null,
172
+ store: injectedStore = null,
173
+ pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
174
+ idleTimeoutSeconds = DEFAULT_IDLE_TIMEOUT_SECONDS,
175
+ defaultCommand = 'node --version',
176
+ stopAfter = null,
177
+ now = Date.now,
178
+ } = {}) {
179
+ if (!project) throw new Error('runWorkerLoop: project is required');
180
+ if (!workspace) throw new Error('runWorkerLoop: workspace is required');
181
+ if (!injectedQueue && !sql) throw new Error('runWorkerLoop: sql client (or injected queue) is required');
182
+
183
+ const workerName = readWorkerName();
184
+ const queue = injectedQueue || new PostgresIntakeQueue({ sql, project });
185
+ const store = injectedStore || new FilesystemTaskGraphStore(rootDir);
186
+
187
+ let processed = 0;
188
+ let skipped = 0;
189
+ let idleSince = now();
190
+
191
+ while (true) {
192
+ const claimed = await queue.claim({ claimedBy: workerName });
193
+ if (claimed) {
194
+ const ok = await processClaim({
195
+ packet: claimed,
196
+ queue,
197
+ store,
198
+ rootDir,
199
+ workspace,
200
+ defaultCommand,
201
+ workerName,
202
+ });
203
+ if (ok) processed += 1; else skipped += 1;
204
+ idleSince = now();
205
+ if (stopAfter && (processed + skipped) >= stopAfter) break;
206
+ continue;
207
+ }
208
+
209
+ if ((now() - idleSince) >= idleTimeoutSeconds * 1000) break;
210
+ await sleep(pollIntervalMs);
211
+ }
212
+
213
+ return { processed, skipped, workerName };
214
+ }
215
+
216
+ // CLI entrypoint when invoked directly (the Dockerfile.worker CMD).
217
+ const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
218
+ if (invokedDirectly) {
219
+ const args = parseArgs(process.argv);
220
+ const workspace = args.workspace || process.env.CONSTRUCT_WORKSPACE || process.cwd();
221
+ const rootDir = args['root-dir'] || workspace;
222
+ const project = args.project || process.env.CONSTRUCT_PROJECT_NAME || path.basename(path.resolve(workspace));
223
+ const pollIntervalMs = Number(args['poll-interval-ms'] || DEFAULT_POLL_INTERVAL_MS);
224
+ const idleTimeoutSeconds = Number(args['idle-timeout-seconds'] || DEFAULT_IDLE_TIMEOUT_SECONDS);
225
+
226
+ const sql = createSqlClient(process.env);
227
+ if (!sql) {
228
+ process.stderr.write('[worker] DATABASE_URL is required for the worker entrypoint.\n');
229
+ process.exit(78); // EX_CONFIG
230
+ }
231
+
232
+ try {
233
+ const summary = await runWorkerLoop({
234
+ rootDir, workspace, project, sql, pollIntervalMs, idleTimeoutSeconds,
235
+ });
236
+ process.stdout.write(`[worker] exiting after idle timeout — processed=${summary.processed} skipped=${summary.skipped} worker=${summary.workerName}\n`);
237
+ process.exit(0);
238
+ } finally {
239
+ await closeSqlClient(sql);
240
+ }
241
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * lib/worker/evidence.mjs — record verified evidence onto task graph nodes.
3
+ *
4
+ * Evidence is the contract that lets a node move from in-progress → done.
5
+ * The worker plane emits one evidence record per executed job; the
6
+ * higher-level dispatcher refuses to mark a node done without at least
7
+ * one evidence entry. BLOCKED and NEEDS_MAIN_INPUT statuses carry their
8
+ * own dedicated schemas: BLOCKED requires `attempted` steps; NEEDS_MAIN_INPUT
9
+ * requires `question`, `safeDefault`, and `context` fields.
10
+ *
11
+ * Evidence record shape:
12
+ * {
13
+ * taskId, evidenceType, command?, status, summary,
14
+ * artifacts?, traceId?, createdAt
15
+ * }
16
+ */
17
+
18
+ import { emitTraceEvent } from './trace.mjs';
19
+
20
+ export const EVIDENCE_TYPES = [
21
+ 'test-result',
22
+ 'lint-result',
23
+ 'build-result',
24
+ 'retrieval-eval',
25
+ 'manual-verification',
26
+ 'source-citation',
27
+ 'trace-link',
28
+ 'sample-output',
29
+ ];
30
+
31
+ function buildSummary(jobResult) {
32
+ if (!jobResult) return '';
33
+ const head = `${jobResult.status} · exit ${jobResult.exitCode} · ${jobResult.durationMs}ms`;
34
+ return head;
35
+ }
36
+
37
+ export function evidenceFromJobResult(jobResult, { evidenceType = 'test-result', summary } = {}) {
38
+ if (!jobResult?.taskId) throw new Error('evidenceFromJobResult: jobResult.taskId is required');
39
+ return {
40
+ taskId: jobResult.taskId,
41
+ evidenceType,
42
+ command: jobResult.command || null,
43
+ status: jobResult.status,
44
+ summary: summary || buildSummary(jobResult),
45
+ artifacts: jobResult.artifacts || [],
46
+ traceId: jobResult.traceId || null,
47
+ createdAt: new Date().toISOString(),
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Append an evidence record to a task graph node and emit the matching
53
+ * trace event. Returns the updated node.
54
+ */
55
+ export function recordEvidence({ store, graphId, nodeId, evidence, rootDir, emitEvent = emitTraceEvent }) {
56
+ if (!store) throw new Error('recordEvidence: store is required');
57
+ if (!graphId || !nodeId) throw new Error('recordEvidence: graphId and nodeId are required');
58
+ if (!evidence) throw new Error('recordEvidence: evidence is required');
59
+
60
+ const node = store.updateNodeStatus(graphId, nodeId, undefined, { addEvidence: evidence });
61
+
62
+ if (rootDir) {
63
+ emitEvent({
64
+ rootDir,
65
+ eventType: 'evidence.recorded',
66
+ traceId: evidence.traceId || undefined,
67
+ project: node.project,
68
+ taskId: nodeId,
69
+ metadata: {
70
+ evidenceType: evidence.evidenceType,
71
+ status: evidence.status,
72
+ summary: evidence.summary,
73
+ },
74
+ });
75
+ }
76
+ return node;
77
+ }
78
+
79
+ export function blockedPacket({ taskId, attempted = [], reason }) {
80
+ if (!taskId) throw new Error('blockedPacket: taskId is required');
81
+ if (!reason) throw new Error('blockedPacket: reason is required');
82
+ if (!Array.isArray(attempted) || attempted.length === 0) {
83
+ throw new Error('blockedPacket: at least one attempted step is required');
84
+ }
85
+ return {
86
+ taskId,
87
+ status: 'blocked',
88
+ reason,
89
+ attempted,
90
+ createdAt: new Date().toISOString(),
91
+ };
92
+ }
93
+
94
+ export function needsInputPacket({ taskId, question, safeDefault, context }) {
95
+ if (!taskId) throw new Error('needsInputPacket: taskId is required');
96
+ if (!question) throw new Error('needsInputPacket: question is required');
97
+ if (safeDefault === undefined) throw new Error('needsInputPacket: safeDefault is required (use null if none)');
98
+ if (!context) throw new Error('needsInputPacket: context is required');
99
+ return {
100
+ taskId,
101
+ status: 'needs-input',
102
+ question,
103
+ safeDefault,
104
+ context,
105
+ createdAt: new Date().toISOString(),
106
+ };
107
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * lib/worker/run.mjs — bounded command execution for the worker plane.
3
+ *
4
+ * Runs a single shell command with a hard timeout and a path-policy
5
+ * check so workers cannot write outside their allowed workspace. The
6
+ * worker plane stays decoupled from the control plane: this module
7
+ * accepts a job descriptor and returns a job result; it does not pick
8
+ * what to run or interpret the result. Higher layers (intake claim,
9
+ * task graph dispatcher, evidence recorder) glue the pieces together.
10
+ *
11
+ * Job input:
12
+ * { jobId, taskId, project, workspaceRef, command, args?,
13
+ * timeoutSeconds, envPolicy, allowedPaths, network }
14
+ *
15
+ * Job output:
16
+ * { jobId, taskId, status, exitCode, stdoutPath, stderrPath,
17
+ * durationMs, artifacts, traceId, spanId }
18
+ *
19
+ * `envPolicy: 'restricted'` strips the env down to PATH, HOME, USER,
20
+ * TZ, LANG plus an explicit allowlist. Path policy is enforced by
21
+ * resolving every entry in allowedPaths and refusing to start when
22
+ * the resolved workspaceRef sits outside that allowlist.
23
+ */
24
+
25
+ import { spawn } from 'node:child_process';
26
+ import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
27
+ import path from 'node:path';
28
+
29
+ import { emitTraceEvent, newTraceId, newSpanId } from './trace.mjs';
30
+
31
+ const DEFAULT_TIMEOUT_SECONDS = 300;
32
+ const ARTIFACTS_SUBDIR = '.cx/runtime/worker';
33
+
34
+ function artifactsDir(rootDir) {
35
+ return path.join(rootDir, ARTIFACTS_SUBDIR);
36
+ }
37
+
38
+ function buildRestrictedEnv(allowedKeys = []) {
39
+ const base = ['PATH', 'HOME', 'USER', 'TZ', 'LANG', 'LC_ALL', 'TMPDIR'];
40
+ const env = {};
41
+ for (const key of [...base, ...allowedKeys]) {
42
+ if (process.env[key]) env[key] = process.env[key];
43
+ }
44
+ return env;
45
+ }
46
+
47
+ function isInsideAllowedPaths(workspaceRef, allowedPaths) {
48
+ if (!allowedPaths || allowedPaths.length === 0) return true;
49
+ const target = path.resolve(workspaceRef);
50
+ return allowedPaths
51
+ .map((p) => path.resolve(p))
52
+ .some((allowed) => target === allowed || target.startsWith(`${allowed}${path.sep}`));
53
+ }
54
+
55
+ /**
56
+ * Run a worker job. Resolves with the result regardless of command outcome;
57
+ * rejects only on programmer errors (invalid job shape, path policy denial).
58
+ */
59
+ export async function runJob({
60
+ rootDir,
61
+ job,
62
+ emitEvent = emitTraceEvent,
63
+ spawnFn = spawn,
64
+ } = {}) {
65
+ if (!rootDir) throw new Error('runJob: rootDir is required');
66
+ if (!job?.jobId) throw new Error('runJob: job.jobId is required');
67
+ if (!job?.command) throw new Error('runJob: job.command is required');
68
+
69
+ const traceId = job.traceId || newTraceId();
70
+ const spanId = newSpanId();
71
+ const timeoutSeconds = Number(job.timeoutSeconds || DEFAULT_TIMEOUT_SECONDS);
72
+ const workspaceRef = path.resolve(job.workspaceRef || rootDir);
73
+
74
+ if (!isInsideAllowedPaths(workspaceRef, job.allowedPaths)) {
75
+ throw new Error(`runJob: workspaceRef ${workspaceRef} is outside allowedPaths`);
76
+ }
77
+
78
+ const artifactsRoot = artifactsDir(rootDir);
79
+ if (!existsSync(artifactsRoot)) mkdirSync(artifactsRoot, { recursive: true });
80
+ const stdoutPath = path.join(artifactsRoot, `${job.jobId}.stdout.log`);
81
+ const stderrPath = path.join(artifactsRoot, `${job.jobId}.stderr.log`);
82
+
83
+ const env = job.envPolicy === 'restricted'
84
+ ? buildRestrictedEnv(job.allowedEnvKeys)
85
+ : { ...process.env };
86
+
87
+ emitEvent({
88
+ rootDir,
89
+ eventType: 'worker.started',
90
+ traceId,
91
+ spanId,
92
+ project: job.project,
93
+ taskId: job.taskId,
94
+ metadata: { jobId: job.jobId, command: job.command, timeoutSeconds, workspaceRef },
95
+ });
96
+
97
+ const started = Date.now();
98
+ const child = spawnFn(job.command, job.args || [], {
99
+ cwd: workspaceRef,
100
+ env,
101
+ stdio: ['ignore', 'pipe', 'pipe'],
102
+ shell: typeof job.command === 'string' && (!job.args || job.args.length === 0),
103
+ });
104
+
105
+ let stdout = '';
106
+ let stderr = '';
107
+ let timedOut = false;
108
+ child.stdout.on('data', (d) => { stdout += d.toString(); });
109
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
110
+
111
+ const timer = setTimeout(() => {
112
+ timedOut = true;
113
+ child.kill('SIGTERM');
114
+ setTimeout(() => child.kill('SIGKILL'), 2000).unref();
115
+ }, timeoutSeconds * 1000);
116
+
117
+ const exitCode = await new Promise((resolve) => {
118
+ child.once('close', (code) => resolve(code ?? -1));
119
+ child.once('error', () => resolve(-1));
120
+ });
121
+ clearTimeout(timer);
122
+
123
+ writeFileSync(stdoutPath, stdout, 'utf8');
124
+ writeFileSync(stderrPath, stderr, 'utf8');
125
+
126
+ const durationMs = Date.now() - started;
127
+ const status = timedOut ? 'timeout' : exitCode === 0 ? 'passed' : 'failed';
128
+
129
+ const result = {
130
+ jobId: job.jobId,
131
+ taskId: job.taskId || null,
132
+ project: job.project || null,
133
+ status,
134
+ exitCode,
135
+ stdoutPath,
136
+ stderrPath,
137
+ durationMs,
138
+ artifacts: [{ path: stdoutPath, kind: 'stdout' }, { path: stderrPath, kind: 'stderr' }],
139
+ traceId,
140
+ spanId,
141
+ };
142
+
143
+ emitEvent({
144
+ rootDir,
145
+ eventType: 'worker.completed',
146
+ traceId,
147
+ spanId,
148
+ project: job.project,
149
+ taskId: job.taskId,
150
+ metadata: { jobId: job.jobId, status, exitCode, durationMs, stdoutPath, stderrPath, timedOut },
151
+ });
152
+
153
+ return result;
154
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * lib/worker/trace.mjs — append-only trace event log for worker + intake flow.
3
+ *
4
+ * Trace events tie an intake packet → task graph → role dispatch → worker
5
+ * job → evidence record together via traceId + spanId. Events land in two
6
+ * places concurrently:
7
+ *
8
+ * 1. `.cx/traces/<YYYY-MM-DD>.jsonl` — append-only local JSONL. Always on,
9
+ * no credentials required. Tail live, archive, or replay.
10
+ * 2. Remote telemetry backend — fire-and-forget batched ingest when
11
+ * CONSTRUCT_TELEMETRY_PUBLIC_KEY + CONSTRUCT_TELEMETRY_SECRET_KEY are
12
+ * configured and CONSTRUCT_TRACE_BACKEND is `remote` (or unset).
13
+ * Each unique traceId triggers one trace-create plus an event-create
14
+ * per emitted event. Failures never throw — observability must not
15
+ * break the caller.
16
+ *
17
+ * Reads CONSTRUCT_TELEMETRY_PUBLIC_KEY / CONSTRUCT_TELEMETRY_SECRET_KEY /
18
+ * CONSTRUCT_TELEMETRY_URL from env.
19
+ *
20
+ * Event types in current use:
21
+ * intake.received — daemon ingested a file
22
+ * intake.triaged — classifyRdIntake produced a triage block
23
+ * task_graph.created — graph derived from triage
24
+ * role.dispatched — context router selected a persona
25
+ * tool.called — agent invoked a tool
26
+ * worker.started — worker began executing a command
27
+ * worker.completed — worker finished (status: passed | failed | timeout)
28
+ * evidence.recorded — evidence appended to a task graph node
29
+ * approval.requested — high-risk action blocked on approval
30
+ * approval.resolved — approval granted or denied
31
+ * memory.written — durable memory record persisted
32
+ */
33
+
34
+ import { existsSync, mkdirSync, appendFileSync } from 'node:fs';
35
+ import { randomBytes } from 'node:crypto';
36
+ import path from 'node:path';
37
+
38
+ import { createIngestClient } from '../telemetry/ingest.mjs';
39
+
40
+ const TRACE_SUBDIR = '.cx/traces';
41
+
42
+ export const TRACE_EVENT_TYPES = [
43
+ 'daemon.started',
44
+ 'daemon.heartbeat',
45
+ 'daemon.stopped',
46
+ 'lifecycle.completed',
47
+ 'intake.received',
48
+ 'intake.triaged',
49
+ 'task_graph.created',
50
+ 'role.dispatched',
51
+ 'tool.called',
52
+ 'worker.started',
53
+ 'worker.completed',
54
+ 'evidence.recorded',
55
+ 'approval.requested',
56
+ 'approval.resolved',
57
+ 'memory.written',
58
+ ];
59
+
60
+ export function traceDir(rootDir) {
61
+ return path.join(rootDir, TRACE_SUBDIR);
62
+ }
63
+
64
+ function todayShard() {
65
+ return new Date().toISOString().slice(0, 10);
66
+ }
67
+
68
+ export function newTraceId() {
69
+ return `trace-${randomBytes(8).toString('hex')}`;
70
+ }
71
+
72
+ export function newSpanId() {
73
+ return `span-${randomBytes(6).toString('hex')}`;
74
+ }
75
+
76
+ // Reused per-process. The ingest client batches; recreating it would lose
77
+ // the queue. `available` flips false when keys aren't set, which is the
78
+ // graceful no-op path for solo-mode users without no telemetry backend configured.
79
+ let telemetryClient = null;
80
+ const seenTraces = new Set();
81
+
82
+ function getTelemetryClient(env = process.env) {
83
+ if (telemetryClient !== null) return telemetryClient;
84
+ const backend = (env.CONSTRUCT_TRACE_BACKEND || 'remote').toLowerCase();
85
+ if (backend === 'none' || backend === 'off' || backend === 'local') {
86
+ telemetryClient = { available: false, trace: () => {}, event: () => {} };
87
+ return telemetryClient;
88
+ }
89
+ telemetryClient = createIngestClient({
90
+ baseUrl: (env.CONSTRUCT_TELEMETRY_URL ?? '').replace(/\/$/, ''),
91
+ publicKey: env.CONSTRUCT_TELEMETRY_PUBLIC_KEY,
92
+ secretKey: env.CONSTRUCT_TELEMETRY_SECRET_KEY,
93
+ });
94
+ return telemetryClient;
95
+ }
96
+
97
+ /**
98
+ * Internal: reset the cached telemetry client. Test-only.
99
+ */
100
+ export function _resetTelemetryClient() {
101
+ telemetryClient = null;
102
+ seenTraces.clear();
103
+ }
104
+
105
+
106
+ function exportToRemote(event, env) {
107
+ const client = getTelemetryClient(env);
108
+ if (!client.available) return;
109
+
110
+ // First time we see a traceId, register the trace so subsequent events
111
+ // attach to a known parent.
112
+ if (event.traceId && !seenTraces.has(event.traceId)) {
113
+ seenTraces.add(event.traceId);
114
+ try {
115
+ client.trace({
116
+ id: event.traceId,
117
+ name: event.eventType,
118
+ metadata: {
119
+ project: event.project || undefined,
120
+ role: event.role || undefined,
121
+ taskId: event.taskId || undefined,
122
+ intakeId: event.intakeId || undefined,
123
+ },
124
+ timestamp: event.createdAt,
125
+ });
126
+ } catch { /* observability must not break the caller */ }
127
+ }
128
+
129
+ try {
130
+ client.event({
131
+ id: event.spanId,
132
+ traceId: event.traceId,
133
+ parentObservationId: event.parentSpanId || undefined,
134
+ name: event.eventType,
135
+ metadata: event.metadata,
136
+ input: undefined,
137
+ output: undefined,
138
+ startTime: event.createdAt,
139
+ });
140
+ } catch { /* swallow */ }
141
+ }
142
+
143
+ export function emitTraceEvent({
144
+ rootDir,
145
+ eventType,
146
+ traceId,
147
+ spanId,
148
+ parentSpanId = null,
149
+ project = null,
150
+ role = null,
151
+ taskId = null,
152
+ intakeId = null,
153
+ metadata = {},
154
+ env = process.env,
155
+ }) {
156
+ if (!rootDir) throw new Error('emitTraceEvent: rootDir is required');
157
+ if (!eventType) throw new Error('emitTraceEvent: eventType is required');
158
+ if (!TRACE_EVENT_TYPES.includes(eventType)) {
159
+ throw new Error(`emitTraceEvent: unknown eventType ${eventType}`);
160
+ }
161
+
162
+ const dir = traceDir(rootDir);
163
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
164
+
165
+ const event = {
166
+ traceId: traceId || newTraceId(),
167
+ spanId: spanId || newSpanId(),
168
+ parentSpanId,
169
+ eventType,
170
+ project,
171
+ role,
172
+ taskId,
173
+ intakeId,
174
+ metadata,
175
+ createdAt: new Date().toISOString(),
176
+ };
177
+ appendFileSync(path.join(dir, `${todayShard()}.jsonl`), `${JSON.stringify(event)}\n`, 'utf8');
178
+
179
+ // Fire-and-forget export to remote telemetry backend. Silent no-op when not configured.
180
+ exportToRemote(event, env);
181
+ return event;
182
+ }