@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
|
@@ -2,31 +2,121 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* lib/hooks/stop-notify.mjs — Stop notify hook — emits a session summary notification when Claude stops.
|
|
4
4
|
*
|
|
5
|
-
* Runs as a Stop hook. Summarizes the session work
|
|
5
|
+
* Runs as a Stop hook. Summarizes the session work and efficiency
|
|
6
|
+
* signals into a final message written to stdout for the user.
|
|
7
|
+
*
|
|
8
|
+
* Per-transcript checkpoints in ~/.cx/transcript-checkpoints.json track the last processed
|
|
9
|
+
* line so every assistant turn in a Stop cycle is priced, not just the final one.
|
|
10
|
+
* Cost is computed via estimateUsageCost using the model ID from each transcript entry.
|
|
11
|
+
*
|
|
12
|
+
* @p95ms 500
|
|
13
|
+
* @maxBlockingScope Stop
|
|
6
14
|
*/
|
|
7
|
-
import { readFileSync, writeFileSync, existsSync, appendFileSync,
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync, appendFileSync, mkdirSync } from 'fs';
|
|
8
16
|
import { execSync } from 'child_process';
|
|
9
17
|
import { homedir } from 'os';
|
|
10
|
-
import { join } from 'path';
|
|
11
|
-
import {
|
|
18
|
+
import { join, dirname } from 'path';
|
|
19
|
+
import { listSessions, loadSession, updateSession } from '../session-store.mjs';
|
|
20
|
+
import { captureSessionArtifacts } from '../artifact-capture.mjs';
|
|
21
|
+
import { appendSessionStats } from '../memory-stats.mjs';
|
|
22
|
+
import { estimateUsageCost } from '../telemetry/model-pricing-catalog.mjs';
|
|
12
23
|
|
|
13
|
-
function
|
|
24
|
+
function loadTranscriptCheckpoints(checkpointPath) {
|
|
14
25
|
try {
|
|
15
|
-
if (!
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
if (!existsSync(checkpointPath)) return {};
|
|
27
|
+
return JSON.parse(readFileSync(checkpointPath, 'utf8')) || {};
|
|
28
|
+
} catch {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function saveTranscriptCheckpoints(checkpointPath, data) {
|
|
34
|
+
try {
|
|
35
|
+
mkdirSync(dirname(checkpointPath), { recursive: true });
|
|
36
|
+
writeFileSync(checkpointPath, JSON.stringify(data));
|
|
37
|
+
} catch { /* non-critical */ }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function collectUnprocessedUsage(transcriptPath, checkpoints) {
|
|
41
|
+
const entries = [];
|
|
42
|
+
try {
|
|
43
|
+
if (!transcriptPath || !existsSync(transcriptPath)) return entries;
|
|
18
44
|
const content = readFileSync(transcriptPath, 'utf8');
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
45
|
+
const lines = content.split(/\r?\n/);
|
|
46
|
+
const state = checkpoints[transcriptPath] || { lastLine: 0 };
|
|
47
|
+
|
|
48
|
+
// Reset on truncation/rotation rather than skipping lines or double-counting.
|
|
49
|
+
const startLine = state.lastLine > lines.length ? 0 : state.lastLine;
|
|
50
|
+
|
|
51
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
52
|
+
const line = lines[i];
|
|
53
|
+
if (!line) continue;
|
|
54
|
+
let obj;
|
|
55
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
56
|
+
const usage = obj?.message?.usage || obj?.usage;
|
|
57
|
+
if (!usage) continue;
|
|
58
|
+
if (!usage.input_tokens && !usage.output_tokens && !usage.cache_read_input_tokens && !usage.cache_creation_input_tokens) continue;
|
|
59
|
+
const model =
|
|
60
|
+
obj?.message?.model ||
|
|
61
|
+
obj?.model ||
|
|
62
|
+
obj?.message?.modelID ||
|
|
63
|
+
obj?.modelID ||
|
|
64
|
+
null;
|
|
65
|
+
const ts = obj?.timestamp || obj?.message?.timestamp || null;
|
|
66
|
+
entries.push({ usage, model, ts });
|
|
27
67
|
}
|
|
28
|
-
|
|
29
|
-
|
|
68
|
+
checkpoints[transcriptPath] = { lastLine: lines.length };
|
|
69
|
+
} catch { /* non-critical */ }
|
|
70
|
+
return entries;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function priceUsage(model, usage) {
|
|
74
|
+
const inputTokens = Number(usage?.input_tokens || 0);
|
|
75
|
+
const outputTokens = Number(usage?.output_tokens || 0);
|
|
76
|
+
const reasoningTokens = Number(
|
|
77
|
+
usage?.reasoning_tokens ||
|
|
78
|
+
usage?.output_token_details?.reasoning ||
|
|
79
|
+
usage?.completion_tokens_details?.reasoning_tokens ||
|
|
80
|
+
0,
|
|
81
|
+
);
|
|
82
|
+
const cacheReadInputTokens = Number(usage?.cache_read_input_tokens || 0);
|
|
83
|
+
const cacheCreation5mInputTokens = Number(
|
|
84
|
+
usage?.cache_creation?.ephemeral_5m_input_tokens ||
|
|
85
|
+
usage?.cache_creation_5m_input_tokens ||
|
|
86
|
+
0,
|
|
87
|
+
);
|
|
88
|
+
const cacheCreation1hInputTokens = Number(
|
|
89
|
+
usage?.cache_creation?.ephemeral_1h_input_tokens ||
|
|
90
|
+
usage?.cache_creation_1h_input_tokens ||
|
|
91
|
+
0,
|
|
92
|
+
);
|
|
93
|
+
const explicitCacheCreationInputTokens = Number(usage?.cache_creation_input_tokens || 0);
|
|
94
|
+
const cacheCreationInputTokens =
|
|
95
|
+
explicitCacheCreationInputTokens ||
|
|
96
|
+
(cacheCreation5mInputTokens + cacheCreation1hInputTokens);
|
|
97
|
+
|
|
98
|
+
const pricing = estimateUsageCost(model, {
|
|
99
|
+
inputTokens,
|
|
100
|
+
outputTokens,
|
|
101
|
+
reasoningTokens,
|
|
102
|
+
cacheReadInputTokens,
|
|
103
|
+
cacheCreationInputTokens,
|
|
104
|
+
cacheCreation5mInputTokens,
|
|
105
|
+
cacheCreation1hInputTokens,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
inputTokens,
|
|
110
|
+
outputTokens,
|
|
111
|
+
reasoningTokens,
|
|
112
|
+
cacheReadInputTokens,
|
|
113
|
+
cacheCreationInputTokens,
|
|
114
|
+
cacheCreation5mInputTokens,
|
|
115
|
+
cacheCreation1hInputTokens,
|
|
116
|
+
costUsd: pricing.costUsd || 0,
|
|
117
|
+
costSource: pricing.costSource,
|
|
118
|
+
modelName: pricing.modelName || model || null,
|
|
119
|
+
};
|
|
30
120
|
}
|
|
31
121
|
|
|
32
122
|
function resolveAttribution(workflow) {
|
|
@@ -40,9 +130,8 @@ function resolveAttribution(workflow) {
|
|
|
40
130
|
}
|
|
41
131
|
}
|
|
42
132
|
|
|
43
|
-
// Stop hooks must echo stdin to stdout
|
|
44
133
|
let raw = '';
|
|
45
|
-
try { raw = readFileSync(0, 'utf8'); } catch { /*
|
|
134
|
+
try { raw = readFileSync(0, 'utf8'); } catch { /* non-critical */ }
|
|
46
135
|
if (raw) process.stdout.write(raw);
|
|
47
136
|
|
|
48
137
|
const home = homedir();
|
|
@@ -51,108 +140,105 @@ const warnFlagsPath = join(home, '.cx', 'warn-flags.txt');
|
|
|
51
140
|
const countPath = join(home, '.cx', 'files-changed-count.txt');
|
|
52
141
|
const costLogPath = join(home, '.cx', 'session-cost.jsonl');
|
|
53
142
|
const lastAgentPath = join(home, '.cx', 'last-agent.json');
|
|
143
|
+
const checkpointPath = join(home, '.cx', 'transcript-checkpoints.json');
|
|
54
144
|
|
|
55
|
-
// Read TS result
|
|
56
145
|
let tsResult = 'unchecked';
|
|
57
|
-
try { tsResult = existsSync(tsResultPath) ? readFileSync(tsResultPath, 'utf8').trim() : 'unchecked'; } catch { /*
|
|
146
|
+
try { tsResult = existsSync(tsResultPath) ? readFileSync(tsResultPath, 'utf8').trim() : 'unchecked'; } catch { /* non-critical */ }
|
|
58
147
|
|
|
59
|
-
// Read and clear warn flags
|
|
60
148
|
let warnings = [];
|
|
61
149
|
try {
|
|
62
150
|
if (existsSync(warnFlagsPath)) {
|
|
63
151
|
warnings = readFileSync(warnFlagsPath, 'utf8').split('\n').filter(Boolean);
|
|
64
152
|
writeFileSync(warnFlagsPath, '');
|
|
65
153
|
}
|
|
66
|
-
} catch { /*
|
|
154
|
+
} catch { /* non-critical */ }
|
|
67
155
|
|
|
68
|
-
// Read and clear files-changed count
|
|
69
156
|
let fileCount = 0;
|
|
70
157
|
try {
|
|
71
158
|
if (existsSync(countPath)) {
|
|
72
159
|
fileCount = parseInt(readFileSync(countPath, 'utf8').trim() || '0', 10) || 0;
|
|
73
160
|
writeFileSync(countPath, '0');
|
|
74
161
|
}
|
|
75
|
-
} catch { /*
|
|
162
|
+
} catch { /* non-critical */ }
|
|
76
163
|
|
|
77
|
-
// Estimate cost from stdin usage tokens using Sonnet 4.5/4.6 defaults.
|
|
78
164
|
let costNote = '';
|
|
165
|
+
let totalCostUsd = 0;
|
|
166
|
+
let turnsLogged = 0;
|
|
79
167
|
try {
|
|
80
168
|
const payload = raw ? JSON.parse(raw) : {};
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
169
|
+
const transcriptPath =
|
|
170
|
+
payload?.transcript_path ||
|
|
171
|
+
payload?.transcriptPath ||
|
|
172
|
+
process.env.CLAUDE_TRANSCRIPT_PATH;
|
|
173
|
+
|
|
174
|
+
let agentName = null;
|
|
175
|
+
let taskKey = null;
|
|
176
|
+
try {
|
|
177
|
+
if (existsSync(lastAgentPath)) {
|
|
178
|
+
const lastAgent = JSON.parse(readFileSync(lastAgentPath, 'utf8'));
|
|
179
|
+
agentName = lastAgent.agent || null;
|
|
180
|
+
taskKey = lastAgent.taskKey || null;
|
|
181
|
+
writeFileSync(lastAgentPath, '');
|
|
182
|
+
}
|
|
183
|
+
} catch { /* non-critical */ }
|
|
184
|
+
|
|
185
|
+
if (!agentName) {
|
|
186
|
+
agentName = 'construct';
|
|
86
187
|
}
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const cost = (
|
|
97
|
-
inputTokens * 3 +
|
|
98
|
-
cacheCreation5mInputTokens * 3.75 +
|
|
99
|
-
cacheCreation1hInputTokens * 6 +
|
|
100
|
-
Math.max(0, cacheCreationInputTokens - cacheCreation5mInputTokens - cacheCreation1hInputTokens) * 3.75 +
|
|
101
|
-
cacheReadInputTokens * 0.3 +
|
|
102
|
-
outputTokens * 15
|
|
103
|
-
) / 1_000_000;
|
|
104
|
-
costNote = `~${cost.toFixed(2)} this response`;
|
|
105
|
-
|
|
106
|
-
// Read agent attribution recorded by agent-tracker hook (best effort, cleared after read)
|
|
107
|
-
let agentName = null;
|
|
108
|
-
let taskKey = null;
|
|
109
|
-
try {
|
|
110
|
-
if (existsSync(lastAgentPath)) {
|
|
111
|
-
const lastAgent = JSON.parse(readFileSync(lastAgentPath, 'utf8'));
|
|
112
|
-
agentName = lastAgent.agent || null;
|
|
113
|
-
taskKey = lastAgent.taskKey || null;
|
|
114
|
-
writeFileSync(lastAgentPath, '');
|
|
115
|
-
}
|
|
116
|
-
} catch { /* best effort */ }
|
|
117
|
-
|
|
118
|
-
// Fallback attribution: active workflow task owner, else 'construct'
|
|
119
|
-
if (!agentName) {
|
|
120
|
-
try {
|
|
121
|
-
const wf = loadWorkflow(process.cwd());
|
|
122
|
-
const resolved = resolveAttribution(wf);
|
|
123
|
-
agentName = resolved.agent;
|
|
124
|
-
if (!taskKey && resolved.taskKey) taskKey = resolved.taskKey;
|
|
125
|
-
} catch { agentName = 'construct'; }
|
|
188
|
+
|
|
189
|
+
const checkpoints = loadTranscriptCheckpoints(checkpointPath);
|
|
190
|
+
let turns = collectUnprocessedUsage(transcriptPath, checkpoints);
|
|
191
|
+
|
|
192
|
+
if (turns.length === 0) {
|
|
193
|
+
const usage = payload?.usage || payload?.stop_hook_active?.usage || null;
|
|
194
|
+
const model = payload?.model || payload?.session?.model || null;
|
|
195
|
+
if (usage && (usage.input_tokens || usage.output_tokens)) {
|
|
196
|
+
turns = [{ usage, model, ts: null }];
|
|
126
197
|
}
|
|
198
|
+
}
|
|
127
199
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
200
|
+
const nowIso = new Date().toISOString();
|
|
201
|
+
for (const turn of turns) {
|
|
202
|
+
const priced = priceUsage(turn.model, turn.usage);
|
|
203
|
+
if (
|
|
204
|
+
!priced.inputTokens &&
|
|
205
|
+
!priced.outputTokens &&
|
|
206
|
+
!priced.cacheReadInputTokens &&
|
|
207
|
+
!priced.cacheCreationInputTokens
|
|
208
|
+
) continue;
|
|
209
|
+
|
|
210
|
+
const entry = {
|
|
211
|
+
ts: turn.ts || nowIso,
|
|
212
|
+
input_tokens: priced.inputTokens,
|
|
213
|
+
output_tokens: priced.outputTokens,
|
|
214
|
+
cost_usd: priced.costUsd,
|
|
215
|
+
};
|
|
216
|
+
if (priced.reasoningTokens) entry.reasoning_tokens = priced.reasoningTokens;
|
|
217
|
+
entry.total_tokens = priced.inputTokens + priced.outputTokens + priced.reasoningTokens;
|
|
218
|
+
if (priced.cacheReadInputTokens) entry.cache_read_input_tokens = priced.cacheReadInputTokens;
|
|
219
|
+
if (priced.cacheCreationInputTokens) entry.cache_creation_input_tokens = priced.cacheCreationInputTokens;
|
|
220
|
+
if (priced.cacheCreation5mInputTokens) entry.cache_creation_5m_input_tokens = priced.cacheCreation5mInputTokens;
|
|
221
|
+
if (priced.cacheCreation1hInputTokens) entry.cache_creation_1h_input_tokens = priced.cacheCreation1hInputTokens;
|
|
222
|
+
if (priced.modelName) entry.model = priced.modelName;
|
|
223
|
+
if (priced.costSource) entry.cost_source = priced.costSource;
|
|
224
|
+
if (agentName) entry.agent = agentName;
|
|
225
|
+
if (taskKey) entry.task_key = taskKey;
|
|
226
|
+
|
|
227
|
+
try { appendFileSync(costLogPath, JSON.stringify(entry) + '\n'); } catch { /* non-critical */ }
|
|
228
|
+
|
|
229
|
+
totalCostUsd += priced.costUsd;
|
|
230
|
+
turnsLogged += 1;
|
|
138
231
|
}
|
|
139
|
-
} catch { /* payload parse failure */ }
|
|
140
232
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const findings = alignmentFindings(workflow);
|
|
147
|
-
const high = findings.filter(f => f.severity === 'HIGH').length;
|
|
148
|
-
const phase = workflow.phase.toUpperCase();
|
|
149
|
-
execSummary = `${phase}: ${workflow.status}${high > 0 ? ` (${high} blocks)` : ''}`;
|
|
233
|
+
saveTranscriptCheckpoints(checkpointPath, checkpoints);
|
|
234
|
+
|
|
235
|
+
if (totalCostUsd > 0) {
|
|
236
|
+
const label = turnsLogged > 1 ? `${turnsLogged} turns` : 'this response';
|
|
237
|
+
costNote = `~$${totalCostUsd.toFixed(2)} ${label}`;
|
|
150
238
|
}
|
|
151
|
-
} catch { /*
|
|
239
|
+
} catch { /* non-critical */ }
|
|
152
240
|
|
|
153
|
-
// Compose message parts
|
|
154
241
|
const parts = [];
|
|
155
|
-
if (execSummary) parts.push(execSummary);
|
|
156
242
|
if (fileCount > 0) parts.push(`${fileCount} file${fileCount !== 1 ? 's' : ''} updated`);
|
|
157
243
|
if (tsResult === 'pass') parts.push('TS OK');
|
|
158
244
|
else if (tsResult !== 'unchecked') parts.push(`TS: ${tsResult}`);
|
|
@@ -165,7 +251,62 @@ const body = [
|
|
|
165
251
|
parts.join(' · '),
|
|
166
252
|
].filter(Boolean).join('\n');
|
|
167
253
|
|
|
168
|
-
|
|
254
|
+
try {
|
|
255
|
+
const cwd = process.cwd();
|
|
256
|
+
const activeSessions = listSessions(cwd, { status: 'active', limit: 1 });
|
|
257
|
+
if (activeSessions.length > 0) {
|
|
258
|
+
const sid = activeSessions[0].id;
|
|
259
|
+
const summaryParts = [];
|
|
260
|
+
try {
|
|
261
|
+
const recentCommits = execSync(`git -C "${cwd}" log --oneline -5 2>/dev/null`, { timeout: 4000 }).toString().trim();
|
|
262
|
+
if (recentCommits) summaryParts.push('commits: ' + recentCommits.split('\n').map((l) => l.trim()).slice(0, 3).join('; '));
|
|
263
|
+
} catch { /* non-critical */ }
|
|
264
|
+
if (fileCount > 0) summaryParts.push(fileCount + ' file' + (fileCount !== 1 ? 's' : '') + ' updated');
|
|
265
|
+
if (warnings.length > 0) summaryParts.push(warnings.length + ' warning' + (warnings.length !== 1 ? 's' : ''));
|
|
266
|
+
const summary = summaryParts.join(' — ') || 'Session completed';
|
|
267
|
+
updateSession(cwd, sid, { status: 'completed', summary });
|
|
268
|
+
}
|
|
269
|
+
} catch { /* non-critical */ }
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const cwd2 = process.cwd();
|
|
273
|
+
const closedSessions = listSessions(cwd2, { status: 'completed', limit: 1 });
|
|
274
|
+
if (closedSessions.length > 0) {
|
|
275
|
+
const session = loadSession(cwd2, closedSessions[0].id);
|
|
276
|
+
if (session) captureSessionArtifacts(cwd2, session);
|
|
277
|
+
}
|
|
278
|
+
} catch { /* non-critical */ }
|
|
279
|
+
|
|
280
|
+
const memStatsPath = join(home, '.cx', 'session-memory-stats.json');
|
|
281
|
+
try {
|
|
282
|
+
if (existsSync(memStatsPath)) {
|
|
283
|
+
const memStats = JSON.parse(readFileSync(memStatsPath, 'utf8'));
|
|
284
|
+
appendSessionStats(process.cwd(), {
|
|
285
|
+
project: memStats.project || null,
|
|
286
|
+
observationsInjected: memStats.observationsInjected || 0,
|
|
287
|
+
memoryEnabled: memStats.memoryEnabled !== false,
|
|
288
|
+
});
|
|
289
|
+
writeFileSync(memStatsPath, '');
|
|
290
|
+
}
|
|
291
|
+
} catch { /* non-critical */ }
|
|
292
|
+
|
|
293
|
+
// Auto-generate a performance review every 24h. Cheap — reads the cost
|
|
294
|
+
// ledger + (optionally) telemetry generations and writes a structured
|
|
295
|
+
// JSON file the dashboard's Performance page consumes. Skipped on
|
|
296
|
+
// repeat Stop events within the window to avoid review-spam.
|
|
297
|
+
try {
|
|
298
|
+
const homeDir = process.env.HOME || homedir();
|
|
299
|
+
const stampPath = join(homeDir, '.cx', 'performance-reviews', '.last-run');
|
|
300
|
+
let lastRun = 0;
|
|
301
|
+
try { lastRun = Number(readFileSync(stampPath, 'utf8')) || 0; } catch { /* fresh */ }
|
|
302
|
+
if (Date.now() - lastRun > 24 * 60 * 60 * 1000) {
|
|
303
|
+
const { runGenerator } = await import('../performance/generate.mjs');
|
|
304
|
+
await runGenerator({ env: process.env });
|
|
305
|
+
mkdirSync(dirname(stampPath), { recursive: true });
|
|
306
|
+
writeFileSync(stampPath, String(Date.now()));
|
|
307
|
+
}
|
|
308
|
+
} catch { /* review is best-effort */ }
|
|
309
|
+
|
|
169
310
|
const platform = process.platform;
|
|
170
311
|
try {
|
|
171
312
|
if (platform === 'darwin') {
|
|
@@ -184,7 +325,6 @@ try {
|
|
|
184
325
|
process.stderr.write(`[stop-notify] ${title}: ${body}\n`);
|
|
185
326
|
}
|
|
186
327
|
} catch {
|
|
187
|
-
// Fallback to stderr if notification fails
|
|
188
328
|
process.stderr.write(`[stop-notify] ${title}: ${body}\n`);
|
|
189
329
|
}
|
|
190
330
|
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* lib/hooks/stop-typecheck.mjs — Stop typecheck hook — runs TypeScript type-check at session end and records result.
|
|
4
4
|
*
|
|
5
5
|
* Runs as a Stop hook. Invokes tsc --noEmit on the project and records pass/fail to ~/.cx/pending-typecheck.txt for the next session-start hook to surface.
|
|
6
|
+
*
|
|
7
|
+
* @p95ms 2000
|
|
8
|
+
* @maxBlockingScope Stop
|
|
6
9
|
*/
|
|
7
10
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
8
11
|
import { execSync } from 'child_process';
|
|
@@ -64,7 +67,6 @@ try {
|
|
|
64
67
|
try { writeFileSync(tsResultPath, 'pass'); } catch { /* best effort */ }
|
|
65
68
|
} catch (e) {
|
|
66
69
|
const output = (e.stdout?.toString() || '') + (e.stderr?.toString() || '');
|
|
67
|
-
// Count error lines (lines ending with "error TS####:")
|
|
68
70
|
const errorLines = output.split('\n').filter(l => /error TS\d+:/.test(l));
|
|
69
71
|
const count = errorLines.length || 1;
|
|
70
72
|
|
|
@@ -78,6 +80,16 @@ try {
|
|
|
78
80
|
process.stderr.write(
|
|
79
81
|
`[stop-typecheck] TypeScript found ${count} error${count !== 1 ? 's' : ''} that need attention. The code runs but won't compile cleanly. Review before releasing.\n`
|
|
80
82
|
);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const { emitRoleEvent } = await import('../roles/hook-emit.mjs');
|
|
86
|
+
emitRoleEvent({
|
|
87
|
+
type: 'test.fail',
|
|
88
|
+
summary: `TypeScript: ${count} error${count !== 1 ? 's' : ''}\n${errorLines.slice(0, 3).join('\n')}`,
|
|
89
|
+
hookInput: { cwd: projectDir },
|
|
90
|
+
context: { tool: 'tsc', count },
|
|
91
|
+
});
|
|
92
|
+
} catch { /* best effort */ }
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
process.exit(0);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* test-watch.mjs — PostToolUse / Bash (async)
|
|
4
|
+
*
|
|
5
|
+
* When a Bash invocation ran tests (vitest/jest/npm test/bun test/pytest) and
|
|
6
|
+
* exited non-zero, emit `test.fail` for cx-qa. Detects flake patterns when the
|
|
7
|
+
* same test name passes and then fails within an hour and emits `test.flake`.
|
|
8
|
+
*
|
|
9
|
+
* @p95ms 30
|
|
10
|
+
* @maxBlockingScope none (async, non-blocking)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, existsSync, appendFileSync } from 'node:fs';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
17
|
+
import { emitRoleEvent } from '../roles/hook-emit.mjs';
|
|
18
|
+
|
|
19
|
+
let input = {};
|
|
20
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); }
|
|
21
|
+
catch (err) { logHookFailure({ hook: 'test-watch', err, phase: 'parse' }); process.exit(0); }
|
|
22
|
+
|
|
23
|
+
const command = input?.tool_input?.command || '';
|
|
24
|
+
const exitCode = input?.tool_response?.exit_code ?? input?.tool_response?.exitCode;
|
|
25
|
+
const stdout = String(input?.tool_response?.stdout || '');
|
|
26
|
+
const stderr = String(input?.tool_response?.stderr || '');
|
|
27
|
+
|
|
28
|
+
const isTestCmd = /\b(vitest|jest|npm\s+test|bun\s+test|pnpm\s+test|yarn\s+test|pytest|cargo\s+test|go\s+test)\b/.test(command);
|
|
29
|
+
if (!isTestCmd) process.exit(0);
|
|
30
|
+
if (exitCode === 0 || exitCode === undefined) process.exit(0);
|
|
31
|
+
|
|
32
|
+
const combined = (stderr + '\n' + stdout).slice(-4000);
|
|
33
|
+
const failedNames = [...combined.matchAll(/(?:FAIL|×|✗)\s+([^\n]{3,160})/g)].slice(0, 5).map((m) => m[1].trim());
|
|
34
|
+
const summary = `Tests failed: ${failedNames.length || 'unknown'}; first: ${failedNames[0] || combined.split('\n').find(Boolean)?.slice(0, 160) || command}`;
|
|
35
|
+
|
|
36
|
+
emitRoleEvent({
|
|
37
|
+
type: 'test.fail',
|
|
38
|
+
summary,
|
|
39
|
+
hookInput: input,
|
|
40
|
+
context: { command, exitCode, failedNames },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const flakeTrackerPath = join(homedir(), '.cx', 'test-watch-state.json');
|
|
44
|
+
try {
|
|
45
|
+
const state = existsSync(flakeTrackerPath) ? JSON.parse(readFileSync(flakeTrackerPath, 'utf8')) : {};
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
for (const name of failedNames) {
|
|
48
|
+
const prev = state[name];
|
|
49
|
+
if (prev && prev.passedAt && now - prev.passedAt < 60 * 60 * 1000) {
|
|
50
|
+
emitRoleEvent({
|
|
51
|
+
type: 'test.flake',
|
|
52
|
+
summary: `Flake suspected: ${name} (passed ${Math.round((now - prev.passedAt) / 60000)} min ago, now failing)`,
|
|
53
|
+
hookInput: input,
|
|
54
|
+
context: { name, prevPassedAt: prev.passedAt },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
state[name] = { failedAt: now };
|
|
58
|
+
}
|
|
59
|
+
appendFileSync(flakeTrackerPath, '');
|
|
60
|
+
const trimmed = {};
|
|
61
|
+
for (const [k, v] of Object.entries(state)) {
|
|
62
|
+
if ((v.failedAt || v.passedAt || 0) > now - 24 * 60 * 60 * 1000) trimmed[k] = v;
|
|
63
|
+
}
|
|
64
|
+
const writeFileSync = (await import('node:fs')).writeFileSync;
|
|
65
|
+
writeFileSync(flakeTrackerPath, JSON.stringify(trimmed));
|
|
66
|
+
} catch { /* best effort */ }
|
|
67
|
+
|
|
68
|
+
process.exit(0);
|
|
@@ -1,22 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* lib/host-capabilities.mjs —
|
|
3
|
+
* lib/host-capabilities.mjs — Detect the active AI harness and its tool surface.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Inspects the runtime environment (process title, env vars, OpenCode config)
|
|
6
|
+
* to classify whether Construct is running inside Claude Code, OpenCode,
|
|
7
|
+
* a plain terminal, or a multi-agent subagent context. The MCP server and
|
|
8
|
+
* CLI consult the classification to gate harness-specific features.
|
|
6
9
|
*/
|
|
7
10
|
import { execFileSync, execSync } from "node:child_process";
|
|
8
11
|
import { readOpenCodeConfig } from "./opencode-config.mjs";
|
|
9
12
|
import net from "node:net";
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
const LOWEST_PORT = 0;
|
|
15
|
+
const HIGHEST_PORT = 65535;
|
|
16
|
+
const RETRYABLE_PORT_ERRORS = new Set(["EADDRINUSE", "EACCES"]);
|
|
17
|
+
|
|
18
|
+
export async function findAvailablePort(startPort, { maxPort = HIGHEST_PORT } = {}) {
|
|
19
|
+
if (!Number.isInteger(startPort) || startPort < LOWEST_PORT || startPort > HIGHEST_PORT) {
|
|
20
|
+
throw new RangeError(`startPort must be an integer between ${LOWEST_PORT} and ${HIGHEST_PORT}. Received ${startPort}.`);
|
|
21
|
+
}
|
|
22
|
+
if (!Number.isInteger(maxPort) || maxPort < LOWEST_PORT || maxPort > HIGHEST_PORT) {
|
|
23
|
+
throw new RangeError(`maxPort must be an integer between ${LOWEST_PORT} and ${HIGHEST_PORT}. Received ${maxPort}.`);
|
|
24
|
+
}
|
|
25
|
+
if (startPort > maxPort) {
|
|
26
|
+
throw new RangeError(`startPort ${startPort} must be less than or equal to maxPort ${maxPort}.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
13
30
|
const server = net.createServer();
|
|
14
31
|
server.listen(startPort, () => {
|
|
15
32
|
const { port } = server.address();
|
|
16
33
|
server.close(() => resolve(port));
|
|
17
34
|
});
|
|
18
|
-
server.on("error", () => {
|
|
19
|
-
|
|
35
|
+
server.on("error", (error) => {
|
|
36
|
+
if (!RETRYABLE_PORT_ERRORS.has(error?.code) || startPort >= maxPort) {
|
|
37
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
resolve(findAvailablePort(startPort + 1, { maxPort }));
|
|
20
41
|
});
|
|
21
42
|
});
|
|
22
43
|
}
|
|
@@ -81,11 +102,11 @@ export function detectHostCapabilities() {
|
|
|
81
102
|
version: opencodeRaw,
|
|
82
103
|
orchestration: "primary-plus-subagents",
|
|
83
104
|
promptableWorkers: false,
|
|
84
|
-
sharedTaskRuntime: "
|
|
105
|
+
sharedTaskRuntime: "tracker-plus-plan",
|
|
85
106
|
lifecycleHooks: ["session.error", "session.idle", "tool.execute.before", "tool.execute.after"],
|
|
86
107
|
notes: [
|
|
87
108
|
"Primary agents are promptable; subagents are bounded worker sessions.",
|
|
88
|
-
"Construct uses .
|
|
109
|
+
"Construct uses tracker state, plan.md, task permissions, and plugins to coordinate parallel worker execution.",
|
|
89
110
|
"Use NEEDS_MAIN_INPUT to route user questions back to the primary persona.",
|
|
90
111
|
],
|
|
91
112
|
},
|
|
@@ -95,10 +116,10 @@ export function detectHostCapabilities() {
|
|
|
95
116
|
version: codexRaw,
|
|
96
117
|
orchestration: "profile-and-mcp",
|
|
97
118
|
promptableWorkers: false,
|
|
98
|
-
sharedTaskRuntime: "
|
|
119
|
+
sharedTaskRuntime: "tracker-plus-plan",
|
|
99
120
|
lifecycleHooks: [],
|
|
100
121
|
notes: [
|
|
101
|
-
"Use Construct profiles, MCP
|
|
122
|
+
"Use Construct profiles, MCP project/memory tools, and the active session.",
|
|
102
123
|
"Native profile switching is not automatic; continue in-session when dispatch is unavailable.",
|
|
103
124
|
],
|
|
104
125
|
},
|