@geraldmaron/construct 1.4.1 → 1.5.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 (556) hide show
  1. package/.env.example +36 -0
  2. package/README.md +41 -7
  3. package/bin/construct +1029 -65
  4. package/bin/construct-postinstall.mjs +27 -2
  5. package/config/tag-vocabulary.json +264 -0
  6. package/examples/distribution/sources/deck-one-pager.md +1 -1
  7. package/lib/acp/server.mjs +21 -7
  8. package/lib/adapters-sync.mjs +2 -1
  9. package/lib/audit-trail.mjs +185 -2
  10. package/lib/beads/auto-close.mjs +2 -1
  11. package/lib/beads/drift.mjs +2 -1
  12. package/lib/bridges/copilot-proxy.mjs +20 -5
  13. package/lib/certification/skill-inventory.mjs +10 -1
  14. package/lib/cli/approvals.mjs +147 -0
  15. package/lib/cli-commands.mjs +105 -14
  16. package/lib/comment-lint.mjs +127 -8
  17. package/lib/config/schema.mjs +7 -30
  18. package/lib/config/source-target-registry.mjs +70 -0
  19. package/lib/config/source-targets.mjs +179 -205
  20. package/lib/contracts/coverage.mjs +77 -0
  21. package/lib/contracts/validate.mjs +102 -13
  22. package/lib/contracts/violation-log.mjs +50 -4
  23. package/lib/db/migrate.mjs +69 -0
  24. package/lib/db/migrations/001_orchestration_runs.sql +9 -0
  25. package/lib/db/migrations/002_queue_provider.sql +50 -0
  26. package/lib/db/migrations/003_worker_registry.sql +17 -0
  27. package/lib/db/migrations/004_trace_events.sql +16 -0
  28. package/lib/db/migrations/005_shared_memory.sql +15 -0
  29. package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
  30. package/lib/decisions/enforced-baseline.json +0 -2
  31. package/lib/decisions/registry.mjs +3 -2
  32. package/lib/deployment/parity-contract.mjs +1 -1
  33. package/lib/deployment-mode.mjs +78 -2
  34. package/lib/diagram-export.mjs +10 -1
  35. package/lib/distill.mjs +5 -2
  36. package/lib/docs-verify.mjs +2 -1
  37. package/lib/doctor/cli.mjs +1 -0
  38. package/lib/doctor/command-on-path.mjs +24 -0
  39. package/lib/doctor/diagnosis.mjs +109 -0
  40. package/lib/doctor/embedding-health.mjs +50 -0
  41. package/lib/doctor/engine-health.mjs +93 -0
  42. package/lib/doctor/graph-validate.mjs +47 -0
  43. package/lib/doctor/index.mjs +23 -4
  44. package/lib/doctor/sidecar-providers.mjs +56 -0
  45. package/lib/doctor/watchers/consistency.mjs +93 -1
  46. package/lib/doctor/watchers/cx-budget.mjs +1 -1
  47. package/lib/doctor/watchers/graph-staleness.mjs +1 -1
  48. package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
  49. package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
  50. package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
  51. package/lib/doctor/watchers/provider-breaker.mjs +78 -0
  52. package/lib/document-export.mjs +52 -27
  53. package/lib/document-extract/docling-client.mjs +9 -5
  54. package/lib/document-extract/docling-sidecar.py +30 -0
  55. package/lib/document-extract.mjs +1 -1
  56. package/lib/document-ingest.mjs +9 -0
  57. package/lib/embed/approval-queue.mjs +184 -88
  58. package/lib/embed/authority-guard.mjs +65 -2
  59. package/lib/embed/capability-jobs.mjs +365 -0
  60. package/lib/embed/capability-lifecycle.mjs +219 -0
  61. package/lib/embed/capability-loader.mjs +303 -0
  62. package/lib/embed/capability-runtime.mjs +58 -0
  63. package/lib/embed/cli.mjs +185 -8
  64. package/lib/embed/config.mjs +4 -0
  65. package/lib/embed/daemon.mjs +125 -6
  66. package/lib/embed/demand-fetch.mjs +190 -54
  67. package/lib/embed/inbox.mjs +12 -10
  68. package/lib/embed/presets/ops-triage.mjs +236 -0
  69. package/lib/embed/presets/pm-feedback.mjs +341 -0
  70. package/lib/embed/presets/tpm.mjs +401 -0
  71. package/lib/embed/providers/jira.mjs +72 -1
  72. package/lib/embed/providers/registry.mjs +140 -33
  73. package/lib/embed/worker.mjs +5 -0
  74. package/lib/embedded-contract/capability.mjs +4 -0
  75. package/lib/embedded-contract/execution.mjs +1 -1
  76. package/lib/embedded-contract/model-resolve.mjs +53 -3
  77. package/lib/embedded-contract/workflow-defs.mjs +48 -73
  78. package/lib/embedded-contract/workflow-invoke.mjs +144 -4
  79. package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
  80. package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
  81. package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
  82. package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
  83. package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
  84. package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
  85. package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
  86. package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
  87. package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
  88. package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
  89. package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
  90. package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
  91. package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
  92. package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
  93. package/lib/env-config.mjs +60 -11
  94. package/lib/export-validate.mjs +34 -2
  95. package/lib/extensions/index.mjs +27 -0
  96. package/lib/extensions/loader.mjs +120 -0
  97. package/lib/extensions/manifest-schema.mjs +141 -0
  98. package/lib/extensions/manifests/anthropic.manifest.json +12 -0
  99. package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
  100. package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
  101. package/lib/extensions/manifests/directory.manifest.json +12 -0
  102. package/lib/extensions/manifests/docling.manifest.json +37 -0
  103. package/lib/extensions/manifests/echo.manifest.json +8 -0
  104. package/lib/extensions/manifests/feedback.manifest.json +12 -0
  105. package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
  106. package/lib/extensions/manifests/github.manifest.json +52 -0
  107. package/lib/extensions/manifests/linear.manifest.json +50 -0
  108. package/lib/extensions/manifests/local.manifest.json +12 -0
  109. package/lib/extensions/manifests/ollama.manifest.json +12 -0
  110. package/lib/extensions/manifests/openai.manifest.json +12 -0
  111. package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
  112. package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
  113. package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
  114. package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
  115. package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
  116. package/lib/extensions/manifests/openrouter.manifest.json +12 -0
  117. package/lib/extensions/manifests/postgres.manifest.json +12 -0
  118. package/lib/extensions/manifests/salesforce.manifest.json +12 -0
  119. package/lib/extensions/manifests/slack.manifest.json +58 -0
  120. package/lib/extensions/manifests/whisper.manifest.json +36 -0
  121. package/lib/extensions/validate.mjs +175 -0
  122. package/lib/features.mjs +13 -9
  123. package/lib/flows/checkpoint.mjs +184 -0
  124. package/lib/flows/constants.mjs +27 -0
  125. package/lib/flows/define.mjs +146 -0
  126. package/lib/flows/engine.mjs +249 -0
  127. package/lib/flows/errors.mjs +19 -0
  128. package/lib/flows/index.mjs +27 -0
  129. package/lib/flows/joins.mjs +18 -0
  130. package/lib/flows/schema.mjs +90 -0
  131. package/lib/flows/state.mjs +31 -0
  132. package/lib/frameworks/loader.mjs +99 -0
  133. package/lib/frameworks/schema.mjs +157 -0
  134. package/lib/graph/build-from-corpus.mjs +67 -0
  135. package/lib/graph/build-from-embed.mjs +82 -0
  136. package/lib/graph/build-from-registry.mjs +164 -3
  137. package/lib/graph/cli.mjs +456 -12
  138. package/lib/graph/gap-queries.mjs +156 -0
  139. package/lib/graph/gaps.mjs +41 -0
  140. package/lib/graph/impacted.mjs +129 -0
  141. package/lib/graph/runtime-evidence.mjs +177 -0
  142. package/lib/graph/security-coverage.mjs +113 -0
  143. package/lib/graph/staleness.mjs +241 -10
  144. package/lib/graph/store.mjs +24 -7
  145. package/lib/graph/validate.mjs +161 -0
  146. package/lib/headhunt.mjs +9 -8
  147. package/lib/health-check.mjs +15 -7
  148. package/lib/hook-health.mjs +1 -1
  149. package/lib/hooks/agent-tracker.mjs +10 -4
  150. package/lib/hooks/edit-guard.mjs +3 -3
  151. package/lib/hooks/graph-impact-advisory.mjs +2 -2
  152. package/lib/hooks/guard-bash.mjs +41 -15
  153. package/lib/hooks/mcp-health-check.mjs +11 -4
  154. package/lib/hooks/model-fallback.mjs +50 -6
  155. package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
  156. package/lib/hooks/pre-compact.mjs +181 -173
  157. package/lib/hooks/rule-verifier.mjs +2 -1
  158. package/lib/hooks/session-reflect.mjs +2 -1
  159. package/lib/hooks/session-start.mjs +3 -3
  160. package/lib/host/readiness.mjs +109 -0
  161. package/lib/host-capabilities.mjs +13 -2
  162. package/lib/host-disposition.mjs +29 -8
  163. package/lib/identity.mjs +92 -0
  164. package/lib/ingest/degraded-extract.mjs +96 -0
  165. package/lib/ingest/docling-remote.mjs +2 -1
  166. package/lib/ingest/provider-extract.mjs +7 -19
  167. package/lib/ingest/sidecar-providers.mjs +196 -0
  168. package/lib/ingest-tooling.mjs +11 -5
  169. package/lib/init-unified.mjs +55 -38
  170. package/lib/init-update.mjs +2 -1
  171. package/lib/install/legacy-global-cleanup.mjs +25 -0
  172. package/lib/install/stage-project.mjs +41 -4
  173. package/lib/intake/daemon.mjs +99 -8
  174. package/lib/intake/git-queue.mjs +110 -38
  175. package/lib/intake/prepare.mjs +2 -0
  176. package/lib/intake/queue-registry.mjs +50 -0
  177. package/lib/intake/queue.mjs +133 -17
  178. package/lib/intake/session-prelude.mjs +57 -8
  179. package/lib/integrations/intake-integrations.mjs +61 -111
  180. package/lib/intent-classifier.mjs +43 -5
  181. package/lib/libreoffice-export.mjs +8 -8
  182. package/lib/logging/rotate.mjs +5 -4
  183. package/lib/mcp/broker.mjs +332 -17
  184. package/lib/mcp/denied-store.mjs +95 -0
  185. package/lib/mcp/destructive-approval.mjs +57 -0
  186. package/lib/mcp/destructive-gate.mjs +30 -0
  187. package/lib/mcp/dispatch-envelope.mjs +199 -0
  188. package/lib/mcp/memory-bridge.mjs +2 -1
  189. package/lib/mcp/server.mjs +185 -1268
  190. package/lib/mcp/tool-definitions-memory.mjs +376 -0
  191. package/lib/mcp/tool-definitions-project.mjs +271 -0
  192. package/lib/mcp/tool-definitions-skills.mjs +311 -0
  193. package/lib/mcp/tool-definitions-workflow.mjs +398 -0
  194. package/lib/mcp/tool-definitions.mjs +25 -0
  195. package/lib/mcp/tool-rate-limit.mjs +47 -0
  196. package/lib/mcp/tool-registry.mjs +107 -0
  197. package/lib/mcp/tool-safety.mjs +95 -0
  198. package/lib/mcp/tool-surface-parity.mjs +60 -0
  199. package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
  200. package/lib/mcp/tools/orchestration-run.mjs +197 -19
  201. package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
  202. package/lib/mcp/tools/project.mjs +25 -8
  203. package/lib/mcp/tools/provider-write.mjs +187 -0
  204. package/lib/mcp/tools/scope.mjs +0 -3
  205. package/lib/mcp/tools/skills.mjs +1 -1
  206. package/lib/mcp/tools/storage.mjs +2 -2
  207. package/lib/mcp/tools/web-search-governance.mjs +96 -0
  208. package/lib/mcp/tools/web-search.mjs +5 -76
  209. package/lib/mcp/transport/auth.mjs +238 -0
  210. package/lib/mcp/transport/http.mjs +136 -0
  211. package/lib/mcp/transport/mode.mjs +31 -0
  212. package/lib/mcp/transport/stdio.mjs +22 -0
  213. package/lib/mcp-catalog.json +4 -4
  214. package/lib/mcp-manager.mjs +34 -23
  215. package/lib/mcp-platform-config.mjs +53 -20
  216. package/lib/mode-capabilities.mjs +109 -0
  217. package/lib/model-router.mjs +42 -9
  218. package/lib/models/catalog.mjs +40 -1
  219. package/lib/net-guard.mjs +247 -0
  220. package/lib/observation-store.mjs +6 -2
  221. package/lib/ollama/provision-context.mjs +2 -1
  222. package/lib/opencode-config.mjs +22 -3
  223. package/lib/opencode-runtime-plugin.mjs +120 -2
  224. package/lib/oracle/actions.mjs +204 -10
  225. package/lib/oracle/cli.mjs +24 -3
  226. package/lib/oracle/daemon-entry.mjs +6 -0
  227. package/lib/oracle/dispatch.mjs +2 -1
  228. package/lib/oracle/execute.mjs +2 -2
  229. package/lib/oracle/gaps.mjs +3 -3
  230. package/lib/oracle/heartbeat.mjs +53 -0
  231. package/lib/oracle/read-model.mjs +69 -13
  232. package/lib/oracle/reconcile.mjs +3 -46
  233. package/lib/oracle/routing.mjs +37 -31
  234. package/lib/oracle/synthesize.mjs +1 -1
  235. package/lib/orchestration/classification.mjs +434 -0
  236. package/lib/orchestration/delegation-flow.mjs +132 -0
  237. package/lib/orchestration/flow-selection.mjs +418 -0
  238. package/lib/orchestration/gates.mjs +244 -0
  239. package/lib/orchestration/host-sampling.mjs +121 -0
  240. package/lib/orchestration/policy-constants.mjs +48 -0
  241. package/lib/orchestration/provider-outcome.mjs +197 -0
  242. package/lib/orchestration/readiness.mjs +114 -13
  243. package/lib/orchestration/run-store-postgres.mjs +32 -26
  244. package/lib/orchestration/run-store-sqlite.mjs +8 -14
  245. package/lib/orchestration/run-store.mjs +25 -12
  246. package/lib/orchestration/runtime.mjs +508 -62
  247. package/lib/orchestration/store.mjs +87 -12
  248. package/lib/orchestration/trace-store.mjs +125 -0
  249. package/lib/orchestration/web-capability.mjs +60 -0
  250. package/lib/orchestration/worker-runtime.mjs +162 -0
  251. package/lib/orchestration/worker.mjs +763 -80
  252. package/lib/orchestration-policy.mjs +67 -1111
  253. package/lib/output-quality.mjs +61 -2
  254. package/lib/packs/cli.mjs +144 -0
  255. package/lib/packs/core-pack.mjs +112 -0
  256. package/lib/packs/enablement.mjs +187 -0
  257. package/lib/packs/index.mjs +23 -0
  258. package/lib/packs/loader.mjs +176 -0
  259. package/lib/packs/manifest-schema.mjs +58 -0
  260. package/lib/packs/prompts.mjs +120 -0
  261. package/lib/packs/validate.mjs +208 -0
  262. package/lib/path-policy.mjs +56 -0
  263. package/lib/plugin-registry.mjs +4 -5
  264. package/lib/policy/audit-gate.mjs +42 -0
  265. package/lib/policy/consumption-budget.mjs +149 -0
  266. package/lib/policy/engine.mjs +81 -6
  267. package/lib/policy/role-authority.mjs +89 -0
  268. package/lib/prompt-composer.js +19 -3
  269. package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
  270. package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
  271. package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
  272. package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
  273. package/lib/providers/contract/adapters/github/index.mjs +38 -3
  274. package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
  275. package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
  276. package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
  277. package/lib/providers/contract/adapters/jira/manifest.json +17 -0
  278. package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
  279. package/lib/providers/contract.mjs +207 -0
  280. package/lib/providers/credential-bootstrap.mjs +7 -4
  281. package/lib/providers/credential-sources.mjs +14 -2
  282. package/lib/providers/directory/index.mjs +261 -0
  283. package/lib/providers/feedback/index.mjs +392 -0
  284. package/lib/providers/filter-audit.mjs +68 -0
  285. package/lib/providers/filter-schema.mjs +54 -0
  286. package/lib/providers/github/index.mjs +31 -0
  287. package/lib/providers/instance-config.mjs +215 -0
  288. package/lib/providers/op-locate.mjs +96 -0
  289. package/lib/providers/op-run.mjs +64 -9
  290. package/lib/providers/registry.mjs +26 -3
  291. package/lib/providers/secret-audit-wiring.mjs +41 -18
  292. package/lib/providers/secret-resolver.mjs +263 -31
  293. package/lib/publish-template.mjs +6 -3
  294. package/lib/publish-tooling.mjs +4 -0
  295. package/lib/publish.mjs +32 -4
  296. package/lib/queue/pg-queue.mjs +395 -0
  297. package/lib/reflect.mjs +2 -1
  298. package/lib/registry/assemble.mjs +28 -2
  299. package/lib/registry/catalog.mjs +6 -0
  300. package/lib/registry/consolidation.mjs +8 -1
  301. package/lib/registry/custom-scaffold.mjs +200 -0
  302. package/lib/registry/custom-schema.mjs +137 -0
  303. package/lib/registry/loader.mjs +15 -4
  304. package/lib/registry/manifests/format-engines.default.json +15 -0
  305. package/lib/registry/manifests/surface-map.default.json +46 -0
  306. package/lib/registry/retired-paths.mjs +1 -0
  307. package/lib/registry/surface-map.mjs +100 -50
  308. package/lib/registry/validate.mjs +3 -3
  309. package/lib/render-visual-check.mjs +240 -0
  310. package/lib/resources/budget.mjs +40 -17
  311. package/lib/roles/flavor-bindings.mjs +36 -14
  312. package/lib/roles/gateway.mjs +15 -8
  313. package/lib/roots.mjs +107 -0
  314. package/lib/runtime/uv-bootstrap.mjs +32 -6
  315. package/lib/runtime/whisper-bootstrap.mjs +11 -3
  316. package/lib/runtime-env.mjs +5 -2
  317. package/lib/sandbox.mjs +1 -1
  318. package/lib/scheduler/index.mjs +8 -20
  319. package/lib/schema-infer.mjs +31 -2
  320. package/lib/scopes/lifecycle.mjs +29 -25
  321. package/lib/scopes/loader.mjs +49 -12
  322. package/lib/scopes/teams.mjs +1 -1
  323. package/lib/security/ingest-boundary.mjs +80 -0
  324. package/lib/security/recall-wrapper.mjs +99 -0
  325. package/lib/security/trust.mjs +132 -0
  326. package/lib/service-manager.mjs +93 -24
  327. package/lib/setup.mjs +41 -23
  328. package/lib/skills-apply.mjs +4 -2
  329. package/lib/specialists/postconditions.mjs +2 -2
  330. package/lib/state-root.mjs +147 -0
  331. package/lib/status.mjs +597 -6
  332. package/lib/storage/admin.mjs +84 -7
  333. package/lib/storage/backend-registry.mjs +73 -0
  334. package/lib/storage/backend.mjs +63 -9
  335. package/lib/storage/embeddings-openai.mjs +12 -2
  336. package/lib/storage/hybrid-query.mjs +11 -3
  337. package/lib/storage/retrieval-hardening.mjs +384 -0
  338. package/lib/storage/shared-memory.mjs +158 -0
  339. package/lib/storage/vector-client.mjs +69 -2
  340. package/lib/team/health.mjs +72 -0
  341. package/lib/telemetry/backends/local.mjs +9 -3
  342. package/lib/telemetry/client.mjs +3 -7
  343. package/lib/template-registry.mjs +10 -12
  344. package/lib/tenant/context.mjs +82 -0
  345. package/lib/tenant/isolation.mjs +129 -0
  346. package/lib/test-corpus-inventory.mjs +103 -2
  347. package/lib/uninstall/uninstall.mjs +78 -12
  348. package/lib/validator.mjs +4 -6
  349. package/lib/worker/entrypoint.mjs +2 -1
  350. package/lib/worker/run.mjs +2 -2
  351. package/lib/worker/trace.mjs +5 -11
  352. package/lib/workflow-state.mjs +52 -8
  353. package/lib/workflows/liveness.mjs +203 -0
  354. package/lib/workflows/loader.mjs +177 -0
  355. package/lib/workflows/manifest-schema.mjs +71 -0
  356. package/lib/workflows/surface-parity.mjs +118 -0
  357. package/lib/workflows/validate.mjs +127 -0
  358. package/lib/writes/control-plane.mjs +140 -0
  359. package/lib/writes/envelope.mjs +206 -0
  360. package/lib/writes/sent-log.mjs +86 -0
  361. package/lib/writes/write-intent.mjs +109 -0
  362. package/package.json +20 -7
  363. package/personas/construct.md +12 -3
  364. package/registry/agent-manifest.json +117 -0
  365. package/registry/capabilities.json +1987 -0
  366. package/rules/common/comments.md +1 -0
  367. package/schemas/brand-voice.schema.json +24 -0
  368. package/schemas/capability-registry.schema.json +72 -0
  369. package/schemas/certification-run.schema.json +130 -0
  370. package/schemas/demo-recording.schema.json +46 -0
  371. package/schemas/eval-dataset.schema.json +79 -0
  372. package/schemas/execution-capability-profile.schema.json +46 -0
  373. package/schemas/execution-policy.schema.json +114 -0
  374. package/schemas/improvement-proposal.schema.json +65 -0
  375. package/schemas/mcp-tool-output.schema.json +61 -0
  376. package/schemas/platform-capabilities.schema.json +83 -0
  377. package/schemas/project-config.schema.json +215 -0
  378. package/schemas/project-demo.schema.json +60 -0
  379. package/schemas/provider-behavior-matrix.schema.json +91 -0
  380. package/schemas/scope.schema.json +197 -0
  381. package/schemas/specialist-trace.schema.json +107 -0
  382. package/schemas/team.schema.json +99 -0
  383. package/schemas/unified-registry.schema.json +546 -0
  384. package/scripts/sync-specialists.mjs +336 -106
  385. package/skills/docs/adr-workflow.md +1 -1
  386. package/skills/docs/codebase-research-workflow.md +3 -3
  387. package/skills/docs/prd-workflow.md +5 -6
  388. package/skills/docs/runbook-workflow.md +2 -2
  389. package/skills/docs/user-research-workflow.md +3 -3
  390. package/skills/operating/fleet-health-routing.md +77 -0
  391. package/skills/roles/ai-engineer.md +1 -1
  392. package/skills/roles/architect.md +0 -1
  393. package/skills/roles/business-strategist.md +1 -1
  394. package/skills/roles/data-analyst.telemetry.md +1 -1
  395. package/skills/roles/data-engineer.md +1 -1
  396. package/skills/roles/data-engineer.pipeline.md +1 -1
  397. package/skills/roles/data-engineer.vector-retrieval.md +1 -2
  398. package/skills/roles/data-engineer.warehouse.md +1 -1
  399. package/skills/roles/designer.accessibility.md +1 -1
  400. package/skills/roles/designer.md +0 -1
  401. package/skills/roles/devil-advocate.md +1 -1
  402. package/skills/roles/docs-keeper.md +1 -1
  403. package/skills/roles/evaluator.md +1 -1
  404. package/skills/roles/explorer.md +1 -1
  405. package/skills/roles/platform-engineer.md +1 -1
  406. package/skills/roles/qa.ai-eval.md +1 -2
  407. package/skills/roles/qa.api-contract.md +0 -1
  408. package/skills/roles/qa.data-pipeline.md +0 -1
  409. package/skills/roles/qa.md +0 -1
  410. package/skills/roles/qa.web-ui.md +0 -1
  411. package/skills/roles/release-manager.md +1 -1
  412. package/skills/roles/researcher.md +0 -2
  413. package/skills/roles/reviewer.md +0 -3
  414. package/skills/roles/security.ai.md +1 -1
  415. package/skills/roles/security.legal-compliance.md +1 -1
  416. package/skills/roles/security.md +0 -1
  417. package/skills/roles/security.privacy.md +0 -1
  418. package/skills/roles/security.supply-chain.md +1 -1
  419. package/skills/roles/sre.md +1 -1
  420. package/skills/roles/test-automation.md +1 -1
  421. package/skills/roles/trace-reviewer.md +1 -1
  422. package/skills/roles/ux-researcher.md +1 -1
  423. package/specialists/artifact-manifest.json +36 -36
  424. package/specialists/org/contracts/accessibility-to-qa.json +11 -3
  425. package/specialists/org/contracts/any-to-business-strategist.json +19 -7
  426. package/specialists/org/contracts/any-to-debugger.json +7 -1
  427. package/specialists/org/contracts/any-to-designer.json +7 -1
  428. package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
  429. package/specialists/org/contracts/any-to-explorer.json +13 -4
  430. package/specialists/org/contracts/any-to-sre-incident.json +6 -2
  431. package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
  432. package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
  433. package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
  434. package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
  435. package/specialists/org/contracts/architect-to-engineer.json +20 -4
  436. package/specialists/org/contracts/architect-to-evaluator.json +15 -5
  437. package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
  438. package/specialists/org/contracts/architect-to-operations.json +17 -4
  439. package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
  440. package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
  441. package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
  442. package/specialists/org/contracts/engineer-to-qa.json +20 -4
  443. package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
  444. package/specialists/org/contracts/explorer-to-engineer.json +11 -3
  445. package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
  446. package/specialists/org/contracts/operations-to-user.json +53 -0
  447. package/specialists/org/contracts/operations-triage-output.json +53 -0
  448. package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
  449. package/specialists/org/contracts/product-manager-to-architect.json +30 -10
  450. package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
  451. package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
  452. package/specialists/org/contracts/qa-to-release-manager.json +11 -3
  453. package/specialists/org/contracts/researcher-to-architect.json +10 -2
  454. package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
  455. package/specialists/org/contracts/reviewer-to-security.json +17 -3
  456. package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
  457. package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
  458. package/specialists/org/contracts/user-to-construct.json +11 -4
  459. package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
  460. package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
  461. package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
  462. package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
  463. package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
  464. package/specialists/org/groups/engineering-group.json +2 -6
  465. package/specialists/org/groups/governance-group.json +2 -3
  466. package/specialists/org/groups/operations-group.json +5 -8
  467. package/specialists/org/groups/product-group.json +4 -8
  468. package/specialists/org/groups/quality-group.json +3 -7
  469. package/specialists/org/groups/strategy-group.json +6 -9
  470. package/specialists/org/models.json.example +8 -0
  471. package/specialists/org/scopes/creative.json +6 -1
  472. package/specialists/org/scopes/operations.json +7 -1
  473. package/specialists/org/scopes/research.json +6 -1
  474. package/specialists/org/scopes/rnd.json +7 -1
  475. package/specialists/org/specialists/cx-architect.json +27 -8
  476. package/specialists/org/specialists/cx-designer.json +22 -8
  477. package/specialists/org/specialists/cx-engineer.json +71 -7
  478. package/specialists/org/specialists/cx-operations.json +89 -15
  479. package/specialists/org/specialists/cx-orchestrator.json +16 -4
  480. package/specialists/org/specialists/cx-product-manager.json +28 -9
  481. package/specialists/org/specialists/cx-qa.json +16 -6
  482. package/specialists/org/specialists/cx-researcher.json +41 -7
  483. package/specialists/org/specialists/cx-reviewer.json +61 -11
  484. package/specialists/org/specialists/cx-security.json +30 -10
  485. package/specialists/org/teams/design-team.json +3 -4
  486. package/specialists/org/teams/engineering-team.json +3 -10
  487. package/specialists/org/teams/governance-team.json +3 -5
  488. package/specialists/org/teams/operations-team.json +6 -12
  489. package/specialists/org/teams/product-management-team.json +2 -3
  490. package/specialists/org/teams/quality-team.json +4 -12
  491. package/specialists/org/teams/research-team.json +3 -3
  492. package/specialists/org/teams/strategy-team.json +7 -14
  493. package/specialists/prompts/cx-architect.md +20 -1
  494. package/specialists/prompts/cx-data-analyst.md +1 -1
  495. package/specialists/prompts/cx-designer.md +12 -4
  496. package/specialists/prompts/cx-engineer.md +15 -1
  497. package/specialists/prompts/cx-operations.md +14 -2
  498. package/specialists/prompts/cx-orchestrator.md +9 -5
  499. package/specialists/prompts/cx-product-manager.md +5 -1
  500. package/specialists/prompts/cx-qa.md +6 -2
  501. package/specialists/prompts/cx-researcher.md +27 -31
  502. package/specialists/prompts/cx-reviewer.md +19 -1
  503. package/specialists/prompts/cx-security.md +5 -1
  504. package/templates/distribution/construct-brand.typ +123 -59
  505. package/templates/distribution/construct-decision.typ +6 -3
  506. package/templates/distribution/construct-pdf.typ +11 -4
  507. package/templates/distribution/construct-prd.typ +6 -3
  508. package/templates/distribution/construct-research.typ +7 -4
  509. package/vendor/pandoc-ext/README.md +3 -0
  510. package/vendor/pandoc-ext/diagram.lua +687 -0
  511. package/lib/providers/contract/adapters/git/index.mjs +0 -115
  512. package/lib/providers/contract/adapters/slack/index.mjs +0 -175
  513. package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
  514. package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
  515. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
  516. package/specialists/org/contracts/designer-to-accessibility.json +0 -36
  517. package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
  518. package/specialists/org/contracts/qa-to-test-automation.json +0 -42
  519. package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
  520. package/specialists/org/contracts/sre-to-release-manager.json +0 -36
  521. package/specialists/org/specialists/cx-accessibility.json +0 -69
  522. package/specialists/org/specialists/cx-ai-engineer.json +0 -75
  523. package/specialists/org/specialists/cx-business-strategist.json +0 -72
  524. package/specialists/org/specialists/cx-data-engineer.json +0 -71
  525. package/specialists/org/specialists/cx-devil-advocate.json +0 -71
  526. package/specialists/org/specialists/cx-docs-keeper.json +0 -82
  527. package/specialists/org/specialists/cx-evaluator.json +0 -69
  528. package/specialists/org/specialists/cx-explorer.json +0 -69
  529. package/specialists/org/specialists/cx-legal-compliance.json +0 -74
  530. package/specialists/org/specialists/cx-oracle.json +0 -46
  531. package/specialists/org/specialists/cx-platform-engineer.json +0 -80
  532. package/specialists/org/specialists/cx-rd-lead.json +0 -73
  533. package/specialists/org/specialists/cx-release-manager.json +0 -77
  534. package/specialists/org/specialists/cx-sre.json +0 -94
  535. package/specialists/org/specialists/cx-test-automation.json +0 -69
  536. package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
  537. package/specialists/org/specialists/cx-ux-researcher.json +0 -68
  538. package/specialists/org/teams/accessibility-team.json +0 -39
  539. package/specialists/org/teams/ux-research-team.json +0 -39
  540. package/specialists/prompts/cx-accessibility.md +0 -55
  541. package/specialists/prompts/cx-ai-engineer.md +0 -124
  542. package/specialists/prompts/cx-business-strategist.md +0 -58
  543. package/specialists/prompts/cx-data-engineer.md +0 -55
  544. package/specialists/prompts/cx-devil-advocate.md +0 -59
  545. package/specialists/prompts/cx-docs-keeper.md +0 -164
  546. package/specialists/prompts/cx-evaluator.md +0 -49
  547. package/specialists/prompts/cx-explorer.md +0 -71
  548. package/specialists/prompts/cx-legal-compliance.md +0 -58
  549. package/specialists/prompts/cx-oracle.md +0 -98
  550. package/specialists/prompts/cx-platform-engineer.md +0 -97
  551. package/specialists/prompts/cx-rd-lead.md +0 -59
  552. package/specialists/prompts/cx-release-manager.md +0 -54
  553. package/specialists/prompts/cx-sre.md +0 -111
  554. package/specialists/prompts/cx-test-automation.md +0 -55
  555. package/specialists/prompts/cx-trace-reviewer.md +0 -101
  556. package/specialists/prompts/cx-ux-researcher.md +0 -53
@@ -9,6 +9,9 @@
9
9
  * Storage layout:
10
10
  * .cx/observations/index.json — lightweight listing for fast filtering
11
11
  * .cx/observations/<id>.json — full observation record
12
+ * <stateRoot>/lancedb — vector index (ADR-0066: machine-scoped,
13
+ * not project-local); the JSON records
14
+ * above remain project-local for now
12
15
  */
13
16
  import crypto from 'node:crypto';
14
17
  import fs from 'node:fs';
@@ -16,6 +19,7 @@ import path from 'node:path';
16
19
  import { embedText as embedTextEngine } from './storage/embeddings-engine.mjs';
17
20
  import { VectorClient } from './storage/vector-client.mjs';
18
21
  import { ensureCxDir } from './project-init-shared.mjs';
22
+ import { resolveStateDir } from './state-root.mjs';
19
23
 
20
24
  const OBS_DIR = '.cx/observations';
21
25
  const INDEX_FILE = 'index.json';
@@ -31,7 +35,7 @@ const MAX_INDEX = 1000;
31
35
  function vectorClientFor(rootDir) {
32
36
  const env = process.env.CONSTRUCT_LANCEDB_PATH
33
37
  ? process.env
34
- : { ...process.env, CONSTRUCT_LANCEDB_PATH: path.join(rootDir, '.cx', 'lancedb') };
38
+ : { ...process.env, CONSTRUCT_LANCEDB_PATH: resolveStateDir(rootDir, 'lancedb', { ensureDir: false }) };
35
39
  return new VectorClient({ env });
36
40
  }
37
41
 
@@ -111,7 +115,7 @@ function logCapDrop(rootDir, kind, dropped, total) {
111
115
  dropped,
112
116
  total,
113
117
  cap: MAX_INDEX,
114
- remedy: 'run `construct memory consolidate` or raise --max-stored',
118
+ remedy: "Run 'construct memory consolidate' to compact stored observations.",
115
119
  });
116
120
  fs.appendFileSync(logPath, entry + '\n');
117
121
  } catch { /* cap warnings are best-effort */ }
@@ -18,6 +18,7 @@ import { spawnSync } from "node:child_process";
18
18
  import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
19
19
  import { tmpdir } from "node:os";
20
20
  import { join } from "node:path";
21
+ import { isMainModule } from '../roots.mjs';
21
22
 
22
23
  const DEFAULT_NUM_CTX = 32768;
23
24
  const CHATML_STOPS = ["<|im_start|>", "<|im_end|>", "<|endoftext|>"];
@@ -325,7 +326,7 @@ export async function probeStreamingToolCallLeak(model, { baseURL = "http://127.
325
326
  }
326
327
  }
327
328
 
328
- if (import.meta.url === `file://${process.argv[1]}`) {
329
+ if (isMainModule(import.meta.url)) {
329
330
  if (process.argv.includes("--probe")) {
330
331
  const model = process.argv.find((a) => a.startsWith("--model="))?.split("=")[1];
331
332
  const targets = model ? [model] : listModels();
@@ -175,10 +175,20 @@ export function writeOpenCodeConfig(config, file = findOpenCodeConfigPath()) {
175
175
  if (sessions.includes('github-copilot')) {
176
176
  const envValues = parseEnvFile(getUserEnvPath());
177
177
  const port = envValues.COPILOT_BRIDGE_PORT || '5174';
178
- config.provider['github-copilot'] = config.provider['github-copilot'] || {
178
+ const provider = config.provider['github-copilot'] || {
179
179
  baseUrl: `http://localhost:${port}/v1`,
180
- apiKey: "noop" // The bridge handles auth via gh CLI
180
+ apiKey: "noop" // Copilot itself is authed via gh CLI inside the bridge; this field is unused
181
181
  };
182
+
183
+ // The bridge now requires a per-launch bearer token. Wire it as an {env:} reference
184
+ // so OpenCode resolves it from its own env at request time and the token is never
185
+ // written into opencode.json in the clear.
186
+ if (envValues.CONSTRUCT_COPILOT_BRIDGE_TOKEN) {
187
+ provider.options = provider.options || {};
188
+ provider.options.headers = provider.options.headers || {};
189
+ provider.options.headers.Authorization = '{env:CONSTRUCT_COPILOT_BRIDGE_TOKEN}';
190
+ }
191
+ config.provider['github-copilot'] = provider;
182
192
  }
183
193
  }
184
194
 
@@ -259,6 +269,15 @@ export function writeOpenCodeConfig(config, file = findOpenCodeConfigPath()) {
259
269
  });
260
270
  }
261
271
 
262
- fs.writeFileSync(target, `${JSON.stringify(sanitizeOpenCodeConfig(config), null, 2)}\n`, "utf8");
272
+ fs.writeFileSync(target, `${JSON.stringify(sanitizeOpenCodeConfig(config), null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
273
+
274
+ // opencode.json can carry a resolved provider apiKey or Authorization header.
275
+ // writeFileSync keeps an existing inode's mode, so the mode option above does not
276
+ // tighten a file an earlier write left 0644; re-apply 0600 on every write, matching
277
+ // lib/env-config.mjs's writeEnvValues contract for config.env.
278
+
279
+ if (process.platform !== "win32") {
280
+ try { fs.chmodSync(target, 0o600); } catch { /* exotic filesystem */ }
281
+ }
263
282
  return target;
264
283
  }
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Intercepts OpenCode session events (session.created, session.idle, session.error),
5
5
  * records each as a telemetry trace, and applies model fallback on rate-limit errors.
6
+ * Also flags model-attempted calls to tools OpenCode rejects as unavailable
7
+ * (construct-2q2m) into the shared tool-failures observation log.
6
8
  */
7
9
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
8
10
  import { loadRegistry } from './registry/loader.mjs';
@@ -11,6 +13,7 @@ import { dirname, join, resolve } from "node:path";
11
13
  import { spawnSync } from "node:child_process";
12
14
  import { randomUUID } from "node:crypto";
13
15
  import { findOpenCodeConfigPath } from "./opencode-config.mjs";
16
+ import { isPlaceholder } from "./providers/credential-sources.mjs";
14
17
  import { classifyProviderFailure, readCurrentModels, resolveExecutionContractModelMetadata, resolveFallbackAction, selectModelTierForWorkCategory } from "./model-router.mjs";
15
18
  import { resolveRuntimePromptMetadata } from "./prompt-composer.js";
16
19
  import { enrichMetadataWithPrompt } from "./prompt-metadata.mjs";
@@ -30,6 +33,7 @@ import { syncModelPricing, refreshPricingCatalog } from "./telemetry/model-prici
30
33
  import { estimateUsageCost } from "./telemetry/model-pricing-catalog.mjs";
31
34
  import { buildSessionPrelude } from "./intake/session-prelude.mjs";
32
35
  import { doctorRoot } from './config/xdg.mjs';
36
+ import { recordToolFailure } from "./mcp/tool-recovery.mjs";
33
37
 
34
38
  const EFFICIENCY_SESSION_IDLE_RESET_MS = 2 * 60 * 60 * 1000;
35
39
  const EFFICIENCY_REPEATED_READ_THRESHOLD = 5;
@@ -59,6 +63,119 @@ const RATE_LIMIT_PATTERNS = [
59
63
  /no such model/i,
60
64
  ];
61
65
 
66
+ // construct-2q2m: OpenCode's own tool resolution (packages/opencode/src/session/llm/request.ts,
67
+ // resolveTools) filters disabled/denied tools out of the schema before the model ever sees them,
68
+ // so a model-hallucinated call to a tool name outside that resolved set has no single documented
69
+ // error shape in this repo's OpenCode source audit (docs/notes/research/2026-06-construct-audit/
70
+ // 20-opencode-ecosystem.md) — it is an AI-SDK/provider-level rejection, not an OpenCode permission
71
+ // deny (which instead flows through permission.ask and stays in-schema). These patterns match the
72
+ // Vercel AI SDK's NoSuchToolError phrasing plus common "tool not found" provider error text, so at
73
+ // least one branch catches whichever shape OpenCode actually surfaces. Unverified against a live
74
+ // OpenCode session — see detectUnavailableToolRejections doc comment.
75
+ const UNAVAILABLE_TOOL_PATTERNS = [
76
+ /\bno such tool\b/i,
77
+ /\bunknown tool\b/i,
78
+ /\bunavailable tool\b/i,
79
+ /\btool\b[^.\n]{0,60}\b(?:not found|not available|not registered|not permitted|does not exist)\b/i,
80
+ /\bNoSuchToolError\b/i,
81
+ /is not a (?:known|recognized|valid|registered) tool/i,
82
+ ];
83
+
84
+ function textLooksLikeUnavailableTool(text) {
85
+ return typeof text === "string" && text.length > 0 && UNAVAILABLE_TOOL_PATTERNS.some((pattern) => pattern.test(text));
86
+ }
87
+
88
+ function toolNameFromPart(part) {
89
+ if (!part || typeof part !== "object") return null;
90
+ const typ = typeof part.type === "string" ? part.type.toLowerCase() : "";
91
+ if (typ === "tool" || typ === "tool-call" || typ === "tool_call") {
92
+ return stripPlaceholder(part.tool || part.toolName || part.name) || null;
93
+ }
94
+ if (typ === "tool-invocation" || typ === "tool_invocation") {
95
+ const invocation = part.toolInvocation || part;
96
+ return stripPlaceholder(invocation.toolName || invocation.tool || invocation.name) || null;
97
+ }
98
+ return null;
99
+ }
100
+
101
+ function toolPartErrorText(part) {
102
+ if (!part || typeof part !== "object") return "";
103
+ const typ = typeof part.type === "string" ? part.type.toLowerCase() : "";
104
+ if (typ === "tool" || typ === "tool-call" || typ === "tool_call") {
105
+ return flatten(part.state?.error ?? part.state?.message ?? "");
106
+ }
107
+ if (typ === "tool-invocation" || typ === "tool_invocation") {
108
+ const invocation = part.toolInvocation || part;
109
+ return flatten(invocation.error ?? invocation.state?.error ?? "");
110
+ }
111
+ return "";
112
+ }
113
+
114
+ function toolPartIsErrorState(part) {
115
+ const typ = typeof part?.type === "string" ? part.type.toLowerCase() : "";
116
+ if (typ === "tool" || typ === "tool-call" || typ === "tool_call") {
117
+ const status = part.state?.status ?? part.state;
118
+ return status === "error";
119
+ }
120
+ if (typ === "tool-invocation" || typ === "tool_invocation") {
121
+ const invocation = part.toolInvocation || part;
122
+ return invocation?.state === "error" || invocation?.status === "error";
123
+ }
124
+ return false;
125
+ }
126
+
127
+ // Detects a model-attempted call to a tool OpenCode rejects as unavailable, from
128
+ // either a session.error event or an error-state tool part inside message.updated.
129
+ // Returns one hit per match; each hit best-effort resolves the tool name (null when
130
+ // the raw text does not identify it structurally). Hits append to the shared
131
+ // tool-failures observation log (lib/mcp/tool-recovery.mjs) so both this host-side
132
+ // signal and construct-mcp's own server-side tool-name-miss capture feed the same
133
+ // learning-loop aggregation (summarizeToolFailures).
134
+ //
135
+ // Pattern-matched against documented AI-SDK/provider error phrasing rather than a
136
+ // live OpenCode session; construct-2q2m's open item is confirming the real event
137
+ // shape and, if it differs, adjusting the patterns/field lookups above.
138
+
139
+ export function detectUnavailableToolRejections(event) {
140
+ if (event?.type === "session.error") {
141
+ const error = event?.error && typeof event.error === "object" ? event.error : {};
142
+ const text = flatten([error.message, error.name, error.code, event?.message]);
143
+ if (!textLooksLikeUnavailableTool(text)) return [];
144
+ return [{
145
+ tool: stripPlaceholder(error.tool || error.toolName) || null,
146
+ source: "session.error",
147
+ text: text.slice(0, 500),
148
+ }];
149
+ }
150
+ if (event?.type === "message.updated") {
151
+ const info = event?.properties?.info;
152
+ const parts = Array.isArray(info?.parts) ? info.parts : [];
153
+ const hits = [];
154
+ for (const part of parts) {
155
+ if (!toolPartIsErrorState(part)) continue;
156
+ const text = toolPartErrorText(part);
157
+ if (!textLooksLikeUnavailableTool(text)) continue;
158
+ hits.push({ tool: toolNameFromPart(part), source: "message.part", text: text.slice(0, 500) });
159
+ }
160
+ return hits;
161
+ }
162
+ return [];
163
+ }
164
+
165
+ export function recordUnavailableToolRejections(event, { toolkitDir = process.env.CX_TOOLKIT_DIR, env = process.env } = {}) {
166
+ const hits = detectUnavailableToolRejections(event);
167
+ if (!hits.length) return hits;
168
+ const rootDir = toolkitDir || env.CX_TOOLKIT_DIR || process.cwd();
169
+ for (const hit of hits) {
170
+ recordToolFailure(rootDir, {
171
+ tool: hit.tool || "(unknown)",
172
+ code: "opencode_tool_unavailable",
173
+ message: hit.text,
174
+ });
175
+ }
176
+ return hits;
177
+ }
178
+
62
179
  const RUNTIME_TRACE_EVENTS = new Set([
63
180
  "session.created",
64
181
  "session.idle",
@@ -787,14 +904,14 @@ function writeState(payload, env = process.env) {
787
904
  writeFileSync(statePath, JSON.stringify(payload, null, 2));
788
905
  }
789
906
 
790
- function readOpenRouterApiKey(configPath = findOpenCodeConfigPath(), env = process.env) {
907
+ export function readOpenRouterApiKey(configPath = findOpenCodeConfigPath(), env = process.env) {
791
908
  if (env.OPENROUTER_API_KEY) return env.OPENROUTER_API_KEY;
792
909
  try {
793
910
  const config = JSON.parse(readFileSync(configPath, "utf8"));
794
911
  const auth = config?.provider?.openrouter?.options?.headers?.Authorization;
795
912
  if (typeof auth !== "string") return "";
796
913
  const key = auth.replace(/^Bearer\s+/i, "").trim();
797
- if (!key || key.includes("__OPENROUTER_API_KEY__")) return "";
914
+ if (isPlaceholder(key)) return "";
798
915
  return key;
799
916
  } catch {
800
917
  return "";
@@ -916,6 +1033,7 @@ export function createConstructOpenCodePlugin({
916
1033
  await safe(() => emitRuntimeTrace(event, { client, env }), "legacy-trace", client);
917
1034
  await safe(() => emitSessionPrelude(event, { client, env, cwd: toolkitDir }), "session-prelude", client);
918
1035
  await maybeApplyModelFallback(event, { client, env, toolkitDir, configPath });
1036
+ await safe(() => recordUnavailableToolRejections(event, { toolkitDir, env }), "tool-miss-capture", client);
919
1037
  try {
920
1038
  const { warnings } = trackReadEfficiencyFromMessage(event, { env, cwd: toolkitDir || process.cwd() });
921
1039
  for (const message of warnings) {
@@ -5,10 +5,16 @@
5
5
  * sync (Construct tool repo only). Approve actions queue to
6
6
  * <project>/.cx/oracle/pending.jsonl. High-severity gaps auto-raise beads
7
7
  * when enabled. Denied actions are skipped.
8
+ *
9
+ * triagePending() flushes an accumulated pending.jsonl backlog to one
10
+ * representative row per dedupKey (pending or already-expired; approved
11
+ * rows are left untouched), archiving every superseded row to
12
+ * pending-archive.jsonl so the original set stays recoverable.
8
13
  */
9
14
 
10
15
  import fs from 'node:fs';
11
16
  import path from 'node:path';
17
+ import crypto from 'node:crypto';
12
18
  import { spawnSync } from 'node:child_process';
13
19
  import { fileURLToPath } from 'node:url';
14
20
 
@@ -27,19 +33,59 @@ function pendingPath(projectDir) {
27
33
  return path.join(projectDir, '.cx', 'oracle', 'pending.jsonl');
28
34
  }
29
35
 
36
+ function pendingArchivePath(projectDir) {
37
+ return path.join(projectDir, '.cx', 'oracle', 'pending-archive.jsonl');
38
+ }
39
+
40
+ // Recurring machine-generated decisions (re-run tests / review contract
41
+ // violations / rebuild the dependency matrix) are symptoms of noisy
42
+ // producers, not decisions in their own right — approving the survivor
43
+ // clears the queue but does not fix the producer. Surfacing the linkage on
44
+ // the survivor keeps that distinction visible to whoever reviews the triage
45
+ // output.
46
+
47
+ const RECURRING_PRODUCER_LINKAGE = [
48
+ { pattern: /re-run affected tests/i, linkedTo: ['construct-r8wr.6'] },
49
+ { pattern: /review recent contract violations/i, linkedTo: ['construct-r8wr.1'] },
50
+ { pattern: /rebuild the dependency matrix/i, linkedTo: ['construct-r8wr.6'] },
51
+ ];
52
+
53
+ function producerLinkageFor(summary) {
54
+ const match = RECURRING_PRODUCER_LINKAGE.find((entry) => entry.pattern.test(summary || ''));
55
+ return match ? match.linkedTo : undefined;
56
+ }
57
+
58
+ const ORACLE_PENDING_MAX = 50;
59
+ const ORACLE_PENDING_TTL_MS = 7 * 24 * 60 * 60 * 1000;
60
+
30
61
  function ensureOracleDir(projectDir) {
31
62
  const dir = path.join(projectDir, '.cx', 'oracle');
32
63
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
33
64
  return dir;
34
65
  }
35
66
 
36
- function appendPending(projectDir, record) {
37
- ensureOracleDir(projectDir);
38
- const line = JSON.stringify({ ...record, queuedAt: new Date().toISOString() }) + '\n';
39
- fs.appendFileSync(pendingPath(projectDir), line, 'utf8');
67
+ function sha256(input) {
68
+ return crypto.createHash('sha256').update(input).digest('hex');
69
+ }
70
+
71
+ function normalizeSummary(summary) {
72
+ return String(summary || '').trim().toLowerCase().replace(/\s+/g, ' ');
73
+ }
74
+
75
+ function pendingTarget(record = {}) {
76
+ return record.target
77
+ || record.signOff?.id
78
+ || record.context?.signOff?.id
79
+ || record.context?.remediationRoute?.primary
80
+ || '';
40
81
  }
41
82
 
42
- export function listPending(projectDir) {
83
+ function pendingDedupKey(record = {}) {
84
+ const parts = [record.kind || '', pendingTarget(record), normalizeSummary(record.summary)];
85
+ return `oracle:${sha256(parts.join('\u0000')).slice(0, 16)}`;
86
+ }
87
+
88
+ function readPendingRecords(projectDir) {
43
89
  const file = pendingPath(projectDir);
44
90
  if (!fs.existsSync(file)) return [];
45
91
  return fs.readFileSync(file, 'utf8')
@@ -49,15 +95,165 @@ export function listPending(projectDir) {
49
95
  .filter(Boolean);
50
96
  }
51
97
 
98
+ function sweepPendingRecords(records, now = Date.now()) {
99
+ let changed = false;
100
+ const next = records.map((record) => {
101
+ const dedupKey = record.dedupKey || pendingDedupKey(record);
102
+ if (record.dedupKey !== dedupKey) changed = true;
103
+ const count = Number.isFinite(record.count) && record.count > 0 ? record.count : 1;
104
+ const firstSeenAt = record.firstSeenAt || record.queuedAt || new Date(now).toISOString();
105
+ const lastSeenAt = record.lastSeenAt || record.queuedAt || firstSeenAt;
106
+ const updated = {
107
+ ...record,
108
+ dedupKey,
109
+ count,
110
+ firstSeenAt,
111
+ lastSeenAt,
112
+ };
113
+ const ageMs = now - Date.parse(lastSeenAt);
114
+ if (updated.status === 'pending' && Number.isFinite(ageMs) && ageMs > ORACLE_PENDING_TTL_MS) {
115
+ updated.status = 'expired';
116
+ updated.expiredAt = new Date(now).toISOString();
117
+ updated.expiredReason = 'ttl';
118
+ changed = true;
119
+ }
120
+ return updated;
121
+ });
122
+
123
+ const activePending = next
124
+ .filter((record) => record.status === 'pending')
125
+ .sort((a, b) => Date.parse(b.lastSeenAt || b.queuedAt || 0) - Date.parse(a.lastSeenAt || a.queuedAt || 0));
126
+
127
+ for (const overflow of activePending.slice(ORACLE_PENDING_MAX)) {
128
+ if (overflow.status !== 'pending') continue;
129
+ overflow.status = 'expired';
130
+ overflow.expiredAt = new Date(now).toISOString();
131
+ overflow.expiredReason = 'cap';
132
+ changed = true;
133
+ }
134
+
135
+ return { records: next, changed };
136
+ }
137
+
52
138
  function rewritePending(projectDir, pending) {
53
139
  const file = pendingPath(projectDir);
140
+ ensureOracleDir(projectDir);
54
141
  fs.writeFileSync(file, pending.map((p) => JSON.stringify(p)).join('\n') + (pending.length ? '\n' : ''), 'utf8');
55
142
  }
56
143
 
144
+ function enqueuePending(projectDir, record, now = Date.now()) {
145
+ const { records, changed } = sweepPendingRecords(readPendingRecords(projectDir), now);
146
+ const dedupKey = pendingDedupKey(record);
147
+ const queuedAt = new Date(now).toISOString();
148
+ const existing = records.find((entry) => entry.status === 'pending' && entry.dedupKey === dedupKey);
149
+ if (existing) {
150
+ existing.summary = record.summary;
151
+ existing.signOff = record.signOff;
152
+ existing.context = record.context;
153
+ existing.classification = record.classification;
154
+ existing.target = pendingTarget(record);
155
+ existing.count = (Number.isFinite(existing.count) ? existing.count : 1) + 1;
156
+ existing.lastSeenAt = queuedAt;
157
+ rewritePending(projectDir, records);
158
+ return existing;
159
+ }
160
+ const pending = {
161
+ ...record,
162
+ id: record.id || `oracle-${dedupKey.slice('oracle:'.length)}`,
163
+ dedupKey,
164
+ count: 1,
165
+ queuedAt,
166
+ firstSeenAt: queuedAt,
167
+ lastSeenAt: queuedAt,
168
+ target: pendingTarget(record),
169
+ };
170
+ rewritePending(projectDir, changed ? records.concat(pending) : [...records, pending]);
171
+ return pending;
172
+ }
173
+
174
+ export function listPending(projectDir, { includeExpired = false, now = Date.now() } = {}) {
175
+ const { records, changed } = sweepPendingRecords(readPendingRecords(projectDir), now);
176
+ if (changed) rewritePending(projectDir, records);
177
+ return records.filter((record) => includeExpired || record.status !== 'expired');
178
+ }
179
+
180
+ /**
181
+ * Flush the pending.jsonl backlog to one representative row per dedupKey.
182
+ * Groups every non-approved row (pending or expired — a backlog that has
183
+ * already TTL/cap-expired is still a backlog) by dedupKey, keeps the
184
+ * most-recently-seen row as the survivor with an accumulated
185
+ * occurrenceCount, and archives every other row in the group to
186
+ * pending-archive.jsonl with a supersededBy/supersededAt trail. Approved
187
+ * rows are real decisions already made and are left untouched.
188
+ */
189
+ export function triagePending(projectDir, { dryRun = false, now = Date.now() } = {}) {
190
+ const { records } = sweepPendingRecords(readPendingRecords(projectDir), now);
191
+ const protectedRecords = records.filter((r) => r.status === 'approved');
192
+ const eligible = records.filter((r) => r.status !== 'approved');
193
+
194
+ const groups = new Map();
195
+ for (const record of eligible) {
196
+ const key = record.dedupKey || pendingDedupKey(record);
197
+ if (!groups.has(key)) groups.set(key, []);
198
+ groups.get(key).push(record);
199
+ }
200
+
201
+ const survivors = [];
202
+ const supersededRows = [];
203
+ const archivedAt = new Date(now).toISOString();
204
+
205
+ for (const group of groups.values()) {
206
+ const sorted = [...group].sort((a, b) => {
207
+ const bySeen = Date.parse(b.lastSeenAt || b.queuedAt || 0) - Date.parse(a.lastSeenAt || a.queuedAt || 0);
208
+ if (bySeen !== 0) return bySeen;
209
+ return (b.count || 1) - (a.count || 1);
210
+ });
211
+ const [representative, ...rest] = sorted;
212
+ const occurrenceCount = group.reduce((sum, r) => sum + (Number.isFinite(r.count) && r.count > 0 ? r.count : 1), 0);
213
+ const linkedTo = producerLinkageFor(representative.summary);
214
+
215
+ survivors.push({
216
+ ...representative,
217
+ count: occurrenceCount,
218
+ occurrenceCount,
219
+ ...(rest.length ? { supersededIds: rest.map((r) => r.id) } : {}),
220
+ ...(linkedTo ? { producerLinkage: linkedTo } : {}),
221
+ });
222
+
223
+ for (const row of rest) {
224
+ supersededRows.push({
225
+ ...row,
226
+ supersededBy: representative.id,
227
+ supersededAt: archivedAt,
228
+ supersededReason: 'duplicate-content-key',
229
+ });
230
+ }
231
+ }
232
+
233
+ if (!dryRun) {
234
+ if (supersededRows.length) {
235
+ ensureOracleDir(projectDir);
236
+ const archiveFile = pendingArchivePath(projectDir);
237
+ const lines = supersededRows.map((r) => JSON.stringify(r)).join('\n') + '\n';
238
+ fs.appendFileSync(archiveFile, lines, 'utf8');
239
+ }
240
+ rewritePending(projectDir, [...protectedRecords, ...survivors]);
241
+ }
242
+
243
+ return {
244
+ dryRun,
245
+ uniqueCount: survivors.length,
246
+ superseded: supersededRows.length,
247
+ survivors,
248
+ supersededRows,
249
+ };
250
+ }
251
+
57
252
  export async function approvePending(projectDir, id, { execute = true, rootDir, homeDir, dryRun = false } = {}) {
58
- const pending = listPending(projectDir);
253
+ const pending = listPending(projectDir, { includeExpired: true });
59
254
  const match = pending.find((p) => p.id === id);
60
255
  if (!match) return { ok: false, reason: 'not-found' };
256
+ if (match.status === 'expired') return { ok: false, reason: 'expired', action: match };
61
257
  if (match.status === 'approved' && match.executedAt) {
62
258
  return { ok: true, already: true, action: match };
63
259
  }
@@ -147,9 +343,7 @@ export async function runOracleTick({ rootDir, projectDir, homeDir, dryRun = fal
147
343
  executed.push({ ...rec, classification, result });
148
344
  continue;
149
345
  }
150
- const id = `oracle-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
151
346
  const pending = {
152
- id,
153
347
  kind: rec.kind,
154
348
  summary: rec.summary,
155
349
  classification,
@@ -157,8 +351,8 @@ export async function runOracleTick({ rootDir, projectDir, homeDir, dryRun = fal
157
351
  signOff: rec.signOff ?? signOffMetadata({ id: rec.kind }, projectDir),
158
352
  context: { ...rec },
159
353
  };
160
- if (!dryRun) appendPending(projectDir, pending);
161
- queued.push(pending);
354
+ const persisted = dryRun ? pending : enqueuePending(projectDir, pending);
355
+ queued.push(persisted);
162
356
  }
163
357
 
164
358
  const tick = {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * lib/oracle/cli.mjs — CLI handlers for `construct oracle <subcommand>`.
3
3
  *
4
- * Subcommands: start, status, review, pending, approve, gaps, reconcile.
4
+ * Subcommands: start, status, review, pending, approve, gaps, reconcile, triage.
5
5
  */
6
6
 
7
7
  import fs from 'node:fs';
@@ -17,7 +17,7 @@ import {
17
17
  readLastTick,
18
18
  KILLSWITCH_ENV,
19
19
  } from './index.mjs';
20
- import { runOracleTick, listPending, approvePending, defaultRootDir } from './actions.mjs';
20
+ import { runOracleTick, listPending, approvePending, triagePending, defaultRootDir } from './actions.mjs';
21
21
  import { collectOracleGaps, formatOracleGapsReport } from './gaps.mjs';
22
22
  import { reconcileOracleHygieneBeads } from './reconcile.mjs';
23
23
  import { doctorRoot } from '../config/xdg.mjs';
@@ -153,6 +153,26 @@ function cmdGaps(args, { projectDir }) {
153
153
  return 0;
154
154
  }
155
155
 
156
+ function cmdTriage(args, { projectDir }) {
157
+ const dryRun = args.includes('--dry-run');
158
+ const json = args.includes('--json');
159
+ const result = triagePending(projectDir, { dryRun });
160
+ if (json) {
161
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
162
+ return 0;
163
+ }
164
+ process.stdout.write(
165
+ `Oracle triage${dryRun ? ' (dry-run)' : ''}: ${result.uniqueCount} unique decision(s), ${result.superseded} superseded\n`,
166
+ );
167
+ for (const s of result.survivors) {
168
+ process.stdout.write(` ${s.id} [${s.status}] x${s.occurrenceCount}: ${s.summary}\n`);
169
+ if (s.producerLinkage) {
170
+ process.stdout.write(` producer linkage: ${s.producerLinkage.join(', ')} (approving this does not fix the producer)\n`);
171
+ }
172
+ }
173
+ return 0;
174
+ }
175
+
156
176
  async function cmdReconcile(args, { projectDir }) {
157
177
  const dryRun = args.includes('--dry-run');
158
178
  const json = args.includes('--json');
@@ -194,9 +214,10 @@ export async function runOracleCli(args, opts = {}) {
194
214
  case 'approve': return cmdApprove(subArgs, { projectDir, rootDir, homeDir });
195
215
  case 'gaps': return cmdGaps(subArgs, { projectDir });
196
216
  case 'reconcile': return cmdReconcile(subArgs, { projectDir });
217
+ case 'triage': return cmdTriage(subArgs, { projectDir });
197
218
  default:
198
219
  process.stderr.write(
199
- 'Usage: construct oracle <start|status|review|pending|approve|gaps|reconcile> [--json] [--dry-run] [--foreground] [--no-execute]\n',
220
+ 'Usage: construct oracle <start|status|review|pending|approve|gaps|reconcile|triage> [--json] [--dry-run] [--foreground] [--no-execute]\n',
200
221
  );
201
222
  if (sub && sub !== '--help' && sub !== '-h') {
202
223
  process.stderr.write(`Unknown subcommand: ${sub}\n`);
@@ -7,8 +7,14 @@
7
7
 
8
8
  import { runOracleDaemon } from './index.mjs';
9
9
  import { defaultRootDir } from './actions.mjs';
10
+ import { enableSecretAuditTrail } from '../providers/secret-audit-wiring.mjs';
10
11
 
11
12
  const rootDir = process.env.CONSTRUCT_ORACLE_ROOT || defaultRootDir();
12
13
  const projectDir = process.env.CONSTRUCT_ORACLE_PROJECT || process.cwd();
13
14
 
15
+ // A long-lived daemon in its own process: wire the audit sink at the entry so any
16
+ // credential resolution the tick performs is recorded on the shared trail rather than
17
+ // escaping it, matching the CLI entrypoint's own wiring.
18
+ enableSecretAuditTrail();
19
+
14
20
  await runOracleDaemon({ rootDir, projectDir });
@@ -1,5 +1,6 @@
1
1
  /**
2
- * lib/oracle/dispatch.mjs — cx-oracle routing artifacts (deterministic, LLM-free).
2
+ * lib/oracle/dispatch.mjs — fleet-health routing artifacts for cx-orchestrator's
3
+ * absorbed cx-oracle duty (deterministic, LLM-free; construct-rf26.11).
3
4
  *
4
5
  * Writes audit markdown under .cx/oracle/routing/ for approve-gated dispatch.
5
6
  * Optional high-severity auto-artifact on tick when gaps are load-bearing.
@@ -159,11 +159,11 @@ export async function executeApprovedAction(action, { rootDir, projectDir, homeD
159
159
  });
160
160
  case 'trace-review':
161
161
  return dispatchRemediation({
162
- action, projectDir, homeDir, personaId: 'cx-trace-reviewer', eventType: 'outcomes.degraded',
162
+ action, projectDir, homeDir, personaId: 'cx-reviewer', eventType: 'outcomes.degraded',
163
163
  });
164
164
  case 'doctor-followup':
165
165
  return dispatchRemediation({
166
- action, projectDir, homeDir, personaId: 'cx-sre', eventType: 'service.down',
166
+ action, projectDir, homeDir, personaId: 'cx-operations', eventType: 'service.down',
167
167
  });
168
168
  case 'executive-signoff-required':
169
169
  case 'structure-cleanup-proposal':
@@ -10,10 +10,10 @@ import { isVerdictOnlyGap, autoRaiseEnabledForGap } from './policy.mjs';
10
10
 
11
11
  const HYGIENE_HINTS = {
12
12
  'beads-hygiene': 'Run `construct beads drift` to inspect stuck/stale beads.',
13
- 'workflow-misaligned': 'Run `construct workflow init` or `construct init` to align workflow state.',
13
+ 'workflow-misaligned': 'Run `construct workflow new` or `construct init` to align workflow state.',
14
14
  'propagation-stale': 'Run `construct sync` to refresh platform propagation.',
15
- 'census-stale': 'Run `construct census` to refresh the fleet census.',
16
- 'outcomes-missing': 'Run `construct outcomes aggregate` when outcome data is available.',
15
+ 'census-stale': 'Run `node scripts/alignment/census.mjs` to refresh the fleet census.',
16
+ 'outcomes-missing': 'Run `construct oracle` action `outcomes-aggregate` when outcome data is available.',
17
17
  };
18
18
 
19
19
  /**