@geraldmaron/construct 1.0.0 → 1.0.1
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 +10 -7
- 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 +1116 -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 +525 -38
- 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/features.mjs
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/features.mjs —
|
|
2
|
+
* lib/features.mjs — Feature flag resolution for Construct capabilities.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Reads ~/.cx/features.json and project-level .cx/features.json to determine
|
|
5
|
+
* which optional features are active. Provides isFlagEnabled() used throughout
|
|
6
|
+
* the codebase to gate experimental paths without modifying core logic.
|
|
5
7
|
*/
|
|
6
|
-
import { readFileSync, existsSync } from 'fs';
|
|
8
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
7
9
|
import { homedir } from 'os';
|
|
8
|
-
import { join
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const CATALOG_PATH = join(__dirname, 'mcp-catalog.json');
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { loadPluginRegistry } from './plugin-registry.mjs';
|
|
13
12
|
|
|
14
13
|
function getHomeDir(overrides = {}) {
|
|
15
14
|
return overrides.homeDir ?? homedir();
|
|
@@ -94,58 +93,189 @@ function readJSON(p) {
|
|
|
94
93
|
try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return null; }
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
|
|
98
|
-
const
|
|
96
|
+
function getHostSupport(mcp, host) {
|
|
97
|
+
const support = mcp?.hostSupport?.[host];
|
|
98
|
+
if (!support) return { mode: 'managed' };
|
|
99
|
+
if (typeof support === 'string') return { mode: support };
|
|
100
|
+
if (typeof support === 'object') return { mode: support.mode ?? 'managed', ...support };
|
|
101
|
+
return { mode: 'managed' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function escapeRegExp(value) {
|
|
105
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build the set of IDs to match against host configs for a given MCP entry.
|
|
110
|
+
* Includes the canonical id, any declared aliases, and the OpenCode mapping.
|
|
111
|
+
*/
|
|
112
|
+
function getMcpMatchIds(mcp) {
|
|
113
|
+
const ids = new Set([mcp.id]);
|
|
114
|
+
for (const alias of mcp.aliases ?? []) {
|
|
115
|
+
ids.add(alias);
|
|
116
|
+
}
|
|
117
|
+
return ids;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check whether any of the given IDs appear as keys in a config object.
|
|
122
|
+
*/
|
|
123
|
+
function configHasAny(config, ids) {
|
|
124
|
+
if (!config || typeof config !== 'object') return false;
|
|
125
|
+
for (const id of ids) {
|
|
126
|
+
if (id in config) return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resolve all project-level .mcp.json paths to check.
|
|
133
|
+
* Claude Code supports .mcp.json at project root and in .claude/ directories.
|
|
134
|
+
*/
|
|
135
|
+
function getProjectMcpPaths(overrides = {}) {
|
|
136
|
+
if (overrides.projectMcpPaths) return overrides.projectMcpPaths;
|
|
137
|
+
const cwd = overrides.cwd ?? process.cwd();
|
|
138
|
+
return [
|
|
139
|
+
join(cwd, '.mcp.json'),
|
|
140
|
+
join(cwd, '.claude', 'mcp.json'),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Load Claude.ai server-side managed MCP IDs.
|
|
146
|
+
* These are integrations connected through Claude.ai's web interface
|
|
147
|
+
* (e.g., Notion, Google Drive) that aren't discoverable from local config.
|
|
148
|
+
* Users declare them via ~/.construct/claude-ai-mcps.json: { "mcps": ["notion", "slack"] }
|
|
149
|
+
*/
|
|
150
|
+
function getClaudeAiManagedMcps(overrides = {}) {
|
|
151
|
+
const p = overrides.claudeAiMcpsPath ?? join(getHomeDir(overrides), '.construct', 'claude-ai-mcps.json');
|
|
152
|
+
if (!existsSync(p)) return [];
|
|
153
|
+
try {
|
|
154
|
+
const data = JSON.parse(readFileSync(p, 'utf8'));
|
|
155
|
+
return Array.isArray(data?.mcps) ? data.mcps : [];
|
|
156
|
+
} catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Resolve Claude marketplace plugin directories to scan for .mcp.json files.
|
|
163
|
+
*/
|
|
164
|
+
function getMarketplaceMcpPaths(overrides = {}) {
|
|
165
|
+
if (overrides.marketplaceMcpPaths) return overrides.marketplaceMcpPaths;
|
|
166
|
+
const home = getHomeDir(overrides);
|
|
167
|
+
const marketplaceRoot = join(home, '.claude', 'plugins', 'marketplaces');
|
|
168
|
+
if (!existsSync(marketplaceRoot)) return [];
|
|
99
169
|
|
|
100
|
-
|
|
170
|
+
const paths = [];
|
|
171
|
+
try {
|
|
172
|
+
for (const marketplace of readdirSync(marketplaceRoot)) {
|
|
173
|
+
const extDir = join(marketplaceRoot, marketplace, 'external_plugins');
|
|
174
|
+
if (!existsSync(extDir)) continue;
|
|
175
|
+
try {
|
|
176
|
+
for (const plugin of readdirSync(extDir)) {
|
|
177
|
+
const mcpFile = join(extDir, plugin, '.mcp.json');
|
|
178
|
+
if (existsSync(mcpFile)) paths.push(mcpFile);
|
|
179
|
+
}
|
|
180
|
+
} catch { /* skip unreadable dirs */ }
|
|
181
|
+
}
|
|
182
|
+
} catch { /* skip unreadable marketplace root */ }
|
|
183
|
+
return paths;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function getMcpPlatforms(mcp, overrides = {}) {
|
|
101
187
|
const platforms = [];
|
|
188
|
+
const matchIds = getMcpMatchIds(mcp);
|
|
189
|
+
|
|
190
|
+
// --- Claude.ai server-side managed MCPs ---
|
|
191
|
+
const cloudMcps = getClaudeAiManagedMcps(overrides);
|
|
192
|
+
if (cloudMcps.some((id) => matchIds.has(id))) {
|
|
193
|
+
platforms.push('Claude.ai');
|
|
194
|
+
}
|
|
102
195
|
|
|
196
|
+
// --- Claude Code: user settings (~/.claude/settings.json) ---
|
|
103
197
|
const claudeSettings = readClaudeSettings(overrides);
|
|
104
|
-
if (
|
|
198
|
+
if (getHostSupport(mcp, 'claude').mode === 'managed' && configHasAny(claudeSettings?.mcpServers, matchIds)) {
|
|
105
199
|
platforms.push('Claude Code');
|
|
106
200
|
}
|
|
107
201
|
|
|
202
|
+
// --- Claude Code: project-level .mcp.json ---
|
|
203
|
+
if (!platforms.includes('Claude Code')) {
|
|
204
|
+
for (const p of getProjectMcpPaths(overrides)) {
|
|
205
|
+
if (!existsSync(p)) continue;
|
|
206
|
+
const projectMcp = readJSON(p);
|
|
207
|
+
if (configHasAny(projectMcp?.mcpServers, matchIds) || configHasAny(projectMcp, matchIds)) {
|
|
208
|
+
platforms.push('Claude Code');
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// --- Claude marketplace plugins (~/.claude/plugins/marketplaces/*/external_plugins/*/.mcp.json) ---
|
|
215
|
+
if (!platforms.includes('Claude Code')) {
|
|
216
|
+
for (const p of getMarketplaceMcpPaths(overrides)) {
|
|
217
|
+
const pluginMcp = readJSON(p);
|
|
218
|
+
if (configHasAny(pluginMcp, matchIds)) {
|
|
219
|
+
platforms.push('Claude Code');
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- OpenCode ---
|
|
108
226
|
const ocConfig = readOpenCodeConfig(overrides);
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
227
|
+
const ocIds = new Set([...matchIds]);
|
|
228
|
+
if (getHostSupport(mcp, 'opencode').mode === 'managed' && configHasAny(ocConfig?.mcp, ocIds)) {
|
|
111
229
|
platforms.push('OpenCode');
|
|
112
230
|
}
|
|
113
231
|
|
|
232
|
+
// --- VS Code / Copilot ---
|
|
114
233
|
for (const p of getVSCodeSettingsPaths(overrides)) {
|
|
115
234
|
const settings = readJSON(p);
|
|
116
|
-
if (settings?.['github.copilot.mcpServers']
|
|
235
|
+
if (configHasAny(settings?.['github.copilot.mcpServers'], matchIds)) {
|
|
117
236
|
const label = p.includes('Insiders') ? 'VS Code Insiders' : 'VS Code';
|
|
118
237
|
if (!platforms.includes(label)) platforms.push(label);
|
|
119
238
|
}
|
|
120
239
|
}
|
|
121
240
|
|
|
241
|
+
// --- Cursor ---
|
|
122
242
|
const cursorMcpPath = overrides.cursorMcpPath ?? join(getHomeDir(overrides), '.cursor', 'mcp.json');
|
|
123
243
|
if (existsSync(cursorMcpPath)) {
|
|
124
244
|
const cursorConfig = readJSON(cursorMcpPath);
|
|
125
|
-
if (cursorConfig?.mcpServers
|
|
245
|
+
if (configHasAny(cursorConfig?.mcpServers, matchIds)) {
|
|
126
246
|
platforms.push('Cursor');
|
|
127
247
|
}
|
|
128
248
|
}
|
|
129
249
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
250
|
+
// --- Codex ---
|
|
251
|
+
const codexConfigPath = overrides.codexConfigPath ?? join(getHomeDir(overrides), '.codex', 'config.toml');
|
|
252
|
+
if (existsSync(codexConfigPath)) {
|
|
253
|
+
const codexConfig = readFileSync(codexConfigPath, 'utf8');
|
|
254
|
+
const codexSupport = getHostSupport(mcp, 'codex');
|
|
255
|
+
if (codexSupport.mode === 'plugin' && codexSupport.plugin) {
|
|
256
|
+
const pattern = new RegExp(`\\[plugins\\.${JSON.stringify(codexSupport.plugin)}\\]\\s*\\nenabled = true`, 'm');
|
|
257
|
+
if (pattern.test(codexConfig)) platforms.push(codexSupport.label ?? 'Codex Plugin');
|
|
258
|
+
} else if (codexSupport.mode === 'managed') {
|
|
259
|
+
const idAlts = [...matchIds].map(escapeRegExp).join('|');
|
|
260
|
+
const pattern = new RegExp(`^\\[mcp_servers\\.("?)(?:${idAlts})\\1\\]`, 'm');
|
|
261
|
+
if (pattern.test(codexConfig)) platforms.push('Codex');
|
|
262
|
+
}
|
|
139
263
|
}
|
|
264
|
+
|
|
265
|
+
return platforms;
|
|
140
266
|
}
|
|
141
267
|
|
|
142
268
|
/**
|
|
143
|
-
* Build features dynamically from
|
|
269
|
+
* Build features dynamically from the plugin registry.
|
|
144
270
|
* Each MCP reports configured state when present in host configs and unavailable when missing.
|
|
145
271
|
*/
|
|
146
272
|
function buildFeatures(overrides = {}) {
|
|
147
|
-
const
|
|
148
|
-
|
|
273
|
+
const registry = loadPluginRegistry({
|
|
274
|
+
cwd: overrides.cwd ?? process.cwd(),
|
|
275
|
+
homeDir: getHomeDir(overrides),
|
|
276
|
+
env: overrides.env ?? process.env,
|
|
277
|
+
});
|
|
278
|
+
return registry.mcps.map((mcp) => ({
|
|
149
279
|
id: mcp.id,
|
|
150
280
|
name: mcp.name,
|
|
151
281
|
category: mcp.category,
|
|
@@ -153,7 +283,7 @@ function buildFeatures(overrides = {}) {
|
|
|
153
283
|
degradedMessage: mcp.degradedMessage,
|
|
154
284
|
usedBy: mcp.usedBy ?? [],
|
|
155
285
|
async check() {
|
|
156
|
-
const platforms = getMcpPlatforms(mcp
|
|
286
|
+
const platforms = getMcpPlatforms(mcp, overrides);
|
|
157
287
|
if (platforms.length > 0) {
|
|
158
288
|
return { status: 'configured', message: `Configured in ${platforms.join(', ')}` };
|
|
159
289
|
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/gates-audit.mjs — Cross-surface enforcement audit.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that every policy gate is consistently expressed across the four
|
|
5
|
+
* places enforcement can live: CI workflow jobs, the local pre-push hook,
|
|
6
|
+
* the local pre-commit hook (when `core.hooksPath` is wired), and GitHub
|
|
7
|
+
* branch protection (required status checks). Identifies gaps — checks that
|
|
8
|
+
* exist in one place but not another, or that are required-to-merge but
|
|
9
|
+
* absent from CI, or that are CI-only with no local mirror.
|
|
10
|
+
*
|
|
11
|
+
* The mapping between CI job names and local commands lives in the
|
|
12
|
+
* GATE_DEFINITIONS table below. When a new CI job is added, the mapping
|
|
13
|
+
* must be updated to declare what its local equivalent should be (or that
|
|
14
|
+
* none is expected). This is the same single-source-of-truth pattern the
|
|
15
|
+
* project already uses for CLI commands in lib/cli-commands.mjs.
|
|
16
|
+
*
|
|
17
|
+
* Exit code (in `construct gates:audit`): 0 if no critical gaps; 1 if any
|
|
18
|
+
* critical gap is found. Non-critical advisories never fail the command.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
22
|
+
import { join, dirname } from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { execSync } from 'node:child_process';
|
|
25
|
+
|
|
26
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const DEFAULT_ROOT_DIR = join(MODULE_DIR, '..');
|
|
28
|
+
|
|
29
|
+
// Authoritative gate mapping. Each entry declares a CI job name and its
|
|
30
|
+
// expected local mirror. `critical: true` means this gate MUST be in CI and
|
|
31
|
+
// SHOULD be in branch-protection required checks; absence is a hard gap.
|
|
32
|
+
// `localMirror: 'ci-only'` flags gates that legitimately have no local
|
|
33
|
+
// equivalent (e.g., dockerized integration tests).
|
|
34
|
+
|
|
35
|
+
const GATE_DEFINITIONS = [
|
|
36
|
+
{ ciJob: 'test (ubuntu-latest / node 22)', prePushLabel: 'tests', critical: true },
|
|
37
|
+
{ ciJob: 'test (macos-latest / node 22)', prePushLabel: 'tests', critical: true },
|
|
38
|
+
{ ciJob: 'test (ubuntu-latest / node 20)', prePushLabel: 'tests', critical: true },
|
|
39
|
+
{ ciJob: 'test (macos-latest / node 20)', prePushLabel: 'tests', critical: true },
|
|
40
|
+
{ ciJob: 'retrieval evals', prePushLabel: 'evals', critical: true },
|
|
41
|
+
{ ciJob: 'dependency CVE audit', prePushLabel: 'audit', critical: true },
|
|
42
|
+
{ ciJob: 'secret scanning', prePushLabel: null, preCommitCheck: 'ECC secret scan', critical: true, note: 'pre-commit ECC scan covers a subset of gitleaks rules' },
|
|
43
|
+
{ ciJob: 'postgres + pgvector integration', prePushLabel: null, preCommitCheck: null, critical: false, note: 'CI-only: requires Docker Postgres + pgvector container; not practical locally', localMirror: 'ci-only' },
|
|
44
|
+
{ ciJob: 'docs drift check', prePushLabel: 'docs', critical: true },
|
|
45
|
+
{ ciJob: 'comment policy', prePushLabel: null, preCommitCheck: 'Construct comment-lint (staged)', critical: true, note: 'pre-commit Construct policy section calls `lint:comments --staged`' },
|
|
46
|
+
{ ciJob: 'template policy', prePushLabel: null, preCommitGhPrIntercept: true, critical: true, note: 'pre-push-gate intercepts `gh pr create` / `gh pr edit` and lints the body' },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
function parseCIJobs(rootDir) {
|
|
50
|
+
const path = join(rootDir, '.github', 'workflows', 'ci.yml');
|
|
51
|
+
if (!existsSync(path)) return [];
|
|
52
|
+
const yml = readFileSync(path, 'utf8');
|
|
53
|
+
return [...yml.matchAll(/^ name:\s*(.+?)\s*$/gm)]
|
|
54
|
+
.map((m) => m[1].trim())
|
|
55
|
+
.map((n) => n.replace(/\$\{\{\s*matrix\.(\w+)\s*\}\}/g, (_, k) => `<${k}>`));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parsePrePushJobs(rootDir) {
|
|
59
|
+
const path = join(rootDir, 'lib', 'hooks', 'pre-push-gate.mjs');
|
|
60
|
+
if (!existsSync(path)) return [];
|
|
61
|
+
const src = readFileSync(path, 'utf8');
|
|
62
|
+
return [...src.matchAll(/label:\s*['"]([^'"]+)['"]/g)].map((m) => m[1]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parsePreCommitChecks(rootDir) {
|
|
66
|
+
const path = join(rootDir, '.beads', 'hooks', 'pre-commit');
|
|
67
|
+
if (!existsSync(path)) return [];
|
|
68
|
+
const src = readFileSync(path, 'utf8');
|
|
69
|
+
const checks = [];
|
|
70
|
+
if (src.includes('scan_added_lines')) checks.push('ECC secret scan');
|
|
71
|
+
if (src.includes('construct lint:comments --staged')) checks.push('Construct comment-lint (staged)');
|
|
72
|
+
if (src.includes('construct docs:verify --staged')) checks.push('Construct docs:verify (staged)');
|
|
73
|
+
if (src.includes('BEADS INTEGRATION')) checks.push('BEADS dispatcher');
|
|
74
|
+
return checks;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getCoreHooksPath(rootDir) {
|
|
78
|
+
try {
|
|
79
|
+
return execSync('git config --get core.hooksPath', {
|
|
80
|
+
cwd: rootDir, stdio: 'pipe', encoding: 'utf8', timeout: 2000,
|
|
81
|
+
}).trim();
|
|
82
|
+
} catch {
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fetchBranchProtection(branch, { repo = 'geraldmaron/construct' } = {}) {
|
|
88
|
+
try {
|
|
89
|
+
const out = execSync(
|
|
90
|
+
`gh api repos/${repo}/branches/${branch}/protection/required_status_checks`,
|
|
91
|
+
{ stdio: 'pipe', encoding: 'utf8', timeout: 5000 },
|
|
92
|
+
);
|
|
93
|
+
const data = JSON.parse(out);
|
|
94
|
+
return { protected: true, requiredContexts: data.contexts || [], status: 'fetched' };
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const msg = err?.stderr?.toString() || err?.message || '';
|
|
97
|
+
if (/Branch not protected/i.test(msg)) {
|
|
98
|
+
return { protected: false, requiredContexts: [], status: 'unprotected' };
|
|
99
|
+
}
|
|
100
|
+
return { protected: false, requiredContexts: [], status: 'unfetchable' };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function ciHasJob(ciJobs, jobName) {
|
|
105
|
+
if (ciJobs.includes(jobName)) return true;
|
|
106
|
+
const stripped = jobName.replace(/\s*\([^)]*\)\s*/g, '').trim();
|
|
107
|
+
return ciJobs.some((j) => j.replace(/\s*\(.*?\)\s*/g, '').trim() === stripped);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function identifyGaps(state) {
|
|
111
|
+
const { ciJobs, prePushJobs, preCommitChecks, branchProtection, hooksPath, rootDir, inCI } = state;
|
|
112
|
+
const gaps = [];
|
|
113
|
+
const protectionVisible = branchProtection.status === 'fetched' || branchProtection.status === 'unprotected';
|
|
114
|
+
|
|
115
|
+
for (const def of GATE_DEFINITIONS) {
|
|
116
|
+
const inCi = ciHasJob(ciJobs, def.ciJob);
|
|
117
|
+
if (!inCi) {
|
|
118
|
+
gaps.push({ kind: 'missing-ci', gate: def.ciJob, critical: def.critical });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (def.critical && protectionVisible) {
|
|
123
|
+
const required = branchProtection.requiredContexts.includes(def.ciJob);
|
|
124
|
+
if (!required) {
|
|
125
|
+
gaps.push({ kind: 'not-required-to-merge', gate: def.ciJob, critical: true });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const hasPrePush = def.prePushLabel ? prePushJobs.includes(def.prePushLabel) : false;
|
|
130
|
+
const hasPreCommit = def.preCommitCheck ? preCommitChecks.includes(def.preCommitCheck) : false;
|
|
131
|
+
const hasIntercept = !!def.preCommitGhPrIntercept;
|
|
132
|
+
const hasLocal = hasPrePush || hasPreCommit || hasIntercept || def.localMirror === 'ci-only';
|
|
133
|
+
|
|
134
|
+
if (def.critical && !hasLocal) {
|
|
135
|
+
gaps.push({ kind: 'no-local-mirror', gate: def.ciJob, note: def.note, critical: true });
|
|
136
|
+
}
|
|
137
|
+
if (!def.critical && !hasLocal && !def.note) {
|
|
138
|
+
gaps.push({ kind: 'no-local-mirror-advisory', gate: def.ciJob, critical: false });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (protectionVisible) {
|
|
143
|
+
for (const ctx of branchProtection.requiredContexts) {
|
|
144
|
+
if (!ciHasJob(ciJobs, ctx)) {
|
|
145
|
+
gaps.push({ kind: 'required-but-not-in-ci', gate: ctx, critical: true });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!inCI && existsSync(join(rootDir, '.beads', 'hooks', 'pre-commit')) && hooksPath !== '.beads/hooks') {
|
|
151
|
+
gaps.push({
|
|
152
|
+
kind: 'hooks-unwired',
|
|
153
|
+
expected: '.beads/hooks',
|
|
154
|
+
actual: hooksPath || '(unset)',
|
|
155
|
+
critical: true,
|
|
156
|
+
note: 'pre-commit policy gates are inactive locally; fix with: git config core.hooksPath .beads/hooks',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return gaps;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function auditGates({ rootDir = DEFAULT_ROOT_DIR, repo, branch = 'main' } = {}) {
|
|
164
|
+
const inCI = process.env.CI === 'true' || process.env.CI === '1';
|
|
165
|
+
const ciJobs = parseCIJobs(rootDir);
|
|
166
|
+
const prePushJobs = parsePrePushJobs(rootDir);
|
|
167
|
+
const preCommitChecks = parsePreCommitChecks(rootDir);
|
|
168
|
+
const hooksPath = getCoreHooksPath(rootDir);
|
|
169
|
+
const branchProtection = fetchBranchProtection(branch, repo ? { repo } : {});
|
|
170
|
+
|
|
171
|
+
const state = { ciJobs, prePushJobs, preCommitChecks, branchProtection, hooksPath, rootDir, inCI };
|
|
172
|
+
const gaps = identifyGaps(state);
|
|
173
|
+
const criticalGaps = gaps.filter((g) => g.critical);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
rootDir,
|
|
177
|
+
ciJobs,
|
|
178
|
+
prePushJobs,
|
|
179
|
+
preCommitChecks,
|
|
180
|
+
hooksPath,
|
|
181
|
+
branchProtection,
|
|
182
|
+
gaps,
|
|
183
|
+
criticalGaps,
|
|
184
|
+
inCI,
|
|
185
|
+
ok: criticalGaps.length === 0,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function formatReport(report) {
|
|
190
|
+
const lines = [];
|
|
191
|
+
lines.push('Construct Gates Audit');
|
|
192
|
+
lines.push('═════════════════════');
|
|
193
|
+
lines.push('');
|
|
194
|
+
|
|
195
|
+
lines.push(`CI jobs (${report.ciJobs.length}):`);
|
|
196
|
+
for (const j of report.ciJobs) lines.push(` • ${j}`);
|
|
197
|
+
lines.push('');
|
|
198
|
+
|
|
199
|
+
lines.push(`Local pre-push gate jobs (${report.prePushJobs.length}):`);
|
|
200
|
+
for (const j of report.prePushJobs) lines.push(` • ${j}`);
|
|
201
|
+
lines.push('');
|
|
202
|
+
|
|
203
|
+
lines.push(`Local pre-commit checks (${report.preCommitChecks.length}):`);
|
|
204
|
+
for (const j of report.preCommitChecks) lines.push(` • ${j}`);
|
|
205
|
+
lines.push('');
|
|
206
|
+
|
|
207
|
+
lines.push(`core.hooksPath: ${report.hooksPath || '(unset)'}`);
|
|
208
|
+
lines.push('');
|
|
209
|
+
|
|
210
|
+
if (report.branchProtection.status === 'fetched') {
|
|
211
|
+
lines.push(`Branch protection (main): ${report.branchProtection.requiredContexts.length} required contexts`);
|
|
212
|
+
for (const c of report.branchProtection.requiredContexts) lines.push(` • ${c}`);
|
|
213
|
+
} else if (report.branchProtection.status === 'unprotected') {
|
|
214
|
+
lines.push('Branch protection (main): NOT PROTECTED — no gate enforces CI before merge');
|
|
215
|
+
} else {
|
|
216
|
+
lines.push('Branch protection (main): unfetchable (insufficient gh permissions) — protection-dependent gaps skipped');
|
|
217
|
+
}
|
|
218
|
+
lines.push('');
|
|
219
|
+
|
|
220
|
+
if (report.gaps.length === 0) {
|
|
221
|
+
lines.push('Gaps: none');
|
|
222
|
+
} else {
|
|
223
|
+
lines.push(`Gaps (${report.gaps.length}; ${report.criticalGaps.length} critical):`);
|
|
224
|
+
for (const g of report.gaps) {
|
|
225
|
+
const marker = g.critical ? '✗' : '⚠';
|
|
226
|
+
const detail = g.note ? ` — ${g.note}` : '';
|
|
227
|
+
const extra = g.kind === 'hooks-unwired' ? ` (expected ${g.expected}, actual ${g.actual})` : '';
|
|
228
|
+
lines.push(` ${marker} ${g.kind}: ${g.gate || ''}${extra}${detail}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
lines.push('');
|
|
232
|
+
|
|
233
|
+
lines.push(`Summary: ${report.ok ? 'OK ✓' : `${report.criticalGaps.length} critical gap(s) ✗`}`);
|
|
234
|
+
|
|
235
|
+
return lines.join('\n') + '\n';
|
|
236
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/git-hooks/prepare-commit-msg
|
|
4
|
+
*
|
|
5
|
+
* Git hook that appends AI provenance trailers to every commit message.
|
|
6
|
+
* Follows the RFC 9562 / git-interpret-trailers convention.
|
|
7
|
+
*
|
|
8
|
+
* Trailers added (when env vars are present):
|
|
9
|
+
* AI-Generator: construct/sync-agents
|
|
10
|
+
* AI-Model: claude-sonnet-4-6
|
|
11
|
+
* AI-Session: 019dbb90-5885-...
|
|
12
|
+
*
|
|
13
|
+
* Install for a repo:
|
|
14
|
+
* construct doc install-hooks
|
|
15
|
+
*
|
|
16
|
+
* Or manually:
|
|
17
|
+
* cp node_modules/.bin/construct-prepare-commit-msg .git/hooks/prepare-commit-msg
|
|
18
|
+
* chmod +x .git/hooks/prepare-commit-msg
|
|
19
|
+
*
|
|
20
|
+
* The hook reads env vars set by Construct sessions:
|
|
21
|
+
* CONSTRUCT_MODEL — active model name
|
|
22
|
+
* CONSTRUCT_SESSION_ID — UUIDv7 session id
|
|
23
|
+
* CONSTRUCT_GENERATOR — generator label (optional)
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
|
|
28
|
+
const msgFile = process.argv[2];
|
|
29
|
+
const source = process.argv[3]; // 'message', 'template', 'merge', 'squash', 'commit'
|
|
30
|
+
|
|
31
|
+
// Only annotate regular commits and explicit message commits, not merges/squashes
|
|
32
|
+
if (source === 'merge' || source === 'squash') process.exit(0);
|
|
33
|
+
if (!msgFile) process.exit(0);
|
|
34
|
+
|
|
35
|
+
const model = process.env.CONSTRUCT_MODEL;
|
|
36
|
+
const sessionId = process.env.CONSTRUCT_SESSION_ID;
|
|
37
|
+
const generator = process.env.CONSTRUCT_GENERATOR;
|
|
38
|
+
|
|
39
|
+
// Nothing to add if no Construct env vars are set
|
|
40
|
+
if (!model && !sessionId && !generator) process.exit(0);
|
|
41
|
+
|
|
42
|
+
const original = fs.readFileSync(msgFile, 'utf8');
|
|
43
|
+
|
|
44
|
+
// Build trailers — only include fields that are populated
|
|
45
|
+
const trailers = [];
|
|
46
|
+
if (generator) trailers.push(`AI-Generator: ${generator}`);
|
|
47
|
+
if (model) trailers.push(`AI-Model: ${model}`);
|
|
48
|
+
if (sessionId) trailers.push(`AI-Session: ${sessionId}`);
|
|
49
|
+
|
|
50
|
+
if (!trailers.length) process.exit(0);
|
|
51
|
+
|
|
52
|
+
// git-interpret-trailers convention: trailers follow a blank line after the body.
|
|
53
|
+
// We append only if the trailers aren't already present (idempotent).
|
|
54
|
+
const alreadyPresent = trailers.every(t => original.includes(t.split(':')[0] + ':'));
|
|
55
|
+
if (alreadyPresent) process.exit(0);
|
|
56
|
+
|
|
57
|
+
const separator = original.trimEnd().endsWith('\n\n') ? '' : '\n\n';
|
|
58
|
+
fs.writeFileSync(msgFile, original.trimEnd() + separator + trailers.join('\n') + '\n');
|