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
@@ -0,0 +1,1838 @@
1
+ """core.web.routes.cognition — /api/cognition/* HTTP wrappers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import dataclasses
7
+ import json
8
+ import logging
9
+ import os
10
+ import sqlite3
11
+ import sys
12
+ import time
13
+ from collections.abc import AsyncGenerator
14
+ from pathlib import Path
15
+ from typing import Any, Optional
16
+
17
+ from fastapi import APIRouter, Body, Depends, HTTPException, Query
18
+ from fastapi.responses import StreamingResponse
19
+
20
+ from .._deps import make_metrics_dep, make_rate_limit_dep
21
+ from .._envelope import ENVELOPE_ERROR_RESPONSES, unwrap
22
+ from ._bounded_read import DEFAULT_WINDOW, tail_lines
23
+
24
+ # A trace jsonl can reach GBs (e.g. a long run_await loop). Read only the tail
25
+ # so the viewer shows the most-recent events without OOMing the server. TASK-225.
26
+ _MAX_TRACE_EVENTS = 2000
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ _CORE_DIR = Path(__file__).resolve().parents[3]
31
+ if str(_CORE_DIR) not in sys.path:
32
+ sys.path.insert(0, str(_CORE_DIR))
33
+
34
+ router = APIRouter(prefix="/api/cognition", tags=["cognition"], responses=ENVELOPE_ERROR_RESPONSES)
35
+
36
+
37
+ def _state_dir() -> Path:
38
+ """Resolve the .coding-os state directory.
39
+
40
+ Per-project requests (`/api/p/<slug>/...`) ALWAYS use that project's
41
+ `.coding-os/` — env vars cannot override scope. Otherwise env vars
42
+ win for backwards compatibility with tests + manual overrides.
43
+ """
44
+ from web._project_context import current_project_root, is_explicit_project_scope
45
+
46
+ if is_explicit_project_scope():
47
+ return current_project_root() / ".coding-os"
48
+ base = os.environ.get("COS_STATE_DIR") or os.environ.get("COS_AGENT_DIR")
49
+ if base:
50
+ return Path(base).resolve()
51
+ return current_project_root() / ".coding-os"
52
+
53
+
54
+ def _cognition_module():
55
+ """Lazy import for cognition tools."""
56
+ try:
57
+ tos_dir = _CORE_DIR / "thinking_os"
58
+ if str(tos_dir) not in sys.path:
59
+ sys.path.insert(0, str(tos_dir))
60
+ from tools import cognition as _cog # type: ignore
61
+
62
+ return _cog
63
+ except ImportError:
64
+ return None
65
+
66
+
67
+ def _unavailable(msg: str = "cognition tools not available"):
68
+ return json.dumps(
69
+ {
70
+ "ok": False,
71
+ "error": {"category": "unavailable", "retryable": False, "message": msg},
72
+ }
73
+ )
74
+
75
+
76
+ def _auto_route_model(prompt: str) -> dict:
77
+ """Deterministic Auto-model triage (hub-architecture.md § Hub settings
78
+ contract): classify the prompt, prefer cos_route_model's empirical pick
79
+ when history exists, else the settings' orchestrator_model."""
80
+ from .settings import _load as _load_hub_settings
81
+
82
+ routing_cfg = _load_hub_settings().get("model_routing") or {}
83
+ if not routing_cfg.get("enabled"):
84
+ return {"error": "model 'auto' requires settings.model_routing.enabled"}
85
+
86
+ cog = _cognition_module()
87
+ complexity = "COMPLICATED"
88
+ if cog is not None and hasattr(cog, "classify_prompt_heuristic"):
89
+ complexity = cog.classify_prompt_heuristic(prompt)["complexity"]
90
+
91
+ routed = ""
92
+ source = "orchestrator_default"
93
+ try:
94
+ from thinking_os.database import resolve_db_path # type: ignore
95
+ from tools.routing import route_model # type: ignore
96
+
97
+ conn = sqlite3.connect(str(resolve_db_path()))
98
+ try:
99
+ recommendation = route_model(conn, complexity=complexity)
100
+ finally:
101
+ conn.close()
102
+ if int(recommendation.get("data_points") or 0) > 0:
103
+ routed = str(recommendation.get("recommended_model") or "")
104
+ source = "empirical"
105
+ except Exception as exc:
106
+ logger.debug("auto-route empirical lookup failed: %s", exc)
107
+
108
+ if not routed:
109
+ routed = str(routing_cfg.get("orchestrator_model") or "")
110
+ source = "orchestrator_default"
111
+ return {"model": routed, "complexity": complexity, "source": source}
112
+
113
+
114
+ def _enrich_trace_row(row: dict) -> dict:
115
+ """Augment a session row with cheap trace stats (event_count, first_kind).
116
+
117
+ Every row returns the same shape — frontend `TraceList` indexes
118
+ `mtime_ts`, `event_count`, `first_event_kind` unconditionally; missing
119
+ fields make the rows render with broken relative-time and "0ev"
120
+ placeholders. Session-only entries (no jsonl yet) get derived stats:
121
+ - mtime_ts = newest activity timestamp from the session.json
122
+ - event_count = 0
123
+ - first_event_kind = None
124
+ """
125
+ trace_path = row.get("trace_path")
126
+ enriched = dict(row)
127
+ enriched.setdefault("event_count", 0)
128
+ enriched.setdefault("first_event_kind", None)
129
+
130
+ if trace_path:
131
+ p = Path(trace_path)
132
+ event_count = 0
133
+ first_kind: str | None = None
134
+ try:
135
+ with p.open("r", encoding="utf-8", errors="ignore") as fh:
136
+ for i, line in enumerate(fh):
137
+ stripped = line.strip()
138
+ if not stripped:
139
+ continue
140
+ event_count += 1
141
+ if first_kind is None:
142
+ try:
143
+ payload = json.loads(stripped)
144
+ first_kind = payload.get("kind") if isinstance(payload, dict) else None
145
+ except (json.JSONDecodeError, ValueError):
146
+ first_kind = "raw"
147
+ if i > 5000:
148
+ break # cap — prevent pathological scans
149
+ except OSError as exc:
150
+ logger.debug("trace scan skipped %s: %s", p, exc)
151
+ enriched["path"] = trace_path # legacy alias kept for older clients
152
+ enriched["event_count"] = event_count
153
+ enriched["first_event_kind"] = first_kind
154
+
155
+ # Pick the most-recent timestamp available — same rule as the
156
+ # /api/cognition/traces sort key (max, not first-truthy).
157
+ candidates = [
158
+ row.get("modified_ts"),
159
+ row.get("last_tool_at"),
160
+ row.get("last_prompt_at"),
161
+ row.get("last_stop_at"),
162
+ row.get("started_at"),
163
+ ]
164
+ fresh = max((float(c) for c in candidates if c is not None), default=0.0)
165
+ enriched["mtime_ts"] = int(fresh) if fresh > 0 else None
166
+ return enriched
167
+
168
+
169
+ @router.get("/traces")
170
+ def list_traces(
171
+ agent: str | None = Query(None, description="Agent name (e.g. 'claude')"),
172
+ _rl=Depends(make_rate_limit_dep("cognition.traces")),
173
+ _m=Depends(make_metrics_dep("cognition.traces")),
174
+ ):
175
+ """List trace + session-only entries with activity sort."""
176
+ from web.routes.observability import _scan_sessions # type: ignore
177
+
178
+ state = _state_dir()
179
+ if not state.exists():
180
+ return unwrap(
181
+ json.dumps(
182
+ {
183
+ "ok": True,
184
+ "data": {
185
+ "sessions": [],
186
+ "count": 0,
187
+ "trace_count": 0,
188
+ "session_count": 0,
189
+ "meta": {"layer": "cognition"},
190
+ },
191
+ }
192
+ )
193
+ )
194
+
195
+ rows = _scan_sessions(state, agent_filter=agent)
196
+ enriched = [_enrich_trace_row(r) for r in rows]
197
+ trace_count = sum(1 for r in enriched if r.get("has_trace"))
198
+ session_count = sum(1 for r in enriched if r.get("source") in ("session-only", "trace+session"))
199
+
200
+ return unwrap(
201
+ json.dumps(
202
+ {
203
+ "ok": True,
204
+ "data": {
205
+ "sessions": enriched,
206
+ "count": len(enriched),
207
+ "trace_count": trace_count,
208
+ "session_count": session_count,
209
+ "meta": {"layer": "cognition"},
210
+ },
211
+ }
212
+ )
213
+ )
214
+
215
+
216
+ def _safe_seg(seg: str | None) -> bool:
217
+ # A path segment (session_id / agent) is safe iff it is a non-empty run of
218
+ # [A-Za-z0-9_-] — rejects '/', '..', and any traversal before it reaches a
219
+ # filesystem join. Session ids are ses-<agent>-<ts>-<pid>; agents are alnum.
220
+ return bool(seg) and all(c.isalnum() or c in "-_" for c in seg)
221
+
222
+
223
+ def _find_trace_file(
224
+ state: Path, session_id: str, agent: str | None
225
+ ) -> tuple[Path | None, str | None]:
226
+ """Locate a session's jsonl trace file across all (or one) agent dir."""
227
+ if not _safe_seg(session_id) or (agent is not None and not _safe_seg(agent)):
228
+ return (None, None)
229
+ if agent:
230
+ candidate = state / agent / "traces" / f"{session_id}.jsonl"
231
+ return (candidate, agent) if candidate.exists() else (None, agent)
232
+ if not state.is_dir():
233
+ return (None, None)
234
+ for agent_dir in state.iterdir():
235
+ if not agent_dir.is_dir():
236
+ continue
237
+ candidate = agent_dir / "traces" / f"{session_id}.jsonl"
238
+ if candidate.exists():
239
+ return (candidate, agent_dir.name)
240
+ return (None, None)
241
+
242
+
243
+ def _find_session_meta(
244
+ state: Path, session_id: str, agent: str | None
245
+ ) -> tuple[dict | None, str | None]:
246
+ """Locate a session's .json metadata across all (or one) agent dir."""
247
+ if not _safe_seg(session_id) or (agent is not None and not _safe_seg(agent)):
248
+ return (None, None)
249
+ if agent:
250
+ p = state / agent / "sessions" / f"{session_id}.json"
251
+ if p.exists():
252
+ try:
253
+ return json.loads(p.read_text(encoding="utf-8")), agent
254
+ except (OSError, json.JSONDecodeError):
255
+ return None, agent
256
+ return None, agent
257
+ if not state.is_dir():
258
+ return None, None
259
+ for agent_dir in state.iterdir():
260
+ if not agent_dir.is_dir():
261
+ continue
262
+ p = agent_dir / "sessions" / f"{session_id}.json"
263
+ if p.exists():
264
+ try:
265
+ return json.loads(p.read_text(encoding="utf-8")), agent_dir.name
266
+ except (OSError, json.JSONDecodeError):
267
+ return None, agent_dir.name
268
+ return None, None
269
+
270
+
271
+ @router.get("/trace/{session_id}")
272
+ def get_trace(
273
+ session_id: str,
274
+ agent: str | None = Query(None),
275
+ _rl=Depends(make_rate_limit_dep("cognition.trace")),
276
+ _m=Depends(make_metrics_dep("cognition.trace")),
277
+ ):
278
+ """Read trace events for a session; fall back to session metadata when no jsonl yet."""
279
+ state = _state_dir()
280
+ target, resolved_agent = _find_trace_file(state, session_id, agent)
281
+ session_meta, meta_agent = _find_session_meta(state, session_id, agent)
282
+
283
+ if target is None and session_meta is None:
284
+ raise HTTPException(
285
+ status_code=404,
286
+ detail=f"trace {session_id!r} not found (no jsonl trace, no session.json metadata)",
287
+ )
288
+
289
+ events: list[dict] = []
290
+ trace_truncated = False
291
+ if target is not None:
292
+ # Tail-read only the last _MAX_TRACE_EVENTS lines (≤256KB window) so a
293
+ # multi-GB trace cannot OOM the server or the response.
294
+ lines, trace_truncated = tail_lines(target, max_lines=_MAX_TRACE_EVENTS)
295
+ for line in lines:
296
+ stripped = line.strip()
297
+ if not stripped:
298
+ continue
299
+ try:
300
+ events.append(json.loads(stripped))
301
+ except json.JSONDecodeError:
302
+ events.append({"raw": stripped})
303
+
304
+ return unwrap(
305
+ json.dumps(
306
+ {
307
+ "ok": True,
308
+ "data": {
309
+ "session_id": session_id,
310
+ "agent": resolved_agent or meta_agent,
311
+ "events": events,
312
+ "count": len(events),
313
+ "truncated": trace_truncated,
314
+ "session": session_meta,
315
+ "has_trace": target is not None,
316
+ "source": "trace+session"
317
+ if target and session_meta
318
+ else ("trace-only" if target else "session-only"),
319
+ "meta": {"layer": "cognition"},
320
+ },
321
+ }
322
+ )
323
+ )
324
+
325
+
326
+ def _drain_trace_events(log: Path, pos: int) -> tuple[list[dict[str, Any]], int]:
327
+ # Blocking stat + read (run on a worker thread by the SSE loop) — mirrors
328
+ # hooks.stream so a burst of trace lines never stalls the event loop.
329
+ if not log.exists():
330
+ return [], pos
331
+ size = log.stat().st_size
332
+ if size < pos:
333
+ pos = 0 # rotated
334
+ if size <= pos:
335
+ return [], pos
336
+ with log.open("r", encoding="utf-8", errors="ignore") as fh:
337
+ fh.seek(pos)
338
+ chunk = fh.read()
339
+ pos = fh.tell()
340
+ parsed: list[dict[str, Any]] = []
341
+ for line in chunk.splitlines():
342
+ stripped = line.strip()
343
+ if not stripped:
344
+ continue
345
+ try:
346
+ parsed.append(json.loads(stripped))
347
+ except json.JSONDecodeError:
348
+ parsed.append({"raw": stripped})
349
+ return parsed, pos
350
+
351
+
352
+ def _initial_trace_pos(log: Path, max_bytes: int = DEFAULT_WINDOW) -> int:
353
+ """Byte offset for a bounded initial replay: the start of the first whole
354
+ line within the last `max_bytes`. A long session's trace jsonl can reach
355
+ many MB; replaying it whole on connect would read the entire file into
356
+ memory (the same DoS the bounded-read helper guards for the non-streaming
357
+ reads). Returns 0 when the file fits the window or cannot be stat'd."""
358
+ try:
359
+ size = log.stat().st_size
360
+ except OSError:
361
+ return 0
362
+ if size <= max_bytes:
363
+ return 0
364
+ try:
365
+ with log.open("rb") as fh:
366
+ fh.seek(size - max_bytes)
367
+ fh.readline() # discard the partial leading line
368
+ return fh.tell()
369
+ except OSError:
370
+ return 0
371
+
372
+
373
+ @router.get("/trace/{session_id}/stream")
374
+ async def stream_trace(
375
+ session_id: str,
376
+ agent: str | None = Query(None),
377
+ _rl=Depends(make_rate_limit_dep("cognition.trace_stream")),
378
+ _m=Depends(make_metrics_dep("cognition.trace_stream")),
379
+ ):
380
+ """SSE: tail (and replay from the start) a session's append-only cognition trace jsonl."""
381
+ if not _safe_seg(session_id) or (agent is not None and not _safe_seg(agent)):
382
+ raise HTTPException(status_code=400, detail="invalid session_id or agent")
383
+ state = _state_dir()
384
+ target, _resolved_agent = _find_trace_file(state, session_id, agent)
385
+ # The dispatch may not have created the file yet — resolve the canonical
386
+ # path so tailing begins the instant the first event lands. Segments are
387
+ # validated above, so this join cannot escape the state dir.
388
+ log = (
389
+ target
390
+ if target is not None
391
+ else state / (agent or "claude") / "traces" / f"{session_id}.jsonl"
392
+ )
393
+ poll_secs = float(os.environ.get("COS_TRACE_STREAM_POLL_MS", "750")) / 1000.0
394
+ heartbeat_secs = 15.0
395
+ idle_terminate_secs = float(os.environ.get("COS_TRACE_STREAM_IDLE_TERMINATE_S", "30"))
396
+
397
+ async def gen() -> AsyncGenerator[bytes, None]:
398
+ yield f"event: connected\ndata: {json.dumps({'session_id': session_id})}\n\n".encode()
399
+ # Replay from a bounded tail window so a viewer connecting mid-run sees
400
+ # the recent trace, then keeps tailing new lines. Starting at 0 would
401
+ # read a multi-MB trace whole on connect (worker-memory spike).
402
+ pos = _initial_trace_pos(log)
403
+ last_beat = time.monotonic()
404
+ last_event = time.monotonic()
405
+ saw_completed = False
406
+ try:
407
+ while True:
408
+ events, pos = await asyncio.to_thread(_drain_trace_events, log, pos)
409
+ for evt in events:
410
+ yield f"event: trace\ndata: {json.dumps(evt, ensure_ascii=False)}\n\n".encode()
411
+ if events:
412
+ last_event = time.monotonic()
413
+ if any(
414
+ isinstance(e, dict) and e.get("kind") == "dispatch_completed"
415
+ for e in events
416
+ ):
417
+ saw_completed = True
418
+ # Self-terminate once a completion has been seen AND the trace has
419
+ # been idle for a grace period. A supervisor session emits one
420
+ # dispatch_completed per role in a chain, so terminating on the
421
+ # FIRST would drop later roles — the idle window ends a lone
422
+ # sub-session promptly while letting a multi-role run keep going.
423
+ elif saw_completed and (time.monotonic() - last_event) > idle_terminate_secs:
424
+ yield b"event: done\ndata: {}\n\n"
425
+ return
426
+ if time.monotonic() - last_beat > heartbeat_secs:
427
+ yield f"event: heartbeat\ndata: {json.dumps({'ts': int(time.time())})}\n\n".encode()
428
+ last_beat = time.monotonic()
429
+ await asyncio.sleep(poll_secs)
430
+ except asyncio.CancelledError:
431
+ raise
432
+ except Exception as exc:
433
+ logger.exception("trace stream failed")
434
+ yield f"event: error\ndata: {json.dumps({'message': str(exc)})}\n\n".encode()
435
+
436
+ return StreamingResponse(
437
+ gen(),
438
+ media_type="text/event-stream",
439
+ headers={
440
+ "Cache-Control": "no-cache",
441
+ "X-Accel-Buffering": "no",
442
+ "Connection": "keep-alive",
443
+ },
444
+ )
445
+
446
+
447
+ # ---------------------------------------------------------------------------
448
+ # T2.4 / T19.1 — Dispatcher cost panel
449
+ # ---------------------------------------------------------------------------
450
+
451
+
452
+ def _db_path() -> str | None:
453
+ """Resolve coding-os SQLite DB path via canonical helper.
454
+
455
+ Returns None when nothing exists yet (the route returns a typed
456
+ ``unavailable`` envelope in that case).
457
+ """
458
+ try:
459
+ from thinking_os.database import resolve_db_path # type: ignore
460
+ from web._project_context import current_project_root # type: ignore[import]
461
+
462
+ path = resolve_db_path(current_project_root())
463
+ if path.exists():
464
+ return str(path)
465
+ except Exception as exc:
466
+ logger.debug("project-root db path resolve failed: %s", exc)
467
+ return None
468
+
469
+
470
+ @router.get("/cost")
471
+ def dispatcher_cost_summary(
472
+ formula_id: str | None = Query(None, description="Filter to one formula"),
473
+ limit: int = Query(50, ge=1, le=500),
474
+ _rl=Depends(make_rate_limit_dep("cognition.cost")),
475
+ _m=Depends(make_metrics_dep("cognition.cost")),
476
+ ):
477
+ """Aggregate dispatch cost rolled up by formula and day (T2.4)."""
478
+ db = _db_path()
479
+ if db is None:
480
+ return unwrap(
481
+ json.dumps(
482
+ {
483
+ "ok": True,
484
+ "data": {
485
+ "rows": [],
486
+ "total_usd": 0.0,
487
+ "count": 0,
488
+ "meta": {"layer": "cognition"},
489
+ },
490
+ }
491
+ )
492
+ )
493
+
494
+ try:
495
+ params: list = []
496
+ where = "WHERE cost_usd IS NOT NULL"
497
+ if formula_id:
498
+ where += " AND formula_id = ?"
499
+ params.append(formula_id)
500
+ query_sql = (
501
+ f"SELECT formula_id, date(ts) as day, "
502
+ f"SUM(cost_usd) as total_cost_usd, COUNT(*) as count, "
503
+ f"AVG(latency_ms) as avg_latency_ms "
504
+ f"FROM formula_dispatches {where} "
505
+ f"GROUP BY formula_id, day "
506
+ f"ORDER BY day DESC, total_cost_usd DESC "
507
+ f"LIMIT ?"
508
+ )
509
+ params.append(limit)
510
+ with sqlite3.connect(db) as conn:
511
+ conn.row_factory = sqlite3.Row
512
+ rows = [dict(r) for r in conn.execute(query_sql, params).fetchall()]
513
+ total_usd = sum(r["total_cost_usd"] or 0 for r in rows)
514
+ except Exception as exc:
515
+ return unwrap(
516
+ json.dumps(
517
+ {
518
+ "ok": False,
519
+ "error": {"category": "internal", "retryable": False, "message": str(exc)},
520
+ }
521
+ )
522
+ )
523
+
524
+ return unwrap(
525
+ json.dumps(
526
+ {
527
+ "ok": True,
528
+ "data": {
529
+ "rows": rows,
530
+ "total_usd": round(total_usd, 6),
531
+ "count": len(rows),
532
+ "meta": {"layer": "cognition"},
533
+ },
534
+ }
535
+ )
536
+ )
537
+
538
+
539
+ @router.get("/cost/health")
540
+ def dispatcher_cost_health(
541
+ _rl=Depends(make_rate_limit_dep("cognition.cost_health")),
542
+ _m=Depends(make_metrics_dep("cognition.cost_health")),
543
+ ):
544
+ """Cost-health gauges over formula_dispatches: MAD anomaly, burn-rate, budget ladder."""
545
+ empty = {
546
+ "anomaly": {"ok": True, "n": 0, "outliers": []},
547
+ "burn": {"days": 0},
548
+ "budget": {"level": "ok", "cap_usd": None, "spent_usd": 0.0, "allowed": True},
549
+ "overall_ok": True,
550
+ "meta": {"layer": "cognition"},
551
+ }
552
+ db = _db_path()
553
+ if db is None:
554
+ return unwrap(json.dumps({"ok": True, "data": empty}))
555
+ try:
556
+ from thinking_os import budget
557
+
558
+ anomaly = budget.cost_anomaly(db)
559
+ burn = budget.cost_burn_rate(db)
560
+ gate = budget.check(db)
561
+ data = {
562
+ "anomaly": anomaly,
563
+ "burn": burn,
564
+ "budget": {
565
+ "level": gate.level,
566
+ "cap_usd": gate.cap_usd,
567
+ "spent_usd": round(gate.spent_usd, 6),
568
+ "allowed": gate.allowed,
569
+ },
570
+ "overall_ok": bool(anomaly.get("ok", True) and gate.level != "hard_stop"),
571
+ "meta": {"layer": "cognition"},
572
+ }
573
+ return unwrap(json.dumps({"ok": True, "data": data}))
574
+ except Exception as exc:
575
+ logger.debug("cost/health failed, failing open: %s", exc)
576
+ return unwrap(json.dumps({"ok": True, "data": empty}))
577
+
578
+
579
+ @router.get("/dispatchers")
580
+ def list_dispatchers(
581
+ limit: int = Query(100, ge=1, le=1000),
582
+ status: str | None = Query(None),
583
+ _rl=Depends(make_rate_limit_dep("cognition.dispatchers")),
584
+ _m=Depends(make_metrics_dep("cognition.dispatchers")),
585
+ ):
586
+ """List recent formula dispatches with telemetry (T19.1)."""
587
+ db = _db_path()
588
+ if db is None:
589
+ return unwrap(
590
+ json.dumps(
591
+ {
592
+ "ok": True,
593
+ "data": {"dispatches": [], "count": 0, "meta": {"layer": "cognition"}},
594
+ }
595
+ )
596
+ )
597
+
598
+ try:
599
+ params: list = []
600
+ where = "WHERE cost_usd IS NOT NULL"
601
+ if status:
602
+ where += " AND status = ?"
603
+ params.append(status)
604
+ params.append(limit)
605
+ with sqlite3.connect(db) as conn:
606
+ conn.row_factory = sqlite3.Row
607
+ rows = [
608
+ dict(r)
609
+ for r in conn.execute(
610
+ f"SELECT session_id, formula_id, ts, cost_usd, budget_usd, "
611
+ f"status, latency_ms "
612
+ f"FROM formula_dispatches {where} "
613
+ f"ORDER BY ts DESC LIMIT ?",
614
+ params,
615
+ ).fetchall()
616
+ ]
617
+ except Exception as exc:
618
+ return unwrap(
619
+ json.dumps(
620
+ {
621
+ "ok": False,
622
+ "error": {"category": "internal", "retryable": False, "message": str(exc)},
623
+ }
624
+ )
625
+ )
626
+
627
+ return unwrap(
628
+ json.dumps(
629
+ {
630
+ "ok": True,
631
+ "data": {"dispatches": rows, "count": len(rows), "meta": {"layer": "cognition"}},
632
+ }
633
+ )
634
+ )
635
+
636
+
637
+ @router.get("/dispatchers/{session_id}/tools")
638
+ def dispatcher_tools(
639
+ session_id: str,
640
+ _rl=Depends(make_rate_limit_dep("cognition.dispatcher_tools")),
641
+ _m=Depends(make_metrics_dep("cognition.dispatcher_tools")),
642
+ ):
643
+ """Parse tool_calls_jsonb for one dispatch session (T19.2)."""
644
+ db = _db_path()
645
+ if db is None:
646
+ raise HTTPException(status_code=503, detail="DB not available")
647
+
648
+ try:
649
+ with sqlite3.connect(db) as conn:
650
+ conn.row_factory = sqlite3.Row
651
+ row = conn.execute(
652
+ "SELECT tool_calls_jsonb, tool_failures_jsonb "
653
+ "FROM formula_dispatches WHERE session_id = ? "
654
+ "ORDER BY ts DESC LIMIT 1",
655
+ (session_id,),
656
+ ).fetchone()
657
+ except Exception as exc:
658
+ raise HTTPException(status_code=500, detail=str(exc)) from exc
659
+
660
+ if row is None:
661
+ raise HTTPException(status_code=404, detail=f"session {session_id!r} not found")
662
+
663
+ def _parse(col: str | None) -> list:
664
+ if not col:
665
+ return []
666
+ try:
667
+ parsed = json.loads(col)
668
+ return parsed if isinstance(parsed, list) else []
669
+ except (json.JSONDecodeError, TypeError):
670
+ return []
671
+
672
+ tool_calls = _parse(row["tool_calls_jsonb"])
673
+ failures = _parse(row["tool_failures_jsonb"])
674
+ return unwrap(
675
+ json.dumps(
676
+ {
677
+ "ok": True,
678
+ "data": {
679
+ "session_id": session_id,
680
+ "tool_calls": tool_calls,
681
+ "failures": failures,
682
+ "count": len(tool_calls),
683
+ "meta": {"layer": "cognition"},
684
+ },
685
+ }
686
+ )
687
+ )
688
+
689
+
690
+ @router.get("/analyze")
691
+ def cognition_analyze(
692
+ task_description: str = Query(...),
693
+ complexity_hint: str | None = Query(None),
694
+ _rl=Depends(make_rate_limit_dep("cognition.analyze")),
695
+ _m=Depends(make_metrics_dep("cognition.analyze")),
696
+ ):
697
+ """Analyze a task via cos_analyze_task."""
698
+ cog = _cognition_module()
699
+ if cog is None:
700
+ return unwrap(_unavailable())
701
+ # The cognition module exposes analyze_task directly (not through MCP wrapper).
702
+ try:
703
+ if hasattr(cog, "analyze_task"):
704
+ result = cog.analyze_task(task_description, complexity_hint=complexity_hint)
705
+ return unwrap(result if isinstance(result, str) else json.dumps(result))
706
+ except Exception as exc:
707
+ return unwrap(
708
+ json.dumps(
709
+ {
710
+ "ok": False,
711
+ "error": {"category": "internal", "retryable": False, "message": str(exc)},
712
+ }
713
+ )
714
+ )
715
+ return unwrap(_unavailable("analyze_task not available in this cognition module version"))
716
+
717
+
718
+ # ---------------------------------------------------------------------------
719
+ # Chat surface — Claude Agent SDK transcript browser + resume (TASK-chat)
720
+ # ---------------------------------------------------------------------------
721
+
722
+
723
+ def _claude_sdk():
724
+ """Lazy import the Claude Agent SDK; return None when missing."""
725
+ try:
726
+ import claude_agent_sdk # type: ignore
727
+
728
+ return claude_agent_sdk
729
+ except ImportError as exc:
730
+ logger.debug("claude_agent_sdk unavailable: %s", exc)
731
+ return None
732
+
733
+
734
+ def _project_cwd() -> str:
735
+ from web._project_context import current_project_root
736
+
737
+ return str(current_project_root())
738
+
739
+
740
+ _ADAPTER_DISPATCHER_MOD = None
741
+ _ADAPTER_DISPATCHER_TRIED = False
742
+
743
+
744
+ def _adapter_dispatcher():
745
+ """Load src/adapters/claude/sdk_dispatcher.py once — the adapter SDK-construction
746
+ seam (P8: every ClaudeAgentOptions build crosses this boundary into the adapter)."""
747
+ global _ADAPTER_DISPATCHER_MOD, _ADAPTER_DISPATCHER_TRIED
748
+ if _ADAPTER_DISPATCHER_TRIED:
749
+ return _ADAPTER_DISPATCHER_MOD
750
+ _ADAPTER_DISPATCHER_TRIED = True
751
+ try:
752
+ import importlib.util
753
+ from pathlib import Path
754
+
755
+ path = Path(__file__).resolve().parents[3] / "adapters" / "claude" / "sdk_dispatcher.py"
756
+ spec = importlib.util.spec_from_file_location("cos_adapter_claude_dispatcher", path)
757
+ if spec and spec.loader:
758
+ mod = importlib.util.module_from_spec(spec)
759
+ spec.loader.exec_module(mod)
760
+ _ADAPTER_DISPATCHER_MOD = mod
761
+ except Exception as exc:
762
+ logger.debug("adapter dispatcher load failed: %s", exc)
763
+ return _ADAPTER_DISPATCHER_MOD
764
+
765
+
766
+ def _session_options_builder():
767
+ """The adapter's profile-based session-options builder (SSOT), or None."""
768
+ mod = _adapter_dispatcher()
769
+ return getattr(mod, "claude_session_options", None) if mod else None
770
+
771
+
772
+ def _build_agent_options(**kwargs):
773
+ """Construct ClaudeAgentOptions via the adapter seam — P8: core never builds the
774
+ SDK type itself. Raises if the adapter dispatcher cannot be loaded."""
775
+ mod = _adapter_dispatcher()
776
+ builder = getattr(mod, "claude_agent_options", None) if mod else None
777
+ if builder is None:
778
+ raise RuntimeError("claude adapter ClaudeAgentOptions seam unavailable")
779
+ return builder(**kwargs)
780
+
781
+
782
+ def _chat_session_options(
783
+ profile, *, cwd, model, system_prompt, effort=None, resume=None, fork=False
784
+ ):
785
+ """Build chat ClaudeAgentOptions via the adapter SSOT builder; on builder error
786
+ fall back to the chat-light kwargs, still constructed through the adapter seam."""
787
+ build = _session_options_builder()
788
+ if build is not None:
789
+ try:
790
+ return build(
791
+ profile,
792
+ cwd=cwd,
793
+ model=model,
794
+ system_prompt=system_prompt,
795
+ effort=effort,
796
+ resume=resume,
797
+ fork=fork,
798
+ )
799
+ except Exception as exc:
800
+ logger.debug("session-options builder call failed (%s); generic seam fallback", exc)
801
+ kwargs = dict(
802
+ cwd=cwd,
803
+ model=model,
804
+ permission_mode="dontAsk",
805
+ setting_sources=[],
806
+ include_partial_messages=True,
807
+ system_prompt=system_prompt,
808
+ )
809
+ if effort:
810
+ kwargs["effort"] = effort
811
+ if profile == "chat_resume":
812
+ if resume:
813
+ kwargs["resume"] = resume
814
+ kwargs["fork_session"] = fork
815
+ return _build_agent_options(**kwargs)
816
+
817
+
818
+ _CHAT_PRESENCE_WRITER = None
819
+ _CHAT_PRESENCE_TRIED = False
820
+
821
+
822
+ def _chat_presence_write(cwd: str, sid: str, event: str) -> None:
823
+ """Fire-and-forget Hub-chat presence so the chat shows in the Live-agents HUD (P13)."""
824
+ # Reuse the adapter's unified 12-key writer and stamp the long-lived host
825
+ # pid, so the board's glob reader sees the live chat session (the chat path
826
+ # fires no shell hooks, so nothing else writes its presence).
827
+ global _CHAT_PRESENCE_WRITER, _CHAT_PRESENCE_TRIED
828
+ try:
829
+ if not _CHAT_PRESENCE_TRIED:
830
+ _CHAT_PRESENCE_TRIED = True
831
+ import importlib.util
832
+ from pathlib import Path as _Path
833
+
834
+ path = (
835
+ _Path(__file__).resolve().parents[3] / "adapters" / "claude" / "sdk_dispatcher.py"
836
+ )
837
+ spec = importlib.util.spec_from_file_location("cos_adapter_claude_presence", path)
838
+ if spec and spec.loader:
839
+ mod = importlib.util.module_from_spec(spec)
840
+ spec.loader.exec_module(mod)
841
+ _CHAT_PRESENCE_WRITER = getattr(mod, "_presence_write", None)
842
+ if _CHAT_PRESENCE_WRITER is not None:
843
+ import os
844
+ from pathlib import Path as _Path
845
+
846
+ _CHAT_PRESENCE_WRITER(_Path(cwd), "claude", sid, event, pid=os.getpid())
847
+ except Exception as exc:
848
+ logger.debug("chat presence write skipped (%s): %s", event, exc)
849
+
850
+
851
+ def _serialize_session_info(info: Any) -> dict:
852
+ return {
853
+ "session_id": getattr(info, "session_id", None),
854
+ "summary": getattr(info, "summary", None),
855
+ "custom_title": getattr(info, "custom_title", None),
856
+ "first_prompt": (getattr(info, "first_prompt", None) or "")[:400] or None,
857
+ "last_modified": getattr(info, "last_modified", None),
858
+ "file_size": getattr(info, "file_size", None),
859
+ "git_branch": getattr(info, "git_branch", None),
860
+ "cwd": getattr(info, "cwd", None),
861
+ "tag": getattr(info, "tag", None),
862
+ "created_at": getattr(info, "created_at", None),
863
+ "agent": "claude",
864
+ "writable": True,
865
+ }
866
+
867
+
868
+ def _coerce_block(block: Any) -> dict:
869
+ if not isinstance(block, dict):
870
+ return {"type": "raw", "value": str(block)[:2000]}
871
+ btype = str(block.get("type") or "unknown")
872
+ out: dict = {"type": btype}
873
+ if btype == "text":
874
+ out["text"] = str(block.get("text") or "")
875
+ elif btype == "thinking":
876
+ out["text"] = str(block.get("thinking") or block.get("text") or "")
877
+ elif btype == "tool_use":
878
+ out["id"] = block.get("id")
879
+ out["name"] = block.get("name")
880
+ inp = block.get("input")
881
+ try:
882
+ out["input"] = (
883
+ inp
884
+ if isinstance(inp, (dict, list, str, int, float, bool, type(None)))
885
+ else str(inp)
886
+ )
887
+ except Exception as exc:
888
+ logger.debug("tool_use input coerce fallback: %s", exc)
889
+ out["input"] = str(inp)[:2000]
890
+ elif btype == "tool_result":
891
+ out["tool_use_id"] = block.get("tool_use_id")
892
+ content = block.get("content")
893
+ if isinstance(content, list):
894
+ out["content"] = [
895
+ c.get("text") if isinstance(c, dict) and c.get("type") == "text" else str(c)[:1500]
896
+ for c in content
897
+ ]
898
+ else:
899
+ out["content"] = str(content)[:4000] if content is not None else None
900
+ out["is_error"] = bool(block.get("is_error"))
901
+ elif btype == "image":
902
+ out["source_type"] = (
903
+ block.get("source", {}).get("type") if isinstance(block.get("source"), dict) else None
904
+ )
905
+ else:
906
+ # Catch-all: keep small primitive fields, drop binary noise.
907
+ for k, v in block.items():
908
+ if k == "type":
909
+ continue
910
+ if isinstance(v, (str, int, float, bool, type(None))):
911
+ out[k] = v
912
+ return out
913
+
914
+
915
+ def _serialize_message(msg: Any) -> dict:
916
+ raw = getattr(msg, "message", None)
917
+ if not isinstance(raw, dict):
918
+ raw = {}
919
+ role = raw.get("role") or getattr(msg, "type", None) or "unknown"
920
+ content = raw.get("content")
921
+ blocks: list[dict]
922
+ if isinstance(content, str):
923
+ blocks = [{"type": "text", "text": content}]
924
+ elif isinstance(content, list):
925
+ blocks = [_coerce_block(b) for b in content]
926
+ else:
927
+ blocks = []
928
+ return {
929
+ "uuid": getattr(msg, "uuid", None),
930
+ "session_id": getattr(msg, "session_id", None),
931
+ "type": getattr(msg, "type", None),
932
+ "role": role,
933
+ "model": raw.get("model"),
934
+ "stop_reason": raw.get("stop_reason"),
935
+ "usage": raw.get("usage") if isinstance(raw.get("usage"), dict) else None,
936
+ "blocks": blocks,
937
+ "parent_tool_use_id": getattr(msg, "parent_tool_use_id", None),
938
+ }
939
+
940
+
941
+ def _session_agent_hints(session_id: str) -> set[str]:
942
+ hints: set[str] = set()
943
+ state = _state_dir()
944
+ if not state.is_dir():
945
+ return hints
946
+ for agent_dir in state.iterdir():
947
+ sessions_dir = agent_dir / "sessions"
948
+ if not sessions_dir.is_dir():
949
+ continue
950
+ for path in sessions_dir.glob("*.json"):
951
+ try:
952
+ payload = json.loads(path.read_text(encoding="utf-8"))
953
+ except (OSError, json.JSONDecodeError):
954
+ continue
955
+ if payload.get("sdk_uuid") == session_id:
956
+ hints.add(str(payload.get("agent") or agent_dir.name))
957
+ return hints
958
+
959
+
960
+ @router.get("/chats")
961
+ async def list_chats(
962
+ limit: int = Query(50, ge=1, le=500),
963
+ _rl=Depends(make_rate_limit_dep("cognition.chats")),
964
+ _m=Depends(make_metrics_dep("cognition.chats")),
965
+ ):
966
+ """List chat sessions from every available transcript provider."""
967
+ from web.chat_providers import list_sessions as list_adapter_sessions
968
+
969
+ cwd = _project_cwd()
970
+ adapter_rows, adapter_sources = await list_adapter_sessions(cwd, limit)
971
+ rows_by_id = {str(row["session_id"]): row for row in adapter_rows}
972
+ sources = [f"{agent}_sdk" for agent in adapter_sources]
973
+ sdk = _claude_sdk()
974
+ if sdk is not None:
975
+ try:
976
+ sessions = sdk.list_sessions(directory=cwd, limit=limit)
977
+ except Exception as exc:
978
+ logger.warning("Claude chat list failed: %s", exc)
979
+ else:
980
+ for session in sessions:
981
+ row = _serialize_session_info(session)
982
+ rows_by_id.setdefault(str(row["session_id"]), row)
983
+ sources.append("claude_agent_sdk")
984
+ if not sources:
985
+ return unwrap(_unavailable("no chat transcript provider is available"))
986
+ rows = list(rows_by_id.values())
987
+ rows.sort(key=lambda r: r.get("last_modified") or 0, reverse=True)
988
+ rows = rows[:limit]
989
+ return unwrap(
990
+ json.dumps(
991
+ {
992
+ "ok": True,
993
+ "data": {
994
+ "sessions": rows,
995
+ "count": len(rows),
996
+ "cwd": cwd,
997
+ "meta": {"layer": "cognition", "sources": sources},
998
+ },
999
+ }
1000
+ )
1001
+ )
1002
+
1003
+
1004
+ def _dispatch_transcript_chat(session_id: str) -> dict | None:
1005
+ # Fall back to a dispatched sub-session's persisted transcript when the live
1006
+ # Claude SDK session no longer exists on disk — resolves the dead sdk_uuid
1007
+ # modal link (TASK-667). Keyed on formula_dispatches.sub_session_id (= the
1008
+ # SDK session_id the UI links from). Read-only, fail-open.
1009
+ db_path = _db_path()
1010
+ if not db_path:
1011
+ return None
1012
+ try:
1013
+ with sqlite3.connect(db_path) as conn:
1014
+ conn.row_factory = sqlite3.Row
1015
+ row = conn.execute(
1016
+ "SELECT formula_id, status, model, raw_transcript "
1017
+ "FROM formula_dispatches "
1018
+ "WHERE sub_session_id = ? AND raw_transcript IS NOT NULL "
1019
+ "ORDER BY ts DESC LIMIT 1",
1020
+ (session_id,),
1021
+ ).fetchone()
1022
+ except sqlite3.Error as exc:
1023
+ logger.debug("dispatch transcript fallback query failed: %s", exc)
1024
+ return None
1025
+ if row is None or not row["raw_transcript"]:
1026
+ return None
1027
+ return {
1028
+ "session": {
1029
+ "session_id": session_id,
1030
+ "source": "dispatch_transcript",
1031
+ "formula_id": row["formula_id"],
1032
+ "model": row["model"],
1033
+ "status": row["status"],
1034
+ # Mirror the fields ChatView reads (custom_title ?? summary ?? id;
1035
+ # git_branch/cwd/last_modified in the header) so the fallback renders
1036
+ # a real title instead of the raw session id, and reads no undefined.
1037
+ "custom_title": f"dispatch: {row['formula_id']} ({row['status']})",
1038
+ "summary": None,
1039
+ "first_prompt": None,
1040
+ "last_modified": None,
1041
+ "file_size": None,
1042
+ "git_branch": None,
1043
+ "cwd": None,
1044
+ "tag": None,
1045
+ "created_at": None,
1046
+ },
1047
+ "messages": [
1048
+ {
1049
+ "uuid": None,
1050
+ "session_id": session_id,
1051
+ "type": "assistant",
1052
+ "role": "assistant",
1053
+ "model": row["model"],
1054
+ "stop_reason": None,
1055
+ "usage": None,
1056
+ "blocks": [{"type": "text", "text": row["raw_transcript"]}],
1057
+ "parent_tool_use_id": None,
1058
+ }
1059
+ ],
1060
+ "count": 1,
1061
+ "offset": 0,
1062
+ "meta": {"layer": "cognition", "source": "formula_dispatches"},
1063
+ }
1064
+
1065
+
1066
+ @router.get("/chat/{session_id}")
1067
+ async def get_chat(
1068
+ session_id: str,
1069
+ limit: int = Query(500, ge=1, le=5000),
1070
+ offset: int = Query(0, ge=0),
1071
+ _rl=Depends(make_rate_limit_dep("cognition.chat_get")),
1072
+ _m=Depends(make_metrics_dep("cognition.chat_get")),
1073
+ ):
1074
+ """Return one adapter-normalized transcript."""
1075
+ from web.chat_providers import get_session as get_adapter_session
1076
+
1077
+ cwd = _project_cwd()
1078
+ agent_hints = _session_agent_hints(session_id)
1079
+ if agent_hints and "claude" not in agent_hints:
1080
+ adapter_data = await get_adapter_session(
1081
+ session_id, cwd, limit, offset, agent_hints=agent_hints
1082
+ )
1083
+ if adapter_data is not None:
1084
+ return unwrap(json.dumps({"ok": True, "data": adapter_data}))
1085
+
1086
+ sdk = _claude_sdk()
1087
+ info = None
1088
+ if sdk is not None:
1089
+ try:
1090
+ info = sdk.get_session_info(session_id, directory=cwd)
1091
+ except Exception as exc:
1092
+ logger.warning("get_session_info(%s) failed: %s", session_id, exc)
1093
+ if info is None:
1094
+ adapter_data = await get_adapter_session(
1095
+ session_id,
1096
+ cwd,
1097
+ limit,
1098
+ offset,
1099
+ agent_hints=agent_hints or None,
1100
+ )
1101
+ if adapter_data is not None:
1102
+ return unwrap(json.dumps({"ok": True, "data": adapter_data}))
1103
+ fallback = _dispatch_transcript_chat(session_id)
1104
+ if fallback is not None:
1105
+ return unwrap(json.dumps({"ok": True, "data": fallback}))
1106
+ raise HTTPException(status_code=404, detail=f"chat session {session_id!r} not found")
1107
+ try:
1108
+ messages = sdk.get_session_messages(session_id, directory=cwd, limit=limit, offset=offset)
1109
+ except Exception as exc:
1110
+ return unwrap(
1111
+ json.dumps(
1112
+ {
1113
+ "ok": False,
1114
+ "error": {
1115
+ "category": "internal",
1116
+ "retryable": False,
1117
+ "message": f"get_session_messages failed: {exc}",
1118
+ },
1119
+ }
1120
+ )
1121
+ )
1122
+ return unwrap(
1123
+ json.dumps(
1124
+ {
1125
+ "ok": True,
1126
+ "data": {
1127
+ "session": _serialize_session_info(info),
1128
+ "messages": [_serialize_message(m) for m in messages],
1129
+ "count": len(messages),
1130
+ "offset": offset,
1131
+ "meta": {"layer": "cognition", "source": "claude_agent_sdk"},
1132
+ },
1133
+ }
1134
+ )
1135
+ )
1136
+
1137
+
1138
+ # SDK content-block dataclasses don't carry a `type` discriminator
1139
+ # field — they're disambiguated by Python class. The frontend renders
1140
+ # blocks by `b.type === 'text' | 'thinking' | 'tool_use' | …`, so
1141
+ # without this map every block round-trips as a typeless dict and the
1142
+ # Cognition / Chats panel shows an empty assistant pill (TASK 2026-05-20
1143
+ # UI audit). Names map to the wire-format discriminators that
1144
+ # ChatView.tsx already understands.
1145
+ _BLOCK_TYPE_BY_CLASS = {
1146
+ "TextBlock": "text",
1147
+ "ThinkingBlock": "thinking",
1148
+ "ToolUseBlock": "tool_use",
1149
+ "ToolResultBlock": "tool_result",
1150
+ "ServerToolUseBlock": "server_tool_use",
1151
+ "ServerToolResultBlock": "server_tool_result",
1152
+ }
1153
+
1154
+
1155
+ def _safe_serialize(obj: Any) -> Any:
1156
+ """Best-effort recursive serializer for SDK dataclass events."""
1157
+ if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
1158
+ cls_name = type(obj).__name__
1159
+ # Recurse field-by-field via getattr — NOT dataclasses.asdict, which
1160
+ # pre-flattens the whole tree so a nested TextBlock arrives here as a
1161
+ # plain dict and never gets its `type` stamped (the streamed
1162
+ # AssistantMessage.content[] blocks then lack `type` and the UI drops
1163
+ # them → "agent draft shows nothing").
1164
+ out: dict[str, Any] = {
1165
+ f.name: _safe_serialize(getattr(obj, f.name)) for f in dataclasses.fields(obj)
1166
+ }
1167
+ block_type = _BLOCK_TYPE_BY_CLASS.get(cls_name)
1168
+ if block_type is not None:
1169
+ out["type"] = block_type
1170
+ # ThinkingBlock stores its text under `.thinking`; the UI
1171
+ # reads `.text` for both text + thinking blocks, so mirror
1172
+ # the field rather than forking the frontend.
1173
+ if block_type == "thinking" and "text" not in out and "thinking" in out:
1174
+ out["text"] = out["thinking"]
1175
+ return out
1176
+ if isinstance(obj, dict):
1177
+ return {str(k): _safe_serialize(v) for k, v in obj.items()}
1178
+ if isinstance(obj, (list, tuple)):
1179
+ return [_safe_serialize(v) for v in obj]
1180
+ if isinstance(obj, (str, int, float, bool, type(None))):
1181
+ return obj
1182
+ return str(obj)[:4000]
1183
+
1184
+
1185
+ def _sse_chunk(event: str, payload: dict) -> bytes:
1186
+ return f"event: {event}\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n".encode()
1187
+
1188
+
1189
+ def _role_system_prompt(role: str | None):
1190
+ """Load a role's agent prompt as a claude_code system-prompt append, if valid."""
1191
+ import re as _re
1192
+
1193
+ if not role or not _re.match(r"^[a-z_]+$", role):
1194
+ return None
1195
+ agent_md = Path(__file__).resolve().parents[2] / "thinking_os" / "agents" / f"{role}.md"
1196
+ try:
1197
+ if agent_md.exists():
1198
+ return {
1199
+ "type": "preset",
1200
+ "preset": "claude_code",
1201
+ "append": agent_md.read_text(encoding="utf-8"),
1202
+ }
1203
+ except OSError:
1204
+ pass
1205
+ return None
1206
+
1207
+
1208
+ def _role_names(agents_dir: Path) -> list[str]:
1209
+ import re as _re
1210
+
1211
+ try:
1212
+ return sorted(
1213
+ p.stem
1214
+ for p in agents_dir.glob("*.md")
1215
+ if _re.match(r"^[a-z_]+$", p.stem) and not p.stem.startswith("_")
1216
+ )
1217
+ except OSError as exc:
1218
+ logger.debug("roles scan skipped %s: %s", agents_dir, exc)
1219
+ return []
1220
+
1221
+
1222
+ @router.get("/roles")
1223
+ def list_roles(
1224
+ _rl=Depends(make_rate_limit_dep("cognition.roles")),
1225
+ _m=Depends(make_metrics_dep("cognition.roles")),
1226
+ ):
1227
+ """List the semantic roles a chat session can adopt (producer: thinking_os/agents/*.md)."""
1228
+ roles = _role_names(Path(__file__).resolve().parents[2] / "thinking_os" / "agents")
1229
+ return unwrap(
1230
+ json.dumps(
1231
+ {
1232
+ "ok": True,
1233
+ "data": {"roles": roles, "count": len(roles), "meta": {"layer": "cognition"}},
1234
+ }
1235
+ )
1236
+ )
1237
+
1238
+
1239
+ @router.post("/chat")
1240
+ async def chat_new(
1241
+ body: dict = Body(...),
1242
+ _rl=Depends(make_rate_limit_dep("cognition.chat_new")),
1243
+ _m=Depends(make_metrics_dep("cognition.chat_new")),
1244
+ ):
1245
+ """Start a FRESH Claude session from a prompt (no resume); stream SSE.
1246
+
1247
+ Body: ``{"prompt": str, "model": str|null, "role": str|null}``. Emits a
1248
+ ``session`` event carrying the SDK-resolved session id so the UI can open
1249
+ the chat under the id that get_session_info / list_sessions actually use.
1250
+ Claude-only — returns an ``unavailable`` envelope without the SDK.
1251
+ """
1252
+ prompt = str(body.get("prompt") or "").strip()
1253
+ if not prompt:
1254
+ return unwrap(
1255
+ json.dumps(
1256
+ {
1257
+ "ok": False,
1258
+ "error": {
1259
+ "category": "validation",
1260
+ "retryable": False,
1261
+ "message": "prompt must be non-empty",
1262
+ },
1263
+ }
1264
+ )
1265
+ )
1266
+ sdk = _claude_sdk()
1267
+ if sdk is None:
1268
+ return unwrap(_unavailable("claude_agent_sdk not installed"))
1269
+
1270
+ import secrets
1271
+ import time as _time
1272
+
1273
+ model = body.get("model") or None
1274
+ routing_decision: dict | None = None
1275
+ if model == "auto":
1276
+ routing_decision = _auto_route_model(prompt)
1277
+ if "error" in routing_decision:
1278
+ return unwrap(
1279
+ json.dumps(
1280
+ {
1281
+ "ok": False,
1282
+ "error": {
1283
+ "category": "validation",
1284
+ "retryable": False,
1285
+ "message": routing_decision["error"],
1286
+ },
1287
+ }
1288
+ )
1289
+ )
1290
+ model = routing_decision["model"] or None
1291
+ role = (str(body.get("role") or "")).strip() or None
1292
+ effort = (str(body.get("effort") or "")).strip() or None
1293
+ if effort not in (None, "low", "medium", "high", "xhigh", "max"):
1294
+ effort = None # ignore unknown levels rather than failing the turn
1295
+ cwd = _project_cwd()
1296
+ system_prompt = _role_system_prompt(role) or _chat_system_prompt(model)
1297
+ system_prompt = _prime_with_project_description(system_prompt, cwd)
1298
+ new_session_id = f"ses-claude-ui-{int(_time.time())}-{secrets.token_hex(3)}"
1299
+ # SSOT builder (chat profile): setting_sources=[] (no ~40s SessionStart
1300
+ # suite) + programmatic coding-os MCP (cos_* capability) + base-tool
1301
+ # allow-list (no Write/Edit → chat can't mutate code) + destructive-Bash
1302
+ # deny floor. No session_id: the CLI rejects non-UUID ids; the SDK mints
1303
+ # its own UUID, surfaced below from the stream as the `session` event.
1304
+ options = _chat_session_options(
1305
+ "chat", cwd=cwd, model=model, system_prompt=system_prompt, effort=effort
1306
+ )
1307
+
1308
+ async def event_gen():
1309
+ yield _sse_chunk(
1310
+ "started",
1311
+ {"session_id": new_session_id, "prompt": prompt[:200], "model": model, "role": role},
1312
+ )
1313
+ if routing_decision is not None:
1314
+ yield _sse_chunk("routing", routing_decision)
1315
+ # The Claude SDK rekeys the minted ses-claude-ui-* id to its OWN
1316
+ # transcript uuid, so the minted id 404s on get_session_info and never
1317
+ # appears in list_sessions. Emit the SDK-resolved id the moment the
1318
+ # stream reveals it (SDK messages carry .session_id) so the UI opens /
1319
+ # lists the chat under the id that actually resolves.
1320
+ resolved_id = new_session_id
1321
+ emitted_session = False
1322
+ try:
1323
+ async for event in sdk.query(prompt=prompt, options=options):
1324
+ if not emitted_session:
1325
+ real_id = getattr(event, "session_id", None)
1326
+ if real_id:
1327
+ resolved_id = str(real_id)
1328
+ logger.info(
1329
+ "chat_new resolved session id=%s (minted=%s)",
1330
+ resolved_id,
1331
+ new_session_id,
1332
+ )
1333
+ yield _sse_chunk("session", {"session_id": resolved_id})
1334
+ emitted_session = True
1335
+ _chat_presence_write(cwd, resolved_id, "prompt")
1336
+ kind = type(event).__name__.lower().replace("message", "") or "event"
1337
+ yield _sse_chunk(kind, _safe_serialize(event))
1338
+ except asyncio.CancelledError:
1339
+ raise
1340
+ except Exception as exc:
1341
+ logger.exception("chat_new stream failed")
1342
+ yield _sse_chunk("error", {"message": str(exc)})
1343
+ if not emitted_session:
1344
+ # No event ever carried a session_id — the turn produced no resolvable
1345
+ # session. Emitting the minted ses-claude-ui-* id strands the UI on an
1346
+ # id that 404s on get_chat (the "session vanished" report), so log it
1347
+ # loudly. The fallback id keeps the UI from hanging with no handle.
1348
+ logger.warning(
1349
+ "chat_new: stream produced no SDK session_id (minted=%s) — UI will 404 on this id",
1350
+ new_session_id,
1351
+ )
1352
+ yield _sse_chunk("session", {"session_id": resolved_id})
1353
+ _chat_presence_write(cwd, resolved_id, "stop")
1354
+ yield _sse_chunk("done", {"session_id": resolved_id})
1355
+
1356
+ return StreamingResponse(
1357
+ event_gen(),
1358
+ media_type="text/event-stream",
1359
+ headers={
1360
+ "Cache-Control": "no-cache",
1361
+ "X-Accel-Buffering": "no",
1362
+ "Connection": "keep-alive",
1363
+ },
1364
+ )
1365
+
1366
+
1367
+ _CHAT_SYSTEM = (
1368
+ "You are the coding-os Hub chat assistant — a direct, helpful conversational "
1369
+ "agent for this project. Answer the user's message conversationally in Markdown. "
1370
+ "Do NOT prepend the transparency banner (the line starting with the bell emoji) "
1371
+ "and skip any cognitive-state / gate / work-log ceremony — that protocol is for "
1372
+ "terminal sessions, not Hub chat; just answer. You MAY use the cos_* tools "
1373
+ "(memory, graph, docs, board) to ground an answer when it genuinely helps, but "
1374
+ "keep replies focused and readable rather than running a full work protocol. "
1375
+ "When you commit code for a specific task, include its id like `(TASK-NNN)` in "
1376
+ "the commit subject so the board links the commit to that task."
1377
+ )
1378
+
1379
+
1380
+ def _prime_with_project_description(system_prompt: dict, cwd: str) -> dict:
1381
+ """Append the onboarding intake (docs/_meta/project-description.md) to the
1382
+ chat system prompt so the first session knows what the project IS (TASK-364).
1383
+ Fail-open: missing/unreadable intake leaves the prompt untouched."""
1384
+ try:
1385
+ intake = Path(cwd) / "docs" / "_meta" / "project-description.md"
1386
+ if not intake.is_file():
1387
+ return system_prompt
1388
+ text = intake.read_text(encoding="utf-8").strip()[:2000]
1389
+ if not text or not isinstance(system_prompt, dict) or "append" not in system_prompt:
1390
+ return system_prompt
1391
+ return {
1392
+ **system_prompt,
1393
+ "append": system_prompt["append"]
1394
+ + "\n\n## Project context (onboarding intake)\n"
1395
+ + text,
1396
+ }
1397
+ except OSError:
1398
+ return system_prompt
1399
+
1400
+
1401
+ def _chat_system_prompt(model: str | None) -> dict:
1402
+ """claude_code preset + the chat framing, pinning the model name when known."""
1403
+ append = _CHAT_SYSTEM
1404
+ if model:
1405
+ append = (
1406
+ f"{_CHAT_SYSTEM}\n\nYou are answering as the `{model}` model. If the user "
1407
+ f"asks which model you are, tell them exactly `{model}`."
1408
+ )
1409
+ return {"type": "preset", "preset": "claude_code", "append": append}
1410
+
1411
+
1412
+ _TASK_AUTHOR_SYSTEM = (
1413
+ "You are a task-authoring agent for coding-os. Using ONLY cos_* tools, "
1414
+ "research the codebase (cos_graph_query/context, cos_doc_search, "
1415
+ "cos_task_search/board) and then create EXACTLY ONE well-formed Scrumban "
1416
+ "task with cos_task_create: choose the correct swimlane and kind, write a "
1417
+ "one-sentence Outcome and a Given/When/Then Acceptance, and list 1-4 Read "
1418
+ "First files. Reconcile against the existing board first and reuse a task "
1419
+ "instead of duplicating when appropriate. Do NOT write or edit any code or "
1420
+ "files. After creating or identifying the task, state its id and stop."
1421
+ )
1422
+
1423
+
1424
+ @router.post("/author-task")
1425
+ async def author_task(
1426
+ body: dict = Body(...),
1427
+ _rl=Depends(make_rate_limit_dep("cognition.author_task")),
1428
+ _m=Depends(make_metrics_dep("cognition.author_task")),
1429
+ ):
1430
+ """Headless research+author session that creates one task via cos_task_create. Claude-only."""
1431
+ prompt = str(body.get("prompt") or "").strip()
1432
+ if not prompt:
1433
+ return unwrap(
1434
+ json.dumps(
1435
+ {
1436
+ "ok": False,
1437
+ "error": {
1438
+ "category": "validation",
1439
+ "retryable": False,
1440
+ "message": "prompt must be non-empty",
1441
+ },
1442
+ }
1443
+ )
1444
+ )
1445
+ sdk = _claude_sdk()
1446
+ if sdk is None:
1447
+ return unwrap(_unavailable("claude_agent_sdk not installed"))
1448
+
1449
+ import secrets
1450
+ import time as _time
1451
+
1452
+ model = body.get("model") or None
1453
+ cwd = _project_cwd()
1454
+ sid = f"ses-claude-author-{int(_time.time())}-{secrets.token_hex(3)}"
1455
+ options = _build_agent_options(
1456
+ cwd=cwd,
1457
+ model=model,
1458
+ permission_mode="dontAsk",
1459
+ setting_sources=["project"],
1460
+ # No session_id — claude CLI requires a UUID; SDK mints its own (emitted below).
1461
+ # cos_* only — no Write/Edit/Bash, so it can research + author but never touch code.
1462
+ allowed_tools=["mcp__coding-os__*"],
1463
+ disallowed_tools=["Write", "Edit", "MultiEdit", "Bash"],
1464
+ system_prompt={"type": "preset", "preset": "claude_code", "append": _TASK_AUTHOR_SYSTEM},
1465
+ max_turns=30,
1466
+ )
1467
+
1468
+ async def event_gen():
1469
+ yield _sse_chunk("started", {"session_id": sid, "prompt": prompt[:200], "model": model})
1470
+ resolved_id = sid
1471
+ emitted_session = False
1472
+ try:
1473
+ async for event in sdk.query(prompt=prompt, options=options):
1474
+ if not emitted_session:
1475
+ real_id = getattr(event, "session_id", None)
1476
+ if real_id:
1477
+ resolved_id = str(real_id)
1478
+ yield _sse_chunk("session", {"session_id": resolved_id})
1479
+ emitted_session = True
1480
+ kind = type(event).__name__.lower().replace("message", "") or "event"
1481
+ yield _sse_chunk(kind, _safe_serialize(event))
1482
+ except asyncio.CancelledError:
1483
+ raise
1484
+ except Exception as exc:
1485
+ logger.exception("author_task stream failed")
1486
+ yield _sse_chunk("error", {"message": str(exc)})
1487
+ if not emitted_session:
1488
+ yield _sse_chunk("session", {"session_id": resolved_id})
1489
+ yield _sse_chunk("done", {"session_id": resolved_id})
1490
+
1491
+ return StreamingResponse(
1492
+ event_gen(),
1493
+ media_type="text/event-stream",
1494
+ headers={
1495
+ "Cache-Control": "no-cache",
1496
+ "X-Accel-Buffering": "no",
1497
+ "Connection": "keep-alive",
1498
+ },
1499
+ )
1500
+
1501
+
1502
+ # ---------------------------------------------------------------------------
1503
+ # Onboarding — docs-scoped session (TASK-246)
1504
+ # ---------------------------------------------------------------------------
1505
+
1506
+ _ONBOARD_WRITE_TOOLS = frozenset({"Write", "Edit", "MultiEdit", "NotebookEdit"})
1507
+ _ONBOARD_ALLOWED_TOOLS = [
1508
+ "mcp__coding-os__*",
1509
+ "Write",
1510
+ "Edit",
1511
+ "MultiEdit",
1512
+ "Read",
1513
+ "Glob",
1514
+ "Grep",
1515
+ "TodoWrite",
1516
+ "WebFetch",
1517
+ "WebSearch",
1518
+ ]
1519
+
1520
+
1521
+ def _is_path_under_docs(file_path: str, project_root: Path) -> bool:
1522
+ """True when file_path resolves to <project_root>/docs (or below)."""
1523
+ if not file_path:
1524
+ return False
1525
+ try:
1526
+ p = Path(file_path)
1527
+ if not p.is_absolute():
1528
+ p = project_root / p
1529
+ p = p.resolve()
1530
+ docs = (project_root / "docs").resolve()
1531
+ return p == docs or docs in p.parents
1532
+ except (OSError, ValueError, RuntimeError):
1533
+ return False
1534
+
1535
+
1536
+ def _onboard_write_allowed(tool_input: dict, project_root: Path) -> bool:
1537
+ """Permission contract for the onboard session: a write tool may only target
1538
+ a path under docs/. Non-dict input or a missing path denies (fail-closed)."""
1539
+ if not isinstance(tool_input, dict):
1540
+ return False
1541
+ path = tool_input.get("file_path") or tool_input.get("path") or tool_input.get("notebook_path")
1542
+ return _is_path_under_docs(str(path or ""), project_root)
1543
+
1544
+
1545
+ def _count_placeholder_todos(project_root: Path) -> tuple[int, bool]:
1546
+ """Scan docs/prd/*.md for scaffold `_TODO:` markers.
1547
+
1548
+ Returns (todo_count, prd_exists). prd_exists=False means there is no PRD
1549
+ scaffold at all → nothing to onboard."""
1550
+ prd_dir = project_root / "docs" / "prd"
1551
+ if not prd_dir.is_dir():
1552
+ return 0, False
1553
+ total = 0
1554
+ found_any = False
1555
+ for md in prd_dir.glob("*.md"):
1556
+ found_any = True
1557
+ try:
1558
+ total += md.read_text(encoding="utf-8").count("_TODO:")
1559
+ except OSError as exc:
1560
+ logger.debug("onboarding scan skipped %s: %s", md, exc)
1561
+ continue
1562
+ return total, found_any
1563
+
1564
+
1565
+ def _onboarding_state(project_root: Path, state_dir: Path) -> dict:
1566
+ """Resolve onboarding completeness: onboarding.json override, else _TODO scan."""
1567
+ marker = state_dir / "onboarding.json"
1568
+ if marker.exists():
1569
+ try:
1570
+ data = json.loads(marker.read_text(encoding="utf-8"))
1571
+ if isinstance(data, dict) and data.get("completed") is True:
1572
+ return {"complete": True, "source": "onboarding_json", "placeholders_remaining": 0}
1573
+ except (OSError, json.JSONDecodeError) as exc:
1574
+ logger.debug("onboarding.json unreadable: %s", exc)
1575
+ todos, prd_exists = _count_placeholder_todos(project_root)
1576
+ if not prd_exists:
1577
+ return {
1578
+ "complete": True,
1579
+ "source": "no_prd",
1580
+ "placeholders_remaining": 0,
1581
+ "reason": "no PRD scaffold to onboard",
1582
+ }
1583
+ return {
1584
+ "complete": todos == 0,
1585
+ "source": "placeholder_scan",
1586
+ "placeholders_remaining": todos,
1587
+ "reason": (
1588
+ "PRD still has scaffold _TODO markers" if todos else "PRD placeholders authored"
1589
+ ),
1590
+ }
1591
+
1592
+
1593
+ @router.get("/onboarding-status")
1594
+ def onboarding_status(
1595
+ _rl=Depends(make_rate_limit_dep("cognition.onboarding_status")),
1596
+ _m=Depends(make_metrics_dep("cognition.onboarding_status")),
1597
+ ):
1598
+ """Whether the project still needs onboarding (placeholder-scan first, onboarding.json override)."""
1599
+ from web._project_context import current_project_root # type: ignore
1600
+
1601
+ project = current_project_root()
1602
+ state = _state_dir()
1603
+ payload = _onboarding_state(project, state)
1604
+ payload["meta"] = {"layer": "cognition"}
1605
+ return unwrap(json.dumps({"ok": True, "data": payload}))
1606
+
1607
+
1608
+ @router.post("/onboarding-status/dismiss")
1609
+ def onboarding_dismiss(
1610
+ _rl=Depends(make_rate_limit_dep("cognition.onboarding_dismiss")),
1611
+ _m=Depends(make_metrics_dep("cognition.onboarding_dismiss")),
1612
+ ):
1613
+ """Persist the onboarding hero dismissal so it stops reappearing on reload."""
1614
+ state = _state_dir()
1615
+ marker = state / "onboarding.json"
1616
+ try:
1617
+ state.mkdir(parents=True, exist_ok=True)
1618
+ marker.write_text(
1619
+ json.dumps({"completed": True, "source": "dismissed"}, indent=2) + "\n",
1620
+ encoding="utf-8",
1621
+ )
1622
+ except OSError as exc:
1623
+ return unwrap(
1624
+ json.dumps(
1625
+ {
1626
+ "ok": False,
1627
+ "error": {
1628
+ "category": "internal",
1629
+ "retryable": False,
1630
+ "message": f"could not write onboarding marker: {exc}",
1631
+ },
1632
+ }
1633
+ )
1634
+ )
1635
+ return unwrap(
1636
+ json.dumps({"ok": True, "data": {"complete": True, "meta": {"layer": "cognition"}}})
1637
+ )
1638
+
1639
+
1640
+ @router.post("/onboard")
1641
+ async def onboard(
1642
+ body: dict = Body(...),
1643
+ _rl=Depends(make_rate_limit_dep("cognition.onboard")),
1644
+ _m=Depends(make_metrics_dep("cognition.onboard")),
1645
+ ):
1646
+ """Run the onboarder role with Write/Edit confined to docs/ (PreToolUse-gated). Claude-only."""
1647
+ prompt = str(body.get("prompt") or "").strip()
1648
+ if not prompt:
1649
+ return unwrap(
1650
+ json.dumps(
1651
+ {
1652
+ "ok": False,
1653
+ "error": {
1654
+ "category": "validation",
1655
+ "retryable": False,
1656
+ "message": "prompt must be non-empty",
1657
+ },
1658
+ }
1659
+ )
1660
+ )
1661
+ sdk = _claude_sdk()
1662
+ if sdk is None:
1663
+ return unwrap(_unavailable("claude_agent_sdk not installed"))
1664
+
1665
+ import secrets
1666
+ import time as _time
1667
+
1668
+ model = body.get("model") or None
1669
+ cwd = _project_cwd()
1670
+ project_root = Path(cwd)
1671
+ sid = f"ses-claude-onboard-{int(_time.time())}-{secrets.token_hex(3)}"
1672
+ system_prompt = _role_system_prompt("onboarder") or {
1673
+ "type": "preset",
1674
+ "preset": "claude_code",
1675
+ }
1676
+
1677
+ async def _deny_non_docs_write(input_data: dict, _tool_use_id, _ctx) -> dict:
1678
+ # PreToolUse is evaluated FIRST and honored even under dontAsk (where
1679
+ # can_use_tool is skipped) — the only reliable place to path-scope writes.
1680
+ try:
1681
+ if input_data.get("tool_name") in _ONBOARD_WRITE_TOOLS and not _onboard_write_allowed(
1682
+ input_data.get("tool_input") or {}, project_root
1683
+ ):
1684
+ return {
1685
+ "hookSpecificOutput": {
1686
+ "hookEventName": "PreToolUse",
1687
+ "permissionDecision": "deny",
1688
+ "permissionDecisionReason": "onboard sessions may only write under docs/",
1689
+ }
1690
+ }
1691
+ except Exception as exc: # never raise from a hook — would kill the stream
1692
+ logger.debug("onboard PreToolUse gate error: %s", exc)
1693
+ return {}
1694
+
1695
+ options = _build_agent_options(
1696
+ cwd=cwd,
1697
+ model=model,
1698
+ permission_mode="dontAsk",
1699
+ setting_sources=["project"],
1700
+ # No session_id — claude CLI requires a UUID; SDK mints its own (emitted below).
1701
+ include_partial_messages=True, # token-by-token streaming for the live UI
1702
+ allowed_tools=list(_ONBOARD_ALLOWED_TOOLS),
1703
+ disallowed_tools=["Bash"], # deny wins even over the allow-list
1704
+ system_prompt=system_prompt,
1705
+ # HookMatcher is the adapter SDK's type, constructed here because the hook
1706
+ # closure is core-local; ClaudeAgentOptions itself still routes through the
1707
+ # adapter seam. Migrating HookMatcher is tracked separately (out of scope).
1708
+ hooks={
1709
+ "PreToolUse": [
1710
+ sdk.HookMatcher(
1711
+ matcher="Write|Edit|MultiEdit|NotebookEdit", hooks=[_deny_non_docs_write]
1712
+ )
1713
+ ]
1714
+ },
1715
+ max_turns=40,
1716
+ )
1717
+
1718
+ async def event_gen():
1719
+ yield _sse_chunk("started", {"session_id": sid, "prompt": prompt[:200], "model": model})
1720
+ resolved_id = sid
1721
+ emitted_session = False
1722
+ try:
1723
+ async for event in sdk.query(prompt=prompt, options=options):
1724
+ if not emitted_session:
1725
+ real_id = getattr(event, "session_id", None)
1726
+ if real_id:
1727
+ resolved_id = str(real_id)
1728
+ yield _sse_chunk("session", {"session_id": resolved_id})
1729
+ emitted_session = True
1730
+ kind = type(event).__name__.lower().replace("message", "") or "event"
1731
+ yield _sse_chunk(kind, _safe_serialize(event))
1732
+ except asyncio.CancelledError:
1733
+ raise
1734
+ except Exception as exc:
1735
+ logger.exception("onboard stream failed")
1736
+ yield _sse_chunk("error", {"message": str(exc)})
1737
+ if not emitted_session:
1738
+ yield _sse_chunk("session", {"session_id": resolved_id})
1739
+ yield _sse_chunk("done", {"session_id": resolved_id})
1740
+
1741
+ return StreamingResponse(
1742
+ event_gen(),
1743
+ media_type="text/event-stream",
1744
+ headers={
1745
+ "Cache-Control": "no-cache",
1746
+ "X-Accel-Buffering": "no",
1747
+ "Connection": "keep-alive",
1748
+ },
1749
+ )
1750
+
1751
+
1752
+ @router.post("/chat/{session_id}/send")
1753
+ async def chat_send(
1754
+ session_id: str,
1755
+ body: dict = Body(...),
1756
+ _rl=Depends(make_rate_limit_dep("cognition.chat_send")),
1757
+ _m=Depends(make_metrics_dep("cognition.chat_send")),
1758
+ ):
1759
+ """Resume a Claude session with a new prompt; stream events as SSE.
1760
+
1761
+ Body: ``{"prompt": str, "fork": bool=false, "model": str|null}``.
1762
+ Each SSE event ``data`` payload is a serialized SDK message; ``event``
1763
+ field is one of ``user``, ``assistant``, ``system``, ``result``,
1764
+ ``stream``, ``rate_limit``, ``error``, ``done``.
1765
+ """
1766
+ prompt = str(body.get("prompt") or "").strip()
1767
+ if not prompt:
1768
+ return unwrap(
1769
+ json.dumps(
1770
+ {
1771
+ "ok": False,
1772
+ "error": {
1773
+ "category": "validation",
1774
+ "retryable": False,
1775
+ "message": "prompt must be non-empty",
1776
+ },
1777
+ }
1778
+ )
1779
+ )
1780
+ sdk = _claude_sdk()
1781
+ if sdk is None:
1782
+ return unwrap(_unavailable("claude_agent_sdk not installed"))
1783
+
1784
+ cwd = _project_cwd()
1785
+ fork = bool(body.get("fork"))
1786
+ model = body.get("model") or None
1787
+ # SSOT builder (chat_resume profile) — same chat-light policy as chat_new
1788
+ # (mcp + deny floor + no Write/Edit), plus resume/fork for the follow-up turn.
1789
+ options = _chat_session_options(
1790
+ "chat_resume",
1791
+ cwd=cwd,
1792
+ model=model,
1793
+ system_prompt=_chat_system_prompt(model),
1794
+ resume=session_id,
1795
+ fork=fork,
1796
+ )
1797
+
1798
+ async def event_gen():
1799
+ yield _sse_chunk(
1800
+ "started", {"session_id": session_id, "prompt": prompt[:200], "fork": fork}
1801
+ )
1802
+ _chat_presence_write(cwd, session_id, "prompt")
1803
+ emitted_kinds: list[str] = []
1804
+ try:
1805
+ async for event in sdk.query(prompt=prompt, options=options):
1806
+ kind = type(event).__name__.lower().replace("message", "")
1807
+ if not kind:
1808
+ kind = "event"
1809
+ emitted_kinds.append(kind)
1810
+ logger.info(
1811
+ "chat_send stream: session=%s kind=%s class=%s",
1812
+ session_id,
1813
+ kind,
1814
+ type(event).__name__,
1815
+ )
1816
+ yield _sse_chunk(kind, _safe_serialize(event))
1817
+ except asyncio.CancelledError:
1818
+ raise
1819
+ except Exception as exc:
1820
+ logger.exception("chat resume stream failed")
1821
+ yield _sse_chunk("error", {"message": str(exc)})
1822
+ logger.info(
1823
+ "chat_send stream done: session=%s emitted=%s",
1824
+ session_id,
1825
+ ",".join(emitted_kinds) or "(none)",
1826
+ )
1827
+ _chat_presence_write(cwd, session_id, "stop")
1828
+ yield _sse_chunk("done", {"session_id": session_id})
1829
+
1830
+ return StreamingResponse(
1831
+ event_gen(),
1832
+ media_type="text/event-stream",
1833
+ headers={
1834
+ "Cache-Control": "no-cache",
1835
+ "X-Accel-Buffering": "no",
1836
+ "Connection": "keep-alive",
1837
+ },
1838
+ )