coding-os 0.3.2__py3-none-any.whl

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 (1304) hide show
  1. adapters/claude/README.md +6 -0
  2. adapters/claude/_install_helpers/extract_stacks.py +43 -0
  3. adapters/claude/_install_helpers/update_mcp_json.py +75 -0
  4. adapters/claude/adapter.yaml +164 -0
  5. adapters/claude/hooks/README.md +40 -0
  6. adapters/claude/hooks/agent_memory_sync.py +131 -0
  7. adapters/claude/hooks/ensure-agent-memory-link.sh +36 -0
  8. adapters/claude/hooks/sync-agent-memory.sh +21 -0
  9. adapters/claude/install.sh +86 -0
  10. adapters/claude/sdk_dispatcher.py +871 -0
  11. adapters/claude/settings.local.template.json +31 -0
  12. adapters/claude/settings.template.json +808 -0
  13. adapters/claude/update_mcp_json.py +85 -0
  14. adapters/codex/adapter.yaml +254 -0
  15. adapters/codex/chat_provider.py +230 -0
  16. adapters/codex/commands/formula-f1.md +129 -0
  17. adapters/codex/commands/formula-f10.md +100 -0
  18. adapters/codex/commands/formula-f11.md +123 -0
  19. adapters/codex/commands/formula-f2.md +139 -0
  20. adapters/codex/commands/formula-f3.md +127 -0
  21. adapters/codex/commands/formula-f4.md +101 -0
  22. adapters/codex/commands/formula-f5.md +135 -0
  23. adapters/codex/commands/formula-f6.md +147 -0
  24. adapters/codex/commands/formula-f7.md +111 -0
  25. adapters/codex/commands/formula-f8.md +133 -0
  26. adapters/codex/commands/formula-f9.md +112 -0
  27. adapters/codex/enable_codex_hooks.py +94 -0
  28. adapters/codex/ensure_codex_mcp.py +124 -0
  29. adapters/codex/hooks/codex-merge-hook-output.py +72 -0
  30. adapters/codex/hooks/codex-normalize-edit.py +96 -0
  31. adapters/codex/hooks/codex-postedit-dispatch.sh +75 -0
  32. adapters/codex/hooks/codex-posttool-dispatch.sh +70 -0
  33. adapters/codex/hooks/codex-preedit-dispatch.sh +83 -0
  34. adapters/codex/hooks/codex-pretool-dispatch.sh +82 -0
  35. adapters/codex/hooks/codex-sessionend-dispatch.sh +20 -0
  36. adapters/codex/hooks/codex-sessionstart-dispatch.sh +68 -0
  37. adapters/codex/hooks/codex-stop-dispatch.sh +73 -0
  38. adapters/codex/hooks/codex-userpromptsubmit-dispatch.sh +74 -0
  39. adapters/codex/hooks.template.json +208 -0
  40. adapters/codex/install.sh +83 -0
  41. adapters/codex/sdk_dispatcher.py +449 -0
  42. board_os/__init__.py +39 -0
  43. board_os/_agent_runtime.py +256 -0
  44. board_os/config.py +421 -0
  45. board_os/git_coherence.py +107 -0
  46. board_os/hub_adapter_manifest.py +140 -0
  47. board_os/mcp_tools.py +3228 -0
  48. board_os/migration.py +166 -0
  49. board_os/parser.py +317 -0
  50. board_os/presence.py +156 -0
  51. board_os/sync.py +320 -0
  52. board_os/transition_gates.py +224 -0
  53. board_os/transition_gates_cli.py +272 -0
  54. board_os/transition_gates_validator.py +551 -0
  55. board_os/verify_suites.py +126 -0
  56. board_os/verify_suites_cli.py +328 -0
  57. board_os/workflow.py +967 -0
  58. cli/__init__.py +0 -0
  59. cli/_data_types.py +248 -0
  60. cli/_init_helpers.py +587 -0
  61. cli/_resources.py +100 -0
  62. cli/adapter_registry.py +239 -0
  63. cli/add_stack.py +315 -0
  64. cli/aggregator.py +438 -0
  65. cli/board_commands.py +1218 -0
  66. cli/brain_commands.py +255 -0
  67. cli/cognition.py +345 -0
  68. cli/config_composer.py +349 -0
  69. cli/core_version.py +41 -0
  70. cli/cron_commands.py +278 -0
  71. cli/db_reset.py +298 -0
  72. cli/doc_commands.py +111 -0
  73. cli/doctor.py +2953 -0
  74. cli/doctor_board.py +365 -0
  75. cli/doctor_extras.py +1121 -0
  76. cli/doctor_graph.py +608 -0
  77. cli/doctor_tokens.py +254 -0
  78. cli/graph_commands.py +1265 -0
  79. cli/hook_renderer.py +393 -0
  80. cli/hub_commands.py +580 -0
  81. cli/list_adapters.py +79 -0
  82. cli/list_stacks.py +105 -0
  83. cli/logs_commands.py +89 -0
  84. cli/main.py +3070 -0
  85. cli/materialize_file.py +65 -0
  86. cli/mcp_start.py +153 -0
  87. cli/module_commands.py +513 -0
  88. cli/pr_commands.py +2024 -0
  89. cli/preset_commands.py +126 -0
  90. cli/preset_registry.py +171 -0
  91. cli/project_overrides.py +119 -0
  92. cli/registry.py +365 -0
  93. cli/remove_stack.py +492 -0
  94. cli/renderer.py +620 -0
  95. cli/setup.py +464 -0
  96. cli/skill_commands.py +689 -0
  97. cli/skill_registry.py +235 -0
  98. cli/skills_list.py +332 -0
  99. cli/stack_lint.py +351 -0
  100. cli/stack_registry.py +688 -0
  101. cli/subsystems.py +335 -0
  102. cli/sync_all.py +310 -0
  103. cli/tail_command.py +410 -0
  104. cli/update.py +608 -0
  105. cli/verify_since_edit.py +439 -0
  106. coding_os-0.3.2.dist-info/METADATA +508 -0
  107. coding_os-0.3.2.dist-info/RECORD +1304 -0
  108. coding_os-0.3.2.dist-info/WHEEL +5 -0
  109. coding_os-0.3.2.dist-info/entry_points.txt +5 -0
  110. coding_os-0.3.2.dist-info/licenses/LICENSE +201 -0
  111. coding_os-0.3.2.dist-info/top_level.txt +10 -0
  112. core/__init__.py +0 -0
  113. core/board_os/__init__.py +39 -0
  114. core/board_os/_agent_runtime.py +256 -0
  115. core/board_os/config.py +421 -0
  116. core/board_os/git_coherence.py +107 -0
  117. core/board_os/hub_adapter_manifest.py +140 -0
  118. core/board_os/mcp_tools.py +3228 -0
  119. core/board_os/migration.py +166 -0
  120. core/board_os/parser.py +317 -0
  121. core/board_os/presence.py +156 -0
  122. core/board_os/sync.py +320 -0
  123. core/board_os/transition-gates.yaml +176 -0
  124. core/board_os/transition_gates.py +224 -0
  125. core/board_os/transition_gates_cli.py +272 -0
  126. core/board_os/transition_gates_validator.py +551 -0
  127. core/board_os/verify-suites.yaml +113 -0
  128. core/board_os/verify_suites.py +126 -0
  129. core/board_os/verify_suites_cli.py +328 -0
  130. core/board_os/workflow.py +967 -0
  131. core/commands/board.md +27 -0
  132. core/commands/classify.md +23 -0
  133. core/commands/compose.md +23 -0
  134. core/commands/daily.md +31 -0
  135. core/commands/diagnose.md +7 -0
  136. core/commands/memory-search.md +23 -0
  137. core/commands/new-project.md +33 -0
  138. core/commands/retro.md +38 -0
  139. core/commands/review.md +14 -0
  140. core/commands/task.md +17 -0
  141. core/commands/verify.md +34 -0
  142. core/docs/thinking_os-final-edition.md +1449 -0
  143. core/doctor-config.yaml +74 -0
  144. core/graph_os/__init__.py +29 -0
  145. core/graph_os/backend.py +233 -0
  146. core/graph_os/backends/__init__.py +13 -0
  147. core/graph_os/backends/sqlite_backend.py +1053 -0
  148. core/graph_os/bench/__init__.py +17 -0
  149. core/graph_os/bench/fixtures.py +56 -0
  150. core/graph_os/bench/harness.py +95 -0
  151. core/graph_os/bench/persian_precision.py +142 -0
  152. core/graph_os/bench/scale_500k.py +120 -0
  153. core/graph_os/bench/token_cost.py +170 -0
  154. core/graph_os/bench/viewer_fps.py +110 -0
  155. core/graph_os/communities.py +410 -0
  156. core/graph_os/enterprise.py +218 -0
  157. core/graph_os/entry_points.py +226 -0
  158. core/graph_os/extractors/__init__.py +25 -0
  159. core/graph_os/extractors/code_generic.py +914 -0
  160. core/graph_os/extractors/code_go.py +1422 -0
  161. core/graph_os/extractors/code_json.py +340 -0
  162. core/graph_os/extractors/code_php.py +979 -0
  163. core/graph_os/extractors/code_python.py +1454 -0
  164. core/graph_os/extractors/code_shell.py +538 -0
  165. core/graph_os/extractors/code_toml.py +302 -0
  166. core/graph_os/extractors/code_ts.py +1665 -0
  167. core/graph_os/extractors/code_yaml.py +394 -0
  168. core/graph_os/extractors/contracts.py +1592 -0
  169. core/graph_os/extractors/md_links.py +890 -0
  170. core/graph_os/extractors/task_deps.py +345 -0
  171. core/graph_os/groups/__init__.py +22 -0
  172. core/graph_os/groups/cross_repo.py +156 -0
  173. core/graph_os/groups/manifest.py +141 -0
  174. core/graph_os/ingest/__init__.py +19 -0
  175. core/graph_os/ingest/base.py +306 -0
  176. core/graph_os/ingest/github.py +112 -0
  177. core/graph_os/ingest/zip.py +95 -0
  178. core/graph_os/toolchain.py +393 -0
  179. core/graph_os/tools/__init__.py +9 -0
  180. core/graph_os/tools/graph.py +5573 -0
  181. core/graph_os/tools/reindex_dispatch.py +730 -0
  182. core/graph_os/tree_sitter_overlay.py +235 -0
  183. core/graph_os/types.py +252 -0
  184. core/graph_os/vec_index.py +277 -0
  185. core/graph_os/viewer/__init__.py +12 -0
  186. core/graph_os/viewer/exporter.py +93 -0
  187. core/graph_os/viewer/template.py +189 -0
  188. core/hooks/_helpers/_paths.py +40 -0
  189. core/hooks/_helpers/advance_role.py +72 -0
  190. core/hooks/_helpers/auto_compose.py +228 -0
  191. core/hooks/_helpers/auto_validate_lessons.py +55 -0
  192. core/hooks/_helpers/branch_guard_check.py +796 -0
  193. core/hooks/_helpers/check_commit_message.py +108 -0
  194. core/hooks/_helpers/check_dangerous_rm.py +80 -0
  195. core/hooks/_helpers/check_git_bypass.py +154 -0
  196. core/hooks/_helpers/check_git_destructive.py +77 -0
  197. core/hooks/_helpers/check_settings_write.py +97 -0
  198. core/hooks/_helpers/consume_override.py +51 -0
  199. core/hooks/_helpers/context_budget.py +77 -0
  200. core/hooks/_helpers/cos_say_json.py +103 -0
  201. core/hooks/_helpers/destructive_edit_check.py +163 -0
  202. core/hooks/_helpers/detect_status_transition.py +82 -0
  203. core/hooks/_helpers/digest_regen.py +56 -0
  204. core/hooks/_helpers/doc_sync_check.py +498 -0
  205. core/hooks/_helpers/drain_embedding_outbox.py +52 -0
  206. core/hooks/_helpers/extract_additional_context.py +51 -0
  207. core/hooks/_helpers/extract_commit_msg_arg.py +74 -0
  208. core/hooks/_helpers/git_command_parse.py +424 -0
  209. core/hooks/_helpers/git_settings_fields.py +47 -0
  210. core/hooks/_helpers/graph_context_match.py +37 -0
  211. core/hooks/_helpers/graph_marker_check.py +70 -0
  212. core/hooks/_helpers/jit_recall.py +56 -0
  213. core/hooks/_helpers/json_field.py +41 -0
  214. core/hooks/_helpers/narrative_signal.py +59 -0
  215. core/hooks/_helpers/observation_count.py +31 -0
  216. core/hooks/_helpers/pre_commit_batch.py +177 -0
  217. core/hooks/_helpers/pre_commit_fake_input.py +42 -0
  218. core/hooks/_helpers/presence_gc.py +102 -0
  219. core/hooks/_helpers/presence_write.py +167 -0
  220. core/hooks/_helpers/recover_indirect.py +35 -0
  221. core/hooks/_helpers/routing_evolution.py +104 -0
  222. core/hooks/_helpers/session_recap.py +72 -0
  223. core/hooks/_helpers/skill_primer.py +229 -0
  224. core/hooks/_helpers/task_sync.py +59 -0
  225. core/hooks/_helpers/tool_failure_capture.py +147 -0
  226. core/hooks/_helpers/trajectory_autosnap.py +278 -0
  227. core/hooks/_helpers/trajectory_startup.py +62 -0
  228. core/hooks/_helpers/turn_summary.py +82 -0
  229. core/hooks/_helpers/validate_task_frontmatter.py +98 -0
  230. core/hooks/_helpers/wip_limit_check.py +103 -0
  231. core/hooks/_helpers/wip_lines.py +53 -0
  232. core/hooks/_helpers/work_log_append.py +89 -0
  233. core/hooks/_helpers/wrap_dispatch_output.py +82 -0
  234. core/hooks/advance-role.sh +48 -0
  235. core/hooks/agent-presence.sh +179 -0
  236. core/hooks/auto-brain-decay.sh +184 -0
  237. core/hooks/auto-compose-roles.sh +83 -0
  238. core/hooks/auto-graph-reconcile-shell.sh +119 -0
  239. core/hooks/auto-regen-doc-index.sh +120 -0
  240. core/hooks/auto-reindex-docs.sh +130 -0
  241. core/hooks/auto-task-sync.sh +56 -0
  242. core/hooks/auto-trace-rotate.sh +88 -0
  243. core/hooks/block-bad-patterns.sh +212 -0
  244. core/hooks/block-dangerous-commands.sh +182 -0
  245. core/hooks/block-hardcoded-literals.sh +90 -0
  246. core/hooks/block-migration-conflict.sh +114 -0
  247. core/hooks/block-protected-files.sh +129 -0
  248. core/hooks/block-secrets.sh +185 -0
  249. core/hooks/block-shared-tree-edit.sh +75 -0
  250. core/hooks/block-uv-heredoc.sh +78 -0
  251. core/hooks/branch-guard.sh +122 -0
  252. core/hooks/capture-observation.sh +76 -0
  253. core/hooks/capture-tool-failure.sh +24 -0
  254. core/hooks/capture-work-log.sh +89 -0
  255. core/hooks/check-agents-md-refs.sh +75 -0
  256. core/hooks/check-agents-md-size.sh +49 -0
  257. core/hooks/check-capture-worked.sh +148 -0
  258. core/hooks/check-doc-size.sh +61 -0
  259. core/hooks/check-mcp-extras.sh +92 -0
  260. core/hooks/check-state.sh +87 -0
  261. core/hooks/classify-task-mode.sh +103 -0
  262. core/hooks/cos-env.sh +1301 -0
  263. core/hooks/drain-embedding-outbox.sh +24 -0
  264. core/hooks/enforce-anti-ambiguity.sh +74 -0
  265. core/hooks/enforce-commit-message.sh +73 -0
  266. core/hooks/enforce-doc-anchor.sh +224 -0
  267. core/hooks/enforce-doc-sync.sh +206 -0
  268. core/hooks/enforce-graph-context.sh +89 -0
  269. core/hooks/enforce-graph-first-read.sh +94 -0
  270. core/hooks/enforce-memory-check.sh +128 -0
  271. core/hooks/enforce-rename-plan.sh +78 -0
  272. core/hooks/enforce-scaffold-boundary.sh +68 -0
  273. core/hooks/enforce-skill.sh +125 -0
  274. core/hooks/enforce-task-body.sh +52 -0
  275. core/hooks/enforce-task-start.sh +81 -0
  276. core/hooks/enforce-task-transition.sh +75 -0
  277. core/hooks/enforce-template.sh +143 -0
  278. core/hooks/enforce-verify.sh +112 -0
  279. core/hooks/enforce-wip-limit.sh +43 -0
  280. core/hooks/enforce-zoom.sh +69 -0
  281. core/hooks/ensure-hub-up.sh +67 -0
  282. core/hooks/inject-mcp-caller-session.sh +70 -0
  283. core/hooks/jit-recall.sh +65 -0
  284. core/hooks/link-commit-to-task.sh +143 -0
  285. core/hooks/lint-task.sh +40 -0
  286. core/hooks/nudge-docs-first.sh +71 -0
  287. core/hooks/nudge-git-mode.sh +29 -0
  288. core/hooks/nudge-graph-os.sh +118 -0
  289. core/hooks/nudge-learn-narrative.sh +36 -0
  290. core/hooks/nudge-model-routing.sh +32 -0
  291. core/hooks/nudge-reentry.sh +101 -0
  292. core/hooks/nudge-reuse-first.sh +68 -0
  293. core/hooks/nudge-task-discovery.sh +81 -0
  294. core/hooks/nudge-thinking-os.sh +109 -0
  295. core/hooks/pr-reap.sh +23 -0
  296. core/hooks/reclaim-sweep.sh +58 -0
  297. core/hooks/record-verify-auto.sh +77 -0
  298. core/hooks/record-verify.sh +74 -0
  299. core/hooks/regen-reminder.sh +104 -0
  300. core/hooks/registry.yaml +1262 -0
  301. core/hooks/remind-daily.sh +27 -0
  302. core/hooks/remind-dogfood.sh +70 -0
  303. core/hooks/remind-learn-validate.sh +94 -0
  304. core/hooks/rules-primer.sh +50 -0
  305. core/hooks/search-enforce-inventory.sh +108 -0
  306. core/hooks/search-verify-remaining.sh +132 -0
  307. core/hooks/session-context.sh +729 -0
  308. core/hooks/session-end.sh +145 -0
  309. core/hooks/session-skill-primer.sh +43 -0
  310. core/hooks/snapshot-transcript.sh +56 -0
  311. core/hooks/sync-task-current.sh +85 -0
  312. core/hooks/test-first-reminder.sh +120 -0
  313. core/hooks/test-governor.sh +173 -0
  314. core/hooks/thinking_os-gate.sh +52 -0
  315. core/hooks/track-backtrack.sh +35 -0
  316. core/hooks/track-discovery.sh +121 -0
  317. core/hooks/track-skill.sh +53 -0
  318. core/hooks/validate-task-frontmatter.sh +49 -0
  319. core/hooks/verify-rename-callers.sh +119 -0
  320. core/hooks/warn-abandoned-task.sh +99 -0
  321. core/hooks/warn-destructive-edit.sh +64 -0
  322. core/hooks/warn-diff-size.sh +43 -0
  323. core/hooks/warn-graph-empty.sh +81 -0
  324. core/hooks/warn-mcp-down.sh +190 -0
  325. core/hooks/write-state.sh +55 -0
  326. core/logging_os/__init__.py +33 -0
  327. core/logging_os/api.py +127 -0
  328. core/logging_os/bridge.py +80 -0
  329. core/logging_os/config.py +172 -0
  330. core/logging_os/fingerprint.py +25 -0
  331. core/logging_os/redact.py +53 -0
  332. core/logging_os/render.py +83 -0
  333. core/logging_os/sinks.py +164 -0
  334. core/rules/anti-overengineering.md +44 -0
  335. core/rules/api-contract-discipline.md +41 -0
  336. core/rules/dimension-registry.md +155 -0
  337. core/rules/git-workflow.md +57 -0
  338. core/rules/memory.md +46 -0
  339. core/rules/model-routing.md +22 -0
  340. core/rules/skill-enforcement.md +75 -0
  341. core/rules/test-discipline.md +38 -0
  342. core/rules/thinking_os.md +48 -0
  343. core/rules/transparency-banner.md +37 -0
  344. core/runtime_paths.yaml +36 -0
  345. core/scaffold_manifest.json +14430 -0
  346. core/scheduled/__init__.py +0 -0
  347. core/scheduled/_activity.py +126 -0
  348. core/scheduled/_state.py +113 -0
  349. core/scheduled/config.py +86 -0
  350. core/scheduled/dep_reconcile.py +135 -0
  351. core/scheduled/error_sweep.py +137 -0
  352. core/scheduled/nightly.py +930 -0
  353. core/scheduled/responsive_extract.py +65 -0
  354. core/schemas/adapter.schema.json +269 -0
  355. core/schemas/preset.schema.json +50 -0
  356. core/schemas/skill.schema.json +81 -0
  357. core/schemas/stack.schema.json +404 -0
  358. core/scripts/_lib.sh +9 -0
  359. core/scripts/docs-lint.sh +228 -0
  360. core/scripts/docs-nav-fix.sh +133 -0
  361. core/scripts/docs-staleness-check.sh +154 -0
  362. core/scripts/install-adapter.sh +266 -0
  363. core/scripts/link-stack-skills.sh +52 -0
  364. core/scripts/log-latest.sh +106 -0
  365. core/scripts/log-search.sh +89 -0
  366. core/scripts/log-write.sh +134 -0
  367. core/scripts/ref-resolve.sh +71 -0
  368. core/skills/a11y/SKILL.md +305 -0
  369. core/skills/a11y/assets/a11y-checklist.md +137 -0
  370. core/skills/a11y/references/aria-and-focus.md +247 -0
  371. core/skills/a11y/references/rn-accessibility.md +343 -0
  372. core/skills/a11y/references/screen-reader-testing.md +190 -0
  373. core/skills/agent-memory/SKILL.md +191 -0
  374. core/skills/agent-memory/assets/memory-checklist.md +21 -0
  375. core/skills/agent-memory/references/memory-recipes.md +57 -0
  376. core/skills/api-design/SKILL.md +232 -0
  377. core/skills/api-design/assets/api-design-checklist.md +110 -0
  378. core/skills/api-design/references/error-envelope.md +381 -0
  379. core/skills/api-design/references/idempotency-pagination.md +312 -0
  380. core/skills/api-design/references/rest-contracts.md +426 -0
  381. core/skills/auth-patterns/SKILL.md +352 -0
  382. core/skills/auth-patterns/assets/auth-checklist.md +118 -0
  383. core/skills/auth-patterns/references/jwt-and-service-tokens.md +343 -0
  384. core/skills/auth-patterns/references/passkeys-2fa.md +289 -0
  385. core/skills/auth-patterns/references/sessions-vs-jwt.md +230 -0
  386. core/skills/auth-patterns/scripts/cookie-flag-check.py +146 -0
  387. core/skills/backend-fundamentals/SKILL.md +238 -0
  388. core/skills/backend-fundamentals/assets/backend-checklist.md +27 -0
  389. core/skills/backend-fundamentals/references/backend-patterns.md +56 -0
  390. core/skills/backend-fundamentals/scripts/check_layering.py +83 -0
  391. core/skills/clean-code/SKILL.md +642 -0
  392. core/skills/clean-code/scripts/audit-fail-closed.py +167 -0
  393. core/skills/codebase-explorer/SKILL.md +89 -0
  394. core/skills/codebase-explorer/assets/reading-checklist.md +24 -0
  395. core/skills/codebase-explorer/references/reading-strategies.md +52 -0
  396. core/skills/codebase-explorer/scripts/outline.py +99 -0
  397. core/skills/db-design/SKILL.md +327 -0
  398. core/skills/db-design/assets/migration-template.sql +49 -0
  399. core/skills/db-design/references/migration-discipline.md +290 -0
  400. core/skills/db-design/references/postgres-patterns.md +340 -0
  401. core/skills/db-design/scripts/migration-safety.sh +150 -0
  402. core/skills/deployment-cicd/SKILL.md +260 -0
  403. core/skills/deployment-cicd/assets/deploy-checklist.md +26 -0
  404. core/skills/deployment-cicd/references/pipeline-and-release.md +54 -0
  405. core/skills/deployment-cicd/scripts/lint_workflow.py +79 -0
  406. core/skills/docker/SKILL.md +114 -0
  407. core/skills/docker/assets/dockerfile-checklist.md +31 -0
  408. core/skills/docker/references/compose-patterns.md +66 -0
  409. core/skills/docker/references/dockerfile-optimization.md +64 -0
  410. core/skills/docker/scripts/lint_dockerfile.sh +48 -0
  411. core/skills/docker/versions.json +16 -0
  412. core/skills/end-to-end-testing/SKILL.md +101 -0
  413. core/skills/end-to-end-testing/assets/e2e-checklist.md +23 -0
  414. core/skills/end-to-end-testing/references/maestro.md +63 -0
  415. core/skills/end-to-end-testing/references/playwright.md +68 -0
  416. core/skills/end-to-end-testing/scripts/lint_e2e.py +88 -0
  417. core/skills/end-to-end-testing/versions.json +16 -0
  418. core/skills/frontend-design/SKILL.md +76 -0
  419. core/skills/frontend-design/assets/design-checklist.md +29 -0
  420. core/skills/frontend-design/references/design-principles.md +65 -0
  421. core/skills/frontend-design/scripts/check_contrast.py +89 -0
  422. core/skills/frontend-fundamentals/SKILL.md +213 -0
  423. core/skills/frontend-fundamentals/assets/frontend-checklist.md +25 -0
  424. core/skills/frontend-fundamentals/references/rendering-and-state.md +66 -0
  425. core/skills/frontend-fundamentals/scripts/check_frontend.py +86 -0
  426. core/skills/graph-explorer/SKILL.md +215 -0
  427. core/skills/graph-explorer/scripts/explain-impact.sh +64 -0
  428. core/skills/graphql/SKILL.md +187 -0
  429. core/skills/grpc-microservices/SKILL.md +174 -0
  430. core/skills/hexagonal-architecture/SKILL.md +199 -0
  431. core/skills/hexagonal-architecture/assets/folder-scaffold.md +233 -0
  432. core/skills/hexagonal-architecture/references/anti-patterns.md +129 -0
  433. core/skills/hexagonal-architecture/references/go-fiber-layout.md +429 -0
  434. core/skills/hexagonal-architecture/references/python-fastapi-layout.md +453 -0
  435. core/skills/hexagonal-architecture/references/react-native-layout.md +428 -0
  436. core/skills/i18n/SKILL.md +126 -0
  437. core/skills/incident-response/SKILL.md +225 -0
  438. core/skills/incident-response/assets/incident-checklist.md +29 -0
  439. core/skills/incident-response/references/severity-and-runbook.md +53 -0
  440. core/skills/incident-response/scripts/classify_severity.py +86 -0
  441. core/skills/linux-sysadmin/SKILL.md +115 -0
  442. core/skills/linux-sysadmin/assets/hardening-checklist.md +29 -0
  443. core/skills/linux-sysadmin/references/ssh-hardening.md +62 -0
  444. core/skills/linux-sysadmin/references/systemd-and-services.md +77 -0
  445. core/skills/linux-sysadmin/scripts/triage.sh +50 -0
  446. core/skills/linux-sysadmin/versions.json +17 -0
  447. core/skills/llm-patterns/SKILL.md +410 -0
  448. core/skills/llm-patterns/assets/llm-feature-checklist.md +26 -0
  449. core/skills/llm-patterns/references/rag-and-evals.md +59 -0
  450. core/skills/llm-patterns/scripts/estimate_tokens.py +75 -0
  451. core/skills/messaging-queues/SKILL.md +142 -0
  452. core/skills/mobile-fundamentals/SKILL.md +406 -0
  453. core/skills/mobile-fundamentals/assets/mobile-launch-checklist.md +130 -0
  454. core/skills/mobile-fundamentals/references/navigation-and-deep-links.md +337 -0
  455. core/skills/mobile-fundamentals/references/offline-sync.md +339 -0
  456. core/skills/node-backend/SKILL.md +114 -0
  457. core/skills/node-backend/assets/node-checklist.md +25 -0
  458. core/skills/node-backend/references/async-and-errors.md +67 -0
  459. core/skills/node-backend/references/event-loop.md +65 -0
  460. core/skills/node-backend/scripts/check_package.py +78 -0
  461. core/skills/node-backend/versions.json +17 -0
  462. core/skills/observability/SKILL.md +289 -0
  463. core/skills/observability/assets/observability-checklist.md +27 -0
  464. core/skills/observability/references/instrumentation.md +57 -0
  465. core/skills/observability/scripts/lint_logging.py +77 -0
  466. core/skills/payments/SKILL.md +102 -0
  467. core/skills/performance/SKILL.md +305 -0
  468. core/skills/performance/assets/perf-checklist.md +143 -0
  469. core/skills/performance/references/mobile-performance.md +249 -0
  470. core/skills/performance/references/web-vitals.md +209 -0
  471. core/skills/php/SKILL.md +116 -0
  472. core/skills/php/assets/php-checklist.md +26 -0
  473. core/skills/php/references/modern-php.md +64 -0
  474. core/skills/php/references/security.md +72 -0
  475. core/skills/php/scripts/scan_php_smells.py +99 -0
  476. core/skills/php/versions.json +9 -0
  477. core/skills/pr-mode-driver/SKILL.md +65 -0
  478. core/skills/realtime-websockets/SKILL.md +152 -0
  479. core/skills/redis/SKILL.md +105 -0
  480. core/skills/redis/assets/redis-checklist.md +27 -0
  481. core/skills/redis/references/operations.md +66 -0
  482. core/skills/redis/references/patterns.md +62 -0
  483. core/skills/redis/scripts/analyze_info.py +101 -0
  484. core/skills/redis/versions.json +9 -0
  485. core/skills/search/SKILL.md +91 -0
  486. core/skills/search/references/grep.md +76 -0
  487. core/skills/search/scripts/verify-count.sh +73 -0
  488. core/skills/search-infra/SKILL.md +109 -0
  489. core/skills/security-mobile/SKILL.md +394 -0
  490. core/skills/security-mobile/assets/mobile-security-checklist.md +117 -0
  491. core/skills/security-mobile/references/masvs-l1-checklist.md +127 -0
  492. core/skills/security-web/SKILL.md +217 -0
  493. core/skills/security-web/assets/security-web-checklist.md +167 -0
  494. core/skills/security-web/references/owasp-top-10.md +551 -0
  495. core/skills/security-web/references/supply-chain.md +179 -0
  496. core/skills/security-web/scripts/csp-check.sh +153 -0
  497. core/skills/shell-scripting/SKILL.md +128 -0
  498. core/skills/shell-scripting/assets/script-checklist.md +33 -0
  499. core/skills/shell-scripting/references/argument-parsing.md +84 -0
  500. core/skills/shell-scripting/references/bash-robustness.md +78 -0
  501. core/skills/shell-scripting/scripts/lint_script.sh +53 -0
  502. core/skills/shell-scripting/scripts/new_script.py +131 -0
  503. core/skills/shell-scripting/versions.json +23 -0
  504. core/skills/sql-authoring/SKILL.md +104 -0
  505. core/skills/sql-authoring/assets/query-review-checklist.md +26 -0
  506. core/skills/sql-authoring/references/query-patterns.md +89 -0
  507. core/skills/sql-authoring/references/reading-explain.md +52 -0
  508. core/skills/sql-authoring/scripts/analyze_plan.py +100 -0
  509. core/skills/sql-authoring/versions.json +17 -0
  510. core/skills/state-management/SKILL.md +428 -0
  511. core/skills/state-management/references/tanstack-query-recipes.md +301 -0
  512. core/skills/state-management/references/zustand-recipes.md +364 -0
  513. core/skills/supabase/SKILL.md +108 -0
  514. core/skills/supabase/assets/supabase-checklist.md +26 -0
  515. core/skills/supabase/references/realtime-and-storage.md +55 -0
  516. core/skills/supabase/references/rls-and-auth.md +65 -0
  517. core/skills/supabase/scripts/check_rls.py +88 -0
  518. core/skills/supabase/versions.json +9 -0
  519. core/skills/task-driver/SKILL.md +272 -0
  520. core/skills/task-driver/scripts/task-lint.sh +163 -0
  521. core/skills/technical-writing/SKILL.md +85 -0
  522. core/skills/technical-writing/assets/doc-checklist.md +29 -0
  523. core/skills/technical-writing/references/doc-anatomy.md +53 -0
  524. core/skills/technical-writing/references/writing-craft.md +59 -0
  525. core/skills/technical-writing/scripts/new_doc.py +81 -0
  526. core/skills/terraform-k8s/SKILL.md +133 -0
  527. core/skills/testing-strategy/SKILL.md +266 -0
  528. core/skills/testing-strategy/assets/test-review-checklist.md +25 -0
  529. core/skills/testing-strategy/references/test-types.md +52 -0
  530. core/skills/testing-strategy/scripts/coverage_gate.py +79 -0
  531. core/skills/thinking_os/SKILL.md +288 -0
  532. core/skills/thinking_os/scripts/classify.sh +122 -0
  533. core/skills/typescript/SKILL.md +110 -0
  534. core/skills/typescript/assets/typescript-checklist.md +24 -0
  535. core/skills/typescript/references/strictness.md +53 -0
  536. core/skills/typescript/references/type-system.md +86 -0
  537. core/skills/typescript/scripts/check_tsconfig.py +92 -0
  538. core/skills/typescript/versions.json +9 -0
  539. core/subsystems.yaml +202 -0
  540. core/thinking_os/__init__.py +1 -0
  541. core/thinking_os/_agent_markers.py +32 -0
  542. core/thinking_os/agents/README.md +71 -0
  543. core/thinking_os/agents/analyst.md +139 -0
  544. core/thinking_os/agents/architect.md +127 -0
  545. core/thinking_os/agents/debugger.md +111 -0
  546. core/thinking_os/agents/deployer.md +112 -0
  547. core/thinking_os/agents/distiller.md +28 -0
  548. core/thinking_os/agents/documenter.md +101 -0
  549. core/thinking_os/agents/implementer.md +135 -0
  550. core/thinking_os/agents/internal/session_observer.md +39 -0
  551. core/thinking_os/agents/observer.md +100 -0
  552. core/thinking_os/agents/onboarder.md +81 -0
  553. core/thinking_os/agents/refactorer.md +123 -0
  554. core/thinking_os/agents/repairer.md +47 -0
  555. core/thinking_os/agents/researcher.md +129 -0
  556. core/thinking_os/agents/reviewer.md +147 -0
  557. core/thinking_os/agents/security_auditor.md +133 -0
  558. core/thinking_os/background.py +405 -0
  559. core/thinking_os/bootstrap_outcomes.py +200 -0
  560. core/thinking_os/budget.py +302 -0
  561. core/thinking_os/capture.py +495 -0
  562. core/thinking_os/cognition.py +516 -0
  563. core/thinking_os/cognition_schemas.py +517 -0
  564. core/thinking_os/compress.py +192 -0
  565. core/thinking_os/concepts.py +233 -0
  566. core/thinking_os/dashboard.py +159 -0
  567. core/thinking_os/database.py +2883 -0
  568. core/thinking_os/decay.py +393 -0
  569. core/thinking_os/digest.py +295 -0
  570. core/thinking_os/dispatcher.py +192 -0
  571. core/thinking_os/dispatcher_helpers.py +48 -0
  572. core/thinking_os/dispatchers/__init__.py +3 -0
  573. core/thinking_os/dispatchers/default.py +47 -0
  574. core/thinking_os/distill.py +192 -0
  575. core/thinking_os/doc_indexer.py +905 -0
  576. core/thinking_os/embeddings.py +943 -0
  577. core/thinking_os/formula_composer.py +556 -0
  578. core/thinking_os/gate_marker.py +75 -0
  579. core/thinking_os/graph.py +296 -0
  580. core/thinking_os/graph_indexer.py +360 -0
  581. core/thinking_os/health_check.py +517 -0
  582. core/thinking_os/impact.py +119 -0
  583. core/thinking_os/memory_gc.py +356 -0
  584. core/thinking_os/migrator_embeddings.py +318 -0
  585. core/thinking_os/precision.py +194 -0
  586. core/thinking_os/presets/registry.yaml +127 -0
  587. core/thinking_os/record_outcome.py +399 -0
  588. core/thinking_os/repair.py +105 -0
  589. core/thinking_os/retrieval_quality.py +239 -0
  590. core/thinking_os/roles/analyst.yaml +83 -0
  591. core/thinking_os/roles/architect.yaml +88 -0
  592. core/thinking_os/roles/debugger.yaml +71 -0
  593. core/thinking_os/roles/deployer.yaml +71 -0
  594. core/thinking_os/roles/documenter.yaml +72 -0
  595. core/thinking_os/roles/implementer.yaml +83 -0
  596. core/thinking_os/roles/observer.yaml +70 -0
  597. core/thinking_os/roles/refactorer.yaml +71 -0
  598. core/thinking_os/roles/researcher.yaml +72 -0
  599. core/thinking_os/roles/reviewer.yaml +79 -0
  600. core/thinking_os/roles/security_auditor.yaml +83 -0
  601. core/thinking_os/roles_state.py +168 -0
  602. core/thinking_os/sanitizer.py +320 -0
  603. core/thinking_os/server.py +3160 -0
  604. core/thinking_os/session_enrich.py +272 -0
  605. core/thinking_os/session_observe_worker.py +111 -0
  606. core/thinking_os/session_startup.py +74 -0
  607. core/thinking_os/session_summary.py +245 -0
  608. core/thinking_os/situations/registry.yaml +99 -0
  609. core/thinking_os/task_analyzer.py +462 -0
  610. core/thinking_os/task_parser.py +342 -0
  611. core/thinking_os/task_sync.py +73 -0
  612. core/thinking_os/tools/__init__.py +6 -0
  613. core/thinking_os/tools/_shared.py +947 -0
  614. core/thinking_os/tools/cognition.py +1867 -0
  615. core/thinking_os/tools/docs.py +770 -0
  616. core/thinking_os/tools/learning.py +2078 -0
  617. core/thinking_os/tools/logs.py +79 -0
  618. core/thinking_os/tools/memory.py +840 -0
  619. core/thinking_os/tools/metrics.py +200 -0
  620. core/thinking_os/tools/retrieve.py +415 -0
  621. core/thinking_os/tools/routing.py +658 -0
  622. core/thinking_os/tools/tasks.py +449 -0
  623. core/thinking_os/tools/trajectory.py +181 -0
  624. core/thinking_os/tracing.py +235 -0
  625. core/web/__init__.py +5 -0
  626. core/web/_cache.py +118 -0
  627. core/web/_deps.py +56 -0
  628. core/web/_envelope.py +85 -0
  629. core/web/_project_context.py +140 -0
  630. core/web/chat_providers.py +108 -0
  631. core/web/init_jobs.py +216 -0
  632. core/web/routes/__init__.py +25 -0
  633. core/web/routes/_bounded_read.py +76 -0
  634. core/web/routes/board.py +1089 -0
  635. core/web/routes/cognition.py +1838 -0
  636. core/web/routes/config.py +635 -0
  637. core/web/routes/graph.py +513 -0
  638. core/web/routes/health.py +180 -0
  639. core/web/routes/hooks.py +288 -0
  640. core/web/routes/hub.py +1219 -0
  641. core/web/routes/logs.py +374 -0
  642. core/web/routes/metrics.py +43 -0
  643. core/web/routes/observability.py +400 -0
  644. core/web/routes/patterns.py +227 -0
  645. core/web/routes/presence.py +609 -0
  646. core/web/routes/roles.py +446 -0
  647. core/web/routes/scheduled.py +261 -0
  648. core/web/routes/search.py +238 -0
  649. core/web/routes/sessions.py +220 -0
  650. core/web/routes/settings.py +363 -0
  651. core/web/routes/stream.py +547 -0
  652. core/web/security.py +157 -0
  653. core/web/server.py +283 -0
  654. graph_os/__init__.py +29 -0
  655. graph_os/backend.py +233 -0
  656. graph_os/backends/__init__.py +13 -0
  657. graph_os/backends/sqlite_backend.py +1053 -0
  658. graph_os/communities.py +410 -0
  659. graph_os/enterprise.py +218 -0
  660. graph_os/entry_points.py +226 -0
  661. graph_os/extractors/__init__.py +25 -0
  662. graph_os/extractors/code_generic.py +914 -0
  663. graph_os/extractors/code_go.py +1422 -0
  664. graph_os/extractors/code_json.py +340 -0
  665. graph_os/extractors/code_php.py +979 -0
  666. graph_os/extractors/code_python.py +1454 -0
  667. graph_os/extractors/code_shell.py +538 -0
  668. graph_os/extractors/code_toml.py +302 -0
  669. graph_os/extractors/code_ts.py +1665 -0
  670. graph_os/extractors/code_yaml.py +394 -0
  671. graph_os/extractors/contracts.py +1592 -0
  672. graph_os/extractors/md_links.py +890 -0
  673. graph_os/extractors/task_deps.py +345 -0
  674. graph_os/groups/__init__.py +22 -0
  675. graph_os/groups/cross_repo.py +156 -0
  676. graph_os/groups/manifest.py +141 -0
  677. graph_os/ingest/__init__.py +19 -0
  678. graph_os/ingest/base.py +306 -0
  679. graph_os/ingest/github.py +112 -0
  680. graph_os/ingest/zip.py +95 -0
  681. graph_os/toolchain.py +393 -0
  682. graph_os/tools/__init__.py +9 -0
  683. graph_os/tools/graph.py +5573 -0
  684. graph_os/tools/reindex_dispatch.py +730 -0
  685. graph_os/tree_sitter_overlay.py +235 -0
  686. graph_os/types.py +252 -0
  687. graph_os/vec_index.py +277 -0
  688. graph_os/viewer/__init__.py +12 -0
  689. graph_os/viewer/exporter.py +93 -0
  690. graph_os/viewer/template.py +189 -0
  691. scheduled/__init__.py +0 -0
  692. scheduled/_activity.py +126 -0
  693. scheduled/_state.py +113 -0
  694. scheduled/config.py +86 -0
  695. scheduled/dep_reconcile.py +135 -0
  696. scheduled/error_sweep.py +137 -0
  697. scheduled/nightly.py +930 -0
  698. scheduled/responsive_extract.py +65 -0
  699. scripts/__init__.py +4 -0
  700. scripts/_commit_msg_body.sh +31 -0
  701. scripts/_post_commit_body.sh +49 -0
  702. scripts/_pre_commit_body.sh +121 -0
  703. scripts/_prepare_commit_msg_body.sh +53 -0
  704. scripts/audit_mcp_tools.py +693 -0
  705. scripts/bench_sdk_dispatcher.py +177 -0
  706. scripts/capture_golden.py +169 -0
  707. scripts/check_graph_phantoms.py +75 -0
  708. scripts/dev/audit_doc_links.py +359 -0
  709. scripts/dev/audit_scaffold_module_tags.py +107 -0
  710. scripts/dev/backfill_doc_headers.py +328 -0
  711. scripts/dev/backfill_nav_lines.py +119 -0
  712. scripts/dev/fix_nav_placement.py +106 -0
  713. scripts/dev/inspect_sdk_options.py +45 -0
  714. scripts/dev/migrate_check_ids.py +170 -0
  715. scripts/dev/strip_purpose_blocks.py +154 -0
  716. scripts/dump_openapi.py +66 -0
  717. scripts/e2e_dispatch_tool.py +195 -0
  718. scripts/generate_manifest.py +166 -0
  719. scripts/golden_sections.py +20 -0
  720. scripts/graph_demo.py +161 -0
  721. scripts/install-git-hooks.sh +47 -0
  722. scripts/migrate_embeddings_minilm_to_bge_m3.py +84 -0
  723. scripts/operational_eval.py +445 -0
  724. scripts/probe_agent_session_resolver.py +59 -0
  725. scripts/prune_deleted_path.py +127 -0
  726. scripts/refactor_agent_dual_mode.py +171 -0
  727. scripts/refresh_skill_versions.py +302 -0
  728. scripts/regen_doc_index.py +209 -0
  729. scripts/regen_doctor_schema.py +62 -0
  730. scripts/regen_rules.py +94 -0
  731. scripts/rename_formulas_to_semantic.py +241 -0
  732. scripts/smoke_db_connections.py +183 -0
  733. scripts/smoke_doc_header.py +72 -0
  734. scripts/smoke_graph_e2e.py +374 -0
  735. scripts/smoke_sdk_dispatch.py +84 -0
  736. scripts/smoke_uid_resolver.py +164 -0
  737. scripts/verify_dispatchers.py +244 -0
  738. scripts/verify_phase_c_e2e.py +436 -0
  739. templates/__init__.py +6 -0
  740. templates/_base/Makefile.base +353 -0
  741. templates/_base/base.yaml +59 -0
  742. templates/_base/coding-os.yaml.template +39 -0
  743. templates/_base/dimension-registry.template.md +68 -0
  744. templates/_base/domain-config.template.json +46 -0
  745. templates/_base/fragments/anatomy-map.md.tmpl +11 -0
  746. templates/_base/fragments/context-discipline.md.tmpl +3 -0
  747. templates/_base/fragments/core-loop.md.tmpl +52 -0
  748. templates/_base/fragments/engineering-routing.md.tmpl +3 -0
  749. templates/_base/fragments/header.md.tmpl +6 -0
  750. templates/_base/fragments/identity.md.tmpl +3 -0
  751. templates/_base/fragments/principles.md.tmpl +3 -0
  752. templates/_base/fragments/retrieval-routing.md.tmpl +23 -0
  753. templates/_base/fragments/session-handoff.md.tmpl +3 -0
  754. templates/_base/fragments/skills.md.tmpl +3 -0
  755. templates/_base/fragments/ssot-map.md.tmpl +3 -0
  756. templates/_base/fragments/stop-conditions.md.tmpl +3 -0
  757. templates/_base/fragments/subagent-dispatch.md.tmpl +3 -0
  758. templates/_base/fragments/task-authoring.md.tmpl +69 -0
  759. templates/_base/fragments/task-logging.md.tmpl +8 -0
  760. templates/_base/fragments/tool-routing.md.tmpl +9 -0
  761. templates/_base/fragments/verification-matrix.md.tmpl +12 -0
  762. templates/_base/lang/dart/analysis_options.yaml +7 -0
  763. templates/_base/lang/php/phpcs.xml.dist +10 -0
  764. templates/_base/lang/python/pyproject.toml +19 -0
  765. templates/_base/lang/rust/clippy.toml +5 -0
  766. templates/_base/lang/rust/rustfmt.toml +3 -0
  767. templates/_base/lang/typescript/eslint.config.js +26 -0
  768. templates/_base/lang/typescript/tsconfig.json +15 -0
  769. templates/_base/lang/typescript/vitest.config.ts +10 -0
  770. templates/_base/scaffold/changes.log +1 -0
  771. templates/_base/scaffold/docs/00-index.md +55 -0
  772. templates/_base/scaffold/docs/_meta/feature-dependency-tree.md +30 -0
  773. templates/_base/scaffold/docs/_meta/foundation-map.md +56 -0
  774. templates/_base/scaffold/docs/_meta/questions.md +8 -0
  775. templates/_base/scaffold/docs/_meta/roadmap.md +33 -0
  776. templates/_base/scaffold/docs/api-contracts/00-index.md +58 -0
  777. templates/_base/scaffold/docs/api-contracts/error-format.md +58 -0
  778. templates/_base/scaffold/docs/architecture/00-index.md +42 -0
  779. templates/_base/scaffold/docs/architecture/adr/00-index.md +39 -0
  780. templates/_base/scaffold/docs/engineering/00-index.md +9 -0
  781. templates/_base/scaffold/docs/governance/00-index.md +55 -0
  782. templates/_base/scaffold/docs/governance/_templates/doc-cheat-sheet.md +202 -0
  783. templates/_base/scaffold/docs/governance/_templates/playbook-template.md +81 -0
  784. templates/_base/scaffold/docs/governance/_templates/post-mortem-template.md +85 -0
  785. templates/_base/scaffold/docs/governance/_templates/runbook-template.md +88 -0
  786. templates/_base/scaffold/docs/governance/_templates/security-review-template.md +111 -0
  787. templates/_base/scaffold/docs/governance/_templates/task-detail.md +61 -0
  788. templates/_base/scaffold/docs/governance/agent-workflow.md +101 -0
  789. templates/_base/scaffold/docs/governance/anatomy-contract.md +150 -0
  790. templates/_base/scaffold/docs/governance/critical-rules.md +224 -0
  791. templates/_base/scaffold/docs/governance/decision-records.md +58 -0
  792. templates/_base/scaffold/docs/governance/docs-first-protocol.md +157 -0
  793. templates/_base/scaffold/docs/governance/docs-system.md +151 -0
  794. templates/_base/scaffold/docs/governance/gdpr-compliance.md +66 -0
  795. templates/_base/scaffold/docs/governance/mcp-tool-inventory.md +112 -0
  796. templates/_base/scaffold/docs/governance/risk-register.md +26 -0
  797. templates/_base/scaffold/docs/governance/scaffold-boundary-contract.md +161 -0
  798. templates/_base/scaffold/docs/governance/task-lifecycle.md +125 -0
  799. templates/_base/scaffold/docs/governance/wrapper-derivation.md +50 -0
  800. templates/_base/scaffold/docs/insights/00-index.md +17 -0
  801. templates/_base/scaffold/docs/ops/00-index.md +59 -0
  802. templates/_base/scaffold/docs/ops/runbooks/00-index.md +9 -0
  803. templates/_base/scaffold/docs/playbooks/00-index.md +12 -0
  804. templates/_base/scaffold/docs/playbooks/research-validation.md +29 -0
  805. templates/_base/scaffold/docs/playbooks/security-review.md +41 -0
  806. templates/_base/scaffold/docs/prd/00-index.md +43 -0
  807. templates/_base/scaffold/docs/prd/01-snapshot-vision.md +56 -0
  808. templates/_base/scaffold/docs/workflow/workflow-guide.md +138 -0
  809. templates/_base/scaffold/src/shared/README.md +23 -0
  810. templates/_base/skill-enforcement.template.md +14 -0
  811. templates/_base/task-detail.template.md +61 -0
  812. templates/_presets/ai-saas.yaml +9 -0
  813. templates/_presets/django-next.yaml +8 -0
  814. templates/_presets/dotnet-react.yaml +8 -0
  815. templates/_presets/flutter-baas.yaml +8 -0
  816. templates/_presets/go-react.yaml +8 -0
  817. templates/_presets/hexagonal-product.yaml +16 -0
  818. templates/_presets/jamstack.yaml +8 -0
  819. templates/_presets/laravel-vue.yaml +8 -0
  820. templates/_presets/mean.yaml +8 -0
  821. templates/_presets/mern.yaml +9 -0
  822. templates/_presets/nest-angular.yaml +8 -0
  823. templates/_presets/nextjs-fastapi.yaml +10 -0
  824. templates/_presets/nuxt-fullstack.yaml +9 -0
  825. templates/_presets/pern.yaml +9 -0
  826. templates/_presets/rails-react.yaml +8 -0
  827. templates/_presets/rn-api.yaml +8 -0
  828. templates/_presets/rust-svelte.yaml +8 -0
  829. templates/_presets/spring-react.yaml +8 -0
  830. templates/_presets/t3-style.yaml +9 -0
  831. templates/_presets/tall.yaml +8 -0
  832. templates/_presets/wordpress-cms.yaml +8 -0
  833. templates/angular/rules/frontend.md +19 -0
  834. templates/angular/scaffold/docs/engineering/accessibility.md +46 -0
  835. templates/angular/scaffold/docs/engineering/angular-rules.md +35 -0
  836. templates/angular/scaffold/docs/playbooks/angular-app.md +42 -0
  837. templates/angular/scaffold/src/frontend/angular.json +52 -0
  838. templates/angular/scaffold/src/frontend/package.json +28 -0
  839. templates/angular/scaffold/src/frontend/src/app/app.component.ts +19 -0
  840. templates/angular/scaffold/src/frontend/src/app/app.config.ts +22 -0
  841. templates/angular/scaffold/src/frontend/src/app/app.routes.ts +8 -0
  842. templates/angular/scaffold/src/frontend/src/app/core/global-error-handler.ts +12 -0
  843. templates/angular/scaffold/src/frontend/src/app/health/health.component.ts +14 -0
  844. templates/angular/scaffold/src/frontend/src/app/health/health.service.spec.ts +16 -0
  845. templates/angular/scaffold/src/frontend/src/app/health/health.service.ts +10 -0
  846. templates/angular/scaffold/src/frontend/src/index.html +11 -0
  847. templates/angular/scaffold/src/frontend/src/main.ts +9 -0
  848. templates/angular/scaffold/src/frontend/src/styles.css +14 -0
  849. templates/angular/scaffold/src/frontend/tsconfig.app.json +8 -0
  850. templates/angular/scaffold/src/frontend/tsconfig.json +27 -0
  851. templates/angular/scaffold/src/frontend/tsconfig.spec.json +8 -0
  852. templates/angular/scaffold-boundary.yaml +27 -0
  853. templates/angular/skills/angular/SKILL.md +79 -0
  854. templates/angular/skills/angular/references/anatomy.md +69 -0
  855. templates/angular/stack.yaml +75 -0
  856. templates/aspnet-core/rules/backend.md +20 -0
  857. templates/aspnet-core/scaffold/docs/engineering/aspnet-core-rules.md +36 -0
  858. templates/aspnet-core/scaffold/docs/playbooks/aspnet-core-service.md +40 -0
  859. templates/aspnet-core/scaffold/src/backend/Backend.csproj +11 -0
  860. templates/aspnet-core/scaffold/src/backend/Backend.sln +27 -0
  861. templates/aspnet-core/scaffold/src/backend/Common/ExceptionHandlingMiddleware.cs +35 -0
  862. templates/aspnet-core/scaffold/src/backend/Features/Health/HealthEndpoints.cs +10 -0
  863. templates/aspnet-core/scaffold/src/backend/Features/Health/HealthService.cs +9 -0
  864. templates/aspnet-core/scaffold/src/backend/Program.cs +22 -0
  865. templates/aspnet-core/scaffold/src/backend/tests/Backend.Tests/Backend.Tests.csproj +22 -0
  866. templates/aspnet-core/scaffold/src/backend/tests/Backend.Tests/HealthServiceTests.cs +15 -0
  867. templates/aspnet-core/scaffold-boundary.yaml +24 -0
  868. templates/aspnet-core/skills/aspnet-core/SKILL.md +80 -0
  869. templates/aspnet-core/skills/aspnet-core/references/anatomy.md +65 -0
  870. templates/aspnet-core/stack.yaml +68 -0
  871. templates/astro/rules/frontend.md +20 -0
  872. templates/astro/scaffold/docs/engineering/astro-rules.md +40 -0
  873. templates/astro/scaffold/docs/playbooks/astro-app.md +57 -0
  874. templates/astro/scaffold/docs/playbooks/content-seo.md +37 -0
  875. templates/astro/scaffold/src/frontend/astro.config.mjs +10 -0
  876. templates/astro/scaffold/src/frontend/package.json +23 -0
  877. templates/astro/scaffold/src/frontend/src/components/Greeting.astro +13 -0
  878. templates/astro/scaffold/src/frontend/src/content/posts/hello.md +10 -0
  879. templates/astro/scaffold/src/frontend/src/content.config.ts +19 -0
  880. templates/astro/scaffold/src/frontend/src/lib/problem.test.ts +39 -0
  881. templates/astro/scaffold/src/frontend/src/lib/problem.ts +30 -0
  882. templates/astro/scaffold/src/frontend/src/pages/api/health.ts +13 -0
  883. templates/astro/scaffold/src/frontend/src/pages/index.astro +21 -0
  884. templates/astro/scaffold/src/frontend/tsconfig.json +9 -0
  885. templates/astro/scaffold/src/frontend/vitest.config.ts +10 -0
  886. templates/astro/scaffold-boundary.yaml +28 -0
  887. templates/astro/skills/astro/SKILL.md +71 -0
  888. templates/astro/skills/astro/references/anatomy.md +66 -0
  889. templates/astro/stack.yaml +75 -0
  890. templates/csharp-plain/scaffold/src/backend/Backend.csproj +12 -0
  891. templates/csharp-plain/scaffold/src/backend/Program.cs +1 -0
  892. templates/csharp-plain/scaffold-boundary.yaml +23 -0
  893. templates/csharp-plain/stack.yaml +50 -0
  894. templates/django/rules/backend.md +18 -0
  895. templates/django/scaffold/docs/engineering/anti-ambiguity.md +74 -0
  896. templates/django/scaffold/docs/engineering/backend-rules.md +133 -0
  897. templates/django/scaffold/docs/engineering/glossary.md +51 -0
  898. templates/django/scaffold/docs/engineering/logging-standards.md +107 -0
  899. templates/django/scaffold/docs/engineering/naming-conventions.md +68 -0
  900. templates/django/scaffold/docs/engineering/secrets-rotation-runbook.md +142 -0
  901. templates/django/scaffold/docs/playbooks/backend-api.md +119 -0
  902. templates/django/scaffold/src/backend/config/__init__.py +0 -0
  903. templates/django/scaffold/src/backend/config/settings.py +31 -0
  904. templates/django/scaffold/src/backend/config/urls.py +11 -0
  905. templates/django/scaffold/src/backend/config/wsgi.py +6 -0
  906. templates/django/scaffold/src/backend/manage.py +14 -0
  907. templates/django/scaffold/src/backend/pyproject.toml +37 -0
  908. templates/django/scaffold/src/backend/tests/test_health.py +4 -0
  909. templates/django/scaffold-boundary.yaml +25 -0
  910. templates/django/skills/python-django/SKILL.md +450 -0
  911. templates/django/skills/python-django/references/anatomy.md +117 -0
  912. templates/django/skills/python-django/scripts/new_endpoint.py +89 -0
  913. templates/django/stack.yaml +73 -0
  914. templates/fastapi/rules/backend.md +18 -0
  915. templates/fastapi/scaffold/docs/engineering/fastapi-rules.md +37 -0
  916. templates/fastapi/scaffold/docs/playbooks/fastapi-service.md +30 -0
  917. templates/fastapi/scaffold/src/backend/app/__init__.py +0 -0
  918. templates/fastapi/scaffold/src/backend/app/main.py +8 -0
  919. templates/fastapi/scaffold/src/backend/pyproject.toml +39 -0
  920. templates/fastapi/scaffold/src/backend/tests/test_health.py +11 -0
  921. templates/fastapi/scaffold-boundary.yaml +25 -0
  922. templates/fastapi/skills/python-fastapi/SKILL.md +75 -0
  923. templates/fastapi/skills/python-fastapi/references/anatomy.md +117 -0
  924. templates/fastapi/skills/python-fastapi/scripts/new_endpoint.py +101 -0
  925. templates/fastapi/stack.yaml +57 -0
  926. templates/flutter/rules/mobile.md +26 -0
  927. templates/flutter/scaffold/docs/engineering/flutter-rules.md +35 -0
  928. templates/flutter/scaffold/docs/playbooks/flutter-app.md +45 -0
  929. templates/flutter/scaffold/src/mobile/lib/core/error_mapper.dart +17 -0
  930. templates/flutter/scaffold/src/mobile/lib/core/router.dart +13 -0
  931. templates/flutter/scaffold/src/mobile/lib/main.dart +22 -0
  932. templates/flutter/scaffold/src/mobile/lib/screens/health_screen.dart +32 -0
  933. templates/flutter/scaffold/src/mobile/lib/services/health_service.dart +11 -0
  934. templates/flutter/scaffold/src/mobile/lib/state/health_provider.dart +13 -0
  935. templates/flutter/scaffold/src/mobile/pubspec.yaml +22 -0
  936. templates/flutter/scaffold/src/mobile/test/health_provider_test.dart +90 -0
  937. templates/flutter/scaffold-boundary.yaml +28 -0
  938. templates/flutter/skills/flutter/SKILL.md +76 -0
  939. templates/flutter/skills/flutter/references/anatomy.md +64 -0
  940. templates/flutter/stack.yaml +70 -0
  941. templates/go/rules/backend.md +19 -0
  942. templates/go/scaffold/docs/engineering/go-rules.md +45 -0
  943. templates/go/scaffold/docs/playbooks/go-service.md +30 -0
  944. templates/go/scaffold/src/backend/cmd/api/main.go +22 -0
  945. templates/go/scaffold/src/backend/cmd/api/main_test.go +20 -0
  946. templates/go/scaffold/src/backend/go.mod +3 -0
  947. templates/go/scaffold-boundary.yaml +25 -0
  948. templates/go/skills/go-patterns/SKILL.md +68 -0
  949. templates/go/skills/go-patterns/assets/go-checklist.md +29 -0
  950. templates/go/skills/go-patterns/references/anatomy.md +115 -0
  951. templates/go/skills/go-patterns/references/go-2026-idioms.md +92 -0
  952. templates/go/skills/go-patterns/scripts/new_endpoint.py +117 -0
  953. templates/go/skills/go-patterns/versions.json +16 -0
  954. templates/go/stack.yaml +54 -0
  955. templates/go-fiber/rules/backend.md +20 -0
  956. templates/go-fiber/scaffold/docs/engineering/fiber-rules.md +97 -0
  957. templates/go-fiber/scaffold/docs/playbooks/fiber-service.md +149 -0
  958. templates/go-fiber/scaffold/src/backend/cmd/api/main.go +21 -0
  959. templates/go-fiber/scaffold/src/backend/cmd/api/main_test.go +17 -0
  960. templates/go-fiber/scaffold/src/backend/go.mod +23 -0
  961. templates/go-fiber/scaffold/src/backend/go.sum +49 -0
  962. templates/go-fiber/scaffold-boundary.yaml +26 -0
  963. templates/go-fiber/skills/go-fiber/SKILL.md +203 -0
  964. templates/go-fiber/skills/go-fiber/assets/fiber-checklist.md +26 -0
  965. templates/go-fiber/skills/go-fiber/references/anatomy.md +116 -0
  966. templates/go-fiber/skills/go-fiber/references/fiber-v3-patterns.md +89 -0
  967. templates/go-fiber/skills/go-fiber/scripts/new_endpoint.py +107 -0
  968. templates/go-fiber/skills/go-fiber/versions.json +16 -0
  969. templates/go-fiber/stack.yaml +59 -0
  970. templates/go-plain/scaffold/src/backend/go.mod +3 -0
  971. templates/go-plain/scaffold/src/backend/main.go +7 -0
  972. templates/go-plain/scaffold-boundary.yaml +22 -0
  973. templates/go-plain/stack.yaml +51 -0
  974. templates/java-plain/scaffold/src/backend/mvnw +302 -0
  975. templates/java-plain/scaffold/src/backend/pom.xml +41 -0
  976. templates/java-plain/scaffold/src/backend/src/main/java/com/example/app/Main.java +10 -0
  977. templates/java-plain/scaffold-boundary.yaml +23 -0
  978. templates/java-plain/stack.yaml +51 -0
  979. templates/laravel/rules/backend.md +20 -0
  980. templates/laravel/scaffold/docs/engineering/laravel-rules.md +28 -0
  981. templates/laravel/scaffold/docs/playbooks/laravel-service.md +28 -0
  982. templates/laravel/scaffold/src/backend/app/Exceptions/Handler.php +27 -0
  983. templates/laravel/scaffold/src/backend/app/Http/Controllers/HealthController.php +15 -0
  984. templates/laravel/scaffold/src/backend/app/Support/HealthStatus.php +14 -0
  985. templates/laravel/scaffold/src/backend/composer.json +24 -0
  986. templates/laravel/scaffold/src/backend/phpunit.xml +10 -0
  987. templates/laravel/scaffold/src/backend/public/index.php +8 -0
  988. templates/laravel/scaffold/src/backend/routes/api.php +7 -0
  989. templates/laravel/scaffold/src/backend/tests/Unit/HealthStatusTest.php +16 -0
  990. templates/laravel/scaffold-boundary.yaml +23 -0
  991. templates/laravel/skills/laravel/SKILL.md +56 -0
  992. templates/laravel/skills/laravel/references/anatomy.md +65 -0
  993. templates/laravel/stack.yaml +66 -0
  994. templates/meta/rules/graph-first.md +27 -0
  995. templates/meta/rules/hook-author.md +19 -0
  996. templates/meta/rules/mcp-tool-author.md +18 -0
  997. templates/meta/rules/meta-engineering.md +17 -0
  998. templates/meta/scaffold-boundary.yaml +55 -0
  999. templates/meta/skills/claude-sdk-integration/SKILL.md +163 -0
  1000. templates/meta/skills/claude-sdk-integration/assets/sdk-checklist.md +27 -0
  1001. templates/meta/skills/claude-sdk-integration/scripts/check_model_ids.py +97 -0
  1002. templates/meta/skills/graph-os-authoring/SKILL.md +278 -0
  1003. templates/meta/skills/graph-os-authoring/assets/graph-os-checklist.md +25 -0
  1004. templates/meta/skills/graph-os-authoring/scripts/new_extractor.py +76 -0
  1005. templates/meta/skills/hook-authoring/SKILL.md +292 -0
  1006. templates/meta/skills/hook-authoring/assets/hook-checklist.md +30 -0
  1007. templates/meta/skills/hook-authoring/scripts/new_hook.sh +75 -0
  1008. templates/meta/skills/mcp-tool-authoring/SKILL.md +301 -0
  1009. templates/meta/skills/mcp-tool-authoring/assets/mcp-tool-checklist.md +29 -0
  1010. templates/meta/skills/mcp-tool-authoring/scripts/new_tool.py +74 -0
  1011. templates/meta/skills/meta-engineering/SKILL.md +151 -0
  1012. templates/meta/skills/meta-engineering/assets/meta-edit-checklist.md +28 -0
  1013. templates/meta/skills/meta-engineering/scripts/which_layer.py +61 -0
  1014. templates/meta/skills/python-meta-server/SKILL.md +162 -0
  1015. templates/meta/skills/python-meta-server/assets/meta-server-checklist.md +28 -0
  1016. templates/meta/skills/python-meta-server/scripts/check_envelope.py +91 -0
  1017. templates/meta/skills/react-vite-hub/SKILL.md +140 -0
  1018. templates/meta/skills/react-vite-hub/assets/hub-ui-checklist.md +23 -0
  1019. templates/meta/skills/react-vite-hub/scripts/check_vite_env.py +73 -0
  1020. templates/meta/stack.yaml +119 -0
  1021. templates/nestjs/rules/backend.md +20 -0
  1022. templates/nestjs/scaffold/docs/engineering/nestjs-rules.md +33 -0
  1023. templates/nestjs/scaffold/docs/playbooks/nestjs-service.md +39 -0
  1024. templates/nestjs/scaffold/src/backend/nest-cli.json +5 -0
  1025. templates/nestjs/scaffold/src/backend/package.json +28 -0
  1026. templates/nestjs/scaffold/src/backend/src/app.module.ts +9 -0
  1027. templates/nestjs/scaffold/src/backend/src/common/all-exceptions.filter.ts +59 -0
  1028. templates/nestjs/scaffold/src/backend/src/health/health.controller.ts +14 -0
  1029. templates/nestjs/scaffold/src/backend/src/health/health.module.ts +10 -0
  1030. templates/nestjs/scaffold/src/backend/src/health/health.service.spec.ts +21 -0
  1031. templates/nestjs/scaffold/src/backend/src/health/health.service.ts +9 -0
  1032. templates/nestjs/scaffold/src/backend/src/main.ts +26 -0
  1033. templates/nestjs/scaffold/src/backend/tsconfig.json +16 -0
  1034. templates/nestjs/scaffold/src/backend/vitest.config.ts +9 -0
  1035. templates/nestjs/scaffold-boundary.yaml +25 -0
  1036. templates/nestjs/skills/nestjs/SKILL.md +67 -0
  1037. templates/nestjs/skills/nestjs/references/anatomy.md +65 -0
  1038. templates/nestjs/stack.yaml +68 -0
  1039. templates/nextjs/rules/frontend.md +18 -0
  1040. templates/nextjs/scaffold/docs/design/00-index.md +23 -0
  1041. templates/nextjs/scaffold/docs/design/colors-tokens.md +141 -0
  1042. templates/nextjs/scaffold/docs/design/components-patterns.md +159 -0
  1043. templates/nextjs/scaffold/docs/design/motion-accessibility.md +137 -0
  1044. templates/nextjs/scaffold/docs/design/typography-spacing.md +107 -0
  1045. templates/nextjs/scaffold/docs/engineering/accessibility-web.md +56 -0
  1046. templates/nextjs/scaffold/docs/engineering/copywriting-standard.md +102 -0
  1047. templates/nextjs/scaffold/docs/engineering/formatting-rules.md +89 -0
  1048. templates/nextjs/scaffold/docs/engineering/frontend-rendering-rules.md +80 -0
  1049. templates/nextjs/scaffold/docs/engineering/frontend-rules.md +183 -0
  1050. templates/nextjs/scaffold/docs/engineering/i18n-policy.md +99 -0
  1051. templates/nextjs/scaffold/docs/pages-content-spec/00-index.md +78 -0
  1052. templates/nextjs/scaffold/docs/playbooks/content-seo.md +55 -0
  1053. templates/nextjs/scaffold/docs/playbooks/docs-governance.md +51 -0
  1054. templates/nextjs/scaffold/docs/playbooks/frontend-ui.md +63 -0
  1055. templates/nextjs/scaffold/src/frontend/app/layout.tsx +14 -0
  1056. templates/nextjs/scaffold/src/frontend/app/page.tsx +3 -0
  1057. templates/nextjs/scaffold/src/frontend/eslint.config.js +17 -0
  1058. templates/nextjs/scaffold/src/frontend/lib/greeting.test.ts +9 -0
  1059. templates/nextjs/scaffold/src/frontend/lib/greeting.ts +3 -0
  1060. templates/nextjs/scaffold/src/frontend/package.json +28 -0
  1061. templates/nextjs/scaffold/src/frontend/tsconfig.json +18 -0
  1062. templates/nextjs/scaffold/src/frontend/vitest.config.ts +10 -0
  1063. templates/nextjs/scaffold-boundary.yaml +30 -0
  1064. templates/nextjs/skills/nextjs-react/SKILL.md +485 -0
  1065. templates/nextjs/skills/nextjs-react/references/anatomy.md +116 -0
  1066. templates/nextjs/skills/nextjs-react/scripts/new_component.py +72 -0
  1067. templates/nextjs/stack.yaml +81 -0
  1068. templates/node-express/rules/backend.md +20 -0
  1069. templates/node-express/scaffold/docs/engineering/express-rules.md +30 -0
  1070. templates/node-express/scaffold/docs/playbooks/express-service.md +35 -0
  1071. templates/node-express/scaffold/src/backend/package.json +24 -0
  1072. templates/node-express/scaffold/src/backend/src/index.ts +17 -0
  1073. templates/node-express/scaffold/src/backend/src/middleware/error-handler.ts +12 -0
  1074. templates/node-express/scaffold/src/backend/src/routes/health.test.ts +33 -0
  1075. templates/node-express/scaffold/src/backend/src/routes/health.ts +7 -0
  1076. templates/node-express/scaffold/src/backend/tsconfig.json +15 -0
  1077. templates/node-express/scaffold/src/backend/types/express-bootstrap.d.ts +21 -0
  1078. templates/node-express/scaffold-boundary.yaml +25 -0
  1079. templates/node-express/skills/node-express/SKILL.md +70 -0
  1080. templates/node-express/skills/node-express/references/anatomy.md +63 -0
  1081. templates/node-express/stack.yaml +63 -0
  1082. templates/python/scaffold/docs/engineering/python-rules.md +27 -0
  1083. templates/python/scaffold/docs/playbooks/python-library.md +35 -0
  1084. templates/python/stack.yaml +60 -0
  1085. templates/rails/rules/backend.md +10 -0
  1086. templates/rails/scaffold/docs/engineering/rails-rules.md +33 -0
  1087. templates/rails/scaffold/docs/playbooks/rails-service.md +42 -0
  1088. templates/rails/scaffold/src/backend/Gemfile +12 -0
  1089. templates/rails/scaffold/src/backend/app/controllers/application_controller.rb +26 -0
  1090. templates/rails/scaffold/src/backend/app/controllers/health_controller.rb +6 -0
  1091. templates/rails/scaffold/src/backend/app/models/health.rb +6 -0
  1092. templates/rails/scaffold/src/backend/config/application.rb +12 -0
  1093. templates/rails/scaffold/src/backend/config/boot.rb +3 -0
  1094. templates/rails/scaffold/src/backend/config/routes.rb +4 -0
  1095. templates/rails/scaffold/src/backend/config.ru +5 -0
  1096. templates/rails/scaffold/src/backend/spec/rails_helper.rb +18 -0
  1097. templates/rails/scaffold/src/backend/spec/requests/health_spec.rb +24 -0
  1098. templates/rails/scaffold-boundary.yaml +25 -0
  1099. templates/rails/skills/rails/SKILL.md +62 -0
  1100. templates/rails/skills/rails/references/anatomy.md +71 -0
  1101. templates/rails/stack.yaml +72 -0
  1102. templates/react-native/rules/mobile.md +26 -0
  1103. templates/react-native/scaffold/docs/engineering/accessibility-mobile.md +95 -0
  1104. templates/react-native/scaffold/docs/engineering/mobile-rules.md +56 -0
  1105. templates/react-native/scaffold/docs/engineering/offline-first.md +61 -0
  1106. templates/react-native/scaffold/docs/playbooks/mobile-app.md +49 -0
  1107. templates/react-native/scaffold/src/mobile/App.tsx +9 -0
  1108. templates/react-native/scaffold/src/mobile/eslint.config.js +17 -0
  1109. templates/react-native/scaffold/src/mobile/package.json +23 -0
  1110. templates/react-native/scaffold/src/mobile/src/greeting.test.ts +9 -0
  1111. templates/react-native/scaffold/src/mobile/src/greeting.ts +3 -0
  1112. templates/react-native/scaffold/src/mobile/tsconfig.json +17 -0
  1113. templates/react-native/scaffold/src/mobile/vitest.config.ts +10 -0
  1114. templates/react-native/scaffold-boundary.yaml +30 -0
  1115. templates/react-native/skills/react-native-mobile/SKILL.md +119 -0
  1116. templates/react-native/skills/react-native-mobile/assets/rn-mobile-checklist.md +28 -0
  1117. templates/react-native/skills/react-native-mobile/references/anatomy.md +140 -0
  1118. templates/react-native/skills/react-native-mobile/references/rn-2026-practices.md +54 -0
  1119. templates/react-native/skills/react-native-mobile/scripts/new_screen.py +73 -0
  1120. templates/react-native/skills/react-native-mobile/versions.json +16 -0
  1121. templates/react-native/skills/react-native-patterns/SKILL.md +512 -0
  1122. templates/react-native/skills/react-native-patterns/assets/rn-review-checklist.md +26 -0
  1123. templates/react-native/skills/react-native-patterns/references/anatomy.md +62 -0
  1124. templates/react-native/skills/react-native-patterns/references/list-performance.md +70 -0
  1125. templates/react-native/skills/react-native-patterns/scripts/scan_rn_perf.py +77 -0
  1126. templates/react-native/stack.yaml +70 -0
  1127. templates/ruby-plain/scaffold/src/backend/Gemfile +8 -0
  1128. templates/ruby-plain/scaffold/src/backend/main.rb +3 -0
  1129. templates/ruby-plain/scaffold-boundary.yaml +23 -0
  1130. templates/ruby-plain/stack.yaml +50 -0
  1131. templates/rust-axum/rules/backend.md +20 -0
  1132. templates/rust-axum/scaffold/docs/engineering/rust-axum-rules.md +35 -0
  1133. templates/rust-axum/scaffold/docs/playbooks/rust-axum-service.md +43 -0
  1134. templates/rust-axum/scaffold/src/backend/Cargo.toml +20 -0
  1135. templates/rust-axum/scaffold/src/backend/src/app.rs +12 -0
  1136. templates/rust-axum/scaffold/src/backend/src/error.rs +48 -0
  1137. templates/rust-axum/scaffold/src/backend/src/main.rs +24 -0
  1138. templates/rust-axum/scaffold/src/backend/src/routes/health.rs +37 -0
  1139. templates/rust-axum/scaffold/src/backend/src/routes/mod.rs +2 -0
  1140. templates/rust-axum/scaffold-boundary.yaml +25 -0
  1141. templates/rust-axum/skills/rust/SKILL.md +73 -0
  1142. templates/rust-axum/skills/rust/references/anatomy.md +63 -0
  1143. templates/rust-axum/stack.yaml +65 -0
  1144. templates/rust-plain/scaffold/src/backend/Cargo.toml +6 -0
  1145. templates/rust-plain/scaffold/src/backend/src/main.rs +3 -0
  1146. templates/rust-plain/scaffold-boundary.yaml +23 -0
  1147. templates/rust-plain/stack.yaml +51 -0
  1148. templates/spring-boot/rules/backend.md +20 -0
  1149. templates/spring-boot/scaffold/docs/engineering/spring-boot-rules.md +35 -0
  1150. templates/spring-boot/scaffold/docs/playbooks/spring-boot-service.md +45 -0
  1151. templates/spring-boot/scaffold/src/backend/mvnw +302 -0
  1152. templates/spring-boot/scaffold/src/backend/pom.xml +69 -0
  1153. templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/Application.java +16 -0
  1154. templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/common/GlobalExceptionHandler.java +31 -0
  1155. templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/health/HealthController.java +22 -0
  1156. templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/health/HealthService.java +12 -0
  1157. templates/spring-boot/scaffold/src/backend/src/main/java/com/example/app/health/HealthStatus.java +4 -0
  1158. templates/spring-boot/scaffold/src/backend/src/test/java/com/example/app/health/HealthServiceTest.java +15 -0
  1159. templates/spring-boot/scaffold-boundary.yaml +26 -0
  1160. templates/spring-boot/skills/spring-boot/SKILL.md +84 -0
  1161. templates/spring-boot/skills/spring-boot/references/anatomy.md +63 -0
  1162. templates/spring-boot/stack.yaml +65 -0
  1163. templates/svelte-sveltekit/rules/frontend.md +20 -0
  1164. templates/svelte-sveltekit/scaffold/docs/engineering/svelte-sveltekit-rules.md +37 -0
  1165. templates/svelte-sveltekit/scaffold/docs/playbooks/svelte-sveltekit-app.md +36 -0
  1166. templates/svelte-sveltekit/scaffold/src/frontend/package.json +22 -0
  1167. templates/svelte-sveltekit/scaffold/src/frontend/src/app.html +12 -0
  1168. templates/svelte-sveltekit/scaffold/src/frontend/src/hooks.server.ts +14 -0
  1169. templates/svelte-sveltekit/scaffold/src/frontend/src/lib/components/Greeting.svelte +6 -0
  1170. templates/svelte-sveltekit/scaffold/src/frontend/src/lib/stores/count.test.ts +26 -0
  1171. templates/svelte-sveltekit/scaffold/src/frontend/src/lib/stores/count.ts +4 -0
  1172. templates/svelte-sveltekit/scaffold/src/frontend/src/routes/+layout.svelte +24 -0
  1173. templates/svelte-sveltekit/scaffold/src/frontend/src/routes/+page.svelte +9 -0
  1174. templates/svelte-sveltekit/scaffold/src/frontend/src/routes/+page.ts +7 -0
  1175. templates/svelte-sveltekit/scaffold/src/frontend/src/routes/health/+server.ts +7 -0
  1176. templates/svelte-sveltekit/scaffold/src/frontend/svelte.config.js +10 -0
  1177. templates/svelte-sveltekit/scaffold/src/frontend/tsconfig.json +7 -0
  1178. templates/svelte-sveltekit/scaffold/src/frontend/vite.config.ts +7 -0
  1179. templates/svelte-sveltekit/scaffold/src/frontend/vitest.config.ts +11 -0
  1180. templates/svelte-sveltekit/scaffold-boundary.yaml +29 -0
  1181. templates/svelte-sveltekit/skills/svelte/SKILL.md +90 -0
  1182. templates/svelte-sveltekit/skills/svelte/references/anatomy.md +62 -0
  1183. templates/svelte-sveltekit/stack.yaml +70 -0
  1184. templates/typescript-plain/scaffold/src/index.ts +3 -0
  1185. templates/typescript-plain/scaffold/tsconfig.json +13 -0
  1186. templates/typescript-plain/scaffold-boundary.yaml +22 -0
  1187. templates/typescript-plain/stack.yaml +44 -0
  1188. templates/vue-nuxt/rules/frontend.md +19 -0
  1189. templates/vue-nuxt/scaffold/docs/engineering/nuxt-rules.md +30 -0
  1190. templates/vue-nuxt/scaffold/docs/playbooks/nuxt-app.md +29 -0
  1191. templates/vue-nuxt/scaffold/src/frontend/app.vue +3 -0
  1192. templates/vue-nuxt/scaffold/src/frontend/nuxt.config.ts +11 -0
  1193. templates/vue-nuxt/scaffold/src/frontend/package.json +20 -0
  1194. templates/vue-nuxt/scaffold/src/frontend/pages/index.test.ts +19 -0
  1195. templates/vue-nuxt/scaffold/src/frontend/pages/index.vue +11 -0
  1196. templates/vue-nuxt/scaffold/src/frontend/vitest.config.ts +12 -0
  1197. templates/vue-nuxt/scaffold-boundary.yaml +26 -0
  1198. templates/vue-nuxt/skills/vue-nuxt/SKILL.md +57 -0
  1199. templates/vue-nuxt/skills/vue-nuxt/references/anatomy.md +60 -0
  1200. templates/vue-nuxt/stack.yaml +61 -0
  1201. templates/wordpress/rules/backend.md +19 -0
  1202. templates/wordpress/scaffold/docs/engineering/wordpress-rules.md +28 -0
  1203. templates/wordpress/scaffold/docs/playbooks/wordpress-service.md +29 -0
  1204. templates/wordpress/scaffold/src/backend/composer.json +17 -0
  1205. templates/wordpress/scaffold/src/backend/phpcs.xml.dist +11 -0
  1206. templates/wordpress/scaffold/src/backend/phpunit.xml +10 -0
  1207. templates/wordpress/scaffold/src/backend/plugin/inc/health.php +8 -0
  1208. templates/wordpress/scaffold/src/backend/plugin/plugin.php +28 -0
  1209. templates/wordpress/scaffold/src/backend/tests/HealthStatusTest.php +15 -0
  1210. templates/wordpress/scaffold/src/backend/theme/functions.php +18 -0
  1211. templates/wordpress/scaffold/src/backend/theme/style.css +11 -0
  1212. templates/wordpress/scaffold-boundary.yaml +23 -0
  1213. templates/wordpress/skills/wordpress/SKILL.md +110 -0
  1214. templates/wordpress/skills/wordpress/assets/wp-checklist.md +28 -0
  1215. templates/wordpress/skills/wordpress/references/wp-development.md +75 -0
  1216. templates/wordpress/skills/wordpress/references/wp-security.md +68 -0
  1217. templates/wordpress/skills/wordpress/scripts/scan_wp_smells.py +91 -0
  1218. templates/wordpress/skills/wordpress/versions.json +16 -0
  1219. templates/wordpress/stack.yaml +60 -0
  1220. thinking_os/__init__.py +1 -0
  1221. thinking_os/_agent_markers.py +32 -0
  1222. thinking_os/background.py +405 -0
  1223. thinking_os/bootstrap_outcomes.py +200 -0
  1224. thinking_os/budget.py +302 -0
  1225. thinking_os/capture.py +495 -0
  1226. thinking_os/cognition.py +516 -0
  1227. thinking_os/cognition_schemas.py +517 -0
  1228. thinking_os/compress.py +192 -0
  1229. thinking_os/concepts.py +233 -0
  1230. thinking_os/dashboard.py +159 -0
  1231. thinking_os/database.py +2883 -0
  1232. thinking_os/decay.py +393 -0
  1233. thinking_os/digest.py +295 -0
  1234. thinking_os/dispatcher.py +192 -0
  1235. thinking_os/dispatcher_helpers.py +48 -0
  1236. thinking_os/dispatchers/__init__.py +3 -0
  1237. thinking_os/dispatchers/default.py +47 -0
  1238. thinking_os/distill.py +192 -0
  1239. thinking_os/doc_indexer.py +905 -0
  1240. thinking_os/embeddings.py +943 -0
  1241. thinking_os/formula_composer.py +556 -0
  1242. thinking_os/gate_marker.py +75 -0
  1243. thinking_os/graph.py +296 -0
  1244. thinking_os/graph_indexer.py +360 -0
  1245. thinking_os/health_check.py +517 -0
  1246. thinking_os/impact.py +119 -0
  1247. thinking_os/memory_gc.py +356 -0
  1248. thinking_os/migrator_embeddings.py +318 -0
  1249. thinking_os/precision.py +194 -0
  1250. thinking_os/record_outcome.py +399 -0
  1251. thinking_os/repair.py +105 -0
  1252. thinking_os/retrieval_quality.py +239 -0
  1253. thinking_os/roles_state.py +168 -0
  1254. thinking_os/sanitizer.py +320 -0
  1255. thinking_os/server.py +3160 -0
  1256. thinking_os/session_enrich.py +272 -0
  1257. thinking_os/session_observe_worker.py +111 -0
  1258. thinking_os/session_startup.py +74 -0
  1259. thinking_os/session_summary.py +245 -0
  1260. thinking_os/task_analyzer.py +462 -0
  1261. thinking_os/task_parser.py +342 -0
  1262. thinking_os/task_sync.py +73 -0
  1263. thinking_os/tools/__init__.py +6 -0
  1264. thinking_os/tools/_shared.py +947 -0
  1265. thinking_os/tools/cognition.py +1867 -0
  1266. thinking_os/tools/docs.py +770 -0
  1267. thinking_os/tools/learning.py +2078 -0
  1268. thinking_os/tools/logs.py +79 -0
  1269. thinking_os/tools/memory.py +840 -0
  1270. thinking_os/tools/metrics.py +200 -0
  1271. thinking_os/tools/retrieve.py +415 -0
  1272. thinking_os/tools/routing.py +658 -0
  1273. thinking_os/tools/tasks.py +449 -0
  1274. thinking_os/tools/trajectory.py +181 -0
  1275. thinking_os/tracing.py +235 -0
  1276. web/__init__.py +5 -0
  1277. web/_cache.py +118 -0
  1278. web/_deps.py +56 -0
  1279. web/_envelope.py +85 -0
  1280. web/_project_context.py +140 -0
  1281. web/chat_providers.py +108 -0
  1282. web/init_jobs.py +216 -0
  1283. web/routes/__init__.py +25 -0
  1284. web/routes/_bounded_read.py +76 -0
  1285. web/routes/board.py +1089 -0
  1286. web/routes/cognition.py +1838 -0
  1287. web/routes/config.py +635 -0
  1288. web/routes/graph.py +513 -0
  1289. web/routes/health.py +180 -0
  1290. web/routes/hooks.py +288 -0
  1291. web/routes/hub.py +1219 -0
  1292. web/routes/logs.py +374 -0
  1293. web/routes/metrics.py +43 -0
  1294. web/routes/observability.py +400 -0
  1295. web/routes/patterns.py +227 -0
  1296. web/routes/presence.py +609 -0
  1297. web/routes/roles.py +446 -0
  1298. web/routes/scheduled.py +261 -0
  1299. web/routes/search.py +238 -0
  1300. web/routes/sessions.py +220 -0
  1301. web/routes/settings.py +363 -0
  1302. web/routes/stream.py +547 -0
  1303. web/security.py +157 -0
  1304. web/server.py +283 -0
cli/main.py ADDED
@@ -0,0 +1,3070 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Coding OS — CLI tool for installing and managing the cognitive operating system.
4
+
5
+ Usage:
6
+ coding-os init --agent claude,codex [--template django]
7
+ coding-os add-adapter codex
8
+ coding-os health
9
+ coding-os adopt # overlay onto an existing repo
10
+ coding-os eject # remove coding-os, keep your code/docs
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import contextlib
16
+ import functools
17
+ import json
18
+ import os
19
+ import re
20
+ import shlex
21
+ import shutil
22
+ import subprocess
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ import click
27
+ import yaml
28
+
29
+ from cli._data_types import AggregatedWorld
30
+ from cli._resources import (
31
+ adapters_dir,
32
+ core_dir,
33
+ data_root,
34
+ overlay_adapter_dirs,
35
+ overlay_template_dirs,
36
+ templates_dir,
37
+ )
38
+ from cli._init_helpers import (
39
+ InitError,
40
+ ensure_agents_md,
41
+ ensure_gitignore,
42
+ install_consumer_git_hooks,
43
+ materialize_ci_workflow,
44
+ materialize_dockerfiles,
45
+ materialize_makefile_targets,
46
+ maybe_git_init,
47
+ maybe_initial_commit,
48
+ resolve_init_target,
49
+ )
50
+ from cli.adapter_registry import load_adapter_registry
51
+ from cli.add_stack import add_stack as add_stack_cmd
52
+ from cli.remove_stack import remove_stack as remove_stack_cmd
53
+ from cli.config_composer import COMPOSED_FILENAMES, compose_coding_os_configs
54
+ from cli.aggregator import aggregate, today_iso
55
+ from cli.brain_commands import (
56
+ brain_decay as brain_decay_cmd,
57
+ brain_gc as brain_gc_cmd,
58
+ brain_sweep_changelog as brain_sweep_changelog_cmd,
59
+ docs_index as docs_index_cmd,
60
+ reindex as reindex_cmd,
61
+ task_sync as task_sync_cmd,
62
+ )
63
+ from cli.core_version import stamp_core_version
64
+ from cli.doctor import doctor as doctor_cmd
65
+ from cli.materialize_file import materialize_file as materialize_file_cmd
66
+ from cli.list_adapters import list_adapters as list_adapters_cmd
67
+ from cli.list_stacks import list_stacks as list_stacks_cmd
68
+ from cli.setup import setup as setup_cmd
69
+ from cli.skills_list import skills_list as skills_list_cmd
70
+ from cli.stack_registry import (
71
+ load_base_profile,
72
+ load_stack_registry,
73
+ resolve_relocated_profiles,
74
+ service_relocations,
75
+ )
76
+ from cli.tail_command import tail_cmd
77
+ from cli.update import update as update_cmd
78
+
79
+ # CODING_OS_ROOT is the source-checkout root — kept for dev-only operations; it
80
+ # is meaningless under a wheel install. The bundled DATA trees resolve via
81
+ # importlib so they are found under both src-layout and wheel installs (TASK-219).
82
+ CODING_OS_ROOT = Path(__file__).resolve().parent.parent.parent
83
+ ADAPTERS_DIR = adapters_dir()
84
+ CORE_DIR = core_dir()
85
+ TEMPLATES_DIR = templates_dir()
86
+
87
+ CONFIG_FILE = ".coding-os.yaml"
88
+ STATE_DIR = ".coding-os"
89
+
90
+
91
+ def _discover_valid_agents() -> list[str]:
92
+ """Read adapter ids from adapters/*/adapter.yaml at CLI startup.
93
+
94
+ Deliberately separate from `_get_adapter_registry()` because click
95
+ needs a plain list at decorator evaluation time, before module
96
+ initialization has completed. Returns a conservative fallback on
97
+ any load error so the CLI stays bootable.
98
+ """
99
+ try:
100
+ return sorted(
101
+ load_adapter_registry(ADAPTERS_DIR, overlay_dirs=overlay_adapter_dirs()).keys()
102
+ )
103
+ except Exception:
104
+ return []
105
+
106
+
107
+ def _discover_valid_templates() -> list[str]:
108
+ """Read stack ids from templates/*/stack.yaml (+ community overlay) at CLI startup."""
109
+ try:
110
+ return sorted(
111
+ load_stack_registry(TEMPLATES_DIR, overlay_dirs=overlay_template_dirs()).keys()
112
+ )
113
+ except Exception:
114
+ return []
115
+
116
+
117
+ VALID_AGENTS: list[str] = _discover_valid_agents()
118
+ VALID_TEMPLATES: list[str] = _discover_valid_templates()
119
+
120
+ # Stack and adapter metadata live in templates/*/stack.yaml and
121
+ # adapters/*/adapter.yaml. Adding a new stack or adapter is a pure data-file
122
+ # change — never touch this module.
123
+ #
124
+ # The caches below memoize registry loads within a single CLI invocation so
125
+ # cos init/doctor/add-stack don't re-parse YAML repeatedly. Tests can reset
126
+ # them via _reset_registries_for_tests().
127
+
128
+ _base_cache = None
129
+ _stack_cache = None
130
+ _adapter_cache = None
131
+
132
+
133
+ def _get_base_profile():
134
+ global _base_cache
135
+ if _base_cache is None:
136
+ _base_cache = load_base_profile(TEMPLATES_DIR / "_base")
137
+ return _base_cache
138
+
139
+
140
+ def _get_stack_registry():
141
+ global _stack_cache
142
+ if _stack_cache is None:
143
+ # Consumer-discovery path: include out-of-tree community stacks
144
+ # ($COS_USER_TEMPLATES_DIR). The meta-repo SSOT regen/lint scripts load
145
+ # the registry bundled-only (no overlay) so a community stack never leaks
146
+ # into scaffold_manifest.json / dimension-registry.md (TASK-458/471).
147
+ _stack_cache = load_stack_registry(TEMPLATES_DIR, overlay_dirs=overlay_template_dirs())
148
+ return _stack_cache
149
+
150
+
151
+ def _get_adapter_registry():
152
+ global _adapter_cache
153
+ if _adapter_cache is None:
154
+ _adapter_cache = load_adapter_registry(ADAPTERS_DIR, overlay_dirs=overlay_adapter_dirs())
155
+ return _adapter_cache
156
+
157
+
158
+ def _reset_registries_for_tests() -> None:
159
+ """Clear cached registry state. Call from test fixtures that mutate
160
+ templates/ or adapters/ between invocations within a single process."""
161
+ global _base_cache, _stack_cache, _adapter_cache
162
+ _base_cache = None
163
+ _stack_cache = None
164
+ _adapter_cache = None
165
+
166
+
167
+ def _build_world(
168
+ agent: str,
169
+ templates: tuple[str, ...],
170
+ project: Path,
171
+ *,
172
+ today: str | None = None,
173
+ ) -> AggregatedWorld:
174
+ """Load base + requested stacks + adapter and aggregate into a world.
175
+
176
+ `today` is an optional ISO-8601 override for deterministic fixtures
177
+ (golden parity tests). Production callers leave it None so the
178
+ current wall-clock date is used.
179
+ """
180
+ base = _get_base_profile()
181
+ stack_registry = _get_stack_registry()
182
+ adapter_registry = _get_adapter_registry()
183
+
184
+ if agent not in adapter_registry:
185
+ raise click.ClickException(f"adapter '{agent}' not found in {ADAPTERS_DIR}")
186
+ adapter_profile = adapter_registry[agent]
187
+
188
+ for t in templates:
189
+ if t not in stack_registry:
190
+ raise click.ClickException(
191
+ f"stack '{t}' not found — available: {sorted(stack_registry.keys())}"
192
+ )
193
+ # Colliding structure.roots are relocated to src/services/<id> BEFORE
194
+ # aggregation so every derived artifact is service-scoped
195
+ # (project-anatomy.md § Glob/verify propagation, TASK-355).
196
+ stack_profiles = resolve_relocated_profiles(stack_registry, templates)
197
+
198
+ return aggregate(
199
+ base,
200
+ stack_profiles,
201
+ adapter_profile,
202
+ project.name,
203
+ today=today or today_iso(),
204
+ )
205
+
206
+
207
+ def _load_config(project_dir: Path) -> dict:
208
+ """Load .coding-os.yaml from project directory."""
209
+ config_path = project_dir / CONFIG_FILE
210
+ if config_path.exists():
211
+ with open(config_path) as f:
212
+ return yaml.safe_load(f) or {}
213
+ return {}
214
+
215
+
216
+ def _save_config(project_dir: Path, config: dict) -> None:
217
+ """Save config to .coding-os.yaml."""
218
+ config_path = project_dir / CONFIG_FILE
219
+ with open(config_path, "w") as f:
220
+ yaml.dump(config, f, default_flow_style=False, sort_keys=False)
221
+
222
+
223
+ def _detect_existing_install(path: Path) -> dict | None:
224
+ """Return install snapshot dict if `path` has a coding-os config, else None.
225
+
226
+ Used by `cos init` to pivot into idempotent sync mode when the user
227
+ accidentally re-runs init in an already-initialized project.
228
+ """
229
+ cfg = path / CONFIG_FILE
230
+ if not cfg.exists():
231
+ return None
232
+ try:
233
+ data = yaml.safe_load(cfg.read_text(encoding="utf-8")) or {}
234
+ except yaml.YAMLError:
235
+ return None
236
+ return {
237
+ "agents": list(data.get("agents") or []),
238
+ "templates": list(data.get("templates") or []),
239
+ "version": data.get("version"),
240
+ "state_dir": data.get("state_dir", STATE_DIR),
241
+ }
242
+
243
+
244
+ def _sync_missing(project: Path, *, output_format: str = "text") -> None:
245
+ """Re-link any missing adapter hooks/rules/commands/skills for a project.
246
+
247
+ Non-destructive: existing files and symlinks are left alone; only gaps
248
+ are filled. Used both by `cos init` on an already-initialized project
249
+ and (in D.3) by `cos update`.
250
+ """
251
+ config = _load_config(project) or {}
252
+ agents = config.get("agents") or []
253
+ templates = tuple(config.get("templates") or [])
254
+ added: list[str] = []
255
+
256
+ for agent in agents:
257
+ _run_adapter_install(agent, project)
258
+ if templates:
259
+ _link_stack_skills(agent, templates, project)
260
+ added.append(agent)
261
+
262
+ if output_format == "text":
263
+ click.echo(f" Synced adapters: {', '.join(added) if added else '(none)'}")
264
+ click.echo(" (idempotent — existing files untouched)")
265
+
266
+
267
+ def _prompt_templates() -> tuple[str, ...]:
268
+ """Ask the user which stack templates to apply. Returns a tuple of IDs.
269
+
270
+ Stacks render grouped by language (template-authoring.md § Language
271
+ layer): the user can answer with a stack id, a number, OR a bare
272
+ language name — the latter resolves to that language's plain stack.
273
+ """
274
+ from cli.stack_registry import group_stacks_by_language, plain_stack_by_language
275
+
276
+ registry = _get_stack_registry()
277
+ if not registry.keys():
278
+ return ()
279
+ profiles = {sid: registry[sid] for sid in registry.keys()}
280
+ groups = group_stacks_by_language(profiles)
281
+ language_to_plain = plain_stack_by_language(profiles)
282
+
283
+ available: list[str] = []
284
+ click.echo("\nAvailable stacks (a bare language name picks its plain stack):")
285
+ for language, members in groups.items():
286
+ click.echo(f" [{language}]")
287
+ for profile in members:
288
+ available.append(profile.id)
289
+ click.echo(f" {len(available)}. {profile.id:17s} — {profile.label}")
290
+ click.echo(" 0. none")
291
+ click.echo(" (ready-made compositions: cos init --preset <id> — list with `cos list-stacks`)")
292
+ raw = click.prompt(
293
+ "Select stacks (numbers, names, or a language — e.g. '1,4', 'django,nextjs', 'go')",
294
+ default="0",
295
+ show_default=False,
296
+ )
297
+ tokens = [t.strip() for t in raw.split(",") if t.strip()]
298
+ chosen: list[str] = []
299
+ for tok in tokens:
300
+ if tok == "0":
301
+ return ()
302
+ if tok.isdigit():
303
+ i = int(tok) - 1
304
+ if 0 <= i < len(available):
305
+ chosen.append(available[i])
306
+ elif tok in registry:
307
+ chosen.append(tok)
308
+ elif tok in language_to_plain:
309
+ chosen.append(language_to_plain[tok])
310
+ # Deduplicate while preserving order.
311
+ seen = set()
312
+ result: list[str] = []
313
+ for s in chosen:
314
+ if s not in seen:
315
+ seen.add(s)
316
+ result.append(s)
317
+ return tuple(result)
318
+
319
+
320
+ def _parse_agents(raw: str) -> list[str]:
321
+ """Parse and validate a comma-separated agent string (e.g. 'claude,codex').
322
+
323
+ Raises click.ClickException if any token is not a known adapter.
324
+ """
325
+ tokens = [t.strip() for t in raw.split(",") if t.strip()]
326
+ if not tokens:
327
+ raise click.ClickException("--agent value is empty")
328
+ invalid = [t for t in tokens if t not in VALID_AGENTS]
329
+ if invalid:
330
+ raise click.ClickException(
331
+ f"unknown agent(s): {', '.join(invalid)} — available: {', '.join(VALID_AGENTS)}"
332
+ )
333
+ # Deduplicate preserving order.
334
+ seen: set[str] = set()
335
+ result: list[str] = []
336
+ for t in tokens:
337
+ if t not in seen:
338
+ seen.add(t)
339
+ result.append(t)
340
+ return result
341
+
342
+
343
+ def _prompt_agents() -> list[str]:
344
+ """Interactively prompt for one or more agents (comma-separated)."""
345
+ label = ", ".join(VALID_AGENTS)
346
+ raw = click.prompt(
347
+ f"Agent(s) — comma-separated ({label})",
348
+ default=VALID_AGENTS[0] if VALID_AGENTS else None,
349
+ )
350
+ return _parse_agents(raw)
351
+
352
+
353
+ def _prompt_name_and_location(shell_cwd: Path) -> tuple[str | None, str | None]:
354
+ """Decide whether to use the current dir or create a subdir.
355
+
356
+ Returns (name, project_dir) — either may be None to fall back to the
357
+ resolver default. `project_dir` is returned as a string path to match
358
+ the CLI option type.
359
+ """
360
+ default_name = shell_cwd.name
361
+ use_current = click.confirm(
362
+ f"Use current directory ({shell_cwd})?",
363
+ default=True,
364
+ )
365
+ if use_current:
366
+ return None, str(shell_cwd)
367
+ name = click.prompt("Project name (subdirectory)", default=default_name)
368
+ return name, str(shell_cwd)
369
+
370
+
371
+ def _derive_verify_from_world(world: AggregatedWorld) -> dict[str, str]:
372
+ """Extract domain → command mapping from aggregated VERIFY_* substitutions.
373
+
374
+ Looks for keys of the form `VERIFY_<DOMAIN>` (exact — not `_GLOB`, not
375
+ `_SUITES`) and maps them to lowercased domain names. Strips surrounding
376
+ backticks from values so `.coding-os.yaml.verify` stores raw shell
377
+ commands, not display-formatted ones.
378
+ """
379
+ result: dict[str, str] = {}
380
+ for key, value in world.substitutions.items():
381
+ if not key.startswith("VERIFY_"):
382
+ continue
383
+ if key.endswith("_GLOB") or key.endswith("_SUITES"):
384
+ continue
385
+ domain = key.removeprefix("VERIFY_").lower()
386
+ cleaned = value.strip().strip("`").strip()
387
+ if not cleaned or cleaned == "(none)":
388
+ continue
389
+ result[domain] = cleaned
390
+ return result
391
+
392
+
393
+ def _link_stack_skills(
394
+ agent: str,
395
+ templates: tuple[str, ...],
396
+ project_dir: Path,
397
+ ) -> None:
398
+ """Symlink each applied stack's skills into the agent's skills_dir.
399
+
400
+ No-op for adapters whose skills_dir is null (e.g. Codex). Delegates the
401
+ filesystem work to src/core/scripts/link-stack-skills.sh so the same logic
402
+ is callable from Make targets / cos update.
403
+ """
404
+ registry = _get_adapter_registry()
405
+ if agent not in registry:
406
+ return
407
+ skills_dir = registry[agent].skills_dir
408
+ if not skills_dir:
409
+ return
410
+ linker = CORE_DIR / "scripts" / "link-stack-skills.sh"
411
+ if not linker.exists():
412
+ click.echo(f" WARN: stack-skill linker missing: {linker}", err=True)
413
+ return
414
+ agent_skills_abs = str(project_dir / skills_dir)
415
+ result = subprocess.run(
416
+ ["bash", str(linker), agent_skills_abs, str(data_root()), *templates],
417
+ capture_output=True,
418
+ text=True,
419
+ )
420
+ if result.returncode != 0:
421
+ click.echo(
422
+ f" WARN: stack-skill linking failed:\n{result.stderr}",
423
+ err=True,
424
+ )
425
+ return
426
+ linked = []
427
+ for t in templates:
428
+ stack_skills = templates_dir(t, "skills")
429
+ if stack_skills.exists():
430
+ linked.extend(sorted(p.name for p in stack_skills.iterdir() if p.is_dir()))
431
+ if linked:
432
+ click.echo(f" Linked stack skills: {', '.join(linked)}")
433
+
434
+ # Community-overlay stacks live outside the bundled tree the shell linker
435
+ # scans (data_root), so link their skills here from the resolved source_dir
436
+ # (TASK-479). Only fires for a stack whose source_dir is NOT under TEMPLATES_DIR.
437
+ stack_registry = _get_stack_registry()
438
+ bundled_root = TEMPLATES_DIR.resolve()
439
+ community: list[str] = []
440
+ for t in templates:
441
+ if t not in stack_registry:
442
+ continue
443
+ source_dir = stack_registry[t].source_dir.resolve()
444
+ try:
445
+ source_dir.relative_to(bundled_root)
446
+ continue # bundled — already linked by the shell
447
+ except ValueError:
448
+ pass # community overlay stack
449
+ skills_src = source_dir / "skills"
450
+ if not skills_src.is_dir():
451
+ continue
452
+ for skill_dir in sorted(p for p in skills_src.iterdir() if p.is_dir()):
453
+ src_md = skill_dir / "SKILL.md"
454
+ dest = project_dir / skills_dir / skill_dir.name / "SKILL.md"
455
+ if not src_md.is_file() or dest.exists():
456
+ continue
457
+ dest.parent.mkdir(parents=True, exist_ok=True)
458
+ dest.symlink_to(src_md)
459
+ community.append(skill_dir.name)
460
+ if community:
461
+ click.echo(f" Linked community stack skills: {', '.join(community)}")
462
+
463
+
464
+ def _run_adapter_install(agent: str, project_dir: Path) -> None:
465
+ """Run the adapter's declared install script.
466
+
467
+ Uses adapter.yaml::install_script so a new adapter is pure data — no
468
+ hardcoded path assumption.
469
+ """
470
+ registry = _get_adapter_registry()
471
+ if agent not in registry:
472
+ click.echo(
473
+ f" ERROR: Unknown adapter '{agent}' — available: {sorted(registry.keys())}",
474
+ err=True,
475
+ )
476
+ sys.exit(1)
477
+ install_script = registry[agent].install_script
478
+ if not install_script.exists():
479
+ click.echo(
480
+ f" ERROR: Adapter install script not found: {install_script}",
481
+ err=True,
482
+ )
483
+ sys.exit(1)
484
+
485
+ result = subprocess.run(
486
+ ["bash", str(install_script)],
487
+ cwd=str(project_dir),
488
+ capture_output=True,
489
+ text=True,
490
+ )
491
+ if result.stdout:
492
+ click.echo(result.stdout)
493
+ if result.returncode != 0:
494
+ click.echo(f" ERROR: Adapter install failed:\n{result.stderr}", err=True)
495
+ sys.exit(1)
496
+
497
+
498
+ def _apply_template(
499
+ template_name: str,
500
+ project_dir: Path,
501
+ agent: str | None = None,
502
+ ) -> None:
503
+ """Apply a stack template to the project.
504
+
505
+ 1. Copies `src/templates/<name>/{rules,skills,playbooks,hooks}/` to
506
+ `<project>/.coding-os/templates/<name>/…` for browsing.
507
+ 2. If `agent` is provided and that adapter supports path-scoped rules
508
+ (`adapter.rules_dir` != null), also copies every
509
+ `src/templates/<name>/rules/*.md` into the adapter's rules dir with a
510
+ `<stack>-<filename>` prefix so multiple stacks coexist.
511
+ """
512
+ stack_registry = _get_stack_registry()
513
+ if template_name not in stack_registry:
514
+ click.echo(
515
+ f" WARN: Template '{template_name}' not in registry "
516
+ f"(available: {sorted(stack_registry.keys())})",
517
+ err=True,
518
+ )
519
+ return
520
+ stack_profile = stack_registry[template_name]
521
+ template_dir = stack_profile.source_dir
522
+
523
+ # 1. Mirror every subdir into .coding-os/templates/<name>/
524
+ for subdir in ("rules", "skills", "playbooks", "hooks"):
525
+ src = template_dir / subdir
526
+ if src.exists():
527
+ dest = project_dir / STATE_DIR / "src" / "templates" / template_name / subdir
528
+ dest.mkdir(parents=True, exist_ok=True)
529
+ for item in src.iterdir():
530
+ if item.is_file():
531
+ shutil.copy2(item, dest / item.name)
532
+ elif item.is_dir():
533
+ shutil.copytree(item, dest / item.name, dirs_exist_ok=True)
534
+
535
+ # 2. Copy path-scoped rules into the adapter's rules_dir (if supported).
536
+ if agent is not None:
537
+ adapters = _get_adapter_registry()
538
+ if agent in adapters:
539
+ adapter_profile = adapters[agent]
540
+ if adapter_profile.supports_rules and adapter_profile.rules_dir:
541
+ rules_src = template_dir / "rules"
542
+ if rules_src.exists():
543
+ rules_dest = project_dir / adapter_profile.rules_dir
544
+ rules_dest.mkdir(parents=True, exist_ok=True)
545
+ for rule_file in sorted(rules_src.glob("*.md")):
546
+ # Prefix with stack id to avoid collisions between stacks.
547
+ out = rules_dest / f"{template_name}-{rule_file.name}"
548
+ shutil.copy2(rule_file, out)
549
+ elif not adapter_profile.supports_rules:
550
+ click.echo(
551
+ f" INFO: adapter '{agent}' does not support path-scoped "
552
+ f"rules — skipping rules copy for '{template_name}'",
553
+ )
554
+
555
+ click.echo(f" Template '{template_name}' applied.")
556
+
557
+
558
+ # _merge_profiles, _build_substitutions, _list_installed_skills have all
559
+ # been replaced by the aggregator pipeline. See _build_world() above and
560
+ # src/cli/aggregator.py::aggregate() for the data-driven replacement.
561
+
562
+
563
+ def _resolve_placeholders(text: str, substitutions: dict[str, str]) -> str:
564
+ """Replace `{{KEY}}` placeholders. Unknown keys are left intact for later overlay."""
565
+ result = text
566
+ for key, value in substitutions.items():
567
+ result = result.replace(f"{{{{{key}}}}}", value)
568
+ return result
569
+
570
+
571
+ # Tag-driven docs composition (TASK-360):
572
+ # - file-level: a `module:<id>` token in the first-line header comment skips
573
+ # the whole doc when that module is disabled;
574
+ # - block-level: `<!-- if-stack:a,b -->` / `<!-- if-module:docs -->` ...
575
+ # `<!-- end-if -->` keep the block only when ANY listed stack is installed /
576
+ # the module is enabled. Markers and tags are stripped from the copy, so a
577
+ # fully-default project's output is byte-identical to untagged sources.
578
+ _DOC_MODULE_TAG_RE = re.compile(r"\s*\|\s*module:([a-z][a-z0-9_-]*)")
579
+ _DOC_IF_RE = re.compile(r"^<!--\s*if-(stack|module):([a-z0-9_,-]+)\s*-->\s*$")
580
+ _DOC_ENDIF_RE = re.compile(r"^<!--\s*end-if\s*-->\s*$")
581
+
582
+
583
+ def _apply_doc_conditions(
584
+ text: str, disabled_modules: set[str], active_stacks: set[str]
585
+ ) -> tuple[bool, str]:
586
+ """(skip_file, transformed_text) — see the marker contract above."""
587
+ lines = text.split("\n")
588
+ if lines:
589
+ tag = _DOC_MODULE_TAG_RE.search(lines[0])
590
+ if tag and lines[0].lstrip().startswith("<!--"):
591
+ if tag.group(1) in disabled_modules:
592
+ return True, ""
593
+ lines[0] = _DOC_MODULE_TAG_RE.sub("", lines[0], count=1)
594
+
595
+ out: list[str] = []
596
+ keeping = True
597
+ in_block = False
598
+ for line in lines:
599
+ opener = _DOC_IF_RE.match(line)
600
+ if opener and not in_block:
601
+ in_block = True
602
+ kind, raw_ids = opener.group(1), opener.group(2)
603
+ wanted = {x for x in raw_ids.split(",") if x}
604
+ if kind == "stack":
605
+ keeping = bool(wanted & active_stacks)
606
+ else:
607
+ keeping = not (wanted & disabled_modules)
608
+ continue
609
+ if _DOC_ENDIF_RE.match(line) and in_block:
610
+ in_block = False
611
+ keeping = True
612
+ continue
613
+ if keeping:
614
+ out.append(line)
615
+ return False, "\n".join(out)
616
+
617
+
618
+ def module_scaffold_doc_rels(templates: tuple[str, ...], module_id: str) -> list[str]:
619
+ """Consumer-relative paths of scaffold `.md` docs tagged `| module:<module_id>`.
620
+
621
+ Reuses the source-root + relocation mapping of the scaffold overlay so the
622
+ result matches what init composed. Shared by the toggle doc-sync (prune /
623
+ restore, TASK-813) and cos doctor's modules.doc_drift backstop — the consumer
624
+ copy has its tag STRIPPED at init, so drift/prune must map via the tagged
625
+ SOURCE, never the untagged destination."""
626
+ registry = _get_stack_registry()
627
+ relocations = _service_relocations(templates)
628
+ sources: list[tuple[Path, str | None]] = [(TEMPLATES_DIR / "_base" / "scaffold", None)]
629
+ for name in templates:
630
+ stack_root = registry[name].source_dir if name in registry.keys() else TEMPLATES_DIR / name
631
+ candidate = stack_root / "scaffold"
632
+ if candidate.exists():
633
+ sources.append((candidate, name))
634
+ rels: set[str] = set()
635
+ for src_root, stack_id in sources:
636
+ if not src_root.exists():
637
+ continue
638
+ relocated_root = relocations.get(stack_id) if stack_id else None
639
+ declared_root = (
640
+ (registry[stack_id].structure or {}).get("root", "").rstrip("/")
641
+ if stack_id and stack_id in registry.keys()
642
+ else ""
643
+ )
644
+ for src_file in src_root.rglob("*.md"):
645
+ if not src_file.is_file():
646
+ continue
647
+ try:
648
+ first = src_file.read_text(encoding="utf-8").split("\n", 1)[0]
649
+ except OSError:
650
+ continue
651
+ if not first.lstrip().startswith("<!--"):
652
+ continue
653
+ tag = _DOC_MODULE_TAG_RE.search(first)
654
+ if not (tag and tag.group(1) == module_id):
655
+ continue
656
+ rel = src_file.relative_to(src_root)
657
+ if relocated_root and declared_root and str(rel).startswith(declared_root + "/"):
658
+ rel = Path(relocated_root) / str(rel)[len(declared_root) + 1 :]
659
+ rels.add(str(rel))
660
+ return sorted(rels)
661
+
662
+
663
+ def _dry_config_preview(templates: tuple[str, ...], output_format: str) -> None:
664
+ """`cos init --dry-config` — merged .coding-os preview, zero writes."""
665
+ from cli.config_composer import preview_coding_os_configs
666
+
667
+ merged, conflicts = preview_coding_os_configs(list(templates), templates_dir=TEMPLATES_DIR)
668
+ if output_format == "json":
669
+ click.echo(
670
+ json.dumps(
671
+ {"stacks": list(templates), "configs": merged, "conflicts": conflicts},
672
+ indent=2,
673
+ ensure_ascii=False,
674
+ )
675
+ )
676
+ return
677
+ scrumban = merged.get("scrumban-config.yaml") or {}
678
+ lanes = [lane.get("id") for lane in scrumban.get("swimlanes") or [] if isinstance(lane, dict)]
679
+ click.echo(f"Merge preview for stacks: {', '.join(templates) or '(base only)'}")
680
+ click.echo(f" swimlanes: {', '.join(lanes) or '(none)'}")
681
+ for filename in merged:
682
+ click.echo(f" composed: {filename}")
683
+ if conflicts:
684
+ click.echo(f" conflicts ({len(conflicts)} — later wins):")
685
+ for line in conflicts:
686
+ click.echo(f" WARN: {line}")
687
+ else:
688
+ click.echo(" conflicts: none")
689
+ click.echo("(dry-config — nothing written)")
690
+
691
+
692
+ def _scaffold_tree_preview(
693
+ templates: tuple[str, ...], disabled_modules: tuple[str, ...] = ()
694
+ ) -> tuple[list[str], list[str]]:
695
+ """Relative paths `cos init` WOULD create — zero reads of the target, zero writes.
696
+
697
+ Returns (sorted paths, config-merge conflicts). Mirrors the source roots,
698
+ service-relocation logic AND the `<!-- module:X -->` doc-skip of
699
+ `_overlay_scaffold` / `_run_scaffold_phase` so the preview matches what an
700
+ actual init writes (audit INIT-4). disabled_modules is taken LITERALLY from the
701
+ validated `--disable-module` flags; the real init additionally resolves
702
+ dependency-refusal (e.g. `docs` stays enabled while `tasks` depends on it) and
703
+ preset-declared disables at scaffold time, so for those two cases the preview
704
+ is a best-effort upper bound on what gets dropped, not byte-exact (pass-3).
705
+ """
706
+ from cli.config_composer import preview_coding_os_configs
707
+
708
+ relocations = _service_relocations(templates)
709
+ registry = _get_stack_registry()
710
+ disabled_set = {m.strip() for m in disabled_modules if m.strip()}
711
+ active_set = set(templates)
712
+ paths: set[str] = set()
713
+
714
+ sources: list[tuple[Path, str | None]] = [(TEMPLATES_DIR / "_base" / "scaffold", None)]
715
+ for name in templates:
716
+ # Community stacks resolve from source_dir, not the bundled tree (TASK-479).
717
+ stack_root = registry[name].source_dir if name in registry.keys() else TEMPLATES_DIR / name
718
+ candidate = stack_root / "scaffold"
719
+ if candidate.exists():
720
+ sources.append((candidate, name))
721
+
722
+ for src_root, stack_id in sources:
723
+ if not src_root.exists():
724
+ continue
725
+ relocated_root = relocations.get(stack_id) if stack_id else None
726
+ declared_root = (
727
+ (registry[stack_id].structure or {}).get("root", "").rstrip("/")
728
+ if stack_id and stack_id in registry.keys()
729
+ else ""
730
+ )
731
+ for src_file in src_root.rglob("*"):
732
+ if not src_file.is_file() or src_file.name == ".gitkeep":
733
+ continue
734
+ rel = src_file.relative_to(src_root)
735
+ # A module-tagged doc the actual --disable-module init would drop
736
+ # must not appear in the preview (INIT-4: preview == actual). Only
737
+ # .md files carry the marker, mirroring the overlay's own scope.
738
+ if disabled_set and src_file.suffix == ".md":
739
+ try:
740
+ skip_file, _ = _apply_doc_conditions(
741
+ src_file.read_text(encoding="utf-8"), disabled_set, active_set
742
+ )
743
+ if skip_file:
744
+ continue
745
+ except OSError as exc:
746
+ logging.getLogger(__name__).debug(
747
+ "doc-condition preview check skipped for %s: %s", src_file, exc
748
+ )
749
+ if relocated_root and declared_root and str(rel).startswith(declared_root + "/"):
750
+ rel = Path(relocated_root) / str(rel)[len(declared_root) + 1 :]
751
+ # Composed configs come from the merge step below, not the overlay.
752
+ if rel.parent.name == ".coding-os" and rel.name in COMPOSED_FILENAMES:
753
+ continue
754
+ paths.add(str(rel))
755
+
756
+ merged, conflicts = preview_coding_os_configs(list(templates), templates_dir=TEMPLATES_DIR)
757
+ for filename in merged:
758
+ paths.add(f"{STATE_DIR}/{filename}")
759
+
760
+ # Generated artifacts the scaffold phase always writes (not under scaffold/).
761
+ paths.update(
762
+ {
763
+ CONFIG_FILE,
764
+ "AGENTS.md",
765
+ "Makefile",
766
+ f"{STATE_DIR}/coding-os.db",
767
+ f"{STATE_DIR}/Makefile.base",
768
+ }
769
+ )
770
+ return sorted(paths), conflicts
771
+
772
+
773
+ def _dry_run_preview(
774
+ templates: tuple[str, ...], output_format: str, disabled_modules: tuple[str, ...] = ()
775
+ ) -> None:
776
+ """`cos init --dry-run` — preview the scaffold tree with ZERO writes."""
777
+ paths, conflicts = _scaffold_tree_preview(templates, disabled_modules)
778
+ if output_format == "json":
779
+ click.echo(
780
+ json.dumps(
781
+ {
782
+ "stacks": list(templates),
783
+ "disabled_modules": [m for m in disabled_modules if m],
784
+ "files": paths,
785
+ "conflicts": conflicts,
786
+ "note": "the .claude/ agent surface (hooks/skills/commands/rules) is installed by the adapter and NOT previewed here",
787
+ },
788
+ indent=2,
789
+ ensure_ascii=False,
790
+ )
791
+ )
792
+ return
793
+ click.echo(f"Scaffold preview for stacks: {', '.join(templates) or '(base only)'}")
794
+ click.echo(f" {len(paths)} file(s) would be created:")
795
+ for path in paths:
796
+ click.echo(f" {path}")
797
+ if conflicts:
798
+ click.echo(f" config conflicts ({len(conflicts)} — later wins):")
799
+ for line in conflicts:
800
+ click.echo(f" WARN: {line}")
801
+ click.echo(
802
+ " note: the .claude/ agent surface (hooks · skills · commands · rules) is "
803
+ "installed by the adapter and NOT previewed here."
804
+ )
805
+ click.echo("(dry-run — nothing written)")
806
+
807
+
808
+ def _registered_slug(project: Path) -> str:
809
+ # The hub slug the Composer navigates by; "" under --no-register or a failed
810
+ # registry write, both of which are non-fatal for init itself.
811
+ import logging
812
+
813
+ try:
814
+ from cli.registry import load_registry
815
+
816
+ # add_project matches on the unresolved path, so match both forms —
817
+ # resolving only would miss a symlinked entry it had just written.
818
+ wanted = {str(project), str(project.resolve())}
819
+ for entry in load_registry().projects:
820
+ stored = Path(entry.path)
821
+ if str(stored) in wanted or str(stored.resolve()) in wanted:
822
+ return entry.slug
823
+ except Exception as exc:
824
+ logging.getLogger(__name__).debug("slug lookup skipped: %s", exc)
825
+ return ""
826
+
827
+
828
+ @functools.lru_cache(maxsize=1)
829
+ def _subsystem_help_lists() -> tuple[str, str]:
830
+ # Rule 11 — the ids come from subsystems.yaml, never a literal that rots as
831
+ # modules are added (hidden ones are not user-selectable, so they stay out).
832
+ # Cached: these run at decoration time on every `cos` invocation, and both
833
+ # init and adopt declare the flags, so an uncached read costs 4 yaml loads.
834
+ fallback = "see src/core/subsystems.yaml"
835
+ try:
836
+ from cli.subsystems import load_profiles, load_subsystems
837
+
838
+ ids = [m.id for m in load_subsystems().values() if not m.kernel and not m.hidden]
839
+ profiles, default_profile = load_profiles()
840
+ except Exception:
841
+ return fallback, fallback
842
+ return (
843
+ ", ".join(ids) or fallback,
844
+ ", ".join(f"{n} (default)" if n == default_profile else n for n in profiles) or fallback,
845
+ )
846
+
847
+
848
+ def _module_flag_help() -> str:
849
+ return (
850
+ f"Subsystem module to disable at create (repeatable): {_subsystem_help_lists()[0]}. "
851
+ "kernel can't be disabled; modules that depend on it are disabled with it. "
852
+ "Wizard parity with the Composer module toggles."
853
+ )
854
+
855
+
856
+ def _profile_flag_help() -> str:
857
+ return (
858
+ f"Module profile curating the agent's MCP tool surface: {_subsystem_help_lists()[1]}. "
859
+ "UNIONED with --disable-module (a profile can only remove modules, never "
860
+ "re-add one — use --enable-module to keep one on); omit to use the "
861
+ "registry default."
862
+ )
863
+
864
+
865
+ def _enable_flag_help() -> str:
866
+ return (
867
+ "Force-enable a module after the profile union (repeatable) — the escape "
868
+ "from profile+--disable-module union semantics, which can only remove. "
869
+ "Pulls the module's depends_on chain in with it; combining with "
870
+ "--disable-module of the same id is an error."
871
+ )
872
+
873
+
874
+ def _validated_disabled_modules(disable_module: tuple[str, ...]) -> list[str]:
875
+ # Validate --disable-module up-front so BOTH the dry-run preview and the real
876
+ # init reject the same ids (pass-3 review: dry-run returned before validation,
877
+ # so a typo'd module gave a false all-clear). kernel ids are non-disableable.
878
+ if not disable_module:
879
+ return []
880
+ from cli.subsystems import close_over_dependents, load_subsystems
881
+
882
+ registry_modules = load_subsystems()
883
+ disabled = list(dict.fromkeys(m.strip() for m in disable_module if m.strip()))
884
+ unknown = [m for m in disabled if m not in registry_modules]
885
+ if unknown:
886
+ click.echo(
887
+ f"ERROR: unknown module(s) {unknown} — available: {sorted(registry_modules)}.",
888
+ err=True,
889
+ )
890
+ sys.exit(2)
891
+ kernel = [m for m in disabled if registry_modules[m].kernel]
892
+ if kernel:
893
+ click.echo(f"ERROR: module(s) {kernel} are kernel and cannot be disabled.", err=True)
894
+ sys.exit(2)
895
+ closed = close_over_dependents(disabled, registry_modules)
896
+ added = [m for m in closed if m not in disabled]
897
+ if added:
898
+ # stderr: this runs before the json-mode stdout redirect, and a progress
899
+ # line on stdout makes `cos init --format json | jq` a parse error.
900
+ click.echo(f" Also disabling dependent module(s): {', '.join(sorted(added))}", err=True)
901
+ return closed
902
+
903
+
904
+ def _apply_enable_modules(
905
+ disabled: list[str],
906
+ enable_module: tuple[str, ...],
907
+ explicit_disable: tuple[str, ...],
908
+ ) -> list[str]:
909
+ # The escape from union semantics: --enable-module wins over a profile's
910
+ # disable, but contradicting an explicit --disable-module is an error, not
911
+ # a merge. Dependencies come along so the final set stays closed.
912
+ if not enable_module:
913
+ return disabled
914
+ from cli.subsystems import load_subsystems
915
+
916
+ registry_modules = load_subsystems()
917
+ enabled = list(dict.fromkeys(m.strip() for m in enable_module if m.strip()))
918
+ unknown = [m for m in enabled if m not in registry_modules]
919
+ if unknown:
920
+ click.echo(
921
+ f"ERROR: unknown module(s) {unknown} — available: {sorted(registry_modules)}.",
922
+ err=True,
923
+ )
924
+ sys.exit(2)
925
+ conflict = sorted(set(enabled) & {m.strip() for m in explicit_disable if m.strip()})
926
+ if conflict:
927
+ click.echo(
928
+ f"ERROR: module(s) {conflict} passed to both --enable-module and --disable-module.",
929
+ err=True,
930
+ )
931
+ sys.exit(2)
932
+ keep: set[str] = set()
933
+ frontier = list(enabled)
934
+ while frontier:
935
+ module_id = frontier.pop()
936
+ if module_id in keep:
937
+ continue
938
+ keep.add(module_id)
939
+ frontier.extend(d for d in registry_modules[module_id].depends_on if d in registry_modules)
940
+ re_enabled = sorted(set(disabled) & keep)
941
+ if re_enabled:
942
+ click.echo(f" Re-enabling module(s): {', '.join(re_enabled)}", err=True)
943
+ return [m for m in disabled if m not in keep]
944
+
945
+
946
+ def _service_relocations(templates: tuple[str, ...]) -> dict[str, str]:
947
+ """stack-id → relocated root for stacks whose structure.root collides.
948
+
949
+ Thin wrapper over the SSOT in cli.stack_registry (shared with
950
+ cli.update._aggregate_world); see project-anatomy.md.
951
+ """
952
+ return service_relocations(_get_stack_registry(), templates)
953
+
954
+
955
+ def _overlay_scaffold(
956
+ project: Path,
957
+ templates: tuple[str, ...],
958
+ substitutions: dict[str, str],
959
+ ) -> int:
960
+ """Copy `_base/scaffold/` then each template's `scaffold/` into `project/`.
961
+
962
+ Existing project files are NEVER overwritten (idempotent init).
963
+ Markdown files have their `{{KEY}}` placeholders resolved.
964
+
965
+ Returns: count of files copied.
966
+ """
967
+ # Source roots in overlay order: _base first, then each template overlay.
968
+ # Each entry: (scaffold dir, owning stack id or None for _base). A community
969
+ # stack's scaffold lives at its resolved source_dir, not the bundled tree (TASK-479).
970
+ registry = _get_stack_registry()
971
+ sources: list[tuple[Path, str | None]] = [(TEMPLATES_DIR / "_base" / "scaffold", None)]
972
+ for name in templates:
973
+ stack_root = registry[name].source_dir if name in registry.keys() else TEMPLATES_DIR / name
974
+ candidate = stack_root / "scaffold"
975
+ if candidate.exists():
976
+ sources.append((candidate, name))
977
+
978
+ # Per-language toolchain config (ruff/pytest, eslint/prettier/vitest) lives once
979
+ # under _base/lang/<language>/, selected by each active stack's declared language.
980
+ # Overlaid LAST so a stack's own scaffold config wins the idempotent first-write.
981
+ seen_languages: set[str] = set()
982
+ for name in templates:
983
+ language = registry[name].language if name in registry.keys() else ""
984
+ if not language or language in seen_languages:
985
+ continue
986
+ seen_languages.add(language)
987
+ lang_dir = TEMPLATES_DIR / "_base" / "lang" / language
988
+ if lang_dir.exists():
989
+ sources.append((lang_dir, None))
990
+
991
+ relocations = _service_relocations(templates)
992
+
993
+ from cli.subsystems import module_state
994
+
995
+ disabled_modules = {
996
+ module_id for module_id, enabled in module_state(project).items() if not enabled
997
+ }
998
+ active_stacks = set(templates)
999
+
1000
+ copied = 0
1001
+ for src_root, stack_id in sources:
1002
+ if not src_root.exists():
1003
+ continue
1004
+ relocated_root = relocations.get(stack_id) if stack_id else None
1005
+ declared_root = (
1006
+ (registry[stack_id].structure or {}).get("root", "").rstrip("/")
1007
+ if stack_id and stack_id in registry.keys()
1008
+ else ""
1009
+ )
1010
+ for src_file in src_root.rglob("*"):
1011
+ if not src_file.is_file():
1012
+ continue
1013
+ if src_file.name == ".gitkeep":
1014
+ # .gitkeep just ensures the parent dir exists in the copy —
1015
+ # honoring service relocation like any other scaffold path.
1016
+ rel = src_file.relative_to(src_root)
1017
+ if relocated_root and declared_root and str(rel).startswith(declared_root + "/"):
1018
+ rel = Path(relocated_root) / str(rel)[len(declared_root) + 1 :]
1019
+ dest = project / rel
1020
+ dest.parent.mkdir(parents=True, exist_ok=True)
1021
+ continue
1022
+
1023
+ rel = src_file.relative_to(src_root)
1024
+ if relocated_root and declared_root and str(rel).startswith(declared_root + "/"):
1025
+ rel = Path(relocated_root) / str(rel)[len(declared_root) + 1 :]
1026
+ if rel.parent.name == ".coding-os" and rel.name in COMPOSED_FILENAMES:
1027
+ # These are deep-merged from base + every stack by
1028
+ # compose_coding_os_configs — overlaying base first would
1029
+ # shadow the merge (first-writer-wins). See config-composition.md.
1030
+ continue
1031
+ dest = project / rel
1032
+ if dest.exists():
1033
+ # Idempotent: never overwrite existing project files.
1034
+ continue
1035
+
1036
+ dest.parent.mkdir(parents=True, exist_ok=True)
1037
+ try:
1038
+ content = src_file.read_text(encoding="utf-8")
1039
+ except (UnicodeDecodeError, ValueError):
1040
+ # Binary asset (image, font, ...) — copy verbatim, never substitute.
1041
+ shutil.copy2(src_file, dest)
1042
+ copied += 1
1043
+ continue
1044
+ content = _resolve_placeholders(content, substitutions)
1045
+ if src_file.suffix == ".md":
1046
+ skip_file, content = _apply_doc_conditions(content, disabled_modules, active_stacks)
1047
+ if skip_file:
1048
+ continue
1049
+ dest.write_text(content, encoding="utf-8")
1050
+ shutil.copymode(src_file, dest)
1051
+ copied += 1
1052
+
1053
+ return copied
1054
+
1055
+
1056
+ def _aggregate_scaffold_boundaries(
1057
+ project: Path,
1058
+ state: Path,
1059
+ templates: list[str],
1060
+ ) -> None:
1061
+ """Merge per-stack `scaffold-boundary.yaml` files into the consumer."""
1062
+ import yaml
1063
+
1064
+ stacks_data: list[dict] = []
1065
+ for stack_id in templates:
1066
+ boundary_src = TEMPLATES_DIR / stack_id / "scaffold-boundary.yaml"
1067
+ if not boundary_src.exists():
1068
+ continue
1069
+ try:
1070
+ data = yaml.safe_load(boundary_src.read_text(encoding="utf-8"))
1071
+ except yaml.YAMLError as exc:
1072
+ raise click.ClickException(
1073
+ f"src/templates/{stack_id}/scaffold-boundary.yaml is not valid YAML: {exc}"
1074
+ )
1075
+ if not isinstance(data, dict):
1076
+ continue
1077
+ stacks_data.append(
1078
+ {
1079
+ "stack": data.get("stack") or stack_id,
1080
+ "roots": list(data.get("roots") or []),
1081
+ "file_patterns": list(data.get("file_patterns") or []),
1082
+ "imports_from": list(data.get("imports_from") or []),
1083
+ "forbids_writing_in": list(data.get("forbids_writing_in") or []),
1084
+ }
1085
+ )
1086
+
1087
+ target = state / "scaffold-boundary.yaml"
1088
+ if not stacks_data:
1089
+ if target.exists():
1090
+ target.unlink()
1091
+ return
1092
+
1093
+ # Multi-backend relocation (project-anatomy.md): colliding declared roots
1094
+ # move each stack's boundary to src/services/<stack-id>/ BEFORE the
1095
+ # shared-root invariant — composed backends coexist by design.
1096
+ relocations = _service_relocations(tuple(templates))
1097
+ if relocations:
1098
+ registry = _get_stack_registry()
1099
+ for entry in stacks_data:
1100
+ new_root = relocations.get(entry["stack"])
1101
+ if not new_root or entry["stack"] not in registry.keys():
1102
+ continue
1103
+ declared = (registry[entry["stack"]].structure or {}).get("root", "").rstrip("/")
1104
+ if not declared:
1105
+ continue
1106
+
1107
+ def _remap(path: str) -> str:
1108
+ stripped = path.rstrip("/")
1109
+ if stripped == declared or stripped.startswith(declared + "/"):
1110
+ remapped = new_root + stripped[len(declared) :]
1111
+ return remapped + "/" if path.endswith("/") else remapped
1112
+ return path
1113
+
1114
+ entry["roots"] = [_remap(r) for r in entry["roots"]]
1115
+ entry["file_patterns"] = [_remap(p) for p in entry["file_patterns"]]
1116
+
1117
+ # Cross-service walls: each relocated root becomes forbidden to every
1118
+ # OTHER stack, so an unowned write into a sibling service is flagged
1119
+ # (project-anatomy.md § Glob/verify propagation — parameterized, never
1120
+ # hand-listed in any stack's scaffold-boundary.yaml).
1121
+ for entry in stacks_data:
1122
+ for other_id, other_root in relocations.items():
1123
+ wall = other_root.rstrip("/") + "/"
1124
+ if other_id != entry["stack"] and wall not in entry["forbids_writing_in"]:
1125
+ entry["forbids_writing_in"].append(wall)
1126
+
1127
+ # Invariant 1: no two installed stacks may share a root.
1128
+ seen: dict[str, str] = {}
1129
+ for entry in stacks_data:
1130
+ for root in entry["roots"]:
1131
+ existing = seen.get(root)
1132
+ if existing and existing != entry["stack"]:
1133
+ raise click.ClickException(
1134
+ f"scaffold-boundary aggregation: root '{root}' claimed by "
1135
+ f"both '{existing}' and '{entry['stack']}'. Two installed "
1136
+ f"stacks may not share a root — pick one per project."
1137
+ )
1138
+ seen[root] = entry["stack"]
1139
+
1140
+ # Invariant 2: every forbid references an installed root OR `shared/`.
1141
+ all_roots = {root.rstrip("/") for root in seen}
1142
+ all_roots.add("shared")
1143
+ for entry in stacks_data:
1144
+ for forbidden in entry["forbids_writing_in"]:
1145
+ stripped = forbidden.rstrip("/")
1146
+ if stripped not in all_roots:
1147
+ # Soft: mention but do not fail — a stack may legitimately
1148
+ # forbid a subtree no installed stack owns yet.
1149
+ click.echo(
1150
+ f" WARN: stack '{entry['stack']}' forbids writes in "
1151
+ f"'{forbidden}', but no installed stack owns that root.",
1152
+ err=True,
1153
+ )
1154
+
1155
+ aggregated = {
1156
+ "version": 1,
1157
+ "generated_by": "src/cli/_aggregate_scaffold_boundaries",
1158
+ "stacks": stacks_data,
1159
+ }
1160
+ target.write_text(
1161
+ yaml.safe_dump(aggregated, sort_keys=False, default_flow_style=False),
1162
+ encoding="utf-8",
1163
+ )
1164
+ click.echo(
1165
+ f" Aggregated scaffold-boundary for {len(stacks_data)} stack(s) → {target.relative_to(project)}"
1166
+ )
1167
+
1168
+
1169
+ def _copy_workflow_docs(project: Path) -> None:
1170
+ """Copy thinking_os-final-edition.md from src/core/docs/ into project workflow/.
1171
+
1172
+ The full thinking_os reference is too large (57KB, 1439 lines) to duplicate
1173
+ in the scaffold dir. Instead, we copy it from src/core/docs/ at init time.
1174
+ """
1175
+ src = CORE_DIR / "docs" / "thinking_os-final-edition.md"
1176
+ if not src.exists():
1177
+ return
1178
+ dest = project / "docs" / "workflow" / "thinking_os-final-edition.md"
1179
+ if dest.exists():
1180
+ return
1181
+ dest.parent.mkdir(parents=True, exist_ok=True)
1182
+ shutil.copy2(src, dest)
1183
+
1184
+
1185
+ def _bootstrap_hub_dir_if_first_run() -> None:
1186
+ """Seed ~/.coding-os/ the very first time the CLI is invoked."""
1187
+ import os as _os
1188
+
1189
+ override = _os.environ.get("COS_REGISTRY_PATH")
1190
+ hub_dir = Path(override).parent if override else Path.home() / ".coding-os"
1191
+ try:
1192
+ if not hub_dir.exists():
1193
+ hub_dir.mkdir(parents=True, exist_ok=True)
1194
+ registry_file = hub_dir / "registry.json"
1195
+ if not registry_file.exists():
1196
+ # Same shape save_registry() writes — keep in sync with
1197
+ # cli.registry.Registry.to_dict().
1198
+ registry_file.write_text(
1199
+ '{\n "version": 1,\n "projects": []\n}\n',
1200
+ encoding="utf-8",
1201
+ )
1202
+ except OSError as exc:
1203
+ import logging as _logging
1204
+
1205
+ _logging.getLogger("cli.main").debug("hub-dir bootstrap skipped: %s", exc)
1206
+
1207
+
1208
+ from importlib.metadata import (
1209
+ PackageNotFoundError as _PackageNotFoundError,
1210
+ version as _pkg_version,
1211
+ )
1212
+
1213
+
1214
+ def _resolve_cli_version() -> str:
1215
+ try:
1216
+ return _pkg_version("coding-os")
1217
+ except _PackageNotFoundError:
1218
+ return "unknown"
1219
+
1220
+
1221
+ def _warn_dangling_agent_links() -> None:
1222
+ """One stderr nudge when the cwd project's agent symlinks dangle (moved meta-repo).
1223
+
1224
+ Fail-open by contract: the probe must never break or slow a command —
1225
+ hub-architecture.md § Symlink health is the spec for this passive layer.
1226
+ """
1227
+ try:
1228
+ project = Path.cwd()
1229
+ if not (project / CONFIG_FILE).exists():
1230
+ return
1231
+ from cli.sync_all import _dangling, _iter_symlinks
1232
+
1233
+ for link in _iter_symlinks(project):
1234
+ if _dangling(link):
1235
+ click.echo(
1236
+ "WARN: dangling coding-os symlinks detected (meta-repo moved or removed?) "
1237
+ "— run: cos sync-doctor --repair",
1238
+ err=True,
1239
+ )
1240
+ return
1241
+ except Exception as exc:
1242
+ import logging
1243
+
1244
+ logging.getLogger(__name__).debug("dangling-link probe skipped: %s", exc)
1245
+
1246
+
1247
+ @click.group()
1248
+ @click.version_option(version=_resolve_cli_version(), prog_name="coding-os")
1249
+ def cli() -> None:
1250
+ """Coding OS — the cognitive operating system that gives AI agents memory, structure, and discipline."""
1251
+ _warn_dangling_agent_links()
1252
+ # Route every stdlib logger.error from doctor /
1253
+ # health / any cos command into logging_os so the CLI process is no longer
1254
+ # blind to its own failures. Idempotent — install_bridge() removes a prior
1255
+ # bridge handler before adding. See docs/engineering/observability-eye.md §1.
1256
+ try:
1257
+ from core.logging_os import setup as _logging_os_setup
1258
+
1259
+ _logging_os_setup(level="info")
1260
+ except Exception as _bridge_exc: # pragma: no cover — never block the CLI on logging setup
1261
+ import logging as _logging
1262
+
1263
+ _logging.getLogger("coding_os.cli").debug("logging_os bridge unavailable: %s", _bridge_exc)
1264
+
1265
+ _bootstrap_hub_dir_if_first_run()
1266
+
1267
+
1268
+ cli.add_command(doctor_cmd)
1269
+ cli.add_command(list_stacks_cmd)
1270
+ cli.add_command(list_adapters_cmd)
1271
+ cli.add_command(add_stack_cmd)
1272
+ cli.add_command(remove_stack_cmd)
1273
+ cli.add_command(docs_index_cmd)
1274
+ cli.add_command(task_sync_cmd)
1275
+ cli.add_command(reindex_cmd)
1276
+ cli.add_command(brain_decay_cmd)
1277
+ cli.add_command(brain_gc_cmd)
1278
+ cli.add_command(brain_sweep_changelog_cmd)
1279
+ cli.add_command(update_cmd)
1280
+ cli.add_command(setup_cmd)
1281
+ cli.add_command(materialize_file_cmd)
1282
+ cli.add_command(tail_cmd)
1283
+ cli.add_command(skills_list_cmd)
1284
+
1285
+ from cli.module_commands import module_group as module_group_cmd # noqa: E402
1286
+ from cli.preset_commands import preset_group as preset_group_cmd # noqa: E402
1287
+ from cli.skill_commands import skill_group as skill_group_cmd # noqa: E402
1288
+ from cli.stack_lint import stack_lint as stack_lint_cmd # noqa: E402
1289
+
1290
+ cli.add_command(module_group_cmd)
1291
+ cli.add_command(preset_group_cmd)
1292
+ cli.add_command(skill_group_cmd)
1293
+ cli.add_command(stack_lint_cmd)
1294
+
1295
+ # Durable error/log query CLI (cos errors / cos logs).
1296
+ try:
1297
+ from cli.logs_commands import errors_cmd as _errors_cmd
1298
+ from cli.logs_commands import logs_cmd as _logs_cmd
1299
+
1300
+ cli.add_command(_logs_cmd)
1301
+ cli.add_command(_errors_cmd)
1302
+ except ImportError as _logs_cli_exc: # pragma: no cover — defensive
1303
+ import logging as _logging
1304
+
1305
+ _logging.getLogger("coding_os.cli").debug("logs CLI unavailable: %s", _logs_cli_exc)
1306
+
1307
+ # Doc lifecycle CLI (cos doc-new / doc-history / doc-lint).
1308
+ try:
1309
+ from cli.doc_commands import doc_history_cmd as _doc_history_cmd
1310
+ from cli.doc_commands import doc_lint_cmd as _doc_lint_cmd
1311
+ from cli.doc_commands import doc_new_cmd as _doc_new_cmd
1312
+
1313
+ cli.add_command(_doc_new_cmd)
1314
+ cli.add_command(_doc_history_cmd)
1315
+ cli.add_command(_doc_lint_cmd)
1316
+ except ImportError as _doc_cli_exc: # pragma: no cover — defensive
1317
+ import logging as _logging
1318
+
1319
+ _logging.getLogger("coding_os.cli").debug("doc CLI unavailable: %s", _doc_cli_exc)
1320
+
1321
+ # Fast scope-aware verification: `cos verify --since-edit`.
1322
+ try:
1323
+ from cli.verify_since_edit import verify_since_edit_cmd as _verify_cmd
1324
+
1325
+ cli.add_command(_verify_cmd)
1326
+ except ImportError as _verify_exc: # pragma: no cover — defensive
1327
+ import logging as _logging
1328
+
1329
+ _logging.getLogger("coding_os.cli").debug("verify CLI unavailable: %s", _verify_exc)
1330
+
1331
+ # Hub propagation: push meta-repo edits to every registered
1332
+ # project via symlink re-link + DB migration. Lives in src/cli/sync_all.py
1333
+ # so registry.py stays focused on the JSON CRUD.
1334
+ try:
1335
+ from cli.sync_all import sync_all_cmd, sync_doctor_cmd
1336
+
1337
+ cli.add_command(sync_all_cmd)
1338
+ cli.add_command(sync_doctor_cmd)
1339
+ except ImportError as _e:
1340
+ import logging as _logging
1341
+
1342
+ _logging.getLogger("cli.main").debug("sync_all unavailable: %s", _e)
1343
+
1344
+ # board_os CLI surface (16 commands).
1345
+ try:
1346
+ from cli.board_commands import BOARD_COMMANDS
1347
+
1348
+ for _bc in BOARD_COMMANDS:
1349
+ cli.add_command(_bc)
1350
+ except ImportError:
1351
+ pass # board_os optional — don't break `cos` if deps missing.
1352
+
1353
+ # pr-mode multi-agent git executor (cos pr open/submit/status/cleanup/preflight).
1354
+ try:
1355
+ from cli.pr_commands import pr_group as _pr_group
1356
+
1357
+ cli.add_command(_pr_group)
1358
+ except ImportError as _pr_cli_exc: # pragma: no cover — defensive
1359
+ import logging as _logging
1360
+
1361
+ _logging.getLogger("coding_os.cli").debug("pr CLI unavailable: %s", _pr_cli_exc)
1362
+
1363
+ # Scheduled jobs (CRON A/B).
1364
+ try:
1365
+ from cli.cron_commands import cron_cmd
1366
+
1367
+ cli.add_command(cron_cmd)
1368
+ except ImportError as _e:
1369
+ import logging as _logging
1370
+
1371
+ _logging.getLogger("cli.main").debug("cron CLI unavailable: %s", _e)
1372
+
1373
+ # cognition CLI (formula dispatches, persona selections, backtracks).
1374
+ try:
1375
+ from cli.cognition import COGNITION_COMMANDS
1376
+
1377
+ for _cc in COGNITION_COMMANDS:
1378
+ cli.add_command(_cc)
1379
+ except ImportError:
1380
+ pass # cognition optional — don't break `cos` if click missing.
1381
+
1382
+
1383
+ def _resolve_project_dir(raw: str) -> Path:
1384
+ """Resolve the `--project-dir` value to an absolute path.
1385
+
1386
+ Handles the `uv run --directory <coding-os>` invocation pattern
1387
+ correctly: when uv changes cwd to the coding-os repo before launching
1388
+ Python, a default `.` would resolve to coding-os itself, silently
1389
+ initializing the coding-os repo instead of the user's project.
1390
+
1391
+ Resolution order:
1392
+ 1. If the raw value is NOT exactly "." (user passed an explicit path)
1393
+ → resolve relative to the current Python cwd.
1394
+ 2. Otherwise, prefer the shell's `$PWD` env var (uv and most shells
1395
+ preserve it — it's the original invocation directory).
1396
+ 3. Fall back to `os.getcwd()` for non-uv invocations.
1397
+
1398
+ This is defensive — `Path(".").resolve()` alone is dangerous under
1399
+ `uv --directory` because uv rewrites cwd before Python starts.
1400
+ """
1401
+ if raw != ".":
1402
+ return Path(raw).resolve()
1403
+
1404
+ shell_pwd = os.environ.get("PWD")
1405
+ if shell_pwd and Path(shell_pwd).is_dir():
1406
+ return Path(shell_pwd).resolve()
1407
+ return Path.cwd().resolve()
1408
+
1409
+
1410
+ def _refuse_coding_os_self_init(project: Path) -> None:
1411
+ """Block init from running inside the coding-os repo itself.
1412
+
1413
+ The coding-os source tree already contains `AGENTS.md`, `Makefile`,
1414
+ `docs/`, `core/` etc — running `init` against it scatters scaffold
1415
+ files across the repo and can overwrite real development docs.
1416
+ Detect this by checking for the telltale `src/core/thinking_os/server.py`
1417
+ file and refuse.
1418
+ """
1419
+ from cli._init_helpers import is_coding_os_source_tree
1420
+
1421
+ if is_coding_os_source_tree(project):
1422
+ click.echo(
1423
+ f"\nERROR: Refusing to init inside the coding-os repo itself ({project}).\n"
1424
+ f" This path contains src/core/thinking_os/server.py — it is the source tree.\n"
1425
+ f" Initializing here would scatter scaffold files into the repo.\n\n"
1426
+ f" Fix:\n"
1427
+ f" cd /path/to/your/actual-project\n"
1428
+ f" uv run --directory {project} python -m cli.main init \\\n"
1429
+ f' --agent claude --project-dir "$(pwd)"\n\n'
1430
+ f" Or use the alias:\n"
1431
+ f" alias cos-init='uv run --directory {project} python -m cli.main init'\n"
1432
+ f' cos-init --agent claude --project-dir "$(pwd)"\n',
1433
+ err=True,
1434
+ )
1435
+ sys.exit(1)
1436
+
1437
+
1438
+ @cli.command()
1439
+ @click.option(
1440
+ "--agent",
1441
+ "-a",
1442
+ default=None,
1443
+ help="Agent adapter(s) to install, comma-separated (e.g. 'claude,codex'). Prompted if omitted (unless --yes).",
1444
+ )
1445
+ @click.option("--template", "-t", multiple=True, help="Stack template(s) to apply")
1446
+ @click.option(
1447
+ "--preset",
1448
+ "preset_id",
1449
+ default=None,
1450
+ help="Named stack composition from templates/_presets/ (mutually exclusive with --template). Discover with `cos list-stacks`.",
1451
+ )
1452
+ @click.option(
1453
+ "--dry-config",
1454
+ is_flag=True,
1455
+ default=False,
1456
+ help="Print the merged .coding-os config preview (swimlane union + conflicts) for the requested stacks/preset and exit without writing anything.",
1457
+ )
1458
+ @click.option(
1459
+ "--dry-run",
1460
+ is_flag=True,
1461
+ default=False,
1462
+ help="Preview the would-be scaffold tree (files + composed configs) for the requested stacks/preset and exit without writing anything.",
1463
+ )
1464
+ @click.option(
1465
+ "--skills",
1466
+ "extra_skills_csv",
1467
+ default=None,
1468
+ help="Extra core skills beyond the stacks' own, comma-separated (wizard parity). Validated against the skill registry.",
1469
+ )
1470
+ @click.option(
1471
+ "--summary",
1472
+ "project_summary",
1473
+ default=None,
1474
+ help="1-2 paragraph project description; seeds docs/_meta/project-description.md (wizard parity, TASK-364 intake).",
1475
+ )
1476
+ @click.option(
1477
+ "--project-dir",
1478
+ "-d",
1479
+ default=None,
1480
+ help="Parent directory for the project (default: shell cwd). Mutually exclusive with --debug.",
1481
+ )
1482
+ @click.option(
1483
+ "--name",
1484
+ "-n",
1485
+ default=None,
1486
+ help="Create a new directory with this name inside --project-dir (or cwd). Validated: ^[a-z0-9][a-z0-9._-]{0,63}$",
1487
+ )
1488
+ @click.option(
1489
+ "--debug",
1490
+ is_flag=True,
1491
+ default=False,
1492
+ help="Scaffold into <coding-os>/.build/debug/<name>/ (or 'the-script-output'). Requires running inside the coding-os repo.",
1493
+ )
1494
+ @click.option(
1495
+ "--git/--no-git",
1496
+ default=True,
1497
+ help="Run `git init` in the new project (default: --git). Skipped silently if target is nested in an existing git repo.",
1498
+ )
1499
+ @click.option(
1500
+ "--force",
1501
+ is_flag=True,
1502
+ default=False,
1503
+ help="Overwrite target directory if it already exists and is non-empty.",
1504
+ )
1505
+ @click.option(
1506
+ "--adopt",
1507
+ is_flag=True,
1508
+ default=False,
1509
+ help="Overlay onto an existing non-empty repo in place — never wipe user files (brownfield). Prefer `cos adopt`.",
1510
+ )
1511
+ @click.option(
1512
+ "--yes",
1513
+ "-y",
1514
+ is_flag=True,
1515
+ default=False,
1516
+ help="Non-interactive: use defaults for anything not passed via flags. Required in CI / non-TTY.",
1517
+ )
1518
+ @click.option(
1519
+ "--format",
1520
+ "output_format",
1521
+ type=click.Choice(["text", "json"]),
1522
+ default="text",
1523
+ help="Output format.",
1524
+ )
1525
+ @click.option(
1526
+ "--today",
1527
+ "today_override",
1528
+ default=None,
1529
+ help="ISO-8601 date to use for {{DATE}} substitutions (default: today). Deterministic fixture for golden tests.",
1530
+ )
1531
+ @click.option(
1532
+ "--no-register",
1533
+ is_flag=True,
1534
+ default=False,
1535
+ help="Skip writing this project to the global ~/.coding-os/registry.json. Used by sandbox fixtures (manifest-regen, golden tests) so disposable temp dirs don't pollute the hub registry.",
1536
+ )
1537
+ @click.option(
1538
+ "--index/--no-index",
1539
+ "do_index",
1540
+ default=True,
1541
+ help="Seed the doc-search index after scaffold (loads the embedding model, ~15s). --no-index skips it for fast / CI / fixture scaffolds — the index lives in the gitignored runtime DB, so golden captures never need it.",
1542
+ )
1543
+ @click.option(
1544
+ "--graph-index/--no-graph-index",
1545
+ "graph_index",
1546
+ default=False,
1547
+ help="Build the knowledge graph even under --no-index (AST walk, no embedding model). The Hub Composer passes this so a fast --no-index create still gets a populated Graph tab; default off keeps CI/fixture scaffolds (which pass --no-index) graph-free.",
1548
+ )
1549
+ @click.option(
1550
+ "--disable-module",
1551
+ "disable_module",
1552
+ multiple=True,
1553
+ help=_module_flag_help(),
1554
+ )
1555
+ @click.option(
1556
+ "--profile",
1557
+ "profile",
1558
+ default=None,
1559
+ help=_profile_flag_help(),
1560
+ )
1561
+ @click.option(
1562
+ "--enable-module",
1563
+ "enable_module",
1564
+ multiple=True,
1565
+ help=_enable_flag_help(),
1566
+ )
1567
+ def init(
1568
+ agent: str | None,
1569
+ template: tuple[str, ...],
1570
+ preset_id: str | None,
1571
+ dry_config: bool,
1572
+ dry_run: bool,
1573
+ extra_skills_csv: str | None,
1574
+ project_summary: str | None,
1575
+ project_dir: str | None,
1576
+ name: str | None,
1577
+ debug: bool,
1578
+ git: bool,
1579
+ force: bool,
1580
+ adopt: bool,
1581
+ yes: bool,
1582
+ output_format: str,
1583
+ today_override: str | None,
1584
+ no_register: bool,
1585
+ do_index: bool,
1586
+ graph_index: bool,
1587
+ disable_module: tuple[str, ...],
1588
+ profile: str | None,
1589
+ enable_module: tuple[str, ...],
1590
+ ) -> None:
1591
+ """Initialize coding-os in a project.
1592
+
1593
+ Interactive by default — prompts for missing agent/template/name when a
1594
+ TTY is attached. Pass --yes for fully non-interactive runs (CI) using
1595
+ whatever flags are provided plus sensible defaults.
1596
+ """
1597
+ shell_cwd_raw = os.environ.get("PWD") or os.getcwd()
1598
+ shell_cwd = Path(shell_cwd_raw).resolve()
1599
+
1600
+ # --preset expands to its stack list before anything else touches
1601
+ # `template` (config-composition.md § Presets).
1602
+ active_preset = None
1603
+ if preset_id:
1604
+ if template:
1605
+ click.echo("ERROR: --preset and --template are mutually exclusive.", err=True)
1606
+ sys.exit(2)
1607
+ from cli.preset_registry import load_preset_registry
1608
+
1609
+ presets = load_preset_registry(
1610
+ TEMPLATES_DIR, known_stacks=set(_get_stack_registry().keys())
1611
+ )
1612
+ for warning in presets.warnings:
1613
+ click.echo(f" WARN: {warning}", err=True)
1614
+ if preset_id not in presets:
1615
+ click.echo(
1616
+ f"ERROR: preset '{preset_id}' not found — available: "
1617
+ f"{sorted(presets.keys()) or '(none)'}",
1618
+ err=True,
1619
+ )
1620
+ sys.exit(2)
1621
+ active_preset = presets[preset_id]
1622
+ template = active_preset.stacks
1623
+ click.echo(f"Preset '{preset_id}' → stacks: {', '.join(template)}")
1624
+
1625
+ # A --profile expands to a curated disabled-module set (subsystems.yaml::
1626
+ # profiles) MERGED with explicit --disable-module flags; omitted → the
1627
+ # registry default_profile. Resolved before validation so the union flows
1628
+ # through the same dependency-checked apply path (TASK-509).
1629
+ from cli.subsystems import load_profiles, resolve_profile
1630
+
1631
+ _chosen_profile = profile or load_profiles()[1]
1632
+ try:
1633
+ _profile_disabled = resolve_profile(_chosen_profile)
1634
+ except ValueError as exc:
1635
+ click.echo(f"ERROR: {exc}", err=True)
1636
+ sys.exit(2)
1637
+ _explicit_disable = tuple(disable_module)
1638
+ disable_module = tuple(_profile_disabled) + tuple(disable_module)
1639
+
1640
+ # Validate --disable-module BEFORE the dry-run/real split so the preview and
1641
+ # the real init reject the same ids (pass-3 review).
1642
+ disabled_modules = _validated_disabled_modules(disable_module)
1643
+ disabled_modules = _apply_enable_modules(disabled_modules, enable_module, _explicit_disable)
1644
+
1645
+ if dry_config:
1646
+ _dry_config_preview(template, output_format)
1647
+ return
1648
+
1649
+ if dry_run:
1650
+ _dry_run_preview(template, output_format, tuple(disabled_modules))
1651
+ return
1652
+
1653
+ # --skills validated up-front (fail fast, wizard parity: the wizard only
1654
+ # offers known core skills).
1655
+ extra_skills: list[str] = []
1656
+ if extra_skills_csv:
1657
+ from cli.skill_registry import load_skill_registry
1658
+ from cli.skills_list import CORE_SKILLS_DIR
1659
+
1660
+ known_skills = set(load_skill_registry(CORE_SKILLS_DIR).skills.keys())
1661
+ extra_skills = [s.strip() for s in extra_skills_csv.split(",") if s.strip()]
1662
+ unknown_skills = [s for s in extra_skills if s not in known_skills]
1663
+ if unknown_skills:
1664
+ click.echo(
1665
+ f"ERROR: unknown skill(s) {unknown_skills} — see `cos skills-list`.", err=True
1666
+ )
1667
+ sys.exit(2)
1668
+
1669
+ # Idempotent detection: existing install → offer sync instead of re-init.
1670
+ existing = _detect_existing_install(shell_cwd) if not name and not project_dir else None
1671
+ if existing is not None:
1672
+ if output_format == "text":
1673
+ click.echo(
1674
+ f"Existing coding-os install detected at {shell_cwd}\n"
1675
+ f" agents: {', '.join(existing['agents']) or '(none)'}\n"
1676
+ f" templates: {', '.join(existing['templates']) or '(none)'}"
1677
+ )
1678
+ if yes or click.confirm("Sync missing components (links + config)?", default=True):
1679
+ _sync_missing(shell_cwd, output_format=output_format)
1680
+ return
1681
+ click.echo("Aborted.")
1682
+ sys.exit(0)
1683
+
1684
+ # Non-TTY without --yes: refuse to guess targets silently (TASK-359).
1685
+ # Sits AFTER existing-install detection so the idempotent sync path keeps
1686
+ # working for a bare re-`cos init` inside a project.
1687
+ if not yes and not sys.stdin.isatty():
1688
+ if agent is None:
1689
+ click.echo(
1690
+ "ERROR: non-interactive shell — pass --agent (and --name/--project-dir), "
1691
+ "or use --yes.",
1692
+ err=True,
1693
+ )
1694
+ sys.exit(2)
1695
+ if name is None and project_dir is None and not debug:
1696
+ click.echo(
1697
+ "ERROR: non-interactive shell — pass --name and/or --project-dir "
1698
+ "(or --yes to scaffold into the current directory).",
1699
+ err=True,
1700
+ )
1701
+ sys.exit(2)
1702
+
1703
+ # Prompt for missing inputs. --yes disables all prompting. Prompts that
1704
+ # hit EOF (closed stdin — CI, scaffold tests) fall back to sensible
1705
+ # defaults instead of aborting, so flag-based test helpers that don't
1706
+ # provide stdin keep working.
1707
+ def _safe_prompt(prompt_fn, fallback):
1708
+ try:
1709
+ return prompt_fn()
1710
+ except (click.exceptions.Abort, click.exceptions.UsageError, EOFError):
1711
+ return fallback
1712
+
1713
+ # Parse --agent: accepts comma-separated values (e.g. "claude,codex").
1714
+ agents: list[str] | None = None
1715
+ if agent is not None:
1716
+ agents = _parse_agents(agent)
1717
+ if agents is None:
1718
+ if yes:
1719
+ click.echo("ERROR: --agent is required with --yes.", err=True)
1720
+ sys.exit(2)
1721
+ agents = _safe_prompt(_prompt_agents, None)
1722
+ if agents is None:
1723
+ click.echo("ERROR: --agent is required (no input available).", err=True)
1724
+ sys.exit(2)
1725
+ if not template and not yes:
1726
+ template = _safe_prompt(_prompt_templates, ())
1727
+ if name is None and project_dir is None and not yes:
1728
+ name, project_dir = _safe_prompt(
1729
+ lambda: _prompt_name_and_location(shell_cwd),
1730
+ (None, None),
1731
+ )
1732
+
1733
+ try:
1734
+ target = resolve_init_target(
1735
+ name=name,
1736
+ project_dir=project_dir,
1737
+ debug=debug,
1738
+ force=force,
1739
+ adopt=adopt,
1740
+ cwd=shell_cwd,
1741
+ # Refuse self-init BEFORE resolve_init_target's --force wipe runs.
1742
+ # Fires only when the target already exists and is non-empty.
1743
+ pre_wipe_hook=_refuse_coding_os_self_init,
1744
+ )
1745
+ except InitError as exc:
1746
+ click.echo(f"ERROR: {exc}", err=True)
1747
+ sys.exit(exc.exit_code)
1748
+
1749
+ # Also check the (possibly fresh, possibly empty) target path — catches
1750
+ # the case where self-init was called without --force.
1751
+ _refuse_coding_os_self_init(target.path)
1752
+
1753
+ project = target.path
1754
+ _refuse_coding_os_self_init(project)
1755
+
1756
+ # JSON mode keeps stdout pure for programmatic callers, but the scaffold's
1757
+ # progress echoes are what the Hub's job runner scrapes for phase markers —
1758
+ # buffering them into the void left the create progress bar on "validate".
1759
+ # Streaming them to stderr keeps stdout clean AND the progress bar live.
1760
+ _stdout_redirect = (
1761
+ contextlib.redirect_stdout(sys.stderr)
1762
+ if output_format == "json"
1763
+ else contextlib.nullcontext()
1764
+ )
1765
+
1766
+ if output_format == "text":
1767
+ click.echo(f"Initializing coding-os in {project}")
1768
+ click.echo(f" Agents: {', '.join(agents)}")
1769
+ if template:
1770
+ click.echo(f" Templates: {', '.join(template)}")
1771
+ if debug:
1772
+ click.echo(" Mode: debug (under .build/debug/)")
1773
+ if target.forced_empty:
1774
+ click.echo(" Note: target existed and was wiped (--force)")
1775
+
1776
+ with _stdout_redirect:
1777
+ _run_scaffold_phase(
1778
+ agents,
1779
+ template,
1780
+ project,
1781
+ today=today_override,
1782
+ no_register=no_register,
1783
+ do_index=do_index,
1784
+ graph_index=graph_index,
1785
+ active_preset=active_preset,
1786
+ extra_skills=extra_skills,
1787
+ project_summary=project_summary,
1788
+ disabled_modules=disabled_modules,
1789
+ )
1790
+
1791
+ git_result = maybe_git_init(target, enabled=git)
1792
+ # .gitignore whenever git is in play (fresh or nested repo) so the
1793
+ # mutating runtime DB never gets committed; the baseline commit runs
1794
+ # only when init created the repo — never sweep a parent project's tree.
1795
+ if git:
1796
+ ensure_gitignore(project)
1797
+ commit_result = maybe_initial_commit(target, enabled=git and git_result.ran)
1798
+ # Install the human-persona git hooks AFTER the baseline commit so that
1799
+ # tool-generated commit isn't gated by the freshly-installed pre-commit.
1800
+ hooks_result = install_consumer_git_hooks(project, enabled=git and git_result.ran)
1801
+ files_created = sum(1 for _ in project.rglob("*") if _.is_file())
1802
+
1803
+ summary: dict[str, object] = {
1804
+ "status": "ok",
1805
+ "path": str(project),
1806
+ "slug": _registered_slug(project),
1807
+ "agents": agents,
1808
+ "templates": list(template),
1809
+ "debug": debug,
1810
+ "forced_empty": target.forced_empty,
1811
+ "git": {
1812
+ "ran": git_result.ran,
1813
+ "skipped_reason": git_result.skipped_reason,
1814
+ "error": git_result.error,
1815
+ "initial_commit": commit_result.committed,
1816
+ "commit_error": commit_result.error,
1817
+ "hooks_installed": list(hooks_result.installed),
1818
+ "hooks_error": hooks_result.error,
1819
+ },
1820
+ "files_created": files_created,
1821
+ "db_path": str(project / STATE_DIR / "coding-os.db"),
1822
+ "config_file": str(project / CONFIG_FILE),
1823
+ "warnings": (
1824
+ ["No stack template selected — AGENTS.md has placeholder routing. Run: cos add-stack"]
1825
+ if not template
1826
+ else []
1827
+ ),
1828
+ }
1829
+
1830
+ if output_format == "json":
1831
+ click.echo(json.dumps(summary, indent=2))
1832
+ return
1833
+
1834
+ # text mode — final summary
1835
+ if git_result.ran:
1836
+ click.echo(" git: initialized")
1837
+ if commit_result.committed:
1838
+ click.echo(" git: baseline commit created")
1839
+ elif commit_result.error:
1840
+ click.echo(f" git: WARN baseline commit failed — {commit_result.error}")
1841
+ if hooks_result.installed:
1842
+ click.echo(f" git: hooks installed ({', '.join(hooks_result.installed)})")
1843
+ elif hooks_result.error:
1844
+ click.echo(f" git: WARN hooks not installed — {hooks_result.error}")
1845
+ elif git_result.skipped_reason:
1846
+ click.echo(f" git: skipped ({git_result.skipped_reason})")
1847
+ elif git_result.error:
1848
+ click.echo(f" git: WARN {git_result.error}")
1849
+
1850
+ click.echo("\ncoding-os initialized successfully!")
1851
+ click.echo(f" Path: {project}")
1852
+ click.echo(f" Files: {files_created}")
1853
+ click.echo(f" Config: {CONFIG_FILE}")
1854
+ click.echo(f" State: {STATE_DIR}/")
1855
+ click.echo(" Makefile: make help")
1856
+ click.echo("\nQuick start:")
1857
+ click.echo(" cos daily # Project status + today's tasks")
1858
+ click.echo(" cos task-pick # See next recommended task")
1859
+ click.echo(" cos task-start TASK-001 # Start working")
1860
+
1861
+ if not template:
1862
+ available = sorted(_get_stack_registry().keys())
1863
+ click.echo(
1864
+ "\n WARN: No stack template selected.\n"
1865
+ " AGENTS.md has placeholder routing — agent works but lacks domain rules,\n"
1866
+ " verify commands, and engineering guidelines.\n"
1867
+ f" Add a stack now: cos add-stack <id>\n"
1868
+ f" Available stacks: {', '.join(available)}"
1869
+ )
1870
+
1871
+ import platform as _platform
1872
+
1873
+ if _platform.system() == "Darwin":
1874
+ click.echo("\nNightly maintenance (optional):")
1875
+ click.echo(" cos cron install # launchd job — decay, learn, routing (daily 03:00)")
1876
+
1877
+
1878
+ _STACK_MARKER_LANGUAGES: dict[str, str] = {
1879
+ # Build-manifest marker → base language. Resolved to that language's plain
1880
+ # stack via the registry below, so no stack id is ever hardcoded (Rule 11).
1881
+ "pyproject.toml": "python",
1882
+ "setup.py": "python",
1883
+ "requirements.txt": "python",
1884
+ "package.json": "typescript",
1885
+ "go.mod": "go",
1886
+ "Cargo.toml": "rust",
1887
+ "Gemfile": "ruby",
1888
+ "composer.json": "php",
1889
+ "pom.xml": "java",
1890
+ "build.gradle": "java",
1891
+ }
1892
+
1893
+
1894
+ def _detect_stacks_from_markers(path: Path) -> list[str]:
1895
+ """Resolve build-manifest markers in `path` to plain-stack ids via the
1896
+ registry — the brownfield-adopt stack proposal (no anatomy relocation)."""
1897
+ from cli.stack_registry import plain_stack_by_language
1898
+
1899
+ registry = _get_stack_registry()
1900
+ profiles = {sid: registry[sid] for sid in registry.keys()}
1901
+ language_to_plain = plain_stack_by_language(profiles)
1902
+ detected: list[str] = []
1903
+ for marker, language in _STACK_MARKER_LANGUAGES.items():
1904
+ if not (path / marker).exists():
1905
+ continue
1906
+ stack = language_to_plain.get(language)
1907
+ if stack and stack not in detected:
1908
+ detected.append(stack)
1909
+ return detected
1910
+
1911
+
1912
+ @cli.command()
1913
+ @click.option(
1914
+ "--agent",
1915
+ "-a",
1916
+ default=None,
1917
+ help="Agent adapter(s) to install, comma-separated (required with --yes).",
1918
+ )
1919
+ @click.option(
1920
+ "--template",
1921
+ "-t",
1922
+ multiple=True,
1923
+ help="Stack template(s) to record — overrides build-marker auto-detection.",
1924
+ )
1925
+ @click.option(
1926
+ "--yes",
1927
+ "-y",
1928
+ is_flag=True,
1929
+ default=False,
1930
+ help="Non-interactive: adopt into the current directory using flags + defaults.",
1931
+ )
1932
+ @click.option("--git/--no-git", default=True, help="Run `git init` if the repo isn't already one.")
1933
+ @click.option(
1934
+ "--format",
1935
+ "output_format",
1936
+ type=click.Choice(["text", "json"]),
1937
+ default="text",
1938
+ help="Output format.",
1939
+ )
1940
+ @click.option(
1941
+ "--no-register",
1942
+ is_flag=True,
1943
+ default=False,
1944
+ help="Skip writing this project to the global registry (sandbox fixtures).",
1945
+ )
1946
+ @click.option(
1947
+ "--index/--no-index",
1948
+ "do_index",
1949
+ default=True,
1950
+ help="Seed the doc-search index after adopt (--no-index for fast / CI runs).",
1951
+ )
1952
+ @click.option(
1953
+ "--disable-module",
1954
+ "disable_module",
1955
+ multiple=True,
1956
+ help=_module_flag_help(),
1957
+ )
1958
+ @click.option(
1959
+ "--profile",
1960
+ "profile",
1961
+ default=None,
1962
+ help=_profile_flag_help(),
1963
+ )
1964
+ @click.option(
1965
+ "--enable-module",
1966
+ "enable_module",
1967
+ multiple=True,
1968
+ help=_enable_flag_help(),
1969
+ )
1970
+ @click.pass_context
1971
+ def adopt(
1972
+ ctx: click.Context,
1973
+ agent: str | None,
1974
+ template: tuple[str, ...],
1975
+ yes: bool,
1976
+ git: bool,
1977
+ output_format: str,
1978
+ no_register: bool,
1979
+ do_index: bool,
1980
+ disable_module: tuple[str, ...],
1981
+ profile: str | None,
1982
+ enable_module: tuple[str, ...],
1983
+ ) -> None:
1984
+ """Overlay coding-os onto an existing repo without touching user code.
1985
+
1986
+ Adds .coding-os/ state, adapter dirs and AGENTS.md in place, auto-detecting
1987
+ stacks from build markers (pyproject.toml / package.json / go.mod / …). An
1988
+ already-adopted repo pivots to the idempotent sync path instead of re-installing.
1989
+ """
1990
+ target = Path(os.environ.get("PWD") or os.getcwd()).resolve()
1991
+
1992
+ # Already adopted → idempotent sync (same path as a bare re-`cos init`).
1993
+ if _detect_existing_install(target) is not None:
1994
+ if output_format == "text":
1995
+ click.echo(f"coding-os already present at {target} — syncing missing components.")
1996
+ _sync_missing(target, output_format=output_format)
1997
+ return
1998
+
1999
+ # Propose stacks from build markers unless the caller pinned --template.
2000
+ if not template:
2001
+ detected = _detect_stacks_from_markers(target)
2002
+ if detected and output_format == "text":
2003
+ click.echo(f"Detected stacks: {', '.join(detected)}")
2004
+ template = tuple(detected)
2005
+
2006
+ # Reuse the init scaffold in place — name/project_dir unset ⇒ current dir,
2007
+ # force unset ⇒ no wipe, so pre-existing user files are never overwritten.
2008
+ ctx.invoke(
2009
+ init,
2010
+ agent=agent,
2011
+ template=template,
2012
+ adopt=True,
2013
+ yes=yes,
2014
+ git=git,
2015
+ output_format=output_format,
2016
+ no_register=no_register,
2017
+ do_index=do_index,
2018
+ disable_module=disable_module,
2019
+ profile=profile,
2020
+ enable_module=enable_module,
2021
+ )
2022
+
2023
+
2024
+ def _run_scaffold_phase(
2025
+ agents: list[str],
2026
+ template: tuple[str, ...],
2027
+ project: Path,
2028
+ *,
2029
+ today: str | None = None,
2030
+ no_register: bool = False,
2031
+ do_index: bool = True,
2032
+ graph_index: bool = False,
2033
+ active_preset=None,
2034
+ extra_skills: list[str] | None = None,
2035
+ project_summary: str | None = None,
2036
+ disabled_modules: list[str] | None = None,
2037
+ ) -> None:
2038
+ """Original scaffolding body — extracted so it can be redirected in JSON mode.
2039
+
2040
+ `today` is an optional ISO-8601 override for {{DATE}} substitution
2041
+ in scaffolded files (used by golden parity tests for determinism).
2042
+
2043
+ `no_register` skips the global registry write (step 12). Sandbox
2044
+ fixtures (manifest-regen, golden parity tests) pass it so disposable
2045
+ temp dirs don't pollute ~/.coding-os/registry.json.
2046
+ """
2047
+
2048
+ # 1. Create state directory
2049
+ state = project / STATE_DIR
2050
+ state.mkdir(parents=True, exist_ok=True)
2051
+ click.echo(f" Created {STATE_DIR}/")
2052
+ stamp_core_version(state)
2053
+ # First-edit grace marker (TASK-372): lets the agent's first legitimate code
2054
+ # edit in a brand-new project skip the doc-anchor BLOCK. enforce-doc-anchor.sh
2055
+ # consumes it on that first edit, so the grace is exactly one edit, bounded.
2056
+ (state / ".fresh-init").touch()
2057
+
2058
+ # 2. Initialize DB directory
2059
+ db_path = state / "coding-os.db"
2060
+ if not db_path.exists():
2061
+ # Initialize the database
2062
+ brain_dir = str(CORE_DIR / "thinking_os")
2063
+ init_code = (
2064
+ "import sys; "
2065
+ f"sys.path.insert(0, {brain_dir!r}); "
2066
+ "from database import init_db; "
2067
+ f"init_db({str(db_path)!r})"
2068
+ )
2069
+ env = os.environ.copy()
2070
+ env["COS_DB_PATH"] = str(db_path)
2071
+ proc = subprocess.run(
2072
+ [sys.executable, "-c", init_code],
2073
+ env=env,
2074
+ capture_output=True,
2075
+ text=True,
2076
+ )
2077
+ if proc.returncode != 0:
2078
+ click.echo(" ERROR: failed to initialize thinking_os database", err=True)
2079
+ if proc.stderr:
2080
+ click.echo(proc.stderr.strip(), err=True)
2081
+ click.echo(
2082
+ " HINT: missing Python deps are the usual cause — run "
2083
+ "`uv sync --extra rag` in the coding-os checkout, then re-run `cos init` "
2084
+ "(machine prerequisites: `cos doctor --bootstrap`)",
2085
+ err=True,
2086
+ )
2087
+ raise SystemExit(1)
2088
+ click.echo(" Initialized thinking_os database")
2089
+
2090
+ # 3. Generate config
2091
+ config = {
2092
+ "version": "1.0",
2093
+ "agents": agents,
2094
+ "templates": list(template),
2095
+ "state_dir": STATE_DIR,
2096
+ "code_extensions": ["py", "ts", "tsx", "js", "jsx"],
2097
+ "verify": {},
2098
+ "protected_files": [],
2099
+ }
2100
+ if active_preset is not None:
2101
+ # Provenance + pass-through for later layers: extra-skill linking is
2102
+ # TASK-370, module toggle behavior is TASK-349.
2103
+ config["preset"] = active_preset.id
2104
+ if active_preset.skills:
2105
+ config["extra_skills"] = list(active_preset.skills)
2106
+ if active_preset.modules:
2107
+ config["modules"] = dict(active_preset.modules)
2108
+ # CLI / wizard --disable-module entries merge on top of preset-declared
2109
+ # module state; the module_toggles pass below disables them in dependency
2110
+ # order (set_module_enabled refuses invalid chains, e.g. docs while tasks on).
2111
+ if disabled_modules:
2112
+ merged_modules = dict(config.get("modules") or {})
2113
+ for module_id in disabled_modules:
2114
+ merged_modules[module_id] = False
2115
+ config["modules"] = merged_modules
2116
+ if extra_skills:
2117
+ # --skills / wizard extras merge on top of preset-declared ones.
2118
+ config["extra_skills"] = list(
2119
+ dict.fromkeys([*(config.get("extra_skills") or []), *extra_skills])
2120
+ )
2121
+ _save_config(project, config)
2122
+ # Preset/wizard module toggles land in project state BEFORE the scaffold
2123
+ # copy so tag-driven docs composition sees them (TASK-360). Disable order:
2124
+ # dependents first (the registry refuses chains, e.g. docs before tasks).
2125
+ module_toggles = {k: v for k, v in (config.get("modules") or {}).items() if v is False}
2126
+ if module_toggles:
2127
+ from cli.subsystems import load_subsystems, set_module_enabled
2128
+
2129
+ registry_modules = load_subsystems()
2130
+
2131
+ def _dependents_being_disabled(module_id: str) -> int:
2132
+ # Dependents disable BEFORE their dependencies (the registry
2133
+ # refuses e.g. docs-off while tasks is still enabled).
2134
+ return sum(
2135
+ 1
2136
+ for other in module_toggles
2137
+ if other in registry_modules and module_id in registry_modules[other].depends_on
2138
+ )
2139
+
2140
+ ordered = sorted(module_toggles, key=_dependents_being_disabled)
2141
+ for module_id in ordered:
2142
+ toggle = set_module_enabled(project, module_id, False)
2143
+ if not toggle.ok:
2144
+ click.echo(f" WARN: module '{module_id}': {toggle.reason}", err=True)
2145
+ else:
2146
+ click.echo(f" Module disabled per preset: {module_id}")
2147
+ # SI-1 (TASK-439): route init through the SAME runtime-allowlist path
2148
+ # as `cos module disable`. set_module_enabled alone only flips state;
2149
+ # without this, .coding-os/disabled-hook-scripts is never written at
2150
+ # init time and the disabled modules' hooks keep firing. AGENTS.md is
2151
+ # written fresh by the scaffold copy below, so only the allowlist needs
2152
+ # regenerating here (not the full toggle_and_regen).
2153
+ from cli.project_overrides import write_runtime_allowlist
2154
+
2155
+ allowlist = write_runtime_allowlist(project)
2156
+ click.echo(f" Runtime hook allowlist → {allowlist.relative_to(project)}")
2157
+ if project_summary and project_summary.strip():
2158
+ # Onboarding intake — consumed by the description→PRD pipeline (TASK-364).
2159
+ meta_dir = project / "docs" / "_meta"
2160
+ meta_dir.mkdir(parents=True, exist_ok=True)
2161
+ (meta_dir / "project-description.md").write_text(
2162
+ "# Project Description (onboarding intake)\n\n" + project_summary.strip() + "\n",
2163
+ encoding="utf-8",
2164
+ )
2165
+ click.echo(" Seeded docs/_meta/project-description.md")
2166
+ # Docs-module gate: preset/wizard module toggles are stored in config
2167
+ # (behavior SSOT lands with TASK-349); docs defaults ON.
2168
+ if (config.get("modules") or {}).get("docs", True):
2169
+ from cli.setup import seed_prd_from_text
2170
+
2171
+ seeded = seed_prd_from_text(project, project_summary, date=today)
2172
+ if seeded:
2173
+ click.echo(f" Seeded {len(seeded)} PRD doc(s): {', '.join(seeded)}")
2174
+ click.echo(f" Generated {CONFIG_FILE}")
2175
+
2176
+ # 4. Run adapter install for each agent
2177
+ for agent in agents:
2178
+ click.echo(f"\nInstalling {agent} adapter...")
2179
+ _run_adapter_install(agent, project)
2180
+
2181
+ # 5. Apply templates (agent-agnostic content first)
2182
+ for t in template:
2183
+ click.echo(f"\nApplying template: {t}")
2184
+ # Pass first agent for path-scoped rules; additional agents get
2185
+ # rules via their own adapter install or add-adapter.
2186
+ _apply_template(t, project, agent=agents[0])
2187
+
2188
+ # 5b. Link stack-scoped skills into each agent's skills_dir.
2189
+ if template:
2190
+ for agent in agents:
2191
+ _link_stack_skills(agent, template, project)
2192
+
2193
+ # 5c. A module disabled above must shed its owned skills too (audit D2-1):
2194
+ # the adapter install (step 4) links every core skill, so init reaches the
2195
+ # same skill-parity `cos module disable` has at runtime by running the same
2196
+ # ref-counted cascade (a skill another enabled module owns is kept).
2197
+ if module_toggles:
2198
+ from cli.module_commands import cascade_module_commands
2199
+ from cli.skill_commands import cascade_module_skills
2200
+
2201
+ for module_id in module_toggles:
2202
+ try:
2203
+ cascade = cascade_module_skills(project, module_id, enabled=False)
2204
+ except Exception as exc: # noqa: BLE001 — best-effort; `cos doctor` reconciles drift
2205
+ click.echo(f" WARN: skill cascade for '{module_id}' skipped ({exc})", err=True)
2206
+ else:
2207
+ if cascade["unlinked"]:
2208
+ click.echo(
2209
+ f" Skills unlinked ({module_id} off): {', '.join(cascade['unlinked'])}"
2210
+ )
2211
+ try:
2212
+ cmd_cascade = cascade_module_commands(project, module_id, enabled=False)
2213
+ except Exception as exc: # noqa: BLE001 — best-effort; `cos doctor` reconciles drift
2214
+ click.echo(f" WARN: command cascade for '{module_id}' skipped ({exc})", err=True)
2215
+ else:
2216
+ if cmd_cascade["unlinked"]:
2217
+ click.echo(
2218
+ f" Commands unlinked ({module_id} off): "
2219
+ f"{', '.join(cmd_cascade['unlinked'])}"
2220
+ )
2221
+
2222
+ # 6. Aggregate base + stacks + adapter into a world.
2223
+ # Use the first agent for world building (substitutions, AGENTS.md).
2224
+ # All adapters share the same core content; adapter-specific setup
2225
+ # was handled in step 4.
2226
+ for w in _get_stack_registry().warnings:
2227
+ click.echo(f" WARN: {w}", err=True)
2228
+ world = _build_world(agents[0], template, project, today=today)
2229
+ for msg in world.conflicts:
2230
+ click.echo(f" WARN: {msg}", err=True)
2231
+ substitutions = world.substitutions
2232
+ if project_summary and project_summary.strip():
2233
+ # The user's own words replace the generic default everywhere the
2234
+ # {{PROJECT_DESCRIPTION}} placeholder appears (TASK-364).
2235
+ substitutions = {
2236
+ **substitutions,
2237
+ "PROJECT_DESCRIPTION": " ".join(project_summary.split()),
2238
+ }
2239
+
2240
+ # 6b. Patch .coding-os.yaml.verify with derived commands.
2241
+ # step 3 wrote an empty dict because the world is only available here.
2242
+ # enforce-verify.sh reads this map to know which suite to require per
2243
+ # changed-file glob, so we must populate it before any hook runs.
2244
+ verify_map = _derive_verify_from_world(world)
2245
+ if verify_map:
2246
+ existing = _load_config(project) or {}
2247
+ existing["verify"] = verify_map
2248
+ _save_config(project, existing)
2249
+ click.echo(f" Populated verify config: {', '.join(sorted(verify_map))}")
2250
+
2251
+ # 7. Overlay scaffold files (_base + each template overlay) with placeholder resolution
2252
+ copied = _overlay_scaffold(project, template, substitutions)
2253
+ if copied:
2254
+ click.echo(f" Copied {copied} scaffold file(s) (docs/, governance/, playbooks/, ...)")
2255
+
2256
+ # 7b. Compose .coding-os/ configs (rag/scrumban/domain) from base + every
2257
+ # installed stack — deep-merged, multi-stack-correct. The overlay (step 7)
2258
+ # deliberately skips these. SSOT: docs/engineering/config-composition.md.
2259
+ config_conflicts: list[str] = []
2260
+ composed = compose_coding_os_configs(
2261
+ project, state, list(template), templates_dir=TEMPLATES_DIR, conflicts=config_conflicts
2262
+ )
2263
+ if composed:
2264
+ click.echo(f" Composed {len(composed)} .coding-os config(s): {', '.join(composed)}")
2265
+ for line in config_conflicts:
2266
+ click.echo(f" WARN: config conflict (later wins) — {line}", err=True)
2267
+
2268
+ # 8. Copy thinking_os reference doc from src/core/docs/
2269
+ _copy_workflow_docs(project)
2270
+
2271
+ # 9. Copy Makefile.base verbatim. The `cos` CLI binary (installed
2272
+ # via `uv tool install`) owns path discovery — Makefile.base calls
2273
+ # `cos docs-index`, `cos task-sync`, etc. and stays fully portable.
2274
+ makefile_src = TEMPLATES_DIR / "_base" / "Makefile.base"
2275
+ if makefile_src.exists():
2276
+ makefile_dest = state / "Makefile.base"
2277
+ shutil.copy2(makefile_src, makefile_dest)
2278
+ click.echo(f" Copied Makefile.base to {STATE_DIR}/")
2279
+
2280
+ # Materialize stack-contributed targets (lint-backend, test-backend-<id>,
2281
+ # …) into a generated include so the suites named in AGENTS.md are
2282
+ # runnable. Writes .coding-os/Makefile.stacks; the project Makefile (if
2283
+ # it already exists) gets the `-include` wired in idempotently.
2284
+ materialize_makefile_targets(project, state, world)
2285
+
2286
+ # Create a project Makefile if none exists
2287
+ project_makefile = project / "Makefile"
2288
+ if not project_makefile.exists():
2289
+ project_makefile.write_text(
2290
+ f"# Project Makefile\n"
2291
+ f"# coding-os universal targets\n"
2292
+ f"include {STATE_DIR}/Makefile.base\n"
2293
+ f"-include {STATE_DIR}/Makefile.stacks\n\n"
2294
+ f"# Add your project-specific targets below:\n\n"
2295
+ )
2296
+ click.echo(" Generated Makefile")
2297
+
2298
+ # CI workflow + backend Dockerfiles — gated behind the `cicd` module (off in
2299
+ # lean profiles), independent of the Makefile.base copy so init mirrors the
2300
+ # update.py materialize step. Both delegate to generated artifacts.
2301
+ from cli.subsystems import module_state
2302
+
2303
+ if module_state(project).get("cicd", True):
2304
+ if materialize_ci_workflow(project, world):
2305
+ click.echo(" Generated .github/workflows/ci.yml")
2306
+ if materialize_dockerfiles(project, world):
2307
+ click.echo(" Generated backend Dockerfile(s)")
2308
+
2309
+ # 9b. Aggregate scaffold-boundary.yaml from every installed stack so the
2310
+ # consumer-side enforce-scaffold-boundary.sh hook can enforce subtree
2311
+ # isolation at runtime. SSOT spec: docs/governance/scaffold-boundary-contract.md.
2312
+ _aggregate_scaffold_boundaries(project, state, template)
2313
+
2314
+ # 10. Generate AGENTS.md by composing fragments from base + stacks.
2315
+ # No template file is read; the content is assembled by render_agents_md()
2316
+ # from the fragments registered in base.yaml::agents_md_sections (and any
2317
+ # fragments stacks contribute via their own stack.yaml::agents_md_sections).
2318
+ if ensure_agents_md(project, world):
2319
+ click.echo(" Generated AGENTS.md")
2320
+
2321
+ # 11. Initial RAG indexing of the scaffolded docs so `cos_doc_search`
2322
+ # returns hits from the very first session. Without this, the
2323
+ # consumer's document_chunks table is empty until the user runs
2324
+ # `make docs-index` manually — Rule 19 (doc-sync) enforcement is
2325
+ # also effectively off until something hits the FTS index.
2326
+ # Skipped under --no-index: the index lives in the gitignored runtime DB,
2327
+ # so fast/CI/fixture scaffolds (e.g. golden capture) don't pay the
2328
+ # ~15s embedding-model load for output they discard.
2329
+ if do_index:
2330
+ _initial_doc_index(project, state)
2331
+ else:
2332
+ click.echo(" Skipped initial doc index (--no-index)")
2333
+
2334
+ # 11b. Seed the knowledge graph so the Hub Graph tab + cos_graph_* tools work
2335
+ # from the first session with NO manual `cos graph-reindex` (TASK-423). Built
2336
+ # when --index (the default) OR --graph-index is set — the latter lets a fast
2337
+ # --no-index create (the Hub Composer) still get a populated graph (AST walk,
2338
+ # no embedding model), while CI/fixture scaffolds that pass only --no-index
2339
+ # stay graph-free. Inside, it is also gated on the graph module being enabled
2340
+ # — a disabled graph module owns no tools, so building its graph is wasted.
2341
+ if do_index or graph_index:
2342
+ _initial_graph_index(project, state)
2343
+ else:
2344
+ click.echo(" Skipped initial graph index (--no-index)")
2345
+
2346
+ # 12. Register project in the global ~/.coding-os/registry.json so the
2347
+ # Hub web UI (`cos hub`) can enumerate it and serve its sqlite DB.
2348
+ # Skipped when --no-register passed (sandbox fixtures use disposable
2349
+ # temp dirs — registering them creates stale entries doctor then warns
2350
+ # about in hub.project_paths_exist).
2351
+ if no_register:
2352
+ click.echo(" Skipped hub registry write (--no-register)")
2353
+ else:
2354
+ try:
2355
+ from cli.registry import add_project as _registry_add_project
2356
+
2357
+ entry = _registry_add_project(project)
2358
+ click.echo(f" Registered in hub registry: {entry.slug}")
2359
+ except Exception as exc:
2360
+ # Registry is non-fatal — a failed write should not break init.
2361
+ click.echo(f" WARN: could not register project in hub registry: {exc}", err=True)
2362
+ click.echo(
2363
+ " HINT: register later with `cos registry add <project-path>` "
2364
+ "so the hub web UI can see this project",
2365
+ err=True,
2366
+ )
2367
+
2368
+
2369
+ def _initial_doc_index(project: Path, state: Path) -> None:
2370
+ """Seed document_chunks + FTS for a freshly-scaffolded project."""
2371
+ rag_config = state / "rag-config.yaml"
2372
+ if not rag_config.exists():
2373
+ return
2374
+ db_path = state / "coding-os.db"
2375
+ brain_dir = str(CORE_DIR / "thinking_os")
2376
+ code = (
2377
+ "import sys; "
2378
+ f"sys.path.insert(0, {brain_dir!r}); "
2379
+ "from database import init_db; "
2380
+ "from doc_indexer import index_docs; "
2381
+ "from pathlib import Path; "
2382
+ f"conn = init_db({str(db_path)!r}); "
2383
+ f"stats = index_docs(conn, Path({str(rag_config)!r}), Path({str(project)!r})); "
2384
+ "conn.close(); "
2385
+ "print(f\" Indexed {stats['updated_files']} doc(s), {stats['new_chunks']} chunk(s)\")"
2386
+ )
2387
+ env = os.environ.copy()
2388
+ env["COS_DB_PATH"] = str(db_path)
2389
+ result = subprocess.run(
2390
+ [sys.executable, "-c", code],
2391
+ env=env,
2392
+ capture_output=True,
2393
+ text=True,
2394
+ )
2395
+ if result.returncode == 0 and result.stdout.strip():
2396
+ click.echo(result.stdout.rstrip())
2397
+ elif result.returncode != 0:
2398
+ # Non-fatal: missing yaml / embeddings extras shouldn't break init.
2399
+ click.echo(
2400
+ f" WARN: initial doc index skipped: {result.stderr.strip().splitlines()[-1] if result.stderr else 'unknown'}",
2401
+ err=True,
2402
+ )
2403
+ click.echo(
2404
+ " HINT: doc search stays empty until indexed — install extras with "
2405
+ "`uv sync --extra rag` in the coding-os checkout, then run `make docs-index` here",
2406
+ err=True,
2407
+ )
2408
+
2409
+
2410
+ def _initial_graph_index(project: Path, state: Path) -> None:
2411
+ """Build the knowledge graph for a fresh project when the graph module is on (TASK-423)."""
2412
+ try:
2413
+ from cli.subsystems import module_state
2414
+
2415
+ if not module_state(project).get("graph", True):
2416
+ click.echo(" Skipped graph index (graph module disabled)")
2417
+ return
2418
+ except Exception:
2419
+ # State unreadable → graph is on by default; fall through and build.
2420
+ pass
2421
+ db_path = state / "coding-os.db"
2422
+ core_path = str(CORE_DIR)
2423
+ brain_dir = str(CORE_DIR / "thinking_os")
2424
+ # include_docs=False: the docs RAG layer was just seeded by
2425
+ # _initial_doc_index; here we want only the graph (AST + doc structure),
2426
+ # which needs no embedding model. Runs in-process python (sys.executable),
2427
+ # NOT the global `cos`, so an env without the graph deps fails fast instead
2428
+ # of doing heavy work (mirrors _initial_doc_index).
2429
+ code = (
2430
+ "import sys; "
2431
+ f"sys.path.insert(0, {core_path!r}); "
2432
+ f"sys.path.insert(0, {brain_dir!r}); "
2433
+ "from graph_os.ingest.base import walk_local; "
2434
+ "from graph_os.tools.reindex_dispatch import dispatch; "
2435
+ f"plan = walk_local({str(project)!r}); "
2436
+ "reports = [dispatch(str(f), project_root="
2437
+ f"{str(project)!r}, db_path={str(db_path)!r}, "
2438
+ "include_docs=False, link_stubs=True) for f in plan.files]; "
2439
+ "ok = sum(1 for r in reports if r.get('status') == 'ok'); "
2440
+ "print(f' Built knowledge graph: {ok}/{len(reports)} file(s) indexed')"
2441
+ )
2442
+ env = os.environ.copy()
2443
+ env["COS_DB_PATH"] = str(db_path)
2444
+ # Bounded so a very large repo never blows the init budget (the Hub Composer
2445
+ # wraps `cos init` in its own timeout). On timeout the graph is left empty —
2446
+ # valid, since cos_graph_export returns ok([]) for an empty graph — with a
2447
+ # clear repair HINT, far better than hard-failing a half-created project.
2448
+ timeout_s = int(os.environ.get("COS_INIT_GRAPH_TIMEOUT", "180"))
2449
+ try:
2450
+ result = subprocess.run(
2451
+ [sys.executable, "-c", code],
2452
+ env=env,
2453
+ capture_output=True,
2454
+ text=True,
2455
+ timeout=timeout_s,
2456
+ )
2457
+ except subprocess.TimeoutExpired:
2458
+ click.echo(
2459
+ f" WARN: initial graph index exceeded {timeout_s}s — graph left empty", err=True
2460
+ )
2461
+ click.echo(" HINT: graph stays empty until built — run `cos graph-reindex` here", err=True)
2462
+ return
2463
+ if result.returncode == 0 and result.stdout.strip():
2464
+ click.echo(result.stdout.rstrip())
2465
+ elif result.returncode != 0:
2466
+ # Non-fatal: missing graph deps shouldn't break init.
2467
+ detail = result.stderr.strip().splitlines()[-1] if result.stderr.strip() else "unknown"
2468
+ click.echo(f" WARN: initial graph index skipped: {detail}", err=True)
2469
+ click.echo(
2470
+ " HINT: graph stays empty until built — run `cos graph-reindex` here",
2471
+ err=True,
2472
+ )
2473
+
2474
+
2475
+ @cli.command("add-adapter")
2476
+ @click.argument("agent", type=click.Choice(VALID_AGENTS))
2477
+ @click.option("--project-dir", "-d", default=".", help="Project directory")
2478
+ def add_adapter(agent: str, project_dir: str) -> None:
2479
+ """Add an additional agent adapter to the project."""
2480
+ project = _resolve_project_dir(project_dir)
2481
+ config = _load_config(project)
2482
+
2483
+ if not config:
2484
+ click.echo("ERROR: No .coding-os.yaml found. Run 'coding-os init' first.", err=True)
2485
+ sys.exit(1)
2486
+
2487
+ agents = config.get("agents", [])
2488
+ if agent in agents:
2489
+ click.echo(f"Adapter '{agent}' is already installed.")
2490
+ return
2491
+
2492
+ click.echo(f"Adding {agent} adapter...")
2493
+ _run_adapter_install(agent, project)
2494
+
2495
+ agents.append(agent)
2496
+ config["agents"] = agents
2497
+ _save_config(project, config)
2498
+ click.echo(f" Updated {CONFIG_FILE}")
2499
+
2500
+ # AGENTS.md is the canonical per-project instruction file (read by both
2501
+ # Claude and Codex). `cos init` generates it, but older projects or
2502
+ # partial installs may be missing it — fill the gap so the newly added
2503
+ # adapter has something to read on first session.
2504
+ templates = tuple(config.get("templates", []) or [])
2505
+ world = _build_world(agent, templates, project)
2506
+ if ensure_agents_md(project, world):
2507
+ click.echo(" Generated AGENTS.md")
2508
+
2509
+
2510
+ @cli.command("codex-mcp-install")
2511
+ @click.option(
2512
+ "--config",
2513
+ "config_path",
2514
+ default=None,
2515
+ help="Codex config file (default: ./.codex/config.toml)",
2516
+ )
2517
+ @click.option(
2518
+ "--global",
2519
+ "global_scope",
2520
+ is_flag=True,
2521
+ default=False,
2522
+ help="Write to ~/.codex/config.toml instead of the project-local .codex/config.toml",
2523
+ )
2524
+ @click.option("--dry-run", is_flag=True, default=False, help="Print the snippet without writing")
2525
+ def codex_mcp_install(config_path: str | None, global_scope: bool, dry_run: bool) -> None:
2526
+ """Register the coding-os MCP server in Codex config.
2527
+
2528
+ Codex CLI supports both user-level ~/.codex/config.toml and trusted
2529
+ project overrides in .codex/config.toml. This command defaults to the
2530
+ project-local config so coding-os MCP stays scoped to the current repo;
2531
+ pass `--global` only when you explicitly want the server available
2532
+ everywhere. Safe to re-run — it repairs or replaces the
2533
+ `[mcp_servers.coding-os]` section idempotently.
2534
+
2535
+ Uses append-based text edits (no TOML parser required) so it works on
2536
+ Python 3.10 and preserves any hand-authored comments in config.toml.
2537
+ """
2538
+ if config_path and global_scope:
2539
+ raise click.ClickException("use either --config or --global, not both")
2540
+
2541
+ default_path = (
2542
+ Path.home() / ".codex" / "config.toml"
2543
+ if global_scope
2544
+ else Path.cwd() / ".codex" / "config.toml"
2545
+ )
2546
+ target = Path(config_path).expanduser().resolve() if config_path else default_path
2547
+
2548
+ has_cos = shutil.which("cos") is not None
2549
+ if has_cos:
2550
+ snippet = '\n[mcp_servers.coding-os]\ncommand = "cos"\nargs = ["server-start"]\n'
2551
+ command = "cos"
2552
+ args = ["server-start"]
2553
+ else:
2554
+ server_py = core_dir("thinking_os", "server.py").as_posix()
2555
+ python = sys.executable
2556
+ snippet = f'\n[mcp_servers.coding-os]\ncommand = "{python}"\nargs = ["{server_py}"]\n'
2557
+ command = python
2558
+ args = [server_py]
2559
+
2560
+ if dry_run:
2561
+ click.echo(f"# Would append to {target}:")
2562
+ click.echo(snippet.rstrip())
2563
+ return
2564
+
2565
+ # Locate the adapter that ships an MCP-helper script. This is the
2566
+ # codex adapter by design — discovered via registry metadata so the
2567
+ # adapter id is not hardcoded in Python code (tests/test_no_hardcoded_stacks).
2568
+ _helper_profile = next(
2569
+ (p for p in load_adapter_registry(ADAPTERS_DIR).values() if p.mcp_helper),
2570
+ None,
2571
+ )
2572
+ if _helper_profile is None:
2573
+ raise click.ClickException(
2574
+ "no adapter declares mcp_helper in adapter.yaml; cannot install MCP"
2575
+ )
2576
+ helper = _helper_profile.source_dir / _helper_profile.mcp_helper
2577
+ proc = subprocess.run(
2578
+ [sys.executable, str(helper), str(target), command, *args],
2579
+ capture_output=True,
2580
+ text=True,
2581
+ check=False,
2582
+ )
2583
+ if proc.returncode != 0:
2584
+ raise click.ClickException(
2585
+ proc.stderr.strip() or f"failed to configure coding-os MCP in {target}"
2586
+ )
2587
+
2588
+ status = (proc.stdout or "").strip()
2589
+ if status.startswith("already configured"):
2590
+ click.echo(f"Already registered in {target} — no changes made.")
2591
+ return
2592
+
2593
+ click.echo(f"OK: registered coding-os MCP in {target}")
2594
+ click.echo("Reload Codex CLI (or start a new session) to pick up the new server.")
2595
+
2596
+
2597
+ @cli.command()
2598
+ @click.option("--project-dir", "-d", default=".", help="Project directory")
2599
+ def health(project_dir: str) -> None:
2600
+ """Check coding-os health status."""
2601
+ project = _resolve_project_dir(project_dir)
2602
+ config = _load_config(project)
2603
+
2604
+ click.echo("Coding OS Health Check")
2605
+ click.echo("=" * 40)
2606
+
2607
+ # Config
2608
+ if config:
2609
+ click.echo(f" Config: OK ({CONFIG_FILE})")
2610
+ click.echo(f" Agents: {', '.join(config.get('agents', []))}")
2611
+ click.echo(f" Templates: {', '.join(config.get('templates', [])) or 'none'}")
2612
+ else:
2613
+ click.echo(" Config: MISSING (run 'coding-os init')")
2614
+ return
2615
+
2616
+ # State dir
2617
+ state = project / config.get("state_dir", STATE_DIR)
2618
+ if state.exists():
2619
+ click.echo(f" State dir: OK ({state.name}/)")
2620
+ else:
2621
+ click.echo(" State dir: MISSING")
2622
+
2623
+ # Database
2624
+ db_path = state / "coding-os.db"
2625
+ if db_path.exists():
2626
+ size_kb = db_path.stat().st_size / 1024
2627
+ click.echo(f" Database: OK ({size_kb:.0f} KB)")
2628
+ else:
2629
+ click.echo(" Database: MISSING")
2630
+
2631
+ # Hooks
2632
+ hooks_dir = CORE_DIR / "hooks"
2633
+ hook_count = len(list(hooks_dir.glob("*.sh"))) if hooks_dir.exists() else 0
2634
+ click.echo(f" Core hooks: {hook_count} scripts")
2635
+
2636
+ # MCP server
2637
+ server_py = CORE_DIR / "thinking_os" / "server.py"
2638
+ if server_py.exists():
2639
+ click.echo(" MCP server: OK")
2640
+ else:
2641
+ click.echo(" MCP server: MISSING")
2642
+
2643
+ click.echo("")
2644
+ click.echo("Run 'coding-os init' to fix any missing components.")
2645
+
2646
+
2647
+ @cli.command()
2648
+ @click.option("--project-dir", "-d", default=".", help="Project directory")
2649
+ def materialize(project_dir: str) -> None:
2650
+ """Convert coding-os symlinks to real files (self-contained project)."""
2651
+ project = _resolve_project_dir(project_dir)
2652
+ materialized = 0
2653
+
2654
+ for root, dirs, files in os.walk(project):
2655
+ for name in files:
2656
+ filepath = Path(root) / name
2657
+ if filepath.is_symlink():
2658
+ target = filepath.resolve()
2659
+ if target.exists():
2660
+ filepath.unlink()
2661
+ shutil.copy2(target, filepath)
2662
+ materialized += 1
2663
+ if materialized % 50 == 0:
2664
+ click.echo(f" … materialized {materialized} symlinks so far", err=True)
2665
+
2666
+ click.echo(f"Materialized {materialized} symlinks to real files.")
2667
+ click.echo("Project is now self-contained.")
2668
+
2669
+
2670
+ def _is_coding_os_symlink(link: Path) -> bool:
2671
+ """True if `link` is coding-os wiring — dangling or resolving into the
2672
+ meta-repo checkout (a user's own symlink elsewhere is left alone)."""
2673
+ try:
2674
+ real = link.resolve()
2675
+ except OSError:
2676
+ return True # broken/cyclic — it was one of ours
2677
+ if not real.exists():
2678
+ return True # dangling: source moved/removed
2679
+ try:
2680
+ real.relative_to(CODING_OS_ROOT.resolve())
2681
+ return True
2682
+ except ValueError:
2683
+ return False
2684
+
2685
+
2686
+ @cli.command()
2687
+ @click.option("--project-dir", "-d", default=".", help="Project directory")
2688
+ @click.option("--yes", "-y", is_flag=True, default=False, help="Skip the confirmation prompt.")
2689
+ def eject(project_dir: str, yes: bool) -> None:
2690
+ """Remove coding-os from a project, keeping your code and docs.
2691
+
2692
+ Deletes coding-os symlinks, the .coding-os/ state dir and the generated
2693
+ AGENTS.md / .coding-os.yaml entrypoints, then deregisters the project. Real
2694
+ files you authored (source, docs, anything that isn't a managed symlink) are
2695
+ never touched. Re-running on an already-ejected project is a no-op.
2696
+ """
2697
+ project = _resolve_project_dir(project_dir)
2698
+ state_dir = project / STATE_DIR
2699
+ config = project / CONFIG_FILE
2700
+ coding_os_links = [
2701
+ p
2702
+ for p in (Path(root) / n for root, _d, fs in os.walk(project) for n in fs)
2703
+ if p.is_symlink() and _is_coding_os_symlink(p)
2704
+ ]
2705
+ # CLAUDE.md is the generated symlink to AGENTS.md (points at a sibling, so
2706
+ # the meta-repo filter above misses it) — treat any symlinked one as ours.
2707
+ claude_md = project / "CLAUDE.md"
2708
+ generated_entrypoints = [config, project / "AGENTS.md"]
2709
+ if claude_md.is_symlink():
2710
+ generated_entrypoints.append(claude_md)
2711
+
2712
+ present = [f for f in generated_entrypoints if f.exists() or f.is_symlink()]
2713
+ if not coding_os_links and not state_dir.exists() and not present:
2714
+ click.echo("No coding-os install found here — nothing to eject.")
2715
+ return
2716
+
2717
+ if not yes and not click.confirm(
2718
+ f"Remove coding-os from {project}? Your code and docs stay.", default=False
2719
+ ):
2720
+ click.echo("Aborted.")
2721
+ return
2722
+
2723
+ for link in coding_os_links:
2724
+ link.unlink()
2725
+ removed_files = 0
2726
+ for f in present:
2727
+ f.unlink()
2728
+ removed_files += 1
2729
+ removed_state = False
2730
+ if state_dir.exists():
2731
+ shutil.rmtree(state_dir)
2732
+ removed_state = True
2733
+
2734
+ # Prune adapter dirs left empty after the symlinks went; keep any that still
2735
+ # hold real (user-authored or materialized) files.
2736
+ kept_dirs: list[str] = []
2737
+ for agent_dir in sorted({p.parent for p in coding_os_links if p.parent.name.startswith(".")}):
2738
+ if agent_dir.is_dir() and not any(agent_dir.iterdir()):
2739
+ agent_dir.rmdir()
2740
+ elif agent_dir.is_dir():
2741
+ kept_dirs.append(agent_dir.name)
2742
+
2743
+ from cli.registry import remove_project
2744
+
2745
+ deregistered = False
2746
+ try:
2747
+ deregistered = remove_project(str(project)) is not None
2748
+ except Exception as exc: # registry is best-effort — never block an eject
2749
+ _logging.getLogger("coding_os.cli").debug("eject: deregister skipped: %s", exc)
2750
+
2751
+ click.echo(f"Ejected coding-os from {project}")
2752
+ click.echo(
2753
+ f" removed: {len(coding_os_links)} symlinks · {removed_files} config file(s)"
2754
+ + (" · .coding-os/ state" if removed_state else "")
2755
+ + (" · global-registry entry" if deregistered else "")
2756
+ )
2757
+ kept = "your source, docs, and any files you authored"
2758
+ if kept_dirs:
2759
+ kept += f" (incl. real files under {', '.join(sorted(set(kept_dirs)))})"
2760
+ click.echo(f" kept: {kept}")
2761
+
2762
+
2763
+ @cli.command("hooks-dir")
2764
+ def hooks_dir() -> None:
2765
+ """Print the path to the core hooks directory."""
2766
+ click.echo(CORE_DIR / "hooks")
2767
+
2768
+
2769
+ @cli.command("hooks-log")
2770
+ @click.option("--project-dir", "-d", default=".", help="Project directory")
2771
+ @click.option("-n", "tail_count", default=50, help="Show last N lines (default 50)")
2772
+ @click.option("--follow", "-f", is_flag=True, default=False, help="Follow new entries (tail -f)")
2773
+ @click.option("--agent", type=str, default=None, help="Filter by agent (claude|codex|unknown)")
2774
+ @click.option("--session", type=str, default=None, help="Filter by session id (substring match)")
2775
+ @click.option("--task", type=str, default=None, help="Filter by task name (substring match)")
2776
+ @click.option("--hook", type=str, default=None, help="Filter by hook name (substring match)")
2777
+ @click.option(
2778
+ "--all",
2779
+ "--verbose",
2780
+ "show_all",
2781
+ is_flag=True,
2782
+ default=False,
2783
+ help="Show lifecycle rows (enter/ok) too; default hides them.",
2784
+ )
2785
+ def hooks_log(
2786
+ project_dir: str,
2787
+ tail_count: int,
2788
+ follow: bool,
2789
+ agent: str | None,
2790
+ session: str | None,
2791
+ task: str | None,
2792
+ hook: str | None,
2793
+ show_all: bool,
2794
+ ) -> None:
2795
+ """Show recent hook activity from .coding-os/.hooks.log.
2796
+
2797
+ Hooks call `cos_log_hook` (from src/core/hooks/cos-env.sh) on fire / block /
2798
+ allow / warn. Every line carries `agent=X session=Y task=Z` identity
2799
+ fields so you can filter by any combination:
2800
+
2801
+ cos hooks-log --agent claude # only claude runs
2802
+ cos hooks-log --session ses-20260418-143638-c769
2803
+ cos hooks-log --task governance-mcp-envelope --hook enforce-
2804
+ cos hooks-log --agent codex --follow # live codex stream
2805
+ cos hooks-log --all # include enter/ok noise
2806
+
2807
+ By default only decision-states (fire/block/warn/paths/reminded/full/
2808
+ debounced/skip/bypass) are shown; lifecycle rows ([enter]/[ok]) are hidden
2809
+ behind --all/--verbose. Filters are AND-ed together (case-sensitive
2810
+ substring match).
2811
+ """
2812
+ project = _resolve_project_dir(project_dir)
2813
+ config = _load_config(project) or {}
2814
+ state = project / config.get("state_dir", STATE_DIR)
2815
+ log_path = state / ".hooks.log"
2816
+
2817
+ if not log_path.exists():
2818
+ click.echo(f"No hook activity yet ({log_path} does not exist).")
2819
+ click.echo(
2820
+ "Hint: hooks log on fire — if you expected events, check"
2821
+ " .claude/settings.json or .codex/hooks.json wiring."
2822
+ )
2823
+ return
2824
+
2825
+ # Lifecycle actions are bookkeeping, not decisions — hide them by default
2826
+ # so `cos hooks-log` surfaces signal (fire/block/warn/...) over noise.
2827
+ lifecycle_actions = {"[enter]", "[ok]"}
2828
+
2829
+ def _is_decision_state(line: str) -> bool:
2830
+ return not any(token in line for token in lifecycle_actions)
2831
+
2832
+ filters: list[str] = []
2833
+ if agent:
2834
+ filters.append(f"agent={agent}")
2835
+ if session:
2836
+ filters.append(f"session={session}")
2837
+ if task:
2838
+ filters.append(f"task={task}")
2839
+ if hook:
2840
+ filters.append(f"[{hook}")
2841
+
2842
+ if follow:
2843
+ tail_cmd = f"tail -f -n {tail_count} {shlex.quote(str(log_path))}"
2844
+ pipe = [tail_cmd]
2845
+ if not show_all:
2846
+ pipe.append("grep --line-buffered -vE '\\[(enter|ok)\\]'")
2847
+ pipe.extend(f"grep -F --line-buffered {shlex.quote(f)}" for f in filters)
2848
+ subprocess.run(["bash", "-c", " | ".join(pipe)])
2849
+ return
2850
+
2851
+ try:
2852
+ lines = log_path.read_text(errors="replace").splitlines()
2853
+ except OSError as exc:
2854
+ click.echo(f"Could not read {log_path}: {exc}", err=True)
2855
+ return
2856
+ matched = [
2857
+ ln for ln in lines if all(f in ln for f in filters) and (show_all or _is_decision_state(ln))
2858
+ ]
2859
+ for ln in matched[-tail_count:]:
2860
+ click.echo(ln)
2861
+
2862
+
2863
+ @cli.command("hooks-list")
2864
+ @click.option("--agent", type=str, default=None, help="Filter by adapter (claude|codex)")
2865
+ @click.option("--category", type=str, default=None, help="Filter by category")
2866
+ @click.option("--phase", type=str, default=None, help="Filter by phase")
2867
+ def hooks_list(agent: str | None, category: str | None, phase: str | None) -> None:
2868
+ """List hooks registered in src/core/hooks/registry.yaml with filters.
2869
+
2870
+ Reads the manifest SSOT and prints a summary. With --agent, filters to
2871
+ hooks whose events fit that adapter's declared capabilities — answers
2872
+ "what enforcement is active for Codex?" without grepping settings.
2873
+ """
2874
+ from cli.hook_renderer import list_hooks_for_agent, load_registry
2875
+
2876
+ registry_path = CORE_DIR / "hooks" / "registry.yaml"
2877
+ if not registry_path.exists():
2878
+ click.echo(f"ERROR: {registry_path} not found", err=True)
2879
+ sys.exit(1)
2880
+
2881
+ entries = load_registry(registry_path)
2882
+ if agent:
2883
+ entries = list_hooks_for_agent(entries, agent, ADAPTERS_DIR)
2884
+ if category:
2885
+ entries = [h for h in entries if h.category == category]
2886
+ if phase:
2887
+ entries = [h for h in entries if str(h.phase) == phase]
2888
+
2889
+ if not entries:
2890
+ click.echo("No hooks match the filters.")
2891
+ return
2892
+
2893
+ by_cat: dict[str, list] = {}
2894
+ for h in entries:
2895
+ by_cat.setdefault(h.category or "uncategorized", []).append(h)
2896
+
2897
+ for cat in sorted(by_cat):
2898
+ click.echo(f"\n[{cat}]")
2899
+ for h in by_cat[cat]:
2900
+ events = ", ".join(
2901
+ f"{e['event']}::{e.get('matcher', '')}".rstrip(":") for e in h.events
2902
+ )
2903
+ click.echo(f" {h.id:30s} phase={h.phase!s:3s} events=[{events}]")
2904
+ if h.description:
2905
+ click.echo(f" {h.description}")
2906
+ click.echo("")
2907
+
2908
+
2909
+ @cli.command("server-start")
2910
+ def server_start() -> None:
2911
+ """Start the thinking_os MCP server (wrapper used by .mcp.json).
2912
+
2913
+ Projects register `cos server-start` in their .mcp.json so the MCP
2914
+ entry stays portable — coding-os location is resolved at call time by
2915
+ whichever `cos` binary is on PATH, not hardcoded per-install.
2916
+
2917
+ Historically this wrapper re-entered `uv run --directory ...`, which
2918
+ dragged in `~/.cache/uv` at every MCP launch. In sandboxed runtimes
2919
+ that cache path may be unreadable, causing MCP startup to fail before
2920
+ the server process even booted. We already have a Python interpreter
2921
+ available — the one running `cos` itself — so execute `server.py`
2922
+ directly with that interpreter instead.
2923
+
2924
+ We still capture the caller's cwd (the real project root the agent
2925
+ launched us from) and export it as COS_DB_PATH / COS_STATE_DIR so the
2926
+ server reads the right DB regardless of its own source location.
2927
+ """
2928
+ server_py = CORE_DIR / "thinking_os" / "server.py"
2929
+ if not server_py.exists():
2930
+ click.echo(f"ERROR: MCP server not found at {server_py}", err=True)
2931
+ sys.exit(1)
2932
+
2933
+ caller_cwd = Path.cwd().resolve()
2934
+ env = os.environ.copy()
2935
+ # Only inject if the caller hasn't already set them — respects
2936
+ # explicit overrides for tests / multi-project setups.
2937
+ env.setdefault(
2938
+ "COS_DB_PATH",
2939
+ str(caller_cwd / STATE_DIR / "coding-os.db"),
2940
+ )
2941
+ env.setdefault(
2942
+ "COS_STATE_DIR",
2943
+ str(caller_cwd / STATE_DIR),
2944
+ )
2945
+
2946
+ # Exec so signals / stdio pass through cleanly (MCP is stdio-based).
2947
+ python = sys.executable
2948
+ os.execvpe(
2949
+ python,
2950
+ [
2951
+ python,
2952
+ str(server_py),
2953
+ ],
2954
+ env,
2955
+ )
2956
+
2957
+
2958
+ @cli.command("session-state")
2959
+ @click.option("--project-dir", "-d", default=".", help="Project directory")
2960
+ def session_state(project_dir: str) -> None:
2961
+ """Show current session gate, task, and skill state."""
2962
+ import time
2963
+
2964
+ from cli.board_commands import _detect_agent_runtime
2965
+
2966
+ project = Path(project_dir).resolve()
2967
+ agent = os.environ.get("COS_AGENT") or _detect_agent_runtime()
2968
+ if not agent:
2969
+ adapters = sorted(load_adapter_registry(ADAPTERS_DIR).keys())
2970
+ if not adapters:
2971
+ click.echo("No adapters registered under src/adapters/.", err=True)
2972
+ sys.exit(1)
2973
+ agent = adapters[0]
2974
+ agent_dir = project / ".coding-os" / agent
2975
+
2976
+ if not agent_dir.exists():
2977
+ click.echo(f"No session state at {agent_dir}")
2978
+ sys.exit(1)
2979
+
2980
+ session_file = agent_dir / "session-id"
2981
+ current_session = session_file.read_text().strip() if session_file.exists() else ""
2982
+
2983
+ def _read_state(path: Path, max_age: int = 7200) -> tuple[str, str]:
2984
+ if not path.exists():
2985
+ return ("none", "")
2986
+ try:
2987
+ content = path.read_text().splitlines()[0] if path.exists() else ""
2988
+ except OSError:
2989
+ return ("error", "")
2990
+ parts = content.split(" ", 1)
2991
+ file_session = parts[0] if parts else ""
2992
+ value = parts[1] if len(parts) > 1 else ""
2993
+ if current_session and file_session and file_session != current_session:
2994
+ return ("session-mismatch", value)
2995
+ age = int(time.time() - path.stat().st_mtime)
2996
+ if age > max_age:
2997
+ return (f"stale ({age // 60}min old, max {max_age // 60}min)", value)
2998
+ return ("valid", value)
2999
+
3000
+ gate_status, gate_val = _read_state(agent_dir / ".thinking_os-gate")
3001
+ task_status, task_val = _read_state(agent_dir / ".task-current")
3002
+ skill_status, skill_val = _read_state(agent_dir / ".active-skill")
3003
+ zoom_status, _ = _read_state(agent_dir / ".zoom-checkpoint")
3004
+ doc_status, _ = _read_state(agent_dir / ".doc-anchor")
3005
+
3006
+ click.echo(f"Session : {current_session or '(unset)'}")
3007
+ click.echo(f"Agent : {agent}")
3008
+ click.echo(f"Gate : {gate_status:30s} {gate_val}")
3009
+ click.echo(f"Zoom : {zoom_status}")
3010
+ click.echo(f"Task : {task_status:30s} {task_val}")
3011
+ click.echo(f"Skill : {skill_status:30s} {skill_val}")
3012
+ click.echo(f"DocAnchor : {doc_status}")
3013
+
3014
+ if "stale" in gate_status or gate_status == "none":
3015
+ click.echo("")
3016
+ click.echo("Gate not valid — next Write/Edit on .py/.ts/.tsx will BLOCK")
3017
+ click.echo(f' Re-record: bash "{agent_dir}/hooks/write-state.sh" \\')
3018
+ click.echo(' .thinking_os-gate "CLEAR 1"')
3019
+ click.echo(" (bare basename auto-routes to $COS_PANEL_DIR via cos_state_path)")
3020
+
3021
+
3022
+ # ---------------------------------------------------------------------------
3023
+ # graph_os subcommand family (`cos graph-*`).
3024
+ # Registration lives in src/cli/graph_commands.py so the main file stays lean.
3025
+ # ---------------------------------------------------------------------------
3026
+ try:
3027
+ from cli import graph_commands as _graph_commands
3028
+
3029
+ _graph_commands.register(cli)
3030
+ except ImportError as _graph_cli_exc: # pragma: no cover — defensive
3031
+ import logging as _logging
3032
+
3033
+ _logging.getLogger("coding_os.cli").debug("graph_os CLI unavailable: %s", _graph_cli_exc)
3034
+
3035
+
3036
+ # ---------------------------------------------------------------------------
3037
+ # DB lifecycle — `cos db-stats`, `cos db-reset`. Spec: docs/playbooks/db-reset.md.
3038
+ # ---------------------------------------------------------------------------
3039
+ try:
3040
+ from cli import db_reset as _db_reset
3041
+
3042
+ _db_reset.register(cli)
3043
+ except ImportError as _db_reset_exc: # pragma: no cover — defensive
3044
+ import logging as _logging
3045
+
3046
+ _logging.getLogger("coding_os.cli").debug("db_reset CLI unavailable: %s", _db_reset_exc)
3047
+
3048
+
3049
+ # ---------------------------------------------------------------------------
3050
+ # S4/S5 — registry + hub CLI. (`cos web` removed: it duplicated
3051
+ # `cos hub start --foreground` — both just call web.server.run_server. Dev
3052
+ # auto-reload lives in `make ui-dev`.)
3053
+ # ---------------------------------------------------------------------------
3054
+ try:
3055
+ from cli.registry import registry_cli as _registry_cli
3056
+
3057
+ cli.add_command(_registry_cli)
3058
+
3059
+ from cli.hub_commands import hub_cli as _hub_cli, service_cli as _service_cli
3060
+
3061
+ cli.add_command(_hub_cli)
3062
+ cli.add_command(_service_cli)
3063
+ except ImportError as _web_cli_exc: # pragma: no cover — defensive
3064
+ import logging as _logging
3065
+
3066
+ _logging.getLogger("coding_os.cli").debug("web CLI unavailable: %s", _web_cli_exc)
3067
+
3068
+
3069
+ if __name__ == "__main__":
3070
+ cli()