@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
@@ -0,0 +1,92 @@
1
+ /**
2
+ * lib/doctor/watchers/oracle-liveness.mjs — oracle producer-liveness watcher.
3
+ *
4
+ * Applies the same idle-safe gate as lib/intake/session-prelude.mjs's
5
+ * buildOraclePrelude()/readOracleDockState(): the oracle daemon idles itself
6
+ * out after a few no-work ticks, so a stale last-tick record alone is not
7
+ * death — only heartbeatFreshness().stale AND at least one pending oracle
8
+ * item (lib/oracle/actions.mjs listPending(projectDir), filtered to
9
+ * status==='pending') together mean the producer looks stalled rather than
10
+ * quiet. Escalates once per stale episode (deduped by the last-tick record's
11
+ * own `asOf`, so a producer stuck stale for hours doesn't spam every tick);
12
+ * a fresh tick or an empty pending queue clears the episode. Tick: 60s.
13
+ *
14
+ * Needs the project directory (not just homeDir) to call listPending — the
15
+ * doctor daemon's tick loop (lib/doctor/index.mjs) threads `projectDir`
16
+ * through runWatcher()/tick(ctx) for this reason; every other watcher
17
+ * ignores that extra argument.
18
+ */
19
+
20
+ import { readHeartbeatStatus, formatAge } from '../../oracle/heartbeat.mjs';
21
+ import { listPending } from '../../oracle/actions.mjs';
22
+ import { record } from '../audit.mjs';
23
+ import { escalate } from '../escalate.mjs';
24
+
25
+ export const name = 'oracle-liveness';
26
+ export const intervalMs = 60 * 1000;
27
+
28
+ let lastEscalatedAsOf = null;
29
+
30
+ export async function tick({ projectDir = process.cwd(), env = process.env, homeDir } = {}) {
31
+ const actions = [];
32
+ const escalations = [];
33
+ const notes = [];
34
+
35
+ const heartbeat = readHeartbeatStatus({ homeDir, env });
36
+ if (!heartbeat.enabled) {
37
+ notes.push({ enabled: false });
38
+ return { actions, escalations, notes };
39
+ }
40
+
41
+ let pending;
42
+ try {
43
+ pending = listPending(projectDir).filter((p) => p.status === 'pending');
44
+ } catch (err) {
45
+ notes.push({ error: `listPending failed: ${err.message}` });
46
+ return { actions, escalations, notes };
47
+ }
48
+
49
+ notes.push({ stale: heartbeat.stale, ageMs: heartbeat.ageMs, asOf: heartbeat.asOf, pendingCount: pending.length });
50
+
51
+ const stalled = heartbeat.stale && pending.length > 0;
52
+
53
+ if (!stalled) {
54
+ if (lastEscalatedAsOf) {
55
+ record({
56
+ kind: 'recovery',
57
+ watcher: name,
58
+ summary: `oracle producer recovered (fresh tick or pending queue cleared; was stalled as of ${lastEscalatedAsOf})`,
59
+ context: { previousAsOf: lastEscalatedAsOf },
60
+ });
61
+ }
62
+ lastEscalatedAsOf = null;
63
+ return { actions, escalations, notes };
64
+ }
65
+
66
+ const episodeKey = heartbeat.asOf || 'unknown';
67
+ if (lastEscalatedAsOf === episodeKey) return { actions, escalations, notes };
68
+
69
+ const summary = `oracle-producer-stalled: no oracle tick in ${formatAge(heartbeat.ageMs)} while ${pending.length} pending oracle work item(s) wait (last tick as of ${heartbeat.asOf ?? 'unknown'})`;
70
+ record({
71
+ kind: 'sample',
72
+ watcher: name,
73
+ result: 'stalled',
74
+ summary,
75
+ context: { ageMs: heartbeat.ageMs, asOf: heartbeat.asOf, pendingCount: pending.length },
76
+ });
77
+
78
+ const result = await escalate({
79
+ watcher: name,
80
+ eventType: 'oracle.producer_stalled',
81
+ summary,
82
+ context: { ageMs: heartbeat.ageMs, asOf: heartbeat.asOf, pendingCount: pending.length },
83
+ });
84
+ lastEscalatedAsOf = episodeKey;
85
+ escalations.push({ eventType: 'oracle.producer_stalled', result });
86
+
87
+ return { actions, escalations, notes };
88
+ }
89
+
90
+ export function __resetOracleLivenessWatcherState() {
91
+ lastEscalatedAsOf = null;
92
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * lib/doctor/watchers/orchestration-runs.mjs — surfaces error/degraded and
3
+ * stale awaiting-host orchestration runs to the doctor audit log.
4
+ *
5
+ * lib/orchestration/runtime.mjs persists a terminal 'error' status (thrown
6
+ * mid-execution) or a 'degraded'/'completed-with-failures' status (a run
7
+ * that finished but not cleanly) to the run store via getRun/getRuns. Each
8
+ * tick here scans the most recent runs for the project the daemon is
9
+ * running in and records a finding when any carry an error or
10
+ * degraded/failed terminal status, so `construct doctor logs
11
+ * --watcher=orchestration-runs` (and the audit trail generally) reflects
12
+ * runs that did not complete cleanly, not just clean completions.
13
+ *
14
+ * A host-backend run standing at 'awaiting-host' is not itself an error — it
15
+ * is the designed non-terminal state while a host executes the materialized
16
+ * prompts — but a run whose `updatedAt` has not moved in
17
+ * CONSTRUCT_AWAITING_HOST_STALE_MS (default 30 minutes) means the host that
18
+ * requested it likely disconnected before submitting every task result.
19
+ * That is flagged as a separate advisory finding, never a hard failure, so an
20
+ * abandoned run stays visible rather than silently invisible.
21
+ *
22
+ * Read-only and advisory: this watcher never mutates a run or retries it.
23
+ */
24
+
25
+ import { record } from '../audit.mjs';
26
+ import { resolveNonNegativeSetting } from '../../env-config.mjs';
27
+
28
+ const ATTENTION_STATUSES = new Set(['error', 'degraded', 'completed-with-failures']);
29
+
30
+ // 30 minutes: long enough that a host actively working through a multi-task
31
+ // run (each submission round-trips the calling agent's own reasoning) is never
32
+ // falsely flagged, short enough that a genuinely disconnected session surfaces
33
+ // within one doctor watch cycle or two.
34
+
35
+ const DEFAULT_AWAITING_HOST_STALE_MS = 30 * 60 * 1000;
36
+
37
+ export const name = 'orchestration-runs';
38
+ export const intervalMs = 10 * 60 * 1000;
39
+
40
+ function projectRoot() {
41
+ return process.env.CONSTRUCT_PROJECT_ROOT || process.cwd();
42
+ }
43
+
44
+ function needsAttention(run) {
45
+ return ATTENTION_STATUSES.has(run.status) || run.degraded === true;
46
+ }
47
+
48
+ function isStaleAwaitingHost(run, staleMs, now) {
49
+ if (run.status !== 'awaiting-host') return false;
50
+ const touchedAt = Date.parse(run.updatedAt || run.createdAt || '');
51
+ if (!Number.isFinite(touchedAt)) return false;
52
+ return now - touchedAt >= staleMs;
53
+ }
54
+
55
+ export async function tick() {
56
+ const actions = [];
57
+ const escalations = [];
58
+ const root = projectRoot();
59
+ const now = Date.now();
60
+ const staleMs = resolveNonNegativeSetting(process.env, 'CONSTRUCT_AWAITING_HOST_STALE_MS', DEFAULT_AWAITING_HOST_STALE_MS);
61
+
62
+ const { getRuns } = await import('../../orchestration/runtime.mjs');
63
+ const runs = await getRuns(root, { env: process.env, limit: 50 });
64
+ const affected = runs.filter(needsAttention);
65
+ const staleAwaitingHost = runs.filter((r) => isStaleAwaitingHost(r, staleMs, now));
66
+
67
+ if (affected.length > 0) {
68
+ // getRuns/listRuns sorts newest-first (createdAt desc) across every store
69
+ // backend, so the first affected entry is the newest.
70
+ const newest = affected[0];
71
+ record({
72
+ kind: 'finding',
73
+ watcher: name,
74
+ result: 'attention',
75
+ summary: `${affected.length} of ${runs.length} recent run(s) carry an error/degraded status; newest is ${newest.runId} (${newest.status})`,
76
+ context: {
77
+ scanned: runs.length,
78
+ affectedCount: affected.length,
79
+ newestRunId: newest.runId,
80
+ newestStatus: newest.status,
81
+ affectedRunIds: affected.map((r) => r.runId).slice(0, 10),
82
+ },
83
+ });
84
+ }
85
+
86
+ if (staleAwaitingHost.length > 0) {
87
+ const newest = staleAwaitingHost[0];
88
+ record({
89
+ kind: 'finding',
90
+ watcher: name,
91
+ result: 'attention',
92
+ summary: `${staleAwaitingHost.length} of ${runs.length} recent run(s) have stood 'awaiting-host' for over ${Math.round(staleMs / 60000)} minute(s) with no submitted result; newest is ${newest.runId}`,
93
+ context: {
94
+ scanned: runs.length,
95
+ staleAwaitingHostCount: staleAwaitingHost.length,
96
+ staleThresholdMs: staleMs,
97
+ newestRunId: newest.runId,
98
+ staleRunIds: staleAwaitingHost.map((r) => r.runId).slice(0, 10),
99
+ },
100
+ });
101
+ }
102
+
103
+ if (affected.length === 0 && staleAwaitingHost.length === 0) {
104
+ return { actions, escalations, notes: [{ scanned: runs.length, affected: 0, staleAwaitingHost: 0 }] };
105
+ }
106
+
107
+ return { actions, escalations, notes: [{ scanned: runs.length, affected: affected.length, staleAwaitingHost: staleAwaitingHost.length }] };
108
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * lib/doctor/watchers/provider-breaker.mjs — surfaces open provider circuit breakers.
3
+ *
4
+ * Reads lib/providers/circuit-breaker.mjs's describeBreakers(), which lib/
5
+ * providers/registry.mjs populates by wrapping every resolved provider's read/
6
+ * search/watch/write/webhook methods (failureThreshold=5, cooldownMs=30_000).
7
+ * Surfaces that breaker state continuously, so a downed provider shows as
8
+ * "provider X circuit OPEN since T, N failures" in the doctor audit log and
9
+ * role-escalation path instead of a mystery failure.
10
+ *
11
+ * Escalates once per open episode (keyed by the breaker's own `openedAt`), not
12
+ * once per tick, so a provider stuck OPEN for an hour doesn't spam. Recovery
13
+ * to CLOSED/HALF_OPEN logs a plain recovery record. Tick: 60s.
14
+ */
15
+
16
+ import { record } from '../audit.mjs';
17
+ import { escalate } from '../escalate.mjs';
18
+ import { describeBreakers } from '../../providers/circuit-breaker.mjs';
19
+
20
+ export const name = 'provider-breaker';
21
+ export const intervalMs = 60 * 1000;
22
+
23
+ const PROVIDER_KEY_PREFIX = 'provider:';
24
+ const escalatedOpenedAt = {};
25
+
26
+ export async function tick() {
27
+ const actions = [];
28
+ const escalations = [];
29
+ const notes = [];
30
+
31
+ const breakers = describeBreakers().filter((b) => b.key.startsWith(PROVIDER_KEY_PREFIX));
32
+
33
+ for (const breaker of breakers) {
34
+ const providerId = breaker.key.slice(PROVIDER_KEY_PREFIX.length);
35
+ notes.push({ id: providerId, state: breaker.state, failureCount: breaker.failureCount });
36
+
37
+ if (breaker.state !== 'open') {
38
+ if (escalatedOpenedAt[providerId]) {
39
+ record({
40
+ kind: 'recovery',
41
+ watcher: name,
42
+ target: providerId,
43
+ summary: `${providerId} provider circuit recovered (was OPEN since ${new Date(escalatedOpenedAt[providerId]).toISOString()})`,
44
+ context: { previousOpenedAt: escalatedOpenedAt[providerId] },
45
+ });
46
+ }
47
+ delete escalatedOpenedAt[providerId];
48
+ continue;
49
+ }
50
+
51
+ if (escalatedOpenedAt[providerId] === breaker.openedAt) continue;
52
+
53
+ const summary = `${providerId} provider circuit OPEN since ${new Date(breaker.openedAt).toISOString()} (${breaker.failureCount} consecutive failures)`;
54
+ record({
55
+ kind: 'sample',
56
+ watcher: name,
57
+ target: providerId,
58
+ result: 'open',
59
+ summary,
60
+ context: { openedAt: breaker.openedAt, failureCount: breaker.failureCount },
61
+ });
62
+
63
+ const result = await escalate({
64
+ watcher: name,
65
+ eventType: 'provider.circuit_open',
66
+ summary,
67
+ context: { provider: providerId, openedAt: breaker.openedAt, failureCount: breaker.failureCount },
68
+ });
69
+ escalatedOpenedAt[providerId] = breaker.openedAt;
70
+ escalations.push({ eventType: 'provider.circuit_open', provider: providerId, result });
71
+ }
72
+
73
+ return { actions, escalations, notes };
74
+ }
75
+
76
+ export function __resetProviderBreakerWatcherState() {
77
+ for (const key of Object.keys(escalatedOpenedAt)) delete escalatedOpenedAt[key];
78
+ }
@@ -29,28 +29,45 @@ import { pdfUsesBundledBrandSans } from './brand-fonts.mjs';
29
29
  import { convertDocxToDoc, libreOfficeInstallHint, libreOfficePresent, resolveLibreOfficeBin } from './libreoffice-export.mjs';
30
30
  import { resolveExportBranding } from './export-branding.mjs';
31
31
 
32
- export const EXPORT_FORMATS = ['pdf', 'docx', 'doc', 'deck', 'pptx', 'html', 'rtf', 'odt', 'epub', 'tex', 'txt', 'md', 'mdx'];
33
-
34
32
  const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
35
33
 
36
- // Pandoc drives typeset formats; pptxgenjs builds slide decks; legacy `.doc` has no
37
- // pandoc writer export DOCX first, then LibreOffice headless down-converts.
38
-
39
- const FORMAT_ENGINES = {
40
- pdf: { engine: 'pandoc', writer: null, pdfEngine: 'typst', extraBinaries: ['typst'] },
41
- docx: { engine: 'pandoc', writer: null, pdfEngine: null, extraBinaries: [] },
42
- doc: { engine: 'pandoc', writer: null, pdfEngine: null, extraBinaries: [], via: 'libreoffice', intermediate: 'docx' },
43
- deck: { engine: 'pandoc', writer: null, pdfEngine: null, extraBinaries: [] },
44
- pptx: { engine: 'pptxgenjs', writer: null, pdfEngine: null, extraBinaries: [] },
45
- html: { engine: 'pandoc', writer: null, pdfEngine: null, extraBinaries: [] },
46
- rtf: { engine: 'pandoc', writer: 'rtf', pdfEngine: null, extraBinaries: [] },
47
- odt: { engine: 'pandoc', writer: 'odt', pdfEngine: null, extraBinaries: [] },
48
- epub: { engine: 'pandoc', writer: 'epub', pdfEngine: null, extraBinaries: [] },
49
- tex: { engine: 'pandoc', writer: 'latex', pdfEngine: null, extraBinaries: [] },
50
- txt: { engine: 'pandoc', writer: 'plain', pdfEngine: null, extraBinaries: [] },
51
- md: { engine: 'copy', writer: null, pdfEngine: null, extraBinaries: [] },
52
- mdx: { engine: 'copy', writer: null, pdfEngine: null, extraBinaries: [] },
53
- };
34
+ // LMCP-B7: the format→engine map ships as lib/registry/manifests/format-engines.default.json
35
+ // (Pandoc drives typeset formats; pptxgenjs builds slide decks; legacy `.doc` has no
36
+ // pandoc writer — export DOCX first, then LibreOffice headless down-converts), with an
37
+ // optional `.cx/registry/format-engines.json` project override merged in per-resolve.
38
+ // An override entry adds a format or replaces a default one; every other default format
39
+ // survives untouched. EXPORT_FORMATS stays the default-only key list (byte-identical to
40
+ // the prior hardcoded array) since it's a static, non-cwd-aware export consumed by CLI
41
+ // help text; detect()/exportMarkdown() resolve the cwd-aware merged map per call instead.
42
+
43
+ const DEFAULT_FORMAT_ENGINES_PATH = path.join(REPO_ROOT, 'lib', 'registry', 'manifests', 'format-engines.default.json');
44
+
45
+ function readJsonIfExists(filePath) {
46
+ try {
47
+ if (!fs.existsSync(filePath)) return null;
48
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function loadDefaultFormatEngines() {
55
+ const manifest = readJsonIfExists(DEFAULT_FORMAT_ENGINES_PATH);
56
+ if (!manifest || typeof manifest !== 'object') {
57
+ throw new Error(`lib/document-export.mjs: missing or malformed default manifest at ${DEFAULT_FORMAT_ENGINES_PATH}`);
58
+ }
59
+ return manifest;
60
+ }
61
+
62
+ const DEFAULT_FORMAT_ENGINES = loadDefaultFormatEngines();
63
+
64
+ export const EXPORT_FORMATS = Object.keys(DEFAULT_FORMAT_ENGINES);
65
+
66
+ export function resolveFormatEngines({ repoRoot = REPO_ROOT, cwd = process.cwd() } = {}) {
67
+ const base = repoRoot === REPO_ROOT ? DEFAULT_FORMAT_ENGINES : loadDefaultFormatEngines();
68
+ const override = readJsonIfExists(path.join(cwd, '.cx', 'registry', 'format-engines.json'));
69
+ return override && typeof override === 'object' ? { ...base, ...override } : base;
70
+ }
54
71
 
55
72
  export function htmlTemplatePath(repoRoot = REPO_ROOT) {
56
73
  const p = path.join(repoRoot, 'templates', 'distribution', 'construct-web.html');
@@ -190,7 +207,11 @@ function binVersion(name, args = ['--version'], env = process.env) {
190
207
  return line || null;
191
208
  }
192
209
 
193
- function installHint(name) {
210
+ // Exported so lib/doctor/engine-health.mjs (LMCP-K3) can surface the same
211
+ // per-binary install hint construct doctor reports, without duplicating the
212
+ // hint strings.
213
+
214
+ export function installHint(name) {
194
215
  if (name === 'pandoc') return 'Install pandoc to enable document export (e.g. `brew install pandoc` on macOS, `apt install pandoc` on Debian/Ubuntu, or https://pandoc.org/installing.html).';
195
216
  if (name === 'typst') return 'Install typst to enable PDF export via Pandoc (`brew install typst` on macOS, https://github.com/typst/typst/releases for binaries).';
196
217
  if (name === 'construct-pdf.typ') return 'Bundled PDF template missing from templates/distribution/construct-pdf.typ (Construct install integrity issue).';
@@ -209,12 +230,14 @@ export function detect(format, env = process.env, {
209
230
  branding = 'construct',
210
231
  cwd = process.cwd(),
211
232
  repoRoot = REPO_ROOT,
233
+ libreOfficeExistsSyncFn,
212
234
  } = {}) {
213
235
  const brandingPolicy = resolveExportBranding(format, branding);
236
+ const formatEngines = resolveFormatEngines({ repoRoot, cwd });
214
237
  const config = format === 'pptx' && brandingPolicy.applied === 'plain'
215
238
  ? { engine: 'pandoc', writer: 'pptx', pdfEngine: null, extraBinaries: [] }
216
- : FORMAT_ENGINES[format];
217
- if (!config) return { ok: false, format, branding: brandingPolicy, present: false, missing: [], message: `Unsupported format: ${format}. Supported: ${EXPORT_FORMATS.join(', ')}.` };
239
+ : formatEngines[format];
240
+ if (!config) return { ok: false, format, branding: brandingPolicy, present: false, missing: [], message: `Unsupported format: ${format}. Supported: ${Object.keys(formatEngines).join(', ')}.` };
218
241
  if (config.engine === 'copy') {
219
242
  return { ok: true, format, branding: brandingPolicy, figures: false, present: true, binaries: [], missing: [], message: `Ready: source emit (${format})` };
220
243
  }
@@ -237,7 +260,7 @@ export function detect(format, env = process.env, {
237
260
  // the export passes --reference-doc only when present, so unlike the PDF/HTML/deck templates a
238
261
  // missing reference degrades gracefully rather than blocking the export.
239
262
 
240
- if (format === 'doc' && !libreOfficePresent(env)) {
263
+ if (format === 'doc' && !libreOfficePresent(env, libreOfficeExistsSyncFn ? { existsSyncFn: libreOfficeExistsSyncFn } : undefined)) {
241
264
  required.push('libreoffice');
242
265
  }
243
266
  if (figures) {
@@ -257,7 +280,8 @@ export function detect(format, env = process.env, {
257
280
  return { name, path: pptxgenPresent() ? 'bundled' : null, version: null };
258
281
  }
259
282
  if (name === 'libreoffice') {
260
- return { name, path: libreOfficePresent(env) ? resolveLibreOfficeBin(env) : null, version: null };
283
+ const loOpts = libreOfficeExistsSyncFn ? { existsSyncFn: libreOfficeExistsSyncFn } : undefined;
284
+ return { name, path: libreOfficePresent(env, loOpts) ? resolveLibreOfficeBin(env, loOpts) : null, version: null };
261
285
  }
262
286
  return { name, path: whichBin(name, env), version: null };
263
287
  });
@@ -356,7 +380,8 @@ export function exportMarkdown({
356
380
  repoRoot = REPO_ROOT,
357
381
  } = {}) {
358
382
  if (!inputPath) return { ok: false, format, message: 'exportMarkdown: inputPath is required.' };
359
- if (!EXPORT_FORMATS.includes(format)) return { ok: false, format, inputPath, message: `Unsupported format: ${format}. Supported: ${EXPORT_FORMATS.join(', ')}.` };
383
+ const formatEngines = resolveFormatEngines({ repoRoot, cwd });
384
+ if (!formatEngines[format]) return { ok: false, format, inputPath, message: `Unsupported format: ${format}. Supported: ${Object.keys(formatEngines).join(', ')}.` };
360
385
  if (!fs.existsSync(inputPath)) return { ok: false, format, inputPath, message: `exportMarkdown: input does not exist: ${inputPath}` };
361
386
 
362
387
  const metadata = parseArtifactMetadata(inputPath);
@@ -371,7 +396,7 @@ export function exportMarkdown({
371
396
 
372
397
  const config = format === 'pptx' && brandingPolicy.applied === 'plain'
373
398
  ? { engine: 'pandoc', writer: 'pptx', pdfEngine: null, extraBinaries: [] }
374
- : FORMAT_ENGINES[format];
399
+ : formatEngines[format];
375
400
  const target = path.resolve(outputPath || defaultOutputPath(inputPath, format));
376
401
  fs.mkdirSync(path.dirname(target), { recursive: true });
377
402
 
@@ -1,16 +1,20 @@
1
1
  /**
2
2
  * lib/document-extract/docling-client.mjs — Node side of the docling sidecar.
3
3
  *
4
- * Spawns one long-lived Python process per Node session, framed by newline-
5
- * delimited JSON over stdin/stdout. Avoids per-call uv/venv warm-up
6
- * (~2s otherwise) and keeps the docling model resident.
4
+ * Implements the ingestion sidecar process contract (ADR-0068): spawns one
5
+ * long-lived Python process per Node session, framed by newline-delimited
6
+ * JSON over stdin/stdout, as a direct child (so the sidecar's own parent-PID
7
+ * watch resolves the right PID with no explicit handoff needed). Avoids
8
+ * per-call uv/venv warm-up (~2s otherwise) and keeps the docling model
9
+ * resident.
7
10
  *
8
11
  * Public API:
9
12
  * extractViaDocling(filePath) → { markdown, metadata, droppedInfo }
10
13
  * shutdownDoclingSidecar() → idempotent process exit
11
14
  *
12
- * The first call provisions the venv on demand via ensureDoclingVenv().
13
- * Subsequent calls reuse the running sidecar.
15
+ * The first call provisions the shared, machine-scoped venv on demand via
16
+ * ensureDoclingVenv() never at `construct init`, only on first extraction
17
+ * (construct-rf26.16). Subsequent calls reuse the running sidecar.
14
18
  */
15
19
  import { spawn } from 'node:child_process';
16
20
  import path from 'node:path';
@@ -2,6 +2,11 @@
2
2
  """
3
3
  lib/document-extract/docling-sidecar.py — long-lived JSON-RPC wrapper around docling.
4
4
 
5
+ Governed by the ingestion sidecar process contract (ADR-0068): one JSON-RPC
6
+ surface, no business logic beyond the extraction call itself — routing,
7
+ fallback selection, and degradation-chain decisions all live on the Node
8
+ side (lib/document-extract/docling-client.mjs, lib/document-ingest.mjs).
9
+
5
10
  Protocol: newline-delimited JSON over stdin/stdout. Each request is
6
11
  {"id": <int>, "method": <str>, "params": <obj>}. Each response is
7
12
  {"id": <int>, "result": <obj>} or {"id": <int>, "error": {"code": <str>, "message": <str>}}.
@@ -11,6 +16,15 @@ Methods:
11
16
  - extract {path} → {"markdown": "...", "metadata": {...}, "droppedInfo": [...]}
12
17
  - shutdown → {"ok": true}; process exits after acknowledgement
13
18
 
19
+ Parent-PID watch (ADR-0068): spawned as a direct child of the Node process
20
+ via child_process.spawn, so os.getppid() at start is that Node process's PID.
21
+ A background thread polls it every PARENT_POLL_INTERVAL_S; a `kill -9` (or
22
+ any other death Node's own `process.on('exit')` handler cannot run for) still
23
+ leaves this sidecar exiting on its own rather than orphaned. POSIX reparents
24
+ an orphan to init (or the platform's subreaper), so a ppid change is a
25
+ reliable signal; on Windows, ppid does not get reparented the same way, so
26
+ this is best-effort there rather than a guarantee.
27
+
14
28
  Best-practice notes (2026-06):
15
29
  - One sidecar per Node session, kept warm to avoid uv/venv startup (~2s).
16
30
  - Stdio JSON-RPC is the leanest Python↔Node IPC for non-LLM use cases;
@@ -20,10 +34,23 @@ Best-practice notes (2026-06):
20
34
  loss is observable to the CLI, not silent.
21
35
  """
22
36
  import json
37
+ import os
23
38
  import sys
39
+ import threading
40
+ import time
24
41
  import traceback
25
42
  from pathlib import Path
26
43
 
44
+ PARENT_POLL_INTERVAL_S = 2.0
45
+
46
+
47
+ def watch_parent(initial_ppid):
48
+ while True:
49
+ time.sleep(PARENT_POLL_INTERVAL_S)
50
+ if os.getppid() != initial_ppid:
51
+ os._exit(3)
52
+
53
+
27
54
  try:
28
55
  from docling.document_converter import DocumentConverter
29
56
  from importlib.metadata import version as _pkg_version
@@ -125,6 +152,9 @@ def handle(request):
125
152
 
126
153
 
127
154
  def main():
155
+ watcher = threading.Thread(target=watch_parent, args=(os.getppid(),), daemon=True)
156
+ watcher.start()
157
+
128
158
  for line in sys.stdin:
129
159
  line = line.strip()
130
160
  if not line:
@@ -513,7 +513,7 @@ export function extractDocumentText(filePath, { maxChars = null } = {}) {
513
513
  // Throw a typed error so callers can route to .cx/intake/needs-asr/.
514
514
  if (AUDIO_VIDEO_EXTS.has(extension)) {
515
515
  const err = new Error(
516
- `Audio/video extraction requires ASR. Set CONSTRUCT_ASR_BACKEND=whisper|assemblyai to enable. See docs/guides/intake/audio-video.md.`,
516
+ `Audio/video extraction requires ASR. Install a local whisper ASR (brew install whisper-cpp). See docs/guides/intake/audio-video.md.`,
517
517
  );
518
518
  err.code = 'ASR_REQUIRED';
519
519
  err.extension = extension;
@@ -11,6 +11,7 @@ import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'nod
11
11
  import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from 'node:path';
12
12
  import { extractDocumentTextAsync, extractDocumentTextNodeNative, extractDocumentMetadata, isExtractableDocumentPath } from './document-extract.mjs';
13
13
  import { syncFileStateToSql } from './storage/sync.mjs';
14
+ import { purgeExpiredData } from './storage/admin.mjs';
14
15
  import { stampFrontmatter } from './doc-stamp.mjs';
15
16
  import { KNOWLEDGE_ROOT, KNOWLEDGE_SUBDIRS } from './knowledge/layout.mjs';
16
17
  import { loadProjectConfig } from './config/project-config.mjs';
@@ -338,6 +339,14 @@ export async function ingestDocuments(inputPaths, {
338
339
  env,
339
340
  project: inferProjectName(cwd),
340
341
  });
342
+
343
+ // Retention purge on every real sync, matching the MCP storage_sync tool
344
+ // (lib/mcp/tools/storage.mjs) — best-effort since eviction must never
345
+ // block a successful ingest.
346
+ try {
347
+ const purge = await purgeExpiredData(cwd, { env, project: inferProjectName(cwd) });
348
+ if (purge.status === 'ok') syncResult = { ...syncResult, retention: purge };
349
+ } catch { /* best effort */ }
341
350
  }
342
351
 
343
352
  if (strict && totalDrops > 0) {