@geraldmaron/construct 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.env.example +10 -7
  2. package/LICENSE +201 -21
  3. package/README.md +132 -257
  4. package/agents/contracts.json +387 -0
  5. package/agents/prompts/cx-accessibility.md +8 -0
  6. package/agents/prompts/cx-ai-engineer.md +92 -0
  7. package/agents/prompts/cx-architect.md +10 -2
  8. package/agents/prompts/cx-business-strategist.md +13 -0
  9. package/agents/prompts/cx-data-analyst.md +80 -0
  10. package/agents/prompts/cx-data-engineer.md +4 -0
  11. package/agents/prompts/cx-debugger.md +8 -0
  12. package/agents/prompts/cx-designer.md +19 -0
  13. package/agents/prompts/cx-devil-advocate.md +4 -0
  14. package/agents/prompts/cx-docs-keeper.md +128 -0
  15. package/agents/prompts/cx-engineer.md +13 -0
  16. package/agents/prompts/cx-evaluator.md +4 -0
  17. package/agents/prompts/cx-explorer.md +12 -0
  18. package/agents/prompts/cx-legal-compliance.md +13 -0
  19. package/agents/prompts/cx-operations.md +13 -0
  20. package/agents/prompts/cx-orchestrator.md +107 -4
  21. package/agents/prompts/cx-platform-engineer.md +75 -0
  22. package/agents/prompts/cx-product-manager.md +8 -0
  23. package/agents/prompts/cx-qa.md +100 -0
  24. package/agents/prompts/cx-rd-lead.md +13 -0
  25. package/agents/prompts/cx-release-manager.md +8 -0
  26. package/agents/prompts/cx-researcher.md +21 -1
  27. package/agents/prompts/cx-reviewer.md +8 -0
  28. package/agents/prompts/cx-security.md +104 -0
  29. package/agents/prompts/cx-sre.md +104 -0
  30. package/agents/prompts/cx-test-automation.md +4 -0
  31. package/agents/prompts/cx-trace-reviewer.md +7 -3
  32. package/agents/prompts/cx-ux-researcher.md +4 -0
  33. package/agents/registry.json +365 -90
  34. package/agents/role-manifests.json +217 -0
  35. package/bin/construct +3345 -141
  36. package/bin/construct-postinstall.mjs +87 -0
  37. package/commands/build/feature.md +6 -6
  38. package/commands/plan/feature.md +6 -5
  39. package/commands/plan/requirements.md +1 -1
  40. package/commands/ship/status.md +1 -1
  41. package/commands/work/drive.md +3 -3
  42. package/commands/work/optimize-prompts.md +3 -3
  43. package/db/{migrations → schema}/001_init.sql +2 -0
  44. package/db/schema/002_pgvector.sql +182 -0
  45. package/db/schema/003_intake.sql +47 -0
  46. package/examples/README.md +85 -0
  47. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
  48. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
  49. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
  50. package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
  51. package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
  52. package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
  53. package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
  54. package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
  55. package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
  56. package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
  57. package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
  58. package/examples/personas/construct/bad/commit-without-approval.md +30 -0
  59. package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
  60. package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
  61. package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
  62. package/examples/provider-plugin/README.md +34 -0
  63. package/examples/provider-plugin/index.mjs +74 -0
  64. package/examples/provider-plugin/package.json +15 -0
  65. package/examples/seed-observations/README.md +38 -0
  66. package/examples/seed-observations/anti-patterns.md +44 -0
  67. package/examples/seed-observations/decisions.md +36 -0
  68. package/examples/seed-observations/patterns.md +42 -0
  69. package/lib/agent-contracts-enforce.mjs +158 -0
  70. package/lib/agent-contracts.mjs +231 -0
  71. package/lib/agents/postconditions.mjs +126 -0
  72. package/lib/agents/schema.mjs +124 -0
  73. package/lib/artifact-capture.mjs +183 -0
  74. package/lib/audit-trail.mjs +149 -0
  75. package/lib/auto-docs.mjs +245 -65
  76. package/lib/beads/auto-close.mjs +126 -0
  77. package/lib/beads/drift.mjs +171 -0
  78. package/lib/beads-automation.mjs +542 -0
  79. package/lib/beads-client.mjs +518 -0
  80. package/lib/beads-lock.mjs +377 -0
  81. package/lib/beads-optimistic.mjs +365 -0
  82. package/lib/bootstrap/built-ins.mjs +136 -0
  83. package/lib/bootstrap/lazy-install.mjs +161 -0
  84. package/lib/bootstrap/resources.mjs +120 -0
  85. package/lib/bootstrap.mjs +105 -0
  86. package/lib/cache-governor.js +213 -0
  87. package/lib/cache-strategy-anthropic.js +62 -0
  88. package/lib/cache-strategy-google.js +79 -0
  89. package/lib/cache-strategy-none.js +30 -0
  90. package/lib/cache-strategy-openai.js +48 -0
  91. package/lib/cache-strategy.js +91 -0
  92. package/lib/claude-allow.mjs +149 -0
  93. package/lib/cli-commands.mjs +413 -258
  94. package/lib/codex-config.mjs +3 -1
  95. package/lib/comment-lint.mjs +55 -6
  96. package/lib/completions.mjs +21 -6
  97. package/lib/config/alias.mjs +56 -0
  98. package/lib/config/project-config.mjs +335 -0
  99. package/lib/config/schema.mjs +159 -0
  100. package/lib/context-router.mjs +308 -0
  101. package/lib/cost-ledger.mjs +177 -0
  102. package/lib/cost.mjs +171 -10
  103. package/lib/dashboard-static.mjs +158 -0
  104. package/lib/deployment-mode.mjs +86 -0
  105. package/lib/deprecate.mjs +49 -0
  106. package/lib/dispatch-batch.js +183 -0
  107. package/lib/distill.mjs +21 -8
  108. package/lib/doc-stamp.mjs +164 -0
  109. package/lib/doc-verify.mjs +119 -0
  110. package/lib/docs-routing.mjs +89 -0
  111. package/lib/docs-verify.mjs +417 -0
  112. package/lib/doctor/audit.mjs +71 -0
  113. package/lib/doctor/cli.mjs +99 -0
  114. package/lib/doctor/escalate.mjs +29 -0
  115. package/lib/doctor/index.mjs +140 -0
  116. package/lib/doctor/report.mjs +170 -0
  117. package/lib/doctor/watchers/bd-watch.mjs +117 -0
  118. package/lib/doctor/watchers/cost.mjs +130 -0
  119. package/lib/doctor/watchers/disk.mjs +122 -0
  120. package/lib/doctor/watchers/handoffs.mjs +33 -0
  121. package/lib/doctor/watchers/process-pressure.mjs +60 -0
  122. package/lib/doctor/watchers/service-health.mjs +188 -0
  123. package/lib/document-extract.mjs +288 -0
  124. package/lib/document-ingest.mjs +230 -0
  125. package/lib/drop.mjs +282 -0
  126. package/lib/embed/approval-queue.mjs +176 -0
  127. package/lib/embed/artifact.mjs +349 -0
  128. package/lib/embed/authority-guard.mjs +155 -0
  129. package/lib/embed/cli.mjs +408 -0
  130. package/lib/embed/config.mjs +355 -0
  131. package/lib/embed/conflict-detection.mjs +264 -0
  132. package/lib/embed/customer-profiles.mjs +480 -0
  133. package/lib/embed/daemon.mjs +1309 -0
  134. package/lib/embed/demand-fetch.mjs +449 -0
  135. package/lib/embed/docs-lifecycle.mjs +349 -0
  136. package/lib/embed/inbox-live-watcher.mjs +119 -0
  137. package/lib/embed/inbox.mjs +343 -0
  138. package/lib/embed/intake-metrics.mjs +190 -0
  139. package/lib/embed/jobs/vector-sync.mjs +198 -0
  140. package/lib/embed/notifications.mjs +75 -0
  141. package/lib/embed/output.mjs +79 -0
  142. package/lib/embed/providers/github.mjs +295 -0
  143. package/lib/embed/providers/jira.mjs +192 -0
  144. package/lib/embed/providers/linear.mjs +186 -0
  145. package/lib/embed/providers/registry.mjs +115 -0
  146. package/lib/embed/providers/slack.mjs +203 -0
  147. package/lib/embed/recommendation-store.mjs +378 -0
  148. package/lib/embed/roadmap.mjs +374 -0
  149. package/lib/embed/role-framing.mjs +110 -0
  150. package/lib/embed/scheduler.mjs +99 -0
  151. package/lib/embed/semantic.mjs +325 -0
  152. package/lib/embed/snapshot.mjs +191 -0
  153. package/lib/embed/supervision.mjs +235 -0
  154. package/lib/embed/target-resolver.mjs +186 -0
  155. package/lib/embed/worker.mjs +62 -0
  156. package/lib/embed/workspaces.mjs +297 -0
  157. package/lib/engine/chunker-headings.mjs +110 -0
  158. package/lib/engine/compressor-heuristic.mjs +100 -0
  159. package/lib/engine/consolidate.mjs +287 -0
  160. package/lib/engine/contracts.mjs +126 -0
  161. package/lib/engine/defaults.mjs +129 -0
  162. package/lib/engine/eval-retrieval.mjs +148 -0
  163. package/lib/engine/fuser-rrf.mjs +62 -0
  164. package/lib/engine/index.mjs +37 -0
  165. package/lib/engine/registry.mjs +146 -0
  166. package/lib/engine/reranker-mmr.mjs +90 -0
  167. package/lib/engine/tokens.mjs +77 -0
  168. package/lib/entity-store.mjs +280 -0
  169. package/lib/env-config.mjs +69 -4
  170. package/lib/evals/retrieval-bench.mjs +159 -0
  171. package/lib/evaluator-optimizer.mjs +317 -0
  172. package/lib/features.mjs +159 -29
  173. package/lib/gates-audit.mjs +236 -0
  174. package/lib/git-hooks/prepare-commit-msg +58 -0
  175. package/lib/handoffs/cleanup.mjs +159 -0
  176. package/lib/handoffs/contract.mjs +162 -0
  177. package/lib/handoffs/inventory.mjs +120 -0
  178. package/lib/headhunt.mjs +28 -47
  179. package/lib/health-check.mjs +399 -0
  180. package/lib/hook-health.mjs +442 -0
  181. package/lib/hooks/_lib/log.mjs +82 -0
  182. package/lib/hooks/adaptive-lint.mjs +26 -2
  183. package/lib/hooks/agent-tracker.mjs +171 -14
  184. package/lib/hooks/audit-reads.mjs +109 -0
  185. package/lib/hooks/audit-trail.mjs +153 -0
  186. package/lib/hooks/bash-output-logger.mjs +71 -0
  187. package/lib/hooks/block-no-verify.mjs +41 -0
  188. package/lib/hooks/ci-status-check.mjs +82 -0
  189. package/lib/hooks/comment-lint.mjs +29 -7
  190. package/lib/hooks/config-protection.mjs +39 -18
  191. package/lib/hooks/context-watch.mjs +137 -0
  192. package/lib/hooks/context-window-recovery.mjs +4 -20
  193. package/lib/hooks/dep-audit.mjs +16 -0
  194. package/lib/hooks/doc-coupling-check.mjs +80 -0
  195. package/lib/hooks/edit-accumulator.mjs +3 -0
  196. package/lib/hooks/edit-error-recovery.mjs +3 -0
  197. package/lib/hooks/edit-guard.mjs +45 -4
  198. package/lib/hooks/env-check.mjs +3 -0
  199. package/lib/hooks/guard-bash.mjs +58 -1
  200. package/lib/hooks/mcp-audit.mjs +18 -20
  201. package/lib/hooks/mcp-health-check.mjs +36 -0
  202. package/lib/hooks/model-fallback.mjs +42 -56
  203. package/lib/hooks/policy-engine.mjs +209 -0
  204. package/lib/hooks/post-merge-docs-check.mjs +63 -0
  205. package/lib/hooks/pre-compact.mjs +4 -24
  206. package/lib/hooks/pre-push-gate.mjs +200 -37
  207. package/lib/hooks/proactive-activation.mjs +284 -0
  208. package/lib/hooks/read-tracker.mjs +27 -4
  209. package/lib/hooks/readme-age-check.mjs +77 -0
  210. package/lib/hooks/registry-sync.mjs +12 -5
  211. package/lib/hooks/scan-secrets.mjs +50 -7
  212. package/lib/hooks/session-optimize.mjs +311 -0
  213. package/lib/hooks/session-start.mjs +287 -28
  214. package/lib/hooks/stop-notify.mjs +236 -96
  215. package/lib/hooks/stop-typecheck.mjs +13 -1
  216. package/lib/hooks/test-watch.mjs +68 -0
  217. package/lib/host-capabilities.mjs +31 -10
  218. package/lib/init-docs.mjs +731 -291
  219. package/lib/init-unified.mjs +1116 -0
  220. package/lib/init-update.mjs +168 -0
  221. package/lib/init.mjs +107 -0
  222. package/lib/install/first-invocation.mjs +119 -0
  223. package/lib/install/stage-project.mjs +69 -0
  224. package/lib/intake/classify.mjs +278 -0
  225. package/lib/intake/feedback.mjs +273 -0
  226. package/lib/intake/filesystem-queue.mjs +158 -0
  227. package/lib/intake/intake-config.mjs +132 -0
  228. package/lib/intake/postgres-queue.mjs +198 -0
  229. package/lib/intake/prepare.mjs +139 -0
  230. package/lib/intake/queue.mjs +83 -0
  231. package/lib/intake/session-prelude.mjs +50 -0
  232. package/lib/integrations/intake-integrations.mjs +740 -0
  233. package/lib/intent-classifier.mjs +253 -0
  234. package/lib/knowledge/layout.mjs +72 -0
  235. package/lib/knowledge/rag.mjs +331 -0
  236. package/lib/knowledge/search.mjs +315 -0
  237. package/lib/knowledge/trends.mjs +261 -0
  238. package/lib/logger.mjs +85 -0
  239. package/lib/mcp/broker.mjs +124 -0
  240. package/lib/mcp/server.mjs +554 -854
  241. package/lib/mcp/tools/document.mjs +132 -0
  242. package/lib/mcp/tools/memory.mjs +209 -0
  243. package/lib/mcp/tools/project.mjs +349 -0
  244. package/lib/mcp/tools/skills.mjs +409 -0
  245. package/lib/mcp/tools/storage.mjs +60 -0
  246. package/lib/mcp/tools/telemetry.mjs +315 -0
  247. package/lib/mcp/tools/workflow.mjs +112 -0
  248. package/lib/mcp-catalog.json +54 -3
  249. package/lib/mcp-manager.mjs +96 -48
  250. package/lib/mcp-platform-config.mjs +22 -22
  251. package/lib/memory-stats.mjs +121 -0
  252. package/lib/mode-commands.mjs +124 -0
  253. package/lib/model-free-selector.mjs +184 -0
  254. package/lib/model-pricing.mjs +152 -0
  255. package/lib/model-registry.mjs +226 -0
  256. package/lib/model-router.mjs +362 -386
  257. package/lib/observation-store.mjs +391 -0
  258. package/lib/ollama-manager.mjs +428 -0
  259. package/lib/opencode-config.mjs +13 -3
  260. package/lib/opencode-runtime-plugin.mjs +70 -38
  261. package/lib/opencode-telemetry.mjs +64 -6
  262. package/lib/orchestration-policy.mjs +509 -6
  263. package/lib/overrides/resolver.mjs +207 -0
  264. package/lib/parity.mjs +147 -0
  265. package/lib/paths.mjs +33 -0
  266. package/lib/performance/generate.mjs +212 -0
  267. package/lib/plugin-registry.mjs +268 -0
  268. package/lib/policy/engine.mjs +130 -0
  269. package/lib/policy/unified-gates.mjs +96 -0
  270. package/lib/project-detection.mjs +129 -0
  271. package/lib/project-init-shared.mjs +272 -0
  272. package/lib/project-profile.mjs +447 -0
  273. package/lib/prompt-composer.js +434 -0
  274. package/lib/prompt-metadata.mjs +1 -1
  275. package/lib/provider-capabilities-anthropic.js +44 -0
  276. package/lib/provider-capabilities-deepseek.js +37 -0
  277. package/lib/provider-capabilities-generic.js +26 -0
  278. package/lib/provider-capabilities-google.js +47 -0
  279. package/lib/provider-capabilities-openai.js +45 -0
  280. package/lib/provider-capabilities.js +142 -0
  281. package/lib/providers/atlassian-confluence/index.mjs +103 -0
  282. package/lib/providers/atlassian-jira/index.mjs +100 -0
  283. package/lib/providers/auth-manager.mjs +126 -0
  284. package/lib/providers/circuit-breaker.mjs +124 -0
  285. package/lib/providers/contract.mjs +93 -0
  286. package/lib/providers/github/index.mjs +126 -0
  287. package/lib/providers/registry.mjs +184 -0
  288. package/lib/providers/salesforce/index.mjs +100 -0
  289. package/lib/providers/slack/index.mjs +80 -0
  290. package/lib/reflect.mjs +137 -0
  291. package/lib/research-lint.mjs +164 -0
  292. package/lib/resources/budget.mjs +259 -0
  293. package/lib/role-preload.mjs +21 -6
  294. package/lib/roles/approval-surface.mjs +54 -0
  295. package/lib/roles/cli.mjs +118 -0
  296. package/lib/roles/event-bus.mjs +79 -0
  297. package/lib/roles/fence.mjs +84 -0
  298. package/lib/roles/gateway.mjs +260 -0
  299. package/lib/roles/hook-emit.mjs +37 -0
  300. package/lib/roles/manifest.mjs +48 -0
  301. package/lib/roles/router.mjs +27 -0
  302. package/lib/runtime-pressure.mjs +360 -0
  303. package/lib/schema-artifact.mjs +134 -0
  304. package/lib/schema-infer.mjs +551 -0
  305. package/lib/server/auth.mjs +168 -0
  306. package/lib/server/chat.mjs +336 -0
  307. package/lib/server/cors.mjs +77 -0
  308. package/lib/server/csrf.mjs +91 -0
  309. package/lib/server/index.mjs +1927 -78
  310. package/lib/server/insights.mjs +765 -0
  311. package/lib/server/rate-limit.mjs +91 -0
  312. package/lib/server/static/assets/index-ab25c707.js +70 -0
  313. package/lib/server/static/assets/index-f0c80a2b.css +1 -0
  314. package/lib/server/static/index.html +12 -817
  315. package/lib/server/telemetry-login.mjs +108 -0
  316. package/lib/server/webhook.mjs +510 -0
  317. package/lib/service-manager.mjs +522 -58
  318. package/lib/services/pattern-promotion-service.mjs +167 -0
  319. package/lib/services/telemetry-backend.mjs +178 -0
  320. package/lib/session-store.mjs +374 -0
  321. package/lib/setup-prompts.mjs +96 -0
  322. package/lib/setup.mjs +525 -38
  323. package/lib/skills-apply.mjs +280 -0
  324. package/lib/skills-scope.mjs +118 -0
  325. package/lib/status.mjs +261 -70
  326. package/lib/storage/admin.mjs +355 -0
  327. package/lib/storage/backend.mjs +2 -1
  328. package/lib/storage/backup.mjs +347 -0
  329. package/lib/storage/embeddings-engine.mjs +133 -0
  330. package/lib/storage/embeddings-legacy.mjs +85 -0
  331. package/lib/storage/embeddings-local.mjs +108 -0
  332. package/lib/storage/embeddings-ollama.mjs +78 -0
  333. package/lib/storage/embeddings-openai.mjs +85 -0
  334. package/lib/storage/embeddings.mjs +92 -33
  335. package/lib/storage/file-lock.mjs +130 -0
  336. package/lib/storage/fusion.mjs +95 -0
  337. package/lib/storage/hybrid-query.mjs +34 -27
  338. package/lib/storage/migrations.mjs +187 -0
  339. package/lib/storage/postgres-backup.mjs +124 -0
  340. package/lib/storage/sql-store.mjs +5 -15
  341. package/lib/storage/state-source.mjs +12 -13
  342. package/lib/storage/store-version.mjs +115 -0
  343. package/lib/storage/sync.mjs +144 -35
  344. package/lib/storage/unified-storage.mjs +550 -0
  345. package/lib/storage/vector-client.mjs +286 -0
  346. package/lib/storage/vector-store.mjs +71 -30
  347. package/lib/task-graph/generate.mjs +135 -0
  348. package/lib/task-graph/schema.mjs +81 -0
  349. package/lib/task-graph/store.mjs +71 -0
  350. package/lib/telemetry/backends/local.mjs +62 -0
  351. package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
  352. package/lib/telemetry/backfill.mjs +180 -0
  353. package/lib/telemetry/eval-datasets.mjs +203 -0
  354. package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
  355. package/lib/telemetry/intent-verifications.mjs +86 -0
  356. package/lib/telemetry/llm-judge.mjs +350 -0
  357. package/lib/telemetry/model-pricing-catalog.mjs +557 -0
  358. package/lib/telemetry/setup.mjs +151 -0
  359. package/lib/telemetry/skill-calls.mjs +78 -0
  360. package/lib/telemetry/team-rollup.mjs +4 -4
  361. package/lib/token-engine.js +117 -0
  362. package/lib/token-estimator-anthropic.js +15 -0
  363. package/lib/token-estimator-deepseek.js +13 -0
  364. package/lib/token-estimator-default.js +13 -0
  365. package/lib/token-estimator-google.js +13 -0
  366. package/lib/token-estimator-openai.js +13 -0
  367. package/lib/toolkit-env.mjs +1 -1
  368. package/lib/tty-prompts.mjs +211 -0
  369. package/lib/uninstall/uninstall.mjs +423 -0
  370. package/lib/update.mjs +115 -0
  371. package/lib/upgrade.mjs +141 -0
  372. package/lib/validator.mjs +51 -2
  373. package/lib/validators/skills.mjs +142 -0
  374. package/lib/wireframe.mjs +422 -0
  375. package/lib/worker/entrypoint.mjs +241 -0
  376. package/lib/worker/evidence.mjs +107 -0
  377. package/lib/worker/run.mjs +154 -0
  378. package/lib/worker/trace.mjs +182 -0
  379. package/lib/workflow-state.mjs +14 -18
  380. package/package.json +21 -8
  381. package/personas/construct.md +53 -51
  382. package/platforms/claude/CLAUDE.md +1 -1
  383. package/platforms/claude/settings.template.json +115 -68
  384. package/platforms/opencode/config.template.json +3 -7
  385. package/rules/common/agents.md +11 -38
  386. package/rules/common/beads-hygiene.md +75 -0
  387. package/rules/common/code-review.md +11 -100
  388. package/rules/common/coding-style.md +5 -3
  389. package/rules/common/comments.md +30 -111
  390. package/rules/common/commit-approval.md +53 -0
  391. package/rules/common/cx-agent-routing.md +1 -0
  392. package/rules/common/development-workflow.md +23 -41
  393. package/rules/common/doc-ownership.md +54 -0
  394. package/rules/common/efficiency.md +57 -0
  395. package/rules/common/framing.md +76 -0
  396. package/rules/common/git-workflow.md +2 -4
  397. package/rules/common/patterns.md +3 -7
  398. package/rules/common/performance.md +15 -31
  399. package/rules/common/release-gates.md +69 -0
  400. package/rules/common/research.md +107 -0
  401. package/rules/common/security.md +6 -6
  402. package/rules/common/skill-composition.md +67 -0
  403. package/rules/common/testing.md +15 -27
  404. package/rules/golang/hooks.md +0 -4
  405. package/rules/policy/bootstrap.yaml +11 -0
  406. package/rules/policy/drive.yaml +8 -0
  407. package/rules/policy/task.yaml +9 -0
  408. package/rules/policy/workflow.yaml +9 -0
  409. package/rules/python/hooks.md +0 -4
  410. package/rules/swift/hooks.md +0 -4
  411. package/rules/typescript/hooks.md +0 -4
  412. package/rules/web/hooks.md +0 -2
  413. package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
  414. package/skills/ai/prompt-optimizer.md +7 -7
  415. package/skills/compliance/ai-disclosure.md +58 -0
  416. package/skills/compliance/data-privacy.md +46 -0
  417. package/skills/compliance/license-audit.md +40 -0
  418. package/skills/compliance/regulatory-review.md +61 -0
  419. package/skills/docs/document-ingest-workflow.md +52 -0
  420. package/skills/docs/evidence-ingest-workflow.md +9 -0
  421. package/skills/docs/init-docs.md +51 -18
  422. package/skills/docs/product-intelligence-workflow.md +12 -0
  423. package/skills/docs/product-signal-workflow.md +4 -0
  424. package/skills/docs/research-workflow.md +23 -8
  425. package/skills/docs/runbook-workflow.md +1 -1
  426. package/skills/operating/orchestration-reference.md +151 -0
  427. package/skills/roles/architect.md +5 -0
  428. package/skills/roles/engineer.md +32 -0
  429. package/skills/roles/operator.md +12 -0
  430. package/skills/roles/reviewer.md +23 -0
  431. package/skills/routing.md +1 -1
  432. package/templates/devcontainer/Dockerfile.devcontainer +38 -0
  433. package/templates/devcontainer/devcontainer.json +31 -0
  434. package/templates/distribution/bootstrap.ps1 +142 -0
  435. package/templates/distribution/bootstrap.sh +196 -0
  436. package/templates/distribution/run.mjs +187 -0
  437. package/templates/docs/adr.md +44 -8
  438. package/templates/docs/changelog-entry.md +43 -0
  439. package/templates/docs/construct_guide.md +149 -0
  440. package/templates/docs/evidence-brief.md +2 -2
  441. package/templates/docs/meta-prd.md +140 -19
  442. package/templates/docs/onboarding.md +57 -0
  443. package/templates/docs/prd.md +159 -15
  444. package/templates/docs/research-brief.md +4 -4
  445. package/templates/homebrew/construct.rb +67 -0
  446. package/langfuse/docker-compose.yml +0 -82
  447. package/lib/eval-harness.mjs +0 -59
  448. package/lib/hooks/bootstrap-guard.mjs +0 -90
  449. package/lib/hooks/console-warn.mjs +0 -43
  450. package/lib/hooks/continuation-enforcer.mjs +0 -72
  451. package/lib/hooks/drive-guard.mjs +0 -89
  452. package/lib/hooks/mcp-task-scope.mjs +0 -47
  453. package/lib/hooks/task-completed-guard.mjs +0 -43
  454. package/lib/hooks/teammate-idle-guard.mjs +0 -54
  455. package/lib/hooks/workflow-guard.mjs +0 -62
  456. package/lib/prompt-composer.mjs +0 -196
  457. package/lib/review.mjs +0 -429
  458. package/lib/server/static/app.js +0 -841
  459. package/lib/telemetry/langfuse-model-sync.mjs +0 -270
  460. package/rules/common/hooks.md +0 -35
  461. package/rules/zh/README.md +0 -113
  462. package/rules/zh/agents.md +0 -55
  463. package/rules/zh/code-review.md +0 -129
  464. package/rules/zh/coding-style.md +0 -53
  465. package/rules/zh/development-workflow.md +0 -49
  466. package/rules/zh/git-workflow.md +0 -29
  467. package/rules/zh/hooks.md +0 -35
  468. package/rules/zh/patterns.md +0 -36
  469. package/rules/zh/performance.md +0 -60
  470. package/rules/zh/security.md +0 -34
  471. package/rules/zh/testing.md +0 -34
@@ -0,0 +1,159 @@
1
+ /**
2
+ * lib/handoffs/cleanup.mjs — automatic handoff hygiene.
3
+ *
4
+ * Lifecycle:
5
+ * 1. Open handoffs (referenced beads not all closed) stay where they are.
6
+ * 2. Resolved handoffs (referenced beads all closed) older than
7
+ * `handoffsMaxDays` get MOVED to `.cx/handoffs/archive/`.
8
+ * 3. Archived handoffs older than `2 * handoffsMaxDays` get DELETED.
9
+ * 4. Any open handoff past `handoffsMaxItems` (FIFO by mtime) is
10
+ * surfaced as a warning but never deleted — open work is sacred.
11
+ *
12
+ * The function is idempotent and runs from:
13
+ * - `construct down` (best effort, time-boxed)
14
+ * - doctor daemon tick (lib/doctor/watchers/handoffs.mjs)
15
+ * - `construct handoffs prune` (manual)
16
+ *
17
+ * Planning never recurses into archive/. The archive deletion threshold
18
+ * compares against archive-dir mtimes, reset on move.
19
+ */
20
+
21
+ import fs from 'node:fs';
22
+ import path from 'node:path';
23
+ import { spawnSync } from 'node:child_process';
24
+ import { loadProjectConfig } from '../config/project-config.mjs';
25
+ import { parseHandoffFile } from './contract.mjs';
26
+
27
+ const HANDOFFS_DIR = '.cx/handoffs';
28
+ const ARCHIVE_DIR = '.cx/handoffs/archive';
29
+
30
+ function bdRootCwd(projectRoot) {
31
+ try {
32
+ const r = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd: projectRoot, encoding: 'utf8' });
33
+ if (r.status !== 0) return projectRoot;
34
+ const trimmed = r.stdout.trim();
35
+ const resolved = trimmed.startsWith('/') ? trimmed : path.join(projectRoot, trimmed);
36
+ const candidate = path.dirname(resolved);
37
+ if (fs.existsSync(path.join(candidate, '.beads'))) return candidate;
38
+ } catch { /* fall through */ }
39
+ return projectRoot;
40
+ }
41
+
42
+ function loadClosedBeadIds(projectRoot) {
43
+ try {
44
+ const r = spawnSync('bd', ['list', '--status', 'closed', '--json'], {
45
+ cwd: bdRootCwd(projectRoot),
46
+ encoding: 'utf8',
47
+ env: { ...process.env, NO_COLOR: '1' },
48
+ timeout: 5000,
49
+ });
50
+ if (r.status !== 0) return new Set();
51
+ const issues = JSON.parse(r.stdout || '[]');
52
+ return new Set(issues.map((it) => String(it.id).toLowerCase()));
53
+ } catch { return new Set(); }
54
+ }
55
+
56
+ function listHandoffFiles(dir) {
57
+ if (!fs.existsSync(dir)) return [];
58
+ return fs.readdirSync(dir)
59
+ .filter((f) => f.endsWith('.md'))
60
+ .map((f) => {
61
+ const full = path.join(dir, f);
62
+ const stat = fs.statSync(full);
63
+ return { filename: f, path: full, mtimeMs: stat.mtimeMs, bytes: stat.size };
64
+ });
65
+ }
66
+
67
+ function getRefBeads(filePath) {
68
+ try {
69
+ const parsed = parseHandoffFile(filePath);
70
+ if (Array.isArray(parsed.frontmatter?.beads) && parsed.frontmatter.beads.length > 0) {
71
+ return parsed.frontmatter.beads.map((s) => String(s).toLowerCase());
72
+ }
73
+ // Legacy fallback: scrape `construct-xxx` references from the body.
74
+ const text = parsed.body || '';
75
+ const matches = text.match(/\bconstruct-[a-z0-9]+\b/gi) || [];
76
+ return Array.from(new Set(matches.map((s) => s.toLowerCase())));
77
+ } catch {
78
+ return [];
79
+ }
80
+ }
81
+
82
+ export function planHandoffCleanup(projectRoot = process.cwd(), env = process.env, { now = Date.now() } = {}) {
83
+ const { config } = loadProjectConfig(projectRoot, env);
84
+ const disk = config?.resources?.disk || {};
85
+ const maxDays = disk.handoffsMaxDays ?? 30;
86
+ const maxItems = disk.handoffsMaxItems ?? 50;
87
+
88
+ const liveDir = path.join(projectRoot, HANDOFFS_DIR);
89
+ const archiveDir = path.join(projectRoot, ARCHIVE_DIR);
90
+ const liveFiles = listHandoffFiles(liveDir);
91
+ const archiveFiles = listHandoffFiles(archiveDir);
92
+
93
+ const archiveCutoff = now - maxDays * 24 * 60 * 60 * 1000;
94
+ const deleteCutoff = now - 2 * maxDays * 24 * 60 * 60 * 1000;
95
+
96
+ const closed = loadClosedBeadIds(projectRoot);
97
+ const actions = [];
98
+ const warnings = [];
99
+
100
+ for (const f of liveFiles) {
101
+ const beads = getRefBeads(f.path);
102
+ const allClosed = beads.length > 0 && beads.every((id) => closed.has(id));
103
+ if (allClosed && f.mtimeMs < archiveCutoff) {
104
+ actions.push({
105
+ kind: 'archive',
106
+ from: f.path,
107
+ to: path.join(archiveDir, f.filename),
108
+ reason: `referenced beads (${beads.join(', ')}) are closed and file is older than ${maxDays}d`,
109
+ bytes: f.bytes,
110
+ });
111
+ }
112
+ }
113
+
114
+ for (const f of archiveFiles) {
115
+ if (f.mtimeMs < deleteCutoff) {
116
+ actions.push({
117
+ kind: 'delete',
118
+ path: f.path,
119
+ reason: `archived handoff older than ${2 * maxDays}d`,
120
+ bytes: f.bytes,
121
+ });
122
+ }
123
+ }
124
+
125
+ // Soft warning when too many live handoffs accumulate. Never delete
126
+ // live handoffs automatically — they may reference open work.
127
+ if (liveFiles.length > maxItems) {
128
+ warnings.push(`${liveFiles.length} live handoffs exceeds maxItems=${maxItems}. Close or archive resolved beads to bring it down.`);
129
+ }
130
+
131
+ return { actions, warnings, maxDays, maxItems, archiveCutoff, deleteCutoff };
132
+ }
133
+
134
+ export function executeHandoffCleanup(plan) {
135
+ const moved = [];
136
+ const deleted = [];
137
+ for (const a of plan.actions) {
138
+ try {
139
+ if (a.kind === 'archive') {
140
+ fs.mkdirSync(path.dirname(a.to), { recursive: true });
141
+ fs.renameSync(a.from, a.to);
142
+ moved.push(a);
143
+ } else if (a.kind === 'delete') {
144
+ fs.unlinkSync(a.path);
145
+ deleted.push(a);
146
+ }
147
+ } catch (err) {
148
+ a.error = err.message;
149
+ }
150
+ }
151
+ return { moved, deleted, warnings: plan.warnings };
152
+ }
153
+
154
+ export function autoCleanHandoffs(projectRoot = process.cwd(), env = process.env) {
155
+ const plan = planHandoffCleanup(projectRoot, env);
156
+ if (plan.actions.length === 0) return { plan, result: { moved: [], deleted: [], warnings: plan.warnings } };
157
+ const result = executeHandoffCleanup(plan);
158
+ return { plan, result };
159
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * lib/handoffs/contract.mjs — handoff schema + parse/validate/format.
3
+ *
4
+ * A handoff is a markdown file in `.cx/handoffs/` written when a session
5
+ * bridges work to the next agent. Until now the shape was implicit, so
6
+ * handoffs varied in quality and there was no automated cleanup beyond
7
+ * raw mtime/count caps. This module defines a contract:
8
+ *
9
+ * ---
10
+ * schema: cx-handoff/v1
11
+ * id: 2026-05-18-something-something # filename stem
12
+ * created: 2026-05-18T14:00:00.000Z
13
+ * from_session: <session-uuid-or-alias>
14
+ * beads: [construct-xyz, construct-abc] # work this handoff is about
15
+ * status: open # open | resolved | archived
16
+ * title: One-line summary
17
+ * tags: [optional, free-form]
18
+ * ---
19
+ *
20
+ * ## What was done
21
+ * …
22
+ * ## What's left
23
+ * …
24
+ * ## Open questions
25
+ * …
26
+ * ## How to resume
27
+ * …
28
+ *
29
+ * Status semantics:
30
+ * - open — bead(s) still in progress or open; handoff is live context
31
+ * - resolved — every referenced bead is closed; handoff is informational
32
+ * - archived — moved to .cx/handoffs/archive/ during cleanup
33
+ *
34
+ * Cleanup uses these fields:
35
+ * - resolved handoffs older than retention → archived
36
+ * - archived handoffs older than 2× retention → deleted
37
+ * - any handoff past `handoffsMaxItems` (FIFO by mtime) → deleted
38
+ *
39
+ * `parseHandoff` is intentionally permissive — pre-contract handoffs with
40
+ * no frontmatter still parse with status='legacy' so they aren't lost.
41
+ * `validateHandoff` is the strict version, used when writing.
42
+ */
43
+
44
+ import { readFileSync } from 'node:fs';
45
+
46
+ export const HANDOFF_SCHEMA_VERSION = 'cx-handoff/v1';
47
+ export const HANDOFF_STATUSES = Object.freeze(['open', 'resolved', 'archived']);
48
+ export const REQUIRED_SECTIONS = Object.freeze(['What was done', "What's left"]);
49
+
50
+ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
51
+
52
+ function parseFrontmatterBlock(text) {
53
+ const out = {};
54
+ for (const line of text.split('\n')) {
55
+ if (!line.trim() || line.trim().startsWith('#')) continue;
56
+ const idx = line.indexOf(':');
57
+ if (idx === -1) continue;
58
+ const key = line.slice(0, idx).trim();
59
+ const rawValue = line.slice(idx + 1).trim();
60
+ if (!key) continue;
61
+ if (rawValue.startsWith('[') && rawValue.endsWith(']')) {
62
+ out[key] = rawValue.slice(1, -1).split(',').map((s) => s.trim()).filter(Boolean);
63
+ } else if (rawValue === 'true' || rawValue === 'false') {
64
+ out[key] = rawValue === 'true';
65
+ } else if (/^-?\d+(\.\d+)?$/.test(rawValue)) {
66
+ out[key] = Number(rawValue);
67
+ } else {
68
+ const unquoted = rawValue.replace(/^["']|["']$/g, '');
69
+ out[key] = unquoted;
70
+ }
71
+ }
72
+ return out;
73
+ }
74
+
75
+ export function parseHandoff(text) {
76
+ if (typeof text !== 'string') {
77
+ return { frontmatter: {}, body: '', sections: {}, status: 'invalid', error: 'text required' };
78
+ }
79
+ const fmMatch = text.match(FRONTMATTER_RE);
80
+ let frontmatter = {};
81
+ let body = text;
82
+ if (fmMatch) {
83
+ frontmatter = parseFrontmatterBlock(fmMatch[1]);
84
+ body = text.slice(fmMatch[0].length);
85
+ }
86
+ const sections = {};
87
+ let currentHeading = null;
88
+ let currentLines = [];
89
+ for (const line of body.split('\n')) {
90
+ const heading = line.match(/^##\s+(.+?)\s*$/);
91
+ if (heading) {
92
+ if (currentHeading) sections[currentHeading] = currentLines.join('\n').trim();
93
+ currentHeading = heading[1];
94
+ currentLines = [];
95
+ } else if (currentHeading) {
96
+ currentLines.push(line);
97
+ }
98
+ }
99
+ if (currentHeading) sections[currentHeading] = currentLines.join('\n').trim();
100
+
101
+ const status = frontmatter?.schema === HANDOFF_SCHEMA_VERSION
102
+ ? (HANDOFF_STATUSES.includes(frontmatter.status) ? frontmatter.status : 'open')
103
+ : 'legacy';
104
+ return { frontmatter, body, sections, status };
105
+ }
106
+
107
+ export function parseHandoffFile(filePath) {
108
+ const text = readFileSync(filePath, 'utf8');
109
+ return parseHandoff(text);
110
+ }
111
+
112
+ export function validateHandoff(parsed) {
113
+ const errors = [];
114
+ const fm = parsed?.frontmatter || {};
115
+ if (fm.schema !== HANDOFF_SCHEMA_VERSION) errors.push(`schema must be '${HANDOFF_SCHEMA_VERSION}'`);
116
+ if (typeof fm.id !== 'string' || !fm.id) errors.push('id required (typically the filename stem)');
117
+ if (typeof fm.created !== 'string' || !/\d{4}-\d{2}-\d{2}/.test(fm.created)) errors.push("created required (ISO-8601 string starting with YYYY-MM-DD)");
118
+ if (typeof fm.title !== 'string' || !fm.title.trim()) errors.push('title required');
119
+ if (fm.status && !HANDOFF_STATUSES.includes(fm.status)) errors.push(`status must be one of ${HANDOFF_STATUSES.join(', ')}`);
120
+ if (fm.beads !== undefined && !Array.isArray(fm.beads)) errors.push('beads must be an array of bd issue ids');
121
+ for (const section of REQUIRED_SECTIONS) {
122
+ if (!parsed?.sections?.[section] || !parsed.sections[section].trim()) {
123
+ errors.push(`required section missing or empty: "${section}"`);
124
+ }
125
+ }
126
+ return { valid: errors.length === 0, errors };
127
+ }
128
+
129
+ export function formatHandoff({ id, title, beads = [], status = 'open', from_session = null, tags = [], whatWasDone = '', whatsLeft = '', openQuestions = '', howToResume = '' } = {}) {
130
+ if (!id) throw new Error('formatHandoff: id required');
131
+ if (!title) throw new Error('formatHandoff: title required');
132
+ const lines = [
133
+ '---',
134
+ `schema: ${HANDOFF_SCHEMA_VERSION}`,
135
+ `id: ${id}`,
136
+ `created: ${new Date().toISOString()}`,
137
+ from_session ? `from_session: ${from_session}` : null,
138
+ `beads: [${beads.join(', ')}]`,
139
+ `status: ${status}`,
140
+ `title: ${JSON.stringify(title)}`,
141
+ tags.length ? `tags: [${tags.join(', ')}]` : null,
142
+ '---',
143
+ '',
144
+ '## What was done',
145
+ '',
146
+ whatWasDone.trim() || '_TBD_',
147
+ '',
148
+ "## What's left",
149
+ '',
150
+ whatsLeft.trim() || '_TBD_',
151
+ '',
152
+ '## Open questions',
153
+ '',
154
+ openQuestions.trim() || '_(none)_',
155
+ '',
156
+ '## How to resume',
157
+ '',
158
+ howToResume.trim() || '_TBD_',
159
+ '',
160
+ ].filter((line) => line !== null);
161
+ return lines.join('\n');
162
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * lib/handoffs/inventory.mjs — handoff hygiene primitives.
3
+ *
4
+ * Handoffs (markdown files in `.cx/handoffs/`) are written by
5
+ * `lib/beads-automation.mjs::create-handoff` whenever a session bridges
6
+ * to the next agent. The inventory helper surfaces:
7
+ *
8
+ * - total file count + total bytes
9
+ * - oldest mtime (age, in days)
10
+ * - count of files past `handoffsMaxDays` (eligible to prune)
11
+ * - count of files referencing a *closed* bead (stale-but-resolved)
12
+ * - per-file id, status, and path (from contract parser) for CLI display
13
+ *
14
+ * The dashboard insights card consumes this; doctor surfaces a warning
15
+ * when stale handoffs accumulate past the threshold.
16
+ */
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { spawnSync } from 'node:child_process';
20
+ import { loadProjectConfig } from '../config/project-config.mjs';
21
+ import { parseHandoff } from './contract.mjs';
22
+
23
+ const HANDOFFS_REL = '.cx/handoffs';
24
+ const BEAD_REF_RE = /\bconstruct-[a-z0-9]+\b/gi;
25
+
26
+ function readFiles(dir) {
27
+ if (!fs.existsSync(dir)) return [];
28
+ return fs.readdirSync(dir)
29
+ .filter((f) => f.endsWith('.md'))
30
+ .map((f) => {
31
+ const full = path.join(dir, f);
32
+ const stat = fs.statSync(full);
33
+ const content = fs.readFileSync(full, 'utf8');
34
+ const beadRefs = Array.from(new Set((content.match(BEAD_REF_RE) || []).map((s) => s.toLowerCase())));
35
+ const parsed = parseHandoff(content);
36
+ const id = parsed.frontmatter?.id || f.replace(/\.md$/, '');
37
+ return { filename: f, id, path: full, mtimeMs: stat.mtimeMs, bytes: stat.size, beadRefs, status: parsed.status };
38
+ });
39
+ }
40
+
41
+ function bdRootCwd(projectRoot) {
42
+ try {
43
+ const r = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd: projectRoot, encoding: 'utf8' });
44
+ if (r.status !== 0) return projectRoot;
45
+ const trimmed = r.stdout.trim();
46
+ const resolved = trimmed.startsWith('/') ? trimmed : path.join(projectRoot, trimmed);
47
+ const candidate = path.dirname(resolved);
48
+ if (fs.existsSync(path.join(candidate, '.beads'))) return candidate;
49
+ } catch { /* fall through */ }
50
+ return projectRoot;
51
+ }
52
+
53
+ function closedBeadIds(projectRoot) {
54
+ try {
55
+ const r = spawnSync('bd', ['list', '--status', 'closed', '--json'], {
56
+ cwd: bdRootCwd(projectRoot),
57
+ encoding: 'utf8',
58
+ env: { ...process.env, NO_COLOR: '1' },
59
+ timeout: 5000,
60
+ });
61
+ if (r.status !== 0) return new Set();
62
+ const issues = JSON.parse(r.stdout);
63
+ return new Set(issues.map((it) => String(it.id).toLowerCase()));
64
+ } catch { return new Set(); }
65
+ }
66
+
67
+ export function summarizeHandoffs(projectRoot = process.cwd(), env = process.env, { now = Date.now() } = {}) {
68
+ const dir = path.join(projectRoot, HANDOFFS_REL);
69
+ const files = readFiles(dir);
70
+ if (files.length === 0) {
71
+ return {
72
+ state: 'empty',
73
+ total: 0,
74
+ bytes: 0,
75
+ oldestAgeDays: null,
76
+ pastRetentionCount: 0,
77
+ resolvedCount: 0,
78
+ };
79
+ }
80
+ const { config } = loadProjectConfig(projectRoot, env);
81
+ const maxDays = config?.resources?.disk?.handoffsMaxDays ?? 30;
82
+ const maxItems = config?.resources?.disk?.handoffsMaxItems ?? 50;
83
+ const cutoff = now - maxDays * 24 * 60 * 60 * 1000;
84
+ const sorted = [...files].sort((a, b) => a.mtimeMs - b.mtimeMs);
85
+ const oldestAgeMs = now - sorted[0].mtimeMs;
86
+ const closed = closedBeadIds(projectRoot);
87
+ let pastRetentionCount = 0;
88
+ let resolvedCount = 0;
89
+ let resolvedFiles = [];
90
+ for (const f of files) {
91
+ if (f.mtimeMs < cutoff) pastRetentionCount += 1;
92
+ if (f.beadRefs.length > 0 && f.beadRefs.every((id) => closed.has(id))) {
93
+ resolvedCount += 1;
94
+ resolvedFiles.push(f.filename);
95
+ }
96
+ }
97
+ const bytes = files.reduce((s, f) => s + f.bytes, 0);
98
+ const enriched = files.map((f) => ({
99
+ ...f,
100
+ ageDays: (now - f.mtimeMs) / (24 * 60 * 60 * 1000),
101
+ }));
102
+ return {
103
+ state: 'ok',
104
+ total: files.length,
105
+ bytes,
106
+ oldestAgeDays: oldestAgeMs / (24 * 60 * 60 * 1000),
107
+ maxDays,
108
+ maxItems,
109
+ pastRetentionCount,
110
+ resolvedCount,
111
+ resolvedFiles: resolvedFiles.slice(0, 10),
112
+ files: enriched,
113
+ recent: sorted.slice(-5).reverse().map((f) => ({
114
+ filename: f.filename,
115
+ ageDays: (now - f.mtimeMs) / (24 * 60 * 60 * 1000),
116
+ beadRefs: f.beadRefs,
117
+ bytes: f.bytes,
118
+ })),
119
+ };
120
+ }
package/lib/headhunt.mjs CHANGED
@@ -1,16 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * lib/headhunt.mjs — <one-line purpose>
3
+ * lib/headhunt.mjs — Domain overlay activation and promotion for Construct agents.
4
4
  *
5
- * <2–6 line summary.>
5
+ * Creates temporary domain overlays that attach specialist knowledge to named
6
+ * agents, tracks overlay expiry, and manages promotion requests when a transient
7
+ * overlay should become a permanent skill. Backs the `construct headhunt` CLI
8
+ * and the MCP headhunt tool.
6
9
  */
7
10
 
8
11
  import fs from 'node:fs';
9
12
  import os from 'node:os';
10
13
  import path from 'node:path';
11
14
  import { createInterface } from 'node:readline';
12
-
13
- import { loadWorkflow, updateTask } from './workflow-state.mjs';
15
+ import { selectOption } from './tty-prompts.mjs';
14
16
 
15
17
  const OVERLAY_DIRNAME = 'domain-overlays';
16
18
  const PROMOTION_DIRNAME = 'promotion-requests';
@@ -181,45 +183,12 @@ function unique(values = []) {
181
183
  return [...new Set((values || []).filter(Boolean))];
182
184
  }
183
185
 
184
- function findWorkflowTaskForOverlay(workflow, overlayId = null) {
185
- const tasks = workflow?.tasks || [];
186
- if (overlayId) {
187
- const linkedTask = tasks.find((task) => Array.isArray(task.overlays) && task.overlays.includes(overlayId));
188
- if (linkedTask) return linkedTask;
189
- }
190
- if (workflow?.currentTaskKey) {
191
- const currentTask = tasks.find((task) => task.key === workflow.currentTaskKey);
192
- if (currentTask) return currentTask;
193
- }
194
- return tasks.find((task) => task.status === 'in-progress' || task.status === 'blocked_needs_user') || null;
195
- }
196
-
197
186
  function attachOverlayToWorkflow(root, overlay, { challengeRequired = false, challengeStatus = null } = {}) {
198
- const workflow = loadWorkflow(root);
199
- const task = findWorkflowTaskForOverlay(workflow);
200
- if (!task) return null;
201
-
202
- updateTask(root, task.key, {
203
- overlays: unique([...(task.overlays || []), overlay.id]),
204
- challengeRequired: challengeRequired || task.challengeRequired,
205
- challengeStatus: challengeStatus ?? task.challengeStatus,
206
- note: `Attached domain overlay ${overlay.id} (${overlay.domain}).`,
207
- });
208
- return task.key;
187
+ return null;
209
188
  }
210
189
 
211
190
  function syncPromotionToWorkflow(root, request) {
212
- const workflow = loadWorkflow(root);
213
- const task = findWorkflowTaskForOverlay(workflow, request.id);
214
- if (!task) return null;
215
-
216
- updateTask(root, task.key, {
217
- overlays: unique([...(task.overlays || []), request.id]),
218
- challengeRequired: true,
219
- challengeStatus: request.challenge?.status || task.challengeStatus || 'pending',
220
- note: `Updated promotion request ${request.id} challenge status to ${request.challenge?.status || 'pending'}.`,
221
- });
222
- return task.key;
191
+ return null;
223
192
  }
224
193
 
225
194
  function isExpired(record) {
@@ -384,7 +353,6 @@ export async function runHeadhunt({ args = [], cwd = process.cwd(), homeDir = os
384
353
  const overlayId = `${slugify(templateName)}-team-${now.toISOString().replace(/[:.]/g, '-')}`;
385
354
  const overlayDir = getOverlayDir(root);
386
355
  const expiresAt = permanence === 'temporary' ? new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString() : null;
387
- const workflow = loadWorkflow(root);
388
356
 
389
357
  const overlay = {
390
358
  id: overlayId,
@@ -401,8 +369,8 @@ export async function runHeadhunt({ args = [], cwd = process.cwd(), homeDir = os
401
369
  createdAt: now.toISOString(),
402
370
  expiresAt,
403
371
  project: path.basename(root),
404
- workflowId: workflow?.id || null,
405
- taskKey: workflow?.currentTaskKey || null,
372
+ workflowId: null,
373
+ taskKey: null,
406
374
  status: 'active',
407
375
  prompt: buildOverlayPrompt({ domain: templateName, objective, scope: typeof flags.scope === 'string' ? flags.scope : '', permanence, focus: tpl.focus, freshness }),
408
376
  skills: tpl.skills ?? [],
@@ -450,8 +418,22 @@ export async function runHeadhunt({ args = [], cwd = process.cwd(), homeDir = os
450
418
  try {
451
419
  let permanence = flags.save ? 'permanent_request' : 'temporary';
452
420
  if (!flags.temp && !flags.save && interactive) {
453
- const answer = (await promptQuestion(rl, 'Use this expertise temporarily for the current task, or save it as a promotion request? [temp/save]: ')).trim().toLowerCase();
454
- permanence = answer === 'save' ? 'permanent_request' : 'temporary';
421
+ permanence = await selectOption({
422
+ title: 'Choose overlay mode',
423
+ instructions: 'Pick whether this expertise should be temporary or saved as a promotion request.',
424
+ options: [
425
+ {
426
+ value: 'temporary',
427
+ label: 'Temporary overlay',
428
+ description: 'Use this expertise for the current task and let it expire automatically.',
429
+ },
430
+ {
431
+ value: 'permanent_request',
432
+ label: 'Promotion request',
433
+ description: 'Save a request to promote this expertise into a more permanent capability.',
434
+ },
435
+ ],
436
+ });
455
437
  }
456
438
 
457
439
  let objective = typeof flags.for === 'string' ? flags.for.trim() : rest.slice(1).join(' ').trim();
@@ -469,7 +451,6 @@ export async function runHeadhunt({ args = [], cwd = process.cwd(), homeDir = os
469
451
  const classification = classifyOverlay(objective, typeof flags.team === 'string' ? flags.team : '');
470
452
  const now = new Date();
471
453
  const root = findConstructRoot(cwd);
472
- const workflow = loadWorkflow(root);
473
454
  const overlayId = `${slugify(domain)}-${now.toISOString().replace(/[:.]/g, '-')}`;
474
455
  const overlayDir = getOverlayDir(root);
475
456
  const promotionDir = getPromotionDir(root);
@@ -488,8 +469,8 @@ export async function runHeadhunt({ args = [], cwd = process.cwd(), homeDir = os
488
469
  createdAt: now.toISOString(),
489
470
  expiresAt,
490
471
  project: path.basename(root),
491
- workflowId: workflow?.id || null,
492
- taskKey: workflow?.currentTaskKey || null,
472
+ workflowId: null,
473
+ taskKey: null,
493
474
  status: permanence === 'temporary' ? 'active' : 'promotion_requested',
494
475
  teamId: overlayId,
495
476
  sourceStrategy: [