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,1592 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime
4
+ from enum import StrEnum
5
+ from typing import Literal
6
+
7
+ from pydantic import Field, StrictStr, field_validator, model_validator
8
+ from pydantic import ValidationError as PydanticValidationError
9
+ from pydantic.json_schema import SkipJsonSchema
10
+
11
+ from mednotes.domains.wiki.contracts.agent_report import (
12
+ ProcessChatsCoverageStatus,
13
+ ProcessChatsLinkerStatus,
14
+ ProcessChatsNotesStatus,
15
+ ProcessChatsObjectiveStatus,
16
+ ProcessChatsPrimaryObjectiveSummary,
17
+ ProcessChatsRawStatus,
18
+ )
19
+ from mednotes.domains.wiki.contracts.publish import PublishReceipt
20
+ from mednotes.domains.wiki.contracts.workflow_blockers import BlockerRegistryError, blocker_entry
21
+ from mednotes.domains.wiki.contracts.workflow_outcomes import (
22
+ DecisionEvidence,
23
+ RejectedAutomation,
24
+ WorkflowDecision,
25
+ WorkflowDecisionKind,
26
+ )
27
+ from mednotes.domains.wiki.flows.process_chats.process_chats_machine import (
28
+ ProcessChatsBoundaryEvent,
29
+ ProcessChatsMachine,
30
+ ProcessChatsPublishRuntimeObservation,
31
+ )
32
+ from mednotes.domains.wiki.flows.process_chats.process_chats_machine import (
33
+ ProcessChatsState as MachineProcessChatsState,
34
+ )
35
+ from mednotes.kernel.agent_directive import (
36
+ AgentDirective,
37
+ agent_directive_from_progress_view_model,
38
+ assert_agent_directive_matches_progress,
39
+ )
40
+ from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
41
+ from mednotes.kernel.effects import WorkflowEffect, WorkflowEffectKind, WorkflowEffectResult
42
+ from mednotes.kernel.fsm_event import WorkflowEventLike
43
+ from mednotes.kernel.fsm_model import WorkflowModel
44
+ from mednotes.kernel.fsm_transition_result import WorkflowTransitionResult
45
+ from mednotes.kernel.progress import (
46
+ WorkflowProgressCounts,
47
+ WorkflowProgressEvent,
48
+ WorkflowProgressEventType,
49
+ WorkflowProgressState,
50
+ WorkflowProgressStatus,
51
+ WorkflowProgressViewModel,
52
+ build_progress_view_model,
53
+ progress_state_from_view_model,
54
+ )
55
+ from mednotes.kernel.public_report import (
56
+ WorkflowPublicReport,
57
+ WorkflowReports,
58
+ assert_public_report_matches_progress,
59
+ public_progress_followup_line,
60
+ )
61
+ from mednotes.kernel.state_machine import (
62
+ WorkflowStateCategory,
63
+ WorkflowStateMachineSnapshot,
64
+ WorkflowTransition,
65
+ send_workflow_event,
66
+ )
67
+ from mednotes.kernel.workflow import (
68
+ HumanDecisionPacket,
69
+ ReceiptStatus,
70
+ VersionControlSafety,
71
+ WorkflowReceiptPayload,
72
+ assert_diagnostic_context_evidence_only,
73
+ diagnostic_context_evidence_only,
74
+ )
75
+
76
+ PROCESS_CHATS_WORKFLOW: Literal["/mednotes:process-chats"] = "/mednotes:process-chats"
77
+ PROCESS_CHATS_SCHEMA = "medical-notes-workbench.process-chats-fsm-result.v1"
78
+ PROCESS_CHATS_RECEIPT_SCHEMA = "medical-notes-workbench.process-chats-receipt.v1"
79
+ MEDNOTES_AGENT_DIRECTIVE_SCHEMA = "medical-notes-workbench.agent-directive.v1"
80
+ PROCESS_CHATS_AGENT_DIRECTIVE_FIELD = "agent_directive"
81
+
82
+
83
+ PROCESS_CHATS_ALLOWED_ROOT_KEYS = frozenset(
84
+ {
85
+ "schema",
86
+ "workflow",
87
+ "run_id",
88
+ "state_machine_snapshot",
89
+ "progress_view_model",
90
+ "decision",
91
+ "human_decision_packet",
92
+ "receipt",
93
+ "reports",
94
+ "agent_directive",
95
+ "artifacts",
96
+ "version_control_safety",
97
+ "diagnostic_context",
98
+ "error_context",
99
+ }
100
+ )
101
+ PROCESS_CHATS_FORBIDDEN_ROOT_KEYS = frozenset(
102
+ {
103
+ "status",
104
+ "phase",
105
+ "blocked_reason",
106
+ "next_action",
107
+ "required_inputs",
108
+ "human_decision_required",
109
+ "dry_run",
110
+ "dry_run_receipt",
111
+ "created",
112
+ "created_count",
113
+ "raw_updates",
114
+ "processed_raw_count",
115
+ "publish_receipt",
116
+ "planned_batches",
117
+ "linker",
118
+ "linker_applied",
119
+ "linker_skipped_reason",
120
+ "link_trigger_context_path",
121
+ "linker_trigger_context_path",
122
+ "linker_diagnosis_path",
123
+ "linker_receipt_path",
124
+ }
125
+ )
126
+
127
+ _PHASE_BY_STATE = {
128
+ MachineProcessChatsState.ENVIRONMENT_CHECKING.value: "environment",
129
+ MachineProcessChatsState.ENVIRONMENT_PATHS_MISSING.value: "environment",
130
+ MachineProcessChatsState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED.value: "environment",
131
+ MachineProcessChatsState.BACKLOG_NO_PENDING_RAW_CHATS.value: "backlog",
132
+ MachineProcessChatsState.BACKLOG_NO_TRIAGED_RAW_CHATS.value: "backlog",
133
+ MachineProcessChatsState.BACKLOG_TRIAGED_RAW_CHATS_READY.value: "backlog",
134
+ MachineProcessChatsState.VAULT_GUARD_DECISION_REQUIRED.value: "vault_guard",
135
+ MachineProcessChatsState.VAULT_GUARD_REJECTED.value: "vault_guard",
136
+ MachineProcessChatsState.TRIAGE_PLANNING.value: "triage",
137
+ MachineProcessChatsState.ARCHITECT_WORK_REQUESTED.value: "architect",
138
+ MachineProcessChatsState.ARCHITECT_AWAITING_SPECIALIST_CAPACITY.value: "architect",
139
+ MachineProcessChatsState.ARCHITECT_REVIEWING_OUTPUT.value: "architect",
140
+ MachineProcessChatsState.SUBAGENT_PLAN_ATTESTATION_REQUIRED.value: "subagent_plan_attestation",
141
+ MachineProcessChatsState.SUBAGENT_PLAN_ATTESTATION_INVALID.value: "subagent_plan_attestation",
142
+ MachineProcessChatsState.NOTE_VALIDATION_RUNNING.value: "note_validation",
143
+ MachineProcessChatsState.NOTE_VALIDATION_COVERAGE_GAP.value: "note_validation",
144
+ MachineProcessChatsState.NOTE_VALIDATION_MANIFEST_MISMATCH.value: "note_validation",
145
+ MachineProcessChatsState.NOTE_VALIDATION_CONTENT_INVALID.value: "note_validation",
146
+ MachineProcessChatsState.STAGING_MANIFEST_READY.value: "staging",
147
+ MachineProcessChatsState.PUBLISH_AWAITING_CONFIRMATION.value: "publish_preview",
148
+ MachineProcessChatsState.PUBLISH_CANCELLED_BY_HUMAN.value: "publish_preview",
149
+ MachineProcessChatsState.PUBLISH_APPLY_REQUESTED.value: "publish_apply",
150
+ MachineProcessChatsState.PUBLISH_PAUSED_FOR_QUOTA.value: "publish_apply",
151
+ MachineProcessChatsState.PUBLISH_DRY_RUN_RECEIPT_REQUIRED.value: "publish_apply",
152
+ MachineProcessChatsState.PUBLISH_STALE_RECEIPT.value: "publish_apply",
153
+ MachineProcessChatsState.PUBLISH_DUPLICATE_TARGET.value: "publish_apply",
154
+ MachineProcessChatsState.PUBLISH_PROVENANCE_GAP.value: "publish_apply",
155
+ MachineProcessChatsState.PUBLISH_RECEIPT_INVALID.value: "publish_apply",
156
+ MachineProcessChatsState.LINK_RUN_REQUESTED.value: "link_package",
157
+ MachineProcessChatsState.CONTRACT_GAP_MISSING_NEXT_ACTION.value: "contract_gap",
158
+ MachineProcessChatsState.CONTRACT_GAP_MISSING_ERROR_CONTEXT.value: "contract_gap",
159
+ MachineProcessChatsState.AGENT_TOOL_CONTRACT_VIOLATION.value: "agent_tool_contract",
160
+ MachineProcessChatsState.ROLLBACK_RECORDED.value: "rollback",
161
+ MachineProcessChatsState.PUBLISHED.value: "publish_apply",
162
+ MachineProcessChatsState.COMPLETED_WITH_LINK_BLOCKERS.value: "link_package",
163
+ MachineProcessChatsState.TERMINAL_FAILURE_RECORDED.value: "publish_failed",
164
+ }
165
+
166
+
167
+ class ProcessChatsOutcomeReason(StrEnum):
168
+ NO_PENDING = "no_pending"
169
+ TRIAGED_READY = "triaged_raw_chats_ready"
170
+ READY_TO_PUBLISH = "ready_to_publish"
171
+ PUBLISHED = "published"
172
+ LINKER_BLOCKED = "process_chats_linker_blocked"
173
+ RECOVERABLE_BLOCKED = "recoverable_blocked"
174
+ WAITING_HUMAN = "waiting_human"
175
+ BLOCKED = "blocked"
176
+ FAILED = "failed"
177
+
178
+
179
+ class ProcessChatsBatchState(ContractModel):
180
+ batch_id: str = ""
181
+ run_id: str = ""
182
+ note_plan_hash: str = ""
183
+ coverage_hash: str = ""
184
+ source_artifact_hash: str = ""
185
+ raw_file: str = ""
186
+ raw_files: str = ""
187
+ coverage_path: str = ""
188
+
189
+
190
+ class ProcessChatsDryRunReceipt(ContractModel):
191
+ path: str = ""
192
+ expires_at: int = Field(default=0, ge=0, strict=True)
193
+ manifest_hash: str = ""
194
+ dry_run_options_hash: str = ""
195
+ batch_state: list[ProcessChatsBatchState] = Field(default_factory=list)
196
+
197
+ @field_validator("expires_at", mode="before")
198
+ @classmethod
199
+ def _normalize_expires_at(cls, value: object) -> int:
200
+ if value in (None, ""):
201
+ return 0
202
+ if isinstance(value, bool):
203
+ raise ValueError("expires_at must be an epoch timestamp")
204
+ if isinstance(value, int):
205
+ return value
206
+ if isinstance(value, str):
207
+ text = value.strip()
208
+ if text.isdigit():
209
+ return int(text)
210
+ try:
211
+ parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
212
+ except ValueError as exc:
213
+ raise ValueError("expires_at must be an epoch timestamp or ISO datetime") from exc
214
+ if parsed.tzinfo is None:
215
+ parsed = parsed.replace(tzinfo=UTC)
216
+ return int(parsed.timestamp())
217
+ raise ValueError("expires_at must be an epoch timestamp")
218
+
219
+
220
+ class TaxonomyCanonicalizationStep(ContractModel):
221
+ from_taxonomy: str = Field(default="", alias="from")
222
+ to: str = ""
223
+ under: str = ""
224
+
225
+
226
+ class ProcessChatsDecisionSummary(ContractModel):
227
+ kind: WorkflowDecisionKind
228
+ phase: str = ""
229
+ reason_code: str = ""
230
+ public_summary: str = ""
231
+ developer_summary: str = ""
232
+ rejected_automations: list[RejectedAutomation] = Field(default_factory=list)
233
+ evidence: list[DecisionEvidence] = Field(default_factory=list)
234
+
235
+
236
+ class ProcessChatsArtifactReport(ContractModel):
237
+ schema_id: str = Field(default="", alias="schema")
238
+ scope: str = ""
239
+ required: bool = Field(default=False, strict=True)
240
+ manifest_count: int = Field(default=0, ge=0, strict=True)
241
+ artifact_count: int = Field(default=0, ge=0, strict=True)
242
+ included_artifact_count: int = Field(default=0, ge=0, strict=True)
243
+ covered_artifact_count: int = Field(default=0, ge=0, strict=True)
244
+ missing_artifact_count: int = Field(default=0, ge=0, strict=True)
245
+ errors: list[str] = Field(default_factory=list)
246
+ note: str = ""
247
+ manifests: list[JsonObject] = Field(default_factory=list)
248
+ artifacts: list[JsonObject] = Field(default_factory=list)
249
+ included_artifacts: list[JsonObject] = Field(default_factory=list)
250
+ missing_artifacts: list[JsonObject] = Field(default_factory=list)
251
+ partial_artifacts: list[JsonObject] = Field(default_factory=list)
252
+
253
+
254
+ class ProcessChatsArtifactValidation(ProcessChatsArtifactReport):
255
+ notes: list[ProcessChatsArtifactReport] = Field(default_factory=list)
256
+
257
+
258
+ class ProcessChatsCoverageSource(ContractModel):
259
+ raw_file: str = Field(min_length=1)
260
+ status: Literal["covered", "already_covered", "not_relevant"]
261
+ target_title: str = ""
262
+ target_section: str = ""
263
+ new_information_summary: str = ""
264
+ reference_added: str = ""
265
+ reason: str = ""
266
+ existing_title: str = ""
267
+
268
+
269
+ class ProcessChatsCoverageSummary(ContractModel):
270
+ """Coverage summary accepted at the typed publish-to-FSM boundary."""
271
+
272
+ schema_id: str = Field(default="", alias="schema")
273
+ coverage_path: str = ""
274
+ coverage_hash: str = ""
275
+ coverage_hashes: list[str] = Field(default_factory=list)
276
+ raw_file: str = ""
277
+ raw_files: list[str] = Field(default_factory=list)
278
+ multi_source: bool = Field(default=False, strict=True)
279
+ source_count: int = Field(default=0, ge=0, strict=True)
280
+ exhaustive: bool = Field(default=False, strict=True)
281
+ status: str = ""
282
+ item_count: int = Field(default=0, ge=0, strict=True)
283
+ planned_meaning_count: int = Field(default=0, ge=0, strict=True)
284
+ not_a_note_count: int = Field(default=0, ge=0, strict=True)
285
+ raw_file_count: int = Field(default=0, ge=0, strict=True)
286
+ covered_count: int = Field(default=0, ge=0, strict=True)
287
+ sources: list[ProcessChatsCoverageSource] = Field(default_factory=list)
288
+ source_status_counts: dict[str, int] = Field(default_factory=dict)
289
+ staged_note_count: int = Field(default=0, ge=0, strict=True)
290
+ note_plan_hash: str = ""
291
+ batch_id: str = ""
292
+ run_id: str = ""
293
+ source_artifact_hash: str = ""
294
+ note_plan_source_count: int = Field(default=0, ge=0, strict=True)
295
+ note_plan_hashes: dict[str, str] = Field(default_factory=dict)
296
+ note_plan_item_count: int = Field(default=0, ge=0, strict=True)
297
+ note_plan_planned_meaning_count: int = Field(default=0, ge=0, strict=True)
298
+ note_plan_attach_count: int = Field(default=0, ge=0, strict=True)
299
+ note_plan_not_a_note_count: int = Field(default=0, ge=0, strict=True)
300
+ note_plan_needs_context_count: int = Field(default=0, ge=0, strict=True)
301
+
302
+
303
+ class ProcessChatsRawUpdate(ContractModel):
304
+ raw_file: str = Field(min_length=1)
305
+ backup: str | None = None
306
+ updated: bool = Field(strict=True)
307
+ updates: dict[str, str] = Field(default_factory=dict)
308
+
309
+
310
+ class ProcessChatsNewTaxonomyLeafAuthorizationNote(ContractModel):
311
+ target_path: str = Field(min_length=1)
312
+ taxonomy: str = ""
313
+ taxonomy_requested: str = ""
314
+ taxonomy_new_dirs: list[str] = Field(default_factory=list)
315
+
316
+
317
+ class ProcessChatsNewTaxonomyLeafAuthorization(ContractModel):
318
+ required: bool = Field(default=False, strict=True)
319
+ authorized_by_dry_run_receipt: bool = Field(default=False, strict=True)
320
+ note_count: int = Field(default=0, ge=0, strict=True)
321
+ notes: list[ProcessChatsNewTaxonomyLeafAuthorizationNote] = Field(default_factory=list)
322
+
323
+
324
+ class ProcessChatsPlannedNoteSummary(ContractModel):
325
+ title: str = ""
326
+ taxonomy: str = ""
327
+ taxonomy_requested: str = ""
328
+ taxonomy_canonicalized: list[TaxonomyCanonicalizationStep] = Field(default_factory=list)
329
+ taxonomy_new_dirs: list[str] = Field(default_factory=list)
330
+ content_path: str = ""
331
+ target_path: str = ""
332
+ artifact_validation: ProcessChatsArtifactValidation | None = None
333
+
334
+
335
+ class ProcessChatsPlannedBatchSummary(ContractModel):
336
+ raw_file: str = ""
337
+ raw_files: list[str] = Field(default_factory=list)
338
+ notes: list[ProcessChatsPlannedNoteSummary] = Field(default_factory=list)
339
+ coverage_path: str = ""
340
+ coverage: ProcessChatsCoverageSummary | None = None
341
+ artifact_validation: ProcessChatsArtifactValidation | None = None
342
+ batch_state: ProcessChatsBatchState | None = None
343
+
344
+
345
+ class ProcessChatsLinkerRun(ContractModel):
346
+ """Typed evidence packet for a parent workflow delegating to `/mednotes:link`."""
347
+
348
+ schema_id: str | None = Field(default=None, alias="schema")
349
+ phase: str = ""
350
+ status: str = ""
351
+ next_action: str = ""
352
+ trigger_context_path: str = ""
353
+ diagnosis_path: str = ""
354
+ receipt_path: str = ""
355
+ diagnosis_status: str = ""
356
+ diagnosis_blocked_reason: str = ""
357
+ blocker_count: int = Field(default=0, ge=0, strict=True)
358
+ linker_applied: bool = Field(default=False, strict=True)
359
+ linker_skipped_reason: str = ""
360
+ apply_status: str = ""
361
+ apply_blocked_reason: str = ""
362
+ changed_files: list[str] = Field(default_factory=list)
363
+ files_changed: int = Field(default=0, ge=0, strict=True)
364
+ workflow_effect_results: list[WorkflowEffectResult] = Field(default_factory=list)
365
+
366
+
367
+ class ProcessChatsContinuationEffect(ContractModel):
368
+ kind: str = Field(min_length=1)
369
+ workflow: Literal["/mednotes:process-chats"] = PROCESS_CHATS_WORKFLOW
370
+ blocked_reason: str = Field(min_length=1)
371
+
372
+
373
+ class ProcessChatsContinuationPlan(ContractModel):
374
+ schema_id: Literal["medical-notes-workbench.process-chats-continuation-plan.v1"] = Field(
375
+ default="medical-notes-workbench.process-chats-continuation-plan.v1",
376
+ alias="schema",
377
+ )
378
+ status: Literal["ready"] = "ready"
379
+ workflow: Literal["/mednotes:process-chats"] = PROCESS_CHATS_WORKFLOW
380
+ lane: str = Field(min_length=1)
381
+ blocked_reason: str = Field(min_length=1)
382
+ next_effect: ProcessChatsContinuationEffect
383
+ retry_budget: int = Field(default=1, ge=1, strict=True)
384
+ summary: str = Field(min_length=1)
385
+ directive_instructions: list[str] = Field(default_factory=list, exclude=True)
386
+
387
+ def to_payload(self) -> JsonObject:
388
+ return JsonObjectAdapter.validate_python(self.model_dump(by_alias=True))
389
+
390
+
391
+ class ProcessChatsPublishOperationResult(ContractModel):
392
+ """Closed operation payload consumed by the FSM boundary.
393
+
394
+ `runtime_observation` is the sole state-driving publish/link fact packet.
395
+ Historical status fields remain diagnostic UX fields and must not select
396
+ FSM entry states or leaf states at this boundary.
397
+ """
398
+
399
+ schema_id: str | None = Field(default=None, alias="schema")
400
+ workflow: str | None = None
401
+ phase: str = ""
402
+ status: Literal[
403
+ "ready_to_publish",
404
+ "published",
405
+ "completed_with_link_blockers",
406
+ "completed",
407
+ "blocked",
408
+ "failed",
409
+ ]
410
+ blocked_reason: str = ""
411
+ next_action: str = ""
412
+ required_inputs: list[str] = Field(default_factory=list)
413
+ human_decision_required: bool = Field(default=False, strict=True)
414
+ human_decision_packet: HumanDecisionPacket | None = None
415
+ decision_summary: ProcessChatsDecisionSummary | None = None
416
+ error_context: JsonObject = Field(default_factory=dict)
417
+ diagnostic_context: JsonObject = Field(default_factory=dict)
418
+ error: str | None = None
419
+ parse_error: str | None = None
420
+ dry_run: bool = Field(default=False, strict=True)
421
+ backup: bool = Field(default=False, strict=True)
422
+ manifest: str = ""
423
+ manifest_hash: str = ""
424
+ allow_new_taxonomy_leaf: bool = Field(default=True, strict=True)
425
+ require_coverage: bool = Field(default=True, strict=True)
426
+ batch_state: list[ProcessChatsBatchState] = Field(default_factory=list)
427
+ new_taxonomy_leaf_authorization: ProcessChatsNewTaxonomyLeafAuthorization | None = None
428
+ planned_batches: list[ProcessChatsPlannedBatchSummary] = Field(default_factory=list)
429
+ coverage_summary: ProcessChatsCoverageSummary | None = None
430
+ coverage: ProcessChatsCoverageSummary | None = None
431
+ created: list[str] = Field(default_factory=list)
432
+ raw_updates: list[ProcessChatsRawUpdate] = Field(default_factory=list)
433
+ created_count: int = Field(default=0, ge=0, strict=True)
434
+ processed_raw_count: int = Field(default=0, ge=0, strict=True)
435
+ publish_receipt: PublishReceipt | None = None
436
+ dry_run_receipt: ProcessChatsDryRunReceipt | None = None
437
+ linker: ProcessChatsLinkerRun | None = None
438
+ linker_applied: bool = Field(default=False, strict=True)
439
+ linker_skipped_reason: str = ""
440
+ link_trigger_context_path: str = ""
441
+ linker_trigger_context_path: str = ""
442
+ linker_diagnosis_path: str = ""
443
+ linker_receipt_path: str = ""
444
+ runtime_observation: ProcessChatsPublishRuntimeObservation
445
+
446
+ @model_validator(mode="after")
447
+ def _workflow_must_match_process_chats(self) -> ProcessChatsPublishOperationResult:
448
+ if self.workflow is not None and self.workflow != PROCESS_CHATS_WORKFLOW:
449
+ raise ValueError("process-chats publish operation result has invalid workflow")
450
+ return self
451
+
452
+
453
+ class ProcessChatsPublishDiagnostic(ContractModel):
454
+ """Non-authoritative publish details rendered after the StateChart decides state."""
455
+
456
+ status: str = ""
457
+ receipt_status: str = ""
458
+ dry_run: bool = Field(default=False, strict=True)
459
+ manifest: str = ""
460
+ dry_run_receipt: ProcessChatsDryRunReceipt | None = None
461
+ new_taxonomy_leaf_authorization: ProcessChatsNewTaxonomyLeafAuthorization | None = None
462
+
463
+
464
+ class ProcessChatsLinkerDiagnostic(ContractModel):
465
+ """Non-authoritative linker details rendered after the StateChart decides state."""
466
+
467
+ status: str = ""
468
+ next_action: str = ""
469
+ diagnosis_status: str = ""
470
+ applied: bool = Field(default=False, strict=True)
471
+ skipped_reason: str = ""
472
+ blocker_count: int = Field(default=0, ge=0, strict=True)
473
+
474
+
475
+ class ProcessChatsOperationalSummary(ContractModel):
476
+ """Operational counts and artifacts that cannot choose the FSM transition."""
477
+
478
+ note_count: int = Field(default=0, ge=0, strict=True)
479
+ raw_count: int = Field(default=0, ge=0, strict=True)
480
+ coverage_raw_count: int = Field(default=0, ge=0, strict=True)
481
+ planned_note_count: int = Field(default=0, ge=0, strict=True)
482
+ mutated: bool = Field(default=False, strict=True)
483
+ changed_files: list[str] = Field(default_factory=list)
484
+ blocked_item_count: int = Field(default=0, ge=0, strict=True)
485
+ next_action: str = ""
486
+ publish: ProcessChatsPublishDiagnostic = Field(default_factory=ProcessChatsPublishDiagnostic)
487
+ linker: ProcessChatsLinkerDiagnostic = Field(default_factory=ProcessChatsLinkerDiagnostic)
488
+ artifacts: JsonObject = Field(default_factory=dict)
489
+
490
+
491
+ class ProcessChatsFsmFacts(ContractModel):
492
+ run_id: str = Field(min_length=1)
493
+ initial_state: MachineProcessChatsState
494
+ event: ProcessChatsBoundaryEvent
495
+ operational_summary: ProcessChatsOperationalSummary = Field(default_factory=ProcessChatsOperationalSummary)
496
+ version_control_safety: VersionControlSafety
497
+ error_context: JsonObject = Field(default_factory=dict)
498
+
499
+ @model_validator(mode="after")
500
+ def _event_must_match_process_chats_entry(self) -> ProcessChatsFsmFacts:
501
+ if self.event.workflow != PROCESS_CHATS_WORKFLOW:
502
+ raise ValueError(f"process-chats event workflow must be {PROCESS_CHATS_WORKFLOW}")
503
+ if self.event.run_id != self.run_id:
504
+ raise ValueError("process-chats event run_id must match ProcessChatsFsmFacts.run_id")
505
+ if self.event.current_state != self.initial_state.value:
506
+ raise ValueError("process-chats event current_state must match initial_state")
507
+ return self
508
+
509
+
510
+ class _ProcessChatsMachineProjection(ContractModel):
511
+ """Public payload lens derived only from `ProcessChatsMachine`.
512
+
513
+ This is not a second workflow state. Effects are emitted by the StateChart
514
+ transition and then projected outward; agent-facing control must not infer
515
+ or rebuild them from status strings or adapter payloads.
516
+ """
517
+
518
+ reason: ProcessChatsOutcomeReason
519
+ reason_code: str = ""
520
+ state: MachineProcessChatsState
521
+ category: WorkflowStateCategory
522
+ status: WorkflowProgressStatus
523
+ event_type: WorkflowProgressEventType
524
+ message: str
525
+ trigger: str
526
+ decision: WorkflowDecision | None = None
527
+ human_decision_packet: HumanDecisionPacket | None = None
528
+ next_action: str = ""
529
+ resume_action: str = ""
530
+ resume_supported: bool = False
531
+ can_continue_now: bool = False
532
+ effects: list[WorkflowEffect] = Field(default_factory=list)
533
+
534
+
535
+ class _ProcessChatsPayloadProgressView(ContractModel):
536
+ status: StrictStr
537
+ state: StrictStr = ""
538
+
539
+
540
+ class _ProcessChatsPayloadSnapshot(ContractModel):
541
+ current_category: StrictStr
542
+
543
+
544
+ class _ProcessChatsPayloadReceipt(ContractModel):
545
+ status: StrictStr
546
+
547
+
548
+ class _ProcessChatsPayloadFields(ContractModel):
549
+ workflow: Literal["/mednotes:process-chats"]
550
+ progress_view_model: _ProcessChatsPayloadProgressView
551
+ state_machine_snapshot: _ProcessChatsPayloadSnapshot
552
+ receipt: _ProcessChatsPayloadReceipt
553
+
554
+
555
+ class ProcessChatsFsmResult(ContractModel):
556
+ schema_id: Literal["medical-notes-workbench.process-chats-fsm-result.v1"] = Field(
557
+ default=PROCESS_CHATS_SCHEMA,
558
+ alias="schema",
559
+ )
560
+ workflow: Literal["/mednotes:process-chats"] = PROCESS_CHATS_WORKFLOW
561
+ run_id: str = Field(min_length=1)
562
+ progress_state: SkipJsonSchema[WorkflowProgressState]
563
+ progress_view_model: WorkflowProgressViewModel
564
+ state_machine_snapshot: WorkflowStateMachineSnapshot
565
+ decision: WorkflowDecision | None = None
566
+ human_decision_packet: HumanDecisionPacket | None = None
567
+ receipt: WorkflowReceiptPayload
568
+ reports: WorkflowReports
569
+ agent_directive: JsonObject
570
+ artifacts: JsonObject = Field(default_factory=dict)
571
+ version_control_safety: VersionControlSafety
572
+ diagnostic_context: JsonObject = Field(default_factory=dict)
573
+ error_context: JsonObject = Field(default_factory=dict)
574
+
575
+ @model_validator(mode="before")
576
+ @classmethod
577
+ def _hydrate_progress_state_from_public_payload(cls, value: object) -> object:
578
+ """Accept public payloads where progress_state is intentionally hidden."""
579
+
580
+ if not isinstance(value, dict) or "progress_state" in value or "progress_view_model" not in value:
581
+ return value
582
+ hydrated = dict(value)
583
+ progress_view = WorkflowProgressViewModel.model_validate(value["progress_view_model"])
584
+ hydrated["progress_state"] = progress_state_from_view_model(progress_view).to_payload()
585
+ return hydrated
586
+
587
+ @model_validator(mode="after")
588
+ def _progress_view_model_matches_state(self) -> ProcessChatsFsmResult:
589
+ expected = build_progress_view_model(self.progress_state).to_payload()
590
+ if self.progress_view_model.to_payload() != expected:
591
+ raise ValueError("progress_view_model must match progress_state")
592
+ return self
593
+
594
+ def to_payload(self) -> JsonObject:
595
+ payload: JsonObject = {
596
+ "schema": self.schema_id,
597
+ "workflow": self.workflow,
598
+ "run_id": self.run_id,
599
+ "state_machine_snapshot": self.state_machine_snapshot.to_payload(),
600
+ "progress_view_model": self.progress_view_model.to_payload(),
601
+ "decision": self.decision.to_payload() if self.decision is not None else None,
602
+ "human_decision_packet": self.human_decision_packet.to_payload()
603
+ if self.human_decision_packet is not None
604
+ else None,
605
+ "receipt": self.receipt.to_payload(),
606
+ "reports": self.reports.to_payload(),
607
+ "agent_directive": dict(self.agent_directive),
608
+ "artifacts": dict(self.artifacts),
609
+ "version_control_safety": self.version_control_safety.to_payload(),
610
+ "error_context": dict(self.error_context),
611
+ }
612
+ if self.diagnostic_context:
613
+ payload["diagnostic_context"] = dict(self.diagnostic_context)
614
+ payload = JsonObjectAdapter.validate_python(payload)
615
+ assert_process_chats_fsm_payload(payload)
616
+ return payload
617
+
618
+
619
+ def build_process_chats_fsm_result(facts: ProcessChatsFsmFacts) -> ProcessChatsFsmResult:
620
+ machine_model = _process_chats_model_after_event(facts.initial_state, facts.event)
621
+ projection = _project_machine_outcome(facts, machine_model)
622
+ progress_state = _progress_state(facts, projection)
623
+ progress_view_model = build_progress_view_model(progress_state)
624
+ snapshot = _transition_from_machine_model(machine_model, projection, progress_state)
625
+ receipt = _receipt(facts, projection, progress_state, snapshot)
626
+ reports = _reports(facts, projection, progress_state)
627
+ public_report = reports.public_report
628
+ diagnostic_context = _diagnostic_context(
629
+ facts,
630
+ projection,
631
+ )
632
+ agent_directive = _agent_directive(
633
+ projection,
634
+ progress_view_model=progress_view_model,
635
+ user_visible_summary=public_report.summary_text(),
636
+ )
637
+ diagnostic_context = _problem_diagnostic_context(diagnostic_context, projection, facts.operational_summary)
638
+ if facts.error_context:
639
+ diagnostic_context = diagnostic_context_evidence_only(
640
+ {**diagnostic_context, "error_context": dict(facts.error_context)}
641
+ )
642
+ return ProcessChatsFsmResult(
643
+ run_id=facts.run_id,
644
+ progress_state=progress_state,
645
+ progress_view_model=progress_view_model,
646
+ state_machine_snapshot=snapshot,
647
+ decision=projection.decision,
648
+ human_decision_packet=projection.human_decision_packet,
649
+ receipt=receipt,
650
+ reports=reports,
651
+ agent_directive=agent_directive,
652
+ artifacts=dict(facts.operational_summary.artifacts),
653
+ version_control_safety=facts.version_control_safety,
654
+ diagnostic_context=diagnostic_context,
655
+ error_context=facts.error_context,
656
+ )
657
+
658
+
659
+ def _process_chats_model_after_event(
660
+ initial_state: MachineProcessChatsState,
661
+ event: ProcessChatsBoundaryEvent,
662
+ ) -> WorkflowModel:
663
+ model = WorkflowModel.start(workflow=PROCESS_CHATS_WORKFLOW, run_id=event.run_id, initial_state=initial_state.value)
664
+ _send_machine_event(model, event)
665
+ return model
666
+
667
+
668
+ def _send_machine_event(model: WorkflowModel, event: WorkflowEventLike) -> WorkflowTransitionResult:
669
+ machine = ProcessChatsMachine(model=model, state_field=WorkflowModel.STATECHART_STATE_FIELD)
670
+ return send_workflow_event(machine, event)
671
+
672
+
673
+ def _project_machine_outcome(facts: ProcessChatsFsmFacts, model: WorkflowModel) -> _ProcessChatsMachineProjection:
674
+ transition = model.last_transition
675
+ if transition is None:
676
+ raise ValueError("process-chats machine model has no transition to project")
677
+ state = MachineProcessChatsState(model.state)
678
+ category = _machine_category_for_state(state.value)
679
+ status = _progress_status_for_category(category)
680
+ reason = _machine_reason_for_state(state)
681
+ decision = transition.decision
682
+ next_action = _machine_next_action(facts, transition)
683
+ resume_action = transition.resume_action or (decision.resume_action if decision is not None else "")
684
+ return _projection(
685
+ reason=reason,
686
+ reason_code=transition.reason_code,
687
+ state=state,
688
+ category=category,
689
+ status=status,
690
+ event_type=_event_type_for_status(status),
691
+ decision=decision,
692
+ human_decision_packet=transition.human_decision_packet,
693
+ next_action=next_action,
694
+ resume_action=resume_action,
695
+ resume_supported=bool(resume_action),
696
+ can_continue_now=status == WorkflowProgressStatus.WAITING_AGENT,
697
+ effects=list(transition.effects),
698
+ message=_machine_message_for_state(state, reason),
699
+ trigger=transition.trigger,
700
+ )
701
+
702
+
703
+ def _machine_category_for_state(state: str) -> WorkflowStateCategory:
704
+ model = WorkflowModel.start(workflow=PROCESS_CHATS_WORKFLOW, run_id="category", initial_state=state)
705
+ machine = ProcessChatsMachine(model=model, state_field=WorkflowModel.STATECHART_STATE_FIELD)
706
+ return machine.category_for_state(state)
707
+
708
+
709
+ def _progress_status_for_category(category: WorkflowStateCategory) -> WorkflowProgressStatus:
710
+ match category:
711
+ case WorkflowStateCategory.PREPARING | WorkflowStateCategory.RUNNING:
712
+ return WorkflowProgressStatus.RUNNING
713
+ case WorkflowStateCategory.WAITING_AGENT:
714
+ return WorkflowProgressStatus.WAITING_AGENT
715
+ case WorkflowStateCategory.WAITING_EXTERNAL:
716
+ return WorkflowProgressStatus.WAITING_EXTERNAL
717
+ case WorkflowStateCategory.WAITING_HUMAN:
718
+ return WorkflowProgressStatus.WAITING_HUMAN
719
+ case WorkflowStateCategory.BLOCKED:
720
+ return WorkflowProgressStatus.BLOCKED
721
+ case WorkflowStateCategory.FAILED:
722
+ return WorkflowProgressStatus.FAILED
723
+ case WorkflowStateCategory.COMPLETED:
724
+ return WorkflowProgressStatus.COMPLETED
725
+ case WorkflowStateCategory.COMPLETED_WITH_WARNINGS:
726
+ return WorkflowProgressStatus.COMPLETED_WITH_WARNINGS
727
+
728
+
729
+ def _event_type_for_status(status: WorkflowProgressStatus) -> WorkflowProgressEventType:
730
+ match status:
731
+ case WorkflowProgressStatus.COMPLETED | WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
732
+ return WorkflowProgressEventType.WORKFLOW_COMPLETED
733
+ case WorkflowProgressStatus.FAILED:
734
+ return WorkflowProgressEventType.WORKFLOW_FAILED
735
+ case WorkflowProgressStatus.BLOCKED | WorkflowProgressStatus.WAITING_HUMAN | WorkflowProgressStatus.WAITING_AGENT:
736
+ return WorkflowProgressEventType.DECISION_EMITTED
737
+ case WorkflowProgressStatus.WAITING_EXTERNAL:
738
+ return WorkflowProgressEventType.EXTERNAL_WAIT_STARTED
739
+ case _:
740
+ return WorkflowProgressEventType.STATE_ENTERED
741
+
742
+
743
+ def _machine_reason_for_state(state: MachineProcessChatsState) -> ProcessChatsOutcomeReason:
744
+ match state:
745
+ case MachineProcessChatsState.BACKLOG_NO_PENDING_RAW_CHATS | MachineProcessChatsState.BACKLOG_NO_TRIAGED_RAW_CHATS:
746
+ return ProcessChatsOutcomeReason.NO_PENDING
747
+ case MachineProcessChatsState.BACKLOG_TRIAGED_RAW_CHATS_READY:
748
+ return ProcessChatsOutcomeReason.TRIAGED_READY
749
+ case MachineProcessChatsState.PUBLISH_AWAITING_CONFIRMATION:
750
+ return ProcessChatsOutcomeReason.READY_TO_PUBLISH
751
+ case MachineProcessChatsState.PUBLISHED:
752
+ return ProcessChatsOutcomeReason.PUBLISHED
753
+ case MachineProcessChatsState.COMPLETED_WITH_LINK_BLOCKERS:
754
+ return ProcessChatsOutcomeReason.LINKER_BLOCKED
755
+ case MachineProcessChatsState.TERMINAL_FAILURE_RECORDED:
756
+ return ProcessChatsOutcomeReason.FAILED
757
+ case (
758
+ MachineProcessChatsState.ARCHITECT_WORK_REQUESTED
759
+ | MachineProcessChatsState.ENVIRONMENT_PATHS_MISSING
760
+ | MachineProcessChatsState.ENVIRONMENT_WINDOWS_PATH_OR_VENV_BLOCKED
761
+ | MachineProcessChatsState.SUBAGENT_PLAN_ATTESTATION_REQUIRED
762
+ | MachineProcessChatsState.SUBAGENT_PLAN_ATTESTATION_INVALID
763
+ | MachineProcessChatsState.NOTE_VALIDATION_COVERAGE_GAP
764
+ | MachineProcessChatsState.NOTE_VALIDATION_MANIFEST_MISMATCH
765
+ | MachineProcessChatsState.NOTE_VALIDATION_CONTENT_INVALID
766
+ | MachineProcessChatsState.PUBLISH_DRY_RUN_RECEIPT_REQUIRED
767
+ | MachineProcessChatsState.PUBLISH_STALE_RECEIPT
768
+ | MachineProcessChatsState.PUBLISH_DUPLICATE_TARGET
769
+ | MachineProcessChatsState.PUBLISH_PROVENANCE_GAP
770
+ | MachineProcessChatsState.PUBLISH_RECEIPT_INVALID
771
+ ):
772
+ return ProcessChatsOutcomeReason.RECOVERABLE_BLOCKED
773
+ case _:
774
+ return ProcessChatsOutcomeReason.BLOCKED
775
+
776
+
777
+ def _machine_next_action(facts: ProcessChatsFsmFacts, transition: WorkflowTransitionResult) -> str:
778
+ if transition.decision is not None and transition.decision.next_action.strip():
779
+ return transition.decision.next_action
780
+ if transition.resume_action.strip():
781
+ return transition.resume_action
782
+ return _default_next_action(facts, _machine_reason_for_state(MachineProcessChatsState(transition.to_state)))
783
+
784
+
785
+ def _machine_message_for_state(state: MachineProcessChatsState, reason: ProcessChatsOutcomeReason) -> str:
786
+ match state:
787
+ case MachineProcessChatsState.BACKLOG_NO_PENDING_RAW_CHATS:
788
+ return "Nenhum chat novo para processar."
789
+ case MachineProcessChatsState.BACKLOG_NO_TRIAGED_RAW_CHATS:
790
+ return "Nenhum chat triado para publicar."
791
+ case MachineProcessChatsState.BACKLOG_TRIAGED_RAW_CHATS_READY:
792
+ return "Nenhum chat bruto novo está pendente; há chats triados aguardando arquitetura."
793
+ case MachineProcessChatsState.PUBLISH_AWAITING_CONFIRMATION:
794
+ return "Previa pronta; aguardando confirmacao humana para publicar."
795
+ case MachineProcessChatsState.PUBLISHED:
796
+ return "Notas publicadas, raws atualizados e conexões executadas."
797
+ case MachineProcessChatsState.COMPLETED_WITH_LINK_BLOCKERS:
798
+ return "Publicacao concluida com pendencias de conexoes/grafo."
799
+ case MachineProcessChatsState.TERMINAL_FAILURE_RECORDED:
800
+ return "Process-chats falhou antes de concluir."
801
+ case _:
802
+ return f"Process-chats em {state.value} por {reason.value}."
803
+
804
+
805
+ def _transition_from_machine_model(
806
+ model: WorkflowModel,
807
+ projection: _ProcessChatsMachineProjection,
808
+ progress_state: WorkflowProgressState,
809
+ ) -> WorkflowStateMachineSnapshot:
810
+ transition = model.last_transition
811
+ if transition is None:
812
+ raise ValueError("process-chats machine model has no last transition")
813
+ event = _progress_event_from_transition(transition, projection, progress_state)
814
+ projected_transition = WorkflowTransition(
815
+ workflow=transition.workflow,
816
+ from_state=transition.from_state,
817
+ to_state=transition.to_state,
818
+ to_category=projection.category,
819
+ trigger=transition.trigger,
820
+ effects=list(transition.effects),
821
+ progress_events=[event],
822
+ decision=projection.decision,
823
+ resume_action=projection.resume_action,
824
+ )
825
+ return WorkflowStateMachineSnapshot(
826
+ workflow=PROCESS_CHATS_WORKFLOW,
827
+ run_id=model.run_id,
828
+ current_state=model.state,
829
+ current_category=projection.category,
830
+ transitions=[projected_transition],
831
+ metadata={"reason": projection.reason.value, "statechart": "process_chats_machine"},
832
+ )
833
+
834
+
835
+ def _progress_event_from_transition(
836
+ transition: WorkflowTransitionResult,
837
+ projection: _ProcessChatsMachineProjection,
838
+ progress_state: WorkflowProgressState,
839
+ ) -> WorkflowProgressEvent:
840
+ return WorkflowProgressEvent(
841
+ workflow=PROCESS_CHATS_WORKFLOW,
842
+ run_id=transition.run_id,
843
+ state=transition.to_state,
844
+ phase=progress_state.phase,
845
+ event_type=projection.event_type,
846
+ message=projection.message,
847
+ status=projection.status,
848
+ current=progress_state.current,
849
+ total=progress_state.total,
850
+ counts=progress_state.counts,
851
+ resume_action=projection.resume_action,
852
+ resume_supported=projection.resume_supported,
853
+ can_continue_now=projection.can_continue_now,
854
+ decision=progress_state.decision,
855
+ technical_context=progress_state.technical_context,
856
+ )
857
+
858
+
859
+ def _projection(
860
+ *,
861
+ reason: ProcessChatsOutcomeReason,
862
+ reason_code: str = "",
863
+ state: MachineProcessChatsState,
864
+ category: WorkflowStateCategory,
865
+ status: WorkflowProgressStatus,
866
+ event_type: WorkflowProgressEventType,
867
+ message: str,
868
+ trigger: str,
869
+ decision: WorkflowDecision | None = None,
870
+ human_decision_packet: HumanDecisionPacket | None = None,
871
+ next_action: str = "",
872
+ resume_action: str = "",
873
+ resume_supported: bool = False,
874
+ can_continue_now: bool = False,
875
+ effects: list[WorkflowEffect] | None = None,
876
+ ) -> _ProcessChatsMachineProjection:
877
+ return _ProcessChatsMachineProjection(
878
+ reason=reason,
879
+ reason_code=reason_code,
880
+ state=state,
881
+ category=category,
882
+ status=status,
883
+ event_type=event_type,
884
+ decision=decision,
885
+ human_decision_packet=human_decision_packet,
886
+ next_action=next_action,
887
+ resume_action=resume_action,
888
+ resume_supported=resume_supported,
889
+ can_continue_now=can_continue_now,
890
+ effects=list(effects or []),
891
+ message=message,
892
+ trigger=trigger,
893
+ )
894
+
895
+
896
+ def _default_next_action(facts: ProcessChatsFsmFacts, reason: ProcessChatsOutcomeReason) -> str:
897
+ linker_next_action = _linker_next_action_after_link_attempt(
898
+ facts.operational_summary.linker.next_action.strip()
899
+ if reason == ProcessChatsOutcomeReason.LINKER_BLOCKED
900
+ else ""
901
+ )
902
+ if facts.operational_summary.next_action.strip():
903
+ next_action = _linker_next_action_after_link_attempt(facts.operational_summary.next_action.strip())
904
+ if linker_next_action and linker_next_action not in next_action:
905
+ return f"{next_action} Detalhe das conexões/grafo: {linker_next_action}"
906
+ return next_action
907
+ if linker_next_action:
908
+ return f"Resolver pendências de conexões/grafo: {linker_next_action}"
909
+ match reason:
910
+ case ProcessChatsOutcomeReason.NO_PENDING:
911
+ return ""
912
+ case ProcessChatsOutcomeReason.TRIAGED_READY:
913
+ return "Continuar para os chats triados com list-triados e plan-subagents --phase architect."
914
+ case ProcessChatsOutcomeReason.READY_TO_PUBLISH:
915
+ return "Revisar a prévia e confirmar publicação pela rota oficial."
916
+ case ProcessChatsOutcomeReason.LINKER_BLOCKED:
917
+ return "Resolver pendências de conexões/grafo pela rota oficial antes de considerar o lote concluído."
918
+ case ProcessChatsOutcomeReason.WAITING_HUMAN:
919
+ return "Responder a decisão solicitada para continuar."
920
+ case ProcessChatsOutcomeReason.RECOVERABLE_BLOCKED:
921
+ return "Continuar automaticamente pela etapa de recuperacao oficial antes de concluir."
922
+ case ProcessChatsOutcomeReason.BLOCKED:
923
+ return "Corrigir o bloqueio informado e repetir /mednotes:process-chats pela rota oficial."
924
+ case ProcessChatsOutcomeReason.FAILED:
925
+ return "Revisar o erro do workflow e retomar /mednotes:process-chats pela rota oficial."
926
+ case ProcessChatsOutcomeReason.PUBLISHED:
927
+ return ""
928
+
929
+
930
+ def _linker_next_action_after_link_attempt(value: str) -> str:
931
+ """Drop stale pre-link diagnosis commands after the linker child already ran."""
932
+
933
+ if "run-linker --diagnose" not in value:
934
+ return value
935
+ detail_marker = "Detalhe das conexões/grafo:"
936
+ if detail_marker in value:
937
+ detail = value.split(detail_marker, maxsplit=1)[1].strip()
938
+ if detail:
939
+ return detail
940
+ return "Resolver pendências de conexões/grafo pela rota oficial antes de considerar o lote concluído."
941
+
942
+
943
+ def _progress_state(facts: ProcessChatsFsmFacts, projection: _ProcessChatsMachineProjection) -> WorkflowProgressState:
944
+ summary = facts.operational_summary
945
+ note_count = summary.note_count
946
+ raw_count = summary.raw_count
947
+ coverage_count = summary.coverage_raw_count
948
+ planned = max(note_count, summary.planned_note_count, coverage_count, raw_count)
949
+ current = planned if projection.status == WorkflowProgressStatus.COMPLETED else note_count
950
+ if projection.reason == ProcessChatsOutcomeReason.READY_TO_PUBLISH:
951
+ current = planned
952
+ counts = WorkflowProgressCounts(
953
+ planned_items=planned,
954
+ processed_items=current,
955
+ mutated_files=note_count if summary.mutated else 0,
956
+ written_files=note_count if summary.mutated else 0,
957
+ blocked_items=_blocked_item_count(facts, projection),
958
+ )
959
+ return WorkflowProgressState(
960
+ workflow=PROCESS_CHATS_WORKFLOW,
961
+ run_id=facts.run_id,
962
+ state=projection.state.value,
963
+ phase=_PHASE_BY_STATE[projection.state.value],
964
+ event_type=projection.event_type,
965
+ message=projection.message,
966
+ status=projection.status,
967
+ current=current,
968
+ total=planned,
969
+ counts=counts,
970
+ resume_action=projection.resume_action,
971
+ resume_supported=projection.resume_supported,
972
+ can_continue_now=projection.can_continue_now,
973
+ decision=projection.decision.decision_summary() if projection.decision is not None else None,
974
+ technical_context={
975
+ "reason": projection.reason.value,
976
+ "trigger": projection.trigger,
977
+ "process_status": projection.state.value,
978
+ "note_count": note_count,
979
+ "raw_count": raw_count,
980
+ },
981
+ )
982
+
983
+
984
+ def _receipt(
985
+ facts: ProcessChatsFsmFacts,
986
+ projection: _ProcessChatsMachineProjection,
987
+ progress_state: WorkflowProgressState,
988
+ snapshot: WorkflowStateMachineSnapshot,
989
+ ) -> WorkflowReceiptPayload:
990
+ view_model = build_progress_view_model(progress_state)
991
+ return WorkflowReceiptPayload(
992
+ schema=PROCESS_CHATS_RECEIPT_SCHEMA,
993
+ workflow=PROCESS_CHATS_WORKFLOW,
994
+ run_id=facts.run_id,
995
+ status=_receipt_status(projection),
996
+ mutated=facts.operational_summary.mutated,
997
+ next_action=_receipt_next_action(projection),
998
+ human_decision_required=projection.status == WorkflowProgressStatus.WAITING_HUMAN,
999
+ human_decision_packet=projection.human_decision_packet,
1000
+ changed_files=list(facts.operational_summary.changed_files),
1001
+ version_control_safety=facts.version_control_safety,
1002
+ progress_state=progress_state,
1003
+ progress_view_model=view_model,
1004
+ state_machine_snapshot=snapshot,
1005
+ )
1006
+
1007
+
1008
+ def _receipt_status(projection: _ProcessChatsMachineProjection) -> ReceiptStatus:
1009
+ match projection.status:
1010
+ case WorkflowProgressStatus.COMPLETED:
1011
+ return "completed"
1012
+ case WorkflowProgressStatus.COMPLETED_WITH_WARNINGS:
1013
+ return "completed_with_warnings"
1014
+ case WorkflowProgressStatus.WAITING_HUMAN:
1015
+ return "waiting_human"
1016
+ case WorkflowProgressStatus.WAITING_AGENT:
1017
+ return "waiting_agent"
1018
+ case WorkflowProgressStatus.WAITING_EXTERNAL:
1019
+ return "waiting_external"
1020
+ case WorkflowProgressStatus.BLOCKED:
1021
+ return "blocked"
1022
+ case WorkflowProgressStatus.FAILED:
1023
+ return "failed"
1024
+ case _:
1025
+ return "blocked"
1026
+
1027
+
1028
+ def _receipt_next_action(projection: _ProcessChatsMachineProjection) -> str:
1029
+ if projection.reason == ProcessChatsOutcomeReason.PUBLISHED:
1030
+ return ""
1031
+ return projection.next_action
1032
+
1033
+
1034
+ def _reports(
1035
+ facts: ProcessChatsFsmFacts,
1036
+ projection: _ProcessChatsMachineProjection,
1037
+ progress_state: WorkflowProgressState,
1038
+ ) -> WorkflowReports:
1039
+ note_count = facts.operational_summary.note_count
1040
+ raw_count = facts.operational_summary.raw_count
1041
+ match projection.reason:
1042
+ case ProcessChatsOutcomeReason.NO_PENDING:
1043
+ summary = "Nenhum chat novo para processar."
1044
+ case ProcessChatsOutcomeReason.TRIAGED_READY:
1045
+ summary = "Nenhum chat bruto novo está pendente; ainda há chats triados para preparar."
1046
+ case ProcessChatsOutcomeReason.READY_TO_PUBLISH:
1047
+ summary = "Prévia pronta; nenhuma nota foi publicada."
1048
+ case ProcessChatsOutcomeReason.PUBLISHED:
1049
+ summary = f"Publiquei {note_count} nota(s), atualizei {raw_count} raw chat(s) e rodei o pacote de links."
1050
+ case ProcessChatsOutcomeReason.LINKER_BLOCKED:
1051
+ summary = f"Publiquei {note_count} nota(s), mas o pacote de links/grafo ficou pendente."
1052
+ case ProcessChatsOutcomeReason.WAITING_HUMAN:
1053
+ summary = "Preciso de uma escolha sua antes de continuar o process-chats."
1054
+ case ProcessChatsOutcomeReason.RECOVERABLE_BLOCKED:
1055
+ summary = "Encontrei uma pendencia recuperavel e vou continuar pela rota oficial."
1056
+ case ProcessChatsOutcomeReason.BLOCKED:
1057
+ summary = "Process-chats bloqueou antes de publicar o lote."
1058
+ case ProcessChatsOutcomeReason.FAILED:
1059
+ summary = "Process-chats falhou antes de concluir."
1060
+ public_lines = [summary]
1061
+ followup_line = public_progress_followup_line(progress_state)
1062
+ if followup_line:
1063
+ public_lines.append(followup_line)
1064
+ public_report = WorkflowPublicReport(
1065
+ workflow=PROCESS_CHATS_WORKFLOW,
1066
+ run_id=facts.run_id,
1067
+ headline=summary,
1068
+ lines=public_lines,
1069
+ )
1070
+ return WorkflowReports(
1071
+ summary=summary,
1072
+ public_report=public_report,
1073
+ details={"primary_objective_summary": _primary_objective_summary(facts, projection).to_payload()},
1074
+ )
1075
+
1076
+
1077
+ def _primary_objective_summary(
1078
+ facts: ProcessChatsFsmFacts,
1079
+ projection: _ProcessChatsMachineProjection,
1080
+ ) -> ProcessChatsPrimaryObjectiveSummary:
1081
+ """Derive the public objective answer from FSM facts, not legacy fields."""
1082
+ note_count = facts.operational_summary.note_count
1083
+ raw_count = facts.operational_summary.raw_count
1084
+ coverage_count = facts.operational_summary.coverage_raw_count
1085
+ status: ProcessChatsObjectiveStatus = _primary_process_status(projection)
1086
+ linker_status = _primary_linker_status(facts, projection)
1087
+ return ProcessChatsPrimaryObjectiveSummary(
1088
+ process_status=status,
1089
+ process_summary=_primary_process_summary(status=status, note_count=note_count, raw_count=raw_count),
1090
+ notes_status=_primary_notes_status(status),
1091
+ note_count=note_count,
1092
+ wiki_write_summary=_primary_wiki_write_summary(status=status, note_count=note_count),
1093
+ raw_status=_primary_raw_status(status=status, raw_count=raw_count, coverage_count=coverage_count),
1094
+ raw_count=raw_count,
1095
+ raw_summary=_primary_raw_summary(status=status, raw_count=raw_count, coverage_count=coverage_count),
1096
+ coverage_status=_primary_coverage_status(status=status, coverage_count=coverage_count),
1097
+ coverage_summary=_primary_coverage_summary(status=status, coverage_count=coverage_count),
1098
+ linker_status=linker_status,
1099
+ linker_summary=_primary_linker_summary(linker_status=linker_status, facts=facts, status=status),
1100
+ )
1101
+
1102
+
1103
+ def _primary_process_status(projection: _ProcessChatsMachineProjection) -> ProcessChatsObjectiveStatus:
1104
+ match projection.reason:
1105
+ case ProcessChatsOutcomeReason.NO_PENDING:
1106
+ return "no_pending"
1107
+ case ProcessChatsOutcomeReason.TRIAGED_READY:
1108
+ return "ready_to_publish"
1109
+ case ProcessChatsOutcomeReason.READY_TO_PUBLISH | ProcessChatsOutcomeReason.WAITING_HUMAN:
1110
+ return "ready_to_publish"
1111
+ case ProcessChatsOutcomeReason.PUBLISHED:
1112
+ return "published"
1113
+ case ProcessChatsOutcomeReason.LINKER_BLOCKED:
1114
+ return "completed_with_link_blockers"
1115
+ case ProcessChatsOutcomeReason.FAILED:
1116
+ return "failed"
1117
+ case ProcessChatsOutcomeReason.BLOCKED | ProcessChatsOutcomeReason.RECOVERABLE_BLOCKED:
1118
+ return "blocked"
1119
+
1120
+
1121
+ def _primary_process_summary(*, status: ProcessChatsObjectiveStatus, note_count: int, raw_count: int) -> str:
1122
+ match status:
1123
+ case "ready_to_publish":
1124
+ return "process-chats preparou a prévia; nenhuma nota foi publicada."
1125
+ case "no_pending":
1126
+ return "Não havia raw chat novo ou triado para processar; nada foi publicado."
1127
+ case "published":
1128
+ return f"process-chats publicou {note_count} nota(s) e processou {raw_count} raw chat(s)."
1129
+ case "completed_with_link_blockers":
1130
+ return (
1131
+ f"process-chats publicou {note_count} nota(s) e processou {raw_count} raw chat(s), "
1132
+ "mas as conexões/grafo ficaram pendentes."
1133
+ )
1134
+ case "failed":
1135
+ return "process-chats falhou antes de concluir a publicação."
1136
+ case _:
1137
+ return "process-chats bloqueou antes de concluir a publicação."
1138
+
1139
+
1140
+ def _primary_notes_status(status: ProcessChatsObjectiveStatus) -> ProcessChatsNotesStatus:
1141
+ match status:
1142
+ case "no_pending":
1143
+ return "not_written"
1144
+ case "ready_to_publish":
1145
+ return "ready_to_publish"
1146
+ case "published" | "completed_with_link_blockers":
1147
+ return "published"
1148
+ case "blocked" | "failed":
1149
+ return "blocked"
1150
+ case _:
1151
+ return "unknown"
1152
+
1153
+
1154
+ def _primary_wiki_write_summary(*, status: ProcessChatsObjectiveStatus, note_count: int) -> str:
1155
+ match status:
1156
+ case "no_pending":
1157
+ return "Nenhuma nota foi escrita porque não havia chat novo para processar."
1158
+ case "ready_to_publish":
1159
+ return "Nenhum arquivo da Wiki foi escrito; a publicação ainda está em prévia."
1160
+ case "published" | "completed_with_link_blockers":
1161
+ return f"{note_count} arquivo(s) da Wiki foram escritos."
1162
+ case "blocked" | "failed":
1163
+ return "Nenhum arquivo da Wiki deve ser considerado publicado neste estado."
1164
+ case _:
1165
+ return "O payload FSM não confirmou escrita real na Wiki."
1166
+
1167
+
1168
+ def _primary_raw_status(
1169
+ *,
1170
+ status: ProcessChatsObjectiveStatus,
1171
+ raw_count: int,
1172
+ coverage_count: int,
1173
+ ) -> ProcessChatsRawStatus:
1174
+ if status == "no_pending":
1175
+ return "not_processed"
1176
+ if status == "ready_to_publish":
1177
+ return "covered" if coverage_count else "not_processed"
1178
+ if status in {"published", "completed_with_link_blockers"}:
1179
+ return "processed" if raw_count else "unknown"
1180
+ if status in {"blocked", "failed"}:
1181
+ return "not_processed"
1182
+ return "unknown"
1183
+
1184
+
1185
+ def _primary_raw_summary(*, status: ProcessChatsObjectiveStatus, raw_count: int, coverage_count: int) -> str:
1186
+ if status == "no_pending":
1187
+ return "Nenhum raw chat foi processado porque não havia item novo nesta fase."
1188
+ if status == "ready_to_publish":
1189
+ if coverage_count:
1190
+ return f"{coverage_count} raw chat(s) estão cobertos, mas ainda não foram marcados como processados."
1191
+ return "Nenhum raw chat foi marcado como processado nesta prévia."
1192
+ if status in {"published", "completed_with_link_blockers"}:
1193
+ if raw_count:
1194
+ return f"{raw_count} raw chat(s) foram marcados como processados."
1195
+ return "O payload FSM publicou notas, mas não confirmou raws processados."
1196
+ if status in {"blocked", "failed"}:
1197
+ return "Nenhum raw chat deve ser considerado processado neste estado."
1198
+ return "O payload FSM não confirmou cobertura ou processamento dos raw chats."
1199
+
1200
+
1201
+ def _primary_coverage_status(*, status: ProcessChatsObjectiveStatus, coverage_count: int) -> ProcessChatsCoverageStatus:
1202
+ if status == "no_pending":
1203
+ return "not_applicable"
1204
+ if status == "ready_to_publish" and coverage_count:
1205
+ return "valid"
1206
+ if status in {"published", "completed_with_link_blockers"} and coverage_count:
1207
+ return "valid"
1208
+ if status in {"blocked", "failed"}:
1209
+ return "unknown"
1210
+ return "unknown"
1211
+
1212
+
1213
+ def _primary_coverage_summary(*, status: ProcessChatsObjectiveStatus, coverage_count: int) -> str:
1214
+ if status == "no_pending":
1215
+ return "Coverage/manifest não se aplicam porque nenhuma publicação foi preparada."
1216
+ if coverage_count:
1217
+ return f"Coverage/manifest coerentes para {coverage_count} raw chat(s)."
1218
+ if status == "ready_to_publish":
1219
+ return "O payload FSM não trouxe confirmação suficiente de coverage/manifest."
1220
+ return "Coverage/manifest não foram confirmados neste estado."
1221
+
1222
+
1223
+ def _primary_linker_status(
1224
+ facts: ProcessChatsFsmFacts,
1225
+ projection: _ProcessChatsMachineProjection,
1226
+ ) -> ProcessChatsLinkerStatus:
1227
+ if projection.reason == ProcessChatsOutcomeReason.NO_PENDING:
1228
+ return "not_applicable"
1229
+ if projection.reason == ProcessChatsOutcomeReason.TRIAGED_READY:
1230
+ return "not_run"
1231
+ if projection.reason == ProcessChatsOutcomeReason.READY_TO_PUBLISH:
1232
+ return "not_run"
1233
+ if projection.reason == ProcessChatsOutcomeReason.LINKER_BLOCKED:
1234
+ return "blocked"
1235
+ if projection.reason == ProcessChatsOutcomeReason.PUBLISHED:
1236
+ linker = facts.operational_summary.linker
1237
+ if linker.applied or linker.status == "completed" or linker.blocker_count == 0:
1238
+ return "applied"
1239
+ return "unknown"
1240
+ if projection.reason in {ProcessChatsOutcomeReason.BLOCKED, ProcessChatsOutcomeReason.RECOVERABLE_BLOCKED, ProcessChatsOutcomeReason.FAILED}:
1241
+ return "not_run"
1242
+ return "unknown"
1243
+
1244
+
1245
+ def _primary_linker_summary(
1246
+ *,
1247
+ linker_status: ProcessChatsLinkerStatus,
1248
+ facts: ProcessChatsFsmFacts,
1249
+ status: ProcessChatsObjectiveStatus,
1250
+ ) -> str:
1251
+ linker = facts.operational_summary.linker
1252
+ match linker_status:
1253
+ case "not_applicable":
1254
+ return "Conexões/grafo não se aplicam porque nenhuma nota foi publicada."
1255
+ case "not_run":
1256
+ if status == "ready_to_publish":
1257
+ return "Conexões/grafo ainda não rodaram porque a publicação não foi confirmada."
1258
+ return "Conexões/grafo não rodaram porque a publicação não foi concluída."
1259
+ case "applied":
1260
+ return "Conexões/grafo aplicadas sem bloqueios."
1261
+ case "blocked":
1262
+ reason = linker.skipped_reason or f"{linker.blocker_count} blocker(s)"
1263
+ return f"Conexões/grafo ficaram pendentes: {reason}."
1264
+ case _:
1265
+ return "O payload FSM não confirmou o estado de conexões/grafo."
1266
+
1267
+
1268
+ def _diagnostic_context(
1269
+ facts: ProcessChatsFsmFacts,
1270
+ projection: _ProcessChatsMachineProjection,
1271
+ ) -> JsonObject:
1272
+ """Build explanatory diagnostics without carrying executable control."""
1273
+
1274
+ summary = facts.operational_summary
1275
+ publish = summary.publish
1276
+ linker = summary.linker
1277
+ publish_context: JsonObject = {
1278
+ "status": publish.status,
1279
+ "receipt_status": publish.receipt_status,
1280
+ "dry_run": publish.dry_run,
1281
+ "manifest": publish.manifest,
1282
+ }
1283
+ if publish.dry_run_receipt is not None:
1284
+ publish_context["dry_run_receipt"] = {
1285
+ "path": publish.dry_run_receipt.path,
1286
+ "expires_at": publish.dry_run_receipt.expires_at,
1287
+ "manifest_hash": publish.dry_run_receipt.manifest_hash,
1288
+ "dry_run_options_hash": publish.dry_run_receipt.dry_run_options_hash,
1289
+ "batch_state": [item.to_payload() for item in publish.dry_run_receipt.batch_state],
1290
+ }
1291
+ if publish.new_taxonomy_leaf_authorization:
1292
+ publish_context["new_taxonomy_leaf_authorization"] = publish.new_taxonomy_leaf_authorization.to_payload()
1293
+ context: JsonObject = {
1294
+ "schema": "medical-notes-workbench.process-chats-fsm-diagnostic-context.v1",
1295
+ "reason": projection.reason.value,
1296
+ "outcome_reason": projection.reason.value,
1297
+ "state": projection.state.value,
1298
+ "publish": publish_context,
1299
+ "counts": {
1300
+ "note_count": summary.note_count,
1301
+ "raw_count": summary.raw_count,
1302
+ "coverage_raw_count": summary.coverage_raw_count,
1303
+ "linker_applied": linker.applied,
1304
+ },
1305
+ "linker": {
1306
+ "status": linker.status,
1307
+ "diagnosis_status": linker.diagnosis_status,
1308
+ "applied": linker.applied,
1309
+ "skipped_reason": linker.skipped_reason,
1310
+ "blocker_count": linker.blocker_count,
1311
+ },
1312
+ }
1313
+ return diagnostic_context_evidence_only(context)
1314
+
1315
+
1316
+ def _agent_directive(
1317
+ projection: _ProcessChatsMachineProjection,
1318
+ *,
1319
+ progress_view_model: WorkflowProgressViewModel,
1320
+ user_visible_summary: str,
1321
+ ) -> JsonObject:
1322
+ """Build the root executable agent contract directly from FSM state."""
1323
+
1324
+ directive_instructions: list[str] = []
1325
+ if projection.reason == ProcessChatsOutcomeReason.RECOVERABLE_BLOCKED:
1326
+ plan = _recoverable_blocker_plan(projection.reason_code or projection.trigger)
1327
+ if plan is None:
1328
+ raise ValueError("recoverable process-chats diagnostic context requires a recovery plan")
1329
+ directive_instructions = list(plan.directive_instructions)
1330
+ typed = agent_directive_from_progress_view_model(
1331
+ progress_view_model,
1332
+ schema=MEDNOTES_AGENT_DIRECTIVE_SCHEMA,
1333
+ reason=projection.reason.value,
1334
+ effects=projection.effects,
1335
+ blockers=_blocked_by_for_directive(projection),
1336
+ resume=projection.resume_action,
1337
+ report_requires=["primary_objective", "raw_coverage", "manifest", "linker"],
1338
+ summary=user_visible_summary,
1339
+ instructions=_plain_agent_directive_instructions(directive_instructions),
1340
+ )
1341
+ return JsonObjectAdapter.validate_python(typed.to_payload())
1342
+
1343
+
1344
+ def _problem_diagnostic_context(
1345
+ context: JsonObject,
1346
+ projection: _ProcessChatsMachineProjection,
1347
+ summary: ProcessChatsOperationalSummary,
1348
+ ) -> JsonObject:
1349
+ publish = summary.publish
1350
+ linker = summary.linker
1351
+ if projection.status == WorkflowProgressStatus.COMPLETED and publish.dry_run is True:
1352
+ return diagnostic_context_evidence_only(context)
1353
+ if projection.status == WorkflowProgressStatus.COMPLETED:
1354
+ linker_deviation = linker.applied is not True or bool(linker.skipped_reason.strip())
1355
+ if linker_deviation:
1356
+ return diagnostic_context_evidence_only(context)
1357
+ return {}
1358
+ return diagnostic_context_evidence_only(context)
1359
+
1360
+
1361
+ def _blocked_by_for_directive(projection: _ProcessChatsMachineProjection) -> list[str]:
1362
+ if projection.status not in {
1363
+ WorkflowProgressStatus.BLOCKED,
1364
+ WorkflowProgressStatus.FAILED,
1365
+ WorkflowProgressStatus.WAITING_EXTERNAL,
1366
+ WorkflowProgressStatus.WAITING_HUMAN,
1367
+ }:
1368
+ return []
1369
+ if projection.decision is not None:
1370
+ return [projection.decision.reason_code]
1371
+ return [projection.trigger or projection.reason.value]
1372
+
1373
+
1374
+ def _plain_agent_directive_instructions(lines: list[str]) -> list[str]:
1375
+ cleaned: list[str] = []
1376
+ for line in lines:
1377
+ text = line.strip()
1378
+ prefix = "agent_instruction:"
1379
+ if text.casefold().startswith(prefix):
1380
+ text = text[len(prefix):].strip()
1381
+ if text:
1382
+ cleaned.append(text)
1383
+ return cleaned
1384
+
1385
+
1386
+ def assert_process_chats_fsm_payload(payload: JsonObject) -> None:
1387
+ forbidden_keys = set(payload) & PROCESS_CHATS_FORBIDDEN_ROOT_KEYS
1388
+ if forbidden_keys:
1389
+ raise ValueError(f"process-chats FSM payload contains non-FSM root fields: {sorted(forbidden_keys)}")
1390
+ required_root_keys = PROCESS_CHATS_ALLOWED_ROOT_KEYS - {"diagnostic_context"}
1391
+ missing_keys = required_root_keys - set(payload)
1392
+ if missing_keys:
1393
+ raise ValueError(f"process-chats FSM payload missing canonical root fields: {sorted(missing_keys)}")
1394
+ unexpected_keys = set(payload) - PROCESS_CHATS_ALLOWED_ROOT_KEYS
1395
+ if unexpected_keys:
1396
+ raise ValueError(f"process-chats FSM payload contains unexpected root fields: {sorted(unexpected_keys)}")
1397
+ diagnostic_context = payload["diagnostic_context"] if "diagnostic_context" in payload else {}
1398
+ assert_diagnostic_context_evidence_only(diagnostic_context)
1399
+ if isinstance(diagnostic_context, dict) and "agent_directive" in diagnostic_context:
1400
+ raise ValueError("process-chats FSM diagnostic_context must not contain agent_directive")
1401
+ fields = _process_chats_payload_fields(payload)
1402
+ if fields.progress_view_model.status != fields.state_machine_snapshot.current_category:
1403
+ raise ValueError("process-chats FSM status must match state_machine_snapshot category")
1404
+ if fields.receipt.status != fields.progress_view_model.status:
1405
+ raise ValueError("process-chats FSM receipt status must match progress view status")
1406
+ if fields.progress_view_model.status in {
1407
+ WorkflowStateCategory.BLOCKED.value,
1408
+ WorkflowStateCategory.FAILED.value,
1409
+ } and not payload["error_context"]:
1410
+ raise ValueError("process-chats FSM blocked/failed payload requires error_context")
1411
+ reports = WorkflowReports.model_validate(payload["reports"])
1412
+ if "human" in payload["reports"]:
1413
+ raise ValueError("process-chats FSM reports must not expose legacy human report text")
1414
+ public_report = reports.public_report
1415
+ snapshot = WorkflowStateMachineSnapshot.model_validate(payload["state_machine_snapshot"])
1416
+ progress_view_model = WorkflowProgressViewModel.model_validate(payload["progress_view_model"])
1417
+ assert_public_report_matches_progress(
1418
+ public_report,
1419
+ workflow=PROCESS_CHATS_WORKFLOW,
1420
+ run_id=str(payload["run_id"]),
1421
+ progress_view_model=progress_view_model,
1422
+ label="process-chats FSM",
1423
+ )
1424
+ assert_agent_directive_matches_progress(
1425
+ AgentDirective.model_validate(payload[PROCESS_CHATS_AGENT_DIRECTIVE_FIELD]),
1426
+ workflow=PROCESS_CHATS_WORKFLOW,
1427
+ run_id=str(payload["run_id"]),
1428
+ progress_view_model=progress_view_model,
1429
+ snapshot=snapshot,
1430
+ allowed_effect_kinds=_allowed_agent_effect_kinds_for_category(snapshot.current_category),
1431
+ label="process-chats FSM",
1432
+ )
1433
+ _assert_process_chats_machine_snapshot(snapshot)
1434
+
1435
+
1436
+ def _assert_process_chats_machine_snapshot(snapshot: WorkflowStateMachineSnapshot) -> None:
1437
+ if snapshot.workflow != PROCESS_CHATS_WORKFLOW:
1438
+ raise ValueError("process-chats FSM snapshot has invalid workflow")
1439
+ if snapshot.current_category != _machine_category_for_state(snapshot.current_state):
1440
+ raise ValueError("process-chats FSM snapshot category does not match StateChart state")
1441
+ edges = _process_chats_machine_edges()
1442
+ for transition in snapshot.transitions:
1443
+ if transition.to_category != _machine_category_for_state(transition.to_state):
1444
+ raise ValueError("process-chats FSM transition category does not match StateChart state")
1445
+ edge = (transition.trigger, transition.from_state, transition.to_state)
1446
+ if edge not in edges:
1447
+ raise ValueError(f"unauthorized FSM transition: {edge}")
1448
+
1449
+
1450
+ def _allowed_agent_effect_kinds_for_category(category: WorkflowStateCategory) -> set[WorkflowEffectKind]:
1451
+ """Declare which executable effects may be projected to the agent per lane."""
1452
+
1453
+ match category:
1454
+ case WorkflowStateCategory.WAITING_AGENT:
1455
+ return {WorkflowEffectKind.CALL_SPECIALIST_MODEL, WorkflowEffectKind.RUN_SUBWORKFLOW}
1456
+ case WorkflowStateCategory.WAITING_EXTERNAL:
1457
+ return {WorkflowEffectKind.WAIT_EXTERNAL}
1458
+ case WorkflowStateCategory.WAITING_HUMAN:
1459
+ return {WorkflowEffectKind.ASK_HUMAN}
1460
+ case WorkflowStateCategory.BLOCKED | WorkflowStateCategory.FAILED:
1461
+ return {WorkflowEffectKind.RUN_SUBWORKFLOW}
1462
+ case _:
1463
+ return set()
1464
+
1465
+
1466
+ def _process_chats_machine_edges() -> set[tuple[str, str, str]]:
1467
+ edges: set[tuple[str, str, str]] = set()
1468
+ for event in ProcessChatsMachine.events:
1469
+ for transition in event._transitions:
1470
+ for target in transition._targets:
1471
+ edges.add((event.id, str(transition.source.value), str(target.value)))
1472
+ return edges
1473
+
1474
+
1475
+ def _process_chats_payload_fields(payload: JsonObject) -> _ProcessChatsPayloadFields:
1476
+ raw_fields: JsonObject = {
1477
+ "workflow": payload["workflow"],
1478
+ "progress_view_model": _json_object_subset(payload, "progress_view_model", ("status", "state")),
1479
+ "state_machine_snapshot": _json_object_subset(payload, "state_machine_snapshot", ("current_category",)),
1480
+ "receipt": _json_object_subset(payload, "receipt", ("status",)),
1481
+ }
1482
+ try:
1483
+ return _ProcessChatsPayloadFields.model_validate(raw_fields)
1484
+ except PydanticValidationError as exc:
1485
+ first = exc.errors()[0] if exc.errors() else {}
1486
+ loc = ".".join(str(part) for part in first.get("loc", ())) or "$"
1487
+ msg = str(first.get("msg") or str(exc))
1488
+ raise ValueError(f"process-chats FSM payload invalid: {loc}: {msg}") from exc
1489
+
1490
+
1491
+ def _json_object_subset(payload: JsonObject, field_name: str, keys: tuple[str, ...]) -> JsonObject:
1492
+ try:
1493
+ source = JsonObjectAdapter.validate_python(payload[field_name])
1494
+ except PydanticValidationError as exc:
1495
+ raise ValueError(f"process-chats FSM payload invalid: {field_name} must be an object") from exc
1496
+ return {key: source[key] for key in keys if key in source}
1497
+
1498
+
1499
+ def process_chats_cli_exit_code(payload: JsonObject) -> int:
1500
+ fields = _process_chats_payload_fields(payload)
1501
+ status = fields.progress_view_model.status
1502
+ match status:
1503
+ case "completed" | "completed_with_warnings":
1504
+ return 0
1505
+ case "waiting_human" if (
1506
+ fields.progress_view_model.state == MachineProcessChatsState.PUBLISH_AWAITING_CONFIRMATION.value
1507
+ ):
1508
+ return 0
1509
+ case "waiting_human" | "waiting_agent" | "blocked":
1510
+ return 3
1511
+ case "failed":
1512
+ return 5
1513
+ case _:
1514
+ return 1
1515
+
1516
+
1517
+ def _blocked_item_count(facts: ProcessChatsFsmFacts, projection: _ProcessChatsMachineProjection) -> int:
1518
+ if projection.status not in {
1519
+ WorkflowProgressStatus.BLOCKED,
1520
+ WorkflowProgressStatus.FAILED,
1521
+ WorkflowProgressStatus.WAITING_HUMAN,
1522
+ WorkflowProgressStatus.WAITING_AGENT,
1523
+ }:
1524
+ return 0
1525
+ return max(1, facts.operational_summary.blocked_item_count)
1526
+
1527
+
1528
+ def _recoverable_blocker_plan(blocked_reason: str) -> ProcessChatsContinuationPlan | None:
1529
+ code = blocked_reason.strip()
1530
+ if not code:
1531
+ return None
1532
+ if code in {"ValidationError", "validation_errors", "validation_failed"}:
1533
+ lane, effect = _recovery_lane_and_effect(code)
1534
+ return ProcessChatsContinuationPlan(
1535
+ lane=lane,
1536
+ blocked_reason=code,
1537
+ next_effect=ProcessChatsContinuationEffect(kind=effect, blocked_reason=code),
1538
+ summary=_recovery_summary(code),
1539
+ directive_instructions=[
1540
+ "agent_instruction: workflow is waiting_agent, not completed.",
1541
+ "agent_instruction: repair or quarantine the invalid item before writing a final success report.",
1542
+ ],
1543
+ )
1544
+ try:
1545
+ entry = blocker_entry(code)
1546
+ except BlockerRegistryError:
1547
+ return None
1548
+ if entry.default_decision not in {
1549
+ WorkflowDecisionKind.AUTO_FIX,
1550
+ WorkflowDecisionKind.AUTO_PLAN,
1551
+ WorkflowDecisionKind.AUTO_DEFER,
1552
+ }:
1553
+ return None
1554
+ lane, effect = _recovery_lane_and_effect(code)
1555
+ return ProcessChatsContinuationPlan(
1556
+ lane=lane,
1557
+ blocked_reason=code,
1558
+ next_effect=ProcessChatsContinuationEffect(kind=effect, blocked_reason=code),
1559
+ summary=_recovery_summary(code),
1560
+ directive_instructions=[
1561
+ "agent_instruction: workflow is waiting_agent, not completed.",
1562
+ "agent_instruction: run the official next_effect before writing a final success report.",
1563
+ ],
1564
+ )
1565
+
1566
+
1567
+ def _recovery_lane_and_effect(blocked_reason: str) -> tuple[str, str]:
1568
+ match blocked_reason:
1569
+ case "coverage_invalid" | "coverage_path_missing":
1570
+ return ("coverage_recovery", "regenerate_raw_coverage")
1571
+ case "dry_run_receipt_invalid" | "new_taxonomy_leaf_requires_dry_run_authorization":
1572
+ return ("publish_preview_recovery", "rerun_publish_preview")
1573
+ case "ValidationError" | "validation_errors" | "validation_failed":
1574
+ return ("note_validation_repair", "repair_or_quarantine_note")
1575
+ case "taxonomy_action_required" | "taxonomy_plan_blocked":
1576
+ return ("taxonomy_resolution", "resolve_taxonomy_or_quarantine")
1577
+ case _:
1578
+ return ("workflow_recovery", "recover_process_chats_blocker")
1579
+
1580
+
1581
+ def _recovery_summary(blocked_reason: str) -> str:
1582
+ match blocked_reason:
1583
+ case "coverage_invalid" | "coverage_path_missing":
1584
+ return "Reconstruir a cobertura dos raw chats pela rota oficial e repetir a etapa de publicacao."
1585
+ case "dry_run_receipt_invalid" | "new_taxonomy_leaf_requires_dry_run_authorization":
1586
+ return "Gerar uma nova previa oficial de publicacao antes de aplicar o lote."
1587
+ case "ValidationError" | "validation_errors" | "validation_failed":
1588
+ return "Reparar ou quarentenar a nota invalida e continuar os demais itens seguros."
1589
+ case "taxonomy_action_required" | "taxonomy_plan_blocked":
1590
+ return "Resolver a taxonomia pela politica oficial antes de publicar o item afetado."
1591
+ case _:
1592
+ return "Recuperar o blocker pela rota oficial do process-chats antes de concluir."