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,1470 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ import {
7
+ firstObject,
8
+ isAgyRuntime,
9
+ normalizedToolName,
10
+ runtimeFromPayload,
11
+ sessionIdFromPayload,
12
+ toolCommandLine,
13
+ toolInput,
14
+ } from "./adapters/harness_payload.mjs";
15
+ import {
16
+ contextFromDirective,
17
+ contextFromWorkflowIntent,
18
+ controlStatus,
19
+ directiveAllowsFinalReport,
20
+ directiveAllowsPauseReport,
21
+ isAgentDirective,
22
+ isAgentDirectiveCarrierPayload,
23
+ limitIsFalse,
24
+ } from "./domain/agent_directive_core.mjs";
25
+ import { deny, quiet } from "./runtime.mjs";
26
+
27
+ const CARD_SCHEMA = "medical-notes-workbench.workflow-hook-directive-card.v1";
28
+ const INTENT_SCHEMA = "medical-notes-workbench.workflow-hook-intent.v1";
29
+ const SCRIPT_EXTENSIONS = new Set([".py", ".js", ".mjs", ".cjs", ".sh", ".ps1", ".cmd", ".bat"]);
30
+ const UNSUPPORTED_TOOL_PARAMETERS = new Set(["wait_for_previous"]);
31
+ const AGY_TOOL_METADATA_PARAMETERS = new Set(["toolAction", "toolSummary", "IsSkillFile"]);
32
+ const PRE_PAYLOAD_BLOCKED_TOOLS = new Set([
33
+ "ask_permission",
34
+ "call_mcp_tool",
35
+ "grep_search",
36
+ "invoke_agent",
37
+ "invoke_subagent",
38
+ "list_dir",
39
+ "list_directory",
40
+ "list_permissions",
41
+ "read_url_content",
42
+ "schedule",
43
+ ]);
44
+ const PUBLIC_WORKFLOWS = new Set([
45
+ "/flashcards",
46
+ "/mednotes:fix-wiki",
47
+ "/mednotes:history",
48
+ "/mednotes:link",
49
+ "/mednotes:link-related",
50
+ "/mednotes:process-chats",
51
+ "/mednotes:setup",
52
+ ]);
53
+ const MUTATING_TOOLS = new Set([
54
+ "bash",
55
+ "edit",
56
+ "multiedit",
57
+ "multi_replace_file_content",
58
+ "powershell",
59
+ "pwsh",
60
+ "replace",
61
+ "replace_file_content",
62
+ "run_command",
63
+ "run_shell",
64
+ "run_shell_command",
65
+ "shell",
66
+ "shelltool",
67
+ "write",
68
+ "write_file",
69
+ "write_to_file",
70
+ ]);
71
+ const TERMINAL_STATUSES = new Set(["completed", "completed_with_warnings"]);
72
+ const MAX_TASK_LOG_BYTES = 2 * 1024 * 1024;
73
+ const SPECIALIST_WORK_ITEM_RAW_CONTENT_KEYS = new Set([
74
+ "content",
75
+ "html",
76
+ "markdown",
77
+ "note_text",
78
+ "raw",
79
+ "raw_chat",
80
+ "raw_chat_content",
81
+ "raw_markdown",
82
+ "raw_markdown_content",
83
+ ]);
84
+ const SPECIALIST_WORK_ITEM_SAFE_TEXT_KEYS = new Set([
85
+ "agent",
86
+ "attestation_created_by",
87
+ "coverage_path",
88
+ "expected_model",
89
+ "item_type",
90
+ "missing_specialist_task_run_receipt_action",
91
+ "model_policy",
92
+ "phase",
93
+ "preferred_model_tier",
94
+ "raw_file",
95
+ "required_model_tier",
96
+ "rewrite_prompt",
97
+ "schema",
98
+ "source_work_id",
99
+ "specialist_task_run_receipt_path",
100
+ "staged_title",
101
+ "target_hash_before",
102
+ "target_kind",
103
+ "target_path",
104
+ "taxonomy",
105
+ "temp_output",
106
+ "temp_output_path",
107
+ "title",
108
+ "work_id",
109
+ "write_markdown_to",
110
+ "write_policy",
111
+ ]);
112
+
113
+ export async function captureAgentDirectiveAfterTool(payload) {
114
+ const sessionId = sessionIdFromPayload(payload);
115
+ const responsePayload = officialWorkflowPayloadFromToolResponse(payload);
116
+ let workflowPayload = isAgentDirectiveCarrierPayload(responsePayload) ? responsePayload : {};
117
+ if (!isAgentDirectiveCarrierPayload(workflowPayload)) {
118
+ const artifactPayload = await officialWorkflowPayloadFromToolArtifact(payload);
119
+ workflowPayload = isAgentDirectiveCarrierPayload(artifactPayload) ? artifactPayload : {};
120
+ }
121
+ const directive = directiveFromWorkflowPayload(workflowPayload);
122
+ if (!directive) return quiet();
123
+
124
+ if (!sessionId) return quiet();
125
+
126
+ if (directiveAllowsFinalReport(directive) && TERMINAL_STATUSES.has(controlStatus(directive))) {
127
+ await clearDirectiveCard(sessionId);
128
+ return quiet();
129
+ }
130
+
131
+ const now = new Date();
132
+ const card = {
133
+ schema: CARD_SCHEMA,
134
+ session_id: sessionId,
135
+ runtime: runtimeFromPayload(payload),
136
+ captured_at: now.toISOString(),
137
+ expires_at: new Date(now.getTime() + ttlMs()).toISOString(),
138
+ source: {
139
+ hook_event: String(payload?.hook_event_name || "AfterTool"),
140
+ tool_name: String(payload?.tool_name || ""),
141
+ payload_hash: `sha256:${sha256(canonicalJson(workflowPayload))}`,
142
+ },
143
+ directive,
144
+ enforcement: {
145
+ mode: "p0",
146
+ p0_blocking_enabled: true,
147
+ ...expectedContinuationEnforcement(workflowPayload),
148
+ },
149
+ };
150
+ await writeDirectiveCard(card);
151
+ await clearWorkflowIntent(sessionId);
152
+ return quiet();
153
+ }
154
+
155
+ export async function injectAgentDirectiveBeforeAgent(payload) {
156
+ const sessionId = sessionIdFromPayload(payload);
157
+ const card = await readActiveDirectiveCard(sessionId);
158
+ if (!card) {
159
+ const intent = (await readActiveWorkflowIntent(sessionId)) || (await recordWorkflowIntentFromPayload(payload));
160
+ if (!intent) return quiet();
161
+ const context = contextFromWorkflowIntent(intent);
162
+ if (!context) return quiet();
163
+ return {
164
+ suppressOutput: true,
165
+ hookSpecificOutput: {
166
+ additionalContext: context,
167
+ },
168
+ };
169
+ }
170
+ const context = contextFromDirectiveCard(card, payload);
171
+ if (!context) return quiet();
172
+ return {
173
+ suppressOutput: true,
174
+ hookSpecificOutput: {
175
+ additionalContext: context,
176
+ },
177
+ };
178
+ }
179
+
180
+ export async function guardAgentDirectiveBeforeTool(payload) {
181
+ const globalGuard = guardAgyOperationalToolContract(payload);
182
+ if (globalGuard) return globalGuard;
183
+
184
+ const card = await readActiveDirectiveCard(sessionIdFromPayload(payload));
185
+ if (!card) {
186
+ const intent = await readActiveWorkflowIntent(sessionIdFromPayload(payload));
187
+ if (!intent) return quiet();
188
+ return guardWorkflowIntentBeforeTool(payload, intent);
189
+ }
190
+ const directive = card.directive;
191
+ const input = toolInput(payload);
192
+ const unsupported = firstUnsupportedParameter(input);
193
+ if (unsupported) {
194
+ return denyP0({
195
+ blockedReason: "unsupported_tool_parameter",
196
+ reason: `Bloqueei a chamada porque o parâmetro \`${unsupported}\` não existe no contrato da tool.`,
197
+ agentMessage: `Remova \`${unsupported}\`; agent hooks não aceitam parâmetros de ferramenta inventados.`,
198
+ directiveField: "tool.parameters",
199
+ });
200
+ }
201
+ if (toolRunsRetiredSpecialistRunner(payload)) {
202
+ return denyP0({
203
+ blockedReason: "retired_specialist_runner_command",
204
+ reason: "run-specialist-task foi removido da superfície pública; a FSM deve expor call_specialist_model.",
205
+ agentMessage:
206
+ "Consuma agent_directive.control.effects[].payload.current_batch_items pelo harness atual; não chame run-specialist-task.",
207
+ directiveField: "agent_directive.control.effects",
208
+ });
209
+ }
210
+
211
+ const status = controlStatus(directive);
212
+ if (status === "waiting_human" && toolLooksMutating(payload)) {
213
+ return denyP0({
214
+ blockedReason: "human_decision_required",
215
+ reason: "human_decision_required: Bloqueei a mutação porque a FSM aguarda decisão humana.",
216
+ agentMessage: "Peça a decisão humana exposta pelo workflow oficial antes de aplicar ou publicar.",
217
+ directiveField: "agent_directive.control.status=waiting_human",
218
+ });
219
+ }
220
+ if (status === "waiting_external" && toolLooksMutating(payload)) {
221
+ return denyP0({
222
+ blockedReason: "external_resource_wait_required",
223
+ reason: "Bloqueei a mutação porque a FSM aguarda recurso externo.",
224
+ agentMessage: "Aguarde a retomada oficial; não repita mutações enquanto o workflow está em waiting_external.",
225
+ directiveField: "agent_directive.control.status=waiting_external",
226
+ });
227
+ }
228
+ if ((status === "blocked" || status === "failed") && !toolRunsVaultGuardFinish(payload)) {
229
+ return denyP0({
230
+ blockedReason: "workflow_blocked_no_continuation",
231
+ reason: "Bloqueei a chamada porque a FSM registrou bloqueio sem continuação executável.",
232
+ agentMessage: [
233
+ "Feche a proteção do vault se houver lease ativa e reporte o bloqueio em linguagem pública; ",
234
+ "não abra novo subagente, não rode nova conferência e não investigue código para contornar o bloqueio.",
235
+ ].join(""),
236
+ directiveField: `agent_directive.control.status=${status}`,
237
+ });
238
+ }
239
+ if (!directiveAllowsFinalReport(directive) && toolRunsAgentReportValidation(payload)) {
240
+ return denyP0({
241
+ blockedReason: "agent_report_validation_premature",
242
+ reason: [
243
+ "agent_report_validation_premature: Bloqueei validate-agent-run-report porque ",
244
+ "agent_directive.control.capabilities.final_report=false. ",
245
+ "Siga agent_directive.control.effects antes de validar ou concluir relatório final.",
246
+ ].join(""),
247
+ agentMessage:
248
+ "Continue pelo agent_directive.control.effects da FSM; validação de relatório final só depois de final_report=true.",
249
+ directiveField: "agent_directive.control.capabilities.final_report=false",
250
+ });
251
+ }
252
+ if (limitIsFalse(directive, "ad_hoc_scripts") && !toolCommandLine(payload) && toolCreatesAdHocScript(payload)) {
253
+ return denyP0({
254
+ blockedReason: "ad_hoc_script_forbidden",
255
+ reason: "Bloqueei script ad hoc porque a FSM exige rota oficial.",
256
+ agentMessage: "Use a rota oficial do Workbench; agent_directive.control.limits.ad_hoc_scripts=false.",
257
+ directiveField: "agent_directive.control.limits.ad_hoc_scripts=false",
258
+ });
259
+ }
260
+ const opencodeTaskPromptGuard = guardOpenCodeTaskPromptContract(payload, directive);
261
+ if (opencodeTaskPromptGuard) return opencodeTaskPromptGuard;
262
+ if (
263
+ limitIsFalse(directive, "raw_content") &&
264
+ toolCanCarryOperationalRawContent(payload) &&
265
+ toolContainsRawContent(input)
266
+ ) {
267
+ return denyP0({
268
+ blockedReason: "raw_content_forbidden",
269
+ reason: "Bloqueei conteúdo bruto porque a FSM proíbe colar Markdown/chat/HTML no payload da ferramenta.",
270
+ agentMessage: "Passe paths, hashes e work_item tipado; agent_directive.control.limits.raw_content=false.",
271
+ directiveField: "agent_directive.control.limits.raw_content=false",
272
+ });
273
+ }
274
+ const effectGuard = guardWaitingAgentConcreteEffects(payload, directive);
275
+ if (effectGuard) return effectGuard;
276
+ if (limitIsFalse(directive, "ad_hoc_scripts") && toolCreatesAdHocScript(payload)) {
277
+ return denyP0({
278
+ blockedReason: "ad_hoc_script_forbidden",
279
+ reason: "Bloqueei script ad hoc porque a FSM exige rota oficial.",
280
+ agentMessage: "Use a rota oficial do Workbench; agent_directive.control.limits.ad_hoc_scripts=false.",
281
+ directiveField: "agent_directive.control.limits.ad_hoc_scripts=false",
282
+ });
283
+ }
284
+ return quiet();
285
+ }
286
+
287
+ function contextFromDirectiveCard(card, payload) {
288
+ const baseContext = contextFromDirective(card.directive);
289
+ const runtimeContext = opencodeSpecialistTaskContext(card.directive, payload);
290
+ const context = [baseContext, runtimeContext].filter(Boolean).join("\n");
291
+ return context.length <= 2600 ? context : `${context.slice(0, 2597).trimEnd()}...`;
292
+ }
293
+
294
+ function opencodeSpecialistTaskContext(directive, payload) {
295
+ if (runtimeFromPayload(payload) !== "opencode") return "";
296
+ if (!directiveHasCurrentBatchItems(directive)) return "";
297
+ return [
298
+ "MEDNOTES OPENCODE EFFECT ROUTE",
299
+ "instruction: call the native task tool once per current_batch_items member.",
300
+ 'instruction: task.prompt must be exactly JSON.stringify({"current_batch_items":[one item from agent_directive.control.effects[].payload.current_batch_items]}); no prose, labels, Markdown, target_path lines or duplicated root fields.',
301
+ "instruction: after task completes, run the official finalizer directly; do not read hook-state metadata.",
302
+ ].join("\n");
303
+ }
304
+
305
+ function directiveHasCurrentBatchItems(directive) {
306
+ const effects = Array.isArray(directive?.control?.effects) ? directive.control.effects : [];
307
+ return effects.some((effect) => {
308
+ const payload = effect?.payload && typeof effect.payload === "object" ? effect.payload : {};
309
+ return Array.isArray(payload.current_batch_items) && payload.current_batch_items.length > 0;
310
+ });
311
+ }
312
+
313
+ function guardOpenCodeTaskPromptContract(payload, directive) {
314
+ if (runtimeFromPayload(payload) !== "opencode") return null;
315
+ if (normalizedToolName(payload) !== "task") return null;
316
+ if (!directiveHasCurrentBatchItems(directive)) return null;
317
+ const prompt = String(toolInput(payload).prompt || "");
318
+ const parsed = parseWholeJsonObject(prompt);
319
+ const isPureSingleItemPacket =
320
+ parsed &&
321
+ Object.keys(parsed).length === 1 &&
322
+ Array.isArray(parsed.current_batch_items) &&
323
+ parsed.current_batch_items.length === 1;
324
+ if (isPureSingleItemPacket) return null;
325
+ return denyP0({
326
+ blockedReason: "opencode_task_prompt_contract_violation",
327
+ reason: [
328
+ "opencode_task_prompt_contract_violation: Bloqueei a task porque o prompt do OpenCode ",
329
+ 'precisa ser JSON puro com raiz {"current_batch_items":[...]} e exatamente um item vindo ',
330
+ "de agent_directive.control.effects[].payload.current_batch_items.",
331
+ ].join(""),
332
+ agentMessage: [
333
+ 'Repita a mesma task com prompt exatamente JSON.stringify({"current_batch_items":[item]}); ',
334
+ "sem prosa, sem rótulos Contract/Target, sem Markdown e sem campos duplicados no root.",
335
+ ].join(""),
336
+ directiveField: "agent_directive.control.effects[].payload.current_batch_items",
337
+ });
338
+ }
339
+
340
+ function guardAgyOperationalToolContract(payload) {
341
+ if (!isAgyRuntime(payload)) return null;
342
+ if (toolWritesArtifactMetadataToWorkbenchTemp(payload)) {
343
+ return denyP0({
344
+ blockedReason: "agy_artifact_metadata_forbidden_for_operational_output",
345
+ reason:
346
+ "Bloqueei ArtifactMetadata em arquivo operacional do Workbench porque o AGY trata isso como artifact path e rejeita o temp_output.",
347
+ agentMessage: [
348
+ "Repita write_to_file sem ArtifactMetadata. O TargetFile operacional em mednotes-home/tmp/agent-work ",
349
+ "não é artifact do AGY; ele deve ser escrito como arquivo comum.",
350
+ ].join(""),
351
+ directiveField: "subagent_output_contract.write_markdown_to=temp_output",
352
+ });
353
+ }
354
+ return null;
355
+ }
356
+
357
+ async function guardWorkflowIntentBeforeTool(payload, intent) {
358
+ const input = toolInput(payload);
359
+ if (await isOfficialTaskLogRead(payload, intent)) return quiet();
360
+ if (isAllowedPrePayloadTool(payload, intent)) return quiet();
361
+
362
+ const unsupportedMetadata = firstUnsupportedParameter(input, 0, AGY_TOOL_METADATA_PARAMETERS);
363
+ if (unsupportedMetadata) {
364
+ return denyP0({
365
+ blockedReason: "unsupported_tool_parameter",
366
+ reason: `Bloqueei a chamada porque \`${unsupportedMetadata}\` e metadado inventado para esta ferramenta AGY; remova esse campo e repita a mesma ferramenta.`,
367
+ agentMessage: `Remova \`${unsupportedMetadata}\` e repita a mesma ferramenta sem metadados inventados; depois siga a rota oficial de ${intent.workflow}.`,
368
+ directiveField: "workflow_intent.before_payload.tool.parameters",
369
+ });
370
+ }
371
+
372
+ const toolName = normalizedToolName(payload);
373
+ if (PRE_PAYLOAD_BLOCKED_TOOLS.has(toolName) || !isOfficialPrePayloadCommand(payload, intent)) {
374
+ return denyP0({
375
+ blockedReason: "workflow_payload_missing",
376
+ reason: `Bloqueei ${toolName || "tool"} porque ${intent.workflow} ainda nao emitiu payload oficial.`,
377
+ agentMessage: `Nao use ${toolName || "esta ferramenta"} para auto-debug antes do payload oficial. Carregue a skill oficial se necessario, abra run-start e execute a rota publica do workflow.`,
378
+ directiveField: "workflow_intent.stage=before_workflow_payload",
379
+ });
380
+ }
381
+ return quiet();
382
+ }
383
+
384
+ export async function validateAgentDirectiveAfterAgent(payload) {
385
+ const card = await readActiveDirectiveCard(sessionIdFromPayload(payload));
386
+ if (!card) return quiet();
387
+ const directive = card.directive;
388
+ const hookEvent = String(payload?.hook_event_name || payload?.hookEventName || "").toLowerCase();
389
+ if (!directiveAllowsFinalReport(directive) && hookEvent === "stop" && !directiveAllowsPauseReport(directive)) {
390
+ return denyP0({
391
+ blockedReason: "final_report_forbidden_by_agent_directive",
392
+ reason: "agent_directive.control.capabilities.final_report=false; o workflow ainda não autoriza relatório final.",
393
+ agentMessage: "Continue pela rota oficial antes de declarar conclusão.",
394
+ directiveField: "agent_directive.control.capabilities.final_report=false",
395
+ });
396
+ }
397
+ return quiet();
398
+ }
399
+
400
+ export function officialWorkflowPayloadFromToolResponse(payload) {
401
+ const response = firstObject(payload?.tool_response, payload?.toolResponse, payload?.response, payload?.result);
402
+ if (isAgentDirectiveCarrierPayload(response)) return response;
403
+ for (const value of responseCandidates(response)) {
404
+ const parsed = parseWholeJsonObject(value);
405
+ if (isAgentDirectiveCarrierPayload(parsed)) return parsed;
406
+ const embedded = parseOfficialWorkflowPayloadFromText(value);
407
+ if (isAgentDirectiveCarrierPayload(embedded)) return embedded;
408
+ if (looksLikeAgyTaskLogViewOutput(payload, value)) {
409
+ const embeddedFromView = parseOfficialWorkflowPayloadFromText(stripViewFileLineNumbers(value));
410
+ if (isAgentDirectiveCarrierPayload(embeddedFromView)) return embeddedFromView;
411
+ }
412
+ }
413
+ return {};
414
+ }
415
+
416
+ async function officialWorkflowPayloadFromToolArtifact(payload) {
417
+ if (!toolLooksLikeFileRead(payload)) return {};
418
+ const target = toolTargetPath(payload);
419
+ if (!isAgyTaskLogPath(target)) return {};
420
+ try {
421
+ const stats = await fs.stat(target);
422
+ if (!stats.isFile() || stats.size > MAX_TASK_LOG_BYTES) return {};
423
+ const text = await fs.readFile(target, "utf8");
424
+ const artifactPayload = parseWholeJsonObject(text);
425
+ if (!isCorrelatedTaskLogArtifact(payload, artifactPayload)) return {};
426
+ return parseOfficialWorkflowPayloadFromText(text);
427
+ } catch {
428
+ return {};
429
+ }
430
+ }
431
+
432
+ function isCorrelatedTaskLogArtifact(payload, artifactPayload) {
433
+ // Reading a task log by path is not enough to establish ownership. AGY task
434
+ // logs are accepted only when the file proves it belongs to this exact tool
435
+ // call and response; otherwise a stale log could inject a directive card.
436
+ const artifactCall = firstObject(artifactPayload?.toolCall, artifactPayload?.tool_call);
437
+ const artifactResponse = firstObject(artifactPayload?.toolResponse, artifactPayload?.tool_response);
438
+ const currentCall = firstObject(payload?.toolCall, payload?.tool_call);
439
+ const currentResponse = firstObject(
440
+ payload?.toolResponse,
441
+ payload?.tool_response,
442
+ payload?.response,
443
+ payload?.result,
444
+ );
445
+ const artifactCallId = stableToolId(artifactCall);
446
+ const artifactResponseId = stableToolId(artifactResponse);
447
+ const currentCallId = stableToolId(currentCall);
448
+ const currentResponseId = stableToolId(currentResponse);
449
+ return Boolean(
450
+ currentCallId && currentResponseId && artifactCallId === currentCallId && artifactResponseId === currentResponseId,
451
+ );
452
+ }
453
+
454
+ function stableToolId(value) {
455
+ const raw = value?.id || value?.toolCallId || value?.tool_call_id || value?.callId || value?.call_id || "";
456
+ return typeof raw === "string" && raw.trim() ? raw.trim() : "";
457
+ }
458
+
459
+ function directiveFromWorkflowPayload(payload) {
460
+ if (!isAgentDirectiveCarrierPayload(payload)) return null;
461
+ const directive = payload.agent_directive;
462
+ if (!isAgentDirective(directive)) return null;
463
+ return JSON.parse(canonicalJson(directive));
464
+ }
465
+
466
+ function expectedContinuationEnforcement(payload) {
467
+ // Enforcement card data is metadata about the current FSM payload only. It
468
+ // must not synthesize runtime-specific continuation tasks from legacy
469
+ // diagnostic_context, run records, phase names, or workflow-specific payloads.
470
+ void payload;
471
+ return {};
472
+ }
473
+
474
+ function responseCandidates(response) {
475
+ const candidates = [];
476
+ if (!response || typeof response !== "object") return candidates;
477
+ for (const key of ["returnDisplay", "llmContent", "stdout", "output", "content", "text"]) {
478
+ const value = response[key];
479
+ if (typeof value === "string") candidates.push(value);
480
+ }
481
+ return candidates;
482
+ }
483
+
484
+ function parseWholeJsonObject(value) {
485
+ const text = String(value || "").trim();
486
+ if (!text.startsWith("{") || !text.endsWith("}")) return {};
487
+ try {
488
+ const parsed = JSON.parse(text);
489
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
490
+ } catch {
491
+ return {};
492
+ }
493
+ }
494
+
495
+ function parseOfficialWorkflowPayloadFromText(value) {
496
+ // The name is kept for stable callers: the parser now returns any official
497
+ // directive carrier, not only root FSM payloads.
498
+ const text = String(value || "");
499
+ let index = 0;
500
+ while (index < text.length) {
501
+ const start = text.indexOf('{"schema"', index);
502
+ if (start < 0) return {};
503
+ const candidate = balancedJsonObjectAt(text, start);
504
+ if (!candidate) {
505
+ index = start + 1;
506
+ continue;
507
+ }
508
+ const parsed = parseWholeJsonObject(candidate);
509
+ if (isAgentDirectiveCarrierPayload(parsed)) return parsed;
510
+ index = start + 1;
511
+ }
512
+ return {};
513
+ }
514
+
515
+ function looksLikeAgyTaskLogViewOutput(payload, value) {
516
+ if (!toolLooksLikeFileRead(payload)) return false;
517
+ const text = String(value || "");
518
+ return text.includes("/.system_generated/tasks/task-") && text.includes(".log") && text.includes('{"schema"');
519
+ }
520
+
521
+ function stripViewFileLineNumbers(value) {
522
+ return String(value || "").replace(/(^|\n)\s*\d+:\s+\{/g, "$1{");
523
+ }
524
+
525
+ function balancedJsonObjectAt(text, start) {
526
+ let depth = 0;
527
+ let inString = false;
528
+ let escaped = false;
529
+ for (let index = start; index < text.length; index += 1) {
530
+ const char = text[index];
531
+ if (inString) {
532
+ if (escaped) {
533
+ escaped = false;
534
+ } else if (char === "\\") {
535
+ escaped = true;
536
+ } else if (char === '"') {
537
+ inString = false;
538
+ }
539
+ continue;
540
+ }
541
+ if (char === '"') {
542
+ inString = true;
543
+ } else if (char === "{") {
544
+ depth += 1;
545
+ } else if (char === "}") {
546
+ depth -= 1;
547
+ if (depth === 0) return text.slice(start, index + 1);
548
+ }
549
+ }
550
+ return "";
551
+ }
552
+
553
+ function denyP0({ blockedReason, reason, agentMessage, directiveField }) {
554
+ return deny(reason, {
555
+ status: "blocked_agent_directive_enforcement",
556
+ blocked_reason: blockedReason,
557
+ agent_message: agentMessage,
558
+ directive_field: directiveField,
559
+ });
560
+ }
561
+
562
+ function appHomeDir() {
563
+ return process.env.MEDNOTES_HOME || path.join(os.homedir(), ".mednotes");
564
+ }
565
+
566
+ function stateDir() {
567
+ return path.join(appHomeDir(), "hook-state", "fsm-directive");
568
+ }
569
+
570
+ function cardPath(sessionId) {
571
+ return path.join(stateDir(), `${sessionId}.json`);
572
+ }
573
+
574
+ function intentStateDir() {
575
+ return path.join(appHomeDir(), "hook-state", "workflow-intent");
576
+ }
577
+
578
+ function intentPath(sessionId) {
579
+ return path.join(intentStateDir(), `${sessionId}.json`);
580
+ }
581
+
582
+ async function writeDirectiveCard(card) {
583
+ await fs.mkdir(stateDir(), { recursive: true });
584
+ const file = cardPath(card.session_id);
585
+ const tmp = `${file}.tmp`;
586
+ await fs.writeFile(tmp, `${JSON.stringify(card, null, 2)}\n`, "utf8");
587
+ await fs.rename(tmp, file);
588
+ }
589
+
590
+ async function clearDirectiveCard(sessionId) {
591
+ try {
592
+ await fs.unlink(cardPath(sessionId));
593
+ } catch {
594
+ // Stale or missing card is already safe.
595
+ }
596
+ }
597
+
598
+ async function readActiveDirectiveCard(sessionId) {
599
+ if (!sessionId) return null;
600
+ try {
601
+ const parsed = JSON.parse(await fs.readFile(cardPath(sessionId), "utf8"));
602
+ if (!parsed || parsed.schema !== CARD_SCHEMA || !isAgentDirective(parsed.directive)) return null;
603
+ const expiresAt = Date.parse(String(parsed.expires_at || ""));
604
+ if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) {
605
+ await clearDirectiveCard(sessionId);
606
+ return null;
607
+ }
608
+ return parsed;
609
+ } catch {
610
+ return null;
611
+ }
612
+ }
613
+
614
+ async function recordWorkflowIntentFromPayload(payload) {
615
+ // pre-payload guard only: this records that a public workflow was invoked
616
+ // before official JSON exists. It does not create agent_directive and cannot
617
+ // satisfy waiting_agent continuation after a real FSM payload is captured.
618
+ const sessionId = sessionIdFromPayload(payload);
619
+ if (!sessionId) return null;
620
+ const workflow = await workflowFromPayload(payload);
621
+ if (!workflow) return null;
622
+ const now = new Date();
623
+ const intent = {
624
+ schema: INTENT_SCHEMA,
625
+ session_id: sessionId,
626
+ runtime: runtimeFromPayload(payload),
627
+ workflow,
628
+ stage: "before_workflow_payload",
629
+ captured_at: now.toISOString(),
630
+ expires_at: new Date(now.getTime() + ttlMs()).toISOString(),
631
+ source: {
632
+ hook_event: String(payload?.hook_event_name || payload?.hookEventName || "PreInvocation"),
633
+ transcript_seen: Boolean(payload?.transcript_path),
634
+ },
635
+ };
636
+ await writeWorkflowIntent(intent);
637
+ return intent;
638
+ }
639
+
640
+ async function writeWorkflowIntent(intent) {
641
+ await fs.mkdir(intentStateDir(), { recursive: true });
642
+ const file = intentPath(intent.session_id);
643
+ const tmp = `${file}.tmp`;
644
+ await fs.writeFile(tmp, `${JSON.stringify(intent, null, 2)}\n`, "utf8");
645
+ await fs.rename(tmp, file);
646
+ }
647
+
648
+ async function readActiveWorkflowIntent(sessionId) {
649
+ if (!sessionId) return null;
650
+ try {
651
+ const parsed = JSON.parse(await fs.readFile(intentPath(sessionId), "utf8"));
652
+ if (!parsed || parsed.schema !== INTENT_SCHEMA || !PUBLIC_WORKFLOWS.has(String(parsed.workflow || ""))) {
653
+ return null;
654
+ }
655
+ const expiresAt = Date.parse(String(parsed.expires_at || ""));
656
+ if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) {
657
+ await clearWorkflowIntent(sessionId);
658
+ return null;
659
+ }
660
+ return parsed;
661
+ } catch {
662
+ return null;
663
+ }
664
+ }
665
+
666
+ async function clearWorkflowIntent(sessionId) {
667
+ try {
668
+ await fs.unlink(intentPath(sessionId));
669
+ } catch {
670
+ // Stale or missing intent is already safe.
671
+ }
672
+ }
673
+
674
+ async function workflowFromPayload(payload) {
675
+ const direct = workflowFromText(
676
+ [
677
+ payload?.prompt,
678
+ payload?.user_input,
679
+ payload?.userInput,
680
+ payload?.message,
681
+ payload?.content,
682
+ payload?.request,
683
+ payload?.userRequest,
684
+ ]
685
+ .filter((value) => typeof value === "string")
686
+ .join("\n"),
687
+ );
688
+ if (direct) return direct;
689
+ return workflowFromText(await transcriptUserText(payload?.transcript_path));
690
+ }
691
+
692
+ async function transcriptUserText(transcriptPath) {
693
+ const file = String(transcriptPath || "");
694
+ if (!file) return "";
695
+ try {
696
+ const stats = await fs.stat(file);
697
+ if (!stats.isFile() || stats.size > 2 * 1024 * 1024) return "";
698
+ const text = await fs.readFile(file, "utf8");
699
+ const excerpts = [];
700
+ for (const line of text.split(/\r?\n/)) {
701
+ const trimmed = line.trim();
702
+ if (!trimmed) continue;
703
+ try {
704
+ const record = JSON.parse(trimmed);
705
+ if (!record || typeof record !== "object" || Array.isArray(record)) continue;
706
+ const source = String(record.source || record.role || record.type || "");
707
+ if (/USER/i.test(source)) excerpts.push(String(record.content || record.text || record.message || ""));
708
+ } catch {
709
+ excerpts.push(trimmed);
710
+ }
711
+ if (excerpts.join("\n").length > 12000) break;
712
+ }
713
+ return excerpts.join("\n");
714
+ } catch {
715
+ return "";
716
+ }
717
+ }
718
+
719
+ function workflowFromText(value) {
720
+ const text = String(value || "");
721
+ for (const workflow of PUBLIC_WORKFLOWS) {
722
+ if (new RegExp(`${escapeRegExp(workflow)}(?:\\s|$)`).test(text)) return workflow;
723
+ }
724
+ return "";
725
+ }
726
+
727
+ function ttlMs() {
728
+ const hours = Number(process.env.MEDNOTES_FSM_HOOK_TTL_HOURS || "6");
729
+ const safeHours = Number.isFinite(hours) ? Math.min(24, Math.max(1, hours)) : 6;
730
+ return safeHours * 60 * 60 * 1000;
731
+ }
732
+
733
+ function canonicalJson(value) {
734
+ return JSON.stringify(sortJson(value));
735
+ }
736
+
737
+ function sortJson(value) {
738
+ if (Array.isArray(value)) return value.map(sortJson);
739
+ if (!value || typeof value !== "object") return value;
740
+ return Object.fromEntries(
741
+ Object.entries(value)
742
+ .sort(([left], [right]) => left.localeCompare(right))
743
+ .map(([key, item]) => [key, sortJson(item)]),
744
+ );
745
+ }
746
+
747
+ function sha256(value) {
748
+ return crypto
749
+ .createHash("sha256")
750
+ .update(String(value || ""), "utf8")
751
+ .digest("hex");
752
+ }
753
+
754
+ function toolLooksMutating(payload) {
755
+ const name = normalizedToolName(payload);
756
+ if (MUTATING_TOOLS.has(name)) return true;
757
+ const command = toolCommandLine(payload);
758
+ return /\b(write|replace|edit|apply|publish|rm|mv|cp|sed|perl|python|node|bash|pwsh|powershell)\b/i.test(command);
759
+ }
760
+
761
+ function toolRunsRetiredSpecialistRunner(payload) {
762
+ return /\brun-specialist-task\b/.test(toolCommandLine(payload));
763
+ }
764
+
765
+ function toolCanCarryOperationalRawContent(payload) {
766
+ const name = normalizedToolName(payload);
767
+ if (["todo", "todowrite", "todo_write", "todoread", "todo_read"].includes(name)) return false;
768
+ if (
769
+ [
770
+ "bash",
771
+ "run_shell_command",
772
+ "task",
773
+ "invoke_subagent",
774
+ "send_message",
775
+ "write_file",
776
+ "write_to_file",
777
+ "edit",
778
+ "replace",
779
+ ].includes(name)
780
+ )
781
+ return true;
782
+ return toolLooksMutating(payload);
783
+ }
784
+
785
+ function guardWaitingAgentConcreteEffects(payload, directive) {
786
+ if (controlStatus(directive) !== "waiting_agent") return null;
787
+ const routes = concreteEffectRoutes(directive);
788
+ if (!routes.length) {
789
+ if (!toolLooksOperationalWhileEffectPending(payload)) return null;
790
+ return denyP0({
791
+ blockedReason: "agent_directive_effect_required",
792
+ reason: [
793
+ "agent_directive_effect_required: Bloqueei a chamada porque a FSM está em waiting_agent, ",
794
+ "mas agent_directive.control.effects não contém rota concreta executável. ",
795
+ "Trate isso como contract_gap do produtor da FSM; não continue por texto de resume, fase ou histórico.",
796
+ ].join(""),
797
+ agentMessage:
798
+ "A FSM precisa emitir agent_directive.control.effects com payload executável antes de qualquer continuação agentica.",
799
+ directiveField: "agent_directive.control.effects",
800
+ });
801
+ }
802
+ if (toolMatchesConcreteEffectRoute(payload, routes)) return null;
803
+ const defineSubagentGuard = guardPackagedDefineSubagentRoute(payload, routes);
804
+ if (defineSubagentGuard) return defineSubagentGuard;
805
+ if (!toolLooksOperationalWhileEffectPending(payload)) return null;
806
+ return denyP0({
807
+ blockedReason: "agent_directive_effect_required",
808
+ reason: [
809
+ "agent_directive_effect_required: Bloqueei a chamada porque a FSM já expôs efeito(s) ",
810
+ "executável(is) em agent_directive.control.effects. Execute a rota tipada do efeito atual ",
811
+ "ou aguarde novo payload oficial; não rederive a próxima ação por diagnóstico, fase ou histórico.",
812
+ ].join(""),
813
+ agentMessage:
814
+ "Use somente a rota concreta exposta por agent_directive.control.effects para continuar este estado waiting_agent.",
815
+ directiveField: "agent_directive.control.effects",
816
+ });
817
+ }
818
+
819
+ function guardPackagedDefineSubagentRoute(payload, routes) {
820
+ if (normalizedToolName(payload) !== "define_subagent") return null;
821
+ if (!routes.some((route) => route.kind === "define_subagent")) return null;
822
+ return denyP0({
823
+ blockedReason: "packaged_agent_template_required",
824
+ reason: [
825
+ "packaged_agent_template_required: Bloqueei define_subagent porque TypeName sozinho ",
826
+ "nao prova que o agente empacotado completo foi usado. Leia o template oficial e passe ",
827
+ "o prompt com packaged_agent_template_contract antes de definir o subagente.",
828
+ ].join(""),
829
+ agentMessage:
830
+ "Use o template empacotado completo e preserve packaged_agent_template_contract no prompt de define_subagent.",
831
+ directiveField: "agent_directive.control.effects[].payload.harness_routes",
832
+ });
833
+ }
834
+
835
+ function concreteEffectRoutes(directive) {
836
+ const routes = [];
837
+ const effects = Array.isArray(directive?.control?.effects) ? directive.control.effects : [];
838
+ for (const effect of effects) {
839
+ if (!effect || typeof effect !== "object" || Array.isArray(effect)) continue;
840
+ const payload =
841
+ effect.payload && typeof effect.payload === "object" && !Array.isArray(effect.payload) ? effect.payload : {};
842
+ const commandFamily = String(payload.command_family || effect.target || "").trim();
843
+ const args = Array.isArray(payload.arguments) ? payload.arguments.map((item) => String(item || "")) : [];
844
+ if (commandFamily && args.length) routes.push({ kind: "command", commandFamily, args });
845
+ addCommandRoute(routes, payload.apply_command);
846
+ const finalizers = Array.isArray(payload.receipt_finalizers) ? payload.receipt_finalizers : [];
847
+ for (const finalizer of finalizers) addCommandRoute(routes, finalizer);
848
+ addHarnessRoutes(routes, payload.harness_routes);
849
+ const items = Array.isArray(payload.current_batch_items) ? payload.current_batch_items : [];
850
+ if (items.length > 0) {
851
+ routes.push({
852
+ kind: "current_batch_items",
853
+ currentBatchItemsJson: canonicalJson({ current_batch_items: items }),
854
+ });
855
+ // The FSM authorizes the whole specialist batch. Harnesses such as
856
+ // OpenCode may execute that batch as one task per member, so each member
857
+ // is an allowed concrete route without becoming a new policy decision.
858
+ for (const item of items) {
859
+ if (!item || typeof item !== "object" || Array.isArray(item)) continue;
860
+ routes.push({
861
+ kind: "single_current_batch_item",
862
+ currentBatchItemJson: canonicalJson(item),
863
+ });
864
+ routes.push({
865
+ kind: "single_current_batch_item",
866
+ currentBatchItemJson: canonicalJson({ current_batch_items: [item] }),
867
+ });
868
+ }
869
+ }
870
+ }
871
+ return routes;
872
+ }
873
+
874
+ function addCommandRoute(routes, value) {
875
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
876
+ const commandFamily = String(value.command_family || "").trim();
877
+ const args = Array.isArray(value.arguments) ? value.arguments.map((item) => String(item || "")) : [];
878
+ if (commandFamily && args.length) routes.push({ kind: "command", commandFamily, args });
879
+ }
880
+
881
+ function addHarnessRoutes(routes, value) {
882
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
883
+ const agy = value.antigravity_cli && typeof value.antigravity_cli === "object" ? value.antigravity_cli : {};
884
+ const templateCandidates = Array.isArray(agy.template_path_candidates) ? agy.template_path_candidates : [];
885
+ const templatePaths = templateCandidates.map((candidate) => normalizePathText(candidate)).filter(Boolean);
886
+ for (const candidate of templateCandidates) {
887
+ const normalized = normalizePathText(candidate);
888
+ if (normalized) routes.push({ kind: "template_path", path: normalized });
889
+ }
890
+ const agentName = String(agy.agent_name || "").trim();
891
+ if (agy.define_subagent_source === "packaged_agent_template_only" && agentName) {
892
+ routes.push({
893
+ kind: "define_subagent",
894
+ agentName,
895
+ templatePaths,
896
+ promptContract: String(agy.prompt_contract || "single_current_batch_items_json").trim(),
897
+ packagedTemplateContract: String(
898
+ agy.packaged_agent_template_contract || "medical-notes-workbench.packaged-agent-template.v1",
899
+ ).trim(),
900
+ });
901
+ }
902
+ }
903
+
904
+ function toolMatchesConcreteEffectRoute(payload, routes) {
905
+ const command = toolCommandLine(payload);
906
+ if (command) {
907
+ const invocation = parseSingleCommandInvocation(command);
908
+ for (const route of routes) {
909
+ if (route.kind !== "command") continue;
910
+ if (invocation && commandInvocationMatchesRoute(invocation, route)) return true;
911
+ }
912
+ }
913
+ const embedded = toolEmbeddedJsonObjects(payload);
914
+ for (const route of routes) {
915
+ if (route.kind !== "current_batch_items") continue;
916
+ if (embedded.some((item) => canonicalJson(item) === route.currentBatchItemsJson)) return true;
917
+ }
918
+ for (const route of routes) {
919
+ if (route.kind !== "single_current_batch_item") continue;
920
+ if (embedded.some((item) => canonicalJson(item) === route.currentBatchItemJson)) return true;
921
+ }
922
+ const target = normalizePathText(toolTargetPath(payload));
923
+ for (const route of routes) {
924
+ if (route.kind !== "template_path") continue;
925
+ if (target && target === route.path) return true;
926
+ }
927
+ if (normalizedToolName(payload) === "define_subagent") {
928
+ for (const route of routes) {
929
+ if (route.kind === "define_subagent" && packagedDefineSubagentMatchesRoute(payload, route)) return true;
930
+ }
931
+ }
932
+ return false;
933
+ }
934
+
935
+ function parseSingleCommandInvocation(command) {
936
+ // Command effects authorize one concrete CLI invocation. Text probes,
937
+ // shell chaining and interpreter snippets must not satisfy a route.
938
+ if (!commandLooksLikeSingleRouteInvocation(command)) return null;
939
+ if (/(`|\$\(|>|<|\n|\r)/.test(command)) return null;
940
+ const tokens = shellWords(command);
941
+ if (!tokens?.length) return null;
942
+ if (tokens.some((token) => shellControlToken(token))) return null;
943
+ if (tokens.some((token) => interpreterExecutionFlag(token))) return null;
944
+ const first = commandTokenBasename(tokens[0]);
945
+ return {
946
+ tokens,
947
+ normalized: tokens.map((token) => normalizeCommandToken(token)),
948
+ first,
949
+ };
950
+ }
951
+
952
+ function shellWords(command) {
953
+ const words = [];
954
+ let current = "";
955
+ let quote = "";
956
+ let escaped = false;
957
+ for (const char of String(command || "")) {
958
+ if (escaped) {
959
+ current += char;
960
+ escaped = false;
961
+ continue;
962
+ }
963
+ if (char === "\\") {
964
+ escaped = true;
965
+ continue;
966
+ }
967
+ if (quote) {
968
+ if (char === quote) {
969
+ quote = "";
970
+ } else {
971
+ current += char;
972
+ }
973
+ continue;
974
+ }
975
+ if (char === "'" || char === '"') {
976
+ quote = char;
977
+ continue;
978
+ }
979
+ if (/\s/.test(char)) {
980
+ if (current) {
981
+ words.push(current);
982
+ current = "";
983
+ }
984
+ continue;
985
+ }
986
+ current += char;
987
+ }
988
+ if (escaped || quote) return null;
989
+ if (current) words.push(current);
990
+ return words;
991
+ }
992
+
993
+ function commandInvocationMatchesRoute(invocation, route) {
994
+ const family = normalizeCommandFamily(route.commandFamily);
995
+ if (!family || !commandExecutableCanHostRoute(invocation, family)) return false;
996
+ const familyIndex = routeFamilyTokenIndex(invocation, family);
997
+ if (familyIndex < 0) return false;
998
+ return routeArgsMatch(invocation.normalized, route.args, familyIndex + 1);
999
+ }
1000
+
1001
+ function commandExecutableCanHostRoute(invocation, family) {
1002
+ if (invocation.first === family) return true;
1003
+ return ["uv", "python", "python3", "node", "run_python.mjs", "cli.py", "mednotes"].includes(invocation.first);
1004
+ }
1005
+
1006
+ function routeFamilyTokenIndex(invocation, family) {
1007
+ for (let index = 0; index < invocation.normalized.length; index += 1) {
1008
+ const token = invocation.normalized[index];
1009
+ if (!tokenMatchesCommandFamily(token, family)) continue;
1010
+ if (index === 0) return index;
1011
+ if (previousTokenCanIntroduceSubcommand(invocation.normalized[index - 1])) return index;
1012
+ }
1013
+ return -1;
1014
+ }
1015
+
1016
+ function previousTokenCanIntroduceSubcommand(token) {
1017
+ return ["cli.py", "mednotes", "wiki"].includes(commandTokenBasename(token));
1018
+ }
1019
+
1020
+ function routeArgsMatch(tokens, expectedArgs, startIndex) {
1021
+ let cursor = startIndex;
1022
+ for (let index = 0; index < expectedArgs.length; index += 1) {
1023
+ const expected = normalizeCommandToken(expectedArgs[index]);
1024
+ if (!expected) continue;
1025
+ const nextExpected = index + 1 < expectedArgs.length ? normalizeCommandToken(expectedArgs[index + 1]) : "";
1026
+ const found = findExpectedArg(tokens, expected, nextExpected, cursor);
1027
+ if (!found) return false;
1028
+ cursor = found.index + 1;
1029
+ if (found.consumedNext) index += 1;
1030
+ }
1031
+ return cursor >= tokens.length;
1032
+ }
1033
+
1034
+ function shellControlToken(token) {
1035
+ return /[;&|]/.test(String(token || ""));
1036
+ }
1037
+
1038
+ function interpreterExecutionFlag(token) {
1039
+ const value = String(token || "");
1040
+ return value === "-c" || value === "--command" || value === "-e" || /^-[^-]*[ce]/.test(value);
1041
+ }
1042
+
1043
+ function findExpectedArg(tokens, expected, nextExpected, startIndex) {
1044
+ for (let index = startIndex; index < tokens.length; index += 1) {
1045
+ if (tokens[index] === expected) return { index, consumedNext: false };
1046
+ if (expected.startsWith("--") && nextExpected && tokens[index] === `${expected}=${nextExpected}`) {
1047
+ return { index, consumedNext: true };
1048
+ }
1049
+ }
1050
+ return null;
1051
+ }
1052
+
1053
+ function tokenMatchesCommandFamily(token, family) {
1054
+ const base = commandTokenBasename(token);
1055
+ return token === family || base === family;
1056
+ }
1057
+
1058
+ function normalizeCommandFamily(value) {
1059
+ return commandTokenBasename(value);
1060
+ }
1061
+
1062
+ function normalizeCommandToken(value) {
1063
+ return normalizePathText(value).trim();
1064
+ }
1065
+
1066
+ function commandTokenBasename(value) {
1067
+ return path.posix.basename(normalizeCommandToken(value)).toLowerCase();
1068
+ }
1069
+
1070
+ function packagedDefineSubagentMatchesRoute(payload, route) {
1071
+ const input = toolInput(payload);
1072
+ const typeName = String(input.TypeName || input.typeName || input.name || input.Name || "").trim();
1073
+ if (typeName !== route.agentName) return false;
1074
+ const prompt = String(input.Prompt || input.prompt || input.SystemPrompt || input.systemPrompt || "");
1075
+ if (!prompt) return false;
1076
+ if (!prompt.includes(route.agentName)) return false;
1077
+ if (!prompt.includes(route.packagedTemplateContract)) return false;
1078
+ const templatePath = toolTemplatePath(payload);
1079
+ if (templatePath && route.templatePaths?.length && !route.templatePaths.includes(templatePath)) return false;
1080
+ return true;
1081
+ }
1082
+
1083
+ function toolTemplatePath(payload) {
1084
+ const input = toolInput(payload);
1085
+ return normalizePathText(
1086
+ input.TemplatePath ||
1087
+ input.templatePath ||
1088
+ input.SourcePath ||
1089
+ input.sourcePath ||
1090
+ input.TemplateFile ||
1091
+ input.templateFile ||
1092
+ "",
1093
+ );
1094
+ }
1095
+
1096
+ function toolEmbeddedJsonObjects(payload) {
1097
+ const input = toolInput(payload);
1098
+ const candidates = [
1099
+ input.prompt,
1100
+ input.Prompt,
1101
+ input.message,
1102
+ input.Message,
1103
+ input.content,
1104
+ input.Content,
1105
+ input.text,
1106
+ ];
1107
+ for (const subagent of subagentToolInputs(input)) {
1108
+ candidates.push(subagent.Prompt, subagent.prompt);
1109
+ }
1110
+ const objects = [];
1111
+ for (const candidate of candidates) {
1112
+ if (typeof candidate !== "string") continue;
1113
+ const parsed = parseWholeJsonObject(candidate);
1114
+ if (parsed && Object.keys(parsed).length > 0) objects.push(parsed);
1115
+ }
1116
+ return objects;
1117
+ }
1118
+
1119
+ function commandLooksLikeSingleRouteInvocation(command) {
1120
+ // A command route authorizes one official continuation command, not a shell
1121
+ // probe chained before or after it.
1122
+ return !/(;|\n|\|)/.test(command);
1123
+ }
1124
+
1125
+ function subagentToolInputs(input) {
1126
+ const subagents = Array.isArray(input.Subagents)
1127
+ ? input.Subagents
1128
+ : Array.isArray(input.subagents)
1129
+ ? input.subagents
1130
+ : [];
1131
+ return subagents.filter((item) => item && typeof item === "object" && !Array.isArray(item));
1132
+ }
1133
+
1134
+ function toolLooksOperationalWhileEffectPending(payload) {
1135
+ const name = normalizedToolName(payload);
1136
+ if (
1137
+ [
1138
+ "glob",
1139
+ "grep",
1140
+ "grep_search",
1141
+ "define_subagent",
1142
+ "invoke_agent",
1143
+ "invoke_subagent",
1144
+ "list_dir",
1145
+ "list_directory",
1146
+ "read",
1147
+ "read_file",
1148
+ "search_file_content",
1149
+ "send_message",
1150
+ "task",
1151
+ "todo",
1152
+ "todowrite",
1153
+ "todo_write",
1154
+ "view_file",
1155
+ ].includes(name)
1156
+ ) {
1157
+ return true;
1158
+ }
1159
+ return Boolean(toolCommandLine(payload)) || toolLooksMutating(payload);
1160
+ }
1161
+
1162
+ function toolRunsAgentReportValidation(payload) {
1163
+ return /\bvalidate-agent-run-report\b/.test(toolCommandLine(payload));
1164
+ }
1165
+
1166
+ function toolRunsVaultGuardFinish(payload) {
1167
+ const command = toolCommandLine(payload);
1168
+ return /\bvault_git\.py["']?\s+run-finish\b/.test(command);
1169
+ }
1170
+
1171
+ function toolWritesArtifactMetadataToWorkbenchTemp(payload) {
1172
+ const name = normalizedToolName(payload);
1173
+ if (name !== "write_to_file" && name !== "write_file") return false;
1174
+ const input = toolInput(payload);
1175
+ const hasArtifactMetadata =
1176
+ Object.hasOwn(input, "ArtifactMetadata") ||
1177
+ Object.hasOwn(input, "artifactMetadata") ||
1178
+ Object.hasOwn(input, "artifact_metadata");
1179
+ if (!hasArtifactMetadata) return false;
1180
+ return isWorkbenchAgentWorkTempPath(toolTargetPath(payload));
1181
+ }
1182
+
1183
+ function isWorkbenchAgentWorkTempPath(value) {
1184
+ const normalized = normalizePathText(value);
1185
+ return normalized.includes("/mednotes-home/tmp/agent-work/") || normalized.includes("/.mednotes/tmp/agent-work/");
1186
+ }
1187
+
1188
+ function isAllowedPrePayloadTool(payload, intent) {
1189
+ const name = normalizedToolName(payload);
1190
+ const target = toolTargetPath(payload);
1191
+ if ((name === "view_file" || name === "read_file") && isOfficialSkillRead(target, intent)) return true;
1192
+ if (isOfficialPrePayloadCommand(payload, intent)) return true;
1193
+ return false;
1194
+ }
1195
+
1196
+ async function isOfficialTaskLogRead(payload, intent) {
1197
+ if (!toolLooksLikeFileRead(payload)) return false;
1198
+ const target = toolTargetPath(payload);
1199
+ if (!isAgyTaskLogPath(target)) return false;
1200
+ return transcriptReferencesOfficialTaskLog(payload, intent, target);
1201
+ }
1202
+
1203
+ async function transcriptReferencesOfficialTaskLog(payload, intent, target) {
1204
+ const transcriptPath = String(payload?.transcript_path || "");
1205
+ if (!transcriptPath) return false;
1206
+ try {
1207
+ const stats = await fs.stat(transcriptPath);
1208
+ if (!stats.isFile() || stats.size > MAX_TASK_LOG_BYTES) return false;
1209
+ const text = await fs.readFile(transcriptPath, "utf8");
1210
+ const normalizedTarget = normalizePathText(target);
1211
+ if (!text.includes(normalizedTarget) && !text.includes(`file://${normalizedTarget}`)) return false;
1212
+ void intent;
1213
+ return officialWorkflowCommandPattern().test(text);
1214
+ } catch {
1215
+ return false;
1216
+ }
1217
+ }
1218
+
1219
+ function officialWorkflowCommandPattern() {
1220
+ return /\b(mednotes|flashcards|wiki)[^\n]+--json\b/i;
1221
+ }
1222
+
1223
+ function toolLooksLikeFileRead(payload) {
1224
+ const name = normalizedToolName(payload);
1225
+ return name === "read" || name === "view_file" || name === "read_file";
1226
+ }
1227
+
1228
+ function toolTargetPath(payload) {
1229
+ const input = toolInput(payload);
1230
+ return String(
1231
+ input.file_path ||
1232
+ input.filePath ||
1233
+ input.path ||
1234
+ input.target_file ||
1235
+ input.targetFile ||
1236
+ input.TargetFile ||
1237
+ input.AbsolutePath ||
1238
+ input.SearchDirectory ||
1239
+ input.searchDirectory ||
1240
+ input.DirectoryPath ||
1241
+ input.directoryPath ||
1242
+ "",
1243
+ );
1244
+ }
1245
+
1246
+ function isAgyTaskLogPath(target) {
1247
+ const normalized = normalizePathText(target);
1248
+ return (
1249
+ (normalized.includes("/.gemini/antigravity-cli/brain/") &&
1250
+ normalized.includes("/.system_generated/tasks/") &&
1251
+ /\/task-[^/]+\.log$/.test(normalized)) ||
1252
+ (normalized.includes("/.system_generated/tasks/") && /\/task-[^/]+\.log$/.test(normalized))
1253
+ );
1254
+ }
1255
+
1256
+ function isOfficialSkillRead(target, intent) {
1257
+ const normalized = normalizePathText(target);
1258
+ if (!normalized.endsWith("/SKILL.md")) return false;
1259
+ if (!normalized.includes("/skills/")) return false;
1260
+ const skillName =
1261
+ normalized
1262
+ .split("/skills/")
1263
+ .pop()
1264
+ ?.replace(/\/SKILL\.md$/, "") || "";
1265
+ if (isSharedOfficialSkill(skillName)) return true;
1266
+ const tokens = workflowTokens(intent);
1267
+ return tokens.length > 0 && tokens.every((token) => skillName.includes(token));
1268
+ }
1269
+
1270
+ function isOfficialPrePayloadCommand(payload, intent) {
1271
+ const command = toolCommandLine(payload);
1272
+ if (!command) return false;
1273
+ const invocation = parseSingleCommandInvocation(command);
1274
+ if (!invocation) return false;
1275
+ if (isOfficialVaultRunStartInvocation(invocation)) return true;
1276
+ return isOfficialWorkbenchInvocation(invocation, intent);
1277
+ }
1278
+
1279
+ function workflowTokens(intent) {
1280
+ return String(intent?.workflow || "")
1281
+ .replace(/^\//, "")
1282
+ .split(/[:/_-]+/)
1283
+ .map((item) => item.trim().toLowerCase())
1284
+ .filter((item) => item && !["mednotes", "medical"].includes(item));
1285
+ }
1286
+
1287
+ function isSharedOfficialSkill(skillName) {
1288
+ return ["obsidian-cli", "obsidian-markdown", "obsidian-ops"].includes(String(skillName || ""));
1289
+ }
1290
+
1291
+ function normalizePathText(value) {
1292
+ return String(value || "")
1293
+ .replace(/^["']|["']$/g, "")
1294
+ .replace(/^file:\/+/, "/")
1295
+ .replace(/\\/g, "/")
1296
+ .replace(/\/+/g, "/");
1297
+ }
1298
+
1299
+ function firstUnsupportedParameter(value, depth = 0, unsupported = UNSUPPORTED_TOOL_PARAMETERS) {
1300
+ if (!value || typeof value !== "object" || depth > 4) return "";
1301
+ if (Array.isArray(value)) {
1302
+ for (const item of value) {
1303
+ const found = firstUnsupportedParameter(item, depth + 1, unsupported);
1304
+ if (found) return found;
1305
+ }
1306
+ return "";
1307
+ }
1308
+ for (const [key, item] of Object.entries(value)) {
1309
+ if (unsupported.has(key)) return key;
1310
+ const found = firstUnsupportedParameter(item, depth + 1, unsupported);
1311
+ if (found) return found;
1312
+ }
1313
+ return "";
1314
+ }
1315
+
1316
+ function toolCreatesAdHocScript(payload) {
1317
+ const input = toolInput(payload);
1318
+ const _name = normalizedToolName(payload);
1319
+ const filePath = String(
1320
+ input.file_path || input.filePath || input.path || input.target_file || input.targetFile || "",
1321
+ );
1322
+ if (filePath && SCRIPT_EXTENSIONS.has(path.extname(filePath).toLowerCase())) return true;
1323
+ const command = toolCommandLine(payload);
1324
+ if (!command) return false;
1325
+ if (isOfficialWorkbenchCommand(command) || toolRunsVaultGuardFinish(payload)) return false;
1326
+ return (
1327
+ /\b(python3?|node|bash|sh|pwsh|powershell)\b/i.test(command) && /\.(py|js|mjs|cjs|sh|ps1|cmd|bat)\b/i.test(command)
1328
+ );
1329
+ }
1330
+
1331
+ function isOfficialWorkbenchCommand(command) {
1332
+ const invocation = parseSingleCommandInvocation(command);
1333
+ return Boolean(invocation && isOfficialWorkbenchInvocation(invocation, null));
1334
+ }
1335
+
1336
+ function isOfficialVaultRunStartInvocation(invocation) {
1337
+ const scriptIndex = invocation.normalized.findIndex((token) => commandTokenBasename(token) === "vault_git.py");
1338
+ if (scriptIndex < 0 || !officialScriptInvocationHostIsValid(invocation, scriptIndex)) return false;
1339
+ return scriptIndex >= 0 && invocation.normalized[scriptIndex + 1] === "run-start";
1340
+ }
1341
+
1342
+ function isOfficialWorkbenchInvocation(invocation, intent) {
1343
+ if (invocation.normalized.at(-1) !== "--json") return false;
1344
+ const scriptIndex = invocation.normalized.findIndex((token) => isOfficialWikiCliScriptToken(token));
1345
+ if (scriptIndex < 0) return false;
1346
+ if (!officialScriptInvocationHostIsValid(invocation, scriptIndex)) return false;
1347
+ const expected = workflowSubcommand(intent);
1348
+ if (!expected) return true;
1349
+ return invocation.normalized.slice(scriptIndex + 1, -1).includes(expected);
1350
+ }
1351
+
1352
+ function officialScriptInvocationHostIsValid(invocation, scriptIndex) {
1353
+ const tokens = invocation.normalized;
1354
+ if (scriptIndex === 0) return ["cli.py", "vault_git.py"].includes(invocation.first);
1355
+ if (scriptIndex === 1 && invocation.first === "run_python.mjs") return true;
1356
+ if (scriptIndex === 2 && invocation.first === "node" && commandTokenBasename(tokens[1]) === "run_python.mjs") {
1357
+ return true;
1358
+ }
1359
+ if (
1360
+ scriptIndex === 3 &&
1361
+ invocation.first === "uv" &&
1362
+ tokens[1] === "run" &&
1363
+ ["python", "python3"].includes(commandTokenBasename(tokens[2]))
1364
+ ) {
1365
+ return true;
1366
+ }
1367
+ if (
1368
+ scriptIndex === 4 &&
1369
+ invocation.first === "uv" &&
1370
+ tokens[1] === "run" &&
1371
+ commandTokenBasename(tokens[2]) === "node" &&
1372
+ commandTokenBasename(tokens[3]) === "run_python.mjs"
1373
+ ) {
1374
+ return true;
1375
+ }
1376
+ return false;
1377
+ }
1378
+
1379
+ function isOfficialWikiCliScriptToken(token) {
1380
+ return normalizeCommandToken(token).endsWith("scripts/mednotes/wiki/cli.py");
1381
+ }
1382
+
1383
+ function workflowSubcommand(intent) {
1384
+ const workflow = String(intent?.workflow || "").trim();
1385
+ if (!workflow) return "";
1386
+ if (workflow === "/flashcards") return "flashcards";
1387
+ if (workflow.startsWith("/mednotes:")) return workflow.slice("/mednotes:".length);
1388
+ return workflow.replace(/^\//, "");
1389
+ }
1390
+
1391
+ function toolContainsRawContent(value, depth = 0) {
1392
+ if (!value || typeof value !== "object" || depth > 4) return false;
1393
+ if (Array.isArray(value)) return value.some((item) => toolContainsRawContent(item, depth + 1));
1394
+ if (specialistBatchEmbedsRawContent(value)) return true;
1395
+ for (const [key, item] of Object.entries(value)) {
1396
+ const lower = key.toLowerCase();
1397
+ if (
1398
+ [
1399
+ "content",
1400
+ "prompt",
1401
+ "message",
1402
+ "markdown",
1403
+ "html",
1404
+ "raw",
1405
+ "raw_chat",
1406
+ "raw_markdown",
1407
+ "raw_markdown_content",
1408
+ "note_text",
1409
+ ].includes(lower) &&
1410
+ typeof item === "string"
1411
+ ) {
1412
+ if (textLooksLikeJsonPayload(item)) {
1413
+ try {
1414
+ if (toolContainsRawContent(JSON.parse(item), depth + 1)) return true;
1415
+ } catch {
1416
+ if (textLooksLikeRawContent(item)) return true;
1417
+ }
1418
+ } else if (textLooksLikeRawContent(item)) {
1419
+ return true;
1420
+ }
1421
+ }
1422
+ if (item && typeof item === "object" && toolContainsRawContent(item, depth + 1)) return true;
1423
+ }
1424
+ return false;
1425
+ }
1426
+
1427
+ function specialistBatchEmbedsRawContent(value) {
1428
+ // Specialist work items may be valid JSON routes but still violate the FSM
1429
+ // raw-content contract; detect that before the harness launches a subagent.
1430
+ const items = Array.isArray(value.current_batch_items)
1431
+ ? value.current_batch_items
1432
+ : Array.isArray(value.currentBatchItems)
1433
+ ? value.currentBatchItems
1434
+ : [];
1435
+ return items.some((item) => specialistWorkItemEmbedsRawContent(item));
1436
+ }
1437
+
1438
+ function specialistWorkItemEmbedsRawContent(value, parentKey = "") {
1439
+ if (Array.isArray(value)) return value.some((item) => specialistWorkItemEmbedsRawContent(item, parentKey));
1440
+ if (!value || typeof value !== "object") {
1441
+ if (typeof value !== "string") return false;
1442
+ const key = String(parentKey || "").toLowerCase();
1443
+ if (SPECIALIST_WORK_ITEM_SAFE_TEXT_KEYS.has(key)) return false;
1444
+ return textLooksLikeRawContent(value);
1445
+ }
1446
+ for (const [key, item] of Object.entries(value)) {
1447
+ const normalizedKey = String(key || "").toLowerCase();
1448
+ if (SPECIALIST_WORK_ITEM_RAW_CONTENT_KEYS.has(normalizedKey)) return true;
1449
+ if (specialistWorkItemEmbedsRawContent(item, normalizedKey)) return true;
1450
+ }
1451
+ return false;
1452
+ }
1453
+
1454
+ function textLooksLikeJsonPayload(value) {
1455
+ const text = String(value || "").trim();
1456
+ return (text.startsWith("{") && text.endsWith("}")) || (text.startsWith("[") && text.endsWith("]"));
1457
+ }
1458
+
1459
+ function textLooksLikeRawContent(value) {
1460
+ const text = String(value || "");
1461
+ if (/<(?:!doctype|html|body|article|section)\b/i.test(text)) return true;
1462
+ if (/(^|\n)---\s*\n[\s\S]{0,200}\n---\s*\n#\s+/m.test(text)) return true;
1463
+ if (/(^|\n)#\s+[^\n]+\n/.test(text) && /\n##\s+/.test(text)) return true;
1464
+ if (/\bChats_Raw\b|\bWiki_Medicina\b|\[Chat Original\]|gemini\.google\.com\/app\//i.test(text)) return true;
1465
+ return false;
1466
+ }
1467
+
1468
+ function escapeRegExp(value) {
1469
+ return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1470
+ }