@geraldmaron/construct 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. package/README.md +29 -19
  2. package/bin/construct +311 -81
  3. package/bin/construct-postinstall.mjs +30 -2
  4. package/examples/distribution/README.md +42 -0
  5. package/examples/distribution/manifest.json +53 -0
  6. package/examples/distribution/sources/adr.md +84 -0
  7. package/examples/distribution/sources/deck-one-pager.md +65 -0
  8. package/examples/distribution/sources/prd-platform.md +161 -0
  9. package/examples/distribution/sources/research-brief.md +80 -0
  10. package/examples/distribution/sources/rfc-platform.md +82 -0
  11. package/examples/distribution/sources/runbook.md +103 -0
  12. package/examples/distribution/sources/strategy.md +88 -0
  13. package/lib/adapters-sync.mjs +67 -0
  14. package/lib/artifact-gate-notice.mjs +38 -0
  15. package/lib/artifact-manifest.mjs +112 -0
  16. package/lib/artifact-release-gate.mjs +171 -0
  17. package/lib/artifact-reviewers.mjs +68 -0
  18. package/lib/artifact-type-from-path.mjs +79 -0
  19. package/lib/audit-skills.mjs +4 -0
  20. package/lib/audit-specialists.mjs +285 -0
  21. package/lib/audit-trail.mjs +1 -1
  22. package/lib/auto-docs.mjs +87 -18
  23. package/lib/beads/drift.mjs +27 -6
  24. package/lib/beads-client.mjs +49 -121
  25. package/lib/beads-optimistic.mjs +5 -12
  26. package/lib/brand-fonts.mjs +93 -0
  27. package/lib/brand-prose.mjs +214 -0
  28. package/lib/brand-tokens.mjs +92 -0
  29. package/lib/bridges/copilot-proxy.mjs +13 -26
  30. package/lib/capability-ledger.mjs +156 -0
  31. package/lib/certification/artifact-fixtures.mjs +132 -0
  32. package/lib/certification/artifact-gates.mjs +63 -0
  33. package/lib/certification/artifact-provenance.mjs +97 -0
  34. package/lib/certification/canonical-scenarios.mjs +78 -0
  35. package/lib/certification/cli.mjs +191 -0
  36. package/lib/certification/dashboard-api.mjs +71 -0
  37. package/lib/certification/demo-parity.mjs +116 -0
  38. package/lib/certification/document-io-fixtures.mjs +246 -0
  39. package/lib/certification/document-workflow.mjs +97 -0
  40. package/lib/certification/eval-bridge.mjs +77 -0
  41. package/lib/certification/model-routing.mjs +149 -0
  42. package/lib/certification/prompt-budget.mjs +119 -0
  43. package/lib/certification/rc-gate.mjs +206 -0
  44. package/lib/certification/real-llm-scenarios.mjs +303 -0
  45. package/lib/certification/role-cards.mjs +113 -0
  46. package/lib/certification/role-overlays.mjs +117 -0
  47. package/lib/certification/run.mjs +122 -0
  48. package/lib/certification/runner.mjs +323 -0
  49. package/lib/certification/scenarios.mjs +51 -0
  50. package/lib/certification/skill-inventory.mjs +289 -0
  51. package/lib/certification/skill-scenarios.mjs +147 -0
  52. package/lib/certification/specialist-contracts.mjs +85 -0
  53. package/lib/certification/specialist-scenarios.mjs +175 -0
  54. package/lib/certification/stale-impact.mjs +146 -0
  55. package/lib/certification/status.mjs +252 -0
  56. package/lib/certification/store.mjs +77 -0
  57. package/lib/chat/cli.mjs +333 -0
  58. package/lib/chat/command-suggest.mjs +161 -0
  59. package/lib/chat/commands.mjs +215 -0
  60. package/lib/chat/config.mjs +142 -0
  61. package/lib/chat/context-compactor.mjs +250 -0
  62. package/lib/chat/context-continuation.mjs +253 -0
  63. package/lib/chat/continuation-source.mjs +58 -0
  64. package/lib/chat/demo-guide.mjs +61 -0
  65. package/lib/chat/design-tokens.mjs +91 -0
  66. package/lib/chat/desktop-binary.mjs +79 -0
  67. package/lib/chat/desktop-build.mjs +130 -0
  68. package/lib/chat/desktop-launcher.mjs +133 -0
  69. package/lib/chat/evidence.mjs +145 -0
  70. package/lib/chat/export.mjs +74 -0
  71. package/lib/chat/harness/driver.mjs +91 -0
  72. package/lib/chat/list-picker.mjs +112 -0
  73. package/lib/chat/model-picker.mjs +356 -0
  74. package/lib/chat/openrouter-fallback.mjs +151 -0
  75. package/lib/chat/permission-prompt.mjs +33 -0
  76. package/lib/chat/picker-catalog.mjs +45 -0
  77. package/lib/chat/policy-telemetry.mjs +34 -0
  78. package/lib/chat/present.mjs +245 -0
  79. package/lib/chat/session-context.mjs +39 -0
  80. package/lib/chat/session-persist.mjs +73 -0
  81. package/lib/chat/session-restore.mjs +71 -0
  82. package/lib/chat/session-settings.mjs +53 -0
  83. package/lib/chat/system-prompt.mjs +52 -0
  84. package/lib/chat/transparency.mjs +93 -0
  85. package/lib/chat/tui/color-scheme.mjs +42 -0
  86. package/lib/chat/tui/markdown.mjs +123 -0
  87. package/lib/chat/tui/presentation.mjs +100 -0
  88. package/lib/chat/tui/render.mjs +500 -0
  89. package/lib/chat/tui/turn-block.mjs +284 -0
  90. package/lib/chat/tui/turn-present.mjs +18 -0
  91. package/lib/chat/tui/turn-state.mjs +88 -0
  92. package/lib/chat/tui/usage.mjs +122 -0
  93. package/lib/chat/web-commands.mjs +146 -0
  94. package/lib/chat/web-launcher.mjs +63 -0
  95. package/lib/chat/web-picker-keys.mjs +46 -0
  96. package/lib/chat/web-session.mjs +159 -0
  97. package/lib/cli-commands.mjs +129 -15
  98. package/lib/comment-lint.mjs +6 -4
  99. package/lib/config/schema.mjs +36 -5
  100. package/lib/contract-schemas/decision.json +50 -0
  101. package/lib/contract-schemas/implementation.json +51 -0
  102. package/lib/contract-schemas/review-report.json +32 -0
  103. package/lib/contract-schemas/test-report.json +43 -0
  104. package/lib/contracts/construct-handoff.mjs +60 -0
  105. package/lib/contracts/validate.mjs +32 -3
  106. package/lib/contracts/violation-log.mjs +58 -5
  107. package/lib/dashboard-demo.mjs +71 -0
  108. package/lib/dashboard-static.mjs +19 -5
  109. package/lib/decisions/registry.mjs +5 -3
  110. package/lib/deck-export-pptx.mjs +1152 -0
  111. package/lib/demo-recording.mjs +142 -0
  112. package/lib/demo-script.mjs +114 -0
  113. package/lib/demo-surface.mjs +249 -0
  114. package/lib/demo.mjs +598 -140
  115. package/lib/diagram-export.mjs +192 -0
  116. package/lib/diagram.mjs +13 -11
  117. package/lib/docs-verify.mjs +25 -7
  118. package/lib/doctor/index.mjs +3 -1
  119. package/lib/doctor/project-adapters.mjs +44 -0
  120. package/lib/doctor/watchers/cx-budget.mjs +98 -0
  121. package/lib/doctor/watchers/graph-staleness.mjs +52 -0
  122. package/lib/doctor/watchers/process-pressure.mjs +4 -3
  123. package/lib/document-export.mjs +360 -36
  124. package/lib/document-extract.mjs +44 -4
  125. package/lib/document-ingest.mjs +29 -20
  126. package/lib/embed/cli.mjs +2 -2
  127. package/lib/embed/daemon.mjs +18 -46
  128. package/lib/embed/worker.mjs +2 -2
  129. package/lib/embedded-contract/model-resolve.mjs +10 -9
  130. package/lib/env-config.mjs +26 -0
  131. package/lib/evals/dataset.mjs +137 -0
  132. package/lib/evals/gates.mjs +175 -0
  133. package/lib/graph/build-co-change.mjs +54 -0
  134. package/lib/graph/build-from-registry.mjs +136 -0
  135. package/lib/graph/build-import-graph.mjs +153 -0
  136. package/lib/graph/cli.mjs +110 -0
  137. package/lib/graph/impact-cli.mjs +89 -0
  138. package/lib/graph/impact.mjs +108 -0
  139. package/lib/graph/staleness.mjs +44 -0
  140. package/lib/graph/store.mjs +172 -0
  141. package/lib/health-check.mjs +90 -76
  142. package/lib/hooks/artifact-release-gate.mjs +43 -0
  143. package/lib/hooks/brand-prose-lint.mjs +38 -0
  144. package/lib/hooks/graph-impact-advisory.mjs +62 -0
  145. package/lib/hooks/session-optimize.mjs +84 -207
  146. package/lib/hooks/session-start.mjs +4 -5
  147. package/lib/host-disposition.mjs +7 -0
  148. package/lib/improvement/cli.mjs +189 -0
  149. package/lib/improvement/controller.mjs +137 -0
  150. package/lib/improvement/proposal.mjs +120 -0
  151. package/lib/improvement/specialist-loop.mjs +192 -0
  152. package/lib/improvement/store.mjs +89 -0
  153. package/lib/improvement/surface.mjs +219 -0
  154. package/lib/ingest-tooling.mjs +97 -0
  155. package/lib/init/doc-lanes.mjs +165 -0
  156. package/lib/init-docs.mjs +31 -178
  157. package/lib/init-unified.mjs +27 -97
  158. package/lib/init-update-guide.mjs +102 -0
  159. package/lib/init-update.mjs +32 -1
  160. package/lib/init.mjs +8 -0
  161. package/lib/install/desktop-binary-download.mjs +85 -0
  162. package/lib/install/legacy-global-cleanup.mjs +189 -0
  163. package/lib/intake/daemon.mjs +2 -0
  164. package/lib/intake/git-queue.mjs +9 -8
  165. package/lib/intake/queue.mjs +2 -1
  166. package/lib/intake/session-prelude.mjs +90 -1
  167. package/lib/libreoffice-export.mjs +97 -0
  168. package/lib/logging/rotate.mjs +9 -0
  169. package/lib/maintenance/docker-reclaim.mjs +206 -0
  170. package/lib/mcp/external-schema-cost.mjs +74 -0
  171. package/lib/mcp/server.mjs +84 -9
  172. package/lib/mcp/stdio-mcp-probe.mjs +188 -0
  173. package/lib/mcp/tool-budget.mjs +55 -4
  174. package/lib/mcp/tools/document.mjs +18 -4
  175. package/lib/mcp/tools/skills.mjs +32 -3
  176. package/lib/mcp/tools/workflow.mjs +13 -1
  177. package/lib/model-free-selector.mjs +18 -0
  178. package/lib/model-registry.mjs +13 -229
  179. package/lib/model-router.mjs +373 -92
  180. package/lib/models/behavior-matrix.mjs +289 -0
  181. package/lib/models/catalog.mjs +209 -0
  182. package/lib/models/execution-capability-profile.mjs +196 -0
  183. package/lib/models/execution-policy.mjs +307 -0
  184. package/lib/models/provider-poll.mjs +383 -0
  185. package/lib/npm-spawn-env.mjs +17 -0
  186. package/lib/ollama/installed-models.mjs +129 -0
  187. package/lib/oracle/actions.mjs +187 -0
  188. package/lib/oracle/artifact-gate.mjs +99 -0
  189. package/lib/oracle/cli.mjs +204 -0
  190. package/lib/oracle/daemon-entry.mjs +14 -0
  191. package/lib/oracle/dispatch.mjs +81 -0
  192. package/lib/oracle/execute.mjs +143 -0
  193. package/lib/oracle/gaps.mjs +76 -0
  194. package/lib/oracle/index.mjs +84 -0
  195. package/lib/oracle/issues.mjs +164 -0
  196. package/lib/oracle/org-graph.mjs +170 -0
  197. package/lib/oracle/policy.mjs +80 -0
  198. package/lib/oracle/read-model.mjs +398 -0
  199. package/lib/oracle/reconcile.mjs +191 -0
  200. package/lib/oracle/routing.mjs +89 -0
  201. package/lib/oracle/synthesize.mjs +384 -0
  202. package/lib/oracle/verdicts.mjs +51 -0
  203. package/lib/orchestration/worker.mjs +88 -27
  204. package/lib/orchestration-policy.mjs +50 -0
  205. package/lib/parity.mjs +96 -2
  206. package/lib/playwright-demo.mjs +252 -0
  207. package/lib/project-init-shared.mjs +4 -1
  208. package/lib/prompt-composer.js +7 -3
  209. package/lib/prompt-validation-contract.mjs +24 -0
  210. package/lib/provider-capabilities.js +57 -11
  211. package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
  212. package/lib/providers/contract/adapters/git/index.mjs +115 -0
  213. package/lib/providers/contract/adapters/github/index.mjs +166 -0
  214. package/lib/providers/contract/adapters/jira/index.mjs +187 -0
  215. package/lib/providers/contract/adapters/slack/index.mjs +175 -0
  216. package/lib/providers/contract/contract-tests.mjs +57 -0
  217. package/lib/providers/contract/errors.mjs +48 -0
  218. package/lib/providers/contract/interface.mjs +50 -0
  219. package/lib/providers/contract/registry.mjs +102 -0
  220. package/lib/providers/copilot-auth.mjs +297 -0
  221. package/lib/providers/credential-bootstrap.mjs +178 -0
  222. package/lib/providers/credential-catalog.mjs +46 -0
  223. package/lib/providers/credential-sources.mjs +63 -0
  224. package/lib/providers/creds.mjs +5 -2
  225. package/lib/providers/op-run.mjs +59 -0
  226. package/lib/providers/secret-resolver.mjs +159 -0
  227. package/lib/publish-template.mjs +163 -0
  228. package/lib/publish-tooling.mjs +119 -0
  229. package/lib/publish.mjs +305 -0
  230. package/lib/registry/cli.mjs +82 -0
  231. package/lib/registry/consolidation.mjs +147 -0
  232. package/lib/registry/generate-docs.mjs +75 -0
  233. package/lib/registry/skill-verification.mjs +57 -0
  234. package/lib/registry/surface-map.mjs +76 -0
  235. package/lib/registry/validate.mjs +135 -0
  236. package/lib/resources/budget.mjs +82 -0
  237. package/lib/resources/process-budget.mjs +45 -0
  238. package/lib/rules-delivery.mjs +9 -2
  239. package/lib/rules-read.mjs +26 -0
  240. package/lib/runtime-env.mjs +23 -0
  241. package/lib/runtime-pressure.mjs +51 -7
  242. package/lib/schema-infer.mjs +13 -2
  243. package/lib/server/chat-loop.mjs +622 -0
  244. package/lib/server/demo-preview.mjs +63 -0
  245. package/lib/server/index.mjs +228 -7
  246. package/lib/service-manager.mjs +60 -8
  247. package/lib/setup.mjs +70 -7
  248. package/lib/specialists/prompt-schema.mjs +19 -10
  249. package/lib/specialists/roster.mjs +43 -0
  250. package/lib/specialists/scaffold.mjs +56 -0
  251. package/lib/storage/backend.mjs +6 -6
  252. package/lib/storage/hybrid-query.mjs +7 -4
  253. package/lib/storage/state-source.mjs +5 -5
  254. package/lib/storage/sync.mjs +2 -3
  255. package/lib/telemetry/rule-calls.mjs +52 -0
  256. package/lib/template-registry.mjs +2 -2
  257. package/lib/templates/visual-requirements.mjs +27 -51
  258. package/lib/test-corpus-inventory.mjs +313 -0
  259. package/lib/uninstall/uninstall.mjs +19 -7
  260. package/lib/update.mjs +12 -0
  261. package/lib/upgrade.mjs +14 -0
  262. package/lib/wireframe.mjs +20 -14
  263. package/lib/worker/run.mjs +17 -4
  264. package/lib/worker/trace.mjs +11 -3
  265. package/package.json +28 -14
  266. package/personas/construct.md +2 -2
  267. package/platforms/claude/settings.template.json +36 -0
  268. package/rules/common/patterns.md +1 -1
  269. package/rules/common/release-gates.md +4 -3
  270. package/scripts/sync-specialists.mjs +41 -28
  271. package/skills/devops/data-engineering.md +1 -1
  272. package/skills/docs/adr-workflow.md +1 -0
  273. package/skills/docs/backlog-proposal-workflow.md +1 -0
  274. package/skills/docs/codebase-research-workflow.md +40 -0
  275. package/skills/docs/customer-profile-workflow.md +1 -0
  276. package/skills/docs/document-ingest-workflow.md +1 -0
  277. package/skills/docs/evidence-ingest-workflow.md +1 -0
  278. package/skills/docs/init-docs.md +2 -2
  279. package/skills/docs/init-project.md +7 -2
  280. package/skills/docs/prd-workflow.md +24 -1
  281. package/skills/docs/prfaq-workflow.md +1 -0
  282. package/skills/docs/product-intelligence-workflow.md +1 -0
  283. package/skills/docs/product-signal-workflow.md +1 -0
  284. package/skills/docs/research-workflow.md +54 -37
  285. package/skills/docs/runbook-workflow.md +1 -0
  286. package/skills/docs/strategy-workflow.md +1 -0
  287. package/skills/docs/user-research-workflow.md +40 -0
  288. package/skills/operating/orchestration-reference.md +1 -1
  289. package/skills/roles/architect.md +5 -0
  290. package/skills/roles/operator.docs.md +4 -0
  291. package/skills/routing.md +4 -2
  292. package/specialists/artifact-manifest.json +480 -0
  293. package/specialists/artifact-manifest.schema.json +53 -0
  294. package/specialists/audit-enrichments.json +454 -0
  295. package/specialists/contracts.json +37 -25
  296. package/specialists/prompts/_shared/validation-contract.md +26 -0
  297. package/specialists/prompts/cx-accessibility.md +21 -2
  298. package/specialists/prompts/cx-ai-engineer.md +24 -1
  299. package/specialists/prompts/cx-architect.md +4 -1
  300. package/specialists/prompts/cx-business-strategist.md +23 -2
  301. package/specialists/prompts/cx-data-analyst.md +22 -1
  302. package/specialists/prompts/cx-data-engineer.md +23 -2
  303. package/specialists/prompts/cx-debugger.md +21 -2
  304. package/specialists/prompts/cx-designer.md +26 -3
  305. package/specialists/prompts/cx-devil-advocate.md +19 -2
  306. package/specialists/prompts/cx-docs-keeper.md +28 -1
  307. package/specialists/prompts/cx-engineer.md +22 -1
  308. package/specialists/prompts/cx-evaluator.md +18 -1
  309. package/specialists/prompts/cx-explorer.md +21 -2
  310. package/specialists/prompts/cx-legal-compliance.md +21 -2
  311. package/specialists/prompts/cx-operations.md +21 -2
  312. package/specialists/prompts/cx-oracle.md +94 -0
  313. package/specialists/prompts/cx-orchestrator.md +20 -1
  314. package/specialists/prompts/cx-platform-engineer.md +25 -2
  315. package/specialists/prompts/cx-product-manager.md +32 -1
  316. package/specialists/prompts/cx-qa.md +24 -2
  317. package/specialists/prompts/cx-rd-lead.md +23 -2
  318. package/specialists/prompts/cx-release-manager.md +23 -2
  319. package/specialists/prompts/cx-researcher.md +22 -2
  320. package/specialists/prompts/cx-reviewer.md +18 -1
  321. package/specialists/prompts/cx-security.md +22 -2
  322. package/specialists/prompts/cx-sre.md +30 -3
  323. package/specialists/prompts/cx-test-automation.md +7 -1
  324. package/specialists/prompts/cx-trace-reviewer.md +22 -3
  325. package/specialists/prompts/cx-ux-researcher.md +21 -2
  326. package/specialists/registry.json +52 -173
  327. package/specialists/tone-profiles.json +42 -0
  328. package/templates/demos/playwright/demo-recording.config.mjs +47 -0
  329. package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
  330. package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
  331. package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
  332. package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
  333. package/templates/demos/tapes/resource-guard-rails.tape +49 -0
  334. package/templates/demos/vhs/construct-cockpit.json +24 -0
  335. package/templates/distribution/construct-brand.typ +446 -0
  336. package/templates/distribution/construct-decision.typ +38 -0
  337. package/templates/distribution/construct-deck.html +95 -0
  338. package/templates/distribution/construct-pdf.typ +38 -0
  339. package/templates/distribution/construct-prd.typ +38 -0
  340. package/templates/distribution/construct-research.typ +38 -0
  341. package/templates/distribution/construct-web.html +92 -0
  342. package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
  343. package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
  344. package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
  345. package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
  346. package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
  347. package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
  348. package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
  349. package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
  350. package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
  351. package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
  352. package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
  353. package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
  354. package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
  355. package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
  356. package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
  357. package/templates/distribution/fonts/README.md +36 -0
  358. package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
  359. package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
  360. package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
  361. package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
  362. package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
  363. package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
  364. package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
  365. package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
  366. package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
  367. package/templates/distribution/icons/activity.svg +15 -0
  368. package/templates/distribution/icons/alert-triangle.svg +17 -0
  369. package/templates/distribution/icons/book-open.svg +16 -0
  370. package/templates/distribution/icons/bot.svg +20 -0
  371. package/templates/distribution/icons/brain.svg +22 -0
  372. package/templates/distribution/icons/circle-check.svg +16 -0
  373. package/templates/distribution/icons/clipboard-check.svg +17 -0
  374. package/templates/distribution/icons/cpu.svg +28 -0
  375. package/templates/distribution/icons/database.svg +17 -0
  376. package/templates/distribution/icons/eye.svg +16 -0
  377. package/templates/distribution/icons/file-text.svg +19 -0
  378. package/templates/distribution/icons/gauge.svg +16 -0
  379. package/templates/distribution/icons/git-branch.svg +17 -0
  380. package/templates/distribution/icons/key.svg +17 -0
  381. package/templates/distribution/icons/layers.svg +17 -0
  382. package/templates/distribution/icons/list-checks.svg +19 -0
  383. package/templates/distribution/icons/lock.svg +16 -0
  384. package/templates/distribution/icons/message-square.svg +15 -0
  385. package/templates/distribution/icons/network.svg +19 -0
  386. package/templates/distribution/icons/route.svg +17 -0
  387. package/templates/distribution/icons/scale.svg +19 -0
  388. package/templates/distribution/icons/search.svg +16 -0
  389. package/templates/distribution/icons/send.svg +16 -0
  390. package/templates/distribution/icons/server.svg +18 -0
  391. package/templates/distribution/icons/shield-check.svg +16 -0
  392. package/templates/distribution/icons/users.svg +18 -0
  393. package/templates/distribution/icons/webhook.svg +17 -0
  394. package/templates/distribution/icons/workflow.svg +17 -0
  395. package/templates/distribution/icons/wrench.svg +15 -0
  396. package/templates/distribution/run.mjs +18 -1
  397. package/templates/docs/adr.md +7 -0
  398. package/templates/docs/construct_guide.md +8 -8
  399. package/templates/docs/customer-profile.md +4 -0
  400. package/templates/docs/one-pager.md +4 -0
  401. package/templates/docs/postmortem.md +31 -0
  402. package/templates/docs/prd-platform.md +19 -0
  403. package/templates/docs/prd.md +15 -0
  404. package/templates/docs/prfaq.md +7 -0
  405. package/templates/docs/qa-strategy.md +103 -0
  406. package/templates/docs/research-brief.md +8 -0
  407. package/templates/docs/rfc-platform.md +12 -0
  408. package/templates/docs/test-plan.md +7 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * lib/oracle/routing.mjs — deterministic gap → specialist routing table.
3
+ *
4
+ * Mirrors cx-oracle.md routing without LLM involvement. Issue raising,
5
+ * approve execution, and routing artifact generation consume these mappings.
6
+ */
7
+
8
+ const GAP_ROUTES = {
9
+ 'parity-drift': { primary: 'cx-platform-engineer', secondary: 'cx-docs-keeper' },
10
+ 'contract-violations': { primary: 'cx-reviewer', secondary: 'cx-engineer' },
11
+ 'doctor-escalation': { primary: 'cx-sre', secondary: 'cx-operations' },
12
+ 'outcomes-degradation': { primary: 'cx-trace-reviewer', secondary: null },
13
+ 'census-stale': { primary: 'cx-architect', secondary: 'cx-docs-keeper' },
14
+ 'alignment-regression': { primary: 'cx-architect', secondary: 'cx-docs-keeper' },
15
+ 'true-skill-orphan': { primary: 'cx-architect', secondary: 'cx-docs-keeper' },
16
+ 'observations-empty': { primary: 'cx-explorer', secondary: 'cx-data-engineer' },
17
+ 'beads-hygiene': { primary: 'cx-operations', secondary: null },
18
+ 'hook-failures': { primary: 'cx-sre', secondary: null },
19
+ 'registry-warn': { primary: 'cx-platform-engineer', secondary: null },
20
+ 'consistency-drift': { primary: 'cx-platform-engineer', secondary: null },
21
+ 'propagation-stale': { primary: 'cx-platform-engineer', secondary: 'cx-docs-keeper' },
22
+ 'policy-coverage-gap': { primary: 'cx-architect', secondary: null },
23
+ 'workflow-misaligned': { primary: 'cx-product-manager', secondary: null },
24
+ 'legal-review-pending': { primary: 'cx-legal-compliance', secondary: null },
25
+ 'capability-unvalidated': { primary: 'cx-platform-engineer', secondary: null },
26
+ 'dead-code-regression': { primary: 'cx-architect', secondary: null },
27
+ 'structure-sprawl': { primary: 'cx-architect', secondary: null },
28
+ 'init-duplicate-lanes': { primary: 'cx-architect', secondary: null },
29
+ 'outcomes-missing': { primary: 'cx-data-engineer', secondary: null },
30
+ 'repo-layout-legacy': { primary: 'cx-platform-engineer', secondary: 'cx-architect' },
31
+ 'specialist-audit-drift': { primary: 'cx-architect', secondary: 'cx-docs-keeper' },
32
+ 'artifact-gate-bypass': { primary: 'cx-reviewer', secondary: 'cx-docs-keeper' },
33
+ 'artifact-reviewer-gap': { primary: 'cx-reviewer', secondary: null },
34
+ 'dependency-graph-stale': { primary: 'cx-platform-engineer', secondary: null },
35
+ 'matrix-coverage-gap': { primary: 'cx-architect', secondary: 'cx-qa' },
36
+ 'impact-untested': { primary: 'cx-qa', secondary: 'cx-engineer' },
37
+ };
38
+
39
+ const ACTION_ROUTES = {
40
+ 'specialist-review': { primary: 'cx-reviewer', gateType: 'specialist-dispatch' },
41
+ 'doctor-followup': { primary: 'cx-sre', gateType: 'operational-followup' },
42
+ 'trace-review': { primary: 'cx-trace-reviewer', gateType: 'specialist-dispatch' },
43
+ 'outcomes-aggregate': { primary: 'cx-data-engineer', gateType: 'maintenance' },
44
+ 'executive-signoff-required': { primary: 'cx-product-manager', gateType: 'executive-gate' },
45
+ 'structure-cleanup-proposal': { primary: 'cx-architect', gateType: 'human-approval' },
46
+ 'graph-rebuild': { primary: 'cx-platform-engineer', gateType: 'maintenance' },
47
+ };
48
+
49
+ /**
50
+ * @param {{ id?: string, signal?: string }} gap
51
+ * @returns {{ primary: string, secondary: string|null, gateType?: string }}
52
+ */
53
+ export function routeGap(gap) {
54
+ const id = gap?.id ?? gap?.signal ?? '';
55
+ const row = GAP_ROUTES[id];
56
+ if (row) return { ...row, gateType: signOffGateForGap(id) };
57
+ return { primary: 'cx-oracle', secondary: null, gateType: 'human-approval' };
58
+ }
59
+
60
+ /**
61
+ * @param {string} actionKind
62
+ */
63
+ export function routeAction(actionKind) {
64
+ return ACTION_ROUTES[actionKind] ?? { primary: 'cx-oracle', gateType: 'human-approval' };
65
+ }
66
+
67
+ function signOffGateForGap(gapId) {
68
+ if (gapId === 'legal-review-pending') return 'legal-compliance';
69
+ if (gapId === 'workflow-misaligned') return 'executive-gate';
70
+ if (gapId === 'capability-unvalidated') return 'capability-human-gate';
71
+ if (gapId === 'structure-sprawl' || gapId === 'dead-code-regression') return 'human-approval';
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Sign-off metadata attached to approve actions and high-severity gaps.
77
+ *
78
+ * @param {{ id: string, severity?: string }} gap
79
+ * @param {string} projectDir
80
+ */
81
+ export function signOffMetadata(gap, projectDir) {
82
+ const route = routeGap(gap);
83
+ return {
84
+ gateType: route.gateType ?? 'human-approval',
85
+ requiredApprover: route.primary,
86
+ artifactPath: `.cx/oracle/verdicts/`,
87
+ projectDir,
88
+ };
89
+ }
@@ -0,0 +1,384 @@
1
+ /**
2
+ * lib/oracle/synthesize.mjs — derive verdicts, gaps, and recommended actions
3
+ * from the Oracle read model. Pure function — no side effects.
4
+ */
5
+
6
+ import { orgGraphToGapHints } from './org-graph.mjs';
7
+ import { routeAction, routeGap, signOffMetadata } from './routing.mjs';
8
+
9
+ const LOW_SUCCESS_RATE = 0.6;
10
+
11
+ function gap(id, severity, signal, detail, extra = {}) {
12
+ return { id, severity, signal, detail, ...extra };
13
+ }
14
+
15
+ function action(kind, summary, context = {}) {
16
+ return { kind, summary, ...context };
17
+ }
18
+
19
+ function isDoctorEscalation(entry) {
20
+ return entry.result === 'escalated'
21
+ || entry.kind === 'escalation'
22
+ || entry.kind === 'escalate';
23
+ }
24
+
25
+ /**
26
+ * @param {object} readModel — output of collectReadModel / enrichReadModel
27
+ * @returns {{ verdict: string, gaps: object[], recommendedActions: object[] }}
28
+ */
29
+ export function synthesizeVerdict(readModel) {
30
+ const gaps = [];
31
+ const recommendedActions = [];
32
+
33
+ if (readModel.parity && !readModel.parity.skipped && !readModel.parity.ok) {
34
+ const drift = (readModel.parity.summary ?? []).filter((s) => s.includes('drift') || s.includes('stale'));
35
+ gaps.push(gap(
36
+ 'parity-drift',
37
+ 'high',
38
+ 'parity',
39
+ drift.length ? drift.join('; ') : 'Project adapter parity check failed',
40
+ ));
41
+ recommendedActions.push(action(
42
+ 'adapters-sync',
43
+ 'Sync project adapters to repair parity drift',
44
+ { surfaces: (readModel.parity.surfaces ?? []).filter((s) => s.status !== 'ok').map((s) => s.surface) },
45
+ ));
46
+ }
47
+
48
+ const violations = readModel.contractViolations?.recentCount ?? 0;
49
+ if (violations > 0) {
50
+ gaps.push(gap(
51
+ 'contract-violations',
52
+ violations >= 5 ? 'high' : 'medium',
53
+ 'contract-violations',
54
+ `${violations} contract violation(s) in the last 24h`,
55
+ ));
56
+ recommendedActions.push(action(
57
+ 'specialist-review',
58
+ 'Review recent contract violations and route remediation to owning specialists',
59
+ { count: violations, signOff: signOffMetadata({ id: 'contract-violations' }, readModel.projectDir) },
60
+ ));
61
+ }
62
+
63
+ const doctorEscalations = (readModel.doctorLog?.recent ?? []).filter(isDoctorEscalation);
64
+ if (doctorEscalations.length > 0) {
65
+ gaps.push(gap(
66
+ 'doctor-escalation',
67
+ 'high',
68
+ 'doctor-log',
69
+ `${doctorEscalations.length} doctor escalation(s) in the last 24h`,
70
+ ));
71
+ recommendedActions.push(action(
72
+ 'doctor-followup',
73
+ 'Inspect doctor escalations and confirm L0 recovery or open a beads issue',
74
+ { count: doctorEscalations.length, signOff: signOffMetadata({ id: 'doctor-escalation' }, readModel.projectDir) },
75
+ ));
76
+ }
77
+
78
+ const roles = readModel.outcomes?.roles ?? {};
79
+ const degradedRoles = Object.entries(roles)
80
+ .filter(([, stats]) => {
81
+ const rate = stats?.last30?.successRate ?? stats?.successRate;
82
+ return Number.isFinite(rate) && rate < LOW_SUCCESS_RATE && (stats?.last30?.count ?? stats?.count ?? 0) >= 3;
83
+ })
84
+ .map(([role]) => role);
85
+
86
+ if (degradedRoles.length > 0) {
87
+ gaps.push(gap(
88
+ 'outcomes-degradation',
89
+ 'medium',
90
+ 'outcomes',
91
+ `Low 30-day success rate for: ${degradedRoles.join(', ')}`,
92
+ ));
93
+ recommendedActions.push(action(
94
+ 'trace-review',
95
+ 'Dispatch cx-trace-reviewer to inspect degraded specialist outcomes',
96
+ { roles: degradedRoles, signOff: signOffMetadata({ id: 'outcomes-degradation' }, readModel.projectDir) },
97
+ ));
98
+ }
99
+
100
+ if (!readModel.outcomes?.present) {
101
+ gaps.push(gap(
102
+ 'outcomes-missing',
103
+ 'low',
104
+ 'outcomes',
105
+ 'No .cx/outcomes/_summary.json — learning tiebreakers are blind',
106
+ ));
107
+ recommendedActions.push(action(
108
+ 'outcomes-aggregate',
109
+ 'Run outcomes aggregation to populate _summary.json',
110
+ ));
111
+ }
112
+
113
+ const census = readModel.alignmentCensus;
114
+ if (census?.present === false) {
115
+ gaps.push(gap(
116
+ 'census-stale',
117
+ 'low',
118
+ 'alignment-census',
119
+ 'No audit-artifacts/alignment-census.json — alignment scorecard may be stale',
120
+ ));
121
+ recommendedActions.push(action(
122
+ 'census-run',
123
+ 'Run alignment census to refresh scorecard inputs',
124
+ ));
125
+ } else if (census?.present) {
126
+ if (census.stale) {
127
+ gaps.push(gap(
128
+ 'census-stale',
129
+ 'medium',
130
+ 'alignment-census',
131
+ `Alignment census older than 7 days (${census.generatedAt})`,
132
+ ));
133
+ recommendedActions.push(action('census-run', 'Refresh stale alignment census'));
134
+ }
135
+ const regressions = census.audit?.regressions ?? [];
136
+ if (regressions.length > 0) {
137
+ gaps.push(gap(
138
+ 'alignment-regression',
139
+ 'high',
140
+ 'alignment-census',
141
+ `${regressions.length} alignment ratchet regression(s): ${regressions.slice(0, 3).join(', ')}`,
142
+ ));
143
+ }
144
+ const findingsCount = census.audit?.findingsCount ?? 0;
145
+ if (findingsCount > 0 && regressions.length === 0) {
146
+ gaps.push(gap(
147
+ 'alignment-regression',
148
+ 'medium',
149
+ 'alignment-census',
150
+ `${findingsCount} open alignment audit finding(s)`,
151
+ ));
152
+ }
153
+ const trueOrphans = census.skills?.trueOrphanCount ?? 0;
154
+ if (trueOrphans > 0) {
155
+ gaps.push(gap(
156
+ 'true-skill-orphan',
157
+ 'medium',
158
+ 'alignment-census',
159
+ `${trueOrphans} true skill orphan(s) in census`,
160
+ ));
161
+ }
162
+ }
163
+
164
+ if (readModel.observations?.present && readModel.observations.count === 0) {
165
+ gaps.push(gap(
166
+ 'observations-empty',
167
+ 'low',
168
+ 'observations',
169
+ 'Observation store exists but contains no records',
170
+ ));
171
+ }
172
+
173
+ if (readModel.registryValidate?.needsRun) {
174
+ recommendedActions.push(action(
175
+ 'registry-validate',
176
+ 'Validate capability registry against repo reality',
177
+ { warnings: readModel.registryValidate.warningCount, errors: readModel.registryValidate.errorCount },
178
+ ));
179
+ }
180
+
181
+ if ((readModel.registryValidate?.warningCount ?? 0) > 0) {
182
+ gaps.push(gap(
183
+ 'registry-warn',
184
+ 'medium',
185
+ 'registry',
186
+ `${readModel.registryValidate.warningCount} capability registry warning(s)`,
187
+ ));
188
+ }
189
+
190
+ const hookFailures = readModel.hookFailures?.count ?? 0;
191
+ if (hookFailures > 0) {
192
+ gaps.push(gap(
193
+ 'hook-failures',
194
+ 'high',
195
+ 'doctor-log',
196
+ `${hookFailures} hook failure(s) in the last 24h`,
197
+ ));
198
+ }
199
+
200
+ const stuck = readModel.beads?.stuckInProgress ?? 0;
201
+ const staleOpen = readModel.beads?.staleOpen ?? 0;
202
+ if (stuck > 0 || staleOpen > 2) {
203
+ gaps.push(gap(
204
+ 'beads-hygiene',
205
+ stuck > 0 ? 'high' : 'medium',
206
+ 'beads',
207
+ `Beads hygiene: ${stuck} stuck in_progress, ${staleOpen} stale-open`,
208
+ ));
209
+ }
210
+
211
+ const deadRegressions = readModel.deadCode?.regressions ?? [];
212
+ if (deadRegressions.length > 0) {
213
+ gaps.push(gap(
214
+ 'dead-code-regression',
215
+ 'high',
216
+ 'dead-code',
217
+ `${deadRegressions.length} new dead module(s): ${deadRegressions.slice(0, 3).join(', ')}`,
218
+ ));
219
+ recommendedActions.push(action(
220
+ 'structure-cleanup-proposal',
221
+ 'Propose dead-code cleanup (no auto-delete)',
222
+ { modules: deadRegressions, signOff: signOffMetadata({ id: 'dead-code-regression' }, readModel.projectDir) },
223
+ ));
224
+ }
225
+
226
+ const dupLanes = readModel.structure?.duplicateLanes ?? [];
227
+ if (dupLanes.length > 0) {
228
+ gaps.push(gap(
229
+ 'structure-sprawl',
230
+ 'medium',
231
+ 'structure',
232
+ `Parallel doc lanes detected: ${dupLanes.join(', ')}`,
233
+ ));
234
+ recommendedActions.push(action(
235
+ 'structure-cleanup-proposal',
236
+ 'Review duplicate doc lane scaffolding',
237
+ { lanes: dupLanes, signOff: signOffMetadata({ id: 'structure-sprawl' }, readModel.projectDir) },
238
+ ));
239
+ }
240
+
241
+ const rootLayout = readModel.alignmentCensus?.rootLayout;
242
+ if (rootLayout && rootLayout.clean === false) {
243
+ const parts = [];
244
+ if (rootLayout.legacyDirs?.length) parts.push(`legacy dirs: ${rootLayout.legacyDirs.join(', ')}`);
245
+ if (rootLayout.phantomPackPaths?.length) parts.push(`phantom pack: ${rootLayout.phantomPackPaths.join(', ')}`);
246
+ if (rootLayout.legacyImports?.length) parts.push(`${rootLayout.legacyImports.length} legacy import(s)`);
247
+ gaps.push(gap(
248
+ 'repo-layout-legacy',
249
+ 'high',
250
+ 'root-layout',
251
+ `Tool-repo layout drift (${parts.join('; ') || `${rootLayout.findingCount} finding(s)`})`,
252
+ ));
253
+ recommendedActions.push(action(
254
+ 'structure-cleanup-proposal',
255
+ 'Remediate legacy root layout per audit phase 03c-root-layout',
256
+ { rootLayout, signOff: signOffMetadata({ id: 'repo-layout-legacy' }, readModel.projectDir) },
257
+ ));
258
+ }
259
+
260
+ const artifactGate = readModel.artifactGate ?? {};
261
+ if (artifactGate.specialistAudit?.present && !artifactGate.specialistAudit.pass) {
262
+ gaps.push(gap(
263
+ 'specialist-audit-drift',
264
+ 'high',
265
+ 'artifact-manifest',
266
+ `Specialist audit cross-check failed (${artifactGate.specialistAudit.crossCheckCount} issue(s))`,
267
+ ));
268
+ recommendedActions.push(action(
269
+ 'registry-validate',
270
+ 'Run construct audit specialists and repair manifest/registry drift',
271
+ ));
272
+ }
273
+
274
+ if ((artifactGate.bypassCount ?? 0) > 0) {
275
+ gaps.push(gap(
276
+ 'artifact-gate-bypass',
277
+ 'medium',
278
+ 'artifact-manifest',
279
+ `${artifactGate.bypassCount} artifact(s) bypass release gate via cx_release_gate frontmatter`,
280
+ ));
281
+ recommendedActions.push(action(
282
+ 'specialist-review',
283
+ 'Review bypassed artifacts and confirm cx_release_gate_reason is durable',
284
+ { paths: (artifactGate.bypassed ?? []).map((b) => b.path).slice(0, 5) },
285
+ ));
286
+ }
287
+
288
+ if ((artifactGate.reviewerGapCount ?? 0) > 0) {
289
+ gaps.push(gap(
290
+ 'artifact-reviewer-gap',
291
+ 'medium',
292
+ 'artifact-manifest',
293
+ `${artifactGate.reviewerGapCount} high-risk artifact(s) missing manifest required reviewers in agent log`,
294
+ ));
295
+ }
296
+
297
+ const dg = readModel.dependencyGraph;
298
+ if (dg?.present) {
299
+ if (dg.stale) {
300
+ gaps.push(gap(
301
+ 'dependency-graph-stale',
302
+ 'low',
303
+ 'dependency-graph',
304
+ `Dependency matrix stale (${dg.staleReason ?? 'seeds changed'}) — run \`construct matrix build\``,
305
+ ));
306
+ recommendedActions.push(action('graph-rebuild', 'Rebuild the dependency matrix to clear staleness'));
307
+ }
308
+
309
+ const cov = dg.coverage ?? {};
310
+ const noTest = cov.capabilitiesWithoutTest ?? [];
311
+ const noImpl = cov.capabilitiesWithoutImpl ?? [];
312
+ const uncovered = cov.workflowsUncovered ?? [];
313
+ const coverageMisses = noTest.length + noImpl.length + uncovered.length;
314
+ if (coverageMisses > 0) {
315
+ const parts = [];
316
+ if (noTest.length) parts.push(`${noTest.length} capability(ies) with no validating test`);
317
+ if (noImpl.length) parts.push(`${noImpl.length} with no implementation edge`);
318
+ if (uncovered.length) parts.push(`${uncovered.length} workflow(s) with no capability`);
319
+ gaps.push(gap(
320
+ 'matrix-coverage-gap',
321
+ coverageMisses >= 5 ? 'medium' : 'low',
322
+ 'dependency-graph',
323
+ `Dependency matrix coverage: ${parts.join('; ')}`,
324
+ ));
325
+ }
326
+
327
+ if (dg.untested?.length) {
328
+ gaps.push(gap(
329
+ 'impact-untested',
330
+ dg.untested.length >= 5 ? 'high' : 'medium',
331
+ 'dependency-graph',
332
+ `${dg.untested.length} capability(ies) have implementation changes since last validation: ${dg.untested.slice(0, 5).map((u) => u.capability).join(', ')}`,
333
+ { signOff: signOffMetadata({ id: 'impact-untested' }, readModel.projectDir) },
334
+ ));
335
+ recommendedActions.push(action(
336
+ 'specialist-review',
337
+ 'Re-run affected tests and refresh lastValidated for capabilities with changed implementation',
338
+ { capabilities: dg.untested.map((u) => u.capability) },
339
+ ));
340
+ }
341
+ }
342
+
343
+ for (const hint of orgGraphToGapHints(readModel.orgGraph ?? {})) {
344
+ if (!gaps.some((g) => g.id === hint.id)) gaps.push(hint);
345
+ if (hint.id === 'workflow-misaligned') {
346
+ recommendedActions.push(action(
347
+ 'executive-signoff-required',
348
+ 'Workflow alignment requires executive gate review',
349
+ { signOff: signOffMetadata(hint, readModel.projectDir) },
350
+ ));
351
+ }
352
+ if (hint.id === 'legal-review-pending') {
353
+ recommendedActions.push(action(
354
+ 'specialist-review',
355
+ 'Route legal-compliance intake for sign-off',
356
+ { signOff: signOffMetadata(hint, readModel.projectDir) },
357
+ ));
358
+ }
359
+ }
360
+
361
+ for (const g of gaps) {
362
+ const route = routeGap(g);
363
+ g.remediationRoute = {
364
+ primary: route.primary,
365
+ secondary: route.secondary ?? null,
366
+ gateType: route.gateType ?? null,
367
+ };
368
+ }
369
+ for (const a of recommendedActions) {
370
+ const route = routeAction(a.kind);
371
+ a.remediationRoute = {
372
+ primary: route.primary,
373
+ gateType: route.gateType ?? null,
374
+ };
375
+ }
376
+
377
+ let verdict = 'healthy';
378
+ if (gaps.some((g) => g.severity === 'high')) verdict = 'degraded';
379
+ else if (gaps.length > 0) verdict = 'attention';
380
+
381
+ return { verdict, gaps, recommendedActions };
382
+ }
383
+
384
+ export { isDoctorEscalation };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * lib/oracle/verdicts.mjs — durable per-tick verdict history under
3
+ * <project>/.cx/oracle/verdicts/.
4
+ */
5
+
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+
9
+ export function verdictsDir(projectDir) {
10
+ return path.join(projectDir, '.cx', 'oracle', 'verdicts');
11
+ }
12
+
13
+ function dateKey(iso = new Date().toISOString()) {
14
+ return iso.slice(0, 10);
15
+ }
16
+
17
+ /**
18
+ * Write one tick verdict. Same calendar day overwrites the latest snapshot for
19
+ * that date (last tick wins) while preserving tick history in an array.
20
+ *
21
+ * @param {string} projectDir
22
+ * @param {object} tick — runOracleTick tick payload
23
+ * @param {object} [extra] — beadsRaised, orgGraph, etc.
24
+ */
25
+ export function writeVerdict(projectDir, tick, extra = {}) {
26
+ const dir = verdictsDir(projectDir);
27
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
28
+ const file = path.join(dir, `${dateKey(tick.at)}.json`);
29
+ let existing = { date: dateKey(tick.at), ticks: [] };
30
+ if (fs.existsSync(file)) {
31
+ try { existing = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { /* reset */ }
32
+ }
33
+ if (!Array.isArray(existing.ticks)) existing.ticks = [];
34
+ existing.ticks.push({ ...tick, ...extra });
35
+ existing.latest = existing.ticks[existing.ticks.length - 1];
36
+ fs.writeFileSync(file, JSON.stringify(existing, null, 2));
37
+ return file;
38
+ }
39
+
40
+ export function readLatestVerdict(projectDir) {
41
+ const dir = verdictsDir(projectDir);
42
+ if (!fs.existsSync(dir)) return null;
43
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
44
+ if (!files.length) return null;
45
+ try {
46
+ const data = JSON.parse(fs.readFileSync(path.join(dir, files[files.length - 1]), 'utf8'));
47
+ return data.latest ?? data;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
@@ -23,8 +23,8 @@
23
23
 
24
24
  import { readFileSync, existsSync } from 'node:fs';
25
25
  import { dirname, join } from 'node:path';
26
- import { homedir } from 'node:os';
27
26
  import { fileURLToPath } from 'node:url';
27
+ import { resolveSecret } from '../providers/secret-resolver.mjs';
28
28
 
29
29
  export const INLINE = 'inline';
30
30
  export const PROVIDER = 'provider';
@@ -52,6 +52,33 @@ function isAnthropic(provider, model) {
52
52
  return /anthropic|claude/i.test(provider || '') || /claude/i.test(model || '');
53
53
  }
54
54
 
55
+ // Provider family for execution dispatch. The explicit `provider` argument is
56
+ // authoritative — orchestration model ids are OpenRouter-style slugs (for
57
+ // example `openai/gpt-4o-mini` is a model served *through* OpenRouter), so the
58
+ // direct OpenAI endpoint is taken only when the caller names `openai`. Copilot
59
+ // is resolved separately because it authenticates via the device-flow token
60
+ // store rather than an API key; everything unrecognized stays on OpenRouter.
61
+
62
+ function classifyWorkerProvider(provider, model) {
63
+ const p = (provider || '').toLowerCase();
64
+ if (p === 'github-copilot' || /^github-copilot\//.test(model || '')) return 'github-copilot';
65
+ if (p === 'openai') return 'openai';
66
+ if (isAnthropic(provider, model)) return 'anthropic';
67
+ return 'openrouter';
68
+ }
69
+
70
+ const WORKER_KEY_VAR = {
71
+ anthropic: 'ANTHROPIC_API_KEY',
72
+ openai: 'OPENAI_API_KEY',
73
+ openrouter: 'OPENROUTER_API_KEY',
74
+ };
75
+
76
+ const WORKER_PROVIDER_LABEL = {
77
+ anthropic: 'Anthropic',
78
+ openai: 'OpenAI',
79
+ openrouter: 'OpenRouter',
80
+ };
81
+
55
82
  // Extended-thinking shape is model-specific. Opus 4.8 / 4.7 REJECT the legacy
56
83
  // `{type:'enabled', budget_tokens}` form with a 400 and require adaptive
57
84
  // thinking; Opus 4.6 and Sonnet 4.6 recommend adaptive too. Only genuinely
@@ -66,23 +93,14 @@ function anthropicThinkingConfig(model) {
66
93
  : { type: 'enabled', budget_tokens: REASONING_BUDGET_TOKENS };
67
94
  }
68
95
 
69
- // Key resolution mirrors provider-extract: env first, then the two cheap dotenv
70
- // files, and only when the caller did not inject an explicit env. A caller that
71
- // passes its own env object (embedded callers, tests) is authoritative and
72
- // hermetic ambient discovery is suppressed so a developer's real key never
73
- // bleeds into a test run.
96
+ // Key resolution delegates to the shared secret resolver so env, the dotenv
97
+ // files, shell rc exports, and 1Password op:// references all behave identically
98
+ // across the worker, the chat engine, and the router. allowAmbient stays the
99
+ // hermetic switch: a caller that injects its own env (embedded callers, tests)
100
+ // suppresses file/rc discovery so a developer's real key never bleeds into a run.
74
101
 
75
102
  function resolveKey(varName, env, allowAmbient) {
76
- if (env[varName] && typeof env[varName] === 'string' && env[varName].length > 0) return env[varName];
77
- if (!allowAmbient) return null;
78
- for (const file of [join(homedir(), '.construct', 'config.env'), join(homedir(), '.env')]) {
79
- try {
80
- if (!existsSync(file)) continue;
81
- const m = readFileSync(file, 'utf8').match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
82
- if (m && m[1]) return m[1].trim();
83
- } catch { /* unreadable dotenv is not authoritative */ }
84
- }
85
- return null;
103
+ return resolveSecret(varName, { env, allowAmbient });
86
104
  }
87
105
 
88
106
  // The persona prompt is the specialist's system context. It lives in
@@ -171,6 +189,42 @@ async function callOpenRouter({ model, apiKey, system, user, fetchImpl, reasonin
171
189
  return { output: message.content || '', reasoning: reasoning ? extractOpenRouterReasoning(message) : '' };
172
190
  }
173
191
 
192
+ async function callCopilot({ model, system, user, fetchImpl }) {
193
+ const { getCopilotToken, copilotApiHeaders, COPILOT_API_BASE } = await import('../providers/copilot-auth.mjs');
194
+ const token = await getCopilotToken({ fetchImpl });
195
+ const res = await fetchImpl(`${COPILOT_API_BASE}/chat/completions`, {
196
+ method: 'POST',
197
+ headers: { Authorization: `Bearer ${token}`, 'content-type': 'application/json', ...copilotApiHeaders() },
198
+ body: JSON.stringify({
199
+ model: model.replace(/^github-copilot\//, ''),
200
+ messages: [{ role: 'system', content: system }, { role: 'user', content: user }],
201
+ }),
202
+ });
203
+ if (!res.ok) {
204
+ throw providerError('PROVIDER_EXECUTION_FAILED', `GitHub Copilot specialist execution failed (HTTP ${res.status}): ${await bodyExcerpt(res)}`, 'Run `construct creds login copilot`, or set orchestration.workerBackend to "inline".');
205
+ }
206
+ const data = await res.json();
207
+ const message = data.choices?.[0]?.message || {};
208
+ return { output: message.content || '', reasoning: '' };
209
+ }
210
+
211
+ async function callOpenAI({ model, apiKey, system, user, fetchImpl }) {
212
+ const res = await fetchImpl('https://api.openai.com/v1/chat/completions', {
213
+ method: 'POST',
214
+ headers: { Authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
215
+ body: JSON.stringify({
216
+ model: model.replace(/^openai\//, ''),
217
+ messages: [{ role: 'system', content: system }, { role: 'user', content: user }],
218
+ }),
219
+ });
220
+ if (!res.ok) {
221
+ throw providerError('PROVIDER_EXECUTION_FAILED', `OpenAI specialist execution failed (HTTP ${res.status}): ${await bodyExcerpt(res)}`, 'Verify the model id and OPENAI_API_KEY, or set orchestration.workerBackend to "inline".');
222
+ }
223
+ const data = await res.json();
224
+ const message = data.choices?.[0]?.message || {};
225
+ return { output: message.content || '', reasoning: '' };
226
+ }
227
+
174
228
  /**
175
229
  * Execute one specialist task via the configured provider/model.
176
230
  *
@@ -190,26 +244,33 @@ export async function runTaskViaProvider({ task, run, model, provider = null, en
190
244
  if (!model) throw providerError('PROVIDER_MODEL_UNRESOLVED', 'Provider worker backend selected but no model resolved.', 'Configure the model tier registry so a model resolves, or set orchestration.workerBackend to "inline".');
191
245
  if (typeof fetchImpl !== 'function') throw providerError('PROVIDER_NO_FETCH', 'No fetch implementation available for provider execution.', 'Run on a runtime with global fetch (Node 18+) or inject fetchImpl.');
192
246
 
193
- const anthropic = isAnthropic(provider, model);
194
- const keyVar = anthropic ? 'ANTHROPIC_API_KEY' : 'OPENROUTER_API_KEY';
195
- const apiKey = resolveKey(keyVar, env, env === process.env);
196
- if (!apiKey) {
197
- throw providerError('PROVIDER_KEY_MISSING', `No API key for ${anthropic ? 'Anthropic' : 'OpenRouter'} specialist execution.`, `Set ${keyVar}, or set orchestration.workerBackend to "inline".`);
198
- }
199
-
247
+ const family = classifyWorkerProvider(provider, model);
200
248
  const system = loadPersona(task?.role);
201
249
  const user = buildUserPrompt({ task, run });
202
250
  const wantsReasoning = chainOfThought !== 'hidden';
203
- const { output, reasoning } = anthropic
204
- ? await callAnthropic({ model, apiKey, system, user, fetchImpl, reasoning: wantsReasoning })
205
- : await callOpenRouter({ model, apiKey, system, user, fetchImpl, reasoning: wantsReasoning });
251
+
252
+ let output;
253
+ let reasoning;
254
+ if (family === 'github-copilot') {
255
+ ({ output, reasoning } = await callCopilot({ model, system, user, fetchImpl }));
256
+ } else {
257
+ const keyVar = WORKER_KEY_VAR[family];
258
+ const apiKey = resolveKey(keyVar, env, env === process.env);
259
+ if (!apiKey) {
260
+ throw providerError('PROVIDER_KEY_MISSING', `No API key for ${WORKER_PROVIDER_LABEL[family]} specialist execution.`, `Set ${keyVar} (a value or an op:// reference), or set orchestration.workerBackend to "inline".`);
261
+ }
262
+ const call = family === 'anthropic' ? callAnthropic
263
+ : family === 'openai' ? callOpenAI
264
+ : callOpenRouter;
265
+ ({ output, reasoning } = await call({ model, apiKey, system, user, fetchImpl, reasoning: wantsReasoning }));
266
+ }
206
267
 
207
268
  const text = output || '';
208
269
  return {
209
270
  output: text,
210
271
  reasoning: wantsReasoning ? (reasoning || '') : '',
211
272
  model,
212
- provider: anthropic ? 'anthropic' : 'openrouter',
273
+ provider: family,
213
274
  characters: text.length,
214
275
  };
215
276
  }