@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
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/plugin-registry.mjs — load and validate Construct plugin manifests.
|
|
3
|
+
*
|
|
4
|
+
* Built-in integrations and external manifests share one registry so new
|
|
5
|
+
* integrations can plug into Construct without editing core source files.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
export const MANIFEST_VERSION = 1;
|
|
14
|
+
|
|
15
|
+
function readJson(filePath) {
|
|
16
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isPlainObject(value) {
|
|
20
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeMcp(mcp, plugin) {
|
|
24
|
+
return {
|
|
25
|
+
...mcp,
|
|
26
|
+
pluginId: plugin.id,
|
|
27
|
+
pluginName: plugin.name,
|
|
28
|
+
aliases: Array.isArray(mcp.aliases) ? mcp.aliases : [],
|
|
29
|
+
args: Array.isArray(mcp.args) ? mcp.args : [],
|
|
30
|
+
env: isPlainObject(mcp.env) ? mcp.env : {},
|
|
31
|
+
headers: isPlainObject(mcp.headers) ? mcp.headers : {},
|
|
32
|
+
requiredEnv: Array.isArray(mcp.requiredEnv) ? mcp.requiredEnv : [],
|
|
33
|
+
setupModes: Array.isArray(mcp.setupModes) ? mcp.setupModes : ["manual"],
|
|
34
|
+
usedBy: Array.isArray(mcp.usedBy) ? mcp.usedBy : [],
|
|
35
|
+
hostSupport: isPlainObject(mcp.hostSupport) ? mcp.hostSupport : {},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validateMcp(mcp, { pluginId, manifestPath }) {
|
|
40
|
+
const errors = [];
|
|
41
|
+
const label = `${pluginId}:${mcp?.id ?? "(missing-id)"}`;
|
|
42
|
+
if (!isPlainObject(mcp)) {
|
|
43
|
+
return [`${manifestPath}: MCP entry must be an object (${label})`];
|
|
44
|
+
}
|
|
45
|
+
if (!mcp.id || typeof mcp.id !== "string") errors.push(`${manifestPath}: MCP entry missing string id (${label})`);
|
|
46
|
+
if (!mcp.name || typeof mcp.name !== "string") errors.push(`${manifestPath}: MCP entry missing string name (${label})`);
|
|
47
|
+
if (!mcp.category || typeof mcp.category !== "string") errors.push(`${manifestPath}: MCP entry missing string category (${label})`);
|
|
48
|
+
if (!mcp.description || typeof mcp.description !== "string") errors.push(`${manifestPath}: MCP entry missing string description (${label})`);
|
|
49
|
+
if (!Array.isArray(mcp.requiredEnv)) errors.push(`${manifestPath}: MCP ${label} missing requiredEnv array`);
|
|
50
|
+
if (!Array.isArray(mcp.usedBy)) errors.push(`${manifestPath}: MCP ${label} missing usedBy array`);
|
|
51
|
+
if (!isPlainObject(mcp.env)) errors.push(`${manifestPath}: MCP ${label} missing env object`);
|
|
52
|
+
if (mcp.headers !== undefined && !isPlainObject(mcp.headers)) errors.push(`${manifestPath}: MCP ${label} headers must be an object when present`);
|
|
53
|
+
if (mcp.type === "url" && typeof mcp.url !== "string") errors.push(`${manifestPath}: MCP ${label} with type=url must declare url`);
|
|
54
|
+
if (mcp.type !== "url" && typeof mcp.command !== "string") errors.push(`${manifestPath}: MCP ${label} must declare command unless type=url`);
|
|
55
|
+
return errors;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function validatePluginManifest(manifest, { manifestPath = "<memory>" } = {}) {
|
|
59
|
+
const errors = [];
|
|
60
|
+
if (!isPlainObject(manifest)) {
|
|
61
|
+
return { valid: false, errors: [`${manifestPath}: manifest must be a JSON object`] };
|
|
62
|
+
}
|
|
63
|
+
if (manifest.version !== MANIFEST_VERSION) {
|
|
64
|
+
errors.push(`${manifestPath}: unsupported manifest version ${JSON.stringify(manifest.version)} (expected ${MANIFEST_VERSION})`);
|
|
65
|
+
}
|
|
66
|
+
if (!Array.isArray(manifest.plugins) || manifest.plugins.length === 0) {
|
|
67
|
+
errors.push(`${manifestPath}: manifest must contain a non-empty plugins array`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const pluginIds = new Set();
|
|
71
|
+
for (const plugin of manifest.plugins ?? []) {
|
|
72
|
+
const pluginLabel = plugin?.id ?? "(missing-id)";
|
|
73
|
+
if (!isPlainObject(plugin)) {
|
|
74
|
+
errors.push(`${manifestPath}: plugin entry must be an object (${pluginLabel})`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!plugin.id || typeof plugin.id !== "string") errors.push(`${manifestPath}: plugin missing string id`);
|
|
78
|
+
if (!plugin.name || typeof plugin.name !== "string") errors.push(`${manifestPath}: plugin ${pluginLabel} missing string name`);
|
|
79
|
+
if (!plugin.version || typeof plugin.version !== "string") errors.push(`${manifestPath}: plugin ${pluginLabel} missing string version`);
|
|
80
|
+
if (!plugin.description || typeof plugin.description !== "string") errors.push(`${manifestPath}: plugin ${pluginLabel} missing string description`);
|
|
81
|
+
if (plugin.capabilities !== undefined && !Array.isArray(plugin.capabilities)) {
|
|
82
|
+
errors.push(`${manifestPath}: plugin ${pluginLabel} capabilities must be an array when present`);
|
|
83
|
+
}
|
|
84
|
+
if (!Array.isArray(plugin.mcps)) errors.push(`${manifestPath}: plugin ${pluginLabel} missing mcps array`);
|
|
85
|
+
if (plugin.id) {
|
|
86
|
+
if (pluginIds.has(plugin.id)) errors.push(`${manifestPath}: duplicate plugin id ${plugin.id}`);
|
|
87
|
+
pluginIds.add(plugin.id);
|
|
88
|
+
}
|
|
89
|
+
for (const mcp of plugin.mcps ?? []) {
|
|
90
|
+
errors.push(...validateMcp(mcp, { pluginId: pluginLabel, manifestPath }));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { valid: errors.length === 0, errors };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function loadBuiltinPlugin(rootDir) {
|
|
98
|
+
const rootCatalogPath = join(rootDir, "lib", "mcp-catalog.json");
|
|
99
|
+
const catalogPath = existsSync(rootCatalogPath) ? rootCatalogPath : join(__dirname, "mcp-catalog.json");
|
|
100
|
+
const catalog = readJson(catalogPath);
|
|
101
|
+
const plugin = {
|
|
102
|
+
id: "construct-builtins",
|
|
103
|
+
name: "Construct Built-ins",
|
|
104
|
+
version: "1.0.0",
|
|
105
|
+
description: "Built-in Construct integrations bundled with the CLI.",
|
|
106
|
+
capabilities: ["mcp"],
|
|
107
|
+
manifestPath: catalogPath,
|
|
108
|
+
builtIn: true,
|
|
109
|
+
};
|
|
110
|
+
plugin.mcps = (catalog.mcps ?? []).map((mcp) => ({
|
|
111
|
+
...normalizeMcp(mcp, plugin),
|
|
112
|
+
manifestPath: catalogPath,
|
|
113
|
+
}));
|
|
114
|
+
return plugin;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function resolvePluginDirs({ cwd = process.cwd(), homeDir = homedir(), env = process.env } = {}) {
|
|
118
|
+
const extra = String(env.CONSTRUCT_PLUGIN_DIRS || "")
|
|
119
|
+
.split(env.PATH?.includes(";") ? ";" : ":")
|
|
120
|
+
.map((entry) => entry.trim())
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
|
|
123
|
+
return [...new Set([
|
|
124
|
+
join(cwd, ".cx", "plugins"),
|
|
125
|
+
join(cwd, ".construct", "plugins"),
|
|
126
|
+
join(homeDir, ".construct", "plugins"),
|
|
127
|
+
...extra,
|
|
128
|
+
].map((dir) => resolve(dir)))];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function findManifestFiles(pluginDirs) {
|
|
132
|
+
const files = [];
|
|
133
|
+
for (const dir of pluginDirs) {
|
|
134
|
+
if (!existsSync(dir)) continue;
|
|
135
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
136
|
+
if (!entry.isFile()) continue;
|
|
137
|
+
if (extname(entry.name).toLowerCase() !== ".json") continue;
|
|
138
|
+
files.push(join(dir, entry.name));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return files.sort();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function loadPluginRegistry({ cwd = process.cwd(), homeDir = homedir(), rootDir = resolve(__dirname, ".."), env = process.env } = {}) {
|
|
145
|
+
const errors = [];
|
|
146
|
+
const plugins = [loadBuiltinPlugin(rootDir)];
|
|
147
|
+
const pluginDirs = resolvePluginDirs({ cwd, homeDir, env });
|
|
148
|
+
|
|
149
|
+
for (const manifestPath of findManifestFiles(pluginDirs)) {
|
|
150
|
+
try {
|
|
151
|
+
const manifest = readJson(manifestPath);
|
|
152
|
+
const validation = validatePluginManifest(manifest, { manifestPath });
|
|
153
|
+
if (!validation.valid) {
|
|
154
|
+
errors.push(...validation.errors);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
for (const rawPlugin of manifest.plugins) {
|
|
158
|
+
const plugin = {
|
|
159
|
+
...rawPlugin,
|
|
160
|
+
capabilities: Array.isArray(rawPlugin.capabilities) ? rawPlugin.capabilities : ["mcp"],
|
|
161
|
+
manifestPath,
|
|
162
|
+
builtIn: false,
|
|
163
|
+
};
|
|
164
|
+
plugin.mcps = (rawPlugin.mcps ?? []).map((mcp) => ({
|
|
165
|
+
...normalizeMcp(mcp, plugin),
|
|
166
|
+
manifestPath,
|
|
167
|
+
}));
|
|
168
|
+
plugins.push(plugin);
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
errors.push(`${manifestPath}: failed to load manifest (${error.message})`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const pluginIds = new Set();
|
|
176
|
+
const mcpIds = new Set();
|
|
177
|
+
const dedupedPlugins = [];
|
|
178
|
+
const mcps = [];
|
|
179
|
+
|
|
180
|
+
for (const plugin of plugins) {
|
|
181
|
+
if (pluginIds.has(plugin.id)) {
|
|
182
|
+
errors.push(`${plugin.manifestPath}: duplicate plugin id ${plugin.id}`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
pluginIds.add(plugin.id);
|
|
186
|
+
dedupedPlugins.push(plugin);
|
|
187
|
+
for (const mcp of plugin.mcps ?? []) {
|
|
188
|
+
if (mcpIds.has(mcp.id)) {
|
|
189
|
+
errors.push(`${mcp.manifestPath}: duplicate MCP id ${mcp.id}`);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
mcpIds.add(mcp.id);
|
|
193
|
+
mcps.push(mcp);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
valid: errors.length === 0,
|
|
199
|
+
errors,
|
|
200
|
+
pluginDirs,
|
|
201
|
+
plugins: dedupedPlugins,
|
|
202
|
+
mcps,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function getPluginById(id, options = {}) {
|
|
207
|
+
const registry = loadPluginRegistry(options);
|
|
208
|
+
return registry.plugins.find((plugin) => plugin.id === id) ?? null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function getMcpById(id, options = {}) {
|
|
212
|
+
const registry = loadPluginRegistry(options);
|
|
213
|
+
return registry.mcps.find((mcp) => mcp.id === id) ?? null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function initPluginManifest(id, {
|
|
217
|
+
cwd = process.cwd(),
|
|
218
|
+
force = false,
|
|
219
|
+
version = "0.1.0",
|
|
220
|
+
name = null,
|
|
221
|
+
description = "External Construct plugin.",
|
|
222
|
+
} = {}) {
|
|
223
|
+
if (!id || typeof id !== "string" || !id.trim()) {
|
|
224
|
+
throw new Error("plugin id is required");
|
|
225
|
+
}
|
|
226
|
+
const safeId = id.trim();
|
|
227
|
+
const dir = join(cwd, ".cx", "plugins");
|
|
228
|
+
const filePath = join(dir, `${safeId}.json`);
|
|
229
|
+
if (existsSync(filePath) && !force) {
|
|
230
|
+
throw new Error(`plugin manifest already exists at ${filePath}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
mkdirSync(dir, { recursive: true });
|
|
234
|
+
const manifest = {
|
|
235
|
+
version: MANIFEST_VERSION,
|
|
236
|
+
plugins: [
|
|
237
|
+
{
|
|
238
|
+
id: safeId,
|
|
239
|
+
name: name ?? safeId,
|
|
240
|
+
version,
|
|
241
|
+
description,
|
|
242
|
+
capabilities: ["mcp"],
|
|
243
|
+
mcps: [
|
|
244
|
+
{
|
|
245
|
+
id: `${safeId}-example`,
|
|
246
|
+
name: `${name ?? safeId} Example`,
|
|
247
|
+
category: "integration",
|
|
248
|
+
description: "Example MCP entry. Replace with your real integration.",
|
|
249
|
+
command: "npx",
|
|
250
|
+
args: ["-y", "@example/mcp-server"],
|
|
251
|
+
env: {},
|
|
252
|
+
requiredEnv: [],
|
|
253
|
+
setupModes: ["manual"],
|
|
254
|
+
hostSupport: {
|
|
255
|
+
claude: { mode: "managed" },
|
|
256
|
+
opencode: { mode: "managed" },
|
|
257
|
+
codex: { mode: "managed" },
|
|
258
|
+
},
|
|
259
|
+
usedBy: ["construct"],
|
|
260
|
+
degradedMessage: "Example integration unavailable.",
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
};
|
|
266
|
+
writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
267
|
+
return filePath;
|
|
268
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/policy/engine.mjs — role-based policy decisions for tool / action access.
|
|
3
|
+
*
|
|
4
|
+
* Reads role permissions from `agents/role-manifests.json` and decides
|
|
5
|
+
* whether a given (role, project, tool, action, risk) tuple is allowed,
|
|
6
|
+
* needs approval, or is denied outright. Powers the MCP broker for team
|
|
7
|
+
* and enterprise deployments; solo mode leaves the broker off so this
|
|
8
|
+
* engine returns allow-without-approval everywhere.
|
|
9
|
+
*
|
|
10
|
+
* Decision precedence:
|
|
11
|
+
* 1. Explicit deny in the role's fence → denied + typed reason.
|
|
12
|
+
* 2. Action falls in the role's approvalRequired list → allowed but
|
|
13
|
+
* approvalRequired = true (UI must collect human consent).
|
|
14
|
+
* 3. risk === 'high' and role is not in HIGH_RISK_AUTONOMOUS → approval required.
|
|
15
|
+
* 4. Otherwise → allowed.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const DEFAULT_MANIFEST_PATH = path.join(MODULE_DIR, '..', '..', 'agents', 'role-manifests.json');
|
|
24
|
+
|
|
25
|
+
const HIGH_RISK_AUTONOMOUS = new Set(['security', 'sre', 'release-manager']);
|
|
26
|
+
|
|
27
|
+
let cachedManifests = null;
|
|
28
|
+
|
|
29
|
+
export function loadRoleManifests(manifestPath = DEFAULT_MANIFEST_PATH) {
|
|
30
|
+
if (cachedManifests && cachedManifests.path === manifestPath) return cachedManifests.data;
|
|
31
|
+
if (!existsSync(manifestPath)) {
|
|
32
|
+
cachedManifests = { path: manifestPath, data: { personas: {} } };
|
|
33
|
+
return cachedManifests.data;
|
|
34
|
+
}
|
|
35
|
+
const raw = readFileSync(manifestPath, 'utf8');
|
|
36
|
+
const data = JSON.parse(raw);
|
|
37
|
+
cachedManifests = { path: manifestPath, data };
|
|
38
|
+
return data;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function clearManifestCache() {
|
|
42
|
+
cachedManifests = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function manifestFor(role, manifests) {
|
|
46
|
+
return manifests?.personas?.[role] || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function actionMatches(pattern, action) {
|
|
50
|
+
if (!pattern || !action) return false;
|
|
51
|
+
if (pattern === action) return true;
|
|
52
|
+
if (pattern.endsWith(':**')) return action.startsWith(pattern.slice(0, -3));
|
|
53
|
+
if (pattern.endsWith('/**')) return action.startsWith(pattern.slice(0, -3));
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isExplicitlyDenied(action, manifest) {
|
|
58
|
+
const deny = manifest?.fence?.deniedActions || [];
|
|
59
|
+
return deny.some((p) => actionMatches(p, action));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function needsApprovalFromManifest(action, manifest) {
|
|
63
|
+
const list = manifest?.fence?.approvalRequired || [];
|
|
64
|
+
return list.some((p) => actionMatches(p, action));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Decide whether a tool / action is allowed for a role.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} input
|
|
71
|
+
* @param {string} input.role
|
|
72
|
+
* @param {string} [input.project]
|
|
73
|
+
* @param {string} input.tool
|
|
74
|
+
* @param {string} input.action
|
|
75
|
+
* @param {string} [input.risk]
|
|
76
|
+
* @param {object} [opts]
|
|
77
|
+
* @returns {{allowed: boolean, reason: string, approvalRequired: boolean, source: string}}
|
|
78
|
+
*/
|
|
79
|
+
export function policyDecision(input = {}, opts = {}) {
|
|
80
|
+
const { role, tool, action, risk = 'low' } = input;
|
|
81
|
+
if (!role) return { allowed: false, reason: 'role is required', approvalRequired: false, source: 'engine' };
|
|
82
|
+
if (!tool) return { allowed: false, reason: 'tool is required', approvalRequired: false, source: 'engine' };
|
|
83
|
+
if (!action) return { allowed: false, reason: 'action is required', approvalRequired: false, source: 'engine' };
|
|
84
|
+
|
|
85
|
+
const manifests = opts.manifests || loadRoleManifests(opts.manifestPath);
|
|
86
|
+
const manifest = manifestFor(role, manifests);
|
|
87
|
+
|
|
88
|
+
if (!manifest) {
|
|
89
|
+
return {
|
|
90
|
+
allowed: false,
|
|
91
|
+
reason: `no role manifest for "${role}"; explicit allowlist required for tool access in team / enterprise mode`,
|
|
92
|
+
approvalRequired: false,
|
|
93
|
+
source: 'engine',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isExplicitlyDenied(action, manifest)) {
|
|
98
|
+
return {
|
|
99
|
+
allowed: false,
|
|
100
|
+
reason: `action "${action}" is in deny list for role "${role}"`,
|
|
101
|
+
approvalRequired: false,
|
|
102
|
+
source: 'manifest.deniedActions',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (needsApprovalFromManifest(action, manifest)) {
|
|
107
|
+
return {
|
|
108
|
+
allowed: true,
|
|
109
|
+
reason: `action "${action}" requires approval per role "${role}" manifest`,
|
|
110
|
+
approvalRequired: true,
|
|
111
|
+
source: 'manifest.approvalRequired',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (risk === 'high' && !HIGH_RISK_AUTONOMOUS.has(role)) {
|
|
116
|
+
return {
|
|
117
|
+
allowed: true,
|
|
118
|
+
reason: `high-risk action "${action}" needs approval for non-autonomous role "${role}"`,
|
|
119
|
+
approvalRequired: true,
|
|
120
|
+
source: 'risk-tier',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
allowed: true,
|
|
126
|
+
reason: `action "${action}" permitted for role "${role}"`,
|
|
127
|
+
approvalRequired: false,
|
|
128
|
+
source: 'default',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// lib/policy/unified-gates.mjs
|
|
2
|
+
// Single policy declaration for all enforcement layers
|
|
3
|
+
|
|
4
|
+
export const POLICIES = {
|
|
5
|
+
commentStyle: {
|
|
6
|
+
id: 'comment-style',
|
|
7
|
+
description: 'Comments must follow project standards',
|
|
8
|
+
layers: ['write', 'commit', 'ci'],
|
|
9
|
+
bypass: 'CONSTRUCT_SKIP_COMMENT_STYLE',
|
|
10
|
+
critical: false,
|
|
11
|
+
},
|
|
12
|
+
docCoupling: {
|
|
13
|
+
id: 'doc-coupling',
|
|
14
|
+
description: 'Code changes require documentation updates',
|
|
15
|
+
layers: ['commit', 'push'],
|
|
16
|
+
bypass: 'CONSTRUCT_SKIP_DOCS',
|
|
17
|
+
critical: false,
|
|
18
|
+
},
|
|
19
|
+
secretScan: {
|
|
20
|
+
id: 'secret-scan',
|
|
21
|
+
description: 'No secrets in committed code',
|
|
22
|
+
layers: ['commit', 'push', 'ci'],
|
|
23
|
+
bypass: 'CONSTRUCT_SKIP_SECRET_SCAN',
|
|
24
|
+
critical: true,
|
|
25
|
+
},
|
|
26
|
+
ciStatus: {
|
|
27
|
+
id: 'ci-status',
|
|
28
|
+
description: 'CI must be green before push',
|
|
29
|
+
layers: ['push', 'session-end'],
|
|
30
|
+
bypass: 'CONSTRUCT_SKIP_CI_CHECK',
|
|
31
|
+
critical: true,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export class UnifiedGateEngine {
|
|
36
|
+
constructor(options = {}) {
|
|
37
|
+
this.env = options.env || process.env;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
isBypassed(policyId) {
|
|
41
|
+
const policy = POLICIES[policyId];
|
|
42
|
+
if (!policy) return { bypassed: false };
|
|
43
|
+
|
|
44
|
+
if (policy.bypass && this.env[policy.bypass] === '1') {
|
|
45
|
+
return { bypassed: true, envVar: policy.bypass };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (this.env.CONSTRUCT_SKIP_ALL_GATES === '1') {
|
|
49
|
+
return { bypassed: true, envVar: 'CONSTRUCT_SKIP_ALL_GATES' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { bypassed: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async evaluateLayer(layer, context = {}) {
|
|
56
|
+
const results = [];
|
|
57
|
+
|
|
58
|
+
for (const [id, policy] of Object.entries(POLICIES)) {
|
|
59
|
+
if (policy.layers.includes(layer)) {
|
|
60
|
+
const bypass = this.isBypassed(id);
|
|
61
|
+
results.push({
|
|
62
|
+
policy: id,
|
|
63
|
+
passed: bypass.bypassed || true,
|
|
64
|
+
bypassed: bypass.bypassed,
|
|
65
|
+
bypassEnvVar: bypass.envVar,
|
|
66
|
+
critical: policy.critical,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const failed = results.filter(r => !r.passed && r.critical);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
layer,
|
|
75
|
+
passed: failed.length === 0,
|
|
76
|
+
canProceed: failed.length === 0,
|
|
77
|
+
results,
|
|
78
|
+
summary: `${results.filter(r => r.passed).length}/${results.length} passed`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function checkGates(layer, context = {}, options = {}) {
|
|
84
|
+
const engine = new UnifiedGateEngine(options);
|
|
85
|
+
return await engine.evaluateLayer(layer, context);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function listPolicies() {
|
|
89
|
+
return Object.entries(POLICIES).map(([id, policy]) => ({
|
|
90
|
+
id,
|
|
91
|
+
description: policy.description,
|
|
92
|
+
layers: policy.layers,
|
|
93
|
+
bypass: policy.bypass,
|
|
94
|
+
critical: policy.critical,
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/project-detection.mjs — Shared utilities for detecting project initialization state.
|
|
4
|
+
*
|
|
5
|
+
* Backs hooks that decide whether to create .cx directories or perform other
|
|
6
|
+
* project-specific operations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if a directory looks like an initialized project.
|
|
14
|
+
*
|
|
15
|
+
* Criteria (any of the following):
|
|
16
|
+
* 1. Has package.json (npm project)
|
|
17
|
+
* 2. Has .git directory (git repository)
|
|
18
|
+
* 3. Has .claude/settings.json or .codex/settings.json (Claude/OpenCode configured)
|
|
19
|
+
* 4. Has AGENTS.md or plan.md (Construct project)
|
|
20
|
+
* 5. Has .cx directory already (already has Construct context)
|
|
21
|
+
*
|
|
22
|
+
* @param {string} cwd Directory path to check
|
|
23
|
+
* @returns {boolean} True if this appears to be an initialized project
|
|
24
|
+
*/
|
|
25
|
+
export function isProjectInitialized(cwd) {
|
|
26
|
+
// Check for package.json (npm project)
|
|
27
|
+
const hasPackageJson = existsSync(join(cwd, 'package.json'));
|
|
28
|
+
|
|
29
|
+
// Check for .git (git repository)
|
|
30
|
+
const hasGit = existsSync(join(cwd, '.git'));
|
|
31
|
+
|
|
32
|
+
// Check for Claude/OpenCode configuration
|
|
33
|
+
const hasCliSurface = existsSync(join(cwd, '.claude', 'settings.json')) ||
|
|
34
|
+
existsSync(join(cwd, '.codex', 'settings.json'));
|
|
35
|
+
|
|
36
|
+
// Check for Construct project markers
|
|
37
|
+
const hasConstructConfig = existsSync(join(cwd, 'AGENTS.md')) ||
|
|
38
|
+
existsSync(join(cwd, 'plan.md'));
|
|
39
|
+
|
|
40
|
+
// Check if .cx already exists (already has Construct context)
|
|
41
|
+
const hasCxDir = existsSync(join(cwd, '.cx'));
|
|
42
|
+
|
|
43
|
+
// Also check for common project files
|
|
44
|
+
const hasProjectFile = existsSync(join(cwd, 'Cargo.toml')) || // Rust
|
|
45
|
+
existsSync(join(cwd, 'pyproject.toml')) || // Python
|
|
46
|
+
existsSync(join(cwd, 'go.mod')) || // Go
|
|
47
|
+
existsSync(join(cwd, 'pom.xml')) || // Maven
|
|
48
|
+
existsSync(join(cwd, 'build.gradle')) || // Gradle
|
|
49
|
+
existsSync(join(cwd, 'composer.json')) || // PHP
|
|
50
|
+
existsSync(join(cwd, 'Gemfile')) || // Ruby
|
|
51
|
+
existsSync(join(cwd, 'requirements.txt')); // Python
|
|
52
|
+
|
|
53
|
+
return hasPackageJson || hasGit || hasCliSurface || hasConstructConfig || hasCxDir || hasProjectFile;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a directory should have .cx directories created.
|
|
58
|
+
* More restrictive than isProjectInitialized - only creates .cx for:
|
|
59
|
+
* 1. Already has .cx directory
|
|
60
|
+
* 2. Has Construct config (AGENTS.md, plan.md)
|
|
61
|
+
* 3. Has Claude/OpenCode configuration
|
|
62
|
+
* 4. Has package.json AND .git (established npm project)
|
|
63
|
+
*
|
|
64
|
+
* @param {string} cwd Directory path to check
|
|
65
|
+
* @returns {boolean} True if .cx should be created
|
|
66
|
+
*/
|
|
67
|
+
export function shouldCreateCx(cwd) {
|
|
68
|
+
// Already has .cx directory
|
|
69
|
+
if (existsSync(join(cwd, '.cx'))) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Has Construct project markers
|
|
74
|
+
const hasConstructConfig = existsSync(join(cwd, 'AGENTS.md')) ||
|
|
75
|
+
existsSync(join(cwd, 'plan.md'));
|
|
76
|
+
if (hasConstructConfig) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Has Claude/OpenCode configuration
|
|
81
|
+
const hasCliSurface = existsSync(join(cwd, '.claude', 'settings.json')) ||
|
|
82
|
+
existsSync(join(cwd, '.codex', 'settings.json'));
|
|
83
|
+
if (hasCliSurface) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Has both package.json and .git (established npm project)
|
|
88
|
+
const hasPackageJson = existsSync(join(cwd, 'package.json'));
|
|
89
|
+
const hasGit = existsSync(join(cwd, '.git'));
|
|
90
|
+
if (hasPackageJson && hasGit) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Default: don't create .cx in uninitialized directories
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if this is likely a new project setup phase.
|
|
100
|
+
* Returns true if directory looks empty/uninitialized.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} cwd Directory path to check
|
|
103
|
+
* @returns {boolean} True if this appears to be a new project setup
|
|
104
|
+
*/
|
|
105
|
+
export function isNewProjectSetup(cwd) {
|
|
106
|
+
// Has no project markers
|
|
107
|
+
const hasPackageJson = existsSync(join(cwd, 'package.json'));
|
|
108
|
+
const hasGit = existsSync(join(cwd, '.git'));
|
|
109
|
+
const hasConstructConfig = existsSync(join(cwd, 'AGENTS.md')) ||
|
|
110
|
+
existsSync(join(cwd, 'plan.md'));
|
|
111
|
+
const hasCliSurface = existsSync(join(cwd, '.claude', 'settings.json')) ||
|
|
112
|
+
existsSync(join(cwd, '.codex', 'settings.json'));
|
|
113
|
+
const hasCxDir = existsSync(join(cwd, '.cx'));
|
|
114
|
+
|
|
115
|
+
return !hasPackageJson && !hasGit && !hasConstructConfig && !hasCliSurface && !hasCxDir;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if this is a Construct project.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} cwd Directory path to check
|
|
122
|
+
* @returns {boolean} True if this is a Construct project
|
|
123
|
+
*/
|
|
124
|
+
export function isConstructProject(cwd) {
|
|
125
|
+
return existsSync(join(cwd, 'AGENTS.md')) ||
|
|
126
|
+
existsSync(join(cwd, 'plan.md')) ||
|
|
127
|
+
existsSync(join(cwd, '.cx', 'context.md')) ||
|
|
128
|
+
existsSync(join(cwd, '.cx', 'context.json'));
|
|
129
|
+
}
|