@geraldmaron/construct 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +9 -6
- package/LICENSE +201 -21
- package/README.md +132 -257
- package/agents/contracts.json +387 -0
- package/agents/prompts/cx-accessibility.md +8 -0
- package/agents/prompts/cx-ai-engineer.md +92 -0
- package/agents/prompts/cx-architect.md +10 -2
- package/agents/prompts/cx-business-strategist.md +13 -0
- package/agents/prompts/cx-data-analyst.md +80 -0
- package/agents/prompts/cx-data-engineer.md +4 -0
- package/agents/prompts/cx-debugger.md +8 -0
- package/agents/prompts/cx-designer.md +19 -0
- package/agents/prompts/cx-devil-advocate.md +4 -0
- package/agents/prompts/cx-docs-keeper.md +128 -0
- package/agents/prompts/cx-engineer.md +13 -0
- package/agents/prompts/cx-evaluator.md +4 -0
- package/agents/prompts/cx-explorer.md +12 -0
- package/agents/prompts/cx-legal-compliance.md +13 -0
- package/agents/prompts/cx-operations.md +13 -0
- package/agents/prompts/cx-orchestrator.md +107 -4
- package/agents/prompts/cx-platform-engineer.md +75 -0
- package/agents/prompts/cx-product-manager.md +8 -0
- package/agents/prompts/cx-qa.md +100 -0
- package/agents/prompts/cx-rd-lead.md +13 -0
- package/agents/prompts/cx-release-manager.md +8 -0
- package/agents/prompts/cx-researcher.md +21 -1
- package/agents/prompts/cx-reviewer.md +8 -0
- package/agents/prompts/cx-security.md +104 -0
- package/agents/prompts/cx-sre.md +104 -0
- package/agents/prompts/cx-test-automation.md +4 -0
- package/agents/prompts/cx-trace-reviewer.md +7 -3
- package/agents/prompts/cx-ux-researcher.md +4 -0
- package/agents/registry.json +365 -90
- package/agents/role-manifests.json +217 -0
- package/bin/construct +3345 -141
- package/bin/construct-postinstall.mjs +87 -0
- package/commands/build/feature.md +6 -6
- package/commands/plan/feature.md +6 -5
- package/commands/plan/requirements.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/work/drive.md +3 -3
- package/commands/work/optimize-prompts.md +3 -3
- package/db/{migrations → schema}/001_init.sql +2 -0
- package/db/schema/002_pgvector.sql +182 -0
- package/db/schema/003_intake.sql +47 -0
- package/examples/README.md +85 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
- package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
- package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
- package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
- package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
- package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
- package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
- package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
- package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
- package/examples/personas/construct/bad/commit-without-approval.md +30 -0
- package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
- package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
- package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
- package/examples/provider-plugin/README.md +34 -0
- package/examples/provider-plugin/index.mjs +74 -0
- package/examples/provider-plugin/package.json +15 -0
- package/examples/seed-observations/README.md +38 -0
- package/examples/seed-observations/anti-patterns.md +44 -0
- package/examples/seed-observations/decisions.md +36 -0
- package/examples/seed-observations/patterns.md +42 -0
- package/lib/agent-contracts-enforce.mjs +158 -0
- package/lib/agent-contracts.mjs +231 -0
- package/lib/agents/postconditions.mjs +126 -0
- package/lib/agents/schema.mjs +124 -0
- package/lib/artifact-capture.mjs +183 -0
- package/lib/audit-trail.mjs +149 -0
- package/lib/auto-docs.mjs +245 -65
- package/lib/beads/auto-close.mjs +126 -0
- package/lib/beads/drift.mjs +171 -0
- package/lib/beads-automation.mjs +542 -0
- package/lib/beads-client.mjs +518 -0
- package/lib/beads-lock.mjs +377 -0
- package/lib/beads-optimistic.mjs +365 -0
- package/lib/bootstrap/built-ins.mjs +136 -0
- package/lib/bootstrap/lazy-install.mjs +161 -0
- package/lib/bootstrap/resources.mjs +120 -0
- package/lib/bootstrap.mjs +105 -0
- package/lib/cache-governor.js +213 -0
- package/lib/cache-strategy-anthropic.js +62 -0
- package/lib/cache-strategy-google.js +79 -0
- package/lib/cache-strategy-none.js +30 -0
- package/lib/cache-strategy-openai.js +48 -0
- package/lib/cache-strategy.js +91 -0
- package/lib/claude-allow.mjs +149 -0
- package/lib/cli-commands.mjs +413 -258
- package/lib/codex-config.mjs +3 -1
- package/lib/comment-lint.mjs +55 -6
- package/lib/completions.mjs +21 -6
- package/lib/config/alias.mjs +56 -0
- package/lib/config/project-config.mjs +335 -0
- package/lib/config/schema.mjs +159 -0
- package/lib/context-router.mjs +308 -0
- package/lib/cost-ledger.mjs +177 -0
- package/lib/cost.mjs +171 -10
- package/lib/dashboard-static.mjs +158 -0
- package/lib/deployment-mode.mjs +86 -0
- package/lib/deprecate.mjs +49 -0
- package/lib/dispatch-batch.js +183 -0
- package/lib/distill.mjs +21 -8
- package/lib/doc-stamp.mjs +164 -0
- package/lib/doc-verify.mjs +119 -0
- package/lib/docs-routing.mjs +89 -0
- package/lib/docs-verify.mjs +417 -0
- package/lib/doctor/audit.mjs +71 -0
- package/lib/doctor/cli.mjs +99 -0
- package/lib/doctor/escalate.mjs +29 -0
- package/lib/doctor/index.mjs +140 -0
- package/lib/doctor/report.mjs +170 -0
- package/lib/doctor/watchers/bd-watch.mjs +117 -0
- package/lib/doctor/watchers/cost.mjs +130 -0
- package/lib/doctor/watchers/disk.mjs +122 -0
- package/lib/doctor/watchers/handoffs.mjs +33 -0
- package/lib/doctor/watchers/process-pressure.mjs +60 -0
- package/lib/doctor/watchers/service-health.mjs +188 -0
- package/lib/document-extract.mjs +288 -0
- package/lib/document-ingest.mjs +230 -0
- package/lib/drop.mjs +282 -0
- package/lib/embed/approval-queue.mjs +176 -0
- package/lib/embed/artifact.mjs +349 -0
- package/lib/embed/authority-guard.mjs +155 -0
- package/lib/embed/cli.mjs +408 -0
- package/lib/embed/config.mjs +355 -0
- package/lib/embed/conflict-detection.mjs +264 -0
- package/lib/embed/customer-profiles.mjs +480 -0
- package/lib/embed/daemon.mjs +1309 -0
- package/lib/embed/demand-fetch.mjs +449 -0
- package/lib/embed/docs-lifecycle.mjs +349 -0
- package/lib/embed/inbox-live-watcher.mjs +119 -0
- package/lib/embed/inbox.mjs +343 -0
- package/lib/embed/intake-metrics.mjs +190 -0
- package/lib/embed/jobs/vector-sync.mjs +198 -0
- package/lib/embed/notifications.mjs +75 -0
- package/lib/embed/output.mjs +79 -0
- package/lib/embed/providers/github.mjs +295 -0
- package/lib/embed/providers/jira.mjs +192 -0
- package/lib/embed/providers/linear.mjs +186 -0
- package/lib/embed/providers/registry.mjs +115 -0
- package/lib/embed/providers/slack.mjs +203 -0
- package/lib/embed/recommendation-store.mjs +378 -0
- package/lib/embed/roadmap.mjs +374 -0
- package/lib/embed/role-framing.mjs +110 -0
- package/lib/embed/scheduler.mjs +99 -0
- package/lib/embed/semantic.mjs +325 -0
- package/lib/embed/snapshot.mjs +191 -0
- package/lib/embed/supervision.mjs +235 -0
- package/lib/embed/target-resolver.mjs +186 -0
- package/lib/embed/worker.mjs +62 -0
- package/lib/embed/workspaces.mjs +297 -0
- package/lib/engine/chunker-headings.mjs +110 -0
- package/lib/engine/compressor-heuristic.mjs +100 -0
- package/lib/engine/consolidate.mjs +287 -0
- package/lib/engine/contracts.mjs +126 -0
- package/lib/engine/defaults.mjs +129 -0
- package/lib/engine/eval-retrieval.mjs +148 -0
- package/lib/engine/fuser-rrf.mjs +62 -0
- package/lib/engine/index.mjs +37 -0
- package/lib/engine/registry.mjs +146 -0
- package/lib/engine/reranker-mmr.mjs +90 -0
- package/lib/engine/tokens.mjs +77 -0
- package/lib/entity-store.mjs +280 -0
- package/lib/env-config.mjs +69 -4
- package/lib/evals/retrieval-bench.mjs +159 -0
- package/lib/evaluator-optimizer.mjs +317 -0
- package/lib/features.mjs +159 -29
- package/lib/gates-audit.mjs +236 -0
- package/lib/git-hooks/prepare-commit-msg +58 -0
- package/lib/handoffs/cleanup.mjs +159 -0
- package/lib/handoffs/contract.mjs +162 -0
- package/lib/handoffs/inventory.mjs +120 -0
- package/lib/headhunt.mjs +28 -47
- package/lib/health-check.mjs +399 -0
- package/lib/hook-health.mjs +442 -0
- package/lib/hooks/_lib/log.mjs +82 -0
- package/lib/hooks/adaptive-lint.mjs +26 -2
- package/lib/hooks/agent-tracker.mjs +171 -14
- package/lib/hooks/audit-reads.mjs +109 -0
- package/lib/hooks/audit-trail.mjs +153 -0
- package/lib/hooks/bash-output-logger.mjs +71 -0
- package/lib/hooks/block-no-verify.mjs +41 -0
- package/lib/hooks/ci-status-check.mjs +82 -0
- package/lib/hooks/comment-lint.mjs +29 -7
- package/lib/hooks/config-protection.mjs +39 -18
- package/lib/hooks/context-watch.mjs +137 -0
- package/lib/hooks/context-window-recovery.mjs +4 -20
- package/lib/hooks/dep-audit.mjs +16 -0
- package/lib/hooks/doc-coupling-check.mjs +80 -0
- package/lib/hooks/edit-accumulator.mjs +3 -0
- package/lib/hooks/edit-error-recovery.mjs +3 -0
- package/lib/hooks/edit-guard.mjs +45 -4
- package/lib/hooks/env-check.mjs +3 -0
- package/lib/hooks/guard-bash.mjs +58 -1
- package/lib/hooks/mcp-audit.mjs +18 -20
- package/lib/hooks/mcp-health-check.mjs +36 -0
- package/lib/hooks/model-fallback.mjs +42 -56
- package/lib/hooks/policy-engine.mjs +209 -0
- package/lib/hooks/post-merge-docs-check.mjs +63 -0
- package/lib/hooks/pre-compact.mjs +4 -24
- package/lib/hooks/pre-push-gate.mjs +200 -37
- package/lib/hooks/proactive-activation.mjs +284 -0
- package/lib/hooks/read-tracker.mjs +27 -4
- package/lib/hooks/readme-age-check.mjs +77 -0
- package/lib/hooks/registry-sync.mjs +12 -5
- package/lib/hooks/scan-secrets.mjs +50 -7
- package/lib/hooks/session-optimize.mjs +311 -0
- package/lib/hooks/session-start.mjs +287 -28
- package/lib/hooks/stop-notify.mjs +236 -96
- package/lib/hooks/stop-typecheck.mjs +13 -1
- package/lib/hooks/test-watch.mjs +68 -0
- package/lib/host-capabilities.mjs +31 -10
- package/lib/init-docs.mjs +731 -291
- package/lib/init-unified.mjs +1000 -0
- package/lib/init-update.mjs +168 -0
- package/lib/init.mjs +107 -0
- package/lib/install/first-invocation.mjs +119 -0
- package/lib/install/stage-project.mjs +69 -0
- package/lib/intake/classify.mjs +278 -0
- package/lib/intake/feedback.mjs +273 -0
- package/lib/intake/filesystem-queue.mjs +158 -0
- package/lib/intake/intake-config.mjs +132 -0
- package/lib/intake/postgres-queue.mjs +198 -0
- package/lib/intake/prepare.mjs +139 -0
- package/lib/intake/queue.mjs +83 -0
- package/lib/intake/session-prelude.mjs +50 -0
- package/lib/integrations/intake-integrations.mjs +740 -0
- package/lib/intent-classifier.mjs +253 -0
- package/lib/knowledge/layout.mjs +72 -0
- package/lib/knowledge/rag.mjs +331 -0
- package/lib/knowledge/search.mjs +315 -0
- package/lib/knowledge/trends.mjs +261 -0
- package/lib/logger.mjs +85 -0
- package/lib/mcp/broker.mjs +124 -0
- package/lib/mcp/server.mjs +554 -854
- package/lib/mcp/tools/document.mjs +132 -0
- package/lib/mcp/tools/memory.mjs +209 -0
- package/lib/mcp/tools/project.mjs +349 -0
- package/lib/mcp/tools/skills.mjs +409 -0
- package/lib/mcp/tools/storage.mjs +60 -0
- package/lib/mcp/tools/telemetry.mjs +315 -0
- package/lib/mcp/tools/workflow.mjs +112 -0
- package/lib/mcp-catalog.json +54 -3
- package/lib/mcp-manager.mjs +96 -48
- package/lib/mcp-platform-config.mjs +22 -22
- package/lib/memory-stats.mjs +121 -0
- package/lib/mode-commands.mjs +124 -0
- package/lib/model-free-selector.mjs +184 -0
- package/lib/model-pricing.mjs +152 -0
- package/lib/model-registry.mjs +226 -0
- package/lib/model-router.mjs +362 -386
- package/lib/observation-store.mjs +391 -0
- package/lib/ollama-manager.mjs +428 -0
- package/lib/opencode-config.mjs +13 -3
- package/lib/opencode-runtime-plugin.mjs +70 -38
- package/lib/opencode-telemetry.mjs +64 -6
- package/lib/orchestration-policy.mjs +509 -6
- package/lib/overrides/resolver.mjs +207 -0
- package/lib/parity.mjs +147 -0
- package/lib/paths.mjs +33 -0
- package/lib/performance/generate.mjs +212 -0
- package/lib/plugin-registry.mjs +268 -0
- package/lib/policy/engine.mjs +130 -0
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-detection.mjs +129 -0
- package/lib/project-init-shared.mjs +272 -0
- package/lib/project-profile.mjs +447 -0
- package/lib/prompt-composer.js +434 -0
- package/lib/prompt-metadata.mjs +1 -1
- package/lib/provider-capabilities-anthropic.js +44 -0
- package/lib/provider-capabilities-deepseek.js +37 -0
- package/lib/provider-capabilities-generic.js +26 -0
- package/lib/provider-capabilities-google.js +47 -0
- package/lib/provider-capabilities-openai.js +45 -0
- package/lib/provider-capabilities.js +142 -0
- package/lib/providers/atlassian-confluence/index.mjs +103 -0
- package/lib/providers/atlassian-jira/index.mjs +100 -0
- package/lib/providers/auth-manager.mjs +126 -0
- package/lib/providers/circuit-breaker.mjs +124 -0
- package/lib/providers/contract.mjs +93 -0
- package/lib/providers/github/index.mjs +126 -0
- package/lib/providers/registry.mjs +184 -0
- package/lib/providers/salesforce/index.mjs +100 -0
- package/lib/providers/slack/index.mjs +80 -0
- package/lib/reflect.mjs +137 -0
- package/lib/research-lint.mjs +164 -0
- package/lib/resources/budget.mjs +259 -0
- package/lib/role-preload.mjs +21 -6
- package/lib/roles/approval-surface.mjs +54 -0
- package/lib/roles/cli.mjs +118 -0
- package/lib/roles/event-bus.mjs +79 -0
- package/lib/roles/fence.mjs +84 -0
- package/lib/roles/gateway.mjs +260 -0
- package/lib/roles/hook-emit.mjs +37 -0
- package/lib/roles/manifest.mjs +48 -0
- package/lib/roles/router.mjs +27 -0
- package/lib/runtime-pressure.mjs +360 -0
- package/lib/schema-artifact.mjs +134 -0
- package/lib/schema-infer.mjs +551 -0
- package/lib/server/auth.mjs +168 -0
- package/lib/server/chat.mjs +336 -0
- package/lib/server/cors.mjs +77 -0
- package/lib/server/csrf.mjs +91 -0
- package/lib/server/index.mjs +1927 -78
- package/lib/server/insights.mjs +765 -0
- package/lib/server/rate-limit.mjs +91 -0
- package/lib/server/static/assets/index-ab25c707.js +70 -0
- package/lib/server/static/assets/index-f0c80a2b.css +1 -0
- package/lib/server/static/index.html +12 -817
- package/lib/server/telemetry-login.mjs +108 -0
- package/lib/server/webhook.mjs +510 -0
- package/lib/service-manager.mjs +522 -58
- package/lib/services/pattern-promotion-service.mjs +167 -0
- package/lib/services/telemetry-backend.mjs +178 -0
- package/lib/session-store.mjs +374 -0
- package/lib/setup-prompts.mjs +96 -0
- package/lib/setup.mjs +523 -36
- package/lib/skills-apply.mjs +280 -0
- package/lib/skills-scope.mjs +118 -0
- package/lib/status.mjs +261 -70
- package/lib/storage/admin.mjs +355 -0
- package/lib/storage/backend.mjs +2 -1
- package/lib/storage/backup.mjs +347 -0
- package/lib/storage/embeddings-engine.mjs +133 -0
- package/lib/storage/embeddings-legacy.mjs +85 -0
- package/lib/storage/embeddings-local.mjs +108 -0
- package/lib/storage/embeddings-ollama.mjs +78 -0
- package/lib/storage/embeddings-openai.mjs +85 -0
- package/lib/storage/embeddings.mjs +92 -33
- package/lib/storage/file-lock.mjs +130 -0
- package/lib/storage/fusion.mjs +95 -0
- package/lib/storage/hybrid-query.mjs +34 -27
- package/lib/storage/migrations.mjs +187 -0
- package/lib/storage/postgres-backup.mjs +124 -0
- package/lib/storage/sql-store.mjs +5 -15
- package/lib/storage/state-source.mjs +12 -13
- package/lib/storage/store-version.mjs +115 -0
- package/lib/storage/sync.mjs +144 -35
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/storage/vector-client.mjs +286 -0
- package/lib/storage/vector-store.mjs +71 -30
- package/lib/task-graph/generate.mjs +135 -0
- package/lib/task-graph/schema.mjs +81 -0
- package/lib/task-graph/store.mjs +71 -0
- package/lib/telemetry/backends/local.mjs +62 -0
- package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
- package/lib/telemetry/backfill.mjs +180 -0
- package/lib/telemetry/eval-datasets.mjs +203 -0
- package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
- package/lib/telemetry/intent-verifications.mjs +86 -0
- package/lib/telemetry/llm-judge.mjs +350 -0
- package/lib/telemetry/model-pricing-catalog.mjs +557 -0
- package/lib/telemetry/setup.mjs +151 -0
- package/lib/telemetry/skill-calls.mjs +78 -0
- package/lib/telemetry/team-rollup.mjs +4 -4
- package/lib/token-engine.js +117 -0
- package/lib/token-estimator-anthropic.js +15 -0
- package/lib/token-estimator-deepseek.js +13 -0
- package/lib/token-estimator-default.js +13 -0
- package/lib/token-estimator-google.js +13 -0
- package/lib/token-estimator-openai.js +13 -0
- package/lib/toolkit-env.mjs +1 -1
- package/lib/tty-prompts.mjs +211 -0
- package/lib/uninstall/uninstall.mjs +423 -0
- package/lib/update.mjs +115 -0
- package/lib/upgrade.mjs +141 -0
- package/lib/validator.mjs +51 -2
- package/lib/validators/skills.mjs +142 -0
- package/lib/wireframe.mjs +422 -0
- package/lib/worker/entrypoint.mjs +241 -0
- package/lib/worker/evidence.mjs +107 -0
- package/lib/worker/run.mjs +154 -0
- package/lib/worker/trace.mjs +182 -0
- package/lib/workflow-state.mjs +14 -18
- package/package.json +21 -8
- package/personas/construct.md +53 -51
- package/platforms/claude/CLAUDE.md +1 -1
- package/platforms/claude/settings.template.json +115 -68
- package/platforms/opencode/config.template.json +3 -7
- package/rules/common/agents.md +11 -38
- package/rules/common/beads-hygiene.md +75 -0
- package/rules/common/code-review.md +11 -100
- package/rules/common/coding-style.md +5 -3
- package/rules/common/comments.md +30 -111
- package/rules/common/commit-approval.md +53 -0
- package/rules/common/cx-agent-routing.md +1 -0
- package/rules/common/development-workflow.md +23 -41
- package/rules/common/doc-ownership.md +54 -0
- package/rules/common/efficiency.md +57 -0
- package/rules/common/framing.md +76 -0
- package/rules/common/git-workflow.md +2 -4
- package/rules/common/patterns.md +3 -7
- package/rules/common/performance.md +15 -31
- package/rules/common/release-gates.md +69 -0
- package/rules/common/research.md +107 -0
- package/rules/common/security.md +6 -6
- package/rules/common/skill-composition.md +67 -0
- package/rules/common/testing.md +15 -27
- package/rules/golang/hooks.md +0 -4
- package/rules/policy/bootstrap.yaml +11 -0
- package/rules/policy/drive.yaml +8 -0
- package/rules/policy/task.yaml +9 -0
- package/rules/policy/workflow.yaml +9 -0
- package/rules/python/hooks.md +0 -4
- package/rules/swift/hooks.md +0 -4
- package/rules/typescript/hooks.md +0 -4
- package/rules/web/hooks.md +0 -2
- package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
- package/skills/ai/prompt-optimizer.md +7 -7
- package/skills/compliance/ai-disclosure.md +58 -0
- package/skills/compliance/data-privacy.md +46 -0
- package/skills/compliance/license-audit.md +40 -0
- package/skills/compliance/regulatory-review.md +61 -0
- package/skills/docs/document-ingest-workflow.md +52 -0
- package/skills/docs/evidence-ingest-workflow.md +9 -0
- package/skills/docs/init-docs.md +51 -18
- package/skills/docs/product-intelligence-workflow.md +12 -0
- package/skills/docs/product-signal-workflow.md +4 -0
- package/skills/docs/research-workflow.md +23 -8
- package/skills/docs/runbook-workflow.md +1 -1
- package/skills/operating/orchestration-reference.md +151 -0
- package/skills/roles/architect.md +5 -0
- package/skills/roles/engineer.md +32 -0
- package/skills/roles/operator.md +12 -0
- package/skills/roles/reviewer.md +23 -0
- package/skills/routing.md +1 -1
- package/templates/devcontainer/Dockerfile.devcontainer +38 -0
- package/templates/devcontainer/devcontainer.json +31 -0
- package/templates/distribution/bootstrap.ps1 +142 -0
- package/templates/distribution/bootstrap.sh +196 -0
- package/templates/distribution/run.mjs +187 -0
- package/templates/docs/adr.md +44 -8
- package/templates/docs/changelog-entry.md +43 -0
- package/templates/docs/construct_guide.md +149 -0
- package/templates/docs/evidence-brief.md +2 -2
- package/templates/docs/meta-prd.md +140 -19
- package/templates/docs/onboarding.md +57 -0
- package/templates/docs/prd.md +159 -15
- package/templates/docs/research-brief.md +4 -4
- package/templates/homebrew/construct.rb +67 -0
- package/langfuse/docker-compose.yml +0 -82
- package/lib/eval-harness.mjs +0 -59
- package/lib/hooks/bootstrap-guard.mjs +0 -90
- package/lib/hooks/console-warn.mjs +0 -43
- package/lib/hooks/continuation-enforcer.mjs +0 -72
- package/lib/hooks/drive-guard.mjs +0 -89
- package/lib/hooks/mcp-task-scope.mjs +0 -47
- package/lib/hooks/task-completed-guard.mjs +0 -43
- package/lib/hooks/teammate-idle-guard.mjs +0 -54
- package/lib/hooks/workflow-guard.mjs +0 -62
- package/lib/prompt-composer.mjs +0 -196
- package/lib/review.mjs +0 -429
- package/lib/server/static/app.js +0 -841
- package/lib/telemetry/langfuse-model-sync.mjs +0 -270
- package/rules/common/hooks.md +0 -35
- package/rules/zh/README.md +0 -113
- package/rules/zh/agents.md +0 -55
- package/rules/zh/code-review.md +0 -129
- package/rules/zh/coding-style.md +0 -53
- package/rules/zh/development-workflow.md +0 -49
- package/rules/zh/git-workflow.md +0 -29
- package/rules/zh/hooks.md +0 -35
- package/rules/zh/patterns.md +0 -36
- package/rules/zh/performance.md +0 -60
- package/rules/zh/security.md +0 -34
- package/rules/zh/testing.md +0 -34
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/agent-contracts-enforce.mjs — runtime contract enforcement.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `validatePacket` from `lib/agent-contracts.mjs` with three production
|
|
5
|
+
* concerns:
|
|
6
|
+
*
|
|
7
|
+
* 1. Block on violation. `enforcePacket` throws a `ContractViolationError`
|
|
8
|
+
* when the packet doesn't satisfy the contract direction. Callers can
|
|
9
|
+
* catch and degrade if they want advisory mode, but the default is hard
|
|
10
|
+
* enforcement so contract bugs surface loudly.
|
|
11
|
+
*
|
|
12
|
+
* 2. Tamper-evident violation log. Every violation appends a JSONL record
|
|
13
|
+
* to `~/.cx/contract-violations.jsonl` with a sha256 chain-hash over
|
|
14
|
+
* the prior line. Same pattern as the mutation audit trail. Operators
|
|
15
|
+
* can replay the chain to detect after-the-fact tampering.
|
|
16
|
+
*
|
|
17
|
+
* 3. Visibility into `construct doctor`. `recentViolations(windowMs)`
|
|
18
|
+
* returns the last N violations so doctor can surface
|
|
19
|
+
* "3 contract violations in the last 24h — see ~/.cx/contract-violations.jsonl".
|
|
20
|
+
*
|
|
21
|
+
* The enforcement is identity-aware. Each violation logs the active agent
|
|
22
|
+
* (via `~/.cx/last-agent.json` like audit-trail does) so operators can
|
|
23
|
+
* identify which producer or consumer is at fault.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync, mkdirSync, statSync, readFileSync, appendFileSync } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
import { homedir } from 'node:os';
|
|
29
|
+
import { createHash } from 'node:crypto';
|
|
30
|
+
import { getContractById, validatePacket } from './agent-contracts.mjs';
|
|
31
|
+
import { validatePostconditions } from './agents/postconditions.mjs';
|
|
32
|
+
|
|
33
|
+
const CX_DIR = join(homedir(), '.cx');
|
|
34
|
+
const LOG_FILE = join(CX_DIR, 'contract-violations.jsonl');
|
|
35
|
+
const LAST_AGENT = join(CX_DIR, 'last-agent.json');
|
|
36
|
+
|
|
37
|
+
export class ContractViolationError extends Error {
|
|
38
|
+
constructor({ contractId, direction, missing, verdict = 'CONTRACT_VIOLATION', postconditionFailures = [] }) {
|
|
39
|
+
const detail = postconditionFailures.length > 0
|
|
40
|
+
? `postcondition(s) failed: ${postconditionFailures.map((f) => f.id).join(', ')}`
|
|
41
|
+
: `missing field(s): ${(missing || []).join(', ')}`;
|
|
42
|
+
super(`contract '${contractId}' (${direction}) violated — ${detail}`);
|
|
43
|
+
this.name = 'ContractViolationError';
|
|
44
|
+
this.contractId = contractId;
|
|
45
|
+
this.direction = direction;
|
|
46
|
+
this.missing = missing || [];
|
|
47
|
+
this.verdict = verdict;
|
|
48
|
+
this.postconditionFailures = postconditionFailures;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sha256(input) { return createHash('sha256').update(input).digest('hex'); }
|
|
53
|
+
|
|
54
|
+
function readLastAgent() {
|
|
55
|
+
try { return JSON.parse(readFileSync(LAST_AGENT, 'utf8'))?.agent || 'construct'; }
|
|
56
|
+
catch { return 'construct'; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readPrevLineHash() {
|
|
60
|
+
try {
|
|
61
|
+
if (!existsSync(LOG_FILE)) return null;
|
|
62
|
+
const size = statSync(LOG_FILE).size;
|
|
63
|
+
if (size === 0) return null;
|
|
64
|
+
const tail = readFileSync(LOG_FILE, 'utf8').slice(Math.max(0, size - 2048));
|
|
65
|
+
const lines = tail.split('\n').filter(Boolean);
|
|
66
|
+
return lines.length === 0 ? null : sha256(lines[lines.length - 1]);
|
|
67
|
+
} catch { return null; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function logViolation(contractId, direction, missing, packet, extra = {}) {
|
|
71
|
+
try {
|
|
72
|
+
mkdirSync(CX_DIR, { recursive: true });
|
|
73
|
+
const record = {
|
|
74
|
+
ts: new Date().toISOString(),
|
|
75
|
+
agent: readLastAgent(),
|
|
76
|
+
contractId,
|
|
77
|
+
direction,
|
|
78
|
+
missing,
|
|
79
|
+
packet_keys: packet && typeof packet === 'object' ? Object.keys(packet) : null,
|
|
80
|
+
prev_line_hash: readPrevLineHash(),
|
|
81
|
+
...extra,
|
|
82
|
+
};
|
|
83
|
+
appendFileSync(LOG_FILE, JSON.stringify(record) + '\n', 'utf8');
|
|
84
|
+
} catch { /* logging is best-effort */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validate `packet` against `contractId` in the given `direction`. Throws
|
|
89
|
+
* a `ContractViolationError` on failure (and logs the violation). Returns
|
|
90
|
+
* the validation result on success so callers can inspect the resolved
|
|
91
|
+
* contract metadata.
|
|
92
|
+
*
|
|
93
|
+
* For `direction === 'output'`, also evaluates the producer's binary
|
|
94
|
+
* postconditions from `lib/agents/postconditions.mjs`. A failed postcondition
|
|
95
|
+
* raises with `verdict: 'BLOCKED_CONTRACT'` so the dispatcher can branch on
|
|
96
|
+
* the typed verdict rather than parsing the error message.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} contractId
|
|
99
|
+
* @param {object} packet
|
|
100
|
+
* @param {'input'|'output'} [direction]
|
|
101
|
+
* @returns {{ ok: true, contract, direction }}
|
|
102
|
+
*/
|
|
103
|
+
export function enforcePacket(contractId, packet, direction = 'input') {
|
|
104
|
+
const result = validatePacket(contractId, packet, direction);
|
|
105
|
+
if (!result.ok) {
|
|
106
|
+
if (result.reason === 'contract-not-found') {
|
|
107
|
+
throw new ContractViolationError({
|
|
108
|
+
contractId,
|
|
109
|
+
direction,
|
|
110
|
+
missing: ['(contract not found)'],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
logViolation(contractId, direction, result.missing || [], packet);
|
|
114
|
+
throw new ContractViolationError({
|
|
115
|
+
contractId,
|
|
116
|
+
direction,
|
|
117
|
+
missing: result.missing || [],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (direction === 'output') {
|
|
122
|
+
const contract = result.contract || getContractById(contractId);
|
|
123
|
+
const producer = contract?.producer;
|
|
124
|
+
if (producer) {
|
|
125
|
+
const postcondition = validatePostconditions(producer, packet);
|
|
126
|
+
if (!postcondition.ok) {
|
|
127
|
+
logViolation(contractId, direction, [], packet, {
|
|
128
|
+
verdict: 'BLOCKED_CONTRACT',
|
|
129
|
+
postconditionFailures: postcondition.failures,
|
|
130
|
+
});
|
|
131
|
+
throw new ContractViolationError({
|
|
132
|
+
contractId,
|
|
133
|
+
direction,
|
|
134
|
+
verdict: 'BLOCKED_CONTRACT',
|
|
135
|
+
postconditionFailures: postcondition.failures,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Read recent violations from the chain log. Returns oldest-first.
|
|
146
|
+
*/
|
|
147
|
+
export function recentViolations({ windowMs = 24 * 60 * 60 * 1000 } = {}) {
|
|
148
|
+
if (!existsSync(LOG_FILE)) return [];
|
|
149
|
+
try {
|
|
150
|
+
const cutoff = Date.now() - windowMs;
|
|
151
|
+
return readFileSync(LOG_FILE, 'utf8')
|
|
152
|
+
.trim()
|
|
153
|
+
.split('\n')
|
|
154
|
+
.filter(Boolean)
|
|
155
|
+
.map((line) => { try { return JSON.parse(line); } catch { return null; } })
|
|
156
|
+
.filter((r) => r && new Date(r.ts).getTime() >= cutoff);
|
|
157
|
+
} catch { return []; }
|
|
158
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/agent-contracts.mjs — loader + query layer for agents/contracts.json.
|
|
3
|
+
*
|
|
4
|
+
* Agent contracts are explicit service contracts between Construct roles —
|
|
5
|
+
* "when cx-product-manager hands off to cx-architect, the packet MUST contain
|
|
6
|
+
* X and the response MUST be shape Y." Single machine-readable source of
|
|
7
|
+
* truth for handoff schemas, doc ownership, specialist mappings, and the
|
|
8
|
+
* framing/research gates.
|
|
9
|
+
*
|
|
10
|
+
* Consumed by:
|
|
11
|
+
* - lib/orchestration-policy.mjs → routeRequest returns the contract chain
|
|
12
|
+
* for a request, not just a list of specialists.
|
|
13
|
+
* - lib/mcp/server.mjs → exposes agent_contract MCP tool for specialists
|
|
14
|
+
* to introspect their inputs and expected outputs at handoff time.
|
|
15
|
+
* - future: handoff validators that check packet shape before dispatch.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
18
|
+
import { dirname, join, resolve } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const REPO_ROOT = resolve(MODULE_DIR, '..');
|
|
23
|
+
const CONTRACTS_PATH = join(REPO_ROOT, 'agents', 'contracts.json');
|
|
24
|
+
|
|
25
|
+
let _cache = null;
|
|
26
|
+
|
|
27
|
+
function loadRaw() {
|
|
28
|
+
if (_cache) return _cache;
|
|
29
|
+
if (!existsSync(CONTRACTS_PATH)) {
|
|
30
|
+
_cache = { version: 0, contracts: [] };
|
|
31
|
+
return _cache;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
_cache = JSON.parse(readFileSync(CONTRACTS_PATH, 'utf8'));
|
|
35
|
+
} catch (err) {
|
|
36
|
+
_cache = { version: 0, contracts: [], parseError: err?.message || String(err) };
|
|
37
|
+
}
|
|
38
|
+
return _cache;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getAllContracts() {
|
|
42
|
+
return loadRaw().contracts || [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getContractById(id) {
|
|
46
|
+
return getAllContracts().find((c) => c.id === id) || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns the contract for a specific producer→consumer pair. If multiple
|
|
51
|
+
* contracts match (e.g. wildcard producer "*"), returns the most specific one.
|
|
52
|
+
*/
|
|
53
|
+
export function getContract(producer, consumer) {
|
|
54
|
+
const all = getAllContracts();
|
|
55
|
+
const exact = all.find((c) => c.producer === producer && c.consumer === consumer);
|
|
56
|
+
if (exact) return exact;
|
|
57
|
+
return all.find((c) => (c.producer === '*' && c.consumer === consumer)) || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns all contracts where the given agent is the consumer — the set of
|
|
62
|
+
* inbound packets the agent is expected to receive from upstream producers.
|
|
63
|
+
*/
|
|
64
|
+
export function getIncomingContracts(consumer) {
|
|
65
|
+
return getAllContracts().filter((c) => c.consumer === consumer);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Returns all contracts where the given agent is the producer — the set of
|
|
70
|
+
* outbound packets the agent is expected to send to downstream consumers.
|
|
71
|
+
*/
|
|
72
|
+
export function getOutgoingContracts(producer) {
|
|
73
|
+
return getAllContracts().filter((c) => c.producer === producer || c.producer === '*');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Matches contracts whose `trigger` is satisfied by the given routing
|
|
78
|
+
* context. Surfaces which contracts apply to a dispatch plan so routeRequest
|
|
79
|
+
* can attach them to the response.
|
|
80
|
+
*
|
|
81
|
+
* Trigger language:
|
|
82
|
+
* { always: true } → always fires
|
|
83
|
+
* { intent: "fix" | ["fix","investigation"] } → intent match
|
|
84
|
+
* { workCategory: "deep" } → work category match
|
|
85
|
+
* { track: "orchestrated" } → execution track match
|
|
86
|
+
* { riskFlags: ["security"] } → any-of risk flags set
|
|
87
|
+
* { "framingChallenge.required": true } → gate required
|
|
88
|
+
* { "externalResearch.required": true } → gate required
|
|
89
|
+
* { "docAuthoring.docType": ["prd","adr"] } → authoring intent match
|
|
90
|
+
* { postConsumer: true, changedCoreDocs: true } → fires after consumer ran and core docs changed
|
|
91
|
+
*/
|
|
92
|
+
export function triggerMatches(contract, ctx = {}) {
|
|
93
|
+
const t = contract.trigger || {};
|
|
94
|
+
if (t.always === true) return true;
|
|
95
|
+
|
|
96
|
+
// intent: string | string[]
|
|
97
|
+
if (t.intent) {
|
|
98
|
+
const intents = Array.isArray(t.intent) ? t.intent : [t.intent];
|
|
99
|
+
if (!intents.includes(ctx.intent)) return false;
|
|
100
|
+
}
|
|
101
|
+
if (t.workCategory) {
|
|
102
|
+
const cats = Array.isArray(t.workCategory) ? t.workCategory : [t.workCategory];
|
|
103
|
+
if (!cats.includes(ctx.workCategory)) return false;
|
|
104
|
+
}
|
|
105
|
+
if (t.track) {
|
|
106
|
+
const tracks = Array.isArray(t.track) ? t.track : [t.track];
|
|
107
|
+
if (!tracks.includes(ctx.track)) return false;
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(t.riskFlags)) {
|
|
110
|
+
const anyMatch = t.riskFlags.some((flag) => ctx.riskFlags?.[flag]);
|
|
111
|
+
if (!anyMatch) return false;
|
|
112
|
+
}
|
|
113
|
+
if (t['framingChallenge.required'] === true) {
|
|
114
|
+
if (!ctx.framingChallenge?.required) return false;
|
|
115
|
+
}
|
|
116
|
+
if (t['externalResearch.required'] === true) {
|
|
117
|
+
if (!ctx.externalResearch?.required) return false;
|
|
118
|
+
}
|
|
119
|
+
if (t['docAuthoring.docType']) {
|
|
120
|
+
const docTypes = Array.isArray(t['docAuthoring.docType'])
|
|
121
|
+
? t['docAuthoring.docType']
|
|
122
|
+
: [t['docAuthoring.docType']];
|
|
123
|
+
if (!ctx.docAuthoring?.docType || !docTypes.includes(ctx.docAuthoring.docType)) return false;
|
|
124
|
+
}
|
|
125
|
+
// Post-consumer contracts (e.g. docs-keeper) fire as side effects, not
|
|
126
|
+
// from the initial routing decision. They're returned by
|
|
127
|
+
// resolveContractChain but marked so callers can defer them.
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function contractParticipantsApply(contract, ctx = {}) {
|
|
132
|
+
const specialists = Array.isArray(ctx.specialists) ? ctx.specialists : null;
|
|
133
|
+
if (!specialists) return true;
|
|
134
|
+
|
|
135
|
+
const participants = new Set(['user', 'construct', ...specialists]);
|
|
136
|
+
const producerApplies = contract.producer === '*' || participants.has(contract.producer);
|
|
137
|
+
const consumerApplies = participants.has(contract.consumer);
|
|
138
|
+
return producerApplies && consumerApplies;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Returns the ordered contract chain that applies to a routing context.
|
|
143
|
+
* Each element is `{ contract, stage }` where stage is "precheck" (runs
|
|
144
|
+
* before work begins), "handoff" (runs between specialists), or "followup"
|
|
145
|
+
* (runs after DONE, e.g. docs-keeper).
|
|
146
|
+
*/
|
|
147
|
+
export function resolveContractChain(ctx = {}) {
|
|
148
|
+
const chain = [];
|
|
149
|
+
for (const contract of getAllContracts()) {
|
|
150
|
+
if (!triggerMatches(contract, ctx)) continue;
|
|
151
|
+
if (!contractParticipantsApply(contract, ctx)) continue;
|
|
152
|
+
const stage = contract.trigger?.postConsumer ? 'followup'
|
|
153
|
+
: contract.producer === 'user' ? 'precheck'
|
|
154
|
+
: 'handoff';
|
|
155
|
+
chain.push({ contract, stage });
|
|
156
|
+
}
|
|
157
|
+
return chain;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function fieldPresent(packet, key) {
|
|
161
|
+
const value = packet?.[key];
|
|
162
|
+
if (value === undefined || value === null) return false;
|
|
163
|
+
if (typeof value === 'string' && value.trim() === '') return false;
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Lightweight shape validation for handoff packets. Checks three shapes:
|
|
169
|
+
* mustContain — every key must be present (non-null, non-empty-string).
|
|
170
|
+
* mustContainOneOf — array of OR groups; at least one field in each group
|
|
171
|
+
* must be present. Missing entries report joined keys:
|
|
172
|
+
* ["findings", "noIssuesFoundAt"] → "findings|noIssuesFoundAt".
|
|
173
|
+
* mustMatchEnum — map of key → allowed values. Mismatch reports
|
|
174
|
+
* "<key>!=<allowed>" so callers can branch on it.
|
|
175
|
+
*
|
|
176
|
+
* Empty arrays and empty objects count as present — they signal "declared
|
|
177
|
+
* but empty," a legitimate state. Use `mustHaveNonEmpty` in a future spec
|
|
178
|
+
* if you need a stricter check.
|
|
179
|
+
*/
|
|
180
|
+
export function validatePacket(contractId, packet, direction = 'input') {
|
|
181
|
+
const contract = getContractById(contractId);
|
|
182
|
+
if (!contract) return { ok: false, reason: 'contract-not-found', contractId };
|
|
183
|
+
const spec = direction === 'input' ? contract.input : contract.output;
|
|
184
|
+
if (!spec) return { ok: true, contract, direction };
|
|
185
|
+
|
|
186
|
+
const missing = [];
|
|
187
|
+
|
|
188
|
+
if (Array.isArray(spec.mustContain)) {
|
|
189
|
+
for (const key of spec.mustContain) {
|
|
190
|
+
if (!fieldPresent(packet, key)) missing.push(key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (Array.isArray(spec.mustContainOneOf)) {
|
|
195
|
+
for (const group of spec.mustContainOneOf) {
|
|
196
|
+
if (!Array.isArray(group) || group.length === 0) continue;
|
|
197
|
+
const anyPresent = group.some((key) => fieldPresent(packet, key));
|
|
198
|
+
if (!anyPresent) missing.push(group.join('|'));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (spec.mustMatchEnum && typeof spec.mustMatchEnum === 'object') {
|
|
203
|
+
for (const [key, allowed] of Object.entries(spec.mustMatchEnum)) {
|
|
204
|
+
if (!Array.isArray(allowed) || allowed.length === 0) continue;
|
|
205
|
+
if (!fieldPresent(packet, key)) continue;
|
|
206
|
+
if (!allowed.includes(packet[key])) missing.push(`${key}!=${allowed.join('|')}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { ok: missing.length === 0, missing, contract, direction };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Summary statistics for `construct status` / `construct doctor` surfaces.
|
|
215
|
+
*/
|
|
216
|
+
export function summarize() {
|
|
217
|
+
const all = getAllContracts();
|
|
218
|
+
const byProducer = {};
|
|
219
|
+
const byConsumer = {};
|
|
220
|
+
for (const c of all) {
|
|
221
|
+
byProducer[c.producer] = (byProducer[c.producer] || 0) + 1;
|
|
222
|
+
byConsumer[c.consumer] = (byConsumer[c.consumer] || 0) + 1;
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
total: all.length,
|
|
226
|
+
producers: Object.keys(byProducer).length,
|
|
227
|
+
consumers: Object.keys(byConsumer).length,
|
|
228
|
+
byProducer,
|
|
229
|
+
byConsumer,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/agents/postconditions.mjs — hard binary postconditions per producer persona.
|
|
3
|
+
*
|
|
4
|
+
* `agents/contracts.json` carries free-text postconditions for documentation
|
|
5
|
+
* purposes. This module adds binary, programmatically-validated assertions
|
|
6
|
+
* that the persona's output packet must satisfy before a handoff is allowed
|
|
7
|
+
* to proceed. Failure throws `ContractViolationError` with reason
|
|
8
|
+
* `BLOCKED_CONTRACT` so the dispatcher refuses to forward a packet that
|
|
9
|
+
* silently rubber-stamps, fixes symptoms without root cause, ships stale
|
|
10
|
+
* docs, or skips accessibility review.
|
|
11
|
+
*
|
|
12
|
+
* Each rule is identified by a stable id so violations are greppable in
|
|
13
|
+
* `~/.cx/contract-violations.jsonl`. Rules are pure functions of the
|
|
14
|
+
* output packet — no IO, no LLM calls — so they're deterministic and
|
|
15
|
+
* cheap to evaluate.
|
|
16
|
+
*
|
|
17
|
+
* Producers covered (CF3 round 1 from the 2026-05-13 UX audit):
|
|
18
|
+
* - cx-reviewer prevents silent rubber-stamp reviews
|
|
19
|
+
* - cx-security prevents post-hoc threat models
|
|
20
|
+
* - cx-debugger prevents symptom-only fixes
|
|
21
|
+
* - cx-docs-keeper prevents stale-doc PRs
|
|
22
|
+
* - cx-designer prevents post-hoc accessibility
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const ROOT_CAUSE_SOURCES = new Set(['reproduction', 'trace', 'test']);
|
|
26
|
+
|
|
27
|
+
function isNonEmptyArray(value) {
|
|
28
|
+
return Array.isArray(value) && value.length > 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isNonEmptyString(value) {
|
|
32
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isLaterOrEqual(a, b) {
|
|
36
|
+
const ta = a instanceof Date ? a.getTime() : Date.parse(a);
|
|
37
|
+
const tb = b instanceof Date ? b.getTime() : Date.parse(b);
|
|
38
|
+
if (!Number.isFinite(ta) || !Number.isFinite(tb)) return false;
|
|
39
|
+
return ta >= tb;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Rule table. Keyed by producer name (matching agents/registry.json).
|
|
44
|
+
* Each rule:
|
|
45
|
+
* id — stable identifier for violation logs
|
|
46
|
+
* description — single-line documentation
|
|
47
|
+
* check — (packet) => boolean — true when satisfied
|
|
48
|
+
* reason — explanation surfaced when violated
|
|
49
|
+
*/
|
|
50
|
+
export const POSTCONDITIONS = {
|
|
51
|
+
'cx-reviewer': [
|
|
52
|
+
{
|
|
53
|
+
id: 'reviewer.findings-or-explicit-clear',
|
|
54
|
+
description: 'Reviewer must either return at least one finding or explicitly state "no issues found at: <paths>".',
|
|
55
|
+
check: (p) => isNonEmptyArray(p?.findings) || isNonEmptyArray(p?.noIssuesFoundAt) || isNonEmptyString(p?.noIssuesFoundStatement),
|
|
56
|
+
reason: 'Reviewer output rubber-stamped: empty findings and no explicit "no issues found at: <paths>" statement.',
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
'cx-security': [
|
|
60
|
+
{
|
|
61
|
+
id: 'security.threat-model-not-post-hoc',
|
|
62
|
+
description: 'Threat model must be updated at or after the contract start (not retrofitted).',
|
|
63
|
+
check: (p) => {
|
|
64
|
+
if (!p?.threatModelUpdatedAt || !p?.contractStart) return false;
|
|
65
|
+
return isLaterOrEqual(p.threatModelUpdatedAt, p.contractStart);
|
|
66
|
+
},
|
|
67
|
+
reason: 'Threat model missing or older than the contract start — likely retrofitted after implementation.',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
'cx-debugger': [
|
|
71
|
+
{
|
|
72
|
+
id: 'debugger.root-cause-confirmed-via',
|
|
73
|
+
description: 'Root cause must be confirmed via reproduction, trace, or test (not inferred).',
|
|
74
|
+
check: (p) => typeof p?.rootCauseConfirmedVia === 'string' && ROOT_CAUSE_SOURCES.has(p.rootCauseConfirmedVia),
|
|
75
|
+
reason: `rootCauseConfirmedVia must be one of: ${[...ROOT_CAUSE_SOURCES].join(', ')}.`,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
'cx-docs-keeper': [
|
|
79
|
+
{
|
|
80
|
+
id: 'docs-keeper.cross-doc-coherence-check-ran',
|
|
81
|
+
description: 'Docs-keeper must run the cross-doc coherence check and attach a named diff.',
|
|
82
|
+
check: (p) => p?.crossDocCoherenceCheckRan === true && isNonEmptyString(p?.coherenceDiff),
|
|
83
|
+
reason: 'crossDocCoherenceCheckRan must be true AND coherenceDiff must be a non-empty named diff.',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
'cx-designer': [
|
|
87
|
+
{
|
|
88
|
+
id: 'designer.accessibility-check-ran',
|
|
89
|
+
description: 'Designer must run the accessibility check before handoff (no post-hoc a11y).',
|
|
90
|
+
check: (p) => p?.accessibilityCheckRan === true,
|
|
91
|
+
reason: 'accessibilityCheckRan must be true — accessibility review is a precondition for any visual deliverable.',
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Evaluate the binary postcondition rules for a producer's output packet.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} producer — persona name (e.g. `cx-reviewer`)
|
|
100
|
+
* @param {object} packet — the output packet about to hand off
|
|
101
|
+
* @returns {{ ok: boolean, producer: string, failures: Array<{id, reason}> }}
|
|
102
|
+
*/
|
|
103
|
+
export function validatePostconditions(producer, packet) {
|
|
104
|
+
const rules = POSTCONDITIONS[producer] || [];
|
|
105
|
+
if (rules.length === 0) return { ok: true, producer, failures: [] };
|
|
106
|
+
const failures = [];
|
|
107
|
+
for (const rule of rules) {
|
|
108
|
+
let satisfied;
|
|
109
|
+
try {
|
|
110
|
+
satisfied = rule.check(packet);
|
|
111
|
+
} catch {
|
|
112
|
+
satisfied = false;
|
|
113
|
+
}
|
|
114
|
+
if (!satisfied) failures.push({ id: rule.id, reason: rule.reason });
|
|
115
|
+
}
|
|
116
|
+
return { ok: failures.length === 0, producer, failures };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* List the rule ids registered for a producer — useful for tests and
|
|
121
|
+
* for the `agent_contract` MCP tool when surfacing what a persona must
|
|
122
|
+
* produce.
|
|
123
|
+
*/
|
|
124
|
+
export function describePostconditions(producer) {
|
|
125
|
+
return (POSTCONDITIONS[producer] || []).map((r) => ({ id: r.id, description: r.description }));
|
|
126
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/agents/schema.mjs — validation contract for agents/registry.json entries.
|
|
3
|
+
*
|
|
4
|
+
* Catches drift before it ships: missing description, unknown tool names,
|
|
5
|
+
* promptFile pointing at a path that does not resolve, unrecognized
|
|
6
|
+
* modelTier values. Run via `construct lint:agents` and in CI so a typo
|
|
7
|
+
* in the registry can't silently disable an agent.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
const REQUIRED_FIELDS = ['name', 'description', 'promptFile'];
|
|
14
|
+
|
|
15
|
+
// Tool names that can appear in claudeTools. Anything outside this allowlist
|
|
16
|
+
// surfaces as an error; the host platform will silently drop unknown tool
|
|
17
|
+
// references and the agent will appear to "work" without the tool, which is
|
|
18
|
+
// the failure mode this lint catches.
|
|
19
|
+
|
|
20
|
+
export const ALLOWED_TOOLS = new Set([
|
|
21
|
+
// Anthropic Claude Code built-ins
|
|
22
|
+
'Read', 'Write', 'Edit', 'Glob', 'Grep', 'LS', 'Bash', 'BashOutput',
|
|
23
|
+
'WebSearch', 'WebFetch', 'TodoWrite', 'NotebookEdit', 'Task',
|
|
24
|
+
// Construct MCP tools (from construct-mcp server)
|
|
25
|
+
'list_skills', 'get_skill', 'search_skills',
|
|
26
|
+
'list_teams', 'get_team',
|
|
27
|
+
'list_templates', 'get_template',
|
|
28
|
+
'agent_contract', 'orchestration_policy',
|
|
29
|
+
'cx_trace', 'cx_score',
|
|
30
|
+
// Memory MCP server
|
|
31
|
+
'memory_search', 'memory_add_observations', 'memory_open_nodes',
|
|
32
|
+
'memory_create_entities', 'memory_create_relations',
|
|
33
|
+
// Other MCP servers Construct ships with
|
|
34
|
+
'context7_resolve', 'context7_docs',
|
|
35
|
+
'sequential_thinking',
|
|
36
|
+
'playwright_navigate', 'playwright_screenshot',
|
|
37
|
+
'github_search', 'github_get_pull_request',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validate one agent record. Returns an array of error strings; empty means
|
|
42
|
+
* the record passes.
|
|
43
|
+
*/
|
|
44
|
+
export function validateAgentRecord(record, { rootDir, registryKey } = {}) {
|
|
45
|
+
const errors = [];
|
|
46
|
+
const id = record?.name || `#${registryKey ?? '?'}`;
|
|
47
|
+
|
|
48
|
+
if (!record || typeof record !== 'object') {
|
|
49
|
+
return [`${id}: agent record must be an object`];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const field of REQUIRED_FIELDS) {
|
|
53
|
+
if (!record[field]) errors.push(`${id}: missing required field "${field}"`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (record.description) {
|
|
57
|
+
const desc = String(record.description).trim();
|
|
58
|
+
if (desc.length < 20) errors.push(`${id}: description is too short (<20 chars) — the description is load-bearing for routing`);
|
|
59
|
+
if (desc.length > 500) errors.push(`${id}: description is too long (>500 chars) — keep it scannable`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (record.when_to_use !== undefined && record.when_to_use !== null) {
|
|
63
|
+
const wtu = String(record.when_to_use).trim();
|
|
64
|
+
if (wtu.length < 20) errors.push(`${id}: when_to_use is too short (<20 chars) — should describe the trigger conditions`);
|
|
65
|
+
if (wtu.length > 500) errors.push(`${id}: when_to_use is too long (>500 chars) — keep it scannable for routing`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (record.promptFile && rootDir) {
|
|
69
|
+
const promptPath = path.join(rootDir, record.promptFile);
|
|
70
|
+
if (!fs.existsSync(promptPath)) errors.push(`${id}: promptFile "${record.promptFile}" does not exist`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (record.claudeTools) {
|
|
74
|
+
const tools = String(record.claudeTools).split(',').map((t) => t.trim()).filter(Boolean);
|
|
75
|
+
for (const tool of tools) {
|
|
76
|
+
if (!ALLOWED_TOOLS.has(tool)) {
|
|
77
|
+
errors.push(`${id}: unknown tool "${tool}" in claudeTools — host platforms silently drop unknown tool names, causing this agent to appear to work without it`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (record.modelTier && !['fast', 'standard', 'reasoning'].includes(record.modelTier)) {
|
|
83
|
+
errors.push(`${id}: modelTier must be one of fast | standard | reasoning (got "${record.modelTier}")`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return errors;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate every agent in a registry. Returns { errors: string[], agentCount }.
|
|
91
|
+
*/
|
|
92
|
+
export function validateRegistry(registry, { rootDir } = {}) {
|
|
93
|
+
if (!registry || !Array.isArray(registry.agents)) {
|
|
94
|
+
return { errors: ['registry.agents is missing or not an array'], agentCount: 0 };
|
|
95
|
+
}
|
|
96
|
+
const errors = [];
|
|
97
|
+
const seenNames = new Set();
|
|
98
|
+
for (let i = 0; i < registry.agents.length; i += 1) {
|
|
99
|
+
const record = registry.agents[i];
|
|
100
|
+
const recordErrors = validateAgentRecord(record, { rootDir, registryKey: i });
|
|
101
|
+
errors.push(...recordErrors);
|
|
102
|
+
if (record?.name) {
|
|
103
|
+
if (seenNames.has(record.name)) errors.push(`${record.name}: duplicate agent name at registry index ${i}`);
|
|
104
|
+
seenNames.add(record.name);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { errors, agentCount: registry.agents.length };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Convenience: load registry.json from disk and validate it.
|
|
112
|
+
*/
|
|
113
|
+
export function validateRegistryFile({ registryPath, rootDir } = {}) {
|
|
114
|
+
if (!fs.existsSync(registryPath)) {
|
|
115
|
+
return { errors: [`registry not found at ${registryPath}`], agentCount: 0 };
|
|
116
|
+
}
|
|
117
|
+
let registry;
|
|
118
|
+
try {
|
|
119
|
+
registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return { errors: [`registry parse error: ${err.message}`], agentCount: 0 };
|
|
122
|
+
}
|
|
123
|
+
return validateRegistry(registry, { rootDir });
|
|
124
|
+
}
|