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,148 @@
1
+ """Workbench attestation for subagent plans."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import hmac
6
+ import json
7
+ import os
8
+ import secrets
9
+ from datetime import UTC, datetime
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from pydantic import ValidationError as PydanticValidationError
14
+
15
+ from mednotes.domains.wiki.common import MissingPathError, ValidationError
16
+ from mednotes.domains.wiki.config import _user_state_dir
17
+ from mednotes.domains.wiki.contracts.agents import SubagentBatchPlan, SubagentPlanAttestation
18
+ from mednotes.kernel.base import contract_error
19
+
20
+ SUBAGENT_PLAN_ATTESTATION_SCHEMA = "medical-notes-workbench.subagent-plan-attestation.v1"
21
+ SUBAGENT_PLAN_ATTESTATION_KIND = "workbench_hmac_sha256.v1"
22
+
23
+
24
+ def canonical_subagent_plan_payload(payload: dict[str, Any]) -> dict[str, Any]:
25
+ return {key: value for key, value in payload.items() if key != "plan_attestation"}
26
+
27
+
28
+ def subagent_plan_hash(payload: dict[str, Any]) -> str:
29
+ unsigned = canonical_subagent_plan_payload(payload)
30
+ encoded = json.dumps(unsigned, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
31
+ return "sha256:" + hashlib.sha256(encoded).hexdigest()
32
+
33
+
34
+ def _subagent_plan_attestation_key_path() -> Path:
35
+ configured = os.getenv("MEDNOTES_SUBAGENT_PLAN_ATTESTATION_KEY_PATH", "").strip()
36
+ if configured:
37
+ return Path(configured).expanduser()
38
+ return _user_state_dir() / "subagent-plan-attestation.key"
39
+
40
+
41
+ def _subagent_plan_attestation_key(*, create: bool) -> bytes:
42
+ configured = os.getenv("MEDNOTES_SUBAGENT_PLAN_ATTESTATION_KEY", "").strip()
43
+ if configured:
44
+ return configured.encode("utf-8")
45
+ key_path = _subagent_plan_attestation_key_path()
46
+ if key_path.exists():
47
+ return key_path.read_bytes().strip()
48
+ if not create:
49
+ raise MissingPathError(f"subagent plan attestation key not found: {key_path}")
50
+ key_path.parent.mkdir(parents=True, exist_ok=True)
51
+ key = secrets.token_hex(32).encode("ascii")
52
+ tmp_path = key_path.with_name(f"{key_path.name}.tmp")
53
+ tmp_path.write_bytes(key + b"\n")
54
+ try:
55
+ os.chmod(tmp_path, 0o600)
56
+ except OSError:
57
+ pass
58
+ os.replace(tmp_path, key_path)
59
+ return key
60
+
61
+
62
+ def _attestation_signing_payload(payload: dict[str, Any]) -> bytes:
63
+ unsigned = {key: value for key, value in payload.items() if key != "signature"}
64
+ return json.dumps(unsigned, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
65
+
66
+
67
+ def _attestation_signature(payload: dict[str, Any], *, create_key: bool) -> str:
68
+ digest = hmac.new(
69
+ _subagent_plan_attestation_key(create=create_key),
70
+ _attestation_signing_payload(payload),
71
+ hashlib.sha256,
72
+ ).hexdigest()
73
+ return f"hmac-sha256:{digest}"
74
+
75
+
76
+ def _verify_attestation_signature(payload: dict[str, Any]) -> bool:
77
+ try:
78
+ expected = _attestation_signature(payload, create_key=False)
79
+ except MissingPathError:
80
+ return False
81
+ return hmac.compare_digest(str(payload.get("signature") or ""), expected)
82
+
83
+
84
+ def _typed_subagent_plan_for_attestation(payload: dict[str, Any]) -> SubagentBatchPlan:
85
+ """Validate the full plan before its identity fields participate in signing."""
86
+
87
+ try:
88
+ return SubagentBatchPlan.model_validate(payload)
89
+ except PydanticValidationError as exc:
90
+ raise contract_error(exc, prefix="subagent plan attestation payload invalid") from exc
91
+
92
+
93
+ def build_subagent_plan_attestation(payload: dict[str, Any]) -> dict[str, Any]:
94
+ plan = _typed_subagent_plan_for_attestation(payload)
95
+ attestation_payload: dict[str, Any] = {
96
+ "schema": SUBAGENT_PLAN_ATTESTATION_SCHEMA,
97
+ "phase": plan.phase,
98
+ "plan_schema": plan.schema_,
99
+ "plan_hash": subagent_plan_hash(payload),
100
+ "attestation_kind": SUBAGENT_PLAN_ATTESTATION_KIND,
101
+ "created_by": "plan-subagents",
102
+ "issued_at": datetime.now(UTC).isoformat(timespec="seconds"),
103
+ "nonce": secrets.token_hex(16),
104
+ }
105
+ attestation_payload["signature"] = _attestation_signature(attestation_payload, create_key=True)
106
+ try:
107
+ attestation = SubagentPlanAttestation.model_validate(attestation_payload)
108
+ except PydanticValidationError as exc:
109
+ raise contract_error(exc, prefix="subagent plan attestation invalid") from exc
110
+ return attestation.model_dump(mode="json", by_alias=True)
111
+
112
+
113
+ def attach_subagent_plan_attestation(payload: dict[str, Any]) -> dict[str, Any]:
114
+ attested = canonical_subagent_plan_payload(payload)
115
+ attested["plan_attestation"] = build_subagent_plan_attestation(attested)
116
+ return attested
117
+
118
+
119
+ def validate_subagent_plan_attestation(payload: dict[str, Any]) -> str:
120
+ raw_attestation = payload.get("plan_attestation")
121
+ if isinstance(raw_attestation, dict):
122
+ expected_hash = subagent_plan_hash(payload)
123
+ if str(raw_attestation.get("plan_hash") or "") != expected_hash:
124
+ raise ValidationError("subagent plan attestation invalid: plan_hash")
125
+ plan = _typed_subagent_plan_for_attestation(payload)
126
+ if plan.plan_attestation is None:
127
+ raise ValidationError("subagent plan attestation required")
128
+ attestation = plan.plan_attestation
129
+ expected_hash = subagent_plan_hash(payload)
130
+ if attestation.plan_hash != expected_hash:
131
+ raise ValidationError("subagent plan attestation invalid: plan_hash")
132
+ if attestation.phase != plan.phase:
133
+ raise ValidationError("subagent plan attestation invalid: phase")
134
+ if attestation.plan_schema != plan.schema_:
135
+ raise ValidationError("subagent plan attestation invalid: plan_schema")
136
+ if attestation.attestation_kind != SUBAGENT_PLAN_ATTESTATION_KIND:
137
+ raise ValidationError("subagent plan attestation invalid: attestation_kind")
138
+ if not _verify_attestation_signature(attestation.to_payload()):
139
+ raise ValidationError("subagent plan attestation invalid: signature")
140
+ return expected_hash
141
+
142
+
143
+ def subagent_plan_attestation_blocked_reason(exc: Exception) -> str:
144
+ return (
145
+ "subagent_plan_attestation_required"
146
+ if "attestation required" in str(exc).lower()
147
+ else "subagent_plan_attestation_invalid"
148
+ )
@@ -0,0 +1,360 @@
1
+ """Workbench attestation for specialist task run receipts."""
2
+ from __future__ import annotations
3
+
4
+ import base64
5
+ import binascii
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import secrets
10
+ from datetime import UTC, datetime
11
+ from pathlib import Path
12
+ from typing import Literal
13
+
14
+ from cryptography.exceptions import InvalidSignature
15
+ from cryptography.hazmat.primitives import serialization
16
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
17
+ from pydantic import Field
18
+ from pydantic import ValidationError as PydanticValidationError
19
+
20
+ from mednotes.domains.wiki.common import MissingPathError, ValidationError
21
+ from mednotes.domains.wiki.config import _user_state_dir
22
+ from mednotes.domains.wiki.contracts.specialist import (
23
+ SpecialistHarness,
24
+ SpecialistModelEvidence,
25
+ SpecialistOutputAttestationReference,
26
+ SpecialistOutputReceiptReference,
27
+ SpecialistQualityReviewStatus,
28
+ SpecialistRunStatus,
29
+ SpecialistTaskPhase,
30
+ SpecialistTaskRunReceipt,
31
+ SpecialistTaskRunReceiptAttestation,
32
+ SpecialistValidationStatus,
33
+ )
34
+ from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter, contract_error
35
+
36
+ SPECIALIST_TASK_RUN_RECEIPT_ATTESTATION_SCHEMA = (
37
+ "medical-notes-workbench.specialist-task-run-receipt-attestation.v1"
38
+ )
39
+ SPECIALIST_TASK_RUN_RECEIPT_ATTESTATION_KIND = "workbench_ed25519.v1"
40
+ SPECIALIST_TASK_RUN_RECEIPT_ATTESTATION_CREATED_BY = "specialist-task-runner"
41
+ _PRIVATE_KEY_ENV = "MEDNOTES_SPECIALIST_TASK_RECEIPT_ATTESTATION_PRIVATE_KEY"
42
+ _PRIVATE_KEY_PATH_ENV = "MEDNOTES_SPECIALIST_TASK_RECEIPT_ATTESTATION_PRIVATE_KEY_PATH"
43
+ _PUBLIC_KEY_ENV = "MEDNOTES_SPECIALIST_TASK_RECEIPT_ATTESTATION_PUBLIC_KEY"
44
+ _PUBLIC_KEY_PATH_ENV = "MEDNOTES_SPECIALIST_TASK_RECEIPT_ATTESTATION_PUBLIC_KEY_PATH"
45
+ _DEFAULT_PRIVATE_KEY_NAME = "specialist-task-receipt-attestation.ed25519.private.key"
46
+ _DEFAULT_PUBLIC_KEY_NAME = "specialist-task-receipt-attestation.ed25519.public.key"
47
+
48
+
49
+ class _UnsignedSpecialistTaskRunReceipt(ContractModel):
50
+ """Closed pre-signing view of the receipt fields bound into attestation."""
51
+
52
+ schema_id: Literal["medical-notes-workbench.specialist-task-run-receipt.v1"] = Field(
53
+ default="medical-notes-workbench.specialist-task-run-receipt.v1",
54
+ alias="schema",
55
+ )
56
+ work_id: str = Field(min_length=1)
57
+ phase: SpecialistTaskPhase
58
+ harness: SpecialistHarness
59
+ adapter: str = Field(min_length=1)
60
+ requested_agent: str = Field(min_length=1)
61
+ requested_model_policy: str = Field(min_length=1)
62
+ requested_model: str = Field(min_length=1)
63
+ observed_model: str = ""
64
+ model_evidence: SpecialistModelEvidence | None = None
65
+ input_packet_path: str = Field(min_length=1)
66
+ input_packet_sha256: str = Field(pattern=r"^sha256:[0-9a-f]{64}$")
67
+ output_path: str = ""
68
+ output_sha256: str = ""
69
+ status: SpecialistRunStatus
70
+ validation_status: SpecialistValidationStatus
71
+ quality_review_status: SpecialistQualityReviewStatus
72
+ parent_session_id: str = ""
73
+ specialist_session_id: str = ""
74
+ transcript_artifact_path: str = ""
75
+ transcript_artifact_sha256: str = ""
76
+ error_context: JsonObject = Field(default_factory=dict)
77
+ next_action: str = ""
78
+ specialist_output_receipt: SpecialistOutputReceiptReference | None = None
79
+ specialist_output_attestation: SpecialistOutputAttestationReference | None = None
80
+
81
+
82
+ def _unsigned_receipt_for_attestation(payload: JsonObject) -> _UnsignedSpecialistTaskRunReceipt:
83
+ try:
84
+ return _UnsignedSpecialistTaskRunReceipt.model_validate(payload)
85
+ except PydanticValidationError as exc:
86
+ raise contract_error(exc, prefix="specialist task run receipt payload invalid") from exc
87
+
88
+
89
+ def _receipt_without_attestation(payload: JsonObject) -> JsonObject:
90
+ return JsonObjectAdapter.validate_python(
91
+ {key: value for key, value in payload.items() if key != "receipt_attestation"}
92
+ )
93
+
94
+
95
+ def specialist_task_run_receipt_hash(payload: JsonObject) -> str:
96
+ encoded = json.dumps(
97
+ _receipt_without_attestation(payload),
98
+ ensure_ascii=False,
99
+ sort_keys=True,
100
+ separators=(",", ":"),
101
+ ).encode("utf-8")
102
+ return "sha256:" + hashlib.sha256(encoded).hexdigest()
103
+
104
+
105
+ def _base64_decode_key(raw: str, *, label: str) -> bytes:
106
+ compact = raw.strip()
107
+ if not compact:
108
+ raise ValidationError(f"specialist task run receipt attestation {label} required")
109
+ try:
110
+ return base64.b64decode(compact, validate=True)
111
+ except (ValueError, binascii.Error) as exc:
112
+ raise ValidationError(f"specialist task run receipt attestation {label} must be base64") from exc
113
+
114
+
115
+ def _key_bytes_from_env_or_path(
116
+ *,
117
+ env_name: str,
118
+ path_env_name: str,
119
+ label: str,
120
+ ) -> bytes | None:
121
+ configured = os.getenv(env_name, "").strip()
122
+ if configured:
123
+ return _base64_decode_key(configured, label=label)
124
+ configured_path = os.getenv(path_env_name, "").strip()
125
+ if configured_path:
126
+ key_path = Path(configured_path).expanduser()
127
+ if not key_path.exists():
128
+ raise MissingPathError(f"specialist task run receipt attestation {label} not found: {key_path}")
129
+ return _base64_decode_key(key_path.read_text(encoding="utf-8"), label=label)
130
+ return None
131
+
132
+
133
+ def _local_private_key_path() -> Path:
134
+ return _user_state_dir() / _DEFAULT_PRIVATE_KEY_NAME
135
+
136
+
137
+ def _local_public_key_path() -> Path:
138
+ return _user_state_dir() / _DEFAULT_PUBLIC_KEY_NAME
139
+
140
+
141
+ def _write_local_key(path: Path, key_bytes: bytes) -> None:
142
+ path.parent.mkdir(parents=True, exist_ok=True)
143
+ encoded = base64.b64encode(key_bytes) + b"\n"
144
+ tmp_path = path.with_name(f"{path.name}.tmp")
145
+ tmp_path.write_bytes(encoded)
146
+ try:
147
+ os.chmod(tmp_path, 0o600)
148
+ except OSError:
149
+ pass
150
+ os.replace(tmp_path, path)
151
+
152
+
153
+ def _private_key_raw_bytes(private_key: Ed25519PrivateKey) -> bytes:
154
+ return private_key.private_bytes(
155
+ encoding=serialization.Encoding.Raw,
156
+ format=serialization.PrivateFormat.Raw,
157
+ encryption_algorithm=serialization.NoEncryption(),
158
+ )
159
+
160
+
161
+ def _public_key_raw_bytes(public_key: Ed25519PublicKey) -> bytes:
162
+ return public_key.public_bytes(
163
+ encoding=serialization.Encoding.Raw,
164
+ format=serialization.PublicFormat.Raw,
165
+ )
166
+
167
+
168
+ def _create_local_key_pair() -> bytes:
169
+ private_key = Ed25519PrivateKey.generate()
170
+ private_bytes = _private_key_raw_bytes(private_key)
171
+ _write_local_key(_local_private_key_path(), private_bytes)
172
+ _write_local_key(_local_public_key_path(), _public_key_raw_bytes(private_key.public_key()))
173
+ return private_bytes
174
+
175
+
176
+ def _local_private_key_bytes(*, create: bool) -> bytes:
177
+ private_path = _local_private_key_path()
178
+ if private_path.exists():
179
+ return _base64_decode_key(private_path.read_text(encoding="utf-8"), label="private signing key")
180
+ if create:
181
+ return _create_local_key_pair()
182
+ raise MissingPathError(f"specialist task run receipt attestation private signing key not found: {private_path}")
183
+
184
+
185
+ def _derive_local_public_key_from_private() -> bytes | None:
186
+ private_path = _local_private_key_path()
187
+ if not private_path.exists():
188
+ return None
189
+ private_bytes = _base64_decode_key(private_path.read_text(encoding="utf-8"), label="private signing key")
190
+ try:
191
+ private_key = Ed25519PrivateKey.from_private_bytes(private_bytes)
192
+ except ValueError as exc:
193
+ raise ValidationError("specialist task run receipt attestation private signing key invalid") from exc
194
+ public_bytes = _public_key_raw_bytes(private_key.public_key())
195
+ _write_local_key(_local_public_key_path(), public_bytes)
196
+ return public_bytes
197
+
198
+
199
+ def _local_public_key_bytes() -> bytes:
200
+ public_path = _local_public_key_path()
201
+ if public_path.exists():
202
+ return _base64_decode_key(public_path.read_text(encoding="utf-8"), label="trusted public key")
203
+ derived = _derive_local_public_key_from_private()
204
+ if derived is not None:
205
+ return derived
206
+ raise MissingPathError(f"specialist task run receipt attestation trusted public key not found: {public_path}")
207
+
208
+
209
+ def _private_key() -> Ed25519PrivateKey:
210
+ key_bytes = _key_bytes_from_env_or_path(
211
+ env_name=_PRIVATE_KEY_ENV,
212
+ path_env_name=_PRIVATE_KEY_PATH_ENV,
213
+ label="private signing key",
214
+ ) or _local_private_key_bytes(create=True)
215
+ try:
216
+ return Ed25519PrivateKey.from_private_bytes(key_bytes)
217
+ except ValueError as exc:
218
+ raise ValidationError("specialist task run receipt attestation private signing key invalid") from exc
219
+
220
+
221
+ def _public_key() -> Ed25519PublicKey:
222
+ key_bytes = _key_bytes_from_env_or_path(
223
+ env_name=_PUBLIC_KEY_ENV,
224
+ path_env_name=_PUBLIC_KEY_PATH_ENV,
225
+ label="trusted public key",
226
+ ) or _local_public_key_bytes()
227
+ try:
228
+ return Ed25519PublicKey.from_public_bytes(key_bytes)
229
+ except ValueError as exc:
230
+ raise ValidationError("specialist task run receipt attestation trusted public key invalid") from exc
231
+
232
+
233
+ def _public_key_bytes(public_key: Ed25519PublicKey) -> bytes:
234
+ return _public_key_raw_bytes(public_key)
235
+
236
+
237
+ def _public_key_id(public_key: Ed25519PublicKey) -> str:
238
+ return "sha256:" + hashlib.sha256(_public_key_bytes(public_key)).hexdigest()
239
+
240
+
241
+ def _attestation_signing_payload(payload: JsonObject) -> bytes:
242
+ unsigned = {key: value for key, value in payload.items() if key != "signature"}
243
+ return json.dumps(unsigned, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
244
+
245
+
246
+ def _attestation_signature(payload: JsonObject, private_key: Ed25519PrivateKey) -> str:
247
+ signature = private_key.sign(_attestation_signing_payload(payload))
248
+ return "ed25519:" + base64.urlsafe_b64encode(signature).decode("ascii")
249
+
250
+
251
+ def _signature_bytes(signature: str) -> bytes:
252
+ prefix = "ed25519:"
253
+ if not signature.startswith(prefix):
254
+ raise ValidationError("specialist task run receipt attestation invalid: signature_kind")
255
+ try:
256
+ return base64.urlsafe_b64decode(signature[len(prefix):].encode("ascii"))
257
+ except ValueError as exc:
258
+ raise ValidationError("specialist task run receipt attestation invalid: signature_encoding") from exc
259
+
260
+
261
+ def attach_specialist_task_run_receipt_attestation(payload: JsonObject) -> JsonObject:
262
+ private_key = _private_key()
263
+ public_key = private_key.public_key()
264
+ attested = JsonObjectAdapter.validate_python(dict(payload))
265
+ attested.pop("receipt_attestation", None)
266
+ receipt = _unsigned_receipt_for_attestation(attested)
267
+ attestation_payload = JsonObjectAdapter.validate_python({
268
+ "schema": SPECIALIST_TASK_RUN_RECEIPT_ATTESTATION_SCHEMA,
269
+ "attestation_kind": SPECIALIST_TASK_RUN_RECEIPT_ATTESTATION_KIND,
270
+ "created_by": SPECIALIST_TASK_RUN_RECEIPT_ATTESTATION_CREATED_BY,
271
+ "receipt_schema": receipt.schema_id,
272
+ "receipt_hash": specialist_task_run_receipt_hash(attested),
273
+ "work_id": receipt.work_id,
274
+ "phase": receipt.phase,
275
+ "harness": receipt.harness.value,
276
+ "adapter": receipt.adapter,
277
+ "key_id": _public_key_id(public_key),
278
+ "nonce": secrets.token_hex(16),
279
+ "issued_at": datetime.now(UTC).replace(microsecond=0).isoformat(),
280
+ })
281
+ attestation_payload["signature"] = _attestation_signature(attestation_payload, private_key)
282
+ try:
283
+ attestation = SpecialistTaskRunReceiptAttestation.model_validate(attestation_payload)
284
+ except PydanticValidationError as exc:
285
+ raise contract_error(exc, prefix="specialist task run receipt attestation invalid") from exc
286
+ attested["receipt_attestation"] = attestation.to_payload()
287
+ return attested
288
+
289
+
290
+ def _validate_receipt_artifact(
291
+ *,
292
+ path_value: str,
293
+ sha_value: str,
294
+ path_field: str,
295
+ sha_field: str,
296
+ ) -> None:
297
+ if not path_value:
298
+ raise ValidationError(f"specialist task run receipt artifact invalid: {path_field} required")
299
+ if not sha_value:
300
+ raise ValidationError(f"specialist task run receipt artifact invalid: {sha_field} required")
301
+ artifact_path = Path(path_value)
302
+ if not artifact_path.exists():
303
+ raise ValidationError(f"specialist task run receipt artifact invalid: {path_field} not found")
304
+ content = artifact_path.read_bytes()
305
+ if len(content.strip()) <= 2:
306
+ raise ValidationError(f"specialist task run receipt artifact invalid: {path_field} is empty")
307
+ actual = "sha256:" + hashlib.sha256(content).hexdigest()
308
+ if actual != sha_value:
309
+ raise ValidationError(f"specialist task run receipt artifact invalid: {sha_field}")
310
+
311
+
312
+ def validate_specialist_task_run_receipt_attestation(
313
+ payload: JsonObject,
314
+ *,
315
+ require_artifacts: bool = True,
316
+ ) -> None:
317
+ try:
318
+ receipt = SpecialistTaskRunReceipt.from_operation_payload(payload)
319
+ except PydanticValidationError as exc:
320
+ raise contract_error(exc, prefix="specialist task run receipt invalid") from exc
321
+ if receipt.receipt_attestation is None:
322
+ raise ValidationError("specialist task run receipt receipt_attestation required")
323
+ attestation = receipt.receipt_attestation
324
+ raw_attestation = attestation.to_payload()
325
+ expected_hash = specialist_task_run_receipt_hash(payload)
326
+ if attestation.receipt_hash != expected_hash:
327
+ raise ValidationError("specialist task run receipt attestation invalid: receipt_hash")
328
+ if attestation.receipt_schema != receipt.schema_id:
329
+ raise ValidationError("specialist task run receipt attestation invalid: receipt_schema")
330
+ if attestation.work_id != receipt.work_id:
331
+ raise ValidationError("specialist task run receipt attestation invalid: work_id")
332
+ if attestation.phase != receipt.phase:
333
+ raise ValidationError("specialist task run receipt attestation invalid: phase")
334
+ if attestation.harness != receipt.harness:
335
+ raise ValidationError("specialist task run receipt attestation invalid: harness")
336
+ if attestation.adapter != receipt.adapter:
337
+ raise ValidationError("specialist task run receipt attestation invalid: adapter")
338
+ try:
339
+ public_key = _public_key()
340
+ except (MissingPathError, ValidationError) as exc:
341
+ raise ValidationError(f"specialist task run receipt attestation invalid: {exc}") from exc
342
+ if attestation.key_id != _public_key_id(public_key):
343
+ raise ValidationError("specialist task run receipt attestation invalid: key_id")
344
+ try:
345
+ public_key.verify(_signature_bytes(attestation.signature), _attestation_signing_payload(raw_attestation))
346
+ except InvalidSignature as err:
347
+ raise ValidationError("specialist task run receipt attestation invalid: signature") from err
348
+ if require_artifacts and receipt.status == SpecialistRunStatus.COMPLETED:
349
+ _validate_receipt_artifact(
350
+ path_value=receipt.input_packet_path,
351
+ sha_value=receipt.input_packet_sha256,
352
+ path_field="input_packet_path",
353
+ sha_field="input_packet_sha256",
354
+ )
355
+ _validate_receipt_artifact(
356
+ path_value=receipt.transcript_artifact_path,
357
+ sha_value=receipt.transcript_artifact_sha256,
358
+ path_field="transcript_artifact_path",
359
+ sha_field="transcript_artifact_sha256",
360
+ )
@@ -0,0 +1,52 @@
1
+ """Runtime trust helpers for Workbench-mediated specialist calls."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ _TRUSTED_GEMINI_BINARY_NAMES = frozenset({"gemini", "gemini.cmd", "gemini.exe"})
9
+
10
+
11
+ def specialist_dev_escape_enabled() -> bool:
12
+ return os.environ.get("MEDNOTES_ALLOW_DEV_ESCAPE", "").strip() == "1"
13
+
14
+
15
+ def gemini_binary_identity(binary: str) -> str:
16
+ value = binary.strip()
17
+ if not value:
18
+ return ""
19
+ if "/" in value or "\\" in value:
20
+ return str(Path(value).expanduser().resolve(strict=False))
21
+ resolved = shutil.which(value)
22
+ return str(Path(resolved).resolve(strict=False)) if resolved else value
23
+
24
+
25
+ def gemini_binary_is_public_trusted(binary: str) -> bool:
26
+ value = binary.strip()
27
+ if not value:
28
+ return False
29
+ if value in _TRUSTED_GEMINI_BINARY_NAMES:
30
+ return True
31
+ default = shutil.which("gemini")
32
+ if not default:
33
+ return False
34
+ default_identity = str(Path(default).resolve(strict=False))
35
+ return gemini_binary_identity(value) == default_identity
36
+
37
+
38
+ def gemini_binary_override_block_reason(binary: str) -> str:
39
+ if gemini_binary_is_public_trusted(binary):
40
+ return ""
41
+ if specialist_dev_escape_enabled():
42
+ return ""
43
+ return "specialist_runner_untrusted_gemini_binary"
44
+
45
+
46
+ def transcript_command_untrusted_gemini_binary(command: object) -> str:
47
+ if not isinstance(command, list) or not command:
48
+ return "missing_command"
49
+ binary = command[0]
50
+ if not isinstance(binary, str) or not binary.strip():
51
+ return "missing_binary"
52
+ return "" if gemini_binary_is_public_trusted(binary) else binary.strip()