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,624 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import { pruneHookEventFilesSync } from "./retention.mjs";
8
+ import { allow, deny } from "./runtime.mjs";
9
+
10
+ const STATE_SUBDIR = path.join(".gemini", "medical-notes-workbench");
11
+ const LEASE_SUBDIR = path.join("vault-guard", "leases");
12
+ const DENY_REASON =
13
+ "Bloqueei esta alteração porque o vault ainda não tem um ponto de restauração ativo para este run. Rode `uv run python scripts/vault/vault_git.py run-start --agent gemini-cli --workflow <workflow> --json` e repita a operação.";
14
+ const DENY_DETAILS = {
15
+ status: "blocked_vault_guard_required",
16
+ blocked_reason: "vault_guard_required",
17
+ human_message: "Bloqueei esta alteração porque ainda não existe ponto de restauração ativo para este run.",
18
+ next_action:
19
+ "Abrir um ponto de restauração para este run e repetir a operação. Faça isso uma vez por lote, não por nota.",
20
+ agent_message:
21
+ "Abra o guard com run-start uma vez por lote no começo do workflow, execute todas as mutações, e feche com run-finish uma vez por lote no final.",
22
+ recovery_command:
23
+ "uv run python scripts/vault/vault_git.py run-start --agent gemini-cli --workflow <workflow> --json",
24
+ required_inputs: ["agent", "workflow"],
25
+ human_decision_required: false,
26
+ };
27
+ const DENY_DIRECT_GIT_REASON =
28
+ "Bloqueei este comando porque operações Git diretas no vault devem passar por `scripts/vault/vault_git.py`, mesmo com ponto de restauração ativo.";
29
+ const DENY_DIRECT_GIT_DETAILS = {
30
+ status: "blocked_direct_mutation_forbidden",
31
+ blocked_reason: "direct_mutation_forbidden",
32
+ human_message: "Operação Git direta no vault bloqueada. Use o fluxo oficial de proteção/restauração do vault.",
33
+ next_action:
34
+ "Use `uv run python scripts/vault/vault_git.py run-finish --agent gemini-cli --workflow <workflow> --title <titulo> --json` para registrar mudanças.",
35
+ agent_message:
36
+ "Não rode git add/commit/push/reset/clean diretamente no vault. Use vault_git.py para preservar restore points, leases e backup online.",
37
+ recovery_command:
38
+ "uv run python scripts/vault/vault_git.py run-finish --agent gemini-cli --workflow <workflow> --title <titulo> --json",
39
+ required_inputs: ["title"],
40
+ human_decision_required: false,
41
+ };
42
+ const DENY_GENERATED_SCRIPT_REASON =
43
+ "Bloqueei este comando porque scripts de reparo gerados não podem modificar o vault ou artefatos do workflow. Use a rota oficial do Workbench.";
44
+ const DENY_GENERATED_SCRIPT_DETAILS = {
45
+ status: "blocked_generated_script_workaround_forbidden",
46
+ blocked_reason: "generated_script_workaround_forbidden",
47
+ human_message:
48
+ "Script de reparo manual bloqueado. Correções de raw chat, coverage, manifest, staged notes, links ou Wiki devem passar pelo CLI oficial.",
49
+ next_action:
50
+ "Use `uv run python scripts/mednotes/wiki/cli.py <comando-oficial> --json` ou bloqueie com error_context se não houver rota oficial.",
51
+ agent_message:
52
+ "Não crie nem execute scripts ad hoc para editar raw chats, coverage, manifests, staged notes, Related Notes, WikiLinks ou notas do vault.",
53
+ recovery_command: "uv run python scripts/mednotes/wiki/cli.py <comando-oficial> --json",
54
+ required_inputs: ["official_workflow_command"],
55
+ human_decision_required: false,
56
+ };
57
+ const DENY_DIRECT_RAW_REASON =
58
+ "Bloqueei esta alteração porque raw chats não podem ser editados diretamente. Metadados YAML/status só devem ser alterados pelo CLI oficial do Workbench.";
59
+ const DENY_DIRECT_RAW_DETAILS = {
60
+ status: "blocked_direct_raw_chat_edit_forbidden",
61
+ blocked_reason: "direct_raw_chat_edit_forbidden",
62
+ human_message:
63
+ "Raw chat bloqueado contra edição direta. O conteúdo do chat é imutável; metadados YAML/status devem passar pelo script oficial.",
64
+ next_action:
65
+ "Use `wiki/cli.py triage`, `wiki/cli.py discard` ou `wiki/cli.py publish-batch` via `scripts/run_python.mjs`, conforme a etapa do workflow.",
66
+ agent_message:
67
+ "Não use write_file, replace, sed, echo, redirecionamento shell ou script ad hoc em Chats_Raw. Se precisar mudar status/YAML, use a porta oficial wiki/cli.py.",
68
+ recovery_command:
69
+ 'node "${extensionPath}/scripts/run_python.mjs" "${extensionPath}/scripts/mednotes/wiki/cli.py" <triage|discard|publish-batch> --json',
70
+ required_inputs: ["official_workflow_command"],
71
+ human_decision_required: false,
72
+ };
73
+ const DENY_WORKFLOW_ARTIFACT_REASON =
74
+ "Bloqueei esta alteração porque artefatos de workflow devem ser gerados pelo CLI oficial do Workbench, não por escrita direta do agente.";
75
+ const DENY_WORKFLOW_ARTIFACT_DETAILS = {
76
+ status: "blocked_workflow_artifact_direct_write_forbidden",
77
+ blocked_reason: "workflow_artifact_direct_write_forbidden",
78
+ human_message:
79
+ "Artefato de workflow bloqueado contra edição direta. Plans, manifests, receipts e reports precisam ser gerados pela rota oficial.",
80
+ next_action:
81
+ "Reexecute o comando oficial de wiki/cli.py que gera este artefato; não use write_file, replace, edit ou multiedit em runs/*.json.",
82
+ agent_message:
83
+ "Não fabrique nem edite artefatos de workflow manualmente. Use wiki/cli.py para gerar plans, manifests, receipts, reports e diagnoses.",
84
+ recovery_command: "uv run python scripts/mednotes/wiki/cli.py <comando-oficial> --json",
85
+ required_inputs: ["official_workflow_command"],
86
+ human_decision_required: false,
87
+ };
88
+ const DENY_INSTALLED_EXTENSION_EDIT_REASON =
89
+ "Bloqueei esta alteração porque o bundle instalado da extensão não é a fonte de verdade. Corrija o arquivo fonte no repositório e reinstale/atualize a extensão.";
90
+ const DENY_INSTALLED_EXTENSION_EDIT_DETAILS = {
91
+ status: "blocked_installed_extension_runtime_edit_forbidden",
92
+ blocked_reason: "installed_extension_runtime_edit_forbidden",
93
+ human_message:
94
+ "Edição direta do bundle instalado bloqueada. A correção precisa ser feita no repositório fonte e distribuída pela rota oficial.",
95
+ next_action:
96
+ "Patch canonical source under bundle/ in the repository, then rebuild and reinstall/update the extension.",
97
+ agent_message:
98
+ "Do not edit ~/.gemini/extensions/medical-notes-workbench, ~/.gemini/config/plugins/medical-notes-workbench or Windows equivalents. Report the source file under bundle/.",
99
+ required_inputs: ["canonical_source_patch"],
100
+ human_decision_required: false,
101
+ };
102
+ const DENY_UNSUPPORTED_TOOL_PARAMETER_REASON =
103
+ "Bloqueei esta chamada porque ela contém parâmetro que não existe no contrato da tool.";
104
+ const DENY_PUBLIC_DEV_ESCAPE_REASON =
105
+ "Bloqueei este comando porque escapes de desenvolvedor não podem ser usados em workflow público.";
106
+
107
+ const WRITE_TOOLS = new Set(["write_file", "write", "replace", "edit", "multiedit"]);
108
+ const SHELL_TOOLS = new Set(["run_shell_command", "run_shell", "shelltool", "bash", "shell", "powershell", "pwsh"]);
109
+ const UNSUPPORTED_TOOL_PARAMETERS = new Set(["wait_for_previous"]);
110
+ const WORKFLOW_ARTIFACT_NAME_RE =
111
+ /(^|[-_])(plan|manifest|receipt|report|diagnosis|trigger-context|trigger_context|run_state)([-_.]|$)/i;
112
+
113
+ function homeDir() {
114
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
115
+ }
116
+
117
+ function stateDir() {
118
+ return path.join(homeDir(), STATE_SUBDIR);
119
+ }
120
+
121
+ function readFirstLine(filePath) {
122
+ try {
123
+ return fs
124
+ .readFileSync(filePath, "utf8")
125
+ .split(/\r?\n/)
126
+ .map((line) => line.trim())
127
+ .find(Boolean);
128
+ } catch {
129
+ return "";
130
+ }
131
+ }
132
+
133
+ function normalizeForCompare(value) {
134
+ const normalized = path.normalize(String(value || ""));
135
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
136
+ }
137
+
138
+ function canonicalPath(value) {
139
+ if (!value) return "";
140
+ const absolute = path.resolve(String(value));
141
+ try {
142
+ return fs.realpathSync.native(absolute);
143
+ } catch {
144
+ const parent = path.dirname(absolute);
145
+ try {
146
+ return path.join(fs.realpathSync.native(parent), path.basename(absolute));
147
+ } catch {
148
+ return absolute;
149
+ }
150
+ }
151
+ }
152
+
153
+ function isInsideOrSame(candidate, root) {
154
+ if (!candidate || !root) return false;
155
+ const left = normalizeForCompare(canonicalPath(candidate));
156
+ const right = normalizeForCompare(canonicalPath(root));
157
+ return left === right || left.startsWith(right.endsWith(path.sep) ? right : `${right}${path.sep}`);
158
+ }
159
+
160
+ function installedExtensionRoot() {
161
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
162
+ }
163
+
164
+ function looksLikeInstalledExtensionRoot(root) {
165
+ const normalized = normalizeForCompare(root).replace(/[\\/]+/g, "/");
166
+ const home = normalizeForCompare(homeDir()).replace(/[\\/]+/g, "/");
167
+ return (
168
+ normalized === `${home}/.gemini/extensions/medical-notes-workbench` ||
169
+ normalized.startsWith(`${home}/.gemini/extensions/medical-notes-workbench/`) ||
170
+ normalized === `${home}/.gemini/config/plugins/medical-notes-workbench` ||
171
+ normalized.startsWith(`${home}/.gemini/config/plugins/medical-notes-workbench/`)
172
+ );
173
+ }
174
+
175
+ function configuredInstalledExtensionRoots() {
176
+ const currentRoot = installedExtensionRoot();
177
+ const roots = [
178
+ path.join(homeDir(), ".gemini", "extensions", "medical-notes-workbench"),
179
+ path.join(homeDir(), ".gemini", "config", "plugins", "medical-notes-workbench"),
180
+ ];
181
+ if (looksLikeInstalledExtensionRoot(currentRoot)) {
182
+ roots.unshift(currentRoot);
183
+ }
184
+ return [...new Set(roots.filter(Boolean).map((root) => path.resolve(root)))];
185
+ }
186
+
187
+ function isInsideInstalledExtensionRoot(candidate) {
188
+ return configuredInstalledExtensionRoots().some((root) => isInsideOrSame(candidate, root));
189
+ }
190
+
191
+ function configuredVaultPath() {
192
+ return readFirstLine(path.join(stateDir(), "vault.path"));
193
+ }
194
+
195
+ function configuredRawDirs() {
196
+ const candidates = [];
197
+ for (const envName of ["MED_RAW_DIR", "MEDNOTES_RAW_DIR"]) {
198
+ const value = process.env[envName];
199
+ if (value) candidates.push(expandConfiguredPath(value));
200
+ }
201
+ const configPath = configuredAppConfigPath();
202
+ const rawDir =
203
+ readTomlPathValue(configPath, "paths", "raw_dir") || readTomlPathValue(configPath, "chat_processor", "raw_dir");
204
+ if (rawDir) candidates.push(expandConfiguredPath(rawDir, configPath));
205
+ return [...new Set(candidates.filter(Boolean).map((candidate) => path.resolve(candidate)))];
206
+ }
207
+
208
+ function configuredAppConfigPath() {
209
+ const configured = process.env.MEDNOTES_CONFIG;
210
+ if (configured) return path.resolve(expandConfiguredPath(configured));
211
+ return path.join(stateDir(), "config.toml");
212
+ }
213
+
214
+ function expandConfiguredPath(value, configPath = "") {
215
+ const text = String(value || "").trim();
216
+ if (!text) return "";
217
+ const expanded = text.startsWith("~") ? path.join(homeDir(), text.slice(1)) : text;
218
+ if (path.isAbsolute(expanded)) return expanded;
219
+ if (configPath) return path.resolve(path.dirname(configPath), expanded);
220
+ return path.resolve(expanded);
221
+ }
222
+
223
+ function readTomlPathValue(configPath, sectionName, keyName) {
224
+ if (!configPath) return "";
225
+ let text = "";
226
+ try {
227
+ text = fs.readFileSync(configPath, "utf8");
228
+ } catch {
229
+ return "";
230
+ }
231
+ let activeSection = "";
232
+ for (const line of text.split(/\r?\n/)) {
233
+ const section = line.match(/^\s*\[([^\]]+)]\s*(?:#.*)?$/);
234
+ if (section) {
235
+ activeSection = section[1].trim();
236
+ continue;
237
+ }
238
+ if (activeSection !== sectionName) continue;
239
+ const match = line.match(new RegExp(`^\\s*${keyName}\\s*=\\s*(['"])(.*?)\\1\\s*(?:#.*)?$`));
240
+ if (match) return unescapeTomlBasicString(match[2], match[1]);
241
+ }
242
+ return "";
243
+ }
244
+
245
+ function unescapeTomlBasicString(value, quote) {
246
+ if (quote === "'") return value;
247
+ return String(value || "")
248
+ .replace(/\\n/g, "\n")
249
+ .replace(/\\t/g, "\t")
250
+ .replace(/\\"/g, '"')
251
+ .replace(/\\\\/g, "\\");
252
+ }
253
+
254
+ export function normalizedToolName(name) {
255
+ return String(name || "")
256
+ .trim()
257
+ .toLowerCase();
258
+ }
259
+
260
+ export function toolInput(payload) {
261
+ if (!payload || typeof payload !== "object") return {};
262
+ for (const input of [payload.tool_input, payload.toolInput, payload.input, payload.parameters, payload.args]) {
263
+ if (input && typeof input === "object" && Object.keys(input).length > 0) return input;
264
+ }
265
+ return {};
266
+ }
267
+
268
+ export function isWriteTool(toolName) {
269
+ return WRITE_TOOLS.has(normalizedToolName(toolName));
270
+ }
271
+
272
+ export function isShellTool(toolName) {
273
+ return SHELL_TOOLS.has(normalizedToolName(toolName));
274
+ }
275
+
276
+ export function shellLooksMutating(command) {
277
+ const text = String(command || "");
278
+ if (!text.trim()) return false;
279
+ return (
280
+ /\b(uv\s+run\s+python|python3?|node|npm|powershell|pwsh|bash|sh)\b/i.test(text) ||
281
+ /\b(sed\s+-i|perl\s+-pi|rm|del|move|mv|copy|cp|Set-Content|Out-File|Add-Content|Remove-Item|Copy-Item|Move-Item|Rename-Item|New-Item|Clear-Content|tee)\b/i.test(
282
+ text,
283
+ ) ||
284
+ /(^|[^>])>>?($|[^>])/.test(text) ||
285
+ /\bgit\s+(add|commit|push|checkout|reset|clean|revert|merge|rebase)\b/i.test(text)
286
+ );
287
+ }
288
+
289
+ export function shellLooksDirectGitMutation(command) {
290
+ return /\bgit\s+(add|commit|push|checkout|reset|clean|revert|merge|rebase)\b/i.test(String(command || ""));
291
+ }
292
+
293
+ function runsPythonViaUv(command) {
294
+ return /(?:^|\s)uv\s+run(?:\s+(?:--project|--with)\s+(?:"[^"]+"|'[^']+'|\S+))*\s+python\s+/i.test(
295
+ String(command || ""),
296
+ );
297
+ }
298
+
299
+ function runsWikiCliViaRunPythonWrapper(command) {
300
+ const text = String(command || "");
301
+ return (
302
+ /(?:^|\s)node(?:\.exe)?\s+(?:"[^"]*scripts[\\/]run_python\.mjs"|'[^']*scripts[\\/]run_python\.mjs'|\S*scripts[\\/]run_python\.mjs)\s+(?:"[^"]*(?:extension[\\/])?scripts[\\/]mednotes[\\/]wiki[\\/]cli\.py"|'[^']*(?:extension[\\/])?scripts[\\/]mednotes[\\/]wiki[\\/]cli\.py'|\S*(?:extension[\\/])?scripts[\\/]mednotes[\\/]wiki[\\/]cli\.py)\s+/i.test(
303
+ text,
304
+ ) ||
305
+ /(?:^|\s)node(?:\.exe)?\s+(?:"[^"]*scripts[\\/]run_python\.mjs"|'[^']*scripts[\\/]run_python\.mjs'|\S*scripts[\\/]run_python\.mjs)\s+(?:"[^"]*wiki[\\/]cli\.py"|'[^']*wiki[\\/]cli\.py'|\S*wiki[\\/]cli\.py)\s+/i.test(
306
+ text,
307
+ )
308
+ );
309
+ }
310
+
311
+ export function isPrivilegedVaultGitCommand(command) {
312
+ const text = String(command || "");
313
+ if (/vault_(precommit|commit)\.(sh|ps1)\b/.test(text)) return true;
314
+ if (!runsPythonViaUv(text)) return false;
315
+ if (!/scripts[\\/]+vault[\\/]+/.test(text)) return false;
316
+ return /vault_git\.py["']?\s+(setup|run-start|run-finish|timeline|restore-preview|restore-apply|guard-status)\b/.test(
317
+ text,
318
+ );
319
+ }
320
+
321
+ export function isOfficialWorkflowCommand(command) {
322
+ const text = String(command || "");
323
+ return (
324
+ isPrivilegedVaultGitCommand(text) ||
325
+ runsWikiCliViaRunPythonWrapper(text) ||
326
+ (runsPythonViaUv(text) &&
327
+ (/(?:^|\s)uv\s+run(?:\s+(?:--project|--with)\s+(?:"[^"]+"|'[^']+'|\S+))*\s+python\s+["']?[^"'\s]*(?:extension[\\/])?scripts[\\/]mednotes[\\/]wiki[\\/]cli\.py["']?\s+/i.test(
328
+ text,
329
+ ) ||
330
+ /(?:^|\s)uv\s+run(?:\s+(?:--project|--with)\s+(?:"[^"]+"|'[^']+'|\S+))*\s+python\s+["']?[^"'\s]*wiki[\\/]cli\.py["']?\s+/i.test(
331
+ text,
332
+ )))
333
+ );
334
+ }
335
+
336
+ export function shellLooksGeneratedWorkflowBypass(command) {
337
+ const text = String(command || "");
338
+ if (!text.trim() || isOfficialWorkflowCommand(text)) return false;
339
+ const runsInterpreter = /\b(uv\s+run\s+python|python3?|node|npm|powershell|pwsh|bash|sh)\b/i.test(text);
340
+ const mentionsWorkflowArtifact =
341
+ /\b(raw|coverage|manifest|stage(?:d)?|wiki|wikilink|related[-_ ]?notes|linker|vault|note|markdown|frontmatter)\b/i.test(
342
+ text,
343
+ ) ||
344
+ /\b(repair|fix|rewrite|migrate|merge|sync|cleanup|normalize|dedupe|publish)\b/i.test(text) ||
345
+ /\.(py|js|mjs|ps1|sh)\b/i.test(text);
346
+ return runsInterpreter && mentionsWorkflowArtifact;
347
+ }
348
+
349
+ export function targetPathsFromPayload(payload) {
350
+ const input = toolInput(payload);
351
+ const cwd = payload?.cwd ? String(payload.cwd) : process.cwd();
352
+ const candidates = [input.file_path, input.path, input.absolute_path, input.target_path, input.output].filter(
353
+ Boolean,
354
+ );
355
+ return candidates.map((candidate) => path.resolve(cwd, String(candidate)));
356
+ }
357
+
358
+ function firstString(...values) {
359
+ for (const value of values) {
360
+ if (typeof value === "string" && value.length > 0) return value;
361
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
362
+ }
363
+ return "";
364
+ }
365
+
366
+ function shellCommand(input) {
367
+ return firstString(input.command, input.cmd, input.script);
368
+ }
369
+
370
+ function unsupportedToolParameter(input) {
371
+ for (const key of Object.keys(input || {})) {
372
+ if (UNSUPPORTED_TOOL_PARAMETERS.has(key)) return key;
373
+ }
374
+ return "";
375
+ }
376
+
377
+ function shellLooksPublicDevEscape(command) {
378
+ const text = String(command || "");
379
+ return /\bMEDNOTES_ALLOW_DEV_ESCAPE\s*=\s*(?:1|true|yes)\b/i.test(text) || /\b--skip-prompt-eval\b/i.test(text);
380
+ }
381
+
382
+ function commandMentionsVault(command, vaultDir) {
383
+ const text = String(command || "");
384
+ if (!text || !vaultDir) return false;
385
+ const raw = path.resolve(String(vaultDir));
386
+ const real = canonicalPath(raw);
387
+ return text.includes(raw) || (real && text.includes(real));
388
+ }
389
+
390
+ function targetsRawChatMarkdown(candidate, rawDirs) {
391
+ if (!candidate || path.extname(String(candidate)).toLowerCase() !== ".md") return false;
392
+ return rawDirs.some((rawDir) => isInsideOrSame(candidate, rawDir));
393
+ }
394
+
395
+ function commandMentionsPath(command, targetDir) {
396
+ const text = String(command || "");
397
+ if (!text || !targetDir) return false;
398
+ const raw = path.resolve(String(targetDir));
399
+ const real = canonicalPath(raw);
400
+ return text.includes(raw) || (real && text.includes(real));
401
+ }
402
+
403
+ function shellRedirectionTargets(command, cwd) {
404
+ const targets = [];
405
+ const text = String(command || "");
406
+ const redirectPattern = /(?:^|\s)(?:>>?|1>|2>|&>)\s*(?:"([^"]+)"|'([^']+)'|(\S+))/g;
407
+ let match;
408
+ // biome-ignore lint/suspicious/noAssignInExpressions: canonical regex-exec iteration idiom (assign-and-test in while).
409
+ while ((match = redirectPattern.exec(text)) !== null) {
410
+ const value = match[1] || match[2] || match[3] || "";
411
+ if (!value || value.startsWith("&")) continue;
412
+ targets.push(path.resolve(cwd || process.cwd(), value));
413
+ }
414
+ return targets;
415
+ }
416
+
417
+ function shellLooksDirectRawPathMutation(command) {
418
+ return /\b(sed\s+-i|perl\s+-pi|rm|del|move|mv|copy|cp|Set-Content|Out-File|Add-Content|Remove-Item|Copy-Item|Move-Item|Rename-Item|Clear-Content)\b/i.test(
419
+ String(command || ""),
420
+ );
421
+ }
422
+
423
+ function commandWritesConfiguredRawDir(command, cwd, rawDirs) {
424
+ const redirectedTargets = shellRedirectionTargets(command, cwd);
425
+ if (redirectedTargets.some((candidate) => targetsRawChatMarkdown(candidate, rawDirs))) return true;
426
+ return shellLooksDirectRawPathMutation(command) && rawDirs.some((rawDir) => commandMentionsPath(command, rawDir));
427
+ }
428
+
429
+ function targetsConfiguredRawDir(payload, toolName, cwd, command, rawDirs) {
430
+ if (rawDirs.length === 0) return false;
431
+ if (isWriteTool(toolName)) {
432
+ return targetPathsFromPayload(payload).some((candidate) => targetsRawChatMarkdown(candidate, rawDirs));
433
+ }
434
+ if (!isShellTool(toolName) || !shellLooksMutating(command)) return false;
435
+ if (isOfficialWorkflowCommand(command)) return false;
436
+ return rawDirs.some((rawDir) => isInsideOrSame(cwd, rawDir)) || commandWritesConfiguredRawDir(command, cwd, rawDirs);
437
+ }
438
+
439
+ function looksLikeWorkflowArtifactPath(candidate) {
440
+ const normalized = String(candidate || "").replace(/\\/g, "/");
441
+ const name = normalized.split("/").pop() || "";
442
+ return normalized.includes("/runs/") && name.endsWith(".json") && WORKFLOW_ARTIFACT_NAME_RE.test(name);
443
+ }
444
+
445
+ function targetsWorkflowArtifact(payload, toolName) {
446
+ if (!isWriteTool(toolName)) return false;
447
+ return targetPathsFromPayload(payload).some((candidate) => looksLikeWorkflowArtifactPath(candidate));
448
+ }
449
+
450
+ export async function activeLeasesForVault(vaultDir) {
451
+ const leaseDir = path.join(stateDir(), LEASE_SUBDIR);
452
+ let entries = [];
453
+ try {
454
+ entries = fs.readdirSync(leaseDir);
455
+ } catch {
456
+ return [];
457
+ }
458
+ const now = Date.now();
459
+ const active = [];
460
+ for (const entry of entries) {
461
+ if (!entry.endsWith(".json")) continue;
462
+ try {
463
+ const lease = JSON.parse(fs.readFileSync(path.join(leaseDir, entry), "utf8"));
464
+ if (lease.status !== "active") continue;
465
+ if (
466
+ !isInsideOrSame(String(lease.vault_dir || ""), vaultDir) ||
467
+ !isInsideOrSame(vaultDir, String(lease.vault_dir || ""))
468
+ ) {
469
+ continue;
470
+ }
471
+ const expiresAt = Date.parse(String(lease.expires_at || ""));
472
+ if (Number.isFinite(expiresAt) && expiresAt <= now) continue;
473
+ active.push(lease);
474
+ } catch {
475
+ // Ignore malformed leases; hooks must not fail closed because of corrupt local state.
476
+ }
477
+ }
478
+ return active;
479
+ }
480
+
481
+ export async function guardVaultBefore(payload) {
482
+ const toolName =
483
+ payload?.tool_name || payload?.toolName || payload?.name || payload?.tool || payload?.original_request_name;
484
+ const input = toolInput(payload);
485
+ const cwd = payload?.cwd ? String(payload.cwd) : "";
486
+ const command = shellCommand(input);
487
+ const rawDirs = configuredRawDirs();
488
+
489
+ const badParam = unsupportedToolParameter(input);
490
+ if (badParam) {
491
+ recordVaultGuardEvent(payload, { status: "blocked", error: "unsupported_tool_parameter" });
492
+ return deny(DENY_UNSUPPORTED_TOOL_PARAMETER_REASON, {
493
+ status: "blocked_unsupported_tool_parameter",
494
+ blocked_reason: "unsupported_tool_parameter",
495
+ bad_param: badParam,
496
+ human_message: "O agente tentou usar um parâmetro inexistente da ferramenta.",
497
+ next_action: "Remova o parâmetro e aguarde o resultado da tool anterior antes de emitir a próxima chamada.",
498
+ agent_message:
499
+ "Do not pass wait_for_previous or other undocumented tool parameters. Sequence by waiting for the previous tool result.",
500
+ required_inputs: [],
501
+ human_decision_required: false,
502
+ });
503
+ }
504
+
505
+ if (isShellTool(toolName) && shellLooksPublicDevEscape(command)) {
506
+ recordVaultGuardEvent(payload, { status: "blocked", error: "public_dev_escape_forbidden" });
507
+ return deny(DENY_PUBLIC_DEV_ESCAPE_REASON, {
508
+ status: "blocked_public_dev_escape_forbidden",
509
+ blocked_reason: "public_dev_escape_forbidden",
510
+ human_message: "Escape técnico bloqueado em workflow público.",
511
+ next_action:
512
+ "Pare e retome pela rota oficial com recibo/proveniência tipados; não use MEDNOTES_ALLOW_DEV_ESCAPE nem --skip-prompt-eval.",
513
+ agent_message:
514
+ "Developer escapes are forbidden in public workflows. Report the typed blocker instead of bypassing it.",
515
+ required_inputs: [],
516
+ human_decision_required: false,
517
+ });
518
+ }
519
+
520
+ if (
521
+ isWriteTool(toolName) &&
522
+ targetPathsFromPayload(payload).some((candidate) => isInsideInstalledExtensionRoot(candidate))
523
+ ) {
524
+ recordVaultGuardEvent(payload, { status: "blocked", error: "installed_extension_runtime_edit_forbidden" });
525
+ return deny(DENY_INSTALLED_EXTENSION_EDIT_REASON, DENY_INSTALLED_EXTENSION_EDIT_DETAILS);
526
+ }
527
+
528
+ if (targetsWorkflowArtifact(payload, toolName)) {
529
+ recordVaultGuardEvent(payload, { status: "blocked", error: "workflow_artifact_direct_write_forbidden" });
530
+ return deny(DENY_WORKFLOW_ARTIFACT_REASON, DENY_WORKFLOW_ARTIFACT_DETAILS);
531
+ }
532
+
533
+ if (targetsConfiguredRawDir(payload, toolName, cwd, command, rawDirs)) {
534
+ recordVaultGuardEvent(payload, { status: "blocked", error: "direct_raw_chat_edit_forbidden" });
535
+ return deny(DENY_DIRECT_RAW_REASON, DENY_DIRECT_RAW_DETAILS);
536
+ }
537
+
538
+ const vaultDir = configuredVaultPath();
539
+ if (!vaultDir) return allow();
540
+
541
+ if (isShellTool(toolName) && isPrivilegedVaultGitCommand(command)) return allow();
542
+
543
+ let targetsVault = false;
544
+ if (isWriteTool(toolName)) {
545
+ targetsVault = targetPathsFromPayload(payload).some((candidate) => isInsideOrSame(candidate, vaultDir));
546
+ } else if (isShellTool(toolName) && shellLooksMutating(command)) {
547
+ targetsVault = isInsideOrSame(cwd, vaultDir) || commandMentionsVault(command, vaultDir);
548
+ }
549
+
550
+ if (!targetsVault) return allow();
551
+ if (isShellTool(toolName) && shellLooksDirectGitMutation(command)) {
552
+ recordVaultGuardEvent(payload, { status: "blocked", error: "direct_mutation_forbidden" });
553
+ return deny(DENY_DIRECT_GIT_REASON, DENY_DIRECT_GIT_DETAILS);
554
+ }
555
+ if (maintainerBypassEnabled()) {
556
+ recordVaultGuardEvent(payload, { status: "bypassed", error: "maintainer_bypass" });
557
+ return allow({ bypassed: true });
558
+ }
559
+ const active = await activeLeasesForVault(vaultDir);
560
+ if (active.length > 0) {
561
+ if (isShellTool(toolName) && shellLooksGeneratedWorkflowBypass(command)) {
562
+ recordVaultGuardEvent(payload, { status: "blocked", error: "generated_script_workaround_forbidden" });
563
+ return deny(DENY_GENERATED_SCRIPT_REASON, DENY_GENERATED_SCRIPT_DETAILS);
564
+ }
565
+ return allow();
566
+ }
567
+ recordVaultGuardEvent(payload, { status: "blocked", error: "vault_guard_required" });
568
+ return deny(DENY_REASON, DENY_DETAILS);
569
+ }
570
+
571
+ function maintainerBypassEnabled() {
572
+ return (
573
+ process.env.MEDNOTES_VAULT_GUARD_DISABLE === "1" &&
574
+ String(process.env.MEDNOTES_VAULT_GUARD_DISABLE_REASON || "").trim().length > 0
575
+ );
576
+ }
577
+
578
+ function recordVaultGuardEvent(payload, { status, error }) {
579
+ try {
580
+ const event = {
581
+ schema: "medical-notes-workbench.agent-hook-event.v1",
582
+ event_id: crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString("hex"),
583
+ recorded_at: new Date().toISOString(),
584
+ hook_event_name: String(payload?.hook_event_name || "BeforeTool"),
585
+ session_id: String(payload?.session_id || ""),
586
+ transcript_path: compactPath(String(payload?.transcript_path || "")),
587
+ cwd: compactPath(String(payload?.cwd || process.cwd())),
588
+ tool_name: String(payload?.tool_name || ""),
589
+ tool_kind: "vault_guard",
590
+ original_request_name: String(payload?.original_request_name || ""),
591
+ generated_scripts: [],
592
+ command_events: [
593
+ {
594
+ command_family: "vault_guard",
595
+ status,
596
+ error,
597
+ capture_method: "guard-vault-before",
598
+ },
599
+ ],
600
+ };
601
+ const dir = path.join(feedbackRoot(), "hook-events");
602
+ fs.mkdirSync(dir, { recursive: true });
603
+ const file = path.join(dir, `${event.recorded_at.replace(/[:.]/g, "-")}-${event.event_id}.json`);
604
+ const tmp = `${file}.tmp`;
605
+ fs.writeFileSync(tmp, `${JSON.stringify(event, null, 2)}\n`, "utf8");
606
+ fs.renameSync(tmp, file);
607
+ pruneHookEventFilesSync(dir);
608
+ } catch {
609
+ // Hook event capture must never change the allow/deny decision.
610
+ }
611
+ }
612
+
613
+ function feedbackRoot() {
614
+ const configured = process.env.MEDNOTES_FEEDBACK_DIR || process.env.MEDICAL_NOTES_FEEDBACK_DIR;
615
+ if (configured) return configured;
616
+ return path.join(homeDir(), ".gemini", "medical-notes-workbench", "feedback");
617
+ }
618
+
619
+ function compactPath(value) {
620
+ const text = String(value || "");
621
+ if (!text) return "";
622
+ const home = homeDir();
623
+ return text.startsWith(home) ? `~${text.slice(home.length)}` : text;
624
+ }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "./mednotes_hook/cli.mjs";
4
+
5
+ await main();
@@ -0,0 +1,24 @@
1
+ """Bootstrap de sys.path para os entry-points distribuídos (chamados por caminho).
2
+
3
+ Os scripts em ``bundle/scripts`` são invocados por caminho
4
+ (``uv run python .../X.py``), sem o pacote instalado. Este helper garante que
5
+ tanto o runtime (``bundle/scripts/mednotes`` — pacotes wiki/flashcards/...)
6
+ quanto as libs (``bundle/src`` — o namespace ``mednotes.*``) estejam no
7
+ ``sys.path``. Fonte ÚNICA do path-bootstrap (ADR-0001 regra 10): em vez de cada
8
+ entry recalcular ``parents[N]``, todos chamam :func:`ensure_runtime_paths`.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ _RUNTIME = Path(__file__).resolve().parent # bundle/scripts/mednotes (pacotes do bounded context)
16
+ _LIB_SRC = _RUNTIME.parents[1] / "src" # bundle/src (namespace mednotes.*)
17
+
18
+
19
+ def ensure_runtime_paths() -> None:
20
+ """Põe o runtime e as libs no sys.path (idempotente)."""
21
+ for root in (_RUNTIME, _LIB_SRC):
22
+ marker = str(root)
23
+ if marker not in sys.path:
24
+ sys.path.insert(0, marker)
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env python3
2
+ """Public CLI alias for Anki model validation."""
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ SCRIPT_DIR = Path(__file__).resolve().parent
9
+ if str(SCRIPT_DIR) not in sys.path:
10
+ sys.path.insert(0, str(SCRIPT_DIR))
11
+ from _runtime_paths import ensure_runtime_paths # noqa: E402
12
+
13
+ ensure_runtime_paths()
14
+
15
+ from mednotes.domains.flashcards.model import main # noqa: E402
16
+
17
+ if __name__ == "__main__":
18
+ raise SystemExit(main())