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,1767 @@
|
|
|
1
|
+
"""Safe subagent planning for Wiki workflows."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import unicodedata
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, StrictStr, field_validator
|
|
14
|
+
|
|
15
|
+
from mednotes.domains.wiki.capabilities.atomicity.atomicity import build_atomicity_split_plan
|
|
16
|
+
from mednotes.domains.wiki.capabilities.notes.artifacts import discover_artifact_manifests
|
|
17
|
+
from mednotes.domains.wiki.capabilities.notes.meaning_planner import plan_meaning_work_items
|
|
18
|
+
from mednotes.domains.wiki.capabilities.notes.note_plan import (
|
|
19
|
+
PLANNED_MEANING_ACTION,
|
|
20
|
+
TRIAGE_NOTE_PLAN_V2_SCHEMA,
|
|
21
|
+
note_plan_summary,
|
|
22
|
+
parse_triage_note_plan,
|
|
23
|
+
)
|
|
24
|
+
from mednotes.domains.wiki.capabilities.notes.raw_chats import covered_raw_chat_index, list_by_status, read_note_meta
|
|
25
|
+
from mednotes.domains.wiki.capabilities.specialist.plan_attestation import attach_subagent_plan_attestation
|
|
26
|
+
from mednotes.domains.wiki.capabilities.style.style import validate_wiki_style
|
|
27
|
+
from mednotes.domains.wiki.capabilities.vocabulary.link_terms import normalize_key
|
|
28
|
+
from mednotes.domains.wiki.capabilities.vocabulary.vocabulary_curator_batch import build_vocabulary_curator_batch_plan
|
|
29
|
+
from mednotes.domains.wiki.common import SUBAGENT_PLAN_SCHEMA, MedOpsError, ValidationError
|
|
30
|
+
from mednotes.domains.wiki.config import MedConfig, _user_state_dir
|
|
31
|
+
from mednotes.domains.wiki.contracts.agents import SubagentBatchPlan
|
|
32
|
+
from mednotes.domains.wiki.contracts.workflow_guardrails import (
|
|
33
|
+
PROCESS_CHATS_REQUIRED_INPUTS,
|
|
34
|
+
STYLE_REWRITE_REQUIRED_INPUTS,
|
|
35
|
+
annotate_payload,
|
|
36
|
+
note_target_index,
|
|
37
|
+
plan_status,
|
|
38
|
+
)
|
|
39
|
+
from mednotes.domains.wiki.contracts.workflow_outcomes import (
|
|
40
|
+
DecisionEvidence,
|
|
41
|
+
RejectedAutomation,
|
|
42
|
+
WorkflowDecision,
|
|
43
|
+
)
|
|
44
|
+
from mednotes.kernel.agent_directive import AgentDirective
|
|
45
|
+
from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
|
|
46
|
+
from mednotes.kernel.public_report import WorkflowPublicReport
|
|
47
|
+
from mednotes.platform.user_config import ParallelismConfig
|
|
48
|
+
|
|
49
|
+
_DEFAULT_PARALLELISM = ParallelismConfig()
|
|
50
|
+
DEFAULT_PROCESS_CHATS_MAX_CONCURRENCY = _DEFAULT_PARALLELISM.process_chats_max_parallel_architects
|
|
51
|
+
DEFAULT_STYLE_REWRITE_MAX_CONCURRENCY = _DEFAULT_PARALLELISM.fix_wiki_max_parallel_rewrites
|
|
52
|
+
CANONICAL_MERGE_PLAN_SCHEMA = "medical-notes-workbench.canonical-merge-plan.v1"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _PlannedMeaningTarget(ContractModel):
|
|
56
|
+
"""Typed target identity extracted from a validated triage note plan."""
|
|
57
|
+
|
|
58
|
+
id: StrictStr
|
|
59
|
+
title: StrictStr
|
|
60
|
+
target_key: StrictStr
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class _PlannedMatch(ContractModel):
|
|
64
|
+
"""Typed source-to-target match used to decide canonical merge routes."""
|
|
65
|
+
|
|
66
|
+
raw_file: StrictStr
|
|
67
|
+
work_id: StrictStr
|
|
68
|
+
id: StrictStr
|
|
69
|
+
title: StrictStr
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class _TriageNotePlanItem(BaseModel):
|
|
73
|
+
"""Typed view of a triage note-plan item used for routing decisions."""
|
|
74
|
+
|
|
75
|
+
model_config = ConfigDict(extra="ignore")
|
|
76
|
+
|
|
77
|
+
id: StrictStr = ""
|
|
78
|
+
action: StrictStr = ""
|
|
79
|
+
staged_title: StrictStr = ""
|
|
80
|
+
title: StrictStr = ""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _TriageNotePlan(BaseModel):
|
|
84
|
+
"""Validated triage note plan plus its public JSON payload.
|
|
85
|
+
|
|
86
|
+
The parent workflow still passes the full note-plan payload to downstream
|
|
87
|
+
contracts, but all routing decisions in this module read this typed view.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
model_config = ConfigDict(extra="ignore")
|
|
91
|
+
|
|
92
|
+
schema_: StrictStr = Field(default="", alias="schema")
|
|
93
|
+
items: list[_TriageNotePlanItem] = Field(default_factory=list)
|
|
94
|
+
_payload: JsonObject = PrivateAttr(default_factory=dict)
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_payload(cls, payload: object) -> _TriageNotePlan:
|
|
98
|
+
json_payload = JsonObjectAdapter.validate_python(payload)
|
|
99
|
+
plan = cls.model_validate(json_payload)
|
|
100
|
+
plan._payload = json_payload
|
|
101
|
+
return plan
|
|
102
|
+
|
|
103
|
+
def public_payload(self) -> JsonObject:
|
|
104
|
+
return dict(self._payload)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class _DuplicateTarget(ContractModel):
|
|
108
|
+
"""Typed duplicate target; JSON projection happens only at boundaries."""
|
|
109
|
+
|
|
110
|
+
id: StrictStr = ""
|
|
111
|
+
title: StrictStr = ""
|
|
112
|
+
target_key: StrictStr
|
|
113
|
+
conflict_type: Literal["ambiguous_existing_wiki_note", "existing_wiki_note", "planned_in_batch"]
|
|
114
|
+
existing_paths: list[StrictStr] = Field(default_factory=list)
|
|
115
|
+
planned_matches: list[_PlannedMatch] = Field(default_factory=list)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class _SubagentAnnotationPayload(BaseModel):
|
|
119
|
+
"""Typed read model for annotating a public subagent plan payload."""
|
|
120
|
+
|
|
121
|
+
model_config = ConfigDict(extra="ignore")
|
|
122
|
+
|
|
123
|
+
phase: StrictStr = ""
|
|
124
|
+
agent: StrictStr = ""
|
|
125
|
+
work_items: list[JsonObject] = Field(default_factory=list)
|
|
126
|
+
blocked_items: list[JsonObject] = Field(default_factory=list)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class _SubagentAnnotationWorkItem(BaseModel):
|
|
130
|
+
"""Typed read model for one work item while preserving its raw output."""
|
|
131
|
+
|
|
132
|
+
model_config = ConfigDict(extra="ignore")
|
|
133
|
+
|
|
134
|
+
phase: StrictStr = ""
|
|
135
|
+
expected_output_schema: JsonObject | StrictStr | None = None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class _SubagentGeneratedPlanSummary(BaseModel):
|
|
139
|
+
"""Minimal typed view for generated plans before concurrency policy."""
|
|
140
|
+
|
|
141
|
+
model_config = ConfigDict(extra="ignore")
|
|
142
|
+
|
|
143
|
+
item_count: int = Field(default=0, ge=0)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class _StyleRewriteAuditReport(BaseModel):
|
|
147
|
+
"""Typed style-audit row used to plan specialist rewrite work."""
|
|
148
|
+
|
|
149
|
+
model_config = ConfigDict(extra="ignore")
|
|
150
|
+
|
|
151
|
+
requires_llm_rewrite: bool = False
|
|
152
|
+
path: StrictStr = ""
|
|
153
|
+
title: StrictStr = ""
|
|
154
|
+
rewrite_prompt: StrictStr = ""
|
|
155
|
+
errors: list[JsonObject] = Field(default_factory=list)
|
|
156
|
+
warnings: list[JsonObject] = Field(default_factory=list)
|
|
157
|
+
|
|
158
|
+
@field_validator("path", "title", "rewrite_prompt", mode="before")
|
|
159
|
+
@classmethod
|
|
160
|
+
def _optional_text(cls, value: object) -> str:
|
|
161
|
+
return "" if value is None else str(value)
|
|
162
|
+
|
|
163
|
+
@field_validator("errors", "warnings", mode="before")
|
|
164
|
+
@classmethod
|
|
165
|
+
def _optional_json_list(cls, value: object) -> list[JsonObject]:
|
|
166
|
+
if not isinstance(value, list):
|
|
167
|
+
return []
|
|
168
|
+
return [JsonObjectAdapter.validate_python(item) for item in value if isinstance(item, dict)]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class _StyleRewriteAuditSummary(BaseModel):
|
|
172
|
+
"""Typed style-audit summary used in the public subagent plan payload."""
|
|
173
|
+
|
|
174
|
+
model_config = ConfigDict(extra="ignore")
|
|
175
|
+
|
|
176
|
+
schema_: StrictStr = Field(default="", alias="schema")
|
|
177
|
+
wiki_dir: StrictStr = ""
|
|
178
|
+
file_count: int = Field(default=0, ge=0)
|
|
179
|
+
error_count: int = Field(default=0, ge=0)
|
|
180
|
+
warning_count: int = Field(default=0, ge=0)
|
|
181
|
+
reports: list[_StyleRewriteAuditReport] = Field(default_factory=list)
|
|
182
|
+
|
|
183
|
+
class _RawChatPlanningRow(BaseModel):
|
|
184
|
+
"""Typed raw-chat listing row used by subagent planning."""
|
|
185
|
+
|
|
186
|
+
model_config = ConfigDict(extra="ignore")
|
|
187
|
+
|
|
188
|
+
path: StrictStr
|
|
189
|
+
titulo_triagem: StrictStr = ""
|
|
190
|
+
fonte_id: StrictStr = ""
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class _ReadNoteMeta(BaseModel):
|
|
194
|
+
"""Typed metadata slice read from raw chat YAML/frontmatter."""
|
|
195
|
+
|
|
196
|
+
model_config = ConfigDict(extra="ignore")
|
|
197
|
+
|
|
198
|
+
note_plan: StrictStr = ""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class _MeaningPlannerResult(BaseModel):
|
|
202
|
+
"""Typed view of meaning-planner output before architect fan-out."""
|
|
203
|
+
|
|
204
|
+
model_config = ConfigDict(extra="ignore")
|
|
205
|
+
|
|
206
|
+
work_items: list[JsonObject] = Field(default_factory=list)
|
|
207
|
+
blocked_items: list[JsonObject] = Field(default_factory=list)
|
|
208
|
+
next_action: StrictStr = ""
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class _ArchitectParsedItem(ContractModel):
|
|
212
|
+
"""Internal typed architect planning row before public payload emission."""
|
|
213
|
+
|
|
214
|
+
item: JsonObject
|
|
215
|
+
note_plan: _TriageNotePlan
|
|
216
|
+
targets: list[_PlannedMeaningTarget] = Field(default_factory=list)
|
|
217
|
+
duplicate_targets: list[_DuplicateTarget] = Field(default_factory=list)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _json_str_field(payload: JsonObject, key: str, default: str = "") -> str:
|
|
221
|
+
"""Read an optional public JSON string after the object boundary is typed."""
|
|
222
|
+
|
|
223
|
+
if key not in payload:
|
|
224
|
+
return default
|
|
225
|
+
value = payload[key]
|
|
226
|
+
if value is None:
|
|
227
|
+
return default
|
|
228
|
+
return str(value)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _json_list_field(payload: JsonObject, key: str) -> list[object]:
|
|
232
|
+
"""Read an optional public JSON list without `.get()` fallback semantics."""
|
|
233
|
+
|
|
234
|
+
if key not in payload:
|
|
235
|
+
return []
|
|
236
|
+
value = payload[key]
|
|
237
|
+
return list(value) if isinstance(value, list) else []
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _json_object_list_field(payload: JsonObject, key: str) -> list[JsonObject]:
|
|
241
|
+
"""Read an optional list of public JSON objects from a typed payload."""
|
|
242
|
+
|
|
243
|
+
return [JsonObjectAdapter.validate_python(item) for item in _json_list_field(payload, key) if isinstance(item, dict)]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _json_object_field(payload: JsonObject, key: str) -> JsonObject | None:
|
|
247
|
+
"""Read one optional public JSON object from a typed payload."""
|
|
248
|
+
|
|
249
|
+
if key not in payload:
|
|
250
|
+
return None
|
|
251
|
+
value = payload[key]
|
|
252
|
+
return JsonObjectAdapter.validate_python(value) if isinstance(value, dict) else None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _slug(value: str) -> str:
|
|
256
|
+
normalized = unicodedata.normalize("NFKD", value)
|
|
257
|
+
ascii_text = "".join(char for char in normalized if not unicodedata.combining(char))
|
|
258
|
+
slug = re.sub(r"[^A-Za-z0-9._-]+", "-", ascii_text).strip("-._").lower()
|
|
259
|
+
return slug or "raw"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _file_sha256(path: Path) -> str:
|
|
263
|
+
return "sha256:" + hashlib.sha256(path.read_bytes()).hexdigest()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def configured_subagent_max_concurrency(config: MedConfig, phase: str) -> int:
|
|
267
|
+
parallelism = config.user_config.parallelism
|
|
268
|
+
defaults = {
|
|
269
|
+
"triage": parallelism.process_chats_max_parallel_triagers,
|
|
270
|
+
"architect": parallelism.process_chats_max_parallel_architects,
|
|
271
|
+
"style-rewrite": parallelism.fix_wiki_max_parallel_rewrites,
|
|
272
|
+
"note-merge": parallelism.fix_wiki_max_parallel_rewrites,
|
|
273
|
+
"atomicity-split": parallelism.fix_wiki_max_parallel_rewrites,
|
|
274
|
+
"vocabulary-curation": parallelism.link_max_parallel_curators,
|
|
275
|
+
}
|
|
276
|
+
if phase not in defaults:
|
|
277
|
+
raise ValidationError(f"Unknown subagent planning phase: {phase}")
|
|
278
|
+
return defaults[phase]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _chunked(items: list[JsonObject], size: int) -> list[list[JsonObject]]:
|
|
282
|
+
return [items[index : index + size] for index in range(0, len(items), size)]
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _batch_refs(items: list[JsonObject], size: int) -> list[JsonObject]:
|
|
286
|
+
batches: list[JsonObject] = []
|
|
287
|
+
for batch_index, batch in enumerate(_chunked(items, size), start=1):
|
|
288
|
+
batches.append(
|
|
289
|
+
{
|
|
290
|
+
"batch": batch_index,
|
|
291
|
+
"max_concurrency": size,
|
|
292
|
+
"item_count": len(batch),
|
|
293
|
+
"work_ids": [str(item["work_id"]) for item in batch],
|
|
294
|
+
"owner_keys": [str(item["owner_key"]) for item in batch],
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
return batches
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _expected_output_schema_for_phase(phase: str) -> dict[str, str]:
|
|
301
|
+
schemas = {
|
|
302
|
+
"triage": {
|
|
303
|
+
"schema": "medical-notes-workbench.triage-output.v1",
|
|
304
|
+
"description": "Saida estruturada do triager com note_plan v2.",
|
|
305
|
+
},
|
|
306
|
+
"architect": {
|
|
307
|
+
"schema": "medical-notes-workbench.architect-output.v1",
|
|
308
|
+
"description": "Nota ou rewrite produzido pelo architect.",
|
|
309
|
+
},
|
|
310
|
+
"style-rewrite": {
|
|
311
|
+
"schema": "medical-notes-workbench.style-rewrite-output-attestation.v1",
|
|
312
|
+
"description": (
|
|
313
|
+
"O subagente escreve Markdown em temp_output; o pai gera a atestação assinada do Workbench com "
|
|
314
|
+
"finalize-style-rewrite-output."
|
|
315
|
+
),
|
|
316
|
+
},
|
|
317
|
+
"note-merge": {
|
|
318
|
+
"schema": "medical-notes-workbench.note-merge-output.v1",
|
|
319
|
+
"description": "Merge semantico para apply-note-merge.",
|
|
320
|
+
},
|
|
321
|
+
"atomicity-split": {
|
|
322
|
+
"schema": "medical-notes-workbench.atomicity-split-bundle.v1",
|
|
323
|
+
"description": "Bundle de split atomico para apply-atomicity-split.",
|
|
324
|
+
},
|
|
325
|
+
"atomicity_split": {
|
|
326
|
+
"schema": "medical-notes-workbench.atomicity-split-bundle.v1",
|
|
327
|
+
"description": "Bundle de split atomico para apply-atomicity-split.",
|
|
328
|
+
},
|
|
329
|
+
"vocabulary-curation": {
|
|
330
|
+
"schema": "medical-notes-workbench.note-semantic-ingestion.v1",
|
|
331
|
+
"description": "JSON de curadoria semantica para apply-curator-batch.",
|
|
332
|
+
},
|
|
333
|
+
"vocabulary_curation": {
|
|
334
|
+
"schema": "medical-notes-workbench.note-semantic-ingestion.v1",
|
|
335
|
+
"description": "JSON de curadoria semantica para apply-curator-batch.",
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
if phase in schemas:
|
|
339
|
+
return dict(schemas[phase])
|
|
340
|
+
return dict(schemas["architect"])
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _annotate_work_items_for_subagent_contract(payload: JsonObject) -> JsonObject:
|
|
344
|
+
typed_payload = _SubagentAnnotationPayload.model_validate(payload)
|
|
345
|
+
phase = typed_payload.phase
|
|
346
|
+
agent = typed_payload.agent
|
|
347
|
+
annotated = dict(payload)
|
|
348
|
+
work_items = []
|
|
349
|
+
for raw_item in typed_payload.work_items:
|
|
350
|
+
typed_item = _SubagentAnnotationWorkItem.model_validate(raw_item)
|
|
351
|
+
item = dict(raw_item)
|
|
352
|
+
item_phase = typed_item.phase or phase
|
|
353
|
+
item.setdefault("phase", item_phase)
|
|
354
|
+
item.setdefault("agent", agent)
|
|
355
|
+
if not isinstance(typed_item.expected_output_schema, dict):
|
|
356
|
+
item["expected_output_schema"] = _expected_output_schema_for_phase(item_phase)
|
|
357
|
+
work_items.append(item)
|
|
358
|
+
annotated["work_items"] = work_items
|
|
359
|
+
annotated.setdefault("blocked_item_count", len(typed_payload.blocked_items))
|
|
360
|
+
annotated.setdefault("parent_applies_outputs", True)
|
|
361
|
+
return annotated
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _typed_subagent_plan_payload(payload: JsonObject) -> JsonObject:
|
|
365
|
+
typed_payload = attach_subagent_plan_attestation(_annotate_work_items_for_subagent_contract(payload))
|
|
366
|
+
SubagentBatchPlan.model_validate(typed_payload)
|
|
367
|
+
return typed_payload
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _default_subagent_temp_root(phase: str) -> Path:
|
|
371
|
+
base = _user_state_dir() / "tmp" / "agent-work"
|
|
372
|
+
if phase == "triage":
|
|
373
|
+
return base / "process-chats" / "triage"
|
|
374
|
+
if phase == "architect":
|
|
375
|
+
return base / "process-chats"
|
|
376
|
+
if phase in {"style-rewrite", "note-merge", "atomicity-split", "atomicity_split"}:
|
|
377
|
+
return base / "fix-wiki"
|
|
378
|
+
if phase in {"vocabulary-curation", "vocabulary_curation"}:
|
|
379
|
+
return base / "vocabulary-curation"
|
|
380
|
+
return base / _slug(phase)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _style_rewrite_subagent_output_contract() -> JsonObject:
|
|
384
|
+
return {
|
|
385
|
+
"schema": "medical-notes-workbench.subagent-output-contract.v1",
|
|
386
|
+
"write_markdown_to": "temp_output",
|
|
387
|
+
"subagent_must_create_attestation": False,
|
|
388
|
+
"subagent_must_create_specialist_task_run_receipt": False,
|
|
389
|
+
"parent_must_not_fabricate_specialist_task_run_receipt": True,
|
|
390
|
+
"parent_may_call_specialist_task_receipt_finalizer": True,
|
|
391
|
+
"official_receipt_finalizers": [
|
|
392
|
+
"call_specialist_model",
|
|
393
|
+
"finalize-agy-specialist-task",
|
|
394
|
+
"finalize-opencode-specialist-task",
|
|
395
|
+
],
|
|
396
|
+
"missing_specialist_task_run_receipt_action": "run_official_receipt_finalizer_or_stop",
|
|
397
|
+
"parent_only_fields": ["output_attestation_path"],
|
|
398
|
+
"runner_only_fields": ["specialist_task_run_receipt_path"],
|
|
399
|
+
"attestation_created_by": "finalize-style-rewrite-output",
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _extension_root() -> Path:
|
|
404
|
+
from mednotes.platform.paths import extension_root
|
|
405
|
+
|
|
406
|
+
return extension_root()
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _agent_readable_docs_root(root: Path) -> Path:
|
|
410
|
+
"""Prefer source docs when running the built extension from ignored dist/ in dev."""
|
|
411
|
+
|
|
412
|
+
if root.name == "gemini-cli-extension" and root.parent.name == "dist":
|
|
413
|
+
source_root = root.parents[1] / "extension"
|
|
414
|
+
required = (
|
|
415
|
+
source_root / "docs" / "agent-prompt-hardening.md",
|
|
416
|
+
source_root / "docs" / "knowledge-architect.md",
|
|
417
|
+
source_root / "docs" / "semantic-linker.md",
|
|
418
|
+
)
|
|
419
|
+
if all(path.exists() for path in required):
|
|
420
|
+
return source_root
|
|
421
|
+
return root
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _style_rewrite_context_docs() -> JsonObject:
|
|
425
|
+
root = _agent_readable_docs_root(_extension_root())
|
|
426
|
+
return {
|
|
427
|
+
"schema": "medical-notes-workbench.subagent-context-docs.v1",
|
|
428
|
+
"required_read_files": [
|
|
429
|
+
str(root / "docs" / "agent-prompt-hardening.md"),
|
|
430
|
+
str(root / "docs" / "knowledge-architect.md"),
|
|
431
|
+
str(root / "docs" / "semantic-linker.md"),
|
|
432
|
+
],
|
|
433
|
+
"forbidden_discovery_roots": [str(Path.home())],
|
|
434
|
+
"agent_instruction": (
|
|
435
|
+
"Leia os required_read_files empacotados antes de redigir a nota. "
|
|
436
|
+
"Não faça descoberta ampla em forbidden_discovery_roots; se um doc faltar, bloqueie como packaged_agent_template_unavailable."
|
|
437
|
+
),
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _planned_meaning_targets(note_plan: _TriageNotePlan) -> list[_PlannedMeaningTarget]:
|
|
442
|
+
targets: list[_PlannedMeaningTarget] = []
|
|
443
|
+
for item in note_plan.items:
|
|
444
|
+
if item.action != PLANNED_MEANING_ACTION:
|
|
445
|
+
continue
|
|
446
|
+
title = str(item.staged_title or item.title).strip()
|
|
447
|
+
if not title:
|
|
448
|
+
continue
|
|
449
|
+
targets.append(
|
|
450
|
+
_PlannedMeaningTarget(
|
|
451
|
+
id=item.id.strip(),
|
|
452
|
+
title=title,
|
|
453
|
+
target_key=normalize_key(title),
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
return targets
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _launchable_work_item(item: JsonObject, *, write_policy: str = "temp_note_allowed") -> JsonObject:
|
|
460
|
+
item["launchable"] = True
|
|
461
|
+
item["write_policy"] = write_policy
|
|
462
|
+
return item
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _non_launchable_blocked_item(item: JsonObject, *, write_policy: str = "no_temp_note") -> JsonObject:
|
|
466
|
+
item["launchable"] = False
|
|
467
|
+
item["write_policy"] = write_policy
|
|
468
|
+
item.pop("temp_dir", None)
|
|
469
|
+
item.pop("temp_output", None)
|
|
470
|
+
return item
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _duplicate_next_action(blocked_reason: str = "duplicate_planned_meaning_targets") -> str:
|
|
474
|
+
if blocked_reason == "canonical_merge_required":
|
|
475
|
+
return (
|
|
476
|
+
"Chame architect para merge canônico no alvo existente: gerar rewrite completo com delta validado, "
|
|
477
|
+
"ou ajustar a triagem para not_a_note se não houver delta."
|
|
478
|
+
)
|
|
479
|
+
if blocked_reason == "human_decision_required.ambiguous_canonical_target":
|
|
480
|
+
return (
|
|
481
|
+
"Escolha explicitamente o alvo canônico antes de lançar architects; depois "
|
|
482
|
+
"replaneje ou ajuste a triagem para planned_meaning/not_a_note."
|
|
483
|
+
)
|
|
484
|
+
return (
|
|
485
|
+
"Revise o note_plan antes de arquitetura: converta duplicatas para "
|
|
486
|
+
"not_a_note ou consolide fontes em um unico planned_meaning."
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _duplicate_blocked_reason(duplicate_targets: Sequence[_DuplicateTarget]) -> str:
|
|
491
|
+
if any(target.conflict_type == "ambiguous_existing_wiki_note" for target in duplicate_targets):
|
|
492
|
+
return "human_decision_required.ambiguous_canonical_target"
|
|
493
|
+
if any(target.conflict_type == "existing_wiki_note" for target in duplicate_targets):
|
|
494
|
+
return "canonical_merge_required"
|
|
495
|
+
return "duplicate_planned_meaning_targets"
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _decision_options_for_existing_paths(paths: Sequence[object]) -> list[JsonObject]:
|
|
499
|
+
options: list[JsonObject] = []
|
|
500
|
+
for index, path in enumerate(paths, start=1):
|
|
501
|
+
label = str(path)
|
|
502
|
+
options.append(
|
|
503
|
+
{
|
|
504
|
+
"id": f"use_existing_{index}",
|
|
505
|
+
"label": label,
|
|
506
|
+
"value": label,
|
|
507
|
+
"consequence": "Consolidar informação nova nesse alvo canônico ou marcar como not_a_note.",
|
|
508
|
+
}
|
|
509
|
+
)
|
|
510
|
+
return options
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _decision_options_for_planned_matches(matches: Sequence[_PlannedMatch]) -> list[JsonObject]:
|
|
514
|
+
return [
|
|
515
|
+
{
|
|
516
|
+
"id": "canonical_merge",
|
|
517
|
+
"label": "Fundir em uma nota canônica",
|
|
518
|
+
"value": "canonical_merge",
|
|
519
|
+
"consequence": "Um architect consolida todas as fontes e preserva múltiplas referências.",
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
"id": "split_triage",
|
|
523
|
+
"label": "Separar triagem",
|
|
524
|
+
"value": "split_triage",
|
|
525
|
+
"consequence": "Ajustar note_plan para separar temas ou remover duplicata antes da arquitetura.",
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
"id": "mark_not_a_note",
|
|
529
|
+
"label": "Marcar como já coberto",
|
|
530
|
+
"value": "not_a_note",
|
|
531
|
+
"consequence": "Atualizar note_plan como not_a_note quando a informação não exigir nota nova.",
|
|
532
|
+
},
|
|
533
|
+
] + [
|
|
534
|
+
{
|
|
535
|
+
"id": f"inspect_{index}",
|
|
536
|
+
"label": f"Inspecionar {Path(match.raw_file).name}",
|
|
537
|
+
"value": match.raw_file,
|
|
538
|
+
"consequence": "Usar este raw como evidência antes de escolher a rota.",
|
|
539
|
+
}
|
|
540
|
+
for index, match in enumerate(matches[:3], start=1)
|
|
541
|
+
]
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _planned_match_payloads(matches: Sequence[_PlannedMatch]) -> list[JsonObject]:
|
|
545
|
+
"""Serialize typed planned matches before they cross a JSON/public boundary."""
|
|
546
|
+
|
|
547
|
+
return [match.to_payload() for match in matches]
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _packet_for_ambiguous_target(
|
|
551
|
+
*,
|
|
552
|
+
target_key: str,
|
|
553
|
+
target_title: str,
|
|
554
|
+
options: Sequence[object],
|
|
555
|
+
planned_matches: list[_PlannedMatch] | None = None,
|
|
556
|
+
) -> JsonObject:
|
|
557
|
+
option_payload = (
|
|
558
|
+
_decision_options_for_existing_paths(options)
|
|
559
|
+
if options and not isinstance(options[0], dict)
|
|
560
|
+
else _decision_options_for_planned_matches(planned_matches or [])
|
|
561
|
+
)
|
|
562
|
+
return _ask_human_packet(
|
|
563
|
+
kind="ambiguous_canonical_target",
|
|
564
|
+
phase="architect",
|
|
565
|
+
blocked_reason="human_decision_required.ambiguous_canonical_target",
|
|
566
|
+
target_kind="wiki_note",
|
|
567
|
+
target_key=target_key,
|
|
568
|
+
question=f"Qual alvo canônico deve receber '{target_title}'?",
|
|
569
|
+
options=option_payload,
|
|
570
|
+
resume_action="Registrar a escolha no note_plan e reexecutar plan-subagents --phase architect.",
|
|
571
|
+
context={"planned_matches": _planned_match_payloads(planned_matches or [])},
|
|
572
|
+
evidence_summary=f"'{target_title}' tem mais de um alvo canônico plausível.",
|
|
573
|
+
developer_summary="Ambiguous planned_meaning target after accent/case normalization.",
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _packet_for_existing_canonical_target(target: _DuplicateTarget) -> JsonObject:
|
|
578
|
+
title = target.title
|
|
579
|
+
target_key = target.target_key
|
|
580
|
+
existing_paths = target.existing_paths
|
|
581
|
+
return _ask_human_packet(
|
|
582
|
+
kind="canonical_merge_required",
|
|
583
|
+
phase="architect",
|
|
584
|
+
blocked_reason="canonical_merge_required",
|
|
585
|
+
target_kind="existing_wiki_note",
|
|
586
|
+
target_key=target_key,
|
|
587
|
+
question=f"Como tratar a informação nova planejada para nota existente '{title}'?",
|
|
588
|
+
options=[
|
|
589
|
+
*_decision_options_for_existing_paths(existing_paths),
|
|
590
|
+
{
|
|
591
|
+
"id": "rename_new_note",
|
|
592
|
+
"label": "Criar nota separada com outro título",
|
|
593
|
+
"value": "rename_new_note",
|
|
594
|
+
"consequence": "Ajustar staged_title no note_plan e repetir arquitetura.",
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
resume_action="Escolher rota de merge/renomeação, ajustar note_plan e reexecutar plan-subagents --phase architect.",
|
|
598
|
+
context={"existing_paths": existing_paths, "target_title": title},
|
|
599
|
+
evidence_summary=f"'{title}' já existe na Wiki e pode receber merge ou nota separada.",
|
|
600
|
+
developer_summary="Existing canonical target requires an editorial route before architect fan-out.",
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _ask_human_packet(
|
|
605
|
+
*,
|
|
606
|
+
kind: str,
|
|
607
|
+
phase: str,
|
|
608
|
+
blocked_reason: str,
|
|
609
|
+
target_kind: str,
|
|
610
|
+
target_key: str,
|
|
611
|
+
question: str,
|
|
612
|
+
options: list[JsonObject],
|
|
613
|
+
resume_action: str,
|
|
614
|
+
context: JsonObject,
|
|
615
|
+
evidence_summary: str,
|
|
616
|
+
developer_summary: str,
|
|
617
|
+
) -> JsonObject:
|
|
618
|
+
recommended_option_id = _json_str_field(options[0], "id") if options else "inspect_first"
|
|
619
|
+
if not options:
|
|
620
|
+
options = [
|
|
621
|
+
{
|
|
622
|
+
"id": "inspect_first",
|
|
623
|
+
"label": "Inspecionar antes de escolher",
|
|
624
|
+
"value": "inspect_first",
|
|
625
|
+
"consequence": "Replanejar depois de revisar os candidatos.",
|
|
626
|
+
}
|
|
627
|
+
]
|
|
628
|
+
decision = WorkflowDecision(
|
|
629
|
+
kind="ask_human",
|
|
630
|
+
phase=phase,
|
|
631
|
+
reason_code=blocked_reason,
|
|
632
|
+
public_summary=question,
|
|
633
|
+
developer_summary=developer_summary,
|
|
634
|
+
evidence=[
|
|
635
|
+
DecisionEvidence(
|
|
636
|
+
summary=evidence_summary,
|
|
637
|
+
technical_code=blocked_reason,
|
|
638
|
+
source=phase,
|
|
639
|
+
candidates=[{"target_key": target_key, **context}],
|
|
640
|
+
risk="Escolha automatica pode fundir ou separar notas canônicas incorretamente.",
|
|
641
|
+
)
|
|
642
|
+
],
|
|
643
|
+
rejected_automations=[
|
|
644
|
+
RejectedAutomation(kind="auto_fix", reason_code="ambiguous_canonical_target", reason="Nao ha alvo unico dominante para corrigir automaticamente."),
|
|
645
|
+
RejectedAutomation(kind="auto_defer", reason_code="blocks_architect", reason="Pular a escolha impediria cobertura correta do raw chat."),
|
|
646
|
+
RejectedAutomation(kind="auto_plan", reason_code="plan_needs_canonical_target", reason="O plano precisa de um alvo canônico antes de lançar subagentes."),
|
|
647
|
+
],
|
|
648
|
+
next_action=resume_action,
|
|
649
|
+
resume_action=resume_action,
|
|
650
|
+
recommended_option_id=recommended_option_id,
|
|
651
|
+
options=options,
|
|
652
|
+
)
|
|
653
|
+
packet = decision.to_human_decision_packet()
|
|
654
|
+
packet["kind"] = kind
|
|
655
|
+
packet["type"] = kind
|
|
656
|
+
packet["target_kind"] = target_kind
|
|
657
|
+
packet["target_key"] = target_key
|
|
658
|
+
packet.setdefault("context", {}).update(context)
|
|
659
|
+
return packet
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _single_planned_meaning_target(parsed: _ArchitectParsedItem, target_key: str) -> bool:
|
|
663
|
+
targets = parsed.targets
|
|
664
|
+
if len(targets) != 1:
|
|
665
|
+
return False
|
|
666
|
+
return targets[0].target_key == target_key
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _find_note_plan_item(note_plan: _TriageNotePlan, item_id: str) -> JsonObject:
|
|
670
|
+
raw_items = _json_list_field(note_plan.public_payload(), "items")
|
|
671
|
+
for item in raw_items:
|
|
672
|
+
if isinstance(item, dict):
|
|
673
|
+
payload = JsonObjectAdapter.validate_python(item)
|
|
674
|
+
if _json_str_field(payload, "id").strip() == item_id:
|
|
675
|
+
return payload
|
|
676
|
+
return {}
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _artifact_payload_for_raw(config: MedConfig, raw_file: Path) -> JsonObject:
|
|
680
|
+
artifact_manifests = discover_artifact_manifests(raw_file, artifact_dir=config.artifact_dir)
|
|
681
|
+
payload: JsonObject = {
|
|
682
|
+
"artifact_manifest_count": len(artifact_manifests),
|
|
683
|
+
"artifact_count": sum(len(manifest.artifacts) for manifest in artifact_manifests),
|
|
684
|
+
}
|
|
685
|
+
if artifact_manifests:
|
|
686
|
+
payload["artifact_manifests"] = [manifest.to_json() for manifest in artifact_manifests]
|
|
687
|
+
return payload
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _canonical_merge_work_item(
|
|
691
|
+
config: MedConfig,
|
|
692
|
+
spec: JsonObject,
|
|
693
|
+
*,
|
|
694
|
+
target_key: str,
|
|
695
|
+
planned_matches: list[_PlannedMatch],
|
|
696
|
+
parsed_by_work_id: dict[str, _ArchitectParsedItem],
|
|
697
|
+
temp_root: Path,
|
|
698
|
+
index: int,
|
|
699
|
+
) -> JsonObject:
|
|
700
|
+
target_title = planned_matches[0].title
|
|
701
|
+
work_id = f"canonical-merge-{index:03d}-{_slug(target_title)}"
|
|
702
|
+
sources: list[JsonObject] = []
|
|
703
|
+
artifact_manifest_count = 0
|
|
704
|
+
artifact_count = 0
|
|
705
|
+
artifact_manifests: list[JsonObject] = []
|
|
706
|
+
for match in planned_matches:
|
|
707
|
+
parsed = parsed_by_work_id[match.work_id]
|
|
708
|
+
item = parsed.item
|
|
709
|
+
raw_file = Path(str(item["raw_file"]))
|
|
710
|
+
note_plan_item = _find_note_plan_item(parsed.note_plan, match.id)
|
|
711
|
+
source: JsonObject = {
|
|
712
|
+
"raw_file": str(raw_file),
|
|
713
|
+
"work_id": str(item["work_id"]),
|
|
714
|
+
"fonte_id": _json_str_field(item, "fonte_id"),
|
|
715
|
+
"titulo_triagem": _json_str_field(item, "titulo_triagem"),
|
|
716
|
+
"note_plan_item_id": match.id,
|
|
717
|
+
"planned_title": match.title,
|
|
718
|
+
"note_plan_item": note_plan_item,
|
|
719
|
+
}
|
|
720
|
+
artifact_payload = _artifact_payload_for_raw(config, raw_file)
|
|
721
|
+
artifact_manifest_count += int(artifact_payload["artifact_manifest_count"])
|
|
722
|
+
artifact_count += int(artifact_payload["artifact_count"])
|
|
723
|
+
artifact_manifests.extend(_json_object_list_field(artifact_payload, "artifact_manifests"))
|
|
724
|
+
sources.append(source)
|
|
725
|
+
|
|
726
|
+
temp_dir = temp_root / work_id
|
|
727
|
+
merge_plan_sources = [
|
|
728
|
+
{
|
|
729
|
+
"raw_file": source["raw_file"],
|
|
730
|
+
"note_plan_item_id": source["note_plan_item_id"],
|
|
731
|
+
"planned_title": source["planned_title"],
|
|
732
|
+
"fonte_id": source["fonte_id"],
|
|
733
|
+
}
|
|
734
|
+
for source in sources
|
|
735
|
+
]
|
|
736
|
+
item: JsonObject = {
|
|
737
|
+
"work_id": work_id,
|
|
738
|
+
"agent": spec["agent"],
|
|
739
|
+
"item_type": "canonical_merge",
|
|
740
|
+
"merge_action": "create_new_canonical_note",
|
|
741
|
+
"target_kind": "new_wiki_note",
|
|
742
|
+
"target_title": target_title,
|
|
743
|
+
"target_key": target_key,
|
|
744
|
+
"owner_key": f"target:{target_key}",
|
|
745
|
+
"source_count": len(sources),
|
|
746
|
+
"raw_files": [source["raw_file"] for source in sources],
|
|
747
|
+
"sources": sources,
|
|
748
|
+
"canonical_merge_plan": {
|
|
749
|
+
"schema": CANONICAL_MERGE_PLAN_SCHEMA,
|
|
750
|
+
"target_kind": "new_wiki_note",
|
|
751
|
+
"target_title": target_title,
|
|
752
|
+
"target_key": target_key,
|
|
753
|
+
"sources": merge_plan_sources,
|
|
754
|
+
"required_delta_per_source": True,
|
|
755
|
+
"required_multi_reference_provenance": True,
|
|
756
|
+
},
|
|
757
|
+
"artifact_manifest_count": artifact_manifest_count,
|
|
758
|
+
"artifact_count": artifact_count,
|
|
759
|
+
"temp_dir": str(temp_dir),
|
|
760
|
+
"temp_output": str(temp_dir / f"{_slug(target_title)}.md"),
|
|
761
|
+
}
|
|
762
|
+
if artifact_manifests:
|
|
763
|
+
item["artifact_manifests"] = artifact_manifests
|
|
764
|
+
return _launchable_work_item(item)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _existing_canonical_merge_work_item(
|
|
768
|
+
config: MedConfig,
|
|
769
|
+
spec: JsonObject,
|
|
770
|
+
*,
|
|
771
|
+
target_key: str,
|
|
772
|
+
planned_matches: list[_PlannedMatch],
|
|
773
|
+
parsed_by_work_id: dict[str, _ArchitectParsedItem],
|
|
774
|
+
existing_paths: Sequence[object],
|
|
775
|
+
temp_root: Path,
|
|
776
|
+
index: int,
|
|
777
|
+
) -> JsonObject:
|
|
778
|
+
target_title = planned_matches[0].title
|
|
779
|
+
existing_path = str(existing_paths[0])
|
|
780
|
+
target_path = config.wiki_dir / existing_path
|
|
781
|
+
work_id = f"canonical-existing-merge-{index:03d}-{_slug(target_path.stem)}"
|
|
782
|
+
sources: list[JsonObject] = []
|
|
783
|
+
artifact_manifest_count = 0
|
|
784
|
+
artifact_count = 0
|
|
785
|
+
artifact_manifests: list[JsonObject] = []
|
|
786
|
+
for match in planned_matches:
|
|
787
|
+
parsed = parsed_by_work_id[match.work_id]
|
|
788
|
+
item = parsed.item
|
|
789
|
+
raw_file = Path(str(item["raw_file"]))
|
|
790
|
+
note_plan_item = _find_note_plan_item(parsed.note_plan, match.id)
|
|
791
|
+
source: JsonObject = {
|
|
792
|
+
"raw_file": str(raw_file),
|
|
793
|
+
"work_id": str(item["work_id"]),
|
|
794
|
+
"fonte_id": _json_str_field(item, "fonte_id"),
|
|
795
|
+
"titulo_triagem": _json_str_field(item, "titulo_triagem"),
|
|
796
|
+
"note_plan_item_id": match.id,
|
|
797
|
+
"planned_title": match.title,
|
|
798
|
+
"note_plan_item": note_plan_item,
|
|
799
|
+
}
|
|
800
|
+
artifact_payload = _artifact_payload_for_raw(config, raw_file)
|
|
801
|
+
artifact_manifest_count += int(artifact_payload["artifact_manifest_count"])
|
|
802
|
+
artifact_count += int(artifact_payload["artifact_count"])
|
|
803
|
+
artifact_manifests.extend(_json_object_list_field(artifact_payload, "artifact_manifests"))
|
|
804
|
+
sources.append(source)
|
|
805
|
+
|
|
806
|
+
temp_dir = temp_root / work_id
|
|
807
|
+
merge_plan_sources = [
|
|
808
|
+
{
|
|
809
|
+
"raw_file": source["raw_file"],
|
|
810
|
+
"note_plan_item_id": source["note_plan_item_id"],
|
|
811
|
+
"planned_title": source["planned_title"],
|
|
812
|
+
"fonte_id": source["fonte_id"],
|
|
813
|
+
}
|
|
814
|
+
for source in sources
|
|
815
|
+
]
|
|
816
|
+
item: JsonObject = {
|
|
817
|
+
"work_id": work_id,
|
|
818
|
+
"agent": spec["agent"],
|
|
819
|
+
"item_type": "canonical_merge",
|
|
820
|
+
"merge_action": "update_existing_canonical_note",
|
|
821
|
+
"target_kind": "existing_wiki_note",
|
|
822
|
+
"target_title": target_path.stem,
|
|
823
|
+
"requested_title": target_title,
|
|
824
|
+
"target_key": target_key,
|
|
825
|
+
"target_path": str(target_path),
|
|
826
|
+
"existing_paths": [str(path) for path in existing_paths],
|
|
827
|
+
"owner_key": f"target:{target_key}",
|
|
828
|
+
"source_count": len(sources),
|
|
829
|
+
"raw_files": [source["raw_file"] for source in sources],
|
|
830
|
+
"sources": sources,
|
|
831
|
+
"canonical_merge_plan": {
|
|
832
|
+
"schema": CANONICAL_MERGE_PLAN_SCHEMA,
|
|
833
|
+
"target_kind": "existing_wiki_note",
|
|
834
|
+
"target_title": target_path.stem,
|
|
835
|
+
"requested_title": target_title,
|
|
836
|
+
"target_key": target_key,
|
|
837
|
+
"target_path": str(target_path),
|
|
838
|
+
"existing_paths": [str(path) for path in existing_paths],
|
|
839
|
+
"sources": merge_plan_sources,
|
|
840
|
+
"required_delta_per_source": True,
|
|
841
|
+
"required_multi_reference_provenance": True,
|
|
842
|
+
},
|
|
843
|
+
"apply_command": "apply-canonical-merge",
|
|
844
|
+
"artifact_manifest_count": artifact_manifest_count,
|
|
845
|
+
"artifact_count": artifact_count,
|
|
846
|
+
"temp_dir": str(temp_dir),
|
|
847
|
+
"temp_output": str(temp_dir / f"{target_path.stem}.rewrite.md"),
|
|
848
|
+
}
|
|
849
|
+
if artifact_manifests:
|
|
850
|
+
item["artifact_manifests"] = artifact_manifests
|
|
851
|
+
return _launchable_work_item(item, write_policy="existing_note_rewrite")
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def _canonical_merge_blocked_item(
|
|
855
|
+
*,
|
|
856
|
+
target_key: str,
|
|
857
|
+
planned_matches: list[_PlannedMatch],
|
|
858
|
+
reason: str,
|
|
859
|
+
message: str,
|
|
860
|
+
) -> JsonObject:
|
|
861
|
+
first_match = planned_matches[0]
|
|
862
|
+
item: JsonObject = {
|
|
863
|
+
"work_id": f"canonical-merge-blocked-{_slug(target_key)}",
|
|
864
|
+
"item_type": "canonical_merge",
|
|
865
|
+
"blocked_reason": reason,
|
|
866
|
+
"target_key": target_key,
|
|
867
|
+
"target_title": first_match.title,
|
|
868
|
+
"planned_matches": [match.to_payload() for match in planned_matches],
|
|
869
|
+
"reason": message,
|
|
870
|
+
"next_action": _duplicate_next_action(reason),
|
|
871
|
+
}
|
|
872
|
+
if reason == "human_decision_required.ambiguous_canonical_target":
|
|
873
|
+
packet = _packet_for_ambiguous_target(
|
|
874
|
+
target_key=target_key,
|
|
875
|
+
target_title=first_match.title or target_key,
|
|
876
|
+
options=[],
|
|
877
|
+
planned_matches=planned_matches,
|
|
878
|
+
)
|
|
879
|
+
item["human_decision_packet"] = packet
|
|
880
|
+
return _non_launchable_blocked_item(item)
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _decision_packets_from_blocked_items(blocked_items: list[JsonObject]) -> list[JsonObject]:
|
|
884
|
+
packets: list[JsonObject] = []
|
|
885
|
+
seen: set[str] = set()
|
|
886
|
+
for item in blocked_items:
|
|
887
|
+
packet = _json_object_field(item, "human_decision_packet")
|
|
888
|
+
if packet is not None:
|
|
889
|
+
packet_key = json.dumps(packet, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
890
|
+
if packet_key not in seen:
|
|
891
|
+
packets.append(packet)
|
|
892
|
+
seen.add(packet_key)
|
|
893
|
+
for packet in _json_object_list_field(item, "human_decision_packets"):
|
|
894
|
+
packet_key = json.dumps(packet, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
895
|
+
if packet_key in seen:
|
|
896
|
+
continue
|
|
897
|
+
packets.append(packet)
|
|
898
|
+
seen.add(packet_key)
|
|
899
|
+
return packets
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _is_canonical_merge_work_item(item: JsonObject) -> bool:
|
|
903
|
+
"""Count only typed work-item intent, not arbitrary payload text."""
|
|
904
|
+
|
|
905
|
+
return _json_str_field(item, "item_type") == "canonical_merge"
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def _with_decision_packets(payload: JsonObject, blocked_items: list[JsonObject]) -> JsonObject:
|
|
909
|
+
packets = _decision_packets_from_blocked_items(blocked_items)
|
|
910
|
+
if not packets:
|
|
911
|
+
return payload
|
|
912
|
+
payload["human_decision_packets"] = packets
|
|
913
|
+
if len(packets) == 1:
|
|
914
|
+
payload["human_decision_packet"] = packets[0]
|
|
915
|
+
return payload
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _process_chats_no_pending_directive() -> JsonObject:
|
|
919
|
+
return AgentDirective.model_validate(
|
|
920
|
+
{
|
|
921
|
+
"workflow": "/mednotes:process-chats",
|
|
922
|
+
"run_id": "process-chats-architect-no-pending",
|
|
923
|
+
"control": {
|
|
924
|
+
"status": "completed",
|
|
925
|
+
"state": "no_pending",
|
|
926
|
+
"phase": "architect",
|
|
927
|
+
"reason": "no_pending",
|
|
928
|
+
"capabilities": {"continue": False, "final_report": True},
|
|
929
|
+
"effects": [],
|
|
930
|
+
"blockers": [],
|
|
931
|
+
"resume": "",
|
|
932
|
+
"report": {"requires": ["public_report"]},
|
|
933
|
+
"limits": {"raw_content": False, "absolute_paths": False, "ad_hoc_scripts": False},
|
|
934
|
+
},
|
|
935
|
+
"summary": "Nenhum chat novo para processar.",
|
|
936
|
+
"instructions": [
|
|
937
|
+
"This completes /mednotes:process-chats because there are no new chats to process.",
|
|
938
|
+
"Use reports.public_report.lines as the public response.",
|
|
939
|
+
"Do not run validate-wiki, fix-wiki, run-linker, publish-batch or subagents.",
|
|
940
|
+
"Report zero vault mutations and do not expose local paths or file links.",
|
|
941
|
+
"Do not add a technical summary after reports.public_report.lines.",
|
|
942
|
+
"Do not mention internal terminal-state field names, schemas, hashes or local paths in the public response.",
|
|
943
|
+
],
|
|
944
|
+
}
|
|
945
|
+
).to_payload()
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def _process_chats_terminal_no_pending_contract() -> JsonObject:
|
|
949
|
+
directive = _process_chats_no_pending_directive()
|
|
950
|
+
public_report = WorkflowPublicReport(
|
|
951
|
+
workflow="/mednotes:process-chats",
|
|
952
|
+
run_id="process-chats-architect-no-pending",
|
|
953
|
+
headline="Nenhum chat novo para processar.",
|
|
954
|
+
lines=[
|
|
955
|
+
"Nenhuma nota foi publicada ou preparada.",
|
|
956
|
+
"Nenhum raw chat novo foi processado.",
|
|
957
|
+
"Nada foi escrito na Wiki.",
|
|
958
|
+
"Coverage/manifest não se aplicam porque não houve publicação.",
|
|
959
|
+
"O linker/grafo não precisa rodar porque nenhuma nota foi publicada.",
|
|
960
|
+
],
|
|
961
|
+
).to_payload()
|
|
962
|
+
return {
|
|
963
|
+
"workflow": "/mednotes:process-chats",
|
|
964
|
+
"process_chats_terminal_state": "no_pending",
|
|
965
|
+
"reports": {
|
|
966
|
+
"summary": "Nenhum chat novo para processar.",
|
|
967
|
+
"public_report": public_report,
|
|
968
|
+
},
|
|
969
|
+
"agent_directive": directive,
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def _plan_architect_subagents(
|
|
974
|
+
config: MedConfig,
|
|
975
|
+
spec: JsonObject,
|
|
976
|
+
rows: list[JsonObject],
|
|
977
|
+
*,
|
|
978
|
+
total_available_count: int,
|
|
979
|
+
concurrency: int,
|
|
980
|
+
temp_root: Path,
|
|
981
|
+
limit: int | None,
|
|
982
|
+
) -> JsonObject:
|
|
983
|
+
existing_targets = note_target_index(config.wiki_dir, as_relative=True)
|
|
984
|
+
parsed_items: list[_ArchitectParsedItem] = []
|
|
985
|
+
meaning_work_items: list[JsonObject] = []
|
|
986
|
+
blocked_items: list[JsonObject] = []
|
|
987
|
+
seen: set[str] = set()
|
|
988
|
+
|
|
989
|
+
typed_rows = [_RawChatPlanningRow.model_validate(row) for row in rows]
|
|
990
|
+
for index, row in enumerate(typed_rows, start=1):
|
|
991
|
+
raw_file = row.path
|
|
992
|
+
raw_key = str(Path(raw_file).expanduser())
|
|
993
|
+
if raw_key in seen:
|
|
994
|
+
continue
|
|
995
|
+
seen.add(raw_key)
|
|
996
|
+
work_id = f"architect-{index:03d}-{_slug(Path(raw_file).stem)}"
|
|
997
|
+
item: JsonObject = {
|
|
998
|
+
"work_id": work_id,
|
|
999
|
+
"agent": spec["agent"],
|
|
1000
|
+
"item_type": spec["item_type"],
|
|
1001
|
+
"raw_file": raw_file,
|
|
1002
|
+
"owner_key": raw_key,
|
|
1003
|
+
"titulo_triagem": row.titulo_triagem,
|
|
1004
|
+
"fonte_id": row.fonte_id,
|
|
1005
|
+
}
|
|
1006
|
+
try:
|
|
1007
|
+
raw_plan = _ReadNoteMeta.model_validate(read_note_meta(Path(raw_file))).note_plan
|
|
1008
|
+
if not raw_plan:
|
|
1009
|
+
raise ValidationError("Raw chat missing triage note_plan; rerun triage with --note-plan")
|
|
1010
|
+
note_plan = _TriageNotePlan.from_payload(parse_triage_note_plan(raw_plan, Path(raw_file)))
|
|
1011
|
+
except ValidationError as exc:
|
|
1012
|
+
item.update(
|
|
1013
|
+
{
|
|
1014
|
+
"blocked_reason": "missing_or_invalid_note_plan",
|
|
1015
|
+
"note_plan_error": str(exc),
|
|
1016
|
+
"next_action": "Refaça a triagem com --note-plan exaustivo antes de planejar arquitetura.",
|
|
1017
|
+
}
|
|
1018
|
+
)
|
|
1019
|
+
_non_launchable_blocked_item(item)
|
|
1020
|
+
blocked_items.append(item)
|
|
1021
|
+
continue
|
|
1022
|
+
|
|
1023
|
+
item["note_plan"] = note_plan.public_payload()
|
|
1024
|
+
item.update(note_plan_summary(note_plan.public_payload()))
|
|
1025
|
+
if note_plan.schema_ == TRIAGE_NOTE_PLAN_V2_SCHEMA:
|
|
1026
|
+
planner = _MeaningPlannerResult.model_validate(
|
|
1027
|
+
plan_meaning_work_items(
|
|
1028
|
+
config,
|
|
1029
|
+
note_plan.public_payload(),
|
|
1030
|
+
raw_file=Path(raw_file),
|
|
1031
|
+
temp_root=temp_root,
|
|
1032
|
+
agent=str(spec["agent"]),
|
|
1033
|
+
)
|
|
1034
|
+
)
|
|
1035
|
+
for blocked_item in planner.blocked_items:
|
|
1036
|
+
blocked_item.setdefault("agent", spec["agent"])
|
|
1037
|
+
blocked_item.setdefault("source_work_id", work_id)
|
|
1038
|
+
blocked_item.setdefault("owner_key", raw_key)
|
|
1039
|
+
blocked_item.setdefault(
|
|
1040
|
+
"next_action",
|
|
1041
|
+
planner.next_action or "Corrija o triage-note-plan.v2 antes do architect.",
|
|
1042
|
+
)
|
|
1043
|
+
blocked_item["note_plan"] = note_plan.public_payload()
|
|
1044
|
+
blocked_item.update(note_plan_summary(note_plan.public_payload()))
|
|
1045
|
+
_non_launchable_blocked_item(blocked_item)
|
|
1046
|
+
blocked_items.append(blocked_item)
|
|
1047
|
+
item["meaning_planner_work_items"] = list(planner.work_items)
|
|
1048
|
+
targets = _planned_meaning_targets(note_plan)
|
|
1049
|
+
duplicate_targets: list[_DuplicateTarget] = []
|
|
1050
|
+
for target in targets:
|
|
1051
|
+
matches = existing_targets[target.target_key] if target.target_key in existing_targets else []
|
|
1052
|
+
if matches:
|
|
1053
|
+
duplicate_targets.append(
|
|
1054
|
+
_DuplicateTarget(
|
|
1055
|
+
id=target.id,
|
|
1056
|
+
title=target.title,
|
|
1057
|
+
target_key=target.target_key,
|
|
1058
|
+
conflict_type="ambiguous_existing_wiki_note" if len(matches) > 1 else "existing_wiki_note",
|
|
1059
|
+
existing_paths=[str(path) for path in matches[:5]],
|
|
1060
|
+
)
|
|
1061
|
+
)
|
|
1062
|
+
parsed_items.append(
|
|
1063
|
+
_ArchitectParsedItem(
|
|
1064
|
+
item=item,
|
|
1065
|
+
note_plan=note_plan,
|
|
1066
|
+
targets=targets,
|
|
1067
|
+
duplicate_targets=duplicate_targets,
|
|
1068
|
+
)
|
|
1069
|
+
)
|
|
1070
|
+
continue
|
|
1071
|
+
targets = _planned_meaning_targets(note_plan)
|
|
1072
|
+
duplicate_targets: list[_DuplicateTarget] = []
|
|
1073
|
+
for target in targets:
|
|
1074
|
+
matches = existing_targets[target.target_key] if target.target_key in existing_targets else []
|
|
1075
|
+
if matches:
|
|
1076
|
+
duplicate_targets.append(
|
|
1077
|
+
_DuplicateTarget(
|
|
1078
|
+
id=target.id,
|
|
1079
|
+
title=target.title,
|
|
1080
|
+
target_key=target.target_key,
|
|
1081
|
+
conflict_type="ambiguous_existing_wiki_note" if len(matches) > 1 else "existing_wiki_note",
|
|
1082
|
+
existing_paths=[str(path) for path in matches[:5]],
|
|
1083
|
+
)
|
|
1084
|
+
)
|
|
1085
|
+
parsed_items.append(
|
|
1086
|
+
_ArchitectParsedItem(
|
|
1087
|
+
item=item,
|
|
1088
|
+
note_plan=note_plan,
|
|
1089
|
+
targets=targets,
|
|
1090
|
+
duplicate_targets=duplicate_targets,
|
|
1091
|
+
)
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
planned_by_key: dict[str, list[_PlannedMatch]] = {}
|
|
1095
|
+
parsed_by_work_id: dict[str, _ArchitectParsedItem] = {}
|
|
1096
|
+
for parsed in parsed_items:
|
|
1097
|
+
item = parsed.item
|
|
1098
|
+
parsed_by_work_id[str(item["work_id"])] = parsed
|
|
1099
|
+
for target in parsed.targets:
|
|
1100
|
+
planned_by_key.setdefault(target.target_key, []).append(
|
|
1101
|
+
_PlannedMatch(
|
|
1102
|
+
raw_file=str(item["raw_file"]),
|
|
1103
|
+
work_id=str(item["work_id"]),
|
|
1104
|
+
id=target.id,
|
|
1105
|
+
title=target.title,
|
|
1106
|
+
)
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
work_items: list[JsonObject] = list(meaning_work_items)
|
|
1110
|
+
consumed_work_ids: set[str] = set()
|
|
1111
|
+
canonical_merge_index = 1
|
|
1112
|
+
for target_key, planned_matches in planned_by_key.items():
|
|
1113
|
+
if len(planned_matches) <= 1 or target_key in existing_targets:
|
|
1114
|
+
continue
|
|
1115
|
+
group = [parsed_by_work_id[match.work_id] for match in planned_matches]
|
|
1116
|
+
if not all(_single_planned_meaning_target(parsed, target_key) for parsed in group):
|
|
1117
|
+
blocked_items.append(
|
|
1118
|
+
_canonical_merge_blocked_item(
|
|
1119
|
+
target_key=target_key,
|
|
1120
|
+
planned_matches=planned_matches,
|
|
1121
|
+
reason="human_decision_required.ambiguous_canonical_target",
|
|
1122
|
+
message=(
|
|
1123
|
+
"At least one raw chat has additional planned_meaning targets; choose whether to "
|
|
1124
|
+
"split triage or make one canonical merge work item before spawning architects."
|
|
1125
|
+
),
|
|
1126
|
+
)
|
|
1127
|
+
)
|
|
1128
|
+
consumed_work_ids.update(match.work_id for match in planned_matches)
|
|
1129
|
+
continue
|
|
1130
|
+
try:
|
|
1131
|
+
work_items.append(
|
|
1132
|
+
_canonical_merge_work_item(
|
|
1133
|
+
config,
|
|
1134
|
+
spec,
|
|
1135
|
+
target_key=target_key,
|
|
1136
|
+
planned_matches=planned_matches,
|
|
1137
|
+
parsed_by_work_id=parsed_by_work_id,
|
|
1138
|
+
temp_root=temp_root,
|
|
1139
|
+
index=canonical_merge_index,
|
|
1140
|
+
)
|
|
1141
|
+
)
|
|
1142
|
+
except MedOpsError as exc:
|
|
1143
|
+
blocked_items.append(
|
|
1144
|
+
_canonical_merge_blocked_item(
|
|
1145
|
+
target_key=target_key,
|
|
1146
|
+
planned_matches=planned_matches,
|
|
1147
|
+
reason="missing_or_invalid_artifact_manifest",
|
|
1148
|
+
message=str(exc),
|
|
1149
|
+
)
|
|
1150
|
+
)
|
|
1151
|
+
consumed_work_ids.update(match.work_id for match in planned_matches)
|
|
1152
|
+
canonical_merge_index += 1
|
|
1153
|
+
|
|
1154
|
+
existing_merge_index = 1
|
|
1155
|
+
for target_key, planned_matches in planned_by_key.items():
|
|
1156
|
+
existing_matches = existing_targets[target_key] if target_key in existing_targets else []
|
|
1157
|
+
if len(existing_matches) != 1:
|
|
1158
|
+
continue
|
|
1159
|
+
group = [parsed_by_work_id[match.work_id] for match in planned_matches]
|
|
1160
|
+
if not all(_single_planned_meaning_target(parsed, target_key) for parsed in group):
|
|
1161
|
+
blocked_items.append(
|
|
1162
|
+
_canonical_merge_blocked_item(
|
|
1163
|
+
target_key=target_key,
|
|
1164
|
+
planned_matches=planned_matches,
|
|
1165
|
+
reason="human_decision_required.ambiguous_canonical_target",
|
|
1166
|
+
message=(
|
|
1167
|
+
"At least one raw chat has additional planned_meaning targets; choose whether to "
|
|
1168
|
+
"split triage or merge only the intended delta into the existing canonical note."
|
|
1169
|
+
),
|
|
1170
|
+
)
|
|
1171
|
+
)
|
|
1172
|
+
consumed_work_ids.update(match.work_id for match in planned_matches)
|
|
1173
|
+
continue
|
|
1174
|
+
try:
|
|
1175
|
+
work_items.append(
|
|
1176
|
+
_existing_canonical_merge_work_item(
|
|
1177
|
+
config,
|
|
1178
|
+
spec,
|
|
1179
|
+
target_key=target_key,
|
|
1180
|
+
planned_matches=planned_matches,
|
|
1181
|
+
parsed_by_work_id=parsed_by_work_id,
|
|
1182
|
+
existing_paths=existing_matches,
|
|
1183
|
+
temp_root=temp_root,
|
|
1184
|
+
index=existing_merge_index,
|
|
1185
|
+
)
|
|
1186
|
+
)
|
|
1187
|
+
except MedOpsError as exc:
|
|
1188
|
+
blocked_items.append(
|
|
1189
|
+
_canonical_merge_blocked_item(
|
|
1190
|
+
target_key=target_key,
|
|
1191
|
+
planned_matches=planned_matches,
|
|
1192
|
+
reason="missing_or_invalid_artifact_manifest",
|
|
1193
|
+
message=str(exc),
|
|
1194
|
+
)
|
|
1195
|
+
)
|
|
1196
|
+
consumed_work_ids.update(match.work_id for match in planned_matches)
|
|
1197
|
+
existing_merge_index += 1
|
|
1198
|
+
|
|
1199
|
+
for parsed in parsed_items:
|
|
1200
|
+
item = parsed.item
|
|
1201
|
+
if str(item["work_id"]) in consumed_work_ids:
|
|
1202
|
+
continue
|
|
1203
|
+
duplicate_targets: list[_DuplicateTarget] = [
|
|
1204
|
+
target if isinstance(target, _DuplicateTarget) else _DuplicateTarget.model_validate(target)
|
|
1205
|
+
for target in parsed.duplicate_targets
|
|
1206
|
+
]
|
|
1207
|
+
for target in parsed.targets:
|
|
1208
|
+
planned_matches = planned_by_key[target.target_key] if target.target_key in planned_by_key else []
|
|
1209
|
+
if len(planned_matches) > 1:
|
|
1210
|
+
duplicate_targets.append(
|
|
1211
|
+
_DuplicateTarget(
|
|
1212
|
+
id=target.id,
|
|
1213
|
+
title=target.title,
|
|
1214
|
+
target_key=target.target_key,
|
|
1215
|
+
conflict_type="planned_in_batch",
|
|
1216
|
+
planned_matches=planned_matches,
|
|
1217
|
+
)
|
|
1218
|
+
)
|
|
1219
|
+
if duplicate_targets:
|
|
1220
|
+
blocked_reason = _duplicate_blocked_reason(duplicate_targets)
|
|
1221
|
+
item.update(
|
|
1222
|
+
{
|
|
1223
|
+
"blocked_reason": blocked_reason,
|
|
1224
|
+
"duplicate_targets": [target.to_payload() for target in duplicate_targets],
|
|
1225
|
+
"next_action": _duplicate_next_action(blocked_reason),
|
|
1226
|
+
}
|
|
1227
|
+
)
|
|
1228
|
+
if blocked_reason == "canonical_merge_required":
|
|
1229
|
+
packet = _packet_for_existing_canonical_target(duplicate_targets[0])
|
|
1230
|
+
item["canonical_merge"] = {
|
|
1231
|
+
"schema": CANONICAL_MERGE_PLAN_SCHEMA,
|
|
1232
|
+
"target_kind": "existing_wiki_note",
|
|
1233
|
+
"target_title": duplicate_targets[0].title,
|
|
1234
|
+
"target_key": duplicate_targets[0].target_key,
|
|
1235
|
+
"existing_paths": duplicate_targets[0].existing_paths,
|
|
1236
|
+
}
|
|
1237
|
+
item["human_decision_packet"] = packet
|
|
1238
|
+
elif blocked_reason == "human_decision_required.ambiguous_canonical_target":
|
|
1239
|
+
packets = [
|
|
1240
|
+
_packet_for_ambiguous_target(
|
|
1241
|
+
target_key=target.target_key,
|
|
1242
|
+
target_title=target.title or target.target_key,
|
|
1243
|
+
options=target.existing_paths,
|
|
1244
|
+
planned_matches=target.planned_matches,
|
|
1245
|
+
)
|
|
1246
|
+
for target in duplicate_targets
|
|
1247
|
+
if target.conflict_type == "ambiguous_existing_wiki_note"
|
|
1248
|
+
]
|
|
1249
|
+
if not packets:
|
|
1250
|
+
packets = [
|
|
1251
|
+
_packet_for_ambiguous_target(
|
|
1252
|
+
target_key=target.target_key,
|
|
1253
|
+
target_title=target.title or target.target_key,
|
|
1254
|
+
options=[],
|
|
1255
|
+
planned_matches=target.planned_matches,
|
|
1256
|
+
)
|
|
1257
|
+
for target in duplicate_targets
|
|
1258
|
+
if target.conflict_type == "planned_in_batch"
|
|
1259
|
+
]
|
|
1260
|
+
item["human_decision_packets"] = packets
|
|
1261
|
+
_non_launchable_blocked_item(item)
|
|
1262
|
+
blocked_items.append(item)
|
|
1263
|
+
continue
|
|
1264
|
+
|
|
1265
|
+
meaning_items = _json_object_list_field(item, "meaning_planner_work_items")
|
|
1266
|
+
if meaning_items:
|
|
1267
|
+
for planned in meaning_items:
|
|
1268
|
+
work_item = dict(planned)
|
|
1269
|
+
work_item["source_work_id"] = item["work_id"]
|
|
1270
|
+
work_item["titulo_triagem"] = _json_str_field(item, "titulo_triagem")
|
|
1271
|
+
work_item["fonte_id"] = _json_str_field(item, "fonte_id")
|
|
1272
|
+
work_item["note_plan"] = item["note_plan"]
|
|
1273
|
+
work_item.update(note_plan_summary(parsed.note_plan.public_payload()))
|
|
1274
|
+
target_path = _json_str_field(work_item, "target_path")
|
|
1275
|
+
note_plan_item_id = _json_str_field(work_item, "note_plan_item_id")
|
|
1276
|
+
work_item.setdefault(
|
|
1277
|
+
"owner_key",
|
|
1278
|
+
target_path or f"meaning:{note_plan_item_id or item['owner_key']}",
|
|
1279
|
+
)
|
|
1280
|
+
try:
|
|
1281
|
+
work_item.update(_artifact_payload_for_raw(config, Path(item["raw_file"])))
|
|
1282
|
+
except MedOpsError as exc:
|
|
1283
|
+
work_item.update(
|
|
1284
|
+
{
|
|
1285
|
+
"blocked_reason": "missing_or_invalid_artifact_manifest",
|
|
1286
|
+
"artifact_manifest_error": str(exc),
|
|
1287
|
+
"next_action": (
|
|
1288
|
+
"Corrija o manifesto HTML do Gemini ou remova a dependência antes de lançar architects."
|
|
1289
|
+
),
|
|
1290
|
+
}
|
|
1291
|
+
)
|
|
1292
|
+
_non_launchable_blocked_item(work_item)
|
|
1293
|
+
blocked_items.append(work_item)
|
|
1294
|
+
continue
|
|
1295
|
+
write_policy = (
|
|
1296
|
+
"existing_note_rewrite"
|
|
1297
|
+
if _json_str_field(work_item, "target_kind") == "existing_wiki_note"
|
|
1298
|
+
else "temp_note_allowed"
|
|
1299
|
+
)
|
|
1300
|
+
_launchable_work_item(work_item, write_policy=write_policy)
|
|
1301
|
+
work_items.append(work_item)
|
|
1302
|
+
continue
|
|
1303
|
+
|
|
1304
|
+
try:
|
|
1305
|
+
artifact_payload = _artifact_payload_for_raw(config, Path(item["raw_file"]))
|
|
1306
|
+
except MedOpsError as exc:
|
|
1307
|
+
item.update(
|
|
1308
|
+
{
|
|
1309
|
+
"blocked_reason": "missing_or_invalid_artifact_manifest",
|
|
1310
|
+
"artifact_manifest_error": str(exc),
|
|
1311
|
+
"next_action": "Corrija o manifesto HTML do Gemini ou remova a dependência antes de lançar architects.",
|
|
1312
|
+
}
|
|
1313
|
+
)
|
|
1314
|
+
_non_launchable_blocked_item(item)
|
|
1315
|
+
blocked_items.append(item)
|
|
1316
|
+
continue
|
|
1317
|
+
item.update(artifact_payload)
|
|
1318
|
+
item["temp_dir"] = str(temp_root / item["work_id"])
|
|
1319
|
+
_launchable_work_item(item)
|
|
1320
|
+
work_items.append(item)
|
|
1321
|
+
|
|
1322
|
+
batches = _batch_refs(work_items, concurrency)
|
|
1323
|
+
status, next_action, _blocked_requires_attention = plan_status(
|
|
1324
|
+
item_count=len(work_items),
|
|
1325
|
+
blocked_item_count=len(blocked_items),
|
|
1326
|
+
)
|
|
1327
|
+
human_decision_required = bool(_decision_packets_from_blocked_items(blocked_items))
|
|
1328
|
+
payload = _with_decision_packets({
|
|
1329
|
+
"schema": SUBAGENT_PLAN_SCHEMA,
|
|
1330
|
+
"phase": "architect",
|
|
1331
|
+
"agent": spec["agent"],
|
|
1332
|
+
"unit": spec["unit"],
|
|
1333
|
+
"max_concurrency": concurrency,
|
|
1334
|
+
"item_count": len(work_items),
|
|
1335
|
+
"total_available_count": total_available_count,
|
|
1336
|
+
"blocked_item_count": len(blocked_items),
|
|
1337
|
+
"blocked_items": blocked_items,
|
|
1338
|
+
"canonical_merge_item_count": sum(1 for item in work_items if _is_canonical_merge_work_item(item)),
|
|
1339
|
+
"limit": limit,
|
|
1340
|
+
"truncated": limit is not None and len(rows) < total_available_count,
|
|
1341
|
+
"parallel_safe": len(work_items) > 1,
|
|
1342
|
+
"launch_source": "work_items",
|
|
1343
|
+
"batch_contract": "batches contain work_id references only; never spawn from both work_items and batch references.",
|
|
1344
|
+
"work_items": work_items,
|
|
1345
|
+
"batches": batches,
|
|
1346
|
+
"rules": [
|
|
1347
|
+
"Spawn at most one subagent per work_item.owner_key.",
|
|
1348
|
+
"Never spawn multiple subagents for the same raw chat or generated note.",
|
|
1349
|
+
"Use work_items as the only full launch payload; batches only group existing work_ids.",
|
|
1350
|
+
"Only work_items with launchable=true may be sent to med-knowledge-architect; blocked_items are stop packets.",
|
|
1351
|
+
"Canonical merges into an existing Wiki note are launchable architect work_items with write_policy=existing_note_rewrite and must be applied with apply-canonical-merge.",
|
|
1352
|
+
"Blocked architect items with write_policy=no_temp_note must not produce temp Markdown and must not be deferred to fix-wiki.",
|
|
1353
|
+
"Do not split one raw chat across multiple med-knowledge-architect agents.",
|
|
1354
|
+
"Architect work_items must follow the triage-authored note_plan exactly.",
|
|
1355
|
+
"raw-coverage.v1 includes only coverage-bearing v2 items: planned_meaning and not_a_note. Do not include attach_to_planned_meaning or needs_context as raw-coverage items; attach details must be folded into the target note, and needs_context blocks before architect.",
|
|
1356
|
+
"raw-coverage.v1 must carry raw_file, exhaustive=true, items[], and the same batch_id/run_id/source_artifact_hash metadata as the note_plan when present.",
|
|
1357
|
+
"Architect planning blocks planned_meaning targets that duplicate existing Wiki notes or another planned raw chat after accent/case normalization.",
|
|
1358
|
+
"When several simple raw chats target the same new note, plan one canonical_merge work_item owned by target_key.",
|
|
1359
|
+
"Canonical merge work_items must preserve new information from every source and report delta_per_source plus multi-reference provenance.",
|
|
1360
|
+
"Every architect result must include an exhaustive raw coverage inventory before staging.",
|
|
1361
|
+
"If artifact_manifests is non-empty, the staged note group for that raw chat must cover every listed artifact: HTML needs iframe/link/provenance, image needs Markdown embed/Figura caption/provenance.",
|
|
1362
|
+
"Do not launch more subagents than item_count or max_concurrency.",
|
|
1363
|
+
"If item_count is 0 or 1, there is no useful fan-out for this phase.",
|
|
1364
|
+
"When limit is set, spawn only the returned work_items",
|
|
1365
|
+
"Rerun planning after serial consolidation before launching more.",
|
|
1366
|
+
"Run serial consolidation after each batch returns.",
|
|
1367
|
+
],
|
|
1368
|
+
"serial_after": spec["serial_after"],
|
|
1369
|
+
"canonical_parent_commands": spec["canonical_parent_commands"],
|
|
1370
|
+
}, blocked_items)
|
|
1371
|
+
terminal_no_pending = not work_items and not blocked_items
|
|
1372
|
+
if terminal_no_pending:
|
|
1373
|
+
payload.update(_process_chats_terminal_no_pending_contract())
|
|
1374
|
+
payload["parent_applies_outputs"] = False
|
|
1375
|
+
payload["serial_after"] = ["terminal no-op: write the final no-pending report and stop"]
|
|
1376
|
+
payload["canonical_parent_commands"] = [
|
|
1377
|
+
"terminal no-op: no publish/link/fix command is valid when there are no new chats to process"
|
|
1378
|
+
]
|
|
1379
|
+
return _typed_subagent_plan_payload(annotate_payload(payload,
|
|
1380
|
+
phase="architect",
|
|
1381
|
+
status=status,
|
|
1382
|
+
blocked_reason="preconditions_failed" if blocked_items and not work_items else "",
|
|
1383
|
+
next_action="" if terminal_no_pending else next_action,
|
|
1384
|
+
required_inputs=[] if terminal_no_pending else PROCESS_CHATS_REQUIRED_INPUTS,
|
|
1385
|
+
human_decision_required=human_decision_required,
|
|
1386
|
+
))
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def plan_subagents(
|
|
1390
|
+
config: MedConfig,
|
|
1391
|
+
phase: str,
|
|
1392
|
+
max_concurrency: int | None = None,
|
|
1393
|
+
temp_root: Path | None = None,
|
|
1394
|
+
limit: int | None = None,
|
|
1395
|
+
fix_wiki_plan_path: Path | None = None,
|
|
1396
|
+
style_audit: JsonObject | None = None,
|
|
1397
|
+
) -> JsonObject:
|
|
1398
|
+
specs: dict[str, JsonObject] = {
|
|
1399
|
+
"triage": {
|
|
1400
|
+
"agent": "med-chat-triager",
|
|
1401
|
+
"mode": "pending",
|
|
1402
|
+
"default_max_concurrency": configured_subagent_max_concurrency(config, "triage"),
|
|
1403
|
+
"item_type": "raw_chat",
|
|
1404
|
+
"unit": "one pending raw chat per subagent",
|
|
1405
|
+
"serial_after": [
|
|
1406
|
+
"official subagent runner saves the top-level triager output to work_item.triager_output_path and writes a signed subagent-run-receipt.v1 for that exact output",
|
|
1407
|
+
"parent extracts note_plan to work_item.note_plan_path, writes eval to work_item.triager_eval_path with --subagent-run-receipt and --require-subagent-run-receipt, and only applies triage when triager-prompt-eval.v1 passes and the signed receipt/output/note_plan chain revalidates",
|
|
1408
|
+
"parent must not create, edit, re-sign, or patch subagent-run-receipt.v1; missing/invalid receipt means re-run the packaged triager through the official runner",
|
|
1409
|
+
"parent never patches the triager output or note_plan by hand; failed eval means re-run the triager with error_context or stop",
|
|
1410
|
+
"parent does not read raw chat content before plan-subagents returns work_items and does not write triage artifacts under repo-root tmp/",
|
|
1411
|
+
"parent refreshes list-triados before architect planning",
|
|
1412
|
+
],
|
|
1413
|
+
"canonical_parent_commands": [
|
|
1414
|
+
'eval triager output (triager-prompt-eval.v1): uv run python "<wiki/cli.py>" eval-triager-output --raw-file "<raw_file>" --output "<triager-output.json>" --subagent-run-receipt "<subagent-run-receipt.json>" --require-subagent-run-receipt --report "<triager-eval.json>" --json',
|
|
1415
|
+
'triage: uv run python "<wiki/cli.py>" triage --raw-file "<raw_file>" --tipo medicina --titulo "<titulo_triagem>" --fonte-id "<fonte_id>" --note-plan "<note-plan.json>" --triager-eval "<triager-eval.json>" --json',
|
|
1416
|
+
'discard: uv run python "<wiki/cli.py>" discard --raw-file "<raw_file>" --reason "<reason>"',
|
|
1417
|
+
],
|
|
1418
|
+
},
|
|
1419
|
+
"architect": {
|
|
1420
|
+
"agent": "med-knowledge-architect",
|
|
1421
|
+
"mode": "triados",
|
|
1422
|
+
"default_max_concurrency": configured_subagent_max_concurrency(config, "architect"),
|
|
1423
|
+
"item_type": "triaged_raw_chat",
|
|
1424
|
+
"unit": "one triaged raw chat per subagent, or one canonical merge target; all notes split from a raw chat stay together",
|
|
1425
|
+
"serial_after": [
|
|
1426
|
+
"parent validates/fixes each returned temp note or existing-note rewrite",
|
|
1427
|
+
"parent stages new notes with wiki/cli.py stage-note and the architect coverage inventory",
|
|
1428
|
+
"parent applies existing-note canonical rewrites with wiki/cli.py apply-canonical-merge",
|
|
1429
|
+
"catalog, dry-run, guard, publish and linker stay serial for staged new notes",
|
|
1430
|
+
],
|
|
1431
|
+
"canonical_parent_commands": [
|
|
1432
|
+
'validate-note: uv run python "<wiki/cli.py>" validate-note --content "<temp.md>" --title "<title>" --raw-file "<raw_file>" --json',
|
|
1433
|
+
'fix-note: uv run python "<wiki/cli.py>" fix-note --content "<temp.md>" --title "<title>" --raw-file "<raw_file>" --output "<temp.md>" --json',
|
|
1434
|
+
'apply canonical merge dry-run: uv run python "<wiki/cli.py>" apply-canonical-merge --target "<existing-note.md>" --content "<rewrite.md>" --coverage "<coverage.json>" --dry-run --json',
|
|
1435
|
+
'apply canonical merge: uv run python "<wiki/cli.py>" apply-canonical-merge --target "<existing-note.md>" --content "<rewrite.md>" --coverage "<coverage.json>" --json',
|
|
1436
|
+
'stage-note: uv run python "<wiki/cli.py>" stage-note --manifest "<manifest.json>" --raw-file "<raw_file>" --coverage "<coverage.json>" --taxonomy "<taxonomy>" --title "<title>" --content "<temp.md>"',
|
|
1437
|
+
'publish dry-run: uv run python "<wiki/cli.py>" publish-batch --manifest "<manifest.json>" --dry-run',
|
|
1438
|
+
'publish: uv run python "<wiki/cli.py>" publish-batch --manifest "<manifest.json>"',
|
|
1439
|
+
'diagnose links: uv run python "<wiki/cli.py>" run-linker --diagnose --json',
|
|
1440
|
+
'apply links if diagnosis is safe: uv run python "<wiki/cli.py>" run-linker --apply --diagnosis "<link-diagnosis.json>" --json',
|
|
1441
|
+
],
|
|
1442
|
+
},
|
|
1443
|
+
"style-rewrite": {
|
|
1444
|
+
"agent": "med-knowledge-architect",
|
|
1445
|
+
"mode": "wiki_style_rewrite",
|
|
1446
|
+
"default_max_concurrency": configured_subagent_max_concurrency(config, "style-rewrite"),
|
|
1447
|
+
"item_type": "wiki_note_style_rewrite",
|
|
1448
|
+
"unit": "one existing Wiki_Medicina note per subagent; each target path is unique",
|
|
1449
|
+
"serial_after": [
|
|
1450
|
+
"parent applies each returned temp rewrite atomically with wiki/cli.py apply-specialist-style-rewrite",
|
|
1451
|
+
"parent refreshes the next style-rewrite batch with plan-subagents until the style queue is empty",
|
|
1452
|
+
"parent runs full fix-wiki verification once after the style queue is empty",
|
|
1453
|
+
],
|
|
1454
|
+
"canonical_parent_commands": [
|
|
1455
|
+
"Gemini CLI specialist rewrite: consume the call_specialist_model WorkflowEffect with one current_batch_items entry and the packaged med-knowledge-architect agent",
|
|
1456
|
+
'AGY specialist receipt finalization after packaged invoke_subagent: uv run python "<wiki/cli.py>" finalize-agy-specialist-task --plan "<style-rewrite-plan.json>" --work-id "<work_id>" --transcript "<agy-transcript-or-task-log>" [--runtime-log "<agy-cli.log>"] --json',
|
|
1457
|
+
'OpenCode specialist receipt finalization after native task: uv run python "<wiki/cli.py>" finalize-opencode-specialist-task --plan "<style-rewrite-plan.json>" --work-id "<work_id>" --json',
|
|
1458
|
+
'apply specialist rewrite: uv run python "<wiki/cli.py>" apply-specialist-style-rewrite --plan "<style-rewrite-plan.json>" --manifest "<style-rewrite-manifest.json>" --work-id "<work_id>" --specialist-run-receipt "<specialist-task-run-receipt.json>" --json',
|
|
1459
|
+
],
|
|
1460
|
+
},
|
|
1461
|
+
"note-merge": {
|
|
1462
|
+
"agent": "med-knowledge-architect",
|
|
1463
|
+
"mode": "wiki_note_merge",
|
|
1464
|
+
"default_max_concurrency": configured_subagent_max_concurrency(config, "note-merge"),
|
|
1465
|
+
"item_type": "wiki_note_merge",
|
|
1466
|
+
"unit": "one semantic note merge group per subagent; title/stem duplicates are not sufficient",
|
|
1467
|
+
"serial_after": [
|
|
1468
|
+
"parent validates each returned merge with wiki/cli.py apply-note-merge --dry-run",
|
|
1469
|
+
"parent applies accepted merges serially with wiki/cli.py apply-note-merge",
|
|
1470
|
+
"parent runs /mednotes:link once after accepted note merges",
|
|
1471
|
+
],
|
|
1472
|
+
"canonical_parent_commands": [
|
|
1473
|
+
'apply merge dry-run: uv run python "<wiki/cli.py>" apply-note-merge --plan "<plan.json>" --content "<merged.md>" --dry-run --json',
|
|
1474
|
+
'apply merge: uv run python "<wiki/cli.py>" apply-note-merge --plan "<plan.json>" --content "<merged.md>" --json',
|
|
1475
|
+
],
|
|
1476
|
+
},
|
|
1477
|
+
"vocabulary-curation": {
|
|
1478
|
+
"agent": "med-link-graph-curator",
|
|
1479
|
+
"mode": "vocabulary_curation",
|
|
1480
|
+
"default_max_concurrency": configured_subagent_max_concurrency(config, "vocabulary-curation"),
|
|
1481
|
+
"item_type": "vocabulary_semantic_ingestion",
|
|
1482
|
+
"unit": "one pending vocabulary note per subagent",
|
|
1483
|
+
"serial_after": [
|
|
1484
|
+
"parent collects note-semantic-ingestion.v1 outputs",
|
|
1485
|
+
"parent writes vocabulary-curator-batch-output-manifest.v1",
|
|
1486
|
+
"parent runs wiki/cli.py eval-curator-batch",
|
|
1487
|
+
"parent applies outputs with wiki/cli.py apply-curator-batch --prompt-eval",
|
|
1488
|
+
],
|
|
1489
|
+
"canonical_parent_commands": [
|
|
1490
|
+
'eval curator batch: uv run python "<wiki/cli.py>" eval-curator-batch --plan "<plan.json>" --outputs "<manifest.json>" --report "<curator-prompt-eval.json>" --json',
|
|
1491
|
+
'apply curator batch: uv run python "<wiki/cli.py>" apply-curator-batch --plan "<plan.json>" --outputs "<manifest.json>" --prompt-eval "<curator-prompt-eval.json>" --receipt "<receipt.json>" --json',
|
|
1492
|
+
],
|
|
1493
|
+
},
|
|
1494
|
+
"atomicity-split": {
|
|
1495
|
+
"agent": "med-knowledge-architect",
|
|
1496
|
+
"mode": "wiki_atomicity_split",
|
|
1497
|
+
"default_max_concurrency": configured_subagent_max_concurrency(config, "atomicity-split"),
|
|
1498
|
+
"item_type": "wiki_atomicity_split",
|
|
1499
|
+
"unit": "one non-atomic source note per subagent",
|
|
1500
|
+
"serial_after": [
|
|
1501
|
+
"parent collects atomicity-split-bundle.v1 outputs",
|
|
1502
|
+
"parent applies accepted bundles serially with wiki/cli.py apply-atomicity-split",
|
|
1503
|
+
"parent runs the linker once per parent batch unless apply-atomicity-split was not deferred",
|
|
1504
|
+
],
|
|
1505
|
+
"canonical_parent_commands": [
|
|
1506
|
+
'apply split: uv run python "<wiki/cli.py>" apply-atomicity-split --bundle "<bundle.json>" --json',
|
|
1507
|
+
],
|
|
1508
|
+
},
|
|
1509
|
+
}
|
|
1510
|
+
if phase not in specs:
|
|
1511
|
+
raise ValidationError(f"Unknown subagent planning phase: {phase}")
|
|
1512
|
+
spec = specs[phase]
|
|
1513
|
+
concurrency = int(max_concurrency) if max_concurrency is not None else int(spec["default_max_concurrency"])
|
|
1514
|
+
if concurrency < 1:
|
|
1515
|
+
raise ValidationError("--max-concurrency must be at least 1")
|
|
1516
|
+
if limit is not None and limit < 1:
|
|
1517
|
+
raise ValidationError("--limit must be at least 1")
|
|
1518
|
+
if temp_root is None:
|
|
1519
|
+
temp_root = _default_subagent_temp_root(phase)
|
|
1520
|
+
|
|
1521
|
+
if phase == "vocabulary-curation":
|
|
1522
|
+
if config.vocabulary_db_path is None:
|
|
1523
|
+
raise ValidationError("vocabulary-curation requires a vocabulary DB path")
|
|
1524
|
+
if temp_root is None:
|
|
1525
|
+
raise ValidationError("Internal error: vocabulary-curation temp_root was not resolved")
|
|
1526
|
+
batch_id = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ-vocabulary-curation")
|
|
1527
|
+
plan = build_vocabulary_curator_batch_plan(
|
|
1528
|
+
db_path=config.vocabulary_db_path,
|
|
1529
|
+
batch_id=batch_id,
|
|
1530
|
+
output_dir=temp_root,
|
|
1531
|
+
limit=limit or 20,
|
|
1532
|
+
)
|
|
1533
|
+
plan_summary = _SubagentGeneratedPlanSummary.model_validate(plan)
|
|
1534
|
+
if max_concurrency is not None:
|
|
1535
|
+
plan["max_concurrency"] = min(int(max_concurrency), plan_summary.item_count) if plan_summary.item_count else 0
|
|
1536
|
+
else:
|
|
1537
|
+
plan["max_concurrency"] = min(concurrency, plan_summary.item_count) if plan_summary.item_count else 0
|
|
1538
|
+
plan["serial_after"] = spec["serial_after"]
|
|
1539
|
+
plan["canonical_parent_commands"] = spec["canonical_parent_commands"]
|
|
1540
|
+
return _typed_subagent_plan_payload(plan)
|
|
1541
|
+
|
|
1542
|
+
if phase == "atomicity-split":
|
|
1543
|
+
if fix_wiki_plan_path is None:
|
|
1544
|
+
raise ValidationError("atomicity-split requires --fix-wiki-plan <fix-wiki-plan.json>")
|
|
1545
|
+
if temp_root is None:
|
|
1546
|
+
raise ValidationError("Internal error: atomicity-split temp_root was not resolved")
|
|
1547
|
+
batch_id = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ-atomicity-split")
|
|
1548
|
+
plan = build_atomicity_split_plan(
|
|
1549
|
+
fix_wiki_plan_path=fix_wiki_plan_path,
|
|
1550
|
+
batch_id=batch_id,
|
|
1551
|
+
temp_root=temp_root,
|
|
1552
|
+
limit=limit or 20,
|
|
1553
|
+
)
|
|
1554
|
+
plan_summary = _SubagentGeneratedPlanSummary.model_validate(plan)
|
|
1555
|
+
if max_concurrency is not None:
|
|
1556
|
+
plan["max_concurrency"] = min(int(max_concurrency), plan_summary.item_count) if plan_summary.item_count else 0
|
|
1557
|
+
else:
|
|
1558
|
+
plan["max_concurrency"] = min(concurrency, plan_summary.item_count) if plan_summary.item_count else 0
|
|
1559
|
+
plan["serial_after"] = spec["serial_after"]
|
|
1560
|
+
plan["canonical_parent_commands"] = spec["canonical_parent_commands"]
|
|
1561
|
+
return _typed_subagent_plan_payload(plan)
|
|
1562
|
+
|
|
1563
|
+
if phase == "note-merge":
|
|
1564
|
+
return _typed_subagent_plan_payload({
|
|
1565
|
+
"schema": SUBAGENT_PLAN_SCHEMA,
|
|
1566
|
+
"phase": "note-merge",
|
|
1567
|
+
"agent": spec["agent"],
|
|
1568
|
+
"status": "skipped",
|
|
1569
|
+
"skipped_reason": "no_note_merge_work",
|
|
1570
|
+
"mode": spec["mode"],
|
|
1571
|
+
"item_type": spec["item_type"],
|
|
1572
|
+
"unit": spec["unit"],
|
|
1573
|
+
"max_concurrency": concurrency,
|
|
1574
|
+
"item_count": 0,
|
|
1575
|
+
"blocked_item_count": 0,
|
|
1576
|
+
"total_available_count": 0,
|
|
1577
|
+
"work_items": [],
|
|
1578
|
+
"blocked_items": [],
|
|
1579
|
+
"batches": [],
|
|
1580
|
+
"parallel_safe": False,
|
|
1581
|
+
"serial_after": spec["serial_after"],
|
|
1582
|
+
"canonical_parent_commands": spec["canonical_parent_commands"],
|
|
1583
|
+
"rules": [
|
|
1584
|
+
"Only semantic identity evidence may create a note-merge work item.",
|
|
1585
|
+
"Do not infer a merge from title, stem, accent, case, or path similarity alone.",
|
|
1586
|
+
"Every apply requires note-merge-plan.v1, preservation report, source hashes, expected aliases, and expected chats.",
|
|
1587
|
+
],
|
|
1588
|
+
"next_action": "Sem grupos semânticos de note_merge prontos neste diagnóstico.",
|
|
1589
|
+
})
|
|
1590
|
+
|
|
1591
|
+
if phase == "style-rewrite":
|
|
1592
|
+
if temp_root is None:
|
|
1593
|
+
raise ValidationError("Internal error: style-rewrite temp_root was not resolved")
|
|
1594
|
+
audit = _StyleRewriteAuditSummary.model_validate(
|
|
1595
|
+
style_audit if style_audit is not None else validate_wiki_style(config.wiki_dir)
|
|
1596
|
+
)
|
|
1597
|
+
work_items: list[JsonObject] = []
|
|
1598
|
+
seen: set[str] = set()
|
|
1599
|
+
rewrite_reports = [report for report in audit.reports if report.requires_llm_rewrite and report.path]
|
|
1600
|
+
total_available_count = len(rewrite_reports)
|
|
1601
|
+
if limit is not None:
|
|
1602
|
+
rewrite_reports = rewrite_reports[:limit]
|
|
1603
|
+
for index, report in enumerate(rewrite_reports, start=1):
|
|
1604
|
+
target_path = Path(report.path)
|
|
1605
|
+
owner_key = str(target_path.expanduser())
|
|
1606
|
+
if owner_key in seen:
|
|
1607
|
+
continue
|
|
1608
|
+
seen.add(owner_key)
|
|
1609
|
+
work_id = f"{phase}-{index:03d}-{_slug(target_path.stem)}"
|
|
1610
|
+
item: JsonObject = {
|
|
1611
|
+
"work_id": work_id,
|
|
1612
|
+
"agent": spec["agent"],
|
|
1613
|
+
"item_type": spec["item_type"],
|
|
1614
|
+
"target_path": str(target_path),
|
|
1615
|
+
"target_hash_before": _file_sha256(target_path),
|
|
1616
|
+
"owner_key": owner_key,
|
|
1617
|
+
"title": report.title or target_path.stem,
|
|
1618
|
+
"rewrite_prompt": report.rewrite_prompt,
|
|
1619
|
+
"model_policy": "medical_specialist_authoring.v1",
|
|
1620
|
+
"required_model_tier": "specialist",
|
|
1621
|
+
"preferred_model_tier": "pro",
|
|
1622
|
+
"errors": report.errors,
|
|
1623
|
+
"warnings": report.warnings,
|
|
1624
|
+
"temp_dir": str(temp_root / work_id),
|
|
1625
|
+
"temp_output": str(temp_root / work_id / f"{target_path.stem}.rewrite.md"),
|
|
1626
|
+
"output_attestation_path": str(temp_root / work_id / f"{target_path.stem}.rewrite.md.attestation.json"),
|
|
1627
|
+
"specialist_task_run_receipt_path": str(
|
|
1628
|
+
temp_root / work_id / f"{target_path.stem}.specialist-task-run-receipt.json"
|
|
1629
|
+
),
|
|
1630
|
+
"subagent_output_contract": _style_rewrite_subagent_output_contract(),
|
|
1631
|
+
"context_docs": _style_rewrite_context_docs(),
|
|
1632
|
+
}
|
|
1633
|
+
temp_dir = Path(str(item["temp_dir"]))
|
|
1634
|
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
1635
|
+
(temp_dir / ".keep").touch(exist_ok=True)
|
|
1636
|
+
work_items.append(item)
|
|
1637
|
+
batches = _batch_refs(work_items, concurrency)
|
|
1638
|
+
return _typed_subagent_plan_payload({
|
|
1639
|
+
"schema": SUBAGENT_PLAN_SCHEMA,
|
|
1640
|
+
"phase": phase,
|
|
1641
|
+
"agent": spec["agent"],
|
|
1642
|
+
"status": "ready" if work_items else "skipped",
|
|
1643
|
+
"skipped_reason": "" if work_items else "no_style_rewrite_work",
|
|
1644
|
+
"unit": spec["unit"],
|
|
1645
|
+
"max_concurrency": concurrency,
|
|
1646
|
+
"item_count": len(work_items),
|
|
1647
|
+
"total_available_count": total_available_count,
|
|
1648
|
+
"blocked_item_count": 0,
|
|
1649
|
+
"limit": limit,
|
|
1650
|
+
"truncated": len(work_items) < total_available_count,
|
|
1651
|
+
"parallel_safe": len(work_items) > 1,
|
|
1652
|
+
"launch_source": "work_items",
|
|
1653
|
+
"batch_contract": "batches contain work_id references only; never spawn from both work_items and batch references.",
|
|
1654
|
+
"work_items": work_items,
|
|
1655
|
+
"batches": batches,
|
|
1656
|
+
"rules": [
|
|
1657
|
+
"Spawn at most one subagent per work_item.target_path.",
|
|
1658
|
+
"Never spawn multiple subagents for the same Wiki note.",
|
|
1659
|
+
"Use work_items as the only full launch payload; batches only group existing work_ids.",
|
|
1660
|
+
"Do not split one note rewrite across multiple med-knowledge-architect agents.",
|
|
1661
|
+
"Do not launch more subagents than item_count or max_concurrency.",
|
|
1662
|
+
"If item_count is 0 or 1, there is no useful fan-out for this phase.",
|
|
1663
|
+
"When limit is set, spawn only the returned work_items",
|
|
1664
|
+
"Rerun planning after serial consolidation before launching more.",
|
|
1665
|
+
"Run serial apply-style-rewrite validation and application after each batch returns.",
|
|
1666
|
+
],
|
|
1667
|
+
"serial_after": spec["serial_after"],
|
|
1668
|
+
"canonical_parent_commands": spec["canonical_parent_commands"],
|
|
1669
|
+
"source_audit": {
|
|
1670
|
+
"schema": audit.schema_,
|
|
1671
|
+
"wiki_dir": audit.wiki_dir or str(config.wiki_dir),
|
|
1672
|
+
"file_count": audit.file_count,
|
|
1673
|
+
"error_count": audit.error_count,
|
|
1674
|
+
"warning_count": audit.warning_count,
|
|
1675
|
+
},
|
|
1676
|
+
})
|
|
1677
|
+
|
|
1678
|
+
covered_ids = set(covered_raw_chat_index(config.wiki_dir)) if spec["mode"] == "pending" else set()
|
|
1679
|
+
rows = list_by_status(config.raw_dir, str(spec["mode"]), covered_raw_chat_ids=covered_ids)
|
|
1680
|
+
total_available_count = len(rows)
|
|
1681
|
+
if limit is not None:
|
|
1682
|
+
rows = rows[:limit]
|
|
1683
|
+
if phase == "architect":
|
|
1684
|
+
if temp_root is None:
|
|
1685
|
+
raise ValidationError("Internal error: architect temp_root was not resolved")
|
|
1686
|
+
return _plan_architect_subagents(
|
|
1687
|
+
config,
|
|
1688
|
+
spec,
|
|
1689
|
+
rows,
|
|
1690
|
+
total_available_count=total_available_count,
|
|
1691
|
+
concurrency=concurrency,
|
|
1692
|
+
temp_root=temp_root,
|
|
1693
|
+
limit=limit,
|
|
1694
|
+
)
|
|
1695
|
+
work_items: list[JsonObject] = []
|
|
1696
|
+
blocked_items: list[JsonObject] = []
|
|
1697
|
+
seen: set[str] = set()
|
|
1698
|
+
for index, row in enumerate(rows, start=1):
|
|
1699
|
+
raw_file = str(row["path"])
|
|
1700
|
+
raw_key = str(Path(raw_file).expanduser())
|
|
1701
|
+
if raw_key in seen:
|
|
1702
|
+
continue
|
|
1703
|
+
seen.add(raw_key)
|
|
1704
|
+
work_id = f"{phase}-{index:03d}-{_slug(Path(raw_file).stem)}"
|
|
1705
|
+
item: JsonObject = {
|
|
1706
|
+
"work_id": work_id,
|
|
1707
|
+
"agent": spec["agent"],
|
|
1708
|
+
"item_type": spec["item_type"],
|
|
1709
|
+
"raw_file": raw_file,
|
|
1710
|
+
"owner_key": raw_key,
|
|
1711
|
+
"titulo_triagem": _json_str_field(row, "titulo_triagem"),
|
|
1712
|
+
"fonte_id": _json_str_field(row, "fonte_id"),
|
|
1713
|
+
}
|
|
1714
|
+
if temp_root is not None:
|
|
1715
|
+
temp_dir = temp_root / work_id
|
|
1716
|
+
item["temp_dir"] = str(temp_dir)
|
|
1717
|
+
if phase == "triage":
|
|
1718
|
+
item["triager_output_path"] = str(temp_dir / "triager-output.json")
|
|
1719
|
+
item["note_plan_path"] = str(temp_dir / "note-plan.json")
|
|
1720
|
+
item["triager_eval_path"] = str(temp_dir / "triager-eval.json")
|
|
1721
|
+
work_items.append(item)
|
|
1722
|
+
|
|
1723
|
+
batches = _batch_refs(work_items, concurrency)
|
|
1724
|
+
status, next_action, _blocked_requires_attention = plan_status(
|
|
1725
|
+
item_count=len(work_items),
|
|
1726
|
+
blocked_item_count=len(blocked_items),
|
|
1727
|
+
)
|
|
1728
|
+
human_decision_required = bool(_decision_packets_from_blocked_items(blocked_items))
|
|
1729
|
+
return _typed_subagent_plan_payload(annotate_payload({
|
|
1730
|
+
"schema": SUBAGENT_PLAN_SCHEMA,
|
|
1731
|
+
"phase": phase,
|
|
1732
|
+
"agent": spec["agent"],
|
|
1733
|
+
"unit": spec["unit"],
|
|
1734
|
+
"max_concurrency": concurrency,
|
|
1735
|
+
"item_count": len(work_items),
|
|
1736
|
+
"total_available_count": total_available_count,
|
|
1737
|
+
"blocked_item_count": len(blocked_items),
|
|
1738
|
+
"blocked_items": blocked_items,
|
|
1739
|
+
"limit": limit,
|
|
1740
|
+
"truncated": limit is not None and len(rows) < total_available_count,
|
|
1741
|
+
"parallel_safe": len(work_items) > 1,
|
|
1742
|
+
"launch_source": "work_items",
|
|
1743
|
+
"batch_contract": "batches contain work_id references only; never spawn from both work_items and batch references.",
|
|
1744
|
+
"work_items": work_items,
|
|
1745
|
+
"batches": batches,
|
|
1746
|
+
"rules": [
|
|
1747
|
+
"Spawn at most one subagent per work_item.raw_file.",
|
|
1748
|
+
"Never spawn multiple subagents for the same raw chat or generated note.",
|
|
1749
|
+
"Use work_items as the only full launch payload; batches only group existing work_ids.",
|
|
1750
|
+
"Do not replace med-chat-triager by reading multiple raw chats in the parent agent.",
|
|
1751
|
+
"Parent must use work_item.triager_output_path, work_item.note_plan_path, and work_item.triager_eval_path; do not write workflow artifacts under repo-root tmp/.",
|
|
1752
|
+
"Do not launch more subagents than item_count or max_concurrency.",
|
|
1753
|
+
"If item_count is 0 or 1, there is no useful fan-out for this phase.",
|
|
1754
|
+
"When limit is set, spawn only the returned work_items",
|
|
1755
|
+
"Rerun planning after serial consolidation before launching more.",
|
|
1756
|
+
"Parent must apply triage or discard serially after each batch returns.",
|
|
1757
|
+
],
|
|
1758
|
+
"serial_after": spec["serial_after"],
|
|
1759
|
+
"canonical_parent_commands": spec["canonical_parent_commands"],
|
|
1760
|
+
},
|
|
1761
|
+
phase=phase,
|
|
1762
|
+
status=status,
|
|
1763
|
+
blocked_reason="preconditions_failed" if blocked_items and not work_items else "",
|
|
1764
|
+
next_action=next_action,
|
|
1765
|
+
required_inputs=STYLE_REWRITE_REQUIRED_INPUTS if phase == "style-rewrite" else PROCESS_CHATS_REQUIRED_INPUTS,
|
|
1766
|
+
human_decision_required=human_decision_required,
|
|
1767
|
+
))
|