@geraldmaron/construct 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +10 -7
- package/LICENSE +201 -21
- package/README.md +132 -257
- package/agents/contracts.json +387 -0
- package/agents/prompts/cx-accessibility.md +8 -0
- package/agents/prompts/cx-ai-engineer.md +92 -0
- package/agents/prompts/cx-architect.md +10 -2
- package/agents/prompts/cx-business-strategist.md +13 -0
- package/agents/prompts/cx-data-analyst.md +80 -0
- package/agents/prompts/cx-data-engineer.md +4 -0
- package/agents/prompts/cx-debugger.md +8 -0
- package/agents/prompts/cx-designer.md +19 -0
- package/agents/prompts/cx-devil-advocate.md +4 -0
- package/agents/prompts/cx-docs-keeper.md +128 -0
- package/agents/prompts/cx-engineer.md +13 -0
- package/agents/prompts/cx-evaluator.md +4 -0
- package/agents/prompts/cx-explorer.md +12 -0
- package/agents/prompts/cx-legal-compliance.md +13 -0
- package/agents/prompts/cx-operations.md +13 -0
- package/agents/prompts/cx-orchestrator.md +107 -4
- package/agents/prompts/cx-platform-engineer.md +75 -0
- package/agents/prompts/cx-product-manager.md +8 -0
- package/agents/prompts/cx-qa.md +100 -0
- package/agents/prompts/cx-rd-lead.md +13 -0
- package/agents/prompts/cx-release-manager.md +8 -0
- package/agents/prompts/cx-researcher.md +21 -1
- package/agents/prompts/cx-reviewer.md +8 -0
- package/agents/prompts/cx-security.md +104 -0
- package/agents/prompts/cx-sre.md +104 -0
- package/agents/prompts/cx-test-automation.md +4 -0
- package/agents/prompts/cx-trace-reviewer.md +7 -3
- package/agents/prompts/cx-ux-researcher.md +4 -0
- package/agents/registry.json +365 -90
- package/agents/role-manifests.json +217 -0
- package/bin/construct +3345 -141
- package/bin/construct-postinstall.mjs +87 -0
- package/commands/build/feature.md +6 -6
- package/commands/plan/feature.md +6 -5
- package/commands/plan/requirements.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/work/drive.md +3 -3
- package/commands/work/optimize-prompts.md +3 -3
- package/db/{migrations → schema}/001_init.sql +2 -0
- package/db/schema/002_pgvector.sql +182 -0
- package/db/schema/003_intake.sql +47 -0
- package/examples/README.md +85 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
- package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
- package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
- package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
- package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
- package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
- package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
- package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
- package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
- package/examples/personas/construct/bad/commit-without-approval.md +30 -0
- package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
- package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
- package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
- package/examples/provider-plugin/README.md +34 -0
- package/examples/provider-plugin/index.mjs +74 -0
- package/examples/provider-plugin/package.json +15 -0
- package/examples/seed-observations/README.md +38 -0
- package/examples/seed-observations/anti-patterns.md +44 -0
- package/examples/seed-observations/decisions.md +36 -0
- package/examples/seed-observations/patterns.md +42 -0
- package/lib/agent-contracts-enforce.mjs +158 -0
- package/lib/agent-contracts.mjs +231 -0
- package/lib/agents/postconditions.mjs +126 -0
- package/lib/agents/schema.mjs +124 -0
- package/lib/artifact-capture.mjs +183 -0
- package/lib/audit-trail.mjs +149 -0
- package/lib/auto-docs.mjs +245 -65
- package/lib/beads/auto-close.mjs +126 -0
- package/lib/beads/drift.mjs +171 -0
- package/lib/beads-automation.mjs +542 -0
- package/lib/beads-client.mjs +518 -0
- package/lib/beads-lock.mjs +377 -0
- package/lib/beads-optimistic.mjs +365 -0
- package/lib/bootstrap/built-ins.mjs +136 -0
- package/lib/bootstrap/lazy-install.mjs +161 -0
- package/lib/bootstrap/resources.mjs +120 -0
- package/lib/bootstrap.mjs +105 -0
- package/lib/cache-governor.js +213 -0
- package/lib/cache-strategy-anthropic.js +62 -0
- package/lib/cache-strategy-google.js +79 -0
- package/lib/cache-strategy-none.js +30 -0
- package/lib/cache-strategy-openai.js +48 -0
- package/lib/cache-strategy.js +91 -0
- package/lib/claude-allow.mjs +149 -0
- package/lib/cli-commands.mjs +413 -258
- package/lib/codex-config.mjs +3 -1
- package/lib/comment-lint.mjs +55 -6
- package/lib/completions.mjs +21 -6
- package/lib/config/alias.mjs +56 -0
- package/lib/config/project-config.mjs +335 -0
- package/lib/config/schema.mjs +159 -0
- package/lib/context-router.mjs +308 -0
- package/lib/cost-ledger.mjs +177 -0
- package/lib/cost.mjs +171 -10
- package/lib/dashboard-static.mjs +158 -0
- package/lib/deployment-mode.mjs +86 -0
- package/lib/deprecate.mjs +49 -0
- package/lib/dispatch-batch.js +183 -0
- package/lib/distill.mjs +21 -8
- package/lib/doc-stamp.mjs +164 -0
- package/lib/doc-verify.mjs +119 -0
- package/lib/docs-routing.mjs +89 -0
- package/lib/docs-verify.mjs +417 -0
- package/lib/doctor/audit.mjs +71 -0
- package/lib/doctor/cli.mjs +99 -0
- package/lib/doctor/escalate.mjs +29 -0
- package/lib/doctor/index.mjs +140 -0
- package/lib/doctor/report.mjs +170 -0
- package/lib/doctor/watchers/bd-watch.mjs +117 -0
- package/lib/doctor/watchers/cost.mjs +130 -0
- package/lib/doctor/watchers/disk.mjs +122 -0
- package/lib/doctor/watchers/handoffs.mjs +33 -0
- package/lib/doctor/watchers/process-pressure.mjs +60 -0
- package/lib/doctor/watchers/service-health.mjs +188 -0
- package/lib/document-extract.mjs +288 -0
- package/lib/document-ingest.mjs +230 -0
- package/lib/drop.mjs +282 -0
- package/lib/embed/approval-queue.mjs +176 -0
- package/lib/embed/artifact.mjs +349 -0
- package/lib/embed/authority-guard.mjs +155 -0
- package/lib/embed/cli.mjs +408 -0
- package/lib/embed/config.mjs +355 -0
- package/lib/embed/conflict-detection.mjs +264 -0
- package/lib/embed/customer-profiles.mjs +480 -0
- package/lib/embed/daemon.mjs +1309 -0
- package/lib/embed/demand-fetch.mjs +449 -0
- package/lib/embed/docs-lifecycle.mjs +349 -0
- package/lib/embed/inbox-live-watcher.mjs +119 -0
- package/lib/embed/inbox.mjs +343 -0
- package/lib/embed/intake-metrics.mjs +190 -0
- package/lib/embed/jobs/vector-sync.mjs +198 -0
- package/lib/embed/notifications.mjs +75 -0
- package/lib/embed/output.mjs +79 -0
- package/lib/embed/providers/github.mjs +295 -0
- package/lib/embed/providers/jira.mjs +192 -0
- package/lib/embed/providers/linear.mjs +186 -0
- package/lib/embed/providers/registry.mjs +115 -0
- package/lib/embed/providers/slack.mjs +203 -0
- package/lib/embed/recommendation-store.mjs +378 -0
- package/lib/embed/roadmap.mjs +374 -0
- package/lib/embed/role-framing.mjs +110 -0
- package/lib/embed/scheduler.mjs +99 -0
- package/lib/embed/semantic.mjs +325 -0
- package/lib/embed/snapshot.mjs +191 -0
- package/lib/embed/supervision.mjs +235 -0
- package/lib/embed/target-resolver.mjs +186 -0
- package/lib/embed/worker.mjs +62 -0
- package/lib/embed/workspaces.mjs +297 -0
- package/lib/engine/chunker-headings.mjs +110 -0
- package/lib/engine/compressor-heuristic.mjs +100 -0
- package/lib/engine/consolidate.mjs +287 -0
- package/lib/engine/contracts.mjs +126 -0
- package/lib/engine/defaults.mjs +129 -0
- package/lib/engine/eval-retrieval.mjs +148 -0
- package/lib/engine/fuser-rrf.mjs +62 -0
- package/lib/engine/index.mjs +37 -0
- package/lib/engine/registry.mjs +146 -0
- package/lib/engine/reranker-mmr.mjs +90 -0
- package/lib/engine/tokens.mjs +77 -0
- package/lib/entity-store.mjs +280 -0
- package/lib/env-config.mjs +69 -4
- package/lib/evals/retrieval-bench.mjs +159 -0
- package/lib/evaluator-optimizer.mjs +317 -0
- package/lib/features.mjs +159 -29
- package/lib/gates-audit.mjs +236 -0
- package/lib/git-hooks/prepare-commit-msg +58 -0
- package/lib/handoffs/cleanup.mjs +159 -0
- package/lib/handoffs/contract.mjs +162 -0
- package/lib/handoffs/inventory.mjs +120 -0
- package/lib/headhunt.mjs +28 -47
- package/lib/health-check.mjs +399 -0
- package/lib/hook-health.mjs +442 -0
- package/lib/hooks/_lib/log.mjs +82 -0
- package/lib/hooks/adaptive-lint.mjs +26 -2
- package/lib/hooks/agent-tracker.mjs +171 -14
- package/lib/hooks/audit-reads.mjs +109 -0
- package/lib/hooks/audit-trail.mjs +153 -0
- package/lib/hooks/bash-output-logger.mjs +71 -0
- package/lib/hooks/block-no-verify.mjs +41 -0
- package/lib/hooks/ci-status-check.mjs +82 -0
- package/lib/hooks/comment-lint.mjs +29 -7
- package/lib/hooks/config-protection.mjs +39 -18
- package/lib/hooks/context-watch.mjs +137 -0
- package/lib/hooks/context-window-recovery.mjs +4 -20
- package/lib/hooks/dep-audit.mjs +16 -0
- package/lib/hooks/doc-coupling-check.mjs +80 -0
- package/lib/hooks/edit-accumulator.mjs +3 -0
- package/lib/hooks/edit-error-recovery.mjs +3 -0
- package/lib/hooks/edit-guard.mjs +45 -4
- package/lib/hooks/env-check.mjs +3 -0
- package/lib/hooks/guard-bash.mjs +58 -1
- package/lib/hooks/mcp-audit.mjs +18 -20
- package/lib/hooks/mcp-health-check.mjs +36 -0
- package/lib/hooks/model-fallback.mjs +42 -56
- package/lib/hooks/policy-engine.mjs +209 -0
- package/lib/hooks/post-merge-docs-check.mjs +63 -0
- package/lib/hooks/pre-compact.mjs +4 -24
- package/lib/hooks/pre-push-gate.mjs +200 -37
- package/lib/hooks/proactive-activation.mjs +284 -0
- package/lib/hooks/read-tracker.mjs +27 -4
- package/lib/hooks/readme-age-check.mjs +77 -0
- package/lib/hooks/registry-sync.mjs +12 -5
- package/lib/hooks/scan-secrets.mjs +50 -7
- package/lib/hooks/session-optimize.mjs +311 -0
- package/lib/hooks/session-start.mjs +287 -28
- package/lib/hooks/stop-notify.mjs +236 -96
- package/lib/hooks/stop-typecheck.mjs +13 -1
- package/lib/hooks/test-watch.mjs +68 -0
- package/lib/host-capabilities.mjs +31 -10
- package/lib/init-docs.mjs +731 -291
- package/lib/init-unified.mjs +1116 -0
- package/lib/init-update.mjs +168 -0
- package/lib/init.mjs +107 -0
- package/lib/install/first-invocation.mjs +119 -0
- package/lib/install/stage-project.mjs +69 -0
- package/lib/intake/classify.mjs +278 -0
- package/lib/intake/feedback.mjs +273 -0
- package/lib/intake/filesystem-queue.mjs +158 -0
- package/lib/intake/intake-config.mjs +132 -0
- package/lib/intake/postgres-queue.mjs +198 -0
- package/lib/intake/prepare.mjs +139 -0
- package/lib/intake/queue.mjs +83 -0
- package/lib/intake/session-prelude.mjs +50 -0
- package/lib/integrations/intake-integrations.mjs +740 -0
- package/lib/intent-classifier.mjs +253 -0
- package/lib/knowledge/layout.mjs +72 -0
- package/lib/knowledge/rag.mjs +331 -0
- package/lib/knowledge/search.mjs +315 -0
- package/lib/knowledge/trends.mjs +261 -0
- package/lib/logger.mjs +85 -0
- package/lib/mcp/broker.mjs +124 -0
- package/lib/mcp/server.mjs +554 -854
- package/lib/mcp/tools/document.mjs +132 -0
- package/lib/mcp/tools/memory.mjs +209 -0
- package/lib/mcp/tools/project.mjs +349 -0
- package/lib/mcp/tools/skills.mjs +409 -0
- package/lib/mcp/tools/storage.mjs +60 -0
- package/lib/mcp/tools/telemetry.mjs +315 -0
- package/lib/mcp/tools/workflow.mjs +112 -0
- package/lib/mcp-catalog.json +54 -3
- package/lib/mcp-manager.mjs +96 -48
- package/lib/mcp-platform-config.mjs +22 -22
- package/lib/memory-stats.mjs +121 -0
- package/lib/mode-commands.mjs +124 -0
- package/lib/model-free-selector.mjs +184 -0
- package/lib/model-pricing.mjs +152 -0
- package/lib/model-registry.mjs +226 -0
- package/lib/model-router.mjs +362 -386
- package/lib/observation-store.mjs +391 -0
- package/lib/ollama-manager.mjs +428 -0
- package/lib/opencode-config.mjs +13 -3
- package/lib/opencode-runtime-plugin.mjs +70 -38
- package/lib/opencode-telemetry.mjs +64 -6
- package/lib/orchestration-policy.mjs +509 -6
- package/lib/overrides/resolver.mjs +207 -0
- package/lib/parity.mjs +147 -0
- package/lib/paths.mjs +33 -0
- package/lib/performance/generate.mjs +212 -0
- package/lib/plugin-registry.mjs +268 -0
- package/lib/policy/engine.mjs +130 -0
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-detection.mjs +129 -0
- package/lib/project-init-shared.mjs +272 -0
- package/lib/project-profile.mjs +447 -0
- package/lib/prompt-composer.js +434 -0
- package/lib/prompt-metadata.mjs +1 -1
- package/lib/provider-capabilities-anthropic.js +44 -0
- package/lib/provider-capabilities-deepseek.js +37 -0
- package/lib/provider-capabilities-generic.js +26 -0
- package/lib/provider-capabilities-google.js +47 -0
- package/lib/provider-capabilities-openai.js +45 -0
- package/lib/provider-capabilities.js +142 -0
- package/lib/providers/atlassian-confluence/index.mjs +103 -0
- package/lib/providers/atlassian-jira/index.mjs +100 -0
- package/lib/providers/auth-manager.mjs +126 -0
- package/lib/providers/circuit-breaker.mjs +124 -0
- package/lib/providers/contract.mjs +93 -0
- package/lib/providers/github/index.mjs +126 -0
- package/lib/providers/registry.mjs +184 -0
- package/lib/providers/salesforce/index.mjs +100 -0
- package/lib/providers/slack/index.mjs +80 -0
- package/lib/reflect.mjs +137 -0
- package/lib/research-lint.mjs +164 -0
- package/lib/resources/budget.mjs +259 -0
- package/lib/role-preload.mjs +21 -6
- package/lib/roles/approval-surface.mjs +54 -0
- package/lib/roles/cli.mjs +118 -0
- package/lib/roles/event-bus.mjs +79 -0
- package/lib/roles/fence.mjs +84 -0
- package/lib/roles/gateway.mjs +260 -0
- package/lib/roles/hook-emit.mjs +37 -0
- package/lib/roles/manifest.mjs +48 -0
- package/lib/roles/router.mjs +27 -0
- package/lib/runtime-pressure.mjs +360 -0
- package/lib/schema-artifact.mjs +134 -0
- package/lib/schema-infer.mjs +551 -0
- package/lib/server/auth.mjs +168 -0
- package/lib/server/chat.mjs +336 -0
- package/lib/server/cors.mjs +77 -0
- package/lib/server/csrf.mjs +91 -0
- package/lib/server/index.mjs +1927 -78
- package/lib/server/insights.mjs +765 -0
- package/lib/server/rate-limit.mjs +91 -0
- package/lib/server/static/assets/index-ab25c707.js +70 -0
- package/lib/server/static/assets/index-f0c80a2b.css +1 -0
- package/lib/server/static/index.html +12 -817
- package/lib/server/telemetry-login.mjs +108 -0
- package/lib/server/webhook.mjs +510 -0
- package/lib/service-manager.mjs +522 -58
- package/lib/services/pattern-promotion-service.mjs +167 -0
- package/lib/services/telemetry-backend.mjs +178 -0
- package/lib/session-store.mjs +374 -0
- package/lib/setup-prompts.mjs +96 -0
- package/lib/setup.mjs +525 -38
- package/lib/skills-apply.mjs +280 -0
- package/lib/skills-scope.mjs +118 -0
- package/lib/status.mjs +261 -70
- package/lib/storage/admin.mjs +355 -0
- package/lib/storage/backend.mjs +2 -1
- package/lib/storage/backup.mjs +347 -0
- package/lib/storage/embeddings-engine.mjs +133 -0
- package/lib/storage/embeddings-legacy.mjs +85 -0
- package/lib/storage/embeddings-local.mjs +108 -0
- package/lib/storage/embeddings-ollama.mjs +78 -0
- package/lib/storage/embeddings-openai.mjs +85 -0
- package/lib/storage/embeddings.mjs +92 -33
- package/lib/storage/file-lock.mjs +130 -0
- package/lib/storage/fusion.mjs +95 -0
- package/lib/storage/hybrid-query.mjs +34 -27
- package/lib/storage/migrations.mjs +187 -0
- package/lib/storage/postgres-backup.mjs +124 -0
- package/lib/storage/sql-store.mjs +5 -15
- package/lib/storage/state-source.mjs +12 -13
- package/lib/storage/store-version.mjs +115 -0
- package/lib/storage/sync.mjs +144 -35
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/storage/vector-client.mjs +286 -0
- package/lib/storage/vector-store.mjs +71 -30
- package/lib/task-graph/generate.mjs +135 -0
- package/lib/task-graph/schema.mjs +81 -0
- package/lib/task-graph/store.mjs +71 -0
- package/lib/telemetry/backends/local.mjs +62 -0
- package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
- package/lib/telemetry/backfill.mjs +180 -0
- package/lib/telemetry/eval-datasets.mjs +203 -0
- package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
- package/lib/telemetry/intent-verifications.mjs +86 -0
- package/lib/telemetry/llm-judge.mjs +350 -0
- package/lib/telemetry/model-pricing-catalog.mjs +557 -0
- package/lib/telemetry/setup.mjs +151 -0
- package/lib/telemetry/skill-calls.mjs +78 -0
- package/lib/telemetry/team-rollup.mjs +4 -4
- package/lib/token-engine.js +117 -0
- package/lib/token-estimator-anthropic.js +15 -0
- package/lib/token-estimator-deepseek.js +13 -0
- package/lib/token-estimator-default.js +13 -0
- package/lib/token-estimator-google.js +13 -0
- package/lib/token-estimator-openai.js +13 -0
- package/lib/toolkit-env.mjs +1 -1
- package/lib/tty-prompts.mjs +211 -0
- package/lib/uninstall/uninstall.mjs +423 -0
- package/lib/update.mjs +115 -0
- package/lib/upgrade.mjs +141 -0
- package/lib/validator.mjs +51 -2
- package/lib/validators/skills.mjs +142 -0
- package/lib/wireframe.mjs +422 -0
- package/lib/worker/entrypoint.mjs +241 -0
- package/lib/worker/evidence.mjs +107 -0
- package/lib/worker/run.mjs +154 -0
- package/lib/worker/trace.mjs +182 -0
- package/lib/workflow-state.mjs +14 -18
- package/package.json +21 -8
- package/personas/construct.md +53 -51
- package/platforms/claude/CLAUDE.md +1 -1
- package/platforms/claude/settings.template.json +115 -68
- package/platforms/opencode/config.template.json +3 -7
- package/rules/common/agents.md +11 -38
- package/rules/common/beads-hygiene.md +75 -0
- package/rules/common/code-review.md +11 -100
- package/rules/common/coding-style.md +5 -3
- package/rules/common/comments.md +30 -111
- package/rules/common/commit-approval.md +53 -0
- package/rules/common/cx-agent-routing.md +1 -0
- package/rules/common/development-workflow.md +23 -41
- package/rules/common/doc-ownership.md +54 -0
- package/rules/common/efficiency.md +57 -0
- package/rules/common/framing.md +76 -0
- package/rules/common/git-workflow.md +2 -4
- package/rules/common/patterns.md +3 -7
- package/rules/common/performance.md +15 -31
- package/rules/common/release-gates.md +69 -0
- package/rules/common/research.md +107 -0
- package/rules/common/security.md +6 -6
- package/rules/common/skill-composition.md +67 -0
- package/rules/common/testing.md +15 -27
- package/rules/golang/hooks.md +0 -4
- package/rules/policy/bootstrap.yaml +11 -0
- package/rules/policy/drive.yaml +8 -0
- package/rules/policy/task.yaml +9 -0
- package/rules/policy/workflow.yaml +9 -0
- package/rules/python/hooks.md +0 -4
- package/rules/swift/hooks.md +0 -4
- package/rules/typescript/hooks.md +0 -4
- package/rules/web/hooks.md +0 -2
- package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
- package/skills/ai/prompt-optimizer.md +7 -7
- package/skills/compliance/ai-disclosure.md +58 -0
- package/skills/compliance/data-privacy.md +46 -0
- package/skills/compliance/license-audit.md +40 -0
- package/skills/compliance/regulatory-review.md +61 -0
- package/skills/docs/document-ingest-workflow.md +52 -0
- package/skills/docs/evidence-ingest-workflow.md +9 -0
- package/skills/docs/init-docs.md +51 -18
- package/skills/docs/product-intelligence-workflow.md +12 -0
- package/skills/docs/product-signal-workflow.md +4 -0
- package/skills/docs/research-workflow.md +23 -8
- package/skills/docs/runbook-workflow.md +1 -1
- package/skills/operating/orchestration-reference.md +151 -0
- package/skills/roles/architect.md +5 -0
- package/skills/roles/engineer.md +32 -0
- package/skills/roles/operator.md +12 -0
- package/skills/roles/reviewer.md +23 -0
- package/skills/routing.md +1 -1
- package/templates/devcontainer/Dockerfile.devcontainer +38 -0
- package/templates/devcontainer/devcontainer.json +31 -0
- package/templates/distribution/bootstrap.ps1 +142 -0
- package/templates/distribution/bootstrap.sh +196 -0
- package/templates/distribution/run.mjs +187 -0
- package/templates/docs/adr.md +44 -8
- package/templates/docs/changelog-entry.md +43 -0
- package/templates/docs/construct_guide.md +149 -0
- package/templates/docs/evidence-brief.md +2 -2
- package/templates/docs/meta-prd.md +140 -19
- package/templates/docs/onboarding.md +57 -0
- package/templates/docs/prd.md +159 -15
- package/templates/docs/research-brief.md +4 -4
- package/templates/homebrew/construct.rb +67 -0
- package/langfuse/docker-compose.yml +0 -82
- package/lib/eval-harness.mjs +0 -59
- package/lib/hooks/bootstrap-guard.mjs +0 -90
- package/lib/hooks/console-warn.mjs +0 -43
- package/lib/hooks/continuation-enforcer.mjs +0 -72
- package/lib/hooks/drive-guard.mjs +0 -89
- package/lib/hooks/mcp-task-scope.mjs +0 -47
- package/lib/hooks/task-completed-guard.mjs +0 -43
- package/lib/hooks/teammate-idle-guard.mjs +0 -54
- package/lib/hooks/workflow-guard.mjs +0 -62
- package/lib/prompt-composer.mjs +0 -196
- package/lib/review.mjs +0 -429
- package/lib/server/static/app.js +0 -841
- package/lib/telemetry/langfuse-model-sync.mjs +0 -270
- package/rules/common/hooks.md +0 -35
- package/rules/zh/README.md +0 -113
- package/rules/zh/agents.md +0 -55
- package/rules/zh/code-review.md +0 -129
- package/rules/zh/coding-style.md +0 -53
- package/rules/zh/development-workflow.md +0 -49
- package/rules/zh/git-workflow.md +0 -29
- package/rules/zh/hooks.md +0 -35
- package/rules/zh/patterns.md +0 -36
- package/rules/zh/performance.md +0 -60
- package/rules/zh/security.md +0 -34
- package/rules/zh/testing.md +0 -34
package/lib/init-docs.mjs
CHANGED
|
@@ -1,191 +1,328 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* lib/init-docs.mjs —
|
|
3
|
+
* lib/init-docs.mjs — stand up a documentation system for a project.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Intentionally separate from `construct init`. Creates the docs surface only:
|
|
6
|
+
* docs/README.md plus selected lane directories such as docs/adr/, docs/intake/,
|
|
7
|
+
* docs/memos/, docs/notes/, docs/prds/, and docs/rfcs/ with starter templates
|
|
8
|
+
* copied into per-lane templates/ directories from Construct's template library.
|
|
8
9
|
*
|
|
9
10
|
* Usage:
|
|
10
|
-
* node lib/init-docs.mjs [target-path] [--yes]
|
|
11
|
-
* construct init-docs [path] [--yes]
|
|
11
|
+
* node lib/init-docs.mjs [target-path] [--yes] [--docs=prds,rfcs,adrs] [--with-architecture] [--suggest-org] [--organize]
|
|
12
|
+
* construct init-docs [path] [--yes] [--docs=prds,rfcs,adrs] [--with-architecture] [--suggest-org] [--organize]
|
|
13
|
+
*
|
|
14
|
+
* Flags:
|
|
15
|
+
* --yes Skip interactive prompts and use defaults.
|
|
16
|
+
* --docs Comma-separated list of lanes to initialize (default: adrs,intake,memos,notes,prds).
|
|
17
|
+
* --with-architecture Also create docs/architecture.md.
|
|
18
|
+
* --suggest-org Scan existing .md files and suggest where they might belong (no changes made).
|
|
19
|
+
* --organize Actually move files to suggested locations (implies --suggest-org, requires --yes to avoid prompts).
|
|
12
20
|
*/
|
|
13
21
|
|
|
14
22
|
import fs from "node:fs";
|
|
15
23
|
import path from "node:path";
|
|
16
|
-
import readline from "node:readline";
|
|
17
|
-
import { execSync } from "node:child_process";
|
|
18
24
|
import { fileURLToPath } from "node:url";
|
|
19
|
-
|
|
20
|
-
import {
|
|
25
|
+
|
|
26
|
+
import { suggestDocsLaneForFile } from './docs-routing.mjs';
|
|
27
|
+
import { stampFrontmatter } from "./doc-stamp.mjs";
|
|
28
|
+
import readline from "node:readline";
|
|
21
29
|
|
|
22
30
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
23
31
|
const ROOT_DIR = path.join(__dirname, "..");
|
|
32
|
+
const TEMPLATE_DIR = path.join(ROOT_DIR, "templates", "docs");
|
|
24
33
|
|
|
25
34
|
const args = process.argv.slice(2);
|
|
26
35
|
const skipInteractive = args.includes("--yes") || !process.stdin.isTTY;
|
|
27
|
-
const
|
|
36
|
+
const docsArg = args.find((arg) => arg.startsWith("--docs="));
|
|
37
|
+
const extrasArg = args.find((arg) => arg.startsWith("--extras="));
|
|
38
|
+
const withArchitectureFlag = args.includes("--with-architecture");
|
|
39
|
+
const suggestOrg = args.includes("--suggest-org");
|
|
40
|
+
const organize = args.includes("--organize");
|
|
41
|
+
const targetArg = args.find((arg) => !arg.startsWith("--"));
|
|
28
42
|
const target = path.resolve(targetArg ?? process.cwd());
|
|
29
43
|
|
|
30
|
-
const
|
|
44
|
+
const docsRootArg = args.find((arg) => arg.startsWith('--docs-root='));
|
|
45
|
+
const docsRootRelative = docsRootArg ? docsRootArg.split('=')[1] : 'docs';
|
|
46
|
+
const docsDir = path.join(target, docsRootRelative);
|
|
31
47
|
|
|
32
|
-
|
|
48
|
+
const created = [];
|
|
49
|
+
const skipped = [];
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
const DOC_LANES = {
|
|
52
|
+
adrs: {
|
|
53
|
+
title: "ADRs",
|
|
54
|
+
dir: "adr",
|
|
55
|
+
description: "Architecture decision records for decisions that have already been made.",
|
|
56
|
+
templates: ["adr.md"],
|
|
57
|
+
},
|
|
58
|
+
briefs: {
|
|
59
|
+
title: "Briefs",
|
|
60
|
+
dir: "briefs",
|
|
61
|
+
description: "Research, evidence, signal, and one-pager style documents.",
|
|
62
|
+
templates: [
|
|
63
|
+
"research-brief.md",
|
|
64
|
+
"evidence-brief.md",
|
|
65
|
+
"signal-brief.md",
|
|
66
|
+
"one-pager.md",
|
|
67
|
+
"customer-profile.md",
|
|
68
|
+
"product-intelligence-report.md",
|
|
69
|
+
"backlog-proposal.md",
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
changelogs: {
|
|
73
|
+
title: "Changelogs",
|
|
74
|
+
dir: "changelogs",
|
|
75
|
+
description: "User-facing release notes and version history entries.",
|
|
76
|
+
templates: ["changelog-entry.md"],
|
|
77
|
+
},
|
|
78
|
+
intake: {
|
|
79
|
+
title: "Intake",
|
|
80
|
+
dir: "intake",
|
|
81
|
+
description: "Intake batch records that explain what arrived, why it matters, and how it should be ingested.",
|
|
82
|
+
templates: ["__intake-template__"],
|
|
83
|
+
},
|
|
84
|
+
memos: {
|
|
85
|
+
title: "Memos",
|
|
86
|
+
dir: "memos",
|
|
87
|
+
description: "Decision memos and internal arguments for alignment and approval.",
|
|
88
|
+
templates: ["memo.md"],
|
|
89
|
+
},
|
|
90
|
+
meetings: {
|
|
91
|
+
title: 'Meetings',
|
|
92
|
+
dir: 'meetings',
|
|
93
|
+
description: 'Meeting notes, minutes, retros, standups, planning sessions, and agendas.',
|
|
94
|
+
templates: ['__meeting-notes-template__'],
|
|
95
|
+
},
|
|
96
|
+
notes: {
|
|
97
|
+
title: "Notes",
|
|
98
|
+
dir: "notes",
|
|
99
|
+
description: "Working notes and lightweight durable context outside formal docs or meetings.",
|
|
100
|
+
templates: ["__notes-template__"],
|
|
101
|
+
},
|
|
102
|
+
onboarding: {
|
|
103
|
+
title: "Onboarding",
|
|
104
|
+
dir: "onboarding",
|
|
105
|
+
description: "Runnable setup guides and first-day workflows for engineers, product, or ops.",
|
|
106
|
+
templates: ["onboarding.md"],
|
|
107
|
+
},
|
|
108
|
+
postmortems: {
|
|
109
|
+
title: "Postmortems",
|
|
110
|
+
dir: "postmortems",
|
|
111
|
+
description: "Blameless incident reports: timeline, root cause, contributing factors, and corrective actions.",
|
|
112
|
+
templates: ["incident-report.md"],
|
|
113
|
+
},
|
|
114
|
+
prds: {
|
|
115
|
+
title: "PRDs",
|
|
116
|
+
dir: "prds",
|
|
117
|
+
description: "Product and capability requirement documents.",
|
|
118
|
+
templates: ["prd.md", "meta-prd.md", "prd-business.md", "prd-platform.md", "prfaq.md"],
|
|
119
|
+
},
|
|
120
|
+
rfcs: {
|
|
121
|
+
title: "RFCs",
|
|
122
|
+
dir: "rfcs",
|
|
123
|
+
description: "Architecture and implementation proposals that need review before a decision.",
|
|
124
|
+
templates: ["rfc.md", "rfc-platform.md"],
|
|
125
|
+
},
|
|
126
|
+
runbooks: {
|
|
127
|
+
title: "Runbooks",
|
|
128
|
+
dir: "runbooks",
|
|
129
|
+
description: "Operational procedures, diagnostics, remediation, and escalation paths.",
|
|
130
|
+
templates: ["runbook.md"],
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const DOC_PRESETS = {
|
|
135
|
+
lean: ["adrs", "intake", "memos", "meetings", "notes", "prds"],
|
|
136
|
+
product: ["adrs", "intake", "memos", "meetings", "notes", "prds", "rfcs"],
|
|
137
|
+
full: ["adrs", "briefs", "changelogs", "intake", "memos", "meetings", "notes", "onboarding", "postmortems", "prds", "rfcs", "runbooks"],
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const DEFAULT_LANES = DOC_PRESETS.lean;
|
|
141
|
+
const LANE_ORDER = ["adrs", "briefs", "changelogs", "intake", "memos", "meetings", "notes", "onboarding", "postmortems", "prds", "rfcs", "runbooks"];
|
|
142
|
+
const LANE_ALIASES = {
|
|
143
|
+
adr: "adrs",
|
|
144
|
+
adrs: "adrs",
|
|
145
|
+
brief: "briefs",
|
|
146
|
+
briefs: "briefs",
|
|
147
|
+
changelog: "changelogs",
|
|
148
|
+
changelogs: "changelogs",
|
|
149
|
+
releases: "changelogs",
|
|
150
|
+
release: "changelogs",
|
|
151
|
+
intake: "intake",
|
|
152
|
+
memo: "memos",
|
|
153
|
+
memos: "memos",
|
|
154
|
+
meeting: 'meetings',
|
|
155
|
+
meetings: 'meetings',
|
|
156
|
+
minutes: 'meetings',
|
|
157
|
+
retro: 'meetings',
|
|
158
|
+
note: "notes",
|
|
159
|
+
notes: "notes",
|
|
160
|
+
onboard: "onboarding",
|
|
161
|
+
onboarding: "onboarding",
|
|
162
|
+
postmortem: "postmortems",
|
|
163
|
+
postmortems: "postmortems",
|
|
164
|
+
incident: "postmortems",
|
|
165
|
+
incidents: "postmortems",
|
|
166
|
+
prd: "prds",
|
|
167
|
+
prds: "prds",
|
|
168
|
+
rfc: "rfcs",
|
|
169
|
+
rfcs: "rfcs",
|
|
170
|
+
runbook: "runbooks",
|
|
171
|
+
runbooks: "runbooks",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
function inferProjectName(targetPath) {
|
|
175
|
+
const packageJsonPath = path.join(targetPath, "package.json");
|
|
176
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
177
|
+
try {
|
|
178
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
179
|
+
return pkg.name || path.basename(targetPath);
|
|
180
|
+
} catch {}
|
|
44
181
|
}
|
|
182
|
+
return path.basename(targetPath);
|
|
45
183
|
}
|
|
46
184
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (anthropicKey) {
|
|
54
|
-
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
55
|
-
method: "POST",
|
|
56
|
-
headers: {
|
|
57
|
-
"x-api-key": anthropicKey,
|
|
58
|
-
"anthropic-version": "2023-06-01",
|
|
59
|
-
"content-type": "application/json",
|
|
60
|
-
},
|
|
61
|
-
body: JSON.stringify({ model: FAST_MODEL, max_tokens: 4096, system, messages }),
|
|
62
|
-
});
|
|
63
|
-
if (!res.ok) throw new Error(`Anthropic API ${res.status}: ${await res.text()}`);
|
|
64
|
-
const data = await res.json();
|
|
65
|
-
return data.content[0].text;
|
|
66
|
-
}
|
|
185
|
+
function parseCsvList(value) {
|
|
186
|
+
return value
|
|
187
|
+
.split(",")
|
|
188
|
+
.map((entry) => entry.trim())
|
|
189
|
+
.filter(Boolean);
|
|
190
|
+
}
|
|
67
191
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
192
|
+
const NO_ANSWER_PATTERNS = new Set([
|
|
193
|
+
"",
|
|
194
|
+
"n",
|
|
195
|
+
"no",
|
|
196
|
+
"none",
|
|
197
|
+
"nope",
|
|
198
|
+
"nah",
|
|
199
|
+
"nothing",
|
|
200
|
+
"blank",
|
|
201
|
+
"skip",
|
|
202
|
+
"no thanks",
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
const ALL_ANSWER_PATTERNS = new Set([
|
|
206
|
+
"all",
|
|
207
|
+
"all of them",
|
|
208
|
+
"everything",
|
|
209
|
+
"default",
|
|
210
|
+
"defaults",
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
function normalizeAnswer(value) {
|
|
214
|
+
return value.trim().toLowerCase().replace(/\s+/g, " ");
|
|
215
|
+
}
|
|
87
216
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// ─── Project context ──────────────────────────────────────────────────────────
|
|
92
|
-
|
|
93
|
-
function dirTree(dir, depth = 0, maxDepth = 2) {
|
|
94
|
-
if (depth > maxDepth) return [];
|
|
95
|
-
const skip = new Set(["node_modules", ".git", ".next", "dist", "build", "out",
|
|
96
|
-
"__pycache__", ".venv", "vendor", "target", ".turbo", "coverage"]);
|
|
97
|
-
let items;
|
|
98
|
-
try { items = fs.readdirSync(dir); } catch { return []; }
|
|
99
|
-
const entries = [];
|
|
100
|
-
for (const item of items.sort()) {
|
|
101
|
-
if (skip.has(item) || (item.startsWith(".") && depth === 0 && item !== ".cx")) continue;
|
|
102
|
-
const full = path.join(dir, item);
|
|
103
|
-
let stat;
|
|
104
|
-
try { stat = fs.statSync(full); } catch { continue; }
|
|
105
|
-
if (stat.isDirectory()) {
|
|
106
|
-
entries.push(`${" ".repeat(depth)}${item}/`);
|
|
107
|
-
entries.push(...dirTree(full, depth + 1, maxDepth));
|
|
108
|
-
} else {
|
|
109
|
-
entries.push(`${" ".repeat(depth)}${item}`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return entries;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function gatherContext(targetPath) {
|
|
116
|
-
const ctx = { name: path.basename(targetPath) };
|
|
117
|
-
|
|
118
|
-
for (const name of ["package.json", "pyproject.toml", "Cargo.toml", "go.mod", "build.gradle", "composer.json"]) {
|
|
119
|
-
const p = path.join(targetPath, name);
|
|
120
|
-
if (!fs.existsSync(p)) continue;
|
|
121
|
-
if (name === "package.json") {
|
|
122
|
-
try {
|
|
123
|
-
const pkg = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
124
|
-
ctx.name = pkg.name ?? ctx.name;
|
|
125
|
-
ctx.description = pkg.description;
|
|
126
|
-
const deps = Object.keys({ ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) });
|
|
127
|
-
ctx.dependencies = deps.slice(0, 40);
|
|
128
|
-
ctx.scripts = Object.keys(pkg.scripts ?? {});
|
|
129
|
-
} catch {}
|
|
130
|
-
} else {
|
|
131
|
-
ctx.manifest = fs.readFileSync(p, "utf8").slice(0, 600);
|
|
132
|
-
}
|
|
133
|
-
ctx.manifestFile = name;
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
217
|
+
function isNegativeAnswer(value) {
|
|
218
|
+
return NO_ANSWER_PATTERNS.has(normalizeAnswer(value));
|
|
219
|
+
}
|
|
136
220
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
ctx.readme = fs.readFileSync(p, "utf8").slice(0, 2000);
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
221
|
+
function isAllAnswer(value) {
|
|
222
|
+
return ALL_ANSWER_PATTERNS.has(normalizeAnswer(value));
|
|
223
|
+
}
|
|
144
224
|
|
|
145
|
-
|
|
225
|
+
function parseLaneSelection(value) {
|
|
226
|
+
const normalized = normalizeAnswer(value);
|
|
227
|
+
if (!value.trim() || isAllAnswer(value)) return DEFAULT_LANES;
|
|
228
|
+
if (isNegativeAnswer(value)) return [];
|
|
229
|
+
if (DOC_PRESETS[normalized]) return DOC_PRESETS[normalized];
|
|
230
|
+
return parseSelectableLanes(value);
|
|
231
|
+
}
|
|
146
232
|
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
233
|
+
function parseExtraLaneSelection(value) {
|
|
234
|
+
if (!value.trim() || isNegativeAnswer(value)) return [];
|
|
235
|
+
return parseCsvList(value)
|
|
236
|
+
.map(normalizeCustomLaneName)
|
|
237
|
+
.filter(Boolean);
|
|
238
|
+
}
|
|
151
239
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
240
|
+
function parseSelectableLanes(value) {
|
|
241
|
+
return value
|
|
242
|
+
.split(/[,\n]/)
|
|
243
|
+
.map((entry) => entry.trim())
|
|
244
|
+
.filter(Boolean)
|
|
245
|
+
.map((entry) => {
|
|
246
|
+
if (/^\d+$/.test(entry)) {
|
|
247
|
+
const lane = LANE_ORDER[Number(entry) - 1];
|
|
248
|
+
return lane ?? "";
|
|
249
|
+
}
|
|
250
|
+
return normalizeLaneKey(entry);
|
|
251
|
+
})
|
|
252
|
+
.filter((lane) => lane in DOC_LANES);
|
|
253
|
+
}
|
|
156
254
|
|
|
157
|
-
|
|
255
|
+
function parseBooleanAnswer(value, defaultValue = false) {
|
|
256
|
+
if (!value.trim()) return defaultValue;
|
|
257
|
+
const normalized = normalizeAnswer(value);
|
|
258
|
+
if (["y", "yes", "true"].includes(normalized)) return true;
|
|
259
|
+
if (["n", "no", "false", "nope", "nah"].includes(normalized)) return false;
|
|
260
|
+
return defaultValue;
|
|
158
261
|
}
|
|
159
262
|
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (ctx.scripts?.length) parts.push(`npm scripts: ${ctx.scripts.join(", ")}`);
|
|
167
|
-
if (ctx.manifest) parts.push(`\nManifest content:\n${ctx.manifest}`);
|
|
168
|
-
if (ctx.readme) parts.push(`\nREADME:\n${ctx.readme}`);
|
|
169
|
-
if (ctx.structure) parts.push(`\nDirectory structure:\n${ctx.structure}`);
|
|
170
|
-
if (ctx.existingDocs) parts.push(`\nExisting docs/: ${ctx.existingDocs}`);
|
|
171
|
-
return parts.join("\n");
|
|
263
|
+
function normalizeCustomLaneName(name) {
|
|
264
|
+
return name
|
|
265
|
+
.trim()
|
|
266
|
+
.toLowerCase()
|
|
267
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
268
|
+
.replace(/^-+|-+$/g, "");
|
|
172
269
|
}
|
|
173
270
|
|
|
174
|
-
|
|
271
|
+
function normalizeLaneKey(name) {
|
|
272
|
+
return LANE_ALIASES[name.trim().toLowerCase()] ?? name.trim().toLowerCase();
|
|
273
|
+
}
|
|
175
274
|
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
const raw = (fence ? fence[1] : text).trim();
|
|
179
|
-
const start = raw.indexOf("{");
|
|
180
|
-
const end = raw.lastIndexOf("}");
|
|
181
|
-
if (start === -1 || end === -1) throw new Error("No JSON object found in response");
|
|
182
|
-
return JSON.parse(raw.slice(start, end + 1));
|
|
275
|
+
function repoHasAny(targetDir, candidates) {
|
|
276
|
+
return candidates.some((candidate) => fs.existsSync(path.join(targetDir, candidate)));
|
|
183
277
|
}
|
|
184
278
|
|
|
185
|
-
|
|
279
|
+
function scanNames(targetDir, maxDepth = 2) {
|
|
280
|
+
const ignored = new Set(['.git', 'node_modules', '.next', 'dist', 'build', 'coverage', '.cx', 'docs']);
|
|
281
|
+
const names = [];
|
|
282
|
+
|
|
283
|
+
function walk(dir, depth) {
|
|
284
|
+
if (depth > maxDepth) return;
|
|
285
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
286
|
+
for (const entry of entries) {
|
|
287
|
+
if (ignored.has(entry.name)) continue;
|
|
288
|
+
names.push(entry.name.toLowerCase());
|
|
289
|
+
if (entry.isDirectory()) walk(path.join(dir, entry.name), depth + 1);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
186
292
|
|
|
187
|
-
|
|
188
|
-
|
|
293
|
+
walk(targetDir, 0);
|
|
294
|
+
return names;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function suggestContextualLanes(targetDir) {
|
|
298
|
+
const suggestions = [];
|
|
299
|
+
const names = scanNames(targetDir);
|
|
300
|
+
const hasKeyword = (keywords) => keywords.some((keyword) => names.some((name) => name.includes(keyword)));
|
|
301
|
+
|
|
302
|
+
if (repoHasAny(targetDir, ['package.json', 'src', 'lib', 'apps', 'services', 'api']) || hasKeyword(['proposal', 'interface', 'contract', 'schema', 'openapi'])) {
|
|
303
|
+
suggestions.push({ lane: 'rfcs', reason: 'codebase and interface changes usually benefit from proposal docs' });
|
|
304
|
+
}
|
|
305
|
+
if (repoHasAny(targetDir, ['Dockerfile', 'deploy', 'infra', '.github', 'ops', 'terraform']) || hasKeyword(['incident', 'deploy', 'runbook', 'oncall'])) {
|
|
306
|
+
suggestions.push({ lane: 'runbooks', reason: 'deployment and operations files suggest an ops lane is useful' });
|
|
307
|
+
}
|
|
308
|
+
if (repoHasAny(targetDir, ['Dockerfile', 'deploy', 'infra', '.github']) || hasKeyword(['incident', 'postmortem', 'oncall', 'sev', 'pagerduty'])) {
|
|
309
|
+
suggestions.push({ lane: 'postmortems', reason: 'ops setup suggests an incident post-mortem lane is useful' });
|
|
310
|
+
}
|
|
311
|
+
if (repoHasAny(targetDir, ['CHANGELOG.md', 'CHANGELOG', 'RELEASES.md']) || hasKeyword(['changelog', 'release', 'version'])) {
|
|
312
|
+
suggestions.push({ lane: 'changelogs', reason: 'existing changelog or release files suggest a changelogs lane' });
|
|
313
|
+
}
|
|
314
|
+
if (repoHasAny(targetDir, ['onboarding', 'setup', 'getting-started']) || hasKeyword(['onboarding', 'setup', 'getting-started', 'local-dev'])) {
|
|
315
|
+
suggestions.push({ lane: 'onboarding', reason: 'setup or onboarding files suggest an onboarding lane' });
|
|
316
|
+
}
|
|
317
|
+
if (hasKeyword(['research', 'brief', 'customer', 'interview', 'market', 'competitive', 'signal'])) {
|
|
318
|
+
suggestions.push({ lane: 'briefs', reason: 'research-style source material suggests a briefs lane' });
|
|
319
|
+
}
|
|
320
|
+
if (hasKeyword(['meeting', 'minutes', 'standup', 'retro', 'agenda', 'sync', '1:1'])) {
|
|
321
|
+
suggestions.push({ lane: 'meetings', reason: 'meeting artifacts suggest a dedicated meetings lane' });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return suggestions.filter((suggestion, index, arr) => arr.findIndex((item) => item.lane === suggestion.lane) === index);
|
|
325
|
+
}
|
|
189
326
|
|
|
190
327
|
function writeIfMissing(filePath, content) {
|
|
191
328
|
if (fs.existsSync(filePath)) {
|
|
@@ -193,190 +330,493 @@ function writeIfMissing(filePath, content) {
|
|
|
193
330
|
return;
|
|
194
331
|
}
|
|
195
332
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
196
|
-
|
|
333
|
+
const stamped = filePath.endsWith(".md")
|
|
334
|
+
? stampFrontmatter(content, { generator: "construct/init-docs" })
|
|
335
|
+
: content;
|
|
336
|
+
fs.writeFileSync(filePath, stamped, "utf8");
|
|
197
337
|
created.push(path.relative(target, filePath));
|
|
198
338
|
}
|
|
199
339
|
|
|
200
|
-
|
|
340
|
+
function sortLaneKeys(lanes) {
|
|
341
|
+
return [...lanes].sort((a, b) => {
|
|
342
|
+
const left = DOC_LANES[a]?.title ?? titleCase(a);
|
|
343
|
+
const right = DOC_LANES[b]?.title ?? titleCase(b);
|
|
344
|
+
return left.localeCompare(right);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function buildDocsReadme(projectName, selectedLanes) {
|
|
349
|
+
const laneLines = sortLaneKeys(selectedLanes).map((lane) => {
|
|
350
|
+
const label = DOC_LANES[lane]?.title ?? titleCase(lane);
|
|
351
|
+
const laneDir = DOC_LANES[lane]?.dir ?? lane;
|
|
352
|
+
const desc = DOC_LANES[lane]?.description ?? "Custom documentation lane.";
|
|
353
|
+
return `- [${label}](./${laneDir}/) — ${desc}`;
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return `<!--
|
|
357
|
+
docs/README.md — documentation index and maintenance contract.
|
|
358
|
+
|
|
359
|
+
Generated by \`construct init-docs\`. Keep this file aligned with the actual doc
|
|
360
|
+
lanes in the repo. Update it when lanes are added, removed, or repurposed, and
|
|
361
|
+
prune stale links instead of letting the doc surface drift.
|
|
362
|
+
-->
|
|
201
363
|
|
|
202
|
-
|
|
203
|
-
writeIfMissing(path.join(target, ".cx", "context.json"),
|
|
204
|
-
`${JSON.stringify({ format: "json", savedAt: new Date().toISOString(), source: "init-docs", projectName, activeWork: [], recentDecisions: [], architectureNotes: [], openQuestions: [] }, null, 2)}\n`);
|
|
205
|
-
writeIfMissing(path.join(target, ".cx", "context.md"),
|
|
206
|
-
`# Project Context\n\n> Required project state. Keep this file updated. All LLMs working in this repo, including Construct, read it at session start and must keep it current. Keep under 100 lines.\n\n## Active Work\n\n## Recent Decisions\n\n## Architecture Notes\n\n## Open Questions\n`);
|
|
207
|
-
writeIfMissing(path.join(target, ".cx", "workflow.json"), workflowJson);
|
|
208
|
-
writeIfMissing(path.join(target, ".cx", "decisions", "_template.md"),
|
|
209
|
-
`# ADR-{NNN}: {title}\n\nDate: {YYYY-MM-DD}\nStatus: proposed | accepted | deprecated\n\n## Context\n\n## Decision\n\n## Consequences\n`);
|
|
210
|
-
writeIfMissing(path.join(target, "docs", "README.md"),
|
|
211
|
-
`# ${projectName} — Documentation
|
|
364
|
+
# ${projectName} Documentation
|
|
212
365
|
|
|
213
|
-
>
|
|
366
|
+
> This docs surface is the canonical home for long-lived project documents such as ADRs, briefs, intake material, memos, notes, PRDs, RFCs, and runbooks.
|
|
214
367
|
|
|
215
|
-
##
|
|
368
|
+
## Operating model
|
|
216
369
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
| docs/README.md | Docs index and documentation contract | Core docs set or maintenance expectations change |
|
|
223
|
-
| docs/architecture.md | Canonical architecture and invariants | Runtime shape, contracts, boundaries, or major dependencies change |
|
|
370
|
+
- Use Beads or the project's external tracker for durable task tracking.
|
|
371
|
+
- Use \`plan.md\` for the current implementation plan.
|
|
372
|
+
- Use this \`docs/\` tree for durable narrative artifacts and decision records.
|
|
373
|
+
- If multiple agent or harness sessions are active, use a single writer per file and coordinate handoffs in the tracker or \`plan.md\`.
|
|
374
|
+
- Prune stale sections and directories when they stop matching how the repo is actually run.
|
|
224
375
|
|
|
225
|
-
##
|
|
376
|
+
## Lanes
|
|
226
377
|
|
|
227
|
-
|
|
228
|
-
- [Runbooks](./runbooks/)
|
|
229
|
-
- [ADRs](../.cx/decisions/)
|
|
378
|
+
${laneLines.join("\n")}
|
|
230
379
|
|
|
231
380
|
## Maintenance rule
|
|
232
381
|
|
|
233
|
-
If
|
|
234
|
-
|
|
235
|
-
writeIfMissing(path.join(target, "docs", "architecture.md"),
|
|
236
|
-
`# ${projectName} Architecture\n\n> Required project state. Keep this file updated when system shape, contracts, boundaries, or dependencies materially change. All LLMs working in this repo should treat it as canonical architecture context.\n\n## System overview\n\nDescribe the main runtime shape, primary modules, and external dependencies.\n\n## Core layers\n\n- CLI / entrypoints\n- Application/runtime modules\n- State, data, and storage\n- External integrations\n\n## Key invariants\n\n- Public surface and ownership boundaries\n- Data/contract expectations\n- Safety or review gates\n`);
|
|
237
|
-
writeIfMissing(path.join(target, "docs", "runbooks", "README.md"),
|
|
238
|
-
`# Runbooks\n\n## Contents\n\n- Local development startup\n- Verification / health checks\n- Incident recovery\n- Release checklist\n`);
|
|
382
|
+
If a document lane stops serving a real purpose, remove it or archive it intentionally. This tree should stay opinionated and current, not become a graveyard of stale templates.
|
|
383
|
+
`;
|
|
239
384
|
}
|
|
240
385
|
|
|
241
|
-
|
|
386
|
+
function buildArchitectureDoc(projectName, selectedLanes) {
|
|
387
|
+
const laneLines = sortLaneKeys(selectedLanes).map((lane) => {
|
|
388
|
+
const label = DOC_LANES[lane]?.title ?? titleCase(lane);
|
|
389
|
+
const desc = DOC_LANES[lane]?.description ?? "Custom documentation lane.";
|
|
390
|
+
return `- **${label}** — ${desc}`;
|
|
391
|
+
});
|
|
242
392
|
|
|
243
|
-
|
|
393
|
+
return `<!--
|
|
394
|
+
docs/architecture.md — canonical architecture context and documentation-system contract.
|
|
244
395
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
396
|
+
Generated by \`construct init-docs\`. Update this file when the system shape,
|
|
397
|
+
ownership boundaries, or documentation operating model changes. Remove stale
|
|
398
|
+
assumptions as soon as they stop matching the codebase.
|
|
399
|
+
-->
|
|
248
400
|
|
|
249
|
-
|
|
250
|
-
Each question should have a short, specific answer (a few words or a sentence).
|
|
401
|
+
# ${projectName} Architecture
|
|
251
402
|
|
|
252
|
-
|
|
253
|
-
{
|
|
254
|
-
"questions": [
|
|
255
|
-
{ "id": "q1", "question": "..." },
|
|
256
|
-
{ "id": "q2", "question": "..." }
|
|
257
|
-
]
|
|
258
|
-
}`;
|
|
403
|
+
## System overview
|
|
259
404
|
|
|
260
|
-
|
|
405
|
+
Describe the runtime shape, major modules, external dependencies, and key data boundaries.
|
|
261
406
|
|
|
262
|
-
|
|
263
|
-
meaningful placeholder content, and examples that match the actual tech stack and project type.
|
|
407
|
+
## Project-state hierarchy
|
|
264
408
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
-
|
|
268
|
-
|
|
269
|
-
- Use the provided .cx/workflow.json verbatim
|
|
270
|
-
- Only include docs that make sense for THIS project — skip what doesn't apply
|
|
271
|
-
- Prefer fewer, higher-quality files over many hollow ones
|
|
272
|
-
- For template files (e.g. ADR template), use the filename _template.md inside the relevant folder
|
|
409
|
+
1. External tracker, preferably Beads, owns the durable backlog and issue status.
|
|
410
|
+
2. \`plan.md\` owns the current human-readable implementation plan.
|
|
411
|
+
3. cass-memory through MCP \`memory\` stores cross-session observations and preferences.
|
|
412
|
+
4. \`docs/\` stores durable narrative artifacts such as ADRs, briefs, intake notes, memos, notes, PRDs, RFCs, and runbooks.
|
|
273
413
|
|
|
274
|
-
|
|
275
|
-
{
|
|
276
|
-
"summary": "One sentence: what was generated and why",
|
|
277
|
-
"files": [
|
|
278
|
-
{ "path": "relative/path/file.md", "content": "complete file content" }
|
|
279
|
-
]
|
|
280
|
-
}`;
|
|
414
|
+
## Documentation lanes
|
|
281
415
|
|
|
282
|
-
|
|
416
|
+
${laneLines.join("\n")}
|
|
283
417
|
|
|
284
|
-
|
|
285
|
-
console.log(`\nConstruct init-docs → ${target}\n`);
|
|
286
|
-
|
|
287
|
-
const ctx = gatherContext(target);
|
|
288
|
-
const contextText = contextToText(ctx);
|
|
289
|
-
const workflowJson = JSON.stringify(defaultWorkflow(target, ctx.name), null, 2) + "\n";
|
|
290
|
-
|
|
291
|
-
// Attempt AI path
|
|
292
|
-
let questionsResponse = null;
|
|
293
|
-
try {
|
|
294
|
-
process.stdout.write(" Analyzing project...");
|
|
295
|
-
questionsResponse = await callModel([{ role: "user", content: contextText }], SYSTEM_QUESTIONS);
|
|
296
|
-
process.stdout.write(" done\n\n");
|
|
297
|
-
} catch (err) {
|
|
298
|
-
process.stdout.write(` failed (${err.message})\n`);
|
|
299
|
-
}
|
|
418
|
+
## Key invariants
|
|
300
419
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
420
|
+
- Keep one source of truth per concern instead of parallel trackers.
|
|
421
|
+
- When multiple agent or harness sessions run in parallel, use a single writer per file.
|
|
422
|
+
- Update or prune stale docs when work changes project reality.
|
|
423
|
+
- Prefer adding a lane only when it has a distinct audience and decision rhythm.
|
|
424
|
+
`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function buildLaneReadme(laneKey) {
|
|
428
|
+
const lane = DOC_LANES[laneKey];
|
|
429
|
+
const title = lane?.title ?? titleCase(laneKey);
|
|
430
|
+
const description = lane?.description ?? "Custom documentation lane.";
|
|
431
|
+
const dirName = lane?.dir ?? laneKey;
|
|
432
|
+
const templateLines = (lane?.templates ?? []).map((templateName, index) => {
|
|
433
|
+
const filename = index === 0 ? "_template.md" : templateName.replace(/\.md$/, ".template.md");
|
|
434
|
+
return `- [${filename}](./templates/${filename})`;
|
|
435
|
+
});
|
|
436
|
+
const usageSection = laneKey === "intake"
|
|
437
|
+
? `
|
|
438
|
+
## Intake flow
|
|
439
|
+
|
|
440
|
+
- Drop source files into either [\`.cx/inbox/\`](../../.cx/inbox/) or this lane's directory when you want Construct to ingest them.
|
|
441
|
+
- Run \`construct ingest ./docs/intake --sync\` or \`construct ingest ./.cx/inbox --sync\` to convert supported files into retrieval-ready markdown, or let the embed daemon watch those drop zones automatically.
|
|
442
|
+
- Durable ingested knowledge lands under \`.cx/knowledge/internal/\` by default, which is where Construct's learning and search paths already operate.
|
|
443
|
+
`
|
|
444
|
+
: "";
|
|
445
|
+
|
|
446
|
+
return `<!--
|
|
447
|
+
docs/${dirName}/README.md — lane guide for ${title}.
|
|
448
|
+
|
|
449
|
+
Generated by \`construct init-docs\`. Keep this lane focused on one document
|
|
450
|
+
family. If it no longer has a distinct purpose, prune it or merge it elsewhere.
|
|
451
|
+
-->
|
|
452
|
+
|
|
453
|
+
# ${title}
|
|
454
|
+
|
|
455
|
+
${description}
|
|
456
|
+
|
|
457
|
+
## Starter templates
|
|
458
|
+
|
|
459
|
+
${templateLines.join("\n")}
|
|
460
|
+
${usageSection}
|
|
461
|
+
`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function buildCustomLaneReadme(laneDir) {
|
|
465
|
+
return `<!--
|
|
466
|
+
docs/${laneDir}/README.md — custom documentation lane.
|
|
467
|
+
|
|
468
|
+
Generated by \`construct init-docs\`. Rename, refine, or remove this lane once
|
|
469
|
+
its real purpose is clear. Do not keep placeholder structures around indefinitely.
|
|
470
|
+
-->
|
|
471
|
+
|
|
472
|
+
# ${titleCase(laneDir)}
|
|
473
|
+
|
|
474
|
+
Custom documentation lane for this project.
|
|
475
|
+
|
|
476
|
+
## Starter templates
|
|
477
|
+
|
|
478
|
+
- [\`_template.md\`](./templates/_template.md)
|
|
479
|
+
`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function buildCustomLaneTemplate(laneDir) {
|
|
483
|
+
return `<!--
|
|
484
|
+
docs/${laneDir}/_template.md — starter template for a custom documentation lane.
|
|
485
|
+
|
|
486
|
+
Replace this with a real template once the lane's purpose is clear. If the lane
|
|
487
|
+
never becomes meaningful, delete the lane instead of keeping placeholder docs.
|
|
488
|
+
-->
|
|
489
|
+
|
|
490
|
+
# ${titleCase(laneDir)}: {title}
|
|
491
|
+
|
|
492
|
+
- **Date**: {YYYY-MM-DD}
|
|
493
|
+
- **Author**: {name}
|
|
494
|
+
- **Status**: draft | active | superseded
|
|
495
|
+
|
|
496
|
+
## Summary
|
|
497
|
+
|
|
498
|
+
<!-- What this document exists to explain or decide. -->
|
|
499
|
+
|
|
500
|
+
## Context
|
|
501
|
+
|
|
502
|
+
<!-- Why this matters now and what the reader needs to know first. -->
|
|
503
|
+
|
|
504
|
+
## Details
|
|
505
|
+
|
|
506
|
+
<!-- The actual content for this lane. -->
|
|
507
|
+
|
|
508
|
+
## Decisions or next steps
|
|
509
|
+
|
|
510
|
+
<!-- What changes because of this document. -->
|
|
511
|
+
|
|
512
|
+
## References
|
|
513
|
+
|
|
514
|
+
<!-- Links to related docs, code, or evidence. -->
|
|
515
|
+
`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function buildNotesTemplate() {
|
|
519
|
+
return `<!--
|
|
520
|
+
docs/notes/templates/_template.md — starter template for durable project notes.
|
|
521
|
+
|
|
522
|
+
Keep notes concise, dated, and easy to skim. Promote major decisions into ADRs,
|
|
523
|
+
PRDs, or RFCs when they stop being just notes.
|
|
524
|
+
-->
|
|
525
|
+
|
|
526
|
+
# Note: {title}
|
|
527
|
+
|
|
528
|
+
- **Date**: {YYYY-MM-DD}
|
|
529
|
+
- **Author**: {name}
|
|
530
|
+
- **Topic**: {topic}
|
|
531
|
+
|
|
532
|
+
## Summary
|
|
533
|
+
|
|
534
|
+
<!-- One-paragraph summary. -->
|
|
535
|
+
|
|
536
|
+
## Details
|
|
537
|
+
|
|
538
|
+
<!-- Main notes. -->
|
|
539
|
+
|
|
540
|
+
## Follow-ups
|
|
541
|
+
|
|
542
|
+
<!-- Next actions, questions, or references. -->
|
|
543
|
+
`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function buildMeetingNotesTemplate() {
|
|
547
|
+
return `<!--
|
|
548
|
+
docs/notes/meetings/_template.md — starter template for meeting notes.
|
|
549
|
+
|
|
550
|
+
Use this for meeting minutes, standups, reviews, planning, retros, and working sessions.
|
|
551
|
+
Promote durable decisions into ADRs, memos, or PRDs when needed.
|
|
552
|
+
-->
|
|
553
|
+
|
|
554
|
+
# Meeting: {title}
|
|
555
|
+
|
|
556
|
+
- **Date**: {YYYY-MM-DD}
|
|
557
|
+
- **Attendees**: {names}
|
|
558
|
+
- **Type**: standup | planning | retro | review | 1:1 | working-session
|
|
559
|
+
|
|
560
|
+
## Summary
|
|
561
|
+
|
|
562
|
+
<!-- What happened and why it mattered. -->
|
|
563
|
+
|
|
564
|
+
## Decisions
|
|
565
|
+
|
|
566
|
+
<!-- Decisions made in the meeting. -->
|
|
567
|
+
|
|
568
|
+
## Action items
|
|
569
|
+
|
|
570
|
+
<!-- Follow-ups, owners, and due dates. -->
|
|
571
|
+
|
|
572
|
+
## References
|
|
573
|
+
|
|
574
|
+
<!-- Relevant docs, tickets, links, or recordings. -->
|
|
575
|
+
`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function buildIntakeTemplate() {
|
|
579
|
+
return `<!--
|
|
580
|
+
docs/intake/templates/_template.md — starter template for logging an intake batch.
|
|
581
|
+
|
|
582
|
+
Use this when you want a durable record of what was dropped into intake and why.
|
|
583
|
+
Raw source files belong in .cx/inbox/ until they are ingested.
|
|
584
|
+
-->
|
|
585
|
+
|
|
586
|
+
# Intake Batch: {title}
|
|
587
|
+
|
|
588
|
+
- **Date**: {YYYY-MM-DD}
|
|
589
|
+
- **Owner**: {name}
|
|
590
|
+
- **Source**: {vendor, teammate, export, upload}
|
|
591
|
+
|
|
592
|
+
## What arrived
|
|
593
|
+
|
|
594
|
+
<!-- Files, folders, or links dropped into .cx/inbox/. -->
|
|
595
|
+
|
|
596
|
+
## Why it matters
|
|
597
|
+
|
|
598
|
+
<!-- Why Construct should ingest this material. -->
|
|
599
|
+
|
|
600
|
+
## Ingest plan
|
|
601
|
+
|
|
602
|
+
- Run: \`construct ingest ./.cx/inbox --sync\`
|
|
603
|
+
- Target: \`.cx/knowledge/internal/\` unless a different knowledge target is more appropriate
|
|
604
|
+
|
|
605
|
+
## Notes
|
|
606
|
+
|
|
607
|
+
<!-- Caveats, access concerns, or cleanup notes. -->
|
|
608
|
+
`;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function titleCase(value) {
|
|
612
|
+
return value
|
|
613
|
+
.split(/[-_\s]+/)
|
|
614
|
+
.filter(Boolean)
|
|
615
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
616
|
+
.join(" ");
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Scan for markdown files in the target directory, excluding common ignored directories.
|
|
621
|
+
* Returns array of objects: { filePath: absolute path, relPath: path relative to target, content: file content }
|
|
622
|
+
*/
|
|
623
|
+
function scanMarkdownFiles(targetDir) {
|
|
624
|
+
const ignoredDirs = new Set([
|
|
625
|
+
'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
|
|
626
|
+
'.claude', '.cx', 'templates', 'scripts', 'platforms',
|
|
627
|
+
'docs', // exclude existing docs lane files from reorganization suggestions
|
|
628
|
+
]);
|
|
629
|
+
|
|
630
|
+
const results = [];
|
|
631
|
+
|
|
632
|
+
function walk(dir) {
|
|
633
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
634
|
+
for (const entry of entries) {
|
|
635
|
+
const fullPath = path.join(dir, entry.name);
|
|
636
|
+
if (entry.isDirectory()) {
|
|
637
|
+
if (!ignoredDirs.has(entry.name)) {
|
|
638
|
+
walk(fullPath);
|
|
639
|
+
}
|
|
640
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
641
|
+
// Skip files that are already in a docs lane directory (we don't want to suggest moving them)
|
|
642
|
+
const relPath = path.relative(targetDir, fullPath);
|
|
643
|
+
if (!relPath.startsWith('docs/') || !relPath.includes('/')) {
|
|
644
|
+
// Only suggest files at the repo root or in non-docs top-level dirs
|
|
645
|
+
try {
|
|
646
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
647
|
+
results.push({ filePath: fullPath, relPath, content });
|
|
648
|
+
} catch (err) {
|
|
649
|
+
// If we can't read, skip
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
307
654
|
}
|
|
308
655
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
656
|
+
walk(targetDir);
|
|
657
|
+
return results;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Suggest a documentation lane for a given file based on its content and filename.
|
|
662
|
+
* Returns the canonical lane key (e.g., 'prds', 'rfcs', 'adrs') or null if no clear suggestion.
|
|
663
|
+
*/
|
|
664
|
+
function suggestLocationForFile(filePath, content) {
|
|
665
|
+
return suggestDocsLaneForFile(filePath, content);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function copyLaneTemplates(laneKey) {
|
|
669
|
+
const lane = DOC_LANES[laneKey];
|
|
670
|
+
if (!lane) return;
|
|
671
|
+
const laneRoot = path.join(docsDir, lane.dir);
|
|
672
|
+
writeIfMissing(path.join(laneRoot, "README.md"), buildLaneReadme(laneKey));
|
|
673
|
+
for (const [index, templateName] of lane.templates.entries()) {
|
|
674
|
+
const outputName = index === 0 ? "_template.md" : templateName.replace(/\.md$/, ".template.md");
|
|
675
|
+
const content =
|
|
676
|
+
templateName === "__notes-template__" ? buildNotesTemplate()
|
|
677
|
+
: templateName === '__meeting-notes-template__' ? buildMeetingNotesTemplate()
|
|
678
|
+
: templateName === "__intake-template__" ? buildIntakeTemplate()
|
|
679
|
+
: fs.readFileSync(path.join(TEMPLATE_DIR, templateName), "utf8");
|
|
680
|
+
writeIfMissing(path.join(laneRoot, "templates", outputName), content);
|
|
315
681
|
}
|
|
682
|
+
}
|
|
316
683
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
684
|
+
function createCustomLane(laneDir) {
|
|
685
|
+
const laneRoot = path.join(docsDir, laneDir);
|
|
686
|
+
writeIfMissing(path.join(laneRoot, "README.md"), buildCustomLaneReadme(laneDir));
|
|
687
|
+
writeIfMissing(path.join(laneRoot, "templates", "_template.md"), buildCustomLaneTemplate(laneDir));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/* ─── askQuestions ───────────────────────────────────────────────────────── */
|
|
691
|
+
async function askQuestions() {
|
|
692
|
+
if (skipInteractive) {
|
|
693
|
+
return {
|
|
694
|
+
lanes: docsArg ? parseLaneSelection(docsArg.split("=")[1]) : DEFAULT_LANES,
|
|
695
|
+
extraLanes: extrasArg ? parseExtraLaneSelection(extrasArg.split("=")[1]) : [],
|
|
696
|
+
withArchitecture: withArchitectureFlag,
|
|
697
|
+
};
|
|
327
698
|
}
|
|
328
699
|
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
700
|
+
const contextualSuggestions = suggestContextualLanes(target);
|
|
701
|
+
const suggestedKeys = new Set(contextualSuggestions.map((s) => s.lane));
|
|
702
|
+
const suggestedReasons = Object.fromEntries(contextualSuggestions.map((s) => [s.lane, s.reason]));
|
|
703
|
+
|
|
704
|
+
const defaultSet = new Set(DEFAULT_LANES);
|
|
705
|
+
const items = LANE_ORDER.map((key) => ({
|
|
706
|
+
value: key,
|
|
707
|
+
label: DOC_LANES[key].title,
|
|
708
|
+
description: DOC_LANES[key].description,
|
|
709
|
+
checked: defaultSet.has(key) || suggestedKeys.has(key),
|
|
710
|
+
suggestion: suggestedKeys.has(key) ? suggestedReasons[key] : null,
|
|
711
|
+
meta: `docs/${DOC_LANES[key].dir}/`,
|
|
712
|
+
}));
|
|
713
|
+
|
|
714
|
+
const selectedKeys = await multiSelect({
|
|
715
|
+
title: 'Select doc lanes',
|
|
716
|
+
instructions: 'Use arrows to move, Space to toggle, a to toggle all, i to invert, Enter to confirm.',
|
|
717
|
+
options: items,
|
|
718
|
+
});
|
|
719
|
+
const lanes = selectedKeys.length ? selectedKeys : DEFAULT_LANES;
|
|
720
|
+
|
|
721
|
+
const withArchitecture = await selectOption({
|
|
722
|
+
title: 'Create docs/architecture.md?',
|
|
723
|
+
instructions: 'Pick whether to scaffold the architecture document now.',
|
|
724
|
+
options: [
|
|
725
|
+
{ value: true, label: 'Yes', description: 'Create docs/architecture.md with the project-state hierarchy and lane summary.' },
|
|
726
|
+
{ value: false, label: 'No', description: 'Skip docs/architecture.md for now. You can add it later.' },
|
|
727
|
+
],
|
|
728
|
+
});
|
|
729
|
+
process.stdout.write("\n");
|
|
730
|
+
|
|
731
|
+
return { lanes, extraLanes: [], withArchitecture };
|
|
732
|
+
}
|
|
334
733
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
734
|
+
async function main() {
|
|
735
|
+
const projectName = inferProjectName(target);
|
|
736
|
+
const { lanes, extraLanes, withArchitecture } = await askQuestions();
|
|
737
|
+
const normalizedLanes = Array.from(new Set(
|
|
738
|
+
lanes
|
|
739
|
+
.map((lane) => normalizeLaneKey(lane))
|
|
740
|
+
.filter((lane) => lane in DOC_LANES),
|
|
741
|
+
));
|
|
742
|
+
const selectedLanes = normalizedLanes.length ? normalizedLanes : DEFAULT_LANES;
|
|
743
|
+
const selectedCustomLanes = Array.from(new Set(
|
|
744
|
+
extraLanes
|
|
745
|
+
.map(normalizeCustomLaneName)
|
|
746
|
+
.filter(Boolean)
|
|
747
|
+
.filter((lane) => !(lane in DOC_LANES)),
|
|
748
|
+
));
|
|
749
|
+
const allLaneKeys = sortLaneKeys([...selectedLanes, ...selectedCustomLanes]);
|
|
750
|
+
|
|
751
|
+
process.stdout.write(`\nConstruct init-docs → ${target}\n\n`);
|
|
752
|
+
|
|
753
|
+
writeIfMissing(path.join(docsDir, "README.md"), buildDocsReadme(projectName, allLaneKeys));
|
|
754
|
+
if (withArchitecture) {
|
|
755
|
+
writeIfMissing(path.join(docsDir, "architecture.md"), buildArchitectureDoc(projectName, allLaneKeys));
|
|
756
|
+
}
|
|
757
|
+
if (selectedLanes.includes('intake')) {
|
|
758
|
+
writeIfMissing(path.join(target, '.cx', 'inbox', '.gitkeep'), '');
|
|
348
759
|
}
|
|
349
760
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
761
|
+
for (const laneKey of selectedLanes) copyLaneTemplates(laneKey);
|
|
762
|
+
for (const laneKey of selectedCustomLanes) createCustomLane(laneKey);
|
|
763
|
+
|
|
764
|
+
// Handle suggestion and organization of existing markdown files
|
|
765
|
+
if (suggestOrg || organize) {
|
|
766
|
+
if (organize && !skipInteractive) {
|
|
767
|
+
process.stdout.write("Error: --organize requires --yes to avoid interactive prompts.\n");
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const markdownFiles = scanMarkdownFiles(target);
|
|
772
|
+
const suggestions = [];
|
|
773
|
+
|
|
774
|
+
for (const file of markdownFiles) {
|
|
775
|
+
const suggestedLane = suggestLocationForFile(file.filePath, file.content);
|
|
776
|
+
if (suggestedLane) {
|
|
777
|
+
suggestions.push({ file: file, lane: suggestedLane });
|
|
356
778
|
}
|
|
357
779
|
}
|
|
358
|
-
} catch (err) {
|
|
359
|
-
console.warn(` Could not parse model output (${err.message}). Falling back to static scaffold.\n`);
|
|
360
|
-
scaffoldStatic(ctx.name, workflowJson);
|
|
361
|
-
}
|
|
362
780
|
|
|
363
|
-
|
|
364
|
-
|
|
781
|
+
if (suggestions.length === 0) {
|
|
782
|
+
process.stdout.write("No files found that could be organized into documentation lanes.\n");
|
|
783
|
+
} else {
|
|
784
|
+
process.stdout.write(`Found ${suggestions.length} file(s) that could be organized:\n\n`);
|
|
785
|
+
for (const { file, lane } of suggestions) {
|
|
786
|
+
process.stdout.write(` ${file.relPath} → docs/${DOC_LANES[lane]?.dir ?? lane}/\n`);
|
|
787
|
+
}
|
|
788
|
+
process.stdout.write("\n");
|
|
789
|
+
|
|
790
|
+
if (organize) {
|
|
791
|
+
// Actually move the files
|
|
792
|
+
process.stdout.write("Moving files to suggested locations...\n");
|
|
793
|
+
for (const { file, lane } of suggestions) {
|
|
794
|
+
const targetDir = path.join(target, "docs", DOC_LANES[lane]?.dir ?? lane);
|
|
795
|
+
const targetPath = path.join(targetDir, path.basename(file.filePath));
|
|
796
|
+
try {
|
|
797
|
+
fs.renameSync(file.filePath, targetPath);
|
|
798
|
+
process.stdout.write(` Moved: ${file.relPath} → docs/${DOC_LANES[lane]?.dir ?? lane}/${path.basename(file.filePath)}\n`);
|
|
799
|
+
} catch (err) {
|
|
800
|
+
process.stdout.write(` Failed to move ${file.relPath}: ${err.message}\n`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
process.stdout.write("Organization complete.\n");
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
365
807
|
|
|
366
|
-
function printSummary() {
|
|
367
|
-
console.log(`Doc structure initialized at: ${target}\n`);
|
|
368
808
|
if (created.length) {
|
|
369
|
-
|
|
370
|
-
for (const
|
|
809
|
+
process.stdout.write("Created:\n");
|
|
810
|
+
for (const file of created) process.stdout.write(` + ${file}\n`);
|
|
371
811
|
}
|
|
372
812
|
if (skipped.length) {
|
|
373
|
-
|
|
374
|
-
for (const
|
|
813
|
+
process.stdout.write("\nSkipped (already exist):\n");
|
|
814
|
+
for (const file of skipped) process.stdout.write(` ~ ${file}\n`);
|
|
375
815
|
}
|
|
376
|
-
|
|
816
|
+
process.stdout.write(`\n${created.length} created, ${skipped.length} skipped.\n`);
|
|
377
817
|
}
|
|
378
818
|
|
|
379
|
-
main().catch((
|
|
380
|
-
|
|
819
|
+
main().catch((error) => {
|
|
820
|
+
process.stderr.write(`Error: ${error.message}\n`);
|
|
381
821
|
process.exit(1);
|
|
382
822
|
});
|