@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
package/lib/auto-docs.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* <!-- /AUTO:region-name -->
|
|
8
8
|
*
|
|
9
9
|
* Running regenerateDocs() is idempotent. With check:true it returns whether
|
|
10
|
-
* anything would change without writing files
|
|
10
|
+
* anything would change without writing files — the mode CI uses to detect drift.
|
|
11
11
|
*
|
|
12
12
|
* buildSite() writes site/docs/ from the same sources feeding the AUTO regions
|
|
13
13
|
* so the GitHub Pages site never drifts from the in-repo markdown.
|
|
@@ -65,24 +65,30 @@ function buildCoreDocsContract() {
|
|
|
65
65
|
'',
|
|
66
66
|
'| File | Purpose | Update when |',
|
|
67
67
|
'|---|---|---|',
|
|
68
|
+
'| `AGENTS.md` | Canonical agent operating contract | Workflow rules, tracker hierarchy, or repo-wide guardrails change |',
|
|
68
69
|
'| `.cx/context.md` | Human-readable resumable project context | Active work, decisions, architecture assumptions, or open questions change |',
|
|
69
70
|
'| `.cx/context.json` | Machine-readable resumable context | Context state needs to stay in sync with `.cx/context.md` |',
|
|
70
|
-
'| `.cx/workflow.json` | Canonical workflow/task state | Non-trivial work starts, changes phase, or completes |',
|
|
71
71
|
'| `docs/README.md` | Docs index and maintenance contract | Core docs set or maintenance expectations change |',
|
|
72
|
-
'| `docs/architecture.md` | Canonical architecture and invariants | Runtime shape, contracts, boundaries, or major dependencies change |',
|
|
72
|
+
'| `docs/concepts/architecture.md` | Canonical architecture and invariants | Runtime shape, contracts, boundaries, or major dependencies change |',
|
|
73
73
|
'',
|
|
74
|
-
'
|
|
74
|
+
'`plan.md` is a local working document. `construct init` creates it for the active session, but it is gitignored and not committed — durable work belongs in the tracker (Beads or external).',
|
|
75
|
+
'',
|
|
76
|
+
'Tracker hierarchy: external tracker (prefer Beads) for durable work, `plan.md` for the local working plan, and cass-memory via MCP `memory` for cross-session recall.',
|
|
77
|
+
'',
|
|
78
|
+
'`AGENTS.md` is the canonical agent instruction file. On case-sensitive filesystems you may also add a lowercase `agents.md` shim for tools that require it.',
|
|
79
|
+
'All LLMs working in the repo, including Construct, must read these as project state, keep them current when work changes project reality, and prune stale sections instead of letting managed docs drift.',
|
|
75
80
|
].join('\n');
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
const DIR_DESCRIPTIONS = {
|
|
79
84
|
agents: 'Registry and generated platform adapter chains',
|
|
85
|
+
apps: 'User-facing apps shipped from this repo (e.g., apps/docs/ — Fumadocs docs site)',
|
|
80
86
|
bin: 'CLI entrypoint (`construct`)',
|
|
81
87
|
claude: 'Claude Code integration (agents, settings template)',
|
|
82
88
|
commands: 'Command prompt assets',
|
|
83
89
|
codex: 'GitHub Copilot / Codex integration',
|
|
84
90
|
docs: 'Architecture notes, runbooks, and documentation contract',
|
|
85
|
-
|
|
91
|
+
telemetry: 'Telemetry backend for agent observability',
|
|
86
92
|
lib: 'Core runtime: CLI, hooks, MCP, status, sync, workflow',
|
|
87
93
|
opencode: 'OpenCode integration config',
|
|
88
94
|
personas: 'Persona prompt definitions',
|
|
@@ -166,7 +172,7 @@ function buildAgentsTable(rootDir) {
|
|
|
166
172
|
// --- public API ---
|
|
167
173
|
|
|
168
174
|
/**
|
|
169
|
-
* Regenerate all AUTO regions in README.md
|
|
175
|
+
* Regenerate all AUTO regions in README.md, docs/README.md, and docs/concepts/architecture.md.
|
|
170
176
|
* Returns { changed: string[], checked: boolean }.
|
|
171
177
|
* With check:true writes nothing and sets changed to files that would differ.
|
|
172
178
|
*/
|
|
@@ -184,7 +190,7 @@ export async function regenerateDocs({ rootDir, check = false } = {}) {
|
|
|
184
190
|
},
|
|
185
191
|
},
|
|
186
192
|
{
|
|
187
|
-
file: path.join(rootDir, 'docs', 'architecture.md'),
|
|
193
|
+
file: path.join(rootDir, 'docs', 'concepts', 'architecture.md'),
|
|
188
194
|
regions: {
|
|
189
195
|
agents: buildAgentsTable(rootDir),
|
|
190
196
|
},
|
|
@@ -223,71 +229,245 @@ export async function regenerateDocs({ rootDir, check = false } = {}) {
|
|
|
223
229
|
}
|
|
224
230
|
|
|
225
231
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
232
|
+
* Check CLI command coverage against how-to links in docs/README.md.
|
|
233
|
+
*
|
|
234
|
+
* Returns an object with:
|
|
235
|
+
* - covered: string[] commands that have a linked how-to
|
|
236
|
+
* - uncovered: string[] commands with no how-to link in docs/README.md
|
|
237
|
+
* - total: number
|
|
238
|
+
*
|
|
239
|
+
* A command is considered covered if its name appears in any markdown link
|
|
240
|
+
* inside the how-to guides section of docs/README.md.
|
|
228
241
|
*/
|
|
229
|
-
export
|
|
242
|
+
export function checkDocsCoverage({ rootDir } = {}) {
|
|
230
243
|
rootDir = rootDir ?? process.cwd();
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
244
|
+
const docsReadme = readFile(path.join(rootDir, 'docs', 'README.md')) ?? '';
|
|
245
|
+
|
|
246
|
+
// Extract all href targets from markdown links in docs/README.md
|
|
247
|
+
const linkTargets = [...docsReadme.matchAll(/\[.*?\]\((.*?)\)/g)].map(m => m[1]);
|
|
248
|
+
|
|
249
|
+
// Build a combined corpus: docs/README.md + every linked how-to file
|
|
250
|
+
const howToDir = path.join(rootDir, 'docs', 'how-to');
|
|
251
|
+
let corpus = docsReadme;
|
|
252
|
+
for (const target of linkTargets) {
|
|
253
|
+
if (!target.startsWith('./how-to/')) continue;
|
|
254
|
+
const filePath = path.join(rootDir, 'docs', target.replace(/^\.\//, ''));
|
|
255
|
+
const content = readFile(filePath);
|
|
256
|
+
if (content) corpus += '\n' + content;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Capture command mentions from both inline code (`construct cmd`) and code blocks (bare lines)
|
|
260
|
+
const mentionedCmds = new Set([
|
|
261
|
+
...corpus.matchAll(/`construct\s+([\w:_-]+)`/g),
|
|
262
|
+
...corpus.matchAll(/^construct\s+([\w:_-]+)/gm),
|
|
263
|
+
].map(m => m[1]));
|
|
264
|
+
|
|
265
|
+
// Collect all command names from the registry
|
|
266
|
+
const allCommands = [];
|
|
267
|
+
for (const cmds of Object.values(CLI_COMMANDS_BY_CATEGORY)) {
|
|
268
|
+
for (const cmd of cmds) allCommands.push(cmd.name);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Skip internal / plumbing commands and simple commands covered by getting-started.md
|
|
272
|
+
const skipList = new Set([
|
|
273
|
+
'version', 'diff', 'validate', 'docs:update', 'docs:site', 'docs:check', 'sync',
|
|
274
|
+
'list', 'setup', 'show', 'hosts', 'plugin', 'mcp', 'evals',
|
|
275
|
+
'telemetry-backfill', 'lint:comments', 'lint:research',
|
|
276
|
+
// short commands documented in getting-started.md
|
|
277
|
+
'up', 'down', 'status', 'serve', 'init', 'update', 'doctor',
|
|
278
|
+
// team sub-commands — covered as part of review how-to
|
|
279
|
+
'team',
|
|
280
|
+
// covered in getting-started.md under Memory Layer section
|
|
281
|
+
'bootstrap', 'memory',
|
|
282
|
+
// niche overlay command — documented in registry description
|
|
283
|
+
'headhunt',
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
const covered = [];
|
|
287
|
+
const uncovered = [];
|
|
288
|
+
|
|
289
|
+
for (const name of allCommands) {
|
|
290
|
+
if (skipList.has(name)) continue;
|
|
291
|
+
const slug = name.replace(':', '-');
|
|
292
|
+
const isCovered =
|
|
293
|
+
linkTargets.some(t => t.includes(slug) || t.includes(name)) ||
|
|
294
|
+
mentionedCmds.has(name);
|
|
295
|
+
if (isCovered) covered.push(name);
|
|
296
|
+
else uncovered.push(name);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { covered, uncovered, total: covered.length + uncovered.length };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Categories surfaced in the Fumadocs reference. Internal commands (e.g.,
|
|
303
|
+
// `construct hook <name>`) are not user-facing reference material.
|
|
304
|
+
const REFERENCE_CATEGORIES = [
|
|
305
|
+
'Services',
|
|
306
|
+
'Agents & Sync',
|
|
307
|
+
'Work',
|
|
308
|
+
'Embed',
|
|
309
|
+
'Models & Integrations',
|
|
310
|
+
'Providers',
|
|
311
|
+
'Observability',
|
|
312
|
+
'Docs',
|
|
313
|
+
'Diagnostics',
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
function slugify(category) {
|
|
317
|
+
return category.toLowerCase()
|
|
318
|
+
.replace(/&/g, 'and')
|
|
319
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
320
|
+
.replace(/^-|-$/g, '');
|
|
321
|
+
}
|
|
254
322
|
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
323
|
+
function renderCommandPage(category, commands) {
|
|
324
|
+
const lines = [];
|
|
325
|
+
lines.push('---');
|
|
326
|
+
lines.push(`title: ${category}`);
|
|
327
|
+
lines.push(`description: ${category} commands for Construct.`);
|
|
328
|
+
lines.push('---');
|
|
329
|
+
lines.push('');
|
|
330
|
+
lines.push(`# ${category}`);
|
|
331
|
+
lines.push('');
|
|
332
|
+
lines.push('| Command | What it does |');
|
|
333
|
+
lines.push('|---|---|');
|
|
334
|
+
for (const cmd of commands) {
|
|
335
|
+
lines.push(`| \`construct ${cmd.name}\` | ${cmd.description} |`);
|
|
336
|
+
}
|
|
337
|
+
lines.push('');
|
|
338
|
+
for (const cmd of commands) {
|
|
339
|
+
lines.push(`## construct ${cmd.name}`);
|
|
340
|
+
lines.push('');
|
|
341
|
+
lines.push(cmd.description);
|
|
342
|
+
lines.push('');
|
|
343
|
+
if (cmd.usage) {
|
|
344
|
+
lines.push('**Usage**');
|
|
345
|
+
lines.push('');
|
|
346
|
+
lines.push('```bash');
|
|
347
|
+
lines.push(cmd.usage);
|
|
348
|
+
lines.push('```');
|
|
349
|
+
lines.push('');
|
|
350
|
+
}
|
|
351
|
+
if (cmd.subcommands && cmd.subcommands.length) {
|
|
352
|
+
lines.push('**Subcommands**');
|
|
353
|
+
lines.push('');
|
|
354
|
+
for (const sub of cmd.subcommands) {
|
|
355
|
+
lines.push(`- \`${sub}\``);
|
|
356
|
+
}
|
|
357
|
+
lines.push('');
|
|
358
|
+
}
|
|
359
|
+
if (cmd.options && cmd.options.length) {
|
|
360
|
+
lines.push('**Options**');
|
|
361
|
+
lines.push('');
|
|
362
|
+
lines.push('| Flag | Description |');
|
|
363
|
+
lines.push('|---|---|');
|
|
364
|
+
for (const opt of cmd.options) {
|
|
365
|
+
lines.push(`| \`${opt.flag}\` | ${opt.desc} |`);
|
|
366
|
+
}
|
|
367
|
+
lines.push('');
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return lines.join('\n');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function renderHooksPage(rootDir) {
|
|
374
|
+
return [
|
|
375
|
+
'---',
|
|
376
|
+
'title: Hooks',
|
|
377
|
+
'description: Hooks fire during Claude Code sessions, on file edits, commits, pushes, and prompts. Generated from lib/hooks/.',
|
|
378
|
+
'---',
|
|
379
|
+
'',
|
|
380
|
+
'> Generated from `lib/hooks/`. Re-run `construct docs:site` to refresh.',
|
|
381
|
+
'',
|
|
382
|
+
'Hooks are wired in `platforms/claude/settings.template.json` and execute as child processes during Claude Code sessions. Each hook reads JSON on stdin, performs its check or transform, and exits with a status that signals whether the surrounding tool call should proceed.',
|
|
383
|
+
'',
|
|
260
384
|
buildHooksTable(rootDir),
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
385
|
+
'',
|
|
386
|
+
].join('\n');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function renderAgentsPage(rootDir) {
|
|
390
|
+
const table = buildAgentsTable(rootDir);
|
|
391
|
+
if (!table) return null;
|
|
392
|
+
return [
|
|
393
|
+
'---',
|
|
394
|
+
'title: Agents',
|
|
395
|
+
'description: The 28 cx-* specialists plus the construct persona. Generated from agents/registry.json.',
|
|
396
|
+
'---',
|
|
397
|
+
'',
|
|
398
|
+
'> Generated from `agents/registry.json`. Re-run `construct docs:site` to refresh.',
|
|
399
|
+
'',
|
|
400
|
+
'Construct ships one persona (`construct`) plus 28 specialists. Each specialist has a role, model tier, and prompt that defines its decision authority. Specialists are addressable directly when you want a focused conversation; for everyday work, address `@construct` and let it route.',
|
|
401
|
+
'',
|
|
402
|
+
table,
|
|
403
|
+
'',
|
|
404
|
+
].join('\n');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Emit MDX reference pages into docs/reference/ for the Fumadocs site.
|
|
409
|
+
* Mirrors the data sources behind AUTO regions and buildSite() but writes
|
|
410
|
+
* Markdown files Fumadocs can render directly (no MkDocs intermediate).
|
|
411
|
+
*/
|
|
412
|
+
export function buildFumadocsReference({ rootDir } = {}) {
|
|
413
|
+
rootDir = rootDir ?? process.cwd();
|
|
414
|
+
const refDir = path.join(rootDir, 'docs', 'reference');
|
|
415
|
+
const cliDir = path.join(refDir, 'cli');
|
|
416
|
+
fs.mkdirSync(cliDir, { recursive: true });
|
|
417
|
+
|
|
418
|
+
const written = [];
|
|
419
|
+
|
|
420
|
+
const cliPages = [];
|
|
421
|
+
for (const category of REFERENCE_CATEGORIES) {
|
|
422
|
+
const commands = CLI_COMMANDS_BY_CATEGORY[category] ?? [];
|
|
423
|
+
if (!commands.length) continue;
|
|
424
|
+
const slug = slugify(category);
|
|
425
|
+
const out = path.join(cliDir, `${slug}.md`);
|
|
426
|
+
if (writeIfChanged(out, renderCommandPage(category, commands))) {
|
|
427
|
+
written.push(path.relative(rootDir, out));
|
|
428
|
+
}
|
|
429
|
+
cliPages.push(slug);
|
|
273
430
|
}
|
|
274
431
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
432
|
+
const cliMeta = path.join(cliDir, 'meta.json');
|
|
433
|
+
const cliMetaContent = JSON.stringify({ title: 'CLI', pages: ['index', ...cliPages] }, null, 2) + '\n';
|
|
434
|
+
if (writeIfChanged(cliMeta, cliMetaContent)) written.push(path.relative(rootDir, cliMeta));
|
|
435
|
+
|
|
436
|
+
const cliIndexContent = [
|
|
437
|
+
'---',
|
|
438
|
+
'title: CLI',
|
|
439
|
+
'description: Every construct command, grouped by category. Generated from lib/cli-commands.mjs.',
|
|
440
|
+
'---',
|
|
441
|
+
'',
|
|
442
|
+
'> Generated from `lib/cli-commands.mjs`. Re-run `construct docs:site` to refresh.',
|
|
443
|
+
'',
|
|
444
|
+
'Commands are grouped by what they do. Pick a category below.',
|
|
445
|
+
'',
|
|
446
|
+
...REFERENCE_CATEGORIES.map((cat) => {
|
|
447
|
+
const slug = slugify(cat);
|
|
448
|
+
const commands = CLI_COMMANDS_BY_CATEGORY[cat] ?? [];
|
|
449
|
+
if (!commands.length) return null;
|
|
450
|
+
return `- [${cat}](/reference/cli/${slug}) — ${commands.length} command${commands.length === 1 ? '' : 's'}`;
|
|
451
|
+
}).filter(Boolean),
|
|
452
|
+
'',
|
|
453
|
+
].join('\n');
|
|
454
|
+
if (writeIfChanged(path.join(cliDir, 'index.md'), cliIndexContent)) {
|
|
455
|
+
written.push(path.relative(rootDir, path.join(cliDir, 'index.md')));
|
|
289
456
|
}
|
|
290
457
|
|
|
291
|
-
|
|
458
|
+
const hooksPage = path.join(refDir, 'hooks.md');
|
|
459
|
+
if (writeIfChanged(hooksPage, renderHooksPage(rootDir))) {
|
|
460
|
+
written.push(path.relative(rootDir, hooksPage));
|
|
461
|
+
}
|
|
292
462
|
|
|
463
|
+
const agentsContent = renderAgentsPage(rootDir);
|
|
464
|
+
if (agentsContent) {
|
|
465
|
+
const agentsPage = path.join(refDir, 'agents.md');
|
|
466
|
+
if (writeIfChanged(agentsPage, agentsContent)) {
|
|
467
|
+
written.push(path.relative(rootDir, agentsPage));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return { written };
|
|
293
472
|
}
|
|
473
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/beads-auto-close.mjs — auto-close beads on merge.
|
|
4
|
+
*
|
|
5
|
+
* Scans the commit messages on the just-merged range for any
|
|
6
|
+
* `closes construct-xxx` / `fixes construct-xxx` / `resolves construct-xxx`
|
|
7
|
+
* references (case-insensitive). For each unique bead id found, runs
|
|
8
|
+
* `bd close <id> --reason="Auto-closed on merge <sha>: <subject>"` so
|
|
9
|
+
* the tracker matches reality without manual intervention.
|
|
10
|
+
*
|
|
11
|
+
* Invocation: the `.beads/hooks/post-merge` git hook shim runs this,
|
|
12
|
+
* or `--range=<git-range>` triggers a standalone backfill.
|
|
13
|
+
*
|
|
14
|
+
* Exit codes:
|
|
15
|
+
* 0 no closes referenced, or all closes succeeded
|
|
16
|
+
* 1 one or more bd close calls failed
|
|
17
|
+
* 2 could not parse git range / no bd CLI
|
|
18
|
+
*
|
|
19
|
+
* Idempotent: closing an already-closed bead is a no-op so re-running
|
|
20
|
+
* the hook after a merge is safe.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
|
|
25
|
+
const CLOSES_RE = /\b(?:closes?|close|fix(?:es)?|fixed|resolves?|resolved)\s+(construct-[a-z0-9]+)/gi;
|
|
26
|
+
|
|
27
|
+
export function parseClosesFromMessage(message) {
|
|
28
|
+
if (!message) return [];
|
|
29
|
+
const found = new Set();
|
|
30
|
+
let match;
|
|
31
|
+
CLOSES_RE.lastIndex = 0;
|
|
32
|
+
while ((match = CLOSES_RE.exec(message)) !== null) {
|
|
33
|
+
found.add(match[1].toLowerCase());
|
|
34
|
+
}
|
|
35
|
+
return [...found];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function listCommitsInRange(range, { runner = spawnSync } = {}) {
|
|
39
|
+
const result = runner('git', ['log', range, '--pretty=format:%H%x09%s%x09%B%x00'], {
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
42
|
+
});
|
|
43
|
+
if (result.status !== 0) return [];
|
|
44
|
+
return result.stdout
|
|
45
|
+
.split('\x00')
|
|
46
|
+
.map((entry) => entry.trim())
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.map((entry) => {
|
|
49
|
+
const [sha, subject, ...rest] = entry.split('\t');
|
|
50
|
+
return { sha, subject, body: rest.join('\t') };
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function collectAutoCloses(commits) {
|
|
55
|
+
const closesById = new Map();
|
|
56
|
+
for (const commit of commits) {
|
|
57
|
+
const ids = parseClosesFromMessage(`${commit.subject}\n${commit.body}`);
|
|
58
|
+
for (const id of ids) {
|
|
59
|
+
if (!closesById.has(id)) closesById.set(id, []);
|
|
60
|
+
closesById.get(id).push({ sha: commit.sha, subject: commit.subject });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return closesById;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildAutoCloseReason(commits) {
|
|
67
|
+
const first = commits[0];
|
|
68
|
+
if (!first) return 'Auto-closed on merge';
|
|
69
|
+
const shortSha = first.sha.slice(0, 12);
|
|
70
|
+
return `Auto-closed on merge ${shortSha}: ${first.subject}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Drive the auto-close: list commits in the range, parse closes, invoke
|
|
75
|
+
* `bd close` for each. Returns a summary the caller can log or test.
|
|
76
|
+
*/
|
|
77
|
+
export async function runAutoClose({ range, runner = spawnSync } = {}) {
|
|
78
|
+
if (!range) return { ok: false, reason: 'no range', closed: [], failed: [] };
|
|
79
|
+
|
|
80
|
+
const commits = listCommitsInRange(range, { runner });
|
|
81
|
+
if (commits.length === 0) return { ok: true, closed: [], failed: [], commits: 0 };
|
|
82
|
+
|
|
83
|
+
const closesById = collectAutoCloses(commits);
|
|
84
|
+
if (closesById.size === 0) return { ok: true, closed: [], failed: [], commits: commits.length };
|
|
85
|
+
|
|
86
|
+
const closed = [];
|
|
87
|
+
const failed = [];
|
|
88
|
+
for (const [id, refs] of closesById) {
|
|
89
|
+
const reason = buildAutoCloseReason(refs);
|
|
90
|
+
const result = runner('bd', ['close', id, `--reason=${reason}`], {
|
|
91
|
+
encoding: 'utf8',
|
|
92
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
93
|
+
});
|
|
94
|
+
if (result.status === 0) {
|
|
95
|
+
closed.push({ id, sha: refs[0]?.sha, subject: refs[0]?.subject });
|
|
96
|
+
} else {
|
|
97
|
+
failed.push({
|
|
98
|
+
id,
|
|
99
|
+
exitCode: result.status,
|
|
100
|
+
stderr: (result.stderr || '').slice(0, 500),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { ok: failed.length === 0, closed, failed, commits: commits.length };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// CLI entry: read --range, run, print summary.
|
|
108
|
+
const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
|
|
109
|
+
if (invokedDirectly) {
|
|
110
|
+
const args = process.argv.slice(2);
|
|
111
|
+
const rangeArg = args.find((a) => a.startsWith('--range='));
|
|
112
|
+
// Default: every commit unique to the current branch that landed via the
|
|
113
|
+
// current HEAD's range (post-merge sees ORIG_HEAD..HEAD typically).
|
|
114
|
+
const range = rangeArg ? rangeArg.split('=')[1] : (process.env.BEADS_AUTO_CLOSE_RANGE || 'ORIG_HEAD..HEAD');
|
|
115
|
+
const summary = await runAutoClose({ range });
|
|
116
|
+
if (summary.closed.length > 0) {
|
|
117
|
+
process.stdout.write(`[beads-auto-close] closed ${summary.closed.length} bead(s): ${summary.closed.map((c) => c.id).join(', ')}\n`);
|
|
118
|
+
}
|
|
119
|
+
if (summary.failed.length > 0) {
|
|
120
|
+
for (const f of summary.failed) {
|
|
121
|
+
process.stderr.write(`[beads-auto-close] failed ${f.id} (exit ${f.exitCode}): ${f.stderr}\n`);
|
|
122
|
+
}
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/beads-drift.mjs — beads hygiene drift detection.
|
|
4
|
+
*
|
|
5
|
+
* Three checks that surface stale tracker state before it accumulates:
|
|
6
|
+
*
|
|
7
|
+
* 1. Stale-open: open beads whose updatedAt is older than the staleness
|
|
8
|
+
* window (default 14 days). Stale-open isn't a bug by itself but
|
|
9
|
+
* means nobody has reviewed the bead in two weeks — likely irrelevant.
|
|
10
|
+
*
|
|
11
|
+
* 2. Stuck-in-progress: in_progress beads whose updatedAt is older than
|
|
12
|
+
* the stuck window (default 3 days). A bead in_progress is supposed
|
|
13
|
+
* to be claimed by an active session; 3+ days untouched means the
|
|
14
|
+
* session that claimed it didn't follow through.
|
|
15
|
+
*
|
|
16
|
+
* 3. Merge-drift: open beads whose title or description appears verbatim
|
|
17
|
+
* in a commit subject from the last N commits (default 50). Cross-
|
|
18
|
+
* checks tracker state against recently merged work to catch beads
|
|
19
|
+
* that should have been closed in the merge.
|
|
20
|
+
*
|
|
21
|
+
* The module exposes `detectBeadsDrift({...})` as a pure function so
|
|
22
|
+
* `construct doctor` and the pre-push gate can both consume it. CLI
|
|
23
|
+
* entry writes a human-readable report (and machine-readable JSON via
|
|
24
|
+
* `--json`) and exits non-zero when any check exceeds its threshold.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { spawnSync } from 'node:child_process';
|
|
28
|
+
|
|
29
|
+
const DEFAULT_STALE_OPEN_DAYS = 14;
|
|
30
|
+
const DEFAULT_STUCK_IN_PROGRESS_DAYS = 3;
|
|
31
|
+
const DEFAULT_MERGE_LOOKBACK = 50;
|
|
32
|
+
|
|
33
|
+
function daysAgo(value) {
|
|
34
|
+
if (!value) return Infinity;
|
|
35
|
+
const ts = value instanceof Date ? value.getTime() : Date.parse(value);
|
|
36
|
+
if (!Number.isFinite(ts)) return Infinity;
|
|
37
|
+
return (Date.now() - ts) / (24 * 60 * 60 * 1000);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function listBeadsJson({ status, runner = spawnSync } = {}) {
|
|
41
|
+
const args = ['list', '--json'];
|
|
42
|
+
if (status) args.push(`--status=${status}`);
|
|
43
|
+
const result = runner('bd', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
44
|
+
if (result.status !== 0) return [];
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(result.stdout);
|
|
47
|
+
if (Array.isArray(parsed)) return parsed;
|
|
48
|
+
if (Array.isArray(parsed?.issues)) return parsed.issues;
|
|
49
|
+
return [];
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function recentCommitSubjects(limit, { runner = spawnSync } = {}) {
|
|
56
|
+
const result = runner('git', ['log', '-n', String(limit), '--pretty=format:%s'], {
|
|
57
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
58
|
+
});
|
|
59
|
+
if (result.status !== 0) return [];
|
|
60
|
+
return result.stdout.split('\n').filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function tokenize(text) {
|
|
64
|
+
return String(text || '').toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 4);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function titleAppearsInSubject(beadTitle, commitSubject) {
|
|
68
|
+
// Normalize both sides, then check that the bead's first significant
|
|
69
|
+
// tokens are present in the commit subject. Strict enough to avoid
|
|
70
|
+
// false positives, lax enough to catch reasonable matches.
|
|
71
|
+
const beadTokens = tokenize(beadTitle).slice(0, 5);
|
|
72
|
+
if (beadTokens.length < 3) return false;
|
|
73
|
+
const lowerSubject = String(commitSubject || '').toLowerCase();
|
|
74
|
+
let hits = 0;
|
|
75
|
+
for (const token of beadTokens) {
|
|
76
|
+
if (lowerSubject.includes(token)) hits += 1;
|
|
77
|
+
}
|
|
78
|
+
return hits >= 3;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run the three drift checks against the live tracker + git state.
|
|
83
|
+
* Pure-ish — accepts a runner injection for tests.
|
|
84
|
+
*/
|
|
85
|
+
export function detectBeadsDrift({
|
|
86
|
+
staleOpenDays = DEFAULT_STALE_OPEN_DAYS,
|
|
87
|
+
stuckInProgressDays = DEFAULT_STUCK_IN_PROGRESS_DAYS,
|
|
88
|
+
mergeLookback = DEFAULT_MERGE_LOOKBACK,
|
|
89
|
+
runner = spawnSync,
|
|
90
|
+
} = {}) {
|
|
91
|
+
const open = listBeadsJson({ status: 'open', runner });
|
|
92
|
+
const inProgress = listBeadsJson({ status: 'in_progress', runner });
|
|
93
|
+
const commits = recentCommitSubjects(mergeLookback, { runner });
|
|
94
|
+
|
|
95
|
+
const staleOpen = open.filter((b) => daysAgo(b.updated || b.updatedAt) > staleOpenDays);
|
|
96
|
+
const stuckInProgress = inProgress.filter((b) => daysAgo(b.updated || b.updatedAt) > stuckInProgressDays);
|
|
97
|
+
|
|
98
|
+
const mergeDrift = [];
|
|
99
|
+
for (const bead of open) {
|
|
100
|
+
for (const subject of commits) {
|
|
101
|
+
if (titleAppearsInSubject(bead.title, subject)) {
|
|
102
|
+
mergeDrift.push({ id: bead.id, title: bead.title, matchedSubject: subject });
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
staleOpen: staleOpen.map((b) => ({ id: b.id, title: b.title, ageDays: Math.round(daysAgo(b.updated || b.updatedAt)) })),
|
|
110
|
+
stuckInProgress: stuckInProgress.map((b) => ({ id: b.id, title: b.title, ageDays: Math.round(daysAgo(b.updated || b.updatedAt)) })),
|
|
111
|
+
mergeDrift,
|
|
112
|
+
thresholds: { staleOpenDays, stuckInProgressDays, mergeLookback },
|
|
113
|
+
counts: {
|
|
114
|
+
staleOpen: staleOpen.length,
|
|
115
|
+
stuckInProgress: stuckInProgress.length,
|
|
116
|
+
mergeDrift: mergeDrift.length,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function formatDriftReport(report) {
|
|
122
|
+
const lines = [];
|
|
123
|
+
lines.push(`Beads hygiene drift report — checked against last ${report.thresholds.mergeLookback} commits`);
|
|
124
|
+
lines.push('');
|
|
125
|
+
if (report.counts.staleOpen === 0 && report.counts.stuckInProgress === 0 && report.counts.mergeDrift === 0) {
|
|
126
|
+
lines.push('✓ no drift detected');
|
|
127
|
+
return lines.join('\n') + '\n';
|
|
128
|
+
}
|
|
129
|
+
if (report.counts.stuckInProgress > 0) {
|
|
130
|
+
lines.push(`Stuck in_progress (> ${report.thresholds.stuckInProgressDays}d untouched):`);
|
|
131
|
+
for (const b of report.stuckInProgress) lines.push(` ${b.id} · ${b.ageDays}d · ${b.title}`);
|
|
132
|
+
lines.push('');
|
|
133
|
+
}
|
|
134
|
+
if (report.counts.staleOpen > 0) {
|
|
135
|
+
lines.push(`Stale open (> ${report.thresholds.staleOpenDays}d untouched):`);
|
|
136
|
+
for (const b of report.staleOpen) lines.push(` ${b.id} · ${b.ageDays}d · ${b.title}`);
|
|
137
|
+
lines.push('');
|
|
138
|
+
}
|
|
139
|
+
if (report.counts.mergeDrift > 0) {
|
|
140
|
+
lines.push(`Possible merge drift (open bead title overlaps a recent commit subject):`);
|
|
141
|
+
for (const b of report.mergeDrift) lines.push(` ${b.id} · ${b.title}\n matched: ${b.matchedSubject}`);
|
|
142
|
+
lines.push('');
|
|
143
|
+
}
|
|
144
|
+
return lines.join('\n');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// CLI: print report, exit non-zero when thresholds breached unless --report-only.
|
|
148
|
+
const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
|
|
149
|
+
if (invokedDirectly) {
|
|
150
|
+
const args = process.argv.slice(2);
|
|
151
|
+
const json = args.includes('--json');
|
|
152
|
+
const reportOnly = args.includes('--report-only');
|
|
153
|
+
const opts = {};
|
|
154
|
+
for (const arg of args) {
|
|
155
|
+
const m = arg.match(/^--([a-z-]+)=(\d+)$/);
|
|
156
|
+
if (!m) continue;
|
|
157
|
+
if (m[1] === 'stale-open-days') opts.staleOpenDays = Number(m[2]);
|
|
158
|
+
if (m[1] === 'stuck-in-progress-days') opts.stuckInProgressDays = Number(m[2]);
|
|
159
|
+
if (m[1] === 'merge-lookback') opts.mergeLookback = Number(m[2]);
|
|
160
|
+
}
|
|
161
|
+
const report = detectBeadsDrift(opts);
|
|
162
|
+
if (json) {
|
|
163
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
164
|
+
} else {
|
|
165
|
+
process.stdout.write(formatDriftReport(report));
|
|
166
|
+
}
|
|
167
|
+
if (!reportOnly && (report.counts.stuckInProgress > 0 || report.counts.mergeDrift > 0)) {
|
|
168
|
+
process.exit(2);
|
|
169
|
+
}
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|