@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,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/dispatch-batch.js — Batch multiplexer for parallel agents.
|
|
3
|
+
*
|
|
4
|
+
* When orchestration policy returns a dispatch plan with parallel: true:
|
|
5
|
+
* 1. Check all agents use the same model tier AND same provider family.
|
|
6
|
+
* 2. Compose one shared system message (deduplicated static fragments).
|
|
7
|
+
* 3. Append N per-agent task suffixes as separate user messages.
|
|
8
|
+
* 4. Request structured JSON output with agent names as keys.
|
|
9
|
+
* 5. Fall back to sequential dispatch if tiers differ.
|
|
10
|
+
*
|
|
11
|
+
* Only activates when dispatchPlan.someParallel === true AND agents share the same model.
|
|
12
|
+
*/
|
|
13
|
+
import { composePrompt } from './prompt-composer.js';
|
|
14
|
+
import { resolveProviderCapabilities } from './provider-capabilities.js';
|
|
15
|
+
import { estimateTokensSync } from './token-engine.js';
|
|
16
|
+
|
|
17
|
+
const MAX_BATCH_AGENTS = 5; // limit batch size for token budget
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if a dispatch plan can be batched.
|
|
21
|
+
*
|
|
22
|
+
* @param {Array} agents - [{ name, ... }]
|
|
23
|
+
* @param {object} dispatchPlan
|
|
24
|
+
* @returns {{ canBatch: boolean, reason: string, sharedModel: string }}
|
|
25
|
+
*/
|
|
26
|
+
export function canBatch(agents, dispatchPlan) {
|
|
27
|
+
if (!dispatchPlan?.someParallel) {
|
|
28
|
+
return { canBatch: false, reason: 'Dispatch plan does not request parallel execution' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!Array.isArray(agents) || agents.length < 2) {
|
|
32
|
+
return { canBatch: false, reason: 'Need at least 2 agents for batch' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (agents.length > MAX_BATCH_AGENTS) {
|
|
36
|
+
return { canBatch: false, reason: `Batch size ${agents.length} exceeds max ${MAX_BATCH_AGENTS}` };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check all agents share the same model tier
|
|
40
|
+
const tiers = new Set();
|
|
41
|
+
const providers = new Set();
|
|
42
|
+
|
|
43
|
+
for (const agent of agents) {
|
|
44
|
+
tiers.add(agent.tier || 'standard');
|
|
45
|
+
providers.add(agent.provider || 'unknown');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (tiers.size > 1) {
|
|
49
|
+
return { canBatch: false, reason: `Multiple tiers: ${[...tiers].join(', ')}` };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (providers.size > 1) {
|
|
53
|
+
return { canBatch: false, reason: `Multiple providers: ${[...providers].join(', ')}` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const sharedModel = agents[0].modelId || '';
|
|
57
|
+
return { canBatch: true, reason: null, sharedModel, sharedTier: [...tiers][0] };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build a batch prompt for parallel agents.
|
|
62
|
+
*
|
|
63
|
+
* @param {Array} agents - [{ name, task, ... }]
|
|
64
|
+
* @param {object} options - { sharedModel, rootDir, ... }]
|
|
65
|
+
* @returns {Promise<{ system, messages, expectedTokens }>}
|
|
66
|
+
*/
|
|
67
|
+
export async function buildBatchPrompt(agents, {
|
|
68
|
+
rootDir = process.cwd(),
|
|
69
|
+
...opts
|
|
70
|
+
} = {}) {
|
|
71
|
+
if (!agents?.length) return null;
|
|
72
|
+
|
|
73
|
+
// Compose prompt for first agent (the template)
|
|
74
|
+
const first = await composePrompt(agents[0].name, {
|
|
75
|
+
rootDir,
|
|
76
|
+
task: agents[0].task,
|
|
77
|
+
intent: agents[0].intent,
|
|
78
|
+
workCategory: agents[0].workCategory,
|
|
79
|
+
...opts,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Extract shared system from first agent
|
|
83
|
+
const sharedSystem = first.system || '';
|
|
84
|
+
const staticEndIndex = first.staticEndIndex;
|
|
85
|
+
|
|
86
|
+
// Build per-agent suffixes
|
|
87
|
+
const agentSuffixes = [];
|
|
88
|
+
let totalSuffixTokens = 0;
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < agents.length; i++) {
|
|
91
|
+
const agent = agents[i];
|
|
92
|
+
const agentPrompt = await composePrompt(agent.name, {
|
|
93
|
+
rootDir,
|
|
94
|
+
task: agent.task,
|
|
95
|
+
intent: agent.intent,
|
|
96
|
+
workCategory: agent.workCategory,
|
|
97
|
+
injectLearnedPatterns: i === 0, // only inject once
|
|
98
|
+
...opts,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// The dynamic part (after system)
|
|
102
|
+
const suffix = agentPrompt.messages?.[1]?.content || agentPrompt.system || '';
|
|
103
|
+
const suffixTokens = estimateTokensSync(suffix, { modelId: opts.sharedModel });
|
|
104
|
+
|
|
105
|
+
agentSuffixes.push({
|
|
106
|
+
agent: agent.name,
|
|
107
|
+
content: suffix,
|
|
108
|
+
tokens: suffixTokens,
|
|
109
|
+
});
|
|
110
|
+
totalSuffixTokens += suffixTokens;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const systemTokens = estimateTokensSync(sharedSystem, { modelId: opts.sharedModel });
|
|
114
|
+
const totalTokens = systemTokens + totalSuffixTokens;
|
|
115
|
+
|
|
116
|
+
// Build messages: one system + N user messages
|
|
117
|
+
const messages = [
|
|
118
|
+
{ role: 'system', content: sharedSystem },
|
|
119
|
+
...agentSuffixes.map(a => ({
|
|
120
|
+
role: 'user',
|
|
121
|
+
content: `Agent: ${a.agent}\n\n${a.content}`,
|
|
122
|
+
})),
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
system: sharedSystem,
|
|
127
|
+
messages,
|
|
128
|
+
expectedTokens: totalTokens,
|
|
129
|
+
agentSuffixes,
|
|
130
|
+
canSend: totalTokens < 30000, // conservative limit for batch
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Parse a structured batch response.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} responseText - LLM response with JSON
|
|
138
|
+
* @param {Array} agentNames
|
|
139
|
+
* @returns {object} - { agentName: responseText }
|
|
140
|
+
*/
|
|
141
|
+
export function parseBatchResponse(responseText, agentNames) {
|
|
142
|
+
if (!responseText || !Array.isArray(agentNames)) return {};
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// Try to parse as JSON
|
|
146
|
+
const parsed = JSON.parse(responseText);
|
|
147
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
148
|
+
// Check if response has agent keys
|
|
149
|
+
const result = {};
|
|
150
|
+
for (const name of agentNames) {
|
|
151
|
+
result[name] = parsed[name] || parsed[name.replace(/^cx-/, '')] || '';
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
// Not JSON — return full text as fallback
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Fallback: split by agent headers
|
|
160
|
+
const result = {};
|
|
161
|
+
const lines = responseText.split('\n');
|
|
162
|
+
let currentAgent = null;
|
|
163
|
+
let currentText = [];
|
|
164
|
+
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
const agentMatch = line.match(/^Agent:\s*(cx-[^\s]+)/);
|
|
167
|
+
if (agentMatch) {
|
|
168
|
+
if (currentAgent) {
|
|
169
|
+
result[currentAgent] = currentText.join('\n').trim();
|
|
170
|
+
}
|
|
171
|
+
currentAgent = agentMatch[1];
|
|
172
|
+
currentText = [];
|
|
173
|
+
} else {
|
|
174
|
+
currentText.push(line);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (currentAgent) {
|
|
179
|
+
result[currentAgent] = currentText.join('\n').trim();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return result;
|
|
183
|
+
}
|
package/lib/distill.mjs
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* lib/distill.mjs —
|
|
3
|
+
* lib/distill.mjs — Query-focused document distillation for large context windows.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Chunks local files, scores relevance against a query, and returns the top-k
|
|
6
|
+
* evidence blocks with citations. Backs the MCP distill tool and the RAG
|
|
7
|
+
* knowledge layer for shrinking context before LLM calls.
|
|
6
8
|
*/
|
|
7
9
|
import { readdirSync, statSync, readFileSync, writeFileSync, existsSync, mkdirSync, openSync, readSync, closeSync } from 'fs';
|
|
8
10
|
import { join, extname, relative, dirname, basename } from 'path';
|
|
9
11
|
import { execSync, spawnSync } from 'child_process';
|
|
10
12
|
import { homedir } from 'os';
|
|
11
13
|
import { getActiveOverlays } from './headhunt.mjs';
|
|
14
|
+
import {
|
|
15
|
+
EXTRACTABLE_DOCUMENT_EXTS,
|
|
16
|
+
UTF8_TEXT_EXTS,
|
|
17
|
+
extractDocumentText as extractSharedDocumentText,
|
|
18
|
+
} from './document-extract.mjs';
|
|
12
19
|
|
|
13
|
-
const TEXT_EXTS =
|
|
14
|
-
'.md', '.txt', '.rst', '.adoc', '.json', '.yaml', '.yml', '.toml',
|
|
15
|
-
'.js', '.mjs', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs', '.sh', '.bash',
|
|
16
|
-
'.html', '.css', '.csv', '.xml', '.env', '.env.example', '.conf', '.ini', '.tf', '.hcl',
|
|
17
|
-
]);
|
|
20
|
+
const TEXT_EXTS = EXTRACTABLE_DOCUMENT_EXTS;
|
|
18
21
|
|
|
19
22
|
const SKIP_DIRS = new Set(['node_modules', '.git', '.next', 'dist', 'build', '__pycache__', '.cache']);
|
|
20
23
|
const MAX_FILE_BYTES = 80_000;
|
|
@@ -180,7 +183,17 @@ function readFiles(files, dir, options = {}) {
|
|
|
180
183
|
if (chunks.length >= MAX_FILES) break;
|
|
181
184
|
if (totalBytes >= MAX_TOTAL_BYTES) break;
|
|
182
185
|
try {
|
|
183
|
-
const bounded =
|
|
186
|
+
const bounded = UTF8_TEXT_EXTS.has(file.ext)
|
|
187
|
+
? readBoundedText(file.path, file.size)
|
|
188
|
+
: (() => {
|
|
189
|
+
const extracted = extractSharedDocumentText(file.path, { maxChars: MAX_FILE_BYTES });
|
|
190
|
+
return {
|
|
191
|
+
content: extracted.text,
|
|
192
|
+
truncated: extracted.truncated,
|
|
193
|
+
sampledBytes: Math.min(file.size, MAX_FILE_BYTES),
|
|
194
|
+
originalBytes: file.size,
|
|
195
|
+
};
|
|
196
|
+
})();
|
|
184
197
|
totalBytes += bounded.sampledBytes;
|
|
185
198
|
if (bounded.truncated) truncatedFileCount += 1;
|
|
186
199
|
chunks.push(...chunkContent(relative(dir, file.path), bounded.content, {
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/doc-stamp.mjs — Auditability stamps for Construct-generated documents.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* uuidv7() — time-ordered UUID (RFC 9562) for document identity
|
|
6
|
+
* bodyHash(text) — SHA-256 of document body for tamper detection
|
|
7
|
+
* stampFrontmatter — inject/update audit block at top of a markdown string
|
|
8
|
+
* verifyStamp — verify body_hash against current content
|
|
9
|
+
* parseStamp — extract stamp fields from a document string
|
|
10
|
+
*
|
|
11
|
+
* Design:
|
|
12
|
+
* - Zero npm dependencies (Node built-ins only)
|
|
13
|
+
* - Stamp is a YAML-fenced block at the very top of the file, before any
|
|
14
|
+
* other content. Markdown renderers show it as a metadata block.
|
|
15
|
+
* - body_hash covers everything after the closing --- of the stamp block,
|
|
16
|
+
* trimmed, so whitespace-only edits do not break verification.
|
|
17
|
+
* - generator field is intentionally short (no version pinning) so the
|
|
18
|
+
* stamp survives construct upgrades without false-positive hash mismatches.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// UUIDv7 — RFC 9562 §5.7
|
|
25
|
+
// Top 48 bits: Unix ms timestamp. Next 4 bits: version=7. Next 12 bits:
|
|
26
|
+
// random seq. Next 2 bits: variant=10. Remaining 62 bits: random.
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
export function uuidv7() {
|
|
29
|
+
const ms = BigInt(Date.now());
|
|
30
|
+
const rand = randomBytes(10);
|
|
31
|
+
// Build 128-bit value as two 64-bit halves
|
|
32
|
+
const hi =
|
|
33
|
+
((ms & 0xffffffffffffn) << 16n) |
|
|
34
|
+
(7n << 12n) |
|
|
35
|
+
(BigInt(rand[0] & 0x0f) << 8n) |
|
|
36
|
+
BigInt(rand[1]);
|
|
37
|
+
const lo =
|
|
38
|
+
(0x8000000000000000n) |
|
|
39
|
+
(BigInt(rand[2] & 0x3f) << 56n) |
|
|
40
|
+
(BigInt(rand[3]) << 48n) |
|
|
41
|
+
(BigInt(rand[4]) << 40n) |
|
|
42
|
+
(BigInt(rand[5]) << 32n) |
|
|
43
|
+
(BigInt(rand[6]) << 24n) |
|
|
44
|
+
(BigInt(rand[7]) << 16n) |
|
|
45
|
+
(BigInt(rand[8]) << 8n) |
|
|
46
|
+
BigInt(rand[9]);
|
|
47
|
+
|
|
48
|
+
const hex = hi.toString(16).padStart(16, '0') + lo.toString(16).padStart(16, '0');
|
|
49
|
+
return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Content hash — SHA-256 of trimmed body text
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
export function bodyHash(bodyText) {
|
|
56
|
+
return 'sha256:' + createHash('sha256').update(bodyText.trim(), 'utf8').digest('hex');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Stamp block helpers
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
const STAMP_OPEN = '---';
|
|
63
|
+
const STAMP_CLOSE = '---';
|
|
64
|
+
const STAMP_MARKER = 'cx_doc_id:';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns true if the document already has a Construct stamp block.
|
|
68
|
+
*/
|
|
69
|
+
export function hasStamp(content) {
|
|
70
|
+
if (!content.startsWith(STAMP_OPEN)) return false;
|
|
71
|
+
const rest = content.slice(3);
|
|
72
|
+
return rest.includes(STAMP_MARKER);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Split document into { stampBlock, body }.
|
|
77
|
+
* stampBlock includes the opening and closing --- lines.
|
|
78
|
+
* body is everything after the closing ---.
|
|
79
|
+
* Returns null if no stamp found.
|
|
80
|
+
*/
|
|
81
|
+
function splitStamp(content) {
|
|
82
|
+
if (!content.startsWith(STAMP_OPEN + '\n')) return null;
|
|
83
|
+
const afterOpen = content.slice(4);
|
|
84
|
+
const closeIdx = afterOpen.indexOf('\n' + STAMP_CLOSE + '\n');
|
|
85
|
+
if (closeIdx === -1) return null;
|
|
86
|
+
const stampBlock = STAMP_OPEN + '\n' + afterOpen.slice(0, closeIdx) + '\n' + STAMP_CLOSE;
|
|
87
|
+
const body = afterOpen.slice(closeIdx + STAMP_CLOSE.length + 2);
|
|
88
|
+
return { stampBlock, body };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Inject a new audit stamp at the top of a markdown string.
|
|
93
|
+
* If a stamp already exists it is replaced (id preserved, hash updated).
|
|
94
|
+
*
|
|
95
|
+
* @param {string} content - Raw markdown content (may already have a stamp)
|
|
96
|
+
* @param {object} options
|
|
97
|
+
* generator {string} - Short generator label (default: 'construct')
|
|
98
|
+
* sessionId {string} - Construct session ID if available
|
|
99
|
+
* model {string} - Model name/version if available
|
|
100
|
+
* preserve_id {boolean} - Keep existing cx_doc_id if present (default: true)
|
|
101
|
+
*/
|
|
102
|
+
export function stampFrontmatter(content, {
|
|
103
|
+
generator = 'construct',
|
|
104
|
+
sessionId = null,
|
|
105
|
+
model = null,
|
|
106
|
+
preserve_id = true,
|
|
107
|
+
} = {}) {
|
|
108
|
+
const existing = hasStamp(content) ? splitStamp(content) : null;
|
|
109
|
+
const existingFields = existing ? parseStamp(content) : {};
|
|
110
|
+
|
|
111
|
+
const id = (preserve_id && existingFields.cx_doc_id) ? existingFields.cx_doc_id : uuidv7();
|
|
112
|
+
const createdAt = existingFields.created_at || new Date().toISOString();
|
|
113
|
+
const updatedAt = new Date().toISOString();
|
|
114
|
+
|
|
115
|
+
// Body is everything after stamp (or all content if no prior stamp)
|
|
116
|
+
const body = existing ? existing.body : content;
|
|
117
|
+
const hash = bodyHash(body);
|
|
118
|
+
|
|
119
|
+
const lines = [
|
|
120
|
+
STAMP_OPEN,
|
|
121
|
+
`cx_doc_id: ${id}`,
|
|
122
|
+
`created_at: ${createdAt}`,
|
|
123
|
+
`updated_at: ${updatedAt}`,
|
|
124
|
+
`generator: ${generator}`,
|
|
125
|
+
];
|
|
126
|
+
if (model) lines.push(`model: ${model}`);
|
|
127
|
+
if (sessionId) lines.push(`session_id: ${sessionId}`);
|
|
128
|
+
lines.push(`body_hash: ${hash}`);
|
|
129
|
+
lines.push(STAMP_CLOSE);
|
|
130
|
+
lines.push('');
|
|
131
|
+
|
|
132
|
+
return lines.join('\n') + body;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Parse stamp fields from a stamped document.
|
|
137
|
+
* Returns {} if no stamp found.
|
|
138
|
+
*/
|
|
139
|
+
export function parseStamp(content) {
|
|
140
|
+
const parts = splitStamp(content);
|
|
141
|
+
if (!parts) return {};
|
|
142
|
+
const fields = {};
|
|
143
|
+
for (const line of parts.stampBlock.split('\n')) {
|
|
144
|
+
const m = line.match(/^(\w+):\s*(.+)$/);
|
|
145
|
+
if (m) fields[m[1]] = m[2].trim();
|
|
146
|
+
}
|
|
147
|
+
return fields;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Verify that the stored body_hash matches the current body.
|
|
152
|
+
* Returns { valid: true } or { valid: false, reason, stored, computed }.
|
|
153
|
+
*/
|
|
154
|
+
export function verifyStamp(content) {
|
|
155
|
+
const parts = splitStamp(content);
|
|
156
|
+
if (!parts) return { valid: false, reason: 'no stamp found' };
|
|
157
|
+
const fields = parseStamp(content);
|
|
158
|
+
if (!fields.body_hash) return { valid: false, reason: 'no body_hash in stamp' };
|
|
159
|
+
const computed = bodyHash(parts.body);
|
|
160
|
+
if (computed !== fields.body_hash) {
|
|
161
|
+
return { valid: false, reason: 'body_hash mismatch', stored: fields.body_hash, computed };
|
|
162
|
+
}
|
|
163
|
+
return { valid: true, id: fields.cx_doc_id, created_at: fields.created_at };
|
|
164
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/doc-verify.mjs — CLI handler for `construct doc verify`
|
|
3
|
+
*
|
|
4
|
+
* Walks one file or a directory tree, checks every stamped .md file for
|
|
5
|
+
* body_hash integrity, and reports pass/fail/unstamped counts.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* construct doc verify [path] [--json]
|
|
9
|
+
*
|
|
10
|
+
* Exit codes:
|
|
11
|
+
* 0 all stamped files pass (or no stamped files found)
|
|
12
|
+
* 1 one or more stamped files fail verification
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { verifyStamp, hasStamp } from './doc-stamp.mjs';
|
|
19
|
+
|
|
20
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
21
|
+
|
|
22
|
+
function walkMd(target) {
|
|
23
|
+
const stat = fs.statSync(target);
|
|
24
|
+
if (stat.isFile()) return target.endsWith('.md') ? [target] : [];
|
|
25
|
+
const files = [];
|
|
26
|
+
for (const entry of fs.readdirSync(target, { withFileTypes: true })) {
|
|
27
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
28
|
+
const full = path.join(target, entry.name);
|
|
29
|
+
if (entry.isDirectory()) files.push(...walkMd(full));
|
|
30
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) files.push(full);
|
|
31
|
+
}
|
|
32
|
+
return files;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function runDocVerifyCli(args) {
|
|
36
|
+
const jsonMode = args.includes('--json');
|
|
37
|
+
const targetArg = args.find(a => !a.startsWith('--'));
|
|
38
|
+
const target = path.resolve(targetArg ?? process.cwd());
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(target)) {
|
|
41
|
+
console.error(`construct doc verify: path not found: ${target}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const files = walkMd(target);
|
|
46
|
+
const results = { pass: [], fail: [], unstamped: [] };
|
|
47
|
+
|
|
48
|
+
for (const file of files) {
|
|
49
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
50
|
+
if (!hasStamp(content)) {
|
|
51
|
+
results.unstamped.push(file);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const v = verifyStamp(content);
|
|
55
|
+
if (v.valid) results.pass.push({ file, id: v.id, created_at: v.created_at });
|
|
56
|
+
else results.fail.push({ file, reason: v.reason, stored: v.stored, computed: v.computed });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (jsonMode) {
|
|
60
|
+
console.log(JSON.stringify(results, null, 2));
|
|
61
|
+
} else {
|
|
62
|
+
const rel = f => path.relative(process.cwd(), f);
|
|
63
|
+
for (const r of results.fail) {
|
|
64
|
+
console.log(` FAIL ${rel(r.file)}`);
|
|
65
|
+
console.log(` ${r.reason}`);
|
|
66
|
+
}
|
|
67
|
+
for (const r of results.pass) {
|
|
68
|
+
console.log(` ok ${rel(r.file)} [${r.id}]`);
|
|
69
|
+
}
|
|
70
|
+
if (results.unstamped.length) {
|
|
71
|
+
console.log(`\n ${results.unstamped.length} file(s) have no stamp (not generated by construct).`);
|
|
72
|
+
}
|
|
73
|
+
const summary = `\n ${results.pass.length} passed, ${results.fail.length} failed, ${results.unstamped.length} unstamped`;
|
|
74
|
+
console.log(summary);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (results.fail.length > 0) process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Install the prepare-commit-msg git hook into the nearest .git/hooks/ directory.
|
|
82
|
+
*/
|
|
83
|
+
export function runDocInstallHooksCli(_args) {
|
|
84
|
+
const src = path.join(__dirname, 'git-hooks', 'prepare-commit-msg');
|
|
85
|
+
if (!fs.existsSync(src)) {
|
|
86
|
+
console.error(`construct doc install-hooks: hook source not found: ${src}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Walk up from cwd to find .git
|
|
91
|
+
let dir = process.cwd();
|
|
92
|
+
let gitDir = null;
|
|
93
|
+
while (true) {
|
|
94
|
+
const candidate = path.join(dir, '.git');
|
|
95
|
+
if (fs.existsSync(candidate)) { gitDir = candidate; break; }
|
|
96
|
+
const parent = path.dirname(dir);
|
|
97
|
+
if (parent === dir) break;
|
|
98
|
+
dir = parent;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!gitDir) {
|
|
102
|
+
console.error('construct doc install-hooks: not inside a git repository');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
107
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
108
|
+
const dest = path.join(hooksDir, 'prepare-commit-msg');
|
|
109
|
+
|
|
110
|
+
if (fs.existsSync(dest)) {
|
|
111
|
+
console.log(` already installed: ${path.relative(process.cwd(), dest)}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fs.copyFileSync(src, dest);
|
|
116
|
+
fs.chmodSync(dest, 0o755);
|
|
117
|
+
console.log(` installed: ${path.relative(process.cwd(), dest)}`);
|
|
118
|
+
console.log('\n Set CONSTRUCT_MODEL and CONSTRUCT_SESSION_ID in your shell or Construct config to populate trailers.');
|
|
119
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/docs-routing.mjs — suggests documentation lanes for files based on content and naming.
|
|
3
|
+
*
|
|
4
|
+
* Maps file paths and content patterns to documentation sections (prds, adrs, rfcs, etc.)
|
|
5
|
+
* for automatic documentation organization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
export const DOC_LANE_DIRS = {
|
|
11
|
+
adrs: 'adr',
|
|
12
|
+
briefs: 'briefs',
|
|
13
|
+
changelogs: 'changelogs',
|
|
14
|
+
intake: 'intake',
|
|
15
|
+
memos: 'memos',
|
|
16
|
+
meetings: 'meetings',
|
|
17
|
+
notes: 'notes',
|
|
18
|
+
onboarding: 'onboarding',
|
|
19
|
+
postmortems: 'postmortems',
|
|
20
|
+
prds: 'prds',
|
|
21
|
+
rfcs: 'rfcs',
|
|
22
|
+
runbooks: 'runbooks',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function docLaneDir(laneKey) {
|
|
26
|
+
return DOC_LANE_DIRS[laneKey] ?? laneKey;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function suggestDocsLaneForFile(filePath, content = '') {
|
|
30
|
+
const fileName = path.basename(filePath).toLowerCase();
|
|
31
|
+
|
|
32
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
33
|
+
if (frontmatterMatch) {
|
|
34
|
+
const frontmatter = frontmatterMatch[1];
|
|
35
|
+
const typeMatch = frontmatter.match(/type:\s*(prd|adr|rfc|memo|runbook|brief|meeting|note)/i);
|
|
36
|
+
if (typeMatch) {
|
|
37
|
+
const type = typeMatch[1].toLowerCase();
|
|
38
|
+
if (type === 'adr') return 'adrs';
|
|
39
|
+
if (type === 'prd') return 'prds';
|
|
40
|
+
if (type === 'rfc') return 'rfcs';
|
|
41
|
+
if (type === 'memo') return 'memos';
|
|
42
|
+
if (type === 'runbook') return 'runbooks';
|
|
43
|
+
if (type === 'brief') return 'briefs';
|
|
44
|
+
if (type === 'meeting') return 'meetings';
|
|
45
|
+
if (type === 'note') return 'notes';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const prdPatterns = ['prd', 'product', 'requirement', 'spec', 'feature'];
|
|
50
|
+
const adrPatterns = ['adr', 'architecture', 'design', 'decision'];
|
|
51
|
+
const rfcPatterns = ['rfc', 'request', 'comment', 'proposal'];
|
|
52
|
+
const memoPatterns = ['memo', 'memorandum'];
|
|
53
|
+
const runbookPatterns = ['runbook', 'run book', 'operational', 'procedure'];
|
|
54
|
+
const briefPatterns = ['brief', 'one-pager', 'signal', 'evidence', 'research'];
|
|
55
|
+
const changelogPatterns = ['changelog', 'release-notes', 'releases'];
|
|
56
|
+
const onboardingPatterns = ['onboarding', 'setup', 'getting-started', 'local-setup'];
|
|
57
|
+
const postmortemPatterns = ['postmortem', 'incident-report', 'incident', 'sev-'];
|
|
58
|
+
const meetingPatterns = ['meeting', 'minutes', 'retro', 'standup', 'agenda', 'sync', '1-1', '1:1'];
|
|
59
|
+
const notePatterns = ['note', 'notes', 'scratchpad', 'journal'];
|
|
60
|
+
|
|
61
|
+
const checkPatterns = (patterns) => patterns.some((p) => fileName.includes(p));
|
|
62
|
+
|
|
63
|
+
if (checkPatterns(prdPatterns)) return 'prds';
|
|
64
|
+
if (checkPatterns(adrPatterns)) return 'adrs';
|
|
65
|
+
if (checkPatterns(rfcPatterns)) return 'rfcs';
|
|
66
|
+
if (checkPatterns(memoPatterns)) return 'memos';
|
|
67
|
+
if (checkPatterns(runbookPatterns)) return 'runbooks';
|
|
68
|
+
if (checkPatterns(briefPatterns)) return 'briefs';
|
|
69
|
+
if (checkPatterns(changelogPatterns)) return 'changelogs';
|
|
70
|
+
if (checkPatterns(onboardingPatterns)) return 'onboarding';
|
|
71
|
+
if (checkPatterns(postmortemPatterns)) return 'postmortems';
|
|
72
|
+
if (checkPatterns(meetingPatterns)) return 'meetings';
|
|
73
|
+
if (checkPatterns(notePatterns)) return 'notes';
|
|
74
|
+
|
|
75
|
+
const preview = content.slice(0, 4000).toLowerCase();
|
|
76
|
+
if (preview.includes('product requirement') || preview.includes('user story') || preview.includes('acceptance criteria')) return 'prds';
|
|
77
|
+
if (preview.includes('architecture decision') || preview.includes('design decision') || preview.includes('we decided')) return 'adrs';
|
|
78
|
+
if (preview.includes('request for comments') || preview.includes('please comment') || preview.includes('drawbacks')) return 'rfcs';
|
|
79
|
+
if (preview.includes('decision memo') || preview.includes('memorandum')) return 'memos';
|
|
80
|
+
if (preview.includes('runbook') || preview.includes('operational procedure') || preview.includes('troubleshooting')) return 'runbooks';
|
|
81
|
+
if (preview.includes('signal') || preview.includes('evidence') || preview.includes('competitive analysis') || preview.includes('research brief')) return 'briefs';
|
|
82
|
+
if (preview.includes('## added') || preview.includes('## changed') || preview.includes('## fixed') || preview.includes('release notes')) return 'changelogs';
|
|
83
|
+
if (preview.includes('local setup') || preview.includes('getting started') || preview.includes('prerequisites')) return 'onboarding';
|
|
84
|
+
if (preview.includes('postmortem') || preview.includes('incident report') || (preview.includes('root cause') && preview.includes('timeline') && preview.includes('impact'))) return 'postmortems';
|
|
85
|
+
if (preview.includes('meeting notes') || preview.includes('meeting minutes') || preview.includes('attendees') && preview.includes('action items')) return 'meetings';
|
|
86
|
+
if (preview.includes('notes') || preview.includes('working notes')) return 'notes';
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|