@geraldmaron/construct 1.1.1 → 1.2.0

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 (408) hide show
  1. package/README.md +29 -19
  2. package/bin/construct +311 -81
  3. package/bin/construct-postinstall.mjs +30 -2
  4. package/examples/distribution/README.md +42 -0
  5. package/examples/distribution/manifest.json +53 -0
  6. package/examples/distribution/sources/adr.md +84 -0
  7. package/examples/distribution/sources/deck-one-pager.md +65 -0
  8. package/examples/distribution/sources/prd-platform.md +161 -0
  9. package/examples/distribution/sources/research-brief.md +80 -0
  10. package/examples/distribution/sources/rfc-platform.md +82 -0
  11. package/examples/distribution/sources/runbook.md +103 -0
  12. package/examples/distribution/sources/strategy.md +88 -0
  13. package/lib/adapters-sync.mjs +67 -0
  14. package/lib/artifact-gate-notice.mjs +38 -0
  15. package/lib/artifact-manifest.mjs +112 -0
  16. package/lib/artifact-release-gate.mjs +171 -0
  17. package/lib/artifact-reviewers.mjs +68 -0
  18. package/lib/artifact-type-from-path.mjs +79 -0
  19. package/lib/audit-skills.mjs +4 -0
  20. package/lib/audit-specialists.mjs +285 -0
  21. package/lib/audit-trail.mjs +1 -1
  22. package/lib/auto-docs.mjs +87 -18
  23. package/lib/beads/drift.mjs +27 -6
  24. package/lib/beads-client.mjs +49 -121
  25. package/lib/beads-optimistic.mjs +5 -12
  26. package/lib/brand-fonts.mjs +93 -0
  27. package/lib/brand-prose.mjs +214 -0
  28. package/lib/brand-tokens.mjs +92 -0
  29. package/lib/bridges/copilot-proxy.mjs +13 -26
  30. package/lib/capability-ledger.mjs +156 -0
  31. package/lib/certification/artifact-fixtures.mjs +132 -0
  32. package/lib/certification/artifact-gates.mjs +63 -0
  33. package/lib/certification/artifact-provenance.mjs +97 -0
  34. package/lib/certification/canonical-scenarios.mjs +78 -0
  35. package/lib/certification/cli.mjs +191 -0
  36. package/lib/certification/dashboard-api.mjs +71 -0
  37. package/lib/certification/demo-parity.mjs +116 -0
  38. package/lib/certification/document-io-fixtures.mjs +246 -0
  39. package/lib/certification/document-workflow.mjs +97 -0
  40. package/lib/certification/eval-bridge.mjs +77 -0
  41. package/lib/certification/model-routing.mjs +149 -0
  42. package/lib/certification/prompt-budget.mjs +119 -0
  43. package/lib/certification/rc-gate.mjs +206 -0
  44. package/lib/certification/real-llm-scenarios.mjs +303 -0
  45. package/lib/certification/role-cards.mjs +113 -0
  46. package/lib/certification/role-overlays.mjs +117 -0
  47. package/lib/certification/run.mjs +122 -0
  48. package/lib/certification/runner.mjs +323 -0
  49. package/lib/certification/scenarios.mjs +51 -0
  50. package/lib/certification/skill-inventory.mjs +289 -0
  51. package/lib/certification/skill-scenarios.mjs +147 -0
  52. package/lib/certification/specialist-contracts.mjs +85 -0
  53. package/lib/certification/specialist-scenarios.mjs +175 -0
  54. package/lib/certification/stale-impact.mjs +146 -0
  55. package/lib/certification/status.mjs +252 -0
  56. package/lib/certification/store.mjs +77 -0
  57. package/lib/chat/cli.mjs +333 -0
  58. package/lib/chat/command-suggest.mjs +161 -0
  59. package/lib/chat/commands.mjs +215 -0
  60. package/lib/chat/config.mjs +142 -0
  61. package/lib/chat/context-compactor.mjs +250 -0
  62. package/lib/chat/context-continuation.mjs +253 -0
  63. package/lib/chat/continuation-source.mjs +58 -0
  64. package/lib/chat/demo-guide.mjs +61 -0
  65. package/lib/chat/design-tokens.mjs +91 -0
  66. package/lib/chat/desktop-binary.mjs +79 -0
  67. package/lib/chat/desktop-build.mjs +130 -0
  68. package/lib/chat/desktop-launcher.mjs +133 -0
  69. package/lib/chat/evidence.mjs +145 -0
  70. package/lib/chat/export.mjs +74 -0
  71. package/lib/chat/harness/driver.mjs +91 -0
  72. package/lib/chat/list-picker.mjs +112 -0
  73. package/lib/chat/model-picker.mjs +356 -0
  74. package/lib/chat/openrouter-fallback.mjs +151 -0
  75. package/lib/chat/permission-prompt.mjs +33 -0
  76. package/lib/chat/picker-catalog.mjs +45 -0
  77. package/lib/chat/policy-telemetry.mjs +34 -0
  78. package/lib/chat/present.mjs +245 -0
  79. package/lib/chat/session-context.mjs +39 -0
  80. package/lib/chat/session-persist.mjs +73 -0
  81. package/lib/chat/session-restore.mjs +71 -0
  82. package/lib/chat/session-settings.mjs +53 -0
  83. package/lib/chat/system-prompt.mjs +52 -0
  84. package/lib/chat/transparency.mjs +93 -0
  85. package/lib/chat/tui/color-scheme.mjs +42 -0
  86. package/lib/chat/tui/markdown.mjs +123 -0
  87. package/lib/chat/tui/presentation.mjs +100 -0
  88. package/lib/chat/tui/render.mjs +500 -0
  89. package/lib/chat/tui/turn-block.mjs +284 -0
  90. package/lib/chat/tui/turn-present.mjs +18 -0
  91. package/lib/chat/tui/turn-state.mjs +88 -0
  92. package/lib/chat/tui/usage.mjs +122 -0
  93. package/lib/chat/web-commands.mjs +146 -0
  94. package/lib/chat/web-launcher.mjs +63 -0
  95. package/lib/chat/web-picker-keys.mjs +46 -0
  96. package/lib/chat/web-session.mjs +159 -0
  97. package/lib/cli-commands.mjs +129 -15
  98. package/lib/comment-lint.mjs +6 -4
  99. package/lib/config/schema.mjs +36 -5
  100. package/lib/contract-schemas/decision.json +50 -0
  101. package/lib/contract-schemas/implementation.json +51 -0
  102. package/lib/contract-schemas/review-report.json +32 -0
  103. package/lib/contract-schemas/test-report.json +43 -0
  104. package/lib/contracts/construct-handoff.mjs +60 -0
  105. package/lib/contracts/validate.mjs +32 -3
  106. package/lib/contracts/violation-log.mjs +58 -5
  107. package/lib/dashboard-demo.mjs +71 -0
  108. package/lib/dashboard-static.mjs +19 -5
  109. package/lib/decisions/registry.mjs +5 -3
  110. package/lib/deck-export-pptx.mjs +1152 -0
  111. package/lib/demo-recording.mjs +142 -0
  112. package/lib/demo-script.mjs +114 -0
  113. package/lib/demo-surface.mjs +249 -0
  114. package/lib/demo.mjs +598 -140
  115. package/lib/diagram-export.mjs +192 -0
  116. package/lib/diagram.mjs +13 -11
  117. package/lib/docs-verify.mjs +25 -7
  118. package/lib/doctor/index.mjs +3 -1
  119. package/lib/doctor/project-adapters.mjs +44 -0
  120. package/lib/doctor/watchers/cx-budget.mjs +98 -0
  121. package/lib/doctor/watchers/graph-staleness.mjs +52 -0
  122. package/lib/doctor/watchers/process-pressure.mjs +4 -3
  123. package/lib/document-export.mjs +360 -36
  124. package/lib/document-extract.mjs +44 -4
  125. package/lib/document-ingest.mjs +29 -20
  126. package/lib/embed/cli.mjs +2 -2
  127. package/lib/embed/daemon.mjs +18 -46
  128. package/lib/embed/worker.mjs +2 -2
  129. package/lib/embedded-contract/model-resolve.mjs +10 -9
  130. package/lib/env-config.mjs +26 -0
  131. package/lib/evals/dataset.mjs +137 -0
  132. package/lib/evals/gates.mjs +175 -0
  133. package/lib/graph/build-co-change.mjs +54 -0
  134. package/lib/graph/build-from-registry.mjs +136 -0
  135. package/lib/graph/build-import-graph.mjs +153 -0
  136. package/lib/graph/cli.mjs +110 -0
  137. package/lib/graph/impact-cli.mjs +89 -0
  138. package/lib/graph/impact.mjs +108 -0
  139. package/lib/graph/staleness.mjs +44 -0
  140. package/lib/graph/store.mjs +172 -0
  141. package/lib/health-check.mjs +90 -76
  142. package/lib/hooks/artifact-release-gate.mjs +43 -0
  143. package/lib/hooks/brand-prose-lint.mjs +38 -0
  144. package/lib/hooks/graph-impact-advisory.mjs +62 -0
  145. package/lib/hooks/session-optimize.mjs +84 -207
  146. package/lib/hooks/session-start.mjs +4 -5
  147. package/lib/host-disposition.mjs +7 -0
  148. package/lib/improvement/cli.mjs +189 -0
  149. package/lib/improvement/controller.mjs +137 -0
  150. package/lib/improvement/proposal.mjs +120 -0
  151. package/lib/improvement/specialist-loop.mjs +192 -0
  152. package/lib/improvement/store.mjs +89 -0
  153. package/lib/improvement/surface.mjs +219 -0
  154. package/lib/ingest-tooling.mjs +97 -0
  155. package/lib/init/doc-lanes.mjs +165 -0
  156. package/lib/init-docs.mjs +31 -178
  157. package/lib/init-unified.mjs +27 -97
  158. package/lib/init-update-guide.mjs +102 -0
  159. package/lib/init-update.mjs +32 -1
  160. package/lib/init.mjs +8 -0
  161. package/lib/install/desktop-binary-download.mjs +85 -0
  162. package/lib/install/legacy-global-cleanup.mjs +189 -0
  163. package/lib/intake/daemon.mjs +2 -0
  164. package/lib/intake/git-queue.mjs +9 -8
  165. package/lib/intake/queue.mjs +2 -1
  166. package/lib/intake/session-prelude.mjs +90 -1
  167. package/lib/libreoffice-export.mjs +97 -0
  168. package/lib/logging/rotate.mjs +9 -0
  169. package/lib/maintenance/docker-reclaim.mjs +206 -0
  170. package/lib/mcp/external-schema-cost.mjs +74 -0
  171. package/lib/mcp/server.mjs +84 -9
  172. package/lib/mcp/stdio-mcp-probe.mjs +188 -0
  173. package/lib/mcp/tool-budget.mjs +55 -4
  174. package/lib/mcp/tools/document.mjs +18 -4
  175. package/lib/mcp/tools/skills.mjs +32 -3
  176. package/lib/mcp/tools/workflow.mjs +13 -1
  177. package/lib/model-free-selector.mjs +18 -0
  178. package/lib/model-registry.mjs +13 -229
  179. package/lib/model-router.mjs +373 -92
  180. package/lib/models/behavior-matrix.mjs +289 -0
  181. package/lib/models/catalog.mjs +209 -0
  182. package/lib/models/execution-capability-profile.mjs +196 -0
  183. package/lib/models/execution-policy.mjs +307 -0
  184. package/lib/models/provider-poll.mjs +383 -0
  185. package/lib/npm-spawn-env.mjs +17 -0
  186. package/lib/ollama/installed-models.mjs +129 -0
  187. package/lib/oracle/actions.mjs +187 -0
  188. package/lib/oracle/artifact-gate.mjs +99 -0
  189. package/lib/oracle/cli.mjs +204 -0
  190. package/lib/oracle/daemon-entry.mjs +14 -0
  191. package/lib/oracle/dispatch.mjs +81 -0
  192. package/lib/oracle/execute.mjs +143 -0
  193. package/lib/oracle/gaps.mjs +76 -0
  194. package/lib/oracle/index.mjs +84 -0
  195. package/lib/oracle/issues.mjs +164 -0
  196. package/lib/oracle/org-graph.mjs +170 -0
  197. package/lib/oracle/policy.mjs +80 -0
  198. package/lib/oracle/read-model.mjs +398 -0
  199. package/lib/oracle/reconcile.mjs +191 -0
  200. package/lib/oracle/routing.mjs +89 -0
  201. package/lib/oracle/synthesize.mjs +384 -0
  202. package/lib/oracle/verdicts.mjs +51 -0
  203. package/lib/orchestration/worker.mjs +88 -27
  204. package/lib/orchestration-policy.mjs +50 -0
  205. package/lib/parity.mjs +96 -2
  206. package/lib/playwright-demo.mjs +252 -0
  207. package/lib/project-init-shared.mjs +4 -1
  208. package/lib/prompt-composer.js +7 -3
  209. package/lib/prompt-validation-contract.mjs +24 -0
  210. package/lib/provider-capabilities.js +57 -11
  211. package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
  212. package/lib/providers/contract/adapters/git/index.mjs +115 -0
  213. package/lib/providers/contract/adapters/github/index.mjs +166 -0
  214. package/lib/providers/contract/adapters/jira/index.mjs +187 -0
  215. package/lib/providers/contract/adapters/slack/index.mjs +175 -0
  216. package/lib/providers/contract/contract-tests.mjs +57 -0
  217. package/lib/providers/contract/errors.mjs +48 -0
  218. package/lib/providers/contract/interface.mjs +50 -0
  219. package/lib/providers/contract/registry.mjs +102 -0
  220. package/lib/providers/copilot-auth.mjs +297 -0
  221. package/lib/providers/credential-bootstrap.mjs +178 -0
  222. package/lib/providers/credential-catalog.mjs +46 -0
  223. package/lib/providers/credential-sources.mjs +63 -0
  224. package/lib/providers/creds.mjs +5 -2
  225. package/lib/providers/op-run.mjs +59 -0
  226. package/lib/providers/secret-resolver.mjs +159 -0
  227. package/lib/publish-template.mjs +163 -0
  228. package/lib/publish-tooling.mjs +119 -0
  229. package/lib/publish.mjs +305 -0
  230. package/lib/registry/cli.mjs +82 -0
  231. package/lib/registry/consolidation.mjs +147 -0
  232. package/lib/registry/generate-docs.mjs +75 -0
  233. package/lib/registry/skill-verification.mjs +57 -0
  234. package/lib/registry/surface-map.mjs +76 -0
  235. package/lib/registry/validate.mjs +135 -0
  236. package/lib/resources/budget.mjs +82 -0
  237. package/lib/resources/process-budget.mjs +45 -0
  238. package/lib/rules-delivery.mjs +9 -2
  239. package/lib/rules-read.mjs +26 -0
  240. package/lib/runtime-env.mjs +23 -0
  241. package/lib/runtime-pressure.mjs +51 -7
  242. package/lib/schema-infer.mjs +13 -2
  243. package/lib/server/chat-loop.mjs +622 -0
  244. package/lib/server/demo-preview.mjs +63 -0
  245. package/lib/server/index.mjs +228 -7
  246. package/lib/service-manager.mjs +60 -8
  247. package/lib/setup.mjs +70 -7
  248. package/lib/specialists/prompt-schema.mjs +19 -10
  249. package/lib/specialists/roster.mjs +43 -0
  250. package/lib/specialists/scaffold.mjs +56 -0
  251. package/lib/storage/backend.mjs +6 -6
  252. package/lib/storage/hybrid-query.mjs +7 -4
  253. package/lib/storage/state-source.mjs +5 -5
  254. package/lib/storage/sync.mjs +2 -3
  255. package/lib/telemetry/rule-calls.mjs +52 -0
  256. package/lib/template-registry.mjs +2 -2
  257. package/lib/templates/visual-requirements.mjs +27 -51
  258. package/lib/test-corpus-inventory.mjs +313 -0
  259. package/lib/uninstall/uninstall.mjs +19 -7
  260. package/lib/update.mjs +12 -0
  261. package/lib/upgrade.mjs +14 -0
  262. package/lib/wireframe.mjs +20 -14
  263. package/lib/worker/run.mjs +17 -4
  264. package/lib/worker/trace.mjs +11 -3
  265. package/package.json +28 -14
  266. package/personas/construct.md +2 -2
  267. package/platforms/claude/settings.template.json +36 -0
  268. package/rules/common/patterns.md +1 -1
  269. package/rules/common/release-gates.md +4 -3
  270. package/scripts/sync-specialists.mjs +41 -28
  271. package/skills/devops/data-engineering.md +1 -1
  272. package/skills/docs/adr-workflow.md +1 -0
  273. package/skills/docs/backlog-proposal-workflow.md +1 -0
  274. package/skills/docs/codebase-research-workflow.md +40 -0
  275. package/skills/docs/customer-profile-workflow.md +1 -0
  276. package/skills/docs/document-ingest-workflow.md +1 -0
  277. package/skills/docs/evidence-ingest-workflow.md +1 -0
  278. package/skills/docs/init-docs.md +2 -2
  279. package/skills/docs/init-project.md +7 -2
  280. package/skills/docs/prd-workflow.md +24 -1
  281. package/skills/docs/prfaq-workflow.md +1 -0
  282. package/skills/docs/product-intelligence-workflow.md +1 -0
  283. package/skills/docs/product-signal-workflow.md +1 -0
  284. package/skills/docs/research-workflow.md +54 -37
  285. package/skills/docs/runbook-workflow.md +1 -0
  286. package/skills/docs/strategy-workflow.md +1 -0
  287. package/skills/docs/user-research-workflow.md +40 -0
  288. package/skills/operating/orchestration-reference.md +1 -1
  289. package/skills/roles/architect.md +5 -0
  290. package/skills/roles/operator.docs.md +4 -0
  291. package/skills/routing.md +4 -2
  292. package/specialists/artifact-manifest.json +480 -0
  293. package/specialists/artifact-manifest.schema.json +53 -0
  294. package/specialists/audit-enrichments.json +454 -0
  295. package/specialists/contracts.json +37 -25
  296. package/specialists/prompts/_shared/validation-contract.md +26 -0
  297. package/specialists/prompts/cx-accessibility.md +21 -2
  298. package/specialists/prompts/cx-ai-engineer.md +24 -1
  299. package/specialists/prompts/cx-architect.md +4 -1
  300. package/specialists/prompts/cx-business-strategist.md +23 -2
  301. package/specialists/prompts/cx-data-analyst.md +22 -1
  302. package/specialists/prompts/cx-data-engineer.md +23 -2
  303. package/specialists/prompts/cx-debugger.md +21 -2
  304. package/specialists/prompts/cx-designer.md +26 -3
  305. package/specialists/prompts/cx-devil-advocate.md +19 -2
  306. package/specialists/prompts/cx-docs-keeper.md +28 -1
  307. package/specialists/prompts/cx-engineer.md +22 -1
  308. package/specialists/prompts/cx-evaluator.md +18 -1
  309. package/specialists/prompts/cx-explorer.md +21 -2
  310. package/specialists/prompts/cx-legal-compliance.md +21 -2
  311. package/specialists/prompts/cx-operations.md +21 -2
  312. package/specialists/prompts/cx-oracle.md +94 -0
  313. package/specialists/prompts/cx-orchestrator.md +20 -1
  314. package/specialists/prompts/cx-platform-engineer.md +25 -2
  315. package/specialists/prompts/cx-product-manager.md +32 -1
  316. package/specialists/prompts/cx-qa.md +24 -2
  317. package/specialists/prompts/cx-rd-lead.md +23 -2
  318. package/specialists/prompts/cx-release-manager.md +23 -2
  319. package/specialists/prompts/cx-researcher.md +22 -2
  320. package/specialists/prompts/cx-reviewer.md +18 -1
  321. package/specialists/prompts/cx-security.md +22 -2
  322. package/specialists/prompts/cx-sre.md +30 -3
  323. package/specialists/prompts/cx-test-automation.md +7 -1
  324. package/specialists/prompts/cx-trace-reviewer.md +22 -3
  325. package/specialists/prompts/cx-ux-researcher.md +21 -2
  326. package/specialists/registry.json +52 -173
  327. package/specialists/tone-profiles.json +42 -0
  328. package/templates/demos/playwright/demo-recording.config.mjs +47 -0
  329. package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
  330. package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
  331. package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
  332. package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
  333. package/templates/demos/tapes/resource-guard-rails.tape +49 -0
  334. package/templates/demos/vhs/construct-cockpit.json +24 -0
  335. package/templates/distribution/construct-brand.typ +446 -0
  336. package/templates/distribution/construct-decision.typ +38 -0
  337. package/templates/distribution/construct-deck.html +95 -0
  338. package/templates/distribution/construct-pdf.typ +38 -0
  339. package/templates/distribution/construct-prd.typ +38 -0
  340. package/templates/distribution/construct-research.typ +38 -0
  341. package/templates/distribution/construct-web.html +92 -0
  342. package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
  343. package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
  344. package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
  345. package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
  346. package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
  347. package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
  348. package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
  349. package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
  350. package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
  351. package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
  352. package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
  353. package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
  354. package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
  355. package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
  356. package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
  357. package/templates/distribution/fonts/README.md +36 -0
  358. package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
  359. package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
  360. package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
  361. package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
  362. package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
  363. package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
  364. package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
  365. package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
  366. package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
  367. package/templates/distribution/icons/activity.svg +15 -0
  368. package/templates/distribution/icons/alert-triangle.svg +17 -0
  369. package/templates/distribution/icons/book-open.svg +16 -0
  370. package/templates/distribution/icons/bot.svg +20 -0
  371. package/templates/distribution/icons/brain.svg +22 -0
  372. package/templates/distribution/icons/circle-check.svg +16 -0
  373. package/templates/distribution/icons/clipboard-check.svg +17 -0
  374. package/templates/distribution/icons/cpu.svg +28 -0
  375. package/templates/distribution/icons/database.svg +17 -0
  376. package/templates/distribution/icons/eye.svg +16 -0
  377. package/templates/distribution/icons/file-text.svg +19 -0
  378. package/templates/distribution/icons/gauge.svg +16 -0
  379. package/templates/distribution/icons/git-branch.svg +17 -0
  380. package/templates/distribution/icons/key.svg +17 -0
  381. package/templates/distribution/icons/layers.svg +17 -0
  382. package/templates/distribution/icons/list-checks.svg +19 -0
  383. package/templates/distribution/icons/lock.svg +16 -0
  384. package/templates/distribution/icons/message-square.svg +15 -0
  385. package/templates/distribution/icons/network.svg +19 -0
  386. package/templates/distribution/icons/route.svg +17 -0
  387. package/templates/distribution/icons/scale.svg +19 -0
  388. package/templates/distribution/icons/search.svg +16 -0
  389. package/templates/distribution/icons/send.svg +16 -0
  390. package/templates/distribution/icons/server.svg +18 -0
  391. package/templates/distribution/icons/shield-check.svg +16 -0
  392. package/templates/distribution/icons/users.svg +18 -0
  393. package/templates/distribution/icons/webhook.svg +17 -0
  394. package/templates/distribution/icons/workflow.svg +17 -0
  395. package/templates/distribution/icons/wrench.svg +15 -0
  396. package/templates/distribution/run.mjs +18 -1
  397. package/templates/docs/adr.md +7 -0
  398. package/templates/docs/construct_guide.md +8 -8
  399. package/templates/docs/customer-profile.md +4 -0
  400. package/templates/docs/one-pager.md +4 -0
  401. package/templates/docs/postmortem.md +31 -0
  402. package/templates/docs/prd-platform.md +19 -0
  403. package/templates/docs/prd.md +15 -0
  404. package/templates/docs/prfaq.md +7 -0
  405. package/templates/docs/qa-strategy.md +103 -0
  406. package/templates/docs/research-brief.md +8 -0
  407. package/templates/docs/rfc-platform.md +12 -0
  408. package/templates/docs/test-plan.md +7 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * lib/graph/impact-cli.mjs — `construct impact` command surface.
3
+ *
4
+ * Maps changed files to the tests that should run and the capabilities and
5
+ * workflows they touch, using the living dependency graph. Advisory by default;
6
+ * `--run` executes the selected tests via `node --test`. Files come from
7
+ * positional args, or — when none are given — from `git diff --name-only HEAD`.
8
+ *
9
+ * construct impact lib/oracle/synthesize.mjs
10
+ * construct impact --run
11
+ * git diff --name-only origin/main | construct impact --stdin --json
12
+ */
13
+
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+ import path from 'node:path';
16
+ import { spawnSync } from 'node:child_process';
17
+
18
+ import { computeImpact } from './impact.mjs';
19
+
20
+ function readStdin() {
21
+ try { return readFileSync(0, 'utf8'); } catch { return ''; }
22
+ }
23
+
24
+ function gitChangedFiles(projectDir) {
25
+ const res = spawnSync('git', ['diff', '--name-only', 'HEAD'], { cwd: projectDir, encoding: 'utf8' });
26
+ if (res.status !== 0) return [];
27
+ return res.stdout.split('\n').map((l) => l.trim()).filter(Boolean);
28
+ }
29
+
30
+ /**
31
+ * @param {string[]} args
32
+ * @param {{ rootDir: string, projectDir: string }} ctx
33
+ * @returns {Promise<number>} exit code
34
+ */
35
+ export async function runImpactCli(args, { projectDir }) {
36
+ const json = args.includes('--json');
37
+ const run = args.includes('--run');
38
+ const useStdin = args.includes('--stdin');
39
+ let files = args.filter((a) => !a.startsWith('--'));
40
+
41
+ if (useStdin) files = readStdin().split('\n').map((l) => l.trim()).filter(Boolean);
42
+ if (files.length === 0) files = gitChangedFiles(projectDir);
43
+
44
+ if (files.length === 0) {
45
+ process.stderr.write('No changed files given (args, --stdin, or git diff). Nothing to analyze.\n');
46
+ return 1;
47
+ }
48
+
49
+ const result = computeImpact({ rootDir: projectDir, changedFiles: files });
50
+
51
+ if (!result.graphPresent) {
52
+ process.stderr.write('No dependency graph found. Run `construct matrix build` first.\n');
53
+ return 1;
54
+ }
55
+
56
+ if (json && !run) {
57
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
58
+ return 0;
59
+ }
60
+
61
+ if (!json) {
62
+ process.stdout.write(`Changed: ${result.changed.length} file(s)\n`);
63
+ process.stdout.write(`Impacted capabilities (${result.impactedCapabilities.length}): ${result.impactedCapabilities.join(', ') || '(none)'}\n`);
64
+ process.stdout.write(`Stale capabilities (${result.staleCapabilities.length}): ${result.staleCapabilities.join(', ') || '(none)'}\n`);
65
+ process.stdout.write(`Impacted workflows (${result.impactedWorkflows.length}): ${result.impactedWorkflows.join(', ') || '(none)'}\n`);
66
+ process.stdout.write(`Affected tests (${result.affectedTests.length}):\n`);
67
+ for (const t of result.affectedTests) process.stdout.write(` ${t}\n`);
68
+ if (result.coverageGaps.length) {
69
+ process.stdout.write(`\n⚠ Coverage gap — changed files realizing no capability (${result.coverageGaps.length}):\n`);
70
+ for (const f of result.coverageGaps) process.stdout.write(` ${f}\n`);
71
+ }
72
+ if (result.unknown.length) {
73
+ process.stdout.write(`\nNot in graph (${result.unknown.length}): ${result.unknown.join(', ')}\n`);
74
+ }
75
+ }
76
+
77
+ if (run) {
78
+ const runnable = result.affectedTests.filter((t) => existsSync(path.join(projectDir, t)));
79
+ if (runnable.length === 0) {
80
+ process.stdout.write('\nNo runnable affected tests on disk.\n');
81
+ return 0;
82
+ }
83
+ process.stdout.write(`\n▶ Running ${runnable.length} affected test(s)…\n`);
84
+ const res = spawnSync(process.execPath, ['--test', ...runnable], { cwd: projectDir, stdio: 'inherit' });
85
+ return res.status ?? 1;
86
+ }
87
+
88
+ return 0;
89
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * lib/graph/impact.mjs — forward change-impact over the dependency graph.
3
+ *
4
+ * Given a set of changed files, computes the tests that should run, the
5
+ * capabilities and workflows impacted, and any changed file that realizes no
6
+ * capability (a coverage gap). Test selection is Test Impact Analysis over the
7
+ * static import graph: a test is affected if it transitively imports a changed
8
+ * file, or if it validates a capability that a changed file realizes. The
9
+ * union is intentionally conservative — over-selection is safe, under-selection
10
+ * silently ships untested change.
11
+ */
12
+
13
+ import { loadGraph, dependentsOf, dependenciesOf } from './store.mjs';
14
+ import { rollupStaleImpact } from '../certification/stale-impact.mjs';
15
+
16
+ function isTestRel(rel) {
17
+ return rel.endsWith('.test.mjs') || rel.endsWith('.test.js');
18
+ }
19
+
20
+ function toNodeId(rel) {
21
+ return isTestRel(rel) ? `test:${rel}` : `file:${rel}`;
22
+ }
23
+
24
+ function reverseImportClosure(graph, startId) {
25
+ const reached = new Set();
26
+ const stack = [startId];
27
+ while (stack.length) {
28
+ const id = stack.pop();
29
+ for (const importer of dependentsOf(graph, id, 'imports')) {
30
+ if (reached.has(importer)) continue;
31
+ reached.add(importer);
32
+ stack.push(importer);
33
+ }
34
+ }
35
+ return reached;
36
+ }
37
+
38
+ /**
39
+ * @param {object} opts
40
+ * @param {string} opts.rootDir — project root holding .cx/graph/.
41
+ * @param {string[]} opts.changedFiles — repo-relative paths.
42
+ * @returns {{ changed: string[], unknown: string[], affectedTests: string[], impactedCapabilities: string[], impactedWorkflows: string[], staleCapabilities: string[], coverageGaps: string[], graphPresent: boolean }}
43
+ */
44
+ export function computeImpact({ rootDir, changedFiles }) {
45
+ const graph = loadGraph(rootDir);
46
+ const norm = [...new Set((changedFiles || []).map((f) => f.split('\\').join('/').replace(/^\.\//, '')))].filter(Boolean);
47
+ if (!graph.exists) {
48
+ return {
49
+ changed: norm,
50
+ unknown: norm,
51
+ affectedTests: [],
52
+ impactedCapabilities: [],
53
+ impactedWorkflows: [],
54
+ staleCapabilities: rollupStaleImpact({ rootDir, changedFiles: norm }),
55
+ coverageGaps: [],
56
+ graphPresent: false,
57
+ };
58
+ }
59
+
60
+ const tests = new Set();
61
+ const capabilities = new Set();
62
+ const unknown = [];
63
+ const coverageGaps = [];
64
+
65
+ for (const rel of norm) {
66
+ const id = toNodeId(rel);
67
+ if (!graph.nodes.has(id)) { unknown.push(rel); continue; }
68
+
69
+ if (id.startsWith('test:')) tests.add(id);
70
+
71
+ for (const importer of reverseImportClosure(graph, id)) {
72
+ if (importer.startsWith('test:')) tests.add(importer);
73
+ }
74
+
75
+ const caps = dependenciesOf(graph, id, 'realizes');
76
+ for (const c of caps) capabilities.add(c);
77
+ if (id.startsWith('file:') && caps.length === 0) coverageGaps.push(rel);
78
+ }
79
+
80
+ // Capabilities reached transitively also pull in their declared verification
81
+ // tests; their embedded workflows complete the requirement trace.
82
+
83
+ for (const cap of [...capabilities]) {
84
+ for (const t of dependentsOf(graph, cap, 'validates')) tests.add(t);
85
+ }
86
+ for (const t of tests) {
87
+ for (const cap of dependenciesOf(graph, t, 'validates')) capabilities.add(cap);
88
+ }
89
+
90
+ const workflows = new Set();
91
+ for (const cap of capabilities) {
92
+ for (const wf of dependenciesOf(graph, cap, 'embeds')) workflows.add(wf);
93
+ }
94
+
95
+ const strip = (set, prefix) => [...set].map((id) => id.slice(prefix.length)).sort();
96
+ const impactedCapabilityIds = strip(capabilities, 'capability:');
97
+ const staleCapabilities = rollupStaleImpact({ rootDir, changedFiles: norm });
98
+ return {
99
+ changed: norm,
100
+ unknown,
101
+ affectedTests: [...tests].map((id) => id.slice('test:'.length)).sort(),
102
+ impactedCapabilities: impactedCapabilityIds,
103
+ impactedWorkflows: strip(workflows, 'workflow:'),
104
+ staleCapabilities,
105
+ coverageGaps: coverageGaps.sort(),
106
+ graphPresent: true,
107
+ };
108
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * lib/graph/staleness.mjs — dependency graph seed-hash staleness checks.
3
+ *
4
+ * Compares the hash of registry/contracts/workflow seed files against the
5
+ * hash stored at last graph build (.cx/graph/meta.json). Shared by Oracle,
6
+ * the doctor graph-staleness watcher, and PostToolUse advisories.
7
+ */
8
+
9
+ import { hashFiles } from './build-from-registry.mjs';
10
+ import { loadGraph } from './store.mjs';
11
+
12
+ export const GRAPH_SEED_FILES = [
13
+ 'registry/capabilities.json',
14
+ 'specialists/contracts.json',
15
+ 'lib/embedded-contract/workflow-defs.mjs',
16
+ ];
17
+
18
+ /**
19
+ * @param {string} rootDir — project root holding .cx/graph/.
20
+ * @returns {{ present: boolean, stale: boolean, staleReason: string|null, currentHash?: string, storedHash?: string|null }}
21
+ */
22
+ export function checkGraphStaleness(rootDir) {
23
+ const graph = loadGraph(rootDir);
24
+ if (!graph.exists) {
25
+ return { present: false, stale: false, staleReason: null };
26
+ }
27
+
28
+ try {
29
+ const current = hashFiles(rootDir, GRAPH_SEED_FILES);
30
+ const stored = graph.meta?.sourceHash ?? null;
31
+ if (stored && current !== stored) {
32
+ return {
33
+ present: true,
34
+ stale: true,
35
+ staleReason: 'registry/contracts/workflow seeds changed since last build',
36
+ currentHash: current,
37
+ storedHash: stored,
38
+ };
39
+ }
40
+ return { present: true, stale: false, staleReason: null, currentHash: current, storedHash: stored };
41
+ } catch {
42
+ return { present: true, stale: false, staleReason: null };
43
+ }
44
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * lib/graph/store.mjs — typed, directed dependency-graph store.
3
+ *
4
+ * The substrate for Construct's living dependency matrix. Nodes are typed
5
+ * entities (file, module, workflow, capability, test, contract, surface,
6
+ * skill, rule) addressed by canonical ids of the form `type:key`. Edges are
7
+ * directed and carry a relation kind (imports, realizes, validates, covers,
8
+ * exposes, governed_by, uses, embeds, co_changes) and a provenance source
9
+ * (registry, import-graph, co-change, override).
10
+ *
11
+ * Persisted under `.cx/graph/` as deterministic JSONL so a graph round-trips
12
+ * losslessly and diffs cleanly: nodes.jsonl (sorted by id), edges.jsonl
13
+ * (sorted by from,rel,to), meta.json (generatedAt, sourceHash, counts).
14
+ * Dependency-free and pure-JS per ADR-0001.
15
+ */
16
+
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
18
+ import path from 'node:path';
19
+
20
+ export const NODE_TYPES = new Set([
21
+ 'file', 'module', 'workflow', 'capability', 'test', 'contract', 'surface', 'skill', 'rule',
22
+ ]);
23
+
24
+ export const EDGE_RELS = new Set([
25
+ 'imports', 'realizes', 'validates', 'covers', 'exposes', 'governed_by', 'uses', 'embeds', 'co_changes', 'contains',
26
+ ]);
27
+
28
+ export const EDGE_SOURCES = new Set(['registry', 'import-graph', 'co-change', 'override']);
29
+
30
+ const STORE_SUBDIR = path.join('.cx', 'graph');
31
+
32
+ export function graphDir(rootDir) {
33
+ return path.join(rootDir, STORE_SUBDIR);
34
+ }
35
+
36
+ export function nodeId(type, key) {
37
+ return `${type}:${key}`;
38
+ }
39
+
40
+ function nodesPath(rootDir) { return path.join(graphDir(rootDir), 'nodes.jsonl'); }
41
+ function edgesPath(rootDir) { return path.join(graphDir(rootDir), 'edges.jsonl'); }
42
+ function metaPath(rootDir) { return path.join(graphDir(rootDir), 'meta.json'); }
43
+
44
+ function readJsonl(filePath) {
45
+ if (!existsSync(filePath)) return [];
46
+ const out = [];
47
+ for (const line of readFileSync(filePath, 'utf8').split('\n')) {
48
+ if (!line.trim()) continue;
49
+ try { out.push(JSON.parse(line)); } catch { /* skip malformed line */ }
50
+ }
51
+ return out;
52
+ }
53
+
54
+ function writeAtomic(filePath, contents) {
55
+ const tmp = `${filePath}.tmp`;
56
+ writeFileSync(tmp, contents, 'utf8');
57
+ renameSync(tmp, filePath);
58
+ }
59
+
60
+ // Canonical de-dup: nodes keyed by id (last write wins on attrs), edges keyed
61
+ // by from|rel|to (weights summed so repeated co-change observations accumulate).
62
+
63
+ function normalizeNodes(nodes) {
64
+ const byId = new Map();
65
+ for (const n of nodes) {
66
+ if (!n?.id || !n?.type) continue;
67
+ const prev = byId.get(n.id);
68
+ byId.set(n.id, prev ? { ...prev, ...n, attrs: { ...(prev.attrs || {}), ...(n.attrs || {}) } } : { id: n.id, type: n.type, name: n.name ?? n.id, attrs: n.attrs || {} });
69
+ }
70
+ return [...byId.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
71
+ }
72
+
73
+ function edgeKey(e) { return `${e.from}|${e.rel}|${e.to}`; }
74
+
75
+ function normalizeEdges(edges) {
76
+ const byKey = new Map();
77
+ for (const e of edges) {
78
+ if (!e?.from || !e?.to || !e?.rel) continue;
79
+ const key = edgeKey(e);
80
+ const prev = byKey.get(key);
81
+ if (prev) {
82
+ prev.weight = (prev.weight || 1) + (e.weight || 1);
83
+ if (e.source && !prev.sources.includes(e.source)) prev.sources.push(e.source);
84
+ } else {
85
+ byKey.set(key, { from: e.from, to: e.to, rel: e.rel, weight: e.weight || 1, sources: e.source ? [e.source] : (e.sources || []) });
86
+ }
87
+ }
88
+ return [...byKey.values()].sort((a, b) => (edgeKey(a) < edgeKey(b) ? -1 : 1));
89
+ }
90
+
91
+ function countBy(items, key) {
92
+ const out = {};
93
+ for (const it of items) { const k = it[key]; out[k] = (out[k] || 0) + 1; }
94
+ return out;
95
+ }
96
+
97
+ /**
98
+ * Persist a full graph (regenerate semantics). Nodes and edges are normalized
99
+ * and sorted; meta records counts and the caller-supplied source hash.
100
+ *
101
+ * @param {string} rootDir
102
+ * @param {{ nodes: object[], edges: object[], generatedAt?: string, sourceHash?: string }} graph
103
+ * @returns {{ nodeCount: number, edgeCount: number, dir: string }}
104
+ */
105
+ export function writeGraph(rootDir, { nodes = [], edges = [], generatedAt = null, sourceHash = null } = {}) {
106
+ const dir = graphDir(rootDir);
107
+ mkdirSync(dir, { recursive: true });
108
+ const normNodes = normalizeNodes(nodes);
109
+ const normEdges = normalizeEdges(edges);
110
+ writeAtomic(nodesPath(rootDir), normNodes.map((n) => JSON.stringify(n)).join('\n') + (normNodes.length ? '\n' : ''));
111
+ writeAtomic(edgesPath(rootDir), normEdges.map((e) => JSON.stringify(e)).join('\n') + (normEdges.length ? '\n' : ''));
112
+ const meta = {
113
+ generatedAt,
114
+ sourceHash,
115
+ nodeCount: normNodes.length,
116
+ edgeCount: normEdges.length,
117
+ nodesByType: countBy(normNodes, 'type'),
118
+ edgesByRel: countBy(normEdges, 'rel'),
119
+ };
120
+ writeAtomic(metaPath(rootDir), JSON.stringify(meta, null, 2) + '\n');
121
+ return { nodeCount: normNodes.length, edgeCount: normEdges.length, dir };
122
+ }
123
+
124
+ /**
125
+ * Load the graph into an indexed, queryable view. Out/in adjacency maps make
126
+ * forward (dependencies) and reverse (dependents) traversal O(degree).
127
+ *
128
+ * @param {string} rootDir
129
+ * @returns {{ nodes: Map<string,object>, edges: object[], out: Map<string,object[]>, in: Map<string,object[]>, meta: object|null, exists: boolean }}
130
+ */
131
+ export function loadGraph(rootDir) {
132
+ const nodeList = readJsonl(nodesPath(rootDir));
133
+ const edgeList = readJsonl(edgesPath(rootDir));
134
+ let meta = null;
135
+ try { meta = JSON.parse(readFileSync(metaPath(rootDir), 'utf8')); } catch { /* no meta */ }
136
+
137
+ const nodes = new Map();
138
+ for (const n of nodeList) nodes.set(n.id, n);
139
+ const out = new Map();
140
+ const inc = new Map();
141
+ for (const e of edgeList) {
142
+ if (!out.has(e.from)) out.set(e.from, []);
143
+ if (!inc.has(e.to)) inc.set(e.to, []);
144
+ out.get(e.from).push(e);
145
+ inc.get(e.to).push(e);
146
+ }
147
+ return { nodes, edges: edgeList, out, in: inc, meta, exists: existsSync(nodesPath(rootDir)) };
148
+ }
149
+
150
+ function filterRel(edges, rel) {
151
+ if (!rel) return edges;
152
+ const rels = Array.isArray(rel) ? new Set(rel) : new Set([rel]);
153
+ return edges.filter((e) => rels.has(e.rel));
154
+ }
155
+
156
+ /**
157
+ * Nodes that `id` points to along `rel` (its dependencies).
158
+ */
159
+ export function dependenciesOf(graph, id, rel = null) {
160
+ return filterRel(graph.out.get(id) || [], rel).map((e) => e.to);
161
+ }
162
+
163
+ /**
164
+ * Nodes that point to `id` along `rel` (its dependents).
165
+ */
166
+ export function dependentsOf(graph, id, rel = null) {
167
+ return filterRel(graph.in.get(id) || [], rel).map((e) => e.from);
168
+ }
169
+
170
+ export function nodesByType(graph, type) {
171
+ return [...graph.nodes.values()].filter((n) => n.type === type);
172
+ }
@@ -10,6 +10,7 @@ import { spawnSync } from 'node:child_process';
10
10
  import os from 'node:os';
11
11
  import path from 'node:path';
12
12
  import { existsSync, readFileSync } from 'node:fs';
13
+ import { extractOpRef, hasAnySecret, hasSecret } from './providers/secret-resolver.mjs';
13
14
 
14
15
  const isWindows = os.platform() === 'win32';
15
16
 
@@ -165,37 +166,73 @@ export async function checkPrerequisites(options = {}) {
165
166
  }
166
167
 
167
168
  /**
168
- * Get a value from shell env by checking common sources.
169
- * Looks in process.env project .env ~/.env ~/.construct/config.env
170
- * shell rc files for op:// refs (resolved via 'op read').
169
+ * Read a credential raw value from dotenv files and shell rc without invoking
170
+ * `op read`. Returns op:// references as-is so callers can persist or report
171
+ * presence without biometric prompts (ADR-0042).
171
172
  */
172
- function getEnvFromShell(varName, homeDir) {
173
+ function readCredentialRaw(varName, homeDir) {
173
174
  if (process.env[varName]) return process.env[varName];
174
175
 
175
- const projectEnv = path.join(process.cwd(), '.env');
176
- if (existsSync(projectEnv)) {
176
+ for (const envPath of [
177
+ path.join(process.cwd(), '.env'),
178
+ path.join(homeDir, '.env'),
179
+ path.join(homeDir, '.construct', 'config.env'),
180
+ ]) {
181
+ if (!existsSync(envPath)) continue;
177
182
  try {
178
- const content = readFileSync(projectEnv, 'utf8');
183
+ const content = readFileSync(envPath, 'utf8');
179
184
  const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
180
185
  if (m) return m[1].trim();
181
186
  } catch { /* skip */ }
182
187
  }
183
188
 
184
- const homeEnv = path.join(homeDir, '.env');
185
- if (existsSync(homeEnv)) {
189
+ const shellFiles = [
190
+ path.join(homeDir, '.zshrc'),
191
+ path.join(homeDir, '.bashrc'),
192
+ path.join(homeDir, '.bash_profile'),
193
+ path.join(homeDir, '.profile'),
194
+ ];
195
+ for (const rcPath of shellFiles) {
196
+ if (!existsSync(rcPath)) continue;
186
197
  try {
187
- const content = readFileSync(homeEnv, 'utf8');
188
- const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
189
- if (m) return m[1].trim();
198
+ const content = readFileSync(rcPath, 'utf8');
199
+ const opRe = new RegExp(`export\\s+${varName}=["']?\\$\\(op read '([^']+)'\\)["']?`, 'm');
200
+ const opMatch = content.match(opRe);
201
+ if (opMatch) return opMatch[1];
202
+ const directRe = new RegExp(`^\\s*export\\s+${varName}=(.+)$`, 'm');
203
+ const directMatch = content.match(directRe);
204
+ if (directMatch) return directMatch[1].trim().replace(/^["']|["']$/g, '');
190
205
  } catch { /* skip */ }
191
206
  }
192
207
 
193
- const cfgPath = path.join(homeDir, '.construct', 'config.env');
194
- if (existsSync(cfgPath)) {
208
+ return null;
209
+ }
210
+
211
+ /**
212
+ * Describe where a credential is configured without resolving op:// refs.
213
+ */
214
+ export function describeCredentialPresence(varName, { homeDir = os.homedir(), cwd = process.cwd() } = {}) {
215
+ const sources = [];
216
+ const direct = process.env[varName];
217
+ if (direct) {
218
+ const ref = extractOpRef(direct);
219
+ sources.push(ref ? `process.env (op:// ref)` : `process.env (plain value)`);
220
+ }
221
+
222
+ for (const envPath of [
223
+ path.join(cwd, '.env'),
224
+ path.join(homeDir, '.env'),
225
+ path.join(homeDir, '.construct', 'config.env'),
226
+ ]) {
227
+ if (!existsSync(envPath)) continue;
195
228
  try {
196
- const content = readFileSync(cfgPath, 'utf8');
197
- const m = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
198
- if (m) return m[1].trim();
229
+ const content = readFileSync(envPath, 'utf8');
230
+ const m = content.match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
231
+ if (m) {
232
+ const ref = extractOpRef(m[1]);
233
+ const label = envPath.replace(homeDir, '~');
234
+ sources.push(ref ? `${label} (op:// ref)` : `${label} (plain value)`);
235
+ }
199
236
  } catch { /* skip */ }
200
237
  }
201
238
 
@@ -209,47 +246,18 @@ function getEnvFromShell(varName, homeDir) {
209
246
  if (!existsSync(rcPath)) continue;
210
247
  try {
211
248
  const content = readFileSync(rcPath, 'utf8');
212
- const re = new RegExp(`export\\s+${varName}=["']?\\$\\(op read '([^']+)'\\)["']?`, 'm');
213
- const m = content.match(re);
214
- if (m) {
215
- const opRef = m[1];
216
- try {
217
- const result = spawnSync('op', ['read', opRef], {
218
- encoding: 'utf8',
219
- stdio: ['pipe', 'pipe', 'pipe'],
220
- timeout: 5000,
221
- });
222
- if (result.status === 0) return result.stdout.trim();
223
- } catch { /* op read failed */ }
249
+ const opRe = new RegExp(`export\\s+${varName}=["']?\\$\\(op read '([^']+)'\\)["']?`, 'm');
250
+ if (opRe.test(content)) {
251
+ sources.push(`${rcPath.replace(homeDir, '~')} (op read in rc — use op:// in config.env instead)`);
224
252
  }
225
- } catch { /* skip unreadable files */ }
226
- }
227
-
228
- // If we found an op:// reference but couldn't read it, log the error
229
- // for diagnostic purposes when DEBUG is on.
230
- if (process.env.CONSTRUCT_DEBUG_CREDENTIALS === '1') {
231
- for (const rcPath of shellFiles) {
232
- if (!existsSync(rcPath)) continue;
233
- try {
234
- const content = readFileSync(rcPath, 'utf8');
235
- const re = new RegExp(`export\\s+${varName}=["']?\\$\\(op read '([^']+)'\\)["']?`, 'm');
236
- const m = content.match(re);
237
- if (m) {
238
- try {
239
- const result = spawnSync('op', ['read', m[1]], { encoding: 'utf8', timeout: 5000 });
240
- if (result.status !== 0) {
241
- const errMsg = (result.stderr || '').trim().slice(0, 200);
242
- process.stderr.write(`[credentials] ${varName}: op read '${m[1]}' failed: ${errMsg}\n`);
243
- }
244
- } catch (e) {
245
- process.stderr.write(`[credentials] ${varName}: op CLI error: ${e.message}\n`);
246
- }
247
- }
248
- } catch { /* skip */ }
249
- }
253
+ const directRe = new RegExp(`^\\s*export\\s+${varName}=`, 'm');
254
+ if (directRe.test(content) && !sources.some((s) => s.includes(rcPath.replace(homeDir, '~')))) {
255
+ sources.push(`${rcPath.replace(homeDir, '~')} (export found)`);
256
+ }
257
+ } catch { /* skip */ }
250
258
  }
251
259
 
252
- return null;
260
+ return { configured: hasSecret(varName, { env: process.env, cwd }), sources };
253
261
  }
254
262
 
255
263
  /**
@@ -287,30 +295,34 @@ const CREDENTIAL_VARS = [
287
295
  ];
288
296
 
289
297
  /**
290
- * Resolve all known credentials from env, .env files, and 1Password.
298
+ * Link discovered credential references into ~/.construct/config.env without
299
+ * invoking `op read`. Plain values and op:// refs are persisted as found.
291
300
  * @param {Object} options
292
301
  * @param {string} options.homeDir
293
- * @param {boolean} options.writeConfig - Write resolved keys to config.env (default: true)
294
- * @returns {Promise<Record<string, string>>} resolved key-value pairs
302
+ * @param {boolean} options.writeConfig - Write keys to config.env (default: false)
303
+ * @returns {Promise<Record<string, string>>} raw values linked (never plaintext from op)
295
304
  */
296
305
  export async function resolveCredentials(options = {}) {
297
- const { homeDir = os.homedir(), writeConfig = true } = options;
298
- const resolved = {};
306
+ const { homeDir = os.homedir(), writeConfig = false } = options;
307
+ const linked = {};
308
+ const { parseEnvFile, getUserEnvPath, writeEnvValues } = await import('./env-config.mjs');
309
+ const existing = parseEnvFile(getUserEnvPath(homeDir));
299
310
 
300
311
  for (const varName of CREDENTIAL_VARS) {
301
- const value = getEnvFromShell(varName, homeDir);
302
- if (value) resolved[varName] = value;
312
+ if (existing[varName]) continue;
313
+ const raw = readCredentialRaw(varName, homeDir);
314
+ if (!raw) continue;
315
+ const ref = extractOpRef(raw);
316
+ linked[varName] = ref || raw;
303
317
  }
304
318
 
305
- // Write resolved keys to config.env so subprocesses and the dashboard find them
306
- if (writeConfig && Object.keys(resolved).length > 0) {
319
+ if (writeConfig && Object.keys(linked).length > 0) {
307
320
  try {
308
- const { writeEnvValues, getUserEnvPath } = await import('./env-config.mjs');
309
- writeEnvValues(getUserEnvPath(homeDir), resolved);
321
+ writeEnvValues(getUserEnvPath(homeDir), linked);
310
322
  } catch { /* best effort */ }
311
323
  }
312
324
 
313
- return resolved;
325
+ return linked;
314
326
  }
315
327
 
316
328
  /**
@@ -327,22 +339,24 @@ export function detectCredentials(options = {}) {
327
339
  const ghAuth = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
328
340
  found.github_cli = ghAuth.status === 0;
329
341
 
330
- // GitHub Copilot (check VS Code extension state)
342
+ // GitHub Copilot: a device-flow credential (Construct's own store or the shared
343
+ // github-copilot store) is the usable signal; the VS Code extension and the
344
+ // copilot CLI are softer fallbacks.
345
+ const copilotAuthStore = existsSync(path.join(homeDir, '.construct', 'auth', 'github-copilot.json')) ||
346
+ existsSync(path.join(homeDir, '.config', 'github-copilot', 'apps.json')) ||
347
+ existsSync(path.join(homeDir, '.config', 'github-copilot', 'hosts.json'));
331
348
  const copilotDir = path.join(homeDir, '.vscode', 'extensions');
332
349
  const copilotExists = existsSync(copilotDir) && existsSync(path.join(homeDir, '.config', 'Code', 'User', 'globalStorage', 'github.copilot'));
333
- found.copilot = copilotExists || commandExists('copilot');
350
+ found.copilot = copilotAuthStore || copilotExists || commandExists('copilot');
334
351
 
335
352
  // OpenRouter API key
336
- found.openrouter_key = !!getEnvFromShell('OPENROUTER_API_KEY', homeDir);
353
+ found.openrouter_key = hasAnySecret(['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'], { env: process.env, cwd: process.cwd() });
337
354
 
338
- // Anthropic API key
339
- found.anthropic_key = !!getEnvFromShell('ANTHROPIC_API_KEY', homeDir);
355
+ found.anthropic_key = hasAnySecret(['ANTHROPIC_API_KEY'], { env: process.env, cwd: process.cwd() });
340
356
 
341
- // OpenAI API key
342
- found.openai_key = !!getEnvFromShell('OPENAI_API_KEY', homeDir);
357
+ found.openai_key = hasAnySecret(['OPENAI_API_KEY'], { env: process.env, cwd: process.cwd() });
343
358
 
344
- // GitHub token
345
- found.github_token = !!getEnvFromShell('GITHUB_TOKEN', homeDir) || !!getEnvFromShell('GH_TOKEN', homeDir);
359
+ found.github_token = hasAnySecret(['GITHUB_TOKEN', 'GH_TOKEN'], { env: process.env, cwd: process.cwd() });
346
360
 
347
361
  // VS Code installed
348
362
  found.vscode = commandExists('code');