mednotes-opencode 0.1.0

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 (430) hide show
  1. package/.opencode/agents/med-chat-triager.md +204 -0
  2. package/.opencode/agents/med-flashcard-maker.md +63 -0
  3. package/.opencode/agents/med-knowledge-architect.md +230 -0
  4. package/.opencode/agents/med-link-graph-curator.md +177 -0
  5. package/.opencode/agents/med-publish-guard.md +62 -0
  6. package/.opencode/commands/flashcards.md +25 -0
  7. package/.opencode/commands/mednotes/create.md +25 -0
  8. package/.opencode/commands/mednotes/enrich.md +27 -0
  9. package/.opencode/commands/mednotes/fix-wiki.md +27 -0
  10. package/.opencode/commands/mednotes/history.md +22 -0
  11. package/.opencode/commands/mednotes/link-body.md +25 -0
  12. package/.opencode/commands/mednotes/link-related.md +27 -0
  13. package/.opencode/commands/mednotes/link.md +27 -0
  14. package/.opencode/commands/mednotes/pdf-library.md +27 -0
  15. package/.opencode/commands/mednotes/process-chats.md +23 -0
  16. package/.opencode/commands/mednotes/setup.md +21 -0
  17. package/.opencode/commands/mednotes/status.md +27 -0
  18. package/.opencode/commands/mednotes/telemetry.md +27 -0
  19. package/.opencode/commands/report.md +26 -0
  20. package/.opencode/mednotes/AGENTS.md +57 -0
  21. package/.opencode/mednotes/agents/med-chat-triager.md +197 -0
  22. package/.opencode/mednotes/agents/med-flashcard-maker.md +56 -0
  23. package/.opencode/mednotes/agents/med-knowledge-architect.md +224 -0
  24. package/.opencode/mednotes/agents/med-link-graph-curator.md +171 -0
  25. package/.opencode/mednotes/agents/med-publish-guard.md +55 -0
  26. package/.opencode/mednotes/contracts/.gitkeep +1 -0
  27. package/.opencode/mednotes/contracts/agents.json +116 -0
  28. package/.opencode/mednotes/contracts/opencode-plugin.json +70 -0
  29. package/.opencode/mednotes/docs/agent-prompt-hardening.md +567 -0
  30. package/.opencode/mednotes/docs/agent-role-contracts.md +94 -0
  31. package/.opencode/mednotes/docs/anki-mcp-twenty-rules.md +214 -0
  32. package/.opencode/mednotes/docs/anki-templates/README.md +39 -0
  33. package/.opencode/mednotes/docs/anki-templates/cloze.back.html +23 -0
  34. package/.opencode/mednotes/docs/anki-templates/cloze.front.html +14 -0
  35. package/.opencode/mednotes/docs/anki-templates/qa.back.html +24 -0
  36. package/.opencode/mednotes/docs/anki-templates/qa.front.html +14 -0
  37. package/.opencode/mednotes/docs/anki-templates/style.css +182 -0
  38. package/.opencode/mednotes/docs/atomicity-splitting-policy.md +113 -0
  39. package/.opencode/mednotes/docs/extension-docs.md +40 -0
  40. package/.opencode/mednotes/docs/flashcard-ingestion.md +278 -0
  41. package/.opencode/mednotes/docs/knowledge-architect.md +208 -0
  42. package/.opencode/mednotes/docs/merge-policy.md +110 -0
  43. package/.opencode/mednotes/docs/public-vocabulary.md +104 -0
  44. package/.opencode/mednotes/docs/semantic-linker.md +141 -0
  45. package/.opencode/mednotes/docs/taxonomy-policy.md +90 -0
  46. package/.opencode/mednotes/docs/triage-policy.md +187 -0
  47. package/.opencode/mednotes/docs/vault-version-control.md +758 -0
  48. package/.opencode/mednotes/docs/vocabulary-db-recovery.md +58 -0
  49. package/.opencode/mednotes/docs/workflow-output-contract.md +779 -0
  50. package/.opencode/mednotes/hooks/hooks.json +79 -0
  51. package/.opencode/mednotes/package-lock.json +6361 -0
  52. package/.opencode/mednotes/package.json +15 -0
  53. package/.opencode/mednotes/pyproject.toml +48 -0
  54. package/.opencode/mednotes/scripts/bootstrap_windows_python_uv.cmd +13 -0
  55. package/.opencode/mednotes/scripts/bootstrap_windows_python_uv.ps1 +172 -0
  56. package/.opencode/mednotes/scripts/enrich_notes.py +23 -0
  57. package/.opencode/mednotes/scripts/full_reset_windows_python_uv.cmd +13 -0
  58. package/.opencode/mednotes/scripts/hooks/antigravity_hook_status.mjs +212 -0
  59. package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/antigravity.mjs +169 -0
  60. package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/harness_payload.mjs +103 -0
  61. package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/opencode_plugin.mjs +341 -0
  62. package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/opencode_user_config_sync.mjs +177 -0
  63. package/.opencode/mednotes/scripts/hooks/mednotes_hook/anki_preflight.mjs +214 -0
  64. package/.opencode/mednotes/scripts/hooks/mednotes_hook/cli.mjs +143 -0
  65. package/.opencode/mednotes/scripts/hooks/mednotes_hook/diagnostics.mjs +11 -0
  66. package/.opencode/mednotes/scripts/hooks/mednotes_hook/domain/agent_directive_core.mjs +160 -0
  67. package/.opencode/mednotes/scripts/hooks/mednotes_hook/fsm_directive.mjs +1470 -0
  68. package/.opencode/mednotes/scripts/hooks/mednotes_hook/hook_errors.mjs +120 -0
  69. package/.opencode/mednotes/scripts/hooks/mednotes_hook/retention.mjs +114 -0
  70. package/.opencode/mednotes/scripts/hooks/mednotes_hook/runtime.mjs +174 -0
  71. package/.opencode/mednotes/scripts/hooks/mednotes_hook/telemetry_capture.mjs +511 -0
  72. package/.opencode/mednotes/scripts/hooks/mednotes_hook/vault_guard.mjs +624 -0
  73. package/.opencode/mednotes/scripts/hooks/mednotes_hook.mjs +5 -0
  74. package/.opencode/mednotes/scripts/mednotes/_runtime_paths.py +24 -0
  75. package/.opencode/mednotes/scripts/mednotes/anki_model_validator.py +18 -0
  76. package/.opencode/mednotes/scripts/mednotes/capture_extension_diff.py +1562 -0
  77. package/.opencode/mednotes/scripts/mednotes/feedback_report.py +16 -0
  78. package/.opencode/mednotes/scripts/mednotes/flashcard_index.py +18 -0
  79. package/.opencode/mednotes/scripts/mednotes/flashcard_pipeline.py +18 -0
  80. package/.opencode/mednotes/scripts/mednotes/flashcard_report.py +18 -0
  81. package/.opencode/mednotes/scripts/mednotes/flashcard_sources.py +18 -0
  82. package/.opencode/mednotes/scripts/mednotes/obsidian/README.md +6 -0
  83. package/.opencode/mednotes/scripts/mednotes/obsidian_note_utils.py +20 -0
  84. package/.opencode/mednotes/scripts/mednotes/pdf_library/cli.py +16 -0
  85. package/.opencode/mednotes/scripts/mednotes/project_fsm.py +229 -0
  86. package/.opencode/mednotes/scripts/mednotes/setup_telemetry_email.py +404 -0
  87. package/.opencode/mednotes/scripts/mednotes/sync_anki_twenty_rules.py +18 -0
  88. package/.opencode/mednotes/scripts/mednotes/sync_opencode_user_config.py +36 -0
  89. package/.opencode/mednotes/scripts/mednotes/wiki/cli.py +20 -0
  90. package/.opencode/mednotes/scripts/mednotes/wiki_graph.py +18 -0
  91. package/.opencode/mednotes/scripts/mednotes/wiki_tree.py +134 -0
  92. package/.opencode/mednotes/scripts/reset_windows_python_uv.ps1 +625 -0
  93. package/.opencode/mednotes/scripts/run_python.mjs +109 -0
  94. package/.opencode/mednotes/scripts/vault/vault_commit.ps1 +19 -0
  95. package/.opencode/mednotes/scripts/vault/vault_commit.sh +18 -0
  96. package/.opencode/mednotes/scripts/vault/vault_git.ps1 +19 -0
  97. package/.opencode/mednotes/scripts/vault/vault_git.py +3107 -0
  98. package/.opencode/mednotes/scripts/vault/vault_git.sh +18 -0
  99. package/.opencode/mednotes/scripts/vault/vault_precommit.ps1 +19 -0
  100. package/.opencode/mednotes/scripts/vault/vault_precommit.sh +18 -0
  101. package/.opencode/mednotes/skills/THIRD_PARTY_NOTICES.md +45 -0
  102. package/.opencode/mednotes/skills/create-medical-flashcards/SKILL.md +113 -0
  103. package/.opencode/mednotes/skills/create-medical-note/SKILL.md +90 -0
  104. package/.opencode/mednotes/skills/enrich-medical-note/SKILL.md +120 -0
  105. package/.opencode/mednotes/skills/fix-medical-wiki/SKILL.md +559 -0
  106. package/.opencode/mednotes/skills/link-medical-wiki/SKILL.md +224 -0
  107. package/.opencode/mednotes/skills/obsidian-cli/SKILL.md +118 -0
  108. package/.opencode/mednotes/skills/obsidian-markdown/SKILL.md +207 -0
  109. package/.opencode/mednotes/skills/obsidian-markdown/references/CALLOUTS.md +58 -0
  110. package/.opencode/mednotes/skills/obsidian-markdown/references/EMBEDS.md +63 -0
  111. package/.opencode/mednotes/skills/obsidian-markdown/references/PROPERTIES.md +61 -0
  112. package/.opencode/mednotes/skills/obsidian-ops/SKILL.md +136 -0
  113. package/.opencode/mednotes/skills/pdf-library/SKILL.md +45 -0
  114. package/.opencode/mednotes/skills/process-medical-chats/SKILL.md +246 -0
  115. package/.opencode/mednotes/skills/workflow-report/SKILL.md +100 -0
  116. package/.opencode/mednotes/src/mednotes/__init__.py +5 -0
  117. package/.opencode/mednotes/src/mednotes/domains/__init__.py +5 -0
  118. package/.opencode/mednotes/src/mednotes/domains/flashcards/README.md +26 -0
  119. package/.opencode/mednotes/src/mednotes/domains/flashcards/__init__.py +2 -0
  120. package/.opencode/mednotes/src/mednotes/domains/flashcards/build_demo_apkg.py +177 -0
  121. package/.opencode/mednotes/src/mednotes/domains/flashcards/contracts.py +385 -0
  122. package/.opencode/mednotes/src/mednotes/domains/flashcards/flashcards_machine.py +522 -0
  123. package/.opencode/mednotes/src/mednotes/domains/flashcards/fsm.py +817 -0
  124. package/.opencode/mednotes/src/mednotes/domains/flashcards/index.py +630 -0
  125. package/.opencode/mednotes/src/mednotes/domains/flashcards/install_models.py +445 -0
  126. package/.opencode/mednotes/src/mednotes/domains/flashcards/model.py +359 -0
  127. package/.opencode/mednotes/src/mednotes/domains/flashcards/obsidian_links.py +135 -0
  128. package/.opencode/mednotes/src/mednotes/domains/flashcards/obsidian_note_utils.py +546 -0
  129. package/.opencode/mednotes/src/mednotes/domains/flashcards/pipeline.py +580 -0
  130. package/.opencode/mednotes/src/mednotes/domains/flashcards/report.py +510 -0
  131. package/.opencode/mednotes/src/mednotes/domains/flashcards/sources.py +682 -0
  132. package/.opencode/mednotes/src/mednotes/domains/flashcards/sync_rules.py +184 -0
  133. package/.opencode/mednotes/src/mednotes/domains/history/__init__.py +1 -0
  134. package/.opencode/mednotes/src/mednotes/domains/history/history_fsm.py +852 -0
  135. package/.opencode/mednotes/src/mednotes/domains/history/history_machine.py +453 -0
  136. package/.opencode/mednotes/src/mednotes/domains/setup/__init__.py +7 -0
  137. package/.opencode/mednotes/src/mednotes/domains/setup/setup_fsm.py +808 -0
  138. package/.opencode/mednotes/src/mednotes/domains/setup/setup_machine.py +973 -0
  139. package/.opencode/mednotes/src/mednotes/domains/wiki/README.md +64 -0
  140. package/.opencode/mednotes/src/mednotes/domains/wiki/__init__.py +1 -0
  141. package/.opencode/mednotes/src/mednotes/domains/wiki/api.py +668 -0
  142. package/.opencode/mednotes/src/mednotes/domains/wiki/batch_state.py +102 -0
  143. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/__init__.py +1 -0
  144. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/atomicity/__init__.py +1 -0
  145. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/atomicity/atomicity.py +877 -0
  146. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/body_link/__init__.py +1 -0
  147. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/body_link/body_linker.py +1562 -0
  148. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/__init__.py +1 -0
  149. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/effect_adapters.py +949 -0
  150. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/fix_wiki_runtime_adapters.py +433 -0
  151. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/__init__.py +1 -0
  152. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/coverage.py +413 -0
  153. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/graph.py +396 -0
  154. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/graph_fixes.py +161 -0
  155. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/hygiene/__init__.py +1 -0
  156. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/hygiene/hygiene.py +483 -0
  157. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/__init__.py +2 -0
  158. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/anchors.py +185 -0
  159. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/__init__.py +0 -0
  160. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/cache.py +223 -0
  161. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/config.py +131 -0
  162. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/download.py +224 -0
  163. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/frontmatter.py +59 -0
  164. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/insert.py +227 -0
  165. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/local_import.py +54 -0
  166. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/__init__.py +42 -0
  167. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/web_profiles.py +99 -0
  168. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/web_search.py +203 -0
  169. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/wikimedia.py +102 -0
  170. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/__init__.py +1 -0
  171. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_db_adapter.mjs +434 -0
  172. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_node_runtime.py +274 -0
  173. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_query.py +227 -0
  174. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/__init__.py +1 -0
  175. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/artifacts.py +605 -0
  176. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/canonical_merge.py +277 -0
  177. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/markdown_zones.py +85 -0
  178. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/meaning_planner.py +307 -0
  179. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_iter.py +67 -0
  180. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_merge.py +278 -0
  181. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_plan.py +409 -0
  182. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_policy.py +22 -0
  183. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/__init__.py +79 -0
  184. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/fixes.py +264 -0
  185. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/frontmatter.py +435 -0
  186. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/models.py +208 -0
  187. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/prompts.py +37 -0
  188. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/tables.py +236 -0
  189. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/validate.py +404 -0
  190. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/provenance.py +478 -0
  191. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/raw_chats.py +273 -0
  192. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/sources_backfill.py +235 -0
  193. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/__init__.py +10 -0
  194. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/anchors.py +16 -0
  195. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/captions.py +47 -0
  196. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/cli.py +179 -0
  197. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/cloud.py +52 -0
  198. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/config.py +196 -0
  199. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/context_packets.py +76 -0
  200. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/db.py +81 -0
  201. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/doctor.py +102 -0
  202. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/figure_ids.py +42 -0
  203. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/ingest.py +326 -0
  204. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/insert.py +316 -0
  205. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/mentions.py +57 -0
  206. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/ocr.py +71 -0
  207. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/paths.py +35 -0
  208. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/pdf_engine.py +77 -0
  209. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/schema.py +155 -0
  210. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/search.py +188 -0
  211. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/__init__.py +1 -0
  212. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/app.py +89 -0
  213. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/image_backend.py +29 -0
  214. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/state.py +65 -0
  215. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/__init__.py +1 -0
  216. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish.py +1139 -0
  217. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish_receipts.py +365 -0
  218. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish_recovery.py +240 -0
  219. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/__init__.py +1 -0
  220. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_behavior_corpus.py +2069 -0
  221. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_report_validation.py +4448 -0
  222. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_run_audit.py +852 -0
  223. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/architect_prompt_eval.py +341 -0
  224. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/body_linker_eval.py +240 -0
  225. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/curator_output_validation.py +175 -0
  226. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/curator_prompt_eval.py +865 -0
  227. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/triager_prompt_eval.py +1295 -0
  228. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/__init__.py +1 -0
  229. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/related_notes.py +1920 -0
  230. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/related_notes_headless.py +1186 -0
  231. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/__init__.py +1 -0
  232. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/plan_attestation.py +148 -0
  233. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_receipts.py +360 -0
  234. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_runtime.py +52 -0
  235. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_task_runner.py +2470 -0
  236. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/style/__init__.py +1 -0
  237. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/style/style.py +1952 -0
  238. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/subagents/__init__.py +1 -0
  239. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/subagents/agents.py +1767 -0
  240. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/__init__.py +1 -0
  241. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/alias_projection.py +331 -0
  242. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/link_terms.py +151 -0
  243. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/llm_disambiguation.py +182 -0
  244. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/__init__.py +116 -0
  245. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/audit.py +201 -0
  246. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/migration.py +314 -0
  247. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/normalize.py +72 -0
  248. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/policy.py +135 -0
  249. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/resolve.py +413 -0
  250. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/schema.py +157 -0
  251. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/status.py +137 -0
  252. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_bootstrap.py +509 -0
  253. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_curator_batch.py +1115 -0
  254. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_ingestion.py +632 -0
  255. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_map.py +930 -0
  256. package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_recovery.py +1388 -0
  257. package/.opencode/mednotes/src/mednotes/domains/wiki/cli.py +6665 -0
  258. package/.opencode/mednotes/src/mednotes/domains/wiki/common.py +69 -0
  259. package/.opencode/mednotes/src/mednotes/domains/wiki/config.py +210 -0
  260. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/__init__.py +74 -0
  261. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agent_report.py +242 -0
  262. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agent_run_audit.py +196 -0
  263. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agents.py +601 -0
  264. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/curator.py +256 -0
  265. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/effect_payloads.py +519 -0
  266. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/happy_path.py +190 -0
  267. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/link_git.py +110 -0
  268. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/link_runtime_artifact.py +52 -0
  269. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/note_plan.py +75 -0
  270. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/paths.py +114 -0
  271. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/public_report.py +53 -0
  272. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/publish.py +111 -0
  273. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/raw_coverage.py +217 -0
  274. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes.py +136 -0
  275. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes_headless.py +153 -0
  276. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes_runtime.py +395 -0
  277. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/schema_registry.py +637 -0
  278. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/specialist.py +432 -0
  279. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/status.py +62 -0
  280. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/style_rewrite.py +568 -0
  281. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/vocabulary_ingestion.py +223 -0
  282. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_blockers.py +510 -0
  283. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_guardrails.py +637 -0
  284. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_outcomes.py +121 -0
  285. package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_receipts.py +100 -0
  286. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/__init__.py +1 -0
  287. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/__init__.py +1 -0
  288. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/__main__.py +4 -0
  289. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/cli.py +275 -0
  290. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/__init__.py +2 -0
  291. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/candidates.py +193 -0
  292. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/cli.py +189 -0
  293. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/gemini.py +220 -0
  294. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/inputs.py +120 -0
  295. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/models.py +34 -0
  296. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/parsing.py +48 -0
  297. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/prompts.py +216 -0
  298. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/quality.py +54 -0
  299. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/reporting.py +24 -0
  300. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/runner.py +433 -0
  301. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/utils.py +39 -0
  302. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/vault_guard_bridge.py +17 -0
  303. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/__init__.py +1 -0
  304. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_context_packets.py +454 -0
  305. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_decision_projection.py +133 -0
  306. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_effects.py +1260 -0
  307. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_fsm.py +2768 -0
  308. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_machine.py +1588 -0
  309. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_plan.py +306 -0
  310. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_primary_objective.py +316 -0
  311. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_problem.py +153 -0
  312. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_receipt_evidence.py +306 -0
  313. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_states.py +290 -0
  314. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_user_report.py +342 -0
  315. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/health.py +6332 -0
  316. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/__init__.py +1 -0
  317. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_fsm.py +1119 -0
  318. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_git.py +638 -0
  319. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_machine.py +1106 -0
  320. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_retry_governance.py +374 -0
  321. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_runtime_result.py +485 -0
  322. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_triggers.py +183 -0
  323. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/linking.py +2758 -0
  324. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/reference_repair.py +718 -0
  325. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/related_notes_fsm.py +1855 -0
  326. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link_related/__init__.py +1 -0
  327. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link_related/link_related_machine.py +834 -0
  328. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/__init__.py +1 -0
  329. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_fsm.py +1592 -0
  330. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_machine.py +3097 -0
  331. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_primary_objective.py +28 -0
  332. package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_runtime_result.py +185 -0
  333. package/.opencode/mednotes/src/mednotes/domains/wiki/performance.py +97 -0
  334. package/.opencode/mednotes/src/mednotes/kernel/__init__.py +6 -0
  335. package/.opencode/mednotes/src/mednotes/kernel/agent_directive.py +336 -0
  336. package/.opencode/mednotes/src/mednotes/kernel/base.py +51 -0
  337. package/.opencode/mednotes/src/mednotes/kernel/blockers.py +39 -0
  338. package/.opencode/mednotes/src/mednotes/kernel/effect_executor.py +55 -0
  339. package/.opencode/mednotes/src/mednotes/kernel/effect_intent.py +69 -0
  340. package/.opencode/mednotes/src/mednotes/kernel/effects.py +160 -0
  341. package/.opencode/mednotes/src/mednotes/kernel/errors.py +38 -0
  342. package/.opencode/mednotes/src/mednotes/kernel/fsm_event.py +35 -0
  343. package/.opencode/mednotes/src/mednotes/kernel/fsm_model.py +55 -0
  344. package/.opencode/mednotes/src/mednotes/kernel/fsm_transition_result.py +75 -0
  345. package/.opencode/mednotes/src/mednotes/kernel/guardrails.py +188 -0
  346. package/.opencode/mednotes/src/mednotes/kernel/progress.py +319 -0
  347. package/.opencode/mednotes/src/mednotes/kernel/public_report.py +346 -0
  348. package/.opencode/mednotes/src/mednotes/kernel/state_machine.py +164 -0
  349. package/.opencode/mednotes/src/mednotes/kernel/workflow.py +619 -0
  350. package/.opencode/mednotes/src/mednotes/platform/__init__.py +5 -0
  351. package/.opencode/mednotes/src/mednotes/platform/backup_policy.py +382 -0
  352. package/.opencode/mednotes/src/mednotes/platform/feedback/__init__.py +62 -0
  353. package/.opencode/mednotes/src/mednotes/platform/feedback/cli.py +275 -0
  354. package/.opencode/mednotes/src/mednotes/platform/feedback/contracts.py +83 -0
  355. package/.opencode/mednotes/src/mednotes/platform/feedback/core.py +4168 -0
  356. package/.opencode/mednotes/src/mednotes/platform/feedback/integrity.py +989 -0
  357. package/.opencode/mednotes/src/mednotes/platform/feedback/operational_contract.py +2293 -0
  358. package/.opencode/mednotes/src/mednotes/platform/feedback/telemetry.py +875 -0
  359. package/.opencode/mednotes/src/mednotes/platform/feedback/telemetry_config.py +65 -0
  360. package/.opencode/mednotes/src/mednotes/platform/opencode_runtime_config.py +182 -0
  361. package/.opencode/mednotes/src/mednotes/platform/paths/__init__.py +1560 -0
  362. package/.opencode/mednotes/src/mednotes/platform/secrets.py +89 -0
  363. package/.opencode/mednotes/src/mednotes/platform/user_config.py +103 -0
  364. package/.opencode/mednotes/src/mednotes/platform/vault_guard.py +214 -0
  365. package/.opencode/mednotes/uv.lock +932 -0
  366. package/.opencode/mednotes.generated.json +395 -0
  367. package/.opencode/opencode.json +31 -0
  368. package/.opencode/plugins/mednotes-fsm.mjs +7 -0
  369. package/.opencode/plugins/mednotes_hook/adapters/antigravity.mjs +169 -0
  370. package/.opencode/plugins/mednotes_hook/adapters/harness_payload.mjs +103 -0
  371. package/.opencode/plugins/mednotes_hook/adapters/opencode_plugin.mjs +341 -0
  372. package/.opencode/plugins/mednotes_hook/adapters/opencode_user_config_sync.mjs +177 -0
  373. package/.opencode/plugins/mednotes_hook/anki_preflight.mjs +214 -0
  374. package/.opencode/plugins/mednotes_hook/cli.mjs +143 -0
  375. package/.opencode/plugins/mednotes_hook/diagnostics.mjs +11 -0
  376. package/.opencode/plugins/mednotes_hook/domain/agent_directive_core.mjs +160 -0
  377. package/.opencode/plugins/mednotes_hook/fsm_directive.mjs +1470 -0
  378. package/.opencode/plugins/mednotes_hook/hook_errors.mjs +120 -0
  379. package/.opencode/plugins/mednotes_hook/retention.mjs +114 -0
  380. package/.opencode/plugins/mednotes_hook/runtime.mjs +174 -0
  381. package/.opencode/plugins/mednotes_hook/telemetry_capture.mjs +511 -0
  382. package/.opencode/plugins/mednotes_hook/vault_guard.mjs +624 -0
  383. package/AGENTS.md +57 -0
  384. package/README.md +194 -0
  385. package/adapters/antigravity/agents.json +80 -0
  386. package/adapters/antigravity/templates/med-chat-triager.md +214 -0
  387. package/adapters/antigravity/templates/med-flashcard-maker.md +72 -0
  388. package/adapters/antigravity/templates/med-knowledge-architect.md +241 -0
  389. package/adapters/antigravity/templates/med-link-graph-curator.md +187 -0
  390. package/adapters/antigravity/templates/med-publish-guard.md +71 -0
  391. package/adapters/gemini-cli/gemini-extension.json +14 -0
  392. package/adapters/gemini-cli/package.json +15 -0
  393. package/adapters/gemini-cli/pyproject.toml +48 -0
  394. package/bin/mednotes-opencode.mjs +155 -0
  395. package/contracts/agents.json +116 -0
  396. package/core/agents/med-chat-triager.md +197 -0
  397. package/core/agents/med-flashcard-maker.md +56 -0
  398. package/core/agents/med-knowledge-architect.md +224 -0
  399. package/core/agents/med-link-graph-curator.md +171 -0
  400. package/core/agents/med-publish-guard.md +55 -0
  401. package/core/commands/flashcards.toml +22 -0
  402. package/core/commands/mednotes/create.toml +22 -0
  403. package/core/commands/mednotes/enrich.toml +24 -0
  404. package/core/commands/mednotes/fix-wiki.toml +24 -0
  405. package/core/commands/mednotes/history.toml +19 -0
  406. package/core/commands/mednotes/link-body.toml +22 -0
  407. package/core/commands/mednotes/link-related.toml +24 -0
  408. package/core/commands/mednotes/link.toml +24 -0
  409. package/core/commands/mednotes/pdf-library.toml +24 -0
  410. package/core/commands/mednotes/process-chats.toml +20 -0
  411. package/core/commands/mednotes/setup.toml +18 -0
  412. package/core/commands/mednotes/status.toml +24 -0
  413. package/core/commands/mednotes/telemetry.toml +24 -0
  414. package/core/commands/report.toml +23 -0
  415. package/core/skills/THIRD_PARTY_NOTICES.md +45 -0
  416. package/core/skills/create-medical-flashcards/SKILL.md +113 -0
  417. package/core/skills/create-medical-note/SKILL.md +90 -0
  418. package/core/skills/enrich-medical-note/SKILL.md +120 -0
  419. package/core/skills/fix-medical-wiki/SKILL.md +559 -0
  420. package/core/skills/link-medical-wiki/SKILL.md +224 -0
  421. package/core/skills/obsidian-cli/SKILL.md +118 -0
  422. package/core/skills/obsidian-markdown/SKILL.md +207 -0
  423. package/core/skills/obsidian-markdown/references/CALLOUTS.md +58 -0
  424. package/core/skills/obsidian-markdown/references/EMBEDS.md +63 -0
  425. package/core/skills/obsidian-markdown/references/PROPERTIES.md +61 -0
  426. package/core/skills/obsidian-ops/SKILL.md +136 -0
  427. package/core/skills/pdf-library/SKILL.md +45 -0
  428. package/core/skills/process-medical-chats/SKILL.md +246 -0
  429. package/core/skills/workflow-report/SKILL.md +100 -0
  430. package/package.json +45 -0
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated, Any
4
+
5
+ from pydantic import AfterValidator, BaseModel, ConfigDict, TypeAdapter
6
+ from pydantic import ValidationError as PydanticValidationError
7
+ from typing_extensions import TypeAliasType
8
+
9
+ from mednotes.kernel.errors import ValidationError
10
+
11
+ JsonValue = TypeAliasType(
12
+ "JsonValue",
13
+ str | int | float | bool | None | list["JsonValue"] | dict[str, "JsonValue"],
14
+ )
15
+ _StrictJsonObject = dict[str, JsonValue]
16
+ _StrictJsonArray = list[JsonValue]
17
+
18
+ JsonObjectAdapter = TypeAdapter(_StrictJsonObject)
19
+ JsonArrayAdapter = TypeAdapter(_StrictJsonArray)
20
+
21
+
22
+ def _validated_json_object(value: Any) -> dict[str, Any]:
23
+ return JsonObjectAdapter.validate_python(value)
24
+
25
+
26
+ def _validated_json_array(value: Any) -> list[Any]:
27
+ return JsonArrayAdapter.validate_python(value)
28
+
29
+
30
+ JsonObject = Annotated[dict[str, Any], AfterValidator(_validated_json_object)]
31
+ JsonArray = Annotated[list[Any], AfterValidator(_validated_json_array)]
32
+
33
+
34
+ class ContractModel(BaseModel):
35
+ model_config = ConfigDict(
36
+ extra="forbid",
37
+ populate_by_name=True,
38
+ validate_assignment=True,
39
+ )
40
+
41
+ def to_payload(self) -> dict[str, Any]:
42
+ payload = self.model_dump(mode="json", by_alias=True)
43
+ JsonObjectAdapter.validate_python(payload)
44
+ return payload
45
+
46
+
47
+ def contract_error(exc: PydanticValidationError, *, prefix: str) -> ValidationError:
48
+ first = exc.errors()[0] if exc.errors() else {}
49
+ loc = ".".join(str(part) for part in first.get("loc", ())) or "$"
50
+ msg = str(first.get("msg") or str(exc))
51
+ return ValidationError(f"{prefix}: {loc}: {msg}")
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import field_validator, model_validator
4
+ from pydantic_core.core_schema import ValidationInfo
5
+
6
+ from mednotes.kernel.base import ContractModel
7
+ from mednotes.kernel.workflow import WorkflowDecisionKind
8
+
9
+
10
+ def _non_empty(value: str, field_name: str) -> str:
11
+ cleaned = str(value or "").strip()
12
+ if not cleaned:
13
+ raise ValueError(f"{field_name} must be non-empty")
14
+ return cleaned
15
+
16
+
17
+ class BlockerEntryModel(ContractModel):
18
+ code: str
19
+ owner_phase: str
20
+ default_decision: WorkflowDecisionKind
21
+ safe_to_continue_batch: bool
22
+ requires_human_packet: bool
23
+ public_label: str
24
+ public_explanation: str
25
+ developer_explanation: str
26
+ test_fixture: str
27
+
28
+ @field_validator("code", "owner_phase", "public_label", "public_explanation", "developer_explanation", "test_fixture")
29
+ @classmethod
30
+ def _required_text(cls, value: str, info: ValidationInfo) -> str:
31
+ return _non_empty(value, str(info.field_name))
32
+
33
+ @model_validator(mode="after")
34
+ def _validate_human_packet_policy(self) -> BlockerEntryModel:
35
+ if self.default_decision == WorkflowDecisionKind.ASK_HUMAN and not self.requires_human_packet:
36
+ raise ValueError("requires_human_packet must be true when default_decision is ask_human")
37
+ if self.default_decision != WorkflowDecisionKind.ASK_HUMAN and self.requires_human_packet:
38
+ raise ValueError("requires_human_packet is only valid for ask_human blockers")
39
+ return self
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Protocol
5
+
6
+ from mednotes.kernel.effects import WorkflowEffect, WorkflowEffectKind, WorkflowEffectResult
7
+
8
+
9
+ class MissingWorkflowEffectAdapter(ValueError):
10
+ """Raised when an FSM emitted an effect that has no official adapter."""
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class WorkflowEffectExecutionContext:
15
+ dry_run: bool = False
16
+ environment: dict[str, str] = field(default_factory=dict)
17
+ artifacts_dir: str = ""
18
+
19
+
20
+ class WorkflowEffectAdapter(Protocol):
21
+ def run(self, effect: WorkflowEffect, context: WorkflowEffectExecutionContext) -> WorkflowEffectResult:
22
+ """Materialize one already-authorized workflow effect."""
23
+
24
+
25
+ @dataclass
26
+ class WorkflowEffectExecutor:
27
+ adapters: dict[WorkflowEffectKind, WorkflowEffectAdapter]
28
+ context: WorkflowEffectExecutionContext = field(default_factory=WorkflowEffectExecutionContext)
29
+
30
+ def execute(
31
+ self,
32
+ effect: WorkflowEffect,
33
+ *,
34
+ context: WorkflowEffectExecutionContext | None = None,
35
+ ) -> WorkflowEffectResult:
36
+ if not isinstance(effect, WorkflowEffect):
37
+ raise TypeError("WorkflowEffectExecutor.execute requires WorkflowEffect")
38
+ adapter = self._adapter_for(effect.kind)
39
+ return adapter.run(effect, context or self.context)
40
+
41
+ def _adapter_for(self, kind: WorkflowEffectKind) -> WorkflowEffectAdapter:
42
+ match kind:
43
+ case (
44
+ WorkflowEffectKind.RUN_SUBWORKFLOW
45
+ | WorkflowEffectKind.CALL_SPECIALIST_MODEL
46
+ | WorkflowEffectKind.ASK_HUMAN
47
+ | WorkflowEffectKind.WAIT_EXTERNAL
48
+ ):
49
+ return self._required(kind)
50
+
51
+ def _required(self, kind: WorkflowEffectKind) -> WorkflowEffectAdapter:
52
+ adapter = self.adapters.get(kind)
53
+ if adapter is None:
54
+ raise MissingWorkflowEffectAdapter(f"no adapter registered for workflow effect kind: {kind.value}")
55
+ return adapter
@@ -0,0 +1,69 @@
1
+ """Executable workflow effect intents without workflow-result dependencies.
2
+
3
+ `WorkflowEffect` is the FSM-owned intent that adapters may execute later. It
4
+ must stay independent from `workflow.py` so `state_machine.py` can type
5
+ transition effects without creating a kernel import cycle.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from enum import StrEnum
10
+
11
+ from pydantic import Field, ValidationInfo, field_validator, model_validator
12
+
13
+ from mednotes.kernel.base import ContractModel, JsonObject
14
+
15
+
16
+ class WorkflowEffectKind(StrEnum):
17
+ RUN_SUBWORKFLOW = "run_subworkflow"
18
+ CALL_SPECIALIST_MODEL = "call_specialist_model"
19
+ ASK_HUMAN = "ask_human"
20
+ WAIT_EXTERNAL = "wait_external"
21
+
22
+
23
+ class WorkflowEffect(ContractModel):
24
+ """Executable work intent emitted from one stable workflow state."""
25
+
26
+ schema_id: str = Field(default="workflow-effect.v1", alias="schema")
27
+ workflow: str = Field(min_length=1)
28
+ run_id: str = Field(min_length=1)
29
+ effect_id: str = Field(min_length=1)
30
+ origin_state: str = Field(min_length=1)
31
+ kind: WorkflowEffectKind
32
+ target: str = ""
33
+ payload: JsonObject = Field(default_factory=dict)
34
+ mutates_resources: bool = Field(default=False, strict=True)
35
+ no_resource_mutation: bool = Field(default=False, strict=True)
36
+ rollback_declared: bool = Field(default=False, strict=True)
37
+ requires_receipt: bool = Field(default=True, strict=True)
38
+ requires_attestation: bool = Field(default=False, strict=True)
39
+ model_policy: JsonObject = Field(default_factory=dict)
40
+ resume_action: str = ""
41
+ metadata: JsonObject = Field(default_factory=dict)
42
+
43
+ @field_validator("workflow", "run_id", "effect_id", "origin_state")
44
+ @classmethod
45
+ def _required_text(cls, value: str, info: ValidationInfo) -> str:
46
+ cleaned = value.strip()
47
+ if not cleaned:
48
+ raise ValueError(f"{info.field_name} must be non-empty")
49
+ return cleaned
50
+
51
+ @model_validator(mode="after")
52
+ def _validate_effect_contract(self) -> WorkflowEffect:
53
+ if (
54
+ self.kind
55
+ in {
56
+ WorkflowEffectKind.RUN_SUBWORKFLOW,
57
+ WorkflowEffectKind.CALL_SPECIALIST_MODEL,
58
+ WorkflowEffectKind.WAIT_EXTERNAL,
59
+ }
60
+ and not self.target.strip()
61
+ ):
62
+ raise ValueError(f"{self.kind} requires target")
63
+ if self.mutates_resources and self.no_resource_mutation:
64
+ raise ValueError("mutates_resources cannot be combined with no_resource_mutation")
65
+ if self.mutates_resources and not self.rollback_declared:
66
+ raise ValueError("mutates_resources requires rollback_declared")
67
+ if self.kind == WorkflowEffectKind.CALL_SPECIALIST_MODEL and not self.model_policy:
68
+ raise ValueError("call_specialist_model requires model_policy")
69
+ return self
@@ -0,0 +1,160 @@
1
+ """Framework workflow-effect contracts (pure FSM kernel, domain-agnostic).
2
+
3
+ WorkflowEffect / WorkflowEffectResult are the generic effect intent + result of
4
+ the FSM kernel. Concrete product payloads live in domain modules; adapters are
5
+ the only layer allowed to materialize those effects in the outside world.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from enum import StrEnum
10
+
11
+ from pydantic import Field, SerializeAsAny, model_validator
12
+
13
+ from mednotes.kernel.base import ContractModel, JsonObject
14
+ from mednotes.kernel.effect_intent import WorkflowEffect, WorkflowEffectKind
15
+ from mednotes.kernel.progress import WorkflowProgressEvent
16
+ from mednotes.kernel.workflow import HumanDecisionPacket
17
+
18
+ __all__ = [
19
+ "WorkflowEffect",
20
+ "WorkflowEffectKind",
21
+ "WorkflowEffectOutcome",
22
+ "WorkflowEffectResult",
23
+ "WorkflowEffectStatus",
24
+ "workflow_effect_blocked_outcome",
25
+ "workflow_effect_completed_outcome",
26
+ "workflow_effect_failed_outcome",
27
+ "workflow_effect_skipped_outcome",
28
+ "workflow_effect_waiting_agent_outcome",
29
+ "workflow_effect_waiting_external_outcome",
30
+ "workflow_effect_waiting_human_outcome",
31
+ "workflow_effect_warning_outcome",
32
+ ]
33
+
34
+
35
+ class WorkflowEffectStatus(StrEnum):
36
+ COMPLETED = "completed"
37
+ COMPLETED_WITH_WARNINGS = "completed_with_warnings"
38
+ WAITING_AGENT = "waiting_agent"
39
+ WAITING_EXTERNAL = "waiting_external"
40
+ WAITING_HUMAN = "waiting_human"
41
+ BLOCKED = "blocked"
42
+ FAILED = "failed"
43
+ SKIPPED = "skipped"
44
+
45
+
46
+ class WorkflowEffectOutcome(ContractModel):
47
+ """Generic outcome discriminator for framework-level roundtrips.
48
+
49
+ Domain adapters should return stricter outcome models at their own
50
+ boundary. The kernel only requires a stable `code` field so result envelopes
51
+ can be serialized without knowing product-specific outcome matrices.
52
+ """
53
+
54
+ code: str = Field(min_length=1)
55
+ reason_code: str = ""
56
+
57
+
58
+ def workflow_effect_completed_outcome() -> WorkflowEffectOutcome:
59
+ """Factory for framework-only tests and adapters without domain policy."""
60
+
61
+ return WorkflowEffectOutcome(code="workflow_effect.completed")
62
+
63
+
64
+ def workflow_effect_warning_outcome(*, reason_code: str = "") -> WorkflowEffectOutcome:
65
+ """Factory for framework-level warning outcomes."""
66
+
67
+ return WorkflowEffectOutcome(code="workflow_effect.completed_with_warnings", reason_code=reason_code)
68
+
69
+
70
+ def workflow_effect_waiting_external_outcome(*, reason_code: str = "") -> WorkflowEffectOutcome:
71
+ """Factory for framework-level resumable external waits."""
72
+
73
+ return WorkflowEffectOutcome(code="workflow_effect.waiting_external", reason_code=reason_code)
74
+
75
+
76
+ def workflow_effect_waiting_agent_outcome(*, reason_code: str = "") -> WorkflowEffectOutcome:
77
+ """Factory for framework-level executable agent work."""
78
+
79
+ return WorkflowEffectOutcome(code="workflow_effect.waiting_agent", reason_code=reason_code)
80
+
81
+
82
+ def workflow_effect_waiting_human_outcome(*, reason_code: str = "") -> WorkflowEffectOutcome:
83
+ """Factory for framework-level human-decision waits."""
84
+
85
+ return WorkflowEffectOutcome(code="workflow_effect.waiting_human", reason_code=reason_code)
86
+
87
+
88
+ def workflow_effect_blocked_outcome(*, reason_code: str = "") -> WorkflowEffectOutcome:
89
+ """Factory for framework-level blocked outcomes."""
90
+
91
+ return WorkflowEffectOutcome(code="workflow_effect.blocked", reason_code=reason_code)
92
+
93
+
94
+ def workflow_effect_failed_outcome(*, reason_code: str = "") -> WorkflowEffectOutcome:
95
+ """Factory for framework-level failed outcomes."""
96
+
97
+ return WorkflowEffectOutcome(code="workflow_effect.failed", reason_code=reason_code)
98
+
99
+
100
+ def workflow_effect_skipped_outcome(*, reason_code: str = "") -> WorkflowEffectOutcome:
101
+ """Factory for intentionally skipped effects."""
102
+
103
+ return WorkflowEffectOutcome(code="workflow_effect.skipped", reason_code=reason_code)
104
+
105
+
106
+ class WorkflowEffectResult(ContractModel):
107
+ """Typed result returned by the adapter that materialized one effect."""
108
+
109
+ schema_id: str = Field(default="workflow-effect-result.v1", alias="schema")
110
+ effect: WorkflowEffect
111
+ status: WorkflowEffectStatus
112
+ # Effect outcomes are polymorphic by design: adapters may return a stricter
113
+ # domain model while the kernel only requires the generic code contract.
114
+ outcome: SerializeAsAny[WorkflowEffectOutcome | ContractModel]
115
+ public_summary: str = Field(min_length=1)
116
+ developer_summary: str = Field(min_length=1)
117
+ payload: JsonObject = Field(default_factory=dict)
118
+ receipt: JsonObject | None = None
119
+ attestation: JsonObject | None = None
120
+ human_decision_packet: HumanDecisionPacket | None = None
121
+ error_context: JsonObject = Field(default_factory=dict)
122
+ progress_events: list[WorkflowProgressEvent] = Field(default_factory=list)
123
+ next_action: str = ""
124
+ resume_action: str = ""
125
+
126
+ @model_validator(mode="after")
127
+ def _validate_result_contract(self) -> WorkflowEffectResult:
128
+ if (
129
+ self.status
130
+ in {
131
+ WorkflowEffectStatus.BLOCKED,
132
+ WorkflowEffectStatus.FAILED,
133
+ WorkflowEffectStatus.COMPLETED_WITH_WARNINGS,
134
+ }
135
+ and not self.next_action.strip()
136
+ ):
137
+ raise ValueError(f"{self.status} requires next_action")
138
+ if self.status == WorkflowEffectStatus.WAITING_EXTERNAL:
139
+ if not self.resume_action.strip():
140
+ raise ValueError("waiting_external effect result requires resume_action")
141
+ if not self.next_action.strip():
142
+ object.__setattr__(self, "next_action", self.resume_action)
143
+ if self.status == WorkflowEffectStatus.WAITING_HUMAN and self.human_decision_packet is None:
144
+ raise ValueError("waiting_human effect result requires human_decision_packet")
145
+ outcome_code = getattr(self.outcome, "code", "")
146
+ if not isinstance(outcome_code, str) or not outcome_code.strip():
147
+ raise ValueError("effect result outcome requires code")
148
+ if (
149
+ self.status in {WorkflowEffectStatus.COMPLETED, WorkflowEffectStatus.COMPLETED_WITH_WARNINGS}
150
+ and self.effect.requires_receipt
151
+ and self.receipt is None
152
+ ):
153
+ raise ValueError("completed effect result requires receipt")
154
+ if (
155
+ self.status in {WorkflowEffectStatus.COMPLETED, WorkflowEffectStatus.COMPLETED_WITH_WARNINGS}
156
+ and self.effect.requires_attestation
157
+ and self.attestation is None
158
+ ):
159
+ raise ValueError("effect result requires attestation")
160
+ return self
@@ -0,0 +1,38 @@
1
+ """Framework process-exit codes and the typed-error hierarchy.
2
+
3
+ Domain-agnostic: plain exit codes plus a base exception that carries one. Lives
4
+ in the framework so the FSM kernel can raise/type its errors without importing
5
+ domain facades. Layering rule: framework <- domain <- adapters
6
+ (tools/audit/import_layering.py).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ EXIT_OK = 0
11
+ EXIT_USAGE = 2
12
+ EXIT_VALIDATION = 3
13
+ EXIT_MISSING = 4
14
+ EXIT_IO = 5
15
+
16
+
17
+ class MedOpsError(Exception):
18
+ """Base exception carrying a process exit code."""
19
+
20
+ exit_code = EXIT_IO
21
+
22
+
23
+ class ValidationError(MedOpsError):
24
+ exit_code = EXIT_VALIDATION
25
+
26
+
27
+ class MissingPathError(MedOpsError):
28
+ exit_code = EXIT_MISSING
29
+
30
+
31
+ class CollisionError(MedOpsError):
32
+ exit_code = EXIT_VALIDATION
33
+
34
+
35
+ class FileWriteError(MedOpsError):
36
+ """Filesystem write failed after local retry/recovery attempts."""
37
+
38
+ exit_code = EXIT_IO
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from pydantic import Field
6
+
7
+ from mednotes.kernel.base import ContractModel, JsonObject
8
+
9
+
10
+ class WorkflowEvent(ContractModel):
11
+ """Base fact consumed by workflow statecharts; domains add typed fields."""
12
+
13
+ workflow: str = Field(min_length=1)
14
+ run_id: str = Field(min_length=1)
15
+ name: str = Field(min_length=1)
16
+ current_state: str = Field(min_length=1)
17
+ # Redacted replay/debug material only; FSM decisions must use typed fields.
18
+ audit_evidence: JsonObject = Field(default_factory=dict)
19
+
20
+
21
+ class WorkflowEventLike(Protocol):
22
+ """Structural event surface required by the StateChart kernel wrapper."""
23
+
24
+ @property
25
+ def workflow(self) -> str: ...
26
+
27
+ @property
28
+ def run_id(self) -> str: ...
29
+
30
+ @property
31
+ def name(self) -> str:
32
+ ...
33
+
34
+ @property
35
+ def current_state(self) -> str: ...
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import ClassVar
4
+
5
+ from pydantic import Field
6
+
7
+ from mednotes.kernel.base import ContractModel, JsonObject
8
+ from mednotes.kernel.effects import WorkflowEffect
9
+ from mednotes.kernel.fsm_event import WorkflowEventLike
10
+ from mednotes.kernel.fsm_transition_result import WorkflowTransitionResult
11
+
12
+
13
+ class WorkflowModel(ContractModel):
14
+ """Persisted workflow state used directly as python-statemachine carrier."""
15
+
16
+ STATECHART_STATE_FIELD: ClassVar[str] = "state"
17
+
18
+ workflow: str = Field(min_length=1)
19
+ run_id: str = Field(min_length=1)
20
+ # Public persisted state is the single mutable StateChart carrier.
21
+ state: str = Field(min_length=1)
22
+ # Events are typed before sending; the persisted log stores JSON evidence
23
+ # so WorkflowModel can rehydrate without importing every domain union.
24
+ event_log: list[JsonObject] = Field(default_factory=list)
25
+ transition_log: list[WorkflowTransitionResult] = Field(default_factory=list)
26
+ last_transition: WorkflowTransitionResult | None = None
27
+ pending_effects: list[WorkflowEffect] = Field(default_factory=list)
28
+
29
+ @classmethod
30
+ def start(cls, *, workflow: str, run_id: str, initial_state: str) -> WorkflowModel:
31
+ return cls(workflow=workflow, run_id=run_id, state=initial_state)
32
+
33
+ def __setattr__(self, name: str, value: object) -> None:
34
+ """Allow python-statemachine's transient empty carrier during microsteps."""
35
+
36
+ if name == self.STATECHART_STATE_FIELD and value is None:
37
+ object.__setattr__(self, name, value)
38
+ return
39
+ super().__setattr__(name, value)
40
+
41
+ def record_event(self, event: WorkflowEventLike) -> None:
42
+ if not isinstance(event, ContractModel):
43
+ raise TypeError("workflow events must be Pydantic contract models")
44
+ if event.workflow != self.workflow or event.run_id != self.run_id:
45
+ raise ValueError("event belongs to a different workflow run")
46
+ self.event_log.append(event.to_payload())
47
+
48
+ def record_transition(self, transition: WorkflowTransitionResult) -> None:
49
+ if transition.workflow != self.workflow or transition.run_id != self.run_id:
50
+ raise ValueError("transition belongs to a different workflow run")
51
+ if self.state != transition.to_state:
52
+ raise ValueError("machine state does not match transition target")
53
+ object.__setattr__(self, "last_transition", transition)
54
+ object.__setattr__(self, "pending_effects", list(transition.effects))
55
+ self.transition_log.append(transition)
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+
5
+ from pydantic import Field, StrictStr, ValidationInfo, field_validator
6
+
7
+ from mednotes.kernel.base import ContractModel, JsonObject
8
+ from mednotes.kernel.effects import WorkflowEffect, WorkflowEffectKind
9
+ from mednotes.kernel.workflow import HumanDecisionPacket, WorkflowDecision
10
+
11
+
12
+ class WorkflowTransitionResult(ContractModel):
13
+ """Typed result returned by a real StateChart transition callback."""
14
+
15
+ workflow: StrictStr = Field(min_length=1)
16
+ run_id: StrictStr = Field(min_length=1)
17
+ from_state: StrictStr = Field(min_length=1)
18
+ to_state: StrictStr = Field(min_length=1)
19
+ trigger: StrictStr = Field(min_length=1)
20
+ reason_code: StrictStr = Field(min_length=1)
21
+ effects: list[WorkflowEffect] = Field(default_factory=list)
22
+ decision: WorkflowDecision | None = None
23
+ human_decision_packet: HumanDecisionPacket | None = None
24
+ resume_action: str = ""
25
+ # Redacted/debug-only evidence; categories and effects are validated from typed fields.
26
+ audit_evidence: JsonObject = Field(default_factory=dict)
27
+
28
+ @field_validator("workflow", "run_id", "from_state", "to_state", "trigger", "reason_code")
29
+ @classmethod
30
+ def _required_text(cls, value: str, info: ValidationInfo) -> str:
31
+ cleaned = value.strip()
32
+ if not cleaned:
33
+ raise ValueError(f"{info.field_name} must be non-empty")
34
+ return cleaned
35
+
36
+
37
+ def validate_transition_result(
38
+ transition: WorkflowTransitionResult,
39
+ *,
40
+ category_for_state: Callable[[str], object],
41
+ ) -> WorkflowTransitionResult:
42
+ """Validate category-dependent contracts without importing domain state maps."""
43
+
44
+ for effect in transition.effects:
45
+ if effect.origin_state != transition.to_state:
46
+ raise ValueError("effect origin_state must match transition target")
47
+
48
+ category = _category_value(category_for_state(transition.to_state))
49
+ match category:
50
+ case "waiting_agent":
51
+ if not transition.effects:
52
+ raise ValueError("waiting_agent transition requires at least one workflow effect")
53
+ case "waiting_human":
54
+ if transition.human_decision_packet is None:
55
+ raise ValueError("waiting_human transition requires human_decision_packet")
56
+ if not _has_effect_kind(transition, WorkflowEffectKind.ASK_HUMAN):
57
+ raise ValueError("waiting_human transition requires ask_human effect")
58
+ case "waiting_external":
59
+ if not transition.resume_action.strip():
60
+ raise ValueError("waiting_external transition requires resume_action")
61
+ if not _has_effect_kind(transition, WorkflowEffectKind.WAIT_EXTERNAL):
62
+ raise ValueError("waiting_external transition requires wait_external effect")
63
+ case "blocked" | "failed":
64
+ if transition.decision is None:
65
+ raise ValueError(f"{category} transition requires decision")
66
+ return transition
67
+
68
+
69
+ def _category_value(category: object) -> str:
70
+ value = getattr(category, "value", category)
71
+ return str(value)
72
+
73
+
74
+ def _has_effect_kind(transition: WorkflowTransitionResult, kind: WorkflowEffectKind) -> bool:
75
+ return any(effect.kind == kind for effect in transition.effects)