@geraldmaron/construct 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +10 -7
- package/LICENSE +201 -21
- package/README.md +132 -257
- package/agents/contracts.json +387 -0
- package/agents/prompts/cx-accessibility.md +8 -0
- package/agents/prompts/cx-ai-engineer.md +92 -0
- package/agents/prompts/cx-architect.md +10 -2
- package/agents/prompts/cx-business-strategist.md +13 -0
- package/agents/prompts/cx-data-analyst.md +80 -0
- package/agents/prompts/cx-data-engineer.md +4 -0
- package/agents/prompts/cx-debugger.md +8 -0
- package/agents/prompts/cx-designer.md +19 -0
- package/agents/prompts/cx-devil-advocate.md +4 -0
- package/agents/prompts/cx-docs-keeper.md +128 -0
- package/agents/prompts/cx-engineer.md +13 -0
- package/agents/prompts/cx-evaluator.md +4 -0
- package/agents/prompts/cx-explorer.md +12 -0
- package/agents/prompts/cx-legal-compliance.md +13 -0
- package/agents/prompts/cx-operations.md +13 -0
- package/agents/prompts/cx-orchestrator.md +107 -4
- package/agents/prompts/cx-platform-engineer.md +75 -0
- package/agents/prompts/cx-product-manager.md +8 -0
- package/agents/prompts/cx-qa.md +100 -0
- package/agents/prompts/cx-rd-lead.md +13 -0
- package/agents/prompts/cx-release-manager.md +8 -0
- package/agents/prompts/cx-researcher.md +21 -1
- package/agents/prompts/cx-reviewer.md +8 -0
- package/agents/prompts/cx-security.md +104 -0
- package/agents/prompts/cx-sre.md +104 -0
- package/agents/prompts/cx-test-automation.md +4 -0
- package/agents/prompts/cx-trace-reviewer.md +7 -3
- package/agents/prompts/cx-ux-researcher.md +4 -0
- package/agents/registry.json +365 -90
- package/agents/role-manifests.json +217 -0
- package/bin/construct +3345 -141
- package/bin/construct-postinstall.mjs +87 -0
- package/commands/build/feature.md +6 -6
- package/commands/plan/feature.md +6 -5
- package/commands/plan/requirements.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/work/drive.md +3 -3
- package/commands/work/optimize-prompts.md +3 -3
- package/db/{migrations → schema}/001_init.sql +2 -0
- package/db/schema/002_pgvector.sql +182 -0
- package/db/schema/003_intake.sql +47 -0
- package/examples/README.md +85 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
- package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
- package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
- package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
- package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
- package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
- package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
- package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
- package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
- package/examples/personas/construct/bad/commit-without-approval.md +30 -0
- package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
- package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
- package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
- package/examples/provider-plugin/README.md +34 -0
- package/examples/provider-plugin/index.mjs +74 -0
- package/examples/provider-plugin/package.json +15 -0
- package/examples/seed-observations/README.md +38 -0
- package/examples/seed-observations/anti-patterns.md +44 -0
- package/examples/seed-observations/decisions.md +36 -0
- package/examples/seed-observations/patterns.md +42 -0
- package/lib/agent-contracts-enforce.mjs +158 -0
- package/lib/agent-contracts.mjs +231 -0
- package/lib/agents/postconditions.mjs +126 -0
- package/lib/agents/schema.mjs +124 -0
- package/lib/artifact-capture.mjs +183 -0
- package/lib/audit-trail.mjs +149 -0
- package/lib/auto-docs.mjs +245 -65
- package/lib/beads/auto-close.mjs +126 -0
- package/lib/beads/drift.mjs +171 -0
- package/lib/beads-automation.mjs +542 -0
- package/lib/beads-client.mjs +518 -0
- package/lib/beads-lock.mjs +377 -0
- package/lib/beads-optimistic.mjs +365 -0
- package/lib/bootstrap/built-ins.mjs +136 -0
- package/lib/bootstrap/lazy-install.mjs +161 -0
- package/lib/bootstrap/resources.mjs +120 -0
- package/lib/bootstrap.mjs +105 -0
- package/lib/cache-governor.js +213 -0
- package/lib/cache-strategy-anthropic.js +62 -0
- package/lib/cache-strategy-google.js +79 -0
- package/lib/cache-strategy-none.js +30 -0
- package/lib/cache-strategy-openai.js +48 -0
- package/lib/cache-strategy.js +91 -0
- package/lib/claude-allow.mjs +149 -0
- package/lib/cli-commands.mjs +413 -258
- package/lib/codex-config.mjs +3 -1
- package/lib/comment-lint.mjs +55 -6
- package/lib/completions.mjs +21 -6
- package/lib/config/alias.mjs +56 -0
- package/lib/config/project-config.mjs +335 -0
- package/lib/config/schema.mjs +159 -0
- package/lib/context-router.mjs +308 -0
- package/lib/cost-ledger.mjs +177 -0
- package/lib/cost.mjs +171 -10
- package/lib/dashboard-static.mjs +158 -0
- package/lib/deployment-mode.mjs +86 -0
- package/lib/deprecate.mjs +49 -0
- package/lib/dispatch-batch.js +183 -0
- package/lib/distill.mjs +21 -8
- package/lib/doc-stamp.mjs +164 -0
- package/lib/doc-verify.mjs +119 -0
- package/lib/docs-routing.mjs +89 -0
- package/lib/docs-verify.mjs +417 -0
- package/lib/doctor/audit.mjs +71 -0
- package/lib/doctor/cli.mjs +99 -0
- package/lib/doctor/escalate.mjs +29 -0
- package/lib/doctor/index.mjs +140 -0
- package/lib/doctor/report.mjs +170 -0
- package/lib/doctor/watchers/bd-watch.mjs +117 -0
- package/lib/doctor/watchers/cost.mjs +130 -0
- package/lib/doctor/watchers/disk.mjs +122 -0
- package/lib/doctor/watchers/handoffs.mjs +33 -0
- package/lib/doctor/watchers/process-pressure.mjs +60 -0
- package/lib/doctor/watchers/service-health.mjs +188 -0
- package/lib/document-extract.mjs +288 -0
- package/lib/document-ingest.mjs +230 -0
- package/lib/drop.mjs +282 -0
- package/lib/embed/approval-queue.mjs +176 -0
- package/lib/embed/artifact.mjs +349 -0
- package/lib/embed/authority-guard.mjs +155 -0
- package/lib/embed/cli.mjs +408 -0
- package/lib/embed/config.mjs +355 -0
- package/lib/embed/conflict-detection.mjs +264 -0
- package/lib/embed/customer-profiles.mjs +480 -0
- package/lib/embed/daemon.mjs +1309 -0
- package/lib/embed/demand-fetch.mjs +449 -0
- package/lib/embed/docs-lifecycle.mjs +349 -0
- package/lib/embed/inbox-live-watcher.mjs +119 -0
- package/lib/embed/inbox.mjs +343 -0
- package/lib/embed/intake-metrics.mjs +190 -0
- package/lib/embed/jobs/vector-sync.mjs +198 -0
- package/lib/embed/notifications.mjs +75 -0
- package/lib/embed/output.mjs +79 -0
- package/lib/embed/providers/github.mjs +295 -0
- package/lib/embed/providers/jira.mjs +192 -0
- package/lib/embed/providers/linear.mjs +186 -0
- package/lib/embed/providers/registry.mjs +115 -0
- package/lib/embed/providers/slack.mjs +203 -0
- package/lib/embed/recommendation-store.mjs +378 -0
- package/lib/embed/roadmap.mjs +374 -0
- package/lib/embed/role-framing.mjs +110 -0
- package/lib/embed/scheduler.mjs +99 -0
- package/lib/embed/semantic.mjs +325 -0
- package/lib/embed/snapshot.mjs +191 -0
- package/lib/embed/supervision.mjs +235 -0
- package/lib/embed/target-resolver.mjs +186 -0
- package/lib/embed/worker.mjs +62 -0
- package/lib/embed/workspaces.mjs +297 -0
- package/lib/engine/chunker-headings.mjs +110 -0
- package/lib/engine/compressor-heuristic.mjs +100 -0
- package/lib/engine/consolidate.mjs +287 -0
- package/lib/engine/contracts.mjs +126 -0
- package/lib/engine/defaults.mjs +129 -0
- package/lib/engine/eval-retrieval.mjs +148 -0
- package/lib/engine/fuser-rrf.mjs +62 -0
- package/lib/engine/index.mjs +37 -0
- package/lib/engine/registry.mjs +146 -0
- package/lib/engine/reranker-mmr.mjs +90 -0
- package/lib/engine/tokens.mjs +77 -0
- package/lib/entity-store.mjs +280 -0
- package/lib/env-config.mjs +69 -4
- package/lib/evals/retrieval-bench.mjs +159 -0
- package/lib/evaluator-optimizer.mjs +317 -0
- package/lib/features.mjs +159 -29
- package/lib/gates-audit.mjs +236 -0
- package/lib/git-hooks/prepare-commit-msg +58 -0
- package/lib/handoffs/cleanup.mjs +159 -0
- package/lib/handoffs/contract.mjs +162 -0
- package/lib/handoffs/inventory.mjs +120 -0
- package/lib/headhunt.mjs +28 -47
- package/lib/health-check.mjs +399 -0
- package/lib/hook-health.mjs +442 -0
- package/lib/hooks/_lib/log.mjs +82 -0
- package/lib/hooks/adaptive-lint.mjs +26 -2
- package/lib/hooks/agent-tracker.mjs +171 -14
- package/lib/hooks/audit-reads.mjs +109 -0
- package/lib/hooks/audit-trail.mjs +153 -0
- package/lib/hooks/bash-output-logger.mjs +71 -0
- package/lib/hooks/block-no-verify.mjs +41 -0
- package/lib/hooks/ci-status-check.mjs +82 -0
- package/lib/hooks/comment-lint.mjs +29 -7
- package/lib/hooks/config-protection.mjs +39 -18
- package/lib/hooks/context-watch.mjs +137 -0
- package/lib/hooks/context-window-recovery.mjs +4 -20
- package/lib/hooks/dep-audit.mjs +16 -0
- package/lib/hooks/doc-coupling-check.mjs +80 -0
- package/lib/hooks/edit-accumulator.mjs +3 -0
- package/lib/hooks/edit-error-recovery.mjs +3 -0
- package/lib/hooks/edit-guard.mjs +45 -4
- package/lib/hooks/env-check.mjs +3 -0
- package/lib/hooks/guard-bash.mjs +58 -1
- package/lib/hooks/mcp-audit.mjs +18 -20
- package/lib/hooks/mcp-health-check.mjs +36 -0
- package/lib/hooks/model-fallback.mjs +42 -56
- package/lib/hooks/policy-engine.mjs +209 -0
- package/lib/hooks/post-merge-docs-check.mjs +63 -0
- package/lib/hooks/pre-compact.mjs +4 -24
- package/lib/hooks/pre-push-gate.mjs +200 -37
- package/lib/hooks/proactive-activation.mjs +284 -0
- package/lib/hooks/read-tracker.mjs +27 -4
- package/lib/hooks/readme-age-check.mjs +77 -0
- package/lib/hooks/registry-sync.mjs +12 -5
- package/lib/hooks/scan-secrets.mjs +50 -7
- package/lib/hooks/session-optimize.mjs +311 -0
- package/lib/hooks/session-start.mjs +287 -28
- package/lib/hooks/stop-notify.mjs +236 -96
- package/lib/hooks/stop-typecheck.mjs +13 -1
- package/lib/hooks/test-watch.mjs +68 -0
- package/lib/host-capabilities.mjs +31 -10
- package/lib/init-docs.mjs +731 -291
- package/lib/init-unified.mjs +1116 -0
- package/lib/init-update.mjs +168 -0
- package/lib/init.mjs +107 -0
- package/lib/install/first-invocation.mjs +119 -0
- package/lib/install/stage-project.mjs +69 -0
- package/lib/intake/classify.mjs +278 -0
- package/lib/intake/feedback.mjs +273 -0
- package/lib/intake/filesystem-queue.mjs +158 -0
- package/lib/intake/intake-config.mjs +132 -0
- package/lib/intake/postgres-queue.mjs +198 -0
- package/lib/intake/prepare.mjs +139 -0
- package/lib/intake/queue.mjs +83 -0
- package/lib/intake/session-prelude.mjs +50 -0
- package/lib/integrations/intake-integrations.mjs +740 -0
- package/lib/intent-classifier.mjs +253 -0
- package/lib/knowledge/layout.mjs +72 -0
- package/lib/knowledge/rag.mjs +331 -0
- package/lib/knowledge/search.mjs +315 -0
- package/lib/knowledge/trends.mjs +261 -0
- package/lib/logger.mjs +85 -0
- package/lib/mcp/broker.mjs +124 -0
- package/lib/mcp/server.mjs +554 -854
- package/lib/mcp/tools/document.mjs +132 -0
- package/lib/mcp/tools/memory.mjs +209 -0
- package/lib/mcp/tools/project.mjs +349 -0
- package/lib/mcp/tools/skills.mjs +409 -0
- package/lib/mcp/tools/storage.mjs +60 -0
- package/lib/mcp/tools/telemetry.mjs +315 -0
- package/lib/mcp/tools/workflow.mjs +112 -0
- package/lib/mcp-catalog.json +54 -3
- package/lib/mcp-manager.mjs +96 -48
- package/lib/mcp-platform-config.mjs +22 -22
- package/lib/memory-stats.mjs +121 -0
- package/lib/mode-commands.mjs +124 -0
- package/lib/model-free-selector.mjs +184 -0
- package/lib/model-pricing.mjs +152 -0
- package/lib/model-registry.mjs +226 -0
- package/lib/model-router.mjs +362 -386
- package/lib/observation-store.mjs +391 -0
- package/lib/ollama-manager.mjs +428 -0
- package/lib/opencode-config.mjs +13 -3
- package/lib/opencode-runtime-plugin.mjs +70 -38
- package/lib/opencode-telemetry.mjs +64 -6
- package/lib/orchestration-policy.mjs +509 -6
- package/lib/overrides/resolver.mjs +207 -0
- package/lib/parity.mjs +147 -0
- package/lib/paths.mjs +33 -0
- package/lib/performance/generate.mjs +212 -0
- package/lib/plugin-registry.mjs +268 -0
- package/lib/policy/engine.mjs +130 -0
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-detection.mjs +129 -0
- package/lib/project-init-shared.mjs +272 -0
- package/lib/project-profile.mjs +447 -0
- package/lib/prompt-composer.js +434 -0
- package/lib/prompt-metadata.mjs +1 -1
- package/lib/provider-capabilities-anthropic.js +44 -0
- package/lib/provider-capabilities-deepseek.js +37 -0
- package/lib/provider-capabilities-generic.js +26 -0
- package/lib/provider-capabilities-google.js +47 -0
- package/lib/provider-capabilities-openai.js +45 -0
- package/lib/provider-capabilities.js +142 -0
- package/lib/providers/atlassian-confluence/index.mjs +103 -0
- package/lib/providers/atlassian-jira/index.mjs +100 -0
- package/lib/providers/auth-manager.mjs +126 -0
- package/lib/providers/circuit-breaker.mjs +124 -0
- package/lib/providers/contract.mjs +93 -0
- package/lib/providers/github/index.mjs +126 -0
- package/lib/providers/registry.mjs +184 -0
- package/lib/providers/salesforce/index.mjs +100 -0
- package/lib/providers/slack/index.mjs +80 -0
- package/lib/reflect.mjs +137 -0
- package/lib/research-lint.mjs +164 -0
- package/lib/resources/budget.mjs +259 -0
- package/lib/role-preload.mjs +21 -6
- package/lib/roles/approval-surface.mjs +54 -0
- package/lib/roles/cli.mjs +118 -0
- package/lib/roles/event-bus.mjs +79 -0
- package/lib/roles/fence.mjs +84 -0
- package/lib/roles/gateway.mjs +260 -0
- package/lib/roles/hook-emit.mjs +37 -0
- package/lib/roles/manifest.mjs +48 -0
- package/lib/roles/router.mjs +27 -0
- package/lib/runtime-pressure.mjs +360 -0
- package/lib/schema-artifact.mjs +134 -0
- package/lib/schema-infer.mjs +551 -0
- package/lib/server/auth.mjs +168 -0
- package/lib/server/chat.mjs +336 -0
- package/lib/server/cors.mjs +77 -0
- package/lib/server/csrf.mjs +91 -0
- package/lib/server/index.mjs +1927 -78
- package/lib/server/insights.mjs +765 -0
- package/lib/server/rate-limit.mjs +91 -0
- package/lib/server/static/assets/index-ab25c707.js +70 -0
- package/lib/server/static/assets/index-f0c80a2b.css +1 -0
- package/lib/server/static/index.html +12 -817
- package/lib/server/telemetry-login.mjs +108 -0
- package/lib/server/webhook.mjs +510 -0
- package/lib/service-manager.mjs +522 -58
- package/lib/services/pattern-promotion-service.mjs +167 -0
- package/lib/services/telemetry-backend.mjs +178 -0
- package/lib/session-store.mjs +374 -0
- package/lib/setup-prompts.mjs +96 -0
- package/lib/setup.mjs +525 -38
- package/lib/skills-apply.mjs +280 -0
- package/lib/skills-scope.mjs +118 -0
- package/lib/status.mjs +261 -70
- package/lib/storage/admin.mjs +355 -0
- package/lib/storage/backend.mjs +2 -1
- package/lib/storage/backup.mjs +347 -0
- package/lib/storage/embeddings-engine.mjs +133 -0
- package/lib/storage/embeddings-legacy.mjs +85 -0
- package/lib/storage/embeddings-local.mjs +108 -0
- package/lib/storage/embeddings-ollama.mjs +78 -0
- package/lib/storage/embeddings-openai.mjs +85 -0
- package/lib/storage/embeddings.mjs +92 -33
- package/lib/storage/file-lock.mjs +130 -0
- package/lib/storage/fusion.mjs +95 -0
- package/lib/storage/hybrid-query.mjs +34 -27
- package/lib/storage/migrations.mjs +187 -0
- package/lib/storage/postgres-backup.mjs +124 -0
- package/lib/storage/sql-store.mjs +5 -15
- package/lib/storage/state-source.mjs +12 -13
- package/lib/storage/store-version.mjs +115 -0
- package/lib/storage/sync.mjs +144 -35
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/storage/vector-client.mjs +286 -0
- package/lib/storage/vector-store.mjs +71 -30
- package/lib/task-graph/generate.mjs +135 -0
- package/lib/task-graph/schema.mjs +81 -0
- package/lib/task-graph/store.mjs +71 -0
- package/lib/telemetry/backends/local.mjs +62 -0
- package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
- package/lib/telemetry/backfill.mjs +180 -0
- package/lib/telemetry/eval-datasets.mjs +203 -0
- package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
- package/lib/telemetry/intent-verifications.mjs +86 -0
- package/lib/telemetry/llm-judge.mjs +350 -0
- package/lib/telemetry/model-pricing-catalog.mjs +557 -0
- package/lib/telemetry/setup.mjs +151 -0
- package/lib/telemetry/skill-calls.mjs +78 -0
- package/lib/telemetry/team-rollup.mjs +4 -4
- package/lib/token-engine.js +117 -0
- package/lib/token-estimator-anthropic.js +15 -0
- package/lib/token-estimator-deepseek.js +13 -0
- package/lib/token-estimator-default.js +13 -0
- package/lib/token-estimator-google.js +13 -0
- package/lib/token-estimator-openai.js +13 -0
- package/lib/toolkit-env.mjs +1 -1
- package/lib/tty-prompts.mjs +211 -0
- package/lib/uninstall/uninstall.mjs +423 -0
- package/lib/update.mjs +115 -0
- package/lib/upgrade.mjs +141 -0
- package/lib/validator.mjs +51 -2
- package/lib/validators/skills.mjs +142 -0
- package/lib/wireframe.mjs +422 -0
- package/lib/worker/entrypoint.mjs +241 -0
- package/lib/worker/evidence.mjs +107 -0
- package/lib/worker/run.mjs +154 -0
- package/lib/worker/trace.mjs +182 -0
- package/lib/workflow-state.mjs +14 -18
- package/package.json +21 -8
- package/personas/construct.md +53 -51
- package/platforms/claude/CLAUDE.md +1 -1
- package/platforms/claude/settings.template.json +115 -68
- package/platforms/opencode/config.template.json +3 -7
- package/rules/common/agents.md +11 -38
- package/rules/common/beads-hygiene.md +75 -0
- package/rules/common/code-review.md +11 -100
- package/rules/common/coding-style.md +5 -3
- package/rules/common/comments.md +30 -111
- package/rules/common/commit-approval.md +53 -0
- package/rules/common/cx-agent-routing.md +1 -0
- package/rules/common/development-workflow.md +23 -41
- package/rules/common/doc-ownership.md +54 -0
- package/rules/common/efficiency.md +57 -0
- package/rules/common/framing.md +76 -0
- package/rules/common/git-workflow.md +2 -4
- package/rules/common/patterns.md +3 -7
- package/rules/common/performance.md +15 -31
- package/rules/common/release-gates.md +69 -0
- package/rules/common/research.md +107 -0
- package/rules/common/security.md +6 -6
- package/rules/common/skill-composition.md +67 -0
- package/rules/common/testing.md +15 -27
- package/rules/golang/hooks.md +0 -4
- package/rules/policy/bootstrap.yaml +11 -0
- package/rules/policy/drive.yaml +8 -0
- package/rules/policy/task.yaml +9 -0
- package/rules/policy/workflow.yaml +9 -0
- package/rules/python/hooks.md +0 -4
- package/rules/swift/hooks.md +0 -4
- package/rules/typescript/hooks.md +0 -4
- package/rules/web/hooks.md +0 -2
- package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
- package/skills/ai/prompt-optimizer.md +7 -7
- package/skills/compliance/ai-disclosure.md +58 -0
- package/skills/compliance/data-privacy.md +46 -0
- package/skills/compliance/license-audit.md +40 -0
- package/skills/compliance/regulatory-review.md +61 -0
- package/skills/docs/document-ingest-workflow.md +52 -0
- package/skills/docs/evidence-ingest-workflow.md +9 -0
- package/skills/docs/init-docs.md +51 -18
- package/skills/docs/product-intelligence-workflow.md +12 -0
- package/skills/docs/product-signal-workflow.md +4 -0
- package/skills/docs/research-workflow.md +23 -8
- package/skills/docs/runbook-workflow.md +1 -1
- package/skills/operating/orchestration-reference.md +151 -0
- package/skills/roles/architect.md +5 -0
- package/skills/roles/engineer.md +32 -0
- package/skills/roles/operator.md +12 -0
- package/skills/roles/reviewer.md +23 -0
- package/skills/routing.md +1 -1
- package/templates/devcontainer/Dockerfile.devcontainer +38 -0
- package/templates/devcontainer/devcontainer.json +31 -0
- package/templates/distribution/bootstrap.ps1 +142 -0
- package/templates/distribution/bootstrap.sh +196 -0
- package/templates/distribution/run.mjs +187 -0
- package/templates/docs/adr.md +44 -8
- package/templates/docs/changelog-entry.md +43 -0
- package/templates/docs/construct_guide.md +149 -0
- package/templates/docs/evidence-brief.md +2 -2
- package/templates/docs/meta-prd.md +140 -19
- package/templates/docs/onboarding.md +57 -0
- package/templates/docs/prd.md +159 -15
- package/templates/docs/research-brief.md +4 -4
- package/templates/homebrew/construct.rb +67 -0
- package/langfuse/docker-compose.yml +0 -82
- package/lib/eval-harness.mjs +0 -59
- package/lib/hooks/bootstrap-guard.mjs +0 -90
- package/lib/hooks/console-warn.mjs +0 -43
- package/lib/hooks/continuation-enforcer.mjs +0 -72
- package/lib/hooks/drive-guard.mjs +0 -89
- package/lib/hooks/mcp-task-scope.mjs +0 -47
- package/lib/hooks/task-completed-guard.mjs +0 -43
- package/lib/hooks/teammate-idle-guard.mjs +0 -54
- package/lib/hooks/workflow-guard.mjs +0 -62
- package/lib/prompt-composer.mjs +0 -196
- package/lib/review.mjs +0 -429
- package/lib/server/static/app.js +0 -841
- package/lib/telemetry/langfuse-model-sync.mjs +0 -270
- package/rules/common/hooks.md +0 -35
- package/rules/zh/README.md +0 -113
- package/rules/zh/agents.md +0 -55
- package/rules/zh/code-review.md +0 -129
- package/rules/zh/coding-style.md +0 -53
- package/rules/zh/development-workflow.md +0 -49
- package/rules/zh/git-workflow.md +0 -29
- package/rules/zh/hooks.md +0 -35
- package/rules/zh/patterns.md +0 -36
- package/rules/zh/performance.md +0 -60
- package/rules/zh/security.md +0 -34
- package/rules/zh/testing.md +0 -34
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/hooks/health-monitor.mjs — Comprehensive hook health verification.
|
|
3
|
+
*
|
|
4
|
+
* Addresses silent hook failures by:
|
|
5
|
+
* 1. Runtime verification that hooks actually execute
|
|
6
|
+
* 2. Health check endpoints for each hook type
|
|
7
|
+
* 3. Automatic recovery and alerting
|
|
8
|
+
* 4. Integration with construct doctor for actionable diagnostics
|
|
9
|
+
*
|
|
10
|
+
* @p95ms 100
|
|
11
|
+
* @maxBlockingScope none
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
|
|
19
|
+
const CX_DIR = path.join(homedir(), '.cx');
|
|
20
|
+
const HOOK_HEALTH_DIR = path.join(CX_DIR, 'hook-health');
|
|
21
|
+
const HEALTH_CHECK_INTERVAL_MS = 60 * 1000; // 1 minute
|
|
22
|
+
|
|
23
|
+
// Ensure health tracking directory exists
|
|
24
|
+
try {
|
|
25
|
+
fs.mkdirSync(HOOK_HEALTH_DIR, { recursive: true });
|
|
26
|
+
} catch { /* ignore */ }
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Hook registration and tracking
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const REGISTERED_HOOKS = {
|
|
33
|
+
'session-start': {
|
|
34
|
+
name: 'Session Start',
|
|
35
|
+
description: 'Injects context at the beginning of each session',
|
|
36
|
+
critical: true, // Session is unusable without this
|
|
37
|
+
checkMethod: 'ping',
|
|
38
|
+
},
|
|
39
|
+
'stop': {
|
|
40
|
+
name: 'Session Stop',
|
|
41
|
+
description: 'Persists session summary and cost data',
|
|
42
|
+
critical: true, // Cost tracking breaks without this
|
|
43
|
+
checkMethod: 'file',
|
|
44
|
+
evidenceFile: path.join(CX_DIR, 'session-memory-stats.json'),
|
|
45
|
+
},
|
|
46
|
+
'audit-trail': {
|
|
47
|
+
name: 'Audit Trail',
|
|
48
|
+
description: 'Records all mutations for compliance',
|
|
49
|
+
critical: false,
|
|
50
|
+
checkMethod: 'file',
|
|
51
|
+
evidenceFile: path.join(CX_DIR, 'audit-trail.jsonl'),
|
|
52
|
+
},
|
|
53
|
+
'comment-lint': {
|
|
54
|
+
name: 'Comment Lint',
|
|
55
|
+
description: 'Enforces comment standards on edits',
|
|
56
|
+
critical: false,
|
|
57
|
+
checkMethod: 'ping',
|
|
58
|
+
},
|
|
59
|
+
'policy-engine': {
|
|
60
|
+
name: 'Policy Engine',
|
|
61
|
+
description: 'Enforces gates at session end',
|
|
62
|
+
critical: true,
|
|
63
|
+
checkMethod: 'ping',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Health state management
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
function getHealthFilePath(hookName) {
|
|
72
|
+
return path.join(HOOK_HEALTH_DIR, `${hookName}.json`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readHookHealth(hookName) {
|
|
76
|
+
try {
|
|
77
|
+
const file = getHealthFilePath(hookName);
|
|
78
|
+
if (fs.existsSync(file)) {
|
|
79
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
80
|
+
}
|
|
81
|
+
} catch { /* ignore */ }
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
status: 'unknown',
|
|
85
|
+
lastCheck: null,
|
|
86
|
+
lastSuccess: null,
|
|
87
|
+
consecutiveFailures: 0,
|
|
88
|
+
totalChecks: 0,
|
|
89
|
+
totalFailures: 0,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function writeHookHealth(hookName, health) {
|
|
94
|
+
try {
|
|
95
|
+
fs.writeFileSync(getHealthFilePath(hookName), JSON.stringify(health, null, 2));
|
|
96
|
+
} catch { /* ignore */ }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Health check implementations
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* "Ping" check: Actually executes the hook with test input
|
|
105
|
+
* and verifies it completes successfully.
|
|
106
|
+
*/
|
|
107
|
+
async function checkPingHook(hookName, hookPath) {
|
|
108
|
+
try {
|
|
109
|
+
// Create test input based on hook type
|
|
110
|
+
let testInput = {};
|
|
111
|
+
|
|
112
|
+
if (hookName === 'session-start') {
|
|
113
|
+
testInput = {
|
|
114
|
+
cwd: process.cwd(),
|
|
115
|
+
session_id: `health-check-${Date.now()}`,
|
|
116
|
+
platform: 'health-check',
|
|
117
|
+
};
|
|
118
|
+
} else if (hookName === 'audit-trail') {
|
|
119
|
+
testInput = {
|
|
120
|
+
tool_name: 'Write',
|
|
121
|
+
tool_input: { file_path: '/dev/null', content: 'health-check' },
|
|
122
|
+
cwd: process.cwd(),
|
|
123
|
+
session_id: `health-check-${Date.now()}`,
|
|
124
|
+
};
|
|
125
|
+
} else if (hookName === 'policy-engine') {
|
|
126
|
+
testInput = {
|
|
127
|
+
type: 'Stop',
|
|
128
|
+
cwd: process.cwd(),
|
|
129
|
+
session_id: `health-check-${Date.now()}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Execute hook with test input
|
|
134
|
+
const result = spawnSync(
|
|
135
|
+
process.execPath,
|
|
136
|
+
[hookPath],
|
|
137
|
+
{
|
|
138
|
+
input: JSON.stringify(testInput),
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
timeout: 5000,
|
|
141
|
+
env: process.env,
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
healthy: result.status === 0,
|
|
147
|
+
exitCode: result.status,
|
|
148
|
+
stderr: result.stderr?.slice(0, 500),
|
|
149
|
+
stdout: result.stdout?.slice(0, 200),
|
|
150
|
+
};
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return {
|
|
153
|
+
healthy: false,
|
|
154
|
+
error: error.message,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* "File" check: Verifies evidence file is being updated.
|
|
161
|
+
*/
|
|
162
|
+
async function checkFileHook(hookName, evidenceFile) {
|
|
163
|
+
try {
|
|
164
|
+
if (!fs.existsSync(evidenceFile)) {
|
|
165
|
+
return {
|
|
166
|
+
healthy: false,
|
|
167
|
+
error: `Evidence file not found: ${evidenceFile}`,
|
|
168
|
+
recommendation: 'Hook may not be running or file path is wrong',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const stats = fs.statSync(evidenceFile);
|
|
173
|
+
const ageMs = Date.now() - stats.mtimeMs;
|
|
174
|
+
const ageMinutes = Math.floor(ageMs / 60000);
|
|
175
|
+
|
|
176
|
+
// Consider unhealthy if no updates in 30 minutes for active sessions
|
|
177
|
+
const staleThreshold = 30 * 60 * 1000; // 30 minutes
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
healthy: ageMs < staleThreshold,
|
|
181
|
+
lastUpdate: stats.mtime.toISOString(),
|
|
182
|
+
ageMinutes,
|
|
183
|
+
stale: ageMs >= staleThreshold,
|
|
184
|
+
error: ageMs >= staleThreshold
|
|
185
|
+
? `No updates in ${ageMinutes} minutes`
|
|
186
|
+
: undefined,
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return {
|
|
190
|
+
healthy: false,
|
|
191
|
+
error: error.message,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Main health check orchestrator
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Run a comprehensive health check on all registered hooks.
|
|
202
|
+
*/
|
|
203
|
+
export async function checkAllHooksHealth(options = {}) {
|
|
204
|
+
const { verbose = false, fix = false } = options;
|
|
205
|
+
const results = [];
|
|
206
|
+
|
|
207
|
+
for (const [hookId, hookConfig] of Object.entries(REGISTERED_HOOKS)) {
|
|
208
|
+
const hookPath = findHookPath(hookId);
|
|
209
|
+
const previousHealth = readHookHealth(hookId);
|
|
210
|
+
|
|
211
|
+
const checkResult = await checkSingleHook(hookId, hookConfig, hookPath);
|
|
212
|
+
|
|
213
|
+
// Update health state
|
|
214
|
+
const newHealth = updateHealthState(previousHealth, checkResult);
|
|
215
|
+
writeHookHealth(hookId, newHealth);
|
|
216
|
+
|
|
217
|
+
results.push({
|
|
218
|
+
id: hookId,
|
|
219
|
+
name: hookConfig.name,
|
|
220
|
+
critical: hookConfig.critical,
|
|
221
|
+
...checkResult,
|
|
222
|
+
health: newHealth,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (verbose) {
|
|
226
|
+
const status = checkResult.healthy ? '✓' : (hookConfig.critical ? '✗' : '⚠');
|
|
227
|
+
console.error(`${status} ${hookConfig.name}: ${checkResult.healthy ? 'healthy' : (checkResult.error || 'unhealthy')}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Attempt auto-fix if requested and hook is unhealthy
|
|
231
|
+
if (fix && !checkResult.healthy) {
|
|
232
|
+
const fixResult = await attemptHookFix(hookId, hookPath, checkResult);
|
|
233
|
+
if (fixResult.fixed) {
|
|
234
|
+
checkResult.fixed = true;
|
|
235
|
+
checkResult.fixMethod = fixResult.method;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
healthy: results.every(r => r.healthy || !r.critical),
|
|
242
|
+
criticalHealthy: results.filter(r => r.critical).every(r => r.healthy),
|
|
243
|
+
results,
|
|
244
|
+
timestamp: new Date().toISOString(),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function checkSingleHook(hookId, config, hookPath) {
|
|
249
|
+
// First check: does the hook file exist?
|
|
250
|
+
if (!hookPath) {
|
|
251
|
+
return {
|
|
252
|
+
healthy: false,
|
|
253
|
+
error: `Hook file not found for ${hookId}`,
|
|
254
|
+
recommendation: `Run \`construct sync\` to regenerate hook configuration`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Run the appropriate check method
|
|
259
|
+
if (config.checkMethod === 'ping') {
|
|
260
|
+
return checkPingHook(hookId, hookPath);
|
|
261
|
+
} else if (config.checkMethod === 'file') {
|
|
262
|
+
return checkFileHook(hookId, config.evidenceFile);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return { healthy: false, error: 'Unknown check method' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function findHookPath(hookName) {
|
|
269
|
+
// Check standard locations
|
|
270
|
+
const possiblePaths = [
|
|
271
|
+
path.join(homedir(), '.construct', 'lib', 'hooks', `${hookName}.mjs`),
|
|
272
|
+
path.join(process.cwd(), '.construct', 'lib', 'hooks', `${hookName}.mjs`),
|
|
273
|
+
path.join(homedir(), '.npm-global', 'lib', 'node_modules', '@geraldmaron/construct', 'lib', 'hooks', `${hookName}.mjs`),
|
|
274
|
+
// Try to find from PATH
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
for (const p of possiblePaths) {
|
|
278
|
+
if (fs.existsSync(p)) return p;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Try to resolve from construct binary location
|
|
282
|
+
try {
|
|
283
|
+
const which = spawnSync('which', ['construct'], { encoding: 'utf8' });
|
|
284
|
+
if (which.status === 0) {
|
|
285
|
+
const constructPath = which.stdout.trim();
|
|
286
|
+
const constructDir = path.dirname(constructPath);
|
|
287
|
+
const fromBin = path.join(constructDir, '..', 'lib', 'node_modules', '@geraldmaron/construct', 'lib', 'hooks', `${hookName}.mjs`);
|
|
288
|
+
if (fs.existsSync(fromBin)) return fromBin;
|
|
289
|
+
}
|
|
290
|
+
} catch { /* ignore */ }
|
|
291
|
+
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function updateHealthState(previous, checkResult) {
|
|
296
|
+
const now = new Date().toISOString();
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
status: checkResult.healthy ? 'healthy' : 'unhealthy',
|
|
300
|
+
lastCheck: now,
|
|
301
|
+
lastSuccess: checkResult.healthy ? now : previous.lastSuccess,
|
|
302
|
+
consecutiveFailures: checkResult.healthy ? 0 : previous.consecutiveFailures + 1,
|
|
303
|
+
totalChecks: previous.totalChecks + 1,
|
|
304
|
+
totalFailures: checkResult.healthy ? previous.totalFailures : previous.totalFailures + 1,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// Auto-fix attempts
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
async function attemptHookFix(hookId, hookPath, checkResult) {
|
|
313
|
+
const fixes = [];
|
|
314
|
+
|
|
315
|
+
// Fix 1: Missing hook file - regenerate
|
|
316
|
+
if (!hookPath) {
|
|
317
|
+
try {
|
|
318
|
+
const result = spawnSync('construct', ['sync'], { encoding: 'utf8', timeout: 30000 });
|
|
319
|
+
if (result.status === 0) {
|
|
320
|
+
fixes.push('regenerated hooks via construct sync');
|
|
321
|
+
// Re-check if hook now exists
|
|
322
|
+
const newPath = findHookPath(hookId);
|
|
323
|
+
if (newPath) {
|
|
324
|
+
return { fixed: true, method: 'regenerated hooks' };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} catch { /* ignore */ }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Fix 2: Hook file exists but not executable - fix permissions
|
|
331
|
+
if (hookPath && checkResult.error?.includes('permission')) {
|
|
332
|
+
try {
|
|
333
|
+
fs.chmodSync(hookPath, 0o755);
|
|
334
|
+
fixes.push('fixed permissions');
|
|
335
|
+
return { fixed: true, method: 'fixed permissions' };
|
|
336
|
+
} catch { /* ignore */ }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Fix 3: Settings.json misconfiguration
|
|
340
|
+
if (checkResult.error?.includes('not configured')) {
|
|
341
|
+
try {
|
|
342
|
+
const result = spawnSync('construct', ['sync'], { encoding: 'utf8', timeout: 30000 });
|
|
343
|
+
if (result.status === 0) {
|
|
344
|
+
return { fixed: true, method: 'resynced settings' };
|
|
345
|
+
}
|
|
346
|
+
} catch { /* ignore */ }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return { fixed: false, attempted: fixes };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// Public API for integration with doctor and CLI
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get a quick health summary for construct doctor.
|
|
358
|
+
*/
|
|
359
|
+
export async function getHooksHealthSummary() {
|
|
360
|
+
const health = await checkAllHooksHealth({ verbose: false });
|
|
361
|
+
|
|
362
|
+
const criticalFailed = health.results.filter(r => r.critical && !r.healthy);
|
|
363
|
+
const nonCriticalFailed = health.results.filter(r => !r.critical && !r.healthy);
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
overall: health.healthy ? 'healthy' : (health.criticalHealthy ? 'degraded' : 'critical'),
|
|
367
|
+
criticalFailed: criticalFailed.map(r => r.id),
|
|
368
|
+
nonCriticalFailed: nonCriticalFailed.map(r => r.id),
|
|
369
|
+
details: health.results.map(r => ({
|
|
370
|
+
name: r.name,
|
|
371
|
+
status: r.healthy ? 'healthy' : 'unhealthy',
|
|
372
|
+
critical: r.critical,
|
|
373
|
+
error: r.error,
|
|
374
|
+
})),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Verify a specific hook is working.
|
|
380
|
+
*/
|
|
381
|
+
export async function verifyHook(hookName) {
|
|
382
|
+
const config = REGISTERED_HOOKS[hookName];
|
|
383
|
+
if (!config) {
|
|
384
|
+
return { exists: false, error: `Unknown hook: ${hookName}` };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const hookPath = findHookPath(hookName);
|
|
388
|
+
const result = await checkSingleHook(hookName, config, hookPath);
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
exists: !!hookPath,
|
|
392
|
+
path: hookPath,
|
|
393
|
+
...result,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Start continuous health monitoring (for daemon use).
|
|
399
|
+
*/
|
|
400
|
+
export function startHealthMonitoring(options = {}) {
|
|
401
|
+
const { intervalMs = HEALTH_CHECK_INTERVAL_MS, onUnhealthy } = options;
|
|
402
|
+
|
|
403
|
+
const interval = setInterval(async () => {
|
|
404
|
+
const health = await checkAllHooksHealth({ verbose: false });
|
|
405
|
+
|
|
406
|
+
if (!health.healthy && onUnhealthy) {
|
|
407
|
+
onUnhealthy(health);
|
|
408
|
+
}
|
|
409
|
+
}, intervalMs);
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
stop: () => clearInterval(interval),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Format health status for CLI output.
|
|
418
|
+
*/
|
|
419
|
+
export function formatHealthStatus(health) {
|
|
420
|
+
let output = '';
|
|
421
|
+
|
|
422
|
+
output += `Hook Health: ${health.overall.toUpperCase()}\n`;
|
|
423
|
+
output += '=' .repeat(50) + '\n\n';
|
|
424
|
+
|
|
425
|
+
for (const detail of health.details) {
|
|
426
|
+
const icon = detail.status === 'healthy' ? '✓' : (detail.critical ? '✗' : '⚠');
|
|
427
|
+
output += `${icon} ${detail.name}\n`;
|
|
428
|
+
if (detail.error) {
|
|
429
|
+
output += ` Error: ${detail.error}\n`;
|
|
430
|
+
}
|
|
431
|
+
output += '\n';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (health.criticalFailed.length > 0) {
|
|
435
|
+
output += `\nCRITICAL ISSUES:\n`;
|
|
436
|
+
output += `The following hooks are critical and not functioning:\n`;
|
|
437
|
+
output += health.criticalFailed.join(', ') + '\n';
|
|
438
|
+
output += `\nRecommendation: Run \`construct doctor hooks --fix\` to attempt repair\n`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return output;
|
|
442
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/hooks/_lib/log.mjs — Centralized hook failure telemetry.
|
|
3
|
+
*
|
|
4
|
+
* Hook scripts (lib/hooks/*.mjs) catch and swallow most stdin/parse errors so
|
|
5
|
+
* a regression in the hook harness or a malformed payload does not block the
|
|
6
|
+
* user. The trade-off is silent failure: until now, hook regressions were
|
|
7
|
+
* invisible. This helper writes a single JSONL entry per failure to
|
|
8
|
+
* ~/.cx/hook-failures.jsonl so `construct doctor` can surface the top
|
|
9
|
+
* failing hooks without changing the fail-open behavior of any individual
|
|
10
|
+
* hook.
|
|
11
|
+
*
|
|
12
|
+
* Usage from a hook:
|
|
13
|
+
* import { logHookFailure } from './_lib/log.mjs';
|
|
14
|
+
* try { ... } catch (err) { logHookFailure({ hook: 'session-start', err }); }
|
|
15
|
+
*
|
|
16
|
+
* Failure-of-the-failure is also tolerated — if writing the log itself fails,
|
|
17
|
+
* the helper swallows the error so the hook proceeds normally.
|
|
18
|
+
*
|
|
19
|
+
* The log file is rotated at LOG_MAX_BYTES to keep disk usage bounded.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { appendFileSync, statSync, renameSync, mkdirSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
|
|
26
|
+
const LOG_DIR = join(homedir(), '.cx');
|
|
27
|
+
const LOG_PATH = join(LOG_DIR, 'hook-failures.jsonl');
|
|
28
|
+
const LOG_ROTATED_PATH = join(LOG_DIR, 'hook-failures.1.jsonl');
|
|
29
|
+
const LOG_MAX_BYTES = 1_048_576; // 1 MiB
|
|
30
|
+
|
|
31
|
+
function rotateIfNeeded() {
|
|
32
|
+
try {
|
|
33
|
+
const size = statSync(LOG_PATH).size;
|
|
34
|
+
if (size >= LOG_MAX_BYTES) {
|
|
35
|
+
renameSync(LOG_PATH, LOG_ROTATED_PATH);
|
|
36
|
+
}
|
|
37
|
+
} catch { /* file does not exist yet, nothing to rotate */ }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Append a structured failure record. Never throws.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} args
|
|
44
|
+
* @param {string} args.hook — hook id, e.g. 'session-start' or 'pre-push-gate'
|
|
45
|
+
* @param {Error|string} args.err — caught error or message
|
|
46
|
+
* @param {object} [args.input] — relevant subset of the hook input (avoid large payloads)
|
|
47
|
+
* @param {string} [args.phase] — hook lifecycle phase: 'parse' | 'execute' | 'cleanup'
|
|
48
|
+
*/
|
|
49
|
+
export function logHookFailure({ hook, err, input, phase = 'execute' } = {}) {
|
|
50
|
+
try {
|
|
51
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
52
|
+
rotateIfNeeded();
|
|
53
|
+
const entry = {
|
|
54
|
+
ts: new Date().toISOString(),
|
|
55
|
+
hook: String(hook || 'unknown'),
|
|
56
|
+
phase,
|
|
57
|
+
message: err instanceof Error ? err.message : String(err),
|
|
58
|
+
stack: err instanceof Error ? err.stack?.split('\n').slice(0, 4).join('\n') : null,
|
|
59
|
+
input: input ? safeShallow(input) : null,
|
|
60
|
+
pid: process.pid,
|
|
61
|
+
};
|
|
62
|
+
appendFileSync(LOG_PATH, JSON.stringify(entry) + '\n');
|
|
63
|
+
} catch { /* hook telemetry is best-effort and must never block the hook itself */ }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function safeShallow(obj) {
|
|
67
|
+
try {
|
|
68
|
+
const out = {};
|
|
69
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
70
|
+
if (v == null || typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
71
|
+
out[k] = typeof v === 'string' && v.length > 200 ? v.slice(0, 200) + '…' : v;
|
|
72
|
+
} else if (Array.isArray(v)) {
|
|
73
|
+
out[k] = `[${v.length} items]`;
|
|
74
|
+
} else {
|
|
75
|
+
out[k] = '[object]';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* lib/hooks/adaptive-lint.mjs —
|
|
3
|
+
* lib/hooks/adaptive-lint.mjs — PostToolUse auto-formatter and debug-log detector.
|
|
4
4
|
*
|
|
5
|
-
* Runs
|
|
5
|
+
* Runs after Edit/Write. Attempts the best available formatter for the edited file
|
|
6
|
+
* (eslint --fix, prettier, ruff, gofmt, rustfmt). Also flags console.log/debug
|
|
7
|
+
* statements as lint warnings. Non-blocking — exits 0 regardless of outcome.
|
|
8
|
+
*
|
|
9
|
+
* @p95ms 800
|
|
10
|
+
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
6
11
|
*/
|
|
7
12
|
import { execSync } from 'child_process';
|
|
8
13
|
import { existsSync, readFileSync } from 'fs';
|
|
@@ -109,4 +114,23 @@ if (!acted && ext === '.rs') {
|
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
|
|
117
|
+
// Flag debug logging left in source — non-blocking, informational only.
|
|
118
|
+
if (['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'].includes(ext)) {
|
|
119
|
+
try {
|
|
120
|
+
const src = readFileSync(filePath, 'utf8');
|
|
121
|
+
const lines = src.split('\n');
|
|
122
|
+
const hits = lines
|
|
123
|
+
.map((line, i) => ({ line: line.trimStart(), n: i + 1 }))
|
|
124
|
+
.filter(({ line }) =>
|
|
125
|
+
/\bconsole\.(log|debug)\s*\(/.test(line) && !line.startsWith('//')
|
|
126
|
+
);
|
|
127
|
+
if (hits.length > 0) {
|
|
128
|
+
const locs = hits.map(({ n }) => ` line ${n}`).join('\n');
|
|
129
|
+
process.stderr.write(
|
|
130
|
+
`[adaptive-lint] console.log/debug in ${filePath}:\n${locs}\n`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
} catch { /* unreadable — skip */ }
|
|
134
|
+
}
|
|
135
|
+
|
|
112
136
|
process.exit(0);
|