@geraldmaron/construct 1.4.2 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (519) hide show
  1. package/.env.example +36 -0
  2. package/README.md +41 -7
  3. package/bin/construct +1027 -64
  4. package/examples/distribution/sources/deck-one-pager.md +1 -1
  5. package/lib/acp/server.mjs +21 -7
  6. package/lib/adapters-sync.mjs +2 -1
  7. package/lib/audit-trail.mjs +185 -2
  8. package/lib/beads/auto-close.mjs +2 -1
  9. package/lib/beads/drift.mjs +2 -1
  10. package/lib/bridges/copilot-proxy.mjs +20 -5
  11. package/lib/certification/skill-inventory.mjs +10 -1
  12. package/lib/cli/approvals.mjs +147 -0
  13. package/lib/cli-commands.mjs +105 -14
  14. package/lib/comment-lint.mjs +127 -8
  15. package/lib/config/schema.mjs +6 -29
  16. package/lib/config/source-target-registry.mjs +70 -0
  17. package/lib/config/source-targets.mjs +179 -205
  18. package/lib/contracts/coverage.mjs +77 -0
  19. package/lib/contracts/validate.mjs +102 -13
  20. package/lib/contracts/violation-log.mjs +50 -4
  21. package/lib/db/migrate.mjs +69 -0
  22. package/lib/db/migrations/001_orchestration_runs.sql +9 -0
  23. package/lib/db/migrations/002_queue_provider.sql +50 -0
  24. package/lib/db/migrations/003_worker_registry.sql +17 -0
  25. package/lib/db/migrations/004_trace_events.sql +16 -0
  26. package/lib/db/migrations/005_shared_memory.sql +15 -0
  27. package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
  28. package/lib/decisions/enforced-baseline.json +0 -2
  29. package/lib/decisions/registry.mjs +3 -2
  30. package/lib/deployment/parity-contract.mjs +1 -1
  31. package/lib/deployment-mode.mjs +78 -2
  32. package/lib/diagram-export.mjs +10 -1
  33. package/lib/distill.mjs +5 -2
  34. package/lib/docs-verify.mjs +2 -1
  35. package/lib/doctor/cli.mjs +1 -0
  36. package/lib/doctor/command-on-path.mjs +24 -0
  37. package/lib/doctor/diagnosis.mjs +89 -0
  38. package/lib/doctor/embedding-health.mjs +50 -0
  39. package/lib/doctor/engine-health.mjs +93 -0
  40. package/lib/doctor/graph-validate.mjs +47 -0
  41. package/lib/doctor/index.mjs +18 -4
  42. package/lib/doctor/sidecar-providers.mjs +56 -0
  43. package/lib/doctor/watchers/consistency.mjs +93 -1
  44. package/lib/doctor/watchers/cx-budget.mjs +1 -1
  45. package/lib/doctor/watchers/graph-staleness.mjs +1 -1
  46. package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
  47. package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
  48. package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
  49. package/lib/doctor/watchers/provider-breaker.mjs +78 -0
  50. package/lib/document-export.mjs +52 -27
  51. package/lib/document-extract/docling-client.mjs +9 -5
  52. package/lib/document-extract/docling-sidecar.py +30 -0
  53. package/lib/document-extract.mjs +1 -1
  54. package/lib/document-ingest.mjs +9 -0
  55. package/lib/embed/approval-queue.mjs +184 -88
  56. package/lib/embed/authority-guard.mjs +65 -2
  57. package/lib/embed/capability-jobs.mjs +365 -0
  58. package/lib/embed/capability-lifecycle.mjs +219 -0
  59. package/lib/embed/capability-loader.mjs +303 -0
  60. package/lib/embed/capability-runtime.mjs +58 -0
  61. package/lib/embed/cli.mjs +185 -8
  62. package/lib/embed/config.mjs +4 -0
  63. package/lib/embed/daemon.mjs +125 -6
  64. package/lib/embed/demand-fetch.mjs +190 -54
  65. package/lib/embed/inbox.mjs +12 -10
  66. package/lib/embed/presets/ops-triage.mjs +236 -0
  67. package/lib/embed/presets/pm-feedback.mjs +341 -0
  68. package/lib/embed/presets/tpm.mjs +401 -0
  69. package/lib/embed/providers/jira.mjs +72 -1
  70. package/lib/embed/providers/registry.mjs +140 -33
  71. package/lib/embedded-contract/capability.mjs +4 -0
  72. package/lib/embedded-contract/execution.mjs +1 -1
  73. package/lib/embedded-contract/model-resolve.mjs +53 -3
  74. package/lib/embedded-contract/workflow-defs.mjs +48 -73
  75. package/lib/embedded-contract/workflow-invoke.mjs +144 -4
  76. package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
  77. package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
  78. package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
  79. package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
  80. package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
  81. package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
  82. package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
  83. package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
  84. package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
  85. package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
  86. package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
  87. package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
  88. package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
  89. package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
  90. package/lib/env-config.mjs +50 -9
  91. package/lib/extensions/index.mjs +27 -0
  92. package/lib/extensions/loader.mjs +120 -0
  93. package/lib/extensions/manifest-schema.mjs +141 -0
  94. package/lib/extensions/manifests/anthropic.manifest.json +12 -0
  95. package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
  96. package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
  97. package/lib/extensions/manifests/directory.manifest.json +12 -0
  98. package/lib/extensions/manifests/docling.manifest.json +37 -0
  99. package/lib/extensions/manifests/echo.manifest.json +8 -0
  100. package/lib/extensions/manifests/feedback.manifest.json +12 -0
  101. package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
  102. package/lib/extensions/manifests/github.manifest.json +52 -0
  103. package/lib/extensions/manifests/linear.manifest.json +50 -0
  104. package/lib/extensions/manifests/local.manifest.json +12 -0
  105. package/lib/extensions/manifests/ollama.manifest.json +12 -0
  106. package/lib/extensions/manifests/openai.manifest.json +12 -0
  107. package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
  108. package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
  109. package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
  110. package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
  111. package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
  112. package/lib/extensions/manifests/openrouter.manifest.json +12 -0
  113. package/lib/extensions/manifests/postgres.manifest.json +12 -0
  114. package/lib/extensions/manifests/salesforce.manifest.json +12 -0
  115. package/lib/extensions/manifests/slack.manifest.json +58 -0
  116. package/lib/extensions/manifests/whisper.manifest.json +36 -0
  117. package/lib/extensions/validate.mjs +175 -0
  118. package/lib/features.mjs +13 -9
  119. package/lib/flows/checkpoint.mjs +184 -0
  120. package/lib/flows/constants.mjs +27 -0
  121. package/lib/flows/define.mjs +146 -0
  122. package/lib/flows/engine.mjs +249 -0
  123. package/lib/flows/errors.mjs +19 -0
  124. package/lib/flows/index.mjs +27 -0
  125. package/lib/flows/joins.mjs +18 -0
  126. package/lib/flows/schema.mjs +90 -0
  127. package/lib/flows/state.mjs +31 -0
  128. package/lib/frameworks/loader.mjs +99 -0
  129. package/lib/frameworks/schema.mjs +157 -0
  130. package/lib/graph/build-from-corpus.mjs +67 -0
  131. package/lib/graph/build-from-embed.mjs +82 -0
  132. package/lib/graph/build-from-registry.mjs +164 -3
  133. package/lib/graph/cli.mjs +456 -12
  134. package/lib/graph/gap-queries.mjs +156 -0
  135. package/lib/graph/gaps.mjs +41 -0
  136. package/lib/graph/impacted.mjs +129 -0
  137. package/lib/graph/runtime-evidence.mjs +177 -0
  138. package/lib/graph/security-coverage.mjs +113 -0
  139. package/lib/graph/staleness.mjs +241 -10
  140. package/lib/graph/store.mjs +24 -7
  141. package/lib/graph/validate.mjs +161 -0
  142. package/lib/headhunt.mjs +9 -8
  143. package/lib/health-check.mjs +15 -7
  144. package/lib/hook-health.mjs +1 -1
  145. package/lib/hooks/agent-tracker.mjs +10 -4
  146. package/lib/hooks/edit-guard.mjs +3 -3
  147. package/lib/hooks/graph-impact-advisory.mjs +2 -2
  148. package/lib/hooks/guard-bash.mjs +41 -15
  149. package/lib/hooks/mcp-health-check.mjs +11 -4
  150. package/lib/hooks/model-fallback.mjs +50 -6
  151. package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
  152. package/lib/hooks/pre-compact.mjs +181 -173
  153. package/lib/hooks/rule-verifier.mjs +2 -1
  154. package/lib/hooks/session-reflect.mjs +2 -1
  155. package/lib/hooks/session-start.mjs +3 -3
  156. package/lib/host-capabilities.mjs +13 -2
  157. package/lib/host-disposition.mjs +19 -7
  158. package/lib/identity.mjs +92 -0
  159. package/lib/ingest/degraded-extract.mjs +96 -0
  160. package/lib/ingest/docling-remote.mjs +2 -1
  161. package/lib/ingest/provider-extract.mjs +7 -19
  162. package/lib/ingest/sidecar-providers.mjs +196 -0
  163. package/lib/ingest-tooling.mjs +11 -5
  164. package/lib/init-unified.mjs +57 -45
  165. package/lib/init-update.mjs +2 -1
  166. package/lib/install/legacy-global-cleanup.mjs +25 -0
  167. package/lib/intake/daemon.mjs +99 -8
  168. package/lib/intake/git-queue.mjs +110 -38
  169. package/lib/intake/queue-registry.mjs +50 -0
  170. package/lib/intake/queue.mjs +133 -17
  171. package/lib/intake/session-prelude.mjs +57 -8
  172. package/lib/integrations/intake-integrations.mjs +61 -111
  173. package/lib/intent-classifier.mjs +43 -5
  174. package/lib/libreoffice-export.mjs +8 -8
  175. package/lib/logging/rotate.mjs +5 -4
  176. package/lib/mcp/broker.mjs +332 -17
  177. package/lib/mcp/denied-store.mjs +95 -0
  178. package/lib/mcp/destructive-gate.mjs +30 -0
  179. package/lib/mcp/dispatch-envelope.mjs +199 -0
  180. package/lib/mcp/memory-bridge.mjs +2 -1
  181. package/lib/mcp/server.mjs +154 -1411
  182. package/lib/mcp/tool-definitions-memory.mjs +376 -0
  183. package/lib/mcp/tool-definitions-project.mjs +271 -0
  184. package/lib/mcp/tool-definitions-skills.mjs +311 -0
  185. package/lib/mcp/tool-definitions-workflow.mjs +398 -0
  186. package/lib/mcp/tool-definitions.mjs +25 -0
  187. package/lib/mcp/tool-registry.mjs +107 -0
  188. package/lib/mcp/tool-safety.mjs +1 -0
  189. package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
  190. package/lib/mcp/tools/orchestration-run.mjs +165 -25
  191. package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
  192. package/lib/mcp/tools/provider-write.mjs +187 -0
  193. package/lib/mcp/tools/scope.mjs +0 -3
  194. package/lib/mcp/tools/skills.mjs +1 -1
  195. package/lib/mcp/tools/storage.mjs +0 -8
  196. package/lib/mcp/transport/auth.mjs +238 -0
  197. package/lib/mcp/transport/http.mjs +136 -0
  198. package/lib/mcp/transport/mode.mjs +31 -0
  199. package/lib/mcp/transport/stdio.mjs +22 -0
  200. package/lib/mcp-catalog.json +4 -4
  201. package/lib/mcp-manager.mjs +34 -23
  202. package/lib/mcp-platform-config.mjs +31 -7
  203. package/lib/mode-capabilities.mjs +109 -0
  204. package/lib/model-router.mjs +42 -9
  205. package/lib/models/catalog.mjs +40 -1
  206. package/lib/net-guard.mjs +247 -0
  207. package/lib/observation-store.mjs +6 -2
  208. package/lib/ollama/provision-context.mjs +2 -1
  209. package/lib/opencode-config.mjs +22 -3
  210. package/lib/opencode-runtime-plugin.mjs +120 -2
  211. package/lib/oracle/actions.mjs +204 -10
  212. package/lib/oracle/cli.mjs +24 -3
  213. package/lib/oracle/dispatch.mjs +2 -1
  214. package/lib/oracle/execute.mjs +2 -2
  215. package/lib/oracle/gaps.mjs +3 -3
  216. package/lib/oracle/heartbeat.mjs +53 -0
  217. package/lib/oracle/read-model.mjs +69 -13
  218. package/lib/oracle/reconcile.mjs +3 -46
  219. package/lib/oracle/routing.mjs +37 -31
  220. package/lib/oracle/synthesize.mjs +1 -1
  221. package/lib/orchestration/classification.mjs +434 -0
  222. package/lib/orchestration/delegation-flow.mjs +132 -0
  223. package/lib/orchestration/flow-selection.mjs +418 -0
  224. package/lib/orchestration/gates.mjs +244 -0
  225. package/lib/orchestration/host-sampling.mjs +121 -0
  226. package/lib/orchestration/policy-constants.mjs +48 -0
  227. package/lib/orchestration/provider-outcome.mjs +197 -0
  228. package/lib/orchestration/readiness.mjs +114 -13
  229. package/lib/orchestration/run-store-postgres.mjs +32 -26
  230. package/lib/orchestration/run-store-sqlite.mjs +8 -14
  231. package/lib/orchestration/run-store.mjs +25 -12
  232. package/lib/orchestration/runtime.mjs +446 -39
  233. package/lib/orchestration/store.mjs +87 -12
  234. package/lib/orchestration/trace-store.mjs +125 -0
  235. package/lib/orchestration/web-capability.mjs +3 -2
  236. package/lib/orchestration/worker-runtime.mjs +162 -0
  237. package/lib/orchestration/worker.mjs +528 -89
  238. package/lib/orchestration-policy.mjs +67 -1111
  239. package/lib/packs/cli.mjs +144 -0
  240. package/lib/packs/core-pack.mjs +112 -0
  241. package/lib/packs/enablement.mjs +187 -0
  242. package/lib/packs/index.mjs +23 -0
  243. package/lib/packs/loader.mjs +176 -0
  244. package/lib/packs/manifest-schema.mjs +58 -0
  245. package/lib/packs/prompts.mjs +120 -0
  246. package/lib/packs/validate.mjs +208 -0
  247. package/lib/plugin-registry.mjs +4 -5
  248. package/lib/policy/audit-gate.mjs +42 -0
  249. package/lib/policy/consumption-budget.mjs +149 -0
  250. package/lib/policy/engine.mjs +81 -6
  251. package/lib/policy/role-authority.mjs +89 -0
  252. package/lib/prompt-composer.js +19 -3
  253. package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
  254. package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
  255. package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
  256. package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
  257. package/lib/providers/contract/adapters/github/index.mjs +38 -3
  258. package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
  259. package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
  260. package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
  261. package/lib/providers/contract/adapters/jira/manifest.json +17 -0
  262. package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
  263. package/lib/providers/contract.mjs +207 -0
  264. package/lib/providers/credential-bootstrap.mjs +7 -4
  265. package/lib/providers/credential-sources.mjs +14 -2
  266. package/lib/providers/directory/index.mjs +273 -0
  267. package/lib/providers/feedback/index.mjs +392 -0
  268. package/lib/providers/filter-audit.mjs +68 -0
  269. package/lib/providers/filter-schema.mjs +54 -0
  270. package/lib/providers/github/index.mjs +31 -0
  271. package/lib/providers/instance-config.mjs +215 -0
  272. package/lib/providers/op-locate.mjs +96 -0
  273. package/lib/providers/op-run.mjs +64 -9
  274. package/lib/providers/registry.mjs +26 -3
  275. package/lib/providers/secret-audit-wiring.mjs +6 -0
  276. package/lib/providers/secret-resolver.mjs +240 -23
  277. package/lib/publish-template.mjs +6 -3
  278. package/lib/publish-tooling.mjs +4 -0
  279. package/lib/publish.mjs +32 -4
  280. package/lib/queue/pg-queue.mjs +395 -0
  281. package/lib/reflect.mjs +2 -1
  282. package/lib/registry/assemble.mjs +28 -2
  283. package/lib/registry/consolidation.mjs +8 -1
  284. package/lib/registry/custom-scaffold.mjs +200 -0
  285. package/lib/registry/custom-schema.mjs +137 -0
  286. package/lib/registry/loader.mjs +8 -3
  287. package/lib/registry/manifests/format-engines.default.json +15 -0
  288. package/lib/registry/manifests/surface-map.default.json +46 -0
  289. package/lib/registry/retired-paths.mjs +1 -0
  290. package/lib/registry/surface-map.mjs +100 -50
  291. package/lib/render-visual-check.mjs +240 -0
  292. package/lib/resources/budget.mjs +40 -17
  293. package/lib/roles/flavor-bindings.mjs +36 -14
  294. package/lib/roles/gateway.mjs +15 -8
  295. package/lib/roots.mjs +107 -0
  296. package/lib/runtime/uv-bootstrap.mjs +32 -6
  297. package/lib/runtime/whisper-bootstrap.mjs +11 -3
  298. package/lib/runtime-env.mjs +5 -2
  299. package/lib/scheduler/index.mjs +8 -20
  300. package/lib/schema-infer.mjs +31 -2
  301. package/lib/scopes/lifecycle.mjs +29 -25
  302. package/lib/scopes/loader.mjs +49 -12
  303. package/lib/scopes/teams.mjs +1 -1
  304. package/lib/security/ingest-boundary.mjs +80 -0
  305. package/lib/security/recall-wrapper.mjs +99 -0
  306. package/lib/security/trust.mjs +132 -0
  307. package/lib/service-manager.mjs +38 -19
  308. package/lib/setup.mjs +41 -23
  309. package/lib/skills-apply.mjs +4 -2
  310. package/lib/specialists/postconditions.mjs +2 -2
  311. package/lib/state-root.mjs +147 -0
  312. package/lib/status.mjs +597 -6
  313. package/lib/storage/admin.mjs +77 -5
  314. package/lib/storage/backend-registry.mjs +73 -0
  315. package/lib/storage/backend.mjs +63 -9
  316. package/lib/storage/embeddings-openai.mjs +12 -2
  317. package/lib/storage/hybrid-query.mjs +11 -3
  318. package/lib/storage/retrieval-hardening.mjs +384 -0
  319. package/lib/storage/shared-memory.mjs +158 -0
  320. package/lib/storage/vector-client.mjs +69 -2
  321. package/lib/team/health.mjs +72 -0
  322. package/lib/telemetry/backends/local.mjs +9 -3
  323. package/lib/telemetry/client.mjs +3 -7
  324. package/lib/template-registry.mjs +10 -12
  325. package/lib/tenant/context.mjs +82 -0
  326. package/lib/tenant/isolation.mjs +129 -0
  327. package/lib/test-corpus-inventory.mjs +104 -2
  328. package/lib/uninstall/uninstall.mjs +78 -12
  329. package/lib/validator.mjs +4 -6
  330. package/lib/worker/entrypoint.mjs +2 -1
  331. package/lib/worker/run.mjs +2 -2
  332. package/lib/worker/trace.mjs +5 -11
  333. package/lib/workflow-state.mjs +52 -8
  334. package/lib/workflows/liveness.mjs +203 -0
  335. package/lib/workflows/loader.mjs +177 -0
  336. package/lib/workflows/manifest-schema.mjs +71 -0
  337. package/lib/workflows/surface-parity.mjs +118 -0
  338. package/lib/workflows/validate.mjs +127 -0
  339. package/lib/writes/control-plane.mjs +140 -0
  340. package/lib/writes/envelope.mjs +206 -0
  341. package/lib/writes/sent-log.mjs +86 -0
  342. package/lib/writes/write-intent.mjs +109 -0
  343. package/package.json +16 -8
  344. package/personas/construct.md +12 -3
  345. package/registry/capabilities.json +143 -36
  346. package/rules/common/comments.md +1 -0
  347. package/schemas/project-config.schema.json +0 -12
  348. package/schemas/unified-registry.schema.json +2 -4
  349. package/scripts/sync-specialists.mjs +284 -81
  350. package/skills/docs/adr-workflow.md +1 -1
  351. package/skills/docs/codebase-research-workflow.md +3 -3
  352. package/skills/docs/prd-workflow.md +5 -6
  353. package/skills/docs/runbook-workflow.md +2 -2
  354. package/skills/docs/user-research-workflow.md +3 -3
  355. package/skills/operating/fleet-health-routing.md +77 -0
  356. package/skills/roles/ai-engineer.md +1 -1
  357. package/skills/roles/architect.md +0 -1
  358. package/skills/roles/business-strategist.md +1 -1
  359. package/skills/roles/data-analyst.telemetry.md +1 -1
  360. package/skills/roles/data-engineer.md +1 -1
  361. package/skills/roles/data-engineer.pipeline.md +1 -1
  362. package/skills/roles/data-engineer.vector-retrieval.md +1 -2
  363. package/skills/roles/data-engineer.warehouse.md +1 -1
  364. package/skills/roles/designer.accessibility.md +1 -1
  365. package/skills/roles/designer.md +0 -1
  366. package/skills/roles/devil-advocate.md +1 -1
  367. package/skills/roles/docs-keeper.md +1 -1
  368. package/skills/roles/evaluator.md +1 -1
  369. package/skills/roles/explorer.md +1 -1
  370. package/skills/roles/platform-engineer.md +1 -1
  371. package/skills/roles/qa.ai-eval.md +1 -2
  372. package/skills/roles/qa.api-contract.md +0 -1
  373. package/skills/roles/qa.data-pipeline.md +0 -1
  374. package/skills/roles/qa.md +0 -1
  375. package/skills/roles/qa.web-ui.md +0 -1
  376. package/skills/roles/release-manager.md +1 -1
  377. package/skills/roles/researcher.md +0 -2
  378. package/skills/roles/reviewer.md +0 -3
  379. package/skills/roles/security.ai.md +1 -1
  380. package/skills/roles/security.legal-compliance.md +1 -1
  381. package/skills/roles/security.md +0 -1
  382. package/skills/roles/security.privacy.md +0 -1
  383. package/skills/roles/security.supply-chain.md +1 -1
  384. package/skills/roles/sre.md +1 -1
  385. package/skills/roles/test-automation.md +1 -1
  386. package/skills/roles/trace-reviewer.md +1 -1
  387. package/skills/roles/ux-researcher.md +1 -1
  388. package/specialists/artifact-manifest.json +36 -36
  389. package/specialists/org/contracts/accessibility-to-qa.json +11 -3
  390. package/specialists/org/contracts/any-to-business-strategist.json +19 -7
  391. package/specialists/org/contracts/any-to-debugger.json +7 -1
  392. package/specialists/org/contracts/any-to-designer.json +7 -1
  393. package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
  394. package/specialists/org/contracts/any-to-explorer.json +13 -4
  395. package/specialists/org/contracts/any-to-sre-incident.json +6 -2
  396. package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
  397. package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
  398. package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
  399. package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
  400. package/specialists/org/contracts/architect-to-engineer.json +20 -4
  401. package/specialists/org/contracts/architect-to-evaluator.json +15 -5
  402. package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
  403. package/specialists/org/contracts/architect-to-operations.json +17 -4
  404. package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
  405. package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
  406. package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
  407. package/specialists/org/contracts/engineer-to-qa.json +20 -4
  408. package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
  409. package/specialists/org/contracts/explorer-to-engineer.json +11 -3
  410. package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
  411. package/specialists/org/contracts/operations-to-user.json +53 -0
  412. package/specialists/org/contracts/operations-triage-output.json +53 -0
  413. package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
  414. package/specialists/org/contracts/product-manager-to-architect.json +30 -10
  415. package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
  416. package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
  417. package/specialists/org/contracts/qa-to-release-manager.json +11 -3
  418. package/specialists/org/contracts/researcher-to-architect.json +10 -2
  419. package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
  420. package/specialists/org/contracts/reviewer-to-security.json +17 -3
  421. package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
  422. package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
  423. package/specialists/org/contracts/user-to-construct.json +11 -4
  424. package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
  425. package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
  426. package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
  427. package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
  428. package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
  429. package/specialists/org/groups/engineering-group.json +2 -6
  430. package/specialists/org/groups/governance-group.json +2 -3
  431. package/specialists/org/groups/operations-group.json +5 -8
  432. package/specialists/org/groups/product-group.json +4 -8
  433. package/specialists/org/groups/quality-group.json +3 -7
  434. package/specialists/org/groups/strategy-group.json +6 -9
  435. package/specialists/org/models.json.example +8 -0
  436. package/specialists/org/scopes/creative.json +6 -1
  437. package/specialists/org/scopes/operations.json +7 -1
  438. package/specialists/org/scopes/research.json +6 -1
  439. package/specialists/org/scopes/rnd.json +7 -1
  440. package/specialists/org/specialists/cx-architect.json +27 -8
  441. package/specialists/org/specialists/cx-designer.json +22 -8
  442. package/specialists/org/specialists/cx-engineer.json +71 -7
  443. package/specialists/org/specialists/cx-operations.json +89 -15
  444. package/specialists/org/specialists/cx-orchestrator.json +16 -4
  445. package/specialists/org/specialists/cx-product-manager.json +28 -9
  446. package/specialists/org/specialists/cx-qa.json +16 -6
  447. package/specialists/org/specialists/cx-researcher.json +40 -7
  448. package/specialists/org/specialists/cx-reviewer.json +61 -11
  449. package/specialists/org/specialists/cx-security.json +30 -10
  450. package/specialists/org/teams/design-team.json +3 -4
  451. package/specialists/org/teams/engineering-team.json +3 -10
  452. package/specialists/org/teams/governance-team.json +3 -5
  453. package/specialists/org/teams/operations-team.json +6 -12
  454. package/specialists/org/teams/product-management-team.json +2 -3
  455. package/specialists/org/teams/quality-team.json +4 -12
  456. package/specialists/org/teams/research-team.json +3 -3
  457. package/specialists/org/teams/strategy-team.json +7 -14
  458. package/specialists/prompts/cx-architect.md +20 -1
  459. package/specialists/prompts/cx-data-analyst.md +1 -1
  460. package/specialists/prompts/cx-designer.md +12 -4
  461. package/specialists/prompts/cx-engineer.md +15 -1
  462. package/specialists/prompts/cx-operations.md +14 -2
  463. package/specialists/prompts/cx-orchestrator.md +9 -5
  464. package/specialists/prompts/cx-product-manager.md +5 -1
  465. package/specialists/prompts/cx-qa.md +6 -2
  466. package/specialists/prompts/cx-researcher.md +25 -30
  467. package/specialists/prompts/cx-reviewer.md +19 -1
  468. package/specialists/prompts/cx-security.md +5 -1
  469. package/templates/distribution/construct-brand.typ +123 -59
  470. package/templates/distribution/construct-decision.typ +6 -3
  471. package/templates/distribution/construct-pdf.typ +11 -4
  472. package/templates/distribution/construct-prd.typ +6 -3
  473. package/templates/distribution/construct-research.typ +7 -4
  474. package/lib/providers/contract/adapters/git/index.mjs +0 -115
  475. package/lib/providers/contract/adapters/slack/index.mjs +0 -175
  476. package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
  477. package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
  478. package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
  479. package/specialists/org/contracts/designer-to-accessibility.json +0 -36
  480. package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
  481. package/specialists/org/contracts/qa-to-test-automation.json +0 -42
  482. package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
  483. package/specialists/org/contracts/sre-to-release-manager.json +0 -36
  484. package/specialists/org/specialists/cx-accessibility.json +0 -69
  485. package/specialists/org/specialists/cx-ai-engineer.json +0 -75
  486. package/specialists/org/specialists/cx-business-strategist.json +0 -72
  487. package/specialists/org/specialists/cx-data-engineer.json +0 -71
  488. package/specialists/org/specialists/cx-devil-advocate.json +0 -71
  489. package/specialists/org/specialists/cx-docs-keeper.json +0 -82
  490. package/specialists/org/specialists/cx-evaluator.json +0 -69
  491. package/specialists/org/specialists/cx-explorer.json +0 -69
  492. package/specialists/org/specialists/cx-legal-compliance.json +0 -74
  493. package/specialists/org/specialists/cx-oracle.json +0 -46
  494. package/specialists/org/specialists/cx-platform-engineer.json +0 -80
  495. package/specialists/org/specialists/cx-rd-lead.json +0 -73
  496. package/specialists/org/specialists/cx-release-manager.json +0 -77
  497. package/specialists/org/specialists/cx-sre.json +0 -94
  498. package/specialists/org/specialists/cx-test-automation.json +0 -69
  499. package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
  500. package/specialists/org/specialists/cx-ux-researcher.json +0 -68
  501. package/specialists/org/teams/accessibility-team.json +0 -39
  502. package/specialists/org/teams/ux-research-team.json +0 -39
  503. package/specialists/prompts/cx-accessibility.md +0 -55
  504. package/specialists/prompts/cx-ai-engineer.md +0 -124
  505. package/specialists/prompts/cx-business-strategist.md +0 -58
  506. package/specialists/prompts/cx-data-engineer.md +0 -55
  507. package/specialists/prompts/cx-devil-advocate.md +0 -59
  508. package/specialists/prompts/cx-docs-keeper.md +0 -164
  509. package/specialists/prompts/cx-evaluator.md +0 -49
  510. package/specialists/prompts/cx-explorer.md +0 -71
  511. package/specialists/prompts/cx-legal-compliance.md +0 -58
  512. package/specialists/prompts/cx-oracle.md +0 -98
  513. package/specialists/prompts/cx-platform-engineer.md +0 -97
  514. package/specialists/prompts/cx-rd-lead.md +0 -59
  515. package/specialists/prompts/cx-release-manager.md +0 -54
  516. package/specialists/prompts/cx-sre.md +0 -111
  517. package/specialists/prompts/cx-test-automation.md +0 -55
  518. package/specialists/prompts/cx-trace-reviewer.md +0 -101
  519. package/specialists/prompts/cx-ux-researcher.md +0 -53
@@ -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`);
@@ -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
  /**
@@ -0,0 +1,53 @@
1
+ /**
2
+ * lib/oracle/heartbeat.mjs — oracle producer liveness from the last-tick record.
3
+ *
4
+ * The oracle daemon records last-tick.json when a tick completes and idles itself
5
+ * out after a few no-work ticks, so a stale tick on its own is not death — it is a
6
+ * dead-producer signal only when oracle work is actually waiting (pending approvals
7
+ * or unaddressed gaps). Consumers therefore pair heartbeatFreshness with a pending
8
+ * count; this module owns only the age check. It is deliberately distinct from
9
+ * verdictFreshness in lib/intake/session-prelude.mjs, which ages the verdict's own
10
+ * `at` (is the answer current) rather than the producer's tick `at` (is anything
11
+ * still generating answers). formatAge is shared with the verdict path.
12
+ */
13
+
14
+ import { readLastTick } from './index.mjs';
15
+
16
+ // Six missed 5-minute intervals clears the daemon's idle-shutdown window before a
17
+ // stale tick is treated as a stalled producer, so a briefly-quiet daemon is never
18
+ // mislabelled dead.
19
+ export const ORACLE_TICK_INTERVAL_MS = 5 * 60_000;
20
+ export const HEARTBEAT_STALE_MS = ORACLE_TICK_INTERVAL_MS * 6;
21
+
22
+ export function formatAge(ageMs) {
23
+ if (!Number.isFinite(ageMs)) return 'unknown';
24
+ const hours = Math.floor(ageMs / (60 * 60 * 1000));
25
+ if (hours < 1) return '<1h';
26
+ if (hours < 24) return `${hours}h`;
27
+ return `${Math.floor(hours / 24)}d`;
28
+ }
29
+
30
+ // Missing or unparseable `at` resolves to Infinity — stale, never fresh — so an
31
+ // absent tick record reads as a stalled producer rather than a healthy one.
32
+
33
+ export function heartbeatFreshness(lastTick, now = Date.now()) {
34
+ const at = lastTick?.at;
35
+ const parsed = at ? Date.parse(at) : NaN;
36
+ const ageMs = Number.isFinite(parsed) ? now - parsed : Infinity;
37
+ return {
38
+ ageMs,
39
+ stale: !Number.isFinite(ageMs) || ageMs > HEARTBEAT_STALE_MS,
40
+ asOf: Number.isFinite(parsed) ? new Date(parsed).toISOString() : null,
41
+ };
42
+ }
43
+
44
+ // Honors the oracle off-switch so a deliberately disabled daemon is never flagged.
45
+
46
+ export function readHeartbeatStatus({ homeDir, env = process.env } = {}) {
47
+ if (env?.CONSTRUCT_ORACLE === 'off' || env?.CONSTRUCT_ORACLE === '0') {
48
+ return { enabled: false, lastTick: null, ageMs: null, stale: false, asOf: null };
49
+ }
50
+ const lastTick = readLastTick(homeDir);
51
+ const { ageMs, stale, asOf } = heartbeatFreshness(lastTick);
52
+ return { enabled: true, lastTick, ageMs, stale, asOf };
53
+ }