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,2078 @@
1
+ """
2
+ Thinking OS — MCP learning tools.
3
+
4
+ Pattern mining and rule suggestion:
5
+ - cos_learn_extract: discover patterns from task outcomes
6
+ - cos_learn_suggest: return relevant patterns for current context
7
+ - cos_learn_validate: confirm/deny a pattern's usefulness
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import logging
15
+ import os
16
+ import re
17
+ import sqlite3
18
+ from datetime import datetime, timedelta, timezone
19
+ from pathlib import Path
20
+ from typing import Optional
21
+
22
+ logger = logging.getLogger("thinking_os.learning")
23
+
24
+ MIN_DATA_THRESHOLD = 3 # minimum task outcomes before extraction
25
+
26
+ # self-validation throttle window. Same (session, pattern)
27
+ # positive validation is ignored within this window. 1h is long enough to
28
+ # cover a continuous task loop but short enough that legitimate re-use
29
+ # across sessions isn't suppressed.
30
+ _THROTTLE_WINDOW_SECONDS = 3600
31
+
32
+
33
+ def _read_session_id_for_validate() -> str:
34
+ import os
35
+ from pathlib import Path
36
+
37
+ state_dir = Path(os.environ.get("COS_STATE_DIR", ".coding-os"))
38
+ agent_dir_env = os.environ.get("COS_AGENT_DIR")
39
+ if agent_dir_env:
40
+ f = Path(agent_dir_env) / "session-id"
41
+ if f.exists():
42
+ sid = f.read_text().strip()
43
+ if sid:
44
+ return sid
45
+ agent = os.environ.get("COS_AGENT", "")
46
+ if not agent:
47
+ marker = state_dir / ".agent"
48
+ if marker.exists():
49
+ agent = marker.read_text().strip()
50
+ if agent:
51
+ f = state_dir / agent / "session-id"
52
+ if f.exists():
53
+ sid = f.read_text().strip()
54
+ if sid:
55
+ return sid
56
+ flat = state_dir / "session-id"
57
+ if flat.exists():
58
+ sid = flat.read_text().strip()
59
+ if sid:
60
+ return sid
61
+ return "ses-unknown"
62
+
63
+
64
+ def _has_recent_validation(
65
+ conn: sqlite3.Connection,
66
+ session_id: str,
67
+ pattern_id: int,
68
+ ) -> bool:
69
+ try:
70
+ row = conn.execute(
71
+ "SELECT 1 FROM pattern_validations "
72
+ "WHERE session_id = ? AND pattern_id = ? AND was_helpful = 1 "
73
+ " AND created_at >= datetime('now', '-' || ? || ' seconds') "
74
+ "LIMIT 1",
75
+ (session_id, pattern_id, _THROTTLE_WINDOW_SECONDS),
76
+ ).fetchone()
77
+ except sqlite3.OperationalError:
78
+ return False
79
+ return row is not None
80
+
81
+
82
+ def _log_validation(
83
+ conn: sqlite3.Connection,
84
+ *,
85
+ session_id: str,
86
+ pattern_id: int,
87
+ was_helpful: bool,
88
+ was_throttled: bool,
89
+ ) -> None:
90
+ # Fire-and-forget — never raises (audit row, must not break validation).
91
+ try:
92
+ conn.execute(
93
+ "INSERT INTO pattern_validations "
94
+ "(session_id, pattern_id, was_helpful, was_throttled) "
95
+ "VALUES (?, ?, ?, ?)",
96
+ (session_id, pattern_id, 1 if was_helpful else 0, 1 if was_throttled else 0),
97
+ )
98
+ conn.commit()
99
+ except sqlite3.OperationalError as exc:
100
+ logger.debug("_log_validation skipped: %s", exc)
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Confidence formulas (brain-inspired)
105
+ # ---------------------------------------------------------------------------
106
+
107
+
108
+ def boost_success(conf: float) -> float:
109
+ """LTP with diminishing returns — validated pattern gets stronger."""
110
+ return min(0.95, conf + 0.1 * (1.0 - conf))
111
+
112
+
113
+ def penalize_failure(conf: float) -> float:
114
+ """LTD proportional — violated pattern weakens."""
115
+ return max(0.1, conf - 0.15 * conf)
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # cos_learn_extract
120
+ # ---------------------------------------------------------------------------
121
+
122
+
123
+ def learn_extract(
124
+ conn: sqlite3.Connection,
125
+ *,
126
+ min_occurrences: int = 3,
127
+ ) -> dict:
128
+ """Scan task_outcomes to discover recurring patterns.
129
+
130
+ Detects:
131
+ - domain_rework: domains with high rework rates
132
+ - skill_correlation: skills correlated with success/failure
133
+ - complexity_mismatch: tasks classified too low/high
134
+
135
+ Args:
136
+ conn: SQLite connection.
137
+ min_occurrences: Minimum occurrences to consider a pattern.
138
+
139
+ Returns:
140
+ Dict with extracted patterns and stats.
141
+ """
142
+ min_occurrences = max(1, min_occurrences)
143
+
144
+ # Check data threshold
145
+ total_outcomes = conn.execute("SELECT COUNT(*) FROM task_outcomes").fetchone()[0]
146
+ if total_outcomes < MIN_DATA_THRESHOLD:
147
+ return {
148
+ "status": "insufficient_data",
149
+ "message": f"Insufficient data (need {MIN_DATA_THRESHOLD}+ outcomes, have {total_outcomes})",
150
+ "extracted": [],
151
+ }
152
+
153
+ extracted: list[dict] = []
154
+
155
+ # Heal any legacy count-snapshot duplicates before mining so the upsert
156
+ # below updates a single survivor row per fact.
157
+ _collapse_duplicate_patterns(conn)
158
+
159
+ # --- Domain rework patterns ---
160
+ domain_rows = conn.execute(
161
+ "SELECT domain, COUNT(*) AS total, "
162
+ "SUM(CASE WHEN outcome = 'rework' THEN 1 ELSE 0 END) AS rework_count "
163
+ "FROM task_outcomes "
164
+ "GROUP BY domain "
165
+ "HAVING rework_count >= ?",
166
+ (min_occurrences,),
167
+ ).fetchall()
168
+
169
+ for row in domain_rows:
170
+ d = dict(row)
171
+ rework_rate = d["rework_count"] / d["total"] if d["total"] > 0 else 0
172
+ if rework_rate < 0.2:
173
+ continue # not significant enough
174
+ confidence = min(0.9, d["rework_count"] / (d["total"] * 2))
175
+ pattern_text = (
176
+ f"{d['domain']} domain has {rework_rate:.0%} rework rate "
177
+ f"({d['rework_count']}/{d['total']} tasks)"
178
+ )
179
+ extracted.append(
180
+ _upsert_pattern(
181
+ conn,
182
+ pattern=pattern_text,
183
+ memory_type="pattern",
184
+ domain=d["domain"],
185
+ source="learn_extract",
186
+ confidence=confidence,
187
+ concepts=json.dumps([d["domain"].lower(), "rework", "domain_pattern"]),
188
+ )
189
+ )
190
+
191
+ # --- Skill correlation patterns ---
192
+ skill_rows = conn.execute(
193
+ "SELECT skills_used, outcome, COUNT(*) AS count "
194
+ "FROM task_outcomes "
195
+ "WHERE skills_used IS NOT NULL AND skills_used != '' "
196
+ "GROUP BY skills_used, outcome "
197
+ "HAVING count >= ?",
198
+ (min_occurrences,),
199
+ ).fetchall()
200
+
201
+ for row in skill_rows:
202
+ d = dict(row)
203
+ if d["outcome"] == "rework":
204
+ confidence = min(0.9, d["count"] / 10.0)
205
+ pattern_text = (
206
+ f"Skill '{d['skills_used']}' correlates with rework ({d['count']} occurrences)"
207
+ )
208
+ extracted.append(
209
+ _upsert_pattern(
210
+ conn,
211
+ pattern=pattern_text,
212
+ memory_type="pattern",
213
+ domain=None,
214
+ source="learn_extract",
215
+ confidence=confidence,
216
+ concepts=json.dumps(["skill", d["skills_used"], "rework"]),
217
+ )
218
+ )
219
+
220
+ # --- Complexity mismatch patterns ---
221
+ mismatch_rows = conn.execute(
222
+ "SELECT complexity, outcome, COUNT(*) AS count "
223
+ "FROM task_outcomes "
224
+ "WHERE outcome = 'rework' "
225
+ "GROUP BY complexity "
226
+ "HAVING count >= ?",
227
+ (min_occurrences,),
228
+ ).fetchall()
229
+
230
+ for row in mismatch_rows:
231
+ d = dict(row)
232
+ # Check if CLEAR tasks frequently rework (likely underclassified)
233
+ if d["complexity"] == "CLEAR" and d["count"] >= min_occurrences:
234
+ total_clear = conn.execute(
235
+ "SELECT COUNT(*) FROM task_outcomes WHERE complexity = 'CLEAR'"
236
+ ).fetchone()[0]
237
+ if total_clear > 0:
238
+ rate = d["count"] / total_clear
239
+ if rate > 0.3:
240
+ confidence = min(0.9, rate)
241
+ pattern_text = (
242
+ f"CLEAR tasks rework at {rate:.0%} — may be underclassified "
243
+ f"({d['count']}/{total_clear})"
244
+ )
245
+ extracted.append(
246
+ _upsert_pattern(
247
+ conn,
248
+ pattern=pattern_text,
249
+ memory_type="decision",
250
+ domain=None,
251
+ source="learn_extract",
252
+ confidence=confidence,
253
+ concepts=json.dumps(["complexity", "classification", "mismatch"]),
254
+ )
255
+ )
256
+
257
+ # --- Success baseline patterns (positive-signal mining) ---
258
+ # A healthy success-only history must still yield learnable patterns —
259
+ # without this the loop can ONLY learn from failure, so a project that
260
+ # rarely reworks produces zero patterns forever. Mine per-domain and
261
+ # per-skill success so cos_learn_suggest has positive anchors to rank.
262
+ # Variance gate: a success-rate stat only informs when the corpus has a
263
+ # non-success outcome to contrast against. On a monotone-success corpus
264
+ # every "X succeeds 100%" is a tautology — skip both stat branches.
265
+ # See docs/engineering/learning-extraction.md § Variance gate.
266
+ _has_variance = (
267
+ conn.execute("SELECT COUNT(*) FROM task_outcomes WHERE outcome != 'success'").fetchone()[0]
268
+ > 0
269
+ )
270
+
271
+ success_domain_rows = (
272
+ conn.execute(
273
+ "SELECT domain, COUNT(*) AS total, "
274
+ "SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) AS success_count "
275
+ "FROM task_outcomes WHERE domain IS NOT NULL AND domain != '' "
276
+ "GROUP BY domain HAVING success_count >= ?",
277
+ (min_occurrences,),
278
+ ).fetchall()
279
+ if _has_variance
280
+ else []
281
+ )
282
+ for row in success_domain_rows:
283
+ d = dict(row)
284
+ rate = d["success_count"] / d["total"] if d["total"] else 0
285
+ confidence = min(0.85, 0.4 + d["success_count"] / 20.0)
286
+ pattern_text = (
287
+ f"{d['domain']} domain succeeds at {rate:.0%} "
288
+ f"({d['success_count']}/{d['total']} tasks) — reliable baseline"
289
+ )
290
+ extracted.append(
291
+ _upsert_pattern(
292
+ conn,
293
+ pattern=pattern_text,
294
+ # 'stat' (not a belief): a success rate is observability, not a
295
+ # lesson — excluded from the digest + cos_learn_suggest so it
296
+ # never masquerades as a learning. See learning-extraction.md.
297
+ memory_type="stat",
298
+ domain=d["domain"],
299
+ source="learn_extract",
300
+ confidence=confidence,
301
+ concepts=json.dumps([d["domain"].lower(), "success", "baseline", "stat"]),
302
+ )
303
+ )
304
+
305
+ skill_success_rows = (
306
+ conn.execute(
307
+ "SELECT skills_used, COUNT(*) AS count "
308
+ "FROM task_outcomes "
309
+ "WHERE outcome = 'success' AND skills_used IS NOT NULL AND skills_used != '' "
310
+ "GROUP BY skills_used HAVING count >= ?",
311
+ (min_occurrences,),
312
+ ).fetchall()
313
+ if _has_variance
314
+ else []
315
+ )
316
+ for row in skill_success_rows:
317
+ d = dict(row)
318
+ confidence = min(0.8, 0.4 + d["count"] / 20.0)
319
+ pattern_text = (
320
+ f"Skill set '{d['skills_used']}' correlates with success ({d['count']} tasks)"
321
+ )
322
+ extracted.append(
323
+ _upsert_pattern(
324
+ conn,
325
+ pattern=pattern_text,
326
+ memory_type="stat", # observability, not a belief — see above
327
+ domain=None,
328
+ source="learn_extract",
329
+ confidence=confidence,
330
+ concepts=json.dumps(["skill", "success", "correlation", "stat"]),
331
+ )
332
+ )
333
+
334
+ # --- Failure anatomy patterns (v25) ---
335
+ # Mine structured backtrack_events for recurring root_cause patterns.
336
+ # Only runs when anatomy columns are present (migration v25).
337
+ try:
338
+ from tools.cognition import CANONICAL_REMEDIES
339
+
340
+ anat_rows = conn.execute(
341
+ # Anatomy pairs a recurring cause with its remedy — the one the agent
342
+ # recorded, else the canonical corrective action for that cause. Never
343
+ # a bare count (learning-extraction.md § Anatomy from backtracks).
344
+ "SELECT root_cause, COUNT(*) AS cnt, "
345
+ " GROUP_CONCAT(DISTINCT from_formula) AS formulas, "
346
+ " MAX(corrective_action) AS remedy "
347
+ "FROM backtrack_events "
348
+ "WHERE root_cause IS NOT NULL "
349
+ "GROUP BY root_cause "
350
+ "HAVING cnt >= ?",
351
+ (min_occurrences,),
352
+ ).fetchall()
353
+ for row in anat_rows:
354
+ d = dict(row)
355
+ remedy = (d["remedy"] or "").strip() or CANONICAL_REMEDIES.get(d["root_cause"], "")
356
+ if not remedy:
357
+ continue # no recorded nor canonical remedy — skip, never a bare count
358
+ confidence = min(0.85, d["cnt"] / 20.0 + 0.3)
359
+ formulas_str = d["formulas"] or ""
360
+ pattern_text = (
361
+ f"Recurring backtrack root cause '{d['root_cause']}' "
362
+ f"({d['cnt']} occurrences"
363
+ + (f"; formulas: {formulas_str[:60]}" if formulas_str else "")
364
+ + f") → {remedy[:160]}"
365
+ )
366
+ extracted.append(
367
+ _upsert_pattern(
368
+ conn,
369
+ pattern=pattern_text,
370
+ memory_type="failure",
371
+ domain=None,
372
+ source="learn_extract",
373
+ confidence=confidence,
374
+ concepts=json.dumps(["failure", d["root_cause"], "backtrack"]),
375
+ )
376
+ )
377
+ except Exception as exc: # backtrack_events or anatomy columns absent — fire-and-forget
378
+ logger.debug("learn_extract: failure anatomy skipped: %s", exc)
379
+
380
+ # --- Friction lessons ---
381
+ # The abundant, automatic learning signal: hook BLOCKs and tool failures
382
+ # the agent emits every session. Mined into actionable
383
+ # `lesson` patterns so the loop learns from mistakes — not just success
384
+ # statistics. Contract: docs/engineering/learning-extraction.md.
385
+ try:
386
+ distill_budget = int(os.environ.get("COS_DISTILL_MAX_CLUSTERS", "20"))
387
+ except ValueError:
388
+ distill_budget = 20
389
+ distill_state = {"remaining": max(0, distill_budget)}
390
+ extracted.extend(
391
+ _mine_friction_lessons(conn, min_occurrences=min_occurrences, distill_state=distill_state)
392
+ )
393
+ # Hook BLOCKs live in the activity log (not observations) on Claude — mine
394
+ # them too so the richest friction signal becomes a lesson.
395
+ extracted.extend(
396
+ _mine_hook_block_lessons(conn, min_occurrences=min_occurrences, distill_state=distill_state)
397
+ )
398
+ # fix:/revert: commit subjects — the real engineering-lesson signal that
399
+ # reasoning records in git history, not in any friction table (§5).
400
+ extracted.extend(_mine_commit_lessons(conn, min_occurrences=min_occurrences))
401
+
402
+ # Generalize related lessons into human-review drafts (B3). Fire-and-forget;
403
+ # writes only when a NEW cluster forms (deduped). Never blocks extraction.
404
+ try:
405
+ generalize_lessons(conn)
406
+ except Exception as exc:
407
+ logger.debug("generalize_lessons skipped: %s", exc)
408
+
409
+ conn.commit()
410
+ return {
411
+ "status": "ok",
412
+ "total_outcomes_analyzed": total_outcomes,
413
+ "extracted": extracted,
414
+ }
415
+
416
+
417
+ _SOURCE_TO_PROVENANCE: dict[str, str] = {
418
+ "learn_extract": "extracted_from_outcome",
419
+ "friction": "extracted_from_observation",
420
+ "commit": "extracted_from_commit",
421
+ "breakthrough": "agent_self",
422
+ "manual": "user_directive",
423
+ "import": "imported",
424
+ }
425
+
426
+
427
+ # Volatile counters embedded in mined pattern text — the running task
428
+ # count grows every extraction run, so it must NOT be part of a pattern's
429
+ # identity or each run mints a new snapshot row instead of updating one.
430
+ _IDENTITY_COUNT_RE = re.compile(r"\(\d+(?:/\d+)?\s*(?:tasks?|occurrences?)[^)]*\)", re.IGNORECASE)
431
+ _IDENTITY_RATIO_RE = re.compile(r"\(\d+/\d+\)")
432
+ _IDENTITY_PCT_RE = re.compile(r"\d+(?:\.\d+)?%")
433
+
434
+
435
+ def _pattern_identity(text: str) -> str:
436
+ # Count-agnostic dedup key: strip the running counts / percentages so a
437
+ # re-mined fact ("INFRA succeeds … (40/40)" → "(83/83)") maps to the
438
+ # SAME row. The displayed `pattern` keeps the live numbers; only the
439
+ # identity ignores them.
440
+ t = _IDENTITY_COUNT_RE.sub("", text)
441
+ t = _IDENTITY_RATIO_RE.sub("", t)
442
+ t = _IDENTITY_PCT_RE.sub("", t)
443
+ return " ".join(t.split()).lower()
444
+
445
+
446
+ def _collapse_duplicate_patterns(conn: sqlite3.Connection) -> int:
447
+ # Self-healing one-shot: merge legacy count-snapshot duplicates that the
448
+ # previously exact-text dedup let accumulate. Idempotent — once each
449
+ # (identity, domain) group is a single row, this is a no-op. Returns the
450
+ # number of rows deleted.
451
+ rows = conn.execute(
452
+ "SELECT id, pattern, domain, confidence, times_seen, times_validated FROM learned_patterns"
453
+ ).fetchall()
454
+ groups: dict[tuple[str, object], list] = {}
455
+ for r in rows:
456
+ groups.setdefault((_pattern_identity(r["pattern"]), r["domain"]), []).append(r)
457
+ removed = 0
458
+ for members in groups.values():
459
+ if len(members) < 2:
460
+ continue
461
+ # Survivor = the most-established row by occurrences; fold BOTH counters so
462
+ # neither the occurrence total (times_seen) nor real validations are lost.
463
+ survivor = max(members, key=lambda m: ((m["times_seen"] or 0), m["confidence"], m["id"]))
464
+ losers = [m["id"] for m in members if m["id"] != survivor["id"]]
465
+ conn.execute(
466
+ "UPDATE learned_patterns SET pattern = ?, confidence = ?, times_seen = ?, "
467
+ "times_validated = ?, last_validated = CURRENT_TIMESTAMP WHERE id = ?",
468
+ (
469
+ survivor["pattern"],
470
+ max(m["confidence"] for m in members),
471
+ sum((m["times_seen"] or 0) for m in members) + len(losers),
472
+ sum((m["times_validated"] or 0) for m in members),
473
+ survivor["id"],
474
+ ),
475
+ )
476
+ conn.executemany("DELETE FROM learned_patterns WHERE id = ?", [(i,) for i in losers])
477
+ removed += len(losers)
478
+ return removed
479
+
480
+
481
+ def _consolidate_semantic_duplicates(
482
+ conn: sqlite3.Connection, *, threshold: float = 0.85, dry_run: bool = False
483
+ ) -> int:
484
+ # Survivor = highest (confidence, times_seen, oldest id); loser's access_count
485
+ # + times_seen + times_validated fold in before delete. No-op without embeddings.
486
+ try:
487
+ from embeddings import cosine_similarity, is_available
488
+ except ImportError:
489
+ return 0
490
+ if not is_available():
491
+ return 0
492
+ try:
493
+ rows = conn.execute(
494
+ "SELECT lp.id, lp.confidence, lp.times_seen, lp.times_validated, lp.access_count, e.embedding "
495
+ "FROM learned_patterns lp JOIN embeddings e "
496
+ " ON e.source_table = 'learned_patterns' AND e.source_id = lp.id "
497
+ "WHERE lp.promoted_to IS NULL AND lp.archived_at IS NULL"
498
+ ).fetchall()
499
+ except sqlite3.OperationalError as exc:
500
+ logger.debug("semantic consolidation skipped: %s", exc)
501
+ return 0
502
+
503
+ items = [dict(r) for r in rows if r["embedding"]]
504
+ if len(items) < 2:
505
+ return 0
506
+ # Stronger row first → it becomes the survivor of any similar pair.
507
+ items.sort(key=lambda x: (-(x["confidence"] or 0.0), -(x["times_seen"] or 0), x["id"]))
508
+
509
+ removed: set[int] = set()
510
+ merged = 0
511
+ for i, survivor in enumerate(items):
512
+ if survivor["id"] in removed:
513
+ continue
514
+ cands = [c for c in items[i + 1 :] if c["id"] not in removed]
515
+ if not cands:
516
+ continue
517
+ scores = cosine_similarity(survivor["embedding"], [c["embedding"] for c in cands])
518
+ for cand, score in zip(cands, scores):
519
+ if score < threshold:
520
+ continue
521
+ if not dry_run:
522
+ conn.execute(
523
+ "UPDATE learned_patterns SET access_count = COALESCE(access_count, 0) + ?, "
524
+ "times_seen = COALESCE(times_seen, 0) + ?, "
525
+ "times_validated = COALESCE(times_validated, 0) + ? WHERE id = ?",
526
+ (
527
+ cand["access_count"] or 0,
528
+ cand["times_seen"] or 0,
529
+ cand["times_validated"] or 0,
530
+ survivor["id"],
531
+ ),
532
+ )
533
+ conn.execute("DELETE FROM learned_patterns WHERE id = ?", (cand["id"],))
534
+ conn.execute(
535
+ "DELETE FROM embeddings WHERE source_table = 'learned_patterns' AND source_id = ?",
536
+ (cand["id"],),
537
+ )
538
+ removed.add(cand["id"])
539
+ merged += 1
540
+ return merged
541
+
542
+
543
+ def _format_generalize_draft(cluster: list[dict]) -> str:
544
+ lines = [
545
+ "---",
546
+ "type: feedback",
547
+ "status: draft",
548
+ f"lessons: {len(cluster)}",
549
+ "---",
550
+ "",
551
+ f"# Generalize {len(cluster)} related lessons",
552
+ "",
553
+ "These lessons recur on a shared theme. Consider distilling ONE general",
554
+ "rule and promoting it — this is a HUMAN-REVIEW draft; the system never",
555
+ "auto-writes rules.",
556
+ "",
557
+ "## Member lessons",
558
+ ]
559
+ lines += [f"- (#{c['id']}) {c['pattern']}" for c in cluster]
560
+ lines += [
561
+ "",
562
+ "## Suggested action",
563
+ "- If they share a root cause, write one rule that covers all of them.",
564
+ "- Then `cos_promote(pattern_id=<strongest>, target='feedback'|'rule')`.",
565
+ ]
566
+ return "\n".join(lines) + "\n"
567
+
568
+
569
+ def generalize_lessons(
570
+ conn: sqlite3.Connection, *, min_cluster: int = 3, sim_threshold: float = 0.6
571
+ ) -> dict:
572
+ """Surface generalizable lesson clusters as human-review drafts (B3).
573
+
574
+ Greedily clusters `lesson` patterns by embeddings cosine; when >= min_cluster
575
+ related lessons share a theme, writes a feedback draft to
576
+ `.coding-os/memory/drafts/` suggesting one general rule. NO LLM, NEVER writes
577
+ to rules/docs — abstraction stays human-gated. Deduped by cluster signature.
578
+ Returns {"drafts": [filenames]}. No-op without embeddings / project root.
579
+ """
580
+ try:
581
+ from embeddings import cosine_similarity, is_available
582
+ except ImportError:
583
+ return {"drafts": []}
584
+ if not is_available():
585
+ return {"drafts": []}
586
+ root = _derive_project_root(conn)
587
+ if root is None:
588
+ return {"drafts": []}
589
+ try:
590
+ rows = conn.execute(
591
+ "SELECT lp.id, lp.pattern, e.embedding FROM learned_patterns lp "
592
+ "JOIN embeddings e ON e.source_table = 'learned_patterns' AND e.source_id = lp.id "
593
+ "WHERE lp.memory_type = 'lesson' AND lp.archived_at IS NULL AND lp.promoted_to IS NULL"
594
+ ).fetchall()
595
+ except sqlite3.OperationalError as exc:
596
+ logger.debug("generalize_lessons skipped: %s", exc)
597
+ return {"drafts": []}
598
+
599
+ items = [dict(r) for r in rows if r["embedding"]]
600
+ if len(items) < min_cluster:
601
+ return {"drafts": []}
602
+
603
+ drafts_dir = root / ".coding-os" / "memory" / "drafts"
604
+ clustered: set[int] = set()
605
+ drafts: list[str] = []
606
+ for seed in items:
607
+ if seed["id"] in clustered:
608
+ continue
609
+ rest = [c for c in items if c["id"] != seed["id"] and c["id"] not in clustered]
610
+ if not rest:
611
+ break
612
+ scores = cosine_similarity(seed["embedding"], [c["embedding"] for c in rest])
613
+ cluster = [seed] + [c for c, s in zip(rest, scores) if s >= sim_threshold]
614
+ if len(cluster) < min_cluster:
615
+ continue
616
+ for c in cluster:
617
+ clustered.add(c["id"])
618
+ sig = "-".join(str(c["id"]) for c in sorted(cluster, key=lambda x: x["id"]))
619
+ fname = f"generalize-{hashlib.sha1(sig.encode()).hexdigest()[:10]}.md"
620
+ target = drafts_dir / fname
621
+ if target.exists():
622
+ continue
623
+ try:
624
+ drafts_dir.mkdir(parents=True, exist_ok=True)
625
+ target.write_text(_format_generalize_draft(cluster), encoding="utf-8")
626
+ drafts.append(fname)
627
+ except OSError as exc:
628
+ logger.debug("generalize draft write failed: %s", exc)
629
+ return {"drafts": drafts}
630
+
631
+
632
+ def _upsert_pattern(
633
+ conn: sqlite3.Connection,
634
+ *,
635
+ pattern: str,
636
+ memory_type: str,
637
+ domain: str | None,
638
+ source: str,
639
+ confidence: float,
640
+ concepts: str,
641
+ provenance: str | None = None,
642
+ distill_fingerprint: str | None = None,
643
+ evidence_json: str | None = None,
644
+ ) -> dict:
645
+ # Sanitizer runs before any DB write; a rejected pattern returns
646
+ # {"action": "rejected", ...} with no row touched. provenance keeps
647
+ # agent_self writes distinguishable from mined data for sycophancy analysis.
648
+ if provenance is None:
649
+ provenance = _SOURCE_TO_PROVENANCE.get(source, "agent_self")
650
+ from sanitizer import sanitize_write
651
+
652
+ p_sr = sanitize_write(
653
+ "pattern",
654
+ pattern,
655
+ actor="learning._upsert_pattern",
656
+ source_table="learned_patterns",
657
+ conn=conn,
658
+ )
659
+ if not p_sr.ok:
660
+ return {
661
+ "id": None,
662
+ "pattern": (pattern or "")[:60],
663
+ "confidence": 0.0,
664
+ "action": "rejected",
665
+ "reason": p_sr.reason,
666
+ }
667
+ pattern = p_sr.cleaned
668
+
669
+ # Match on a count-agnostic identity, not exact text: a re-mined fact
670
+ # whose running count grew ("(40/40)" → "(83/83)") is the SAME pattern
671
+ # and must update its row, not insert a snapshot. The table is small, so
672
+ # canonicalise candidate rows in the same domain.
673
+ identity = _pattern_identity(pattern)
674
+ existing = None
675
+ if distill_fingerprint:
676
+ try:
677
+ existing = conn.execute(
678
+ "SELECT id, pattern, confidence, times_validated FROM learned_patterns "
679
+ "WHERE distill_fingerprint = ?",
680
+ (distill_fingerprint,),
681
+ ).fetchone()
682
+ except sqlite3.OperationalError:
683
+ existing = None
684
+ if existing is None:
685
+ for cand in conn.execute(
686
+ "SELECT id, pattern, confidence, times_validated FROM learned_patterns WHERE domain IS ?",
687
+ (domain,),
688
+ ):
689
+ if _pattern_identity(cand["pattern"]) == identity:
690
+ existing = cand
691
+ break
692
+
693
+ if existing:
694
+ # Confidence is owned by validation (LTP/LTD), not re-extraction: a
695
+ # re-mine bumps times_seen (the occurrence count) and refreshes the text,
696
+ # but must NOT raise confidence — otherwise re-mining resurrects a belief
697
+ # that learn_validate penalized, and LTD could never lower a bad pattern.
698
+ # First-insert seeds the prior; validation moves it from there.
699
+ new_conf = existing["confidence"]
700
+ # Re-extraction is a positive signal: refresh recency AND revive a row a
701
+ # prior decay run archived. A REAL promotion (promoted_to='rule:…' /
702
+ # 'feedback:…') survives the re-mine — the knowledge now lives in the
703
+ # rule layer, and un-promoting it would put the same fact in two places.
704
+ conn.execute(
705
+ # Refresh memory_type too: a re-mine reclassifies a row whose class
706
+ # changed (e.g. a legacy success baseline minted as 'pattern' becomes
707
+ # 'stat'), so old garbage reclassifies on the next loop run.
708
+ "UPDATE learned_patterns SET pattern = ?, memory_type = ?, confidence = ?, "
709
+ "times_seen = COALESCE(times_seen, 0) + 1, last_validated = CURRENT_TIMESTAMP, "
710
+ "last_accessed_at = CURRENT_TIMESTAMP, "
711
+ "promoted_to = CASE WHEN COALESCE(promoted_to, '') IN ('', 'archived') "
712
+ " THEN NULL ELSE promoted_to END, "
713
+ "archived_at = CASE WHEN COALESCE(promoted_to, '') IN ('', 'archived') "
714
+ " THEN NULL ELSE archived_at END, "
715
+ "distill_fingerprint = COALESCE(?, distill_fingerprint), "
716
+ "evidence_json = COALESCE(?, evidence_json) "
717
+ "WHERE id = ?",
718
+ (pattern, memory_type, new_conf, distill_fingerprint, evidence_json, existing["id"]),
719
+ )
720
+ pattern_id = existing["id"]
721
+ result = {"id": pattern_id, "pattern": pattern, "confidence": new_conf, "action": "updated"}
722
+ else:
723
+ # Stamp last_validated/last_accessed_at at creation so a fresh pattern's age is 0.
724
+ # Otherwise run_decay reads _days_since(NULL)→999d and archives it on the FIRST run.
725
+ cursor = conn.execute(
726
+ "INSERT INTO learned_patterns "
727
+ "(pattern, memory_type, domain, source, confidence, concepts, provenance, "
728
+ "distill_fingerprint, evidence_json, last_validated, last_accessed_at) "
729
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
730
+ (
731
+ pattern,
732
+ memory_type,
733
+ domain,
734
+ source,
735
+ confidence,
736
+ concepts,
737
+ provenance,
738
+ distill_fingerprint,
739
+ evidence_json,
740
+ ),
741
+ )
742
+ pattern_id = cursor.lastrowid
743
+ result = {
744
+ "id": pattern_id,
745
+ "pattern": pattern,
746
+ "confidence": confidence,
747
+ "action": "created",
748
+ }
749
+
750
+ # RAG: embed the pattern for semantic search.
751
+ # Suppressed because embeddings are optional enrichment — the upsert
752
+ # itself must succeed even when rag extras / v5 schema are unavailable.
753
+ _embed_pattern_safe(conn, pattern_id, pattern, concepts)
754
+
755
+ return result
756
+
757
+
758
+ def _embed_pattern_safe(
759
+ conn: sqlite3.Connection,
760
+ pattern_id: int,
761
+ pattern: str,
762
+ concepts: str,
763
+ ) -> None:
764
+ # Fire-and-forget: embeddings are optional enrichment — never fail the upsert.
765
+ try:
766
+ from embeddings import upsert_embedding
767
+ except ImportError as exc:
768
+ logger.debug("Skipping pattern embedding (module unavailable): %s", exc)
769
+ return
770
+ try:
771
+ text_to_embed = " ".join(filter(None, [pattern, concepts]))
772
+ upsert_embedding(conn, "learned_patterns", pattern_id, text_to_embed)
773
+ except sqlite3.OperationalError as exc:
774
+ logger.debug("Skipping pattern embedding (table missing): %s", exc)
775
+ except Exception as exc: # pragma: no cover
776
+ logger.debug("Skipping pattern embedding (unexpected): %s", exc)
777
+
778
+
779
+ # ---------------------------------------------------------------------------
780
+ # Friction lesson mining (the real learning signal)
781
+ # ---------------------------------------------------------------------------
782
+
783
+ # A friction event seen this many times is already worth a rule. Lower than the
784
+ # stat threshold (3) because each failure is individually high-value, and never
785
+ # higher than the caller's floor.
786
+ _FRICTION_MIN_OCCURRENCES = 2
787
+
788
+ # Plain-language corrective hint per friction kind — kept beginner-readable.
789
+ _FRICTION_HINTS: dict[str, str] = {
790
+ "hook_block": "satisfy the blocked rule before retrying the action",
791
+ "schema_mismatch": "match the required output schema exactly before resubmitting",
792
+ "error": "fix the failing precondition before retrying",
793
+ }
794
+
795
+ # Normalisers that turn a volatile failure message into a stable cluster key:
796
+ # absolute paths → basename, TASK ids and long hashes → placeholders.
797
+ _ABS_PATH_RE = re.compile(r"(?:/[^\s'\":,]+)+/([^\s'\":/,]+)")
798
+ _TASKID_RE = re.compile(r"TASK-\d+", re.IGNORECASE)
799
+ _LONGHEX_RE = re.compile(r"\b[0-9a-f]{8,}\b", re.IGNORECASE)
800
+ _NONWORD_RE = re.compile(r"[^a-z0-9<>_.-]+")
801
+
802
+
803
+ def _friction_kind(title: str, narrative: str, memory_type: str) -> str:
804
+ # Most-specific signal first. hook_block is detected by the capture's
805
+ # memory_type or a leading "BLOCKED" — NOT a loose "blocked" substring,
806
+ # which appears in unrelated remediation text (e.g. "--to blocked").
807
+ title_l = (title or "").lower()
808
+ narr_l = (narrative or "").lower()
809
+ if "does not match required schema" in narr_l or ("schema" in narr_l and "property" in narr_l):
810
+ return "schema_mismatch"
811
+ if memory_type == "hook_block" or narr_l.startswith("blocked") or "[blocked]" in title_l:
812
+ return "hook_block"
813
+ return "error"
814
+
815
+
816
+ def _clean_failure_text(text: str) -> str:
817
+ line = (text or "").strip().split("\n", 1)[0]
818
+ line = _ABS_PATH_RE.sub(r"\1", line)
819
+ line = _TASKID_RE.sub("TASK-N", line)
820
+ line = _LONGHEX_RE.sub("<hash>", line)
821
+ return " ".join(line.split())[:200]
822
+
823
+
824
+ def _failure_cluster_key(display: str) -> str:
825
+ norm = re.sub(r"\d+", "N", display.lower())
826
+ words = [w for w in _NONWORD_RE.split(norm) if w]
827
+ return " ".join(words[:8])
828
+
829
+
830
+ # Substrings that mark an `error` observation as a tool-fumble or expected
831
+ # refusal — the agent tripping over its own tooling, never an engineering lesson.
832
+ # See learning-extraction.md § Noise filter.
833
+ _NOISE_FAILURE_MARKERS: tuple[str, ...] = (
834
+ "eisdir",
835
+ "illegal operation on a directory",
836
+ "file does not exist",
837
+ "no such file or directory",
838
+ "refusing to write through symlink",
839
+ "structuredoutput", # workflow-internal schema fumble, not a code lesson
840
+ "validation error for cos_", # agent mis-called an MCP tool schema — fumble
841
+ "validation errors for cos_",
842
+ "exceeds maximum allowed", # oversized Read/tool payload — operational refusal
843
+ "scrape aborted", # external scraping engine refusal — environment, not code
844
+ "scraping engines failed",
845
+ "mcp error -", # raw MCP transport error — infrastructure, not a lesson
846
+ )
847
+
848
+
849
+ def _is_noise_failure(display: str) -> bool:
850
+ low = display.lower()
851
+ return any(marker in low for marker in _NOISE_FAILURE_MARKERS)
852
+
853
+
854
+ # Known internal/model jargon → plain language, so a lesson reads for a novice
855
+ # (XAI/PAIR: speak the user's language, not the model's). Applied longest-first.
856
+ _JARGON_TRANSLATIONS: tuple[tuple[str, str], ...] = (
857
+ (
858
+ "predicates_unsatisfied: no evidencebundle for predicates ['coverage_100']",
859
+ "ended a 'fix everything' task without recording proof every case was handled",
860
+ ),
861
+ ("predicates_unsatisfied", "ended the task without the required proof-of-completion"),
862
+ ("no evidencebundle", "no proof-of-completion was recorded"),
863
+ ("task_not_closed", "left a task open"),
864
+ ("does not match required schema", "the output's shape did not match what was required"),
865
+ )
866
+
867
+
868
+ def _humanize_signature(display: str) -> str:
869
+ out = display
870
+ low = out.lower()
871
+ for jargon, plain in _JARGON_TRANSLATIONS:
872
+ idx = low.find(jargon)
873
+ if idx != -1:
874
+ out = out[:idx] + plain + out[idx + len(jargon) :]
875
+ low = out.lower()
876
+ return out
877
+
878
+
879
+ def _normalize_full(text: str) -> str:
880
+ # Lowercase, digits->N, non-word->space — the same rules as the friction
881
+ # cluster key but KEEPING every word, so a failure key stays a contiguous
882
+ # substring of a lesson's full display text.
883
+ return " ".join(_NONWORD_RE.split(re.sub(r"\d+", "N", (text or "").lower())))
884
+
885
+
886
+ def _load_surfaced_suggestions(path: Path) -> list[tuple[int, str]]:
887
+ out: list[tuple[int, str]] = []
888
+ for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
889
+ if "\t" not in line:
890
+ continue
891
+ pid_s, text = line.split("\t", 1)
892
+ try:
893
+ out.append((int(pid_s), text))
894
+ except ValueError:
895
+ continue
896
+ return out
897
+
898
+
899
+ def _distill_fingerprint_safe(kind: str, cluster_key: str) -> str:
900
+ try:
901
+ import distill
902
+
903
+ return distill.cluster_fingerprint(kind, cluster_key)
904
+ except Exception as exc:
905
+ logger.debug("fingerprint skipped: %s", exc)
906
+ return ""
907
+
908
+
909
+ def pattern_tier(confidence: float, times_validated: int) -> str:
910
+ """Confidence tier for a learned pattern — the single mapping used by the UI
911
+ and digest. SSOT: learning-extraction.md § Confidence tier mapping.
912
+
913
+ Trusted = confirmed repeatedly · Fading = decaying, up for re-validation ·
914
+ Forming = seen, not yet confirmed.
915
+ """
916
+ conf = confidence or 0.0
917
+ tv = times_validated or 0
918
+ if conf >= 0.7 and tv >= 3:
919
+ return "Trusted"
920
+ if 0.2 <= conf <= 0.4 and tv >= 1:
921
+ return "Fading"
922
+ return "Forming"
923
+
924
+
925
+ def _distill_safe(**kwargs) -> dict | None:
926
+ # Fire-and-forget: the distiller is optional enrichment — any failure
927
+ # (module missing, dispatcher down, headless without auth) falls back to
928
+ # the template producer.
929
+ try:
930
+ import distill
931
+
932
+ if not distill.enabled():
933
+ return None
934
+ return distill.distill_cluster(**kwargs)
935
+ except Exception as exc:
936
+ logger.debug("distillation skipped: %s", exc)
937
+ return None
938
+
939
+
940
+ def _adopt_legacy_template(conn: sqlite3.Connection, template_text: str, new_id: int) -> None:
941
+ # A distilled lesson supersedes the template row for the same cluster:
942
+ # fold the old counters in, then invalidate (archive), never delete.
943
+ identity = _pattern_identity(template_text)
944
+ for cand in conn.execute(
945
+ "SELECT id, pattern, times_seen, times_validated, access_count FROM learned_patterns "
946
+ "WHERE domain IS NULL AND COALESCE(promoted_to, '') != 'archived'",
947
+ ):
948
+ if cand["id"] == new_id or _pattern_identity(cand["pattern"]) != identity:
949
+ continue
950
+ conn.execute(
951
+ "UPDATE learned_patterns SET times_seen = COALESCE(times_seen, 0) + ?, "
952
+ "times_validated = times_validated + ?, access_count = access_count + ? "
953
+ "WHERE id = ?",
954
+ (
955
+ cand["times_seen"] or 0,
956
+ cand["times_validated"] or 0,
957
+ cand["access_count"] or 0,
958
+ new_id,
959
+ ),
960
+ )
961
+ conn.execute(
962
+ "UPDATE learned_patterns SET promoted_to = 'archived', "
963
+ "archived_at = CURRENT_TIMESTAMP WHERE id = ?",
964
+ (cand["id"],),
965
+ )
966
+ break
967
+
968
+
969
+ def _mint_friction_lesson(
970
+ conn: sqlite3.Connection,
971
+ *,
972
+ kind: str,
973
+ cluster_key: str,
974
+ count: int,
975
+ template_text: str,
976
+ concepts: str,
977
+ hook: str = "",
978
+ rule: str = "",
979
+ samples: list[str] | None = None,
980
+ distill_state: dict | None = None,
981
+ ) -> dict:
982
+ # One write path for both friction miners: refresh an already-distilled
983
+ # cluster for free, distill a new one under the per-run budget, or fall
984
+ # back to the deterministic template.
985
+ fingerprint = None
986
+ try:
987
+ import distill
988
+
989
+ fingerprint = distill.cluster_fingerprint(kind, cluster_key)
990
+ except Exception as exc:
991
+ logger.debug("fingerprint unavailable: %s", exc)
992
+
993
+ if fingerprint:
994
+ try:
995
+ row = conn.execute(
996
+ "SELECT id, pattern FROM learned_patterns WHERE distill_fingerprint = ?",
997
+ (fingerprint,),
998
+ ).fetchone()
999
+ except sqlite3.OperationalError:
1000
+ row = None
1001
+ if row:
1002
+ return _upsert_pattern(
1003
+ conn,
1004
+ pattern=row["pattern"],
1005
+ memory_type="lesson",
1006
+ domain=None,
1007
+ source="friction",
1008
+ confidence=0.5,
1009
+ concepts=concepts,
1010
+ provenance="llm_distilled",
1011
+ distill_fingerprint=fingerprint,
1012
+ )
1013
+
1014
+ budget_left = bool(distill_state) and distill_state.get("remaining", 0) > 0
1015
+ if fingerprint and budget_left:
1016
+ distill_state["remaining"] -= 1
1017
+ distilled = _distill_safe(
1018
+ kind=kind, signature=cluster_key, count=count, hook=hook, rule=rule, samples=samples
1019
+ )
1020
+ if distilled:
1021
+ import distill
1022
+
1023
+ result = _upsert_pattern(
1024
+ conn,
1025
+ pattern=distill.lesson_text(distilled),
1026
+ memory_type="lesson",
1027
+ domain=None,
1028
+ source="friction",
1029
+ confidence=0.5,
1030
+ concepts=concepts,
1031
+ provenance="llm_distilled",
1032
+ distill_fingerprint=fingerprint,
1033
+ evidence_json=json.dumps(
1034
+ {"samples": distill.sanitize_samples(samples or []), "recurrences": count}
1035
+ ),
1036
+ )
1037
+ if result.get("id"):
1038
+ _adopt_legacy_template(conn, template_text, result["id"])
1039
+ return result
1040
+
1041
+ return _upsert_pattern(
1042
+ conn,
1043
+ pattern=template_text,
1044
+ memory_type="lesson",
1045
+ domain=None,
1046
+ source="friction",
1047
+ confidence=min(0.85, 0.4 + count / 10.0),
1048
+ concepts=concepts,
1049
+ )
1050
+
1051
+
1052
+ def _mine_friction_lessons(
1053
+ conn: sqlite3.Connection,
1054
+ *,
1055
+ min_occurrences: int = 3,
1056
+ distill_state: dict | None = None,
1057
+ ) -> list[dict]:
1058
+ # Fire-and-forget: a missing observations table/column never breaks extraction.
1059
+ floor = max(1, min(min_occurrences, _FRICTION_MIN_OCCURRENCES))
1060
+ try:
1061
+ rows = conn.execute(
1062
+ "SELECT title, narrative, memory_type, files_modified FROM observations "
1063
+ "WHERE memory_type IN ('hook_block', 'error') AND COALESCE(narrative, '') != '' "
1064
+ " AND created_at >= datetime('now', '-' || ? || ' days')",
1065
+ (_LESSON_WINDOW_DAYS,),
1066
+ ).fetchall()
1067
+ except sqlite3.OperationalError as exc:
1068
+ logger.debug("friction mining skipped: %s", exc)
1069
+ return []
1070
+
1071
+ clusters: dict[str, dict] = {}
1072
+ for row in rows:
1073
+ d = dict(row)
1074
+ # Screen title AND narrative: a StructuredOutput fumble carries the marker
1075
+ # in the title while the narrative reads like a generic schema error.
1076
+ if _is_noise_failure(f"{d['title'] or ''} {d['narrative'] or ''}"):
1077
+ continue # tool-fumble / expected refusal — never a lesson
1078
+ display = _clean_failure_text(d["narrative"] or d["title"] or "")
1079
+ key = _failure_cluster_key(display)
1080
+ if not key:
1081
+ continue
1082
+ cluster = clusters.setdefault(
1083
+ key,
1084
+ {
1085
+ "count": 0,
1086
+ # store the humanized signature so the minted lesson reads plainly
1087
+ "display": _humanize_signature(display),
1088
+ "kind": _friction_kind(d["title"], d["narrative"], d["memory_type"]),
1089
+ "files": set(), # source-file basenames → concepts, for JIT recall
1090
+ "samples": [],
1091
+ },
1092
+ )
1093
+ cluster["count"] += 1
1094
+ if len(cluster["samples"]) < 3:
1095
+ cluster["samples"].append(display)
1096
+ fm = d.get("files_modified") or ""
1097
+ if fm:
1098
+ cluster["files"].add(fm.rsplit("/", 1)[-1])
1099
+
1100
+ lessons: list[dict] = []
1101
+ for key, cluster in clusters.items():
1102
+ if cluster["count"] < floor:
1103
+ continue
1104
+ hint = _FRICTION_HINTS.get(cluster["kind"], _FRICTION_HINTS["error"])
1105
+ # Count rendered as "(N occurrences)" so _pattern_identity strips it and
1106
+ # a re-mined cluster UPDATES its row instead of inserting a snapshot.
1107
+ pattern_text = (
1108
+ f"Recurring {cluster['kind'].replace('_', ' ')} "
1109
+ f"({cluster['count']} occurrences): {cluster['display']} → {hint}"
1110
+ )
1111
+ lessons.append(
1112
+ _mint_friction_lesson(
1113
+ conn,
1114
+ kind=cluster["kind"],
1115
+ cluster_key=key,
1116
+ count=cluster["count"],
1117
+ template_text=pattern_text,
1118
+ samples=cluster["samples"],
1119
+ distill_state=distill_state,
1120
+ # file:<basename> tokens key JIT recall on the friction's source
1121
+ # file (not basename-in-humanized-text, which never matched).
1122
+ concepts=json.dumps(
1123
+ ["lesson", cluster["kind"], "friction"]
1124
+ + [f"file:{b}" for b in sorted(cluster["files"])[:5] if b]
1125
+ ),
1126
+ )
1127
+ )
1128
+ return lessons
1129
+
1130
+
1131
+ # A hook-log block line: "[<ts>] [<hook>] [block] … rule=<rule> …".
1132
+ _BLOCK_LINE_RE = re.compile(
1133
+ r"^\[(?P<ts>[^\]]+)\]\s+\[(?P<hook>[^\]]+)\]\s+\[block\]\s*(?P<rest>.*)$"
1134
+ )
1135
+ _BLOCK_RULE_RE = re.compile(r"\brule=(\S+)")
1136
+ # Recency window shared by both friction miners: a failure/block only counts as
1137
+ # a recurring lesson if it recurs within this window. Old/resolved/renamed-rule
1138
+ # failures age out (stop being re-confirmed) and decay instead of persisting.
1139
+ _LESSON_WINDOW_DAYS = 90
1140
+
1141
+
1142
+ def _hook_log_paths(conn: sqlite3.Connection) -> list[Path]:
1143
+ # Most-durable first: block-only log (survives the main log's cap) then the
1144
+ # main hook log. Env overrides win; otherwise derive from the project root.
1145
+ paths: list[Path] = []
1146
+ blk = os.environ.get("COS_HOOK_BLOCK_LOG")
1147
+ main = os.environ.get("COS_HOOK_LOG")
1148
+ if blk:
1149
+ paths.append(Path(blk))
1150
+ if main:
1151
+ paths.append(Path(main))
1152
+ if not paths:
1153
+ root = _derive_project_root(conn)
1154
+ if root:
1155
+ paths.append(root / ".coding-os" / ".hook-blocks.log")
1156
+ paths.append(root / ".coding-os" / ".hooks.log")
1157
+ return paths
1158
+
1159
+
1160
+ def _mine_hook_block_lessons(
1161
+ conn: sqlite3.Connection,
1162
+ *,
1163
+ min_occurrences: int = 3,
1164
+ distill_state: dict | None = None,
1165
+ ) -> list[dict]:
1166
+ # Hook BLOCKs never reach the observations table on Claude (no PostToolUseFailure)
1167
+ # but are in the append-only hook log — mine them there. Fire-and-forget.
1168
+ floor = max(1, min(min_occurrences, _FRICTION_MIN_OCCURRENCES))
1169
+ # Single source, not a merge: every block is mirrored to both logs, so the
1170
+ # block-only log is a strict superset of the main log's surviving blocks.
1171
+ # Read the first existing, non-empty candidate (block log preferred) — this
1172
+ # avoids double-counting a mirrored block while preserving genuine repeats.
1173
+ log_path = None
1174
+ for lp in _hook_log_paths(conn):
1175
+ try:
1176
+ if lp.exists() and lp.stat().st_size > 0:
1177
+ log_path = lp
1178
+ break
1179
+ except OSError:
1180
+ continue
1181
+ if log_path is None:
1182
+ return []
1183
+ try:
1184
+ lines = log_path.read_text(encoding="utf-8", errors="replace").splitlines()
1185
+ except OSError as exc:
1186
+ logger.debug("hook-block mining skipped (read %s): %s", log_path, exc)
1187
+ return []
1188
+
1189
+ cutoff = datetime.now(timezone.utc) - timedelta(days=_LESSON_WINDOW_DAYS)
1190
+ clusters: dict[str, dict] = {}
1191
+ for line in lines:
1192
+ match = _BLOCK_LINE_RE.match(line)
1193
+ if not match:
1194
+ continue
1195
+ try:
1196
+ ts = datetime.fromisoformat(match.group("ts").replace("Z", "+00:00"))
1197
+ if ts.tzinfo is None:
1198
+ ts = ts.replace(tzinfo=timezone.utc)
1199
+ if ts < cutoff:
1200
+ continue
1201
+ except ValueError:
1202
+ continue # unparseable timestamp — skip, don't guess
1203
+ hook = match.group("hook").strip()
1204
+ rest = match.group("rest") or ""
1205
+ rule_match = _BLOCK_RULE_RE.search(rest)
1206
+ rule = rule_match.group(1) if rule_match else ""
1207
+ key = f"{hook}:{rule}"
1208
+ cluster = clusters.setdefault(key, {"count": 0, "hook": hook, "rule": rule, "samples": []})
1209
+ cluster["count"] += 1
1210
+ if rest and len(cluster["samples"]) < 3:
1211
+ cluster["samples"].append(rest)
1212
+
1213
+ lessons: list[dict] = []
1214
+ for key, cluster in clusters.items():
1215
+ if cluster["count"] < floor:
1216
+ continue
1217
+ subject = f"{cluster['hook']} — {cluster['rule']}" if cluster["rule"] else cluster["hook"]
1218
+ pattern_text = (
1219
+ f"Recurring block ({cluster['count']} occurrences): {subject} "
1220
+ f"→ satisfy the blocked rule before retrying the action"
1221
+ )
1222
+ lessons.append(
1223
+ _mint_friction_lesson(
1224
+ conn,
1225
+ kind="hook_block",
1226
+ cluster_key=key,
1227
+ count=cluster["count"],
1228
+ template_text=pattern_text,
1229
+ hook=cluster["hook"],
1230
+ rule=cluster["rule"],
1231
+ samples=cluster["samples"],
1232
+ distill_state=distill_state,
1233
+ concepts=json.dumps(["lesson", "hook_block", cluster["hook"]]),
1234
+ )
1235
+ )
1236
+ return lessons
1237
+
1238
+
1239
+ # ---------------------------------------------------------------------------
1240
+ # Commit-history lesson mining (the real engineering-lesson signal)
1241
+ # ---------------------------------------------------------------------------
1242
+
1243
+ # A Conventional-Commit subject whose type means "something was wrong → fixed":
1244
+ # fix:/revert: (optional scope, optional !). The subject IS a recorded lesson.
1245
+ _FIX_COMMIT_RE = re.compile(
1246
+ r"^(?P<type>fix|revert)(?:\([^)]*\))?!?:\s*(?P<subject>.+)$", re.IGNORECASE
1247
+ )
1248
+
1249
+ # A one-off `fix:` subject is terse shorthand with no reusable rule — noise.
1250
+ # Only a fix that RECURS this many times is a systemic-gap signal. Reverts are
1251
+ # minted at any count (a revert is itself a recorded mistake). See §5 of the doc.
1252
+ _COMMIT_FIX_MIN_RECURRENCE = 3
1253
+
1254
+
1255
+ def _commit_subject_key(subject: str) -> str:
1256
+ s = _TASKID_RE.sub("TASK-N", subject)
1257
+ s = _LONGHEX_RE.sub("<hash>", s)
1258
+ s = re.sub(r"\d+", "N", s.lower())
1259
+ words = [w for w in _NONWORD_RE.split(s) if w]
1260
+ return " ".join(words[:8])
1261
+
1262
+
1263
+ def _mine_commit_lessons(conn: sqlite3.Connection, *, min_occurrences: int = 3) -> list[dict]:
1264
+ # A fix:/revert: commit IS a recorded "something was wrong → correction".
1265
+ # Read-only git log, bounded, no-op outside a work-tree.
1266
+ # Contract: docs/engineering/learning-extraction.md §5.
1267
+ import subprocess
1268
+
1269
+ root = _derive_project_root(conn)
1270
+ if root is None:
1271
+ return []
1272
+ try:
1273
+ proc = subprocess.run(
1274
+ [
1275
+ "git",
1276
+ "-C",
1277
+ str(root),
1278
+ "log",
1279
+ f"--since={_LESSON_WINDOW_DAYS} days ago",
1280
+ "--max-count=2000",
1281
+ "--no-merges",
1282
+ "--pretty=format:%s",
1283
+ ],
1284
+ capture_output=True,
1285
+ text=True,
1286
+ timeout=10,
1287
+ check=False,
1288
+ )
1289
+ except (OSError, subprocess.SubprocessError) as exc:
1290
+ logger.debug("commit mining skipped: %s", exc)
1291
+ return []
1292
+ if proc.returncode != 0:
1293
+ return []
1294
+
1295
+ clusters: dict[str, dict] = {}
1296
+ for line in proc.stdout.splitlines():
1297
+ match = _FIX_COMMIT_RE.match(line.strip())
1298
+ if not match:
1299
+ continue
1300
+ subject = match.group("subject").strip()
1301
+ key = _commit_subject_key(subject)
1302
+ if not key:
1303
+ continue
1304
+ cluster = clusters.setdefault(key, {"count": 0, "subject": subject, "revert": False})
1305
+ cluster["count"] += 1
1306
+ if match.group("type").lower() == "revert":
1307
+ cluster["revert"] = True
1308
+
1309
+ lessons: list[dict] = []
1310
+ for cluster in clusters.values():
1311
+ is_revert = cluster["revert"]
1312
+ subject = _clean_failure_text(cluster["subject"])
1313
+ if is_revert:
1314
+ # A revert is a recorded "we shipped this and undid it" — real signal.
1315
+ pattern_text = (
1316
+ f"Reverted before: {subject} → reconsider before re-introducing this change."
1317
+ )
1318
+ elif cluster["count"] >= _COMMIT_FIX_MIN_RECURRENCE:
1319
+ # The RECURRENCE is the signal (same thing keeps breaking), not the
1320
+ # subject itself. "(N occurrences)" so _pattern_identity dedups it.
1321
+ pattern_text = (
1322
+ f"Fixed repeatedly ({cluster['count']} occurrences): {subject} "
1323
+ f"→ address the root cause, not the symptom."
1324
+ )
1325
+ else:
1326
+ continue # one-off / 2x fix subject — no reusable lesson, drop it
1327
+ lessons.append(
1328
+ _upsert_pattern(
1329
+ conn,
1330
+ pattern=pattern_text,
1331
+ memory_type="lesson",
1332
+ domain=None,
1333
+ source="commit",
1334
+ confidence=min(0.85, 0.4 + cluster["count"] / 10.0),
1335
+ concepts=json.dumps(["lesson", "commit", "revert" if is_revert else "fix"]),
1336
+ )
1337
+ )
1338
+ return lessons
1339
+
1340
+
1341
+ # ---------------------------------------------------------------------------
1342
+ # cos_learn_suggest
1343
+ # ---------------------------------------------------------------------------
1344
+
1345
+
1346
+ def learn_suggest(
1347
+ conn: sqlite3.Connection,
1348
+ *,
1349
+ domain: str | None = None,
1350
+ complexity: str | None = None,
1351
+ task_type: str | None = None,
1352
+ limit: int = 5,
1353
+ ) -> dict:
1354
+ """Return relevant patterns for the current task context.
1355
+
1356
+ Includes spaced repetition: patterns at 0.2-0.4 confidence that
1357
+ were once validated get priority with "fading" label.
1358
+
1359
+ Args:
1360
+ conn: SQLite connection.
1361
+ domain: Task domain (e.g. "BACKEND").
1362
+ complexity: Cynefin classification.
1363
+ task_type: Type of task (e.g. "feat", "fix").
1364
+ limit: Max suggestions (1-20, default 5).
1365
+
1366
+ Returns:
1367
+ Dict with suggestions list.
1368
+ """
1369
+ limit = max(1, min(20, limit))
1370
+ suggestions: list[dict] = []
1371
+
1372
+ # --- Active patterns (confidence > 0.3) ---
1373
+ # Exclude stats — a success-rate baseline is observability, never a
1374
+ # suggestion to act on. See docs/engineering/learning-extraction.md.
1375
+ conditions = [
1376
+ "confidence >= 0.3",
1377
+ "COALESCE(memory_type, '') != 'stat'",
1378
+ "promoted_to IS NULL",
1379
+ ]
1380
+ params: list = []
1381
+ if domain:
1382
+ conditions.append("(domain = ? OR domain IS NULL)")
1383
+ params.append(domain)
1384
+ where = " AND ".join(conditions)
1385
+
1386
+ # Relevance boost: complexity + task_type used to be accepted
1387
+ # then ignored, so recall was relevance-blind. There is no per-pattern
1388
+ # complexity/task_type column, so we BOOST (never exclude) patterns whose
1389
+ # concepts/pattern text mention the term — a matching pattern outranks an
1390
+ # equally-confident non-match. Boost params bind first (SELECT precedes WHERE).
1391
+ boost_terms: list[str] = []
1392
+ boost_params: list = []
1393
+ for term in (complexity, task_type):
1394
+ if term:
1395
+ boost_terms.append(
1396
+ "(CASE WHEN LOWER(COALESCE(concepts,'')) LIKE ? "
1397
+ "OR LOWER(pattern) LIKE ? THEN 1 ELSE 0 END)"
1398
+ )
1399
+ like = f"%{term.lower()}%"
1400
+ boost_params += [like, like]
1401
+ relevance = " + ".join(boost_terms) if boost_terms else "0"
1402
+
1403
+ active_rows = conn.execute(
1404
+ f"SELECT id, pattern, memory_type, domain, confidence, impact_score, "
1405
+ f"times_validated, times_violated, ({relevance}) AS relevance "
1406
+ f"FROM learned_patterns WHERE {where} "
1407
+ "ORDER BY relevance DESC, confidence DESC, impact_score DESC LIMIT ?",
1408
+ boost_params + params + [limit],
1409
+ ).fetchall()
1410
+
1411
+ for row in active_rows:
1412
+ d = dict(row)
1413
+ suggestions.append(
1414
+ {
1415
+ "id": d["id"],
1416
+ "pattern": d["pattern"],
1417
+ "confidence": d["confidence"],
1418
+ "impact_score": d.get("impact_score", 0.5),
1419
+ "memory_type": d["memory_type"],
1420
+ "reason": "active",
1421
+ }
1422
+ )
1423
+
1424
+ # --- Fading patterns (spaced repetition) ---
1425
+ fading_conditions = [
1426
+ "confidence BETWEEN 0.2 AND 0.4",
1427
+ # Established-ness (occurrence), not validation: after the honest
1428
+ # times_validated reset a "seen" pattern still resurfaces for review.
1429
+ "times_seen >= 1",
1430
+ "COALESCE(memory_type, '') != 'stat'",
1431
+ "promoted_to IS NULL",
1432
+ ]
1433
+ fading_params: list = []
1434
+ if domain:
1435
+ fading_conditions.append("(domain = ? OR domain IS NULL)")
1436
+ fading_params.append(domain)
1437
+ fading_where = " AND ".join(fading_conditions)
1438
+
1439
+ fading_rows = conn.execute(
1440
+ f"SELECT id, pattern, memory_type, domain, confidence, impact_score, "
1441
+ f"times_validated, times_violated "
1442
+ f"FROM learned_patterns WHERE {fading_where} "
1443
+ "ORDER BY confidence ASC LIMIT 3",
1444
+ fading_params,
1445
+ ).fetchall()
1446
+
1447
+ for row in fading_rows:
1448
+ d = dict(row)
1449
+ suggestions.insert(
1450
+ 0,
1451
+ { # fading patterns go first
1452
+ "id": d["id"],
1453
+ "pattern": d["pattern"],
1454
+ "confidence": d["confidence"],
1455
+ "impact_score": d.get("impact_score", 0.5),
1456
+ "memory_type": d["memory_type"],
1457
+ "reason": "fading",
1458
+ },
1459
+ )
1460
+
1461
+ # --- Breakthrough narratives (high-value lessons from past struggles) ---
1462
+ try:
1463
+ bt_conditions = ["oh.is_breakthrough = 1", "oh.narrative_key_insight IS NOT NULL"]
1464
+ bt_params: list = []
1465
+ if domain:
1466
+ bt_conditions.append("t.domain = ?")
1467
+ bt_params.append(domain)
1468
+ bt_where = " AND ".join(bt_conditions)
1469
+
1470
+ bt_rows = conn.execute(
1471
+ f"SELECT oh.task_id, oh.narrative_key_insight, oh.narrative_what_failed, "
1472
+ f"oh.previous_outcome, t.domain "
1473
+ f"FROM outcome_history oh "
1474
+ f"LEFT JOIN task_outcomes t ON oh.task_id = t.task_id "
1475
+ f"WHERE {bt_where} "
1476
+ "ORDER BY oh.created_at DESC LIMIT 3",
1477
+ bt_params,
1478
+ ).fetchall()
1479
+
1480
+ for row in bt_rows:
1481
+ d = dict(row)
1482
+ insight = d["narrative_key_insight"] or ""
1483
+ failed = d.get("narrative_what_failed") or ""
1484
+ label = f"[Breakthrough] {insight}"
1485
+ if failed:
1486
+ label += f" (avoid: {failed[:60]})"
1487
+ suggestions.append(
1488
+ {
1489
+ "id": None,
1490
+ "pattern": label,
1491
+ "confidence": 0.8,
1492
+ "impact_score": 0.9,
1493
+ "memory_type": "breakthrough",
1494
+ "reason": f"breakthrough from {d['task_id']}",
1495
+ }
1496
+ )
1497
+ except Exception:
1498
+ pass # outcome_history may not exist on pre-v4 DBs
1499
+
1500
+ return {"suggestions": suggestions[:limit], "count": min(len(suggestions), limit)}
1501
+
1502
+
1503
+ # ---------------------------------------------------------------------------
1504
+ # cos_learn_validate
1505
+ # ---------------------------------------------------------------------------
1506
+
1507
+
1508
+ def learn_validate(
1509
+ conn: sqlite3.Connection,
1510
+ *,
1511
+ pattern_id: int,
1512
+ was_helpful: bool,
1513
+ ) -> dict:
1514
+ """Record whether a suggested pattern was helpful.
1515
+
1516
+ Applies confidence formulas:
1517
+ - helpful: LTP with diminishing returns + temporal proximity check
1518
+ - not helpful: LTD proportional penalty
1519
+
1520
+ Self-validation throttle:
1521
+ - Every call is logged to `pattern_validations` (INSERT, append-only).
1522
+ - If the same (session_id, pattern_id, was_helpful=True) was already
1523
+ recorded within THROTTLE_WINDOW_SECONDS, the call is marked
1524
+ `was_throttled=1` and confidence is NOT boosted. Violation (negative
1525
+ feedback) is never throttled — agents must always be able to flag
1526
+ bad patterns.
1527
+
1528
+ Args:
1529
+ conn: SQLite connection.
1530
+ pattern_id: ID in learned_patterns table.
1531
+ was_helpful: Whether the pattern was useful.
1532
+
1533
+ Returns:
1534
+ Dict with updated confidence and status.
1535
+ """
1536
+ row = conn.execute(
1537
+ "SELECT id, confidence, times_validated, times_violated, decay_rate, trust_tier "
1538
+ "FROM learned_patterns WHERE id = ?",
1539
+ (pattern_id,),
1540
+ ).fetchone()
1541
+
1542
+ if row is None:
1543
+ return {"error": f"Pattern not found: id={pattern_id}"}
1544
+
1545
+ # guard: locked/core patterns cannot be mutated via this path
1546
+ # even though the trigger would also block it. Return a clean validation
1547
+ # error instead of letting SQLite raise.
1548
+ trust_tier = row["trust_tier"] if "trust_tier" in row.keys() else "volatile"
1549
+ if trust_tier in {"locked", "core"}:
1550
+ return {
1551
+ "error": f"Pattern {pattern_id} is {trust_tier} — immutable via cos_learn_validate",
1552
+ "pattern_id": pattern_id,
1553
+ "trust_tier": trust_tier,
1554
+ }
1555
+
1556
+ # throttle — only applies to positive validations
1557
+ throttled = False
1558
+ session_id = _read_session_id_for_validate()
1559
+ if was_helpful and _has_recent_validation(conn, session_id, pattern_id):
1560
+ throttled = True
1561
+
1562
+ # Always log the attempt (throttled or not) for audit + sycophancy
1563
+ # detection in later phases.
1564
+ _log_validation(
1565
+ conn,
1566
+ session_id=session_id,
1567
+ pattern_id=pattern_id,
1568
+ was_helpful=was_helpful,
1569
+ was_throttled=throttled,
1570
+ )
1571
+
1572
+ if throttled:
1573
+ # Return current state without confidence mutation
1574
+ return {
1575
+ "status": "throttled",
1576
+ "pattern_id": pattern_id,
1577
+ "old_confidence": round(row["confidence"], 4),
1578
+ "new_confidence": round(row["confidence"], 4),
1579
+ "was_helpful": was_helpful,
1580
+ "reason": f"same (session, pattern) validated within {_THROTTLE_WINDOW_SECONDS}s",
1581
+ }
1582
+
1583
+ old_conf = row["confidence"]
1584
+ decay_rate = row["decay_rate"]
1585
+
1586
+ if was_helpful:
1587
+ new_conf = boost_success(old_conf)
1588
+
1589
+ # Temporal proximity check — 2+ validations in 48h
1590
+ recent_count = conn.execute(
1591
+ "SELECT COUNT(*) FROM learned_patterns "
1592
+ "WHERE id = ? AND last_validated >= datetime('now', '-48 hours')",
1593
+ (pattern_id,),
1594
+ ).fetchone()[0]
1595
+
1596
+ if recent_count >= 1: # this will be the 2nd+ in 48h
1597
+ new_conf = min(0.95, new_conf + 0.05)
1598
+ decay_rate = decay_rate * 0.7
1599
+
1600
+ conn.execute(
1601
+ "UPDATE learned_patterns SET "
1602
+ "confidence = ?, "
1603
+ "times_validated = times_validated + 1, "
1604
+ "last_validated = CURRENT_TIMESTAMP, "
1605
+ "decay_rate = ? "
1606
+ "WHERE id = ?",
1607
+ (new_conf, decay_rate, pattern_id),
1608
+ )
1609
+ else:
1610
+ new_conf = penalize_failure(old_conf)
1611
+ conn.execute(
1612
+ "UPDATE learned_patterns SET "
1613
+ "confidence = ?, "
1614
+ "times_violated = times_violated + 1, "
1615
+ "last_validated = CURRENT_TIMESTAMP "
1616
+ "WHERE id = ?",
1617
+ (new_conf, pattern_id),
1618
+ )
1619
+
1620
+ conn.commit()
1621
+ return {
1622
+ "status": "validated" if was_helpful else "penalized",
1623
+ "pattern_id": pattern_id,
1624
+ "old_confidence": round(old_conf, 4),
1625
+ "new_confidence": round(new_conf, 4),
1626
+ "was_helpful": was_helpful,
1627
+ }
1628
+
1629
+
1630
+ def validate_surfaced_lessons(
1631
+ conn: sqlite3.Connection,
1632
+ *,
1633
+ session_id: str,
1634
+ suggestions_path: str | Path,
1635
+ ) -> dict:
1636
+ """Close the learn->apply->confirm loop for one completed task: validate each
1637
+ lesson surfaced during Orient against this session's post-recall friction — a
1638
+ lesson whose failure recurred is penalized (LTD), the rest reinforced (LTP).
1639
+
1640
+ The single primitive BOTH the task-done Bash hook and the MCP completion path
1641
+ call; divergence here was why surfaced patterns never reached the Trusted tier.
1642
+ """
1643
+ sf = Path(suggestions_path)
1644
+ if not session_id or not sf.exists():
1645
+ return {"status": "skipped"}
1646
+ surfaced = _load_surfaced_suggestions(sf)
1647
+ if not surfaced:
1648
+ return {"status": "no_suggestions"}
1649
+
1650
+ # Only failures recorded AT/AFTER the recall (suggestions file mtime) count.
1651
+ recall_at = datetime.fromtimestamp(sf.stat().st_mtime, tz=timezone.utc).strftime(
1652
+ "%Y-%m-%d %H:%M:%S"
1653
+ )
1654
+ rows = conn.execute(
1655
+ "SELECT narrative, title, memory_type FROM observations "
1656
+ "WHERE session_id = ? AND memory_type IN ('hook_block', 'error') "
1657
+ " AND created_at >= ?",
1658
+ (session_id, recall_at),
1659
+ ).fetchall()
1660
+ failure_keys: list[str] = []
1661
+ failure_fingerprints: set[str] = set()
1662
+ for r in rows:
1663
+ d = dict(r)
1664
+ key = _failure_cluster_key(_clean_failure_text(d["narrative"] or d["title"] or ""))
1665
+ if not key:
1666
+ continue
1667
+ failure_keys.append(key)
1668
+ fp = _distill_fingerprint_safe(
1669
+ _friction_kind(d["title"], d["narrative"], d["memory_type"]), key
1670
+ )
1671
+ if fp:
1672
+ failure_fingerprints.add(fp)
1673
+
1674
+ # A distilled lesson no longer contains the raw failure text, so matching its
1675
+ # display text alone would always read helpful=True. Match the stored
1676
+ # fingerprint and evidence samples too.
1677
+ lesson_meta: dict[int, tuple[str, str]] = {}
1678
+ try:
1679
+ placeholders = ",".join("?" * len(surfaced))
1680
+ for row in conn.execute(
1681
+ "SELECT id, distill_fingerprint, evidence_json FROM learned_patterns "
1682
+ f"WHERE id IN ({placeholders})",
1683
+ [pid for pid, _ in surfaced],
1684
+ ):
1685
+ d = dict(row)
1686
+ lesson_meta[d["id"]] = (
1687
+ d.get("distill_fingerprint") or "",
1688
+ _normalize_full(d.get("evidence_json") or ""),
1689
+ )
1690
+ except sqlite3.Error:
1691
+ lesson_meta = {}
1692
+
1693
+ helpful = unhelpful = 0
1694
+ for pid, text in surfaced:
1695
+ lesson_norm = _normalize_full(text)
1696
+ fingerprint, evidence_norm = lesson_meta.get(pid, ("", ""))
1697
+ recurred = (fingerprint and fingerprint in failure_fingerprints) or any(
1698
+ key in lesson_norm or (evidence_norm and key in evidence_norm) for key in failure_keys
1699
+ )
1700
+ learn_validate(conn, pattern_id=pid, was_helpful=not recurred)
1701
+ if recurred:
1702
+ unhelpful += 1
1703
+ else:
1704
+ helpful += 1
1705
+ return {
1706
+ "status": "ok",
1707
+ "surfaced": len(surfaced),
1708
+ "helpful": helpful,
1709
+ "unhelpful": unhelpful,
1710
+ }
1711
+
1712
+
1713
+ # ---------------------------------------------------------------------------
1714
+ # Breakthrough narrative capture
1715
+ # ---------------------------------------------------------------------------
1716
+
1717
+
1718
+ _GENERIC_INSIGHT_RE = re.compile(
1719
+ r"\b(be careful|be more careful|double[- ]check|pay attention|take care|"
1720
+ r"more thorough|review carefully|test more|don'?t forget)\b",
1721
+ re.IGNORECASE,
1722
+ )
1723
+
1724
+
1725
+ def _is_low_quality_insight(text: str) -> bool:
1726
+ # Reject ultra-terse / generic "be careful" slop with no transferable rule;
1727
+ # specific-but-short insights like "Money must use Decimal" still pass.
1728
+ t = (text or "").strip()
1729
+ if len(t) < 8:
1730
+ return True
1731
+ return bool(_GENERIC_INSIGHT_RE.search(t))
1732
+
1733
+
1734
+ def learn_narrative(
1735
+ conn: sqlite3.Connection,
1736
+ *,
1737
+ task_id: str,
1738
+ what_failed: str = "",
1739
+ what_worked: str = "",
1740
+ key_insight: str = "",
1741
+ ) -> dict:
1742
+ """Record a breakthrough narrative and create a high-impact learned pattern.
1743
+
1744
+ Called by the agent after a rework→success breakthrough. Updates the
1745
+ outcome_history narrative fields and creates a learned_pattern with
1746
+ memory_type='error' and high confidence.
1747
+
1748
+ Args:
1749
+ conn: SQLite connection.
1750
+ task_id: Task identifier (e.g. "TASK-100").
1751
+ what_failed: Approaches that didn't work.
1752
+ what_worked: The solution that resolved the issue.
1753
+ key_insight: Reusable lesson learned.
1754
+
1755
+ Returns:
1756
+ Dict with status, history_id, pattern_id.
1757
+ """
1758
+ if not task_id:
1759
+ return {"error": "task_id is required"}
1760
+ if not key_insight:
1761
+ return {"error": "key_insight is required — what did you learn?"}
1762
+
1763
+ # sanitize all narrative fields before they enter memory.
1764
+ # Reject on injection patterns; truncate over-length text.
1765
+ # Single-pass: compute cleaned values once so audit log records each
1766
+ # truncation/reject exactly once.
1767
+ from sanitizer import sanitize_write
1768
+
1769
+ _sanitized: dict[str, str] = {}
1770
+ for _field, _value in (
1771
+ ("key_insight", key_insight),
1772
+ ("what_failed", what_failed),
1773
+ ("what_worked", what_worked),
1774
+ ):
1775
+ _sr = sanitize_write(
1776
+ _field,
1777
+ _value,
1778
+ actor="learn_narrative",
1779
+ source_table="outcome_history",
1780
+ conn=conn,
1781
+ )
1782
+ if not _sr.ok:
1783
+ return {"error": f"rejected {_field}: {_sr.reason}"}
1784
+ _sanitized[_field] = _sr.cleaned or ""
1785
+
1786
+ key_insight = _sanitized["key_insight"]
1787
+ what_failed = _sanitized["what_failed"]
1788
+ what_worked = _sanitized["what_worked"]
1789
+
1790
+ # Quality bar: a narrative is only worth storing if the insight is specific.
1791
+ # Blocks "be careful"-class slop the nudge could otherwise elicit.
1792
+ if _is_low_quality_insight(key_insight):
1793
+ return {
1794
+ "error": "key_insight too generic — state the specific situation, why the "
1795
+ "naive approach failed, and the rule to apply (not 'be careful')."
1796
+ }
1797
+
1798
+ # Find the most recent breakthrough for this task
1799
+ row = conn.execute(
1800
+ "SELECT id, outcome, previous_outcome FROM outcome_history "
1801
+ "WHERE task_id = ? AND is_breakthrough = 1 "
1802
+ "ORDER BY created_at DESC LIMIT 1",
1803
+ (task_id,),
1804
+ ).fetchone()
1805
+
1806
+ if row is None:
1807
+ # No breakthrough found — create a general narrative entry anyway
1808
+ cursor = conn.execute(
1809
+ "INSERT INTO outcome_history "
1810
+ "(task_id, outcome, previous_outcome, is_breakthrough, "
1811
+ "narrative_what_failed, narrative_what_worked, narrative_key_insight, triggered_by) "
1812
+ "VALUES (?, 'success', NULL, 0, ?, ?, ?, 'learn_narrative')",
1813
+ (task_id, what_failed, what_worked, key_insight),
1814
+ )
1815
+ history_id = cursor.lastrowid
1816
+ else:
1817
+ history_id = row["id"]
1818
+ conn.execute(
1819
+ "UPDATE outcome_history SET "
1820
+ "narrative_what_failed = ?, narrative_what_worked = ?, narrative_key_insight = ? "
1821
+ "WHERE id = ?",
1822
+ (what_failed, what_worked, key_insight, history_id),
1823
+ )
1824
+
1825
+ # Get task domain for the pattern; a still-open task has no outcome row
1826
+ # yet, so fall back to the board's tasks table before giving up.
1827
+ task_row = conn.execute(
1828
+ "SELECT domain, complexity FROM task_outcomes WHERE task_id = ?", (task_id,)
1829
+ ).fetchone()
1830
+ domain = task_row["domain"] if task_row else None
1831
+ if not domain:
1832
+ try:
1833
+ board_row = conn.execute(
1834
+ "SELECT domain FROM tasks WHERE task_id = ?", (task_id,)
1835
+ ).fetchone()
1836
+ domain = board_row[0] if board_row and board_row[0] else None
1837
+ except sqlite3.Error as exc:
1838
+ logger.debug("narrative domain fallback lookup failed: %s", exc)
1839
+
1840
+ # Build concepts from narrative text
1841
+ words = set()
1842
+ for text in (what_failed, what_worked, key_insight):
1843
+ words.update(w.lower() for w in text.split() if len(w) > 3)
1844
+ # Keep only meaningful concept words (no stop words)
1845
+ stop = {"that", "this", "with", "from", "have", "been", "were", "will", "didn't", "wasn't"}
1846
+ concept_list = sorted(words - stop)[:7]
1847
+ if domain:
1848
+ concept_list.insert(0, domain.lower())
1849
+
1850
+ # Create a high-impact learned pattern
1851
+ pattern_text = f"[Breakthrough] {key_insight}"
1852
+ if what_failed:
1853
+ pattern_text += f" (failed: {what_failed[:80]})"
1854
+
1855
+ # evidence-based auto-promote.
1856
+ # Previously this inserted with confidence=0.7 / impact=0.85 /
1857
+ # no provenance, letting the agent self-certify a "breakthrough"
1858
+ # at high trust after a single call (audit finding A7). Now the
1859
+ # row is explicitly volatile/agent_self at moderate confidence;
1860
+ # promotion to `validated` requires external evidence (outcome
1861
+ # history or explicit `cos_promote`), handled elsewhere.
1862
+ # Stamp last_validated/last_accessed_at so a fresh breakthrough has age 0.
1863
+ # Otherwise run_decay reads _days_since(NULL)->999d and archives it on the
1864
+ # FIRST run (the same fix learn_extract's _upsert_pattern already carries).
1865
+ cursor = conn.execute(
1866
+ "INSERT INTO learned_patterns "
1867
+ "(pattern, memory_type, domain, source, confidence, impact_score, "
1868
+ "concepts, trust_tier, provenance, last_validated, last_accessed_at) "
1869
+ "VALUES (?, 'error', ?, 'breakthrough', 0.3, 0.5, ?, "
1870
+ "'volatile', 'agent_self', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
1871
+ (pattern_text, domain, json.dumps(concept_list)),
1872
+ )
1873
+ pattern_id = cursor.lastrowid
1874
+
1875
+ conn.commit()
1876
+
1877
+ # RAG: embed both the breakthrough narrative (outcome_history)
1878
+ # and the high-impact learned pattern. Errors are intentionally suppressed
1879
+ # because embeddings are an optional enrichment — never fail the narrative
1880
+ # recording itself if rag extras are not installed or v5 not yet applied.
1881
+ _embed_narrative_and_pattern(
1882
+ conn=conn,
1883
+ history_id=history_id,
1884
+ pattern_id=pattern_id,
1885
+ pattern_text=pattern_text,
1886
+ concept_list=concept_list,
1887
+ key_insight=key_insight,
1888
+ what_failed=what_failed,
1889
+ what_worked=what_worked,
1890
+ )
1891
+
1892
+ # Filing-back: write a human-readable markdown file to docs/insights/.
1893
+ # Fire-and-forget — filing failure must never break narrative recording.
1894
+ filed_path = _file_back_narrative_safe(
1895
+ conn=conn,
1896
+ task_id=task_id,
1897
+ domain=domain,
1898
+ key_insight=key_insight,
1899
+ what_failed=what_failed,
1900
+ what_worked=what_worked,
1901
+ history_id=history_id,
1902
+ pattern_id=pattern_id,
1903
+ )
1904
+
1905
+ return {
1906
+ "status": "narrative_recorded",
1907
+ "history_id": history_id,
1908
+ "pattern_id": pattern_id,
1909
+ "task_id": task_id,
1910
+ "domain": domain,
1911
+ "filed_path": str(filed_path) if filed_path else None,
1912
+ }
1913
+
1914
+
1915
+ def _embed_narrative_and_pattern(
1916
+ *,
1917
+ conn: sqlite3.Connection,
1918
+ history_id: int,
1919
+ pattern_id: int,
1920
+ pattern_text: str,
1921
+ concept_list: list,
1922
+ key_insight: str,
1923
+ what_failed: str,
1924
+ what_worked: str,
1925
+ ) -> None:
1926
+ # Fire-and-forget: embeddings are optional enrichment — never fail the
1927
+ # narrative recording (missing module/table/model load all swallowed).
1928
+ try:
1929
+ from embeddings import upsert_embedding
1930
+ except ImportError as exc:
1931
+ logger.debug("Skipping embedding (module unavailable): %s", exc)
1932
+ return
1933
+
1934
+ try:
1935
+ narrative_text = " ".join(filter(None, [key_insight, what_failed, what_worked]))
1936
+ upsert_embedding(conn, "outcome_history", history_id, narrative_text)
1937
+ pattern_concepts_str = " ".join(concept_list)
1938
+ upsert_embedding(
1939
+ conn,
1940
+ "learned_patterns",
1941
+ pattern_id,
1942
+ f"{pattern_text} {pattern_concepts_str}".strip(),
1943
+ )
1944
+ except sqlite3.OperationalError as exc:
1945
+ logger.debug("Skipping embedding (table missing — pre-v5 DB): %s", exc)
1946
+ except Exception as exc: # pragma: no cover - defensive against model load errors
1947
+ logger.debug("Skipping embedding (unexpected): %s", exc)
1948
+
1949
+
1950
+ # ---------------------------------------------------------------------------
1951
+ # Breakthrough narrative filing-back (human-readable markdown artifact)
1952
+ # ---------------------------------------------------------------------------
1953
+
1954
+ _SLUG_RE = re.compile(r"[^a-z0-9]+")
1955
+
1956
+
1957
+ def _slugify(text: str, max_len: int = 50) -> str:
1958
+ slug = _SLUG_RE.sub("-", text.lower()).strip("-")
1959
+ if not slug:
1960
+ return "untitled"
1961
+ return slug[:max_len].rstrip("-") or "untitled"
1962
+
1963
+
1964
+ def _derive_project_root(conn: sqlite3.Connection) -> Path | None:
1965
+ # Root = parent of the .coding-os/ dir holding the DB. None for in-memory
1966
+ # DBs or any DB outside the expected <root>/.coding-os/coding-os.db layout.
1967
+ rows = conn.execute("PRAGMA database_list").fetchall()
1968
+ for row in rows:
1969
+ db_path_str = row[2] if len(row) > 2 else None
1970
+ if not db_path_str:
1971
+ continue
1972
+ if db_path_str in ("", ":memory:"):
1973
+ continue
1974
+ db_path = Path(db_path_str).resolve()
1975
+ if db_path.parent.name == ".coding-os":
1976
+ return db_path.parent.parent
1977
+ return None
1978
+
1979
+
1980
+ def _format_narrative_markdown(
1981
+ *,
1982
+ task_id: str,
1983
+ domain: str | None,
1984
+ key_insight: str,
1985
+ what_failed: str,
1986
+ what_worked: str,
1987
+ history_id: int,
1988
+ pattern_id: int,
1989
+ task_file_name: str | None = None,
1990
+ ) -> str:
1991
+ date_iso = datetime.now(timezone.utc).strftime("%Y-%m-%d")
1992
+ # The docs-lint hard gate requires `domain:[A-Z_]+` in the header; XXX is
1993
+ # the canonical unknown-placeholder in its enum ("n/a" fails the regex).
1994
+ # The body's **Domain:** line stays human-readable.
1995
+ domain_header = (domain or "").strip().upper().replace("-", "_")
1996
+ if not re.fullmatch(r"[A-Z_]+", domain_header):
1997
+ domain_header = "XXX"
1998
+ domain_line = domain or "n/a"
1999
+ failed_block = what_failed.strip() or "_(not recorded)_"
2000
+ worked_block = what_worked.strip() or "_(not recorded)_"
2001
+ # Task files are slugged (TASK-NNN-<slug>.md) — a guessed TASK-NNN.md link
2002
+ # is always dead and trips the docs-lint hard gate; plain text when unknown.
2003
+ source_line = (
2004
+ f"**Source task:** [{task_id}](../tasks/{task_file_name})\n\n"
2005
+ if task_file_name
2006
+ else f"**Source task:** {task_id}\n\n"
2007
+ )
2008
+ return (
2009
+ f"<!-- domain:{domain_header} | layer:reference | ssot:false | "
2010
+ f"source:outcome_history#{history_id} | updated:{date_iso} -->\n"
2011
+ f"# {task_id}: {key_insight}\n\n"
2012
+ f"**Date:** {date_iso} \n"
2013
+ f"**Domain:** {domain_line} \n"
2014
+ f"{source_line}"
2015
+ f"## Key Insight\n\n{key_insight}\n\n"
2016
+ f"## What Failed\n\n{failed_block}\n\n"
2017
+ f"## What Worked\n\n{worked_block}\n\n"
2018
+ f"## Links\n\n"
2019
+ f"- Pattern: `learned_patterns#{pattern_id}` — retrievable via `cos_details`\n"
2020
+ f"- History: `outcome_history#{history_id}`\n"
2021
+ )
2022
+
2023
+
2024
+ def _file_back_narrative_safe(
2025
+ *,
2026
+ conn: sqlite3.Connection,
2027
+ task_id: str,
2028
+ domain: str | None,
2029
+ key_insight: str,
2030
+ what_failed: str,
2031
+ what_worked: str,
2032
+ history_id: int,
2033
+ pattern_id: int,
2034
+ ) -> Path | None:
2035
+ # Fire-and-forget write to <root>/docs/insights/; returns None (skipped)
2036
+ # for in-memory DBs or when <root>/docs/ is absent. Never breaks recording.
2037
+ try:
2038
+ project_root = _derive_project_root(conn)
2039
+ if project_root is None:
2040
+ logger.debug("Skipping narrative filing (project root not derivable)")
2041
+ return None
2042
+ docs_root = project_root / "docs"
2043
+ if not docs_root.exists():
2044
+ logger.debug("Skipping narrative filing (no docs/ at %s)", project_root)
2045
+ return None
2046
+
2047
+ target_dir = docs_root / "insights"
2048
+ target_dir.mkdir(parents=True, exist_ok=True)
2049
+
2050
+ slug = _slugify(f"{task_id}-{key_insight}")
2051
+ target_path = target_dir / f"{slug}.md"
2052
+ task_file_name: str | None = None
2053
+ try:
2054
+ row = conn.execute(
2055
+ "SELECT file_path FROM tasks WHERE task_id = ?", (task_id,)
2056
+ ).fetchone()
2057
+ if row and row[0] and (project_root / str(row[0])).exists():
2058
+ task_file_name = Path(str(row[0])).name
2059
+ except sqlite3.Error as exc:
2060
+ logger.debug("narrative task-file lookup failed: %s", exc)
2061
+ content = _format_narrative_markdown(
2062
+ task_id=task_id,
2063
+ domain=domain,
2064
+ key_insight=key_insight,
2065
+ what_failed=what_failed,
2066
+ what_worked=what_worked,
2067
+ history_id=history_id,
2068
+ pattern_id=pattern_id,
2069
+ task_file_name=task_file_name,
2070
+ )
2071
+ target_path.write_text(content, encoding="utf-8")
2072
+ return target_path
2073
+ except OSError as exc:
2074
+ logger.debug("Skipping narrative filing (OS error): %s", exc)
2075
+ return None
2076
+ except Exception as exc: # pragma: no cover - defensive
2077
+ logger.debug("Skipping narrative filing (unexpected): %s", exc)
2078
+ return None