@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
@@ -4,7 +4,7 @@
4
4
  * Serves the single-page dashboard from lib/server/static/, provides a JSON
5
5
  * status API, SSE live-reload, REST endpoints for registry management,
6
6
  * artifact generation/listing, approval queue inspection, snapshot data,
7
- * token-based auth, and SSE-streamed chat via the claude CLI.
7
+ * token-based auth, legacy claude CLI chat, and owned-loop SSE web chat.
8
8
  * Runs on port 4242 (overridable via PORT env var), bound to 127.0.0.1.
9
9
  */
10
10
  import { createServer } from 'http';
@@ -19,15 +19,21 @@ import { buildStatus as buildSharedStatus } from '../status.mjs';
19
19
  import { generateArtifact, listArtifacts } from '../embed/artifact.mjs';
20
20
  import { ApprovalQueue } from '../embed/approval-queue.mjs';
21
21
  import { resolveEmbedStatus } from '../embed/cli.mjs';
22
- import { loadConstructEnv } from '../env-config.mjs';
22
+ import { loadConstructEnv, prepareConstructEnv } from '../runtime-env.mjs';
23
23
  import {
24
24
  isAuthConfigured, isAuthenticated, rejectUnauthorized,
25
25
  validateToken, createSession, sessionCookieHeader, clearSessionCookieHeader,
26
26
  getDashboardToken, getAuthConfig,
27
27
  } from './auth.mjs';
28
28
  import { handleChatStream, handleChat, handleChatHistory } from './chat.mjs';
29
+ import {
30
+ handleChatLoopStream, handleChatLoopPermission, handleChatLoopPending, handleChatLoopConfig,
31
+ handleChatLoopCommand, handleChatLoopHistory, handleChatLoopCancel, handleChatModels, handleChatModelSelect,
32
+ } from './chat-loop.mjs';
29
33
  import { createWebhookHandler, createSlackCommandHandler } from './webhook.mjs';
30
34
  import { onEmbedNotification } from '../embed/notifications.mjs';
35
+ import { artifactTypes, templateMetadata } from '../artifact-manifest.mjs';
36
+ import { resolveDemoPreviewPath, demoPreviewMime } from './demo-preview.mjs';
31
37
 
32
38
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
33
39
  const ROOT_DIR = join(__dirname, '..', '..');
@@ -355,6 +361,9 @@ async function buildStatus() {
355
361
  ...status,
356
362
  skills: listSkills(),
357
363
  commands: listCommands(),
364
+ artifactManifest: Object.fromEntries(
365
+ artifactTypes({ rootDir: ROOT_DIR }).map((type) => [type, templateMetadata(type, { rootDir: ROOT_DIR })]),
366
+ ),
358
367
  };
359
368
  }
360
369
 
@@ -693,6 +702,7 @@ const server = createServer(async (req, res) => {
693
702
  ensureCsrfCookie(req, res);
694
703
  }
695
704
  const rateTier = url.pathname === '/api/chat/stream' || url.pathname === '/api/chat'
705
+ || url.pathname === '/api/chat/loop/stream'
696
706
  ? 'chat'
697
707
  : (req.method !== 'GET' && req.method !== 'HEAD' ? 'write' : 'read');
698
708
  if (checkRateLimit) {
@@ -836,6 +846,141 @@ const server = createServer(async (req, res) => {
836
846
  return;
837
847
  }
838
848
 
849
+ if (url.pathname === '/api/certification/status' && req.method === 'GET') {
850
+ try {
851
+ const { buildCertificationDashboard } = await import('../certification/dashboard-api.mjs');
852
+ const capabilityId = url.searchParams.get('capabilityId');
853
+ const payload = capabilityId
854
+ ? (await import('../certification/status.mjs')).buildCertificationStatus({
855
+ rootDir: process.cwd(),
856
+ capabilityId,
857
+ })
858
+ : buildCertificationDashboard({ rootDir: process.cwd() });
859
+ res.writeHead(200, { 'Content-Type': 'application/json' });
860
+ res.end(JSON.stringify(payload));
861
+ } catch (err) {
862
+ res.writeHead(500, { 'Content-Type': 'application/json' });
863
+ res.end(JSON.stringify({ error: err.message }));
864
+ }
865
+ return;
866
+ }
867
+
868
+ if (url.pathname === '/api/oracle/pending' && req.method === 'GET') {
869
+ try {
870
+ const { listPending } = await import('../oracle/actions.mjs');
871
+ const pending = listPending(process.cwd()).filter((p) => p.status !== 'approved');
872
+ res.writeHead(200, { 'Content-Type': 'application/json' });
873
+ res.end(JSON.stringify({ pending }));
874
+ } catch (err) {
875
+ res.writeHead(500, { 'Content-Type': 'application/json' });
876
+ res.end(JSON.stringify({ error: err.message }));
877
+ }
878
+ return;
879
+ }
880
+
881
+ if (url.pathname === '/api/improvement/pending' && req.method === 'GET') {
882
+ try {
883
+ const { listPending } = await import('../improvement/surface.mjs');
884
+ const pending = listPending(process.cwd());
885
+ res.writeHead(200, { 'Content-Type': 'application/json' });
886
+ res.end(JSON.stringify({ pending }));
887
+ } catch (err) {
888
+ res.writeHead(500, { 'Content-Type': 'application/json' });
889
+ res.end(JSON.stringify({ error: err.message }));
890
+ }
891
+ return;
892
+ }
893
+
894
+ if (url.pathname.startsWith('/api/improvement/') && req.method === 'POST') {
895
+ const action = url.pathname.slice('/api/improvement/'.length);
896
+ let body = '';
897
+ req.on('data', (chunk) => { body += chunk; });
898
+ req.on('end', async () => {
899
+ try {
900
+ const data = JSON.parse(body || '{}');
901
+ const projectDir = process.cwd();
902
+ const { reviewRecord, approveRecord, applyRecord, rollbackRecord } = await import('../improvement/surface.mjs');
903
+ let result;
904
+ if (action === 'review') {
905
+ if (!data.id) throw new Error('Missing id');
906
+ result = reviewRecord(projectDir, data.id);
907
+ } else if (action === 'approve') {
908
+ if (!data.id) throw new Error('Missing id');
909
+ result = approveRecord(projectDir, data.id, { identity: data.identity });
910
+ } else if (action === 'apply') {
911
+ if (!data.id) throw new Error('Missing id');
912
+ result = applyRecord(projectDir, data.id, { monitor: data.monitor });
913
+ } else if (action === 'rollback') {
914
+ if (!data.id) throw new Error('Missing id');
915
+ result = rollbackRecord(projectDir, data.id, { reason: data.reason });
916
+ } else {
917
+ throw new Error(`Unknown improvement action: ${action}`);
918
+ }
919
+ const status = result.ok === false && result.error ? 400 : 200;
920
+ res.writeHead(status, { 'Content-Type': 'application/json' });
921
+ res.end(JSON.stringify(result));
922
+ notifyClients();
923
+ } catch (err) {
924
+ res.writeHead(400, { 'Content-Type': 'application/json' });
925
+ res.end(JSON.stringify({ error: err.message }));
926
+ }
927
+ });
928
+ return;
929
+ }
930
+
931
+ if (url.pathname.startsWith('/api/improvement/') && req.method === 'GET') {
932
+ const id = decodeURIComponent(url.pathname.slice('/api/improvement/'.length));
933
+ if (id && id !== 'pending') {
934
+ try {
935
+ const { loadRecord } = await import('../improvement/store.mjs');
936
+ const record = loadRecord(process.cwd(), id);
937
+ if (!record) {
938
+ res.writeHead(404, { 'Content-Type': 'application/json' });
939
+ res.end(JSON.stringify({ error: 'not-found' }));
940
+ return;
941
+ }
942
+ res.writeHead(200, { 'Content-Type': 'application/json' });
943
+ res.end(JSON.stringify(record));
944
+ } catch (err) {
945
+ res.writeHead(500, { 'Content-Type': 'application/json' });
946
+ res.end(JSON.stringify({ error: err.message }));
947
+ }
948
+ return;
949
+ }
950
+ }
951
+
952
+ if (url.pathname === '/api/oracle' && req.method === 'GET') {
953
+ try {
954
+ const { homedir: osHome } = await import('node:os');
955
+ const { readHeartbeat } = await import('../daemons/contract.mjs');
956
+ const { heartbeatPath, readLastTick } = await import('../oracle/index.mjs');
957
+ const { collectReadModel, enrichReadModel } = await import('../oracle/read-model.mjs');
958
+ const { synthesizeVerdict } = await import('../oracle/synthesize.mjs');
959
+ const home = osHome();
960
+ const projectDir = process.cwd();
961
+ const readModel = collectReadModel({ rootDir: ROOT_DIR, projectDir, homeDir: home });
962
+ const enriched = await enrichReadModel(readModel);
963
+ const synthesis = synthesizeVerdict(enriched);
964
+ const hb = readHeartbeat(heartbeatPath(home));
965
+ res.writeHead(200, { 'Content-Type': 'application/json' });
966
+ res.end(JSON.stringify({
967
+ running: !!hb,
968
+ heartbeat: hb,
969
+ lastTick: readLastTick(home),
970
+ verdict: synthesis.verdict,
971
+ gaps: synthesis.gaps,
972
+ recommendedActions: synthesis.recommendedActions,
973
+ orgGraph: enriched.orgGraph,
974
+ openBeads: enriched.beads?.openCount ?? null,
975
+ lastPropagationCheck: enriched.orgGraph?.propagation ?? null,
976
+ }));
977
+ } catch (err) {
978
+ res.writeHead(500, { 'Content-Type': 'application/json' });
979
+ res.end(JSON.stringify({ error: err.message }));
980
+ }
981
+ return;
982
+ }
983
+
839
984
  if (url.pathname === '/api/doctor') {
840
985
  try {
841
986
  const { readState } = await import('../doctor/index.mjs');
@@ -1438,7 +1583,9 @@ const server = createServer(async (req, res) => {
1438
1583
  if (url.pathname === '/api/intake/config' && req.method === 'GET') {
1439
1584
  try {
1440
1585
  const { loadIntakeConfig, INTAKE_DEPTH_GUIDANCE, INTAKE_HARD_MAX_DEPTH } = await import('../intake/intake-config.mjs');
1586
+ const { getRebrand } = await import('../profiles/rebrand.mjs');
1441
1587
  const config = loadIntakeConfig(ROOT_DIR, process.env);
1588
+ const rebrand = getRebrand(ROOT_DIR);
1442
1589
  res.writeHead(200, { 'Content-Type': 'application/json' });
1443
1590
  res.end(JSON.stringify({
1444
1591
  config,
@@ -1446,6 +1593,8 @@ const server = createServer(async (req, res) => {
1446
1593
  guidance: INTAKE_DEPTH_GUIDANCE,
1447
1594
  hardMaxDepth: INTAKE_HARD_MAX_DEPTH,
1448
1595
  defaults: { projectInbox: '.cx/inbox', docsIntake: 'docs/intake' },
1596
+ label: rebrand.intakeQueueLabel,
1597
+ itemNoun: rebrand.signalNoun,
1449
1598
  }));
1450
1599
  } catch (err) {
1451
1600
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1457,10 +1606,17 @@ const server = createServer(async (req, res) => {
1457
1606
  if (url.pathname === '/api/intake/list' && req.method === 'GET') {
1458
1607
  try {
1459
1608
  const { createIntakeQueue } = await import('../intake/queue.mjs');
1609
+ const { getRebrand } = await import('../profiles/rebrand.mjs');
1460
1610
  const queue = createIntakeQueue(ROOT_DIR, process.env);
1461
1611
  const items = queue.listPending();
1612
+ const rebrand = getRebrand(ROOT_DIR);
1462
1613
  res.writeHead(200, { 'Content-Type': 'application/json' });
1463
- res.end(JSON.stringify({ items, total: items.length }));
1614
+ res.end(JSON.stringify({
1615
+ items,
1616
+ total: items.length,
1617
+ label: rebrand.intakeQueueLabel,
1618
+ itemNoun: rebrand.signalNoun,
1619
+ }));
1464
1620
  } catch (err) {
1465
1621
  res.writeHead(500, { 'Content-Type': 'application/json' });
1466
1622
  res.end(JSON.stringify({ error: err.message }));
@@ -1934,6 +2090,51 @@ const server = createServer(async (req, res) => {
1934
2090
  return;
1935
2091
  }
1936
2092
 
2093
+ if (url.pathname === '/api/chat/loop/stream' && req.method === 'GET') {
2094
+ handleChatLoopStream(req, res, { rootDir: ROOT_DIR });
2095
+ return;
2096
+ }
2097
+
2098
+ if (url.pathname === '/api/chat/loop/pending' && req.method === 'GET') {
2099
+ handleChatLoopPending(req, res);
2100
+ return;
2101
+ }
2102
+
2103
+ if (url.pathname === '/api/chat/loop/permission' && req.method === 'POST') {
2104
+ handleChatLoopPermission(req, res);
2105
+ return;
2106
+ }
2107
+
2108
+ if (url.pathname === '/api/chat/config' && (req.method === 'GET' || req.method === 'POST')) {
2109
+ handleChatLoopConfig(req, res, { rootDir: ROOT_DIR });
2110
+ return;
2111
+ }
2112
+
2113
+ if (url.pathname === '/api/chat/models' && req.method === 'GET') {
2114
+ void handleChatModels(req, res, { rootDir: ROOT_DIR });
2115
+ return;
2116
+ }
2117
+
2118
+ if (url.pathname === '/api/chat/models/select' && req.method === 'POST') {
2119
+ handleChatModelSelect(req, res, { rootDir: ROOT_DIR });
2120
+ return;
2121
+ }
2122
+
2123
+ if (url.pathname === '/api/chat/loop/command' && req.method === 'POST') {
2124
+ handleChatLoopCommand(req, res, { rootDir: ROOT_DIR });
2125
+ return;
2126
+ }
2127
+
2128
+ if (url.pathname === '/api/chat/loop/history' && req.method === 'GET') {
2129
+ handleChatLoopHistory(req, res, { rootDir: ROOT_DIR });
2130
+ return;
2131
+ }
2132
+
2133
+ if (url.pathname === '/api/chat/loop/cancel' && req.method === 'POST') {
2134
+ handleChatLoopCancel(req, res);
2135
+ return;
2136
+ }
2137
+
1937
2138
  if (req.method === 'POST') {
1938
2139
  let body = '';
1939
2140
  req.on('data', chunk => { body += chunk; });
@@ -2298,9 +2499,21 @@ const server = createServer(async (req, res) => {
2298
2499
  return;
2299
2500
  }
2300
2501
 
2502
+ if (url.pathname.startsWith('/demo-preview/')) {
2503
+ const preview = resolveDemoPreviewPath(url.pathname);
2504
+ if (!preview.ok) {
2505
+ res.writeHead(preview.status);
2506
+ res.end(preview.message);
2507
+ return;
2508
+ }
2509
+ res.writeHead(200, { 'Content-Type': demoPreviewMime(preview.ext) });
2510
+ res.end(readFileSync(preview.filePath));
2511
+ return;
2512
+ }
2513
+
2301
2514
  // Static files
2302
2515
  let filePath = url.pathname === '/' ? '/index.html' : url.pathname;
2303
- const fullPath = join(STATIC_DIR, filePath);
2516
+ let fullPath = join(STATIC_DIR, filePath);
2304
2517
 
2305
2518
  if (!fullPath.startsWith(STATIC_DIR)) {
2306
2519
  res.writeHead(403);
@@ -2308,7 +2521,14 @@ const server = createServer(async (req, res) => {
2308
2521
  return;
2309
2522
  }
2310
2523
 
2311
- if (!existsSync(fullPath)) {
2524
+ if (existsSync(fullPath) && statSync(fullPath).isDirectory()) {
2525
+ fullPath = join(fullPath, 'index.html');
2526
+ } else if (!existsSync(fullPath)) {
2527
+ const indexCandidate = join(STATIC_DIR, filePath, 'index.html');
2528
+ if (existsSync(indexCandidate)) fullPath = indexCandidate;
2529
+ }
2530
+
2531
+ if (!existsSync(fullPath) || statSync(fullPath).isDirectory()) {
2312
2532
  res.writeHead(404);
2313
2533
  res.end('Not found');
2314
2534
  return;
@@ -2345,11 +2565,12 @@ function runEmbedSync() {
2345
2565
  // from process.env (telemetry credentials, model provider keys, etc.) find them
2346
2566
  // even when the server is started directly (not through bin/construct).
2347
2567
  try {
2348
- const { loadConstructEnv, getUserEnvPath } = await import('../env-config.mjs');
2568
+ const { getUserEnvPath } = await import('../env-config.mjs');
2569
+ const { prepareConstructEnv } = await import('../runtime-env.mjs');
2349
2570
  const homeDir = HOME || homedir();
2350
2571
  const envPath = getUserEnvPath(homeDir);
2351
2572
  if (existsSync(envPath)) {
2352
- const ENV = loadConstructEnv({ rootDir: ROOT_DIR, homeDir, env: process.env });
2573
+ const ENV = prepareConstructEnv({ rootDir: ROOT_DIR, homeDir, env: process.env });
2353
2574
  for (const [key, value] of Object.entries(ENV)) {
2354
2575
  if (!(key in process.env)) process.env[key] = value;
2355
2576
  }
@@ -13,10 +13,13 @@ import path from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
14
14
 
15
15
  import { findAvailablePort } from './host-capabilities.mjs';
16
+ import { wrapWithOpRun } from './providers/op-run.mjs';
16
17
  import { getUserEnvPath, loadConstructEnv, parseEnvFile, writeEnvValues } from './env-config.mjs';
17
18
  import { runPressureRelease } from './runtime-pressure.mjs';
18
19
  import { resolveTraceBackend, telemetryProviderLabel } from './telemetry/client.mjs';
19
20
  import { memoryPort as derivedMemoryPort } from './home-namespace.mjs';
21
+ import { readHeartbeat } from './daemons/contract.mjs';
22
+ import { heartbeatPath } from './oracle/index.mjs';
20
23
 
21
24
  const DASHBOARD_STATE_FILE = 'dashboard.json';
22
25
 
@@ -95,7 +98,7 @@ export function buildRuntimeRecoverySummary({ rootDir, homeDir = os.homedir(), r
95
98
  }
96
99
 
97
100
  export function isManagedConstructPostgresUrl(databaseUrl = '') {
98
- // Construct no longer manages local Postgres containers.
101
+ // Local Postgres lifecycle is out of Construct's scope, so no URL is Construct-managed.
99
102
  return false;
100
103
  }
101
104
 
@@ -249,6 +252,25 @@ export function startDoctor({ rootDir, homeDir = os.homedir() } = {}) {
249
252
  return { started: true, logPath };
250
253
  }
251
254
 
255
+ export function startOracle({ rootDir, homeDir = os.homedir(), projectDir = rootDir } = {}) {
256
+ if (process.env.CONSTRUCT_ORACLE === 'off' || process.env.CONSTRUCT_ORACLE === '0') {
257
+ return { started: false, reason: 'disabled' };
258
+ }
259
+ const entry = path.join(INSTALL_ROOT, 'lib', 'oracle', 'daemon-entry.mjs');
260
+ if (!fs.existsSync(entry)) return { started: false, reason: 'missing-binary' };
261
+ const live = readHeartbeat(heartbeatPath(homeDir));
262
+ if (live) return { started: false, reused: true, pid: live.pid };
263
+ const { logPath } = spawnDetached('node', [entry], homeDir, 'oracle-daemon.log', {
264
+ cwd: projectDir,
265
+ env: {
266
+ ...process.env,
267
+ CONSTRUCT_ORACLE_ROOT: rootDir,
268
+ CONSTRUCT_ORACLE_PROJECT: projectDir,
269
+ },
270
+ });
271
+ return { started: true, logPath };
272
+ }
273
+
252
274
  export async function startDashboard({ rootDir, homeDir = os.homedir(), preferredPort = 4242 } = {}) {
253
275
  const existing = readDashboardState(homeDir);
254
276
  if (existing && await isDashboardRunning(existing.port)) {
@@ -262,7 +284,16 @@ export async function startDashboard({ rootDir, homeDir = os.homedir(), preferre
262
284
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
263
285
  const out = fs.openSync(outPath, 'a');
264
286
  const err = fs.openSync(outPath, 'a');
265
- const child = spawn(process.execPath, [path.join(INSTALL_ROOT, 'lib', 'server', 'index.mjs')], {
287
+
288
+ // Opt-in: with CONSTRUCT_OP_ENV_FILE set, launch the daemon under `op run` so
289
+ // 1Password op:// refs resolve once at startup into its env. The biometric
290
+ // unlock prompt makes first launch slower, so widen the readiness window then.
291
+ const { command, args, wrapped } = wrapWithOpRun(
292
+ process.execPath,
293
+ [path.join(INSTALL_ROOT, 'lib', 'server', 'index.mjs')],
294
+ { env: process.env },
295
+ );
296
+ const child = spawn(command, args, {
266
297
  detached: true,
267
298
  stdio: ['ignore', out, err],
268
299
  env: { ...process.env, PORT: String(port) },
@@ -270,7 +301,8 @@ export async function startDashboard({ rootDir, homeDir = os.homedir(), preferre
270
301
  child.unref();
271
302
 
272
303
  let ready = false;
273
- for (let i = 0; i < 30; i += 1) {
304
+ const maxTries = wrapped ? 150 : 30;
305
+ for (let i = 0; i < maxTries; i += 1) {
274
306
  if (await probeRuntimePort(port)) { ready = true; break; }
275
307
  await new Promise((resolve) => setTimeout(resolve, 200));
276
308
  }
@@ -278,7 +310,8 @@ export async function startDashboard({ rootDir, homeDir = os.homedir(), preferre
278
310
  if (!ready) {
279
311
  let logTail = '';
280
312
  try { logTail = fs.readFileSync(outPath, 'utf8').split('\n').slice(-12).join('\n'); } catch { /* log unreadable */ }
281
- return { started: false, failed: true, port, logPath: outPath, error: `dashboard did not bind 127.0.0.1:${port} within 6s`, logTail };
313
+ const window = wrapped ? '30s (op run awaits 1Password unlock)' : '6s';
314
+ return { started: false, failed: true, port, logPath: outPath, error: `dashboard did not bind 127.0.0.1:${port} within ${window}`, logTail };
282
315
  }
283
316
 
284
317
  const state = {
@@ -342,6 +375,16 @@ export const SELECTABLE_SERVICES = Object.freeze([
342
375
  { key: 'copilot-bridge', label: 'Copilot Bridge', description: 'Host-native Copilot bridge proxy (requires gh auth).' },
343
376
  ]);
344
377
 
378
+ // Construct does not impose an Ollama keep-alive: an operator value is honored when
379
+ // present, otherwise the variable is left unset so the Ollama server applies its own
380
+ // default and idle models unload on their own. A forced value would keep every
381
+ // requested model resident in unified memory for the whole window.
382
+
383
+ export function resolveOllamaKeepAlive(env = {}) {
384
+ const operatorValue = typeof env?.OLLAMA_KEEP_ALIVE === 'string' ? env.OLLAMA_KEEP_ALIVE.trim() : '';
385
+ return operatorValue || null;
386
+ }
387
+
345
388
  export async function startServices({
346
389
  rootDir,
347
390
  homeDir = os.homedir(),
@@ -370,10 +413,10 @@ export async function startServices({
370
413
 
371
414
  const liveEnv = loadConstructEnvFn({ rootDir, homeDir });
372
415
  const results = [];
373
-
374
- // Set Ollama persistence for local inference performance
375
- liveEnv.OLLAMA_KEEP_ALIVE = "-1";
376
-
416
+
417
+ const operatorKeepAlive = resolveOllamaKeepAlive(liveEnv);
418
+ if (operatorKeepAlive) liveEnv.OLLAMA_KEEP_ALIVE = operatorKeepAlive;
419
+
377
420
  const pressureReport = runPressureReleaseFn({ env: { ...liveEnv, HOME: homeDir } });
378
421
  if (pressureReport?.killed?.length) {
379
422
  results.push({
@@ -481,6 +524,15 @@ export async function startServices({
481
524
  results.push({ name: 'Doctor', status: 'unavailable', note: doctor.reason });
482
525
  }
483
526
 
527
+ const oracle = startOracle({ rootDir, homeDir, projectDir: rootDir });
528
+ if (oracle.started) {
529
+ results.push({ name: 'Oracle', status: 'started', note: `L0.5 overseer · logs: ${oracle.logPath}` });
530
+ } else if (oracle.reused) {
531
+ results.push({ name: 'Oracle', status: 'reused', note: `pid ${oracle.pid}` });
532
+ } else if (oracle.reason !== 'disabled') {
533
+ results.push({ name: 'Oracle', status: 'unavailable', note: oracle.reason });
534
+ }
535
+
484
536
  emitServiceFailures(results);
485
537
 
486
538
  return {
package/lib/setup.mjs CHANGED
@@ -32,6 +32,9 @@ import { getCanonicalOpenCodeConfigPath, readOpenCodeConfig, writeOpenCodeConfig
32
32
  import { getEmbeddingModelInfo, warmupEmbeddingModel } from './storage/embeddings-engine.mjs';
33
33
  import { buildPressureGuardValues, installPressureGuardLaunchAgent, loadPressureGuardLaunchAgent } from './runtime-pressure.mjs';
34
34
  import { consentToInstall } from './setup-prompts.mjs';
35
+ import { cleanupLegacyGlobalConstruct } from './install/legacy-global-cleanup.mjs';
36
+ import { downloadDesktopBinary } from './install/desktop-binary-download.mjs';
37
+ import { buildDesktopBinary, hasDesktopSource } from './chat/desktop-build.mjs';
35
38
 
36
39
  const ROOT_DIR = path.resolve(import.meta.dirname, '..');
37
40
  const HOME = os.homedir();
@@ -40,7 +43,7 @@ function printHelp() {
40
43
  console.log(`Construct install — machine setup (once per machine)
41
44
 
42
45
  Usage:
43
- construct install [--scope=project|user|both] [--yes] [--dry-run] [--no-launch-agent] [--reconfigure] [--with-docling]
46
+ construct install [--scope=project|user|both] [--yes] [--dry-run] [--no-launch-agent] [--reconfigure] [--with-docling] [--cleanup-legacy-global]
44
47
 
45
48
  Flags:
46
49
  --scope=<s> project | user | both (default: project — see ADR-0029)
@@ -56,6 +59,9 @@ Flags:
56
59
  --with-docling eagerly provision the docling document-extraction venv now
57
60
  (heavy, ~10 min via uv; otherwise provisioned lazily on
58
61
  first document ingest)
62
+ --cleanup-legacy-global
63
+ remove legacy user-global Construct adapters/MCP/history
64
+ before setup; does not uninstall a global construct CLI
59
65
 
60
66
  What --scope=user does:
61
67
  - creates ~/.construct/config.env
@@ -307,14 +313,35 @@ function printProjectScopeGuidance() {
307
313
  console.log(' construct install --scope=both');
308
314
  }
309
315
 
316
+ function runLegacyGlobalCleanup({ homeDir, dryRun }) {
317
+ const result = cleanupLegacyGlobalConstruct({ homeDir, dryRun });
318
+ const action = dryRun ? 'would remove' : 'removed';
319
+ console.log(`Legacy global cleanup: ${result.removed.length ? `${action} ${result.removed.length} item(s)` : 'nothing detected'}`);
320
+ for (const item of result.removed.slice(0, 20)) {
321
+ console.log(` - ${item}`);
322
+ }
323
+ if (result.removed.length > 20) {
324
+ console.log(` - ...and ${result.removed.length - 20} more`);
325
+ }
326
+ return result;
327
+ }
328
+
310
329
  // Dry-run renders the user-scope plan as intent only — every line names a write,
311
330
  // network call, or service registration that the real run would perform — and
312
331
  // returns before touching disk so the preview is provably side-effect-free.
313
332
 
314
- function printInstallPlan({ scope, homeDir, withDocling, noLaunchAgent }) {
333
+ function printInstallPlan({ scope, homeDir, withDocling, noLaunchAgent, cleanupLegacyGlobal }) {
315
334
  console.log('Construct install — dry-run (no changes written)');
316
335
  console.log('────────────────────────────────────────────────');
317
336
  console.log(`Scope: ${scope}`);
337
+ if (cleanupLegacyGlobal) {
338
+ console.log('\nLegacy global cleanup:');
339
+ runLegacyGlobalCleanup({ homeDir, dryRun: true });
340
+ }
341
+ if (scope === 'project') {
342
+ console.log('\nWould not run user-scope setup. Re-run without --dry-run to apply cleanup only.');
343
+ return;
344
+ }
318
345
  if (scope === 'both') {
319
346
  console.log(' · would print project-scope guidance, then run the user-scope plan below');
320
347
  }
@@ -326,6 +353,7 @@ function printInstallPlan({ scope, homeDir, withDocling, noLaunchAgent }) {
326
353
  console.log(` · ${path.dirname(defaultVectorIndexPath(homeDir))} (LanceDB vector store dir)`);
327
354
  console.log('\nWould check / install runtime tooling:');
328
355
  console.log(' · cm (memory CLI) and cass (session search) when available on PATH');
356
+ console.log(' · construct-chat desktop binary (from GitHub releases → ~/.construct/bin/)');
329
357
  console.log('\nWould reach the network / external services:');
330
358
  console.log(' · embedding model warmup (loads from local cache; offline-safe, no download)');
331
359
  if (withDocling) console.log(' · docling Python venv provisioning via uv (~10 min)');
@@ -341,13 +369,14 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
341
369
  const argSet = new Set(args);
342
370
  const isYes = argSet.has('--yes');
343
371
  const reconfigure = argSet.has('--reconfigure');
372
+ const cleanupLegacyGlobal = argSet.has('--cleanup-legacy-global');
344
373
 
345
374
  if (argSet.has('--help') || argSet.has('-h')) {
346
375
  printHelp();
347
376
  return;
348
377
  }
349
378
 
350
- const KNOWN_FLAGS = new Set(['--yes', '--dry-run', '--no-launch-agent', '--reconfigure', '--with-docling', '--help', '-h']);
379
+ const KNOWN_FLAGS = new Set(['--yes', '--dry-run', '--no-launch-agent', '--reconfigure', '--with-docling', '--cleanup-legacy-global', '--help', '-h']);
351
380
  const dryRun = argSet.has('--dry-run');
352
381
  const withDocling = argSet.has('--with-docling');
353
382
  const unknownFlags = args.filter((a) => {
@@ -369,13 +398,25 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
369
398
  }
370
399
  const scope = scopeResult.scope;
371
400
 
372
- if (scope === 'project') {
373
- printProjectScopeGuidance();
401
+ if (dryRun) {
402
+ if (scope === 'project' && !cleanupLegacyGlobal) {
403
+ printProjectScopeGuidance();
404
+ return;
405
+ }
406
+ printInstallPlan({ scope, homeDir, withDocling, noLaunchAgent: argSet.has('--no-launch-agent'), cleanupLegacyGlobal });
374
407
  return;
375
408
  }
376
409
 
377
- if (dryRun) {
378
- printInstallPlan({ scope, homeDir, withDocling, noLaunchAgent: argSet.has('--no-launch-agent') });
410
+ if (cleanupLegacyGlobal) {
411
+ runLegacyGlobalCleanup({ homeDir, dryRun: false });
412
+ if (scope === 'project') {
413
+ printProjectScopeGuidance();
414
+ return;
415
+ }
416
+ }
417
+
418
+ if (scope === 'project') {
419
+ printProjectScopeGuidance();
379
420
  return;
380
421
  }
381
422
 
@@ -415,6 +456,28 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
415
456
  if (cassInstall.installCommand) console.log(` Install with: ${cassInstall.installCommand}`);
416
457
  }
417
458
 
459
+ try {
460
+ if (hasDesktopSource(ROOT_DIR)) {
461
+ const built = buildDesktopBinary({ repoRoot: ROOT_DIR, env: process.env });
462
+ if (built.ok) {
463
+ console.log(`Desktop binary: built from source → ${built.binaryPath}`);
464
+ } else {
465
+ console.log(`Desktop binary: build skipped (${built.reason}) — runs on first \`construct chat\``);
466
+ }
467
+ } else {
468
+ const desktop = await downloadDesktopBinary({ homeDir, env: process.env });
469
+ if (desktop.status === 'downloaded') {
470
+ console.log(`Desktop binary: ${desktop.version} → ${desktop.dest}`);
471
+ } else if (desktop.status === 'failed') {
472
+ console.log(`Desktop binary: download failed (${desktop.reason}) — run \`construct install --scope=user\` again to retry`);
473
+ } else {
474
+ console.log(`Desktop binary: ${desktop.reason}`);
475
+ }
476
+ }
477
+ } catch (err) {
478
+ console.log(`Desktop binary: skipped (${err?.message || 'unknown error'})`);
479
+ }
480
+
418
481
  const telemetryResult = process.env.CONSTRUCT_TELEMETRY_URL
419
482
  ? { status: 'configured', note: `remote export configured (${process.env.CONSTRUCT_TELEMETRY_URL})` }
420
483
  : { status: 'local', note: 'local JSONL traces in .cx/traces; remote export optional' };
@@ -5,8 +5,8 @@
5
5
  * shape: YAML frontmatter (structured metadata) + markdown body (the authored
6
6
  * prose), mirroring skills/roles/*.md. This validates the frontmatter and the
7
7
  * canonical-section contract, and gates the highest-value invariant — the
8
- * frontmatter `perspective{}` must equal the registry's, since the two
9
- * currently duplicate each other and drift silently (ADR-0037).
8
+ * frontmatter `perspective{}` is the sole source of truth (ADR-0037); registry.json
9
+ * carries routing metadata only.
10
10
  *
11
11
  * A file with no frontmatter is treated as not-yet-converted: reported, never an
12
12
  * error, so the linter can run across a half-migrated corpus. Frontmatter is the
@@ -55,11 +55,6 @@ function hasHeading(body, title) {
55
55
  return false;
56
56
  }
57
57
 
58
- function samePerspective(a, b) {
59
- if (!a || !b) return false;
60
- return PERSPECTIVE_FIELDS.every((f) => String(a[f] ?? '').trim() === String(b[f] ?? '').trim());
61
- }
62
-
63
58
  /**
64
59
  * Validate one prompt file's content against the hybrid format.
65
60
  *
@@ -103,9 +98,6 @@ export function validatePromptContent({ content, id = '(prompt)', registryEntry
103
98
  for (const f of PERSPECTIVE_FIELDS) {
104
99
  if (!persp[f] || !String(persp[f]).trim()) errors.push(`${id}: perspective.${f} is required and must be non-empty`);
105
100
  }
106
- if (registryEntry?.perspective && !samePerspective(persp, registryEntry.perspective)) {
107
- errors.push(`${id}: perspective{} drifts from registry.json — they must match while both exist (frontmatter is the source of truth; update the registry)`);
108
- }
109
101
  } else if (frontmatter.perspective != null) {
110
102
  errors.push(`${id}: perspective must be an object with ${PERSPECTIVE_FIELDS.join(', ')}`);
111
103
  }
@@ -160,3 +152,20 @@ function loadRegistry(rootDir) {
160
152
  return { specialists: [] };
161
153
  }
162
154
  }
155
+
156
+ /**
157
+ * Resolve a specialist's perspective from its prompt frontmatter.
158
+ * @param {string} agentName - registry name or cx-prefixed id
159
+ * @returns {object|null}
160
+ */
161
+ export function resolvePerspectiveFromPrompt(agentName, { rootDir = process.cwd(), registry } = {}) {
162
+ const reg = registry ?? loadRegistry(rootDir);
163
+ const normalized = String(agentName ?? '').trim().replace(/^cx-/, '');
164
+ const entry = (reg.specialists ?? []).find((s) => s.name === normalized);
165
+ if (!entry?.promptFile) return null;
166
+ const filePath = path.join(rootDir, entry.promptFile);
167
+ if (!fs.existsSync(filePath)) return null;
168
+ const { frontmatter } = splitFrontmatter(fs.readFileSync(filePath, 'utf8'));
169
+ const persp = frontmatter?.perspective;
170
+ return persp && typeof persp === 'object' ? persp : null;
171
+ }