@geraldmaron/construct 1.0.0 → 1.0.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.
- package/.env.example +10 -7
- package/LICENSE +201 -21
- package/README.md +132 -257
- package/agents/contracts.json +387 -0
- package/agents/prompts/cx-accessibility.md +8 -0
- package/agents/prompts/cx-ai-engineer.md +92 -0
- package/agents/prompts/cx-architect.md +10 -2
- package/agents/prompts/cx-business-strategist.md +13 -0
- package/agents/prompts/cx-data-analyst.md +80 -0
- package/agents/prompts/cx-data-engineer.md +4 -0
- package/agents/prompts/cx-debugger.md +8 -0
- package/agents/prompts/cx-designer.md +19 -0
- package/agents/prompts/cx-devil-advocate.md +4 -0
- package/agents/prompts/cx-docs-keeper.md +128 -0
- package/agents/prompts/cx-engineer.md +13 -0
- package/agents/prompts/cx-evaluator.md +4 -0
- package/agents/prompts/cx-explorer.md +12 -0
- package/agents/prompts/cx-legal-compliance.md +13 -0
- package/agents/prompts/cx-operations.md +13 -0
- package/agents/prompts/cx-orchestrator.md +107 -4
- package/agents/prompts/cx-platform-engineer.md +75 -0
- package/agents/prompts/cx-product-manager.md +8 -0
- package/agents/prompts/cx-qa.md +100 -0
- package/agents/prompts/cx-rd-lead.md +13 -0
- package/agents/prompts/cx-release-manager.md +8 -0
- package/agents/prompts/cx-researcher.md +21 -1
- package/agents/prompts/cx-reviewer.md +8 -0
- package/agents/prompts/cx-security.md +104 -0
- package/agents/prompts/cx-sre.md +104 -0
- package/agents/prompts/cx-test-automation.md +4 -0
- package/agents/prompts/cx-trace-reviewer.md +7 -3
- package/agents/prompts/cx-ux-researcher.md +4 -0
- package/agents/registry.json +365 -90
- package/agents/role-manifests.json +217 -0
- package/bin/construct +3345 -141
- package/bin/construct-postinstall.mjs +87 -0
- package/commands/build/feature.md +6 -6
- package/commands/plan/feature.md +6 -5
- package/commands/plan/requirements.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/work/drive.md +3 -3
- package/commands/work/optimize-prompts.md +3 -3
- package/db/{migrations → schema}/001_init.sql +2 -0
- package/db/schema/002_pgvector.sql +182 -0
- package/db/schema/003_intake.sql +47 -0
- package/examples/README.md +85 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
- package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
- package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
- package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
- package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
- package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
- package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
- package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
- package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
- package/examples/personas/construct/bad/commit-without-approval.md +30 -0
- package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
- package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
- package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
- package/examples/provider-plugin/README.md +34 -0
- package/examples/provider-plugin/index.mjs +74 -0
- package/examples/provider-plugin/package.json +15 -0
- package/examples/seed-observations/README.md +38 -0
- package/examples/seed-observations/anti-patterns.md +44 -0
- package/examples/seed-observations/decisions.md +36 -0
- package/examples/seed-observations/patterns.md +42 -0
- package/lib/agent-contracts-enforce.mjs +158 -0
- package/lib/agent-contracts.mjs +231 -0
- package/lib/agents/postconditions.mjs +126 -0
- package/lib/agents/schema.mjs +124 -0
- package/lib/artifact-capture.mjs +183 -0
- package/lib/audit-trail.mjs +149 -0
- package/lib/auto-docs.mjs +245 -65
- package/lib/beads/auto-close.mjs +126 -0
- package/lib/beads/drift.mjs +171 -0
- package/lib/beads-automation.mjs +542 -0
- package/lib/beads-client.mjs +518 -0
- package/lib/beads-lock.mjs +377 -0
- package/lib/beads-optimistic.mjs +365 -0
- package/lib/bootstrap/built-ins.mjs +136 -0
- package/lib/bootstrap/lazy-install.mjs +161 -0
- package/lib/bootstrap/resources.mjs +120 -0
- package/lib/bootstrap.mjs +105 -0
- package/lib/cache-governor.js +213 -0
- package/lib/cache-strategy-anthropic.js +62 -0
- package/lib/cache-strategy-google.js +79 -0
- package/lib/cache-strategy-none.js +30 -0
- package/lib/cache-strategy-openai.js +48 -0
- package/lib/cache-strategy.js +91 -0
- package/lib/claude-allow.mjs +149 -0
- package/lib/cli-commands.mjs +413 -258
- package/lib/codex-config.mjs +3 -1
- package/lib/comment-lint.mjs +55 -6
- package/lib/completions.mjs +21 -6
- package/lib/config/alias.mjs +56 -0
- package/lib/config/project-config.mjs +335 -0
- package/lib/config/schema.mjs +159 -0
- package/lib/context-router.mjs +308 -0
- package/lib/cost-ledger.mjs +177 -0
- package/lib/cost.mjs +171 -10
- package/lib/dashboard-static.mjs +158 -0
- package/lib/deployment-mode.mjs +86 -0
- package/lib/deprecate.mjs +49 -0
- package/lib/dispatch-batch.js +183 -0
- package/lib/distill.mjs +21 -8
- package/lib/doc-stamp.mjs +164 -0
- package/lib/doc-verify.mjs +119 -0
- package/lib/docs-routing.mjs +89 -0
- package/lib/docs-verify.mjs +417 -0
- package/lib/doctor/audit.mjs +71 -0
- package/lib/doctor/cli.mjs +99 -0
- package/lib/doctor/escalate.mjs +29 -0
- package/lib/doctor/index.mjs +140 -0
- package/lib/doctor/report.mjs +170 -0
- package/lib/doctor/watchers/bd-watch.mjs +117 -0
- package/lib/doctor/watchers/cost.mjs +130 -0
- package/lib/doctor/watchers/disk.mjs +122 -0
- package/lib/doctor/watchers/handoffs.mjs +33 -0
- package/lib/doctor/watchers/process-pressure.mjs +60 -0
- package/lib/doctor/watchers/service-health.mjs +188 -0
- package/lib/document-extract.mjs +288 -0
- package/lib/document-ingest.mjs +230 -0
- package/lib/drop.mjs +282 -0
- package/lib/embed/approval-queue.mjs +176 -0
- package/lib/embed/artifact.mjs +349 -0
- package/lib/embed/authority-guard.mjs +155 -0
- package/lib/embed/cli.mjs +408 -0
- package/lib/embed/config.mjs +355 -0
- package/lib/embed/conflict-detection.mjs +264 -0
- package/lib/embed/customer-profiles.mjs +480 -0
- package/lib/embed/daemon.mjs +1309 -0
- package/lib/embed/demand-fetch.mjs +449 -0
- package/lib/embed/docs-lifecycle.mjs +349 -0
- package/lib/embed/inbox-live-watcher.mjs +119 -0
- package/lib/embed/inbox.mjs +343 -0
- package/lib/embed/intake-metrics.mjs +190 -0
- package/lib/embed/jobs/vector-sync.mjs +198 -0
- package/lib/embed/notifications.mjs +75 -0
- package/lib/embed/output.mjs +79 -0
- package/lib/embed/providers/github.mjs +295 -0
- package/lib/embed/providers/jira.mjs +192 -0
- package/lib/embed/providers/linear.mjs +186 -0
- package/lib/embed/providers/registry.mjs +115 -0
- package/lib/embed/providers/slack.mjs +203 -0
- package/lib/embed/recommendation-store.mjs +378 -0
- package/lib/embed/roadmap.mjs +374 -0
- package/lib/embed/role-framing.mjs +110 -0
- package/lib/embed/scheduler.mjs +99 -0
- package/lib/embed/semantic.mjs +325 -0
- package/lib/embed/snapshot.mjs +191 -0
- package/lib/embed/supervision.mjs +235 -0
- package/lib/embed/target-resolver.mjs +186 -0
- package/lib/embed/worker.mjs +62 -0
- package/lib/embed/workspaces.mjs +297 -0
- package/lib/engine/chunker-headings.mjs +110 -0
- package/lib/engine/compressor-heuristic.mjs +100 -0
- package/lib/engine/consolidate.mjs +287 -0
- package/lib/engine/contracts.mjs +126 -0
- package/lib/engine/defaults.mjs +129 -0
- package/lib/engine/eval-retrieval.mjs +148 -0
- package/lib/engine/fuser-rrf.mjs +62 -0
- package/lib/engine/index.mjs +37 -0
- package/lib/engine/registry.mjs +146 -0
- package/lib/engine/reranker-mmr.mjs +90 -0
- package/lib/engine/tokens.mjs +77 -0
- package/lib/entity-store.mjs +280 -0
- package/lib/env-config.mjs +69 -4
- package/lib/evals/retrieval-bench.mjs +159 -0
- package/lib/evaluator-optimizer.mjs +317 -0
- package/lib/features.mjs +159 -29
- package/lib/gates-audit.mjs +236 -0
- package/lib/git-hooks/prepare-commit-msg +58 -0
- package/lib/handoffs/cleanup.mjs +159 -0
- package/lib/handoffs/contract.mjs +162 -0
- package/lib/handoffs/inventory.mjs +120 -0
- package/lib/headhunt.mjs +28 -47
- package/lib/health-check.mjs +399 -0
- package/lib/hook-health.mjs +442 -0
- package/lib/hooks/_lib/log.mjs +82 -0
- package/lib/hooks/adaptive-lint.mjs +26 -2
- package/lib/hooks/agent-tracker.mjs +171 -14
- package/lib/hooks/audit-reads.mjs +109 -0
- package/lib/hooks/audit-trail.mjs +153 -0
- package/lib/hooks/bash-output-logger.mjs +71 -0
- package/lib/hooks/block-no-verify.mjs +41 -0
- package/lib/hooks/ci-status-check.mjs +82 -0
- package/lib/hooks/comment-lint.mjs +29 -7
- package/lib/hooks/config-protection.mjs +39 -18
- package/lib/hooks/context-watch.mjs +137 -0
- package/lib/hooks/context-window-recovery.mjs +4 -20
- package/lib/hooks/dep-audit.mjs +16 -0
- package/lib/hooks/doc-coupling-check.mjs +80 -0
- package/lib/hooks/edit-accumulator.mjs +3 -0
- package/lib/hooks/edit-error-recovery.mjs +3 -0
- package/lib/hooks/edit-guard.mjs +45 -4
- package/lib/hooks/env-check.mjs +3 -0
- package/lib/hooks/guard-bash.mjs +58 -1
- package/lib/hooks/mcp-audit.mjs +18 -20
- package/lib/hooks/mcp-health-check.mjs +36 -0
- package/lib/hooks/model-fallback.mjs +42 -56
- package/lib/hooks/policy-engine.mjs +209 -0
- package/lib/hooks/post-merge-docs-check.mjs +63 -0
- package/lib/hooks/pre-compact.mjs +4 -24
- package/lib/hooks/pre-push-gate.mjs +200 -37
- package/lib/hooks/proactive-activation.mjs +284 -0
- package/lib/hooks/read-tracker.mjs +27 -4
- package/lib/hooks/readme-age-check.mjs +77 -0
- package/lib/hooks/registry-sync.mjs +12 -5
- package/lib/hooks/scan-secrets.mjs +50 -7
- package/lib/hooks/session-optimize.mjs +311 -0
- package/lib/hooks/session-start.mjs +287 -28
- package/lib/hooks/stop-notify.mjs +236 -96
- package/lib/hooks/stop-typecheck.mjs +13 -1
- package/lib/hooks/test-watch.mjs +68 -0
- package/lib/host-capabilities.mjs +31 -10
- package/lib/init-docs.mjs +731 -291
- package/lib/init-unified.mjs +1116 -0
- package/lib/init-update.mjs +168 -0
- package/lib/init.mjs +107 -0
- package/lib/install/first-invocation.mjs +119 -0
- package/lib/install/stage-project.mjs +69 -0
- package/lib/intake/classify.mjs +278 -0
- package/lib/intake/feedback.mjs +273 -0
- package/lib/intake/filesystem-queue.mjs +158 -0
- package/lib/intake/intake-config.mjs +132 -0
- package/lib/intake/postgres-queue.mjs +198 -0
- package/lib/intake/prepare.mjs +139 -0
- package/lib/intake/queue.mjs +83 -0
- package/lib/intake/session-prelude.mjs +50 -0
- package/lib/integrations/intake-integrations.mjs +740 -0
- package/lib/intent-classifier.mjs +253 -0
- package/lib/knowledge/layout.mjs +72 -0
- package/lib/knowledge/rag.mjs +331 -0
- package/lib/knowledge/search.mjs +315 -0
- package/lib/knowledge/trends.mjs +261 -0
- package/lib/logger.mjs +85 -0
- package/lib/mcp/broker.mjs +124 -0
- package/lib/mcp/server.mjs +554 -854
- package/lib/mcp/tools/document.mjs +132 -0
- package/lib/mcp/tools/memory.mjs +209 -0
- package/lib/mcp/tools/project.mjs +349 -0
- package/lib/mcp/tools/skills.mjs +409 -0
- package/lib/mcp/tools/storage.mjs +60 -0
- package/lib/mcp/tools/telemetry.mjs +315 -0
- package/lib/mcp/tools/workflow.mjs +112 -0
- package/lib/mcp-catalog.json +54 -3
- package/lib/mcp-manager.mjs +96 -48
- package/lib/mcp-platform-config.mjs +22 -22
- package/lib/memory-stats.mjs +121 -0
- package/lib/mode-commands.mjs +124 -0
- package/lib/model-free-selector.mjs +184 -0
- package/lib/model-pricing.mjs +152 -0
- package/lib/model-registry.mjs +226 -0
- package/lib/model-router.mjs +362 -386
- package/lib/observation-store.mjs +391 -0
- package/lib/ollama-manager.mjs +428 -0
- package/lib/opencode-config.mjs +13 -3
- package/lib/opencode-runtime-plugin.mjs +70 -38
- package/lib/opencode-telemetry.mjs +64 -6
- package/lib/orchestration-policy.mjs +509 -6
- package/lib/overrides/resolver.mjs +207 -0
- package/lib/parity.mjs +147 -0
- package/lib/paths.mjs +33 -0
- package/lib/performance/generate.mjs +212 -0
- package/lib/plugin-registry.mjs +268 -0
- package/lib/policy/engine.mjs +130 -0
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-detection.mjs +129 -0
- package/lib/project-init-shared.mjs +272 -0
- package/lib/project-profile.mjs +447 -0
- package/lib/prompt-composer.js +434 -0
- package/lib/prompt-metadata.mjs +1 -1
- package/lib/provider-capabilities-anthropic.js +44 -0
- package/lib/provider-capabilities-deepseek.js +37 -0
- package/lib/provider-capabilities-generic.js +26 -0
- package/lib/provider-capabilities-google.js +47 -0
- package/lib/provider-capabilities-openai.js +45 -0
- package/lib/provider-capabilities.js +142 -0
- package/lib/providers/atlassian-confluence/index.mjs +103 -0
- package/lib/providers/atlassian-jira/index.mjs +100 -0
- package/lib/providers/auth-manager.mjs +126 -0
- package/lib/providers/circuit-breaker.mjs +124 -0
- package/lib/providers/contract.mjs +93 -0
- package/lib/providers/github/index.mjs +126 -0
- package/lib/providers/registry.mjs +184 -0
- package/lib/providers/salesforce/index.mjs +100 -0
- package/lib/providers/slack/index.mjs +80 -0
- package/lib/reflect.mjs +137 -0
- package/lib/research-lint.mjs +164 -0
- package/lib/resources/budget.mjs +259 -0
- package/lib/role-preload.mjs +21 -6
- package/lib/roles/approval-surface.mjs +54 -0
- package/lib/roles/cli.mjs +118 -0
- package/lib/roles/event-bus.mjs +79 -0
- package/lib/roles/fence.mjs +84 -0
- package/lib/roles/gateway.mjs +260 -0
- package/lib/roles/hook-emit.mjs +37 -0
- package/lib/roles/manifest.mjs +48 -0
- package/lib/roles/router.mjs +27 -0
- package/lib/runtime-pressure.mjs +360 -0
- package/lib/schema-artifact.mjs +134 -0
- package/lib/schema-infer.mjs +551 -0
- package/lib/server/auth.mjs +168 -0
- package/lib/server/chat.mjs +336 -0
- package/lib/server/cors.mjs +77 -0
- package/lib/server/csrf.mjs +91 -0
- package/lib/server/index.mjs +1927 -78
- package/lib/server/insights.mjs +765 -0
- package/lib/server/rate-limit.mjs +91 -0
- package/lib/server/static/assets/index-ab25c707.js +70 -0
- package/lib/server/static/assets/index-f0c80a2b.css +1 -0
- package/lib/server/static/index.html +12 -817
- package/lib/server/telemetry-login.mjs +108 -0
- package/lib/server/webhook.mjs +510 -0
- package/lib/service-manager.mjs +522 -58
- package/lib/services/pattern-promotion-service.mjs +167 -0
- package/lib/services/telemetry-backend.mjs +178 -0
- package/lib/session-store.mjs +374 -0
- package/lib/setup-prompts.mjs +96 -0
- package/lib/setup.mjs +525 -38
- package/lib/skills-apply.mjs +280 -0
- package/lib/skills-scope.mjs +118 -0
- package/lib/status.mjs +261 -70
- package/lib/storage/admin.mjs +355 -0
- package/lib/storage/backend.mjs +2 -1
- package/lib/storage/backup.mjs +347 -0
- package/lib/storage/embeddings-engine.mjs +133 -0
- package/lib/storage/embeddings-legacy.mjs +85 -0
- package/lib/storage/embeddings-local.mjs +108 -0
- package/lib/storage/embeddings-ollama.mjs +78 -0
- package/lib/storage/embeddings-openai.mjs +85 -0
- package/lib/storage/embeddings.mjs +92 -33
- package/lib/storage/file-lock.mjs +130 -0
- package/lib/storage/fusion.mjs +95 -0
- package/lib/storage/hybrid-query.mjs +34 -27
- package/lib/storage/migrations.mjs +187 -0
- package/lib/storage/postgres-backup.mjs +124 -0
- package/lib/storage/sql-store.mjs +5 -15
- package/lib/storage/state-source.mjs +12 -13
- package/lib/storage/store-version.mjs +115 -0
- package/lib/storage/sync.mjs +144 -35
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/storage/vector-client.mjs +286 -0
- package/lib/storage/vector-store.mjs +71 -30
- package/lib/task-graph/generate.mjs +135 -0
- package/lib/task-graph/schema.mjs +81 -0
- package/lib/task-graph/store.mjs +71 -0
- package/lib/telemetry/backends/local.mjs +62 -0
- package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
- package/lib/telemetry/backfill.mjs +180 -0
- package/lib/telemetry/eval-datasets.mjs +203 -0
- package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
- package/lib/telemetry/intent-verifications.mjs +86 -0
- package/lib/telemetry/llm-judge.mjs +350 -0
- package/lib/telemetry/model-pricing-catalog.mjs +557 -0
- package/lib/telemetry/setup.mjs +151 -0
- package/lib/telemetry/skill-calls.mjs +78 -0
- package/lib/telemetry/team-rollup.mjs +4 -4
- package/lib/token-engine.js +117 -0
- package/lib/token-estimator-anthropic.js +15 -0
- package/lib/token-estimator-deepseek.js +13 -0
- package/lib/token-estimator-default.js +13 -0
- package/lib/token-estimator-google.js +13 -0
- package/lib/token-estimator-openai.js +13 -0
- package/lib/toolkit-env.mjs +1 -1
- package/lib/tty-prompts.mjs +211 -0
- package/lib/uninstall/uninstall.mjs +423 -0
- package/lib/update.mjs +115 -0
- package/lib/upgrade.mjs +141 -0
- package/lib/validator.mjs +51 -2
- package/lib/validators/skills.mjs +142 -0
- package/lib/wireframe.mjs +422 -0
- package/lib/worker/entrypoint.mjs +241 -0
- package/lib/worker/evidence.mjs +107 -0
- package/lib/worker/run.mjs +154 -0
- package/lib/worker/trace.mjs +182 -0
- package/lib/workflow-state.mjs +14 -18
- package/package.json +21 -8
- package/personas/construct.md +53 -51
- package/platforms/claude/CLAUDE.md +1 -1
- package/platforms/claude/settings.template.json +115 -68
- package/platforms/opencode/config.template.json +3 -7
- package/rules/common/agents.md +11 -38
- package/rules/common/beads-hygiene.md +75 -0
- package/rules/common/code-review.md +11 -100
- package/rules/common/coding-style.md +5 -3
- package/rules/common/comments.md +30 -111
- package/rules/common/commit-approval.md +53 -0
- package/rules/common/cx-agent-routing.md +1 -0
- package/rules/common/development-workflow.md +23 -41
- package/rules/common/doc-ownership.md +54 -0
- package/rules/common/efficiency.md +57 -0
- package/rules/common/framing.md +76 -0
- package/rules/common/git-workflow.md +2 -4
- package/rules/common/patterns.md +3 -7
- package/rules/common/performance.md +15 -31
- package/rules/common/release-gates.md +69 -0
- package/rules/common/research.md +107 -0
- package/rules/common/security.md +6 -6
- package/rules/common/skill-composition.md +67 -0
- package/rules/common/testing.md +15 -27
- package/rules/golang/hooks.md +0 -4
- package/rules/policy/bootstrap.yaml +11 -0
- package/rules/policy/drive.yaml +8 -0
- package/rules/policy/task.yaml +9 -0
- package/rules/policy/workflow.yaml +9 -0
- package/rules/python/hooks.md +0 -4
- package/rules/swift/hooks.md +0 -4
- package/rules/typescript/hooks.md +0 -4
- package/rules/web/hooks.md +0 -2
- package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
- package/skills/ai/prompt-optimizer.md +7 -7
- package/skills/compliance/ai-disclosure.md +58 -0
- package/skills/compliance/data-privacy.md +46 -0
- package/skills/compliance/license-audit.md +40 -0
- package/skills/compliance/regulatory-review.md +61 -0
- package/skills/docs/document-ingest-workflow.md +52 -0
- package/skills/docs/evidence-ingest-workflow.md +9 -0
- package/skills/docs/init-docs.md +51 -18
- package/skills/docs/product-intelligence-workflow.md +12 -0
- package/skills/docs/product-signal-workflow.md +4 -0
- package/skills/docs/research-workflow.md +23 -8
- package/skills/docs/runbook-workflow.md +1 -1
- package/skills/operating/orchestration-reference.md +151 -0
- package/skills/roles/architect.md +5 -0
- package/skills/roles/engineer.md +32 -0
- package/skills/roles/operator.md +12 -0
- package/skills/roles/reviewer.md +23 -0
- package/skills/routing.md +1 -1
- package/templates/devcontainer/Dockerfile.devcontainer +38 -0
- package/templates/devcontainer/devcontainer.json +31 -0
- package/templates/distribution/bootstrap.ps1 +142 -0
- package/templates/distribution/bootstrap.sh +196 -0
- package/templates/distribution/run.mjs +187 -0
- package/templates/docs/adr.md +44 -8
- package/templates/docs/changelog-entry.md +43 -0
- package/templates/docs/construct_guide.md +149 -0
- package/templates/docs/evidence-brief.md +2 -2
- package/templates/docs/meta-prd.md +140 -19
- package/templates/docs/onboarding.md +57 -0
- package/templates/docs/prd.md +159 -15
- package/templates/docs/research-brief.md +4 -4
- package/templates/homebrew/construct.rb +67 -0
- package/langfuse/docker-compose.yml +0 -82
- package/lib/eval-harness.mjs +0 -59
- package/lib/hooks/bootstrap-guard.mjs +0 -90
- package/lib/hooks/console-warn.mjs +0 -43
- package/lib/hooks/continuation-enforcer.mjs +0 -72
- package/lib/hooks/drive-guard.mjs +0 -89
- package/lib/hooks/mcp-task-scope.mjs +0 -47
- package/lib/hooks/task-completed-guard.mjs +0 -43
- package/lib/hooks/teammate-idle-guard.mjs +0 -54
- package/lib/hooks/workflow-guard.mjs +0 -62
- package/lib/prompt-composer.mjs +0 -196
- package/lib/review.mjs +0 -429
- package/lib/server/static/app.js +0 -841
- package/lib/telemetry/langfuse-model-sync.mjs +0 -270
- package/rules/common/hooks.md +0 -35
- package/rules/zh/README.md +0 -113
- package/rules/zh/agents.md +0 -55
- package/rules/zh/code-review.md +0 -129
- package/rules/zh/coding-style.md +0 -53
- package/rules/zh/development-workflow.md +0 -49
- package/rules/zh/git-workflow.md +0 -29
- package/rules/zh/hooks.md +0 -35
- package/rules/zh/patterns.md +0 -36
- package/rules/zh/performance.md +0 -60
- package/rules/zh/security.md +0 -34
- package/rules/zh/testing.md +0 -34
package/lib/mcp-manager.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP manager — add, remove, list MCPs in
|
|
3
|
-
* Uses
|
|
2
|
+
* MCP manager — add, remove, list MCPs in host configs.
|
|
3
|
+
* Uses the plugin registry as the source of truth for built-in and external integrations.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { execSync } from 'node:child_process';
|
|
7
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
8
8
|
import { createInterface } from 'node:readline';
|
|
9
9
|
import { homedir, platform } from 'node:os';
|
|
10
10
|
import { join, dirname } from 'node:path';
|
|
11
|
-
import {
|
|
11
|
+
import { selectOption } from './tty-prompts.mjs';
|
|
12
12
|
import { readOpenCodeConfig, writeOpenCodeConfig } from './opencode-config.mjs';
|
|
13
13
|
import { getUserEnvPath, loadConstructEnv, writeEnvValues } from './env-config.mjs';
|
|
14
14
|
import {
|
|
@@ -26,29 +26,18 @@ import {
|
|
|
26
26
|
buildClaudeMcpEntry,
|
|
27
27
|
buildOpenCodeMcpEntry,
|
|
28
28
|
getOpenCodeMcpId,
|
|
29
|
-
normalizeInstalledOpenCodeMcpId,
|
|
30
29
|
} from './mcp-platform-config.mjs';
|
|
30
|
+
import { getMcpById as getMcpByIdFromRegistry, loadPluginRegistry } from './plugin-registry.mjs';
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const CLAUDE_SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
|
|
32
|
+
function getClaudeSettingsPath() {
|
|
33
|
+
return join(homedir(), '.claude', 'settings.json');
|
|
34
|
+
}
|
|
36
35
|
|
|
37
36
|
function openUrl(url) {
|
|
38
37
|
try {
|
|
39
38
|
const opener = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open';
|
|
40
39
|
execSync(`${opener} "${url}"`, { stdio: 'ignore' });
|
|
41
|
-
} catch {
|
|
42
|
-
// best effort
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function loadCatalog() {
|
|
47
|
-
const catalog = JSON.parse(readFileSync(CATALOG_PATH, 'utf8'));
|
|
48
|
-
return {
|
|
49
|
-
...catalog,
|
|
50
|
-
mcps: (catalog.mcps ?? []).map(normalizeMcp),
|
|
51
|
-
};
|
|
40
|
+
} catch { /* browser launch is non-critical */ }
|
|
52
41
|
}
|
|
53
42
|
|
|
54
43
|
function normalizeMcp(mcp) {
|
|
@@ -59,6 +48,7 @@ function normalizeMcp(mcp) {
|
|
|
59
48
|
requiredEnv: Array.isArray(mcp.requiredEnv) ? mcp.requiredEnv : [],
|
|
60
49
|
setupModes: Array.isArray(mcp.setupModes) ? mcp.setupModes : ['manual'],
|
|
61
50
|
usedBy: Array.isArray(mcp.usedBy) ? mcp.usedBy : [],
|
|
51
|
+
hostSupport: mcp.hostSupport && typeof mcp.hostSupport === 'object' ? mcp.hostSupport : {},
|
|
62
52
|
};
|
|
63
53
|
}
|
|
64
54
|
|
|
@@ -66,6 +56,24 @@ function hasSetupMode(mcp, mode) {
|
|
|
66
56
|
return mcp.setupModes.includes(mode);
|
|
67
57
|
}
|
|
68
58
|
|
|
59
|
+
function getHostSupport(mcp, host) {
|
|
60
|
+
const support = mcp?.hostSupport?.[host];
|
|
61
|
+
if (!support) return { mode: 'managed' };
|
|
62
|
+
if (typeof support === 'string') return { mode: support };
|
|
63
|
+
if (typeof support === 'object') return { mode: support.mode ?? 'managed', ...support };
|
|
64
|
+
return { mode: 'managed' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isManagedOnHost(mcp, host) {
|
|
68
|
+
return getHostSupport(mcp, host).mode === 'managed';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function collectPluginBackedMcpIds(catalog) {
|
|
72
|
+
return (catalog.mcps ?? [])
|
|
73
|
+
.filter((mcp) => ['claude', 'opencode', 'codex'].some((host) => getHostSupport(mcp, host).mode === 'plugin'))
|
|
74
|
+
.map((mcp) => mcp.id);
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
function parseAddFlags() {
|
|
70
78
|
const args = new Set(process.argv.slice(2));
|
|
71
79
|
return {
|
|
@@ -86,13 +94,22 @@ async function chooseSetupMode(mcp, flags) {
|
|
|
86
94
|
if (!canManual) return 'auto';
|
|
87
95
|
if (!process.stdin.isTTY) return 'manual';
|
|
88
96
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
return selectOption({
|
|
98
|
+
title: 'Choose setup mode',
|
|
99
|
+
instructions: 'Pick how Construct should configure this integration.',
|
|
100
|
+
options: [
|
|
101
|
+
{
|
|
102
|
+
value: 'auto',
|
|
103
|
+
label: 'Auto-configure',
|
|
104
|
+
description: 'Let Construct detect, provision, or prefill what it can.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
value: 'manual',
|
|
108
|
+
label: 'Manual setup',
|
|
109
|
+
description: 'Provide credentials, URLs, and config values yourself.',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
});
|
|
96
113
|
}
|
|
97
114
|
|
|
98
115
|
function getConfiguredEnvValue(name) {
|
|
@@ -130,17 +147,19 @@ function getConfiguredEnvValues(mcp) {
|
|
|
130
147
|
}
|
|
131
148
|
|
|
132
149
|
function loadSettings() {
|
|
133
|
-
|
|
150
|
+
const claudeSettingsPath = getClaudeSettingsPath();
|
|
151
|
+
if (!existsSync(claudeSettingsPath)) return {};
|
|
134
152
|
try {
|
|
135
|
-
return JSON.parse(readFileSync(
|
|
153
|
+
return JSON.parse(readFileSync(claudeSettingsPath, 'utf8'));
|
|
136
154
|
} catch {
|
|
137
155
|
return {};
|
|
138
156
|
}
|
|
139
157
|
}
|
|
140
158
|
|
|
141
159
|
function saveSettings(settings) {
|
|
142
|
-
|
|
143
|
-
|
|
160
|
+
const claudeSettingsPath = getClaudeSettingsPath();
|
|
161
|
+
mkdirSync(dirname(claudeSettingsPath), { recursive: true });
|
|
162
|
+
writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
144
163
|
}
|
|
145
164
|
|
|
146
165
|
function loadOpenCodeConfig() {
|
|
@@ -152,20 +171,23 @@ function saveOpenCodeConfig(config) {
|
|
|
152
171
|
}
|
|
153
172
|
|
|
154
173
|
function getMcpById(id) {
|
|
155
|
-
|
|
156
|
-
return catalog.mcps.find(m => m.id === id) ?? null;
|
|
174
|
+
return getMcpByIdFromRegistry(id, { cwd: process.cwd(), homeDir: homedir() });
|
|
157
175
|
}
|
|
158
176
|
|
|
159
177
|
function getInstalledMcps() {
|
|
178
|
+
const registry = loadPluginRegistry({ cwd: process.cwd(), homeDir: homedir() });
|
|
160
179
|
const settings = loadSettings();
|
|
161
180
|
const oc = loadOpenCodeConfig();
|
|
162
181
|
const codexConfig = readCodexConfig();
|
|
163
182
|
const codexServers = Array.from(codexConfig.matchAll(/^\[mcp_servers\.("?)([^"\]\n]+)\1\]/gm)).map((match) => match[2]);
|
|
164
183
|
const set = new Set([
|
|
165
184
|
...Object.keys(settings.mcpServers ?? {}),
|
|
166
|
-
...Object.keys(oc.mcp ?? {})
|
|
185
|
+
...Object.keys(oc.mcp ?? {}),
|
|
167
186
|
...codexServers,
|
|
168
187
|
]);
|
|
188
|
+
for (const id of collectPluginBackedMcpIds({ mcps: registry.mcps.map(normalizeMcp) })) {
|
|
189
|
+
set.add(id);
|
|
190
|
+
}
|
|
169
191
|
return Array.from(set);
|
|
170
192
|
}
|
|
171
193
|
|
|
@@ -196,14 +218,20 @@ function removeCodexMcp(id) {
|
|
|
196
218
|
* Show all MCPs from catalog with installed/missing status.
|
|
197
219
|
*/
|
|
198
220
|
export function cmdMcpList() {
|
|
199
|
-
const
|
|
221
|
+
const registry = loadPluginRegistry({ cwd: process.cwd(), homeDir: homedir() });
|
|
200
222
|
const installed = new Set(getInstalledMcps());
|
|
201
223
|
|
|
224
|
+
if (!registry.valid) {
|
|
225
|
+
console.log('Plugin registry errors:');
|
|
226
|
+
for (const error of registry.errors) console.log(` ✗ ${error}`);
|
|
227
|
+
console.log('');
|
|
228
|
+
}
|
|
229
|
+
|
|
202
230
|
const categories = ['core', 'optional', 'integration'];
|
|
203
231
|
const labels = { core: 'Core (Essential)', optional: 'Enhancements', integration: 'Third-Party' };
|
|
204
232
|
|
|
205
233
|
for (const cat of categories) {
|
|
206
|
-
const mcps =
|
|
234
|
+
const mcps = registry.mcps.filter(m => m.category === cat);
|
|
207
235
|
if (!mcps.length) continue;
|
|
208
236
|
|
|
209
237
|
console.log(`\n${labels[cat]}:`);
|
|
@@ -212,7 +240,8 @@ export function cmdMcpList() {
|
|
|
212
240
|
const status = installed.has(mcp.id) ? '✓' : '·';
|
|
213
241
|
const name = mcp.name.padEnd(20);
|
|
214
242
|
const req = mcp.requiredEnv.length ? ` [needs: ${mcp.requiredEnv.join(', ')}]` : '';
|
|
215
|
-
|
|
243
|
+
const source = mcp.pluginId !== 'construct-builtins' ? ` [plugin: ${mcp.pluginId}]` : '';
|
|
244
|
+
console.log(` ${status} ${name} ${mcp.description.substring(0, 50)}${req}${source}`);
|
|
216
245
|
}
|
|
217
246
|
}
|
|
218
247
|
|
|
@@ -338,7 +367,11 @@ export async function cmdMcpAdd(id) {
|
|
|
338
367
|
// Write to Claude settings
|
|
339
368
|
const settings = loadSettings();
|
|
340
369
|
if (!settings.mcpServers) settings.mcpServers = {};
|
|
341
|
-
|
|
370
|
+
if (isManagedOnHost(mcp, 'claude')) {
|
|
371
|
+
settings.mcpServers[id] = buildClaudeMcpEntry(id, mcp, resolvedValues);
|
|
372
|
+
} else {
|
|
373
|
+
delete settings.mcpServers[id];
|
|
374
|
+
}
|
|
342
375
|
saveSettings(settings);
|
|
343
376
|
|
|
344
377
|
// Write to OpenCode config
|
|
@@ -346,13 +379,19 @@ export async function cmdMcpAdd(id) {
|
|
|
346
379
|
if (!oc.mcp) oc.mcp = {};
|
|
347
380
|
const openCodeId = getOpenCodeMcpId(id);
|
|
348
381
|
delete oc.mcp[id];
|
|
349
|
-
|
|
382
|
+
if (id === 'memory') delete oc.mcp.cass;
|
|
383
|
+
if (isManagedOnHost(mcp, 'opencode')) {
|
|
384
|
+
oc.mcp[openCodeId] = buildOpenCodeMcpEntry(id, mcp, resolvedValues).entry;
|
|
385
|
+
} else {
|
|
386
|
+
delete oc.mcp[openCodeId];
|
|
387
|
+
}
|
|
350
388
|
saveOpenCodeConfig(oc);
|
|
351
389
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
390
|
+
if (isManagedOnHost(mcp, 'codex')) {
|
|
391
|
+
writeCodexMcp(id, mcp, resolvedValues);
|
|
392
|
+
} else {
|
|
393
|
+
removeCodexMcp(id);
|
|
394
|
+
}
|
|
356
395
|
|
|
357
396
|
// --- SYNC TO .ENV ---
|
|
358
397
|
const envPath = getUserEnvPath(homedir());
|
|
@@ -364,8 +403,14 @@ export async function cmdMcpAdd(id) {
|
|
|
364
403
|
console.log(`✓ Setup mode: ${setupMode}`);
|
|
365
404
|
console.log(`✓ Services synchronized.`);
|
|
366
405
|
console.log(`\nNext: Restart OpenCode, Claude Code, or Codex to activate the new tools.`);
|
|
367
|
-
|
|
368
|
-
|
|
406
|
+
for (const host of ['claude', 'opencode', 'codex']) {
|
|
407
|
+
const support = getHostSupport(mcp, host);
|
|
408
|
+
const hostLabel = host === 'claude' ? 'Claude Code' : host === 'opencode' ? 'OpenCode' : 'Codex';
|
|
409
|
+
if (support.mode === 'plugin') {
|
|
410
|
+
console.log(` ${hostLabel} uses ${support.label ?? 'a native plugin/app path'} for ${mcp.name}; Construct does not install a standalone MCP there.`);
|
|
411
|
+
} else if (support.mode === 'unsupported') {
|
|
412
|
+
console.log(` ${hostLabel} does not support managed ${mcp.name} setup.`);
|
|
413
|
+
}
|
|
369
414
|
}
|
|
370
415
|
|
|
371
416
|
if (mcp.usedBy.length) {
|
|
@@ -382,11 +427,11 @@ export function cmdMcpRemove(id) {
|
|
|
382
427
|
const oc = openCodeState.config ?? {};
|
|
383
428
|
const openCodeId = getOpenCodeMcpId(id);
|
|
384
429
|
const hasClaudeEntry = Boolean(settings.mcpServers?.[id]);
|
|
385
|
-
const hasOpenCodeEntry = Boolean(oc.mcp?.[openCodeId] || oc.mcp?.[id]);
|
|
430
|
+
const hasOpenCodeEntry = Boolean(oc.mcp?.[openCodeId] || oc.mcp?.[id] || (id === 'memory' && oc.mcp?.cass));
|
|
386
431
|
const hasCodexEntry = readCodexConfig().includes(`[mcp_servers.${id}]`) || readCodexConfig().includes(`[mcp_servers.${tomlString(id)}]`);
|
|
387
432
|
if (!hasClaudeEntry && !hasOpenCodeEntry && !hasCodexEntry) {
|
|
388
|
-
console.
|
|
389
|
-
|
|
433
|
+
console.log(`${id} is not installed. Nothing to remove.`);
|
|
434
|
+
return;
|
|
390
435
|
}
|
|
391
436
|
|
|
392
437
|
const mcp = getMcpById(id);
|
|
@@ -398,8 +443,10 @@ export function cmdMcpRemove(id) {
|
|
|
398
443
|
if (oc.mcp) {
|
|
399
444
|
delete oc.mcp[openCodeId];
|
|
400
445
|
delete oc.mcp[id];
|
|
446
|
+
if (id === 'memory') delete oc.mcp.cass;
|
|
401
447
|
}
|
|
402
448
|
if (openCodeState.config || hasOpenCodeEntry) saveOpenCodeConfig(oc);
|
|
449
|
+
else if (openCodeState.file && existsSync(openCodeState.file)) rmSync(openCodeState.file, { force: true });
|
|
403
450
|
removeCodexMcp(id);
|
|
404
451
|
|
|
405
452
|
console.log(`✓ ${name} removed from Claude Code, OpenCode, and Codex config`);
|
|
@@ -422,6 +469,7 @@ export function cmdMcpInfo(id) {
|
|
|
422
469
|
console.log('─'.repeat(48));
|
|
423
470
|
console.log(`Status: ${installed ? '✓ installed' : '· not installed'}`);
|
|
424
471
|
console.log(`Category: ${mcp.category}`);
|
|
472
|
+
console.log(`Plugin: ${mcp.pluginName} (${mcp.pluginId})`);
|
|
425
473
|
if (mcp.package) console.log(`Package: ${mcp.package}`);
|
|
426
474
|
console.log(`\n${mcp.description}`);
|
|
427
475
|
if (mcp.setupUrl) console.log(`\nSetup: ${mcp.setupUrl}`);
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/mcp-platform-config.mjs —
|
|
2
|
+
* lib/mcp-platform-config.mjs — Resolve MCP server config paths for Claude and OpenCode.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Provides platform-aware path resolution for ~/.claude/claude_desktop_config.json
|
|
5
|
+
* and the OpenCode settings file, abstracting OS and install-location differences.
|
|
6
|
+
* Backs mcp-manager and setup when they read and write MCP server registrations.
|
|
5
7
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
8
|
+
import { dirname, resolve } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
+
|
|
13
|
+
export function getOpenCodeMcpId(id) {
|
|
14
|
+
return id;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
function resolveTemplateString(value, resolvedValues) {
|
|
@@ -68,36 +71,33 @@ export function buildClaudeMcpEntry(id, mcpDef, resolvedValues = {}) {
|
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
export function buildOpenCodeMcpEntry(id, mcpDef, resolvedValues = {}) {
|
|
74
|
+
const runtimeValues = {
|
|
75
|
+
CX_TOOLKIT_DIR: ROOT_DIR,
|
|
76
|
+
...resolvedValues,
|
|
77
|
+
};
|
|
71
78
|
const openCodeId = getOpenCodeMcpId(id);
|
|
72
79
|
|
|
73
|
-
if (id === "memory") {
|
|
74
|
-
return {
|
|
75
|
-
id: openCodeId,
|
|
76
|
-
entry: {
|
|
77
|
-
type: "local",
|
|
78
|
-
command: ["npx", "-y", "@modelcontextprotocol/server-memory@latest"],
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
80
|
if (mcpDef.type === "url") {
|
|
84
|
-
const
|
|
81
|
+
const url =
|
|
82
|
+
id === "memory" && runtimeValues.MEMORY_PORT
|
|
83
|
+
? `http://127.0.0.1:${runtimeValues.MEMORY_PORT}/`
|
|
84
|
+
: resolveTemplateString(mcpDef.url, runtimeValues);
|
|
85
85
|
return {
|
|
86
86
|
id: openCodeId,
|
|
87
87
|
entry: {
|
|
88
88
|
type: "remote",
|
|
89
|
-
url
|
|
90
|
-
...(Object.keys(
|
|
89
|
+
url,
|
|
90
|
+
...(Object.keys(buildRemoteHeaders(mcpDef, runtimeValues)).length > 0 ? { headers: buildRemoteHeaders(mcpDef, runtimeValues) } : {}),
|
|
91
91
|
},
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
const environment = buildLocalEnvironment(mcpDef,
|
|
95
|
+
const environment = buildLocalEnvironment(mcpDef, runtimeValues);
|
|
96
96
|
return {
|
|
97
97
|
id: openCodeId,
|
|
98
98
|
entry: {
|
|
99
99
|
type: "local",
|
|
100
|
-
command: [mcpDef.command, ...resolveArgs(mcpDef.args,
|
|
100
|
+
command: [mcpDef.command, ...resolveArgs(mcpDef.args, runtimeValues)],
|
|
101
101
|
...(Object.keys(environment).length > 0 ? { environment } : {}),
|
|
102
102
|
},
|
|
103
103
|
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/memory-stats.mjs — memory layer usage statistics and JSONL snapshot appender.
|
|
3
|
+
*
|
|
4
|
+
* Reads .cx/memory-stats.jsonl (one JSON line per session) and computes aggregates:
|
|
5
|
+
* queries/session, avg observations injected, p95 retrieval latency, hit rate.
|
|
6
|
+
* appendSessionStats() is called at session end to record the current session.
|
|
7
|
+
*
|
|
8
|
+
* Stats are approximated from session-start injection counts and search call counts
|
|
9
|
+
* recorded by the observation store instrumentation. CONSTRUCT_MEMORY=off skips injection
|
|
10
|
+
* but still records a zero-hit entry so A/B comparisons are possible.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
const STATS_FILE = '.cx/memory-stats.jsonl';
|
|
17
|
+
|
|
18
|
+
function statsPath(rootDir) {
|
|
19
|
+
return path.join(rootDir, STATS_FILE);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ensureDir(p) {
|
|
23
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readLines(rootDir) {
|
|
27
|
+
const p = statsPath(rootDir);
|
|
28
|
+
if (!fs.existsSync(p)) return [];
|
|
29
|
+
return fs.readFileSync(p, 'utf8')
|
|
30
|
+
.split('\n')
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.map((line) => { try { return JSON.parse(line); } catch { return null; } })
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Append one session's memory stats to the JSONL log.
|
|
38
|
+
* Fields: sessionId, project, at, queriesIssued, observationsInjected,
|
|
39
|
+
* retrievalMs (p50 estimate), memoryEnabled.
|
|
40
|
+
*/
|
|
41
|
+
export function appendSessionStats(rootDir, {
|
|
42
|
+
sessionId = null,
|
|
43
|
+
project = null,
|
|
44
|
+
queriesIssued = 0,
|
|
45
|
+
observationsInjected = 0,
|
|
46
|
+
retrievalMs = null,
|
|
47
|
+
memoryEnabled = true,
|
|
48
|
+
} = {}) {
|
|
49
|
+
const entry = {
|
|
50
|
+
sessionId,
|
|
51
|
+
project,
|
|
52
|
+
at: new Date().toISOString(),
|
|
53
|
+
queriesIssued,
|
|
54
|
+
observationsInjected,
|
|
55
|
+
retrievalMs,
|
|
56
|
+
memoryEnabled,
|
|
57
|
+
};
|
|
58
|
+
const p = statsPath(rootDir);
|
|
59
|
+
ensureDir(p);
|
|
60
|
+
fs.appendFileSync(p, JSON.stringify(entry) + '\n');
|
|
61
|
+
return entry;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Compute aggregate stats from the JSONL log.
|
|
66
|
+
* Returns { sessions, avgQueriesPerSession, avgInjectedPerSession,
|
|
67
|
+
* p95RetrievalMs, hitRate, memoryEnabledPct }.
|
|
68
|
+
*/
|
|
69
|
+
export function computeStats(rootDir, { project = null, lastN = 50 } = {}) {
|
|
70
|
+
let lines = readLines(rootDir);
|
|
71
|
+
if (project) lines = lines.filter((l) => l.project === project);
|
|
72
|
+
lines = lines.slice(-lastN);
|
|
73
|
+
|
|
74
|
+
if (!lines.length) {
|
|
75
|
+
return { sessions: 0, avgQueriesPerSession: 0, avgInjectedPerSession: 0, p95RetrievalMs: null, hitRate: 0, memoryEnabledPct: 100 };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const sessions = lines.length;
|
|
79
|
+
const totalQueries = lines.reduce((s, l) => s + (l.queriesIssued || 0), 0);
|
|
80
|
+
const totalInjected = lines.reduce((s, l) => s + (l.observationsInjected || 0), 0);
|
|
81
|
+
const enabledCount = lines.filter((l) => l.memoryEnabled !== false).length;
|
|
82
|
+
|
|
83
|
+
const latencies = lines
|
|
84
|
+
.map((l) => l.retrievalMs)
|
|
85
|
+
.filter((v) => typeof v === 'number' && v > 0)
|
|
86
|
+
.sort((a, b) => a - b);
|
|
87
|
+
|
|
88
|
+
const p95RetrievalMs = latencies.length
|
|
89
|
+
? latencies[Math.floor(latencies.length * 0.95)]
|
|
90
|
+
: null;
|
|
91
|
+
|
|
92
|
+
const sessionsWithHits = lines.filter((l) => (l.observationsInjected || 0) > 0).length;
|
|
93
|
+
const hitRate = sessions > 0 ? Math.round((sessionsWithHits / sessions) * 100) : 0;
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
sessions,
|
|
97
|
+
avgQueriesPerSession: Math.round((totalQueries / sessions) * 10) / 10,
|
|
98
|
+
avgInjectedPerSession: Math.round((totalInjected / sessions) * 10) / 10,
|
|
99
|
+
p95RetrievalMs,
|
|
100
|
+
hitRate,
|
|
101
|
+
memoryEnabledPct: Math.round((enabledCount / sessions) * 100),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Format stats for terminal display.
|
|
107
|
+
*/
|
|
108
|
+
export function formatStats(stats) {
|
|
109
|
+
if (stats.sessions === 0) {
|
|
110
|
+
return ' No memory stats recorded yet. Run a few sessions and try again.\n';
|
|
111
|
+
}
|
|
112
|
+
const lines = [
|
|
113
|
+
` Sessions tracked: ${stats.sessions}`,
|
|
114
|
+
` Avg queries/session: ${stats.avgQueriesPerSession}`,
|
|
115
|
+
` Avg injected/session: ${stats.avgInjectedPerSession}`,
|
|
116
|
+
` Hit rate: ${stats.hitRate}%`,
|
|
117
|
+
` p95 retrieval latency: ${stats.p95RetrievalMs != null ? `${stats.p95RetrievalMs}ms` : 'n/a'}`,
|
|
118
|
+
` Memory enabled: ${stats.memoryEnabledPct}% of sessions`,
|
|
119
|
+
];
|
|
120
|
+
return lines.join('\n') + '\n';
|
|
121
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/mode-commands.mjs — Standardized command contracts across deployment modes.
|
|
3
|
+
*
|
|
4
|
+
* Provides mode-aware command execution for intake, memory, and workflow operations.
|
|
5
|
+
* Routes to file-based implementations in solo mode, Postgres in team/enterprise mode.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// lib/mode-commands.mjs
|
|
9
|
+
// Standardized command contracts across deployment modes
|
|
10
|
+
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
|
|
13
|
+
export const MODE_COMMANDS = {
|
|
14
|
+
// Intake queue operations
|
|
15
|
+
intake: {
|
|
16
|
+
list: async (options = {}) => {
|
|
17
|
+
const mode = getDeploymentMode();
|
|
18
|
+
|
|
19
|
+
if (mode === 'solo') {
|
|
20
|
+
// File-based intake
|
|
21
|
+
const result = spawnSync('find', ['.cx/intake/pending', '-name', '*.json', '-type', 'f'],
|
|
22
|
+
{ encoding: 'utf8', cwd: options.cwd });
|
|
23
|
+
return parseIntakeFiles(result.stdout);
|
|
24
|
+
} else {
|
|
25
|
+
// Postgres-based intake
|
|
26
|
+
return queryPostgresIntake(options);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
show: async (id, options = {}) => {
|
|
31
|
+
const mode = getDeploymentMode();
|
|
32
|
+
|
|
33
|
+
if (mode === 'solo') {
|
|
34
|
+
return readIntakeFile(id, options);
|
|
35
|
+
} else {
|
|
36
|
+
return queryPostgresIntakeItem(id, options);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Storage operations
|
|
42
|
+
storage: {
|
|
43
|
+
sync: async (options = {}) => {
|
|
44
|
+
const mode = getDeploymentMode();
|
|
45
|
+
|
|
46
|
+
// Same interface, different implementation
|
|
47
|
+
if (mode === 'solo') {
|
|
48
|
+
return syncToFileStorage(options);
|
|
49
|
+
} else {
|
|
50
|
+
return syncToPostgresStorage(options);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
query: async (criteria, options = {}) => {
|
|
55
|
+
// Unified query interface
|
|
56
|
+
return queryStorage(criteria, options);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function getDeploymentMode() {
|
|
62
|
+
return process.env.CONSTRUCT_DEPLOYMENT_MODE || 'solo';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseIntakeFiles(stdout) {
|
|
66
|
+
return stdout.split('\n')
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.map(f => ({ id: path.basename(f, '.json'), source: 'file' }));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readIntakeFile(id, options) {
|
|
72
|
+
const fs = require('fs');
|
|
73
|
+
const path = require('path');
|
|
74
|
+
const filePath = path.join(options.cwd || process.cwd(), '.cx/intake/pending', `${id}.json`);
|
|
75
|
+
|
|
76
|
+
if (!fs.existsSync(filePath)) {
|
|
77
|
+
return { error: `Intake item ${id} not found` };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function queryPostgresIntake(options) {
|
|
84
|
+
// Would use SQL client
|
|
85
|
+
return { items: [], mode: 'postgres' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function queryPostgresIntakeItem(id, options) {
|
|
89
|
+
return { id, mode: 'postgres' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function syncToFileStorage(options) {
|
|
93
|
+
return {
|
|
94
|
+
success: true,
|
|
95
|
+
mode: 'solo',
|
|
96
|
+
backend: 'file',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function syncToPostgresStorage(options) {
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
mode: 'team',
|
|
104
|
+
backend: 'postgres',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function queryStorage(criteria, options) {
|
|
109
|
+
// Unified query - works across modes
|
|
110
|
+
return {
|
|
111
|
+
results: [],
|
|
112
|
+
mode: getDeploymentMode(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getCommandInterface(command, operation) {
|
|
117
|
+
const cmd = MODE_COMMANDS[command];
|
|
118
|
+
if (!cmd) return null;
|
|
119
|
+
|
|
120
|
+
const fn = cmd[operation];
|
|
121
|
+
if (!fn) return null;
|
|
122
|
+
|
|
123
|
+
return fn;
|
|
124
|
+
}
|