@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.
Files changed (471) hide show
  1. package/.env.example +9 -6
  2. package/LICENSE +201 -21
  3. package/README.md +132 -257
  4. package/agents/contracts.json +387 -0
  5. package/agents/prompts/cx-accessibility.md +8 -0
  6. package/agents/prompts/cx-ai-engineer.md +92 -0
  7. package/agents/prompts/cx-architect.md +10 -2
  8. package/agents/prompts/cx-business-strategist.md +13 -0
  9. package/agents/prompts/cx-data-analyst.md +80 -0
  10. package/agents/prompts/cx-data-engineer.md +4 -0
  11. package/agents/prompts/cx-debugger.md +8 -0
  12. package/agents/prompts/cx-designer.md +19 -0
  13. package/agents/prompts/cx-devil-advocate.md +4 -0
  14. package/agents/prompts/cx-docs-keeper.md +128 -0
  15. package/agents/prompts/cx-engineer.md +13 -0
  16. package/agents/prompts/cx-evaluator.md +4 -0
  17. package/agents/prompts/cx-explorer.md +12 -0
  18. package/agents/prompts/cx-legal-compliance.md +13 -0
  19. package/agents/prompts/cx-operations.md +13 -0
  20. package/agents/prompts/cx-orchestrator.md +107 -4
  21. package/agents/prompts/cx-platform-engineer.md +75 -0
  22. package/agents/prompts/cx-product-manager.md +8 -0
  23. package/agents/prompts/cx-qa.md +100 -0
  24. package/agents/prompts/cx-rd-lead.md +13 -0
  25. package/agents/prompts/cx-release-manager.md +8 -0
  26. package/agents/prompts/cx-researcher.md +21 -1
  27. package/agents/prompts/cx-reviewer.md +8 -0
  28. package/agents/prompts/cx-security.md +104 -0
  29. package/agents/prompts/cx-sre.md +104 -0
  30. package/agents/prompts/cx-test-automation.md +4 -0
  31. package/agents/prompts/cx-trace-reviewer.md +7 -3
  32. package/agents/prompts/cx-ux-researcher.md +4 -0
  33. package/agents/registry.json +365 -90
  34. package/agents/role-manifests.json +217 -0
  35. package/bin/construct +3345 -141
  36. package/bin/construct-postinstall.mjs +87 -0
  37. package/commands/build/feature.md +6 -6
  38. package/commands/plan/feature.md +6 -5
  39. package/commands/plan/requirements.md +1 -1
  40. package/commands/ship/status.md +1 -1
  41. package/commands/work/drive.md +3 -3
  42. package/commands/work/optimize-prompts.md +3 -3
  43. package/db/{migrations → schema}/001_init.sql +2 -0
  44. package/db/schema/002_pgvector.sql +182 -0
  45. package/db/schema/003_intake.sql +47 -0
  46. package/examples/README.md +85 -0
  47. package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
  48. package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
  49. package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
  50. package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
  51. package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
  52. package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
  53. package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
  54. package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
  55. package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
  56. package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
  57. package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
  58. package/examples/personas/construct/bad/commit-without-approval.md +30 -0
  59. package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
  60. package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
  61. package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
  62. package/examples/provider-plugin/README.md +34 -0
  63. package/examples/provider-plugin/index.mjs +74 -0
  64. package/examples/provider-plugin/package.json +15 -0
  65. package/examples/seed-observations/README.md +38 -0
  66. package/examples/seed-observations/anti-patterns.md +44 -0
  67. package/examples/seed-observations/decisions.md +36 -0
  68. package/examples/seed-observations/patterns.md +42 -0
  69. package/lib/agent-contracts-enforce.mjs +158 -0
  70. package/lib/agent-contracts.mjs +231 -0
  71. package/lib/agents/postconditions.mjs +126 -0
  72. package/lib/agents/schema.mjs +124 -0
  73. package/lib/artifact-capture.mjs +183 -0
  74. package/lib/audit-trail.mjs +149 -0
  75. package/lib/auto-docs.mjs +245 -65
  76. package/lib/beads/auto-close.mjs +126 -0
  77. package/lib/beads/drift.mjs +171 -0
  78. package/lib/beads-automation.mjs +542 -0
  79. package/lib/beads-client.mjs +518 -0
  80. package/lib/beads-lock.mjs +377 -0
  81. package/lib/beads-optimistic.mjs +365 -0
  82. package/lib/bootstrap/built-ins.mjs +136 -0
  83. package/lib/bootstrap/lazy-install.mjs +161 -0
  84. package/lib/bootstrap/resources.mjs +120 -0
  85. package/lib/bootstrap.mjs +105 -0
  86. package/lib/cache-governor.js +213 -0
  87. package/lib/cache-strategy-anthropic.js +62 -0
  88. package/lib/cache-strategy-google.js +79 -0
  89. package/lib/cache-strategy-none.js +30 -0
  90. package/lib/cache-strategy-openai.js +48 -0
  91. package/lib/cache-strategy.js +91 -0
  92. package/lib/claude-allow.mjs +149 -0
  93. package/lib/cli-commands.mjs +413 -258
  94. package/lib/codex-config.mjs +3 -1
  95. package/lib/comment-lint.mjs +55 -6
  96. package/lib/completions.mjs +21 -6
  97. package/lib/config/alias.mjs +56 -0
  98. package/lib/config/project-config.mjs +335 -0
  99. package/lib/config/schema.mjs +159 -0
  100. package/lib/context-router.mjs +308 -0
  101. package/lib/cost-ledger.mjs +177 -0
  102. package/lib/cost.mjs +171 -10
  103. package/lib/dashboard-static.mjs +158 -0
  104. package/lib/deployment-mode.mjs +86 -0
  105. package/lib/deprecate.mjs +49 -0
  106. package/lib/dispatch-batch.js +183 -0
  107. package/lib/distill.mjs +21 -8
  108. package/lib/doc-stamp.mjs +164 -0
  109. package/lib/doc-verify.mjs +119 -0
  110. package/lib/docs-routing.mjs +89 -0
  111. package/lib/docs-verify.mjs +417 -0
  112. package/lib/doctor/audit.mjs +71 -0
  113. package/lib/doctor/cli.mjs +99 -0
  114. package/lib/doctor/escalate.mjs +29 -0
  115. package/lib/doctor/index.mjs +140 -0
  116. package/lib/doctor/report.mjs +170 -0
  117. package/lib/doctor/watchers/bd-watch.mjs +117 -0
  118. package/lib/doctor/watchers/cost.mjs +130 -0
  119. package/lib/doctor/watchers/disk.mjs +122 -0
  120. package/lib/doctor/watchers/handoffs.mjs +33 -0
  121. package/lib/doctor/watchers/process-pressure.mjs +60 -0
  122. package/lib/doctor/watchers/service-health.mjs +188 -0
  123. package/lib/document-extract.mjs +288 -0
  124. package/lib/document-ingest.mjs +230 -0
  125. package/lib/drop.mjs +282 -0
  126. package/lib/embed/approval-queue.mjs +176 -0
  127. package/lib/embed/artifact.mjs +349 -0
  128. package/lib/embed/authority-guard.mjs +155 -0
  129. package/lib/embed/cli.mjs +408 -0
  130. package/lib/embed/config.mjs +355 -0
  131. package/lib/embed/conflict-detection.mjs +264 -0
  132. package/lib/embed/customer-profiles.mjs +480 -0
  133. package/lib/embed/daemon.mjs +1309 -0
  134. package/lib/embed/demand-fetch.mjs +449 -0
  135. package/lib/embed/docs-lifecycle.mjs +349 -0
  136. package/lib/embed/inbox-live-watcher.mjs +119 -0
  137. package/lib/embed/inbox.mjs +343 -0
  138. package/lib/embed/intake-metrics.mjs +190 -0
  139. package/lib/embed/jobs/vector-sync.mjs +198 -0
  140. package/lib/embed/notifications.mjs +75 -0
  141. package/lib/embed/output.mjs +79 -0
  142. package/lib/embed/providers/github.mjs +295 -0
  143. package/lib/embed/providers/jira.mjs +192 -0
  144. package/lib/embed/providers/linear.mjs +186 -0
  145. package/lib/embed/providers/registry.mjs +115 -0
  146. package/lib/embed/providers/slack.mjs +203 -0
  147. package/lib/embed/recommendation-store.mjs +378 -0
  148. package/lib/embed/roadmap.mjs +374 -0
  149. package/lib/embed/role-framing.mjs +110 -0
  150. package/lib/embed/scheduler.mjs +99 -0
  151. package/lib/embed/semantic.mjs +325 -0
  152. package/lib/embed/snapshot.mjs +191 -0
  153. package/lib/embed/supervision.mjs +235 -0
  154. package/lib/embed/target-resolver.mjs +186 -0
  155. package/lib/embed/worker.mjs +62 -0
  156. package/lib/embed/workspaces.mjs +297 -0
  157. package/lib/engine/chunker-headings.mjs +110 -0
  158. package/lib/engine/compressor-heuristic.mjs +100 -0
  159. package/lib/engine/consolidate.mjs +287 -0
  160. package/lib/engine/contracts.mjs +126 -0
  161. package/lib/engine/defaults.mjs +129 -0
  162. package/lib/engine/eval-retrieval.mjs +148 -0
  163. package/lib/engine/fuser-rrf.mjs +62 -0
  164. package/lib/engine/index.mjs +37 -0
  165. package/lib/engine/registry.mjs +146 -0
  166. package/lib/engine/reranker-mmr.mjs +90 -0
  167. package/lib/engine/tokens.mjs +77 -0
  168. package/lib/entity-store.mjs +280 -0
  169. package/lib/env-config.mjs +69 -4
  170. package/lib/evals/retrieval-bench.mjs +159 -0
  171. package/lib/evaluator-optimizer.mjs +317 -0
  172. package/lib/features.mjs +159 -29
  173. package/lib/gates-audit.mjs +236 -0
  174. package/lib/git-hooks/prepare-commit-msg +58 -0
  175. package/lib/handoffs/cleanup.mjs +159 -0
  176. package/lib/handoffs/contract.mjs +162 -0
  177. package/lib/handoffs/inventory.mjs +120 -0
  178. package/lib/headhunt.mjs +28 -47
  179. package/lib/health-check.mjs +399 -0
  180. package/lib/hook-health.mjs +442 -0
  181. package/lib/hooks/_lib/log.mjs +82 -0
  182. package/lib/hooks/adaptive-lint.mjs +26 -2
  183. package/lib/hooks/agent-tracker.mjs +171 -14
  184. package/lib/hooks/audit-reads.mjs +109 -0
  185. package/lib/hooks/audit-trail.mjs +153 -0
  186. package/lib/hooks/bash-output-logger.mjs +71 -0
  187. package/lib/hooks/block-no-verify.mjs +41 -0
  188. package/lib/hooks/ci-status-check.mjs +82 -0
  189. package/lib/hooks/comment-lint.mjs +29 -7
  190. package/lib/hooks/config-protection.mjs +39 -18
  191. package/lib/hooks/context-watch.mjs +137 -0
  192. package/lib/hooks/context-window-recovery.mjs +4 -20
  193. package/lib/hooks/dep-audit.mjs +16 -0
  194. package/lib/hooks/doc-coupling-check.mjs +80 -0
  195. package/lib/hooks/edit-accumulator.mjs +3 -0
  196. package/lib/hooks/edit-error-recovery.mjs +3 -0
  197. package/lib/hooks/edit-guard.mjs +45 -4
  198. package/lib/hooks/env-check.mjs +3 -0
  199. package/lib/hooks/guard-bash.mjs +58 -1
  200. package/lib/hooks/mcp-audit.mjs +18 -20
  201. package/lib/hooks/mcp-health-check.mjs +36 -0
  202. package/lib/hooks/model-fallback.mjs +42 -56
  203. package/lib/hooks/policy-engine.mjs +209 -0
  204. package/lib/hooks/post-merge-docs-check.mjs +63 -0
  205. package/lib/hooks/pre-compact.mjs +4 -24
  206. package/lib/hooks/pre-push-gate.mjs +200 -37
  207. package/lib/hooks/proactive-activation.mjs +284 -0
  208. package/lib/hooks/read-tracker.mjs +27 -4
  209. package/lib/hooks/readme-age-check.mjs +77 -0
  210. package/lib/hooks/registry-sync.mjs +12 -5
  211. package/lib/hooks/scan-secrets.mjs +50 -7
  212. package/lib/hooks/session-optimize.mjs +311 -0
  213. package/lib/hooks/session-start.mjs +287 -28
  214. package/lib/hooks/stop-notify.mjs +236 -96
  215. package/lib/hooks/stop-typecheck.mjs +13 -1
  216. package/lib/hooks/test-watch.mjs +68 -0
  217. package/lib/host-capabilities.mjs +31 -10
  218. package/lib/init-docs.mjs +731 -291
  219. package/lib/init-unified.mjs +1000 -0
  220. package/lib/init-update.mjs +168 -0
  221. package/lib/init.mjs +107 -0
  222. package/lib/install/first-invocation.mjs +119 -0
  223. package/lib/install/stage-project.mjs +69 -0
  224. package/lib/intake/classify.mjs +278 -0
  225. package/lib/intake/feedback.mjs +273 -0
  226. package/lib/intake/filesystem-queue.mjs +158 -0
  227. package/lib/intake/intake-config.mjs +132 -0
  228. package/lib/intake/postgres-queue.mjs +198 -0
  229. package/lib/intake/prepare.mjs +139 -0
  230. package/lib/intake/queue.mjs +83 -0
  231. package/lib/intake/session-prelude.mjs +50 -0
  232. package/lib/integrations/intake-integrations.mjs +740 -0
  233. package/lib/intent-classifier.mjs +253 -0
  234. package/lib/knowledge/layout.mjs +72 -0
  235. package/lib/knowledge/rag.mjs +331 -0
  236. package/lib/knowledge/search.mjs +315 -0
  237. package/lib/knowledge/trends.mjs +261 -0
  238. package/lib/logger.mjs +85 -0
  239. package/lib/mcp/broker.mjs +124 -0
  240. package/lib/mcp/server.mjs +554 -854
  241. package/lib/mcp/tools/document.mjs +132 -0
  242. package/lib/mcp/tools/memory.mjs +209 -0
  243. package/lib/mcp/tools/project.mjs +349 -0
  244. package/lib/mcp/tools/skills.mjs +409 -0
  245. package/lib/mcp/tools/storage.mjs +60 -0
  246. package/lib/mcp/tools/telemetry.mjs +315 -0
  247. package/lib/mcp/tools/workflow.mjs +112 -0
  248. package/lib/mcp-catalog.json +54 -3
  249. package/lib/mcp-manager.mjs +96 -48
  250. package/lib/mcp-platform-config.mjs +22 -22
  251. package/lib/memory-stats.mjs +121 -0
  252. package/lib/mode-commands.mjs +124 -0
  253. package/lib/model-free-selector.mjs +184 -0
  254. package/lib/model-pricing.mjs +152 -0
  255. package/lib/model-registry.mjs +226 -0
  256. package/lib/model-router.mjs +362 -386
  257. package/lib/observation-store.mjs +391 -0
  258. package/lib/ollama-manager.mjs +428 -0
  259. package/lib/opencode-config.mjs +13 -3
  260. package/lib/opencode-runtime-plugin.mjs +70 -38
  261. package/lib/opencode-telemetry.mjs +64 -6
  262. package/lib/orchestration-policy.mjs +509 -6
  263. package/lib/overrides/resolver.mjs +207 -0
  264. package/lib/parity.mjs +147 -0
  265. package/lib/paths.mjs +33 -0
  266. package/lib/performance/generate.mjs +212 -0
  267. package/lib/plugin-registry.mjs +268 -0
  268. package/lib/policy/engine.mjs +130 -0
  269. package/lib/policy/unified-gates.mjs +96 -0
  270. package/lib/project-detection.mjs +129 -0
  271. package/lib/project-init-shared.mjs +272 -0
  272. package/lib/project-profile.mjs +447 -0
  273. package/lib/prompt-composer.js +434 -0
  274. package/lib/prompt-metadata.mjs +1 -1
  275. package/lib/provider-capabilities-anthropic.js +44 -0
  276. package/lib/provider-capabilities-deepseek.js +37 -0
  277. package/lib/provider-capabilities-generic.js +26 -0
  278. package/lib/provider-capabilities-google.js +47 -0
  279. package/lib/provider-capabilities-openai.js +45 -0
  280. package/lib/provider-capabilities.js +142 -0
  281. package/lib/providers/atlassian-confluence/index.mjs +103 -0
  282. package/lib/providers/atlassian-jira/index.mjs +100 -0
  283. package/lib/providers/auth-manager.mjs +126 -0
  284. package/lib/providers/circuit-breaker.mjs +124 -0
  285. package/lib/providers/contract.mjs +93 -0
  286. package/lib/providers/github/index.mjs +126 -0
  287. package/lib/providers/registry.mjs +184 -0
  288. package/lib/providers/salesforce/index.mjs +100 -0
  289. package/lib/providers/slack/index.mjs +80 -0
  290. package/lib/reflect.mjs +137 -0
  291. package/lib/research-lint.mjs +164 -0
  292. package/lib/resources/budget.mjs +259 -0
  293. package/lib/role-preload.mjs +21 -6
  294. package/lib/roles/approval-surface.mjs +54 -0
  295. package/lib/roles/cli.mjs +118 -0
  296. package/lib/roles/event-bus.mjs +79 -0
  297. package/lib/roles/fence.mjs +84 -0
  298. package/lib/roles/gateway.mjs +260 -0
  299. package/lib/roles/hook-emit.mjs +37 -0
  300. package/lib/roles/manifest.mjs +48 -0
  301. package/lib/roles/router.mjs +27 -0
  302. package/lib/runtime-pressure.mjs +360 -0
  303. package/lib/schema-artifact.mjs +134 -0
  304. package/lib/schema-infer.mjs +551 -0
  305. package/lib/server/auth.mjs +168 -0
  306. package/lib/server/chat.mjs +336 -0
  307. package/lib/server/cors.mjs +77 -0
  308. package/lib/server/csrf.mjs +91 -0
  309. package/lib/server/index.mjs +1927 -78
  310. package/lib/server/insights.mjs +765 -0
  311. package/lib/server/rate-limit.mjs +91 -0
  312. package/lib/server/static/assets/index-ab25c707.js +70 -0
  313. package/lib/server/static/assets/index-f0c80a2b.css +1 -0
  314. package/lib/server/static/index.html +12 -817
  315. package/lib/server/telemetry-login.mjs +108 -0
  316. package/lib/server/webhook.mjs +510 -0
  317. package/lib/service-manager.mjs +522 -58
  318. package/lib/services/pattern-promotion-service.mjs +167 -0
  319. package/lib/services/telemetry-backend.mjs +178 -0
  320. package/lib/session-store.mjs +374 -0
  321. package/lib/setup-prompts.mjs +96 -0
  322. package/lib/setup.mjs +523 -36
  323. package/lib/skills-apply.mjs +280 -0
  324. package/lib/skills-scope.mjs +118 -0
  325. package/lib/status.mjs +261 -70
  326. package/lib/storage/admin.mjs +355 -0
  327. package/lib/storage/backend.mjs +2 -1
  328. package/lib/storage/backup.mjs +347 -0
  329. package/lib/storage/embeddings-engine.mjs +133 -0
  330. package/lib/storage/embeddings-legacy.mjs +85 -0
  331. package/lib/storage/embeddings-local.mjs +108 -0
  332. package/lib/storage/embeddings-ollama.mjs +78 -0
  333. package/lib/storage/embeddings-openai.mjs +85 -0
  334. package/lib/storage/embeddings.mjs +92 -33
  335. package/lib/storage/file-lock.mjs +130 -0
  336. package/lib/storage/fusion.mjs +95 -0
  337. package/lib/storage/hybrid-query.mjs +34 -27
  338. package/lib/storage/migrations.mjs +187 -0
  339. package/lib/storage/postgres-backup.mjs +124 -0
  340. package/lib/storage/sql-store.mjs +5 -15
  341. package/lib/storage/state-source.mjs +12 -13
  342. package/lib/storage/store-version.mjs +115 -0
  343. package/lib/storage/sync.mjs +144 -35
  344. package/lib/storage/unified-storage.mjs +550 -0
  345. package/lib/storage/vector-client.mjs +286 -0
  346. package/lib/storage/vector-store.mjs +71 -30
  347. package/lib/task-graph/generate.mjs +135 -0
  348. package/lib/task-graph/schema.mjs +81 -0
  349. package/lib/task-graph/store.mjs +71 -0
  350. package/lib/telemetry/backends/local.mjs +62 -0
  351. package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
  352. package/lib/telemetry/backfill.mjs +180 -0
  353. package/lib/telemetry/eval-datasets.mjs +203 -0
  354. package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
  355. package/lib/telemetry/intent-verifications.mjs +86 -0
  356. package/lib/telemetry/llm-judge.mjs +350 -0
  357. package/lib/telemetry/model-pricing-catalog.mjs +557 -0
  358. package/lib/telemetry/setup.mjs +151 -0
  359. package/lib/telemetry/skill-calls.mjs +78 -0
  360. package/lib/telemetry/team-rollup.mjs +4 -4
  361. package/lib/token-engine.js +117 -0
  362. package/lib/token-estimator-anthropic.js +15 -0
  363. package/lib/token-estimator-deepseek.js +13 -0
  364. package/lib/token-estimator-default.js +13 -0
  365. package/lib/token-estimator-google.js +13 -0
  366. package/lib/token-estimator-openai.js +13 -0
  367. package/lib/toolkit-env.mjs +1 -1
  368. package/lib/tty-prompts.mjs +211 -0
  369. package/lib/uninstall/uninstall.mjs +423 -0
  370. package/lib/update.mjs +115 -0
  371. package/lib/upgrade.mjs +141 -0
  372. package/lib/validator.mjs +51 -2
  373. package/lib/validators/skills.mjs +142 -0
  374. package/lib/wireframe.mjs +422 -0
  375. package/lib/worker/entrypoint.mjs +241 -0
  376. package/lib/worker/evidence.mjs +107 -0
  377. package/lib/worker/run.mjs +154 -0
  378. package/lib/worker/trace.mjs +182 -0
  379. package/lib/workflow-state.mjs +14 -18
  380. package/package.json +21 -8
  381. package/personas/construct.md +53 -51
  382. package/platforms/claude/CLAUDE.md +1 -1
  383. package/platforms/claude/settings.template.json +115 -68
  384. package/platforms/opencode/config.template.json +3 -7
  385. package/rules/common/agents.md +11 -38
  386. package/rules/common/beads-hygiene.md +75 -0
  387. package/rules/common/code-review.md +11 -100
  388. package/rules/common/coding-style.md +5 -3
  389. package/rules/common/comments.md +30 -111
  390. package/rules/common/commit-approval.md +53 -0
  391. package/rules/common/cx-agent-routing.md +1 -0
  392. package/rules/common/development-workflow.md +23 -41
  393. package/rules/common/doc-ownership.md +54 -0
  394. package/rules/common/efficiency.md +57 -0
  395. package/rules/common/framing.md +76 -0
  396. package/rules/common/git-workflow.md +2 -4
  397. package/rules/common/patterns.md +3 -7
  398. package/rules/common/performance.md +15 -31
  399. package/rules/common/release-gates.md +69 -0
  400. package/rules/common/research.md +107 -0
  401. package/rules/common/security.md +6 -6
  402. package/rules/common/skill-composition.md +67 -0
  403. package/rules/common/testing.md +15 -27
  404. package/rules/golang/hooks.md +0 -4
  405. package/rules/policy/bootstrap.yaml +11 -0
  406. package/rules/policy/drive.yaml +8 -0
  407. package/rules/policy/task.yaml +9 -0
  408. package/rules/policy/workflow.yaml +9 -0
  409. package/rules/python/hooks.md +0 -4
  410. package/rules/swift/hooks.md +0 -4
  411. package/rules/typescript/hooks.md +0 -4
  412. package/rules/web/hooks.md +0 -2
  413. package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
  414. package/skills/ai/prompt-optimizer.md +7 -7
  415. package/skills/compliance/ai-disclosure.md +58 -0
  416. package/skills/compliance/data-privacy.md +46 -0
  417. package/skills/compliance/license-audit.md +40 -0
  418. package/skills/compliance/regulatory-review.md +61 -0
  419. package/skills/docs/document-ingest-workflow.md +52 -0
  420. package/skills/docs/evidence-ingest-workflow.md +9 -0
  421. package/skills/docs/init-docs.md +51 -18
  422. package/skills/docs/product-intelligence-workflow.md +12 -0
  423. package/skills/docs/product-signal-workflow.md +4 -0
  424. package/skills/docs/research-workflow.md +23 -8
  425. package/skills/docs/runbook-workflow.md +1 -1
  426. package/skills/operating/orchestration-reference.md +151 -0
  427. package/skills/roles/architect.md +5 -0
  428. package/skills/roles/engineer.md +32 -0
  429. package/skills/roles/operator.md +12 -0
  430. package/skills/roles/reviewer.md +23 -0
  431. package/skills/routing.md +1 -1
  432. package/templates/devcontainer/Dockerfile.devcontainer +38 -0
  433. package/templates/devcontainer/devcontainer.json +31 -0
  434. package/templates/distribution/bootstrap.ps1 +142 -0
  435. package/templates/distribution/bootstrap.sh +196 -0
  436. package/templates/distribution/run.mjs +187 -0
  437. package/templates/docs/adr.md +44 -8
  438. package/templates/docs/changelog-entry.md +43 -0
  439. package/templates/docs/construct_guide.md +149 -0
  440. package/templates/docs/evidence-brief.md +2 -2
  441. package/templates/docs/meta-prd.md +140 -19
  442. package/templates/docs/onboarding.md +57 -0
  443. package/templates/docs/prd.md +159 -15
  444. package/templates/docs/research-brief.md +4 -4
  445. package/templates/homebrew/construct.rb +67 -0
  446. package/langfuse/docker-compose.yml +0 -82
  447. package/lib/eval-harness.mjs +0 -59
  448. package/lib/hooks/bootstrap-guard.mjs +0 -90
  449. package/lib/hooks/console-warn.mjs +0 -43
  450. package/lib/hooks/continuation-enforcer.mjs +0 -72
  451. package/lib/hooks/drive-guard.mjs +0 -89
  452. package/lib/hooks/mcp-task-scope.mjs +0 -47
  453. package/lib/hooks/task-completed-guard.mjs +0 -43
  454. package/lib/hooks/teammate-idle-guard.mjs +0 -54
  455. package/lib/hooks/workflow-guard.mjs +0 -62
  456. package/lib/prompt-composer.mjs +0 -196
  457. package/lib/review.mjs +0 -429
  458. package/lib/server/static/app.js +0 -841
  459. package/lib/telemetry/langfuse-model-sync.mjs +0 -270
  460. package/rules/common/hooks.md +0 -35
  461. package/rules/zh/README.md +0 -113
  462. package/rules/zh/agents.md +0 -55
  463. package/rules/zh/code-review.md +0 -129
  464. package/rules/zh/coding-style.md +0 -53
  465. package/rules/zh/development-workflow.md +0 -49
  466. package/rules/zh/git-workflow.md +0 -29
  467. package/rules/zh/hooks.md +0 -35
  468. package/rules/zh/patterns.md +0 -36
  469. package/rules/zh/performance.md +0 -60
  470. package/rules/zh/security.md +0 -34
  471. 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 — AI-powered doc structure scaffolding
3
+ * lib/init-docs.mjs — stand up a documentation system for a project.
4
4
  *
5
- * Uses a fast model (Haiku) to analyze the project, ask 2-3 clarifying
6
- * questions, then generate a tailored documentation structure.
7
- * Falls back to a minimal static scaffold if no API key is available.
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
- import { defaultWorkflow } from "./workflow-state.mjs";
20
- import { readOpenRouterApiKeyFromOpenCodeConfig } from "./model-router.mjs";
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 targetArg = args.find((a) => !a.startsWith("--"));
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 FAST_MODEL = "claude-haiku-4-5-20251001";
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
- // ─── .env loader ──────────────────────────────────────────────────────────────
48
+ const created = [];
49
+ const skipped = [];
33
50
 
34
- function loadEnv(envPath) {
35
- if (!fs.existsSync(envPath)) return;
36
- for (const line of fs.readFileSync(envPath, "utf8").split("\n")) {
37
- const trimmed = line.trim();
38
- if (!trimmed || trimmed.startsWith("#")) continue;
39
- const eq = trimmed.indexOf("=");
40
- if (eq === -1) continue;
41
- const key = trimmed.slice(0, eq).trim();
42
- const val = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
43
- if (!process.env[key]) process.env[key] = val;
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
- loadEnv(path.join(ROOT_DIR, ".env"));
48
-
49
- // ─── API caller ───────────────────────────────────────────────────────────────
50
-
51
- async function callModel(messages, system) {
52
- const anthropicKey = process.env.ANTHROPIC_API_KEY;
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
- const orKey = process.env.OPENROUTER_API_KEY || readOpenRouterApiKeyFromOpenCodeConfig();
69
- if (orKey) {
70
- const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
71
- method: "POST",
72
- headers: {
73
- Authorization: `Bearer ${orKey}`,
74
- "content-type": "application/json",
75
- "HTTP-Referer": "https://github.com/construct",
76
- },
77
- body: JSON.stringify({
78
- model: `anthropic/${FAST_MODEL}`,
79
- max_tokens: 4096,
80
- messages: [{ role: "system", content: system }, ...messages],
81
- }),
82
- });
83
- if (!res.ok) throw new Error(`OpenRouter API ${res.status}: ${await res.text()}`);
84
- const data = await res.json();
85
- return data.choices[0].message.content;
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
- return null;
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
- for (const name of ["README.md", "readme.md", "README.txt"]) {
138
- const p = path.join(targetPath, name);
139
- if (fs.existsSync(p)) {
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
- ctx.structure = dirTree(targetPath).join("\n");
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
- const docsPath = path.join(targetPath, "docs");
148
- if (fs.existsSync(docsPath)) {
149
- ctx.existingDocs = fs.readdirSync(docsPath).slice(0, 20).join(", ");
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
- try {
153
- ctx.gitRemote = execSync("git remote get-url origin", { cwd: targetPath, timeout: 3000, stdio: ["pipe", "pipe", "pipe"] })
154
- .toString().trim();
155
- } catch {}
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
- return ctx;
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 contextToText(ctx) {
161
- const parts = [`Project: ${ctx.name}`];
162
- if (ctx.description) parts.push(`Description: ${ctx.description}`);
163
- if (ctx.gitRemote) parts.push(`Git remote: ${ctx.gitRemote}`);
164
- if (ctx.manifestFile) parts.push(`Manifest: ${ctx.manifestFile}`);
165
- if (ctx.dependencies?.length) parts.push(`Dependencies: ${ctx.dependencies.join(", ")}`);
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
- // ─── JSON extraction ──────────────────────────────────────────────────────────
271
+ function normalizeLaneKey(name) {
272
+ return LANE_ALIASES[name.trim().toLowerCase()] ?? name.trim().toLowerCase();
273
+ }
175
274
 
176
- function extractJson(text) {
177
- const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/);
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
- // ─── File writing ─────────────────────────────────────────────────────────────
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
- const created = [];
188
- const skipped = [];
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
- fs.writeFileSync(filePath, content, "utf8");
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
- // ─── Static fallback ──────────────────────────────────────────────────────────
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
- function scaffoldStatic(projectName, workflowJson) {
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
- > Required project state. All LLMs working in this repo, including Construct, must keep the core documents below current.
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
- ## Required core documents
368
+ ## Operating model
216
369
 
217
- | File | Purpose | Update when |
218
- |---|---|---|
219
- | .cx/context.md | Session-resumable human summary | Active work, decisions, architecture assumptions, or open questions change |
220
- | .cx/context.json | Machine-readable resumable context | Context state needs to stay in sync with .cx/context.md |
221
- | .cx/workflow.json | Canonical workflow/task state | Non-trivial work starts, changes phase, or completes |
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
- ## Contents
376
+ ## Lanes
226
377
 
227
- - [Architecture](./architecture.md)
228
- - [Runbooks](./runbooks/)
229
- - [ADRs](../.cx/decisions/)
378
+ ${laneLines.join("\n")}
230
379
 
231
380
  ## Maintenance rule
232
381
 
233
- If work changes project reality, update the affected core document before calling it done.
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
- // ─── Prompts ──────────────────────────────────────────────────────────────────
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
- const SYSTEM_QUESTIONS = `You are a documentation architect. You've been given context about a software project.
393
+ return `<!--
394
+ docs/architecture.md — canonical architecture context and documentation-system contract.
244
395
 
245
- Ask 2–3 targeted clarifying questions to fill gaps that will materially affect the doc structure.
246
- Good questions cover things like: team collaboration style, external consumers, operational complexity,
247
- compliance needs, or release cadence only when these aren't clear from the context.
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
- Do NOT ask about things already obvious from the code. Do NOT ask open-ended questions.
250
- Each question should have a short, specific answer (a few words or a sentence).
401
+ # ${projectName} Architecture
251
402
 
252
- Output ONLY valid JSON:
253
- {
254
- "questions": [
255
- { "id": "q1", "question": "..." },
256
- { "id": "q2", "question": "..." }
257
- ]
258
- }`;
403
+ ## System overview
259
404
 
260
- const SYSTEM_GENERATE = `You are a documentation architect generating a tailored doc structure.
405
+ Describe the runtime shape, major modules, external dependencies, and key data boundaries.
261
406
 
262
- Generate complete, project-specific files — not generic templates. Use real section headings,
263
- meaningful placeholder content, and examples that match the actual tech stack and project type.
407
+ ## Project-state hierarchy
264
408
 
265
- Rules:
266
- - Always include .cx/context.md, .cx/context.json, .cx/workflow.json, docs/README.md, and docs/architecture.md
267
- - Treat these files as required, maintained project state for all LLMs working in the repo, including Construct
268
- - .cx/context.md should stay under 100 lines and contain real project sections
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
- Output ONLY valid JSON:
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
- // ─── Main ─────────────────────────────────────────────────────────────────────
416
+ ${laneLines.join("\n")}
283
417
 
284
- async function main() {
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
- if (!questionsResponse) {
302
- console.log(" No API key found using static scaffold.\n");
303
- console.log(" Set ANTHROPIC_API_KEY or OPENROUTER_API_KEY in .env for AI-tailored output.\n");
304
- scaffoldStatic(ctx.name, workflowJson);
305
- printSummary();
306
- return;
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
- // Parse and ask questions
310
- let questions = [];
311
- try {
312
- questions = extractJson(questionsResponse).questions ?? [];
313
- } catch {
314
- // skip questions if parse fails, go straight to generation
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
- const answers = {};
318
- if (questions.length && !skipInteractive) {
319
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
320
- console.log("A few questions to tailor the structure:\n");
321
- for (const q of questions) {
322
- const answer = await new Promise((resolve) => rl.question(` ${q.question}\n > `, resolve));
323
- answers[q.id] = answer.trim();
324
- console.log();
325
- }
326
- rl.close();
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
- // Build generation prompt
330
- const qaText = questions.length && Object.keys(answers).length
331
- ? "\n\nAnswers to clarifying questions:\n" +
332
- questions.filter((q) => answers[q.id]).map((q) => `Q: ${q.question}\nA: ${answers[q.id]}`).join("\n\n")
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
- const genPrompt = `${contextText}${qaText}\n\n---\n.cx/workflow.json (use verbatim):\n${workflowJson}`;
336
-
337
- let genResponse = null;
338
- try {
339
- process.stdout.write(" Generating doc structure...");
340
- genResponse = await callModel([{ role: "user", content: genPrompt }], SYSTEM_GENERATE);
341
- process.stdout.write(" done\n\n");
342
- } catch (err) {
343
- process.stdout.write(` failed (${err.message})\n`);
344
- console.log(" Falling back to static scaffold.\n");
345
- scaffoldStatic(ctx.name, workflowJson);
346
- printSummary();
347
- return;
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
- try {
351
- const plan = extractJson(genResponse);
352
- if (plan.summary) console.log(` ${plan.summary}\n`);
353
- for (const file of plan.files ?? []) {
354
- if (file.path && file.content) {
355
- writeIfMissing(path.join(target, file.path), file.content);
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
- printSummary();
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
- console.log("Created:");
370
- for (const f of created) console.log(` + ${f}`);
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
- console.log("\nSkipped (already exist):");
374
- for (const f of skipped) console.log(` ~ ${f}`);
813
+ process.stdout.write("\nSkipped (already exist):\n");
814
+ for (const file of skipped) process.stdout.write(` ~ ${file}\n`);
375
815
  }
376
- console.log(`\n${created.length} created, ${skipped.length} skipped.`);
816
+ process.stdout.write(`\n${created.length} created, ${skipped.length} skipped.\n`);
377
817
  }
378
818
 
379
- main().catch((err) => {
380
- console.error("Error:", err.message);
819
+ main().catch((error) => {
820
+ process.stderr.write(`Error: ${error.message}\n`);
381
821
  process.exit(1);
382
822
  });