@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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/provider-capabilities.js — Model-agnostic provider capability interface.
|
|
3
|
+
*
|
|
4
|
+
* Follows the embeddings-engine.js pattern:
|
|
5
|
+
* - This file is the model-agnostic router.
|
|
6
|
+
* - Provider-specific logic lives in provider-capabilities-*.js adapters.
|
|
7
|
+
* - All adapters return the same capability shape.
|
|
8
|
+
*
|
|
9
|
+
* Standard capability interface (all adapters implement this shape):
|
|
10
|
+
* - cacheControl: boolean
|
|
11
|
+
* - cacheMechanism: 'annotation' | 'automatic' | 'resource' | 'none'
|
|
12
|
+
* - cacheTTL: { '5m': number, '1h': number } | null (tokens, not ms)
|
|
13
|
+
* - structuredOutput: boolean
|
|
14
|
+
* - maxContextWindow: number (tokens)
|
|
15
|
+
* - tokenRatio: number (chars per token for this provider)
|
|
16
|
+
* - annotationFormat: 'anthropic' | 'google' | 'openai' | 'none'
|
|
17
|
+
* - annotationHeaders: object | null
|
|
18
|
+
*/
|
|
19
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
|
|
23
|
+
const CAPABILITY_CACHE_PATH = join(homedir(), '.cx', 'provider-capabilities.json');
|
|
24
|
+
const CAPABILITY_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
25
|
+
|
|
26
|
+
const ADAPTERS = {
|
|
27
|
+
anthropic: () => import('./provider-capabilities-anthropic.js'),
|
|
28
|
+
'anthropic-direct': () => import('./provider-capabilities-anthropic.js'),
|
|
29
|
+
google: () => import('./provider-capabilities-google.js'),
|
|
30
|
+
openai: () => import('./provider-capabilities-openai.js'),
|
|
31
|
+
deepseek: () => import('./provider-capabilities-deepseek.js'),
|
|
32
|
+
generic: () => import('./provider-capabilities-generic.js'),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function resolveAdapterKey(modelId) {
|
|
36
|
+
const id = String(modelId || '').toLowerCase();
|
|
37
|
+
if (/^anthropic\//.test(id) || /^openrouter\/anthropic\//.test(id)) return 'anthropic';
|
|
38
|
+
if (/^google\//.test(id) || /^openrouter\/google\//.test(id)) return 'google';
|
|
39
|
+
if (/^openai\//.test(id) || /^openrouter\/openai\//.test(id) || /^github-copilot\//.test(id)) return 'openai';
|
|
40
|
+
if (/^deepseek\//.test(id) || /^openrouter\/deepseek\//.test(id)) return 'deepseek';
|
|
41
|
+
return 'generic';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readCapabilityCache() {
|
|
45
|
+
try {
|
|
46
|
+
if (!existsSync(CAPABILITY_CACHE_PATH)) return {};
|
|
47
|
+
const cached = JSON.parse(readFileSync(CAPABILITY_CACHE_PATH, 'utf8'));
|
|
48
|
+
if (cached?.fetchedAt && Date.now() - cached.fetchedAt < CAPABILITY_CACHE_TTL_MS) {
|
|
49
|
+
return cached.capabilities || {};
|
|
50
|
+
}
|
|
51
|
+
} catch { /* stale or corrupt */ }
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeCapabilityCache(capabilities) {
|
|
56
|
+
try {
|
|
57
|
+
mkdirSync(join(homedir(), '.cx'), { recursive: true });
|
|
58
|
+
writeFileSync(CAPABILITY_CACHE_PATH, JSON.stringify({
|
|
59
|
+
fetchedAt: Date.now(),
|
|
60
|
+
capabilities,
|
|
61
|
+
}, null, 2));
|
|
62
|
+
} catch { /* best effort */ }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let _cache = null;
|
|
66
|
+
function getCache() {
|
|
67
|
+
if (_cache === null) _cache = readCapabilityCache();
|
|
68
|
+
return _cache;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve provider capabilities for a given model ID.
|
|
73
|
+
* Returns the standard capability interface.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} modelId - The model identifier (e.g., "anthropic/claude-opus-4-6")
|
|
76
|
+
* @returns {object} Capability object with the standard shape
|
|
77
|
+
*/
|
|
78
|
+
export async function resolveProviderCapabilities(modelId) {
|
|
79
|
+
const adapterKey = resolveAdapterKey(modelId);
|
|
80
|
+
const loader = ADAPTERS[adapterKey] || ADAPTERS.generic;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const { capabilities } = await loader();
|
|
84
|
+
return capabilities(modelId);
|
|
85
|
+
} catch {
|
|
86
|
+
// Fallback to generic
|
|
87
|
+
const { capabilities } = await ADAPTERS.generic();
|
|
88
|
+
return capabilities(modelId);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Synchronous version — uses cache if available, otherwise returns generic.
|
|
94
|
+
* Use this in hot paths where async is not possible.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} modelId
|
|
97
|
+
* @returns {object} Capability object
|
|
98
|
+
*/
|
|
99
|
+
export function resolveProviderCapabilitiesSync(modelId) {
|
|
100
|
+
const adapterKey = resolveAdapterKey(modelId);
|
|
101
|
+
const cache = getCache();
|
|
102
|
+
|
|
103
|
+
// Check cache first (keyed by adapter type, not full model ID)
|
|
104
|
+
if (cache[adapterKey]) return cache[adapterKey];
|
|
105
|
+
|
|
106
|
+
// Return generic synchronously (can't load adapter in sync context)
|
|
107
|
+
return {
|
|
108
|
+
cacheControl: false,
|
|
109
|
+
cacheMechanism: 'none',
|
|
110
|
+
cacheTTL: null,
|
|
111
|
+
structuredOutput: false,
|
|
112
|
+
maxContextWindow: 200000,
|
|
113
|
+
tokenRatio: 4,
|
|
114
|
+
annotationFormat: 'none',
|
|
115
|
+
annotationHeaders: null,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Probe provider for live capabilities (async).
|
|
121
|
+
* Results are cached for 24h.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} modelId
|
|
124
|
+
* @param {object} opts - { probe: boolean }
|
|
125
|
+
* @returns {Promise<object>}
|
|
126
|
+
*/
|
|
127
|
+
export async function probeProviderCapabilities(modelId, { probe = false } = {}) {
|
|
128
|
+
const caps = await resolveProviderCapabilities(modelId);
|
|
129
|
+
|
|
130
|
+
if (probe) {
|
|
131
|
+
// TODO: implement live probing (API call) in Phase A follow-up
|
|
132
|
+
// For now, just return the static capabilities
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Cache by adapter key
|
|
136
|
+
const adapterKey = resolveAdapterKey(modelId);
|
|
137
|
+
const cache = getCache();
|
|
138
|
+
cache[adapterKey] = caps;
|
|
139
|
+
writeCapabilityCache(cache);
|
|
140
|
+
|
|
141
|
+
return caps;
|
|
142
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/providers/atlassian-confluence/index.mjs — Confluence data-source provider.
|
|
3
|
+
*
|
|
4
|
+
* Capabilities: read, search.
|
|
5
|
+
*
|
|
6
|
+
* Auth: shares JIRA_* env vars with the Jira provider (Atlassian Cloud uses
|
|
7
|
+
* the same email + API token across products) plus optional
|
|
8
|
+
* `CONFLUENCE_BASE_URL` if it differs from `JIRA_BASE_URL`.
|
|
9
|
+
*
|
|
10
|
+
* Config (per call):
|
|
11
|
+
* - pageId: numeric id of the page to read
|
|
12
|
+
* - cql: CQL query string (e.g. 'space = ENG AND text ~ "auth"')
|
|
13
|
+
* - limit: integer (default 25)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const DEFAULT_LIMIT = 25;
|
|
17
|
+
const HARD_LIMIT = 100;
|
|
18
|
+
|
|
19
|
+
function authConfig(env) {
|
|
20
|
+
const baseUrl = (env.CONFLUENCE_BASE_URL || env.JIRA_BASE_URL || '').replace(/\/$/, '');
|
|
21
|
+
const email = env.CONFLUENCE_EMAIL || env.JIRA_EMAIL || '';
|
|
22
|
+
const token = env.CONFLUENCE_API_TOKEN || env.JIRA_API_TOKEN || '';
|
|
23
|
+
return { baseUrl, email, token, ok: Boolean(baseUrl && email && token) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function authHeader({ email, token }) {
|
|
27
|
+
if (!email || !token) return {};
|
|
28
|
+
const basic = Buffer.from(`${email}:${token}`).toString('base64');
|
|
29
|
+
return { Authorization: `Basic ${basic}` };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function confluenceFetch(path, env, init = {}) {
|
|
33
|
+
const auth = authConfig(env);
|
|
34
|
+
if (!auth.ok) throw new Error('confluence: base URL + email + token required');
|
|
35
|
+
const res = await fetch(`${auth.baseUrl}${path}`, {
|
|
36
|
+
...init,
|
|
37
|
+
headers: {
|
|
38
|
+
'Accept': 'application/json',
|
|
39
|
+
...authHeader(auth),
|
|
40
|
+
...(init.headers || {}),
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const body = await res.text().catch(() => '');
|
|
45
|
+
throw new Error(`Confluence ${res.status} ${res.statusText}: ${body.slice(0, 200)}`);
|
|
46
|
+
}
|
|
47
|
+
return res.json();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function create({ env = process.env } = {}) {
|
|
51
|
+
return {
|
|
52
|
+
meta: {
|
|
53
|
+
id: 'atlassian-confluence',
|
|
54
|
+
displayName: 'Atlassian Confluence',
|
|
55
|
+
capabilities: ['read', 'search'],
|
|
56
|
+
description: 'Pages, spaces, CQL search.',
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
configSchema: {
|
|
60
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
pageId: { type: 'string', pattern: '^[0-9]+$' },
|
|
64
|
+
cql: { type: 'string' },
|
|
65
|
+
limit: { type: 'integer', minimum: 1, maximum: HARD_LIMIT, default: DEFAULT_LIMIT },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async health() {
|
|
70
|
+
const auth = authConfig(env);
|
|
71
|
+
if (!auth.ok) {
|
|
72
|
+
return { ok: false, detail: 'CONFLUENCE_BASE_URL/JIRA_BASE_URL + email + token not set' };
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
await confluenceFetch('/wiki/rest/api/space?limit=1', env);
|
|
76
|
+
return { ok: true, detail: `connected to ${auth.baseUrl}` };
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return { ok: false, detail: err.message };
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
async read(config) {
|
|
83
|
+
if (!config?.pageId) throw new Error('confluence.read: config.pageId required');
|
|
84
|
+
return confluenceFetch(
|
|
85
|
+
`/wiki/rest/api/content/${encodeURIComponent(config.pageId)}?expand=body.storage,version`,
|
|
86
|
+
env
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
async search(config) {
|
|
91
|
+
const cql = config?.cql;
|
|
92
|
+
if (!cql) throw new Error('confluence.search: config.cql required');
|
|
93
|
+
const limit = Math.min(config?.limit || DEFAULT_LIMIT, HARD_LIMIT);
|
|
94
|
+
const data = await confluenceFetch(
|
|
95
|
+
`/wiki/rest/api/content/search?cql=${encodeURIComponent(cql)}&limit=${limit}`,
|
|
96
|
+
env
|
|
97
|
+
);
|
|
98
|
+
return Array.isArray(data?.results) ? data.results : [];
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default create;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/providers/atlassian-jira/index.mjs — Jira data-source provider.
|
|
3
|
+
*
|
|
4
|
+
* Capabilities: read, search.
|
|
5
|
+
*
|
|
6
|
+
* Auth: `JIRA_BASE_URL`, `JIRA_EMAIL`, `JIRA_API_TOKEN` from env.
|
|
7
|
+
*
|
|
8
|
+
* Config (per call):
|
|
9
|
+
* - issueKey: 'PROJ-123' for read
|
|
10
|
+
* - jql: JQL string for search (e.g. 'project = RLBLT AND assignee = currentUser()')
|
|
11
|
+
* - maxResults: integer (default 50, max 100)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const DEFAULT_MAX = 50;
|
|
15
|
+
const HARD_MAX = 100;
|
|
16
|
+
|
|
17
|
+
function authConfig(env) {
|
|
18
|
+
const baseUrl = env.JIRA_BASE_URL?.replace(/\/$/, '') || '';
|
|
19
|
+
const email = env.JIRA_EMAIL || '';
|
|
20
|
+
const token = env.JIRA_API_TOKEN || '';
|
|
21
|
+
return { baseUrl, email, token, ok: Boolean(baseUrl && email && token) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function authHeader({ email, token }) {
|
|
25
|
+
if (!email || !token) return {};
|
|
26
|
+
const basic = Buffer.from(`${email}:${token}`).toString('base64');
|
|
27
|
+
return { Authorization: `Basic ${basic}` };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function jiraFetch(path, env, init = {}) {
|
|
31
|
+
const auth = authConfig(env);
|
|
32
|
+
if (!auth.ok) throw new Error('jira: JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN must be set');
|
|
33
|
+
const url = `${auth.baseUrl}${path}`;
|
|
34
|
+
const res = await fetch(url, {
|
|
35
|
+
...init,
|
|
36
|
+
headers: {
|
|
37
|
+
'Accept': 'application/json',
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
...authHeader(auth),
|
|
40
|
+
...(init.headers || {}),
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const body = await res.text().catch(() => '');
|
|
45
|
+
throw new Error(`Jira ${res.status} ${res.statusText}: ${body.slice(0, 200)}`);
|
|
46
|
+
}
|
|
47
|
+
return res.json();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function create({ env = process.env } = {}) {
|
|
51
|
+
return {
|
|
52
|
+
meta: {
|
|
53
|
+
id: 'atlassian-jira',
|
|
54
|
+
displayName: 'Atlassian Jira',
|
|
55
|
+
capabilities: ['read', 'search'],
|
|
56
|
+
description: 'Issues, sprints, JQL search.',
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
configSchema: {
|
|
60
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
issueKey: { type: 'string', pattern: '^[A-Z][A-Z0-9_]+-[0-9]+$' },
|
|
64
|
+
jql: { type: 'string' },
|
|
65
|
+
maxResults: { type: 'integer', minimum: 1, maximum: HARD_MAX, default: DEFAULT_MAX },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async health() {
|
|
70
|
+
const auth = authConfig(env);
|
|
71
|
+
if (!auth.ok) {
|
|
72
|
+
return { ok: false, detail: 'JIRA_BASE_URL / JIRA_EMAIL / JIRA_API_TOKEN not set' };
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
await jiraFetch('/rest/api/3/myself', env);
|
|
76
|
+
return { ok: true, detail: `connected to ${auth.baseUrl}` };
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return { ok: false, detail: err.message };
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
async read(config) {
|
|
83
|
+
if (!config?.issueKey) throw new Error('jira.read: config.issueKey required');
|
|
84
|
+
return jiraFetch(`/rest/api/3/issue/${encodeURIComponent(config.issueKey)}`, env);
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
async search(config) {
|
|
88
|
+
const jql = config?.jql;
|
|
89
|
+
if (!jql) throw new Error('jira.search: config.jql required');
|
|
90
|
+
const max = Math.min(config?.maxResults || DEFAULT_MAX, HARD_MAX);
|
|
91
|
+
const data = await jiraFetch('/rest/api/3/search', env, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
body: JSON.stringify({ jql, maxResults: max, fields: ['summary', 'status', 'assignee', 'priority', 'updated'] }),
|
|
94
|
+
});
|
|
95
|
+
return Array.isArray(data?.issues) ? data.issues : [];
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default create;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// lib/providers/auth-manager.mjs
|
|
2
|
+
// OAuth refresh and token lifecycle management
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
|
|
8
|
+
const AUTH_DIR = path.join(homedir(), '.construct', 'auth');
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
|
|
12
|
+
} catch { /* ignore */ }
|
|
13
|
+
|
|
14
|
+
const PROVIDER_CONFIGS = {
|
|
15
|
+
github: {
|
|
16
|
+
name: 'GitHub',
|
|
17
|
+
tokenExpiry: null,
|
|
18
|
+
refreshable: false,
|
|
19
|
+
envVar: 'GITHUB_TOKEN',
|
|
20
|
+
},
|
|
21
|
+
salesforce: {
|
|
22
|
+
name: 'Salesforce',
|
|
23
|
+
tokenExpiry: 7200,
|
|
24
|
+
refreshable: true,
|
|
25
|
+
refreshBuffer: 300,
|
|
26
|
+
envVar: 'SALESFORCE_ACCESS_TOKEN',
|
|
27
|
+
refreshEnvVar: 'SALESFORCE_REFRESH_TOKEN',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function loadAuthState(provider) {
|
|
32
|
+
try {
|
|
33
|
+
const file = path.join(AUTH_DIR, `${provider}.json`);
|
|
34
|
+
if (fs.existsSync(file)) {
|
|
35
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
36
|
+
}
|
|
37
|
+
} catch { /* ignore */ }
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function saveAuthState(provider, state) {
|
|
42
|
+
try {
|
|
43
|
+
const file = path.join(AUTH_DIR, `${provider}.json`);
|
|
44
|
+
fs.writeFileSync(file, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getTokenStatus(provider) {
|
|
52
|
+
const config = PROVIDER_CONFIGS[provider];
|
|
53
|
+
if (!config) return { valid: false, error: 'Unknown provider' };
|
|
54
|
+
|
|
55
|
+
const state = loadAuthState(provider);
|
|
56
|
+
if (!state) {
|
|
57
|
+
// Fall back to env var
|
|
58
|
+
const envToken = process.env[config.envVar];
|
|
59
|
+
if (!envToken) {
|
|
60
|
+
return { valid: false, error: 'No token configured' };
|
|
61
|
+
}
|
|
62
|
+
return { valid: true, source: 'env', expiresAt: null };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!state.expiresAt) {
|
|
66
|
+
return { valid: true, source: 'auth-store' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const expiresAt = new Date(state.expiresAt);
|
|
70
|
+
const now = new Date();
|
|
71
|
+
const bufferMs = (config.refreshBuffer || 0) * 1000;
|
|
72
|
+
|
|
73
|
+
if (expiresAt - now < bufferMs) {
|
|
74
|
+
return {
|
|
75
|
+
valid: false,
|
|
76
|
+
error: 'Token expired or expiring soon',
|
|
77
|
+
needsRefresh: config.refreshable,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
valid: true,
|
|
83
|
+
source: 'auth-store',
|
|
84
|
+
expiresAt: state.expiresAt,
|
|
85
|
+
expiresIn: Math.floor((expiresAt - now) / 1000),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function withValidToken(provider, fn) {
|
|
90
|
+
const status = getTokenStatus(provider);
|
|
91
|
+
|
|
92
|
+
if (!status.valid) {
|
|
93
|
+
if (status.needsRefresh) {
|
|
94
|
+
const refreshed = await refreshToken(provider);
|
|
95
|
+
if (!refreshed.success) {
|
|
96
|
+
throw new Error(`Token refresh failed: ${refreshed.error}`);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
throw new Error(`Invalid token: ${status.error}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const state = loadAuthState(provider);
|
|
104
|
+
const token = state?.token || process.env[PROVIDER_CONFIGS[provider].envVar];
|
|
105
|
+
|
|
106
|
+
return await fn(token);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function refreshToken(provider) {
|
|
110
|
+
const config = PROVIDER_CONFIGS[provider];
|
|
111
|
+
if (!config?.refreshable) {
|
|
112
|
+
return { success: false, error: 'Provider does not support refresh' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const state = loadAuthState(provider);
|
|
116
|
+
if (!state?.refreshToken) {
|
|
117
|
+
return { success: false, error: 'No refresh token available' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Provider-specific refresh logic would go here
|
|
121
|
+
// For now, just indicate refresh is needed
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
error: 'Token refresh required - reauthenticate manually',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/providers/circuit-breaker.mjs — per-provider circuit breaker.
|
|
3
|
+
*
|
|
4
|
+
* Wraps any async function with three-state breaker semantics:
|
|
5
|
+
*
|
|
6
|
+
* CLOSED — calls run normally. Consecutive failures are counted.
|
|
7
|
+
* OPEN — calls fail-fast with a `CircuitOpenError`. After
|
|
8
|
+
* `cooldownMs`, the breaker transitions to HALF_OPEN.
|
|
9
|
+
* HALF_OPEN — the next call is a probe. Success → CLOSED, failure → OPEN.
|
|
10
|
+
*
|
|
11
|
+
* Wraps provider HTTP calls so when a remote system goes down, Construct
|
|
12
|
+
* stops drilling it (and stops blocking user-facing operations on the failure
|
|
13
|
+
* mode) until the cooldown elapses. State is per-key, so one provider going
|
|
14
|
+
* down doesn't trip another's breaker.
|
|
15
|
+
*
|
|
16
|
+
* The breaker classifies errors via `isFailure(err)`. By default any
|
|
17
|
+
* non-null error counts as a failure. Callers can pass a predicate to
|
|
18
|
+
* exclude expected errors (e.g. 4xx auth errors that don't indicate the
|
|
19
|
+
* remote system is unhealthy).
|
|
20
|
+
*
|
|
21
|
+
* Inspection: `getBreaker(key).state`, `.openedAt`, `.failureCount` are
|
|
22
|
+
* read-only views, useful for `construct doctor` to surface "GitHub
|
|
23
|
+
* provider circuit OPEN since 12:34:56".
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const STATES = Object.freeze({ CLOSED: 'closed', OPEN: 'open', HALF_OPEN: 'half_open' });
|
|
27
|
+
|
|
28
|
+
const REGISTRY = new Map();
|
|
29
|
+
|
|
30
|
+
class CircuitOpenError extends Error {
|
|
31
|
+
constructor(key, openedAt) {
|
|
32
|
+
super(`circuit breaker '${key}' is open (since ${new Date(openedAt).toISOString()})`);
|
|
33
|
+
this.name = 'CircuitOpenError';
|
|
34
|
+
this.key = key;
|
|
35
|
+
this.openedAt = openedAt;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class Breaker {
|
|
40
|
+
constructor(key, options = {}) {
|
|
41
|
+
this.key = key;
|
|
42
|
+
this.failureThreshold = options.failureThreshold ?? 5;
|
|
43
|
+
this.cooldownMs = options.cooldownMs ?? 30_000;
|
|
44
|
+
this.isFailure = options.isFailure || ((err) => err != null);
|
|
45
|
+
this.state = STATES.CLOSED;
|
|
46
|
+
this.failureCount = 0;
|
|
47
|
+
this.openedAt = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
reset() {
|
|
51
|
+
this.state = STATES.CLOSED;
|
|
52
|
+
this.failureCount = 0;
|
|
53
|
+
this.openedAt = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_maybeHalfOpen() {
|
|
57
|
+
if (this.state !== STATES.OPEN) return;
|
|
58
|
+
if (Date.now() - this.openedAt >= this.cooldownMs) {
|
|
59
|
+
this.state = STATES.HALF_OPEN;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_recordSuccess() {
|
|
64
|
+
if (this.state === STATES.HALF_OPEN) {
|
|
65
|
+
this.reset();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (this.state === STATES.CLOSED) {
|
|
69
|
+
this.failureCount = 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_recordFailure() {
|
|
74
|
+
if (this.state === STATES.HALF_OPEN) {
|
|
75
|
+
this.state = STATES.OPEN;
|
|
76
|
+
this.openedAt = Date.now();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.failureCount += 1;
|
|
80
|
+
if (this.failureCount >= this.failureThreshold) {
|
|
81
|
+
this.state = STATES.OPEN;
|
|
82
|
+
this.openedAt = Date.now();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async run(fn, ...args) {
|
|
87
|
+
this._maybeHalfOpen();
|
|
88
|
+
if (this.state === STATES.OPEN) {
|
|
89
|
+
throw new CircuitOpenError(this.key, this.openedAt);
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const result = await fn(...args);
|
|
93
|
+
this._recordSuccess();
|
|
94
|
+
return result;
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (this.isFailure(err)) this._recordFailure();
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getBreaker(key, options) {
|
|
103
|
+
if (!REGISTRY.has(key)) REGISTRY.set(key, new Breaker(key, options));
|
|
104
|
+
return REGISTRY.get(key);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function describeBreakers() {
|
|
108
|
+
return [...REGISTRY.values()].map((b) => ({
|
|
109
|
+
key: b.key,
|
|
110
|
+
state: b.state,
|
|
111
|
+
failureCount: b.failureCount,
|
|
112
|
+
openedAt: b.openedAt,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resetBreaker(key) {
|
|
117
|
+
REGISTRY.get(key)?.reset();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function clearBreakerRegistry() {
|
|
121
|
+
REGISTRY.clear();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { STATES, CircuitOpenError };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/providers/contract.mjs — data-source provider contract.
|
|
3
|
+
*
|
|
4
|
+
* A provider integrates Construct with an external system that holds work
|
|
5
|
+
* artefacts: GitHub repos, Jira projects, Confluence spaces, Slack channels,
|
|
6
|
+
* Salesforce orgs, etc. Providers are stateless adapters — durable state
|
|
7
|
+
* (cached search results, last-seen markers, observations distilled from
|
|
8
|
+
* fetched items) lives in core stores, not in the provider.
|
|
9
|
+
*
|
|
10
|
+
* A provider module exports a factory:
|
|
11
|
+
*
|
|
12
|
+
* export function create(options) {
|
|
13
|
+
* return {
|
|
14
|
+
* meta: { id, displayName, capabilities: [...], description },
|
|
15
|
+
* configSchema: { ... } // JSON-Schema draft 2020-12
|
|
16
|
+
* health: async (config) => ({ ok, detail }),
|
|
17
|
+
* read?: async (config, query) => items[],
|
|
18
|
+
* search?: async (config, query) => items[],
|
|
19
|
+
* watch?: async (config, callback) => unsubscribe,
|
|
20
|
+
* write?: async (config, payload) => result,
|
|
21
|
+
* webhook?: async (config, request) => ack,
|
|
22
|
+
* };
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* Capabilities are an unordered set drawn from the canonical list in
|
|
26
|
+
* `CAPABILITIES`. A provider declares only what it implements; callers
|
|
27
|
+
* consult `meta.capabilities` and skip optional methods that aren't
|
|
28
|
+
* declared.
|
|
29
|
+
*
|
|
30
|
+
* `assertProviderContract(provider)` runs at registry load time and refuses
|
|
31
|
+
* to accept providers that miss required fields or declare capabilities
|
|
32
|
+
* they don't implement. The check is deliberately loose — providers can
|
|
33
|
+
* extend the shape with extra methods — but it gates the load-bearing
|
|
34
|
+
* fields so a typo in a plugin can't take down the registry.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export const CAPABILITIES = Object.freeze([
|
|
38
|
+
'read',
|
|
39
|
+
'search',
|
|
40
|
+
'watch',
|
|
41
|
+
'write',
|
|
42
|
+
'webhook',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const REQUIRED_META = ['id', 'displayName', 'capabilities'];
|
|
46
|
+
const REQUIRED_METHODS = ['health'];
|
|
47
|
+
const CAPABILITY_TO_METHOD = {
|
|
48
|
+
read: 'read',
|
|
49
|
+
search: 'search',
|
|
50
|
+
watch: 'watch',
|
|
51
|
+
write: 'write',
|
|
52
|
+
webhook: 'webhook',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function assertProviderContract(provider) {
|
|
56
|
+
if (!provider || typeof provider !== 'object') {
|
|
57
|
+
throw new Error('provider: must be a non-null object returned from create()');
|
|
58
|
+
}
|
|
59
|
+
if (!provider.meta || typeof provider.meta !== 'object') {
|
|
60
|
+
throw new Error('provider: missing required `meta` object');
|
|
61
|
+
}
|
|
62
|
+
for (const field of REQUIRED_META) {
|
|
63
|
+
if (provider.meta[field] === undefined || provider.meta[field] === null || provider.meta[field] === '') {
|
|
64
|
+
throw new Error(`provider: meta.${field} is required`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!Array.isArray(provider.meta.capabilities) || provider.meta.capabilities.length === 0) {
|
|
68
|
+
throw new Error(`provider (${provider.meta.id}): meta.capabilities must be a non-empty array`);
|
|
69
|
+
}
|
|
70
|
+
for (const cap of provider.meta.capabilities) {
|
|
71
|
+
if (!CAPABILITIES.includes(cap)) {
|
|
72
|
+
throw new Error(`provider (${provider.meta.id}): unknown capability '${cap}'. Allowed: ${CAPABILITIES.join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
const method = CAPABILITY_TO_METHOD[cap];
|
|
75
|
+
if (typeof provider[method] !== 'function') {
|
|
76
|
+
throw new Error(`provider (${provider.meta.id}): capability '${cap}' declared but method '${method}' is missing`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const fn of REQUIRED_METHODS) {
|
|
80
|
+
if (typeof provider[fn] !== 'function') {
|
|
81
|
+
throw new Error(`provider (${provider.meta.id}): missing required method '${fn}'`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function checkProviderContract(provider) {
|
|
87
|
+
try {
|
|
88
|
+
assertProviderContract(provider);
|
|
89
|
+
return { ok: true, errors: [] };
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return { ok: false, errors: [err.message] };
|
|
92
|
+
}
|
|
93
|
+
}
|