@geraldmaron/construct 1.0.0 → 1.0.2
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 +9 -6
- 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 +1000 -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 +523 -36
- 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
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/ollama-manager.mjs — Ollama local LLM management.
|
|
4
|
+
*
|
|
5
|
+
* Provides commands for checking Ollama status, listing models,
|
|
6
|
+
* pulling new models, and configuring Ollama as a provider.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
|
|
13
|
+
const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if Ollama is installed and running
|
|
17
|
+
*/
|
|
18
|
+
export function checkOllamaStatus() {
|
|
19
|
+
try {
|
|
20
|
+
execSync('which ollama', { stdio: 'pipe' });
|
|
21
|
+
const installed = true;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const response = execSync(`curl -s ${OLLAMA_BASE_URL}/api/tags`, {
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
timeout: 3000
|
|
27
|
+
});
|
|
28
|
+
const data = JSON.parse(response);
|
|
29
|
+
return {
|
|
30
|
+
installed,
|
|
31
|
+
running: true,
|
|
32
|
+
models: data.models || [],
|
|
33
|
+
baseUrl: OLLAMA_BASE_URL
|
|
34
|
+
};
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return {
|
|
37
|
+
installed,
|
|
38
|
+
running: false,
|
|
39
|
+
error: 'Ollama server not responding',
|
|
40
|
+
baseUrl: OLLAMA_BASE_URL
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return {
|
|
45
|
+
installed: false,
|
|
46
|
+
running: false,
|
|
47
|
+
error: 'Ollama not installed'
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* List available Ollama models
|
|
54
|
+
*/
|
|
55
|
+
export function listModels() {
|
|
56
|
+
try {
|
|
57
|
+
const response = execSync(`curl -s ${OLLAMA_BASE_URL}/api/tags`, {
|
|
58
|
+
encoding: 'utf8',
|
|
59
|
+
timeout: 5000
|
|
60
|
+
});
|
|
61
|
+
const data = JSON.parse(response);
|
|
62
|
+
return data.models || [];
|
|
63
|
+
} catch (e) {
|
|
64
|
+
throw new Error(`Failed to fetch models: ${e.message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pull a model from Ollama registry
|
|
70
|
+
*/
|
|
71
|
+
export function pullModel(modelName, { verbose = false } = {}) {
|
|
72
|
+
console.log(`Pulling ${modelName}...`);
|
|
73
|
+
try {
|
|
74
|
+
const output = execSync(`ollama pull ${modelName}`, {
|
|
75
|
+
encoding: 'utf8',
|
|
76
|
+
stdio: verbose ? 'inherit' : 'pipe'
|
|
77
|
+
});
|
|
78
|
+
return { success: true, output };
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return { success: false, error: e.message };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Remove a model
|
|
86
|
+
*/
|
|
87
|
+
export function removeModel(modelName) {
|
|
88
|
+
try {
|
|
89
|
+
execSync(`ollama rm ${modelName}`, { encoding: 'utf8' });
|
|
90
|
+
return { success: true };
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return { success: false, error: e.message };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Show model details
|
|
98
|
+
*/
|
|
99
|
+
export function showModel(modelName) {
|
|
100
|
+
try {
|
|
101
|
+
const response = execSync(`curl -s ${OLLAMA_BASE_URL}/api/show -d '{"name":"${modelName}"}'`, {
|
|
102
|
+
encoding: 'utf8',
|
|
103
|
+
timeout: 5000
|
|
104
|
+
});
|
|
105
|
+
return JSON.parse(response);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
throw new Error(`Failed to fetch model details: ${e.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Run a quick inference test
|
|
113
|
+
*/
|
|
114
|
+
export function testModel(modelName, prompt = 'Say hello in one sentence.') {
|
|
115
|
+
try {
|
|
116
|
+
const response = execSync(`curl -s ${OLLAMA_BASE_URL}/api/generate -d '${JSON.stringify({
|
|
117
|
+
model: modelName,
|
|
118
|
+
prompt: prompt,
|
|
119
|
+
stream: false
|
|
120
|
+
})}'`, {
|
|
121
|
+
encoding: 'utf8',
|
|
122
|
+
timeout: 30000
|
|
123
|
+
});
|
|
124
|
+
const data = JSON.parse(response);
|
|
125
|
+
return {
|
|
126
|
+
success: true,
|
|
127
|
+
response: data.response,
|
|
128
|
+
duration: data.total_duration ? `${(data.total_duration / 1e9).toFixed(2)}s` : 'unknown'
|
|
129
|
+
};
|
|
130
|
+
} catch (e) {
|
|
131
|
+
return { success: false, error: e.message };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* CLI entry point
|
|
137
|
+
*/
|
|
138
|
+
export async function cmdOllama(argv = process.argv.slice(2)) {
|
|
139
|
+
const command = argv[0];
|
|
140
|
+
const args = argv.slice(1);
|
|
141
|
+
|
|
142
|
+
if (!command || command === '--help' || command === '-h') {
|
|
143
|
+
printHelp();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
switch (command) {
|
|
148
|
+
case 'status':
|
|
149
|
+
await cmdOllamaStatus();
|
|
150
|
+
break;
|
|
151
|
+
case 'list':
|
|
152
|
+
case 'ls':
|
|
153
|
+
await cmdOllamaList(args);
|
|
154
|
+
break;
|
|
155
|
+
case 'pull':
|
|
156
|
+
await cmdOllamaPull(args);
|
|
157
|
+
break;
|
|
158
|
+
case 'rm':
|
|
159
|
+
case 'remove':
|
|
160
|
+
await cmdOllamaRemove(args);
|
|
161
|
+
break;
|
|
162
|
+
case 'show':
|
|
163
|
+
await cmdOllamaShow(args);
|
|
164
|
+
break;
|
|
165
|
+
case 'test':
|
|
166
|
+
await cmdOllamaTest(args);
|
|
167
|
+
break;
|
|
168
|
+
case 'setup':
|
|
169
|
+
await cmdOllamaSetup(args);
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
console.error(`Unknown command: ${command}`);
|
|
173
|
+
printHelp();
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function printHelp() {
|
|
179
|
+
console.log(`
|
|
180
|
+
Usage: construct ollama <command> [options]
|
|
181
|
+
|
|
182
|
+
Commands:
|
|
183
|
+
status Check if Ollama is installed and running
|
|
184
|
+
list, ls List available local models
|
|
185
|
+
pull <model> Pull a model from Ollama registry
|
|
186
|
+
rm <model> Remove a local model
|
|
187
|
+
show <model> Show model details (architecture, parameters)
|
|
188
|
+
test <model> Run a quick inference test
|
|
189
|
+
setup Interactive setup: install, start service, pull default model
|
|
190
|
+
|
|
191
|
+
Options:
|
|
192
|
+
--verbose Show detailed output (for pull)
|
|
193
|
+
--help, -h Show this help message
|
|
194
|
+
|
|
195
|
+
Examples:
|
|
196
|
+
construct ollama status
|
|
197
|
+
construct ollama list
|
|
198
|
+
construct ollama pull llama3.2
|
|
199
|
+
construct ollama test llama3.2
|
|
200
|
+
construct ollama setup --yes
|
|
201
|
+
`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function cmdOllamaStatus() {
|
|
205
|
+
const status = checkOllamaStatus();
|
|
206
|
+
|
|
207
|
+
if (!status.installed) {
|
|
208
|
+
console.log('❌ Ollama is not installed');
|
|
209
|
+
console.log(' Install with: brew install ollama');
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!status.running) {
|
|
214
|
+
console.log('⚠️ Ollama is installed but not running');
|
|
215
|
+
console.log(' Start with: brew services start ollama');
|
|
216
|
+
console.log(` Base URL: ${status.baseUrl}`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log('✅ Ollama is running');
|
|
221
|
+
console.log(` Base URL: ${status.baseUrl}`);
|
|
222
|
+
console.log(` Models: ${status.models.length}`);
|
|
223
|
+
|
|
224
|
+
if (status.models.length > 0) {
|
|
225
|
+
console.log('\n Available models:');
|
|
226
|
+
status.models.forEach(m => {
|
|
227
|
+
console.log(` - ${m.name} (${formatSize(m.size)})`);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function cmdOllamaList(args) {
|
|
233
|
+
const status = checkOllamaStatus();
|
|
234
|
+
if (!status.running) {
|
|
235
|
+
console.error('Error: Ollama is not running. Start with: brew services start ollama');
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const models = listModels();
|
|
240
|
+
if (models.length === 0) {
|
|
241
|
+
console.log('No models installed. Pull one with: construct ollama pull <model>');
|
|
242
|
+
console.log('\nPopular models:');
|
|
243
|
+
console.log(' - llama3.2 (3B, fast)');
|
|
244
|
+
console.log(' - llama3.1:8b (8B, balanced)');
|
|
245
|
+
console.log(' - llama3.1:70b (70B, reasoning)');
|
|
246
|
+
console.log(' - qwen2.5:7b (7B, balanced)');
|
|
247
|
+
console.log(' - mistral:7b (7B, fast)');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log('NAME'.padEnd(25), 'SIZE'.padEnd(12), 'MODIFIED');
|
|
252
|
+
console.log('─'.repeat(50));
|
|
253
|
+
models.forEach(m => {
|
|
254
|
+
const name = m.name.padEnd(25);
|
|
255
|
+
const size = formatSize(m.size).padEnd(12);
|
|
256
|
+
const modified = new Date(m.modified_at).toLocaleDateString();
|
|
257
|
+
console.log(name, size, modified);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function cmdOllamaPull(args) {
|
|
262
|
+
const modelName = args[0];
|
|
263
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
264
|
+
|
|
265
|
+
if (!modelName) {
|
|
266
|
+
console.error('Error: Model name required');
|
|
267
|
+
console.log('Usage: construct ollama pull <model>');
|
|
268
|
+
console.log('\nPopular models:');
|
|
269
|
+
console.log(' - llama3.2 (3B, fast)');
|
|
270
|
+
console.log(' - llama3.1:8b (8B, balanced)');
|
|
271
|
+
console.log(' - llama3.1:70b (70B, reasoning)');
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const result = pullModel(modelName, { verbose });
|
|
276
|
+
if (result.success) {
|
|
277
|
+
console.log(`✅ Successfully pulled ${modelName}`);
|
|
278
|
+
} else {
|
|
279
|
+
console.error(`❌ Failed to pull ${modelName}: ${result.error}`);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function cmdOllamaRemove(args) {
|
|
285
|
+
const modelName = args[0];
|
|
286
|
+
|
|
287
|
+
if (!modelName) {
|
|
288
|
+
console.error('Error: Model name required');
|
|
289
|
+
console.log('Usage: construct ollama rm <model>');
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const result = removeModel(modelName);
|
|
294
|
+
if (result.success) {
|
|
295
|
+
console.log(`✅ Removed ${modelName}`);
|
|
296
|
+
} else {
|
|
297
|
+
console.error(`❌ Failed to remove ${modelName}: ${result.error}`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function cmdOllamaShow(args) {
|
|
303
|
+
const modelName = args[0];
|
|
304
|
+
|
|
305
|
+
if (!modelName) {
|
|
306
|
+
console.error('Error: Model name required');
|
|
307
|
+
console.log('Usage: construct ollama show <model>');
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const details = showModel(modelName);
|
|
313
|
+
console.log(`Model: ${modelName}`);
|
|
314
|
+
console.log('─'.repeat(50));
|
|
315
|
+
|
|
316
|
+
if (details.details) {
|
|
317
|
+
console.log(` Format: ${details.details.format || 'unknown'}`);
|
|
318
|
+
console.log(` Family: ${details.details.family || 'unknown'}`);
|
|
319
|
+
console.log(` Parameters: ${details.details.parameter_size || 'unknown'}`);
|
|
320
|
+
console.log(` Quantization: ${details.details.quantization_level || 'unknown'}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (details.model_info) {
|
|
324
|
+
const keys = Object.keys(details.model_info);
|
|
325
|
+
if (keys.length > 0) {
|
|
326
|
+
console.log('\n Info:');
|
|
327
|
+
keys.slice(0, 5).forEach(key => {
|
|
328
|
+
console.log(` ${key}: ${details.model_info[key]}`);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} catch (e) {
|
|
333
|
+
console.error(e.message);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function cmdOllamaTest(args) {
|
|
339
|
+
const modelName = args[0];
|
|
340
|
+
|
|
341
|
+
if (!modelName) {
|
|
342
|
+
console.error('Error: Model name required');
|
|
343
|
+
console.log('Usage: construct ollama test <model>');
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
console.log(`Testing ${modelName}...`);
|
|
348
|
+
const result = testModel(modelName);
|
|
349
|
+
|
|
350
|
+
if (result.success) {
|
|
351
|
+
console.log(`✅ Response (${result.duration}):`);
|
|
352
|
+
console.log('─'.repeat(50));
|
|
353
|
+
console.log(result.response);
|
|
354
|
+
} else {
|
|
355
|
+
console.error(`❌ Test failed: ${result.error}`);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function cmdOllamaSetup(args) {
|
|
361
|
+
const autoYes = args.includes('--yes') || args.includes('-y');
|
|
362
|
+
|
|
363
|
+
console.log('Ollama Setup');
|
|
364
|
+
console.log('─'.repeat(50));
|
|
365
|
+
|
|
366
|
+
// Check installation
|
|
367
|
+
const status = checkOllamaStatus();
|
|
368
|
+
|
|
369
|
+
if (!status.installed) {
|
|
370
|
+
console.log('❌ Ollama is not installed');
|
|
371
|
+
if (autoYes || process.platform === 'darwin') {
|
|
372
|
+
console.log('Installing via Homebrew...');
|
|
373
|
+
try {
|
|
374
|
+
execSync('brew install ollama', { stdio: 'inherit' });
|
|
375
|
+
console.log('✅ Ollama installed');
|
|
376
|
+
} catch (e) {
|
|
377
|
+
console.error('❌ Installation failed. Please install manually: https://ollama.com');
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
console.log('Install with: brew install ollama');
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
console.log('✅ Ollama is installed');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Check if running
|
|
389
|
+
if (!status.running) {
|
|
390
|
+
console.log('⚠️ Ollama is not running');
|
|
391
|
+
console.log('Starting Ollama service...');
|
|
392
|
+
try {
|
|
393
|
+
execSync('brew services start ollama', { stdio: 'pipe' });
|
|
394
|
+
console.log('✅ Ollama service started');
|
|
395
|
+
// Wait for it to be ready
|
|
396
|
+
console.log('Waiting for Ollama to be ready...');
|
|
397
|
+
execSync('sleep 3');
|
|
398
|
+
} catch (e) {
|
|
399
|
+
console.log('You can start it manually with: ollama serve');
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
console.log('✅ Ollama is running');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Pull default model
|
|
406
|
+
console.log('\nPulling default model (llama3.2)...');
|
|
407
|
+
const pullResult = pullModel('llama3.2', { verbose: false });
|
|
408
|
+
if (pullResult.success) {
|
|
409
|
+
console.log('✅ llama3.2 ready');
|
|
410
|
+
} else {
|
|
411
|
+
console.log('⚠️ Could not pull llama3.2, you can pull it later with: construct ollama pull llama3.2');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
console.log('\n─'.repeat(50));
|
|
415
|
+
console.log('Setup complete!');
|
|
416
|
+
console.log('\nUsage:');
|
|
417
|
+
console.log(' construct ollama list # List available models');
|
|
418
|
+
console.log(' construct ollama test llama3.2 # Test a model');
|
|
419
|
+
console.log(' construct models --set=ollama/llama3.2 # Set as default');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function formatSize(bytes) {
|
|
423
|
+
if (!bytes) return 'unknown';
|
|
424
|
+
const gb = bytes / (1024 * 1024 * 1024);
|
|
425
|
+
if (gb >= 1) return `${gb.toFixed(1)} GB`;
|
|
426
|
+
const mb = bytes / (1024 * 1024);
|
|
427
|
+
return `${mb.toFixed(0)} MB`;
|
|
428
|
+
}
|
package/lib/opencode-config.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/opencode-config.mjs —
|
|
2
|
+
* lib/opencode-config.mjs — Read and write the OpenCode settings.json config file.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Locates the active OpenCode config across standard install paths, parses it,
|
|
5
|
+
* and provides typed helpers to read provider settings, MCP registrations, and
|
|
6
|
+
* model assignments. Changes are written atomically. Backs model-router,
|
|
7
|
+
* mcp-manager, and the runtime plugin.
|
|
5
8
|
*/
|
|
6
9
|
import fs from "node:fs";
|
|
7
10
|
import path from "node:path";
|
|
@@ -36,9 +39,16 @@ export function readOpenCodeConfig() {
|
|
|
36
39
|
};
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
export function sanitizeOpenCodeConfig(config) {
|
|
43
|
+
if (!config || Array.isArray(config) || typeof config !== "object") return config;
|
|
44
|
+
const sanitized = { ...config };
|
|
45
|
+
delete sanitized.construct;
|
|
46
|
+
return sanitized;
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
export function writeOpenCodeConfig(config, file = findOpenCodeConfigPath()) {
|
|
40
50
|
const canonical = getCanonicalOpenCodeConfigPath();
|
|
41
51
|
fs.mkdirSync(path.dirname(canonical), { recursive: true });
|
|
42
|
-
fs.writeFileSync(canonical, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
52
|
+
fs.writeFileSync(canonical, `${JSON.stringify(sanitizeOpenCodeConfig(config), null, 2)}\n`, "utf8");
|
|
43
53
|
return canonical;
|
|
44
54
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/opencode-runtime-plugin.mjs — OpenCode plugin:
|
|
2
|
+
* lib/opencode-runtime-plugin.mjs — OpenCode plugin: Telemetry + model fallback.
|
|
3
3
|
*
|
|
4
4
|
* Intercepts OpenCode session events (session.created, session.idle, session.error),
|
|
5
|
-
* records each as a
|
|
5
|
+
* records each as a telemetry trace, and applies model fallback on rate-limit errors.
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
@@ -11,7 +11,7 @@ import { spawnSync } from "node:child_process";
|
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
12
|
import { findOpenCodeConfigPath } from "./opencode-config.mjs";
|
|
13
13
|
import { classifyProviderFailure, readCurrentModels, resolveExecutionContractModelMetadata, resolveFallbackAction, selectModelTierForWorkCategory } from "./model-router.mjs";
|
|
14
|
-
import { resolveRuntimePromptMetadata } from "./prompt-composer.
|
|
14
|
+
import { resolveRuntimePromptMetadata } from "./prompt-composer.js";
|
|
15
15
|
import { enrichMetadataWithPrompt } from "./prompt-metadata.mjs";
|
|
16
16
|
import { loadToolkitEnv } from "./toolkit-env.mjs";
|
|
17
17
|
import { routeRequest } from "./orchestration-policy.mjs";
|
|
@@ -25,8 +25,9 @@ import {
|
|
|
25
25
|
onCommandBefore,
|
|
26
26
|
getIngestClient,
|
|
27
27
|
} from "./opencode-telemetry.mjs";
|
|
28
|
-
import { syncModelPricing } from "./telemetry/
|
|
29
|
-
import { estimateUsageCost } from "./telemetry/
|
|
28
|
+
import { syncModelPricing, refreshPricingCatalog } from "./telemetry/model-pricing-catalog.mjs";
|
|
29
|
+
import { estimateUsageCost } from "./telemetry/model-pricing-catalog.mjs";
|
|
30
|
+
import { buildSessionPrelude } from "./intake/session-prelude.mjs";
|
|
30
31
|
|
|
31
32
|
const EFFICIENCY_SESSION_IDLE_RESET_MS = 2 * 60 * 60 * 1000;
|
|
32
33
|
const EFFICIENCY_REPEATED_READ_THRESHOLD = 5;
|
|
@@ -63,6 +64,32 @@ const RUNTIME_TRACE_EVENTS = new Set([
|
|
|
63
64
|
"message.updated",
|
|
64
65
|
]);
|
|
65
66
|
|
|
67
|
+
// One-shot prelude emission per session id — kept in module scope so the
|
|
68
|
+
// prelude surfaces on session.created and not on every subsequent event
|
|
69
|
+
// that carries the same session id.
|
|
70
|
+
const preludeEmittedSessions = new Set();
|
|
71
|
+
|
|
72
|
+
export function _resetPreludeForTests() {
|
|
73
|
+
preludeEmittedSessions.clear();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function emitSessionPrelude(event, { client, env = process.env, cwd } = {}) {
|
|
77
|
+
if (event?.type !== "session.created") return false;
|
|
78
|
+
const sessionId = getSessionId(event) || "anonymous";
|
|
79
|
+
if (preludeEmittedSessions.has(sessionId)) return false;
|
|
80
|
+
preludeEmittedSessions.add(sessionId);
|
|
81
|
+
const prelude = buildSessionPrelude({ cwd: cwd || env.CX_TOOLKIT_DIR || process.cwd(), env });
|
|
82
|
+
if (!prelude) return false;
|
|
83
|
+
await client?.app?.log?.({
|
|
84
|
+
body: {
|
|
85
|
+
service: "construct",
|
|
86
|
+
level: "info",
|
|
87
|
+
message: prelude,
|
|
88
|
+
},
|
|
89
|
+
}).catch(() => {});
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
66
93
|
const COOLDOWN_MS = 10 * 60 * 1000;
|
|
67
94
|
|
|
68
95
|
function getStatePath(env = process.env) {
|
|
@@ -179,12 +206,25 @@ function sanitizeUsage(event) {
|
|
|
179
206
|
legacyUsage?.cacheReadInputTokens,
|
|
180
207
|
legacyUsage?.prompt_tokens_details?.cached_tokens,
|
|
181
208
|
);
|
|
209
|
+
const cacheCreation5mInputTokens = positiveNumber(
|
|
210
|
+
infoTokens?.cache?.write_5m,
|
|
211
|
+
infoTokens?.cache?.create_5m,
|
|
212
|
+
legacyUsage?.cache_creation?.ephemeral_5m_input_tokens,
|
|
213
|
+
legacyUsage?.cache_creation_5m_input_tokens,
|
|
214
|
+
legacyUsage?.cacheCreation5mInputTokens,
|
|
215
|
+
);
|
|
216
|
+
const cacheCreation1hInputTokens = positiveNumber(
|
|
217
|
+
infoTokens?.cache?.write_1h,
|
|
218
|
+
infoTokens?.cache?.create_1h,
|
|
219
|
+
legacyUsage?.cache_creation?.ephemeral_1h_input_tokens,
|
|
220
|
+
legacyUsage?.cache_creation_1h_input_tokens,
|
|
221
|
+
legacyUsage?.cacheCreation1hInputTokens,
|
|
222
|
+
);
|
|
182
223
|
const cacheCreationInputTokens = positiveNumber(
|
|
183
224
|
infoTokens?.cache?.write,
|
|
184
225
|
legacyUsage?.cache_creation_input_tokens,
|
|
185
226
|
legacyUsage?.cacheCreationInputTokens,
|
|
186
|
-
|
|
187
|
-
(legacyUsage?.cache_creation?.ephemeral_1h_input_tokens ?? 0),
|
|
227
|
+
cacheCreation5mInputTokens + cacheCreation1hInputTokens,
|
|
188
228
|
);
|
|
189
229
|
const totalTokens = positiveNumber(
|
|
190
230
|
legacyUsage?.total_tokens,
|
|
@@ -201,6 +241,8 @@ function sanitizeUsage(event) {
|
|
|
201
241
|
totalTokens,
|
|
202
242
|
...(cacheReadInputTokens ? { cacheReadInputTokens } : {}),
|
|
203
243
|
...(cacheCreationInputTokens ? { cacheCreationInputTokens } : {}),
|
|
244
|
+
...(cacheCreation5mInputTokens ? { cacheCreation5mInputTokens } : {}),
|
|
245
|
+
...(cacheCreation1hInputTokens ? { cacheCreation1hInputTokens } : {}),
|
|
204
246
|
...(reasoningTokens ? { reasoningTokens } : {}),
|
|
205
247
|
...(cost ? { costUsd: cost, costSource: "event" } : {}),
|
|
206
248
|
};
|
|
@@ -367,7 +409,7 @@ function buildHostConstraints() {
|
|
|
367
409
|
return {
|
|
368
410
|
runtime: "opencode",
|
|
369
411
|
providerAgnostic: true,
|
|
370
|
-
telemetryBackend: "
|
|
412
|
+
telemetryBackend: "telemetry",
|
|
371
413
|
};
|
|
372
414
|
}
|
|
373
415
|
|
|
@@ -426,12 +468,12 @@ export function buildRuntimeTracePayload(event, { env = process.env } = {}) {
|
|
|
426
468
|
eventType: event.type,
|
|
427
469
|
agent: agent || undefined,
|
|
428
470
|
status: stripPlaceholder(event?.status || event?.session?.status || event?.data?.status),
|
|
471
|
+
...runtimePromptMetadata,
|
|
429
472
|
...usage,
|
|
430
473
|
...(pricing.costUsd ? { costUsd: pricing.costUsd, costSource: pricing.costSource, costModel: pricing.modelName } : { costUsd: 0, costSource: pricing.costSource, costModel: pricing.modelName }),
|
|
431
474
|
...(modelInfo.modelName ? { modelName: modelInfo.modelName } : {}),
|
|
432
475
|
...(modelInfo.provider ? { provider: modelInfo.provider } : {}),
|
|
433
476
|
...errorMeta,
|
|
434
|
-
...runtimePromptMetadata,
|
|
435
477
|
}, { rootDir });
|
|
436
478
|
|
|
437
479
|
// Extract assistant response text for message.updated events so the trace output is populated.
|
|
@@ -632,23 +674,6 @@ export function trackReadEfficiencyFromMessage(event, { env = process.env, cwd =
|
|
|
632
674
|
return { warnings };
|
|
633
675
|
}
|
|
634
676
|
|
|
635
|
-
function langfuseAvailable(env = process.env) {
|
|
636
|
-
return Boolean(env.LANGFUSE_PUBLIC_KEY && env.LANGFUSE_SECRET_KEY);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function langfuseHeaders(env = process.env) {
|
|
640
|
-
const key = env.LANGFUSE_PUBLIC_KEY;
|
|
641
|
-
const secret = env.LANGFUSE_SECRET_KEY;
|
|
642
|
-
return {
|
|
643
|
-
Authorization: `Basic ${Buffer.from(`${key}:${secret}`).toString('base64')}`,
|
|
644
|
-
'Content-Type': 'application/json',
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function langfuseBaseUrl(env = process.env) {
|
|
649
|
-
return (env.LANGFUSE_BASEURL ?? 'https://cloud.langfuse.com').replace(/\/$/, '');
|
|
650
|
-
}
|
|
651
|
-
|
|
652
677
|
async function emitRuntimeTrace(event, { client, env = process.env }) {
|
|
653
678
|
const payload = buildRuntimeTracePayload(event, { env });
|
|
654
679
|
if (!payload) return;
|
|
@@ -683,7 +708,7 @@ async function emitRuntimeTrace(event, { client, env = process.env }) {
|
|
|
683
708
|
return text || undefined;
|
|
684
709
|
})();
|
|
685
710
|
const toolCalls = extractReadToolCalls(info);
|
|
686
|
-
const
|
|
711
|
+
const telemetryUsage = usage.inputTokens ? {
|
|
687
712
|
input: usage.inputTokens,
|
|
688
713
|
output: usage.outputTokens,
|
|
689
714
|
total: usage.totalTokens,
|
|
@@ -697,7 +722,7 @@ async function emitRuntimeTrace(event, { client, env = process.env }) {
|
|
|
697
722
|
startTime: info?.time?.created ? new Date(info.time.created).toISOString() : payload.timestamp,
|
|
698
723
|
endTime: info?.time?.completed ? new Date(info.time.completed).toISOString() : new Date().toISOString(),
|
|
699
724
|
model,
|
|
700
|
-
usage:
|
|
725
|
+
usage: telemetryUsage,
|
|
701
726
|
output: {
|
|
702
727
|
kind: "assistant_generation",
|
|
703
728
|
agent: payload.metadata?.agent || undefined,
|
|
@@ -709,12 +734,12 @@ async function emitRuntimeTrace(event, { client, env = process.env }) {
|
|
|
709
734
|
costSource: pricing.costSource,
|
|
710
735
|
costModel: pricing.modelName,
|
|
711
736
|
toolCalls: toolCalls.length ? toolCalls.map((call) => ({ filePath: call.filePath, limit: call.limit, callId: call.callId })) : undefined,
|
|
712
|
-
usage:
|
|
713
|
-
input:
|
|
714
|
-
output:
|
|
715
|
-
total:
|
|
716
|
-
unit:
|
|
717
|
-
...(
|
|
737
|
+
usage: telemetryUsage ? {
|
|
738
|
+
input: telemetryUsage.input,
|
|
739
|
+
output: telemetryUsage.output,
|
|
740
|
+
total: telemetryUsage.total,
|
|
741
|
+
unit: telemetryUsage.unit,
|
|
742
|
+
...(telemetryUsage.totalCost ? { totalCost: telemetryUsage.totalCost } : {}),
|
|
718
743
|
} : undefined,
|
|
719
744
|
},
|
|
720
745
|
metadata: {
|
|
@@ -725,6 +750,9 @@ async function emitRuntimeTrace(event, { client, env = process.env }) {
|
|
|
725
750
|
costModel: pricing.modelName,
|
|
726
751
|
...(usage.reasoningTokens ? { reasoningTokens: usage.reasoningTokens } : {}),
|
|
727
752
|
...(usage.cacheReadInputTokens ? { cacheReadInputTokens: usage.cacheReadInputTokens } : {}),
|
|
753
|
+
...(usage.cacheCreationInputTokens ? { cacheCreationInputTokens: usage.cacheCreationInputTokens } : {}),
|
|
754
|
+
...(usage.cacheCreation5mInputTokens ? { cacheCreation5mInputTokens: usage.cacheCreation5mInputTokens } : {}),
|
|
755
|
+
...(usage.cacheCreation1hInputTokens ? { cacheCreation1hInputTokens: usage.cacheCreation1hInputTokens } : {}),
|
|
728
756
|
},
|
|
729
757
|
});
|
|
730
758
|
await ingest.flush();
|
|
@@ -856,11 +884,14 @@ export function createConstructOpenCodePlugin({
|
|
|
856
884
|
configPath = findOpenCodeConfigPath(),
|
|
857
885
|
} = {}) {
|
|
858
886
|
loadToolkitEnv(toolkitDir, env);
|
|
859
|
-
//
|
|
887
|
+
// Refresh local pricing catalog from LiteLLM (24h cached, no auth). Runs before
|
|
888
|
+
// Sync pricing so estimateUsageCost uses live rates from the first assistant turn.
|
|
889
|
+
void refreshPricingCatalog().catch(() => {});
|
|
890
|
+
// Fire-and-forget: sync model pricing into telemetry backend on plugin load.
|
|
860
891
|
void syncModelPricing({
|
|
861
|
-
baseUrl: env.
|
|
862
|
-
publicKey: env.
|
|
863
|
-
secretKey: env.
|
|
892
|
+
baseUrl: env.CONSTRUCT_TELEMETRY_URL,
|
|
893
|
+
publicKey: env.CONSTRUCT_TELEMETRY_PUBLIC_KEY,
|
|
894
|
+
secretKey: env.CONSTRUCT_TELEMETRY_SECRET_KEY,
|
|
864
895
|
}).catch(() => {});
|
|
865
896
|
const safe = async (fn, label, client) => {
|
|
866
897
|
try {
|
|
@@ -879,6 +910,7 @@ export function createConstructOpenCodePlugin({
|
|
|
879
910
|
event: async ({ event }) => {
|
|
880
911
|
await safe(() => onBusEvent(event, { env }), "event", client);
|
|
881
912
|
await safe(() => emitRuntimeTrace(event, { client, env }), "legacy-trace", client);
|
|
913
|
+
await safe(() => emitSessionPrelude(event, { client, env, cwd: toolkitDir }), "session-prelude", client);
|
|
882
914
|
await maybeApplyModelFallback(event, { client, env, toolkitDir, configPath });
|
|
883
915
|
try {
|
|
884
916
|
const { warnings } = trackReadEfficiencyFromMessage(event, { env, cwd: toolkitDir || process.cwd() });
|