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.
- package/.opencode/agents/med-chat-triager.md +204 -0
- package/.opencode/agents/med-flashcard-maker.md +63 -0
- package/.opencode/agents/med-knowledge-architect.md +230 -0
- package/.opencode/agents/med-link-graph-curator.md +177 -0
- package/.opencode/agents/med-publish-guard.md +62 -0
- package/.opencode/commands/flashcards.md +25 -0
- package/.opencode/commands/mednotes/create.md +25 -0
- package/.opencode/commands/mednotes/enrich.md +27 -0
- package/.opencode/commands/mednotes/fix-wiki.md +27 -0
- package/.opencode/commands/mednotes/history.md +22 -0
- package/.opencode/commands/mednotes/link-body.md +25 -0
- package/.opencode/commands/mednotes/link-related.md +27 -0
- package/.opencode/commands/mednotes/link.md +27 -0
- package/.opencode/commands/mednotes/pdf-library.md +27 -0
- package/.opencode/commands/mednotes/process-chats.md +23 -0
- package/.opencode/commands/mednotes/setup.md +21 -0
- package/.opencode/commands/mednotes/status.md +27 -0
- package/.opencode/commands/mednotes/telemetry.md +27 -0
- package/.opencode/commands/report.md +26 -0
- package/.opencode/mednotes/AGENTS.md +57 -0
- package/.opencode/mednotes/agents/med-chat-triager.md +197 -0
- package/.opencode/mednotes/agents/med-flashcard-maker.md +56 -0
- package/.opencode/mednotes/agents/med-knowledge-architect.md +224 -0
- package/.opencode/mednotes/agents/med-link-graph-curator.md +171 -0
- package/.opencode/mednotes/agents/med-publish-guard.md +55 -0
- package/.opencode/mednotes/contracts/.gitkeep +1 -0
- package/.opencode/mednotes/contracts/agents.json +116 -0
- package/.opencode/mednotes/contracts/opencode-plugin.json +70 -0
- package/.opencode/mednotes/docs/agent-prompt-hardening.md +567 -0
- package/.opencode/mednotes/docs/agent-role-contracts.md +94 -0
- package/.opencode/mednotes/docs/anki-mcp-twenty-rules.md +214 -0
- package/.opencode/mednotes/docs/anki-templates/README.md +39 -0
- package/.opencode/mednotes/docs/anki-templates/cloze.back.html +23 -0
- package/.opencode/mednotes/docs/anki-templates/cloze.front.html +14 -0
- package/.opencode/mednotes/docs/anki-templates/qa.back.html +24 -0
- package/.opencode/mednotes/docs/anki-templates/qa.front.html +14 -0
- package/.opencode/mednotes/docs/anki-templates/style.css +182 -0
- package/.opencode/mednotes/docs/atomicity-splitting-policy.md +113 -0
- package/.opencode/mednotes/docs/extension-docs.md +40 -0
- package/.opencode/mednotes/docs/flashcard-ingestion.md +278 -0
- package/.opencode/mednotes/docs/knowledge-architect.md +208 -0
- package/.opencode/mednotes/docs/merge-policy.md +110 -0
- package/.opencode/mednotes/docs/public-vocabulary.md +104 -0
- package/.opencode/mednotes/docs/semantic-linker.md +141 -0
- package/.opencode/mednotes/docs/taxonomy-policy.md +90 -0
- package/.opencode/mednotes/docs/triage-policy.md +187 -0
- package/.opencode/mednotes/docs/vault-version-control.md +758 -0
- package/.opencode/mednotes/docs/vocabulary-db-recovery.md +58 -0
- package/.opencode/mednotes/docs/workflow-output-contract.md +779 -0
- package/.opencode/mednotes/hooks/hooks.json +79 -0
- package/.opencode/mednotes/package-lock.json +6361 -0
- package/.opencode/mednotes/package.json +15 -0
- package/.opencode/mednotes/pyproject.toml +48 -0
- package/.opencode/mednotes/scripts/bootstrap_windows_python_uv.cmd +13 -0
- package/.opencode/mednotes/scripts/bootstrap_windows_python_uv.ps1 +172 -0
- package/.opencode/mednotes/scripts/enrich_notes.py +23 -0
- package/.opencode/mednotes/scripts/full_reset_windows_python_uv.cmd +13 -0
- package/.opencode/mednotes/scripts/hooks/antigravity_hook_status.mjs +212 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/antigravity.mjs +169 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/harness_payload.mjs +103 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/opencode_plugin.mjs +341 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/opencode_user_config_sync.mjs +177 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/anki_preflight.mjs +214 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/cli.mjs +143 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/diagnostics.mjs +11 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/domain/agent_directive_core.mjs +160 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/fsm_directive.mjs +1470 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/hook_errors.mjs +120 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/retention.mjs +114 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/runtime.mjs +174 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/telemetry_capture.mjs +511 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/vault_guard.mjs +624 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook.mjs +5 -0
- package/.opencode/mednotes/scripts/mednotes/_runtime_paths.py +24 -0
- package/.opencode/mednotes/scripts/mednotes/anki_model_validator.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/capture_extension_diff.py +1562 -0
- package/.opencode/mednotes/scripts/mednotes/feedback_report.py +16 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_index.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_pipeline.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_report.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_sources.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/obsidian/README.md +6 -0
- package/.opencode/mednotes/scripts/mednotes/obsidian_note_utils.py +20 -0
- package/.opencode/mednotes/scripts/mednotes/pdf_library/cli.py +16 -0
- package/.opencode/mednotes/scripts/mednotes/project_fsm.py +229 -0
- package/.opencode/mednotes/scripts/mednotes/setup_telemetry_email.py +404 -0
- package/.opencode/mednotes/scripts/mednotes/sync_anki_twenty_rules.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/sync_opencode_user_config.py +36 -0
- package/.opencode/mednotes/scripts/mednotes/wiki/cli.py +20 -0
- package/.opencode/mednotes/scripts/mednotes/wiki_graph.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/wiki_tree.py +134 -0
- package/.opencode/mednotes/scripts/reset_windows_python_uv.ps1 +625 -0
- package/.opencode/mednotes/scripts/run_python.mjs +109 -0
- package/.opencode/mednotes/scripts/vault/vault_commit.ps1 +19 -0
- package/.opencode/mednotes/scripts/vault/vault_commit.sh +18 -0
- package/.opencode/mednotes/scripts/vault/vault_git.ps1 +19 -0
- package/.opencode/mednotes/scripts/vault/vault_git.py +3107 -0
- package/.opencode/mednotes/scripts/vault/vault_git.sh +18 -0
- package/.opencode/mednotes/scripts/vault/vault_precommit.ps1 +19 -0
- package/.opencode/mednotes/scripts/vault/vault_precommit.sh +18 -0
- package/.opencode/mednotes/skills/THIRD_PARTY_NOTICES.md +45 -0
- package/.opencode/mednotes/skills/create-medical-flashcards/SKILL.md +113 -0
- package/.opencode/mednotes/skills/create-medical-note/SKILL.md +90 -0
- package/.opencode/mednotes/skills/enrich-medical-note/SKILL.md +120 -0
- package/.opencode/mednotes/skills/fix-medical-wiki/SKILL.md +559 -0
- package/.opencode/mednotes/skills/link-medical-wiki/SKILL.md +224 -0
- package/.opencode/mednotes/skills/obsidian-cli/SKILL.md +118 -0
- package/.opencode/mednotes/skills/obsidian-markdown/SKILL.md +207 -0
- package/.opencode/mednotes/skills/obsidian-markdown/references/CALLOUTS.md +58 -0
- package/.opencode/mednotes/skills/obsidian-markdown/references/EMBEDS.md +63 -0
- package/.opencode/mednotes/skills/obsidian-markdown/references/PROPERTIES.md +61 -0
- package/.opencode/mednotes/skills/obsidian-ops/SKILL.md +136 -0
- package/.opencode/mednotes/skills/pdf-library/SKILL.md +45 -0
- package/.opencode/mednotes/skills/process-medical-chats/SKILL.md +246 -0
- package/.opencode/mednotes/skills/workflow-report/SKILL.md +100 -0
- package/.opencode/mednotes/src/mednotes/__init__.py +5 -0
- package/.opencode/mednotes/src/mednotes/domains/__init__.py +5 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/README.md +26 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/__init__.py +2 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/build_demo_apkg.py +177 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/contracts.py +385 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/flashcards_machine.py +522 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/fsm.py +817 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/index.py +630 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/install_models.py +445 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/model.py +359 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/obsidian_links.py +135 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/obsidian_note_utils.py +546 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/pipeline.py +580 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/report.py +510 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/sources.py +682 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/sync_rules.py +184 -0
- package/.opencode/mednotes/src/mednotes/domains/history/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/history/history_fsm.py +852 -0
- package/.opencode/mednotes/src/mednotes/domains/history/history_machine.py +453 -0
- package/.opencode/mednotes/src/mednotes/domains/setup/__init__.py +7 -0
- package/.opencode/mednotes/src/mednotes/domains/setup/setup_fsm.py +808 -0
- package/.opencode/mednotes/src/mednotes/domains/setup/setup_machine.py +973 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/README.md +64 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/api.py +668 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/batch_state.py +102 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/atomicity/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/atomicity/atomicity.py +877 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/body_link/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/body_link/body_linker.py +1562 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/effect_adapters.py +949 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/fix_wiki_runtime_adapters.py +433 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/coverage.py +413 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/graph.py +396 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/graph_fixes.py +161 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/hygiene/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/hygiene/hygiene.py +483 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/__init__.py +2 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/anchors.py +185 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/__init__.py +0 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/cache.py +223 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/config.py +131 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/download.py +224 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/frontmatter.py +59 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/insert.py +227 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/local_import.py +54 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/__init__.py +42 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/web_profiles.py +99 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/web_search.py +203 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/wikimedia.py +102 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_db_adapter.mjs +434 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_node_runtime.py +274 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_query.py +227 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/artifacts.py +605 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/canonical_merge.py +277 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/markdown_zones.py +85 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/meaning_planner.py +307 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_iter.py +67 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_merge.py +278 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_plan.py +409 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_policy.py +22 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/__init__.py +79 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/fixes.py +264 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/frontmatter.py +435 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/models.py +208 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/prompts.py +37 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/tables.py +236 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/validate.py +404 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/provenance.py +478 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/raw_chats.py +273 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/sources_backfill.py +235 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/__init__.py +10 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/anchors.py +16 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/captions.py +47 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/cli.py +179 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/cloud.py +52 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/config.py +196 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/context_packets.py +76 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/db.py +81 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/doctor.py +102 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/figure_ids.py +42 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/ingest.py +326 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/insert.py +316 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/mentions.py +57 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/ocr.py +71 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/paths.py +35 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/pdf_engine.py +77 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/schema.py +155 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/search.py +188 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/app.py +89 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/image_backend.py +29 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/state.py +65 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish.py +1139 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish_receipts.py +365 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish_recovery.py +240 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_behavior_corpus.py +2069 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_report_validation.py +4448 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_run_audit.py +852 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/architect_prompt_eval.py +341 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/body_linker_eval.py +240 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/curator_output_validation.py +175 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/curator_prompt_eval.py +865 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/triager_prompt_eval.py +1295 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/related_notes.py +1920 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/related_notes_headless.py +1186 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/plan_attestation.py +148 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_receipts.py +360 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_runtime.py +52 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_task_runner.py +2470 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/style/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/style/style.py +1952 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/subagents/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/subagents/agents.py +1767 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/alias_projection.py +331 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/link_terms.py +151 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/llm_disambiguation.py +182 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/__init__.py +116 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/audit.py +201 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/migration.py +314 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/normalize.py +72 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/policy.py +135 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/resolve.py +413 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/schema.py +157 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/status.py +137 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_bootstrap.py +509 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_curator_batch.py +1115 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_ingestion.py +632 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_map.py +930 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_recovery.py +1388 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/cli.py +6665 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/common.py +69 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/config.py +210 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/__init__.py +74 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agent_report.py +242 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agent_run_audit.py +196 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agents.py +601 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/curator.py +256 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/effect_payloads.py +519 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/happy_path.py +190 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/link_git.py +110 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/link_runtime_artifact.py +52 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/note_plan.py +75 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/paths.py +114 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/public_report.py +53 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/publish.py +111 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/raw_coverage.py +217 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes.py +136 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes_headless.py +153 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes_runtime.py +395 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/schema_registry.py +637 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/specialist.py +432 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/status.py +62 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/style_rewrite.py +568 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/vocabulary_ingestion.py +223 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_blockers.py +510 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_guardrails.py +637 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_outcomes.py +121 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_receipts.py +100 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/__main__.py +4 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/cli.py +275 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/__init__.py +2 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/candidates.py +193 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/cli.py +189 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/gemini.py +220 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/inputs.py +120 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/models.py +34 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/parsing.py +48 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/prompts.py +216 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/quality.py +54 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/reporting.py +24 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/runner.py +433 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/utils.py +39 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/vault_guard_bridge.py +17 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_context_packets.py +454 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_decision_projection.py +133 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_effects.py +1260 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_fsm.py +2768 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_machine.py +1588 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_plan.py +306 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_primary_objective.py +316 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_problem.py +153 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_receipt_evidence.py +306 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_states.py +290 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_user_report.py +342 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/health.py +6332 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_fsm.py +1119 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_git.py +638 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_machine.py +1106 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_retry_governance.py +374 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_runtime_result.py +485 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_triggers.py +183 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/linking.py +2758 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/reference_repair.py +718 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/related_notes_fsm.py +1855 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link_related/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link_related/link_related_machine.py +834 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_fsm.py +1592 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_machine.py +3097 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_primary_objective.py +28 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_runtime_result.py +185 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/performance.py +97 -0
- package/.opencode/mednotes/src/mednotes/kernel/__init__.py +6 -0
- package/.opencode/mednotes/src/mednotes/kernel/agent_directive.py +336 -0
- package/.opencode/mednotes/src/mednotes/kernel/base.py +51 -0
- package/.opencode/mednotes/src/mednotes/kernel/blockers.py +39 -0
- package/.opencode/mednotes/src/mednotes/kernel/effect_executor.py +55 -0
- package/.opencode/mednotes/src/mednotes/kernel/effect_intent.py +69 -0
- package/.opencode/mednotes/src/mednotes/kernel/effects.py +160 -0
- package/.opencode/mednotes/src/mednotes/kernel/errors.py +38 -0
- package/.opencode/mednotes/src/mednotes/kernel/fsm_event.py +35 -0
- package/.opencode/mednotes/src/mednotes/kernel/fsm_model.py +55 -0
- package/.opencode/mednotes/src/mednotes/kernel/fsm_transition_result.py +75 -0
- package/.opencode/mednotes/src/mednotes/kernel/guardrails.py +188 -0
- package/.opencode/mednotes/src/mednotes/kernel/progress.py +319 -0
- package/.opencode/mednotes/src/mednotes/kernel/public_report.py +346 -0
- package/.opencode/mednotes/src/mednotes/kernel/state_machine.py +164 -0
- package/.opencode/mednotes/src/mednotes/kernel/workflow.py +619 -0
- package/.opencode/mednotes/src/mednotes/platform/__init__.py +5 -0
- package/.opencode/mednotes/src/mednotes/platform/backup_policy.py +382 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/__init__.py +62 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/cli.py +275 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/contracts.py +83 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/core.py +4168 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/integrity.py +989 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/operational_contract.py +2293 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/telemetry.py +875 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/telemetry_config.py +65 -0
- package/.opencode/mednotes/src/mednotes/platform/opencode_runtime_config.py +182 -0
- package/.opencode/mednotes/src/mednotes/platform/paths/__init__.py +1560 -0
- package/.opencode/mednotes/src/mednotes/platform/secrets.py +89 -0
- package/.opencode/mednotes/src/mednotes/platform/user_config.py +103 -0
- package/.opencode/mednotes/src/mednotes/platform/vault_guard.py +214 -0
- package/.opencode/mednotes/uv.lock +932 -0
- package/.opencode/mednotes.generated.json +395 -0
- package/.opencode/opencode.json +31 -0
- package/.opencode/plugins/mednotes-fsm.mjs +7 -0
- package/.opencode/plugins/mednotes_hook/adapters/antigravity.mjs +169 -0
- package/.opencode/plugins/mednotes_hook/adapters/harness_payload.mjs +103 -0
- package/.opencode/plugins/mednotes_hook/adapters/opencode_plugin.mjs +341 -0
- package/.opencode/plugins/mednotes_hook/adapters/opencode_user_config_sync.mjs +177 -0
- package/.opencode/plugins/mednotes_hook/anki_preflight.mjs +214 -0
- package/.opencode/plugins/mednotes_hook/cli.mjs +143 -0
- package/.opencode/plugins/mednotes_hook/diagnostics.mjs +11 -0
- package/.opencode/plugins/mednotes_hook/domain/agent_directive_core.mjs +160 -0
- package/.opencode/plugins/mednotes_hook/fsm_directive.mjs +1470 -0
- package/.opencode/plugins/mednotes_hook/hook_errors.mjs +120 -0
- package/.opencode/plugins/mednotes_hook/retention.mjs +114 -0
- package/.opencode/plugins/mednotes_hook/runtime.mjs +174 -0
- package/.opencode/plugins/mednotes_hook/telemetry_capture.mjs +511 -0
- package/.opencode/plugins/mednotes_hook/vault_guard.mjs +624 -0
- package/AGENTS.md +57 -0
- package/README.md +194 -0
- package/adapters/antigravity/agents.json +80 -0
- package/adapters/antigravity/templates/med-chat-triager.md +214 -0
- package/adapters/antigravity/templates/med-flashcard-maker.md +72 -0
- package/adapters/antigravity/templates/med-knowledge-architect.md +241 -0
- package/adapters/antigravity/templates/med-link-graph-curator.md +187 -0
- package/adapters/antigravity/templates/med-publish-guard.md +71 -0
- package/adapters/gemini-cli/gemini-extension.json +14 -0
- package/adapters/gemini-cli/package.json +15 -0
- package/adapters/gemini-cli/pyproject.toml +48 -0
- package/bin/mednotes-opencode.mjs +155 -0
- package/contracts/agents.json +116 -0
- package/core/agents/med-chat-triager.md +197 -0
- package/core/agents/med-flashcard-maker.md +56 -0
- package/core/agents/med-knowledge-architect.md +224 -0
- package/core/agents/med-link-graph-curator.md +171 -0
- package/core/agents/med-publish-guard.md +55 -0
- package/core/commands/flashcards.toml +22 -0
- package/core/commands/mednotes/create.toml +22 -0
- package/core/commands/mednotes/enrich.toml +24 -0
- package/core/commands/mednotes/fix-wiki.toml +24 -0
- package/core/commands/mednotes/history.toml +19 -0
- package/core/commands/mednotes/link-body.toml +22 -0
- package/core/commands/mednotes/link-related.toml +24 -0
- package/core/commands/mednotes/link.toml +24 -0
- package/core/commands/mednotes/pdf-library.toml +24 -0
- package/core/commands/mednotes/process-chats.toml +20 -0
- package/core/commands/mednotes/setup.toml +18 -0
- package/core/commands/mednotes/status.toml +24 -0
- package/core/commands/mednotes/telemetry.toml +24 -0
- package/core/commands/report.toml +23 -0
- package/core/skills/THIRD_PARTY_NOTICES.md +45 -0
- package/core/skills/create-medical-flashcards/SKILL.md +113 -0
- package/core/skills/create-medical-note/SKILL.md +90 -0
- package/core/skills/enrich-medical-note/SKILL.md +120 -0
- package/core/skills/fix-medical-wiki/SKILL.md +559 -0
- package/core/skills/link-medical-wiki/SKILL.md +224 -0
- package/core/skills/obsidian-cli/SKILL.md +118 -0
- package/core/skills/obsidian-markdown/SKILL.md +207 -0
- package/core/skills/obsidian-markdown/references/CALLOUTS.md +58 -0
- package/core/skills/obsidian-markdown/references/EMBEDS.md +63 -0
- package/core/skills/obsidian-markdown/references/PROPERTIES.md +61 -0
- package/core/skills/obsidian-ops/SKILL.md +136 -0
- package/core/skills/pdf-library/SKILL.md +45 -0
- package/core/skills/process-medical-chats/SKILL.md +246 -0
- package/core/skills/workflow-report/SKILL.md +100 -0
- package/package.json +45 -0
|
@@ -0,0 +1,2470 @@
|
|
|
1
|
+
"""Official specialist task runners for Workbench-mediated medical authoring."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import Field
|
|
14
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
15
|
+
|
|
16
|
+
from mednotes.domains.wiki.capabilities.graph.coverage import validate_raw_coverage_structure
|
|
17
|
+
from mednotes.domains.wiki.capabilities.notes.raw_chats import atomic_write_text
|
|
18
|
+
from mednotes.domains.wiki.capabilities.specialist.plan_attestation import validate_subagent_plan_attestation
|
|
19
|
+
from mednotes.domains.wiki.capabilities.specialist.specialist_receipts import (
|
|
20
|
+
attach_specialist_task_run_receipt_attestation,
|
|
21
|
+
)
|
|
22
|
+
from mednotes.domains.wiki.capabilities.style.style import (
|
|
23
|
+
_normalize_style_rewrite_output_file,
|
|
24
|
+
_read_json_object,
|
|
25
|
+
_sha256_bytes,
|
|
26
|
+
_style_rewrite_model_policy,
|
|
27
|
+
_style_rewrite_output_attestation_path,
|
|
28
|
+
_style_rewrite_output_receipt_path,
|
|
29
|
+
_style_rewrite_work_item,
|
|
30
|
+
_validate_style_rewrite_plan,
|
|
31
|
+
_verify_style_rewrite_plan_attestation,
|
|
32
|
+
apply_style_rewrite,
|
|
33
|
+
fix_note_style_file,
|
|
34
|
+
validate_note_style_file,
|
|
35
|
+
)
|
|
36
|
+
from mednotes.domains.wiki.common import DOCS_RELPATH, MissingPathError, ValidationError
|
|
37
|
+
from mednotes.domains.wiki.contracts.agent_report import FixWikiPrimaryObjectiveSummary
|
|
38
|
+
from mednotes.domains.wiki.contracts.agents import SubagentBatchPlan, SubagentWorkItem
|
|
39
|
+
from mednotes.domains.wiki.contracts.specialist import (
|
|
40
|
+
SpecialistHarness,
|
|
41
|
+
SpecialistNextApplyStep,
|
|
42
|
+
SpecialistRunStatus,
|
|
43
|
+
SpecialistTaskRunReceipt,
|
|
44
|
+
)
|
|
45
|
+
from mednotes.domains.wiki.contracts.workflow_guardrails import error_context
|
|
46
|
+
from mednotes.domains.wiki.flows.fix_wiki.fix_wiki_primary_objective import fix_wiki_primary_objective_summary
|
|
47
|
+
from mednotes.kernel.agent_directive import AgentDirective
|
|
48
|
+
from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter, contract_error
|
|
49
|
+
|
|
50
|
+
SPECIALIST_TASK_RUNNER_RESULT_SCHEMA = "medical-notes-workbench.specialist-task-runner-result.v1"
|
|
51
|
+
SPECIALIST_TASK_RUNNER_INPUT_SCHEMA = "medical-notes-workbench.specialist-task-runner-input.v1"
|
|
52
|
+
SPECIALIST_TASK_RUNNER_TRANSCRIPT_SCHEMA = "medical-notes-workbench.specialist-task-runner-transcript.v1"
|
|
53
|
+
MEDNOTES_AGENT_DIRECTIVE_SCHEMA = "medical-notes-workbench.agent-directive.v1"
|
|
54
|
+
AGY_SPECIALIST_TRANSCRIPT_ARTIFACT_SCHEMA = "medical-notes-workbench.agy-specialist-transcript-artifact.v1"
|
|
55
|
+
OPENCODE_SPECIALIST_TASK_METADATA_SCHEMA = "medical-notes-workbench.opencode-specialist-task-metadata.v1"
|
|
56
|
+
OPENCODE_SPECIALIST_TASK_ARTIFACT_SCHEMA = "medical-notes-workbench.opencode-specialist-task-artifact.v1"
|
|
57
|
+
ARCHITECT_TASK_RUNNER_RESULT_SCHEMA = "medical-notes-workbench.architect-task-runner-result.v1"
|
|
58
|
+
ARCHITECT_TASK_RUN_RECEIPT_SCHEMA = "medical-notes-workbench.architect-task-run-receipt.v1"
|
|
59
|
+
ARCHITECT_TASK_OUTPUT_SCHEMA = "medical-notes-workbench.architect-output.v1"
|
|
60
|
+
ARCHITECT_NEXT_SERIAL_STEP_SCHEMA = "medical-notes-workbench.architect-next-serial-step.v1"
|
|
61
|
+
GEMINI_CLI_SPECIALIST_ADAPTER = "gemini_cli_headless_runner"
|
|
62
|
+
AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER = "agy_packaged_template_subagent"
|
|
63
|
+
OPENCODE_TASK_SUBAGENT_ADAPTER = "opencode_task_subagent"
|
|
64
|
+
AGY_SELECTED_MODEL_OVERRIDE_RE = re.compile(
|
|
65
|
+
r'Propagating selected model override to backend:\s+label="(?P<label>[^"]+)"'
|
|
66
|
+
)
|
|
67
|
+
_MAX_CAPTURE_CHARS_PER_STREAM = 512_000
|
|
68
|
+
_NO_MCP_SERVER_SENTINEL = "__mednotes_no_mcp__"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SpecialistTaskRunnerResult(ContractModel):
|
|
72
|
+
schema_id: Literal["medical-notes-workbench.specialist-task-runner-result.v1"] = Field(
|
|
73
|
+
default=SPECIALIST_TASK_RUNNER_RESULT_SCHEMA,
|
|
74
|
+
alias="schema",
|
|
75
|
+
)
|
|
76
|
+
phase: Literal["style_rewrite"] = "style_rewrite"
|
|
77
|
+
status: SpecialistRunStatus
|
|
78
|
+
blocked_reason: str = ""
|
|
79
|
+
next_action: str = ""
|
|
80
|
+
required_inputs: list[str] = Field(default_factory=list)
|
|
81
|
+
human_decision_required: bool = False
|
|
82
|
+
work_id: str = Field(min_length=1)
|
|
83
|
+
harness: SpecialistHarness
|
|
84
|
+
adapter: str = Field(min_length=1)
|
|
85
|
+
requested_agent: str = "med-knowledge-architect"
|
|
86
|
+
requested_model: str = Field(min_length=1)
|
|
87
|
+
observed_model: str = ""
|
|
88
|
+
plan_path: str
|
|
89
|
+
target_path: str = ""
|
|
90
|
+
output_path: str = ""
|
|
91
|
+
output_sha256: str = ""
|
|
92
|
+
receipt_path: str = ""
|
|
93
|
+
input_packet_path: str = ""
|
|
94
|
+
transcript_artifact_path: str = ""
|
|
95
|
+
validation: JsonObject = Field(default_factory=dict)
|
|
96
|
+
next_apply_step: SpecialistNextApplyStep | None = None
|
|
97
|
+
error_context: JsonObject = Field(default_factory=dict)
|
|
98
|
+
parent_workflow_summary: JsonObject = Field(default_factory=dict)
|
|
99
|
+
public_report: JsonObject = Field(default_factory=dict)
|
|
100
|
+
agent_directive: JsonObject = Field(default_factory=dict)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class OpenCodeSpecialistTaskMetadata(ContractModel):
|
|
104
|
+
schema_id: Literal["medical-notes-workbench.opencode-specialist-task-metadata.v1"] = Field(
|
|
105
|
+
default=OPENCODE_SPECIALIST_TASK_METADATA_SCHEMA,
|
|
106
|
+
alias="schema",
|
|
107
|
+
)
|
|
108
|
+
work_id: str = Field(min_length=1)
|
|
109
|
+
task_id: str = Field(min_length=1)
|
|
110
|
+
parent_session_id: str = Field(min_length=1)
|
|
111
|
+
specialist_session_id: str = Field(min_length=1)
|
|
112
|
+
provider_id: str = Field(min_length=1)
|
|
113
|
+
model_id: str = Field(min_length=1)
|
|
114
|
+
model_tier: str = Field(min_length=1)
|
|
115
|
+
tool_sequence: list[str] = Field(default_factory=list)
|
|
116
|
+
prompt_contract: Literal["single_current_batch_items_json"] = "single_current_batch_items_json"
|
|
117
|
+
raw_content_embedded: bool = False
|
|
118
|
+
capture_source: Literal["opencode_tool_execute_after"]
|
|
119
|
+
capture_session_id: str = Field(min_length=1)
|
|
120
|
+
tool_call_id: str = Field(min_length=1)
|
|
121
|
+
tool_prompt_sha256: str = Field(min_length=1)
|
|
122
|
+
tool_response_sha256: str = Field(min_length=1)
|
|
123
|
+
captured_at: str = Field(min_length=1)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ArchitectTaskOutput(ContractModel):
|
|
127
|
+
"""Structured output returned by med-knowledge-architect for process-chats."""
|
|
128
|
+
|
|
129
|
+
schema_id: Literal["medical-notes-workbench.architect-output.v1"] = Field(
|
|
130
|
+
default=ARCHITECT_TASK_OUTPUT_SCHEMA,
|
|
131
|
+
alias="schema",
|
|
132
|
+
)
|
|
133
|
+
status: Literal["completed"]
|
|
134
|
+
original_path: str = Field(min_length=1)
|
|
135
|
+
temp_output_path: str = Field(min_length=1)
|
|
136
|
+
coverage_path: str = Field(min_length=1)
|
|
137
|
+
title: str = Field(min_length=1)
|
|
138
|
+
staged_title: str = Field(min_length=1)
|
|
139
|
+
taxonomy: str = Field(min_length=1)
|
|
140
|
+
aliases: list[str] = Field(default_factory=list)
|
|
141
|
+
entity_proposals: list[JsonObject] = Field(default_factory=list)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ArchitectNextSerialStep(ContractModel):
|
|
145
|
+
"""Next parent-owned command after a validated architect output."""
|
|
146
|
+
|
|
147
|
+
schema_id: Literal["medical-notes-workbench.architect-next-serial-step.v1"] = Field(
|
|
148
|
+
default=ARCHITECT_NEXT_SERIAL_STEP_SCHEMA,
|
|
149
|
+
alias="schema",
|
|
150
|
+
)
|
|
151
|
+
command_family: Literal["stage-note"] = "stage-note"
|
|
152
|
+
arguments: list[str] = Field(min_length=1)
|
|
153
|
+
must_run_before: list[str] = Field(default_factory=list)
|
|
154
|
+
agent_instruction: str = Field(min_length=1)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class ArchitectTaskRunReceipt(ContractModel):
|
|
158
|
+
"""Receipt proving one OpenCode architect task before process-chats stages it."""
|
|
159
|
+
|
|
160
|
+
schema_id: Literal["medical-notes-workbench.architect-task-run-receipt.v1"] = Field(
|
|
161
|
+
default=ARCHITECT_TASK_RUN_RECEIPT_SCHEMA,
|
|
162
|
+
alias="schema",
|
|
163
|
+
)
|
|
164
|
+
phase: Literal["architect"] = "architect"
|
|
165
|
+
status: Literal["completed"] = "completed"
|
|
166
|
+
work_id: str = Field(min_length=1)
|
|
167
|
+
harness: SpecialistHarness
|
|
168
|
+
adapter: str = Field(min_length=1)
|
|
169
|
+
requested_agent: str = "med-knowledge-architect"
|
|
170
|
+
requested_model: str = Field(min_length=1)
|
|
171
|
+
observed_model: str = Field(min_length=1)
|
|
172
|
+
plan_path: str = Field(min_length=1)
|
|
173
|
+
raw_file: str = Field(min_length=1)
|
|
174
|
+
title: str = Field(min_length=1)
|
|
175
|
+
taxonomy: str = Field(min_length=1)
|
|
176
|
+
output_path: str = Field(min_length=1)
|
|
177
|
+
output_sha256: str = Field(pattern=r"^sha256:[0-9a-f]{64}$")
|
|
178
|
+
coverage_path: str = Field(min_length=1)
|
|
179
|
+
coverage_sha256: str = Field(pattern=r"^sha256:[0-9a-f]{64}$")
|
|
180
|
+
architect_output_path: str = Field(min_length=1)
|
|
181
|
+
architect_output_sha256: str = Field(pattern=r"^sha256:[0-9a-f]{64}$")
|
|
182
|
+
task_metadata_path: str = Field(min_length=1)
|
|
183
|
+
task_metadata_sha256: str = Field(pattern=r"^sha256:[0-9a-f]{64}$")
|
|
184
|
+
model_evidence: JsonObject
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class ArchitectTaskRunnerResult(ContractModel):
|
|
188
|
+
"""Local finalizer result; process-chats FSM remains the workflow truth."""
|
|
189
|
+
|
|
190
|
+
schema_id: Literal["medical-notes-workbench.architect-task-runner-result.v1"] = Field(
|
|
191
|
+
default=ARCHITECT_TASK_RUNNER_RESULT_SCHEMA,
|
|
192
|
+
alias="schema",
|
|
193
|
+
)
|
|
194
|
+
phase: Literal["architect"] = "architect"
|
|
195
|
+
status: SpecialistRunStatus
|
|
196
|
+
blocked_reason: str = ""
|
|
197
|
+
next_action: str = ""
|
|
198
|
+
required_inputs: list[str] = Field(default_factory=list)
|
|
199
|
+
human_decision_required: bool = False
|
|
200
|
+
work_id: str = Field(min_length=1)
|
|
201
|
+
harness: SpecialistHarness
|
|
202
|
+
adapter: str = Field(min_length=1)
|
|
203
|
+
requested_agent: str = "med-knowledge-architect"
|
|
204
|
+
requested_model: str = Field(min_length=1)
|
|
205
|
+
observed_model: str = ""
|
|
206
|
+
plan_path: str = Field(min_length=1)
|
|
207
|
+
raw_file: str = ""
|
|
208
|
+
title: str = ""
|
|
209
|
+
taxonomy: str = ""
|
|
210
|
+
output_path: str = ""
|
|
211
|
+
output_sha256: str = ""
|
|
212
|
+
coverage_path: str = ""
|
|
213
|
+
receipt_path: str = ""
|
|
214
|
+
architect_output_path: str = ""
|
|
215
|
+
task_metadata_path: str = ""
|
|
216
|
+
validation: JsonObject = Field(default_factory=dict)
|
|
217
|
+
next_serial_step: ArchitectNextSerialStep | None = None
|
|
218
|
+
error_context: JsonObject = Field(default_factory=dict)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _result(
|
|
222
|
+
*,
|
|
223
|
+
status: SpecialistRunStatus,
|
|
224
|
+
work_id: str,
|
|
225
|
+
harness: SpecialistHarness,
|
|
226
|
+
requested_model: str,
|
|
227
|
+
plan_path: Path,
|
|
228
|
+
blocked_reason: str = "",
|
|
229
|
+
next_action: str = "",
|
|
230
|
+
required_inputs: list[str] | None = None,
|
|
231
|
+
target_path: Path | None = None,
|
|
232
|
+
output_path: Path | None = None,
|
|
233
|
+
output_sha256: str = "",
|
|
234
|
+
receipt_path: Path | None = None,
|
|
235
|
+
input_packet_path: Path | None = None,
|
|
236
|
+
transcript_artifact_path: Path | None = None,
|
|
237
|
+
observed_model: str = "",
|
|
238
|
+
validation: JsonObject | None = None,
|
|
239
|
+
adapter: str = GEMINI_CLI_SPECIALIST_ADAPTER,
|
|
240
|
+
) -> JsonObject:
|
|
241
|
+
parent_workflow_summary = _parent_workflow_summary_for_plan(plan_path)
|
|
242
|
+
next_apply_step = _specialist_task_next_apply_step(
|
|
243
|
+
status=status,
|
|
244
|
+
plan_path=plan_path,
|
|
245
|
+
work_id=work_id,
|
|
246
|
+
receipt_path=receipt_path,
|
|
247
|
+
)
|
|
248
|
+
payload = SpecialistTaskRunnerResult(
|
|
249
|
+
status=status,
|
|
250
|
+
blocked_reason=blocked_reason,
|
|
251
|
+
next_action=next_action,
|
|
252
|
+
required_inputs=required_inputs or [],
|
|
253
|
+
work_id=work_id,
|
|
254
|
+
harness=harness,
|
|
255
|
+
adapter=adapter,
|
|
256
|
+
requested_model=requested_model,
|
|
257
|
+
observed_model=observed_model,
|
|
258
|
+
plan_path=str(plan_path),
|
|
259
|
+
target_path=str(target_path or ""),
|
|
260
|
+
output_path=str(output_path or ""),
|
|
261
|
+
output_sha256=output_sha256,
|
|
262
|
+
receipt_path=str(receipt_path or ""),
|
|
263
|
+
input_packet_path=str(input_packet_path or ""),
|
|
264
|
+
transcript_artifact_path=str(transcript_artifact_path or ""),
|
|
265
|
+
validation=validation or {},
|
|
266
|
+
next_apply_step=next_apply_step,
|
|
267
|
+
error_context=error_context(
|
|
268
|
+
phase="style_rewrite",
|
|
269
|
+
blocked_reason=blocked_reason,
|
|
270
|
+
root_cause=blocked_reason,
|
|
271
|
+
affected_artifact=str(output_path or receipt_path or plan_path),
|
|
272
|
+
error_summary="Specialist task runner could not complete the Workbench receipt boundary."
|
|
273
|
+
if status != SpecialistRunStatus.COMPLETED
|
|
274
|
+
else "",
|
|
275
|
+
suggested_fix=next_action,
|
|
276
|
+
next_action=next_action,
|
|
277
|
+
retry_scope="single_style_rewrite_work_item",
|
|
278
|
+
)
|
|
279
|
+
if status != SpecialistRunStatus.COMPLETED
|
|
280
|
+
else {},
|
|
281
|
+
parent_workflow_summary=parent_workflow_summary,
|
|
282
|
+
public_report=_specialist_task_public_report(
|
|
283
|
+
status=status,
|
|
284
|
+
blocked_reason=blocked_reason,
|
|
285
|
+
target_title=(target_path.stem if target_path else work_id),
|
|
286
|
+
parent_workflow_summary=parent_workflow_summary,
|
|
287
|
+
),
|
|
288
|
+
agent_directive=_specialist_task_agent_directive(
|
|
289
|
+
status=status,
|
|
290
|
+
parent_workflow_summary=parent_workflow_summary,
|
|
291
|
+
next_apply_step=next_apply_step,
|
|
292
|
+
blocked_reason=blocked_reason,
|
|
293
|
+
next_action=next_action,
|
|
294
|
+
),
|
|
295
|
+
)
|
|
296
|
+
return payload.to_payload()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _parent_workflow_summary_for_plan(plan_path: Path) -> JsonObject:
|
|
300
|
+
for candidate in (
|
|
301
|
+
plan_path.parent / "compact-report.json",
|
|
302
|
+
plan_path.parent / "full-report.json",
|
|
303
|
+
plan_path.parent / "run_state.json",
|
|
304
|
+
):
|
|
305
|
+
if not candidate.exists():
|
|
306
|
+
continue
|
|
307
|
+
try:
|
|
308
|
+
payload = _read_json_object(candidate, label=f"parent workflow report {candidate.name}")
|
|
309
|
+
except (MissingPathError, ValidationError):
|
|
310
|
+
continue
|
|
311
|
+
direct = _fix_wiki_summary_from_payload(payload)
|
|
312
|
+
if direct:
|
|
313
|
+
return direct
|
|
314
|
+
nested_report = payload["report"] if "report" in payload else None
|
|
315
|
+
if isinstance(nested_report, dict):
|
|
316
|
+
nested = _fix_wiki_summary_from_payload(JsonObjectAdapter.validate_python(nested_report))
|
|
317
|
+
if nested:
|
|
318
|
+
return nested
|
|
319
|
+
return {}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _fix_wiki_summary_from_payload(payload: JsonObject) -> JsonObject:
|
|
323
|
+
reports_payload = payload["reports"] if "reports" in payload else {}
|
|
324
|
+
reports = JsonObjectAdapter.validate_python(reports_payload)
|
|
325
|
+
details_payload = reports["details"] if "details" in reports else {}
|
|
326
|
+
details = JsonObjectAdapter.validate_python(details_payload)
|
|
327
|
+
summary_payload = details["primary_objective_summary"] if "primary_objective_summary" in details else None
|
|
328
|
+
if isinstance(summary_payload, dict):
|
|
329
|
+
try:
|
|
330
|
+
return FixWikiPrimaryObjectiveSummary.model_validate(summary_payload).to_payload()
|
|
331
|
+
except PydanticValidationError:
|
|
332
|
+
pass
|
|
333
|
+
try:
|
|
334
|
+
objective = fix_wiki_primary_objective_summary(payload)
|
|
335
|
+
except (TypeError, ValueError, PydanticValidationError):
|
|
336
|
+
return {}
|
|
337
|
+
return objective.to_payload() if objective is not None else {}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _typed_subagent_work_item(payload: JsonObject) -> SubagentWorkItem:
|
|
341
|
+
"""Validate runner work items before they select outputs or receipt paths."""
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
return SubagentWorkItem.model_validate(payload)
|
|
345
|
+
except PydanticValidationError as exc:
|
|
346
|
+
raise contract_error(exc, prefix="specialist task runner work item invalid") from exc
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _specialist_task_next_apply_step(
|
|
350
|
+
*,
|
|
351
|
+
status: SpecialistRunStatus,
|
|
352
|
+
plan_path: Path,
|
|
353
|
+
work_id: str,
|
|
354
|
+
receipt_path: Path | None,
|
|
355
|
+
) -> SpecialistNextApplyStep | None:
|
|
356
|
+
if status != SpecialistRunStatus.COMPLETED or receipt_path is None:
|
|
357
|
+
return None
|
|
358
|
+
manifest_path = plan_path.with_name("style-rewrite-manifest.json")
|
|
359
|
+
return SpecialistNextApplyStep.model_validate(
|
|
360
|
+
{
|
|
361
|
+
"schema": "medical-notes-workbench.specialist-next-apply-step.v1",
|
|
362
|
+
"command_family": "apply-specialist-style-rewrite",
|
|
363
|
+
"arguments": [
|
|
364
|
+
"--plan",
|
|
365
|
+
str(plan_path),
|
|
366
|
+
"--manifest",
|
|
367
|
+
str(manifest_path),
|
|
368
|
+
"--work-id",
|
|
369
|
+
work_id,
|
|
370
|
+
"--specialist-run-receipt",
|
|
371
|
+
str(receipt_path),
|
|
372
|
+
"--json",
|
|
373
|
+
],
|
|
374
|
+
"must_run_before": [
|
|
375
|
+
"fix-wiki --apply",
|
|
376
|
+
"plan-subagents --phase style-rewrite",
|
|
377
|
+
"another specialist invocation",
|
|
378
|
+
"read_file manifest/plan probing",
|
|
379
|
+
],
|
|
380
|
+
"agent_instruction": (
|
|
381
|
+
"Especialista validado. Aplique este recibo agora com apply-specialist-style-rewrite; "
|
|
382
|
+
"nao reavalie a Wiki, nao rode plan-subagents e nao leia manifest/plan para descobrir a rota antes do apply."
|
|
383
|
+
),
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _specialist_task_public_report(
|
|
389
|
+
*,
|
|
390
|
+
status: SpecialistRunStatus,
|
|
391
|
+
blocked_reason: str,
|
|
392
|
+
target_title: str,
|
|
393
|
+
parent_workflow_summary: JsonObject | None = None,
|
|
394
|
+
) -> JsonObject:
|
|
395
|
+
if status == SpecialistRunStatus.COMPLETED:
|
|
396
|
+
return {}
|
|
397
|
+
parent_lines = _parent_workflow_public_lines(parent_workflow_summary or {})
|
|
398
|
+
if blocked_reason == "specialist_model_quota_exhausted":
|
|
399
|
+
headline = "A Wiki ainda precisa da reescrita médica especializada."
|
|
400
|
+
lines = [
|
|
401
|
+
"A Wiki não foi fixada por completo nesta execução.",
|
|
402
|
+
*parent_lines,
|
|
403
|
+
f"Nenhuma nota foi reescrita neste lote; o primeiro item era {target_title}.",
|
|
404
|
+
"A etapa automática segura já chegou até a chamada do modelo médico, mas a capacidade desse modelo acabou agora.",
|
|
405
|
+
"Retome pelo fluxo oficial quando a capacidade voltar; não substitua por outro modelo nem tente contornar manualmente.",
|
|
406
|
+
]
|
|
407
|
+
else:
|
|
408
|
+
headline = "A reescrita médica especializada parou antes de aplicar a nota."
|
|
409
|
+
lines = [
|
|
410
|
+
"A Wiki não foi fixada por completo nesta execução.",
|
|
411
|
+
*parent_lines,
|
|
412
|
+
f"Nenhuma nota foi reescrita neste lote; o primeiro item era {target_title}.",
|
|
413
|
+
"A etapa de reescrita médica especializada parou antes de gerar uma nota validada.",
|
|
414
|
+
"Retome pelo fluxo oficial depois de resolver o bloqueio indicado no relatório técnico.",
|
|
415
|
+
]
|
|
416
|
+
return {
|
|
417
|
+
"schema": "medical-notes-workbench.specialist-task-public-report.v1",
|
|
418
|
+
"audience": "user",
|
|
419
|
+
"status": str(status),
|
|
420
|
+
"headline": headline,
|
|
421
|
+
"lines": lines,
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _parent_workflow_public_lines(parent_workflow_summary: JsonObject) -> list[str]:
|
|
426
|
+
if not parent_workflow_summary:
|
|
427
|
+
return []
|
|
428
|
+
try:
|
|
429
|
+
summary = FixWikiPrimaryObjectiveSummary.model_validate(parent_workflow_summary)
|
|
430
|
+
except PydanticValidationError:
|
|
431
|
+
return []
|
|
432
|
+
return [
|
|
433
|
+
summary.wiki_summary,
|
|
434
|
+
summary.mutation_summary,
|
|
435
|
+
summary.graph_summary,
|
|
436
|
+
summary.related_notes_summary,
|
|
437
|
+
]
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _specialist_task_agent_directive(
|
|
441
|
+
*,
|
|
442
|
+
status: SpecialistRunStatus,
|
|
443
|
+
parent_workflow_summary: JsonObject | None = None,
|
|
444
|
+
next_apply_step: SpecialistNextApplyStep | None = None,
|
|
445
|
+
blocked_reason: str = "",
|
|
446
|
+
next_action: str = "",
|
|
447
|
+
) -> JsonObject:
|
|
448
|
+
carry_forward_lines = []
|
|
449
|
+
if parent_workflow_summary:
|
|
450
|
+
carry_forward_lines = [
|
|
451
|
+
"Este resultado do especialista nao substitui o resultado principal do fix-wiki.",
|
|
452
|
+
"Preserve parent_workflow_summary.wiki_summary, mutation_summary, graph_summary e related_notes_summary na resposta final.",
|
|
453
|
+
"Nao substitua as contagens de mutacao do parent_workflow_summary pela contagem deste item especialista.",
|
|
454
|
+
]
|
|
455
|
+
if status == SpecialistRunStatus.COMPLETED:
|
|
456
|
+
resume = ""
|
|
457
|
+
if next_apply_step is not None:
|
|
458
|
+
resume = " ".join([next_apply_step.command_family, *next_apply_step.arguments]).strip()
|
|
459
|
+
directive = AgentDirective.model_validate(
|
|
460
|
+
{
|
|
461
|
+
"workflow": "/mednotes:fix-wiki",
|
|
462
|
+
"run_id": "specialist-task",
|
|
463
|
+
"control": {
|
|
464
|
+
"status": "running",
|
|
465
|
+
"state": "specialist_task_completed",
|
|
466
|
+
"phase": "style_rewrite",
|
|
467
|
+
"reason": "specialist_output_ready",
|
|
468
|
+
"capabilities": {"continue": True, "final_report": False},
|
|
469
|
+
"effects": [],
|
|
470
|
+
"blockers": [],
|
|
471
|
+
"resume": resume,
|
|
472
|
+
"report": {"requires": ["parent_workflow_summary", "specialist_checkpoint"]},
|
|
473
|
+
"limits": {"raw_content": False, "absolute_paths": False, "ad_hoc_scripts": False},
|
|
474
|
+
},
|
|
475
|
+
"summary": "O especialista gerou a saida; aplique o proximo passo oficial antes do relatorio final.",
|
|
476
|
+
"instructions": [
|
|
477
|
+
*carry_forward_lines,
|
|
478
|
+
"status=completed significa que o especialista gerou temp_output e receipt_path oficiais.",
|
|
479
|
+
"O proximo passo imediato e next_apply_step.command_family=apply-specialist-style-rewrite.",
|
|
480
|
+
"Nao rode fix-wiki --apply, plan-subagents, read_file de manifest/plan ou outra chamada especialista antes de aplicar next_apply_step.arguments.",
|
|
481
|
+
"Depois do apply, reporte um checkpoint humano curto de qualidade/YAML/proveniencia/links antes de planejar a proxima leva.",
|
|
482
|
+
],
|
|
483
|
+
}
|
|
484
|
+
).to_payload()
|
|
485
|
+
directive["schema"] = MEDNOTES_AGENT_DIRECTIVE_SCHEMA
|
|
486
|
+
return JsonObjectAdapter.validate_python(directive)
|
|
487
|
+
directive = AgentDirective.model_validate(
|
|
488
|
+
{
|
|
489
|
+
"workflow": "/mednotes:fix-wiki",
|
|
490
|
+
"run_id": "specialist-task",
|
|
491
|
+
"control": {
|
|
492
|
+
"status": "blocked",
|
|
493
|
+
"state": "specialist_task_blocked",
|
|
494
|
+
"phase": "style_rewrite",
|
|
495
|
+
"reason": blocked_reason or "specialist_task_blocked",
|
|
496
|
+
"capabilities": {"continue": False, "final_report": False},
|
|
497
|
+
"effects": [],
|
|
498
|
+
"blockers": [blocked_reason or "specialist_task_blocked"],
|
|
499
|
+
"resume": next_action,
|
|
500
|
+
"report": {"requires": ["public_report", "error_context"]},
|
|
501
|
+
"limits": {"raw_content": False, "absolute_paths": False, "ad_hoc_scripts": False},
|
|
502
|
+
},
|
|
503
|
+
"summary": "A tarefa especialista esta bloqueada; reporte o bloqueio sem declarar sucesso.",
|
|
504
|
+
"instructions": [
|
|
505
|
+
*carry_forward_lines,
|
|
506
|
+
"This is a specialist-task payload, not the parent FSM result; use its root public_report.lines only for this local blocker.",
|
|
507
|
+
"Do not use sucesso, com sucesso, concluido, concluiu, finalizado or pronto while this specialist task is blocked.",
|
|
508
|
+
"Do not render blocked_reason, root_cause, returncode, exit code or internal blocker codes in the public response.",
|
|
509
|
+
"Do not render target_path, output_path, receipt_path, transcript_artifact_path, file links or local absolute paths in the public response.",
|
|
510
|
+
"If CPU samples show high CPU, report it as observed impact; do not call 100% CPU expected or harmless.",
|
|
511
|
+
"No specialist note was rewritten unless status=completed and receipt_path exists.",
|
|
512
|
+
],
|
|
513
|
+
}
|
|
514
|
+
).to_payload()
|
|
515
|
+
directive["schema"] = MEDNOTES_AGENT_DIRECTIVE_SCHEMA
|
|
516
|
+
return JsonObjectAdapter.validate_python(directive)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _bounded_text(value: str) -> tuple[str, bool]:
|
|
520
|
+
if len(value) <= _MAX_CAPTURE_CHARS_PER_STREAM:
|
|
521
|
+
return value, False
|
|
522
|
+
return value[:_MAX_CAPTURE_CHARS_PER_STREAM], True
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _timeout_stream_text(value: object, *, fallback: str = "") -> str:
|
|
526
|
+
if isinstance(value, str):
|
|
527
|
+
return value
|
|
528
|
+
if isinstance(value, bytes):
|
|
529
|
+
return value.decode("utf-8", errors="replace")
|
|
530
|
+
return fallback
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _write_json(path: Path, payload: JsonObject) -> None:
|
|
534
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
535
|
+
atomic_write_text(path, json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _write_input_packet(*, path: Path, work_item: JsonObject, plan_path: Path, model: str) -> str:
|
|
539
|
+
packet = JsonObjectAdapter.validate_python(
|
|
540
|
+
{
|
|
541
|
+
"schema": SPECIALIST_TASK_RUNNER_INPUT_SCHEMA,
|
|
542
|
+
"phase": "style_rewrite",
|
|
543
|
+
"work_id": str(work_item.get("work_id") or ""),
|
|
544
|
+
"requested_agent": str(work_item.get("agent") or "med-knowledge-architect"),
|
|
545
|
+
"requested_model": model,
|
|
546
|
+
"model_policy": _style_rewrite_model_policy(work_item),
|
|
547
|
+
"target_path": str(work_item.get("target_path") or ""),
|
|
548
|
+
"target_hash_before": str(work_item.get("target_hash_before") or ""),
|
|
549
|
+
"temp_output": str(work_item.get("temp_output") or work_item.get("output_path") or ""),
|
|
550
|
+
"plan_path": str(plan_path),
|
|
551
|
+
"agent_input_rule": "O especialista deve ler a nota alvo pelo target_path e gravar somente temp_output.",
|
|
552
|
+
"raw_content_in_prompt": False,
|
|
553
|
+
}
|
|
554
|
+
)
|
|
555
|
+
_write_json(path, packet)
|
|
556
|
+
return _sha256_bytes(path.read_bytes())
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _extension_root() -> Path:
|
|
560
|
+
from mednotes.platform.paths import extension_root
|
|
561
|
+
|
|
562
|
+
return extension_root()
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _agent_template_path() -> Path:
|
|
566
|
+
return _extension_root() / "agents" / "med-knowledge-architect.md"
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _required_read_files() -> tuple[Path, ...]:
|
|
570
|
+
docs_dir = _extension_root() / "docs"
|
|
571
|
+
return (
|
|
572
|
+
docs_dir / "agent-prompt-hardening.md",
|
|
573
|
+
docs_dir / "knowledge-architect.md",
|
|
574
|
+
docs_dir / "semantic-linker.md",
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _prompt_for_gemini_cli(
|
|
579
|
+
*,
|
|
580
|
+
work_item: JsonObject,
|
|
581
|
+
input_packet_path: Path,
|
|
582
|
+
attempt: int = 1,
|
|
583
|
+
previous_failure: str = "",
|
|
584
|
+
) -> str:
|
|
585
|
+
extension_root = _extension_root()
|
|
586
|
+
pointer_json = json.dumps(
|
|
587
|
+
{
|
|
588
|
+
"work_id": str(work_item.get("work_id") or ""),
|
|
589
|
+
"input_packet_path": str(input_packet_path),
|
|
590
|
+
"target_path": str(work_item.get("target_path") or ""),
|
|
591
|
+
"target_hash_before": str(work_item.get("target_hash_before") or ""),
|
|
592
|
+
"temp_output": str(work_item.get("temp_output") or ""),
|
|
593
|
+
},
|
|
594
|
+
ensure_ascii=False,
|
|
595
|
+
indent=2,
|
|
596
|
+
sort_keys=True,
|
|
597
|
+
)
|
|
598
|
+
required_read_files = "\n".join(f"- {path}" for path in _required_read_files())
|
|
599
|
+
retry_block = ""
|
|
600
|
+
if attempt > 1:
|
|
601
|
+
if previous_failure.startswith("validation:"):
|
|
602
|
+
retry_block = (
|
|
603
|
+
"\nPrevious attempt failed validation.\n"
|
|
604
|
+
f"Validation feedback: {previous_failure.removeprefix('validation:').strip()}.\n"
|
|
605
|
+
"Regenerate the full note and fix every listed issue. Do not explain the failure as completion. "
|
|
606
|
+
"Write the file first, then respond briefly.\n\n"
|
|
607
|
+
)
|
|
608
|
+
else:
|
|
609
|
+
retry_block = (
|
|
610
|
+
"\nPrevious attempt failed before producing the required temp_output file.\n"
|
|
611
|
+
f"Failure reason: {previous_failure or 'temp_output was not created'}.\n"
|
|
612
|
+
"This retry is still the official Workbench route. Do not explain the failure as completion. "
|
|
613
|
+
"Write the file first, then respond briefly.\n\n"
|
|
614
|
+
)
|
|
615
|
+
return (
|
|
616
|
+
"You are running the packaged med-knowledge-architect specialist task for Medical Notes Workbench.\n"
|
|
617
|
+
"Do not paste raw clinical note content into the response. Read the target_path yourself and write the full "
|
|
618
|
+
"rewritten Markdown note to temp_output. Do not create Workbench receipts or attestations; the runner creates "
|
|
619
|
+
"them after validation. A textual answer without the temp_output file is a failed task.\n\n"
|
|
620
|
+
f"{retry_block}"
|
|
621
|
+
"Required action order:\n"
|
|
622
|
+
"1. Read the input packet and the work_item target_path.\n"
|
|
623
|
+
"2. Write the complete rewritten Markdown note to the exact work_item temp_output path.\n"
|
|
624
|
+
"3. Verify that temp_output exists and contains the full note.\n"
|
|
625
|
+
"4. Only then respond briefly; do not summarize instead of writing the file.\n\n"
|
|
626
|
+
f"Packaged extension root: {extension_root}\n"
|
|
627
|
+
f"Required packaged docs directory: {extension_root / 'docs'}\n"
|
|
628
|
+
f"Packaged specialist template path: {_agent_template_path()}\n"
|
|
629
|
+
f"Use only packaged paths shown in this prompt and in the input packet; do not resolve {DOCS_RELPATH} against "
|
|
630
|
+
"the source checkout.\n\n"
|
|
631
|
+
"Required read files before writing:\n"
|
|
632
|
+
f"{required_read_files}\n\n"
|
|
633
|
+
f"Input packet path: {input_packet_path}\n\n"
|
|
634
|
+
"Official work_item pointer JSON. The full typed contract is in the input packet; read it there.\n"
|
|
635
|
+
f"{pointer_json}\n"
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _find_string_field(value: object, field_names: set[str]) -> str:
|
|
640
|
+
if isinstance(value, dict):
|
|
641
|
+
for key, raw in value.items():
|
|
642
|
+
if key.lower() in field_names and isinstance(raw, str) and raw.strip():
|
|
643
|
+
return raw.strip()
|
|
644
|
+
for raw in value.values():
|
|
645
|
+
found = _find_string_field(raw, field_names)
|
|
646
|
+
if found:
|
|
647
|
+
return found
|
|
648
|
+
if isinstance(value, list):
|
|
649
|
+
for item in value:
|
|
650
|
+
found = _find_string_field(item, field_names)
|
|
651
|
+
if found:
|
|
652
|
+
return found
|
|
653
|
+
return ""
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _extract_gemini_metadata(stdout: str) -> tuple[str, str]:
|
|
657
|
+
observed_model = ""
|
|
658
|
+
session_id = ""
|
|
659
|
+
for line in stdout.splitlines():
|
|
660
|
+
compact = line.strip()
|
|
661
|
+
if not compact:
|
|
662
|
+
continue
|
|
663
|
+
try:
|
|
664
|
+
payload = json.loads(compact)
|
|
665
|
+
except json.JSONDecodeError:
|
|
666
|
+
continue
|
|
667
|
+
if not observed_model:
|
|
668
|
+
observed_model = _find_string_field(payload, {"model", "model_id", "modelid"})
|
|
669
|
+
if not session_id:
|
|
670
|
+
session_id = _find_string_field(payload, {"session_id", "sessionid", "conversation_id"})
|
|
671
|
+
if observed_model and session_id:
|
|
672
|
+
break
|
|
673
|
+
return observed_model, session_id
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _gemini_cli_quota_exhausted(*, stdout: str, stderr: str) -> bool:
|
|
677
|
+
combined = f"{stdout}\n{stderr}".lower()
|
|
678
|
+
return (
|
|
679
|
+
"terminalquotaerror" in combined
|
|
680
|
+
or "quota_exhausted" in combined
|
|
681
|
+
or "resource_exhausted" in combined
|
|
682
|
+
or "exhausted your capacity" in combined
|
|
683
|
+
or "exceeded your current quota" in combined
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _gemini_cli_model_unavailable(*, stdout: str, stderr: str) -> bool:
|
|
688
|
+
combined = f"{stdout}\n{stderr}".lower()
|
|
689
|
+
return (
|
|
690
|
+
"modelnotfounderror" in combined
|
|
691
|
+
or "requested entity was not found" in combined
|
|
692
|
+
or "model not found" in combined
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _gemini_cli_policy_config_invalid(*, stdout: str, stderr: str) -> bool:
|
|
697
|
+
combined = f"{stdout}\n{stderr}".lower()
|
|
698
|
+
return (
|
|
699
|
+
"invalid policy rule" in combined
|
|
700
|
+
or "mcpname is required" in combined
|
|
701
|
+
or "rule source: settings (mcp allowed)" in combined
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _redact_prompt_argument(command: list[str]) -> list[str]:
|
|
706
|
+
redacted: list[str] = []
|
|
707
|
+
skip_next = False
|
|
708
|
+
for arg in command:
|
|
709
|
+
if skip_next:
|
|
710
|
+
skip_next = False
|
|
711
|
+
continue
|
|
712
|
+
if arg == "--prompt":
|
|
713
|
+
redacted.append("--prompt=<redacted>")
|
|
714
|
+
skip_next = True
|
|
715
|
+
continue
|
|
716
|
+
redacted.append(arg)
|
|
717
|
+
return redacted
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _specialist_inter_call_delay_seconds() -> float:
|
|
721
|
+
raw = os.environ.get("MEDNOTES_SPECIALIST_INTER_CALL_DELAY_SECONDS", "10").strip()
|
|
722
|
+
try:
|
|
723
|
+
delay = float(raw)
|
|
724
|
+
except ValueError:
|
|
725
|
+
return 10.0
|
|
726
|
+
return max(0.0, delay)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _specialist_max_attempts() -> int:
|
|
730
|
+
raw = os.environ.get("MEDNOTES_SPECIALIST_MAX_ATTEMPTS", "4").strip()
|
|
731
|
+
try:
|
|
732
|
+
attempts = int(raw)
|
|
733
|
+
except ValueError:
|
|
734
|
+
return 4
|
|
735
|
+
return min(6, max(1, attempts))
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _style_validation_retry_reason(validation_block: JsonObject) -> str:
|
|
739
|
+
if validation_block.get("errors"):
|
|
740
|
+
return "style_rewrite_agent_contract_violation"
|
|
741
|
+
if validation_block.get("requires_llm_rewrite"):
|
|
742
|
+
return "style_rewrite_still_requires_rewrite"
|
|
743
|
+
return ""
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _style_validation_retry_feedback(validation_block: JsonObject) -> str:
|
|
747
|
+
issues: list[str] = []
|
|
748
|
+
for field in ("errors", "warnings"):
|
|
749
|
+
raw_items = validation_block.get(field)
|
|
750
|
+
if not isinstance(raw_items, list):
|
|
751
|
+
continue
|
|
752
|
+
for raw_item in raw_items:
|
|
753
|
+
if not isinstance(raw_item, dict):
|
|
754
|
+
continue
|
|
755
|
+
code = str(raw_item.get("code") or "").strip()
|
|
756
|
+
message = str(raw_item.get("message") or "").strip()
|
|
757
|
+
section = str(raw_item.get("section") or "").strip()
|
|
758
|
+
if not code and not message:
|
|
759
|
+
continue
|
|
760
|
+
parts = [part for part in (code, message, f"section={section}" if section else "") if part]
|
|
761
|
+
issue = ": ".join(parts[:2]) + (f" ({parts[2]})" if len(parts) > 2 else "")
|
|
762
|
+
if code == "excessive_callouts":
|
|
763
|
+
issue += "; action: keep at most two callouts in the whole note and convert the rest to normal prose or bullets"
|
|
764
|
+
elif code == "didactic_visual_opportunity":
|
|
765
|
+
suggested_visual = str(raw_item.get("suggested_visual") or "").strip().lower()
|
|
766
|
+
if suggested_visual == "mermaid":
|
|
767
|
+
location = f" in {section}" if section else " in the indicated clinical section"
|
|
768
|
+
issue += (
|
|
769
|
+
"; action: add a ```mermaid fenced diagram"
|
|
770
|
+
f"{location}, immediately after the paragraph/table it clarifies"
|
|
771
|
+
)
|
|
772
|
+
else:
|
|
773
|
+
issue += "; action: add the requested visual in the indicated clinical section"
|
|
774
|
+
issues.append(issue)
|
|
775
|
+
if issues:
|
|
776
|
+
return "validation: " + "; ".join(issues[:6])
|
|
777
|
+
return "validation: generated rewrite still failed Workbench style validation"
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _rate_limit_state_path(plan_path: Path) -> Path:
|
|
781
|
+
return plan_path.parent / "specialist-task-runner-rate-limit.json"
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _enforce_specialist_inter_call_delay(*, plan_path: Path, work_id: str) -> JsonObject:
|
|
785
|
+
delay_seconds = _specialist_inter_call_delay_seconds()
|
|
786
|
+
state_path = _rate_limit_state_path(plan_path)
|
|
787
|
+
waited_seconds = 0.0
|
|
788
|
+
now = time.time()
|
|
789
|
+
previous_started_at = 0.0
|
|
790
|
+
if state_path.exists():
|
|
791
|
+
try:
|
|
792
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
793
|
+
if isinstance(state, dict):
|
|
794
|
+
previous_started_at = float(state.get("last_started_at_epoch") or 0.0)
|
|
795
|
+
except (OSError, ValueError, json.JSONDecodeError, TypeError):
|
|
796
|
+
previous_started_at = 0.0
|
|
797
|
+
if delay_seconds > 0 and previous_started_at > 0:
|
|
798
|
+
elapsed = max(0.0, now - previous_started_at)
|
|
799
|
+
waited_seconds = max(0.0, delay_seconds - elapsed)
|
|
800
|
+
if waited_seconds > 0:
|
|
801
|
+
print(
|
|
802
|
+
"[mednotes] Aguardando intervalo controlado antes do proximo especialista "
|
|
803
|
+
f"({waited_seconds:.1f}s).",
|
|
804
|
+
file=sys.stderr,
|
|
805
|
+
flush=True,
|
|
806
|
+
)
|
|
807
|
+
time.sleep(waited_seconds)
|
|
808
|
+
started_at = time.time()
|
|
809
|
+
_write_json(
|
|
810
|
+
state_path,
|
|
811
|
+
{
|
|
812
|
+
"schema": "medical-notes-workbench.specialist-task-runner-rate-limit.v1",
|
|
813
|
+
"last_started_at_epoch": started_at,
|
|
814
|
+
"last_work_id": work_id,
|
|
815
|
+
"delay_seconds": delay_seconds,
|
|
816
|
+
},
|
|
817
|
+
)
|
|
818
|
+
return JsonObjectAdapter.validate_python(
|
|
819
|
+
{
|
|
820
|
+
"schema": "medical-notes-workbench.specialist-task-runner-rate-limit.v1",
|
|
821
|
+
"state_path": str(state_path),
|
|
822
|
+
"delay_seconds": delay_seconds,
|
|
823
|
+
"waited_seconds": round(waited_seconds, 3),
|
|
824
|
+
"previous_started_at_epoch": previous_started_at,
|
|
825
|
+
"started_at_epoch": started_at,
|
|
826
|
+
}
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _write_transcript(
|
|
831
|
+
*,
|
|
832
|
+
path: Path,
|
|
833
|
+
work_id: str,
|
|
834
|
+
command: list[str],
|
|
835
|
+
prompt_sha256: str,
|
|
836
|
+
completed: subprocess.CompletedProcess[str],
|
|
837
|
+
) -> str:
|
|
838
|
+
stdout, stdout_truncated = _bounded_text(completed.stdout or "")
|
|
839
|
+
stderr, stderr_truncated = _bounded_text(completed.stderr or "")
|
|
840
|
+
redacted_command = _redact_prompt_argument(command)
|
|
841
|
+
transcript = JsonObjectAdapter.validate_python(
|
|
842
|
+
{
|
|
843
|
+
"schema": SPECIALIST_TASK_RUNNER_TRANSCRIPT_SCHEMA,
|
|
844
|
+
"work_id": work_id,
|
|
845
|
+
"harness": "gemini_cli",
|
|
846
|
+
"adapter": GEMINI_CLI_SPECIALIST_ADAPTER,
|
|
847
|
+
"command": redacted_command,
|
|
848
|
+
"prompt_sha256": prompt_sha256,
|
|
849
|
+
"returncode": completed.returncode,
|
|
850
|
+
"stdout": stdout,
|
|
851
|
+
"stdout_truncated": stdout_truncated,
|
|
852
|
+
"stderr": stderr,
|
|
853
|
+
"stderr_truncated": stderr_truncated,
|
|
854
|
+
}
|
|
855
|
+
)
|
|
856
|
+
_write_json(path, transcript)
|
|
857
|
+
return _sha256_bytes(path.read_bytes())
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def _transcript_path_for_attempt(base_path: Path, *, attempt: int, final: bool) -> Path:
|
|
861
|
+
if final or attempt <= 0:
|
|
862
|
+
return base_path
|
|
863
|
+
return base_path.with_name(f"{base_path.stem}.attempt-{attempt}{base_path.suffix}")
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _retryable_gemini_stream_failure(*, stdout: str, stderr: str) -> bool:
|
|
867
|
+
combined = f"{stdout}\n{stderr}".lower()
|
|
868
|
+
return (
|
|
869
|
+
"invalid stream" in combined
|
|
870
|
+
or "empty response" in combined
|
|
871
|
+
or "malformed tool call" in combined
|
|
872
|
+
or "incomplete json" in combined
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def _attempt_record(
|
|
877
|
+
*,
|
|
878
|
+
attempt: int,
|
|
879
|
+
completed: subprocess.CompletedProcess[str],
|
|
880
|
+
transcript_path: Path,
|
|
881
|
+
transcript_sha256: str,
|
|
882
|
+
observed_model: str,
|
|
883
|
+
retry_reason: str = "",
|
|
884
|
+
) -> JsonObject:
|
|
885
|
+
return JsonObjectAdapter.validate_python(
|
|
886
|
+
{
|
|
887
|
+
"attempt": attempt,
|
|
888
|
+
"returncode": completed.returncode,
|
|
889
|
+
"observed_model": observed_model,
|
|
890
|
+
"transcript_artifact_path": str(transcript_path),
|
|
891
|
+
"transcript_sha256": transcript_sha256,
|
|
892
|
+
"retry_reason": retry_reason,
|
|
893
|
+
}
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def _completed_receipt(
|
|
898
|
+
*,
|
|
899
|
+
work_item: JsonObject,
|
|
900
|
+
model: str,
|
|
901
|
+
observed_model: str,
|
|
902
|
+
input_packet_path: Path,
|
|
903
|
+
input_packet_sha256: str,
|
|
904
|
+
output_path: Path,
|
|
905
|
+
transcript_artifact_path: Path,
|
|
906
|
+
transcript_artifact_sha256: str,
|
|
907
|
+
specialist_session_id: str,
|
|
908
|
+
harness: SpecialistHarness = SpecialistHarness.GEMINI_CLI,
|
|
909
|
+
adapter: str = GEMINI_CLI_SPECIALIST_ADAPTER,
|
|
910
|
+
model_evidence: JsonObject | None = None,
|
|
911
|
+
parent_session_id: str = "workbench-specialist-runner",
|
|
912
|
+
) -> JsonObject:
|
|
913
|
+
typed_work_item = _typed_subagent_work_item(work_item)
|
|
914
|
+
work_id = typed_work_item.work_id
|
|
915
|
+
output_sha256 = _sha256_bytes(output_path.read_bytes())
|
|
916
|
+
evidence = model_evidence or {
|
|
917
|
+
"source": "gemini_cli_agent_metadata",
|
|
918
|
+
"requested_model": model,
|
|
919
|
+
"observed_provider_id": "gemini-cli",
|
|
920
|
+
"observed_model_id": observed_model,
|
|
921
|
+
"evidence_strength": "runtime_metadata",
|
|
922
|
+
"evidence_excerpt": f"model: {observed_model}",
|
|
923
|
+
}
|
|
924
|
+
payload = JsonObjectAdapter.validate_python(
|
|
925
|
+
{
|
|
926
|
+
"schema": "medical-notes-workbench.specialist-task-run-receipt.v1",
|
|
927
|
+
"work_id": work_id,
|
|
928
|
+
"phase": "style_rewrite",
|
|
929
|
+
"harness": harness.value,
|
|
930
|
+
"adapter": adapter,
|
|
931
|
+
"requested_agent": typed_work_item.agent,
|
|
932
|
+
"requested_model_policy": _style_rewrite_model_policy(work_item),
|
|
933
|
+
"requested_model": model,
|
|
934
|
+
"observed_model": observed_model,
|
|
935
|
+
"model_evidence": evidence,
|
|
936
|
+
"input_packet_path": str(input_packet_path),
|
|
937
|
+
"input_packet_sha256": input_packet_sha256,
|
|
938
|
+
"output_path": str(output_path),
|
|
939
|
+
"output_sha256": output_sha256,
|
|
940
|
+
"status": "completed",
|
|
941
|
+
"validation_status": "validated",
|
|
942
|
+
"quality_review_status": "accepted",
|
|
943
|
+
"parent_session_id": parent_session_id,
|
|
944
|
+
"specialist_session_id": specialist_session_id or "gemini-cli-session",
|
|
945
|
+
"transcript_artifact_path": str(transcript_artifact_path),
|
|
946
|
+
"transcript_artifact_sha256": transcript_artifact_sha256,
|
|
947
|
+
"error_context": {},
|
|
948
|
+
"next_action": "",
|
|
949
|
+
"specialist_output_receipt": {
|
|
950
|
+
"schema": "medical-notes-workbench.style-rewrite-output.v1",
|
|
951
|
+
"work_id": work_id,
|
|
952
|
+
"phase": "style_rewrite",
|
|
953
|
+
"status": "completed",
|
|
954
|
+
"output_path": str(output_path),
|
|
955
|
+
"output_sha256": output_sha256,
|
|
956
|
+
},
|
|
957
|
+
"specialist_output_attestation": {
|
|
958
|
+
"schema": "medical-notes-workbench.style-rewrite-output-attestation.v1",
|
|
959
|
+
"work_id": work_id,
|
|
960
|
+
"phase": "style_rewrite",
|
|
961
|
+
"status": "completed",
|
|
962
|
+
"output_path": str(output_path),
|
|
963
|
+
"output_sha256": output_sha256,
|
|
964
|
+
},
|
|
965
|
+
}
|
|
966
|
+
)
|
|
967
|
+
attested = attach_specialist_task_run_receipt_attestation(payload)
|
|
968
|
+
try:
|
|
969
|
+
receipt = SpecialistTaskRunReceipt.from_operation_payload(attested)
|
|
970
|
+
except PydanticValidationError as exc:
|
|
971
|
+
raise contract_error(exc, prefix="specialist task run receipt invalid") from exc
|
|
972
|
+
return receipt.to_payload()
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def _receipt_path_for_work_item(work_item: JsonObject, output_path: Path) -> Path:
|
|
976
|
+
typed_work_item = _typed_subagent_work_item(work_item)
|
|
977
|
+
explicit = (typed_work_item.specialist_task_run_receipt_path or "").strip()
|
|
978
|
+
return Path(explicit) if explicit else output_path.with_suffix(".specialist-task-run-receipt.json")
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def _remove_existing_specialist_outputs(
|
|
982
|
+
*,
|
|
983
|
+
work_item: JsonObject,
|
|
984
|
+
output_path: Path,
|
|
985
|
+
receipt_path: Path,
|
|
986
|
+
) -> JsonObject:
|
|
987
|
+
candidates = [
|
|
988
|
+
output_path,
|
|
989
|
+
receipt_path,
|
|
990
|
+
_style_rewrite_output_receipt_path(work_item, output_path),
|
|
991
|
+
_style_rewrite_output_attestation_path(work_item, output_path),
|
|
992
|
+
]
|
|
993
|
+
removed: list[str] = []
|
|
994
|
+
seen: set[Path] = set()
|
|
995
|
+
for candidate in candidates:
|
|
996
|
+
if candidate in seen:
|
|
997
|
+
continue
|
|
998
|
+
seen.add(candidate)
|
|
999
|
+
if candidate.exists():
|
|
1000
|
+
candidate.unlink()
|
|
1001
|
+
removed.append(str(candidate))
|
|
1002
|
+
return {
|
|
1003
|
+
"preexisting_output_removed": bool(removed),
|
|
1004
|
+
"preexisting_output_removed_paths": removed,
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def _remove_untrusted_specialist_outputs(
|
|
1009
|
+
*,
|
|
1010
|
+
work_item: JsonObject,
|
|
1011
|
+
output_path: Path,
|
|
1012
|
+
receipt_path: Path,
|
|
1013
|
+
) -> JsonObject:
|
|
1014
|
+
cleanup = _remove_existing_specialist_outputs(
|
|
1015
|
+
work_item=work_item,
|
|
1016
|
+
output_path=output_path,
|
|
1017
|
+
receipt_path=receipt_path,
|
|
1018
|
+
)
|
|
1019
|
+
return {
|
|
1020
|
+
"untrusted_output_removed": bool(cleanup["preexisting_output_removed"]),
|
|
1021
|
+
"untrusted_output_removed_paths": cleanup["preexisting_output_removed_paths"],
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def _parsed_transcript_values(text: str) -> list[object]:
|
|
1026
|
+
values: list[object] = []
|
|
1027
|
+
stripped = text.strip()
|
|
1028
|
+
if stripped:
|
|
1029
|
+
try:
|
|
1030
|
+
values.append(json.loads(stripped))
|
|
1031
|
+
except json.JSONDecodeError:
|
|
1032
|
+
pass
|
|
1033
|
+
for line in text.splitlines():
|
|
1034
|
+
candidate = line.strip()
|
|
1035
|
+
if not candidate:
|
|
1036
|
+
continue
|
|
1037
|
+
try:
|
|
1038
|
+
values.append(json.loads(candidate))
|
|
1039
|
+
except json.JSONDecodeError:
|
|
1040
|
+
continue
|
|
1041
|
+
return values
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
def _find_list_field(value: object, field_names: set[str]) -> list[str]:
|
|
1045
|
+
if isinstance(value, dict):
|
|
1046
|
+
for key, raw in value.items():
|
|
1047
|
+
if key.lower() in field_names and isinstance(raw, list):
|
|
1048
|
+
return [str(item) for item in raw if isinstance(item, str) and item.strip()]
|
|
1049
|
+
for raw in value.values():
|
|
1050
|
+
found = _find_list_field(raw, field_names)
|
|
1051
|
+
if found:
|
|
1052
|
+
return found
|
|
1053
|
+
if isinstance(value, list):
|
|
1054
|
+
for item in value:
|
|
1055
|
+
found = _find_list_field(item, field_names)
|
|
1056
|
+
if found:
|
|
1057
|
+
return found
|
|
1058
|
+
return []
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def _agy_transcript_metadata(*, transcript_text: str, requested_model: str) -> JsonObject:
|
|
1062
|
+
parsed_values = _parsed_transcript_values(transcript_text)
|
|
1063
|
+
observed_model = ""
|
|
1064
|
+
provider = ""
|
|
1065
|
+
specialist_session_id = ""
|
|
1066
|
+
tool_sequence: list[str] = []
|
|
1067
|
+
for value in parsed_values:
|
|
1068
|
+
if not observed_model:
|
|
1069
|
+
observed_model = _find_string_field(
|
|
1070
|
+
value,
|
|
1071
|
+
{"model", "model_id", "modelid", "modelname", "observed_model_id", "selected_model"},
|
|
1072
|
+
)
|
|
1073
|
+
if not provider:
|
|
1074
|
+
provider = _find_string_field(value, {"provider", "provider_id", "observed_provider_id", "runtime"})
|
|
1075
|
+
if not specialist_session_id:
|
|
1076
|
+
specialist_session_id = _find_string_field(
|
|
1077
|
+
value,
|
|
1078
|
+
{"session_id", "sessionid", "conversation_id", "conversationid", "task_id", "taskid"},
|
|
1079
|
+
)
|
|
1080
|
+
if not tool_sequence:
|
|
1081
|
+
tool_sequence = _find_list_field(value, {"tool_sequence", "toolsequence", "tools"})
|
|
1082
|
+
if not observed_model and requested_model and requested_model in transcript_text:
|
|
1083
|
+
observed_model = requested_model
|
|
1084
|
+
if not provider and "antigravity" in transcript_text.casefold():
|
|
1085
|
+
provider = "antigravity-cli"
|
|
1086
|
+
return JsonObjectAdapter.validate_python(
|
|
1087
|
+
{
|
|
1088
|
+
"observed_model": observed_model,
|
|
1089
|
+
"provider": provider or "antigravity-cli",
|
|
1090
|
+
"specialist_session_id": specialist_session_id,
|
|
1091
|
+
"tool_sequence": tool_sequence,
|
|
1092
|
+
}
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def _agy_selected_model_from_runtime_log(runtime_log_text: str) -> str:
|
|
1097
|
+
labels = [match.group("label").strip() for match in AGY_SELECTED_MODEL_OVERRIDE_RE.finditer(runtime_log_text)]
|
|
1098
|
+
return labels[-1] if labels else ""
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def _agy_transcript_has_native_specialist_invocation(*, transcript_text: str, metadata: JsonObject) -> bool:
|
|
1102
|
+
tool_sequence = metadata.get("tool_sequence")
|
|
1103
|
+
if isinstance(tool_sequence, list):
|
|
1104
|
+
names = {str(item).strip().lower() for item in tool_sequence}
|
|
1105
|
+
if {"define_subagent", "invoke_subagent"}.issubset(names):
|
|
1106
|
+
return True
|
|
1107
|
+
folded = transcript_text.casefold()
|
|
1108
|
+
return "define_subagent" in folded and "invoke_subagent" in folded
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def _write_agy_transcript_artifact(
|
|
1112
|
+
*,
|
|
1113
|
+
path: Path,
|
|
1114
|
+
work_id: str,
|
|
1115
|
+
source_transcript_path: Path,
|
|
1116
|
+
source_transcript_sha256: str,
|
|
1117
|
+
metadata: JsonObject,
|
|
1118
|
+
model_evidence: JsonObject,
|
|
1119
|
+
) -> str:
|
|
1120
|
+
artifact = JsonObjectAdapter.validate_python(
|
|
1121
|
+
{
|
|
1122
|
+
"schema": AGY_SPECIALIST_TRANSCRIPT_ARTIFACT_SCHEMA,
|
|
1123
|
+
"work_id": work_id,
|
|
1124
|
+
"harness": "agy",
|
|
1125
|
+
"adapter": AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1126
|
+
"source_transcript_path": str(source_transcript_path),
|
|
1127
|
+
"source_transcript_sha256": source_transcript_sha256,
|
|
1128
|
+
"observed_model": str(metadata.get("observed_model") or ""),
|
|
1129
|
+
"observed_provider_id": str(metadata.get("provider") or "antigravity-cli"),
|
|
1130
|
+
"specialist_session_id": str(metadata.get("specialist_session_id") or ""),
|
|
1131
|
+
"tool_sequence": metadata.get("tool_sequence") if isinstance(metadata.get("tool_sequence"), list) else [],
|
|
1132
|
+
"model_evidence": model_evidence,
|
|
1133
|
+
"raw_transcript_embedded": False,
|
|
1134
|
+
}
|
|
1135
|
+
)
|
|
1136
|
+
_write_json(path, artifact)
|
|
1137
|
+
return _sha256_bytes(path.read_bytes())
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
def _write_opencode_task_artifact(
|
|
1141
|
+
*,
|
|
1142
|
+
path: Path,
|
|
1143
|
+
metadata_path: Path,
|
|
1144
|
+
metadata_sha256: str,
|
|
1145
|
+
metadata: OpenCodeSpecialistTaskMetadata,
|
|
1146
|
+
model_evidence: JsonObject,
|
|
1147
|
+
) -> str:
|
|
1148
|
+
artifact = JsonObjectAdapter.validate_python(
|
|
1149
|
+
{
|
|
1150
|
+
"schema": OPENCODE_SPECIALIST_TASK_ARTIFACT_SCHEMA,
|
|
1151
|
+
"work_id": metadata.work_id,
|
|
1152
|
+
"harness": "opencode",
|
|
1153
|
+
"adapter": OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1154
|
+
"source_metadata_path": str(metadata_path),
|
|
1155
|
+
"source_metadata_sha256": metadata_sha256,
|
|
1156
|
+
"task_id": metadata.task_id,
|
|
1157
|
+
"parent_session_id": metadata.parent_session_id,
|
|
1158
|
+
"specialist_session_id": metadata.specialist_session_id,
|
|
1159
|
+
"observed_provider_id": metadata.provider_id,
|
|
1160
|
+
"observed_model": metadata.model_id,
|
|
1161
|
+
"model_tier": metadata.model_tier,
|
|
1162
|
+
"tool_sequence": metadata.tool_sequence,
|
|
1163
|
+
"prompt_contract": metadata.prompt_contract,
|
|
1164
|
+
"raw_content_embedded": metadata.raw_content_embedded,
|
|
1165
|
+
"model_evidence": model_evidence,
|
|
1166
|
+
}
|
|
1167
|
+
)
|
|
1168
|
+
_write_json(path, artifact)
|
|
1169
|
+
return _sha256_bytes(path.read_bytes())
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
def _opencode_model_has_forbidden_specialist_token(model_id: str) -> bool:
|
|
1173
|
+
tokens = re.findall(r"[a-z0-9]+", model_id.lower())
|
|
1174
|
+
return any(token in {"flash", "lite", "nano"} for token in tokens)
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
def _opencode_plan_work_item(raw_work_item: JsonObject) -> SubagentWorkItem:
|
|
1178
|
+
try:
|
|
1179
|
+
return SubagentWorkItem.model_validate(raw_work_item)
|
|
1180
|
+
except PydanticValidationError as exc:
|
|
1181
|
+
raise contract_error(exc, prefix="style rewrite work item invalid") from exc
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
def finalize_agy_specialist_task(
|
|
1185
|
+
*,
|
|
1186
|
+
plan_path: Path,
|
|
1187
|
+
work_id: str,
|
|
1188
|
+
transcript_path: Path,
|
|
1189
|
+
runtime_log_path: Path | None = None,
|
|
1190
|
+
requested_model: str = "Gemini 3.1 Pro (High)",
|
|
1191
|
+
) -> JsonObject:
|
|
1192
|
+
try:
|
|
1193
|
+
plan_payload = _read_json_object(plan_path, label="style rewrite plan")
|
|
1194
|
+
_validate_style_rewrite_plan(plan_payload)
|
|
1195
|
+
_verify_style_rewrite_plan_attestation(plan_payload)
|
|
1196
|
+
except (MissingPathError, ValidationError) as exc:
|
|
1197
|
+
return _result(
|
|
1198
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1199
|
+
blocked_reason="style_rewrite_plan_contract_invalid",
|
|
1200
|
+
next_action="Regere o plano pela rota oficial plan-subagents antes de finalizar o especialista AGY.",
|
|
1201
|
+
required_inputs=["plan"],
|
|
1202
|
+
work_id=work_id,
|
|
1203
|
+
harness=SpecialistHarness.AGY,
|
|
1204
|
+
requested_model=requested_model,
|
|
1205
|
+
plan_path=plan_path,
|
|
1206
|
+
adapter=AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1207
|
+
validation={"error": str(exc)},
|
|
1208
|
+
)
|
|
1209
|
+
work_item = _style_rewrite_work_item(plan_payload, work_id)
|
|
1210
|
+
if work_item is None:
|
|
1211
|
+
return _result(
|
|
1212
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1213
|
+
blocked_reason="style_rewrite_plan_contract_invalid",
|
|
1214
|
+
next_action="Regere o plano; o work_id solicitado não existe no plano oficial.",
|
|
1215
|
+
required_inputs=["work_id"],
|
|
1216
|
+
work_id=work_id,
|
|
1217
|
+
harness=SpecialistHarness.AGY,
|
|
1218
|
+
requested_model=requested_model,
|
|
1219
|
+
plan_path=plan_path,
|
|
1220
|
+
adapter=AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1221
|
+
)
|
|
1222
|
+
target_path = Path(str(work_item.get("target_path") or ""))
|
|
1223
|
+
output_path = Path(str(work_item.get("temp_output") or work_item.get("output_path") or ""))
|
|
1224
|
+
receipt_path = _receipt_path_for_work_item(work_item, output_path)
|
|
1225
|
+
if not target_path.exists():
|
|
1226
|
+
return _result(
|
|
1227
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1228
|
+
blocked_reason="style_rewrite_target_missing",
|
|
1229
|
+
next_action="Replaneje style-rewrite; a nota alvo não existe mais.",
|
|
1230
|
+
required_inputs=["target_path"],
|
|
1231
|
+
work_id=work_id,
|
|
1232
|
+
harness=SpecialistHarness.AGY,
|
|
1233
|
+
requested_model=requested_model,
|
|
1234
|
+
plan_path=plan_path,
|
|
1235
|
+
target_path=target_path,
|
|
1236
|
+
output_path=output_path,
|
|
1237
|
+
receipt_path=receipt_path,
|
|
1238
|
+
adapter=AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1239
|
+
)
|
|
1240
|
+
if _sha256_bytes(target_path.read_bytes()) != str(work_item.get("target_hash_before") or ""):
|
|
1241
|
+
return _result(
|
|
1242
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1243
|
+
blocked_reason="style_rewrite_stale_target_hash",
|
|
1244
|
+
next_action="Replaneje style-rewrite; a nota alvo mudou desde o plano.",
|
|
1245
|
+
required_inputs=["plan"],
|
|
1246
|
+
work_id=work_id,
|
|
1247
|
+
harness=SpecialistHarness.AGY,
|
|
1248
|
+
requested_model=requested_model,
|
|
1249
|
+
plan_path=plan_path,
|
|
1250
|
+
target_path=target_path,
|
|
1251
|
+
output_path=output_path,
|
|
1252
|
+
receipt_path=receipt_path,
|
|
1253
|
+
adapter=AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1254
|
+
)
|
|
1255
|
+
if not output_path.exists():
|
|
1256
|
+
return _result(
|
|
1257
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1258
|
+
blocked_reason="style_rewrite_output_missing",
|
|
1259
|
+
next_action="Relance invoke_subagent no AGY para este work_item; o temp_output oficial não existe.",
|
|
1260
|
+
required_inputs=["temp_output"],
|
|
1261
|
+
work_id=work_id,
|
|
1262
|
+
harness=SpecialistHarness.AGY,
|
|
1263
|
+
requested_model=requested_model,
|
|
1264
|
+
plan_path=plan_path,
|
|
1265
|
+
target_path=target_path,
|
|
1266
|
+
output_path=output_path,
|
|
1267
|
+
receipt_path=receipt_path,
|
|
1268
|
+
adapter=AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1269
|
+
)
|
|
1270
|
+
if not transcript_path.exists():
|
|
1271
|
+
return _result(
|
|
1272
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1273
|
+
blocked_reason="agy_transcript_evidence_missing",
|
|
1274
|
+
next_action="Forneça o transcript/task log AGY oficial para finalizar o recibo do especialista.",
|
|
1275
|
+
required_inputs=["agy_transcript"],
|
|
1276
|
+
work_id=work_id,
|
|
1277
|
+
harness=SpecialistHarness.AGY,
|
|
1278
|
+
requested_model=requested_model,
|
|
1279
|
+
plan_path=plan_path,
|
|
1280
|
+
target_path=target_path,
|
|
1281
|
+
output_path=output_path,
|
|
1282
|
+
receipt_path=receipt_path,
|
|
1283
|
+
adapter=AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1284
|
+
)
|
|
1285
|
+
transcript_bytes = transcript_path.read_bytes()
|
|
1286
|
+
transcript_text = transcript_bytes.decode("utf-8", errors="replace")
|
|
1287
|
+
source_transcript_sha256 = _sha256_bytes(transcript_bytes)
|
|
1288
|
+
metadata = _agy_transcript_metadata(transcript_text=transcript_text, requested_model=requested_model)
|
|
1289
|
+
observed_model = str(metadata.get("observed_model") or "")
|
|
1290
|
+
model_evidence_source = "agy_transcript_metadata"
|
|
1291
|
+
model_evidence_excerpt = f"model: {observed_model}" if observed_model else ""
|
|
1292
|
+
if runtime_log_path is not None:
|
|
1293
|
+
try:
|
|
1294
|
+
runtime_log_text = runtime_log_path.read_text(encoding="utf-8", errors="replace")
|
|
1295
|
+
except OSError as exc:
|
|
1296
|
+
return _result(
|
|
1297
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1298
|
+
blocked_reason="agy_runtime_log_unavailable",
|
|
1299
|
+
next_action="Forneça o log AGY oficial para validar a troca de modelo da sessão especialista.",
|
|
1300
|
+
required_inputs=["agy_runtime_log"],
|
|
1301
|
+
work_id=work_id,
|
|
1302
|
+
harness=SpecialistHarness.AGY,
|
|
1303
|
+
requested_model=requested_model,
|
|
1304
|
+
plan_path=plan_path,
|
|
1305
|
+
target_path=target_path,
|
|
1306
|
+
output_path=output_path,
|
|
1307
|
+
receipt_path=receipt_path,
|
|
1308
|
+
adapter=AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1309
|
+
validation={"error": str(exc), "source_transcript_sha256": source_transcript_sha256},
|
|
1310
|
+
)
|
|
1311
|
+
selected_model = _agy_selected_model_from_runtime_log(runtime_log_text)
|
|
1312
|
+
if selected_model and selected_model != requested_model:
|
|
1313
|
+
return _result(
|
|
1314
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1315
|
+
blocked_reason="agy_specialist_model_evidence_mismatch",
|
|
1316
|
+
next_action=(
|
|
1317
|
+
"Repita a janela AGY settings switch com modelo especialista Pro/High; "
|
|
1318
|
+
"o log runtime não comprova o modelo solicitado."
|
|
1319
|
+
),
|
|
1320
|
+
required_inputs=["agy_model_evidence"],
|
|
1321
|
+
work_id=work_id,
|
|
1322
|
+
harness=SpecialistHarness.AGY,
|
|
1323
|
+
requested_model=requested_model,
|
|
1324
|
+
observed_model=selected_model,
|
|
1325
|
+
plan_path=plan_path,
|
|
1326
|
+
target_path=target_path,
|
|
1327
|
+
output_path=output_path,
|
|
1328
|
+
receipt_path=receipt_path,
|
|
1329
|
+
adapter=AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1330
|
+
validation={
|
|
1331
|
+
"source_transcript_sha256": source_transcript_sha256,
|
|
1332
|
+
"runtime_log_path": str(runtime_log_path),
|
|
1333
|
+
"observed_selected_model": selected_model,
|
|
1334
|
+
},
|
|
1335
|
+
)
|
|
1336
|
+
if selected_model:
|
|
1337
|
+
if not _agy_transcript_has_native_specialist_invocation(
|
|
1338
|
+
transcript_text=transcript_text,
|
|
1339
|
+
metadata=metadata,
|
|
1340
|
+
):
|
|
1341
|
+
return _result(
|
|
1342
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1343
|
+
blocked_reason="agy_specialist_invocation_evidence_missing",
|
|
1344
|
+
next_action=(
|
|
1345
|
+
"Forneça transcript/task log AGY com define_subagent e invoke_subagent oficiais "
|
|
1346
|
+
"para vincular a troca de modelo ao output especialista."
|
|
1347
|
+
),
|
|
1348
|
+
required_inputs=["agy_transcript"],
|
|
1349
|
+
work_id=work_id,
|
|
1350
|
+
harness=SpecialistHarness.AGY,
|
|
1351
|
+
requested_model=requested_model,
|
|
1352
|
+
observed_model=selected_model,
|
|
1353
|
+
plan_path=plan_path,
|
|
1354
|
+
target_path=target_path,
|
|
1355
|
+
output_path=output_path,
|
|
1356
|
+
receipt_path=receipt_path,
|
|
1357
|
+
adapter=AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1358
|
+
validation={
|
|
1359
|
+
"source_transcript_sha256": source_transcript_sha256,
|
|
1360
|
+
"runtime_log_path": str(runtime_log_path),
|
|
1361
|
+
"observed_selected_model": selected_model,
|
|
1362
|
+
},
|
|
1363
|
+
)
|
|
1364
|
+
observed_model = selected_model
|
|
1365
|
+
model_evidence_source = "agy_settings_snapshot"
|
|
1366
|
+
model_evidence_excerpt = f'selected_model_override: "{selected_model}"'
|
|
1367
|
+
if not observed_model:
|
|
1368
|
+
return _result(
|
|
1369
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1370
|
+
blocked_reason="agy_specialist_model_evidence_missing",
|
|
1371
|
+
next_action=(
|
|
1372
|
+
"Repita a chamada AGY garantindo transcript/task log com metadado do modelo efetivo; "
|
|
1373
|
+
"não finalize autoria médica por alegação manual de modelo."
|
|
1374
|
+
),
|
|
1375
|
+
required_inputs=["agy_model_evidence"],
|
|
1376
|
+
work_id=work_id,
|
|
1377
|
+
harness=SpecialistHarness.AGY,
|
|
1378
|
+
requested_model=requested_model,
|
|
1379
|
+
plan_path=plan_path,
|
|
1380
|
+
target_path=target_path,
|
|
1381
|
+
output_path=output_path,
|
|
1382
|
+
receipt_path=receipt_path,
|
|
1383
|
+
adapter=AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1384
|
+
validation={"source_transcript_sha256": source_transcript_sha256},
|
|
1385
|
+
)
|
|
1386
|
+
input_packet_path = output_path.with_suffix(".agy-input.json")
|
|
1387
|
+
transcript_artifact_path = output_path.with_suffix(".agy-transcript.json")
|
|
1388
|
+
input_packet_sha256 = _write_input_packet(
|
|
1389
|
+
path=input_packet_path,
|
|
1390
|
+
work_item=work_item,
|
|
1391
|
+
plan_path=plan_path,
|
|
1392
|
+
model=requested_model,
|
|
1393
|
+
)
|
|
1394
|
+
deterministic_fixes = _normalize_style_rewrite_output_file(target_path=target_path, output_path=output_path)
|
|
1395
|
+
validation = apply_style_rewrite(target_path, output_path, dry_run=True)
|
|
1396
|
+
validation["deterministic_fixes_applied"] = deterministic_fixes
|
|
1397
|
+
validation_block = validation.get("validation") if isinstance(validation, dict) else {}
|
|
1398
|
+
if isinstance(validation_block, dict) and validation_block.get("errors"):
|
|
1399
|
+
return _result(
|
|
1400
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1401
|
+
blocked_reason="style_rewrite_agent_contract_violation",
|
|
1402
|
+
next_action="Regenerar o rewrite pelo subagente AGY empacotado para este work_item.",
|
|
1403
|
+
required_inputs=["specialist_output"],
|
|
1404
|
+
work_id=work_id,
|
|
1405
|
+
harness=SpecialistHarness.AGY,
|
|
1406
|
+
requested_model=requested_model,
|
|
1407
|
+
observed_model=observed_model,
|
|
1408
|
+
plan_path=plan_path,
|
|
1409
|
+
target_path=target_path,
|
|
1410
|
+
output_path=output_path,
|
|
1411
|
+
receipt_path=receipt_path,
|
|
1412
|
+
input_packet_path=input_packet_path,
|
|
1413
|
+
adapter=AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1414
|
+
validation=validation,
|
|
1415
|
+
)
|
|
1416
|
+
if isinstance(validation_block, dict) and validation_block.get("requires_llm_rewrite"):
|
|
1417
|
+
return _result(
|
|
1418
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1419
|
+
blocked_reason="style_rewrite_still_requires_rewrite",
|
|
1420
|
+
next_action="Regenerar o rewrite no AGY até requires_llm_rewrite=false antes de assinar o recibo.",
|
|
1421
|
+
required_inputs=["specialist_output"],
|
|
1422
|
+
work_id=work_id,
|
|
1423
|
+
harness=SpecialistHarness.AGY,
|
|
1424
|
+
requested_model=requested_model,
|
|
1425
|
+
observed_model=observed_model,
|
|
1426
|
+
plan_path=plan_path,
|
|
1427
|
+
target_path=target_path,
|
|
1428
|
+
output_path=output_path,
|
|
1429
|
+
receipt_path=receipt_path,
|
|
1430
|
+
input_packet_path=input_packet_path,
|
|
1431
|
+
adapter=AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1432
|
+
validation=validation,
|
|
1433
|
+
)
|
|
1434
|
+
model_evidence = JsonObjectAdapter.validate_python(
|
|
1435
|
+
{
|
|
1436
|
+
"source": model_evidence_source,
|
|
1437
|
+
"requested_model": requested_model,
|
|
1438
|
+
"observed_provider_id": str(metadata.get("provider") or "antigravity-cli"),
|
|
1439
|
+
"observed_model_id": observed_model,
|
|
1440
|
+
"evidence_strength": "settings_and_transcript",
|
|
1441
|
+
"evidence_excerpt": model_evidence_excerpt,
|
|
1442
|
+
}
|
|
1443
|
+
)
|
|
1444
|
+
transcript_artifact_sha256 = _write_agy_transcript_artifact(
|
|
1445
|
+
path=transcript_artifact_path,
|
|
1446
|
+
work_id=work_id,
|
|
1447
|
+
source_transcript_path=transcript_path,
|
|
1448
|
+
source_transcript_sha256=source_transcript_sha256,
|
|
1449
|
+
metadata=metadata,
|
|
1450
|
+
model_evidence=model_evidence,
|
|
1451
|
+
)
|
|
1452
|
+
try:
|
|
1453
|
+
receipt = _completed_receipt(
|
|
1454
|
+
work_item=work_item,
|
|
1455
|
+
model=requested_model,
|
|
1456
|
+
observed_model=observed_model,
|
|
1457
|
+
input_packet_path=input_packet_path,
|
|
1458
|
+
input_packet_sha256=input_packet_sha256,
|
|
1459
|
+
output_path=output_path,
|
|
1460
|
+
transcript_artifact_path=transcript_artifact_path,
|
|
1461
|
+
transcript_artifact_sha256=transcript_artifact_sha256,
|
|
1462
|
+
specialist_session_id=str(metadata.get("specialist_session_id") or f"agy-{source_transcript_sha256[7:19]}"),
|
|
1463
|
+
harness=SpecialistHarness.AGY,
|
|
1464
|
+
adapter=AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1465
|
+
model_evidence=model_evidence,
|
|
1466
|
+
parent_session_id="agy-parent-session",
|
|
1467
|
+
)
|
|
1468
|
+
except (MissingPathError, ValidationError, PydanticValidationError) as exc:
|
|
1469
|
+
return _result(
|
|
1470
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1471
|
+
blocked_reason="specialist_task_run_receipt_invalid",
|
|
1472
|
+
next_action="Corrija a evidência AGY/modelo e finalize novamente pela rota oficial.",
|
|
1473
|
+
required_inputs=["specialist_task_run_receipt"],
|
|
1474
|
+
work_id=work_id,
|
|
1475
|
+
harness=SpecialistHarness.AGY,
|
|
1476
|
+
requested_model=requested_model,
|
|
1477
|
+
observed_model=observed_model,
|
|
1478
|
+
plan_path=plan_path,
|
|
1479
|
+
target_path=target_path,
|
|
1480
|
+
output_path=output_path,
|
|
1481
|
+
receipt_path=receipt_path,
|
|
1482
|
+
input_packet_path=input_packet_path,
|
|
1483
|
+
transcript_artifact_path=transcript_artifact_path,
|
|
1484
|
+
adapter=AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1485
|
+
validation={"error": str(exc), **validation},
|
|
1486
|
+
)
|
|
1487
|
+
_write_json(receipt_path, receipt)
|
|
1488
|
+
return _result(
|
|
1489
|
+
status=SpecialistRunStatus.COMPLETED,
|
|
1490
|
+
work_id=work_id,
|
|
1491
|
+
harness=SpecialistHarness.AGY,
|
|
1492
|
+
requested_model=requested_model,
|
|
1493
|
+
observed_model=observed_model,
|
|
1494
|
+
plan_path=plan_path,
|
|
1495
|
+
target_path=target_path,
|
|
1496
|
+
output_path=output_path,
|
|
1497
|
+
output_sha256=_sha256_bytes(output_path.read_bytes()),
|
|
1498
|
+
receipt_path=receipt_path,
|
|
1499
|
+
input_packet_path=input_packet_path,
|
|
1500
|
+
transcript_artifact_path=transcript_artifact_path,
|
|
1501
|
+
adapter=AGY_PACKAGED_TEMPLATE_SPECIALIST_ADAPTER,
|
|
1502
|
+
validation={
|
|
1503
|
+
"source_transcript_sha256": source_transcript_sha256,
|
|
1504
|
+
"transcript_artifact_sha256": transcript_artifact_sha256,
|
|
1505
|
+
**validation,
|
|
1506
|
+
},
|
|
1507
|
+
)
|
|
1508
|
+
|
|
1509
|
+
|
|
1510
|
+
def finalize_opencode_specialist_task(
|
|
1511
|
+
*,
|
|
1512
|
+
plan_path: Path,
|
|
1513
|
+
work_id: str,
|
|
1514
|
+
task_metadata_path: Path | None,
|
|
1515
|
+
requested_model: str = "antigravity/gemini-3.1-pro",
|
|
1516
|
+
) -> JsonObject:
|
|
1517
|
+
try:
|
|
1518
|
+
plan_payload = _read_json_object(plan_path, label="style rewrite plan")
|
|
1519
|
+
_validate_style_rewrite_plan(plan_payload)
|
|
1520
|
+
_verify_style_rewrite_plan_attestation(plan_payload)
|
|
1521
|
+
except (MissingPathError, ValidationError) as exc:
|
|
1522
|
+
return _result(
|
|
1523
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1524
|
+
blocked_reason="style_rewrite_plan_contract_invalid",
|
|
1525
|
+
next_action="Regere o plano pela rota oficial plan-subagents antes de finalizar o especialista OpenCode.",
|
|
1526
|
+
required_inputs=["plan"],
|
|
1527
|
+
work_id=work_id,
|
|
1528
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1529
|
+
requested_model=requested_model,
|
|
1530
|
+
plan_path=plan_path,
|
|
1531
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1532
|
+
validation={"error": str(exc)},
|
|
1533
|
+
)
|
|
1534
|
+
raw_work_item = _style_rewrite_work_item(plan_payload, work_id)
|
|
1535
|
+
if raw_work_item is None:
|
|
1536
|
+
return _result(
|
|
1537
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1538
|
+
blocked_reason="style_rewrite_plan_contract_invalid",
|
|
1539
|
+
next_action="Regere o plano; o work_id solicitado não existe no plano oficial.",
|
|
1540
|
+
required_inputs=["work_id"],
|
|
1541
|
+
work_id=work_id,
|
|
1542
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1543
|
+
requested_model=requested_model,
|
|
1544
|
+
plan_path=plan_path,
|
|
1545
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1546
|
+
)
|
|
1547
|
+
try:
|
|
1548
|
+
work_item = _opencode_plan_work_item(raw_work_item)
|
|
1549
|
+
except ValidationError as exc:
|
|
1550
|
+
return _result(
|
|
1551
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1552
|
+
blocked_reason="style_rewrite_plan_contract_invalid",
|
|
1553
|
+
next_action="Regere o plano; o work_item de style-rewrite não passa no contrato tipado.",
|
|
1554
|
+
required_inputs=["plan"],
|
|
1555
|
+
work_id=work_id,
|
|
1556
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1557
|
+
requested_model=requested_model,
|
|
1558
|
+
plan_path=plan_path,
|
|
1559
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1560
|
+
validation={"error": str(exc)},
|
|
1561
|
+
)
|
|
1562
|
+
target_path = Path(work_item.target_path or "")
|
|
1563
|
+
output_path = Path(work_item.temp_output or work_item.output_path or "")
|
|
1564
|
+
receipt_path = Path(work_item.specialist_task_run_receipt_path or "") if work_item.specialist_task_run_receipt_path else _receipt_path_for_work_item(raw_work_item, output_path)
|
|
1565
|
+
if not target_path.exists():
|
|
1566
|
+
return _result(
|
|
1567
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1568
|
+
blocked_reason="style_rewrite_target_missing",
|
|
1569
|
+
next_action="Replaneje style-rewrite; a nota alvo não existe mais.",
|
|
1570
|
+
required_inputs=["target_path"],
|
|
1571
|
+
work_id=work_id,
|
|
1572
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1573
|
+
requested_model=requested_model,
|
|
1574
|
+
plan_path=plan_path,
|
|
1575
|
+
target_path=target_path,
|
|
1576
|
+
output_path=output_path,
|
|
1577
|
+
receipt_path=receipt_path,
|
|
1578
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1579
|
+
)
|
|
1580
|
+
if _sha256_bytes(target_path.read_bytes()) != (work_item.target_hash_before or ""):
|
|
1581
|
+
return _result(
|
|
1582
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1583
|
+
blocked_reason="style_rewrite_stale_target_hash",
|
|
1584
|
+
next_action="Replaneje style-rewrite; a nota alvo mudou desde o plano.",
|
|
1585
|
+
required_inputs=["plan"],
|
|
1586
|
+
work_id=work_id,
|
|
1587
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1588
|
+
requested_model=requested_model,
|
|
1589
|
+
plan_path=plan_path,
|
|
1590
|
+
target_path=target_path,
|
|
1591
|
+
output_path=output_path,
|
|
1592
|
+
receipt_path=receipt_path,
|
|
1593
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1594
|
+
)
|
|
1595
|
+
if not output_path.exists():
|
|
1596
|
+
return _result(
|
|
1597
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1598
|
+
blocked_reason="style_rewrite_output_missing",
|
|
1599
|
+
next_action="Relance a task OpenCode para este work_item; o temp_output oficial não existe.",
|
|
1600
|
+
required_inputs=["temp_output"],
|
|
1601
|
+
work_id=work_id,
|
|
1602
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1603
|
+
requested_model=requested_model,
|
|
1604
|
+
plan_path=plan_path,
|
|
1605
|
+
target_path=target_path,
|
|
1606
|
+
output_path=output_path,
|
|
1607
|
+
receipt_path=receipt_path,
|
|
1608
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1609
|
+
)
|
|
1610
|
+
resolved_task_metadata_path = task_metadata_path or _default_opencode_task_metadata_path(work_id)
|
|
1611
|
+
if not resolved_task_metadata_path.exists():
|
|
1612
|
+
return _result(
|
|
1613
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1614
|
+
blocked_reason="opencode_specialist_task_metadata_missing",
|
|
1615
|
+
next_action="Forneça o metadata oficial da task OpenCode para finalizar o recibo do especialista.",
|
|
1616
|
+
required_inputs=["opencode_task_metadata"],
|
|
1617
|
+
work_id=work_id,
|
|
1618
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1619
|
+
requested_model=requested_model,
|
|
1620
|
+
plan_path=plan_path,
|
|
1621
|
+
target_path=target_path,
|
|
1622
|
+
output_path=output_path,
|
|
1623
|
+
receipt_path=receipt_path,
|
|
1624
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1625
|
+
)
|
|
1626
|
+
try:
|
|
1627
|
+
metadata_payload = _read_json_object(resolved_task_metadata_path, label="OpenCode task metadata")
|
|
1628
|
+
except (MissingPathError, ValidationError) as exc:
|
|
1629
|
+
return _result(
|
|
1630
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1631
|
+
blocked_reason="opencode_specialist_task_metadata_invalid",
|
|
1632
|
+
next_action="Forneça metadata JSON oficial da task OpenCode.",
|
|
1633
|
+
required_inputs=["opencode_task_metadata"],
|
|
1634
|
+
work_id=work_id,
|
|
1635
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1636
|
+
requested_model=requested_model,
|
|
1637
|
+
plan_path=plan_path,
|
|
1638
|
+
target_path=target_path,
|
|
1639
|
+
output_path=output_path,
|
|
1640
|
+
receipt_path=receipt_path,
|
|
1641
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1642
|
+
validation={"error": str(exc)},
|
|
1643
|
+
)
|
|
1644
|
+
if not str(metadata_payload.get("model_id") or "").strip():
|
|
1645
|
+
return _result(
|
|
1646
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1647
|
+
blocked_reason="opencode_specialist_model_evidence_missing",
|
|
1648
|
+
next_action="Repita a task OpenCode com metadata que exponha provider_id/model_id efetivos.",
|
|
1649
|
+
required_inputs=["opencode_task_metadata"],
|
|
1650
|
+
work_id=work_id,
|
|
1651
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1652
|
+
requested_model=requested_model,
|
|
1653
|
+
plan_path=plan_path,
|
|
1654
|
+
target_path=target_path,
|
|
1655
|
+
output_path=output_path,
|
|
1656
|
+
receipt_path=receipt_path,
|
|
1657
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1658
|
+
)
|
|
1659
|
+
try:
|
|
1660
|
+
metadata = OpenCodeSpecialistTaskMetadata.model_validate(metadata_payload)
|
|
1661
|
+
except PydanticValidationError as exc:
|
|
1662
|
+
return _result(
|
|
1663
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1664
|
+
blocked_reason="opencode_specialist_task_metadata_invalid",
|
|
1665
|
+
next_action="Forneça metadata OpenCode que satisfaça o contrato opencode-specialist-task-metadata.v1.",
|
|
1666
|
+
required_inputs=["opencode_task_metadata"],
|
|
1667
|
+
work_id=work_id,
|
|
1668
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1669
|
+
requested_model=requested_model,
|
|
1670
|
+
plan_path=plan_path,
|
|
1671
|
+
target_path=target_path,
|
|
1672
|
+
output_path=output_path,
|
|
1673
|
+
receipt_path=receipt_path,
|
|
1674
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1675
|
+
validation={"error": str(contract_error(exc, prefix="OpenCode task metadata invalid"))},
|
|
1676
|
+
)
|
|
1677
|
+
placeholder_field = _opencode_metadata_placeholder_field(metadata)
|
|
1678
|
+
if placeholder_field:
|
|
1679
|
+
return _result(
|
|
1680
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1681
|
+
blocked_reason="opencode_specialist_task_metadata_placeholder",
|
|
1682
|
+
next_action="Forneça metadata OpenCode nativo da task; placeholders não comprovam a execução real.",
|
|
1683
|
+
required_inputs=["opencode_task_metadata"],
|
|
1684
|
+
work_id=work_id,
|
|
1685
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1686
|
+
requested_model=requested_model,
|
|
1687
|
+
observed_model=metadata.model_id,
|
|
1688
|
+
plan_path=plan_path,
|
|
1689
|
+
target_path=target_path,
|
|
1690
|
+
output_path=output_path,
|
|
1691
|
+
receipt_path=receipt_path,
|
|
1692
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1693
|
+
validation={"placeholder_field": placeholder_field},
|
|
1694
|
+
)
|
|
1695
|
+
if metadata.work_id != work_id:
|
|
1696
|
+
return _result(
|
|
1697
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1698
|
+
blocked_reason="opencode_specialist_task_metadata_mismatch",
|
|
1699
|
+
next_action="Use metadata OpenCode gerado para o mesmo work_id do plano oficial.",
|
|
1700
|
+
required_inputs=["opencode_task_metadata"],
|
|
1701
|
+
work_id=work_id,
|
|
1702
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1703
|
+
requested_model=requested_model,
|
|
1704
|
+
observed_model=metadata.model_id,
|
|
1705
|
+
plan_path=plan_path,
|
|
1706
|
+
target_path=target_path,
|
|
1707
|
+
output_path=output_path,
|
|
1708
|
+
receipt_path=receipt_path,
|
|
1709
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1710
|
+
validation={"metadata_work_id": metadata.work_id},
|
|
1711
|
+
)
|
|
1712
|
+
if metadata.raw_content_embedded:
|
|
1713
|
+
return _result(
|
|
1714
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1715
|
+
blocked_reason="opencode_specialist_raw_content_contract_violation",
|
|
1716
|
+
next_action="Relance a task OpenCode com prompt contendo apenas o work_item tipado e paths oficiais.",
|
|
1717
|
+
required_inputs=["opencode_task_metadata"],
|
|
1718
|
+
work_id=work_id,
|
|
1719
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1720
|
+
requested_model=requested_model,
|
|
1721
|
+
observed_model=metadata.model_id,
|
|
1722
|
+
plan_path=plan_path,
|
|
1723
|
+
target_path=target_path,
|
|
1724
|
+
output_path=output_path,
|
|
1725
|
+
receipt_path=receipt_path,
|
|
1726
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1727
|
+
)
|
|
1728
|
+
if "task" not in metadata.tool_sequence:
|
|
1729
|
+
return _result(
|
|
1730
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1731
|
+
blocked_reason="opencode_specialist_task_metadata_invalid",
|
|
1732
|
+
next_action="Forneça metadata OpenCode que comprove chamada nativa de task.",
|
|
1733
|
+
required_inputs=["opencode_task_metadata"],
|
|
1734
|
+
work_id=work_id,
|
|
1735
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1736
|
+
requested_model=requested_model,
|
|
1737
|
+
observed_model=metadata.model_id,
|
|
1738
|
+
plan_path=plan_path,
|
|
1739
|
+
target_path=target_path,
|
|
1740
|
+
output_path=output_path,
|
|
1741
|
+
receipt_path=receipt_path,
|
|
1742
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1743
|
+
validation={"tool_sequence": metadata.tool_sequence},
|
|
1744
|
+
)
|
|
1745
|
+
if _opencode_model_has_forbidden_specialist_token(metadata.model_id):
|
|
1746
|
+
return _result(
|
|
1747
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1748
|
+
blocked_reason="opencode_specialist_model_fallback_forbidden",
|
|
1749
|
+
next_action="Repita a task OpenCode com modelo especialista aceito; Flash/Lite/Nano não podem assinar autoria médica.",
|
|
1750
|
+
required_inputs=["opencode_model_evidence"],
|
|
1751
|
+
work_id=work_id,
|
|
1752
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1753
|
+
requested_model=requested_model,
|
|
1754
|
+
observed_model=metadata.model_id,
|
|
1755
|
+
plan_path=plan_path,
|
|
1756
|
+
target_path=target_path,
|
|
1757
|
+
output_path=output_path,
|
|
1758
|
+
receipt_path=receipt_path,
|
|
1759
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1760
|
+
)
|
|
1761
|
+
|
|
1762
|
+
input_packet_path = output_path.with_suffix(".opencode-input.json")
|
|
1763
|
+
transcript_artifact_path = output_path.with_suffix(".opencode-task.json")
|
|
1764
|
+
input_packet_sha256 = _write_input_packet(
|
|
1765
|
+
path=input_packet_path,
|
|
1766
|
+
work_item=raw_work_item,
|
|
1767
|
+
plan_path=plan_path,
|
|
1768
|
+
model=requested_model,
|
|
1769
|
+
)
|
|
1770
|
+
deterministic_fixes = _normalize_style_rewrite_output_file(target_path=target_path, output_path=output_path)
|
|
1771
|
+
validation = apply_style_rewrite(target_path, output_path, dry_run=True)
|
|
1772
|
+
validation["deterministic_fixes_applied"] = deterministic_fixes
|
|
1773
|
+
validation_block = validation.get("validation") if isinstance(validation, dict) else {}
|
|
1774
|
+
if isinstance(validation_block, dict) and validation_block.get("errors"):
|
|
1775
|
+
return _result(
|
|
1776
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1777
|
+
blocked_reason="style_rewrite_agent_contract_violation",
|
|
1778
|
+
next_action="Regenerar o rewrite pela task OpenCode oficial para este work_item.",
|
|
1779
|
+
required_inputs=["specialist_output"],
|
|
1780
|
+
work_id=work_id,
|
|
1781
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1782
|
+
requested_model=requested_model,
|
|
1783
|
+
observed_model=metadata.model_id,
|
|
1784
|
+
plan_path=plan_path,
|
|
1785
|
+
target_path=target_path,
|
|
1786
|
+
output_path=output_path,
|
|
1787
|
+
receipt_path=receipt_path,
|
|
1788
|
+
input_packet_path=input_packet_path,
|
|
1789
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1790
|
+
validation=validation,
|
|
1791
|
+
)
|
|
1792
|
+
if isinstance(validation_block, dict) and validation_block.get("requires_llm_rewrite"):
|
|
1793
|
+
return _result(
|
|
1794
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1795
|
+
blocked_reason="style_rewrite_still_requires_rewrite",
|
|
1796
|
+
next_action="Regenerar o rewrite no OpenCode até requires_llm_rewrite=false antes de assinar o recibo.",
|
|
1797
|
+
required_inputs=["specialist_output"],
|
|
1798
|
+
work_id=work_id,
|
|
1799
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1800
|
+
requested_model=requested_model,
|
|
1801
|
+
observed_model=metadata.model_id,
|
|
1802
|
+
plan_path=plan_path,
|
|
1803
|
+
target_path=target_path,
|
|
1804
|
+
output_path=output_path,
|
|
1805
|
+
receipt_path=receipt_path,
|
|
1806
|
+
input_packet_path=input_packet_path,
|
|
1807
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1808
|
+
validation=validation,
|
|
1809
|
+
)
|
|
1810
|
+
metadata_sha256 = _sha256_bytes(resolved_task_metadata_path.read_bytes())
|
|
1811
|
+
model_evidence = JsonObjectAdapter.validate_python(
|
|
1812
|
+
{
|
|
1813
|
+
"source": "opencode_task_metadata",
|
|
1814
|
+
"requested_model": requested_model,
|
|
1815
|
+
"observed_provider_id": metadata.provider_id,
|
|
1816
|
+
"observed_model_id": metadata.model_id,
|
|
1817
|
+
"evidence_strength": "runtime_metadata",
|
|
1818
|
+
"evidence_excerpt": f"opencode task metadata: {metadata.task_id}",
|
|
1819
|
+
}
|
|
1820
|
+
)
|
|
1821
|
+
transcript_artifact_sha256 = _write_opencode_task_artifact(
|
|
1822
|
+
path=transcript_artifact_path,
|
|
1823
|
+
metadata_path=resolved_task_metadata_path,
|
|
1824
|
+
metadata_sha256=metadata_sha256,
|
|
1825
|
+
metadata=metadata,
|
|
1826
|
+
model_evidence=model_evidence,
|
|
1827
|
+
)
|
|
1828
|
+
try:
|
|
1829
|
+
receipt = _completed_receipt(
|
|
1830
|
+
work_item=raw_work_item,
|
|
1831
|
+
model=requested_model,
|
|
1832
|
+
observed_model=metadata.model_id,
|
|
1833
|
+
input_packet_path=input_packet_path,
|
|
1834
|
+
input_packet_sha256=input_packet_sha256,
|
|
1835
|
+
output_path=output_path,
|
|
1836
|
+
transcript_artifact_path=transcript_artifact_path,
|
|
1837
|
+
transcript_artifact_sha256=transcript_artifact_sha256,
|
|
1838
|
+
specialist_session_id=metadata.specialist_session_id,
|
|
1839
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1840
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1841
|
+
model_evidence=model_evidence,
|
|
1842
|
+
parent_session_id=metadata.parent_session_id,
|
|
1843
|
+
)
|
|
1844
|
+
except (MissingPathError, ValidationError, PydanticValidationError) as exc:
|
|
1845
|
+
lowered = str(exc).lower()
|
|
1846
|
+
if "forbids flash" in lowered or "forbids flash/lite/nano" in lowered:
|
|
1847
|
+
blocked_reason = "opencode_specialist_model_fallback_forbidden"
|
|
1848
|
+
required_inputs = ["opencode_model_evidence"]
|
|
1849
|
+
next_action = "Repita a task OpenCode com modelo especialista aceito; Flash/Lite/Nano não podem assinar autoria médica."
|
|
1850
|
+
elif "requires pro" in lowered or "specialist-grade" in lowered:
|
|
1851
|
+
blocked_reason = "opencode_specialist_model_evidence_missing"
|
|
1852
|
+
required_inputs = ["opencode_model_evidence"]
|
|
1853
|
+
next_action = "Repita a task OpenCode com metadata que comprove modelo especialista aceito."
|
|
1854
|
+
else:
|
|
1855
|
+
blocked_reason = "specialist_task_run_receipt_invalid"
|
|
1856
|
+
required_inputs = ["specialist_task_run_receipt"]
|
|
1857
|
+
next_action = "Corrija a evidência OpenCode/modelo e finalize novamente pela rota oficial."
|
|
1858
|
+
return _result(
|
|
1859
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1860
|
+
blocked_reason=blocked_reason,
|
|
1861
|
+
next_action=next_action,
|
|
1862
|
+
required_inputs=required_inputs,
|
|
1863
|
+
work_id=work_id,
|
|
1864
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1865
|
+
requested_model=requested_model,
|
|
1866
|
+
observed_model=metadata.model_id,
|
|
1867
|
+
plan_path=plan_path,
|
|
1868
|
+
target_path=target_path,
|
|
1869
|
+
output_path=output_path,
|
|
1870
|
+
receipt_path=receipt_path,
|
|
1871
|
+
input_packet_path=input_packet_path,
|
|
1872
|
+
transcript_artifact_path=transcript_artifact_path,
|
|
1873
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1874
|
+
validation={
|
|
1875
|
+
"error": str(exc),
|
|
1876
|
+
"task_metadata_path": str(resolved_task_metadata_path),
|
|
1877
|
+
"task_metadata_sha256": metadata_sha256,
|
|
1878
|
+
**validation,
|
|
1879
|
+
},
|
|
1880
|
+
)
|
|
1881
|
+
_write_json(receipt_path, receipt)
|
|
1882
|
+
return _result(
|
|
1883
|
+
status=SpecialistRunStatus.COMPLETED,
|
|
1884
|
+
work_id=work_id,
|
|
1885
|
+
harness=SpecialistHarness.OPENCODE,
|
|
1886
|
+
requested_model=requested_model,
|
|
1887
|
+
observed_model=metadata.model_id,
|
|
1888
|
+
plan_path=plan_path,
|
|
1889
|
+
target_path=target_path,
|
|
1890
|
+
output_path=output_path,
|
|
1891
|
+
output_sha256=_sha256_bytes(output_path.read_bytes()),
|
|
1892
|
+
receipt_path=receipt_path,
|
|
1893
|
+
input_packet_path=input_packet_path,
|
|
1894
|
+
transcript_artifact_path=transcript_artifact_path,
|
|
1895
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
1896
|
+
validation={
|
|
1897
|
+
"status": "validated",
|
|
1898
|
+
"task_metadata_path": str(resolved_task_metadata_path),
|
|
1899
|
+
"task_metadata_sha256": metadata_sha256,
|
|
1900
|
+
"transcript_artifact_sha256": transcript_artifact_sha256,
|
|
1901
|
+
**validation,
|
|
1902
|
+
},
|
|
1903
|
+
)
|
|
1904
|
+
|
|
1905
|
+
|
|
1906
|
+
def finalize_opencode_architect_task(
|
|
1907
|
+
*,
|
|
1908
|
+
plan_path: Path,
|
|
1909
|
+
work_id: str,
|
|
1910
|
+
task_metadata_path: Path | None,
|
|
1911
|
+
architect_output_path: Path | None,
|
|
1912
|
+
requested_model: str = "antigravity/gemini-3.1-pro",
|
|
1913
|
+
) -> JsonObject:
|
|
1914
|
+
try:
|
|
1915
|
+
plan_payload = _read_json_object(plan_path, label="architect plan")
|
|
1916
|
+
plan = _validate_architect_plan(plan_payload)
|
|
1917
|
+
validate_subagent_plan_attestation(plan_payload)
|
|
1918
|
+
except (MissingPathError, ValidationError, PydanticValidationError) as exc:
|
|
1919
|
+
return _architect_task_result(
|
|
1920
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1921
|
+
blocked_reason="architect_plan_contract_invalid",
|
|
1922
|
+
next_action="Regere o plano pela rota oficial plan-subagents --phase architect antes de finalizar o architect OpenCode.",
|
|
1923
|
+
required_inputs=["plan"],
|
|
1924
|
+
work_id=work_id,
|
|
1925
|
+
requested_model=requested_model,
|
|
1926
|
+
plan_path=plan_path,
|
|
1927
|
+
validation={"error": str(exc)},
|
|
1928
|
+
)
|
|
1929
|
+
work_item = _architect_work_item(plan, work_id)
|
|
1930
|
+
if work_item is None:
|
|
1931
|
+
return _architect_task_result(
|
|
1932
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1933
|
+
blocked_reason="architect_work_item_missing",
|
|
1934
|
+
next_action="Regere o plano; o work_id solicitado não existe no plano official de architect.",
|
|
1935
|
+
required_inputs=["work_id"],
|
|
1936
|
+
work_id=work_id,
|
|
1937
|
+
requested_model=requested_model,
|
|
1938
|
+
plan_path=plan_path,
|
|
1939
|
+
)
|
|
1940
|
+
raw_file = Path(work_item.raw_file or "")
|
|
1941
|
+
output_path = Path(work_item.temp_output or "")
|
|
1942
|
+
if not raw_file.exists():
|
|
1943
|
+
return _architect_task_result(
|
|
1944
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1945
|
+
blocked_reason="architect_raw_file_missing",
|
|
1946
|
+
next_action="Regere o plano; o raw chat oficial não existe mais.",
|
|
1947
|
+
required_inputs=["raw_file"],
|
|
1948
|
+
work_id=work_id,
|
|
1949
|
+
requested_model=requested_model,
|
|
1950
|
+
plan_path=plan_path,
|
|
1951
|
+
raw_file=raw_file,
|
|
1952
|
+
output_path=output_path,
|
|
1953
|
+
)
|
|
1954
|
+
if not output_path.exists():
|
|
1955
|
+
return _architect_task_result(
|
|
1956
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1957
|
+
blocked_reason="architect_output_missing",
|
|
1958
|
+
next_action="Relance a task OpenCode para este work_item; o temp_output oficial não existe.",
|
|
1959
|
+
required_inputs=["temp_output"],
|
|
1960
|
+
work_id=work_id,
|
|
1961
|
+
requested_model=requested_model,
|
|
1962
|
+
plan_path=plan_path,
|
|
1963
|
+
raw_file=raw_file,
|
|
1964
|
+
output_path=output_path,
|
|
1965
|
+
)
|
|
1966
|
+
resolved_metadata_path = task_metadata_path or _default_opencode_task_metadata_path(work_id)
|
|
1967
|
+
metadata_result = _validated_opencode_task_metadata(
|
|
1968
|
+
path=resolved_metadata_path,
|
|
1969
|
+
work_id=work_id,
|
|
1970
|
+
requested_model=requested_model,
|
|
1971
|
+
plan_path=plan_path,
|
|
1972
|
+
raw_file=raw_file,
|
|
1973
|
+
output_path=output_path,
|
|
1974
|
+
)
|
|
1975
|
+
if "result" in metadata_result:
|
|
1976
|
+
return JsonObjectAdapter.validate_python(metadata_result["result"])
|
|
1977
|
+
metadata = OpenCodeSpecialistTaskMetadata.model_validate(metadata_result["metadata"])
|
|
1978
|
+
resolved_architect_output_path = architect_output_path or _default_opencode_task_output_path(work_id)
|
|
1979
|
+
try:
|
|
1980
|
+
architect_output = ArchitectTaskOutput.model_validate(
|
|
1981
|
+
_read_json_object(resolved_architect_output_path, label="architect output")
|
|
1982
|
+
)
|
|
1983
|
+
except (MissingPathError, ValidationError, PydanticValidationError) as exc:
|
|
1984
|
+
return _architect_task_result(
|
|
1985
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
1986
|
+
blocked_reason="architect_output_contract_invalid",
|
|
1987
|
+
next_action="Forneça o artifact JSON architect-output.v1 capturado da task OpenCode.",
|
|
1988
|
+
required_inputs=["architect_output"],
|
|
1989
|
+
work_id=work_id,
|
|
1990
|
+
requested_model=requested_model,
|
|
1991
|
+
observed_model=metadata.model_id,
|
|
1992
|
+
plan_path=plan_path,
|
|
1993
|
+
raw_file=raw_file,
|
|
1994
|
+
output_path=output_path,
|
|
1995
|
+
architect_output_path=resolved_architect_output_path,
|
|
1996
|
+
task_metadata_path=resolved_metadata_path,
|
|
1997
|
+
validation={"error": str(exc)},
|
|
1998
|
+
)
|
|
1999
|
+
if architect_output.original_path != str(raw_file) or architect_output.temp_output_path != str(output_path):
|
|
2000
|
+
return _architect_task_result(
|
|
2001
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
2002
|
+
blocked_reason="architect_output_plan_mismatch",
|
|
2003
|
+
next_action="Relance a task usando exatamente o work_item do plano oficial; paths divergentes não podem seguir para stage.",
|
|
2004
|
+
required_inputs=["architect_output"],
|
|
2005
|
+
work_id=work_id,
|
|
2006
|
+
requested_model=requested_model,
|
|
2007
|
+
observed_model=metadata.model_id,
|
|
2008
|
+
plan_path=plan_path,
|
|
2009
|
+
raw_file=raw_file,
|
|
2010
|
+
output_path=output_path,
|
|
2011
|
+
architect_output_path=resolved_architect_output_path,
|
|
2012
|
+
task_metadata_path=resolved_metadata_path,
|
|
2013
|
+
validation={
|
|
2014
|
+
"expected_raw_file": str(raw_file),
|
|
2015
|
+
"observed_raw_file": architect_output.original_path,
|
|
2016
|
+
"expected_output_path": str(output_path),
|
|
2017
|
+
"observed_output_path": architect_output.temp_output_path,
|
|
2018
|
+
},
|
|
2019
|
+
)
|
|
2020
|
+
coverage_path = Path(architect_output.coverage_path)
|
|
2021
|
+
try:
|
|
2022
|
+
coverage = validate_raw_coverage_structure(coverage_path, raw_file)
|
|
2023
|
+
except (MissingPathError, ValidationError, PydanticValidationError) as exc:
|
|
2024
|
+
return _architect_task_result(
|
|
2025
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
2026
|
+
blocked_reason="architect_coverage_invalid",
|
|
2027
|
+
next_action="Regere a coverage raw-coverage.v1 a partir do note_plan e repita a finalização.",
|
|
2028
|
+
required_inputs=["coverage_path"],
|
|
2029
|
+
work_id=work_id,
|
|
2030
|
+
requested_model=requested_model,
|
|
2031
|
+
observed_model=metadata.model_id,
|
|
2032
|
+
plan_path=plan_path,
|
|
2033
|
+
raw_file=raw_file,
|
|
2034
|
+
output_path=output_path,
|
|
2035
|
+
coverage_path=coverage_path,
|
|
2036
|
+
architect_output_path=resolved_architect_output_path,
|
|
2037
|
+
task_metadata_path=resolved_metadata_path,
|
|
2038
|
+
validation={"error": str(exc)},
|
|
2039
|
+
)
|
|
2040
|
+
|
|
2041
|
+
note_style = validate_note_style_file(output_path, architect_output.staged_title, raw_file=raw_file)
|
|
2042
|
+
deterministic_fix: JsonObject = {}
|
|
2043
|
+
if _note_style_blocked(note_style):
|
|
2044
|
+
deterministic_fix = fix_note_style_file(output_path, architect_output.staged_title, output_path, raw_file=raw_file)
|
|
2045
|
+
note_style = validate_note_style_file(output_path, architect_output.staged_title, raw_file=raw_file)
|
|
2046
|
+
if _note_style_blocked(note_style):
|
|
2047
|
+
return _architect_task_result(
|
|
2048
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
2049
|
+
blocked_reason="architect_note_validation_failed",
|
|
2050
|
+
next_action="Passe error_context e rewrite_prompt ao med-knowledge-architect e repita a finalização antes de stage-note.",
|
|
2051
|
+
required_inputs=["specialist_output"],
|
|
2052
|
+
work_id=work_id,
|
|
2053
|
+
requested_model=requested_model,
|
|
2054
|
+
observed_model=metadata.model_id,
|
|
2055
|
+
plan_path=plan_path,
|
|
2056
|
+
raw_file=raw_file,
|
|
2057
|
+
title=architect_output.staged_title,
|
|
2058
|
+
taxonomy=architect_output.taxonomy,
|
|
2059
|
+
output_path=output_path,
|
|
2060
|
+
coverage_path=coverage_path,
|
|
2061
|
+
architect_output_path=resolved_architect_output_path,
|
|
2062
|
+
task_metadata_path=resolved_metadata_path,
|
|
2063
|
+
validation={"note_style": note_style, "deterministic_fix": deterministic_fix},
|
|
2064
|
+
)
|
|
2065
|
+
|
|
2066
|
+
metadata_sha256 = _sha256_bytes(resolved_metadata_path.read_bytes())
|
|
2067
|
+
architect_output_sha256 = _sha256_bytes(resolved_architect_output_path.read_bytes())
|
|
2068
|
+
model_evidence = JsonObjectAdapter.validate_python(
|
|
2069
|
+
{
|
|
2070
|
+
"source": "opencode_task_metadata",
|
|
2071
|
+
"requested_model": requested_model,
|
|
2072
|
+
"observed_provider_id": metadata.provider_id,
|
|
2073
|
+
"observed_model_id": metadata.model_id,
|
|
2074
|
+
"evidence_strength": "runtime_metadata",
|
|
2075
|
+
"evidence_excerpt": f"opencode task metadata: {metadata.task_id}",
|
|
2076
|
+
}
|
|
2077
|
+
)
|
|
2078
|
+
receipt_path = output_path.with_suffix(".architect-task-run-receipt.json")
|
|
2079
|
+
receipt = ArchitectTaskRunReceipt(
|
|
2080
|
+
work_id=work_id,
|
|
2081
|
+
harness=SpecialistHarness.OPENCODE,
|
|
2082
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
2083
|
+
requested_model=requested_model,
|
|
2084
|
+
observed_model=metadata.model_id,
|
|
2085
|
+
plan_path=str(plan_path),
|
|
2086
|
+
raw_file=str(raw_file),
|
|
2087
|
+
title=architect_output.staged_title,
|
|
2088
|
+
taxonomy=architect_output.taxonomy,
|
|
2089
|
+
output_path=str(output_path),
|
|
2090
|
+
output_sha256=_sha256_bytes(output_path.read_bytes()),
|
|
2091
|
+
coverage_path=str(coverage_path),
|
|
2092
|
+
coverage_sha256=_sha256_bytes(coverage_path.read_bytes()),
|
|
2093
|
+
architect_output_path=str(resolved_architect_output_path),
|
|
2094
|
+
architect_output_sha256=architect_output_sha256,
|
|
2095
|
+
task_metadata_path=str(resolved_metadata_path),
|
|
2096
|
+
task_metadata_sha256=metadata_sha256,
|
|
2097
|
+
model_evidence=model_evidence,
|
|
2098
|
+
).to_payload()
|
|
2099
|
+
_write_json(receipt_path, receipt)
|
|
2100
|
+
next_step = _architect_next_serial_step(
|
|
2101
|
+
raw_file=raw_file,
|
|
2102
|
+
output_path=output_path,
|
|
2103
|
+
coverage_path=coverage_path,
|
|
2104
|
+
title=architect_output.staged_title,
|
|
2105
|
+
taxonomy=architect_output.taxonomy,
|
|
2106
|
+
)
|
|
2107
|
+
return _architect_task_result(
|
|
2108
|
+
status=SpecialistRunStatus.COMPLETED,
|
|
2109
|
+
work_id=work_id,
|
|
2110
|
+
requested_model=requested_model,
|
|
2111
|
+
observed_model=metadata.model_id,
|
|
2112
|
+
plan_path=plan_path,
|
|
2113
|
+
raw_file=raw_file,
|
|
2114
|
+
title=architect_output.staged_title,
|
|
2115
|
+
taxonomy=architect_output.taxonomy,
|
|
2116
|
+
output_path=output_path,
|
|
2117
|
+
coverage_path=coverage_path,
|
|
2118
|
+
receipt_path=receipt_path,
|
|
2119
|
+
architect_output_path=resolved_architect_output_path,
|
|
2120
|
+
task_metadata_path=resolved_metadata_path,
|
|
2121
|
+
next_serial_step=next_step,
|
|
2122
|
+
validation={
|
|
2123
|
+
"note_style": note_style,
|
|
2124
|
+
"coverage": coverage,
|
|
2125
|
+
"deterministic_fix": deterministic_fix,
|
|
2126
|
+
"task_metadata_sha256": metadata_sha256,
|
|
2127
|
+
"architect_output_sha256": architect_output_sha256,
|
|
2128
|
+
},
|
|
2129
|
+
)
|
|
2130
|
+
|
|
2131
|
+
|
|
2132
|
+
def _architect_task_result(
|
|
2133
|
+
*,
|
|
2134
|
+
status: SpecialistRunStatus,
|
|
2135
|
+
work_id: str,
|
|
2136
|
+
requested_model: str,
|
|
2137
|
+
plan_path: Path,
|
|
2138
|
+
blocked_reason: str = "",
|
|
2139
|
+
next_action: str = "",
|
|
2140
|
+
required_inputs: list[str] | None = None,
|
|
2141
|
+
observed_model: str = "",
|
|
2142
|
+
raw_file: Path | None = None,
|
|
2143
|
+
title: str = "",
|
|
2144
|
+
taxonomy: str = "",
|
|
2145
|
+
output_path: Path | None = None,
|
|
2146
|
+
coverage_path: Path | None = None,
|
|
2147
|
+
receipt_path: Path | None = None,
|
|
2148
|
+
architect_output_path: Path | None = None,
|
|
2149
|
+
task_metadata_path: Path | None = None,
|
|
2150
|
+
validation: JsonObject | None = None,
|
|
2151
|
+
next_serial_step: ArchitectNextSerialStep | None = None,
|
|
2152
|
+
) -> JsonObject:
|
|
2153
|
+
output_sha256 = ""
|
|
2154
|
+
if output_path is not None and output_path.exists():
|
|
2155
|
+
output_sha256 = _sha256_bytes(output_path.read_bytes())
|
|
2156
|
+
payload = ArchitectTaskRunnerResult(
|
|
2157
|
+
status=status,
|
|
2158
|
+
blocked_reason=blocked_reason,
|
|
2159
|
+
next_action=next_action,
|
|
2160
|
+
required_inputs=required_inputs or [],
|
|
2161
|
+
work_id=work_id,
|
|
2162
|
+
harness=SpecialistHarness.OPENCODE,
|
|
2163
|
+
adapter=OPENCODE_TASK_SUBAGENT_ADAPTER,
|
|
2164
|
+
requested_model=requested_model,
|
|
2165
|
+
observed_model=observed_model,
|
|
2166
|
+
plan_path=str(plan_path),
|
|
2167
|
+
raw_file=str(raw_file or ""),
|
|
2168
|
+
title=title,
|
|
2169
|
+
taxonomy=taxonomy,
|
|
2170
|
+
output_path=str(output_path or ""),
|
|
2171
|
+
output_sha256=output_sha256,
|
|
2172
|
+
coverage_path=str(coverage_path or ""),
|
|
2173
|
+
receipt_path=str(receipt_path or ""),
|
|
2174
|
+
architect_output_path=str(architect_output_path or ""),
|
|
2175
|
+
task_metadata_path=str(task_metadata_path or ""),
|
|
2176
|
+
validation=validation or {},
|
|
2177
|
+
next_serial_step=next_serial_step,
|
|
2178
|
+
error_context=error_context(
|
|
2179
|
+
phase="architect",
|
|
2180
|
+
blocked_reason=blocked_reason,
|
|
2181
|
+
root_cause=blocked_reason,
|
|
2182
|
+
affected_artifact=str(output_path or architect_output_path or plan_path),
|
|
2183
|
+
error_summary="Architect task finalizer could not validate the Workbench receipt boundary."
|
|
2184
|
+
if status != SpecialistRunStatus.COMPLETED
|
|
2185
|
+
else "",
|
|
2186
|
+
suggested_fix=next_action,
|
|
2187
|
+
next_action=next_action,
|
|
2188
|
+
retry_scope="single_architect_work_item",
|
|
2189
|
+
)
|
|
2190
|
+
if status != SpecialistRunStatus.COMPLETED
|
|
2191
|
+
else {},
|
|
2192
|
+
)
|
|
2193
|
+
return payload.to_payload()
|
|
2194
|
+
|
|
2195
|
+
|
|
2196
|
+
def _validate_architect_plan(payload: JsonObject) -> SubagentBatchPlan:
|
|
2197
|
+
try:
|
|
2198
|
+
plan = SubagentBatchPlan.model_validate(payload)
|
|
2199
|
+
except PydanticValidationError as exc:
|
|
2200
|
+
raise contract_error(exc, prefix="architect_plan_contract_invalid") from exc
|
|
2201
|
+
if plan.phase != "architect":
|
|
2202
|
+
raise ValidationError("architect_plan_contract_invalid: phase must be architect")
|
|
2203
|
+
return plan
|
|
2204
|
+
|
|
2205
|
+
|
|
2206
|
+
def _architect_work_item(plan: SubagentBatchPlan, work_id: str) -> SubagentWorkItem | None:
|
|
2207
|
+
for item in plan.work_items:
|
|
2208
|
+
if item.work_id == work_id:
|
|
2209
|
+
return item
|
|
2210
|
+
return None
|
|
2211
|
+
|
|
2212
|
+
|
|
2213
|
+
def _validated_opencode_task_metadata(
|
|
2214
|
+
*,
|
|
2215
|
+
path: Path,
|
|
2216
|
+
work_id: str,
|
|
2217
|
+
requested_model: str,
|
|
2218
|
+
plan_path: Path,
|
|
2219
|
+
raw_file: Path,
|
|
2220
|
+
output_path: Path,
|
|
2221
|
+
) -> JsonObject:
|
|
2222
|
+
if not path.exists():
|
|
2223
|
+
return {
|
|
2224
|
+
"result": _architect_task_result(
|
|
2225
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
2226
|
+
blocked_reason="opencode_specialist_task_metadata_missing",
|
|
2227
|
+
next_action="Forneça o metadata oficial da task OpenCode para finalizar o output do architect.",
|
|
2228
|
+
required_inputs=["opencode_task_metadata"],
|
|
2229
|
+
work_id=work_id,
|
|
2230
|
+
requested_model=requested_model,
|
|
2231
|
+
plan_path=plan_path,
|
|
2232
|
+
raw_file=raw_file,
|
|
2233
|
+
output_path=output_path,
|
|
2234
|
+
task_metadata_path=path,
|
|
2235
|
+
)
|
|
2236
|
+
}
|
|
2237
|
+
try:
|
|
2238
|
+
payload = _read_json_object(path, label="OpenCode architect task metadata")
|
|
2239
|
+
except (MissingPathError, ValidationError) as exc:
|
|
2240
|
+
return {
|
|
2241
|
+
"result": _architect_task_result(
|
|
2242
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
2243
|
+
blocked_reason="opencode_specialist_task_metadata_invalid",
|
|
2244
|
+
next_action="Forneça metadata JSON oficial da task OpenCode.",
|
|
2245
|
+
required_inputs=["opencode_task_metadata"],
|
|
2246
|
+
work_id=work_id,
|
|
2247
|
+
requested_model=requested_model,
|
|
2248
|
+
plan_path=plan_path,
|
|
2249
|
+
raw_file=raw_file,
|
|
2250
|
+
output_path=output_path,
|
|
2251
|
+
task_metadata_path=path,
|
|
2252
|
+
validation={"error": str(exc)},
|
|
2253
|
+
)
|
|
2254
|
+
}
|
|
2255
|
+
if "model_id" not in payload or not str(payload["model_id"]).strip():
|
|
2256
|
+
return {
|
|
2257
|
+
"result": _architect_task_result(
|
|
2258
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
2259
|
+
blocked_reason="opencode_specialist_model_evidence_missing",
|
|
2260
|
+
next_action="Repita a task OpenCode com metadata que exponha provider_id/model_id efetivos.",
|
|
2261
|
+
required_inputs=["opencode_task_metadata"],
|
|
2262
|
+
work_id=work_id,
|
|
2263
|
+
requested_model=requested_model,
|
|
2264
|
+
plan_path=plan_path,
|
|
2265
|
+
raw_file=raw_file,
|
|
2266
|
+
output_path=output_path,
|
|
2267
|
+
task_metadata_path=path,
|
|
2268
|
+
)
|
|
2269
|
+
}
|
|
2270
|
+
try:
|
|
2271
|
+
metadata = OpenCodeSpecialistTaskMetadata.model_validate(payload)
|
|
2272
|
+
except PydanticValidationError as exc:
|
|
2273
|
+
return {
|
|
2274
|
+
"result": _architect_task_result(
|
|
2275
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
2276
|
+
blocked_reason="opencode_specialist_task_metadata_invalid",
|
|
2277
|
+
next_action="Forneça metadata OpenCode que satisfaça o contrato opencode-specialist-task-metadata.v1.",
|
|
2278
|
+
required_inputs=["opencode_task_metadata"],
|
|
2279
|
+
work_id=work_id,
|
|
2280
|
+
requested_model=requested_model,
|
|
2281
|
+
plan_path=plan_path,
|
|
2282
|
+
raw_file=raw_file,
|
|
2283
|
+
output_path=output_path,
|
|
2284
|
+
task_metadata_path=path,
|
|
2285
|
+
validation={"error": str(contract_error(exc, prefix="OpenCode task metadata invalid"))},
|
|
2286
|
+
)
|
|
2287
|
+
}
|
|
2288
|
+
placeholder_field = _opencode_metadata_placeholder_field(metadata)
|
|
2289
|
+
if placeholder_field:
|
|
2290
|
+
return {
|
|
2291
|
+
"result": _architect_task_result(
|
|
2292
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
2293
|
+
blocked_reason="opencode_specialist_task_metadata_placeholder",
|
|
2294
|
+
next_action="Forneça metadata OpenCode nativo da task; placeholders não comprovam a execução real.",
|
|
2295
|
+
required_inputs=["opencode_task_metadata"],
|
|
2296
|
+
work_id=work_id,
|
|
2297
|
+
requested_model=requested_model,
|
|
2298
|
+
observed_model=metadata.model_id,
|
|
2299
|
+
plan_path=plan_path,
|
|
2300
|
+
raw_file=raw_file,
|
|
2301
|
+
output_path=output_path,
|
|
2302
|
+
task_metadata_path=path,
|
|
2303
|
+
validation={"placeholder_field": placeholder_field},
|
|
2304
|
+
)
|
|
2305
|
+
}
|
|
2306
|
+
if metadata.work_id != work_id:
|
|
2307
|
+
return {
|
|
2308
|
+
"result": _architect_task_result(
|
|
2309
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
2310
|
+
blocked_reason="opencode_specialist_task_metadata_mismatch",
|
|
2311
|
+
next_action="Use metadata OpenCode gerado para o mesmo work_id do plano oficial.",
|
|
2312
|
+
required_inputs=["opencode_task_metadata"],
|
|
2313
|
+
work_id=work_id,
|
|
2314
|
+
requested_model=requested_model,
|
|
2315
|
+
observed_model=metadata.model_id,
|
|
2316
|
+
plan_path=plan_path,
|
|
2317
|
+
raw_file=raw_file,
|
|
2318
|
+
output_path=output_path,
|
|
2319
|
+
task_metadata_path=path,
|
|
2320
|
+
validation={"metadata_work_id": metadata.work_id},
|
|
2321
|
+
)
|
|
2322
|
+
}
|
|
2323
|
+
if metadata.raw_content_embedded:
|
|
2324
|
+
return {
|
|
2325
|
+
"result": _architect_task_result(
|
|
2326
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
2327
|
+
blocked_reason="opencode_specialist_raw_content_contract_violation",
|
|
2328
|
+
next_action="Relance a task OpenCode com prompt contendo apenas o work_item tipado e paths oficiais.",
|
|
2329
|
+
required_inputs=["opencode_task_metadata"],
|
|
2330
|
+
work_id=work_id,
|
|
2331
|
+
requested_model=requested_model,
|
|
2332
|
+
observed_model=metadata.model_id,
|
|
2333
|
+
plan_path=plan_path,
|
|
2334
|
+
raw_file=raw_file,
|
|
2335
|
+
output_path=output_path,
|
|
2336
|
+
task_metadata_path=path,
|
|
2337
|
+
)
|
|
2338
|
+
}
|
|
2339
|
+
if "task" not in metadata.tool_sequence:
|
|
2340
|
+
return {
|
|
2341
|
+
"result": _architect_task_result(
|
|
2342
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
2343
|
+
blocked_reason="opencode_specialist_task_metadata_invalid",
|
|
2344
|
+
next_action="Forneça metadata OpenCode que comprove chamada nativa de task.",
|
|
2345
|
+
required_inputs=["opencode_task_metadata"],
|
|
2346
|
+
work_id=work_id,
|
|
2347
|
+
requested_model=requested_model,
|
|
2348
|
+
observed_model=metadata.model_id,
|
|
2349
|
+
plan_path=plan_path,
|
|
2350
|
+
raw_file=raw_file,
|
|
2351
|
+
output_path=output_path,
|
|
2352
|
+
task_metadata_path=path,
|
|
2353
|
+
validation={"tool_sequence": metadata.tool_sequence},
|
|
2354
|
+
)
|
|
2355
|
+
}
|
|
2356
|
+
if _opencode_model_has_forbidden_specialist_token(metadata.model_id):
|
|
2357
|
+
return {
|
|
2358
|
+
"result": _architect_task_result(
|
|
2359
|
+
status=SpecialistRunStatus.BLOCKED,
|
|
2360
|
+
blocked_reason="opencode_specialist_model_fallback_forbidden",
|
|
2361
|
+
next_action="Repita a task OpenCode com modelo especialista aceito; Flash/Lite/Nano não podem assinar autoria médica.",
|
|
2362
|
+
required_inputs=["opencode_model_evidence"],
|
|
2363
|
+
work_id=work_id,
|
|
2364
|
+
requested_model=requested_model,
|
|
2365
|
+
observed_model=metadata.model_id,
|
|
2366
|
+
plan_path=plan_path,
|
|
2367
|
+
raw_file=raw_file,
|
|
2368
|
+
output_path=output_path,
|
|
2369
|
+
task_metadata_path=path,
|
|
2370
|
+
)
|
|
2371
|
+
}
|
|
2372
|
+
return {"metadata": metadata.to_payload()}
|
|
2373
|
+
|
|
2374
|
+
|
|
2375
|
+
def _note_style_blocked(payload: JsonObject) -> bool:
|
|
2376
|
+
errors = payload["errors"] if "errors" in payload and isinstance(payload["errors"], list) else []
|
|
2377
|
+
return bool(errors) or bool(payload["requires_llm_rewrite"] if "requires_llm_rewrite" in payload else False)
|
|
2378
|
+
|
|
2379
|
+
|
|
2380
|
+
def _architect_next_serial_step(
|
|
2381
|
+
*,
|
|
2382
|
+
raw_file: Path,
|
|
2383
|
+
output_path: Path,
|
|
2384
|
+
coverage_path: Path,
|
|
2385
|
+
title: str,
|
|
2386
|
+
taxonomy: str,
|
|
2387
|
+
) -> ArchitectNextSerialStep:
|
|
2388
|
+
return ArchitectNextSerialStep.model_validate(
|
|
2389
|
+
{
|
|
2390
|
+
"schema": ARCHITECT_NEXT_SERIAL_STEP_SCHEMA,
|
|
2391
|
+
"command_family": "stage-note",
|
|
2392
|
+
"arguments": [
|
|
2393
|
+
"--manifest",
|
|
2394
|
+
"<manifest.json>",
|
|
2395
|
+
"--raw-file",
|
|
2396
|
+
str(raw_file),
|
|
2397
|
+
"--coverage",
|
|
2398
|
+
str(coverage_path),
|
|
2399
|
+
"--taxonomy",
|
|
2400
|
+
taxonomy,
|
|
2401
|
+
"--title",
|
|
2402
|
+
title,
|
|
2403
|
+
"--content",
|
|
2404
|
+
str(output_path),
|
|
2405
|
+
],
|
|
2406
|
+
"must_run_before": [
|
|
2407
|
+
"publish-batch --dry-run",
|
|
2408
|
+
"publish-batch",
|
|
2409
|
+
"run-linker",
|
|
2410
|
+
"another architect subagent",
|
|
2411
|
+
],
|
|
2412
|
+
"agent_instruction": (
|
|
2413
|
+
"Architect output validated. Run stage-note with these arguments before publish-batch; "
|
|
2414
|
+
"do not inspect source code or infer an alternate finalizer."
|
|
2415
|
+
),
|
|
2416
|
+
}
|
|
2417
|
+
)
|
|
2418
|
+
|
|
2419
|
+
|
|
2420
|
+
def _opencode_metadata_placeholder_field(metadata: OpenCodeSpecialistTaskMetadata) -> str:
|
|
2421
|
+
for field_name in (
|
|
2422
|
+
"task_id",
|
|
2423
|
+
"parent_session_id",
|
|
2424
|
+
"specialist_session_id",
|
|
2425
|
+
"provider_id",
|
|
2426
|
+
"model_id",
|
|
2427
|
+
):
|
|
2428
|
+
if _metadata_value_looks_placeholder(str(getattr(metadata, field_name))):
|
|
2429
|
+
return field_name
|
|
2430
|
+
return ""
|
|
2431
|
+
|
|
2432
|
+
|
|
2433
|
+
def _metadata_value_looks_placeholder(value: str) -> bool:
|
|
2434
|
+
normalized = value.strip().casefold()
|
|
2435
|
+
if not normalized:
|
|
2436
|
+
return True
|
|
2437
|
+
if normalized in {"unknown", "none", "null", "n/a", "na", "manual", "fabricated"}:
|
|
2438
|
+
return True
|
|
2439
|
+
if normalized.startswith(("default", "placeholder")):
|
|
2440
|
+
return True
|
|
2441
|
+
return "mock" in normalized or "placeholder" in normalized
|
|
2442
|
+
|
|
2443
|
+
|
|
2444
|
+
def _default_opencode_task_metadata_path(work_id: str) -> Path:
|
|
2445
|
+
return (
|
|
2446
|
+
_mednotes_app_home()
|
|
2447
|
+
/ "hook-state"
|
|
2448
|
+
/ "opencode-task-metadata"
|
|
2449
|
+
/ "by-work-id"
|
|
2450
|
+
/ f"{_safe_file_stem(work_id)}.json"
|
|
2451
|
+
)
|
|
2452
|
+
|
|
2453
|
+
|
|
2454
|
+
def _default_opencode_task_output_path(work_id: str) -> Path:
|
|
2455
|
+
return (
|
|
2456
|
+
_mednotes_app_home()
|
|
2457
|
+
/ "hook-state"
|
|
2458
|
+
/ "opencode-task-output"
|
|
2459
|
+
/ "by-work-id"
|
|
2460
|
+
/ f"{_safe_file_stem(work_id)}.json"
|
|
2461
|
+
)
|
|
2462
|
+
|
|
2463
|
+
|
|
2464
|
+
def _mednotes_app_home() -> Path:
|
|
2465
|
+
configured = os.environ.get("MEDNOTES_HOME") or str(Path.home() / ".mednotes")
|
|
2466
|
+
return Path(configured).expanduser()
|
|
2467
|
+
|
|
2468
|
+
|
|
2469
|
+
def _safe_file_stem(value: str) -> str:
|
|
2470
|
+
return re.sub(r"[^a-zA-Z0-9_.-]", "_", value)[:120] or "unknown"
|