@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
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* lib/hooks/agent-tracker.mjs — Agent task lifecycle hook — tracks task start, completion, and handoffs.
|
|
4
4
|
*
|
|
5
5
|
* Runs as PostToolUse after Agent tool calls. Records agent invocations and their outcomes to ~/.cx/agent-log.json for telemetry and performance review.
|
|
6
|
+
* Also records observations for learning system to identify patterns/anti-patterns.
|
|
7
|
+
*
|
|
8
|
+
* @p95ms 10
|
|
9
|
+
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
6
10
|
*/
|
|
7
|
-
|
|
8
|
-
// stop-notify.mjs reads this to attribute per-turn token costs to the dispatching agent.
|
|
9
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, appendFileSync } from 'fs';
|
|
10
12
|
import { join } from 'path';
|
|
11
13
|
import { homedir } from 'os';
|
|
12
14
|
|
|
@@ -17,7 +19,7 @@ const toolName = input?.tool_name || '';
|
|
|
17
19
|
if (toolName !== 'Task') process.exit(0);
|
|
18
20
|
|
|
19
21
|
const toolInput = input?.tool_input || {};
|
|
20
|
-
const
|
|
22
|
+
const toolResult = input?.tool_result || {};
|
|
21
23
|
|
|
22
24
|
// Extract agent identity from the Task tool input.
|
|
23
25
|
// Claude Code passes subagent_type or description in tool_input.
|
|
@@ -31,20 +33,175 @@ if (!agentName) {
|
|
|
31
33
|
agentName = descMatch ? descMatch[1].toLowerCase() : 'subagent';
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} catch { /* no workflow */ }
|
|
36
|
+
// Extract success/failure indicators from result
|
|
37
|
+
const resultText = toolResult?.result || '';
|
|
38
|
+
const successIndicators = ['success', 'completed', 'finished', 'done', '✅', '✔'];
|
|
39
|
+
const errorIndicators = ['error', 'failed', 'failure', '❌', '✗', 'exception', 'timed out'];
|
|
40
|
+
const warningIndicators = ['warning', 'warn', '⚠', 'note:', 'attention'];
|
|
40
41
|
|
|
42
|
+
let outcome = 'unknown';
|
|
43
|
+
let success = null;
|
|
44
|
+
|
|
45
|
+
const lowerResult = resultText.toLowerCase();
|
|
46
|
+
if (errorIndicators.some(ind => lowerResult.includes(ind))) {
|
|
47
|
+
outcome = 'error';
|
|
48
|
+
success = false;
|
|
49
|
+
} else if (warningIndicators.some(ind => lowerResult.includes(ind))) {
|
|
50
|
+
outcome = 'warning';
|
|
51
|
+
success = null;
|
|
52
|
+
} else if (successIndicators.some(ind => lowerResult.includes(ind))) {
|
|
53
|
+
outcome = 'success';
|
|
54
|
+
success = true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Record agent invocation
|
|
41
58
|
try {
|
|
42
59
|
const home = homedir();
|
|
43
|
-
|
|
60
|
+
const cxDir = join(home, '.cx');
|
|
61
|
+
mkdirSync(cxDir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
// Update last-agent file for coordination
|
|
44
64
|
writeFileSync(
|
|
45
|
-
join(
|
|
46
|
-
JSON.stringify({
|
|
65
|
+
join(cxDir, 'last-agent.json'),
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
agent: agentName,
|
|
68
|
+
coordination: 'tracker-plus-plan',
|
|
69
|
+
ts: new Date().toISOString(),
|
|
70
|
+
outcome,
|
|
71
|
+
description: description.slice(0, 200)
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Append to agent log for telemetry
|
|
76
|
+
const agentLogFile = join(cxDir, 'agent-log.jsonl');
|
|
77
|
+
const logEntry = {
|
|
78
|
+
timestamp: new Date().toISOString(),
|
|
79
|
+
agent: agentName,
|
|
80
|
+
outcome,
|
|
81
|
+
success,
|
|
82
|
+
description: description.slice(0, 500),
|
|
83
|
+
resultLength: resultText.length,
|
|
84
|
+
toolInputKeys: Object.keys(toolInput).filter(k => !k.includes('secret') && !k.includes('password') && !k.includes('token'))
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
appendFileSync(agentLogFile, JSON.stringify(logEntry) + '\n');
|
|
88
|
+
|
|
89
|
+
// Record observation for pattern learning (only if we have meaningful outcome)
|
|
90
|
+
if (outcome !== 'unknown' && description.length > 10) {
|
|
91
|
+
const observationsDir = join(cxDir, 'observations', 'agent-outcomes');
|
|
92
|
+
mkdirSync(observationsDir, { recursive: true });
|
|
93
|
+
|
|
94
|
+
const observationFile = join(observationsDir, `${Date.now()}-${agentName}.json`);
|
|
95
|
+
|
|
96
|
+
// Categorize as pattern or anti-pattern based on outcome
|
|
97
|
+
const category = success === true ? 'pattern' :
|
|
98
|
+
success === false ? 'anti-pattern' : 'observation';
|
|
99
|
+
|
|
100
|
+
const summary = success === true
|
|
101
|
+
? `${agentName} successfully completed: ${description.slice(0, 100)}`
|
|
102
|
+
: success === false
|
|
103
|
+
? `${agentName} failed: ${description.slice(0, 100)}`
|
|
104
|
+
: `${agentName} executed with warnings: ${description.slice(0, 100)}`;
|
|
105
|
+
|
|
106
|
+
const observation = {
|
|
107
|
+
role: 'agent-tracker',
|
|
108
|
+
category,
|
|
109
|
+
summary,
|
|
110
|
+
content: `${agentName} was invoked with description: "${description.slice(0, 500)}"\n\nOutcome: ${outcome}\nResult length: ${resultText.length} chars`,
|
|
111
|
+
tags: ['agent-invocation', agentName, outcome],
|
|
112
|
+
confidence: 0.8,
|
|
113
|
+
timestamp: new Date().toISOString()
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
writeFileSync(observationFile, JSON.stringify(observation, null, 2) + '\n');
|
|
117
|
+
|
|
118
|
+
// Also record in vector-ready format for learning system
|
|
119
|
+
if (success !== null) {
|
|
120
|
+
const learningDir = join(cxDir, 'learning', agentName);
|
|
121
|
+
mkdirSync(learningDir, { recursive: true });
|
|
122
|
+
|
|
123
|
+
const learningFile = join(learningDir, `${Date.now()}-${success ? 'success' : 'failure'}.json`);
|
|
124
|
+
const learningEntry = {
|
|
125
|
+
agent: agentName,
|
|
126
|
+
success,
|
|
127
|
+
description,
|
|
128
|
+
outcome,
|
|
129
|
+
timestamp: new Date().toISOString(),
|
|
130
|
+
// Extract potential patterns from description
|
|
131
|
+
keywords: extractKeywords(description),
|
|
132
|
+
taskType: classifyTaskType(description)
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
writeFileSync(learningFile, JSON.stringify(learningEntry, null, 2) + '\n');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// Non-critical - best effort tracking
|
|
141
|
+
console.error(`Agent tracker error: ${error.message}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Auto-enqueue handoff when a completing persona's result references
|
|
145
|
+
// `next:cx-<role>` and the target persona is onboarded in the role framework.
|
|
146
|
+
// The next session-start will pick up the queued entry and dispatch via Task.
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
if (process.env.CONSTRUCT_ROLES !== 'off' && agentName && /^cx-/.test(agentName)) {
|
|
150
|
+
const matches = [...resultText.matchAll(/\bnext:cx-([a-z][a-z-]+)\b/g)].map((m) => m[1]);
|
|
151
|
+
const unique = [...new Set(matches)];
|
|
152
|
+
if (unique.length > 0) {
|
|
153
|
+
const { isOnboarded, loadManifest } = await import('../roles/manifest.mjs');
|
|
154
|
+
const pendingPath = join(homedir(), '.cx', 'role-pending.jsonl');
|
|
155
|
+
const bdMatch = /\b(construct-[a-z0-9]+)\b/i.exec(resultText) || [];
|
|
156
|
+
const bdIssueId = bdMatch[1] || null;
|
|
157
|
+
for (const targetId of unique) {
|
|
158
|
+
if (!isOnboarded(targetId)) continue;
|
|
159
|
+
const manifest = loadManifest(targetId);
|
|
160
|
+
const entry = {
|
|
161
|
+
ts: Date.now(),
|
|
162
|
+
personaId: targetId,
|
|
163
|
+
cxId: `cx-${targetId}`,
|
|
164
|
+
bdIssueId,
|
|
165
|
+
fingerprint: `handoff-${agentName}-${targetId}-${Date.now().toString(36)}`,
|
|
166
|
+
eventType: 'handoff.received',
|
|
167
|
+
summary: `Handoff from ${agentName}${bdIssueId ? ` (re ${bdIssueId})` : ''}`,
|
|
168
|
+
killSwitchEnv: manifest?.killSwitchEnv || '',
|
|
169
|
+
handoffFrom: agentName,
|
|
170
|
+
};
|
|
171
|
+
appendFileSync(pendingPath, JSON.stringify(entry) + '\n');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch { /* best effort — handoff enqueue never blocks */ }
|
|
176
|
+
|
|
177
|
+
// Helper functions for learning system
|
|
178
|
+
function extractKeywords(text) {
|
|
179
|
+
const commonStop = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by'];
|
|
180
|
+
const words = text.toLowerCase().split(/[\s\.,;!?]+/).filter(word =>
|
|
181
|
+
word.length > 2 && !commonStop.includes(word) && /^[a-z]+$/.test(word)
|
|
47
182
|
);
|
|
48
|
-
|
|
183
|
+
|
|
184
|
+
// Count frequency
|
|
185
|
+
const freq = {};
|
|
186
|
+
words.forEach(word => freq[word] = (freq[word] || 0) + 1);
|
|
187
|
+
|
|
188
|
+
// Return top 5 keywords
|
|
189
|
+
return Object.entries(freq)
|
|
190
|
+
.sort((a, b) => b[1] - a[1])
|
|
191
|
+
.slice(0, 5)
|
|
192
|
+
.map(([word]) => word);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function classifyTaskType(description) {
|
|
196
|
+
const lower = description.toLowerCase();
|
|
197
|
+
if (lower.includes('fix') || lower.includes('bug') || lower.includes('error')) return 'bug-fix';
|
|
198
|
+
if (lower.includes('implement') || lower.includes('create') || lower.includes('add')) return 'implementation';
|
|
199
|
+
if (lower.includes('refactor') || lower.includes('improve') || lower.includes('optimize')) return 'refactoring';
|
|
200
|
+
if (lower.includes('review') || lower.includes('audit') || lower.includes('check')) return 'review';
|
|
201
|
+
if (lower.includes('test') || lower.includes('verify') || lower.includes('validate')) return 'testing';
|
|
202
|
+
if (lower.includes('document') || lower.includes('write') || lower.includes('readme')) return 'documentation';
|
|
203
|
+
if (lower.includes('research') || lower.includes('analyze') || lower.includes('investigate')) return 'research';
|
|
204
|
+
return 'other';
|
|
205
|
+
}
|
|
49
206
|
|
|
50
207
|
process.exit(0);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/audit-reads.mjs — opt-in tamper-evident log of every Read tool call.
|
|
4
|
+
*
|
|
5
|
+
* Symmetric counterpart to audit-trail.mjs (which records mutations). Reads
|
|
6
|
+
* are off by default because they happen much more often than writes; enable
|
|
7
|
+
* with `CONSTRUCT_AUDIT_READS=1`. When enabled, every Read tool call writes
|
|
8
|
+
* a single JSONL line to ~/.cx/audit-reads.jsonl with:
|
|
9
|
+
*
|
|
10
|
+
* - timestamp ISO
|
|
11
|
+
* - session_id (when supplied by the harness)
|
|
12
|
+
* - tool_name (always Read)
|
|
13
|
+
* - agent (from ~/.cx/last-agent.json — falls back to "construct")
|
|
14
|
+
* - cwd
|
|
15
|
+
* - target (the file_path read)
|
|
16
|
+
* - bytes (size of the file at read time)
|
|
17
|
+
* - content_hash (sha256 of the file's first 64KiB at read time, truncated)
|
|
18
|
+
* - prev_line_hash (chained tamper-evidence — hash of previous JSONL line)
|
|
19
|
+
*
|
|
20
|
+
* The prev_line_hash chain means any after-the-fact reordering or deletion
|
|
21
|
+
* breaks the chain and is detectable by a simple replay (same approach as
|
|
22
|
+
* the mutation audit).
|
|
23
|
+
*
|
|
24
|
+
* @p95ms 8
|
|
25
|
+
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
26
|
+
*/
|
|
27
|
+
import { readFileSync, appendFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
|
|
28
|
+
import { createHash } from 'node:crypto';
|
|
29
|
+
import { join, resolve } from 'node:path';
|
|
30
|
+
import { homedir } from 'node:os';
|
|
31
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
32
|
+
|
|
33
|
+
const CX_DIR = join(homedir(), '.cx');
|
|
34
|
+
const AUDIT_FILE = join(CX_DIR, 'audit-reads.jsonl');
|
|
35
|
+
const LAST_AGENT = join(CX_DIR, 'last-agent.json');
|
|
36
|
+
const HASH_PREFIX_BYTES = 64 * 1024;
|
|
37
|
+
|
|
38
|
+
if (process.env.CONSTRUCT_AUDIT_READS !== '1') process.exit(0);
|
|
39
|
+
|
|
40
|
+
function sha256(input) { return createHash('sha256').update(input).digest('hex'); }
|
|
41
|
+
|
|
42
|
+
function readLastAgent() {
|
|
43
|
+
try {
|
|
44
|
+
const data = JSON.parse(readFileSync(LAST_AGENT, 'utf8'));
|
|
45
|
+
return data?.agent || data?.name || 'construct';
|
|
46
|
+
} catch { return 'construct'; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readPrevLineHash() {
|
|
50
|
+
try {
|
|
51
|
+
if (!existsSync(AUDIT_FILE)) return null;
|
|
52
|
+
const size = statSync(AUDIT_FILE).size;
|
|
53
|
+
if (size === 0) return null;
|
|
54
|
+
const readFrom = Math.max(0, size - 2048);
|
|
55
|
+
const tail = readFileSync(AUDIT_FILE, 'utf8').slice(readFrom);
|
|
56
|
+
const lines = tail.split('\n').filter(Boolean);
|
|
57
|
+
if (lines.length === 0) return null;
|
|
58
|
+
return sha256(lines[lines.length - 1]);
|
|
59
|
+
} catch { return null; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let input = {};
|
|
63
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); }
|
|
64
|
+
catch (err) { logHookFailure({ hook: 'audit-reads', err, phase: 'parse' }); process.exit(0); }
|
|
65
|
+
|
|
66
|
+
if ((input?.tool_name || '') !== 'Read') process.exit(0);
|
|
67
|
+
|
|
68
|
+
const cwd = input?.cwd || process.cwd();
|
|
69
|
+
const filePath = input?.tool_input?.file_path;
|
|
70
|
+
if (!filePath) process.exit(0);
|
|
71
|
+
|
|
72
|
+
const absPath = filePath.startsWith('/') ? filePath : resolve(cwd, filePath);
|
|
73
|
+
|
|
74
|
+
let bytes = null;
|
|
75
|
+
let contentHash = null;
|
|
76
|
+
try {
|
|
77
|
+
if (existsSync(absPath)) {
|
|
78
|
+
bytes = statSync(absPath).size;
|
|
79
|
+
const buf = Buffer.alloc(Math.min(bytes, HASH_PREFIX_BYTES));
|
|
80
|
+
const fd = await import('node:fs').then((m) => m.openSync(absPath, 'r'));
|
|
81
|
+
const fsModule = await import('node:fs');
|
|
82
|
+
fsModule.readSync(fd, buf, 0, buf.length, 0);
|
|
83
|
+
fsModule.closeSync(fd);
|
|
84
|
+
contentHash = sha256(buf).slice(0, 32);
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
logHookFailure({ hook: 'audit-reads', err, phase: 'hash', input: { filePath: absPath } });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const record = {
|
|
91
|
+
ts: new Date().toISOString(),
|
|
92
|
+
session_id: input?.session_id || null,
|
|
93
|
+
tool: 'Read',
|
|
94
|
+
agent: readLastAgent(),
|
|
95
|
+
cwd,
|
|
96
|
+
target: filePath,
|
|
97
|
+
bytes,
|
|
98
|
+
content_hash: contentHash,
|
|
99
|
+
prev_line_hash: readPrevLineHash(),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
mkdirSync(CX_DIR, { recursive: true });
|
|
104
|
+
appendFileSync(AUDIT_FILE, JSON.stringify(record) + '\n', 'utf8');
|
|
105
|
+
} catch (err) {
|
|
106
|
+
logHookFailure({ hook: 'audit-reads', err, phase: 'append' });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
process.exit(0);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/audit-trail.mjs — append-only audit log of every mutation Construct
|
|
4
|
+
* (or any subagent it dispatches) makes.
|
|
5
|
+
*
|
|
6
|
+
* Runs as PostToolUse on Edit | Write | MultiEdit | NotebookEdit | Bash.
|
|
7
|
+
* For Bash, only records commands that mutate state (git/npm/docker/cargo etc.
|
|
8
|
+
* write operations, rm, mv, chmod). Read-only commands are skipped.
|
|
9
|
+
*
|
|
10
|
+
* Each record is a single JSONL line written to ~/.cx/audit-trail.jsonl with:
|
|
11
|
+
* - timestamp ISO
|
|
12
|
+
* - session_id (from hook input when available)
|
|
13
|
+
* - tool_name
|
|
14
|
+
* - agent (from ~/.cx/last-agent.json, falls back to "construct")
|
|
15
|
+
* - task metadata (from the last-agent hint when available)
|
|
16
|
+
* - cwd
|
|
17
|
+
* - target (file_path for edits/writes, command for Bash)
|
|
18
|
+
* - detail (old_string prefix + new_string prefix for Edit; truncated diff
|
|
19
|
+
* for Write; command summary for Bash)
|
|
20
|
+
* - content_hash (sha256 of post-change file for edits, null for Bash)
|
|
21
|
+
* - prev_line_hash (chained tamper-evidence — hash of previous JSONL line)
|
|
22
|
+
*
|
|
23
|
+
* The prev_line_hash chain means any after-the-fact reordering or deletion
|
|
24
|
+
* breaks the chain and is detectable by a simple replay.
|
|
25
|
+
*
|
|
26
|
+
* @p95ms 15
|
|
27
|
+
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
28
|
+
*/
|
|
29
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
|
|
30
|
+
import { createHash } from 'node:crypto';
|
|
31
|
+
import { join, resolve } from 'node:path';
|
|
32
|
+
import { homedir } from 'node:os';
|
|
33
|
+
|
|
34
|
+
const CX_DIR = join(homedir(), '.cx');
|
|
35
|
+
const AUDIT_FILE = join(CX_DIR, 'audit-trail.jsonl');
|
|
36
|
+
const LAST_AGENT = join(CX_DIR, 'last-agent.json');
|
|
37
|
+
const MUTATING_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash']);
|
|
38
|
+
|
|
39
|
+
const BASH_MUTATING_PATTERNS = [
|
|
40
|
+
/^\s*git\s+(add|commit|push|rebase|merge|reset|checkout\s+-b|branch\s+-D|tag|cherry-pick|stash\s+(?:push|drop))/,
|
|
41
|
+
/^\s*(rm|mv|cp|chmod|chown|ln|mkdir|rmdir|touch|install)\b/,
|
|
42
|
+
/^\s*(npm|pnpm|yarn|bun|pip|pip3|poetry|cargo|go|brew)\s+(install|add|remove|update|upgrade|uninstall|publish|link)/,
|
|
43
|
+
/^\s*docker\s+(run|build|rm|rmi|push|tag|stop|kill|exec)/,
|
|
44
|
+
/^\s*kubectl\s+(apply|delete|create|replace|scale|rollout)/,
|
|
45
|
+
/^\s*terraform\s+(apply|destroy)/,
|
|
46
|
+
/^\s*psql\b|^\s*sqlite3\b|\bINSERT\s+INTO\b|\bUPDATE\s+\w+\s+SET\b|\bDELETE\s+FROM\b/i,
|
|
47
|
+
/^\s*construct\s+(sync|setup|mcp\s+(add|remove)|init|init-docs)/,
|
|
48
|
+
/>\s*[^|&]|>>\s*[^|&]/, // shell redirection that writes
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function isMutatingBash(command) {
|
|
52
|
+
if (!command) return false;
|
|
53
|
+
return BASH_MUTATING_PATTERNS.some((re) => re.test(command));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sha256(input) {
|
|
57
|
+
return createHash('sha256').update(input).digest('hex');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readLastAgent() {
|
|
61
|
+
try {
|
|
62
|
+
const raw = readFileSync(LAST_AGENT, 'utf8');
|
|
63
|
+
const data = JSON.parse(raw);
|
|
64
|
+
return data?.agent || data?.name || 'construct';
|
|
65
|
+
} catch {
|
|
66
|
+
return 'construct';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readPrevLineHash() {
|
|
71
|
+
try {
|
|
72
|
+
if (!existsSync(AUDIT_FILE)) return null;
|
|
73
|
+
const size = statSync(AUDIT_FILE).size;
|
|
74
|
+
if (size === 0) return null;
|
|
75
|
+
const readFrom = Math.max(0, size - 2048);
|
|
76
|
+
const fs = readFileSync(AUDIT_FILE, 'utf8').slice(readFrom);
|
|
77
|
+
const lines = fs.split('\n').filter(Boolean);
|
|
78
|
+
if (lines.length === 0) return null;
|
|
79
|
+
return sha256(lines[lines.length - 1]);
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function truncateSnippet(s, limit = 280) {
|
|
86
|
+
if (!s) return '';
|
|
87
|
+
const str = String(s);
|
|
88
|
+
return str.length <= limit ? str : str.slice(0, limit) + '…';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let input = {};
|
|
92
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
93
|
+
|
|
94
|
+
const toolName = input?.tool_name || '';
|
|
95
|
+
if (!MUTATING_TOOLS.has(toolName)) process.exit(0);
|
|
96
|
+
|
|
97
|
+
const cwd = input?.cwd || process.cwd();
|
|
98
|
+
const toolInput = input?.tool_input || {};
|
|
99
|
+
|
|
100
|
+
let target = null;
|
|
101
|
+
let detail = null;
|
|
102
|
+
let contentHash = null;
|
|
103
|
+
|
|
104
|
+
if (toolName === 'Bash') {
|
|
105
|
+
const command = String(toolInput?.command || '');
|
|
106
|
+
if (!isMutatingBash(command)) process.exit(0);
|
|
107
|
+
target = 'bash';
|
|
108
|
+
detail = truncateSnippet(command, 400);
|
|
109
|
+
} else if (toolName === 'Edit') {
|
|
110
|
+
target = toolInput.file_path || null;
|
|
111
|
+
detail = `-- ${truncateSnippet(toolInput.old_string, 140)}\n++ ${truncateSnippet(toolInput.new_string, 140)}`;
|
|
112
|
+
} else if (toolName === 'Write') {
|
|
113
|
+
target = toolInput.file_path || null;
|
|
114
|
+
detail = `WRITE ${String(toolInput.content || '').length} chars`;
|
|
115
|
+
} else if (toolName === 'MultiEdit') {
|
|
116
|
+
target = toolInput.file_path || null;
|
|
117
|
+
const editsCount = Array.isArray(toolInput.edits) ? toolInput.edits.length : 0;
|
|
118
|
+
detail = `MultiEdit ${editsCount} edits`;
|
|
119
|
+
} else if (toolName === 'NotebookEdit') {
|
|
120
|
+
target = toolInput.notebook_path || null;
|
|
121
|
+
detail = `NotebookEdit cell ${toolInput.cell_number ?? '?'}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!target) process.exit(0);
|
|
125
|
+
|
|
126
|
+
if (toolName !== 'Bash' && target) {
|
|
127
|
+
const absPath = target.startsWith('/') ? target : resolve(cwd, target);
|
|
128
|
+
try {
|
|
129
|
+
if (existsSync(absPath)) {
|
|
130
|
+
contentHash = sha256(readFileSync(absPath, 'utf8')).slice(0, 32);
|
|
131
|
+
}
|
|
132
|
+
} catch { /* best effort */ }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const record = {
|
|
136
|
+
ts: new Date().toISOString(),
|
|
137
|
+
session_id: input?.session_id || null,
|
|
138
|
+
tool: toolName,
|
|
139
|
+
agent: readLastAgent(),
|
|
140
|
+
task: null,
|
|
141
|
+
cwd,
|
|
142
|
+
target,
|
|
143
|
+
detail,
|
|
144
|
+
content_hash: contentHash,
|
|
145
|
+
prev_line_hash: readPrevLineHash(),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
mkdirSync(CX_DIR, { recursive: true });
|
|
150
|
+
appendFileSync(AUDIT_FILE, JSON.stringify(record) + '\n', 'utf8');
|
|
151
|
+
} catch { /* best effort — audit failure must not block the user's work */ }
|
|
152
|
+
|
|
153
|
+
process.exit(0);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/bash-output-logger.mjs — persists long Bash outputs to disk and nudges
|
|
4
|
+
* the model to reference the log instead of re-running the command.
|
|
5
|
+
*
|
|
6
|
+
* Runs as PostToolUse on Bash. If stdout exceeds a threshold, writes the full
|
|
7
|
+
* output to ~/.cx/bash-logs/ and emits a short stderr note that Claude sees in
|
|
8
|
+
* the next turn. The current turn's conversation still contains the full output
|
|
9
|
+
* (hooks cannot retroactively edit past tool outputs), but subsequent turns are
|
|
10
|
+
* steered toward disk-backed references instead of re-running the same command.
|
|
11
|
+
*
|
|
12
|
+
* Threshold chosen conservatively at 4000 chars (~100 lines). Below that, the
|
|
13
|
+
* hook is a no-op.
|
|
14
|
+
*
|
|
15
|
+
* @p95ms 20
|
|
16
|
+
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { homedir } from 'node:os';
|
|
21
|
+
|
|
22
|
+
const SIZE_THRESHOLD_CHARS = 4000;
|
|
23
|
+
const LOG_DIR = join(homedir(), '.cx', 'bash-logs');
|
|
24
|
+
const WARN_FLAGS = join(homedir(), '.cx', 'warn-flags.txt');
|
|
25
|
+
|
|
26
|
+
let input = {};
|
|
27
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
28
|
+
|
|
29
|
+
if ((input?.tool_name || '') !== 'Bash') process.exit(0);
|
|
30
|
+
|
|
31
|
+
const stdout = String(input?.tool_response?.stdout ?? '');
|
|
32
|
+
const stderr = String(input?.tool_response?.stderr ?? '');
|
|
33
|
+
const command = String(input?.tool_input?.command ?? '');
|
|
34
|
+
const totalSize = stdout.length + stderr.length;
|
|
35
|
+
|
|
36
|
+
if (totalSize < SIZE_THRESHOLD_CHARS) process.exit(0);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
40
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
41
|
+
const logPath = join(LOG_DIR, `bash-${ts}.log`);
|
|
42
|
+
const payload = [
|
|
43
|
+
`# Command`,
|
|
44
|
+
command,
|
|
45
|
+
``,
|
|
46
|
+
`# Stdout (${stdout.length} chars)`,
|
|
47
|
+
stdout,
|
|
48
|
+
``,
|
|
49
|
+
`# Stderr (${stderr.length} chars)`,
|
|
50
|
+
stderr,
|
|
51
|
+
].join('\n');
|
|
52
|
+
writeFileSync(logPath, payload, 'utf8');
|
|
53
|
+
|
|
54
|
+
const approxLines = stdout.split('\n').length;
|
|
55
|
+
const kb = Math.round(totalSize / 1024);
|
|
56
|
+
process.stderr.write(
|
|
57
|
+
`[bash-output-logger] Output was ${approxLines} lines (${kb} KB). ` +
|
|
58
|
+
`Full log saved to ${logPath}. ` +
|
|
59
|
+
`Before re-running this command, reference the log with: grep/sed/head on ${logPath}. ` +
|
|
60
|
+
`Prefer \`| head -N\` or \`| tail -N\` on future runs to keep context lean.\n`
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
appendFileSync(
|
|
65
|
+
WARN_FLAGS,
|
|
66
|
+
`Bash output ${kb} KB saved to ${logPath} — prefer grepping the log over re-running.\n`,
|
|
67
|
+
);
|
|
68
|
+
} catch { /* best effort */ }
|
|
69
|
+
} catch { /* best effort */ }
|
|
70
|
+
|
|
71
|
+
process.exit(0);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/block-no-verify.mjs — refuse `git commit/push/merge --no-verify`.
|
|
4
|
+
*
|
|
5
|
+
* Runs as PreToolUse on Bash. Pre-commit / pre-push hooks exist to keep red
|
|
6
|
+
* code from landing; --no-verify bypasses them entirely. If a hook is
|
|
7
|
+
* failing, fix the underlying issue.
|
|
8
|
+
*
|
|
9
|
+
* Vendored from block-no-verify@1.1.2 (Elastic-2.0) to drop the per-Bash-call
|
|
10
|
+
* npx cold-start tax and remove the third-party dependency for ~30 lines of
|
|
11
|
+
* logic that we already maintain comment + audit policy for.
|
|
12
|
+
*
|
|
13
|
+
* @p95ms 5
|
|
14
|
+
* @maxBlockingScope PreToolUse
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
|
|
18
|
+
let input = {};
|
|
19
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
20
|
+
|
|
21
|
+
const command = String(input?.tool_input?.command || '').trim();
|
|
22
|
+
if (!command) process.exit(0);
|
|
23
|
+
|
|
24
|
+
const isGitCommit = /^git\s+commit\b/.test(command);
|
|
25
|
+
const isGitPushMergeEtc = /^git\s+(push|merge|am|rebase|cherry-pick|notes|revert|tag)\b/.test(command);
|
|
26
|
+
|
|
27
|
+
if (!isGitCommit && !isGitPushMergeEtc) process.exit(0);
|
|
28
|
+
|
|
29
|
+
const hasLongFlag = /(?:^|\s)--no-verify(?:\s|=|$)/.test(command);
|
|
30
|
+
const hasShortN = isGitCommit && /(?:^|\s)-n(?:\s|$)/.test(command);
|
|
31
|
+
|
|
32
|
+
if (hasLongFlag || hasShortN) {
|
|
33
|
+
process.stderr.write(
|
|
34
|
+
`[block-no-verify] BLOCKED: --no-verify bypasses pre-commit / pre-push hooks.\n` +
|
|
35
|
+
`Command: ${command.slice(0, 200)}\n` +
|
|
36
|
+
`If a hook is failing, fix the underlying issue instead of skipping it.\n`,
|
|
37
|
+
);
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
process.exit(0);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/ci-status-check.mjs — UserPromptSubmit hook: inject remote CI status into agent context.
|
|
4
|
+
*
|
|
5
|
+
* Queries `gh run list --branch=<current> --limit=1` once per user prompt and,
|
|
6
|
+
* if the last run on the current branch failed, prints a stderr line that the
|
|
7
|
+
* agent will see in its next observation. Without this, an agent has no way
|
|
8
|
+
* to know remote CI is red without explicitly running gh — which means failed
|
|
9
|
+
* jobs go unnoticed until the user catches them. Non-blocking.
|
|
10
|
+
*
|
|
11
|
+
* Cached on disk for 60s so frequent prompts don't spam the gh API.
|
|
12
|
+
* Skipped silently if gh is not installed, not authenticated, or the dir is
|
|
13
|
+
* not a git repo.
|
|
14
|
+
*
|
|
15
|
+
* @p95ms 4000
|
|
16
|
+
* @maxBlockingScope none (UserPromptSubmit, non-blocking)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
|
|
20
|
+
import { execSync } from 'node:child_process';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { join, dirname } from 'node:path';
|
|
23
|
+
|
|
24
|
+
const CACHE_PATH = join(homedir(), '.cx', 'ci-status-cache.json');
|
|
25
|
+
const CACHE_TTL_MS = 60_000;
|
|
26
|
+
|
|
27
|
+
let cwd = process.cwd();
|
|
28
|
+
try {
|
|
29
|
+
const input = JSON.parse(readFileSync(0, 'utf8'));
|
|
30
|
+
if (input?.cwd) cwd = input.cwd;
|
|
31
|
+
} catch {}
|
|
32
|
+
|
|
33
|
+
function safeExec(cmd, opts = {}) {
|
|
34
|
+
try { return execSync(cmd, { cwd, stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000, ...opts }).toString().trim(); }
|
|
35
|
+
catch { return null; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const branch = safeExec('git branch --show-current');
|
|
39
|
+
if (!branch) process.exit(0);
|
|
40
|
+
|
|
41
|
+
const cacheKey = `${cwd}::${branch}`;
|
|
42
|
+
let cache = {};
|
|
43
|
+
try { cache = JSON.parse(readFileSync(CACHE_PATH, 'utf8')); } catch {}
|
|
44
|
+
const cached = cache[cacheKey];
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
|
|
47
|
+
let conclusion = null;
|
|
48
|
+
let runId = null;
|
|
49
|
+
let runUrl = null;
|
|
50
|
+
|
|
51
|
+
if (cached && (now - cached.fetchedAt) < CACHE_TTL_MS) {
|
|
52
|
+
conclusion = cached.conclusion;
|
|
53
|
+
runId = cached.runId;
|
|
54
|
+
runUrl = cached.runUrl;
|
|
55
|
+
} else {
|
|
56
|
+
const json = safeExec(
|
|
57
|
+
`gh run list --branch=${JSON.stringify(branch)} --limit=1 --json conclusion,databaseId,url`,
|
|
58
|
+
{ timeout: 4000 },
|
|
59
|
+
);
|
|
60
|
+
if (!json) process.exit(0);
|
|
61
|
+
try {
|
|
62
|
+
const [run] = JSON.parse(json);
|
|
63
|
+
if (run) {
|
|
64
|
+
conclusion = run.conclusion;
|
|
65
|
+
runId = run.databaseId;
|
|
66
|
+
runUrl = run.url;
|
|
67
|
+
cache[cacheKey] = { conclusion, runId, runUrl, fetchedAt: now };
|
|
68
|
+
try {
|
|
69
|
+
mkdirSync(dirname(CACHE_PATH), { recursive: true });
|
|
70
|
+
writeFileSync(CACHE_PATH, JSON.stringify(cache));
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (conclusion === 'failure') {
|
|
77
|
+
process.stderr.write(
|
|
78
|
+
`[ci] Last CI run on branch '${branch}' FAILED (run ${runId}).\n ${runUrl || ''}\n Investigate with: gh run view ${runId} --log-failed\n Do not stop the session until the failure is fixed or explicitly acknowledged (CONSTRUCT_STOP_OK_RED_CI=1).\n`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
process.exit(0);
|