@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.
- package/README.md +21 -2
- package/bin/construct +146 -83
- package/commands/build/feature.md +0 -6
- package/commands/build/fix.md +0 -6
- package/commands/design/access.md +0 -6
- package/commands/design/flow.md +0 -6
- package/commands/design/ui.md +0 -6
- package/commands/measure/experiment.md +0 -6
- package/commands/measure/metrics.md +0 -6
- package/commands/measure/results.md +0 -6
- package/commands/plan/api.md +0 -6
- package/commands/plan/challenge.md +0 -6
- package/commands/plan/decide.md +0 -6
- package/commands/plan/feature.md +0 -7
- package/commands/plan/requirements.md +0 -6
- package/commands/remember/context.md +0 -6
- package/commands/remember/handoff.md +0 -6
- package/commands/remember/runbook.md +0 -6
- package/commands/review/code.md +0 -6
- package/commands/review/quality.md +0 -6
- package/commands/review/security.md +0 -6
- package/commands/ship/ready.md +0 -6
- package/commands/ship/release.md +0 -6
- package/commands/ship/status.md +0 -6
- package/commands/understand/docs.md +0 -6
- package/commands/understand/research.md +0 -6
- package/commands/understand/this.md +0 -6
- package/commands/understand/why.md +0 -6
- package/commands/work/clean.md +0 -6
- package/commands/work/drive.md +0 -6
- package/commands/work/optimize-prompts.md +0 -6
- package/commands/work/parallel-review.md +0 -6
- package/db/schema/010_cx_scores.sql +51 -0
- package/lib/beads-client.mjs +19 -1
- package/lib/cli-commands.mjs +280 -146
- package/lib/comment-lint.mjs +5 -1
- package/lib/context-state.mjs +15 -3
- package/lib/contracts/validate.mjs +72 -18
- package/lib/contracts/violation-log.mjs +169 -0
- package/lib/document-extract/docling-client.mjs +114 -0
- package/lib/document-extract/docling-sidecar.py +122 -0
- package/lib/document-extract/whisper-client.mjs +79 -0
- package/lib/document-extract.mjs +73 -8
- package/lib/document-ingest.mjs +26 -4
- package/lib/embed/daemon.mjs +6 -1
- package/lib/flavors/loader.mjs +1 -1
- package/lib/handoffs/contract.mjs +2 -2
- package/lib/hooks/session-start.mjs +1 -1
- package/lib/hooks/stop-notify.mjs +7 -0
- package/lib/ingest/chunker.mjs +94 -0
- package/lib/ingest/pipeline.mjs +53 -0
- package/lib/ingest/store.mjs +82 -0
- package/lib/install/first-invocation.mjs +5 -1
- package/lib/intake/prepare.mjs +22 -4
- package/lib/integrations/intake-integrations.mjs +28 -2
- package/lib/knowledge/search.mjs +12 -0
- package/lib/mcp/server.mjs +142 -1
- package/lib/mcp/tools/skills.mjs +6 -3
- package/lib/mcp/tools/telemetry.mjs +30 -0
- package/lib/mcp/tools/workflow.mjs +6 -2
- package/lib/observation-store.mjs +14 -5
- package/lib/ollama-manager.mjs +32 -28
- package/lib/orchestration-policy.mjs +15 -0
- package/lib/prompt-composer.js +11 -1
- package/lib/roles/gateway.mjs +30 -1
- package/lib/runtime/uv-bootstrap.mjs +129 -0
- package/lib/runtime/whisper-bootstrap.mjs +102 -0
- package/lib/scheduler/index.mjs +24 -4
- package/lib/server/index.mjs +22 -1
- package/lib/server/insights.mjs +12 -0
- package/lib/server/static/index.html +1 -1
- package/lib/setup.mjs +54 -9
- package/lib/specialists/postconditions.mjs +1 -1
- package/lib/sync/skill-frontmatter.mjs +113 -21
- package/lib/tracking-surfaces.mjs +2 -0
- package/lib/update.mjs +31 -3
- package/lib/upgrade.mjs +31 -4
- package/lib/validators/skills.mjs +86 -54
- package/package.json +5 -5
- package/personas/construct.md +6 -7
- package/platforms/claude/CLAUDE.md +43 -15
- package/platforms/claude/settings.template.json +1 -1
- package/rules/common/beads-hygiene.md +3 -9
- package/rules/common/code-review.md +3 -6
- package/rules/common/coding-style.md +3 -6
- package/rules/common/comments.md +3 -7
- package/rules/common/commit-approval.md +3 -6
- package/rules/common/cx-agent-routing.md +3 -7
- package/rules/common/cx-skill-routing.md +3 -3
- package/rules/common/doc-ownership.md +3 -8
- package/rules/common/efficiency.md +3 -8
- package/rules/common/framing.md +3 -8
- package/rules/common/git-workflow.md +3 -5
- package/rules/common/no-fabrication.md +4 -12
- package/rules/common/patterns.md +3 -5
- package/rules/common/release-gates.md +3 -8
- package/rules/common/research.md +3 -8
- package/rules/common/review-before-change.md +3 -9
- package/rules/common/security.md +3 -6
- package/rules/common/skill-composition.md +3 -7
- package/rules/common/testing.md +3 -6
- package/rules/golang/coding-style.md +1 -5
- package/rules/golang/hooks.md +1 -5
- package/rules/golang/patterns.md +1 -5
- package/rules/golang/security.md +1 -5
- package/rules/golang/testing.md +1 -5
- package/rules/python/coding-style.md +1 -5
- package/rules/python/hooks.md +1 -5
- package/rules/python/patterns.md +1 -5
- package/rules/python/security.md +1 -5
- package/rules/python/testing.md +1 -5
- package/rules/swift/coding-style.md +1 -5
- package/rules/swift/hooks.md +1 -5
- package/rules/swift/patterns.md +1 -5
- package/rules/swift/security.md +1 -5
- package/rules/swift/testing.md +1 -5
- package/rules/typescript/coding-style.md +1 -5
- package/rules/typescript/hooks.md +1 -5
- package/rules/typescript/patterns.md +1 -5
- package/rules/typescript/security.md +1 -5
- package/rules/typescript/testing.md +1 -5
- package/rules/web/coding-style.md +3 -5
- package/rules/web/design-quality.md +3 -5
- package/rules/web/hooks.md +3 -5
- package/rules/web/patterns.md +3 -5
- package/rules/web/performance.md +3 -5
- package/rules/web/security.md +3 -5
- package/rules/web/testing.md +3 -5
- package/scripts/sync-specialists.mjs +20 -2
- package/skills/ai/agent-dev.md +4 -5
- package/skills/ai/llm-security.md +4 -5
- package/skills/ai/ml-ops.md +4 -5
- package/skills/ai/orchestration-workflow.md +4 -5
- package/skills/ai/prompt-and-eval.md +4 -5
- package/skills/ai/prompt-optimizer.md +4 -6
- package/skills/ai/rag-system.md +4 -5
- package/skills/architecture/api-design.md +4 -5
- package/skills/architecture/caching.md +4 -5
- package/skills/architecture/cloud-native.md +4 -5
- package/skills/architecture/message-queue.md +4 -5
- package/skills/architecture/security-arch.md +4 -5
- package/skills/compliance/ai-disclosure.md +4 -5
- package/skills/compliance/data-privacy.md +4 -5
- package/skills/compliance/license-audit.md +4 -5
- package/skills/compliance/regulatory-review.md +4 -5
- package/skills/development/cpp.md +4 -5
- package/skills/development/go.md +4 -5
- package/skills/development/java.md +4 -5
- package/skills/development/kotlin.md +4 -5
- package/skills/development/mobile-crossplatform.md +4 -5
- package/skills/development/python.md +4 -5
- package/skills/development/rust.md +4 -5
- package/skills/development/shell.md +4 -5
- package/skills/development/swift.md +4 -5
- package/skills/development/typescript.md +4 -5
- package/skills/devops/ci-cd.md +4 -5
- package/skills/devops/containerization.md +4 -5
- package/skills/devops/cost-optimization.md +4 -5
- package/skills/devops/data-engineering.md +4 -5
- package/skills/devops/database.md +4 -5
- package/skills/devops/dependency-management.md +4 -5
- package/skills/devops/devsecops.md +4 -5
- package/skills/devops/git-workflow.md +4 -5
- package/skills/devops/incident-response.md +4 -5
- package/skills/devops/monorepo.md +4 -5
- package/skills/devops/observability.md +4 -5
- package/skills/devops/performance.md +4 -5
- package/skills/devops/testing.md +4 -5
- package/skills/docs/adr-workflow.md +4 -7
- package/skills/docs/backlog-proposal-workflow.md +4 -3
- package/skills/docs/customer-profile-workflow.md +4 -3
- package/skills/docs/document-ingest-workflow.md +4 -3
- package/skills/docs/evidence-ingest-workflow.md +4 -3
- package/skills/docs/init-docs.md +4 -5
- package/skills/docs/init-project.md +4 -5
- package/skills/docs/prd-workflow.md +4 -7
- package/skills/docs/prfaq-workflow.md +4 -3
- package/skills/docs/product-intelligence-review.md +4 -3
- package/skills/docs/product-intelligence-workflow.md +4 -6
- package/skills/docs/product-signal-workflow.md +4 -5
- package/skills/docs/research-workflow.md +4 -5
- package/skills/docs/runbook-workflow.md +4 -5
- package/skills/docs/strategy-workflow.md +4 -5
- package/skills/exploration/dependency-graph-reading.md +4 -7
- package/skills/exploration/repo-map.md +4 -5
- package/skills/exploration/tracer-bullet-method.md +4 -7
- package/skills/exploration/unknown-codebase-onboarding.md +4 -7
- package/skills/frameworks/django.md +4 -5
- package/skills/frameworks/nextjs.md +4 -5
- package/skills/frameworks/react.md +4 -5
- package/skills/frameworks/spring-boot.md +4 -5
- package/skills/frontend-design/accessibility.md +4 -5
- package/skills/frontend-design/component-patterns.md +4 -5
- package/skills/frontend-design/engineering.md +4 -5
- package/skills/frontend-design/state-management.md +4 -5
- package/skills/frontend-design/ui-aesthetics.md +4 -5
- package/skills/frontend-design/ux-principles.md +4 -5
- package/skills/operating/change-management.md +4 -8
- package/skills/operating/incident-response.md +4 -8
- package/skills/operating/oncall-rotation.md +4 -7
- package/skills/operating/orchestration-reference.md +4 -10
- package/skills/quality-gates/review-work.md +4 -5
- package/skills/quality-gates/verify-change.md +4 -5
- package/skills/quality-gates/verify-module.md +4 -5
- package/skills/quality-gates/verify-quality.md +4 -5
- package/skills/quality-gates/verify-security.md +4 -5
- package/skills/roles/architect.ai-systems.md +6 -9
- package/skills/roles/architect.data.md +6 -9
- package/skills/roles/architect.enterprise.md +6 -9
- package/skills/roles/architect.integration.md +6 -9
- package/skills/roles/architect.md +7 -14
- package/skills/roles/architect.platform.md +6 -9
- package/skills/roles/data-analyst.experiment.md +6 -9
- package/skills/roles/data-analyst.md +6 -9
- package/skills/roles/data-analyst.product-intelligence.md +7 -9
- package/skills/roles/data-analyst.product.md +6 -9
- package/skills/roles/data-analyst.telemetry.md +7 -9
- package/skills/roles/data-engineer.pipeline.md +6 -9
- package/skills/roles/data-engineer.vector-retrieval.md +7 -9
- package/skills/roles/data-engineer.warehouse.md +6 -9
- package/skills/roles/debugger.md +6 -9
- package/skills/roles/designer.accessibility.md +6 -9
- package/skills/roles/designer.md +7 -9
- package/skills/roles/engineer.ai.md +6 -9
- package/skills/roles/engineer.data.md +6 -9
- package/skills/roles/engineer.md +9 -9
- package/skills/roles/engineer.platform.md +6 -9
- package/skills/roles/operator.docs.md +6 -9
- package/skills/roles/operator.md +9 -9
- package/skills/roles/operator.release.md +6 -9
- package/skills/roles/operator.sre.md +6 -9
- package/skills/roles/orchestrator.md +6 -9
- package/skills/roles/product-manager.ai-product.md +6 -9
- package/skills/roles/product-manager.business-strategy.md +6 -9
- package/skills/roles/product-manager.enterprise.md +6 -9
- package/skills/roles/product-manager.growth.md +7 -9
- package/skills/roles/product-manager.md +7 -9
- package/skills/roles/product-manager.platform.md +6 -9
- package/skills/roles/product-manager.product.md +6 -9
- package/skills/roles/qa.ai-eval.md +8 -9
- package/skills/roles/qa.api-contract.md +7 -9
- package/skills/roles/qa.data-pipeline.md +7 -9
- package/skills/roles/qa.md +7 -9
- package/skills/roles/qa.test-automation.md +6 -9
- package/skills/roles/qa.web-ui.md +7 -9
- package/skills/roles/researcher.explorer.md +6 -9
- package/skills/roles/researcher.md +8 -9
- package/skills/roles/researcher.ux.md +6 -9
- package/skills/roles/reviewer.devil-advocate.md +6 -9
- package/skills/roles/reviewer.evaluator.md +6 -9
- package/skills/roles/reviewer.md +9 -9
- package/skills/roles/reviewer.trace.md +6 -9
- package/skills/roles/security.ai.md +7 -9
- package/skills/roles/security.appsec.md +6 -9
- package/skills/roles/security.cloud.md +6 -9
- package/skills/roles/security.legal-compliance.md +6 -9
- package/skills/roles/security.md +7 -9
- package/skills/roles/security.privacy.md +7 -9
- package/skills/roles/security.supply-chain.md +7 -9
- package/skills/security/blue-team.md +4 -5
- package/skills/security/code-audit.md +4 -5
- package/skills/security/pentest.md +4 -5
- package/skills/security/red-team.md +4 -5
- package/skills/security/threat-intel.md +4 -5
- package/skills/security/vuln-research.md +4 -5
- package/skills/strategy/competitive-landscape.md +4 -8
- package/skills/strategy/market-research-methods.md +4 -8
- package/skills/strategy/narrative-arc.md +4 -8
- package/skills/strategy/pricing-positioning.md +4 -8
- package/skills/utility/clean-code.md +4 -5
- package/specialists/policy-inventory.json +14 -0
- package/lib/specialist-contracts-enforce.mjs +0 -172
- package/rules/common/agents.md +0 -28
- package/rules/common/development-workflow.md +0 -32
- package/rules/common/performance.md +0 -55
package/lib/mcp/server.mjs
CHANGED
|
@@ -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
|
|
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
|
|
package/lib/mcp/tools/skills.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import {
|
|
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 =
|
|
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 =
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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 {
|
package/lib/ollama-manager.mjs
CHANGED
|
@@ -6,10 +6,26 @@
|
|
|
6
6
|
* pulling new models, and configuring Ollama as a provider.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
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
|
-
|
|
36
|
+
execFileSync('which', ['ollama'], { stdio: 'pipe' });
|
|
21
37
|
const installed = true;
|
|
22
|
-
|
|
38
|
+
|
|
23
39
|
try {
|
|
24
|
-
const response =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
package/lib/prompt-composer.js
CHANGED
|
@@ -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
|
-
|
|
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, {
|
package/lib/roles/gateway.mjs
CHANGED
|
@@ -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
|
+
}
|