@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
package/lib/hooks/mcp-audit.mjs
CHANGED
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
* lib/hooks/mcp-audit.mjs — MCP audit hook — logs all MCP tool calls for observability and review.
|
|
4
4
|
*
|
|
5
5
|
* Runs as PostToolUse on MCP tool calls. Records tool name, input, and output summary to ~/.cx/mcp-audit.json for telemetry and security review.
|
|
6
|
+
*
|
|
7
|
+
* @p95ms 10
|
|
8
|
+
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
6
9
|
*/
|
|
7
|
-
// PostToolUse(mcp__*) — audits every MCP tool call against the active workflow task.
|
|
8
|
-
// Writes .cx/mcp-audit.json: per-server call log with task attribution.
|
|
9
|
-
// Gives per-task MCP usage visibility that per-task scoped MCPs (OmO) cannot provide.
|
|
10
10
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
11
11
|
import { join } from 'path';
|
|
12
|
+
import { shouldCreateCx } from '../project-detection.mjs';
|
|
12
13
|
|
|
13
14
|
let input = {};
|
|
14
15
|
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
15
16
|
|
|
16
|
-
const cwd = input?.cwd || process.cwd();
|
|
17
17
|
const toolName = input?.tool_name || '';
|
|
18
18
|
|
|
19
19
|
const match = toolName.match(/^mcp__([^_]+(?:__[^_]+)*)__(.+)$/);
|
|
@@ -22,36 +22,34 @@ if (!match) process.exit(0);
|
|
|
22
22
|
const mcpServer = match[1].replace(/__/g, '-');
|
|
23
23
|
const mcpTool = match[2];
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const wf = JSON.parse(readFileSync(join(cwd, '.cx', 'workflow.json'), 'utf8'));
|
|
30
|
-
activeTaskKey = wf.currentTaskKey || null;
|
|
31
|
-
if (activeTaskKey) {
|
|
32
|
-
const t = (wf.tasks || []).find(t => t.key === activeTaskKey);
|
|
33
|
-
activeTaskTitle = t?.title || null;
|
|
34
|
-
}
|
|
35
|
-
} catch { /* no workflow */ }
|
|
25
|
+
const cwd = input?.cwd || process.cwd();
|
|
26
|
+
|
|
27
|
+
// Only create .cx if this is an initialized project
|
|
28
|
+
const shouldCreateCxDir = shouldCreateCx(cwd);
|
|
36
29
|
|
|
37
30
|
const auditPath = join(cwd, '.cx', 'mcp-audit.json');
|
|
38
31
|
let audit = {};
|
|
39
|
-
|
|
32
|
+
|
|
33
|
+
// Try to read existing audit file if it exists
|
|
34
|
+
if (existsSync(auditPath)) {
|
|
35
|
+
try { audit = JSON.parse(readFileSync(auditPath, 'utf8')); } catch { /* fresh */ }
|
|
36
|
+
}
|
|
40
37
|
|
|
41
38
|
if (!audit[mcpServer]) audit[mcpServer] = [];
|
|
42
39
|
audit[mcpServer].push({
|
|
43
40
|
tool: mcpTool,
|
|
44
41
|
ts: new Date().toISOString(),
|
|
45
|
-
|
|
46
|
-
taskTitle: activeTaskTitle,
|
|
42
|
+
coordination: 'tracker-plus-plan',
|
|
47
43
|
});
|
|
48
44
|
|
|
49
45
|
// Keep last 200 per server
|
|
50
46
|
if (audit[mcpServer].length > 200) audit[mcpServer] = audit[mcpServer].slice(-200);
|
|
51
47
|
|
|
52
48
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
if (shouldCreateCxDir) {
|
|
50
|
+
mkdirSync(join(cwd, '.cx'), { recursive: true });
|
|
51
|
+
writeFileSync(auditPath, JSON.stringify(audit, null, 2));
|
|
52
|
+
}
|
|
55
53
|
} catch { /* best effort */ }
|
|
56
54
|
|
|
57
55
|
process.exit(0);
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* lib/hooks/mcp-health-check.mjs — MCP health check hook — verifies MCP servers are reachable before tool use.
|
|
4
4
|
*
|
|
5
5
|
* Runs as PreToolUse on MCP tool calls. Checks that the target MCP server is running and reachable. Emits a warning (does not block) if the server is unavailable.
|
|
6
|
+
*
|
|
7
|
+
* @p95ms 51
|
|
8
|
+
* @maxBlockingScope none (PreToolUse, warn-only)
|
|
6
9
|
*/
|
|
7
10
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
8
11
|
import { homedir } from 'os';
|
|
@@ -47,5 +50,38 @@ if (entry?.status === 'unhealthy' && (Date.now() - entry.since) < CACHE_TTL_MS)
|
|
|
47
50
|
process.exit(2);
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
// Role-fence check for MCP tool calls. Most personas have no MCP fence
|
|
54
|
+
// declared (allows everything) — when fence.allowedMcpTools is present, the
|
|
55
|
+
// tool name must match an entry. Out-of-fence MCP calls block with exit 2.
|
|
56
|
+
|
|
57
|
+
if (process.env.CONSTRUCT_ROLES !== 'off') {
|
|
58
|
+
try {
|
|
59
|
+
const lastAgentPath = `${homedir()}/.cx/last-agent.json`;
|
|
60
|
+
if (existsSync(lastAgentPath)) {
|
|
61
|
+
const last = JSON.parse(readFileSync(lastAgentPath, 'utf8'));
|
|
62
|
+
const lastTs = last?.ts ? Date.parse(last.ts) : 0;
|
|
63
|
+
const fresh = lastTs && (Date.now() - lastTs) < 10 * 60 * 1000;
|
|
64
|
+
const id = String(last?.agent || '').replace(/^cx-/, '');
|
|
65
|
+
if (fresh && id) {
|
|
66
|
+
const { isOnboarded, loadManifest } = await import('../roles/manifest.mjs');
|
|
67
|
+
if (isOnboarded(id)) {
|
|
68
|
+
const manifest = loadManifest(id);
|
|
69
|
+
const allowedMcpTools = manifest?.fence?.allowedMcpTools;
|
|
70
|
+
if (Array.isArray(allowedMcpTools) && allowedMcpTools.length > 0) {
|
|
71
|
+
const matches = allowedMcpTools.some((p) => toolName === p || toolName.startsWith(p));
|
|
72
|
+
if (!matches) {
|
|
73
|
+
process.stderr.write(
|
|
74
|
+
`[fence] cx-${id} cannot call MCP tool ${toolName} — outside declared fence.\n` +
|
|
75
|
+
`Allowed MCP tools: ${allowedMcpTools.join(', ')}\n`
|
|
76
|
+
);
|
|
77
|
+
process.exit(2);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch { /* best effort */ }
|
|
84
|
+
}
|
|
85
|
+
|
|
50
86
|
process.stdout.write(JSON.stringify(input) + '\n');
|
|
51
87
|
process.exit(0);
|
|
@@ -1,36 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* lib/hooks/model-fallback.mjs —
|
|
3
|
+
* lib/hooks/model-fallback.mjs — Provider-aware model fallback hook.
|
|
4
4
|
*
|
|
5
|
-
* Runs as PostToolUse
|
|
5
|
+
* Runs as PostToolUse. On a retryable provider failure it:
|
|
6
|
+
* 1. Classifies the error via classifyProviderFailure.
|
|
7
|
+
* 2. Selects a fallback candidate via selectFallbackModel, skipping any
|
|
8
|
+
* provider currently in its 5-minute cooldown window.
|
|
9
|
+
* 3. Writes the new model directly to the project .env via applyToEnv.
|
|
10
|
+
* 4. Records a cooldown entry for the failing provider.
|
|
11
|
+
*
|
|
12
|
+
* Falls back to `construct models --apply` only when no candidate is resolved
|
|
13
|
+
* (e.g. no registry fallback list, all candidates on cooldown, or no OpenRouter
|
|
14
|
+
* key for the free-tier path).
|
|
15
|
+
*
|
|
16
|
+
* @p95ms 150
|
|
17
|
+
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
6
18
|
*/
|
|
7
19
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
20
|
import { execFileSync } from 'node:child_process';
|
|
9
21
|
import { homedir } from 'node:os';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import {
|
|
24
|
+
applyToEnv,
|
|
25
|
+
classifyProviderFailure,
|
|
26
|
+
readOpenRouterApiKeyFromOpenCodeConfig,
|
|
27
|
+
selectFallbackModel,
|
|
28
|
+
writeProviderCooldown,
|
|
29
|
+
} from '../model-router.mjs';
|
|
12
30
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
/rate limit/i,
|
|
16
|
-
/usage limits?/i,
|
|
17
|
-
/specified API usage limits?/i,
|
|
18
|
-
/regain access/i,
|
|
19
|
-
/weekly limit/i,
|
|
20
|
-
/monthly limit/i,
|
|
21
|
-
/daily limit/i,
|
|
22
|
-
/too many requests/i,
|
|
23
|
-
/quota exceeded/i,
|
|
24
|
-
/model unavailable/i,
|
|
25
|
-
/model.*overloaded/i,
|
|
26
|
-
/timeout/i,
|
|
27
|
-
/timed out/i,
|
|
28
|
-
/ETIMEDOUT/i,
|
|
29
|
-
/ECONNRESET/i,
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
const COOLDOWN_MS = 10 * 60 * 1000;
|
|
33
|
-
const statePath = join(homedir(), '.cx', 'construct-model-fallback.json');
|
|
31
|
+
const cooldownPath = join(homedir(), '.cx', 'provider-cooldowns.json');
|
|
32
|
+
const envPath = join(process.cwd(), '.env');
|
|
34
33
|
|
|
35
34
|
function readInput() {
|
|
36
35
|
try {
|
|
@@ -41,65 +40,52 @@ function readInput() {
|
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
if (value === null || value === undefined) return '';
|
|
46
|
-
if (typeof value === 'string') return value;
|
|
47
|
-
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
48
|
-
if (Array.isArray(value)) return value.map(flatten).join('\n');
|
|
49
|
-
if (typeof value === 'object') return Object.values(value).map(flatten).join('\n');
|
|
50
|
-
return '';
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function recentlyApplied(now) {
|
|
54
|
-
try {
|
|
55
|
-
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
56
|
-
return typeof state.lastAppliedAt === 'number' && now - state.lastAppliedAt < COOLDOWN_MS;
|
|
57
|
-
} catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
43
|
+
const input = readInput();
|
|
61
44
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
writeFileSync(statePath, JSON.stringify(payload, null, 2));
|
|
65
|
-
}
|
|
45
|
+
const classified = classifyProviderFailure(input);
|
|
46
|
+
if (!classified || !classified.retryable) process.exit(0);
|
|
66
47
|
|
|
67
|
-
|
|
68
|
-
const haystack = flatten(input);
|
|
69
|
-
if (!RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(haystack))) process.exit(0);
|
|
48
|
+
process.stderr.write(`[model-fallback] ${classified.kind} detected (provider: ${classified.provider ?? 'unknown'}).\n`);
|
|
70
49
|
|
|
71
50
|
const now = Date.now();
|
|
72
|
-
|
|
73
|
-
|
|
51
|
+
const result = selectFallbackModel({ hookInput: input, envPath, cooldownPath, now });
|
|
52
|
+
|
|
53
|
+
if (result) {
|
|
54
|
+
process.stderr.write(`[model-fallback] Switching ${result.tier} → ${result.targetModel} (reason: ${result.reason}).\n`);
|
|
55
|
+
applyToEnv(envPath, { [result.tier]: result.targetModel });
|
|
56
|
+
if (classified.provider) writeProviderCooldown(cooldownPath, classified.provider, now);
|
|
74
57
|
process.exit(0);
|
|
75
58
|
}
|
|
76
59
|
|
|
60
|
+
// No candidate resolved — fall back to full `construct models --apply` if possible.
|
|
77
61
|
const toolkitDir = process.env.CX_TOOLKIT_DIR || join(homedir(), '.construct');
|
|
78
62
|
const constructBin = join(toolkitDir, 'bin', 'construct');
|
|
79
63
|
|
|
80
64
|
if (!existsSync(constructBin)) {
|
|
81
|
-
process.stderr.write(`[model-fallback] construct binary not found at ${constructBin}
|
|
65
|
+
process.stderr.write(`[model-fallback] No fallback candidate and construct binary not found at ${constructBin}.\n`);
|
|
66
|
+
if (classified.provider) writeProviderCooldown(cooldownPath, classified.provider, now);
|
|
82
67
|
process.exit(0);
|
|
83
68
|
}
|
|
84
69
|
|
|
85
70
|
const openRouterApiKey = process.env.OPENROUTER_API_KEY || readOpenRouterApiKeyFromOpenCodeConfig();
|
|
86
71
|
|
|
87
72
|
if (!openRouterApiKey) {
|
|
88
|
-
process.stderr.write('[model-fallback]
|
|
73
|
+
process.stderr.write('[model-fallback] No fallback candidate and no OpenRouter API key — cannot poll models.\n');
|
|
74
|
+
if (classified.provider) writeProviderCooldown(cooldownPath, classified.provider, now);
|
|
89
75
|
process.exit(0);
|
|
90
76
|
}
|
|
91
77
|
|
|
92
78
|
try {
|
|
93
|
-
process.stderr.write('[model-fallback]
|
|
79
|
+
process.stderr.write('[model-fallback] No direct candidate — running construct models --apply.\n');
|
|
94
80
|
execFileSync(constructBin, ['models', '--apply'], {
|
|
95
81
|
cwd: toolkitDir,
|
|
96
82
|
stdio: 'inherit',
|
|
97
83
|
env: { ...process.env, CX_TOOLKIT_DIR: toolkitDir, OPENROUTER_API_KEY: openRouterApiKey },
|
|
98
84
|
timeout: 120_000,
|
|
99
85
|
});
|
|
100
|
-
|
|
86
|
+
if (classified.provider) writeProviderCooldown(cooldownPath, classified.provider, now);
|
|
101
87
|
} catch (error) {
|
|
102
|
-
process.stderr.write(`[model-fallback]
|
|
88
|
+
process.stderr.write(`[model-fallback] construct models --apply failed: ${error.message}\n`);
|
|
103
89
|
}
|
|
104
90
|
|
|
105
91
|
process.exit(0);
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/policy-engine.mjs — consolidated session policy enforcement hook.
|
|
4
|
+
*
|
|
5
|
+
* Handles three hook events:
|
|
6
|
+
* PreToolUse bootstrap gating (Write/Edit blocked until session is grounded)
|
|
7
|
+
* Stop drive-mode criteria blocks; red-CI and open-bd are advisory warns
|
|
8
|
+
* UserPromptSubmit reserved for workflow rule (no-op today)
|
|
9
|
+
*
|
|
10
|
+
* Stop is intentionally non-blocking for red CI and in-progress beads — those
|
|
11
|
+
* are normal states across sessions, not session-ending failures. Blocking
|
|
12
|
+
* with a sticky env-var bypass is the antipattern we removed elsewhere; it
|
|
13
|
+
* trades real friction for theoretical safety. Drive mode still blocks because
|
|
14
|
+
* drive mode represents an explicit user opt-in.
|
|
15
|
+
*
|
|
16
|
+
* @p95ms 80
|
|
17
|
+
* @maxBlockingScope PreToolUse, Stop, UserPromptSubmit
|
|
18
|
+
*/
|
|
19
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
20
|
+
import { execSync } from 'node:child_process';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { homedir } from 'node:os';
|
|
23
|
+
|
|
24
|
+
let input = {};
|
|
25
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')) || {}; } catch { process.exit(0); }
|
|
26
|
+
|
|
27
|
+
const hookEvent = process.env.CONSTRUCT_HOOK_EVENT || input?.hook_event || process.argv[2] || '';
|
|
28
|
+
const toolName = input?.tool_name || input?.tool || '';
|
|
29
|
+
const toolInput = input?.tool_input || {};
|
|
30
|
+
const cwd = input?.cwd || process.cwd();
|
|
31
|
+
const home = homedir();
|
|
32
|
+
|
|
33
|
+
function safeExec(cmd, opts = {}) {
|
|
34
|
+
try { return execSync(cmd, { cwd, stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000, ...opts }).toString().trim(); }
|
|
35
|
+
catch { return null; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (hookEvent === 'PreToolUse') {
|
|
39
|
+
// Bootstrap gate scope: only mutation tools (Write/Edit/MultiEdit/
|
|
40
|
+
// NotebookEdit/TodoWrite) require grounding. Bash is not gated here —
|
|
41
|
+
// dangerous Bash is owned by guard-bash.mjs; gating read-only shell
|
|
42
|
+
// here would block grep/find/git log and create chicken-and-egg
|
|
43
|
+
// friction when the same hook also needs Read calls to mark the
|
|
44
|
+
// session bootstrapped.
|
|
45
|
+
|
|
46
|
+
const BOOTSTRAP_TOOLS = new Set([
|
|
47
|
+
'mcp__construct-mcp__project_context',
|
|
48
|
+
'mcp__construct-mcp__memory_search',
|
|
49
|
+
'project_context', 'memory_search',
|
|
50
|
+
]);
|
|
51
|
+
const READ_TOOLS = new Set(['Read', 'Grep', 'Glob', 'LS', 'NotebookRead']);
|
|
52
|
+
const BLOCKED_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'TodoWrite']);
|
|
53
|
+
|
|
54
|
+
const stateDir = join(home, '.cx');
|
|
55
|
+
const statePath = join(stateDir, 'bootstrap-state.json');
|
|
56
|
+
const sessionId = input.session_id || input.sessionId || 'default';
|
|
57
|
+
|
|
58
|
+
let state = {};
|
|
59
|
+
if (existsSync(statePath)) {
|
|
60
|
+
try { state = JSON.parse(readFileSync(statePath, 'utf8')) || {}; } catch { state = {}; }
|
|
61
|
+
}
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
for (const [k, v] of Object.entries(state)) {
|
|
64
|
+
if (!v?.ts || now - v.ts > 24 * 60 * 60 * 1000) delete state[k];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const s = state[sessionId] || { ts: now, reads: 0, bootstrap: [], done: false };
|
|
68
|
+
const bootstrapSet = new Set(Array.isArray(s.bootstrap) ? s.bootstrap : []);
|
|
69
|
+
s.ts = now;
|
|
70
|
+
|
|
71
|
+
if (BOOTSTRAP_TOOLS.has(toolName)) bootstrapSet.add(toolName.replace(/^.*__/, ''));
|
|
72
|
+
if (READ_TOOLS.has(toolName)) s.reads = (s.reads || 0) + 1;
|
|
73
|
+
|
|
74
|
+
if (!s.done) {
|
|
75
|
+
const hasPair = bootstrapSet.has('project_context') && bootstrapSet.has('memory_search');
|
|
76
|
+
if (hasPair || (s.reads || 0) >= 3) s.done = true;
|
|
77
|
+
}
|
|
78
|
+
if (!s.done && READ_TOOLS.has(toolName) && (s.reads || 0) >= 2) s.done = true;
|
|
79
|
+
|
|
80
|
+
const block = !s.done && BLOCKED_TOOLS.has(toolName);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
mkdirSync(stateDir, { recursive: true });
|
|
84
|
+
state[sessionId] = { ...s, bootstrap: Array.from(bootstrapSet) };
|
|
85
|
+
writeFileSync(statePath, JSON.stringify(state));
|
|
86
|
+
} catch {}
|
|
87
|
+
|
|
88
|
+
if (block) {
|
|
89
|
+
process.stderr.write(
|
|
90
|
+
`[policy-engine/bootstrap] ${toolName} requires session bootstrap.\n` +
|
|
91
|
+
`Run in parallel first: project_context, memory_search.\n` +
|
|
92
|
+
`(Or do 3+ exploratory reads — Read/Grep/Glob/LS.)\n`
|
|
93
|
+
);
|
|
94
|
+
process.exit(2);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (hookEvent === 'Stop') {
|
|
101
|
+
const driveStatePath = join(cwd, '.cx', 'drive-state.json');
|
|
102
|
+
|
|
103
|
+
if (existsSync(driveStatePath)) {
|
|
104
|
+
let driveState = {};
|
|
105
|
+
try { driveState = JSON.parse(readFileSync(driveStatePath, 'utf8')); } catch { driveState = {}; }
|
|
106
|
+
|
|
107
|
+
if (driveState.active && !driveState.canStop) {
|
|
108
|
+
const allCriteria = Array.isArray(driveState.criteria) ? driveState.criteria : [];
|
|
109
|
+
const criteriaStatus = driveState.criteriaStatus || {};
|
|
110
|
+
const metCriteria = allCriteria.filter(c => criteriaStatus[c]?.met);
|
|
111
|
+
const unmetCriteria = allCriteria.filter(c => !criteriaStatus[c]?.met);
|
|
112
|
+
const pendingTodos = driveState.pendingTodos || 0;
|
|
113
|
+
const iteration = driveState.iteration || 1;
|
|
114
|
+
const momentum = driveState.momentumScore != null ? Math.round(driveState.momentumScore * 100) : null;
|
|
115
|
+
|
|
116
|
+
const hasPendingWork = unmetCriteria.length > 0 || (allCriteria.length === 0 && pendingTodos > 0);
|
|
117
|
+
if (hasPendingWork) {
|
|
118
|
+
const lines = [`[policy-engine/drive] Drive mode active — iteration ${iteration}. Cannot stop yet.`, ''];
|
|
119
|
+
if (unmetCriteria.length > 0) {
|
|
120
|
+
lines.push(`Unmet criteria (${unmetCriteria.length}/${allCriteria.length}):`);
|
|
121
|
+
for (const c of unmetCriteria) lines.push(` ✗ ${c}`);
|
|
122
|
+
lines.push('');
|
|
123
|
+
}
|
|
124
|
+
if (metCriteria.length > 0) {
|
|
125
|
+
lines.push(`Verified criteria (${metCriteria.length}/${allCriteria.length}):`);
|
|
126
|
+
for (const c of metCriteria) {
|
|
127
|
+
const ev = criteriaStatus[c]?.evidence || 'recorded';
|
|
128
|
+
lines.push(` ✓ ${c} — ${ev.slice(0, 80)}`);
|
|
129
|
+
}
|
|
130
|
+
lines.push('');
|
|
131
|
+
}
|
|
132
|
+
if (allCriteria.length === 0) {
|
|
133
|
+
lines.push(`Remaining: ${pendingTodos} pending todos`, '');
|
|
134
|
+
}
|
|
135
|
+
if (momentum != null) lines.push(`Momentum: ${momentum}% (iteration ${iteration})`);
|
|
136
|
+
lines.push('', `[policy-engine/drive] Set criteriaStatus["<criterion>"] = { met: true, evidence: "..." } to unblock.`);
|
|
137
|
+
|
|
138
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
139
|
+
process.exit(2);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const branch = safeExec('git branch --show-current');
|
|
145
|
+
const isFeatureBranch = branch && branch !== 'main' && branch !== 'dev' && branch !== 'master';
|
|
146
|
+
const filesChangedPath = join(home, '.cx', 'files-changed-count.txt');
|
|
147
|
+
let filesChanged = 0;
|
|
148
|
+
try { filesChanged = parseInt(readFileSync(filesChangedPath, 'utf8').trim() || '0', 10); } catch {}
|
|
149
|
+
|
|
150
|
+
if (isFeatureBranch && filesChanged > 0) {
|
|
151
|
+
const json = safeExec(
|
|
152
|
+
`gh run list --branch=${JSON.stringify(branch)} --limit=1 --json conclusion,databaseId,url`,
|
|
153
|
+
{ timeout: 4000 },
|
|
154
|
+
);
|
|
155
|
+
if (json) {
|
|
156
|
+
let run = null;
|
|
157
|
+
try { [run] = JSON.parse(json); } catch {}
|
|
158
|
+
if (run?.conclusion === 'failure') {
|
|
159
|
+
process.stderr.write(
|
|
160
|
+
`[policy-engine/red-ci] CI is red on '${branch}' (advisory).\n` +
|
|
161
|
+
` Run: ${run.url || `gh run view ${run.databaseId}`}\n` +
|
|
162
|
+
` Logs: gh run view ${run.databaseId} --log-failed\n`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (safeExec('command -v bd', { timeout: 1000 })) {
|
|
169
|
+
const bdJson = safeExec('bd list --status in_progress --json', { timeout: 3000 });
|
|
170
|
+
if (bdJson) {
|
|
171
|
+
let issues = [];
|
|
172
|
+
try { issues = JSON.parse(bdJson) || []; } catch {}
|
|
173
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
174
|
+
const lines = issues.slice(0, 10).map((iss) => {
|
|
175
|
+
const id = iss.id || iss.ID || '?';
|
|
176
|
+
const title = iss.title || iss.summary || '';
|
|
177
|
+
return ` - ${id}: ${title}`;
|
|
178
|
+
});
|
|
179
|
+
process.stderr.write(
|
|
180
|
+
`[policy-engine/open-bd] In-progress beads issues (${issues.length} total, advisory):\n` +
|
|
181
|
+
lines.join('\n') + '\n',
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const driveSessionPath = join(home, '.cx', 'drive-session.json');
|
|
188
|
+
if (existsSync(driveSessionPath)) {
|
|
189
|
+
let ds = {};
|
|
190
|
+
try { ds = JSON.parse(readFileSync(driveSessionPath, 'utf8')); } catch {}
|
|
191
|
+
if (ds?.open === true) {
|
|
192
|
+
const id = ds.sessionId || ds.id || '<unknown>';
|
|
193
|
+
const goal = ds.goal || ds.title || '';
|
|
194
|
+
process.stderr.write(
|
|
195
|
+
`[policy-engine/drive-session] Open drive session detected (id ${id}).\n` +
|
|
196
|
+
(goal ? ` Goal: ${goal}\n` : '') +
|
|
197
|
+
` Close it before assuming session is complete (advisory).\n`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (hookEvent === 'UserPromptSubmit') {
|
|
206
|
+
process.exit(0);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
process.exit(0);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* post-merge-docs-check.mjs — PostToolUse / Bash (async)
|
|
4
|
+
*
|
|
5
|
+
* When a merge or pull lands on the working tree (detected via `git log -1
|
|
6
|
+
* --format=%P` returning two parents), diff the merge and emit
|
|
7
|
+
* `pr.merged.no-docs` if `src/`, `lib/`, or `bin/` changed without a touching
|
|
8
|
+
* file under `docs/**` or `CHANGELOG.md`. Emits `changelog.missing`
|
|
9
|
+
* specifically when no CHANGELOG entry was added.
|
|
10
|
+
*
|
|
11
|
+
* @p95ms 1500
|
|
12
|
+
* @maxBlockingScope none (async, non-blocking)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { execSync } from 'node:child_process';
|
|
17
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
18
|
+
import { emitRoleEvent } from '../roles/hook-emit.mjs';
|
|
19
|
+
|
|
20
|
+
let input = {};
|
|
21
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); }
|
|
22
|
+
catch (err) { logHookFailure({ hook: 'post-merge-docs-check', err, phase: 'parse' }); process.exit(0); }
|
|
23
|
+
|
|
24
|
+
const command = input?.tool_input?.command || '';
|
|
25
|
+
const exitCode = input?.tool_response?.exit_code ?? input?.tool_response?.exitCode;
|
|
26
|
+
const isMergePull = /\bgit\s+(merge|pull|rebase|cherry-pick)\b/.test(command);
|
|
27
|
+
if (!isMergePull) process.exit(0);
|
|
28
|
+
if (exitCode !== 0 && exitCode !== undefined) process.exit(0);
|
|
29
|
+
|
|
30
|
+
const cwd = input?.cwd || process.cwd();
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const parents = execSync('git log -1 --format=%P', { cwd, timeout: 2000 }).toString().trim();
|
|
34
|
+
if (parents.split(/\s+/).filter(Boolean).length < 2) process.exit(0);
|
|
35
|
+
|
|
36
|
+
const files = execSync('git diff-tree --no-commit-id --name-only -r HEAD', { cwd, timeout: 2000 })
|
|
37
|
+
.toString().split('\n').filter(Boolean);
|
|
38
|
+
|
|
39
|
+
const codeChanged = files.some((f) => /^(src|lib|bin|app)\//.test(f));
|
|
40
|
+
if (!codeChanged) process.exit(0);
|
|
41
|
+
|
|
42
|
+
const docChanged = files.some((f) => /^docs\//.test(f) || /(^|\/)README\.md$/.test(f));
|
|
43
|
+
const changelogChanged = files.some((f) => /(^|\/)CHANGELOG\.md$/.test(f));
|
|
44
|
+
|
|
45
|
+
if (!changelogChanged) {
|
|
46
|
+
emitRoleEvent({
|
|
47
|
+
type: 'changelog.missing',
|
|
48
|
+
summary: `Merge landed without CHANGELOG update — ${files.length} file(s) changed`,
|
|
49
|
+
hookInput: input,
|
|
50
|
+
context: { files: files.slice(0, 50) },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (!docChanged && !changelogChanged) {
|
|
54
|
+
emitRoleEvent({
|
|
55
|
+
type: 'pr.merged.no-docs',
|
|
56
|
+
summary: `Merge landed touching code without docs — ${files.length} file(s) changed`,
|
|
57
|
+
hookInput: input,
|
|
58
|
+
context: { files: files.slice(0, 50) },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} catch {}
|
|
62
|
+
|
|
63
|
+
process.exit(0);
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* lib/hooks/pre-compact.mjs — Pre-compact hook — prepares context summary before compaction runs.
|
|
4
4
|
*
|
|
5
|
-
* Runs before context compaction. Writes a structured summary of
|
|
5
|
+
* Runs before context compaction. Writes a structured summary of context state and pending tasks to preserve continuity across the compaction boundary.
|
|
6
|
+
*
|
|
7
|
+
* @p95ms 100
|
|
8
|
+
* @maxBlockingScope PreCompact
|
|
6
9
|
*/
|
|
7
10
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
8
11
|
import { homedir } from 'os';
|
|
@@ -18,7 +21,6 @@ const transcriptPath = input?.transcript_path || '';
|
|
|
18
21
|
const filesChanged = [];
|
|
19
22
|
const decisions = [];
|
|
20
23
|
const pendingTodos = [];
|
|
21
|
-
const workflowTasks = [];
|
|
22
24
|
const warnFlagsPath = join(homedir(), '.cx', 'warn-flags.txt');
|
|
23
25
|
const efficiencyPath = join(homedir(), '.cx', 'session-efficiency.json');
|
|
24
26
|
let lastSummary = '';
|
|
@@ -85,19 +87,6 @@ if (transcriptPath && existsSync(transcriptPath)) {
|
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
// Extract active workflow tasks from .cx/workflow.json
|
|
89
|
-
const workflowPath = join(cwd, '.cx', 'workflow.json');
|
|
90
|
-
if (existsSync(workflowPath)) {
|
|
91
|
-
try {
|
|
92
|
-
const wf = JSON.parse(readFileSync(workflowPath, 'utf8'));
|
|
93
|
-
const tasks = wf?.tasks || wf?.state?.tasks || [];
|
|
94
|
-
for (const task of tasks) {
|
|
95
|
-
if (task.status !== 'done' && task.status !== 'skipped') {
|
|
96
|
-
workflowTasks.push({ key: task.key || task.id, title: task.title || task.description || '' });
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
} catch { /* best effort */ }
|
|
100
|
-
}
|
|
101
90
|
} catch { /* best effort */ }
|
|
102
91
|
}
|
|
103
92
|
|
|
@@ -154,14 +143,6 @@ if (pendingTodos.length) {
|
|
|
154
143
|
contextLines.push('');
|
|
155
144
|
}
|
|
156
145
|
|
|
157
|
-
if (workflowTasks.length) {
|
|
158
|
-
contextLines.push('## Active workflow tasks');
|
|
159
|
-
for (const task of workflowTasks.slice(0, 10)) {
|
|
160
|
-
contextLines.push(`- ${task.key}: ${task.title}`);
|
|
161
|
-
}
|
|
162
|
-
contextLines.push('');
|
|
163
|
-
}
|
|
164
|
-
|
|
165
146
|
if (efficiencySummary) {
|
|
166
147
|
contextLines.push('## Session efficiency snapshot');
|
|
167
148
|
contextLines.push(efficiencySummary);
|
|
@@ -179,7 +160,6 @@ const contextJson = {
|
|
|
179
160
|
filesChanged: filesChanged.slice(0, 20),
|
|
180
161
|
decisions: decisions.slice(-10),
|
|
181
162
|
pendingTodos: pendingTodos.slice(0, 20),
|
|
182
|
-
workflowTasks: workflowTasks.slice(0, 10),
|
|
183
163
|
efficiencySummary,
|
|
184
164
|
warnFlags: warnFlags || 'None',
|
|
185
165
|
};
|