@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,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embed/providers/jira.mjs — Jira (Atlassian) provider for embed mode.
|
|
3
|
+
*
|
|
4
|
+
* Reads issues from Jira Cloud using the REST API v3.
|
|
5
|
+
* Zero external deps — uses Node's built-in fetch.
|
|
6
|
+
*
|
|
7
|
+
* Supported refs:
|
|
8
|
+
* issues Issues matching a JQL query or project filter
|
|
9
|
+
* sprints Active sprint issues for a board
|
|
10
|
+
*
|
|
11
|
+
* Source config fields (embed.yaml):
|
|
12
|
+
* provider: jira
|
|
13
|
+
* project: PROJ # project key — drives the default JQL
|
|
14
|
+
* projects: # OR list of project keys
|
|
15
|
+
* - PROJ
|
|
16
|
+
* - INFRA
|
|
17
|
+
* jql: "project = PROJ AND sprint in openSprints()" # override JQL
|
|
18
|
+
* refs: [issues]
|
|
19
|
+
* limit: 50 # max issues (default: 50)
|
|
20
|
+
* fields: [summary,status,assignee,priority] # optional field subset
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export class JiraProvider {
|
|
24
|
+
#baseUrl;
|
|
25
|
+
#auth;
|
|
26
|
+
#fetchFn;
|
|
27
|
+
|
|
28
|
+
constructor({ baseUrl, email, token, fetchFn = globalThis.fetch } = {}) {
|
|
29
|
+
if (!baseUrl || !email || !token) {
|
|
30
|
+
throw new Error('JiraProvider requires baseUrl, email, and token');
|
|
31
|
+
}
|
|
32
|
+
this.#baseUrl = baseUrl.replace(/\/$/, '');
|
|
33
|
+
this.#auth = `Basic ${Buffer.from(`${email}:${token}`).toString('base64')}`;
|
|
34
|
+
this.#fetchFn = fetchFn;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Default sources used when no embed.yaml is present.
|
|
39
|
+
* Queries all non-done issues assigned to the current user.
|
|
40
|
+
*/
|
|
41
|
+
defaultSources() {
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
provider: 'jira',
|
|
45
|
+
refs: ['issues'],
|
|
46
|
+
jql: 'assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC',
|
|
47
|
+
limit: 50,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} ref - 'issues' | 'sprints'
|
|
54
|
+
* @param {object} opts - source config (project, projects, jql, limit, fields)
|
|
55
|
+
* @returns {Promise<Item[]>}
|
|
56
|
+
*/
|
|
57
|
+
async read(ref, opts = {}) {
|
|
58
|
+
switch (ref) {
|
|
59
|
+
case 'issues': return this.#listIssues(opts);
|
|
60
|
+
case 'sprints': return this.#listSprintIssues(opts);
|
|
61
|
+
default: throw new Error(`Jira provider: unknown ref "${ref}"`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async #listIssues(opts) {
|
|
66
|
+
const limit = Number(opts.limit ?? 50);
|
|
67
|
+
const jql = opts.jql ?? buildDefaultJql(opts);
|
|
68
|
+
const fields = opts.fields ?? ['summary', 'status', 'assignee', 'priority', 'issuetype', 'labels', 'updated', 'created'];
|
|
69
|
+
|
|
70
|
+
const data = await this.#post('/rest/api/3/search/jql', {
|
|
71
|
+
jql,
|
|
72
|
+
maxResults: limit,
|
|
73
|
+
fields,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return (data.issues ?? []).map((issue) => ({
|
|
77
|
+
type: 'issue',
|
|
78
|
+
source: 'jira',
|
|
79
|
+
key: issue.key,
|
|
80
|
+
id: issue.id,
|
|
81
|
+
title: issue.fields?.summary ?? '',
|
|
82
|
+
status: issue.fields?.status?.name ?? null,
|
|
83
|
+
statusCategory: issue.fields?.status?.statusCategory?.name ?? null,
|
|
84
|
+
assignee: issue.fields?.assignee?.displayName ?? null,
|
|
85
|
+
priority: issue.fields?.priority?.name ?? null,
|
|
86
|
+
issueType: issue.fields?.issuetype?.name ?? null,
|
|
87
|
+
labels: issue.fields?.labels ?? [],
|
|
88
|
+
project: issue.key.split('-')[0],
|
|
89
|
+
url: `${this.#baseUrl}/browse/${issue.key}`,
|
|
90
|
+
createdAt: issue.fields?.created,
|
|
91
|
+
updatedAt: issue.fields?.updated,
|
|
92
|
+
summary: `[${issue.key}] ${issue.fields?.summary ?? ''}`,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async #listSprintIssues(opts) {
|
|
97
|
+
// Fetch all boards, find ones matching the project, get active sprint
|
|
98
|
+
const projects = resolveProjects(opts);
|
|
99
|
+
if (!projects.length) throw new Error('Jira sprints source requires "project" or "projects"');
|
|
100
|
+
|
|
101
|
+
const results = [];
|
|
102
|
+
for (const projectKey of projects) {
|
|
103
|
+
try {
|
|
104
|
+
const boards = await this.#get(`/rest/agile/1.0/board?projectKeyOrId=${projectKey}&type=scrum`);
|
|
105
|
+
const board = boards.values?.[0];
|
|
106
|
+
if (!board) continue;
|
|
107
|
+
|
|
108
|
+
const sprints = await this.#get(`/rest/agile/1.0/board/${board.id}/sprint?state=active`);
|
|
109
|
+
const sprint = sprints.values?.[0];
|
|
110
|
+
if (!sprint) continue;
|
|
111
|
+
|
|
112
|
+
const sprintIssues = await this.#get(
|
|
113
|
+
`/rest/agile/1.0/sprint/${sprint.id}/issue?maxResults=${opts.limit ?? 50}`,
|
|
114
|
+
);
|
|
115
|
+
const items = (sprintIssues.issues ?? []).map((issue) => ({
|
|
116
|
+
type: 'issue',
|
|
117
|
+
source: 'jira',
|
|
118
|
+
sprint: sprint.name,
|
|
119
|
+
key: issue.key,
|
|
120
|
+
title: issue.fields?.summary ?? '',
|
|
121
|
+
status: issue.fields?.status?.name ?? null,
|
|
122
|
+
assignee: issue.fields?.assignee?.displayName ?? null,
|
|
123
|
+
priority: issue.fields?.priority?.name ?? null,
|
|
124
|
+
project: projectKey,
|
|
125
|
+
url: `${this.#baseUrl}/browse/${issue.key}`,
|
|
126
|
+
updatedAt: issue.fields?.updated,
|
|
127
|
+
summary: `[${issue.key}] ${issue.fields?.summary ?? ''}`,
|
|
128
|
+
}));
|
|
129
|
+
results.push(...items);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
results.push({ type: 'error', source: 'jira', project: projectKey, message: err.message });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return results;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async #get(path) {
|
|
138
|
+
const controller = new AbortController();
|
|
139
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
140
|
+
try {
|
|
141
|
+
const res = await this.#fetchFn(`${this.#baseUrl}${path}`, {
|
|
142
|
+
headers: { Authorization: this.#auth, Accept: 'application/json' },
|
|
143
|
+
signal: controller.signal,
|
|
144
|
+
});
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
const body = await res.text().catch(() => '');
|
|
147
|
+
throw new Error(`Jira API ${res.status}: ${body.slice(0, 200)}`);
|
|
148
|
+
}
|
|
149
|
+
return res.json();
|
|
150
|
+
} finally {
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async #post(path, body) {
|
|
156
|
+
const controller = new AbortController();
|
|
157
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
158
|
+
try {
|
|
159
|
+
const res = await this.#fetchFn(`${this.#baseUrl}${path}`, {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
headers: {
|
|
162
|
+
Authorization: this.#auth,
|
|
163
|
+
Accept: 'application/json',
|
|
164
|
+
'Content-Type': 'application/json',
|
|
165
|
+
},
|
|
166
|
+
body: JSON.stringify(body),
|
|
167
|
+
signal: controller.signal,
|
|
168
|
+
});
|
|
169
|
+
if (!res.ok) {
|
|
170
|
+
const text = await res.text().catch(() => '');
|
|
171
|
+
throw new Error(`Jira API ${res.status}: ${text.slice(0, 200)}`);
|
|
172
|
+
}
|
|
173
|
+
return res.json();
|
|
174
|
+
} finally {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function resolveProjects(opts) {
|
|
181
|
+
if (Array.isArray(opts.projects)) return opts.projects.filter(Boolean);
|
|
182
|
+
if (opts.project) return [opts.project];
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildDefaultJql(opts) {
|
|
187
|
+
const projects = resolveProjects(opts);
|
|
188
|
+
const projectClause = projects.length
|
|
189
|
+
? `project in (${projects.map((p) => `"${p}"`).join(', ')}) AND `
|
|
190
|
+
: '';
|
|
191
|
+
return `${projectClause}statusCategory != Done ORDER BY updated DESC`;
|
|
192
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embed/providers/linear.mjs — Linear provider for embed mode.
|
|
3
|
+
*
|
|
4
|
+
* Reads issues from Linear using the GraphQL API.
|
|
5
|
+
* Zero external deps — uses Node's built-in fetch.
|
|
6
|
+
*
|
|
7
|
+
* Supported refs:
|
|
8
|
+
* issues Open/in-progress issues
|
|
9
|
+
* cycles Active cycle issues
|
|
10
|
+
*
|
|
11
|
+
* Source config fields (embed.yaml):
|
|
12
|
+
* provider: linear
|
|
13
|
+
* team: ENG # team key or ID (optional — all teams if omitted)
|
|
14
|
+
* project: My Project # project name filter (optional)
|
|
15
|
+
* refs: [issues]
|
|
16
|
+
* states: [Todo, In Progress] # filter by state names (default: excludes Done/Cancelled)
|
|
17
|
+
* limit: 50 # max issues (default: 50)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const LINEAR_API = 'https://api.linear.app/graphql';
|
|
21
|
+
|
|
22
|
+
export class LinearProvider {
|
|
23
|
+
#apiKey;
|
|
24
|
+
#fetchFn;
|
|
25
|
+
|
|
26
|
+
constructor({ apiKey, fetchFn = globalThis.fetch } = {}) {
|
|
27
|
+
if (!apiKey) throw new Error('LinearProvider requires an apiKey');
|
|
28
|
+
this.#apiKey = apiKey;
|
|
29
|
+
this.#fetchFn = fetchFn;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Auto-discovery: return assigned non-completed issues when LINEAR_API_KEY is present.
|
|
34
|
+
*/
|
|
35
|
+
defaultSources() {
|
|
36
|
+
return [{ provider: 'linear', refs: ['issues'], limit: 50 }];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} ref - 'issues' | 'cycles'
|
|
41
|
+
* @param {object} opts - source config (team, project, states, limit)
|
|
42
|
+
* @returns {Promise<Item[]>}
|
|
43
|
+
*/
|
|
44
|
+
async read(ref, opts = {}) {
|
|
45
|
+
switch (ref) {
|
|
46
|
+
case 'issues': return this.#listIssues(opts);
|
|
47
|
+
case 'cycles': return this.#listCycleIssues(opts);
|
|
48
|
+
default: throw new Error(`Linear provider: unknown ref "${ref}"`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async #listIssues(opts) {
|
|
53
|
+
const limit = Number(opts.limit ?? 50);
|
|
54
|
+
const stateFilter = Array.isArray(opts.states) ? opts.states : null;
|
|
55
|
+
|
|
56
|
+
// Build filter clauses
|
|
57
|
+
const filters = [];
|
|
58
|
+
if (opts.team) filters.push(`team: { key: { eq: "${opts.team}" } }`);
|
|
59
|
+
if (stateFilter) {
|
|
60
|
+
const stateIn = stateFilter.map((s) => `"${s}"`).join(', ');
|
|
61
|
+
filters.push(`state: { name: { in: [${stateIn}] } }`);
|
|
62
|
+
} else {
|
|
63
|
+
// Default: exclude terminal states
|
|
64
|
+
filters.push(`state: { type: { nin: ["completed", "cancelled"] } }`);
|
|
65
|
+
}
|
|
66
|
+
if (opts.project) filters.push(`project: { name: { eq: "${opts.project}" } }`);
|
|
67
|
+
|
|
68
|
+
const filterClause = filters.length ? `filter: { ${filters.join(', ')} }` : '';
|
|
69
|
+
|
|
70
|
+
const query = `{
|
|
71
|
+
issues(first: ${limit} ${filterClause} orderBy: updatedAt) {
|
|
72
|
+
nodes {
|
|
73
|
+
id
|
|
74
|
+
identifier
|
|
75
|
+
title
|
|
76
|
+
description
|
|
77
|
+
priority
|
|
78
|
+
priorityLabel
|
|
79
|
+
state { name type }
|
|
80
|
+
assignee { name }
|
|
81
|
+
team { name key }
|
|
82
|
+
project { name }
|
|
83
|
+
url
|
|
84
|
+
createdAt
|
|
85
|
+
updatedAt
|
|
86
|
+
labels { nodes { name } }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}`;
|
|
90
|
+
|
|
91
|
+
const data = await this.#graphql(query);
|
|
92
|
+
return (data?.issues?.nodes ?? []).map((issue) => ({
|
|
93
|
+
type: 'issue',
|
|
94
|
+
source: 'linear',
|
|
95
|
+
id: issue.identifier,
|
|
96
|
+
title: issue.title,
|
|
97
|
+
description: issue.description?.slice(0, 200) ?? null,
|
|
98
|
+
state: issue.state?.name,
|
|
99
|
+
stateType: issue.state?.type,
|
|
100
|
+
priority: issue.priorityLabel,
|
|
101
|
+
assignee: issue.assignee?.name ?? null,
|
|
102
|
+
team: issue.team?.key,
|
|
103
|
+
teamName: issue.team?.name,
|
|
104
|
+
project: issue.project?.name ?? null,
|
|
105
|
+
labels: (issue.labels?.nodes ?? []).map((l) => l.name),
|
|
106
|
+
url: issue.url,
|
|
107
|
+
createdAt: issue.createdAt,
|
|
108
|
+
updatedAt: issue.updatedAt,
|
|
109
|
+
summary: `[${issue.identifier}] ${issue.title}`,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async #listCycleIssues(opts) {
|
|
114
|
+
const limit = Number(opts.limit ?? 50);
|
|
115
|
+
const teamFilter = opts.team ? `filter: { team: { key: { eq: "${opts.team}" } } }` : '';
|
|
116
|
+
|
|
117
|
+
const query = `{
|
|
118
|
+
cycles(first: 1 ${teamFilter} filter: { isActive: { eq: true } }) {
|
|
119
|
+
nodes {
|
|
120
|
+
id
|
|
121
|
+
name
|
|
122
|
+
number
|
|
123
|
+
startsAt
|
|
124
|
+
endsAt
|
|
125
|
+
team { name key }
|
|
126
|
+
issues(first: ${limit}) {
|
|
127
|
+
nodes {
|
|
128
|
+
id
|
|
129
|
+
identifier
|
|
130
|
+
title
|
|
131
|
+
state { name type }
|
|
132
|
+
assignee { name }
|
|
133
|
+
priority
|
|
134
|
+
priorityLabel
|
|
135
|
+
url
|
|
136
|
+
updatedAt
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}`;
|
|
142
|
+
|
|
143
|
+
const data = await this.#graphql(query);
|
|
144
|
+
const cycle = data?.cycles?.nodes?.[0];
|
|
145
|
+
if (!cycle) return [];
|
|
146
|
+
|
|
147
|
+
return (cycle.issues?.nodes ?? []).map((issue) => ({
|
|
148
|
+
type: 'issue',
|
|
149
|
+
source: 'linear',
|
|
150
|
+
cycle: `${cycle.team?.key ?? ''} Cycle ${cycle.number}`,
|
|
151
|
+
id: issue.identifier,
|
|
152
|
+
title: issue.title,
|
|
153
|
+
state: issue.state?.name,
|
|
154
|
+
stateType: issue.state?.type,
|
|
155
|
+
priority: issue.priorityLabel,
|
|
156
|
+
assignee: issue.assignee?.name ?? null,
|
|
157
|
+
url: issue.url,
|
|
158
|
+
updatedAt: issue.updatedAt,
|
|
159
|
+
summary: `[${issue.identifier}] ${issue.title}`,
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async #graphql(query) {
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
const timer = setTimeout(() => controller.abort(), 15_000);
|
|
166
|
+
try {
|
|
167
|
+
const res = await this.#fetchFn(LINEAR_API, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: {
|
|
170
|
+
Authorization: this.#apiKey,
|
|
171
|
+
'Content-Type': 'application/json',
|
|
172
|
+
},
|
|
173
|
+
body: JSON.stringify({ query }),
|
|
174
|
+
signal: controller.signal,
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok) throw new Error(`Linear API HTTP ${res.status}`);
|
|
177
|
+
const json = await res.json();
|
|
178
|
+
if (json.errors?.length) {
|
|
179
|
+
throw new Error(`Linear GraphQL: ${json.errors[0].message}`);
|
|
180
|
+
}
|
|
181
|
+
return json.data;
|
|
182
|
+
} finally {
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embed/providers/registry.mjs — ProviderRegistry.
|
|
3
|
+
*
|
|
4
|
+
* Holds named provider instances and exposes an async factory that builds
|
|
5
|
+
* the standard set from environment variables. Each provider implements:
|
|
6
|
+
*
|
|
7
|
+
* read(ref, opts) → Promise<Item[]>
|
|
8
|
+
* ref: source-type string e.g. 'prs', 'issues', 'commits', 'messages'
|
|
9
|
+
* opts: source config fields (repo, channel, project, …)
|
|
10
|
+
*
|
|
11
|
+
* write(action) → Promise<void> (optional — only mutation providers)
|
|
12
|
+
* action: { type, channel?, text?, … }
|
|
13
|
+
*
|
|
14
|
+
* Providers are registered by name and can be swapped for test doubles.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export class ProviderRegistry {
|
|
18
|
+
#providers = new Map();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register a provider under one or more names.
|
|
22
|
+
* @param {string|string[]} names
|
|
23
|
+
* @param {object} provider
|
|
24
|
+
*/
|
|
25
|
+
register(names, provider) {
|
|
26
|
+
for (const name of [].concat(names)) {
|
|
27
|
+
this.#providers.set(name, provider);
|
|
28
|
+
}
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Retrieve a provider by name. Returns null if not registered.
|
|
34
|
+
* @param {string} name
|
|
35
|
+
*/
|
|
36
|
+
get(name) {
|
|
37
|
+
return this.#providers.get(name) ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build the union of all registered providers' default sources.
|
|
42
|
+
* Called when no embed.yaml is present.
|
|
43
|
+
* @param {object} env - Merged environment (for providers that need it, e.g. GitHub repos)
|
|
44
|
+
* @returns {object[]} - Array of source config objects ready for SnapshotEngine
|
|
45
|
+
*/
|
|
46
|
+
autoSources(env = {}) {
|
|
47
|
+
const sources = [];
|
|
48
|
+
for (const provider of this.#providers.values()) {
|
|
49
|
+
if (typeof provider.defaultSources === 'function') {
|
|
50
|
+
sources.push(...provider.defaultSources(env));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Deduplicate by provider+refs signature — multiple registry aliases can point to same instance
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
return sources.filter((s) => {
|
|
56
|
+
const key = `${s.provider}:${(s.refs ?? []).join(',')}`;
|
|
57
|
+
if (seen.has(key)) return false;
|
|
58
|
+
seen.add(key);
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* List all registered provider names.
|
|
65
|
+
*/
|
|
66
|
+
names() {
|
|
67
|
+
return [...this.#providers.keys()];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build a registry pre-populated with all concrete providers derived
|
|
72
|
+
* from the supplied environment. Providers whose required credentials
|
|
73
|
+
* are absent are silently omitted — callers get a warning at runtime
|
|
74
|
+
* when a source references a missing provider.
|
|
75
|
+
*
|
|
76
|
+
* @param {object} env - Merged environment (loadConstructEnv output)
|
|
77
|
+
* @returns {Promise<ProviderRegistry>}
|
|
78
|
+
*/
|
|
79
|
+
static async fromEnv(env = process.env) {
|
|
80
|
+
const registry = new ProviderRegistry();
|
|
81
|
+
|
|
82
|
+
if (env.GITHUB_TOKEN || env.GITHUB_PERSONAL_ACCESS_TOKEN) {
|
|
83
|
+
const { GitHubProvider } = await import('./github.mjs');
|
|
84
|
+
registry.register(['github', 'gh'], new GitHubProvider({
|
|
85
|
+
token: env.GITHUB_TOKEN || env.GITHUB_PERSONAL_ACCESS_TOKEN,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (env.SLACK_BOT_TOKEN) {
|
|
90
|
+
const { SlackProvider } = await import('./slack.mjs');
|
|
91
|
+
registry.register(['slack'], new SlackProvider({
|
|
92
|
+
token: env.SLACK_BOT_TOKEN,
|
|
93
|
+
teamId: env.SLACK_TEAM_ID,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (env.LINEAR_API_KEY) {
|
|
98
|
+
const { LinearProvider } = await import('./linear.mjs');
|
|
99
|
+
registry.register(['linear'], new LinearProvider({
|
|
100
|
+
apiKey: env.LINEAR_API_KEY,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (env.JIRA_API_TOKEN && env.JIRA_USER_EMAIL && env.JIRA_BASE_URL) {
|
|
105
|
+
const { JiraProvider } = await import('./jira.mjs');
|
|
106
|
+
registry.register(['jira', 'atlassian'], new JiraProvider({
|
|
107
|
+
baseUrl: env.JIRA_BASE_URL,
|
|
108
|
+
email: env.JIRA_USER_EMAIL,
|
|
109
|
+
token: env.JIRA_API_TOKEN,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return registry;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embed/providers/slack.mjs — Slack provider for embed mode.
|
|
3
|
+
*
|
|
4
|
+
* Reads recent messages from one or more channels using the Slack Web API.
|
|
5
|
+
* Zero external deps — uses Node's built-in fetch.
|
|
6
|
+
*
|
|
7
|
+
* Supported refs:
|
|
8
|
+
* messages Recent messages in a channel
|
|
9
|
+
*
|
|
10
|
+
* Source config fields (embed.yaml):
|
|
11
|
+
* provider: slack
|
|
12
|
+
* channel: C12345678 # single channel ID or name
|
|
13
|
+
* channels: # OR list, optionally with intent tags
|
|
14
|
+
* - general # no intent — defaults to 'internal'
|
|
15
|
+
* - incidents:risk # channel:intent — drives observation category
|
|
16
|
+
* - customer-feedback:external
|
|
17
|
+
* refs: [messages]
|
|
18
|
+
* limit: 20 # max messages per channel (default: 20)
|
|
19
|
+
* oldest: 86400 # seconds of history to fetch (default: 86400 = 24h)
|
|
20
|
+
*
|
|
21
|
+
* SLACK_CHANNELS env format (config.env):
|
|
22
|
+
* SLACK_CHANNELS=#eng-general,#incidents:risk,#decisions:decision,#customer:external
|
|
23
|
+
*
|
|
24
|
+
* Intent values and their observation category mapping:
|
|
25
|
+
* internal → insight (default — team chat, general discussion)
|
|
26
|
+
* risk → anti-pattern (incidents, alerts, on-call, escalations)
|
|
27
|
+
* decision → decision (decision-making channels, RFC discussions)
|
|
28
|
+
* external → insight + tag:external (customer feedback, support, sales)
|
|
29
|
+
* how-to → pattern (tips, best practices, knowledge sharing)
|
|
30
|
+
* (none) → insight (fallback)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const SLACK_API = 'https://slack.com/api';
|
|
34
|
+
|
|
35
|
+
export class SlackProvider {
|
|
36
|
+
#token;
|
|
37
|
+
#teamId;
|
|
38
|
+
#fetchFn;
|
|
39
|
+
|
|
40
|
+
constructor({ token, teamId, fetchFn = globalThis.fetch } = {}) {
|
|
41
|
+
if (!token) throw new Error('SlackProvider requires a token');
|
|
42
|
+
this.#token = token;
|
|
43
|
+
this.#teamId = teamId;
|
|
44
|
+
this.#fetchFn = fetchFn;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Auto-discovery: return one source per intent group from SLACK_CHANNELS.
|
|
49
|
+
* Format: SLACK_CHANNELS=#eng-general,#incidents:risk,#decisions:decision
|
|
50
|
+
* Channels without an intent tag default to 'internal'.
|
|
51
|
+
* Each unique intent becomes its own source so snapshot sections are typed.
|
|
52
|
+
*/
|
|
53
|
+
defaultSources(env = {}) {
|
|
54
|
+
const raw = env.SLACK_CHANNELS ?? env.SLACK_CHANNEL ?? '';
|
|
55
|
+
const entries = raw.split(',').map((c) => c.trim()).filter(Boolean).map(parseChannelEntry);
|
|
56
|
+
if (!entries.length) return [];
|
|
57
|
+
|
|
58
|
+
// Group channels by intent so each source section carries a consistent intent
|
|
59
|
+
const byIntent = new Map();
|
|
60
|
+
for (const { channel, intent } of entries) {
|
|
61
|
+
if (!byIntent.has(intent)) byIntent.set(intent, []);
|
|
62
|
+
byIntent.get(intent).push(channel);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return [...byIntent.entries()].map(([intent, channels]) => ({
|
|
66
|
+
provider: 'slack',
|
|
67
|
+
refs: ['messages'],
|
|
68
|
+
channels,
|
|
69
|
+
intent,
|
|
70
|
+
oldest: 86400,
|
|
71
|
+
limit: 50,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} ref - 'messages'
|
|
77
|
+
* @param {object} opts - source config (channel, channels, limit, oldest)
|
|
78
|
+
* @returns {Promise<Item[]>}
|
|
79
|
+
*/
|
|
80
|
+
async read(ref, opts = {}) {
|
|
81
|
+
if (ref !== 'messages') throw new Error(`Slack provider: unknown ref "${ref}"`);
|
|
82
|
+
|
|
83
|
+
const channelEntries = resolveChannels(opts);
|
|
84
|
+
if (!channelEntries.length) {
|
|
85
|
+
throw new Error(`Slack source requires "channel" or "channels" field`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const intent = opts.intent ?? 'internal';
|
|
89
|
+
const limit = Number(opts.limit ?? 20);
|
|
90
|
+
const oldestSec = Number(opts.oldest ?? 86400);
|
|
91
|
+
const oldest = String(Math.floor(Date.now() / 1000) - oldestSec);
|
|
92
|
+
const results = [];
|
|
93
|
+
|
|
94
|
+
for (const { channel, intent: entryIntent } of channelEntries) {
|
|
95
|
+
const effectiveIntent = entryIntent ?? intent;
|
|
96
|
+
try {
|
|
97
|
+
const channelId = await this.#resolveChannelId(channel);
|
|
98
|
+
const items = await this.#listMessages(channelId, { limit, oldest });
|
|
99
|
+
results.push(...items.map((i) => ({ ...i, channelName: channel, intent: effectiveIntent })));
|
|
100
|
+
} catch (err) {
|
|
101
|
+
results.push({ type: 'error', source: 'slack', channel, message: err.message });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return results;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async #resolveChannelId(channelOrId) {
|
|
109
|
+
// Already looks like an ID (C... or D... or G...)
|
|
110
|
+
if (/^[A-Z][A-Z0-9]{8,}$/.test(channelOrId)) return channelOrId;
|
|
111
|
+
|
|
112
|
+
// Resolve by name
|
|
113
|
+
const name = channelOrId.replace(/^#/, '');
|
|
114
|
+
const data = await this.#apiGet('conversations.list', { limit: 200, exclude_archived: true });
|
|
115
|
+
const match = (data.channels ?? []).find(
|
|
116
|
+
(c) => c.name === name || c.name_normalized === name,
|
|
117
|
+
);
|
|
118
|
+
if (!match) throw new Error(`Slack channel not found: ${channelOrId}`);
|
|
119
|
+
return match.id;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async #listMessages(channelId, { limit, oldest }) {
|
|
123
|
+
const data = await this.#apiGet('conversations.history', {
|
|
124
|
+
channel: channelId,
|
|
125
|
+
limit,
|
|
126
|
+
oldest,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return (data.messages ?? [])
|
|
130
|
+
.filter((m) => m.type === 'message' && !m.subtype)
|
|
131
|
+
.map((m) => ({
|
|
132
|
+
type: 'message',
|
|
133
|
+
source: 'slack',
|
|
134
|
+
channel: channelId,
|
|
135
|
+
user: m.user ?? m.username ?? 'unknown',
|
|
136
|
+
text: m.text ?? '',
|
|
137
|
+
ts: m.ts,
|
|
138
|
+
threadTs: m.thread_ts ?? null,
|
|
139
|
+
replyCount: m.reply_count ?? 0,
|
|
140
|
+
reactions: (m.reactions ?? []).map((r) => `${r.name}:${r.count}`),
|
|
141
|
+
url: this.#teamId
|
|
142
|
+
? `https://app.slack.com/client/${this.#teamId}/${channelId}/p${m.ts.replace('.', '')}`
|
|
143
|
+
: null,
|
|
144
|
+
summary: `<${m.user ?? 'unknown'}> ${(m.text ?? '').slice(0, 80)}`,
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async #apiGet(method, params = {}) {
|
|
149
|
+
const qs = new URLSearchParams(
|
|
150
|
+
Object.entries(params).map(([k, v]) => [k, String(v)]),
|
|
151
|
+
).toString();
|
|
152
|
+
const controller = new AbortController();
|
|
153
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
154
|
+
try {
|
|
155
|
+
const res = await this.#fetchFn(`${SLACK_API}/${method}?${qs}`, {
|
|
156
|
+
headers: {
|
|
157
|
+
Authorization: `Bearer ${this.#token}`,
|
|
158
|
+
'Content-Type': 'application/json',
|
|
159
|
+
},
|
|
160
|
+
signal: controller.signal,
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok) throw new Error(`Slack HTTP ${res.status}`);
|
|
163
|
+
const json = await res.json();
|
|
164
|
+
if (!json.ok) throw new Error(`Slack API error: ${json.error ?? 'unknown'}`);
|
|
165
|
+
return json;
|
|
166
|
+
} finally {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Parse a channel entry that may carry an intent tag: "#name:intent" or "name".
|
|
174
|
+
* Returns { channel: string, intent: string }.
|
|
175
|
+
*/
|
|
176
|
+
function parseChannelEntry(raw) {
|
|
177
|
+
const s = String(raw).trim().replace(/^#/, '');
|
|
178
|
+
const colonIdx = s.lastIndexOf(':');
|
|
179
|
+
// Avoid splitting channel IDs like C12345678 — intents are alpha-only words
|
|
180
|
+
if (colonIdx > 0 && /^[a-z-]+$/.test(s.slice(colonIdx + 1))) {
|
|
181
|
+
return { channel: s.slice(0, colonIdx), intent: s.slice(colonIdx + 1) };
|
|
182
|
+
}
|
|
183
|
+
return { channel: s, intent: 'internal' };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Map an intent label to an observation category.
|
|
188
|
+
*/
|
|
189
|
+
export function intentToCategory(intent) {
|
|
190
|
+
switch ((intent ?? '').toLowerCase()) {
|
|
191
|
+
case 'risk': return 'anti-pattern';
|
|
192
|
+
case 'decision': return 'decision';
|
|
193
|
+
case 'how-to':
|
|
194
|
+
case 'howto': return 'pattern';
|
|
195
|
+
default: return 'insight';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resolveChannels(opts) {
|
|
200
|
+
if (Array.isArray(opts.channels)) return opts.channels.filter(Boolean).map(parseChannelEntry);
|
|
201
|
+
if (opts.channel) return [parseChannelEntry(opts.channel)];
|
|
202
|
+
return [];
|
|
203
|
+
}
|