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,2293 @@
|
|
|
1
|
+
"""Shared operational API contract for workflow outputs and feedback records."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import unquote
|
|
9
|
+
|
|
10
|
+
from pydantic import ConfigDict, Field, StrictStr
|
|
11
|
+
|
|
12
|
+
from mednotes.kernel.agent_directive import AgentDirective, AgentDirectiveControl
|
|
13
|
+
from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter
|
|
14
|
+
|
|
15
|
+
FSM_FIRST_SCHEMAS = {
|
|
16
|
+
"medical-notes-workbench.fix-wiki-fsm-result.v1",
|
|
17
|
+
"medical-notes-workbench.flashcards-fsm-result.v1",
|
|
18
|
+
"medical-notes-workbench.link-fsm-result.v1",
|
|
19
|
+
"medical-notes-workbench.link-related-fsm-result.v1",
|
|
20
|
+
"medical-notes-workbench.process-chats-fsm-result.v1",
|
|
21
|
+
"medical-notes-workbench.setup-fsm-result.v1",
|
|
22
|
+
"medical-notes-workbench.history-fsm-result.v1",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_AGENT_DIRECTIVE_SCHEMA = "medical-notes-workbench.agent-directive.v1"
|
|
26
|
+
|
|
27
|
+
_AGENT_DIRECTIVE_STATUSES = {
|
|
28
|
+
"running",
|
|
29
|
+
"waiting_agent",
|
|
30
|
+
"waiting_external",
|
|
31
|
+
"waiting_human",
|
|
32
|
+
"blocked",
|
|
33
|
+
"failed",
|
|
34
|
+
"completed",
|
|
35
|
+
"completed_with_warnings",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_AGENT_PREAMBLE_FIELD_PREFIXES = (
|
|
39
|
+
"Status:",
|
|
40
|
+
"phase:",
|
|
41
|
+
"workflow_exit_code:",
|
|
42
|
+
"workflow_result_label:",
|
|
43
|
+
"blocked_reason:",
|
|
44
|
+
"continuation_reason:",
|
|
45
|
+
"blocking_reasons:",
|
|
46
|
+
"next_action:",
|
|
47
|
+
"next_command:",
|
|
48
|
+
"execution_gate:",
|
|
49
|
+
"resume_after_resolution:",
|
|
50
|
+
"progress_view_model.status:",
|
|
51
|
+
"progress_view_model.phase:",
|
|
52
|
+
"progress_view_model.state:",
|
|
53
|
+
"progress_view_model.can_continue_now:",
|
|
54
|
+
"progress_view_model.resume_action:",
|
|
55
|
+
"state_machine_snapshot.current_category:",
|
|
56
|
+
"state_machine_snapshot.current_state:",
|
|
57
|
+
"receipt.status:",
|
|
58
|
+
"receipt.next_action:",
|
|
59
|
+
"required_inputs:",
|
|
60
|
+
"human_decision_required:",
|
|
61
|
+
"decision.kind:",
|
|
62
|
+
"decision.reason_code:",
|
|
63
|
+
"decision.next_action:",
|
|
64
|
+
"human_decision_packet:",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
TOOL_PARAMETER_CONTRACT_VIOLATION = "agent.tool_param_contract_violation"
|
|
68
|
+
TOOL_CALL_ERROR = "agent.tool_call_error"
|
|
69
|
+
PUBLIC_TOOL_TEXT_CONTRACT_VIOLATION = "agent.public_tool_text_contract_violation"
|
|
70
|
+
PUBLIC_DEV_ESCAPE_CONTRACT_VIOLATION = "agent.public_dev_escape_contract_violation"
|
|
71
|
+
SUBAGENT_BATCH_CONTRACT_VIOLATION = "agent.subagent_batch_contract_violation"
|
|
72
|
+
SUBAGENT_RAW_CONTENT_CONTRACT_VIOLATION = "agent.subagent_raw_content_contract_violation"
|
|
73
|
+
SUBAGENT_INVOCATION_PACKET_CONTRACT_VIOLATION = "agent.subagent_invocation_packet_contract_violation"
|
|
74
|
+
SPECIALIST_PARALLEL_INVOCATION_CONTRACT_VIOLATION = "agent.specialist_parallel_invocation_contract_violation"
|
|
75
|
+
SPECIALIST_DUPLICATE_INVOCATION_CONTRACT_VIOLATION = "agent.specialist_duplicate_invocation_contract_violation"
|
|
76
|
+
WORKFLOW_CONTINUED_AFTER_BLOCKED_PAYLOAD = "agent.workflow_continued_after_blocked_payload"
|
|
77
|
+
MANUAL_SUBAGENT_CONTRACT_VIOLATION = "agent.manual_subagent_contract_violation"
|
|
78
|
+
WORKSPACE_ADD_DIR_HIDDEN_IGNORED = "agent.workspace_add_dir_hidden_ignored"
|
|
79
|
+
STYLE_REWRITE_WORKSPACE_PERMISSION_TIMEOUT = "agent.style_rewrite_workspace_permission_timeout"
|
|
80
|
+
PARALLEL_STYLE_REWRITE_CONTRACT_VIOLATION = "agent.parallel_style_rewrite_contract_violation"
|
|
81
|
+
DEPENDENT_STYLE_REWRITE_BATCH_CONTRACT_VIOLATION = "agent.dependent_style_rewrite_batch_contract_violation"
|
|
82
|
+
INVALID_EXTENSION_COMMAND_PATH = "agent.invalid_extension_command_path"
|
|
83
|
+
SHELL_CHAIN_CONTRACT_VIOLATION = "agent.shell_chain_contract_violation"
|
|
84
|
+
STYLE_REWRITE_DIRECT_CONTENT_APPLY = "agent.style_rewrite_direct_content_apply_contract_violation"
|
|
85
|
+
STYLE_REWRITE_UNVERIFIED_MODEL_CLAIM = "agent.style_rewrite_unverified_model_claim_contract_violation"
|
|
86
|
+
STYLE_REWRITE_PARENT_OUTPUT_WRITE = "agent.style_rewrite_parent_output_write_contract_violation"
|
|
87
|
+
SPECIALIST_UNVERIFIED_MODEL_ESCAPE = "agent.specialist_unverified_model_escape_contract_violation"
|
|
88
|
+
PROCESS_CHATS_RAW_WRITE = "agent.process_chats_raw_write_contract_violation"
|
|
89
|
+
PROCESS_CHATS_PARENT_ARTIFACT_WRITE_WITHOUT_SUBAGENT = (
|
|
90
|
+
"agent.process_chats_parent_artifact_write_without_subagent"
|
|
91
|
+
)
|
|
92
|
+
WORKFLOW_SOURCE_DISCOVERY_AFTER_BLOCK = "agent.workflow_source_discovery_after_block"
|
|
93
|
+
STALE_EXTENSION_SCRIPT_PATH = "agent.stale_extension_script_path"
|
|
94
|
+
STALE_EXTENSION_SKILL_PATH = "agent.stale_extension_skill_path"
|
|
95
|
+
STALE_SUPERPOWERS_SKILL_PATH = "agent.stale_superpowers_skill_path"
|
|
96
|
+
WORKFLOW_ARTIFACT_DIRECT_WRITE = "agent.workflow_artifact_direct_write"
|
|
97
|
+
WORKFLOW_ARTIFACT_SHELL_COPY = "agent.workflow_artifact_shell_copy"
|
|
98
|
+
WORKFLOW_ARTIFACT_SHELL_REDIRECT = "agent.workflow_artifact_shell_redirect"
|
|
99
|
+
DUPLICATE_WORKFLOW_COMMAND = "agent.duplicate_workflow_command"
|
|
100
|
+
PREPARATORY_PERMISSION_PROBE = "agent.preparatory_permission_probe"
|
|
101
|
+
NONCANONICAL_PYTHON_ENVIRONMENT_PROBE = "agent.noncanonical_python_environment_probe"
|
|
102
|
+
FINAL_ARTIFACT_PATH_INVALID = "agent.final_artifact_path_invalid"
|
|
103
|
+
PACKAGED_AGENT_TEMPLATE_CONTRACT = "medical-notes-workbench.packaged-agent-template.v1"
|
|
104
|
+
|
|
105
|
+
_TRANSCRIPT_CHILD_CONTAINER_KEYS = (
|
|
106
|
+
"$set",
|
|
107
|
+
"content",
|
|
108
|
+
"events",
|
|
109
|
+
"items",
|
|
110
|
+
"messages",
|
|
111
|
+
"records",
|
|
112
|
+
"response",
|
|
113
|
+
"responses",
|
|
114
|
+
"result",
|
|
115
|
+
"tool_calls",
|
|
116
|
+
"toolCalls",
|
|
117
|
+
"transcript",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
_RETRYABLE_SPECIALIST_BLOCKED_REASONS = {
|
|
121
|
+
"specialist_model_metadata_missing",
|
|
122
|
+
"style_rewrite_agent_contract_violation",
|
|
123
|
+
"style_rewrite_output_missing",
|
|
124
|
+
"style_rewrite_still_requires_rewrite",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_RUN_SHELL_TOOL_ALIASES = {
|
|
128
|
+
"bash",
|
|
129
|
+
"powershell",
|
|
130
|
+
"pwsh",
|
|
131
|
+
"run_shell",
|
|
132
|
+
"run_shell_command",
|
|
133
|
+
"run_command",
|
|
134
|
+
"shell",
|
|
135
|
+
"shelltool",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_RUN_SHELL_ALLOWED_PARAMETERS = {
|
|
139
|
+
"CommandLine",
|
|
140
|
+
"Cwd",
|
|
141
|
+
"TimeoutMs",
|
|
142
|
+
"WaitMsBeforeAsync",
|
|
143
|
+
"cmd",
|
|
144
|
+
"command",
|
|
145
|
+
"cwd",
|
|
146
|
+
"description",
|
|
147
|
+
"delay_ms",
|
|
148
|
+
"dirPath",
|
|
149
|
+
"dir_path",
|
|
150
|
+
"directory",
|
|
151
|
+
"max_output_chars",
|
|
152
|
+
"max_output_tokens",
|
|
153
|
+
"script",
|
|
154
|
+
"timeout_ms",
|
|
155
|
+
"toolAction",
|
|
156
|
+
"toolSummary",
|
|
157
|
+
"workingDirectory",
|
|
158
|
+
"working_directory",
|
|
159
|
+
"yield_time_ms",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_SHELL_COMMAND_PARAMETER_FIELDS = ("command", "cmd", "script", "CommandLine", "commandLine")
|
|
163
|
+
|
|
164
|
+
_UPDATE_TOPIC_ALLOWED_PARAMETERS = {
|
|
165
|
+
"strategic_intent",
|
|
166
|
+
"summary",
|
|
167
|
+
"title",
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_INVOKE_AGENT_ALLOWED_PARAMETERS = {
|
|
171
|
+
"agent_name",
|
|
172
|
+
"prompt",
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
_TOOL_ALLOWED_PARAMETERS = {
|
|
176
|
+
"run_shell_command": _RUN_SHELL_ALLOWED_PARAMETERS,
|
|
177
|
+
"update_topic": _UPDATE_TOPIC_ALLOWED_PARAMETERS,
|
|
178
|
+
"invoke_agent": _INVOKE_AGENT_ALLOWED_PARAMETERS,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
_UPDATE_TOPIC_PUBLIC_TEXT_FIELDS = ("title", "summary", "strategic_intent")
|
|
182
|
+
_PUBLIC_TOOL_TEXT_FORBIDDEN_TERMS = (
|
|
183
|
+
"--apply",
|
|
184
|
+
"--dry-run",
|
|
185
|
+
"--json",
|
|
186
|
+
"apply-note-merge",
|
|
187
|
+
"apply-style-rewrite",
|
|
188
|
+
"apply-specialist-style-rewrite",
|
|
189
|
+
"fix-wiki --",
|
|
190
|
+
"guard_lease",
|
|
191
|
+
"plan-subagents",
|
|
192
|
+
"receipt",
|
|
193
|
+
"run-finish",
|
|
194
|
+
"run-linker",
|
|
195
|
+
"run-start",
|
|
196
|
+
"run_id",
|
|
197
|
+
"schema",
|
|
198
|
+
"scripts/",
|
|
199
|
+
"uv run",
|
|
200
|
+
)
|
|
201
|
+
_PUBLIC_FINAL_RESPONSE_AGENT_INSTRUCTIONS = (
|
|
202
|
+
"agent_instruction: nao anexe bloco diagnostico, JSON, XML, YAML ou campos tecnicos na resposta publica final; use logs/JSON para detalhes tecnicos.",
|
|
203
|
+
"agent_instruction: em mensagens publicas de progresso e resposta final, nao cite subcomandos internos; diga correcao da Wiki e modelo medico especialista.",
|
|
204
|
+
)
|
|
205
|
+
_WORKFLOW_ARTIFACT_WRITE_TOOLS = {"write_file", "write_to_file", "write", "replace", "edit", "multiedit"}
|
|
206
|
+
_WORKFLOW_ARTIFACT_PATH_FIELDS = (
|
|
207
|
+
"AbsolutePath",
|
|
208
|
+
"TargetFile",
|
|
209
|
+
"absolutePath",
|
|
210
|
+
"absolute_path",
|
|
211
|
+
"filePath",
|
|
212
|
+
"file_path",
|
|
213
|
+
"path",
|
|
214
|
+
"targetFile",
|
|
215
|
+
"target_file",
|
|
216
|
+
)
|
|
217
|
+
_WORKFLOW_ARTIFACT_NAME_RE = re.compile(
|
|
218
|
+
r"(^|[-_])("
|
|
219
|
+
r"plan|manifest|receipt|report|diagnosis|trigger-context|trigger_context|run_state"
|
|
220
|
+
r")([-_.]|$)"
|
|
221
|
+
)
|
|
222
|
+
_WORKFLOW_ARTIFACT_SCRATCH_NAMES = (
|
|
223
|
+
"compact-report.json",
|
|
224
|
+
"dry_run_output.json",
|
|
225
|
+
"fix-wiki-plan.json",
|
|
226
|
+
"fix-wiki-user-report.md",
|
|
227
|
+
"full-report.json",
|
|
228
|
+
"link-diagnosis.json",
|
|
229
|
+
"run_state.json",
|
|
230
|
+
)
|
|
231
|
+
_UNVERIFIED_SPECIALIST_MODEL_ENV = "MEDNOTES_ALLOW_UNVERIFIED_SPECIALIST_MODEL"
|
|
232
|
+
_PROCESS_CHATS_CONTEXT_MARKERS = (
|
|
233
|
+
"/mednotes:process-chats",
|
|
234
|
+
"process-medical-chats",
|
|
235
|
+
"mednotes-process-chats",
|
|
236
|
+
)
|
|
237
|
+
_PROCESS_CHATS_ARTIFACT_SUFFIXES = (
|
|
238
|
+
".md",
|
|
239
|
+
"coverage.json",
|
|
240
|
+
"manifest.json",
|
|
241
|
+
"raw-coverage.v1.json",
|
|
242
|
+
"medical-notes-workbench.raw-coverage.v1.json",
|
|
243
|
+
"note-plan.json",
|
|
244
|
+
"triager-output.json",
|
|
245
|
+
)
|
|
246
|
+
_PACKAGED_SPECIALIST_AGENTS = frozenset({"med-knowledge-architect"})
|
|
247
|
+
_PACKAGED_SPECIALIST_AGENT_TEMPLATE_MARKERS = {
|
|
248
|
+
"med-knowledge-architect": (
|
|
249
|
+
"packaged_agent_template_contract: medical-notes-workbench.packaged-agent-template.v1",
|
|
250
|
+
'You = "A Mente"',
|
|
251
|
+
"Parent packet contract:",
|
|
252
|
+
"parent_raw_content_bypass",
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
_INTER_AGENT_MESSAGE_TOOLS = frozenset({"send_message", "invoke_subagent"})
|
|
256
|
+
_SUBAGENT_DEFINITION_TOOLS = frozenset({"define_subagent", "define_agent", "create_subagent"})
|
|
257
|
+
_MESSAGE_PARAMETER_FIELDS = ("Message", "message", "prompt", "Prompt", "content", "Content")
|
|
258
|
+
_AGENT_NAME_PARAMETER_FIELDS = ("agent_name", "agentName", "name", "Name", "agent", "Agent", "TypeName", "typeName")
|
|
259
|
+
_AGY_SUBAGENT_LIST_FIELDS = ("Subagents", "subagents")
|
|
260
|
+
_SUBAGENT_SYSTEM_PROMPT_PARAMETER_FIELDS = (
|
|
261
|
+
"system_prompt",
|
|
262
|
+
"SystemPrompt",
|
|
263
|
+
"instructions",
|
|
264
|
+
"Instructions",
|
|
265
|
+
"prompt",
|
|
266
|
+
"Prompt",
|
|
267
|
+
)
|
|
268
|
+
_STYLE_REWRITE_SUBAGENT_PROMPT_MARKERS = (
|
|
269
|
+
"style-rewrite-",
|
|
270
|
+
"wiki_note_style_rewrite",
|
|
271
|
+
"rewrite prompt",
|
|
272
|
+
"style-rewrite job",
|
|
273
|
+
)
|
|
274
|
+
_STYLE_REWRITE_TYPED_WORK_ITEM_TOKENS = (
|
|
275
|
+
'"work_id"',
|
|
276
|
+
'"item_type"',
|
|
277
|
+
'"target_path"',
|
|
278
|
+
'"target_hash_before"',
|
|
279
|
+
'"temp_output"',
|
|
280
|
+
'"subagent_output_contract"',
|
|
281
|
+
)
|
|
282
|
+
_HANDWRITTEN_SUBAGENT_PROMPT_MARKERS = (
|
|
283
|
+
"CRITICAL MANDATORY INSTRUCTIONS",
|
|
284
|
+
"You are assigned the style-rewrite job for:",
|
|
285
|
+
"- Work ID:",
|
|
286
|
+
"- Target Path:",
|
|
287
|
+
"- Temp Output:",
|
|
288
|
+
)
|
|
289
|
+
_AGY_HIDDEN_WORKSPACE_RE = re.compile(
|
|
290
|
+
r"failed\s+to\s+add\s+workspace\s+folder\b[\s\S]{0,500}\bis\s+hidden\s*:\s*ignore\s+uri",
|
|
291
|
+
re.IGNORECASE,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class AgentPreambleProgressView(ContractModel):
|
|
296
|
+
"""Typed fallback lens used only to fail closed on malformed FSM payloads."""
|
|
297
|
+
|
|
298
|
+
model_config = ConfigDict(extra="ignore")
|
|
299
|
+
|
|
300
|
+
status: StrictStr = ""
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class AgentPreambleSnapshot(ContractModel):
|
|
304
|
+
"""Typed fallback lens for the current FSM category in invalid payloads."""
|
|
305
|
+
|
|
306
|
+
model_config = ConfigDict(extra="ignore")
|
|
307
|
+
|
|
308
|
+
current_category: StrictStr = ""
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class AgentPreamblePayload(ContractModel):
|
|
312
|
+
"""Typed preamble input; valid directives remain the only executable route."""
|
|
313
|
+
|
|
314
|
+
model_config = ConfigDict(extra="ignore")
|
|
315
|
+
|
|
316
|
+
schema_id: StrictStr = Field(default="", alias="schema")
|
|
317
|
+
agent_directive: JsonObject | None = None
|
|
318
|
+
progress_view_model: AgentPreambleProgressView = Field(default_factory=AgentPreambleProgressView)
|
|
319
|
+
state_machine_snapshot: AgentPreambleSnapshot = Field(default_factory=AgentPreambleSnapshot)
|
|
320
|
+
human_decision_required: bool = False
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
AgentPreambleProgressView.model_rebuild(_types_namespace=globals())
|
|
324
|
+
AgentPreambleSnapshot.model_rebuild(_types_namespace=globals())
|
|
325
|
+
AgentPreamblePayload.model_rebuild(_types_namespace=globals())
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def agent_preamble_lines(payload: object) -> list[str]:
|
|
329
|
+
"""Return an agent-facing preamble projected from the operational contract."""
|
|
330
|
+
preamble = AgentPreamblePayload.model_validate(payload)
|
|
331
|
+
directive = _agent_directive(preamble)
|
|
332
|
+
if _is_fsm_first_payload(preamble):
|
|
333
|
+
if directive is None:
|
|
334
|
+
return _invalid_agent_directive_preamble_lines(preamble)
|
|
335
|
+
directive_lines = _agent_directive_preamble_lines(directive)
|
|
336
|
+
if directive_lines:
|
|
337
|
+
return directive_lines
|
|
338
|
+
return []
|
|
339
|
+
if directive is None:
|
|
340
|
+
return []
|
|
341
|
+
return _agent_directive_preamble_lines(directive)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _is_fsm_first_payload(payload: AgentPreamblePayload) -> bool:
|
|
345
|
+
return payload.schema_id in FSM_FIRST_SCHEMAS
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _agent_directive(payload: AgentPreamblePayload) -> AgentDirective | JsonObject | None:
|
|
349
|
+
if payload.agent_directive is None:
|
|
350
|
+
return None
|
|
351
|
+
return _canonical_agent_directive(payload.agent_directive)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _agent_directive_preamble_lines(directive: AgentDirective | JsonObject) -> list[str]:
|
|
355
|
+
if isinstance(directive, dict):
|
|
356
|
+
directive = JsonObjectAdapter.validate_python(directive)
|
|
357
|
+
control = directive.get("control")
|
|
358
|
+
if not isinstance(control, dict):
|
|
359
|
+
return []
|
|
360
|
+
control = JsonObjectAdapter.validate_python(control)
|
|
361
|
+
banner = _agent_directive_banner(str(control.get("status") or "").strip())
|
|
362
|
+
if not banner:
|
|
363
|
+
return []
|
|
364
|
+
lines = [banner]
|
|
365
|
+
lines.extend(_string_list(directive.get("instructions")))
|
|
366
|
+
summary = str(directive.get("summary") or "").strip()
|
|
367
|
+
if summary:
|
|
368
|
+
lines.append(f"agent_directive.summary: {summary}")
|
|
369
|
+
lines.extend(_fallback_agent_directive_control_lines(control))
|
|
370
|
+
if len(lines) == 1:
|
|
371
|
+
return []
|
|
372
|
+
lines.append("---")
|
|
373
|
+
return lines
|
|
374
|
+
control = directive.control
|
|
375
|
+
banner = _agent_directive_banner(control.status)
|
|
376
|
+
if not banner:
|
|
377
|
+
return []
|
|
378
|
+
lines = [banner]
|
|
379
|
+
lines.extend(directive.instructions)
|
|
380
|
+
summary = directive.summary.strip()
|
|
381
|
+
if summary:
|
|
382
|
+
lines.append(f"agent_directive.summary: {summary}")
|
|
383
|
+
lines.extend(_agent_directive_control_lines(control))
|
|
384
|
+
if len(lines) == 1:
|
|
385
|
+
return []
|
|
386
|
+
lines.append("---")
|
|
387
|
+
return lines
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _canonical_agent_directive(directive: JsonObject) -> AgentDirective | JsonObject | None:
|
|
391
|
+
canonical_error = _canonical_agent_directive_error(directive)
|
|
392
|
+
if canonical_error is None:
|
|
393
|
+
return _fallback_agent_directive(directive)
|
|
394
|
+
if canonical_error:
|
|
395
|
+
return None
|
|
396
|
+
try:
|
|
397
|
+
return AgentDirective.model_validate(directive)
|
|
398
|
+
except ValueError as exc:
|
|
399
|
+
del exc
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _canonical_agent_directive_error(directive: JsonObject) -> str | None:
|
|
404
|
+
try:
|
|
405
|
+
AgentDirective.model_validate(directive)
|
|
406
|
+
except ValueError as exc:
|
|
407
|
+
return str(exc)
|
|
408
|
+
return ""
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _fallback_agent_directive(directive: JsonObject) -> JsonObject | None:
|
|
412
|
+
error = _agent_directive_fallback_error(directive)
|
|
413
|
+
if error:
|
|
414
|
+
return None
|
|
415
|
+
return directive
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _agent_directive_fallback_error(directive: JsonObject) -> str:
|
|
419
|
+
if directive.get("schema") != _AGENT_DIRECTIVE_SCHEMA:
|
|
420
|
+
return "agent_directive.schema invalid"
|
|
421
|
+
if not _non_empty_text(directive.get("workflow")):
|
|
422
|
+
return "agent_directive.workflow must be non-empty"
|
|
423
|
+
if not _non_empty_text(directive.get("run_id")):
|
|
424
|
+
return "agent_directive.run_id must be non-empty"
|
|
425
|
+
instructions = directive.get("instructions")
|
|
426
|
+
if instructions is not None:
|
|
427
|
+
if not isinstance(instructions, list):
|
|
428
|
+
return "agent_directive.instructions must be a list"
|
|
429
|
+
for line in instructions:
|
|
430
|
+
if not isinstance(line, str):
|
|
431
|
+
return "agent_directive.instructions must be text"
|
|
432
|
+
if line.strip().casefold().startswith("agent_instruction:"):
|
|
433
|
+
return "agent_directive.instructions must not include agent_instruction prefix"
|
|
434
|
+
control = directive.get("control")
|
|
435
|
+
if not isinstance(control, dict):
|
|
436
|
+
return "agent_directive.control must be an object"
|
|
437
|
+
if not _non_empty_text(control.get("state")):
|
|
438
|
+
return "agent_directive.control.state must be non-empty"
|
|
439
|
+
status = str(control.get("status") or "").strip()
|
|
440
|
+
capabilities = control.get("capabilities")
|
|
441
|
+
capabilities = capabilities if isinstance(capabilities, dict) else {}
|
|
442
|
+
continue_allowed = capabilities.get("continue")
|
|
443
|
+
final_report_allowed = capabilities.get("final_report")
|
|
444
|
+
effects = control.get("effects")
|
|
445
|
+
effects = effects if isinstance(effects, list) else []
|
|
446
|
+
resume = str(control.get("resume") or "").strip()
|
|
447
|
+
blockers = _string_list(control.get("blockers"))
|
|
448
|
+
if status == "waiting_agent":
|
|
449
|
+
if continue_allowed is not True:
|
|
450
|
+
return "waiting_agent requires control.capabilities.continue=true"
|
|
451
|
+
if final_report_allowed is True:
|
|
452
|
+
return "waiting_agent requires control.capabilities.final_report=false"
|
|
453
|
+
if not effects and not resume:
|
|
454
|
+
return "waiting_agent requires effects or resume"
|
|
455
|
+
if status in {"completed", "completed_with_warnings"} and final_report_allowed is not True:
|
|
456
|
+
return "completed directive requires control.capabilities.final_report=true"
|
|
457
|
+
if status in {"waiting_human", "waiting_external", "blocked", "failed"} and not blockers and not resume:
|
|
458
|
+
return f"{status} directive requires blockers or resume"
|
|
459
|
+
return ""
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _non_empty_text(value: object) -> bool:
|
|
463
|
+
return isinstance(value, str) and bool(value.strip())
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _invalid_agent_directive_preamble_lines(payload: AgentPreamblePayload) -> list[str]:
|
|
467
|
+
banner = _agent_preamble_banner(payload)
|
|
468
|
+
if not banner:
|
|
469
|
+
return []
|
|
470
|
+
return [
|
|
471
|
+
banner,
|
|
472
|
+
"agent_directive: missing_or_invalid",
|
|
473
|
+
(
|
|
474
|
+
"agent_instruction: pare e reporte bug de contrato em "
|
|
475
|
+
"agent_directive root; nao use diagnostic_context nem campos agent-facing legados."
|
|
476
|
+
),
|
|
477
|
+
"---",
|
|
478
|
+
]
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _agent_directive_banner(status: str) -> str:
|
|
482
|
+
match status:
|
|
483
|
+
case "running":
|
|
484
|
+
return ">>> WORKFLOW EM EXECUCAO"
|
|
485
|
+
case "waiting_agent":
|
|
486
|
+
return ">>> CONTINUACAO AUTOMATICA OBRIGATORIA"
|
|
487
|
+
case "waiting_human":
|
|
488
|
+
return "??? DECISAO HUMANA NECESSARIA"
|
|
489
|
+
case "failed":
|
|
490
|
+
return "!!! WORKFLOW FALHOU"
|
|
491
|
+
case "blocked":
|
|
492
|
+
return "!!! ACAO OBRIGATORIA DO WORKFLOW"
|
|
493
|
+
case "waiting_external":
|
|
494
|
+
return "... AGUARDANDO CONDICAO EXTERNA"
|
|
495
|
+
case _:
|
|
496
|
+
return ""
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _agent_directive_control_lines(control: AgentDirectiveControl | JsonObject) -> list[str]:
|
|
500
|
+
if isinstance(control, AgentDirectiveControl):
|
|
501
|
+
return _canonical_agent_directive_control_lines(control)
|
|
502
|
+
if isinstance(control, dict):
|
|
503
|
+
return _fallback_agent_directive_control_lines(JsonObjectAdapter.validate_python(control))
|
|
504
|
+
return []
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _canonical_agent_directive_control_lines(control: AgentDirectiveControl) -> list[str]:
|
|
508
|
+
lines: list[str] = []
|
|
509
|
+
fields = ("status", "state", "phase", "reason", "resume") if control.status == "running" else (
|
|
510
|
+
"status",
|
|
511
|
+
"state",
|
|
512
|
+
"reason",
|
|
513
|
+
"resume",
|
|
514
|
+
)
|
|
515
|
+
for field in fields:
|
|
516
|
+
value = str(getattr(control, field)).strip()
|
|
517
|
+
if value:
|
|
518
|
+
lines.append(f"agent_directive.control.{field}: {value}")
|
|
519
|
+
lines.append(f"agent_directive.control.capabilities.continue: {_json_bool(control.capabilities.continue_)}")
|
|
520
|
+
lines.append(f"agent_directive.control.capabilities.final_report: {_json_bool(control.capabilities.final_report)}")
|
|
521
|
+
effect_kinds = [effect.kind.value for effect in control.effects]
|
|
522
|
+
if effect_kinds:
|
|
523
|
+
lines.append(f"agent_directive.control.effects: {json.dumps(effect_kinds, ensure_ascii=False)}")
|
|
524
|
+
if control.blockers:
|
|
525
|
+
lines.append(f"agent_directive.control.blockers: {json.dumps(control.blockers, ensure_ascii=False)}")
|
|
526
|
+
return lines
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _fallback_agent_directive_control_lines(control: JsonObject) -> list[str]:
|
|
530
|
+
lines: list[str] = []
|
|
531
|
+
status = str(control.get("status") or "").strip()
|
|
532
|
+
fields = ("status", "state", "phase", "reason", "resume") if status == "running" else (
|
|
533
|
+
"status",
|
|
534
|
+
"state",
|
|
535
|
+
"reason",
|
|
536
|
+
"resume",
|
|
537
|
+
)
|
|
538
|
+
for field in fields:
|
|
539
|
+
value = str(control.get(field) or "").strip()
|
|
540
|
+
if value:
|
|
541
|
+
lines.append(f"agent_directive.control.{field}: {value}")
|
|
542
|
+
capabilities = control.get("capabilities")
|
|
543
|
+
if isinstance(capabilities, dict):
|
|
544
|
+
capabilities = JsonObjectAdapter.validate_python(capabilities)
|
|
545
|
+
if "continue" in capabilities:
|
|
546
|
+
lines.append(f"agent_directive.control.capabilities.continue: {_json_bool(capabilities.get('continue'))}")
|
|
547
|
+
if "final_report" in capabilities:
|
|
548
|
+
lines.append(
|
|
549
|
+
f"agent_directive.control.capabilities.final_report: {_json_bool(capabilities.get('final_report'))}"
|
|
550
|
+
)
|
|
551
|
+
effects = control.get("effects")
|
|
552
|
+
if isinstance(effects, list):
|
|
553
|
+
effect_kinds = [
|
|
554
|
+
str(item.get("kind")).strip()
|
|
555
|
+
for item in effects
|
|
556
|
+
if isinstance(item, dict) and str(item.get("kind") or "").strip()
|
|
557
|
+
]
|
|
558
|
+
if effect_kinds:
|
|
559
|
+
lines.append(f"agent_directive.control.effects: {json.dumps(effect_kinds, ensure_ascii=False)}")
|
|
560
|
+
blockers = _string_list(control.get("blockers"))
|
|
561
|
+
if blockers:
|
|
562
|
+
lines.append(f"agent_directive.control.blockers: {json.dumps(blockers, ensure_ascii=False)}")
|
|
563
|
+
return lines
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _json_bool(value: object) -> str:
|
|
567
|
+
return "true" if value is True else "false"
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _string_list(value: object) -> list[str]:
|
|
571
|
+
if not isinstance(value, list):
|
|
572
|
+
return []
|
|
573
|
+
return [str(item).strip() for item in value if isinstance(item, str) and item.strip()]
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _agent_preamble_banner(payload: AgentPreamblePayload) -> str:
|
|
577
|
+
status = payload.progress_view_model.status.strip()
|
|
578
|
+
category = payload.state_machine_snapshot.current_category.strip()
|
|
579
|
+
if status == "running" or category == "running":
|
|
580
|
+
return ">>> WORKFLOW EM EXECUCAO"
|
|
581
|
+
if status == "waiting_agent" or category == "waiting_agent":
|
|
582
|
+
return ">>> CONTINUACAO AUTOMATICA OBRIGATORIA"
|
|
583
|
+
if status == "waiting_human" or category == "waiting_human" or payload.human_decision_required is True:
|
|
584
|
+
return "??? DECISAO HUMANA NECESSARIA"
|
|
585
|
+
if status == "failed" or category == "failed":
|
|
586
|
+
return "!!! WORKFLOW FALHOU"
|
|
587
|
+
if status in {"blocked", "error", "needs_review", "completed_with_link_blockers"} or category == "blocked":
|
|
588
|
+
return "!!! ACAO OBRIGATORIA DO WORKFLOW"
|
|
589
|
+
if status == "waiting_external" or category == "waiting_external":
|
|
590
|
+
return "... AGUARDANDO CONDICAO EXTERNA"
|
|
591
|
+
return ""
|
|
592
|
+
|
|
593
|
+
def validate_agent_tool_calls(transcript: Any) -> list[dict[str, Any]]:
|
|
594
|
+
"""Detect tool calls that include unsupported parameters.
|
|
595
|
+
|
|
596
|
+
This is intentionally a transcript validator, not a prompt rule. It gives
|
|
597
|
+
the lab and hooks a deterministic way to flag tool-contract drift such as
|
|
598
|
+
`wait_for_previous` on shell calls.
|
|
599
|
+
"""
|
|
600
|
+
findings: list[dict[str, Any]] = []
|
|
601
|
+
seen: set[tuple[str, ...]] = set()
|
|
602
|
+
agy_plugin_context = _transcript_contains(
|
|
603
|
+
transcript,
|
|
604
|
+
".gemini/config/plugins/medical-notes-workbench/skills/",
|
|
605
|
+
)
|
|
606
|
+
process_chats_context = any(_transcript_contains(transcript, marker) for marker in _PROCESS_CHATS_CONTEXT_MARKERS)
|
|
607
|
+
process_chats_specialist_seen = False
|
|
608
|
+
for tool_name, parameters in _iter_agent_tool_calls(transcript):
|
|
609
|
+
canonical_tool = _canonical_tool_name(tool_name)
|
|
610
|
+
allowed = _TOOL_ALLOWED_PARAMETERS.get(canonical_tool)
|
|
611
|
+
if allowed:
|
|
612
|
+
for key in parameters:
|
|
613
|
+
if key in allowed:
|
|
614
|
+
continue
|
|
615
|
+
finding_key = (canonical_tool, key)
|
|
616
|
+
if finding_key in seen:
|
|
617
|
+
continue
|
|
618
|
+
seen.add(finding_key)
|
|
619
|
+
findings.append(
|
|
620
|
+
{
|
|
621
|
+
"code": TOOL_PARAMETER_CONTRACT_VIOLATION,
|
|
622
|
+
"severity": "medium",
|
|
623
|
+
"tool_name": canonical_tool,
|
|
624
|
+
"bad_param": key,
|
|
625
|
+
"message": f"Tool call {canonical_tool} included unsupported parameter {key}.",
|
|
626
|
+
"next_action": (
|
|
627
|
+
"Reportar como bug de contrato de tool; sequencie comandos esperando "
|
|
628
|
+
"o resultado da chamada anterior."
|
|
629
|
+
),
|
|
630
|
+
}
|
|
631
|
+
)
|
|
632
|
+
permission_probe_finding = _permission_probe_finding(canonical_tool)
|
|
633
|
+
if permission_probe_finding and (canonical_tool, "permission_probe") not in seen:
|
|
634
|
+
seen.add((canonical_tool, "permission_probe"))
|
|
635
|
+
findings.append(permission_probe_finding)
|
|
636
|
+
batch_finding = _subagent_batch_finding(canonical_tool, parameters)
|
|
637
|
+
if batch_finding and (canonical_tool, "subagent_batch") not in seen:
|
|
638
|
+
seen.add((canonical_tool, "subagent_batch"))
|
|
639
|
+
findings.append(batch_finding)
|
|
640
|
+
raw_content_finding = _subagent_raw_content_finding(canonical_tool, parameters)
|
|
641
|
+
if raw_content_finding and (canonical_tool, "subagent_raw_content") not in seen:
|
|
642
|
+
seen.add((canonical_tool, "subagent_raw_content"))
|
|
643
|
+
findings.append(raw_content_finding)
|
|
644
|
+
for invocation_finding in _subagent_invocation_packet_findings(canonical_tool, parameters):
|
|
645
|
+
finding_key = (
|
|
646
|
+
canonical_tool,
|
|
647
|
+
"subagent_invocation_packet",
|
|
648
|
+
str(invocation_finding.get("agent_name") or ""),
|
|
649
|
+
str(invocation_finding.get("bad_param") or ""),
|
|
650
|
+
)
|
|
651
|
+
if finding_key in seen:
|
|
652
|
+
continue
|
|
653
|
+
seen.add(finding_key)
|
|
654
|
+
findings.append(invocation_finding)
|
|
655
|
+
invalid_extension_path_finding = _invalid_extension_command_path_finding(canonical_tool, parameters)
|
|
656
|
+
if invalid_extension_path_finding and (canonical_tool, "invalid_extension_command_path") not in seen:
|
|
657
|
+
seen.add((canonical_tool, "invalid_extension_command_path"))
|
|
658
|
+
findings.append(invalid_extension_path_finding)
|
|
659
|
+
stale_script_path_finding = _stale_extension_script_path_finding(
|
|
660
|
+
canonical_tool,
|
|
661
|
+
parameters,
|
|
662
|
+
agy_plugin_context=agy_plugin_context,
|
|
663
|
+
)
|
|
664
|
+
if stale_script_path_finding and (canonical_tool, "stale_extension_script_path") not in seen:
|
|
665
|
+
seen.add((canonical_tool, "stale_extension_script_path"))
|
|
666
|
+
findings.append(stale_script_path_finding)
|
|
667
|
+
shell_chain_finding = _shell_chain_finding(canonical_tool, parameters)
|
|
668
|
+
if shell_chain_finding and (canonical_tool, "shell_chain") not in seen:
|
|
669
|
+
seen.add((canonical_tool, "shell_chain"))
|
|
670
|
+
findings.append(shell_chain_finding)
|
|
671
|
+
public_dev_escape_finding = _public_dev_escape_finding(canonical_tool, parameters)
|
|
672
|
+
if public_dev_escape_finding and (canonical_tool, "public_dev_escape") not in seen:
|
|
673
|
+
seen.add((canonical_tool, "public_dev_escape"))
|
|
674
|
+
findings.append(public_dev_escape_finding)
|
|
675
|
+
direct_style_apply_finding = _style_rewrite_direct_content_apply_finding(canonical_tool, parameters)
|
|
676
|
+
if direct_style_apply_finding and (canonical_tool, "style_rewrite_direct_content_apply") not in seen:
|
|
677
|
+
seen.add((canonical_tool, "style_rewrite_direct_content_apply"))
|
|
678
|
+
findings.append(direct_style_apply_finding)
|
|
679
|
+
unverified_model_finding = _style_rewrite_unverified_model_claim_finding(canonical_tool, parameters)
|
|
680
|
+
if unverified_model_finding and (canonical_tool, "style_rewrite_unverified_model_claim") not in seen:
|
|
681
|
+
seen.add((canonical_tool, "style_rewrite_unverified_model_claim"))
|
|
682
|
+
findings.append(unverified_model_finding)
|
|
683
|
+
unverified_escape_finding = _specialist_unverified_model_escape_finding(canonical_tool, parameters)
|
|
684
|
+
if unverified_escape_finding and (canonical_tool, "specialist_unverified_model_escape") not in seen:
|
|
685
|
+
seen.add((canonical_tool, "specialist_unverified_model_escape"))
|
|
686
|
+
findings.append(unverified_escape_finding)
|
|
687
|
+
style_output_write_finding = _style_rewrite_parent_output_write_finding(canonical_tool, parameters)
|
|
688
|
+
if style_output_write_finding and (
|
|
689
|
+
canonical_tool,
|
|
690
|
+
"style_rewrite_parent_output_write",
|
|
691
|
+
str(style_output_write_finding.get("path") or ""),
|
|
692
|
+
) not in seen:
|
|
693
|
+
seen.add(
|
|
694
|
+
(
|
|
695
|
+
canonical_tool,
|
|
696
|
+
"style_rewrite_parent_output_write",
|
|
697
|
+
str(style_output_write_finding.get("path") or ""),
|
|
698
|
+
)
|
|
699
|
+
)
|
|
700
|
+
findings.append(style_output_write_finding)
|
|
701
|
+
if process_chats_context:
|
|
702
|
+
raw_write_finding = _process_chats_raw_write_finding(canonical_tool, parameters)
|
|
703
|
+
if raw_write_finding and (
|
|
704
|
+
canonical_tool,
|
|
705
|
+
"process_chats_raw_write",
|
|
706
|
+
str(raw_write_finding.get("path") or ""),
|
|
707
|
+
) not in seen:
|
|
708
|
+
seen.add(
|
|
709
|
+
(
|
|
710
|
+
canonical_tool,
|
|
711
|
+
"process_chats_raw_write",
|
|
712
|
+
str(raw_write_finding.get("path") or ""),
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
findings.append(raw_write_finding)
|
|
716
|
+
artifact_write_finding = _process_chats_parent_artifact_write_without_subagent_finding(
|
|
717
|
+
canonical_tool,
|
|
718
|
+
parameters,
|
|
719
|
+
process_chats_specialist_seen=process_chats_specialist_seen,
|
|
720
|
+
)
|
|
721
|
+
if artifact_write_finding and (
|
|
722
|
+
canonical_tool,
|
|
723
|
+
"process_chats_parent_artifact_write_without_subagent",
|
|
724
|
+
str(artifact_write_finding.get("path") or ""),
|
|
725
|
+
) not in seen:
|
|
726
|
+
seen.add(
|
|
727
|
+
(
|
|
728
|
+
canonical_tool,
|
|
729
|
+
"process_chats_parent_artifact_write_without_subagent",
|
|
730
|
+
str(artifact_write_finding.get("path") or ""),
|
|
731
|
+
)
|
|
732
|
+
)
|
|
733
|
+
findings.append(artifact_write_finding)
|
|
734
|
+
source_discovery_finding = _workflow_source_discovery_after_block_finding(canonical_tool, parameters)
|
|
735
|
+
if source_discovery_finding and (canonical_tool, "workflow_source_discovery_after_block") not in seen:
|
|
736
|
+
seen.add((canonical_tool, "workflow_source_discovery_after_block"))
|
|
737
|
+
findings.append(source_discovery_finding)
|
|
738
|
+
python_environment_probe_finding = _python_environment_probe_finding(canonical_tool, parameters)
|
|
739
|
+
if python_environment_probe_finding and (canonical_tool, "python_environment_probe") not in seen:
|
|
740
|
+
seen.add((canonical_tool, "python_environment_probe"))
|
|
741
|
+
findings.append(python_environment_probe_finding)
|
|
742
|
+
workflow_artifact_write_finding = _workflow_artifact_direct_write_finding(canonical_tool, parameters)
|
|
743
|
+
if workflow_artifact_write_finding and (
|
|
744
|
+
canonical_tool,
|
|
745
|
+
"workflow_artifact_direct_write",
|
|
746
|
+
str(workflow_artifact_write_finding.get("path") or ""),
|
|
747
|
+
) not in seen:
|
|
748
|
+
seen.add(
|
|
749
|
+
(
|
|
750
|
+
canonical_tool,
|
|
751
|
+
"workflow_artifact_direct_write",
|
|
752
|
+
str(workflow_artifact_write_finding.get("path") or ""),
|
|
753
|
+
)
|
|
754
|
+
)
|
|
755
|
+
findings.append(workflow_artifact_write_finding)
|
|
756
|
+
workflow_artifact_shell_copy_finding = _workflow_artifact_shell_copy_finding(canonical_tool, parameters)
|
|
757
|
+
if workflow_artifact_shell_copy_finding and (canonical_tool, "workflow_artifact_shell_copy") not in seen:
|
|
758
|
+
seen.add((canonical_tool, "workflow_artifact_shell_copy"))
|
|
759
|
+
findings.append(workflow_artifact_shell_copy_finding)
|
|
760
|
+
workflow_artifact_shell_redirect_finding = _workflow_artifact_shell_redirect_finding(canonical_tool, parameters)
|
|
761
|
+
if workflow_artifact_shell_redirect_finding and (
|
|
762
|
+
canonical_tool,
|
|
763
|
+
"workflow_artifact_shell_redirect",
|
|
764
|
+
) not in seen:
|
|
765
|
+
seen.add((canonical_tool, "workflow_artifact_shell_redirect"))
|
|
766
|
+
findings.append(workflow_artifact_shell_redirect_finding)
|
|
767
|
+
if process_chats_context and _is_process_chats_specialist_invocation(canonical_tool):
|
|
768
|
+
process_chats_specialist_seen = True
|
|
769
|
+
for batch_finding in _parallel_style_rewrite_findings(transcript):
|
|
770
|
+
finding_key = ("run_shell_command", "parallel_style_rewrite", str(batch_finding.get("mode") or ""))
|
|
771
|
+
if finding_key in seen:
|
|
772
|
+
continue
|
|
773
|
+
seen.add(finding_key)
|
|
774
|
+
findings.append(batch_finding)
|
|
775
|
+
for specialist_finding in _parallel_specialist_invocation_findings(transcript):
|
|
776
|
+
finding_key = ("invoke_agent", "parallel_specialist_invocation", str(specialist_finding.get("call_count") or ""))
|
|
777
|
+
if finding_key in seen:
|
|
778
|
+
continue
|
|
779
|
+
seen.add(finding_key)
|
|
780
|
+
findings.append(specialist_finding)
|
|
781
|
+
for specialist_finding in _duplicate_specialist_invocation_findings(transcript):
|
|
782
|
+
finding_key = (
|
|
783
|
+
"invoke_agent",
|
|
784
|
+
"duplicate_specialist_invocation",
|
|
785
|
+
str(specialist_finding.get("work_id") or ""),
|
|
786
|
+
)
|
|
787
|
+
if finding_key in seen:
|
|
788
|
+
continue
|
|
789
|
+
seen.add(finding_key)
|
|
790
|
+
findings.append(specialist_finding)
|
|
791
|
+
for blocked_finding in _continued_after_blocked_payload_findings(transcript):
|
|
792
|
+
finding_key = (
|
|
793
|
+
"workflow_continued_after_blocked_payload",
|
|
794
|
+
str(blocked_finding.get("blocked_reason") or ""),
|
|
795
|
+
str(blocked_finding.get("tool_name") or ""),
|
|
796
|
+
)
|
|
797
|
+
if finding_key in seen:
|
|
798
|
+
continue
|
|
799
|
+
seen.add(finding_key)
|
|
800
|
+
findings.append(blocked_finding)
|
|
801
|
+
for duplicate_finding in _duplicate_workflow_command_findings(transcript):
|
|
802
|
+
finding_key = (
|
|
803
|
+
"run_shell_command",
|
|
804
|
+
"duplicate_workflow_command",
|
|
805
|
+
str(duplicate_finding.get("workflow") or ""),
|
|
806
|
+
str(duplicate_finding.get("mode") or ""),
|
|
807
|
+
)
|
|
808
|
+
if finding_key in seen:
|
|
809
|
+
continue
|
|
810
|
+
seen.add(finding_key)
|
|
811
|
+
findings.append(duplicate_finding)
|
|
812
|
+
for error_finding in _agent_tool_error_findings(transcript):
|
|
813
|
+
finding_key = ("tool_error", str(error_finding.get("message") or ""))
|
|
814
|
+
if finding_key in seen:
|
|
815
|
+
continue
|
|
816
|
+
seen.add(finding_key)
|
|
817
|
+
findings.append(error_finding)
|
|
818
|
+
for skill_finding in _stale_extension_skill_findings(transcript, agy_plugin_context=agy_plugin_context):
|
|
819
|
+
finding_key = ("stale_skill", str(skill_finding.get("path") or ""))
|
|
820
|
+
if finding_key in seen:
|
|
821
|
+
continue
|
|
822
|
+
seen.add(finding_key)
|
|
823
|
+
findings.append(skill_finding)
|
|
824
|
+
for artifact_finding in _final_artifact_path_findings(transcript):
|
|
825
|
+
finding_key = ("final_artifact_path", str(artifact_finding.get("path") or ""))
|
|
826
|
+
if finding_key in seen:
|
|
827
|
+
continue
|
|
828
|
+
seen.add(finding_key)
|
|
829
|
+
findings.append(artifact_finding)
|
|
830
|
+
for manual_subagent_finding in _manual_packaged_subagent_findings(transcript):
|
|
831
|
+
finding_key = (
|
|
832
|
+
"manual_subagent_definition",
|
|
833
|
+
str(manual_subagent_finding.get("agent_name") or ""),
|
|
834
|
+
)
|
|
835
|
+
if finding_key in seen:
|
|
836
|
+
continue
|
|
837
|
+
seen.add(finding_key)
|
|
838
|
+
findings.append(manual_subagent_finding)
|
|
839
|
+
for hidden_workspace_finding in _agy_hidden_workspace_findings(transcript):
|
|
840
|
+
finding_key = ("agy_hidden_workspace", str(hidden_workspace_finding.get("path") or ""))
|
|
841
|
+
if finding_key in seen:
|
|
842
|
+
continue
|
|
843
|
+
seen.add(finding_key)
|
|
844
|
+
findings.append(hidden_workspace_finding)
|
|
845
|
+
return findings
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _agent_tool_error_findings(transcript: Any) -> list[dict[str, Any]]:
|
|
849
|
+
findings: list[dict[str, Any]] = []
|
|
850
|
+
|
|
851
|
+
def visit(value: Any) -> None:
|
|
852
|
+
if isinstance(value, list):
|
|
853
|
+
for item in value:
|
|
854
|
+
visit(item)
|
|
855
|
+
return
|
|
856
|
+
if not isinstance(value, dict):
|
|
857
|
+
return
|
|
858
|
+
workspace_timeout_finding = _style_rewrite_workspace_permission_timeout_finding(value)
|
|
859
|
+
if workspace_timeout_finding:
|
|
860
|
+
findings.append(workspace_timeout_finding)
|
|
861
|
+
return
|
|
862
|
+
stale_superpowers_finding = _stale_superpowers_tool_error_finding(value)
|
|
863
|
+
if stale_superpowers_finding:
|
|
864
|
+
findings.append(stale_superpowers_finding)
|
|
865
|
+
return
|
|
866
|
+
error_payload = _tool_error_payload(value)
|
|
867
|
+
if error_payload:
|
|
868
|
+
error_type, severity, text = error_payload
|
|
869
|
+
findings.append(
|
|
870
|
+
{
|
|
871
|
+
"code": TOOL_CALL_ERROR,
|
|
872
|
+
"severity": severity,
|
|
873
|
+
"error_type": error_type,
|
|
874
|
+
"tool_type": str(value.get("type") or ""),
|
|
875
|
+
"message": f"Tool call failed before execution: {_normalize_tool_error_text(text)}",
|
|
876
|
+
"next_action": "Reportar a tool call falha no relatório final mesmo se um retry posterior recuperar.",
|
|
877
|
+
}
|
|
878
|
+
)
|
|
879
|
+
for key in _TRANSCRIPT_CHILD_CONTAINER_KEYS:
|
|
880
|
+
child = value.get(key)
|
|
881
|
+
if isinstance(child, (dict, list)):
|
|
882
|
+
visit(child)
|
|
883
|
+
|
|
884
|
+
visit(transcript)
|
|
885
|
+
return findings
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _style_rewrite_workspace_permission_timeout_finding(value: dict[str, Any]) -> dict[str, Any] | None:
|
|
889
|
+
raw = value.get("error") or value.get("content") or value.get("message") or ""
|
|
890
|
+
text = str(raw)
|
|
891
|
+
lowered = text.lower()
|
|
892
|
+
status = str(value.get("status") or "").lower()
|
|
893
|
+
if status not in {"error", "failed"}:
|
|
894
|
+
return None
|
|
895
|
+
if "permission prompt" not in lowered or "write_file" not in lowered or "timed out" not in lowered:
|
|
896
|
+
return None
|
|
897
|
+
match = re.search(r"target ['\"](?P<path>[^'\"]*tmp/agent-work/fix-wiki/[^'\"]+\.rewrite\.md)['\"]", text)
|
|
898
|
+
if not match:
|
|
899
|
+
return None
|
|
900
|
+
path = match.group("path")
|
|
901
|
+
return {
|
|
902
|
+
"code": STYLE_REWRITE_WORKSPACE_PERMISSION_TIMEOUT,
|
|
903
|
+
"severity": "high",
|
|
904
|
+
"tool_name": "write_file",
|
|
905
|
+
"bad_param": "target",
|
|
906
|
+
"path": path,
|
|
907
|
+
"message": "Style rewrite temp_output was outside the writable AGY/subagent workspace.",
|
|
908
|
+
"next_action": (
|
|
909
|
+
"Repetir a rodada com o temp_dir do work_item adicionado ao workspace antes de invocar "
|
|
910
|
+
"o subagente; não tente contornar por scratch, run_command ou conteúdo colado."
|
|
911
|
+
),
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def _stale_superpowers_tool_error_finding(value: dict[str, Any]) -> dict[str, Any] | None:
|
|
916
|
+
raw = value.get("error") or value.get("content") or value.get("message") or ""
|
|
917
|
+
text = str(raw).replace("\\", "/").casefold()
|
|
918
|
+
status = str(value.get("status") or "").lower()
|
|
919
|
+
event_type = str(value.get("type") or "").upper()
|
|
920
|
+
if ".gemini/extensions/superpowers/skills" not in text:
|
|
921
|
+
return None
|
|
922
|
+
if event_type != "ERROR_MESSAGE" and status not in {"error", "failed"}:
|
|
923
|
+
return None
|
|
924
|
+
return {
|
|
925
|
+
"code": STALE_SUPERPOWERS_SKILL_PATH,
|
|
926
|
+
"severity": "high",
|
|
927
|
+
"tool_name": "read_file",
|
|
928
|
+
"bad_param": "path",
|
|
929
|
+
"path": "~/.gemini/extensions/superpowers/skills/*",
|
|
930
|
+
"message": "Agent tried to load stale Superpowers skill files outside the AGY plugin surface.",
|
|
931
|
+
"next_action": (
|
|
932
|
+
"Reportar como bug de roteamento AGY; use somente skills/docs empacotados no plugin "
|
|
933
|
+
"ou caminhos explícitos do payload oficial."
|
|
934
|
+
),
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def _tool_error_payload(value: dict[str, Any]) -> tuple[str, str, str] | None:
|
|
939
|
+
raw = value.get("error") or value.get("content") or value.get("message") or ""
|
|
940
|
+
text = str(raw)
|
|
941
|
+
lowered = text.lower()
|
|
942
|
+
status = str(value.get("status") or "").lower()
|
|
943
|
+
event_type = str(value.get("type") or "").upper()
|
|
944
|
+
if (
|
|
945
|
+
("invalid tool call" in lowered or "invalid_tool_params" in lowered)
|
|
946
|
+
and (event_type == "ERROR_MESSAGE" or status in {"error", "failed"})
|
|
947
|
+
):
|
|
948
|
+
return ("invalid_tool_params", "medium", text)
|
|
949
|
+
if status in {"error", "failed"} and _looks_like_transcript_tool_event(value):
|
|
950
|
+
severity = "low" if _is_low_severity_tool_error(text) else "medium"
|
|
951
|
+
return ("tool_status_error", severity, text)
|
|
952
|
+
return None
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def _looks_like_transcript_tool_event(value: dict[str, Any]) -> bool:
|
|
956
|
+
event_type = str(value.get("type") or "").upper()
|
|
957
|
+
if not event_type or event_type in {"ERROR_MESSAGE", "PLANNER_RESPONSE", "SYSTEM_MESSAGE"}:
|
|
958
|
+
return False
|
|
959
|
+
return any(key in value for key in ("step_index", "source", "created_at", "tool_name", "content"))
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def _is_low_severity_tool_error(text: str) -> bool:
|
|
963
|
+
lowered = text.lower()
|
|
964
|
+
return (
|
|
965
|
+
"permission denied" in lowered
|
|
966
|
+
and ("read_file" in lowered or "list" in lowered)
|
|
967
|
+
and "system protection boundary" in lowered
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def _normalize_tool_error_text(text: str) -> str:
|
|
972
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
973
|
+
marker = "Error Message:"
|
|
974
|
+
if marker in text:
|
|
975
|
+
text = text.split(marker, 1)[1].strip()
|
|
976
|
+
if len(text) > 300:
|
|
977
|
+
text = text[:297].rstrip() + "..."
|
|
978
|
+
return text
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def _public_tool_text_finding(tool_name: str, parameters: dict[str, Any]) -> dict[str, Any] | None:
|
|
982
|
+
if tool_name != "update_topic":
|
|
983
|
+
return None
|
|
984
|
+
field_hits: list[str] = []
|
|
985
|
+
term_hits: list[str] = []
|
|
986
|
+
for field in _UPDATE_TOPIC_PUBLIC_TEXT_FIELDS:
|
|
987
|
+
value = parameters.get(field)
|
|
988
|
+
if not isinstance(value, str):
|
|
989
|
+
continue
|
|
990
|
+
lowered = value.lower()
|
|
991
|
+
matches = [term for term in _PUBLIC_TOOL_TEXT_FORBIDDEN_TERMS if term in lowered]
|
|
992
|
+
if not matches:
|
|
993
|
+
continue
|
|
994
|
+
field_hits.append(field)
|
|
995
|
+
for term in matches:
|
|
996
|
+
if term not in term_hits:
|
|
997
|
+
term_hits.append(term)
|
|
998
|
+
if not field_hits:
|
|
999
|
+
return None
|
|
1000
|
+
return {
|
|
1001
|
+
"code": PUBLIC_TOOL_TEXT_CONTRACT_VIOLATION,
|
|
1002
|
+
"severity": "medium",
|
|
1003
|
+
"tool_name": tool_name,
|
|
1004
|
+
"bad_param": "public_text",
|
|
1005
|
+
"message": "Tool call update_topic exposed internal workflow terms in public text.",
|
|
1006
|
+
"fields": field_hits,
|
|
1007
|
+
"forbidden_terms": term_hits,
|
|
1008
|
+
"next_action": (
|
|
1009
|
+
"Reportar como bug de UX/tool-contract; traduza update_topic para linguagem pública "
|
|
1010
|
+
"sem flags, comandos, paths ou IDs técnicos."
|
|
1011
|
+
),
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
def _permission_probe_finding(tool_name: str) -> dict[str, Any] | None:
|
|
1016
|
+
if tool_name != "list_permissions":
|
|
1017
|
+
return None
|
|
1018
|
+
return {
|
|
1019
|
+
"code": PREPARATORY_PERMISSION_PROBE,
|
|
1020
|
+
"severity": "low",
|
|
1021
|
+
"tool_name": tool_name,
|
|
1022
|
+
"bad_param": "tool_call",
|
|
1023
|
+
"message": "Agent listed AGY permissions as a preparatory probe before the workflow.",
|
|
1024
|
+
"next_action": (
|
|
1025
|
+
"Reportar como desvio operacional leve; em ambiente já preparado, execute o workflow público "
|
|
1026
|
+
"e reporte bloqueios do payload em vez de sondar permissões."
|
|
1027
|
+
),
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def _subagent_batch_finding(tool_name: str, parameters: dict[str, Any]) -> dict[str, Any] | None:
|
|
1032
|
+
if tool_name != "invoke_agent":
|
|
1033
|
+
return None
|
|
1034
|
+
agent_name = str(parameters.get("agent_name") or "")
|
|
1035
|
+
if agent_name != "med-knowledge-architect":
|
|
1036
|
+
return None
|
|
1037
|
+
prompt = parameters.get("prompt")
|
|
1038
|
+
if not isinstance(prompt, str):
|
|
1039
|
+
return None
|
|
1040
|
+
work_ids = sorted(set(re.findall(r"style-rewrite-\d{3}-[a-z0-9-]+", prompt)))
|
|
1041
|
+
if len(work_ids) <= 1:
|
|
1042
|
+
return None
|
|
1043
|
+
return {
|
|
1044
|
+
"code": SUBAGENT_BATCH_CONTRACT_VIOLATION,
|
|
1045
|
+
"severity": "medium",
|
|
1046
|
+
"tool_name": tool_name,
|
|
1047
|
+
"bad_param": "prompt",
|
|
1048
|
+
"message": "Tool call invoke_agent batched multiple style rewrite work items into one med-knowledge-architect.",
|
|
1049
|
+
"work_item_count": len(work_ids),
|
|
1050
|
+
"work_ids": work_ids,
|
|
1051
|
+
"next_action": (
|
|
1052
|
+
"Reportar como bug de orquestração; lance um med-knowledge-architect por work_item.target_path."
|
|
1053
|
+
),
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def _subagent_raw_content_finding(tool_name: str, parameters: dict[str, Any]) -> dict[str, Any] | None:
|
|
1058
|
+
if tool_name == "invoke_agent":
|
|
1059
|
+
agent_name = str(parameters.get("agent_name") or "")
|
|
1060
|
+
if agent_name != "med-knowledge-architect":
|
|
1061
|
+
return None
|
|
1062
|
+
prompt = parameters.get("prompt")
|
|
1063
|
+
if not isinstance(prompt, str) or not _looks_like_raw_markdown_note(prompt):
|
|
1064
|
+
return None
|
|
1065
|
+
return {
|
|
1066
|
+
"code": SUBAGENT_RAW_CONTENT_CONTRACT_VIOLATION,
|
|
1067
|
+
"severity": "high",
|
|
1068
|
+
"tool_name": tool_name,
|
|
1069
|
+
"bad_param": "prompt",
|
|
1070
|
+
"message": "Tool call invoke_agent embedded raw Markdown note content in a med-knowledge-architect prompt.",
|
|
1071
|
+
"next_action": (
|
|
1072
|
+
"Reportar como bug de privacidade/orquestração; passe apenas work_item, target_path, "
|
|
1073
|
+
"rewrite_prompt e temp_output oficiais, sem colar conteúdo clínico no prompt pai."
|
|
1074
|
+
),
|
|
1075
|
+
}
|
|
1076
|
+
if tool_name not in _INTER_AGENT_MESSAGE_TOOLS:
|
|
1077
|
+
return None
|
|
1078
|
+
field_name, message = _first_string_parameter(parameters, _MESSAGE_PARAMETER_FIELDS)
|
|
1079
|
+
if not message or not _looks_like_raw_markdown_note(message):
|
|
1080
|
+
return None
|
|
1081
|
+
return {
|
|
1082
|
+
"code": SUBAGENT_RAW_CONTENT_CONTRACT_VIOLATION,
|
|
1083
|
+
"severity": "high",
|
|
1084
|
+
"tool_name": tool_name,
|
|
1085
|
+
"bad_param": field_name,
|
|
1086
|
+
"message": f"Tool call {tool_name} embedded raw Markdown note content in an inter-agent message.",
|
|
1087
|
+
"next_action": (
|
|
1088
|
+
"Reportar como bug de privacidade/orquestração; não cole conteúdo clínico no parent "
|
|
1089
|
+
"ou em mensagens entre agentes. Recomece pela rota oficial com o work_item tipado."
|
|
1090
|
+
),
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def _subagent_invocation_packet_findings(tool_name: str, parameters: dict[str, Any]) -> list[dict[str, Any]]:
|
|
1095
|
+
if tool_name not in {"invoke_agent", "invoke_subagent"}:
|
|
1096
|
+
return []
|
|
1097
|
+
findings: list[dict[str, Any]] = []
|
|
1098
|
+
for agent_name, field_name, prompt in _packaged_subagent_invocation_prompts(parameters):
|
|
1099
|
+
if agent_name != "med-knowledge-architect":
|
|
1100
|
+
continue
|
|
1101
|
+
if not _looks_like_style_rewrite_invocation_prompt(prompt):
|
|
1102
|
+
continue
|
|
1103
|
+
if _is_official_typed_style_rewrite_invocation_prompt(prompt):
|
|
1104
|
+
continue
|
|
1105
|
+
findings.append(
|
|
1106
|
+
{
|
|
1107
|
+
"code": SUBAGENT_INVOCATION_PACKET_CONTRACT_VIOLATION,
|
|
1108
|
+
"severity": "high",
|
|
1109
|
+
"tool_name": tool_name,
|
|
1110
|
+
"bad_param": field_name,
|
|
1111
|
+
"agent_name": agent_name,
|
|
1112
|
+
"message": (
|
|
1113
|
+
f"Tool call {tool_name} sent a handwritten med-knowledge-architect prompt instead "
|
|
1114
|
+
"of the official typed work item packet."
|
|
1115
|
+
),
|
|
1116
|
+
"next_action": (
|
|
1117
|
+
"Reportar como bug de orquestração; invoque o subagente com o work_item tipado do plano oficial, "
|
|
1118
|
+
"incluindo target_hash_before, temp_output e subagent_output_contract, sem instruções manuais extras."
|
|
1119
|
+
),
|
|
1120
|
+
}
|
|
1121
|
+
)
|
|
1122
|
+
return findings
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def _packaged_subagent_invocation_prompts(parameters: dict[str, Any]) -> list[tuple[str, str, str]]:
|
|
1126
|
+
prompts: list[tuple[str, str, str]] = []
|
|
1127
|
+
field_name, prompt = _first_string_parameter(parameters, _MESSAGE_PARAMETER_FIELDS)
|
|
1128
|
+
_, agent_name = _first_string_parameter(parameters, _AGENT_NAME_PARAMETER_FIELDS)
|
|
1129
|
+
if agent_name and prompt:
|
|
1130
|
+
prompts.append((agent_name, field_name, prompt))
|
|
1131
|
+
for list_field in _AGY_SUBAGENT_LIST_FIELDS:
|
|
1132
|
+
value = parameters.get(list_field)
|
|
1133
|
+
if not isinstance(value, list):
|
|
1134
|
+
continue
|
|
1135
|
+
for item in value:
|
|
1136
|
+
if not isinstance(item, dict):
|
|
1137
|
+
continue
|
|
1138
|
+
nested_field_name, nested_prompt = _first_string_parameter(item, _MESSAGE_PARAMETER_FIELDS)
|
|
1139
|
+
_, nested_agent_name = _first_string_parameter(item, _AGENT_NAME_PARAMETER_FIELDS)
|
|
1140
|
+
if nested_agent_name and nested_prompt:
|
|
1141
|
+
prompts.append((nested_agent_name, f"{list_field}[].{nested_field_name}", nested_prompt))
|
|
1142
|
+
return prompts
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _looks_like_style_rewrite_invocation_prompt(prompt: str) -> bool:
|
|
1146
|
+
lowered = prompt.casefold()
|
|
1147
|
+
return any(marker in lowered for marker in _STYLE_REWRITE_SUBAGENT_PROMPT_MARKERS)
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def _is_official_typed_style_rewrite_invocation_prompt(prompt: str) -> bool:
|
|
1151
|
+
if any(marker in prompt for marker in _HANDWRITTEN_SUBAGENT_PROMPT_MARKERS):
|
|
1152
|
+
return False
|
|
1153
|
+
return all(token in prompt for token in _STYLE_REWRITE_TYPED_WORK_ITEM_TOKENS)
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
def _looks_like_raw_markdown_note(text: str) -> bool:
|
|
1157
|
+
lowered = text.lower()
|
|
1158
|
+
has_raw_note_marker = any(
|
|
1159
|
+
marker in lowered for marker in ("material-fonte", "nota atual", "nota médica abaixo", "nota medica abaixo")
|
|
1160
|
+
)
|
|
1161
|
+
has_markdown_note = bool(re.search(r"(?m)^#\s+\S+", text) or "\n---\n" in text or "```markdown" in lowered)
|
|
1162
|
+
return has_raw_note_marker and has_markdown_note
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def _first_string_parameter(parameters: dict[str, Any], fields: tuple[str, ...]) -> tuple[str, str]:
|
|
1166
|
+
for field in fields:
|
|
1167
|
+
value = parameters.get(field)
|
|
1168
|
+
if isinstance(value, str) and value.strip():
|
|
1169
|
+
return field, value
|
|
1170
|
+
return "", ""
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def _parallel_style_rewrite_findings(transcript: Any) -> list[dict[str, Any]]:
|
|
1174
|
+
findings: list[dict[str, Any]] = []
|
|
1175
|
+
for batch in _iter_tool_call_batches(transcript):
|
|
1176
|
+
style_calls: list[dict[str, str]] = []
|
|
1177
|
+
dependent_families: list[str] = []
|
|
1178
|
+
for tool_name, parameters in batch:
|
|
1179
|
+
if _canonical_tool_name(tool_name) != "run_shell_command":
|
|
1180
|
+
continue
|
|
1181
|
+
command = _shell_command_text(parameters)
|
|
1182
|
+
for command_family in (
|
|
1183
|
+
"finalize-style-rewrite-output",
|
|
1184
|
+
"collect-style-rewrite-outputs",
|
|
1185
|
+
"apply-style-rewrite",
|
|
1186
|
+
):
|
|
1187
|
+
if command_family in command:
|
|
1188
|
+
dependent_families.append(command_family)
|
|
1189
|
+
if "apply-style-rewrite" in command or "apply-specialist-style-rewrite" in command:
|
|
1190
|
+
style_calls.append(
|
|
1191
|
+
{
|
|
1192
|
+
"mode": "dry_run" if "--dry-run" in command else "apply",
|
|
1193
|
+
"command": command,
|
|
1194
|
+
}
|
|
1195
|
+
)
|
|
1196
|
+
unique_dependent_families = sorted(set(dependent_families))
|
|
1197
|
+
if len(unique_dependent_families) > 1:
|
|
1198
|
+
findings.append(
|
|
1199
|
+
{
|
|
1200
|
+
"code": DEPENDENT_STYLE_REWRITE_BATCH_CONTRACT_VIOLATION,
|
|
1201
|
+
"severity": "high",
|
|
1202
|
+
"tool_name": "run_shell_command",
|
|
1203
|
+
"bad_param": "tool_batch",
|
|
1204
|
+
"command_families": unique_dependent_families,
|
|
1205
|
+
"message": "Dependent style-rewrite commands were emitted in the same tool batch.",
|
|
1206
|
+
"next_action": (
|
|
1207
|
+
"Reportar como bug de orquestração; use apply-specialist-style-rewrite para finalizar, "
|
|
1208
|
+
"coletar e aplicar um item em uma única chamada oficial."
|
|
1209
|
+
),
|
|
1210
|
+
}
|
|
1211
|
+
)
|
|
1212
|
+
if len(style_calls) <= 1:
|
|
1213
|
+
continue
|
|
1214
|
+
modes = sorted({item["mode"] for item in style_calls})
|
|
1215
|
+
findings.append(
|
|
1216
|
+
{
|
|
1217
|
+
"code": PARALLEL_STYLE_REWRITE_CONTRACT_VIOLATION,
|
|
1218
|
+
"severity": "medium",
|
|
1219
|
+
"tool_name": "run_shell_command",
|
|
1220
|
+
"bad_param": "tool_batch",
|
|
1221
|
+
"mode": "+".join(modes),
|
|
1222
|
+
"call_count": len(style_calls),
|
|
1223
|
+
"message": "Multiple apply-style-rewrite commands were emitted in the same tool batch.",
|
|
1224
|
+
"next_action": (
|
|
1225
|
+
"Reportar como bug de orquestração; valide e aplique cada rewrite em série, "
|
|
1226
|
+
"aguardando o resultado JSON antes do próximo comando."
|
|
1227
|
+
),
|
|
1228
|
+
}
|
|
1229
|
+
)
|
|
1230
|
+
return findings
|
|
1231
|
+
|
|
1232
|
+
|
|
1233
|
+
def _parallel_specialist_invocation_findings(transcript: Any) -> list[dict[str, Any]]:
|
|
1234
|
+
for batch in _iter_tool_call_batches(transcript):
|
|
1235
|
+
specialist_calls = [
|
|
1236
|
+
parameters
|
|
1237
|
+
for tool_name, parameters in batch
|
|
1238
|
+
if _is_style_rewrite_specialist_invocation(_canonical_tool_name(tool_name), parameters)
|
|
1239
|
+
]
|
|
1240
|
+
if len(specialist_calls) > 1:
|
|
1241
|
+
return [_parallel_specialist_invocation_finding(len(specialist_calls))]
|
|
1242
|
+
|
|
1243
|
+
active_tool_ids: list[str] = []
|
|
1244
|
+
active_count = 0
|
|
1245
|
+
for record in _iter_agent_tool_event_records(transcript):
|
|
1246
|
+
tool_name = _canonical_tool_name(_tool_name_from_record(record))
|
|
1247
|
+
event_type = str(record.get("type") or record.get("event_type") or "").casefold()
|
|
1248
|
+
tool_id = str(record.get("tool_id") or record.get("id") or "").strip()
|
|
1249
|
+
parameters = _tool_parameters_from_record(record) or {}
|
|
1250
|
+
if event_type == "tool_result":
|
|
1251
|
+
if tool_id and tool_id in active_tool_ids:
|
|
1252
|
+
active_tool_ids.remove(tool_id)
|
|
1253
|
+
active_count = max(0, active_count - 1)
|
|
1254
|
+
continue
|
|
1255
|
+
if event_type != "tool_use":
|
|
1256
|
+
continue
|
|
1257
|
+
if not _is_style_rewrite_specialist_invocation(tool_name, parameters):
|
|
1258
|
+
continue
|
|
1259
|
+
if active_count > 0:
|
|
1260
|
+
return [_parallel_specialist_invocation_finding(active_count + 1)]
|
|
1261
|
+
active_count += 1
|
|
1262
|
+
if tool_id:
|
|
1263
|
+
active_tool_ids.append(tool_id)
|
|
1264
|
+
return []
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
def _parallel_specialist_invocation_finding(call_count: int) -> dict[str, Any]:
|
|
1268
|
+
return {
|
|
1269
|
+
"code": SPECIALIST_PARALLEL_INVOCATION_CONTRACT_VIOLATION,
|
|
1270
|
+
"severity": "high",
|
|
1271
|
+
"tool_name": "invoke_agent",
|
|
1272
|
+
"bad_param": "tool_sequence",
|
|
1273
|
+
"call_count": call_count,
|
|
1274
|
+
"message": "Multiple med-knowledge-architect invoke_agent calls were started before a prior specialist receipt/result.",
|
|
1275
|
+
"next_action": (
|
|
1276
|
+
"Reportar como bug de orquestração; no Gemini CLI, execute o lote de reescrita em série, "
|
|
1277
|
+
"aguardando resultado e specialist_task_run_receipt_path antes do próximo invoke_agent."
|
|
1278
|
+
),
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
def _duplicate_specialist_invocation_findings(transcript: Any) -> list[dict[str, Any]]:
|
|
1283
|
+
seen_work_ids: set[str] = set()
|
|
1284
|
+
for record in _iter_agent_tool_event_records(transcript):
|
|
1285
|
+
event_type = str(record.get("type") or record.get("event_type") or "").casefold()
|
|
1286
|
+
if event_type != "tool_use":
|
|
1287
|
+
continue
|
|
1288
|
+
tool_name = _canonical_tool_name(_tool_name_from_record(record))
|
|
1289
|
+
parameters = _tool_parameters_from_record(record) or {}
|
|
1290
|
+
if not _is_style_rewrite_specialist_invocation(tool_name, parameters):
|
|
1291
|
+
continue
|
|
1292
|
+
work_id = _style_rewrite_work_id_from_parameters(parameters)
|
|
1293
|
+
if not work_id:
|
|
1294
|
+
continue
|
|
1295
|
+
if work_id in seen_work_ids:
|
|
1296
|
+
return [
|
|
1297
|
+
{
|
|
1298
|
+
"code": SPECIALIST_DUPLICATE_INVOCATION_CONTRACT_VIOLATION,
|
|
1299
|
+
"severity": "high",
|
|
1300
|
+
"tool_name": "invoke_agent",
|
|
1301
|
+
"bad_param": "tool_sequence",
|
|
1302
|
+
"work_id": work_id,
|
|
1303
|
+
"message": "The same style rewrite work item was sent to med-knowledge-architect more than once.",
|
|
1304
|
+
"next_action": (
|
|
1305
|
+
"Reportar como bug de orquestração; depois do primeiro invoke_agent, finalize com "
|
|
1306
|
+
"recibo oficial se existir, ou pare e reporte o bloqueio de recibo/modelo sem repetir "
|
|
1307
|
+
"o subagente para o mesmo work_id."
|
|
1308
|
+
),
|
|
1309
|
+
}
|
|
1310
|
+
]
|
|
1311
|
+
seen_work_ids.add(work_id)
|
|
1312
|
+
return []
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
def _continued_after_blocked_payload_findings(transcript: object) -> list[JsonObject]:
|
|
1316
|
+
blocked_reason = ""
|
|
1317
|
+
for record in _iter_agent_tool_event_records(transcript):
|
|
1318
|
+
event_type = str(record.get("type") or record.get("event_type") or "").casefold()
|
|
1319
|
+
tool_name = _canonical_tool_name(_tool_name_from_record(record))
|
|
1320
|
+
if event_type == "tool_result":
|
|
1321
|
+
output = str(record.get("output") or record.get("content") or "")
|
|
1322
|
+
found_reason = _blocked_payload_reason_from_output(output)
|
|
1323
|
+
if found_reason:
|
|
1324
|
+
blocked_reason = found_reason
|
|
1325
|
+
continue
|
|
1326
|
+
if event_type != "tool_use" or not blocked_reason:
|
|
1327
|
+
continue
|
|
1328
|
+
parameters = _tool_parameters_from_record(record) or {}
|
|
1329
|
+
if _is_allowed_after_blocked_payload_tool_use(tool_name, parameters, blocked_reason=blocked_reason):
|
|
1330
|
+
continue
|
|
1331
|
+
return [
|
|
1332
|
+
{
|
|
1333
|
+
"code": WORKFLOW_CONTINUED_AFTER_BLOCKED_PAYLOAD,
|
|
1334
|
+
"severity": "high",
|
|
1335
|
+
"tool_name": tool_name,
|
|
1336
|
+
"bad_param": "tool_sequence",
|
|
1337
|
+
"blocked_reason": blocked_reason,
|
|
1338
|
+
"message": "Agent continued executing tools after a workflow payload explicitly blocked continuation.",
|
|
1339
|
+
"next_action": (
|
|
1340
|
+
"Reportar como bug de orquestração; quando o payload disser WORKFLOW BLOQUEADO e "
|
|
1341
|
+
"next_command=null, pare a continuação pública e só feche a proteção do vault quando aplicável."
|
|
1342
|
+
),
|
|
1343
|
+
}
|
|
1344
|
+
]
|
|
1345
|
+
return []
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
def _blocked_payload_reason_from_output(output: str) -> str:
|
|
1349
|
+
if not output or "blocked" not in output or "blocked_reason" not in output:
|
|
1350
|
+
return ""
|
|
1351
|
+
patterns = (
|
|
1352
|
+
r"blocked_reason:\s*([A-Za-z0-9_.-]+)",
|
|
1353
|
+
r'"blocked_reason"\s*:\s*"([^"]+)"',
|
|
1354
|
+
)
|
|
1355
|
+
for pattern in patterns:
|
|
1356
|
+
match = re.search(pattern, output)
|
|
1357
|
+
if match:
|
|
1358
|
+
return match.group(1)
|
|
1359
|
+
return ""
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
def _is_allowed_after_blocked_payload_tool_use(
|
|
1363
|
+
tool_name: str,
|
|
1364
|
+
parameters: JsonObject,
|
|
1365
|
+
*,
|
|
1366
|
+
blocked_reason: str,
|
|
1367
|
+
) -> bool:
|
|
1368
|
+
if tool_name in {"tracker_create_task", "tracker_update_task", "tracker_visualize", "update_topic"}:
|
|
1369
|
+
return True
|
|
1370
|
+
if blocked_reason in _RETRYABLE_SPECIALIST_BLOCKED_REASONS:
|
|
1371
|
+
return _is_style_rewrite_specialist_invocation(tool_name, parameters)
|
|
1372
|
+
if tool_name != "run_shell_command":
|
|
1373
|
+
return False
|
|
1374
|
+
command = _shell_command_text(parameters)
|
|
1375
|
+
if "vault_git.py" in command and "run-finish" in command:
|
|
1376
|
+
return True
|
|
1377
|
+
return False
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
def _is_style_rewrite_specialist_invocation(tool_name: str, parameters: dict[str, Any]) -> bool:
|
|
1381
|
+
if tool_name != "invoke_agent":
|
|
1382
|
+
return False
|
|
1383
|
+
agent_name = str(parameters.get("agent_name") or parameters.get("name") or "").strip()
|
|
1384
|
+
if agent_name == "med-knowledge-architect":
|
|
1385
|
+
return True
|
|
1386
|
+
_field_name, prompt = _first_string_parameter(parameters, _MESSAGE_PARAMETER_FIELDS)
|
|
1387
|
+
return "med-knowledge-architect" in prompt or "style-rewrite-" in prompt
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
def _style_rewrite_work_id_from_parameters(parameters: dict[str, Any]) -> str:
|
|
1391
|
+
_field_name, prompt = _first_string_parameter(parameters, _MESSAGE_PARAMETER_FIELDS)
|
|
1392
|
+
match = re.search(r"style-rewrite-\d{3}-[a-z0-9-]+", prompt)
|
|
1393
|
+
return match.group(0) if match else ""
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
def _iter_agent_tool_event_records(node: Any) -> list[dict[str, Any]]:
|
|
1397
|
+
records: list[dict[str, Any]] = []
|
|
1398
|
+
|
|
1399
|
+
def visit(value: Any) -> None:
|
|
1400
|
+
if isinstance(value, list):
|
|
1401
|
+
for item in value:
|
|
1402
|
+
visit(item)
|
|
1403
|
+
return
|
|
1404
|
+
if not isinstance(value, dict):
|
|
1405
|
+
return
|
|
1406
|
+
event_type = str(value.get("type") or value.get("event_type") or "").casefold()
|
|
1407
|
+
if event_type in {"tool_use", "tool_result"}:
|
|
1408
|
+
records.append(value)
|
|
1409
|
+
for key in _TRANSCRIPT_CHILD_CONTAINER_KEYS:
|
|
1410
|
+
child = value.get(key)
|
|
1411
|
+
if isinstance(child, (dict, list)):
|
|
1412
|
+
visit(child)
|
|
1413
|
+
|
|
1414
|
+
visit(node)
|
|
1415
|
+
return records
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
def _invalid_extension_command_path_finding(tool_name: str, parameters: dict[str, Any]) -> dict[str, Any] | None:
|
|
1419
|
+
if tool_name != "run_shell_command":
|
|
1420
|
+
return None
|
|
1421
|
+
command = _shell_command_text(parameters)
|
|
1422
|
+
if "dist/gemini-cli-" not in command:
|
|
1423
|
+
return None
|
|
1424
|
+
path_hits = sorted(
|
|
1425
|
+
path for path in set(re.findall(r"""[^\s"']*dist/gemini-cli-[^\s"']+""", command))
|
|
1426
|
+
if "dist/gemini-cli-extension" not in path
|
|
1427
|
+
)
|
|
1428
|
+
if not path_hits:
|
|
1429
|
+
return None
|
|
1430
|
+
return {
|
|
1431
|
+
"code": INVALID_EXTENSION_COMMAND_PATH,
|
|
1432
|
+
"severity": "high",
|
|
1433
|
+
"tool_name": tool_name,
|
|
1434
|
+
"bad_param": "command",
|
|
1435
|
+
"message": "Tool call run_shell_command referenced a non-canonical Gemini extension dist path.",
|
|
1436
|
+
"paths": path_hits[:5],
|
|
1437
|
+
"next_action": (
|
|
1438
|
+
"Reportar como bug de descoberta de caminho; use somente o extensionPath carregado "
|
|
1439
|
+
"pelo bundle ativo e não invente variantes de dist/gemini-cli-extension."
|
|
1440
|
+
),
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
def _stale_extension_script_path_finding(
|
|
1445
|
+
tool_name: str,
|
|
1446
|
+
parameters: dict[str, Any],
|
|
1447
|
+
*,
|
|
1448
|
+
agy_plugin_context: bool = False,
|
|
1449
|
+
) -> dict[str, Any] | None:
|
|
1450
|
+
if tool_name != "run_shell_command":
|
|
1451
|
+
return None
|
|
1452
|
+
command = _shell_command_text(parameters)
|
|
1453
|
+
if agy_plugin_context and re.search(
|
|
1454
|
+
r"(?:~|/Users/[^/\s\"']+)/\.gemini/extensions/medical-notes-workbench/",
|
|
1455
|
+
command,
|
|
1456
|
+
):
|
|
1457
|
+
return {
|
|
1458
|
+
"code": STALE_EXTENSION_SCRIPT_PATH,
|
|
1459
|
+
"severity": "high",
|
|
1460
|
+
"tool_name": tool_name,
|
|
1461
|
+
"bad_param": "command",
|
|
1462
|
+
"message": "Tool call run_shell_command used the global Gemini extension path while the session was running from an AGY plugin root.",
|
|
1463
|
+
"next_action": (
|
|
1464
|
+
"Reportar como bug de descoberta de caminho; carregue a skill escopada do plugin "
|
|
1465
|
+
"e use o extensionPath ativo em vez de ~/.gemini/extensions/medical-notes-workbench."
|
|
1466
|
+
),
|
|
1467
|
+
}
|
|
1468
|
+
if re.search(r"scripts[/\\]mednotes[/\\]vault[/\\]vault_git\.py", command):
|
|
1469
|
+
return {
|
|
1470
|
+
"code": STALE_EXTENSION_SCRIPT_PATH,
|
|
1471
|
+
"severity": "high",
|
|
1472
|
+
"tool_name": tool_name,
|
|
1473
|
+
"bad_param": "command",
|
|
1474
|
+
"message": "Tool call run_shell_command referenced stale vault_git.py path under scripts/mednotes/vault.",
|
|
1475
|
+
"next_action": (
|
|
1476
|
+
"Reportar como bug de descoberta de caminho; use scripts/vault/vault_git.py do bundle ativo."
|
|
1477
|
+
),
|
|
1478
|
+
}
|
|
1479
|
+
return None
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
def _stale_extension_skill_findings(transcript: Any, *, agy_plugin_context: bool = False) -> list[dict[str, Any]]:
|
|
1483
|
+
if not agy_plugin_context:
|
|
1484
|
+
return []
|
|
1485
|
+
if not _transcript_contains_stale_skill_view(transcript):
|
|
1486
|
+
return []
|
|
1487
|
+
return [
|
|
1488
|
+
{
|
|
1489
|
+
"code": STALE_EXTENSION_SKILL_PATH,
|
|
1490
|
+
"severity": "high",
|
|
1491
|
+
"tool_name": "view_file",
|
|
1492
|
+
"bad_param": "path",
|
|
1493
|
+
"path": "~/.gemini/config/skills/fix-medical-wiki/SKILL.md",
|
|
1494
|
+
"message": "Agent loaded the unscoped global fix-medical-wiki skill after loading the AGY plugin launcher.",
|
|
1495
|
+
"next_action": (
|
|
1496
|
+
"Reportar como bug de skill routing; o launcher deve carregar "
|
|
1497
|
+
"${extensionPath}/skills/fix-medical-wiki/SKILL.md por path escopado."
|
|
1498
|
+
),
|
|
1499
|
+
}
|
|
1500
|
+
]
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
def _transcript_contains_stale_skill_view(transcript: Any) -> bool:
|
|
1504
|
+
stale_path = ".gemini/config/skills/fix-medical-wiki/skill.md"
|
|
1505
|
+
|
|
1506
|
+
def visit(value: Any) -> bool:
|
|
1507
|
+
if isinstance(value, list):
|
|
1508
|
+
return any(visit(item) for item in value)
|
|
1509
|
+
if not isinstance(value, dict):
|
|
1510
|
+
return False
|
|
1511
|
+
event_type = str(value.get("type") or "").lower()
|
|
1512
|
+
content = str(value.get("content") or "").lower()
|
|
1513
|
+
if event_type == "view_file" and stale_path in content:
|
|
1514
|
+
return True
|
|
1515
|
+
tool_name = _canonical_tool_name(_tool_name_from_record(value))
|
|
1516
|
+
parameters = _tool_parameters_from_record(value)
|
|
1517
|
+
if tool_name in {"view_file", "read_file"} and isinstance(parameters, dict):
|
|
1518
|
+
for raw in parameters.values():
|
|
1519
|
+
if isinstance(raw, str) and stale_path in raw.lower():
|
|
1520
|
+
return True
|
|
1521
|
+
for key in _TRANSCRIPT_CHILD_CONTAINER_KEYS:
|
|
1522
|
+
child = value.get(key)
|
|
1523
|
+
if isinstance(child, (dict, list)) and visit(child):
|
|
1524
|
+
return True
|
|
1525
|
+
return False
|
|
1526
|
+
|
|
1527
|
+
return visit(transcript)
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
def _shell_chain_finding(tool_name: str, parameters: dict[str, Any]) -> dict[str, Any] | None:
|
|
1531
|
+
if tool_name != "run_shell_command":
|
|
1532
|
+
return None
|
|
1533
|
+
command = _shell_command_text(parameters)
|
|
1534
|
+
operator = _first_unquoted_shell_chain_operator(command)
|
|
1535
|
+
if not operator:
|
|
1536
|
+
return None
|
|
1537
|
+
return {
|
|
1538
|
+
"code": SHELL_CHAIN_CONTRACT_VIOLATION,
|
|
1539
|
+
"severity": "medium",
|
|
1540
|
+
"tool_name": tool_name,
|
|
1541
|
+
"bad_param": "command",
|
|
1542
|
+
"operator": operator,
|
|
1543
|
+
"message": "Tool call run_shell_command chained multiple shell operations in one command.",
|
|
1544
|
+
"next_action": (
|
|
1545
|
+
"Reportar como bug de orquestração; emita uma tool call por comando e aguarde cada JSON/exit code."
|
|
1546
|
+
),
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
def _public_dev_escape_finding(tool_name: str, parameters: dict[str, Any]) -> dict[str, Any] | None:
|
|
1551
|
+
if tool_name != "run_shell_command":
|
|
1552
|
+
return None
|
|
1553
|
+
command = _shell_command_text(parameters)
|
|
1554
|
+
if not command:
|
|
1555
|
+
return None
|
|
1556
|
+
if not re.search(r"\bMEDNOTES_ALLOW_DEV_ESCAPE\s*=\s*(?:1|true|yes)\b", command, re.IGNORECASE):
|
|
1557
|
+
if not re.search(r"\b--skip-prompt-eval\b", command):
|
|
1558
|
+
return None
|
|
1559
|
+
return {
|
|
1560
|
+
"code": PUBLIC_DEV_ESCAPE_CONTRACT_VIOLATION,
|
|
1561
|
+
"severity": "high",
|
|
1562
|
+
"tool_name": "run_shell_command",
|
|
1563
|
+
"bad_param": "command",
|
|
1564
|
+
"message": "Tool call run_shell_command attempted to use a developer escape in a public workflow.",
|
|
1565
|
+
"next_action": (
|
|
1566
|
+
"Reportar como bug de orquestração; pare a execução pública e retome pela rota oficial "
|
|
1567
|
+
"com recibo/proveniência tipados, sem MEDNOTES_ALLOW_DEV_ESCAPE ou --skip-prompt-eval."
|
|
1568
|
+
),
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
def _style_rewrite_direct_content_apply_finding(tool_name: str, parameters: dict[str, Any]) -> dict[str, Any] | None:
|
|
1573
|
+
if tool_name != "run_shell_command":
|
|
1574
|
+
return None
|
|
1575
|
+
command = _shell_command_text(parameters)
|
|
1576
|
+
if not command or "apply-style-rewrite" not in command:
|
|
1577
|
+
return None
|
|
1578
|
+
if "--target" not in command or "--content" not in command or "--dry-run" in command:
|
|
1579
|
+
return None
|
|
1580
|
+
return {
|
|
1581
|
+
"code": STYLE_REWRITE_DIRECT_CONTENT_APPLY,
|
|
1582
|
+
"severity": "high",
|
|
1583
|
+
"tool_name": "run_shell_command",
|
|
1584
|
+
"bad_param": "command",
|
|
1585
|
+
"message": "Agent attempted to apply a style rewrite from loose --target/--content paths.",
|
|
1586
|
+
"next_action": (
|
|
1587
|
+
"Reportar como bug de orquestração; aplique reescrita médica somente por "
|
|
1588
|
+
"apply-specialist-style-rewrite com plan, manifest, work_id e recibo especialista oficial."
|
|
1589
|
+
),
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
|
|
1593
|
+
def _style_rewrite_unverified_model_claim_finding(tool_name: str, parameters: dict[str, Any]) -> dict[str, Any] | None:
|
|
1594
|
+
if tool_name != "run_shell_command":
|
|
1595
|
+
return None
|
|
1596
|
+
command = _shell_command_text(parameters)
|
|
1597
|
+
if not command or "finalize-style-rewrite-output" not in command:
|
|
1598
|
+
return None
|
|
1599
|
+
if "--specialist-run-receipt" in command:
|
|
1600
|
+
return None
|
|
1601
|
+
if "--actual-model" not in command and "--provider" not in command:
|
|
1602
|
+
return None
|
|
1603
|
+
return {
|
|
1604
|
+
"code": STYLE_REWRITE_UNVERIFIED_MODEL_CLAIM,
|
|
1605
|
+
"severity": "high",
|
|
1606
|
+
"tool_name": "run_shell_command",
|
|
1607
|
+
"bad_param": "command",
|
|
1608
|
+
"message": "Agent attempted to finalize a specialist rewrite using parent-declared model provenance.",
|
|
1609
|
+
"next_action": (
|
|
1610
|
+
"Reportar como bug de orquestração; o parent não pode declarar Pro/Flash manualmente. "
|
|
1611
|
+
"Use somente specialist-task-run-receipt.v1 validado pelo Workbench."
|
|
1612
|
+
),
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
|
|
1616
|
+
def _specialist_unverified_model_escape_finding(tool_name: str, parameters: dict[str, Any]) -> dict[str, Any] | None:
|
|
1617
|
+
field_name, text = _first_parameter_containing(parameters, _UNVERIFIED_SPECIALIST_MODEL_ENV)
|
|
1618
|
+
if not text:
|
|
1619
|
+
return None
|
|
1620
|
+
return {
|
|
1621
|
+
"code": SPECIALIST_UNVERIFIED_MODEL_ESCAPE,
|
|
1622
|
+
"severity": "high",
|
|
1623
|
+
"tool_name": tool_name,
|
|
1624
|
+
"bad_param": field_name,
|
|
1625
|
+
"message": "Agent attempted to enable the unverified specialist model escape during a public workflow.",
|
|
1626
|
+
"next_action": (
|
|
1627
|
+
"Reportar como bug de orquestração; reescrita médica pública só pode avançar com "
|
|
1628
|
+
"specialist-task-run-receipt.v1 validado, sem MEDNOTES_ALLOW_UNVERIFIED_SPECIALIST_MODEL."
|
|
1629
|
+
),
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
|
|
1633
|
+
def _style_rewrite_parent_output_write_finding(tool_name: str, parameters: dict[str, Any]) -> dict[str, Any] | None:
|
|
1634
|
+
if tool_name not in _WORKFLOW_ARTIFACT_WRITE_TOOLS:
|
|
1635
|
+
return None
|
|
1636
|
+
for field, path in _workflow_artifact_write_paths(parameters):
|
|
1637
|
+
if not _looks_like_style_rewrite_parent_output_path(path):
|
|
1638
|
+
continue
|
|
1639
|
+
return {
|
|
1640
|
+
"code": STYLE_REWRITE_PARENT_OUTPUT_WRITE,
|
|
1641
|
+
"severity": "high",
|
|
1642
|
+
"tool_name": tool_name,
|
|
1643
|
+
"bad_param": field,
|
|
1644
|
+
"path": path,
|
|
1645
|
+
"message": (
|
|
1646
|
+
"Agent directly wrote a style-rewrite output artifact that must be produced by "
|
|
1647
|
+
"the specialist runner and Workbench finalization commands."
|
|
1648
|
+
),
|
|
1649
|
+
"next_action": (
|
|
1650
|
+
"Reportar como bug de autoria/recibo; o parent deve chamar o especialista oficial "
|
|
1651
|
+
"e depois apply-specialist-style-rewrite, nunca escrever .rewrite.md, attestation "
|
|
1652
|
+
"ou receipt por write_file/write_to_file."
|
|
1653
|
+
),
|
|
1654
|
+
}
|
|
1655
|
+
return None
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
def _process_chats_raw_write_finding(tool_name: str, parameters: dict[str, Any]) -> dict[str, Any] | None:
|
|
1659
|
+
if tool_name not in _WORKFLOW_ARTIFACT_WRITE_TOOLS:
|
|
1660
|
+
return None
|
|
1661
|
+
for field, path in _workflow_artifact_write_paths(parameters):
|
|
1662
|
+
if not _looks_like_chats_raw_path(path):
|
|
1663
|
+
continue
|
|
1664
|
+
return {
|
|
1665
|
+
"code": PROCESS_CHATS_RAW_WRITE,
|
|
1666
|
+
"severity": "high",
|
|
1667
|
+
"tool_name": tool_name,
|
|
1668
|
+
"bad_param": field,
|
|
1669
|
+
"path": path,
|
|
1670
|
+
"message": "Agent attempted to write a raw chat file during process-chats instead of using wiki/cli.py.",
|
|
1671
|
+
"next_action": (
|
|
1672
|
+
"Reportar como bug de integridade; raw chat body é imutável e YAML/status "
|
|
1673
|
+
"só pode ser mutado por wiki/cli.py triage/discard/publish-batch."
|
|
1674
|
+
),
|
|
1675
|
+
}
|
|
1676
|
+
return None
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
def _process_chats_parent_artifact_write_without_subagent_finding(
|
|
1680
|
+
tool_name: str,
|
|
1681
|
+
parameters: dict[str, Any],
|
|
1682
|
+
*,
|
|
1683
|
+
process_chats_specialist_seen: bool,
|
|
1684
|
+
) -> dict[str, Any] | None:
|
|
1685
|
+
if process_chats_specialist_seen:
|
|
1686
|
+
return None
|
|
1687
|
+
if tool_name not in _WORKFLOW_ARTIFACT_WRITE_TOOLS:
|
|
1688
|
+
return None
|
|
1689
|
+
for field, path in _workflow_artifact_write_paths(parameters):
|
|
1690
|
+
if not _looks_like_process_chats_generated_artifact_path(path):
|
|
1691
|
+
continue
|
|
1692
|
+
return {
|
|
1693
|
+
"code": PROCESS_CHATS_PARENT_ARTIFACT_WRITE_WITHOUT_SUBAGENT,
|
|
1694
|
+
"severity": "high",
|
|
1695
|
+
"tool_name": tool_name,
|
|
1696
|
+
"bad_param": field,
|
|
1697
|
+
"path": path,
|
|
1698
|
+
"message": "Agent wrote a process-chats artifact before any specialist/subagent invocation.",
|
|
1699
|
+
"next_action": (
|
|
1700
|
+
"Reportar como bug de autoria; use plan-subagents e um subagent/runner oficial "
|
|
1701
|
+
"antes de salvar note_plan, coverage, manifest ou Markdown temporário."
|
|
1702
|
+
),
|
|
1703
|
+
}
|
|
1704
|
+
return None
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
def _workflow_source_discovery_after_block_finding(tool_name: str, parameters: JsonObject) -> JsonObject | None:
|
|
1708
|
+
command = _shell_command_text(parameters) if tool_name == "run_shell_command" else ""
|
|
1709
|
+
file_path = ""
|
|
1710
|
+
if tool_name in {"read_file", "view_file"}:
|
|
1711
|
+
_field, file_path = _first_string_parameter(parameters, ("file_path", "path", "absolute_path"))
|
|
1712
|
+
if command:
|
|
1713
|
+
lowered_command = command.lower()
|
|
1714
|
+
source_probe = bool(
|
|
1715
|
+
"bundle/scripts/mednotes" in lowered_command
|
|
1716
|
+
and (
|
|
1717
|
+
(
|
|
1718
|
+
re.search(r"\b(?:grep|rg)\b", lowered_command)
|
|
1719
|
+
and (
|
|
1720
|
+
"mednotes_allow_dev_escape" in lowered_command
|
|
1721
|
+
or "specialist-task-run-receipt" in lowered_command
|
|
1722
|
+
or "apply-style-rewrite" in lowered_command
|
|
1723
|
+
or "finalize-style-rewrite-output" in lowered_command
|
|
1724
|
+
)
|
|
1725
|
+
)
|
|
1726
|
+
or (
|
|
1727
|
+
re.search(r"\b(?:cat|sed|nl|head|tail|less)\b", lowered_command)
|
|
1728
|
+
and re.search(r"bundle/scripts/mednotes/[^\"' ]+\.py\b", lowered_command)
|
|
1729
|
+
)
|
|
1730
|
+
)
|
|
1731
|
+
)
|
|
1732
|
+
else:
|
|
1733
|
+
lowered_path = file_path.lower()
|
|
1734
|
+
source_probe = "bundle/scripts/mednotes/" in lowered_path and lowered_path.endswith(".py")
|
|
1735
|
+
if not source_probe:
|
|
1736
|
+
return None
|
|
1737
|
+
return {
|
|
1738
|
+
"code": WORKFLOW_SOURCE_DISCOVERY_AFTER_BLOCK,
|
|
1739
|
+
"severity": "medium",
|
|
1740
|
+
"tool_name": tool_name,
|
|
1741
|
+
"bad_param": "command" if command else "file_path",
|
|
1742
|
+
"message": "Agent inspected Workbench source code while executing a public workflow instead of following the typed payload.",
|
|
1743
|
+
"next_action": (
|
|
1744
|
+
"Reportar como atrito de UX; o workflow deve oferecer continuação oficial ou bloqueio terminal, "
|
|
1745
|
+
"sem induzir o agente a procurar bypass em código-fonte."
|
|
1746
|
+
),
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
|
|
1750
|
+
def _python_environment_probe_finding(tool_name: str, parameters: dict[str, Any]) -> dict[str, Any] | None:
|
|
1751
|
+
if tool_name != "run_shell_command":
|
|
1752
|
+
return None
|
|
1753
|
+
command = _shell_command_text(parameters)
|
|
1754
|
+
if not re.search(r"\bpython(?:3(?:\.\d+)?)?\s+-c\b", command):
|
|
1755
|
+
return None
|
|
1756
|
+
if not any(
|
|
1757
|
+
marker in command for marker in ("MEDNOTES", "MEDICAL_NOTES_WORKBENCH", "GEMINI", "medical-notes-workbench")
|
|
1758
|
+
):
|
|
1759
|
+
return None
|
|
1760
|
+
if "uv run" in command or "scripts/run_python.mjs" in command or "wiki/cli.py" in command:
|
|
1761
|
+
return None
|
|
1762
|
+
return {
|
|
1763
|
+
"code": NONCANONICAL_PYTHON_ENVIRONMENT_PROBE,
|
|
1764
|
+
"severity": "medium",
|
|
1765
|
+
"tool_name": tool_name,
|
|
1766
|
+
"bad_param": "command",
|
|
1767
|
+
"message": "Tool call run_shell_command used ad hoc python -c to inspect Workbench environment state.",
|
|
1768
|
+
"next_action": (
|
|
1769
|
+
"Reportar como desvio operacional; use a rota oficial do Workbench ou um recibo tipado, "
|
|
1770
|
+
"e inclua o probe no relatório final se ele já ocorreu."
|
|
1771
|
+
),
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
def _workflow_artifact_direct_write_finding(tool_name: str, parameters: JsonObject) -> JsonObject | None:
|
|
1776
|
+
if tool_name not in _WORKFLOW_ARTIFACT_WRITE_TOOLS:
|
|
1777
|
+
return None
|
|
1778
|
+
for field, path in _workflow_artifact_write_paths(parameters):
|
|
1779
|
+
if not _looks_like_workflow_artifact_path(path):
|
|
1780
|
+
continue
|
|
1781
|
+
return {
|
|
1782
|
+
"code": WORKFLOW_ARTIFACT_DIRECT_WRITE,
|
|
1783
|
+
"severity": "high",
|
|
1784
|
+
"tool_name": tool_name,
|
|
1785
|
+
"bad_param": field,
|
|
1786
|
+
"path": path,
|
|
1787
|
+
"message": f"Tool call {tool_name} directly modified a workflow artifact that must be produced by wiki/cli.py.",
|
|
1788
|
+
"next_action": (
|
|
1789
|
+
"Reportar como bug de orquestração; regenere o artefato pela rota oficial do "
|
|
1790
|
+
"wiki/cli.py e não use write_file/replace/edit para plans, manifests, receipts ou reports."
|
|
1791
|
+
),
|
|
1792
|
+
}
|
|
1793
|
+
return None
|
|
1794
|
+
|
|
1795
|
+
|
|
1796
|
+
def _workflow_artifact_shell_copy_finding(tool_name: str, parameters: JsonObject) -> JsonObject | None:
|
|
1797
|
+
if tool_name != "run_shell_command":
|
|
1798
|
+
return None
|
|
1799
|
+
command = _shell_command_text(parameters)
|
|
1800
|
+
if not command or not re.search(r"\b(cp|copy|Copy-Item)\b", command):
|
|
1801
|
+
return None
|
|
1802
|
+
normalized = command.replace("\\", "/")
|
|
1803
|
+
if "/.gemini/antigravity-cli/scratch/" not in normalized:
|
|
1804
|
+
return None
|
|
1805
|
+
artifact_names = _workflow_artifact_names_in_command(normalized)
|
|
1806
|
+
if not artifact_names:
|
|
1807
|
+
return None
|
|
1808
|
+
return {
|
|
1809
|
+
"code": WORKFLOW_ARTIFACT_SHELL_COPY,
|
|
1810
|
+
"severity": "medium",
|
|
1811
|
+
"tool_name": tool_name,
|
|
1812
|
+
"bad_param": "command",
|
|
1813
|
+
"artifact_names": artifact_names,
|
|
1814
|
+
"message": "Tool call run_shell_command copied workflow artifacts into AGY scratch outside the official run directory.",
|
|
1815
|
+
"next_action": (
|
|
1816
|
+
"Reportar como contaminação do experimento; preserve artefatos na pasta oficial da rodada "
|
|
1817
|
+
"ou no diretório lab artifact, não em scratch global."
|
|
1818
|
+
),
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
|
|
1822
|
+
def _workflow_artifact_shell_redirect_finding(tool_name: str, parameters: JsonObject) -> JsonObject | None:
|
|
1823
|
+
if tool_name != "run_shell_command":
|
|
1824
|
+
return None
|
|
1825
|
+
command = _shell_command_text(parameters)
|
|
1826
|
+
if not command or not _has_shell_stdout_redirect(command):
|
|
1827
|
+
return None
|
|
1828
|
+
normalized = command.replace("\\", "/")
|
|
1829
|
+
if "/.gemini/antigravity-cli/scratch/" not in normalized:
|
|
1830
|
+
return None
|
|
1831
|
+
artifact_names = _workflow_artifact_names_in_command(normalized)
|
|
1832
|
+
if not artifact_names:
|
|
1833
|
+
return None
|
|
1834
|
+
return {
|
|
1835
|
+
"code": WORKFLOW_ARTIFACT_SHELL_REDIRECT,
|
|
1836
|
+
"severity": "medium",
|
|
1837
|
+
"tool_name": tool_name,
|
|
1838
|
+
"bad_param": "command",
|
|
1839
|
+
"artifact_names": artifact_names,
|
|
1840
|
+
"message": "Tool call run_shell_command redirected workflow output into AGY scratch outside the official run directory.",
|
|
1841
|
+
"next_action": (
|
|
1842
|
+
"Reportar como contaminação do experimento; use os artefatos oficiais emitidos pelo workflow "
|
|
1843
|
+
"ou o diretório lab artifact, não scratch global."
|
|
1844
|
+
),
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
|
|
1848
|
+
def _duplicate_workflow_command_findings(transcript: object) -> list[JsonObject]:
|
|
1849
|
+
counts: dict[tuple[str, str], int] = {}
|
|
1850
|
+
for tool_name, parameters in _iter_agent_tool_calls(transcript):
|
|
1851
|
+
if _canonical_tool_name(tool_name) != "run_shell_command":
|
|
1852
|
+
continue
|
|
1853
|
+
command = _shell_command_text(parameters)
|
|
1854
|
+
workflow_key = _workflow_command_key(command)
|
|
1855
|
+
if not workflow_key:
|
|
1856
|
+
continue
|
|
1857
|
+
counts[workflow_key] = counts.get(workflow_key, 0) + 1
|
|
1858
|
+
|
|
1859
|
+
findings: list[JsonObject] = []
|
|
1860
|
+
for (workflow, mode), count in sorted(counts.items()):
|
|
1861
|
+
if count <= 1:
|
|
1862
|
+
continue
|
|
1863
|
+
findings.append(
|
|
1864
|
+
{
|
|
1865
|
+
"code": DUPLICATE_WORKFLOW_COMMAND,
|
|
1866
|
+
"severity": "medium",
|
|
1867
|
+
"tool_name": "run_shell_command",
|
|
1868
|
+
"workflow": workflow,
|
|
1869
|
+
"mode": mode,
|
|
1870
|
+
"count": count,
|
|
1871
|
+
"message": f"Agent invoked the same {workflow} {mode} workflow more than once in one session.",
|
|
1872
|
+
"next_action": (
|
|
1873
|
+
"Reportar como desvio operacional; leia compact_report/full_report ou artefatos oficiais em vez de repetir "
|
|
1874
|
+
"o workflow sem mudança de entrada."
|
|
1875
|
+
),
|
|
1876
|
+
}
|
|
1877
|
+
)
|
|
1878
|
+
return findings
|
|
1879
|
+
|
|
1880
|
+
|
|
1881
|
+
def _workflow_command_key(command: str) -> tuple[str, str] | None:
|
|
1882
|
+
if not command:
|
|
1883
|
+
return None
|
|
1884
|
+
if re.search(r"(?:^|[\s\"'])fix-wiki(?:\s|$)", command) and "--dry-run" in command and "--apply" not in command:
|
|
1885
|
+
return ("/mednotes:fix-wiki", "preview")
|
|
1886
|
+
return None
|
|
1887
|
+
|
|
1888
|
+
|
|
1889
|
+
def _final_artifact_path_findings(transcript: Any) -> list[dict[str, Any]]:
|
|
1890
|
+
findings: list[dict[str, Any]] = []
|
|
1891
|
+
for path in _final_response_file_uri_paths(transcript):
|
|
1892
|
+
if not _looks_like_reported_workflow_artifact_path(path):
|
|
1893
|
+
continue
|
|
1894
|
+
if Path(path).exists():
|
|
1895
|
+
continue
|
|
1896
|
+
findings.append(
|
|
1897
|
+
{
|
|
1898
|
+
"code": FINAL_ARTIFACT_PATH_INVALID,
|
|
1899
|
+
"severity": "medium",
|
|
1900
|
+
"tool_name": "planner_response",
|
|
1901
|
+
"bad_param": "content",
|
|
1902
|
+
"path": path,
|
|
1903
|
+
"artifact_name": path.replace("\\", "/").rsplit("/", 1)[-1],
|
|
1904
|
+
"message": "Agent final response linked a workflow artifact path that does not exist.",
|
|
1905
|
+
"next_action": (
|
|
1906
|
+
"Reportar como bug de relatório final; use somente os caminhos oficiais emitidos pelo workflow "
|
|
1907
|
+
"e confira existência antes de publicar links de artefato."
|
|
1908
|
+
),
|
|
1909
|
+
}
|
|
1910
|
+
)
|
|
1911
|
+
return findings
|
|
1912
|
+
|
|
1913
|
+
|
|
1914
|
+
def _manual_packaged_subagent_findings(transcript: Any) -> list[dict[str, Any]]:
|
|
1915
|
+
findings: list[dict[str, Any]] = []
|
|
1916
|
+
seen: set[str] = set()
|
|
1917
|
+
valid_packaged_definitions: set[str] = set()
|
|
1918
|
+
|
|
1919
|
+
def append(agent_name: str) -> None:
|
|
1920
|
+
if agent_name not in _PACKAGED_SPECIALIST_AGENTS:
|
|
1921
|
+
return
|
|
1922
|
+
if agent_name in valid_packaged_definitions:
|
|
1923
|
+
return
|
|
1924
|
+
if agent_name in seen:
|
|
1925
|
+
return
|
|
1926
|
+
seen.add(agent_name)
|
|
1927
|
+
findings.append(
|
|
1928
|
+
{
|
|
1929
|
+
"code": MANUAL_SUBAGENT_CONTRACT_VIOLATION,
|
|
1930
|
+
"severity": "high",
|
|
1931
|
+
"tool_name": "define_subagent",
|
|
1932
|
+
"bad_param": "agent_name",
|
|
1933
|
+
"agent_name": agent_name,
|
|
1934
|
+
"message": f"Agent manually defined the packaged {agent_name} instead of using the bundled agent.",
|
|
1935
|
+
"next_action": (
|
|
1936
|
+
"Reportar como bug de orquestração/modelo; para style_rewrite no AGY, leia o template "
|
|
1937
|
+
"empacotado completo, use define_subagent autorizado e finalize com finalize-agy-specialist-task."
|
|
1938
|
+
),
|
|
1939
|
+
}
|
|
1940
|
+
)
|
|
1941
|
+
|
|
1942
|
+
def is_authorized_template_definition(agent_name: str, parameters: dict[str, Any]) -> bool:
|
|
1943
|
+
markers = _PACKAGED_SPECIALIST_AGENT_TEMPLATE_MARKERS.get(agent_name)
|
|
1944
|
+
if not markers:
|
|
1945
|
+
return False
|
|
1946
|
+
prompt_parts = [
|
|
1947
|
+
str(parameters.get(field) or "")
|
|
1948
|
+
for field in _SUBAGENT_SYSTEM_PROMPT_PARAMETER_FIELDS
|
|
1949
|
+
if parameters.get(field)
|
|
1950
|
+
]
|
|
1951
|
+
prompt = "\n".join(prompt_parts)
|
|
1952
|
+
return bool(prompt) and all(marker in prompt for marker in markers)
|
|
1953
|
+
|
|
1954
|
+
def visit(value: Any) -> None:
|
|
1955
|
+
if isinstance(value, list):
|
|
1956
|
+
for item in value:
|
|
1957
|
+
visit(item)
|
|
1958
|
+
return
|
|
1959
|
+
if not isinstance(value, dict):
|
|
1960
|
+
return
|
|
1961
|
+
tool_name = _canonical_tool_name(_tool_name_from_record(value))
|
|
1962
|
+
parameters = _tool_parameters_from_record(value)
|
|
1963
|
+
if tool_name in _SUBAGENT_DEFINITION_TOOLS and parameters is not None:
|
|
1964
|
+
_, agent_name = _first_string_parameter(parameters, _AGENT_NAME_PARAMETER_FIELDS)
|
|
1965
|
+
if is_authorized_template_definition(agent_name, parameters):
|
|
1966
|
+
valid_packaged_definitions.add(agent_name)
|
|
1967
|
+
return
|
|
1968
|
+
append(agent_name)
|
|
1969
|
+
raw_text = str(value.get("content") or value.get("message") or value.get("text") or "")
|
|
1970
|
+
folded = raw_text.casefold()
|
|
1971
|
+
for agent_name in _PACKAGED_SPECIALIST_AGENTS:
|
|
1972
|
+
if f'subagent "{agent_name}" defined successfully' in folded:
|
|
1973
|
+
append(agent_name)
|
|
1974
|
+
for key in _TRANSCRIPT_CHILD_CONTAINER_KEYS:
|
|
1975
|
+
child = value.get(key)
|
|
1976
|
+
if isinstance(child, (dict, list)):
|
|
1977
|
+
visit(child)
|
|
1978
|
+
|
|
1979
|
+
visit(transcript)
|
|
1980
|
+
return findings
|
|
1981
|
+
|
|
1982
|
+
|
|
1983
|
+
def _agy_hidden_workspace_findings(transcript: Any) -> list[dict[str, Any]]:
|
|
1984
|
+
findings: list[dict[str, Any]] = []
|
|
1985
|
+
|
|
1986
|
+
def visit(value: Any) -> None:
|
|
1987
|
+
if isinstance(value, str):
|
|
1988
|
+
if _AGY_HIDDEN_WORKSPACE_RE.search(value):
|
|
1989
|
+
findings.append(
|
|
1990
|
+
{
|
|
1991
|
+
"code": WORKSPACE_ADD_DIR_HIDDEN_IGNORED,
|
|
1992
|
+
"severity": "high",
|
|
1993
|
+
"tool_name": "add_workspace_folder",
|
|
1994
|
+
"bad_param": "path",
|
|
1995
|
+
"message": "AGY ignored an --add-dir/workspace folder because the path is hidden.",
|
|
1996
|
+
"next_action": (
|
|
1997
|
+
"Preparar o vault de experimento em diretório visível ao AGY e repetir a rodada; "
|
|
1998
|
+
"não contorne a falha lendo e colando conteúdo bruto no subagente."
|
|
1999
|
+
),
|
|
2000
|
+
}
|
|
2001
|
+
)
|
|
2002
|
+
return
|
|
2003
|
+
if isinstance(value, list):
|
|
2004
|
+
for item in value:
|
|
2005
|
+
visit(item)
|
|
2006
|
+
return
|
|
2007
|
+
if isinstance(value, dict):
|
|
2008
|
+
for item in value.values():
|
|
2009
|
+
visit(item)
|
|
2010
|
+
|
|
2011
|
+
visit(transcript)
|
|
2012
|
+
return findings
|
|
2013
|
+
|
|
2014
|
+
|
|
2015
|
+
def _final_response_file_uri_paths(transcript: Any) -> list[str]:
|
|
2016
|
+
paths: list[str] = []
|
|
2017
|
+
|
|
2018
|
+
def visit(value: Any) -> None:
|
|
2019
|
+
if isinstance(value, list):
|
|
2020
|
+
for item in value:
|
|
2021
|
+
visit(item)
|
|
2022
|
+
return
|
|
2023
|
+
if not isinstance(value, dict):
|
|
2024
|
+
return
|
|
2025
|
+
event_type = str(value.get("type") or "").upper()
|
|
2026
|
+
if event_type == "PLANNER_RESPONSE":
|
|
2027
|
+
for field in ("content", "text", "message", "response"):
|
|
2028
|
+
raw = value.get(field)
|
|
2029
|
+
if isinstance(raw, str):
|
|
2030
|
+
paths.extend(_file_uri_paths_in_text(raw))
|
|
2031
|
+
for key in _TRANSCRIPT_CHILD_CONTAINER_KEYS:
|
|
2032
|
+
child = value.get(key)
|
|
2033
|
+
if isinstance(child, (dict, list)):
|
|
2034
|
+
visit(child)
|
|
2035
|
+
|
|
2036
|
+
visit(transcript)
|
|
2037
|
+
return paths
|
|
2038
|
+
|
|
2039
|
+
|
|
2040
|
+
def _file_uri_paths_in_text(text: str) -> list[str]:
|
|
2041
|
+
paths: list[str] = []
|
|
2042
|
+
for match in re.finditer(r"file://(?P<path>/[^\s)\]`>\"']+)", text):
|
|
2043
|
+
raw_path = unquote(match.group("path")).rstrip(".,;")
|
|
2044
|
+
if raw_path and raw_path not in paths:
|
|
2045
|
+
paths.append(raw_path)
|
|
2046
|
+
return paths
|
|
2047
|
+
|
|
2048
|
+
|
|
2049
|
+
def _workflow_artifact_names_in_command(command: str) -> list[str]:
|
|
2050
|
+
return sorted(name for name in _WORKFLOW_ARTIFACT_SCRATCH_NAMES if name in command)
|
|
2051
|
+
|
|
2052
|
+
|
|
2053
|
+
def _has_shell_stdout_redirect(command: str) -> bool:
|
|
2054
|
+
quote = ""
|
|
2055
|
+
escaped = False
|
|
2056
|
+
index = 0
|
|
2057
|
+
while index < len(command):
|
|
2058
|
+
char = command[index]
|
|
2059
|
+
if escaped:
|
|
2060
|
+
escaped = False
|
|
2061
|
+
index += 1
|
|
2062
|
+
continue
|
|
2063
|
+
if char == "\\":
|
|
2064
|
+
escaped = True
|
|
2065
|
+
index += 1
|
|
2066
|
+
continue
|
|
2067
|
+
if quote:
|
|
2068
|
+
if char == quote:
|
|
2069
|
+
quote = ""
|
|
2070
|
+
index += 1
|
|
2071
|
+
continue
|
|
2072
|
+
if char in {"'", '"'}:
|
|
2073
|
+
quote = char
|
|
2074
|
+
index += 1
|
|
2075
|
+
continue
|
|
2076
|
+
if char == ">":
|
|
2077
|
+
return True
|
|
2078
|
+
index += 1
|
|
2079
|
+
return bool(re.search(r"\b(?:Out-File|Set-Content)\b", command))
|
|
2080
|
+
|
|
2081
|
+
|
|
2082
|
+
def _workflow_artifact_write_paths(parameters: JsonObject) -> list[tuple[str, str]]:
|
|
2083
|
+
paths: list[tuple[str, str]] = []
|
|
2084
|
+
for field in _WORKFLOW_ARTIFACT_PATH_FIELDS:
|
|
2085
|
+
value = parameters.get(field)
|
|
2086
|
+
if isinstance(value, str) and value.strip():
|
|
2087
|
+
paths.append((field, value.strip()))
|
|
2088
|
+
return paths
|
|
2089
|
+
|
|
2090
|
+
|
|
2091
|
+
def _shell_command_text(parameters: dict[str, Any]) -> str:
|
|
2092
|
+
for field in _SHELL_COMMAND_PARAMETER_FIELDS:
|
|
2093
|
+
value = parameters.get(field)
|
|
2094
|
+
if isinstance(value, str) and value.strip():
|
|
2095
|
+
return _unwrap_tool_command_line(value.strip())
|
|
2096
|
+
return ""
|
|
2097
|
+
|
|
2098
|
+
|
|
2099
|
+
def _unwrap_tool_command_line(command: str) -> str:
|
|
2100
|
+
if len(command) < 2 or command[0] != command[-1] or command[0] not in {"'", '"'}:
|
|
2101
|
+
return command
|
|
2102
|
+
quote = command[0]
|
|
2103
|
+
unwrapped = command[1:-1]
|
|
2104
|
+
if quote == '"':
|
|
2105
|
+
return unwrapped.replace('\\"', '"')
|
|
2106
|
+
return unwrapped.replace("\\'", "'")
|
|
2107
|
+
|
|
2108
|
+
|
|
2109
|
+
def _transcript_contains(transcript: Any, needle: str) -> bool:
|
|
2110
|
+
needle = needle.lower()
|
|
2111
|
+
|
|
2112
|
+
def visit(value: Any) -> bool:
|
|
2113
|
+
if isinstance(value, str):
|
|
2114
|
+
return needle in value.lower()
|
|
2115
|
+
if isinstance(value, list):
|
|
2116
|
+
return any(visit(item) for item in value)
|
|
2117
|
+
if isinstance(value, dict):
|
|
2118
|
+
return any(visit(str(key)) or visit(item) for key, item in value.items())
|
|
2119
|
+
return False
|
|
2120
|
+
|
|
2121
|
+
return visit(transcript)
|
|
2122
|
+
|
|
2123
|
+
|
|
2124
|
+
def _first_parameter_containing(parameters: dict[str, Any], needle: str) -> tuple[str, str]:
|
|
2125
|
+
folded_needle = needle.casefold()
|
|
2126
|
+
for field, value in parameters.items():
|
|
2127
|
+
if not isinstance(value, str):
|
|
2128
|
+
continue
|
|
2129
|
+
if folded_needle in value.casefold():
|
|
2130
|
+
return field, value
|
|
2131
|
+
return "", ""
|
|
2132
|
+
|
|
2133
|
+
|
|
2134
|
+
def _normalized_operational_path(value: str) -> str:
|
|
2135
|
+
return value.replace("\\", "/").casefold()
|
|
2136
|
+
|
|
2137
|
+
|
|
2138
|
+
def _looks_like_workflow_artifact_path(path: str) -> bool:
|
|
2139
|
+
normalized = path.replace("\\", "/")
|
|
2140
|
+
name = normalized.rsplit("/", 1)[-1]
|
|
2141
|
+
if not name.endswith(".json"):
|
|
2142
|
+
return False
|
|
2143
|
+
return "/runs/" in normalized and bool(_WORKFLOW_ARTIFACT_NAME_RE.search(name))
|
|
2144
|
+
|
|
2145
|
+
|
|
2146
|
+
def _looks_like_style_rewrite_parent_output_path(path: str) -> bool:
|
|
2147
|
+
normalized = _normalized_operational_path(path)
|
|
2148
|
+
if "/tmp/agent-work/fix-wiki/style-rewrite-" not in normalized:
|
|
2149
|
+
return False
|
|
2150
|
+
return normalized.endswith(
|
|
2151
|
+
(
|
|
2152
|
+
".rewrite.md",
|
|
2153
|
+
".rewrite.md.attestation.json",
|
|
2154
|
+
".rewrite.md.receipt.json",
|
|
2155
|
+
".style-rewrite-output.json",
|
|
2156
|
+
)
|
|
2157
|
+
)
|
|
2158
|
+
|
|
2159
|
+
|
|
2160
|
+
def _looks_like_chats_raw_path(path: str) -> bool:
|
|
2161
|
+
return "/chats_raw/" in _normalized_operational_path(path)
|
|
2162
|
+
|
|
2163
|
+
|
|
2164
|
+
def _looks_like_process_chats_generated_artifact_path(path: str) -> bool:
|
|
2165
|
+
normalized = _normalized_operational_path(path)
|
|
2166
|
+
if "/process-chats/" not in normalized:
|
|
2167
|
+
return False
|
|
2168
|
+
return normalized.endswith(_PROCESS_CHATS_ARTIFACT_SUFFIXES)
|
|
2169
|
+
|
|
2170
|
+
|
|
2171
|
+
def _is_process_chats_specialist_invocation(tool_name: str) -> bool:
|
|
2172
|
+
return tool_name in {"invoke_agent", "invoke_subagent", "send_message"}
|
|
2173
|
+
|
|
2174
|
+
|
|
2175
|
+
def _looks_like_reported_workflow_artifact_path(path: str) -> bool:
|
|
2176
|
+
normalized = path.replace("\\", "/")
|
|
2177
|
+
name = normalized.rsplit("/", 1)[-1]
|
|
2178
|
+
return name in _WORKFLOW_ARTIFACT_SCRATCH_NAMES or _looks_like_workflow_artifact_path(path)
|
|
2179
|
+
|
|
2180
|
+
|
|
2181
|
+
def _first_unquoted_shell_chain_operator(command: str) -> str:
|
|
2182
|
+
quote = ""
|
|
2183
|
+
escaped = False
|
|
2184
|
+
index = 0
|
|
2185
|
+
while index < len(command):
|
|
2186
|
+
char = command[index]
|
|
2187
|
+
if escaped:
|
|
2188
|
+
escaped = False
|
|
2189
|
+
index += 1
|
|
2190
|
+
continue
|
|
2191
|
+
if char == "\\":
|
|
2192
|
+
escaped = True
|
|
2193
|
+
index += 1
|
|
2194
|
+
continue
|
|
2195
|
+
if quote:
|
|
2196
|
+
if char == quote:
|
|
2197
|
+
quote = ""
|
|
2198
|
+
index += 1
|
|
2199
|
+
continue
|
|
2200
|
+
if char in {"'", '"'}:
|
|
2201
|
+
quote = char
|
|
2202
|
+
index += 1
|
|
2203
|
+
continue
|
|
2204
|
+
if command.startswith("&&", index):
|
|
2205
|
+
return "&&"
|
|
2206
|
+
if command.startswith("||", index):
|
|
2207
|
+
return "||"
|
|
2208
|
+
if char == ";":
|
|
2209
|
+
return ";"
|
|
2210
|
+
if char == "\n" and command[:index].strip() and command[index + 1 :].strip():
|
|
2211
|
+
return "newline"
|
|
2212
|
+
index += 1
|
|
2213
|
+
return ""
|
|
2214
|
+
|
|
2215
|
+
|
|
2216
|
+
def _iter_agent_tool_calls(node: Any) -> list[tuple[str, dict[str, Any]]]:
|
|
2217
|
+
calls: list[tuple[str, dict[str, Any]]] = []
|
|
2218
|
+
|
|
2219
|
+
def visit(value: Any) -> None:
|
|
2220
|
+
if isinstance(value, list):
|
|
2221
|
+
for item in value:
|
|
2222
|
+
visit(item)
|
|
2223
|
+
return
|
|
2224
|
+
if not isinstance(value, dict):
|
|
2225
|
+
return
|
|
2226
|
+
tool_name = _tool_name_from_record(value)
|
|
2227
|
+
parameters = _tool_parameters_from_record(value)
|
|
2228
|
+
if tool_name and parameters is not None:
|
|
2229
|
+
calls.append((tool_name, parameters))
|
|
2230
|
+
for key in _TRANSCRIPT_CHILD_CONTAINER_KEYS:
|
|
2231
|
+
child = value.get(key)
|
|
2232
|
+
if isinstance(child, (dict, list)):
|
|
2233
|
+
visit(child)
|
|
2234
|
+
|
|
2235
|
+
visit(node)
|
|
2236
|
+
return calls
|
|
2237
|
+
|
|
2238
|
+
|
|
2239
|
+
def _iter_tool_call_batches(node: Any) -> list[list[tuple[str, dict[str, Any]]]]:
|
|
2240
|
+
batches: list[list[tuple[str, dict[str, Any]]]] = []
|
|
2241
|
+
|
|
2242
|
+
def visit(value: Any) -> None:
|
|
2243
|
+
if isinstance(value, list):
|
|
2244
|
+
for item in value:
|
|
2245
|
+
visit(item)
|
|
2246
|
+
return
|
|
2247
|
+
if not isinstance(value, dict):
|
|
2248
|
+
return
|
|
2249
|
+
for key in ("tool_calls", "toolCalls", "calls"):
|
|
2250
|
+
raw_batch = value.get(key)
|
|
2251
|
+
if not isinstance(raw_batch, list):
|
|
2252
|
+
continue
|
|
2253
|
+
batch: list[tuple[str, dict[str, Any]]] = []
|
|
2254
|
+
for item in raw_batch:
|
|
2255
|
+
if not isinstance(item, dict):
|
|
2256
|
+
continue
|
|
2257
|
+
tool_name = _tool_name_from_record(item)
|
|
2258
|
+
parameters = _tool_parameters_from_record(item)
|
|
2259
|
+
if tool_name and parameters is not None:
|
|
2260
|
+
batch.append((tool_name, parameters))
|
|
2261
|
+
if batch:
|
|
2262
|
+
batches.append(batch)
|
|
2263
|
+
for key in _TRANSCRIPT_CHILD_CONTAINER_KEYS:
|
|
2264
|
+
child = value.get(key)
|
|
2265
|
+
if isinstance(child, (dict, list)):
|
|
2266
|
+
visit(child)
|
|
2267
|
+
|
|
2268
|
+
visit(node)
|
|
2269
|
+
return batches
|
|
2270
|
+
|
|
2271
|
+
|
|
2272
|
+
def _tool_name_from_record(record: dict[str, Any]) -> str:
|
|
2273
|
+
for key in ("tool_name", "toolName", "name", "tool", "recipient_name", "recipient"):
|
|
2274
|
+
value = record.get(key)
|
|
2275
|
+
if isinstance(value, str) and value.strip():
|
|
2276
|
+
return value.strip()
|
|
2277
|
+
return ""
|
|
2278
|
+
|
|
2279
|
+
|
|
2280
|
+
def _tool_parameters_from_record(record: dict[str, Any]) -> dict[str, Any] | None:
|
|
2281
|
+
for key in ("parameters", "args", "arguments", "tool_input", "toolInput", "input"):
|
|
2282
|
+
value = record.get(key)
|
|
2283
|
+
if isinstance(value, dict):
|
|
2284
|
+
return value
|
|
2285
|
+
return None
|
|
2286
|
+
|
|
2287
|
+
|
|
2288
|
+
def _canonical_tool_name(tool_name: str) -> str:
|
|
2289
|
+
normalized = str(tool_name or "").replace("functions.", "").replace("tools.", "")
|
|
2290
|
+
normalized = normalized.replace(" ", "_").lower()
|
|
2291
|
+
if normalized in _RUN_SHELL_TOOL_ALIASES:
|
|
2292
|
+
return "run_shell_command"
|
|
2293
|
+
return normalized
|