@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,740 @@
1
+ /**
2
+ * lib/integrations/intake-integrations.mjs — External system integrations
3
+ * for intake packets and artifacts.
4
+ *
5
+ * Creates GitHub Issues, Jira tickets, and Confluence pages from intake
6
+ * packets and Construct artifacts. Each integration is optional and
7
+ * configured via env vars or embed.yaml.
8
+ *
9
+ * Integrations:
10
+ * - GitHub Issues: requires GITHUB_TOKEN + GITHUB_REPO (owner/repo)
11
+ * - Jira: requires JIRA_HOST + JIRA_USER + JIRA_API_TOKEN + JIRA_PROJECT
12
+ * - Confluence: requires CONFLUENCE_HOST + CONFLUENCE_USER + CONFLUENCE_API_TOKEN + CONFLUENCE_SPACE
13
+ *
14
+ * All operations return { ok, externalUrl, externalId, error? }.
15
+ */
16
+
17
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import { homedir } from 'node:os';
20
+ import { spawnSync } from 'node:child_process';
21
+
22
+ // ── Multi-method auth helpers ────────────────────────────────────────────
23
+
24
+ /**
25
+ * Resolve GitHub credentials using multiple methods (in priority order):
26
+ * 1. `gh` CLI (already authenticated via gh auth)
27
+ * 2. GITHUB_TOKEN env var / .env / config.env / shell rc
28
+ * 3. 1Password op:// refs in shell rc files
29
+ *
30
+ * Returns { token, repo, method } or { token: null }.
31
+ */
32
+ function resolveGitHubAuth(homeDir) {
33
+ // Method 1: gh CLI
34
+ try {
35
+ const status = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', timeout: 5000 });
36
+ if (status.status === 0) {
37
+ // gh is authenticated — resolve the repo from env or gh itself
38
+ let repo = process.env.GITHUB_REPO || resolveCredentialSync('GITHUB_REPO', homeDir);
39
+ if (!repo) {
40
+ // Try to get the default remote from gh
41
+ const remote = spawnSync('gh', ['repo', 'view', '--json', 'nameWithOwner', '--jq', '.nameWithOwner'], { encoding: 'utf8', timeout: 5000 });
42
+ if (remote.status === 0 && remote.stdout?.trim()) {
43
+ repo = remote.stdout.trim();
44
+ }
45
+ }
46
+ return { token: 'gh-cli', repo, method: 'gh' };
47
+ }
48
+ } catch { /* gh not available */ }
49
+
50
+ // Method 2: Env vars (includes .env, config.env, shell rc via resolveCredentialSync)
51
+ const envToken = resolveCredentialSync('GITHUB_TOKEN', homeDir) || resolveCredentialSync('GH_TOKEN', homeDir);
52
+ const envRepo = process.env.GITHUB_REPO || resolveCredentialSync('GITHUB_REPO', homeDir);
53
+ if (envToken && envRepo) {
54
+ return { token: envToken, repo: envRepo, method: 'env' };
55
+ }
56
+
57
+ // Token found but no repo
58
+ if (envToken) {
59
+ return { token: envToken, repo: null, method: 'env' };
60
+ }
61
+
62
+ return { token: null, repo: null, method: null };
63
+ }
64
+
65
+ /**
66
+ * Resolve Jira credentials (env → .env → config.env → shell rc → 1Password).
67
+ */
68
+ function resolveJiraAuth(homeDir) {
69
+ return {
70
+ host: resolveCredentialSync('JIRA_HOST', homeDir) || process.env.JIRA_HOST,
71
+ email: resolveCredentialSync('JIRA_USER', homeDir) || process.env.JIRA_USER,
72
+ token: resolveCredentialSync('JIRA_API_TOKEN', homeDir),
73
+ project: resolveCredentialSync('JIRA_PROJECT', homeDir) || process.env.JIRA_PROJECT,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Resolve Confluence credentials (env → .env → config.env → shell rc → 1Password).
79
+ */
80
+ function resolveConfluenceAuth(homeDir) {
81
+ return {
82
+ host: resolveCredentialSync('CONFLUENCE_HOST', homeDir) || process.env.CONFLUENCE_HOST,
83
+ email: resolveCredentialSync('CONFLUENCE_USER', homeDir) || process.env.CONFLUENCE_USER,
84
+ token: resolveCredentialSync('CONFLUENCE_API_TOKEN', homeDir),
85
+ space: resolveCredentialSync('CONFLUENCE_SPACE', homeDir) || process.env.CONFLUENCE_SPACE,
86
+ };
87
+ }
88
+
89
+ // ── GitHub Issues ────────────────────────────────────────────────────────
90
+
91
+ /**
92
+ * Create a GitHub issue from an intake packet.
93
+ *
94
+ * @param {object} packet - Intake packet with triage, excerpt
95
+ * @param {object} [opts]
96
+ * @param {string} [opts.repo] - owner/repo (default: GITHUB_REPO env)
97
+ * @param {string} [opts.token] - GitHub PAT (default: GITHUB_TOKEN env)
98
+ * @param {string} [opts.apiUrl] - GitHub API URL (default: api.github.com)
99
+ * @param {string} [opts.host] - GitHub host (default: github.com)
100
+ * @param {object} [opts.fetchImpl]
101
+ * @returns {Promise<{ ok: boolean, externalUrl: string|null, externalId: string|null, error?: string }>}
102
+ */
103
+ export async function createGitHubIssue(packet, { repo, token, apiUrl, host, fetchImpl = globalThis.fetch } = {}) {
104
+ const hd = homedir();
105
+ const auth = resolveGitHubAuth(hd);
106
+ const resolvedRepo = repo || auth.repo || process.env.GITHUB_REPO;
107
+ const resolvedToken = token || (auth.method === 'env' ? auth.token : null);
108
+ const useGhCli = auth.method === 'gh';
109
+ const resolvedApi = apiUrl || process.env.GITHUB_API_URL || 'https://api.github.com';
110
+
111
+ if (!resolvedRepo) return { ok: false, externalUrl: null, externalId: null, error: 'No repo found. Set GITHUB_REPO or run `gh repo view` to detect.' };
112
+ if (!resolvedToken && !useGhCli) return { ok: false, externalUrl: null, externalId: null, error: 'No GitHub auth found. Try: gh auth login, or set GITHUB_TOKEN.' };
113
+
114
+ const t = packet?.triage || {};
115
+ const title = buildGitHubTitle(packet);
116
+ const body = buildGitHubBody(packet);
117
+ const labels = buildLabels(t);
118
+
119
+ // Method 1: gh CLI (no token needed, labels skipped to avoid missing-label errors)
120
+ if (useGhCli) {
121
+ try {
122
+ const result = spawnSync('gh', [
123
+ 'issue', 'create',
124
+ '--repo', resolvedRepo,
125
+ '--title', title,
126
+ '--body', body,
127
+ ], { encoding: 'utf8', timeout: 15000 });
128
+
129
+ if (result.status === 0) {
130
+ const url = result.stdout?.trim();
131
+ const number = url?.split('/').pop();
132
+ return {
133
+ ok: true,
134
+ externalUrl: url,
135
+ externalId: number || url,
136
+ };
137
+ }
138
+ // gh failed — fall through to API method
139
+ const stderr = (result.stderr || '').slice(0, 200);
140
+ if (stderr) process.stderr.write(`[integrations] gh issue create failed: ${stderr}\n`);
141
+ } catch (err) {
142
+ process.stderr.write(`[integrations] gh CLI error: ${err.message}\n`);
143
+ }
144
+ }
145
+
146
+ // Method 2: REST API with token
147
+ if (resolvedToken) {
148
+ try {
149
+ const res = await fetchImpl(`${resolvedApi}/repos/${resolvedRepo}/issues`, {
150
+ method: 'POST',
151
+ headers: {
152
+ Authorization: `Bearer ${resolvedToken}`,
153
+ 'Content-Type': 'application/json',
154
+ Accept: 'application/vnd.github.v3+json',
155
+ },
156
+ body: JSON.stringify({ title, body, labels }),
157
+ });
158
+
159
+ if (!res.ok) {
160
+ const detail = await res.text().catch(() => '');
161
+ return { ok: false, externalUrl: null, externalId: null, error: `GitHub API ${res.status}: ${detail.slice(0, 200)}` };
162
+ }
163
+
164
+ const json = await res.json();
165
+ return { ok: true, externalUrl: json.html_url, externalId: String(json.number) };
166
+ } catch (err) {
167
+ return { ok: false, externalUrl: null, externalId: null, error: err.message };
168
+ }
169
+ }
170
+
171
+ return { ok: false, externalUrl: null, externalId: null, error: 'All auth methods exhausted' };
172
+ }
173
+
174
+ function buildGitHubTitle(packet) {
175
+ const t = packet?.triage || {};
176
+ const prefix = t.intakeType ? `[${t.intakeType}] ` : '';
177
+ const source = packet?.intake?.sourcePath?.split('/')?.pop() || 'intake signal';
178
+ const excerpt = packet?.excerpt?.slice(0, 80)?.replace(/\n/g, ' ')?.trim() || '';
179
+ return `${prefix}${excerpt || source}`.slice(0, 256);
180
+ }
181
+
182
+ function buildGitHubBody(packet) {
183
+ const lines = [];
184
+ lines.push('<!-- Auto-created from Construct intake -->');
185
+ lines.push('');
186
+ if (packet?.id) lines.push(`**Intake ID:** \`${packet.id}\``);
187
+ if (packet?.intake?.sourcePath) lines.push(`**Source:** \`${packet.intake.sourcePath}\``);
188
+ lines.push('');
189
+
190
+ const t = packet?.triage || {};
191
+ if (t.intakeType) lines.push(`**Type:** ${t.intakeType}`);
192
+ if (t.rdStage) lines.push(`**Stage:** ${t.rdStage}`);
193
+ if (t.primaryOwner) lines.push(`**Owner:** ${t.primaryOwner}`);
194
+ if (t.recommendedAction) lines.push(`**Recommended action:** ${t.recommendedAction}`);
195
+ if (t.risk) lines.push(`**Risk:** ${t.risk}`);
196
+ lines.push('');
197
+
198
+ if (packet?.excerpt) {
199
+ lines.push('## Signal content');
200
+ lines.push('');
201
+ lines.push(packet.excerpt.slice(0, 3000));
202
+ lines.push('');
203
+ }
204
+
205
+ if (packet?.related?.length) {
206
+ lines.push('## Related artifacts');
207
+ for (const r of packet.related) {
208
+ lines.push(`- ${r.path || r.title}${r.score ? ` (score: ${r.score.toFixed(2)})` : ''}`);
209
+ }
210
+ }
211
+
212
+ return lines.join('\n');
213
+ }
214
+
215
+ function buildLabels(t) {
216
+ const labels = ['construct-intake'];
217
+ if (t.intakeType) labels.push(t.intakeType);
218
+ if (t.risk) labels.push(`risk-${t.risk}`);
219
+ return labels;
220
+ }
221
+
222
+ // ── Jira ─────────────────────────────────────────────────────────────────
223
+
224
+ /**
225
+ * Create a Jira ticket from an intake packet.
226
+ *
227
+ * @param {object} packet - Intake packet with triage, excerpt
228
+ * @param {object} [opts]
229
+ * @param {string} [opts.host] - Jira host (default: JIRA_HOST env)
230
+ * @param {string} [opts.email] - Jira user email (default: JIRA_USER env)
231
+ * @param {string} [opts.token] - Jira API token (default: JIRA_API_TOKEN env)
232
+ * @param {string} [opts.project] - Jira project key (default: JIRA_PROJECT env)
233
+ * @param {string} [opts.issueType] - Issue type (default: Task)
234
+ * @param {object} [opts.fetchImpl]
235
+ * @returns {Promise<{ ok: boolean, externalUrl: string|null, externalId: string|null, error?: string }>}
236
+ */
237
+ export async function createJiraTicket(packet, { host, email, token, project, issueType = 'Task', fetchImpl = globalThis.fetch } = {}) {
238
+ const hd = homedir();
239
+ const auth = resolveJiraAuth(hd);
240
+ const resolvedHost = host || auth.host || process.env.JIRA_HOST;
241
+ const resolvedEmail = email || auth.email || process.env.JIRA_USER;
242
+ const resolvedToken = token || auth.token || process.env.JIRA_API_TOKEN;
243
+ const resolvedProject = project || auth.project || process.env.JIRA_PROJECT;
244
+
245
+ if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_HOST not set (e.g. https://your-domain.atlassian.net). Check ~/.construct/config.env or shell rc.' };
246
+ if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_USER and JIRA_API_TOKEN required. Check ~/.construct/config.env, ~/.env, or 1Password.' };
247
+ if (!resolvedProject) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_PROJECT not set (e.g. PROJ)' };
248
+
249
+ const t = packet?.triage || {};
250
+ const jiraAuthHeader = Buffer.from(`${resolvedEmail}:${resolvedToken}`).toString('base64');
251
+ const summary = buildJiraSummary(packet);
252
+ const description = buildJiraDescription(packet);
253
+
254
+ try {
255
+ const res = await fetchImpl(`${resolvedHost.replace(/\/$/, '')}/rest/api/2/issue`, {
256
+ method: 'POST',
257
+ headers: {
258
+ Authorization: `Basic ${jiraAuthHeader}`,
259
+ 'Content-Type': 'application/json',
260
+ },
261
+ body: JSON.stringify({
262
+ fields: {
263
+ project: { key: resolvedProject },
264
+ summary: summary.slice(0, 255),
265
+ description,
266
+ issuetype: { name: issueType },
267
+ labels: buildLabels(t),
268
+ },
269
+ }),
270
+ });
271
+
272
+ if (!res.ok) {
273
+ const detail = await res.text().catch(() => '');
274
+ return { ok: false, externalUrl: null, externalId: null, error: `Jira API ${res.status}: ${detail.slice(0, 200)}` };
275
+ }
276
+
277
+ const json = await res.json();
278
+
279
+ // We don't have the browse URL directly from the create response,
280
+ // so construct it from the host + issue key
281
+ const issueKey = json.key;
282
+ const browseUrl = `${resolvedHost.replace(/\/$/, '')}/browse/${issueKey}`;
283
+
284
+ return {
285
+ ok: true,
286
+ externalUrl: browseUrl,
287
+ externalId: issueKey,
288
+ };
289
+ } catch (err) {
290
+ return { ok: false, externalUrl: null, externalId: null, error: err.message };
291
+ }
292
+ }
293
+
294
+ function buildJiraSummary(packet) {
295
+ const t = packet?.triage || {};
296
+ const prefix = t.intakeType ? `[${t.intakeType}] ` : '';
297
+ const excerpt = packet?.excerpt?.slice(0, 80)?.replace(/\n/g, ' ')?.trim() || packet?.id || 'Intake signal';
298
+ return `${prefix}${excerpt}`.slice(0, 255);
299
+ }
300
+
301
+ function buildJiraDescription(packet) {
302
+ const lines = [];
303
+ lines.push('h3. Auto-created from Construct intake');
304
+ lines.push('');
305
+
306
+ if (packet?.id) lines.push(`* Intake ID: ${packet.id}`);
307
+ if (packet?.intake?.sourcePath) lines.push(`* Source: ${packet.intake.sourcePath}`);
308
+ lines.push('');
309
+
310
+ const t = packet?.triage || {};
311
+ if (t.intakeType) lines.push(`* Type: ${t.intakeType}`);
312
+ if (t.rdStage) lines.push(`* Stage: ${t.rdStage}`);
313
+ if (t.primaryOwner) lines.push(`* Owner: ${t.primaryOwner}`);
314
+ if (t.recommendedAction) lines.push(`* Recommended action: ${t.recommendedAction}`);
315
+ if (t.rationale) lines.push(`* Rationale: ${t.rationale}`);
316
+ lines.push('');
317
+
318
+ if (packet?.excerpt) {
319
+ lines.push('h3. Signal content');
320
+ lines.push('{noformat}');
321
+ lines.push(packet.excerpt.slice(0, 3000));
322
+ lines.push('{noformat}');
323
+ }
324
+
325
+ if (packet?.related?.length) {
326
+ lines.push('h3. Related artifacts');
327
+ for (const r of packet.related) {
328
+ lines.push(`* ${r.path || r.title}`);
329
+ }
330
+ }
331
+
332
+ return lines.join('\n');
333
+ }
334
+
335
+ // ── Confluence ───────────────────────────────────────────────────────────
336
+
337
+ /**
338
+ * Publish a Construct artifact (PRD/ADR/RFC) as a Confluence page.
339
+ *
340
+ * @param {object} artifact - Artifact metadata + content
341
+ * @param {string} artifact.type - 'prd' | 'adr' | 'rfc'
342
+ * @param {number} artifact.number - Artifact sequence number
343
+ * @param {string} artifact.title - Artifact title
344
+ * @param {string} artifact.content - Full markdown content
345
+ * @param {string} [artifact.path] - File path for source ref
346
+ * @param {object} [opts]
347
+ * @param {string} [opts.host] - Confluence host (default: CONFLUENCE_HOST env)
348
+ * @param {string} [opts.email] - Confluence user (default: CONFLUENCE_USER env)
349
+ * @param {string} [opts.token] - Confluence API token (default: CONFLUENCE_API_TOKEN env)
350
+ * @param {string} [opts.space] - Confluence space key (default: CONFLUENCE_SPACE env)
351
+ * @param {string} [opts.parentId] - Optional parent page ID
352
+ * @param {object} [opts.fetchImpl]
353
+ * @returns {Promise<{ ok: boolean, externalUrl: string|null, externalId: string|null, error?: string }>}
354
+ */
355
+ export async function publishArtifactToConfluence(artifact, { host, email, token, space, parentId, fetchImpl = globalThis.fetch } = {}) {
356
+ const hd = homedir();
357
+ const auth = resolveConfluenceAuth(hd);
358
+ const resolvedHost = host || auth.host || process.env.CONFLUENCE_HOST;
359
+ const resolvedEmail = email || auth.email || process.env.CONFLUENCE_USER;
360
+ const resolvedToken = token || auth.token || process.env.CONFLUENCE_API_TOKEN;
361
+ const resolvedSpace = space || auth.space || process.env.CONFLUENCE_SPACE;
362
+
363
+ if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_HOST not set (e.g. https://your-domain.atlassian.net/wiki). Check ~/.construct/config.env or shell rc.' };
364
+ if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_USER and CONFLUENCE_API_TOKEN required. Check ~/.construct/config.env, ~/.env, or 1Password.' };
365
+ if (!resolvedSpace) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_SPACE not set (e.g. PROD)' };
366
+
367
+ const prefix = { prd: 'PRD', adr: 'ADR', rfc: 'RFC' }[artifact.type] || artifact.type.toUpperCase();
368
+ const pageTitle = `${prefix}-${String(artifact.number).padStart(4, '0')}: ${artifact.title}`;
369
+
370
+ // Convert basic markdown to Confluence storage format
371
+ const storage = markdownToConfluenceStorage(artifact.content);
372
+
373
+ try {
374
+ const res = await fetchImpl(`${resolvedHost.replace(/\/$/, '')}/rest/api/content`, {
375
+ method: 'POST',
376
+ headers: {
377
+ Authorization: `Basic ${Buffer.from(`${resolvedEmail}:${resolvedToken}`).toString('base64')}`,
378
+ 'Content-Type': 'application/json',
379
+ },
380
+ body: JSON.stringify({
381
+ type: 'page',
382
+ title: pageTitle,
383
+ space: { key: resolvedSpace },
384
+ ...(parentId ? { ancestors: [{ id: parentId }] } : {}),
385
+ body: {
386
+ storage: {
387
+ value: storage,
388
+ representation: 'storage',
389
+ },
390
+ },
391
+ metadata: {
392
+ labels: [{ prefix: 'construct', name: `artifact-${artifact.type}` }],
393
+ },
394
+ }),
395
+ });
396
+
397
+ if (!res.ok) {
398
+ // Check if page already exists (409 = conflict)
399
+ if (res.status === 409) {
400
+ return await updateConfluencePage(artifact, { host: resolvedHost, email: resolvedEmail, token: resolvedToken, space: resolvedSpace, fetchImpl });
401
+ }
402
+ const detail = await res.text().catch(() => '');
403
+ return { ok: false, externalUrl: null, externalId: null, error: `Confluence API ${res.status}: ${detail.slice(0, 200)}` };
404
+ }
405
+
406
+ const json = await res.json();
407
+ return {
408
+ ok: true,
409
+ externalUrl: `${resolvedHost.replace(/\/$/, '')}/spaces/${resolvedSpace}/pages/${json.id}`,
410
+ externalId: json.id,
411
+ };
412
+ } catch (err) {
413
+ return { ok: false, externalUrl: null, externalId: null, error: err.message };
414
+ }
415
+ }
416
+
417
+ async function updateConfluencePage(artifact, { host, email, token, space, fetchImpl }) {
418
+ const prefix = { prd: 'PRD', adr: 'ADR', rfc: 'RFC' }[artifact.type] || artifact.type.toUpperCase();
419
+ const pageTitle = `${prefix}-${String(artifact.number).padStart(4, '0')}: ${artifact.title}`;
420
+ const auth = Buffer.from(`${email}:${token}`).toString('base64');
421
+ const baseUrl = host.replace(/\/$/, '');
422
+
423
+ try {
424
+ // Find existing page by title
425
+ const searchRes = await fetchImpl(`${baseUrl}/rest/api/content?title=${encodeURIComponent(pageTitle)}&spaceKey=${space}`, {
426
+ headers: { Authorization: `Basic ${auth}` },
427
+ });
428
+ if (!searchRes.ok) {
429
+ return { ok: false, externalUrl: null, externalId: null, error: `Confluence search failed: ${searchRes.status}` };
430
+ }
431
+ const searchJson = await searchRes.json();
432
+ const existing = searchJson.results?.[0];
433
+ if (!existing) {
434
+ return { ok: false, externalUrl: null, externalId: null, error: 'Page creation conflict but could not find existing page' };
435
+ }
436
+
437
+ // Update the existing page
438
+ const storage = markdownToConfluenceStorage(artifact.content);
439
+ const updateRes = await fetchImpl(`${baseUrl}/rest/api/content/${existing.id}`, {
440
+ method: 'PUT',
441
+ headers: {
442
+ Authorization: `Basic ${auth}`,
443
+ 'Content-Type': 'application/json',
444
+ },
445
+ body: JSON.stringify({
446
+ id: existing.id,
447
+ type: 'page',
448
+ title: pageTitle,
449
+ version: { number: existing.version.number + 1 },
450
+ body: {
451
+ storage: {
452
+ value: storage,
453
+ representation: 'storage',
454
+ },
455
+ },
456
+ }),
457
+ });
458
+
459
+ if (!updateRes.ok) {
460
+ const detail = await updateRes.text().catch(() => '');
461
+ return { ok: false, externalUrl: null, externalId: null, error: `Confluence update ${updateRes.status}: ${detail.slice(0, 200)}` };
462
+ }
463
+
464
+ const json = await updateRes.json();
465
+ return {
466
+ ok: true,
467
+ externalUrl: `${baseUrl}/spaces/${space}/pages/${json.id}`,
468
+ externalId: json.id,
469
+ updated: true,
470
+ };
471
+ } catch (err) {
472
+ return { ok: false, externalUrl: null, externalId: null, error: err.message };
473
+ }
474
+ }
475
+
476
+ /**
477
+ * Minimal markdown to Confluence storage format conversion.
478
+ * Handles the subset of markdown used by Construct artifacts.
479
+ */
480
+ function markdownToConfluenceStorage(md) {
481
+ if (!md) return '';
482
+
483
+ let html = md
484
+ // Headers
485
+ .replace(/^###### (.+)$/gm, '<h6>$1</h6>')
486
+ .replace(/^##### (.+)$/gm, '<h5>$1</h5>')
487
+ .replace(/^#### (.+)$/gm, '<h4>$1</h4>')
488
+ .replace(/^### (.+)$/gm, '<h3>$1</h3>')
489
+ .replace(/^## (.+)$/gm, '<h2>$1</h2>')
490
+ .replace(/^# (.+)$/gm, '<h1>$1</h1>')
491
+ // Bold and italic
492
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
493
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
494
+ // Inline code
495
+ .replace(/`(.+?)`/g, '<code>$1</code>')
496
+ // Horizontal rule
497
+ .replace(/^---$/gm, '<hr/>')
498
+ // Tables (simplified)
499
+ .replace(/^\|(.+)\|$/gm, (match) => {
500
+ const cells = match.split('|').filter(c => c.trim()).map(c => `<td>${c.trim()}</td>`).join('');
501
+ // Check if this is a header row (next row has ---)
502
+ return `<tr>${cells}</tr>`;
503
+ })
504
+ // Unordered lists
505
+ .replace(/^- (.+)$/gm, '<li>$1</li>')
506
+ // Paragraphs (double newlines)
507
+ .replace(/\n\n/g, '</p><p>')
508
+ // Line breaks within paragraphs
509
+ .replace(/\n/g, '<br/>');
510
+
511
+ // Wrap consecutive <li> in <ul>
512
+ html = html.replace(/(<li>.*?<\/li>)(\s*<li>.*?<\/li>)*/g, (match) => {
513
+ return `<ul>${match}</ul>`;
514
+ });
515
+
516
+ // Wrap tables
517
+ const tableRows = html.match(/(<tr>.*?<\/tr>\s*)+/g);
518
+ if (tableRows) {
519
+ for (const rows of tableRows) {
520
+ const headerClass = rows.includes('**') ? ' class="confluenceTable"' : '';
521
+ html = html.replace(rows, `<table${headerClass}>${rows}</table>`);
522
+ }
523
+ }
524
+
525
+ // Code blocks
526
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">$1</ac:parameter><ac:plain-text-body><![CDATA[$2]]></ac:plain-text-body></ac:structured-macro>');
527
+
528
+ // Front-matter (remove)
529
+ html = html.replace(/^---[\s\S]*?---\n*/, '');
530
+
531
+ return `<p>${html}</p>`;
532
+ }
533
+
534
+ // ── Update intake packet with external reference ─────────────────────────
535
+
536
+ /**
537
+ * Write an external reference into an intake packet's queue entry.
538
+ * Updates the JSON file for filesystem-backed queues.
539
+ *
540
+ * @param {string} rootDir - Project root
541
+ * @param {string} intakeId - Intake packet ID
542
+ * @param {string} system - 'github' | 'jira' | 'confluence'
543
+ * @param {string} externalUrl - URL to the external item
544
+ * @param {string} externalId - External system's ID
545
+ */
546
+ export function tagIntakeWithExternalRef(rootDir, intakeId, system, externalUrl, externalId) {
547
+ const pendingDir = join(rootDir, '.cx', 'intake', 'pending');
548
+ if (!existsSync(pendingDir)) return;
549
+
550
+ const files = readdirSync(pendingDir).filter(f => f.endsWith('.json'));
551
+ for (const file of files) {
552
+ if (!file.includes(intakeId)) continue;
553
+ try {
554
+ const filePath = join(pendingDir, file);
555
+ const entry = JSON.parse(readFileSync(filePath, 'utf8'));
556
+ entry.externalRefs = entry.externalRefs || {};
557
+ entry.externalRefs[system] = { url: externalUrl, id: externalId, createdAt: new Date().toISOString() };
558
+ writeFileSync(filePath, JSON.stringify(entry, null, 2) + '\n');
559
+ } catch { /* non-fatal */ }
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Synchronous credential resolver for use in constructors and sync contexts.
565
+ * Mirrors the async resolveCredential above. Checks env → .env → config.env → shell rc.
566
+ */
567
+ function resolveCredentialSync(varName, homeDir) {
568
+ if (process.env[varName]) return process.env[varName];
569
+ try {
570
+ const projectEnv = join(process.cwd(), '.env');
571
+ if (existsSync(projectEnv)) {
572
+ const content = readFileSync(projectEnv, 'utf8');
573
+ const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
574
+ if (m) return m[1].trim();
575
+ }
576
+ } catch { /* skip */ }
577
+ try {
578
+ const homeEnv = join(homeDir, '.env');
579
+ if (existsSync(homeEnv)) {
580
+ const content = readFileSync(homeEnv, 'utf8');
581
+ const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
582
+ if (m) return m[1].trim();
583
+ }
584
+ } catch { /* skip */ }
585
+ try {
586
+ const cfgPath = join(homeDir, '.construct', 'config.env');
587
+ if (existsSync(cfgPath)) {
588
+ const content = readFileSync(cfgPath, 'utf8');
589
+ const m = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
590
+ if (m) return m[1].trim();
591
+ }
592
+ } catch { /* skip */ }
593
+ try {
594
+ const shellFiles = [join(homeDir, '.zshrc'), join(homeDir, '.bashrc'), join(homeDir, '.bash_profile'), join(homeDir, '.profile')];
595
+ for (const rcPath of shellFiles) {
596
+ if (!existsSync(rcPath)) continue;
597
+ const content = readFileSync(rcPath, 'utf8');
598
+ const directRe = new RegExp(`export\\s+${varName}=["']?(.+?)["']?$`, 'm');
599
+ const directMatch = content.match(directRe);
600
+ if (directMatch) return directMatch[1].trim();
601
+ const opRe = new RegExp(`export\\s+${varName}=["']?\\$\\(op read '([^']+)'\\)["']?`, 'm');
602
+ const opMatch = content.match(opRe);
603
+ if (opMatch) {
604
+ try {
605
+ const result = spawnSync('op', ['read', opMatch[1]], { encoding: 'utf8', timeout: 5000 });
606
+ if (result.status === 0) return result.stdout.trim();
607
+ } catch { /* op read failed */ }
608
+ }
609
+ }
610
+ } catch { /* skip */ }
611
+ return null;
612
+ }
613
+
614
+ // ── Configuration detection ──────────────────────────────────────────────
615
+
616
+ /**
617
+ * Check which integrations are configured.
618
+ * Scopes environment, .env files, ~/.construct/config.env, and shell rc
619
+ * files (including 1Password op:// refs).
620
+ *
621
+ * @param {object} [opts]
622
+ * @param {string} [opts.homeDir]
623
+ * @returns {{ github: boolean, jira: boolean, confluence: boolean, githubToken: boolean, jiraToken: boolean, confluenceToken: boolean }}
624
+ */
625
+ export function detectIntegrationConfig({ homeDir } = {}) {
626
+ const hd = homeDir || homedir();
627
+
628
+ // GitHub — try gh CLI first, then env vars
629
+ const ghAuth = resolveGitHubAuth(hd);
630
+
631
+ // Jira
632
+ const jiraHost = resolveCredentialSync('JIRA_HOST', hd);
633
+ const jiraUser = resolveCredentialSync('JIRA_USER', hd);
634
+ const jiraToken = resolveCredentialSync('JIRA_API_TOKEN', hd);
635
+ const jiraProject = resolveCredentialSync('JIRA_PROJECT', hd);
636
+
637
+ // Confluence
638
+ const confHost = resolveCredentialSync('CONFLUENCE_HOST', hd);
639
+ const confUser = resolveCredentialSync('CONFLUENCE_USER', hd);
640
+ const confToken = resolveCredentialSync('CONFLUENCE_API_TOKEN', hd);
641
+ const confSpace = resolveCredentialSync('CONFLUENCE_SPACE', hd);
642
+
643
+ return {
644
+ github: ghAuth.method === 'gh' || Boolean(ghAuth.token && ghAuth.repo),
645
+ githubMethod: ghAuth.method || null,
646
+ githubRepo: ghAuth.repo || null,
647
+ jira: Boolean(jiraHost && jiraUser && jiraToken && jiraProject),
648
+ jiraHost: jiraHost || null,
649
+ confluence: Boolean(confHost && confUser && confToken && confSpace),
650
+ confluenceSpace: confSpace || null,
651
+ };
652
+ }
653
+
654
+ /**
655
+ * Resolve a credential from env, .env files, config.env, or shell rc files.
656
+ * Mirrors the approach in lib/health-check.mjs.
657
+ */
658
+ function resolveCredential(varName, homeDir) {
659
+ if (process.env[varName]) return process.env[varName];
660
+
661
+ // Project .env
662
+ try {
663
+ const projectEnv = join(process.cwd(), '.env');
664
+ if (existsSync(projectEnv)) {
665
+ const content = readFileSync(projectEnv, 'utf8');
666
+ const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
667
+ if (m) return m[1].trim();
668
+ }
669
+ } catch { /* skip */ }
670
+
671
+ // ~/.env
672
+ try {
673
+ const homeEnv = join(homeDir, '.env');
674
+ if (existsSync(homeEnv)) {
675
+ const content = readFileSync(homeEnv, 'utf8');
676
+ const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
677
+ if (m) return m[1].trim();
678
+ }
679
+ } catch { /* skip */ }
680
+
681
+ // ~/.construct/config.env
682
+ try {
683
+ const cfgPath = join(homeDir, '.construct', 'config.env');
684
+ if (existsSync(cfgPath)) {
685
+ const content = readFileSync(cfgPath, 'utf8');
686
+ const m = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
687
+ if (m) return m[1].trim();
688
+ }
689
+ } catch { /* skip */ }
690
+
691
+ // Shell rc files (including 1Password op:// refs and command substitutions)
692
+ try {
693
+ const shellFiles = [join(homeDir, '.zshrc'), join(homeDir, '.bashrc'), join(homeDir, '.bash_profile'), join(homeDir, '.profile')];
694
+ for (const rcPath of shellFiles) {
695
+ if (!existsSync(rcPath)) continue;
696
+ try {
697
+ const content = readFileSync(rcPath, 'utf8');
698
+ // Direct export (simple values only, not command substitutions)
699
+ const directRe = new RegExp(`^\\s*export\\s+${varName}="?([^"'\\$\\(]+)"?$`, 'm');
700
+ const directMatch = content.match(directRe);
701
+ if (directMatch) return directMatch[1].trim();
702
+
703
+ // op:// ref — try 1Password CLI
704
+ const opRe = new RegExp(`export\\s+${varName}=["']?\\$\\(op read '([^']+)'\\)["']?`, 'm');
705
+ const opMatch = content.match(opRe);
706
+ if (opMatch) {
707
+ try {
708
+ const result = spawnSync('op', ['read', opMatch[1]], { encoding: 'utf8', timeout: 5000 });
709
+ if (result.status === 0) return result.stdout.trim();
710
+ // Log op read failures — often the field name in the rc file
711
+ // doesn't match the actual 1Password item field name.
712
+ const errMsg = (result.stderr || '').trim().slice(0, 200);
713
+ if (errMsg && process.env.CONSTRUCT_DEBUG_CREDENTIALS === '1') {
714
+ process.stderr.write(`[credentials] op read failed for ${varName}: ${errMsg}\n`);
715
+ }
716
+ } catch { /* op read failed */ }
717
+ }
718
+
719
+ // If the value uses command substitution ($(...)), try evaluating via
720
+ // a shell that sources the rc file. This catches any $(...) pattern
721
+ // including op read with different field names or other secret managers.
722
+ const cmdSubRe = new RegExp(`^export\\s+${varName}="?\\$\\((.+)\\)"?`, 'm');
723
+ const cmdMatch = content.match(cmdSubRe);
724
+ if (cmdMatch) {
725
+ try {
726
+ const shellResult = spawnSync('zsh', ['-c', `source "${rcPath}" 2>/dev/null; echo "\${${varName}}"`], {
727
+ encoding: 'utf8', timeout: 5000,
728
+ });
729
+ if (shellResult.status === 0 && shellResult.stdout?.trim()) {
730
+ const val = shellResult.stdout.trim().split('\n').pop()?.trim();
731
+ if (val && !val.includes('ERROR') && !val.includes('could not read')) return val;
732
+ }
733
+ } catch { /* shell eval failed */ }
734
+ }
735
+ } catch { /* skip unreadable */ }
736
+ }
737
+ } catch { /* skip */ }
738
+
739
+ return null;
740
+ }