@geraldmaron/construct 1.0.14 → 1.0.16

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 (275) hide show
  1. package/README.md +21 -2
  2. package/bin/construct +146 -83
  3. package/commands/build/feature.md +0 -6
  4. package/commands/build/fix.md +0 -6
  5. package/commands/design/access.md +0 -6
  6. package/commands/design/flow.md +0 -6
  7. package/commands/design/ui.md +0 -6
  8. package/commands/measure/experiment.md +0 -6
  9. package/commands/measure/metrics.md +0 -6
  10. package/commands/measure/results.md +0 -6
  11. package/commands/plan/api.md +0 -6
  12. package/commands/plan/challenge.md +0 -6
  13. package/commands/plan/decide.md +0 -6
  14. package/commands/plan/feature.md +0 -7
  15. package/commands/plan/requirements.md +0 -6
  16. package/commands/remember/context.md +0 -6
  17. package/commands/remember/handoff.md +0 -6
  18. package/commands/remember/runbook.md +0 -6
  19. package/commands/review/code.md +0 -6
  20. package/commands/review/quality.md +0 -6
  21. package/commands/review/security.md +0 -6
  22. package/commands/ship/ready.md +0 -6
  23. package/commands/ship/release.md +0 -6
  24. package/commands/ship/status.md +0 -6
  25. package/commands/understand/docs.md +0 -6
  26. package/commands/understand/research.md +0 -6
  27. package/commands/understand/this.md +0 -6
  28. package/commands/understand/why.md +0 -6
  29. package/commands/work/clean.md +0 -6
  30. package/commands/work/drive.md +0 -6
  31. package/commands/work/optimize-prompts.md +0 -6
  32. package/commands/work/parallel-review.md +0 -6
  33. package/db/schema/010_cx_scores.sql +51 -0
  34. package/lib/beads-client.mjs +19 -1
  35. package/lib/cli-commands.mjs +280 -146
  36. package/lib/comment-lint.mjs +5 -1
  37. package/lib/context-state.mjs +15 -3
  38. package/lib/contracts/validate.mjs +72 -18
  39. package/lib/contracts/violation-log.mjs +169 -0
  40. package/lib/document-extract/docling-client.mjs +114 -0
  41. package/lib/document-extract/docling-sidecar.py +122 -0
  42. package/lib/document-extract/whisper-client.mjs +79 -0
  43. package/lib/document-extract.mjs +73 -8
  44. package/lib/document-ingest.mjs +26 -4
  45. package/lib/embed/daemon.mjs +6 -1
  46. package/lib/flavors/loader.mjs +1 -1
  47. package/lib/handoffs/contract.mjs +2 -2
  48. package/lib/hooks/session-start.mjs +1 -1
  49. package/lib/hooks/stop-notify.mjs +7 -0
  50. package/lib/ingest/chunker.mjs +94 -0
  51. package/lib/ingest/pipeline.mjs +53 -0
  52. package/lib/ingest/store.mjs +82 -0
  53. package/lib/install/first-invocation.mjs +5 -1
  54. package/lib/intake/prepare.mjs +22 -4
  55. package/lib/integrations/intake-integrations.mjs +28 -2
  56. package/lib/knowledge/search.mjs +12 -0
  57. package/lib/mcp/server.mjs +142 -1
  58. package/lib/mcp/tools/skills.mjs +6 -3
  59. package/lib/mcp/tools/telemetry.mjs +30 -0
  60. package/lib/mcp/tools/workflow.mjs +6 -2
  61. package/lib/observation-store.mjs +14 -5
  62. package/lib/ollama-manager.mjs +32 -28
  63. package/lib/orchestration-policy.mjs +15 -0
  64. package/lib/prompt-composer.js +11 -1
  65. package/lib/roles/gateway.mjs +30 -1
  66. package/lib/runtime/uv-bootstrap.mjs +129 -0
  67. package/lib/runtime/whisper-bootstrap.mjs +102 -0
  68. package/lib/scheduler/index.mjs +24 -4
  69. package/lib/server/index.mjs +22 -1
  70. package/lib/server/insights.mjs +12 -0
  71. package/lib/server/static/index.html +1 -1
  72. package/lib/setup.mjs +54 -9
  73. package/lib/specialists/postconditions.mjs +1 -1
  74. package/lib/sync/skill-frontmatter.mjs +113 -21
  75. package/lib/tracking-surfaces.mjs +2 -0
  76. package/lib/update.mjs +31 -3
  77. package/lib/upgrade.mjs +31 -4
  78. package/lib/validators/skills.mjs +86 -54
  79. package/package.json +5 -5
  80. package/personas/construct.md +6 -7
  81. package/platforms/claude/CLAUDE.md +43 -15
  82. package/platforms/claude/settings.template.json +1 -1
  83. package/rules/common/beads-hygiene.md +3 -9
  84. package/rules/common/code-review.md +3 -6
  85. package/rules/common/coding-style.md +3 -6
  86. package/rules/common/comments.md +3 -7
  87. package/rules/common/commit-approval.md +3 -6
  88. package/rules/common/cx-agent-routing.md +3 -7
  89. package/rules/common/cx-skill-routing.md +3 -3
  90. package/rules/common/doc-ownership.md +3 -8
  91. package/rules/common/efficiency.md +3 -8
  92. package/rules/common/framing.md +3 -8
  93. package/rules/common/git-workflow.md +3 -5
  94. package/rules/common/no-fabrication.md +4 -12
  95. package/rules/common/patterns.md +3 -5
  96. package/rules/common/release-gates.md +3 -8
  97. package/rules/common/research.md +3 -8
  98. package/rules/common/review-before-change.md +3 -9
  99. package/rules/common/security.md +3 -6
  100. package/rules/common/skill-composition.md +3 -7
  101. package/rules/common/testing.md +3 -6
  102. package/rules/golang/coding-style.md +1 -5
  103. package/rules/golang/hooks.md +1 -5
  104. package/rules/golang/patterns.md +1 -5
  105. package/rules/golang/security.md +1 -5
  106. package/rules/golang/testing.md +1 -5
  107. package/rules/python/coding-style.md +1 -5
  108. package/rules/python/hooks.md +1 -5
  109. package/rules/python/patterns.md +1 -5
  110. package/rules/python/security.md +1 -5
  111. package/rules/python/testing.md +1 -5
  112. package/rules/swift/coding-style.md +1 -5
  113. package/rules/swift/hooks.md +1 -5
  114. package/rules/swift/patterns.md +1 -5
  115. package/rules/swift/security.md +1 -5
  116. package/rules/swift/testing.md +1 -5
  117. package/rules/typescript/coding-style.md +1 -5
  118. package/rules/typescript/hooks.md +1 -5
  119. package/rules/typescript/patterns.md +1 -5
  120. package/rules/typescript/security.md +1 -5
  121. package/rules/typescript/testing.md +1 -5
  122. package/rules/web/coding-style.md +3 -5
  123. package/rules/web/design-quality.md +3 -5
  124. package/rules/web/hooks.md +3 -5
  125. package/rules/web/patterns.md +3 -5
  126. package/rules/web/performance.md +3 -5
  127. package/rules/web/security.md +3 -5
  128. package/rules/web/testing.md +3 -5
  129. package/scripts/sync-specialists.mjs +20 -2
  130. package/skills/ai/agent-dev.md +4 -5
  131. package/skills/ai/llm-security.md +4 -5
  132. package/skills/ai/ml-ops.md +4 -5
  133. package/skills/ai/orchestration-workflow.md +4 -5
  134. package/skills/ai/prompt-and-eval.md +4 -5
  135. package/skills/ai/prompt-optimizer.md +4 -6
  136. package/skills/ai/rag-system.md +4 -5
  137. package/skills/architecture/api-design.md +4 -5
  138. package/skills/architecture/caching.md +4 -5
  139. package/skills/architecture/cloud-native.md +4 -5
  140. package/skills/architecture/message-queue.md +4 -5
  141. package/skills/architecture/security-arch.md +4 -5
  142. package/skills/compliance/ai-disclosure.md +4 -5
  143. package/skills/compliance/data-privacy.md +4 -5
  144. package/skills/compliance/license-audit.md +4 -5
  145. package/skills/compliance/regulatory-review.md +4 -5
  146. package/skills/development/cpp.md +4 -5
  147. package/skills/development/go.md +4 -5
  148. package/skills/development/java.md +4 -5
  149. package/skills/development/kotlin.md +4 -5
  150. package/skills/development/mobile-crossplatform.md +4 -5
  151. package/skills/development/python.md +4 -5
  152. package/skills/development/rust.md +4 -5
  153. package/skills/development/shell.md +4 -5
  154. package/skills/development/swift.md +4 -5
  155. package/skills/development/typescript.md +4 -5
  156. package/skills/devops/ci-cd.md +4 -5
  157. package/skills/devops/containerization.md +4 -5
  158. package/skills/devops/cost-optimization.md +4 -5
  159. package/skills/devops/data-engineering.md +4 -5
  160. package/skills/devops/database.md +4 -5
  161. package/skills/devops/dependency-management.md +4 -5
  162. package/skills/devops/devsecops.md +4 -5
  163. package/skills/devops/git-workflow.md +4 -5
  164. package/skills/devops/incident-response.md +4 -5
  165. package/skills/devops/monorepo.md +4 -5
  166. package/skills/devops/observability.md +4 -5
  167. package/skills/devops/performance.md +4 -5
  168. package/skills/devops/testing.md +4 -5
  169. package/skills/docs/adr-workflow.md +4 -7
  170. package/skills/docs/backlog-proposal-workflow.md +4 -3
  171. package/skills/docs/customer-profile-workflow.md +4 -3
  172. package/skills/docs/document-ingest-workflow.md +4 -3
  173. package/skills/docs/evidence-ingest-workflow.md +4 -3
  174. package/skills/docs/init-docs.md +4 -5
  175. package/skills/docs/init-project.md +4 -5
  176. package/skills/docs/prd-workflow.md +4 -7
  177. package/skills/docs/prfaq-workflow.md +4 -3
  178. package/skills/docs/product-intelligence-review.md +4 -3
  179. package/skills/docs/product-intelligence-workflow.md +4 -6
  180. package/skills/docs/product-signal-workflow.md +4 -5
  181. package/skills/docs/research-workflow.md +4 -5
  182. package/skills/docs/runbook-workflow.md +4 -5
  183. package/skills/docs/strategy-workflow.md +4 -5
  184. package/skills/exploration/dependency-graph-reading.md +4 -7
  185. package/skills/exploration/repo-map.md +4 -5
  186. package/skills/exploration/tracer-bullet-method.md +4 -7
  187. package/skills/exploration/unknown-codebase-onboarding.md +4 -7
  188. package/skills/frameworks/django.md +4 -5
  189. package/skills/frameworks/nextjs.md +4 -5
  190. package/skills/frameworks/react.md +4 -5
  191. package/skills/frameworks/spring-boot.md +4 -5
  192. package/skills/frontend-design/accessibility.md +4 -5
  193. package/skills/frontend-design/component-patterns.md +4 -5
  194. package/skills/frontend-design/engineering.md +4 -5
  195. package/skills/frontend-design/state-management.md +4 -5
  196. package/skills/frontend-design/ui-aesthetics.md +4 -5
  197. package/skills/frontend-design/ux-principles.md +4 -5
  198. package/skills/operating/change-management.md +4 -8
  199. package/skills/operating/incident-response.md +4 -8
  200. package/skills/operating/oncall-rotation.md +4 -7
  201. package/skills/operating/orchestration-reference.md +4 -10
  202. package/skills/quality-gates/review-work.md +4 -5
  203. package/skills/quality-gates/verify-change.md +4 -5
  204. package/skills/quality-gates/verify-module.md +4 -5
  205. package/skills/quality-gates/verify-quality.md +4 -5
  206. package/skills/quality-gates/verify-security.md +4 -5
  207. package/skills/roles/architect.ai-systems.md +6 -9
  208. package/skills/roles/architect.data.md +6 -9
  209. package/skills/roles/architect.enterprise.md +6 -9
  210. package/skills/roles/architect.integration.md +6 -9
  211. package/skills/roles/architect.md +7 -14
  212. package/skills/roles/architect.platform.md +6 -9
  213. package/skills/roles/data-analyst.experiment.md +6 -9
  214. package/skills/roles/data-analyst.md +6 -9
  215. package/skills/roles/data-analyst.product-intelligence.md +7 -9
  216. package/skills/roles/data-analyst.product.md +6 -9
  217. package/skills/roles/data-analyst.telemetry.md +7 -9
  218. package/skills/roles/data-engineer.pipeline.md +6 -9
  219. package/skills/roles/data-engineer.vector-retrieval.md +7 -9
  220. package/skills/roles/data-engineer.warehouse.md +6 -9
  221. package/skills/roles/debugger.md +6 -9
  222. package/skills/roles/designer.accessibility.md +6 -9
  223. package/skills/roles/designer.md +7 -9
  224. package/skills/roles/engineer.ai.md +6 -9
  225. package/skills/roles/engineer.data.md +6 -9
  226. package/skills/roles/engineer.md +9 -9
  227. package/skills/roles/engineer.platform.md +6 -9
  228. package/skills/roles/operator.docs.md +6 -9
  229. package/skills/roles/operator.md +9 -9
  230. package/skills/roles/operator.release.md +6 -9
  231. package/skills/roles/operator.sre.md +6 -9
  232. package/skills/roles/orchestrator.md +6 -9
  233. package/skills/roles/product-manager.ai-product.md +6 -9
  234. package/skills/roles/product-manager.business-strategy.md +6 -9
  235. package/skills/roles/product-manager.enterprise.md +6 -9
  236. package/skills/roles/product-manager.growth.md +7 -9
  237. package/skills/roles/product-manager.md +7 -9
  238. package/skills/roles/product-manager.platform.md +6 -9
  239. package/skills/roles/product-manager.product.md +6 -9
  240. package/skills/roles/qa.ai-eval.md +8 -9
  241. package/skills/roles/qa.api-contract.md +7 -9
  242. package/skills/roles/qa.data-pipeline.md +7 -9
  243. package/skills/roles/qa.md +7 -9
  244. package/skills/roles/qa.test-automation.md +6 -9
  245. package/skills/roles/qa.web-ui.md +7 -9
  246. package/skills/roles/researcher.explorer.md +6 -9
  247. package/skills/roles/researcher.md +8 -9
  248. package/skills/roles/researcher.ux.md +6 -9
  249. package/skills/roles/reviewer.devil-advocate.md +6 -9
  250. package/skills/roles/reviewer.evaluator.md +6 -9
  251. package/skills/roles/reviewer.md +9 -9
  252. package/skills/roles/reviewer.trace.md +6 -9
  253. package/skills/roles/security.ai.md +7 -9
  254. package/skills/roles/security.appsec.md +6 -9
  255. package/skills/roles/security.cloud.md +6 -9
  256. package/skills/roles/security.legal-compliance.md +6 -9
  257. package/skills/roles/security.md +7 -9
  258. package/skills/roles/security.privacy.md +7 -9
  259. package/skills/roles/security.supply-chain.md +7 -9
  260. package/skills/security/blue-team.md +4 -5
  261. package/skills/security/code-audit.md +4 -5
  262. package/skills/security/pentest.md +4 -5
  263. package/skills/security/red-team.md +4 -5
  264. package/skills/security/threat-intel.md +4 -5
  265. package/skills/security/vuln-research.md +4 -5
  266. package/skills/strategy/competitive-landscape.md +4 -8
  267. package/skills/strategy/market-research-methods.md +4 -8
  268. package/skills/strategy/narrative-arc.md +4 -8
  269. package/skills/strategy/pricing-positioning.md +4 -8
  270. package/skills/utility/clean-code.md +4 -5
  271. package/specialists/policy-inventory.json +14 -0
  272. package/lib/specialist-contracts-enforce.mjs +0 -172
  273. package/rules/common/agents.md +0 -28
  274. package/rules/common/development-workflow.md +0 -32
  275. package/rules/common/performance.md +0 -55
@@ -116,7 +116,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
116
116
  },
117
117
  {
118
118
  name: 'extract_document_text',
119
- description: 'Extracts readable text from a local document path. Supports PDF on macOS plus common text and office document formats.',
119
+ description: 'Extracts readable text from a local document path. High-fidelity backend (docling Python sidecar via uv, whisper.cpp for audio/video) is wired into `construct ingest`; this MCP path returns the legacy regex extraction for backwards compatibility. Supports PDF, DOCX, XLSX, PPTX, HTML, plain text, email, and transcripts.',
120
120
  inputSchema: {
121
121
  type: 'object',
122
122
  properties: {
@@ -903,6 +903,147 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
903
903
  },
904
904
  },
905
905
  },
906
+ {
907
+ name: 'workflow_init',
908
+ description: 'Initialize a new workflow for the current project. Creates plan.md state if not already present and returns the initial workflow envelope.',
909
+ inputSchema: {
910
+ type: 'object',
911
+ properties: {
912
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
913
+ title: { type: 'string', description: 'Workflow title shown in the plan header (default: "Untitled workflow").' },
914
+ spec_ref: { type: 'string', description: 'Optional reference to a spec/PRD/ADR id this workflow implements.' },
915
+ },
916
+ },
917
+ },
918
+ {
919
+ name: 'workflow_add_task',
920
+ description: 'Add a task to the current workflow. Pass `request` for intent-based routing (the classifier picks track + specialist) or pass explicit task fields (`key`, `title`, etc.) for manual entry.',
921
+ inputSchema: {
922
+ type: 'object',
923
+ properties: {
924
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
925
+ request: { type: 'string', description: 'Natural-language task request; when present, intent-based routing is used and the explicit fields below act as overrides.' },
926
+ key: { type: 'string', description: 'Stable task key (e.g. T-001). Generated when omitted.' },
927
+ title: { type: 'string', description: 'Short task title.' },
928
+ phase: { type: 'string', description: 'Phase bucket (plan, build, validate, ship, etc.).' },
929
+ owner: { type: 'string', description: 'Specialist or persona that owns the task.' },
930
+ files: { type: 'array', items: { type: 'string' }, description: 'File paths this task touches.' },
931
+ readFirst: { type: 'array', items: { type: 'string' }, description: 'Files the owner should read before editing.' },
932
+ doNotChange: { type: 'array', items: { type: 'string' }, description: 'Files/regions the owner must not modify.' },
933
+ acceptanceCriteria: { type: 'array', items: { type: 'string' }, description: 'Acceptance criteria as a checklist.' },
934
+ verification: { type: 'string', description: 'Command(s) or description of how to verify the task is done.' },
935
+ dependsOn: { type: 'array', items: { type: 'string' }, description: 'Task keys this task depends on.' },
936
+ overlays: { type: 'array', items: { type: 'string' }, description: 'Role flavors that augment the owner persona for this task.' },
937
+ challengeRequired: { type: 'boolean', description: 'Force a cx-devil-advocate challenge before the task can complete.' },
938
+ challengeStatus: { type: 'string', description: 'Initial challenge status when seeded.' },
939
+ tokenBudget: { type: 'number', description: 'Per-task token budget for cost tracking.' },
940
+ status: { type: 'string', description: 'Initial status override.' },
941
+ },
942
+ },
943
+ },
944
+ {
945
+ name: 'workflow_update_task',
946
+ description: 'Update fields on an existing workflow task. Requires the task `key`. Only fields supplied are changed.',
947
+ inputSchema: {
948
+ type: 'object',
949
+ required: ['key'],
950
+ properties: {
951
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
952
+ key: { type: 'string', description: 'Task key to update.' },
953
+ status: { type: 'string', description: 'New status (pending, in_progress, blocked_needs_user, blocked_by_dep, done, etc.).' },
954
+ owner: { type: 'string', description: 'New owner persona.' },
955
+ phase: { type: 'string', description: 'New phase bucket.' },
956
+ note: { type: 'string', description: 'Append-only progress note.' },
957
+ verification: { type: 'string', description: 'Updated verification description.' },
958
+ overlays: { type: 'array', items: { type: 'string' }, description: 'Replace the overlay list.' },
959
+ challengeRequired: { type: 'boolean', description: 'Toggle whether a challenge is required.' },
960
+ challengeStatus: { type: 'string', description: 'Update the challenge status (proposed, accepted, refused, etc.).' },
961
+ },
962
+ },
963
+ },
964
+ {
965
+ name: 'workflow_needs_main_input',
966
+ description: 'Mark a workflow task as blocked pending user input. Sets status to blocked_needs_user and writes a packet describing the blocker for the orchestrator to surface.',
967
+ inputSchema: {
968
+ type: 'object',
969
+ required: ['taskKey', 'blocker', 'question'],
970
+ properties: {
971
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
972
+ taskKey: { type: 'string', description: 'Task key to mark blocked.' },
973
+ worker: { type: 'string', description: 'Specialist that needs input (default: current owner).' },
974
+ blocker: { type: 'string', description: 'One-line description of what is blocking progress.' },
975
+ question: { type: 'string', description: 'The specific question to put to the user.' },
976
+ },
977
+ },
978
+ },
979
+ {
980
+ name: 'workflow_validate',
981
+ description: 'Validate the current workflow state against the schema and run consistency checks (no orphan tasks, no circular dependencies, every owner resolves to a known persona).',
982
+ inputSchema: {
983
+ type: 'object',
984
+ properties: {
985
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
986
+ },
987
+ },
988
+ },
989
+ {
990
+ name: 'workflow_status',
991
+ description: 'Return the full workflow snapshot for the current project: tasks, summary, alignment health, and the public-health surface used by the dashboard.',
992
+ inputSchema: {
993
+ type: 'object',
994
+ properties: {
995
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
996
+ },
997
+ },
998
+ },
999
+ {
1000
+ name: 'workflow_contract_validate',
1001
+ description: 'Validate a producer→consumer handoff against specialists/contracts.json. Required when a specialist hands off to another role: enforces input.mustContain, output schema, disk-artifact postconditions, and binary postconditions per producer (rubber-stamp prevention, post-hoc threat-model prevention, etc.). Self-enforcing: a producer with binary rules MUST pass `packet`, or the call itself is a contract violation.',
1002
+ inputSchema: {
1003
+ type: 'object',
1004
+ required: ['producer', 'consumer'],
1005
+ properties: {
1006
+ producer: { type: 'string', description: 'Producer agent or persona name (e.g. cx-reviewer, cx-security).' },
1007
+ consumer: { type: 'string', description: 'Consumer agent or persona name receiving the handoff.' },
1008
+ id: { type: 'string', description: 'Optional contract id; overrides producer/consumer lookup.' },
1009
+ artifact: { type: 'object', description: 'The handoff payload to validate against the contract schema and disk-artifact postconditions.' },
1010
+ packet: { type: 'object', description: 'The producer\'s in-memory output packet. REQUIRED when the producer has binary postconditions; omitting it is itself a contract violation.' },
1011
+ enforcement: { type: 'string', enum: ['block', 'warn'], description: 'Enforcement mode (default: block). Use warn only when explicitly advisory.' },
1012
+ },
1013
+ },
1014
+ },
1015
+ {
1016
+ name: 'workflow_import_plan',
1017
+ description: 'Bulk-add tasks from a markdown plan to the current workflow. Parses headings and bullet structure to extract task titles, owners, and acceptance criteria.',
1018
+ inputSchema: {
1019
+ type: 'object',
1020
+ required: ['markdown'],
1021
+ properties: {
1022
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
1023
+ markdown: { type: 'string', description: 'Plan markdown to parse.' },
1024
+ phase: { type: 'string', description: 'Phase bucket applied to all imported tasks.' },
1025
+ owner: { type: 'string', description: 'Default owner for tasks that do not specify one.' },
1026
+ readFirst: { type: 'array', items: { type: 'string' }, description: 'Default readFirst files applied to all imported tasks.' },
1027
+ doNotChange: { type: 'array', items: { type: 'string' }, description: 'Default doNotChange files applied to all imported tasks.' },
1028
+ acceptanceCriteria: { type: 'array', items: { type: 'string' }, description: 'Default acceptance criteria applied to all imported tasks.' },
1029
+ title: { type: 'string', description: 'Workflow title to set if the workflow is newly created.' },
1030
+ spec_ref: { type: 'string', description: 'Spec reference to associate with the workflow.' },
1031
+ },
1032
+ },
1033
+ },
1034
+ {
1035
+ name: 'cx_trace_telemetry',
1036
+ description: 'Record a single CX telemetry trace for an agent invocation. Use to log start/end, model used, token cost, and outcome verdict for performance review.',
1037
+ inputSchema: {
1038
+ type: 'object',
1039
+ required: ['agent', 'trace'],
1040
+ properties: {
1041
+ agent: { type: 'string', description: 'Agent or persona name being traced.' },
1042
+ trace: { type: 'object', description: 'Trace record: start_ts, end_ts, model, tokens, verdict, notes, etc.' },
1043
+ cwd: { type: 'string', description: 'Project root (default: server cwd).' },
1044
+ },
1045
+ },
1046
+ },
906
1047
  ],
907
1048
  }));
908
1049
 
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { readFileSync, readdirSync, existsSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
- import { execSync } from 'node:child_process';
10
+ import { execFileSync } from 'node:child_process';
11
11
  import { routeRequest, routeRequestVerified, requiresExecutiveApproval, TERMINAL_STATES } from '../../orchestration-policy.mjs';
12
12
  import { buildTaskPacketFromIntent } from '../../workflow-state.mjs';
13
13
  import { logSkillCall } from '../../telemetry/skill-calls.mjs';
@@ -73,12 +73,15 @@ export function searchSkills(args, { ROOT_DIR }) {
73
73
  if (!pattern) return { error: 'Missing pattern argument' };
74
74
  const skillsDir = join(ROOT_DIR, 'skills');
75
75
 
76
+ // argv-array form: pattern is a discrete argument, never interpreted by a
77
+ // shell. MCP tool inputs are model-controlled and must not reach a shell.
78
+
76
79
  try {
77
- const output = execSync(`rg -i "${pattern.replace(/"/g, '\\"')}" "${skillsDir}"`, { encoding: 'utf8' });
80
+ const output = execFileSync('rg', ['-i', pattern, skillsDir], { encoding: 'utf8' });
78
81
  return { results: output.split('\n').filter(Boolean) };
79
82
  } catch {
80
83
  try {
81
- const output = execSync(`grep -ri "${pattern.replace(/"/g, '\\"')}" "${skillsDir}"`, { encoding: 'utf8' });
84
+ const output = execFileSync('grep', ['-ri', pattern, skillsDir], { encoding: 'utf8' });
82
85
  return { results: output.split('\n').filter(Boolean) };
83
86
  } catch {
84
87
  return { results: [], note: 'No matches found or grep error' };
@@ -18,6 +18,7 @@ import { buildStatus } from '../../status.mjs';
18
18
  import { readEfficiencyLog, buildCompactEfficiencyDigest } from '../../efficiency.mjs';
19
19
  import { addObservation } from '../../observation-store.mjs';
20
20
  import { loadConstructEnv } from '../../env-config.mjs';
21
+ import { createSqlClient, closeSqlClient } from '../../storage/backend.mjs';
21
22
 
22
23
  // Load config.env once at module init so config.env values win over shell env
23
24
  // (shell env may have stale/truncated credentials from earlier sessions)
@@ -243,6 +244,35 @@ export async function cxScore(args) {
243
244
  }
244
245
  }
245
246
 
247
+ // Postgres write-through (team/enterprise). Backs the
248
+ // construct_skill_quality_correlation view that `construct skills
249
+ // correlate-quality` reads — without this, the view stays empty and
250
+ // the CLI surface that promised correlation data delivered nothing.
251
+ // Best-effort: a missing DATABASE_URL or schema misalignment never
252
+ // blocks the score from reaching the remote telemetry or the local
253
+ // observation store above.
254
+
255
+ if (Number.isFinite(numericValue)) {
256
+ const sqlClient = createSqlClient(process.env);
257
+ if (sqlClient) {
258
+ try {
259
+ await sqlClient`
260
+ insert into construct_cx_scores (ts, trace_id, session_id, agent_id, name, value, comment)
261
+ values (
262
+ ${new Date().toISOString()},
263
+ ${traceId},
264
+ ${args.session_id ?? null},
265
+ ${args.agent_id ?? args.name ?? null},
266
+ ${args.name ?? 'quality'},
267
+ ${numericValue},
268
+ ${args.comment ?? null}
269
+ )
270
+ `;
271
+ } catch { /* best-effort write — never block on DB issues */ }
272
+ finally { await closeSqlClient(sqlClient).catch(() => {}); }
273
+ }
274
+ }
275
+
246
276
  return { ok: true, traceId, backend: client.backend, remoteStatus: client.remoteStatus };
247
277
  } catch (err) {
248
278
  return { ok: false, error: err.message };
@@ -104,7 +104,10 @@ export function workflowValidate(args) {
104
104
  * consumer — agent or persona name receiving it
105
105
  * id — optional contract id (overrides producer/consumer lookup)
106
106
  * artifact — the handoff payload to validate
107
- * enforcement — 'warn' (default) or 'block'; 'block' returns ok:false on
107
+ * packet the producer's in-memory output packet. Required when the
108
+ * producer has binary postconditions; omitting it is itself
109
+ * a contract violation.
110
+ * enforcement — 'block' (default) or 'warn'; 'block' returns ok:false on
108
111
  * violation so the workflow can refuse to advance
109
112
  *
110
113
  * Returns { ok, status?, errors?, warnings?, contract } where status is set
@@ -117,7 +120,8 @@ export async function workflowContractValidate(args) {
117
120
  consumer: args.consumer,
118
121
  id: args.id,
119
122
  artifact: args.artifact,
120
- enforcement: args.enforcement || process.env.CONSTRUCT_CONTRACT_ENFORCEMENT || 'warn',
123
+ packet: args.packet,
124
+ enforcement: args.enforcement || 'block',
121
125
  });
122
126
  }
123
127
 
@@ -112,10 +112,12 @@ function logCapDrop(rootDir, kind, dropped, total) {
112
112
  function writeIndex(rootDir, entries) {
113
113
  ensureDir(obsDir(rootDir), rootDir);
114
114
  const trimmed = entries.slice(0, MAX_INDEX);
115
- if (entries.length > MAX_INDEX) {
116
- logCapDrop(rootDir, 'observation-index', entries.length - MAX_INDEX, entries.length);
115
+ const dropped = Math.max(0, entries.length - MAX_INDEX);
116
+ if (dropped > 0) {
117
+ logCapDrop(rootDir, 'observation-index', dropped, entries.length);
117
118
  }
118
119
  fs.writeFileSync(indexPath(rootDir), JSON.stringify(trimmed, null, 2) + '\n');
120
+ return { dropped };
119
121
  }
120
122
 
121
123
  function readVectors(rootDir) {
@@ -130,10 +132,12 @@ function readVectors(rootDir) {
130
132
 
131
133
  function writeVectors(rootDir, records) {
132
134
  ensureDir(obsDir(rootDir), rootDir);
133
- if (records.length > MAX_INDEX) {
134
- logCapDrop(rootDir, 'observation-vectors', records.length - MAX_INDEX, records.length);
135
+ const dropped = Math.max(0, records.length - MAX_INDEX);
136
+ if (dropped > 0) {
137
+ logCapDrop(rootDir, 'observation-vectors', dropped, records.length);
135
138
  }
136
139
  fs.writeFileSync(vectorsPath(rootDir), JSON.stringify(records, null, 2) + '\n');
140
+ return { dropped };
137
141
  }
138
142
 
139
143
  /**
@@ -188,6 +192,7 @@ export async function addObservation(rootDir, {
188
192
  JSON.stringify(record, null, 2) + '\n',
189
193
  );
190
194
 
195
+ let indexDropped = 0;
191
196
  withFileLockSync(indexPath(rootDir), () => {
192
197
  const index = readIndex(rootDir);
193
198
  index.unshift({
@@ -198,8 +203,12 @@ export async function addObservation(rootDir, {
198
203
  project: record.project,
199
204
  createdAt: now,
200
205
  });
201
- writeIndex(rootDir, index);
206
+ indexDropped = writeIndex(rootDir, index).dropped;
202
207
  });
208
+ if (indexDropped > 0) {
209
+ record.capDropped = indexDropped;
210
+ process.stderr.write(`[observation-store] observation cap reached: ${indexDropped} oldest entries evicted (cap=${MAX_INDEX})\n`);
211
+ }
203
212
 
204
213
  // Try SQL storage with neural embeddings
205
214
  try {
@@ -6,10 +6,26 @@
6
6
  * pulling new models, and configuring Ollama as a provider.
7
7
  */
8
8
 
9
- import { execSync } from 'node:child_process';
9
+ import { execFileSync } from 'node:child_process';
10
10
  import { join } from 'node:path';
11
11
  import { homedir } from 'node:os';
12
12
 
13
+ // argv-array form for every shell-out: no shell interpolation, model names and
14
+ // JSON bodies are passed as discrete arguments / stdin so user-supplied values
15
+ // cannot break out of the command.
16
+
17
+ function curlGet(url, { timeout } = {}) {
18
+ return execFileSync('curl', ['-s', url], { encoding: 'utf8', timeout });
19
+ }
20
+
21
+ function curlPost(url, body, { timeout } = {}) {
22
+ return execFileSync('curl', ['-s', url, '--data-binary', '@-'], {
23
+ encoding: 'utf8',
24
+ timeout,
25
+ input: typeof body === 'string' ? body : JSON.stringify(body),
26
+ });
27
+ }
28
+
13
29
  const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || process.env.OLLAMA_HOST || 'http://localhost:11434';
14
30
 
15
31
  /**
@@ -17,14 +33,11 @@ const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || process.env.OLLAMA_HOST |
17
33
  */
18
34
  export function checkOllamaStatus() {
19
35
  try {
20
- execSync('which ollama', { stdio: 'pipe' });
36
+ execFileSync('which', ['ollama'], { stdio: 'pipe' });
21
37
  const installed = true;
22
-
38
+
23
39
  try {
24
- const response = execSync(`curl -s ${OLLAMA_BASE_URL}/api/tags`, {
25
- encoding: 'utf8',
26
- timeout: 3000
27
- });
40
+ const response = curlGet(`${OLLAMA_BASE_URL}/api/tags`, { timeout: 3000 });
28
41
  const data = JSON.parse(response);
29
42
  return {
30
43
  installed,
@@ -54,10 +67,7 @@ export function checkOllamaStatus() {
54
67
  */
55
68
  export function listModels() {
56
69
  try {
57
- const response = execSync(`curl -s ${OLLAMA_BASE_URL}/api/tags`, {
58
- encoding: 'utf8',
59
- timeout: 5000
60
- });
70
+ const response = curlGet(`${OLLAMA_BASE_URL}/api/tags`, { timeout: 5000 });
61
71
  const data = JSON.parse(response);
62
72
  return data.models || [];
63
73
  } catch (e) {
@@ -71,7 +81,7 @@ export function listModels() {
71
81
  export function pullModel(modelName, { verbose = false } = {}) {
72
82
  console.log(`Pulling ${modelName}...`);
73
83
  try {
74
- const output = execSync(`ollama pull ${modelName}`, {
84
+ const output = execFileSync('ollama', ['pull', modelName], {
75
85
  encoding: 'utf8',
76
86
  stdio: verbose ? 'inherit' : 'pipe'
77
87
  });
@@ -86,7 +96,7 @@ export function pullModel(modelName, { verbose = false } = {}) {
86
96
  */
87
97
  export function removeModel(modelName) {
88
98
  try {
89
- execSync(`ollama rm ${modelName}`, { encoding: 'utf8' });
99
+ execFileSync('ollama', ['rm', modelName], { encoding: 'utf8' });
90
100
  return { success: true };
91
101
  } catch (e) {
92
102
  return { success: false, error: e.message };
@@ -98,10 +108,7 @@ export function removeModel(modelName) {
98
108
  */
99
109
  export function showModel(modelName) {
100
110
  try {
101
- const response = execSync(`curl -s ${OLLAMA_BASE_URL}/api/show -d '{"name":"${modelName}"}'`, {
102
- encoding: 'utf8',
103
- timeout: 5000
104
- });
111
+ const response = curlPost(`${OLLAMA_BASE_URL}/api/show`, { name: modelName }, { timeout: 5000 });
105
112
  return JSON.parse(response);
106
113
  } catch (e) {
107
114
  throw new Error(`Failed to fetch model details: ${e.message}`);
@@ -113,14 +120,11 @@ export function showModel(modelName) {
113
120
  */
114
121
  export function testModel(modelName, prompt = 'Say hello in one sentence.') {
115
122
  try {
116
- const response = execSync(`curl -s ${OLLAMA_BASE_URL}/api/generate -d '${JSON.stringify({
117
- model: modelName,
118
- prompt: prompt,
119
- stream: false
120
- })}'`, {
121
- encoding: 'utf8',
122
- timeout: 30000
123
- });
123
+ const response = curlPost(
124
+ `${OLLAMA_BASE_URL}/api/generate`,
125
+ { model: modelName, prompt, stream: false },
126
+ { timeout: 30000 },
127
+ );
124
128
  const data = JSON.parse(response);
125
129
  return {
126
130
  success: true,
@@ -371,7 +375,7 @@ async function cmdOllamaSetup(args) {
371
375
  if (autoYes || process.platform === 'darwin') {
372
376
  console.log('Installing via Homebrew...');
373
377
  try {
374
- execSync('brew install ollama', { stdio: 'inherit' });
378
+ execFileSync('brew', ['install', 'ollama'], { stdio: 'inherit' });
375
379
  console.log('✅ Ollama installed');
376
380
  } catch (e) {
377
381
  console.error('❌ Installation failed. Please install manually: https://ollama.com');
@@ -390,11 +394,11 @@ async function cmdOllamaSetup(args) {
390
394
  console.log('⚠️ Ollama is not running');
391
395
  console.log('Starting Ollama service...');
392
396
  try {
393
- execSync('brew services start ollama', { stdio: 'pipe' });
397
+ execFileSync('brew', ['services', 'start', 'ollama'], { stdio: 'pipe' });
394
398
  console.log('✅ Ollama service started');
395
399
  // Wait for it to be ready
396
400
  console.log('Waiting for Ollama to be ready...');
397
- execSync('sleep 3');
401
+ execFileSync('sleep', ['3']);
398
402
  } catch (e) {
399
403
  console.log('You can start it manually with: ollama serve');
400
404
  }
@@ -352,6 +352,21 @@ export function classifyRoleFlavors(request = '') {
352
352
  };
353
353
  }
354
354
 
355
+ // One line per non-null flavor: "cx-<role>: loaded <role>.<flavor>
356
+ // overlay". Verbose chat surface + cx_trace span attribute; lets a
357
+ // post-hoc reviewer see which overlays drove a given dispatch.
358
+
359
+ export function formatOverlaySelection(roleFlavors) {
360
+ if (!roleFlavors || typeof roleFlavors !== 'object') return [];
361
+ const lines = [];
362
+ for (const [role, flavor] of Object.entries(roleFlavors)) {
363
+ if (!flavor) continue;
364
+ const specialist = `cx-${role.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
365
+ lines.push(`${specialist}: loaded ${role}.${flavor} overlay`);
366
+ }
367
+ return lines;
368
+ }
369
+
355
370
  export function detectRiskFlags(request = '') {
356
371
  const text = String(request).toLowerCase();
357
372
  return {
@@ -94,7 +94,17 @@ function compactTokens(text, tokenLimit = 300, { modelId = 'default' } = {}) {
94
94
  function readPromptBody(promptFile, rootDir) {
95
95
  const filePath = path.join(rootDir, promptFile);
96
96
  if (!fs.existsSync(filePath)) return '';
97
- return fs.readFileSync(filePath, 'utf8').trim();
97
+ const raw = fs.readFileSync(filePath, 'utf8');
98
+ return stripLeadingYamlFrontmatter(raw).trim();
99
+ }
100
+
101
+ function stripLeadingYamlFrontmatter(content) {
102
+ if (!content.startsWith('---\n') && !content.startsWith('---\r\n')) return content;
103
+ const closeIdx = content.indexOf('\n---', 4);
104
+ if (closeIdx === -1) return content;
105
+ const afterClose = content.indexOf('\n', closeIdx + 1);
106
+ if (afterClose === -1) return '';
107
+ return content.slice(afterClose + 1);
98
108
  }
99
109
 
100
110
  function buildLearnedPatternsBlock(agentName, {
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
14
- import { homedir } from 'node:os';
14
+ import { homedir, tmpdir } from 'node:os';
15
15
  import { join } from 'node:path';
16
16
 
17
17
  import { emit, _paths } from './event-bus.mjs';
@@ -54,7 +54,36 @@ function appendPending(entry) {
54
54
  appendFileSync(pendingPath(), JSON.stringify(entry) + '\n');
55
55
  }
56
56
 
57
+ // Paths under the OS tmpdir are by definition test fixtures or sandbox
58
+ // runs, not real project state. Escalating from them spawns persistent
59
+ // beads for ephemeral files — pure tracker noise. macOS resolves
60
+ // $TMPDIR through a /private/var/folders/... symlink; the raw tmpdir
61
+ // and its /private prefix are both checked so either resolved form
62
+ // matches.
63
+
64
+ export function isTestFixturePath(p) {
65
+ if (!p || typeof p !== 'string') return false;
66
+ const tmp = tmpdir();
67
+ if (p.startsWith(tmp)) return true;
68
+ if (p.startsWith('/private' + tmp)) return true;
69
+ if (p.startsWith('/tmp/')) return true;
70
+ // macOS canonical user-tmp prefix when $TMPDIR is set elsewhere
71
+ if (/^\/private\/var\/folders\/[^/]+\/[^/]+\/T\//.test(p)) return true;
72
+ if (/^\/var\/folders\/[^/]+\/[^/]+\/T\//.test(p)) return true;
73
+ return false;
74
+ }
75
+
76
+ function eventIsFromTestFixture(event) {
77
+ if (isTestFixturePath(event?.cwd)) return true;
78
+ if (isTestFixturePath(event?.project)) return true;
79
+ if (isTestFixturePath(event?.context?.filePath)) return true;
80
+ return false;
81
+ }
82
+
57
83
  export function shouldEscalate(event, manifest, now = Date.now()) {
84
+ if (eventIsFromTestFixture(event)) {
85
+ return { escalate: false, reason: 'test-fixture-path' };
86
+ }
58
87
  const fp = event.fingerprint;
59
88
  const pending = readPending();
60
89
 
@@ -0,0 +1,129 @@
1
+ /**
2
+ * lib/runtime/uv-bootstrap.mjs — idempotent uv + docling venv provisioner.
3
+ *
4
+ * On first use: downloads uv via the official Astral installer, creates
5
+ * `<rootDir>/.cx/runtime/docling/.venv`, installs the pinned docling release.
6
+ * Subsequent calls are no-ops if the venv is already present.
7
+ *
8
+ * Best-practice notes (2026-06):
9
+ * - uv (Astral) is the consensus Python tooling, ~10× faster than pip, single
10
+ * binary, lockfile-based. OpenAI acquired Astral on 2026-03-19; the project
11
+ * remains Apache-2.0 and actively maintained, but single-vendor governance
12
+ * is a documented concern — flagged for re-evaluation if licensing shifts.
13
+ * - docling (IBM, MIT, donated to LF AI & Data early 2026) is the consensus
14
+ * multi-format doc parser for layout-aware MD/JSON output.
15
+ *
16
+ * Returns the path to the venv's python binary so callers can spawn the
17
+ * sidecar without relying on PATH state.
18
+ */
19
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
20
+ import { spawnSync } from 'node:child_process';
21
+ import path from 'node:path';
22
+ import os from 'node:os';
23
+
24
+ const DOCLING_PIN = '2.45.0';
25
+ const UV_INSTALL_URL = 'https://astral.sh/uv/install.sh';
26
+ const UV_TIMEOUT_MS = 120_000;
27
+ const VENV_TIMEOUT_MS = 240_000;
28
+ const INSTALL_TIMEOUT_MS = 600_000;
29
+
30
+ export function defaultRuntimeDir(cwd = process.cwd()) {
31
+ return path.join(cwd, '.cx', 'runtime', 'docling');
32
+ }
33
+
34
+ function which(bin) {
35
+ const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [bin], { encoding: 'utf8' });
36
+ if (result.status !== 0) return null;
37
+ return result.stdout.trim().split('\n')[0] || null;
38
+ }
39
+
40
+ function ensureUv(installDir) {
41
+ const fromPath = which('uv');
42
+ if (fromPath) return fromPath;
43
+ const cachedUv = path.join(installDir, 'bin', 'uv');
44
+ if (existsSync(cachedUv)) return cachedUv;
45
+ mkdirSync(installDir, { recursive: true });
46
+ const env = { ...process.env, UV_INSTALL_DIR: path.join(installDir, 'bin'), UV_NO_MODIFY_PATH: '1' };
47
+ const sh = spawnSync('sh', ['-c', `curl -LsSf ${UV_INSTALL_URL} | sh`], {
48
+ env,
49
+ timeout: UV_TIMEOUT_MS,
50
+ encoding: 'utf8',
51
+ });
52
+ if (sh.status !== 0) {
53
+ throw new Error(`uv install failed (${sh.status}): ${sh.stderr || sh.stdout}`);
54
+ }
55
+ if (!existsSync(cachedUv)) {
56
+ throw new Error(`uv install reported success but binary not found at ${cachedUv}`);
57
+ }
58
+ return cachedUv;
59
+ }
60
+
61
+ function pythonBinFor(venvDir) {
62
+ const candidates = process.platform === 'win32'
63
+ ? [path.join(venvDir, 'Scripts', 'python.exe')]
64
+ : [path.join(venvDir, 'bin', 'python'), path.join(venvDir, 'bin', 'python3')];
65
+ return candidates.find((p) => existsSync(p));
66
+ }
67
+
68
+ function readMarker(markerPath) {
69
+ try { return JSON.parse(readFileSync(markerPath, 'utf8')); }
70
+ catch { return null; }
71
+ }
72
+
73
+ function writeMarker(markerPath, payload) {
74
+ mkdirSync(path.dirname(markerPath), { recursive: true });
75
+ writeFileSync(markerPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
76
+ }
77
+
78
+ export function ensureDoclingVenv({ runtimeDir = defaultRuntimeDir(), force = false } = {}) {
79
+ mkdirSync(runtimeDir, { recursive: true });
80
+ const venvDir = path.join(runtimeDir, '.venv');
81
+ const markerPath = path.join(runtimeDir, '.install-marker.json');
82
+ const existingMarker = readMarker(markerPath);
83
+ const existingPython = pythonBinFor(venvDir);
84
+
85
+ if (!force && existingPython && existingMarker?.doclingVersion === DOCLING_PIN) {
86
+ return { pythonBin: existingPython, venvDir, runtimeDir, fresh: false };
87
+ }
88
+
89
+ const uv = ensureUv(runtimeDir);
90
+
91
+ const venvResult = spawnSync(uv, ['venv', venvDir, '--python', '3.11'], {
92
+ timeout: VENV_TIMEOUT_MS,
93
+ encoding: 'utf8',
94
+ });
95
+ if (venvResult.status !== 0) {
96
+ throw new Error(`uv venv failed (${venvResult.status}): ${venvResult.stderr}`);
97
+ }
98
+
99
+ const installResult = spawnSync(uv, ['pip', 'install', '--python', pythonBinFor(venvDir), `docling==${DOCLING_PIN}`], {
100
+ timeout: INSTALL_TIMEOUT_MS,
101
+ encoding: 'utf8',
102
+ });
103
+ if (installResult.status !== 0) {
104
+ throw new Error(`docling install failed (${installResult.status}): ${installResult.stderr}`);
105
+ }
106
+
107
+ const pythonBin = pythonBinFor(venvDir);
108
+ writeMarker(markerPath, {
109
+ doclingVersion: DOCLING_PIN,
110
+ installedAt: new Date().toISOString(),
111
+ pythonBin,
112
+ uvVersion: spawnSync(uv, ['--version'], { encoding: 'utf8' }).stdout.trim() || null,
113
+ platform: `${process.platform}-${process.arch}`,
114
+ node: process.version,
115
+ });
116
+
117
+ return { pythonBin, venvDir, runtimeDir, fresh: true };
118
+ }
119
+
120
+ export function describeDoclingRuntime({ runtimeDir = defaultRuntimeDir() } = {}) {
121
+ const venvDir = path.join(runtimeDir, '.venv');
122
+ return {
123
+ runtimeDir,
124
+ venvDir,
125
+ pythonBin: pythonBinFor(venvDir),
126
+ marker: readMarker(path.join(runtimeDir, '.install-marker.json')),
127
+ available: !!pythonBinFor(venvDir),
128
+ };
129
+ }