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,852 @@
1
+ """Audit real harness transcripts against the FSM-first workflow contract.
2
+
3
+ This module intentionally normalizes raw Gemini/AGY/OpenCode transcript shapes
4
+ into typed observations before applying policy. Adapters and transcript parsers
5
+ detect facts; audit rules decide whether those facts are deviations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ from pathlib import Path
13
+ from typing import Literal, cast
14
+
15
+ from pydantic import ValidationError as PydanticValidationError
16
+
17
+ from mednotes.domains.wiki.contracts.agent_run_audit import (
18
+ AgentTranscriptSource,
19
+ AuditConfidence,
20
+ AuditSeverity,
21
+ AuditStatus,
22
+ AuditWorkflow,
23
+ CanonicalArtifactKind,
24
+ CanonicalArtifactObservation,
25
+ HardeningRecommendation,
26
+ RecommendedAction,
27
+ SubagentInvocationObservation,
28
+ ToolCallObservation,
29
+ WorkflowDeviationFinding,
30
+ WorkflowTranscriptAuditResult,
31
+ WorkflowTranscriptAuditSummary,
32
+ )
33
+ from mednotes.kernel.base import JsonValue
34
+
35
+ RawJsonObject = dict[str, JsonValue]
36
+
37
+ _TRANSCRIPT_CHILD_KEYS = (
38
+ "$set",
39
+ "content",
40
+ "events",
41
+ "items",
42
+ "messages",
43
+ "records",
44
+ "response",
45
+ "responses",
46
+ "result",
47
+ "toolCalls",
48
+ "tool_calls",
49
+ "transcript",
50
+ )
51
+ _RAW_CONTENT_MARKERS = ("RAW_CHAT_BODY_START", "Chats_Raw content:", "```markdown", "<html", "<!doctype html")
52
+ _ARTIFACT_TOKEN = r"[^ \t\r\n\"'<>|;]*"
53
+ _LOCAL_PATH_PLACEHOLDER = "<local-path>"
54
+ _SENSITIVE_OUTPUT_PLACEHOLDER = "<redacted-sensitive-output>"
55
+ _AGY_CONFIG_SKILLS_PATH_RE = re.compile(
56
+ (
57
+ r"(?:/Users/[^\s\"']*?|/private/tmp/[^\s\"']*?|/tmp/[^\s\"']*?|"
58
+ r"\b[A-Za-z]:(?:\\{1,2})Users(?:\\{1,2})[^\s\"']*?)"
59
+ r"(?P<suffix>(?:[/\\]{1,2})\.gemini(?:[/\\]{1,2})config(?:[/\\]{1,2})skills"
60
+ r"(?:[/\\]{1,2})[^\s\"']+)"
61
+ ),
62
+ re.IGNORECASE,
63
+ )
64
+ _POSIX_LOCAL_PATH_RE = re.compile(r"/Users/[^\s\"']+|/private/tmp/[^\s\"']+|/tmp/[^\s\"']+")
65
+ _WINDOWS_USER_PATH_RE = re.compile(r"\b[A-Za-z]:(?:\\{1,2})Users(?:\\{1,2})[^\s\"']+")
66
+ _CANONICAL_ARTIFACT_PATTERNS: tuple[tuple[re.Pattern[str], CanonicalArtifactKind], ...] = (
67
+ (
68
+ re.compile(rf"(?P<path>{_ARTIFACT_TOKEN}triage-note-plan{_ARTIFACT_TOKEN}\.json)", re.IGNORECASE),
69
+ "triage_note_plan",
70
+ ),
71
+ (
72
+ re.compile(rf"(?P<path>{_ARTIFACT_TOKEN}raw-coverage{_ARTIFACT_TOKEN}\.json)", re.IGNORECASE),
73
+ "raw_coverage",
74
+ ),
75
+ (
76
+ re.compile(rf"(?P<path>{_ARTIFACT_TOKEN}manifest{_ARTIFACT_TOKEN}\.json)", re.IGNORECASE),
77
+ "manifest",
78
+ ),
79
+ (
80
+ re.compile(rf"(?P<path>{_ARTIFACT_TOKEN}receipt{_ARTIFACT_TOKEN}\.json)", re.IGNORECASE),
81
+ "receipt",
82
+ ),
83
+ (
84
+ re.compile(rf"(?P<path>{_ARTIFACT_TOKEN}\.rewrite\.md)", re.IGNORECASE),
85
+ "style_rewrite_output",
86
+ ),
87
+ (
88
+ re.compile(
89
+ rf"(?P<path>{_ARTIFACT_TOKEN}(?:pendencias|pendencias_processor|human-decision){_ARTIFACT_TOKEN}\.md)",
90
+ re.IGNORECASE,
91
+ ),
92
+ "human_decision_backlog",
93
+ ),
94
+ )
95
+ _SHELL_TOOL_NAMES = {"run_shell_command", "bash", "shell", "run_command", "run_shell"}
96
+ _DISCOVERY_TOOL_NAMES = {
97
+ "glob",
98
+ "grep",
99
+ "grep_search",
100
+ "list_dir",
101
+ "list_directory",
102
+ "read",
103
+ "read_file",
104
+ "search_file_content",
105
+ "view_file",
106
+ }
107
+ _DISCOVERY_SHELL_COMMAND_RE = re.compile(r"^\s*(?:rg|grep|find|ls|cat|sed)\b")
108
+
109
+
110
+ def audit_agent_transcript(
111
+ *,
112
+ transcript_path: Path | None,
113
+ workflow: AuditWorkflow = "unknown",
114
+ workflow_payload_path: Path | None = None,
115
+ final_report_path: Path | None = None,
116
+ runtime_log_paths: list[Path] | None = None,
117
+ ) -> WorkflowTranscriptAuditResult:
118
+ runtime_log_paths = runtime_log_paths or []
119
+ findings: list[WorkflowDeviationFinding] = []
120
+ sources = _sources(transcript_path, workflow_payload_path, final_report_path, runtime_log_paths)
121
+ transcript: JsonValue | None = None
122
+ if transcript_path is not None:
123
+ try:
124
+ transcript = _read_transcript(transcript_path)
125
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError, PydanticValidationError) as exc:
126
+ findings.append(
127
+ _finding(
128
+ code="agent.transcript_unreadable",
129
+ workflow=workflow,
130
+ severity="blocking_candidate",
131
+ confidence="high",
132
+ evidence_ref="transcript:load",
133
+ expected_contract="transcript must be readable JSON, JSONL, or text for post-run audit",
134
+ observed_behavior=f"transcript could not be parsed: {type(exc).__name__}",
135
+ recommended_action="test_fixture",
136
+ promotion_gate="keep as validation blocker for unreadable transcript inputs",
137
+ )
138
+ )
139
+ if transcript is not None:
140
+ observations = _collect_observations(transcript)
141
+ findings.extend(_detect_deviations(observations, workflow=workflow))
142
+ if workflow_payload_path is None:
143
+ findings.extend(_workflow_payload_missing_findings(transcript, workflow=workflow))
144
+ return _result(
145
+ workflow=workflow,
146
+ transcript_present=transcript_path is not None,
147
+ workflow_payload_present=workflow_payload_path is not None,
148
+ final_report_present=final_report_path is not None,
149
+ findings=findings,
150
+ sources=sources,
151
+ )
152
+
153
+
154
+ def _sources(
155
+ transcript_path: Path | None,
156
+ workflow_payload_path: Path | None,
157
+ final_report_path: Path | None,
158
+ runtime_log_paths: list[Path],
159
+ ) -> list[AgentTranscriptSource]:
160
+ items: list[AgentTranscriptSource] = [
161
+ _source(transcript_path, "transcript"),
162
+ _source(workflow_payload_path, "workflow_payload"),
163
+ _source(final_report_path, "final_report"),
164
+ ]
165
+ items.extend(_source(path, "runtime_log") for path in runtime_log_paths)
166
+ return items
167
+
168
+
169
+ def _source(
170
+ path: Path | None,
171
+ source_kind: Literal["transcript", "workflow_payload", "final_report", "runtime_log"],
172
+ ) -> AgentTranscriptSource:
173
+ present = path is not None and path.exists()
174
+ suffix = path.suffix.lower() if path is not None else ""
175
+ fmt: Literal["json", "jsonl", "text", "missing", "unknown"]
176
+ if not present:
177
+ fmt = "missing"
178
+ elif suffix == ".json":
179
+ fmt = "json"
180
+ elif suffix in {".jsonl", ".ndjson"}:
181
+ fmt = "jsonl"
182
+ elif suffix in {".txt", ".log", ".md"}:
183
+ fmt = "text"
184
+ else:
185
+ fmt = "unknown"
186
+ return AgentTranscriptSource(
187
+ path_label=path.name if path is not None else "",
188
+ source_kind=source_kind,
189
+ present=present,
190
+ format=fmt,
191
+ )
192
+
193
+
194
+ def _read_transcript(path: Path) -> JsonValue:
195
+ text = path.read_text(encoding="utf-8-sig")
196
+ suffix = path.suffix.lower()
197
+ if suffix == ".json":
198
+ return cast(JsonValue, json.loads(text))
199
+ if suffix in {".jsonl", ".ndjson"}:
200
+ records: list[JsonValue] = []
201
+ for line_number, line in enumerate(text.splitlines(), start=1):
202
+ stripped = line.strip()
203
+ if not stripped:
204
+ continue
205
+ try:
206
+ records.append(cast(JsonValue, json.loads(stripped)))
207
+ except json.JSONDecodeError as exc:
208
+ raise json.JSONDecodeError(f"{exc.msg} at JSONL line {line_number}", exc.doc, exc.pos) from exc
209
+ return {"records": records}
210
+ records = _json_records_from_mixed_text(text)
211
+ if records:
212
+ return {"records": records}
213
+ return {"records": [{"type": "text", "text": text}]}
214
+
215
+
216
+ def _json_records_from_mixed_text(text: str) -> list[JsonValue]:
217
+ records: list[JsonValue] = []
218
+ for line in text.splitlines():
219
+ stripped = line.strip()
220
+ if not stripped or not stripped.startswith(("{", "[")):
221
+ continue
222
+ try:
223
+ records.append(cast(JsonValue, json.loads(stripped)))
224
+ except json.JSONDecodeError:
225
+ continue
226
+ return records
227
+
228
+
229
+ def _collect_observations(
230
+ transcript: JsonValue,
231
+ ) -> list[ToolCallObservation | SubagentInvocationObservation | CanonicalArtifactObservation]:
232
+ observations: list[ToolCallObservation | SubagentInvocationObservation | CanonicalArtifactObservation] = []
233
+ subagent_seen = False
234
+ for index, record in enumerate(_iter_records(transcript)):
235
+ tool_name = _tool_name(record)
236
+ if not tool_name:
237
+ continue
238
+ params = _tool_parameters(record)
239
+ status = _tool_status(record)
240
+ raw_command_text = _command_text(tool_name, params)
241
+ raw_target_path = _target_path(params)
242
+ raw_output_text = _output_text(record)
243
+ command_text = _redacted_text(raw_command_text)
244
+ target_path = _redacted_text(raw_target_path)
245
+ output_excerpt = _redacted_excerpt(raw_output_text)
246
+ target_is_agy_config_skill = _is_agy_config_skills_path(raw_target_path)
247
+ observations.append(
248
+ ToolCallObservation(
249
+ index=index,
250
+ tool_name=tool_name,
251
+ status=status,
252
+ command_text=command_text,
253
+ target_path=target_path,
254
+ output_excerpt=output_excerpt,
255
+ agent_effect_pending_signal=_has_executable_agent_effect_signal(raw_output_text),
256
+ target_is_agy_config_skill=target_is_agy_config_skill,
257
+ stale_materialized_skill_signal=_has_stale_materialized_skill_signal(raw_output_text),
258
+ parameter_keys=sorted(params),
259
+ )
260
+ )
261
+ if tool_name in {"invoke_agent", "invoke_subagent", "send_message"}:
262
+ prompt = str(params.get("prompt") or params.get("message") or params.get("content") or "")
263
+ observations.append(
264
+ SubagentInvocationObservation(
265
+ index=index,
266
+ tool_name=tool_name,
267
+ agent_name=str(params.get("agent") or params.get("agent_name") or ""),
268
+ prompt_length=len(prompt),
269
+ has_work_item='"work_item"' in prompt or "work_item" in params,
270
+ has_raw_content_markers=_has_raw_content_markers(prompt),
271
+ )
272
+ )
273
+ subagent_seen = True
274
+ artifact = _artifact_match(raw_target_path or raw_command_text)
275
+ if artifact:
276
+ artifact_kind, artifact_path = artifact
277
+ observations.append(
278
+ CanonicalArtifactObservation(
279
+ index=index,
280
+ tool_name=tool_name,
281
+ path=artifact_path,
282
+ artifact_kind=artifact_kind,
283
+ after_subagent=subagent_seen,
284
+ )
285
+ )
286
+ return observations
287
+
288
+
289
+ def _iter_records(node: JsonValue) -> list[RawJsonObject]:
290
+ records: list[RawJsonObject] = []
291
+ if isinstance(node, dict):
292
+ if _tool_name(node):
293
+ records.append(node)
294
+ for key in _TRANSCRIPT_CHILD_KEYS:
295
+ child = node.get(key)
296
+ if child is not None:
297
+ records.extend(_iter_records(child))
298
+ elif isinstance(node, list):
299
+ for item in node:
300
+ records.extend(_iter_records(item))
301
+ return records
302
+
303
+
304
+ def _tool_name(record: RawJsonObject) -> str:
305
+ for key in ("tool_name", "name", "function_name", "recipient_name", "tool"):
306
+ value = record.get(key)
307
+ if isinstance(value, str) and value:
308
+ return value.rsplit(".", 1)[-1]
309
+ call = record.get("tool_call")
310
+ if isinstance(call, dict):
311
+ return _tool_name(call)
312
+ part = record.get("part")
313
+ if isinstance(part, dict):
314
+ return _tool_name(cast(RawJsonObject, part))
315
+ return ""
316
+
317
+
318
+ def _tool_parameters(record: RawJsonObject) -> RawJsonObject:
319
+ for key in ("arguments", "args", "parameters", "input"):
320
+ value = record.get(key)
321
+ if isinstance(value, dict):
322
+ return value
323
+ if isinstance(value, str):
324
+ try:
325
+ parsed = cast(JsonValue, json.loads(value))
326
+ except json.JSONDecodeError:
327
+ return {"text": value}
328
+ return parsed if isinstance(parsed, dict) else {"value": parsed}
329
+ for state in _state_objects(record):
330
+ value = state.get("input")
331
+ if isinstance(value, dict):
332
+ return cast(RawJsonObject, value)
333
+ call = record.get("tool_call")
334
+ if isinstance(call, dict):
335
+ return _tool_parameters(call)
336
+ part = record.get("part")
337
+ if isinstance(part, dict):
338
+ return _tool_parameters(cast(RawJsonObject, part))
339
+ return {}
340
+
341
+
342
+ def _tool_status(record: RawJsonObject) -> str:
343
+ for key in ("status", "state"):
344
+ value = record.get(key)
345
+ if isinstance(value, str):
346
+ return value
347
+ for state in _state_objects(record):
348
+ value = state.get("status")
349
+ if isinstance(value, str):
350
+ return value
351
+ return ""
352
+
353
+
354
+ def _command_text(tool_name: str, params: RawJsonObject) -> str:
355
+ if tool_name not in _SHELL_TOOL_NAMES:
356
+ return ""
357
+ for key in ("command", "cmd", "CommandLine", "commandLine"):
358
+ value = params.get(key)
359
+ if isinstance(value, str):
360
+ return value
361
+ return ""
362
+
363
+
364
+ def _target_path(params: RawJsonObject) -> str:
365
+ for key in (
366
+ "path",
367
+ "file_path",
368
+ "filePath",
369
+ "absolute_path",
370
+ "absolutePath",
371
+ "target_path",
372
+ "targetPath",
373
+ "output_path",
374
+ "outputPath",
375
+ "SearchDirectory",
376
+ "searchDirectory",
377
+ "DirectoryPath",
378
+ "directoryPath",
379
+ ):
380
+ value = params.get(key)
381
+ if isinstance(value, str):
382
+ return value
383
+ return ""
384
+
385
+
386
+ def _output_text(record: RawJsonObject) -> str:
387
+ result = record.get("result")
388
+ if isinstance(result, dict):
389
+ parts = [str(result.get(key) or "") for key in ("stdout", "stderr", "output", "text")]
390
+ return "\n".join(part for part in parts if part)
391
+ if isinstance(result, str):
392
+ return result
393
+ for state in _state_objects(record):
394
+ parts = [str(state.get(key) or "") for key in ("stdout", "stderr", "output", "text")]
395
+ text = "\n".join(part for part in parts if part)
396
+ if text:
397
+ return text
398
+ return str(record.get("output") or record.get("text") or "")
399
+
400
+
401
+ def _state_objects(record: RawJsonObject) -> list[RawJsonObject]:
402
+ """Return runtime state objects without treating nested args as tool calls."""
403
+
404
+ states: list[RawJsonObject] = []
405
+ value = record.get("state")
406
+ if isinstance(value, dict):
407
+ states.append(cast(RawJsonObject, value))
408
+ part = record.get("part")
409
+ if isinstance(part, dict):
410
+ part_state = part.get("state")
411
+ if isinstance(part_state, dict):
412
+ states.append(cast(RawJsonObject, part_state))
413
+ return states
414
+
415
+
416
+ def _has_executable_agent_effect_signal(text: str) -> bool:
417
+ """Detect FSM-to-agent continuation evidence inside raw tool output."""
418
+
419
+ if not text:
420
+ return False
421
+ try:
422
+ payload = cast(JsonValue, json.loads(text))
423
+ except json.JSONDecodeError:
424
+ lowered = text.lower()
425
+ return "agent_directive" in lowered and "waiting_agent" in lowered and "effects" in lowered
426
+ return _json_contains_executable_agent_effect(payload)
427
+
428
+
429
+ def _json_contains_executable_agent_effect(node: JsonValue) -> bool:
430
+ if isinstance(node, dict):
431
+ directive = node.get("agent_directive")
432
+ if isinstance(directive, dict) and _directive_has_executable_effect(cast(RawJsonObject, directive)):
433
+ return True
434
+ return any(_json_contains_executable_agent_effect(value) for value in node.values())
435
+ if isinstance(node, list):
436
+ return any(_json_contains_executable_agent_effect(item) for item in node)
437
+ return False
438
+
439
+
440
+ def _directive_has_executable_effect(directive: RawJsonObject) -> bool:
441
+ control = directive.get("control")
442
+ if not isinstance(control, dict):
443
+ return False
444
+ status = control.get("status")
445
+ can_continue = control.get("can_continue_now")
446
+ effects = control.get("effects")
447
+ if status != "waiting_agent" or can_continue is not True or not isinstance(effects, list):
448
+ return False
449
+ return any(isinstance(effect, dict) and effect.get("kind") for effect in effects)
450
+
451
+
452
+ def _redacted_excerpt(text: str, *, limit: int = 240) -> str:
453
+ if _has_raw_content_markers(text):
454
+ return _SENSITIVE_OUTPUT_PLACEHOLDER
455
+ cleaned = text.replace("\r", " ").replace("\n", " ").strip()
456
+ cleaned = _redacted_text(cleaned)
457
+ return cleaned[:limit]
458
+
459
+
460
+ def _redacted_text(text: str) -> str:
461
+ redacted = _AGY_CONFIG_SKILLS_PATH_RE.sub(_redacted_agy_config_skills_path, text)
462
+ redacted = _POSIX_LOCAL_PATH_RE.sub(_LOCAL_PATH_PLACEHOLDER, redacted)
463
+ return _WINDOWS_USER_PATH_RE.sub(_LOCAL_PATH_PLACEHOLDER, redacted)
464
+
465
+
466
+ def _redacted_agy_config_skills_path(match: re.Match[str]) -> str:
467
+ suffix = match.group("suffix").replace("\\", "/")
468
+ suffix = re.sub(r"/+", "/", suffix)
469
+ return f"{_LOCAL_PATH_PLACEHOLDER}{suffix}"
470
+
471
+
472
+ def _is_agy_config_skills_path(text: str) -> bool:
473
+ normalized = re.sub(r"/+", "/", text.replace("\\", "/")).lower()
474
+ return "/.gemini/config/skills/" in normalized
475
+
476
+
477
+ def _has_raw_content_markers(text: str) -> bool:
478
+ lowered = text.lower()
479
+ return any(marker.lower() in lowered for marker in _RAW_CONTENT_MARKERS) or len(text) > 8000
480
+
481
+
482
+ def _has_stale_materialized_skill_signal(text: str) -> bool:
483
+ lowered = text.lower()
484
+ return "stale" in lowered and ("materialized skill" in lowered or "config/skills" in lowered)
485
+
486
+
487
+ def _artifact_match(text: str) -> tuple[CanonicalArtifactKind, str] | None:
488
+ for pattern, kind in _CANONICAL_ARTIFACT_PATTERNS:
489
+ match = pattern.search(text)
490
+ if match:
491
+ return kind, _redacted_text(match.group("path"))
492
+ return None
493
+
494
+
495
+ def _detect_deviations(
496
+ observations: list[ToolCallObservation | SubagentInvocationObservation | CanonicalArtifactObservation],
497
+ *,
498
+ workflow: AuditWorkflow,
499
+ ) -> list[WorkflowDeviationFinding]:
500
+ findings: list[WorkflowDeviationFinding] = []
501
+ seen: set[tuple[str, str]] = set()
502
+ fsm_effect_pending = False
503
+ for observation in observations:
504
+ if isinstance(observation, ToolCallObservation) and fsm_effect_pending:
505
+ finding = _discovery_tool_while_effect_pending_finding(observation, workflow=workflow)
506
+ if finding is not None:
507
+ key = (finding.code, finding.evidence_ref)
508
+ if key not in seen:
509
+ seen.add(key)
510
+ findings.append(finding)
511
+ for finding in _findings_for_observation(observation, workflow=workflow):
512
+ key = (finding.code, finding.evidence_ref)
513
+ if key in seen:
514
+ continue
515
+ seen.add(key)
516
+ findings.append(finding)
517
+ if isinstance(observation, ToolCallObservation) and observation.agent_effect_pending_signal:
518
+ fsm_effect_pending = True
519
+ return findings
520
+
521
+
522
+ def _workflow_payload_missing_findings(transcript: JsonValue, *, workflow: AuditWorkflow) -> list[WorkflowDeviationFinding]:
523
+ if not _transcript_invokes_workflow(transcript, workflow=workflow):
524
+ return []
525
+ return [
526
+ _finding(
527
+ code="agent.workflow_payload_missing",
528
+ workflow=workflow,
529
+ severity="blocking_candidate",
530
+ confidence="high",
531
+ evidence_ref="transcript:workflow-invocation",
532
+ expected_contract="public workflow invocation must produce an official workflow payload before final audit can pass",
533
+ observed_behavior="transcript invoked a public workflow but no workflow payload path was provided or discovered",
534
+ recommended_action="runtime_guardrail",
535
+ promotion_gate="block the run and repeat only after the agent reaches the official workflow JSON payload",
536
+ )
537
+ ]
538
+
539
+
540
+ def _transcript_invokes_workflow(transcript: JsonValue, *, workflow: AuditWorkflow) -> bool:
541
+ text = "\n".join(_text_fragments(transcript)).lower()
542
+ if not text:
543
+ return False
544
+ if workflow == "fix-wiki":
545
+ return "/mednotes:fix-wiki" in text or "fix-wiki --apply" in text or "fix-wiki --dry-run" in text
546
+ if workflow == "process-chats":
547
+ return "/mednotes:process-chats" in text or "process-chats" in text or "process-medical-chats" in text
548
+ if workflow == "link":
549
+ return "/mednotes:link" in text
550
+ return any(marker in text for marker in ("/mednotes:", "/flashcards"))
551
+
552
+
553
+ def _text_fragments(node: JsonValue) -> list[str]:
554
+ fragments: list[str] = []
555
+ if isinstance(node, dict):
556
+ for key, value in node.items():
557
+ if isinstance(value, str) and key in {"content", "text", "message", "prompt", "command", "CommandLine"}:
558
+ fragments.append(value)
559
+ elif isinstance(value, (dict, list)):
560
+ fragments.extend(_text_fragments(value))
561
+ elif isinstance(node, list):
562
+ for item in node:
563
+ fragments.extend(_text_fragments(item))
564
+ return fragments
565
+
566
+
567
+ def _findings_for_observation(
568
+ observation: ToolCallObservation | SubagentInvocationObservation | CanonicalArtifactObservation,
569
+ *,
570
+ workflow: AuditWorkflow,
571
+ ) -> list[WorkflowDeviationFinding]:
572
+ if isinstance(observation, SubagentInvocationObservation):
573
+ return _subagent_findings(observation, workflow=workflow)
574
+ if isinstance(observation, CanonicalArtifactObservation):
575
+ return _canonical_artifact_findings(observation, workflow=workflow)
576
+ return _tool_observation_findings(observation, workflow=workflow)
577
+
578
+
579
+ def _subagent_findings(
580
+ observation: SubagentInvocationObservation,
581
+ *,
582
+ workflow: AuditWorkflow,
583
+ ) -> list[WorkflowDeviationFinding]:
584
+ if not observation.has_raw_content_markers:
585
+ return []
586
+ return [
587
+ _finding(
588
+ code="agent.subagent_raw_content_contract_violation",
589
+ workflow=workflow,
590
+ severity="blocking_candidate",
591
+ confidence="high",
592
+ evidence_ref=f"tool:{observation.index}",
593
+ expected_contract=(
594
+ "subagents receive work_item, official paths, hashes, and output paths instead of raw clinical content"
595
+ ),
596
+ observed_behavior=(
597
+ f"{observation.tool_name} prompt length was {observation.prompt_length} and included raw-content markers"
598
+ ),
599
+ recommended_action="runtime_guardrail",
600
+ promotion_gate="block immediately once a synthetic fixture covers the marker and long-prompt path",
601
+ )
602
+ ]
603
+
604
+
605
+ def _canonical_artifact_findings(
606
+ observation: CanonicalArtifactObservation,
607
+ *,
608
+ workflow: AuditWorkflow,
609
+ ) -> list[WorkflowDeviationFinding]:
610
+ if observation.tool_name not in {"write_file", "write_to_file", "replace"}:
611
+ return []
612
+ if observation.artifact_kind == "human_decision_backlog":
613
+ return []
614
+ code = (
615
+ "agent.parent_canonical_artifact_write_after_subagent"
616
+ if observation.after_subagent
617
+ else "agent.parent_canonical_artifact_write_before_subagent"
618
+ )
619
+ return [
620
+ _finding(
621
+ code=code,
622
+ workflow=workflow,
623
+ severity="contract_violation",
624
+ confidence="high",
625
+ evidence_ref=f"tool:{observation.index}",
626
+ expected_contract=(
627
+ "canonical workflow artifacts are produced by official CLI, typed adapters, or signed subagent outputs"
628
+ ),
629
+ observed_behavior=f"parent used {observation.tool_name} for {observation.artifact_kind}",
630
+ recommended_action="test_fixture",
631
+ promotion_gate=(
632
+ "promote to runtime guardrail when the artifact can drive publish, apply, coverage, linker, "
633
+ "or report status"
634
+ ),
635
+ )
636
+ ]
637
+
638
+
639
+ def _tool_observation_findings(
640
+ observation: ToolCallObservation,
641
+ *,
642
+ workflow: AuditWorkflow,
643
+ ) -> list[WorkflowDeviationFinding]:
644
+ findings: list[WorkflowDeviationFinding] = []
645
+ if _is_recoverable_tool_error(observation):
646
+ findings.append(
647
+ _finding(
648
+ code="agent.recoverable_tool_error_observed",
649
+ workflow=workflow,
650
+ severity="warning",
651
+ confidence="medium",
652
+ evidence_ref=f"tool:{observation.index}",
653
+ expected_contract=(
654
+ "recoverable tool errors are reported as execution friction without becoming primary workflow failure"
655
+ ),
656
+ observed_behavior=f"{observation.tool_name} returned status={observation.status}",
657
+ recommended_action="document",
658
+ promotion_gate=(
659
+ "promote only if retry hides the error from final report or mutates state through an unofficial path"
660
+ ),
661
+ )
662
+ )
663
+ if _is_agy_materialized_skill_stale_instruction(observation):
664
+ findings.append(
665
+ _finding(
666
+ code="agent.agy_materialized_skill_misclassified_as_stale",
667
+ workflow=workflow,
668
+ severity="warning",
669
+ confidence="high",
670
+ evidence_ref=f"tool:{observation.index}",
671
+ expected_contract="AGY config/skills materialization is acceptable when it is the native runtime surface",
672
+ observed_behavior="skill text instructs the agent to treat config/skills as stale context",
673
+ recommended_action="prompt_hardening",
674
+ promotion_gate="promote when the instruction causes bypass of activate_skill or native AGY orchestration",
675
+ )
676
+ )
677
+ return findings
678
+
679
+
680
+ def _discovery_tool_while_effect_pending_finding(
681
+ observation: ToolCallObservation,
682
+ *,
683
+ workflow: AuditWorkflow,
684
+ ) -> WorkflowDeviationFinding | None:
685
+ if not _is_discovery_tool_while_fsm_effect_pending(observation):
686
+ return None
687
+ return _finding(
688
+ code="agent.discovery_tool_while_fsm_effect_pending",
689
+ workflow=workflow,
690
+ severity="blocking_candidate",
691
+ confidence="high",
692
+ evidence_ref=f"tool:{observation.index}",
693
+ expected_contract=(
694
+ "once agent_directive.control.effects exposes an executable FSM continuation, "
695
+ "the agent follows that official effect instead of probing files or manifests"
696
+ ),
697
+ observed_behavior=(
698
+ f"agent used {observation.tool_name} while an executable FSM effect was already pending"
699
+ ),
700
+ recommended_action="runtime_guardrail",
701
+ promotion_gate="block controlled run promotion and harden hooks/audit before repeating the experiment",
702
+ )
703
+
704
+
705
+ def _is_discovery_tool_while_fsm_effect_pending(observation: ToolCallObservation) -> bool:
706
+ tool_name = observation.tool_name.lower()
707
+ if tool_name in _DISCOVERY_TOOL_NAMES:
708
+ return True
709
+ if tool_name in _SHELL_TOOL_NAMES:
710
+ return bool(_DISCOVERY_SHELL_COMMAND_RE.search(observation.command_text))
711
+ return False
712
+
713
+
714
+ def _is_recoverable_tool_error(observation: ToolCallObservation) -> bool:
715
+ if observation.tool_name not in _SHELL_TOOL_NAMES:
716
+ return False
717
+ if observation.status.lower() not in {"error", "failed"}:
718
+ return False
719
+ command = observation.command_text.lower()
720
+ if any(mutating in command for mutating in (" --apply", " publish-batch", " apply-", " taxonomy-apply")):
721
+ return False
722
+ return True
723
+
724
+
725
+ def _is_agy_materialized_skill_stale_instruction(observation: ToolCallObservation) -> bool:
726
+ return observation.target_is_agy_config_skill and observation.stale_materialized_skill_signal
727
+
728
+
729
+ def _finding(
730
+ *,
731
+ code: str,
732
+ workflow: AuditWorkflow,
733
+ severity: str,
734
+ confidence: str,
735
+ evidence_ref: str,
736
+ expected_contract: str,
737
+ observed_behavior: str,
738
+ recommended_action: str,
739
+ promotion_gate: str,
740
+ ) -> WorkflowDeviationFinding:
741
+ return WorkflowDeviationFinding(
742
+ code=code,
743
+ workflow=workflow,
744
+ severity=cast(AuditSeverity, severity),
745
+ confidence=cast(AuditConfidence, confidence),
746
+ evidence_ref=evidence_ref,
747
+ expected_contract=expected_contract,
748
+ observed_behavior=observed_behavior,
749
+ recommended_action=cast(RecommendedAction, recommended_action),
750
+ promotion_gate=promotion_gate,
751
+ )
752
+
753
+
754
+ def _result(
755
+ *,
756
+ workflow: AuditWorkflow,
757
+ transcript_present: bool,
758
+ workflow_payload_present: bool,
759
+ final_report_present: bool,
760
+ findings: list[WorkflowDeviationFinding],
761
+ sources: list[AgentTranscriptSource],
762
+ ) -> WorkflowTranscriptAuditResult:
763
+ summary = _summary(findings)
764
+ status = "clean" if not findings else "blocked" if summary.blocking_candidate_count else "findings"
765
+ return WorkflowTranscriptAuditResult(
766
+ status=cast(AuditStatus, status),
767
+ workflow=workflow,
768
+ transcript_present=transcript_present,
769
+ workflow_payload_present=workflow_payload_present,
770
+ final_report_present=final_report_present,
771
+ blocked_reason=_blocked_reason(findings),
772
+ next_action="" if not findings else _next_action(findings, summary),
773
+ finding_count=len(findings),
774
+ summary=summary,
775
+ findings=findings,
776
+ hardening_recommendations=_recommendations(findings),
777
+ behavior_case_candidates=_behavior_case_candidates(findings),
778
+ sources=sources,
779
+ )
780
+
781
+
782
+ def _blocked_reason(findings: list[WorkflowDeviationFinding]) -> str:
783
+ for finding in findings:
784
+ if finding.severity == "blocking_candidate":
785
+ return finding.code
786
+ return ""
787
+
788
+
789
+ def _next_action(findings: list[WorkflowDeviationFinding], summary: WorkflowTranscriptAuditSummary) -> str:
790
+ for finding in findings:
791
+ if finding.severity == "blocking_candidate":
792
+ return finding.promotion_gate
793
+ return summary.recommended_next_action
794
+
795
+
796
+ def _summary(findings: list[WorkflowDeviationFinding]) -> WorkflowTranscriptAuditSummary:
797
+ blocking = sum(1 for item in findings if item.severity == "blocking_candidate")
798
+ contracts = sum(1 for item in findings if item.severity == "contract_violation")
799
+ warnings = sum(1 for item in findings if item.severity == "warning")
800
+ infos = sum(1 for item in findings if item.severity == "info")
801
+ highest = "blocking_candidate" if blocking else "contract_violation" if contracts else "warning" if warnings else "info"
802
+ next_action = "no transcript audit findings" if not findings else "review workflow transcript audit findings"
803
+ return WorkflowTranscriptAuditSummary(
804
+ finding_count=len(findings),
805
+ blocking_candidate_count=blocking,
806
+ contract_violation_count=contracts,
807
+ warning_count=warnings,
808
+ info_count=infos,
809
+ highest_severity=cast(AuditSeverity, highest),
810
+ recommended_next_action=next_action,
811
+ )
812
+
813
+
814
+ def _recommendations(findings: list[WorkflowDeviationFinding]) -> list[HardeningRecommendation]:
815
+ recommendations: list[HardeningRecommendation] = []
816
+ seen: set[tuple[str, str]] = set()
817
+ for finding in findings:
818
+ key = (finding.code, finding.recommended_action)
819
+ if key in seen:
820
+ continue
821
+ seen.add(key)
822
+ recommendations.append(
823
+ HardeningRecommendation(
824
+ action=finding.recommended_action,
825
+ code=finding.code,
826
+ rationale=finding.observed_behavior,
827
+ target=finding.expected_contract,
828
+ )
829
+ )
830
+ return recommendations
831
+
832
+
833
+ def _behavior_case_candidates(findings: list[WorkflowDeviationFinding]) -> list[RawJsonObject]:
834
+ candidates: list[RawJsonObject] = []
835
+ for finding in findings:
836
+ if finding.severity not in {"contract_violation", "blocking_candidate"}:
837
+ continue
838
+ candidates.append(
839
+ {
840
+ "source_kind": "agent_report",
841
+ "workflow": f"/mednotes:{finding.workflow}" if finding.workflow != "unknown" else "",
842
+ "phase": "post_run_audit",
843
+ "signal": finding.code,
844
+ "severity": "critical" if finding.severity == "blocking_candidate" else "high",
845
+ "redacted_evidence": {
846
+ "evidence_ref": finding.evidence_ref,
847
+ "summary": finding.observed_behavior,
848
+ "expected_contract": finding.expected_contract,
849
+ },
850
+ }
851
+ )
852
+ return candidates