@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,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embed/inbox.mjs — filesystem inbox watcher for the embed daemon.
|
|
3
|
+
*
|
|
4
|
+
* Watches one or more local directories for new ingestable files and
|
|
5
|
+
* processes them into the observation store. Agnostic to content type —
|
|
6
|
+
* handles specs, ADRs, meeting notes, internal docs, research, PDFs,
|
|
7
|
+
* Office files, plain text, code, or any format supported by document-extract.
|
|
8
|
+
*
|
|
9
|
+
* Watch dirs (in priority order):
|
|
10
|
+
* 1. CX_INBOX_DIRS env var — colon-separated absolute paths
|
|
11
|
+
* 2. <rootDir>/.cx/inbox/ — per-project drop folder (always included)
|
|
12
|
+
* 3. <rootDir>/docs/intake/ — project intake drop zone when docs init is used
|
|
13
|
+
*
|
|
14
|
+
* State tracking:
|
|
15
|
+
* <rootDir>/.cx/runtime/inbox-state.json — maps filePath → processedAt
|
|
16
|
+
* Prevents re-processing the same file across daemon restarts.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* const watcher = new InboxWatcher({ rootDir, env });
|
|
20
|
+
* const result = await watcher.poll();
|
|
21
|
+
* // result: { processed: [...], skipped: number, errors: [...] }
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { join, resolve, isAbsolute } from 'node:path';
|
|
26
|
+
import { isExtractableDocumentPath } from '../document-extract.mjs';
|
|
27
|
+
import { ingestDocuments } from '../document-ingest.mjs';
|
|
28
|
+
import { addObservation } from '../observation-store.mjs';
|
|
29
|
+
import { inferKnowledgeTarget, knowledgeDirForCategory } from '../knowledge/layout.mjs';
|
|
30
|
+
import { docLaneDir, suggestDocsLaneForFile } from '../docs-routing.mjs';
|
|
31
|
+
import { shouldCreateCx } from '../project-detection.mjs';
|
|
32
|
+
import { prepareIntakeForIngestedFile } from '../intake/prepare.mjs';
|
|
33
|
+
import { loadIntakeConfig, INTAKE_DEFAULT_MAX_DEPTH } from '../intake/intake-config.mjs';
|
|
34
|
+
|
|
35
|
+
const STATE_FILE = '.cx/runtime/inbox-state.json';
|
|
36
|
+
const DEFAULT_INBOX_SUBDIR = '.cx/inbox';
|
|
37
|
+
const DOCS_INTAKE_SUBDIR = 'docs/intake';
|
|
38
|
+
const MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB hard cap
|
|
39
|
+
|
|
40
|
+
function nextAvailablePath(targetPath) {
|
|
41
|
+
if (!existsSync(targetPath)) return targetPath;
|
|
42
|
+
const dot = targetPath.lastIndexOf('.');
|
|
43
|
+
const base = dot === -1 ? targetPath : targetPath.slice(0, dot);
|
|
44
|
+
const ext = dot === -1 ? '' : targetPath.slice(dot);
|
|
45
|
+
let index = 2;
|
|
46
|
+
while (true) {
|
|
47
|
+
const candidate = `${base}-${index}${ext}`;
|
|
48
|
+
if (!existsSync(candidate)) return candidate;
|
|
49
|
+
index += 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── State helpers ────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function statePath(rootDir) {
|
|
56
|
+
return join(rootDir, STATE_FILE);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readState(rootDir) {
|
|
60
|
+
const p = statePath(rootDir);
|
|
61
|
+
if (!existsSync(p)) return {};
|
|
62
|
+
try { return JSON.parse(readFileSync(p, 'utf8')); } catch (err) { process.stderr.write('[inbox.mjs] readState: ' + (err?.message ?? String(err)) + '\n'); return {}; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function writeState(rootDir, state) {
|
|
66
|
+
const p = statePath(rootDir);
|
|
67
|
+
|
|
68
|
+
// Only create .cx directories if this is an initialized project
|
|
69
|
+
if (shouldCreateCx(rootDir)) {
|
|
70
|
+
mkdirSync(join(rootDir, '.cx', 'runtime'), { recursive: true });
|
|
71
|
+
writeFileSync(p, JSON.stringify(state, null, 2) + '\n');
|
|
72
|
+
}
|
|
73
|
+
// If not an initialized project, don't create .cx - state will be lost
|
|
74
|
+
// but that's okay since we shouldn't be processing files in uninitialized dirs
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Directory resolution ─────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolve inbox watch directories from the persisted intake-config.
|
|
81
|
+
* Always includes <rootDir>/.cx/inbox/ (created on first use) when the
|
|
82
|
+
* config has includeProjectInbox: true (default).
|
|
83
|
+
* Includes <rootDir>/docs/intake/ when it exists and includeDocsIntake is true.
|
|
84
|
+
* `parentDirs` from intake-config.json plus CX_INBOX_DIRS env var add extras.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveInboxDirs(rootDir, env = process.env) {
|
|
87
|
+
const config = loadIntakeConfig(rootDir, env);
|
|
88
|
+
const dirs = [];
|
|
89
|
+
|
|
90
|
+
if (config.includeProjectInbox) {
|
|
91
|
+
const projectInbox = join(rootDir, DEFAULT_INBOX_SUBDIR);
|
|
92
|
+
mkdirSync(projectInbox, { recursive: true });
|
|
93
|
+
dirs.push(projectInbox);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (config.includeDocsIntake) {
|
|
97
|
+
const docsIntake = join(rootDir, DOCS_INTAKE_SUBDIR);
|
|
98
|
+
if (existsSync(docsIntake)) dirs.push(docsIntake);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const candidate of config.parentDirs) {
|
|
102
|
+
if (!candidate) continue;
|
|
103
|
+
if (!existsSync(candidate)) continue;
|
|
104
|
+
if (dirs.includes(candidate)) continue;
|
|
105
|
+
dirs.push(candidate);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return dirs;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── File scanning ────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Walk `dir` up to `maxDepth` subdirectory levels.
|
|
115
|
+
* maxDepth=0 → only files in `dir` itself (no subdirs).
|
|
116
|
+
* Directories are tracked with their depth so the walk stops at the limit.
|
|
117
|
+
*/
|
|
118
|
+
function scanDir(dir, maxDepth = INTAKE_DEFAULT_MAX_DEPTH) {
|
|
119
|
+
const results = [];
|
|
120
|
+
const stack = [{ path: dir, depth: 0 }];
|
|
121
|
+
try {
|
|
122
|
+
while (stack.length > 0) {
|
|
123
|
+
const { path: current, depth } = stack.pop();
|
|
124
|
+
const entries = readdirSync(current, { withFileTypes: true });
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
if (entry.name.startsWith('.')) continue;
|
|
127
|
+
const full = join(current, entry.name);
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
if (depth < maxDepth) stack.push({ path: full, depth: depth + 1 });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!entry.isFile()) continue;
|
|
133
|
+
if (!isExtractableDocumentPath(full)) continue;
|
|
134
|
+
try {
|
|
135
|
+
const st = statSync(full);
|
|
136
|
+
if (st.size === 0 || st.size > MAX_FILE_SIZE_BYTES) continue;
|
|
137
|
+
results.push({ path: full, size: st.size, mtimeMs: st.mtimeMs });
|
|
138
|
+
} catch (err) { process.stderr.write('[inbox.mjs] scan-file-stat: ' + (err?.message ?? String(err)) + '\n'); }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch (err) { process.stderr.write('[inbox.mjs] scan-dir: ' + (err?.message ?? String(err)) + '\n'); }
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Observation extraction ───────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Derive observation category and knowledge subdir from a file path.
|
|
149
|
+
* Uses inferKnowledgeTarget for routing, then maps subdir → obs category.
|
|
150
|
+
*/
|
|
151
|
+
function inferFileClassification(filePath) {
|
|
152
|
+
const knowledgeSubdir = inferKnowledgeTarget(filePath);
|
|
153
|
+
// Map subdir to observation category
|
|
154
|
+
let category;
|
|
155
|
+
switch (knowledgeSubdir) {
|
|
156
|
+
case 'decisions': category = 'decision'; break;
|
|
157
|
+
case 'how-tos': category = 'pattern'; break;
|
|
158
|
+
case 'reference': category = 'insight'; break;
|
|
159
|
+
case 'external': category = 'insight'; break;
|
|
160
|
+
default: category = 'insight';
|
|
161
|
+
}
|
|
162
|
+
// Postmortem / incident files deserve anti-pattern category regardless of subdir
|
|
163
|
+
const name = filePath.toLowerCase();
|
|
164
|
+
if (/\bpost.?mortem\b|\bincident\b|\brca\b/.test(name)) category = 'anti-pattern';
|
|
165
|
+
return { category, knowledgeSubdir };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Write an observation summarising the ingested document.
|
|
170
|
+
* Summary = first non-empty line of extracted text (≤120 chars).
|
|
171
|
+
* Content = first 1500 chars of extracted text for RAG searchability.
|
|
172
|
+
*/
|
|
173
|
+
function recordInboxObservation(rootDir, { sourcePath, outputPath, characters, extractedText, category, knowledgeSubdir }) {
|
|
174
|
+
const lines = (extractedText ?? '').split('\n').map((l) => l.trim()).filter(Boolean);
|
|
175
|
+
const firstLine = (lines[0] ?? '').slice(0, 120);
|
|
176
|
+
const filename = sourcePath.split('/').pop();
|
|
177
|
+
const summary = firstLine
|
|
178
|
+
? `[inbox] ${filename}: ${firstLine}`
|
|
179
|
+
: `[inbox] Ingested document: ${filename}`;
|
|
180
|
+
const content = (extractedText ?? '').slice(0, 1500);
|
|
181
|
+
const tags = ['inbox', 'ingested-doc', category, `knowledge:${knowledgeSubdir}`];
|
|
182
|
+
|
|
183
|
+
addObservation(rootDir, {
|
|
184
|
+
role: 'construct',
|
|
185
|
+
category,
|
|
186
|
+
summary,
|
|
187
|
+
content: `source: ${sourcePath}\noutput: ${outputPath}\nknowledge-subdir: ${knowledgeSubdir}\ncharacters: ${characters}\n\n${content}`,
|
|
188
|
+
tags,
|
|
189
|
+
confidence: 0.75,
|
|
190
|
+
source: 'inbox-watcher',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function maybePromoteToDocs(rootDir, { sourcePath, extractedText }) {
|
|
195
|
+
const suggestedLane = suggestDocsLaneForFile(sourcePath, extractedText);
|
|
196
|
+
if (!suggestedLane || suggestedLane === 'intake') return null;
|
|
197
|
+
|
|
198
|
+
const laneDir = join(rootDir, 'docs', docLaneDir(suggestedLane));
|
|
199
|
+
if (!existsSync(laneDir)) return null;
|
|
200
|
+
|
|
201
|
+
const sourceName = sourcePath.split('/').pop() ?? 'intake-doc';
|
|
202
|
+
const targetPath = nextAvailablePath(join(laneDir, `${sourceName}.md`));
|
|
203
|
+
const title = sourceName.replace(/\.[^.]+$/, '').replace(/[-_]+/g, ' ').trim() || 'Promoted intake document';
|
|
204
|
+
const markdown = [
|
|
205
|
+
'# ' + title,
|
|
206
|
+
'',
|
|
207
|
+
'> Promoted from intake for review and incorporation into this docs lane.',
|
|
208
|
+
'',
|
|
209
|
+
`- Source: \`${sourcePath}\``,
|
|
210
|
+
`- Suggested lane: \`${suggestedLane}\``,
|
|
211
|
+
'',
|
|
212
|
+
'## Extracted Content',
|
|
213
|
+
'',
|
|
214
|
+
extractedText || '',
|
|
215
|
+
'',
|
|
216
|
+
].join('\n');
|
|
217
|
+
|
|
218
|
+
writeFileSync(targetPath, markdown);
|
|
219
|
+
return targetPath;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── InboxWatcher class ───────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
export class InboxWatcher {
|
|
225
|
+
#rootDir;
|
|
226
|
+
#env;
|
|
227
|
+
#cwd;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {object} opts
|
|
231
|
+
* @param {string} opts.rootDir - Root dir for observation store + state file
|
|
232
|
+
* @param {object} [opts.env] - Env override (default: process.env)
|
|
233
|
+
* @param {string} [opts.cwd] - Working dir for ingest output (default: rootDir)
|
|
234
|
+
*/
|
|
235
|
+
constructor({ rootDir, env = process.env, cwd } = {}) {
|
|
236
|
+
this.#rootDir = rootDir;
|
|
237
|
+
this.#env = env;
|
|
238
|
+
this.#cwd = cwd ?? rootDir;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Scan all inbox dirs, ingest new files, record observations.
|
|
243
|
+
* Returns { processed: [...], skipped: number, errors: [...] }
|
|
244
|
+
*/
|
|
245
|
+
async poll() {
|
|
246
|
+
const config = loadIntakeConfig(this.#rootDir, this.#env);
|
|
247
|
+
const dirs = resolveInboxDirs(this.#rootDir, this.#env);
|
|
248
|
+
const state = readState(this.#rootDir);
|
|
249
|
+
const processed = [];
|
|
250
|
+
const errors = [];
|
|
251
|
+
let skipped = 0;
|
|
252
|
+
|
|
253
|
+
for (const dir of dirs) {
|
|
254
|
+
const candidates = scanDir(dir, config.maxDepth);
|
|
255
|
+
for (const candidate of candidates) {
|
|
256
|
+
const key = candidate.path;
|
|
257
|
+
|
|
258
|
+
// Skip if already processed (state key = path + mtime for change detection)
|
|
259
|
+
const stateKey = `${key}:${candidate.mtimeMs}`;
|
|
260
|
+
if (state[key] && state[key].mtimeMs === candidate.mtimeMs) {
|
|
261
|
+
skipped += 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const { category, knowledgeSubdir } = inferFileClassification(candidate.path);
|
|
267
|
+
const result = await ingestDocuments([candidate.path], {
|
|
268
|
+
cwd: this.#cwd,
|
|
269
|
+
target: `knowledge/${knowledgeSubdir}`,
|
|
270
|
+
sync: false,
|
|
271
|
+
env: this.#env,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const fileResult = result.files?.[0];
|
|
275
|
+
if (fileResult) {
|
|
276
|
+
// Best-effort: read back the extracted text from the written markdown
|
|
277
|
+
let extractedText = '';
|
|
278
|
+
try {
|
|
279
|
+
const md = readFileSync(fileResult.outputPath, 'utf8');
|
|
280
|
+
const contentIdx = md.indexOf('## Extracted Content');
|
|
281
|
+
if (contentIdx !== -1) extractedText = md.slice(contentIdx + 20).trim();
|
|
282
|
+
} catch (err) { process.stderr.write('[inbox.mjs] read-extracted-text: ' + (err?.message ?? String(err)) + '\n'); }
|
|
283
|
+
|
|
284
|
+
recordInboxObservation(this.#rootDir, {
|
|
285
|
+
sourcePath: candidate.path,
|
|
286
|
+
outputPath: fileResult.outputPath,
|
|
287
|
+
characters: fileResult.characters,
|
|
288
|
+
extractedText,
|
|
289
|
+
category,
|
|
290
|
+
knowledgeSubdir,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const docsPath = maybePromoteToDocs(this.#rootDir, {
|
|
294
|
+
sourcePath: candidate.path,
|
|
295
|
+
extractedText,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Queue an intake packet so the agent can triage the new
|
|
299
|
+
// signal against the existing corpus. Failure here is
|
|
300
|
+
// non-fatal — ingest already succeeded; the queue is a
|
|
301
|
+
// best-effort handoff, not the ingestion's source of truth.
|
|
302
|
+
let intakeId = null;
|
|
303
|
+
try {
|
|
304
|
+
const intakeResult = await prepareIntakeForIngestedFile({
|
|
305
|
+
rootDir: this.#rootDir,
|
|
306
|
+
ingestedFile: {
|
|
307
|
+
sourcePath: candidate.path,
|
|
308
|
+
outputPath: fileResult.outputPath,
|
|
309
|
+
characters: fileResult.characters,
|
|
310
|
+
knowledgeSubdir,
|
|
311
|
+
},
|
|
312
|
+
env: this.#env,
|
|
313
|
+
});
|
|
314
|
+
intakeId = intakeResult?.id || null;
|
|
315
|
+
} catch (err) {
|
|
316
|
+
if (this.#env.CONSTRUCT_DEBUG_INTAKE === '1') {
|
|
317
|
+
process.stderr.write(`[inbox] intake queue write failed for ${candidate.path}: ${err.message}\n`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
state[key] = { mtimeMs: candidate.mtimeMs, processedAt: new Date().toISOString(), outputPath: fileResult.outputPath };
|
|
322
|
+
processed.push({ path: candidate.path, outputPath: fileResult.outputPath, docsPath, characters: fileResult.characters, knowledgeSubdir, intakeId });
|
|
323
|
+
}
|
|
324
|
+
} catch (err) {
|
|
325
|
+
errors.push({ path: candidate.path, error: err.message });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (processed.length || errors.length) {
|
|
331
|
+
writeState(this.#rootDir, state);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { processed, skipped, errors, dirs };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Return the configured inbox directories (creates project inbox if missing).
|
|
339
|
+
*/
|
|
340
|
+
dirs() {
|
|
341
|
+
return resolveInboxDirs(this.#rootDir, this.#env);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embed/intake-metrics.mjs — Intake pipeline observability.
|
|
3
|
+
*
|
|
4
|
+
* Aggregates volume, velocity, processing time, and acceptance rate from
|
|
5
|
+
* the filesystem intake queue. Designed for the dashboard /api/insights
|
|
6
|
+
* endpoint and the `construct intake metrics` CLI.
|
|
7
|
+
*
|
|
8
|
+
* Data sources:
|
|
9
|
+
* .cx/intake/pending/ — JSON files waiting for triage
|
|
10
|
+
* .cx/intake/processed/ — JSON files already handled
|
|
11
|
+
* .cx/intake/skipped/ — JSON files intentionally skipped
|
|
12
|
+
* ~/.cx/intake/recommendations-index.json — recommendations
|
|
13
|
+
*
|
|
14
|
+
* All read-only. Never writes to the intake queue.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Scan an intake directory and return file metadata.
|
|
23
|
+
*/
|
|
24
|
+
function scanDir(dir) {
|
|
25
|
+
if (!existsSync(dir)) return [];
|
|
26
|
+
try {
|
|
27
|
+
return readdirSync(dir)
|
|
28
|
+
.filter(f => f.endsWith('.json'))
|
|
29
|
+
.map(f => {
|
|
30
|
+
const fp = join(dir, f);
|
|
31
|
+
try {
|
|
32
|
+
const stat = statSync(fp);
|
|
33
|
+
const content = readFileSync(fp, 'utf8');
|
|
34
|
+
const parsed = JSON.parse(content);
|
|
35
|
+
return {
|
|
36
|
+
id: f.replace('.json', ''),
|
|
37
|
+
createdAt: parsed.createdAt || parsed.triage?.createdAt || stat.birthtime.toISOString(),
|
|
38
|
+
size: stat.size,
|
|
39
|
+
mtimeMs: stat.mtimeMs,
|
|
40
|
+
triage: parsed.triage?.intakeType || 'unknown',
|
|
41
|
+
owner: parsed.triage?.primaryOwner || '—',
|
|
42
|
+
action: parsed.triage?.recommendedAction || '—',
|
|
43
|
+
customers: parsed.customers?.length || 0,
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compute metrics for an intake queue.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} [opts]
|
|
60
|
+
* @param {string} [opts.rootDir] - Project root (default: cwd)
|
|
61
|
+
* @returns {object} Metrics summary
|
|
62
|
+
*/
|
|
63
|
+
export function computeIntakeMetrics({ rootDir } = {}) {
|
|
64
|
+
const root = rootDir || process.cwd();
|
|
65
|
+
const pendingDir = join(root, '.cx', 'intake', 'pending');
|
|
66
|
+
const processedDir = join(root, '.cx', 'intake', 'processed');
|
|
67
|
+
const skippedDir = join(root, '.cx', 'intake', 'skipped');
|
|
68
|
+
|
|
69
|
+
const pending = scanDir(pendingDir);
|
|
70
|
+
const processed = scanDir(processedDir);
|
|
71
|
+
const skipped = scanDir(skippedDir);
|
|
72
|
+
|
|
73
|
+
// Volume
|
|
74
|
+
const volume = {
|
|
75
|
+
pending: pending.length,
|
|
76
|
+
processed: processed.length,
|
|
77
|
+
skipped: skipped.length,
|
|
78
|
+
total: pending.length + processed.length + skipped.length,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Velocity — items per day over trailing 7d window
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
|
|
84
|
+
const allItems = [...pending, ...processed, ...skipped];
|
|
85
|
+
const recentItems = allItems.filter(i => (now - new Date(i.createdAt).getTime()) < SEVEN_DAYS);
|
|
86
|
+
const velocity = Number((recentItems.length / 7).toFixed(1));
|
|
87
|
+
|
|
88
|
+
// Processing time — how long between creation and processing
|
|
89
|
+
const processingTimes = [];
|
|
90
|
+
for (const item of processed) {
|
|
91
|
+
try {
|
|
92
|
+
const fp = join(processedDir, `${item.id}.json`);
|
|
93
|
+
const raw = JSON.parse(readFileSync(fp, 'utf8'));
|
|
94
|
+
if (raw.createdAt && raw.processedAt) {
|
|
95
|
+
const ms = new Date(raw.processedAt) - new Date(raw.createdAt);
|
|
96
|
+
if (ms > 0) processingTimes.push(ms);
|
|
97
|
+
}
|
|
98
|
+
} catch { /* skip */ }
|
|
99
|
+
}
|
|
100
|
+
const avgProcessingTimeMs = processingTimes.length
|
|
101
|
+
? Math.round(processingTimes.reduce((s, t) => s + t, 0) / processingTimes.length)
|
|
102
|
+
: null;
|
|
103
|
+
|
|
104
|
+
// — By type breakdown
|
|
105
|
+
const byType = {};
|
|
106
|
+
for (const item of allItems) {
|
|
107
|
+
byType[item.triage] = (byType[item.triage] || 0) + 1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// — By owner breakdown
|
|
111
|
+
const byOwner = {};
|
|
112
|
+
for (const item of allItems) {
|
|
113
|
+
byOwner[item.owner] = (byOwner[item.owner] || 0) + 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// — Acceptance rate (processed / total actionable)
|
|
117
|
+
const actionable = volume.processed + volume.skipped;
|
|
118
|
+
const acceptRate = actionable > 0
|
|
119
|
+
? Number((volume.processed / actionable * 100).toFixed(1))
|
|
120
|
+
: null;
|
|
121
|
+
|
|
122
|
+
// — Customer-linked items
|
|
123
|
+
const customerLinked = allItems.filter(i => i.customers > 0).length;
|
|
124
|
+
|
|
125
|
+
// — Metrics from recommendation store
|
|
126
|
+
let recStats = { active: 0, dismissed: 0, total: 0 };
|
|
127
|
+
try {
|
|
128
|
+
const recIndexPath = join(homedir(), '.cx', 'intake', 'recommendations-index.json');
|
|
129
|
+
if (existsSync(recIndexPath)) {
|
|
130
|
+
const index = JSON.parse(readFileSync(recIndexPath, 'utf8'));
|
|
131
|
+
const entries = Object.values(index);
|
|
132
|
+
recStats = {
|
|
133
|
+
total: entries.length,
|
|
134
|
+
active: entries.filter(e => !e.dismissedAt && !e.supersededAt).length,
|
|
135
|
+
dismissed: entries.filter(e => e.dismissedAt).length,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
} catch { /* skip */ }
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
volume,
|
|
142
|
+
velocity,
|
|
143
|
+
recentItems: recentItems.length,
|
|
144
|
+
avgProcessingTimeMs,
|
|
145
|
+
avgProcessingTimeFormatted: avgProcessingTimeMs
|
|
146
|
+
? formatDuration(avgProcessingTimeMs)
|
|
147
|
+
: null,
|
|
148
|
+
byType,
|
|
149
|
+
byOwner,
|
|
150
|
+
acceptRate,
|
|
151
|
+
customerLinked,
|
|
152
|
+
recommendations: recStats,
|
|
153
|
+
sampledAt: new Date().toISOString(),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatDuration(ms) {
|
|
158
|
+
if (ms < 1000) return `${ms}ms`;
|
|
159
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
160
|
+
const mins = Math.floor(ms / 60000);
|
|
161
|
+
const secs = Math.round((ms % 60000) / 1000);
|
|
162
|
+
return `${mins}m ${secs}s`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get the age of the oldest pending item.
|
|
167
|
+
*
|
|
168
|
+
* @param {object} [opts]
|
|
169
|
+
* @param {string} [opts.rootDir]
|
|
170
|
+
* @returns {{ oldest: string|null, ageHours: number|null, count: number }}
|
|
171
|
+
*/
|
|
172
|
+
export function pendingAge({ rootDir } = {}) {
|
|
173
|
+
const root = rootDir || process.cwd();
|
|
174
|
+
const pendingDir = join(root, '.cx', 'intake', 'pending');
|
|
175
|
+
const items = scanDir(pendingDir);
|
|
176
|
+
if (!items.length) return { oldest: null, ageHours: null, count: 0 };
|
|
177
|
+
|
|
178
|
+
const oldest = items.reduce((a, b) =>
|
|
179
|
+
new Date(a.createdAt) < new Date(b.createdAt) ? a : b
|
|
180
|
+
);
|
|
181
|
+
const ageHours = (Date.now() - new Date(oldest.createdAt).getTime()) / (60 * 60 * 1000);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
oldest: oldest.id,
|
|
185
|
+
oldestCreatedAt: oldest.createdAt,
|
|
186
|
+
ageHours: Number(ageHours.toFixed(1)),
|
|
187
|
+
count: items.length,
|
|
188
|
+
oldItemsCount: items.filter(i => (Date.now() - new Date(i.createdAt).getTime()) > 24 * 60 * 60 * 1000).length,
|
|
189
|
+
};
|
|
190
|
+
}
|