@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,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/intake/filesystem-queue.mjs — filesystem adapter for the IntakeQueue interface.
|
|
3
|
+
*
|
|
4
|
+
* Backs the `.cx/intake/{pending,processed,skipped}/` layout used in solo
|
|
5
|
+
* mode. Implements the contract defined in lib/intake/queue.mjs: enqueue,
|
|
6
|
+
* listPending, read, markProcessed, markSkipped, reopen, count. The
|
|
7
|
+
* Postgres adapter (for team and enterprise modes) lives alongside and
|
|
8
|
+
* implements the same shape so callers do not branch on backend.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
const QUEUE_SUBDIR = '.cx/intake';
|
|
15
|
+
|
|
16
|
+
export function queueRoot(rootDir) {
|
|
17
|
+
return path.join(rootDir, QUEUE_SUBDIR);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function pendingDir(rootDir) {
|
|
21
|
+
return path.join(queueRoot(rootDir), 'pending');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function processedDir(rootDir) {
|
|
25
|
+
return path.join(queueRoot(rootDir), 'processed');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function skippedDir(rootDir) {
|
|
29
|
+
return path.join(queueRoot(rootDir), 'skipped');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function slugify(value) {
|
|
33
|
+
return String(value || 'untitled')
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
36
|
+
.replace(/^-+|-+$/g, '')
|
|
37
|
+
.slice(0, 60) || 'untitled';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let counter = 0;
|
|
41
|
+
function timestamp() {
|
|
42
|
+
// Milliseconds + a per-process counter to guarantee uniqueness even when
|
|
43
|
+
// two enqueues fire in the same millisecond.
|
|
44
|
+
counter = (counter + 1) % 1000;
|
|
45
|
+
const c = String(counter).padStart(3, '0');
|
|
46
|
+
return `${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 23)}-${c}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class FilesystemIntakeQueue {
|
|
50
|
+
constructor(rootDir) {
|
|
51
|
+
if (!rootDir) throw new Error('FilesystemIntakeQueue: rootDir is required');
|
|
52
|
+
this.rootDir = rootDir;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
enqueue(entry) {
|
|
56
|
+
if (!entry?.intake?.sourcePath) throw new Error('enqueue: entry.intake.sourcePath is required');
|
|
57
|
+
const dir = pendingDir(this.rootDir);
|
|
58
|
+
mkdirSync(dir, { recursive: true });
|
|
59
|
+
|
|
60
|
+
const ts = timestamp();
|
|
61
|
+
const slug = slugify(path.basename(entry.intake.sourcePath, path.extname(entry.intake.sourcePath)));
|
|
62
|
+
const id = `${ts}-${slug}`;
|
|
63
|
+
const filePath = path.join(dir, `${id}.json`);
|
|
64
|
+
|
|
65
|
+
const payload = { id, createdAt: new Date().toISOString(), status: 'pending', ...entry };
|
|
66
|
+
writeFileSync(filePath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
67
|
+
return { id, filePath };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
listPending() {
|
|
71
|
+
const dir = pendingDir(this.rootDir);
|
|
72
|
+
if (!existsSync(dir)) return [];
|
|
73
|
+
return readdirSync(dir)
|
|
74
|
+
.filter((name) => name.endsWith('.json'))
|
|
75
|
+
.map((name) => {
|
|
76
|
+
const filePath = path.join(dir, name);
|
|
77
|
+
try {
|
|
78
|
+
const data = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
79
|
+
return { ...data, filePath };
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.sort((a, b) => (a.createdAt || '').localeCompare(b.createdAt || ''));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
count() {
|
|
89
|
+
const dir = pendingDir(this.rootDir);
|
|
90
|
+
if (!existsSync(dir)) return 0;
|
|
91
|
+
return readdirSync(dir).filter((name) => name.endsWith('.json')).length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
read(id) {
|
|
95
|
+
for (const dir of [pendingDir(this.rootDir), processedDir(this.rootDir), skippedDir(this.rootDir)]) {
|
|
96
|
+
const filePath = path.join(dir, `${id}.json`);
|
|
97
|
+
if (existsSync(filePath)) {
|
|
98
|
+
const data = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
99
|
+
return { ...data, filePath };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
markProcessed(id, { processedBy = 'unknown', notes = '' } = {}) {
|
|
106
|
+
const src = path.join(pendingDir(this.rootDir), `${id}.json`);
|
|
107
|
+
if (!existsSync(src)) throw new Error(`markProcessed: no pending entry ${id}`);
|
|
108
|
+
const data = JSON.parse(readFileSync(src, 'utf8'));
|
|
109
|
+
data.status = 'processed';
|
|
110
|
+
data.processedAt = new Date().toISOString();
|
|
111
|
+
data.processedBy = processedBy;
|
|
112
|
+
if (notes) data.notes = notes;
|
|
113
|
+
|
|
114
|
+
const dst = path.join(processedDir(this.rootDir), `${id}.json`);
|
|
115
|
+
mkdirSync(path.dirname(dst), { recursive: true });
|
|
116
|
+
writeFileSync(dst, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
117
|
+
rmSync(src);
|
|
118
|
+
return { id, filePath: dst };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
markSkipped(id, { skippedBy = 'unknown', reason = '' } = {}) {
|
|
122
|
+
const src = path.join(pendingDir(this.rootDir), `${id}.json`);
|
|
123
|
+
if (!existsSync(src)) throw new Error(`markSkipped: no pending entry ${id}`);
|
|
124
|
+
const data = JSON.parse(readFileSync(src, 'utf8'));
|
|
125
|
+
data.status = 'skipped';
|
|
126
|
+
data.skippedAt = new Date().toISOString();
|
|
127
|
+
data.skippedBy = skippedBy;
|
|
128
|
+
if (reason) data.reason = reason;
|
|
129
|
+
|
|
130
|
+
const dst = path.join(skippedDir(this.rootDir), `${id}.json`);
|
|
131
|
+
mkdirSync(path.dirname(dst), { recursive: true });
|
|
132
|
+
writeFileSync(dst, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
133
|
+
rmSync(src);
|
|
134
|
+
return { id, filePath: dst };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
reopen(id) {
|
|
138
|
+
for (const dir of [processedDir(this.rootDir), skippedDir(this.rootDir)]) {
|
|
139
|
+
const src = path.join(dir, `${id}.json`);
|
|
140
|
+
if (!existsSync(src)) continue;
|
|
141
|
+
const data = JSON.parse(readFileSync(src, 'utf8'));
|
|
142
|
+
data.status = 'pending';
|
|
143
|
+
delete data.processedAt;
|
|
144
|
+
delete data.processedBy;
|
|
145
|
+
delete data.notes;
|
|
146
|
+
delete data.skippedAt;
|
|
147
|
+
delete data.skippedBy;
|
|
148
|
+
delete data.reason;
|
|
149
|
+
|
|
150
|
+
const dst = path.join(pendingDir(this.rootDir), `${id}.json`);
|
|
151
|
+
mkdirSync(path.dirname(dst), { recursive: true });
|
|
152
|
+
writeFileSync(dst, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
153
|
+
rmSync(src);
|
|
154
|
+
return { id, filePath: dst, from: path.basename(dir) };
|
|
155
|
+
}
|
|
156
|
+
throw new Error(`reopen: no processed or skipped entry ${id}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/intake/intake-config.mjs — user-facing intake watcher config.
|
|
3
|
+
*
|
|
4
|
+
* Lets the user define which directories the inbox watcher scans and how
|
|
5
|
+
* deep it descends through subdirectories. Persists to
|
|
6
|
+
* `<rootDir>/.cx/intake-config.json` and merges with env-driven defaults so
|
|
7
|
+
* CLI, dashboard, and process env all converge on the same answer.
|
|
8
|
+
*
|
|
9
|
+
* Schema:
|
|
10
|
+
* {
|
|
11
|
+
* parentDirs: string[] // absolute or rootDir-relative
|
|
12
|
+
* maxDepth: number // 0 = only the parent dir, no subdirs
|
|
13
|
+
* includeProjectInbox: boolean // always include <rootDir>/.cx/inbox
|
|
14
|
+
* includeDocsIntake: boolean // include <rootDir>/docs/intake when present
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* maxDepth guidance (surfaced verbatim in the dashboard):
|
|
18
|
+
* 0 — only files directly inside the parent dir
|
|
19
|
+
* 1 — parent dir + its immediate subdirs
|
|
20
|
+
* 2 — two levels of subdirs (a common default for project intake roots)
|
|
21
|
+
* 3+ — deep scans; use with care on large trees
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
26
|
+
import { shouldCreateCx } from '../project-detection.mjs';
|
|
27
|
+
|
|
28
|
+
export const INTAKE_CONFIG_FILE = '.cx/intake-config.json';
|
|
29
|
+
export const INTAKE_DEFAULT_MAX_DEPTH = 4;
|
|
30
|
+
export const INTAKE_HARD_MAX_DEPTH = 16;
|
|
31
|
+
|
|
32
|
+
export const INTAKE_DEPTH_GUIDANCE = [
|
|
33
|
+
{ value: 0, label: 'Only this directory', detail: 'Scans files directly in the parent dir, ignores all subdirs.' },
|
|
34
|
+
{ value: 1, label: 'One level deep', detail: 'Parent dir plus its immediate subdirs (e.g. parent/intake/file.md).' },
|
|
35
|
+
{ value: 2, label: 'Two levels deep', detail: 'Parent and two subdirs. A reasonable default for organized intake roots.' },
|
|
36
|
+
{ value: 4, label: 'Four levels (default)', detail: 'Catches most nested layouts without scanning huge trees.' },
|
|
37
|
+
{ value: 8, label: 'Deep scan', detail: 'Useful for archives. Slower; skip if the parent contains build output.' },
|
|
38
|
+
{ value: INTAKE_HARD_MAX_DEPTH, label: 'Unlimited (capped)', detail: `Walks up to ${INTAKE_HARD_MAX_DEPTH} levels — effectively unlimited. May be slow.` },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export const DEFAULT_INTAKE_CONFIG = Object.freeze({
|
|
42
|
+
parentDirs: [],
|
|
43
|
+
maxDepth: INTAKE_DEFAULT_MAX_DEPTH,
|
|
44
|
+
includeProjectInbox: true,
|
|
45
|
+
includeDocsIntake: true,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export function intakeConfigPath(rootDir) {
|
|
49
|
+
return join(rootDir, INTAKE_CONFIG_FILE);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function clampDepth(value) {
|
|
53
|
+
const n = Number(value);
|
|
54
|
+
if (!Number.isFinite(n) || n < 0) return INTAKE_DEFAULT_MAX_DEPTH;
|
|
55
|
+
return Math.min(Math.floor(n), INTAKE_HARD_MAX_DEPTH);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeDir(dir, rootDir) {
|
|
59
|
+
if (typeof dir !== 'string') return null;
|
|
60
|
+
const trimmed = dir.trim();
|
|
61
|
+
if (!trimmed) return null;
|
|
62
|
+
return isAbsolute(trimmed) ? trimmed : resolve(rootDir, trimmed);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseEnvDirs(env) {
|
|
66
|
+
const raw = String(env?.CX_INBOX_DIRS ?? '').trim();
|
|
67
|
+
if (!raw) return [];
|
|
68
|
+
return raw.split(':').map((p) => p.trim()).filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function loadIntakeConfig(rootDir, env = process.env) {
|
|
72
|
+
const file = intakeConfigPath(rootDir);
|
|
73
|
+
let stored = {};
|
|
74
|
+
if (existsSync(file)) {
|
|
75
|
+
try { stored = JSON.parse(readFileSync(file, 'utf8')); } catch { stored = {}; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const parentDirs = []
|
|
79
|
+
.concat(Array.isArray(stored.parentDirs) ? stored.parentDirs : [])
|
|
80
|
+
.concat(parseEnvDirs(env))
|
|
81
|
+
.map((dir) => normalizeDir(dir, rootDir))
|
|
82
|
+
.filter(Boolean);
|
|
83
|
+
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const uniqueDirs = [];
|
|
86
|
+
for (const dir of parentDirs) {
|
|
87
|
+
if (seen.has(dir)) continue;
|
|
88
|
+
seen.add(dir);
|
|
89
|
+
uniqueDirs.push(dir);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const envDepth = env?.CX_INTAKE_MAX_DEPTH;
|
|
93
|
+
const maxDepth = clampDepth(envDepth ?? stored.maxDepth ?? DEFAULT_INTAKE_CONFIG.maxDepth);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
parentDirs: uniqueDirs,
|
|
97
|
+
maxDepth,
|
|
98
|
+
includeProjectInbox: stored.includeProjectInbox !== false,
|
|
99
|
+
includeDocsIntake: stored.includeDocsIntake !== false,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function saveIntakeConfig(rootDir, patch = {}) {
|
|
104
|
+
if (!shouldCreateCx(rootDir)) {
|
|
105
|
+
throw new Error('Refusing to write intake config: directory is not an initialized construct project. Run `construct init` first.');
|
|
106
|
+
}
|
|
107
|
+
const current = loadIntakeConfig(rootDir, {});
|
|
108
|
+
const next = {
|
|
109
|
+
parentDirs: Array.isArray(patch.parentDirs)
|
|
110
|
+
? patch.parentDirs.map((dir) => normalizeDir(dir, rootDir)).filter(Boolean)
|
|
111
|
+
: current.parentDirs,
|
|
112
|
+
maxDepth: clampDepth(patch.maxDepth ?? current.maxDepth),
|
|
113
|
+
includeProjectInbox: patch.includeProjectInbox !== undefined ? Boolean(patch.includeProjectInbox) : current.includeProjectInbox,
|
|
114
|
+
includeDocsIntake: patch.includeDocsIntake !== undefined ? Boolean(patch.includeDocsIntake) : current.includeDocsIntake,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const file = intakeConfigPath(rootDir);
|
|
118
|
+
mkdirSync(join(rootDir, '.cx'), { recursive: true });
|
|
119
|
+
writeFileSync(file, JSON.stringify(next, null, 2) + '\n');
|
|
120
|
+
return next;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function describeIntakeDepth(depth = INTAKE_DEFAULT_MAX_DEPTH) {
|
|
124
|
+
const value = clampDepth(depth);
|
|
125
|
+
const exact = INTAKE_DEPTH_GUIDANCE.find((g) => g.value === value);
|
|
126
|
+
if (exact) return exact;
|
|
127
|
+
return {
|
|
128
|
+
value,
|
|
129
|
+
label: `Custom depth (${value})`,
|
|
130
|
+
detail: `Walks up to ${value} levels of subdirectories below each parent.`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/intake/postgres-queue.mjs — Postgres adapter for the IntakeQueue interface.
|
|
3
|
+
*
|
|
4
|
+
* Backs team and enterprise deployment modes. Implements the same six-method
|
|
5
|
+
* contract as FilesystemIntakeQueue (enqueue, listPending, count, read,
|
|
6
|
+
* markProcessed, markSkipped, reopen) plus a claim() method that uses
|
|
7
|
+
* SELECT ... FOR UPDATE SKIP LOCKED so concurrent workers cannot grab the
|
|
8
|
+
* same pending item.
|
|
9
|
+
*
|
|
10
|
+
* Triage fields (intake_type, rd_stage, primary_owner, recommended_action,
|
|
11
|
+
* risk, requires_approval, confidence) are flattened into typed columns for
|
|
12
|
+
* efficient filtering; the full packet — including suggestion / related /
|
|
13
|
+
* excerpt / query — lives in payload jsonb so the on-disk and in-table
|
|
14
|
+
* shapes round-trip without loss.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
|
|
19
|
+
function slugify(value) {
|
|
20
|
+
return String(value || 'untitled')
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
23
|
+
.replace(/^-+|-+$/g, '')
|
|
24
|
+
.slice(0, 60) || 'untitled';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let counter = 0;
|
|
28
|
+
function timestamp() {
|
|
29
|
+
// Milliseconds + a per-process counter to guarantee uniqueness even when
|
|
30
|
+
// two enqueues fire in the same millisecond (test loops, batched ingests).
|
|
31
|
+
counter = (counter + 1) % 1000;
|
|
32
|
+
const c = String(counter).padStart(3, '0');
|
|
33
|
+
return `${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 23)}-${c}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function rowToEntry(row) {
|
|
37
|
+
if (!row) return null;
|
|
38
|
+
const payload = row.payload || {};
|
|
39
|
+
return {
|
|
40
|
+
id: row.id,
|
|
41
|
+
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at,
|
|
42
|
+
status: row.status,
|
|
43
|
+
...payload,
|
|
44
|
+
processedAt: row.processed_at instanceof Date ? row.processed_at.toISOString() : row.processed_at || undefined,
|
|
45
|
+
processedBy: row.processed_by || undefined,
|
|
46
|
+
notes: row.notes || undefined,
|
|
47
|
+
skippedAt: row.skipped_at instanceof Date ? row.skipped_at.toISOString() : row.skipped_at || undefined,
|
|
48
|
+
skippedBy: row.skipped_by || undefined,
|
|
49
|
+
reason: row.skip_reason || undefined,
|
|
50
|
+
claimedAt: row.claimed_at instanceof Date ? row.claimed_at.toISOString() : row.claimed_at || undefined,
|
|
51
|
+
claimedBy: row.claimed_by || undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class PostgresIntakeQueue {
|
|
56
|
+
constructor({ sql, project, tenantId = null } = {}) {
|
|
57
|
+
if (!sql) throw new Error('PostgresIntakeQueue: sql client is required');
|
|
58
|
+
if (!project) throw new Error('PostgresIntakeQueue: project is required');
|
|
59
|
+
this.sql = sql;
|
|
60
|
+
this.project = project;
|
|
61
|
+
this.tenantId = tenantId;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async enqueue(entry) {
|
|
65
|
+
if (!entry?.intake?.sourcePath) throw new Error('enqueue: entry.intake.sourcePath is required');
|
|
66
|
+
|
|
67
|
+
const ts = timestamp();
|
|
68
|
+
const slug = slugify(path.basename(entry.intake.sourcePath, path.extname(entry.intake.sourcePath)));
|
|
69
|
+
const id = `${ts}-${slug}`;
|
|
70
|
+
const triage = entry.triage || {};
|
|
71
|
+
|
|
72
|
+
await this.sql`
|
|
73
|
+
INSERT INTO construct_intake_items (
|
|
74
|
+
id, project, tenant_id, status,
|
|
75
|
+
intake_type, rd_stage, primary_owner, recommended_action,
|
|
76
|
+
risk, requires_approval, confidence, payload
|
|
77
|
+
) VALUES (
|
|
78
|
+
${id}, ${this.project}, ${this.tenantId}, 'pending',
|
|
79
|
+
${triage.intakeType || null}, ${triage.rdStage || null},
|
|
80
|
+
${triage.primaryOwner || null}, ${triage.recommendedAction || null},
|
|
81
|
+
${triage.risk || null}, ${triage.requiresApproval || false},
|
|
82
|
+
${typeof triage.confidence === 'number' ? triage.confidence : null},
|
|
83
|
+
${this.sql.json(entry)}
|
|
84
|
+
)
|
|
85
|
+
`;
|
|
86
|
+
return { id };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async listPending({ limit = 100 } = {}) {
|
|
90
|
+
const rows = await this.sql`
|
|
91
|
+
SELECT * FROM construct_intake_items
|
|
92
|
+
WHERE project = ${this.project}
|
|
93
|
+
AND (${this.tenantId}::text IS NULL OR tenant_id IS NOT DISTINCT FROM ${this.tenantId})
|
|
94
|
+
AND status = 'pending'
|
|
95
|
+
ORDER BY created_at ASC
|
|
96
|
+
LIMIT ${limit}
|
|
97
|
+
`;
|
|
98
|
+
return rows.map(rowToEntry);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async count() {
|
|
102
|
+
const rows = await this.sql`
|
|
103
|
+
SELECT COUNT(*)::int AS n FROM construct_intake_items
|
|
104
|
+
WHERE project = ${this.project}
|
|
105
|
+
AND (${this.tenantId}::text IS NULL OR tenant_id IS NOT DISTINCT FROM ${this.tenantId})
|
|
106
|
+
AND status = 'pending'
|
|
107
|
+
`;
|
|
108
|
+
return rows[0]?.n ?? 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async read(id) {
|
|
112
|
+
const rows = await this.sql`
|
|
113
|
+
SELECT * FROM construct_intake_items
|
|
114
|
+
WHERE id = ${id}
|
|
115
|
+
AND project = ${this.project}
|
|
116
|
+
AND (${this.tenantId}::text IS NULL OR tenant_id IS NOT DISTINCT FROM ${this.tenantId})
|
|
117
|
+
LIMIT 1
|
|
118
|
+
`;
|
|
119
|
+
return rows[0] ? rowToEntry(rows[0]) : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async markProcessed(id, { processedBy = 'unknown', notes = '' } = {}) {
|
|
123
|
+
const result = await this.sql`
|
|
124
|
+
UPDATE construct_intake_items
|
|
125
|
+
SET status = 'processed', processed_at = now(), processed_by = ${processedBy},
|
|
126
|
+
notes = ${notes || null}, updated_at = now()
|
|
127
|
+
WHERE id = ${id}
|
|
128
|
+
AND project = ${this.project}
|
|
129
|
+
AND status IN ('pending', 'claimed')
|
|
130
|
+
RETURNING id
|
|
131
|
+
`;
|
|
132
|
+
if (result.count === 0) throw new Error(`markProcessed: no pending entry ${id}`);
|
|
133
|
+
return { id };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async markSkipped(id, { skippedBy = 'unknown', reason = '' } = {}) {
|
|
137
|
+
const result = await this.sql`
|
|
138
|
+
UPDATE construct_intake_items
|
|
139
|
+
SET status = 'skipped', skipped_at = now(), skipped_by = ${skippedBy},
|
|
140
|
+
skip_reason = ${reason || null}, updated_at = now()
|
|
141
|
+
WHERE id = ${id}
|
|
142
|
+
AND project = ${this.project}
|
|
143
|
+
AND status IN ('pending', 'claimed')
|
|
144
|
+
RETURNING id
|
|
145
|
+
`;
|
|
146
|
+
if (result.count === 0) throw new Error(`markSkipped: no pending entry ${id}`);
|
|
147
|
+
return { id };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async reopen(id) {
|
|
151
|
+
const result = await this.sql`
|
|
152
|
+
UPDATE construct_intake_items
|
|
153
|
+
SET status = 'pending',
|
|
154
|
+
processed_at = NULL, processed_by = NULL, notes = NULL,
|
|
155
|
+
skipped_at = NULL, skipped_by = NULL, skip_reason = NULL,
|
|
156
|
+
claimed_at = NULL, claimed_by = NULL,
|
|
157
|
+
updated_at = now()
|
|
158
|
+
WHERE id = ${id}
|
|
159
|
+
AND project = ${this.project}
|
|
160
|
+
AND status IN ('processed', 'skipped')
|
|
161
|
+
RETURNING id, (CASE WHEN processed_at IS NOT NULL THEN 'processed' ELSE 'skipped' END) AS from_status
|
|
162
|
+
`;
|
|
163
|
+
if (result.count === 0) throw new Error(`reopen: no processed or skipped entry ${id}`);
|
|
164
|
+
return { id, from: result[0]?.from_status || 'unknown' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Atomically claim the next pending intake item for a worker.
|
|
169
|
+
* Uses SELECT ... FOR UPDATE SKIP LOCKED so concurrent workers across
|
|
170
|
+
* processes / containers cannot grab the same item. Returns the claimed
|
|
171
|
+
* entry (with status='claimed'), or null when the queue is empty.
|
|
172
|
+
*/
|
|
173
|
+
async claim({ claimedBy }) {
|
|
174
|
+
if (!claimedBy) throw new Error('claim: claimedBy is required');
|
|
175
|
+
return await this.sql.begin(async (tx) => {
|
|
176
|
+
const rows = await tx`
|
|
177
|
+
SELECT * FROM construct_intake_items
|
|
178
|
+
WHERE project = ${this.project}
|
|
179
|
+
AND (${this.tenantId}::text IS NULL OR tenant_id IS NOT DISTINCT FROM ${this.tenantId})
|
|
180
|
+
AND status = 'pending'
|
|
181
|
+
ORDER BY created_at ASC
|
|
182
|
+
FOR UPDATE SKIP LOCKED
|
|
183
|
+
LIMIT 1
|
|
184
|
+
`;
|
|
185
|
+
const row = rows[0];
|
|
186
|
+
if (!row) return null;
|
|
187
|
+
await tx`
|
|
188
|
+
UPDATE construct_intake_items
|
|
189
|
+
SET status = 'claimed', claimed_at = now(), claimed_by = ${claimedBy}, updated_at = now()
|
|
190
|
+
WHERE id = ${row.id}
|
|
191
|
+
`;
|
|
192
|
+
row.status = 'claimed';
|
|
193
|
+
row.claimed_by = claimedBy;
|
|
194
|
+
row.claimed_at = new Date();
|
|
195
|
+
return rowToEntry(row);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/intake/prepare.mjs — turn a fresh inbox ingestion into an R&D intake packet.
|
|
3
|
+
*
|
|
4
|
+
* Called by `InboxWatcher.poll()` after each successful file ingestion.
|
|
5
|
+
* Four deterministic preparation steps, then write the result to the
|
|
6
|
+
* intake queue:
|
|
7
|
+
*
|
|
8
|
+
* 1. Suggest a docs lane via lib/docs-routing.mjs (postmortem? PRD?).
|
|
9
|
+
* 2. Query the corpus via lib/storage/hybrid-query.mjs for top-K existing
|
|
10
|
+
* docs related to the new content. Uses the file's filename + first
|
|
11
|
+
* paragraph as the query (no LLM call needed for retrieval).
|
|
12
|
+
* 3. Extract a short excerpt of the new content for the agent to see
|
|
13
|
+
* without reopening the file.
|
|
14
|
+
* 4. Run classifyRdIntake to produce the R&D triage object (intake type,
|
|
15
|
+
* owner persona, recommended chain, action, risk).
|
|
16
|
+
*
|
|
17
|
+
* The agent — invoked manually or via session-start hook nudge — reads
|
|
18
|
+
* the resulting `.cx/intake/pending/<id>.json` and does the real
|
|
19
|
+
* comparison work (overlap with existing PRD? contradicts ADR? new RFC
|
|
20
|
+
* candidate?). The daemon never calls an LLM. This separation keeps the
|
|
21
|
+
* daemon cheap and predictable; the model spend stays with the agent.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
|
|
27
|
+
import { buildHybridSearchResultsAsync } from '../storage/hybrid-query.mjs';
|
|
28
|
+
import { suggestDocsLaneForFile } from '../docs-routing.mjs';
|
|
29
|
+
import { createIntakeQueue } from './queue.mjs';
|
|
30
|
+
import { classifyRdIntake } from './classify.mjs';
|
|
31
|
+
import { detectCustomerMentions, linkSignalToCustomer, updateCustomerProfile } from '../embed/customer-profiles.mjs';
|
|
32
|
+
|
|
33
|
+
const DEFAULT_RELATED_LIMIT = 5;
|
|
34
|
+
const EXCERPT_CHARS = 800;
|
|
35
|
+
const QUERY_CHARS = 500;
|
|
36
|
+
|
|
37
|
+
export async function prepareIntakeForIngestedFile({
|
|
38
|
+
rootDir,
|
|
39
|
+
ingestedFile,
|
|
40
|
+
env = process.env,
|
|
41
|
+
relatedLimit = DEFAULT_RELATED_LIMIT,
|
|
42
|
+
hybridSearchFn = buildHybridSearchResultsAsync,
|
|
43
|
+
queue,
|
|
44
|
+
readFileFn = readContent,
|
|
45
|
+
classifyFn = classifyRdIntake,
|
|
46
|
+
} = {}) {
|
|
47
|
+
if (!rootDir) throw new Error('prepareIntakeForIngestedFile: rootDir is required');
|
|
48
|
+
if (!ingestedFile?.sourcePath) throw new Error('prepareIntakeForIngestedFile: ingestedFile.sourcePath is required');
|
|
49
|
+
|
|
50
|
+
const extracted = readFileFn(ingestedFile.outputPath) || '';
|
|
51
|
+
const query = buildQuery(ingestedFile.sourcePath, extracted);
|
|
52
|
+
|
|
53
|
+
let related = [];
|
|
54
|
+
try {
|
|
55
|
+
const search = await hybridSearchFn(rootDir, query, { limit: relatedLimit, env });
|
|
56
|
+
related = (search?.results || []).map((hit) => ({
|
|
57
|
+
path: hit.source_path || hit.id,
|
|
58
|
+
title: hit.title || hit.id,
|
|
59
|
+
score: hit.score,
|
|
60
|
+
summary: hit.summary || '',
|
|
61
|
+
}));
|
|
62
|
+
} catch (err) {
|
|
63
|
+
related = [];
|
|
64
|
+
if (process.env.CONSTRUCT_DEBUG_INTAKE === '1') {
|
|
65
|
+
process.stderr.write(`[intake:prepare] hybrid search failed: ${err.message}\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const lane = suggestDocsLaneForFile(ingestedFile.sourcePath, extracted) || null;
|
|
70
|
+
const triage = classifyFn({
|
|
71
|
+
sourcePath: ingestedFile.sourcePath,
|
|
72
|
+
extractedText: extracted,
|
|
73
|
+
related,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Detect customer mentions and update profiles
|
|
77
|
+
const customerMentions = detectCustomerMentions(extracted.slice(0, 2000));
|
|
78
|
+
const customers = customerMentions.map((c) => c.customerId);
|
|
79
|
+
|
|
80
|
+
const entry = {
|
|
81
|
+
intake: {
|
|
82
|
+
sourcePath: ingestedFile.sourcePath,
|
|
83
|
+
outputPath: ingestedFile.outputPath,
|
|
84
|
+
characters: ingestedFile.characters,
|
|
85
|
+
knowledgeSubdir: ingestedFile.knowledgeSubdir,
|
|
86
|
+
},
|
|
87
|
+
triage,
|
|
88
|
+
suggestion: lane ? { lane, source: 'docs-routing.suggestDocsLaneForFile' } : null,
|
|
89
|
+
related,
|
|
90
|
+
excerpt: extracted.slice(0, EXCERPT_CHARS),
|
|
91
|
+
query,
|
|
92
|
+
customers: customers.length ? customers : undefined,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const intakeQueue = queue || createIntakeQueue(rootDir, env);
|
|
96
|
+
const result = intakeQueue.enqueue(entry);
|
|
97
|
+
const intakeId = result?.id || '';
|
|
98
|
+
|
|
99
|
+
// Update customer profiles with new evidence
|
|
100
|
+
for (const mention of customerMentions) {
|
|
101
|
+
try {
|
|
102
|
+
const intakeType = triage?.intakeType || 'insight';
|
|
103
|
+
const excerptPreview = (extracted || '').slice(0, 200).replace(/\n/g, ' ').trim();
|
|
104
|
+
linkSignalToCustomer(mention.customerId, {
|
|
105
|
+
id: intakeId,
|
|
106
|
+
type: 'intake',
|
|
107
|
+
sourcePath: ingestedFile.sourcePath,
|
|
108
|
+
summary: `[${intakeType}] ${ingestedFile.sourcePath.split('/').pop()}${excerptPreview ? ` — ${excerptPreview.slice(0, 100)}` : ''}`,
|
|
109
|
+
});
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (process.env.CONSTRUCT_DEBUG_INTAKE === '1') {
|
|
112
|
+
process.stderr.write(`[intake:prepare] customer profile update failed for ${mention.customerId}: ${err.message}\n`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildQuery(sourcePath, extracted) {
|
|
121
|
+
const filename = path.basename(sourcePath, path.extname(sourcePath))
|
|
122
|
+
.replace(/[-_]+/g, ' ')
|
|
123
|
+
.trim();
|
|
124
|
+
const firstParagraph = (extracted || '').split(/\n\s*\n/).find((p) => p.trim().length > 20) || '';
|
|
125
|
+
const combined = `${filename}\n\n${firstParagraph}`.slice(0, QUERY_CHARS).trim();
|
|
126
|
+
return combined || filename || 'untitled intake';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readContent(outputPath) {
|
|
130
|
+
if (!outputPath || !existsSync(outputPath)) return '';
|
|
131
|
+
try {
|
|
132
|
+
const text = readFileSync(outputPath, 'utf8');
|
|
133
|
+
const marker = text.indexOf('## Extracted Content');
|
|
134
|
+
if (marker !== -1) return text.slice(marker + '## Extracted Content'.length).trim();
|
|
135
|
+
return text;
|
|
136
|
+
} catch {
|
|
137
|
+
return '';
|
|
138
|
+
}
|
|
139
|
+
}
|