@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
|
@@ -2,28 +2,163 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* pre-push-gate.mjs — PreToolUse / Bash
|
|
4
4
|
*
|
|
5
|
-
* Intercepts
|
|
6
|
-
*
|
|
5
|
+
* Intercepts three command shapes:
|
|
6
|
+
* git push refuses claude/* branches, refuses on red remote CI,
|
|
7
|
+
* runs project test/build/evals/docs in parallel
|
|
8
|
+
* gh pr create lints the --body / --body-file against the PR
|
|
9
|
+
* template policy before the PR is opened
|
|
10
|
+
* gh pr edit same body lint when --body / --body-file is changed
|
|
11
|
+
*
|
|
7
12
|
* Silent and instant when there's nothing to run.
|
|
13
|
+
*
|
|
14
|
+
* Bypasses (env vars):
|
|
15
|
+
* CONSTRUCT_SKIP_PREPUSH=1 skip the entire push gate
|
|
16
|
+
* CONSTRUCT_ALLOW_CLAUDE_PUSH=1 allow pushing a claude/* branch
|
|
17
|
+
* CONSTRUCT_SKIP_PR_LINT=1 skip the gh pr body lint
|
|
18
|
+
*
|
|
19
|
+
* @p95ms 30000
|
|
20
|
+
* @maxBlockingScope PreToolUse
|
|
8
21
|
*/
|
|
9
|
-
import { readFileSync, existsSync } from 'fs';
|
|
10
|
-
import { execSync, spawnSync } from 'child_process';
|
|
11
|
-
import { dirname, join } from 'path';
|
|
22
|
+
import { readFileSync, existsSync, writeFileSync, mkdtempSync, rmSync } from 'fs';
|
|
23
|
+
import { execSync, spawn, spawnSync } from 'child_process';
|
|
24
|
+
import { dirname, join, resolve } from 'path';
|
|
25
|
+
import { tmpdir } from 'os';
|
|
26
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
27
|
+
import { emitRoleEvent } from '../roles/hook-emit.mjs';
|
|
12
28
|
|
|
13
29
|
let input = {};
|
|
14
|
-
try { input = JSON.parse(readFileSync(0, 'utf8')); }
|
|
30
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); }
|
|
31
|
+
catch (err) { logHookFailure({ hook: 'pre-push-gate', err, phase: 'parse' }); }
|
|
15
32
|
|
|
16
|
-
// Must always echo stdin for PreToolUse chaining
|
|
17
33
|
const echo = () => { process.stdout.write(JSON.stringify(input) + '\n'); process.exit(0); };
|
|
18
34
|
|
|
19
35
|
const command = input?.tool_input?.command || input?.command || '';
|
|
36
|
+
const isGitPush = /\bgit\s+push\b/.test(command);
|
|
37
|
+
const isGhPrCreate = /\bgh\s+pr\s+create\b/.test(command);
|
|
38
|
+
const isGhPrEdit = /\bgh\s+pr\s+edit\b/.test(command);
|
|
20
39
|
|
|
21
|
-
|
|
22
|
-
if (!/\bgit\s+push\b/.test(command)) { echo(); }
|
|
40
|
+
if (!isGitPush && !isGhPrCreate && !isGhPrEdit) { echo(); }
|
|
23
41
|
|
|
24
|
-
// Find nearest package.json / Cargo.toml / pyproject.toml from cwd
|
|
25
42
|
const cwd = input?.cwd || process.cwd();
|
|
26
43
|
|
|
44
|
+
if ((isGhPrCreate || isGhPrEdit) && process.env.CONSTRUCT_SKIP_PR_LINT !== '1') {
|
|
45
|
+
const { body, bodyFile } = extractGhPrBody(command);
|
|
46
|
+
const hasArgs = body != null || bodyFile != null;
|
|
47
|
+
|
|
48
|
+
if (!hasArgs) {
|
|
49
|
+
process.stderr.write(
|
|
50
|
+
`[pre-push-gate] ${isGhPrCreate ? 'gh pr create' : 'gh pr edit'} without --body/--body-file/-F — cannot lint PR body locally.\n` +
|
|
51
|
+
` CI will still enforce the template policy. To lint manually:\n` +
|
|
52
|
+
` construct lint:templates --body-file=<path>\n`,
|
|
53
|
+
);
|
|
54
|
+
} else {
|
|
55
|
+
const scriptPath = findRepoFile(cwd, 'scripts/lint-commits-pr.mjs');
|
|
56
|
+
if (!scriptPath) {
|
|
57
|
+
process.stderr.write(`[pre-push-gate] could not locate scripts/lint-commits-pr.mjs from ${cwd}; skipping PR body lint\n`);
|
|
58
|
+
} else {
|
|
59
|
+
const env = { ...process.env };
|
|
60
|
+
let tmpFile = null;
|
|
61
|
+
try {
|
|
62
|
+
if (bodyFile) {
|
|
63
|
+
env.PR_BODY_FILE = resolve(cwd, bodyFile);
|
|
64
|
+
} else if (body != null) {
|
|
65
|
+
const dir = mkdtempSync(join(tmpdir(), 'construct-pr-lint-'));
|
|
66
|
+
tmpFile = join(dir, 'body.md');
|
|
67
|
+
writeFileSync(tmpFile, body);
|
|
68
|
+
env.PR_BODY_FILE = tmpFile;
|
|
69
|
+
}
|
|
70
|
+
const result = spawnSync(process.execPath, [scriptPath], {
|
|
71
|
+
cwd, env, stdio: ['ignore', 'pipe', 'pipe'], timeout: 15_000,
|
|
72
|
+
});
|
|
73
|
+
const out = (result.stdout?.toString() || '') + (result.stderr?.toString() || '');
|
|
74
|
+
if (result.status !== 0) {
|
|
75
|
+
process.stderr.write(`[pre-push-gate] PR body fails template policy:\n${out}\n`);
|
|
76
|
+
process.stderr.write(`Override: CONSTRUCT_SKIP_PR_LINT=1 <command>\n`);
|
|
77
|
+
emitRoleEvent({
|
|
78
|
+
type: 'push_gate.fail',
|
|
79
|
+
summary: 'PR body fails template policy',
|
|
80
|
+
hookInput: input,
|
|
81
|
+
context: { command: isGhPrCreate ? 'gh pr create' : 'gh pr edit', detail: out.slice(0, 800) },
|
|
82
|
+
});
|
|
83
|
+
if (tmpFile) try { rmSync(dirname(tmpFile), { recursive: true, force: true }); } catch {}
|
|
84
|
+
process.exit(2);
|
|
85
|
+
}
|
|
86
|
+
} finally {
|
|
87
|
+
if (tmpFile) try { rmSync(dirname(tmpFile), { recursive: true, force: true }); } catch {}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
echo();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (process.env.CONSTRUCT_SKIP_PREPUSH === '1') { echo(); }
|
|
95
|
+
|
|
96
|
+
function extractGhPrBody(cmd) {
|
|
97
|
+
let m = cmd.match(/--body-file[\s=]+(["']?)(\S+?)\1(?=\s|$)/);
|
|
98
|
+
if (m) return { bodyFile: m[2] };
|
|
99
|
+
m = cmd.match(/(?:^|\s)-F[\s=]+(["']?)(\S+?)\1(?=\s|$)/);
|
|
100
|
+
if (m) return { bodyFile: m[2] };
|
|
101
|
+
m = cmd.match(/--body[\s=]+"((?:\\.|[^"\\])*)"/s);
|
|
102
|
+
if (m) return { body: m[1].replace(/\\(["\\])/g, '$1') };
|
|
103
|
+
m = cmd.match(/--body[\s=]+'([^']*)'/s);
|
|
104
|
+
if (m) return { body: m[1] };
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function findRepoFile(startDir, relPath) {
|
|
109
|
+
let dir = startDir;
|
|
110
|
+
for (let i = 0; i < 10; i++) {
|
|
111
|
+
const candidate = join(dir, relPath);
|
|
112
|
+
if (existsSync(candidate)) return candidate;
|
|
113
|
+
const parent = dirname(dir);
|
|
114
|
+
if (parent === dir) break;
|
|
115
|
+
dir = parent;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (/\bclaude\//.test(command) && process.env.CONSTRUCT_ALLOW_CLAUDE_PUSH !== '1') {
|
|
121
|
+
process.stderr.write(
|
|
122
|
+
`[pre-push-gate] Refusing to push a claude/* branch to remote (user policy).\n` +
|
|
123
|
+
` Use a non-agent branch name, or override with CONSTRUCT_ALLOW_CLAUDE_PUSH=1 git push ...\n`,
|
|
124
|
+
);
|
|
125
|
+
emitRoleEvent({
|
|
126
|
+
type: 'push_gate.fail',
|
|
127
|
+
summary: 'claude/* branch push refused',
|
|
128
|
+
hookInput: input,
|
|
129
|
+
context: { command },
|
|
130
|
+
});
|
|
131
|
+
process.exit(2);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const branch = execSync('git branch --show-current', { cwd, timeout: 1000, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
136
|
+
.toString().trim();
|
|
137
|
+
if (branch && branch !== 'main' && branch !== 'dev' && branch !== 'master') {
|
|
138
|
+
const json = execSync(
|
|
139
|
+
`gh run list --branch=${JSON.stringify(branch)} --limit=1 --json conclusion,databaseId,url`,
|
|
140
|
+
{ cwd, timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] },
|
|
141
|
+
).toString().trim();
|
|
142
|
+
if (json) {
|
|
143
|
+
const [run] = JSON.parse(json);
|
|
144
|
+
if (run?.conclusion === 'failure') {
|
|
145
|
+
process.stderr.write(
|
|
146
|
+
`[pre-push-gate] Last CI run on '${branch}' FAILED — fix it before pushing more.\n` +
|
|
147
|
+
` Run: ${run.url || `gh run view ${run.databaseId}`}\n` +
|
|
148
|
+
` Override: CONSTRUCT_SKIP_PREPUSH=1 git push ...\n`,
|
|
149
|
+
);
|
|
150
|
+
emitRoleEvent({
|
|
151
|
+
type: 'push_gate.fail',
|
|
152
|
+
summary: 'remote CI red — push blocked',
|
|
153
|
+
hookInput: input,
|
|
154
|
+
context: { branch, runId: run.databaseId, runUrl: run.url },
|
|
155
|
+
});
|
|
156
|
+
process.exit(2);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch {}
|
|
161
|
+
|
|
27
162
|
function findUp(filename, from) {
|
|
28
163
|
let dir = from;
|
|
29
164
|
for (let i = 0; i < 8; i++) {
|
|
@@ -52,8 +187,14 @@ function runNpmGate(projectDir) {
|
|
|
52
187
|
: 'npm';
|
|
53
188
|
|
|
54
189
|
const jobs = [];
|
|
55
|
-
if (hasTest || hasCi) jobs.push({ label: 'tests',
|
|
56
|
-
if (hasBuild) jobs.push({ label: 'build',
|
|
190
|
+
if (hasTest || hasCi) jobs.push({ label: 'tests', cmd: runner, args: ['run', testScript], timeout: 90_000 });
|
|
191
|
+
if (hasBuild) jobs.push({ label: 'build', cmd: runner, args: ['run', 'build'], timeout: 120_000 });
|
|
192
|
+
jobs.push({ label: 'audit', cmd: 'npm', args: ['audit', '--omit=dev', '--audit-level=high'], timeout: 30_000 });
|
|
193
|
+
|
|
194
|
+
if (existsSync(join(projectDir, 'bin/construct'))) {
|
|
195
|
+
jobs.push({ label: 'evals', cmd: 'node', args: ['bin/construct', 'evals', 'retrieval'], timeout: 60_000 });
|
|
196
|
+
jobs.push({ label: 'docs', cmd: 'node', args: ['bin/construct', 'docs:verify'], timeout: 15_000 });
|
|
197
|
+
}
|
|
57
198
|
|
|
58
199
|
return { projectDir, jobs };
|
|
59
200
|
}
|
|
@@ -88,42 +229,64 @@ else if (pyproj) gate = runPythonGate(pyproj.dir);
|
|
|
88
229
|
// Nothing to check
|
|
89
230
|
if (!gate || gate.jobs.length === 0) { echo(); }
|
|
90
231
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
232
|
+
// Jobs run concurrently. Wall-clock = max(test, build) instead of sum, so the
|
|
233
|
+
// gate fits inside the harness's 180s timeout even when both jobs near their
|
|
234
|
+
// individual limits.
|
|
235
|
+
function runJob(job) {
|
|
236
|
+
return new Promise((resolve) => {
|
|
237
|
+
let stdoutBuf = '';
|
|
238
|
+
let stderrBuf = '';
|
|
239
|
+
const child = spawn(job.cmd, job.args, {
|
|
240
|
+
cwd: gate.projectDir,
|
|
241
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
242
|
+
env: { ...process.env, CI: '1', FORCE_COLOR: '0' },
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const timer = setTimeout(() => {
|
|
246
|
+
try { child.kill('SIGKILL'); } catch { /* already gone */ }
|
|
247
|
+
}, job.timeout);
|
|
94
248
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
249
|
+
child.stdout?.on('data', (chunk) => { stdoutBuf += chunk.toString(); });
|
|
250
|
+
child.stderr?.on('data', (chunk) => { stderrBuf += chunk.toString(); });
|
|
251
|
+
child.on('error', (err) => {
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
resolve({ label: job.label, status: -1, stdout: stdoutBuf, stderr: stderrBuf || err.message, error: err });
|
|
254
|
+
});
|
|
255
|
+
child.on('close', (code) => {
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
resolve({ label: job.label, status: code ?? -1, stdout: stdoutBuf, stderr: stderrBuf });
|
|
258
|
+
});
|
|
102
259
|
});
|
|
260
|
+
}
|
|
103
261
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
failures.push({ label: job.label, detail: firstError || `exited ${result.status}` });
|
|
116
|
-
}
|
|
262
|
+
const results = await Promise.all(gate.jobs.map(runJob));
|
|
263
|
+
const failures = [];
|
|
264
|
+
for (const result of results) {
|
|
265
|
+
if (result.status === 0) continue;
|
|
266
|
+
const detail = (result.stderr || result.stdout || '').trim();
|
|
267
|
+
const firstError = detail.split('\n')
|
|
268
|
+
.map((l) => l.trim())
|
|
269
|
+
.filter((l) => l && !/^>/.test(l) && !/^\s*at /.test(l))
|
|
270
|
+
.slice(0, 3)
|
|
271
|
+
.join(' · ');
|
|
272
|
+
failures.push({ label: result.label, detail: firstError || `exited ${result.status}` });
|
|
117
273
|
}
|
|
118
274
|
|
|
119
275
|
if (failures.length === 0) { echo(); }
|
|
120
276
|
|
|
121
|
-
// Block the push
|
|
122
277
|
const summary = failures
|
|
123
|
-
.map(f => `${f.label} failed — ${f.detail}`)
|
|
278
|
+
.map((f) => `${f.label} failed — ${f.detail}`)
|
|
124
279
|
.join('\n');
|
|
125
280
|
|
|
126
281
|
process.stderr.write(
|
|
127
282
|
`[pre-push-gate] Push blocked — fix these before pushing:\n${summary}\n\nRun the failing checks locally, then push again.\n`
|
|
128
283
|
);
|
|
284
|
+
|
|
285
|
+
emitRoleEvent({
|
|
286
|
+
type: 'push_gate.fail',
|
|
287
|
+
summary,
|
|
288
|
+
hookInput: input,
|
|
289
|
+
context: { failures },
|
|
290
|
+
});
|
|
291
|
+
|
|
129
292
|
process.exit(2);
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/hooks/proactive-activation.mjs — Event-driven specialist activation.
|
|
3
|
+
*
|
|
4
|
+
* Instead of waiting for user input, specialists can activate proactively
|
|
5
|
+
* based on:
|
|
6
|
+
* - File changes (security on auth code, docs-keeper on README changes)
|
|
7
|
+
* - CI failures (debugger on test fails, qa on coverage drops)
|
|
8
|
+
* - Schedule (docs-keeper on stale READMEs, sre on SLO reviews)
|
|
9
|
+
* - Intake signals (automatic classification → assignment)
|
|
10
|
+
*
|
|
11
|
+
* Each specialist defines triggers in agents/role-manifests.json:
|
|
12
|
+
* {
|
|
13
|
+
* "events": ["test.fail", "secrets.detected"],
|
|
14
|
+
* "severityImmediate": ["secrets.detected"],
|
|
15
|
+
* "fence": { ... },
|
|
16
|
+
* ...
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* The gateway routes events to owning specialists with rate limiting.
|
|
20
|
+
*
|
|
21
|
+
* @p95ms 5
|
|
22
|
+
* @maxBlockingScope none
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { EVENT_OWNERSHIP } from '../orchestration-policy.mjs';
|
|
26
|
+
import { loadProjectConfig } from '../config/project-config.mjs';
|
|
27
|
+
|
|
28
|
+
// Rate limiting: max activations per specialist per hour
|
|
29
|
+
const DEFAULT_RATE_LIMIT = 5;
|
|
30
|
+
const RATE_WINDOW_MS = 60 * 60 * 1000; // 1 hour
|
|
31
|
+
|
|
32
|
+
// Cooldown: minimum time between activations for same specialist
|
|
33
|
+
const DEFAULT_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
|
|
34
|
+
|
|
35
|
+
// Activation state tracking
|
|
36
|
+
const activationState = new Map();
|
|
37
|
+
|
|
38
|
+
function getActivationState(specialist) {
|
|
39
|
+
if (!activationState.has(specialist)) {
|
|
40
|
+
activationState.set(specialist, {
|
|
41
|
+
activations: [],
|
|
42
|
+
lastActivation: null,
|
|
43
|
+
totalActivations: 0,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return activationState.get(specialist);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pruneOldActivations(state, windowMs) {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
state.activations = state.activations.filter(ts => now - ts < windowMs);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if a specialist can be activated (rate limit + cooldown).
|
|
56
|
+
*/
|
|
57
|
+
export function canActivate(specialist, options = {}) {
|
|
58
|
+
const {
|
|
59
|
+
rateLimit = DEFAULT_RATE_LIMIT,
|
|
60
|
+
cooldownMs = DEFAULT_COOLDOWN_MS,
|
|
61
|
+
windowMs = RATE_WINDOW_MS,
|
|
62
|
+
} = options;
|
|
63
|
+
|
|
64
|
+
const state = getActivationState(specialist);
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
|
|
67
|
+
// Check cooldown
|
|
68
|
+
if (state.lastActivation && now - state.lastActivation < cooldownMs) {
|
|
69
|
+
return {
|
|
70
|
+
allowed: false,
|
|
71
|
+
reason: 'cooldown',
|
|
72
|
+
retryAfter: cooldownMs - (now - state.lastActivation),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check rate limit
|
|
77
|
+
pruneOldActivations(state, windowMs);
|
|
78
|
+
if (state.activations.length >= rateLimit) {
|
|
79
|
+
return {
|
|
80
|
+
allowed: false,
|
|
81
|
+
reason: 'rate-limit',
|
|
82
|
+
retryAfter: windowMs - (now - state.activations[0]),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { allowed: true };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Record an activation for rate limiting.
|
|
91
|
+
*/
|
|
92
|
+
export function recordActivation(specialist) {
|
|
93
|
+
const state = getActivationState(specialist);
|
|
94
|
+
state.activations.push(Date.now());
|
|
95
|
+
state.lastActivation = Date.now();
|
|
96
|
+
state.totalActivations++;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get activation stats for a specialist.
|
|
101
|
+
*/
|
|
102
|
+
export function getActivationStats(specialist) {
|
|
103
|
+
const state = getActivationState(specialist);
|
|
104
|
+
pruneOldActivations(state, RATE_WINDOW_MS);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
recentActivations: state.activations.length,
|
|
108
|
+
lastActivation: state.lastActivation,
|
|
109
|
+
totalActivations: state.totalActivations,
|
|
110
|
+
rateLimit: DEFAULT_RATE_LIMIT,
|
|
111
|
+
rateWindowMs: RATE_WINDOW_MS,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Route an event to the owning specialist.
|
|
117
|
+
*/
|
|
118
|
+
export function routeEvent(eventType, eventData = {}) {
|
|
119
|
+
const owner = EVENT_OWNERSHIP[eventType];
|
|
120
|
+
|
|
121
|
+
if (!owner) {
|
|
122
|
+
return {
|
|
123
|
+
routed: false,
|
|
124
|
+
reason: 'no-owner',
|
|
125
|
+
eventType,
|
|
126
|
+
message: `No specialist owns event type: ${eventType}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check if specialist can activate
|
|
131
|
+
const canActivateResult = canActivate(owner);
|
|
132
|
+
|
|
133
|
+
if (!canActivateResult.allowed) {
|
|
134
|
+
return {
|
|
135
|
+
routed: false,
|
|
136
|
+
reason: canActivateResult.reason,
|
|
137
|
+
eventType,
|
|
138
|
+
owner,
|
|
139
|
+
retryAfter: canActivateResult.retryAfter,
|
|
140
|
+
queued: true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Record activation
|
|
145
|
+
recordActivation(owner);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
routed: true,
|
|
149
|
+
owner,
|
|
150
|
+
eventType,
|
|
151
|
+
eventData,
|
|
152
|
+
activationNumber: getActivationState(owner).totalActivations,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* File change watcher callback.
|
|
158
|
+
* Triggers specialists based on file paths and content patterns.
|
|
159
|
+
*/
|
|
160
|
+
export function onFileChange(filePath, changeType, content = '') {
|
|
161
|
+
const events = [];
|
|
162
|
+
|
|
163
|
+
// Security: auth-related files
|
|
164
|
+
if (filePath.includes('auth') || filePath.includes('security') || filePath.includes('permission')) {
|
|
165
|
+
events.push({ type: 'file.change.auth', data: { filePath, changeType } });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Docs-keeper: README, docs changes
|
|
169
|
+
if (filePath.includes('README') || filePath.startsWith('docs/')) {
|
|
170
|
+
events.push({ type: 'file.change.docs', data: { filePath, changeType } });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// SRE: infra, config, deployment files
|
|
174
|
+
if (filePath.includes('infra') || filePath.includes('deploy') || filePath.endsWith('.yaml') || filePath.endsWith('.yml')) {
|
|
175
|
+
events.push({ type: 'file.change.infra', data: { filePath, changeType } });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Debugger: test files
|
|
179
|
+
if (filePath.includes('.test.') || filePath.includes('.spec.') || filePath.startsWith('tests/')) {
|
|
180
|
+
events.push({ type: 'file.change.tests', data: { filePath, changeType } });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return events;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* CI failure watcher callback.
|
|
188
|
+
* Triggers specialists based on failure type.
|
|
189
|
+
*/
|
|
190
|
+
export function onCIFailure(failureType, details = {}) {
|
|
191
|
+
const events = [];
|
|
192
|
+
|
|
193
|
+
if (failureType === 'test.fail') {
|
|
194
|
+
events.push({ type: 'test.fail', data: details });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (failureType === 'test.flake') {
|
|
198
|
+
events.push({ type: 'test.flake', data: details });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (failureType === 'coverage.drop') {
|
|
202
|
+
events.push({ type: 'coverage.drop', data: details });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (failureType === 'build.fail') {
|
|
206
|
+
events.push({ type: 'build.fail', data: details });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (failureType === 'lint.fail') {
|
|
210
|
+
events.push({ type: 'lint.fail', data: details });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return events;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Scheduled activation check.
|
|
218
|
+
* Runs periodically to trigger time-based activations.
|
|
219
|
+
*/
|
|
220
|
+
export function onScheduleCheck() {
|
|
221
|
+
const events = [];
|
|
222
|
+
const now = new Date();
|
|
223
|
+
|
|
224
|
+
// Monday 9am: SRE SLO review
|
|
225
|
+
if (now.getDay() === 1 && now.getHours() === 9) {
|
|
226
|
+
events.push({ type: 'schedule.slo-review', data: { day: 'monday' } });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Daily 6pm: Docs-keeper stale doc check
|
|
230
|
+
if (now.getHours() === 18) {
|
|
231
|
+
events.push({ type: 'schedule.stale-docs', data: { time: 'daily' } });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// First of month: Security CVE review
|
|
235
|
+
if (now.getDate() === 1) {
|
|
236
|
+
events.push({ type: 'schedule.cve-review', data: { frequency: 'monthly' } });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return events;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get all pending activations for a specialist.
|
|
244
|
+
*/
|
|
245
|
+
export function getPendingActivations(specialist) {
|
|
246
|
+
// In a full implementation, this would query a pending queue
|
|
247
|
+
// For now, return recent activation history
|
|
248
|
+
const state = getActivationState(specialist);
|
|
249
|
+
return {
|
|
250
|
+
pending: 0,
|
|
251
|
+
recent: state.activations.length,
|
|
252
|
+
total: state.totalActivations,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Clear activation state (for testing or manual reset).
|
|
258
|
+
*/
|
|
259
|
+
export function clearActivationState(specialist) {
|
|
260
|
+
if (specialist) {
|
|
261
|
+
activationState.delete(specialist);
|
|
262
|
+
} else {
|
|
263
|
+
activationState.clear();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get activation summary for dashboard.
|
|
269
|
+
*/
|
|
270
|
+
export function getActivationSummary() {
|
|
271
|
+
const summary = {};
|
|
272
|
+
|
|
273
|
+
for (const [specialist, state] of activationState.entries()) {
|
|
274
|
+
pruneOldActivations(state, RATE_WINDOW_MS);
|
|
275
|
+
summary[specialist] = {
|
|
276
|
+
recentActivations: state.activations.length,
|
|
277
|
+
totalActivations: state.totalActivations,
|
|
278
|
+
lastActivation: state.lastActivation,
|
|
279
|
+
rateLimitRemaining: DEFAULT_RATE_LIMIT - state.activations.length,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return summary;
|
|
284
|
+
}
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* lib/hooks/read-tracker.mjs — Read tracker hook — tracks file reads for efficiency analysis.
|
|
4
4
|
*
|
|
5
5
|
* Runs as PostToolUse after Read tool calls. Logs each read to ~/.cx/read-log.json including file path, line count, and session timestamp for efficiency reporting.
|
|
6
|
+
*
|
|
7
|
+
* @p95ms 10
|
|
8
|
+
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
6
9
|
*/
|
|
7
|
-
// PostToolUse(Read) — records file content hash after each Read call
|
|
8
|
-
// Powers hash-staleness check in edit-guard.mjs
|
|
9
10
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, appendFileSync } from 'fs';
|
|
10
11
|
import { createHash } from 'crypto';
|
|
11
12
|
import { join, resolve } from 'path';
|
|
@@ -17,9 +18,18 @@ const EFFICIENCY_STORE = join(CX_DIR, 'session-efficiency.json');
|
|
|
17
18
|
const WARN_FLAGS = join(CX_DIR, 'warn-flags.txt');
|
|
18
19
|
const SESSION_IDLE_RESET_MS = 2 * 60 * 60 * 1000;
|
|
19
20
|
const REPEATED_READ_WARNING_THRESHOLD = 5;
|
|
21
|
+
const REPEATED_READ_TIER2_THRESHOLD = 8;
|
|
20
22
|
const LARGE_READ_WARNING_THRESHOLD = 3;
|
|
21
23
|
const TOTAL_BYTES_WARNING_THRESHOLD = 750_000;
|
|
22
24
|
|
|
25
|
+
// Bound per-session `stats.files` so the read-tracking map cannot grow
|
|
26
|
+
// without limit across long sessions. The hook is a fresh node process per
|
|
27
|
+
// Read, so every invocation must read-mutate-write the full state — there
|
|
28
|
+
// is no in-memory accumulator that would survive between calls. The LRU cap
|
|
29
|
+
// drops the oldest entries by `lastReadAt`.
|
|
30
|
+
|
|
31
|
+
const FILES_LRU_CAP = 200;
|
|
32
|
+
|
|
23
33
|
function loadEfficiencyStats(nowIso) {
|
|
24
34
|
const fresh = {
|
|
25
35
|
sessionStartedAt: nowIso,
|
|
@@ -85,8 +95,6 @@ try {
|
|
|
85
95
|
if (new Date(v.ts).getTime() < cutoff) delete store[k];
|
|
86
96
|
}
|
|
87
97
|
|
|
88
|
-
writeFileSync(HASH_STORE, JSON.stringify(store, null, 2));
|
|
89
|
-
|
|
90
98
|
const stats = loadEfficiencyStats(nowIso);
|
|
91
99
|
const existingFile = stats.files[absPath];
|
|
92
100
|
stats.readCount += 1;
|
|
@@ -104,11 +112,25 @@ try {
|
|
|
104
112
|
lastRequestedLimit: effectiveLimit,
|
|
105
113
|
};
|
|
106
114
|
|
|
115
|
+
// LRU cap: bound stats.files to FILES_LRU_CAP entries by lastReadAt so the
|
|
116
|
+
// structure does not grow without limit across long sessions.
|
|
117
|
+
const fileEntries = Object.entries(stats.files);
|
|
118
|
+
if (fileEntries.length > FILES_LRU_CAP) {
|
|
119
|
+
fileEntries.sort((a, b) => (a[1]?.lastReadAt || '').localeCompare(b[1]?.lastReadAt || ''));
|
|
120
|
+
const drop = fileEntries.length - FILES_LRU_CAP;
|
|
121
|
+
for (let i = 0; i < drop; i++) delete stats.files[fileEntries[i][0]];
|
|
122
|
+
}
|
|
123
|
+
|
|
107
124
|
if (stats.repeatedReadCount >= REPEATED_READ_WARNING_THRESHOLD && !stats.warnings.repeatedReads) {
|
|
108
125
|
const top = topRepeatedPath(stats.files);
|
|
109
126
|
const topNote = top ? ` Top repeat: ${top.filePath} (${top.count}x).` : '';
|
|
110
127
|
appendWarning(`Efficiency: ${stats.repeatedReadCount} repeated reads this session.${topNote} Use rg or construct distill before re-reading more files.`);
|
|
111
128
|
stats.warnings.repeatedReads = nowIso;
|
|
129
|
+
|
|
130
|
+
if (stats.repeatedReadCount >= REPEATED_READ_TIER2_THRESHOLD && !stats.warnings.repeatedReadsTier2) {
|
|
131
|
+
appendWarning(`Efficiency: ${stats.repeatedReadCount} repeated reads this session — consider using construct distill or rg before re-reading entire files.`);
|
|
132
|
+
stats.warnings.repeatedReadsTier2 = nowIso;
|
|
133
|
+
}
|
|
112
134
|
}
|
|
113
135
|
|
|
114
136
|
if (stats.largeReadCount >= LARGE_READ_WARNING_THRESHOLD && !stats.warnings.largeReads) {
|
|
@@ -123,6 +145,7 @@ try {
|
|
|
123
145
|
|
|
124
146
|
stats.lastUpdatedAt = nowIso;
|
|
125
147
|
|
|
148
|
+
writeFileSync(HASH_STORE, JSON.stringify(store, null, 2));
|
|
126
149
|
writeFileSync(EFFICIENCY_STORE, JSON.stringify(stats, null, 2));
|
|
127
150
|
} catch { /* best effort */ }
|
|
128
151
|
|