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,637 @@
1
+ """Shared operational guardrails for deterministic Wiki workflows."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from pydantic import Field, StrictBool, StrictStr
7
+ from pydantic import ValidationError as PydanticValidationError
8
+
9
+ from mednotes.domains.wiki.capabilities.notes.note_iter import iter_notes
10
+ from mednotes.domains.wiki.capabilities.vocabulary.link_terms import normalize_key
11
+ from mednotes.domains.wiki.common import ValidationError
12
+ from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter, JsonValue
13
+ from mednotes.kernel.guardrails import (
14
+ BLOCKING_STATUSES_REQUIRING_NEXT_ACTION,
15
+ CONTRACT_GAP_MISSING_NEXT_ACTION,
16
+ OperationalErrorContext,
17
+ WorkflowGuardrailPayloadFields,
18
+ blocked_payload_requires_next_action,
19
+ default_contract_next_action,
20
+ workflow_guardrail_payload_fields,
21
+ )
22
+ from mednotes.kernel.workflow import HUMAN_DECISION_PACKET_SCHEMA
23
+
24
+ SUBAGENT_OUTPUT_CONTRACT_BLOCKED_REASON = "subagent_output_contract.invalid"
25
+ CONTRACT_GAP_MISSING_ERROR_CONTEXT = "contract_gap.missing_error_context"
26
+ PROCESS_CHATS_REQUIRED_INPUTS = ["raw_file", "note_plan", "coverage_path"]
27
+ PUBLISH_REQUIRED_INPUTS = ["manifest", "coverage_path", "dry_run_receipt"]
28
+ LINK_REQUIRED_INPUTS = ["wiki_dir", "vocabulary_db_path"]
29
+ FIX_WIKI_REQUIRED_INPUTS = ["wiki_dir", "vocabulary_db_path"]
30
+ STYLE_REWRITE_REQUIRED_INPUTS = ["target", "content"]
31
+ NOTE_MERGE_REQUIRED_INPUTS = ["plan", "content"]
32
+ BLOCKING_STATUSES = BLOCKING_STATUSES_REQUIRING_NEXT_ACTION
33
+
34
+
35
+ class _SubagentOutputContractFields(ContractModel):
36
+ schema_id: StrictStr | None = Field(default=None, alias="schema")
37
+ workflow: StrictStr | None = None
38
+ phase: StrictStr | None = None
39
+ source_workflow: StrictStr | None = None
40
+ agent: StrictStr | None = None
41
+ status: StrictStr = ""
42
+ blocked_reason: StrictStr = ""
43
+ next_action: StrictStr = ""
44
+ human_decision_required: StrictBool = False
45
+ error_context: JsonObject | None = None
46
+
47
+
48
+ def annotate_payload(
49
+ payload: JsonObject,
50
+ *,
51
+ phase: str,
52
+ status: str,
53
+ blocked_reason: str | None = None,
54
+ next_action: str | None = None,
55
+ required_inputs: list[str] | None = None,
56
+ human_decision_required: bool = False,
57
+ ) -> JsonObject:
58
+ """Attach stable operational fields expected by workflows/tests/agents."""
59
+ annotated = dict(JsonObjectAdapter.validate_python(payload))
60
+ boundary_fields = JsonObjectAdapter.validate_python(
61
+ {
62
+ "phase": phase,
63
+ "status": status,
64
+ "blocked_reason": blocked_reason or "",
65
+ "next_action": next_action or "",
66
+ "required_inputs": list(required_inputs or []),
67
+ "human_decision_required": bool(human_decision_required),
68
+ }
69
+ )
70
+ annotated.update(boundary_fields)
71
+ return _ensure_workflow_decision_boundary(annotated)
72
+
73
+
74
+ def _json_object(value: object) -> JsonObject:
75
+ return JsonObjectAdapter.validate_python(value)
76
+
77
+
78
+ def _object_field(value: object, key: str) -> object:
79
+ """Read optional external fields without letting loose dict access drive flow."""
80
+
81
+ if not isinstance(value, dict) or key not in value:
82
+ return None
83
+ return value[key]
84
+
85
+
86
+ def _json_field(payload: JsonObject, key: str) -> object:
87
+ if key not in payload:
88
+ return None
89
+ return payload[key]
90
+
91
+
92
+ def _json_str_field(payload: JsonObject, key: str) -> str:
93
+ value = _json_field(payload, key)
94
+ return value if isinstance(value, str) else ""
95
+
96
+
97
+ def _json_object_field(payload: JsonObject, key: str) -> JsonObject | None:
98
+ value = _json_field(payload, key)
99
+ return value if isinstance(value, dict) else None
100
+
101
+
102
+ def _json_stringified_field(payload: JsonObject, key: str) -> str:
103
+ value = _json_field(payload, key)
104
+ return str(value) if value else ""
105
+
106
+
107
+ def _guardrail_fields(payload: JsonObject) -> WorkflowGuardrailPayloadFields:
108
+ return workflow_guardrail_payload_fields(payload)
109
+
110
+
111
+ def _strict_guardrail_fields(payload: JsonObject) -> WorkflowGuardrailPayloadFields:
112
+ raw_fields: JsonObject = {}
113
+ for key in (
114
+ "phase",
115
+ "status",
116
+ "blocked_reason",
117
+ "next_action",
118
+ "next_command",
119
+ "required_inputs",
120
+ "human_decision_required",
121
+ "human_decision_packet",
122
+ "human_decision_packets",
123
+ "error_context",
124
+ "diagnostic_context",
125
+ "affected_artifact",
126
+ "error",
127
+ "message",
128
+ ):
129
+ if key in payload:
130
+ raw_fields[key] = payload[key]
131
+ return WorkflowGuardrailPayloadFields.model_validate(raw_fields)
132
+
133
+
134
+ def _subagent_contract_fields(payload: JsonObject) -> tuple[_SubagentOutputContractFields, list[JsonObject]]:
135
+ raw_fields: JsonObject = {}
136
+ for key in (
137
+ "schema",
138
+ "workflow",
139
+ "phase",
140
+ "source_workflow",
141
+ "agent",
142
+ "status",
143
+ "blocked_reason",
144
+ "next_action",
145
+ "error_context",
146
+ ):
147
+ if key in payload:
148
+ raw_fields[key] = payload[key]
149
+ try:
150
+ return _SubagentOutputContractFields.model_validate(raw_fields), []
151
+ except PydanticValidationError as exc:
152
+ errors: list[JsonObject] = []
153
+ for issue in exc.errors():
154
+ loc = _object_field(issue, "loc")
155
+ field = "$"
156
+ if isinstance(loc, (list, tuple)) and loc:
157
+ field = str(loc[0])
158
+ elif isinstance(loc, str) and loc:
159
+ field = loc
160
+ message = _object_field(issue, "msg")
161
+ issue_type = _object_field(issue, "type")
162
+ errors.append(
163
+ {
164
+ "code": f"{field}_invalid_type",
165
+ "field": field,
166
+ "expected": "valid typed contract field",
167
+ "actual": str(message or issue_type or "invalid"),
168
+ }
169
+ )
170
+ return _SubagentOutputContractFields(), errors
171
+
172
+
173
+ def _diagnostic_context(fields: WorkflowGuardrailPayloadFields) -> JsonObject:
174
+ return dict(fields.diagnostic_context)
175
+
176
+
177
+ def _is_blocked_payload(payload: JsonObject) -> bool:
178
+ return blocked_payload_requires_next_action(payload)
179
+
180
+
181
+ def _default_contract_next_action(*, workflow: str, command: str) -> str:
182
+ command_route = f"wiki-cli:{command}" if command and not workflow else command
183
+ return default_contract_next_action(workflow=workflow, command=command_route)
184
+
185
+
186
+ def _invalid_contract_gap_payload(
187
+ *,
188
+ payload: JsonObject,
189
+ phase: str,
190
+ root_cause: str,
191
+ next_action: str,
192
+ ) -> JsonObject:
193
+ hardened = dict(_json_object(payload))
194
+ fields = _guardrail_fields(hardened)
195
+ hardened.update(
196
+ {
197
+ "status": "blocked",
198
+ "blocked_reason": root_cause,
199
+ "next_action": next_action,
200
+ "human_decision_required": False,
201
+ "required_inputs": list(fields.required_inputs),
202
+ "error_context": error_context(
203
+ phase=phase,
204
+ blocked_reason=root_cause,
205
+ root_cause=root_cause,
206
+ affected_artifact=fields.affected_artifact or phase,
207
+ error_summary=root_cause,
208
+ suggested_fix=next_action,
209
+ next_action=next_action,
210
+ retry_scope="restore_official_workflow_route",
211
+ ),
212
+ }
213
+ )
214
+ diagnostic = _diagnostic_context(fields)
215
+ diagnostic["root_cause_code"] = root_cause
216
+ if root_cause == "workflow.invalid_human_decision_packet":
217
+ diagnostic["decision_boundary_error"] = "WorkflowOutcomeError"
218
+ hardened["diagnostic_context"] = diagnostic
219
+ return hardened
220
+
221
+
222
+ def _is_legacy_schema_human_packet(packet: JsonObject) -> bool:
223
+ return _json_str_field(packet, "schema") == HUMAN_DECISION_PACKET_SCHEMA and "decision_summary" not in packet
224
+
225
+
226
+ def harden_operational_payload(
227
+ payload: JsonObject,
228
+ *,
229
+ workflow: str = "",
230
+ command: str = "",
231
+ require_error_context: bool = False,
232
+ ) -> JsonObject:
233
+ """Fail closed on blocked payloads that lack an actionable recovery contract."""
234
+ hardened = dict(_json_object(payload))
235
+ fields = _strict_guardrail_fields(hardened)
236
+ if not _is_blocked_payload(hardened):
237
+ return hardened
238
+
239
+ phase = fields.phase or command or "unknown"
240
+ original_blocked_reason = fields.blocked_reason
241
+ current_status = fields.status
242
+ current_blocked_reason = fields.blocked_reason
243
+ current_next_action = fields.next_action
244
+ current_required_inputs = list(fields.required_inputs)
245
+ current_human_decision_required = fields.human_decision_required
246
+ current_error_context = fields.error_context
247
+
248
+ if fields.human_decision_required:
249
+ from mednotes.domains.wiki.contracts.workflow_outcomes import WorkflowOutcomeError, attach_human_decision_packet
250
+
251
+ candidate_packets: list[JsonObject] = []
252
+ packet = fields.human_decision_packet
253
+ if packet is not None:
254
+ candidate_packets.append(packet)
255
+ candidate_packets.extend(fields.human_decision_packets)
256
+ packet_valid = False
257
+ for candidate in candidate_packets:
258
+ try:
259
+ attach_human_decision_packet(dict(hardened), packet=candidate)
260
+ except WorkflowOutcomeError:
261
+ continue
262
+ packet_valid = True
263
+ break
264
+ if not candidate_packets or not packet_valid or any(_is_legacy_schema_human_packet(item) for item in candidate_packets):
265
+ return _invalid_contract_gap_payload(
266
+ payload=hardened,
267
+ phase=phase,
268
+ root_cause="workflow.invalid_human_decision_packet",
269
+ next_action=(
270
+ "Gerar human_decision_packet pela API WorkflowDecision(kind='ask_human') "
271
+ "com evidencias e automacoes rejeitadas."
272
+ ),
273
+ )
274
+
275
+ if require_error_context and current_error_context is not None:
276
+ from mednotes.kernel.guardrails import validate_error_context
277
+
278
+ normalized_error_context = dict(current_error_context)
279
+ normalized_error_context.setdefault("phase", phase)
280
+ hardened["error_context"] = normalized_error_context
281
+ current_error_context = normalized_error_context
282
+ try:
283
+ validate_error_context(normalized_error_context)
284
+ except PydanticValidationError:
285
+ return _invalid_contract_gap_payload(
286
+ payload=hardened,
287
+ phase=phase,
288
+ root_cause="contract_gap.invalid_error_context",
289
+ next_action=(
290
+ "Emitir error_context completo com root_cause, affected_artifact, error_summary, "
291
+ "suggested_fix, next_action e retry_scope."
292
+ ),
293
+ )
294
+
295
+ missing_fields: list[str] = []
296
+ if not current_next_action.strip():
297
+ missing_fields.append("next_action")
298
+ current_blocked_reason = CONTRACT_GAP_MISSING_NEXT_ACTION
299
+ current_next_action = _default_contract_next_action(workflow=workflow, command=command)
300
+ current_status = "blocked"
301
+ hardened.update(
302
+ {
303
+ "blocked_reason": current_blocked_reason,
304
+ "next_action": current_next_action,
305
+ "status": current_status,
306
+ }
307
+ )
308
+ elif not current_status:
309
+ current_status = "blocked"
310
+ hardened.update({"status": current_status})
311
+
312
+ raw_required_inputs = _json_field(hardened, "required_inputs")
313
+ if "required_inputs" not in hardened or not isinstance(raw_required_inputs, list):
314
+ if missing_fields or require_error_context:
315
+ current_required_inputs = []
316
+ hardened["required_inputs"] = []
317
+ if "human_decision_required" not in hardened:
318
+ if missing_fields or require_error_context:
319
+ current_human_decision_required = False
320
+ hardened["human_decision_required"] = False
321
+
322
+ diagnostic = _diagnostic_context(fields)
323
+ if missing_fields:
324
+ diagnostic["root_cause_code"] = current_blocked_reason
325
+ diagnostic["contract_gap"] = {
326
+ "missing_fields": missing_fields,
327
+ "original_blocked_reason": original_blocked_reason,
328
+ "workflow": workflow,
329
+ "command": command,
330
+ }
331
+
332
+ needs_error_context = require_error_context or bool(missing_fields)
333
+ if needs_error_context and current_error_context is None:
334
+ root_cause = current_blocked_reason or CONTRACT_GAP_MISSING_ERROR_CONTEXT
335
+ if "error_context" not in missing_fields and require_error_context:
336
+ missing_fields.append("error_context")
337
+ missing_inputs = _missing_inputs_for_synthesized_error_context(
338
+ required_inputs=current_required_inputs,
339
+ missing_fields=missing_fields,
340
+ )
341
+ current_error_context = error_context(
342
+ phase=phase,
343
+ blocked_reason=root_cause,
344
+ root_cause=root_cause,
345
+ affected_artifact=fields.affected_artifact or phase,
346
+ error_summary=fields.error or fields.message or root_cause,
347
+ suggested_fix=current_next_action or _default_contract_next_action(workflow=workflow, command=command),
348
+ next_action=current_next_action or _default_contract_next_action(workflow=workflow, command=command),
349
+ retry_scope="restore_official_workflow_route",
350
+ missing_inputs=missing_inputs,
351
+ human_decision_required=current_human_decision_required,
352
+ )
353
+ hardened["error_context"] = current_error_context
354
+ if current_error_context is not None:
355
+ diagnostic["error_context"] = current_error_context
356
+ diagnostic.setdefault("root_cause_code", current_blocked_reason)
357
+ if diagnostic:
358
+ hardened["diagnostic_context"] = diagnostic
359
+ return _ensure_workflow_decision_boundary(hardened)
360
+
361
+
362
+ def _missing_inputs_for_synthesized_error_context(
363
+ *,
364
+ required_inputs: object,
365
+ missing_fields: list[str],
366
+ ) -> list[str]:
367
+ """Prefer workflow inputs over the diagnostic field when synthesizing recovery context."""
368
+
369
+ if missing_fields == ["error_context"] and isinstance(required_inputs, list):
370
+ return [str(item) for item in required_inputs]
371
+ return [str(item) for item in missing_fields]
372
+
373
+
374
+ def _ensure_workflow_decision_boundary(payload: JsonObject) -> JsonObject:
375
+ operational_payload = _json_object(payload)
376
+ fields = _strict_guardrail_fields(operational_payload)
377
+ if fields.status not in {"blocked", "failed"} and not fields.blocked_reason:
378
+ return operational_payload
379
+ if _json_object_field(operational_payload, "decision_summary") is not None:
380
+ return operational_payload
381
+ try:
382
+ packet = fields.human_decision_packet
383
+ if packet is not None and _json_object_field(packet, "decision_summary") is not None:
384
+ from mednotes.domains.wiki.contracts.workflow_outcomes import attach_human_decision_packet
385
+
386
+ return attach_human_decision_packet(dict(operational_payload), packet=packet)
387
+ for item in fields.human_decision_packets:
388
+ if _json_object_field(item, "decision_summary") is not None:
389
+ from mednotes.domains.wiki.contracts.workflow_outcomes import attach_human_decision_packet
390
+
391
+ return attach_human_decision_packet(dict(operational_payload), packet=item)
392
+ except Exception as exc:
393
+ hardened = dict(operational_payload)
394
+ diagnostic = _diagnostic_context(fields)
395
+ diagnostic["decision_boundary_error"] = exc.__class__.__name__
396
+ diagnostic.setdefault("root_cause_code", fields.blocked_reason or "workflow_decision_boundary_failed")
397
+ hardened["diagnostic_context"] = diagnostic
398
+ return hardened
399
+ return operational_payload
400
+
401
+
402
+ def _generalist_signal_path(value: JsonValue, *, path: str = "$") -> str:
403
+ if isinstance(value, dict):
404
+ for key, item in value.items():
405
+ key_path = f"{path}.{key}"
406
+ if str(key).lower() == "used_generalist" and item is True:
407
+ return key_path
408
+ found = _generalist_signal_path(item, path=key_path)
409
+ if found:
410
+ return found
411
+ elif isinstance(value, list):
412
+ for index, item in enumerate(value):
413
+ found = _generalist_signal_path(item, path=f"{path}.{index}")
414
+ if found:
415
+ return found
416
+ elif isinstance(value, str) and "generalist" in value.casefold():
417
+ return path
418
+ return ""
419
+
420
+
421
+ def subagent_output_contract_errors(
422
+ payload: JsonObject,
423
+ *,
424
+ expected_schema: str,
425
+ expected_workflow: str,
426
+ expected_phase: str,
427
+ allowed_agents: set[str] | list[str] | tuple[str, ...],
428
+ source_workflow: str,
429
+ ) -> list[JsonObject]:
430
+ """Return structural issues that make a subagent output unsafe to apply."""
431
+ allowed = {str(agent) for agent in allowed_agents}
432
+ fields, field_errors = _subagent_contract_fields(payload)
433
+ checks = [
434
+ ("schema", expected_schema),
435
+ ("workflow", expected_workflow),
436
+ ("phase", expected_phase),
437
+ ("source_workflow", source_workflow),
438
+ ]
439
+ errors: list[JsonObject] = list(field_errors)
440
+ actual_values = {
441
+ "schema": fields.schema_id,
442
+ "workflow": fields.workflow,
443
+ "phase": fields.phase,
444
+ "source_workflow": fields.source_workflow,
445
+ }
446
+ for field, expected in checks:
447
+ if any(_json_str_field(error, "field") == field for error in field_errors):
448
+ continue
449
+ actual = actual_values[field] or ""
450
+ if actual != expected:
451
+ errors.append(
452
+ {
453
+ "code": f"{field}_mismatch" if actual else f"{field}_missing",
454
+ "field": field,
455
+ "expected": expected,
456
+ "actual": actual,
457
+ }
458
+ )
459
+ agent = fields.agent or ""
460
+ if agent not in allowed:
461
+ if not any(_json_str_field(error, "field") == "agent" for error in field_errors):
462
+ errors.append(
463
+ {
464
+ "code": "agent_not_allowed" if agent else "agent_missing",
465
+ "field": "agent",
466
+ "expected": ",".join(sorted(allowed)),
467
+ "actual": agent,
468
+ }
469
+ )
470
+ generalist_path = _generalist_signal_path(payload)
471
+ if generalist_path:
472
+ errors.append(
473
+ {
474
+ "code": "generalist_forbidden",
475
+ "field": generalist_path,
476
+ "expected": "no generalist signal",
477
+ "actual": "generalist",
478
+ }
479
+ )
480
+ if _is_blocked_payload(payload) and fields.error_context is None:
481
+ errors.append(
482
+ {
483
+ "code": "error_context_missing",
484
+ "field": "error_context",
485
+ "expected": "object",
486
+ "actual": "missing",
487
+ }
488
+ )
489
+ return errors
490
+
491
+
492
+ def require_subagent_output_contract(
493
+ payload: JsonObject,
494
+ *,
495
+ expected_schema: str,
496
+ expected_workflow: str,
497
+ expected_phase: str,
498
+ allowed_agents: set[str] | list[str] | tuple[str, ...],
499
+ source_workflow: str,
500
+ ) -> None:
501
+ errors = subagent_output_contract_errors(
502
+ payload,
503
+ expected_schema=expected_schema,
504
+ expected_workflow=expected_workflow,
505
+ expected_phase=expected_phase,
506
+ allowed_agents=allowed_agents,
507
+ source_workflow=source_workflow,
508
+ )
509
+ if errors:
510
+ summary = "; ".join(f"{error['code']}({error['field']})" for error in errors)
511
+ raise ValidationError(f"{SUBAGENT_OUTPUT_CONTRACT_BLOCKED_REASON}: {summary}")
512
+
513
+
514
+ def error_context(
515
+ *,
516
+ phase: str,
517
+ blocked_reason: str,
518
+ root_cause: str,
519
+ affected_artifact: str,
520
+ error_summary: str,
521
+ suggested_fix: str,
522
+ next_action: str,
523
+ retry_scope: str,
524
+ affected_items: list[str] | None = None,
525
+ missing_inputs: list[str] | None = None,
526
+ max_attempts: int | None = None,
527
+ human_decision_required: bool = False,
528
+ ) -> JsonObject:
529
+ """Build the minimal retry context agents/subagents need to fix safely."""
530
+ return OperationalErrorContext(
531
+ phase=phase,
532
+ blocked_reason=blocked_reason,
533
+ root_cause=root_cause,
534
+ affected_artifact=affected_artifact,
535
+ error_summary=error_summary,
536
+ suggested_fix=suggested_fix,
537
+ next_action=next_action,
538
+ retry_scope=retry_scope,
539
+ human_decision_required=human_decision_required,
540
+ affected_items=list(affected_items or []),
541
+ missing_inputs=list(missing_inputs or []),
542
+ max_attempts=max_attempts,
543
+ ).to_payload()
544
+
545
+
546
+ def human_decision_packet(
547
+ *,
548
+ kind: str,
549
+ question: str,
550
+ options: list[JsonObject | str],
551
+ resume_action: str,
552
+ phase: str,
553
+ blocked_reason: str = "human_decision_required",
554
+ target_kind: str = "",
555
+ target_key: str = "",
556
+ context: JsonObject | None = None,
557
+ ) -> JsonObject:
558
+ """Build a closed, resumable decision packet for agents and telemetry."""
559
+ clean_kind = normalize_key(kind).replace(" ", "_") or "manual_review"
560
+ clean_options: list[JsonObject] = []
561
+ for index, option in enumerate(options, start=1):
562
+ if isinstance(option, dict):
563
+ label = (
564
+ _json_stringified_field(option, "label")
565
+ or _json_stringified_field(option, "value")
566
+ or _json_stringified_field(option, "id")
567
+ or f"Opção {index}"
568
+ )
569
+ option_id = _json_stringified_field(option, "id") or normalize_key(label).replace(" ", "_") or f"option_{index}"
570
+ clean: JsonObject = {"id": option_id, "label": label}
571
+ for key in ("description", "consequence", "value", "resume_action"):
572
+ value = _json_field(option, key)
573
+ if value:
574
+ clean[key] = str(value)
575
+ else:
576
+ label = str(option)
577
+ clean = {
578
+ "id": normalize_key(label).replace(" ", "_") or f"option_{index}",
579
+ "label": label,
580
+ "value": label,
581
+ }
582
+ clean_options.append(clean)
583
+ packet: JsonObject = {
584
+ "schema": HUMAN_DECISION_PACKET_SCHEMA,
585
+ "kind": clean_kind,
586
+ "type": clean_kind,
587
+ "status": "pending",
588
+ "phase": phase,
589
+ "blocked_reason": blocked_reason,
590
+ "question": question,
591
+ "options": clean_options,
592
+ "resume_action": resume_action,
593
+ }
594
+ if target_kind:
595
+ packet["target_kind"] = target_kind
596
+ if target_key:
597
+ packet["target_key"] = target_key
598
+ if context:
599
+ packet["context"] = context
600
+ return packet
601
+
602
+
603
+ def note_target_index(wiki_dir: Path, *, as_relative: bool = False) -> dict[str, list[Path | str]]:
604
+ """Index existing note stems by Obsidian-style normalized target key."""
605
+ targets: dict[str, list[Path | str]] = {}
606
+ if not wiki_dir.exists():
607
+ return targets
608
+ for path in iter_notes(wiki_dir):
609
+ display: Path | str
610
+ if as_relative:
611
+ try:
612
+ display = path.relative_to(wiki_dir).as_posix()
613
+ except ValueError:
614
+ display = str(path)
615
+ else:
616
+ display = path
617
+ targets.setdefault(normalize_key(path.stem), []).append(display)
618
+ return targets
619
+
620
+
621
+ def plan_status(*, item_count: int, blocked_item_count: int) -> tuple[str, str, bool]:
622
+ """Return status, next action and whether a human decision is needed."""
623
+ if item_count == 0 and blocked_item_count:
624
+ return (
625
+ "blocked",
626
+ "Revisar os blocked_items, corrigir as precondições e planejar novamente.",
627
+ True,
628
+ )
629
+ if item_count and blocked_item_count:
630
+ return (
631
+ "ready_with_blockers",
632
+ "Executar apenas os work_items liberados e tratar os blocked_items antes do próximo lote.",
633
+ True,
634
+ )
635
+ if item_count:
636
+ return ("ready", "Executar somente os work_items deste plano e consolidar serialmente depois.", False)
637
+ return ("completed", "Nenhum item pendente para esta fase.", False)