@geraldmaron/construct 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +9 -6
- package/LICENSE +201 -21
- package/README.md +132 -257
- package/agents/contracts.json +387 -0
- package/agents/prompts/cx-accessibility.md +8 -0
- package/agents/prompts/cx-ai-engineer.md +92 -0
- package/agents/prompts/cx-architect.md +10 -2
- package/agents/prompts/cx-business-strategist.md +13 -0
- package/agents/prompts/cx-data-analyst.md +80 -0
- package/agents/prompts/cx-data-engineer.md +4 -0
- package/agents/prompts/cx-debugger.md +8 -0
- package/agents/prompts/cx-designer.md +19 -0
- package/agents/prompts/cx-devil-advocate.md +4 -0
- package/agents/prompts/cx-docs-keeper.md +128 -0
- package/agents/prompts/cx-engineer.md +13 -0
- package/agents/prompts/cx-evaluator.md +4 -0
- package/agents/prompts/cx-explorer.md +12 -0
- package/agents/prompts/cx-legal-compliance.md +13 -0
- package/agents/prompts/cx-operations.md +13 -0
- package/agents/prompts/cx-orchestrator.md +107 -4
- package/agents/prompts/cx-platform-engineer.md +75 -0
- package/agents/prompts/cx-product-manager.md +8 -0
- package/agents/prompts/cx-qa.md +100 -0
- package/agents/prompts/cx-rd-lead.md +13 -0
- package/agents/prompts/cx-release-manager.md +8 -0
- package/agents/prompts/cx-researcher.md +21 -1
- package/agents/prompts/cx-reviewer.md +8 -0
- package/agents/prompts/cx-security.md +104 -0
- package/agents/prompts/cx-sre.md +104 -0
- package/agents/prompts/cx-test-automation.md +4 -0
- package/agents/prompts/cx-trace-reviewer.md +7 -3
- package/agents/prompts/cx-ux-researcher.md +4 -0
- package/agents/registry.json +365 -90
- package/agents/role-manifests.json +217 -0
- package/bin/construct +3345 -141
- package/bin/construct-postinstall.mjs +87 -0
- package/commands/build/feature.md +6 -6
- package/commands/plan/feature.md +6 -5
- package/commands/plan/requirements.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/work/drive.md +3 -3
- package/commands/work/optimize-prompts.md +3 -3
- package/db/{migrations → schema}/001_init.sql +2 -0
- package/db/schema/002_pgvector.sql +182 -0
- package/db/schema/003_intake.sql +47 -0
- package/examples/README.md +85 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
- package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
- package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
- package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
- package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
- package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
- package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
- package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
- package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
- package/examples/personas/construct/bad/commit-without-approval.md +30 -0
- package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
- package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
- package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
- package/examples/provider-plugin/README.md +34 -0
- package/examples/provider-plugin/index.mjs +74 -0
- package/examples/provider-plugin/package.json +15 -0
- package/examples/seed-observations/README.md +38 -0
- package/examples/seed-observations/anti-patterns.md +44 -0
- package/examples/seed-observations/decisions.md +36 -0
- package/examples/seed-observations/patterns.md +42 -0
- package/lib/agent-contracts-enforce.mjs +158 -0
- package/lib/agent-contracts.mjs +231 -0
- package/lib/agents/postconditions.mjs +126 -0
- package/lib/agents/schema.mjs +124 -0
- package/lib/artifact-capture.mjs +183 -0
- package/lib/audit-trail.mjs +149 -0
- package/lib/auto-docs.mjs +245 -65
- package/lib/beads/auto-close.mjs +126 -0
- package/lib/beads/drift.mjs +171 -0
- package/lib/beads-automation.mjs +542 -0
- package/lib/beads-client.mjs +518 -0
- package/lib/beads-lock.mjs +377 -0
- package/lib/beads-optimistic.mjs +365 -0
- package/lib/bootstrap/built-ins.mjs +136 -0
- package/lib/bootstrap/lazy-install.mjs +161 -0
- package/lib/bootstrap/resources.mjs +120 -0
- package/lib/bootstrap.mjs +105 -0
- package/lib/cache-governor.js +213 -0
- package/lib/cache-strategy-anthropic.js +62 -0
- package/lib/cache-strategy-google.js +79 -0
- package/lib/cache-strategy-none.js +30 -0
- package/lib/cache-strategy-openai.js +48 -0
- package/lib/cache-strategy.js +91 -0
- package/lib/claude-allow.mjs +149 -0
- package/lib/cli-commands.mjs +413 -258
- package/lib/codex-config.mjs +3 -1
- package/lib/comment-lint.mjs +55 -6
- package/lib/completions.mjs +21 -6
- package/lib/config/alias.mjs +56 -0
- package/lib/config/project-config.mjs +335 -0
- package/lib/config/schema.mjs +159 -0
- package/lib/context-router.mjs +308 -0
- package/lib/cost-ledger.mjs +177 -0
- package/lib/cost.mjs +171 -10
- package/lib/dashboard-static.mjs +158 -0
- package/lib/deployment-mode.mjs +86 -0
- package/lib/deprecate.mjs +49 -0
- package/lib/dispatch-batch.js +183 -0
- package/lib/distill.mjs +21 -8
- package/lib/doc-stamp.mjs +164 -0
- package/lib/doc-verify.mjs +119 -0
- package/lib/docs-routing.mjs +89 -0
- package/lib/docs-verify.mjs +417 -0
- package/lib/doctor/audit.mjs +71 -0
- package/lib/doctor/cli.mjs +99 -0
- package/lib/doctor/escalate.mjs +29 -0
- package/lib/doctor/index.mjs +140 -0
- package/lib/doctor/report.mjs +170 -0
- package/lib/doctor/watchers/bd-watch.mjs +117 -0
- package/lib/doctor/watchers/cost.mjs +130 -0
- package/lib/doctor/watchers/disk.mjs +122 -0
- package/lib/doctor/watchers/handoffs.mjs +33 -0
- package/lib/doctor/watchers/process-pressure.mjs +60 -0
- package/lib/doctor/watchers/service-health.mjs +188 -0
- package/lib/document-extract.mjs +288 -0
- package/lib/document-ingest.mjs +230 -0
- package/lib/drop.mjs +282 -0
- package/lib/embed/approval-queue.mjs +176 -0
- package/lib/embed/artifact.mjs +349 -0
- package/lib/embed/authority-guard.mjs +155 -0
- package/lib/embed/cli.mjs +408 -0
- package/lib/embed/config.mjs +355 -0
- package/lib/embed/conflict-detection.mjs +264 -0
- package/lib/embed/customer-profiles.mjs +480 -0
- package/lib/embed/daemon.mjs +1309 -0
- package/lib/embed/demand-fetch.mjs +449 -0
- package/lib/embed/docs-lifecycle.mjs +349 -0
- package/lib/embed/inbox-live-watcher.mjs +119 -0
- package/lib/embed/inbox.mjs +343 -0
- package/lib/embed/intake-metrics.mjs +190 -0
- package/lib/embed/jobs/vector-sync.mjs +198 -0
- package/lib/embed/notifications.mjs +75 -0
- package/lib/embed/output.mjs +79 -0
- package/lib/embed/providers/github.mjs +295 -0
- package/lib/embed/providers/jira.mjs +192 -0
- package/lib/embed/providers/linear.mjs +186 -0
- package/lib/embed/providers/registry.mjs +115 -0
- package/lib/embed/providers/slack.mjs +203 -0
- package/lib/embed/recommendation-store.mjs +378 -0
- package/lib/embed/roadmap.mjs +374 -0
- package/lib/embed/role-framing.mjs +110 -0
- package/lib/embed/scheduler.mjs +99 -0
- package/lib/embed/semantic.mjs +325 -0
- package/lib/embed/snapshot.mjs +191 -0
- package/lib/embed/supervision.mjs +235 -0
- package/lib/embed/target-resolver.mjs +186 -0
- package/lib/embed/worker.mjs +62 -0
- package/lib/embed/workspaces.mjs +297 -0
- package/lib/engine/chunker-headings.mjs +110 -0
- package/lib/engine/compressor-heuristic.mjs +100 -0
- package/lib/engine/consolidate.mjs +287 -0
- package/lib/engine/contracts.mjs +126 -0
- package/lib/engine/defaults.mjs +129 -0
- package/lib/engine/eval-retrieval.mjs +148 -0
- package/lib/engine/fuser-rrf.mjs +62 -0
- package/lib/engine/index.mjs +37 -0
- package/lib/engine/registry.mjs +146 -0
- package/lib/engine/reranker-mmr.mjs +90 -0
- package/lib/engine/tokens.mjs +77 -0
- package/lib/entity-store.mjs +280 -0
- package/lib/env-config.mjs +69 -4
- package/lib/evals/retrieval-bench.mjs +159 -0
- package/lib/evaluator-optimizer.mjs +317 -0
- package/lib/features.mjs +159 -29
- package/lib/gates-audit.mjs +236 -0
- package/lib/git-hooks/prepare-commit-msg +58 -0
- package/lib/handoffs/cleanup.mjs +159 -0
- package/lib/handoffs/contract.mjs +162 -0
- package/lib/handoffs/inventory.mjs +120 -0
- package/lib/headhunt.mjs +28 -47
- package/lib/health-check.mjs +399 -0
- package/lib/hook-health.mjs +442 -0
- package/lib/hooks/_lib/log.mjs +82 -0
- package/lib/hooks/adaptive-lint.mjs +26 -2
- package/lib/hooks/agent-tracker.mjs +171 -14
- package/lib/hooks/audit-reads.mjs +109 -0
- package/lib/hooks/audit-trail.mjs +153 -0
- package/lib/hooks/bash-output-logger.mjs +71 -0
- package/lib/hooks/block-no-verify.mjs +41 -0
- package/lib/hooks/ci-status-check.mjs +82 -0
- package/lib/hooks/comment-lint.mjs +29 -7
- package/lib/hooks/config-protection.mjs +39 -18
- package/lib/hooks/context-watch.mjs +137 -0
- package/lib/hooks/context-window-recovery.mjs +4 -20
- package/lib/hooks/dep-audit.mjs +16 -0
- package/lib/hooks/doc-coupling-check.mjs +80 -0
- package/lib/hooks/edit-accumulator.mjs +3 -0
- package/lib/hooks/edit-error-recovery.mjs +3 -0
- package/lib/hooks/edit-guard.mjs +45 -4
- package/lib/hooks/env-check.mjs +3 -0
- package/lib/hooks/guard-bash.mjs +58 -1
- package/lib/hooks/mcp-audit.mjs +18 -20
- package/lib/hooks/mcp-health-check.mjs +36 -0
- package/lib/hooks/model-fallback.mjs +42 -56
- package/lib/hooks/policy-engine.mjs +209 -0
- package/lib/hooks/post-merge-docs-check.mjs +63 -0
- package/lib/hooks/pre-compact.mjs +4 -24
- package/lib/hooks/pre-push-gate.mjs +200 -37
- package/lib/hooks/proactive-activation.mjs +284 -0
- package/lib/hooks/read-tracker.mjs +27 -4
- package/lib/hooks/readme-age-check.mjs +77 -0
- package/lib/hooks/registry-sync.mjs +12 -5
- package/lib/hooks/scan-secrets.mjs +50 -7
- package/lib/hooks/session-optimize.mjs +311 -0
- package/lib/hooks/session-start.mjs +287 -28
- package/lib/hooks/stop-notify.mjs +236 -96
- package/lib/hooks/stop-typecheck.mjs +13 -1
- package/lib/hooks/test-watch.mjs +68 -0
- package/lib/host-capabilities.mjs +31 -10
- package/lib/init-docs.mjs +731 -291
- package/lib/init-unified.mjs +1000 -0
- package/lib/init-update.mjs +168 -0
- package/lib/init.mjs +107 -0
- package/lib/install/first-invocation.mjs +119 -0
- package/lib/install/stage-project.mjs +69 -0
- package/lib/intake/classify.mjs +278 -0
- package/lib/intake/feedback.mjs +273 -0
- package/lib/intake/filesystem-queue.mjs +158 -0
- package/lib/intake/intake-config.mjs +132 -0
- package/lib/intake/postgres-queue.mjs +198 -0
- package/lib/intake/prepare.mjs +139 -0
- package/lib/intake/queue.mjs +83 -0
- package/lib/intake/session-prelude.mjs +50 -0
- package/lib/integrations/intake-integrations.mjs +740 -0
- package/lib/intent-classifier.mjs +253 -0
- package/lib/knowledge/layout.mjs +72 -0
- package/lib/knowledge/rag.mjs +331 -0
- package/lib/knowledge/search.mjs +315 -0
- package/lib/knowledge/trends.mjs +261 -0
- package/lib/logger.mjs +85 -0
- package/lib/mcp/broker.mjs +124 -0
- package/lib/mcp/server.mjs +554 -854
- package/lib/mcp/tools/document.mjs +132 -0
- package/lib/mcp/tools/memory.mjs +209 -0
- package/lib/mcp/tools/project.mjs +349 -0
- package/lib/mcp/tools/skills.mjs +409 -0
- package/lib/mcp/tools/storage.mjs +60 -0
- package/lib/mcp/tools/telemetry.mjs +315 -0
- package/lib/mcp/tools/workflow.mjs +112 -0
- package/lib/mcp-catalog.json +54 -3
- package/lib/mcp-manager.mjs +96 -48
- package/lib/mcp-platform-config.mjs +22 -22
- package/lib/memory-stats.mjs +121 -0
- package/lib/mode-commands.mjs +124 -0
- package/lib/model-free-selector.mjs +184 -0
- package/lib/model-pricing.mjs +152 -0
- package/lib/model-registry.mjs +226 -0
- package/lib/model-router.mjs +362 -386
- package/lib/observation-store.mjs +391 -0
- package/lib/ollama-manager.mjs +428 -0
- package/lib/opencode-config.mjs +13 -3
- package/lib/opencode-runtime-plugin.mjs +70 -38
- package/lib/opencode-telemetry.mjs +64 -6
- package/lib/orchestration-policy.mjs +509 -6
- package/lib/overrides/resolver.mjs +207 -0
- package/lib/parity.mjs +147 -0
- package/lib/paths.mjs +33 -0
- package/lib/performance/generate.mjs +212 -0
- package/lib/plugin-registry.mjs +268 -0
- package/lib/policy/engine.mjs +130 -0
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-detection.mjs +129 -0
- package/lib/project-init-shared.mjs +272 -0
- package/lib/project-profile.mjs +447 -0
- package/lib/prompt-composer.js +434 -0
- package/lib/prompt-metadata.mjs +1 -1
- package/lib/provider-capabilities-anthropic.js +44 -0
- package/lib/provider-capabilities-deepseek.js +37 -0
- package/lib/provider-capabilities-generic.js +26 -0
- package/lib/provider-capabilities-google.js +47 -0
- package/lib/provider-capabilities-openai.js +45 -0
- package/lib/provider-capabilities.js +142 -0
- package/lib/providers/atlassian-confluence/index.mjs +103 -0
- package/lib/providers/atlassian-jira/index.mjs +100 -0
- package/lib/providers/auth-manager.mjs +126 -0
- package/lib/providers/circuit-breaker.mjs +124 -0
- package/lib/providers/contract.mjs +93 -0
- package/lib/providers/github/index.mjs +126 -0
- package/lib/providers/registry.mjs +184 -0
- package/lib/providers/salesforce/index.mjs +100 -0
- package/lib/providers/slack/index.mjs +80 -0
- package/lib/reflect.mjs +137 -0
- package/lib/research-lint.mjs +164 -0
- package/lib/resources/budget.mjs +259 -0
- package/lib/role-preload.mjs +21 -6
- package/lib/roles/approval-surface.mjs +54 -0
- package/lib/roles/cli.mjs +118 -0
- package/lib/roles/event-bus.mjs +79 -0
- package/lib/roles/fence.mjs +84 -0
- package/lib/roles/gateway.mjs +260 -0
- package/lib/roles/hook-emit.mjs +37 -0
- package/lib/roles/manifest.mjs +48 -0
- package/lib/roles/router.mjs +27 -0
- package/lib/runtime-pressure.mjs +360 -0
- package/lib/schema-artifact.mjs +134 -0
- package/lib/schema-infer.mjs +551 -0
- package/lib/server/auth.mjs +168 -0
- package/lib/server/chat.mjs +336 -0
- package/lib/server/cors.mjs +77 -0
- package/lib/server/csrf.mjs +91 -0
- package/lib/server/index.mjs +1927 -78
- package/lib/server/insights.mjs +765 -0
- package/lib/server/rate-limit.mjs +91 -0
- package/lib/server/static/assets/index-ab25c707.js +70 -0
- package/lib/server/static/assets/index-f0c80a2b.css +1 -0
- package/lib/server/static/index.html +12 -817
- package/lib/server/telemetry-login.mjs +108 -0
- package/lib/server/webhook.mjs +510 -0
- package/lib/service-manager.mjs +522 -58
- package/lib/services/pattern-promotion-service.mjs +167 -0
- package/lib/services/telemetry-backend.mjs +178 -0
- package/lib/session-store.mjs +374 -0
- package/lib/setup-prompts.mjs +96 -0
- package/lib/setup.mjs +523 -36
- package/lib/skills-apply.mjs +280 -0
- package/lib/skills-scope.mjs +118 -0
- package/lib/status.mjs +261 -70
- package/lib/storage/admin.mjs +355 -0
- package/lib/storage/backend.mjs +2 -1
- package/lib/storage/backup.mjs +347 -0
- package/lib/storage/embeddings-engine.mjs +133 -0
- package/lib/storage/embeddings-legacy.mjs +85 -0
- package/lib/storage/embeddings-local.mjs +108 -0
- package/lib/storage/embeddings-ollama.mjs +78 -0
- package/lib/storage/embeddings-openai.mjs +85 -0
- package/lib/storage/embeddings.mjs +92 -33
- package/lib/storage/file-lock.mjs +130 -0
- package/lib/storage/fusion.mjs +95 -0
- package/lib/storage/hybrid-query.mjs +34 -27
- package/lib/storage/migrations.mjs +187 -0
- package/lib/storage/postgres-backup.mjs +124 -0
- package/lib/storage/sql-store.mjs +5 -15
- package/lib/storage/state-source.mjs +12 -13
- package/lib/storage/store-version.mjs +115 -0
- package/lib/storage/sync.mjs +144 -35
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/storage/vector-client.mjs +286 -0
- package/lib/storage/vector-store.mjs +71 -30
- package/lib/task-graph/generate.mjs +135 -0
- package/lib/task-graph/schema.mjs +81 -0
- package/lib/task-graph/store.mjs +71 -0
- package/lib/telemetry/backends/local.mjs +62 -0
- package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
- package/lib/telemetry/backfill.mjs +180 -0
- package/lib/telemetry/eval-datasets.mjs +203 -0
- package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
- package/lib/telemetry/intent-verifications.mjs +86 -0
- package/lib/telemetry/llm-judge.mjs +350 -0
- package/lib/telemetry/model-pricing-catalog.mjs +557 -0
- package/lib/telemetry/setup.mjs +151 -0
- package/lib/telemetry/skill-calls.mjs +78 -0
- package/lib/telemetry/team-rollup.mjs +4 -4
- package/lib/token-engine.js +117 -0
- package/lib/token-estimator-anthropic.js +15 -0
- package/lib/token-estimator-deepseek.js +13 -0
- package/lib/token-estimator-default.js +13 -0
- package/lib/token-estimator-google.js +13 -0
- package/lib/token-estimator-openai.js +13 -0
- package/lib/toolkit-env.mjs +1 -1
- package/lib/tty-prompts.mjs +211 -0
- package/lib/uninstall/uninstall.mjs +423 -0
- package/lib/update.mjs +115 -0
- package/lib/upgrade.mjs +141 -0
- package/lib/validator.mjs +51 -2
- package/lib/validators/skills.mjs +142 -0
- package/lib/wireframe.mjs +422 -0
- package/lib/worker/entrypoint.mjs +241 -0
- package/lib/worker/evidence.mjs +107 -0
- package/lib/worker/run.mjs +154 -0
- package/lib/worker/trace.mjs +182 -0
- package/lib/workflow-state.mjs +14 -18
- package/package.json +21 -8
- package/personas/construct.md +53 -51
- package/platforms/claude/CLAUDE.md +1 -1
- package/platforms/claude/settings.template.json +115 -68
- package/platforms/opencode/config.template.json +3 -7
- package/rules/common/agents.md +11 -38
- package/rules/common/beads-hygiene.md +75 -0
- package/rules/common/code-review.md +11 -100
- package/rules/common/coding-style.md +5 -3
- package/rules/common/comments.md +30 -111
- package/rules/common/commit-approval.md +53 -0
- package/rules/common/cx-agent-routing.md +1 -0
- package/rules/common/development-workflow.md +23 -41
- package/rules/common/doc-ownership.md +54 -0
- package/rules/common/efficiency.md +57 -0
- package/rules/common/framing.md +76 -0
- package/rules/common/git-workflow.md +2 -4
- package/rules/common/patterns.md +3 -7
- package/rules/common/performance.md +15 -31
- package/rules/common/release-gates.md +69 -0
- package/rules/common/research.md +107 -0
- package/rules/common/security.md +6 -6
- package/rules/common/skill-composition.md +67 -0
- package/rules/common/testing.md +15 -27
- package/rules/golang/hooks.md +0 -4
- package/rules/policy/bootstrap.yaml +11 -0
- package/rules/policy/drive.yaml +8 -0
- package/rules/policy/task.yaml +9 -0
- package/rules/policy/workflow.yaml +9 -0
- package/rules/python/hooks.md +0 -4
- package/rules/swift/hooks.md +0 -4
- package/rules/typescript/hooks.md +0 -4
- package/rules/web/hooks.md +0 -2
- package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
- package/skills/ai/prompt-optimizer.md +7 -7
- package/skills/compliance/ai-disclosure.md +58 -0
- package/skills/compliance/data-privacy.md +46 -0
- package/skills/compliance/license-audit.md +40 -0
- package/skills/compliance/regulatory-review.md +61 -0
- package/skills/docs/document-ingest-workflow.md +52 -0
- package/skills/docs/evidence-ingest-workflow.md +9 -0
- package/skills/docs/init-docs.md +51 -18
- package/skills/docs/product-intelligence-workflow.md +12 -0
- package/skills/docs/product-signal-workflow.md +4 -0
- package/skills/docs/research-workflow.md +23 -8
- package/skills/docs/runbook-workflow.md +1 -1
- package/skills/operating/orchestration-reference.md +151 -0
- package/skills/roles/architect.md +5 -0
- package/skills/roles/engineer.md +32 -0
- package/skills/roles/operator.md +12 -0
- package/skills/roles/reviewer.md +23 -0
- package/skills/routing.md +1 -1
- package/templates/devcontainer/Dockerfile.devcontainer +38 -0
- package/templates/devcontainer/devcontainer.json +31 -0
- package/templates/distribution/bootstrap.ps1 +142 -0
- package/templates/distribution/bootstrap.sh +196 -0
- package/templates/distribution/run.mjs +187 -0
- package/templates/docs/adr.md +44 -8
- package/templates/docs/changelog-entry.md +43 -0
- package/templates/docs/construct_guide.md +149 -0
- package/templates/docs/evidence-brief.md +2 -2
- package/templates/docs/meta-prd.md +140 -19
- package/templates/docs/onboarding.md +57 -0
- package/templates/docs/prd.md +159 -15
- package/templates/docs/research-brief.md +4 -4
- package/templates/homebrew/construct.rb +67 -0
- package/langfuse/docker-compose.yml +0 -82
- package/lib/eval-harness.mjs +0 -59
- package/lib/hooks/bootstrap-guard.mjs +0 -90
- package/lib/hooks/console-warn.mjs +0 -43
- package/lib/hooks/continuation-enforcer.mjs +0 -72
- package/lib/hooks/drive-guard.mjs +0 -89
- package/lib/hooks/mcp-task-scope.mjs +0 -47
- package/lib/hooks/task-completed-guard.mjs +0 -43
- package/lib/hooks/teammate-idle-guard.mjs +0 -54
- package/lib/hooks/workflow-guard.mjs +0 -62
- package/lib/prompt-composer.mjs +0 -196
- package/lib/review.mjs +0 -429
- package/lib/server/static/app.js +0 -841
- package/lib/telemetry/langfuse-model-sync.mjs +0 -270
- package/rules/common/hooks.md +0 -35
- package/rules/zh/README.md +0 -113
- package/rules/zh/agents.md +0 -55
- package/rules/zh/code-review.md +0 -129
- package/rules/zh/coding-style.md +0 -53
- package/rules/zh/development-workflow.md +0 -49
- package/rules/zh/git-workflow.md +0 -29
- package/rules/zh/hooks.md +0 -35
- package/rules/zh/patterns.md +0 -36
- package/rules/zh/performance.md +0 -60
- package/rules/zh/security.md +0 -34
- package/rules/zh/testing.md +0 -34
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/docs-verify.mjs — comprehensive documentation validation.
|
|
4
|
+
*
|
|
5
|
+
* Checks:
|
|
6
|
+
* 1. README.md exists and has basic sections
|
|
7
|
+
* 2. AGENTS.md exists and has required sections
|
|
8
|
+
* 3. plan.md exists and is current (< 7 days)
|
|
9
|
+
* 4. .cx/context.md exists and is current (< 7 days)
|
|
10
|
+
* 5. docs/README.md exists (if docs system initialized)
|
|
11
|
+
* 6. Required documentation lanes exist (if specified in init)
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node lib/docs-verify.mjs [--quick] [--fix] [--cwd=path]
|
|
15
|
+
* construct docs:verify [--quick] [--fix]
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { statSync, readFileSync, existsSync } from "node:fs";
|
|
21
|
+
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
const quickMode = args.includes("--quick");
|
|
24
|
+
const fixMode = args.includes("--fix");
|
|
25
|
+
const stagedMode = args.includes("--staged");
|
|
26
|
+
const cwdArg = args.find(arg => arg.startsWith("--cwd="));
|
|
27
|
+
const cwd = cwdArg ? path.resolve(cwdArg.split("=")[1]) : process.cwd();
|
|
28
|
+
|
|
29
|
+
const errors = [];
|
|
30
|
+
const warnings = [];
|
|
31
|
+
const fixed = [];
|
|
32
|
+
|
|
33
|
+
if (stagedMode) {
|
|
34
|
+
const { execSync } = await import("node:child_process");
|
|
35
|
+
let stagedList = "";
|
|
36
|
+
try {
|
|
37
|
+
stagedList = execSync("git diff --cached --name-only --diff-filter=ACMR", {
|
|
38
|
+
cwd, timeout: 3000, stdio: ["ignore", "pipe", "ignore"],
|
|
39
|
+
}).toString();
|
|
40
|
+
} catch {
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
const files = stagedList.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
44
|
+
const codeRe = /^(?:lib|bin|src|app)\/.*\.(?:m?js|jsx?|tsx?|cjs)$/;
|
|
45
|
+
const docRe = /^(?:CHANGELOG\.md|docs\/.+\.md|\.cx\/context\.(?:md|json))$/;
|
|
46
|
+
const changedCode = files.filter((f) => codeRe.test(f));
|
|
47
|
+
const changedDocs = files.filter((f) => docRe.test(f));
|
|
48
|
+
if (changedCode.length === 0) process.exit(0);
|
|
49
|
+
if (changedDocs.length > 0) process.exit(0);
|
|
50
|
+
|
|
51
|
+
if (process.env.CONSTRUCT_SKIP_DOCS === "1") {
|
|
52
|
+
process.stderr.write(
|
|
53
|
+
`[docs:verify --staged] CONSTRUCT_SKIP_DOCS=1 — skipping doc-coupling check for ${changedCode.length} code file(s).\n`,
|
|
54
|
+
);
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sample = changedCode.slice(0, 6).map((f) => ` - ${f}`).join("\n");
|
|
59
|
+
process.stderr.write(
|
|
60
|
+
`[docs:verify --staged] ${changedCode.length} code file(s) staged but no CHANGELOG.md / docs/* / .cx/context.* updates.\n` +
|
|
61
|
+
`${sample}${changedCode.length > 6 ? `\n ... (${changedCode.length - 6} more)` : ""}\n` +
|
|
62
|
+
`Update docs in this commit, or override with CONSTRUCT_SKIP_DOCS=1.\n`,
|
|
63
|
+
);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function checkExists(filePath, description, isCritical = true) {
|
|
68
|
+
if (!fs.existsSync(filePath)) {
|
|
69
|
+
const msg = `Missing ${description}: ${path.relative(cwd, filePath)}`;
|
|
70
|
+
if (isCritical) errors.push(msg);
|
|
71
|
+
else warnings.push(msg);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function checkFileAge(filePath, maxDays, description, isCritical = true) {
|
|
78
|
+
if (!fs.existsSync(filePath)) return false;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const stats = statSync(filePath);
|
|
82
|
+
const ageDays = (Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60 * 24);
|
|
83
|
+
|
|
84
|
+
if (ageDays > maxDays) {
|
|
85
|
+
const msg = `Stale ${description}: ${path.relative(cwd, filePath)} (last modified ${Math.floor(ageDays)} days ago, max ${maxDays} days)`;
|
|
86
|
+
if (isCritical) errors.push(msg);
|
|
87
|
+
else warnings.push(msg);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
const msg = `Cannot read ${description}: ${path.relative(cwd, filePath)} (${err.message})`;
|
|
93
|
+
if (isCritical) errors.push(msg);
|
|
94
|
+
else warnings.push(msg);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function checkReadmeBasicSections(readmePath) {
|
|
100
|
+
if (!fs.existsSync(readmePath)) return false;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const content = readFileSync(readmePath, "utf8").toLowerCase();
|
|
104
|
+
const hasGettingStarted = content.includes("getting started") || content.includes("## getting started");
|
|
105
|
+
const hasUsage = content.includes("usage") || content.includes("## usage");
|
|
106
|
+
const hasConstruct = content.includes("construct");
|
|
107
|
+
|
|
108
|
+
if (!hasGettingStarted) {
|
|
109
|
+
warnings.push(`README.md missing "Getting Started" section: ${path.relative(cwd, readmePath)}`);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (!hasUsage) {
|
|
113
|
+
warnings.push(`README.md missing "Usage" section: ${path.relative(cwd, readmePath)}`);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (!hasConstruct) {
|
|
117
|
+
warnings.push(`README.md doesn't mention Construct: ${path.relative(cwd, readmePath)}`);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return true;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
warnings.push(`Cannot read README.md: ${path.relative(cwd, readmePath)} (${err.message})`);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function checkAgentsRequiredSections(agentsPath) {
|
|
129
|
+
if (!fs.existsSync(agentsPath)) return false;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const content = readFileSync(agentsPath, "utf8").toLowerCase();
|
|
133
|
+
const requiredSections = [
|
|
134
|
+
"operating hierarchy",
|
|
135
|
+
"start-of-session rules",
|
|
136
|
+
"maintenance rules",
|
|
137
|
+
"end-of-session rules",
|
|
138
|
+
"verification rules"
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const missingSections = requiredSections.filter(section => !content.includes(section));
|
|
142
|
+
|
|
143
|
+
if (missingSections.length > 0) {
|
|
144
|
+
warnings.push(`AGENTS.md missing sections: ${missingSections.join(", ")} in ${path.relative(cwd, agentsPath)}`);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return true;
|
|
149
|
+
} catch (err) {
|
|
150
|
+
warnings.push(`Cannot read AGENTS.md: ${path.relative(cwd, agentsPath)} (${err.message})`);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function checkPlanLinkedToIssues(planPath) {
|
|
156
|
+
if (!fs.existsSync(planPath)) return false;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const content = readFileSync(planPath, "utf8");
|
|
160
|
+
// Check if plan mentions any issue tracker format (construct-xxx, beads issue, etc.)
|
|
161
|
+
const hasIssueRef = /construct-\w{3}|bd\s+issue|beads\s+issue|tracker\s+link|issue:\s*\w+/i.test(content);
|
|
162
|
+
|
|
163
|
+
if (!hasIssueRef) {
|
|
164
|
+
warnings.push(`plan.md doesn't appear linked to tracker issues: ${path.relative(cwd, planPath)}`);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return true;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
warnings.push(`Cannot read plan.md: ${path.relative(cwd, planPath)} (${err.message})`);
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function checkContextHasProgress(contextPath) {
|
|
176
|
+
if (!fs.existsSync(contextPath)) return false;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const content = readFileSync(contextPath, "utf8").toLowerCase();
|
|
180
|
+
const hasProgress = content.includes("what was in progress") || content.includes("## what was in progress");
|
|
181
|
+
const hasOpenIssues = content.includes("open issues") || content.includes("## open issues");
|
|
182
|
+
|
|
183
|
+
if (!hasProgress) {
|
|
184
|
+
warnings.push(`.cx/context.md missing "What was in progress" section: ${path.relative(cwd, contextPath)}`);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (!hasOpenIssues) {
|
|
188
|
+
warnings.push(`.cx/context.md missing "Open issues" section: ${path.relative(cwd, contextPath)}`);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return true;
|
|
193
|
+
} catch (err) {
|
|
194
|
+
warnings.push(`Cannot read .cx/context.md: ${path.relative(cwd, contextPath)} (${err.message})`);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function checkDocsSystem(docsPath) {
|
|
200
|
+
if (!fs.existsSync(docsPath)) {
|
|
201
|
+
// docs/ directory doesn't exist, which is fine if project doesn't use docs system
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const docsReadmePath = path.join(docsPath, "README.md");
|
|
207
|
+
if (!fs.existsSync(docsReadmePath)) {
|
|
208
|
+
warnings.push(`docs/ directory exists but docs/README.md is missing: ${path.relative(cwd, docsReadmePath)}`);
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check if docs/ has subdirectories that should have README.md
|
|
213
|
+
const entries = fs.readdirSync(docsPath, { withFileTypes: true });
|
|
214
|
+
const dirs = entries.filter(entry => entry.isDirectory() && !entry.name.startsWith("."));
|
|
215
|
+
|
|
216
|
+
for (const dir of dirs) {
|
|
217
|
+
const dirReadmePath = path.join(docsPath, dir.name, "README.md");
|
|
218
|
+
if (!fs.existsSync(dirReadmePath)) {
|
|
219
|
+
warnings.push(`docs/${dir.name}/ directory exists but docs/${dir.name}/README.md is missing: ${path.relative(cwd, dirReadmePath)}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return true;
|
|
224
|
+
} catch (err) {
|
|
225
|
+
warnings.push(`Cannot inspect docs/ directory: ${path.relative(cwd, docsPath)} (${err.message})`);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function attemptFixMissingReadme() {
|
|
231
|
+
const readmePath = path.join(cwd, "README.md");
|
|
232
|
+
if (fs.existsSync(readmePath)) return false;
|
|
233
|
+
|
|
234
|
+
const projectName = path.basename(cwd);
|
|
235
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
236
|
+
let name = projectName;
|
|
237
|
+
|
|
238
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
239
|
+
try {
|
|
240
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
241
|
+
name = pkg.name || projectName;
|
|
242
|
+
} catch {}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const content = `# ${name}
|
|
246
|
+
|
|
247
|
+
## Getting Started
|
|
248
|
+
|
|
249
|
+
This project uses [Construct](https://github.com/geraldmaron/construct) for agentic software development.
|
|
250
|
+
|
|
251
|
+
## Usage
|
|
252
|
+
|
|
253
|
+
From inside OpenCode, Claude Code, or similar agent surfaces:
|
|
254
|
+
|
|
255
|
+
\`\`\`text
|
|
256
|
+
@construct build the feature and ship it when it's verified
|
|
257
|
+
@construct fix the bug
|
|
258
|
+
@construct review the changes before release
|
|
259
|
+
\`\`\`
|
|
260
|
+
|
|
261
|
+
Construct routes work across specialists, maintains project state, and ensures quality gates pass before shipping.
|
|
262
|
+
|
|
263
|
+
## Project Structure
|
|
264
|
+
|
|
265
|
+
- \`AGENTS.md\` — Operating contract for AI agents
|
|
266
|
+
- \`plan.md\` — Current implementation plan
|
|
267
|
+
- \`.cx/context.md\` — Session context and handoff state
|
|
268
|
+
- \`docs/\` — Documentation system (if initialized with \`construct init --docs-preset\`)
|
|
269
|
+
|
|
270
|
+
## Development
|
|
271
|
+
|
|
272
|
+
\`\`\`bash
|
|
273
|
+
# Initialize project with Construct
|
|
274
|
+
construct init --docs-preset=lean
|
|
275
|
+
|
|
276
|
+
# Check documentation quality
|
|
277
|
+
construct docs:verify
|
|
278
|
+
|
|
279
|
+
# Run tests
|
|
280
|
+
npm test
|
|
281
|
+
\`\`\`
|
|
282
|
+
|
|
283
|
+
## CI Enforcement
|
|
284
|
+
|
|
285
|
+
This project enforces documentation quality:
|
|
286
|
+
- README.md must exist and be current
|
|
287
|
+
- AGENTS.md must have required sections
|
|
288
|
+
- plan.md must be updated weekly
|
|
289
|
+
- .cx/context.md must track active work`;
|
|
290
|
+
|
|
291
|
+
fs.writeFileSync(readmePath, content, "utf8");
|
|
292
|
+
fixed.push(`Created README.md: ${path.relative(cwd, readmePath)}`);
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function attemptFixStaleContext() {
|
|
297
|
+
const contextPath = path.join(cwd, ".cx", "context.md");
|
|
298
|
+
if (!fs.existsSync(contextPath)) return false;
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const content = readFileSync(contextPath, "utf8");
|
|
302
|
+
const now = new Date().toISOString().split("T")[0];
|
|
303
|
+
|
|
304
|
+
// Add or update timestamp at top
|
|
305
|
+
const lines = content.split("\n");
|
|
306
|
+
let foundTimestamp = false;
|
|
307
|
+
|
|
308
|
+
for (let i = 0; i < lines.length; i++) {
|
|
309
|
+
if (lines[i].toLowerCase().includes("last saved") || lines[i].toLowerCase().includes("last updated")) {
|
|
310
|
+
lines[i] = `Last saved: ${now}`;
|
|
311
|
+
foundTimestamp = true;
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!foundTimestamp && lines.length > 0) {
|
|
317
|
+
lines.splice(1, 0, `Last saved: ${now}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
fs.writeFileSync(contextPath, lines.join("\n"), "utf8");
|
|
321
|
+
fixed.push(`Updated .cx/context.md timestamp: ${path.relative(cwd, contextPath)}`);
|
|
322
|
+
return true;
|
|
323
|
+
} catch (err) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function main() {
|
|
329
|
+
console.log(`\n📄 Documentation verification for ${path.relative(process.cwd(), cwd) || "."}\n`);
|
|
330
|
+
|
|
331
|
+
// Critical checks (fail CI)
|
|
332
|
+
checkExists(path.join(cwd, "README.md"), "README.md", true);
|
|
333
|
+
checkExists(path.join(cwd, "AGENTS.md"), "AGENTS.md", true);
|
|
334
|
+
checkExists(path.join(cwd, ".cx", "context.md"), ".cx/context.md", true);
|
|
335
|
+
|
|
336
|
+
// plan.md is a local working document — gitignored, not committed.
|
|
337
|
+
// Verify it's healthy when present, but don't fail when it's absent.
|
|
338
|
+
const planExists = fs.existsSync(path.join(cwd, "plan.md"));
|
|
339
|
+
|
|
340
|
+
// Age checks (fail CI if stale)
|
|
341
|
+
if (planExists) checkFileAge(path.join(cwd, "plan.md"), 7, "plan.md", true);
|
|
342
|
+
checkFileAge(path.join(cwd, ".cx", "context.md"), 7, ".cx/context.md", true);
|
|
343
|
+
|
|
344
|
+
if (!quickMode) {
|
|
345
|
+
// Detailed checks (warnings only)
|
|
346
|
+
checkReadmeBasicSections(path.join(cwd, "README.md"));
|
|
347
|
+
checkAgentsRequiredSections(path.join(cwd, "AGENTS.md"));
|
|
348
|
+
if (planExists) checkPlanLinkedToIssues(path.join(cwd, "plan.md"));
|
|
349
|
+
checkContextHasProgress(path.join(cwd, ".cx", "context.md"));
|
|
350
|
+
checkDocsSystem(path.join(cwd, "docs"));
|
|
351
|
+
|
|
352
|
+
// Age warning for README.md (30 days)
|
|
353
|
+
checkFileAge(path.join(cwd, "README.md"), 30, "README.md", false);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Attempt fixes if requested
|
|
357
|
+
if (fixMode) {
|
|
358
|
+
if (errors.some(e => e.includes("Missing README.md"))) {
|
|
359
|
+
attemptFixMissingReadme();
|
|
360
|
+
}
|
|
361
|
+
if (errors.some(e => e.includes("Stale .cx/context.md"))) {
|
|
362
|
+
attemptFixStaleContext();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Output results
|
|
367
|
+
if (fixed.length > 0) {
|
|
368
|
+
console.log("✅ Fixed:");
|
|
369
|
+
for (const fix of fixed) {
|
|
370
|
+
console.log(` • ${fix}`);
|
|
371
|
+
}
|
|
372
|
+
console.log();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
376
|
+
console.log("🎉 All documentation checks passed!");
|
|
377
|
+
process.exit(0);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (warnings.length > 0) {
|
|
381
|
+
console.log("⚠️ Warnings:");
|
|
382
|
+
for (const warning of warnings) {
|
|
383
|
+
console.log(` • ${warning}`);
|
|
384
|
+
}
|
|
385
|
+
console.log();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (errors.length > 0) {
|
|
389
|
+
console.log("❌ Critical errors (CI will fail):");
|
|
390
|
+
for (const error of errors) {
|
|
391
|
+
console.log(` • ${error}`);
|
|
392
|
+
}
|
|
393
|
+
console.log();
|
|
394
|
+
console.log("💡 Run `construct docs:verify --fix` to attempt automatic fixes");
|
|
395
|
+
console.log("💡 Run `construct init:update` to update project to current standards");
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
console.log("✅ Documentation validation passed with warnings");
|
|
400
|
+
process.exit(0);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
404
|
+
main();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export {
|
|
408
|
+
checkExists,
|
|
409
|
+
checkFileAge,
|
|
410
|
+
checkReadmeBasicSections,
|
|
411
|
+
checkAgentsRequiredSections,
|
|
412
|
+
checkPlanLinkedToIssues,
|
|
413
|
+
checkContextHasProgress,
|
|
414
|
+
checkDocsSystem,
|
|
415
|
+
attemptFixMissingReadme,
|
|
416
|
+
attemptFixStaleContext
|
|
417
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/doctor/audit.mjs — append-only audit log for L0 doctor actions.
|
|
3
|
+
*
|
|
4
|
+
* Every deterministic action the doctor takes (restart, kill, rotate, escalate)
|
|
5
|
+
* lands here as a single JSONL line. The audit log is the user's window into
|
|
6
|
+
* what the L0 layer did while they weren't looking — and the basis for rolling
|
|
7
|
+
* back a misfire if rules are wrong.
|
|
8
|
+
*
|
|
9
|
+
* Path: ~/.cx/doctor-log.jsonl (overridable via CONSTRUCT_DOCTOR_ROOT for tests).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
const MAX_LINES = 2000;
|
|
17
|
+
|
|
18
|
+
function rootDir() {
|
|
19
|
+
return process.env.CONSTRUCT_DOCTOR_ROOT || join(homedir(), '.cx');
|
|
20
|
+
}
|
|
21
|
+
function logPath() {
|
|
22
|
+
return join(rootDir(), 'doctor-log.jsonl');
|
|
23
|
+
}
|
|
24
|
+
function ensureDir() {
|
|
25
|
+
const r = rootDir();
|
|
26
|
+
if (!existsSync(r)) mkdirSync(r, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function record({ kind, watcher, action = null, target = null, result = 'ok', summary, context = null }) {
|
|
30
|
+
ensureDir();
|
|
31
|
+
const line = {
|
|
32
|
+
ts: Date.now(),
|
|
33
|
+
kind,
|
|
34
|
+
watcher,
|
|
35
|
+
action,
|
|
36
|
+
target,
|
|
37
|
+
result,
|
|
38
|
+
summary: String(summary || '').slice(0, 2048),
|
|
39
|
+
context,
|
|
40
|
+
};
|
|
41
|
+
appendFileSync(logPath(), JSON.stringify(line) + '\n');
|
|
42
|
+
rotate();
|
|
43
|
+
return line;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function recent({ since = 0, watcher, kind, limit = 200 } = {}) {
|
|
47
|
+
const p = logPath();
|
|
48
|
+
if (!existsSync(p)) return [];
|
|
49
|
+
const lines = readFileSync(p, 'utf8').split('\n').filter(Boolean);
|
|
50
|
+
const out = [];
|
|
51
|
+
for (let i = lines.length - 1; i >= 0 && out.length < limit; i--) {
|
|
52
|
+
let e;
|
|
53
|
+
try { e = JSON.parse(lines[i]); } catch { continue; }
|
|
54
|
+
if (since && e.ts < since) break;
|
|
55
|
+
if (watcher && e.watcher !== watcher) continue;
|
|
56
|
+
if (kind && e.kind !== kind) continue;
|
|
57
|
+
out.push(e);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function rotate() {
|
|
63
|
+
try {
|
|
64
|
+
const p = logPath();
|
|
65
|
+
const raw = readFileSync(p, 'utf8').split('\n').filter(Boolean);
|
|
66
|
+
if (raw.length <= MAX_LINES) return;
|
|
67
|
+
writeFileSync(p, raw.slice(raw.length - MAX_LINES).join('\n') + '\n');
|
|
68
|
+
} catch { /* best effort */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const _paths = { rootDir, logPath };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/doctor/cli.mjs — `construct doctor` handler.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* status — show running state + last tick per watcher
|
|
6
|
+
* watch — run the daemon in foreground (debugging)
|
|
7
|
+
* stop — send SIGTERM to a running daemon
|
|
8
|
+
* logs — print recent audit log entries (default 50; --watcher=X to filter)
|
|
9
|
+
* tick — run all watchers once and exit (useful in tests)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { readState } from './index.mjs';
|
|
17
|
+
import { recent } from './audit.mjs';
|
|
18
|
+
|
|
19
|
+
function fmtTime(ts) {
|
|
20
|
+
if (!ts) return '(never)';
|
|
21
|
+
return new Date(ts).toISOString().replace('T', ' ').slice(0, 19);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ageStr(ts) {
|
|
25
|
+
if (!ts) return '';
|
|
26
|
+
const sec = Math.floor((Date.now() - ts) / 1000);
|
|
27
|
+
if (sec < 60) return `${sec}s ago`;
|
|
28
|
+
if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
|
|
29
|
+
return `${Math.floor(sec / 3600)}h ago`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runCli(args) {
|
|
33
|
+
const sub = args[0] || 'status';
|
|
34
|
+
|
|
35
|
+
if (sub === 'status') {
|
|
36
|
+
const state = readState();
|
|
37
|
+
if (!state) { console.log('doctor: not running'); return 0; }
|
|
38
|
+
console.log(`doctor: running (pid ${state.pid}, started ${fmtTime(state.startedAt)})`);
|
|
39
|
+
console.log(`state updated: ${fmtTime(state.updatedAt)} (${ageStr(state.updatedAt)})`);
|
|
40
|
+
console.log('watchers:');
|
|
41
|
+
for (const name of (state.watchers || [])) {
|
|
42
|
+
const last = recent({ watcher: name, limit: 1 })[0];
|
|
43
|
+
console.log(` ${name.padEnd(20)} last entry: ${last ? fmtTime(last.ts) + ' (' + ageStr(last.ts) + ')' : '(none yet)'}`);
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (sub === 'logs') {
|
|
49
|
+
const watcherArg = args.find((a) => a.startsWith('--watcher='))?.split('=')[1];
|
|
50
|
+
const limitArg = parseInt(args.find((a) => a.startsWith('--limit='))?.split('=')[1] || '50', 10);
|
|
51
|
+
const entries = recent({ watcher: watcherArg, limit: limitArg });
|
|
52
|
+
if (entries.length === 0) { console.log('no audit entries'); return 0; }
|
|
53
|
+
for (const e of entries.reverse()) {
|
|
54
|
+
const action = e.action ? `${e.action}` : e.kind;
|
|
55
|
+
const tgt = e.target ? ` [${e.target}]` : '';
|
|
56
|
+
console.log(`${fmtTime(e.ts)} ${e.watcher.padEnd(18)} ${action.padEnd(12)}${tgt} ${e.result || ''} ${e.summary || ''}`);
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (sub === 'stop') {
|
|
62
|
+
const state = readState();
|
|
63
|
+
if (!state) { console.log('doctor: not running'); return 0; }
|
|
64
|
+
try { process.kill(state.pid, 'SIGTERM'); console.log(`doctor: SIGTERM sent to pid ${state.pid}`); return 0; }
|
|
65
|
+
catch (err) { console.error(`failed to signal pid ${state.pid}: ${err.message}`); return 1; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (sub === 'watch') {
|
|
69
|
+
process.env.CONSTRUCT_DOCTOR_VERBOSE = '1';
|
|
70
|
+
const { start } = await import('./index.mjs');
|
|
71
|
+
await start();
|
|
72
|
+
return new Promise(() => { /* daemon loop runs until signal */ });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (sub === 'report') {
|
|
76
|
+
const { runReport } = await import('./report.mjs');
|
|
77
|
+
console.log(await runReport(args.slice(1)));
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (sub === 'tick') {
|
|
82
|
+
const watchers = await Promise.all([
|
|
83
|
+
import('./watchers/disk.mjs'),
|
|
84
|
+
import('./watchers/cost.mjs'),
|
|
85
|
+
import('./watchers/process-pressure.mjs'),
|
|
86
|
+
import('./watchers/service-health.mjs'),
|
|
87
|
+
import('./watchers/bd-watch.mjs'),
|
|
88
|
+
]);
|
|
89
|
+
for (const w of watchers) {
|
|
90
|
+
const r = await w.tick();
|
|
91
|
+
console.log(`${w.name.padEnd(20)} actions=${r.actions?.length || 0} escalations=${r.escalations?.length || 0}`);
|
|
92
|
+
}
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.error(`Unknown doctor subcommand: ${sub}`);
|
|
97
|
+
console.error('Usage: construct doctor [status|watch|stop|logs|tick]');
|
|
98
|
+
return 2;
|
|
99
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/doctor/escalate.mjs — bridge from L0 watchers to L1 personas.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the role-framework gateway so watchers stay decoupled from it. Every
|
|
5
|
+
* escalation is also recorded in the doctor audit log so the L0 timeline is
|
|
6
|
+
* complete on its own.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { record } from './audit.mjs';
|
|
10
|
+
|
|
11
|
+
export async function escalate({ watcher, eventType, summary, context = null }) {
|
|
12
|
+
let result;
|
|
13
|
+
try {
|
|
14
|
+
const { recordAndMaybeInvoke } = await import('../roles/gateway.mjs');
|
|
15
|
+
result = await recordAndMaybeInvoke(eventType, { summary, context });
|
|
16
|
+
} catch (err) {
|
|
17
|
+
result = { recorded: false, escalated: false, reason: 'gateway-load-failed', error: String(err) };
|
|
18
|
+
}
|
|
19
|
+
record({
|
|
20
|
+
kind: 'escalate',
|
|
21
|
+
watcher,
|
|
22
|
+
action: 'role-event',
|
|
23
|
+
target: eventType,
|
|
24
|
+
result: result.escalated ? 'escalated' : (result.recorded ? 'recorded' : 'failed'),
|
|
25
|
+
summary,
|
|
26
|
+
context: { ...result, originalContext: context },
|
|
27
|
+
});
|
|
28
|
+
return result;
|
|
29
|
+
}
|