@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,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/intent-classifier.mjs — LLM-augmented routing verification.
|
|
3
|
+
*
|
|
4
|
+
* Keyword classifiers in orchestration-policy.mjs match by substring, which
|
|
5
|
+
* structurally cannot distinguish "fundamentally about X" from "casually
|
|
6
|
+
* mentions X". The verifier asks a fast-tier model that question per
|
|
7
|
+
* flavor match. Dispatch trusts the keyword verdict so requests never wait
|
|
8
|
+
* on an LLM round-trip; the verifier fires in the background and writes
|
|
9
|
+
* its agreement record to `~/.cx/intent-verifications.jsonl` for offline
|
|
10
|
+
* analysis (replay later to find low-agreement flavors that need keyword
|
|
11
|
+
* rewording, or to re-introduce inline gating behind a confidence threshold
|
|
12
|
+
* tuned on real data).
|
|
13
|
+
*
|
|
14
|
+
* Public API:
|
|
15
|
+
* verifyIntent({ request, specialist, flavor, matchedKeywords, modelCaller })
|
|
16
|
+
* → { verified, confidence, reason, source, latencyMs }
|
|
17
|
+
* verifyRoute(route, { request, modelCaller, logger })
|
|
18
|
+
* → original route + { verificationsPending: <count> }; background
|
|
19
|
+
* verifications fire via the logger and do not block the caller
|
|
20
|
+
* resetCache() — for tests
|
|
21
|
+
*
|
|
22
|
+
* `modelCaller` and `logger` are dependency-injected for deterministic
|
|
23
|
+
* tests; defaults call the OpenRouter / Anthropic-direct chain mirrored
|
|
24
|
+
* from lib/schema-infer.mjs and append to the JSONL log.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import crypto from 'node:crypto';
|
|
28
|
+
import { readCurrentModels, readOpenRouterApiKeyFromOpenCodeConfig } from './model-router.mjs';
|
|
29
|
+
import { getUserEnvPath } from './env-config.mjs';
|
|
30
|
+
import { logIntentVerification } from './telemetry/intent-verifications.mjs';
|
|
31
|
+
|
|
32
|
+
export const INTENT_VERIFY_TIMEOUT_MS = 3000;
|
|
33
|
+
export const CONFIDENCE_THRESHOLD = 0.6;
|
|
34
|
+
|
|
35
|
+
const CACHE = new Map();
|
|
36
|
+
const CACHE_MAX = 256;
|
|
37
|
+
|
|
38
|
+
const SYSTEM_PROMPT = `You judge whether a keyword-based routing classifier match reflects the true intent of a request, or whether the keyword appears incidentally.
|
|
39
|
+
|
|
40
|
+
Respond with strict JSON only:
|
|
41
|
+
{"verified": <boolean>, "confidence": <number 0.0-1.0>, "reason": "<one short sentence>"}
|
|
42
|
+
|
|
43
|
+
Verified=true means the request is fundamentally about the candidate flavor. Verified=false means the keyword appears casually or the request is about something else.`;
|
|
44
|
+
|
|
45
|
+
function cacheKey({ request, specialist, flavor }) {
|
|
46
|
+
return crypto.createHash('sha1').update(`${specialist}|${flavor}|${request}`).digest('hex');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function cacheGet(key) {
|
|
50
|
+
const hit = CACHE.get(key);
|
|
51
|
+
if (!hit) return null;
|
|
52
|
+
CACHE.delete(key);
|
|
53
|
+
CACHE.set(key, hit);
|
|
54
|
+
return hit;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function cacheSet(key, value) {
|
|
58
|
+
CACHE.set(key, value);
|
|
59
|
+
if (CACHE.size > CACHE_MAX) {
|
|
60
|
+
const firstKey = CACHE.keys().next().value;
|
|
61
|
+
CACHE.delete(firstKey);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resetCache() {
|
|
66
|
+
CACHE.clear();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isApiKeyConfigured() {
|
|
70
|
+
if (process.env.ANTHROPIC_API_KEY) return true;
|
|
71
|
+
if (process.env.OPENROUTER_API_KEY) return true;
|
|
72
|
+
try {
|
|
73
|
+
if (readOpenRouterApiKeyFromOpenCodeConfig()) return true;
|
|
74
|
+
} catch { /* ignore */ }
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildUserContent({ request, specialist, flavor, matchedKeywords = [] }) {
|
|
79
|
+
return `Request: "${request}"
|
|
80
|
+
Matched specialist: ${specialist}
|
|
81
|
+
Candidate flavor: ${flavor}
|
|
82
|
+
Matched keywords: ${JSON.stringify(matchedKeywords)}
|
|
83
|
+
|
|
84
|
+
Is this request fundamentally about ${flavor}, or do the matched keywords appear incidentally?`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseJudgeOutput(text) {
|
|
88
|
+
const trimmed = (text || '').trim();
|
|
89
|
+
const jsonStart = trimmed.indexOf('{');
|
|
90
|
+
const jsonEnd = trimmed.lastIndexOf('}');
|
|
91
|
+
if (jsonStart < 0 || jsonEnd <= jsonStart) {
|
|
92
|
+
throw new Error('No JSON object in judge output');
|
|
93
|
+
}
|
|
94
|
+
const parsed = JSON.parse(trimmed.slice(jsonStart, jsonEnd + 1));
|
|
95
|
+
if (typeof parsed.verified !== 'boolean') throw new Error('verified must be boolean');
|
|
96
|
+
const confidence = Number(parsed.confidence);
|
|
97
|
+
if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
|
|
98
|
+
throw new Error('confidence must be number in [0,1]');
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
verified: parsed.verified,
|
|
102
|
+
confidence,
|
|
103
|
+
reason: String(parsed.reason || '').slice(0, 240),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function fetchWithTimeout(url, options, timeoutMs = INTENT_VERIFY_TIMEOUT_MS) {
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
110
|
+
try {
|
|
111
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
112
|
+
} finally {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function defaultModelCaller({ system, user }) {
|
|
118
|
+
const orKey = process.env.OPENROUTER_API_KEY || readOpenRouterApiKeyFromOpenCodeConfig();
|
|
119
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
120
|
+
if (!orKey && !anthropicKey) throw new Error('no model api key configured');
|
|
121
|
+
|
|
122
|
+
const envPath = getUserEnvPath();
|
|
123
|
+
const configured = readCurrentModels(envPath, {});
|
|
124
|
+
const modelId = configured.fast || 'anthropic/claude-haiku-4-5-20251001';
|
|
125
|
+
const isAnthropicDirect = /^anthropic\//.test(modelId);
|
|
126
|
+
|
|
127
|
+
if (isAnthropicDirect && anthropicKey) {
|
|
128
|
+
const res = await fetchWithTimeout('https://api.anthropic.com/v1/messages', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: {
|
|
131
|
+
'x-api-key': anthropicKey,
|
|
132
|
+
'anthropic-version': '2023-06-01',
|
|
133
|
+
'content-type': 'application/json',
|
|
134
|
+
},
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
model: modelId.replace(/^anthropic\//, ''),
|
|
137
|
+
max_tokens: 256,
|
|
138
|
+
system,
|
|
139
|
+
messages: [{ role: 'user', content: user }],
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
if (!res.ok) throw new Error(`anthropic ${res.status}`);
|
|
143
|
+
const data = await res.json();
|
|
144
|
+
return data.content?.[0]?.text || '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (orKey) {
|
|
148
|
+
const res = await fetchWithTimeout('https://openrouter.ai/api/v1/chat/completions', {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: {
|
|
151
|
+
Authorization: `Bearer ${orKey}`,
|
|
152
|
+
'content-type': 'application/json',
|
|
153
|
+
'HTTP-Referer': 'https://github.com/construct',
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
model: modelId.replace(/^openrouter\//, ''),
|
|
157
|
+
max_tokens: 256,
|
|
158
|
+
messages: [
|
|
159
|
+
{ role: 'system', content: system },
|
|
160
|
+
{ role: 'user', content: user },
|
|
161
|
+
],
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
if (!res.ok) throw new Error(`openrouter ${res.status}`);
|
|
165
|
+
const data = await res.json();
|
|
166
|
+
return data.choices?.[0]?.message?.content || '';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
throw new Error('no usable model configured');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function verifyIntent({ request, specialist, flavor, matchedKeywords = [], modelCaller = defaultModelCaller } = {}) {
|
|
173
|
+
if (!flavor) {
|
|
174
|
+
return { verified: true, confidence: 1, source: 'no-flavor', reason: '', latencyMs: 0 };
|
|
175
|
+
}
|
|
176
|
+
if (process.env.CONSTRUCT_INTENT_VERIFY === 'off') {
|
|
177
|
+
return { verified: true, confidence: 1, source: 'disabled', reason: 'env opt-out', latencyMs: 0 };
|
|
178
|
+
}
|
|
179
|
+
if (modelCaller === defaultModelCaller && !isApiKeyConfigured()) {
|
|
180
|
+
return { verified: true, confidence: 1, source: 'fallback', reason: 'no model api key', latencyMs: 0 };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const key = cacheKey({ request, specialist, flavor });
|
|
184
|
+
const cached = cacheGet(key);
|
|
185
|
+
if (cached) return { ...cached, source: 'cache' };
|
|
186
|
+
|
|
187
|
+
const start = Date.now();
|
|
188
|
+
try {
|
|
189
|
+
const text = await modelCaller({
|
|
190
|
+
system: SYSTEM_PROMPT,
|
|
191
|
+
user: buildUserContent({ request, specialist, flavor, matchedKeywords }),
|
|
192
|
+
});
|
|
193
|
+
const parsed = parseJudgeOutput(text);
|
|
194
|
+
const record = { ...parsed, source: 'llm', latencyMs: Date.now() - start };
|
|
195
|
+
cacheSet(key, record);
|
|
196
|
+
return record;
|
|
197
|
+
} catch (err) {
|
|
198
|
+
return {
|
|
199
|
+
verified: true,
|
|
200
|
+
confidence: 1,
|
|
201
|
+
source: 'fallback',
|
|
202
|
+
reason: `intent-verify error: ${err.message}`,
|
|
203
|
+
latencyMs: Date.now() - start,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Map keyword classifier → matched keywords for explainability in the prompt.
|
|
209
|
+
// Kept here (not in orchestration-policy) so the classifier module stays
|
|
210
|
+
// the canonical source of "what does verified mean for this role."
|
|
211
|
+
const ROLE_PROMPT_KEYWORDS = {
|
|
212
|
+
productManager: ['platform', 'enterprise', 'ai-product', 'growth', 'go-to-market'],
|
|
213
|
+
architect: ['ai-systems', 'integration', 'data model', 'enterprise', 'platform'],
|
|
214
|
+
qa: ['ai-eval', 'api-contract', 'data-pipeline', 'web-ui'],
|
|
215
|
+
security: ['ai', 'privacy', 'supply-chain', 'cloud', 'appsec'],
|
|
216
|
+
dataAnalyst: ['product-intelligence', 'experiment', 'telemetry', 'product-metrics'],
|
|
217
|
+
dataEngineer: ['vector-retrieval', 'warehouse', 'pipeline'],
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export function verifyRoute(route, { request, modelCaller = defaultModelCaller, logger = logIntentVerification } = {}) {
|
|
221
|
+
if (!route || !route.roleFlavors) return route;
|
|
222
|
+
|
|
223
|
+
let pending = 0;
|
|
224
|
+
for (const [role, flavor] of Object.entries(route.roleFlavors)) {
|
|
225
|
+
if (!flavor) continue;
|
|
226
|
+
pending += 1;
|
|
227
|
+
const specialist = `cx-${role.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
|
|
228
|
+
verifyIntent({
|
|
229
|
+
request,
|
|
230
|
+
specialist,
|
|
231
|
+
flavor,
|
|
232
|
+
matchedKeywords: ROLE_PROMPT_KEYWORDS[role] || [],
|
|
233
|
+
modelCaller,
|
|
234
|
+
})
|
|
235
|
+
.then((result) => {
|
|
236
|
+
logger({
|
|
237
|
+
request,
|
|
238
|
+
specialist,
|
|
239
|
+
flavor,
|
|
240
|
+
keywordVerdict: true,
|
|
241
|
+
llmVerdict: result.verified,
|
|
242
|
+
agreed: result.verified,
|
|
243
|
+
confidence: result.confidence,
|
|
244
|
+
reason: result.reason,
|
|
245
|
+
source: result.source,
|
|
246
|
+
latencyMs: result.latencyMs,
|
|
247
|
+
});
|
|
248
|
+
})
|
|
249
|
+
.catch(() => { /* fire and forget; logger swallows its own errors */ });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { ...route, verificationsPending: pending };
|
|
253
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/knowledge/layout.mjs — canonical .cx/knowledge/ directory layout.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the typed knowledge subdirectories. Import this
|
|
5
|
+
* from inbox.mjs, document-ingest.mjs, and anywhere that writes into .cx/.
|
|
6
|
+
*
|
|
7
|
+
* Directory structure:
|
|
8
|
+
* .cx/knowledge/
|
|
9
|
+
* internal/ — team notes, meeting minutes, internal specs, ADRs, PRDs
|
|
10
|
+
* external/ — customer feedback, support tickets, external research
|
|
11
|
+
* decisions/ — architecture decision records, design decisions
|
|
12
|
+
* how-tos/ — runbooks, setup guides, operational procedures, playbooks
|
|
13
|
+
* reference/ — specs, RFCs, architecture docs, schemas, API references
|
|
14
|
+
*
|
|
15
|
+
* Inbox / ingest target names map 1-to-1 to subdirectory names.
|
|
16
|
+
* Use KNOWLEDGE_SUBDIRS to enumerate all valid targets.
|
|
17
|
+
* Use knowledgeDirForCategory() to pick the right subdir from an obs category.
|
|
18
|
+
* Use inferKnowledgeTarget() to pick the right subdir from a file path.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const KNOWLEDGE_ROOT = '.cx/knowledge';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* All valid knowledge subdirectory names.
|
|
25
|
+
* These are the values accepted by --target=knowledge/<name> in the ingest CLI.
|
|
26
|
+
*/
|
|
27
|
+
export const KNOWLEDGE_SUBDIRS = /** @type {const} */ ([
|
|
28
|
+
'internal',
|
|
29
|
+
'external',
|
|
30
|
+
'decisions',
|
|
31
|
+
'how-tos',
|
|
32
|
+
'reference',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Map an observation category to the knowledge subdir that should store it.
|
|
37
|
+
* @param {string} category
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
export function knowledgeDirForCategory(category) {
|
|
41
|
+
switch (category) {
|
|
42
|
+
case 'decision': return 'decisions';
|
|
43
|
+
case 'anti-pattern': return 'internal';
|
|
44
|
+
case 'pattern': return 'how-tos';
|
|
45
|
+
default: return 'internal';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Infer the best knowledge subdir from a file path using naming conventions.
|
|
51
|
+
*
|
|
52
|
+
* Conventions (case-insensitive):
|
|
53
|
+
* ADR / architecture-decision → decisions/
|
|
54
|
+
* postmortem / incident / rca → internal/ (incident records)
|
|
55
|
+
* spec / rfc / schema / api-ref → reference/
|
|
56
|
+
* runbook / playbook / how-to / setup / guide → how-tos/
|
|
57
|
+
* customer / feedback / support / external → external/
|
|
58
|
+
* everything else → internal/
|
|
59
|
+
*
|
|
60
|
+
* @param {string} filePath
|
|
61
|
+
* @returns {string} one of KNOWLEDGE_SUBDIRS
|
|
62
|
+
*/
|
|
63
|
+
export function inferKnowledgeTarget(filePath) {
|
|
64
|
+
const name = filePath.toLowerCase();
|
|
65
|
+
|
|
66
|
+
if (/\badr[-_]\d/.test(name) || /architecture.decision/.test(name)) return 'decisions';
|
|
67
|
+
if (/\bspec\b|\brfc\b|\bschema\b|\bapi.ref/.test(name)) return 'reference';
|
|
68
|
+
if (/\brunbook\b|\bplaybook\b|\bhow.to\b|\bsetup\b|\bguide\b/.test(name)) return 'how-tos';
|
|
69
|
+
if (/\bcustomer\b|\bfeedback\b|\bsupport\b|\bexternal\b/.test(name)) return 'external';
|
|
70
|
+
if (/\bpost.?mortem\b|\bincident\b|\brca\b/.test(name)) return 'internal';
|
|
71
|
+
return 'internal';
|
|
72
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/knowledge/rag.mjs — Retrieval-Augmented Generation pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Indexes all knowledge sources into a unified corpus and answers natural-
|
|
5
|
+
* language queries by retrieving the most relevant chunks, then synthesising
|
|
6
|
+
* a response via the `claude` CLI.
|
|
7
|
+
*
|
|
8
|
+
* Sources indexed:
|
|
9
|
+
* - Observations (.cx/observations/)
|
|
10
|
+
* - Artifacts (docs/adr/, docs/prd/, docs/rfc/)
|
|
11
|
+
* - Snapshots (.cx/snapshot.md + any configured output paths)
|
|
12
|
+
* - Approval queue (.cx/approval-queue.jsonl)
|
|
13
|
+
*
|
|
14
|
+
* Retrieval strategy:
|
|
15
|
+
* Hybrid BM25 + cosine similarity (hashing-bow-v1 embeddings, zero deps).
|
|
16
|
+
* Top-K chunks from each source are merged, deduplicated by id, and re-ranked
|
|
17
|
+
* by a combined score before being assembled into a prompt context window.
|
|
18
|
+
*
|
|
19
|
+
* Context budget:
|
|
20
|
+
* MAX_CONTEXT_CHARS limits total text sent to the model. Chunks are trimmed
|
|
21
|
+
* to fit. The budget is intentionally conservative so the answer fits in a
|
|
22
|
+
* single claude --print call.
|
|
23
|
+
*
|
|
24
|
+
* Zero external deps — uses only the existing embeddings primitives.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { spawnSync } from 'node:child_process';
|
|
30
|
+
import { cosineSimilarity, rankByBm25 } from '../storage/embeddings.mjs';
|
|
31
|
+
import { embedSync as embedText } from '../storage/embeddings-legacy.mjs';
|
|
32
|
+
import { listObservations, getObservation } from '../observation-store.mjs';
|
|
33
|
+
import { listArtifacts } from '../embed/artifact.mjs';
|
|
34
|
+
|
|
35
|
+
const MAX_CONTEXT_CHARS = 12_000;
|
|
36
|
+
const MAX_CHUNKS = 12;
|
|
37
|
+
const CHUNK_PREVIEW = 600;
|
|
38
|
+
|
|
39
|
+
// MIN_SCORE under RRF is a soft floor: with k=60 a top-1 doc in both rankers
|
|
40
|
+
// scores ~0.033, so any positive score means the document appeared in at
|
|
41
|
+
// least one ranker. We keep a tiny epsilon to drop chunks that scored 0
|
|
42
|
+
// from BOTH rankers (which can happen for items not in either ranked list).
|
|
43
|
+
const MIN_SCORE = 1e-6;
|
|
44
|
+
|
|
45
|
+
// ── Source loaders ─────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load all observations as indexable chunks.
|
|
49
|
+
*/
|
|
50
|
+
function loadObservationChunks(rootDir) {
|
|
51
|
+
try {
|
|
52
|
+
const entries = listObservations(rootDir, {});
|
|
53
|
+
return entries.map((e) => {
|
|
54
|
+
const full = getObservation(rootDir, e.id);
|
|
55
|
+
return {
|
|
56
|
+
id: `obs:${e.id}`,
|
|
57
|
+
source: 'observation',
|
|
58
|
+
title: e.summary || 'Observation',
|
|
59
|
+
body: [full?.content, full?.summary].filter(Boolean).join('\n'),
|
|
60
|
+
tags: e.tags || [],
|
|
61
|
+
role: e.role || null,
|
|
62
|
+
category: e.category || null,
|
|
63
|
+
createdAt: e.createdAt || null,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load markdown files from a directory tree as indexable chunks.
|
|
73
|
+
*/
|
|
74
|
+
function loadMarkdownChunks(dir, source) {
|
|
75
|
+
if (!fs.existsSync(dir)) return [];
|
|
76
|
+
const chunks = [];
|
|
77
|
+
const walk = (d) => {
|
|
78
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
79
|
+
const full = path.join(d, entry.name);
|
|
80
|
+
if (entry.isDirectory()) { walk(full); continue; }
|
|
81
|
+
if (!entry.name.endsWith('.md')) continue;
|
|
82
|
+
try {
|
|
83
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
84
|
+
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
85
|
+
chunks.push({
|
|
86
|
+
id: `${source}:${path.relative(process.cwd(), full)}`,
|
|
87
|
+
source,
|
|
88
|
+
title: titleMatch ? titleMatch[1].trim() : entry.name,
|
|
89
|
+
body: content,
|
|
90
|
+
filePath: full,
|
|
91
|
+
createdAt: fs.statSync(full).mtime.toISOString(),
|
|
92
|
+
});
|
|
93
|
+
} catch { /* skip unreadable */ }
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
walk(dir);
|
|
97
|
+
return chunks;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Load artifact docs (ADR, PRD, RFC) from docs/.
|
|
102
|
+
*/
|
|
103
|
+
function loadArtifactChunks(rootDir) {
|
|
104
|
+
const chunks = [];
|
|
105
|
+
for (const subdir of ['docs/adr', 'docs/prd', 'docs/rfc', 'docs/concepts/architecture.md']) {
|
|
106
|
+
const full = path.resolve(rootDir, subdir);
|
|
107
|
+
if (subdir.endsWith('.md')) {
|
|
108
|
+
if (!fs.existsSync(full)) continue;
|
|
109
|
+
try {
|
|
110
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
111
|
+
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
112
|
+
chunks.push({
|
|
113
|
+
id: `artifact:${subdir}`,
|
|
114
|
+
source: 'artifact',
|
|
115
|
+
title: titleMatch ? titleMatch[1].trim() : subdir,
|
|
116
|
+
body: content,
|
|
117
|
+
filePath: full,
|
|
118
|
+
createdAt: fs.statSync(full).mtime.toISOString(),
|
|
119
|
+
});
|
|
120
|
+
} catch { /* skip */ }
|
|
121
|
+
} else {
|
|
122
|
+
chunks.push(...loadMarkdownChunks(full, 'artifact'));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return chunks;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Load snapshot markdown files.
|
|
130
|
+
*/
|
|
131
|
+
function loadSnapshotChunks(rootDir) {
|
|
132
|
+
const candidates = [
|
|
133
|
+
path.resolve(rootDir, '.cx/snapshot.md'),
|
|
134
|
+
path.resolve(rootDir, '.cx/snapshots'),
|
|
135
|
+
];
|
|
136
|
+
const chunks = [];
|
|
137
|
+
for (const c of candidates) {
|
|
138
|
+
if (!fs.existsSync(c)) continue;
|
|
139
|
+
if (fs.statSync(c).isDirectory()) {
|
|
140
|
+
chunks.push(...loadMarkdownChunks(c, 'snapshot'));
|
|
141
|
+
} else {
|
|
142
|
+
try {
|
|
143
|
+
const content = fs.readFileSync(c, 'utf8');
|
|
144
|
+
chunks.push({
|
|
145
|
+
id: 'snapshot:.cx/snapshot.md',
|
|
146
|
+
source: 'snapshot',
|
|
147
|
+
title: 'Latest Snapshot',
|
|
148
|
+
body: content,
|
|
149
|
+
filePath: c,
|
|
150
|
+
createdAt: fs.statSync(c).mtime.toISOString(),
|
|
151
|
+
});
|
|
152
|
+
} catch { /* skip */ }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return chunks;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Index builder ──────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build a full in-memory corpus from all sources.
|
|
162
|
+
* Each chunk gets an embedding vector for cosine scoring.
|
|
163
|
+
*/
|
|
164
|
+
export function buildCorpus(rootDir = process.cwd()) {
|
|
165
|
+
const chunks = [
|
|
166
|
+
...loadObservationChunks(rootDir),
|
|
167
|
+
...loadArtifactChunks(rootDir),
|
|
168
|
+
...loadSnapshotChunks(rootDir),
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
// Embed each chunk
|
|
172
|
+
return chunks.map((chunk) => ({
|
|
173
|
+
...chunk,
|
|
174
|
+
embedding: embedText(`${chunk.title} ${chunk.body}`.slice(0, 2000)),
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Retrieval ──────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Retrieve the top-K most relevant chunks for a query.
|
|
182
|
+
*
|
|
183
|
+
* Pipeline:
|
|
184
|
+
* 1. BM25 ranks the FULL corpus (no top-K window so chunks outside any
|
|
185
|
+
* bounded slice still contribute).
|
|
186
|
+
* 2. Cosine ranks the FULL corpus.
|
|
187
|
+
* 3. RRF fuses the two ranked lists into one (corpus-agnostic; rank-only).
|
|
188
|
+
* 4. MMR reranks the fused candidates with λ=0.7 to drop near-duplicates
|
|
189
|
+
* while preserving relevance.
|
|
190
|
+
*
|
|
191
|
+
* Both fuser and reranker are pluggable via the engine layer
|
|
192
|
+
* (lib/engine/index.mjs); callers can swap RRF for any other Fuser plugin
|
|
193
|
+
* and MMR for any other Reranker plugin without touching this module. The
|
|
194
|
+
* defaults are wired in lib/engine/defaults.mjs.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} query
|
|
197
|
+
* @param {object[]} corpus — from buildCorpus()
|
|
198
|
+
* @param {object} [opts]
|
|
199
|
+
* @returns {Promise<object[]>} top chunks with `.score` (RRF) and `.mmrScore`,
|
|
200
|
+
* sorted by MMR order. Returns a Promise so engine-aligned chunkers and
|
|
201
|
+
* embedders can plug in without changing the call surface.
|
|
202
|
+
*/
|
|
203
|
+
export async function retrieve(query, corpus, { topK = MAX_CHUNKS, minScore = MIN_SCORE } = {}) {
|
|
204
|
+
if (!query || corpus.length === 0) return [];
|
|
205
|
+
|
|
206
|
+
const queryEmbedding = embedText(query);
|
|
207
|
+
|
|
208
|
+
const bm25Ranked = rankByBm25(
|
|
209
|
+
corpus.map((c) => ({ ...c, text: `${c.title} ${c.body}` })),
|
|
210
|
+
query,
|
|
211
|
+
{ limit: corpus.length },
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const cosineRanked = corpus
|
|
215
|
+
.map((chunk) => ({
|
|
216
|
+
...chunk,
|
|
217
|
+
score: cosineSimilarity(queryEmbedding, chunk.embedding || []),
|
|
218
|
+
}))
|
|
219
|
+
.filter((c) => c.score > 0)
|
|
220
|
+
.sort((a, b) => b.score - a.score);
|
|
221
|
+
|
|
222
|
+
const { getEngine } = await import('../engine/index.mjs');
|
|
223
|
+
const engine = await getEngine({ rootDir: process.cwd() });
|
|
224
|
+
const fused = engine.layers.fuser.fuse({ bm25: bm25Ranked, cosine: cosineRanked });
|
|
225
|
+
|
|
226
|
+
const filtered = fused.filter((c) => c.score >= minScore);
|
|
227
|
+
if (filtered.length === 0) return [];
|
|
228
|
+
|
|
229
|
+
const reranked = await engine.layers.reranker.rerank(query, filtered, {
|
|
230
|
+
queryEmbedding,
|
|
231
|
+
topK,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return reranked;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Context assembly ───────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Format retrieved chunks into a context block for the prompt.
|
|
241
|
+
*/
|
|
242
|
+
export function assembleContext(chunks) {
|
|
243
|
+
let budget = MAX_CONTEXT_CHARS;
|
|
244
|
+
const parts = [];
|
|
245
|
+
|
|
246
|
+
for (const chunk of chunks) {
|
|
247
|
+
const preview = chunk.body?.slice(0, CHUNK_PREVIEW) || '';
|
|
248
|
+
const meta = [
|
|
249
|
+
chunk.source && `source:${chunk.source}`,
|
|
250
|
+
chunk.role && `role:${chunk.role}`,
|
|
251
|
+
chunk.category && `category:${chunk.category}`,
|
|
252
|
+
chunk.createdAt && `date:${chunk.createdAt.slice(0, 10)}`,
|
|
253
|
+
].filter(Boolean).join(' ');
|
|
254
|
+
|
|
255
|
+
const block = `### ${chunk.title}\n${meta ? `_${meta}_\n` : ''}${preview}${preview.length === CHUNK_PREVIEW ? '\n…' : ''}`;
|
|
256
|
+
if (block.length > budget) break;
|
|
257
|
+
parts.push(block);
|
|
258
|
+
budget -= block.length;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return parts.join('\n\n---\n\n');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Answer synthesis ───────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
const SYSTEM_PROMPT = `You are Construct's knowledge assistant. Answer questions about the project using only the provided context.
|
|
267
|
+
- Be specific and cite which source you drew from (observation, artifact, snapshot).
|
|
268
|
+
- If the context is insufficient, say so directly — do not speculate.
|
|
269
|
+
- Keep answers concise (under 400 words unless the question demands detail).
|
|
270
|
+
- Format as plain text unless the question explicitly asks for markdown.`;
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Ask a question against the knowledge base.
|
|
274
|
+
*
|
|
275
|
+
* @param {string} question
|
|
276
|
+
* @param {object} opts
|
|
277
|
+
* @param {string} opts.rootDir
|
|
278
|
+
* @param {object[]} [opts.corpus] — pre-built corpus (skips rebuild)
|
|
279
|
+
* @param {boolean} [opts.dryRun] — return retrieved chunks without calling claude
|
|
280
|
+
* @returns {{ answer: string, sources: object[], query: string }}
|
|
281
|
+
*/
|
|
282
|
+
export async function ask(question, { rootDir = process.cwd(), corpus, dryRun = false } = {}) {
|
|
283
|
+
const kb = corpus ?? buildCorpus(rootDir);
|
|
284
|
+
const chunks = await retrieve(question, kb);
|
|
285
|
+
|
|
286
|
+
if (dryRun) {
|
|
287
|
+
return {
|
|
288
|
+
answer: null,
|
|
289
|
+
sources: chunks.map((c) => ({ id: c.id, source: c.source, title: c.title, score: c.score })),
|
|
290
|
+
query: question,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (chunks.length === 0) {
|
|
295
|
+
return {
|
|
296
|
+
answer: 'No relevant information found in the knowledge base for this query.',
|
|
297
|
+
sources: [],
|
|
298
|
+
query: question,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const context = assembleContext(chunks);
|
|
303
|
+
const prompt = `${SYSTEM_PROMPT}\n\n## Knowledge Base Context\n\n${context}\n\n## Question\n\n${question}\n\n## Answer`;
|
|
304
|
+
|
|
305
|
+
// Call claude CLI
|
|
306
|
+
const result = spawnSync('claude', ['--print', prompt], {
|
|
307
|
+
encoding: 'utf8',
|
|
308
|
+
timeout: 60_000,
|
|
309
|
+
env: { ...process.env },
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (result.error || result.status !== 0) {
|
|
313
|
+
// Fallback: return retrieved chunks as a structured answer
|
|
314
|
+
const fallback = chunks
|
|
315
|
+
.slice(0, 5)
|
|
316
|
+
.map((c) => `**${c.title}** (${c.source})\n${c.body?.slice(0, 300) || ''}`)
|
|
317
|
+
.join('\n\n');
|
|
318
|
+
return {
|
|
319
|
+
answer: `[Claude CLI unavailable — showing retrieved context]\n\n${fallback}`,
|
|
320
|
+
sources: chunks.map((c) => ({ id: c.id, source: c.source, title: c.title, score: c.score })),
|
|
321
|
+
query: question,
|
|
322
|
+
cliMissing: true,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
answer: (result.stdout || '').trim(),
|
|
328
|
+
sources: chunks.map((c) => ({ id: c.id, source: c.source, title: c.title, score: c.score })),
|
|
329
|
+
query: question,
|
|
330
|
+
};
|
|
331
|
+
}
|