@geraldmaron/construct 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (426) hide show
  1. package/README.md +30 -19
  2. package/bin/construct +487 -87
  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 +258 -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/artifact-workflow.mjs +252 -0
  20. package/lib/audit-skills.mjs +4 -0
  21. package/lib/audit-specialists.mjs +285 -0
  22. package/lib/audit-trail.mjs +1 -1
  23. package/lib/auto-docs.mjs +90 -19
  24. package/lib/beads/drift.mjs +27 -6
  25. package/lib/beads-client.mjs +49 -121
  26. package/lib/beads-optimistic.mjs +5 -12
  27. package/lib/brand-fonts.mjs +93 -0
  28. package/lib/brand-prose.mjs +214 -0
  29. package/lib/brand-tokens.mjs +92 -0
  30. package/lib/bridges/copilot-proxy.mjs +13 -26
  31. package/lib/capability-ledger.mjs +156 -0
  32. package/lib/certification/artifact-fixtures.mjs +132 -0
  33. package/lib/certification/artifact-gates.mjs +63 -0
  34. package/lib/certification/artifact-provenance.mjs +97 -0
  35. package/lib/certification/canonical-scenarios.mjs +78 -0
  36. package/lib/certification/cli.mjs +191 -0
  37. package/lib/certification/dashboard-api.mjs +71 -0
  38. package/lib/certification/demo-parity.mjs +116 -0
  39. package/lib/certification/document-io-fixtures.mjs +246 -0
  40. package/lib/certification/document-workflow.mjs +97 -0
  41. package/lib/certification/eval-bridge.mjs +77 -0
  42. package/lib/certification/model-routing.mjs +149 -0
  43. package/lib/certification/prompt-budget.mjs +119 -0
  44. package/lib/certification/rc-gate.mjs +206 -0
  45. package/lib/certification/real-llm-scenarios.mjs +303 -0
  46. package/lib/certification/role-cards.mjs +113 -0
  47. package/lib/certification/role-overlays.mjs +117 -0
  48. package/lib/certification/run.mjs +122 -0
  49. package/lib/certification/runner.mjs +323 -0
  50. package/lib/certification/scenarios.mjs +51 -0
  51. package/lib/certification/skill-inventory.mjs +289 -0
  52. package/lib/certification/skill-scenarios.mjs +147 -0
  53. package/lib/certification/specialist-contracts.mjs +85 -0
  54. package/lib/certification/specialist-scenarios.mjs +175 -0
  55. package/lib/certification/stale-impact.mjs +146 -0
  56. package/lib/certification/status.mjs +252 -0
  57. package/lib/certification/store.mjs +77 -0
  58. package/lib/chat/cli.mjs +333 -0
  59. package/lib/chat/command-suggest.mjs +161 -0
  60. package/lib/chat/commands.mjs +215 -0
  61. package/lib/chat/config.mjs +142 -0
  62. package/lib/chat/context-compactor.mjs +250 -0
  63. package/lib/chat/context-continuation.mjs +253 -0
  64. package/lib/chat/continuation-source.mjs +58 -0
  65. package/lib/chat/demo-guide.mjs +61 -0
  66. package/lib/chat/design-tokens.mjs +91 -0
  67. package/lib/chat/desktop-binary.mjs +79 -0
  68. package/lib/chat/desktop-build.mjs +130 -0
  69. package/lib/chat/desktop-launcher.mjs +133 -0
  70. package/lib/chat/evidence.mjs +145 -0
  71. package/lib/chat/export.mjs +74 -0
  72. package/lib/chat/harness/driver.mjs +91 -0
  73. package/lib/chat/list-picker.mjs +112 -0
  74. package/lib/chat/model-picker.mjs +356 -0
  75. package/lib/chat/openrouter-fallback.mjs +151 -0
  76. package/lib/chat/permission-prompt.mjs +33 -0
  77. package/lib/chat/picker-catalog.mjs +45 -0
  78. package/lib/chat/policy-telemetry.mjs +34 -0
  79. package/lib/chat/present.mjs +246 -0
  80. package/lib/chat/session-context.mjs +39 -0
  81. package/lib/chat/session-persist.mjs +73 -0
  82. package/lib/chat/session-restore.mjs +71 -0
  83. package/lib/chat/session-settings.mjs +53 -0
  84. package/lib/chat/system-prompt.mjs +52 -0
  85. package/lib/chat/transparency.mjs +93 -0
  86. package/lib/chat/tui/color-scheme.mjs +42 -0
  87. package/lib/chat/tui/markdown.mjs +123 -0
  88. package/lib/chat/tui/presentation.mjs +100 -0
  89. package/lib/chat/tui/render.mjs +500 -0
  90. package/lib/chat/tui/turn-block.mjs +284 -0
  91. package/lib/chat/tui/turn-present.mjs +18 -0
  92. package/lib/chat/tui/turn-state.mjs +88 -0
  93. package/lib/chat/tui/usage.mjs +122 -0
  94. package/lib/chat/web-commands.mjs +146 -0
  95. package/lib/chat/web-launcher.mjs +63 -0
  96. package/lib/chat/web-picker-keys.mjs +46 -0
  97. package/lib/chat/web-session.mjs +159 -0
  98. package/lib/cli-commands.mjs +146 -15
  99. package/lib/cli-service-inventory.mjs +42 -0
  100. package/lib/comment-lint.mjs +6 -4
  101. package/lib/config/intake-policy.mjs +209 -0
  102. package/lib/config/project-config.mjs +11 -2
  103. package/lib/config/schema.mjs +86 -6
  104. package/lib/config/source-targets.mjs +311 -0
  105. package/lib/contract-schemas/decision.json +50 -0
  106. package/lib/contract-schemas/implementation.json +51 -0
  107. package/lib/contract-schemas/review-report.json +32 -0
  108. package/lib/contract-schemas/test-report.json +43 -0
  109. package/lib/contracts/construct-handoff.mjs +60 -0
  110. package/lib/contracts/validate.mjs +32 -3
  111. package/lib/contracts/violation-log.mjs +58 -5
  112. package/lib/dashboard-demo.mjs +71 -0
  113. package/lib/dashboard-static.mjs +19 -5
  114. package/lib/decisions/registry.mjs +5 -3
  115. package/lib/deck-export-pptx.mjs +1152 -0
  116. package/lib/demo-recording.mjs +142 -0
  117. package/lib/demo-script.mjs +114 -0
  118. package/lib/demo-surface.mjs +249 -0
  119. package/lib/demo.mjs +598 -140
  120. package/lib/diagram-export.mjs +192 -0
  121. package/lib/diagram.mjs +13 -11
  122. package/lib/docs-verify.mjs +25 -7
  123. package/lib/doctor/index.mjs +3 -1
  124. package/lib/doctor/project-adapters.mjs +44 -0
  125. package/lib/doctor/source-checkout.mjs +16 -0
  126. package/lib/doctor/watchers/cx-budget.mjs +98 -0
  127. package/lib/doctor/watchers/graph-staleness.mjs +52 -0
  128. package/lib/doctor/watchers/process-pressure.mjs +4 -3
  129. package/lib/document-export.mjs +384 -38
  130. package/lib/document-extract.mjs +44 -4
  131. package/lib/document-ingest.mjs +29 -20
  132. package/lib/embed/auto-sources.mjs +44 -0
  133. package/lib/embed/cli.mjs +4 -3
  134. package/lib/embed/daemon.mjs +18 -46
  135. package/lib/embed/demand-fetch.mjs +50 -19
  136. package/lib/embed/inbox.mjs +6 -10
  137. package/lib/embed/worker.mjs +4 -3
  138. package/lib/embedded-contract/index.mjs +11 -0
  139. package/lib/embedded-contract/model-resolve.mjs +10 -9
  140. package/lib/env-config.mjs +26 -0
  141. package/lib/evals/dataset.mjs +137 -0
  142. package/lib/evals/gates.mjs +175 -0
  143. package/lib/export-branding.mjs +32 -0
  144. package/lib/graph/build-co-change.mjs +54 -0
  145. package/lib/graph/build-from-registry.mjs +136 -0
  146. package/lib/graph/build-import-graph.mjs +153 -0
  147. package/lib/graph/cli.mjs +110 -0
  148. package/lib/graph/impact-cli.mjs +89 -0
  149. package/lib/graph/impact.mjs +108 -0
  150. package/lib/graph/staleness.mjs +44 -0
  151. package/lib/graph/store.mjs +172 -0
  152. package/lib/health-check.mjs +90 -76
  153. package/lib/hooks/artifact-release-gate.mjs +43 -0
  154. package/lib/hooks/brand-prose-lint.mjs +38 -0
  155. package/lib/hooks/graph-impact-advisory.mjs +62 -0
  156. package/lib/hooks/session-optimize.mjs +84 -207
  157. package/lib/hooks/session-start.mjs +18 -16
  158. package/lib/host-disposition.mjs +7 -0
  159. package/lib/improvement/cli.mjs +189 -0
  160. package/lib/improvement/controller.mjs +137 -0
  161. package/lib/improvement/proposal.mjs +120 -0
  162. package/lib/improvement/specialist-loop.mjs +192 -0
  163. package/lib/improvement/store.mjs +89 -0
  164. package/lib/improvement/surface.mjs +219 -0
  165. package/lib/ingest-tooling.mjs +97 -0
  166. package/lib/init/doc-lanes.mjs +165 -0
  167. package/lib/init-docs.mjs +31 -178
  168. package/lib/init-unified.mjs +50 -119
  169. package/lib/init-update-guide.mjs +102 -0
  170. package/lib/init-update.mjs +32 -1
  171. package/lib/init.mjs +8 -0
  172. package/lib/install/desktop-binary-download.mjs +85 -0
  173. package/lib/install/legacy-global-cleanup.mjs +189 -0
  174. package/lib/intake/constants.mjs +29 -0
  175. package/lib/intake/daemon.mjs +2 -0
  176. package/lib/intake/git-queue.mjs +9 -8
  177. package/lib/intake/intake-config.mjs +52 -105
  178. package/lib/intake/legacy-paths.mjs +5 -0
  179. package/lib/intake/queue.mjs +2 -1
  180. package/lib/intake/session-prelude.mjs +90 -1
  181. package/lib/libreoffice-export.mjs +97 -0
  182. package/lib/logging/rotate.mjs +9 -0
  183. package/lib/maintenance/docker-reclaim.mjs +206 -0
  184. package/lib/mcp/external-schema-cost.mjs +74 -0
  185. package/lib/mcp/server.mjs +103 -10
  186. package/lib/mcp/stdio-mcp-probe.mjs +188 -0
  187. package/lib/mcp/tool-budget.mjs +55 -4
  188. package/lib/mcp/tools/document.mjs +18 -4
  189. package/lib/mcp/tools/embedded-contract.mjs +14 -0
  190. package/lib/mcp/tools/skills.mjs +32 -3
  191. package/lib/mcp/tools/workflow.mjs +13 -1
  192. package/lib/model-free-selector.mjs +18 -0
  193. package/lib/model-registry.mjs +13 -229
  194. package/lib/model-router.mjs +373 -92
  195. package/lib/models/behavior-matrix.mjs +289 -0
  196. package/lib/models/catalog.mjs +209 -0
  197. package/lib/models/execution-capability-profile.mjs +196 -0
  198. package/lib/models/execution-policy.mjs +307 -0
  199. package/lib/models/provider-poll.mjs +383 -0
  200. package/lib/npm-spawn-env.mjs +17 -0
  201. package/lib/ollama/installed-models.mjs +129 -0
  202. package/lib/oracle/actions.mjs +187 -0
  203. package/lib/oracle/artifact-gate.mjs +99 -0
  204. package/lib/oracle/cli.mjs +204 -0
  205. package/lib/oracle/daemon-entry.mjs +14 -0
  206. package/lib/oracle/dispatch.mjs +81 -0
  207. package/lib/oracle/execute.mjs +143 -0
  208. package/lib/oracle/gaps.mjs +76 -0
  209. package/lib/oracle/index.mjs +84 -0
  210. package/lib/oracle/issues.mjs +164 -0
  211. package/lib/oracle/org-graph.mjs +170 -0
  212. package/lib/oracle/policy.mjs +80 -0
  213. package/lib/oracle/read-model.mjs +398 -0
  214. package/lib/oracle/reconcile.mjs +191 -0
  215. package/lib/oracle/routing.mjs +89 -0
  216. package/lib/oracle/synthesize.mjs +384 -0
  217. package/lib/oracle/verdicts.mjs +51 -0
  218. package/lib/orchestration/worker.mjs +88 -27
  219. package/lib/orchestration-policy.mjs +50 -0
  220. package/lib/parity.mjs +96 -2
  221. package/lib/playwright-demo.mjs +254 -0
  222. package/lib/project-init-shared.mjs +4 -1
  223. package/lib/prompt-composer.js +7 -3
  224. package/lib/prompt-validation-contract.mjs +24 -0
  225. package/lib/provider-capabilities.js +57 -11
  226. package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
  227. package/lib/providers/contract/adapters/git/index.mjs +115 -0
  228. package/lib/providers/contract/adapters/github/index.mjs +166 -0
  229. package/lib/providers/contract/adapters/jira/index.mjs +187 -0
  230. package/lib/providers/contract/adapters/slack/index.mjs +175 -0
  231. package/lib/providers/contract/contract-tests.mjs +57 -0
  232. package/lib/providers/contract/errors.mjs +48 -0
  233. package/lib/providers/contract/interface.mjs +50 -0
  234. package/lib/providers/contract/registry.mjs +102 -0
  235. package/lib/providers/copilot-auth.mjs +297 -0
  236. package/lib/providers/credential-bootstrap.mjs +178 -0
  237. package/lib/providers/credential-catalog.mjs +46 -0
  238. package/lib/providers/credential-sources.mjs +63 -0
  239. package/lib/providers/creds.mjs +5 -2
  240. package/lib/providers/op-run.mjs +59 -0
  241. package/lib/providers/secret-resolver.mjs +159 -0
  242. package/lib/publish-template.mjs +163 -0
  243. package/lib/publish-tooling.mjs +119 -0
  244. package/lib/publish.mjs +305 -0
  245. package/lib/registry/cli.mjs +82 -0
  246. package/lib/registry/consolidation.mjs +147 -0
  247. package/lib/registry/generate-docs.mjs +75 -0
  248. package/lib/registry/skill-verification.mjs +57 -0
  249. package/lib/registry/surface-map.mjs +76 -0
  250. package/lib/registry/validate.mjs +135 -0
  251. package/lib/resources/budget.mjs +82 -0
  252. package/lib/resources/process-budget.mjs +45 -0
  253. package/lib/rules-delivery.mjs +9 -2
  254. package/lib/rules-read.mjs +26 -0
  255. package/lib/runtime-env.mjs +23 -0
  256. package/lib/runtime-pressure.mjs +51 -7
  257. package/lib/schema-infer.mjs +13 -2
  258. package/lib/server/chat-loop.mjs +622 -0
  259. package/lib/server/demo-preview.mjs +63 -0
  260. package/lib/server/index.mjs +259 -9
  261. package/lib/server/rate-limit.mjs +5 -3
  262. package/lib/server/webhook.mjs +3 -2
  263. package/lib/service-manager.mjs +60 -8
  264. package/lib/setup.mjs +70 -7
  265. package/lib/specialists/prompt-schema.mjs +19 -10
  266. package/lib/specialists/roster.mjs +43 -0
  267. package/lib/specialists/scaffold.mjs +56 -0
  268. package/lib/storage/backend.mjs +6 -6
  269. package/lib/storage/hybrid-query.mjs +7 -4
  270. package/lib/storage/state-source.mjs +5 -5
  271. package/lib/storage/sync.mjs +2 -3
  272. package/lib/telemetry/rule-calls.mjs +52 -0
  273. package/lib/template-registry.mjs +2 -2
  274. package/lib/templates/visual-requirements.mjs +27 -51
  275. package/lib/test-corpus-inventory.mjs +313 -0
  276. package/lib/uninstall/uninstall.mjs +19 -7
  277. package/lib/update.mjs +12 -0
  278. package/lib/upgrade.mjs +14 -0
  279. package/lib/wireframe.mjs +20 -14
  280. package/lib/worker/run.mjs +17 -4
  281. package/lib/worker/trace.mjs +11 -3
  282. package/package.json +28 -14
  283. package/personas/construct.md +2 -2
  284. package/platforms/claude/settings.template.json +36 -0
  285. package/rules/common/patterns.md +1 -1
  286. package/rules/common/release-gates.md +4 -3
  287. package/scripts/sync-specialists.mjs +41 -28
  288. package/skills/devops/data-engineering.md +1 -1
  289. package/skills/docs/adr-workflow.md +1 -0
  290. package/skills/docs/backlog-proposal-workflow.md +1 -0
  291. package/skills/docs/codebase-research-workflow.md +40 -0
  292. package/skills/docs/customer-profile-workflow.md +1 -0
  293. package/skills/docs/document-ingest-workflow.md +1 -0
  294. package/skills/docs/evidence-ingest-workflow.md +1 -0
  295. package/skills/docs/init-docs.md +2 -2
  296. package/skills/docs/init-project.md +7 -2
  297. package/skills/docs/prd-workflow.md +24 -1
  298. package/skills/docs/prfaq-workflow.md +1 -0
  299. package/skills/docs/product-intelligence-workflow.md +1 -0
  300. package/skills/docs/product-signal-workflow.md +1 -0
  301. package/skills/docs/research-workflow.md +54 -37
  302. package/skills/docs/runbook-workflow.md +1 -0
  303. package/skills/docs/strategy-workflow.md +1 -0
  304. package/skills/docs/user-research-workflow.md +40 -0
  305. package/skills/operating/orchestration-reference.md +1 -1
  306. package/skills/roles/architect.md +5 -0
  307. package/skills/roles/operator.docs.md +4 -0
  308. package/skills/routing.md +4 -2
  309. package/specialists/artifact-manifest.json +489 -0
  310. package/specialists/artifact-manifest.schema.json +83 -0
  311. package/specialists/audit-enrichments.json +454 -0
  312. package/specialists/contracts.json +37 -25
  313. package/specialists/prompts/_shared/validation-contract.md +26 -0
  314. package/specialists/prompts/cx-accessibility.md +21 -2
  315. package/specialists/prompts/cx-ai-engineer.md +24 -1
  316. package/specialists/prompts/cx-architect.md +4 -1
  317. package/specialists/prompts/cx-business-strategist.md +23 -2
  318. package/specialists/prompts/cx-data-analyst.md +22 -1
  319. package/specialists/prompts/cx-data-engineer.md +23 -2
  320. package/specialists/prompts/cx-debugger.md +21 -2
  321. package/specialists/prompts/cx-designer.md +26 -3
  322. package/specialists/prompts/cx-devil-advocate.md +19 -2
  323. package/specialists/prompts/cx-docs-keeper.md +28 -1
  324. package/specialists/prompts/cx-engineer.md +22 -1
  325. package/specialists/prompts/cx-evaluator.md +18 -1
  326. package/specialists/prompts/cx-explorer.md +21 -2
  327. package/specialists/prompts/cx-legal-compliance.md +21 -2
  328. package/specialists/prompts/cx-operations.md +21 -2
  329. package/specialists/prompts/cx-oracle.md +94 -0
  330. package/specialists/prompts/cx-orchestrator.md +20 -1
  331. package/specialists/prompts/cx-platform-engineer.md +25 -2
  332. package/specialists/prompts/cx-product-manager.md +32 -1
  333. package/specialists/prompts/cx-qa.md +24 -2
  334. package/specialists/prompts/cx-rd-lead.md +23 -2
  335. package/specialists/prompts/cx-release-manager.md +23 -2
  336. package/specialists/prompts/cx-researcher.md +22 -2
  337. package/specialists/prompts/cx-reviewer.md +18 -1
  338. package/specialists/prompts/cx-security.md +22 -2
  339. package/specialists/prompts/cx-sre.md +30 -3
  340. package/specialists/prompts/cx-test-automation.md +7 -1
  341. package/specialists/prompts/cx-trace-reviewer.md +22 -3
  342. package/specialists/prompts/cx-ux-researcher.md +21 -2
  343. package/specialists/registry.json +52 -173
  344. package/specialists/tone-profiles.json +42 -0
  345. package/templates/demos/playwright/demo-recording.config.mjs +47 -0
  346. package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
  347. package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
  348. package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
  349. package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
  350. package/templates/demos/tapes/resource-guard-rails.tape +49 -0
  351. package/templates/demos/vhs/construct-cockpit.json +24 -0
  352. package/templates/distribution/construct-brand.typ +446 -0
  353. package/templates/distribution/construct-decision.typ +38 -0
  354. package/templates/distribution/construct-deck.html +95 -0
  355. package/templates/distribution/construct-pdf.typ +38 -0
  356. package/templates/distribution/construct-prd.typ +38 -0
  357. package/templates/distribution/construct-reference.docx +0 -0
  358. package/templates/distribution/construct-research.typ +38 -0
  359. package/templates/distribution/construct-web.html +92 -0
  360. package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
  361. package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
  362. package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
  363. package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
  364. package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
  365. package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
  366. package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
  367. package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
  368. package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
  369. package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
  370. package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
  371. package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
  372. package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
  373. package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
  374. package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
  375. package/templates/distribution/fonts/README.md +36 -0
  376. package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
  377. package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
  378. package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
  379. package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
  380. package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
  381. package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
  382. package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
  383. package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
  384. package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
  385. package/templates/distribution/icons/activity.svg +15 -0
  386. package/templates/distribution/icons/alert-triangle.svg +17 -0
  387. package/templates/distribution/icons/book-open.svg +16 -0
  388. package/templates/distribution/icons/bot.svg +20 -0
  389. package/templates/distribution/icons/brain.svg +22 -0
  390. package/templates/distribution/icons/circle-check.svg +16 -0
  391. package/templates/distribution/icons/clipboard-check.svg +17 -0
  392. package/templates/distribution/icons/cpu.svg +28 -0
  393. package/templates/distribution/icons/database.svg +17 -0
  394. package/templates/distribution/icons/eye.svg +16 -0
  395. package/templates/distribution/icons/file-text.svg +19 -0
  396. package/templates/distribution/icons/gauge.svg +16 -0
  397. package/templates/distribution/icons/git-branch.svg +17 -0
  398. package/templates/distribution/icons/key.svg +17 -0
  399. package/templates/distribution/icons/layers.svg +17 -0
  400. package/templates/distribution/icons/list-checks.svg +19 -0
  401. package/templates/distribution/icons/lock.svg +16 -0
  402. package/templates/distribution/icons/message-square.svg +15 -0
  403. package/templates/distribution/icons/network.svg +19 -0
  404. package/templates/distribution/icons/route.svg +17 -0
  405. package/templates/distribution/icons/scale.svg +19 -0
  406. package/templates/distribution/icons/search.svg +16 -0
  407. package/templates/distribution/icons/send.svg +16 -0
  408. package/templates/distribution/icons/server.svg +18 -0
  409. package/templates/distribution/icons/shield-check.svg +16 -0
  410. package/templates/distribution/icons/users.svg +18 -0
  411. package/templates/distribution/icons/webhook.svg +17 -0
  412. package/templates/distribution/icons/workflow.svg +17 -0
  413. package/templates/distribution/icons/wrench.svg +15 -0
  414. package/templates/distribution/run.mjs +18 -1
  415. package/templates/docs/adr.md +7 -0
  416. package/templates/docs/construct_guide.md +8 -8
  417. package/templates/docs/customer-profile.md +4 -0
  418. package/templates/docs/one-pager.md +4 -0
  419. package/templates/docs/postmortem.md +31 -0
  420. package/templates/docs/prd-platform.md +19 -0
  421. package/templates/docs/prd.md +15 -0
  422. package/templates/docs/prfaq.md +7 -0
  423. package/templates/docs/qa-strategy.md +103 -0
  424. package/templates/docs/research-brief.md +8 -0
  425. package/templates/docs/rfc-platform.md +12 -0
  426. 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,8 +702,11 @@ 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
- : (req.method !== 'GET' && req.method !== 'HEAD' ? 'write' : 'read');
707
+ : url.pathname === '/api/chat/loop/command'
708
+ ? 'command'
709
+ : (req.method !== 'GET' && req.method !== 'HEAD' ? 'write' : 'read');
698
710
  if (checkRateLimit) {
699
711
  const limit = checkRateLimit(req, rateTier, { env: process.env });
700
712
  if (!limit.allowed) {
@@ -836,6 +848,141 @@ const server = createServer(async (req, res) => {
836
848
  return;
837
849
  }
838
850
 
851
+ if (url.pathname === '/api/certification/status' && req.method === 'GET') {
852
+ try {
853
+ const { buildCertificationDashboard } = await import('../certification/dashboard-api.mjs');
854
+ const capabilityId = url.searchParams.get('capabilityId');
855
+ const payload = capabilityId
856
+ ? (await import('../certification/status.mjs')).buildCertificationStatus({
857
+ rootDir: process.cwd(),
858
+ capabilityId,
859
+ })
860
+ : buildCertificationDashboard({ rootDir: process.cwd() });
861
+ res.writeHead(200, { 'Content-Type': 'application/json' });
862
+ res.end(JSON.stringify(payload));
863
+ } catch (err) {
864
+ res.writeHead(500, { 'Content-Type': 'application/json' });
865
+ res.end(JSON.stringify({ error: err.message }));
866
+ }
867
+ return;
868
+ }
869
+
870
+ if (url.pathname === '/api/oracle/pending' && req.method === 'GET') {
871
+ try {
872
+ const { listPending } = await import('../oracle/actions.mjs');
873
+ const pending = listPending(process.cwd()).filter((p) => p.status !== 'approved');
874
+ res.writeHead(200, { 'Content-Type': 'application/json' });
875
+ res.end(JSON.stringify({ pending }));
876
+ } catch (err) {
877
+ res.writeHead(500, { 'Content-Type': 'application/json' });
878
+ res.end(JSON.stringify({ error: err.message }));
879
+ }
880
+ return;
881
+ }
882
+
883
+ if (url.pathname === '/api/improvement/pending' && req.method === 'GET') {
884
+ try {
885
+ const { listPending } = await import('../improvement/surface.mjs');
886
+ const pending = listPending(process.cwd());
887
+ res.writeHead(200, { 'Content-Type': 'application/json' });
888
+ res.end(JSON.stringify({ pending }));
889
+ } catch (err) {
890
+ res.writeHead(500, { 'Content-Type': 'application/json' });
891
+ res.end(JSON.stringify({ error: err.message }));
892
+ }
893
+ return;
894
+ }
895
+
896
+ if (url.pathname.startsWith('/api/improvement/') && req.method === 'POST') {
897
+ const action = url.pathname.slice('/api/improvement/'.length);
898
+ let body = '';
899
+ req.on('data', (chunk) => { body += chunk; });
900
+ req.on('end', async () => {
901
+ try {
902
+ const data = JSON.parse(body || '{}');
903
+ const projectDir = process.cwd();
904
+ const { reviewRecord, approveRecord, applyRecord, rollbackRecord } = await import('../improvement/surface.mjs');
905
+ let result;
906
+ if (action === 'review') {
907
+ if (!data.id) throw new Error('Missing id');
908
+ result = reviewRecord(projectDir, data.id);
909
+ } else if (action === 'approve') {
910
+ if (!data.id) throw new Error('Missing id');
911
+ result = approveRecord(projectDir, data.id, { identity: data.identity });
912
+ } else if (action === 'apply') {
913
+ if (!data.id) throw new Error('Missing id');
914
+ result = applyRecord(projectDir, data.id, { monitor: data.monitor });
915
+ } else if (action === 'rollback') {
916
+ if (!data.id) throw new Error('Missing id');
917
+ result = rollbackRecord(projectDir, data.id, { reason: data.reason });
918
+ } else {
919
+ throw new Error(`Unknown improvement action: ${action}`);
920
+ }
921
+ const status = result.ok === false && result.error ? 400 : 200;
922
+ res.writeHead(status, { 'Content-Type': 'application/json' });
923
+ res.end(JSON.stringify(result));
924
+ notifyClients();
925
+ } catch (err) {
926
+ res.writeHead(400, { 'Content-Type': 'application/json' });
927
+ res.end(JSON.stringify({ error: err.message }));
928
+ }
929
+ });
930
+ return;
931
+ }
932
+
933
+ if (url.pathname.startsWith('/api/improvement/') && req.method === 'GET') {
934
+ const id = decodeURIComponent(url.pathname.slice('/api/improvement/'.length));
935
+ if (id && id !== 'pending') {
936
+ try {
937
+ const { loadRecord } = await import('../improvement/store.mjs');
938
+ const record = loadRecord(process.cwd(), id);
939
+ if (!record) {
940
+ res.writeHead(404, { 'Content-Type': 'application/json' });
941
+ res.end(JSON.stringify({ error: 'not-found' }));
942
+ return;
943
+ }
944
+ res.writeHead(200, { 'Content-Type': 'application/json' });
945
+ res.end(JSON.stringify(record));
946
+ } catch (err) {
947
+ res.writeHead(500, { 'Content-Type': 'application/json' });
948
+ res.end(JSON.stringify({ error: err.message }));
949
+ }
950
+ return;
951
+ }
952
+ }
953
+
954
+ if (url.pathname === '/api/oracle' && req.method === 'GET') {
955
+ try {
956
+ const { homedir: osHome } = await import('node:os');
957
+ const { readHeartbeat } = await import('../daemons/contract.mjs');
958
+ const { heartbeatPath, readLastTick } = await import('../oracle/index.mjs');
959
+ const { collectReadModel, enrichReadModel } = await import('../oracle/read-model.mjs');
960
+ const { synthesizeVerdict } = await import('../oracle/synthesize.mjs');
961
+ const home = osHome();
962
+ const projectDir = process.cwd();
963
+ const readModel = collectReadModel({ rootDir: ROOT_DIR, projectDir, homeDir: home });
964
+ const enriched = await enrichReadModel(readModel);
965
+ const synthesis = synthesizeVerdict(enriched);
966
+ const hb = readHeartbeat(heartbeatPath(home));
967
+ res.writeHead(200, { 'Content-Type': 'application/json' });
968
+ res.end(JSON.stringify({
969
+ running: !!hb,
970
+ heartbeat: hb,
971
+ lastTick: readLastTick(home),
972
+ verdict: synthesis.verdict,
973
+ gaps: synthesis.gaps,
974
+ recommendedActions: synthesis.recommendedActions,
975
+ orgGraph: enriched.orgGraph,
976
+ openBeads: enriched.beads?.openCount ?? null,
977
+ lastPropagationCheck: enriched.orgGraph?.propagation ?? null,
978
+ }));
979
+ } catch (err) {
980
+ res.writeHead(500, { 'Content-Type': 'application/json' });
981
+ res.end(JSON.stringify({ error: err.message }));
982
+ }
983
+ return;
984
+ }
985
+
839
986
  if (url.pathname === '/api/doctor') {
840
987
  try {
841
988
  const { readState } = await import('../doctor/index.mjs');
@@ -1435,17 +1582,47 @@ const server = createServer(async (req, res) => {
1435
1582
  return;
1436
1583
  }
1437
1584
 
1585
+ if (url.pathname === '/api/sources' && req.method === 'GET') {
1586
+ try {
1587
+ const { loadProjectConfig } = await import('../config/project-config.mjs');
1588
+ const {
1589
+ resolveEffectiveSourceTargetsFromConfig,
1590
+ legacyEnvSourceTargets,
1591
+ normalizeConfigTarget,
1592
+ } = await import('../config/source-targets.mjs');
1593
+ const loaded = loadProjectConfig(ROOT_DIR, process.env);
1594
+ const configTargets = (loaded.config.sources?.targets ?? []).map(normalizeConfigTarget);
1595
+ const envTargets = legacyEnvSourceTargets(process.env);
1596
+ const effective = resolveEffectiveSourceTargetsFromConfig(loaded.config, process.env);
1597
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1598
+ res.end(JSON.stringify({
1599
+ path: loaded.path,
1600
+ configTargets,
1601
+ envTargets,
1602
+ effective,
1603
+ }));
1604
+ } catch (err) {
1605
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1606
+ res.end(JSON.stringify({ error: err.message }));
1607
+ }
1608
+ return;
1609
+ }
1610
+
1438
1611
  if (url.pathname === '/api/intake/config' && req.method === 'GET') {
1439
1612
  try {
1440
1613
  const { loadIntakeConfig, INTAKE_DEPTH_GUIDANCE, INTAKE_HARD_MAX_DEPTH } = await import('../intake/intake-config.mjs');
1614
+ const { getRebrand } = await import('../profiles/rebrand.mjs');
1441
1615
  const config = loadIntakeConfig(ROOT_DIR, process.env);
1616
+ const rebrand = getRebrand(ROOT_DIR);
1442
1617
  res.writeHead(200, { 'Content-Type': 'application/json' });
1443
1618
  res.end(JSON.stringify({
1444
1619
  config,
1445
1620
  rootDir: ROOT_DIR,
1446
1621
  guidance: INTAKE_DEPTH_GUIDANCE,
1447
1622
  hardMaxDepth: INTAKE_HARD_MAX_DEPTH,
1448
- defaults: { projectInbox: '.cx/inbox', docsIntake: 'docs/intake' },
1623
+ defaults: { projectInbox: '.cx/inbox', docsIntake: 'docs/intake', rootInbox: 'inbox/' },
1624
+ label: rebrand.intakeQueueLabel,
1625
+ itemNoun: rebrand.signalNoun,
1449
1626
  }));
1450
1627
  } catch (err) {
1451
1628
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1457,10 +1634,17 @@ const server = createServer(async (req, res) => {
1457
1634
  if (url.pathname === '/api/intake/list' && req.method === 'GET') {
1458
1635
  try {
1459
1636
  const { createIntakeQueue } = await import('../intake/queue.mjs');
1637
+ const { getRebrand } = await import('../profiles/rebrand.mjs');
1460
1638
  const queue = createIntakeQueue(ROOT_DIR, process.env);
1461
1639
  const items = queue.listPending();
1640
+ const rebrand = getRebrand(ROOT_DIR);
1462
1641
  res.writeHead(200, { 'Content-Type': 'application/json' });
1463
- res.end(JSON.stringify({ items, total: items.length }));
1642
+ res.end(JSON.stringify({
1643
+ items,
1644
+ total: items.length,
1645
+ label: rebrand.intakeQueueLabel,
1646
+ itemNoun: rebrand.signalNoun,
1647
+ }));
1464
1648
  } catch (err) {
1465
1649
  res.writeHead(500, { 'Content-Type': 'application/json' });
1466
1650
  res.end(JSON.stringify({ error: err.message }));
@@ -1934,6 +2118,51 @@ const server = createServer(async (req, res) => {
1934
2118
  return;
1935
2119
  }
1936
2120
 
2121
+ if (url.pathname === '/api/chat/loop/stream' && req.method === 'GET') {
2122
+ handleChatLoopStream(req, res, { rootDir: ROOT_DIR });
2123
+ return;
2124
+ }
2125
+
2126
+ if (url.pathname === '/api/chat/loop/pending' && req.method === 'GET') {
2127
+ handleChatLoopPending(req, res);
2128
+ return;
2129
+ }
2130
+
2131
+ if (url.pathname === '/api/chat/loop/permission' && req.method === 'POST') {
2132
+ handleChatLoopPermission(req, res);
2133
+ return;
2134
+ }
2135
+
2136
+ if (url.pathname === '/api/chat/config' && (req.method === 'GET' || req.method === 'POST')) {
2137
+ handleChatLoopConfig(req, res, { rootDir: ROOT_DIR });
2138
+ return;
2139
+ }
2140
+
2141
+ if (url.pathname === '/api/chat/models' && req.method === 'GET') {
2142
+ void handleChatModels(req, res, { rootDir: ROOT_DIR });
2143
+ return;
2144
+ }
2145
+
2146
+ if (url.pathname === '/api/chat/models/select' && req.method === 'POST') {
2147
+ handleChatModelSelect(req, res, { rootDir: ROOT_DIR });
2148
+ return;
2149
+ }
2150
+
2151
+ if (url.pathname === '/api/chat/loop/command' && req.method === 'POST') {
2152
+ handleChatLoopCommand(req, res, { rootDir: ROOT_DIR });
2153
+ return;
2154
+ }
2155
+
2156
+ if (url.pathname === '/api/chat/loop/history' && req.method === 'GET') {
2157
+ handleChatLoopHistory(req, res, { rootDir: ROOT_DIR });
2158
+ return;
2159
+ }
2160
+
2161
+ if (url.pathname === '/api/chat/loop/cancel' && req.method === 'POST') {
2162
+ handleChatLoopCancel(req, res);
2163
+ return;
2164
+ }
2165
+
1937
2166
  if (req.method === 'POST') {
1938
2167
  let body = '';
1939
2168
  req.on('data', chunk => { body += chunk; });
@@ -2140,6 +2369,7 @@ const server = createServer(async (req, res) => {
2140
2369
  maxDepth: data.maxDepth,
2141
2370
  includeProjectInbox: data.includeProjectInbox,
2142
2371
  includeDocsIntake: data.includeDocsIntake,
2372
+ includeRootInbox: data.includeRootInbox ?? data.includeArchetypeInbox,
2143
2373
  });
2144
2374
  res.writeHead(200, { 'Content-Type': 'application/json' });
2145
2375
  res.end(JSON.stringify({ success: true, config: next }));
@@ -2298,9 +2528,21 @@ const server = createServer(async (req, res) => {
2298
2528
  return;
2299
2529
  }
2300
2530
 
2531
+ if (url.pathname.startsWith('/demo-preview/')) {
2532
+ const preview = resolveDemoPreviewPath(url.pathname);
2533
+ if (!preview.ok) {
2534
+ res.writeHead(preview.status);
2535
+ res.end(preview.message);
2536
+ return;
2537
+ }
2538
+ res.writeHead(200, { 'Content-Type': demoPreviewMime(preview.ext) });
2539
+ res.end(readFileSync(preview.filePath));
2540
+ return;
2541
+ }
2542
+
2301
2543
  // Static files
2302
2544
  let filePath = url.pathname === '/' ? '/index.html' : url.pathname;
2303
- const fullPath = join(STATIC_DIR, filePath);
2545
+ let fullPath = join(STATIC_DIR, filePath);
2304
2546
 
2305
2547
  if (!fullPath.startsWith(STATIC_DIR)) {
2306
2548
  res.writeHead(403);
@@ -2308,7 +2550,14 @@ const server = createServer(async (req, res) => {
2308
2550
  return;
2309
2551
  }
2310
2552
 
2311
- if (!existsSync(fullPath)) {
2553
+ if (existsSync(fullPath) && statSync(fullPath).isDirectory()) {
2554
+ fullPath = join(fullPath, 'index.html');
2555
+ } else if (!existsSync(fullPath)) {
2556
+ const indexCandidate = join(STATIC_DIR, filePath, 'index.html');
2557
+ if (existsSync(indexCandidate)) fullPath = indexCandidate;
2558
+ }
2559
+
2560
+ if (!existsSync(fullPath) || statSync(fullPath).isDirectory()) {
2312
2561
  res.writeHead(404);
2313
2562
  res.end('Not found');
2314
2563
  return;
@@ -2345,11 +2594,12 @@ function runEmbedSync() {
2345
2594
  // from process.env (telemetry credentials, model provider keys, etc.) find them
2346
2595
  // even when the server is started directly (not through bin/construct).
2347
2596
  try {
2348
- const { loadConstructEnv, getUserEnvPath } = await import('../env-config.mjs');
2597
+ const { getUserEnvPath } = await import('../env-config.mjs');
2598
+ const { prepareConstructEnv } = await import('../runtime-env.mjs');
2349
2599
  const homeDir = HOME || homedir();
2350
2600
  const envPath = getUserEnvPath(homeDir);
2351
2601
  if (existsSync(envPath)) {
2352
- const ENV = loadConstructEnv({ rootDir: ROOT_DIR, homeDir, env: process.env });
2602
+ const ENV = prepareConstructEnv({ rootDir: ROOT_DIR, homeDir, env: process.env });
2353
2603
  for (const [key, value] of Object.entries(ENV)) {
2354
2604
  if (!(key in process.env)) process.env[key] = value;
2355
2605
  }
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * Three tiers, configurable via env:
5
5
  *
6
- * read 60 req/min/key (dashboard polling, list endpoints)
7
- * chat 10 req/min/key (/api/chat/stream — bursty + expensive)
8
- * write 5 req/min/key (state-mutating endpoints approvals, config)
6
+ * read 60 req/min/key (dashboard polling, list endpoints)
7
+ * chat 10 req/min/key (/api/chat/stream — bursty + expensive)
8
+ * command 120 req/min/key (/api/chat/loop/commandslash cmds, no LLM)
9
+ * write 5 req/min/key (state-mutating endpoints — approvals, config)
9
10
  *
10
11
  * `key` is by default the request's source IP, with the active session id
11
12
  * mixed in when present so two users behind the same NAT get separate
@@ -18,6 +19,7 @@
18
19
  const DEFAULT_TIERS = {
19
20
  read: { capacity: 60, refillPerMs: 60 / 60_000 },
20
21
  chat: { capacity: 10, refillPerMs: 10 / 60_000 },
22
+ command: { capacity: 120, refillPerMs: 120 / 60_000 },
21
23
  write: { capacity: 5, refillPerMs: 5 / 60_000 },
22
24
  };
23
25
 
@@ -306,8 +306,9 @@ async function runSnapshotForSlack() {
306
306
  const { SnapshotEngine, renderMarkdown } = await import('../embed/snapshot.mjs');
307
307
  const { EMPTY_CONFIG } = await import('../embed/config.mjs');
308
308
 
309
- const registry = ProviderRegistry.fromEnv(env);
310
- const sources = registry.autoSources(env);
309
+ const registry = await ProviderRegistry.fromEnv(env);
310
+ const { resolveAutoEmbedSources } = await import('../embed/auto-sources.mjs');
311
+ const sources = resolveAutoEmbedSources({ cwd: process.cwd(), env, registry });
311
312
  if (!sources.length) return '⚠️ No embed sources configured. Add credentials to config.env.';
312
313
 
313
314
  const config = { ...EMPTY_CONFIG, sources, snapshot: { maxItems: 10 } };
@@ -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 {