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,2883 @@
1
+ """
2
+ Coding OS — SQLite database module with auto-migration.
3
+
4
+ Provides connection management (WAL mode), schema versioning,
5
+ and migration execution for the thinking_os self-learning system.
6
+
7
+ Agent-agnostic: DB path is configurable via COS_DB_PATH env var.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ import os
15
+ import sqlite3
16
+ from collections.abc import Callable, Generator
17
+ from contextlib import contextmanager
18
+ from contextvars import ContextVar
19
+ from pathlib import Path
20
+ from typing import Union
21
+
22
+ # Per-request project scope hook. The web layer (Hub) binds this CV in
23
+ # `ProjectScopeMiddleware` so every downstream lookup that goes through
24
+ # `resolve_db_path()` lands on the right project's `coding-os.db` —
25
+ # without graph_os / thinking_os having to import from `core.web`.
26
+ #
27
+ # When the CV is None (MCP server, CLI, hooks), `resolve_db_path()`
28
+ # falls through to its legacy behaviour (explicit project_root arg or
29
+ # DEFAULT_DB_PATH). This keeps single-project callers unaffected.
30
+ _active_project_root: ContextVar[Path | None] = ContextVar(
31
+ "cos_active_project_root",
32
+ default=None,
33
+ )
34
+
35
+
36
+ def set_active_project_root(root: Path | None) -> object:
37
+ """Bind the current request's project root; returns a reset token."""
38
+ return _active_project_root.set(root)
39
+
40
+
41
+ def reset_active_project_root(token: object) -> None:
42
+ """Release a binding made by :func:`set_active_project_root`."""
43
+ _active_project_root.reset(token) # type: ignore[arg-type]
44
+
45
+
46
+ def get_active_project_root() -> Path | None:
47
+ """Return the currently-bound project root (or None when unset)."""
48
+ return _active_project_root.get()
49
+
50
+
51
+ logger = logging.getLogger("coding_os.db")
52
+
53
+ # Default DB path — configurable via COS_DB_PATH env var
54
+ # Falls back to .coding-os/coding-os.db in current working directory.
55
+ # Canonical filename, single source of truth for every consumer (MCP server,
56
+ # Hub web, every CLI subcommand, every hook).
57
+ DB_FILENAME = "coding-os.db"
58
+ LEGACY_DB_FILENAME = "thinking_os.db" # rename target for migrate_legacy_db_filename()
59
+ STATE_DIRNAME = ".coding-os"
60
+
61
+ # A true project root co-locates its `.coding-os/` with at least one of these
62
+ # markers. A stray nested `.coding-os/` (lazy-created from a subdir like
63
+ # src/cli/) has none of them — so preferring a marked ancestor lets us skip
64
+ # strays and anchor on the real root.
65
+ _ROOT_MARKERS = (
66
+ ".git",
67
+ ".coding-os.yaml",
68
+ "pyproject.toml",
69
+ "package.json",
70
+ "go.mod",
71
+ "AGENTS.md",
72
+ )
73
+
74
+
75
+ def _find_project_root_from_cwd(start: Path | None = None) -> Path | None:
76
+ """Walk up from cwd to find the enclosing coding-os project root.
77
+
78
+ .coding-os/ lives ONLY at the project root. Anywhere we land —
79
+ src/core/web/, tests/, src/cli/, … — we must walk parents and
80
+ anchor on the first .coding-os/ we find. Without this walk, lazy
81
+ init_db() calls from a subdirectory would CREATE a new stray
82
+ .coding-os/ at cwd, which then surfaces in the Hub as a phantom
83
+ project (the exact bug TASK-117 traced to nested .coding-os/).
84
+ """
85
+ cur = (start or Path.cwd()).resolve()
86
+ try:
87
+ home = Path.home().resolve()
88
+ except (OSError, RuntimeError):
89
+ home = None
90
+ first_with_state: Path | None = None
91
+ for parent in [cur, *cur.parents]:
92
+ # $HOME hard-stop: never inspect or accept $HOME/.coding-os (the global
93
+ # hub state, not a project root). Mirrors the boundary in
94
+ # cos-env.sh::_cos_find_project_root (TASK-498).
95
+ if home is not None and parent == home:
96
+ break
97
+ try:
98
+ if not (parent / STATE_DIRNAME).is_dir():
99
+ continue
100
+ except OSError:
101
+ continue
102
+ if first_with_state is None:
103
+ first_with_state = parent
104
+ # Prefer a `.coding-os/` that co-locates with a project-root marker:
105
+ # this skips a stray nested `.coding-os/` (e.g. src/cli/.coding-os/
106
+ # lazy-created from a subdir) and anchors on the true root. The walk
107
+ # stops at the first MARKED root, so a legitimate checkout is never
108
+ # overridden by an unmarked outer stray (e.g. ~/.coding-os).
109
+ try:
110
+ if any((parent / marker).exists() for marker in _ROOT_MARKERS):
111
+ return parent
112
+ except OSError:
113
+ continue
114
+ if first_with_state is not None:
115
+ return first_with_state
116
+ # No project `.coding-os/` below the $HOME boundary. A real subdir returns
117
+ # cwd so it can lazy-create locally — but $HOME itself is refused: its
118
+ # `.coding-os/` is the global hub state dir, and anchoring there mints a
119
+ # phantom project DB inside it. At bare $HOME there is no project → None.
120
+ if home is not None and cur == home:
121
+ return None
122
+ return cur
123
+
124
+
125
+ DEFAULT_DB_PATH = Path(
126
+ os.environ.get("COS_DB_PATH", "")
127
+ or str((_find_project_root_from_cwd() or Path.cwd()) / STATE_DIRNAME / DB_FILENAME)
128
+ )
129
+
130
+
131
+ def resolve_db_path(project_root: Path | str | None = None) -> Path:
132
+ """Single source of truth for the canonical SQLite DB path.
133
+
134
+ Resolution priority:
135
+ 1. ``<bound_root>/.coding-os/coding-os.db`` when a ProjectScopeMiddleware
136
+ request has bound a per-request project scope. This wins over
137
+ ``$COS_DB_PATH`` because the Hub inherits that env var from the
138
+ directory it was launched in, so a scoped ``/api/p/<slug>/*`` request
139
+ must reach the slug's DB, not the launch project's.
140
+ 2. ``$COS_DB_PATH`` env var, when set (the CLI / MCP default override).
141
+ 3. ``<project_root>/.coding-os/coding-os.db`` when project_root given.
142
+ 4. Walk up from cwd to find the enclosing ``.coding-os/``. RAISES at the
143
+ bare ``$HOME`` boundary (no project below it): ``~/.coding-os/`` is the
144
+ global hub state dir, and every DB-open path funnels through this
145
+ resolver, so raising here is the ONE complete guard against minting a
146
+ phantom ``$HOME/.coding-os/coding-os.db`` — the graph ``SqliteBackend``
147
+ and cognition route ``sqlite3.connect`` directly to this path and would
148
+ otherwise bypass ``init_db``'s guard. Fail-loud at a bare-``$HOME``
149
+ misconfiguration beats a silent phantom; set ``$COS_DB_PATH`` or run
150
+ inside a project. Do NOT weaken this to a cwd fallback.
151
+
152
+ Only the Hub's ProjectScopeMiddleware binds ``_active_project_root``, so
153
+ CLI / MCP callers skip step 1 and keep the prior ``$COS_DB_PATH`` behavior.
154
+
155
+ Use this helper instead of inlining the same fallback formula in
156
+ ~30 different sites — a future filename change becomes one edit
157
+ here, not a sweep across `core/`, `cli/`, `adapters/`, and hooks.
158
+
159
+ The path is returned even if the file does not exist yet — callers
160
+ that need the file present should follow with ``init_db(path)``.
161
+ """
162
+ bound = _active_project_root.get()
163
+ if bound is not None:
164
+ return Path(bound) / STATE_DIRNAME / DB_FILENAME
165
+ explicit = os.environ.get("COS_DB_PATH")
166
+ if explicit:
167
+ return Path(explicit)
168
+ if project_root is not None:
169
+ return Path(project_root) / STATE_DIRNAME / DB_FILENAME
170
+ root = _find_project_root_from_cwd()
171
+ if root is None:
172
+ # Bare $HOME, no project below (see step 4). Every DB-open path resolves
173
+ # through here, so raising is the complete guard — direct-connect
174
+ # callers (graph SqliteBackend, cognition route) bypass init_db.
175
+ raise RuntimeError(
176
+ "no coding-os project found from cwd; set $COS_DB_PATH or run "
177
+ "inside a project — $HOME/.coding-os is the global hub state dir, "
178
+ "not a project DB"
179
+ )
180
+ return root / STATE_DIRNAME / DB_FILENAME
181
+
182
+
183
+ def project_root(start: Path | str | None = None) -> Path:
184
+ """Single source of truth for the project root directory (holds .coding-os/).
185
+
186
+ Precedence:
187
+ 1. ``$COS_PROJECT_ROOT`` env var, when set (explicit override).
188
+ 2. Parent of an absolute ``$COS_STATE_DIR`` — already resolved by
189
+ cos-env.sh, so honoring it means the shell's one resolution is reused
190
+ instead of re-walking.
191
+ 3. Upward marker-walk from cwd (``_find_project_root_from_cwd``), which has
192
+ the $HOME hard-stop so the global hub at $HOME/.coding-os is never bound.
193
+
194
+ Use this instead of the ``os.environ.get("COS_PROJECT_ROOT") or os.getcwd()``
195
+ idiom that was duplicated across the CLI, board, web, background, and hook
196
+ helpers — that idiom mis-resolves from a subdirectory (TASK-498).
197
+ """
198
+ explicit = os.environ.get("COS_PROJECT_ROOT")
199
+ if explicit:
200
+ return Path(explicit).resolve()
201
+ state = os.environ.get("COS_STATE_DIR")
202
+ if state:
203
+ state_path = Path(state)
204
+ if state_path.is_absolute():
205
+ parent = state_path.resolve().parent
206
+ # $HOME hard-stop: COS_STATE_DIR == $HOME/.coding-os is the global
207
+ # hub (set by `cos hub`), not a project root — its parent is $HOME.
208
+ # Reuse the shell's boundary instead of binding $HOME; fall through
209
+ # to the marker-walk (which has its own $HOME hard-stop).
210
+ try:
211
+ home = Path.home().resolve()
212
+ except (OSError, RuntimeError):
213
+ home = None
214
+ if home is None or parent != home:
215
+ return parent
216
+ start_path = Path(start) if start else None
217
+ root = _find_project_root_from_cwd(start_path)
218
+ return root if root is not None else (start_path or Path.cwd()).resolve()
219
+
220
+
221
+ def migrate_legacy_db_filename(target: Path) -> bool:
222
+ """Rename `<dir>/thinking_os.db` → `<dir>/coding-os.db` once, in place."""
223
+ if target.exists():
224
+ return False
225
+ legacy = target.with_name(LEGACY_DB_FILENAME)
226
+ if not legacy.exists():
227
+ return False
228
+ legacy.rename(target)
229
+ for ext in ("-shm", "-wal"):
230
+ legacy_aux = legacy.with_name(legacy.name + ext)
231
+ if legacy_aux.exists():
232
+ legacy_aux.rename(target.with_name(target.name + ext))
233
+ logger.info("Migrated legacy DB filename: %s -> %s", legacy.name, target.name)
234
+ return True
235
+
236
+
237
+ # ---------------------------------------------------------------------------
238
+ # FTS5 detection (must be defined before migrations that use it)
239
+ # ---------------------------------------------------------------------------
240
+
241
+
242
+ def has_fts5(conn: sqlite3.Connection) -> bool:
243
+ """Check whether the current SQLite build supports FTS5."""
244
+ try:
245
+ conn.execute("CREATE VIRTUAL TABLE _fts5_probe USING fts5(x)")
246
+ conn.execute("DROP TABLE _fts5_probe")
247
+ return True
248
+ except sqlite3.OperationalError:
249
+ return False
250
+
251
+
252
+ def has_fts5_table(conn: sqlite3.Connection) -> bool:
253
+ """Check whether the observations_fts table exists (FTS5 was successfully created)."""
254
+ row = conn.execute(
255
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='observations_fts'"
256
+ ).fetchone()
257
+ return row is not None
258
+
259
+
260
+ # ---------------------------------------------------------------------------
261
+ # Callable migrations (defined before MIGRATIONS list so they can be referenced directly)
262
+ # ---------------------------------------------------------------------------
263
+
264
+
265
+ def _migrate_v2_fts5(conn: sqlite3.Connection) -> None:
266
+ """Migration v2: create FTS5 virtual table and sync triggers.
267
+
268
+ Gracefully degrades if FTS5 is not available — logs a warning and skips.
269
+ """
270
+ if not has_fts5(conn):
271
+ logger.warning(
272
+ "FTS5 not available in this SQLite build — skipping FTS5 table creation. "
273
+ "Search will fall back to LIKE queries."
274
+ )
275
+ return
276
+
277
+ conn.executescript("""\
278
+ CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
279
+ title, narrative, concepts,
280
+ content='observations', content_rowid='id'
281
+ );
282
+
283
+ -- Auto-populate on INSERT
284
+ CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
285
+ INSERT INTO observations_fts(rowid, title, narrative, concepts)
286
+ VALUES (new.id, new.title, new.narrative, new.concepts);
287
+ END;
288
+
289
+ -- Re-sync on UPDATE (needed for TASK-155 compression)
290
+ CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
291
+ INSERT INTO observations_fts(observations_fts, rowid, title, narrative, concepts)
292
+ VALUES ('delete', old.id, old.title, old.narrative, old.concepts);
293
+ INSERT INTO observations_fts(rowid, title, narrative, concepts)
294
+ VALUES (new.id, new.title, new.narrative, new.concepts);
295
+ END;
296
+
297
+ -- Auto-cleanup on DELETE
298
+ CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
299
+ INSERT INTO observations_fts(observations_fts, rowid, title, narrative, concepts)
300
+ VALUES ('delete', old.id, old.title, old.narrative, old.concepts);
301
+ END;
302
+ """)
303
+ logger.info("FTS5 observations_fts table and triggers created successfully")
304
+
305
+
306
+ # ---------------------------------------------------------------------------
307
+ # Migration registry
308
+ # ---------------------------------------------------------------------------
309
+ # Each migration is a (version, description, sql_or_callable) tuple.
310
+ # sql_or_callable is either a SQL string (executed via executescript)
311
+ # or a callable(conn) for migrations needing runtime logic (e.g. FTS5 check).
312
+ # Migrations MUST be append-only — never edit an applied migration.
313
+ MigrationAction = Union[str, Callable[[sqlite3.Connection], None]]
314
+
315
+
316
+ def _migrate_v4_brain_features(conn: sqlite3.Connection) -> None:
317
+ """Migration v4: outcome_history, concept_graph, session_summaries enrichment."""
318
+
319
+ # 1. outcome_history — append-only log of every outcome transition
320
+ conn.executescript("""\
321
+ CREATE TABLE IF NOT EXISTS outcome_history (
322
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
323
+ task_id TEXT NOT NULL,
324
+ outcome TEXT NOT NULL,
325
+ previous_outcome TEXT,
326
+ is_breakthrough INTEGER DEFAULT 0,
327
+ narrative_what_failed TEXT,
328
+ narrative_what_worked TEXT,
329
+ narrative_key_insight TEXT,
330
+ triggered_by TEXT,
331
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
332
+ );
333
+
334
+ CREATE INDEX IF NOT EXISTS idx_outcome_history_task
335
+ ON outcome_history(task_id);
336
+ CREATE INDEX IF NOT EXISTS idx_outcome_history_breakthrough
337
+ ON outcome_history(is_breakthrough) WHERE is_breakthrough = 1;
338
+
339
+ -- 2. concept_graph — lightweight adjacency list for file/concept relationships
340
+ CREATE TABLE IF NOT EXISTS concept_graph (
341
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
342
+ source TEXT NOT NULL,
343
+ target TEXT NOT NULL,
344
+ edge_type TEXT NOT NULL,
345
+ weight REAL DEFAULT 1.0,
346
+ evidence TEXT,
347
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
348
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
349
+ UNIQUE(source, target, edge_type)
350
+ );
351
+
352
+ CREATE INDEX IF NOT EXISTS idx_concept_graph_source ON concept_graph(source);
353
+ CREATE INDEX IF NOT EXISTS idx_concept_graph_target ON concept_graph(target);
354
+ CREATE INDEX IF NOT EXISTS idx_concept_graph_type ON concept_graph(edge_type);
355
+ """)
356
+
357
+ # 3. session_summaries enrichment — add columns if missing
358
+ existing_columns = {
359
+ row[1] for row in conn.execute("PRAGMA table_info(session_summaries)").fetchall()
360
+ }
361
+ new_columns = [
362
+ ("previous_session_id", "TEXT"),
363
+ ("duration_minutes", "INTEGER"),
364
+ ("files_touched", "TEXT"),
365
+ ("observations_count", "INTEGER DEFAULT 0"),
366
+ ("breakthrough_ids", "TEXT"),
367
+ ]
368
+ for col_name, col_type in new_columns:
369
+ if col_name not in existing_columns:
370
+ conn.execute(f"ALTER TABLE session_summaries ADD COLUMN {col_name} {col_type}")
371
+
372
+ logger.info(
373
+ "Brain features migration v4 applied: outcome_history, concept_graph, session_summaries enrichment"
374
+ )
375
+
376
+
377
+ def _migrate_v5_rag(conn: sqlite3.Connection) -> None:
378
+ """Migration v5: embeddings + document_chunks for RAG.
379
+
380
+ Adds two new tables:
381
+ - embeddings: vector storage for any source row (observations,
382
+ learned_patterns, outcome_history, document_chunks, tasks).
383
+ BLOB column holds float32 bytes (1536 bytes for 384-dim model).
384
+ - document_chunks: heading-aware chunks of project docs/ for the
385
+ document RAG knowledge base.
386
+
387
+ Both tables are additive — no existing tables are modified. Embeddings
388
+ are populated lazily by the embeddings module, so this migration is
389
+ safe even when sentence-transformers is not installed.
390
+ """
391
+ conn.executescript("""\
392
+ CREATE TABLE IF NOT EXISTS embeddings (
393
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
394
+ source_table TEXT NOT NULL,
395
+ source_id INTEGER NOT NULL,
396
+ text_hash TEXT NOT NULL,
397
+ embedding BLOB NOT NULL,
398
+ model_name TEXT DEFAULT 'BAAI/bge-m3',
399
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
400
+ UNIQUE(source_table, source_id)
401
+ );
402
+
403
+ CREATE INDEX IF NOT EXISTS idx_embeddings_source
404
+ ON embeddings(source_table, source_id);
405
+ CREATE INDEX IF NOT EXISTS idx_embeddings_model
406
+ ON embeddings(model_name);
407
+
408
+ CREATE TABLE IF NOT EXISTS document_chunks (
409
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
410
+ source_path TEXT NOT NULL,
411
+ source_type TEXT NOT NULL,
412
+ chunk_index INTEGER NOT NULL,
413
+ heading_path TEXT,
414
+ content TEXT NOT NULL,
415
+ content_hash TEXT NOT NULL,
416
+ priority REAL DEFAULT 0.5,
417
+ mtime INTEGER NOT NULL,
418
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
419
+ UNIQUE(source_path, chunk_index)
420
+ );
421
+
422
+ CREATE INDEX IF NOT EXISTS idx_doc_chunks_path
423
+ ON document_chunks(source_path);
424
+ CREATE INDEX IF NOT EXISTS idx_doc_chunks_type
425
+ ON document_chunks(source_type);
426
+ """)
427
+ logger.info("RAG migration v5 applied: embeddings + document_chunks tables created")
428
+
429
+
430
+ def has_embeddings_table(conn: sqlite3.Connection) -> bool:
431
+ """Check whether the embeddings table exists (migration v5 applied).
432
+
433
+ Mirrors `has_fts5_table` — used by callers that need to know whether
434
+ semantic search is structurally available before attempting it.
435
+ """
436
+ row = conn.execute(
437
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'"
438
+ ).fetchone()
439
+ return row is not None
440
+
441
+
442
+ def _migrate_v6_tasks(conn: sqlite3.Connection) -> None:
443
+ """Migration v6: tasks table for hybrid task store.
444
+
445
+ Mirrors the structure of `docs/tasks/TASK-###-slug.md` files as a
446
+ queryable index. Files remain SSOT — the table is a derived cache
447
+ populated by `board_os/sync.py` (sole writer since TASK-398).
448
+ Dependencies are stored as a JSON-encoded
449
+ list so we can do `LIKE '%"TASK-195"%'` lookups for `cos_task_dependents`
450
+ without false-positive substring matches (TASK-19 vs TASK-195).
451
+ """
452
+ conn.executescript("""\
453
+ CREATE TABLE IF NOT EXISTS tasks (
454
+ task_id TEXT PRIMARY KEY,
455
+ title TEXT NOT NULL,
456
+ domain TEXT,
457
+ status TEXT NOT NULL DEFAULT 'open',
458
+ file_path TEXT NOT NULL,
459
+ content_hash TEXT NOT NULL,
460
+ mtime INTEGER NOT NULL,
461
+ goal_text TEXT,
462
+ scope_in TEXT,
463
+ scope_out TEXT,
464
+ requirements TEXT,
465
+ dependencies TEXT,
466
+ source_of_truth TEXT,
467
+ read_first TEXT,
468
+ open_questions TEXT,
469
+ rabbit_holes TEXT,
470
+ verification TEXT,
471
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
472
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
473
+ );
474
+
475
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
476
+ CREATE INDEX IF NOT EXISTS idx_tasks_domain ON tasks(domain);
477
+ CREATE INDEX IF NOT EXISTS idx_tasks_file_path ON tasks(file_path);
478
+ """)
479
+ logger.info("Tasks migration v6 applied: tasks table created")
480
+
481
+
482
+ def has_tasks_table(conn: sqlite3.Connection) -> bool:
483
+ """Check whether the tasks table exists (migration v6 applied).
484
+
485
+ Mirrors `has_fts5_table` / `has_embeddings_table` — callers can guard
486
+ task-related queries when running against an older DB.
487
+ """
488
+ row = conn.execute(
489
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='tasks'"
490
+ ).fetchone()
491
+ return row is not None
492
+
493
+
494
+ # ---------------------------------------------------------------------------
495
+ # Brain-hardening validators (shared constants)
496
+ # ---------------------------------------------------------------------------
497
+
498
+ VALID_TRUST_TIERS: frozenset[str] = frozenset({"volatile", "validated", "locked", "core"})
499
+ PROTECTED_TRUST_TIERS: frozenset[str] = frozenset({"locked", "core"})
500
+ VALID_PROVENANCE: frozenset[str] = frozenset(
501
+ {
502
+ "agent_self",
503
+ "user_directive",
504
+ "extracted_from_outcome",
505
+ "promoted_from_rule",
506
+ "imported",
507
+ }
508
+ )
509
+
510
+
511
+ def _column_exists(conn: sqlite3.Connection, table: str, column: str) -> bool:
512
+ """Return True if `column` is present in `table` per PRAGMA table_info."""
513
+ rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
514
+ return any(r[1] == column for r in rows)
515
+
516
+
517
+ def _migrate_v7_brain_hardening(conn: sqlite3.Connection) -> None:
518
+ """Migration v7: trust_tier + provenance + memory_audit."""
519
+ # 1. Add trust_tier + provenance to learned_patterns (idempotent per column)
520
+ if not _column_exists(conn, "learned_patterns", "trust_tier"):
521
+ conn.execute(
522
+ "ALTER TABLE learned_patterns ADD COLUMN trust_tier TEXT NOT NULL DEFAULT 'volatile'"
523
+ )
524
+ if not _column_exists(conn, "learned_patterns", "provenance"):
525
+ conn.execute(
526
+ "ALTER TABLE learned_patterns ADD COLUMN provenance TEXT NOT NULL DEFAULT 'agent_self'"
527
+ )
528
+
529
+ # 2. Add provenance to observations
530
+ if not _column_exists(conn, "observations", "provenance"):
531
+ conn.execute(
532
+ "ALTER TABLE observations ADD COLUMN provenance TEXT NOT NULL DEFAULT 'agent_self'"
533
+ )
534
+
535
+ # 3. memory_audit — append-only audit log
536
+ conn.executescript("""\
537
+ CREATE TABLE IF NOT EXISTS memory_audit (
538
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
539
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
540
+ actor TEXT NOT NULL,
541
+ action TEXT NOT NULL,
542
+ source_table TEXT NOT NULL,
543
+ source_id INTEGER,
544
+ old_value TEXT,
545
+ new_value TEXT,
546
+ reason TEXT
547
+ );
548
+
549
+ CREATE INDEX IF NOT EXISTS idx_memory_audit_table
550
+ ON memory_audit(source_table, source_id);
551
+ CREATE INDEX IF NOT EXISTS idx_memory_audit_created
552
+ ON memory_audit(created_at);
553
+
554
+ -- memory_audit is append-only: block any UPDATE or DELETE
555
+ CREATE TRIGGER IF NOT EXISTS trg_memory_audit_no_update
556
+ BEFORE UPDATE ON memory_audit
557
+ BEGIN
558
+ SELECT RAISE(ABORT, 'memory_audit is append-only');
559
+ END;
560
+
561
+ CREATE TRIGGER IF NOT EXISTS trg_memory_audit_no_delete
562
+ BEFORE DELETE ON memory_audit
563
+ BEGIN
564
+ SELECT RAISE(ABORT, 'memory_audit is append-only');
565
+ END;
566
+
567
+ -- Protect locked/core patterns from UPDATE
568
+ CREATE TRIGGER IF NOT EXISTS trg_learned_patterns_protect_update
569
+ BEFORE UPDATE ON learned_patterns
570
+ WHEN OLD.trust_tier IN ('locked', 'core')
571
+ BEGIN
572
+ SELECT RAISE(ABORT, 'learned_patterns: trust_tier locked/core is immutable via standard path');
573
+ END;
574
+
575
+ -- Protect locked/core patterns from DELETE
576
+ CREATE TRIGGER IF NOT EXISTS trg_learned_patterns_protect_delete
577
+ BEFORE DELETE ON learned_patterns
578
+ WHEN OLD.trust_tier IN ('locked', 'core')
579
+ BEGIN
580
+ SELECT RAISE(ABORT, 'learned_patterns: trust_tier locked/core cannot be deleted via standard path');
581
+ END;
582
+ """)
583
+ logger.info("Brain-hardening migration v7 applied: trust_tier, provenance, memory_audit")
584
+
585
+
586
+ def has_memory_audit_table(conn: sqlite3.Connection) -> bool:
587
+ """Check whether the memory_audit table exists (migration v7 applied)."""
588
+ row = conn.execute(
589
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='memory_audit'"
590
+ ).fetchone()
591
+ return row is not None
592
+
593
+
594
+ def is_pattern_protected(conn: sqlite3.Connection, pattern_id: int) -> bool:
595
+ """Return True if the pattern's trust_tier is in PROTECTED_TRUST_TIERS."""
596
+ if not _column_exists(conn, "learned_patterns", "trust_tier"):
597
+ return False # pre-v7 DB has no concept of protection
598
+ row = conn.execute(
599
+ "SELECT trust_tier FROM learned_patterns WHERE id = ?",
600
+ (pattern_id,),
601
+ ).fetchone()
602
+ if row is None:
603
+ return False
604
+ return row[0] in PROTECTED_TRUST_TIERS
605
+
606
+
607
+ def _migrate_v8_validation_throttle(conn: sqlite3.Connection) -> None:
608
+ """Migration v8: pattern_validations table for anti-sycophancy."""
609
+ conn.executescript("""\
610
+ CREATE TABLE IF NOT EXISTS pattern_validations (
611
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
612
+ session_id TEXT NOT NULL,
613
+ pattern_id INTEGER NOT NULL,
614
+ was_helpful INTEGER NOT NULL,
615
+ was_throttled INTEGER NOT NULL DEFAULT 0,
616
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
617
+ );
618
+
619
+ CREATE INDEX IF NOT EXISTS idx_pattern_validations_session_pattern
620
+ ON pattern_validations(session_id, pattern_id);
621
+ CREATE INDEX IF NOT EXISTS idx_pattern_validations_created
622
+ ON pattern_validations(created_at);
623
+ """)
624
+ logger.info("Validation-throttle migration v8 applied: pattern_validations")
625
+
626
+
627
+ def has_pattern_validations_table(conn: sqlite3.Connection) -> bool:
628
+ """Check whether the pattern_validations table exists (migration v8)."""
629
+ row = conn.execute(
630
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='pattern_validations'"
631
+ ).fetchone()
632
+ return row is not None
633
+
634
+
635
+ def _migrate_v9_docs_fts(conn: sqlite3.Connection) -> None:
636
+ """Migration v9: FTS5 virtual table over document_chunks."""
637
+ if not has_fts5(conn):
638
+ logger.warning(
639
+ "FTS5 unavailable — skipping document_chunks_fts. doc_search "
640
+ "lexical fallback will degrade to LIKE."
641
+ )
642
+ return
643
+
644
+ conn.executescript("""\
645
+ CREATE VIRTUAL TABLE IF NOT EXISTS document_chunks_fts USING fts5(
646
+ heading_path, content,
647
+ content='document_chunks', content_rowid='id'
648
+ );
649
+
650
+ CREATE TRIGGER IF NOT EXISTS document_chunks_ai AFTER INSERT ON document_chunks BEGIN
651
+ INSERT INTO document_chunks_fts(rowid, heading_path, content)
652
+ VALUES (new.id, new.heading_path, new.content);
653
+ END;
654
+
655
+ CREATE TRIGGER IF NOT EXISTS document_chunks_au AFTER UPDATE ON document_chunks BEGIN
656
+ INSERT INTO document_chunks_fts(document_chunks_fts, rowid, heading_path, content)
657
+ VALUES ('delete', old.id, old.heading_path, old.content);
658
+ INSERT INTO document_chunks_fts(rowid, heading_path, content)
659
+ VALUES (new.id, new.heading_path, new.content);
660
+ END;
661
+
662
+ CREATE TRIGGER IF NOT EXISTS document_chunks_ad AFTER DELETE ON document_chunks BEGIN
663
+ INSERT INTO document_chunks_fts(document_chunks_fts, rowid, heading_path, content)
664
+ VALUES ('delete', old.id, old.heading_path, old.content);
665
+ END;
666
+ """)
667
+
668
+ conn.execute(
669
+ "INSERT INTO document_chunks_fts(rowid, heading_path, content) "
670
+ "SELECT id, heading_path, content FROM document_chunks "
671
+ "WHERE id NOT IN (SELECT rowid FROM document_chunks_fts)"
672
+ )
673
+ logger.info("FTS5 docs migration v9 applied: document_chunks_fts")
674
+
675
+
676
+ def has_document_chunks_fts(conn: sqlite3.Connection) -> bool:
677
+ """Check whether document_chunks_fts exists (v9 + FTS5 available)."""
678
+ row = conn.execute(
679
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='document_chunks_fts'"
680
+ ).fetchone()
681
+ return row is not None
682
+
683
+
684
+ def has_tasks_fts(conn: sqlite3.Connection) -> bool:
685
+ """Check whether tasks_fts exists (v35 + FTS5 available)."""
686
+ row = conn.execute(
687
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='tasks_fts'"
688
+ ).fetchone()
689
+ return row is not None
690
+
691
+
692
+ def has_task_dependencies_table(conn: sqlite3.Connection) -> bool:
693
+ """Check whether the task_dependencies junction exists (v35 applied)."""
694
+ row = conn.execute(
695
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='task_dependencies'"
696
+ ).fetchone()
697
+ return row is not None
698
+
699
+
700
+ def _migrate_v10_retrievals(conn: sqlite3.Connection) -> None:
701
+ """Migration v10: retrievals table for outcome-driven priority."""
702
+ conn.executescript("""\
703
+ CREATE TABLE IF NOT EXISTS retrievals (
704
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
705
+ session_id TEXT NOT NULL,
706
+ task_id TEXT,
707
+ layer TEXT NOT NULL,
708
+ query TEXT NOT NULL,
709
+ source_table TEXT NOT NULL,
710
+ source_id INTEGER NOT NULL,
711
+ score REAL,
712
+ was_cited INTEGER NOT NULL DEFAULT 0,
713
+ outcome TEXT,
714
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
715
+ outcome_at DATETIME
716
+ );
717
+
718
+ CREATE INDEX IF NOT EXISTS idx_retrievals_task ON retrievals(task_id);
719
+ CREATE INDEX IF NOT EXISTS idx_retrievals_session ON retrievals(session_id);
720
+ CREATE INDEX IF NOT EXISTS idx_retrievals_source ON retrievals(source_table, source_id);
721
+ CREATE INDEX IF NOT EXISTS idx_retrievals_outcome ON retrievals(outcome);
722
+ """)
723
+ logger.info("Retrievals migration v10 applied")
724
+
725
+
726
+ def has_retrievals_table(conn: sqlite3.Connection) -> bool:
727
+ """Check whether the retrievals table exists (migration v10)."""
728
+ row = conn.execute(
729
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='retrievals'"
730
+ ).fetchone()
731
+ return row is not None
732
+
733
+
734
+ def _migrate_v11_retrieval_quality(conn: sqlite3.Connection) -> None:
735
+ """Migration v11: retrieval precision tracking."""
736
+ conn.executescript("""\
737
+ CREATE TABLE IF NOT EXISTS retrieval_quality (
738
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
739
+ retrieval_id INTEGER NOT NULL,
740
+ task_id TEXT,
741
+ layer TEXT NOT NULL,
742
+ query TEXT,
743
+ precision REAL,
744
+ signal_source TEXT NOT NULL,
745
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
746
+ );
747
+
748
+ CREATE INDEX IF NOT EXISTS idx_retrieval_quality_task
749
+ ON retrieval_quality(task_id);
750
+ CREATE INDEX IF NOT EXISTS idx_retrieval_quality_layer
751
+ ON retrieval_quality(layer);
752
+ CREATE INDEX IF NOT EXISTS idx_retrieval_quality_created
753
+ ON retrieval_quality(created_at);
754
+ """)
755
+
756
+ # Contextual chunk text — LLM-generated situating sentence prepended at
757
+ # embed time. Column is nullable until G.11 enrichment runs; retrieval
758
+ # stays on the plain heading-path prefix meanwhile.
759
+ if not _column_exists(conn, "document_chunks", "contextual_prefix"):
760
+ conn.execute("ALTER TABLE document_chunks ADD COLUMN contextual_prefix TEXT")
761
+ if not _column_exists(conn, "document_chunks", "context_model"):
762
+ conn.execute("ALTER TABLE document_chunks ADD COLUMN context_model TEXT")
763
+ logger.info("Migration v11 applied: retrieval_quality + contextual chunk columns")
764
+
765
+
766
+ def has_retrieval_quality_table(conn: sqlite3.Connection) -> bool:
767
+ """Check whether the retrieval_quality table exists (migration v11)."""
768
+ row = conn.execute(
769
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='retrieval_quality'"
770
+ ).fetchone()
771
+ return row is not None
772
+
773
+
774
+ def _migrate_v12_graph_os(conn: sqlite3.Connection) -> None:
775
+ """Migration v12: graph_os knowledge-graph tables."""
776
+ conn.executescript("""\
777
+ CREATE TABLE IF NOT EXISTS graph_nodes (
778
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
779
+ kind TEXT NOT NULL,
780
+ label TEXT NOT NULL,
781
+ uid TEXT NOT NULL UNIQUE,
782
+ file_path TEXT,
783
+ start_line INTEGER,
784
+ end_line INTEGER,
785
+ signature TEXT,
786
+ lang TEXT,
787
+ doc_blob TEXT,
788
+ ast_hash TEXT,
789
+ content_hash TEXT,
790
+ metadata_json TEXT DEFAULT '{}',
791
+ created_at INTEGER NOT NULL,
792
+ updated_at INTEGER NOT NULL
793
+ );
794
+ CREATE INDEX IF NOT EXISTS idx_graph_nodes_kind_lang ON graph_nodes(kind, lang);
795
+ CREATE INDEX IF NOT EXISTS idx_graph_nodes_file ON graph_nodes(file_path);
796
+
797
+ CREATE TABLE IF NOT EXISTS graph_edges_v12 (
798
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
799
+ source_id INTEGER NOT NULL,
800
+ target_id INTEGER NOT NULL,
801
+ edge_type TEXT NOT NULL,
802
+ confidence REAL NOT NULL DEFAULT 1.0,
803
+ extractor TEXT NOT NULL,
804
+ source_span TEXT,
805
+ created_at INTEGER NOT NULL,
806
+ updated_at INTEGER NOT NULL,
807
+ UNIQUE(source_id, target_id, edge_type, extractor),
808
+ FOREIGN KEY (source_id) REFERENCES graph_nodes(id) ON DELETE CASCADE,
809
+ FOREIGN KEY (target_id) REFERENCES graph_nodes(id) ON DELETE CASCADE
810
+ );
811
+ CREATE INDEX IF NOT EXISTS idx_graph_edges_source ON graph_edges_v12(source_id, edge_type);
812
+ CREATE INDEX IF NOT EXISTS idx_graph_edges_target ON graph_edges_v12(target_id, edge_type);
813
+ CREATE INDEX IF NOT EXISTS idx_graph_edges_type ON graph_edges_v12(edge_type);
814
+
815
+ CREATE TABLE IF NOT EXISTS graph_evidence_v12 (
816
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
817
+ edge_id INTEGER NOT NULL,
818
+ signal_name TEXT NOT NULL,
819
+ weight REAL NOT NULL,
820
+ note TEXT,
821
+ created_at INTEGER NOT NULL,
822
+ FOREIGN KEY (edge_id) REFERENCES graph_edges_v12(id) ON DELETE CASCADE
823
+ );
824
+ CREATE INDEX IF NOT EXISTS idx_graph_evidence_edge ON graph_evidence_v12(edge_id);
825
+ """)
826
+
827
+ if has_fts5(conn):
828
+ conn.executescript("""\
829
+ CREATE VIRTUAL TABLE IF NOT EXISTS graph_nodes_fts USING fts5(
830
+ label, signature, doc_blob,
831
+ content=graph_nodes,
832
+ content_rowid=id,
833
+ tokenize='porter unicode61'
834
+ );
835
+
836
+ CREATE TRIGGER IF NOT EXISTS graph_nodes_fts_ai AFTER INSERT ON graph_nodes BEGIN
837
+ INSERT INTO graph_nodes_fts(rowid, label, signature, doc_blob)
838
+ VALUES (new.id, new.label, COALESCE(new.signature, ''), COALESCE(new.doc_blob, ''));
839
+ END;
840
+ CREATE TRIGGER IF NOT EXISTS graph_nodes_fts_ad AFTER DELETE ON graph_nodes BEGIN
841
+ INSERT INTO graph_nodes_fts(graph_nodes_fts, rowid, label, signature, doc_blob)
842
+ VALUES ('delete', old.id, old.label, COALESCE(old.signature, ''), COALESCE(old.doc_blob, ''));
843
+ END;
844
+ CREATE TRIGGER IF NOT EXISTS graph_nodes_fts_au AFTER UPDATE ON graph_nodes BEGIN
845
+ INSERT INTO graph_nodes_fts(graph_nodes_fts, rowid, label, signature, doc_blob)
846
+ VALUES ('delete', old.id, old.label, COALESCE(old.signature, ''), COALESCE(old.doc_blob, ''));
847
+ INSERT INTO graph_nodes_fts(rowid, label, signature, doc_blob)
848
+ VALUES (new.id, new.label, COALESCE(new.signature, ''), COALESCE(new.doc_blob, ''));
849
+ END;
850
+ """)
851
+
852
+ if has_embeddings_table(conn) and not _column_exists(conn, "embeddings", "embedding_dim"):
853
+ conn.execute("ALTER TABLE embeddings ADD COLUMN embedding_dim INTEGER DEFAULT 384")
854
+
855
+ logger.info(
856
+ "Migration v12 applied: graph_nodes + graph_edges_v12 + "
857
+ "graph_evidence_v12 + graph_nodes_fts; embeddings.embedding_dim added"
858
+ )
859
+
860
+
861
+ def has_graph_nodes_table(conn: sqlite3.Connection) -> bool:
862
+ """Check whether the graph_nodes table exists (migration v12)."""
863
+ row = conn.execute(
864
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='graph_nodes'"
865
+ ).fetchone()
866
+ return row is not None
867
+
868
+
869
+ def has_graph_edges_table(conn: sqlite3.Connection) -> bool:
870
+ """Check whether the graph_edges_v12 table exists (migration v12)."""
871
+ row = conn.execute(
872
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='graph_edges_v12'"
873
+ ).fetchone()
874
+ return row is not None
875
+
876
+
877
+ def has_graph_evidence_table(conn: sqlite3.Connection) -> bool:
878
+ """Check whether the graph_evidence_v12 table exists (migration v12)."""
879
+ row = conn.execute(
880
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='graph_evidence_v12'"
881
+ ).fetchone()
882
+ return row is not None
883
+
884
+
885
+ def has_graph_nodes_fts(conn: sqlite3.Connection) -> bool:
886
+ """Check whether the graph_nodes_fts virtual table exists (v12)."""
887
+ row = conn.execute(
888
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='graph_nodes_fts'"
889
+ ).fetchone()
890
+ return row is not None
891
+
892
+
893
+ def _migrate_v13_board_os(conn: sqlite3.Connection) -> None:
894
+ """Migration v13: board_os Scrumban task workflow extensions."""
895
+ # Idempotent ADD COLUMN (re-running is safe).
896
+ if not _column_exists(conn, "tasks", "swimlane"):
897
+ conn.execute("ALTER TABLE tasks ADD COLUMN swimlane TEXT")
898
+ if not _column_exists(conn, "tasks", "kind"):
899
+ conn.execute("ALTER TABLE tasks ADD COLUMN kind TEXT")
900
+ if not _column_exists(conn, "tasks", "epic"):
901
+ conn.execute("ALTER TABLE tasks ADD COLUMN epic TEXT")
902
+ if not _column_exists(conn, "tasks", "labels_json"):
903
+ conn.execute("ALTER TABLE tasks ADD COLUMN labels_json TEXT DEFAULT '[]'")
904
+ if not _column_exists(conn, "tasks", "priority"):
905
+ conn.execute("ALTER TABLE tasks ADD COLUMN priority TEXT")
906
+ if not _column_exists(conn, "tasks", "appetite"):
907
+ conn.execute("ALTER TABLE tasks ADD COLUMN appetite TEXT")
908
+ if not _column_exists(conn, "tasks", "started_at"):
909
+ conn.execute("ALTER TABLE tasks ADD COLUMN started_at INTEGER")
910
+ if not _column_exists(conn, "tasks", "completed_at"):
911
+ conn.execute("ALTER TABLE tasks ADD COLUMN completed_at INTEGER")
912
+ if not _column_exists(conn, "tasks", "agent_session"):
913
+ conn.execute("ALTER TABLE tasks ADD COLUMN agent_session TEXT")
914
+ if not _column_exists(conn, "tasks", "work_log_last_5"):
915
+ conn.execute("ALTER TABLE tasks ADD COLUMN work_log_last_5 TEXT DEFAULT '[]'")
916
+
917
+ conn.executescript("""\
918
+ CREATE TABLE IF NOT EXISTS task_status_history (
919
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
920
+ task_id TEXT NOT NULL,
921
+ old_status TEXT NOT NULL,
922
+ new_status TEXT NOT NULL,
923
+ agent_session TEXT,
924
+ reason TEXT,
925
+ transitioned_at INTEGER NOT NULL
926
+ );
927
+ CREATE INDEX IF NOT EXISTS idx_tsh_task
928
+ ON task_status_history(task_id, transitioned_at);
929
+ CREATE INDEX IF NOT EXISTS idx_tsh_session
930
+ ON task_status_history(agent_session, transitioned_at)
931
+ WHERE agent_session IS NOT NULL;
932
+
933
+ CREATE INDEX IF NOT EXISTS idx_tasks_swimlane_status
934
+ ON tasks(swimlane, status);
935
+ CREATE INDEX IF NOT EXISTS idx_tasks_kind_status
936
+ ON tasks(kind, status);
937
+ CREATE INDEX IF NOT EXISTS idx_tasks_epic
938
+ ON tasks(epic) WHERE epic IS NOT NULL;
939
+ CREATE INDEX IF NOT EXISTS idx_tasks_priority_status
940
+ ON tasks(priority, status)
941
+ WHERE status IN ('ready', 'in_progress', 'emergency');
942
+ """)
943
+
944
+ logger.info(
945
+ "Migration v13 applied: tasks +swimlane/kind/epic/"
946
+ "priority/appetite/started_at/completed_at/agent_session/"
947
+ "labels_json/work_log_last_5; task_status_history table; "
948
+ "5 new indices"
949
+ )
950
+
951
+
952
+ def _migrate_v14_cognition(conn: sqlite3.Connection) -> None:
953
+ """Migration v14: formula-agent supervisor cognition tables."""
954
+ conn.executescript("""\
955
+ CREATE TABLE IF NOT EXISTS backtrack_events (
956
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
957
+ session_id TEXT NOT NULL,
958
+ from_formula TEXT NOT NULL,
959
+ to_formula TEXT NOT NULL,
960
+ reason TEXT NOT NULL,
961
+ ts TEXT NOT NULL DEFAULT (datetime('now'))
962
+ );
963
+ CREATE INDEX IF NOT EXISTS idx_backtrack_session
964
+ ON backtrack_events(session_id, ts);
965
+
966
+ CREATE TABLE IF NOT EXISTS persona_selections (
967
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
968
+ session_id TEXT NOT NULL,
969
+ task_marker TEXT,
970
+ persona_id TEXT NOT NULL,
971
+ confidence REAL NOT NULL,
972
+ reason TEXT,
973
+ intensity TEXT NOT NULL,
974
+ ts TEXT NOT NULL DEFAULT (datetime('now'))
975
+ );
976
+ CREATE INDEX IF NOT EXISTS idx_persona_session
977
+ ON persona_selections(session_id);
978
+
979
+ CREATE TABLE IF NOT EXISTS ambiguity_violations (
980
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
981
+ session_id TEXT NOT NULL,
982
+ formula_id TEXT NOT NULL,
983
+ step_id TEXT,
984
+ criterion TEXT NOT NULL,
985
+ detail TEXT,
986
+ ts TEXT NOT NULL DEFAULT (datetime('now'))
987
+ );
988
+ CREATE INDEX IF NOT EXISTS idx_ambiguity_session
989
+ ON ambiguity_violations(session_id);
990
+
991
+ CREATE TABLE IF NOT EXISTS formula_dispatches (
992
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
993
+ session_id TEXT NOT NULL,
994
+ task_marker TEXT,
995
+ persona_id TEXT NOT NULL,
996
+ formula_id TEXT NOT NULL,
997
+ input_hash TEXT NOT NULL,
998
+ output_hash TEXT,
999
+ latency_ms INTEGER,
1000
+ status TEXT NOT NULL,
1001
+ ts TEXT NOT NULL DEFAULT (datetime('now'))
1002
+ );
1003
+ CREATE INDEX IF NOT EXISTS idx_dispatches_session
1004
+ ON formula_dispatches(session_id, ts);
1005
+ """)
1006
+ logger.info(
1007
+ "Migration v14 applied: backtrack_events, persona_selections, "
1008
+ "ambiguity_violations, formula_dispatches + 4 indices"
1009
+ )
1010
+
1011
+
1012
+ def _migrate_v15_graph_edges_confidence_check(conn: sqlite3.Connection) -> None:
1013
+ """Migration v15 (graph_os S1 / B17): CHECK (confidence BETWEEN 0 AND 1)."""
1014
+ conn.executescript("""\
1015
+ CREATE TRIGGER IF NOT EXISTS graph_edges_v12_confidence_ins
1016
+ BEFORE INSERT ON graph_edges_v12
1017
+ FOR EACH ROW
1018
+ WHEN NEW.confidence IS NULL
1019
+ OR NEW.confidence < 0.0
1020
+ OR NEW.confidence > 1.0
1021
+ BEGIN
1022
+ SELECT RAISE(ABORT, 'graph_edges_v12.confidence must lie in [0,1]');
1023
+ END;
1024
+
1025
+ CREATE TRIGGER IF NOT EXISTS graph_edges_v12_confidence_upd
1026
+ BEFORE UPDATE OF confidence ON graph_edges_v12
1027
+ FOR EACH ROW
1028
+ WHEN NEW.confidence IS NULL
1029
+ OR NEW.confidence < 0.0
1030
+ OR NEW.confidence > 1.0
1031
+ BEGIN
1032
+ SELECT RAISE(ABORT, 'graph_edges_v12.confidence must lie in [0,1]');
1033
+ END;
1034
+ """)
1035
+ logger.info("Migration v15 applied: graph_edges_v12 confidence CHECK triggers")
1036
+
1037
+
1038
+ def _migrate_v16_normalize_graph_node_kinds(conn: sqlite3.Connection) -> None:
1039
+ """Migration v16 (graph_os S3): normalize graph_nodes.kind values."""
1040
+ row = conn.execute(
1041
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='graph_nodes'"
1042
+ ).fetchone()
1043
+ if row is None:
1044
+ logger.debug("Migration v16: graph_nodes table not present — skip")
1045
+ return
1046
+
1047
+ # Resolve ``normalize_kind`` via a sys.path-side-door so this
1048
+ # migration works both under the MCP server (which already has
1049
+ # ``core/`` on sys.path) and under test fixtures that only
1050
+ # pre-register ``core/thinking_os``.
1051
+ try:
1052
+ import sys as _sys
1053
+ from pathlib import Path as _Path
1054
+
1055
+ core_dir = _Path(__file__).resolve().parent.parent
1056
+ core_str = str(core_dir)
1057
+ if core_str not in _sys.path:
1058
+ _sys.path.insert(0, core_str)
1059
+ from graph_os.types import normalize_kind as _normalize # type: ignore
1060
+ except Exception as exc:
1061
+ logger.warning(
1062
+ "Migration v16 could not import normalize_kind (%s) — "
1063
+ "skipping normalization; rows remain in legacy form",
1064
+ exc,
1065
+ )
1066
+ return
1067
+
1068
+ rows = conn.execute("SELECT DISTINCT kind FROM graph_nodes").fetchall()
1069
+ rename_map: dict[str, str] = {}
1070
+ for r in rows:
1071
+ legacy = r[0]
1072
+ if legacy is None:
1073
+ continue
1074
+ try:
1075
+ canonical = _normalize(legacy).value
1076
+ except ValueError:
1077
+ # Unknown kind — leave as-is so we don't silently drop data.
1078
+ continue
1079
+ if canonical != legacy:
1080
+ rename_map[legacy] = canonical
1081
+
1082
+ total_updated = 0
1083
+ for legacy, canonical in rename_map.items():
1084
+ cur = conn.execute(
1085
+ "UPDATE graph_nodes SET kind = ? WHERE kind = ?",
1086
+ (canonical, legacy),
1087
+ )
1088
+ total_updated += cur.rowcount or 0
1089
+ conn.commit()
1090
+ logger.info(
1091
+ "Migration v16 applied: graph_nodes.kind normalized "
1092
+ "(%d kind(s) rewritten, %d row(s) updated)",
1093
+ len(rename_map),
1094
+ total_updated,
1095
+ )
1096
+
1097
+
1098
+ def _migrate_v17_file_index_state(conn: sqlite3.Connection) -> None:
1099
+ """Migration v17 (graph_os V1): per-file content-hash cache."""
1100
+ conn.executescript(
1101
+ """
1102
+ CREATE TABLE IF NOT EXISTS file_index_state (
1103
+ file_path TEXT NOT NULL,
1104
+ content_hash TEXT NOT NULL,
1105
+ extractor_chain TEXT NOT NULL,
1106
+ nodes_written INTEGER NOT NULL,
1107
+ edges_written INTEGER NOT NULL,
1108
+ parse_errors_count INTEGER NOT NULL DEFAULT 0,
1109
+ last_indexed_at INTEGER NOT NULL,
1110
+ last_error TEXT,
1111
+ PRIMARY KEY (file_path, extractor_chain)
1112
+ );
1113
+ CREATE INDEX IF NOT EXISTS idx_file_index_state_hash
1114
+ ON file_index_state(content_hash);
1115
+ """
1116
+ )
1117
+ conn.commit()
1118
+ logger.info("Migration v17 applied: file_index_state table + hash index")
1119
+
1120
+
1121
+ def _migrate_v18_retrieval_router_log(conn: sqlite3.Connection) -> None:
1122
+ """Migration v18: retrieval_router_log append-only table."""
1123
+ conn.executescript(
1124
+ """
1125
+ CREATE TABLE IF NOT EXISTS retrieval_router_log (
1126
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1127
+ query_hash TEXT NOT NULL,
1128
+ query_shape TEXT NOT NULL,
1129
+ confidence REAL NOT NULL,
1130
+ chosen_layer TEXT,
1131
+ fanout_layers TEXT,
1132
+ bytes_returned INTEGER,
1133
+ truncated INTEGER DEFAULT 0,
1134
+ agent_override TEXT,
1135
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
1136
+ );
1137
+ CREATE INDEX IF NOT EXISTS idx_router_log_created
1138
+ ON retrieval_router_log(created_at);
1139
+ CREATE INDEX IF NOT EXISTS idx_router_log_shape
1140
+ ON retrieval_router_log(query_shape);
1141
+ """
1142
+ )
1143
+ conn.commit()
1144
+ logger.info("Migration v18 applied: retrieval_router_log table + indexes")
1145
+
1146
+
1147
+ def _migrate_v19_drop_ready_status(conn: sqlite3.Connection) -> None:
1148
+ """Migration v19: fold 'ready' status into icebox + 'ready' label."""
1149
+ rows = conn.execute(
1150
+ "SELECT task_id, labels_json FROM tasks WHERE status = 'ready'",
1151
+ ).fetchall()
1152
+
1153
+ if not rows:
1154
+ logger.info("Migration v19 applied: no 'ready' rows to migrate (clean DB)")
1155
+ return
1156
+
1157
+ import json as _json
1158
+ import time as _time
1159
+
1160
+ now_epoch = int(_time.time())
1161
+
1162
+ for task_id, labels_json in rows:
1163
+ try:
1164
+ labels = _json.loads(labels_json) if labels_json else []
1165
+ if not isinstance(labels, list):
1166
+ labels = []
1167
+ except (TypeError, ValueError):
1168
+ labels = []
1169
+ if "ready" not in labels:
1170
+ labels.append("ready")
1171
+ conn.execute(
1172
+ "UPDATE tasks SET status = 'icebox', labels_json = ? WHERE task_id = ?",
1173
+ (_json.dumps(labels, ensure_ascii=False), task_id),
1174
+ )
1175
+ conn.execute(
1176
+ """
1177
+ INSERT INTO task_status_history
1178
+ (task_id, old_status, new_status, agent_session,
1179
+ reason, transitioned_at)
1180
+ VALUES (?, 'ready', 'icebox', NULL, ?, ?)
1181
+ """,
1182
+ (task_id, "migrated from ready column (v19)", now_epoch),
1183
+ )
1184
+ conn.commit()
1185
+ logger.info(
1186
+ "Migration v19 applied: folded %d 'ready' task(s) into icebox + label",
1187
+ len(rows),
1188
+ )
1189
+
1190
+
1191
+ def _column_exists_table(conn: sqlite3.Connection, table: str, column: str) -> bool:
1192
+ """Local helper — pragma table_info reads. Defined inline to keep
1193
+ the migration self-contained (the file already has _column_exists
1194
+ earlier; this is only used by v20)."""
1195
+ rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
1196
+ return any(r[1] == column for r in rows)
1197
+
1198
+
1199
+ def _migrate_v20_override_audit(conn: sqlite3.Connection) -> None:
1200
+ """Migration v20 — override audit columns on task_status_history."""
1201
+ if not has_task_status_history_table(conn):
1202
+ # Older DBs that never reached v13 don't have this table; the
1203
+ # v13 migration will create it with the modern shape via
1204
+ # _migrate_v13_board_os, but if a future re-run order is shuffled
1205
+ # we should be defensive.
1206
+ logger.info(
1207
+ "Migration v20 skipped: task_status_history not present yet "
1208
+ "(v13 will create it; v20 re-runs once v13 lands)"
1209
+ )
1210
+ return
1211
+
1212
+ if not _column_exists_table(conn, "task_status_history", "override_reason"):
1213
+ conn.execute("ALTER TABLE task_status_history ADD COLUMN override_reason TEXT")
1214
+ if not _column_exists_table(conn, "task_status_history", "override_actor"):
1215
+ conn.execute("ALTER TABLE task_status_history ADD COLUMN override_actor TEXT")
1216
+
1217
+ # Index lets retro/audit queries scan only override rows efficiently.
1218
+ conn.execute(
1219
+ "CREATE INDEX IF NOT EXISTS idx_tsh_override "
1220
+ "ON task_status_history(override_reason) "
1221
+ "WHERE override_reason IS NOT NULL"
1222
+ )
1223
+ conn.commit()
1224
+ logger.info("Migration v20 applied: override audit columns on task_status_history")
1225
+
1226
+
1227
+ def _migrate_v21_doc_audit_trail(conn: sqlite3.Connection) -> None:
1228
+ """Migration v21 — append-only doc edit + decision-history log."""
1229
+ conn.executescript(
1230
+ """
1231
+ CREATE TABLE IF NOT EXISTS doc_audit_trail (
1232
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1233
+ doc_path TEXT NOT NULL,
1234
+ session_id TEXT,
1235
+ agent TEXT,
1236
+ action TEXT NOT NULL CHECK (action IN (
1237
+ 'created','updated','deleted','reverted','moved','renamed'
1238
+ )),
1239
+ old_frontmatter TEXT,
1240
+ new_frontmatter TEXT,
1241
+ old_content_hash TEXT,
1242
+ new_content_hash TEXT,
1243
+ reason TEXT,
1244
+ supersedes_id INTEGER REFERENCES doc_audit_trail(id),
1245
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
1246
+ );
1247
+
1248
+ CREATE INDEX IF NOT EXISTS idx_doc_audit_path_created
1249
+ ON doc_audit_trail(doc_path, created_at DESC);
1250
+
1251
+ CREATE INDEX IF NOT EXISTS idx_doc_audit_session
1252
+ ON doc_audit_trail(session_id);
1253
+
1254
+ CREATE INDEX IF NOT EXISTS idx_doc_audit_supersedes
1255
+ ON doc_audit_trail(supersedes_id)
1256
+ WHERE supersedes_id IS NOT NULL;
1257
+
1258
+ -- Append-only: forbid UPDATE / DELETE on this table. The audit
1259
+ -- log is meaningful only if it cannot be rewritten.
1260
+ CREATE TRIGGER IF NOT EXISTS doc_audit_trail_no_update
1261
+ BEFORE UPDATE ON doc_audit_trail
1262
+ BEGIN
1263
+ SELECT RAISE(FAIL, 'doc_audit_trail is append-only');
1264
+ END;
1265
+
1266
+ CREATE TRIGGER IF NOT EXISTS doc_audit_trail_no_delete
1267
+ BEFORE DELETE ON doc_audit_trail
1268
+ BEGIN
1269
+ SELECT RAISE(FAIL, 'doc_audit_trail is append-only');
1270
+ END;
1271
+ """
1272
+ )
1273
+ conn.commit()
1274
+ logger.info("Migration v21 applied: doc_audit_trail (append-only)")
1275
+
1276
+
1277
+ def has_doc_audit_trail_table(conn: sqlite3.Connection) -> bool:
1278
+ """Check whether doc_audit_trail exists (migration v21)."""
1279
+ row = conn.execute(
1280
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='doc_audit_trail'"
1281
+ ).fetchone()
1282
+ return row is not None
1283
+
1284
+
1285
+ def _migrate_v22_doc_chunks_metadata(conn: sqlite3.Connection) -> None:
1286
+ """Migration v22 — frontmatter metadata columns on document_chunks."""
1287
+ if not _table_exists(conn, "document_chunks"):
1288
+ logger.info("Migration v22 skipped: document_chunks not present yet")
1289
+ return
1290
+
1291
+ cols = [
1292
+ ("domain", "TEXT"),
1293
+ ("layer", "TEXT"),
1294
+ ("ssot", "TEXT"),
1295
+ ("updated_iso", "TEXT"),
1296
+ ("is_active", "INTEGER DEFAULT 1"),
1297
+ ]
1298
+ for name, decl in cols:
1299
+ if not _column_exists_table(conn, "document_chunks", name):
1300
+ conn.execute(f"ALTER TABLE document_chunks ADD COLUMN {name} {decl}")
1301
+
1302
+ # Backfill is_active for rows that pre-date this migration.
1303
+ conn.execute("UPDATE document_chunks SET is_active = 1 WHERE is_active IS NULL")
1304
+
1305
+ conn.executescript(
1306
+ """
1307
+ CREATE INDEX IF NOT EXISTS idx_chunks_domain
1308
+ ON document_chunks(domain) WHERE domain IS NOT NULL;
1309
+ CREATE INDEX IF NOT EXISTS idx_chunks_layer
1310
+ ON document_chunks(layer) WHERE layer IS NOT NULL;
1311
+ CREATE INDEX IF NOT EXISTS idx_chunks_active
1312
+ ON document_chunks(is_active);
1313
+ CREATE INDEX IF NOT EXISTS idx_chunks_updated
1314
+ ON document_chunks(updated_iso) WHERE updated_iso IS NOT NULL;
1315
+ """
1316
+ )
1317
+ conn.commit()
1318
+ logger.info(
1319
+ "Migration v22 applied: document_chunks gained domain/layer/ssot/updated_iso/is_active"
1320
+ )
1321
+
1322
+
1323
+ def _migrate_v23_dispatch_cost(conn: sqlite3.Connection) -> None:
1324
+ """Migration v23 — formula_dispatches cost columns."""
1325
+ if not _table_exists(conn, "formula_dispatches"):
1326
+ logger.info("Migration v23 skipped: formula_dispatches not present yet")
1327
+ return
1328
+
1329
+ cols = [
1330
+ ("cost_usd", "REAL"),
1331
+ ("budget_usd", "REAL"),
1332
+ ("usage_jsonb", "TEXT"),
1333
+ ("model_usage_jsonb", "TEXT"),
1334
+ ("tool_calls_jsonb", "TEXT"),
1335
+ ("tool_failures_jsonb", "TEXT"),
1336
+ ]
1337
+ for name, decl in cols:
1338
+ if not _column_exists_table(conn, "formula_dispatches", name):
1339
+ conn.execute(f"ALTER TABLE formula_dispatches ADD COLUMN {name} {decl}")
1340
+
1341
+ conn.executescript(
1342
+ """
1343
+ CREATE INDEX IF NOT EXISTS idx_dispatches_cost
1344
+ ON formula_dispatches(cost_usd) WHERE cost_usd IS NOT NULL;
1345
+ """
1346
+ )
1347
+ conn.commit()
1348
+ logger.info(
1349
+ "Migration v23 applied: formula_dispatches gained "
1350
+ "cost_usd / budget_usd / usage_jsonb / model_usage_jsonb / "
1351
+ "tool_calls_jsonb / tool_failures_jsonb"
1352
+ )
1353
+
1354
+
1355
+ def _migrate_v24_project_trajectory(conn: sqlite3.Connection) -> None:
1356
+ """Migration v24 — project_trajectory table for long-term project intent."""
1357
+ conn.executescript("""\
1358
+ CREATE TABLE IF NOT EXISTS project_trajectory (
1359
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1360
+ session_id TEXT NOT NULL,
1361
+ phase TEXT,
1362
+ current_focus TEXT,
1363
+ architectural_decisions TEXT DEFAULT '[]',
1364
+ anti_patterns_discovered TEXT DEFAULT '[]',
1365
+ open_questions TEXT DEFAULT '[]',
1366
+ next_logical_step TEXT,
1367
+ confidence REAL DEFAULT 0.7,
1368
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1369
+ supersedes_id INTEGER REFERENCES project_trajectory(id)
1370
+ );
1371
+
1372
+ CREATE INDEX IF NOT EXISTS idx_trajectory_session
1373
+ ON project_trajectory(session_id);
1374
+ CREATE INDEX IF NOT EXISTS idx_trajectory_created
1375
+ ON project_trajectory(created_at DESC);
1376
+ """)
1377
+ conn.commit()
1378
+ logger.info("Migration v24 applied: project_trajectory table created")
1379
+
1380
+
1381
+ def _migrate_v25_backtrack_failure_anatomy(conn: sqlite3.Connection) -> None:
1382
+ """Migration v25 — structured failure anatomy columns on backtrack_events."""
1383
+ if not _table_exists(conn, "backtrack_events"):
1384
+ logger.info("Migration v25 skipped: backtrack_events not present yet")
1385
+ return
1386
+
1387
+ cols = [
1388
+ ("hypothesis", "TEXT"),
1389
+ ("failure_signal", "TEXT"),
1390
+ ("root_cause", "TEXT"),
1391
+ ("corrective_action", "TEXT"),
1392
+ ]
1393
+ for name, decl in cols:
1394
+ if not _column_exists_table(conn, "backtrack_events", name):
1395
+ conn.execute(f"ALTER TABLE backtrack_events ADD COLUMN {name} {decl}")
1396
+
1397
+ conn.executescript("""\
1398
+ CREATE INDEX IF NOT EXISTS idx_backtrack_root_cause
1399
+ ON backtrack_events(root_cause) WHERE root_cause IS NOT NULL;
1400
+ """)
1401
+ conn.commit()
1402
+ logger.info(
1403
+ "Migration v25 applied: backtrack_events gained "
1404
+ "hypothesis / failure_signal / root_cause / corrective_action"
1405
+ )
1406
+
1407
+
1408
+ def _migrate_v26_routing_evolution(conn: sqlite3.Connection) -> None:
1409
+ """Migration v26 — routing_weights staleness tracking for autonomous refresh."""
1410
+ if not _table_exists(conn, "routing_weights"):
1411
+ logger.info("Migration v26 skipped: routing_weights not present yet")
1412
+ return
1413
+
1414
+ for name, decl in [("last_recalc_at", "TEXT"), ("outcomes_at_recalc", "INTEGER")]:
1415
+ if not _column_exists_table(conn, "routing_weights", name):
1416
+ conn.execute(f"ALTER TABLE routing_weights ADD COLUMN {name} {decl}")
1417
+ conn.commit()
1418
+ logger.info("Migration v26 applied: routing_weights gained last_recalc_at / outcomes_at_recalc")
1419
+
1420
+
1421
+ def _migrate_v27_dispatch_sdk_columns(conn: sqlite3.Connection) -> None:
1422
+ """Migration v27 — full SDK telemetry on formula_dispatches."""
1423
+ if not _table_exists(conn, "formula_dispatches"):
1424
+ logger.info("Migration v27 skipped: formula_dispatches not present yet")
1425
+ return
1426
+
1427
+ cols = [
1428
+ ("sub_session_id", "TEXT"),
1429
+ ("model", "TEXT"),
1430
+ ("checkpoints_jsonb", "TEXT"),
1431
+ ]
1432
+ for name, decl in cols:
1433
+ if not _column_exists_table(conn, "formula_dispatches", name):
1434
+ conn.execute(f"ALTER TABLE formula_dispatches ADD COLUMN {name} {decl}")
1435
+
1436
+ conn.executescript(
1437
+ """
1438
+ CREATE INDEX IF NOT EXISTS idx_dispatches_sub_session
1439
+ ON formula_dispatches(sub_session_id) WHERE sub_session_id IS NOT NULL;
1440
+ CREATE INDEX IF NOT EXISTS idx_dispatches_model
1441
+ ON formula_dispatches(model) WHERE model IS NOT NULL;
1442
+ """
1443
+ )
1444
+ conn.commit()
1445
+ logger.info(
1446
+ "Migration v27 applied: formula_dispatches gained "
1447
+ "sub_session_id / model / checkpoints_jsonb"
1448
+ )
1449
+
1450
+
1451
+ def _migrate_v28_file_index_duration(conn: sqlite3.Connection) -> None:
1452
+ """Migration v28 — per-extractor timing on file_index_state."""
1453
+ if not _table_exists(conn, "file_index_state"):
1454
+ logger.info("Migration v28 skipped: file_index_state not present yet")
1455
+ return
1456
+ if not _column_exists_table(conn, "file_index_state", "duration_ms"):
1457
+ conn.execute("ALTER TABLE file_index_state ADD COLUMN duration_ms INTEGER")
1458
+ conn.commit()
1459
+ logger.info("Migration v28 applied: file_index_state gained duration_ms")
1460
+
1461
+
1462
+ def _migrate_v29_fts5_unicode_tokenizer(conn: sqlite3.Connection) -> None:
1463
+ """Migration v29 (G14) — drop `porter` from FTS5 so non-English tokens
1464
+ (Persian, Arabic, Chinese) are actually indexed. porter is an
1465
+ English-only stemmer that strips Persian body content silently;
1466
+ unicode61 alone normalises + tokenises by Unicode letter classes.
1467
+ """
1468
+ if not _table_exists(conn, "graph_nodes"):
1469
+ logger.info("Migration v29 skipped: graph_nodes not present yet")
1470
+ return
1471
+ # Detect current tokenizer; only act when porter is still configured.
1472
+ row = conn.execute(
1473
+ "SELECT sql FROM sqlite_master WHERE type='table' AND name='graph_nodes_fts'"
1474
+ ).fetchone()
1475
+ if row is None:
1476
+ logger.info("Migration v29 skipped: graph_nodes_fts not present")
1477
+ return
1478
+ current_sql = row[0] or ""
1479
+ if "porter" not in current_sql:
1480
+ logger.info("Migration v29 skipped: FTS5 already on unicode61-only tokenizer")
1481
+ return
1482
+ conn.executescript(
1483
+ """
1484
+ DROP TRIGGER IF EXISTS graph_nodes_fts_ai;
1485
+ DROP TRIGGER IF EXISTS graph_nodes_fts_ad;
1486
+ DROP TRIGGER IF EXISTS graph_nodes_fts_au;
1487
+ DROP TABLE IF EXISTS graph_nodes_fts;
1488
+ CREATE VIRTUAL TABLE graph_nodes_fts USING fts5(
1489
+ label, signature, doc_blob,
1490
+ content=graph_nodes,
1491
+ content_rowid=id,
1492
+ tokenize='unicode61 remove_diacritics 2'
1493
+ );
1494
+ INSERT INTO graph_nodes_fts(rowid, label, signature, doc_blob)
1495
+ SELECT id, label, COALESCE(signature, ''), COALESCE(doc_blob, '')
1496
+ FROM graph_nodes;
1497
+ CREATE TRIGGER graph_nodes_fts_ai AFTER INSERT ON graph_nodes BEGIN
1498
+ INSERT INTO graph_nodes_fts(rowid, label, signature, doc_blob)
1499
+ VALUES (new.id, new.label, COALESCE(new.signature, ''), COALESCE(new.doc_blob, ''));
1500
+ END;
1501
+ CREATE TRIGGER graph_nodes_fts_ad AFTER DELETE ON graph_nodes BEGIN
1502
+ INSERT INTO graph_nodes_fts(graph_nodes_fts, rowid, label, signature, doc_blob)
1503
+ VALUES ('delete', old.id, old.label, COALESCE(old.signature, ''), COALESCE(old.doc_blob, ''));
1504
+ END;
1505
+ CREATE TRIGGER graph_nodes_fts_au AFTER UPDATE ON graph_nodes BEGIN
1506
+ INSERT INTO graph_nodes_fts(graph_nodes_fts, rowid, label, signature, doc_blob)
1507
+ VALUES ('delete', old.id, old.label, COALESCE(old.signature, ''), COALESCE(old.doc_blob, ''));
1508
+ INSERT INTO graph_nodes_fts(rowid, label, signature, doc_blob)
1509
+ VALUES (new.id, new.label, COALESCE(new.signature, ''), COALESCE(new.doc_blob, ''));
1510
+ END;
1511
+ """
1512
+ )
1513
+ conn.commit()
1514
+ logger.info(
1515
+ "Migration v29 applied: FTS5 tokenizer porter→unicode61 (Persian/Arabic/CJK now indexed)"
1516
+ )
1517
+
1518
+
1519
+ def _migrate_v30_observation_access_signal(conn: sqlite3.Connection) -> None:
1520
+ """Migration v30 — observations gain access_count + last_accessed_at so the
1521
+ 5-signal ranker's access + recency-on-use terms apply to raw observations,
1522
+ not just learned_patterns. Closes the ranking asymmetry where an
1523
+ often-retrieved observation could never accrue an access boost."""
1524
+ if not _table_exists(conn, "observations"):
1525
+ logger.info("Migration v30 skipped: observations not present yet")
1526
+ return
1527
+ if not _column_exists(conn, "observations", "access_count"):
1528
+ conn.execute("ALTER TABLE observations ADD COLUMN access_count INTEGER DEFAULT 0")
1529
+ if not _column_exists(conn, "observations", "last_accessed_at"):
1530
+ conn.execute("ALTER TABLE observations ADD COLUMN last_accessed_at DATETIME")
1531
+ conn.commit()
1532
+ logger.info("Migration v30 applied: observations gained access_count + last_accessed_at")
1533
+
1534
+
1535
+ def _table_exists(conn: sqlite3.Connection, name: str) -> bool:
1536
+ row = conn.execute(
1537
+ "SELECT name FROM sqlite_master WHERE type='table' AND name=?", (name,)
1538
+ ).fetchone()
1539
+ return row is not None
1540
+
1541
+
1542
+ def has_file_index_state_table(conn: sqlite3.Connection) -> bool:
1543
+ """Check whether the file_index_state table exists (migration v17)."""
1544
+ row = conn.execute(
1545
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='file_index_state'"
1546
+ ).fetchone()
1547
+ return row is not None
1548
+
1549
+
1550
+ def has_formula_dispatches_table(conn: sqlite3.Connection) -> bool:
1551
+ """Check whether formula_dispatches exists (migration v14)."""
1552
+ row = conn.execute(
1553
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='formula_dispatches'"
1554
+ ).fetchone()
1555
+ return row is not None
1556
+
1557
+
1558
+ def has_backtrack_events_table(conn: sqlite3.Connection) -> bool:
1559
+ """Check whether backtrack_events exists (migration v14)."""
1560
+ row = conn.execute(
1561
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='backtrack_events'"
1562
+ ).fetchone()
1563
+ return row is not None
1564
+
1565
+
1566
+ def has_persona_selections_table(conn: sqlite3.Connection) -> bool:
1567
+ """Check whether persona_selections exists (migration v14)."""
1568
+ row = conn.execute(
1569
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='persona_selections'"
1570
+ ).fetchone()
1571
+ return row is not None
1572
+
1573
+
1574
+ def has_task_status_history_table(conn: sqlite3.Connection) -> bool:
1575
+ """Check whether task_status_history exists (migration v13)."""
1576
+ row = conn.execute(
1577
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='task_status_history'"
1578
+ ).fetchone()
1579
+ return row is not None
1580
+
1581
+
1582
+ def has_tasks_v13_columns(conn: sqlite3.Connection) -> bool:
1583
+ """Quick check whether the v13 columns are on the tasks table."""
1584
+ return _column_exists(conn, "tasks", "swimlane") and _column_exists(conn, "tasks", "kind")
1585
+
1586
+
1587
+ def record_audit(
1588
+ conn: sqlite3.Connection,
1589
+ *,
1590
+ actor: str,
1591
+ action: str,
1592
+ source_table: str,
1593
+ source_id: int | None = None,
1594
+ old_value: str | None = None,
1595
+ new_value: str | None = None,
1596
+ reason: str | None = None,
1597
+ ) -> int | None:
1598
+ """Append a row to memory_audit. Fire-and-forget — never raises."""
1599
+ if not has_memory_audit_table(conn):
1600
+ return None
1601
+ try:
1602
+ cursor = conn.execute(
1603
+ "INSERT INTO memory_audit "
1604
+ "(actor, action, source_table, source_id, old_value, new_value, reason) "
1605
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
1606
+ (actor, action, source_table, source_id, old_value, new_value, reason),
1607
+ )
1608
+ conn.commit()
1609
+ return cursor.lastrowid
1610
+ except sqlite3.OperationalError as exc:
1611
+ logger.debug("record_audit skipped: %s", exc)
1612
+ return None
1613
+
1614
+
1615
+ def _migrate_v31_task_edit_history(conn: sqlite3.Connection) -> None:
1616
+ """Migration v31 — append-only, actor-attributed field-edit history for tasks."""
1617
+ conn.executescript(
1618
+ """
1619
+ CREATE TABLE IF NOT EXISTS task_edit_history (
1620
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1621
+ task_id TEXT NOT NULL,
1622
+ field TEXT NOT NULL,
1623
+ old_value TEXT,
1624
+ new_value TEXT,
1625
+ actor_type TEXT NOT NULL DEFAULT 'agent',
1626
+ actor_id TEXT,
1627
+ source TEXT,
1628
+ edited_at INTEGER NOT NULL
1629
+ );
1630
+ CREATE INDEX IF NOT EXISTS idx_teh_task
1631
+ ON task_edit_history(task_id, edited_at);
1632
+ CREATE INDEX IF NOT EXISTS idx_teh_actor
1633
+ ON task_edit_history(actor_id, edited_at) WHERE actor_id IS NOT NULL;
1634
+ """
1635
+ )
1636
+ conn.commit()
1637
+ logger.info("Migration v31 applied: task_edit_history (actor-attributed field edits)")
1638
+
1639
+
1640
+ def _migrate_v32_log_events(conn: sqlite3.Connection) -> None:
1641
+ """Migration v32 — durable WARN+ error store + permanent fingerprint rollup (observability eye)."""
1642
+ conn.executescript(
1643
+ """
1644
+ CREATE TABLE IF NOT EXISTS log_events (
1645
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1646
+ ts TEXT NOT NULL,
1647
+ lvl TEXT NOT NULL,
1648
+ scope TEXT NOT NULL,
1649
+ msg TEXT NOT NULL,
1650
+ kv TEXT,
1651
+ exc_type TEXT,
1652
+ stack TEXT,
1653
+ session_id TEXT,
1654
+ trace_id TEXT,
1655
+ fingerprint TEXT NOT NULL,
1656
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
1657
+ );
1658
+ CREATE INDEX IF NOT EXISTS idx_log_events_fp ON log_events(fingerprint);
1659
+ CREATE INDEX IF NOT EXISTS idx_log_events_ts ON log_events(ts);
1660
+ CREATE INDEX IF NOT EXISTS idx_log_events_lvl ON log_events(lvl);
1661
+
1662
+ CREATE TABLE IF NOT EXISTS log_fingerprints (
1663
+ fingerprint TEXT PRIMARY KEY,
1664
+ scope TEXT NOT NULL,
1665
+ exc_type TEXT,
1666
+ sample_msg TEXT NOT NULL,
1667
+ max_lvl TEXT NOT NULL,
1668
+ first_seen TEXT NOT NULL,
1669
+ last_seen TEXT NOT NULL,
1670
+ count INTEGER NOT NULL DEFAULT 0,
1671
+ distinct_sessions INTEGER NOT NULL DEFAULT 0,
1672
+ task_id TEXT,
1673
+ status TEXT NOT NULL DEFAULT 'open'
1674
+ );
1675
+ CREATE INDEX IF NOT EXISTS idx_log_fp_status ON log_fingerprints(status, last_seen);
1676
+ """
1677
+ )
1678
+ conn.commit()
1679
+ logger.info(
1680
+ "Migration v32 applied: log_events + log_fingerprints (observability eye durable store)"
1681
+ )
1682
+
1683
+
1684
+ def _migrate_v33_pattern_archived_at(conn: sqlite3.Connection) -> None:
1685
+ """Migration v33 — learned_patterns.archived_at so the decay prune measures a
1686
+ real time-since-archived grace window (was COALESCE of access/validate/create)."""
1687
+ if not _column_exists(conn, "learned_patterns", "archived_at"):
1688
+ conn.execute("ALTER TABLE learned_patterns ADD COLUMN archived_at DATETIME")
1689
+ logger.info("Migration v33 applied: learned_patterns gained archived_at")
1690
+
1691
+
1692
+ def _migrate_v34_dispatch_error(conn: sqlite3.Connection) -> None:
1693
+ """Migration v34 — formula_dispatches.error captures the dispatch failure reason."""
1694
+ if not _table_exists(conn, "formula_dispatches"):
1695
+ logger.info("Migration v34 skipped: formula_dispatches not present yet")
1696
+ return
1697
+ if not _column_exists_table(conn, "formula_dispatches", "error"):
1698
+ conn.execute("ALTER TABLE formula_dispatches ADD COLUMN error TEXT")
1699
+ conn.commit()
1700
+ logger.info("Migration v34 applied: formula_dispatches gained error")
1701
+
1702
+
1703
+ def _migrate_v35_scale_foundation(conn: sqlite3.Connection) -> None:
1704
+ """Migration v35 — scale foundation for 100K+ tasks (TASK-226).
1705
+
1706
+ Adds the keyset/board indexes pagination + bounded queries need at
1707
+ scale, an FTS5 table so task_search stops full-scanning title/goal, and
1708
+ a task_dependencies junction that replaces the O(n) `dependencies LIKE
1709
+ '%"TASK-NNN"%'` scan with indexed lookups in both directions.
1710
+ """
1711
+ if not has_tasks_table(conn):
1712
+ logger.info("Migration v35 skipped: tasks table not present yet")
1713
+ return
1714
+
1715
+ # 1. Keyset/board indexes. (task_status_history(task_id, transitioned_at)
1716
+ # already exists as idx_tsh_task from v13 — no duplicate needed.)
1717
+ conn.executescript(
1718
+ """
1719
+ CREATE INDEX IF NOT EXISTS idx_tasks_status_completed
1720
+ ON tasks(status, completed_at);
1721
+ CREATE INDEX IF NOT EXISTS idx_tasks_swimlane_status_priority
1722
+ ON tasks(swimlane, status, priority);
1723
+ """
1724
+ )
1725
+
1726
+ # 2. task_dependencies junction. PK(task_id, depends_on) indexes the
1727
+ # dependencies() direction; idx on depends_on indexes dependents().
1728
+ # Triggers derive it from the tasks.dependencies JSON column so EVERY
1729
+ # writer (board_os sync, orphan-delete) keeps it in
1730
+ # step with zero per-writer code. json_each is built into SQLite 3.38+
1731
+ # (the repo already floors at 3.27+ for FTS5 remove_diacritics 2).
1732
+ conn.executescript(
1733
+ """
1734
+ CREATE TABLE IF NOT EXISTS task_dependencies (
1735
+ task_id TEXT NOT NULL,
1736
+ depends_on TEXT NOT NULL,
1737
+ PRIMARY KEY (task_id, depends_on)
1738
+ );
1739
+ CREATE INDEX IF NOT EXISTS idx_task_deps_depends_on
1740
+ ON task_dependencies(depends_on);
1741
+
1742
+ CREATE TRIGGER IF NOT EXISTS tasks_deps_ai AFTER INSERT ON tasks BEGIN
1743
+ INSERT OR IGNORE INTO task_dependencies(task_id, depends_on)
1744
+ SELECT new.task_id, je.value
1745
+ FROM json_each(COALESCE(NULLIF(new.dependencies, ''), '[]')) je;
1746
+ END;
1747
+ CREATE TRIGGER IF NOT EXISTS tasks_deps_au AFTER UPDATE OF dependencies ON tasks BEGIN
1748
+ DELETE FROM task_dependencies WHERE task_id = new.task_id;
1749
+ INSERT OR IGNORE INTO task_dependencies(task_id, depends_on)
1750
+ SELECT new.task_id, je.value
1751
+ FROM json_each(COALESCE(NULLIF(new.dependencies, ''), '[]')) je;
1752
+ END;
1753
+ CREATE TRIGGER IF NOT EXISTS tasks_deps_ad AFTER DELETE ON tasks BEGIN
1754
+ DELETE FROM task_dependencies WHERE task_id = old.task_id;
1755
+ END;
1756
+ """
1757
+ )
1758
+ # Backfill from any populated dependencies JSON (idempotent; the live
1759
+ # column is normally empty — sync.py populates the junction going forward).
1760
+ for task_id, deps_json in conn.execute(
1761
+ "SELECT task_id, dependencies FROM tasks "
1762
+ "WHERE dependencies IS NOT NULL AND dependencies NOT IN ('', '[]')"
1763
+ ).fetchall():
1764
+ try:
1765
+ dep_ids = json.loads(deps_json)
1766
+ except (json.JSONDecodeError, TypeError):
1767
+ continue
1768
+ for dep in dep_ids:
1769
+ if isinstance(dep, str) and dep:
1770
+ conn.execute(
1771
+ "INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)",
1772
+ (task_id, dep),
1773
+ )
1774
+
1775
+ # 3. FTS5 over tasks(title, goal_text). A regular (own-content) table,
1776
+ # NOT external-content: external-content FTS5 raises "database disk
1777
+ # image is malformed" when its 'delete' trigger runs against rows that
1778
+ # were backfilled rather than trigger-inserted (the delete tokens can't
1779
+ # be reconciled). Own-content deletes by rowid and is corruption-safe.
1780
+ # DROP-first heals any partially-applied earlier build of this table.
1781
+ # unicode61 matches v29 so Persian/Arabic/CJK task text is indexed.
1782
+ if has_fts5(conn):
1783
+ conn.executescript(
1784
+ """
1785
+ DROP TRIGGER IF EXISTS tasks_fts_ai;
1786
+ DROP TRIGGER IF EXISTS tasks_fts_ad;
1787
+ DROP TRIGGER IF EXISTS tasks_fts_au;
1788
+ DROP TABLE IF EXISTS tasks_fts;
1789
+ CREATE VIRTUAL TABLE tasks_fts USING fts5(
1790
+ title, goal_text,
1791
+ tokenize='unicode61 remove_diacritics 2'
1792
+ );
1793
+ CREATE TRIGGER tasks_fts_ai AFTER INSERT ON tasks BEGIN
1794
+ INSERT INTO tasks_fts(rowid, title, goal_text)
1795
+ VALUES (new.rowid, new.title, COALESCE(new.goal_text, ''));
1796
+ END;
1797
+ CREATE TRIGGER tasks_fts_ad AFTER DELETE ON tasks BEGIN
1798
+ DELETE FROM tasks_fts WHERE rowid = old.rowid;
1799
+ END;
1800
+ CREATE TRIGGER tasks_fts_au AFTER UPDATE ON tasks BEGIN
1801
+ DELETE FROM tasks_fts WHERE rowid = new.rowid;
1802
+ INSERT INTO tasks_fts(rowid, title, goal_text)
1803
+ VALUES (new.rowid, new.title, COALESCE(new.goal_text, ''));
1804
+ END;
1805
+ """
1806
+ )
1807
+ conn.execute(
1808
+ "INSERT INTO tasks_fts(rowid, title, goal_text) "
1809
+ "SELECT rowid, title, COALESCE(goal_text, '') FROM tasks"
1810
+ )
1811
+ else:
1812
+ logger.warning("Migration v35: FTS5 unavailable — tasks_fts skipped (LIKE fallback active)")
1813
+
1814
+ conn.commit()
1815
+ logger.info(
1816
+ "Migration v35 applied: idx_tasks_status_completed + "
1817
+ "idx_tasks_swimlane_status_priority, task_dependencies junction, tasks_fts"
1818
+ )
1819
+
1820
+
1821
+ def _migrate_v36_scrub_username_from_observations(conn: sqlite3.Connection) -> None:
1822
+ """Migration v36 — backfill: strip the local OS username from existing
1823
+ observations.files_modified + title (pre-fix rows leaked /Users/<name>/…,
1824
+ a PII exposure per memory.md § Privacy). Idempotent: re-running finds no
1825
+ home/root prefix to replace. The capture-time fix (_scrub_username) keeps
1826
+ new rows clean; this scrubs the historical corpus with no backfill before it."""
1827
+ import os
1828
+
1829
+ if not _table_exists(conn, "observations"):
1830
+ logger.info("Migration v36 skipped: observations not present yet")
1831
+ return
1832
+
1833
+ root = None
1834
+ for row in conn.execute("PRAGMA database_list").fetchall():
1835
+ db_str = row[2] if len(row) > 2 else None
1836
+ if db_str and db_str not in ("", ":memory:"):
1837
+ dbp = Path(db_str).resolve()
1838
+ if dbp.parent.name == ".coding-os":
1839
+ root = str(dbp.parent.parent)
1840
+ break
1841
+ home = os.path.expanduser("~")
1842
+
1843
+ def _scrub(text: str) -> str:
1844
+ out = text or ""
1845
+ if root:
1846
+ out = out.replace(root + "/", "")
1847
+ if home and home != "~":
1848
+ out = out.replace(home + "/", "~/")
1849
+ return out
1850
+
1851
+ scrubbed = 0
1852
+ for rid, title, fm in conn.execute(
1853
+ "SELECT id, title, files_modified FROM observations"
1854
+ ).fetchall():
1855
+ new_title, new_fm = _scrub(title or ""), _scrub(fm or "")
1856
+ if new_title != (title or "") or new_fm != (fm or ""):
1857
+ conn.execute(
1858
+ "UPDATE observations SET title = ?, files_modified = ? WHERE id = ?",
1859
+ (new_title, new_fm, rid),
1860
+ )
1861
+ scrubbed += 1
1862
+ conn.commit()
1863
+ logger.info("Migration v36 applied: scrubbed username from %d observation row(s)", scrubbed)
1864
+
1865
+
1866
+ def _migrate_v37_scrub_narrative_and_dash(conn: sqlite3.Connection) -> None:
1867
+ """Migration v37 — completes the v36 PII backfill: (1) scrub observations.narrative
1868
+ (v36 only touched title+files_modified), and (2) strip the dash-encoded username
1869
+ that survives inside agent project-dir slugs (~/.claude/projects/-Users-<name>-…)
1870
+ across title+narrative+files_modified. Idempotent. SSOT scrub: sanitizer.scrub_username."""
1871
+ import os
1872
+
1873
+ if not _table_exists(conn, "observations"):
1874
+ logger.info("Migration v37 skipped: observations not present yet")
1875
+ return
1876
+
1877
+ from sanitizer import scrub_username
1878
+
1879
+ root = None
1880
+ for row in conn.execute("PRAGMA database_list").fetchall():
1881
+ db_str = row[2] if len(row) > 2 else None
1882
+ if db_str and db_str not in ("", ":memory:"):
1883
+ dbp = Path(db_str).resolve()
1884
+ if dbp.parent.name == ".coding-os":
1885
+ root = str(dbp.parent.parent)
1886
+ break
1887
+ home = os.path.expanduser("~")
1888
+
1889
+ def _scrub(text: str) -> str:
1890
+ out = text or ""
1891
+ if root:
1892
+ out = out.replace(root + "/", "")
1893
+ return scrub_username(out, home=home)
1894
+
1895
+ scrubbed = 0
1896
+ for rid, title, narrative, fm in conn.execute(
1897
+ "SELECT id, title, narrative, files_modified FROM observations"
1898
+ ).fetchall():
1899
+ nt, nn, nf = _scrub(title or ""), _scrub(narrative or ""), _scrub(fm or "")
1900
+ if nt != (title or "") or nn != (narrative or "") or nf != (fm or ""):
1901
+ conn.execute(
1902
+ "UPDATE observations SET title = ?, narrative = ?, files_modified = ? WHERE id = ?",
1903
+ (nt, nn, nf, rid),
1904
+ )
1905
+ scrubbed += 1
1906
+ conn.commit()
1907
+ logger.info("Migration v37 applied: scrubbed narrative/dash-username from %d row(s)", scrubbed)
1908
+
1909
+
1910
+ def _migrate_v38_backfill_rework_outcome(conn: sqlite3.Connection) -> None:
1911
+ """Migration v38 — backfill honest outcomes for the historical corpus.
1912
+ Pre-fix every task_outcomes row was hardcoded 'success' (board_commands), so
1913
+ the variance gate suppressed every rework/stat extractor and learn_extract
1914
+ was starved (192 tasks → 4 patterns). Flip 'success' → 'rework' for any task
1915
+ whose task_status_history shows a backward move (reopened after testing/
1916
+ complete/review). Idempotent: only touches rows still 'success' with a reopen."""
1917
+ if not (_table_exists(conn, "task_outcomes") and _table_exists(conn, "task_status_history")):
1918
+ logger.info("Migration v38 skipped: outcome/history tables not present yet")
1919
+ return
1920
+ cur = conn.execute(
1921
+ "UPDATE task_outcomes SET outcome = 'rework' "
1922
+ "WHERE outcome = 'success' AND task_id IN ("
1923
+ " SELECT DISTINCT task_id FROM task_status_history "
1924
+ " WHERE old_status IN ('testing','complete','done','review') "
1925
+ " AND new_status IN ('in_progress','open','ready','icebox'))"
1926
+ )
1927
+ conn.commit()
1928
+ logger.info("Migration v38 applied: backfilled %d task(s) to rework", cur.rowcount)
1929
+
1930
+
1931
+ def _migrate_v39_observations_task_id(conn: sqlite3.Connection) -> None:
1932
+ """Migration v39 — add observations.task_id so an observation can be linked
1933
+ to the task active when it was written. Until now observations carried only
1934
+ session_id, and a session spans many tasks, so NO per-task rework signal
1935
+ (file churn, in-task errors) could be derived — the root reason the learning
1936
+ loop is blind to mid-task rework (audit 2026-06-08). Forward-only: historical
1937
+ rows stay NULL (the linkage was never captured and cannot be recovered);
1938
+ capture.py stamps it going forward. Idempotent — skips if the column exists."""
1939
+ if not _table_exists(conn, "observations"):
1940
+ logger.info("Migration v39 skipped: observations not present yet")
1941
+ return
1942
+ cols = {row[1] for row in conn.execute("PRAGMA table_info(observations)").fetchall()}
1943
+ if "task_id" in cols:
1944
+ logger.info("Migration v39 skipped: observations.task_id already present")
1945
+ return
1946
+ conn.execute("ALTER TABLE observations ADD COLUMN task_id TEXT")
1947
+ conn.commit()
1948
+ logger.info("Migration v39 applied: observations.task_id added")
1949
+
1950
+
1951
+ def _migrate_v40_embedding_outbox(conn: sqlite3.Connection) -> None:
1952
+ """Migration v40 — durable embedding_outbox so the capture hot path can defer
1953
+ embedding off the interactive path (Wave 4). The PostToolUse capture skips
1954
+ the model load (COS_CAPTURE_SKIP_EMBED) to keep Edits fast; without an outbox
1955
+ those rows had NO embedding until a manual reindex that might never run. This
1956
+ table records the backlog; a Stop-hook drains it. UNIQUE(source_table,
1957
+ source_id) makes enqueue idempotent. Idempotent — skips if the table exists."""
1958
+ conn.executescript("""\
1959
+ CREATE TABLE IF NOT EXISTS embedding_outbox (
1960
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1961
+ source_table TEXT NOT NULL,
1962
+ source_id INTEGER NOT NULL,
1963
+ enqueued_at INTEGER NOT NULL,
1964
+ attempts INTEGER NOT NULL DEFAULT 0,
1965
+ last_error TEXT,
1966
+ UNIQUE(source_table, source_id)
1967
+ );
1968
+ CREATE INDEX IF NOT EXISTS idx_embedding_outbox_pending
1969
+ ON embedding_outbox(attempts, enqueued_at);
1970
+ """)
1971
+ conn.commit()
1972
+ logger.info("Migration v40 applied: embedding_outbox table")
1973
+
1974
+
1975
+ def _migrate_v41_tasks_lean_columns(conn: sqlite3.Connection) -> None:
1976
+ """Migration v41 — tasks table catches up with the lean card contract
1977
+ (TASK-398): adds blocked_by_json / references_json / external_ref
1978
+ (parsed since the Scrumban migration but never persisted) and drops the
1979
+ eight v6-era columns with no reader or writer after the legacy
1980
+ task_sync retirement. domain / goal_text / dependencies stay — the
1981
+ cos_task_* tools read them. Idempotent — guarded per column; DROP
1982
+ COLUMN is best-effort so a pre-3.35 SQLite degrades to harmless
1983
+ residue instead of a blocked init."""
1984
+ cols = {row[1] for row in conn.execute("PRAGMA table_info(tasks)")}
1985
+ for name in ("blocked_by_json", "references_json", "external_ref"):
1986
+ if name not in cols:
1987
+ conn.execute(f"ALTER TABLE tasks ADD COLUMN {name} TEXT")
1988
+ dead = (
1989
+ "scope_in",
1990
+ "scope_out",
1991
+ "requirements",
1992
+ "source_of_truth",
1993
+ "open_questions",
1994
+ "rabbit_holes",
1995
+ "verification",
1996
+ "read_first",
1997
+ )
1998
+ for name in dead:
1999
+ if name not in cols:
2000
+ continue
2001
+ try:
2002
+ conn.execute(f"ALTER TABLE tasks DROP COLUMN {name}")
2003
+ except sqlite3.OperationalError as exc:
2004
+ logger.warning(
2005
+ "Migration v41: could not drop tasks.%s (%s) — leaving in place", name, exc
2006
+ )
2007
+ conn.commit()
2008
+ logger.info("Migration v41 applied: lean task columns added, v6 dead columns dropped")
2009
+
2010
+
2011
+ def _migrate_v42_drop_doc_audit_trail(conn: sqlite3.Connection) -> None:
2012
+ """Migration v42 — drop doc_audit_trail (TASK-401): the audit concept is
2013
+ retired project-wide; git is the forensic record for doc edits. Drops
2014
+ the append-only guard triggers first (they would block any future
2015
+ cleanup), then the table. Idempotent via IF EXISTS."""
2016
+ conn.executescript("""\
2017
+ DROP TRIGGER IF EXISTS doc_audit_trail_no_update;
2018
+ DROP TRIGGER IF EXISTS doc_audit_trail_no_delete;
2019
+ DROP TABLE IF EXISTS doc_audit_trail;
2020
+ """)
2021
+ conn.commit()
2022
+ logger.info("Migration v42 applied: doc_audit_trail dropped (audit retirement)")
2023
+
2024
+
2025
+ def _migrate_v43_drop_experiment_log(conn: sqlite3.Connection) -> None:
2026
+ """Migration v43 — drop experiment_log (audit 2026-06-19, group B5). The table
2027
+ was created in the v1 schema for the Experiment Protocol but never got a write
2028
+ path (zero writers in 260 tasks); only a dashboard reader + stats count showed
2029
+ a perpetual 0. Speculative scaffolding for a feature with no demand → removed
2030
+ per anti-overengineering. If the Experiment Protocol later needs persistence, a
2031
+ task adds the table + its write tool together. Idempotent (IF EXISTS)."""
2032
+ conn.executescript("""\
2033
+ DROP TABLE IF EXISTS experiment_log;
2034
+ """)
2035
+ conn.commit()
2036
+ logger.info("Migration v43 applied: experiment_log dropped (no writer, B5)")
2037
+
2038
+
2039
+ def _migrate_v44_dispatch_transcript(conn: sqlite3.Connection) -> None:
2040
+ """Migration v44 — formula_dispatches.raw_transcript so a dispatched
2041
+ sub-agent's chat/session is auditable, not just its summarized output."""
2042
+ if not _table_exists(conn, "formula_dispatches"):
2043
+ logger.info("Migration v44 skipped: formula_dispatches not present yet")
2044
+ return
2045
+ if not _column_exists_table(conn, "formula_dispatches", "raw_transcript"):
2046
+ conn.execute("ALTER TABLE formula_dispatches ADD COLUMN raw_transcript TEXT")
2047
+ conn.commit()
2048
+ logger.info("Migration v44 applied: formula_dispatches gained raw_transcript")
2049
+
2050
+
2051
+ def _migrate_v45_task_child_cascade(conn: sqlite3.Connection) -> None:
2052
+ """Migration v45 — a deleted tasks row cascades to its child tables."""
2053
+ conn.executescript(
2054
+ """
2055
+ CREATE TRIGGER IF NOT EXISTS task_status_history_ad AFTER DELETE ON tasks BEGIN
2056
+ DELETE FROM task_status_history WHERE task_id = old.task_id;
2057
+ END;
2058
+ CREATE TRIGGER IF NOT EXISTS task_outcomes_ad AFTER DELETE ON tasks BEGIN
2059
+ DELETE FROM task_outcomes WHERE task_id = old.task_id;
2060
+ END;
2061
+ CREATE TRIGGER IF NOT EXISTS task_edit_history_ad AFTER DELETE ON tasks BEGIN
2062
+ DELETE FROM task_edit_history WHERE task_id = old.task_id;
2063
+ END;
2064
+ """
2065
+ )
2066
+ conn.commit()
2067
+ logger.info("Migration v45 applied: task child-table delete-cascade triggers")
2068
+
2069
+
2070
+ def _migrate_v46_log_event_class(conn: sqlite3.Connection) -> None:
2071
+ """Migration v46 — log_events.event_class splits policy enforcement (hook
2072
+ BLOCKs) from faults so error_sweep files only genuine bugs."""
2073
+ if not _table_exists(conn, "log_events"):
2074
+ logger.info("Migration v46 skipped: log_events not present yet")
2075
+ return
2076
+ if not _column_exists_table(conn, "log_events", "event_class"):
2077
+ conn.execute("ALTER TABLE log_events ADD COLUMN event_class TEXT NOT NULL DEFAULT 'fault'")
2078
+ conn.execute(
2079
+ "UPDATE log_events SET event_class = 'policy' "
2080
+ "WHERE scope LIKE 'hook.%' AND kv LIKE '%\"action\": \"block\"%'"
2081
+ )
2082
+ conn.commit()
2083
+ logger.info("Migration v46 applied: log_events gained event_class")
2084
+
2085
+
2086
+ def _migrate_v47_distill_columns(conn: sqlite3.Connection) -> None:
2087
+ """Migration v47 — learned_patterns gains distill_fingerprint (idempotent
2088
+ LLM distillation per friction cluster) and evidence_json (sanitized sample
2089
+ failures backing the lesson)."""
2090
+ if not _column_exists_table(conn, "learned_patterns", "distill_fingerprint"):
2091
+ conn.execute("ALTER TABLE learned_patterns ADD COLUMN distill_fingerprint TEXT")
2092
+ if not _column_exists_table(conn, "learned_patterns", "evidence_json"):
2093
+ conn.execute("ALTER TABLE learned_patterns ADD COLUMN evidence_json TEXT")
2094
+ conn.commit()
2095
+ logger.info("Migration v47 applied: learned_patterns gained distill columns")
2096
+
2097
+
2098
+ def _migrate_v48_reclassify_mechanical_observations(conn: sqlite3.Connection) -> None:
2099
+ cur = conn.execute(
2100
+ "UPDATE observations SET memory_type = 'changelog' "
2101
+ "WHERE observation_type IN ('write', 'edit', 'multiedit') "
2102
+ "AND COALESCE(memory_type, '') != 'changelog'"
2103
+ )
2104
+ conn.commit()
2105
+ logger.info(
2106
+ "Migration v48 applied: reclassified %d mechanical observation(s) to changelog",
2107
+ cur.rowcount,
2108
+ )
2109
+
2110
+
2111
+ def _migrate_v49_add_times_seen(conn: sqlite3.Connection) -> None:
2112
+ conn.execute("ALTER TABLE learned_patterns ADD COLUMN times_seen INTEGER DEFAULT 0")
2113
+ cur = conn.execute("UPDATE learned_patterns SET times_seen = COALESCE(times_validated, 0)")
2114
+ conn.commit()
2115
+ logger.info(
2116
+ "Migration v49 applied: learned_patterns gained times_seen; backfilled %d row(s) from times_validated",
2117
+ cur.rowcount,
2118
+ )
2119
+
2120
+
2121
+ def _migrate_v50_reset_times_validated_from_ledger(conn: sqlite3.Connection) -> None:
2122
+ # v49 moved occurrence counts to times_seen but left the historical
2123
+ # times_validated values inflated by the pre-split re-mine/dedup bumps (up to
2124
+ # 534, with zero real validations behind them). Rebuild the counter from the
2125
+ # pattern_validations ledger — the append-only record of genuine validations
2126
+ # (was_helpful, non-throttled) — so pattern_tier's "Trusted" reflects real
2127
+ # confirmation. An empty ledger resets every row to 0; trust is then re-earned
2128
+ # by the now-firing validation loop.
2129
+ try:
2130
+ cur = conn.execute(
2131
+ "UPDATE learned_patterns SET times_validated = ("
2132
+ " SELECT COUNT(*) FROM pattern_validations pv "
2133
+ " WHERE pv.pattern_id = learned_patterns.id "
2134
+ " AND pv.was_helpful = 1 AND COALESCE(pv.was_throttled, 0) = 0"
2135
+ ")"
2136
+ )
2137
+ except sqlite3.OperationalError:
2138
+ # No ledger table → no real validations exist; honest baseline is 0.
2139
+ cur = conn.execute("UPDATE learned_patterns SET times_validated = 0")
2140
+ conn.commit()
2141
+ logger.info(
2142
+ "Migration v50 applied: rebuilt times_validated from pattern_validations ledger (%d row(s))",
2143
+ cur.rowcount,
2144
+ )
2145
+
2146
+
2147
+ def _migrate_v51_observations_dedup_unique(conn: sqlite3.Connection) -> None:
2148
+ # The write path deduped observations by (content_hash, session_id) with a
2149
+ # race-prone SELECT-then-INSERT: two concurrent captures both miss the SELECT
2150
+ # and both insert. Collapse existing duplicates (keep the earliest id) and add
2151
+ # a partial UNIQUE index so INSERT OR IGNORE enforces one row per group
2152
+ # atomically. NULL content_hash/session_id rows are exempt (not dedup targets;
2153
+ # SQLite already treats their NULLs as distinct). The observations_fts index
2154
+ # stays consistent via the AFTER DELETE trigger; no FK references observations.id.
2155
+ cur = conn.execute(
2156
+ "DELETE FROM observations "
2157
+ "WHERE content_hash IS NOT NULL AND session_id IS NOT NULL "
2158
+ " AND id NOT IN ("
2159
+ " SELECT MIN(id) FROM observations "
2160
+ " WHERE content_hash IS NOT NULL AND session_id IS NOT NULL "
2161
+ " GROUP BY content_hash, session_id"
2162
+ " )"
2163
+ )
2164
+ removed = cur.rowcount
2165
+ conn.execute(
2166
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_observations_content_session "
2167
+ "ON observations(content_hash, session_id) "
2168
+ "WHERE content_hash IS NOT NULL AND session_id IS NOT NULL"
2169
+ )
2170
+ conn.commit()
2171
+ logger.info(
2172
+ "Migration v51 applied: collapsed %d duplicate observation(s); "
2173
+ "added partial UNIQUE(content_hash, session_id)",
2174
+ removed,
2175
+ )
2176
+
2177
+
2178
+ def _migrate_v52_derived_outcome_columns(conn: sqlite3.Connection) -> None:
2179
+ if not _column_exists_table(conn, "task_outcomes", "derived_outcome"):
2180
+ conn.execute("ALTER TABLE task_outcomes ADD COLUMN derived_outcome TEXT")
2181
+ if not _column_exists_table(conn, "task_outcomes", "derived_provenance"):
2182
+ conn.execute("ALTER TABLE task_outcomes ADD COLUMN derived_provenance TEXT")
2183
+ conn.commit()
2184
+ logger.info("Migration v52 applied: task_outcomes gained derived_outcome columns")
2185
+
2186
+
2187
+ MIGRATIONS: list[tuple[int, str, MigrationAction]] = [
2188
+ (
2189
+ 1,
2190
+ "TASK-141: initial schema — task_outcomes, agent_metrics, learned_patterns, experiment_log, observations, session_summaries",
2191
+ """\
2192
+ -- task_outcomes: one row per completed task
2193
+ CREATE TABLE IF NOT EXISTS task_outcomes (
2194
+ task_id TEXT PRIMARY KEY,
2195
+ type TEXT NOT NULL,
2196
+ domain TEXT NOT NULL,
2197
+ complexity TEXT NOT NULL,
2198
+ dimensions INTEGER DEFAULT 1,
2199
+ outcome TEXT NOT NULL,
2200
+ duration_min INTEGER,
2201
+ model TEXT,
2202
+ skills_used TEXT,
2203
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
2204
+ );
2205
+
2206
+ -- agent_metrics: per-agent-invocation telemetry
2207
+ CREATE TABLE IF NOT EXISTS agent_metrics (
2208
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2209
+ task_id TEXT,
2210
+ agent_type TEXT NOT NULL,
2211
+ model TEXT,
2212
+ duration_ms INTEGER,
2213
+ domain TEXT,
2214
+ complexity TEXT,
2215
+ outcome TEXT,
2216
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
2217
+ );
2218
+
2219
+ -- learned_patterns: extracted reusable patterns with confidence
2220
+ CREATE TABLE IF NOT EXISTS learned_patterns (
2221
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2222
+ pattern TEXT NOT NULL,
2223
+ memory_type TEXT DEFAULT 'pattern',
2224
+ domain TEXT,
2225
+ source TEXT,
2226
+ confidence REAL DEFAULT 0.5,
2227
+ decay_rate REAL DEFAULT 0.1,
2228
+ impact_score REAL DEFAULT 0.5,
2229
+ concepts TEXT,
2230
+ times_validated INTEGER DEFAULT 0,
2231
+ times_violated INTEGER DEFAULT 0,
2232
+ access_count INTEGER DEFAULT 0,
2233
+ last_accessed_at DATETIME,
2234
+ promoted_to TEXT,
2235
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
2236
+ last_validated DATETIME,
2237
+ archived_at DATETIME
2238
+ );
2239
+
2240
+ -- experiment_log: hypothesis tracking per task
2241
+ CREATE TABLE IF NOT EXISTS experiment_log (
2242
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2243
+ task_id TEXT,
2244
+ hypothesis TEXT NOT NULL,
2245
+ test_description TEXT,
2246
+ outcome TEXT,
2247
+ learning TEXT,
2248
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
2249
+ );
2250
+
2251
+ -- observations: raw captured observations from tool use
2252
+ CREATE TABLE IF NOT EXISTS observations (
2253
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2254
+ session_id TEXT,
2255
+ tool_name TEXT,
2256
+ observation_type TEXT,
2257
+ memory_type TEXT DEFAULT 'discovery',
2258
+ impact_score REAL DEFAULT 0.5,
2259
+ title TEXT,
2260
+ narrative TEXT,
2261
+ facts TEXT,
2262
+ concepts TEXT,
2263
+ files_read TEXT,
2264
+ files_modified TEXT,
2265
+ content_hash TEXT,
2266
+ cost_tokens INTEGER DEFAULT 0,
2267
+ expires_at DATETIME,
2268
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
2269
+ );
2270
+
2271
+ -- session_summaries: end-of-session digests.
2272
+ --
2273
+ -- Writer matrix (2026-04):
2274
+ -- session_id, task_id, previous_session_id, files_touched,
2275
+ -- observations_count, breakthrough_ids, duration_minutes
2276
+ -- → filled by session_summary.build_session_summary on Stop hook.
2277
+ -- request, learned
2278
+ -- → filled by session_enrich.py from tool/outcome signal.
2279
+ -- investigated, completed, next_steps
2280
+ -- → RESERVED for narrative fields the agent populates via
2281
+ -- cos_learn_narrative on breakthrough + a future explicit
2282
+ -- retro tool. Nullable by design; NULL is not a bug.
2283
+ CREATE TABLE IF NOT EXISTS session_summaries (
2284
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2285
+ session_id TEXT,
2286
+ task_id TEXT,
2287
+ request TEXT,
2288
+ investigated TEXT,
2289
+ learned TEXT,
2290
+ completed TEXT,
2291
+ next_steps TEXT,
2292
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
2293
+ );
2294
+ """,
2295
+ ),
2296
+ # FTS5 full-text search layer (callable migration — needs runtime FTS5 check)
2297
+ (
2298
+ 2,
2299
+ "TASK-152: FTS5 observations_fts virtual table + INSERT/UPDATE/DELETE triggers",
2300
+ _migrate_v2_fts5,
2301
+ ),
2302
+ # routing_weights table for adaptive model/skill routing
2303
+ (
2304
+ 3,
2305
+ "TASK-148: routing_weights table for adaptive routing",
2306
+ """\
2307
+ CREATE TABLE IF NOT EXISTS routing_weights (
2308
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2309
+ domain TEXT NOT NULL,
2310
+ complexity TEXT NOT NULL,
2311
+ model TEXT,
2312
+ skill TEXT,
2313
+ success_rate REAL DEFAULT 0.0,
2314
+ sample_count INTEGER DEFAULT 0,
2315
+ last_updated DATETIME DEFAULT CURRENT_TIMESTAMP,
2316
+ UNIQUE(domain, complexity, model, skill)
2317
+ );
2318
+ """,
2319
+ ),
2320
+ # Brain features: outcome_history, concept_graph, session_summaries enrichment
2321
+ (
2322
+ 4,
2323
+ "Brain features: outcome_history, concept_graph, session_summaries enrichment",
2324
+ _migrate_v4_brain_features,
2325
+ ),
2326
+ # embeddings + document_chunks for RAG vector search
2327
+ (5, "RAG: embeddings + document_chunks tables", _migrate_v5_rag),
2328
+ # tasks table for hybrid task store
2329
+ (6, "Task store: tasks table indexing docs/tasks/*.md", _migrate_v6_tasks),
2330
+ # brain hardening — trust_tier, provenance, memory_audit
2331
+ (
2332
+ 7,
2333
+ "Brain hardening: trust_tier + provenance + memory_audit",
2334
+ _migrate_v7_brain_hardening,
2335
+ ),
2336
+ # self-validation throttle
2337
+ (
2338
+ 8,
2339
+ "Validation throttle: pattern_validations table",
2340
+ _migrate_v8_validation_throttle,
2341
+ ),
2342
+ # FTS5 over document_chunks for lexical doc fallback
2343
+ (9, "Docs FTS: document_chunks_fts + triggers", _migrate_v9_docs_fts),
2344
+ # retrievals table — audit + feedback loop
2345
+ (10, "Retrieval-outcome loop: retrievals table", _migrate_v10_retrievals),
2346
+ # retrieval quality tracker + contextual-chunk scaffolding
2347
+ (
2348
+ 11,
2349
+ "Retrieval quality: retrieval_quality + contextual chunk columns",
2350
+ _migrate_v11_retrieval_quality,
2351
+ ),
2352
+ # graph_os knowledge-graph tables + embedding_dim column
2353
+ (
2354
+ 12,
2355
+ "graph_os: graph_nodes + graph_edges_v12 + graph_evidence_v12 + graph_nodes_fts + embeddings.embedding_dim",
2356
+ _migrate_v12_graph_os,
2357
+ ),
2358
+ # board_os Scrumban — extend tasks + task_status_history
2359
+ (
2360
+ 13,
2361
+ "board_os: tasks +swimlane/kind/epic/priority/appetite/started_at/completed_at/agent_session/labels_json/work_log_last_5; task_status_history",
2362
+ _migrate_v13_board_os,
2363
+ ),
2364
+ # formula-agent supervisor — 4 cognition tables
2365
+ (
2366
+ 14,
2367
+ "Formula-agents: backtrack_events + persona_selections + ambiguity_violations + formula_dispatches",
2368
+ _migrate_v14_cognition,
2369
+ ),
2370
+ # graph_os S1 / B17: CHECK(confidence BETWEEN 0 AND 1) triggers on graph_edges_v12
2371
+ (
2372
+ 15,
2373
+ "graph_os S1 B17: graph_edges_v12 confidence CHECK triggers (INSERT + UPDATE)",
2374
+ _migrate_v15_graph_edges_confidence_check,
2375
+ ),
2376
+ # graph_os S3: data migration — normalize graph_nodes.kind legacy values
2377
+ (
2378
+ 16,
2379
+ "graph_os S3: normalize graph_nodes.kind via NodeKind/normalize_kind",
2380
+ _migrate_v16_normalize_graph_node_kinds,
2381
+ ),
2382
+ # graph_os V1: file-level incremental indexing — file_index_state cache
2383
+ (
2384
+ 17,
2385
+ "graph_os V1: file_index_state cache table for incremental reindex",
2386
+ _migrate_v17_file_index_state,
2387
+ ),
2388
+ # retrieval router telemetry table
2389
+ (
2390
+ 18,
2391
+ "Retrieval router telemetry: retrieval_router_log table",
2392
+ _migrate_v18_retrieval_router_log,
2393
+ ),
2394
+ # drop 'ready' column — fold into icebox + 'ready' label
2395
+ (
2396
+ 19,
2397
+ "board_os: drop 'ready' status, migrate existing rows to icebox + 'ready' label",
2398
+ _migrate_v19_drop_ready_status,
2399
+ ),
2400
+ # override audit — task_status_history.override_reason/actor
2401
+ (20, "Override audit columns on task_status_history", _migrate_v20_override_audit),
2402
+ # doc_audit_trail — append-only doc edit + decision history
2403
+ (
2404
+ 21,
2405
+ "doc_audit_trail (append-only) for doc edits + decision history",
2406
+ _migrate_v21_doc_audit_trail,
2407
+ ),
2408
+ # document_chunks frontmatter metadata for Stage-1 RAG pre-filter
2409
+ (
2410
+ 22,
2411
+ "document_chunks frontmatter metadata (domain/layer/ssot/updated_iso/is_active)",
2412
+ _migrate_v22_doc_chunks_metadata,
2413
+ ),
2414
+ (
2415
+ 23,
2416
+ "formula_dispatches cost / budget / usage / tool_calls columns",
2417
+ _migrate_v23_dispatch_cost,
2418
+ ),
2419
+ # v24: project_trajectory — long-term project intent across sessions
2420
+ (
2421
+ 24,
2422
+ "v24: project_trajectory table for cross-session project intent",
2423
+ _migrate_v24_project_trajectory,
2424
+ ),
2425
+ # v25: structured failure anatomy on backtrack_events
2426
+ (
2427
+ 25,
2428
+ "v25: backtrack_events failure anatomy (hypothesis/failure_signal/root_cause/corrective_action)",
2429
+ _migrate_v25_backtrack_failure_anatomy,
2430
+ ),
2431
+ # v26: routing_weights staleness tracking for autonomous refresh
2432
+ (
2433
+ 26,
2434
+ "v26: routing_weights last_recalc_at + outcomes_at_recalc",
2435
+ _migrate_v26_routing_evolution,
2436
+ ),
2437
+ # formula_dispatches gains sub_session_id (SDK key),
2438
+ # model (claude-opus-4-7 / claude-sonnet-4-6), checkpoints_jsonb (T9.2).
2439
+ (
2440
+ 27,
2441
+ "v27: formula_dispatches sub_session_id / model / checkpoints_jsonb",
2442
+ _migrate_v27_dispatch_sdk_columns,
2443
+ ),
2444
+ (
2445
+ 28,
2446
+ "Polyglot v28: file_index_state.duration_ms for per-extractor latency telemetry",
2447
+ _migrate_v28_file_index_duration,
2448
+ ),
2449
+ (
2450
+ 29,
2451
+ "G14 FTS5 v29: porter→unicode61 tokenizer so Persian/Arabic body content is indexed",
2452
+ _migrate_v29_fts5_unicode_tokenizer,
2453
+ ),
2454
+ (
2455
+ 30,
2456
+ "Memory ranking symmetry v30: observations.access_count + last_accessed_at",
2457
+ _migrate_v30_observation_access_signal,
2458
+ ),
2459
+ (
2460
+ 31,
2461
+ "G4 v31: task_edit_history (append-only, actor-attributed field edits)",
2462
+ _migrate_v31_task_edit_history,
2463
+ ),
2464
+ (
2465
+ 32,
2466
+ "Observability eye v32: log_events + log_fingerprints durable error store",
2467
+ _migrate_v32_log_events,
2468
+ ),
2469
+ (
2470
+ 33,
2471
+ "Memory durability v33: learned_patterns.archived_at for prune grace window",
2472
+ _migrate_v33_pattern_archived_at,
2473
+ ),
2474
+ (
2475
+ 34,
2476
+ "formula_dispatches.error captures the dispatch failure reason",
2477
+ _migrate_v34_dispatch_error,
2478
+ ),
2479
+ (
2480
+ 35,
2481
+ "Scale foundation: keyset indexes + tasks_fts + task_dependencies junction",
2482
+ _migrate_v35_scale_foundation,
2483
+ ),
2484
+ (
2485
+ 36,
2486
+ "PII backfill v36: scrub local username from observations.files_modified + title",
2487
+ _migrate_v36_scrub_username_from_observations,
2488
+ ),
2489
+ (
2490
+ 37,
2491
+ "PII backfill v37: scrub observations.narrative + dash-encoded username in project slugs",
2492
+ _migrate_v37_scrub_narrative_and_dash,
2493
+ ),
2494
+ (
2495
+ 38,
2496
+ "Backfill honest rework outcomes from task_status_history reopen signal (unstarves learn_extract)",
2497
+ _migrate_v38_backfill_rework_outcome,
2498
+ ),
2499
+ (
2500
+ 39,
2501
+ "Add observations.task_id — per-task linkage so mid-task rework signals become derivable",
2502
+ _migrate_v39_observations_task_id,
2503
+ ),
2504
+ (
2505
+ 40,
2506
+ "Add embedding_outbox — durable backlog so hot-path-skipped embeddings drain off the interactive path",
2507
+ _migrate_v40_embedding_outbox,
2508
+ ),
2509
+ (
2510
+ 41,
2511
+ "Tasks lean columns — add blocked_by/references/external_ref, drop dead v6 columns (TASK-398)",
2512
+ _migrate_v41_tasks_lean_columns,
2513
+ ),
2514
+ (
2515
+ 42,
2516
+ "Drop doc_audit_trail — audit concept retired project-wide; git is the forensic record (TASK-401)",
2517
+ _migrate_v42_drop_doc_audit_trail,
2518
+ ),
2519
+ (
2520
+ 43,
2521
+ "Drop experiment_log — created in v1 but never wired (zero writers); speculative scaffolding removed (B5)",
2522
+ _migrate_v43_drop_experiment_log,
2523
+ ),
2524
+ (
2525
+ 44,
2526
+ "formula_dispatches.raw_transcript — persist dispatched sub-agent transcript for audit",
2527
+ _migrate_v44_dispatch_transcript,
2528
+ ),
2529
+ (
2530
+ 45,
2531
+ "Delete-cascade triggers: a pruned task removes its task_status_history / task_outcomes / task_edit_history rows (mirrors tasks_deps_ad)",
2532
+ _migrate_v45_task_child_cascade,
2533
+ ),
2534
+ (
2535
+ 46,
2536
+ "log_events.event_class (fault|policy|audit) — hook BLOCKs are policy, not faults, so error_sweep stops mis-filing them as bugs",
2537
+ _migrate_v46_log_event_class,
2538
+ ),
2539
+ (
2540
+ 47,
2541
+ "learned_patterns.distill_fingerprint + evidence_json — idempotent LLM lesson distillation with auditable evidence",
2542
+ _migrate_v47_distill_columns,
2543
+ ),
2544
+ (
2545
+ 48,
2546
+ "Reclassify mechanical auto-capture observations (write/edit/multiedit) to memory_type=changelog so recall exclusion is complete; forward-only, re-derivable from files_modified",
2547
+ _migrate_v48_reclassify_mechanical_observations,
2548
+ ),
2549
+ (
2550
+ 49,
2551
+ "learned_patterns.times_seen — split the conflated times_validated: occurrence re-mines / dedup folds move to times_seen; times_validated reserved for real validation events",
2552
+ _migrate_v49_add_times_seen,
2553
+ ),
2554
+ (
2555
+ 50,
2556
+ "Rebuild times_validated from the pattern_validations ledger — retire the pre-split inflated values so pattern_tier 'Trusted' reflects genuine validations; trust is re-earned by the firing loop",
2557
+ _migrate_v50_reset_times_validated_from_ledger,
2558
+ ),
2559
+ (
2560
+ 51,
2561
+ "Atomic write-path dedup: collapse duplicate observations and add a partial UNIQUE(content_hash, session_id) index so INSERT OR IGNORE enforces one-per-group without the race-prone SELECT-then-INSERT",
2562
+ _migrate_v51_observations_dedup_unique,
2563
+ ),
2564
+ (
2565
+ 52,
2566
+ "task_outcomes gains additive derived_outcome + derived_provenance — reward label sourced from the tree-keyed verify ledger, self-report fallback (ADR-0016 stage 1)",
2567
+ _migrate_v52_derived_outcome_columns,
2568
+ ),
2569
+ ]
2570
+
2571
+
2572
+ # ---------------------------------------------------------------------------
2573
+ # Connection helpers
2574
+ # ---------------------------------------------------------------------------
2575
+
2576
+
2577
+ def _apply_pragmas(conn: sqlite3.Connection) -> None:
2578
+ """Apply performance and safety PRAGMAs.
2579
+
2580
+ Tuned for consumer repos up to ~10x meta-repo size (~400K graph nodes,
2581
+ ~600MB DB). Trade-off chosen: durability >= NORMAL (WAL still crash-safe),
2582
+ throughput maximized via mmap + large cache.
2583
+ """
2584
+ conn.execute("PRAGMA journal_mode = WAL")
2585
+ conn.execute("PRAGMA synchronous = NORMAL") # 3-5x faster writes; WAL still crash-safe
2586
+ conn.execute("PRAGMA foreign_keys = ON")
2587
+ conn.execute("PRAGMA temp_store = MEMORY") # sort/group spill to RAM, not disk
2588
+ conn.execute("PRAGMA cache_size = -65536") # 64 MB page cache (signed = KB)
2589
+ conn.execute("PRAGMA mmap_size = 268435456") # 256 MB memory-mapped I/O — skips read() syscalls
2590
+ conn.execute("PRAGMA wal_autocheckpoint = 1000") # checkpoint every ~4MB of WAL (4KB pages)
2591
+ conn.execute("PRAGMA busy_timeout = 5000") # 5s wait on locked DB instead of immediate fail
2592
+
2593
+
2594
+ def get_connection(db_path: str | Path | None = None) -> sqlite3.Connection:
2595
+ """Open a connection with WAL mode and safety PRAGMAs.
2596
+
2597
+ Args:
2598
+ db_path: Path to the SQLite database file.
2599
+ Defaults to .coding-os/coding-os.db (via COS_DB_PATH env).
2600
+
2601
+ Returns:
2602
+ A configured sqlite3.Connection.
2603
+ """
2604
+ path = str(db_path or DEFAULT_DB_PATH)
2605
+ # check_same_thread=False: single-writer model enforced by SqliteBackend's
2606
+ # RLock + WAL. Without this, any consumer that shares the connection
2607
+ # across threads (e.g. MCP server, web routes, test harness) hits
2608
+ # sqlite3.ProgrammingError. Matches get_pooled_conn above.
2609
+ conn = sqlite3.connect(path, timeout=10, check_same_thread=False)
2610
+ conn.row_factory = sqlite3.Row
2611
+ _apply_pragmas(conn)
2612
+ return conn
2613
+
2614
+
2615
+ # ---------------------------------------------------------------------------
2616
+ # Thread-local connection pool for multi-agent concurrency
2617
+ # Spec: docs/phase-n-role-based-routing-plan.md §7a-A
2618
+ # One cached connection per thread; WAL lets readers run concurrently;
2619
+ # busy_timeout=5000 handles writer contention gracefully.
2620
+ # ---------------------------------------------------------------------------
2621
+
2622
+ import threading # noqa: E402
2623
+
2624
+ _thread_local = threading.local()
2625
+ _pool_lock = threading.Lock()
2626
+ _pool_stats = {"hits": 0, "misses": 0, "active": 0}
2627
+
2628
+
2629
+ def get_pooled_conn(db_path: str | Path | None = None) -> sqlite3.Connection:
2630
+ path = str(db_path or DEFAULT_DB_PATH)
2631
+ existing = getattr(_thread_local, "conns", {}).get(path)
2632
+ if existing is not None:
2633
+ try:
2634
+ existing.execute("SELECT 1").fetchone()
2635
+ with _pool_lock:
2636
+ _pool_stats["hits"] += 1
2637
+ return existing
2638
+ except sqlite3.Error:
2639
+ pass # Dead connection, reopen below
2640
+
2641
+ conn = sqlite3.connect(path, timeout=10, check_same_thread=False)
2642
+ conn.row_factory = sqlite3.Row
2643
+ conn.execute("PRAGMA busy_timeout = 5000")
2644
+ _apply_pragmas(conn)
2645
+ if not hasattr(_thread_local, "conns"):
2646
+ _thread_local.conns = {}
2647
+ _thread_local.conns[path] = conn
2648
+ with _pool_lock:
2649
+ _pool_stats["misses"] += 1
2650
+ _pool_stats["active"] += 1
2651
+ return conn
2652
+
2653
+
2654
+ def close_pool() -> None:
2655
+ """Close all pooled connections for the current thread. Safe to call repeatedly."""
2656
+ conns = getattr(_thread_local, "conns", {})
2657
+ for conn in conns.values():
2658
+ try:
2659
+ conn.close()
2660
+ except sqlite3.Error:
2661
+ pass
2662
+ _thread_local.conns = {}
2663
+ with _pool_lock:
2664
+ _pool_stats["active"] = max(0, _pool_stats["active"] - len(conns))
2665
+
2666
+
2667
+ def pool_stats() -> dict[str, int]:
2668
+ """Return pool stats snapshot for observability (N.5-B)."""
2669
+ with _pool_lock:
2670
+ return dict(_pool_stats)
2671
+
2672
+
2673
+ @contextmanager
2674
+ def db_connection(db_path: str | Path | None = None) -> Generator[sqlite3.Connection, None, None]:
2675
+ """Context manager that yields a connection and closes it on exit."""
2676
+ conn = get_connection(db_path)
2677
+ try:
2678
+ yield conn
2679
+ finally:
2680
+ conn.close()
2681
+
2682
+
2683
+ # ---------------------------------------------------------------------------
2684
+ # Schema versioning & migration
2685
+ # ---------------------------------------------------------------------------
2686
+
2687
+
2688
+ def _ensure_version_table(conn: sqlite3.Connection) -> None:
2689
+ """Create the schema_version table if it doesn't exist."""
2690
+ conn.execute(
2691
+ "CREATE TABLE IF NOT EXISTS schema_version ("
2692
+ " version INTEGER PRIMARY KEY,"
2693
+ " description TEXT,"
2694
+ " applied_at DATETIME DEFAULT CURRENT_TIMESTAMP"
2695
+ ")"
2696
+ )
2697
+ conn.commit()
2698
+
2699
+
2700
+ def get_schema_version(conn: sqlite3.Connection) -> int:
2701
+ """Return the highest applied migration version, or 0 if none."""
2702
+ _ensure_version_table(conn)
2703
+ row = conn.execute("SELECT MAX(version) FROM schema_version").fetchone()
2704
+ return row[0] if row[0] is not None else 0
2705
+
2706
+
2707
+ def run_migrations(conn: sqlite3.Connection) -> list[int]:
2708
+ """Apply any unapplied migrations in order.
2709
+
2710
+ Concurrency-safe: takes an EXCLUSIVE transaction on the version
2711
+ table so two simultaneously-opening connections don't both try to
2712
+ apply the same migration and trip the UNIQUE constraint. Idempotent
2713
+ via INSERT OR IGNORE on the version row.
2714
+
2715
+ Returns:
2716
+ List of migration versions that were applied.
2717
+ """
2718
+ _ensure_version_table(conn)
2719
+ applied: list[int] = []
2720
+
2721
+ try:
2722
+ conn.execute("BEGIN IMMEDIATE")
2723
+ except sqlite3.OperationalError:
2724
+ # Another writer holds the lock — wait briefly and re-read; if
2725
+ # we're already at the target version, return without doing
2726
+ # anything. This is the common case under concurrent dispatcher
2727
+ # workers.
2728
+ pass
2729
+ try:
2730
+ current = get_schema_version(conn)
2731
+ for version, description, action in MIGRATIONS:
2732
+ if version <= current:
2733
+ continue
2734
+ logger.info("Applying migration v%d: %s", version, description)
2735
+ try:
2736
+ if callable(action):
2737
+ action(conn)
2738
+ else:
2739
+ conn.executescript(action)
2740
+ except sqlite3.OperationalError as exc:
2741
+ # ALTER TABLE under concurrent runners can race past the
2742
+ # column-exists guard — skip when the message explicitly
2743
+ # confirms the schema is already where we want it.
2744
+ if "duplicate column name" in str(exc).lower():
2745
+ logger.debug(
2746
+ "migration v%d ALTER race tolerated: %s",
2747
+ version,
2748
+ exc,
2749
+ )
2750
+ else:
2751
+ raise
2752
+ conn.execute(
2753
+ "INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)",
2754
+ (version, description),
2755
+ )
2756
+ applied.append(version)
2757
+ conn.commit()
2758
+ except sqlite3.IntegrityError as exc:
2759
+ logger.debug("migration race resolved: %s", exc)
2760
+ conn.rollback()
2761
+ applied = []
2762
+
2763
+ if applied:
2764
+ logger.info("Migrations applied: %s (now at v%d)", applied, applied[-1])
2765
+ else:
2766
+ logger.debug("Schema up-to-date at v%d", current)
2767
+
2768
+ return applied
2769
+
2770
+
2771
+ # ---------------------------------------------------------------------------
2772
+ # Stats helpers (used by health tool)
2773
+ # ---------------------------------------------------------------------------
2774
+
2775
+ _TABLES = [
2776
+ "task_outcomes",
2777
+ "agent_metrics",
2778
+ "learned_patterns",
2779
+ "observations",
2780
+ "session_summaries",
2781
+ "outcome_history",
2782
+ "concept_graph",
2783
+ "embeddings",
2784
+ "document_chunks",
2785
+ "tasks",
2786
+ "memory_audit",
2787
+ "pattern_validations",
2788
+ "retrievals",
2789
+ "retrieval_quality",
2790
+ "graph_nodes",
2791
+ "graph_edges_v12",
2792
+ "graph_evidence_v12",
2793
+ "file_index_state",
2794
+ "retrieval_router_log",
2795
+ ]
2796
+
2797
+
2798
+ def get_db_stats(conn: sqlite3.Connection) -> dict:
2799
+ """Collect row counts per table and DB file size."""
2800
+ stats: dict = {"tables": {}}
2801
+
2802
+ for table in _TABLES:
2803
+ try:
2804
+ row = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()
2805
+ stats["tables"][table] = row[0]
2806
+ except sqlite3.OperationalError:
2807
+ stats["tables"][table] = None # table doesn't exist yet
2808
+
2809
+ stats["schema_version"] = get_schema_version(conn)
2810
+ stats["fts5_available"] = has_fts5(conn)
2811
+
2812
+ # File size
2813
+ db_path = conn.execute("PRAGMA database_list").fetchone()[2]
2814
+ if db_path and os.path.exists(db_path):
2815
+ stats["db_size_bytes"] = os.path.getsize(db_path)
2816
+ else:
2817
+ stats["db_size_bytes"] = 0
2818
+
2819
+ return stats
2820
+
2821
+
2822
+ # ---------------------------------------------------------------------------
2823
+ # Bootstrap (called on server start and by --test)
2824
+ # ---------------------------------------------------------------------------
2825
+
2826
+
2827
+ def _refuse_global_hub_db(target: Path) -> None:
2828
+ try:
2829
+ hub_state = (Path.home() / STATE_DIRNAME).resolve()
2830
+ except (OSError, RuntimeError):
2831
+ return
2832
+ if target.parent.resolve() == hub_state:
2833
+ raise RuntimeError(
2834
+ f"refusing to create a project DB inside the global hub state dir "
2835
+ f"({hub_state}) — set $COS_DB_PATH or run inside a project"
2836
+ )
2837
+
2838
+
2839
+ def init_db(db_path: str | Path | None = None) -> sqlite3.Connection:
2840
+ """Open the DB, run migrations, return the live connection.
2841
+
2842
+ Renames a legacy `thinking_os.db` sibling to the canonical
2843
+ `coding-os.db` once, before opening — silent no-op when the
2844
+ rename has already happened or no legacy file exists.
2845
+
2846
+ Also asks SQLite to refresh its query-planner statistics
2847
+ (`PRAGMA optimize`) once per process. Without this, the planner
2848
+ falls back to heuristics and routinely picks the slower index for
2849
+ multi-table JOINs (observed: 14ms → 2ms on graph_nodes JOIN
2850
+ graph_edges_v12 after stats present). Cost is bounded — the pragma
2851
+ is a no-op when stats are current.
2852
+ """
2853
+ target = Path(db_path) if db_path else DEFAULT_DB_PATH
2854
+ _refuse_global_hub_db(target)
2855
+ target.parent.mkdir(parents=True, exist_ok=True)
2856
+ migrate_legacy_db_filename(target)
2857
+ conn = get_connection(str(target))
2858
+ run_migrations(conn)
2859
+ _ensure_query_planner_stats(conn)
2860
+ return conn
2861
+
2862
+
2863
+ def _ensure_query_planner_stats(conn: sqlite3.Connection) -> None:
2864
+ """Make sure SQLite has query-planner stats; refresh stale ones cheaply.
2865
+
2866
+ First call on a fresh DB runs a full ``ANALYZE`` (one-shot cost,
2867
+ ~50ms even on 600MB DBs). Subsequent calls hit only ``PRAGMA optimize``
2868
+ which is a no-op when stats are current and very cheap otherwise.
2869
+
2870
+ Without this the planner picks the wrong index on graph JOINs
2871
+ (measured: 14ms vs 3ms on graph_nodes JOIN graph_edges_v12).
2872
+ """
2873
+ try:
2874
+ row = conn.execute(
2875
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='sqlite_stat1'"
2876
+ ).fetchone()
2877
+ if row is None:
2878
+ conn.execute("ANALYZE")
2879
+ conn.commit()
2880
+ else:
2881
+ conn.execute("PRAGMA optimize")
2882
+ except sqlite3.Error as exc:
2883
+ logger.debug("query-planner stats refresh skipped: %s", exc)